@paramour-js/next 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app.d.ts +48 -49
- package/dist/app.js +11 -12
- package/dist/cli-args.d.ts +4 -4
- package/dist/cli-args.js +4 -4
- package/dist/cli-inputs.d.ts +6 -7
- package/dist/cli-inputs.js +6 -7
- package/dist/cli.js +1 -1
- package/dist/collisions.d.ts +8 -8
- package/dist/collisions.js +9 -9
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.js +12 -7
- package/dist/commands/generate.d.ts +55 -7
- package/dist/commands/generate.js +29 -22
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +111 -19
- package/dist/commands/list.d.ts +21 -0
- package/dist/commands/list.js +12 -7
- package/dist/commands/skills.d.ts +45 -0
- package/dist/commands/skills.js +222 -0
- package/dist/config.d.ts +17 -10
- package/dist/config.js +17 -4
- package/dist/devtools-seam.d.ts +19 -19
- package/dist/devtools-seam.js +3 -3
- package/dist/doctor/checks.js +7 -3
- package/dist/emit.d.ts +12 -12
- package/dist/emit.js +13 -13
- package/dist/generate.d.ts +14 -14
- package/dist/generate.js +11 -11
- package/dist/init/agents-md.d.ts +32 -0
- package/dist/init/agents-md.js +78 -0
- package/dist/init/scaffold.js +54 -0
- package/dist/list/discover-route-defs.d.ts +4 -4
- package/dist/list/discover-route-defs.js +4 -4
- package/dist/lock.d.ts +8 -9
- package/dist/lock.js +11 -12
- package/dist/navigation-adapter.d.ts +17 -18
- package/dist/navigation-adapter.js +4 -4
- package/dist/observe.d.ts +14 -14
- package/dist/observe.js +4 -4
- package/dist/pages.d.ts +30 -31
- package/dist/pages.js +10 -10
- package/dist/run-cli.d.ts +3 -1
- package/dist/run-cli.js +7 -3
- package/dist/scan-app.d.ts +10 -10
- package/dist/scan-app.js +30 -30
- package/dist/scan-pages.d.ts +6 -6
- package/dist/scan-pages.js +28 -26
- package/dist/scan.d.ts +13 -10
- package/dist/scan.js +7 -7
- package/dist/select.d.ts +40 -40
- package/dist/select.js +30 -30
- package/dist/skills/doctor.d.ts +11 -0
- package/dist/skills/doctor.js +71 -0
- package/dist/skills/manifest.d.ts +41 -0
- package/dist/skills/manifest.js +96 -0
- package/dist/skills/packaged.d.ts +21 -0
- package/dist/skills/packaged.js +32 -0
- package/dist/skills/sync.d.ts +84 -0
- package/dist/skills/sync.js +136 -0
- package/dist/skills/targets.d.ts +29 -0
- package/dist/skills/targets.js +73 -0
- package/dist/testing.d.ts +28 -32
- package/dist/testing.js +15 -15
- package/dist/watch.d.ts +12 -12
- package/dist/watch.js +13 -13
- package/dist/with-typed-routes.d.ts +11 -10
- package/dist/with-typed-routes.js +42 -39
- package/package.json +4 -3
- package/skills/paramour/SKILL.md +43 -0
- package/skills/paramour/references/authoring.md +113 -0
- package/skills/paramour/references/migration.md +141 -0
- package/skills/paramour/references/reference.md +96 -0
- package/skills/paramour/references/setup.md +139 -0
package/dist/commands/init.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/init.js
CHANGED
|
@@ -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,
|
|
16
|
-
"first generate
|
|
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
|
|
20
|
-
" --force
|
|
21
|
-
" --help, -h
|
|
22
|
-
" --no-
|
|
23
|
-
" --no-
|
|
24
|
-
" --no-
|
|
25
|
-
" --no-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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) {
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/list.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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}`
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
7
|
+
/** App dir, relative to the project root; default: joint discovery. */
|
|
8
8
|
appDir?: string;
|
|
9
|
-
/** Artifact path, relative to the project root
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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) {
|