@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
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
* degrades to a `pragma sources update` hint (never a live re-index).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { mkdtempSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
10
14
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
11
15
|
import { readPackIndex } from "../../kernel/completion/entitySource.js";
|
|
12
16
|
import { verbKey } from "../../kernel/packs/uniqueness.js";
|
|
13
17
|
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
14
18
|
import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
|
|
19
|
+
import { writeLock } from "../../kernel/runtime/lock.js";
|
|
15
20
|
import type { InspectResult } from "../../kernel/runtime/readEntity.js";
|
|
16
21
|
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
17
22
|
import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
|
|
@@ -44,6 +49,48 @@ describe("resource listing (storeless, over the pack index)", () => {
|
|
|
44
49
|
expect(componentIdx).toBeLessThan(buttonIdx);
|
|
45
50
|
});
|
|
46
51
|
|
|
52
|
+
it("emits ONE resource per URI for an OWL-punned subject (A8)", () => {
|
|
53
|
+
// A punned subject (a class IRI also asserted as an individual) is indexed
|
|
54
|
+
// as TWO facets — tbox + abox — sharing one prefixed URI. The listing must
|
|
55
|
+
// dedup them so the MCP resource surface carries no duplicate URI. The abox
|
|
56
|
+
// facet is listed first here to prove the dedup relies on the schema-first
|
|
57
|
+
// SORT, not input order.
|
|
58
|
+
const punned: PackIndex = {
|
|
59
|
+
version: 2,
|
|
60
|
+
contentHash: "test",
|
|
61
|
+
prefixes: {},
|
|
62
|
+
entities: [
|
|
63
|
+
{
|
|
64
|
+
name: "ex:Slider",
|
|
65
|
+
type: "ex:Category",
|
|
66
|
+
uri: "https://ex.test/#Slider",
|
|
67
|
+
prefixed: "ex:Slider",
|
|
68
|
+
types: ["owl:Class", "ex:Category"],
|
|
69
|
+
label: "Slider",
|
|
70
|
+
box: "abox",
|
|
71
|
+
description: null,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "ex:Slider",
|
|
75
|
+
type: "owl:Class",
|
|
76
|
+
uri: "https://ex.test/#Slider",
|
|
77
|
+
prefixed: "ex:Slider",
|
|
78
|
+
types: ["owl:Class", "ex:Category"],
|
|
79
|
+
label: "Slider",
|
|
80
|
+
box: "tbox",
|
|
81
|
+
description: null,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
instanceCountByType: {},
|
|
85
|
+
};
|
|
86
|
+
const slider = buildResourceList(punned).filter(
|
|
87
|
+
(r) => r.uri === "pragma:ex:Slider",
|
|
88
|
+
);
|
|
89
|
+
expect(slider).toHaveLength(1);
|
|
90
|
+
// The retained facet is the schema (tbox) one — surfaced above individuals.
|
|
91
|
+
expect(slider.at(0)?._meta?.["pragma/box"]).toBe("tbox");
|
|
92
|
+
});
|
|
93
|
+
|
|
47
94
|
it("enriches each entry with the _meta taxonomy (pragma/box + priority)", () => {
|
|
48
95
|
const resources = buildResourceList(readPackIndex(process.cwd()));
|
|
49
96
|
const component = resources.find((r) => r.uri === "pragma:ex:Component");
|
|
@@ -111,4 +158,56 @@ describe("resource surface over the server (embedded pack)", () => {
|
|
|
111
158
|
expect(fromResource).toEqual(fromCli);
|
|
112
159
|
expect(fromResource.uri).toBe("https://pragma.canonical.com/sample#Button");
|
|
113
160
|
});
|
|
161
|
+
|
|
162
|
+
it("rejects a read of an absent entity as InvalidParams carrying ENTITY_NOT_FOUND", async () => {
|
|
163
|
+
// The warm embedded pack resolves the `ex:` prefix but holds no `Nonexistent`
|
|
164
|
+
// subject, so the read fails ENTITY_NOT_FOUND — the caller's fault, which
|
|
165
|
+
// `mcpErrorFrom` maps to JSON-RPC InvalidParams, NOT the InternalError a cold
|
|
166
|
+
// store (STORE_UNAVAILABLE) earns.
|
|
167
|
+
const error = await harness.readResource("pragma:ex:Nonexistent").then(
|
|
168
|
+
() => undefined,
|
|
169
|
+
(caught: unknown) => caught,
|
|
170
|
+
);
|
|
171
|
+
expect(error, "the read must throw, not swallow to content").toBeDefined();
|
|
172
|
+
expect((error as Error).message).toMatch(/not found/i);
|
|
173
|
+
const failure = error as { code?: number; data?: { code?: string } };
|
|
174
|
+
expect(failure.data?.code).toBe("ENTITY_NOT_FOUND");
|
|
175
|
+
expect(failure.code).toBe(ErrorCode.InvalidParams);
|
|
176
|
+
expect(failure.code).not.toBe(ErrorCode.InternalError);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* D3 — a resource-read failure surfaces as a JSON-RPC error (the read analogue of
|
|
182
|
+
* a tool result's `isError`) carrying the recovery, NOT swallowed into a
|
|
183
|
+
* `text/plain` "success" body that drops the recovery and reads as though the
|
|
184
|
+
* entity itself were malformed. A lock pointing at an evicted pack is the cold
|
|
185
|
+
* store; the read boots the store, so it must refuse with STORE_UNAVAILABLE.
|
|
186
|
+
*/
|
|
187
|
+
describe("resource read — cold-store failure surfaces isError + recovery (D3)", () => {
|
|
188
|
+
let harness: Awaited<ReturnType<typeof projectMcp>>;
|
|
189
|
+
beforeAll(async () => {
|
|
190
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-resource-cold-"));
|
|
191
|
+
writeLock(cwd, { version: 1, contentHash: "b".repeat(64), packs: [] });
|
|
192
|
+
harness = await projectMcp([graphModule], cwd);
|
|
193
|
+
});
|
|
194
|
+
afterAll(async () => {
|
|
195
|
+
await harness.cleanup();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("rejects (does not return text/plain content) with the STORE_UNAVAILABLE recovery", async () => {
|
|
199
|
+
const error = await harness.readResource("pragma:ex:Button").then(
|
|
200
|
+
() => undefined,
|
|
201
|
+
(caught: unknown) => caught,
|
|
202
|
+
);
|
|
203
|
+
expect(error, "the read must throw, not swallow to content").toBeDefined();
|
|
204
|
+
expect((error as Error).message).toMatch(/store unavailable/i);
|
|
205
|
+
const data = (
|
|
206
|
+
error as {
|
|
207
|
+
data?: { code?: string; recovery?: { mcp?: { tool?: string } } };
|
|
208
|
+
}
|
|
209
|
+
).data;
|
|
210
|
+
expect(data?.code).toBe("STORE_UNAVAILABLE");
|
|
211
|
+
expect(data?.recovery?.mcp?.tool).toBe("sources_update");
|
|
212
|
+
});
|
|
114
213
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-sync pin (PROTECTED): the CLI's redeclared `ScopeBand`/`ScopeSelection`
|
|
3
|
+
* mirrors must stay structurally EQUAL to the `@canonical/harnesses` types.
|
|
4
|
+
*
|
|
5
|
+
* The CLI redeclares these in `doctor/types.ts` and `setup/types.ts` on purpose
|
|
6
|
+
* — so those statically-reachable type modules never pull the harnesses runtime
|
|
7
|
+
* onto the fast-path module graph (the lazy-dispatch invariant). That freedom is
|
|
8
|
+
* only safe if the mirrors do not DRIFT from the source of truth, which this
|
|
9
|
+
* `expectTypeOf` pin enforces at `tsc` time.
|
|
10
|
+
*
|
|
11
|
+
* Every harnesses import here is `import type` (type-only, erased at emit), so
|
|
12
|
+
* this file loads no harnesses runtime — the `lazy.test` no-runtime-import guard
|
|
13
|
+
* stays green (it also never reaches this test file: it walks the graph from
|
|
14
|
+
* `capabilities/index`, which imports no test).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ScopeBand as HarnessScopeBand,
|
|
19
|
+
ScopeSelection as HarnessScopeSelection,
|
|
20
|
+
} from "@canonical/harnesses";
|
|
21
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
22
|
+
import type { ScopeBand as DoctorScopeBand } from "./doctor/types.js";
|
|
23
|
+
import type {
|
|
24
|
+
ScopeBand as SetupScopeBand,
|
|
25
|
+
ScopeSelection as SetupScopeSelection,
|
|
26
|
+
} from "./setup/types.js";
|
|
27
|
+
|
|
28
|
+
describe("scope type-sync — CLI mirrors == @canonical/harnesses", () => {
|
|
29
|
+
it("both ScopeBand mirrors equal the harnesses ScopeBand", () => {
|
|
30
|
+
expectTypeOf<DoctorScopeBand>().toEqualTypeOf<HarnessScopeBand>();
|
|
31
|
+
expectTypeOf<SetupScopeBand>().toEqualTypeOf<HarnessScopeBand>();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("the setup ScopeSelection mirror equals the harnesses ScopeSelection", () => {
|
|
35
|
+
expectTypeOf<SetupScopeSelection>().toEqualTypeOf<HarnessScopeSelection>();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { setupModule } from "./setup.verb.js";
|
|
6
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
ConfiguredTarget,
|
|
8
|
+
ScopeSelection,
|
|
9
|
+
SetupResult,
|
|
10
|
+
SetupSkillsResult,
|
|
11
|
+
SymlinkAction,
|
|
12
|
+
} from "./types.js";
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* `setup completions`").
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
14
|
import { dirname } from "node:path";
|
|
14
15
|
import {
|
|
15
16
|
deleteFile,
|
|
@@ -26,15 +27,39 @@ import {
|
|
|
26
27
|
detectShell,
|
|
27
28
|
type ShellId,
|
|
28
29
|
} from "../shell.js";
|
|
30
|
+
import type { CompletionsState } from "../types.js";
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* The detected completion install target: the shell, the absolute script path,
|
|
32
|
-
*
|
|
34
|
+
* the emitted script body — all `null` when no shell could be detected — and the
|
|
35
|
+
* prior on-disk {@link CompletionsState} (whether an up-to-date/stale script is
|
|
36
|
+
* already installed), read up front so the recap and a `--dry-run` are accurate
|
|
37
|
+
* and an identical rewrite is skipped.
|
|
33
38
|
*/
|
|
34
39
|
export interface CompletionsDetection {
|
|
35
40
|
readonly shell: ShellId | null;
|
|
36
41
|
readonly path: string | null;
|
|
37
42
|
readonly script: string | null;
|
|
43
|
+
readonly state: CompletionsState;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classify the completion script already at `path` against the body we would
|
|
48
|
+
* write: `installed` when byte-identical (a re-run skips it), `stale` when a
|
|
49
|
+
* different script is present, `absent` when no file exists.
|
|
50
|
+
*
|
|
51
|
+
* @param path - The install path.
|
|
52
|
+
* @param script - The body a write would emit.
|
|
53
|
+
* @returns The prior {@link CompletionsState}.
|
|
54
|
+
* @note Impure — reads the install path.
|
|
55
|
+
*/
|
|
56
|
+
function classifyCompletions(path: string, script: string): CompletionsState {
|
|
57
|
+
if (!existsSync(path)) return "absent";
|
|
58
|
+
try {
|
|
59
|
+
return readFileSync(path, "utf8") === script ? "installed" : "stale";
|
|
60
|
+
} catch {
|
|
61
|
+
return "stale";
|
|
62
|
+
}
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
/**
|
|
@@ -52,7 +77,7 @@ export async function detectCompletions(
|
|
|
52
77
|
cwd: string,
|
|
53
78
|
): Promise<CompletionsDetection> {
|
|
54
79
|
const shell = detectShell();
|
|
55
|
-
if (!shell) return { shell: null, path: null, script: null };
|
|
80
|
+
if (!shell) return { shell: null, path: null, script: null, state: "absent" };
|
|
56
81
|
|
|
57
82
|
const [{ capabilities }, { emitScripts }, { readConfig }] = await Promise.all(
|
|
58
83
|
[
|
|
@@ -68,18 +93,16 @@ export async function detectCompletions(
|
|
|
68
93
|
.filter(([, enabled]) => enabled === false)
|
|
69
94
|
.map(([family]) => family)
|
|
70
95
|
: undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
})[shell],
|
|
82
|
-
};
|
|
96
|
+
const path = completionScriptPath(shell);
|
|
97
|
+
const script = emitScripts(capabilities, {
|
|
98
|
+
...(completion?.minChars !== undefined
|
|
99
|
+
? { minChars: completion.minChars }
|
|
100
|
+
: {}),
|
|
101
|
+
...(disabledFamilies && disabledFamilies.length > 0
|
|
102
|
+
? { disabledFamilies }
|
|
103
|
+
: {}),
|
|
104
|
+
})[shell];
|
|
105
|
+
return { shell, path, script, state: classifyCompletions(path, script) };
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
/**
|
|
@@ -89,6 +112,12 @@ export async function detectCompletions(
|
|
|
89
112
|
* interprets a generator's task TWICE — once for the confirm-gate preview and
|
|
90
113
|
* once to perform it — so the composed task must survive a second drive.
|
|
91
114
|
*
|
|
115
|
+
* The prior `state` drives the MESSAGE only ("already up to date" / "Updating" /
|
|
116
|
+
* "Installing"), NOT whether the write happens: the (idempotent) write is always
|
|
117
|
+
* composed so it carries its `undo` — `--undo` reverses the SAME task and must
|
|
118
|
+
* find the reversible effect, and `execute` interprets the task twice. A
|
|
119
|
+
* byte-identical rewrite in the `installed` case is harmless.
|
|
120
|
+
*
|
|
92
121
|
* @param d - The detection gathered up front.
|
|
93
122
|
* @returns A Task that writes the script (with an undo), or warns if no shell.
|
|
94
123
|
*/
|
|
@@ -98,9 +127,13 @@ export function composeCompletions(d: CompletionsDetection): Task<void> {
|
|
|
98
127
|
"Could not detect your shell — set $SHELL to zsh, bash, or fish.",
|
|
99
128
|
);
|
|
100
129
|
}
|
|
101
|
-
const { path, script, shell } = d;
|
|
130
|
+
const { path, script, shell, state } = d;
|
|
131
|
+
const opening =
|
|
132
|
+
state === "installed"
|
|
133
|
+
? `${shell} completions already up to date at ${path}.`
|
|
134
|
+
: `${state === "stale" ? "Updating" : "Installing"} ${shell} completions to ${path}...`;
|
|
102
135
|
return sequence_([
|
|
103
|
-
info(
|
|
136
|
+
info(opening),
|
|
104
137
|
mkdir(dirname(path), true),
|
|
105
138
|
writeFile(path, script, { undo: deleteFile(path) }),
|
|
106
139
|
info(activationHint(shell)),
|