@fluojs/cli 1.0.0-beta.1 → 1.0.0-beta.2

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.
@@ -10,6 +10,53 @@ function writeFileIfChanged(filePath, content) {
10
10
  writeFileSync(filePath, content, 'utf8');
11
11
  return true;
12
12
  }
13
+
14
+ /** Describes how one generated artifact would interact with the workspace. */
15
+
16
+ /** One path-level action reported by generate dry-run previews and structured results. */
17
+
18
+ function planFileWrite(filePath, content, options) {
19
+ if (!existsSync(filePath)) {
20
+ return {
21
+ action: 'create',
22
+ path: filePath
23
+ };
24
+ }
25
+ if (!options.force) {
26
+ return {
27
+ action: 'skip',
28
+ path: filePath
29
+ };
30
+ }
31
+ if (readFileSync(filePath, 'utf8') === content) {
32
+ return {
33
+ action: 'unchanged',
34
+ path: filePath
35
+ };
36
+ }
37
+ return {
38
+ action: 'overwrite',
39
+ path: filePath
40
+ };
41
+ }
42
+ function planModuleWrite(modulePath, content) {
43
+ if (!existsSync(modulePath)) {
44
+ return {
45
+ action: 'module-create',
46
+ path: modulePath
47
+ };
48
+ }
49
+ if (readFileSync(modulePath, 'utf8') === content) {
50
+ return {
51
+ action: 'module-unchanged',
52
+ path: modulePath
53
+ };
54
+ }
55
+ return {
56
+ action: 'module-update',
57
+ path: modulePath
58
+ };
59
+ }
13
60
  function createGeneratorOptions(kind, domainDirectory, kebab, options) {
14
61
  return {
15
62
  ...options,
@@ -17,6 +64,15 @@ function createGeneratorOptions(kind, domainDirectory, kebab, options) {
17
64
  hasService: options.hasService ?? (kind === 'controller' ? existsSync(join(domainDirectory, `${kebab}.service.ts`)) : undefined)
18
65
  };
19
66
  }
67
+ function resolveDomainDirectory(kind, resolvedBase, kebab, options) {
68
+ if (kind === 'request-dto' && options.targetFeature !== undefined) {
69
+ const normalizedFeature = options.targetFeature.trim();
70
+ const featureKebab = assertValidResourceName(normalizedFeature);
71
+ const featureDirectory = /^[A-Z]/u.test(normalizedFeature) ? toPlural(featureKebab) : featureKebab;
72
+ return join(resolvedBase, featureDirectory);
73
+ }
74
+ return join(resolvedBase, toPlural(kebab));
75
+ }
20
76
  function assertValidResourceName(name) {
21
77
  const kebab = toKebabCase(name);
22
78
  if (name.trim().length === 0) {
@@ -88,7 +144,7 @@ function prepareModuleUpdate(domainDirectory, normalizedName, kind, classSuffix,
88
144
  * @param kind Generator kind to execute.
89
145
  * @param name Resource name supplied by the caller before normalization.
90
146
  * @param baseDirectory Source directory that should receive the generated domain folder.
91
- * @param options Optional generation flags that control overwrites and sibling-aware templates.
147
+ * @param options Optional generation flags that control overwrites, request DTO feature placement, and sibling-aware templates.
92
148
  * @returns Structured file and wiring metadata for the completed generation run.
93
149
  * @throws {Error} When the resource name is invalid, the generator kind is unknown, or the target module source cannot be updated safely.
94
150
  */
@@ -97,11 +153,23 @@ export function runGenerateCommand(kind, name, baseDirectory, options = {}) {
97
153
  const kebab = assertValidResourceName(normalizedName);
98
154
  const generator = findGeneratorDefinition(kind);
99
155
  const resolvedBase = resolve(baseDirectory);
100
- const domainDirectory = join(resolvedBase, toPlural(kebab));
156
+ const domainDirectory = resolveDomainDirectory(kind, resolvedBase, kebab, options);
101
157
  const generatorOptions = createGeneratorOptions(kind, domainDirectory, kebab, options);
102
158
  const files = generator.factory(normalizedName, generatorOptions);
103
159
  const moduleRegistration = 'moduleRegistration' in generator ? generator.moduleRegistration : undefined;
104
160
  const moduleUpdate = moduleRegistration ? prepareModuleUpdate(domainDirectory, normalizedName, kind, moduleRegistration.classSuffix, moduleRegistration.arrayKey) : undefined;
161
+ const plannedFiles = files.map(file => planFileWrite(join(domainDirectory, file.path), file.content, options));
162
+ const modulePlan = moduleUpdate ? planModuleWrite(moduleUpdate.modulePath, moduleUpdate.source) : undefined;
163
+ if (options.dryRun) {
164
+ return {
165
+ generatedFiles: [],
166
+ moduleRegistered: moduleUpdate !== undefined,
167
+ modulePath: moduleUpdate?.modulePath,
168
+ nextStepHint: generator.nextStepHint,
169
+ plannedFiles: modulePlan ? [...plannedFiles, modulePlan] : plannedFiles,
170
+ wiringBehavior: generator.wiringBehavior
171
+ };
172
+ }
105
173
  mkdirSync(domainDirectory, {
106
174
  recursive: true
107
175
  });
@@ -129,6 +197,7 @@ export function runGenerateCommand(kind, name, baseDirectory, options = {}) {
129
197
  moduleRegistered: moduleRegistered,
130
198
  modulePath: resolvedModulePath,
131
199
  nextStepHint: generator.nextStepHint,
200
+ plannedFiles: modulePlan ? [...plannedFiles, modulePlan] : plannedFiles,
132
201
  wiringBehavior: generator.wiringBehavior
133
202
  };
134
203
  }
@@ -1,14 +1,34 @@
1
+ import { type PlatformShellSnapshot } from '@fluojs/runtime';
1
2
  type CliStream = {
2
3
  write(message: string): unknown;
3
4
  };
5
+ type InspectPrompter = {
6
+ close?(): void;
7
+ confirm(message: string, defaultValue: boolean): Promise<boolean>;
8
+ };
9
+ type ReadableStream = {
10
+ isTTY?: boolean;
11
+ };
12
+ type StudioMermaidRenderer = (snapshot: PlatformShellSnapshot) => string;
13
+ type StudioMermaidRendererLoader = (cwd: string) => Promise<StudioMermaidRenderer | undefined>;
4
14
  /**
5
15
  * Runtime options for the inspect command when used programmatically.
6
16
  */
7
17
  export interface InspectCommandRuntimeOptions {
18
+ /** Whether the caller is running under CI/non-interactive automation. */
19
+ ci?: boolean;
8
20
  /** Current working directory for module resolution. */
9
21
  cwd?: string;
22
+ /** Force or disable interactive prompts for optional Studio guidance. */
23
+ interactive?: boolean;
24
+ /** Optional test/editor hook for resolving Studio's Mermaid renderer. */
25
+ loadStudioMermaidRenderer?: StudioMermaidRendererLoader;
26
+ /** Custom prompt implementation used only when Studio is missing for Mermaid output. */
27
+ prompt?: InspectPrompter;
10
28
  /** Custom stream for error output. */
11
29
  stderr?: CliStream;
30
+ /** Custom stream for terminal detection. */
31
+ stdin?: ReadableStream;
12
32
  /** Custom stream for standard output. */
13
33
  stdout?: CliStream;
14
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAcA,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yCAAyC;IACzC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAgDD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAarC;AA8ID;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CAkEnH"}
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,qBAAqB,EAE3B,MAAM,iBAAiB,CAAC;AAIzB,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,KAAK,MAAM,CAAC;AAEzE,KAAK,2BAA2B,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAAC;AAE/F;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,yEAAyE;IACzE,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,yBAAyB,CAAC,EAAE,2BAA2B,CAAC;IACxD,wFAAwF;IACxF,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,yCAAyC;IACzC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAkFD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAarC;AA+PD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEnH"}
@@ -1,5 +1,8 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
1
4
  import { pathToFileURL } from 'node:url';
2
- import { resolve } from 'node:path';
5
+ import * as clack from '@clack/prompts';
3
6
  import { FluoFactory, PLATFORM_SHELL } from '@fluojs/runtime';
4
7
  import { renderAliasList, renderHelpTable } from '../help.js';
5
8
 
@@ -13,12 +16,20 @@ const INSPECT_OPTION_HELP = [{
13
16
  option: '--json'
14
17
  }, {
15
18
  aliases: [],
16
- description: 'Emit platform component dependency chains as a Mermaid diagram.',
19
+ description: 'Emit a Mermaid graph through the optional @fluojs/studio rendering contract.',
17
20
  option: '--mermaid'
18
21
  }, {
19
22
  aliases: [],
20
23
  description: 'Bootstrap the application context and emit versioned timing diagnostics.',
21
24
  option: '--timing'
25
+ }, {
26
+ aliases: [],
27
+ description: 'Emit a CI-friendly JSON report with summary, snapshot, diagnostics, and timing.',
28
+ option: '--report'
29
+ }, {
30
+ aliases: [],
31
+ description: 'Write the selected inspect payload to a file instead of stdout.',
32
+ option: '--output <path>'
22
33
  }, {
23
34
  aliases: [],
24
35
  description: 'Select the exported module symbol name (default: AppModule).',
@@ -28,6 +39,8 @@ const INSPECT_OPTION_HELP = [{
28
39
  description: 'Show help for the inspect command.',
29
40
  option: '--help'
30
41
  }];
42
+ const STUDIO_CONTRACT_ENTRYPOINT = '@fluojs/studio/contracts';
43
+ const STUDIO_MISSING_MESSAGE = ['Mermaid graph rendering is owned by @fluojs/studio, but @fluojs/studio is not resolvable from this project.', 'Install @fluojs/studio explicitly (for example: pnpm add -D @fluojs/studio) and rerun fluo inspect --mermaid.'].join('\n');
31
44
  function isHelpFlag(value) {
32
45
  return value === '--help' || value === '-h';
33
46
  }
@@ -54,6 +67,8 @@ function parseInspectArgs(argv) {
54
67
  let exportName = 'AppModule';
55
68
  let json = false;
56
69
  let mermaid = false;
70
+ let outputPath;
71
+ let report = false;
57
72
  let timing = false;
58
73
  for (let index = 0; index < argv.length; index += 1) {
59
74
  const option = argv[index];
@@ -72,6 +87,19 @@ function parseInspectArgs(argv) {
72
87
  timing = true;
73
88
  continue;
74
89
  }
90
+ if (option === '--report') {
91
+ report = true;
92
+ continue;
93
+ }
94
+ if (option === '--output') {
95
+ const next = argv[index + 1];
96
+ if (!next || next.startsWith('-')) {
97
+ throw new Error('Expected --output to have a file path value.');
98
+ }
99
+ outputPath = next;
100
+ index += 1;
101
+ continue;
102
+ }
75
103
  if (option === '--export') {
76
104
  const next = argv[index + 1];
77
105
  if (!next || next.startsWith('-')) {
@@ -92,18 +120,23 @@ function parseInspectArgs(argv) {
92
120
  if (!modulePath) {
93
121
  throw new Error(inspectUsage());
94
122
  }
95
- if (!json && !mermaid && !timing) {
123
+ if (!json && !mermaid && !timing && !report) {
96
124
  json = true;
97
125
  }
98
- const selectedModes = [json, mermaid, timing].filter(Boolean).length;
126
+ const selectedModes = [json, mermaid, report].filter(Boolean).length;
99
127
  if (selectedModes > 1) {
100
- throw new Error('Choose only one inspect output mode: --json, --mermaid, or --timing.');
128
+ throw new Error('Choose only one inspect output mode: --json, --mermaid, or --report.');
129
+ }
130
+ if (mermaid && timing) {
131
+ throw new Error('Use --timing only with JSON inspect output or --report. Mermaid rendering remains delegated to @fluojs/studio.');
101
132
  }
102
133
  return {
103
134
  exportName,
104
135
  json,
105
136
  mermaid,
106
137
  modulePath,
138
+ outputPath,
139
+ report,
107
140
  timing
108
141
  };
109
142
  }
@@ -124,33 +157,111 @@ function stringifyTiming(timing) {
124
157
  function stringifySnapshot(snapshot) {
125
158
  return JSON.stringify(snapshot, null, 2);
126
159
  }
127
- function renderPlatformSnapshotMermaid(snapshot) {
128
- const lines = ['graph TD'];
129
- if (snapshot.components.length === 0) {
130
- lines.push(' EMPTY["No registered platform components"]');
131
- return lines.join('\n');
132
- }
133
- const nodeByComponentId = new Map();
134
- for (const [index, component] of snapshot.components.entries()) {
135
- const nodeId = `C${String(index + 1)}`;
136
- nodeByComponentId.set(component.id, nodeId);
137
- const summary = [component.id, `kind: ${component.kind}`, `state: ${component.state}`, `readiness: ${component.readiness.status}`, `health: ${component.health.status}`].join('\\n');
138
- lines.push(` ${nodeId}["${summary}"]`);
160
+ function createEmptyTimingDiagnostics() {
161
+ return {
162
+ phases: [],
163
+ totalMs: 0,
164
+ version: 1
165
+ };
166
+ }
167
+ function createInspectReport(snapshot, timing) {
168
+ const resolvedTiming = timing ?? createEmptyTimingDiagnostics();
169
+ const errorCount = snapshot.diagnostics.filter(diagnostic => diagnostic.severity === 'error').length;
170
+ const warningCount = snapshot.diagnostics.filter(diagnostic => diagnostic.severity === 'warning').length;
171
+ return {
172
+ generatedAt: snapshot.generatedAt,
173
+ snapshot,
174
+ summary: {
175
+ componentCount: snapshot.components.length,
176
+ diagnosticCount: snapshot.diagnostics.length,
177
+ errorCount,
178
+ healthStatus: snapshot.health.status,
179
+ readinessStatus: snapshot.readiness.status,
180
+ timingTotalMs: resolvedTiming.totalMs,
181
+ warningCount
182
+ },
183
+ timing: resolvedTiming,
184
+ version: 1
185
+ };
186
+ }
187
+ function stringifySnapshotWithTiming(snapshot, timing) {
188
+ return JSON.stringify({
189
+ snapshot,
190
+ timing: timing ?? createEmptyTimingDiagnostics()
191
+ }, null, 2);
192
+ }
193
+ async function emitInspectPayload(payload, parsed, cwd, stdout) {
194
+ if (!parsed.outputPath) {
195
+ stdout.write(`${payload}\n`);
196
+ return;
139
197
  }
140
- for (const component of snapshot.components) {
141
- const from = nodeByComponentId.get(component.id);
142
- if (!from) {
143
- continue;
198
+ const outputPath = resolve(cwd, parsed.outputPath);
199
+ await mkdir(dirname(outputPath), {
200
+ recursive: true
201
+ });
202
+ await writeFile(outputPath, `${payload}\n`, 'utf8');
203
+ }
204
+ function createInspectPrompter() {
205
+ return {
206
+ async confirm(message, defaultValue) {
207
+ const result = await clack.confirm({
208
+ initialValue: defaultValue,
209
+ message
210
+ });
211
+ if (clack.isCancel(result)) {
212
+ clack.cancel('Operation cancelled.');
213
+ process.exit(0);
214
+ }
215
+ return result;
144
216
  }
145
- for (const dependency of component.dependencies) {
146
- const to = nodeByComponentId.get(dependency);
147
- if (!to) {
148
- continue;
217
+ };
218
+ }
219
+ function shouldPromptForStudio(runtime) {
220
+ if (runtime.prompt !== undefined) {
221
+ return runtime.interactive ?? true;
222
+ }
223
+ if (runtime.ci === true) {
224
+ return false;
225
+ }
226
+ return runtime.stdout === undefined && runtime.stderr === undefined && (runtime.interactive ?? true) && Boolean(runtime.stdin?.isTTY ?? process.stdin.isTTY);
227
+ }
228
+ async function loadStudioMermaidRenderer(cwd) {
229
+ const resolvers = [createRequire(resolve(cwd, 'package.json')), createRequire(import.meta.url)];
230
+ for (const resolver of resolvers) {
231
+ try {
232
+ const resolvedEntrypoint = resolver.resolve(STUDIO_CONTRACT_ENTRYPOINT);
233
+ const importedContract = await import(pathToFileURL(resolvedEntrypoint).href);
234
+ if (typeof importedContract.renderMermaid !== 'function') {
235
+ throw new Error(`${STUDIO_CONTRACT_ENTRYPOINT} does not export renderMermaid(snapshot).`);
236
+ }
237
+ return importedContract.renderMermaid;
238
+ } catch (error) {
239
+ const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
240
+ if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
241
+ throw error;
149
242
  }
150
- lines.push(` ${from} --> ${to}`);
151
243
  }
152
244
  }
153
- return lines.join('\n');
245
+ return undefined;
246
+ }
247
+ async function resolveStudioMermaidRenderer(cwd, runtime) {
248
+ const renderer = await (runtime.loadStudioMermaidRenderer ?? loadStudioMermaidRenderer)(cwd);
249
+ if (renderer) {
250
+ return renderer;
251
+ }
252
+ if (!shouldPromptForStudio(runtime)) {
253
+ throw new Error(STUDIO_MISSING_MESSAGE);
254
+ }
255
+ const prompt = runtime.prompt ?? createInspectPrompter();
256
+ try {
257
+ const approvedInstall = await prompt.confirm('Install @fluojs/studio before rendering Mermaid output?', false);
258
+ if (!approvedInstall) {
259
+ throw new Error(`${STUDIO_MISSING_MESSAGE}\nInstallation declined; no package-manager command was run.`);
260
+ }
261
+ throw new Error(`${STUDIO_MISSING_MESSAGE}\nAutomatic installation is not run by fluo inspect. Install @fluojs/studio explicitly, then rerun the command.`);
262
+ } finally {
263
+ prompt.close?.();
264
+ }
154
265
  }
155
266
 
156
267
  /**
@@ -173,7 +284,7 @@ export async function runInspectCommand(argv, runtime = {}) {
173
284
  const modulePath = resolve(cwd, parsed.modulePath);
174
285
  const importedModule = await import(pathToFileURL(modulePath).href);
175
286
  const rootModule = resolveRootModule(importedModule[parsed.exportName], parsed.exportName);
176
- if (parsed.timing) {
287
+ if (parsed.timing && !parsed.json && !parsed.report) {
177
288
  const context = await FluoFactory.createApplicationContext(rootModule, {
178
289
  diagnostics: {
179
290
  timing: true
@@ -186,13 +297,16 @@ export async function runInspectCommand(argv, runtime = {}) {
186
297
  }
187
298
  });
188
299
  try {
189
- stdout.write(`${stringifyTiming(context.bootstrapTiming)}\n`);
300
+ await emitInspectPayload(stringifyTiming(context.bootstrapTiming), parsed, cwd, stdout);
190
301
  } finally {
191
302
  await context.close();
192
303
  }
193
304
  return 0;
194
305
  }
195
306
  const context = await FluoFactory.createApplicationContext(rootModule, {
307
+ diagnostics: parsed.timing || parsed.report ? {
308
+ timing: true
309
+ } : undefined,
196
310
  logger: {
197
311
  debug() {},
198
312
  error() {},
@@ -204,10 +318,14 @@ export async function runInspectCommand(argv, runtime = {}) {
204
318
  const platformShell = await context.get(PLATFORM_SHELL);
205
319
  const snapshot = await platformShell.snapshot();
206
320
  if (parsed.json) {
207
- stdout.write(`${stringifySnapshot(snapshot)}\n`);
321
+ await emitInspectPayload(parsed.timing ? stringifySnapshotWithTiming(snapshot, context.bootstrapTiming) : stringifySnapshot(snapshot), parsed, cwd, stdout);
322
+ }
323
+ if (parsed.report) {
324
+ await emitInspectPayload(JSON.stringify(createInspectReport(snapshot, context.bootstrapTiming), null, 2), parsed, cwd, stdout);
208
325
  }
209
326
  if (parsed.mermaid) {
210
- stdout.write(`${renderPlatformSnapshotMermaid(snapshot)}\n`);
327
+ const renderMermaid = await resolveStudioMermaidRenderer(cwd, runtime);
328
+ await emitInspectPayload(renderMermaid(snapshot), parsed, cwd, stdout);
211
329
  }
212
330
  } finally {
213
331
  await context.close();
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAYA,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yCAAyC;IACzC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AA6HD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAgBrC;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CAmEnH"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAaA,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yCAAyC;IACzC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AA4KD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAgBrC;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEnH"}
@@ -10,6 +10,10 @@ const MIGRATE_OPTION_HELP = [{
10
10
  aliases: ['-a'],
11
11
  description: 'Apply file changes. Dry-run is the default mode.',
12
12
  option: '--apply'
13
+ }, {
14
+ aliases: [],
15
+ description: 'Emit a machine-readable JSON migration report to stdout. Errors still go to stderr.',
16
+ option: '--json'
13
17
  }, {
14
18
  aliases: [],
15
19
  description: `Run only selected transforms. Available: ${MIGRATION_TRANSFORMS.join(', ')}.`,
@@ -40,6 +44,7 @@ function parseTransformList(rawValue, optionName) {
40
44
  function parseArgs(argv) {
41
45
  let pathArgument;
42
46
  let apply = false;
47
+ let json = false;
43
48
  let onlyTransforms;
44
49
  let skipTransforms = [];
45
50
  for (let index = 0; index < argv.length; index += 1) {
@@ -48,6 +53,13 @@ function parseArgs(argv) {
48
53
  apply = true;
49
54
  continue;
50
55
  }
56
+ if (arg === '--json') {
57
+ if (json) {
58
+ throw new Error('Duplicate --json option.');
59
+ }
60
+ json = true;
61
+ continue;
62
+ }
51
63
  if (arg === '--only' || arg === '--skip') {
52
64
  const rawValue = argv[index + 1];
53
65
  if (!rawValue || rawValue.startsWith('-')) {
@@ -85,10 +97,36 @@ function parseArgs(argv) {
85
97
  }
86
98
  return {
87
99
  apply,
100
+ json,
88
101
  path: pathArgument,
89
102
  transforms: enabled
90
103
  };
91
104
  }
105
+ function renderJsonReport(report, transforms) {
106
+ return `${JSON.stringify({
107
+ command: 'migrate',
108
+ mode: report.apply ? 'apply' : 'dry-run',
109
+ apply: report.apply,
110
+ dryRun: !report.apply,
111
+ transforms,
112
+ scannedFiles: report.scannedFiles,
113
+ changedFiles: report.changedFiles,
114
+ warningCount: report.warningCount,
115
+ files: report.fileResults.map(fileResult => ({
116
+ filePath: fileResult.filePath,
117
+ changed: fileResult.changed,
118
+ appliedTransforms: fileResult.appliedTransforms,
119
+ warningCount: fileResult.warnings.length,
120
+ warnings: fileResult.warnings.map(warning => ({
121
+ category: warning.category,
122
+ categoryLabel: getWarningCategoryLabel(warning.category),
123
+ filePath: warning.filePath,
124
+ line: warning.line,
125
+ message: warning.message
126
+ }))
127
+ }))
128
+ }, null, 2)}\n`;
129
+ }
92
130
 
93
131
  /**
94
132
  * Returns usage information for the migrate command.
@@ -131,6 +169,10 @@ export async function runMigrateCommand(argv, runtime = {}) {
131
169
  enabledTransforms: parsed.transforms,
132
170
  targetPath
133
171
  });
172
+ if (parsed.json) {
173
+ stdout.write(renderJsonReport(report, transforms));
174
+ return 0;
175
+ }
134
176
  stdout.write(`Mode: ${parsed.apply ? 'apply' : 'dry-run'}\n`);
135
177
  stdout.write(`Enabled transforms: ${renderTransformList(transforms)}\n`);
136
178
  stdout.write(`Scanned files: ${report.scannedFiles}\n`);
@@ -1 +1 @@
1
- {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AAMA,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAUnF,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE3E,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAuBF;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiSD;;;;GAIG;AACH,wBAAgB,QAAQ,IAAI,MAAM,CA0BjC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiG3G"}
1
+ {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AAMA,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWnF,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE3E,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAuBF;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0UD;;;;GAIG;AACH,wBAAgB,QAAQ,IAAI,MAAM,CA0BjC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,MAAM,CAAC,CAwG3G"}
@@ -3,6 +3,7 @@ import { spinner as clackSpinner, log as clackLog } from '@clack/prompts';
3
3
  import { renderAliasList, renderHelpTable } from '../help.js';
4
4
  import { installDependencies } from '../new/install.js';
5
5
  import { collectBootstrapAnswers } from '../new/prompt.js';
6
+ import { resolveBootstrapPlan } from '../new/resolver.js';
6
7
  import { scaffoldBootstrapApp } from '../new/scaffold.js';
7
8
  import { SUPPORTED_BOOTSTRAP_PLATFORMS, SUPPORTED_BOOTSTRAP_RUNTIMES, SUPPORTED_BOOTSTRAP_SHAPES, SUPPORTED_BOOTSTRAP_TOOLING_PRESETS, SUPPORTED_BOOTSTRAP_TOPOLOGY_MODES, SUPPORTED_BOOTSTRAP_TRANSPORTS } from '../new/starter-profiles.js';
8
9
  function shouldUseInteractiveShell(runtime) {
@@ -79,6 +80,10 @@ const NEW_OPTION_HELP = [{
79
80
  aliases: [],
80
81
  description: 'Skip git repository initialization in the generated starter.',
81
82
  option: '--no-git'
83
+ }, {
84
+ aliases: [],
85
+ description: 'Print the resolved scaffold plan without writing files, installing dependencies, or initializing git.',
86
+ option: '--print-plan'
82
87
  }, {
83
88
  aliases: ['-h'],
84
89
  description: 'Show help for the new command.',
@@ -163,7 +168,7 @@ function parseArgs(argv) {
163
168
  }
164
169
  parsed.platform = readOptionValue(argv, index, '--platform');
165
170
  if (!SUPPORTED_PLATFORMS.has(parsed.platform)) {
166
- throw new Error('Invalid --platform value "' + parsed.platform + '". Use one of: bun, cloudflare-workers, deno, fastify, express, nodejs, none.');
171
+ throw new Error(`Invalid --platform value "${parsed.platform}". Use one of: bun, cloudflare-workers, deno, fastify, express, nodejs, none.`);
167
172
  }
168
173
  index += 1;
169
174
  break;
@@ -204,6 +209,9 @@ function parseArgs(argv) {
204
209
  case '--force':
205
210
  parsed.force = true;
206
211
  break;
212
+ case '--print-plan':
213
+ parsed.printPlan = true;
214
+ break;
207
215
  case '--install':
208
216
  parsed.installDependencies = setBooleanSelection(parsed.installDependencies, true, '--install', '--no-install');
209
217
  break;
@@ -232,6 +240,13 @@ function parseArgs(argv) {
232
240
  }
233
241
  return parsed;
234
242
  }
243
+ function renderDependencyList(dependencies) {
244
+ return dependencies.length > 0 ? dependencies.join(', ') : '(none)';
245
+ }
246
+ function renderScaffoldPlanPreview(answers, resolvedTargetDirectory) {
247
+ const bootstrapPlan = resolveBootstrapPlan(answers);
248
+ return ['fluo new scaffold plan', '', `Project name: ${answers.projectName}`, `Target directory: ${answers.targetDirectory}`, `Resolved target: ${resolvedTargetDirectory}`, `Shape: ${answers.shape}`, `Runtime: ${answers.runtime}`, `Platform: ${answers.platform}`, `Transport: ${answers.transport}`, `Tooling preset: ${answers.tooling}`, `Topology: ${answers.topology.mode}${answers.topology.deferred ? ' (deferred)' : ''}`, `Starter recipe: ${bootstrapPlan.profile.id}`, `Emitter: ${bootstrapPlan.emitter.type}`, `Package manager: ${answers.packageManager}`, `Install dependencies: ${answers.installDependencies ? 'yes' : 'no'}`, `Initialize git: ${answers.initializeGit ? 'yes' : 'no'}`, '', 'Dependencies:', ` runtime: ${renderDependencyList(bootstrapPlan.dependencies.dependencies)}`, ` dev: ${renderDependencyList(bootstrapPlan.dependencies.devDependencies)}`, '', 'Side effects: none. Preview mode does not create files, install dependencies, or initialize git.'].join('\n');
249
+ }
235
250
 
236
251
  /**
237
252
  * Renders CLI help text for `fluo new`.
@@ -287,11 +302,16 @@ export async function runNewCommand(argv, runtime = {}) {
287
302
  }
288
303
  const answers = await collectBootstrapAnswers(partialAnswers, runtime.cwd ?? process.cwd(), runtime.userAgent, {
289
304
  interactive: runtime.interactive,
305
+ completionMessage: parsed.printPlan ? 'Scaffold plan resolved. No files were written.' : undefined,
290
306
  prompt: runtime.prompt,
291
307
  stdin: runtime.stdin,
292
308
  stdout
293
309
  });
294
310
  const targetDirectory = resolve(runtime.cwd ?? process.cwd(), answers.targetDirectory);
311
+ if (parsed.printPlan) {
312
+ stdout.write(`${renderScaffoldPlanPreview(answers, targetDirectory)}\n`);
313
+ return 0;
314
+ }
295
315
  const options = {
296
316
  ...answers,
297
317
  dependencySource: runtime.dependencySource,
@@ -3,11 +3,17 @@ export interface GeneratedFile {
3
3
  content: string;
4
4
  path: string;
5
5
  }
6
- /** Optional generation flags that influence overwrite behavior and sibling-aware templates. */
6
+ /** Optional generation flags that influence overwrite behavior, target placement, plan previews, and sibling-aware templates. */
7
7
  export interface GenerateOptions {
8
+ /** Preview planned writes and module updates without mutating the workspace. */
9
+ dryRun?: boolean;
8
10
  force?: boolean;
9
11
  hasRepo?: boolean;
10
12
  hasService?: boolean;
13
+ /**
14
+ * Feature or slice directory that should receive feature-local files such as request DTOs.
15
+ */
16
+ targetFeature?: string;
11
17
  }
12
18
  /**
13
19
  * Produces the in-memory files for one schematic/resource pair.
@@ -15,7 +21,15 @@ export interface GenerateOptions {
15
21
  export type GeneratorFactory = (name: string, options?: GenerateOptions) => GeneratedFile[];
16
22
  /** Registry shape used by generator manifests to bind a factory to CLI metadata. */
17
23
  export interface GeneratorRegistration {
24
+ collectionId?: string;
18
25
  factory: GeneratorFactory;
19
26
  description?: string;
20
27
  }
28
+ /** Describes a supported option for generator metadata, help output, and docs alignment tests. */
29
+ export interface GeneratorOptionSchema {
30
+ aliases: readonly string[];
31
+ description: string;
32
+ name: string;
33
+ value: 'boolean' | 'path';
34
+ }
21
35
  //# sourceMappingURL=generator-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generator-types.d.ts","sourceRoot":"","sources":["../src/generator-types.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,+FAA+F;AAC/F,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,aAAa,EAAE,CAAC;AAE5F,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
1
+ {"version":3,"file":"generator-types.d.ts","sourceRoot":"","sources":["../src/generator-types.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iIAAiI;AACjI,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,aAAa,EAAE,CAAC;AAE5F,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;CAC3B"}