multi-agents-cli 1.0.17 → 1.0.19

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.
Files changed (2) hide show
  1. package/init.js +904 -111
  2. package/package.json +1 -1
package/init.js CHANGED
@@ -1,16 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Multi-Agent Monorepo Template - Project Initializer
5
- * Run with: npm run init
4
+ * multi-agents - Project Initializer
5
+ * Run with: npm run init (inside existing project)
6
+ * or: multi-agents init my-project (global CLI)
6
7
  *
7
8
  * Runs once. Locked after completion via .scaffold/.initialized
8
- * Delete .scaffold/.initialized to re-run.
9
9
  */
10
10
 
11
11
  const readline = require('readline');
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
+
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');
14
58
  const { execSync, spawn } = require('child_process');
15
59
 
16
60
  // ── Colors ────────────────────────────────────────────────────────────────────
@@ -34,58 +78,69 @@ const cyan = (s) => `${c.cyan}${s}${c.reset}`;
34
78
  const blue = (s) => `${c.blue}${s}${c.reset}`;
35
79
  const red = (s) => `${c.red}${s}${c.reset}`;
36
80
 
37
- // ── Lock check ────────────────────────────────────────────────────────────────
81
+ // ── CLI argument handling ─────────────────────────────────────────────────────
38
82
 
39
- const ROOT = __dirname;
40
- const RUNTIME_DIR = path.join(ROOT, '.scaffold');
41
- const LOCK_FILE = path.join(RUNTIME_DIR, '.initialized');
42
-
43
- // Ensure .scaffold/ runtime dir exists
44
- const fs_temp = require('fs');
45
- if (!fs_temp.existsSync(path.join(__dirname, '.scaffold'))) {
46
- fs_temp.mkdirSync(path.join(__dirname, '.scaffold'), { recursive: true });
47
- }
83
+ const args = process.argv.slice(2);
84
+ const isGlobalCLI = args[0] === 'init' && args[1];
85
+ const projectArg = isGlobalCLI ? args[1] : null;
48
86
 
49
- if (fs.existsSync(LOCK_FILE)) {
50
- const ts = fs.readFileSync(LOCK_FILE, 'utf8').trim();
51
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
52
- const ask2 = (q) => new Promise((resolve) => rl2.question(q, (a) => resolve(a.trim())));
87
+ if (isGlobalCLI) {
88
+ const targetDir = path.resolve(process.cwd(), projectArg);
53
89
 
54
- console.log(`\n${yellow(' This project has already been initialized.')}`);
55
- console.log(dim(` Initialized on: ${ts}\n`));
56
- console.log(` ${dim('1.')} Continue run ${cyan('npm run launch')}`);
57
- console.log(` ${dim('2.')} Reset — delete config and re-run initialization`);
58
- console.log(` ${dim('3.')} Exit\n`);
90
+ if (fs.existsSync(targetDir)) {
91
+ console.log(`\n${red(` Directory "${projectArg}" already exists.`)}`);
92
+ console.log(dim(' Choose a different project name.\n'));
93
+ process.exit(1);
94
+ }
59
95
 
60
- const choice = await ask2(` ${bold('Select')} ${dim('(1-3)')}: `);
96
+ fs.mkdirSync(targetDir, { recursive: true });
97
+ process.chdir(targetDir);
61
98
 
62
- if (choice === '1') {
63
- console.log('');
64
- rl2.close();
65
- const { spawn } = require('child_process');
66
- const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
67
- stdio: 'inherit',
68
- cwd: ROOT,
69
- });
70
- child.on('exit', (code) => process.exit(code));
71
- return;
72
- } else if (choice === '2') {
73
- console.log(yellow('\n Resetting configuration...\n'));
74
- fs.unlinkSync(LOCK_FILE);
75
- const configPath = path.join(__dirname, '.config.json');
76
- if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
77
- rl2.close();
78
- console.log(green(' Reset complete. Re-running initialization...\n'));
79
- // Continue to main() below
80
- } else {
81
- console.log(dim('\n Exited.\n'));
82
- rl2.close();
83
- process.exit(0);
99
+ // Initialize git
100
+ try {
101
+ execSync('git init -b main', { cwd: targetDir, stdio: 'pipe' });
102
+ execSync('git commit --allow-empty -m "init: project created"', { cwd: targetDir, stdio: 'pipe' });
103
+ } catch {
104
+ // Fallback for older git versions that don't support -b flag
105
+ try {
106
+ execSync('git init', { cwd: targetDir, stdio: 'pipe' });
107
+ execSync('git checkout -b main', { cwd: targetDir, stdio: 'pipe' });
108
+ execSync('git commit --allow-empty -m "init: project created"', { cwd: targetDir, stdio: 'pipe' });
109
+ } catch { /* continue */ }
84
110
  }
85
111
  }
86
112
 
113
+ // ── Lock check ────────────────────────────────────────────────────────────────
114
+
115
+ const ROOT = process.cwd();
116
+ const RUNTIME_DIR = path.join(ROOT, '.scaffold');
117
+ const LOCK_FILE = path.join(RUNTIME_DIR, '.initialized');
118
+
119
+ // Ensure .scaffold/ exists
120
+ if (!fs.existsSync(RUNTIME_DIR)) {
121
+ fs.mkdirSync(RUNTIME_DIR, { recursive: true });
122
+ }
123
+
87
124
  // ── Decision tree ─────────────────────────────────────────────────────────────
88
125
 
