wordpress-agent-kit 0.2.2 → 0.3.2

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 (51) hide show
  1. package/.github/agents/wp-architect.agent.md +1 -0
  2. package/.github/skills/blueprint/SKILL.md +418 -0
  3. package/.github/skills/wordpress-router/SKILL.md +1 -0
  4. package/.github/skills/wp-abilities-api/SKILL.md +13 -0
  5. package/.github/skills/wp-abilities-api/references/delegate-helper-pattern.md +241 -0
  6. package/.github/skills/wp-abilities-api/references/domain-vs-projection.md +113 -0
  7. package/.github/skills/wp-abilities-api/references/error-code-vocabulary.md +123 -0
  8. package/.github/skills/wp-abilities-api/references/grouping-heuristic.md +89 -0
  9. package/.github/skills/wp-abilities-api/references/input-schema-gotchas.md +265 -0
  10. package/.github/skills/wp-abilities-api/references/php-registration.md +47 -20
  11. package/.github/skills/wp-abilities-api/references/plugin-family-patterns.md +233 -0
  12. package/.github/skills/wp-abilities-api/references/shared-core-service.md +184 -0
  13. package/.github/skills/wp-abilities-audit/SKILL.md +199 -0
  14. package/.github/skills/wp-abilities-audit/references/audit-schema.md +300 -0
  15. package/.github/skills/wp-abilities-audit/references/capability-gate-tracing.md +197 -0
  16. package/.github/skills/wp-abilities-audit/references/controller-enumeration.md +116 -0
  17. package/.github/skills/wp-abilities-verify/SKILL.md +215 -0
  18. package/.github/skills/wp-abilities-verify/references/annotation-correctness.md +154 -0
  19. package/.github/skills/wp-abilities-verify/references/audit-schema-validation.md +131 -0
  20. package/.github/skills/wp-abilities-verify/references/permission-roundtrip.md +190 -0
  21. package/.github/skills/wp-abilities-verify/references/runtime-harness.md +462 -0
  22. package/.github/skills/wp-abilities-verify/references/schema-lints.md +118 -0
  23. package/.github/skills/wp-abilities-verify/references/static-enumeration.md +126 -0
  24. package/.github/skills/wp-block-development/SKILL.md +1 -0
  25. package/.github/skills/wp-block-themes/SKILL.md +1 -0
  26. package/.github/skills/wp-interactivity-api/SKILL.md +1 -0
  27. package/.github/skills/wp-performance/SKILL.md +1 -0
  28. package/.github/skills/wp-phpstan/SKILL.md +1 -0
  29. package/.github/skills/wp-playground/SKILL.md +1 -0
  30. package/.github/skills/wp-plugin-development/SKILL.md +1 -0
  31. package/.github/skills/wp-plugin-directory-guidelines/SKILL.md +133 -0
  32. package/.github/skills/wp-plugin-directory-guidelines/references/gpl-compliance.md +217 -0
  33. package/.github/skills/wp-plugin-directory-guidelines/references/guideline-review-checklist.md +592 -0
  34. package/.github/skills/wp-plugin-directory-guidelines/references/naming-rules.md +121 -0
  35. package/.github/skills/wp-project-triage/SKILL.md +1 -0
  36. package/.github/skills/wp-project-triage/scripts/detect_wp_project.mjs +22 -4
  37. package/.github/skills/wp-rest-api/SKILL.md +1 -0
  38. package/.github/skills/wp-wpcli-and-ops/SKILL.md +1 -0
  39. package/.github/skills/wpds/SKILL.md +1 -0
  40. package/AGENTS.md +33 -10
  41. package/AGENTS.template.md +63 -18
  42. package/README.md +226 -124
  43. package/biome.json +1 -1
  44. package/dist/commands/install.js +47 -6
  45. package/dist/commands/upgrade.js +34 -5
  46. package/dist/lib/api.js +93 -27
  47. package/dist/lib/installer.js +113 -7
  48. package/dist/lib/updater.js +260 -0
  49. package/extensions/wp-agent-kit/index.ts +452 -0
  50. package/package.json +21 -3
  51. package/kit-learnings.md +0 -192
package/dist/lib/api.js CHANGED
@@ -9,6 +9,7 @@ import { ExitCode, withExitCode } from '../utils/exit-codes.js';
9
9
  import { OutputFormatter } from '../utils/output.js';
