@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
package/dist/lock.d.ts CHANGED
@@ -12,18 +12,17 @@ export interface AcquireLockResult {
12
12
  release?: () => void;
13
13
  }
14
14
  /**
15
- * Cross-process single-writer guard (TR6): a best-effort pidfile lock. On
16
- * startup: read lock → liveness-probe the owner → decline if alive,
17
- * (over)write and acquire if dead or absent. Deliberately best-effort, not
18
- * correct — TR3's deterministic write-if-changed output means two live
19
- * watchers produce identical bytes; imperfect locking costs a log line, not
20
- * corruption. Hence no flock semantics, atomic-rename dances, or PID-reuse
21
- * paranoia. The in-process singleton (TR6 guard 1) lives at the composition
22
- * points, not here.
15
+ * Cross-process single-writer guard: a best-effort pidfile lock. On startup:
16
+ * read lock → liveness-probe the owner → decline if alive, (over)write and
17
+ * acquire if dead or absent. Deliberately best-effort, not correct — the
18
+ * deterministic write-if-changed output means two live watchers produce
19
+ * identical bytes; imperfect locking costs a log line, not corruption. Hence
20
+ * no flock semantics, atomic-rename dances, or PID-reuse paranoia. The
21
+ * in-process singleton guard lives at the composition points, not here.
23
22
  */
24
23
  export declare function acquireWatcherLock(lockPath: string): AcquireLockResult;
25
24
  /**
26
- * The one canonical pidfile location (TR6): CLI-vs-wrapper dedupe only works
25
+ * The one canonical pidfile location: CLI-vs-wrapper dedupe only works
27
26
  * because both paths compute the lock from the same project root.
28
27
  */
29
28
  export declare function watcherLockPath(projectRoot: string): string;
package/dist/lock.js CHANGED
@@ -3,14 +3,13 @@ import { dirname, join } from "node:path";
3
3
  /** Strict anchored PID parse — anything else is a stale/corrupt lock. */
4
4
  const PID_RE = /^\d+$/;
5
5
  /**
6
- * Cross-process single-writer guard (TR6): a best-effort pidfile lock. On
7
- * startup: read lock → liveness-probe the owner → decline if alive,
8
- * (over)write and acquire if dead or absent. Deliberately best-effort, not
9
- * correct — TR3's deterministic write-if-changed output means two live
10
- * watchers produce identical bytes; imperfect locking costs a log line, not
11
- * corruption. Hence no flock semantics, atomic-rename dances, or PID-reuse
12
- * paranoia. The in-process singleton (TR6 guard 1) lives at the composition
13
- * points, not here.
6
+ * Cross-process single-writer guard: a best-effort pidfile lock. On startup:
7
+ * read lock → liveness-probe the owner → decline if alive, (over)write and
8
+ * acquire if dead or absent. Deliberately best-effort, not correct — the
9
+ * deterministic write-if-changed output means two live watchers produce
10
+ * identical bytes; imperfect locking costs a log line, not corruption. Hence
11
+ * no flock semantics, atomic-rename dances, or PID-reuse paranoia. The
12
+ * in-process singleton guard lives at the composition points, not here.
14
13
  */
15
14
  export function acquireWatcherLock(lockPath) {
16
15
  const ownerPid = readOwnerPid(lockPath);
@@ -35,8 +34,8 @@ export function acquireWatcherLock(lockPath) {
35
34
  }
36
35
  }
37
36
  catch {
38
- // Best-effort (TR6): a leftover lock self-heals via the liveness
39
- // probe on the next startup.
37
+ // Best-effort: a leftover lock self-heals via the liveness probe on
38
+ // the next startup.
40
39
  }
41
40
  };
