@paramour-js/next 0.4.0 → 0.5.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 +1 -1
- package/dist/app.d.ts +48 -49
- package/dist/app.js +11 -12
- package/dist/cli-args.d.ts +4 -4
- package/dist/cli-args.js +4 -4
- package/dist/cli-inputs.d.ts +6 -7
- package/dist/cli-inputs.js +6 -7
- package/dist/cli.js +1 -1
- package/dist/collisions.d.ts +8 -8
- package/dist/collisions.js +9 -9
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.js +12 -7
- package/dist/commands/generate.d.ts +55 -7
- package/dist/commands/generate.js +29 -22
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +111 -19
- package/dist/commands/list.d.ts +21 -0
- package/dist/commands/list.js +12 -7
- package/dist/commands/skills.d.ts +45 -0
- package/dist/commands/skills.js +222 -0
- package/dist/config.d.ts +17 -10
- package/dist/config.js +17 -4
- package/dist/devtools-seam.d.ts +19 -19
- package/dist/devtools-seam.js +3 -3
- package/dist/doctor/checks.js +7 -3
- package/dist/emit.d.ts +12 -12
- package/dist/emit.js +13 -13
- package/dist/generate.d.ts +14 -14
- package/dist/generate.js +11 -11
- package/dist/init/agents-md.d.ts +32 -0
- package/dist/init/agents-md.js +78 -0
- package/dist/init/scaffold.js +54 -0
- package/dist/list/discover-route-defs.d.ts +4 -4
- package/dist/list/discover-route-defs.js +4 -4
- package/dist/lock.d.ts +8 -9
- package/dist/lock.js +11 -12
- package/dist/navigation-adapter.d.ts +17 -18
- package/dist/navigation-adapter.js +4 -4
- package/dist/observe.d.ts +14 -14
- package/dist/observe.js +4 -4
- package/dist/pages.d.ts +30 -31
- package/dist/pages.js +10 -10
- package/dist/run-cli.d.ts +3 -1
- package/dist/run-cli.js +7 -3
- package/dist/scan-app.d.ts +10 -10
- package/dist/scan-app.js +30 -30
- package/dist/scan-pages.d.ts +6 -6
- package/dist/scan-pages.js +28 -26
- package/dist/scan.d.ts +13 -10
- package/dist/scan.js +7 -7
- package/dist/select.d.ts +40 -40
- package/dist/select.js +30 -30
- package/dist/skills/doctor.d.ts +11 -0
- package/dist/skills/doctor.js +71 -0
- package/dist/skills/manifest.d.ts +41 -0
- package/dist/skills/manifest.js +96 -0
- package/dist/skills/packaged.d.ts +21 -0
- package/dist/skills/packaged.js +32 -0
- package/dist/skills/sync.d.ts +84 -0
- package/dist/skills/sync.js +136 -0
- package/dist/skills/targets.d.ts +29 -0
- package/dist/skills/targets.js +73 -0
- package/dist/testing.d.ts +28 -32
- package/dist/testing.js +15 -15
- package/dist/watch.d.ts +12 -12
- package/dist/watch.js +13 -13
- package/dist/with-typed-routes.d.ts +11 -10
- package/dist/with-typed-routes.js +42 -39
- package/package.json +4 -3
- package/skills/paramour/SKILL.md +43 -0
- package/skills/paramour/references/authoring.md +113 -0
- package/skills/paramour/references/migration.md +141 -0
- package/skills/paramour/references/reference.md +96 -0
- package/skills/paramour/references/setup.md +139 -0
package/dist/devtools-seam.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
2
2
|
/**
|
|
3
|
-
* The devtools observation seam
|
|
3
|
+
* The devtools observation seam: a dependency-free global
|
|
4
4
|
* slot the hooks push decode observations into and the devtools panel
|
|
5
5
|
* (`@paramour-js/devtools`) reads out of. This module's JSDoc is the
|
|
6
6
|
* CONTRACT OF RECORD for the slot — the panel never imports runtime code
|
|
@@ -10,7 +10,7 @@ import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
|
10
10
|
*
|
|
11
11
|
* - Slot key: `Symbol.for("paramour.devtools.seam")` — the realm-global
|
|
12
12
|
* symbol registry, the same cross-copy identity idiom as core's error
|
|
13
|
-
* brands
|
|
13
|
+
* brands: a second physical copy of this module (dual-package
|
|
14
14
|
* hazard, bundler duplication) mints the SAME symbol and lands on the
|
|
15
15
|
* same slot. Either side — hooks or panel — may create the slot; both
|
|
16
16
|
* sides are create-if-absent.
|
|
@@ -24,14 +24,14 @@ import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
|
24
24
|
* `add` — same JS thread, so nothing can be emitted between the read and
|
|
25
25
|
* the add. Emitters push to `buffer` (FIFO-capped at
|
|
26
26
|
* {@link OBSERVATION_BUFFER_CAP}) and then invoke every listener.
|
|
27
|
-
* - Production
|
|
27
|
+
* - Production: every emit call site sits behind
|
|
28
28
|
* `process.env.NODE_ENV !== "production"`, which Next's compilers
|
|
29
29
|
* constant-fold; with the package's `sideEffects: false` the then-dead
|
|
30
30
|
* import of this module is dropped entirely. The emitted JS here imports
|
|
31
31
|
* NOTHING (the one `paramour` import is type-only) — load-bearing for
|
|
32
32
|
* that erasure.
|
|
33
33
|
*/
|
|
34
|
-
/** The `Symbol.for("paramour.devtools.seam")` slot shape — the
|
|
34
|
+
/** The `Symbol.for("paramour.devtools.seam")` slot shape — the seam contract. */
|
|
35
35
|
export interface ParamourDevtoolsSeam {
|
|
36
36
|
/** Capped FIFO; oldest dropped past the cap. Replay = read it. */
|
|
37
37
|
readonly buffer: ParamourObservation[];
|
|
@@ -43,12 +43,12 @@ export interface ParamourDevtoolsSeam {
|
|
|
43
43
|
*/
|
|
44
44
|
readonly version: 1;
|
|
45
45
|
}
|
|
46
|
-
/** Discriminant naming which hook reported
|
|
46
|
+
/** Discriminant naming which hook reported. */
|
|
47
47
|
export type ParamourHookId = "app.useRouteParams" | "app.useRouteParamsOrThrow" | "app.useSearch" | "app.useSearchOrThrow" | "pages.useRouteParams" | "pages.useSearch";
|
|
48
48
|
/**
|
|
49
|
-
* Navigation capability captured from the EMITTING hook's router
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Navigation capability captured from the EMITTING hook's router: the panel
|
|
50
|
+
* commits URL edits through this, so it never guesses which router is live
|
|
51
|
+
* and never imports Next. The panel passes ONLY the
|
|
52
52
|
* serialized search string (`""` or `"?…"`); the hook resolves it against
|
|
53
53
|
* its OWN current pathname — `usePathname()` (App) / `asPath`'s path part
|
|
54
54
|
* (Pages), both basePath-/locale-relative, which is what `replace()`
|
|
@@ -60,18 +60,18 @@ export type ParamourHookId = "app.useRouteParams" | "app.useRouteParamsOrThrow"
|
|
|
60
60
|
*/
|
|
61
61
|
export type ParamourNavigate = (search: string) => void;
|
|
62
62
|
/**
|
|
63
|
-
* One hook decode, reported on decode CHANGE
|
|
63
|
+
* One hook decode, reported on decode CHANGE — and
|
|
64
64
|
* re-reported when the hook's resolution base moves under an unchanged
|
|
65
65
|
* decode (a layout surviving `/product/1?q=a` → `/product/2?q=a`), so the
|
|
66
66
|
* captured `navigate`/`pathname` never go stale while the hook is mounted.
|
|
67
67
|
*/
|
|
68
68
|
export type ParamourObservation = ParamourParamsObservation | ParamourSearchObservation;
|
|
69
69
|
/**
|
|
70
|
-
* Pre-`select` decode result
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* Pre-`select` decode result: the hook's full `SafeResult` — the error arm
|
|
71
|
+
* carries the LIVE `ParamsDecodeError`/`SearchDecodeError` with its
|
|
72
|
+
* `issues` — never the user's `select` projection. `pending` is the
|
|
73
|
+
* Pages-only third state. Generic-erased on purpose: the panel treats
|
|
74
|
+
* `data` structurally.
|
|
75
75
|
*/
|
|
76
76
|
export type ParamourObservationResult = SafeResult<unknown> | {
|
|
77
77
|
readonly status: "pending";
|
|
@@ -84,7 +84,7 @@ export interface ParamourParamsObservation extends ParamourObservationBase {
|
|
|
84
84
|
/**
|
|
85
85
|
* Search decode: wire is decode-time `[key, value]` pairs in wire order —
|
|
86
86
|
* order is load-bearing for repeated keys (P5/S5), and pairs round-trip
|
|
87
|
-
* losslessly into the panel's raw-wire editing
|
|
87
|
+
* losslessly into the panel's raw-wire editing.
|
|
88
88
|
*/
|
|
89
89
|
export interface ParamourSearchObservation extends ParamourObservationBase {
|
|
90
90
|
readonly kind: "search";
|
|
@@ -106,7 +106,7 @@ interface ParamourObservationBase {
|
|
|
106
106
|
readonly pathname: string;
|
|
107
107
|
readonly result: ParamourObservationResult;
|
|
108
108
|
/**
|
|
109
|
-
* The LIVE route object (
|
|
109
|
+
* The LIVE route object (same JS context, no serialization) — the
|
|
110
110
|
* panel calls `describeRoute`, the route's own codecs, and
|
|
111
111
|
* `buildSearchString` on it directly.
|
|
112
112
|
*/
|
|
@@ -115,7 +115,7 @@ interface ParamourObservationBase {
|
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* 128: replay only needs the pre-panel-mount window. One observation per
|
|
118
|
-
* decode CHANGE per hook
|
|
118
|
+
* decode CHANGE per hook means even a long pre-open session is dozens
|
|
119
119
|
* of entries, not thousands; the panel keys on route, so depth beyond
|
|
120
120
|
* "every route seen recently" adds nothing — the cap mostly bounds how many
|
|
121
121
|
* live route/result references the buffer retains.
|
|
@@ -123,8 +123,8 @@ interface ParamourObservationBase {
|
|
|
123
123
|
export declare const OBSERVATION_BUFFER_CAP = 128;
|
|
124
124
|
/**
|
|
125
125
|
* Pushes one observation and notifies listeners. The internal production
|
|
126
|
-
* early-return is belt-and-suspenders
|
|
127
|
-
*
|
|
126
|
+
* early-return is belt-and-suspenders (every call site is ALSO guarded,
|
|
127
|
+
* which is what the bundler erases); it makes the guard directly
|
|
128
128
|
* unit-testable and keeps a future unguarded call site failing safe.
|
|
129
129
|
*/
|
|
130
130
|
export declare function emitObservation(observation: ParamourObservation): void;
|
package/dist/devtools-seam.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 128: replay only needs the pre-panel-mount window. One observation per
|
|
3
|
-
* decode CHANGE per hook
|
|
3
|
+
* decode CHANGE per hook means even a long pre-open session is dozens
|
|
4
4
|
* of entries, not thousands; the panel keys on route, so depth beyond
|
|
5
5
|
* "every route seen recently" adds nothing — the cap mostly bounds how many
|
|
6
6
|
* live route/result references the buffer retains.
|
|
@@ -10,8 +10,8 @@ const SEAM_KEY = Symbol.for("paramour.devtools.seam");
|
|
|
10
10
|
const globalSlots = globalThis;
|
|
11
11
|
/**
|
|
12
12
|
* Pushes one observation and notifies listeners. The internal production
|
|
13
|
-
* early-return is belt-and-suspenders
|
|
14
|
-
*
|
|
13
|
+
* early-return is belt-and-suspenders (every call site is ALSO guarded,
|
|
14
|
+
* which is what the bundler erases); it makes the guard directly
|
|
15
15
|
* unit-testable and keeps a future unguarded call site failing safe.
|
|
16
16
|
*/
|
|
17
17
|
export function emitObservation(observation) {
|
package/dist/doctor/checks.js
CHANGED
|
@@ -8,6 +8,7 @@ import { tsconfigCheck } from "../init/scaffold.js";
|
|
|
8
8
|
import { detectWrapState, findNextConfig } from "../init/wrap-next-config.js";
|
|
9
9
|
import { discoverRouteDefinitions, routeKey, } from "../list/discover-route-defs.js";
|
|
10
10
|
import { scanRoutes } from "../scan.js";
|
|
11
|
+
import { skillsDoctorChecks } from "../skills/doctor.js";
|
|
11
12
|
/**
|
|
12
13
|
* @internal The check battery, in report order. Each check degrades
|
|
13
14
|
* independently — doctor exists to diagnose broken setups, so a throwing
|
|
@@ -34,7 +35,7 @@ export async function runDoctorChecks(projectRoot) {
|
|
|
34
35
|
status: "fail",
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
|
-
// 2. Route directories resolve (
|
|
38
|
+
// 2. Route directories resolve (joint discovery, config dirs honored).
|
|
38
39
|
let inputs;
|
|
39
40
|
try {
|
|
40
41
|
inputs = await resolveInputs({}, projectRoot, config);
|
|
@@ -135,7 +136,10 @@ export async function runDoctorChecks(projectRoot) {
|
|
|
135
136
|
}
|
|
136
137
|
// 5. Version alignment between the two packages.
|
|
137
138
|
checks.push(versionCheck(projectRoot));
|
|
138
|
-
// 6.
|
|
139
|
+
// 6. Installed agent skills reflect this package's bundled content —
|
|
140
|
+
// upgrade-adjacent like the version check, hence its neighbor.
|
|
141
|
+
checks.push(...skillsDoctorChecks(projectRoot));
|
|
142
|
+
// 7. tsconfig covers the artifact (init's warn-level heuristic).
|
|
139
143
|
// resolve, not join — an absolute outFile must win, as it does in
|
|
140
144
|
// resolveInputs.
|
|
141
145
|
const artifactPath = inputs?.artifactPath ??
|
|
@@ -146,7 +150,7 @@ export async function runDoctorChecks(projectRoot) {
|
|
|
146
150
|
label: `tsconfig: ${coverage.label}`,
|
|
147
151
|
status: coverage.ok ? "pass" : "warn",
|
|
148
152
|
});
|
|
149
|
-
//
|
|
153
|
+
// 8. Route-definition discovery health (list's engine).
|
|
150
154
|
checks.push(await discoveryCheck(projectRoot, config, routes));
|
|
151
155
|
return checks;
|
|
152
156
|
}
|
package/dist/emit.d.ts
CHANGED
|
@@ -2,34 +2,34 @@
|
|
|
2
2
|
export interface WriteIfChangedResult {
|
|
3
3
|
/**
|
|
4
4
|
* Prior file content, or `null` when the file did not exist — the input
|
|
5
|
-
* for
|
|
5
|
+
* for the drift warning (which paths appeared/disappeared).
|
|
6
6
|
*/
|
|
7
7
|
previousContent: null | string;
|
|
8
8
|
/**
|
|
9
|
-
* `false` on a byte-identical no-op — the signal `--check`
|
|
10
|
-
*
|
|
9
|
+
* `false` on a byte-identical no-op — the signal `--check` and the watch
|
|
10
|
+
* loop hang off.
|
|
11
11
|
*/
|
|
12
12
|
written: boolean;
|
|
13
13
|
}
|
|
14
|
-
/** Input of {@link emitArtifact} — one union per router
|
|
14
|
+
/** Input of {@link emitArtifact} — one union per router. */
|
|
15
15
|
export interface EmitRoutes {
|
|
16
16
|
appRoutes: readonly string[];
|
|
17
17
|
pagesRoutes: readonly string[];
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Artifact text for the per-router route unions
|
|
21
|
-
*
|
|
20
|
+
* Artifact text for the per-router route unions: deterministic — sorted,
|
|
21
|
+
* deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
|
|
22
22
|
* happens here as well as in the scanner so byte-identity never depends on
|
|
23
23
|
* who the caller was. Always the leading-pipe multiline union form, even for
|
|
24
24
|
* one path — one shape, no count-dependent formatting. An empty union omits
|
|
25
|
-
* its member entirely (
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* its member entirely (absent, not `never`, applied per router): a
|
|
26
|
+
* pages-only project keeps world-A's permissive `string` fallback for
|
|
27
|
+
* `defineAppRoute`, and vice versa.
|
|
28
28
|
*/
|
|
29
29
|
export declare function emitArtifact(routes: EmitRoutes): string;
|
|
30
30
|
/**
|
|
31
|
-
* Compare-before-write
|
|
32
|
-
*
|
|
33
|
-
* concurrent-generator races
|
|
31
|
+
* Compare-before-write: a no-op regeneration must not touch the file — that
|
|
32
|
+
* property is what prevents TS-server churn, watch-loop feedback, and
|
|
33
|
+
* concurrent-generator races.
|
|
34
34
|
*/
|
|
35
35
|
export declare function writeIfChanged(filePath: string, content: string): WriteIfChangedResult;
|
package/dist/emit.js
CHANGED
|
@@ -2,14 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
const HEADER = "// Generated by @paramour-js/next. Do not edit — regenerate with `paramour generate`.";
|
|
4
4
|
/**
|
|
5
|
-
* Artifact text for the per-router route unions
|
|
6
|
-
*
|
|
5
|
+
* Artifact text for the per-router route unions: deterministic — sorted,
|
|
6
|
+
* deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
|
|
7
7
|
* happens here as well as in the scanner so byte-identity never depends on
|
|
8
8
|
* who the caller was. Always the leading-pipe multiline union form, even for
|
|
9
9
|
* one path — one shape, no count-dependent formatting. An empty union omits
|
|
10
|
-
* its member entirely (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* its member entirely (absent, not `never`, applied per router): a
|
|
11
|
+
* pages-only project keeps world-A's permissive `string` fallback for
|
|
12
|
+
* `defineAppRoute`, and vice versa.
|
|
13
13
|
*/
|
|
14
14
|
export function emitArtifact(routes) {
|
|
15
15
|
const members = ["appRoutes", "pagesRoutes"].flatMap((member) => {
|
|
@@ -21,22 +21,22 @@ export function emitArtifact(routes) {
|
|
|
21
21
|
// contain a quote or backslash (Linux/macOS), which unescaped would emit
|
|
22
22
|
// invalid TS or silently declare the wrong route. JSON string escaping is
|
|
23
23
|
// a subset of TS string-literal escaping (output stays valid TS) and is
|
|
24
|
-
// deterministic, so
|
|
24
|
+
// deterministic, so the byte-identity guarantee holds.
|
|
25
25
|
return [
|
|
26
26
|
` ${member}:`,
|
|
27
27
|
`${sorted.map((path) => ` | ${JSON.stringify(path)}`).join("\n")};`,
|
|
28
28
|
];
|
|
29
29
|
});
|
|
30
30
|
if (members.length === 0) {
|
|
31
|
-
//
|
|
31
|
+
// No routes yet → NO members (explicitly not `never`, which would
|
|
32
32
|
// make every route-constructor call an error). The empty merge keeps
|
|
33
|
-
// the documented world-A `string` fallback
|
|
33
|
+
// the documented world-A `string` fallback.
|
|
34
34
|
return [
|
|
35
35
|
HEADER,
|
|
36
36
|
'import "paramour";',
|
|
37
37
|
"",
|
|
38
38
|
'declare module "paramour" {',
|
|
39
|
-
" //
|
|
39
|
+
" // Empty merge — no routes discovered yet; the pre-generation",
|
|
40
40
|
" // `string` fallback stays in effect.",
|
|
41
41
|
" // eslint-disable-next-line @typescript-eslint/no-empty-object-type",
|
|
42
42
|
" interface ParamourRegister {}",
|
|
@@ -57,9 +57,9 @@ export function emitArtifact(routes) {
|
|
|
57
57
|
].join("\n");
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
|
-
* Compare-before-write
|
|
61
|
-
*
|
|
62
|
-
* concurrent-generator races
|
|
60
|
+
* Compare-before-write: a no-op regeneration must not touch the file — that
|
|
61
|
+
* property is what prevents TS-server churn, watch-loop feedback, and
|
|
62
|
+
* concurrent-generator races.
|
|
63
63
|
*/
|
|
64
64
|
export function writeIfChanged(filePath, content) {
|
|
65
65
|
const previousContent = existsSync(filePath)
|
|
@@ -67,7 +67,7 @@ export function writeIfChanged(filePath, content) {
|
|
|
67
67
|
: null;
|
|
68
68
|
if (previousContent === content)
|
|
69
69
|
return { previousContent, written: false };
|
|
70
|
-
//
|
|
70
|
+
// The outFile escape hatch may point into a not-yet-existing directory.
|
|
71
71
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
72
72
|
writeFileSync(filePath, content);
|
|
73
73
|
return { previousContent, written: true };
|
package/dist/generate.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared generation engine
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The shared generation engine: `withTypedRoutes` and the CLI drive these
|
|
3
|
+
* same functions, so wrapper and `paramour generate` cannot drift on what a
|
|
4
|
+
* pass produces. Everything here is @internal — not barrel API.
|
|
5
5
|
*/
|
|
6
6
|
/** Result of {@link checkArtifact}. */
|
|
7
7
|
export interface CheckResult {
|
|
8
|
-
/** App-router drift — split per router so the report names it
|
|
8
|
+
/** App-router drift — split per router so the report names it. */
|
|
9
9
|
app: RouterDrift;
|
|
10
10
|
/** `true` when the artifact file does not exist at all. */
|
|
11
11
|
missingFile: boolean;
|
|
@@ -14,7 +14,7 @@ export interface CheckResult {
|
|
|
14
14
|
/** `true` on a byte-identical artifact — the only non-drift state. */
|
|
15
15
|
upToDate: boolean;
|
|
16
16
|
}
|
|
17
|
-
/** Inputs to one generation/check pass; either dir may be absent
|
|
17
|
+
/** Inputs to one generation/check pass; either dir may be absent. */
|
|
18
18
|
export interface GenerateInputs {
|
|
19
19
|
appDir?: string | undefined;
|
|
20
20
|
artifactPath: string;
|
|
@@ -29,10 +29,10 @@ export interface GenerateResult {
|
|
|
29
29
|
pagesRoutes: string[];
|
|
30
30
|
/** Prior artifact content, `null` when the file did not exist. */
|
|
31
31
|
previousContent: null | string;
|
|
32
|
-
/** `false` on a byte-identical no-op (
|
|
32
|
+
/** `false` on a byte-identical no-op (write-if-changed). */
|
|
33
33
|
written: boolean;
|
|
34
34
|
}
|
|
35
|
-
/** One router's appeared/disappeared route paths
|
|
35
|
+
/** One router's appeared/disappeared route paths. */
|
|
36
36
|
export interface RouterDrift {
|
|
37
37
|
/** Routes on disk (scan) that the artifact lacks. */
|
|
38
38
|
appeared: string[];
|
|
@@ -40,25 +40,25 @@ export interface RouterDrift {
|
|
|
40
40
|
disappeared: string[];
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* `--check
|
|
44
|
-
*
|
|
45
|
-
* CI-degrades-to-world-A case the committed file exists to prevent
|
|
43
|
+
* `--check`: scan to memory and byte-compare against disk — never writes. A
|
|
44
|
+
* missing artifact is drift, not an error: that is exactly the
|
|
45
|
+
* CI-degrades-to-world-A case the committed file exists to prevent.
|
|
46
46
|
*/
|
|
47
47
|
export declare function checkArtifact(inputs: GenerateInputs): CheckResult;
|
|
48
48
|
/**
|
|
49
49
|
* Per-router drift of a completed {@link generate} pass against the artifact
|
|
50
|
-
* it replaced — the wrapper's build-phase drift report
|
|
50
|
+
* it replaced — the wrapper's build-phase drift report.
|
|
51
51
|
*/
|
|
52
52
|
export declare function diffGenerated(result: GenerateResult): {
|
|
53
53
|
app: RouterDrift;
|
|
54
54
|
pages: RouterDrift;
|
|
55
55
|
};
|
|
56
56
|
/**
|
|
57
|
-
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report
|
|
58
|
-
*
|
|
57
|
+
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report — each
|
|
58
|
+
* line names the router its path moved in.
|
|
59
59
|
*/
|
|
60
60
|
export declare function formatRouteDiff(app: RouterDrift, pages: RouterDrift): string[];
|
|
61
|
-
/** One generation pass: scan → emit → write-if-changed
|
|
61
|
+
/** One generation pass: scan → emit → write-if-changed. */
|
|
62
62
|
export declare function generate(inputs: GenerateInputs): GenerateResult;
|
|
63
63
|
/**
|
|
64
64
|
* Per-router route paths in a previously emitted artifact; both empty for a
|
package/dist/generate.js
CHANGED
|
@@ -3,17 +3,17 @@ import { emitArtifact, writeIfChanged } from "./emit.js";
|
|
|
3
3
|
import { scanRoutes } from "./scan.js";
|
|
4
4
|
/**
|
|
5
5
|
* Reads the per-router unions back out of a previously emitted artifact for
|
|
6
|
-
* drift diffs
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* drift diffs. Only ever applied to text this package generated (its
|
|
7
|
+
* deterministic form), so line-anchored matches on the member headers and
|
|
8
|
+
* union members are exact, not heuristic. `\s*$` on both tolerates a
|
|
9
9
|
* CRLF-resaved artifact.
|
|
10
10
|
*/
|
|
11
11
|
const MEMBER_HEADER = /^\s*(appRoutes|pagesRoutes):\s*$/;
|
|
12
12
|
const UNION_MEMBER = /^\s*\| "(.*)";?\s*$/;
|
|
13
13
|
/**
|
|
14
|
-
* `--check
|
|
15
|
-
*
|
|
16
|
-
* CI-degrades-to-world-A case the committed file exists to prevent
|
|
14
|
+
* `--check`: scan to memory and byte-compare against disk — never writes. A
|
|
15
|
+
* missing artifact is drift, not an error: that is exactly the
|
|
16
|
+
* CI-degrades-to-world-A case the committed file exists to prevent.
|
|
17
17
|
*/
|
|
18
18
|
export function checkArtifact(inputs) {
|
|
19
19
|
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
@@ -39,7 +39,7 @@ export function checkArtifact(inputs) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Per-router drift of a completed {@link generate} pass against the artifact
|
|
42
|
-
* it replaced — the wrapper's build-phase drift report
|
|
42
|
+
* it replaced — the wrapper's build-phase drift report.
|
|
43
43
|
*/
|
|
44
44
|
export function diffGenerated(result) {
|
|
45
45
|
const previous = parseArtifactRoutes(result.previousContent);
|
|
@@ -49,8 +49,8 @@ export function diffGenerated(result) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report
|
|
53
|
-
*
|
|
52
|
+
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report — each
|
|
53
|
+
* line names the router its path moved in.
|
|
54
54
|
*/
|
|
55
55
|
export function formatRouteDiff(app, pages) {
|
|
56
56
|
return [
|
|
@@ -60,7 +60,7 @@ export function formatRouteDiff(app, pages) {
|
|
|
60
60
|
...pages.disappeared.map((path) => ` - ${path} (pages)`),
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
|
-
/** One generation pass: scan → emit → write-if-changed
|
|
63
|
+
/** One generation pass: scan → emit → write-if-changed. */
|
|
64
64
|
export function generate(inputs) {
|
|
65
65
|
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
66
66
|
return {
|
|
@@ -92,7 +92,7 @@ export function parseArtifactRoutes(previousContent) {
|
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
// Any other line ends the member block — union members are contiguous
|
|
95
|
-
// in the
|
|
95
|
+
// in the deterministic emitted form.
|
|
96
96
|
current = undefined;
|
|
97
97
|
}
|
|
98
98
|
return { appRoutes, pagesRoutes };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const AGENTS_MARKER_START = "<!-- paramour:start -->";
|
|
2
|
+
export declare const AGENTS_MARKER_END = "<!-- paramour:end -->";
|
|
3
|
+
/**
|
|
4
|
+
* @internal The marker-managed section init appends to an agent
|
|
5
|
+
* instructions file. Install-dir-agnostic on purpose: the skill lands in
|
|
6
|
+
* `.claude/`, `.cursor/`, or the portable `.agents/` depending on what
|
|
7
|
+
* `detectTargets` found, and this snippet must stay true for all of them.
|
|
8
|
+
*/
|
|
9
|
+
export declare function agentsSnippet(): string;
|
|
10
|
+
/**
|
|
11
|
+
* @internal The instructions file the snippet goes into: AGENTS.md first
|
|
12
|
+
* (the cross-tool convention), CLAUDE.md as the fallback, never both (tools
|
|
13
|
+
* that read both would see duplicated content, and projects keeping both
|
|
14
|
+
* usually mirror them — one marker-managed copy is the single source of
|
|
15
|
+
* truth). Never creates a file: a root AGENTS.md is a `detectTargets`
|
|
16
|
+
* detection signal, so init inventing one would change what the skills
|
|
17
|
+
* step sees on the next run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findAgentsFile(projectRoot: string): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* @internal Add or refresh the marker-delimited paramour section. Pure
|
|
22
|
+
* string→string (the `addPackageScript` pattern) so the write/dry-run
|
|
23
|
+
* decision stays with the caller. Re-runs reconcile the section to the
|
|
24
|
+
* current snippet — the markers exist precisely to delimit
|
|
25
|
+
* paramour-managed content; prose outside them is never touched. A start
|
|
26
|
+
* marker without an end marker is the one state left alone: repairing it
|
|
27
|
+
* would mean guessing where the user's own text begins.
|
|
28
|
+
*/
|
|
29
|
+
export declare function upsertAgentsSection(text: string): {
|
|
30
|
+
status: "added" | "unchanged" | "unterminated" | "updated";
|
|
31
|
+
text: string;
|
|
32
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const AGENTS_MARKER_START = "<!-- paramour:start -->";
|
|
4
|
+
export const AGENTS_MARKER_END = "<!-- paramour:end -->";
|
|
5
|
+
/**
|
|
6
|
+
* @internal The marker-managed section init appends to an agent
|
|
7
|
+
* instructions file. Install-dir-agnostic on purpose: the skill lands in
|
|
8
|
+
* `.claude/`, `.cursor/`, or the portable `.agents/` depending on what
|
|
9
|
+
* `detectTargets` found, and this snippet must stay true for all of them.
|
|
10
|
+
*/
|
|
11
|
+
export function agentsSnippet() {
|
|
12
|
+
return [
|
|
13
|
+
AGENTS_MARKER_START,
|
|
14
|
+
"",
|
|
15
|
+
"## paramour",
|
|
16
|
+
"",
|
|
17
|
+
"This project uses paramour for type-safe routing. The paramour agent",
|
|
18
|
+
"skill (installed by `paramour skills` into detected agent-tool skills",
|
|
19
|
+
"directories) documents codecs, route definitions, hooks, and the CLI —",
|
|
20
|
+
"read it before route work.",
|
|
21
|
+
"",
|
|
22
|
+
"After changing routes: run `paramour generate`, verify with `paramour",
|
|
23
|
+
"check` (exit 0 = artifact current), inspect shapes with `paramour list`,",
|
|
24
|
+
"and commit `paramour-env.d.ts`.",
|
|
25
|
+
"",
|
|
26
|
+
AGENTS_MARKER_END,
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @internal The instructions file the snippet goes into: AGENTS.md first
|
|
31
|
+
* (the cross-tool convention), CLAUDE.md as the fallback, never both (tools
|
|
32
|
+
* that read both would see duplicated content, and projects keeping both
|
|
33
|
+
* usually mirror them — one marker-managed copy is the single source of
|
|
34
|
+
* truth). Never creates a file: a root AGENTS.md is a `detectTargets`
|
|
35
|
+
* detection signal, so init inventing one would change what the skills
|
|
36
|
+
* step sees on the next run.
|
|
37
|
+
*/
|
|
38
|
+
export function findAgentsFile(projectRoot) {
|
|
39
|
+
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
40
|
+
const path = join(projectRoot, name);
|
|
41
|
+
if (existsSync(path))
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @internal Add or refresh the marker-delimited paramour section. Pure
|
|
48
|
+
* string→string (the `addPackageScript` pattern) so the write/dry-run
|
|
49
|
+
* decision stays with the caller. Re-runs reconcile the section to the
|
|
50
|
+
* current snippet — the markers exist precisely to delimit
|
|
51
|
+
* paramour-managed content; prose outside them is never touched. A start
|
|
52
|
+
* marker without an end marker is the one state left alone: repairing it
|
|
53
|
+
* would mean guessing where the user's own text begins.
|
|
54
|
+
*/
|
|
55
|
+
export function upsertAgentsSection(text) {
|
|
56
|
+
const start = text.indexOf(AGENTS_MARKER_START);
|
|
57
|
+
if (start === -1) {
|
|
58
|
+
const base = text === "" || text.endsWith("\n") ? text : `${text}\n`;
|
|
59
|
+
const separator = base === "" ? "" : "\n";
|
|
60
|
+
return {
|
|
61
|
+
status: "added",
|
|
62
|
+
text: `${base}${separator}${agentsSnippet()}\n`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const end = text.indexOf(AGENTS_MARKER_END, start);
|
|
66
|
+
if (end === -1)
|
|
67
|
+
return { status: "unterminated", text };
|
|
68
|
+
const current = text.slice(start, end + AGENTS_MARKER_END.length);
|
|
69
|
+
const snippet = agentsSnippet();
|
|
70
|
+
if (current === snippet)
|
|
71
|
+
return { status: "unchanged", text };
|
|
72
|
+
return {
|
|
73
|
+
status: "updated",
|
|
74
|
+
text: text.slice(0, start) +
|
|
75
|
+
snippet +
|
|
76
|
+
text.slice(end + AGENTS_MARKER_END.length),
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/init/scaffold.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { resolveRouteDirs } from "../scan.js";
|
|
4
|
+
import { loadPackagedSkill } from "../skills/packaged.js";
|
|
5
|
+
import { auditTarget, isOutdated } from "../skills/sync.js";
|
|
6
|
+
import { detectTargets } from "../skills/targets.js";
|
|
4
7
|
/**
|
|
5
8
|
* Insert `"paramour": "paramour generate"` into a package.json's scripts,
|
|
6
9
|
* preserving the file's own indentation and trailing-newline choice.
|
|
@@ -58,6 +61,7 @@ export function checkSetup(projectRoot, artifactPath) {
|
|
|
58
61
|
}
|
|
59
62
|
checks.push(dependenciesCheck(projectRoot));
|
|
60
63
|
checks.push(tsconfigCheck(projectRoot, artifactPath));
|
|
64
|
+
checks.push(...skillsSetupCheck(projectRoot));
|
|
61
65
|
return checks;
|
|
62
66
|
}
|
|
63
67
|
/** The starter `paramour.config.ts` — every field commented-out defaults. */
|
|
@@ -205,6 +209,56 @@ function dependenciesCheck(projectRoot) {
|
|
|
205
209
|
ok: false,
|
|
206
210
|
};
|
|
207
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Agent-skills summary line — only for projects with agent tooling; an
|
|
214
|
+
* agent-free project gets no line at all, keeping the summary signal-dense.
|
|
215
|
+
*/
|
|
216
|
+
function skillsSetupCheck(projectRoot) {
|
|
217
|
+
try {
|
|
218
|
+
const detected = detectTargets(projectRoot);
|
|
219
|
+
if (detected.length === 0)
|
|
220
|
+
return [];
|
|
221
|
+
const packaged = loadPackagedSkill();
|
|
222
|
+
const audits = detected
|
|
223
|
+
.map((target) => auditTarget(target, packaged))
|
|
224
|
+
.filter((audit) => audit.manifest !== undefined);
|
|
225
|
+
if (audits.length === 0) {
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
detail: "run `paramour skills`",
|
|
229
|
+
label: "agent skills: not installed",
|
|
230
|
+
ok: false,
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
// Local edits are legitimate tailoring, not drift — only missing/stale
|
|
235
|
+
// content makes the line a warning.
|
|
236
|
+
const outdated = audits.filter((audit) => audit.files.some((file) => isOutdated(file.status)));
|
|
237
|
+
if (outdated.length > 0) {
|
|
238
|
+
return [
|
|
239
|
+
{
|
|
240
|
+
detail: "run `paramour skills` to re-sync",
|
|
241
|
+
label: `agent skills: ${outdated
|
|
242
|
+
.map((audit) => audit.target.rel)
|
|
243
|
+
.join(", ")} out of date`,
|
|
244
|
+
ok: false,
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
return [
|
|
249
|
+
{
|
|
250
|
+
label: `agent skills: ${audits
|
|
251
|
+
.map((audit) => audit.target.rel)
|
|
252
|
+
.join(", ")} up to date`,
|
|
253
|
+
ok: true,
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// A broken skills probe must not break the warn-level summary.
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
208
262
|
/**
|
|
209
263
|
* String-aware trailing-comma removal over comment-free JSONC — a flat
|
|
210
264
|
* regex would also rewrite `,}`/`,]` inside string literals (glob patterns).
|
|
@@ -37,10 +37,10 @@ export interface RouteDefinition {
|
|
|
37
37
|
* its text mentions a define constructor (grep-then-load). `routeFiles`
|
|
38
38
|
* config globs replace the default patterns when the heuristic misfires.
|
|
39
39
|
*
|
|
40
|
-
* jiti evaluates matched files (the
|
|
41
|
-
* both handled by the per-module degrade: tsconfig `paths`
|
|
42
|
-
* resolved (a future improvement could feed them into jiti's
|
|
43
|
-
* option), and `server-only`-style imports throw outside Next.
|
|
40
|
+
* jiti evaluates matched files (the loader carry-over from config loading).
|
|
41
|
+
* Known limits, both handled by the per-module degrade: tsconfig `paths`
|
|
42
|
+
* aliases are not resolved (a future improvement could feed them into jiti's
|
|
43
|
+
* `alias` option), and `server-only`-style imports throw outside Next.
|
|
44
44
|
*/
|
|
45
45
|
export declare function discoverRouteDefinitions(projectRoot: string, options?: {
|
|
46
46
|
routeFiles?: readonly string[] | undefined;
|
|
@@ -23,10 +23,10 @@ const MAX_PREFILTER_BYTES = 512 * 1024;
|
|
|
23
23
|
* its text mentions a define constructor (grep-then-load). `routeFiles`
|
|
24
24
|
* config globs replace the default patterns when the heuristic misfires.
|
|
25
25
|
*
|
|
26
|
-
* jiti evaluates matched files (the
|
|
27
|
-
* both handled by the per-module degrade: tsconfig `paths`
|
|
28
|
-
* resolved (a future improvement could feed them into jiti's
|
|
29
|
-
* option), and `server-only`-style imports throw outside Next.
|
|
26
|
+
* jiti evaluates matched files (the loader carry-over from config loading).
|
|
27
|
+
* Known limits, both handled by the per-module degrade: tsconfig `paths`
|
|
28
|
+
* aliases are not resolved (a future improvement could feed them into jiti's
|
|
29
|
+
* `alias` option), and `server-only`-style imports throw outside Next.
|
|
30
30
|
*/
|
|
31
31
|
export async function discoverRouteDefinitions(projectRoot, options = {}) {
|
|
32
32
|
// Dynamic imports, same stance as config.ts: only commands that actually
|