@fluojs/cli 1.1.0 → 3.0.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.
Files changed (137) hide show
  1. package/README.ko.md +183 -22
  2. package/README.md +186 -22
  3. package/dist/cli.d.ts +5 -4
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +128 -35
  6. package/dist/commands/diagnostics.d.ts +35 -0
  7. package/dist/commands/diagnostics.d.ts.map +1 -1
  8. package/dist/commands/diagnostics.js +60 -0
  9. package/dist/commands/generate.d.ts +1 -49
  10. package/dist/commands/generate.d.ts.map +1 -1
  11. package/dist/commands/generate.js +1 -214
  12. package/dist/commands/inspect.d.ts +5 -7
  13. package/dist/commands/inspect.d.ts.map +1 -1
  14. package/dist/commands/inspect.js +85 -65
  15. package/dist/commands/migrate.d.ts.map +1 -1
  16. package/dist/commands/migrate.js +27 -15
  17. package/dist/commands/migration-transform-tokens.d.ts +15 -0
  18. package/dist/commands/migration-transform-tokens.d.ts.map +1 -0
  19. package/dist/commands/migration-transform-tokens.js +41 -0
  20. package/dist/commands/new.d.ts +0 -6
  21. package/dist/commands/new.d.ts.map +1 -1
  22. package/dist/commands/new.js +42 -96
  23. package/dist/commands/output-path-safety.d.ts +14 -0
  24. package/dist/commands/output-path-safety.d.ts.map +1 -0
  25. package/dist/commands/output-path-safety.js +34 -0
  26. package/dist/commands/scripts.d.ts +1 -1
  27. package/dist/commands/scripts.d.ts.map +1 -1
  28. package/dist/commands/scripts.js +13 -7
  29. package/dist/commands/typegen-artifact.d.ts +54 -0
  30. package/dist/commands/typegen-artifact.d.ts.map +1 -0
  31. package/dist/commands/typegen-artifact.js +117 -0
  32. package/dist/commands/typegen-generation-child.d.ts +2 -0
  33. package/dist/commands/typegen-generation-child.d.ts.map +1 -0
  34. package/dist/commands/typegen-generation-child.js +59 -0
  35. package/dist/commands/typegen-generation-process.d.ts +46 -0
  36. package/dist/commands/typegen-generation-process.d.ts.map +1 -0
  37. package/dist/commands/typegen-generation-process.js +131 -0
  38. package/dist/commands/typegen-generation-protocol.d.ts +16 -0
  39. package/dist/commands/typegen-generation-protocol.d.ts.map +1 -0
  40. package/dist/commands/typegen-generation-protocol.js +35 -0
  41. package/dist/commands/typegen-isolated-source.d.ts +10 -0
  42. package/dist/commands/typegen-isolated-source.d.ts.map +1 -0
  43. package/dist/commands/typegen-isolated-source.js +75 -0
  44. package/dist/commands/typegen-options.d.ts +20 -0
  45. package/dist/commands/typegen-options.d.ts.map +1 -0
  46. package/dist/commands/typegen-options.js +71 -0
  47. package/dist/commands/typegen-source.d.ts +59 -0
  48. package/dist/commands/typegen-source.d.ts.map +1 -0
  49. package/dist/commands/typegen-source.js +183 -0
  50. package/dist/commands/typegen-watch.d.ts +46 -0
  51. package/dist/commands/typegen-watch.d.ts.map +1 -0
  52. package/dist/commands/typegen-watch.js +212 -0
  53. package/dist/commands/typegen.d.ts +29 -0
  54. package/dist/commands/typegen.d.ts.map +1 -0
  55. package/dist/commands/typegen.js +119 -0
  56. package/dist/dev-runner/node-restart-runner.d.ts +6 -0
  57. package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
  58. package/dist/dev-runner/node-restart-runner.js +64 -13
  59. package/dist/fixtures/inspect-react-app.module.d.ts +4 -0
  60. package/dist/fixtures/inspect-react-app.module.d.ts.map +1 -0
  61. package/dist/fixtures/inspect-react-app.module.js +32 -0
  62. package/dist/fixtures/typegen-react-app.module.d.ts +4 -0
  63. package/dist/fixtures/typegen-react-app.module.d.ts.map +1 -0
  64. package/dist/fixtures/typegen-react-app.module.js +33 -0
  65. package/dist/generate-command.d.ts +50 -0
  66. package/dist/generate-command.d.ts.map +1 -0
  67. package/dist/generate-command.js +214 -0
  68. package/dist/index.d.ts +6 -5
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +6 -5
  71. package/dist/new/install.js +1 -1
  72. package/dist/new/package-spec-resolver.d.ts.map +1 -1
  73. package/dist/new/package-spec-resolver.js +50 -59
  74. package/dist/new/prompt.d.ts +1 -0
  75. package/dist/new/prompt.d.ts.map +1 -1
  76. package/dist/new/prompt.js +40 -12
  77. package/dist/new/published-internal-dependencies.d.ts +20 -0
  78. package/dist/new/published-internal-dependencies.d.ts.map +1 -0
  79. package/dist/new/published-internal-dependencies.js +20 -0
  80. package/dist/new/react-vite-ssr-scaffold.d.ts +19 -0
  81. package/dist/new/react-vite-ssr-scaffold.d.ts.map +1 -0
  82. package/dist/new/react-vite-ssr-scaffold.js +108 -0
  83. package/dist/new/resolver.d.ts +2 -2
  84. package/dist/new/resolver.d.ts.map +1 -1
  85. package/dist/new/resolver.js +5 -1
  86. package/dist/new/scaffold.d.ts.map +1 -1
  87. package/dist/new/scaffold.js +177 -54
  88. package/dist/new/starter-profiles.d.ts +8 -4
  89. package/dist/new/starter-profiles.d.ts.map +1 -1
  90. package/dist/new/starter-profiles.js +52 -18
  91. package/dist/new/templates/react-vite-ssr/README.md.ejs +74 -0
  92. package/dist/new/templates/react-vite-ssr/playwright.config.ts.ejs +26 -0
  93. package/dist/new/templates/react-vite-ssr/src/app.test.ts.ejs +82 -0
  94. package/dist/new/templates/react-vite-ssr/src/app.ts.ejs +91 -0
  95. package/dist/new/templates/react-vite-ssr/src/entry-client.tsx.ejs +29 -0
  96. package/dist/new/templates/react-vite-ssr/src/entry-server.tsx.ejs +52 -0
  97. package/dist/new/templates/react-vite-ssr/src/load-manifest.test.ts.ejs +55 -0
  98. package/dist/new/templates/react-vite-ssr/src/load-manifest.ts.ejs +53 -0
  99. package/dist/new/templates/react-vite-ssr/src/main.ts.ejs +18 -0
  100. package/dist/new/templates/react-vite-ssr/src/page.tsx.ejs +43 -0
  101. package/dist/new/templates/react-vite-ssr/src/react-app.test.tsx.ejs +67 -0
  102. package/dist/new/templates/react-vite-ssr/src/react-app.tsx.ejs +46 -0
  103. package/dist/new/templates/react-vite-ssr/src/styles.css.ejs +25 -0
  104. package/dist/new/templates/react-vite-ssr/src/styles.d.ts.ejs +1 -0
  105. package/dist/new/templates/react-vite-ssr/tests/production-hydration.spec.ts.ejs +54 -0
  106. package/dist/new/templates/react-vite-ssr/tsconfig.json.ejs +16 -0
  107. package/dist/new/templates/react-vite-ssr/vite.client.config.ts.ejs +24 -0
  108. package/dist/new/templates/react-vite-ssr/vite.server.config.ts.ejs +17 -0
  109. package/dist/new/templates/react-vite-ssr/vitest.config.ts.ejs +10 -0
  110. package/dist/new/types.d.ts +4 -2
  111. package/dist/new/types.d.ts.map +1 -1
  112. package/dist/public-generate.d.ts +2 -0
  113. package/dist/public-generate.d.ts.map +1 -0
  114. package/dist/public-generate.js +1 -0
  115. package/dist/public-inspect.d.ts +13 -0
  116. package/dist/public-inspect.d.ts.map +1 -0
  117. package/dist/public-inspect.js +16 -0
  118. package/dist/public-new.d.ts +13 -0
  119. package/dist/public-new.d.ts.map +1 -0
  120. package/dist/public-new.js +16 -0
  121. package/dist/public-typegen.d.ts +14 -0
  122. package/dist/public-typegen.d.ts.map +1 -0
  123. package/dist/public-typegen.js +17 -0
  124. package/dist/run-cli.d.ts +2 -1
  125. package/dist/run-cli.d.ts.map +1 -1
  126. package/dist/studio/sidecar.d.ts.map +1 -1
  127. package/dist/studio/sidecar.js +106 -14
  128. package/dist/transforms/nestjs-migrate.d.ts +4 -1
  129. package/dist/transforms/nestjs-migrate.d.ts.map +1 -1
  130. package/dist/transforms/nestjs-migrate.js +625 -65
  131. package/dist/typegen-contract.d.ts +10 -0
  132. package/dist/typegen-contract.d.ts.map +1 -0
  133. package/dist/typegen-contract.js +9 -0
  134. package/dist/usage.d.ts +19 -0
  135. package/dist/usage.d.ts.map +1 -0
  136. package/dist/usage.js +178 -0
  137. package/package.json +19 -10
