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.
@@ -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
- '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,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 ../