@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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +48 -49
  3. package/dist/app.js +11 -12
  4. package/dist/cli-args.d.ts +4 -4
  5. package/dist/cli-args.js +4 -4
  6. package/dist/cli-inputs.d.ts +6 -7
  7. package/dist/cli-inputs.js +6 -7
  8. package/dist/cli.js +1 -1
  9. package/dist/collisions.d.ts +8 -8
  10. package/dist/collisions.js +9 -9
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.js +12 -7
  13. package/dist/commands/generate.d.ts +55 -7
  14. package/dist/commands/generate.js +29 -22
  15. package/dist/commands/init.d.ts +40 -0
  16. package/dist/commands/init.js +111 -19
  17. package/dist/commands/list.d.ts +21 -0
  18. package/dist/commands/list.js +12 -7
  19. package/dist/commands/skills.d.ts +45 -0
  20. package/dist/commands/skills.js +222 -0
  21. package/dist/config.d.ts +17 -10
  22. package/dist/config.js +17 -4
  23. package/dist/devtools-seam.d.ts +19 -19
  24. package/dist/devtools-seam.js +3 -3
  25. package/dist/doctor/checks.js +7 -3
  26. package/dist/emit.d.ts +12 -12
  27. package/dist/emit.js +13 -13
  28. package/dist/generate.d.ts +14 -14
  29. package/dist/generate.js +11 -11
  30. package/dist/init/agents-md.d.ts +32 -0
  31. package/dist/init/agents-md.js +78 -0
  32. package/dist/init/scaffold.js +54 -0
  33. package/dist/list/discover-route-defs.d.ts +4 -4
  34. package/dist/list/discover-route-defs.js +4 -4
  35. package/dist/lock.d.ts +8 -9
  36. package/dist/lock.js +11 -12
  37. package/dist/navigation-adapter.d.ts +17 -18
  38. package/dist/navigation-adapter.js +4 -4
  39. package/dist/observe.d.ts +14 -14
  40. package/dist/observe.js +4 -4
  41. package/dist/pages.d.ts +30 -31
  42. package/dist/pages.js +10 -10
  43. package/dist/run-cli.d.ts +3 -1
  44. package/dist/run-cli.js +7 -3
  45. package/dist/scan-app.d.ts +10 -10
  46. package/dist/scan-app.js +30 -30
  47. package/dist/scan-pages.d.ts +6 -6
  48. package/dist/scan-pages.js +28 -26
  49. package/dist/scan.d.ts +13 -10
  50. package/dist/scan.js +7 -7
  51. package/dist/select.d.ts +40 -40
  52. package/dist/select.js +30 -30
  53. package/dist/skills/doctor.d.ts +11 -0
  54. package/dist/skills/doctor.js +71 -0
  55. package/dist/skills/manifest.d.ts +41 -0
  56. package/dist/skills/manifest.js +96 -0
  57. package/dist/skills/packaged.d.ts +21 -0
  58. package/dist/skills/packaged.js +32 -0
  59. package/dist/skills/sync.d.ts +84 -0
  60. package/dist/skills/sync.js +136 -0
  61. package/dist/skills/targets.d.ts +29 -0
  62. package/dist/skills/targets.js +73 -0
  63. package/dist/testing.d.ts +28 -32
  64. package/dist/testing.js +15 -15
  65. package/dist/watch.d.ts +12 -12
  66. package/dist/watch.js +13 -13
  67. package/dist/with-typed-routes.d.ts +11 -10
  68. package/dist/with-typed-routes.js +42 -39
  69. package/package.json +4 -3
  70. package/skills/paramour/SKILL.md +43 -0
  71. package/skills/paramour/references/authoring.md +113 -0
  72. package/skills/paramour/references/migration.md +141 -0
  73. package/skills/paramour/references/reference.md +96 -0
  74. package/skills/paramour/references/setup.md +139 -0
@@ -1,6 +1,6 @@
1
1
  import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
