multi-agents-cli 1.0.16 → 1.0.18
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 +876 -108
- package/package.json +1 -1
package/init.js
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
-
// ──
|
|
38
|
-
|
|
39
|
-
const ROOT = __dirname;
|
|
40
|
-
const RUNTIME_DIR = path.join(ROOT, '.scaffold');
|
|
41
|
-
const LOCK_FILE = path.join(RUNTIME_DIR, '.initialized');
|
|
81
|
+
// ── CLI argument handling ─────────────────────────────────────────────────────
|
|
42
82
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
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 (
|
|
50
|
-
const
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
96
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
97
|
+
process.chdir(targetDir);
|
|
61
98
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
894
|
+
if (confirmIdx === 2) {
|
|
360
895
|
console.log(yellow('\n Aborted. No files were changed.\n'));
|
|
361
896
|
rl.close();
|
|
362
897
|
return;
|
|
@@ -388,6 +923,8 @@ const main = async () => {
|
|
|
388
923
|
copyDir(path.join(TEMPLATES, 'shared'), path.join(ROOT, 'shared'));
|
|
389
924
|
if (backendType === 'separate') {
|
|
390
925
|
copyDir(path.join(TEMPLATES, 'backend'), path.join(ROOT, 'backend'));
|
|
926
|
+
// Ensure backend/ is tracked by git even before the API agent scaffolds
|
|
927
|
+
fs.writeFileSync(path.join(ROOT, 'backend', '.gitkeep'), '', 'utf8');
|
|
391
928
|
}
|
|
392
929
|
fs.copyFileSync(path.join(TEMPLATES, 'CLAUDE.md'), path.join(ROOT, 'CLAUDE.md'));
|
|
393
930
|
fs.copyFileSync(path.join(TEMPLATES, 'CONTRACTS.md'), path.join(ROOT, 'CONTRACTS.md'));
|
|
@@ -413,22 +950,24 @@ const main = async () => {
|
|
|
413
950
|
console.log(` ${green('✓')} CLAUDE.md configured`);
|
|
414
951
|
|
|
415
952
|
writeConfig(path.join(ROOT, 'client', 'CLAUDE.md'), {
|
|
416
|
-
PROJECT_NAME:
|
|
417
|
-
FRAMEWORK:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
953
|
+
PROJECT_NAME: projectName,
|
|
954
|
+
FRAMEWORK: clientFw.value,
|
|
955
|
+
FRAMEWORK_VERSION: clientFwVersion || '',
|
|
956
|
+
LANGUAGE: clientLang,
|
|
957
|
+
STATE: clientState,
|
|
958
|
+
UI_LIBRARY: clientUi,
|
|
959
|
+
STYLING: clientStyle,
|
|
422
960
|
});
|
|
423
961
|
console.log(` ${green('✓')} client/CLAUDE.md configured`);
|
|
424
962
|
|
|
425
963
|
if (backendType === 'separate') {
|
|
426
964
|
writeConfig(path.join(ROOT, 'backend', 'CLAUDE.md'), {
|
|
427
|
-
PROJECT_NAME:
|
|
428
|
-
FRAMEWORK:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
965
|
+
PROJECT_NAME: projectName,
|
|
966
|
+
FRAMEWORK: backendFw,
|
|
967
|
+
FRAMEWORK_VERSION: backendFwObj?.version || '',
|
|
968
|
+
LANGUAGE: backendLang,
|
|
969
|
+
ORM: backendOrm,
|
|
970
|
+
AUTH: backendAuth,
|
|
432
971
|
});
|
|
433
972
|
console.log(` ${green('✓')} backend/CLAUDE.md configured`);
|
|
434
973
|
}
|
|
@@ -437,12 +976,33 @@ const main = async () => {
|
|
|
437
976
|
ensureGitignore('.agents-core/');
|
|
438
977
|
ensureGitignore('.scaffold/');
|
|
439
978
|
ensureGitignore('.workflow/');
|
|
979
|
+
|
|
980
|
+
// Remove template-specific gitignore entries so generated files can be committed
|
|
981
|
+
const gitignorePath = path.join(ROOT, '.gitignore');
|
|
982
|
+
let gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
983
|
+
['client/', 'backend/', 'shared/', 'CLAUDE.md', 'CONTRACTS.md', 'BUILD_STATE.md'].forEach(entry => {
|
|
984
|
+
gitignoreContent = gitignoreContent.replace(`\n${entry}`, '');
|
|
985
|
+
gitignoreContent = gitignoreContent.replace(`${entry}\n`, '');
|
|
986
|
+
gitignoreContent = gitignoreContent.replace(entry, '');
|
|
987
|
+
});
|
|
988
|
+
fs.writeFileSync(gitignorePath, gitignoreContent.trim() + '\n', 'utf8');
|
|
440
989
|
console.log(` ${green('✓')} .gitignore updated`);
|
|
441
990
|
|
|
442
991
|
// ── Write .config.json ───────────────────────────────────────────────────────
|
|
443
992
|
|
|
444
993
|
const config = {
|
|
445
994
|
projectName,
|
|
995
|
+
ide: {
|
|
996
|
+
name: ideChoice.name,
|
|
997
|
+
strategy: ideChoice.strategy,
|
|
998
|
+
cmd: ideChoice.cmd || null,
|
|
999
|
+
app: ideChoice.mac?.app || null,
|
|
1000
|
+
openArgs: process.platform === 'darwin' ? (ideChoice.mac?.args || [])
|
|
1001
|
+
: process.platform === 'win32' ? (ideChoice.win?.args || [])
|
|
1002
|
+
: (ideChoice.linux?.args || []),
|
|
1003
|
+
winPaths: (ideChoice.win?.paths || []).map(expandWinPath),
|
|
1004
|
+
linuxPaths: ideChoice.linux?.paths || [],
|
|
1005
|
+
},
|
|
446
1006
|
client: {
|
|
447
1007
|
framework: clientFw.value,
|
|
448
1008
|
language: clientLang,
|
|
@@ -542,6 +1102,67 @@ If a dependency is not met:
|
|
|
542
1102
|
fs.writeFileSync(path.join(ROOT, 'BUILD_STATE.md'), buildState, 'utf8');
|
|
543
1103
|
console.log(` ${green('✓')} BUILD_STATE.md generated`);
|
|
544
1104
|
|
|
1105
|
+
// ── Generate user project package.json ───────────────────────────────────────
|
|
1106
|
+
|
|
1107
|
+
const userPackage = {
|
|
1108
|
+
name: projectName.toLowerCase().replace(/\s+/g, '-'),
|
|
1109
|
+
version: '1.0.0',
|
|
1110
|
+
private: true,
|
|
1111
|
+
dependencies: {
|
|
1112
|
+
prompts: '^2.4.2',
|
|
1113
|
+
},
|
|
1114
|
+
scripts: {
|
|
1115
|
+
launch: 'cd "$(git rev-parse --git-common-dir)/.." && node .workflow/launch.js',
|
|
1116
|
+
complete: 'cd "$(git rev-parse --git-common-dir)/.." && node .workflow/complete.js',
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
1119
|
+
fs.writeFileSync(path.join(ROOT, 'package.json'), JSON.stringify(userPackage, null, 2), 'utf8');
|
|
1120
|
+
console.log(` ${green('✓')} package.json generated`);
|
|
1121
|
+
|
|
1122
|
+
// ── Install dependencies ──────────────────────────────────────────────────────
|
|
1123
|
+
|
|
1124
|
+
try {
|
|
1125
|
+
console.log(dim(' Installing dependencies...'));
|
|
1126
|
+
execSync('npm install', { cwd: ROOT, stdio: 'pipe' });
|
|
1127
|
+
console.log(` ${green('✓')} Dependencies installed`);
|
|
1128
|
+
} catch {
|
|
1129
|
+
console.log(yellow(' ⚠ npm install failed — run npm install manually before launching'));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ── Tracking ──────────────────────────────────────────────────────────────────
|
|
1133
|
+
|
|
1134
|
+
const trackingPath = path.join(RUNTIME_DIR, '.tracking.json');
|
|
1135
|
+
if (!fs.existsSync(trackingPath)) {
|
|
1136
|
+
const trackingStructure = generateTrackingStructure(config);
|
|
1137
|
+
fs.writeFileSync(trackingPath, JSON.stringify(trackingStructure, null, 2), 'utf8');
|
|
1138
|
+
console.log(` ${green('✓')} .tracking.json generated`);
|
|
1139
|
+
} else {
|
|
1140
|
+
console.log(dim(' ℹ .tracking.json already exists — preserved'));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// ── Generate .paths.json ──────────────────────────────────────────────────────
|
|
1144
|
+
|
|
1145
|
+
const pathsMap = {};
|
|
1146
|
+
const clientConventions = FRAMEWORK_CONVENTIONS.client[clientFw?.value] || {};
|
|
1147
|
+
const backendConventions = FRAMEWORK_CONVENTIONS.backend[backendFwObj?.value] || {};
|
|
1148
|
+
|
|
1149
|
+
if (Object.keys(clientConventions).length) {
|
|
1150
|
+
pathsMap.client = {};
|
|
1151
|
+
Object.entries(clientConventions).forEach(([key, value]) => {
|
|
1152
|
+
pathsMap.client[key] = { expected: value, current: null, status: 'pending' };
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (Object.keys(backendConventions).length) {
|
|
1157
|
+
pathsMap.backend = {};
|
|
1158
|
+
Object.entries(backendConventions).forEach(([key, value]) => {
|
|
1159
|
+
pathsMap.backend[key] = { expected: value, current: null, status: 'pending' };
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
fs.writeFileSync(path.join(RUNTIME_DIR, '.paths.json'), JSON.stringify(pathsMap, null, 2), 'utf8');
|
|
1164
|
+
console.log(` ${green('✓')} .paths.json generated`);
|
|
1165
|
+
|
|
545
1166
|
// ── Lock ─────────────────────────────────────────────────────────────────────
|
|
546
1167
|
|
|
547
1168
|
fs.writeFileSync(LOCK_FILE, new Date().toISOString());
|
|
@@ -558,33 +1179,180 @@ If a dependency is not met:
|
|
|
558
1179
|
console.log(dim(' git add . && git commit -m "init: project configuration"'));
|
|
559
1180
|
}
|
|
560
1181
|
|
|
561
|
-
// ──
|
|
1182
|
+
// ── Pre-commit hook — block direct commits to main ───────────────────────────
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
const hooksDir = path.join(ROOT, '.git', 'hooks');
|
|
1186
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
1187
|
+
const hookScript = `#!/bin/sh
|
|
1188
|
+
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
1189
|
+
if [ "$branch" = "main" ]; then
|
|
1190
|
+
echo ""
|
|
1191
|
+
echo " ⚠ Direct commits to main are not allowed."
|
|
1192
|
+
echo " Use npm run launch to start a task."
|
|
1193
|
+
echo ""
|
|
1194
|
+
exit 1
|
|
1195
|
+
fi
|
|
1196
|
+
`;
|
|
1197
|
+
if (!fs.existsSync(hookPath)) {
|
|
1198
|
+
fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
|
|
1199
|
+
console.log(dim(' ℹ Pre-commit hook installed — direct main commits blocked'));
|
|
1200
|
+
}
|
|
1201
|
+
} catch { /* best-effort */ }
|
|
1202
|
+
|
|
1203
|
+
// ── Remote setup ─────────────────────────────────────────────────────────────
|
|
1204
|
+
|
|
1205
|
+
setupUserRemote(ROOT, projectName);
|
|
1206
|
+
|
|
1207
|
+
// ── Trajectory selection ─────────────────────────────────────────────────────
|
|
562
1208
|
|
|
563
1209
|
separator();
|
|
564
1210
|
console.log(`\n${bold(green(' Project initialized successfully!'))}\n`);
|
|
1211
|
+
console.log(` ${bold('How do you want to build?')}\n`);
|
|
1212
|
+
|
|
1213
|
+
console.log(` ${dim('1.')} ${bold('Multi-Agent Driven Orchestration')}`);
|
|
1214
|
+
console.log(`${dim(' · Every task should start with npm run launch')}`);
|
|
1215
|
+
console.log(`${dim(' · Each agent runs in its own git worktree — an isolated branch')}`);
|
|
1216
|
+
console.log(`${dim(' and folder that merges back into main via npm run complete')}`);
|
|
1217
|
+
console.log(`${dim(' · Faster builds and lower token spend than a single long session')}`);
|
|
1218
|
+
console.log(`${yellow(' ⚠ If you commit directly to main yourself, you bypass the framework')}`);
|
|
1219
|
+
console.log(`${yellow(' and break task tracking for any active agent branches')}\n`);
|
|
1220
|
+
|
|
1221
|
+
console.log(` ${dim('2.')} ${bold('Shared Orchestration')}`);
|
|
1222
|
+
console.log(`${dim(' · You and agents co-build — each owning a defined part of the codebase')}`);
|
|
1223
|
+
console.log(`${dim(' · Agent tasks run in git worktrees; your work happens directly in the project')}`);
|
|
1224
|
+
console.log(`${dim(' · Agent tasks are token-efficient; your tasks cost only what you prompt')}`);
|
|
1225
|
+
console.log(`${dim(' · Define boundaries before work begins — agents for well-scoped work,')}`);
|
|
1226
|
+
console.log(`${dim(' you for areas where requirements are still evolving')}`);
|
|
1227
|
+
console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
|
|
1228
|
+
|
|
1229
|
+
const TRAJECTORY_DETAILS = {
|
|
1230
|
+
'1': {
|
|
1231
|
+
label: 'Multi-Agent Driven Orchestration',
|
|
1232
|
+
full: [
|
|
1233
|
+
'Every task must start with npm run launch.',
|
|
1234
|
+
'Agent sessions load only task-relevant context, enabling reliable',
|
|
1235
|
+
'chaining, predictable behavior, and efficient token usage.',
|
|
1236
|
+
'',
|
|
1237
|
+
'⚠ If you commit directly to main yourself, you bypass the framework',
|
|
1238
|
+
' and break task tracking for any active agent branches.',
|
|
1239
|
+
'',
|
|
1240
|
+
'Benefits',
|
|
1241
|
+
'· Scoped context per task',
|
|
1242
|
+
'· Predictable token consumption',
|
|
1243
|
+
'· Lower cost than maintaining large, persistent sessions',
|
|
1244
|
+
'· Better isolation between parallel work streams',
|
|
1245
|
+
],
|
|
1246
|
+
next: 'launch',
|
|
1247
|
+
},
|
|
1248
|
+
'2': {
|
|
1249
|
+
label: 'Shared Orchestration',
|
|
1250
|
+
full: [
|
|
1251
|
+
'You and agents work in the same codebase, each with clearly',
|
|
1252
|
+
'defined ownership. File boundaries must be established before',
|
|
1253
|
+
'work begins and remain fixed throughout the task.',
|
|
1254
|
+
'Agents excel when scope is well-defined;',
|
|
1255
|
+
'you excel when requirements are evolving.',
|
|
1256
|
+
'',
|
|
1257
|
+
'Use agents for',
|
|
1258
|
+
'· Multi-file features',
|
|
1259
|
+
'· Structured implementation work',
|
|
1260
|
+
'· Domain-specific tasks',
|
|
1261
|
+
'· Changes expected to exceed ~200 lines',
|
|
1262
|
+
'',
|
|
1263
|
+
'Handle manually',
|
|
1264
|
+
'· Targeted bug fixes',
|
|
1265
|
+
'· Configuration changes',
|
|
1266
|
+
'· Small refactors',
|
|
1267
|
+
'· Single-file edits under ~50 lines',
|
|
1268
|
+
'',
|
|
1269
|
+
'⚠ Avoid overlapping file ownership. Working on the same files',
|
|
1270
|
+
' as an active agent will create merge conflicts when merged.',
|
|
1271
|
+
'⚠ If you are spending time repeatedly clarifying scope, stop',
|
|
1272
|
+
' and do the task yourself. The coordination cost often',
|
|
1273
|
+
' exceeds the implementation cost.',
|
|
1274
|
+
'',
|
|
1275
|
+
'Benefits',
|
|
1276
|
+
'· Maximum agent efficiency for well-defined work',
|
|
1277
|
+
'· Human flexibility where requirements change',
|
|
1278
|
+
'· Scales well across large projects',
|
|
1279
|
+
'· Most adaptable workflow — requires the most discipline',
|
|
1280
|
+
],
|
|
1281
|
+
next: 'launch',
|
|
1282
|
+
},
|
|
1283
|
+
};
|
|
565
1284
|
|
|
566
|
-
|
|
567
|
-
|
|
1285
|
+
// Wrap in loop to support back navigation
|
|
1286
|
+
let trajectory = null;
|
|
1287
|
+
trajectoryLoop: while (true) {
|
|
1288
|
+
const trajIdx = await arrowSelect('How do you want to build?', [
|
|
1289
|
+
{ label: bold('Multi-Agent Driven Orchestration') },
|
|
1290
|
+
{ label: bold('Shared Orchestration') },
|
|
1291
|
+
], rl);
|
|
1292
|
+
trajectory = String(trajIdx + 1);
|
|
568
1293
|
|
|
569
|
-
|
|
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`);
|
|
1294
|
+
const selected = TRAJECTORY_DETAILS[trajectory];
|
|
581
1295
|
separator();
|
|
1296
|
+
console.log(`\n ${green('✓')} ${bold(selected.label)}\n`);
|
|
1297
|
+
renderTrajectoryLines(selected.full);
|
|
582
1298
|
console.log('');
|
|
583
|
-
|
|
1299
|
+
|
|
1300
|
+
const confirmIdx = await arrowSelect('Confirm?', [
|
|
1301
|
+
{ label: `${green('✓')} Confirm` },
|
|
1302
|
+
{ label: `${yellow('←')} Back — pick differently` },
|
|
1303
|
+
], rl);
|
|
1304
|
+
if (confirmIdx === 0) break trajectoryLoop;
|
|
1305
|
+
trajectory = null;
|
|
1306
|
+
separator();
|
|
1307
|
+
console.log(`\n ${bold('How do you want to build?')}\n`);
|
|
1308
|
+
console.log(` ${dim('1.')} ${bold('Multi-Agent Driven Orchestration')}`);
|
|
1309
|
+
console.log(`${dim(' · Every task should start with npm run launch')}`);
|
|
1310
|
+
console.log(`${dim(' · Each agent runs in its own git worktree — an isolated branch')}`);
|
|
1311
|
+
console.log(`${dim(' and folder that merges back into main via npm run complete')}`);
|
|
1312
|
+
console.log(`${dim(' · Faster builds and lower token spend than a single long session')}`);
|
|
1313
|
+
console.log(`${yellow(' ⚠ If you commit directly to main yourself, you bypass the framework')}`);
|
|
1314
|
+
console.log(`${yellow(' and break task tracking for any active agent branches')}\n`);
|
|
1315
|
+
console.log(` ${dim('2.')} ${bold('Shared Orchestration')}`);
|
|
1316
|
+
console.log(`${dim(' · You and agents co-build — each owning a defined part of the codebase')}`);
|
|
1317
|
+
console.log(`${dim(' · Agent tasks run in git worktrees; your work happens directly in the project')}`);
|
|
1318
|
+
console.log(`${dim(' · Agent tasks are token-efficient; your tasks cost only what you prompt')}`);
|
|
1319
|
+
console.log(`${dim(' · Define boundaries before work begins — agents for well-scoped work,')}`);
|
|
1320
|
+
console.log(`${dim(' you for areas where requirements are still evolving')}`);
|
|
1321
|
+
console.log(`${yellow(' ⚠ If you and an agent touch the same file, expect merge conflicts')}\n`);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const selected = TRAJECTORY_DETAILS[trajectory];
|
|
1325
|
+
|
|
1326
|
+
// Store trajectory in config
|
|
1327
|
+
try {
|
|
1328
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(RUNTIME_DIR, '.config.json'), 'utf8'));
|
|
1329
|
+
cfg.trajectory = selected.label.toLowerCase().replace(/ /g, '-');
|
|
1330
|
+
fs.writeFileSync(path.join(RUNTIME_DIR, '.config.json'), JSON.stringify(cfg, null, 2), 'utf8');
|
|
1331
|
+
} catch { /* best-effort */ }
|
|
1332
|
+
|
|
1333
|
+
if (selected.next === 'launch') {
|
|
1334
|
+
const launchConfirm = await arrowConfirm('Ready to launch your first task?', rl);
|
|
1335
|
+
if (launchConfirm) {
|
|
1336
|
+
rl.close();
|
|
1337
|
+
console.log('');
|
|
1338
|
+
const child = spawn('node', [path.join(ROOT, '.workflow', 'launch.js')], {
|
|
1339
|
+
stdio: 'inherit',
|
|
1340
|
+
cwd: ROOT,
|
|
1341
|
+
});
|
|
1342
|
+
child.on('exit', (code) => process.exit(code));
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
584
1345
|
}
|
|
1346
|
+
|
|
1347
|
+
console.log('');
|
|
1348
|
+
console.log(` ${bold('When ready, run:')}`);
|
|
1349
|
+
console.log(` ${cyan('npm run launch')}\n`);
|
|
1350
|
+
separator();
|
|
1351
|
+
console.log('');
|
|
1352
|
+
rl.close();
|
|
585
1353
|
};
|
|
586
1354
|
|
|
587
1355
|
main().catch((err) => {
|
|
588
1356
|
console.error('\n Error:', err.message);
|
|
589
1357
|
process.exit(1);
|
|
590
|
-
});
|
|
1358
|
+
});
|
package/package.json
CHANGED