aped-method 1.0.0 → 1.6.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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/index.js';
4
+
5
+ run().catch((err) => {
6
+ console.error('\x1b[31mError:\x1b[0m', err.message);
7
+ process.exit(1);
8
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aped-method",
3
- "version": "1.0.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold the APED pipeline (Analyze, PRD, Epics, Dev, Review) into any Claude Code project",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  import { createInterface } from 'node:readline';
2
+ import { existsSync, readFileSync, writeFileSync as writeFS } from 'node:fs';
2
3
  import { stdin, stdout } from 'node:process';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import { scaffold } from './scaffold.js';
4
7
 
8
+ // ── CLI version (read from package.json) ──
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
11
+
5
12
  const DEFAULTS = {
6
13
  apedDir: '.aped',
7
14
  outputDir: 'docs/aped',
@@ -10,8 +17,13 @@ const DEFAULTS = {
10
17
  projectName: '',
11
18
  communicationLang: 'english',
12
19
  documentLang: 'english',
20
+ ticketSystem: 'none',
21
+ gitProvider: 'github',
13
22
  };
14
23
 
24
+ const TICKET_OPTIONS = ['none', 'jira', 'linear', 'github-issues', 'gitlab-issues'];
25
+ const GIT_OPTIONS = ['github', 'gitlab', 'bitbucket'];
26
+
15
27
  // ── ANSI helpers ──
16
28
  const ESC = '\x1b';
17
29
  const a = {
@@ -27,24 +39,16 @@ const a = {
27
39
  magenta: `${ESC}[35m`,
28
40
  cyan: `${ESC}[36m`,
29
41
  white: `${ESC}[37m`,
30
- // 256-color greens for richer palette
31
- lime: `${ESC}[38;5;118m`, // bright lime
32
- emerald: `${ESC}[38;5;42m`, // emerald
33
- mint: `${ESC}[38;5;48m`, // mint
34
- forest: `${ESC}[38;5;34m`, // forest
35
- spring: `${ESC}[38;5;82m`, // spring green
42
+ lime: `${ESC}[38;5;118m`,
43
+ emerald: `${ESC}[38;5;42m`,
44
+ mint: `${ESC}[38;5;48m`,
45
+ forest: `${ESC}[38;5;34m`,
46
+ spring: `${ESC}[38;5;82m`,
36
47
  };
37
48
 
38
49
  const bold = (s) => `${a.bold}${s}${a.reset}`;
39
50
  const dim = (s) => `${a.dim}${s}${a.reset}`;
40
- const green = (s) => `${a.green}${s}${a.reset}`;
41
- const lime = (s) => `${a.lime}${s}${a.reset}`;
42
- const emerald = (s) => `${a.emerald}${s}${a.reset}`;
43
- const mint = (s) => `${a.mint}${s}${a.reset}`;
44
51
  const yellow = (s) => `${a.yellow}${s}${a.reset}`;
45
- const magenta = (s) => `${a.magenta}${s}${a.reset}`;
46
- const red = (s) => `${a.red}${s}${a.reset}`;
47
- const cyan = (s) => `${a.cyan}${s}${a.reset}`;
48
52
 
49
53
  // ── ASCII Art Logo ──
50
54
  const LOGO = `
@@ -59,7 +63,7 @@ ${a.lime}${a.bold} M E T H O D${a.reset}
59
63
  ${a.dim} ─────────────────────────────────${a.reset}
60
64
  `;
61
65
 
62
- const PIPELINE = ` ${a.emerald}${a.bold}A${a.reset}${a.dim}nalyze${a.reset} ${a.dim}→${a.reset} ${a.mint}${a.bold}P${a.reset}${a.dim}RD${a.reset} ${a.dim}→${a.reset} ${a.yellow}${a.bold}E${a.reset}${a.dim}pics${a.reset} ${a.dim}→${a.reset} ${a.lime}${a.bold}D${a.reset}${a.dim}ev${a.reset} ${a.dim}→${a.reset} ${a.red}${a.bold}R${a.reset}${a.dim}eview${a.reset}`;
66
+ const PIPELINE = ` ${a.emerald}${a.bold}A${a.reset}${a.dim}nalyze${a.reset} ${a.dim}→${a.reset} ${a.mint}${a.bold}P${a.reset}${a.dim}RD${a.reset} ${a.dim}→${a.reset} ${a.magenta}${a.bold}UX${a.reset} ${a.dim}→${a.reset} ${a.yellow}${a.bold}E${a.reset}${a.dim}pics${a.reset} ${a.dim}→${a.reset} ${a.lime}${a.bold}D${a.reset}${a.dim}ev${a.reset} ${a.dim}→${a.reset} ${a.red}${a.bold}R${a.reset}${a.dim}eview${a.reset}`;
63
67
 
64
68
  // ── Spinner ──
65
69
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -69,7 +73,7 @@ function createSpinner(text) {
69
73
  let interval;
70
74
  return {
71
75
  start() {
72
- process.stdout.write('\x1b[?25l'); // hide cursor
76
+ process.stdout.write('\x1b[?25l');
73
77
  interval = setInterval(() => {
74
78
  const frame = SPINNER_FRAMES[i % SPINNER_FRAMES.length];
75
79
  process.stdout.write(`\r ${a.emerald}${frame}${a.reset} ${text}`);
@@ -79,7 +83,7 @@ function createSpinner(text) {
79
83
  stop(finalText) {
80
84
  clearInterval(interval);
81
85
  process.stdout.write(`\r ${a.lime}${a.bold}✓${a.reset} ${finalText}\x1b[K\n`);
82
- process.stdout.write('\x1b[?25h'); // show cursor
86
+ process.stdout.write('\x1b[?25h');
83
87
  },
84
88
  fail(finalText) {
85
89
  clearInterval(interval);
@@ -93,7 +97,6 @@ function sleep(ms) {
93
97
  return new Promise((resolve) => setTimeout(resolve, ms));
94
98
  }
95
99
 
96
- // ── Section display ──
97
100
  function sectionHeader(title) {
98
101
  console.log('');
99
102
  console.log(` ${a.emerald}${a.bold}┌─${a.reset} ${a.bold}${title}${a.reset}`);
@@ -104,12 +107,55 @@ function sectionEnd() {
104
107
  console.log(` ${a.emerald}${a.bold}└──────────────────────────────────${a.reset}`);
105
108
  }
106
109
 
110
+ // ── Semver compare (1=a>b, -1=a<b, 0=equal) ──
111
+ function semverCompare(va, vb) {
112
+ const pa = va.split('.').map(Number);
113
+ const pb = vb.split('.').map(Number);
114
+ for (let i = 0; i < 3; i++) {
115
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
116
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
117
+ }
118
+ return 0;
119
+ }
120
+
121
+ // ── Detect existing install & read config ──
122
+ function detectExisting(apedDir) {
123
+ const cwd = process.cwd();
124
+ const configPath = join(cwd, apedDir, 'config.yaml');
125
+ if (!existsSync(configPath)) return null;
126
+
127
+ // Parse existing config.yaml (simple key: value)
128
+ const existing = {};
129
+ try {
130
+ const content = readFileSync(configPath, 'utf-8');
131
+ for (const line of content.split('\n')) {
132
+ const match = line.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
133
+ if (match) existing[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
134
+ }
135
+ } catch { /* ignore */ }
136
+
137
+ return {
138
+ projectName: existing.project_name || '',
139
+ authorName: existing.user_name || '',
140
+ communicationLang: existing.communication_language || DEFAULTS.communicationLang,
141
+ documentLang: existing.document_output_language || DEFAULTS.documentLang,
142
+ apedDir: existing.aped_path || apedDir,
143
+ outputDir: existing.output_path || DEFAULTS.outputDir,
144
+ ticketSystem: existing.ticket_system || DEFAULTS.ticketSystem,
145
+ gitProvider: existing.git_provider || DEFAULTS.gitProvider,
146
+ installedVersion: existing.aped_version || '0.0.0',
147
+ };
148
+ }
149
+
107
150
  // ── Args ──
108
151
  function parseArgs(argv) {
109
152
  const args = {};
110
153
  for (let i = 2; i < argv.length; i++) {
111
154
  const arg = argv[i];
112
155
  if (arg === '--yes' || arg === '-y') { args.yes = true; continue; }
156
+ if (arg === '--update' || arg === '-u') { args.mode = 'update'; continue; }
157
+ if (arg === '--fresh' || arg === '--force') { args.mode = 'fresh'; continue; }
158
+ if (arg === '--version' || arg === '-v') { args.version = true; continue; }
113
159
  const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
114
160
  if (match) {
115
161
  const key = match[1].replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
@@ -147,33 +193,65 @@ export async function run() {
147
193
 
148
194
  let detectedProject = '';
149
195
  try {
150
- const { readFileSync } = await import('node:fs');
151
196
  const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
152
197
  detectedProject = pkg.name || '';
153
198
  } catch {
154
199
  detectedProject = process.cwd().split('/').pop();
155
200
  }
156
201
 
202
+ // ── Version flag ──
203
+ if (args.version) {
204
+ console.log(`aped-method v${CLI_VERSION}`);
205
+ return;
206
+ }
207
+
157
208
  // ── Banner ──
158
209
  console.log(LOGO);
210
+ console.log(`${a.dim} v${CLI_VERSION}${a.reset}`);
211
+ console.log('');
159
212
  console.log(PIPELINE);
160
213
  console.log('');
161
214
 
215
+ // ── Detect existing installation ──
216
+ const apedDir = args.aped || args.apedDir || DEFAULTS.apedDir;
217
+ const existing = detectExisting(apedDir);
218
+
219
+ // ── Version upgrade detection ──
220
+ if (existing && existing.installedVersion !== '0.0.0') {
221
+ if (existing.installedVersion === CLI_VERSION) {
222
+ console.log(` ${a.lime}${a.bold}✓${a.reset} ${dim(`Installed: v${existing.installedVersion} — up to date`)}`);
223
+ } else if (semverCompare(CLI_VERSION, existing.installedVersion) > 0) {
224
+ console.log(` ${a.yellow}${a.bold}↑${a.reset} ${bold(`Upgrade available: v${existing.installedVersion} → v${CLI_VERSION}`)}`);
225
+ console.log(` ${dim(' Run with --update to upgrade engine files')}`);
226
+ } else {
227
+ console.log(` ${a.yellow}${a.bold}!${a.reset} ${dim(`Installed v${existing.installedVersion} is newer than CLI v${CLI_VERSION}`)}`);
228
+ }
229
+ console.log('');
230
+ }
231
+
162
232
  if (args.yes) {
233
+ // Non-interactive mode
234
+ let mode = args.mode || (existing ? 'update' : 'install');
235
+
236
+ const defaults = existing || DEFAULTS;
163
237
  const config = {
164
- projectName: args.project || args.projectName || detectedProject || 'my-project',
165
- authorName: args.author || args.authorName || '',
166
- communicationLang: args.lang || args.communicationLang || DEFAULTS.communicationLang,
167
- documentLang: args.docLang || args.documentLang || DEFAULTS.documentLang,
168
- apedDir: args.aped || args.apedDir || DEFAULTS.apedDir,
169
- outputDir: args.output || args.outputDir || DEFAULTS.outputDir,
238
+ projectName: args.project || args.projectName || defaults.projectName || detectedProject || 'my-project',
239
+ authorName: args.author || args.authorName || defaults.authorName || '',
240
+ communicationLang: args.lang || args.communicationLang || defaults.communicationLang,
241
+ documentLang: args.docLang || args.documentLang || defaults.documentLang,
242
+ apedDir: args.aped || args.apedDir || defaults.apedDir || DEFAULTS.apedDir,
243
+ outputDir: args.output || args.outputDir || defaults.outputDir || DEFAULTS.outputDir,
170
244
  commandsDir: args.commands || args.commandsDir || DEFAULTS.commandsDir,
245
+ ticketSystem: args.tickets || args.ticketSystem || defaults.ticketSystem || DEFAULTS.ticketSystem,
246
+ gitProvider: args.git || args.gitProvider || defaults.gitProvider || DEFAULTS.gitProvider,
247
+ cliVersion: CLI_VERSION,
171
248
  };
172
249
 
173
- await runScaffold(config);
250
+ await runScaffold(config, mode);
174
251
  return;
175
252
  }
176
253
 
254
+ // ── Interactive mode ──
177
255
  const rl = createInterface({ input: stdin, output: stdout });
178
256
 
179
257
  const prompt = stdinLines
@@ -186,20 +264,58 @@ export async function run() {
186
264
  : (question, def) => ask(rl, question, def);
187
265
 
188
266
  try {
267
+ let mode = 'install';
268
+
269
+ // ── Existing install detected ──
270
+ if (existing) {
271
+ const vInfo = existing.installedVersion !== '0.0.0'
272
+ ? ` | v${existing.installedVersion}`
273
+ : '';
274
+ console.log(` ${a.yellow}${a.bold}⚠${a.reset} ${bold('Existing APED installation detected')}`);
275
+ console.log(` ${dim(` Project: ${existing.projectName}${vInfo}`)}`);
276
+ console.log('');
277
+ console.log(` ${a.emerald}│${a.reset} ${bold('1')} ${dim('Update engine')} ${dim('— upgrade skills, scripts, hooks. Preserve config & artifacts')}`);
278
+ console.log(` ${a.emerald}│${a.reset} ${bold('2')} ${dim('Fresh install')} ${dim('— delete everything and start from zero')}`);
279
+ console.log(` ${a.emerald}│${a.reset} ${bold('3')} ${dim('Cancel')}`);
280
+ console.log(` ${a.emerald}│${a.reset}`);
281
+
282
+ const choice = await prompt(`${bold('Choice')}`, '1');
283
+
284
+ if (choice === '3' || choice.toLowerCase() === 'n') {
285
+ console.log(`\n ${yellow('Cancelled.')}\n`);
286
+ return;
287
+ }
288
+
289
+ mode = choice === '2' ? 'fresh' : 'update';
290
+ }
291
+
292
+ // ── Config prompts ──
293
+ // For update: pre-fill with existing values
294
+ const defaults = (mode === 'update' && existing) ? existing : DEFAULTS;
295
+
189
296
  sectionHeader('Configuration');
190
297
 
191
298
  const config = {};
192
- config.projectName = await prompt(`${bold('Project name')}`, detectedProject);
193
- config.authorName = await prompt(`${bold('Author')}`, DEFAULTS.authorName);
194
- config.communicationLang = await prompt(`${bold('Communication language')}`, DEFAULTS.communicationLang);
195
- config.documentLang = await prompt(`${bold('Document language')}`, DEFAULTS.documentLang);
196
- config.apedDir = await prompt(`${bold('APED dir')} ${dim('(engine)')}`, DEFAULTS.apedDir);
197
- config.outputDir = await prompt(`${bold('Output dir')} ${dim('(artifacts)')}`, DEFAULTS.outputDir);
299
+ if (mode === 'update') {
300
+ // In update mode, show current config and only ask what to change
301
+ console.log(` ${a.emerald}│${a.reset} ${dim('Current config loaded. Press Enter to keep, or type new value.')}`);
302
+ console.log(` ${a.emerald}│${a.reset}`);
303
+ }
304
+
305
+ config.projectName = await prompt(`${bold('Project name')}`, defaults.projectName || detectedProject);
306
+ config.authorName = await prompt(`${bold('Author')}`, defaults.authorName);
307
+ config.communicationLang = await prompt(`${bold('Communication language')}`, defaults.communicationLang);
308
+ config.documentLang = await prompt(`${bold('Document language')}`, defaults.documentLang);
309
+ config.apedDir = await prompt(`${bold('APED dir')} ${dim('(engine)')}`, defaults.apedDir || DEFAULTS.apedDir);
310
+ config.outputDir = await prompt(`${bold('Output dir')} ${dim('(artifacts)')}`, defaults.outputDir || DEFAULTS.outputDir);
198
311
  config.commandsDir = await prompt(`${bold('Commands dir')}`, DEFAULTS.commandsDir);
312
+ config.ticketSystem = await prompt(`${bold('Ticket system')} ${dim(`(${TICKET_OPTIONS.join('/')})`)}`, defaults.ticketSystem || DEFAULTS.ticketSystem);
313
+ config.gitProvider = await prompt(`${bold('Git provider')} ${dim(`(${GIT_OPTIONS.join('/')})`)}`, defaults.gitProvider || DEFAULTS.gitProvider);
314
+ config.cliVersion = CLI_VERSION;
199
315
 
200
316
  sectionEnd();
201
317
  console.log('');
202
- printConfig(config);
318
+ printConfig(config, mode);
203
319
  console.log('');
204
320
 
205
321
  const confirm = await prompt(`${bold('Proceed?')}`, 'Y');
@@ -208,50 +324,78 @@ export async function run() {
208
324
  return;
209
325
  }
210
326
 
211
- await runScaffold(config);
327
+ await runScaffold(config, mode);
212
328
  } finally {
213
329
  rl.close();
214
330
  }
215
331
  }
216
332
 
217
- async function runScaffold(config) {
218
- // ── Phase 1: Validating config ──
333
+ async function runScaffold(config, mode) {
334
+ const modeLabel = mode === 'update' ? 'Updating' : mode === 'fresh' ? 'Fresh installing' : 'Installing';
335
+ const modeTag = mode === 'update'
336
+ ? `${a.yellow}${a.bold}UPDATE${a.reset}`
337
+ : mode === 'fresh'
338
+ ? `${a.red}${a.bold}FRESH${a.reset}`
339
+ : `${a.lime}${a.bold}INSTALL${a.reset}`;
340
+
341
+ console.log(` ${modeTag} ${dim(modeLabel + ' APED Method...')}`);
342
+ console.log('');
343
+
344
+ // ── Phase 1: Clean if fresh ──
345
+ if (mode === 'fresh') {
346
+ const s0 = createSpinner('Removing existing installation...');
347
+ s0.start();
348
+ const { rmSync } = await import('node:fs');
349
+ const cwd = process.cwd();
350
+ try { rmSync(join(cwd, config.apedDir), { recursive: true, force: true }); } catch { /* ok */ }
351
+ try { rmSync(join(cwd, config.outputDir), { recursive: true, force: true }); } catch { /* ok */ }
352
+ // Don't delete commands dir — may have non-aped commands
353
+ // Delete only aped-* command files
354
+ const { readdirSync } = await import('node:fs');
355
+ try {
356
+ const cmdDir = join(cwd, config.commandsDir);
357
+ for (const f of readdirSync(cmdDir)) {
358
+ if (f.startsWith('aped-')) {
359
+ rmSync(join(cmdDir, f), { force: true });
360
+ }
361
+ }
362
+ } catch { /* ok */ }
363
+ await sleep(300);
364
+ s0.stop('Previous installation removed');
365
+ }
366
+
367
+ // ── Phase 2: Validate ──
219
368
  const s1 = createSpinner('Validating configuration...');
220
369
  s1.start();
221
370
  await sleep(400);
222
371
  s1.stop('Configuration validated');
223
372
 
224
- // ── Phase 2: Creating directory structure ──
225
- const s2 = createSpinner('Creating directory structure...');
226
- s2.start();
227
- await sleep(300);
228
- s2.stop('Directory structure ready');
229
-
230
- // ── Phase 3: Scaffolding ──
231
- sectionHeader('Scaffolding Pipeline');
373
+ // ── Phase 3: Scaffold ──
374
+ sectionHeader(`Scaffolding Pipeline ${dim(`(${mode})`)}`);
232
375
  console.log(` ${a.emerald}│${a.reset}`);
233
376
 
234
- const count = await scaffoldWithCheckpoints(config);
377
+ const { created, updated, skipped } = await scaffoldWithCheckpoints(config, mode);
235
378
 
236
379
  sectionEnd();
237
380
 
238
- // ── Phase 4: Setting up hooks ──
381
+ // ── Phase 4: Hooks ──
239
382
  const s3 = createSpinner('Installing guardrail hook...');
240
383
  s3.start();
241
384
  await sleep(350);
242
385
  s3.stop('Guardrail hook installed');
243
386
 
244
- // ── Phase 5: Final verification ──
387
+ // ── Phase 5: Verify ──
388
+ const total = created + updated;
245
389
  const s4 = createSpinner('Verifying installation...');
246
390
  s4.start();
247
391
  await sleep(300);
248
- s4.stop(`Installation verified — ${bold(String(count))} files`);
392
+ s4.stop(`Verified — ${bold(String(total))} files`);
249
393
 
250
394
  // ── Done ──
251
- printDone(count);
395
+ printDone(created, updated, skipped, mode);
252
396
  }
253
397
 
254
- async function scaffoldWithCheckpoints(config) {
398
+ async function scaffoldWithCheckpoints(config, mode) {
255
399
  const { getTemplates } = await import('./templates/index.js');
256
400
  const { mkdirSync, writeFileSync, chmodSync, existsSync } = await import('node:fs');
257
401
  const { join, dirname } = await import('node:path');
@@ -280,7 +424,14 @@ async function scaffoldWithCheckpoints(config) {
280
424
  else groups.config.items.push(tpl);
281
425
  }
282
426
 
283
- let count = 0;
427
+ // Paths to preserve during update (user data, not engine)
428
+ const preserveOnUpdate = new Set([
429
+ join(config.outputDir, 'state.yaml'),
430
+ ]);
431
+
432
+ let created = 0;
433
+ let updated = 0;
434
+ let skipped = 0;
284
435
 
285
436
  for (const [, group] of Object.entries(groups)) {
286
437
  if (group.items.length === 0) continue;
@@ -289,31 +440,108 @@ async function scaffoldWithCheckpoints(config) {
289
440
  sp.start();
290
441
  await sleep(200);
291
442
 
292
- let groupCount = 0;
443
+ let groupCreated = 0;
444
+ let groupUpdated = 0;
445
+ let groupSkipped = 0;
446
+
293
447
  for (const tpl of group.items) {
294
448
  const fullPath = join(cwd, tpl.path);
449
+ const fileExists = existsSync(fullPath);
450
+
295
451
  mkdirSync(dirname(fullPath), { recursive: true });
296
- if (!existsSync(fullPath)) {
452
+
453
+ if (!fileExists) {
454
+ // New file — always create
455
+ writeFileSync(fullPath, tpl.content, 'utf-8');
456
+ if (tpl.executable) chmodSync(fullPath, 0o755);
457
+ groupCreated++;
458
+ created++;
459
+ } else if (mode === 'update') {
460
+ // File exists + update mode
461
+ if (preserveOnUpdate.has(tpl.path)) {
462
+ // Preserve user artifacts
463
+ groupSkipped++;
464
+ skipped++;
465
+ } else if (tpl.path.endsWith('settings.local.json')) {
466
+ // Merge settings instead of overwrite
467
+ mergeSettings(fullPath, tpl.content);
468
+ groupUpdated++;
469
+ updated++;
470
+ } else {
471
+ // Engine file — overwrite with new version
472
+ writeFileSync(fullPath, tpl.content, 'utf-8');
473
+ if (tpl.executable) chmodSync(fullPath, 0o755);
474
+ groupUpdated++;
475
+ updated++;
476
+ }
477
+ } else if (mode === 'fresh') {
478
+ // Fresh mode — overwrite everything
297
479
  writeFileSync(fullPath, tpl.content, 'utf-8');
298
480
  if (tpl.executable) chmodSync(fullPath, 0o755);
299
- groupCount++;
300
- count++;
481
+ groupCreated++;
482
+ created++;
483
+ } else {
484
+ // Install mode — skip existing
485
+ groupSkipped++;
486
+ skipped++;
301
487
  }
302
488
  }
303
489
 
304
- sp.stop(`${group.icon} ${group.label} ${dim(`(${groupCount} files)`)}`);
490
+ const parts = [];
491
+ if (groupCreated > 0) parts.push(`${a.lime}+${groupCreated}${a.reset}`);
492
+ if (groupUpdated > 0) parts.push(`${a.yellow}↑${groupUpdated}${a.reset}`);
493
+ if (groupSkipped > 0) parts.push(`${a.dim}=${groupSkipped}${a.reset}`);
494
+ const stats = parts.length > 0 ? parts.join(' ') : `${a.dim}0${a.reset}`;
495
+
496
+ sp.stop(`${group.icon} ${group.label} ${dim('(')}${stats}${dim(')')}`);
305
497
  }
306
498
 
307
- return count;
499
+ return { created, updated, skipped };
500
+ }
501
+
502
+ function mergeSettings(filePath, newContent) {
503
+ try {
504
+ const existing = JSON.parse(readFileSync(filePath, 'utf-8'));
505
+ const incoming = JSON.parse(newContent);
506
+
507
+ // Merge hooks: add guardrail if not already present
508
+ if (incoming.hooks) {
509
+ if (!existing.hooks) existing.hooks = {};
510
+ for (const [event, handlers] of Object.entries(incoming.hooks)) {
511
+ if (!existing.hooks[event]) {
512
+ existing.hooks[event] = handlers;
513
+ } else {
514
+ // Check if guardrail hook already exists
515
+ for (const handler of handlers) {
516
+ const hookCmds = handler.hooks || [];
517
+ for (const hook of hookCmds) {
518
+ const alreadyExists = existing.hooks[event].some((h) =>
519
+ (h.hooks || []).some((hk) => hk.command === hook.command)
520
+ );
521
+ if (!alreadyExists) {
522
+ existing.hooks[event].push(handler);
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+ }
529
+
530
+ writeFS(filePath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
531
+ } catch {
532
+ // If can't parse existing, overwrite
533
+ writeFS(filePath, newContent, 'utf-8');
534
+ }
308
535
  }
309
536
 
310
- function printConfig(config) {
537
+ function printConfig(config, mode) {
311
538
  const box = (label, value, extra) => {
312
539
  const e = extra ? ` ${dim(extra)}` : '';
313
540
  console.log(` ${a.emerald}│${a.reset} ${dim(label.padEnd(16))}${bold(value)}${e}`);
314
541
  };
315
542
 
316
- console.log(` ${a.emerald}${a.bold}┌─${a.reset} ${bold('Summary')}`);
543
+ const modeTag = mode === 'update' ? ` ${a.yellow}${a.bold}UPDATE${a.reset}` : mode === 'fresh' ? ` ${a.red}${a.bold}FRESH${a.reset}` : '';
544
+ console.log(` ${a.emerald}${a.bold}┌─${a.reset} ${bold('Summary')}${modeTag}`);
317
545
  console.log(` ${a.emerald}│${a.reset}`);
318
546
  box('Project', config.projectName);
319
547
  box('Author', config.authorName || dim('(not set)'));
@@ -322,25 +550,52 @@ function printConfig(config) {
322
550
  box('APED', config.apedDir + '/', 'engine');
323
551
  box('Output', config.outputDir + '/', 'artifacts');
324
552
  box('Commands', config.commandsDir + '/');
553
+ box('Tickets', config.ticketSystem, config.ticketSystem === 'none' ? '' : 'integrated');
554
+ box('Git', config.gitProvider);
555
+
556
+ if (mode === 'update') {
557
+ console.log(` ${a.emerald}│${a.reset}`);
558
+ console.log(` ${a.emerald}│${a.reset} ${a.yellow}${a.bold}↑${a.reset} ${dim('Engine files will be overwritten')}`);
559
+ console.log(` ${a.emerald}│${a.reset} ${a.lime}${a.bold}=${a.reset} ${dim('Config, state & artifacts preserved')}`);
560
+ } else if (mode === 'fresh') {
561
+ console.log(` ${a.emerald}│${a.reset}`);
562
+ console.log(` ${a.emerald}│${a.reset} ${a.red}${a.bold}!${a.reset} ${dim('All existing files will be deleted and recreated')}`);
563
+ }
564
+
325
565
  console.log(` ${a.emerald}│${a.reset}`);
326
566
  console.log(` ${a.emerald}${a.bold}└──────────────────────────────────${a.reset}`);
327
567
  }
328
568
 
329
- function printDone(count) {
569
+ function printDone(created, updated, skipped, mode) {
570
+ const total = created + updated;
330
571
  console.log('');
331
572
  console.log(` ${a.emerald}${a.bold}╔══════════════════════════════════════╗${a.reset}`);
332
- console.log(` ${a.emerald}${a.bold}║${a.reset} ${a.lime}${a.bold}✓${a.reset} ${bold(`${count} files scaffolded`)} ${a.emerald}${a.bold}║${a.reset}`);
333
- console.log(` ${a.emerald}${a.bold}║${a.reset} ${dim('Pipeline ready to use')} ${a.emerald}${a.bold}║${a.reset}`);
573
+ if (mode === 'update') {
574
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${a.lime}${a.bold}✓${a.reset} ${bold('Update complete')} ${a.emerald}${a.bold}║${a.reset}`);
575
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${dim(` +${created} created ↑${updated} updated =${skipped} kept`)} ${a.emerald}${a.bold}║${a.reset}`);
576
+ } else {
577
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${a.lime}${a.bold}✓${a.reset} ${bold(`${total} files scaffolded`)} ${a.emerald}${a.bold}║${a.reset}`);
578
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${dim('Pipeline ready to use')} ${a.emerald}${a.bold}║${a.reset}`);
579
+ }
334
580
  console.log(` ${a.emerald}${a.bold}╚══════════════════════════════════════╝${a.reset}`);
335
581
  console.log('');
336
582
 
337
- console.log(` ${a.bold}Available commands:${a.reset}`);
583
+ console.log(` ${a.bold}Pipeline commands:${a.reset}`);
584
+ console.log('');
585
+ console.log(` ${a.emerald}${a.bold}/aped-a${a.reset} ${dim('Analyze — parallel research → product brief')}`);
586
+ console.log(` ${a.mint}${a.bold}/aped-p${a.reset} ${dim('PRD — autonomous generation from brief')}`);
587
+ console.log(` ${a.magenta}${a.bold}/aped-ux${a.reset} ${dim('UX — screen flows, wireframes, components')}`);
588
+ console.log(` ${a.yellow}${a.bold}/aped-e${a.reset} ${dim('Epics — requirements decomposition')}`);
589
+ console.log(` ${a.lime}${a.bold}/aped-d${a.reset} ${dim('Dev — TDD story implementation')}`);
590
+ console.log(` ${a.red}${a.bold}/aped-r${a.reset} ${dim('Review — adversarial code review')}`);
591
+ console.log('');
592
+ console.log(` ${a.bold}Utility commands:${a.reset}`);
338
593
  console.log('');
339
- console.log(` ${a.emerald}${a.bold}/aped-a${a.reset} ${dim('Analyzeparallel research → product brief')}`);
340
- console.log(` ${a.mint}${a.bold}/aped-p${a.reset} ${dim('PRDautonomous generation from brief')}`);
341
- console.log(` ${a.yellow}${a.bold}/aped-e${a.reset} ${dim('Epicsrequirements decomposition')}`);
342
- console.log(` ${a.lime}${a.bold}/aped-d${a.reset} ${dim('DevTDD story implementation')}`);
343
- console.log(` ${a.red}${a.bold}/aped-r${a.reset} ${dim('Reviewadversarial code review')}`);
594
+ console.log(` ${a.spring}${a.bold}/aped-s${a.reset} ${dim('Sprint status progress dashboard')}`);
595
+ console.log(` ${a.spring}${a.bold}/aped-c${a.reset} ${dim('Correct course manage scope changes')}`);
596
+ console.log(` ${a.spring}${a.bold}/aped-ctx${a.reset} ${dim('Project context brownfield analysis')}`);
597
+ console.log(` ${a.spring}${a.bold}/aped-qa${a.reset} ${dim('QAgenerate E2E & integration tests')}`);
598
+ console.log(` ${a.spring}${a.bold}/aped-quick${a.reset} ${dim('Quick fix/feature bypass pipeline')}`);
344
599
  console.log(` ${a.spring}${a.bold}/aped-all${a.reset} ${dim('Full pipeline A→P→E→D→R')}`);
345
600
  console.log('');
346
601
  console.log(` ${dim('Guardrail hook active — pipeline coherence enforced')}`);