multi-agents-cli 1.0.68 → 1.0.69

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/init.js CHANGED
@@ -1,82 +1,71 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * multi-agents - Project Initializer
5
- * Run with: npm run init (inside existing project)
6
- * or: multi-agents init my-project (global CLI)
7
- *
4
+ * multi-agents-cli Project Initializer
5
+ * Run with: multi-agents init <project-name>
6
+ * npm run init (inside existing project)
8
7
  * Runs once. Locked after completion via .scaffold/.initialized
9
8
  */
10
9
 
11
- const readline = require('readline');
12
- const fs = require('fs');
13
- const path = require('path');
10
+ 'use strict';
14
11
 
15
- // ── Prompts (arrow-key navigation) ───────────────────────────────────────────
16
-
17
- let prompts;
18
- try { prompts = require('prompts'); } catch { prompts = null; }
19
-
20
- const arrowSelect = async (message, choices, rl, showBack = false) => {
21
- const allChoices = showBack
22
- ? [...choices, { label: dim('← Restart configuration') }]
23
- : choices;
24
-
25
- if (prompts && process.stdin.isTTY) {
26
- const res = await prompts({
27
- type: 'select',
28
- name: 'value',
29
- message,
30
- choices: allChoices.map((c, i) => ({ title: typeof c === 'string' ? c : c.label, value: i })),
31
- }, { onCancel: () => process.exit(0) });
32
- return res.value ?? 0;
33
- }
34
- allChoices.forEach((c, i) => console.log(` ${dim(`${i + 1}.`)} ${typeof c === 'string' ? c : c.label}`));
35
- return new Promise(resolve => {
36
- rl.question(`\n Select (1-${allChoices.length}): `, ans => {
37
- const n = parseInt(ans) - 1;
38
- resolve(!isNaN(n) && n >= 0 && n < allChoices.length ? n : 0);
39
- });
40
- });
41
- };
42
-
43
- const arrowConfirm = async (message, rl) => {
44
- if (prompts && process.stdin.isTTY) {
45
- const res = await prompts({
46
- type: 'confirm',
47
- name: 'value',
48
- message,
49
- initial: true,
50
- }, { onCancel: () => process.exit(0) });
51
- return res.value ?? true;
52
- }
53
- return new Promise(resolve => {
54
- rl.question(`${message} (y/n): `, ans => resolve(ans.toLowerCase() !== 'n'));
55
- });
56
- };
57
- const os = require('os');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
58
15
  const { execSync, spawn } = require('child_process');
59
16
 
60
- // ── Colors ────────────────────────────────────────────────────────────────────
61
-
62
- const c = {
63
- reset: '\x1b[0m',
64
- bold: '\x1b[1m',
65
- dim: '\x1b[2m',
66
- green: '\x1b[32m',
67
- blue: '\x1b[34m',
68
- yellow: '\x1b[33m',
69
- cyan: '\x1b[36m',
70
- red: '\x1b[31m',
71
- };
17
+ // ── Modules ───────────────────────────────────────────────────────────────────
18
+
19
+ const {
20
+ c, bold, green, yellow, dim, cyan, blue, red,
21
+ rl, ask,
22
+ arrowSelect, arrowConfirm,
23
+ separator, showList, summaryLine, renderTrajectoryLines,
24
+ } = require('./lib/ui');
25
+
26
+ const {
27
+ FRAMEWORK_CONVENTIONS,
28
+ CLIENT_FRAMEWORKS,
29
+ BACKEND_FRAMEWORKS,
30
+ FRAMEWORK_VERSION_FALLBACK,
31
+ fetchLatestVersions,
32
+ STATE_OPTIONS,
33
+ UI_OPTIONS,
34
+ STYLING_OPTIONS,
35
+ DB_OPTIONS,
36
+ ORM_OPTIONS_BY_DB,
37
+ ORM_OPTIONS,
38
+ AUTH_OPTIONS,
39
+ IDE_CANDIDATES,
40
+ } = require('./lib/questions');
41
+
42
+ const {
43
+ expandWinPath,
44
+ buildIDEOptions,
45
+ verifyIDE,
46
+ detectTerminal,
47
+ } = require('./lib/detect');
48
+
49
+ const {
50
+ writeConfig,
51
+ ensureGitignore,
52
+ copyDir,
53
+ generateTrackingStructure,
54
+ setupUserRemote,
55
+ } = require('./lib/writers');
56
+
57
+ const {
58
+ TRAJECTORY_DETAILS,
59
+ renderTrajectoryLines: renderTraj,
60
+ printInitSummary,
61
+ } = require('./lib/summary');
62
+
63
+ const { StepMachine, BACK, CONTINUE, RESTART } = require('./lib/steps');
64
+
65
+ // ── Prompts ───────────────────────────────────────────────────────────────────
72
66
 
73
- const bold = (s) => `${c.bold}${s}${c.reset}`;
74
- const green = (s) => `${c.green}${s}${c.reset}`;
75
- const yellow = (s) => `${c.yellow}${s}${c.reset}`;
76
- const dim = (s) => `${c.dim}${s}${c.reset}`;
77
- const cyan = (s) => `${c.cyan}${s}${c.reset}`;
78
- const blue = (s) => `${c.blue}${s}${c.reset}`;
79
- const red = (s) => `${c.red}${s}${c.reset}`;
67
+ let prompts;
68
+ try { prompts = require('prompts'); } catch { prompts = null; }
80
69
 
81
70
  // ── CLI argument handling ─────────────────────────────────────────────────────
82
71
 
@@ -86,11 +75,9 @@ const isReInit = args[0] === 'init' && !args[1];
86
75
  const projectArg = isGlobalCLI ? args[1] : null;
87
76
 
88
77
  if (isReInit) {
89
- // Re-init from inside project or worktree - self-relocate to repo root
90
78
  try {
91
- const { execSync } = require('child_process');
92
79
  const gitCommonDir = execSync('git rev-parse --git-common-dir', { encoding: 'utf8' }).trim();
93
- const repoRoot = require('path').resolve(gitCommonDir, '..');
80
+ const repoRoot = path.resolve(gitCommonDir, '..');
94
81
  process.chdir(repoRoot);
95
82
  } catch { /* stay in current directory */ }
96
83
  }