42
41
  const reraise = (signal) => {
@@ -57,13 +56,13 @@ export function acquireWatcherLock(lockPath) {
57
56
  return { acquired: true, release };
58
57
  }
59
58
  /**
60
- * The one canonical pidfile location (TR6): CLI-vs-wrapper dedupe only works
59
+ * The one canonical pidfile location: CLI-vs-wrapper dedupe only works
61
60
  * because both paths compute the lock from the same project root.
62
61
  */
63
62
  export function watcherLockPath(projectRoot) {
64
63
  return join(projectRoot, "node_modules", ".cache", "paramour", "watcher.lock");
65
64
  }
66
- /** `true` when `pid` is a live process (TR6 liveness probe). */
65
+ /** `true` when `pid` is a live process — the liveness probe. */
67
66
  function isAlive(pid) {
68
67
  try {
69
68
  process.kill(pid, 0);
@@ -1,14 +1,13 @@
1
1
  import type { ParamsSource } from "paramour";
2
2
  /**
3
- * Adapter seam for the client hooks' framework reads (design-16 TA1): each
4
- * flavor's hooks resolve Next through a React context so the /testing entry
5
- * can override the reads without runner-specific module mocking.
6
- * Deliberately NO `"use client"` directive — app.ts (which carries one) and
7
- * pages.ts (which must not, PR2) both import from here, like
8
- * observe.ts/select.ts; the directive belongs on the entry modules, not a
9
- * shared leaf.
3
+ * Adapter seam for the client hooks' framework reads: each flavor's hooks
4
+ * resolve Next through a React context so the /testing entry can override
5
+ * the reads without runner-specific module mocking. Deliberately NO
6
+ * `"use client"` directive — app.ts (which carries one) and pages.ts (which
7
+ * must not) both import from here, like observe.ts/select.ts; the directive
8
+ * belongs on the entry modules, not a shared leaf.
10
9
  *
11
- * This module is Next-free on purpose (TA3): the contexts default to `null`
10
+ * This module is Next-free on purpose: the contexts default to `null`
12
11
  * and each flavor ENTRY supplies its own real-Next fallback
13
12
  * (`useContext(ctx) ?? realAdapter`), so neither this module nor the
14
13
  * /testing entry ever drags a `next/*` specifier into its graph. The
@@ -20,11 +19,11 @@ import type { ParamsSource } from "paramour";
20
19
  /**
21
20
  * App-flavor adapter: EXACTLY the ambient view of `next/navigation` declared
22
21
  * in `src/types/next-navigation.d.ts` — that ambient is the contract of
23
- * record, and this interface must stay in lockstep with it (TA2; the
22
+ * record, and this interface must stay in lockstep with it (the
24
23
  * `examples/next-compat` pins guard the real-Next side). `useParams()`'s
25
24
  * `null` arm is the outside-App-Router-tree state (Next #48058 family) the
26
25
  * hooks deliberately tolerate; `useRouter().replace`/`usePathname` are the
27
- * devtools `navigate` capability's write path and resolution base (DT8).
26
+ * devtools `navigate` capability's write path and resolution base.
28
27
  */
29
28
  export interface AppNavigationAdapter {
30
29
  useParams(): null | ParamsSource;
@@ -37,10 +36,10 @@ export interface AppNavigationAdapter {
37
36
  /**
38
37
  * Pages-flavor adapter: EXACTLY the ambient view of `next/router.js`
39
38
  * declared in `src/types/next-router.d.ts` — that ambient is the contract of
40
- * record, and this interface must stay in lockstep with it (TA2). The
41
- * ambient's throw-on-unmounted behavior under `app/` (PR5) is part of the
42
- * contract: adapter implementations reproduce it by THROWING from
43
- * `useRouter()`, which pages.ts translates.
39
+ * record, and this interface must stay in lockstep with it. The ambient's
40
+ * throw-on-unmounted behavior under `app/` is part of the contract: adapter
41
+ * implementations reproduce it by THROWING from `useRouter()`, which
42
+ * pages.ts translates.
44
43
  */
45
44
  export interface PagesNavigationAdapter {
46
45
  useRouter(): {
@@ -51,10 +50,10 @@ export interface PagesNavigationAdapter {
51
50
  };
52
51
  }
53
52
  /**
54
- * The `null` default is load-bearing (TA3): with no provider mounted,
55
- * app.ts falls back to its real `next/navigation` adapter — zero markup and
56
- * zero behavior change in production (TA4).
53
+ * The `null` default is load-bearing: with no provider mounted, app.ts falls
54
+ * back to its real `next/navigation` adapter — zero markup and zero behavior
55
+ * change in production.
57
56
  */
58
57
  export declare const AppNavigationContext: import("react").Context<AppNavigationAdapter | null>;
59
- /** The pages twin; `null` default load-bearing for the same TA3 reasons. */
58
+ /** The pages twin; its `null` default is load-bearing for the same reasons. */
60
59
  export declare const PagesNavigationContext: import("react").Context<PagesNavigationAdapter | null>;
@@ -1,9 +1,9 @@
1
1
  import { createContext } from "react";
2
2
  /**
3
- * The `null` default is load-bearing (TA3): with no provider mounted,
4
- * app.ts falls back to its real `next/navigation` adapter — zero markup and
5
- * zero behavior change in production (TA4).
3
+ * The `null` default is load-bearing: with no provider mounted, app.ts falls
4
+ * back to its real `next/navigation` adapter — zero markup and zero behavior
5
+ * change in production.
6
6
  */
7
7
  export const AppNavigationContext = createContext(null);
8
- /** The pages twin; `null` default load-bearing for the same TA3 reasons. */
8
+ /** The pages twin; its `null` default is load-bearing for the same reasons. */
9
9
  export const PagesNavigationContext = createContext(null);
package/dist/observe.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { AnyRoute, ParamsSource, RouterKind } from "paramour";
2
2
  import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, ParamourSearchWire } from "./devtools-seam.js";
3
3
  /**
4
- * Shared devtools seam wiring for the six read hooks (design-12 DT4/DT8):
5
- * the navigate builders (one per router flavor) and the per-hook emitter
6
- * that owns WHEN an observation goes out. Deliberately NO `"use client"`
7
- * directive — app.ts (which carries one) and pages.ts (which must not,
8
- * PR2) both import from here, like select.ts.
4
+ * Shared devtools seam wiring for the six read hooks: the navigate builders
5
+ * (one per router flavor) and the per-hook emitter that owns WHEN an
6
+ * observation goes out. Deliberately NO `"use client"` directive — app.ts
7
+ * (which carries one) and pages.ts (which must not) both import from here,
8
+ * like select.ts.
9
9
  *
10
10
  * Emission policy: the hooks call {@link DevtoolsEmitter.observe} from
11
- * inside the `useStableResult` compute — the SEL4 fingerprint cache miss IS
12
- * the decode-change dedup (DT4), and only render-phase can report the
11
+ * inside the `useStableResult` compute — the fingerprint cache miss IS the
12
+ * decode-change dedup, and only render-phase can report the
13
13
  * OrThrow hooks' error observation before the rethrow. `observe` alone
14
14
  * would leave one staleness hole: a component that survives a navigation
15
15
  * whose decode is unchanged (`/product/1?q=a` → `/product/2?q=a` in a
@@ -18,10 +18,10 @@ import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, Param
18
18
  * navigate back to the old resource. {@link DevtoolsEmitter.refresh},
19
19
  * called render-phase after the stable result returns, closes it: when the
20
20
  * resolution base (or a HMR-reminted route) moved under a cached decode, it
21
- * re-emits the CACHED result with the fresh spec — decode stability (SEL4)
22
- * is untouched, only the seam payload is renewed.
21
+ * re-emits the CACHED result with the fresh spec — decode stability is
22
+ * untouched, only the seam payload is renewed.
23
23
  *
24
- * Production erasure (DT6): every call site keeps a literal
24
+ * Production erasure: every call site keeps a literal
25
25
  * `process.env.NODE_ENV` guard the bundler constant-folds, and both emitter
26
26
  * methods early-return behind the same literal guard, so the
27
27
  * `emitObservation` import above is dead in a production bundle and drops
@@ -51,15 +51,15 @@ export interface ObservationSpec {
51
51
  readonly wire: () => ParamourSearchWire | Readonly<ParamsSource>;
52
52
  }
53
53
  /**
54
- * App-flavor navigate capability (DT8): `next/navigation`'s `replace`
55
- * returns void and resolves the basePath-/locale-relative join itself.
54
+ * App-flavor navigate capability: `next/navigation`'s `replace` returns void
55
+ * and resolves the basePath-/locale-relative join itself.
56
56
  */
57
57
  export declare function makeAppNavigate(router: {
58
58
  replace: (href: string) => void;
59
59
  }, pathname: string): ParamourNavigate;
60
60
  /**
61
- * Pages-flavor navigate capability (DT8): `next/router`'s `replace` returns
62
- * a promise that REJECTS on routine navigation aborts (rapid re-commits
61
+ * Pages-flavor navigate capability: `next/router`'s `replace` returns a
62
+ * promise that REJECTS on routine navigation aborts (rapid re-commits
63
63
  * from the panel), marked with next's `cancelled` discriminant — those must
64
64
  * not surface as unhandled rejections. Anything else is a real failure
65
65
  * (render error, route-info error) silently discarding the user's edit, so
package/dist/observe.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { useRef } from "react";
2
2
  import { emitObservation } from "./devtools-seam.js";
3
3
  /**
4
- * App-flavor navigate capability (DT8): `next/navigation`'s `replace`
5
- * returns void and resolves the basePath-/locale-relative join itself.
4
+ * App-flavor navigate capability: `next/navigation`'s `replace` returns void
5
+ * and resolves the basePath-/locale-relative join itself.
6
6
  */
7
7
  export function makeAppNavigate(router, pathname) {
8
8
  return (search) => {
@@ -10,8 +10,8 @@ export function makeAppNavigate(router, pathname) {
10
10
  };
11
11
  }
12
12
  /**
13
- * Pages-flavor navigate capability (DT8): `next/router`'s `replace` returns
14
- * a promise that REJECTS on routine navigation aborts (rapid re-commits
13
+ * Pages-flavor navigate capability: `next/router`'s `replace` returns a
14
+ * promise that REJECTS on routine navigation aborts (rapid re-commits
15
15
  * from the panel), marked with next's `cancelled` discriminant — those must
16
16
  * not surface as unhandled rejections. Anything else is a real failure
17
17
  * (render error, route-info error) silently discarding the user's edit, so
package/dist/pages.d.ts CHANGED
@@ -2,57 +2,56 @@ import { type AnyPagesRoute, type InferRouteParams, type SafeResult, type Search
2
2
  import { type SelectOptions } from "./select.js";
3
3
  export type { SelectOptions } from "./select.js";
4
4
  /**
5
- * Pages Router hooks (design-06 PR5/PR6, design-07). Deliberately NO
6
- * `"use client"` directive on this module: the directive is an App Router
7
- * (RSC graph) concept, meaningless in a `pages/` bundle (PR2).
5
+ * Pages Router hooks. Deliberately NO `"use client"` directive on this
6
+ * module: the directive is an App Router (RSC graph) concept, meaningless in
7
+ * a `pages/` bundle.
8
8
  *
9
9
  * `useRouter().query` is one merged bag (route params + search), and on a
10
10
  * statically-optimized page it is `{}` until `router.isReady` flips after
11
11
  * hydration — a platform fact the result type admits as a third state
12
- * instead of papering over (PR5). On `getServerSideProps` pages the FIRST
13
- * render is already `isReady: true` with a populated query (design-06
14
- * spike 3), so the `pending` arm never surfaces there.
12
+ * instead of papering over. On `getServerSideProps` pages the FIRST render
13
+ * is already `isReady: true` with a populated query, so the `pending` arm
14
+ * never surfaces there.
15
15
  *
16
- * Deliberately NO `OrThrow` variants (PR6): throwing on `pending` would
17
- * flash the error boundary on every statically-optimized page's first
18
- * render, and returning `T | undefined` would make the name a lie. The
19
- * three-state union forcing the check IS the feature — and users who know
20
- * their page has `getServerSideProps` should be reading typed props from
21
- * `route.parseContext(ctx)` (PR10) rather than reaching for a client hook.
16
+ * Deliberately NO `OrThrow` variants: throwing on `pending` would flash the
17
+ * error boundary on every statically-optimized page's first render, and
18
+ * returning `T | undefined` would make the name a lie. The three-state union
19
+ * forcing the check IS the feature — and users who know their page has
20
+ * `getServerSideProps` should be reading typed props from
21
+ * `route.parseContext(ctx)` rather than reaching for a client hook.
22
22
  *
23
- * Both hooks are gated to `AnyPagesRoute` (PR3) and share the /app hooks'
24
- * design-07 layering: raw-slice stabilization keyed on the declared slice of
25
- * `query` (+ `isReady`), then an optional `{ select }` projection with
23
+ * Both hooks are gated to `AnyPagesRoute` and share the /app hooks'
24
+ * layering: raw-slice stabilization keyed on the declared slice of `query`
25
+ * (+ `isReady`), then an optional `{ select }` projection with
26
26
  * result-equality checking — the `pending` arm passes through the selector
27
- * untouched (SEL2), and `PENDING` itself is one referentially stable object.
27
+ * untouched, and `PENDING` itself is one referentially stable object.
28
28
  *
29
- * Devtools instrumentation (design-12): each hook reports through the
30
- * shared emitter in observe.ts — `observe` from inside the `useStableResult`
31
- * compute callback (DT4 — the fingerprint cache miss IS the decode-change
32
- * dedup; see app.ts's fuller account), `refresh` after it for pathname
33
- * moves under an unchanged decode (DT8). The `pending` arm emits as a
34
- * first-class observation (DT11), keyed by `PENDING_FINGERPRINT` so the
35
- * pre-`isReady` render reports exactly once. Every emit sits behind
36
- * `process.env.NODE_ENV !== "production"` (DT6).
29
+ * Devtools instrumentation: each hook reports through the shared emitter in
30
+ * observe.ts — `observe` from inside the `useStableResult` compute callback
31
+ * (the fingerprint cache miss IS the decode-change dedup; see app.ts's
32
+ * fuller account), `refresh` after it for pathname moves under an unchanged
33
+ * decode. The `pending` arm emits as a first-class observation, keyed by
34
+ * `PENDING_FINGERPRINT` so the pre-`isReady` render reports exactly once.
35
+ * Every emit sits behind `process.env.NODE_ENV !== "production"`.
37
36
  */
38
37
  /**
39
- * Three-state result for the pages hooks (PR5): core's `SafeResult` plus a
38
+ * Three-state result for the pages hooks: core's `SafeResult` plus a
40
39
  * `pending` member for the pre-`isReady` render of a statically-optimized
41
- * page. Literally `SafeResult<T> | { status: "pending" }` (PR12), so both
42
- * routers' results destructure identically.
40
+ * page. Literally `SafeResult<T> | { status: "pending" }`, so both routers'
41
+ * results destructure identically.
43
42
  */
44
43
  export type RouterResult<T> = SafeResult<T> | {
45
44
  status: "pending";
46
45
  };
47
46
  /**
48
- * Decoded route params as a {@link RouterResult} (PR5), optionally projected
49
- * through `options.select` (design-07 SEL1/SEL2).
47
+ * Decoded route params as a {@link RouterResult}, optionally projected
48
+ * through `options.select`.
50
49
  */
51
50
  export declare function useRouteParams<R extends AnyPagesRoute>(route: R): RouterResult<InferRouteParams<R>>;
52
51
  export declare function useRouteParams<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): RouterResult<U>;
53
52
  /**
54
- * Decoded search params as a {@link RouterResult} (PR5), optionally projected
55
- * through `options.select` (design-07 SEL1/SEL2).
53
+ * Decoded search params as a {@link RouterResult}, optionally projected
54
+ * through `options.select`.
56
55
  */
57
56
  export declare function useSearch<R extends AnyPagesRoute>(route: R): RouterResult<SearchOutputOf<R["~search"]>>;
58
57
  export declare function useSearch<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): RouterResult<U>;
package/dist/pages.js CHANGED
@@ -80,15 +80,15 @@ export function useSearch(route, options) {
80
80
  }
81
81
  /**
82
82
  * `asPath`'s path part: basePath-/locale-relative — exactly what
83
- * `replace()` expects back — so the panel's search-only string (DT8)
84
- * resolves without doubling a configured basePath.
83
+ * `replace()` expects back — so the panel's search-only string resolves
84
+ * without doubling a configured basePath.
85
85
  */
86
86
  function asPathPathname(asPath) {
87
87
  return asPath.split(/[#?]/)[0] ?? "/";
88
88
  }
89
89
  /**
90
- * `query` minus the route's own path-param names (PR5) — the client twin of
91
- * `parseContext`'s server-side subtraction (core route.ts, PR10). Entries →
90
+ * `query` minus the route's own path-param names — the client twin of
91
+ * `parseContext`'s server-side subtraction (core route.ts). Entries →
92
92
  * fromEntries so a hostile `?__proto__=` key stays an ordinary own property
93
93
  * (decodeParams's ethos). Names come from the define-time `~segments` token
94
94
  * cache, so nothing re-tokenizes per render.
@@ -102,19 +102,19 @@ function omitPathParams(query, route) {
102
102
  return Object.fromEntries(Object.entries(query).filter(([key]) => !names.has(key)));
103
103
  }
104
104
  /**
105
- * Real-Next fallback for the adapter seam (design-16 TA3): the /testing
106
- * provider overrides the read through {@link PagesNavigationContext}; with
105
+ * Real-Next fallback for the adapter seam: the /testing provider overrides
106
+ * the read through {@link PagesNavigationContext}; with
107
107
  * no provider mounted the context's `null` default resolves here, so
108
108
  * production behavior (and this module's `next/router.js`-only bundle
109
109
  * graph, per dist.test.ts) is unchanged. The adapter is resolved via an
110
110
  * unconditional `useContext` BEFORE the try below, exactly where the direct
111
111
  * call previously sat, keeping hook call order identical across renders and
112
- * across provider presence (TA4).
112
+ * across provider presence.
113
113
  */
114
114
  const realPagesAdapter = { useRouter };
115
115
  /**
116
- * `useRouter` with the one failure the brand cannot catch translated (PR5):
117
- * in a hybrid project a component rendered under `app/` can legally hold a
116
+ * `useRouter` with the one failure the brand cannot catch translated: in a
117
+ * hybrid project a component rendered under `app/` can legally hold a
118
118
  * pages-branded route, but `next/router` has no mount there and throws
119
119
  * "NextRouter was not mounted" — a message pointing at the wrong fix
120
120
  * (component placement is invisible to the type system). Rethrow a
@@ -130,7 +130,7 @@ function usePagesRouter() {
130
130
  catch (error) {
131
131
  if (error instanceof Error &&
132
132
  error.message.includes("NextRouter was not mounted")) {
133
- throw new ParamourError('pages hooks were rendered under the App Router, where next/router is never mounted — import this component\'s hooks from "@paramour-js/next/app" and pass it an app route instead (PR5)', { cause: error });
133
+ throw new ParamourError('pages hooks were rendered under the App Router, where next/router is never mounted — import this component\'s hooks from "@paramour-js/next/app" and pass it an app route instead', { cause: error });
134
134
  }
135
135
  throw error;
136
136
  }
package/dist/run-cli.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { type CliIo } from "./cli-io.js";
2
2
  export { type CliIo } from "./cli-io.js";
3
+ type Command = (argv: readonly string[], io: CliIo) => Promise<number>;
4
+ export declare const COMMANDS: Record<string, Command>;
3
5
  /**
4
- * @internal The CLI dispatcher (TR7), in-process testable: returns the exit
6
+ * @internal The CLI dispatcher, in-process testable: returns the exit
5
7
  * code instead of exiting. The exit-code contract holds across every
6
8
  * command: 0 success, 1 "the thing you asked me to verify is not true"
7
9
  * (`check`/`generate --check` drift, `doctor` failures) ONLY, 2
package/dist/run-cli.js CHANGED
@@ -3,14 +3,17 @@ import { runDoctor } from "./commands/doctor.js";
3
3
  import { runGenerate } from "./commands/generate.js";
4
4
  import { runInit } from "./commands/init.js";
5
5
  import { runList } from "./commands/list.js";
6
+ import { runSkills } from "./commands/skills.js";
6
7
  export {} from "./cli-io.js";
7
- // Alphabetical; the unknown-command message derives from these keys.
8
- const COMMANDS = {
8
+ // Alphabetical; the unknown-command message derives from these keys, and the
9
+ // skill-drift test pins them against the skill's CLI table.
10
+ export const COMMANDS = {
9
11
  check: (argv, io) => runGenerate(argv, io, "check"),
10
12
  doctor: runDoctor,
11
13
  generate: (argv, io) => runGenerate(argv, io, "generate"),
12
14
  init: runInit,
13
15
  list: runList,
16
+ skills: (argv, io) => Promise.resolve(runSkills(argv, io)),
14
17
  };
15
18
  const USAGE = [
16
19
  "Usage: paramour <command> [options]",
@@ -21,11 +24,12 @@ const USAGE = [
21
24
  " generate generate paramour-env.d.ts from the app and pages directories",
22
25
  " init set up paramour in this project",
23
26
  " list print every route with its params/search shape",
27
+ " skills install or verify the bundled agent skill for detected agent tools",
24
28
  "",
25
29
  "Run `paramour <command> --help` for that command's options.",
26
30
  ].join("\n");
27
31
  /**
28
- * @internal The CLI dispatcher (TR7), in-process testable: returns the exit
32
+ * @internal The CLI dispatcher, in-process testable: returns the exit
29
33
  * code instead of exiting. The exit-code contract holds across every
30
34
  * command: 0 success, 1 "the thing you asked me to verify is not true"
31
35
  * (`check`/`generate --check` drift, `doctor` failures) ONLY, 2
@@ -1,12 +1,12 @@
1
1
  import type { Dirent } from "node:fs";
2
- /** Next's default `pageExtensions` — extensions only, no leading dot (TR2). */
2
+ /** Next's default `pageExtensions` — extensions only, no leading dot. */
3
3
  export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"];
4
4
  /**
5
5
  * Whether a directory entry should be treated as a FILE for routing: a real
6
6
  * file, or a symlink whose target is a regular file. `Dirent.isFile()` is
7
7
  * false for a symlink even when it points at a file, yet Next resolves and
8
8
  * serves symlinked `page`/`route` files (common in pnpm-linked monorepos), so
9
- * dropping them would omit routes that Next actually serves (Bug 4, TR2). A
9
+ * dropping them would omit routes that Next actually serves (Bug 4). A
10
10
  * symlink to a DIRECTORY returns false — directory symlinks stay not-followed,
11
11
  * the existing v1 stance — and a broken link (statSync throws ENOENT) also
12
12
  * returns false, i.e. is skipped silently, matching Next's own tolerance of
@@ -15,16 +15,16 @@ export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"
15
15
  export declare function resolvesToFile(entry: Dirent, dir: string): boolean;
16
16
  /**
17
17
  * Walk an app dir and return the sorted union of URL-shaped route paths —
18
- * exactly the strings `defineAppRoute` accepts (TR2, RL2). Pure `fs.readdir`
19
- * recursion; no dependency on Next internals. Two page files resolving to
20
- * one URL path — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx`
21
- * extension twins — throw a {@link RouteCollisionError} instead of being
22
- * deduped (PR4/PR9 alignment ruling): that state is Next's own build error,
18
+ * exactly the strings `defineAppRoute` accepts. Pure `fs.readdir` recursion;
19
+ * no dependency on Next internals. Two page files resolving to one URL path
20
+ * — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx` extension
21
+ * twins — throw a {@link RouteCollisionError} instead of being deduped, the
22
+ * same stance the pages scanner takes: that state is Next's own build error,
23
23
  * and deduping would emit an artifact for a project that cannot build.
24
24
  *
25
25
  * `route.<ext>` handlers are scanned but never emitted (handler typing is
26
- * deferred, §14). They exist only to catch the states Next refuses to build:
27
- * a page and a route handler at the same URL path ("conflicting route and
28
- * page"), and two route handlers at the same path — both throw (PR9).
26
+ * deferred). They exist only to catch the states Next refuses to build: a
27
+ * page and a route handler at the same URL path ("conflicting route and
28
+ * page"), and two route handlers at the same path — both throw.
29
29
  */
30
30
  export declare function scanAppRoutes(appDir: string, pageExtensions?: readonly string[]): string[];
package/dist/scan-app.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions.js";
4
- /** Next's default `pageExtensions` — extensions only, no leading dot (TR2). */
4
+ /** Next's default `pageExtensions` — extensions only, no leading dot. */
5
5
  export const DEFAULT_PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
6
6
  /**
7
- * Interception markers `(.)`/`(..)`/`(...)` (TR2, RL2 / §15.5). A prefix
8
- * match, so chained forms like `(..)(..)segment` are caught too; tested
9
- * BEFORE the route-group test so `(.)foo` is never misread as a group.
7
+ * Interception markers `(.)`/`(..)`/`(...)`. A prefix match, so chained
8
+ * forms like `(..)(..)segment` are caught too; tested BEFORE the route-group
9
+ * test so `(.)foo` is never misread as a group.
10
10
  */
11
11
  const INTERCEPTION_PREFIX = /^\(\.{1,3}\)/;
12
12
  /**
@@ -17,18 +17,18 @@ const INTERCEPTION_PREFIX = /^\(\.{1,3}\)/;
17
17
  * `/_settings`. The escape is defined for the LEADING position only. Because
18
18
  * RFC 3986 percent-encoding is case-insensitive on its hex digits (and this
19
19
  * could not be pinned against Next's source from here), both `%5F` and `%5f`
20
- * are decoded defensively (Bug 8, TR2). The fs name stays raw for error
21
- * messages; only the emitted URL segment is decoded.
20
+ * are decoded defensively (Bug 8). The fs name stays raw for error messages;
21
+ * only the emitted URL segment is decoded.
22
22
  */
23
23
  const LEADING_ESCAPED_UNDERSCORE = /^%5[Ff]/;
24
- /** Route groups `(group)` — stripped from the emitted path (TR2, RL2). */
24
+ /** Route groups `(group)` — stripped from the emitted path. */
25
25
  const ROUTE_GROUP = /^\(.*\)$/;
26
26
  /**
27
27
  * Whether a directory entry should be treated as a FILE for routing: a real
28
28
  * file, or a symlink whose target is a regular file. `Dirent.isFile()` is
29
29
  * false for a symlink even when it points at a file, yet Next resolves and
30
30
  * serves symlinked `page`/`route` files (common in pnpm-linked monorepos), so
31
- * dropping them would omit routes that Next actually serves (Bug 4, TR2). A
31
+ * dropping them would omit routes that Next actually serves (Bug 4). A
32
32
  * symlink to a DIRECTORY returns false — directory symlinks stay not-followed,
33
33
  * the existing v1 stance — and a broken link (statSync throws ENOENT) also
34
34
  * returns false, i.e. is skipped silently, matching Next's own tolerance of
@@ -50,17 +50,17 @@ export function resolvesToFile(entry, dir) {
50
50
  }
51
51
  /**
52
52
  * Walk an app dir and return the sorted union of URL-shaped route paths —
53
- * exactly the strings `defineAppRoute` accepts (TR2, RL2). Pure `fs.readdir`
54
- * recursion; no dependency on Next internals. Two page files resolving to
55
- * one URL path — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx`
56
- * extension twins — throw a {@link RouteCollisionError} instead of being
57
- * deduped (PR4/PR9 alignment ruling): that state is Next's own build error,
53
+ * exactly the strings `defineAppRoute` accepts. Pure `fs.readdir` recursion;
54
+ * no dependency on Next internals. Two page files resolving to one URL path
55
+ * — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx` extension
56
+ * twins — throw a {@link RouteCollisionError} instead of being deduped, the
57
+ * same stance the pages scanner takes: that state is Next's own build error,
58
58
  * and deduping would emit an artifact for a project that cannot build.
59
59
  *
60
60
  * `route.<ext>` handlers are scanned but never emitted (handler typing is
61
- * deferred, §14). They exist only to catch the states Next refuses to build:
62
- * a page and a route handler at the same URL path ("conflicting route and
63
- * page"), and two route handlers at the same path — both throw (PR9).
61
+ * deferred). They exist only to catch the states Next refuses to build: a
62
+ * page and a route handler at the same URL path ("conflicting route and
63
+ * page"), and two route handlers at the same path — both throw.
64
64
  */
65
65
  export function scanAppRoutes(appDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
66
66
  // Path → the fs path (relative to appDir) that produced it, so a collision
@@ -71,20 +71,20 @@ export function scanAppRoutes(appDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS)
71
71
  const pageFileNames = new Set(pageExtensions.map((ext) => `page.${ext}`));
72
72
  const routeFileNames = new Set(pageExtensions.map((ext) => `route.${ext}`));
73
73
  walk(appDir, [], [], pageFileNames, routeFileNames, out, routeOut);
74
- // PR9: a page and a route handler resolving to one URL path is Next's
74
+ // A page and a route handler resolving to one URL path is Next's
75
75
  // "conflicting route and page" build error — no valid artifact exists, so
76
76
  // throw rather than emit the page and silently drop the handler. Sorted so
77
77
  // the reported pair is deterministic across platforms.
78
78
  for (const [path, routeFile] of [...routeOut].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
79
79
  const pageFile = out.get(path);
80
80
  if (pageFile !== undefined) {
81
- throw new RouteCollisionError(`app route collision at "${path}": page ${pageFile} and route handler ${routeFile} resolve to the same path, which Next refuses to build (conflicting route and page) (PR9)`);
81
+ throw new RouteCollisionError(`app route collision at "${path}": page ${pageFile} and route handler ${routeFile} resolve to the same path, which Next refuses to build (conflicting route and page)`);
82
82
  }
83
83
  }
84
- // Code-unit sort, never localeCompare — locale independence feeds TR3's
84
+ // Code-unit sort, never localeCompare — locale independence feeds the
85
85
  // byte-identical-on-every-OS guarantee.
86
86
  const paths = [...out.keys()].sort();
87
- // PR9 structural collisions (different slug names, optional-catch-all
87
+ // Structural collisions (different slug names, optional-catch-all
88
88
  // specificity) — non-equal strings the Map above cannot catch.
89
89
  assertNoStructuralCollisions(paths.map((path) => ({ path, router: "app" })));
90
90
  return paths;
@@ -97,11 +97,11 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
97
97
  const name = entry.name;
98
98
  // A real file, or a symlink whose target is a file (Bug 4). Directory
99
99
  // symlinks fall through to the directory guard below, which is false for
100
- // a symlink Dirent, so their subtree is skipped — not followed (TR2).
100
+ // a symlink Dirent, so their subtree is skipped — not followed.
101
101
  if (resolvesToFile(entry, dir)) {
102
- // Exact, case-sensitive `page.<ext>` / `route.<ext>` match (TR2). Pages
103
- // are emitted; route handlers are tracked separately (never emitted —
104
- // handler typing is §14) purely to detect the build errors above (PR9).
102
+ // Exact, case-sensitive `page.<ext>` / `route.<ext>` match. Pages are
103
+ // emitted; route handlers are tracked separately (never emitted —
104
+ // handler typing is deferred) purely to detect the build errors above.
105
105
  const isPage = pageFileNames.has(name);
106
106
  const isRoute = !isPage && routeFileNames.has(name);
107
107
  if (isPage || isRoute) {
@@ -111,19 +111,19 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
111
111
  const existing = target.get(path);
112
112
  if (existing !== undefined) {
113
113
  throw new RouteCollisionError(isPage
114
- ? `app route collision at "${path}": ${existing} and ${file} resolve to the same path (PR9)`
115
- : `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build (PR9)`);
114
+ ? `app route collision at "${path}": ${existing} and ${file} resolve to the same path`
115
+ : `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build`);
116
116
  }
117
117
  target.set(path, file);
118
118
  }
119
119
  continue;
120
120
  }
121
- // Symlinked directories are deliberately not followed (TR2 v1 stance):
121
+ // Symlinked directories are deliberately not followed (the v1 stance):
122
122
  // `resolvesToFile` returned false and `isDirectory()` is false for the
123
123
  // link Dirent, so the subtree is skipped here.
124
124
  if (!entry.isDirectory())
125
125
  continue;
126
- // TR2 skip rules: private folders, parallel slots, interception routes —
126
+ // Skip rules: private folders, parallel slots, interception routes —
127
127
  // each skips the entire subtree, pages at any depth included. The `_`
128
128
  // test reads the raw fs name, so `%5F`-escaped folders (which do NOT
129
129
  // start with `_`) are correctly NOT skipped (Bug 8).
@@ -134,12 +134,12 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
134
134
  if (INTERCEPTION_PREFIX.test(name))
135
135
  continue;
136
136
  if (ROUTE_GROUP.test(name)) {
137
- // Group stripped: recurse with the SAME url segments (TR2).
137
+ // Group stripped: recurse with the SAME url segments.
138
138
  walk(join(dir, name), urlSegments, [...fsSegments, name], pageFileNames, routeFileNames, out, routeOut);
139
139
  continue;
140
140
  }
141
141
  // Dynamic segments `[id]` / `[...slug]` / `[[...slug]]` pass through
142
- // verbatim (TR2, RL2 URL-shaped literals). A leading `%5F` decodes to `_`
142
+ // verbatim as URL-shaped literals. A leading `%5F` decodes to `_`
143
143
  // for the emitted URL segment so it string-matches the served URL; the fs
144
144
  // name stays raw for error messages, and the decoded form participates in
145
145
  // collision detection via the `out` Map key (Bug 8).