126
+ const FRAMEWORK_CONVENTIONS = {
127
+ client: {
128
+ 'Next.js': { root: 'client', typesDir: 'client/src/types', importAlias: '@/types' },
129
+ 'Angular': { root: 'client', typesDir: 'client/src/app/core/types', importAlias: null },
130
+ 'Nuxt': { root: 'client', typesDir: 'client/types', importAlias: '~/types' },
131
+ 'SvelteKit': { root: 'client', typesDir: 'client/src/lib/types', importAlias: '$lib/types' },
132
+ 'Vite+React': { root: 'client', typesDir: 'client/src/types', importAlias: null },
133
+ 'Remix': { root: 'client', typesDir: 'client/app/types', importAlias: null },
134
+ },
135
+ backend: {
136
+ 'Express': { root: 'backend', typesDir: 'backend/src/types', routesDir: 'backend/src/routes' },
137
+ 'NestJS': { root: 'backend', dtoDir: 'backend/src/dto', entitiesDir:'backend/src/entities' },
138
+ 'Fastify': { root: 'backend', typesDir: 'backend/src/types', routesDir: 'backend/src/routes' },
139
+ 'FastAPI': { root: 'backend', schemasDir: 'backend/app/schemas', modelsDir: 'backend/app/models' },
140
+ 'Django': { root: 'backend', schemasDir: 'backend/api/serializers', modelsDir: 'backend/api/models' },
141
+ },
142
+ };
143
+
89
144
  const CLIENT_FRAMEWORKS = [
90
145
  { label: 'Next.js', value: 'Next.js', language: 'TypeScript', integratedBackend: true },
91
146
  { label: 'Angular', value: 'Angular', language: 'TypeScript', integratedBackend: false },
@@ -100,10 +155,92 @@ const BACKEND_FRAMEWORKS = [
100
155
  { label: 'Express', value: 'Express', language: 'TypeScript' },
101
156
  { label: 'Fastify', value: 'Fastify', language: 'TypeScript' },
102
157
  { label: 'Django', value: 'Django', language: 'Python' },
158
+ { label: 'FastAPI', value: 'FastAPI', language: 'Python' },
103
159
  { label: 'Laravel', value: 'Laravel', language: 'PHP' },
104
160
  { label: 'Rails', value: 'Rails', language: 'Ruby' },
105
161
  ];
106
162
 
163
+ // ── Framework version registry ────────────────────────────────────────────────
164
+
165
+ const FRAMEWORK_REGISTRY = {
166
+ 'Next.js': { registry: 'npm', package: 'next' },
167
+ 'Angular': { registry: 'npm', package: '@angular/core' },
168
+ 'Nuxt': { registry: 'npm', package: 'nuxt' },
169
+ 'SvelteKit': { registry: 'npm', package: '@sveltejs/kit' },
170
+ 'Remix': { registry: 'npm', package: '@remix-run/react' },
171
+ 'Vite+React': { registry: 'npm', package: 'vite' },
172
+ 'NestJS': { registry: 'npm', package: '@nestjs/core' },
173
+ 'Express': { registry: 'npm', package: 'express' },
174
+ 'Fastify': { registry: 'npm', package: 'fastify' },
175
+ 'FastAPI': { registry: 'pypi', package: 'fastapi' },
176
+ 'Django': { registry: 'pypi', package: 'django' },
177
+ 'Laravel': { registry: 'npm', package: null }, // skip — no npm package
178
+ 'Rails': { registry: 'npm', package: null }, // skip — no npm package
179
+ };
180
+
181
+ const FRAMEWORK_VERSION_FALLBACK = {
182
+ 'Next.js': ['15', '14', '13'],
183
+ 'Angular': ['22', '21', '20'],
184
+ 'Nuxt': ['3', '2', null],
185
+ 'SvelteKit': ['2', '1', null],
186
+ 'Remix': ['2', '1', null],
187
+ 'Vite+React': ['6', '5', '4'],
188
+ 'NestJS': ['11', '10', '9' ],
189
+ 'Express': ['5', '4', null],
190
+ 'Fastify': ['5', '4', null],
191
+ 'FastAPI': ['0.115', '0.111', '0.104'],
192
+ 'Django': ['5.1', '4.2', '3.2'],
193
+ };
194
+
195
+ const fetchLatestVersions = async (frameworkValue) => {
196
+ const entry = FRAMEWORK_REGISTRY[frameworkValue];
197
+ if (!entry || !entry.package) return null;
198
+
199
+ try {
200
+ const https = require('https');
201
+ const fetch = (url) => new Promise((resolve, reject) => {
202
+ const req = https.get(url, { timeout: 3000 }, (res) => {
203
+ let data = '';
204
+ res.on('data', chunk => data += chunk);
205
+ res.on('end', () => resolve(data));
206
+ });
207
+ req.on('error', reject);
208
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
209
+ });
210
+
211
+ if (entry.registry === 'npm') {
212
+ const raw = await fetch(`https://registry.npmjs.org/${entry.package}`);
213
+ const json = JSON.parse(raw);
214
+ const versions = Object.keys(json.versions || {})
215
+ .filter(v => /^\d+\.\d+\.\d+$/.test(v) && !v.includes('-'))
216
+ .map(v => parseInt(v.split('.')[0]))
217
+ .filter((v, i, arr) => arr.indexOf(v) === i)
218
+ .sort((a, b) => b - a)
219
+ .slice(0, 3);
220
+ return versions.length ? versions.map(String) : null;
221
+ }
222
+
223
+ if (entry.registry === 'pypi') {
224
+ const raw = await fetch(`https://pypi.org/pypi/${entry.package}/json`);
225
+ const json = JSON.parse(raw);
226
+ const versions = Object.keys(json.releases || {})
227
+ .filter(v => /^\d+\.\d+(\.\d+)?$/.test(v))
228
+ .sort((a, b) => {
229
+ const [aMaj, aMin = 0] = a.split('.').map(Number);
230
+ const [bMaj, bMin = 0] = b.split('.').map(Number);
231
+ return bMaj !== aMaj ? bMaj - aMaj : bMin - aMin;
232
+ })
233
+ .map(v => v.split('.').slice(0, 2).join('.'))
234
+ .filter((v, i, arr) => arr.indexOf(v) === i)
235
+ .slice(0, 3);
236
+ return versions.length ? versions : null;
237
+ }
238
+ } catch {
239
+ return null;
240
+ }
241
+ return null;
242
+ };
243
+
107
244
  const STATE_OPTIONS = {
108
245
  'Next.js': ['Zustand', 'Redux Toolkit', 'Jotai', 'TanStack Query'],
109
246
  'Vite+React': ['Zustand', 'Redux Toolkit', 'Jotai', 'TanStack Query'],
@@ -135,6 +272,7 @@ const ORM_OPTIONS = {
135
272
  'Express': ['Prisma', 'TypeORM', 'Drizzle', 'Sequelize'],
136
273
  'Fastify': ['Prisma', 'TypeORM', 'Drizzle'],
137
274
  'Django': ['Django ORM (built-in)', 'SQLAlchemy'],
275
+ 'FastAPI': ['SQLAlchemy', 'Tortoise ORM', 'Beanie (MongoDB)'],
138
276
  'Laravel': ['Eloquent (built-in)'],
139
277
  'Rails': ['Active Record (built-in)'],
140
278
  };
@@ -144,11 +282,264 @@ const AUTH_OPTIONS = {
144
282
  'Express': ['Passport.js', 'JWT-only', 'OAuth2'],
145
283
  'Fastify': ['fastify-jwt', 'Passport.js', 'OAuth2'],
146
284
  'Django': ['Django Auth (built-in)', 'DRF TokenAuth', 'OAuth2'],
285
+ 'FastAPI': ['JWT-only', 'OAuth2', 'FastAPI-Users'],
147
286
  'Laravel': ['Laravel Sanctum', 'Laravel Passport', 'JWT'],
148
287
  'Rails': ['Devise', 'JWT', 'OAuth2'],
149
288
  };
150
289
 
151
- // ── Readline ──────────────────────────────────────────────────────────────────
290
+ const IDE_CANDIDATES = [
291
+ {
292
+ cmd: 'code',
293
+ name: 'VS Code',
294
+ mac: { app: 'Visual Studio Code', args: ['--new-window'] },
295
+ win: { paths: ['{LOCALAPPDATA}\\Programs\\Microsoft VS Code\\Code.exe', '{ProgramFiles}\\Microsoft VS Code\\Code.exe'], args: ['--new-window'] },
296
+ linux: { paths: ['/snap/bin/code', '/usr/bin/code', '/usr/local/bin/code'], args: ['--new-window'] },
297
+ },
298
+ {
299
+ cmd: 'cursor',
300
+ name: 'Cursor',
301
+ mac: { app: 'Cursor', args: ['--new-window'] },
302
+ win: { paths: ['{LOCALAPPDATA}\\Programs\\cursor\\Cursor.exe'], args: ['--new-window'] },
303
+ linux: { paths: ['/usr/bin/cursor', '/opt/cursor/cursor'], args: ['--new-window'] },
304
+ },
305
+ {
306
+ cmd: 'webstorm',
307
+ name: 'WebStorm',
308
+ mac: { app: 'WebStorm', toolboxApp: 'WebStorm', args: [] },
309
+ win: { paths: [
310
+ '{LOCALAPPDATA}\\JetBrains\\Toolbox\\scripts\\webstorm.cmd',
311
+ '{LOCALAPPDATA}\\Programs\\WebStorm\\bin\\webstorm64.exe',
312
+ ], args: [] },
313
+ linux: { paths: [
314
+ `${os.homedir()}/.local/bin/webstorm`,
315
+ '/opt/webstorm/bin/webstorm.sh',
316
+ '/snap/webstorm/current/bin/webstorm.sh',
317
+ ], args: [] },
318
+ },
319
+ {
320
+ cmd: 'idea',
321
+ name: 'IntelliJ IDEA',
322
+ mac: { app: 'IntelliJ IDEA', toolboxApp: 'IntelliJ IDEA', args: [] },
323
+ win: { paths: [
324
+ '{LOCALAPPDATA}\\JetBrains\\Toolbox\\scripts\\idea.cmd',
325
+ '{LOCALAPPDATA}\\Programs\\IntelliJ IDEA Community Edition\\bin\\idea64.exe',
326
+ '{ProgramFiles}\\JetBrains\\IntelliJ IDEA\\bin\\idea64.exe',
327
+ ], args: [] },
328
+ linux: { paths: [
329
+ `${os.homedir()}/.local/bin/idea`,
330
+ '/opt/idea/bin/idea.sh',
331
+ '/snap/intellij-idea-community/current/bin/idea.sh',
332
+ ], args: [] },
333
+ },
334
+ {
335
+ cmd: 'zed',
336
+ name: 'Zed',
337
+ mac: { app: 'Zed', args: [] },
338
+ win: { paths: [], args: [] },
339
+ linux: { paths: ['/usr/bin/zed', `${os.homedir()}/.local/bin/zed`], args: [] },
340
+ },
341
+ {
342
+ cmd: null,
343
+ name: 'Other / Manual',
344
+ note: 'prints worktree path, open it yourself',
345
+ mac: null,
346
+ win: null,
347
+ linux:null,
348
+ },
349
+ ];
350
+
351
+ // Expands {LOCALAPPDATA} / {ProgramFiles} placeholders for Windows paths
352
+ const expandWinPath = (p) =>
353
+ p.replace('{LOCALAPPDATA}', process.env.LOCALAPPDATA || '')
354
+ .replace('{ProgramFiles}', process.env.ProgramFiles || 'C:\\Program Files');
355
+
356
+ const buildIDEOptions = () => {
357
+ const platform = process.platform;
358
+
359
+ return IDE_CANDIDATES.map(ide => {
360
+ if (!ide.cmd) {
361
+ const noteStr = ide.note ? dim(` (${ide.note})`) : '';
362
+ return { ...ide, detected: false, strategy: 'manual', label: `${ide.name} ${dim('→')}${noteStr}` };
363
+ }
364
+
365
+ let detected = false;
366
+ let strategy = 'cli';
367
+
368
+ if (platform === 'darwin' && ide.mac) {
369
+ // Mac — check .app bundle in /Applications, ~/Applications, and JetBrains Toolbox
370
+ const system = `/Applications/${ide.mac.app}.app`;
371
+ const user = path.join(os.homedir(), 'Applications', `${ide.mac.app}.app`);
372
+ const toolbox = path.join(os.homedir(), 'Applications', 'JetBrains Toolbox', `${ide.mac.app}.app`);
373
+ detected = fs.existsSync(system) || fs.existsSync(user) || fs.existsSync(toolbox);
374
+ if (detected) strategy = 'mac-app';
375
+
376
+ } else if (platform === 'win32' && ide.win) {
377
+ // Windows — CLI first, then known exe paths
378
+ try {
379
+ execSync(`where ${ide.cmd}`, { stdio: 'pipe' });
380
+ detected = true;
381
+ strategy = 'cli';
382
+ } catch {
383
+ const expanded = (ide.win.paths || []).map(expandWinPath);
384
+ detected = expanded.some(p => fs.existsSync(p));
385
+ if (detected) strategy = 'win-exe';
386
+ }
387
+
388
+ } else if (platform === 'linux' && ide.linux) {
389
+ // Linux — CLI first, then known install paths
390
+ try {
391
+ execSync(`which ${ide.cmd}`, { stdio: 'pipe' });
392
+ detected = true;
393
+ strategy = 'cli';
394
+ } catch {
395
+ detected = (ide.linux.paths || []).some(p => fs.existsSync(p));
396
+ if (detected) strategy = 'linux-path';
397
+ }
398
+ }
399
+
400
+ const statusStr = detected ? green('✓ detected') : dim('✗ not found');
401
+ const noteStr = ide.note ? dim(` (${ide.note})`) : '';
402
+ return {
403
+ ...ide,
404
+ detected,
405
+ strategy,
406
+ label: `${ide.name} ${statusStr}${noteStr}`,
407
+ };
408
+ });
409
+ };
410
+
411
+ const verifyIDE = (ide) => {
412
+ const platform = process.platform;
413
+
414
+ if (ide.strategy === 'mac-app' && ide.mac) {
415
+ // Mac — confirm .app exists and try to read version from plist
416
+ const appPath = `/Applications/${ide.mac.app}.app`;
417
+ if (!fs.existsSync(appPath) && !fs.existsSync(path.join(os.homedir(), 'Applications', `${ide.mac.app}.app`))) {
418
+ return { ok: false };
419
+ }
420
+ try {
421
+ const version = execSync(
422
+ `defaults read "/Applications/${ide.mac.app}.app/Contents/Info.plist" CFBundleShortVersionString`,
423
+ { stdio: 'pipe', encoding: 'utf8' }
424
+ ).trim();
425
+ return { ok: true, version };
426
+ } catch {
427
+ return { ok: true, version: null };
428
+ }
429
+ }
430
+
431
+ // Windows exe / Linux path / CLI — try --version
432
+ try {
433
+ const cmd = ide.strategy === 'win-exe'
434
+ ? `"${(ide.win?.paths || []).map(expandWinPath).find(p => fs.existsSync(p))}"`
435
+ : ide.strategy === 'linux-path'
436
+ ? `"${(ide.linux?.paths || []).find(p => fs.existsSync(p))}"`
437
+ : `"${ide.cmd}"`;
438
+ const result = execSync(`${cmd} --version`, { stdio: 'pipe', encoding: 'utf8' });
439
+ const version = result.split('\n')[0].trim();
440
+ return { ok: true, version };
441
+ } catch {
442
+ return { ok: false };
443
+ }
444
+ };
445
+
446
+ // ── Tracking structure ────────────────────────────────────────────────────────
447
+
448
+ const emptySlot = () => ({
449
+ branch: null,
450
+ timestamp: null,
451
+ launchedAt: null,
452
+ status: null,
453
+ missingCount: 0,
454
+ worktreePath: null,
455
+ });
456
+
457
+ const generateTrackingStructure = (config) => {
458
+ const bt = config.backend?.type;
459
+
460
+ const structure = {
461
+ client: {
462
+ UI: emptySlot(),
463
+ LOGIC: emptySlot(),
464
+ FORMS: emptySlot(),
465
+ ROUTING: emptySlot(),
466
+ TESTING: emptySlot(),
467
+ ACCESSIBILITY: emptySlot(),
468
+ },
469
+ shared: {
470
+ SECURITY: emptySlot(),
471
+ },
472
+ };
473
+
474
+ if (bt === 'separate') {
475
+ structure.backend = {
476
+ API: emptySlot(),
477
+ LOGIC: emptySlot(),
478
+ AUTH: emptySlot(),
479
+ DB: emptySlot(),
480
+ EVENTS: emptySlot(),
481
+ JOBS: emptySlot(),
482
+ TESTING: emptySlot(),
483
+ };
484
+ }
485
+
486
+ return structure;
487
+ };
488
+
489
+ // ── GitHub remote setup ───────────────────────────────────────────────────────
490
+
491
+ const detectGitHubUser = () => {
492
+ try {
493
+ return execSync('gh api user --jq .login',
494
+ { encoding: 'utf8', stdio: 'pipe' }).trim();
495
+ } catch {}
496
+ try {
497
+ return execSync('git config user.name',
498
+ { encoding: 'utf8', stdio: 'pipe' }).trim();
499
+ } catch {}
500
+ return null;
501
+ };
502
+
503
+ const setupUserRemote = (ROOT, projectName) => {
504
+ let currentOrigin = null;
505
+ try {
506
+ currentOrigin = execSync('git remote get-url origin',
507
+ { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }).trim();
508
+ } catch {}
509
+
510
+ // Already has their own remote — nothing to do
511
+ if (currentOrigin && !currentOrigin.includes('multi-agents-template')) return;
512
+
513
+ // Demote template origin to upstream
514
+ if (currentOrigin?.includes('multi-agents-template')) {
515
+ try {
516
+ execSync('git remote remove origin', { cwd: ROOT, stdio: 'pipe' });
517
+ execSync(`git remote add upstream ${currentOrigin}`, { cwd: ROOT, stdio: 'pipe' });
518
+ console.log(dim(' ℹ Template remote moved to upstream'));
519
+ } catch {}
520
+ }
521
+
522
+ // Write flag — agent will handle remote setup on first session
523
+ const flagPath = path.join(ROOT, '.scaffold', '.remote-setup-needed');
524
+ fs.writeFileSync(flagPath, JSON.stringify({
525
+ projectName,
526
+ createdAt: new Date().toISOString(),
527
+ }), 'utf8');
528
+
529
+ console.log(`\n ${yellow('ℹ No remote configured.')} Your first agent session will set this up.`);
530
+ console.log(dim(' All work stays local until then.\n'));
531
+ };
532
+
533
+ const renderTrajectoryLines = (lines) => {
534
+ const HEADERS = ['Benefits', 'Best for', 'Use agents for', 'Handle manually'];
535
+ lines.forEach(l => {
536
+ if (!l) { console.log(''); return; }
537
+ if (l.startsWith('⚠')) console.log(` ${yellow(l)}`);
538
+ else if (HEADERS.includes(l)) console.log(`\n ${bold(l)}`);
539
+ else if (l.startsWith('·')) console.log(` ${l}`);
540
+ else console.log(` ${dim(l)}`);
541
+ });
542
+ };
152
543
 
153
544
  const rl = readline.createInterface({
154
545
  input: process.stdin,
@@ -168,30 +559,25 @@ const showList = (items, showSkip = false) => {
168
559
  if (showSkip) console.log(` ${dim('0.')} Skip ${dim('(agent will propose when needed)')}`);
169
560
  };
170
561
 
562
+ // Sentinel value returned when user picks ← Restart
563
+ const BACK = Symbol('BACK');
564
+
171
565
  const selectRequired = async (prompt, items) => {
172
- while (true) {
173
- console.log(`\n${bold(prompt)}`);
174
- showList(items);
175
- const input = await ask(`\n ${bold('Select')} ${dim(`(1-${items.length})`)}: `);
176
- const index = parseInt(input) - 1;
177
- if (!isNaN(index) && index >= 0 && index < items.length) return items[index];
178
- console.log(yellow(` Please enter a number between 1 and ${items.length}.`));
179
- }
566
+ const idx = await arrowSelect(prompt, items.map(i => ({ label: typeof i === 'string' ? i : i.label })), rl, true);
567
+ if (idx === items.length) return BACK;
568
+ return items[idx];
180
569
  };
181
570
 
182
571
  const selectOptional = async (prompt, items) => {
183
572
  if (!items || items.length === 0) return null;
184
- while (true) {
185
- console.log(`\n${bold(prompt)}`);
186
- showList(items, true);
187
- const input = await ask(`\n ${bold('Select')} ${dim(`(0-${items.length})`)}: `);
188
- if (input === '0' || input === '') return null;
189
- const index = parseInt(input) - 1;
190
- if (!isNaN(index) && index >= 0 && index < items.length) {
191
- return typeof items[index] === 'string' ? items[index] : items[index].value;
192
- }
193
- console.log(yellow(` Invalid selection. Please enter a number between 0 and ${items.length}.`));
194
- }
573
+ const choices = [
574
+ ...items.map(i => ({ label: typeof i === 'string' ? i : i.label })),
575
+ { label: dim('Skip (agent will propose when needed)') },
576
+ ];
577
+ const idx = await arrowSelect(prompt, choices, rl, true);
578
+ if (idx === choices.length) return BACK;
579
+ if (idx === items.length) return null;
580
+ return typeof items[idx] === 'string' ? items[idx] : items[idx].value;
195
581
  };
196
582
 
197
583
  const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
@@ -251,13 +637,53 @@ const copyDir = (src, dest) => {
251
637
  // ── Main ──────────────────────────────────────────────────────────────────────
252
638
 
253
639
  const main = async () => {
640
+
641
+ // ── Lock check ───────────────────────────────────────────────────────────────
642
+
643
+ if (fs.existsSync(LOCK_FILE)) {
644
+ const ts = fs.readFileSync(LOCK_FILE, 'utf8').trim();
645
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
646
+ const ask2 = (q) => new Promise((resolve) => rl2.question(q, (a) => resolve(a.trim())));
647
+
648
+ console.log(`\n${yellow(' This project has already been initialized.')}`);
649
+ console.log(dim(` Initialized on: ${ts}\n`));
650
+ console.log(` ${dim('1.')} Continue — run ${cyan('npm run launch')}`);
651
+ console.log(` ${dim('2.')} Reset — delete config and re-run initialization`);
652
+ console.log(` ${dim('3.')} Exit\n`);
653
+
654
+ const choice = await ask2(` ${bold('Select')} ${dim('(1-3)')}: `);
655
+
656
+ if (choice === '1') {
657
+ console.log('');
658
+ rl2.close();
659
+ const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
660
+ stdio: 'inherit',
661
+ cwd: ROOT,
662
+ });
663
+ child.on('exit', (code) => process.exit(code));
664
+ return;
665
+ } else if (choice === '2') {
666
+ console.log(yellow('\n Resetting configuration...\n'));
667
+ fs.unlinkSync(LOCK_FILE);
668
+ const configPath = path.join(RUNTIME_DIR, '.config.json');
669
+ if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
670
+ rl2.close();
671
+ console.log(green(' Reset complete. Re-running initialization...\n'));
672
+ // Fall through to run init again
673
+ } else {
674
+ console.log(dim('\n Exited.\n'));
675
+ rl2.close();
676
+ return;
677
+ }
678
+ }
679
+
254
680
  console.log('\n');
255
681
  console.log(bold(cyan(' Multi-Agent Monorepo Template')));
256
682
  console.log(dim(' Project Initializer\n'));
257
683
  separator();
258
684
 
259
685
  console.log(`\n${bold('Let\'s configure your project.')}`);
260
- console.log(dim(' Required fields must be selected. Optional fields can be skipped (press 0 or Enter).\n'));
686
+ console.log(dim(' Use arrow keys to select. Optional fields can be skipped.\n'));
261
687
  console.log(dim(' Skipped fields will be resolved by the agent when first needed.\n'));
262
688
 
263
689
  // ── Project name ────────────────────────────────────────────────────────────
@@ -268,6 +694,14 @@ const main = async () => {
268
694
  if (!projectName) console.log(yellow(' Project name is required. Please enter a name.'));
269
695
  }
270
696
 
697
+ const restartIfBack = (val) => {
698
+ if (val !== BACK) return false;
699
+ rl.close();
700
+ const { spawn } = require('child_process');
701
+ spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c));
702
+ return true;
703
+ };
704
+
271
705
  separator();
272
706
 
273
707
  // ── Client ──────────────────────────────────────────────────────────────────
@@ -275,10 +709,29 @@ const main = async () => {
275
709
  console.log(`\n${bold(blue('Client configuration'))}`);
276
710
 
277
711
  const clientFw = await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS);
712
+ if (restartIfBack(clientFw)) return;
713
+
714
+ // ── Client framework version ─────────────────────────────────────────────────
715
+ let clientFwVersion = null;
716
+ const clientVersions = await fetchLatestVersions(clientFw.value) || FRAMEWORK_VERSION_FALLBACK[clientFw.value] || [];
717
+ if (clientVersions.length) {
718
+ console.log(dim(' Fetching latest versions...'));
719
+ const versionChoices = clientVersions.map((v, i) => ({
720
+ label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
721
+ value: v,
722
+ }));
723
+ const vIdx = await arrowSelect(`* ${clientFw.value} version:`, versionChoices, rl, true);
724
+ if (vIdx === versionChoices.length) { restartIfBack(BACK); return; }
725
+ clientFwVersion = clientVersions[vIdx];
726
+ }
727
+
278
728
  const clientLang = clientFw.language;
279
729
  const clientState = await selectOptional('State management:', STATE_OPTIONS[clientFw.value] || []);
730
+ if (restartIfBack(clientState)) return;
280
731
  const clientUi = await selectOptional('UI library:', UI_OPTIONS[clientFw.value] || []);
732
+ if (restartIfBack(clientUi)) return;
281
733
  const clientStyle = await selectOptional('Styling:', STYLING_OPTIONS);
734
+ if (restartIfBack(clientStyle)) return;
282
735
 
283
736
  separator();
284
737
 
@@ -293,11 +746,11 @@ const main = async () => {
293
746
  let backendOrm = null;
294
747
  let backendAuth = null;
295
748
  let backendType = null;
749
+ let backendFwObj = null;
296
750
 
297
751
  if (clientFw.integratedBackend) {
298
752
  console.log(dim(` ${clientFw.value} supports server-side rendering and API routes.\n`));
299
- const integratedAnswer = await ask(` ${bold('Use integrated backend')} ${dim(`(${clientFw.value} API routes/SSR)`)} ${dim('instead of a separate backend? (y/n)')}: `);
300
- useIntegratedBackend = integratedAnswer.toLowerCase() === 'y';
753
+ useIntegratedBackend = await arrowConfirm(`Use integrated backend (${clientFw.value} API routes/SSR) instead of a separate backend?`, rl);
301
754
 
302
755
  if (useIntegratedBackend) {
303
756
  backendType = 'integrated';
@@ -308,25 +761,102 @@ const main = async () => {
308
761
  if (!useIntegratedBackend) {
309
762
  console.log(dim(' You can skip the backend framework and decide later.\n'));
310
763
 
311
- const backendFwObj = await (async () => {
312
- console.log(`\n${bold('Backend framework:')}`);
313
- showList(BACKEND_FRAMEWORKS, true);
314
- const input = await ask(`\n ${bold('Select')} ${dim(`(0-${BACKEND_FRAMEWORKS.length})`)}: `);
315
- if (input === '0' || input === '') return null;
316
- const index = parseInt(input) - 1;
317
- if (isNaN(index) || index < 0 || index >= BACKEND_FRAMEWORKS.length) return null;
318
- return BACKEND_FRAMEWORKS[index];
319
- })();
764
+ const backendChoices = [
765
+ ...BACKEND_FRAMEWORKS.map(f => ({ label: f.label || f.value })),
766
+ { label: dim('Skip (decide later)') },
767
+ ];
768
+ const backendIdx = await arrowSelect('Backend framework:', backendChoices, rl);
769
+ backendFwObj = backendIdx === BACKEND_FRAMEWORKS.length ? null : BACKEND_FRAMEWORKS[backendIdx];
320
770
 
321
771
  backendFw = backendFwObj ? backendFwObj.value : null;
322
772
  backendLang = backendFwObj ? backendFwObj.language : null;
773
+
774
+ // ── Backend framework version ──────────────────────────────────────────────
775
+ if (backendFw) {
776
+ const backendVersions = await fetchLatestVersions(backendFw) || FRAMEWORK_VERSION_FALLBACK[backendFw] || [];
777
+ if (backendVersions.length) {
778
+ const vChoices = backendVersions.map((v, i) => ({
779
+ label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
780
+ value: v,
781
+ }));
782
+ const vIdx = await arrowSelect(`* ${backendFw} version:`, vChoices, rl, true);
783
+ if (vIdx === vChoices.length) { restartIfBack(BACK); return; }
784
+ backendFwObj = { ...backendFwObj, version: backendVersions[vIdx] };
785
+ }
786
+ }
787
+
323
788
  backendOrm = backendFw ? await selectOptional('ORM / database layer:', ORM_OPTIONS[backendFw] || []) : null;
789
+ if (restartIfBack(backendOrm)) return;
324
790
  backendAuth = backendFw ? await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFw] || []) : null;
791
+ if (restartIfBack(backendAuth)) return;
325
792
  backendType = backendFw ? 'separate' : null;
326
793
  }
327
794
 
328
795
  separator();
329
796
 
797
+ // ── Environment ─────────────────────────────────────────────────────────────
798
+
799
+ console.log(`\n${bold(blue('Environment'))}`);
800
+
801
+ const osName = { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }[process.platform] || process.platform;
802
+ console.log(`\n ${dim('OS detected:')} ${bold(osName)}`);
803
+ console.log(dim(' Scanning for installed IDEs...\n'));
804
+
805
+ const ideOptions = buildIDEOptions();
806
+
807
+ const detectedIDEs = ideOptions.filter(o => o.detected);
808
+ const undetectedIDEs = ideOptions.filter(o => !o.detected && o.cmd);
809
+ const manualOption = ideOptions.filter(o => !o.cmd);
810
+
811
+ // Detected first → undetected → manual
812
+ const sortedIdeOptions = [...detectedIDEs, ...undetectedIDEs, ...manualOption];
813
+
814
+ if (detectedIDEs.length > 1) {
815
+ console.log(`\n ${yellow('Multiple IDEs found on this machine')} — select your preference:\n`);
816
+ } else if (detectedIDEs.length === 1) {
817
+ console.log(`\n ${green(`1 IDE found:`)} ${bold(detectedIDEs[0].name)}\n`);
818
+ } else {
819
+ console.log(`\n ${yellow('No IDEs detected on this machine.')}\n`);
820
+ }
821
+
822
+ let ideChoice;
823
+ while (true) {
824
+ ideChoice = await selectRequired('* IDE / editor (required):', sortedIdeOptions);
825
+ if (restartIfBack(ideChoice)) return;
826
+
827
+ // ── Confirmation ──────────────────────────────────────────────────────────
828
+ if (ideChoice.cmd && !ideChoice.detected) {
829
+ console.log(`\n ${yellow('⚠')} ${bold(ideChoice.name)} was not detected on this machine.`);
830
+ console.log(dim(' It may not open automatically when launching a task.\n'));
831
+ if (!await arrowConfirm('Continue with this IDE anyway?', rl)) {
832
+ console.log(dim(' Re-selecting...\n'));
833
+ continue;
834
+ }
835
+ }
836
+
837
+ // ── Double-check ──────────────────────────────────────────────────────────
838
+ if (!ideChoice.cmd) {
839
+ // Manual — no verification needed
840
+ console.log(dim(' Manual mode — worktree path will be printed at launch.'));
841
+ break;
842
+ }
843
+
844
+ console.log(dim(`\n Verifying ${ideChoice.name}...`));
845
+ const verified = verifyIDE(ideChoice);
846
+
847
+ if (verified.ok) {
848
+ const versionStr = verified.version ? dim(` (${verified.version})`) : '';
849
+ console.log(` ${green('✓')} ${ideChoice.name} confirmed${versionStr}`);
850
+ break;
851
+ }
852
+
853
+ console.log(` ${yellow('!')} Could not verify ${ideChoice.name}. The CLI may not be installed or accessible.`);
854
+ if (await arrowConfirm('Continue with this IDE anyway?', rl)) break;
855
+ console.log(dim(' Re-selecting...\n'));
856
+ }
857
+
858
+ separator();
859
+
330
860
  // ── Summary ─────────────────────────────────────────────────────────────────
331
861
 
332
862
  console.log(`\n${bold('Review your configuration:')}\n`);
@@ -342,12 +872,17 @@ const main = async () => {
342
872
  summaryLine('ORM', backendOrm);
343
873
  summaryLine('Auth', backendAuth);
344
874
  }
875
+ summaryLine('IDE / Editor', ideChoice.name);
345
876
 
346
877
  console.log('');
347
878
  console.log(dim(' y = confirm | n = abort | e = edit (start over)\n'));
348
- const confirm = await ask(`${bold('Confirm and write to config files?')} ${dim('(y/n/e)')}: `);
879
+ const confirmIdx = await arrowSelect('Confirm and write to config files?', [
880
+ { label: `${green('✓')} Confirm — write config and set up project` },
881
+ { label: `${yellow('↺')} Restart — redo configuration` },
882
+ { label: `${red('✗')} Abort` },
883
+ ], rl);
349
884
 
350
- if (confirm.toLowerCase() === 'e') {
885
+ if (confirmIdx === 1) {
351
886
  console.log(yellow('\n Restarting configuration...\n'));
352
887
  rl.close();
353
888
  const { spawn } = require('child_process');
@@ -356,7 +891,7 @@ const main = async () => {
356
891
  return;
357
892
  }
358
893
 
359
- if (confirm.toLowerCase() !== 'y') {
894
+ if (confirmIdx === 2) {
360
895
  console.log(yellow('\n Aborted. No files were changed.\n'));
361
896
  rl.close();
362
897
  return;
@@ -384,11 +919,38 @@ const main = async () => {
384
919
 
385
920
  const TEMPLATES = path.join(CORE_DIR, 'templates');
386
921
 
387
- copyDir(path.join(TEMPLATES, 'client'), path.join(ROOT, 'client'));
388
- copyDir(path.join(TEMPLATES, 'shared'), path.join(ROOT, 'shared'));
922
+ // ── Copy scope directories (app code + CLAUDE.md only) ─────────────────────
923
+ // agents/ and frameworks/ are now at repo root as .agents/ and .frameworks/
924
+ // We copy client/backend/shared but exclude agents/ and frameworks/ subdirs
925
+
926
+ const copyDirExcluding = (src, dest, exclude = []) => {
927
+ fs.mkdirSync(dest, { recursive: true });
928
+ for (const entry of fs.readdirSync(src)) {
929
+ if (exclude.includes(entry)) continue;
930
+ const srcFile = path.join(src, entry);
931
+ const destFile = path.join(dest, entry);
932
+ if (fs.statSync(srcFile).isDirectory()) copyDirExcluding(srcFile, destFile, []);
933
+ else fs.copyFileSync(srcFile, destFile);
934
+ }
935
+ };
936
+
937
+ copyDirExcluding(path.join(TEMPLATES, 'client'), path.join(ROOT, 'client'), ['agents', 'frameworks']);
938
+ copyDirExcluding(path.join(TEMPLATES, 'shared'), path.join(ROOT, 'shared'), ['agents', 'frameworks']);
939
+ if (backendType === 'separate') {
940
+ copyDirExcluding(path.join(TEMPLATES, 'backend'), path.join(ROOT, 'backend'), ['agents', 'frameworks']);
941
+ fs.writeFileSync(path.join(ROOT, 'backend', '.gitkeep'), '', 'utf8');
942
+ }
943
+
944
+ // ── Copy agents and frameworks to repo root as .agents/ and .frameworks/ ────
945
+
946
+ copyDir(path.join(TEMPLATES, 'client', 'agents'), path.join(ROOT, '.agents', 'client'));
947
+ copyDir(path.join(TEMPLATES, 'client', 'frameworks'), path.join(ROOT, '.frameworks', 'client'));
948
+ copyDir(path.join(TEMPLATES, 'shared', 'agents'), path.join(ROOT, '.agents', 'shared'));
389
949
  if (backendType === 'separate') {
390
- copyDir(path.join(TEMPLATES, 'backend'), path.join(ROOT, 'backend'));
950
+ copyDir(path.join(TEMPLATES, 'backend', 'agents'), path.join(ROOT, '.agents', 'backend'));
951
+ copyDir(path.join(TEMPLATES, 'backend', 'frameworks'), path.join(ROOT, '.frameworks', 'backend'));
391
952
  }
953
+
392
954
  fs.copyFileSync(path.join(TEMPLATES, 'CLAUDE.md'), path.join(ROOT, 'CLAUDE.md'));
393
955
  fs.copyFileSync(path.join(TEMPLATES, 'CONTRACTS.md'), path.join(ROOT, 'CONTRACTS.md'));
394
956
  console.log(` ${green('✓')} Templates copied`);
@@ -413,22 +975,24 @@ const main = async () => {
413
975
  console.log(` ${green('✓')} CLAUDE.md configured`);
414
976
 
415
977
  writeConfig(path.join(ROOT, 'client', 'CLAUDE.md'), {
416
- PROJECT_NAME: projectName,
417
- FRAMEWORK: clientFw.value,
418
- LANGUAGE: clientLang,
419
- STATE: clientState,
420
- UI_LIBRARY: clientUi,
421
- STYLING: clientStyle,
978
+ PROJECT_NAME: projectName,
979
+ FRAMEWORK: clientFw.value,
980
+ FRAMEWORK_VERSION: clientFwVersion || '',
981
+ LANGUAGE: clientLang,
982
+ STATE: clientState,
983
+ UI_LIBRARY: clientUi,
984
+ STYLING: clientStyle,
422
985
  });
423
986
  console.log(` ${green('✓')} client/CLAUDE.md configured`);
424
987
 
425
988
  if (backendType === 'separate') {
426
989
  writeConfig(path.join(ROOT, 'backend', 'CLAUDE.md'), {
427
- PROJECT_NAME: projectName,
428
- FRAMEWORK: backendFw,
429
- LANGUAGE: backendLang,
430
- ORM: backendOrm,
431
- AUTH: backendAuth,
990
+ PROJECT_NAME: projectName,
991
+ FRAMEWORK: backendFw,
992
+ FRAMEWORK_VERSION: backendFwObj?.version || '',
993
+ LANGUAGE: backendLang,
994
+ ORM: backendOrm,
995
+ AUTH: backendAuth,
432
996
  });
433
997
  console.log(` ${green('✓')} backend/CLAUDE.md configured`);
434
998
  }
@@ -437,12 +1001,33 @@ const main = async () => {
437
1001
  ensureGitignore('.agents-core/');
438
1002
  ensureGitignore('.scaffold/');
439
1003
  ensureGitignore('.workflow/');
1004
+
1005
+ // Remove template-specific gitignore entries so generated files can be committed
1006
+ const gitignorePath = path.join(ROOT, '.gitignore');
1007
+ let gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
1008
+ ['client/', 'backend/', 'shared/', 'CLAUDE.md', 'CONTRACTS.md', 'BUILD_STATE.md'].forEach(entry => {
1009
+ gitignoreContent = gitignoreContent.replace(`\n${entry}`, '');
1010
+ gitignoreContent = gitignoreContent.replace(`${entry}\n`, '');
1011
+ gitignoreContent = gitignoreContent.replace(entry, '');
1012
+ });
1013
+ fs.writeFileSync(gitignorePath, gitignoreContent.trim() + '\n', 'utf8');
440
1014
  console.log(` ${green('✓')} .gitignore updated`);
441
1015
 
442
1016
  // ── Write .config.json ───────────────────────────────────────────────────────
443
1017
 
444
1018
  const config = {
445
1019
  projectName,
1020
+ ide: {
1021
+ name: ideChoice.name,
1022
+ strategy: ideChoice.strategy,
1023
+ cmd: ideChoice.cmd || null,
1024
+ app: ideChoice.mac?.app || null,
1025
+ openArgs: process.platform === 'darwin' ? (ideChoice.mac?.args || [])
1026
+ : process.platform === 'win32' ? (ideChoice.win?.args || [])
1027
+ : (ideChoice.linux?.args || []),
1028
+ winPaths: (ideChoice.win?.paths || []).map(expandWinPath),
1029
+ linuxPaths: ideChoice.linux?.paths || [],
1030
+ },
446
1031
  client: {
447
1032
  framework: clientFw.value,
448
1033
  language: clientLang,
@@ -542,6 +1127,67 @@ If a dependency is not met:
542
1127
  fs.writeFileSync(path.join(ROOT, 'BUILD_STATE.md'), buildState, 'utf8');
543
1128
  console.log(` ${green('✓')} BUILD_STATE.md generated`);
544
1129
 
1130
+ // ── Generate user project package.json ───────────────────────────────────────
1131
+
1132
+ const userPackage = {
1133
+ name: projectName.toLowerCase().replace(/\s+/g, '-'),
1134
+ version: '1.0.0',
1135
+ private: true,
1136
+ dependencies: {
1137
+ prompts: '^2.4.2',
1138
+ },
1139
+ scripts: {
1140
+ launch: 'cd "$(git rev-parse --git-common-dir)/.." && node .workflow/launch.js',
1141
+ complete: 'cd "$(git rev-parse --git-common-dir)/.." && node .workflow/complete.js',
1142
+ },
1143
+ };
1144
+ fs.writeFileSync(path.join(ROOT, 'package.json'), JSON.stringify(userPackage, null, 2), 'utf8');
1145
+ console.log(` ${green('✓')} package.json generated`);
1146
+
1147
+ // ── Install dependencies ──────────────────────────────────────────────────────
1148
+
1149
+ try {
1150
+ console.log(dim(' Installing dependencies...'));
1151
+ execSync('npm install', { cwd: ROOT, stdio: 'pipe' });
1152
+ console.log(` ${green('✓')} Dependencies installed`);
1153
+ } catch {
1154
+ console.log(yellow(' ⚠ npm install failed — run npm install manually before launching'));
1155
+ }
1156
+
1157
+ // ── Tracking ──────────────────────────────────────────────────────────────────
1158
+
1159
+ const trackingPath = path.join(RUNTIME_DIR, '.tracking.json');
1160
+ if (!fs.existsSync(trackingPath)) {
1161
+ const trackingStructure = generateTrackingStructure(config);
1162
+ fs.writeFileSync(trackingPath, JSON.stringify(trackingStructure, null, 2), 'utf8');
1163
+ console.log(` ${green('✓')} .tracking.json generated`);
1164
+ } else {
1165
+ console.log(dim(' ℹ .tracking.json already exists — preserved'));
1166
+ }
1167
+
1168
+ // ── Generate .paths.json ──────────────────────────────────────────────────────
1169
+
1170
+ const pathsMap = {};
1171
+ const clientConventions = FRAMEWORK_CONVENTIONS.client[clientFw?.value] || {};
1172
+ const backendConventions = FRAMEWORK_CONVENTIONS.backend[backendFwObj?.value] || {};
1173
+
1174
+ if (Object.keys(clientConventions).length) {
1175
+ pathsMap.client = {};
1176
+ Object.entries(clientConventions).forEach(([key, value]) => {
1177
+ pathsMap.client[key] = { expected: value, current: null, status: 'pending' };
1178
+ });
1179
+ }
1180
+
1181
+ if (Object.keys(backendConventions).length) {
1182
+ pathsMap.backend = {};
1183
+ Object.entries(backendConventions).forEach(([key, value]) => {
1184
+ pathsMap.backend[key] = { expected: value, current: null, status: 'pending' };
1185
+ });
1186
+ }
1187
+
1188
+ fs.writeFileSync(path.join(RUNTIME_DIR, '.paths.json'), JSON.stringify(pathsMap, null, 2), 'utf8');
1189
+ console.log(` ${green('✓')} .paths.json generated`);
1190
+
545
1191
  // ── Lock ─────────────────────────────────────────────────────────────────────
546
1192
 
547
1193
  fs.writeFileSync(LOCK_FILE, new Date().toISOString());
@@ -558,33 +1204,180 @@ If a dependency is not met:
558
1204
  console.log(dim(' git add . && git commit -m "init: project configuration"'));
559
1205
  }
560
1206
 
561
- // ── Chain to launch.js ────────────────────────────────────────────────────────
1207
+ // ── Pre-commit hook — block direct commits to main ───────────────────────────
1208
+
1209
+ try {
1210
+ const hooksDir = path.join(ROOT, '.git', 'hooks');
1211
+ const hookPath = path.join(hooksDir, 'pre-commit');
1212
+ const hookScript = `#!/bin/sh
1213
+ branch=$(git symbolic-ref --short HEAD 2>/dev/null)
1214
+ if [ "$branch" = "main" ]; then
1215
+ echo ""
1216
+ echo " ⚠ Direct commits to main are not allowed."
1217
+ echo " Use npm run launch to start a task."
1218
+ echo ""
1219
+ exit 1
1220
+ fi
1221
+ `;
1222
+ if (!fs.existsSync(hookPath)) {
1223
+ fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
1224
+ console.log(dim(' ℹ Pre-commit hook installed — direct main commits blocked'));
1225
+ }
1226
+ } catch { /* best-effort */ }
1227
+
1228
+ // ── Remote setup ─────────────────────────────────────────────────────────────
1229
+
1230
+ setupUserRemote(ROOT, projectName);
1231
+
1232
+ // ── Trajectory selection ─────────────────────────────────────────────────────
562
1233
 
563
1234
  separator();
564
1235
  console.log(`\n${bold(green(' Project initialized successfully!'))}\n`);
1236
+ console.log(` ${bold('How do you want to build?')}\n`);
1237
+
1238
+ console.log(` ${dim('1.')} ${bold('Multi-Agent Driven Orchestration')}`);
1239
+ console.log(`${dim(' · Every task should start with npm run launch')}`);
1240
+ console.log(`${dim(' · Each agent runs in its own git worktree — an isolated branch')}`);
1241
+ console.log(`${dim(' and folder that merges back into main via npm run complete')}`);
1242
+ console.log(`${dim(' · Faster builds and lower token spend than a single long session')}`);
1243
+ console.log(`${yellow(' ⚠ If you commit directly to main yourself, you bypass the framework')}`);
1244
+ console.log(`${yellow(' and break task tracking for any active agent branches')}\n`);
1245
+
1246
+ console.log(` ${dim('2.')} ${bold('Shared Orchestration')}`);
1247
+ console.log(`${dim(' · You and agents co-build — each owning a defined part of the codebase')}`);
1248
+ console.log(`${dim(' · Agent tasks run in git worktrees; your work happens directly in the project')}`);
1249
+ console.log(`${dim(' · Agent tasks are token-efficient; your tasks cost only what you prompt')}`);
1250
+ console.log(`${dim(' · Define boundaries before work begins — agents for well-scoped work,')}`);
1251
+ console.log(`${dim(' you for areas where requirements are still evolving')}`);
1252
+ console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
1253
+
1254
+ const TRAJECTORY_DETAILS = {
1255
+ '1': {
1256
+ label: 'Multi-Agent Driven Orchestration',
1257
+ full: [
1258
+ 'Every task must start with npm run launch.',
1259
+ 'Agent sessions load only task-relevant context, enabling reliable',
1260
+ 'chaining, predictable behavior, and efficient token usage.',
1261
+ '',
1262
+ '⚠ If you commit directly to main yourself, you bypass the framework',
1263
+ ' and break task tracking for any active agent branches.',
1264
+ '',
1265
+ 'Benefits',
1266
+ '· Scoped context per task',
1267
+ '· Predictable token consumption',
1268
+ '· Lower cost than maintaining large, persistent sessions',
1269
+ '· Better isolation between parallel work streams',
1270
+ ],
1271
+ next: 'launch',
1272
+ },
1273
+ '2': {
1274
+ label: 'Shared Orchestration',
1275
+ full: [
1276
+ 'You and agents work in the same codebase, each with clearly',
1277
+ 'defined ownership. File boundaries must be established before',
1278
+ 'work begins and remain fixed throughout the task.',
1279
+ 'Agents excel when scope is well-defined;',
1280
+ 'you excel when requirements are evolving.',
1281
+ '',
1282
+ 'Use agents for',
1283
+ '· Multi-file features',
1284
+ '· Structured implementation work',
1285
+ '· Domain-specific tasks',
1286
+ '· Changes expected to exceed ~200 lines',
1287
+ '',
1288
+ 'Handle manually',
1289
+ '· Targeted bug fixes',
1290
+ '· Configuration changes',
1291
+ '· Small refactors',
1292
+ '· Single-file edits under ~50 lines',
1293
+ '',
1294
+ '⚠ Avoid overlapping file ownership. Working on the same files',
1295
+ ' as an active agent will create merge conflicts when merged.',
1296
+ '⚠ If you are spending time repeatedly clarifying scope, stop',
1297
+ ' and do the task yourself. The coordination cost often',
1298
+ ' exceeds the implementation cost.',
1299
+ '',
1300
+ 'Benefits',
1301
+ '· Maximum agent efficiency for well-defined work',
1302
+ '· Human flexibility where requirements change',
1303
+ '· Scales well across large projects',
1304
+ '· Most adaptable workflow — requires the most discipline',
1305
+ ],
1306
+ next: 'launch',
1307
+ },
1308
+ };
565
1309
 
566
- const launchInput = await ask(` ${bold('Ready to launch your first task?')} ${dim('(y/n — default: n)')}: `);
567
- const launch = launchInput.toLowerCase() || 'n';
1310
+ // Wrap in loop to support back navigation
1311
+ let trajectory = null;
1312
+ trajectoryLoop: while (true) {
1313
+ const trajIdx = await arrowSelect('How do you want to build?', [
1314
+ { label: bold('Multi-Agent Driven Orchestration') },
1315
+ { label: bold('Shared Orchestration') },
1316
+ ], rl);
1317
+ trajectory = String(trajIdx + 1);
568
1318
 
569
- if (launch === 'y') {
570
- rl.close();
571
- console.log('');
572
- const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
573
- stdio: 'inherit',
574
- cwd: ROOT,
575
- });
576
- child.on('exit', (code) => process.exit(code));
577
- } else {
578
- console.log('');
579
- console.log(` ${bold('When ready, run:')}`);
580
- console.log(` ${cyan('npm run launch')}\n`);
1319
+ const selected = TRAJECTORY_DETAILS[trajectory];
581
1320
  separator();
1321
+ console.log(`\n ${green('✓')} ${bold(selected.label)}\n`);
1322
+ renderTrajectoryLines(selected.full);
582
1323
  console.log('');
583
- rl.close();
1324
+
1325
+ const confirmIdx = await arrowSelect('Confirm?', [
1326
+ { label: `${green('✓')} Confirm` },
1327
+ { label: `${yellow('←')} Back — pick differently` },
1328
+ ], rl);
1329
+ if (confirmIdx === 0) break trajectoryLoop;
1330
+ trajectory = null;
1331
+ separator();
1332
+ console.log(`\n ${bold('How do you want to build?')}\n`);
1333
+ console.log(` ${dim('1.')} ${bold('Multi-Agent Driven Orchestration')}`);
1334
+ console.log(`${dim(' · Every task should start with npm run launch')}`);
1335
+ console.log(`${dim(' · Each agent runs in its own git worktree — an isolated branch')}`);
1336
+ console.log(`${dim(' and folder that merges back into main via npm run complete')}`);
1337
+ console.log(`${dim(' · Faster builds and lower token spend than a single long session')}`);
1338
+ console.log(`${yellow(' ⚠ If you commit directly to main yourself, you bypass the framework')}`);
1339
+ console.log(`${yellow(' and break task tracking for any active agent branches')}\n`);
1340
+ console.log(` ${dim('2.')} ${bold('Shared Orchestration')}`);
1341
+ console.log(`${dim(' · You and agents co-build — each owning a defined part of the codebase')}`);
1342
+ console.log(`${dim(' · Agent tasks run in git worktrees; your work happens directly in the project')}`);
1343
+ console.log(`${dim(' · Agent tasks are token-efficient; your tasks cost only what you prompt')}`);
1344
+ console.log(`${dim(' · Define boundaries before work begins — agents for well-scoped work,')}`);
1345
+ console.log(`${dim(' you for areas where requirements are still evolving')}`);
1346
+ console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
1347
+ }
1348
+
1349
+ const selected = TRAJECTORY_DETAILS[trajectory];
1350
+
1351
+ // Store trajectory in config
1352
+ try {
1353
+ const cfg = JSON.parse(fs.readFileSync(path.join(RUNTIME_DIR, '.config.json'), 'utf8'));
1354
+ cfg.trajectory = selected.label.toLowerCase().replace(/ /g, '-');
1355
+ fs.writeFileSync(path.join(RUNTIME_DIR, '.config.json'), JSON.stringify(cfg, null, 2), 'utf8');
1356
+ } catch { /* best-effort */ }
1357
+
1358
+ if (selected.next === 'launch') {
1359
+ const launchConfirm = await arrowConfirm('Ready to launch your first task?', rl);
1360
+ if (launchConfirm) {
1361
+ rl.close();
1362
+ console.log('');
1363
+ const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
1364
+ stdio: 'inherit',
1365
+ cwd: ROOT,
1366
+ });
1367
+ child.on('exit', (code) => process.exit(code));
1368
+ return;
1369
+ }
584
1370
  }
1371
+
1372
+ console.log('');
1373
+ console.log(` ${bold('When ready, run:')}`);
1374
+ console.log(` ${cyan('npm run launch')}\n`);
1375
+ separator();
1376
+ console.log('');
1377
+ rl.close();
585
1378
  };
586
1379
 
587
1380
  main().catch((err) => {
588
1381
  console.error('\n Error:', err.message);
589
1382
  process.exit(1);
590
- });
1383
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-agents-cli",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Multi-agent workflow orchestration for Claude Code — isolated git worktrees, structured state tracking, autonomous task chaining",
5
5
  "keywords": [
6
6
  "claude-code",