wordpress-agent-kit 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.agents/skills/wp-bootstrap/SKILL.md +314 -0
  2. package/.agents/skills/wp-bootstrap/references/composer-setup.md +275 -0
  3. package/.agents/skills/wp-bootstrap/references/monorepo-patterns.md +184 -0
  4. package/.agents/skills/wp-bootstrap/scripts/bootstrap.sh +151 -0
  5. package/.agents/skills/wp-bootstrap/scripts/detect-structure.mjs +466 -0
  6. package/.agents/skills/wp-bootstrap/scripts/package-wp.sh +173 -0
  7. package/.agents/skills/wp-bootstrap/scripts/playground-start.sh +148 -0
  8. package/.agents/skills/wp-bootstrap/scripts/playground-verify.sh +165 -0
  9. package/.agents/skills/wp-bootstrap/scripts/setup-github.sh +417 -0
  10. package/{.github → .agents}/skills/wp-wpcli-and-ops/SKILL.md +11 -9
  11. package/.agents/skills/wp-wpengine/SKILL.md +462 -0
  12. package/.agents/skills/wp-wpengine/references/ci-gate.md +469 -0
  13. package/.agents/skills/wp-wpengine/references/github-actions-deploy.md +743 -0
  14. package/.agents/skills/wp-wpengine/scripts/ci-gate.sh +118 -0
  15. package/.agents/skills/wp-wpengine/scripts/wpe-check.sh +89 -0
  16. package/.agents/skills/wp-wpengine/scripts/wpe-preflight.sh +104 -0
  17. package/.github/agents/wp-architect.agent.md +1 -2
  18. package/.github/copilot-instructions.md +1 -1
  19. package/.github/instructions/wordpress-workflow.instructions.md +3 -3
  20. package/AGENTS.md +22 -10
  21. package/AGENTS.template.md +20 -10
  22. package/README.md +89 -85
  23. package/dist/cli.js +7 -1
  24. package/dist/commands/bootstrap.js +105 -0
  25. package/dist/commands/clean-skills.js +64 -0
  26. package/dist/commands/setup.js +6 -2
  27. package/dist/commands/sync-skills.js +3 -0
  28. package/dist/lib/api.js +165 -5
  29. package/dist/lib/bootstrap.js +352 -0
  30. package/dist/lib/installer.js +166 -2
  31. package/extensions/wp-agent-kit/index.ts +325 -10
  32. package/package.json +10 -14
  33. package/skills-custom/wp-bootstrap/SKILL.md +314 -0
  34. package/skills-custom/wp-bootstrap/references/composer-setup.md +275 -0
  35. package/skills-custom/wp-bootstrap/references/monorepo-patterns.md +184 -0
  36. package/skills-custom/wp-bootstrap/scripts/bootstrap.sh +151 -0
  37. package/skills-custom/wp-bootstrap/scripts/detect-structure.mjs +466 -0
  38. package/skills-custom/wp-bootstrap/scripts/package-wp.sh +173 -0
  39. package/skills-custom/wp-bootstrap/scripts/playground-start.sh +148 -0
  40. package/skills-custom/wp-bootstrap/scripts/playground-verify.sh +165 -0
  41. package/skills-custom/wp-bootstrap/scripts/setup-github.sh +417 -0
  42. package/skills-custom/wp-wpengine/SKILL.md +362 -27
  43. package/skills-custom/wp-wpengine/references/ci-gate.md +469 -0
  44. package/skills-custom/wp-wpengine/references/github-actions-deploy.md +743 -0
  45. package/skills-custom/wp-wpengine/scripts/ci-gate.sh +118 -0
  46. package/skills-custom/wp-wpengine/scripts/wpe-check.sh +89 -0
  47. package/skills-custom/wp-wpengine/scripts/wpe-preflight.sh +104 -0
  48. package/.github/skills/wp-wpengine/SKILL.md +0 -127
  49. package/.github/workflows/ci.yml +0 -44
  50. package/.husky/pre-commit +0 -7
  51. package/CLI_REVIEW.md +0 -250
  52. package/biome.json +0 -39
  53. /package/{.github → .agents}/skills/blueprint/SKILL.md +0 -0
  54. /package/{.github → .agents}/skills/wordpress-router/SKILL.md +0 -0
  55. /package/{.github → .agents}/skills/wordpress-router/references/decision-tree.md +0 -0
  56. /package/{.github → .agents}/skills/wp-abilities-api/SKILL.md +0 -0
  57. /package/{.github → .agents}/skills/wp-abilities-api/references/delegate-helper-pattern.md +0 -0
  58. /package/{.github → .agents}/skills/wp-abilities-api/references/domain-vs-projection.md +0 -0
  59. /package/{.github → .agents}/skills/wp-abilities-api/references/error-code-vocabulary.md +0 -0
  60. /package/{.github → .agents}/skills/wp-abilities-api/references/grouping-heuristic.md +0 -0
  61. /package/{.github → .agents}/skills/wp-abilities-api/references/input-schema-gotchas.md +0 -0
  62. /package/{.github → .agents}/skills/wp-abilities-api/references/php-registration.md +0 -0
  63. /package/{.github → .agents}/skills/wp-abilities-api/references/plugin-family-patterns.md +0 -0
  64. /package/{.github → .agents}/skills/wp-abilities-api/references/rest-api.md +0 -0
  65. /package/{.github → .agents}/skills/wp-abilities-api/references/shared-core-service.md +0 -0
  66. /package/{.github → .agents}/skills/wp-abilities-audit/SKILL.md +0 -0
  67. /package/{.github → .agents}/skills/wp-abilities-audit/references/audit-schema.md +0 -0
  68. /package/{.github → .agents}/skills/wp-abilities-audit/references/capability-gate-tracing.md +0 -0
  69. /package/{.github → .agents}/skills/wp-abilities-audit/references/controller-enumeration.md +0 -0
  70. /package/{.github → .agents}/skills/wp-abilities-verify/SKILL.md +0 -0
  71. /package/{.github → .agents}/skills/wp-abilities-verify/references/annotation-correctness.md +0 -0
  72. /package/{.github → .agents}/skills/wp-abilities-verify/references/audit-schema-validation.md +0 -0
  73. /package/{.github → .agents}/skills/wp-abilities-verify/references/permission-roundtrip.md +0 -0
  74. /package/{.github → .agents}/skills/wp-abilities-verify/references/runtime-harness.md +0 -0
  75. /package/{.github → .agents}/skills/wp-abilities-verify/references/schema-lints.md +0 -0
  76. /package/{.github → .agents}/skills/wp-abilities-verify/references/static-enumeration.md +0 -0
  77. /package/{.github → .agents}/skills/wp-block-development/SKILL.md +0 -0
  78. /package/{.github → .agents}/skills/wp-block-development/references/attributes-and-serialization.md +0 -0
  79. /package/{.github → .agents}/skills/wp-block-development/references/block-json.md +0 -0
  80. /package/{.github → .agents}/skills/wp-block-development/references/creating-new-blocks.md +0 -0
  81. /package/{.github → .agents}/skills/wp-block-development/references/debugging.md +0 -0
  82. /package/{.github → .agents}/skills/wp-block-development/references/deprecations.md +0 -0
  83. /package/{.github → .agents}/skills/wp-block-development/references/dynamic-rendering.md +0 -0
  84. /package/{.github → .agents}/skills/wp-block-development/references/inner-blocks.md +0 -0
  85. /package/{.github → .agents}/skills/wp-block-development/references/registration.md +0 -0
  86. /package/{.github → .agents}/skills/wp-block-development/references/supports-and-wrappers.md +0 -0
  87. /package/{.github → .agents}/skills/wp-block-development/references/tooling-and-testing.md +0 -0
  88. /package/{.github → .agents}/skills/wp-block-development/scripts/list_blocks.mjs +0 -0
  89. /package/{.github → .agents}/skills/wp-block-themes/SKILL.md +0 -0
  90. /package/{.github → .agents}/skills/wp-block-themes/references/creating-new-block-theme.md +0 -0
  91. /package/{.github → .agents}/skills/wp-block-themes/references/debugging.md +0 -0
  92. /package/{.github → .agents}/skills/wp-block-themes/references/patterns.md +0 -0
  93. /package/{.github → .agents}/skills/wp-block-themes/references/style-variations.md +0 -0
  94. /package/{.github → .agents}/skills/wp-block-themes/references/templates-and-parts.md +0 -0
  95. /package/{.github → .agents}/skills/wp-block-themes/references/theme-json.md +0 -0
  96. /package/{.github → .agents}/skills/wp-block-themes/scripts/detect_block_themes.mjs +0 -0
  97. /package/{.github → .agents}/skills/wp-interactivity-api/SKILL.md +0 -0
  98. /package/{.github → .agents}/skills/wp-interactivity-api/references/debugging.md +0 -0
  99. /package/{.github → .agents}/skills/wp-interactivity-api/references/directives-quickref.md +0 -0
  100. /package/{.github → .agents}/skills/wp-interactivity-api/references/server-side-rendering.md +0 -0
  101. /package/{.github → .agents}/skills/wp-performance/SKILL.md +0 -0
  102. /package/{.github → .agents}/skills/wp-performance/references/autoload-options.md +0 -0
  103. /package/{.github → .agents}/skills/wp-performance/references/cron.md +0 -0
  104. /package/{.github → .agents}/skills/wp-performance/references/database.md +0 -0
  105. /package/{.github → .agents}/skills/wp-performance/references/http-api.md +0 -0
  106. /package/{.github → .agents}/skills/wp-performance/references/measurement.md +0 -0
  107. /package/{.github → .agents}/skills/wp-performance/references/object-cache.md +0 -0
  108. /package/{.github → .agents}/skills/wp-performance/references/query-monitor-headless.md +0 -0
  109. /package/{.github → .agents}/skills/wp-performance/references/server-timing.md +0 -0
  110. /package/{.github → .agents}/skills/wp-performance/references/wp-cli-doctor.md +0 -0
  111. /package/{.github → .agents}/skills/wp-performance/references/wp-cli-profile.md +0 -0
  112. /package/{.github → .agents}/skills/wp-performance/scripts/perf_inspect.mjs +0 -0
  113. /package/{.github → .agents}/skills/wp-phpstan/SKILL.md +0 -0
  114. /package/{.github → .agents}/skills/wp-phpstan/references/configuration.md +0 -0
  115. /package/{.github → .agents}/skills/wp-phpstan/references/third-party-classes.md +0 -0
  116. /package/{.github → .agents}/skills/wp-phpstan/references/wordpress-annotations.md +0 -0
  117. /package/{.github → .agents}/skills/wp-phpstan/scripts/phpstan_inspect.mjs +0 -0
  118. /package/{.github → .agents}/skills/wp-playground/SKILL.md +0 -0
  119. /package/{.github → .agents}/skills/wp-playground/references/blueprints.md +0 -0
  120. /package/{.github → .agents}/skills/wp-playground/references/cli-commands.md +0 -0
  121. /package/{.github → .agents}/skills/wp-playground/references/debugging.md +0 -0
  122. /package/{.github → .agents}/skills/wp-playground/references/e2e-playwright.md +0 -0
  123. /package/{.github → .agents}/skills/wp-plugin-development/SKILL.md +0 -0
  124. /package/{.github → .agents}/skills/wp-plugin-development/references/data-and-cron.md +0 -0
  125. /package/{.github → .agents}/skills/wp-plugin-development/references/debugging.md +0 -0
  126. /package/{.github → .agents}/skills/wp-plugin-development/references/lifecycle.md +0 -0
  127. /package/{.github → .agents}/skills/wp-plugin-development/references/security.md +0 -0
  128. /package/{.github → .agents}/skills/wp-plugin-development/references/settings-api.md +0 -0
  129. /package/{.github → .agents}/skills/wp-plugin-development/references/structure.md +0 -0
  130. /package/{.github → .agents}/skills/wp-plugin-development/scripts/detect_plugins.mjs +0 -0
  131. /package/{.github → .agents}/skills/wp-plugin-directory-guidelines/SKILL.md +0 -0
  132. /package/{.github → .agents}/skills/wp-plugin-directory-guidelines/references/gpl-compliance.md +0 -0
  133. /package/{.github → .agents}/skills/wp-plugin-directory-guidelines/references/guideline-review-checklist.md +0 -0
  134. /package/{.github → .agents}/skills/wp-plugin-directory-guidelines/references/naming-rules.md +0 -0
  135. /package/{.github → .agents}/skills/wp-project-triage/SKILL.md +0 -0
  136. /package/{.github → .agents}/skills/wp-project-triage/references/triage.schema.json +0 -0
  137. /package/{.github → .agents}/skills/wp-project-triage/scripts/detect_wp_project.mjs +0 -0
  138. /package/{.github → .agents}/skills/wp-rest-api/SKILL.md +0 -0
  139. /package/{.github → .agents}/skills/wp-rest-api/references/authentication.md +0 -0
  140. /package/{.github → .agents}/skills/wp-rest-api/references/custom-content-types.md +0 -0
  141. /package/{.github → .agents}/skills/wp-rest-api/references/discovery-and-params.md +0 -0
  142. /package/{.github → .agents}/skills/wp-rest-api/references/responses-and-fields.md +0 -0
  143. /package/{.github → .agents}/skills/wp-rest-api/references/routes-and-endpoints.md +0 -0
  144. /package/{.github → .agents}/skills/wp-rest-api/references/schema.md +0 -0
  145. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/automation.md +0 -0
  146. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/cron-and-cache.md +0 -0
  147. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/debugging.md +0 -0
  148. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/multisite.md +0 -0
  149. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/packages-and-updates.md +0 -0
  150. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/safety.md +0 -0
  151. /package/{.github → .agents}/skills/wp-wpcli-and-ops/references/search-replace.md +0 -0
  152. /package/{.github → .agents}/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +0 -0
  153. /package/{.github → .agents}/skills/wpds/SKILL.md +0 -0
