@nifrajs/cli 3.5.0 → 4.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.
- package/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +57 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +270 -26
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.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 +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.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/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +374 -38
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/src/contracts.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
snapshotProjectEvidence,
|
|
7
7
|
} from "@nifrajs/core/evidence"
|
|
8
8
|
import type { ReflectedRoute } from "@nifrajs/core/reflection"
|
|
9
|
+
import { BACKEND_APP_FILE } from "./app-files.ts"
|
|
10
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
9
11
|
|
|
10
12
|
export interface ContractDigest {
|
|
11
13
|
readonly request: string
|
|
@@ -26,18 +28,58 @@ function recordOf(value: unknown): Record<string, unknown> | undefined {
|
|
|
26
28
|
: undefined
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/** Annotation keywords: editing one is not a contract change. */
|
|
32
|
+
const ANNOTATIONS: ReadonlySet<string> = new Set([
|
|
33
|
+
"description",
|
|
34
|
+
"title",
|
|
35
|
+
"default",
|
|
36
|
+
"example",
|
|
37
|
+
"examples",
|
|
38
|
+
])
|
|
39
|
+
/** Keywords whose value maps user-chosen names to schemas, so a key there is a name, never a keyword. */
|
|
40
|
+
const NAME_MAPS: ReadonlySet<string> = new Set([
|
|
41
|
+
"properties",
|
|
42
|
+
"patternProperties",
|
|
43
|
+
"$defs",
|
|
44
|
+
"definitions",
|
|
45
|
+
"dependentSchemas",
|
|
46
|
+
"dependentRequired",
|
|
47
|
+
"dependencies",
|
|
48
|
+
])
|
|
49
|
+
/** Keywords whose value is instance data, compared whole. */
|
|
50
|
+
const DATA: ReadonlySet<string> = new Set(["const", "enum"])
|
|
51
|
+
|
|
52
|
+
function sortedRecord(
|
|
53
|
+
record: Record<string, unknown>,
|
|
54
|
+
entry: (name: string, item: unknown) => unknown,
|
|
55
|
+
): Record<string, unknown> {
|
|
56
|
+
const entries: Array<[string, unknown]> = []
|
|
57
|
+
for (const name of Object.keys(record).sort()) {
|
|
58
|
+
const item = entry(name, record[name])
|
|
59
|
+
if (item !== undefined) entries.push([name, item])
|
|
60
|
+
}
|
|
61
|
+
// `fromEntries` defines each key, where an assignment would run the `__proto__` setter instead.
|
|
62
|
+
return Object.fromEntries(entries)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function canonicalData(value: unknown): unknown {
|
|
66
|
+
if (Array.isArray(value)) return value.map((item) => canonicalData(item))
|
|
67
|
+
const record = recordOf(value)
|
|
68
|
+
return record === undefined ? value : sortedRecord(record, (_name, item) => canonicalData(item))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonical(value: unknown): unknown {
|
|
32
72
|
if (Array.isArray(value)) return value.map((item) => canonical(item))
|
|
33
73
|
const record = recordOf(value)
|
|
34
74
|
if (record === undefined) return value
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
return sortedRecord(record, (name, item) => {
|
|
76
|
+
if (ANNOTATIONS.has(name)) return undefined
|
|
77
|
+
if (DATA.has(name)) return canonicalData(item)
|
|
78
|
+
const names = NAME_MAPS.has(name) ? recordOf(item) : undefined
|
|
79
|
+
return names === undefined
|
|
80
|
+
? canonical(item)
|
|
81
|
+
: sortedRecord(names, (_key, schema) => canonical(schema))
|
|
82
|
+
})
|
|
41
83
|
}
|
|
42
84
|
|
|
43
85
|
async function sha256(value: string): Promise<string> {
|
|
@@ -104,13 +146,13 @@ export async function buildContractsLock(
|
|
|
104
146
|
)
|
|
105
147
|
return {
|
|
106
148
|
version: 1,
|
|
107
|
-
routes: Object.fromEntries(Object.entries(routes).sort(([a], [b]) => a
|
|
149
|
+
routes: Object.fromEntries(Object.entries(routes).sort(([a], [b]) => codeUnitOrder(a, b))),
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
152
|
|
|
111
153
|
async function loadBackend(cwd: string): Promise<unknown> {
|
|
112
|
-
const backendPath = resolve(cwd,
|
|
113
|
-
if (!existsSync(backendPath)) throw new Error(`[nifra] no
|
|
154
|
+
const backendPath = resolve(cwd, BACKEND_APP_FILE)
|
|
155
|
+
if (!existsSync(backendPath)) throw new Error(`[nifra] no ${BACKEND_APP_FILE} in ${cwd}`)
|
|
114
156
|
const loadedValue: unknown = await import(backendPath)
|
|
115
157
|
const loaded = recordOf(loadedValue)
|
|
116
158
|
if (loaded?.backend === undefined)
|
package/src/db-audit.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.nifra/db-audit.jsonl`: one line per `nifra db` call - what ran, never what came back. Each entry
|
|
3
|
+
* carries the SQL (secrets redacted), its fingerprint, the row count, the duration and the refusal
|
|
4
|
+
* code. The file is owner-only, never followed through a symlink, and rotates at 1 MB to
|
|
5
|
+
* `db-audit.1.jsonl`, so it stays bounded.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "node:crypto"
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
closeSync,
|
|
12
|
+
constants,
|
|
13
|
+
existsSync,
|
|
14
|
+
lstatSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
openSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
writeSync,
|
|
20
|
+
} from "node:fs"
|
|
21
|
+
import { resolve } from "node:path"
|
|
22
|
+
import type { DbRefusalCode } from "@nifrajs/mcp-db/engine"
|
|
23
|
+
|
|
24
|
+
export const DB_AUDIT_FILE = ".nifra/db-audit.jsonl"
|
|
25
|
+
export const DB_AUDIT_ROTATED_FILE = ".nifra/db-audit.1.jsonl"
|
|
26
|
+
export const DB_AUDIT_MAX_BYTES = 1024 * 1024
|
|
27
|
+
|
|
28
|
+
/** One audited call. */
|
|
29
|
+
export interface DbAuditEntry {
|
|
30
|
+
readonly at: string
|
|
31
|
+
readonly tool: "schema" | "query" | "explain" | "role"
|
|
32
|
+
readonly engine?: "sqlite" | "postgres" | undefined
|
|
33
|
+
/** The statement, secrets redacted. */
|
|
34
|
+
readonly sql?: string | undefined
|
|
35
|
+
/** Stable across literal values and whitespace, so repeated shapes group together. */
|
|
36
|
+
readonly fingerprint?: string | undefined
|
|
37
|
+
readonly table?: string | undefined
|
|
38
|
+
readonly ok: boolean
|
|
39
|
+
readonly code?: DbRefusalCode | undefined
|
|
40
|
+
readonly rowCount?: number | undefined
|
|
41
|
+
readonly durationMs: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A statement's shape: literals become `?`, whitespace collapses, case folds; hashed. */
|
|
45
|
+
export function sqlFingerprint(sql: string): string {
|
|
46
|
+
const shape = sql
|
|
47
|
+
.replace(/--[^\n]*/g, " ")
|
|
48
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
49
|
+
.replace(/'(?:[^']|'')*'/g, "?")
|
|
50
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, "?")
|
|
51
|
+
.replace(/\s+/g, " ")
|
|
52
|
+
.trim()
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
return createHash("sha256").update(shape).digest("hex").slice(0, 16)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when `path` exists and is a symlink: the audit never writes through one. */
|
|
58
|
+
function isSymlink(path: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
return lstatSync(path).isSymbolicLink()
|
|
61
|
+
} catch {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Append one entry. Returns false (and writes nothing) when `.nifra` or the log is a symlink or the
|
|
68
|
+
* write fails - an audit problem never fails the call it records.
|
|
69
|
+
*/
|
|
70
|
+
export function appendDbAudit(root: string, entry: DbAuditEntry): boolean {
|
|
71
|
+
const dir = resolve(root, ".nifra")
|
|
72
|
+
const file = resolve(root, DB_AUDIT_FILE)
|
|
73
|
+
try {
|
|
74
|
+
if (isSymlink(dir)) return false
|
|
75
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
76
|
+
if (isSymlink(file)) return false
|
|
77
|
+
const line = `${JSON.stringify(entry)}\n`
|
|
78
|
+
if (existsSync(file) && lstatSync(file).size + Buffer.byteLength(line) > DB_AUDIT_MAX_BYTES) {
|
|
79
|
+
const rotated = resolve(root, DB_AUDIT_ROTATED_FILE)
|
|
80
|
+
if (isSymlink(rotated)) return false
|
|
81
|
+
renameSync(file, rotated)
|
|
82
|
+
}
|
|
83
|
+
const fd = openSync(
|
|
84
|
+
file,
|
|
85
|
+
constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW,
|
|
86
|
+
0o600,
|
|
87
|
+
)
|
|
88
|
+
try {
|
|
89
|
+
writeSync(fd, line)
|
|
90
|
+
} finally {
|
|
91
|
+
closeSync(fd)
|
|
92
|
+
}
|
|
93
|
+
chmodSync(file, 0o600)
|
|
94
|
+
return true
|
|
95
|
+
} catch {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True for a line `appendDbAudit` wrote (its required fields, at least). */
|
|
101
|
+
export const isDbAuditEntry = (value: unknown): value is DbAuditEntry =>
|
|
102
|
+
typeof value === "object" &&
|
|
103
|
+
value !== null &&
|
|
104
|
+
"at" in value &&
|
|
105
|
+
typeof value.at === "string" &&
|
|
106
|
+
"tool" in value &&
|
|
107
|
+
typeof value.tool === "string" &&
|
|
108
|
+
"ok" in value &&
|
|
109
|
+
typeof value.ok === "boolean"
|
|
110
|
+
|
|
111
|
+
/** The newest `limit` entries, oldest first, across the current and the rotated file. */
|
|
112
|
+
export function readDbAudit(root: string, limit: number): DbAuditEntry[] {
|
|
113
|
+
const entries: DbAuditEntry[] = []
|
|
114
|
+
for (const name of [DB_AUDIT_ROTATED_FILE, DB_AUDIT_FILE]) {
|
|
115
|
+
const path = resolve(root, name)
|
|
116
|
+
if (!existsSync(path) || isSymlink(path)) continue
|
|
117
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
118
|
+
if (line.trim() === "") continue
|
|
119
|
+
try {
|
|
120
|
+
const parsed: unknown = JSON.parse(line)
|
|
121
|
+
if (isDbAuditEntry(parsed)) entries.push(parsed)
|
|
122
|
+
} catch {
|
|
123
|
+
// A torn last line from a killed writer is skipped, not fatal.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return entries.slice(-limit)
|
|
128
|
+
}
|
package/src/db-child.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `nifra db` subprocess. The parent (`db-tool.ts`) spawns a fresh one per call: it applies the
|
|
3
|
+
* project's `.env` files, imports `nifra.config.ts`, reports the declared deadline, opens the database,
|
|
4
|
+
* runs one operation, prints the answer and exits. The parent kills it at the deadline, so the deadline
|
|
5
|
+
* bounds the work and not only the response.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: one JSON request on stdin; answers on stdout as lines prefixed with the request's token,
|
|
8
|
+
* so anything the config prints cannot be read as an answer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type DbPlan,
|
|
13
|
+
type DbRedaction,
|
|
14
|
+
type DbRefusal,
|
|
15
|
+
type DbRows,
|
|
16
|
+
type DbSchemaReport,
|
|
17
|
+
dbRefusal,
|
|
18
|
+
explainSqlite,
|
|
19
|
+
isDbRefusal,
|
|
20
|
+
openReadOnlySqlite,
|
|
21
|
+
querySqlite,
|
|
22
|
+
readSqliteSchema,
|
|
23
|
+
resolveSqliteFile,
|
|
24
|
+
} from "@nifrajs/mcp-db/engine"
|
|
25
|
+
import {
|
|
26
|
+
connectPostgres,
|
|
27
|
+
explainPostgres,
|
|
28
|
+
type PostgresRoleSql,
|
|
29
|
+
parsePostgresUrl,
|
|
30
|
+
postgresRoleSql,
|
|
31
|
+
queryPostgres,
|
|
32
|
+
readPostgresSchema,
|
|
33
|
+
} from "@nifrajs/mcp-db/postgres"
|
|
34
|
+
import { createDevFeed } from "@nifrajs/web/dev-feed"
|
|
35
|
+
import { isSensitiveFieldName } from "@nifrajs/web/zones"
|
|
36
|
+
import { type DevDatabase, loadDevDatabase } from "./db-config.ts"
|
|
37
|
+
import { CHILD_INPUT_MAX_BYTES, readBoundedStream } from "./mcp-io.ts"
|
|
38
|
+
|
|
39
|
+
/** The one operation a subprocess runs. */
|
|
40
|
+
export interface DbChildRequest {
|
|
41
|
+
readonly op: "schema" | "query" | "explain" | "role"
|
|
42
|
+
readonly sql?: string | undefined
|
|
43
|
+
readonly table?: string | undefined
|
|
44
|
+
readonly analyze?: boolean | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What an operation produced, before the parent adds timing and the untrusted-data note. */
|
|
48
|
+
export type DbChildAnswer =
|
|
49
|
+
| {
|
|
50
|
+
readonly ok: true
|
|
51
|
+
readonly engine: DevDatabase["kind"]
|
|
52
|
+
readonly database: string
|
|
53
|
+
readonly durationMs: number
|
|
54
|
+
readonly schema?: DbSchemaReport
|
|
55
|
+
readonly rows?: DbRows
|
|
56
|
+
readonly plan?: DbPlan
|
|
57
|
+
readonly role?: Omit<PostgresRoleSql, "serverVersionNum"> | { readonly statements: [] }
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
readonly ok: false
|
|
61
|
+
readonly engine?: DevDatabase["kind"] | undefined
|
|
62
|
+
readonly database?: string | undefined
|
|
63
|
+
readonly durationMs: number
|
|
64
|
+
readonly refusal: DbRefusal
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The lines a subprocess writes, after its token. */
|
|
68
|
+
export type DbChildMessage =
|
|
69
|
+
| { readonly type: "ready"; readonly timeoutMs: number }
|
|
70
|
+
| { readonly type: "answer"; readonly answer: DbChildAnswer }
|
|
71
|
+
|
|
72
|
+
const lower = (name: string): string => name.toLowerCase()
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The masked columns a query may not read (the credential-name classifier, plus `redactColumns`,
|
|
76
|
+
* minus `revealColumns`) and the dev feed's text redactor for every string value.
|
|
77
|
+
*/
|
|
78
|
+
export function dbRedaction(
|
|
79
|
+
root: string,
|
|
80
|
+
db: DevDatabase,
|
|
81
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
82
|
+
): DbRedaction {
|
|
83
|
+
const redact = new Set(db.redactColumns.map(lower))
|
|
84
|
+
const reveal = new Set(db.revealColumns.map(lower))
|
|
85
|
+
const feed = createDevFeed({ root, persist: false, env })
|
|
86
|
+
return {
|
|
87
|
+
column: (name) =>
|
|
88
|
+
!reveal.has(lower(name)) && (isSensitiveFieldName(name) || redact.has(lower(name))),
|
|
89
|
+
text: (value) => feed.redact(value),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const QUERY_OFF = (): DbRefusal =>
|
|
94
|
+
dbRefusal("NIFRA_DB_QUERY_OFF", "devDatabase.query is false: only the schema tool is on")
|
|
95
|
+
|
|
96
|
+
async function runSqlite(
|
|
97
|
+
root: string,
|
|
98
|
+
request: DbChildRequest,
|
|
99
|
+
db: Extract<DevDatabase, { kind: "sqlite" }>,
|
|
100
|
+
redaction: DbRedaction,
|
|
101
|
+
): Promise<Omit<Extract<DbChildAnswer, { ok: true }>, "durationMs"> | DbRefusal> {
|
|
102
|
+
if (request.op === "role") {
|
|
103
|
+
return { ok: true, engine: "sqlite", database: db.file, role: { statements: [] } }
|
|
104
|
+
}
|
|
105
|
+
if (request.op !== "schema" && !db.query) return QUERY_OFF()
|
|
106
|
+
const file = resolveSqliteFile(root, db.file, db.allowFiles)
|
|
107
|
+
if (isDbRefusal(file)) return file
|
|
108
|
+
let handle: Awaited<ReturnType<typeof openReadOnlySqlite>>
|
|
109
|
+
try {
|
|
110
|
+
handle = await openReadOnlySqlite(file)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return dbRefusal("NIFRA_DB_DRIVER", `could not open ${db.file}: ${String(error)}`)
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const base = { ok: true as const, engine: "sqlite" as const, database: db.file }
|
|
116
|
+
if (request.op === "schema") {
|
|
117
|
+
const schema = readSqliteSchema(handle, {
|
|
118
|
+
exclude: db.exclude,
|
|
119
|
+
redaction,
|
|
120
|
+
table: request.table,
|
|
121
|
+
})
|
|
122
|
+
return isDbRefusal(schema) ? schema : { ...base, schema }
|
|
123
|
+
}
|
|
124
|
+
if (request.op === "explain") {
|
|
125
|
+
const plan = explainSqlite(handle, request.sql ?? "", db)
|
|
126
|
+
return isDbRefusal(plan) ? plan : { ...base, plan }
|
|
127
|
+
}
|
|
128
|
+
const rows = querySqlite(handle, request.sql ?? "", { ...db, redaction })
|
|
129
|
+
return isDbRefusal(rows) ? rows : { ...base, rows }
|
|
130
|
+
} finally {
|
|
131
|
+
handle.close()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runPostgres(
|
|
136
|
+
request: DbChildRequest,
|
|
137
|
+
db: Extract<DevDatabase, { kind: "postgres" }>,
|
|
138
|
+
redaction: DbRedaction,
|
|
139
|
+
): Promise<Omit<Extract<DbChildAnswer, { ok: true }>, "durationMs"> | DbRefusal> {
|
|
140
|
+
if ((request.op === "query" || request.op === "explain") && !db.query) return QUERY_OFF()
|
|
141
|
+
const target = parsePostgresUrl(db.url)
|
|
142
|
+
if (isDbRefusal(target)) return target
|
|
143
|
+
const database = `${target.host ?? target.socket}:${target.port}/${target.database}`
|
|
144
|
+
const client = connectPostgres(target, db)
|
|
145
|
+
if (isDbRefusal(client)) return client
|
|
146
|
+
const base = { ok: true as const, engine: "postgres" as const, database }
|
|
147
|
+
try {
|
|
148
|
+
if (request.op === "schema") {
|
|
149
|
+
const schema = await readPostgresSchema(client, {
|
|
150
|
+
...db,
|
|
151
|
+
redaction,
|
|
152
|
+
table: request.table,
|
|
153
|
+
})
|
|
154
|
+
return isDbRefusal(schema) ? schema : { ...base, schema }
|
|
155
|
+
}
|
|
156
|
+
if (request.op === "role") {
|
|
157
|
+
const role = await postgresRoleSql(client, db)
|
|
158
|
+
if (isDbRefusal(role)) return role
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
role: { role: role.role, database: role.database, statements: role.statements },
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (request.op === "explain") {
|
|
165
|
+
const plan = await explainPostgres(client, request.sql ?? "", {
|
|
166
|
+
...db,
|
|
167
|
+
redaction,
|
|
168
|
+
analyze: request.analyze === true,
|
|
169
|
+
})
|
|
170
|
+
return isDbRefusal(plan) ? plan : { ...base, plan }
|
|
171
|
+
}
|
|
172
|
+
const rows = await queryPostgres(client, request.sql ?? "", { ...db, redaction })
|
|
173
|
+
return isDbRefusal(rows) ? rows : { ...base, rows }
|
|
174
|
+
} finally {
|
|
175
|
+
await client.close({ timeout: 0 }).catch(() => {})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Run one operation against a loaded declaration. Every refusal message is redacted too. */
|
|
180
|
+
export async function runDbOperation(
|
|
181
|
+
root: string,
|
|
182
|
+
request: DbChildRequest,
|
|
183
|
+
db: DevDatabase,
|
|
184
|
+
): Promise<DbChildAnswer> {
|
|
185
|
+
const started = performance.now()
|
|
186
|
+
const redaction = dbRedaction(root, db)
|
|
187
|
+
let outcome: Omit<Extract<DbChildAnswer, { ok: true }>, "durationMs"> | DbRefusal
|
|
188
|
+
try {
|
|
189
|
+
outcome =
|
|
190
|
+
db.kind === "sqlite"
|
|
191
|
+
? await runSqlite(root, request, db, redaction)
|
|
192
|
+
: await runPostgres(request, db, redaction)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
outcome = dbRefusal("NIFRA_DB_DRIVER", error instanceof Error ? error.message : String(error))
|
|
195
|
+
}
|
|
196
|
+
const durationMs = Math.round(performance.now() - started)
|
|
197
|
+
if (!isDbRefusal(outcome)) return { ...outcome, durationMs }
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
engine: db.kind,
|
|
201
|
+
durationMs,
|
|
202
|
+
refusal: { ...outcome, message: redaction.text?.(outcome.message) ?? outcome.message },
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const OPS = ["schema", "query", "explain", "role"] as const
|
|
207
|
+
|
|
208
|
+
/** Validate the parent's request (it crosses a process boundary). */
|
|
209
|
+
export function parseDbChildRequest(
|
|
210
|
+
text: string,
|
|
211
|
+
): (DbChildRequest & { readonly token: string }) | undefined {
|
|
212
|
+
let value: unknown
|
|
213
|
+
try {
|
|
214
|
+
value = JSON.parse(text)
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
if (typeof value !== "object" || value === null) return undefined
|
|
219
|
+
const field = (key: string): unknown => Reflect.get(value, key)
|
|
220
|
+
const token = field("token")
|
|
221
|
+
const op = OPS.find((name) => name === field("op"))
|
|
222
|
+
const sql = field("sql")
|
|
223
|
+
const table = field("table")
|
|
224
|
+
const analyze = field("analyze")
|
|
225
|
+
if (typeof token !== "string" || !/^[0-9a-f-]{16,64}$/.test(token)) return undefined
|
|
226
|
+
if (op === undefined) return undefined
|
|
227
|
+
if (sql !== undefined && typeof sql !== "string") return undefined
|
|
228
|
+
if (table !== undefined && typeof table !== "string") return undefined
|
|
229
|
+
if (analyze !== undefined && typeof analyze !== "boolean") return undefined
|
|
230
|
+
return { token, op, sql, table, analyze }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Serve one request: read it from `input`, load the declaration, report the deadline, run the
|
|
235
|
+
* operation and write the answer through `write`, each line prefixed with the request's token.
|
|
236
|
+
* Returns false (writing nothing) for a malformed request.
|
|
237
|
+
*/
|
|
238
|
+
export async function serveDbChild(
|
|
239
|
+
root: string,
|
|
240
|
+
input: ReadableStream<Uint8Array>,
|
|
241
|
+
write: (line: string) => Promise<void>,
|
|
242
|
+
): Promise<boolean> {
|
|
243
|
+
const text = await readBoundedStream(input, CHILD_INPUT_MAX_BYTES)
|
|
244
|
+
const request = text.truncated ? undefined : parseDbChildRequest(text.text)
|
|
245
|
+
if (request === undefined) return false
|
|
246
|
+
const send = (message: DbChildMessage): Promise<void> =>
|
|
247
|
+
write(`${request.token} ${JSON.stringify(message)}\n`)
|
|
248
|
+
const started = performance.now()
|
|
249
|
+
let answer: DbChildAnswer
|
|
250
|
+
try {
|
|
251
|
+
const db = await loadDevDatabase(root)
|
|
252
|
+
if (isDbRefusal(db)) {
|
|
253
|
+
answer = { ok: false, durationMs: Math.round(performance.now() - started), refusal: db }
|
|
254
|
+
} else {
|
|
255
|
+
await send({ type: "ready", timeoutMs: db.timeoutMs })
|
|
256
|
+
answer = await runDbOperation(root, request, db)
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
answer = {
|
|
260
|
+
ok: false,
|
|
261
|
+
durationMs: Math.round(performance.now() - started),
|
|
262
|
+
refusal: dbRefusal("NIFRA_DB_DRIVER", error instanceof Error ? error.message : String(error)),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await send({ type: "answer", answer })
|
|
266
|
+
return true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (import.meta.main) {
|
|
270
|
+
// Bound before the config loads, so nothing it patches can reach the protocol channel.
|
|
271
|
+
const stdout = process.stdout.write.bind(process.stdout)
|
|
272
|
+
const toStderr = (...args: unknown[]): void => {
|
|
273
|
+
process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`)
|
|
274
|
+
}
|
|
275
|
+
console.log = toStderr
|
|
276
|
+
console.info = toStderr
|
|
277
|
+
console.debug = toStderr
|
|
278
|
+
const served = await serveDbChild(
|
|
279
|
+
process.argv[2] ?? process.cwd(),
|
|
280
|
+
Bun.stdin.stream(),
|
|
281
|
+
(line) => new Promise((done) => stdout(line, () => done())),
|
|
282
|
+
)
|
|
283
|
+
if (!served) process.stderr.write("nifra db: invalid request\n")
|
|
284
|
+
process.exit(served ? 0 : 2)
|
|
285
|
+
}
|