multi-agents-cli 1.0.13 → 1.0.15
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 +97 -752
- package/package.json +1 -1
package/init.js
CHANGED
|
@@ -1,60 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Run with: npm run init
|
|
6
|
-
* or: multi-agents init my-project (global CLI)
|
|
4
|
+
* Multi-Agent Monorepo Template - Project Initializer
|
|
5
|
+
* Run with: npm run init
|
|
7
6
|
*
|
|
8
7
|
* 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');
|
|
58
14
|
const { execSync, spawn } = require('child_process');
|
|
59
15
|
|
|
60
16
|
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
@@ -78,69 +34,58 @@ const cyan = (s) => `${c.cyan}${s}${c.reset}`;
|
|
|
78
34
|
const blue = (s) => `${c.blue}${s}${c.reset}`;
|
|
79
35
|
const red = (s) => `${c.red}${s}${c.reset}`;
|
|
80
36
|
|
|
81
|
-
// ──
|
|
82
|
-
|
|
83
|
-
const args = process.argv.slice(2);
|
|
84
|
-
const isGlobalCLI = args[0] === 'init' && args[1];
|
|
85
|
-
const projectArg = isGlobalCLI ? args[1] : null;
|
|
86
|
-
|
|
87
|
-
if (isGlobalCLI) {
|
|
88
|
-
const targetDir = path.resolve(process.cwd(), projectArg);
|
|
89
|
-
|
|
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
|
-
}
|
|
37
|
+
// ── Lock check ────────────────────────────────────────────────────────────────
|
|
95
38
|
|
|
96
|
-
|
|
97
|
-
|
|
39
|
+
const ROOT = __dirname;
|
|
40
|
+
const RUNTIME_DIR = path.join(ROOT, '.scaffold');
|
|
41
|
+
const LOCK_FILE = path.join(RUNTIME_DIR, '.initialized');
|
|
98
42
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 */ }
|
|
110
|
-
}
|
|
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 });
|
|
111
47
|
}
|
|
112
48
|
|
|
113
|
-
|
|
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())));
|
|
114
53
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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`);
|
|
118
59
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
60
|
+
const choice = await ask2(` ${bold('Select')} ${dim('(1-3)')}: `);
|
|
61
|
+
|
|
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);
|
|
84
|
+
}
|
|
122
85
|
}
|
|
123
86
|
|
|
124
87
|
// ── Decision tree ─────────────────────────────────────────────────────────────
|
|
125
88
|
|
|
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
|
-
|
|
144
89
|
const CLIENT_FRAMEWORKS = [
|
|
145
90
|
{ label: 'Next.js', value: 'Next.js', language: 'TypeScript', integratedBackend: true },
|
|
146
91
|
{ label: 'Angular', value: 'Angular', language: 'TypeScript', integratedBackend: false },
|
|
@@ -155,7 +100,6 @@ const BACKEND_FRAMEWORKS = [
|
|
|
155
100
|
{ label: 'Express', value: 'Express', language: 'TypeScript' },
|
|
156
101
|
{ label: 'Fastify', value: 'Fastify', language: 'TypeScript' },
|
|
157
102
|
{ label: 'Django', value: 'Django', language: 'Python' },
|
|
158
|
-
{ label: 'FastAPI', value: 'FastAPI', language: 'Python' },
|
|
159
103
|
{ label: 'Laravel', value: 'Laravel', language: 'PHP' },
|
|
160
104
|
{ label: 'Rails', value: 'Rails', language: 'Ruby' },
|
|
161
105
|
];
|
|
@@ -191,7 +135,6 @@ const ORM_OPTIONS = {
|
|
|
191
135
|
'Express': ['Prisma', 'TypeORM', 'Drizzle', 'Sequelize'],
|
|
192
136
|
'Fastify': ['Prisma', 'TypeORM', 'Drizzle'],
|
|
193
137
|
'Django': ['Django ORM (built-in)', 'SQLAlchemy'],
|
|
194
|
-
'FastAPI': ['SQLAlchemy', 'Tortoise ORM', 'Beanie (MongoDB)'],
|
|
195
138
|
'Laravel': ['Eloquent (built-in)'],
|
|
196
139
|
'Rails': ['Active Record (built-in)'],
|
|
197
140
|
};
|
|
@@ -201,264 +144,11 @@ const AUTH_OPTIONS = {
|
|
|
201
144
|
'Express': ['Passport.js', 'JWT-only', 'OAuth2'],
|
|
202
145
|
'Fastify': ['fastify-jwt', 'Passport.js', 'OAuth2'],
|
|
203
146
|
'Django': ['Django Auth (built-in)', 'DRF TokenAuth', 'OAuth2'],
|
|
204
|
-
'FastAPI': ['JWT-only', 'OAuth2', 'FastAPI-Users'],
|
|
205
147
|
'Laravel': ['Laravel Sanctum', 'Laravel Passport', 'JWT'],
|
|
206
148
|
'Rails': ['Devise', 'JWT', 'OAuth2'],
|
|
207
149
|
};
|
|
208
150
|
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
cmd: 'code',
|
|
212
|
-
name: 'VS Code',
|
|
213
|
-
mac: { app: 'Visual Studio Code', args: ['--new-window'] },
|
|
214
|
-
win: { paths: ['{LOCALAPPDATA}\\Programs\\Microsoft VS Code\\Code.exe', '{ProgramFiles}\\Microsoft VS Code\\Code.exe'], args: ['--new-window'] },
|
|
215
|
-
linux: { paths: ['/snap/bin/code', '/usr/bin/code', '/usr/local/bin/code'], args: ['--new-window'] },
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
cmd: 'cursor',
|
|
219
|
-
name: 'Cursor',
|
|
220
|
-
mac: { app: 'Cursor', args: ['--new-window'] },
|
|
221
|
-
win: { paths: ['{LOCALAPPDATA}\\Programs\\cursor\\Cursor.exe'], args: ['--new-window'] },
|
|
222
|
-
linux: { paths: ['/usr/bin/cursor', '/opt/cursor/cursor'], args: ['--new-window'] },
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
cmd: 'webstorm',
|
|
226
|
-
name: 'WebStorm',
|
|
227
|
-
mac: { app: 'WebStorm', toolboxApp: 'WebStorm', args: [] },
|
|
228
|
-
win: { paths: [
|
|
229
|
-
'{LOCALAPPDATA}\\JetBrains\\Toolbox\\scripts\\webstorm.cmd',
|
|
230
|
-
'{LOCALAPPDATA}\\Programs\\WebStorm\\bin\\webstorm64.exe',
|
|
231
|
-
], args: [] },
|
|
232
|
-
linux: { paths: [
|
|
233
|
-
`${os.homedir()}/.local/bin/webstorm`,
|
|
234
|
-
'/opt/webstorm/bin/webstorm.sh',
|
|
235
|
-
'/snap/webstorm/current/bin/webstorm.sh',
|
|
236
|
-
], args: [] },
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
cmd: 'idea',
|
|
240
|
-
name: 'IntelliJ IDEA',
|
|
241
|
-
mac: { app: 'IntelliJ IDEA', toolboxApp: 'IntelliJ IDEA', args: [] },
|
|
242
|
-
win: { paths: [
|
|
243
|
-
'{LOCALAPPDATA}\\JetBrains\\Toolbox\\scripts\\idea.cmd',
|
|
244
|
-
'{LOCALAPPDATA}\\Programs\\IntelliJ IDEA Community Edition\\bin\\idea64.exe',
|
|
245
|
-
'{ProgramFiles}\\JetBrains\\IntelliJ IDEA\\bin\\idea64.exe',
|
|
246
|
-
], args: [] },
|
|
247
|
-
linux: { paths: [
|
|
248
|
-
`${os.homedir()}/.local/bin/idea`,
|
|
249
|
-
'/opt/idea/bin/idea.sh',
|
|
250
|
-
'/snap/intellij-idea-community/current/bin/idea.sh',
|
|
251
|
-
], args: [] },
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
cmd: 'zed',
|
|
255
|
-
name: 'Zed',
|
|
256
|
-
mac: { app: 'Zed', args: [] },
|
|
257
|
-
win: { paths: [], args: [] },
|
|
258
|
-
linux: { paths: ['/usr/bin/zed', `${os.homedir()}/.local/bin/zed`], args: [] },
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
cmd: null,
|
|
262
|
-
name: 'Other / Manual',
|
|
263
|
-
note: 'prints worktree path, open it yourself',
|
|
264
|
-
mac: null,
|
|
265
|
-
win: null,
|
|
266
|
-
linux:null,
|
|
267
|
-
},
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
// Expands {LOCALAPPDATA} / {ProgramFiles} placeholders for Windows paths
|
|
271
|
-
const expandWinPath = (p) =>
|
|
272
|
-
p.replace('{LOCALAPPDATA}', process.env.LOCALAPPDATA || '')
|
|
273
|
-
.replace('{ProgramFiles}', process.env.ProgramFiles || 'C:\\Program Files');
|
|
274
|
-
|
|
275
|
-
const buildIDEOptions = () => {
|
|
276
|
-
const platform = process.platform;
|
|
277
|
-
|
|
278
|
-
return IDE_CANDIDATES.map(ide => {
|
|
279
|
-
if (!ide.cmd) {
|
|
280
|
-
const noteStr = ide.note ? dim(` (${ide.note})`) : '';
|
|
281
|
-
return { ...ide, detected: false, strategy: 'manual', label: `${ide.name} ${dim('→')}${noteStr}` };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
let detected = false;
|
|
285
|
-
let strategy = 'cli';
|
|
286
|
-
|
|
287
|
-
if (platform === 'darwin' && ide.mac) {
|
|
288
|
-
// Mac — check .app bundle in /Applications, ~/Applications, and JetBrains Toolbox
|
|
289
|
-
const system = `/Applications/${ide.mac.app}.app`;
|
|
290
|
-
const user = path.join(os.homedir(), 'Applications', `${ide.mac.app}.app`);
|
|
291
|
-
const toolbox = path.join(os.homedir(), 'Applications', 'JetBrains Toolbox', `${ide.mac.app}.app`);
|
|
292
|
-
detected = fs.existsSync(system) || fs.existsSync(user) || fs.existsSync(toolbox);
|
|
293
|
-
if (detected) strategy = 'mac-app';
|
|
294
|
-
|
|
295
|
-
} else if (platform === 'win32' && ide.win) {
|
|
296
|
-
// Windows — CLI first, then known exe paths
|
|
297
|
-
try {
|
|
298
|
-
execSync(`where ${ide.cmd}`, { stdio: 'pipe' });
|
|
299
|
-
detected = true;
|
|
300
|
-
strategy = 'cli';
|
|
301
|
-
} catch {
|
|
302
|
-
const expanded = (ide.win.paths || []).map(expandWinPath);
|
|
303
|
-
detected = expanded.some(p => fs.existsSync(p));
|
|
304
|
-
if (detected) strategy = 'win-exe';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
} else if (platform === 'linux' && ide.linux) {
|
|
308
|
-
// Linux — CLI first, then known install paths
|
|
309
|
-
try {
|
|
310
|
-
execSync(`which ${ide.cmd}`, { stdio: 'pipe' });
|
|
311
|
-
detected = true;
|
|
312
|
-
strategy = 'cli';
|
|
313
|
-
} catch {
|
|
314
|
-
detected = (ide.linux.paths || []).some(p => fs.existsSync(p));
|
|
315
|
-
if (detected) strategy = 'linux-path';
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const statusStr = detected ? green('✓ detected') : dim('✗ not found');
|
|
320
|
-
const noteStr = ide.note ? dim(` (${ide.note})`) : '';
|
|
321
|
-
return {
|
|
322
|
-
...ide,
|
|
323
|
-
detected,
|
|
324
|
-
strategy,
|
|
325
|
-
label: `${ide.name} ${statusStr}${noteStr}`,
|
|
326
|
-
};
|
|
327
|
-
});
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const verifyIDE = (ide) => {
|
|
331
|
-
const platform = process.platform;
|
|
332
|
-
|
|
333
|
-
if (ide.strategy === 'mac-app' && ide.mac) {
|
|
334
|
-
// Mac — confirm .app exists and try to read version from plist
|
|
335
|
-
const appPath = `/Applications/${ide.mac.app}.app`;
|
|
336
|
-
if (!fs.existsSync(appPath) && !fs.existsSync(path.join(os.homedir(), 'Applications', `${ide.mac.app}.app`))) {
|
|
337
|
-
return { ok: false };
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
const version = execSync(
|
|
341
|
-
`defaults read "/Applications/${ide.mac.app}.app/Contents/Info.plist" CFBundleShortVersionString`,
|
|
342
|
-
{ stdio: 'pipe', encoding: 'utf8' }
|
|
343
|
-
).trim();
|
|
344
|
-
return { ok: true, version };
|
|
345
|
-
} catch {
|
|
346
|
-
return { ok: true, version: null };
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Windows exe / Linux path / CLI — try --version
|
|
351
|
-
try {
|
|
352
|
-
const cmd = ide.strategy === 'win-exe'
|
|
353
|
-
? `"${(ide.win?.paths || []).map(expandWinPath).find(p => fs.existsSync(p))}"`
|
|
354
|
-
: ide.strategy === 'linux-path'
|
|
355
|
-
? `"${(ide.linux?.paths || []).find(p => fs.existsSync(p))}"`
|
|
356
|
-
: `"${ide.cmd}"`;
|
|
357
|
-
const result = execSync(`${cmd} --version`, { stdio: 'pipe', encoding: 'utf8' });
|
|
358
|
-
const version = result.split('\n')[0].trim();
|
|
359
|
-
return { ok: true, version };
|
|
360
|
-
} catch {
|
|
361
|
-
return { ok: false };
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// ── Tracking structure ────────────────────────────────────────────────────────
|
|
366
|
-
|
|
367
|
-
const emptySlot = () => ({
|
|
368
|
-
branch: null,
|
|
369
|
-
timestamp: null,
|
|
370
|
-
launchedAt: null,
|
|
371
|
-
status: null,
|
|
372
|
-
missingCount: 0,
|
|
373
|
-
worktreePath: null,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const generateTrackingStructure = (config) => {
|
|
377
|
-
const bt = config.backend?.type;
|
|
378
|
-
|
|
379
|
-
const structure = {
|
|
380
|
-
client: {
|
|
381
|
-
UI: emptySlot(),
|
|
382
|
-
LOGIC: emptySlot(),
|
|
383
|
-
FORMS: emptySlot(),
|
|
384
|
-
ROUTING: emptySlot(),
|
|
385
|
-
TESTING: emptySlot(),
|
|
386
|
-
ACCESSIBILITY: emptySlot(),
|
|
387
|
-
},
|
|
388
|
-
shared: {
|
|
389
|
-
SECURITY: emptySlot(),
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
if (bt === 'separate') {
|
|
394
|
-
structure.backend = {
|
|
395
|
-
API: emptySlot(),
|
|
396
|
-
LOGIC: emptySlot(),
|
|
397
|
-
AUTH: emptySlot(),
|
|
398
|
-
DB: emptySlot(),
|
|
399
|
-
EVENTS: emptySlot(),
|
|
400
|
-
JOBS: emptySlot(),
|
|
401
|
-
TESTING: emptySlot(),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return structure;
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
// ── GitHub remote setup ───────────────────────────────────────────────────────
|
|
409
|
-
|
|
410
|
-
const detectGitHubUser = () => {
|
|
411
|
-
try {
|
|
412
|
-
return execSync('gh api user --jq .login',
|
|
413
|
-
{ encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
414
|
-
} catch {}
|
|
415
|
-
try {
|
|
416
|
-
return execSync('git config user.name',
|
|
417
|
-
{ encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
418
|
-
} catch {}
|
|
419
|
-
return null;
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const setupUserRemote = (ROOT, projectName) => {
|
|
423
|
-
let currentOrigin = null;
|
|
424
|
-
try {
|
|
425
|
-
currentOrigin = execSync('git remote get-url origin',
|
|
426
|
-
{ cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
427
|
-
} catch {}
|
|
428
|
-
|
|
429
|
-
// Already has their own remote — nothing to do
|
|
430
|
-
if (currentOrigin && !currentOrigin.includes('multi-agents-template')) return;
|
|
431
|
-
|
|
432
|
-
// Demote template origin to upstream
|
|
433
|
-
if (currentOrigin?.includes('multi-agents-template')) {
|
|
434
|
-
try {
|
|
435
|
-
execSync('git remote remove origin', { cwd: ROOT, stdio: 'pipe' });
|
|
436
|
-
execSync(`git remote add upstream ${currentOrigin}`, { cwd: ROOT, stdio: 'pipe' });
|
|
437
|
-
console.log(dim(' ℹ Template remote moved to upstream'));
|
|
438
|
-
} catch {}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Write flag — agent will handle remote setup on first session
|
|
442
|
-
const flagPath = path.join(ROOT, '.scaffold', '.remote-setup-needed');
|
|
443
|
-
fs.writeFileSync(flagPath, JSON.stringify({
|
|
444
|
-
projectName,
|
|
445
|
-
createdAt: new Date().toISOString(),
|
|
446
|
-
}), 'utf8');
|
|
447
|
-
|
|
448
|
-
console.log(`\n ${yellow('ℹ No remote configured.')} Your first agent session will set this up.`);
|
|
449
|
-
console.log(dim(' All work stays local until then.\n'));
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const renderTrajectoryLines = (lines) => {
|
|
453
|
-
const HEADERS = ['Benefits', 'Best for', 'Use agents for', 'Handle manually'];
|
|
454
|
-
lines.forEach(l => {
|
|
455
|
-
if (!l) { console.log(''); return; }
|
|
456
|
-
if (l.startsWith('⚠')) console.log(` ${yellow(l)}`);
|
|
457
|
-
else if (HEADERS.includes(l)) console.log(`\n ${bold(l)}`);
|
|
458
|
-
else if (l.startsWith('·')) console.log(` ${l}`);
|
|
459
|
-
else console.log(` ${dim(l)}`);
|
|
460
|
-
});
|
|
461
|
-
};
|
|
151
|
+
// ── Readline ──────────────────────────────────────────────────────────────────
|
|
462
152
|
|
|
463
153
|
const rl = readline.createInterface({
|
|
464
154
|
input: process.stdin,
|
|
@@ -478,25 +168,30 @@ const showList = (items, showSkip = false) => {
|
|
|
478
168
|
if (showSkip) console.log(` ${dim('0.')} Skip ${dim('(agent will propose when needed)')}`);
|
|
479
169
|
};
|
|
480
170
|
|
|
481
|
-
// Sentinel value returned when user picks ← Restart
|
|
482
|
-
const BACK = Symbol('BACK');
|
|
483
|
-
|
|
484
171
|
const selectRequired = async (prompt, items) => {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
}
|
|
488
180
|
};
|
|
489
181
|
|
|
490
182
|
const selectOptional = async (prompt, items) => {
|
|
491
183
|
if (!items || items.length === 0) return null;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
+
}
|
|
500
195
|
};
|
|
501
196
|
|
|
502
197
|
const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
|
|
@@ -556,53 +251,13 @@ const copyDir = (src, dest) => {
|
|
|
556
251
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
557
252
|
|
|
558
253
|
const main = async () => {
|
|
559
|
-
|
|
560
|
-
// ── Lock check ───────────────────────────────────────────────────────────────
|
|
561
|
-
|
|
562
|
-
if (fs.existsSync(LOCK_FILE)) {
|
|
563
|
-
const ts = fs.readFileSync(LOCK_FILE, 'utf8').trim();
|
|
564
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
565
|
-
const ask2 = (q) => new Promise((resolve) => rl2.question(q, (a) => resolve(a.trim())));
|
|
566
|
-
|
|
567
|
-
console.log(`\n${yellow(' This project has already been initialized.')}`);
|
|
568
|
-
console.log(dim(` Initialized on: ${ts}\n`));
|
|
569
|
-
console.log(` ${dim('1.')} Continue — run ${cyan('npm run launch')}`);
|
|
570
|
-
console.log(` ${dim('2.')} Reset — delete config and re-run initialization`);
|
|
571
|
-
console.log(` ${dim('3.')} Exit\n`);
|
|
572
|
-
|
|
573
|
-
const choice = await ask2(` ${bold('Select')} ${dim('(1-3)')}: `);
|
|
574
|
-
|
|
575
|
-
if (choice === '1') {
|
|
576
|
-
console.log('');
|
|
577
|
-
rl2.close();
|
|
578
|
-
const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
|
|
579
|
-
stdio: 'inherit',
|
|
580
|
-
cwd: ROOT,
|
|
581
|
-
});
|
|
582
|
-
child.on('exit', (code) => process.exit(code));
|
|
583
|
-
return;
|
|
584
|
-
} else if (choice === '2') {
|
|
585
|
-
console.log(yellow('\n Resetting configuration...\n'));
|
|
586
|
-
fs.unlinkSync(LOCK_FILE);
|
|
587
|
-
const configPath = path.join(RUNTIME_DIR, '.config.json');
|
|
588
|
-
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
589
|
-
rl2.close();
|
|
590
|
-
console.log(green(' Reset complete. Re-running initialization...\n'));
|
|
591
|
-
// Fall through to run init again
|
|
592
|
-
} else {
|
|
593
|
-
console.log(dim('\n Exited.\n'));
|
|
594
|
-
rl2.close();
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
254
|
console.log('\n');
|
|
600
255
|
console.log(bold(cyan(' Multi-Agent Monorepo Template')));
|
|
601
256
|
console.log(dim(' Project Initializer\n'));
|
|
602
257
|
separator();
|
|
603
258
|
|
|
604
259
|
console.log(`\n${bold('Let\'s configure your project.')}`);
|
|
605
|
-
console.log(dim('
|
|
260
|
+
console.log(dim(' Required fields must be selected. Optional fields can be skipped (press 0 or Enter).\n'));
|
|
606
261
|
console.log(dim(' Skipped fields will be resolved by the agent when first needed.\n'));
|
|
607
262
|
|
|
608
263
|
// ── Project name ────────────────────────────────────────────────────────────
|
|
@@ -613,14 +268,6 @@ const main = async () => {
|
|
|
613
268
|
if (!projectName) console.log(yellow(' Project name is required. Please enter a name.'));
|
|
614
269
|
}
|
|
615
270
|
|
|
616
|
-
const restartIfBack = (val) => {
|
|
617
|
-
if (val !== BACK) return false;
|
|
618
|
-
rl.close();
|
|
619
|
-
const { spawn } = require('child_process');
|
|
620
|
-
spawn('node', [__filename], { stdio: 'inherit', cwd: ROOT }).on('exit', c => process.exit(c));
|
|
621
|
-
return true;
|
|
622
|
-
};
|
|
623
|
-
|
|
624
271
|
separator();
|
|
625
272
|
|
|
626
273
|
// ── Client ──────────────────────────────────────────────────────────────────
|
|
@@ -628,14 +275,10 @@ const main = async () => {
|
|
|
628
275
|
console.log(`\n${bold(blue('Client configuration'))}`);
|
|
629
276
|
|
|
630
277
|
const clientFw = await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS);
|
|
631
|
-
if (restartIfBack(clientFw)) return;
|
|
632
278
|
const clientLang = clientFw.language;
|
|
633
279
|
const clientState = await selectOptional('State management:', STATE_OPTIONS[clientFw.value] || []);
|
|
634
|
-
if (restartIfBack(clientState)) return;
|
|
635
280
|
const clientUi = await selectOptional('UI library:', UI_OPTIONS[clientFw.value] || []);
|
|
636
|
-
if (restartIfBack(clientUi)) return;
|
|
637
281
|
const clientStyle = await selectOptional('Styling:', STYLING_OPTIONS);
|
|
638
|
-
if (restartIfBack(clientStyle)) return;
|
|
639
282
|
|
|
640
283
|
separator();
|
|
641
284
|
|
|
@@ -650,11 +293,11 @@ const main = async () => {
|
|
|
650
293
|
let backendOrm = null;
|
|
651
294
|
let backendAuth = null;
|
|
652
295
|
let backendType = null;
|
|
653
|
-
let backendFwObj = null;
|
|
654
296
|
|
|
655
297
|
if (clientFw.integratedBackend) {
|
|
656
298
|
console.log(dim(` ${clientFw.value} supports server-side rendering and API routes.\n`));
|
|
657
|
-
|
|
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';
|
|
658
301
|
|
|
659
302
|
if (useIntegratedBackend) {
|
|
660
303
|
backendType = 'integrated';
|
|
@@ -665,87 +308,25 @@ const main = async () => {
|
|
|
665
308
|
if (!useIntegratedBackend) {
|
|
666
309
|
console.log(dim(' You can skip the backend framework and decide later.\n'));
|
|
667
310
|
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
})();
|
|
674
320
|
|
|
675
321
|
backendFw = backendFwObj ? backendFwObj.value : null;
|
|
676
322
|
backendLang = backendFwObj ? backendFwObj.language : null;
|
|
677
323
|
backendOrm = backendFw ? await selectOptional('ORM / database layer:', ORM_OPTIONS[backendFw] || []) : null;
|
|
678
|
-
if (restartIfBack(backendOrm)) return;
|
|
679
324
|
backendAuth = backendFw ? await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFw] || []) : null;
|
|
680
|
-
if (restartIfBack(backendAuth)) return;
|
|
681
325
|
backendType = backendFw ? 'separate' : null;
|
|
682
326
|
}
|
|
683
327
|
|
|
684
328
|
separator();
|
|
685
329
|
|
|
686
|
-
// ── Environment ─────────────────────────────────────────────────────────────
|
|
687
|
-
|
|
688
|
-
console.log(`\n${bold(blue('Environment'))}`);
|
|
689
|
-
|
|
690
|
-
const osName = { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }[process.platform] || process.platform;
|
|
691
|
-
console.log(`\n ${dim('OS detected:')} ${bold(osName)}`);
|
|
692
|
-
console.log(dim(' Scanning for installed IDEs...\n'));
|
|
693
|
-
|
|
694
|
-
const ideOptions = buildIDEOptions();
|
|
695
|
-
|
|
696
|
-
const detectedIDEs = ideOptions.filter(o => o.detected);
|
|
697
|
-
const undetectedIDEs = ideOptions.filter(o => !o.detected && o.cmd);
|
|
698
|
-
const manualOption = ideOptions.filter(o => !o.cmd);
|
|
699
|
-
|
|
700
|
-
// Detected first → undetected → manual
|
|
701
|
-
const sortedIdeOptions = [...detectedIDEs, ...undetectedIDEs, ...manualOption];
|
|
702
|
-
|
|
703
|
-
if (detectedIDEs.length > 1) {
|
|
704
|
-
console.log(`\n ${yellow('Multiple IDEs found on this machine')} — select your preference:\n`);
|
|
705
|
-
} else if (detectedIDEs.length === 1) {
|
|
706
|
-
console.log(`\n ${green(`1 IDE found:`)} ${bold(detectedIDEs[0].name)}\n`);
|
|
707
|
-
} else {
|
|
708
|
-
console.log(`\n ${yellow('No IDEs detected on this machine.')}\n`);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
let ideChoice;
|
|
712
|
-
while (true) {
|
|
713
|
-
ideChoice = await selectRequired('* IDE / editor (required):', sortedIdeOptions);
|
|
714
|
-
if (restartIfBack(ideChoice)) return;
|
|
715
|
-
|
|
716
|
-
// ── Confirmation ──────────────────────────────────────────────────────────
|
|
717
|
-
if (ideChoice.cmd && !ideChoice.detected) {
|
|
718
|
-
console.log(`\n ${yellow('⚠')} ${bold(ideChoice.name)} was not detected on this machine.`);
|
|
719
|
-
console.log(dim(' It may not open automatically when launching a task.\n'));
|
|
720
|
-
if (!await arrowConfirm('Continue with this IDE anyway?', rl)) {
|
|
721
|
-
console.log(dim(' Re-selecting...\n'));
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// ── Double-check ──────────────────────────────────────────────────────────
|
|
727
|
-
if (!ideChoice.cmd) {
|
|
728
|
-
// Manual — no verification needed
|
|
729
|
-
console.log(dim(' Manual mode — worktree path will be printed at launch.'));
|
|
730
|
-
break;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
console.log(dim(`\n Verifying ${ideChoice.name}...`));
|
|
734
|
-
const verified = verifyIDE(ideChoice);
|
|
735
|
-
|
|
736
|
-
if (verified.ok) {
|
|
737
|
-
const versionStr = verified.version ? dim(` (${verified.version})`) : '';
|
|
738
|
-
console.log(` ${green('✓')} ${ideChoice.name} confirmed${versionStr}`);
|
|
739
|
-
break;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
console.log(` ${yellow('!')} Could not verify ${ideChoice.name}. The CLI may not be installed or accessible.`);
|
|
743
|
-
if (await arrowConfirm('Continue with this IDE anyway?', rl)) break;
|
|
744
|
-
console.log(dim(' Re-selecting...\n'));
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
separator();
|
|
748
|
-
|
|
749
330
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
750
331
|
|
|
751
332
|
console.log(`\n${bold('Review your configuration:')}\n`);
|
|
@@ -761,17 +342,12 @@ const main = async () => {
|
|
|
761
342
|
summaryLine('ORM', backendOrm);
|
|
762
343
|
summaryLine('Auth', backendAuth);
|
|
763
344
|
}
|
|
764
|
-
summaryLine('IDE / Editor', ideChoice.name);
|
|
765
345
|
|
|
766
346
|
console.log('');
|
|
767
347
|
console.log(dim(' y = confirm | n = abort | e = edit (start over)\n'));
|
|
768
|
-
const
|
|
769
|
-
{ label: `${green('✓')} Confirm — write config and set up project` },
|
|
770
|
-
{ label: `${yellow('↺')} Restart — redo configuration` },
|
|
771
|
-
{ label: `${red('✗')} Abort` },
|
|
772
|
-
], rl);
|
|
348
|
+
const confirm = await ask(`${bold('Confirm and write to config files?')} ${dim('(y/n/e)')}: `);
|
|
773
349
|
|
|
774
|
-
if (
|
|
350
|
+
if (confirm.toLowerCase() === 'e') {
|
|
775
351
|
console.log(yellow('\n Restarting configuration...\n'));
|
|
776
352
|
rl.close();
|
|
777
353
|
const { spawn } = require('child_process');
|
|
@@ -780,7 +356,7 @@ const main = async () => {
|
|
|
780
356
|
return;
|
|
781
357
|
}
|
|
782
358
|
|
|
783
|
-
if (
|
|
359
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
784
360
|
console.log(yellow('\n Aborted. No files were changed.\n'));
|
|
785
361
|
rl.close();
|
|
786
362
|
return;
|
|
@@ -812,8 +388,6 @@ const main = async () => {
|
|
|
812
388
|
copyDir(path.join(TEMPLATES, 'shared'), path.join(ROOT, 'shared'));
|
|
813
389
|
if (backendType === 'separate') {
|
|
814
390
|
copyDir(path.join(TEMPLATES, 'backend'), path.join(ROOT, 'backend'));
|
|
815
|
-
// Ensure backend/ is tracked by git even before the API agent scaffolds
|
|
816
|
-
fs.writeFileSync(path.join(ROOT, 'backend', '.gitkeep'), '', 'utf8');
|
|
817
391
|
}
|
|
818
392
|
fs.copyFileSync(path.join(TEMPLATES, 'CLAUDE.md'), path.join(ROOT, 'CLAUDE.md'));
|
|
819
393
|
fs.copyFileSync(path.join(TEMPLATES, 'CONTRACTS.md'), path.join(ROOT, 'CONTRACTS.md'));
|
|
@@ -863,33 +437,12 @@ const main = async () => {
|
|
|
863
437
|
ensureGitignore('.agents-core/');
|
|
864
438
|
ensureGitignore('.scaffold/');
|
|
865
439
|
ensureGitignore('.workflow/');
|
|
866
|
-
|
|
867
|
-
// Remove template-specific gitignore entries so generated files can be committed
|
|
868
|
-
const gitignorePath = path.join(ROOT, '.gitignore');
|
|
869
|
-
let gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
870
|
-
['client/', 'backend/', 'shared/', 'CLAUDE.md', 'CONTRACTS.md', 'BUILD_STATE.md'].forEach(entry => {
|
|
871
|
-
gitignoreContent = gitignoreContent.replace(`\n${entry}`, '');
|
|
872
|
-
gitignoreContent = gitignoreContent.replace(`${entry}\n`, '');
|
|
873
|
-
gitignoreContent = gitignoreContent.replace(entry, '');
|
|
874
|
-
});
|
|
875
|
-
fs.writeFileSync(gitignorePath, gitignoreContent.trim() + '\n', 'utf8');
|
|
876
440
|
console.log(` ${green('✓')} .gitignore updated`);
|
|
877
441
|
|
|
878
442
|
// ── Write .config.json ───────────────────────────────────────────────────────
|
|
879
443
|
|
|
880
444
|
const config = {
|
|
881
445
|
projectName,
|
|
882
|
-
ide: {
|
|
883
|
-
name: ideChoice.name,
|
|
884
|
-
strategy: ideChoice.strategy,
|
|
885
|
-
cmd: ideChoice.cmd || null,
|
|
886
|
-
app: ideChoice.mac?.app || null,
|
|
887
|
-
openArgs: process.platform === 'darwin' ? (ideChoice.mac?.args || [])
|
|
888
|
-
: process.platform === 'win32' ? (ideChoice.win?.args || [])
|
|
889
|
-
: (ideChoice.linux?.args || []),
|
|
890
|
-
winPaths: (ideChoice.win?.paths || []).map(expandWinPath),
|
|
891
|
-
linuxPaths: ideChoice.linux?.paths || [],
|
|
892
|
-
},
|
|
893
446
|
client: {
|
|
894
447
|
framework: clientFw.value,
|
|
895
448
|
language: clientLang,
|
|
@@ -989,67 +542,6 @@ If a dependency is not met:
|
|
|
989
542
|
fs.writeFileSync(path.join(ROOT, 'BUILD_STATE.md'), buildState, 'utf8');
|
|
990
543
|
console.log(` ${green('✓')} BUILD_STATE.md generated`);
|
|
991
544
|
|
|
992
|
-
// ── Generate user project package.json ───────────────────────────────────────
|
|
993
|
-
|
|
994
|
-
const userPackage = {
|
|
995
|
-
name: projectName.toLowerCase().replace(/\s+/g, '-'),
|
|
996
|
-
version: '1.0.0',
|
|
997
|
-
private: true,
|
|
998
|
-
dependencies: {
|
|
999
|
-
prompts: '^2.4.2',
|
|
1000
|
-
},
|
|
1001
|
-
scripts: {
|
|
1002
|
-
launch: 'cd "$(git rev-parse --git-common-dir)/.." && node .workflow/launch.js',
|
|
1003
|
-
complete: 'cd "$(git rev-parse --git-common-dir)/.." && node .workflow/complete.js',
|
|
1004
|
-
},
|
|
1005
|
-
};
|
|
1006
|
-
fs.writeFileSync(path.join(ROOT, 'package.json'), JSON.stringify(userPackage, null, 2), 'utf8');
|
|
1007
|
-
console.log(` ${green('✓')} package.json generated`);
|
|
1008
|
-
|
|
1009
|
-
// ── Install dependencies ──────────────────────────────────────────────────────
|
|
1010
|
-
|
|
1011
|
-
try {
|
|
1012
|
-
console.log(dim(' Installing dependencies...'));
|
|
1013
|
-
execSync('npm install', { cwd: ROOT, stdio: 'pipe' });
|
|
1014
|
-
console.log(` ${green('✓')} Dependencies installed`);
|
|
1015
|
-
} catch {
|
|
1016
|
-
console.log(yellow(' ⚠ npm install failed — run npm install manually before launching'));
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// ── Tracking ──────────────────────────────────────────────────────────────────
|
|
1020
|
-
|
|
1021
|
-
const trackingPath = path.join(RUNTIME_DIR, '.tracking.json');
|
|
1022
|
-
if (!fs.existsSync(trackingPath)) {
|
|
1023
|
-
const trackingStructure = generateTrackingStructure(config);
|
|
1024
|
-
fs.writeFileSync(trackingPath, JSON.stringify(trackingStructure, null, 2), 'utf8');
|
|
1025
|
-
console.log(` ${green('✓')} .tracking.json generated`);
|
|
1026
|
-
} else {
|
|
1027
|
-
console.log(dim(' ℹ .tracking.json already exists — preserved'));
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// ── Generate .paths.json ──────────────────────────────────────────────────────
|
|
1031
|
-
|
|
1032
|
-
const pathsMap = {};
|
|
1033
|
-
const clientConventions = FRAMEWORK_CONVENTIONS.client[clientFw?.value] || {};
|
|
1034
|
-
const backendConventions = FRAMEWORK_CONVENTIONS.backend[backendFwObj?.value] || {};
|
|
1035
|
-
|
|
1036
|
-
if (Object.keys(clientConventions).length) {
|
|
1037
|
-
pathsMap.client = {};
|
|
1038
|
-
Object.entries(clientConventions).forEach(([key, value]) => {
|
|
1039
|
-
pathsMap.client[key] = { expected: value, current: null, status: 'pending' };
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
if (Object.keys(backendConventions).length) {
|
|
1044
|
-
pathsMap.backend = {};
|
|
1045
|
-
Object.entries(backendConventions).forEach(([key, value]) => {
|
|
1046
|
-
pathsMap.backend[key] = { expected: value, current: null, status: 'pending' };
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
fs.writeFileSync(path.join(RUNTIME_DIR, '.paths.json'), JSON.stringify(pathsMap, null, 2), 'utf8');
|
|
1051
|
-
console.log(` ${green('✓')} .paths.json generated`);
|
|
1052
|
-
|
|
1053
545
|
// ── Lock ─────────────────────────────────────────────────────────────────────
|
|
1054
546
|
|
|
1055
547
|
fs.writeFileSync(LOCK_FILE, new Date().toISOString());
|
|
@@ -1066,180 +558,33 @@ If a dependency is not met:
|
|
|
1066
558
|
console.log(dim(' git add . && git commit -m "init: project configuration"'));
|
|
1067
559
|
}
|
|
1068
560
|
|
|
1069
|
-
// ──
|
|
1070
|
-
|
|
1071
|
-
try {
|
|
1072
|
-
const hooksDir = path.join(ROOT, '.git', 'hooks');
|
|
1073
|
-
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
1074
|
-
const hookScript = `#!/bin/sh
|
|
1075
|
-
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
1076
|
-
if [ "$branch" = "main" ]; then
|
|
1077
|
-
echo ""
|
|
1078
|
-
echo " ⚠ Direct commits to main are not allowed."
|
|
1079
|
-
echo " Use npm run launch to start a task."
|
|
1080
|
-
echo ""
|
|
1081
|
-
exit 1
|
|
1082
|
-
fi
|
|
1083
|
-
`;
|
|
1084
|
-
if (!fs.existsSync(hookPath)) {
|
|
1085
|
-
fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
|
|
1086
|
-
console.log(dim(' ℹ Pre-commit hook installed — direct main commits blocked'));
|
|
1087
|
-
}
|
|
1088
|
-
} catch { /* best-effort */ }
|
|
1089
|
-
|
|
1090
|
-
// ── Remote setup ─────────────────────────────────────────────────────────────
|
|
1091
|
-
|
|
1092
|
-
setupUserRemote(ROOT, projectName);
|
|
1093
|
-
|
|
1094
|
-
// ── Trajectory selection ─────────────────────────────────────────────────────
|
|
561
|
+
// ── Chain to launch.js ────────────────────────────────────────────────────────
|
|
1095
562
|
|
|
1096
563
|
separator();
|
|
1097
564
|
console.log(`\n${bold(green(' Project initialized successfully!'))}\n`);
|
|
1098
|
-
console.log(` ${bold('How do you want to build?')}\n`);
|
|
1099
|
-
|
|
1100
|
-
console.log(` ${dim('1.')} ${bold('Multi-Agent Driven Orchestration')}`);
|
|
1101
|
-
console.log(`${dim(' · Every task should start with npm run launch')}`);
|
|
1102
|
-
console.log(`${dim(' · Each agent runs in its own git worktree — an isolated branch')}`);
|
|
1103
|
-
console.log(`${dim(' and folder that merges back into main via npm run complete')}`);
|
|
1104
|
-
console.log(`${dim(' · Faster builds and lower token spend than a single long session')}`);
|
|
1105
|
-
console.log(`${yellow(' ⚠ If you commit directly to main yourself, you bypass the framework')}`);
|
|
1106
|
-
console.log(`${yellow(' and break task tracking for any active agent branches')}\n`);
|
|
1107
|
-
|
|
1108
|
-
console.log(` ${dim('2.')} ${bold('Shared Orchestration')}`);
|
|
1109
|
-
console.log(`${dim(' · You and agents co-build — each owning a defined part of the codebase')}`);
|
|
1110
|
-
console.log(`${dim(' · Agent tasks run in git worktrees; your work happens directly in the project')}`);
|
|
1111
|
-
console.log(`${dim(' · Agent tasks are token-efficient; your tasks cost only what you prompt')}`);
|
|
1112
|
-
console.log(`${dim(' · Define boundaries before work begins — agents for well-scoped work,')}`);
|
|
1113
|
-
console.log(`${dim(' you for areas where requirements are still evolving')}`);
|
|
1114
|
-
console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
|
|
1115
|
-
|
|
1116
|
-
const TRAJECTORY_DETAILS = {
|
|
1117
|
-
'1': {
|
|
1118
|
-
label: 'Multi-Agent Driven Orchestration',
|
|
1119
|
-
full: [
|
|
1120
|
-
'Every task must start with npm run launch.',
|
|
1121
|
-
'Agent sessions load only task-relevant context, enabling reliable',
|
|
1122
|
-
'chaining, predictable behavior, and efficient token usage.',
|
|
1123
|
-
'',
|
|
1124
|
-
'⚠ If you commit directly to main yourself, you bypass the framework',
|
|
1125
|
-
' and break task tracking for any active agent branches.',
|
|
1126
|
-
'',
|
|
1127
|
-
'Benefits',
|
|
1128
|
-
'· Scoped context per task',
|
|
1129
|
-
'· Predictable token consumption',
|
|
1130
|
-
'· Lower cost than maintaining large, persistent sessions',
|
|
1131
|
-
'· Better isolation between parallel work streams',
|
|
1132
|
-
],
|
|
1133
|
-
next: 'launch',
|
|
1134
|
-
},
|
|
1135
|
-
'2': {
|
|
1136
|
-
label: 'Shared Orchestration',
|
|
1137
|
-
full: [
|
|
1138
|
-
'You and agents work in the same codebase, each with clearly',
|
|
1139
|
-
'defined ownership. File boundaries must be established before',
|
|
1140
|
-
'work begins and remain fixed throughout the task.',
|
|
1141
|
-
'Agents excel when scope is well-defined;',
|
|
1142
|
-
'you excel when requirements are evolving.',
|
|
1143
|
-
'',
|
|
1144
|
-
'Use agents for',
|
|
1145
|
-
'· Multi-file features',
|
|
1146
|
-
'· Structured implementation work',
|
|
1147
|
-
'· Domain-specific tasks',
|
|
1148
|
-
'· Changes expected to exceed ~200 lines',
|
|
1149
|
-
'',
|
|
1150
|
-
'Handle manually',
|
|
1151
|
-
'· Targeted bug fixes',
|
|
1152
|
-
'· Configuration changes',
|
|
1153
|
-
'· Small refactors',
|
|
1154
|
-
'· Single-file edits under ~50 lines',
|
|
1155
|
-
'',
|
|
1156
|
-
'⚠ Avoid overlapping file ownership. Working on the same files',
|
|
1157
|
-
' as an active agent will create merge conflicts when merged.',
|
|
1158
|
-
'⚠ If you are spending time repeatedly clarifying scope, stop',
|
|
1159
|
-
' and do the task yourself. The coordination cost often',
|
|
1160
|
-
' exceeds the implementation cost.',
|
|
1161
|
-
'',
|
|
1162
|
-
'Benefits',
|
|
1163
|
-
'· Maximum agent efficiency for well-defined work',
|
|
1164
|
-
'· Human flexibility where requirements change',
|
|
1165
|
-
'· Scales well across large projects',
|
|
1166
|
-
'· Most adaptable workflow — requires the most discipline',
|
|
1167
|
-
],
|
|
1168
|
-
next: 'launch',
|
|
1169
|
-
},
|
|
1170
|
-
};
|
|
1171
565
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
trajectoryLoop: while (true) {
|
|
1175
|
-
const trajIdx = await arrowSelect('How do you want to build?', [
|
|
1176
|
-
{ label: bold('Multi-Agent Driven Orchestration') },
|
|
1177
|
-
{ label: bold('Shared Orchestration') },
|
|
1178
|
-
], rl);
|
|
1179
|
-
trajectory = String(trajIdx + 1);
|
|
566
|
+
const launchInput = await ask(` ${bold('Ready to launch your first task?')} ${dim('(y/n — default: n)')}: `);
|
|
567
|
+
const launch = launchInput.toLowerCase() || 'n';
|
|
1180
568
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
console.log(`\n ${green('✓')} ${bold(selected.label)}\n`);
|
|
1184
|
-
renderTrajectoryLines(selected.full);
|
|
569
|
+
if (launch === 'y') {
|
|
570
|
+
rl.close();
|
|
1185
571
|
console.log('');
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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`);
|
|
1193
581
|
separator();
|
|
1194
|
-
console.log(
|
|
1195
|
-
|
|
1196
|
-
console.log(`${dim(' · Every task should start with npm run launch')}`);
|
|
1197
|
-
console.log(`${dim(' · Each agent runs in its own git worktree — an isolated branch')}`);
|
|
1198
|
-
console.log(`${dim(' and folder that merges back into main via npm run complete')}`);
|
|
1199
|
-
console.log(`${dim(' · Faster builds and lower token spend than a single long session')}`);
|
|
1200
|
-
console.log(`${yellow(' ⚠ If you commit directly to main yourself, you bypass the framework')}`);
|
|
1201
|
-
console.log(`${yellow(' and break task tracking for any active agent branches')}\n`);
|
|
1202
|
-
console.log(` ${dim('2.')} ${bold('Shared Orchestration')}`);
|
|
1203
|
-
console.log(`${dim(' · You and agents co-build — each owning a defined part of the codebase')}`);
|
|
1204
|
-
console.log(`${dim(' · Agent tasks run in git worktrees; your work happens directly in the project')}`);
|
|
1205
|
-
console.log(`${dim(' · Agent tasks are token-efficient; your tasks cost only what you prompt')}`);
|
|
1206
|
-
console.log(`${dim(' · Define boundaries before work begins — agents for well-scoped work,')}`);
|
|
1207
|
-
console.log(`${dim(' you for areas where requirements are still evolving')}`);
|
|
1208
|
-
console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
const selected = TRAJECTORY_DETAILS[trajectory];
|
|
1212
|
-
|
|
1213
|
-
// Store trajectory in config
|
|
1214
|
-
try {
|
|
1215
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(RUNTIME_DIR, '.config.json'), 'utf8'));
|
|
1216
|
-
cfg.trajectory = selected.label.toLowerCase().replace(/ /g, '-');
|
|
1217
|
-
fs.writeFileSync(path.join(RUNTIME_DIR, '.config.json'), JSON.stringify(cfg, null, 2), 'utf8');
|
|
1218
|
-
} catch { /* best-effort */ }
|
|
1219
|
-
|
|
1220
|
-
if (selected.next === 'launch') {
|
|
1221
|
-
const launchConfirm = await arrowConfirm('Ready to launch your first task?', rl);
|
|
1222
|
-
if (launchConfirm) {
|
|
1223
|
-
rl.close();
|
|
1224
|
-
console.log('');
|
|
1225
|
-
const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
|
|
1226
|
-
stdio: 'inherit',
|
|
1227
|
-
cwd: ROOT,
|
|
1228
|
-
});
|
|
1229
|
-
child.on('exit', (code) => process.exit(code));
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
582
|
+
console.log('');
|
|
583
|
+
rl.close();
|
|
1232
584
|
}
|
|
1233
|
-
|
|
1234
|
-
console.log('');
|
|
1235
|
-
console.log(` ${bold('When ready, run:')}`);
|
|
1236
|
-
console.log(` ${cyan('npm run launch')}\n`);
|
|
1237
|
-
separator();
|
|
1238
|
-
console.log('');
|
|
1239
|
-
rl.close();
|
|
1240
585
|
};
|
|
1241
586
|
|
|
1242
587
|
main().catch((err) => {
|
|
1243
588
|
console.error('\n Error:', err.message);
|
|
1244
589
|
process.exit(1);
|
|
1245
|
-
});
|
|
590
|
+
});
|
package/package.json
CHANGED