@@ -1,214 +1 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { basename, dirname, join, normalize, relative, resolve, sep } from 'node:path';
3
- import { findGeneratorDefinition } from '../generators/manifest.js';
4
- import { ensureModuleImport, generateModuleFiles, registerInModule } from '../generators/module.js';
5
- import { toKebabCase, toPascalCase, toPlural } from '../generators/utils.js';
6
- function writeFileIfChanged(filePath, content) {
7
- if (existsSync(filePath) && readFileSync(filePath, 'utf8') === content) {
8
- return false;
9
- }
10
- writeFileSync(filePath, content, 'utf8');
11
- return true;
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
- }
60
- function createGeneratorOptions(kind, domainDirectory, kebab, options, resolvedBase) {
61
- return {
62
- ...options,
63
- e2eRootModuleImport: options.e2eRootModuleImport ?? (kind === 'e2e' ? resolveE2eRootModuleImport(domainDirectory, resolvedBase) : undefined),
64
- hasRepo: options.hasRepo ?? (kind === 'service' ? existsSync(join(domainDirectory, `${kebab}.repo.ts`)) : undefined),
65
- hasService: options.hasService ?? (kind === 'controller' ? existsSync(join(domainDirectory, `${kebab}.service.ts`)) : undefined)
66
- };
67
- }
68
- function toImportSpecifier(path) {
69
- const normalized = path.split(sep).join('/');
70
- return normalized.startsWith('.') ? normalized : `./${normalized}`;
71
- }
72
- function resolveE2eRootModuleImport(domainDirectory, resolvedBase) {
73
- return toImportSpecifier(relative(domainDirectory, join(resolvedBase, 'app')));
74
- }
75
- function resolveDomainDirectory(kind, resolvedBase, kebab, options) {
76
- if (kind === 'e2e') {
77
- return basename(resolvedBase) === 'src' ? join(dirname(resolvedBase), 'test') : join(resolvedBase, 'test');
78
- }
79
- if (kind === 'request-dto' && options.targetFeature !== undefined) {
80
- const normalizedFeature = options.targetFeature.trim();
81
- const featureKebab = assertValidResourceName(normalizedFeature);
82
- const featureDirectory = /^[A-Z]/u.test(normalizedFeature) ? toPlural(featureKebab) : featureKebab;
83
- return join(resolvedBase, featureDirectory);
84
- }
85
- return join(resolvedBase, toPlural(kebab));
86
- }
87
- function assertValidResourceName(name) {
88
- const kebab = toKebabCase(name);
89
- if (name.trim().length === 0) {
90
- throw new Error('Invalid resource name: name must not be empty.');
91
- }
92
- if (kebab !== normalize(kebab) || kebab.includes('/') || kebab.includes('\\') || kebab.includes('..')) {
93
- throw new Error(`Invalid resource name "${name}": must not contain path separators or traversal sequences.`);
94
- }
95
- if (!/^[a-z0-9][a-z0-9-]*$/.test(kebab)) {
96
- throw new Error(`Invalid resource name "${name}": use letters, numbers, spaces, underscores, or hyphens only.`);
97
- }
98
- return kebab;
99
- }
100
- function resolveModulePath(domainDirectory, name) {
101
- const kebab = toKebabCase(name);
102
- return join(domainDirectory, `${kebab}.module.ts`);
103
- }
104
- function readOrCreateModuleSource(modulePath, name) {
105
- if (existsSync(modulePath)) {
106
- return readFileSync(modulePath, 'utf8');
107
- }
108
- const [moduleFile] = generateModuleFiles(name);
109
- if (!moduleFile) {
110
- throw new Error(`Unable to generate module file for resource "${name}".`);
111
- }
112
- return moduleFile.content;
113
- }
114
- function buildUpdatedModuleSource(moduleSource, arrayKey, className, importPath) {
115
- let source = moduleSource;
116
- source = ensureModuleImport(source, className, importPath);
117
- source = registerInModule(source, arrayKey, className);
118
- return source;
119
- }
120
- function prepareModuleUpdate(domainDirectory, normalizedName, kind, classSuffix, arrayKey) {
121
- const kebab = toKebabCase(normalizedName);
122
- const modulePath = resolveModulePath(domainDirectory, normalizedName);
123
- const className = `${toPascalCase(normalizedName)}${classSuffix}`;
124
- const importPath = `${kebab}.${kind}`;
125
- const moduleSource = readOrCreateModuleSource(modulePath, normalizedName);
126
- return {
127
- modulePath,
128
- source: buildUpdatedModuleSource(moduleSource, arrayKey, className, importPath)
129
- };
130
- }
131
-
132
- /**
133
- * Structured result returned by {@link runGenerateCommand} for tooling-friendly automation.
134
- *
135
- * `generatedFiles` only includes files whose on-disk content changed during the command.
136
- * `moduleRegistered` reports whether the target schematic participates in automatic module wiring,
137
- * even when the target module file was already up to date.
138
- */
139
-
140
- /**
141
- * Generates one CLI schematic into a source directory and returns structured wiring metadata.
142
- *
143
- * The command keeps generation idempotent where possible: unchanged files are not rewritten, and
144
- * auto-registered schematics reuse an existing module file when it already contains the required import
145
- * and registration entry.
146
- *
147
- * @example
148
- * ```ts
149
- * const result = runGenerateCommand('service', 'Post', './src');
150
- *
151
- * console.log(result.wiringBehavior);
152
- * console.log(result.nextStepHint);
153
- * ```
154
- *
155
- * @param kind Generator kind to execute.
156
- * @param name Resource name supplied by the caller before normalization.
157
- * @param baseDirectory Source directory that should receive the generated domain folder.
158
- * @param options Optional generation flags that control overwrites, request DTO feature placement, and sibling-aware templates.
159
- * @returns Structured file and wiring metadata for the completed generation run.
160
- * @throws {Error} When the resource name is invalid, the generator kind is unknown, or the target module source cannot be updated safely.
161
- */
162
- export function runGenerateCommand(kind, name, baseDirectory, options = {}) {
163
- const normalizedName = name.trim();
164
- const kebab = assertValidResourceName(normalizedName);
165
- const generator = findGeneratorDefinition(kind);
166
- const resolvedBase = resolve(baseDirectory);
167
- const domainDirectory = resolveDomainDirectory(kind, resolvedBase, kebab, options);
168
- const generatorOptions = createGeneratorOptions(kind, domainDirectory, kebab, options, resolvedBase);
169
- const files = generator.factory(normalizedName, generatorOptions);
170
- const moduleRegistration = 'moduleRegistration' in generator ? generator.moduleRegistration : undefined;
171
- const moduleUpdate = moduleRegistration ? prepareModuleUpdate(domainDirectory, normalizedName, kind, moduleRegistration.classSuffix, moduleRegistration.arrayKey) : undefined;
172
- const plannedFiles = files.map(file => planFileWrite(join(domainDirectory, file.path), file.content, options));
173
- const modulePlan = moduleUpdate ? planModuleWrite(moduleUpdate.modulePath, moduleUpdate.source) : undefined;
174
- if (options.dryRun) {
175
- return {
176
- generatedFiles: [],
177
- moduleRegistered: moduleUpdate !== undefined,
178
- modulePath: moduleUpdate?.modulePath,
179
- nextStepHint: generator.nextStepHint,
180
- plannedFiles: modulePlan ? [...plannedFiles, modulePlan] : plannedFiles,
181
- wiringBehavior: generator.wiringBehavior
182
- };
183
- }
184
- mkdirSync(domainDirectory, {
185
- recursive: true
186
- });
187
- const writtenPaths = files.map(file => {
188
- const filePath = join(domainDirectory, file.path);
189
- if (!options.force && existsSync(filePath)) {
190
- return null;
191
- }
192
- return writeFileIfChanged(filePath, file.content) ? filePath : null;
193
- }).filter(filePath => filePath !== null);
194
- let moduleRegistered = false;
195
- let resolvedModulePath;
196
- if (moduleUpdate && writeFileIfChanged(moduleUpdate.modulePath, moduleUpdate.source)) {
197
- moduleRegistered = true;
198
- resolvedModulePath = moduleUpdate.modulePath;
199
- if (!writtenPaths.includes(moduleUpdate.modulePath)) {
200
- writtenPaths.push(moduleUpdate.modulePath);
201
- }
202
- } else if (moduleUpdate) {
203
- moduleRegistered = true;
204
- resolvedModulePath = moduleUpdate.modulePath;
205
- }
206
- return {
207
- generatedFiles: writtenPaths,
208
- moduleRegistered: moduleRegistered,
209
- modulePath: resolvedModulePath,
210
- nextStepHint: generator.nextStepHint,
211
- plannedFiles: modulePlan ? [...plannedFiles, modulePlan] : plannedFiles,
212
- wiringBehavior: generator.wiringBehavior
213
- };
214
- }
1
+ export { runGenerateCommand } from '../generate-command.js';
@@ -1,4 +1,4 @@
1
- import { type PlatformShellSnapshot } from '@fluojs/runtime';
1
+ import type { PlatformShellSnapshot } from '@fluojs/runtime';
2
2
  type CliStream = {
3
3
  write(message: string): unknown;
4
4
  };
