@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,4 +1,44 @@
1
1
  import { type CliIo } from "../cli-io.js";
2
+ /** @internal `paramour init` flags — skill-drift test's source of truth. */
3
+ export declare const INIT_OPTIONS: {
4
+ readonly "dry-run": {
5
+ readonly default: false;
6
+ readonly type: "boolean";
7
+ };
8
+ readonly force: {
9
+ readonly default: false;
10
+ readonly type: "boolean";
11
+ };
12
+ readonly help: {
13
+ readonly default: false;
14
+ readonly short: "h";
15
+ readonly type: "boolean";
16
+ };
17
+ readonly "no-agents-md": {
18
+ readonly default: false;
19
+ readonly type: "boolean";
20
+ };
21
+ readonly "no-config": {
22
+ readonly default: false;
23
+ readonly type: "boolean";
24
+ };
25
+ readonly "no-generate": {
26
+ readonly default: false;
27
+ readonly type: "boolean";
28
+ };
29
+ readonly "no-script": {
30
+ readonly default: false;
31
+ readonly type: "boolean";
32
+ };
33
+ readonly "no-skills": {
34
+ readonly default: false;
35
+ readonly type: "boolean";
36
+ };
37
+ readonly "no-wrap": {
38
+ readonly default: false;
39
+ readonly type: "boolean";
40
+ };
41
+ };
2
42
  /**
3
43
  * @internal `paramour init` — non-interactive by design: it runs straight
4
44
  * through with defaults and prints one status line per step. Exit codes: 0
@@ -5,25 +5,46 @@ import { NoRouteDirsError, resolveInputs } from "../cli-inputs.js";
5
5
  import { message, resolveIo } from "../cli-io.js";
6
6
  import { CONFIG_FILE_NAMES, loadConfigFile } from "../config.js";
7
7
  import { generate } from "../generate.js";
8
+ import { findAgentsFile, upsertAgentsSection } from "../init/agents-md.js";
8
9
  import { addPackageScript, checkSetup, paramourConfigTemplate, } from "../init/scaffold.js";
9
10
  import { findNextConfig, manualSnippet, wrapNextConfigSource, } from "../init/wrap-next-config.js";
10
11
  import { scanRoutes } from "../scan.js";
12
+ import { loadPackagedSkill } from "../skills/packaged.js";
13
+ import { syncTarget } from "../skills/sync.js";
14
+ import { detectTargets } from "../skills/targets.js";
15
+ import { reportOrphans } from "./skills.js";
11
16
  const USAGE = [
12
17
  "Usage: paramour init [options]",
13
18
  "",
14
19
  "Set up paramour in this project: scaffold paramour.config.ts, wrap",
15
- 'next.config with withTypedRoutes, add a "paramour" script, and run the',
16
- "first generate. Every step is idempotent and individually skippable.",
20
+ 'next.config with withTypedRoutes, add a "paramour" script, run the',
21
+ "first generate, install agent skills for detected agent tools, and add",
22
+ "a paramour section to an existing AGENTS.md/CLAUDE.md. Every step is",
23
+ "idempotent and individually skippable.",
17
24
  "",
18
25
  "Options:",
19
- " --dry-run report every step without writing anything",
20
- " --force overwrite an existing paramour.config with the scaffold",
21
- " --help, -h show this help",
22
- " --no-config skip scaffolding paramour.config.ts",
23
- " --no-generate skip the first generate",
24
- " --no-script skip adding the package.json script",
25
- " --no-wrap skip wrapping next.config",
26
+ " --dry-run report every step without writing anything",
27
+ " --force overwrite an existing paramour.config with the scaffold",
28
+ " --help, -h show this help",
29
+ " --no-agents-md skip the AGENTS.md/CLAUDE.md paramour section",
30
+ " --no-config skip scaffolding paramour.config.ts",
31
+ " --no-generate skip the first generate",
32
+ " --no-script skip adding the package.json script",
33
+ " --no-skills skip installing agent skills",
34
+ " --no-wrap skip wrapping next.config",
26
35
  ].join("\n");
36
+ /** @internal `paramour init` flags — skill-drift test's source of truth. */
37
+ export const INIT_OPTIONS = {
38
+ "dry-run": { default: false, type: "boolean" },
39
+ force: { default: false, type: "boolean" },
40
+ help: { default: false, short: "h", type: "boolean" },
41
+ "no-agents-md": { default: false, type: "boolean" },
42
+ "no-config": { default: false, type: "boolean" },
43
+ "no-generate": { default: false, type: "boolean" },
44
+ "no-script": { default: false, type: "boolean" },
45
+ "no-skills": { default: false, type: "boolean" },
46
+ "no-wrap": { default: false, type: "boolean" },
47
+ };
27
48
  /**
28
49
  * @internal `paramour init` — non-interactive by design: it runs straight
29
50
  * through with defaults and prints one status line per step. Exit codes: 0
@@ -33,15 +54,10 @@ const USAGE = [
33
54
  */
