Setting the file. One moment.
Detect Wp Project · Wp Project Triage · WordPress/agent-skills · Skills Docs
ContentsBack to the top of the page This file
Number 16.2
Position 2 of 2
Type JavaScript
Size 19 KB
Lines 592 scripts/ detect_wp_project.mjs
JavaScript · 592 lines · 19 KB
10 "vendor" ,
11 "dist" ,
12 "build" ,
13 "coverage" ,
14 ".next" ,
15 ".turbo" ,
16 ]);
17
18 function statSafe ( p ) {
19 try {
20 return fs. statSync (p);
21 } catch {
22 return null ;
23 }
24 }
25
26 function readFileSafe ( p , maxBytes = 256 * 1024 ) {
27 try {
28 const buf = fs. readFileSync (p);
29 if (buf.byteLength > maxBytes) return buf. subarray ( 0 , maxBytes). toString ( "utf8" );
30 return buf. toString ( "utf8" );
31 } catch {
32 return null ;
33 }
34 }
35
36 function scanForTokens ( repoRoot , { tokens , exts , maxFiles = 2500 , maxDepth = 8 }) {
37 const loweredTokens = tokens. map (( t ) => t. toLowerCase ());
38 const matches = new Map ();
39
40 const { results : files , truncated } = findFilesRecursive (
41 repoRoot,
42 ( p ) => {
43 const ext = path. extname (p). toLowerCase ();
44 return exts. includes (ext);
45 },
46 { maxFiles, maxDepth }
47 );
48
49 for ( const filePath of files) {
50 const contents = readFileSafe (filePath, 128 * 1024 );
51 if ( ! contents) continue ;
52 const haystack = contents. toLowerCase ();
53
54 for ( let i = 0 ; i < loweredTokens. length ; i += 1 ) {
55 const token = loweredTokens[i];
56 if (matches. has (token)) continue ;
57 if (haystack. includes (token)) matches. set (token, path. relative (repoRoot, filePath));
58 }
59 if (matches.size === loweredTokens. length ) break ;
60 }
61
62 return {
63 truncated,
64 matches: Object. fromEntries ([ ... matches. entries ()]),
65 };
66 }
67
68 function existsFile ( p ) {
69 const st = statSafe (p);
70 return Boolean (st && st. isFile ());
71 }
72
73 function existsDir ( p ) {
74 const st = statSafe (p);
75 return Boolean (st && st. isDirectory ());
76 }
77
78 function detectPackageManager ( repoRoot ) {
79 const hasPnpm = existsFile (path. join (repoRoot, "pnpm-lock.yaml" ));
80 const hasYarn = existsFile (path. join (repoRoot, "yarn.lock" ));
81 const hasNpm = existsFile (path. join (repoRoot, "package-lock.json" ));
82 const hasBun = existsFile (path. join (repoRoot, "bun.lockb" )) || existsFile (path. join (repoRoot, "bun.lock" ));
83 if (hasPnpm) return "pnpm" ;
84 if (hasYarn) return "yarn" ;
85 if (hasBun) return "bun" ;
86 if (hasNpm) return "npm" ;
87 return null ;
88 }
89
90 function findFilesRecursive ( repoRoot , predicate , { maxFiles = 6000 , maxDepth = 8 } = {}) {
91 const results = [];
92 const queue = [{ dir: repoRoot, depth: 0 }];
93 let visited = 0 ;
94
95 while (queue. length > 0 ) {
96 const { dir , depth } = queue. shift ();
97 if (depth > maxDepth) continue ;
98
99 let entries;
100 try {
101 entries = fs. readdirSync (dir, { withFileTypes: true });
102 } catch {
103 continue ;
104 }
105
106 for ( const ent of entries) {
107 const fullPath = path. join (dir, ent.name);
108 if (ent. isDirectory ()) {
109 if ( DEFAULT_IGNORES . has (ent.name)) continue ;
110 queue. push ({ dir: fullPath, depth: depth + 1 });
111 continue ;
112 }
113 if ( ! ent. isFile ()) continue ;
114
115 visited += 1 ;
116 if (visited > maxFiles) return { results, truncated: true };
117 if ( predicate (fullPath)) results. push (fullPath);
118 }
119 }
120
121 return { results, truncated: false };
122 }
123
124 function detectPluginHeaderFromPhpFile ( filePath ) {
125 const contents = readFileSafe (filePath, 128 * 1024 );
126 if ( ! contents) return null ;
127 const headerMatch = contents. match ( / ^ \s * \* ? \s * Plugin Name: \s * ( . + ) \s *$ / im );
128 if ( ! headerMatch) return null ;
129 return headerMatch[ 1 ]. trim ();
130 }
131
132 function detectThemeHeaderFromStyleCss ( filePath ) {
133 const contents = readFileSafe (filePath, 128 * 1024 );
134 if ( ! contents) return null ;
135 const headerMatch = contents. match ( / ^ \s * \* ? \s * Theme Name: \s * ( . + ) \s *$ / im );
136 if ( ! headerMatch) return null ;
137 return headerMatch[ 1 ]. trim ();
138 }
139
140 function guessWpCoreVersionFromCheckout ( repoRoot ) {
141 const versionPhp = path. join (repoRoot, "wp-includes" , "version.php" );
142 if ( ! existsFile (versionPhp)) return { value: null , source: null };
143 const contents = readFileSafe (versionPhp, 64 * 1024 );
144 if ( ! contents) return { value: null , source: null };
145 const match = contents. match ( / \$ wp_version \s * = \s * '( [ ^ '] + )'/ );
146 if ( ! match) return { value: null , source: "wp-includes/version.php" };
147 return { value: match[ 1 ], source: "wp-includes/version.php" };
148 }
149
150 function guessGutenbergVersion ( repoRoot ) {
151 const gutenbergPackageJson = path. join (repoRoot, "packages" , "plugins" , "package.json" );
152 const rootPackageJson = path. join (repoRoot, "package.json" );
153
154 for ( const candidate of [gutenbergPackageJson, rootPackageJson]) {
155 if ( ! existsFile (candidate)) continue ;
156 const txt = readFileSafe (candidate);
157 if ( ! txt) continue ;
158 try {
159 const pkg = JSON . parse (txt);
160 if (pkg?.name === "@wordpress/plugins" && typeof pkg?.version === "string" ) {
161 return { value: pkg.version, source: path. relative (repoRoot, candidate) };
162 }
163 if (pkg?.name === "gutenberg" && typeof pkg?.version === "string" ) {
164 return { value: pkg.version, source: path. relative (repoRoot, candidate) };
165 }
166 } catch {
167 // ignore
168 }
169 }
170 return { value: null , source: null };
171 }
172
173 function parsePackageJson ( repoRoot ) {
174 const p = path. join (repoRoot, "package.json" );
175 if ( ! existsFile (p)) return null ;
176 const txt = readFileSafe (p);
177 if ( ! txt) return null ;
178 try {
179 return JSON . parse (txt);
180 } catch {
181 return null ;
182 }
183 }
184
185 function parseComposerJson ( repoRoot ) {
186 const p = path. join (repoRoot, "composer.json" );
187 if ( ! existsFile (p)) return null ;
188 const txt = readFileSafe (p);
189 if ( ! txt) return null ;
190 try {
191 return JSON . parse (txt);
192 } catch {
193 return null ;
194 }
195 }
196
197 function detectConfigConstants ( repoRoot ) {
198 const { results : configFiles } = findFilesRecursive (repoRoot, ( p ) => path. basename (p) === "wp-config.php" , {
199 maxFiles: 4000 ,
200 maxDepth: 4 ,
201 });
202 const configPath = configFiles[ 0 ] ?? null ;
203 if ( ! configPath) {
204 return { source: null , constants: {} };
205 }
206
207 const contents = readFileSafe (configPath, 256 * 1024 );
208 if ( ! contents) return { source: path. relative (repoRoot, configPath), constants: {} };
209
210 const c = contents;
211 const enabled = ( name ) =>
212 new RegExp ( `define \\ ( \\ s*['"]${ name }['"] \\ s*, \\ s*(true|1) \\ s* \\ )` , "i" ). test (c) ||
213 new RegExp ( ` \\ b${ name } \\ b \\ s*= \\ s*(true|1)` , "i" ). test (c);
214
215 const mentioned = ( name ) => new RegExp ( ` \\ b${ name } \\ b` , "i" ). test (c);
216
217 return {
218 source: path. relative (repoRoot, configPath),
219 constants: {
220 savequeriesMentioned: mentioned ( "SAVEQUERIES" ),
221 savequeriesEnabled: enabled ( "SAVEQUERIES" ),
222 wpDebugMentioned: mentioned ( "WP_DEBUG" ),
223 wpDebugEnabled: enabled ( "WP_DEBUG" ),
224 disableWpCronMentioned: mentioned ( "DISABLE_WP_CRON" ),
225 disableWpCronEnabled: enabled ( "DISABLE_WP_CRON" ),
226 },
227 };
228 }
229
230 function detectKinds ( repoRoot , signals ) {
231 const kinds = new Set ();
232
233 if (signals.isGutenbergRepo) kinds. add ( "gutenberg" );
234 if (signals.isWpCoreCheckout) kinds. add ( "wp-core" );
235 if (signals.hasWpContentDir) kinds. add ( "wp-site" );
236 if (signals.detectedThemeName) kinds. add (signals.isBlockTheme ? "wp-block-theme" : "wp-theme" );
237 if (signals.detectedPluginName) kinds. add (signals.isBlockPlugin ? "wp-block-plugin" : "wp-plugin" );
238 if (signals.hasMuPluginsDir) kinds. add ( "wp-mu-plugin" );
239
240 if (kinds.size === 0 ) kinds. add ( "unknown" );
241
242 const priority = [
243 "gutenberg" ,
244 "wp-core" ,
245 "wp-site" ,
246 "wp-block-theme" ,
247 "wp-block-plugin" ,
248 "wp-theme" ,
249 "wp-mu-plugin" ,
250 "wp-plugin" ,
251 "unknown" ,
252 ];
253 let primary = "unknown" ;
254 for ( const k of priority) {
255 if (kinds. has (k)) {
256 primary = k;
257 break ;
258 }
259 }
260
261 return { kind: [ ... kinds], primary };
262 }
263
264 function buildRecommendations ({ repoRoot , primaryKind , packageManager , packageJson , composerJson , tooling , signals }) {
265 const commands = [];
266 const notes = [];
267
268 if (tooling.node.hasPackageJson) {
269 const pm = packageManager ?? "npm" ;
270 const run = pm === "yarn" ? "yarn" : `${ pm } run` ;
271 const hasScript = ( name ) => Boolean (packageJson?.scripts && Object . prototype .hasOwnProperty. call (packageJson.scripts, name));
272 if ( hasScript ( "lint" )) commands. push ( `${ run } lint` );
273 if ( hasScript ( "test" )) commands. push ( `${ run } test` );
274 if ( hasScript ( "build" )) commands. push ( `${ run } build` );
275 if ( hasScript ( "start" )) commands. push ( `${ run } start` );
276 if (tooling.node.usesWordpressScripts) notes. push ( "Detected @wordpress/scripts usage; prefer its standard lint/build/test scripts." );
277 }
278
279 if (tooling.php.hasComposerJson) {
280 commands. push ( "composer install" );
281 if (tooling.php.phpunitXml. length > 0 ) commands. push ( "vendor/bin/phpunit" );
282 }
283
284 if (tooling.tests.hasWpEnv) notes. push ( "Detected wp-env; E2E workflows may rely on Docker." );
285 if (signals.scanTruncated) notes. push ( "Scan truncated due to file limit; some signals may be missing." );
286 if (primaryKind === "unknown" ) notes. push ( "Could not confidently classify repo; inspect root for plugin/theme headers or wp-content structure." );
287
288 return { commands, notes };
289 }
290
291 function main () {
292 const repoRoot = process. cwd ();
293
294 const wpContent = path. join (repoRoot, "wp-content" );
295 const pluginsDir = path. join (wpContent, "plugins" );
296 const muPluginsDir = path. join (wpContent, "mu-plugins" );
297 const themesDir = path. join (wpContent, "themes" );
298
299 const isWpCoreCheckout = existsFile (path. join (repoRoot, "wp-includes" , "version.php" ));
300 const isGutenbergRepo =
301 existsDir (path. join (repoRoot, "packages" )) &&
302 ( existsDir (path. join (repoRoot, "packages" , "block-editor" )) || existsDir (path. join (repoRoot, "packages" , "components" )));
303
304 const packageJson = parsePackageJson (repoRoot);
305 const composerJson = parseComposerJson (repoRoot);
306 const packageManager = detectPackageManager (repoRoot);
307
308 const usesWordpressScripts = Boolean (
309 packageJson?.devDependencies?.[ "@wordpress/scripts" ] ||
310 packageJson?.dependencies?.[ "@wordpress/scripts" ] ||
311 packageJson?.scripts?.build?. includes ( "wp-scripts" ) ||
312 packageJson?.scripts?.start?. includes ( "wp-scripts" ) ||
313 packageJson?.scripts?.test?. includes ( "wp-scripts" ) ||
314 packageJson?.scripts?.lint?. includes ( "wp-scripts" )
315 );
316
317 const pkgHasInteractivity = Boolean (
318 packageJson?.devDependencies?.[ "@wordpress/interactivity" ] || packageJson?.dependencies?.[ "@wordpress/interactivity" ]
319 );
320 const pkgHasAbilities = Boolean (
321 packageJson?.devDependencies?.[ "@wordpress/abilities" ] || packageJson?.dependencies?.[ "@wordpress/abilities" ]
322 );
323
324 const hasWpContentDir = existsDir (wpContent);
325 const hasPluginsDir = existsDir (pluginsDir);
326 const hasThemesDir = existsDir (themesDir);
327 const hasMuPluginsDir = existsDir (muPluginsDir);
328
329 const config = detectConfigConstants (repoRoot);
330
331 const pluginCandidates = [];
332 const themeCandidates = [];
333
334 // Root-level plugin/theme detection (common when repo root is the plugin/theme).
335 for ( const entry of fs. readdirSync (repoRoot, { withFileTypes: true })) {
336 if ( ! entry. isFile ()) continue ;
337 if (entry.name. toLowerCase (). endsWith ( ".php" )) pluginCandidates. push (path. join (repoRoot, entry.name));
338 if (entry.name === "style.css" ) themeCandidates. push (path. join (repoRoot, entry.name));
339 }
340
341 let detectedPluginName = null ;
342 for ( const phpFile of pluginCandidates) {
343 detectedPluginName = detectPluginHeaderFromPhpFile (phpFile);
344 if (detectedPluginName) break ;
345 }
346
347 let detectedThemeName = null ;
348 for ( const styleCss of themeCandidates) {
349 detectedThemeName = detectThemeHeaderFromStyleCss (styleCss);
350 if (detectedThemeName) break ;
351 }
352
353 const { results : blockJsonFiles , truncated : scanTruncated } = findFilesRecursive (
354 repoRoot,
355 ( p ) => path. basename (p) === "block.json" ,
356 { maxFiles: 6000 , maxDepth: 8 }
357 );
358 const { results : themeJsonFiles } = findFilesRecursive (repoRoot, ( p ) => path. basename (p) === "theme.json" , {
359 maxFiles: 6000 ,
360 maxDepth: 8 ,
361 });
362
363 const templatesDirCandidates = [
364 path. join (repoRoot, "templates" ),
365 path. join (repoRoot, "parts" ),
366 path. join (repoRoot, "patterns" ),
367 ];
368
369 const isBlockTheme = themeJsonFiles. length > 0 && templatesDirCandidates. some (( p ) => existsDir (p));
370 const isBlockPlugin = blockJsonFiles. length > 0 ;
371
372 const interactivityScan = scanForTokens (repoRoot, {
373 tokens: [ "data-wp-interactive" , "@wordpress/interactivity" , "viewScriptModule" ],
374 exts: [ ".php" , ".js" , ".ts" , ".tsx" , ".json" , ".html" ],
375 maxFiles: 2500 ,
376 maxDepth: 8 ,
377 });
378
379 const abilitiesScan = scanForTokens (repoRoot, {
380 tokens: [
381 "wp_register_ability(" ,
382 "wp_register_ability_category(" ,
383 "wp_abilities_api_init" ,
384 "wp_abilities_api_categories_init" ,
385 "wp-abilities/v1" ,
386 "@wordpress/abilities" ,
387 ],
388 exts: [ ".php" , ".js" , ".ts" , ".tsx" ],
389 maxFiles: 2500 ,
390 maxDepth: 8 ,
391 });
392
393 const innerBlocksScan = scanForTokens (repoRoot, {
394 tokens: [ "InnerBlocks" , "useInnerBlocksProps" , "InnerBlocks.Content" ],
395 exts: [ ".js" , ".ts" , ".tsx" ],
396 maxFiles: 2500 ,
397 maxDepth: 8 ,
398 });
399
400 const wpCliConfigBasenames = new Set ([
401 "wp-cli.yml" ,
402 "wp-cli.yaml" ,
403 "wp-cli.local.yml" ,
404 "wp-cli.local.yaml" ,
405 ".wp-cli.yml" ,
406 ".wp-cli.yaml" ,
407 ]);
408 const { results : wpCliConfigFiles , truncated : wpCliConfigTruncated } = findFilesRecursive (
409 repoRoot,
410 ( p ) => wpCliConfigBasenames. has (path. basename (p)),
411 { maxFiles: 6000 , maxDepth: 6 }
412 );
413
414 const composerRequire = composerJson?.require && typeof composerJson.require === "object" ? composerJson.require : {};
415 const composerRequireDev =
416 composerJson?.[ "require-dev" ] && typeof composerJson[ "require-dev" ] === "object" ? composerJson[ "require-dev" ] : {};
417 const composerHasWpCli = Boolean (
418 composerRequire[ "wp-cli/wp-cli" ] ||
419 composerRequireDev[ "wp-cli/wp-cli" ] ||
420 composerRequire[ "wp-cli/wp-cli-bundle" ] ||
421 composerRequireDev[ "wp-cli/wp-cli-bundle" ]
422 );
423
424 const wpCliTokenScan = scanForTokens (repoRoot, {
425 tokens: [
426 "wp search-replace" ,
427 "wp db export" ,
428 "wp db import" ,
429 "wp cron event" ,
430 "wp cache flush" ,
431 "wp rewrite flush" ,
432 "wp plugin update" ,
433 "wp theme update" ,
434 ],
435 exts: [ ".sh" , ".yml" , ".yaml" , ".js" , ".ts" , ".php" , ".json" ],
436 maxFiles: 2500 ,
437 maxDepth: 8 ,
438 });
439
440 const usesInteractivityApi = pkgHasInteractivity || Object. keys (interactivityScan.matches). length > 0 ;
441 const usesAbilitiesApi = pkgHasAbilities || Object. keys (abilitiesScan.matches). length > 0 ;
442 const usesInnerBlocks = Object. keys (innerBlocksScan.matches). length > 0 ;
443 const usesWpCli = composerHasWpCli || wpCliConfigFiles. length > 0 || Object. keys (wpCliTokenScan.matches). length > 0 ;
444
445 const wpContentRoot = path. join (repoRoot, "wp-content" );
446 const hasObjectCacheDropin = existsFile (path. join (wpContentRoot, "object-cache.php" ));
447 const hasAdvancedCacheDropin = existsFile (path. join (wpContentRoot, "advanced-cache.php" ));
448 const hasDbDropin = existsFile (path. join (wpContentRoot, "db.php" ));
449 const hasSunriseDropin = existsFile (path. join (wpContentRoot, "sunrise.php" ));
450 const hasQueryMonitorPlugin = existsDir (path. join (wpContentRoot, "plugins" , "query-monitor" ));
451 const hasPerformanceLabPlugin = existsDir (path. join (wpContentRoot, "plugins" , "performance-lab" ));
452
453 const phpunitXml = [];
454 for ( const candidate of [ "phpunit.xml" , "phpunit.xml.dist" ]) {
455 const full = path. join (repoRoot, candidate);
456 if ( existsFile (full)) phpunitXml. push (candidate);
457 }
458
459 const hasWpEnv =
460 existsFile (path. join (repoRoot, ".wp-env.json" )) ||
461 existsFile (path. join (repoRoot, ".wp-env.override.json" )) ||
462 Boolean (packageJson?.devDependencies?.[ "@wordpress/env" ] || packageJson?.dependencies?.[ "@wordpress/env" ]);
463
464 const hasPlaywright = Boolean (
465 packageJson?.devDependencies?.[ "@playwright/test" ] ||
466 packageJson?.dependencies?.[ "@playwright/test" ] ||
467 packageJson?.devDependencies?.[ "@wordpress/e2e-test-utils-playwright" ] ||
468 packageJson?.dependencies?.[ "@wordpress/e2e-test-utils-playwright" ]
469 );
470
471 const hasJest = Boolean (
472 packageJson?.devDependencies?.jest ||
473 packageJson?.dependencies?.jest ||
474 packageJson?.devDependencies?.[ "@wordpress/jest-preset-default" ] ||
475 packageJson?.dependencies?.[ "@wordpress/jest-preset-default" ]
476 );
477
478 const hasPhpUnit = phpunitXml. length > 0 || Boolean (composerJson?.requireDev?.phpunit || composerJson?.[ "require-dev" ]?.phpunit);
479
480 const signals = {
481 paths: {
482 repoRoot,
483 wpContent: hasWpContentDir ? wpContent : null ,
484 pluginsDir: hasPluginsDir ? pluginsDir : null ,
485 themesDir: hasThemesDir ? themesDir : null ,
486 muPluginsDir: hasMuPluginsDir ? muPluginsDir : null ,
487 },
488 isWpCoreCheckout,
489 isGutenbergRepo,
490 hasWpContentDir,
491 hasPluginsDir,
492 hasThemesDir,
493 hasMuPluginsDir,
494 detectedPluginName,
495 detectedThemeName,
496 isBlockPlugin,
497 isBlockTheme,
498 usesInteractivityApi,
499 usesAbilitiesApi,
500 usesInnerBlocks,
501 usesWpCli,
502 performanceHints: {
503 wpConfig: config.source,
504 constants: config.constants,
505 dropins: {
506 objectCache: hasObjectCacheDropin,
507 advancedCache: hasAdvancedCacheDropin,
508 db: hasDbDropin,
509 sunrise: hasSunriseDropin,
510 },
511 plugins: {
512 queryMonitor: hasQueryMonitorPlugin,
513 performanceLab: hasPerformanceLabPlugin,
514 },
515 },
516 interactivityHints: {
517 packageJson: pkgHasInteractivity,
518 matches: interactivityScan.matches,
519 scanTruncated: interactivityScan.truncated,
520 },
521 abilitiesHints: {
522 packageJson: pkgHasAbilities,
523 matches: abilitiesScan.matches,
524 scanTruncated: abilitiesScan.truncated,
525 },
526 innerBlocksHints: {
527 matches: innerBlocksScan.matches,
528 scanTruncated: innerBlocksScan.truncated,
529 },
530 wpCliHints: {
531 configFiles: wpCliConfigFiles. map (( p ) => path. relative (repoRoot, p)). slice ( 0 , 50 ),
532 configScanTruncated: wpCliConfigTruncated,
533 composerJson: composerHasWpCli,
534 matches: wpCliTokenScan.matches,
535 scanTruncated: wpCliTokenScan.truncated,
536 },
537 blockJsonFiles: blockJsonFiles. map (( p ) => path. relative (repoRoot, p)). slice ( 0 , 50 ),
538 themeJsonFiles: themeJsonFiles. map (( p ) => path. relative (repoRoot, p)). slice ( 0 , 50 ),
539 scanTruncated,
540 };
541
542 const { kind , primary } = detectKinds (repoRoot, signals);
543
544 const versions = {
545 wordpress: {
546 core: guessWpCoreVersionFromCheckout (repoRoot),
547 },
548 gutenberg: guessGutenbergVersion (repoRoot),
549 };
550
551 const tooling = {
552 php: {
553 hasComposerJson: existsFile (path. join (repoRoot, "composer.json" )),
554 hasVendorDir: existsDir (path. join (repoRoot, "vendor" )),
555 phpunitXml,
556 },
557 node: {
558 hasPackageJson: existsFile (path. join (repoRoot, "package.json" )),
559 packageManager,
560 usesWordpressScripts,
561 },
562 tests: {
563 hasPhpUnit,
564 hasWpEnv,
565 hasPlaywright,
566 hasJest,
567 },
568 };
569
570 const recommendations = buildRecommendations ({
571 repoRoot,
572 primaryKind: primary,
573 packageManager,
574 packageJson,
575 composerJson,
576 tooling,
577 signals,
578 });
579
580 const report = {
581 tool: { name: "detect_wp_project" , version: TOOL_VERSION },
582 project: { kind, primary, notes: [] },
583 signals,
584 tooling,
585 versions,
586 recommendations,
587 };
588
589 process.stdout. write ( `${ JSON . stringify ( report , null , 2 ) } \n ` );
590 }
591
592 main ();