@@ -11,6 +11,8 @@ type ReadableStream = {
11
11
  };
12
12
  type StudioMermaidRenderer = (snapshot: PlatformShellSnapshot) => string;
13
13
  type StudioMermaidRendererLoader = (cwd: string) => Promise<StudioMermaidRenderer | undefined>;
14
+ type RuntimeInspectionModule = Pick<typeof import('@fluojs/runtime'), 'createRuntimeInspectionSnapshot' | 'FluoFactory' | 'PLATFORM_SHELL'>;
15
+ type RuntimeInspectionModuleLoader = (cwd: string) => Promise<RuntimeInspectionModule | undefined>;
14
16
  /**
15
17
  * Runtime options for the inspect command when used programmatically.
16
18
  */
@@ -21,6 +23,8 @@ export interface InspectCommandRuntimeOptions {
21
23
  cwd?: string;
22
24
  /** Force or disable interactive prompts for optional Studio guidance. */
23
25
  interactive?: boolean;
26
+ /** Optional test/editor hook for resolving the runtime inspection dependency. */
27
+ loadRuntimeInspectionModule?: RuntimeInspectionModuleLoader;
24
28
  /** Optional test/editor hook for resolving Studio's Mermaid renderer. */
25
29
  loadStudioMermaidRenderer?: StudioMermaidRendererLoader;
26
30
  /** Custom prompt implementation used only when Studio is missing for Mermaid output. */
@@ -32,12 +36,6 @@ export interface InspectCommandRuntimeOptions {
32
36
  /** Custom stream for standard output. */
33
37
  stdout?: CliStream;
34
38
  }
35
- /**
36
- * Returns the usage information string for the inspect command.
37
- *
38
- * @returns Formatted help text including usage and options.
39
- */
40
- export declare function inspectUsage(): string;
41
39
  /**
42
40
  * Executes the inspect command to visualize the application module graph.
43
41
  *
@@ -1 +1 @@
1
- {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAMA,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAMzB,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;AAmFD;;;;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,CAkDnH"}
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAKV,qBAAqB,EAEtB,MAAM,iBAAiB,CAAC;AAQzB,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,KAAK,uBAAuB,GAAG,IAAI,CACjC,cAAc,iBAAiB,CAAC,EAChC,iCAAiC,GAAG,aAAa,GAAG,gBAAgB,CACrE,CAAC;AAEF,KAAK,6BAA6B,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAAC;AAEnG;;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,iFAAiF;IACjF,2BAA2B,CAAC,EAAE,6BAA6B,CAAC;IAC5D,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;AA2WD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CA0DnH"}
@@ -1,70 +1,27 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { mkdir, writeFile } from 'node:fs/promises';
2
3
  import { createRequire } from 'node:module';
3
4
  import { dirname, extname, resolve } from 'node:path';
4
5
  import { pathToFileURL } from 'node:url';
5
- import * as clack from '@clack/prompts';
6
- import { FluoFactory, PLATFORM_SHELL } from '@fluojs/runtime';
7
6
  import { tsImport } from 'tsx/esm/api';
8
- import { renderAliasList, renderHelpTable } from '../help.js';
9
7
  import { CliPromptCancelledError, isCliPromptCancelledError } from '../prompt-cancel.js';
8
+ import { inspectUsage } from '../usage.js';
9
+ import { createCliDiagnosticsLogger } from './diagnostics.js';
10
+ import { assertOutputDoesNotAliasModule } from './output-path-safety.js';
10
11
 
11
12
  /**
12
13
  * Runtime options for the inspect command when used programmatically.
13
14
  */
14
15
 
15
- const INSPECT_OPTION_HELP = [{
16
- aliases: [],
17
- description: 'Emit the runtime platform snapshot/diagnostics payload as JSON (default when no output mode is selected).',
18
- option: '--json'
19
- }, {
20
- aliases: [],
21
- description: 'Emit a Mermaid graph through the optional @fluojs/studio rendering contract.',
22
- option: '--mermaid'
23
- }, {
24
- aliases: [],
25
- description: 'Include bootstrap timing diagnostics next to JSON inspect output.',
26
- option: '--timing'
27
- }, {
28
- aliases: [],
29
- description: 'Emit a CI-friendly JSON report with summary, snapshot, diagnostics, and timing.',
30
- option: '--report'
31
- }, {
32
- aliases: [],
33
- description: 'Write the selected inspect payload to a file instead of stdout.',
34
- option: '--output <path>'
35
- }, {
36
- aliases: [],
37
- description: 'Select the exported module symbol name (default: AppModule).',
38
- option: '--export <name>'
39
- }, {
40
- aliases: ['-h'],
41
- description: 'Show help for the inspect command.',
42
- option: '--help'
43
- }];
44
16
  const STUDIO_CONTRACT_ENTRYPOINT = '@fluojs/studio/contracts';
17
+ const RUNTIME_ENTRYPOINT = '@fluojs/runtime';
45
18
  const TYPESCRIPT_MODULE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
19
+ const RUNTIME_MISSING_MESSAGE = ['Runtime inspection requires @fluojs/runtime, but it is not resolvable from this project.', 'Install a version whose Node.js engine supports your current Node.js version (for example: pnpm add -D @fluojs/runtime) and rerun fluo inspect.'].join('\n');
46
20
  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');
21
+ const CLACK_MISSING_MESSAGE = ['Interactive inspect guidance requires @clack/prompts, but it is not resolvable from this installation.', 'Install a version whose Node.js engine supports your current Node.js version and rerun fluo inspect.'].join('\n');
47
22
  function isHelpFlag(value) {
48
23
  return value === '--help' || value === '-h';
49
24
  }
50
-
51
- /**
52
- * Returns the usage information string for the inspect command.
53
- *
54
- * @returns Formatted help text including usage and options.
55
- */
56
- export function inspectUsage() {
57
- return ['Usage: fluo inspect <module-path> [options]', '', 'Options', renderHelpTable(INSPECT_OPTION_HELP, [{
58
- header: 'Option',
59
- render: entry => entry.option
60
- }, {
61
- header: 'Aliases',
62
- render: entry => renderAliasList(entry.aliases)
63
- }, {
64
- header: 'Description',
65
- render: entry => entry.description
66
- }]), '', 'Docs: https://github.com/fluojs/fluo/tree/main/docs/getting-started/quick-start.md'].join('\n');
67
- }
68
25
  function parseInspectArgs(argv) {
69
26
  let modulePath;
70
27
  let exportName = 'AppModule';
@@ -82,6 +39,18 @@ function parseInspectArgs(argv) {
82
39
  json = true;
83
40
  continue;
84
41
  }
42
+ if (option === '--format') {
43
+ const format = argv[index + 1];
44
+ if (!format || format.startsWith('-')) {
45
+ throw new Error('Expected --format to have a value of "json".');
46
+ }
47
+ if (format !== 'json') {
48
+ throw new Error(`Invalid --format value "${format}". Use "json".`);
49
+ }
50
+ json = true;
51
+ index += 1;
52
+ continue;
53
+ }
85
54
  if (option === '--mermaid') {
86
55
  mermaid = true;
87
56
  continue;
@@ -152,6 +121,13 @@ function resolveRootModule(exportedValue, exportName) {
152
121
  async function importInspectModule(modulePath) {
153
122
  const moduleUrl = pathToFileURL(modulePath).href;
154
123
  if (TYPESCRIPT_MODULE_EXTENSIONS.has(extname(modulePath))) {
124
+ const adjacentTsconfigPath = resolve(dirname(modulePath), 'tsconfig.json');
125
+ if (existsSync(adjacentTsconfigPath)) {
126
+ return tsImport(moduleUrl, {
127
+ parentURL: import.meta.url,
128
+ tsconfig: adjacentTsconfigPath
129
+ });
130
+ }
155
131
  return tsImport(moduleUrl, import.meta.url);
156
132
  }
157
133
  return import(moduleUrl);
@@ -192,18 +168,26 @@ function stringifySnapshotWithTiming(snapshot, timing) {
192
168
  timing: timing ?? createEmptyTimingDiagnostics()
193
169
  }, null, 2);
194
170
  }
195
- async function emitInspectPayload(payload, parsed, cwd, stdout) {
196
- if (!parsed.outputPath) {
171
+ async function emitInspectPayload(payload, outputPath, stdout) {
172
+ if (!outputPath) {
197
173
  stdout.write(`${payload}\n`);
198
174
  return;
199
175
  }
200
- const outputPath = resolve(cwd, parsed.outputPath);
201
176
  await mkdir(dirname(outputPath), {
202
177
  recursive: true
203
178
  });
204
179
  await writeFile(outputPath, `${payload}\n`, 'utf8');
205
180
  }
206
- function createInspectPrompter() {
181
+ async function createInspectPrompter() {
182
+ let clack;
183
+ try {
184
+ clack = await import('@clack/prompts');
185
+ } catch (error) {
186
+ if (isModuleNotFoundError(error)) {
187
+ throw new Error(CLACK_MISSING_MESSAGE);
188
+ }
189
+ throw error;
190
+ }
207
191
  return {
208
192
  async confirm(message, defaultValue) {
209
193
  const result = await clack.confirm({
@@ -227,6 +211,29 @@ function shouldPromptForStudio(runtime) {
227
211
  }
228
212
  return runtime.stdout === undefined && runtime.stderr === undefined && (runtime.interactive ?? true) && Boolean(runtime.stdin?.isTTY ?? process.stdin.isTTY);
229
213
  }
214
+ function isModuleNotFoundError(error) {
215
+ const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
216
+ return code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
217
+ }
218
+ async function loadRuntimeInspectionModule(cwd) {
219
+ const resolver = createRequire(resolve(cwd, 'package.json'));
220
+ try {
221
+ const resolvedEntrypoint = resolver.resolve(RUNTIME_ENTRYPOINT);
222
+ return await import(pathToFileURL(resolvedEntrypoint).href);
223
+ } catch (error) {
224
+ if (isModuleNotFoundError(error)) {
225
+ return undefined;
226
+ }
227
+ throw error;
228
+ }
229
+ }
230
+ async function resolveRuntimeInspectionModule(cwd, runtime) {
231
+ const runtimeModule = await (runtime.loadRuntimeInspectionModule ?? loadRuntimeInspectionModule)(cwd);
232
+ if (!runtimeModule) {
233
+ throw new Error(RUNTIME_MISSING_MESSAGE);
234
+ }
235
+ return runtimeModule;
236
+ }
230
237
  async function loadStudioMermaidRenderer(cwd) {
231
238
  const resolvers = [createRequire(resolve(cwd, 'package.json')), createRequire(import.meta.url)];
232
239
  for (const resolver of resolvers) {
@@ -238,8 +245,7 @@ async function loadStudioMermaidRenderer(cwd) {
238
245
  }
239
246
  return importedContract.renderMermaid;
240
247
  } catch (error) {
241
- const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
242
- if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
248
+ if (!isModuleNotFoundError(error)) {
243
249
  throw error;
244
250
  }
245
251
  }
@@ -254,7 +260,7 @@ async function resolveStudioMermaidRenderer(cwd, runtime) {
254
260
  if (!shouldPromptForStudio(runtime)) {
255
261
  throw new Error(STUDIO_MISSING_MESSAGE);
256
262
  }
257
- const prompt = runtime.prompt ?? createInspectPrompter();
263
+ const prompt = runtime.prompt ?? (await createInspectPrompter());
258
264
  try {
259
265
  const approvedInstall = await prompt.confirm('Install @fluojs/studio before rendering Mermaid output?', false);
260
266
  if (!approvedInstall) {
@@ -284,28 +290,42 @@ export async function runInspectCommand(argv, runtime = {}) {
284
290
  }
285
291
  const parsed = parseInspectArgs(argv);
286
292
  const modulePath = resolve(cwd, parsed.modulePath);
293
+ const outputPath = parsed.outputPath === undefined ? undefined : resolve(cwd, parsed.outputPath);
294
+ if (outputPath !== undefined) {
295
+ await assertOutputDoesNotAliasModule(modulePath, outputPath);
296
+ }
297
+ const {
298
+ createRuntimeInspectionSnapshot,
299
+ FluoFactory,
300
+ PLATFORM_SHELL
301
+ } = await resolveRuntimeInspectionModule(cwd, runtime);
287
302
  const importedModule = await importInspectModule(modulePath);
288
303
  const rootModule = resolveRootModule(importedModule[parsed.exportName], parsed.exportName);
289
- const context = await FluoFactory.createApplicationContext(rootModule, {
304
+ const application = await FluoFactory.create(rootModule, {
290
305
  diagnostics: parsed.timing || parsed.report ? {
291
306
  timing: true
292
- } : undefined
307
+ } : undefined,
308
+ ...(parsed.json || parsed.report ? {
309
+ logger: createCliDiagnosticsLogger(stderr)
310
+ } : {})
293
311
  });
294
312
  try {
295
- const platformShell = await context.get(PLATFORM_SHELL);
296
- const snapshot = await platformShell.snapshot();
313
+ const platformShell = await application.get(PLATFORM_SHELL);
314
+ const platformSnapshot = await platformShell.snapshot();
315
+ const routes = application.dispatcher.describeRoutes?.() ?? [];
316
+ const snapshot = createRuntimeInspectionSnapshot(platformSnapshot, routes);
297
317
  if (parsed.json) {
298
- await emitInspectPayload(parsed.timing ? stringifySnapshotWithTiming(snapshot, context.bootstrapTiming) : stringifySnapshot(snapshot), parsed, cwd, stdout);
318
+ await emitInspectPayload(parsed.timing ? stringifySnapshotWithTiming(snapshot, application.bootstrapTiming) : stringifySnapshot(snapshot), outputPath, stdout);
299
319
  }
300
320
  if (parsed.report) {
301
- await emitInspectPayload(JSON.stringify(createInspectReport(snapshot, context.bootstrapTiming), null, 2), parsed, cwd, stdout);
321
+ await emitInspectPayload(JSON.stringify(createInspectReport(snapshot, application.bootstrapTiming), null, 2), outputPath, stdout);
302
322
  }
303
323
  if (parsed.mermaid) {
304
324
  const renderMermaid = await resolveStudioMermaidRenderer(cwd, runtime);
305
- await emitInspectPayload(renderMermaid(snapshot), parsed, cwd, stdout);
325
+ await emitInspectPayload(renderMermaid(snapshot), outputPath, stdout);
306
326
  }
307
327
  } finally {
308
- await context.close();
328
+ await application.close();
309
329
  }
310
330
  return 0;
311
331
  } catch (error) {
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAgBA,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;AAqLD;;;;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,CAyEnH"}
@@ -1,6 +1,7 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { renderAliasList, renderHelpTable } from '../help.js';
3
- import { MIGRATION_TRANSFORMS, getWarningCategoryLabel, groupWarningsByCategory, renderTransformList, runNestJsMigration } from '../transforms/nestjs-migrate.js';
3
+ import { getWarningCategoryLabel, groupWarningsByCategory, MIGRATION_TRANSFORMS, renderTransformList, runNestJsMigration } from '../transforms/nestjs-migrate.js';
4
+ import { MIGRATION_TRANSFORM_CLI_TOKENS, parseMigrationTransformList } from './migration-transform-tokens.js';
4
5
 
5
6
  /**
6
7
  * Runtime configuration for the migrate command.
@@ -16,12 +17,16 @@ const MIGRATE_OPTION_HELP = [{
16
17
  option: '--json'
17
18
  }, {
18
19
  aliases: [],
19
- description: `Run only selected transforms. Available: ${MIGRATION_TRANSFORMS.join(', ')}.`,
20
+ description: `Run only selected transforms. Available: ${MIGRATION_TRANSFORM_CLI_TOKENS.join(', ')}.`,
20
21
  option: '--only <comma-list>'
21
22
  }, {
22
23
  aliases: [],
23
- description: `Skip selected transforms. Available: ${MIGRATION_TRANSFORMS.join(', ')}.`,
24
+ description: `Skip selected transforms. Available: ${MIGRATION_TRANSFORM_CLI_TOKENS.join(', ')}.`,
24
25
  option: '--skip <comma-list>'
26
+ }, {
27
+ aliases: [],
28
+ description: 'Select the HTTP platform for bootstrap rewrites. Required for automatic bootstrap migration; supported: express.',
29
+ option: '--platform <name>'
25
30
  }, {
26
31
  aliases: ['-h'],
27
32
  description: 'Show help for the migrate command.',
@@ -30,22 +35,12 @@ const MIGRATE_OPTION_HELP = [{
30
35
  function isHelpFlag(value) {
31
36
  return value === '--help' || value === '-h';
32
37
  }
33
- function parseTransformList(rawValue, optionName) {
34
- const values = rawValue.split(',').map(value => value.trim()).filter(value => value.length > 0);
35
- if (values.length === 0) {
36
- throw new Error(`${optionName} requires a non-empty comma-separated transform list.`);
37
- }
38
- const invalid = values.filter(value => !MIGRATION_TRANSFORMS.includes(value));
39
- if (invalid.length > 0) {
40
- throw new Error(`Unknown transform(s): ${invalid.join(', ')}. Available transforms: ${MIGRATION_TRANSFORMS.join(', ')}.`);
41
- }
42
- return values;
43
- }
44
38
  function parseArgs(argv) {
45
39
  let pathArgument;
46
40
  let apply = false;
47
41
  let json = false;
48
42
  let onlyTransforms;
43
+ let platform;
49
44
  let skipTransforms = [];
50
45
  for (let index = 0; index < argv.length; index += 1) {
51
46
  const arg = argv[index];
@@ -65,7 +60,7 @@ function parseArgs(argv) {
65
60
  if (!rawValue || rawValue.startsWith('-')) {
66
61
  throw new Error(`Expected ${arg} to have a comma-separated value.`);
67
62
  }
68
- const parsed = parseTransformList(rawValue, arg);
63
+ const parsed = parseMigrationTransformList(rawValue, arg);
69
64
  if (arg === '--only') {
70
65
  if (onlyTransforms) {
71
66
  throw new Error('Duplicate --only option.');
@@ -77,6 +72,21 @@ function parseArgs(argv) {
77
72
  index += 1;
78
73
  continue;
79
74
  }
75
+ if (arg === '--platform') {
76
+ const rawValue = argv[index + 1];
77
+ if (!rawValue || rawValue.startsWith('-')) {
78
+ throw new Error('Expected --platform to have a platform name.');
79
+ }
80
+ if (platform) {
81
+ throw new Error('Duplicate --platform option.');
82
+ }
83
+ if (rawValue !== 'express') {
84
+ throw new Error(`Unsupported bootstrap platform: ${rawValue}. Supported platform: express.`);
85
+ }
86
+ platform = rawValue;
87
+ index += 1;
88
+ continue;
89
+ }
80
90
  if (arg.startsWith('-')) {
81
91
  throw new Error(`Unknown option for migrate command: ${arg}`);
82
92
  }
@@ -99,6 +109,7 @@ function parseArgs(argv) {
99
109
  apply,
100
110
  json,
101
111
  path: pathArgument,
112
+ platform,
102
113
  transforms: enabled
103
114
  };
104
115
  }
@@ -166,6 +177,7 @@ export async function runMigrateCommand(argv, runtime = {}) {
166
177
  const transforms = [...parsed.transforms];
167
178
  const report = runNestJsMigration({
168
179
  apply: parsed.apply,
180
+ bootstrapPlatform: parsed.platform,
169
181
  enabledTransforms: parsed.transforms,
170
182
  targetPath
171
183
  });
@@ -0,0 +1,15 @@
1
+ import { type MigrationTransformKind } from '../transforms/nestjs-migrate.js';
2
+ /**
3
+ * Provides canonical migration transform tokens accepted by CLI options.
4
+ */
5
+ export declare const MIGRATION_TRANSFORM_CLI_TOKENS: readonly ["imports", "inject-params", "scope", "bootstrap", "tests", "tsconfig"];
6
+ /**
7
+ * Parse canonical or legacy migration transform tokens from one CLI option.
8
+ *
9
+ * @param rawValue Comma-separated transform tokens passed to the CLI.
10
+ * @param optionName CLI option that supplied the transform tokens.
11
+ * @returns Canonical migration transform kinds.
12
+ * @throws When the option has no tokens or includes an unsupported token.
13
+ */
14
+ export declare function parseMigrationTransformList(rawValue: string, optionName: '--only' | '--skip'): MigrationTransformKind[];
15
+ //# sourceMappingURL=migration-transform-tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-transform-tokens.d.ts","sourceRoot":"","sources":["../../src/commands/migration-transform-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAEpG;;GAEG;AACH,eAAO,MAAM,8BAA8B,kFAAmF,CAAC;AAS/H;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAC9B,sBAAsB,EAAE,CA2B1B"}
@@ -0,0 +1,41 @@
1
+ import { MIGRATION_TRANSFORMS } from '../transforms/nestjs-migrate.js';
2
+
3
+ /**
4
+ * Provides canonical migration transform tokens accepted by CLI options.
5
+ */
6
+ export const MIGRATION_TRANSFORM_CLI_TOKENS = ['imports', 'inject-params', 'scope', 'bootstrap', 'tests', 'tsconfig'];
7
+ const MIGRATION_TRANSFORM_ALIASES = {
8
+ 'inject-params': 'injectable',
9
+ injectable: 'injectable',
10
+ tests: 'testing',
11
+ testing: 'testing'
12
+ };
13
+
14
+ /**
15
+ * Parse canonical or legacy migration transform tokens from one CLI option.
16
+ *
17
+ * @param rawValue Comma-separated transform tokens passed to the CLI.
18
+ * @param optionName CLI option that supplied the transform tokens.
19
+ * @returns Canonical migration transform kinds.
20
+ * @throws When the option has no tokens or includes an unsupported token.
21
+ */
22
+ export function parseMigrationTransformList(rawValue, optionName) {
23
+ const values = rawValue.split(',').map(value => value.trim()).filter(value => value.length > 0);
24
+ if (values.length === 0) {
25
+ throw new Error(`${optionName} requires a non-empty comma-separated transform list.`);
26
+ }
27
+ const transforms = [];
28
+ const invalid = [];
29
+ for (const value of values) {
30
+ const transform = MIGRATION_TRANSFORMS.find(candidate => candidate === value) ?? (Object.hasOwn(MIGRATION_TRANSFORM_ALIASES, value) ? MIGRATION_TRANSFORM_ALIASES[value] : undefined);
31
+ if (transform) {
32
+ transforms.push(transform);
33
+ } else {
34
+ invalid.push(value);
35
+ }
36
+ }
37
+ if (invalid.length > 0) {
38
+ throw new Error(`Unknown transform(s): ${invalid.join(', ')}. Available transforms: ${MIGRATION_TRANSFORM_CLI_TOKENS.join(', ')}.`);
39
+ }
40
+ return transforms;
41
+ }