@nifrajs/cli 4.0.0 → 5.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 +3 -0
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +21 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +138 -38
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +28 -3
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- package/dist/internal/source-facts.js.map +1 -1
- 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 +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +958 -71
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts.map +1 -1
- package/dist/recipes/4.0.0.js +2 -0
- package/dist/recipes/4.0.0.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -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/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +192 -40
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +42 -18
- package/dist/sync-manifest.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +42 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +181 -47
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +34 -3
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +8 -3
- package/src/migrate-layout.ts +1109 -70
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/4.0.0.ts +2 -0
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/sync-manifest.ts +48 -18
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra routes --hooks`: which hooks run for each route and for each prefix's unmatched paths, and the
|
|
3
|
+
* diff between two such reports (`--since <file>`, and `nifra upgrade`, which captures one before it
|
|
4
|
+
* writes). The report itself is `reflectHooks` from `@nifrajs/core/reflection`.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReflectedHook, ReflectedHookReport } from "@nifrajs/core/reflection"
|
|
7
|
+
|
|
8
|
+
/** Where `nifra upgrade --write` keeps the report it captured before editing anything. */
|
|
9
|
+
export const HOOKS_BEFORE_UPGRADE_FILE = ".nifra/hooks-before-upgrade.json"
|
|
10
|
+
|
|
11
|
+
export async function collectHookReport(backend: unknown): Promise<ReflectedHookReport> {
|
|
12
|
+
const { reflectHooks } = await import("@nifrajs/core/reflection")
|
|
13
|
+
return reflectHooks(backend)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const entryText = (hook: ReflectedHook): string =>
|
|
17
|
+
`${hook.kind} ${hook.name}${hook.headers === undefined ? "" : ` [${hook.headers.join(", ")}]`} (${hook.site})`
|
|
18
|
+
|
|
19
|
+
/** One line per hook, keyed by `METHOD /path` for routes and `unmatched /prefix` for the rest. */
|
|
20
|
+
export function hookReportTargets(report: ReflectedHookReport): Map<string, readonly string[]> {
|
|
21
|
+
const targets = new Map<string, readonly string[]>()
|
|
22
|
+
for (const route of report.routes) {
|
|
23
|
+
targets.set(`${route.method} ${route.path}`, route.hooks.map(entryText))
|
|
24
|
+
}
|
|
25
|
+
for (const entry of report.unmatched) {
|
|
26
|
+
targets.set(`unmatched ${entry.prefix}`, entry.hooks.map(entryText))
|
|
27
|
+
}
|
|
28
|
+
return targets
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderHookReport(report: ReflectedHookReport): string {
|
|
32
|
+
const targets = hookReportTargets(report)
|
|
33
|
+
if (targets.size === 0) return "no backend routes to report hooks for"
|
|
34
|
+
const lines: string[] = []
|
|
35
|
+
for (const [target, hooks] of targets) {
|
|
36
|
+
lines.push(target)
|
|
37
|
+
if (hooks.length === 0) lines.push(" (no hooks)")
|
|
38
|
+
for (const hook of hooks) lines.push(` ${hook}`)
|
|
39
|
+
}
|
|
40
|
+
if (!report.complete) {
|
|
41
|
+
lines.push(
|
|
42
|
+
"",
|
|
43
|
+
"Some hooks have no recorded origin (site `unknown`): origins are recorded only outside production builds.",
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return lines.join("\n")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface HookReportChange {
|
|
50
|
+
readonly target: string
|
|
51
|
+
readonly added: readonly string[]
|
|
52
|
+
readonly removed: readonly string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What runs differently: per route or unmatched prefix, the hooks it gained and lost. */
|
|
56
|
+
export function diffHookReports(
|
|
57
|
+
before: ReflectedHookReport,
|
|
58
|
+
after: ReflectedHookReport,
|
|
59
|
+
): readonly HookReportChange[] {
|
|
60
|
+
const old = hookReportTargets(before)
|
|
61
|
+
const now = hookReportTargets(after)
|
|
62
|
+
const changes: HookReportChange[] = []
|
|
63
|
+
const minus = (from: readonly string[], take: readonly string[]): string[] => {
|
|
64
|
+
const left = [...take]
|
|
65
|
+
return from.filter((entry) => {
|
|
66
|
+
const index = left.indexOf(entry)
|
|
67
|
+
if (index === -1) return true
|
|
68
|
+
left.splice(index, 1)
|
|
69
|
+
return false
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
const targets = [...new Set([...old.keys(), ...now.keys()])].sort()
|
|
73
|
+
for (const target of targets) {
|
|
74
|
+
const was = old.get(target) ?? []
|
|
75
|
+
const is = now.get(target) ?? []
|
|
76
|
+
const added = minus(is, was)
|
|
77
|
+
const removed = minus(was, is)
|
|
78
|
+
if (added.length > 0 || removed.length > 0) changes.push({ target, added, removed })
|
|
79
|
+
}
|
|
80
|
+
return changes
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderHookDiff(changes: readonly HookReportChange[]): string {
|
|
84
|
+
if (changes.length === 0) return "hook scope unchanged"
|
|
85
|
+
const lines: string[] = []
|
|
86
|
+
for (const change of changes) {
|
|
87
|
+
lines.push(change.target)
|
|
88
|
+
for (const entry of change.added) lines.push(` + ${entry}`)
|
|
89
|
+
for (const entry of change.removed) lines.push(` - ${entry}`)
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const field = (value: unknown, key: string): unknown =>
|
|
95
|
+
typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined
|
|
96
|
+
|
|
97
|
+
const hooksValid = (row: unknown): boolean => {
|
|
98
|
+
const hooks = field(row, "hooks")
|
|
99
|
+
return (
|
|
100
|
+
Array.isArray(hooks) &&
|
|
101
|
+
hooks.every(
|
|
102
|
+
(hook) =>
|
|
103
|
+
typeof field(hook, "kind") === "string" &&
|
|
104
|
+
typeof field(hook, "name") === "string" &&
|
|
105
|
+
typeof field(hook, "site") === "string",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isHookReport(value: unknown): value is ReflectedHookReport {
|
|
111
|
+
const routes = field(value, "routes")
|
|
112
|
+
const unmatched = field(value, "unmatched")
|
|
113
|
+
return (
|
|
114
|
+
typeof field(value, "complete") === "boolean" &&
|
|
115
|
+
Array.isArray(routes) &&
|
|
116
|
+
Array.isArray(unmatched) &&
|
|
117
|
+
routes.every(
|
|
118
|
+
(row) =>
|
|
119
|
+
typeof field(row, "method") === "string" &&
|
|
120
|
+
typeof field(row, "path") === "string" &&
|
|
121
|
+
hooksValid(row),
|
|
122
|
+
) &&
|
|
123
|
+
unmatched.every((row) => typeof field(row, "prefix") === "string" && hooksValid(row))
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A saved report, parsed; `undefined` when the text is not one. */
|
|
128
|
+
export function parseHookReport(text: string): ReflectedHookReport | undefined {
|
|
129
|
+
try {
|
|
130
|
+
const value: unknown = JSON.parse(text)
|
|
131
|
+
return isHookReport(value) ? value : undefined
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/init-agents.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* `## Project structure` section, appended when missing (or a minimal
|
|
10
10
|
* file if none exists)
|
|
11
11
|
*
|
|
12
|
-
* The generators
|
|
13
|
-
*
|
|
12
|
+
* The generators come from `./agent-files.ts`, a byte-identical copy of the module `create-nifra` uses at
|
|
13
|
+
* scaffold time, so a retrofitted app and a freshly scaffolded one get byte-identical configs.
|
|
14
14
|
*
|
|
15
15
|
* Safety: this writes into the user's existing tree, so it NEVER silently clobbers a file they may have
|
|
16
16
|
* customized. By default an existing `.mcp.json` / `CLAUDE.md` / `.cursor/mcp.json` is SKIPPED with a
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
MCP_SERVER_COMMAND,
|
|
45
45
|
mcpJson,
|
|
46
46
|
STRUCTURE_HEADING,
|
|
47
|
-
} from "
|
|
47
|
+
} from "./agent-files.ts"
|
|
48
48
|
import { installedNifraVersion, resolveRootState } from "./mcp-root.ts"
|
|
49
49
|
|
|
50
50
|
/** What happened to one file during the retrofit, for the printed report + the `--json` shape. */
|
|
@@ -121,13 +121,17 @@ async function assertSafeParents(cwd: string, abs: string): Promise<void> {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
126
|
-
|
|
124
|
+
/** Never write anything that is not a plain absent-or-regular file: a symlink here would redirect
|
|
125
|
+
* the write, and a directory or device node would make it fail in a confusing way. Such a file is
|
|
126
|
+
* reported and left alone rather than stopping the run - a symlinked CLAUDE.md is a common alias
|
|
127
|
+
* for AGENTS.md. */
|
|
128
|
+
async function nonRegularTarget(
|
|
129
|
+
abs: string,
|
|
130
|
+
rel: string,
|
|
131
|
+
): Promise<InitAgentsFileResult | undefined> {
|
|
127
132
|
const stat = await lstatOrMissing(abs)
|
|
128
|
-
if (stat
|
|
129
|
-
|
|
130
|
-
}
|
|
133
|
+
if (stat === undefined || (stat.isFile() && !stat.isSymbolicLink())) return undefined
|
|
134
|
+
return { path: rel, action: "skipped", note: "not a regular file - left alone" }
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
/**
|
|
@@ -142,7 +146,8 @@ async function writeOwned(
|
|
|
142
146
|
): Promise<InitAgentsFileResult> {
|
|
143
147
|
const abs = safeJoin(cwd, rel)
|
|
144
148
|
await assertSafeParents(cwd, abs)
|
|
145
|
-
await
|
|
149
|
+
const irregular = await nonRegularTarget(abs, rel)
|
|
150
|
+
if (irregular !== undefined) return irregular
|
|
146
151
|
if (!force && (await fileExists(abs))) {
|
|
147
152
|
return { path: rel, action: "skipped", note: "exists - pass --force to overwrite" }
|
|
148
153
|
}
|
|
@@ -180,7 +185,8 @@ async function atomicWrite(
|
|
|
180
185
|
async function ensureAgentsMd(cwd: string): Promise<InitAgentsFileResult> {
|
|
181
186
|
const abs = safeJoin(cwd, AGENTS_MD_PATH)
|
|
182
187
|
await assertSafeParents(cwd, abs)
|
|
183
|
-
await
|
|
188
|
+
const irregular = await nonRegularTarget(abs, AGENTS_MD_PATH)
|
|
189
|
+
if (irregular !== undefined) return irregular
|
|
184
190
|
const sections = [{ heading: "## MCP server", text: agentsMcpSection() }]
|
|
185
191
|
if (await isDirectory(safeJoin(cwd, "routes"))) {
|
|
186
192
|
sections.push({ heading: STRUCTURE_HEADING, text: agentsStructureSection() })
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join, relative } from "node:path"
|
|
3
|
+
import { CONFIG_FILE, FRAMEWORK_FILE } from "../app-files.ts"
|
|
4
|
+
|
|
5
|
+
const NIFRA_PACKAGE = /^(?:@nifrajs\/|nifra$)/
|
|
6
|
+
const DEPENDENCY_FIELDS = [
|
|
7
|
+
"dependencies",
|
|
8
|
+
"devDependencies",
|
|
9
|
+
"peerDependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
/** Whether a package.json text names `nifra` or a `@nifrajs/` package, or carries a `nifra` setting. */
|
|
14
|
+
function declaresNifra(manifest: string): boolean {
|
|
15
|
+
let parsed: unknown
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(manifest)
|
|
18
|
+
} catch {
|
|
19
|
+
// A manifest nobody can read proves nothing either way, so the directory stays checked.
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
if (typeof parsed !== "object" || parsed === null) return true
|
|
23
|
+
if ("nifra" in parsed) return true
|
|
24
|
+
return DEPENDENCY_FIELDS.some((field) => {
|
|
25
|
+
const deps: unknown = Reflect.get(parsed, field)
|
|
26
|
+
return (
|
|
27
|
+
typeof deps === "object" &&
|
|
28
|
+
deps !== null &&
|
|
29
|
+
Object.keys(deps).some((name) => NIFRA_PACKAGE.test(name))
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a project-relative directory ("" for the root) belongs to a nifra project, read statically.
|
|
36
|
+
* The checked root always does: running the check there says so. Below it, walking up, the first
|
|
37
|
+
* directory with a nifra config decides yes, and the first package.json decides by naming `nifra` or
|
|
38
|
+
* a `@nifrajs/` package, or carrying a `nifra` setting; a nested package that does none of that is
|
|
39
|
+
* another project, such as another framework's app kept beside this one.
|
|
40
|
+
*/
|
|
41
|
+
export function createNifraProjectProbe(root: string): (dir: string) => boolean {
|
|
42
|
+
const cache = new Map<string, boolean>()
|
|
43
|
+
const probe = (dir: string): boolean => {
|
|
44
|
+
const cached = cache.get(dir)
|
|
45
|
+
if (cached !== undefined) return cached
|
|
46
|
+
const at = join(root, dir)
|
|
47
|
+
let nifra: boolean
|
|
48
|
+
if (dir === "" || existsSync(join(at, CONFIG_FILE)) || existsSync(join(at, FRAMEWORK_FILE)))
|
|
49
|
+
nifra = true
|
|
50
|
+
else if (existsSync(join(at, "package.json"))) {
|
|
51
|
+
let manifest: string
|
|
52
|
+
try {
|
|
53
|
+
manifest = readFileSync(join(at, "package.json"), "utf8")
|
|
54
|
+
} catch {
|
|
55
|
+
manifest = ""
|
|
56
|
+
}
|
|
57
|
+
nifra = declaresNifra(manifest)
|
|
58
|
+
} else nifra = probe(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0)))
|
|
59
|
+
cache.set(dir, nifra)
|
|
60
|
+
return nifra
|
|
61
|
+
}
|
|
62
|
+
return probe
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ProjectKind = "api" | "web"
|
|
66
|
+
|
|
67
|
+
export interface KindDeclaration {
|
|
68
|
+
/** `"api"` or `"web"` from `"nifra": { "kind" }`, or `undefined` when the package declares none. */
|
|
69
|
+
readonly kind: ProjectKind | undefined
|
|
70
|
+
/** The package.json that decides, relative to the checked root (`../package.json` above it). */
|
|
71
|
+
readonly manifest: string | undefined
|
|
72
|
+
/** The problem with a declared value that is neither `"api"` nor `"web"`. */
|
|
73
|
+
readonly invalid?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readJson(path: string): unknown {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(readFileSync(path, "utf8"))
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
85
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
86
|
+
? Object.fromEntries(Object.entries(value))
|
|
87
|
+
: undefined
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The `"nifra": { "kind" }` that governs a project-relative directory: read from the nearest
|
|
91
|
+
* package.json at or above it, the way the zone setting is read, with no app code run.
|
|
92
|
+
*/
|
|
93
|
+
export function createKindProbe(root: string): (dir: string) => KindDeclaration {
|
|
94
|
+
const cache = new Map<string, KindDeclaration>()
|
|
95
|
+
const probe = (absolute: string): KindDeclaration => {
|
|
96
|
+
const cached = cache.get(absolute)
|
|
97
|
+
if (cached !== undefined) return cached
|
|
98
|
+
let declaration: KindDeclaration
|
|
99
|
+
const manifestPath = join(absolute, "package.json")
|
|
100
|
+
if (existsSync(manifestPath)) {
|
|
101
|
+
const manifest = relative(root, manifestPath).replaceAll("\\", "/")
|
|
102
|
+
const kind = recordOf(recordOf(readJson(manifestPath))?.nifra)?.kind
|
|
103
|
+
if (kind === undefined) declaration = { kind: undefined, manifest }
|
|
104
|
+
else if (kind === "api" || kind === "web") declaration = { kind, manifest }
|
|
105
|
+
else
|
|
106
|
+
declaration = {
|
|
107
|
+
kind: undefined,
|
|
108
|
+
manifest,
|
|
109
|
+
invalid: `[nifra] ${manifest} "nifra.kind" must be "api" or "web", not ${JSON.stringify(kind)}`,
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const parent = dirname(absolute)
|
|
113
|
+
declaration = parent === absolute ? { kind: undefined, manifest: undefined } : probe(parent)
|
|
114
|
+
}
|
|
115
|
+
cache.set(absolute, declaration)
|
|
116
|
+
return declaration
|
|
117
|
+
}
|
|
118
|
+
return (dir) => probe(join(root, dir))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether a project-relative directory holds web config: a `nifra.config.ts` exporting `adapter`, or
|
|
122
|
+
* `backend/framework.ts`. */
|
|
123
|
+
export function hasWebConfig(root: string, dir: string): boolean {
|
|
124
|
+
if (existsSync(join(root, dir, FRAMEWORK_FILE))) return true
|
|
125
|
+
const config = join(root, dir, CONFIG_FILE)
|
|
126
|
+
if (!existsSync(config)) return false
|
|
127
|
+
let text: string
|
|
128
|
+
try {
|
|
129
|
+
text = readFileSync(config, "utf8")
|
|
130
|
+
} catch {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
return /\bexport\s+(?:const|let|var|function)\s+adapter\b|\bexport\s*\{[^}]*\badapter\b/.test(
|
|
134
|
+
text,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const WEB_PACKAGE = /^@nifrajs\/web(?:-[a-z0-9-]+)?$/
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether a project-relative directory ("" for the root) is inside a nifra web app. Walking up, the
|
|
142
|
+
* first directory with `nifra.config.ts` or `backend/framework.ts` makes it one; the first package.json
|
|
143
|
+
* decides instead: `"nifra": { "kind": "api" }` says no, otherwise naming `@nifrajs/web` or an adapter
|
|
144
|
+
* says yes. Nothing up to the root means no.
|
|
145
|
+
*/
|
|
146
|
+
export function createWebAppProbe(root: string): (dir: string) => boolean {
|
|
147
|
+
const cache = new Map<string, boolean>()
|
|
148
|
+
const probe = (dir: string): boolean => {
|
|
149
|
+
const cached = cache.get(dir)
|
|
150
|
+
if (cached !== undefined) return cached
|
|
151
|
+
const at = join(root, dir)
|
|
152
|
+
let web: boolean
|
|
153
|
+
if (existsSync(join(at, CONFIG_FILE)) || existsSync(join(at, FRAMEWORK_FILE))) web = true
|
|
154
|
+
else if (existsSync(join(at, "package.json"))) {
|
|
155
|
+
const manifest = recordOf(readJson(join(at, "package.json")))
|
|
156
|
+
web =
|
|
157
|
+
recordOf(manifest?.nifra)?.kind !== "api" &&
|
|
158
|
+
DEPENDENCY_FIELDS.some((field) => {
|
|
159
|
+
const deps = recordOf(manifest?.[field])
|
|
160
|
+
return deps !== undefined && Object.keys(deps).some((name) => WEB_PACKAGE.test(name))
|
|
161
|
+
})
|
|
162
|
+
} else web = dir !== "" && probe(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0)))
|
|
163
|
+
cache.set(dir, web)
|
|
164
|
+
return web
|
|
165
|
+
}
|
|
166
|
+
return probe
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A zone or server-only finding outside any web app, with the API-only setting named. */
|
|
170
|
+
export const withApiKindHint = (message: string): string =>
|
|
171
|
+
`${message}${message.endsWith(".") ? "" : "."} If this project serves no pages, declare it in package.json: "nifra": { "kind": "api" }`
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether `"nifra": { "kind": "api" }` takes a project-relative directory out of the zone checks. It
|
|
175
|
+
* does only where no web config sits at or above the directory, up to the checked root: an app that
|
|
176
|
+
* serves pages keeps its zone checks whatever its package.json says (NF-C037 reports the clash).
|
|
177
|
+
*/
|
|
178
|
+
export function createApiOnlyProbe(root: string): (dir: string) => boolean {
|
|
179
|
+
const kindOf = createKindProbe(root)
|
|
180
|
+
const web = new Map<string, boolean>()
|
|
181
|
+
const webConfigUp = (dir: string): boolean => {
|
|
182
|
+
const cached = web.get(dir)
|
|
183
|
+
if (cached !== undefined) return cached
|
|
184
|
+
const found =
|
|
185
|
+
hasWebConfig(root, dir) ||
|
|
186
|
+
(dir !== "" && webConfigUp(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0))))
|
|
187
|
+
web.set(dir, found)
|
|
188
|
+
return found
|
|
189
|
+
}
|
|
190
|
+
return (dir) => kindOf(dir).kind === "api" && !webConfigUp(dir)
|
|
191
|
+
}
|