@canonical/pragma-cli 0.35.0 → 0.36.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 +2 -2
- package/bundled-skills/add-standard/SKILL.md +338 -0
- package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
- package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
- package/bundled-skills/anatomy-author/SKILL.md +1226 -0
- package/bundled-skills/design-auditor/SKILL.md +455 -0
- package/bundled-skills/implement-component/SKILL.md +333 -0
- package/bundled-skills/specify-component/SKILL.md +331 -0
- package/bundled-skills/specify-pattern/SKILL.md +292 -0
- package/dist/package.json +16 -15
- package/dist/pragma.conf.js +236 -34
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/bin.js +1 -1
- package/dist/src/bin.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
- package/dist/src/capabilities/create/mount.js +4 -3
- package/dist/src/capabilities/create/mount.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/index.js +6 -6
- package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.render.js +43 -64
- package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/doctor/runChecks.js +14 -11
- package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +2 -2
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/info/collectInfo.js +8 -2
- package/dist/src/capabilities/info/collectInfo.js.map +1 -1
- package/dist/src/capabilities/info/info.render.js +6 -2
- package/dist/src/capabilities/info/info.render.js.map +1 -1
- package/dist/src/capabilities/prompt/prompt.render.js +2 -2
- package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +43 -39
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/mode.js +2 -2
- package/dist/src/capabilities/setup/operations/index.js +4 -4
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +92 -45
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/plan.render.js +144 -66
- package/dist/src/capabilities/setup/plan.render.js.map +1 -1
- package/dist/src/capabilities/setup/setup.render.js +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +86 -33
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +110 -45
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +6 -5
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/capabilities/shared/packageManager.js +300 -54
- package/dist/src/capabilities/shared/packageManager.js.map +1 -1
- package/dist/src/capabilities/skill/discover.js +97 -11
- package/dist/src/capabilities/skill/discover.js.map +1 -1
- package/dist/src/capabilities/skill/render.js +2 -2
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/sources/installSkills.js +145 -3
- package/dist/src/capabilities/sources/installSkills.js.map +1 -1
- package/dist/src/capabilities/sources/runUpdate.js +68 -2
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +3 -3
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
- package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
- package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
- package/dist/src/constants.js +6 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/index.js +11 -2
- package/dist/src/kernel/index.js.map +1 -1
- package/dist/src/kernel/interactivity.js +68 -0
- package/dist/src/kernel/interactivity.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +3 -3
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +63 -12
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +57 -4
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +54 -2
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +306 -43
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +23 -23
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/globalFlags.js +4 -1
- package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
- package/dist/src/kernel/project/cli/helpFormat.js +1 -2
- package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +34 -4
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +1 -1
- package/dist/src/kernel/render/style.js +29 -9
- package/dist/src/kernel/render/style.js.map +1 -1
- package/dist/src/kernel/render/vocabulary.js +174 -0
- package/dist/src/kernel/render/vocabulary.js.map +1 -0
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +16 -15
- package/pragma.conf.ts +241 -34
- package/src/bin.ts +8 -1
- package/src/capabilities/block.parity.test.ts +25 -11
- package/src/capabilities/block.shipped.exec.test.ts +282 -0
- package/src/capabilities/block.tierRank.exec.test.ts +300 -0
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/create/createGrammar.test.ts +1 -1
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
- package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
- package/src/capabilities/doctor/checks/index.ts +6 -6
- package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
- package/src/capabilities/doctor/doctor.render.test.ts +108 -32
- package/src/capabilities/doctor/doctor.render.ts +56 -80
- package/src/capabilities/doctor/doctor.test.ts +247 -35
- package/src/capabilities/doctor/doctor.verb.ts +7 -2
- package/src/capabilities/doctor/runChecks.ts +14 -11
- package/src/capabilities/doctor/types.ts +15 -24
- package/src/capabilities/emptyRecovery.test.ts +10 -2
- package/src/capabilities/graph/query.render.ts +2 -2
- package/src/capabilities/info/collectInfo.ts +8 -1
- package/src/capabilities/info/info.render.ts +6 -2
- package/src/capabilities/info/info.test.ts +37 -1
- package/src/capabilities/info/types.ts +10 -2
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/scopeTypeSync.test.ts +29 -15
- package/src/capabilities/setup/buildPlan.ts +45 -41
- package/src/capabilities/setup/index.ts +1 -1
- package/src/capabilities/setup/mode.ts +2 -2
- package/src/capabilities/setup/operations/index.ts +6 -1
- package/src/capabilities/setup/operations/setupGenerator.ts +144 -70
- package/src/capabilities/setup/operations/setupLsp.ts +129 -37
- package/src/capabilities/setup/operations/setupMcp.ts +132 -61
- package/src/capabilities/setup/operations/setupSkills.ts +392 -58
- package/src/capabilities/setup/plan.render.ts +171 -76
- package/src/capabilities/setup/plan.ts +17 -12
- package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
- package/src/capabilities/setup/setup.render.test.ts +50 -36
- package/src/capabilities/setup/setup.render.ts +1 -1
- package/src/capabilities/setup/setup.test.ts +881 -50
- package/src/capabilities/setup/setup.verb.ts +91 -33
- package/src/capabilities/setup/targets.ts +120 -52
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +17 -6
- package/src/capabilities/shared/packageManager.test.ts +421 -6
- package/src/capabilities/shared/packageManager.ts +394 -59
- package/src/capabilities/skill/bundledSkills.test.ts +347 -0
- package/src/capabilities/skill/discover.ts +100 -11
- package/src/capabilities/skill/render.ts +2 -2
- package/src/capabilities/skill/skill.test.ts +17 -4
- package/src/capabilities/sources/installSkills.ts +181 -3
- package/src/capabilities/sources/runUpdate.ts +78 -2
- package/src/capabilities/sources/sources.test.ts +228 -0
- package/src/capabilities/sources/update.verb.ts +3 -3
- package/src/capabilities/surface.test.ts +2 -2
- package/src/capabilities/upgrade/runUpgrade.ts +45 -6
- package/src/capabilities/upgrade/types.ts +10 -2
- package/src/capabilities/upgrade/upgrade.render.ts +9 -2
- package/src/capabilities/upgrade/upgrade.test.ts +72 -0
- package/src/constants.ts +6 -1
- package/src/identity.test.ts +18 -5
- package/src/kernel/completion/entitySource.test.ts +15 -2
- package/src/kernel/completion/safety.test.ts +16 -9
- package/src/kernel/config/defaults.test.ts +2 -1
- package/src/kernel/index.ts +11 -2
- package/src/kernel/interactivity.test.ts +76 -0
- package/src/kernel/interactivity.ts +69 -0
- package/src/kernel/packs/compile.test.ts +92 -0
- package/src/kernel/packs/renderPack.ts +3 -3
- package/src/kernel/packs/resolveEntity.test.ts +75 -14
- package/src/kernel/packs/resolveEntity.ts +88 -24
- package/src/kernel/packs/runBodies.ts +84 -19
- package/src/kernel/packs/schema.ts +60 -4
- package/src/kernel/packs/sparql/applyFilters.ts +125 -4
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +323 -45
- package/src/kernel/packs/toolDescription.test.ts +35 -1
- package/src/kernel/packs/types.ts +125 -2
- package/src/kernel/project/cli/buildProgram.test.ts +73 -2
- package/src/kernel/project/cli/buildProgram.ts +8 -1
- package/src/kernel/project/cli/dispatch.test.ts +5 -4
- package/src/kernel/project/cli/dispatch.ts +25 -24
- package/src/kernel/project/cli/globalFlags.ts +4 -1
- package/src/kernel/project/cli/helpFormat.ts +1 -2
- package/src/kernel/project/cli/rootHelp.ts +36 -3
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/renderers.ts +1 -1
- package/src/kernel/render/style.ts +32 -9
- package/src/kernel/render/vocabulary.ts +276 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/types.ts +35 -6
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/docExamples.test.ts +4 -2
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
- package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
- package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
- package/src/testing/eval/cases/readNouns.ts +245 -9
- package/src/testing/fixtures/blockGraph.ts +28 -13
- package/src/testing/fixtures/graph/canonical.ts +95 -12
- package/dist/src/capabilities/shared/bands.js +0 -16
- package/dist/src/capabilities/shared/bands.js.map +0 -1
- package/src/capabilities/shared/bands.ts +0 -16
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BUNDLED skills snapshot — the third discovery root, last in precedence.
|
|
3
|
+
*
|
|
4
|
+
* `scripts/bundle.ts` already resolved every declared pack to read its TTL into
|
|
5
|
+
* `pack.generated.ts`; it now also copies each pack's `skills/<name>/` into the
|
|
6
|
+
* committed `bundled-skills/` directory at the package root. So a fresh install
|
|
7
|
+
* lists skills for the same reason it answers `block lookup`: the release
|
|
8
|
+
* carries a snapshot of what its packs shipped.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THESE CELLS SAMPLE. Every expectation about skill CONTENT is read out of
|
|
11
|
+
* the committed artifact rather than written into a fixture beside the code.
|
|
12
|
+
* Inventing "a bundled skill" here would prove that a directory this test wrote
|
|
13
|
+
* can be discovered — which is already covered — while a snapshot that shipped
|
|
14
|
+
* empty, shipped a folder with no `SKILL.md`, or was dropped from `files` would
|
|
15
|
+
* stay green. The artifact is real upstream data; it is what has to work.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
lstatSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
symlinkSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { dryRun } from "@canonical/task";
|
|
33
|
+
import { runTask } from "@canonical/task/node";
|
|
34
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
35
|
+
import { BIN_NAME } from "../../constants.js";
|
|
36
|
+
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
37
|
+
import type { GlobalFlags } from "../../kernel/runtime/types.js";
|
|
38
|
+
import { runCli } from "../../testing/helpers/runCli.js";
|
|
39
|
+
import { scopedChecks } from "../doctor/checks/targetHealth.js";
|
|
40
|
+
import {
|
|
41
|
+
composeSkills,
|
|
42
|
+
detectSkills,
|
|
43
|
+
staleSkillLinks,
|
|
44
|
+
} from "../setup/operations/setupSkills.js";
|
|
45
|
+
import {
|
|
46
|
+
bundledSkillsDir,
|
|
47
|
+
discoverSkills,
|
|
48
|
+
globalSkillRoots,
|
|
49
|
+
installedSkillsDir,
|
|
50
|
+
parseFrontmatter,
|
|
51
|
+
} from "./discover.js";
|
|
52
|
+
|
|
53
|
+
const packageRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
|
54
|
+
|
|
55
|
+
/** The snapshot's folder → declared `name`, read from the shipped bytes. */
|
|
56
|
+
function shippedSkills(): { folder: string; name: string }[] {
|
|
57
|
+
const root = bundledSkillsDir();
|
|
58
|
+
if (root === undefined) return [];
|
|
59
|
+
return readdirSync(root)
|
|
60
|
+
.map((folder) => {
|
|
61
|
+
const skillMd = join(root, folder, "SKILL.md");
|
|
62
|
+
if (!existsSync(skillMd)) return undefined;
|
|
63
|
+
const frontmatter = parseFrontmatter(readFileSync(skillMd, "utf-8"));
|
|
64
|
+
return frontmatter ? { folder, name: frontmatter.name } : undefined;
|
|
65
|
+
})
|
|
66
|
+
.filter((entry): entry is { folder: string; name: string } => !!entry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A FRESH INSTALL's environment: nothing of this machine is inherited. */
|
|
70
|
+
function freshInstallEnv(): Record<string, string> {
|
|
71
|
+
const home = mkdtempSync(join(tmpdir(), "pragma-fresh-home-"));
|
|
72
|
+
return {
|
|
73
|
+
HOME: home,
|
|
74
|
+
USERPROFILE: home,
|
|
75
|
+
XDG_CONFIG_HOME: mkdtempSync(join(tmpdir(), "pragma-fresh-cfg-")),
|
|
76
|
+
XDG_DATA_HOME: mkdtempSync(join(tmpdir(), "pragma-fresh-data-")),
|
|
77
|
+
XDG_STATE_HOME: mkdtempSync(join(tmpdir(), "pragma-fresh-state-")),
|
|
78
|
+
XDG_CACHE_HOME: mkdtempSync(join(tmpdir(), "pragma-fresh-cache-")),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("bundled skills — the shipped snapshot", () => {
|
|
83
|
+
it("the committed artifact is a real, well-formed skills root", () => {
|
|
84
|
+
const root = bundledSkillsDir();
|
|
85
|
+
expect(root).toBeDefined();
|
|
86
|
+
const entries = readdirSync(root as string);
|
|
87
|
+
// Not merely non-empty: EVERY entry must be a discoverable skill. A folder
|
|
88
|
+
// the copy pass half-wrote (or a stray file) would be skipped silently by
|
|
89
|
+
// discovery, which is exactly how a thinned snapshot ships unnoticed.
|
|
90
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
91
|
+
expect(
|
|
92
|
+
shippedSkills()
|
|
93
|
+
.map((s) => s.folder)
|
|
94
|
+
.sort(),
|
|
95
|
+
).toEqual([...entries].sort());
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("is the LAST discovery root, after project and installed", () => {
|
|
99
|
+
const roots = globalSkillRoots();
|
|
100
|
+
expect(roots[0]).toBe(installedSkillsDir());
|
|
101
|
+
expect(roots.at(-1)).toBe(bundledSkillsDir());
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("bundled skills — a fresh install, through the shipped entry", () => {
|
|
106
|
+
it("lists the packs' skills with no `sources update` (REGRESSION)", () => {
|
|
107
|
+
// THE DEFECT. A fresh install had zero skills and a two-command, network-
|
|
108
|
+
// dependent recovery (`sources update` then `setup skills`) for content the
|
|
109
|
+
// release had already resolved at bundle time. Isolated HOME + all four XDG
|
|
110
|
+
// roots, so anything answered here was answered from the package itself.
|
|
111
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-fresh-proj-"));
|
|
112
|
+
const outcome = runCli(["skill", "list", "--format", "json"], {
|
|
113
|
+
cwd,
|
|
114
|
+
env: freshInstallEnv(),
|
|
115
|
+
});
|
|
116
|
+
expect(outcome.exitCode).toBe(0);
|
|
117
|
+
const envelope = JSON.parse(outcome.stdout) as {
|
|
118
|
+
data: { name: string }[];
|
|
119
|
+
};
|
|
120
|
+
const listed = envelope.data.map((skill) => skill.name).sort();
|
|
121
|
+
expect(listed.length).toBeGreaterThan(0);
|
|
122
|
+
// Every skill the snapshot ships is listed — not just "some skills".
|
|
123
|
+
expect(listed).toEqual(
|
|
124
|
+
expect.arrayContaining(
|
|
125
|
+
shippedSkills()
|
|
126
|
+
.map((s) => s.name)
|
|
127
|
+
.sort(),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
expect(outcome.stderr).not.toContain("No skills found");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("`setup --dry-run` OFFERS a skills row instead of skipping it", () => {
|
|
134
|
+
// The other half of the same defect: the global scope's only source was the
|
|
135
|
+
// installed root, so a fresh machine's setup plan skipped skills entirely
|
|
136
|
+
// and pointed at `sources update` as the remedy.
|
|
137
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-fresh-setup-"));
|
|
138
|
+
const outcome = runCli(["setup", "--dry-run", "--global"], {
|
|
139
|
+
cwd,
|
|
140
|
+
env: freshInstallEnv(),
|
|
141
|
+
});
|
|
142
|
+
expect(outcome.exitCode).toBe(0);
|
|
143
|
+
expect(outcome.stdout).toMatch(/skills\s+link\s+\d+ skills? →/);
|
|
144
|
+
expect(outcome.stdout).not.toContain("no skills installed");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("bundled skills — precedence", () => {
|
|
149
|
+
let previousDataHome: string | undefined;
|
|
150
|
+
let dataHome: string;
|
|
151
|
+
let cwd: string;
|
|
152
|
+
|
|
153
|
+
/** The first skill the snapshot ships — the name both overrides reuse. */
|
|
154
|
+
const shadowed = (): { folder: string; name: string } => {
|
|
155
|
+
const first = shippedSkills()[0];
|
|
156
|
+
if (first === undefined) throw new Error("no bundled skills to shadow");
|
|
157
|
+
return first;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Write a `SKILL.md` for `name` under `root/folder`, with a marker body. */
|
|
161
|
+
const writeSkill = (root: string, folder: string, name: string): void => {
|
|
162
|
+
mkdirSync(join(root, folder), { recursive: true });
|
|
163
|
+
writeFileSync(
|
|
164
|
+
join(root, folder, "SKILL.md"),
|
|
165
|
+
`---\nname: ${name}\ndescription: OVERRIDE.\n---\n`,
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
previousDataHome = process.env.XDG_DATA_HOME;
|
|
171
|
+
dataHome = mkdtempSync(join(tmpdir(), "pragma-prec-data-"));
|
|
172
|
+
process.env.XDG_DATA_HOME = dataHome;
|
|
173
|
+
cwd = mkdtempSync(join(tmpdir(), "pragma-prec-proj-"));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
process.env.XDG_DATA_HOME = previousDataHome;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("an INSTALLED skill of the same name wins over the bundled copy", () => {
|
|
181
|
+
// The rule someone who ran `sources update` is relying on: the update they
|
|
182
|
+
// asked for must reach them, never the copy frozen at release time.
|
|
183
|
+
const { folder, name } = shadowed();
|
|
184
|
+
writeSkill(installedSkillsDir(), folder, name);
|
|
185
|
+
const found = discoverSkills(cwd).find((skill) => skill.name === name);
|
|
186
|
+
expect(found?.description).toBe("OVERRIDE.");
|
|
187
|
+
expect(found?.sourcePath).toBe(join(installedSkillsDir(), folder));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("a PROJECT skill wins over both", () => {
|
|
191
|
+
const { folder, name } = shadowed();
|
|
192
|
+
writeSkill(installedSkillsDir(), folder, name);
|
|
193
|
+
writeSkill(join(cwd, ".pragma", "skills"), folder, `${name}`);
|
|
194
|
+
const found = discoverSkills(cwd).find((skill) => skill.name === name);
|
|
195
|
+
expect(found?.sourcePath).toBe(join(cwd, ".pragma", "skills", folder));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("shadowing REPLACES rather than duplicates the name", () => {
|
|
199
|
+
const { folder, name } = shadowed();
|
|
200
|
+
writeSkill(installedSkillsDir(), folder, name);
|
|
201
|
+
const matches = discoverSkills(cwd).filter((skill) => skill.name === name);
|
|
202
|
+
expect(matches).toHaveLength(1);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("bundled skills — the upgrade hazard", () => {
|
|
207
|
+
// THE HAZARD, spelled out. Every other link `setup skills` writes points into
|
|
208
|
+
// a directory pragma controls; a bundled one points into the package
|
|
209
|
+
// directory, which the PACKAGE MANAGER owns. `npm i -g` reuses the same path,
|
|
210
|
+
// but a pnpm / npx / versioned layout does not — so an upgrade can leave a
|
|
211
|
+
// harness link dangling at a package directory that no longer exists.
|
|
212
|
+
//
|
|
213
|
+
// The design turns on whether the EXISTING reconcile repairs that or leaves
|
|
214
|
+
// it, and that is answered here by construction rather than by reading the
|
|
215
|
+
// code: a link to a plausible previous release's path is planted in the
|
|
216
|
+
// harness directory, and the forward plan is inspected.
|
|
217
|
+
const FLAGS: GlobalFlags = {
|
|
218
|
+
format: "plain",
|
|
219
|
+
color: false,
|
|
220
|
+
quiet: false,
|
|
221
|
+
verbose: false,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let prevHome: string | undefined;
|
|
225
|
+
let prevDataHome: string | undefined;
|
|
226
|
+
let home: string;
|
|
227
|
+
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
prevHome = process.env.HOME;
|
|
230
|
+
prevDataHome = process.env.XDG_DATA_HOME;
|
|
231
|
+
home = mkdtempSync(join(tmpdir(), "pragma-upgrade-home-"));
|
|
232
|
+
process.env.HOME = home;
|
|
233
|
+
// An empty installed root, so the bundled snapshot is the scope's only
|
|
234
|
+
// source — exactly a fresh install that has never run `sources update`.
|
|
235
|
+
process.env.XDG_DATA_HOME = mkdtempSync(
|
|
236
|
+
join(tmpdir(), "pragma-upgrade-data-"),
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
afterEach(() => {
|
|
241
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
242
|
+
else process.env.HOME = prevHome;
|
|
243
|
+
if (prevDataHome === undefined) delete process.env.XDG_DATA_HOME;
|
|
244
|
+
else process.env.XDG_DATA_HOME = prevDataHome;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("repairs a link into a REPLACED package directory (REGRESSION)", async () => {
|
|
248
|
+
const first = shippedSkills()[0];
|
|
249
|
+
if (first === undefined) throw new Error("no bundled skills");
|
|
250
|
+
// The harness directory as an upgrade would leave it: a link at the right
|
|
251
|
+
// name pointing into the version-stamped path of the release before this
|
|
252
|
+
// one, which no longer exists.
|
|
253
|
+
const harnessDir = join(home, ".agents", "skills");
|
|
254
|
+
mkdirSync(harnessDir, { recursive: true });
|
|
255
|
+
const linkPath = join(harnessDir, first.folder);
|
|
256
|
+
symlinkSync(
|
|
257
|
+
join(tmpdir(), "pragma-cli@0.34.0", "bundled-skills", first.folder),
|
|
258
|
+
linkPath,
|
|
259
|
+
);
|
|
260
|
+
expect(lstatSync(linkPath).isSymbolicLink()).toBe(true); // present…
|
|
261
|
+
expect(existsSync(linkPath)).toBe(false); // …and dangling.
|
|
262
|
+
|
|
263
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-upgrade-proj-"));
|
|
264
|
+
const detected = await detectSkills(bootRuntime(FLAGS, cwd), "global");
|
|
265
|
+
|
|
266
|
+
// Planned as `replaced` — NOT skipped as somebody else's link, and NOT
|
|
267
|
+
// swept as an orphan: the per-skill pass covers the path, so the sweep
|
|
268
|
+
// never sees it. Both halves matter; either one alone would lose the link.
|
|
269
|
+
const action = detected.actions.find((a) => a.linkPath === linkPath);
|
|
270
|
+
expect(action?.action).toBe("replaced");
|
|
271
|
+
expect(action?.skillName).toBe(first.name);
|
|
272
|
+
expect(staleSkillLinks(detected).map((a) => a.linkPath)).not.toContain(
|
|
273
|
+
linkPath,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// And the forward run is delete-then-relink onto the CURRENT package, so
|
|
277
|
+
// one `setup skills` after an upgrade converges the harness.
|
|
278
|
+
expect(
|
|
279
|
+
dryRun(composeSkills(detected)).effects.map((e) => e._tag),
|
|
280
|
+
).toContain("DeleteFile");
|
|
281
|
+
await runTask(composeSkills(detected));
|
|
282
|
+
expect(existsSync(linkPath)).toBe(true);
|
|
283
|
+
expect(readFileSync(join(linkPath, "SKILL.md"), "utf-8")).toContain(
|
|
284
|
+
`name: ${first.name}`,
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("doctor makes the upgrade window VISIBLE, with the command that closes it", async () => {
|
|
289
|
+
// The residual risk is the gap between an upgrade and the next
|
|
290
|
+
// `setup skills`: the link dangles and the harness sees a broken skill.
|
|
291
|
+
// One command converges it (above) — so what has to be true is that
|
|
292
|
+
// something TELLS the user to run it. This is that assertion. Without it
|
|
293
|
+
// the window is real but silent, which is the "doctor blind to orphans"
|
|
294
|
+
// shape sitting on a path an upgrade reaches routinely.
|
|
295
|
+
const first = shippedSkills()[0];
|
|
296
|
+
if (first === undefined) throw new Error("no bundled skills");
|
|
297
|
+
const harnessDir = join(home, ".agents", "skills");
|
|
298
|
+
mkdirSync(harnessDir, { recursive: true });
|
|
299
|
+
symlinkSync(
|
|
300
|
+
join(tmpdir(), "pragma-cli@0.34.0", "bundled-skills", first.folder),
|
|
301
|
+
join(harnessDir, first.folder),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-upgrade-doctor-"));
|
|
305
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), BIN_NAME);
|
|
306
|
+
const row = rows.find((r) => r.name === "skills" && r.scope === "global");
|
|
307
|
+
expect(row?.status).toBe("fail");
|
|
308
|
+
expect(row?.detail).toContain("point elsewhere");
|
|
309
|
+
// No AUTHORED remedy on this branch: the row takes the derived one, which
|
|
310
|
+
// is exactly the command proven to repair it in the cell above.
|
|
311
|
+
expect(row?.remedy).toBe(`${BIN_NAME} setup skills`);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("discovery itself cannot degrade on an upgrade", () => {
|
|
315
|
+
// The other half of the worry — that a moved package directory would make
|
|
316
|
+
// the snapshot vanish from DISCOVERY too, silently returning the CLI to the
|
|
317
|
+
// skips-everything state this change removes. It cannot: `bundledSkillsDir`
|
|
318
|
+
// walks up from `import.meta.url`, so it resolves against whichever copy of
|
|
319
|
+
// the package is EXECUTING, which is the new one by definition. There is no
|
|
320
|
+
// recorded path anywhere that an upgrade could invalidate.
|
|
321
|
+
expect((bundledSkillsDir() as string).startsWith(packageRoot)).toBe(true);
|
|
322
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-upgrade-disc-"));
|
|
323
|
+
expect(discoverSkills(cwd).length).toBeGreaterThan(0);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("bundled skills — packaging", () => {
|
|
328
|
+
// Generous timeout: this shells out to the real packer over ~950 files.
|
|
329
|
+
it("the snapshot is in the published tarball (`files` allowlist)", () => {
|
|
330
|
+
// `files` allowlists are exactly the kind of thing that looks right and
|
|
331
|
+
// ships nothing: the directory would exist in the repo, every test above
|
|
332
|
+
// would pass from the source tree, and a consumer's install would carry no
|
|
333
|
+
// skills at all. Only the packer's own answer settles it.
|
|
334
|
+
const packed = spawnSync(
|
|
335
|
+
"npm",
|
|
336
|
+
["pack", "--dry-run", "--json", "--ignore-scripts"],
|
|
337
|
+
{ cwd: packageRoot, encoding: "utf-8", timeout: 120_000 },
|
|
338
|
+
);
|
|
339
|
+
expect(packed.status).toBe(0);
|
|
340
|
+
const files = (
|
|
341
|
+
JSON.parse(packed.stdout) as { files: { path: string }[] }[]
|
|
342
|
+
)[0]?.files.map((file) => file.path);
|
|
343
|
+
for (const { folder } of shippedSkills()) {
|
|
344
|
+
expect(files).toContain(`bundled-skills/${folder}/SKILL.md`);
|
|
345
|
+
}
|
|
346
|
+
}, 120_000);
|
|
347
|
+
});
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
* A skill is a folder with a `SKILL.md` carrying YAML frontmatter (`name`,
|
|
5
5
|
* `description`, and the #856 `prompt` flag among others). Skills are discovered
|
|
6
6
|
* from conventional roots — project skills under `<cwd>/.pragma/skills`, which
|
|
7
|
-
* take precedence, then installed skills under `$XDG_DATA_HOME/pragma/skills
|
|
8
|
-
*
|
|
7
|
+
* take precedence, then installed skills under `$XDG_DATA_HOME/pragma/skills`,
|
|
8
|
+
* then the BUNDLED snapshot this package ships (`bundled-skills/`) — with
|
|
9
|
+
* missing files and invalid frontmatter skipped gracefully. Reads only the
|
|
9
10
|
* filesystem, never the graph store, so `skill list`/`lookup` are storeless
|
|
10
11
|
* (needsStore: false).
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
14
15
|
import { homedir } from "node:os";
|
|
15
|
-
import { basename, join } from "node:path";
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
16
18
|
import { BIN_NAME } from "../../constants.js";
|
|
17
19
|
|
|
18
20
|
/** Parsed SKILL.md frontmatter. */
|
|
@@ -123,8 +125,8 @@ export function installedSkillsDir(): string {
|
|
|
123
125
|
/**
|
|
124
126
|
* The project skills root: `<cwd>/.<bin>/skills` — skills that belong to THIS
|
|
125
127
|
* repository and travel with it. Named separately from {@link skillRoots}
|
|
126
|
-
* because the two roots are also the two setup
|
|
127
|
-
* links this root into the project's harness directories, while the global
|
|
128
|
+
* because the two roots are also the two setup SCOPES: `setup skills --local`
|
|
129
|
+
* links this root into the project's harness directories, while the global scope
|
|
128
130
|
* links {@link installedSkillsDir} into the user-level ones. Mixing them leaks
|
|
129
131
|
* machine state into a repository's directories, or applies one repository's
|
|
130
132
|
* skills machine-wide.
|
|
@@ -133,15 +135,102 @@ export function projectSkillsDir(cwd: string): string {
|
|
|
133
135
|
return join(cwd, `.${BIN_NAME}`, "skills");
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The directory name of the SNAPSHOT of pack skills this package ships, at its
|
|
140
|
+
* own root. Written by `scripts/bundle.ts` and COMMITTED, exactly as
|
|
141
|
+
* `src/kernel/runtime/graphpack/embedded/pack.generated.ts` is — see
|
|
142
|
+
* {@link bundledSkillsDir}.
|
|
143
|
+
*/
|
|
144
|
+
const BUNDLED_SKILLS_DIRNAME = "bundled-skills";
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* How far above this module the package root can be. `src/capabilities/skill`
|
|
148
|
+
* is three levels up; the emitted `dist/src/capabilities/skill` is four. The
|
|
149
|
+
* bound exists so a tree that is MISSING the artifact stops at the package
|
|
150
|
+
* instead of walking to `/` and adopting an unrelated directory.
|
|
151
|
+
*/
|
|
152
|
+
const BUNDLED_SEARCH_DEPTH = 6;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The BUNDLED-skills root: `<package root>/bundled-skills`, the snapshot of the
|
|
156
|
+
* declared packs' `skills/<name>/` that `scripts/bundle.ts` commits alongside
|
|
157
|
+
* the embedded graph — `undefined` when this build ships none.
|
|
158
|
+
*
|
|
159
|
+
* WHY IT EXISTS. The embedded pack (`pack.generated.ts`) is what lets a fresh
|
|
160
|
+
* install answer `block lookup` offline. The skills came out of the very same
|
|
161
|
+
* `resolvePackage` calls and were left on the build machine's disk, so the same
|
|
162
|
+
* fresh install listed ZERO skills and told the user to run `sources update`
|
|
163
|
+
* first — a network round trip and two commands for something already resolved
|
|
164
|
+
* at release time. This is the skills half of the snapshot the graph has had
|
|
165
|
+
* all along.
|
|
166
|
+
*
|
|
167
|
+
* WHY IT IS FOUND BY WALKING UP rather than by a fixed relative path. `files`
|
|
168
|
+
* ships BOTH `src` and `dist`, and `tsconfig.build.json` sets `rootDir: "."`,
|
|
169
|
+
* so this module runs from `src/capabilities/skill/` in a source or test run
|
|
170
|
+
* and from `dist/src/capabilities/skill/` in the shipped one — the package root
|
|
171
|
+
* is three levels up in one and four in the other, and no single
|
|
172
|
+
* `new URL("../../..")` is right for both. The walk anchors on the artifact's
|
|
173
|
+
* own name sitting NEXT TO a `package.json`, which is true only at the package
|
|
174
|
+
* root: `dist/` carries a `package.json` too (`resolveJsonModule` copies it)
|
|
175
|
+
* but never the snapshot.
|
|
176
|
+
*
|
|
177
|
+
* @returns The bundled root, or `undefined` when no ancestor holds one.
|
|
178
|
+
* @note Impure — stats up to {@link BUNDLED_SEARCH_DEPTH} ancestor directories.
|
|
179
|
+
* Reached from the `__complete` fast path, so it stays `node:fs` /
|
|
180
|
+
* `node:path` / `node:url` only and costs a handful of `stat`s.
|
|
181
|
+
*/
|
|
182
|
+
export function bundledSkillsDir(): string | undefined {
|
|
183
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
184
|
+
for (let up = 0; up < BUNDLED_SEARCH_DEPTH; up += 1) {
|
|
185
|
+
const candidate = join(dir, BUNDLED_SKILLS_DIRNAME);
|
|
186
|
+
if (existsSync(join(dir, "package.json")) && existsSync(candidate)) {
|
|
187
|
+
return candidate;
|
|
188
|
+
}
|
|
189
|
+
const parent = dirname(dir);
|
|
190
|
+
if (parent === dir) break;
|
|
191
|
+
dir = parent;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
136
196
|
/**
|
|
137
197
|
* The conventional roots skills are discovered from, in PRECEDENCE order: the
|
|
138
198
|
* project root (`<cwd>/.<bin>/skills`) FIRST so a project-local skill overrides
|
|
139
199
|
* an installed skill of the same name (`discoverSkills` dedups first-seen-wins),
|
|
140
200
|
* then installed skills under `$XDG_DATA_HOME/<bin>/skills` (where package
|
|
141
|
-
* skills land on `sources update`)
|
|
201
|
+
* skills land on `sources update`), then the BUNDLED snapshot
|
|
202
|
+
* ({@link bundledSkillsDir}).
|
|
203
|
+
*
|
|
204
|
+
* BUNDLED IS LAST, and that order is the whole contract. The snapshot is as old
|
|
205
|
+
* as the release; an installed skill came from a `sources update` the user ran
|
|
206
|
+
* deliberately, and a project skill is the repository's own. So someone who
|
|
207
|
+
* updates gets the CURRENT skill and never the shipped copy, while a fresh
|
|
208
|
+
* install — which has neither of the first two roots — still gets the packs'
|
|
209
|
+
* skills instead of an empty list and a two-command recovery.
|
|
142
210
|
*/
|
|
143
211
|
export function skillRoots(cwd: string): string[] {
|
|
144
|
-
return [projectSkillsDir(cwd),
|
|
212
|
+
return [projectSkillsDir(cwd), ...globalSkillRoots()];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The GLOBAL scope's source roots, in precedence order: the INSTALLED root
|
|
217
|
+
* (`sources update`'s output) first, then the BUNDLED snapshot.
|
|
218
|
+
*
|
|
219
|
+
* Named separately from {@link skillRoots} for the reason
|
|
220
|
+
* {@link projectSkillsDir} is: these are the two roots `setup skills --global`
|
|
221
|
+
* links into the user-level harness directories, and `skillRoots` is discovery,
|
|
222
|
+
* which spans both scopes. Reading the project root for the global scope is
|
|
223
|
+
* exactly the cross-scope leak the scope split exists to prevent.
|
|
224
|
+
*
|
|
225
|
+
* A BAND CAN HOLD MORE THAN ONE ROOT, and this is where that became true. The
|
|
226
|
+
* global scope's ownership test and its stale-link sweep therefore range over
|
|
227
|
+
* the SET (see `setup/operations/setupSkills.ts`), never over one path.
|
|
228
|
+
*/
|
|
229
|
+
export function globalSkillRoots(): string[] {
|
|
230
|
+
const bundled = bundledSkillsDir();
|
|
231
|
+
return bundled === undefined
|
|
232
|
+
? [installedSkillsDir()]
|
|
233
|
+
: [installedSkillsDir(), bundled];
|
|
145
234
|
}
|
|
146
235
|
|
|
147
236
|
/** Immediate subdirectories of `root` (each a candidate skill folder). */
|
|
@@ -165,10 +254,10 @@ function subdirs(root: string): string[] {
|
|
|
165
254
|
* Discover every skill reachable from an EXPLICIT list of roots, in precedence
|
|
166
255
|
* order (first-seen wins on a duplicate name).
|
|
167
256
|
*
|
|
168
|
-
* Split out of {@link discoverSkills} so a caller that owns one
|
|
169
|
-
* exactly that
|
|
257
|
+
* Split out of {@link discoverSkills} so a caller that owns one scope can read
|
|
258
|
+
* exactly that scope's root: `setup skills` links the project root into project
|
|
170
259
|
* harness directories and the installed root into user-level ones, and reading
|
|
171
|
-
* both roots for either
|
|
260
|
+
* both roots for either scope is what used to mix the two.
|
|
172
261
|
*
|
|
173
262
|
* @param roots - The roots to read, in precedence order.
|
|
174
263
|
* @returns Discovered skills, sorted by name; malformed ones are skipped.
|
|
@@ -12,7 +12,7 @@ const EMPTY_PLAIN = `No skills found.\nSkills come from design-system packs —
|
|
|
12
12
|
const EMPTY_LLM = `## Skills (0)\n\nNo skills found. Skills come from design-system packs — add one to the project config's packs, run \`${BIN_NAME} sources update\`, then \`${BIN_NAME} setup skills\`.`;
|
|
13
13
|
|
|
14
14
|
export const skillListFormatters: Formatters<DiscoveredSkill[]> = {
|
|
15
|
-
// Zero skills: plain stdout stays empty — the guidance is `
|
|
15
|
+
// Zero skills: plain stdout stays empty — the guidance is `notice`,
|
|
16
16
|
// which the dispatcher routes to stderr (exit 0) so a pipe reads no prose.
|
|
17
17
|
plain: (skills) =>
|
|
18
18
|
skills
|
|
@@ -21,7 +21,7 @@ export const skillListFormatters: Formatters<DiscoveredSkill[]> = {
|
|
|
21
21
|
`${s.name}${s.frontmatter.prompt ? " (prompt)" : ""} ${s.description}`,
|
|
22
22
|
)
|
|
23
23
|
.join("\n"),
|
|
24
|
-
|
|
24
|
+
notice: (skills) => (skills.length === 0 ? EMPTY_PLAIN : undefined),
|
|
25
25
|
llm: (skills) =>
|
|
26
26
|
skills.length === 0
|
|
27
27
|
? EMPTY_LLM
|
|
@@ -15,7 +15,11 @@ import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
|
15
15
|
import type { PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
16
16
|
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
17
17
|
import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
|
|
18
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
bundledSkillsDir,
|
|
20
|
+
type DiscoveredSkill,
|
|
21
|
+
discoverSkillsFrom,
|
|
22
|
+
} from "./discover.js";
|
|
19
23
|
import { skillModule } from "./index.js";
|
|
20
24
|
import { skillListFormatters } from "./render.js";
|
|
21
25
|
import type { SkillLookup } from "./verbs.js";
|
|
@@ -60,7 +64,16 @@ afterAll(() => {
|
|
|
60
64
|
describe("skill list (storeless)", () => {
|
|
61
65
|
it("discovers skills by name, carrying the prompt flag", async () => {
|
|
62
66
|
const skills = (await listVerb.run({}, rt)) as DiscoveredSkill[];
|
|
63
|
-
|
|
67
|
+
// The list is the project root's two skills UNION the bundled snapshot the
|
|
68
|
+
// package ships (`bundledSkills.test.ts` owns the snapshot's own cells).
|
|
69
|
+
// Derived from the shipped artifact rather than spelled out, so a release
|
|
70
|
+
// that changes which skills the packs provide does not rewrite this test.
|
|
71
|
+
const bundled = discoverSkillsFrom([bundledSkillsDir() as string]).map(
|
|
72
|
+
(s) => s.name,
|
|
73
|
+
);
|
|
74
|
+
expect(skills.map((s) => s.name)).toEqual(
|
|
75
|
+
[...new Set(["docx", "pdf", ...bundled])].sort(),
|
|
76
|
+
);
|
|
64
77
|
expect(skills.find((s) => s.name === "pdf")?.frontmatter.prompt).toBe(true);
|
|
65
78
|
expect(
|
|
66
79
|
skills.find((s) => s.name === "docx")?.frontmatter.prompt,
|
|
@@ -87,9 +100,9 @@ describe("skill lookup (storeless)", () => {
|
|
|
87
100
|
describe("skill list empty-state (U5)", () => {
|
|
88
101
|
it("the empty notice (stderr's text) guides toward package sources", () => {
|
|
89
102
|
// Zero skills: plain stdout is empty (a pipe reads no prose) and the
|
|
90
|
-
// guidance rides the dispatcher's stderr via `
|
|
103
|
+
// guidance rides the dispatcher's stderr via `notice`.
|
|
91
104
|
expect(skillListFormatters.plain([])).toBe("");
|
|
92
|
-
const notice = skillListFormatters.
|
|
105
|
+
const notice = skillListFormatters.notice?.([]);
|
|
93
106
|
expect(notice).toContain("No skills found");
|
|
94
107
|
expect(notice).toContain("pragma sources update");
|
|
95
108
|
});
|