34
55
  export async function runInit(argv, io) {
35
56
  const { stderr, stdout } = resolveIo(io);
36
- const parsed = parseCommandFlags(argv, {
37
- "dry-run": { default: false, type: "boolean" },
38
- force: { default: false, type: "boolean" },
39
- help: { default: false, short: "h", type: "boolean" },
40
- "no-config": { default: false, type: "boolean" },
41
- "no-generate": { default: false, type: "boolean" },
42
- "no-script": { default: false, type: "boolean" },
43
- "no-wrap": { default: false, type: "boolean" },
44
- }, USAGE, { stderr, stdout });
57
+ const parsed = parseCommandFlags(argv, INIT_OPTIONS, USAGE, {
58
+ stderr,
59
+ stdout,
60
+ });
45
61
  if ("exit" in parsed)
46
62
  return parsed.exit;
47
63
  const flags = parsed.values;
@@ -67,7 +83,7 @@ export async function runInit(argv, io) {
67
83
  }
68
84
  else {
69
85
  // A .mjs/.json left behind would be shadowed by the scaffold under the
70
- // ts-first discovery order (§7.2) — --force must truly replace it.
86
+ // ts-first discovery order — --force must truly replace it.
71
87
  if (existing !== undefined && existing !== "paramour.config.ts" && !dry) {
72
88
  unlinkSync(join(projectRoot, existing));
73
89
  }
@@ -184,6 +200,82 @@ export async function runInit(argv, io) {
184
200
  }
185
201
  }
186
202
  }
203
+ // 5. Agent skills for detected agent tools. Detection-driven and
204
+ // non-interactive; init never invents an install location — with no
205
+ // tooling present, the explicit `paramour skills` command carries the
206
+ // intent its portable-location fallback needs.
207
+ if (!flags["no-skills"]) {
208
+ const detected = detectTargets(projectRoot);
209
+ if (detected.length === 0) {
210
+ stdout(" • no agent tooling detected — skipped agent skills (`paramour skills` installs to the portable .agents/skills/)");
211
+ }
212
+ else {
213
+ try {
214
+ const packaged = loadPackagedSkill();
215
+ for (const target of detected) {
216
+ const result = syncTarget(target, packaged, { dry, force: false });
217
+ if (result.written.length > 0) {
218
+ stdout(` ✔ ${dry ? "would install" : "installed"} agent skills → ${target.rel}`);
219
+ }
220
+ else if (result.skippedModified.length === 0) {
221
+ stdout(` • agent skills already up to date in ${target.rel}`);
222
+ }
223
+ if (result.skippedModified.length > 0) {
224
+ stdout(` ⚠ ${target.rel}: locally-modified skill files left as-is (\`paramour skills --force\` overwrites)`);
225
+ }
226
+ // A sync can also delete files (managed orphans from a previous
227
+ // install); those must never happen silently, so reuse the exact
228
+ // per-orphan wording `paramour skills` prints.
229
+ reportOrphans(result, dry, stdout);
230
+ }
231
+ }
232
+ catch (error) {
233
+ // Same stance as the wrap step: a failed nicety degrades to a
234
+ // printed pointer, never a failed init.
235
+ stdout(` ⚠ could not install agent skills (${message(error)}) — run \`paramour skills\``);
236
+ }
237
+ }
238
+ }
239
+ // 6. Marker-managed paramour section in AGENTS.md/CLAUDE.md, pointing
240
+ // agents at the skill step 5 installed. Append-only: init never creates
241
+ // the file — a root AGENTS.md is a detectTargets signal, so inventing one
242
+ // would change what the skills step sees on the next run. --force is
243
+ // deliberately not consulted: the markers delimit paramour-owned content,
244
+ // so refreshing it is unconditionally safe.
245
+ if (!flags["no-agents-md"]) {
246
+ const agentsFile = findAgentsFile(projectRoot);
247
+ if (agentsFile === undefined) {
248
+ stdout(" • no AGENTS.md or CLAUDE.md — skipped agents snippet");
249
+ }
250
+ else {
251
+ const name = basename(agentsFile);
252
+ let source;
253
+ try {
254
+ source = readFileSync(agentsFile, "utf8");
255
+ }
256
+ catch (error) {
257
+ // The skills-step stance: a failed nicety never fails init.
258
+ stdout(` ⚠ could not read ${name} (${message(error)}) — skipped agents snippet`);
259
+ }
260
+ if (source !== undefined) {
261
+ const result = upsertAgentsSection(source);
262
+ if (result.status === "added") {
263
+ write(agentsFile, result.text);
264
+ stdout(` ✔ ${dry ? "would append" : "appended"} paramour section to ${name}`);
265
+ }
266
+ else if (result.status === "updated") {
267
+ write(agentsFile, result.text);
268
+ stdout(` ✔ ${dry ? "would update" : "updated"} paramour section in ${name}`);
269
+ }
270
+ else if (result.status === "unchanged") {
271
+ stdout(` • ${name} paramour section already up to date — skipped`);
272
+ }
273
+ else {
274
+ stdout(` ⚠ ${name} has an unterminated paramour marker — left as-is (remove or close the markers, then re-run)`);
275
+ }
276
+ }
277
+ }
278
+ }
187
279
  return finishWithSummary(projectRoot, artifactPath, stdout);