package/dist/lib/api.js CHANGED
@@ -267,21 +267,35 @@ export async function syncSkillsApi(options = {}) {
267
267
  // Merge custom skills (skills-custom/) — these are not part of the upstream
268
268
  // WordPress/agent-skills repo and survive upstream syncs.
269
269
  const customSkillsDir = path.join(PACKAGE_ROOT, 'skills-custom');
270
+ let customMerged = 0;
270
271
  if (fs.existsSync(customSkillsDir)) {
271
272
  for (const skillName of fs.readdirSync(customSkillsDir)) {
272
273
  const src = path.join(customSkillsDir, skillName);
273
274
  const dest = path.join(targetSkills, skillName);
274
275
  if (fs.statSync(src).isDirectory()) {
276
+ const isNew = !fs.existsSync(dest);
275
277
  fs.cpSync(src, dest, { recursive: true });
276
- skillsSynced++;
278
+ if (isNew) {
279
+ skillsSynced++;
280
+ }
281
+ customMerged++;
277
282
  }
278
283
  }
279
284
  }
280
- return { success: true, skillsSynced, method };
285
+ // Copy synced skills to the canonical .agents/skills/ directory
286
+ // (AgentSkills.io convention — the pi.skills path in package.json points here).
287
+ const canonicalSkills = path.join(PACKAGE_ROOT, '.agents', 'skills');
288
+ if (fs.existsSync(canonicalSkills)) {
289
+ fs.rmSync(canonicalSkills, { recursive: true, force: true });
290
+ }
291
+ fs.mkdirSync(path.join(PACKAGE_ROOT, '.agents'), { recursive: true });
292
+ fs.cpSync(targetSkills, canonicalSkills, { recursive: true });
293
+ return { success: true, skillsSynced, customMerged, method };
281
294
  });
