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.
- package/bin/create-aped.js +8 -0
- package/package.json +1 -1
- package/src/index.js +322 -67
- package/src/templates/commands.js +66 -6
- package/src/templates/config.js +38 -1
- package/src/templates/guardrail.js +11 -3
- package/src/templates/references.js +85 -0
- package/src/templates/skills.js +760 -39
package/package.json
CHANGED
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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');
|
|
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');
|
|
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 ||
|
|
167
|
-
documentLang: args.docLang || args.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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
225
|
-
|
|
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
|
|
377
|
+
const { created, updated, skipped } = await scaffoldWithCheckpoints(config, mode);
|
|
235
378
|
|
|
236
379
|
sectionEnd();
|
|
237
380
|
|
|
238
|
-
// ── Phase 4:
|
|
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:
|
|
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(`
|
|
392
|
+
s4.stop(`Verified — ${bold(String(total))} files`);
|
|
249
393
|
|
|
250
394
|
// ── Done ──
|
|
251
|
-
printDone(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
481
|
+
groupCreated++;
|
|
482
|
+
created++;
|
|
483
|
+
} else {
|
|
484
|
+
// Install mode — skip existing
|
|
485
|
+
groupSkipped++;
|
|
486
|
+
skipped++;
|
|
301
487
|
}
|
|
302
488
|
}
|
|
303
489
|
|
|
304
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
333
|
-
|
|
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}
|
|
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.
|
|
340
|
-
console.log(` ${a.
|
|
341
|
-
console.log(` ${a.
|
|
342
|
-
console.log(` ${a.
|
|
343
|
-
console.log(` ${a.
|
|
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('QA — generate 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')}`);
|