wordpress-agent-kit 0.2.1 → 0.3.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 (37) hide show
  1. package/.github/agents/wp-architect.agent.md +1 -0
  2. package/.github/skills/wordpress-router/SKILL.md +1 -0
  3. package/.github/skills/wp-abilities-api/SKILL.md +1 -0
  4. package/.github/skills/wp-block-development/SKILL.md +1 -0
  5. package/.github/skills/wp-block-themes/SKILL.md +1 -0
  6. package/.github/skills/wp-interactivity-api/SKILL.md +1 -0
  7. package/.github/skills/wp-performance/SKILL.md +1 -0
  8. package/.github/skills/wp-phpstan/SKILL.md +1 -0
  9. package/.github/skills/wp-playground/SKILL.md +1 -0
  10. package/.github/skills/wp-plugin-development/SKILL.md +1 -0
  11. package/.github/skills/wp-project-triage/SKILL.md +1 -0
  12. package/.github/skills/wp-rest-api/SKILL.md +1 -0
  13. package/.github/skills/wp-wpcli-and-ops/SKILL.md +1 -0
  14. package/.github/skills/wpds/SKILL.md +1 -0
  15. package/.github/workflows/ci.yml +44 -0
  16. package/.husky/pre-commit +7 -0
  17. package/AGENTS.md +33 -10
  18. package/AGENTS.template.md +63 -18
  19. package/CLI_REVIEW.md +250 -0
  20. package/README.md +240 -68
  21. package/biome.json +39 -0
  22. package/dist/cli.js +75 -4
  23. package/dist/commands/install.js +84 -10
  24. package/dist/commands/run-playground.js +59 -14
  25. package/dist/commands/setup.js +222 -163
  26. package/dist/commands/sync-skills.js +33 -60
  27. package/dist/commands/upgrade.js +211 -0
  28. package/dist/lib/api.js +511 -0
  29. package/dist/lib/installer.js +114 -6
  30. package/dist/lib/triage-mapper.js +18 -20
  31. package/dist/lib/updater.js +260 -0
  32. package/dist/utils/exit-codes.js +60 -0
  33. package/dist/utils/output.js +96 -0
  34. package/dist/utils/paths.js +1 -1
  35. package/dist/utils/run.js +1 -1
  36. package/extensions/wp-agent-kit/index.ts +630 -0
  37. package/package.json +27 -4
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Programmatic API for WordPress Agent Kit.
3
+ * Can be imported directly by agents/scripts: `import { installKit } from 'wordpress-agent-kit/api'`
4
+ */
5
+ import { spawnSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { ExitCode, withExitCode } from '../utils/exit-codes.js';
9
+ import { OutputFormatter } from '../utils/output.js';
10
+ import { PACKAGE_ROOT } from '../utils/paths.js';
11
+ import { installKit } from './installer.js';
12
+ import { computeChanges, isKitInstalled } from './updater.js';
13
+ /**
14
+ * Install the WordPress Agent Kit programmatically.
15
+ */
16
+ export async function installKitApi(options) {
17
+ const startTime = Date.now();
18
+ const formatter = new OutputFormatter('json', 'install', '0.0.0');
19
+ try {
20
+ const { targetDir, platform, force = false, dryRun = false, safe = true, backup = true, } = options;
21
+ if (dryRun) {
22
+ return dryRunInstall(targetDir, platform, { force, safe, backup });
23
+ }
24
+ await withExitCode(async () => {
25
+ installKit(targetDir, platform, { force, safe, backup });
26
+ return { success: true };
27
+ });
28
+ const filesCreated = getInstalledFiles(targetDir, platform);
29
+ const durationMs = Date.now() - startTime;
30
+ return formatter.success({
31
+ targetDir,
32
+ platform,
33
+ filesCreated,
34
+ filesSkipped: [],
35
+ errors: [],
36
+ durationMs,
37
+ isUpdate: isKitInstalled(targetDir, platform),
38
+ backupDir: null,
39
+ });
40
+ }
41
+ catch (error) {
42
+ const err = error;
43
+ return formatter.fail({
44
+ code: err.code || 'INSTALL_FAILED',
45
+ message: err.message || 'Installation failed',
46
+ exitCode: err.exitCode ?? ExitCode.ERROR,
47
+ details: { platform: options.platform, targetDir: options.targetDir },
48
+ });
49
+ }
50
+ }
51
+ /**
52
+ * Dry-run preview for install.
53
+ */
54
+ function dryRunInstall(targetDir, platform, options) {
55
+ const { force = false, safe = true } = options;
56
+ const platformFolder = getPlatformFolder(platform);
57
+ const formatter = new OutputFormatter('json', 'install', '0.0.0');
58
+ // Use change-computation for safe updates on existing installations
59
+ if (safe && isKitInstalled(targetDir, platform)) {
60
+ const changes = computeChanges(targetDir, platform, force);
61
+ const actions = changes.map((c) => {
62
+ const target = path.join(targetDir, platformFolder, c.relativePath);
63
+ return {
64
+ type: c.action === 'created' ? 'create' : c.action === 'updated' ? 'update' : 'skip',
65
+ target,
66
+ description: `${c.action}: ${c.relativePath}${c.reason ? ` (${c.reason})` : ''}`,
67
+ };
68
+ });
69
+ // AGENTS.md check
70
+ const targetAgents = path.join(targetDir, 'AGENTS.md');
71
+ if (!fs.existsSync(targetAgents)) {
72
+ actions.push({
73
+ type: 'create',
74
+ target: targetAgents,
75
+ description: 'Create AGENTS.md from template',
76
+ });
77
+ }
78
+ else {
79
+ actions.push({
80
+ type: 'skip',
81
+ target: targetAgents,
82
+ description: 'AGENTS.md exists (preserved)',
83
+ });
84
+ }
85
+ return formatter.success({
86
+ wouldExecute: true,
87
+ actions,
88
+ summary: {
89
+ targetDir,
90
+ platform,
91
+ filesCreated: changes
92
+ .filter((c) => c.action === 'created')
93
+ .map((c) => path.join(platformFolder, c.relativePath)),
94
+ filesSkipped: changes
95
+ .filter((c) => c.action === 'skipped' || c.action === 'conflict')
96
+ .map((c) => `${path.join(platformFolder, c.relativePath)} (${c.reason})`),
97
+ errors: [],
98
+ durationMs: 0,
99
+ isUpdate: true,
100
+ backupDir: null,
101
+ conflicts: changes
102
+ .filter((c) => c.action === 'conflict')
103
+ .map((c) => path.join(platformFolder, c.relativePath)),
104
+ },
105
+ });
106
+ }
107
+ // Legacy dry-run for fresh installs
108
+ const actions = [];
109
+ const sourceGithub = path.join(PACKAGE_ROOT, '.github');
110
+ const targetPlatform = path.join(targetDir, platformFolder);
111
+ const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
112
+ const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
113
+ const targetAgents = path.join(targetDir, 'AGENTS.md');
114
+ if (fs.existsSync(sourceGithub)) {
115
+ if (fs.existsSync(targetPlatform) && !force) {
116
+ actions.push({
117
+ type: 'update',
118
+ target: targetPlatform,
119
+ description: `Would update ${platformFolder} (use --force to overwrite)`,
120
+ });
121
+ }
122
+ else {
123
+ actions.push({
124
+ type: 'copy',
125
+ source: sourceGithub,
126
+ target: targetPlatform,
127
+ description: `Copy ${platformFolder} from kit`,
128
+ });
129
+ }
130
+ }
131
+ if (fs.existsSync(templatePath)) {
132
+ actions.push({
133
+ type: 'copy',
134
+ source: templatePath,
135
+ target: targetAgentsTemplate,
136
+ description: 'Copy AGENTS.template.md',
137
+ });
138
+ }
139
+ if (!fs.existsSync(targetAgents) || force) {
140
+ if (fs.existsSync(templatePath)) {
141
+ actions.push({
142
+ type: 'copy',
143
+ source: templatePath,
144
+ target: targetAgents,
145
+ description: 'Create AGENTS.md from template',
146
+ });
147
+ }
148
+ }
149
+ else {
150
+ actions.push({
151
+ type: 'update',
152
+ target: targetAgents,
153
+ description: 'AGENTS.md exists (use --force to overwrite)',
154
+ });
155
+ }
156
+ return formatter.success({
157
+ wouldExecute: true,
158
+ actions,
159
+ summary: {
160
+ targetDir,
161
+ platform,
162
+ filesCreated: actions.filter((a) => a.type === 'copy').map((a) => a.target),
163
+ filesSkipped: actions.filter((a) => a.type === 'update').map((a) => a.target),
164
+ errors: [],
165
+ durationMs: 0,
166
+ isUpdate: false,
167
+ backupDir: null,
168
+ },
169
+ });
170
+ }
171
+ /**
172
+ * Sync skills from WordPress/agent-skills programmatically.
173
+ */
174
+ export async function syncSkillsApi(options = {}) {
175
+ const startTime = Date.now();
176
+ const formatter = new OutputFormatter('json', 'sync-skills', '0.0.0');
177
+ const targetDir = options.targetDir || process.cwd();
178
+ const ref = options.ref || 'trunk';
179
+ if (options.dryRun) {
180
+ return dryRunSyncSkills(targetDir, ref);
181
+ }
182
+ try {
183
+ const result = await withExitCode(async () => {
184
+ const repoRoot = targetDir;
185
+ const submodulePath = path.join('vendor', 'wp-agent-skills');
186
+ const vendorSkillsDir = path.join(repoRoot, submodulePath);
187
+ const submoduleGitDir = path.join(vendorSkillsDir, '.git');
188
+ // Clone or update
189
+ if (!fs.existsSync(submoduleGitDir)) {
190
+ fs.mkdirSync(path.join(repoRoot, 'vendor'), { recursive: true });
191
+ const cloneResult = spawnSync('git', ['clone', 'https://github.com/WordPress/agent-skills.git', submodulePath], {
192
+ cwd: repoRoot,
193
+ encoding: 'utf-8',
194
+ });
195
+ if (cloneResult.status !== 0) {
196
+ throw new Error(`Git clone failed: ${cloneResult.stderr?.toString()}`);
197
+ }
198
+ }
199
+ else {
200
+ const fetchResult = spawnSync('git', ['fetch', '--all', '--tags'], {
201
+ cwd: vendorSkillsDir,
202
+ encoding: 'utf-8',
203
+ });
204
+ if (fetchResult.status !== 0) {
205
+ throw new Error(`Git fetch failed: ${fetchResult.stderr?.toString()}`);
206
+ }
207
+ }
208
+ // Checkout ref
209
+ const checkoutResult = spawnSync('git', ['checkout', ref], {
210
+ cwd: vendorSkillsDir,
211
+ encoding: 'utf-8',
212
+ });
213
+ if (checkoutResult.status !== 0) {
214
+ throw new Error(`Git checkout failed: ${checkoutResult.stderr?.toString()}`);
215
+ }
216
+ const pullResult = spawnSync('git', ['pull', 'origin', ref], {
217
+ cwd: vendorSkillsDir,
218
+ encoding: 'utf-8',
219
+ });
220
+ if (pullResult.status !== 0) {
221
+ throw new Error(`Git pull failed: ${pullResult.stderr?.toString()}`);
222
+ }
223
+ const targetSkills = path.join(repoRoot, '.github', 'skills');
224
+ const upstreamBuildScript = path.join(vendorSkillsDir, 'shared', 'scripts', 'skillpack-build.mjs');
225
+ const upstreamInstallScript = path.join(vendorSkillsDir, 'shared', 'scripts', 'skillpack-install.mjs');
226
+ let method = 'direct-copy';
227
+ let skillsSynced = 0;
228
+ if (fs.existsSync(upstreamBuildScript) && fs.existsSync(upstreamInstallScript)) {
229
+ if (fs.existsSync(targetSkills)) {
230
+ fs.rmSync(targetSkills, { recursive: true, force: true });
231
+ }
232
+ fs.mkdirSync(path.join(repoRoot, '.github'), { recursive: true });
233
+ const buildResult = spawnSync('node', ['shared/scripts/skillpack-build.mjs', '--clean', '--targets=vscode'], {
234
+ cwd: vendorSkillsDir,
235
+ encoding: 'utf-8',
236
+ });
237
+ if (buildResult.status !== 0) {
238
+ throw new Error(`Skillpack build failed: ${buildResult.stderr?.toString()}`);
239
+ }
240
+ const installResult = spawnSync('node', [
241
+ 'shared/scripts/skillpack-install.mjs',
242
+ `--dest=${repoRoot}`,
243
+ '--targets=vscode',
244
+ '--from=dist',
245
+ '--mode=replace',
246
+ ], { cwd: vendorSkillsDir, encoding: 'utf-8' });
247
+ if (installResult.status !== 0) {
248
+ throw new Error(`Skillpack install failed: ${installResult.stderr?.toString()}`);
249
+ }
250
+ method = 'skillpack';
251
+ if (fs.existsSync(targetSkills)) {
252
+ skillsSynced = fs.readdirSync(targetSkills).length;
253
+ }
254
+ }
255
+ else {
256
+ const sourceSkills = path.join(vendorSkillsDir, '.github', 'skills');
257
+ if (!fs.existsSync(sourceSkills)) {
258
+ throw new Error(`Upstream skills not found at ${sourceSkills}`);
259
+ }
260
+ if (fs.existsSync(targetSkills)) {
261
+ fs.rmSync(targetSkills, { recursive: true, force: true });
262
+ }
263
+ fs.mkdirSync(path.join(repoRoot, '.github'), { recursive: true });
264
+ fs.cpSync(sourceSkills, targetSkills, { recursive: true });
265
+ skillsSynced = fs.readdirSync(targetSkills).length;
266
+ }
267
+ return { success: true, skillsSynced, method };
268
+ });
269
+ return formatter.success({
270
+ targetDir,
271
+ skillsSynced: result.skillsSynced,
272
+ sourceUrl: 'https://github.com/WordPress/agent-skills.git',
273
+ ref,
274
+ durationMs: Date.now() - startTime,
275
+ method: result.method,
276
+ });
277
+ }
278
+ catch (error) {
279
+ const err = error;
280
+ return formatter.fail({
281
+ code: err.code || 'SYNC_FAILED',
282
+ message: err.message || 'Sync failed',
283
+ exitCode: err.exitCode ?? ExitCode.ERROR,
284
+ details: { ref, targetDir },
285
+ });
286
+ }
287
+ }
288
+ /**
289
+ * Dry-run preview for sync-skills.
290
+ */
291
+ function dryRunSyncSkills(targetDir, ref) {
292
+ const actions = [];
293
+ const targetSkills = path.join(targetDir, '.github', 'skills');
294
+ const vendorDir = path.join(targetDir, 'vendor', 'wp-agent-skills');
295
+ actions.push({
296
+ type: 'mkdir',
297
+ target: path.join(targetDir, 'vendor'),
298
+ description: 'Create vendor directory',
299
+ });
300
+ if (!fs.existsSync(vendorDir)) {
301
+ actions.push({
302
+ type: 'create',
303
+ target: vendorDir,
304
+ description: 'Clone WordPress/agent-skills repository',
305
+ });
306
+ }
307
+ else {
308
+ actions.push({
309
+ type: 'update',
310
+ target: vendorDir,
311
+ description: `Fetch and checkout ${ref}`,
312
+ });
313
+ }
314
+ if (fs.existsSync(targetSkills)) {
315
+ actions.push({
316
+ type: 'delete',
317
+ target: targetSkills,
318
+ description: 'Remove existing skills directory',
319
+ });
320
+ }
321
+ actions.push({
322
+ type: 'create',
323
+ target: targetSkills,
324
+ description: 'Install synced skills',
325
+ });
326
+ return new OutputFormatter('json', 'sync-skills', '0.0.0').success({
327
+ wouldExecute: true,
328
+ actions,
329
+ summary: {
330
+ targetDir,
331
+ skillsSynced: 0,
332
+ sourceUrl: 'https://github.com/WordPress/agent-skills.git',
333
+ ref,
334
+ durationMs: 0,
335
+ method: 'skillpack',
336
+ },
337
+ });
338
+ }
339
+ /**
340
+ * Run project triage detection programmatically.
341
+ */
342
+ export async function runTriageApi(options) {
343
+ const formatter = new OutputFormatter('json', 'triage', '0.0.0');
344
+ const { targetDir, platform = 'github' } = options;
345
+ try {
346
+ const platformFolder = getPlatformFolder(platform);
347
+ const triageScriptPaths = [
348
+ path.join(targetDir, platformFolder, 'skills/wp-project-triage/scripts/detect_wp_project.mjs'),
349
+ path.join(PACKAGE_ROOT, 'vendor/wp-agent-skills/skills/wp-project-triage/scripts/detect_wp_project.mjs'),
350
+ ];
351
+ const triageScriptPath = triageScriptPaths.find((p) => fs.existsSync(p));
352
+ if (!triageScriptPath) {
353
+ return formatter.fail({
354
+ code: 'TRIAGE_NOT_FOUND',
355
+ message: 'Project triage script not found. Run sync-skills first.',
356
+ exitCode: ExitCode.NOT_FOUND,
357
+ });
358
+ }
359
+ const result = spawnSync('node', [triageScriptPath], {
360
+ cwd: targetDir,
361
+ encoding: 'utf-8',
362
+ });
363
+ if (result.status !== 0) {
364
+ return formatter.fail({
365
+ code: 'TRIAGE_FAILED',
366
+ message: result.stderr?.toString() || 'Triage script failed',
367
+ exitCode: ExitCode.ERROR,
368
+ });
369
+ }
370
+ const triageResult = JSON.parse(result.stdout.trim());
371
+ return formatter.success(triageResult);
372
+ }
373
+ catch (error) {
374
+ const err = error;
375
+ return formatter.fail({
376
+ code: 'TRIAGE_ERROR',
377
+ message: err.message || 'Triage failed',
378
+ exitCode: ExitCode.ERROR,
379
+ });
380
+ }
381
+ }
382
+ /**
383
+ * Configure AGENTS.md with project details programmatically.
384
+ */
385
+ export async function configureAgentsMdApi(options) {
386
+ const formatter = new OutputFormatter('json', 'configure', '0.0.0');
387
+ const { targetDir, platform, config, dryRun = false } = options;
388
+ try {
389
+ const platformFolder = getPlatformFolder(platform);
390
+ const agentsPath = path.join(targetDir, 'AGENTS.md');
391
+ const platformInstructionsPath = path.join(targetDir, platformFolder, 'instructions', 'wordpress-workflow.instructions.md');
392
+ if (dryRun) {
393
+ const actions = [];
394
+ if (fs.existsSync(agentsPath)) {
395
+ actions.push({
396
+ type: 'update',
397
+ target: agentsPath,
398
+ description: `Update AGENTS.md with project type: ${config.projectType}, tech: ${config.techStack.join(', ')}`,
399
+ });
400
+ }
401
+ else {
402
+ actions.push({
403
+ type: 'create',
404
+ target: agentsPath,
405
+ description: 'Create AGENTS.md with project configuration',
406
+ });
407
+ }
408
+ if (fs.existsSync(platformInstructionsPath)) {
409
+ actions.push({
410
+ type: 'update',
411
+ target: platformInstructionsPath,
412
+ description: 'Workflow instructions available for customization',
413
+ });
414
+ }
415
+ return formatter.success({
416
+ wouldExecute: true,
417
+ actions,
418
+ summary: {
419
+ targetDir,
420
+ modified: [agentsPath],
421
+ skipped: [],
422
+ dryRun: true,
423
+ },
424
+ });
425
+ }
426
+ const modified = [];
427
+ const skipped = [];
428
+ // Update AGENTS.md
429
+ if (fs.existsSync(agentsPath)) {
430
+ let agentsContent = fs.readFileSync(agentsPath, 'utf-8');
431
+ const pm = config.packageManager || 'npm/pnpm';
432
+ agentsContent = agentsContent.replace(/\*\*Tooling\*\*: .*/, `**Tooling**: ${config.techStack.includes('composer') ? 'Composer for PHP' : ''}${config.techStack.includes('npm') ? `, ${pm} for JS` : ''}.`);
433
+ fs.writeFileSync(agentsPath, agentsContent, 'utf-8');
434
+ modified.push(agentsPath);
435
+ }
436
+ else {
437
+ skipped.push(agentsPath);
438
+ }
439
+ // Note workflow instructions
440
+ if (fs.existsSync(platformInstructionsPath)) {
441
+ modified.push(platformInstructionsPath);
442
+ }
443
+ else {
444
+ skipped.push(platformInstructionsPath);
445
+ }
446
+ return formatter.success({
447
+ targetDir,
448
+ modified,
449
+ skipped,
450
+ dryRun: false,
451
+ });
452
+ }
453
+ catch (error) {
454
+ const err = error;
455
+ return formatter.fail({
456
+ code: 'CONFIGURE_FAILED',
457
+ message: err.message || 'Configuration failed',
458
+ exitCode: ExitCode.ERROR,
459
+ });
460
+ }
461
+ }
462
+ /**
463
+ * Get platform folder name.
464
+ */
465
+ function getPlatformFolder(platform) {
466
+ const folders = {
467
+ github: '.github',
468
+ cursor: '.cursor',
469
+ claude: '.claude',
470
+ agent: '.agent',
471
+ pi: '.pi/agent',
472
+ };
473
+ return folders[platform];
474
+ }
475
+ /**
476
+ * Get list of files that would be/are installed.
477
+ */
478
+ function getInstalledFiles(targetDir, platform) {
479
+ const platformFolder = getPlatformFolder(platform);
480
+ const files = [];
481
+ const targetPlatform = path.join(targetDir, platformFolder);
482
+ if (fs.existsSync(targetPlatform)) {
483
+ function walk(dir, prefix = '') {
484
+ const entries = fs.readdirSync(dir);
485
+ for (const entry of entries) {
486
+ const fullPath = path.join(dir, entry);
487
+ const relPath = path.join(prefix, entry);
488
+ const stat = fs.statSync(fullPath);
489
+ if (stat.isDirectory()) {
490
+ walk(fullPath, relPath);
491
+ }
492
+ else {
493
+ files.push(path.join(platformFolder, relPath));
494
+ }
495
+ }
496
+ }
497
+ walk(targetPlatform);
498
+ }
499
+ const agentsPath = path.join(targetDir, 'AGENTS.md');
500
+ if (fs.existsSync(agentsPath)) {
501
+ files.push('AGENTS.md');
502
+ }
503
+ const agentsTemplatePath = path.join(targetDir, 'AGENTS.template.md');
504
+ if (fs.existsSync(agentsTemplatePath)) {
505
+ files.push('AGENTS.template.md');
506
+ }
507
+ return files;
508
+ }
509
+ export { ExitCode } from '../utils/exit-codes.js';
510
+ export { OutputFormatter, createFormatter, parseOutputFormat } from '../utils/output.js';
511
+ export { computeChanges, isKitInstalled, loadManifest, updateKit } from './updater.js';
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { PACKAGE_ROOT } from '../utils/paths.js';
4
+ import { updateKit } from './updater.js';
4
5
  /**
5
6
  * Platform-specific folder names
6
7
  */
@@ -13,13 +14,36 @@ export const PLATFORM_FOLDERS = {
13
14
  };
14
15
  /**
15
16
  * Installs the WordPress Agent Kit into the specified directory for a given platform.
16
- * Copies the platform-specific folder and AGENTS.md template.
17
+ * If the kit is already installed, uses safe update logic to preserve user modifications.
17
18
  *
18
- * @param {string} targetDir - The directory where the kit should be installed.
19
- * @param {Platform} platform - The target platform (github, cursor, claude, agent)
20
- * @returns {Promise<void>}
19
+ * @param targetDir - The directory where the kit should be installed.
20
+ * @param platform - The target platform (github, cursor, claude, agent, pi)
21
+ * @returns InstallKitResult with details of what was created/skipped
21
22
  */
22
- export async function installKit(targetDir, platform = 'github') {
23
+ export function installKit(targetDir, platform = 'github', options = {}) {
24
+ const { force = false, backup = true, safe = true } = options;
25
+ // Check if kit is already installed
26
+ const isInstalled = isKitAlreadyInstalled(targetDir, platform);
27
+ // Use safe update for existing installations
28
+ if (isInstalled && safe) {
29
+ return safeUpdateInstall(targetDir, platform, { force, backup });
30
+ }
31
+ // Fresh install or fallback to full replacement
32
+ return fullInstall(targetDir, platform, force);
33
+ }
34
+ /**
35
+ * Check if kit is already installed for a platform.
36
+ */
37
+ function isKitAlreadyInstalled(targetDir, platform) {
38
+ const platformFolder = PLATFORM_FOLDERS[platform];
39
+ const targetPlatform = path.join(targetDir, platformFolder);
40
+ return fs.existsSync(targetPlatform);
41
+ }
42
+ /**
43
+ * Full install (fresh or force-replace).
44
+ * Only used when no existing installation is detected or safe=false.
45
+ */
46
+ function fullInstall(targetDir, platform, _force) {
23
47
  const platformFolder = PLATFORM_FOLDERS[platform];
24
48
  console.log(`Installing WordPress Agent Kit (${platform}) into: ${targetDir}`);
25
49
  if (!fs.existsSync(targetDir)) {
@@ -28,6 +52,8 @@ export async function installKit(targetDir, platform = 'github') {
28
52
  const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
29
53
  const agentsPath = path.join(PACKAGE_ROOT, 'AGENTS.md');
30
54
  const sourceGithub = path.join(PACKAGE_ROOT, '.github');
55
+ const filesCreated = [];
56
+ const filesSkipped = [];
31
57
  // Copy platform-specific folder
32
58
  const targetPlatform = path.join(targetDir, platformFolder);
33
59
  if (fs.existsSync(targetPlatform)) {
@@ -39,18 +65,100 @@ export async function installKit(targetDir, platform = 'github') {
39
65
  else {
40
66
  throw new Error('Could not find source .github directory.');
41
67
  }
42
- // Copy AGENTS.md
68
+ // Collect created files
69
+ const collectFiles = (dir, prefix) => {
70
+ if (!fs.existsSync(dir))
71
+ return;
72
+ const entries = fs.readdirSync(dir);
73
+ for (const entry of entries) {
74
+ const fullPath = path.join(dir, entry);
75
+ const stat = fs.statSync(fullPath);
76
+ if (stat.isDirectory()) {
77
+ collectFiles(fullPath, path.join(prefix, entry));
78
+ }
79
+ else {
80
+ filesCreated.push(path.join(prefix, entry));
81
+ }
82
+ }
83
+ };
84
+ collectFiles(targetPlatform, platformFolder);
85
+ // Copy AGENTS.md template
43
86
  const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
44
87
  if (fs.existsSync(templatePath)) {
45
88
  fs.copyFileSync(templatePath, targetAgentsTemplate);
89
+ filesCreated.push('AGENTS.template.md');
46
90
  }
91
+ // Copy AGENTS.md (only if it doesn't already exist)
47
92
  const targetAgents = path.join(targetDir, 'AGENTS.md');
48
93
  if (!fs.existsSync(targetAgents)) {
49
94
  if (fs.existsSync(templatePath)) {
50
95
  fs.copyFileSync(templatePath, targetAgents);
96
+ filesCreated.push('AGENTS.md');
51
97
  }
52
98
  else if (fs.existsSync(agentsPath)) {
53
99
  fs.copyFileSync(agentsPath, targetAgents);
100
+ filesCreated.push('AGENTS.md');
54
101
  }
55
102
  }
103
+ else {
104
+ filesSkipped.push('AGENTS.md (already exists, preserved)');
105
+ }
106
+ return {
107
+ targetDir,
108
+ platform,
109
+ filesCreated,
110
+ filesSkipped,
111
+ errors: [],
112
+ isUpdate: false,
113
+ backupDir: null,
114
+ };
115
+ }
116
+ /**
117
+ * Safe update install using the updater module.
118
+ */
119
+ function safeUpdateInstall(targetDir, platform, options) {
120
+ console.log(`Updating WordPress Agent Kit (${platform}) in: ${targetDir}`);
121
+ const updateOptions = {
122
+ targetDir,
123
+ platform,
124
+ force: options.force ?? false,
125
+ backup: options.backup ?? true,
126
+ };
127
+ const result = updateKit(updateOptions);
128
+ // Handle AGENTS.md separately (not part of the platform folder)
129
+ const filesSkipped = [];
130
+ const filesCreated = [...result.created];
131
+ const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
132
+ const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
133
+ if (fs.existsSync(templatePath)) {
134
+ fs.copyFileSync(templatePath, targetAgentsTemplate);
135
+ // Don't add template to created list as it's always overwritten
136
+ }
137
+ const targetAgents = path.join(targetDir, 'AGENTS.md');
138
+ if (!fs.existsSync(targetAgents)) {
139
+ fs.copyFileSync(templatePath, targetAgents);
140
+ filesCreated.push('AGENTS.md (from template)');
141
+ }
142
+ else {
143
+ filesSkipped.push('AGENTS.md (preserved)');
144
+ }
145
+ return {
146
+ targetDir,
147
+ platform,
148
+ filesCreated: [...result.created, ...result.updated].map((f) => path.join(PLATFORM_FOLDERS[platform], f)),
149
+ filesSkipped: [
150
+ ...filesSkipped,
151
+ ...result.skipped.map((f) => {
152
+ const filePath = path.join(PLATFORM_FOLDERS[platform], f);
153
+ return `${filePath} (skipped - not tracked or user modified)`;
154
+ }),
155
+ ],
156
+ conflicts: result.conflicts.map((f) => {
157
+ const filePath = path.join(PLATFORM_FOLDERS[platform], f);
158
+ return `${filePath} (conflict - user modified, use --force to overwrite)`;
159
+ }),
160
+ errors: [],
161
+ isUpdate: true,
162
+ backupDir: result.backupDir,
163
+ };
56
164
  }