wordpress-agent-kit 0.2.1 → 0.2.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/workflows/ci.yml +44 -0
- package/.husky/pre-commit +7 -0
- package/CLI_REVIEW.md +250 -0
- package/README.md +124 -51
- package/biome.json +39 -0
- package/dist/cli.js +75 -4
- package/dist/commands/install.js +43 -10
- package/dist/commands/run-playground.js +59 -14
- package/dist/commands/setup.js +222 -163
- package/dist/commands/sync-skills.js +33 -60
- package/dist/commands/upgrade.js +182 -0
- package/dist/lib/api.js +456 -0
- package/dist/lib/triage-mapper.js +18 -20
- package/dist/utils/exit-codes.js +60 -0
- package/dist/utils/output.js +96 -0
- package/dist/utils/paths.js +1 -1
- package/dist/utils/run.js +1 -1
- package/kit-learnings.md +192 -0
- package/package.json +7 -2
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Install the WordPress Agent Kit programmatically.
|
|
14
|
+
*/
|
|
15
|
+
export async function installKitApi(options) {
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
const formatter = new OutputFormatter('json', 'install', '0.0.0');
|
|
18
|
+
try {
|
|
19
|
+
const { targetDir, platform, force = false, dryRun = false } = options;
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
return dryRunInstall(targetDir, platform, force);
|
|
22
|
+
}
|
|
23
|
+
await withExitCode(async () => {
|
|
24
|
+
await installKit(targetDir, platform);
|
|
25
|
+
return { success: true };
|
|
26
|
+
});
|
|
27
|
+
const filesCreated = getInstalledFiles(targetDir, platform);
|
|
28
|
+
const durationMs = Date.now() - startTime;
|
|
29
|
+
return formatter.success({
|
|
30
|
+
targetDir,
|
|
31
|
+
platform,
|
|
32
|
+
filesCreated,
|
|
33
|
+
filesSkipped: [],
|
|
34
|
+
errors: [],
|
|
35
|
+
durationMs,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const err = error;
|
|
40
|
+
return formatter.fail({
|
|
41
|
+
code: err.code || 'INSTALL_FAILED',
|
|
42
|
+
message: err.message || 'Installation failed',
|
|
43
|
+
exitCode: err.exitCode ?? ExitCode.ERROR,
|
|
44
|
+
details: { platform: options.platform, targetDir: options.targetDir },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Dry-run preview for install.
|
|
50
|
+
*/
|
|
51
|
+
function dryRunInstall(targetDir, platform, force) {
|
|
52
|
+
const platformFolder = getPlatformFolder(platform);
|
|
53
|
+
const actions = [];
|
|
54
|
+
const sourceGithub = path.join(PACKAGE_ROOT, '.github');
|
|
55
|
+
const targetPlatform = path.join(targetDir, platformFolder);
|
|
56
|
+
const templatePath = path.join(PACKAGE_ROOT, 'AGENTS.template.md');
|
|
57
|
+
const targetAgentsTemplate = path.join(targetDir, 'AGENTS.template.md');
|
|
58
|
+
const targetAgents = path.join(targetDir, 'AGENTS.md');
|
|
59
|
+
// Platform folder
|
|
60
|
+
if (fs.existsSync(sourceGithub)) {
|
|
61
|
+
if (fs.existsSync(targetPlatform) && !force) {
|
|
62
|
+
actions.push({
|
|
63
|
+
type: 'update',
|
|
64
|
+
target: targetPlatform,
|
|
65
|
+
description: `Would update ${platformFolder} (use --force to overwrite)`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
actions.push({
|
|
70
|
+
type: 'copy',
|
|
71
|
+
source: sourceGithub,
|
|
72
|
+
target: targetPlatform,
|
|
73
|
+
description: `Copy ${platformFolder} from kit`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// AGENTS.template.md
|
|
78
|
+
if (fs.existsSync(templatePath)) {
|
|
79
|
+
actions.push({
|
|
80
|
+
type: 'copy',
|
|
81
|
+
source: templatePath,
|
|
82
|
+
target: targetAgentsTemplate,
|
|
83
|
+
description: 'Copy AGENTS.template.md',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// AGENTS.md
|
|
87
|
+
if (!fs.existsSync(targetAgents) || force) {
|
|
88
|
+
if (fs.existsSync(templatePath)) {
|
|
89
|
+
actions.push({
|
|
90
|
+
type: 'copy',
|
|
91
|
+
source: templatePath,
|
|
92
|
+
target: targetAgents,
|
|
93
|
+
description: 'Create AGENTS.md from template',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
actions.push({
|
|
99
|
+
type: 'update',
|
|
100
|
+
target: targetAgents,
|
|
101
|
+
description: 'AGENTS.md exists (use --force to overwrite)',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return new OutputFormatter('json', 'install', '0.0.0').success({
|
|
105
|
+
wouldExecute: true,
|
|
106
|
+
actions,
|
|
107
|
+
summary: {
|
|
108
|
+
targetDir,
|
|
109
|
+
platform,
|
|
110
|
+
filesCreated: actions.filter((a) => a.type === 'copy').map((a) => a.target),
|
|
111
|
+
filesSkipped: actions.filter((a) => a.type === 'update').map((a) => a.target),
|
|
112
|
+
errors: [],
|
|
113
|
+
durationMs: 0,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Sync skills from WordPress/agent-skills programmatically.
|
|
119
|
+
*/
|
|
120
|
+
export async function syncSkillsApi(options = {}) {
|
|
121
|
+
const startTime = Date.now();
|
|
122
|
+
const formatter = new OutputFormatter('json', 'sync-skills', '0.0.0');
|
|
123
|
+
const targetDir = options.targetDir || process.cwd();
|
|
124
|
+
const ref = options.ref || 'trunk';
|
|
125
|
+
if (options.dryRun) {
|
|
126
|
+
return dryRunSyncSkills(targetDir, ref);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const result = await withExitCode(async () => {
|
|
130
|
+
const repoRoot = targetDir;
|
|
131
|
+
const submodulePath = path.join('vendor', 'wp-agent-skills');
|
|
132
|
+
const vendorSkillsDir = path.join(repoRoot, submodulePath);
|
|
133
|
+
const submoduleGitDir = path.join(vendorSkillsDir, '.git');
|
|
134
|
+
// Clone or update
|
|
135
|
+
if (!fs.existsSync(submoduleGitDir)) {
|
|
136
|
+
fs.mkdirSync(path.join(repoRoot, 'vendor'), { recursive: true });
|
|
137
|
+
const cloneResult = spawnSync('git', ['clone', 'https://github.com/WordPress/agent-skills.git', submodulePath], {
|
|
138
|
+
cwd: repoRoot,
|
|
139
|
+
encoding: 'utf-8',
|
|
140
|
+
});
|
|
141
|
+
if (cloneResult.status !== 0) {
|
|
142
|
+
throw new Error(`Git clone failed: ${cloneResult.stderr?.toString()}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const fetchResult = spawnSync('git', ['fetch', '--all', '--tags'], {
|
|
147
|
+
cwd: vendorSkillsDir,
|
|
148
|
+
encoding: 'utf-8',
|
|
149
|
+
});
|
|
150
|
+
if (fetchResult.status !== 0) {
|
|
151
|
+
throw new Error(`Git fetch failed: ${fetchResult.stderr?.toString()}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Checkout ref
|
|
155
|
+
const checkoutResult = spawnSync('git', ['checkout', ref], {
|
|
156
|
+
cwd: vendorSkillsDir,
|
|
157
|
+
encoding: 'utf-8',
|
|
158
|
+
});
|
|
159
|
+
if (checkoutResult.status !== 0) {
|
|
160
|
+
throw new Error(`Git checkout failed: ${checkoutResult.stderr?.toString()}`);
|
|
161
|
+
}
|
|
162
|
+
const pullResult = spawnSync('git', ['pull', 'origin', ref], {
|
|
163
|
+
cwd: vendorSkillsDir,
|
|
164
|
+
encoding: 'utf-8',
|
|
165
|
+
});
|
|
166
|
+
if (pullResult.status !== 0) {
|
|
167
|
+
throw new Error(`Git pull failed: ${pullResult.stderr?.toString()}`);
|
|
168
|
+
}
|
|
169
|
+
const targetSkills = path.join(repoRoot, '.github', 'skills');
|
|
170
|
+
const upstreamBuildScript = path.join(vendorSkillsDir, 'shared', 'scripts', 'skillpack-build.mjs');
|
|
171
|
+
const upstreamInstallScript = path.join(vendorSkillsDir, 'shared', 'scripts', 'skillpack-install.mjs');
|
|
172
|
+
let method = 'direct-copy';
|
|
173
|
+
let skillsSynced = 0;
|
|
174
|
+
if (fs.existsSync(upstreamBuildScript) && fs.existsSync(upstreamInstallScript)) {
|
|
175
|
+
if (fs.existsSync(targetSkills)) {
|
|
176
|
+
fs.rmSync(targetSkills, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
fs.mkdirSync(path.join(repoRoot, '.github'), { recursive: true });
|
|
179
|
+
const buildResult = spawnSync('node', ['shared/scripts/skillpack-build.mjs', '--clean', '--targets=vscode'], {
|
|
180
|
+
cwd: vendorSkillsDir,
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
});
|
|
183
|
+
if (buildResult.status !== 0) {
|
|
184
|
+
throw new Error(`Skillpack build failed: ${buildResult.stderr?.toString()}`);
|
|
185
|
+
}
|
|
186
|
+
const installResult = spawnSync('node', [
|
|
187
|
+
'shared/scripts/skillpack-install.mjs',
|
|
188
|
+
`--dest=${repoRoot}`,
|
|
189
|
+
'--targets=vscode',
|
|
190
|
+
'--from=dist',
|
|
191
|
+
'--mode=replace',
|
|
192
|
+
], { cwd: vendorSkillsDir, encoding: 'utf-8' });
|
|
193
|
+
if (installResult.status !== 0) {
|
|
194
|
+
throw new Error(`Skillpack install failed: ${installResult.stderr?.toString()}`);
|
|
195
|
+
}
|
|
196
|
+
method = 'skillpack';
|
|
197
|
+
if (fs.existsSync(targetSkills)) {
|
|
198
|
+
skillsSynced = fs.readdirSync(targetSkills).length;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const sourceSkills = path.join(vendorSkillsDir, '.github', 'skills');
|
|
203
|
+
if (!fs.existsSync(sourceSkills)) {
|
|
204
|
+
throw new Error(`Upstream skills not found at ${sourceSkills}`);
|
|
205
|
+
}
|
|
206
|
+
if (fs.existsSync(targetSkills)) {
|
|
207
|
+
fs.rmSync(targetSkills, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
fs.mkdirSync(path.join(repoRoot, '.github'), { recursive: true });
|
|
210
|
+
fs.cpSync(sourceSkills, targetSkills, { recursive: true });
|
|
211
|
+
skillsSynced = fs.readdirSync(targetSkills).length;
|
|
212
|
+
}
|
|
213
|
+
return { success: true, skillsSynced, method };
|
|
214
|
+
});
|
|
215
|
+
return formatter.success({
|
|
216
|
+
targetDir,
|
|
217
|
+
skillsSynced: result.skillsSynced,
|
|
218
|
+
sourceUrl: 'https://github.com/WordPress/agent-skills.git',
|
|
219
|
+
ref,
|
|
220
|
+
durationMs: Date.now() - startTime,
|
|
221
|
+
method: result.method,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const err = error;
|
|
226
|
+
return formatter.fail({
|
|
227
|
+
code: err.code || 'SYNC_FAILED',
|
|
228
|
+
message: err.message || 'Sync failed',
|
|
229
|
+
exitCode: err.exitCode ?? ExitCode.ERROR,
|
|
230
|
+
details: { ref, targetDir },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Dry-run preview for sync-skills.
|
|
236
|
+
*/
|
|
237
|
+
function dryRunSyncSkills(targetDir, ref) {
|
|
238
|
+
const actions = [];
|
|
239
|
+
const targetSkills = path.join(targetDir, '.github', 'skills');
|
|
240
|
+
const vendorDir = path.join(targetDir, 'vendor', 'wp-agent-skills');
|
|
241
|
+
actions.push({
|
|
242
|
+
type: 'mkdir',
|
|
243
|
+
target: path.join(targetDir, 'vendor'),
|
|
244
|
+
description: 'Create vendor directory',
|
|
245
|
+
});
|
|
246
|
+
if (!fs.existsSync(vendorDir)) {
|
|
247
|
+
actions.push({
|
|
248
|
+
type: 'create',
|
|
249
|
+
target: vendorDir,
|
|
250
|
+
description: 'Clone WordPress/agent-skills repository',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
actions.push({
|
|
255
|
+
type: 'update',
|
|
256
|
+
target: vendorDir,
|
|
257
|
+
description: `Fetch and checkout ${ref}`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (fs.existsSync(targetSkills)) {
|
|
261
|
+
actions.push({
|
|
262
|
+
type: 'delete',
|
|
263
|
+
target: targetSkills,
|
|
264
|
+
description: 'Remove existing skills directory',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
actions.push({
|
|
268
|
+
type: 'create',
|
|
269
|
+
target: targetSkills,
|
|
270
|
+
description: 'Install synced skills',
|
|
271
|
+
});
|
|
272
|
+
return new OutputFormatter('json', 'sync-skills', '0.0.0').success({
|
|
273
|
+
wouldExecute: true,
|
|
274
|
+
actions,
|
|
275
|
+
summary: {
|
|
276
|
+
targetDir,
|
|
277
|
+
skillsSynced: 0,
|
|
278
|
+
sourceUrl: 'https://github.com/WordPress/agent-skills.git',
|
|
279
|
+
ref,
|
|
280
|
+
durationMs: 0,
|
|
281
|
+
method: 'skillpack',
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Run project triage detection programmatically.
|
|
287
|
+
*/
|
|
288
|
+
export async function runTriageApi(options) {
|
|
289
|
+
const formatter = new OutputFormatter('json', 'triage', '0.0.0');
|
|
290
|
+
const { targetDir, platform = 'github' } = options;
|
|
291
|
+
try {
|
|
292
|
+
const platformFolder = getPlatformFolder(platform);
|
|
293
|
+
const triageScriptPaths = [
|
|
294
|
+
path.join(targetDir, platformFolder, 'skills/wp-project-triage/scripts/detect_wp_project.mjs'),
|
|
295
|
+
path.join(PACKAGE_ROOT, 'vendor/wp-agent-skills/skills/wp-project-triage/scripts/detect_wp_project.mjs'),
|
|
296
|
+
];
|
|
297
|
+
const triageScriptPath = triageScriptPaths.find((p) => fs.existsSync(p));
|
|
298
|
+
if (!triageScriptPath) {
|
|
299
|
+
return formatter.fail({
|
|
300
|
+
code: 'TRIAGE_NOT_FOUND',
|
|
301
|
+
message: 'Project triage script not found. Run sync-skills first.',
|
|
302
|
+
exitCode: ExitCode.NOT_FOUND,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const result = spawnSync('node', [triageScriptPath], {
|
|
306
|
+
cwd: targetDir,
|
|
307
|
+
encoding: 'utf-8',
|
|
308
|
+
});
|
|
309
|
+
if (result.status !== 0) {
|
|
310
|
+
return formatter.fail({
|
|
311
|
+
code: 'TRIAGE_FAILED',
|
|
312
|
+
message: result.stderr?.toString() || 'Triage script failed',
|
|
313
|
+
exitCode: ExitCode.ERROR,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const triageResult = JSON.parse(result.stdout.trim());
|
|
317
|
+
return formatter.success(triageResult);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
const err = error;
|
|
321
|
+
return formatter.fail({
|
|
322
|
+
code: 'TRIAGE_ERROR',
|
|
323
|
+
message: err.message || 'Triage failed',
|
|
324
|
+
exitCode: ExitCode.ERROR,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Configure AGENTS.md with project details programmatically.
|
|
330
|
+
*/
|
|
331
|
+
export async function configureAgentsMdApi(options) {
|
|
332
|
+
const formatter = new OutputFormatter('json', 'configure', '0.0.0');
|
|
333
|
+
const { targetDir, platform, config, dryRun = false } = options;
|
|
334
|
+
try {
|
|
335
|
+
const platformFolder = getPlatformFolder(platform);
|
|
336
|
+
const agentsPath = path.join(targetDir, 'AGENTS.md');
|
|
337
|
+
const platformInstructionsPath = path.join(targetDir, platformFolder, 'instructions', 'wordpress-workflow.instructions.md');
|
|
338
|
+
if (dryRun) {
|
|
339
|
+
const actions = [];
|
|
340
|
+
if (fs.existsSync(agentsPath)) {
|
|
341
|
+
actions.push({
|
|
342
|
+
type: 'update',
|
|
343
|
+
target: agentsPath,
|
|
344
|
+
description: `Update AGENTS.md with project type: ${config.projectType}, tech: ${config.techStack.join(', ')}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
actions.push({
|
|
349
|
+
type: 'create',
|
|
350
|
+
target: agentsPath,
|
|
351
|
+
description: 'Create AGENTS.md with project configuration',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (fs.existsSync(platformInstructionsPath)) {
|
|
355
|
+
actions.push({
|
|
356
|
+
type: 'update',
|
|
357
|
+
target: platformInstructionsPath,
|
|
358
|
+
description: 'Workflow instructions available for customization',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return formatter.success({
|
|
362
|
+
wouldExecute: true,
|
|
363
|
+
actions,
|
|
364
|
+
summary: {
|
|
365
|
+
targetDir,
|
|
366
|
+
modified: [agentsPath],
|
|
367
|
+
skipped: [],
|
|
368
|
+
dryRun: true,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const modified = [];
|
|
373
|
+
const skipped = [];
|
|
374
|
+
// Update AGENTS.md
|
|
375
|
+
if (fs.existsSync(agentsPath)) {
|
|
376
|
+
let agentsContent = fs.readFileSync(agentsPath, 'utf-8');
|
|
377
|
+
const pm = config.packageManager || 'npm/pnpm';
|
|
378
|
+
agentsContent = agentsContent.replace(/\*\*Tooling\*\*: .*/, `**Tooling**: ${config.techStack.includes('composer') ? 'Composer for PHP' : ''}${config.techStack.includes('npm') ? `, ${pm} for JS` : ''}.`);
|
|
379
|
+
fs.writeFileSync(agentsPath, agentsContent, 'utf-8');
|
|
380
|
+
modified.push(agentsPath);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
skipped.push(agentsPath);
|
|
384
|
+
}
|
|
385
|
+
// Note workflow instructions
|
|
386
|
+
if (fs.existsSync(platformInstructionsPath)) {
|
|
387
|
+
modified.push(platformInstructionsPath);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
skipped.push(platformInstructionsPath);
|
|
391
|
+
}
|
|
392
|
+
return formatter.success({
|
|
393
|
+
targetDir,
|
|
394
|
+
modified,
|
|
395
|
+
skipped,
|
|
396
|
+
dryRun: false,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
const err = error;
|
|
401
|
+
return formatter.fail({
|
|
402
|
+
code: 'CONFIGURE_FAILED',
|
|
403
|
+
message: err.message || 'Configuration failed',
|
|
404
|
+
exitCode: ExitCode.ERROR,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get platform folder name.
|
|
410
|
+
*/
|
|
411
|
+
function getPlatformFolder(platform) {
|
|
412
|
+
const folders = {
|
|
413
|
+
github: '.github',
|
|
414
|
+
cursor: '.cursor',
|
|
415
|
+
claude: '.claude',
|
|
416
|
+
agent: '.agent',
|
|
417
|
+
pi: '.pi/agent',
|
|
418
|
+
};
|
|
419
|
+
return folders[platform];
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get list of files that would be/are installed.
|
|
423
|
+
*/
|
|
424
|
+
function getInstalledFiles(targetDir, platform) {
|
|
425
|
+
const platformFolder = getPlatformFolder(platform);
|
|
426
|
+
const files = [];
|
|
427
|
+
const targetPlatform = path.join(targetDir, platformFolder);
|
|
428
|
+
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));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
walk(targetPlatform);
|
|
444
|
+
}
|
|
445
|
+
const agentsPath = path.join(targetDir, 'AGENTS.md');
|
|
446
|
+
if (fs.existsSync(agentsPath)) {
|
|
447
|
+
files.push('AGENTS.md');
|
|
448
|
+
}
|
|
449
|
+
const agentsTemplatePath = path.join(targetDir, 'AGENTS.template.md');
|
|
450
|
+
if (fs.existsSync(agentsTemplatePath)) {
|
|
451
|
+
files.push('AGENTS.template.md');
|
|
452
|
+
}
|
|
453
|
+
return files;
|
|
454
|
+
}
|
|
455
|
+
export { ExitCode } from '../utils/exit-codes.js';
|
|
456
|
+
export { OutputFormatter, createFormatter, parseOutputFormat } from '../utils/output.js';
|
|
@@ -15,14 +15,14 @@ export function mapProjectType(primary) {
|
|
|
15
15
|
'wp-theme': 'theme',
|
|
16
16
|
'wp-site': 'site',
|
|
17
17
|
'wp-core': 'other',
|
|
18
|
-
|
|
19
|
-
|
|
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 {
|
|
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
|
|
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
|
|
74
|
+
export function formatDetectionResults(detectedType, detectedTech) {
|
|
77
75
|
const typeLabels = {
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
plugin: 'WordPress Plugin',
|
|
77
|
+
theme: 'WordPress Theme',
|
|
80
78
|
'block-theme': 'Block Theme',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
site: 'Full Site / Multisite',
|
|
80
|
+
blocks: 'Gutenberg Blocks',
|
|
81
|
+
other: 'Other / Mixed',
|
|
84
82
|
};
|
|
85
83
|
const techLabels = {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
gutenberg: 'Blocks',
|
|
85
|
+
interactivity: 'Interactivity API',
|
|
86
|
+
wpcli: 'WP-CLI',
|
|
89
87
|
'rest-api': 'REST API',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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,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
|
+
}
|
package/dist/utils/paths.js
CHANGED
|
@@ -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 ../
|