@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.
@@ -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
- // 1. Project name
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. Template
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
- if (options.template) {
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 (skipPrompts) {
75
- template = 'sequential';
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
- template = await select({
79
- message: 'Workflow template:',
80
- choices: [
81
- new Separator('── Data Processing ──'),
82
- { value: 'sequential', name: 'sequential', description: 'Linear pipeline' },
83
- { value: 'foreach', name: 'foreach', description: 'Batch iteration' },
84
- { value: 'aggregator', name: 'aggregator', description: 'Collect and aggregate results' },
85
- new Separator('── Automation ──'),
86
- { value: 'conditional', name: 'conditional', description: 'Route by condition' },
87
- new Separator('── AI ──'),
88
- { value: 'ai-agent', name: 'ai-agent', description: 'LLM agent with tool calling' },
89
- { value: 'ai-react', name: 'ai-react', description: 'ReAct pattern' },
90
- { value: 'ai-rag', name: 'ai-rag', description: 'Retrieval-Augmented Generation' },
91
- { value: 'ai-chat', name: 'ai-chat', description: 'Conversational AI' },
92
- new Separator('── Durable ──'),
93
- { value: 'ai-agent-durable', name: 'ai-agent-durable', description: 'Durable agent with approval gate' },
94
- { value: 'ai-pipeline-durable', name: 'ai-pipeline-durable', description: 'Durable data pipeline' },
95
- new Separator('── Integration ──'),
96
- { value: 'webhook', name: 'webhook', description: 'HTTP webhook handler' },
97
- new Separator('── Utility ──'),
98
- {
99
- value: 'error-handler',
100
- name: 'error-handler',
101
- description: 'Error handling and recovery',
102
- },
103
- ],
104
- default: 'sequential',
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
- // 3. Install deps?
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
- // 4. Git init?
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
- // 5. Module format
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
- value: 'esm',
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 differs based on module format
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 differs based on module format
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 import syntax differs based on module format
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
- return {
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
- if (!options.json) {
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.section('Project created');
386
- for (const file of filesCreated) {
387
- logger.success(`Created ${file}`);
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 repository initialized');
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 (npm start works immediately)');
596
+ logger.success('Workflow compiled');
411
597
  }
412
598
  else {
413
- logger.warn(`Workflow compile failed: ${compileResult.error}`);
599
+ logger.warn(`Compile failed: ${compileResult.error}`);
414
600
  }
415
601
  }
416
- logger.newline();
417
- logger.section('Next steps');
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
- if (displayDir) {
421
- logger.log(` cd ${displayDir}`);
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
- if (!config.install) {
424
- logger.log(' npm install');
628
+ // If an agent was spawned, it takes over. Print minimal output.
629
+ if (agentLaunched) {
630
+ return;
425
631
  }
426
- logger.log(compileResult?.success ? ' npm start' : ' npm run dev');
427
- logger.newline();
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.exit(1);
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.exit(1);
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
- const results = await searchPackages({ query, limit, registryUrl: registry });
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.exit(1);
352
+ process.exitCode = 1;
353
+ return;
346
354
  }
347
355
  }
348
356
  /**