@synergenius/flow-weaver 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/compile.js +158 -149
- package/dist/cli/commands/dev.js +14 -11
- package/dist/cli/commands/diff.js +6 -1
- package/dist/cli/commands/doctor.js +16 -25
- package/dist/cli/commands/export.js +2 -0
- package/dist/cli/commands/grammar.js +3 -1
- package/dist/cli/commands/init-personas.d.ts +87 -0
- package/dist/cli/commands/init-personas.js +492 -0
- package/dist/cli/commands/init.d.ts +30 -2
- package/dist/cli/commands/init.js +309 -94
- package/dist/cli/commands/market.js +14 -6
- package/dist/cli/commands/mcp-setup.d.ts +17 -0
- package/dist/cli/commands/mcp-setup.js +44 -0
- package/dist/cli/commands/migrate.js +3 -2
- package/dist/cli/commands/openapi.js +4 -1
- package/dist/cli/commands/run.js +44 -16
- package/dist/cli/commands/status.js +5 -3
- package/dist/cli/commands/strip.js +3 -1
- package/dist/cli/commands/templates.js +1 -1
- package/dist/cli/commands/validate.js +27 -24
- package/dist/cli/env-setup.d.ts +5 -0
- package/dist/cli/env-setup.js +12 -0
- package/dist/cli/flow-weaver.mjs +2973 -2295
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +259 -508
- package/dist/cli/utils/logger.d.ts +14 -0
- package/dist/cli/utils/logger.js +96 -16
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/dist/mcp/workflow-executor.js +7 -1
- package/package.json +2 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
/**
|
|
3
|
-
* Init command — scaffolds a new flow-weaver project interactively
|
|
3
|
+
* Init command — scaffolds a new flow-weaver project interactively.
|
|
4
|
+
* Supports persona-aware onboarding for nocode, low-code, vibe-coder, and expert users.
|
|
4
5
|
*/
|
|
5
6
|
import * as fs from 'fs';
|
|
6
7
|
import * as path from 'path';
|
|
7
|
-
import { execSync } from 'child_process';
|
|
8
|
+
import { execSync, spawn } from 'child_process';
|
|
8
9
|
import input from '@inquirer/input';
|
|
9
10
|
import select, { Separator } from '@inquirer/select';
|
|
10
11
|
import confirm from '@inquirer/confirm';
|
|
@@ -12,6 +13,8 @@ import { ExitPromptError } from '@inquirer/core';
|
|
|
12
13
|
import { getWorkflowTemplate, workflowTemplates } from '../templates/index.js';
|
|
13
14
|
import { logger } from '../utils/logger.js';
|
|
14
15
|
import { compileCommand } from './compile.js';
|
|
16
|
+
import { runMcpSetupFromInit, CLI_TOOL_BINARY } from './mcp-setup.js';
|
|
17
|
+
import { PERSONA_CHOICES, PERSONA_CONFIRMATIONS, USE_CASE_CHOICES, selectTemplateForPersona, getTemplateSubChoices, printNextSteps, generateReadme, generateExampleWorkflow, generateAgentPrompt, generateEditorPrompt, generateSetupPromptFile, printCopyablePrompt, AGENT_LAUNCH_DEFAULTS, } from './init-personas.js';
|
|
15
18
|
// ── Utilities ────────────────────────────────────────────────────────────────
|
|
16
19
|
const PROJECT_NAME_RE = /^[a-zA-Z0-9][-a-zA-Z0-9_.]*$/;
|
|
17
20
|
export function validateProjectName(name) {
|
|
@@ -35,11 +38,14 @@ export function isNonInteractive() {
|
|
|
35
38
|
return !process.stdin.isTTY;
|
|
36
39
|
}
|
|
37
40
|
const VALID_TEMPLATES = workflowTemplates.map((t) => t.id);
|
|
41
|
+
const VALID_PERSONAS = ['nocode', 'vibecoder', 'lowcode', 'expert'];
|
|
42
|
+
const VALID_USE_CASES = ['data', 'ai', 'api', 'automation', 'cicd', 'minimal'];
|
|
38
43
|
// ── Config resolution (prompts) ──────────────────────────────────────────────
|
|
39
44
|
export async function resolveInitConfig(dirArg, options) {
|
|
40
45
|
const skipPrompts = options.yes || isNonInteractive();
|
|
41
46
|
const force = options.force ?? false;
|
|
42
|
-
|
|
47
|
+
const hasExplicitTemplate = !!options.template;
|
|
48
|
+
// 1. Project name (unchanged)
|
|
43
49
|
let projectName;
|
|
44
50
|
if (options.name) {
|
|
45
51
|
projectName = options.name;
|
|
@@ -61,72 +67,151 @@ export async function resolveInitConfig(dirArg, options) {
|
|
|
61
67
|
if (valid !== true) {
|
|
62
68
|
throw new Error(valid);
|
|
63
69
|
}
|
|
64
|
-
// Target directory
|
|
65
70
|
const targetDir = path.resolve(dirArg ?? projectName);
|
|
66
|
-
// 2.
|
|
71
|
+
// 2. Persona
|
|
72
|
+
let persona;
|
|
73
|
+
if (options.preset) {
|
|
74
|
+
if (!VALID_PERSONAS.includes(options.preset)) {
|
|
75
|
+
throw new Error(`Unknown preset "${options.preset}". Available: ${VALID_PERSONAS.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
persona = options.preset;
|
|
78
|
+
}
|
|
79
|
+
else if (skipPrompts || hasExplicitTemplate) {
|
|
80
|
+
persona = 'expert';
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
persona = await select({
|
|
84
|
+
message: 'How do you plan to build?',
|
|
85
|
+
choices: PERSONA_CHOICES,
|
|
86
|
+
default: 'vibecoder',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Print persona confirmation (interactive only)
|
|
90
|
+
if (!skipPrompts) {
|
|
91
|
+
const confirmation = PERSONA_CONFIRMATIONS[persona];
|
|
92
|
+
if (confirmation) {
|
|
93
|
+
logger.log(` ${logger.dim(confirmation)}`);
|
|
94
|
+
logger.newline();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 3. Template selection (persona-dependent)
|
|
67
98
|
let template;
|
|
68
|
-
|
|
99
|
+
let useCase;
|
|
100
|
+
if (hasExplicitTemplate) {
|
|
101
|
+
// Direct --template flag bypasses everything
|
|
69
102
|
template = options.template;
|
|
70
103
|
if (!VALID_TEMPLATES.includes(template)) {
|
|
71
104
|
throw new Error(`Unknown template "${template}". Available: ${VALID_TEMPLATES.join(', ')}`);
|
|
72
105
|
}
|
|
73
106
|
}
|
|
74
|
-
else if (
|
|
75
|
-
|
|
107
|
+
else if (persona === 'expert') {
|
|
108
|
+
// Expert: show today's flat template list
|
|
109
|
+
if (skipPrompts) {
|
|
110
|
+
template = 'sequential';
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
template = await select({
|
|
114
|
+
message: 'Workflow template:',
|
|
115
|
+
choices: [
|
|
116
|
+
new Separator('── Data Processing ──'),
|
|
117
|
+
{ value: 'sequential', name: 'sequential', description: 'Linear pipeline' },
|
|
118
|
+
{ value: 'foreach', name: 'foreach', description: 'Batch iteration' },
|
|
119
|
+
{ value: 'aggregator', name: 'aggregator', description: 'Collect and aggregate results' },
|
|
120
|
+
new Separator('── Automation ──'),
|
|
121
|
+
{ value: 'conditional', name: 'conditional', description: 'Route by condition' },
|
|
122
|
+
new Separator('── AI ──'),
|
|
123
|
+
{ value: 'ai-agent', name: 'ai-agent', description: 'LLM agent with tool calling' },
|
|
124
|
+
{ value: 'ai-react', name: 'ai-react', description: 'ReAct pattern' },
|
|
125
|
+
{ value: 'ai-rag', name: 'ai-rag', description: 'Retrieval-Augmented Generation' },
|
|
126
|
+
{ value: 'ai-chat', name: 'ai-chat', description: 'Conversational AI' },
|
|
127
|
+
new Separator('── Durable ──'),
|
|
128
|
+
{ value: 'ai-agent-durable', name: 'ai-agent-durable', description: 'Durable agent with approval gate' },
|
|
129
|
+
{ value: 'ai-pipeline-durable', name: 'ai-pipeline-durable', description: 'Durable data pipeline' },
|
|
130
|
+
new Separator('── Integration ──'),
|
|
131
|
+
{ value: 'webhook', name: 'webhook', description: 'HTTP webhook handler' },
|
|
132
|
+
new Separator('── Utility ──'),
|
|
133
|
+
{ value: 'error-handler', name: 'error-handler', description: 'Error handling and recovery' },
|
|
134
|
+
new Separator('── CI/CD ──'),
|
|
135
|
+
{ value: 'cicd-test-deploy', name: 'cicd-test-deploy', description: 'Test and deploy pipeline' },
|
|
136
|
+
{ value: 'cicd-docker', name: 'cicd-docker', description: 'Docker build and push' },
|
|
137
|
+
{ value: 'cicd-multi-env', name: 'cicd-multi-env', description: 'Multi-environment deploy' },
|
|
138
|
+
{ value: 'cicd-matrix', name: 'cicd-matrix', description: 'Matrix build strategy' },
|
|
139
|
+
],
|
|
140
|
+
default: 'sequential',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
76
143
|
}
|
|
77
144
|
else {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
new
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
145
|
+
// Non-expert: use-case categories
|
|
146
|
+
if (options.useCase) {
|
|
147
|
+
if (!VALID_USE_CASES.includes(options.useCase)) {
|
|
148
|
+
throw new Error(`Unknown use case "${options.useCase}". Available: ${VALID_USE_CASES.join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
useCase = options.useCase;
|
|
151
|
+
}
|
|
152
|
+
else if (skipPrompts) {
|
|
153
|
+
useCase = 'data';
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
useCase = await select({
|
|
157
|
+
message: 'What are you building?',
|
|
158
|
+
choices: USE_CASE_CHOICES,
|
|
159
|
+
default: 'data',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const selection = selectTemplateForPersona(persona, useCase);
|
|
163
|
+
if (selection.choices && !skipPrompts) {
|
|
164
|
+
// Lowcode with multiple choices: show sub-select
|
|
165
|
+
template = await select({
|
|
166
|
+
message: 'Pick a template:',
|
|
167
|
+
choices: getTemplateSubChoices(selection.choices),
|
|
168
|
+
default: selection.template,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
template = selection.template;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 4. MCP setup (nocode + vibecoder only)
|
|
176
|
+
let mcp;
|
|
177
|
+
if (options.mcp !== undefined) {
|
|
178
|
+
mcp = options.mcp;
|
|
179
|
+
}
|
|
180
|
+
else if (skipPrompts) {
|
|
181
|
+
mcp = false;
|
|
182
|
+
}
|
|
183
|
+
else if (persona === 'nocode' || persona === 'vibecoder') {
|
|
184
|
+
mcp = await confirm({
|
|
185
|
+
message: 'Set up AI editor integration? (Claude Code, Cursor, VS Code, etc.)',
|
|
186
|
+
default: true,
|
|
105
187
|
});
|
|
106
188
|
}
|
|
107
|
-
|
|
189
|
+
else {
|
|
190
|
+
mcp = false;
|
|
191
|
+
}
|
|
192
|
+
// 5. Install deps (expert: prompt, others: auto-yes)
|
|
108
193
|
let installDeps;
|
|
109
194
|
if (options.install !== undefined) {
|
|
110
195
|
installDeps = options.install;
|
|
111
196
|
}
|
|
112
|
-
else if (skipPrompts) {
|
|
197
|
+
else if (skipPrompts || persona !== 'expert') {
|
|
113
198
|
installDeps = true;
|
|
114
199
|
}
|
|
115
200
|
else {
|
|
116
201
|
installDeps = await confirm({ message: 'Install dependencies (npm install)?', default: true });
|
|
117
202
|
}
|
|
118
|
-
//
|
|
203
|
+
// 6. Git init (expert: prompt, others: auto-yes)
|
|
119
204
|
let gitInit;
|
|
120
205
|
if (options.git !== undefined) {
|
|
121
206
|
gitInit = options.git;
|
|
122
207
|
}
|
|
123
|
-
else if (skipPrompts) {
|
|
208
|
+
else if (skipPrompts || persona !== 'expert') {
|
|
124
209
|
gitInit = true;
|
|
125
210
|
}
|
|
126
211
|
else {
|
|
127
212
|
gitInit = await confirm({ message: 'Initialize a git repository?', default: true });
|
|
128
213
|
}
|
|
129
|
-
//
|
|
214
|
+
// 7. Module format (expert only)
|
|
130
215
|
let format;
|
|
131
216
|
if (options.format) {
|
|
132
217
|
format = options.format;
|
|
@@ -134,23 +219,15 @@ export async function resolveInitConfig(dirArg, options) {
|
|
|
134
219
|
throw new Error(`Invalid format "${format}". Use "esm" or "cjs".`);
|
|
135
220
|
}
|
|
136
221
|
}
|
|
137
|
-
else if (skipPrompts) {
|
|
222
|
+
else if (skipPrompts || persona !== 'expert') {
|
|
138
223
|
format = 'esm';
|
|
139
224
|
}
|
|
140
225
|
else {
|
|
141
226
|
format = await select({
|
|
142
227
|
message: 'Module format:',
|
|
143
228
|
choices: [
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
name: 'ESM (Recommended)',
|
|
147
|
-
description: 'ECMAScript modules (import/export)',
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
value: 'cjs',
|
|
151
|
-
name: 'CommonJS',
|
|
152
|
-
description: 'CommonJS modules (require/module.exports)',
|
|
153
|
-
},
|
|
229
|
+
{ value: 'esm', name: 'ESM (Recommended)', description: 'ECMAScript modules (import/export)' },
|
|
230
|
+
{ value: 'cjs', name: 'CommonJS', description: 'CommonJS modules (require/module.exports)' },
|
|
154
231
|
],
|
|
155
232
|
default: 'esm',
|
|
156
233
|
});
|
|
@@ -163,10 +240,13 @@ export async function resolveInitConfig(dirArg, options) {
|
|
|
163
240
|
install: installDeps,
|
|
164
241
|
git: gitInit,
|
|
165
242
|
force,
|
|
243
|
+
persona,
|
|
244
|
+
useCase,
|
|
245
|
+
mcp,
|
|
166
246
|
};
|
|
167
247
|
}
|
|
168
248
|
// ── Pure file generation ─────────────────────────────────────────────────────
|
|
169
|
-
export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
249
|
+
export function generateProjectFiles(projectName, template, format = 'esm', persona = 'expert') {
|
|
170
250
|
const workflowName = toWorkflowName(projectName);
|
|
171
251
|
const workflowFile = `${projectName}-workflow.ts`;
|
|
172
252
|
const tmpl = getWorkflowTemplate(template);
|
|
@@ -174,17 +254,22 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
174
254
|
throw new Error(`Unknown template "${template}"`);
|
|
175
255
|
}
|
|
176
256
|
const workflowCode = tmpl.generate({ workflowName });
|
|
177
|
-
// Package.json
|
|
257
|
+
// Package.json
|
|
258
|
+
const scripts = {
|
|
259
|
+
dev: `npx flow-weaver compile src/${workflowFile} -o src && npx tsx src/main.ts`,
|
|
260
|
+
start: 'npx tsx src/main.ts',
|
|
261
|
+
compile: `npx flow-weaver compile src/${workflowFile} -o src`,
|
|
262
|
+
validate: `npx flow-weaver validate src/${workflowFile}`,
|
|
263
|
+
doctor: 'npx flow-weaver doctor',
|
|
264
|
+
};
|
|
265
|
+
// Add diagram script for non-expert personas
|
|
266
|
+
if (persona !== 'expert') {
|
|
267
|
+
scripts.diagram = `npx flow-weaver diagram src/${workflowFile} --format ascii-compact`;
|
|
268
|
+
}
|
|
178
269
|
const packageJsonContent = {
|
|
179
270
|
name: projectName,
|
|
180
271
|
version: '1.0.0',
|
|
181
|
-
scripts
|
|
182
|
-
dev: `npx flow-weaver compile src/${workflowFile} -o src && npx tsx src/main.ts`,
|
|
183
|
-
start: 'npx tsx src/main.ts',
|
|
184
|
-
compile: `npx flow-weaver compile src/${workflowFile} -o src`,
|
|
185
|
-
validate: `npx flow-weaver validate src/${workflowFile}`,
|
|
186
|
-
doctor: 'npx flow-weaver doctor',
|
|
187
|
-
},
|
|
272
|
+
scripts,
|
|
188
273
|
dependencies: {
|
|
189
274
|
'@synergenius/flow-weaver': 'latest',
|
|
190
275
|
},
|
|
@@ -194,13 +279,11 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
194
279
|
tsx: '^4.21.0',
|
|
195
280
|
},
|
|
196
281
|
};
|
|
197
|
-
// ESM projects need "type": "module"
|
|
198
282
|
if (format === 'esm') {
|
|
199
283
|
packageJsonContent.type = 'module';
|
|
200
284
|
}
|
|
201
|
-
// CJS projects can optionally have "type": "commonjs" but it's the default
|
|
202
285
|
const packageJson = JSON.stringify(packageJsonContent, null, 2);
|
|
203
|
-
// tsconfig.json
|
|
286
|
+
// tsconfig.json
|
|
204
287
|
const tsconfigContent = {
|
|
205
288
|
compilerOptions: {
|
|
206
289
|
target: 'ES2020',
|
|
@@ -216,7 +299,7 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
216
299
|
include: ['src'],
|
|
217
300
|
};
|
|
218
301
|
const tsconfigJson = JSON.stringify(tsconfigContent, null, 2);
|
|
219
|
-
// main.ts
|
|
302
|
+
// main.ts
|
|
220
303
|
const workflowJsFile = workflowFile.replace(/\.ts$/, '.js');
|
|
221
304
|
let mainTs;
|
|
222
305
|
if (format === 'esm') {
|
|
@@ -246,7 +329,6 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
246
329
|
].join('\n');
|
|
247
330
|
}
|
|
248
331
|
else {
|
|
249
|
-
// CommonJS format
|
|
250
332
|
mainTs = [
|
|
251
333
|
'/**',
|
|
252
334
|
` * ${projectName} — workflow runner`,
|
|
@@ -274,7 +356,7 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
274
356
|
}
|
|
275
357
|
const gitignore = `node_modules/\ndist/\n.tsbuildinfo\n`;
|
|
276
358
|
const configYaml = `defaultFileType: ts\n`;
|
|
277
|
-
|
|
359
|
+
const files = {
|
|
278
360
|
'package.json': packageJson,
|
|
279
361
|
'tsconfig.json': tsconfigJson,
|
|
280
362
|
[`src/${workflowFile}`]: workflowCode,
|
|
@@ -282,6 +364,13 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
282
364
|
'.gitignore': gitignore,
|
|
283
365
|
'.flowweaver/config.yaml': configYaml,
|
|
284
366
|
};
|
|
367
|
+
// Add README for all personas
|
|
368
|
+
files['README.md'] = generateReadme(projectName, persona, template);
|
|
369
|
+
// Add example workflow for lowcode persona
|
|
370
|
+
if (persona === 'lowcode') {
|
|
371
|
+
files['examples/example-workflow.ts'] = generateExampleWorkflow(projectName);
|
|
372
|
+
}
|
|
373
|
+
return files;
|
|
285
374
|
}
|
|
286
375
|
// ── Filesystem writer ────────────────────────────────────────────────────────
|
|
287
376
|
export function scaffoldProject(targetDir, files, options) {
|
|
@@ -321,6 +410,74 @@ export function runGitInit(targetDir) {
|
|
|
321
410
|
return { success: false, error: message };
|
|
322
411
|
}
|
|
323
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* After init + MCP setup, offer to launch a CLI agent or generate a prompt for GUI editors.
|
|
415
|
+
* Returns true if a CLI agent was spawned (init should exit and let the agent take over).
|
|
416
|
+
*/
|
|
417
|
+
export async function handleAgentHandoff(opts) {
|
|
418
|
+
const { projectName, persona, template, targetDir, cliTools, guiTools, filesCreated } = opts;
|
|
419
|
+
// Step 1: If CLI agent available, offer to launch it
|
|
420
|
+
if (cliTools.length > 0) {
|
|
421
|
+
const toolId = cliTools[0]; // Prefer first (claude > codex)
|
|
422
|
+
const binary = CLI_TOOL_BINARY[toolId];
|
|
423
|
+
const displayName = toolId === 'claude' ? 'Claude Code' : 'Codex';
|
|
424
|
+
const launchDefault = AGENT_LAUNCH_DEFAULTS[persona];
|
|
425
|
+
const shouldLaunch = await confirm({
|
|
426
|
+
message: `Launch ${displayName} to set up your project?`,
|
|
427
|
+
default: launchDefault,
|
|
428
|
+
});
|
|
429
|
+
if (shouldLaunch && binary) {
|
|
430
|
+
const prompt = generateAgentPrompt(projectName, persona, template);
|
|
431
|
+
logger.newline();
|
|
432
|
+
logger.log(` ${logger.dim(`Starting ${displayName}...`)}`);
|
|
433
|
+
logger.newline();
|
|
434
|
+
const child = spawn(binary, [prompt], {
|
|
435
|
+
cwd: targetDir,
|
|
436
|
+
stdio: 'inherit',
|
|
437
|
+
env: { ...process.env },
|
|
438
|
+
});
|
|
439
|
+
child.on('error', (err) => {
|
|
440
|
+
logger.error(`Failed to start ${displayName}: ${err.message}`);
|
|
441
|
+
});
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Step 2: If GUI editors configured (or user declined CLI), offer prompt options
|
|
446
|
+
if (guiTools.length > 0 || cliTools.length > 0) {
|
|
447
|
+
const promptAction = await select({
|
|
448
|
+
message: 'Generate a setup prompt for your editor?',
|
|
449
|
+
choices: [
|
|
450
|
+
{ value: 'terminal', name: 'Print to terminal', description: 'Copy and paste into your editor' },
|
|
451
|
+
{ value: 'file', name: 'Save as file', description: 'Write PROJECT_SETUP.md to your project' },
|
|
452
|
+
{ value: 'both', name: 'Both' },
|
|
453
|
+
{ value: 'skip', name: 'Skip' },
|
|
454
|
+
],
|
|
455
|
+
default: 'terminal',
|
|
456
|
+
});
|
|
457
|
+
if (promptAction === 'skip')
|
|
458
|
+
return false;
|
|
459
|
+
const editorPrompt = generateEditorPrompt(projectName, persona, template);
|
|
460
|
+
if (promptAction === 'terminal' || promptAction === 'both') {
|
|
461
|
+
printCopyablePrompt(editorPrompt);
|
|
462
|
+
}
|
|
463
|
+
if (promptAction === 'file' || promptAction === 'both') {
|
|
464
|
+
const setupContent = generateSetupPromptFile(projectName, persona, template, filesCreated);
|
|
465
|
+
const setupPath = path.join(targetDir, 'PROJECT_SETUP.md');
|
|
466
|
+
fs.writeFileSync(setupPath, setupContent, 'utf8');
|
|
467
|
+
// Add to .gitignore
|
|
468
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
469
|
+
if (fs.existsSync(gitignorePath)) {
|
|
470
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
471
|
+
if (!existing.includes('PROJECT_SETUP.md')) {
|
|
472
|
+
fs.appendFileSync(gitignorePath, 'PROJECT_SETUP.md\n', 'utf8');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
logger.newline();
|
|
476
|
+
logger.success(`Wrote ${logger.highlight('PROJECT_SETUP.md')} ${logger.dim('(delete after first setup)')}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
324
481
|
// ── CLI entrypoint ───────────────────────────────────────────────────────────
|
|
325
482
|
export async function initCommand(dirArg, options) {
|
|
326
483
|
try {
|
|
@@ -331,34 +488,32 @@ export async function initCommand(dirArg, options) {
|
|
|
331
488
|
throw new Error(`${config.targetDir} already contains a package.json. Use --force to overwrite.`);
|
|
332
489
|
}
|
|
333
490
|
// Generate and scaffold
|
|
334
|
-
const files = generateProjectFiles(config.projectName, config.template, config.format);
|
|
491
|
+
const files = generateProjectFiles(config.projectName, config.template, config.format, config.persona);
|
|
335
492
|
const { filesCreated, filesSkipped } = scaffoldProject(config.targetDir, files, {
|
|
336
493
|
force: config.force,
|
|
337
494
|
});
|
|
338
495
|
// Post-scaffold actions
|
|
339
496
|
let installResult;
|
|
340
497
|
if (config.install) {
|
|
341
|
-
|
|
342
|
-
logger.info('Installing dependencies...');
|
|
343
|
-
}
|
|
498
|
+
const spinner = !options.json ? logger.spinner('Installing dependencies...') : null;
|
|
344
499
|
installResult = runNpmInstall(config.targetDir);
|
|
500
|
+
if (spinner) {
|
|
501
|
+
if (installResult.success)
|
|
502
|
+
spinner.stop('Dependencies installed');
|
|
503
|
+
else
|
|
504
|
+
spinner.fail(`npm install failed: ${installResult.error}`);
|
|
505
|
+
}
|
|
345
506
|
}
|
|
346
507
|
let gitResult;
|
|
347
508
|
if (config.git) {
|
|
348
|
-
if (!options.json) {
|
|
349
|
-
logger.info('Initializing git repository...');
|
|
350
|
-
}
|
|
351
509
|
gitResult = runGitInit(config.targetDir);
|
|
352
510
|
}
|
|
353
511
|
// Auto-compile the workflow so `npm start` works immediately
|
|
354
512
|
const workflowFile = `${config.projectName}-workflow.ts`;
|
|
355
513
|
const workflowPath = path.join(config.targetDir, 'src', workflowFile);
|
|
356
|
-
// Auto-compile the workflow so `npm start` works immediately.
|
|
357
|
-
// Skip in JSON mode since compileCommand writes to logger.
|
|
358
514
|
let compileResult;
|
|
359
515
|
if (!options.json && fs.existsSync(workflowPath)) {
|
|
360
516
|
try {
|
|
361
|
-
logger.info('Compiling workflow...');
|
|
362
517
|
await compileCommand(workflowPath, { format: config.format });
|
|
363
518
|
compileResult = { success: true };
|
|
364
519
|
}
|
|
@@ -367,6 +522,36 @@ export async function initCommand(dirArg, options) {
|
|
|
367
522
|
compileResult = { success: false, error: message };
|
|
368
523
|
}
|
|
369
524
|
}
|
|
525
|
+
// MCP setup
|
|
526
|
+
let mcpConfigured;
|
|
527
|
+
let mcpResult;
|
|
528
|
+
if (config.mcp && !options.json) {
|
|
529
|
+
const spinner = logger.spinner('Configuring AI editors...');
|
|
530
|
+
try {
|
|
531
|
+
mcpResult = await runMcpSetupFromInit();
|
|
532
|
+
mcpConfigured = mcpResult.configured;
|
|
533
|
+
if (mcpResult.configured.length > 0) {
|
|
534
|
+
spinner.stop(`${mcpResult.configured.join(', ')} configured`);
|
|
535
|
+
}
|
|
536
|
+
else if (mcpResult.failed.length > 0) {
|
|
537
|
+
spinner.fail('MCP setup failed');
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
spinner.stop('No AI editors detected');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
spinner.fail('MCP setup failed');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Agent handoff
|
|
548
|
+
let agentLaunched = false;
|
|
549
|
+
let mcpCliTools = [];
|
|
550
|
+
let mcpGuiTools = [];
|
|
551
|
+
if (mcpResult) {
|
|
552
|
+
mcpCliTools = mcpResult.cliTools;
|
|
553
|
+
mcpGuiTools = mcpResult.guiTools;
|
|
554
|
+
}
|
|
370
555
|
// Build report
|
|
371
556
|
const report = {
|
|
372
557
|
projectDir: config.targetDir,
|
|
@@ -374,20 +559,21 @@ export async function initCommand(dirArg, options) {
|
|
|
374
559
|
filesSkipped,
|
|
375
560
|
template: config.template,
|
|
376
561
|
format: config.format,
|
|
562
|
+
persona: config.persona,
|
|
377
563
|
installResult,
|
|
378
564
|
gitResult,
|
|
565
|
+
mcpConfigured,
|
|
566
|
+
agentLaunched: false,
|
|
379
567
|
};
|
|
380
568
|
if (options.json) {
|
|
381
569
|
console.log(JSON.stringify(report, null, 2));
|
|
382
570
|
return;
|
|
383
571
|
}
|
|
384
|
-
// Human output
|
|
385
|
-
logger.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
for (const file of filesSkipped) {
|
|
390
|
-
logger.warn(`Skipped ${file} (already exists)`);
|
|
572
|
+
// Human output — status lines
|
|
573
|
+
logger.newline();
|
|
574
|
+
logger.success(`Created ${logger.highlight(config.projectName)} ${logger.dim(`(${config.template}, ${config.format.toUpperCase()})`)}`);
|
|
575
|
+
if (filesSkipped.length > 0) {
|
|
576
|
+
logger.warn(`Skipped ${filesSkipped.length} existing file(s)`);
|
|
391
577
|
}
|
|
392
578
|
if (installResult) {
|
|
393
579
|
if (installResult.success) {
|
|
@@ -399,7 +585,7 @@ export async function initCommand(dirArg, options) {
|
|
|
399
585
|
}
|
|
400
586
|
if (gitResult) {
|
|
401
587
|
if (gitResult.success) {
|
|
402
|
-
logger.success('Git
|
|
588
|
+
logger.success('Git initialized');
|
|
403
589
|
}
|
|
404
590
|
else {
|
|
405
591
|
logger.warn(`git init failed: ${gitResult.error}`);
|
|
@@ -407,24 +593,53 @@ export async function initCommand(dirArg, options) {
|
|
|
407
593
|
}
|
|
408
594
|
if (compileResult) {
|
|
409
595
|
if (compileResult.success) {
|
|
410
|
-
logger.success('Workflow compiled
|
|
596
|
+
logger.success('Workflow compiled');
|
|
411
597
|
}
|
|
412
598
|
else {
|
|
413
|
-
logger.warn(`
|
|
599
|
+
logger.warn(`Compile failed: ${compileResult.error}`);
|
|
414
600
|
}
|
|
415
601
|
}
|
|
416
|
-
|
|
417
|
-
|
|
602
|
+
// Read the workflow code for preview
|
|
603
|
+
const workflowCode = files[`src/${workflowFile}`] ?? null;
|
|
604
|
+
// Persona-specific rich output
|
|
418
605
|
const relDir = path.relative(process.cwd(), config.targetDir);
|
|
419
606
|
const displayDir = !relDir || relDir === '.' ? null : relDir.startsWith('../../') ? config.targetDir : relDir;
|
|
420
|
-
|
|
421
|
-
|
|
607
|
+
// Agent handoff: offer to launch CLI agent or generate prompt for GUI editors
|
|
608
|
+
const skipAgent = options.agent === false || options.yes || isNonInteractive();
|
|
609
|
+
if (mcpConfigured && mcpConfigured.length > 0 && !skipAgent) {
|
|
610
|
+
try {
|
|
611
|
+
agentLaunched = await handleAgentHandoff({
|
|
612
|
+
projectName: config.projectName,
|
|
613
|
+
persona: config.persona,
|
|
614
|
+
template: config.template,
|
|
615
|
+
targetDir: config.targetDir,
|
|
616
|
+
cliTools: mcpCliTools,
|
|
617
|
+
guiTools: mcpGuiTools,
|
|
618
|
+
filesCreated,
|
|
619
|
+
});
|
|
620
|
+
report.agentLaunched = agentLaunched;
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
if (err instanceof ExitPromptError)
|
|
624
|
+
return;
|
|
625
|
+
// Non-fatal: just skip agent handoff
|
|
626
|
+
}
|
|
422
627
|
}
|
|
423
|
-
|
|
424
|
-
|
|
628
|
+
// If an agent was spawned, it takes over. Print minimal output.
|
|
629
|
+
if (agentLaunched) {
|
|
630
|
+
return;
|
|
425
631
|
}
|
|
426
|
-
|
|
427
|
-
|
|
632
|
+
printNextSteps({
|
|
633
|
+
projectName: config.projectName,
|
|
634
|
+
persona: config.persona,
|
|
635
|
+
template: config.template,
|
|
636
|
+
displayDir,
|
|
637
|
+
installSkipped: !config.install,
|
|
638
|
+
workflowCode,
|
|
639
|
+
workflowFile,
|
|
640
|
+
mcpConfigured,
|
|
641
|
+
agentLaunched,
|
|
642
|
+
});
|
|
428
643
|
}
|
|
429
644
|
catch (err) {
|
|
430
645
|
// Clean exit on Ctrl+C during prompts
|
|
@@ -191,8 +191,9 @@ export async function marketPackCommand(directory, options = {}) {
|
|
|
191
191
|
validation,
|
|
192
192
|
parsedFiles: parsedFiles.length,
|
|
193
193
|
}, null, 2));
|
|
194
|
-
if (!validation.valid)
|
|
195
|
-
process.
|
|
194
|
+
if (!validation.valid) {
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
}
|
|
196
197
|
return;
|
|
197
198
|
}
|
|
198
199
|
// Display results
|
|
@@ -277,7 +278,8 @@ export async function marketInstallCommand(packageSpec, options = {}) {
|
|
|
277
278
|
else {
|
|
278
279
|
logger.error(`npm install failed: ${getErrorMessage(err)}`);
|
|
279
280
|
}
|
|
280
|
-
process.
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
return;
|
|
281
283
|
}
|
|
282
284
|
// 2. Read manifest from installed package
|
|
283
285
|
// Resolve the package name from the spec (could be a tarball path or name@version)
|
|
@@ -316,13 +318,18 @@ export async function marketSearchCommand(query, options = {}) {
|
|
|
316
318
|
logger.newline();
|
|
317
319
|
}
|
|
318
320
|
try {
|
|
319
|
-
|
|
321
|
+
let results = await searchPackages({ query, limit, registryUrl: registry });
|
|
322
|
+
// Client-side filtering: npm search may return broad results, narrow to query match
|
|
323
|
+
if (query) {
|
|
324
|
+
const q = query.toLowerCase();
|
|
325
|
+
results = results.filter((pkg) => pkg.name.toLowerCase().includes(q) || (pkg.description && pkg.description.toLowerCase().includes(q)));
|
|
326
|
+
}
|
|
320
327
|
if (json) {
|
|
321
328
|
console.log(JSON.stringify(results, null, 2));
|
|
322
329
|
return;
|
|
323
330
|
}
|
|
324
331
|
if (results.length === 0) {
|
|
325
|
-
logger.info('No packages found');
|
|
332
|
+
logger.info(query ? `No packages matching "${query}"` : 'No packages found');
|
|
326
333
|
return;
|
|
327
334
|
}
|
|
328
335
|
for (const pkg of results) {
|
|
@@ -342,7 +349,8 @@ export async function marketSearchCommand(query, options = {}) {
|
|
|
342
349
|
else {
|
|
343
350
|
logger.error(`Search failed: ${getErrorMessage(err)}`);
|
|
344
351
|
}
|
|
345
|
-
process.
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
return;
|
|
346
354
|
}
|
|
347
355
|
}
|
|
348
356
|
/**
|