282
295
  return formatter.success({
283
296
  targetDir,
284
297
  skillsSynced: result.skillsSynced,
298
+ customMerged: result.customMerged,
285
299
  sourceUrl: 'https://github.com/WordPress/agent-skills.git',
286
300
  ref,
287
301
  durationMs: Date.now() - startTime,
@@ -334,7 +348,13 @@ function dryRunSyncSkills(targetDir, ref) {
334
348
  actions.push({
335
349
  type: 'create',
336
350
  target: targetSkills,
337
- description: 'Install synced skills',
351
+ description: 'Install synced skills to .github/skills (sync buffer), then copy to .agents/skills (canonical)',
352
+ });
353
+ const canonicalSkills = path.join(targetDir, '.agents', 'skills');
354
+ actions.push({
355
+ type: 'create',
356
+ target: canonicalSkills,
357
+ description: 'Copy synced skills to .agents/skills (canonical, AgentSkills.io convention)',
338
358
  });
339
359
  return new OutputFormatter('json', 'sync-skills', '0.0.0').success({
340
360
  wouldExecute: true,
@@ -342,6 +362,7 @@ function dryRunSyncSkills(targetDir, ref) {
342
362
  summary: {
343
363
  targetDir,
344
364
  skillsSynced: 0,
365
+ customMerged: 0,
345
366
  sourceUrl: 'https://github.com/WordPress/agent-skills.git',
346
367
  ref,
347
368
  durationMs: 0,
@@ -356,9 +377,13 @@ export async function runTriageApi(options) {
356
377
  const formatter = new OutputFormatter('json', 'triage', '0.0.0');
357
378
  const { targetDir, platform = 'github' } = options;
358
379
  try {
359
- const platformFolder = getPlatformFolder(platform);
360
380
  const triageScriptPaths = [
361
- path.join(targetDir, platformFolder, 'skills/wp-project-triage/scripts/detect_wp_project.mjs'),
381
+ // Canonical location (AgentSkills.io convention)
382
+ path.join(targetDir, '.agents', 'skills/wp-project-triage/scripts/detect_wp_project.mjs'),
383
+ // Legacy platform-specific location
384
+ path.join(targetDir, getPlatformFolder(platform), 'skills/wp-project-triage/scripts/detect_wp_project.mjs'),
385
+ // Source repo
386
+ path.join(PACKAGE_ROOT, '.agents', 'skills/wp-project-triage/scripts/detect_wp_project.mjs'),
362
387
  path.join(PACKAGE_ROOT, 'vendor/wp-agent-skills/skills/wp-project-triage/scripts/detect_wp_project.mjs'),
363
388
  ];
364
389
  const triageScriptPath = triageScriptPaths.find((p) => fs.existsSync(p));
@@ -530,6 +555,141 @@ function getInstalledSummary(targetDir, platform) {
530
555
  }
531
556
  return summary;
532
557
  }
558
+ /**
559
+ * Detect and optionally remove orphaned skills from a target installation.
560
+ * Compares the skills in the target platform directory against the source kit
561
+ * (upstream .agents/skills + skills-custom/) and identifies skills that exist
562
+ * in the target but not in the source.
563
+ */
564
+ export async function cleanSkillsApi(options) {
565
+ const startTime = Date.now();
566
+ const formatter = new OutputFormatter('json', 'clean-skills', '0.0.0');
567
+ const { targetDir, platform, dryRun = false, remove = false } = options;
568
+ try {
569
+ // Canonical skills come from .agents/skills/ (AgentSkills.io convention)
570
+ // with skills-custom/ as supplemental source for custom skills.
571
+ const canonicalSkillsDir = path.join(PACKAGE_ROOT, '.agents', 'skills');
572
+ const customSkillsDir = path.join(PACKAGE_ROOT, 'skills-custom');
573
+ const targetSkillsDir = path.join(targetDir, '.agents', 'skills');
574
+ // Build set of canonical skill names (upstream + custom)
575
+ const canonicalSkills = new Set();
576
+ if (fs.existsSync(canonicalSkillsDir)) {
577
+ for (const entry of fs.readdirSync(canonicalSkillsDir)) {
578
+ const entryPath = path.join(canonicalSkillsDir, entry);
579
+ if (fs.statSync(entryPath).isDirectory()) {
580
+ canonicalSkills.add(entry);
581
+ }
582
+ }
583
+ }
584
+ if (fs.existsSync(customSkillsDir)) {
585
+ for (const entry of fs.readdirSync(customSkillsDir)) {
586
+ const entryPath = path.join(customSkillsDir, entry);
587
+ if (fs.statSync(entryPath).isDirectory()) {
588
+ canonicalSkills.add(entry);
589
+ }
590
+ }
591
+ }
592
+ // Find orphaned skills in target .agents/skills/
593
+ const orphanedSkills = [];
594
+ if (fs.existsSync(targetSkillsDir)) {
595
+ for (const entry of fs.readdirSync(targetSkillsDir)) {
596
+ const entryPath = path.join(targetSkillsDir, entry);
597
+ if (fs.statSync(entryPath).isDirectory() && !canonicalSkills.has(entry)) {
598
+ orphanedSkills.push(entry);
599
+ }
600
+ }
601
+ }
602
+ // Detect legacy skill directories (platform-specific skills/ dirs)
603
+ const platformFolder = getPlatformFolder(platform);
604
+ const legacySkillsDir = path.join(targetDir, platformFolder, 'skills');
605
+ const legacySkillDirs = [];
606
+ if (fs.existsSync(legacySkillsDir)) {
607
+ legacySkillDirs.push(legacySkillsDir);
608
+ }
609
+ // Also check .github/skills for github platform (common legacy location)
610
+ if (platform !== 'github') {
611
+ const githubSkills = path.join(targetDir, '.github', 'skills');
612
+ if (fs.existsSync(githubSkills)) {
613
+ legacySkillDirs.push(githubSkills);
614
+ }
615
+ }
616
+ // Migrate legacy skills to .agents/skills/ and remove legacy dirs
617
+ const migratedSkills = [];
618
+ if (remove && !dryRun && legacySkillDirs.length > 0) {
619
+ for (const legacyDir of legacySkillDirs) {
620
+ for (const entry of fs.readdirSync(legacyDir)) {
621
+ const entryPath = path.join(legacyDir, entry);
622
+ if (!fs.statSync(entryPath).isDirectory())
623
+ continue;
624
+ const destPath = path.join(targetSkillsDir, entry);
625
+ if (!fs.existsSync(destPath)) {
626
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
627
+ fs.cpSync(entryPath, destPath, { recursive: true });
628
+ migratedSkills.push(entry);
629
+ }
630
+ }
631
+ // Remove the legacy skills directory
632
+ fs.rmSync(legacyDir, { recursive: true, force: true });
633
+ }
634
+ }
635
+ // Remove orphans if requested
636
+ const removedSkills = [];
637
+ if (remove && !dryRun) {
638
+ for (const orphan of orphanedSkills) {
639
+ const orphanPath = path.join(targetSkillsDir, orphan);
640
+ fs.rmSync(orphanPath, { recursive: true, force: true });
641
+ removedSkills.push(orphan);
642
+ }
643
+ }
644
+ const _durationMs = Date.now() - startTime;
645
+ if (dryRun) {
646
+ const actions = orphanedSkills.map((s) => ({
647
+ type: 'delete',
648
+ target: path.join('.agents', 'skills', s),
649
+ description: `Remove orphaned skill: ${s}`,
650
+ }));
651
+ for (const legacyDir of legacySkillDirs) {
652
+ actions.push({
653
+ type: 'delete',
654
+ target: legacyDir,
655
+ description: `Migrate and remove legacy skill directory: ${path.relative(targetDir, legacyDir)}`,
656
+ });
657
+ }
658
+ return formatter.success({
659
+ wouldExecute: true,
660
+ actions,
661
+ summary: {
662
+ targetDir,
663
+ platform,
664
+ orphanedSkills,
665
+ removedSkills: [],
666
+ legacySkillDirs,
667
+ migratedSkills: [],
668
+ dryRun: true,
669
+ },
670
+ });
671
+ }
672
+ return formatter.success({
673
+ targetDir,
674
+ platform,
675
+ orphanedSkills,
676
+ removedSkills,
677
+ legacySkillDirs,
678
+ migratedSkills,
679
+ dryRun: false,
680
+ });
681
+ }
682
+ catch (error) {
683
+ const err = error;
684
+ return formatter.fail({
685
+ code: err.code || 'CLEAN_FAILED',
686
+ message: err.message || 'Clean failed',
687
+ exitCode: err.exitCode ?? ExitCode.ERROR,
688
+ details: { platform, targetDir },
689
+ });
690
+ }
691
+ }
533
692
  export { ExitCode } from '../utils/exit-codes.js';
534
693
  export { OutputFormatter, createFormatter, parseOutputFormat } from '../utils/output.js';
535
694
  export { computeChanges, isKitInstalled, loadManifest, updateKit } from './updater.js';
695
+ export { bootstrapApi } from './bootstrap.js';
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Bootstrap API — detects WordPress project structure and scaffolds
3
+ * the full wp-agent-kit system (agent kit, Composer, WP-CLI, Playground,
4
+ * WP Engine CI/CD, git hooks). Supports single plugins/themes and monorepos.
5
+ */
6
+ import { spawnSync } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { ExitCode } from '../utils/exit-codes.js';
10
+ import { OutputFormatter } from '../utils/output.js';
11
+ import { PACKAGE_ROOT } from '../utils/paths.js';
12
+ import { installKit } from './installer.js';
13
+ // ── Detection ─────────────────────────────────────────────────────────────
14
+ /**
15
+ * Run the detect-structure.mjs probe script and return the parsed result.
16
+ */
17
+ export function detectStructure(targetDir) {
18
+ const detectScript = path.join(PACKAGE_ROOT, '.agents', 'skills', 'wp-bootstrap', 'scripts', 'detect-structure.mjs');
19
+ if (!fs.existsSync(detectScript)) {
20
+ // Fallback: minimal detection without the full script
21
+ return minimalDetect(targetDir);
22
+ }
23
+ const result = spawnSync('node', [detectScript, targetDir], {
24
+ encoding: 'utf-8',
25
+ cwd: targetDir,
26
+ });
27
+ if (result.status !== 0) {
28
+ return minimalDetect(targetDir);
29
+ }
30
+ try {
31
+ return JSON.parse(result.stdout.trim());
32
+ }
33
+ catch {
34
+ return minimalDetect(targetDir);
35
+ }
36
+ }
37
+ /** Minimal structure detection without the full probe script */
38
+ function minimalDetect(targetDir) {
39
+ const exists = (...parts) => fs.existsSync(path.join(targetDir, ...parts));
40
+ const pm = exists('pnpm-lock.yaml')
41
+ ? 'pnpm'
42
+ : exists('yarn.lock')
43
+ ? 'yarn'
44
+ : exists('package.json')
45
+ ? 'npm'
46
+ : null;
47
+ return {
48
+ repoRoot: targetDir,
49
+ isMonorepo: false,
50
+ packageManager: pm,
51
+ jsWorkspaces: [],
52
+ wpPackages: [],
53
+ wpRoot: exists('wp-config.php') ? '.' : null,
54
+ wpRootExists: exists('wp-config.php'),
55
+ playgroundOnly: !exists('wp-config.php'),
56
+ wpeRemotes: [],
57
+ hasWpeRemote: false,
58
+ php: {
59
+ hasComposer: exists('composer.json'),
60
+ hasPhpcs: exists('vendor/bin/phpcs'),
61
+ hasWpcs: false,
62
+ hasPhpstan: exists('vendor/bin/phpstan'),
63
+ hasPhpstanWp: false,
64
+ hasPest: false,
65
+ phpcsConfig: exists('phpcs.xml.dist') ? 'phpcs.xml.dist' : null,
66
+ phpstanConfig: exists('phpstan.neon.dist') ? 'phpstan.neon.dist' : null,
67
+ composerScripts: [],
68
+ },
69
+ js: {
70
+ hasBiome: exists('biome.json'),
71
+ biomeVersion: null,
72
+ hasEslint: exists('eslint.config.mjs') || exists('.eslintrc.js'),
73
+ hasPrettier: exists('.prettierrc'),
74
+ hasVitest: false,
75
+ hasJest: false,
76
+ hasPlaywright: exists('playwright.config.ts') || exists('playwright.config.js'),
77
+ hasWpScripts: false,
78
+ rootScripts: [],
79
+ },
80
+ playground: {
81
+ hasPlayground: exists('tools/playground') || exists('playground'),
82
+ blueprints: [],
83
+ scripts: [],
84
+ hasWpEnv: exists('.wp-env.json'),
85
+ },
86
+ satispress: { configured: false, url: null, hasAuthJson: false },
87
+ wpackagist: false,
88
+ gitHooks: exists('.githooks') ? '.githooks' : exists('.husky') ? '.husky' : null,
89
+ hasAgentKit: exists('.wp-agent-kit-manifest.github.json') || exists('.agents', 'skills'),
90
+ hasAgentsDir: exists('.agents'),
91
+ hasAgentsMd: exists('AGENTS.md'),
92
+ gitBranch: null,
93
+ gitRemotes: [],
94
+ };
95
+ }
96
+ // ── Scaffolding helpers ────────────────────────────────────────────────────
97
+ function writeIfMissing(filePath, content, dryRun) {
98
+ if (fs.existsSync(filePath))
99
+ return false;
100
+ if (!dryRun) {
101
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
102
+ fs.writeFileSync(filePath, content, 'utf-8');
103
+ }
104
+ return true;
105
+ }
106
+ function scaffoldWpCliYml(targetDir, structure, wpeEnvironments, dryRun) {
107
+ const wpCliPath = path.join(targetDir, 'wp-cli.yml');
108
+ if (fs.existsSync(wpCliPath))
109
+ return [];
110
+ const localPath = structure.wpRoot && structure.wpRoot !== '.' ? `path: ./${structure.wpRoot}\n\n` : '';
111
+ const prod = wpeEnvironments?.production;
112
+ const staging = wpeEnvironments?.staging;
113
+ const dev = wpeEnvironments?.development;
114
+ const content = [
115
+ '# wp-cli.yml — WP-CLI targeting. Commit to repo.',
116
+ '# Get exact git push URLs from: https://my.wpengine.com/installs/<ENV>/git_push',
117
+ '',
118
+ localPath,
119
+ prod
120
+ ? `@production:\n ssh: ${prod}@${prod}.ssh.wpengine.net\n path: /home/wpe-user/sites/${prod}\n`
121
+ : '',
122
+ staging
123
+ ? `\n@staging:\n ssh: ${staging}@${staging}.ssh.wpengine.net\n path: /home/wpe-user/sites/${staging}\n`
124
+ : '',
125
+ dev
126
+ ? `\n@development:\n ssh: ${dev}@${dev}.ssh.wpengine.net\n path: /home/wpe-user/sites/${dev}\n`
127
+ : '',
128
+ ].join('');
129
+ if (writeIfMissing(wpCliPath, content, dryRun)) {
130
+ return ['wp-cli.yml'];
131
+ }
132
+ return [];
133
+ }
134
+ function scaffoldGitHooks(targetDir, dryRun) {
135
+ const hooksDir = path.join(targetDir, '.githooks');
136
+ const prePush = path.join(hooksDir, 'pre-push');
137
+ const readme = path.join(hooksDir, 'README.md');
138
+ const created = [];
139
+ const prePushContent = `#!/usr/bin/env bash
140
+ # .githooks/pre-push — CI gate before every push.
141
+ # Activate once: git config core.hooksPath .githooks
142
+ # Generated by wp-agent-kit bootstrap.
143
+ set -uo pipefail
144
+ root="$(git rev-parse --show-toplevel)"
145
+ cd "$root"
146
+
147
+ # Run the CI gate (PHP + JS/TS)
148
+ GATE_SCRIPT=".agents/skills/wp-bootstrap/scripts/ci-gate.sh"
149
+ if [ -f "$GATE_SCRIPT" ]; then
150
+ bash "$GATE_SCRIPT" || exit 1
151
+ else
152
+ echo "⚠ CI gate script not found at $GATE_SCRIPT" >&2
153
+ echo " Run wp-agent-kit install to set up .agents/skills/" >&2
154
+ fi
155
+ `;
156
+ const readmeContent = `# Git hooks (versioned)
157
+
158
+ Activate once per clone:
159
+ \`\`\`bash
160
+ git config core.hooksPath .githooks
161
+ chmod +x .githooks/pre-push
162
+ \`\`\`
163
+ Or run: \`pnpm run setup\` / \`bash tools/setup.sh\` if that exists.
164
+
165
+ ## What runs
166
+
167
+ - **pre-push**: PHP gate (php -l + phpcs + phpstan) + JS gate (biome check).
168
+ Fix failures with \`composer fix && composer lint\` (PHP) or \`npx biome check --write .\` (JS).
169
+ --no-verify is not allowed on deploy branches; CI will catch it anyway.
170
+ `;
171
+ if (!fs.existsSync(hooksDir) && !dryRun) {
172
+ fs.mkdirSync(hooksDir, { recursive: true });
173
+ }
174
+ if (writeIfMissing(prePush, prePushContent, dryRun)) {
175
+ if (!dryRun)
176
+ fs.chmodSync(prePush, 0o755);
177
+ created.push('.githooks/pre-push');
178
+ }
179
+ if (writeIfMissing(readme, readmeContent, dryRun)) {
180
+ created.push('.githooks/README.md');
181
+ }
182
+ return created;
183
+ }
184
+ function scaffoldSetupSh(targetDir, dryRun) {
185
+ const toolsDir = path.join(targetDir, 'tools');
186
+ const setupSh = path.join(toolsDir, 'setup.sh');
187
+ if (fs.existsSync(setupSh))
188
+ return [];
189
+ const content = `#!/usr/bin/env bash
190
+ # tools/setup.sh — one-command dev setup. Idempotent: safe to re-run.
191
+ # Generated by wp-agent-kit bootstrap.
192
+ set -uo pipefail
193
+ root="$(cd "$(dirname "$0")/.." && pwd)"
194
+ bash "$root/.agents/skills/wp-bootstrap/scripts/bootstrap.sh" "$@"
195
+ `;
196
+ if (writeIfMissing(setupSh, content, dryRun)) {
197
+ if (!dryRun)
198
+ fs.chmodSync(setupSh, 0o755);
199
+ return ['tools/setup.sh'];
200
+ }
201
+ return [];
202
+ }
203
+ function scaffoldBootstrapConfig(targetDir, structure, options, dryRun) {
204
+ const configPath = path.join(targetDir, 'wp-bootstrap.config.json');
205
+ if (fs.existsSync(configPath))
206
+ return [];
207
+ const config = {
208
+ packageManager: options.packageManager ?? structure.packageManager ?? 'npm',
209
+ jsWorkspaces: structure.jsWorkspaces,
210
+ phpDirs: ['.', ...structure.wpPackages.filter((p) => p.hasComposer).map((p) => p.path)],
211
+ hooksDir: '.githooks',
212
+ wpPackages: structure.wpPackages.map((p) => ({
213
+ path: p.path,
214
+ type: p.type,
215
+ slug: p.slug,
216
+ ...(p.mainFile ? { mainFile: p.mainFile } : {}),
217
+ })),
218
+ ...(structure.wpeRemotes.length > 0 || options.wpeEnvironments
219
+ ? {
220
+ wpeEnvironments: {
221
+ ...(options.wpeEnvironments?.production
222
+ ? {
223
+ production: { install: options.wpeEnvironments.production },
224
+ }
225
+ : {}),
226
+ ...(options.wpeEnvironments?.staging
227
+ ? {
228
+ staging: { install: options.wpeEnvironments.staging },
229
+ }
230
+ : {}),
231
+ ...(options.wpeEnvironments?.development
232
+ ? {
233
+ development: { install: options.wpeEnvironments.development },
234
+ }
235
+ : {}),
236
+ },
237
+ }
238
+ : {}),
239
+ };
240
+ if (writeIfMissing(configPath, `${JSON.stringify(config, null, 2)}
241
+ `, dryRun)) {
242
+ return ['wp-bootstrap.config.json'];
243
+ }
244
+ return [];
245
+ }
246
+ // ── Main API ───────────────────────────────────────────────────────────────
247
+ export async function bootstrapApi(options) {
248
+ const formatter = new OutputFormatter('json', 'bootstrap', '0.0.0');
249
+ const { targetDir, platform = 'github', detectOnly = false, dryRun = false, skipInstall = false, } = options;
250
+ try {
251
+ // 1. Detect structure
252
+ const structure = detectStructure(targetDir);
253
+ if (!structure) {
254
+ return formatter.fail({
255
+ code: 'DETECT_FAILED',
256
+ message: 'Failed to detect project structure',
257
+ exitCode: ExitCode.ERROR,
258
+ });
259
+ }
260
+ // Apply overrides
261
+ if (options.wpRoot)
262
+ structure.wpRoot = options.wpRoot;
263
+ if (options.packageManager)
264
+ structure.packageManager = options.packageManager;
265
+ if (detectOnly) {
266
+ return formatter.success({
267
+ structure,
268
+ detectOnly: true,
269
+ actions: [],
270
+ filesCreated: [],
271
+ dryRun,
272
+ });
273
+ }
274
+ const actions = [];
275
+ const filesCreated = [];
276
+ // 2. Install agent kit
277
+ if (!skipInstall && !structure.hasAgentKit) {
278
+ if (!dryRun) {
279
+ installKit(targetDir, platform, { safe: true, backup: false });
280
+ }
281
+ actions.push(`✅ wp-agent-kit installed (platform: ${platform})`);
282
+ filesCreated.push('.agents/skills/', `${platform === 'github' ? '.github' : `.${platform}`}/`);
283
+ }
284
+ else if (structure.hasAgentKit) {
285
+ actions.push('✓ wp-agent-kit already installed');
286
+ }
287
+ // 3. Generate wp-cli.yml
288
+ const wpeEnvs = options.wpeEnvironments;
289
+ const hasWpeConfig = wpeEnvs?.production || wpeEnvs?.staging || wpeEnvs?.development;
290
+ if (hasWpeConfig || structure.wpRoot) {
291
+ const created = scaffoldWpCliYml(targetDir, structure, wpeEnvs, dryRun);
292
+ if (created.length > 0) {
293
+ actions.push('✅ wp-cli.yml created');
294
+ filesCreated.push(...created);
295
+ }
296
+ else {
297
+ actions.push('✓ wp-cli.yml already exists');
298
+ }
299
+ }
300
+ // 4. Git hooks
301
+ const hookFiles = scaffoldGitHooks(targetDir, dryRun);
302
+ if (hookFiles.length > 0) {
303
+ actions.push('✅ .githooks/pre-push created');
304
+ filesCreated.push(...hookFiles);
305
+ if (!dryRun) {
306
+ // Activate hooks
307
+ spawnSync('git', ['config', 'core.hooksPath', '.githooks'], { cwd: targetDir });
308
+ }
309
+ actions.push('✅ git config core.hooksPath .githooks');
310
+ }
311
+ // 5. tools/setup.sh
312
+ const setupFiles = scaffoldSetupSh(targetDir, dryRun);
313
+ if (setupFiles.length > 0) {
314
+ actions.push('✅ tools/setup.sh created');
315
+ filesCreated.push(...setupFiles);
316
+ }
317
+ // 6. wp-bootstrap.config.json
318
+ const configFiles = scaffoldBootstrapConfig(targetDir, structure, options, dryRun);
319
+ if (configFiles.length > 0) {
320
+ actions.push('✅ wp-bootstrap.config.json created');
321
+ filesCreated.push(...configFiles);
322
+ }
323
+ // 7. Report what still needs manual setup
324
+ if (!structure.php.hasComposer) {
325
+ actions.push('⚠ composer.json missing — run: composer init (see wp-bootstrap skill references/composer-setup.md)');
326
+ }
327
+ if (!structure.js.hasBiome) {
328
+ actions.push('⚠ biome.json missing — run: npm install --save-dev @biomejs/biome && npx biome init');
329
+ }
330
+ if (!structure.playground.hasPlayground) {
331
+ actions.push('⚠ Playground not configured — run playground-start.sh to get started');
332
+ }
333
+ if (!structure.hasWpeRemote && !hasWpeConfig) {
334
+ actions.push('⚠ No WP Engine remotes — add --wpe-prod/--wpe-staging/--wpe-dev or configure manually');
335
+ }
336
+ return formatter.success({
337
+ structure,
338
+ detectOnly: false,
339
+ actions,
340
+ filesCreated,
341
+ dryRun,
342
+ });
343
+ }
344
+ catch (error) {
345
+ const err = error;
346
+ return formatter.fail({
347
+ code: 'BOOTSTRAP_FAILED',
348
+ message: err.message || 'Bootstrap failed',
349
+ exitCode: err.exitCode ?? ExitCode.ERROR,
350
+ });
351
+ }
352
+ }