@canonical/pragma-cli 0.35.0 → 0.37.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 +24 -20
- package/dist/pragma.conf.js +266 -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/capabilities/hints.js +1 -1
- package/dist/src/capabilities/capabilities/hints.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/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.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/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.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/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- 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/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
- 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 +165 -63
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +122 -51
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +7 -6
- 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 +13 -2
- 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 +310 -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 +46 -24
- 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 +42 -9
- 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/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +14 -9
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +9 -3
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +24 -20
- package/pragma.conf.ts +272 -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/capabilities/hints.ts +1 -1
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +5 -2
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- 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/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- 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/lazy.test.ts +26 -2
- package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- 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/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
- package/src/capabilities/setup/operations/setupLsp.ts +203 -38
- package/src/capabilities/setup/operations/setupMcp.ts +139 -62
- package/src/capabilities/setup/operations/setupSkills.ts +399 -60
- 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 +1126 -50
- package/src/capabilities/setup/setup.verb.ts +174 -68
- package/src/capabilities/setup/targets.ts +138 -60
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +18 -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 +14 -6
- 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 +14 -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 +19 -3
- package/src/kernel/config/readConfig.test.ts +1 -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 +327 -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 +48 -25
- 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 +44 -8
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/prefixes.test.ts +2 -0
- 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/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +14 -12
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +9 -3
- package/src/kernel/runtime/types.ts +17 -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/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- 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
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* by `assertExecOk` as an actionable UNSUPPORTED (exit 1) — the interpreter
|
|
14
14
|
* RESOLVES on a nonzero exit, so the check is the consumer's job.
|
|
15
15
|
* - offline / already-latest carry NO exec — a real run just returns the status.
|
|
16
|
+
* - a non-global install (linked / ephemeral / workspace / unknown) also
|
|
17
|
+
* carries NO exec even when an update exists: there is no command pragma may
|
|
18
|
+
* run for it, so the task reports the delta plus the state's guidance and
|
|
19
|
+
* mutates nothing. `pmUpdateCommand` only accepts the global arm, so this is
|
|
20
|
+
* enforced by the types, not by convention.
|
|
16
21
|
*
|
|
17
22
|
* The whole registry read is preview-safe, so no `mutation.preview` gating is
|
|
18
23
|
* needed here (unlike `sources update`, whose heavy resolve must be gated).
|
|
@@ -29,6 +34,7 @@ import {
|
|
|
29
34
|
guardMissingBinary,
|
|
30
35
|
PRAGMA_PACKAGE,
|
|
31
36
|
pmUpdateCommand,
|
|
37
|
+
updateGuidance,
|
|
32
38
|
} from "../shared/index.js";
|
|
33
39
|
import type { UpgradeData } from "./types.js";
|
|
34
40
|
|
|
@@ -46,8 +52,15 @@ export async function runUpgrade(
|
|
|
46
52
|
): Promise<Task<UpgradeData>> {
|
|
47
53
|
const { channel } = (await rt.loadConfig()).config;
|
|
48
54
|
const install = detectInstallSource();
|
|
49
|
-
|
|
55
|
+
// Only a GLOBAL install has a sanctioned update command — the type of
|
|
56
|
+
// `pmUpdateCommand` enforces the narrowing. A linked checkout in particular
|
|
57
|
+
// must never be handed `npm i -g`: it would overwrite the development link.
|
|
58
|
+
const command =
|
|
59
|
+
install.kind === "global"
|
|
60
|
+
? pmUpdateCommand(install, PRAGMA_PACKAGE)
|
|
61
|
+
: undefined;
|
|
50
62
|
const pm = install.label;
|
|
63
|
+
const kind = install.kind;
|
|
51
64
|
const current = rt.version;
|
|
52
65
|
|
|
53
66
|
const registry = await checkRegistryVersion(PRAGMA_PACKAGE, channel);
|
|
@@ -55,9 +68,10 @@ export async function runUpgrade(
|
|
|
55
68
|
if (registry === undefined) {
|
|
56
69
|
const data: UpgradeData = {
|
|
57
70
|
pm,
|
|
71
|
+
kind,
|
|
58
72
|
current,
|
|
59
73
|
latest: undefined,
|
|
60
|
-
command,
|
|
74
|
+
...(command !== undefined ? { command } : {}),
|
|
61
75
|
offline: true,
|
|
62
76
|
alreadyLatest: false,
|
|
63
77
|
executed: false,
|
|
@@ -71,9 +85,10 @@ export async function runUpgrade(
|
|
|
71
85
|
if (registry.latest === current) {
|
|
72
86
|
const data: UpgradeData = {
|
|
73
87
|
pm,
|
|
88
|
+
kind,
|
|
74
89
|
current,
|
|
75
90
|
latest: registry.latest,
|
|
76
|
-
command,
|
|
91
|
+
...(command !== undefined ? { command } : {}),
|
|
77
92
|
offline: false,
|
|
78
93
|
alreadyLatest: true,
|
|
79
94
|
executed: false,
|
|
@@ -84,16 +99,40 @@ export async function runUpgrade(
|
|
|
84
99
|
});
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
if (install.kind !== "global") {
|
|
103
|
+
// An update exists, but this install has no command pragma may run for
|
|
104
|
+
// it — say so honestly and run NOTHING. (The discriminant narrowing is
|
|
105
|
+
// what lets `updateGuidance` accept `install` here.)
|
|
106
|
+
const guidance = updateGuidance(install);
|
|
107
|
+
const data: UpgradeData = {
|
|
108
|
+
pm,
|
|
109
|
+
kind,
|
|
110
|
+
current,
|
|
111
|
+
latest: registry.latest,
|
|
112
|
+
guidance,
|
|
113
|
+
offline: false,
|
|
114
|
+
alreadyLatest: false,
|
|
115
|
+
executed: false,
|
|
116
|
+
};
|
|
117
|
+
return gen(function* () {
|
|
118
|
+
yield* $(info(`${current} → ${registry.latest}`));
|
|
119
|
+
yield* $(warn(`No automatic upgrade for this install: ${guidance}`));
|
|
120
|
+
return data;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const updateCommand = pmUpdateCommand(install, PRAGMA_PACKAGE);
|
|
87
125
|
const data: UpgradeData = {
|
|
88
126
|
pm,
|
|
127
|
+
kind,
|
|
89
128
|
current,
|
|
90
129
|
latest: registry.latest,
|
|
91
|
-
command,
|
|
130
|
+
command: updateCommand,
|
|
92
131
|
offline: false,
|
|
93
132
|
alreadyLatest: false,
|
|
94
133
|
executed: true,
|
|
95
134
|
};
|
|
96
|
-
const parts =
|
|
135
|
+
const parts = updateCommand.split(" ");
|
|
97
136
|
const bin = parts[0] ?? "npm";
|
|
98
137
|
const args = parts.slice(1);
|
|
99
138
|
// Guard the spawn: an absent package manager (`bin` not on PATH) REJECTS the
|
|
@@ -112,7 +151,7 @@ export async function runUpgrade(
|
|
|
112
151
|
// so inspect the result: a failed install (e.g. EACCES on a global
|
|
113
152
|
// `npm i -g`) must fail loudly, not report a silent success.
|
|
114
153
|
const result = yield* $(exec(bin, args, rt.cwd));
|
|
115
|
-
assertExecOk(
|
|
154
|
+
assertExecOk(updateCommand, result);
|
|
116
155
|
return data;
|
|
117
156
|
}),
|
|
118
157
|
);
|
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
* Data shape for `pragma upgrade`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { InstallKind } from "../../kernel/render/vocabulary.js";
|
|
6
|
+
|
|
5
7
|
/** The outcome of an upgrade run (or its plan). */
|
|
6
8
|
export interface UpgradeData {
|
|
7
9
|
/** The install-source label (e.g. `bun (global)`). */
|
|
8
10
|
readonly pm: string;
|
|
11
|
+
/** The install-source state behind the label. */
|
|
12
|
+
readonly kind: InstallKind;
|
|
9
13
|
/** The currently-installed version. */
|
|
10
14
|
readonly current: string;
|
|
11
15
|
/** The latest published version, or `undefined` when offline. */
|
|
12
16
|
readonly latest: string | undefined;
|
|
13
|
-
/** The package-manager command that applies the update
|
|
14
|
-
|
|
17
|
+
/** The package-manager command that applies the update — only for a GLOBAL
|
|
18
|
+
* install; every other state carries {@link guidance} instead. */
|
|
19
|
+
readonly command?: string;
|
|
20
|
+
/** The honest sentence for an install with no sanctioned update command
|
|
21
|
+
* (linked / ephemeral / workspace / unknown). */
|
|
22
|
+
readonly guidance?: string;
|
|
15
23
|
/** True when the registry could not be reached. */
|
|
16
24
|
readonly offline: boolean;
|
|
17
25
|
/** True when the installed version is already the latest. */
|
|
@@ -33,8 +33,12 @@ export const upgradeFormatters: Formatters<UpgradeData> = {
|
|
|
33
33
|
"",
|
|
34
34
|
`Updated to ${data.latest}.`,
|
|
35
35
|
);
|
|
36
|
-
} else {
|
|
36
|
+
} else if (data.command) {
|
|
37
37
|
lines.push(`Run: ${chalk.cyan(data.command)}`);
|
|
38
|
+
} else {
|
|
39
|
+
// No sanctioned command for this install (linked / ephemeral /
|
|
40
|
+
// workspace / unknown) — the guidance sentence is the whole story.
|
|
41
|
+
lines.push(`No automatic upgrade for this install: ${data.guidance}`);
|
|
38
42
|
}
|
|
39
43
|
return lines.join("\n");
|
|
40
44
|
},
|
|
@@ -45,7 +49,10 @@ export const upgradeFormatters: Formatters<UpgradeData> = {
|
|
|
45
49
|
return `Already at the latest version (${data.current}).`;
|
|
46
50
|
}
|
|
47
51
|
if (data.executed) return `Upgraded: ${data.current} → ${data.latest}`;
|
|
48
|
-
|
|
52
|
+
if (data.command) {
|
|
53
|
+
return `Update available: ${data.current} → ${data.latest} (\`${data.command}\`)`;
|
|
54
|
+
}
|
|
55
|
+
return `Update available: ${data.current} → ${data.latest} — ${data.guidance}`;
|
|
49
56
|
},
|
|
50
57
|
|
|
51
58
|
json(data) {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
guardMissingBinary,
|
|
29
29
|
isMissingBinaryError,
|
|
30
30
|
} from "../shared/assertExecOk.js";
|
|
31
|
+
import type { InstallSource } from "../shared/index.js";
|
|
31
32
|
import {
|
|
32
33
|
checkRegistryVersion,
|
|
33
34
|
REGISTRY_TIMEOUT_MS,
|
|
@@ -36,6 +37,22 @@ import { runUpgrade } from "./runUpgrade.js";
|
|
|
36
37
|
import type { UpgradeData } from "./types.js";
|
|
37
38
|
import { upgradeModule } from "./upgrade.verb.js";
|
|
38
39
|
|
|
40
|
+
// Pin the install-source detection: the real detector reads THIS process
|
|
41
|
+
// (whose entry is a source checkout — an honest `unknown` with no command,
|
|
42
|
+
// which would starve every update-needed case of its exec). Detection itself
|
|
43
|
+
// is covered by `shared/packageManager.test.ts`; here it is a fixture.
|
|
44
|
+
const detection = vi.hoisted(() => ({ install: undefined as unknown }));
|
|
45
|
+
vi.mock("../shared/index.js", async (importOriginal) => ({
|
|
46
|
+
...(await importOriginal<object>()),
|
|
47
|
+
detectInstallSource: () => detection.install,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const GLOBAL_NPM: InstallSource = {
|
|
51
|
+
kind: "global",
|
|
52
|
+
pm: "npm",
|
|
53
|
+
label: "npm (global)",
|
|
54
|
+
};
|
|
55
|
+
|
|
39
56
|
const upgradeVerb = upgradeModule.verbs[0] as VerbSpec;
|
|
40
57
|
const FLAGS_JSON: GlobalFlags = {
|
|
41
58
|
llm: false,
|
|
@@ -71,6 +88,7 @@ let prevXdg: string | undefined;
|
|
|
71
88
|
beforeEach(() => {
|
|
72
89
|
prevXdg = process.env.XDG_CONFIG_HOME;
|
|
73
90
|
process.env.XDG_CONFIG_HOME = tmpCwd();
|
|
91
|
+
detection.install = GLOBAL_NPM;
|
|
74
92
|
});
|
|
75
93
|
afterEach(() => {
|
|
76
94
|
process.env.XDG_CONFIG_HOME = prevXdg;
|
|
@@ -181,6 +199,60 @@ describe("upgrade — update needed", () => {
|
|
|
181
199
|
});
|
|
182
200
|
});
|
|
183
201
|
|
|
202
|
+
describe("upgrade — a non-global install NEVER gets an exec (the headline)", () => {
|
|
203
|
+
const LINKED: InstallSource = {
|
|
204
|
+
kind: "linked",
|
|
205
|
+
pm: "npm",
|
|
206
|
+
target: "/home/u/code/pragma/packages/cli/pragma",
|
|
207
|
+
label: "npm link (development checkout)",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
it("linked + update available: no command, no exec — guidance instead", async () => {
|
|
211
|
+
// The case that must never regress: `npm i -g` against an npm-linked
|
|
212
|
+
// install would OVERWRITE the symlink to the development worktree.
|
|
213
|
+
detection.install = LINKED;
|
|
214
|
+
stubRegistry("99.0.0");
|
|
215
|
+
const task = await runUpgrade(bootRuntime(FLAGS_JSON, tmpCwd()));
|
|
216
|
+
const { value, effects } = dryRun(task);
|
|
217
|
+
expect(value.executed).toBe(false);
|
|
218
|
+
expect(value.command).toBeUndefined();
|
|
219
|
+
expect(value.kind).toBe("linked");
|
|
220
|
+
expect(value.guidance).toMatch(/development checkout/i);
|
|
221
|
+
expect(effects.some((e) => e._tag === "Exec")).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("--dry-run through dispatch plans NO Execute line for a linked install", async () => {
|
|
225
|
+
detection.install = LINKED;
|
|
226
|
+
stubRegistry("99.0.0");
|
|
227
|
+
const outcome = await executeVerb(
|
|
228
|
+
upgradeVerb,
|
|
229
|
+
{},
|
|
230
|
+
DRY,
|
|
231
|
+
bootRuntime(FLAGS_JSON, tmpCwd()),
|
|
232
|
+
);
|
|
233
|
+
const plan = JSON.parse(outcome.stdout as string).data.plan as string[];
|
|
234
|
+
expect(plan.some((line) => line.startsWith("Execute:"))).toBe(false);
|
|
235
|
+
expect(plan.some((line) => line.includes("No automatic upgrade"))).toBe(
|
|
236
|
+
true,
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("an ephemeral run reports its state with no command either", async () => {
|
|
241
|
+
detection.install = {
|
|
242
|
+
kind: "ephemeral",
|
|
243
|
+
runner: "npx",
|
|
244
|
+
label: "npx (ephemeral)",
|
|
245
|
+
} satisfies InstallSource;
|
|
246
|
+
stubRegistry("99.0.0");
|
|
247
|
+
const { value, effects } = dryRun(
|
|
248
|
+
await runUpgrade(bootRuntime(FLAGS_JSON, tmpCwd())),
|
|
249
|
+
);
|
|
250
|
+
expect(value.executed).toBe(false);
|
|
251
|
+
expect(value.command).toBeUndefined();
|
|
252
|
+
expect(effects.some((e) => e._tag === "Exec")).toBe(false);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
184
256
|
describe("assertExecOk — the exec-exitCode guard", () => {
|
|
185
257
|
it("returns on a zero exit and throws an actionable UNSUPPORTED (with stderr) on nonzero", () => {
|
|
186
258
|
expect(() =>
|
package/src/constants.ts
CHANGED
|
@@ -41,6 +41,13 @@ const ISSUES_URL = identity.issuesUrl;
|
|
|
41
41
|
/** Semver version string read from package.json. */
|
|
42
42
|
const VERSION: string = pkg.version;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The Node.js range this distribution supports, read from `engines.node` in
|
|
46
|
+
* package.json so the manifest stays the single statement of the floor. The
|
|
47
|
+
* doctor check quotes it, and `isSupportedNodeVersion` is pinned to it by test.
|
|
48
|
+
*/
|
|
49
|
+
const SUPPORTED_NODE_RANGE: string = pkg.engines.node;
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* The project config filename the walker looks for and every surface that
|
|
46
53
|
* quotes it — diagnostics, the onboarding note, the generated reference, the
|
|
@@ -76,7 +83,12 @@ type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
|
76
83
|
const DEFAULT_DETAIL_LEVEL: DetailLevel = "standard";
|
|
77
84
|
|
|
78
85
|
/**
|
|
79
|
-
* The wordmark `--help` opens with, as raw ASCII-art lines.
|
|
86
|
+
* The wordmark `--help` opens with FOR A HUMAN READER, as raw ASCII-art lines.
|
|
87
|
+
*
|
|
88
|
+
* Whether it prints is decided at the one emit site (`rootHelp.ts`) from the
|
|
89
|
+
* invocation's global flags: a captured run (`--format llm`/`json`, including
|
|
90
|
+
* the `llm` a non-interactive stdout infers) gets the bare header instead. This
|
|
91
|
+
* constant is the ART, not the policy.
|
|
80
92
|
*
|
|
81
93
|
* PROJECTED from the distribution config, exactly like {@link BIN_NAME} and
|
|
82
94
|
* {@link PROGRAM_DESCRIPTION} — a wordmark spells a name, so it is identity, and
|
|
@@ -111,5 +123,6 @@ export {
|
|
|
111
123
|
PROGRAM_LOGO,
|
|
112
124
|
PROJECT_CONFIG_FILENAME,
|
|
113
125
|
RECOVERY_CLI_PREFIX,
|
|
126
|
+
SUPPORTED_NODE_RANGE,
|
|
114
127
|
VERSION,
|
|
115
128
|
};
|
package/src/identity.test.ts
CHANGED
|
@@ -123,6 +123,9 @@ describe("identity projection — a fork changes values, not code (PROTECTED)",
|
|
|
123
123
|
],
|
|
124
124
|
"9.9.9",
|
|
125
125
|
"https://recipes.test/issues",
|
|
126
|
+
// A human at a terminal: the fork's own art (it declares none) would
|
|
127
|
+
// print here, so the assertions below see the widest page.
|
|
128
|
+
{ llm: false, format: "plain", verbose: false },
|
|
126
129
|
);
|
|
127
130
|
|
|
128
131
|
expect(help).not.toMatch(THIS_DISTRIBUTION);
|
|
@@ -214,15 +217,25 @@ describe("identity projection — a fork changes values, not code (PROTECTED)",
|
|
|
214
217
|
// `$XDG_DATA_HOME/pragma/skills` and one `<cwd>/.pragma/skills`, so a fork
|
|
215
218
|
// read the other's skills and could not install its own without collision.
|
|
216
219
|
process.env.XDG_DATA_HOME = join(tmpdir(), "identity-data");
|
|
217
|
-
const { skillRoots, installedSkillsDir } = await import(
|
|
220
|
+
const { skillRoots, installedSkillsDir, bundledSkillsDir } = await import(
|
|
218
221
|
"./capabilities/skill/discover.js"
|
|
219
222
|
);
|
|
220
223
|
expect(installedSkillsDir().endsWith(join("recipes", "skills"))).toBe(true);
|
|
221
|
-
|
|
224
|
+
// The two roots that live on SHARED ground — the user's data home and the
|
|
225
|
+
// user's repository — are the ones a fork must namespace.
|
|
226
|
+
const namespaced = [
|
|
222
227
|
join("/work", ".recipes", "skills"),
|
|
223
228
|
installedSkillsDir(),
|
|
224
|
-
]
|
|
225
|
-
|
|
229
|
+
];
|
|
230
|
+
// The bundled snapshot is the third, and it is deliberately NOT namespaced
|
|
231
|
+
// by the bin name: it is the distribution's OWN package directory, which is
|
|
232
|
+
// already private to the install. A fork is a different package and ships
|
|
233
|
+
// its own `bundled-skills/`, so there is no shared ground to collide on —
|
|
234
|
+
// and it is exempt from the check below for exactly that reason, since the
|
|
235
|
+
// path this repository's package sits at necessarily says "pragma".
|
|
236
|
+
expect(bundledSkillsDir()).toBeDefined();
|
|
237
|
+
expect(skillRoots("/work")).toEqual([...namespaced, bundledSkillsDir()]);
|
|
238
|
+
for (const root of namespaced) {
|
|
226
239
|
expect(root).not.toMatch(THIS_DISTRIBUTION);
|
|
227
240
|
}
|
|
228
241
|
});
|
|
@@ -259,7 +272,7 @@ describe("identity projection — a fork changes values, not code (PROTECTED)",
|
|
|
259
272
|
args: ["mcp", "serve"],
|
|
260
273
|
cwd: "/work",
|
|
261
274
|
});
|
|
262
|
-
// The global
|
|
275
|
+
// The global scope omits `cwd` (a per-user server is not project-pinned)
|
|
263
276
|
// but still runs the fork's own binary.
|
|
264
277
|
expect(pragmaMcpEntry("/work", "global")).toEqual({
|
|
265
278
|
command: "recipes",
|
|
@@ -2,6 +2,10 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
bundledSkillsDir,
|
|
7
|
+
discoverSkillsFrom,
|
|
8
|
+
} from "../../capabilities/skill/discover.js";
|
|
5
9
|
import { readConfig } from "../config/readConfig.js";
|
|
6
10
|
import { embeddedManifest } from "../runtime/graphpack/embedded.js";
|
|
7
11
|
import { activePackPath, packDir } from "../runtime/paths.js";
|
|
@@ -121,10 +125,19 @@ describe("indexCompletionEnv — multi-source names(ref)", () => {
|
|
|
121
125
|
join(skillDir, "SKILL.md"),
|
|
122
126
|
"---\nname: docx\ndescription: Word docs.\n---\n",
|
|
123
127
|
);
|
|
128
|
+
// The BUNDLED snapshot ships inside the package, so it is a discovery root
|
|
129
|
+
// on every machine — including this one. The project root's skill is what
|
|
130
|
+
// this cell is about; the expectation is derived from the shipped artifact
|
|
131
|
+
// rather than pinned to a list, so a release that changes which skills the
|
|
132
|
+
// packs provide does not rewrite this test.
|
|
133
|
+
const bundled = discoverSkillsFrom([bundledSkillsDir() as string]).map(
|
|
134
|
+
(skill) => skill.name,
|
|
135
|
+
);
|
|
136
|
+
const expected = [...new Set(["docx", ...bundled])].sort();
|
|
124
137
|
const env = indexCompletionEnv(cwd);
|
|
125
|
-
expect(await env.names({ from: "skills" })).toEqual(
|
|
138
|
+
expect(await env.names({ from: "skills" })).toEqual(expected);
|
|
126
139
|
// A second read returns the same list (one filesystem walk per env).
|
|
127
|
-
expect(await env.names({ from: "skills" })).toEqual(
|
|
140
|
+
expect(await env.names({ from: "skills" })).toEqual(expected);
|
|
128
141
|
});
|
|
129
142
|
|
|
130
143
|
it("a type nothing matches yields [] (prefixes still list the default map)", async () => {
|
|
@@ -331,15 +331,22 @@ describe("storeless guarantee (PROTECTED)", () => {
|
|
|
331
331
|
runComplete(["tier", "lookup", "ap"], capabilities, env),
|
|
332
332
|
).resolves.toContain("ds:apps");
|
|
333
333
|
|
|
334
|
-
// skills
|
|
335
|
-
//
|
|
336
|
-
// is
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
334
|
+
// skills: the BUNDLED snapshot ships inside the package, so even this
|
|
335
|
+
// fresh cwd with an empty XDG data home completes REAL skill names — and
|
|
336
|
+
// does it storelessly, which is the whole point of the cell. (It used to
|
|
337
|
+
// assert `[]` here, which was true only because a fresh machine had no
|
|
338
|
+
// skills at all; that is the defect the snapshot fixes.) The names are not
|
|
339
|
+
// spelled out: an empty prefix asks for all of them.
|
|
340
|
+
await expect(
|
|
341
|
+
runComplete(["skill", "lookup", ""], capabilities, env),
|
|
342
|
+
).resolves.not.toHaveLength(0);
|
|
343
|
+
|
|
344
|
+
// prompt labels: this graph carries no prompt entities and no shipped skill
|
|
345
|
+
// declares `prompt: true`, so the honest storeless answer is still an empty
|
|
346
|
+
// list rather than a store boot.
|
|
347
|
+
await expect(
|
|
348
|
+
runComplete(["prompt", "lookup", "bu"], capabilities, env),
|
|
349
|
+
).resolves.toEqual([]);
|
|
343
350
|
|
|
344
351
|
expect(vi.mocked(createStore)).not.toHaveBeenCalled();
|
|
345
352
|
});
|
|
@@ -44,7 +44,12 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
|
|
|
44
44
|
expect(defaults.colophon?.summary?.length).toBeLessThan(400);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Five packs, and ONE carries a subdirectory. That is load-bearing, not
|
|
48
|
+
// decoration: `@canonical/token-ontology` lives inside a monorepo, and a
|
|
49
|
+
// git source names a repository — without `:packages/token-ontology` the
|
|
50
|
+
// clone would succeed against a root that has no `definitions/` or `data/`
|
|
51
|
+
// and the pack would contribute nothing, silently.
|
|
52
|
+
it("ships the five canonical default packs (all git+https)", () => {
|
|
48
53
|
expect(
|
|
49
54
|
defaults.packs?.map((pack) =>
|
|
50
55
|
typeof pack === "string"
|
|
@@ -60,9 +65,15 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
|
|
|
60
65
|
name: "@canonical/anatomy-dsl",
|
|
61
66
|
source: "git+https://github.com/canonical/anatomy-dsl.git#main",
|
|
62
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: "@canonical/token-ontology",
|
|
70
|
+
source:
|
|
71
|
+
"git+https://github.com/canonical/design-tokens.git#main:packages/token-ontology",
|
|
72
|
+
},
|
|
63
73
|
{
|
|
64
74
|
name: "@canonical/code-standards",
|
|
65
|
-
source:
|
|
75
|
+
source:
|
|
76
|
+
"git+https://github.com/canonical/web-code-standards.git#v0.1.5",
|
|
66
77
|
},
|
|
67
78
|
{
|
|
68
79
|
name: "@canonical/ds-implementations",
|
|
@@ -78,7 +89,7 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
|
|
|
78
89
|
const storyCounts = defaults.packs?.map((pack) =>
|
|
79
90
|
typeof pack === "string" ? 0 : (pack.stories?.length ?? 0),
|
|
80
91
|
);
|
|
81
|
-
expect(storyCounts).toEqual([5, 0, 1, 1]);
|
|
92
|
+
expect(storyCounts).toEqual([5, 0, 0, 1, 1]);
|
|
82
93
|
});
|
|
83
94
|
|
|
84
95
|
it("declares no removed field — the validator would refuse to load one", () => {
|
|
@@ -97,6 +108,11 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
|
|
|
97
108
|
// can expand — `cs:` is here for that reader.
|
|
98
109
|
expect(defaults.prefixes).toEqual({
|
|
99
110
|
ds: "https://ds.canonical.com/",
|
|
111
|
+
dt: "https://dt.canonical.com/",
|
|
112
|
+
// Nested under `dt:`, and bound separately because `compactUri` takes the
|
|
113
|
+
// longest match — with only the parent bound, every term of this
|
|
114
|
+
// vocabulary would render as `dt:w3c-tokens/…`.
|
|
115
|
+
"w3c-tokens": "https://dt.canonical.com/w3c-tokens/",
|
|
100
116
|
cs: "http://pragma.canonical.com/codestandards#",
|
|
101
117
|
});
|
|
102
118
|
});
|
|
@@ -59,7 +59,7 @@ describe("readConfig — layering + provenance", () => {
|
|
|
59
59
|
expect(config.tier).toBe("core");
|
|
60
60
|
expect(config.channel).toBe("experimental");
|
|
61
61
|
expect(config.detail).toBe("detailed");
|
|
62
|
-
expect(config.packs).toHaveLength(
|
|
62
|
+
expect(config.packs).toHaveLength(5); // from defaults
|
|
63
63
|
|
|
64
64
|
expect(origins).toMatchObject({
|
|
65
65
|
tier: "project",
|
package/src/kernel/index.ts
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
* is high: it must be a declaration the kernel makes about itself rather than
|
|
11
11
|
* a step in any one pipeline.
|
|
12
12
|
*
|
|
13
|
-
* Today that is the declared vocabulary
|
|
14
|
-
*
|
|
13
|
+
* Today that is the declared vocabulary and the interactivity vocabulary. The
|
|
14
|
+
* latter (`interactivity.ts`) names the questions about attended streams — is
|
|
15
|
+
* stdout captured, may we prompt, may we color (that third one lives with the
|
|
16
|
+
* styler, for the dependency reason its docblock records) — which render,
|
|
17
|
+
* dispatch and flag parsing each used to answer with their own inline probe.
|
|
18
|
+
* They are different questions over different streams, and the names are what
|
|
19
|
+
* keeps them from being flattened into one flag.
|
|
20
|
+
*
|
|
21
|
+
* The declared vocabulary is the distribution's mapping from the roles the
|
|
22
|
+
* CLI knows (a prompt, a resource) onto the RDF terms that actually
|
|
15
23
|
* carry them in the active graph. It is parsed once at module load and read
|
|
16
24
|
* from the prompt capability, the MCP prompt source, and the pack index
|
|
17
25
|
* builder, none of which owns it and any of which would otherwise hardcode
|
|
@@ -22,5 +30,6 @@
|
|
|
22
30
|
* exporting it here would advertise a runtime API the gate does not have.
|
|
23
31
|
*/
|
|
24
32
|
|
|
33
|
+
export { canPrompt, stdoutIsCaptured } from "./interactivity.js";
|
|
25
34
|
export type { DeclaredVocabulary } from "./vocabulary.js";
|
|
26
35
|
export { VOCABULARY } from "./vocabulary.js";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { canPrompt, stdoutIsCaptured } from "./interactivity.js";
|
|
4
|
+
import { canColor } from "./render/style.js";
|
|
5
|
+
|
|
6
|
+
/** Force a stream's `isTTY`, as only a test may. */
|
|
7
|
+
const setTTY = (
|
|
8
|
+
stream: NodeJS.ReadStream | NodeJS.WriteStream,
|
|
9
|
+
value: boolean | undefined,
|
|
10
|
+
): void => {
|
|
11
|
+
(stream as { isTTY?: boolean }).isTTY = value;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const savedIn = process.stdin.isTTY;
|
|
15
|
+
const savedOut = process.stdout.isTTY;
|
|
16
|
+
const savedErr = process.stderr.isTTY;
|
|
17
|
+
const savedLevel = chalk.level;
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
setTTY(process.stdin, savedIn);
|
|
20
|
+
setTTY(process.stdout, savedOut);
|
|
21
|
+
setTTY(process.stderr, savedErr);
|
|
22
|
+
chalk.level = savedLevel;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("the interactivity vocabulary asks THREE questions, not one", () => {
|
|
26
|
+
it("a captured stdout with an attended terminal is still promptable", () => {
|
|
27
|
+
// The case that makes flattening these wrong: an agent runs a mutating
|
|
28
|
+
// verb with `| cat` while a terminal is still attached. Answering the
|
|
29
|
+
// prompting question with stdout's probe would refuse a human; answering
|
|
30
|
+
// the output-shape question with stdin's would hand an agent a page of
|
|
31
|
+
// decoration it cannot use.
|
|
32
|
+
setTTY(process.stdout, false);
|
|
33
|
+
setTTY(process.stdin, true);
|
|
34
|
+
setTTY(process.stderr, true);
|
|
35
|
+
expect(stdoutIsCaptured()).toBe(true);
|
|
36
|
+
expect(canPrompt()).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("a redirected stderr is NOT promptable, however attended stdout is", () => {
|
|
40
|
+
// `<verb> 2>/dev/null`: the step sequence renders to stderr, so gating on
|
|
41
|
+
// stdout would mount an invisible render that then blocks on stdin.
|
|
42
|
+
setTTY(process.stdout, true);
|
|
43
|
+
setTTY(process.stdin, true);
|
|
44
|
+
setTTY(process.stderr, false);
|
|
45
|
+
expect(stdoutIsCaptured()).toBe(false);
|
|
46
|
+
expect(canPrompt()).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("stdin alone does not make a run promptable", () => {
|
|
50
|
+
setTTY(process.stdin, false);
|
|
51
|
+
setTTY(process.stderr, true);
|
|
52
|
+
expect(canPrompt()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("colour needs the terminal AND a usable chalk level", () => {
|
|
56
|
+
setTTY(process.stdout, true);
|
|
57
|
+
chalk.level = 1;
|
|
58
|
+
expect(canColor()).toBe(true);
|
|
59
|
+
// Off in CI-shaped environments where `supports-color` fires off a TTY…
|
|
60
|
+
setTTY(process.stdout, false);
|
|
61
|
+
expect(canColor()).toBe(false);
|
|
62
|
+
// …and off whenever the level is zeroed (what `NO_COLOR` does at load).
|
|
63
|
+
setTTY(process.stdout, true);
|
|
64
|
+
chalk.level = 0;
|
|
65
|
+
expect(canColor()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("treats an absent isTTY as captured / unpromptable, never as attended", () => {
|
|
69
|
+
setTTY(process.stdout, undefined);
|
|
70
|
+
setTTY(process.stdin, undefined);
|
|
71
|
+
setTTY(process.stderr, undefined);
|
|
72
|
+
expect(stdoutIsCaptured()).toBe(true);
|
|
73
|
+
expect(canPrompt()).toBe(false);
|
|
74
|
+
expect(canColor()).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interactivity vocabulary — three named questions over three streams.
|
|
3
|
+
*
|
|
4
|
+
* Every one of these used to be spelled inline as a bare `process.<stream>
|
|
5
|
+
* .isTTY === true` at the site that needed it, in four places, and the
|
|
6
|
+
* spellings did not agree — because they are NOT the same question. Naming them
|
|
7
|
+
* is the point: the names are what stop the next reader from "simplifying"
|
|
8
|
+
* three probes into one flag.
|
|
9
|
+
*
|
|
10
|
+
* - {@link stdoutIsCaptured} reads **stdout**. It governs OUTPUT SHAPE: whether
|
|
11
|
+
* a consumer is piping the data stream somewhere (the shape an agent
|
|
12
|
+
* captures), which is what AUTO-INFERS the condensed `llm` default. Capture
|
|
13
|
+
* is the inference, not the gate: what drops the root-help wordmark is the
|
|
14
|
+
* resulting shape — `llm` or `json` — so an explicit `--format plain` through
|
|
15
|
+
* a pipe is captured and keeps the art, while `--format llm` on an attended
|
|
16
|
+
* terminal is not captured and drops it. Explicit format beats inference, in
|
|
17
|
+
* both directions.
|
|
18
|
+
* - {@link canPrompt} reads **stdin AND stderr** — never stdout. The step
|
|
19
|
+
* sequence renders to stderr and reads stdin, so `<verb> 2>/dev/null` is
|
|
20
|
+
* non-interactive while `<verb> | cat` is not. Gating this one on stdout
|
|
21
|
+
* would mount an invisible render that then blocks on stdin.
|
|
22
|
+
* - `canColor` (`render/style.ts`) reads **stdout** plus chalk's own color
|
|
23
|
+
* level, so piped, redirected and MCP output stays byte-stable even where
|
|
24
|
+
* `supports-color` fires off a TTY. It is the one question of the three that
|
|
25
|
+
* lives with the styler rather than here, because it needs `chalk` and this
|
|
26
|
+
* module is imported by global-flag parsing — which runs before `--version`
|
|
27
|
+
* answers, on a path that must load nothing it does not use. It still asks
|
|
28
|
+
* its stdout half through {@link stdoutIsCaptured}, so the three stay one
|
|
29
|
+
* vocabulary.
|
|
30
|
+
*
|
|
31
|
+
* The distinction is not academic: an agent running a mutating verb with its
|
|
32
|
+
* stdout captured but a terminal still attached is CAPTURED and PROMPTABLE at
|
|
33
|
+
* the same time. Answering either question with the other's probe would either
|
|
34
|
+
* hang a pipeline or refuse a human.
|
|
35
|
+
*
|
|
36
|
+
* These are the only sanctioned readers of `process.*.isTTY` outside a test.
|
|
37
|
+
* Each is a plain function rather than a module-load constant because a test
|
|
38
|
+
* assigns the streams' `isTTY` per case, and a constant would freeze the first
|
|
39
|
+
* value the process ever saw.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether stdout is going somewhere other than an attended terminal — piped,
|
|
44
|
+
* redirected, or read by a parent process.
|
|
45
|
+
*
|
|
46
|
+
* The OUTPUT-SHAPE question, and stdout's alone: it says nothing about whether
|
|
47
|
+
* a human is available to answer a prompt (see {@link canPrompt}).
|
|
48
|
+
*
|
|
49
|
+
* @returns True when stdout is not a TTY.
|
|
50
|
+
* @note Impure — reads `process.stdout.isTTY`.
|
|
51
|
+
*/
|
|
52
|
+
export function stdoutIsCaptured(): boolean {
|
|
53
|
+
return process.stdout.isTTY !== true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether this invocation may ask the user a question: stdin AND stderr are
|
|
58
|
+
* both attended terminals.
|
|
59
|
+
*
|
|
60
|
+
* THE interactivity gate (H3). One exported fact, so the kernel's interaction
|
|
61
|
+
* context and a mounted subtree's own decision can never disagree about what
|
|
62
|
+
* "a TTY" means. Deliberately blind to stdout — see the module docblock.
|
|
63
|
+
*
|
|
64
|
+
* @returns True when a prompt can be both drawn and answered.
|
|
65
|
+
* @note Impure — reads `process.stdin.isTTY` and `process.stderr.isTTY`.
|
|
66
|
+
*/
|
|
67
|
+
export function canPrompt(): boolean {
|
|
68
|
+
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
69
|
+
}
|