@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.
- package/README.ko.md +52 -6
- package/README.md +52 -6
- package/dist/cli.d.ts +12 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +69 -33
- package/dist/commands/generate.d.ts +11 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +71 -2
- package/dist/commands/inspect.d.ts +20 -0
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +149 -31
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +42 -0
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +21 -1
- package/dist/generator-types.d.ts +15 -1
- package/dist/generator-types.d.ts.map +1 -1
- package/dist/generators/manifest.d.ts +267 -0
- package/dist/generators/manifest.d.ts.map +1 -1
- package/dist/generators/manifest.js +86 -1
- package/dist/new/prompt.d.ts +1 -0
- package/dist/new/prompt.d.ts.map +1 -1
- package/dist/new/prompt.js +7 -1
- package/dist/new/scaffold.d.ts.map +1 -1
- package/dist/new/scaffold.js +17 -1
- package/dist/registry.d.ts +4 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +13 -5
- package/dist/update-check.d.ts +50 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +356 -0
- package/package.json +9 -1
|
@@ -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 =
|
|
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":"
|
|
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"}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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 --
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/dist/commands/migrate.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/commands/new.js
CHANGED
|
@@ -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(
|
|
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
|
|
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"}
|