@nifrajs/cli 2.14.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +6 -1
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +3 -2
- package/dist/assure.js.map +1 -1
- package/dist/check-diagnostics.d.ts +165 -0
- package/dist/check-diagnostics.d.ts.map +1 -0
- package/dist/check-diagnostics.js +1009 -0
- package/dist/check-diagnostics.js.map +1 -0
- package/dist/check-scan.d.ts +245 -0
- package/dist/check-scan.d.ts.map +1 -0
- package/dist/check-scan.js +1599 -0
- package/dist/check-scan.js.map +1 -0
- package/dist/check.d.ts +4 -324
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +8 -2346
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +285 -10
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +27 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +142 -2
- package/dist/command-catalog.js.map +1 -1
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +64 -7
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +17 -0
- package/dist/env-file.d.ts.map +1 -0
- package/dist/env-file.js +113 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +91 -15
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.d.ts +57 -0
- package/dist/frontend-guidance.d.ts.map +1 -0
- package/dist/frontend-guidance.js +195 -0
- package/dist/frontend-guidance.js.map +1 -0
- package/dist/internal/reserved-segment-codemod.d.ts +36 -0
- package/dist/internal/reserved-segment-codemod.d.ts.map +1 -0
- package/dist/internal/reserved-segment-codemod.js +134 -0
- package/dist/internal/reserved-segment-codemod.js.map +1 -0
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +62 -11
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +26 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +74 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +6 -0
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +4 -1
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +48 -0
- package/dist/mcp-context.d.ts.map +1 -0
- package/dist/mcp-context.js +215 -0
- package/dist/mcp-context.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +31 -0
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +74 -0
- package/dist/mcp-exec.d.ts.map +1 -0
- package/dist/mcp-exec.js +844 -0
- package/dist/mcp-exec.js.map +1 -0
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -0
- package/dist/mcp-io.js +100 -0
- package/dist/mcp-io.js.map +1 -0
- package/dist/mcp-reflect.d.ts +16 -0
- package/dist/mcp-reflect.d.ts.map +1 -0
- package/dist/mcp-reflect.js +101 -0
- package/dist/mcp-reflect.js.map +1 -0
- package/dist/mcp-root.d.ts +7 -0
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +51 -3
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp.d.ts +10 -84
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +13 -1170
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +5 -10
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js +21 -2
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +21 -0
- package/dist/openapi-types.d.ts.map +1 -0
- package/dist/openapi-types.js +214 -0
- package/dist/openapi-types.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +2 -1
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +2 -1
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/3.0.0.d.ts +10 -0
- package/dist/recipes/3.0.0.d.ts.map +1 -0
- package/dist/recipes/3.0.0.js +23 -0
- package/dist/recipes/3.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +4 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +6 -123
- package/dist/release-verification.js.map +1 -1
- package/dist/rules/codes.d.ts +4 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +4 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/islands.d.ts +4 -0
- package/dist/rules/islands.d.ts.map +1 -0
- package/dist/rules/islands.js +155 -0
- package/dist/rules/islands.js.map +1 -0
- package/dist/rules/nano.d.ts +6 -0
- package/dist/rules/nano.d.ts.map +1 -0
- package/dist/rules/nano.js +264 -0
- package/dist/rules/nano.js.map +1 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +17 -30
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +38 -4
- package/dist/rules/security.js.map +1 -1
- package/dist/scaffold.d.ts +12 -6
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +149 -10
- package/dist/scaffold.js.map +1 -1
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +5 -3
- package/dist/search-terms.js.map +1 -1
- package/dist/stylex-migrate.d.ts +38 -0
- package/dist/stylex-migrate.d.ts.map +1 -0
- package/dist/stylex-migrate.js +1052 -0
- package/dist/stylex-migrate.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/verification-plan.d.ts +24 -0
- package/dist/verification-plan.d.ts.map +1 -0
- package/dist/verification-plan.js +116 -0
- package/dist/verification-plan.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +6 -3
- package/dist/work-graph.js.map +1 -1
- package/dist/workspace-link.d.ts +37 -0
- package/dist/workspace-link.d.ts.map +1 -0
- package/dist/workspace-link.js +72 -0
- package/dist/workspace-link.js.map +1 -0
- package/docs/examples.json +37 -1
- package/docs/llms-full.txt +1598 -44
- package/docs/types.json +5157 -172
- package/package.json +9 -9
- package/src/assure-hydration.ts +6 -1
- package/src/assure.ts +4 -2
- package/src/check-diagnostics.ts +1219 -0
- package/src/check-scan.ts +1825 -0
- package/src/check.ts +47 -2731
- package/src/cli.ts +291 -10
- package/src/command-catalog.ts +177 -2
- package/src/doctor.ts +99 -8
- package/src/env-file.ts +121 -0
- package/src/fix-recipes.ts +101 -15
- package/src/frontend-guidance.ts +259 -0
- package/src/internal/reserved-segment-codemod.ts +150 -0
- package/src/internal/typescript-import.ts +61 -11
- package/src/introspect.ts +82 -21
- package/src/levels-tool.ts +6 -0
- package/src/load.ts +4 -1
- package/src/mcp-context.ts +266 -0
- package/src/mcp-docs-tools.ts +37 -0
- package/src/mcp-exec.ts +1040 -0
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +118 -0
- package/src/mcp-reflect.ts +140 -0
- package/src/mcp-root.ts +62 -4
- package/src/mcp.ts +61 -1436
- package/src/openapi-tool.ts +32 -2
- package/src/openapi-types.ts +261 -0
- package/src/port.ts +2 -1
- package/src/project-facts.ts +2 -4
- package/src/recipes/3.0.0.ts +24 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +22 -147
- package/src/rules/codes.ts +4 -0
- package/src/rules/islands.ts +171 -0
- package/src/rules/nano.ts +295 -0
- package/src/rules/routes.ts +17 -30
- package/src/rules/security.ts +40 -4
- package/src/scaffold.ts +169 -9
- package/src/search-terms.ts +7 -3
- package/src/stylex-migrate.ts +1139 -0
- package/src/sync-manifest.ts +2 -1
- package/src/verification-plan.ts +242 -0
- package/src/work-graph.ts +6 -3
- package/src/workspace-link.ts +80 -0
package/src/cli.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Bun-only (it runs the framework's TS + Bun plugins directly). The *output* runs anywhere.
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
12
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs"
|
|
13
13
|
import { resolve } from "node:path"
|
|
14
|
+
import { fileURLToPath } from "node:url"
|
|
14
15
|
import { inProcessClient } from "@nifrajs/client"
|
|
15
16
|
import {
|
|
16
17
|
type CreateWebAppOptions,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
22
23
|
import type { BunPlugin } from "bun"
|
|
23
24
|
import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
|
|
25
|
+
import { applyEnvFiles, takeEnvFileFlags } from "./env-file.ts"
|
|
24
26
|
import { type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
|
|
25
27
|
import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
|
|
26
28
|
|
|
@@ -46,6 +48,23 @@ export interface Flags {
|
|
|
46
48
|
readonly allowDuplicateIdentity: boolean
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/** Forward a re-exec'd Bun child's output without relying on Windows inheriting a pipe-of-a-pipe. */
|
|
52
|
+
async function forwardChildOutput(
|
|
53
|
+
stream: ReadableStream<Uint8Array>,
|
|
54
|
+
sink: { write(chunk: Uint8Array): unknown },
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const reader = stream.getReader()
|
|
57
|
+
try {
|
|
58
|
+
for (;;) {
|
|
59
|
+
const { done, value } = await reader.read()
|
|
60
|
+
if (done) return
|
|
61
|
+
await sink.write(value)
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
reader.releaseLock()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
const HELP = `nifra - zero-config dev/build/start for a nifra app
|
|
50
69
|
|
|
51
70
|
Usage:
|
|
@@ -188,10 +207,16 @@ routes/ from the current directory. Run from your project root.
|
|
|
188
207
|
Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
|
|
189
208
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
|
|
190
209
|
|
|
210
|
+
Env: \`--env-file <path>\` works on EVERY command and may be repeated (later files win). Commands that
|
|
211
|
+
reflect your app import it, so an app that validates its environment at module scope needs that
|
|
212
|
+
environment present - without it the app aborts before nifra runs a single check. A variable already
|
|
213
|
+
set in the process environment is never overwritten by a file.
|
|
214
|
+
e.g. \`nifra check --env-file .env.local\`
|
|
215
|
+
|
|
191
216
|
${renderCommandCatalogHelp()}`
|
|
192
217
|
|
|
193
218
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
194
|
-
const CLI_VERSION = "2.
|
|
219
|
+
const CLI_VERSION = "3.2.0"
|
|
195
220
|
|
|
196
221
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
197
222
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -275,6 +300,11 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
275
300
|
)
|
|
276
301
|
}
|
|
277
302
|
const { bunfigPath, launchToken } = await writeBunDevConfig(app.cwd, app.configPath)
|
|
303
|
+
// Bun 1.4 on Windows may discard the script-relative portion of argv when `--config` is
|
|
304
|
+
// present. Keep a second, authenticated copy of the user vector in the child's environment so
|
|
305
|
+
// the configured process cannot silently fall back to `nifra`'s help and exit 0. The launch token
|
|
306
|
+
// is still the proof boundary; these arguments are only a transport, never authorization.
|
|
307
|
+
const childCliArgv = [...cliArgv()]
|
|
278
308
|
const child = Bun.spawn(
|
|
279
309
|
[
|
|
280
310
|
process.execPath,
|
|
@@ -288,14 +318,27 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
288
318
|
// One flag per condition. `--conditions=a,b` is accepted and matches nothing: Bun takes the
|
|
289
319
|
// whole string as a single condition name, so the comma form fails silently.
|
|
290
320
|
...(app.framework.conditions ?? []).map((condition) => `--conditions=${condition}`),
|
|
291
|
-
|
|
321
|
+
// Reuse the actual loaded module rather than trusting either runtime's reconstructed
|
|
322
|
+
// `argv[1]` spelling. This also works when a package-bin shim launches the CLI. `cliArgv()`
|
|
323
|
+
// is important here: Bun 1.4 on Windows can omit the script from Bun.argv, making the
|
|
324
|
+
// command start at index 1 instead of index 2. Slicing at a fixed index would silently drop
|
|
325
|
+
// `dev`, causing the child to load this module and exit successfully without starting it.
|
|
326
|
+
fileURLToPath(import.meta.url),
|
|
327
|
+
...childCliArgv,
|
|
292
328
|
],
|
|
293
329
|
{
|
|
294
|
-
|
|
330
|
+
// On Windows, inheriting the parent's already-piped stdout/stderr can make a nested Bun
|
|
331
|
+
// process exit successfully without exposing either its banner or its error. Use explicit
|
|
332
|
+
// pipes and forward them from this process instead; the caller still observes one CLI stream,
|
|
333
|
+
// while the child gets real handles it can write to on every platform.
|
|
334
|
+
stdin: "inherit",
|
|
335
|
+
stdout: "pipe",
|
|
336
|
+
stderr: "pipe",
|
|
295
337
|
env: {
|
|
296
338
|
...process.env,
|
|
297
339
|
NIFRA_BUN_DEV_TOKEN: launchToken,
|
|
298
340
|
NIFRA_BUN_DEV_DEPTH: String(depth + 1),
|
|
341
|
+
NIFRA_BUN_DEV_ARGS: JSON.stringify(childCliArgv),
|
|
299
342
|
// The child is the process that renders, so it is the one that needs dev-shaped runtimes.
|
|
300
343
|
// A library ships its dev build behind the `development` export condition with a `NODE_ENV`
|
|
301
344
|
// fallback for resolvers that set no conditions - which is Bun's runtime, where SSR runs,
|
|
@@ -307,11 +350,28 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
307
350
|
},
|
|
308
351
|
},
|
|
309
352
|
)
|
|
353
|
+
const forwarded = Promise.all([
|
|
354
|
+
forwardChildOutput(child.stdout as ReadableStream<Uint8Array>, Bun.stdout),
|
|
355
|
+
forwardChildOutput(child.stderr as ReadableStream<Uint8Array>, Bun.stderr),
|
|
356
|
+
])
|
|
310
357
|
const forward = (): void => child.kill("SIGINT")
|
|
311
358
|
process.on("SIGINT", forward)
|
|
312
359
|
process.on("SIGTERM", forward)
|
|
313
|
-
|
|
360
|
+
try {
|
|
361
|
+
const [code] = await Promise.all([child.exited, forwarded])
|
|
362
|
+
process.exitCode = code
|
|
363
|
+
} finally {
|
|
364
|
+
process.off("SIGINT", forward)
|
|
365
|
+
process.off("SIGTERM", forward)
|
|
366
|
+
}
|
|
367
|
+
return
|
|
314
368
|
}
|
|
369
|
+
// The verified child no longer needs the transport or its launch proof. Remove both from the
|
|
370
|
+
// long-lived dev process so command-line values (which may include an env-file path) are not
|
|
371
|
+
// inherited by an app-spawned subprocess or exposed through its environment diagnostics.
|
|
372
|
+
delete process.env.NIFRA_BUN_DEV_ARGS
|
|
373
|
+
delete process.env.NIFRA_BUN_DEV_TOKEN
|
|
374
|
+
delete process.env.NIFRA_BUN_DEV_DEPTH
|
|
315
375
|
// SSR runs in THIS process, on Bun's runtime - a different loader from the dev-server bundler the
|
|
316
376
|
// bunfig above configures. Both halves have to agree, so the runtime gets the same transforms:
|
|
317
377
|
//
|
|
@@ -474,6 +534,8 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
|
|
|
474
534
|
let rootPackage: Record<string, unknown>
|
|
475
535
|
try {
|
|
476
536
|
rootPackage = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>
|
|
537
|
+
// @nifra-gate-reviewed: not a security gate. An unreadable/non-JSON package.json is a separate
|
|
538
|
+
// build problem; returning here skips the freshness check rather than masking a denial.
|
|
477
539
|
} catch {
|
|
478
540
|
return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
|
|
479
541
|
}
|
|
@@ -661,10 +723,42 @@ export function assureBundleRequested(argv: readonly string[]): boolean {
|
|
|
661
723
|
)
|
|
662
724
|
}
|
|
663
725
|
|
|
726
|
+
/**
|
|
727
|
+
* A command that reflects the project imports it, and an app that validates its environment at module
|
|
728
|
+
* scope calls `process.exit` from inside that import. Nothing is throwable at that point, so the whole
|
|
729
|
+
* output is the app's own abort message with no hint that a nifra command triggered it. Name the cause
|
|
730
|
+
* on the way out and point at the flag that supplies the environment.
|
|
731
|
+
*/
|
|
732
|
+
function installReflectionExitHint(): (command: string | undefined) => void {
|
|
733
|
+
let inFlight: string | undefined
|
|
734
|
+
process.on("exit", (code) => {
|
|
735
|
+
if (inFlight === undefined || code === 0) return
|
|
736
|
+
process.stderr.write(
|
|
737
|
+
`\n[nifra] \`nifra ${inFlight}\` reflects this project by importing it, and the import exited ` +
|
|
738
|
+
`with code ${code} before the command produced a result - the message above came from your app, not nifra.\n` +
|
|
739
|
+
` If it is the app's environment validation, supply the environment and re-run:\n` +
|
|
740
|
+
` nifra ${inFlight} --env-file .env.local\n`,
|
|
741
|
+
)
|
|
742
|
+
})
|
|
743
|
+
return (command) => {
|
|
744
|
+
inFlight = command
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
664
748
|
async function main(): Promise<void> {
|
|
665
|
-
const argv =
|
|
749
|
+
const { argv: rawArgv, files: envFiles } = takeEnvFileFlags(cliArgv())
|
|
750
|
+
// Applied before any command runs: a reflecting command imports the app on its first await, and the
|
|
751
|
+
// app reads its environment at module scope, so the variables must already be in place by then.
|
|
752
|
+
if (envFiles.length > 0) await applyEnvFiles(process.cwd(), envFiles)
|
|
753
|
+
const argv = rawArgv
|
|
666
754
|
const command = argv[0]
|
|
667
755
|
if (command === undefined || command === "--help" || command === "-h" || command === "help") {
|
|
756
|
+
if (command === undefined && isBunDevReexecChild()) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
"[nifra] the configured Bun dev child lost its command arguments; refusing to serve without " +
|
|
759
|
+
"the verified dev configuration. Re-run `nifra dev`.",
|
|
760
|
+
)
|
|
761
|
+
}
|
|
668
762
|
console.log(HELP)
|
|
669
763
|
return
|
|
670
764
|
}
|
|
@@ -707,11 +801,33 @@ async function main(): Promise<void> {
|
|
|
707
801
|
)
|
|
708
802
|
return
|
|
709
803
|
}
|
|
804
|
+
// `frontend` prints the client-side footgun catalog (the human head of `nifra_frontend`).
|
|
805
|
+
// Project-independent and static. `nifra frontend` for the index, `nifra frontend vue "lost reactivity"`
|
|
806
|
+
// to filter by adapter and/or symptom (either arg may be omitted; an adapter is recognized by name).
|
|
807
|
+
if (command === "frontend") {
|
|
808
|
+
const { renderFrontendResult, parseAdapter } = await import("./frontend-guidance.ts")
|
|
809
|
+
const first = argv[1]
|
|
810
|
+
const maybeAdapter = parseAdapter(first)
|
|
811
|
+
const adapter = maybeAdapter === undefined ? undefined : first
|
|
812
|
+
const symptom = (maybeAdapter === undefined ? argv.slice(1) : argv.slice(2))
|
|
813
|
+
.filter((a) => !a.startsWith("-"))
|
|
814
|
+
.join(" ")
|
|
815
|
+
console.log(
|
|
816
|
+
renderFrontendResult({
|
|
817
|
+
adapter,
|
|
818
|
+
symptom: symptom.length > 0 ? symptom : undefined,
|
|
819
|
+
}),
|
|
820
|
+
)
|
|
821
|
+
return
|
|
822
|
+
}
|
|
710
823
|
const catalogSpec = command === undefined ? undefined : findCommandSpec(command)
|
|
711
824
|
if (catalogSpec?.transports.includes("cli")) {
|
|
825
|
+
const markReflecting = installReflectionExitHint()
|
|
712
826
|
try {
|
|
713
827
|
const input = bindCommandArgv(catalogSpec, argv.slice(1))
|
|
828
|
+
markReflecting(command)
|
|
714
829
|
const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION })
|
|
830
|
+
markReflecting(undefined)
|
|
715
831
|
const asRecord = output as unknown as Record<string, unknown>
|
|
716
832
|
const wantsJson = asRecord.json === true || argv.includes("--json")
|
|
717
833
|
if (wantsJson) {
|
|
@@ -723,6 +839,9 @@ async function main(): Promise<void> {
|
|
|
723
839
|
if (catalogSpec.success !== undefined && !catalogSpec.success(output, input))
|
|
724
840
|
process.exitCode = 1
|
|
725
841
|
} catch (err) {
|
|
842
|
+
// A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
|
|
843
|
+
// hint stays reserved for the case where the process died inside the app import.
|
|
844
|
+
markReflecting(undefined)
|
|
726
845
|
console.error(formatCliError(err))
|
|
727
846
|
process.exitCode = 1
|
|
728
847
|
}
|
|
@@ -809,11 +928,173 @@ async function main(): Promise<void> {
|
|
|
809
928
|
else await start(app, flags)
|
|
810
929
|
}
|
|
811
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Bun 1.4 on Windows can report `import.meta.main === false` for a module launched after a `--config`
|
|
933
|
+
* re-exec, even though that module is the process entry. Treat the runtime flag as authoritative when
|
|
934
|
+
* it is true, but recover from that false negative by comparing the runtime's entry path with this file.
|
|
935
|
+
* The comparison is deliberately realpath-based so a package-bin symlink is accepted while an imported
|
|
936
|
+
* copy of the CLI remains inert (tests import `parseFlags` from this module).
|
|
937
|
+
*/
|
|
938
|
+
function isMainModule(): boolean {
|
|
939
|
+
if (import.meta.main) return true
|
|
940
|
+
const target = fileURLToPath(import.meta.url)
|
|
941
|
+
/**
|
|
942
|
+
* Bun's `--config`/`--conditions` flags can sit before the script in the child argv. `argv[1]` is
|
|
943
|
+
* therefore a runtime flag on Bun 1.4/Windows, not the entry module. `Bun.main` is the authoritative
|
|
944
|
+
* runtime answer when available; the bounded argv fallback covers Bun releases that do not expose it.
|
|
945
|
+
* Only non-flag entries are considered and every match must resolve to this exact file, so an
|
|
946
|
+
* unresolved or arbitrary flag can never turn an imported CLI module into a running CLI.
|
|
947
|
+
*/
|
|
948
|
+
const bunMain = (Bun as unknown as { main?: unknown }).main
|
|
949
|
+
const argvEntries = (argv: readonly string[]): string[] => {
|
|
950
|
+
const entries: string[] = []
|
|
951
|
+
for (const value of argv.slice(1)) {
|
|
952
|
+
if (value === "--") break
|
|
953
|
+
if (value.startsWith("-")) continue
|
|
954
|
+
entries.push(value)
|
|
955
|
+
// The script is before the user's command/flags. A small bound keeps a caller's arbitrary
|
|
956
|
+
// arguments from becoming an entry-point probe while covering a config plus a few conditions.
|
|
957
|
+
if (entries.length === 4) break
|
|
958
|
+
}
|
|
959
|
+
return entries
|
|
960
|
+
}
|
|
961
|
+
const entries = [
|
|
962
|
+
...(typeof bunMain === "string" ? [bunMain] : []),
|
|
963
|
+
...argvEntries(Bun.argv),
|
|
964
|
+
...argvEntries(process.argv),
|
|
965
|
+
]
|
|
966
|
+
const canonical = (value: string): Set<string> => {
|
|
967
|
+
const paths = new Set<string>()
|
|
968
|
+
const add = (path: string): void => {
|
|
969
|
+
const slashed = path
|
|
970
|
+
.replaceAll("\\", "/")
|
|
971
|
+
.replace(/^\/\/\?\/UNC\//i, "//")
|
|
972
|
+
.replace(/^\/\/\?\//, "")
|
|
973
|
+
paths.add(process.platform === "win32" ? slashed.toLowerCase() : slashed)
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
add(resolve(value))
|
|
977
|
+
} catch {
|
|
978
|
+
return paths
|
|
979
|
+
}
|
|
980
|
+
for (const resolver of [realpathSync.native, realpathSync]) {
|
|
981
|
+
try {
|
|
982
|
+
add(resolver(value))
|
|
983
|
+
} catch {
|
|
984
|
+
// A virtual/synthetic argv entry has no realpath; it is not safe evidence of main-ness.
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return paths
|
|
988
|
+
}
|
|
989
|
+
const targetPaths = canonical(target)
|
|
990
|
+
for (const entry of entries) {
|
|
991
|
+
if (entry !== "" && [...canonical(entry)].some((path) => targetPaths.has(path))) return true
|
|
992
|
+
}
|
|
993
|
+
return false
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Bun 1.4 on Windows can omit the script path from both argv vectors when a TS file is launched by
|
|
998
|
+
* `Bun.spawn`. The command token is a safe last-resort discriminator: a test importing this module
|
|
999
|
+
* sees test-file paths and runner flags, not an exact Nifra command, while a direct CLI invocation
|
|
1000
|
+
* has one of the manual or catalogued command names below.
|
|
1001
|
+
*/
|
|
1002
|
+
function isCliCommandToken(value: string): boolean {
|
|
1003
|
+
return (
|
|
1004
|
+
value === "dev" ||
|
|
1005
|
+
value === "build" ||
|
|
1006
|
+
value === "start" ||
|
|
1007
|
+
value === "mcp" ||
|
|
1008
|
+
value === "init-agents" ||
|
|
1009
|
+
value === "upgrade" ||
|
|
1010
|
+
value === "help" ||
|
|
1011
|
+
value === "--help" ||
|
|
1012
|
+
value === "-h" ||
|
|
1013
|
+
value === "--version" ||
|
|
1014
|
+
value === "-v" ||
|
|
1015
|
+
findCommandSpec(value) !== undefined
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function findCliCommand(values: readonly string[]): number {
|
|
1020
|
+
// Most Bun/Node argv vectors reserve index 0 for the runtime and index 1 for the script. Bun 1.4
|
|
1021
|
+
// on Windows can omit the script for a directly spawned TS entry, leaving the command at index 0.
|
|
1022
|
+
// Accept that one exact-token shape while keeping arbitrary imported-module argv inert.
|
|
1023
|
+
return values.findIndex((value) => isCliCommandToken(value))
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function cliArgv(): readonly string[] {
|
|
1027
|
+
const reexec = reexecCliArgv()
|
|
1028
|
+
if (reexec !== undefined) return reexec
|
|
1029
|
+
for (const values of [Bun.argv, process.argv]) {
|
|
1030
|
+
const commandIndex = findCliCommand(values)
|
|
1031
|
+
if (commandIndex >= 0) return values.slice(commandIndex)
|
|
1032
|
+
}
|
|
1033
|
+
return Bun.argv.slice(2)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Recover the original command vector when Bun's Windows `--config` launch drops it from both public
|
|
1038
|
+
* argv arrays. This is deliberately narrow: only a positive re-exec marker can enable the channel, the
|
|
1039
|
+
* first item must be a real Nifra command, and malformed/oversized data is ignored so an inherited
|
|
1040
|
+
* environment can never turn an imported CLI module into an arbitrary command runner.
|
|
1041
|
+
*/
|
|
1042
|
+
function reexecCliArgv(): readonly string[] | undefined {
|
|
1043
|
+
if (!isBunDevReexecChild()) return undefined
|
|
1044
|
+
const encoded = process.env.NIFRA_BUN_DEV_ARGS
|
|
1045
|
+
if (encoded === undefined || encoded.length > 64 * 1024) return undefined
|
|
1046
|
+
let parsed: unknown
|
|
1047
|
+
try {
|
|
1048
|
+
parsed = JSON.parse(encoded)
|
|
1049
|
+
} catch {
|
|
1050
|
+
return undefined
|
|
1051
|
+
}
|
|
1052
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 128) return undefined
|
|
1053
|
+
if (
|
|
1054
|
+
!parsed.every(
|
|
1055
|
+
(value) => typeof value === "string" && value.length <= 16 * 1024 && !value.includes("\0"),
|
|
1056
|
+
)
|
|
1057
|
+
) {
|
|
1058
|
+
return undefined
|
|
1059
|
+
}
|
|
1060
|
+
const command = parsed[0]
|
|
1061
|
+
if (typeof command !== "string") return undefined
|
|
1062
|
+
return isCliCommandToken(command) ? (parsed as string[]) : undefined
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function hasCliCommandToken(): boolean {
|
|
1066
|
+
return findCliCommand(Bun.argv) >= 0 || findCliCommand(process.argv) >= 0
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Bun 1.4/Windows can load the explicit script after a `--config` re-exec while reporting neither a
|
|
1071
|
+
* usable `import.meta.main` nor the script in its public argv vectors. The child still carries the
|
|
1072
|
+
* parent-generated launch proof and a positive depth; accepting that narrow marker lets it enter
|
|
1073
|
+
* `main()`, where `consumeLaunchToken()` remains the fail-closed gate before the configured server can
|
|
1074
|
+
* run. A forged marker only causes the normal token-mismatch relaunch/error path - it cannot bypass the
|
|
1075
|
+
* boundary plugin proof.
|
|
1076
|
+
*/
|
|
1077
|
+
function isBunDevReexecChild(): boolean {
|
|
1078
|
+
const depth = Number(process.env.NIFRA_BUN_DEV_DEPTH)
|
|
1079
|
+
return (
|
|
1080
|
+
process.env.NIFRA_BUN_DEV_TOKEN !== undefined &&
|
|
1081
|
+
process.env.NIFRA_BUN_DEV_TOKEN !== "" &&
|
|
1082
|
+
Number.isSafeInteger(depth) &&
|
|
1083
|
+
depth > 0
|
|
1084
|
+
)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
812
1087
|
// Only run the CLI when invoked as the entry (`bun cli.ts …`), not when a test imports it for the
|
|
813
|
-
// exported `parseFlags`.
|
|
814
|
-
if (
|
|
815
|
-
|
|
1088
|
+
// exported `parseFlags`.
|
|
1089
|
+
if (isMainModule() || isBunDevReexecChild() || hasCliCommandToken()) {
|
|
1090
|
+
// Keep the entry module alive until the async CLI has completed. Bun 1.4 on Windows can otherwise
|
|
1091
|
+
// terminate a process created by Bun.spawn while its first dynamic import is pending, leaving an
|
|
1092
|
+
// exit-0 child with no output. Imported test modules never enter this branch, so the CLI's top-level
|
|
1093
|
+
// await cannot start a server merely because a test imports a helper from this file.
|
|
1094
|
+
try {
|
|
1095
|
+
await main()
|
|
1096
|
+
} catch (err) {
|
|
816
1097
|
console.error(formatCliError(err))
|
|
817
1098
|
process.exitCode = 1
|
|
818
|
-
}
|
|
1099
|
+
}
|
|
819
1100
|
}
|
package/src/command-catalog.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { LoadedApp } from "./load.ts"
|
|
|
21
21
|
import type { ManifestEmitCommandResult } from "./manifest-tool.ts"
|
|
22
22
|
import { collectPortResult, type PortResult, renderReport } from "./port.ts"
|
|
23
23
|
import type { ReplayResult } from "./replay.ts"
|
|
24
|
+
import type { StylexMigrationResult } from "./stylex-migrate.ts"
|
|
24
25
|
import {
|
|
25
26
|
collectProjectWorkGraph,
|
|
26
27
|
type ProjectWorkGraphResult,
|
|
@@ -298,6 +299,12 @@ interface ContextInput {
|
|
|
298
299
|
readonly dir?: string | undefined
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
interface OpenApiInput {
|
|
303
|
+
readonly format?: "json" | "yaml" | undefined
|
|
304
|
+
readonly path?: string | undefined
|
|
305
|
+
readonly dir?: string | undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
301
308
|
interface DoctorInput {
|
|
302
309
|
readonly json?: boolean | undefined
|
|
303
310
|
readonly autoFix?: boolean | undefined
|
|
@@ -312,6 +319,14 @@ interface FixInput {
|
|
|
312
319
|
readonly dir?: string | undefined
|
|
313
320
|
}
|
|
314
321
|
|
|
322
|
+
interface MigrateInput {
|
|
323
|
+
readonly from: "tailwind"
|
|
324
|
+
readonly to: "stylex"
|
|
325
|
+
readonly write?: boolean | undefined
|
|
326
|
+
readonly json?: boolean | undefined
|
|
327
|
+
readonly dir?: string | undefined
|
|
328
|
+
}
|
|
329
|
+
|
|
315
330
|
interface SnapshotInput {
|
|
316
331
|
readonly out?: string | undefined
|
|
317
332
|
readonly json?: boolean | undefined
|
|
@@ -373,11 +388,20 @@ interface ContextCommandOutput {
|
|
|
373
388
|
readonly ok: true
|
|
374
389
|
readonly text: string
|
|
375
390
|
}
|
|
391
|
+
interface OpenApiCommandOutput {
|
|
392
|
+
readonly ok: true
|
|
393
|
+
readonly format: "json" | "yaml"
|
|
394
|
+
readonly text: string
|
|
395
|
+
}
|
|
376
396
|
interface FixCommandOutput {
|
|
377
397
|
readonly ok: boolean
|
|
378
398
|
readonly changed: readonly string[]
|
|
399
|
+
/** Recipes that refused, with the reason. A recipe that cannot act says so here instead of
|
|
400
|
+
* aborting the run or - worse - reporting nothing at all. */
|
|
401
|
+
readonly failed: readonly { readonly code: string; readonly reason: string }[]
|
|
379
402
|
readonly diagnostics: readonly unknown[]
|
|
380
403
|
}
|
|
404
|
+
interface MigrateCommandOutput extends StylexMigrationResult {}
|
|
381
405
|
interface SnapshotCommandOutput {
|
|
382
406
|
readonly ok: true
|
|
383
407
|
readonly file: string
|
|
@@ -569,6 +593,24 @@ const CONTEXT_SCHEMA = input<ContextInput>(
|
|
|
569
593
|
},
|
|
570
594
|
)
|
|
571
595
|
|
|
596
|
+
const OPENAPI_SCHEMA = input<OpenApiInput>(
|
|
597
|
+
objectSchema({
|
|
598
|
+
format: { type: "string", enum: ["json", "yaml"] },
|
|
599
|
+
path: { type: "string" },
|
|
600
|
+
dir: { type: "string" },
|
|
601
|
+
}),
|
|
602
|
+
(value) => {
|
|
603
|
+
const raw = withDir(record(value))
|
|
604
|
+
if (raw.format !== undefined && raw.format !== "json" && raw.format !== "yaml")
|
|
605
|
+
throw new TypeError("format must be json or yaml")
|
|
606
|
+
return {
|
|
607
|
+
format: (raw.format ?? "json") as "json" | "yaml",
|
|
608
|
+
path: optionalString(raw.path, "path"),
|
|
609
|
+
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
)
|
|
613
|
+
|
|
572
614
|
const DOCTOR_SCHEMA = input<DoctorInput>(
|
|
573
615
|
objectSchema({
|
|
574
616
|
json: { type: "boolean" },
|
|
@@ -599,6 +641,32 @@ const FIX_SCHEMA = input<FixInput>(
|
|
|
599
641
|
},
|
|
600
642
|
)
|
|
601
643
|
|
|
644
|
+
const MIGRATE_SCHEMA = input<MigrateInput>(
|
|
645
|
+
objectSchema(
|
|
646
|
+
{
|
|
647
|
+
from: { type: "string", enum: ["tailwind"] },
|
|
648
|
+
to: { type: "string", enum: ["stylex"] },
|
|
649
|
+
write: { type: "boolean" },
|
|
650
|
+
json: { type: "boolean" },
|
|
651
|
+
dir: { type: "string" },
|
|
652
|
+
},
|
|
653
|
+
["from", "to"],
|
|
654
|
+
),
|
|
655
|
+
(value) => {
|
|
656
|
+
const raw = withDir(record(value))
|
|
657
|
+
const from = optionalString(raw.from, "from")
|
|
658
|
+
const to = optionalString(raw.to, "to")
|
|
659
|
+
if (from !== "tailwind") throw new TypeError("from must be tailwind")
|
|
660
|
+
if (to !== "stylex") throw new TypeError("to must be stylex")
|
|
661
|
+
return {
|
|
662
|
+
from,
|
|
663
|
+
to,
|
|
664
|
+
...parseBooleanFlags(raw, ["write", "json"]),
|
|
665
|
+
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
)
|
|
669
|
+
|
|
602
670
|
const SNAPSHOT_SCHEMA = input<SnapshotInput>(
|
|
603
671
|
objectSchema({ out: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }),
|
|
604
672
|
(value) => {
|
|
@@ -1024,6 +1092,38 @@ const contextSpec: CommandSpec<ContextInput, ContextCommandOutput> = {
|
|
|
1024
1092
|
render: (out) => [out.text],
|
|
1025
1093
|
}
|
|
1026
1094
|
|
|
1095
|
+
const openApiSpec: CommandSpec<OpenApiInput, OpenApiCommandOutput> = {
|
|
1096
|
+
name: "openapi",
|
|
1097
|
+
summary:
|
|
1098
|
+
"Generate the backend OpenAPI document, including supported build-time response inference.",
|
|
1099
|
+
input: OPENAPI_SCHEMA,
|
|
1100
|
+
output: output({
|
|
1101
|
+
type: "object",
|
|
1102
|
+
properties: { ok: { type: "boolean" }, format: { type: "string" }, text: { type: "string" } },
|
|
1103
|
+
required: ["ok", "format", "text"],
|
|
1104
|
+
}),
|
|
1105
|
+
transports: ["cli"],
|
|
1106
|
+
stability: "stable",
|
|
1107
|
+
argv: {
|
|
1108
|
+
flags: [
|
|
1109
|
+
{ name: "format", field: "format", type: "string" },
|
|
1110
|
+
{ name: "path", field: "path", type: "string" },
|
|
1111
|
+
],
|
|
1112
|
+
},
|
|
1113
|
+
async run(value, ctx) {
|
|
1114
|
+
const app = await loadAppFor(ctx)
|
|
1115
|
+
const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
|
|
1116
|
+
const format = value.format ?? "json"
|
|
1117
|
+
return {
|
|
1118
|
+
ok: true,
|
|
1119
|
+
format,
|
|
1120
|
+
text: await renderOpenApiWithTypes(app, format, value.path),
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
render: (out) => [out.text],
|
|
1124
|
+
json: (out) => (out.format === "json" ? JSON.parse(out.text) : out.text),
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1027
1127
|
const doctorSpec: CommandSpec<DoctorInput, DoctorResult> = {
|
|
1028
1128
|
name: "doctor",
|
|
1029
1129
|
summary: "Find undeclared imports, duplicate identity installs, and pipeline readiness drift.",
|
|
@@ -1071,6 +1171,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1071
1171
|
properties: {
|
|
1072
1172
|
ok: { type: "boolean" },
|
|
1073
1173
|
changed: { type: "array" },
|
|
1174
|
+
failed: { type: "array" },
|
|
1074
1175
|
diagnostics: { type: "array" },
|
|
1075
1176
|
},
|
|
1076
1177
|
required: ["ok", "diagnostics"],
|
|
@@ -1091,23 +1192,95 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1091
1192
|
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
1092
1193
|
})
|
|
1093
1194
|
const changed: string[] = []
|
|
1195
|
+
const failed: { code: string; reason: string }[] = []
|
|
1094
1196
|
for (const diagnostic of result.structuredDiagnostics ?? [])
|
|
1095
1197
|
if (value.code === undefined || diagnostic.code === value.code)
|
|
1096
|
-
|
|
1198
|
+
// One recipe that cannot act must not cancel the others, and must not pass silently either:
|
|
1199
|
+
// record why and keep going, so a run over many diagnostics still reports every outcome.
|
|
1200
|
+
try {
|
|
1201
|
+
changed.push(...(await applyDiagnosticRecipe(ctx.cwd, diagnostic)))
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
failed.push({
|
|
1204
|
+
code: diagnostic.code,
|
|
1205
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1206
|
+
})
|
|
1207
|
+
}
|
|
1097
1208
|
const final = await collectCheckResult(ctx.cwd, {
|
|
1098
1209
|
lintsOnly: true,
|
|
1099
1210
|
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
1100
1211
|
})
|
|
1101
|
-
return {
|
|
1212
|
+
return {
|
|
1213
|
+
ok: final.ok,
|
|
1214
|
+
changed,
|
|
1215
|
+
failed,
|
|
1216
|
+
diagnostics: final.structuredDiagnostics ?? final.diagnostics,
|
|
1217
|
+
}
|
|
1102
1218
|
},
|
|
1103
1219
|
render: (out) => [
|
|
1104
1220
|
out.ok ? "✓ fixes applied" : "✖ unresolved diagnostics remain",
|
|
1105
1221
|
...out.changed.map((file) => ` changed ${file}`),
|
|
1222
|
+
...out.failed.map((f) => ` could not fix ${f.code}: ${f.reason}`),
|
|
1106
1223
|
...out.diagnostics.map((d) => ` ${String((d as { code?: unknown }).code ?? "diagnostic")}`),
|
|
1107
1224
|
],
|
|
1108
1225
|
success: (out) => out.ok,
|
|
1109
1226
|
}
|
|
1110
1227
|
|
|
1228
|
+
const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
1229
|
+
name: "migrate",
|
|
1230
|
+
summary: "Migrate safe static Tailwind className utilities to native StyleX props.",
|
|
1231
|
+
input: MIGRATE_SCHEMA,
|
|
1232
|
+
output: output({
|
|
1233
|
+
type: "object",
|
|
1234
|
+
properties: {
|
|
1235
|
+
ok: { type: "boolean" },
|
|
1236
|
+
from: { type: "string" },
|
|
1237
|
+
to: { type: "string" },
|
|
1238
|
+
write: { type: "boolean" },
|
|
1239
|
+
scanned: { type: "integer" },
|
|
1240
|
+
changed: { type: "array" },
|
|
1241
|
+
written: { type: "array" },
|
|
1242
|
+
issues: { type: "array" },
|
|
1243
|
+
files: { type: "array" },
|
|
1244
|
+
},
|
|
1245
|
+
required: ["ok", "from", "to", "write", "scanned", "changed", "written", "issues", "files"],
|
|
1246
|
+
}),
|
|
1247
|
+
transports: ["cli"],
|
|
1248
|
+
stability: "stable",
|
|
1249
|
+
argv: {
|
|
1250
|
+
flags: [
|
|
1251
|
+
{ name: "from", field: "from", type: "string" },
|
|
1252
|
+
{ name: "to", field: "to", type: "string" },
|
|
1253
|
+
{ name: "write", field: "write", type: "boolean" },
|
|
1254
|
+
{ name: "json", field: "json", type: "boolean" },
|
|
1255
|
+
{ name: "dir", field: "dir", type: "string" },
|
|
1256
|
+
],
|
|
1257
|
+
},
|
|
1258
|
+
async run(value, ctx) {
|
|
1259
|
+
const { migrateTailwindToStylex } = await import("./stylex-migrate.ts")
|
|
1260
|
+
return migrateTailwindToStylex(resolve(ctx.cwd, value.dir ?? "."), {
|
|
1261
|
+
...(value.write === undefined ? {} : { write: value.write }),
|
|
1262
|
+
})
|
|
1263
|
+
},
|
|
1264
|
+
render: (out) => [
|
|
1265
|
+
out.ok
|
|
1266
|
+
? `✓ migrated ${out.changed.length} file${out.changed.length === 1 ? "" : "s"}`
|
|
1267
|
+
: `⚠ migrated ${out.changed.length} safe file${out.changed.length === 1 ? "" : "s"}; ${out.issues.length} manual issue${out.issues.length === 1 ? "" : "s"} remain`,
|
|
1268
|
+
` scanned ${out.scanned} source file${out.scanned === 1 ? "" : "s"}`,
|
|
1269
|
+
...(out.write
|
|
1270
|
+
? [` wrote ${out.written.length} file${out.written.length === 1 ? "" : "s"}`]
|
|
1271
|
+
: out.changed.length > 0
|
|
1272
|
+
? [" dry run: pass --write to apply the safe changes"]
|
|
1273
|
+
: []),
|
|
1274
|
+
...out.issues
|
|
1275
|
+
.slice(0, 30)
|
|
1276
|
+
.map((issue) => ` ${issue.file}:${issue.line} ${issue.token} — ${issue.reason}`),
|
|
1277
|
+
...(out.issues.length > 30
|
|
1278
|
+
? [` … ${out.issues.length - 30} more issue${out.issues.length - 30 === 1 ? "" : "s"}`]
|
|
1279
|
+
: []),
|
|
1280
|
+
],
|
|
1281
|
+
success: (out) => out.ok,
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1111
1284
|
const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
|
|
1112
1285
|
name: "snapshot",
|
|
1113
1286
|
summary: "Write the backend API contract as a versioned JSON baseline.",
|
|
@@ -1345,8 +1518,10 @@ export const commandSpecs = Object.freeze([
|
|
|
1345
1518
|
manifestSpec,
|
|
1346
1519
|
routesSpec,
|
|
1347
1520
|
contextSpec,
|
|
1521
|
+
openApiSpec,
|
|
1348
1522
|
doctorSpec,
|
|
1349
1523
|
fixSpec,
|
|
1524
|
+
migrateSpec,
|
|
1350
1525
|
snapshotSpec,
|
|
1351
1526
|
diffSpec,
|
|
1352
1527
|
contractsSpec,
|