188
280
  }
189
281
  function countRoutes(routes) {
@@ -1,4 +1,25 @@
1
1
  import { type CliIo } from "../cli-io.js";
2
+ /** @internal `paramour list` flags — skill-drift test's source of truth. */
3
+ export declare const LIST_OPTIONS: {
4
+ readonly "app-dir": {
5
+ readonly type: "string";
6
+ };
7
+ readonly help: {
8
+ readonly default: false;
9
+ readonly short: "h";
10
+ readonly type: "boolean";
11
+ };
12
+ readonly json: {
13
+ readonly default: false;
14
+ readonly type: "boolean";
15
+ };
16
+ readonly "page-extensions": {
17
+ readonly type: "string";
18
+ };
19
+ readonly "pages-dir": {
20
+ readonly type: "string";
21
+ };
22
+ };
2
23
  /**
3
24
  * @internal `paramour list`: the filesystem scan is authoritative for WHICH
4
25
  * routes exist (same engine as generate); discovered definitions overlay
@@ -23,6 +23,14 @@ const USAGE = [
23
23
  " --page-extensions <list> comma-separated, no leading dots (default: tsx,ts,jsx,js)",
24
24
  " --pages-dir <dir> pages directory (default: discovered pages/ or src/pages/)",
25
25
  ].join("\n");
26
+ /** @internal `paramour list` flags — skill-drift test's source of truth. */
27
+ export const LIST_OPTIONS = {
28
+ "app-dir": { type: "string" },
29
+ help: { default: false, short: "h", type: "boolean" },
30
+ json: { default: false, type: "boolean" },
31
+ "page-extensions": { type: "string" },
32
+ "pages-dir": { type: "string" },
33
+ };
26
34
  /**
27
35
  * @internal `paramour list`: the filesystem scan is authoritative for WHICH
28
36
  * routes exist (same engine as generate); discovered definitions overlay
@@ -32,13 +40,10 @@ const USAGE = [
32
40
  */
33
41
  export async function runList(argv, io) {
34
42
  const { stderr, stdout } = resolveIo(io);
35
- const parsed = parseCommandFlags(argv, {
36
- "app-dir": { type: "string" },
37
- help: { default: false, short: "h", type: "boolean" },
38
- json: { default: false, type: "boolean" },
39
- "page-extensions": { type: "string" },
40
- "pages-dir": { type: "string" },
41
- }, USAGE, { stderr, stdout });
43
+ const parsed = parseCommandFlags(argv, LIST_OPTIONS, USAGE, {
44
+ stderr,
45
+ stdout,
46
+ });
42
47
  if ("exit" in parsed)
43
48
  return parsed.exit;
44
49
  const flags = parsed.values;
@@ -0,0 +1,45 @@
1
+ import { type CliIo } from "../cli-io.js";
2
+ import { type TargetSyncResult } from "../skills/sync.js";
3
+ /** @internal `paramour skills` flags — skill-drift test's source of truth. */
4
+ export declare const SKILLS_OPTIONS: {
5
+ readonly check: {
6
+ readonly default: false;
7
+ readonly type: "boolean";
8
+ };
9
+ readonly "dry-run": {
10
+ readonly default: false;
11
+ readonly type: "boolean";
12
+ };
13
+ readonly force: {
14
+ readonly default: false;
15
+ readonly type: "boolean";
16
+ };
17
+ readonly help: {
18
+ readonly default: false;
19
+ readonly short: "h";
20
+ readonly type: "boolean";
21
+ };
22
+ readonly json: {
23
+ readonly default: false;
24
+ readonly type: "boolean";
25
+ };
26
+ readonly tool: {
27
+ readonly multiple: true;
28
+ readonly type: "string";
29
+ };
30
+ };
31
+ /**
32
+ * @internal Per-orphan reporting for a sync: removals (deletions are the one
33
+ * thing a sync does that must never happen silently) and kept locally-modified
34
+ * orphans. Shared by `paramour skills` and `paramour init` so the two commands
35
+ * describe the same operation in the same words.
36
+ */
37
+ export declare function reportOrphans(result: TargetSyncResult, dry: boolean, stdout: (line: string) => void): void;
38
+ /**
39
+ * @internal `paramour skills` — the installer for the bundled agent skill.
40
+ * `--check` follows `check`'s exit class: 1 means "the installed copies do
41
+ * not reflect the installed package". A plain run treats refusing to clobber
42
+ * local edits as success (init's manual-fallback precedent): the refusal is
43
+ * printed, the fix (`--force`) is named, and nothing is broken.
44
+ */
45
+ export declare function runSkills(argv: readonly string[], io: CliIo): number;
@@ -0,0 +1,222 @@
1
+ import { parseCommandFlags } from "../cli-args.js";
2
+ import { message, resolveIo } from "../cli-io.js";
3
+ import { loadPackagedSkill } from "../skills/packaged.js";
4
+ import { auditTarget, FILE_STATUS_DETAIL, isOutdated, syncTarget, } from "../skills/sync.js";
5
+ import { resolveTargets } from "../skills/targets.js";
6
+ const USAGE = [
7
+ "Usage: paramour skills [options]",
8
+ "",
9
+ "Install the bundled Paramour agent skill into each detected agent tool's",
10
+ "skills directory (.agents/, .claude/, .codex/, .cursor/), stamping every",
11
+ "install with a .paramour-skills.json manifest so re-syncs are safe:",
12
+ "locally-modified files are never overwritten without --force. With no",
13
+ "tools detected, installs to the portable .agents/skills/.",
14
+ "",
15
+ "Exit codes: 0 success (including skipped local edits), 1 under --check",
16
+ "when any installed copy is missing or stale, 2 usage/operational errors.",
17
+ "",
18
+ "Options:",
19
+ " --check verify installed skills instead of writing; exit 1 when",
20
+ " any copy is missing or stale; passes with nothing to",
21
+ " check when no agent tooling is detected",
22
+ " --dry-run report what would be written without writing",
23
+ " --force overwrite locally-modified skill files",
24
+ " --help, -h show this help",
25
+ " --json machine-readable output",
26
+ " --tool <t> target tool(s): agents, claude, codex, cursor",
27
+ " (repeatable or comma-separated; overrides detection)",
28
+ ].join("\n");
29
+ /** @internal `paramour skills` flags — skill-drift test's source of truth. */
30
+ export const SKILLS_OPTIONS = {
31
+ check: { default: false, type: "boolean" },
32
+ "dry-run": { default: false, type: "boolean" },
33
+ force: { default: false, type: "boolean" },
34
+ help: { default: false, short: "h", type: "boolean" },
35
+ json: { default: false, type: "boolean" },
36
+ tool: { multiple: true, type: "string" },
37
+ };
38
+ /**
39
+ * @internal Per-orphan reporting for a sync: removals (deletions are the one
40
+ * thing a sync does that must never happen silently) and kept locally-modified
41
+ * orphans. Shared by `paramour skills` and `paramour init` so the two commands
42
+ * describe the same operation in the same words.
43
+ */
44
+ export function reportOrphans(result, dry, stdout) {
45
+ for (const orphan of result.removedOrphans) {
46
+ stdout(` ${dry ? "would remove" : "removed"} ${orphan} — no longer part of the skill`);
47
+ }
48
+ for (const orphan of result.keptOrphans) {
49
+ stdout(` left ${orphan} — locally modified and no longer part of the skill`);
50
+ }
51
+ }
52
+ /**
53
+ * @internal `paramour skills` — the installer for the bundled agent skill.
54
+ * `--check` follows `check`'s exit class: 1 means "the installed copies do
55
+ * not reflect the installed package". A plain run treats refusing to clobber
56
+ * local edits as success (init's manual-fallback precedent): the refusal is
57
+ * printed, the fix (`--force`) is named, and nothing is broken.
58
+ */
59
+ export function runSkills(argv, io) {
60
+ const { stderr, stdout } = resolveIo(io);
61
+ const parsed = parseCommandFlags(argv, SKILLS_OPTIONS, USAGE, {
62
+ stderr,
63
+ stdout,
64
+ });
65
+ if ("exit" in parsed)
66
+ return parsed.exit;
67
+ const flags = parsed.values;
68
+ // --check never writes, so a write-shaping flag alongside it is a
69
+ // contradiction, not a no-op — reject loudly.
70
+ for (const conflicting of ["dry-run", "force"]) {
71
+ if (flags.check && flags[conflicting]) {
72
+ stderr(`paramour: --check cannot be combined with --${conflicting}`);
73
+ stderr(USAGE);
74
+ return 2;
75
+ }
76
+ }
77
+ const projectRoot = process.cwd();
78
+ const resolved = resolveTargets(projectRoot, flags.tool);
79
+ if ("error" in resolved) {
80
+ stderr(`paramour: ${resolved.error}`);
81
+ stderr(USAGE);
82
+ return 2;
83
+ }
84
+ let packaged;
85
+ try {
86
+ packaged = loadPackagedSkill();
87
+ }
88
+ catch (error) {
89
+ stderr(`paramour: bundled skill content unreadable: ${message(error)}`);
90
+ return 2;
91
+ }
92
+ try {
93
+ return flags.check
94
+ ? runCheck(resolved.targets, packaged, { fallback: resolved.fallback, json: flags.json }, stdout)
95
+ : runInstall(resolved.targets, packaged, {
96
+ dry: flags["dry-run"],
97
+ fallback: resolved.fallback,
98
+ force: flags.force,
99
+ json: flags.json,
100
+ }, stdout);
101
+ }
102
+ catch (error) {
103
+ stderr(`paramour: ${message(error)}`);
104
+ return 2;
105
+ }
106
+ }
107
+ /** Target-level `--check` verdict, doctor's status vocabulary. */
108
+ function checkStatus(audit) {
109
+ const statuses = audit.files.map((file) => file.status);
110
+ if (statuses.some(isOutdated))
111
+ return "fail";
112
+ return statuses.some((status) => status === "modified") ? "warn" : "pass";
113
+ }
114
+ function reportInstall(result, dry, stdout) {
115
+ const { rel } = result.audit.target;
116
+ const freshInstall = result.audit.manifest === undefined;
117
+ if (result.written.length > 0) {
118
+ const verb = dry
119
+ ? freshInstall
120
+ ? "would install"
121
+ : "would update"
122
+ : freshInstall
123
+ ? "installed"
124
+ : "updated";
125
+ const count = `${String(result.written.length)} file${result.written.length === 1 ? "" : "s"}`;
126
+ stdout(` ✔ ${rel} — ${verb} ${freshInstall ? `(${count})` : count}`);
127
+ if (!freshInstall)
128
+ stdout(` ${result.written.join(", ")}`);
129
+ }
130
+ else if (result.skippedModified.length === 0) {
131
+ stdout(` • ${rel} — up to date`);
132
+ }
133
+ if (result.skippedModified.length > 0) {
134
+ const count = result.skippedModified.length;
135
+ stdout(` ⚠ ${rel} — ${String(count)} locally-modified file${count === 1 ? "" : "s"} left as-is (--force overwrites)`);
136
+ stdout(` ${result.skippedModified.join(", ")}`);
137
+ }
138
+ reportOrphans(result, dry, stdout);
139
+ }
140
+ function runCheck(targets, packaged, options, stdout) {
141
+ // --check's verdict is about the detection result. With no agent tooling
142
+ // detected (and no explicit --tool), the only "target" is the invented
143
+ // portable fallback a bare install would use — a location this project
144
+ // never opted into, so auditing it can only ever fail. There is nothing
145
+ // to check, and nothing to check is a pass.
146
+ if (options.fallback) {
147
+ if (options.json) {
148
+ stdout(JSON.stringify({ fallback: true, status: "pass", targets: [] }, null, 2));
149
+ }
150
+ else {
151
+ stdout(" • no agent tooling detected — nothing to check");
152
+ }
153
+ return 0;
154
+ }
155
+ const audits = targets.map((target) => auditTarget(target, packaged));
156
+ const verdicts = audits.map(checkStatus);
157
+ const failed = verdicts.filter((verdict) => verdict === "fail").length;
158
+ const warned = verdicts.filter((verdict) => verdict === "warn").length;
159
+ const status = failed > 0 ? "fail" : warned > 0 ? "warn" : "pass";
160
+ if (options.json) {
161
+ stdout(JSON.stringify({
162
+ fallback: false,
163
+ status,
164
+ targets: audits.map((audit) => ({
165
+ files: audit.files,
166
+ rel: audit.target.rel,
167
+ tool: audit.target.tool,
168
+ })),
169
+ }, null, 2));
170
+ return failed > 0 ? 1 : 0;
171
+ }
172
+ const marks = { fail: "✖", pass: "✔", warn: "⚠" };
173
+ for (const [index, audit] of audits.entries()) {
174
+ const verdict = verdicts[index] ?? "pass";
175
+ const label = verdict === "pass"
176
+ ? "up to date"
177
+ : verdict === "warn"
178
+ ? "locally modified"
179
+ : audit.files.every((file) => file.status === "missing")
180
+ ? "not installed (run `paramour skills`)"
181
+ : "stale (run `paramour skills`)";
182
+ stdout(` ${marks[verdict]} ${audit.target.rel} — ${label}`);
183
+ for (const file of audit.files) {
184
+ const detail = FILE_STATUS_DETAIL[file.status];
185
+ if (detail !== undefined)
186
+ stdout(` ${file.relPath}: ${detail}`);
187
+ }
188
+ }
189
+ stdout("");
190
+ stdout(`skills: ${String(audits.length)} target${audits.length === 1 ? "" : "s"} — ${String(failed)} stale, ${String(warned)} modified`);
191
+ return failed > 0 ? 1 : 0;
192
+ }
193
+ function runInstall(targets, packaged, options, stdout) {
194
+ const results = targets.map((target) => syncTarget(target, packaged, { dry: options.dry, force: options.force }));
195
+ if (options.json) {
196
+ stdout(JSON.stringify({
197
+ dry: options.dry,
198
+ fallback: options.fallback,
199
+ status: results.some((result) => result.skippedModified.length > 0)
200
+ ? "warn"
201
+ : "ok",
202
+ targets: results.map((result) => ({
203
+ keptOrphans: result.keptOrphans,
204
+ rel: result.audit.target.rel,
205
+ removedOrphans: result.removedOrphans,
206
+ skippedModified: result.skippedModified,
207
+ tool: result.audit.target.tool,
208
+ written: result.written,
209
+ })),
210
+ }, null, 2));
211
+ return 0;
212
+ }
213
+ stdout(options.dry
214
+ ? "paramour skills (dry run — nothing written)"
215
+ : "paramour skills");
216
+ if (options.fallback) {
217
+ stdout(" → no agent tooling detected — installing to the portable .agents/skills/");
218
+ }
219
+ for (const result of results)
220
+ reportInstall(result, options.dry, stdout);
221
+ return 0;
222
+ }
package/dist/config.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Shape of `paramour.config.{ts,mjs,json}` (§7.2 / TR7) — the CLI's config
3
- * file. Every field is optional; the CLI's precedence is flags → this file →
4
- * inference. `.ts`/`.mjs` files default-export this object.
2
+ * Shape of `paramour.config.{ts,mjs,json}` — the CLI's config file. Every
3
+ * field is optional; the CLI's precedence is flags → this file → inference.
4
+ * `.ts`/`.mjs` files default-export this object.
5
5
  */
6
6
  export interface ParamourConfig {
7
- /** App dir, relative to the project root; default: joint discovery (PR8). */
7
+ /** App dir, relative to the project root; default: joint discovery. */
8
8
  appDir?: string;
9
- /** Artifact path, relative to the project root (TR3 escape hatch). */
9
+ /** Artifact path, relative to the project root — the escape hatch. */
10
10
  outFile?: string;
11
11
  /** Page extensions, no leading dot; default: Next's four. */
12
12
  pageExtensions?: string[];
13
- /** Pages dir, relative to the project root; default: joint discovery (PR8). */
13
+ /** Pages dir, relative to the project root; default: joint discovery. */
14
14
  pagesDir?: string;
15
15
  /**
16
16
  * Globs (relative to the project root) of modules exporting route
@@ -20,14 +20,21 @@ export interface ParamourConfig {
20
20
  */
21
21
  routeFiles?: string[];
22
22
  }
23
- /** @internal Discovery order at the project root (TR7) — first match wins. */
23
+ /**
24
+ * @internal Every ParamourConfig key — the skill-drift test's source of
25
+ * truth. `satisfies Record<keyof ParamourConfig, true>` rejects a missing
26
+ * AND an extra key at compile time, so this roster cannot drift from the
27
+ * interface.
28
+ */
29
+ export declare const PARAMOUR_CONFIG_KEYS: readonly string[];
30
+ /** @internal Discovery order at the project root — first match wins. */
24
31
  export declare const CONFIG_FILE_NAMES: readonly ["paramour.config.ts", "paramour.config.mjs", "paramour.config.json"];
25
32
  /**
26
33
  * @internal Load and validate the project's config file, or `undefined`
27
34
  * when none exists. No upward traversal — the documented contract is three
28
- * filenames at the project root (TR7). jiti (the §7.2 loader carry-over) is
29
- * imported dynamically so only CLI runs that actually have a `.ts`/`.mjs`
30
- * config pay for it; `withTypedRoutes` users never execute it.
35
+ * filenames at the project root. jiti is imported dynamically so only CLI
36
+ * runs that actually have a `.ts`/`.mjs` config pay for it;
37
+ * `withTypedRoutes` users never execute it.
31
38
  */
32
39
  export declare function loadConfigFile(projectRoot: string): Promise<undefined | {
33
40
  config: ParamourConfig;
package/dist/config.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- /** @internal Discovery order at the project root (TR7) — first match wins. */
3
+ /**
4
+ * @internal Every ParamourConfig key — the skill-drift test's source of
5
+ * truth. `satisfies Record<keyof ParamourConfig, true>` rejects a missing
6
+ * AND an extra key at compile time, so this roster cannot drift from the
7
+ * interface.
8
+ */
9
+ export const PARAMOUR_CONFIG_KEYS = Object.keys({
10
+ appDir: true,
11
+ outFile: true,
12
+ pageExtensions: true,
13
+ pagesDir: true,
14
+ routeFiles: true,
15
+ });
16
+ /** @internal Discovery order at the project root — first match wins. */
4
17
  export const CONFIG_FILE_NAMES = [
5
18
  "paramour.config.ts",
6
19
  "paramour.config.mjs",
@@ -9,9 +22,9 @@ export const CONFIG_FILE_NAMES = [
9
22
  /**
10
23
  * @internal Load and validate the project's config file, or `undefined`
11
24
  * when none exists. No upward traversal — the documented contract is three
12
- * filenames at the project root (TR7). jiti (the §7.2 loader carry-over) is
13
- * imported dynamically so only CLI runs that actually have a `.ts`/`.mjs`
14
- * config pay for it; `withTypedRoutes` users never execute it.
25
+ * filenames at the project root. jiti is imported dynamically so only CLI
26
+ * runs that actually have a `.ts`/`.mjs` config pay for it;
27
+ * `withTypedRoutes` users never execute it.
15
28
  */
16
29
  export async function loadConfigFile(projectRoot) {
17
30
  for (const name of CONFIG_FILE_NAMES) {