@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
|
@@ -19,16 +19,16 @@ import {
|
|
|
19
19
|
} from "./plan.js";
|
|
20
20
|
import {
|
|
21
21
|
type AnyTarget,
|
|
22
|
-
|
|
22
|
+
supportsScope,
|
|
23
23
|
TARGETS,
|
|
24
24
|
type TargetDraft,
|
|
25
25
|
} from "./targets.js";
|
|
26
|
-
import type {
|
|
26
|
+
import type { Scope, ScopeSelection } from "./types.js";
|
|
27
27
|
|
|
28
28
|
/** One detection, kept beside the row that produced it (and can read it back). */
|
|
29
29
|
export interface DetectedRow {
|
|
30
30
|
readonly target: AnyTarget;
|
|
31
|
-
readonly
|
|
31
|
+
readonly scope: Scope;
|
|
32
32
|
readonly detection: never;
|
|
33
33
|
/**
|
|
34
34
|
* Why this row's detection did not settle, when it threw. `detection` is then
|
|
@@ -48,8 +48,8 @@ export interface DetectedPlan {
|
|
|
48
48
|
readonly detected: readonly DetectedRow[];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** The
|
|
52
|
-
export const
|
|
51
|
+
/** The scopes a `--scope` selection runs, global before project. */
|
|
52
|
+
export const scopesForSelection = (scope: ScopeSelection): readonly Scope[] =>
|
|
53
53
|
scope === "both" ? ["global", "project"] : [scope];
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -72,7 +72,7 @@ const messageOf = (reason: unknown): string =>
|
|
|
72
72
|
reason instanceof Error ? reason.message : String(reason);
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Detect every requested target in every
|
|
75
|
+
* Detect every requested target in every scope the scope runs.
|
|
76
76
|
*
|
|
77
77
|
* Detections are started together and SETTLED INDEPENDENTLY: they are
|
|
78
78
|
* independent reads, and a run-all that probed five targets serially paid for
|
|
@@ -82,13 +82,13 @@ const messageOf = (reason: unknown): string =>
|
|
|
82
82
|
* unreadable config, a permission-denied directory — takes only its own row
|
|
83
83
|
* down. Awaiting them as one `Promise.all` meant the first rejection prevented
|
|
84
84
|
* a run from being built at all: none of the other targets ran, and doctor
|
|
85
|
-
* rendered no
|
|
85
|
+
* rendered no scoped rows whatsoever. The failed row carries its cause and is
|
|
86
86
|
* reported as a row, which is the same shape a failed COMPOSE already has.
|
|
87
87
|
*
|
|
88
88
|
* @param rt - The per-invocation runtime.
|
|
89
89
|
* @param ids - The targets to plan (all five for the run-all).
|
|
90
|
-
* @param scope - The resolved
|
|
91
|
-
* @returns One entry per (target,
|
|
90
|
+
* @param scope - The resolved scope selection.
|
|
91
|
+
* @returns One entry per (target, scope) the scope actually runs.
|
|
92
92
|
* @note Impure — every target's `detect` reads the real filesystem.
|
|
93
93
|
*/
|
|
94
94
|
export async function detectTargets(
|
|
@@ -97,25 +97,25 @@ export async function detectTargets(
|
|
|
97
97
|
scope: ScopeSelection,
|
|
98
98
|
): Promise<DetectedRow[]> {
|
|
99
99
|
const wanted = TARGETS.filter((target) => ids.includes(target.id));
|
|
100
|
-
const pairs =
|
|
100
|
+
const pairs = scopesForSelection(scope).flatMap((scope) =>
|
|
101
101
|
wanted
|
|
102
|
-
.filter((target) =>
|
|
102
|
+
.filter((target) => supportsScope(target, scope))
|
|
103
103
|
.map((target) => ({
|
|
104
104
|
target,
|
|
105
|
-
|
|
105
|
+
scope,
|
|
106
106
|
})),
|
|
107
107
|
);
|
|
108
108
|
const settled = await Promise.allSettled(
|
|
109
|
-
pairs.map(({ target,
|
|
109
|
+
pairs.map(({ target, scope }) => target.detect(rt, scope)),
|
|
110
110
|
);
|
|
111
|
-
return pairs.map(({ target,
|
|
111
|
+
return pairs.map(({ target, scope }, index) => {
|
|
112
112
|
const outcome = settled[index] as PromiseSettledResult<unknown>;
|
|
113
113
|
if (outcome.status === "fulfilled") {
|
|
114
|
-
return { target,
|
|
114
|
+
return { target, scope, detection: outcome.value as never };
|
|
115
115
|
}
|
|
116
116
|
return {
|
|
117
117
|
target,
|
|
118
|
-
|
|
118
|
+
scope,
|
|
119
119
|
detection: undefined as never,
|
|
120
120
|
failure: messageOf(outcome.reason),
|
|
121
121
|
};
|
|
@@ -123,22 +123,26 @@ export async function detectTargets(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* The dim row a target gets when the SELECTED
|
|
127
|
-
* --local` still shows `completions`, saying why it is not in this run. A
|
|
128
|
-
* merely filters the run-all: the user asked for a
|
|
126
|
+
* The dim row a target gets when the SELECTED scope cannot hold it — `setup
|
|
127
|
+
* --local` still shows `completions`, saying why it is not in this run. A scope
|
|
128
|
+
* merely filters the run-all: the user asked for a scope, not for that target, so
|
|
129
129
|
* nothing errors, but nothing is silently absent either. (Asking for the
|
|
130
|
-
* impossible
|
|
130
|
+
* impossible scope on the SUB-VERB is a different thing entirely — a typed
|
|
131
131
|
* contradiction, and a usage error.)
|
|
132
|
+
*
|
|
133
|
+
* The reason names the flag that WOULD reach the target, so the row is not a
|
|
134
|
+
* dead end: a reader who wanted `completions` under `--local` is told in the
|
|
135
|
+
* same line that `--global` is where it lives.
|
|
132
136
|
*/
|
|
133
|
-
const
|
|
137
|
+
const outOfScopeRow = (target: AnyTarget, scope: Scope): PlanRow => ({
|
|
134
138
|
target: target.id,
|
|
135
|
-
|
|
139
|
+
scope,
|
|
136
140
|
action: "skip",
|
|
137
|
-
detail: "not
|
|
141
|
+
detail: "not part of this scope",
|
|
138
142
|
reason:
|
|
139
|
-
|
|
140
|
-
? "
|
|
141
|
-
: "project
|
|
143
|
+
scope === "project"
|
|
144
|
+
? "this one is global only — run it with `--global`"
|
|
145
|
+
: "this one is per-project only — run it with `--local`",
|
|
142
146
|
selected: false,
|
|
143
147
|
});
|
|
144
148
|
|
|
@@ -151,11 +155,11 @@ const outOfBandRow = (target: AnyTarget, band: ScopeBand): PlanRow => ({
|
|
|
151
155
|
*/
|
|
152
156
|
const failedDetectionRow = (
|
|
153
157
|
target: AnyTarget,
|
|
154
|
-
|
|
158
|
+
scope: Scope,
|
|
155
159
|
failure: string,
|
|
156
160
|
): PlanRow => ({
|
|
157
161
|
target: target.id,
|
|
158
|
-
|
|
162
|
+
scope,
|
|
159
163
|
action: "skip",
|
|
160
164
|
detail: "detection did not complete",
|
|
161
165
|
reason: failure,
|
|
@@ -179,51 +183,51 @@ export function draftFor(
|
|
|
179
183
|
removal: boolean,
|
|
180
184
|
): TargetDraft {
|
|
181
185
|
return removal
|
|
182
|
-
? hit.target.removalPlan(hit.detection, hit.
|
|
183
|
-
: hit.target.plan(hit.detection, hit.
|
|
186
|
+
? hit.target.removalPlan(hit.detection, hit.scope, roots)
|
|
187
|
+
: hit.target.plan(hit.detection, hit.scope, roots);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
/**
|
|
187
191
|
* Project detections into plan rows.
|
|
188
192
|
*
|
|
189
|
-
* @param
|
|
193
|
+
* @param selection - The resolved scope selection (named in the header).
|
|
190
194
|
* @param roots - The two named roots every path renders relative to.
|
|
191
|
-
* @param detected - The detections, in table order per
|
|
192
|
-
* @param ids - The targets that were REQUESTED, so an out-of-
|
|
195
|
+
* @param detected - The detections, in table order per scope.
|
|
196
|
+
* @param ids - The targets that were REQUESTED, so an out-of-scope one still
|
|
193
197
|
* gets a visible row.
|
|
194
198
|
* @param removal - Build the removal plan (`--undo`) instead of the forward one.
|
|
195
199
|
* @returns The plan.
|
|
196
200
|
*/
|
|
197
201
|
export function buildPlan(
|
|
198
|
-
|
|
202
|
+
selection: ScopeSelection,
|
|
199
203
|
roots: SetupPlan["roots"],
|
|
200
204
|
detected: readonly DetectedRow[],
|
|
201
205
|
ids: readonly TargetId[],
|
|
202
206
|
removal = false,
|
|
203
207
|
): SetupPlan {
|
|
204
208
|
const rows: PlanRow[] = [];
|
|
205
|
-
for (const
|
|
209
|
+
for (const scope of scopesForSelection(selection)) {
|
|
206
210
|
for (const target of TARGETS) {
|
|
207
211
|
if (!ids.includes(target.id)) continue;
|
|
208
212
|
const hit = detected.find(
|
|
209
|
-
(d) => d.target.id === target.id && d.
|
|
213
|
+
(d) => d.target.id === target.id && d.scope === scope,
|
|
210
214
|
);
|
|
211
215
|
if (hit === undefined) {
|
|
212
|
-
// Only worth a line when the target has no row in ANY selected
|
|
216
|
+
// Only worth a line when the target has no row in ANY selected scope —
|
|
213
217
|
// under `--scope both` a global-only target already appears above.
|
|
214
218
|
const elsewhere = detected.some((d) => d.target.id === target.id);
|
|
215
|
-
if (!elsewhere) rows.push(
|
|
219
|
+
if (!elsewhere) rows.push(outOfScopeRow(target, scope));
|
|
216
220
|
continue;
|
|
217
221
|
}
|
|
218
222
|
const failure = detectionFailure(hit);
|
|
219
223
|
if (failure !== undefined) {
|
|
220
|
-
rows.push(failedDetectionRow(target,
|
|
224
|
+
rows.push(failedDetectionRow(target, scope, failure));
|
|
221
225
|
continue;
|
|
222
226
|
}
|
|
223
227
|
const draft = draftFor(hit, roots, removal);
|
|
224
228
|
rows.push({
|
|
225
229
|
target: target.id,
|
|
226
|
-
|
|
230
|
+
scope,
|
|
227
231
|
action: draft.action,
|
|
228
232
|
detail: draft.detail,
|
|
229
233
|
...(draft.reason === undefined ? {} : { reason: draft.reason }),
|
|
@@ -232,5 +236,5 @@ export function buildPlan(
|
|
|
232
236
|
});
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
|
-
return { scope, roots, rows };
|
|
239
|
+
return { scope: selection, roots, rows };
|
|
236
240
|
}
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* who forgot `--yes` reads the hint on the very output they are capturing.
|
|
21
21
|
*
|
|
22
22
|
* An MCP tool call is programmatic consent: `transport === "mcp"` resolves as if
|
|
23
|
-
* `--yes` were passed. The
|
|
24
|
-
* global
|
|
23
|
+
* `--yes` were passed. The scope default applies there too — the tool writes the
|
|
24
|
+
* global scope unless told otherwise.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import type { InteractionMode } from "@canonical/summon-core/projection";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* The detection types are part of the surface, not an implementation leak: a
|
|
15
15
|
* detection is the value passed from the read half to the compose half and on
|
|
16
|
-
* to the plan rows, so `doctor`'s
|
|
16
|
+
* to the plan rows, so `doctor`'s scoped checks name the same records `setup`
|
|
17
17
|
* acts on. The presentation helpers travel with them for the same reason — a
|
|
18
18
|
* skip reason or an uninstall remedy is derived from one detection, and
|
|
19
19
|
* deriving it twice is how the two surfaces start disagreeing about the same
|
|
@@ -54,6 +54,7 @@ export {
|
|
|
54
54
|
lspEditorNames,
|
|
55
55
|
lspSkipReason,
|
|
56
56
|
lspUninstallRemedy,
|
|
57
|
+
ownedLspEditors,
|
|
57
58
|
} from "./setupLsp.js";
|
|
58
59
|
export type { McpDetection } from "./setupMcp.js";
|
|
59
60
|
export {
|
|
@@ -61,6 +62,7 @@ export {
|
|
|
61
62
|
composeMcpRemoval,
|
|
62
63
|
detectMcp,
|
|
63
64
|
mcpGroupState,
|
|
65
|
+
mcpWriteState,
|
|
64
66
|
ownedMcpGroups,
|
|
65
67
|
selectedGroups,
|
|
66
68
|
} from "./setupMcp.js";
|
|
@@ -71,4 +73,7 @@ export {
|
|
|
71
73
|
detectSkills,
|
|
72
74
|
ownedSkillLinks,
|
|
73
75
|
skillsSkipReason,
|
|
76
|
+
skillsSkipRemedy,
|
|
77
|
+
staleSkillLinks,
|
|
78
|
+
withinRoot,
|
|
74
79
|
} from "./setupSkills.js";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* gate, live progress, colours, and the shared cancel fixes by construction.
|
|
6
6
|
*
|
|
7
7
|
* The shape mirrors `create`: detection runs FOR REAL up front (once per
|
|
8
|
-
* target/
|
|
8
|
+
* target/scope, in `detectTargets`), then a PURE `generate` composes only the
|
|
9
9
|
* effects for the SELECTED rows. `generate` must stay re-interpretable —
|
|
10
10
|
* `execute` invokes it more than once (the confirm-gate preview and the build) —
|
|
11
11
|
* so it is composed from combinators and does no reads of its own.
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
type TargetId,
|
|
56
56
|
withRows,
|
|
57
57
|
} from "../plan.js";
|
|
58
|
-
import type {
|
|
58
|
+
import type { Scope, ScopeSelection, SetupMode } from "../types.js";
|
|
59
59
|
|
|
60
60
|
/** The answer key the run-all's row multiselect writes. */
|
|
61
61
|
const ROWS_ANSWER = "targets";
|
|
@@ -72,9 +72,26 @@ const CHILD_ANSWER: Partial<Record<TargetId, string>> = {
|
|
|
72
72
|
lsp: LSP_EDITORS_ANSWER,
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
/** A row's identity in an answer bag:
|
|
76
|
-
const rowKey = (
|
|
77
|
-
|
|
75
|
+
/** A row's identity in an answer bag: scope-qualified, so `both` stays unambiguous. */
|
|
76
|
+
const rowKey = (scope: Scope, target: TargetId): string => `${scope}:${target}`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One live row-progress event: a plan row starting, finishing, or failing,
|
|
80
|
+
* reported WHILE the composed task is being interpreted. `label` is the row's
|
|
81
|
+
* progress sentence — the same `target detail — note` shape
|
|
82
|
+
* `renderProgressLine` prints — so the listener (the Ink wizard's step view)
|
|
83
|
+
* shows the run in the plan's own vocabulary, not the effect transcript's.
|
|
84
|
+
*
|
|
85
|
+
* Structurally identical to summon-core's `StepReport` ON PURPOSE: the wizard
|
|
86
|
+
* branch forwards these events verbatim, and this module must not import
|
|
87
|
+
* summon-core values (it stays behind the verb's lazy import).
|
|
88
|
+
*/
|
|
89
|
+
export interface RowEvent {
|
|
90
|
+
/** The row's stable identity — `scope:target`. */
|
|
91
|
+
readonly key: string;
|
|
92
|
+
readonly label: string;
|
|
93
|
+
readonly status: "start" | "done" | "failed";
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
/**
|
|
80
97
|
* A ready-to-run setup invocation: the plan as detected, the synthesized
|
|
@@ -86,6 +103,15 @@ export interface SetupRun {
|
|
|
86
103
|
readonly generator: GeneratorDefinition;
|
|
87
104
|
/** The plan with the wizard's selection and the run's outcomes applied. */
|
|
88
105
|
applied(answers: Record<string, unknown>): SetupPlan;
|
|
106
|
+
/**
|
|
107
|
+
* Register THE live row listener (the Ink wizard's step adapter). The
|
|
108
|
+
* composed rows emit through it while they are interpreted — which means
|
|
109
|
+
* EVERY interpretation fires it, the confirm gate's honest preview included;
|
|
110
|
+
* the receiving session drops reports outside its executing phase, exactly
|
|
111
|
+
* as the {@link OutcomeSink} relies on last-drive-wins for its records. No
|
|
112
|
+
* listener registered (a `--yes` run, `--dry-run`, MCP) costs nothing.
|
|
113
|
+
*/
|
|
114
|
+
setRowListener(listener: (event: RowEvent) => void): void;
|
|
89
115
|
}
|
|
90
116
|
|
|
91
117
|
/**
|
|
@@ -144,25 +170,50 @@ const rowPragmaError = (error: TaskError): PragmaError =>
|
|
|
144
170
|
* throw bypasses the trampoline's recovery frames — which is why the row bodies
|
|
145
171
|
* raise via `checkExecOk`/`failPragma`, never a bare throw. Interruption bypasses
|
|
146
172
|
* recovery by interpreter invariant, so Ctrl-C still stops the whole run.
|
|
173
|
+
*
|
|
174
|
+
* Each row is also BRACKETED with {@link RowEvent}s — `start` as its body is
|
|
175
|
+
* entered, `done`/`failed` as it settles — emitted from the continuations,
|
|
176
|
+
* which run at interpretation time. Like the sink writes beside them, the
|
|
177
|
+
* emits capture nothing IN the task: the composition stays pure and
|
|
178
|
+
* re-interpretable, and each drive simply narrates itself as it runs.
|
|
147
179
|
*/
|
|
148
180
|
const runRowsIsolated = (
|
|
149
181
|
sink: OutcomeSink,
|
|
150
|
-
rows: readonly {
|
|
182
|
+
rows: readonly {
|
|
183
|
+
key: string;
|
|
184
|
+
label: string;
|
|
185
|
+
doneLabel: string;
|
|
186
|
+
task: Task<void>;
|
|
187
|
+
}[],
|
|
188
|
+
emit: (event: RowEvent) => void,
|
|
151
189
|
): Task<void> =>
|
|
152
190
|
flatMap(pure(undefined), (): Task<void> => {
|
|
153
191
|
sink.clear();
|
|
154
192
|
return sequence_(
|
|
155
|
-
rows.map(({ key, task }) =>
|
|
156
|
-
recover(
|
|
157
|
-
flatMap(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
() => {
|
|
162
|
-
sink.record(key, error);
|
|
193
|
+
rows.map(({ key, label, doneLabel, task }) =>
|
|
194
|
+
recover(
|
|
195
|
+
flatMap(pure(undefined), () => {
|
|
196
|
+
emit({ key, label, status: "start" });
|
|
197
|
+
return flatMap(task, () => {
|
|
198
|
+
emit({ key, label: doneLabel, status: "done" });
|
|
163
199
|
return pure(undefined);
|
|
164
|
-
}
|
|
165
|
-
),
|
|
200
|
+
});
|
|
201
|
+
}),
|
|
202
|
+
(error) =>
|
|
203
|
+
flatMap(
|
|
204
|
+
warn(
|
|
205
|
+
`The ${key.split(":")[1]} step did not complete — continuing with the remaining targets.`,
|
|
206
|
+
),
|
|
207
|
+
() => {
|
|
208
|
+
sink.record(key, error);
|
|
209
|
+
emit({
|
|
210
|
+
key,
|
|
211
|
+
label: `${label} — ${rowPragmaError(error).message}`,
|
|
212
|
+
status: "failed",
|
|
213
|
+
});
|
|
214
|
+
return pure(undefined);
|
|
215
|
+
},
|
|
216
|
+
),
|
|
166
217
|
),
|
|
167
218
|
),
|
|
168
219
|
);
|
|
@@ -198,7 +249,7 @@ const buildRowsPrompt = (plan: SetupPlan): PromptDefinition => {
|
|
|
198
249
|
.filter((row) => row.action !== "skip")
|
|
199
250
|
.map((row) => ({
|
|
200
251
|
label: `${row.target} — ${row.detail}${row.action === "none" ? " (already configured)" : ""}`,
|
|
201
|
-
value: rowKey(row.
|
|
252
|
+
value: rowKey(row.scope, row.target),
|
|
202
253
|
}));
|
|
203
254
|
return {
|
|
204
255
|
name: ROWS_ANSWER,
|
|
@@ -207,27 +258,27 @@ const buildRowsPrompt = (plan: SetupPlan): PromptDefinition => {
|
|
|
207
258
|
choices,
|
|
208
259
|
default: plan.rows
|
|
209
260
|
.filter((row) => row.selected)
|
|
210
|
-
.map((row) => rowKey(row.
|
|
261
|
+
.map((row) => rowKey(row.scope, row.target)),
|
|
211
262
|
};
|
|
212
263
|
};
|
|
213
264
|
|
|
214
|
-
/** The opt-in "customize which files" gate — defaults to false. */
|
|
215
|
-
const buildCustomizePrompt = (
|
|
216
|
-
when?: PromptDefinition["when"],
|
|
217
|
-
): PromptDefinition => ({
|
|
218
|
-
name: "customize",
|
|
219
|
-
type: "confirm",
|
|
220
|
-
message: `Customize which files ${BIN_NAME} configures?`,
|
|
221
|
-
default: false,
|
|
222
|
-
when,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
265
|
/**
|
|
226
266
|
* The per-file MCP multiselect — one row per deduplicated config file, across
|
|
227
|
-
* every
|
|
228
|
-
* file that is absent or drifted stays selected.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
267
|
+
* every scope the run covers. An already-current file is DEFAULT-DESELECTED; a
|
|
268
|
+
* file that is absent or drifted stays selected. It is row-level CHILD
|
|
269
|
+
* selection, which is exactly what the plan's child rows are.
|
|
270
|
+
*
|
|
271
|
+
* It used to sit behind a meta-question — "Customize which files pragma
|
|
272
|
+
* configures?", answered no by default. That gate was read in exactly ONE place
|
|
273
|
+
* in the repo (this prompt's `when`) and nowhere else: not by `generate`, not by
|
|
274
|
+
* `applied`, not by any plan, recap, renderer or JSON projection. Answering no
|
|
275
|
+
* meant the child prompt was never asked, its key was absent from the answer
|
|
276
|
+
* bag, `readList` returned `undefined`, and the compose bodies read that as
|
|
277
|
+
* "all" — so everything detected was configured anyway. A meta-question in front
|
|
278
|
+
* of a direct question is one more thing to answer and no more control; the
|
|
279
|
+
* choices below already carry the detected state per child (`— unchanged`,
|
|
280
|
+
* `— add`, `— update`), which is the answer the meta-question was standing in
|
|
281
|
+
* front of.
|
|
231
282
|
*/
|
|
232
283
|
const buildChildPrompt = (
|
|
233
284
|
plan: SetupPlan,
|
|
@@ -251,7 +302,7 @@ const buildChildPrompt = (
|
|
|
251
302
|
};
|
|
252
303
|
};
|
|
253
304
|
|
|
254
|
-
/** Every child row a target contributes, across the
|
|
305
|
+
/** Every child row a target contributes, across the scopes in the plan. */
|
|
255
306
|
const childrenOf = (
|
|
256
307
|
plan: SetupPlan,
|
|
257
308
|
target: TargetId,
|
|
@@ -268,7 +319,7 @@ const isChosen = (
|
|
|
268
319
|
row.action !== "skip" &&
|
|
269
320
|
(chosen === undefined
|
|
270
321
|
? row.selected
|
|
271
|
-
: chosen.includes(rowKey(row.
|
|
322
|
+
: chosen.includes(rowKey(row.scope, row.target)));
|
|
272
323
|
|
|
273
324
|
/**
|
|
274
325
|
* What a row DID, as a past-tense word. The row's detail says what and where;
|
|
@@ -320,7 +371,7 @@ function childNote(
|
|
|
320
371
|
*
|
|
321
372
|
* @param rt - The per-invocation runtime.
|
|
322
373
|
* @param mode - The entry point: the run-all, or one target.
|
|
323
|
-
* @param scope - The resolved
|
|
374
|
+
* @param scope - The resolved scope selection.
|
|
324
375
|
* @param removal - Compose the removal (`--undo`) instead of the install.
|
|
325
376
|
* @returns The plan, the generator, and the answers-to-plan projection.
|
|
326
377
|
* @note Impure — runs every selected target's real detection.
|
|
@@ -350,35 +401,51 @@ export async function buildSetupRun(
|
|
|
350
401
|
] as const
|
|
351
402
|
).filter(([target]) => !removal && childrenOf(plan, target).length > 1);
|
|
352
403
|
|
|
353
|
-
|
|
354
|
-
|
|
404
|
+
for (const [target, message] of narrowable) {
|
|
405
|
+
// Asked whenever the row it narrows is in the run — one direct question,
|
|
406
|
+
// no meta-question in front of it. Removing a prompt cannot break the
|
|
407
|
+
// non-interactive paths: `autoPrompt`/`mcpPrompt` return each question's
|
|
408
|
+
// default, so a smaller prompt list means a smaller answer bag, and the
|
|
409
|
+
// compose bodies already read an absent child answer as "all". (The
|
|
410
|
+
// asymmetry is the reason this direction is safe: ADDING a defaultless
|
|
411
|
+
// prompt would hard-fail with MISSING_REQUIRED_ANSWER.)
|
|
412
|
+
const rowChosen = (answers: Record<string, unknown>): boolean => {
|
|
355
413
|
const chosen = readList(answers, ROWS_ANSWER);
|
|
356
|
-
return
|
|
357
|
-
|
|
414
|
+
return plan.rows.some(
|
|
415
|
+
(row) => row.target === target && isChosen(row, chosen),
|
|
358
416
|
);
|
|
359
417
|
};
|
|
360
|
-
prompts.push(
|
|
361
|
-
for (const [target, message] of narrowable) {
|
|
362
|
-
const rowChosen = (answers: Record<string, unknown>): boolean => {
|
|
363
|
-
const chosen = readList(answers, ROWS_ANSWER);
|
|
364
|
-
return plan.rows.some(
|
|
365
|
-
(row) => row.target === target && isChosen(row, chosen),
|
|
366
|
-
);
|
|
367
|
-
};
|
|
368
|
-
prompts.push(
|
|
369
|
-
buildChildPrompt(
|
|
370
|
-
plan,
|
|
371
|
-
target,
|
|
372
|
-
message,
|
|
373
|
-
(answers) => answers.customize === true && rowChosen(answers),
|
|
374
|
-
),
|
|
375
|
-
);
|
|
376
|
-
}
|
|
418
|
+
prompts.push(buildChildPrompt(plan, target, message, rowChosen));
|
|
377
419
|
}
|
|
378
420
|
|
|
379
421
|
/** The detection behind a row, for composing and for re-reading its draft. */
|
|
380
422
|
const detectionFor = (row: PlanRow): DetectedRow | undefined =>
|
|
381
|
-
detected.find((d) => d.target.id === row.target && d.
|
|
423
|
+
detected.find((d) => d.target.id === row.target && d.scope === row.scope);
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The success note a row's completion carries — ONE derivation for the
|
|
427
|
+
* recap's outcome and the live row event's `done` label, so the sentence a
|
|
428
|
+
* watcher saw land is the sentence the recap repeats. A removal never
|
|
429
|
+
* borrows the forward child summary: its children are the files the entry is
|
|
430
|
+
* being taken OUT of, and counting them as `1 updated` described the
|
|
431
|
+
* opposite of what the run had just done.
|
|
432
|
+
*/
|
|
433
|
+
const noteFor = (
|
|
434
|
+
row: PlanRow,
|
|
435
|
+
answers: Record<string, unknown>,
|
|
436
|
+
): string | undefined => {
|
|
437
|
+
if (removal) return ACTION_NOTES[row.action];
|
|
438
|
+
const childKey = CHILD_ANSWER[row.target];
|
|
439
|
+
const kept =
|
|
440
|
+
childKey === undefined ? undefined : readList(answers, childKey);
|
|
441
|
+
return childNote(row, kept) ?? ACTION_NOTES[row.action];
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/** The live row listener, when a wizard is watching. See {@link SetupRun}. */
|
|
445
|
+
let rowListener: ((event: RowEvent) => void) | undefined;
|
|
446
|
+
|
|
447
|
+
/** The id column width — the recap's own rule, so the columns agree. */
|
|
448
|
+
const idWidth = Math.max(...plan.rows.map((row) => row.target.length));
|
|
382
449
|
|
|
383
450
|
const generator: GeneratorDefinition = {
|
|
384
451
|
meta: buildMeta(
|
|
@@ -399,9 +466,21 @@ export async function buildSetupRun(
|
|
|
399
466
|
const task = removal
|
|
400
467
|
? hit.target.composeRemoval(hit.detection)
|
|
401
468
|
: hit.target.compose(hit.detection, children);
|
|
402
|
-
|
|
469
|
+
// The row's progress sentence — the `target detail — note` shape
|
|
470
|
+
// `renderProgressLine` prints — so the live view and the recap say the
|
|
471
|
+
// same thing about the same row.
|
|
472
|
+
const label = `${row.target.padEnd(idWidth)} ${row.detail}`;
|
|
473
|
+
const note = noteFor(row, answers);
|
|
474
|
+
return [
|
|
475
|
+
{
|
|
476
|
+
key: rowKey(row.scope, row.target),
|
|
477
|
+
label,
|
|
478
|
+
doneLabel: note === undefined ? label : `${label} — ${note}`,
|
|
479
|
+
task,
|
|
480
|
+
},
|
|
481
|
+
];
|
|
403
482
|
});
|
|
404
|
-
return runRowsIsolated(sink, tasks);
|
|
483
|
+
return runRowsIsolated(sink, tasks, (event) => rowListener?.(event));
|
|
405
484
|
},
|
|
406
485
|
};
|
|
407
486
|
|
|
@@ -432,7 +511,7 @@ export async function buildSetupRun(
|
|
|
432
511
|
// Convergence is a real result: one quiet line per row, zero writes.
|
|
433
512
|
if (row.action === "none") return { status: "noop", note: "unchanged" };
|
|
434
513
|
if (!isChosen(row, chosen)) return undefined;
|
|
435
|
-
const error = sink.get(rowKey(row.
|
|
514
|
+
const error = sink.get(rowKey(row.scope, row.target));
|
|
436
515
|
if (error !== undefined) {
|
|
437
516
|
const pragma = rowPragmaError(error);
|
|
438
517
|
return {
|
|
@@ -444,15 +523,7 @@ export async function buildSetupRun(
|
|
|
444
523
|
};
|
|
445
524
|
}
|
|
446
525
|
if (!isActionable(row.action)) return { status: "noop" };
|
|
447
|
-
|
|
448
|
-
// files the entry is being taken OUT of, and counting them as "1 updated"
|
|
449
|
-
// described the opposite of what the run had just done.
|
|
450
|
-
const childKey = CHILD_ANSWER[row.target];
|
|
451
|
-
const kept =
|
|
452
|
-
childKey === undefined ? undefined : readList(answers, childKey);
|
|
453
|
-
const note = removal
|
|
454
|
-
? ACTION_NOTES[row.action]
|
|
455
|
-
: (childNote(row, kept) ?? ACTION_NOTES[row.action]);
|
|
526
|
+
const note = noteFor(row, answers);
|
|
456
527
|
return {
|
|
457
528
|
status: removal ? "removed" : "done",
|
|
458
529
|
...(note === undefined ? {} : { note }),
|
|
@@ -462,6 +533,9 @@ export async function buildSetupRun(
|
|
|
462
533
|
return {
|
|
463
534
|
plan,
|
|
464
535
|
generator,
|
|
536
|
+
setRowListener: (listener) => {
|
|
537
|
+
rowListener = listener;
|
|
538
|
+
},
|
|
465
539
|
applied: (answers) => {
|
|
466
540
|
const chosen = readList(answers, ROWS_ANSWER);
|
|
467
541
|
return withRows(
|