@canonical/pragma-cli 0.33.0 → 0.34.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 +7 -7
- package/dist/pragma +0 -0
- package/package.json +18 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +13 -4
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.verb.ts +31 -13
- package/src/capabilities/capabilities/catalog.ts +18 -10
- package/src/capabilities/capabilities/hints.ts +13 -19
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.verb.ts +8 -4
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +0 -27
- package/src/capabilities/config/set.verb.ts +7 -3
- package/src/capabilities/config/show.render.test.ts +77 -5
- package/src/capabilities/config/show.render.ts +7 -12
- package/src/capabilities/config/show.verb.ts +4 -3
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.test.ts +78 -100
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +162 -15
- package/src/capabilities/create/create.verb.ts +140 -69
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/doctor.render.test.ts +6 -6
- package/src/capabilities/doctor/doctor.render.ts +2 -1
- package/src/capabilities/doctor/doctor.test.ts +37 -8
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +5 -5
- package/src/capabilities/doctor/types.ts +1 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/verbs.ts +21 -21
- package/src/capabilities/prompt/prompt.test.ts +12 -6
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/resources/provider.ts +30 -11
- package/src/capabilities/resources/resources.test.ts +85 -29
- package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
- package/src/capabilities/setup/operations/setupMcp.ts +2 -2
- package/src/capabilities/setup/operations/setupSkills.ts +3 -3
- package/src/capabilities/setup/setup.verb.ts +12 -5
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +55 -50
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +113 -122
- package/src/capabilities/sources/sources.test.ts +111 -117
- package/src/capabilities/sources/status.render.test.ts +53 -24
- package/src/capabilities/sources/status.render.ts +24 -29
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +33 -10
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +8 -6
- package/src/constants.ts +47 -21
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +117 -107
- package/src/kernel/completion/model.test.ts +1 -1
- package/src/kernel/completion/model.ts +11 -0
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +1 -1
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +9 -4
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +105 -105
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +11 -6
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
- package/src/kernel/project/mcp/prompts/provider.ts +22 -8
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +1 -1
- package/src/kernel/runtime/graphpack/build.ts +29 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +4 -9
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +61 -48
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +78 -62
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +9 -8
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
- package/src/kernel/spec/emitReference.test.ts +3 -3
- package/src/kernel/spec/emitReference.ts +144 -11
- package/src/kernel/spec/emitSurface.ts +9 -5
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +8 -2
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/completion.test.ts +19 -6
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -75
- package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +9 -4
- package/src/testing/behavioral/staleCommands.test.ts +10 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
- package/src/testing/eval/cases/assembly.ts +13 -6
- package/src/testing/eval/cases/effectful.ts +25 -13
- package/src/testing/eval/cases/readNouns.ts +22 -20
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +45 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +6 -5
- package/src/testing/fixtures/graph/owlExport.ts +7 -5
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +0 -79
- package/src/capabilities/block/blockList.verb.ts +0 -163
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -166
- package/src/capabilities/block/parity.test.ts +0 -187
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.test.ts +0 -72
- package/src/capabilities/tier/lookup.render.ts +0 -59
- package/src/capabilities/tier/lookup.test.ts +0 -83
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -87
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { fail, succeed } from "@canonical/task";
|
|
2
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { fixturePreviewModule } from "../../../testing/fixtures/fixtureCapability.js";
|
|
3
7
|
import { PragmaError } from "../../error/PragmaError.js";
|
|
4
8
|
import { bootRuntime } from "../../runtime/boot.js";
|
|
5
9
|
import type { GlobalFlags } from "../../runtime/types.js";
|
|
6
10
|
import type { ParamSpec, VerbSpec } from "../../spec/types.js";
|
|
7
11
|
import { dispatch, executeVerb, extractParams } from "./dispatch.js";
|
|
8
12
|
|
|
13
|
+
/** The read-then-write mutation the honest-preview guards drive. */
|
|
14
|
+
const graft = fixturePreviewModule.verbs[0] as VerbSpec;
|
|
15
|
+
|
|
9
16
|
const PLAIN: GlobalFlags = {
|
|
10
17
|
llm: false,
|
|
11
18
|
autoLlm: false,
|
|
@@ -233,6 +240,66 @@ describe("dispatch — errors", () => {
|
|
|
233
240
|
});
|
|
234
241
|
});
|
|
235
242
|
|
|
243
|
+
describe("--dry-run is honest (PRA-104)", () => {
|
|
244
|
+
const savedExit = process.exitCode;
|
|
245
|
+
afterEach(() => {
|
|
246
|
+
process.exitCode = savedExit;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/** Capture stderr for one dispatch. */
|
|
250
|
+
const dispatchCapturing = async (
|
|
251
|
+
positionals: readonly string[],
|
|
252
|
+
opts: Record<string, unknown>,
|
|
253
|
+
): Promise<string> => {
|
|
254
|
+
const errs: string[] = [];
|
|
255
|
+
const spy = vi
|
|
256
|
+
.spyOn(process.stderr, "write")
|
|
257
|
+
.mockImplementation((chunk: string | Uint8Array) => {
|
|
258
|
+
errs.push(String(chunk));
|
|
259
|
+
return true;
|
|
260
|
+
});
|
|
261
|
+
process.exitCode = 0;
|
|
262
|
+
try {
|
|
263
|
+
await dispatch(graft, positionals, opts, PLAIN);
|
|
264
|
+
} finally {
|
|
265
|
+
spy.mockRestore();
|
|
266
|
+
}
|
|
267
|
+
return errs.join("");
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
it("exits NONZERO when the mutation's first read would fail for real", async () => {
|
|
271
|
+
// The structural false positive: this same call used to print a full plan
|
|
272
|
+
// and exit 0, while the run without --dry-run died on the missing file.
|
|
273
|
+
const missing = join(mkdtempSync(join(tmpdir(), "pragma-graft-")), "gone");
|
|
274
|
+
const out = await dispatchCapturing([missing], { dryRun: true });
|
|
275
|
+
expect(process.exitCode).toBe(1);
|
|
276
|
+
expect(out).toContain(missing);
|
|
277
|
+
expect(existsSync(`${missing}.grafted`)).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("still plans, exits 0, and writes nothing when the read would succeed", async () => {
|
|
281
|
+
const dir = mkdtempSync(join(tmpdir(), "pragma-graft-"));
|
|
282
|
+
const source = join(dir, "present.txt");
|
|
283
|
+
writeFileSync(source, "content\n");
|
|
284
|
+
const outs: string[] = [];
|
|
285
|
+
const spy = vi
|
|
286
|
+
.spyOn(process.stdout, "write")
|
|
287
|
+
.mockImplementation((chunk: string | Uint8Array) => {
|
|
288
|
+
outs.push(String(chunk));
|
|
289
|
+
return true;
|
|
290
|
+
});
|
|
291
|
+
process.exitCode = 0;
|
|
292
|
+
try {
|
|
293
|
+
await dispatch(graft, [source], { dryRun: true }, PLAIN);
|
|
294
|
+
} finally {
|
|
295
|
+
spy.mockRestore();
|
|
296
|
+
}
|
|
297
|
+
expect(process.exitCode).toBe(0);
|
|
298
|
+
expect(outs.join("")).toContain(`Write file: ${source}.grafted`);
|
|
299
|
+
expect(readdirSync(dir)).toEqual(["present.txt"]);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
236
303
|
describe("executeVerb — interactivity gate (H3)", () => {
|
|
237
304
|
const setTTY = (
|
|
238
305
|
stream: NodeJS.ReadStream | NodeJS.WriteStream,
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* exit code.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { describeEffect,
|
|
15
|
-
import { runTask, runUndo } from "@canonical/task/node";
|
|
14
|
+
import { describeEffect, type Task } from "@canonical/task";
|
|
15
|
+
import { runPreview, runTask, runUndo } from "@canonical/task/node";
|
|
16
16
|
import {
|
|
17
17
|
asPragmaError,
|
|
18
18
|
CANCELLED_MESSAGE,
|
|
@@ -221,8 +221,11 @@ export async function executeVerb(
|
|
|
221
221
|
// Also hand it the interaction context so an interactive verb can pick its
|
|
222
222
|
// prompt strategy. The verb's `run` sets `mutationRuntime.exec` (the runner
|
|
223
223
|
// options) as its last act; the projector reads it back on the real-run
|
|
224
|
-
// branch
|
|
225
|
-
//
|
|
224
|
+
// branch AND on the dry-run branch, which takes `cwd` (so the preview reads
|
|
225
|
+
// the tree the run would write into) and `onEffectStart` (so the stamping
|
|
226
|
+
// transform runs, and planned byte counts match written ones). It never
|
|
227
|
+
// takes the prompt handler: a preview auto-answers prompts and so can never
|
|
228
|
+
// block on input. `--undo` stays handler-free and untouched by this seam.
|
|
226
229
|
const controller = new AbortController();
|
|
227
230
|
const interaction: InteractionRuntime = {
|
|
228
231
|
// Gate on STDERR (H3): the Ink wizard renders to stderr and reads stdin,
|
|
@@ -252,14 +255,33 @@ export async function executeVerb(
|
|
|
252
255
|
| Promise<Task<unknown>>,
|
|
253
256
|
);
|
|
254
257
|
if (mutation.dryRun) {
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
// The HONEST preview (PR7): reads hit the real filesystem, writes are
|
|
259
|
+
// recorded and never executed. A mutation whose real run would die on its
|
|
260
|
+
// first template read now fails HERE too, so `--dry-run` exits nonzero
|
|
261
|
+
// exactly when the run would — the plan is a prediction, not a wish.
|
|
262
|
+
// `exec.cwd` is the same write root the real run resolves paths against,
|
|
263
|
+
// and `exec.onEffectStart` carries summon's stamping transform, so the
|
|
264
|
+
// planned byte counts are the bytes the run would actually write.
|
|
265
|
+
const previewExec = mutationRuntime.exec ?? {};
|
|
266
|
+
try {
|
|
267
|
+
const { effects } = await runPreview(task, {
|
|
268
|
+
cwd: previewExec.cwd,
|
|
269
|
+
onEffectStart: previewExec.onEffectStart,
|
|
270
|
+
onLog: logToStderr,
|
|
271
|
+
});
|
|
272
|
+
// A plan is the effects a mutation WOULD apply — a `Prompt` is not one,
|
|
273
|
+
// so the interactive confirm gate / answer prompts never clutter it.
|
|
274
|
+
return renderPlan(
|
|
275
|
+
flags,
|
|
276
|
+
effects
|
|
277
|
+
.filter((effect) => effect._tag !== "Prompt")
|
|
278
|
+
.map(describeEffect),
|
|
279
|
+
);
|
|
280
|
+
} finally {
|
|
281
|
+
// A verb that mounted an interactive session before returning its Task
|
|
282
|
+
// must be torn down on this branch too, exactly as on the real run.
|
|
283
|
+
await previewExec.dispose?.();
|
|
284
|
+
}
|
|
263
285
|
}
|
|
264
286
|
if (mutation.undo) {
|
|
265
287
|
const { undoCount } = await runUndo(task, { onLog: logToStderr });
|
|
@@ -17,7 +17,7 @@ const INDENT = " ";
|
|
|
17
17
|
/** Two-space gap between a term column and its description. */
|
|
18
18
|
const GAP = " ";
|
|
19
19
|
|
|
20
|
-
/** A bold section header (`Flags`, `Verbs`, `
|
|
20
|
+
/** A bold section header (`Flags`, `Verbs`, `Set up & maintain`). */
|
|
21
21
|
export function helpHeading(text: string): string {
|
|
22
22
|
return chalk.bold(text);
|
|
23
23
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The curated root `--help` page.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The kernel curates the nouns it ships — and only those. It names no domain:
|
|
5
|
+
* the nouns it does NOT curate lead the page untitled (the header one line up
|
|
6
|
+
* already carries the distribution's blurb) and describe themselves through
|
|
7
|
+
* their own verbs, so the front door follows the content instead of naming it.
|
|
8
|
+
* Every section is reconciled against the *live* nouns (from the registered
|
|
9
|
+
* verbs), so a noun that is not built yet is dropped and one the kernel has
|
|
10
|
+
* never heard of still surfaces.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import type { VerbSpec } from "../../spec/types.js";
|
|
@@ -24,88 +25,86 @@ interface NounSummary {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface HelpGroup {
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Absent for the leading uncurated group: its rows sit directly under the
|
|
30
|
+
* usage line, since repeating the header's blurb as a heading three lines
|
|
31
|
+
* below it says the same thing twice.
|
|
32
|
+
*/
|
|
33
|
+
readonly title?: string;
|
|
28
34
|
readonly nouns: readonly NounSummary[];
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
|
-
*
|
|
33
|
-
* people reach for most
|
|
34
|
-
*
|
|
38
|
+
* The kernel's own nouns, grouped by task with curated summaries. Order is
|
|
39
|
+
* intentional: what people reach for most comes first; agent tooling comes last.
|
|
40
|
+
*
|
|
41
|
+
* @param programName - The CLI binary name (the distribution's `name`).
|
|
42
|
+
* @returns The task-oriented groups, in display order.
|
|
35
43
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
noun: "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
noun
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
noun: "skill",
|
|
102
|
-
summary: "Browse agent skills from design-system packages",
|
|
103
|
-
},
|
|
104
|
-
{ noun: "prompt", summary: "Browse reusable prompt templates" },
|
|
105
|
-
{ noun: "mcp", summary: "Start the MCP server over stdio" },
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
];
|
|
44
|
+
function buildKernelGroups(programName: string): readonly HelpGroup[] {
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
title: "Generate code",
|
|
48
|
+
nouns: [
|
|
49
|
+
{ noun: "create", summary: "Scaffold components, packages, and apps" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: "Query & serve the graph",
|
|
54
|
+
nouns: [
|
|
55
|
+
{
|
|
56
|
+
noun: "graph",
|
|
57
|
+
summary: "Run SPARQL queries or inspect a URI directly",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
noun: "ontology",
|
|
61
|
+
summary: "Explore the loaded ontologies (classes, properties)",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "Set up & maintain",
|
|
67
|
+
nouns: [
|
|
68
|
+
{ noun: "doctor", summary: "Check environment health" },
|
|
69
|
+
{
|
|
70
|
+
noun: "setup",
|
|
71
|
+
summary: "Configure MCP, skills, completions, and the LSP",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
noun: "sources",
|
|
75
|
+
summary: "Build and refresh the local store from packs",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
noun: "config",
|
|
79
|
+
summary: `Read and write ${programName} configuration`,
|
|
80
|
+
},
|
|
81
|
+
{ noun: "info", summary: "Show version, config, and update status" },
|
|
82
|
+
{
|
|
83
|
+
noun: "upgrade",
|
|
84
|
+
summary: `Upgrade the ${programName} CLI to the latest version`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "For AI agents",
|
|
90
|
+
nouns: [
|
|
91
|
+
{
|
|
92
|
+
noun: "capabilities",
|
|
93
|
+
summary: "Discover conventions, tools, and the discovery sequence",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
noun: "colophon",
|
|
97
|
+
summary: `Read how ${programName} and the active domain are made`,
|
|
98
|
+
},
|
|
99
|
+
{ noun: "skill", summary: "Browse agent skills from the active packs" },
|
|
100
|
+
{ noun: "prompt", summary: "Browse reusable prompt templates" },
|
|
101
|
+
// The one noun with no verb to speak for it (the bin special-cases it),
|
|
102
|
+
// so its summary is hand-written rather than derived.
|
|
103
|
+
{ noun: "mcp", summary: "Start the MCP server over stdio" },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
109
108
|
|
|
110
109
|
/** All distinct, non-hidden top-level nouns present in the registered verbs. */
|
|
111
110
|
function nounsFrom(verbs: readonly VerbSpec[]): Set<string> {
|
|
@@ -117,16 +116,21 @@ function nounsFrom(verbs: readonly VerbSpec[]): Set<string> {
|
|
|
117
116
|
return nouns;
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Summarise an uncurated noun from its first live verb — which is the pack's own
|
|
121
|
+
* `description` whenever the pack ships a `list` (`compilePack` compiles that
|
|
122
|
+
* verb first). The terminal period a pack sentence carries is dropped so the
|
|
123
|
+
* column reads as one voice with the kernel's own fragments.
|
|
124
|
+
*/
|
|
125
|
+
function summarizeNoun(noun: string, verbs: readonly VerbSpec[]): string {
|
|
122
126
|
const first = verbs.find((v) => v.path[0] === noun && !v.hidden);
|
|
123
|
-
return first?.summary ?? `${noun} commands
|
|
127
|
+
return (first?.summary ?? `${noun} commands`).replace(/\.$/, "");
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
131
|
* Build the curated root help string.
|
|
128
132
|
*
|
|
129
|
-
* @param programName - The CLI binary name (`
|
|
133
|
+
* @param programName - The CLI binary name (the distribution's `name`).
|
|
130
134
|
* @param description - The program description shown in the header.
|
|
131
135
|
* @param verbs - All registered verbs, used to derive the live noun set.
|
|
132
136
|
* @returns The formatted, colorized help text.
|
|
@@ -140,27 +144,23 @@ export function formatRootHelp(
|
|
|
140
144
|
// `mcp` is served by the bin's special-case, not a projected verb, but is
|
|
141
145
|
// always available — surface it so the front door is complete.
|
|
142
146
|
present.add("mcp");
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
);
|
|
147
|
+
const kernel = buildKernelGroups(programName);
|
|
148
|
+
const curated = new Set(kernel.flatMap((g) => g.nouns.map((n) => n.noun)));
|
|
146
149
|
|
|
147
|
-
const uncatalogued = [...present].filter((n) => !catalogued.has(n)).sort();
|
|
148
150
|
const groups: HelpGroup[] = [
|
|
149
|
-
|
|
151
|
+
// Everything the kernel's curated table does not claim — in the shipped
|
|
152
|
+
// binary the bundled domain packs, in a fork whatever it ships. Untitled
|
|
153
|
+
// and first, so the domain leads the page without the kernel naming it.
|
|
154
|
+
{
|
|
155
|
+
nouns: [...present]
|
|
156
|
+
.filter((n) => !curated.has(n))
|
|
157
|
+
.sort()
|
|
158
|
+
.map((noun) => ({ noun, summary: summarizeNoun(noun, verbs) })),
|
|
159
|
+
},
|
|
160
|
+
...kernel.map((g) => ({
|
|
150
161
|
...g,
|
|
151
162
|
nouns: g.nouns.filter((n) => present.has(n.noun)),
|
|
152
163
|
})),
|
|
153
|
-
...(uncatalogued.length > 0
|
|
154
|
-
? [
|
|
155
|
-
{
|
|
156
|
-
title: "Other",
|
|
157
|
-
nouns: uncatalogued.map((noun) => ({
|
|
158
|
-
noun,
|
|
159
|
-
summary: fallbackSummary(noun, verbs),
|
|
160
|
-
})),
|
|
161
|
-
},
|
|
162
|
-
]
|
|
163
|
-
: []),
|
|
164
164
|
].filter((g) => g.nouns.length > 0);
|
|
165
165
|
|
|
166
166
|
// One column width across ALL groups so the noun column aligns section to
|
|
@@ -180,7 +180,7 @@ export function formatRootHelp(
|
|
|
180
180
|
];
|
|
181
181
|
|
|
182
182
|
for (const group of groups) {
|
|
183
|
-
lines.push(helpHeading(group.title));
|
|
183
|
+
if (group.title) lines.push(helpHeading(group.title));
|
|
184
184
|
lines.push(
|
|
185
185
|
...helpColumns(
|
|
186
186
|
group.nouns.map((n) => [n.noun, n.summary] as const),
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build the MCP server from the grammar.
|
|
3
3
|
*
|
|
4
|
-
* Constructs an {@link McpServer}
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Constructs an {@link McpServer} whose wire identity PROJECTS from the
|
|
5
|
+
* distribution: `serverInfo.name` is the declared distribution name (the same
|
|
6
|
+
* projection that names the CLI bin, so existing agent wiring resolves it) and
|
|
7
|
+
* `serverInfo.version` the package version — a fork's server introduces itself
|
|
8
|
+
* under the fork's own name with no code change. The covenant records the rule
|
|
9
|
+
* (`surface.v2.json` `mcpSurface.serverInfo`) and `identity.test.ts` pins it on
|
|
10
|
+
* the wire from a fork's config. Then registers every exposed verb as a tool
|
|
11
|
+
* and each module's optional `mcpResources` surface (the graph resource
|
|
12
|
+
* browser, wired below via `module.mcpResources?.register`). One runtime is
|
|
13
|
+
* booted for the server's lifetime and shared by every tool; the store stays
|
|
14
|
+
* lazy, booting only when a resource read needs it.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* It carries the same content the retired `llm` tool front-loaded: the
|
|
6
6
|
* conventions + the discovery sequence. To guarantee it never diverges from the
|
|
7
|
-
* `capabilities` tool,
|
|
7
|
+
* `capabilities` tool, the WHOLE orientation — the opening line included —
|
|
8
|
+
* derives from the SAME source: `CONVENTIONS` and
|
|
8
9
|
* `buildDiscoverySequence` in `capabilities/capabilities/catalog.ts`. Live
|
|
9
10
|
* numbers (tier/channel/entity counts) are deliberately DROPPED (they would need
|
|
10
11
|
* a store boot at handshake); agents fetch those via `info`/`config show`/
|
|
@@ -15,8 +16,8 @@
|
|
|
15
16
|
import {
|
|
16
17
|
buildDiscoverySequence,
|
|
17
18
|
CONVENTIONS,
|
|
18
|
-
liveTools,
|
|
19
19
|
} from "../../../capabilities/capabilities/catalog.js";
|
|
20
|
+
import { emitSurface } from "../../spec/emitSurface.js";
|
|
20
21
|
import type { CapabilityModule } from "../../spec/types.js";
|
|
21
22
|
|
|
22
23
|
/** Hard ceiling on the instructions length (asserted by the protected test). */
|
|
@@ -31,18 +32,22 @@ export const INSTRUCTIONS_MAX_CHARS = 1500;
|
|
|
31
32
|
export function buildInstructions(
|
|
32
33
|
modules: readonly CapabilityModule[],
|
|
33
34
|
): string {
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Tools AND resource templates come from the one emitted surface: the
|
|
36
|
+
// `<scheme>:{+uri}` template is declared by the module that serves it and
|
|
37
|
+
// frozen in the covenant, so quoting it here rather than deriving it would be
|
|
38
|
+
// a second copy of a string the kernel does not own.
|
|
39
|
+
const { tools, resources } = emitSurface(modules).mcpSurface;
|
|
40
|
+
const steps = buildDiscoverySequence(tools)
|
|
36
41
|
.map((stage) => `${stage.stage}. ${stage.tool} — ${stage.purpose}`)
|
|
37
42
|
.join("\n");
|
|
43
|
+
const templates = resources.map((template) => `\`${template}\``).join(", ");
|
|
38
44
|
|
|
39
45
|
return [
|
|
40
|
-
"pragma is a CLI and MCP server over a design-system knowledge graph.",
|
|
41
46
|
`${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying} ${CONVENTIONS.mutations}`,
|
|
42
47
|
"",
|
|
43
48
|
"Discovery sequence:",
|
|
44
49
|
steps,
|
|
45
50
|
"",
|
|
46
|
-
|
|
51
|
+
`Call the \`capabilities\` tool for the full annotated tool catalog; read ${templates} resources (or \`graph_inspect\`) for entity detail.`,
|
|
47
52
|
].join("\n");
|
|
48
53
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { describe, expect, it } from "vitest";
|
|
3
5
|
import {
|
|
4
6
|
fixtureEffectsModule,
|
|
7
|
+
fixturePreviewModule,
|
|
5
8
|
touchPath,
|
|
6
9
|
} from "../../../testing/fixtures/fixtureCapability.js";
|
|
7
10
|
import { projectMcp } from "../../../testing/helpers/projectMcp.js";
|
|
@@ -70,3 +73,38 @@ describe("MCP mutation plan-first / confirm (PROTECTED)", () => {
|
|
|
70
73
|
expect(existsSync(touchPath(name))).toBe(true);
|
|
71
74
|
});
|
|
72
75
|
});
|
|
76
|
+
|
|
77
|
+
describe("MCP plan-first is honest (PRA-104)", () => {
|
|
78
|
+
it("returns the ERROR a confirmed run would hit, not a confident plan", async () => {
|
|
79
|
+
// The CLI twin of this guard lives in cli/dispatch.test.ts: the same
|
|
80
|
+
// mutation, the same missing source, exiting nonzero instead of printing a
|
|
81
|
+
// plan. Both surfaces run one interpreter, so they cannot drift.
|
|
82
|
+
const dir = mkdtempSync(join(tmpdir(), "pragma-graft-mcp-"));
|
|
83
|
+
const missing = join(dir, "gone");
|
|
84
|
+
|
|
85
|
+
const mcp = await projectMcp([fixturePreviewModule], dir);
|
|
86
|
+
const planned = await mcp.callTool("probe_graft", { source: missing });
|
|
87
|
+
await mcp.cleanup();
|
|
88
|
+
|
|
89
|
+
expect(planned.ok).toBe(false);
|
|
90
|
+
expect(JSON.stringify(planned.error)).toContain(missing);
|
|
91
|
+
expect(existsSync(`${missing}.grafted`)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns a plan, and writes nothing, when the read would succeed", async () => {
|
|
95
|
+
const dir = mkdtempSync(join(tmpdir(), "pragma-graft-mcp-"));
|
|
96
|
+
const source = join(dir, "present.txt");
|
|
97
|
+
writeFileSync(source, "content\n");
|
|
98
|
+
|
|
99
|
+
const mcp = await projectMcp([fixturePreviewModule], dir);
|
|
100
|
+
const planned = await mcp.callTool("probe_graft", { source });
|
|
101
|
+
await mcp.cleanup();
|
|
102
|
+
|
|
103
|
+
expect(planned.ok).toBe(true);
|
|
104
|
+
expect(planned.meta).toEqual({ planOnly: true, confirmRequired: true });
|
|
105
|
+
expect((planned.data as { plan: string[] }).plan).toContain(
|
|
106
|
+
`Write file: ${source}.grafted (8 bytes)`,
|
|
107
|
+
);
|
|
108
|
+
expect(readdirSync(dir)).toEqual(["present.txt"]);
|
|
109
|
+
});
|
|
110
|
+
});
|