2
2
  /**
3
- * The devtools observation seam (design-12 DT5): a dependency-free global
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 (RL6): a second physical copy of this module (dual-package
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 (DT6): every emit call site sits behind
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 DT5 contract. */
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 (design-12 DT4). */
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 (design-12
50
- * DT8): the panel commits URL edits through this, so it never guesses which
51
- * router is live and never imports Next. The panel passes ONLY the
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 (design-12 DT4) — and
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 (design-12 DT12): the hook's full `SafeResult`
71
- * — the error arm carries the LIVE `ParamsDecodeError`/`SearchDecodeError`
72
- * with its `issues` — never the user's `select` projection. `pending` is
73
- * the Pages-only third state (DT11). Generic-erased on purpose: the panel
74
- * treats `data` structurally.
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 (DT8).
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 (DT5: same JS context, no serialization) — the
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 (DT4) means even a long pre-open session is dozens
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 under DT6 (every call site is ALSO
127
- * guarded, which is what the bundler erases); it makes the guard directly
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;
@@ -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 (DT4) means even a long pre-open session is dozens
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 under DT6 (every call site is ALSO
14
- * guarded, which is what the bundler erases); it makes the guard directly
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) {
@@ -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 (PR8 discovery, config dirs honored).
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. tsconfig covers the artifact (init's warn-level heuristic).
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
- // 7. Route-definition discovery health (list's engine).
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 TR4's drift warning (which paths appeared/disappeared).
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` (TR7) and the
10
- * watch loop (TR5) hang off.
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 (PR9). */
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 (TR3/PR9): deterministic —
21
- * sorted, deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
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 (TR3's absent-not-`never` rule, applied per router in
26
- * PR9): a pages-only project keeps world-A's permissive `string` fallback
27
- * for `defineAppRoute`, and vice versa.
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 (TR3): a no-op regeneration must not touch the file —
32
- * that property is what prevents TS-server churn, watch-loop feedback, and
33
- * concurrent-generator races (TR6).
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 (TR3/PR9): deterministic —
6
- * sorted, deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
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 (TR3's absent-not-`never` rule, applied per router in
11
- * PR9): a pages-only project keeps world-A's permissive `string` fallback
12
- * for `defineAppRoute`, and vice versa.
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 TR3's byte-identity guarantee holds.
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
- // TR3: no routes yet → NO members (explicitly not `never`, which would
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 (RL8).
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
- " // TR3: empty merge — no routes discovered yet; the pre-generation",
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 (TR3): a no-op regeneration must not touch the file —
61
- * that property is what prevents TS-server churn, watch-loop feedback, and
62
- * concurrent-generator races (TR6).
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
- // TR3's outFile escape hatch may point into a not-yet-existing directory.
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 };
@@ -1,11 +1,11 @@
1
1
  /**
2
- * The shared generation engine (TR9): `withTypedRoutes` and the CLI drive
3
- * these same functions, so wrapper and `paramour generate` cannot drift on
4
- * what a pass produces. Everything here is @internal — not barrel API.
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 (PR9). */
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 (PR1). */
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 (TR3 write-if-changed). */
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 (TR4/TR7 drift). */
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` (TR7): scan to memory and byte-compare against disk — never
44
- * writes. A missing artifact is drift, not an error: that is exactly the
45
- * CI-degrades-to-world-A case the committed file exists to prevent (TR3).
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 (TR4).
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
- * (TR4/TR7) — each line names the router its path moved in (PR9).
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 (TR3). */
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 (TR4/TR7). Only ever applied to text this package generated
7
- * (TR3 deterministic form), so line-anchored matches on the member headers
8
- * and union members are exact, not heuristic. `\s*$` on both tolerates a
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` (TR7): scan to memory and byte-compare against disk — never
15
- * writes. A missing artifact is drift, not an error: that is exactly the
16
- * CI-degrades-to-world-A case the committed file exists to prevent (TR3).
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 (TR4).
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
- * (TR4/TR7) — each line names the router its path moved in (PR9).
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 (TR3). */
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 TR3 deterministic form.
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
+ }
@@ -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 §7.2 loader carry-over). Known limits,
41
- * both handled by the per-module degrade: tsconfig `paths` aliases are not
42
- * resolved (a future improvement could feed them into jiti's `alias`
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 §7.2 loader carry-over). Known limits,
27
- * both handled by the per-module degrade: tsconfig `paths` aliases are not
28
- * resolved (a future improvement could feed them into jiti's `alias`
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