10
10
  import { PACKAGE_ROOT } from '../utils/paths.js';
11
11
  import { installKit } from './installer.js';
12
+ import { computeChanges, isKitInstalled } from './updater.js';
12
13
  /**
13
14
  * Install the WordPress Agent Kit programmatically.
14
15
  */
@@ -16,15 +17,15 @@ export async function installKitApi(options) {
16
17
  const startTime = Date.now();
17
18
  const formatter = new OutputFormatter('json', 'install', '0.0.0');
18
19
  try {
19
- const { targetDir, platform, force = false, dryRun = false } = options;
20
+ const { targetDir, platform, force = false, dryRun = false, safe = true, backup = true, } = options;
20
21
  if (dryRun) {
21
- return dryRunInstall(targetDir, platform, force);
22
+ return dryRunInstall(targetDir, platform, { force, safe, backup });
22
23
  }
23
24
  await withExitCode(async () => {
24
- await installKit(targetDir, platform);
25
+ installKit(targetDir, platform, { force, safe, backup });
25
26
  return { success: true };
26
27
  });
27
- const filesCreated = getInstalledFiles(targetDir, platform);
28
+ const filesCreated = getInstalledSummary(targetDir, platform);
28
29
  const durationMs = Date.now() - startTime;
29
30
  return formatter.success({
30
31
  targetDir,
@@ -33,6 +34,8 @@ export async function installKitApi(options) {
33
34
  filesSkipped: [],
34
35
  errors: [],
35
36
  durationMs,
37
+ isUpdate: isKitInstalled(targetDir, platform),
38
+ backupDir: null,
36
39
  });
37
40
  }
38
41
  catch (error) {
@@ -48,15 +51,66 @@ export async function installKitApi(options) {
48
51
  /**
49
52
  * Dry-run preview for install.
50
53
  */
51
- function dryRunInstall(targetDir, platform, force) {
54
+ function dryRunInstall(targetDir, platform, options) {
55
+ const { force = false, safe = true } = options;
52
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
53
108
  const actions = [];
54
109
  const sourceGithub = path.join(PACKAGE_ROOT, '.github');
55
110
  const targetPlatform = path.join(targetDir, platformFolder);
56
111
  const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
57
112
  const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
58
113
  const targetAgents = path.join(targetDir, 'AGENTS.md');
59
- // Platform folder
60
114
  if (fs.existsSync(sourceGithub)) {
61
115
  if (fs.existsSync(targetPlatform) && !force) {
62
116
  actions.push({
@@ -74,7 +128,6 @@ function dryRunInstall(targetDir, platform, force) {
74
128
  });
75
129
  }
76
130
  }
77
- // AGENTS.template.md
78
131
  if (fs.existsSync(templatePath)) {
79
132
  actions.push({
80
133
  type: 'copy',
@@ -83,7 +136,6 @@ function dryRunInstall(targetDir, platform, force) {
83
136
  description: 'Copy AGENTS.template.md',
84
137
  });
85
138
  }
86
- // AGENTS.md
87
139
  if (!fs.existsSync(targetAgents) || force) {
88
140
  if (fs.existsSync(templatePath)) {
89
141
  actions.push({
@@ -101,7 +153,7 @@ function dryRunInstall(targetDir, platform, force) {
101
153
  description: 'AGENTS.md exists (use --force to overwrite)',
102
154
  });
103
155
  }
104
- return new OutputFormatter('json', 'install', '0.0.0').success({
156
+ return formatter.success({
105
157
  wouldExecute: true,
106
158
  actions,
107
159
  summary: {
@@ -111,6 +163,8 @@ function dryRunInstall(targetDir, platform, force) {
111
163
  filesSkipped: actions.filter((a) => a.type === 'update').map((a) => a.target),
112
164
  errors: [],
113
165
  durationMs: 0,
166
+ isUpdate: false,
167
+ backupDir: null,
114
168
  },
115
169
  });
116
170
  }
@@ -419,38 +473,50 @@ function getPlatformFolder(platform) {
419
473
  return folders[platform];
420
474
  }
421
475
  /**
422
- * Get list of files that would be/are installed.
476
+ * Get summary of installed files grouped by directory.
423
477
  */
424
- function getInstalledFiles(targetDir, platform) {
478
+ function getInstalledSummary(targetDir, platform) {
425
479
  const platformFolder = getPlatformFolder(platform);
426
- const files = [];
480
+ const summary = [];
427
481
  const targetPlatform = path.join(targetDir, platformFolder);
428
482
  if (fs.existsSync(targetPlatform)) {
429
- function walk(dir, prefix = '') {
430
- const entries = fs.readdirSync(dir);
431
- for (const entry of entries) {
432
- const fullPath = path.join(dir, entry);
433
- const relPath = path.join(prefix, entry);
434
- const stat = fs.statSync(fullPath);
435
- if (stat.isDirectory()) {
436
- walk(fullPath, relPath);
437
- }
438
- else {
439
- files.push(path.join(platformFolder, relPath));
483
+ // Walk top-level entries for clean summary
484
+ const topEntries = fs.readdirSync(targetPlatform);
485
+ for (const entry of topEntries) {
486
+ const fullPath = path.join(targetPlatform, entry);
487
+ const stat = fs.statSync(fullPath);
488
+ if (stat.isDirectory()) {
489
+ let totalFiles = 0;
490
+ let totalDirs = 0;
491
+ function countAll(dir) {
492
+ const items = fs.readdirSync(dir);
493
+ for (const item of items) {
494
+ const itemPath = path.join(dir, item);
495
+ const s = fs.statSync(itemPath);
496
+ if (s.isDirectory()) {
497
+ totalDirs++;
498
+ countAll(itemPath);
499
+ }
500
+ else {
501
+ totalFiles++;
502
+ }
503
+ }
440
504
  }
505
+ countAll(fullPath);
506
+ summary.push(`${platformFolder}/${entry}/ (${totalDirs + 1} dirs, ${totalFiles} files)`);
441
507
  }
442
508
  }
443
- walk(targetPlatform);
444
509
  }
445
510
  const agentsPath = path.join(targetDir, 'AGENTS.md');
446
511
  if (fs.existsSync(agentsPath)) {
447
- files.push('AGENTS.md');
512
+ summary.push('AGENTS.md');
448
513
  }
449
514
  const agentsTemplatePath = path.join(targetDir, 'AGENTS.template.md');
450
515
  if (fs.existsSync(agentsTemplatePath)) {
451
- files.push('AGENTS.template.md');
516
+ summary.push('AGENTS.template.md');
452
517
  }
453
- return files;
518
+ return summary;
454
519
  }
455
520
  export { ExitCode } from '../utils/exit-codes.js';
456
521
  export { OutputFormatter, createFormatter, parseOutputFormat } from '../utils/output.js';
522
+ 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,21 +14,45 @@ 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
- console.log(`Installing WordPress Agent Kit (${platform}) into: ${targetDir}`);
25
48
  if (!fs.existsSync(targetDir)) {
26
49
  fs.mkdirSync(targetDir, { recursive: true });
27
50
  }
28
51
  const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
29
52
  const agentsPath = path.join(PACKAGE_ROOT, 'AGENTS.md');
30
53
  const sourceGithub = path.join(PACKAGE_ROOT, '.github');
54
+ const filesCreated = [];
55
+ const filesSkipped = [];
31
56
  // Copy platform-specific folder
32
57
  const targetPlatform = path.join(targetDir, platformFolder);
33
58
  if (fs.existsSync(targetPlatform)) {
@@ -39,18 +64,99 @@ export async function installKit(targetDir, platform = 'github') {
39
64
  else {
40
65
  throw new Error('Could not find source .github directory.');
41
66
  }
42
- // Copy AGENTS.md
67
+ // Collect created files
68
+ const collectFiles = (dir, prefix) => {
69
+ if (!fs.existsSync(dir))
70
+ return;
71
+ const entries = fs.readdirSync(dir);
72
+ for (const entry of entries) {
73
+ const fullPath = path.join(dir, entry);
74
+ const stat = fs.statSync(fullPath);
75
+ if (stat.isDirectory()) {
76
+ collectFiles(fullPath, path.join(prefix, entry));
77
+ }
78
+ else {
79
+ filesCreated.push(path.join(prefix, entry));
80
+ }
81
+ }
82
+ };
83
+ collectFiles(targetPlatform, platformFolder);
84
+ // Copy AGENTS.md template
43
85
  const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
44
86
  if (fs.existsSync(templatePath)) {
45
87
  fs.copyFileSync(templatePath, targetAgentsTemplate);
88
+ filesCreated.push('AGENTS.template.md');
46
89
  }
90
+ // Copy AGENTS.md (only if it doesn't already exist)
47
91
  const targetAgents = path.join(targetDir, 'AGENTS.md');
48
92
  if (!fs.existsSync(targetAgents)) {
49
93
  if (fs.existsSync(templatePath)) {
50
94
  fs.copyFileSync(templatePath, targetAgents);
95
+ filesCreated.push('AGENTS.md');
51
96
  }
52
97
  else if (fs.existsSync(agentsPath)) {
53
98
  fs.copyFileSync(agentsPath, targetAgents);
99
+ filesCreated.push('AGENTS.md');
54
100
  }
55
101
  }
102
+ else {
103
+ filesSkipped.push('AGENTS.md (already exists, preserved)');
104
+ }
105
+ return {
106
+ targetDir,
107
+ platform,
108
+ filesCreated,
109
+ filesSkipped,
110
+ errors: [],
111
+ isUpdate: false,
112
+ backupDir: null,
113
+ };
114
+ }
115
+ /**
116
+ * Safe update install using the updater module.
117
+ */
118
+ function safeUpdateInstall(targetDir, platform, options) {
119
+ const updateOptions = {
120
+ targetDir,
121
+ platform,
122
+ force: options.force ?? false,
123
+ backup: options.backup ?? true,
124
+ };
125
+ const result = updateKit(updateOptions);
126
+ // Handle AGENTS.md separately (not part of the platform folder)
127
+ const filesSkipped = [];
128
+ const filesCreated = [...result.created];
129
+ const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
130
+ const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
131
+ if (fs.existsSync(templatePath)) {
132
+ fs.copyFileSync(templatePath, targetAgentsTemplate);
133
+ // Don't add template to created list as it's always overwritten
134
+ }
135
+ const targetAgents = path.join(targetDir, 'AGENTS.md');
136
+ if (!fs.existsSync(targetAgents)) {
137
+ fs.copyFileSync(templatePath, targetAgents);
138
+ filesCreated.push('AGENTS.md (from template)');
139
+ }
140
+ else {
141
+ filesSkipped.push('AGENTS.md (preserved)');
142
+ }
143
+ return {
144
+ targetDir,
145
+ platform,
146
+ filesCreated: [...result.created, ...result.updated].map((f) => path.join(PLATFORM_FOLDERS[platform], f)),
147
+ filesSkipped: [
148
+ ...filesSkipped,
149
+ ...result.skipped.map((f) => {
150
+ const filePath = path.join(PLATFORM_FOLDERS[platform], f);
151
+ return `${filePath} (skipped - not tracked or user modified)`;
152
+ }),
153
+ ],
154
+ conflicts: result.conflicts.map((f) => {
155
+ const filePath = path.join(PLATFORM_FOLDERS[platform], f);
156
+ return `${filePath} (conflict - user modified, use --force to overwrite)`;
157
+ }),
158
+ errors: [],
159
+ isUpdate: true,
160
+ backupDir: result.backupDir,
161
+ };
56
162
  }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Safe update logic for WordPress Agent Kit installations.
3
+ * Tracks file origins and detects user modifications to avoid overwriting custom work.
4
+ */
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { PACKAGE_ROOT } from '../utils/paths.js';
9
+ import { PLATFORM_FOLDERS } from './installer.js';
10
+ /** Get the platform folder for a target */
11
+ export function getPlatformTarget(targetDir, platform) {
12
+ const folder = PLATFORM_FOLDERS[platform];
13
+ return path.join(targetDir, folder);
14
+ }
15
+ /** Get the manifest file path */
16
+ function getManifestPath(targetDir, platform) {
17
+ return path.join(targetDir, `.wp-agent-kit-manifest.${platform}.json`);
18
+ }
19
+ /** Hash a file's content */
20
+ function hashFile(filePath) {
21
+ const content = fs.readFileSync(filePath);
22
+ return crypto.createHash('sha256').update(content).digest('hex');
23
+ }
24
+ /** Walk a directory recursively, returning relative paths */
25
+ function walkDir(dir) {
26
+ const result = [];
27
+ if (!fs.existsSync(dir))
28
+ return result;
29
+ const entries = fs.readdirSync(dir);
30
+ for (const entry of entries) {
31
+ const fullPath = path.join(dir, entry);
32
+ const stat = fs.statSync(fullPath);
33
+ if (stat.isDirectory()) {
34
+ const subPaths = walkDir(fullPath);
35
+ for (const sub of subPaths) {
36
+ result.push(path.join(entry, sub));
37
+ }
38
+ }
39
+ else {
40
+ result.push(entry);
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+ /** Load existing manifest if present */
46
+ export function loadManifest(targetDir, platform) {
47
+ const manifestPath = getManifestPath(targetDir, platform);
48
+ if (!fs.existsSync(manifestPath))
49
+ return null;
50
+ try {
51
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /** Save manifest */
58
+ function saveManifest(targetDir, platform, manifest) {
59
+ const manifestPath = getManifestPath(targetDir, platform);
60
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
61
+ }
62
+ /** Create a backup of target files before overwriting */
63
+ function createBackup(targetDir, platform, files) {
64
+ if (files.length === 0)
65
+ return null;
66
+ const platformFolder = PLATFORM_FOLDERS[platform];
67
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
68
+ const backupDir = path.join(targetDir, `.wp-agent-kit-backup-${timestamp}`);
69
+ for (const file of files) {
70
+ const srcPath = path.join(targetDir, platformFolder, file);
71
+ const destPath = path.join(backupDir, platformFolder, file);
72
+ if (fs.existsSync(srcPath)) {
73
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
74
+ fs.copyFileSync(srcPath, destPath);
75
+ }
76
+ }
77
+ return backupDir;
78
+ }
79
+ /**
80
+ * Compute file changes between current state and what would be installed.
81
+ */
82
+ export function computeChanges(targetDir, platform, force) {
83
+ const existingManifest = loadManifest(targetDir, platform);
84
+ const sourceDir = path.join(PACKAGE_ROOT, '.github');
85
+ const targetPlatform = getPlatformTarget(targetDir, platform);
86
+ const changes = [];
87
+ const sourceFiles = walkDir(sourceDir);
88
+ const targetFiles = fs.existsSync(targetPlatform) ? walkDir(targetPlatform) : [];
89
+ // Build lookup of known files from manifest
90
+ const knownFiles = new Map();
91
+ if (existingManifest) {
92
+ for (const entry of existingManifest.files) {
93
+ knownFiles.set(entry.path, entry.hash);
94
+ }
95
+ }
96
+ // Process source files
97
+ for (const sourceFile of sourceFiles) {
98
+ const targetPath = path.join(targetPlatform, sourceFile);
99
+ const sourceHash = hashFile(path.join(sourceDir, sourceFile));
100
+ if (!fs.existsSync(targetPath)) {
101
+ changes.push({
102
+ relativePath: sourceFile,
103
+ action: 'created',
104
+ reason: 'New file from kit',
105
+ });
106
+ continue;
107
+ }
108
+ const targetHash = hashFile(targetPath);
109
+ if (sourceHash === targetHash) {
110
+ // Identical - no change needed
111
+ changes.push({
112
+ relativePath: sourceFile,
113
+ action: 'unchanged',
114
+ reason: 'Content identical',
115
+ });
116
+ }
117
+ else if (knownFiles.has(sourceFile)) {
118
+ const manifestHash = knownFiles.get(sourceFile);
119
+ if (targetHash === manifestHash) {
120
+ // Same as original from manifest, safe to update
121
+ changes.push({
122
+ relativePath: sourceFile,
123
+ action: 'updated',
124
+ reason: 'Safe update (no user modification)',
125
+ });
126
+ }
127
+ else if (force) {
128
+ changes.push({
129
+ relativePath: sourceFile,
130
+ action: 'updated',
131
+ reason: 'Force update (overwriting user modification)',
132
+ });
133
+ }
134
+ else {
135
+ changes.push({
136
+ relativePath: sourceFile,
137
+ action: 'conflict',
138
+ reason: 'User modified; skipped. Use --force to overwrite.',
139
+ });
140
+ }
141
+ }
142
+ else {
143
+ // Not in manifest (pre-manifest install or manual add), but exists
144
+ if (force) {
145
+ changes.push({
146
+ relativePath: sourceFile,
147
+ action: 'updated',
148
+ reason: 'Force update (file not tracked in manifest)',
149
+ });
150
+ }
151
+ else {
152
+ changes.push({
153
+ relativePath: sourceFile,
154
+ action: 'skipped',
155
+ reason: 'File exists but not tracked; skipped. Use --force to overwrite.',
156
+ });
157
+ }
158
+ }
159
+ }
160
+ // Note user-added files not in source (these are kept, not reported as changes)
161
+ const sourceSet = new Set(sourceFiles);
162
+ for (const targetFile of targetFiles) {
163
+ if (!sourceSet.has(targetFile)) {
164
+ changes.push({
165
+ relativePath: targetFile,
166
+ action: 'unchanged',
167
+ reason: 'User-added file (preserved)',
168
+ });
169
+ }
170
+ }
171
+ return changes;
172
+ }
173
+ /**
174
+ * Perform a safe update of the WordPress Agent Kit in the target directory.
175
+ * Compares files against manifest and source to avoid overwriting user modifications.
176
+ */
177
+ export function updateKit(options) {
178
+ const { targetDir, platform, force = false, backup = true } = options;
179
+ const changes = computeChanges(targetDir, platform, force);
180
+ const sourceDir = path.join(PACKAGE_ROOT, '.github');
181
+ const targetPlatform = getPlatformTarget(targetDir, platform);
182
+ const created = [];
183
+ const updated = [];
184
+ const skipped = [];
185
+ const conflicts = [];
186
+ // Determine which files will be overwritten (for backup)
187
+ const filesToBackup = changes.filter((c) => c.action === 'updated').map((c) => c.relativePath);
188
+ let backupDir = null;
189
+ if (backup && filesToBackup.length > 0) {
190
+ backupDir = createBackup(targetDir, platform, filesToBackup);
191
+ }
192
+ // Apply changes
193
+ for (const change of changes) {
194
+ const sourcePath = path.join(sourceDir, change.relativePath);
195
+ const targetPath = path.join(targetPlatform, change.relativePath);
196
+ switch (change.action) {
197
+ case 'created':
198
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
199
+ fs.copyFileSync(sourcePath, targetPath);
200
+ created.push(change.relativePath);
201
+ break;
202
+ case 'updated':
203
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
204
+ fs.copyFileSync(sourcePath, targetPath);
205
+ updated.push(change.relativePath);
206
+ break;
207
+ case 'skipped':
208
+ skipped.push(change.relativePath);
209
+ break;
210
+ case 'conflict':
211
+ conflicts.push(change.relativePath);
212
+ break;
213
+ // 'unchanged' - do nothing
214
+ }
215
+ }
216
+ // Build and save new manifest
217
+ const newManifest = {
218
+ version: getPackageVersion(),
219
+ platform,
220
+ installedAt: new Date().toISOString(),
221
+ files: walkDir(sourceDir).map((file) => ({
222
+ path: file,
223
+ hash: hashFile(path.join(sourceDir, file)),
224
+ })),
225
+ };
226
+ saveManifest(targetDir, platform, newManifest);
227
+ return {
228
+ targetDir,
229
+ platform,
230
+ changes,
231
+ created,
232
+ updated,
233
+ skipped,
234
+ conflicts,
235
+ backupDir,
236
+ manifestUpdated: true,
237
+ };
238
+ }
239
+ /** Get current package version */
240
+ function getPackageVersion() {
241
+ try {
242
+ const pkgPath = path.join(PACKAGE_ROOT, 'package.json');
243
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
244
+ return pkg.version;
245
+ }
246
+ catch {
247
+ return 'unknown';
248
+ }
249
+ }
250
+ /**
251
+ * Check if a directory has a WordPress Agent Kit installation.
252
+ */
253
+ export function isKitInstalled(targetDir, platform) {
254
+ const manifestPath = getManifestPath(targetDir, platform);
255
+ if (fs.existsSync(manifestPath))
256
+ return true;
257
+ const platformFolder = PLATFORM_FOLDERS[platform];
258
+ const targetPlatform = path.join(targetDir, platformFolder);
259
+ return fs.existsSync(targetPlatform);
260
+ }