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.
- package/.github/agents/wp-architect.agent.md +1 -0
- package/.github/skills/blueprint/SKILL.md +418 -0
- package/.github/skills/wordpress-router/SKILL.md +1 -0
- package/.github/skills/wp-abilities-api/SKILL.md +13 -0
- package/.github/skills/wp-abilities-api/references/delegate-helper-pattern.md +241 -0
- package/.github/skills/wp-abilities-api/references/domain-vs-projection.md +113 -0
- package/.github/skills/wp-abilities-api/references/error-code-vocabulary.md +123 -0
- package/.github/skills/wp-abilities-api/references/grouping-heuristic.md +89 -0
- package/.github/skills/wp-abilities-api/references/input-schema-gotchas.md +265 -0
- package/.github/skills/wp-abilities-api/references/php-registration.md +47 -20
- package/.github/skills/wp-abilities-api/references/plugin-family-patterns.md +233 -0
- package/.github/skills/wp-abilities-api/references/shared-core-service.md +184 -0
- package/.github/skills/wp-abilities-audit/SKILL.md +199 -0
- package/.github/skills/wp-abilities-audit/references/audit-schema.md +300 -0
- package/.github/skills/wp-abilities-audit/references/capability-gate-tracing.md +197 -0
- package/.github/skills/wp-abilities-audit/references/controller-enumeration.md +116 -0
- package/.github/skills/wp-abilities-verify/SKILL.md +215 -0
- package/.github/skills/wp-abilities-verify/references/annotation-correctness.md +154 -0
- package/.github/skills/wp-abilities-verify/references/audit-schema-validation.md +131 -0
- package/.github/skills/wp-abilities-verify/references/permission-roundtrip.md +190 -0
- package/.github/skills/wp-abilities-verify/references/runtime-harness.md +462 -0
- package/.github/skills/wp-abilities-verify/references/schema-lints.md +118 -0
- package/.github/skills/wp-abilities-verify/references/static-enumeration.md +126 -0
- package/.github/skills/wp-block-development/SKILL.md +1 -0
- package/.github/skills/wp-block-themes/SKILL.md +1 -0
- package/.github/skills/wp-interactivity-api/SKILL.md +1 -0
- package/.github/skills/wp-performance/SKILL.md +1 -0
- package/.github/skills/wp-phpstan/SKILL.md +1 -0
- package/.github/skills/wp-playground/SKILL.md +1 -0
- package/.github/skills/wp-plugin-development/SKILL.md +1 -0
- package/.github/skills/wp-plugin-directory-guidelines/SKILL.md +133 -0
- package/.github/skills/wp-plugin-directory-guidelines/references/gpl-compliance.md +217 -0
- package/.github/skills/wp-plugin-directory-guidelines/references/guideline-review-checklist.md +592 -0
- package/.github/skills/wp-plugin-directory-guidelines/references/naming-rules.md +121 -0
- package/.github/skills/wp-project-triage/SKILL.md +1 -0
- package/.github/skills/wp-project-triage/scripts/detect_wp_project.mjs +22 -4
- package/.github/skills/wp-rest-api/SKILL.md +1 -0
- package/.github/skills/wp-wpcli-and-ops/SKILL.md +1 -0
- package/.github/skills/wpds/SKILL.md +1 -0
- package/AGENTS.md +33 -10
- package/AGENTS.template.md +63 -18
- package/README.md +226 -124
- package/biome.json +1 -1
- package/dist/commands/install.js +47 -6
- package/dist/commands/upgrade.js +34 -5
- package/dist/lib/api.js +93 -27
- package/dist/lib/installer.js +113 -7
- package/dist/lib/updater.js +260 -0
- package/extensions/wp-agent-kit/index.ts +452 -0
- package/package.json +21 -3
- 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
|
-
|
|
25
|
+
installKit(targetDir, platform, { force, safe, backup });
|
|
25
26
|
return { success: true };
|
|
26
27
|
});
|
|
27
|
-
const filesCreated =
|
|
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,
|
|
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
|
|
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
|
|
476
|
+
* Get summary of installed files grouped by directory.
|
|
423
477
|
*/
|
|
424
|
-
function
|
|
478
|
+
function getInstalledSummary(targetDir, platform) {
|
|
425
479
|
const platformFolder = getPlatformFolder(platform);
|
|
426
|
-
const
|
|
480
|
+
const summary = [];
|
|
427
481
|
const targetPlatform = path.join(targetDir, platformFolder);
|
|
428
482
|
if (fs.existsSync(targetPlatform)) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
512
|
+
summary.push('AGENTS.md');
|
|
448
513
|
}
|
|
449
514
|
const agentsTemplatePath = path.join(targetDir, 'AGENTS.template.md');
|
|
450
515
|
if (fs.existsSync(agentsTemplatePath)) {
|
|
451
|
-
|
|
516
|
+
summary.push('AGENTS.template.md');
|
|
452
517
|
}
|
|
453
|
-
return
|
|
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';
|
package/dist/lib/installer.js
CHANGED
|
@@ -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
|
-
*
|
|
17
|
+
* If the kit is already installed, uses safe update logic to preserve user modifications.
|
|
17
18
|
*
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
20
|
-
* @returns
|
|
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
|
|
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
|
-
//
|
|
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
|
+
}
|