@nifrajs/cli 3.1.0 → 3.3.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 -2366
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +247 -10
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +21 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +122 -0
- package/dist/command-catalog.js.map +1 -1
- package/dist/env-file.js +5 -5
- package/dist/env-file.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/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-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 -92
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +13 -1232
- 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/release-verification.d.ts +4 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +6 -138
- 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/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/docs/examples.json +46 -4
- package/docs/llms-full.txt +1346 -28
- package/docs/types.json +4185 -87
- 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 -2752
- package/src/cli.ts +252 -10
- package/src/command-catalog.ts +155 -0
- package/src/env-file.ts +5 -5
- package/src/frontend-guidance.ts +259 -0
- 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 -1511
- 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/release-verification.ts +22 -164
- package/src/rules/codes.ts +4 -0
- package/src/rules/islands.ts +171 -0
- package/src/rules/nano.ts +295 -0
- 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/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,
|
|
@@ -47,6 +48,23 @@ export interface Flags {
|
|
|
47
48
|
readonly allowDuplicateIdentity: boolean
|
|
48
49
|
}
|
|
49
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
|
+
|
|
50
68
|
const HELP = `nifra - zero-config dev/build/start for a nifra app
|
|
51
69
|
|
|
52
70
|
Usage:
|
|
@@ -198,7 +216,7 @@ set in the process environment is never overwritten by a file.
|
|
|
198
216
|
${renderCommandCatalogHelp()}`
|
|
199
217
|
|
|
200
218
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
201
|
-
const CLI_VERSION = "3.
|
|
219
|
+
const CLI_VERSION = "3.3.0"
|
|
202
220
|
|
|
203
221
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
204
222
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -282,6 +300,11 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
282
300
|
)
|
|
283
301
|
}
|
|
284
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()]
|
|
285
308
|
const child = Bun.spawn(
|
|
286
309
|
[
|
|
287
310
|
process.execPath,
|
|
@@ -295,14 +318,27 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
295
318
|
// One flag per condition. `--conditions=a,b` is accepted and matches nothing: Bun takes the
|
|
296
319
|
// whole string as a single condition name, so the comma form fails silently.
|
|
297
320
|
...(app.framework.conditions ?? []).map((condition) => `--conditions=${condition}`),
|
|
298
|
-
|
|
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,
|
|
299
328
|
],
|
|
300
329
|
{
|
|
301
|
-
|
|
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",
|
|
302
337
|
env: {
|
|
303
338
|
...process.env,
|
|
304
339
|
NIFRA_BUN_DEV_TOKEN: launchToken,
|
|
305
340
|
NIFRA_BUN_DEV_DEPTH: String(depth + 1),
|
|
341
|
+
NIFRA_BUN_DEV_ARGS: JSON.stringify(childCliArgv),
|
|
306
342
|
// The child is the process that renders, so it is the one that needs dev-shaped runtimes.
|
|
307
343
|
// A library ships its dev build behind the `development` export condition with a `NODE_ENV`
|
|
308
344
|
// fallback for resolvers that set no conditions - which is Bun's runtime, where SSR runs,
|
|
@@ -314,11 +350,28 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
314
350
|
},
|
|
315
351
|
},
|
|
316
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
|
+
])
|
|
317
357
|
const forward = (): void => child.kill("SIGINT")
|
|
318
358
|
process.on("SIGINT", forward)
|
|
319
359
|
process.on("SIGTERM", forward)
|
|
320
|
-
|
|
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
|
|
321
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
|
|
322
375
|
// SSR runs in THIS process, on Bun's runtime - a different loader from the dev-server bundler the
|
|
323
376
|
// bunfig above configures. Both halves have to agree, so the runtime gets the same transforms:
|
|
324
377
|
//
|
|
@@ -481,6 +534,8 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
|
|
|
481
534
|
let rootPackage: Record<string, unknown>
|
|
482
535
|
try {
|
|
483
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.
|
|
484
539
|
} catch {
|
|
485
540
|
return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
|
|
486
541
|
}
|
|
@@ -691,13 +746,19 @@ function installReflectionExitHint(): (command: string | undefined) => void {
|
|
|
691
746
|
}
|
|
692
747
|
|
|
693
748
|
async function main(): Promise<void> {
|
|
694
|
-
const { argv: rawArgv, files: envFiles } = takeEnvFileFlags(
|
|
749
|
+
const { argv: rawArgv, files: envFiles } = takeEnvFileFlags(cliArgv())
|
|
695
750
|
// Applied before any command runs: a reflecting command imports the app on its first await, and the
|
|
696
751
|
// app reads its environment at module scope, so the variables must already be in place by then.
|
|
697
752
|
if (envFiles.length > 0) await applyEnvFiles(process.cwd(), envFiles)
|
|
698
753
|
const argv = rawArgv
|
|
699
754
|
const command = argv[0]
|
|
700
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
|
+
}
|
|
701
762
|
console.log(HELP)
|
|
702
763
|
return
|
|
703
764
|
}
|
|
@@ -740,6 +801,25 @@ async function main(): Promise<void> {
|
|
|
740
801
|
)
|
|
741
802
|
return
|
|
742
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
|
+
}
|
|
743
823
|
const catalogSpec = command === undefined ? undefined : findCommandSpec(command)
|
|
744
824
|
if (catalogSpec?.transports.includes("cli")) {
|
|
745
825
|
const markReflecting = installReflectionExitHint()
|
|
@@ -848,11 +928,173 @@ async function main(): Promise<void> {
|
|
|
848
928
|
else await start(app, flags)
|
|
849
929
|
}
|
|
850
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
|
+
|
|
851
1087
|
// Only run the CLI when invoked as the entry (`bun cli.ts …`), not when a test imports it for the
|
|
852
|
-
// exported `parseFlags`.
|
|
853
|
-
if (
|
|
854
|
-
|
|
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) {
|
|
855
1097
|
console.error(formatCliError(err))
|
|
856
1098
|
process.exitCode = 1
|
|
857
|
-
}
|
|
1099
|
+
}
|
|
858
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,6 +388,11 @@ 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[]
|
|
@@ -381,6 +401,7 @@ interface FixCommandOutput {
|
|
|
381
401
|
readonly failed: readonly { readonly code: string; readonly reason: string }[]
|
|
382
402
|
readonly diagnostics: readonly unknown[]
|
|
383
403
|
}
|
|
404
|
+
interface MigrateCommandOutput extends StylexMigrationResult {}
|
|
384
405
|
interface SnapshotCommandOutput {
|
|
385
406
|
readonly ok: true
|
|
386
407
|
readonly file: string
|
|
@@ -572,6 +593,24 @@ const CONTEXT_SCHEMA = input<ContextInput>(
|
|
|
572
593
|
},
|
|
573
594
|
)
|
|
574
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
|
+
|
|
575
614
|
const DOCTOR_SCHEMA = input<DoctorInput>(
|
|
576
615
|
objectSchema({
|
|
577
616
|
json: { type: "boolean" },
|
|
@@ -602,6 +641,32 @@ const FIX_SCHEMA = input<FixInput>(
|
|
|
602
641
|
},
|
|
603
642
|
)
|
|
604
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
|
+
|
|
605
670
|
const SNAPSHOT_SCHEMA = input<SnapshotInput>(
|
|
606
671
|
objectSchema({ out: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }),
|
|
607
672
|
(value) => {
|
|
@@ -1027,6 +1092,38 @@ const contextSpec: CommandSpec<ContextInput, ContextCommandOutput> = {
|
|
|
1027
1092
|
render: (out) => [out.text],
|
|
1028
1093
|
}
|
|
1029
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
|
+
|
|
1030
1127
|
const doctorSpec: CommandSpec<DoctorInput, DoctorResult> = {
|
|
1031
1128
|
name: "doctor",
|
|
1032
1129
|
summary: "Find undeclared imports, duplicate identity installs, and pipeline readiness drift.",
|
|
@@ -1128,6 +1225,62 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1128
1225
|
success: (out) => out.ok,
|
|
1129
1226
|
}
|
|
1130
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
|
+
|
|
1131
1284
|
const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
|
|
1132
1285
|
name: "snapshot",
|
|
1133
1286
|
summary: "Write the backend API contract as a versioned JSON baseline.",
|
|
@@ -1365,8 +1518,10 @@ export const commandSpecs = Object.freeze([
|
|
|
1365
1518
|
manifestSpec,
|
|
1366
1519
|
routesSpec,
|
|
1367
1520
|
contextSpec,
|
|
1521
|
+
openApiSpec,
|
|
1368
1522
|
doctorSpec,
|
|
1369
1523
|
fixSpec,
|
|
1524
|
+
migrateSpec,
|
|
1370
1525
|
snapshotSpec,
|
|
1371
1526
|
diffSpec,
|
|
1372
1527
|
contractsSpec,
|
package/src/env-file.ts
CHANGED
|
@@ -68,8 +68,8 @@ export function takeEnvFileFlags(argv: readonly string[]): {
|
|
|
68
68
|
const rest: string[] = []
|
|
69
69
|
const files: string[] = []
|
|
70
70
|
for (let index = 0; index < argv.length; index++) {
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
71
|
+
const arg = argv[index] as string
|
|
72
|
+
if (arg === ENV_FILE_FLAG) {
|
|
73
73
|
const value = argv[index + 1]
|
|
74
74
|
if (value === undefined || value.startsWith("-")) {
|
|
75
75
|
throw new Error(`[nifra] ${ENV_FILE_FLAG} needs a path, e.g. ${ENV_FILE_FLAG} .env.local`)
|
|
@@ -78,15 +78,15 @@ export function takeEnvFileFlags(argv: readonly string[]): {
|
|
|
78
78
|
index++
|
|
79
79
|
continue
|
|
80
80
|
}
|
|
81
|
-
if (
|
|
82
|
-
const value =
|
|
81
|
+
if (arg.startsWith(`${ENV_FILE_FLAG}=`)) {
|
|
82
|
+
const value = arg.slice(ENV_FILE_FLAG.length + 1)
|
|
83
83
|
if (value === "") {
|
|
84
84
|
throw new Error(`[nifra] ${ENV_FILE_FLAG} needs a path, e.g. ${ENV_FILE_FLAG}=.env.local`)
|
|
85
85
|
}
|
|
86
86
|
files.push(value)
|
|
87
87
|
continue
|
|
88
88
|
}
|
|
89
|
-
rest.push(
|
|
89
|
+
rest.push(arg)
|
|
90
90
|
}
|
|
91
91
|
return { argv: rest, files }
|
|
92
92
|
}
|