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
@@ -15,14 +15,14 @@ export function mapProjectType(primary) {
15
15
  'wp-theme': 'theme',
16
16
  'wp-site': 'site',
17
17
  'wp-core': 'other',
18
- 'gutenberg': 'blocks',
19
- 'unknown': null,
18
+ gutenberg: 'blocks',
19
+ unknown: null,
20
20
  };
21
21
  return typeMap[primary] || null;
22
22
  }
23
23
  /**
24
24
  * Maps triage signals and tooling to tech stack array
25
- * @param {object} triageResult - Full triage result object
25
+ * @param {TriageResult} triageResult - Full triage result object
26
26
  * @returns {string[]} - Array of tech stack values
27
27
  */
28
28
  export function mapTechStack(triageResult) {
@@ -60,39 +60,37 @@ export function mapTechStack(triageResult) {
60
60
  /**
61
61
  * Checks if detection has enough confidence to skip questions
62
62
  * @param {string|null} detectedType - Detected project type
63
- * @param {string[]} detectedTech - Detected tech stack
64
63
  * @returns {boolean} - True if confident enough
65
64
  */
66
- export function hasConfidentDetection(detectedType, detectedTech) {
65
+ export function hasConfidentDetection(detectedType) {
67
66
  return detectedType !== null && detectedType !== 'other';
68
67
  }
69
68
  /**
70
69
  * Formats detection results for display
71
70
  * @param {string|null} detectedType - Detected project type
72
71
  * @param {string[]} detectedTech - Detected tech stack
73
- * @param {object} triageResult - Full triage result for additional notes
74
72
  * @returns {string} - Formatted string for display
75
73
  */
76
- export function formatDetectionResults(detectedType, detectedTech, triageResult) {
74
+ export function formatDetectionResults(detectedType, detectedTech) {
77
75
  const typeLabels = {
78
- 'plugin': 'WordPress Plugin',
79
- 'theme': 'WordPress Theme',
76
+ plugin: 'WordPress Plugin',
77
+ theme: 'WordPress Theme',
80
78
  'block-theme': 'Block Theme',
81
- 'site': 'Full Site / Multisite',
82
- 'blocks': 'Gutenberg Blocks',
83
- 'other': 'Other / Mixed',
79
+ site: 'Full Site / Multisite',
80
+ blocks: 'Gutenberg Blocks',
81
+ other: 'Other / Mixed',
84
82
  };
85
83
  const techLabels = {
86
- 'gutenberg': 'Blocks',
87
- 'interactivity': 'Interactivity API',
88
- 'wpcli': 'WP-CLI',
84
+ gutenberg: 'Blocks',
85
+ interactivity: 'Interactivity API',
86
+ wpcli: 'WP-CLI',
89
87
  'rest-api': 'REST API',
90
- 'composer': 'Composer',
91
- 'phpstan': 'PHPStan',
92
- 'npm': 'npm/package.json',
93
- 'playground': 'Playground',
88
+ composer: 'Composer',
89
+ phpstan: 'PHPStan',
90
+ npm: 'npm/package.json',
91
+ playground: 'Playground',
94
92
  };
95
93
  const typeLabel = detectedType ? typeLabels[detectedType] : 'Unknown';
96
- const techList = detectedTech.map(t => techLabels[t] || t).join(', ');
94
+ const techList = detectedTech.map((t) => techLabels[t] || t).join(', ');
97
95
  return `Project Type: ${typeLabel}\nTech Stack: ${techList}`;
98
96
  }
@@ -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
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Semantic exit codes for the CLI.
3
+ * Allows scripts and agents to programmatically determine failure reasons.
4
+ */
5
+ export const ExitCode = {
6
+ /** Success */
7
+ OK: 0,
8
+ /** General/unknown error */
9
+ ERROR: 1,
10
+ /** Invalid command-line arguments or usage */
11
+ INVALID_ARGS: 2,
12
+ /** Target not found (ENOENT) */
13
+ NOT_FOUND: 3,
14
+ /** Permission denied (EACCES) */
15
+ PERMISSION_DENIED: 4,
16
+ /** File/directory already exists (EEXIST) - use --force to override */
17
+ ALREADY_EXISTS: 5,
18
+ /** Git/submodule operation failed */
19
+ GIT_ERROR: 6,
20
+ /** Network/fetch operation failed */
21
+ NETWORK_ERROR: 7,
22
+ /** Validation failed (schema, input, config) */
23
+ VALIDATION_ERROR: 8,
24
+ /** Cancelled by user (SIGINT) */
25
+ CANCELLED: 130,
26
+ };
27
+ /**
28
+ * Maps Node.js errno to semantic exit code.
29
+ */
30
+ export function mapErrnoToExitCode(errno) {
31
+ switch (errno) {
32
+ case 'ENOENT':
33
+ return ExitCode.NOT_FOUND;
34
+ case 'EACCES':
35
+ case 'EPERM':
36
+ return ExitCode.PERMISSION_DENIED;
37
+ case 'EEXIST':
38
+ return ExitCode.ALREADY_EXISTS;
39
+ default:
40
+ return ExitCode.ERROR;
41
+ }
42
+ }
43
+ /**
44
+ * Wraps an async operation and maps errors to exit codes.
45
+ * Re-throws with exitCode property attached.
46
+ */
47
+ export async function withExitCode(operation, onError) {
48
+ try {
49
+ return await operation();
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ const exitCode = err.exitCode ?? mapErrnoToExitCode(err.code);
54
+ const enhancedError = Object.assign(err, { exitCode });
55
+ if (onError) {
56
+ onError(enhancedError);
57
+ }
58
+ throw enhancedError;
59
+ }
60
+ }
@@ -0,0 +1,96 @@
1
+ import { ExitCode } from './exit-codes.js';
2
+ /** Output formatter handles all CLI output modes */
3
+ export class OutputFormatter {
4
+ format;
5
+ startTime;
6
+ commandName;
7
+ version;
8
+ ndjsonStarted = false;
9
+ constructor(format, commandName, version) {
10
+ this.format = format;
11
+ this.startTime = Date.now();
12
+ this.commandName = commandName;
13
+ this.version = version;
14
+ }
15
+ /** Set output format at runtime */
16
+ setFormat(format) {
17
+ this.format = format;
18
+ }
19
+ /** Build standard result envelope */
20
+ buildResult(success, data, error) {
21
+ return {
22
+ success,
23
+ data,
24
+ error,
25
+ meta: {
26
+ durationMs: Date.now() - this.startTime,
27
+ timestamp: new Date().toISOString(),
28
+ version: this.version,
29
+ command: this.commandName,
30
+ },
31
+ };
32
+ }
33
+ /** Output success result */
34
+ success(data) {
35
+ const result = this.buildResult(true, data);
36
+ this.emit(result);
37
+ return result;
38
+ }
39
+ /** Output error result */
40
+ fail(error) {
41
+ const result = this.buildResult(false, undefined, error);
42
+ this.emit(result);
43
+ return result;
44
+ }
45
+ /** Emit NDJSON progress event */
46
+ progress(event) {
47
+ if (this.format !== 'ndjson')
48
+ return;
49
+ const fullEvent = {
50
+ ...event,
51
+ timestamp: new Date().toISOString(),
52
+ };
53
+ console.log(JSON.stringify(fullEvent));
54
+ }
55
+ /** Start NDJSON stream */
56
+ startStream() {
57
+ if (this.format === 'ndjson') {
58
+ this.ndjsonStarted = true;
59
+ }
60
+ }
61
+ /** Emit raw object (used for JSON mode) */
62
+ emit(obj) {
63
+ switch (this.format) {
64
+ case 'json':
65
+ case 'ndjson':
66
+ console.log(JSON.stringify(obj));
67
+ break;
68
+ case 'human':
69
+ // Human mode: commands should handle their own console output
70
+ // This is a no-op for structured results
71
+ break;
72
+ case 'quiet':
73
+ // Suppress all output
74
+ break;
75
+ }
76
+ }
77
+ /** Get exit code from result */
78
+ static getExitCode(result) {
79
+ return result.success ? ExitCode.OK : (result.error?.exitCode ?? ExitCode.ERROR);
80
+ }
81
+ }
82
+ /** Parse output format from CLI flags */
83
+ export function parseOutputFormat(json, quiet, ndjson) {
84
+ if (json)
85
+ return 'json';
86
+ if (ndjson)
87
+ return 'ndjson';
88
+ if (quiet)
89
+ return 'quiet';
90
+ return 'human';
91
+ }
92
+ /** Create formatter from parsed options */
93
+ export function createFormatter(options, commandName, version) {
94
+ const format = parseOutputFormat(options.json, options.quiet, options.ndjson);
95
+ return new OutputFormatter(format, commandName, version);
96
+ }
@@ -1,5 +1,5 @@
1
- import { fileURLToPath } from 'node:url';
2
1
  import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
3
  // Get the root directory of the package (where package.json lives)
4
4
  // When running from src (ts-node), it's ../
5
5
  // When running from dist (node), it's ../
package/dist/utils/run.js CHANGED
@@ -11,7 +11,7 @@ export function run(command, args, cwd = process.cwd()) {
11
11
  const result = spawnSync(command, args, {
12
12
  cwd,
13
13
  stdio: 'inherit',
14
- shell: process.platform === 'win32'
14
+ shell: process.platform === 'win32',
15
15
  });
16
16
  if (result.status !== 0) {
17
17
  console.error(`Command failed with status ${result.status}: ${command} ${args.join(' ')}`);