@canonical/pragma-cli 0.32.0 → 0.33.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 +72 -0
- package/dist/pragma +0 -0
- package/package.json +14 -13
- package/src/bin.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +79 -0
- package/src/capabilities/block/blockList.verb.ts +18 -5
- package/src/capabilities/block/pack.ts +12 -1
- package/src/capabilities/block/parity.test.ts +9 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
- package/src/capabilities/capabilities/catalog.ts +7 -3
- package/src/capabilities/capabilities/hints.ts +8 -17
- package/src/capabilities/capabilities/types.ts +2 -0
- package/src/capabilities/colophon/colophon.render.test.ts +31 -4
- package/src/capabilities/colophon/colophon.render.ts +13 -4
- package/src/capabilities/colophon/colophon.test.ts +3 -3
- package/src/capabilities/colophon/colophon.verb.ts +4 -1
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/pragmaColophon.ts +4 -4
- package/src/capabilities/colophon/types.ts +1 -1
- package/src/capabilities/config/fields.ts +21 -92
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +5 -5
- package/src/capabilities/config/show.render.test.ts +65 -0
- package/src/capabilities/config/show.render.ts +56 -10
- package/src/capabilities/config/show.verb.ts +5 -5
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +7 -4
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
- package/src/capabilities/create/create.test.ts +32 -3
- package/src/capabilities/create/create.verb.ts +69 -11
- package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
- package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
- package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
- package/src/capabilities/doctor/doctor.render.test.ts +170 -0
- package/src/capabilities/doctor/doctor.render.ts +116 -34
- package/src/capabilities/doctor/doctor.test.ts +31 -1
- package/src/capabilities/doctor/runChecks.ts +28 -4
- package/src/capabilities/doctor/types.ts +15 -0
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +192 -93
- package/src/capabilities/prompt/prompt.render.test.ts +94 -0
- package/src/capabilities/prompt/prompt.render.ts +23 -6
- package/src/capabilities/prompt/prompt.test.ts +61 -0
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +28 -14
- package/src/capabilities/resources/resources.test.ts +99 -0
- package/src/capabilities/scopeTypeSync.test.ts +37 -0
- package/src/capabilities/setup/index.ts +7 -1
- package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
- package/src/capabilities/setup/operations/setupGenerator.ts +157 -61
- package/src/capabilities/setup/operations/setupLsp.ts +56 -12
- package/src/capabilities/setup/operations/setupMcp.ts +216 -39
- package/src/capabilities/setup/operations/setupSkills.ts +7 -1
- package/src/capabilities/setup/setup.render.test.ts +109 -0
- package/src/capabilities/setup/setup.render.ts +61 -14
- package/src/capabilities/setup/setup.test.ts +352 -1
- package/src/capabilities/setup/setup.verb.ts +57 -5
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/sources/collectStatus.ts +19 -4
- package/src/capabilities/sources/runUpdate.ts +58 -1
- package/src/capabilities/sources/sources.test.ts +187 -1
- package/src/capabilities/sources/status.render.test.ts +65 -0
- package/src/capabilities/sources/status.render.ts +50 -23
- package/src/capabilities/surface.test.ts +34 -9
- package/src/capabilities/tier/lookup.render.test.ts +72 -0
- package/src/capabilities/tier/lookup.render.ts +33 -8
- package/src/capabilities/tier/lookup.test.ts +15 -0
- package/src/capabilities/tier/runLookup.ts +28 -0
- package/src/constants.test.ts +1 -1
- package/src/constants.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
- package/src/kernel/completion/bashDrive.test.ts +3 -3
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/entitySource.ts +14 -6
- package/src/kernel/completion/model.test.ts +2 -2
- package/src/kernel/completion/model.ts +0 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/error/PragmaError.test.ts +4 -4
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/packs/compile.ts +1 -1
- package/src/kernel/project/cli/buildProgram.test.ts +51 -7
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/rootHelp.ts +4 -2
- package/src/kernel/project/mcp/instructions.ts +1 -1
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
- package/src/kernel/project/mcp/prompts/provider.ts +36 -6
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/renderers.test.ts +34 -0
- package/src/kernel/render/renderers.ts +25 -7
- package/src/kernel/render/style.test.ts +31 -0
- package/src/kernel/render/style.ts +74 -0
- package/src/kernel/runtime/graphpack/build.ts +28 -0
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
- package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
- package/src/kernel/runtime/graphpack/read.ts +28 -13
- package/src/kernel/runtime/graphpack/types.ts +14 -0
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/refs/resolve.ts +63 -3
- package/src/kernel/runtime/storeReadiness.ts +61 -0
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +491 -0
- package/src/kernel/spec/emitSurface.ts +4 -2
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/testing/PARITY_GAPS.ts +1 -1
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +2 -2
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
- package/src/testing/behavioral/journeys.cli.test.ts +3 -1
- package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -0
- package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
- package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +176 -0
- package/src/testing/behavioral/mcpSurface.test.ts +1 -1
- package/src/testing/behavioral/staleCommands.test.ts +76 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
- package/src/testing/eval/cases/assembly.ts +3 -3
- package/src/testing/eval/cases/effectful.ts +6 -3
- package/src/testing/eval/cases/readNouns.ts +4 -1
- package/src/testing/fixtures/fixtureCapability.ts +156 -0
- package/src/testing/fixtures/graph/defaultPack.ts +178 -0
- package/src/testing/fixtures/graph/owlExport.ts +95 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/golden.ts +1 -1
- package/src/capabilities/config/field.test.ts +0 -188
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* it more than once (the confirm-gate preview + the build), so all real reads
|
|
12
12
|
* belong in detection, never in `generate`.
|
|
13
13
|
*
|
|
14
|
+
* The `--scope` selection is threaded through detection AND step-offering so
|
|
15
|
+
* `listAvailableSteps` only offers steps whose band the scope runs, and the MCP
|
|
16
|
+
* step targets exactly the deduped files for that scope. Per-file narrowing is
|
|
17
|
+
* opt-in (Item 6): the "all" default never springs a per-file question — it
|
|
18
|
+
* configures every deduped file.
|
|
19
|
+
*
|
|
14
20
|
* This module carries NO static import of `@canonical/summon-core` VALUES or of
|
|
15
21
|
* React/Ink — it only `import type`s the generator shape and builds plain object
|
|
16
22
|
* literals over `@canonical/task`. It is itself reached only through the verb's
|
|
@@ -24,18 +30,26 @@ import type {
|
|
|
24
30
|
import { sequence_, type Task, when } from "@canonical/task";
|
|
25
31
|
import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
|
|
26
32
|
import { guardMissingBinary } from "../../shared/assertExecOk.js";
|
|
27
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
LspState,
|
|
35
|
+
ScopeSelection,
|
|
36
|
+
SetupMode,
|
|
37
|
+
SetupResult,
|
|
38
|
+
} from "../types.js";
|
|
28
39
|
import {
|
|
29
40
|
type CompletionsDetection,
|
|
30
41
|
composeCompletions,
|
|
31
42
|
detectCompletions,
|
|
32
43
|
} from "./setupCompletions.js";
|
|
33
|
-
import { composeLsp } from "./setupLsp.js";
|
|
44
|
+
import { composeLsp, detectLsp, type LspDetection } from "./setupLsp.js";
|
|
34
45
|
import {
|
|
35
46
|
composeMcp,
|
|
36
47
|
detectMcp,
|
|
37
48
|
type McpDetection,
|
|
38
49
|
mcpConfigured,
|
|
50
|
+
mcpGroupState,
|
|
51
|
+
mcpTargets,
|
|
52
|
+
selectedGroups,
|
|
39
53
|
} from "./setupMcp.js";
|
|
40
54
|
import {
|
|
41
55
|
composeSkills,
|
|
@@ -61,22 +75,29 @@ export interface SetupPlan {
|
|
|
61
75
|
/** The union of every step's detection, gathered up front for the run-all. */
|
|
62
76
|
interface SetupDetection {
|
|
63
77
|
readonly completions: CompletionsDetection;
|
|
78
|
+
readonly lsp: LspDetection;
|
|
64
79
|
readonly mcp: McpDetection;
|
|
65
80
|
readonly skills: SkillsDetection;
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
/** Read `answers.steps` (the run-all multiselect) as a string array. */
|
|
69
|
-
const
|
|
84
|
+
const readSteps = (answers: Record<string, unknown>): string[] =>
|
|
70
85
|
Array.isArray(answers.steps) ? (answers.steps as string[]) : [];
|
|
71
86
|
|
|
72
|
-
/** Read `answers.
|
|
73
|
-
const
|
|
87
|
+
/** Read `answers.mcpTargets` (selected file paths), falling back to ALL files. */
|
|
88
|
+
const resolveMcpPaths = (
|
|
74
89
|
d: McpDetection,
|
|
75
90
|
answers: Record<string, unknown>,
|
|
76
91
|
): string[] =>
|
|
77
|
-
Array.isArray(answers.
|
|
78
|
-
? (answers.
|
|
79
|
-
: d.
|
|
92
|
+
Array.isArray(answers.mcpTargets)
|
|
93
|
+
? (answers.mcpTargets as string[])
|
|
94
|
+
: d.groups.map((g) => g.path);
|
|
95
|
+
|
|
96
|
+
/** The groups the user chose (or all, under the "all" default). */
|
|
97
|
+
const selectChosenGroups = (
|
|
98
|
+
d: McpDetection,
|
|
99
|
+
answers: Record<string, unknown>,
|
|
100
|
+
) => selectedGroups(d, resolveMcpPaths(d, answers));
|
|
80
101
|
|
|
81
102
|
/**
|
|
82
103
|
* The LSP-install step, guarded at its use site. An absent `bunx` (no Bun on
|
|
@@ -87,18 +108,18 @@ const mcpSelection = (
|
|
|
87
108
|
* exec (no spawn) — and re-runnable (the guard is a `recover`, and `composeLsp`
|
|
88
109
|
* is combinator-built), so it survives `execute`'s double interpretation.
|
|
89
110
|
*/
|
|
90
|
-
const composeGuardedLsp = (rt: PragmaRuntime): Task<void> =>
|
|
111
|
+
const composeGuardedLsp = (rt: PragmaRuntime, state: LspState): Task<void> =>
|
|
91
112
|
guardMissingBinary(
|
|
92
113
|
"bunx",
|
|
93
114
|
{
|
|
94
115
|
message:
|
|
95
116
|
"Install Bun (https://bun.sh) to provide `bunx`, then run this again.",
|
|
96
117
|
},
|
|
97
|
-
composeLsp(rt.cwd),
|
|
118
|
+
composeLsp(rt.cwd, state),
|
|
98
119
|
);
|
|
99
120
|
|
|
100
121
|
/** Build a generator's `meta` (no stamping — the version is just header text). */
|
|
101
|
-
const
|
|
122
|
+
const buildMeta = (
|
|
102
123
|
rt: PragmaRuntime,
|
|
103
124
|
title: string,
|
|
104
125
|
): GeneratorDefinition["meta"] => ({
|
|
@@ -108,20 +129,50 @@ const metaFor = (
|
|
|
108
129
|
version: rt.version,
|
|
109
130
|
});
|
|
110
131
|
|
|
111
|
-
/** The
|
|
112
|
-
const
|
|
132
|
+
/** The opt-in "customize which files" gate (Item 6) — defaults to false. */
|
|
133
|
+
const buildCustomizePrompt = (
|
|
134
|
+
when?: PromptDefinition["when"],
|
|
135
|
+
): PromptDefinition => ({
|
|
136
|
+
name: "customize",
|
|
137
|
+
type: "confirm",
|
|
138
|
+
message: "Customize which files pragma configures?",
|
|
139
|
+
default: false,
|
|
140
|
+
when,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/** The suffix a group's prior state adds to its multiselect label. */
|
|
144
|
+
const mcpStateSuffix = (d: McpDetection, path: string): string => {
|
|
145
|
+
switch (mcpGroupState(d, path)) {
|
|
146
|
+
case "configured":
|
|
147
|
+
return " — already configured";
|
|
148
|
+
case "drifted":
|
|
149
|
+
return " — needs update";
|
|
150
|
+
default:
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The per-file MCP multiselect — one row per deduped {@link TargetGroup} file.
|
|
157
|
+
* A row's label carries its prior state, and an already-`configured` file is
|
|
158
|
+
* DEFAULT-DESELECTED (so a re-run does not re-offer to rewrite what is already
|
|
159
|
+
* current); `absent`/`drifted` files stay selected by default.
|
|
160
|
+
*/
|
|
161
|
+
const buildMcpTargetsPrompt = (
|
|
113
162
|
d: McpDetection,
|
|
114
163
|
when?: PromptDefinition["when"],
|
|
115
164
|
): PromptDefinition => ({
|
|
116
|
-
name: "
|
|
165
|
+
name: "mcpTargets",
|
|
117
166
|
type: "multiselect",
|
|
118
|
-
message: "Configure MCP for which
|
|
167
|
+
message: "Configure MCP for which files?",
|
|
119
168
|
when,
|
|
120
|
-
choices: d.
|
|
121
|
-
label:
|
|
122
|
-
value:
|
|
169
|
+
choices: d.groups.map((g) => ({
|
|
170
|
+
label: `${g.path} — ${g.harnessNames.join(", ")} [${g.scope}]${mcpStateSuffix(d, g.path)}`,
|
|
171
|
+
value: g.path,
|
|
123
172
|
})),
|
|
124
|
-
default: d.
|
|
173
|
+
default: d.groups
|
|
174
|
+
.filter((g) => mcpGroupState(d, g.path) !== "configured")
|
|
175
|
+
.map((g) => g.path),
|
|
125
176
|
});
|
|
126
177
|
|
|
127
178
|
// =============================================================================
|
|
@@ -129,39 +180,55 @@ const mcpHarnessesPrompt = (
|
|
|
129
180
|
// =============================================================================
|
|
130
181
|
|
|
131
182
|
/** Gather EVERY step's detection up front (real reads), for the run-all. */
|
|
132
|
-
async function gatherDetection(
|
|
133
|
-
|
|
183
|
+
async function gatherDetection(
|
|
184
|
+
rt: PragmaRuntime,
|
|
185
|
+
scope: ScopeSelection,
|
|
186
|
+
): Promise<SetupDetection> {
|
|
187
|
+
const [completions, lsp, mcp, skills] = await Promise.all([
|
|
134
188
|
detectCompletions(rt.cwd),
|
|
135
|
-
|
|
189
|
+
detectLsp(rt.cwd),
|
|
190
|
+
detectMcp(rt, scope),
|
|
136
191
|
detectSkills(rt),
|
|
137
192
|
]);
|
|
138
|
-
return { completions, mcp, skills };
|
|
193
|
+
return { completions, lsp, mcp, skills };
|
|
139
194
|
}
|
|
140
195
|
|
|
141
196
|
/**
|
|
142
|
-
* The steps worth OFFERING, given detection
|
|
143
|
-
* the run-all degrades gracefully instead of
|
|
144
|
-
* EMPTY_RESULTS
|
|
145
|
-
*
|
|
197
|
+
* The steps worth OFFERING, given detection AND the scope selection. An
|
|
198
|
+
* undetectable step is omitted so the run-all degrades gracefully instead of
|
|
199
|
+
* throwing a mid-wizard EMPTY_RESULTS; a step whose band the scope does not run
|
|
200
|
+
* is omitted too (completions/lsp are global-band, skills are project-band, MCP
|
|
201
|
+
* spans both via its resolved groups). Threading `scope` here is what makes
|
|
202
|
+
* `setup --local` skip completions+lsp and `setup --global` skip skills — the
|
|
203
|
+
* MCP groups are already scoped by {@link detectMcp}.
|
|
204
|
+
*
|
|
205
|
+
* @param detected - Every step's up-front detection.
|
|
206
|
+
* @param scope - The resolved `--scope` selection (project/global/both).
|
|
207
|
+
* @returns The offerable steps, in composition/display order.
|
|
146
208
|
*/
|
|
147
|
-
function
|
|
209
|
+
function listAvailableSteps(
|
|
148
210
|
detected: SetupDetection,
|
|
211
|
+
scope: ScopeSelection,
|
|
149
212
|
): { label: string; value: StepId }[] {
|
|
213
|
+
const hasProject = scope !== "global";
|
|
214
|
+
const hasGlobal = scope !== "project";
|
|
150
215
|
const choices: { label: string; value: StepId }[] = [];
|
|
151
|
-
if (detected.completions.shell) {
|
|
216
|
+
if (detected.completions.shell && hasGlobal) {
|
|
152
217
|
choices.push({
|
|
153
218
|
label: `Shell completions (${detected.completions.shell})`,
|
|
154
219
|
value: "completions",
|
|
155
220
|
});
|
|
156
221
|
}
|
|
157
|
-
|
|
158
|
-
|
|
222
|
+
if (hasGlobal) {
|
|
223
|
+
choices.push({ label: "Terrazzo LSP extension", value: "lsp" });
|
|
224
|
+
}
|
|
225
|
+
if (detected.mcp.groups.length > 0) {
|
|
159
226
|
choices.push({
|
|
160
|
-
label: `MCP config (${detected.mcp.
|
|
227
|
+
label: `MCP config (${detected.mcp.groups.length} file(s))`,
|
|
161
228
|
value: "mcp",
|
|
162
229
|
});
|
|
163
230
|
}
|
|
164
|
-
if (detected.skills.available) {
|
|
231
|
+
if (detected.skills.available && hasProject) {
|
|
165
232
|
choices.push({
|
|
166
233
|
label: `Link skills (${detected.skills.skillCount})`,
|
|
167
234
|
value: "skills",
|
|
@@ -174,9 +241,18 @@ function availableSteps(
|
|
|
174
241
|
// Plan builders
|
|
175
242
|
// =============================================================================
|
|
176
243
|
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
244
|
+
/**
|
|
245
|
+
* The run-all self-verb: a step multiselect + per-step composition. The resolved
|
|
246
|
+
* `scope` narrows BOTH the offered steps (via {@link listAvailableSteps}) and the
|
|
247
|
+
* MCP target groups (already scoped by {@link detectMcp}), so `--local` omits the
|
|
248
|
+
* global-band completions+lsp and `--global` omits the project-band skills.
|
|
249
|
+
*/
|
|
250
|
+
function buildRunAllPlan(
|
|
251
|
+
rt: PragmaRuntime,
|
|
252
|
+
detected: SetupDetection,
|
|
253
|
+
scope: ScopeSelection,
|
|
254
|
+
): SetupPlan {
|
|
255
|
+
const steps = listAvailableSteps(detected, scope);
|
|
180
256
|
const prompts: PromptDefinition[] = [
|
|
181
257
|
{
|
|
182
258
|
name: "steps",
|
|
@@ -186,11 +262,16 @@ function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
|
|
|
186
262
|
default: steps.map((c) => c.value), // --yes / non-TTY ⇒ every step
|
|
187
263
|
},
|
|
188
264
|
];
|
|
189
|
-
|
|
265
|
+
// Item 6: per-file narrowing is opt-in. The "customize?" gate (default false)
|
|
266
|
+
// only surfaces when MCP is chosen and there is more than one file; the
|
|
267
|
+
// per-file multiselect only surfaces after an explicit yes — so the "all"
|
|
268
|
+
// default configures every deduped file without an extra question.
|
|
269
|
+
if (detected.mcp.groups.length > 0) {
|
|
190
270
|
prompts.push(
|
|
191
|
-
|
|
271
|
+
buildCustomizePrompt((a) => readSteps(a).includes("mcp")),
|
|
272
|
+
buildMcpTargetsPrompt(
|
|
192
273
|
detected.mcp,
|
|
193
|
-
(a) =>
|
|
274
|
+
(a) => a.customize === true && readSteps(a).includes("mcp"),
|
|
194
275
|
),
|
|
195
276
|
);
|
|
196
277
|
}
|
|
@@ -201,19 +282,19 @@ function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
|
|
|
201
282
|
// reads already happened up front). Composing an unchosen step is harmless:
|
|
202
283
|
// `when(false, …)` discards the (side-effect-free) task it was handed.
|
|
203
284
|
const generator: GeneratorDefinition = {
|
|
204
|
-
meta:
|
|
285
|
+
meta: buildMeta(rt, "pragma setup"),
|
|
205
286
|
prompts,
|
|
206
287
|
generate: (answers) => {
|
|
207
|
-
const chosen =
|
|
288
|
+
const chosen = readSteps(answers);
|
|
208
289
|
return sequence_([
|
|
209
290
|
when(
|
|
210
291
|
chosen.includes("completions"),
|
|
211
292
|
composeCompletions(detected.completions),
|
|
212
293
|
),
|
|
213
|
-
when(chosen.includes("lsp"), composeGuardedLsp(rt)),
|
|
294
|
+
when(chosen.includes("lsp"), composeGuardedLsp(rt, detected.lsp.state)),
|
|
214
295
|
when(
|
|
215
296
|
chosen.includes("mcp"),
|
|
216
|
-
composeMcp(detected.mcp,
|
|
297
|
+
composeMcp(detected.mcp, selectChosenGroups(detected.mcp, answers)),
|
|
217
298
|
),
|
|
218
299
|
when(chosen.includes("skills"), composeSkills(detected.skills)),
|
|
219
300
|
]);
|
|
@@ -222,18 +303,18 @@ function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
|
|
|
222
303
|
|
|
223
304
|
return {
|
|
224
305
|
generator,
|
|
225
|
-
toResult: (answers) => ({ kind: "all", steps:
|
|
306
|
+
toResult: (answers) => ({ kind: "all", steps: readSteps(answers) }),
|
|
226
307
|
};
|
|
227
308
|
}
|
|
228
309
|
|
|
229
310
|
/** A single-step generator (no run-all multiselect) for one sub-verb. */
|
|
230
|
-
function
|
|
311
|
+
function buildSingleStep(
|
|
231
312
|
rt: PragmaRuntime,
|
|
232
313
|
title: string,
|
|
233
314
|
prompts: PromptDefinition[],
|
|
234
315
|
generate: (answers: Record<string, unknown>) => Task<void>,
|
|
235
316
|
): GeneratorDefinition {
|
|
236
|
-
return { meta:
|
|
317
|
+
return { meta: buildMeta(rt, title), prompts, generate };
|
|
237
318
|
}
|
|
238
319
|
|
|
239
320
|
/**
|
|
@@ -244,6 +325,7 @@ function singleStep(
|
|
|
244
325
|
*
|
|
245
326
|
* @param rt - The per-invocation runtime.
|
|
246
327
|
* @param mode - The entry point (`all` or one sub-verb).
|
|
328
|
+
* @param scope - The resolved `--scope` selection (project/global/both).
|
|
247
329
|
* @returns The generator + result mapper.
|
|
248
330
|
* @throws PragmaError EMPTY_RESULTS for a direct `setup skills` with no skills.
|
|
249
331
|
* @note Impure — performs each step's real detection.
|
|
@@ -251,15 +333,16 @@ function singleStep(
|
|
|
251
333
|
export async function buildSetupPlan(
|
|
252
334
|
rt: PragmaRuntime,
|
|
253
335
|
mode: SetupMode,
|
|
336
|
+
scope: ScopeSelection,
|
|
254
337
|
): Promise<SetupPlan> {
|
|
255
338
|
switch (mode) {
|
|
256
339
|
case "all":
|
|
257
|
-
return
|
|
340
|
+
return buildRunAllPlan(rt, await gatherDetection(rt, scope), scope);
|
|
258
341
|
|
|
259
342
|
case "completions": {
|
|
260
343
|
const d = await detectCompletions(rt.cwd);
|
|
261
344
|
return {
|
|
262
|
-
generator:
|
|
345
|
+
generator: buildSingleStep(rt, "pragma setup completions", [], () =>
|
|
263
346
|
composeCompletions(d),
|
|
264
347
|
),
|
|
265
348
|
toResult: () => ({
|
|
@@ -267,29 +350,42 @@ export async function buildSetupPlan(
|
|
|
267
350
|
shell: d.shell,
|
|
268
351
|
path: d.path,
|
|
269
352
|
installed: d.shell !== null,
|
|
353
|
+
state: d.state,
|
|
270
354
|
}),
|
|
271
355
|
};
|
|
272
356
|
}
|
|
273
357
|
|
|
274
|
-
case "lsp":
|
|
358
|
+
case "lsp": {
|
|
359
|
+
const d = await detectLsp(rt.cwd);
|
|
275
360
|
return {
|
|
276
|
-
generator:
|
|
277
|
-
composeGuardedLsp(rt),
|
|
361
|
+
generator: buildSingleStep(rt, "pragma setup lsp", [], () =>
|
|
362
|
+
composeGuardedLsp(rt, d.state),
|
|
278
363
|
),
|
|
279
|
-
toResult: () => ({ kind: "lsp" }),
|
|
364
|
+
toResult: () => ({ kind: "lsp", state: d.state }),
|
|
280
365
|
};
|
|
366
|
+
}
|
|
281
367
|
|
|
282
368
|
case "mcp": {
|
|
283
|
-
const d = await detectMcp(rt);
|
|
284
|
-
const prompts
|
|
369
|
+
const d = await detectMcp(rt, scope);
|
|
370
|
+
const prompts: PromptDefinition[] =
|
|
371
|
+
d.groups.length > 0
|
|
372
|
+
? [
|
|
373
|
+
buildCustomizePrompt(),
|
|
374
|
+
buildMcpTargetsPrompt(d, (a) => a.customize === true),
|
|
375
|
+
]
|
|
376
|
+
: [];
|
|
285
377
|
return {
|
|
286
|
-
generator:
|
|
287
|
-
composeMcp(d,
|
|
378
|
+
generator: buildSingleStep(rt, "pragma setup mcp", prompts, (answers) =>
|
|
379
|
+
composeMcp(d, selectChosenGroups(d, answers)),
|
|
288
380
|
),
|
|
289
|
-
toResult: (answers) =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
381
|
+
toResult: (answers) => {
|
|
382
|
+
const sel = selectChosenGroups(d, answers);
|
|
383
|
+
return {
|
|
384
|
+
kind: "mcp",
|
|
385
|
+
configured: mcpConfigured(sel),
|
|
386
|
+
targets: mcpTargets(d, sel),
|
|
387
|
+
};
|
|
388
|
+
},
|
|
293
389
|
};
|
|
294
390
|
}
|
|
295
391
|
|
|
@@ -297,7 +393,7 @@ export async function buildSetupPlan(
|
|
|
297
393
|
const d = await detectSkills(rt);
|
|
298
394
|
if (!d.available) throw skillsEmptyError();
|
|
299
395
|
return {
|
|
300
|
-
generator:
|
|
396
|
+
generator: buildSingleStep(rt, "pragma setup skills", [], () =>
|
|
301
397
|
composeSkills(d),
|
|
302
398
|
),
|
|
303
399
|
toResult: () => ({ kind: "skills", result: toSkillsResult(d) }),
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `setup lsp` — ensure the Terrazzo LSP VS Code extension is installed.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `detectLsp` probes for the extension FOR REAL up front — it runs
|
|
5
|
+
* `code --list-extensions` (via the `exec` seam, over `runTask`) and matches the
|
|
6
|
+
* `terrazzo` extension id — so a re-run of an already-installed extension is a
|
|
7
|
+
* true no-op and the recap says so, rather than always shelling out. When the
|
|
8
|
+
* `code` CLI is absent from PATH the state is `unknown` (we cannot enumerate,
|
|
9
|
+
* so the installer still runs). `composeLsp` holds the sole mutation, mocked
|
|
10
|
+
* under `--dry-run` and skipped when detection already found the extension.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import {
|
|
@@ -17,18 +20,51 @@ import {
|
|
|
17
20
|
type Task,
|
|
18
21
|
} from "@canonical/task";
|
|
19
22
|
import { assertExecOk } from "../../shared/assertExecOk.js";
|
|
23
|
+
import type { LspState } from "../types.js";
|
|
20
24
|
|
|
21
|
-
/**
|
|
25
|
+
/** The VS Code CLI queried for the installed-extensions list. */
|
|
26
|
+
const VSCODE_CLI = "code";
|
|
27
|
+
|
|
28
|
+
/** The substring an installed Terrazzo extension id contains (publisher-agnostic). */
|
|
29
|
+
const TERRAZZO_EXTENSION_MATCH = "terrazzo";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The detected LSP state: whether the Terrazzo extension is already `installed`,
|
|
33
|
+
* `absent`, or `unknown` (the `code` CLI is not on PATH, so we cannot enumerate).
|
|
34
|
+
*/
|
|
22
35
|
export interface LspDetection {
|
|
23
36
|
readonly available: true;
|
|
37
|
+
readonly state: LspState;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
41
|
+
* Probe VS Code for the Terrazzo extension. Runs `code --list-extensions` and
|
|
42
|
+
* matches the `terrazzo` id; an absent `code` CLI (spawn ENOENT) or a nonzero
|
|
43
|
+
* exit yields `unknown` (we fall through to running the installer).
|
|
44
|
+
*
|
|
45
|
+
* @param cwd - The directory the probe runs in.
|
|
46
|
+
* @returns The detected {@link LspState}.
|
|
47
|
+
* @note Impure — spawns `code --list-extensions`.
|
|
29
48
|
*/
|
|
30
|
-
export function detectLsp(): LspDetection {
|
|
31
|
-
|
|
49
|
+
export async function detectLsp(cwd: string): Promise<LspDetection> {
|
|
50
|
+
const { runTask } = await import("@canonical/task/node");
|
|
51
|
+
let state: LspState = "unknown";
|
|
52
|
+
try {
|
|
53
|
+
const result = (await runTask(
|
|
54
|
+
exec(VSCODE_CLI, ["--list-extensions"], cwd),
|
|
55
|
+
)) as ExecResult;
|
|
56
|
+
if (result.exitCode === 0) {
|
|
57
|
+
const installed = result.stdout
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.includes(TERRAZZO_EXTENSION_MATCH);
|
|
60
|
+
state = installed ? "installed" : "absent";
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// `code` absent from PATH (ENOENT) or any spawn failure → we cannot tell;
|
|
64
|
+
// leave `unknown` so the installer still runs.
|
|
65
|
+
state = "unknown";
|
|
66
|
+
}
|
|
67
|
+
return { available: true, state };
|
|
32
68
|
}
|
|
33
69
|
|
|
34
70
|
/**
|
|
@@ -37,12 +73,20 @@ export function detectLsp(): LspDetection {
|
|
|
37
73
|
* Built from re-runnable combinators (NOT a single-use `gen`) because `execute`
|
|
38
74
|
* interprets the task twice (preview + perform). Under the dry-run preview the
|
|
39
75
|
* `exec` is MOCKED to `exitCode 0`, so `assertExecOk` passes there; a real run's
|
|
40
|
-
* nonzero exit still fails loudly.
|
|
76
|
+
* nonzero exit still fails loudly. When detection already found the extension
|
|
77
|
+
* `installed`, the install is SKIPPED — a re-run is a true no-op.
|
|
41
78
|
*
|
|
42
79
|
* @param cwd - The directory to run the installer in.
|
|
43
|
-
* @
|
|
80
|
+
* @param state - The prior state from {@link detectLsp} (defaults to `unknown`).
|
|
81
|
+
* @returns A Task that execs the installer (or reports already-installed).
|
|
44
82
|
*/
|
|
45
|
-
export function composeLsp(
|
|
83
|
+
export function composeLsp(
|
|
84
|
+
cwd: string,
|
|
85
|
+
state: LspState = "unknown",
|
|
86
|
+
): Task<void> {
|
|
87
|
+
if (state === "installed") {
|
|
88
|
+
return info("Terrazzo LSP VS Code extension already installed — skipped.");
|
|
89
|
+
}
|
|
46
90
|
return sequence_([
|
|
47
91
|
info("Ensuring the Terrazzo LSP VS Code extension is installed..."),
|
|
48
92
|
// The interpreter RESOLVES on a nonzero exit — a failed installer must fail
|