@@ -104,19 +91,16 @@ if (isGlobalCLI) {
104
91
  fs.mkdirSync(targetDir, { recursive: true });
105
92
  process.chdir(targetDir);
106
93
 
107
- // Write temporary package.json so npm run init works if abandoned mid-init
108
94
  fs.writeFileSync(
109
95
  path.join(targetDir, 'package.json'),
110
96
  JSON.stringify({ name: path.basename(targetDir), version: '1.0.0', scripts: { init: 'multi-agents init' } }, null, 2),
111
97
  'utf8'
112
98
  );
113
99
 
114
- // Initialize git
115
100
  try {
116
101
  execSync('git init -b main', { cwd: targetDir, stdio: 'pipe' });
117
102
  execSync('git commit --allow-empty -m "init: project created"', { cwd: targetDir, stdio: 'pipe' });
118
103
  } catch {
119
- // Fallback for older git versions that don't support -b flag
120
104
  try {
121
105
  execSync('git init', { cwd: targetDir, stdio: 'pipe' });
122
106
  execSync('git checkout -b main', { cwd: targetDir, stdio: 'pipe' });
@@ -126,537 +110,60 @@ if (isGlobalCLI) {
126
110
  }
127
111
  }
128
112
 
129
- // ── Lock check ────────────────────────────────────────────────────────────────
113
+ // ── Paths ─────────────────────────────────────────────────────────────────────
130
114
 
131
115
  const ROOT = process.cwd();
132
116
  const RUNTIME_DIR = path.join(ROOT, '.scaffold');
133
117
  const LOCK_FILE = path.join(RUNTIME_DIR, '.initialized');
134
118
 
135
119
  // Ensure .scaffold/ exists
136
- if (!fs.existsSync(RUNTIME_DIR)) {
137
- fs.mkdirSync(RUNTIME_DIR, { recursive: true });
138
- }
120
+ fs.mkdirSync(RUNTIME_DIR, { recursive: true });
139
121
 
140
- // ── Decision tree ─────────────────────────────────────────────────────────────
141
-
142
- const FRAMEWORK_CONVENTIONS = {
143
- client: {
144
- 'Next.js': { root: 'client', typesDir: 'client/src/types', importAlias: '@/types' },
145
- 'Angular': { root: 'client', typesDir: 'client/src/app/core/types', importAlias: null },
146
- 'Nuxt': { root: 'client', typesDir: 'client/types', importAlias: '~/types' },
147
- 'SvelteKit': { root: 'client', typesDir: 'client/src/lib/types', importAlias: '$lib/types' },
148
- 'Vite+React': { root: 'client', typesDir: 'client/src/types', importAlias: null },
149
- 'Remix': { root: 'client', typesDir: 'client/app/types', importAlias: null },
150
- },
151
- backend: {
152
- 'Express': { root: 'backend', typesDir: 'backend/src/types', routesDir: 'backend/src/routes' },
153
- 'NestJS': { root: 'backend', dtoDir: 'backend/src/dto', entitiesDir:'backend/src/entities' },
154
- 'Fastify': { root: 'backend', typesDir: 'backend/src/types', routesDir: 'backend/src/routes' },
155
- 'FastAPI': { root: 'backend', schemasDir: 'backend/app/schemas', modelsDir: 'backend/app/models' },
156
- 'Django': { root: 'backend', schemasDir: 'backend/api/serializers', modelsDir: 'backend/api/models' },
157
- },
158
- };
122
+ // ── Helpers ───────────────────────────────────────────────────────────────────
159
123
 
160
- const CLIENT_FRAMEWORKS = [
161
- { label: 'Next.js', value: 'Next.js', language: 'TypeScript', integratedBackend: true },
162
- { label: 'Angular', value: 'Angular', language: 'TypeScript', integratedBackend: false },
163
- { label: 'Vue / Nuxt', value: 'Nuxt', language: 'TypeScript', integratedBackend: true },
164
- { label: 'SvelteKit', value: 'SvelteKit', language: 'TypeScript', integratedBackend: true },
165
- { label: 'Remix', value: 'Remix', language: 'TypeScript', integratedBackend: true },
166
- { label: 'Vite + React', value: 'Vite+React', language: 'TypeScript', integratedBackend: false },
167
- ];
168
-
169
- const BACKEND_FRAMEWORKS = [
170
- { label: 'NestJS', value: 'NestJS', language: 'TypeScript' },
171
- { label: 'Express', value: 'Express', language: 'TypeScript' },
172
- { label: 'Fastify', value: 'Fastify', language: 'TypeScript' },
173
- { label: 'Django', value: 'Django', language: 'Python' },
174
- { label: 'FastAPI', value: 'FastAPI', language: 'Python' },
175
- { label: 'Laravel', value: 'Laravel', language: 'PHP' },
176
- { label: 'Rails', value: 'Rails', language: 'Ruby' },
177
- ];
178
-
179
- // ── Framework version registry ────────────────────────────────────────────────
180
-
181
- const FRAMEWORK_REGISTRY = {
182
- 'Next.js': { registry: 'npm', package: 'next' },
183
- 'Angular': { registry: 'npm', package: '@angular/core' },
184
- 'Nuxt': { registry: 'npm', package: 'nuxt' },
185
- 'SvelteKit': { registry: 'npm', package: '@sveltejs/kit' },
186
- 'Remix': { registry: 'npm', package: '@remix-run/react' },
187
- 'Vite+React': { registry: 'npm', package: 'vite' },
188
- 'NestJS': { registry: 'npm', package: '@nestjs/core' },
189
- 'Express': { registry: 'npm', package: 'express' },
190
- 'Fastify': { registry: 'npm', package: 'fastify' },
191
- 'FastAPI': { registry: 'pypi', package: 'fastapi' },
192
- 'Django': { registry: 'pypi', package: 'django' },
193
- 'Laravel': { registry: 'npm', package: null }, // skip — no npm package
194
- 'Rails': { registry: 'npm', package: null }, // skip — no npm package
195
- };
196
-
197
- const FRAMEWORK_VERSION_FALLBACK = {
198
- 'Next.js': ['15', '14', '13'],
199
- 'Angular': ['22', '21', '20'],
200
- 'Nuxt': ['3', '2', null],
201
- 'SvelteKit': ['2', '1', null],
202
- 'Remix': ['2', '1', null],
203
- 'Vite+React': ['8', '7', '6'],
204
- 'NestJS': ['11', '10', '9' ],
205
- 'Express': ['5', '4'],
206
- 'Fastify': ['5', '4', null],
207
- 'FastAPI': ['0.115', '0.111', '0.104'],
208
- 'Django': ['5.1', '4.2', '3.2'],
209
- };
124
+ const selectRequired = async (prompt, items, stepMachine, stepIndex) => {
125
+ const isFirstStep = stepIndex <= 1;
126
+ const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
127
+ const allItems = [...items, ...navOpts];
210
128
 
211
- const fetchLatestVersions = async (frameworkValue) => {
212
- const entry = FRAMEWORK_REGISTRY[frameworkValue];
213
- if (!entry || !entry.package) return null;
129
+ const idx = await arrowSelect(prompt, allItems.map(i => ({ label: typeof i === 'string' ? i : i.label })));
214
130
 
215
- try {
216
- const https = require('https');
217
- const fetch = (url) => new Promise((resolve, reject) => {
218
- const req = https.get(url, { timeout: 3000 }, (res) => {
219
- let data = '';
220
- res.on('data', chunk => data += chunk);
221
- res.on('end', () => resolve(data));
222
- });
223
- req.on('error', reject);
224
- req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
225
- });
226
-
227
- if (entry.registry === 'npm') {
228
- const raw = await fetch(`https://registry.npmjs.org/${entry.package}`);
229
- const json = JSON.parse(raw);
230
- const versions = Object.keys(json.versions || {})
231
- .filter(v => /^\d+\.\d+\.\d+$/.test(v) && !v.includes('-'))
232
- .map(v => parseInt(v.split('.')[0]))
233
- .filter((v, i, arr) => arr.indexOf(v) === i)
234
- .sort((a, b) => b - a)
235
- .slice(0, 3);
236
- return versions.length ? versions.map(String) : null;
237
- }
238
-
239
- if (entry.registry === 'pypi') {
240
- const raw = await fetch(`https://pypi.org/pypi/${entry.package}/json`);
241
- const json = JSON.parse(raw);
242
- const versions = Object.keys(json.releases || {})
243
- .filter(v => /^\d+\.\d+(\.\d+)?$/.test(v))
244
- .sort((a, b) => {
245
- const [aMaj, aMin = 0] = a.split('.').map(Number);
246
- const [bMaj, bMin = 0] = b.split('.').map(Number);
247
- return bMaj !== aMaj ? bMaj - aMaj : bMin - aMin;
248
- })
249
- .map(v => v.split('.').slice(0, 2).join('.'))
250
- .filter((v, i, arr) => arr.indexOf(v) === i)
251
- .slice(0, 3);
252
- return versions.length ? versions : null;
253
- }
254
- } catch {
255
- return null;
131
+ if (idx >= items.length) {
132
+ const nav = navOpts[idx - items.length];
133
+ return nav ? nav.value : RESTART;
256
134
  }
257
- return null;
258
- };
259
-
260
- const STATE_OPTIONS = {
261
- 'Next.js': ['Zustand', 'Redux Toolkit', 'Jotai', 'TanStack Query'],
262
- 'Vite+React': ['Zustand', 'Redux Toolkit', 'Jotai', 'TanStack Query'],
263
- 'Remix': ['Zustand', 'Redux Toolkit', 'Jotai', 'TanStack Query'],
264
- 'Angular': ['NgRx', 'Signals (built-in)', 'Akita'],
265
- 'Nuxt': ['Pinia', 'Vuex'],
266
- 'SvelteKit': ['Svelte stores (built-in)', 'Zustand'],
267
- };
268
-
269
- const UI_OPTIONS = {
270
- 'Next.js': ['shadcn/ui', 'Radix UI', 'MUI', 'Chakra UI', 'Ant Design'],
271
- 'Vite+React': ['Radix UI', 'MUI', 'Chakra UI', 'Ant Design'],
272
- 'Remix': ['shadcn/ui', 'Radix UI', 'MUI', 'Chakra UI', 'Ant Design'],
273
- 'Angular': ['Angular Material', 'PrimeNG', 'Clarity'],
274
- 'Nuxt': ['Vuetify', 'PrimeVue', 'Naive UI'],
275
- 'SvelteKit': ['Skeleton', 'daisyUI', 'shadcn-svelte'],
276
- };
277
-
278
- const STYLING_OPTIONS = [
279
- 'Tailwind CSS',
280
- 'CSS Modules',
281
- 'Styled Components',
282
- 'SCSS / SASS',
283
- 'UnoCSS',
284
- ];
285
-
286
- const DB_OPTIONS = ['PostgreSQL', 'MySQL', 'MongoDB', 'SQLite', 'Skip (agent will propose when needed)'];
287
-
288
- const ORM_OPTIONS_BY_DB = {
289
- 'PostgreSQL': ['Prisma', 'TypeORM', 'Drizzle', 'Sequelize', 'raw pg driver', 'Skip (agent will propose when needed)'],
290
- 'MySQL': ['Prisma', 'TypeORM', 'Drizzle', 'Sequelize', 'raw mysql2 driver', 'Skip (agent will propose when needed)'],
291
- 'MongoDB': ['Mongoose', 'Prisma', 'raw MongoDB driver', 'Skip (agent will propose when needed)'],
292
- 'SQLite': ['Prisma', 'Drizzle', 'better-sqlite3', 'Skip (agent will propose when needed)'],
293
- };
294
-
295
- const ORM_OPTIONS = {
296
- 'NestJS': ['TypeORM', 'Prisma', 'MikroORM', 'Drizzle'],
297
- 'Express': ['Prisma', 'TypeORM', 'Drizzle', 'Sequelize'],
298
- 'Fastify': ['Prisma', 'TypeORM', 'Drizzle'],
299
- 'Django': ['Django ORM (built-in)', 'SQLAlchemy'],
300
- 'FastAPI': ['SQLAlchemy', 'Tortoise ORM', 'Beanie (MongoDB)'],
301
- 'Laravel': ['Eloquent (built-in)'],
302
- 'Rails': ['Active Record (built-in)'],
303
- };
304
-
305
- const AUTH_OPTIONS = {
306
- 'NestJS': ['Passport.js', 'JWT-only', 'OAuth2', 'Auth.js'],
307
- 'Express': ['Passport.js', 'JWT-only', 'OAuth2'],
308
- 'Fastify': ['fastify-jwt', 'Passport.js', 'OAuth2'],
309
- 'Django': ['Django Auth (built-in)', 'DRF TokenAuth', 'OAuth2'],
310
- 'FastAPI': ['JWT-only', 'OAuth2', 'FastAPI-Users'],
311
- 'Laravel': ['Laravel Sanctum', 'Laravel Passport', 'JWT'],
312
- 'Rails': ['Devise', 'JWT', 'OAuth2'],
313
- };
314
-
315
- const IDE_CANDIDATES = [
316
- {
317
- cmd: 'code',
318
- name: 'VS Code',
319
- mac: { app: 'Visual Studio Code', args: ['--new-window'] },
320
- win: { paths: ['{LOCALAPPDATA}\\Programs\\Microsoft VS Code\\Code.exe', '{ProgramFiles}\\Microsoft VS Code\\Code.exe'], args: ['--new-window'] },
321
- linux: { paths: ['/snap/bin/code', '/usr/bin/code', '/usr/local/bin/code'], args: ['--new-window'] },
322
- },
323
- {
324
- cmd: 'cursor',
325
- name: 'Cursor',
326
- mac: { app: 'Cursor', args: ['--new-window'] },
327
- win: { paths: ['{LOCALAPPDATA}\\Programs\\cursor\\Cursor.exe'], args: ['--new-window'] },
328
- linux: { paths: ['/usr/bin/cursor', '/opt/cursor/cursor'], args: ['--new-window'] },
329
- },
330
- {
331
- cmd: 'webstorm',
332
- name: 'WebStorm',
333
- mac: { app: 'WebStorm', toolboxApp: 'WebStorm', args: [] },
334
- win: { paths: [
335
- '{LOCALAPPDATA}\\JetBrains\\Toolbox\\scripts\\webstorm.cmd',
336
- '{LOCALAPPDATA}\\Programs\\WebStorm\\bin\\webstorm64.exe',
337
- ], args: [] },
338
- linux: { paths: [
339
- `${os.homedir()}/.local/bin/webstorm`,
340
- '/opt/webstorm/bin/webstorm.sh',
341
- '/snap/webstorm/current/bin/webstorm.sh',
342
- ], args: [] },
343
- },
344
- {
345
- cmd: 'idea',
346
- name: 'IntelliJ IDEA',
347
- mac: { app: 'IntelliJ IDEA', toolboxApp: 'IntelliJ IDEA', args: [] },
348
- win: { paths: [
349
- '{LOCALAPPDATA}\\JetBrains\\Toolbox\\scripts\\idea.cmd',
350
- '{LOCALAPPDATA}\\Programs\\IntelliJ IDEA Community Edition\\bin\\idea64.exe',
351
- '{ProgramFiles}\\JetBrains\\IntelliJ IDEA\\bin\\idea64.exe',
352
- ], args: [] },
353
- linux: { paths: [
354
- `${os.homedir()}/.local/bin/idea`,
355
- '/opt/idea/bin/idea.sh',
356
- '/snap/intellij-idea-community/current/bin/idea.sh',
357
- ], args: [] },
358
- },
359
- {
360
- cmd: 'zed',
361
- name: 'Zed',
362
- mac: { app: 'Zed', args: [] },
363
- win: { paths: [], args: [] },
364
- linux: { paths: ['/usr/bin/zed', `${os.homedir()}/.local/bin/zed`], args: [] },
365
- },
366
- {
367
- cmd: null,
368
- name: 'Other / Manual',
369
- note: 'prints worktree path, open it yourself',
370
- mac: null,
371
- win: null,
372
- linux:null,
373
- },
374
- ];
375
-
376
- // Expands {LOCALAPPDATA} / {ProgramFiles} placeholders for Windows paths
377
- const expandWinPath = (p) =>
378
- p.replace('{LOCALAPPDATA}', process.env.LOCALAPPDATA || '')
379
- .replace('{ProgramFiles}', process.env.ProgramFiles || 'C:\\Program Files');
380
-
381
- const buildIDEOptions = () => {
382
- const platform = process.platform;
383
-
384
- return IDE_CANDIDATES.map(ide => {
385
- if (!ide.cmd) {
386
- const noteStr = ide.note ? dim(` (${ide.note})`) : '';
387
- return { ...ide, detected: false, strategy: 'manual', label: `${ide.name} ${dim('→')}${noteStr}` };
388
- }
389
-
390
- let detected = false;
391
- let strategy = 'cli';
392
-
393
- if (platform === 'darwin' && ide.mac) {
394
- // Mac — check .app bundle in /Applications, ~/Applications, and JetBrains Toolbox
395
- const system = `/Applications/${ide.mac.app}.app`;
396
- const user = path.join(os.homedir(), 'Applications', `${ide.mac.app}.app`);
397
- const toolbox = path.join(os.homedir(), 'Applications', 'JetBrains Toolbox', `${ide.mac.app}.app`);
398
- detected = fs.existsSync(system) || fs.existsSync(user) || fs.existsSync(toolbox);
399
- if (detected) strategy = 'mac-app';
400
-
401
- } else if (platform === 'win32' && ide.win) {
402
- // Windows — CLI first, then known exe paths
403
- try {
404
- execSync(`where ${ide.cmd}`, { stdio: 'pipe' });
405
- detected = true;
406
- strategy = 'cli';
407
- } catch {
408
- const expanded = (ide.win.paths || []).map(expandWinPath);
409
- detected = expanded.some(p => fs.existsSync(p));
410
- if (detected) strategy = 'win-exe';
411
- }
412
-
413
- } else if (platform === 'linux' && ide.linux) {
414
- // Linux — CLI first, then known install paths
415
- try {
416
- execSync(`which ${ide.cmd}`, { stdio: 'pipe' });
417
- detected = true;
418
- strategy = 'cli';
419
- } catch {
420
- detected = (ide.linux.paths || []).some(p => fs.existsSync(p));
421
- if (detected) strategy = 'linux-path';
422
- }
423
- }
424
-
425
- const statusStr = detected ? green('✓ detected') : dim('✗ not found');
426
- const noteStr = ide.note ? dim(` (${ide.note})`) : '';
427
- return {
428
- ...ide,
429
- detected,
430
- strategy,
431
- label: `${ide.name} ${statusStr}${noteStr}`,
432
- };
433
- });
434
- };
435
-
436
- const verifyIDE = (ide) => {
437
- const platform = process.platform;
438
-
439
- if (ide.strategy === 'mac-app' && ide.mac) {
440
- // Mac — confirm .app exists and try to read version from plist
441
- const appPath = `/Applications/${ide.mac.app}.app`;
442
- if (!fs.existsSync(appPath) && !fs.existsSync(path.join(os.homedir(), 'Applications', `${ide.mac.app}.app`))) {
443
- return { ok: false };
444
- }
445
- try {
446
- const version = execSync(
447
- `defaults read "/Applications/${ide.mac.app}.app/Contents/Info.plist" CFBundleShortVersionString`,
448
- { stdio: 'pipe', encoding: 'utf8' }
449
- ).trim();
450
- return { ok: true, version };
451
- } catch {
452
- return { ok: true, version: null };
453
- }
454
- }
455
-
456
- // Windows exe / Linux path / CLI — try --version
457
- try {
458
- const cmd = ide.strategy === 'win-exe'
459
- ? `"${(ide.win?.paths || []).map(expandWinPath).find(p => fs.existsSync(p))}"`
460
- : ide.strategy === 'linux-path'
461
- ? `"${(ide.linux?.paths || []).find(p => fs.existsSync(p))}"`
462
- : `"${ide.cmd}"`;
463
- const result = execSync(`${cmd} --version`, { stdio: 'pipe', encoding: 'utf8' });
464
- const version = result.split('\n')[0].trim();
465
- return { ok: true, version };
466
- } catch {
467
- return { ok: false };
468
- }
469
- };
470
-
471
- // ── Tracking structure ────────────────────────────────────────────────────────
472
-
473
- const emptySlot = () => ({
474
- branch: null,
475
- timestamp: null,
476
- launchedAt: null,
477
- status: null,
478
- missingCount: 0,
479
- worktreePath: null,
480
- });
481
-
482
- const generateTrackingStructure = (config) => {
483
- const bt = config.backend?.type;
484
-
485
- const structure = {
486
- client: {
487
- UI: emptySlot(),
488
- LOGIC: emptySlot(),
489
- FORMS: emptySlot(),
490
- ROUTING: emptySlot(),
491
- TESTING: emptySlot(),
492
- ACCESSIBILITY: emptySlot(),
493
- },
494
- shared: {
495
- SECURITY: emptySlot(),
496
- },
497
- };
498
-
499
- if (bt === 'separate') {
500
- structure.backend = {
501
- API: emptySlot(),
502
- LOGIC: emptySlot(),
503
- AUTH: emptySlot(),
504
- DB: emptySlot(),
505
- EVENTS: emptySlot(),
506
- JOBS: emptySlot(),
507
- TESTING: emptySlot(),
508
- };
509
- }
510
-
511
- return structure;
512
- };
513
-
514
- // ── GitHub remote setup ───────────────────────────────────────────────────────
515
-
516
- const detectGitHubUser = () => {
517
- try {
518
- return execSync('gh api user --jq .login',
519
- { encoding: 'utf8', stdio: 'pipe' }).trim();
520
- } catch {}
521
- try {
522
- return execSync('git config user.name',
523
- { encoding: 'utf8', stdio: 'pipe' }).trim();
524
- } catch {}
525
- return null;
526
- };
527
-
528
- const setupUserRemote = (ROOT, projectName) => {
529
- let currentOrigin = null;
530
- try {
531
- currentOrigin = execSync('git remote get-url origin',
532
- { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }).trim();
533
- } catch {}
534
-
535
- // Already has their own remote — nothing to do
536
- if (currentOrigin && !currentOrigin.includes('multi-agents-template')) return;
537
-
538
- // Demote template origin to upstream
539
- if (currentOrigin?.includes('multi-agents-template')) {
540
- try {
541
- execSync('git remote remove origin', { cwd: ROOT, stdio: 'pipe' });
542
- execSync(`git remote add upstream ${currentOrigin}`, { cwd: ROOT, stdio: 'pipe' });
543
- console.log(dim(' ℹ Template remote moved to upstream'));
544
- } catch {}
545
- }
546
-
547
- // Write flag — agent will handle remote setup on first session
548
- const flagPath = path.join(ROOT, '.scaffold', '.remote-setup-needed');
549
- fs.writeFileSync(flagPath, JSON.stringify({
550
- projectName,
551
- createdAt: new Date().toISOString(),
552
- }), 'utf8');
553
-
554
- console.log(`\n ${yellow('ℹ No remote configured.')} Your first agent session will set this up.`);
555
- console.log(dim(' All work stays local until then.\n'));
556
- };
557
-
558
- const renderTrajectoryLines = (lines) => {
559
- const HEADERS = ['Benefits', 'Best for', 'Use agents for', 'Handle manually'];
560
- lines.forEach(l => {
561
- if (!l) { console.log(''); return; }
562
- if (l.startsWith('⚠')) console.log(` ${yellow(l)}`);
563
- else if (HEADERS.includes(l)) console.log(`\n ${bold(l)}`);
564
- else if (l.startsWith('·')) console.log(` ${l}`);
565
- else console.log(` ${dim(l)}`);
566
- });
567
- };
568
-
569
- const rl = readline.createInterface({
570
- input: process.stdin,
571
- output: process.stdout,
572
- });
573
-
574
- const ask = (question) =>
575
- new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
576
-
577
- // ── Selection helpers ─────────────────────────────────────────────────────────
578
-
579
- const showList = (items, showSkip = false) => {
580
- items.forEach((item, i) => {
581
- const label = typeof item === 'string' ? item : item.label;
582
- console.log(` ${dim(`${i + 1}.`)} ${label}`);
583
- });
584
- if (showSkip) console.log(` ${dim('0.')} Skip ${dim('(agent will propose when needed)')}`);
585
- };
586
-
587
- // Sentinel value returned when user picks ← Restart
588
- const BACK = Symbol('BACK');
589
-
590
- const selectRequired = async (prompt, items) => {
591
- const idx = await arrowSelect(prompt, items.map(i => ({ label: typeof i === 'string' ? i : i.label })), rl, true);
592
- if (idx === items.length) return BACK;
593
135
  return items[idx];
594
136
  };
595
137
 
596
- const selectOptional = async (prompt, items) => {
138
+ const selectOptional = async (prompt, items, stepMachine, stepIndex) => {
597
139
  if (!items || items.length === 0) return null;
140
+ const isFirstStep = stepIndex <= 1;
141
+ const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
598
142
  const choices = [
599
143
  ...items.map(i => ({ label: typeof i === 'string' ? i : i.label })),
600
144
  { label: dim('Skip (agent will propose when needed)') },
145
+ ...navOpts.map(n => ({ label: n.label })),
601
146
  ];
602
- const idx = await arrowSelect(prompt, choices, rl, true);
603
- if (idx === choices.length) return BACK;
604
- if (idx === items.length) return null;
605
- return typeof items[idx] === 'string' ? items[idx] : items[idx].value;
606
- };
607
-
608
- const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
609
-
610
- // ── Config writer ─────────────────────────────────────────────────────────────
611
-
612
- const writeConfig = (filePath, configs) => {
613
- if (!fs.existsSync(filePath)) return;
614
- let content = fs.readFileSync(filePath, 'utf8');
615
-
616
- for (const [key, value] of Object.entries(configs)) {
617
- if (!value) continue;
618
- const regex = new RegExp(`(# @config ${key}\\s*:)([^\\n]*)`, 'g');
619
- content = content.replace(regex, `$1 ${value}`);
620
- }
621
-
622
- for (const [key, value] of Object.entries(configs)) {
623
- if (!value) continue;
624
- const token = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
625
- content = content.replace(token, value);
626
- }
627
147
 
628
- fs.writeFileSync(filePath, content, 'utf8');
629
- };
630
-
631
- const ensureGitignore = (entry) => {
632
- const p = path.join(ROOT, '.gitignore');
633
- const content = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
634
- if (!content.includes(entry)) fs.appendFileSync(p, `\n${entry}\n`);
635
- };
148
+ const idx = await arrowSelect(prompt, choices);
636
149
 
637
- const summaryLine = (label, value) => {
638
- const padded = label.padEnd(20);
639
- if (!value) {
640
- console.log(` ${dim(padded)}: ${yellow('(skipped - agent will propose when needed)')}`);
641
- } else {
642
- console.log(` ${dim(padded)}: ${green(value)}`);
150
+ if (idx === items.length) return null; // skip
151
+ if (idx > items.length) {
152
+ const nav = navOpts[idx - items.length - 1];
153
+ return nav ? nav.value : RESTART;
643
154
  }
155
+ return typeof items[idx] === 'string' ? items[idx] : items[idx].value;
644
156
  };
645
157
 
646
- // ── Copy directory ────────────────────────────────────────────────────────────
647
-
648
- const copyDir = (src, dest) => {
649
- if (!fs.existsSync(src)) return;
158
+ const copyDirExcluding = (src, dest, exclude = []) => {
650
159
  fs.mkdirSync(dest, { recursive: true });
651
- fs.readdirSync(src).forEach(file => {
652
- const srcFile = path.join(src, file);
653
- const destFile = path.join(dest, file);
654
- if (fs.statSync(srcFile).isDirectory()) {
655
- copyDir(srcFile, destFile);
656
- } else {
657
- fs.copyFileSync(srcFile, destFile);
658
- }
659
- });
160
+ for (const entry of fs.readdirSync(src)) {
161
+ if (exclude.includes(entry)) continue;
162
+ const srcFile = path.join(src, entry);
163
+ const destFile = path.join(dest, entry);
164
+ if (fs.statSync(srcFile).isDirectory()) copyDirExcluding(srcFile, destFile, []);
165
+ else fs.copyFileSync(srcFile, destFile);
166
+ }
660
167
  };
661
168
 
662
169
  // ── Main ──────────────────────────────────────────────────────────────────────
@@ -670,11 +177,10 @@ const main = async () => {
670
177
  const trackingPath = path.join(RUNTIME_DIR, '.tracking.json');
671
178
  const tracking = fs.existsSync(trackingPath) ? JSON.parse(fs.readFileSync(trackingPath, 'utf8')) : {};
672
179
 
673
- // Dependency map — primary agents whose restart cascades to dependents
674
180
  const DEPENDENCIES = {
675
- client: { UI: ['LOGIC', 'FORMS', 'ROUTING', 'TESTING', 'ACCESSIBILITY'] },
181
+ client: { UI: ['LOGIC', 'FORMS', 'ROUTING', 'TESTING', 'ACCESSIBILITY'] },
676
182
  backend: { DB: ['API', 'AUTH', 'LOGIC', 'EVENTS', 'JOBS', 'TESTING'] },
677
- shared: {},
183
+ shared: {},
678
184
  };
679
185
 
680
186
  const getActiveAgents = (scope) => {
@@ -683,7 +189,6 @@ const main = async () => {
683
189
  };
684
190
 
685
191
  const showRestartProcess = async () => {
686
- // Build list of active processes across all scopes
687
192
  const active = [];
688
193
  for (const scope of ['client', 'backend', 'shared']) {
689
194
  for (const [agent, data] of getActiveAgents(scope)) {
@@ -699,8 +204,8 @@ const main = async () => {
699
204
  separator();
700
205
 
701
206
  const pickRes = await prompts({
702
- type: 'select',
703
- name: 'value',
207
+ type: 'select',
208
+ name: 'value',
704
209
  message: 'Which process do you want to restart?',
705
210
  choices: [
706
211
  ...active.map(({ scope, agent, data }) => ({
@@ -718,54 +223,39 @@ const main = async () => {
718
223
  const deps = (DEPENDENCIES[scope] || {})[agent] || [];
719
224
  const affectedAgents = [{ scope, agent, data }];
720
225
 
721
- // Find dependent agents that are also active
722
226
  for (const dep of deps) {
723
227
  const depData = (tracking[scope] || {})[dep];
724
- if (depData && depData.branch) {
725
- affectedAgents.push({ scope, agent: dep, data: depData });
726
- }
228
+ if (depData && depData.branch) affectedAgents.push({ scope, agent: dep, data: depData });
727
229
  }
728
230
 
729
- // Show warning with exact names
730
231
  separator();
731
232
  console.log(`\n${yellow(` ⚠ Restarting ${agent} will delete:`)}`);
732
233
  for (const { agent: a, data: d } of affectedAgents) {
733
234
  console.log(`\n ${bold(a)}`);
734
235
  console.log(` - Branch (${d.branch})`);
735
236
  console.log(` - Remote branch (origin/${d.branch})`);
736
- if (d.worktreePath) {
737
- const wtName = path.relative(ROOT, d.worktreePath);
738
- console.log(` - Worktree (${wtName})`);
739
- }
740
- }
741
-
742
- if (affectedAgents.length > 1) {
743
- console.log(`\n ${yellow('Dependent processes will also be wiped.')}`);
237
+ if (d.worktreePath) console.log(` - Worktree (${path.relative(ROOT, d.worktreePath)})`);
744
238
  }
745
239
 
240
+ if (affectedAgents.length > 1) console.log(`\n ${yellow('Dependent processes will also be wiped.')}`);
746
241
  console.log(`\n ${red('This cannot be undone.')}\n`);
747
242
 
748
243
  const confirmRes = await prompts({
749
- type: 'select',
750
- name: 'value',
244
+ type: 'select',
245
+ name: 'value',
751
246
  message: 'Confirm restart?',
752
247
  choices: [
753
248
  { title: 'Yes - wipe and restart', value: 'y' },
754
- { title: 'Cancel', value: 'n' },
249
+ { title: 'Cancel', value: 'n' },
755
250
  ],
756
251
  }, { onCancel: () => process.exit(0) });
757
252
 
758
- if (confirmRes.value !== 'y') {
759
- console.log(dim('\n Cancelled.\n'));
760
- return 'back';
761
- }
253
+ if (confirmRes.value !== 'y') { console.log(dim('\n Cancelled.\n')); return 'back'; }
762
254
 
763
- // Perform wipe
764
255
  for (const { agent: a, data: d, scope: s } of affectedAgents) {
765
256
  try { execSync(`git worktree remove "${d.worktreePath}" --force`, { cwd: ROOT, stdio: 'pipe' }); } catch {}
766
257
  try { execSync(`git branch -D ${d.branch}`, { cwd: ROOT, stdio: 'pipe' }); } catch {}
767
258
  try { execSync(`git push origin --delete ${d.branch}`, { cwd: ROOT, stdio: 'pipe' }); } catch {}
768
- // Clear tracking
769
259
  if (tracking[s] && tracking[s][a]) {
770
260
  tracking[s][a] = { branch: null, timestamp: null, launchedAt: null, status: null, missingCount: 0, worktreePath: null };
771
261
  }
@@ -780,9 +270,9 @@ const main = async () => {
780
270
  separator();
781
271
  console.log(`\n${yellow(' This project has already been initialized.')}`);
782
272
  console.log(dim(` Initialized on: ${ts}\n`));
783
- console.log(dim(` To start a task: `) + cyan('npm run agent'));
784
- console.log(dim(` To restart an agent: `) + cyan('npm run restart'));
785
- console.log(dim(` To wipe everything: `) + cyan('npm run reset') + '\n');
273
+ console.log(dim(' To start a task: ') + cyan('npm run agent'));
274
+ console.log(dim(' To restart an agent: ') + cyan('npm run restart'));
275
+ console.log(dim(' To wipe everything: ') + cyan('npm run reset') + '\n');
786
276
 
787
277
  if (prompts && process.stdin.isTTY) {
788
278
  const res = await prompts({
@@ -791,7 +281,7 @@ const main = async () => {
791
281
  message: 'What would you like to do?',
792
282
  choices: [
793
283
  { title: 'Re-initialize project', description: 'Wipe everything and start fresh', value: '1' },
794
- { title: 'Cancel', value: '2' },
284
+ { title: 'Cancel', value: '2' },
795
285
  ],
796
286
  }, { onCancel: () => process.exit(0) });
797
287
 
@@ -808,12 +298,8 @@ const main = async () => {
808
298
  { title: 'No - Cancel', value: 'no' },
809
299
  ],
810
300
  }, { onCancel: () => process.exit(0) });
811
- if (confirm.value !== 'yes') {
812
- console.log(dim('\n Cancelled.\n'));
813
- process.exit(0);
814
- }
815
- const { spawn: sp } = require('child_process');
816
- const resetChild = sp('node', [path.join(ROOT, '.workflow', 'reset.js')], { stdio: 'inherit', cwd: ROOT });
301
+ if (confirm.value !== 'yes') { console.log(dim('\n Cancelled.\n')); process.exit(0); }
302
+ const resetChild = spawn('node', [path.join(ROOT, '.workflow', 'reset.js')], { stdio: 'inherit', cwd: ROOT });
817
303
  resetChild.on('exit', code => process.exit(code ?? 0));
818
304
  return;
819
305
  } else {
@@ -828,36 +314,36 @@ const main = async () => {
828
314
  console.log(dim(' Project Initializer\n'));
829
315
  separator();
830
316
 
831
- console.log(`\n${bold('Let\'s configure your project.')}`);
317
+ console.log(`\n${bold("Let's configure your project.")}`);
832
318
  console.log(dim(' Use arrow keys to select. Optional fields can be skipped.\n'));
833
319
  console.log(dim(' Skipped fields will be resolved by the agent when first needed.\n'));
834
320
 
835
- // ── Project name ────────────────────────────────────────────────────────────
321
+ // ── Step machine ─────────────────────────────────────────────────────────────
322
+
323
+ const steps = new StepMachine();
324
+
325
+ // ── Project name (step 1) ─────────────────────────────────────────────────────
836
326
 
837
327
  let projectName = '';
838
328
  while (!projectName) {
839
329
  projectName = await ask(`${bold('* Project name')}: `);
840
330
  if (!projectName) console.log(yellow(' Project name is required. Please enter a name.'));
841
331
  }
842
-
843
- const restartIfBack = (val) => {
844
- if (val !== BACK) return false;
845
- rl.close();
846
- const { spawn } = require('child_process');
847
- spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c));
848
- return true;
849
- };
332
+ steps.push(0, 'Project name', projectName);
850
333
 
851
334
  separator();
852
335
 
853
- // ── Client ──────────────────────────────────────────────────────────────────
336
+ // ── Questions loop ────────────────────────────────────────────────────────────
854
337
 
855
- console.log(`\n${bold(blue('Client configuration'))}`);
338
+ let stepIdx = 1;
856
339
 
857
- const clientFw = await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS);
858
- if (restartIfBack(clientFw)) return;
340
+ // Step 1: Client framework
341
+ console.log(`\n${bold(blue('Client configuration'))}`);
342
+ let clientFw = await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS, steps, stepIdx);
343
+ if (clientFw === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
344
+ steps.push(stepIdx++, 'Client framework', clientFw);
859
345
 
860
- // ── Client framework version ─────────────────────────────────────────────────
346
+ // Step 2: Client framework version
861
347
  let clientFwVersion = null;
862
348
  const clientVersions = await fetchLatestVersions(clientFw.value) || FRAMEWORK_VERSION_FALLBACK[clientFw.value] || [];
863
349
  if (clientVersions.length) {
@@ -867,37 +353,52 @@ const main = async () => {
867
353
  value: v,
868
354
  }));
869
355
  const versionLabel = clientFw.value === 'Vite+React' ? '* Vite version:' : `* ${clientFw.value} version:`;
870
- const vIdx = await arrowSelect(versionLabel, versionChoices, rl, true);
871
- if (vIdx === versionChoices.length) { restartIfBack(BACK); return; }
872
- clientFwVersion = clientVersions[vIdx];
356
+ const vIdx = await arrowSelect(versionLabel, [
357
+ ...versionChoices,
358
+ ...steps.navOptions(stepIdx, false).map(n => ({ label: n.label })),
359
+ ]);
360
+ if (vIdx < clientVersions.length) {
361
+ clientFwVersion = clientVersions[vIdx];
362
+ steps.push(stepIdx++, 'Client version', clientFwVersion);
363
+ } else {
364
+ rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return;
365
+ }
873
366
  }
874
367
 
875
- const clientLang = clientFw.language;
876
- const clientState = await selectOptional('State management:', STATE_OPTIONS[clientFw.value] || []);
877
- if (restartIfBack(clientState)) return;
878
- const clientUi = await selectOptional('UI library:', UI_OPTIONS[clientFw.value] || []);
879
- if (restartIfBack(clientUi)) return;
880
- const clientStyle = await selectOptional('Styling:', STYLING_OPTIONS);
881
- if (restartIfBack(clientStyle)) return;
368
+ const clientLang = clientFw.language;
369
+
370
+ // Step 3: State management
371
+ let clientState = await selectOptional('State management:', STATE_OPTIONS[clientFw.value] || [], steps, stepIdx);
372
+ if (clientState === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
373
+ steps.push(stepIdx++, 'State management', clientState);
374
+
375
+ // Step 4: UI library
376
+ let clientUi = await selectOptional('UI library:', UI_OPTIONS[clientFw.value] || [], steps, stepIdx);
377
+ if (clientUi === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
378
+ steps.push(stepIdx++, 'UI library', clientUi);
379
+
380
+ // Step 5: Styling
381
+ let clientStyle = await selectOptional('Styling:', STYLING_OPTIONS, steps, stepIdx);
382
+ if (clientStyle === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
383
+ steps.push(stepIdx++, 'Styling', clientStyle);
882
384
 
883
385
  separator();
884
386
 
885
- // ── Backend ─────────────────────────────────────────────────────────────────
387
+ // ── Backend ──────────────────────────────────────────────────────────────────
886
388
 
887
389
  console.log(`\n${bold(blue('Backend configuration'))}`);
888
390
 
889
- // Check if client framework has integrated backend support
890
391
  let useIntegratedBackend = false;
891
- let backendFw = null;
892
- let backendLang = null;
893
- let backendOrm = null;
894
- let backendAuth = null;
895
- let backendType = null;
392
+ let backendFw = null;
393
+ let backendLang = null;
394
+ let backendOrm = null;
395
+ let backendAuth = null;
396
+ let backendType = null;
896
397
  let backendFwObj = null;
897
398
 
898
399
  if (clientFw.integratedBackend) {
899
400
  console.log(dim(` ${clientFw.value} supports server-side rendering and API routes.\n`));
900
- useIntegratedBackend = await arrowConfirm(`Use integrated backend (${clientFw.value} API routes/SSR) instead of a separate backend?`, rl);
401
+ useIntegratedBackend = await arrowConfirm(`Use integrated backend (${clientFw.value} API routes/SSR) instead of a separate backend?`);
901
402
 
902
403
  if (useIntegratedBackend) {
903
404
  backendType = 'integrated';
@@ -912,13 +413,12 @@ const main = async () => {
912
413
  ...BACKEND_FRAMEWORKS.map(f => ({ label: f.label || f.value })),
913
414
  { label: dim('Skip (decide later)') },
914
415
  ];
915
- const backendIdx = await arrowSelect('Backend framework:', backendChoices, rl);
416
+ const backendIdx = await arrowSelect('Backend framework:', backendChoices);
916
417
  backendFwObj = backendIdx === BACKEND_FRAMEWORKS.length ? null : BACKEND_FRAMEWORKS[backendIdx];
418
+ backendFw = backendFwObj ? backendFwObj.value : null;
419
+ backendLang = backendFwObj ? backendFwObj.language : null;
420
+ steps.push(stepIdx++, 'Backend framework', backendFw);
917
421
 
918
- backendFw = backendFwObj ? backendFwObj.value : null;
919
- backendLang = backendFwObj ? backendFwObj.language : null;
920
-
921
- // ── Backend framework version ──────────────────────────────────────────────
922
422
  if (backendFw) {
923
423
  const backendVersions = await fetchLatestVersions(backendFw) || FRAMEWORK_VERSION_FALLBACK[backendFw] || [];
924
424
  if (backendVersions.length) {
@@ -926,33 +426,40 @@ const main = async () => {
926
426
  label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
927
427
  value: v,
928
428
  }));
929
- const vIdx = await arrowSelect(`* ${backendFw} version:`, vChoices, rl, true);
930
- if (vIdx === vChoices.length) { restartIfBack(BACK); return; }
931
- backendFwObj = { ...backendFwObj, version: backendVersions[vIdx] };
429
+ const vIdx = await arrowSelect(`* ${backendFw} version:`, [
430
+ ...vChoices,
431
+ ...steps.navOptions(stepIdx, false).map(n => ({ label: n.label })),
432
+ ]);
433
+ if (vIdx < backendVersions.length) {
434
+ backendFwObj = { ...backendFwObj, version: backendVersions[vIdx] };
435
+ steps.push(stepIdx++, 'Backend version', backendVersions[vIdx]);
436
+ }
932
437
  }
933
438
  }
934
439
 
935
- // DB type
936
440
  let backendDb = null;
937
441
  if (backendFw) {
938
- backendDb = await selectOptional('Database type:', DB_OPTIONS);
939
- if (restartIfBack(backendDb)) return;
442
+ backendDb = await selectOptional('Database type:', DB_OPTIONS, steps, stepIdx);
443
+ if (backendDb === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
444
+ steps.push(stepIdx++, 'Database', backendDb);
940
445
  }
941
- // ORM / query layer (filtered by DB type)
942
- backendOrm = null;
446
+
943
447
  if (backendFw && backendDb && backendDb !== 'Skip (agent will propose when needed)') {
944
448
  const ormChoices = ORM_OPTIONS_BY_DB[backendDb] || ORM_OPTIONS[backendFw] || [];
945
- backendOrm = await selectOptional('ORM / query layer:', ormChoices);
946
- if (restartIfBack(backendOrm)) return;
449
+ backendOrm = await selectOptional('ORM / query layer:', ormChoices, steps, stepIdx);
450
+ if (backendOrm === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
451
+ steps.push(stepIdx++, 'ORM', backendOrm);
947
452
  }
948
- backendAuth = backendFw ? await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFw] || []) : null;
949
- if (restartIfBack(backendAuth)) return;
453
+
454
+ backendAuth = backendFw ? await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFw] || [], steps, stepIdx) : null;
455
+ if (backendAuth === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
456
+ steps.push(stepIdx++, 'Auth', backendAuth);
950
457
  backendType = backendFw ? 'separate' : null;
951
458
  }
952
459
 
953
460
  separator();
954
461
 
955
- // ── Environment ─────────────────────────────────────────────────────────────
462
+ // ── IDE ───────────────────────────────────────────────────────────────────────
956
463
 
957
464
  console.log(`\n${bold(blue('Environment'))}`);
958
465
 
@@ -960,48 +467,31 @@ const main = async () => {
960
467
  console.log(`\n ${dim('OS detected:')} ${bold(osName)}`);
961
468
  console.log(dim(' Scanning for installed IDEs...\n'));
962
469
 
963
- const ideOptions = buildIDEOptions();
964
-
470
+ const ideOptions = buildIDEOptions(IDE_CANDIDATES);
965
471
  const detectedIDEs = ideOptions.filter(o => o.detected);
966
472
  const undetectedIDEs = ideOptions.filter(o => !o.detected && o.cmd);
967
473
  const manualOption = ideOptions.filter(o => !o.cmd);
968
-
969
- // Detected first → undetected → manual
970
474
  const sortedIdeOptions = [...detectedIDEs, ...undetectedIDEs, ...manualOption];
971
475
 
972
- if (detectedIDEs.length > 1) {
973
- console.log(`\n ${yellow('Multiple IDEs found on this machine')} — select your preference:\n`);
974
- } else if (detectedIDEs.length === 1) {
975
- console.log(`\n ${green(`1 IDE found:`)} ${bold(detectedIDEs[0].name)}\n`);
976
- } else {
977
- console.log(`\n ${yellow('No IDEs detected on this machine.')}\n`);
978
- }
476
+ if (detectedIDEs.length > 1) console.log(`\n ${yellow('Multiple IDEs found on this machine')} — select your preference:\n`);
477
+ else if (detectedIDEs.length === 1) console.log(`\n ${green('1 IDE found:')} ${bold(detectedIDEs[0].name)}\n`);
478
+ else console.log(`\n ${yellow('No IDEs detected on this machine.')}\n`);
979
479
 
980
480
  let ideChoice;
981
481
  while (true) {
982
- ideChoice = await selectRequired('* IDE / editor (required):', sortedIdeOptions);
983
- if (restartIfBack(ideChoice)) return;
482
+ ideChoice = await selectRequired('* IDE / editor (required):', sortedIdeOptions, steps, stepIdx);
483
+ if (ideChoice === RESTART) { rl.close(); spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c)); return; }
984
484
 
985
- // ── Confirmation ──────────────────────────────────────────────────────────
986
485
  if (ideChoice.cmd && !ideChoice.detected) {
987
486
  console.log(`\n ${yellow('⚠')} ${bold(ideChoice.name)} was not detected on this machine.`);
988
487
  console.log(dim(' It may not open automatically when launching a task.\n'));
989
- if (!await arrowConfirm('Continue with this IDE anyway?', rl)) {
990
- console.log(dim(' Re-selecting...\n'));
991
- continue;
992
- }
488
+ if (!await arrowConfirm('Continue with this IDE anyway?')) { console.log(dim(' Re-selecting...\n')); continue; }
993
489
  }
994
490
 
995
- // ── Double-check ──────────────────────────────────────────────────────────
996
- if (!ideChoice.cmd) {
997
- // Manual — no verification needed
998
- console.log(dim(' Manual mode — worktree path will be printed at launch.'));
999
- break;
1000
- }
491
+ if (!ideChoice.cmd) { console.log(dim(' Manual mode — worktree path will be printed at launch.')); break; }
1001
492
 
1002
493
  console.log(dim(`\n Verifying ${ideChoice.name}...`));
1003
494
  const verified = verifyIDE(ideChoice);
1004
-
1005
495
  if (verified.ok) {
1006
496
  const versionStr = verified.version ? dim(` (${verified.version})`) : '';
1007
497
  console.log(` ${green('✓')} ${ideChoice.name} confirmed${versionStr}`);
@@ -1009,39 +499,19 @@ const main = async () => {
1009
499
  }
1010
500
 
1011
501
  console.log(` ${yellow('!')} Could not verify ${ideChoice.name}. The CLI may not be installed or accessible.`);
1012
- if (await arrowConfirm('Continue with this IDE anyway?', rl)) break;
502
+ if (await arrowConfirm('Continue with this IDE anyway?')) break;
1013
503
  console.log(dim(' Re-selecting...\n'));
1014
504
  }
1015
-
505
+ steps.push(stepIdx++, 'IDE', ideChoice.name);
1016
506
 
1017
507
  // ── Terminal detection ────────────────────────────────────────────────────────
1018
508
 
1019
- const detectTerminal = () => {
1020
- const platform = process.platform;
1021
- if (platform === 'darwin') {
1022
- const apps = [
1023
- { name: 'iTerm2', cmd: 'iTerm2', path: '/Applications/iTerm.app' },
1024
- { name: 'Warp', cmd: 'Warp', path: '/Applications/Warp.app' },
1025
- { name: 'Terminal.app', cmd: 'Terminal', path: '/System/Applications/Utilities/Terminal.app' },
1026
- ];
1027
- return apps.find(a => fs.existsSync(a.path)) || { name: 'Terminal.app', cmd: 'Terminal' };
1028
- } else if (platform === 'win32') {
1029
- const wtPath = process.env.LOCALAPPDATA + '\\Microsoft\\WindowsApps\\wt.exe';
1030
- if (fs.existsSync(wtPath)) return { name: 'Windows Terminal', cmd: 'wt' };
1031
- return { name: 'Command Prompt', cmd: 'cmd' };
1032
- } else {
1033
- const terms = ['gnome-terminal', 'konsole', 'xterm'];
1034
- for (const t of terms) {
1035
- try { execSync('which ' + t, { stdio: 'pipe' }); return { name: t, cmd: t }; } catch {}
1036
- }
1037
- return { name: 'xterm', cmd: 'xterm' };
1038
- }
1039
- };
1040
-
1041
509
  const termChoice = detectTerminal();
1042
510
  console.log(dim(' Terminal detected: ') + green(termChoice.name));
1043
511
 
1044
- // ── Summary ─────────────────────────────────────────────────────────────────
512
+ separator();
513
+
514
+ // ── Summary ───────────────────────────────────────────────────────────────────
1045
515
 
1046
516
  console.log(`\n${bold('Review your configuration:')}\n`);
1047
517
  summaryLine('Project', projectName);
@@ -1050,7 +520,7 @@ const main = async () => {
1050
520
  summaryLine('State management', clientState);
1051
521
  summaryLine('UI library', clientUi);
1052
522
  summaryLine('Styling', clientStyle);
1053
- summaryLine('Backend type', backendType === 'integrated' ? `${clientFw.value} integrated` : backendFw || '(skipped)');
523
+ summaryLine('Backend type', backendType === 'integrated' ? `${clientFw.value} integrated` : backendFw || '(skipped)');
1054
524
  if (backendType !== 'integrated') {
1055
525
  summaryLine('Backend language', backendLang);
1056
526
  summaryLine('ORM', backendOrm);
@@ -1060,18 +530,17 @@ const main = async () => {
1060
530
 
1061
531
  console.log('');
1062
532
  console.log(dim(' y = confirm | n = abort | e = edit (start over)\n'));
533
+
1063
534
  const confirmIdx = await arrowSelect('Confirm and write to config files?', [
1064
535
  { label: `${green('✓')} Confirm — write config and set up project` },
1065
536
  { label: `${yellow('↺')} Restart — redo configuration` },
1066
537
  { label: `${red('✗')} Abort` },
1067
- ], rl);
538
+ ]);
1068
539
 
1069
540
  if (confirmIdx === 1) {
1070
541
  console.log(yellow('\n Restarting configuration...\n'));
1071
542
  rl.close();
1072
- const { spawn } = require('child_process');
1073
- const child = spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT });
1074
- child.on('exit', (code) => process.exit(code));
543
+ spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c));
1075
544
  return;
1076
545
  }
1077
546
 
@@ -1081,33 +550,15 @@ const main = async () => {
1081
550
  return;
1082
551
  }
1083
552
 
1084
- // ── Write configs ────────────────────────────────────────────────────────────
553
+ // ── Write configs ─────────────────────────────────────────────────────────────
1085
554
 
1086
555
  separator();
1087
556
  console.log(`\n${bold('Setting up your project...')}\n`);
1088
557
 
1089
- // ── Load bundled core ───────────────────────────────────────────────────────
1090
-
1091
- const CORE_DIR = path.join(__dirname, 'core');
1092
-
1093
- console.log(` ${green('✓')} Templates ready`);
1094
-
558
+ const CORE_DIR = path.join(__dirname, 'core');
1095
559
  const TEMPLATES = path.join(CORE_DIR, 'templates');
1096
560
 
1097
- // ── Copy scope directories (app code + CLAUDE.md only) ─────────────────────
1098
- // agents/ and frameworks/ are now at repo root as .agents/ and .frameworks/
1099
- // We copy client/backend/shared but exclude agents/ and frameworks/ subdirs
1100
-
1101
- const copyDirExcluding = (src, dest, exclude = []) => {
1102
- fs.mkdirSync(dest, { recursive: true });
1103
- for (const entry of fs.readdirSync(src)) {
1104
- if (exclude.includes(entry)) continue;
1105
- const srcFile = path.join(src, entry);
1106
- const destFile = path.join(dest, entry);
1107
- if (fs.statSync(srcFile).isDirectory()) copyDirExcluding(srcFile, destFile, []);
1108
- else fs.copyFileSync(srcFile, destFile);
1109
- }
1110
- };
561
+ console.log(` ${green('✓')} Templates ready`);
1111
562
 
1112
563
  copyDirExcluding(path.join(TEMPLATES, 'client'), path.join(ROOT, 'client'), ['agents', 'frameworks']);
1113
564
  if (fs.existsSync(path.join(TEMPLATES, 'shared')))
@@ -1117,37 +568,27 @@ const main = async () => {
1117
568
  fs.writeFileSync(path.join(ROOT, 'backend', '.gitkeep'), '', 'utf8');
1118
569
  }
1119
570
 
1120
- // ── Copy agents and frameworks to repo root as .agents/ and .frameworks/ ────
1121
-
1122
- copyDir(path.join(TEMPLATES, '.agents', 'client'), path.join(ROOT, '.agents', 'client'));
1123
- copyDir(path.join(TEMPLATES, '.frameworks', 'client'), path.join(ROOT, '.frameworks', 'client'));
1124
- copyDir(path.join(TEMPLATES, '.agents', 'shared'), path.join(ROOT, '.agents', 'shared'));
571
+ copyDir(path.join(TEMPLATES, '.agents', 'client'), path.join(ROOT, '.agents', 'client'));
572
+ copyDir(path.join(TEMPLATES, '.frameworks', 'client'), path.join(ROOT, '.frameworks', 'client'));
573
+ copyDir(path.join(TEMPLATES, '.agents', 'shared'), path.join(ROOT, '.agents', 'shared'));
1125
574
  if (backendType === 'separate') {
1126
- copyDir(path.join(TEMPLATES, '.agents', 'backend'), path.join(ROOT, '.agents', 'backend'));
1127
- copyDir(path.join(TEMPLATES, '.frameworks', 'backend'), path.join(ROOT, '.frameworks', 'backend'));
575
+ copyDir(path.join(TEMPLATES, '.agents', 'backend'), path.join(ROOT, '.agents', 'backend'));
576
+ copyDir(path.join(TEMPLATES, '.frameworks', 'backend'), path.join(ROOT, '.frameworks', 'backend'));
1128
577
  }
1129
578
 
1130
- fs.copyFileSync(path.join(TEMPLATES, 'CLAUDE.md'), path.join(ROOT, 'CLAUDE.md'));
1131
- fs.copyFileSync(path.join(TEMPLATES, 'CONTRACTS.md'), path.join(ROOT, 'CONTRACTS.md'));
579
+ fs.copyFileSync(path.join(TEMPLATES, 'CLAUDE.md'), path.join(ROOT, 'CLAUDE.md'));
580
+ fs.copyFileSync(path.join(TEMPLATES, 'CONTRACTS.md'), path.join(ROOT, 'CONTRACTS.md'));
1132
581
  fs.copyFileSync(path.join(TEMPLATES, 'TASKS_HISTORY.md'), path.join(ROOT, 'TASKS_HISTORY.md'));
1133
- fs.copyFileSync(path.join(TEMPLATES, 'CLOUD_STATE.md'), path.join(ROOT, 'CLOUD_STATE.md'));
582
+ fs.copyFileSync(path.join(TEMPLATES, 'CLOUD_STATE.md'), path.join(ROOT, 'CLOUD_STATE.md'));
1134
583
  console.log(` ${green('✓')} Templates copied`);
1135
584
 
1136
- // ── Copy workflow scripts ────────────────────────────────────────────────────
1137
-
1138
585
  const WORKFLOW_SRC = path.join(CORE_DIR, 'workflow');
1139
586
  const WORKFLOW_DEST = path.join(ROOT, '.workflow');
1140
587
  fs.mkdirSync(WORKFLOW_DEST, { recursive: true });
1141
588
  copyDir(WORKFLOW_SRC, WORKFLOW_DEST);
1142
589
  console.log(` ${green('✓')} Workflow scripts copied (.workflow/)`);
1143
- console.log(` ${green('✓')} Temporary files cleaned up`);
1144
-
1145
- // ── Write @config values ─────────────────────────────────────────────────────
1146
590
 
1147
- writeConfig(path.join(ROOT, 'CLAUDE.md'), {
1148
- PROJECT_NAME: projectName,
1149
- PROJECT_ROOT: projectName,
1150
- });
591
+ writeConfig(path.join(ROOT, 'CLAUDE.md'), { PROJECT_NAME: projectName, PROJECT_ROOT: projectName });
1151
592
  console.log(` ${green('✓')} CLAUDE.md configured`);
1152
593
 
1153
594
  writeConfig(path.join(ROOT, 'client', 'CLAUDE.md'), {
@@ -1173,23 +614,20 @@ const main = async () => {
1173
614
  console.log(` ${green('✓')} backend/CLAUDE.md configured`);
1174
615
  }
1175
616
 
1176
- ensureGitignore('worktrees/');
1177
- ensureGitignore('.scaffold/');
1178
- ensureGitignore('.workflow/');
1179
- ensureGitignore('node_modules/');
617
+ ensureGitignore(ROOT, 'worktrees/');
618
+ ensureGitignore(ROOT, '.scaffold/');
619
+ ensureGitignore(ROOT, '.workflow/');
620
+ ensureGitignore(ROOT, 'node_modules/');
1180
621
 
1181
- // Remove template-specific gitignore entries so generated files can be committed
1182
622
  const gitignorePath = path.join(ROOT, '.gitignore');
1183
623
  let gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
1184
624
  ['client/', 'backend/', 'shared/', 'CLAUDE.md', 'CONTRACTS.md', 'BUILD_STATE.md', 'TASKS_HISTORY.md', 'CLOUD_STATE.md'].forEach(entry => {
1185
- gitignoreContent = gitignoreContent.replace(`\n${entry}`, '');
1186
- gitignoreContent = gitignoreContent.replace(`${entry}\n`, '');
1187
- gitignoreContent = gitignoreContent.replace(entry, '');
625
+ gitignoreContent = gitignoreContent.replace(`\n${entry}`, '').replace(`${entry}\n`, '').replace(entry, '');
1188
626
  });
1189
627
  fs.writeFileSync(gitignorePath, gitignoreContent.trim() + '\n', 'utf8');
1190
628
  console.log(` ${green('✓')} .gitignore updated`);
1191
629
 
1192
- // ── Write .config.json ───────────────────────────────────────────────────────
630
+ // ── .config.json ─────────────────────────────────────────────────────────────
1193
631
 
1194
632
  const config = {
1195
633
  projectName,
@@ -1228,34 +666,24 @@ const main = async () => {
1228
666
  },
1229
667
  };
1230
668
 
1231
- fs.writeFileSync(
1232
- path.join(RUNTIME_DIR, '.config.json'),
1233
- JSON.stringify(config, null, 2),
1234
- 'utf8'
1235
- );
669
+ fs.writeFileSync(path.join(RUNTIME_DIR, '.config.json'), JSON.stringify(config, null, 2), 'utf8');
1236
670
  console.log(` ${green('✓')} .scaffold/.config.json written`);
1237
671
 
1238
- // ── Write scope-policy.json ──────────────────────────────────────────────────
672
+ // ── scope-policy.json ─────────────────────────────────────────────────────────
1239
673
 
1240
674
  const scopePolicy = {
1241
675
  client: {
1242
676
  allowed: ['client/**'],
1243
677
  blocked: ['backend/**', 'shared/**', 'CONTRACTS.md', 'BUILD_STATE.md', 'TASKS_HISTORY.md'],
1244
678
  agentOverrides: {
1245
- UI: {
1246
- allowed: ['client/**', 'shared/wiring.config.json'],
1247
- onlyBeforeScaffolded: true,
1248
- },
679
+ UI: { allowed: ['client/**', 'shared/wiring.config.json'], onlyBeforeScaffolded: true },
1249
680
  },
1250
681
  },
1251
682
  backend: {
1252
683
  allowed: ['backend/**'],
1253
684
  blocked: ['client/**', 'shared/**', 'CONTRACTS.md', 'BUILD_STATE.md', 'TASKS_HISTORY.md'],
1254
685
  agentOverrides: {
1255
- INIT: {
1256
- allowed: ['backend/**', 'shared/wiring.config.json', 'CONTRACTS.md'],
1257
- onlyBeforeScaffolded: true,
1258
- },
686
+ INIT: { allowed: ['backend/**', 'shared/wiring.config.json', 'CONTRACTS.md'], onlyBeforeScaffolded: true },
1259
687
  },
1260
688
  },
1261
689
  shared: {
@@ -1264,22 +692,16 @@ const main = async () => {
1264
692
  },
1265
693
  };
1266
694
 
1267
- fs.writeFileSync(
1268
- path.join(RUNTIME_DIR, 'scope-policy.json'),
1269
- JSON.stringify(scopePolicy, null, 2),
1270
- 'utf8'
1271
- );
695
+ fs.writeFileSync(path.join(RUNTIME_DIR, 'scope-policy.json'), JSON.stringify(scopePolicy, null, 2), 'utf8');
1272
696
  console.log(` ${green('✓')} .scaffold/scope-policy.json written`);
1273
697
 
1274
- // ── Generate BUILD_STATE.md ──────────────────────────────────────────────────
698
+ // ── BUILD_STATE.md ────────────────────────────────────────────────────────────
1275
699
 
1276
700
  const backendDisplay = backendType === 'integrated'
1277
701
  ? `${clientFw.value} integrated (API routes/SSR)`
1278
702
  : backendFw || 'Not configured';
1279
703
 
1280
- const clientStack = [clientFw.value, clientLang, clientStyle, clientUi, clientState]
1281
- .filter(Boolean).join(' + ');
1282
-
704
+ const clientStack = [clientFw.value, clientLang, clientStyle, clientUi, clientState].filter(Boolean).join(' + ');
1283
705
  const backendStack = backendType === 'separate'
1284
706
  ? [backendFw, backendLang, backendOrm, backendAuth].filter(Boolean).join(' + ')
1285
707
  : backendDisplay;
@@ -1335,10 +757,6 @@ Before starting any task, verify:
1335
757
  - Backend AUTH requires: DB User entity done
1336
758
  - Any cross-boundary types: Must exist in CONTRACTS.md first
1337
759
 
1338
- If a dependency is not met:
1339
- DEPENDENCY NOT MET - surface what is missing and propose options.
1340
- Never proceed silently on a missing dependency.
1341
-
1342
760
  ## Agent Log
1343
761
  | Date | Agent | Scope | Task | Status | Branch |
1344
762
  |------|-------|-------|------|--------|--------|
@@ -1347,15 +765,13 @@ If a dependency is not met:
1347
765
  fs.writeFileSync(path.join(ROOT, 'BUILD_STATE.md'), buildState, 'utf8');
1348
766
  console.log(` ${green('✓')} BUILD_STATE.md generated`);
1349
767
 
1350
- // ── Generate user project package.json ───────────────────────────────────────
768
+ // ── package.json ──────────────────────────────────────────────────────────────
1351
769
 
1352
770
  const userPackage = {
1353
771
  name: projectName.toLowerCase().replace(/\s+/g, '-'),
1354
772
  version: '1.0.0',
1355
773
  private: true,
1356
- dependencies: {
1357
- prompts: '^2.4.2',
1358
- },
774
+ dependencies: { prompts: '^2.4.2' },
1359
775
  scripts: {
1360
776
  init: 'multi-agents init',
1361
777
  agent: 'node .workflow/agent.js',
@@ -1367,8 +783,6 @@ If a dependency is not met:
1367
783
  fs.writeFileSync(path.join(ROOT, 'package.json'), JSON.stringify(userPackage, null, 2), 'utf8');
1368
784
  console.log(` ${green('✓')} package.json generated`);
1369
785
 
1370
- // ── Install dependencies ──────────────────────────────────────────────────────
1371
-
1372
786
  try {
1373
787
  console.log(dim(' Installing dependencies...'));
1374
788
  execSync('npm install', { cwd: ROOT, stdio: 'pipe' });
@@ -1381,17 +795,16 @@ If a dependency is not met:
1381
795
 
1382
796
  const trackingPath = path.join(RUNTIME_DIR, '.tracking.json');
1383
797
  if (!fs.existsSync(trackingPath)) {
1384
- const trackingStructure = generateTrackingStructure(config);
1385
- fs.writeFileSync(trackingPath, JSON.stringify(trackingStructure, null, 2), 'utf8');
798
+ fs.writeFileSync(trackingPath, JSON.stringify(generateTrackingStructure(config), null, 2), 'utf8');
1386
799
  console.log(` ${green('✓')} .tracking.json generated`);
1387
800
  } else {
1388
801
  console.log(dim(' ℹ .tracking.json already exists — preserved'));
1389
802
  }
1390
803
 
1391
- // ── Generate .paths.json ──────────────────────────────────────────────────────
804
+ // ── .paths.json ───────────────────────────────────────────────────────────────
1392
805
 
1393
806
  const pathsMap = {};
1394
- const clientConventions = FRAMEWORK_CONVENTIONS.client[clientFw?.value] || {};
807
+ const clientConventions = FRAMEWORK_CONVENTIONS.client[clientFw?.value] || {};
1395
808
  const backendConventions = FRAMEWORK_CONVENTIONS.backend[backendFwObj?.value] || {};
1396
809
 
1397
810
  if (Object.keys(clientConventions).length) {
@@ -1400,7 +813,6 @@ If a dependency is not met:
1400
813
  pathsMap.client[key] = { expected: value, current: null, status: 'pending' };
1401
814
  });
1402
815
  }
1403
-
1404
816
  if (Object.keys(backendConventions).length) {
1405
817
  pathsMap.backend = {};
1406
818
  Object.entries(backendConventions).forEach(([key, value]) => {
@@ -1411,7 +823,7 @@ If a dependency is not met:
1411
823
  fs.writeFileSync(path.join(RUNTIME_DIR, '.paths.json'), JSON.stringify(pathsMap, null, 2), 'utf8');
1412
824
  console.log(` ${green('✓')} .paths.json generated`);
1413
825
 
1414
- // ── Lock ─────────────────────────────────────────────────────────────────────
826
+ // ── Lock ──────────────────────────────────────────────────────────────────────
1415
827
 
1416
828
  fs.writeFileSync(LOCK_FILE, new Date().toISOString());
1417
829
  console.log(` ${green('✓')} Initialization locked`);
@@ -1422,16 +834,15 @@ If a dependency is not met:
1422
834
  execSync('git add .', { cwd: ROOT, stdio: 'pipe' });
1423
835
  execSync('git commit -m "init: project configuration"', { cwd: ROOT, stdio: 'pipe' });
1424
836
  console.log(` ${green('✓')} Project configuration committed`);
1425
- } catch (err) {
837
+ } catch {
1426
838
  console.log(` ${yellow('!')} Could not auto-commit. Run manually:`);
1427
839
  console.log(dim(' git add . && git commit -m "init: project configuration"'));
1428
840
  }
1429
841
 
1430
- // ── Pre-commit hook — block direct commits to main ───────────────────────────
842
+ // ── Pre-commit hook ───────────────────────────────────────────────────────────
1431
843
 
1432
844
  try {
1433
- const hooksDir = path.join(ROOT, '.git', 'hooks');
1434
- const hookPath = path.join(hooksDir, 'pre-commit');
845
+ const hookPath = path.join(ROOT, '.git', 'hooks', 'pre-commit');
1435
846
  const hookScript = `#!/bin/sh
1436
847
  branch=$(git symbolic-ref --short HEAD 2>/dev/null)
1437
848
  if [ "$branch" = "main" ]; then
@@ -1448,11 +859,11 @@ fi
1448
859
  }
1449
860
  } catch { /* best-effort */ }
1450
861
 
1451
- // ── Remote setup ─────────────────────────────────────────────────────────────
862
+ // ── Remote setup ──────────────────────────────────────────────────────────────
1452
863
 
1453
864
  setupUserRemote(ROOT, projectName);
1454
865
 
1455
- // ── Trajectory selection ─────────────────────────────────────────────────────
866
+ // ── Trajectory selection ──────────────────────────────────────────────────────
1456
867
 
1457
868
  separator();
1458
869
  console.log(`\n${bold(green(' Project initialized successfully!'))}\n`);
@@ -1474,69 +885,12 @@ fi
1474
885
  console.log(`${dim(' you for areas where requirements are still evolving')}`);
1475
886
  console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
1476
887
 
1477
- const TRAJECTORY_DETAILS = {
1478
- '1': {
1479
- label: 'Multi-Agent Driven Orchestration',
1480
- full: [
1481
- 'Every task must start with npm run agent.',
1482
- 'Agent sessions load only task-relevant context, enabling reliable',
1483
- 'chaining, predictable behavior, and efficient token usage.',
1484
- '',
1485
- '⚠ If you commit directly to main yourself, you bypass the framework',
1486
- ' and break task tracking for any active agent branches.',
1487
- '',
1488
- 'Benefits',
1489
- '· Scoped context per task',
1490
- '· Predictable token consumption',
1491
- '· Lower cost than maintaining large, persistent sessions',
1492
- '· Better isolation between parallel work streams',
1493
- ],
1494
- next: 'launch',
1495
- },
1496
- '2': {
1497
- label: 'Shared Orchestration',
1498
- full: [
1499
- 'You and agents work in the same codebase, each with clearly',
1500
- 'defined ownership. File boundaries must be established before',
1501
- 'work begins and remain fixed throughout the task.',
1502
- 'Agents excel when scope is well-defined;',
1503
- 'you excel when requirements are evolving.',
1504
- '',
1505
- 'Use agents for',
1506
- '· Multi-file features',
1507
- '· Structured implementation work',
1508
- '· Domain-specific tasks',
1509
- '· Changes expected to exceed ~200 lines',
1510
- '',
1511
- 'Handle manually',
1512
- '· Targeted bug fixes',
1513
- '· Configuration changes',
1514
- '· Small refactors',
1515
- '· Single-file edits under ~50 lines',
1516
- '',
1517
- '⚠ Avoid overlapping file ownership. Working on the same files',
1518
- ' as an active agent will create merge conflicts when merged.',
1519
- '⚠ If you are spending time repeatedly clarifying scope, stop',
1520
- ' and do the task yourself. The coordination cost often',
1521
- ' exceeds the implementation cost.',
1522
- '',
1523
- 'Benefits',
1524
- '· Maximum agent efficiency for well-defined work',
1525
- '· Human flexibility where requirements change',
1526
- '· Scales well across large projects',
1527
- '· Most adaptable workflow — requires the most discipline',
1528
- ],
1529
- next: 'launch',
1530
- },
1531
- };
1532
-
1533
- // Wrap in loop to support back navigation
1534
888
  let trajectory = null;
1535
889
  trajectoryLoop: while (true) {
1536
890
  const trajIdx = await arrowSelect('How do you want to build?', [
1537
891
  { label: bold('Multi-Agent Driven Orchestration') },
1538
892
  { label: bold('Shared Orchestration') },
1539
- ], rl);
893
+ ]);
1540
894
  trajectory = String(trajIdx + 1);
1541
895
 
1542
896
  const selected = TRAJECTORY_DETAILS[trajectory];
@@ -1545,11 +899,11 @@ fi
1545
899
  renderTrajectoryLines(selected.full);
1546
900
  console.log('');
1547
901
 
1548
- const confirmIdx = await arrowSelect('Confirm?', [
902
+ const confirmIdx2 = await arrowSelect('Confirm?', [
1549
903
  { label: `${green('✓')} Confirm` },
1550
904
  { label: `${yellow('←')} Back — pick differently` },
1551
- ], rl);
1552
- if (confirmIdx === 0) break trajectoryLoop;
905
+ ]);
906
+ if (confirmIdx2 === 0) break trajectoryLoop;
1553
907
  trajectory = null;
1554
908
  separator();
1555
909
  console.log(`\n ${bold('How do you want to build?')}\n`);
@@ -1571,7 +925,6 @@ fi
1571
925
 
1572
926
  const selected = TRAJECTORY_DETAILS[trajectory];
1573
927
 
1574
- // Store trajectory in config
1575
928
  try {
1576
929
  const cfg = JSON.parse(fs.readFileSync(path.join(RUNTIME_DIR, '.config.json'), 'utf8'));
1577
930
  cfg.trajectory = selected.label.toLowerCase().replace(/ /g, '-');
@@ -1579,49 +932,7 @@ fi
1579
932
  } catch { /* best-effort */ }
1580
933
 
1581
934
  if (selected.next === 'launch') {
1582
- separator();
1583
- console.log(`\n${bold(green(' Project initialized successfully!'))}\n`);
1584
-
1585
- // ── Summary block ─────────────────────────────────────────────────────────
1586
- const bt = config.backend?.type;
1587
-
1588
- console.log(` ${dim('Project')} : ${bold(projectName)}`);
1589
- console.log(` ${dim('Client')} : ${config.client.framework} / ${config.client.language}${config.client.uiLibrary ? ' / ' + config.client.uiLibrary : ''}`);
1590
- if (bt === 'separate') {
1591
- console.log(` ${dim('Backend')} : ${config.backend.framework} / ${config.backend.language}${config.backend.orm ? ' / ' + config.backend.orm : ''}`);
1592
- } else {
1593
- console.log(` ${dim('Backend')} : integrated (API routes / SSR)`);
1594
- }
1595
- console.log(` ${dim('Workflow')} : ${selected.label}\n`);
1596
-
1597
- console.log(` ${dim('Files generated:')}`);
1598
- console.log(` ${green('+')} CLAUDE.md, client/CLAUDE.md${bt === 'separate' ? ', backend/CLAUDE.md' : ''}`);
1599
- console.log(` ${green('+')} BUILD_STATE.md, TASKS_HISTORY.md, CONTRACTS.md`);
1600
- console.log(` ${green('+')} CLOUD_STATE.md, shared/wiring.config.json`);
1601
- console.log(` ${green('+')} .scaffold/.config.json, .scaffold/scope-policy.json`);
1602
- console.log(` ${green('+')} .agents/, .frameworks/, .workflow/\n`);
1603
-
1604
- console.log(` ${dim('Git:')}`);
1605
- console.log(` ${green('+')} Repository initialized on main`);
1606
- console.log(` ${green('+')} Pre-commit hook installed (direct main commits blocked)`);
1607
- console.log(` ${green('+')} Initial commit created\n`);
1608
-
1609
- console.log(` ${dim('Agents available:')}`);
1610
- console.log(` ${dim('client')} : UI, LOGIC, FORMS, ROUTING, ACCESSIBILITY, TESTING`);
1611
- if (bt === 'separate') {
1612
- console.log(` ${dim('backend')} : INIT, API, AUTH, DB, LOGIC, EVENTS, JOBS, TESTING`);
1613
- }
1614
- console.log(` ${dim('shared')} : CLOUD, SECURITY\n`);
1615
-
1616
- console.log(` ${dim('Starting your first agent session...\n')}`);
1617
- separator();
1618
- console.log('');
1619
- rl.close();
1620
- const { spawn: sp } = require('child_process');
1621
- const agentProc = sp('npm', ['run', 'agent'], { cwd: ROOT, stdio: 'inherit' });
1622
- agentProc.on('error', (err) => {
1623
- console.error(' Could not start agent:', err.message);
1624
- });
935
+ printInitSummary({ projectName, config, selectedLabel: selected.label, ROOT, rl });
1625
936
  return;
1626
937
  }
1627
938
 
@@ -1636,4 +947,4 @@ fi
1636
947
  main().catch((err) => {
1637
948
  console.error('\n Error:', err.message);
1638
949
  process.exit(1);
1639
- });
950
+ });