@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
|
@@ -3,6 +3,7 @@ import { afterAll, describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { VERSION } from "../../../constants.js";
|
|
4
4
|
import { fixtureModule } from "../../../testing/fixtures/fixtureCapability.js";
|
|
5
5
|
import { projectCli } from "../../../testing/helpers/projectCli.js";
|
|
6
|
+
import type { GlobalFlags } from "../../runtime/index.js";
|
|
6
7
|
import type { CapabilityModule, VerbSpec } from "../../spec/types.js";
|
|
7
8
|
import { formatRootHelp } from "./rootHelp.js";
|
|
8
9
|
|
|
@@ -486,8 +487,26 @@ describe("buildProgram — sub-verb-only noun invoked bare (U2)", () => {
|
|
|
486
487
|
});
|
|
487
488
|
});
|
|
488
489
|
|
|
489
|
-
|
|
490
|
-
|
|
490
|
+
/** The global flags of a human at a terminal: plain output, nothing inferred. */
|
|
491
|
+
const HUMAN_FLAGS: GlobalFlags = {
|
|
492
|
+
llm: false,
|
|
493
|
+
format: "plain",
|
|
494
|
+
verbose: false,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/** A run whose stdout is captured: `llm`, inferred or asked for. */
|
|
498
|
+
const CAPTURED_FLAGS: GlobalFlags = {
|
|
499
|
+
llm: true,
|
|
500
|
+
autoLlm: true,
|
|
501
|
+
format: "plain",
|
|
502
|
+
verbose: false,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/** The first line of the wordmark art — present iff the wordmark printed. */
|
|
506
|
+
const WORDMARK_LINE = "(___,";
|
|
507
|
+
|
|
508
|
+
const rootHelpFor = (flags: GlobalFlags): string =>
|
|
509
|
+
formatRootHelp(
|
|
491
510
|
"pragma",
|
|
492
511
|
"pragma test",
|
|
493
512
|
[
|
|
@@ -497,8 +516,60 @@ describe("formatRootHelp — grouping", () => {
|
|
|
497
516
|
],
|
|
498
517
|
"1.2.3",
|
|
499
518
|
"https://example.test/issues",
|
|
519
|
+
flags,
|
|
500
520
|
);
|
|
501
521
|
|
|
522
|
+
describe("formatRootHelp — the wordmark is gated on the AUDIENCE", () => {
|
|
523
|
+
// Asserted on the ART, never on styling: `golden.ts` strips ANSI and this
|
|
524
|
+
// suite pins `chalk.level = 0`, so a colour-only change would be invisible
|
|
525
|
+
// here — and chalk emits no escapes off a TTY anyway, so it would save an
|
|
526
|
+
// agent nothing. The lines have to go.
|
|
527
|
+
it("prints the art for a human reading plain output", () => {
|
|
528
|
+
expect(rootHelpFor(HUMAN_FLAGS)).toContain(WORDMARK_LINE);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("drops the art — every line of it — when stdout is captured", () => {
|
|
532
|
+
const help = rootHelpFor(CAPTURED_FLAGS);
|
|
533
|
+
expect(help).not.toContain(WORDMARK_LINE);
|
|
534
|
+
// No leading blank line either: the header IS the first line.
|
|
535
|
+
expect(help.startsWith("pragma v1.2.3 — pragma test")).toBe(true);
|
|
536
|
+
// Only the art goes; the page itself is untouched.
|
|
537
|
+
expect(help).toContain("Usage: pragma <command> [subcommand] [flags]");
|
|
538
|
+
expect(help).toContain("Global flags");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("drops the art for `--format json` too", () => {
|
|
542
|
+
expect(
|
|
543
|
+
rootHelpFor({ llm: false, format: "json", verbose: false }),
|
|
544
|
+
).not.toContain(WORDMARK_LINE);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("keeps the art for an EXPLICIT `--format plain` down a pipe", () => {
|
|
548
|
+
// Explicit beats inference: asking for the human shape gets the human page,
|
|
549
|
+
// captured or not. (`parseGlobalFlags` leaves `llm` false whenever
|
|
550
|
+
// `--format` was requested.)
|
|
551
|
+
expect(
|
|
552
|
+
rootHelpFor({
|
|
553
|
+
llm: false,
|
|
554
|
+
autoLlm: false,
|
|
555
|
+
format: "plain",
|
|
556
|
+
verbose: false,
|
|
557
|
+
}),
|
|
558
|
+
).toContain(WORDMARK_LINE);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("differs ONLY by the wordmark block", () => {
|
|
562
|
+
const human = rootHelpFor(HUMAN_FLAGS).split("\n");
|
|
563
|
+
const captured = rootHelpFor(CAPTURED_FLAGS).split("\n");
|
|
564
|
+
// 10 art lines + the blank that separates them from the header.
|
|
565
|
+
expect(human.length - captured.length).toBe(11);
|
|
566
|
+
expect(human.slice(11)).toEqual(captured);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("formatRootHelp — grouping", () => {
|
|
571
|
+
const help = rootHelpFor(HUMAN_FLAGS);
|
|
572
|
+
|
|
502
573
|
it("leads with the uncurated nouns, untitled, under the usage line", () => {
|
|
503
574
|
// `block` is not in the kernel's curated table, so it leads the page with
|
|
504
575
|
// no heading of its own — the header already carries the blurb, and the
|
|
@@ -298,7 +298,14 @@ export function buildProgram(
|
|
|
298
298
|
program.enablePositionalOptions();
|
|
299
299
|
program.exitOverride();
|
|
300
300
|
useDesignedHelp(program, () =>
|
|
301
|
-
formatRootHelp(
|
|
301
|
+
formatRootHelp(
|
|
302
|
+
programName,
|
|
303
|
+
description,
|
|
304
|
+
live,
|
|
305
|
+
version,
|
|
306
|
+
issuesUrl,
|
|
307
|
+
options.globalFlags,
|
|
308
|
+
),
|
|
302
309
|
);
|
|
303
310
|
|
|
304
311
|
const groups = new Map<string, VerbSpec[]>();
|
|
@@ -5,10 +5,11 @@ import { $, fail, gen, log, mkdir, succeed, writeFile } from "@canonical/task";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { fixturePreviewModule } from "../../../testing/fixtures/fixtureCapability.js";
|
|
7
7
|
import { PragmaError } from "../../error/PragmaError.js";
|
|
8
|
+
import { canPrompt } from "../../interactivity.js";
|
|
8
9
|
import { bootRuntime } from "../../runtime/boot.js";
|
|
9
10
|
import type { GlobalFlags } from "../../runtime/types.js";
|
|
10
11
|
import type { ParamSpec, VerbSpec } from "../../spec/types.js";
|
|
11
|
-
import {
|
|
12
|
+
import { dispatch, executeVerb, extractParams } from "./dispatch.js";
|
|
12
13
|
|
|
13
14
|
/** The read-then-write mutation the honest-preview guards drive. */
|
|
14
15
|
const graft = fixturePreviewModule.verbs[0] as VerbSpec;
|
|
@@ -218,7 +219,7 @@ describe("--quiet — success is silent, failure is not", () => {
|
|
|
218
219
|
formatters: {
|
|
219
220
|
...passthroughFormatters,
|
|
220
221
|
plain: () => "",
|
|
221
|
-
|
|
222
|
+
notice: () => "No results.",
|
|
222
223
|
},
|
|
223
224
|
},
|
|
224
225
|
run: async () => [],
|
|
@@ -491,7 +492,7 @@ describe("executeVerb — interactivity gate (H3)", () => {
|
|
|
491
492
|
expect(sink.isTTY).toBe(true);
|
|
492
493
|
});
|
|
493
494
|
|
|
494
|
-
it("
|
|
495
|
+
it("canPrompt IS the stdin+stderr conjunction — the one gate both callers read", () => {
|
|
495
496
|
// The mount's create decision reads the same exported function this
|
|
496
497
|
// describe drives through executeVerb, so pinning the truth table here
|
|
497
498
|
// pins BOTH callers (nothing is left to keep two copies in step).
|
|
@@ -506,7 +507,7 @@ describe("executeVerb — interactivity gate (H3)", () => {
|
|
|
506
507
|
setTTY(process.stdin, stdin);
|
|
507
508
|
setTTY(process.stderr, stderr);
|
|
508
509
|
setTTY(process.stdout, stdout);
|
|
509
|
-
expect(
|
|
510
|
+
expect(canPrompt(), `stdin=${stdin} stderr=${stderr}`).toBe(expected);
|
|
510
511
|
}
|
|
511
512
|
});
|
|
512
513
|
});
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
renderErrorLlm,
|
|
26
26
|
renderErrorPlain,
|
|
27
27
|
} from "../../error/index.js";
|
|
28
|
+
import { canPrompt, stdoutIsCaptured } from "../../interactivity.js";
|
|
28
29
|
import type { RenderContext } from "../../render/contracts.js";
|
|
29
30
|
import { successEnvelope, writeStdout } from "../../render/index.js";
|
|
30
31
|
import { bootRuntime } from "../../runtime/index.js";
|
|
@@ -43,18 +44,6 @@ export interface MutationFlags {
|
|
|
43
44
|
readonly yes: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/**
|
|
47
|
-
* THE CLI interactivity gate (H3): stdin AND stderr are TTYs. The Ink wizard
|
|
48
|
-
* renders to stderr and reads stdin, so `<verb> 2>/dev/null` must be
|
|
49
|
-
* non-interactive — gating on stdout would mount an invisible render that
|
|
50
|
-
* blocks on stdin. One exported fact: the kernel's interaction context and a
|
|
51
|
-
* mounted subtree's own decision both read it here, so the two callers can
|
|
52
|
-
* never disagree about what "a TTY" means.
|
|
53
|
-
*/
|
|
54
|
-
export function cliIsTTY(): boolean {
|
|
55
|
-
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
47
|
/**
|
|
59
48
|
* Route a Task's log effects to stderr. The interpreter otherwise falls back to
|
|
60
49
|
* `console.log` (stdout), which would corrupt the `--format json` / MCP stdio
|
|
@@ -171,7 +160,7 @@ export function extractParams(
|
|
|
171
160
|
* pipe would read as a record. `llm` and `json` keep their own empty shapes
|
|
172
161
|
* on stdout: both are machine contracts whose consumers read one stream.
|
|
173
162
|
*
|
|
174
|
-
* @note Impure — reads
|
|
163
|
+
* @note Impure — reads stdout's capture state for the render context.
|
|
175
164
|
*/
|
|
176
165
|
function renderData(
|
|
177
166
|
verb: VerbSpec,
|
|
@@ -181,8 +170,17 @@ function renderData(
|
|
|
181
170
|
): DispatchOutcome {
|
|
182
171
|
if (flags.format === "json") {
|
|
183
172
|
const projection = JSON.parse(verb.output.formatters.json(data));
|
|
173
|
+
// The calm notice rides `meta` on the machine surfaces, so an empty result
|
|
174
|
+
// is distinguishable from an unbuilt store or a mistyped filter, and an
|
|
175
|
+
// ambiguous lookup hit from an unambiguous one. `data` keeps its uniform
|
|
176
|
+
// shape, and MCP builds the same key from the same seam
|
|
177
|
+
// (`mcp/registerVerb.ts#noticeMeta`) — the two machine surfaces stay
|
|
178
|
+
// byte-equal.
|
|
179
|
+
const notice = verb.output.formatters.notice?.(data);
|
|
184
180
|
return {
|
|
185
|
-
stdout: `${JSON.stringify(
|
|
181
|
+
stdout: `${JSON.stringify(
|
|
182
|
+
successEnvelope(projection, notice ? { ...meta, notice } : meta),
|
|
183
|
+
)}\n`,
|
|
186
184
|
exitCode: 0,
|
|
187
185
|
};
|
|
188
186
|
}
|
|
@@ -192,14 +190,12 @@ function renderData(
|
|
|
192
190
|
}
|
|
193
191
|
const context: RenderContext = {
|
|
194
192
|
headers: flags.noHeaders !== true,
|
|
195
|
-
stdoutIsTty:
|
|
193
|
+
stdoutIsTty: !stdoutIsCaptured(),
|
|
196
194
|
};
|
|
197
195
|
const text = verb.output.formatters.plain(data, context);
|
|
198
|
-
// The calm
|
|
196
|
+
// The calm notice is success-path guidance — `--quiet` mutes it.
|
|
199
197
|
const notice =
|
|
200
|
-
flags.quiet === true
|
|
201
|
-
? undefined
|
|
202
|
-
: verb.output.formatters.emptyNotice?.(data);
|
|
198
|
+
flags.quiet === true ? undefined : verb.output.formatters.notice?.(data);
|
|
203
199
|
return {
|
|
204
200
|
stdout: text ? `${text}\n` : "",
|
|
205
201
|
...(notice ? { stderr: `${notice}\n` } : {}),
|
|
@@ -332,8 +328,8 @@ export async function executeVerb(
|
|
|
332
328
|
// block on input. `--undo` stays handler-free and untouched by this seam.
|
|
333
329
|
const controller = new AbortController();
|
|
334
330
|
const interaction: InteractionRuntime = {
|
|
335
|
-
// The shared H3 gate (see
|
|
336
|
-
isTTY:
|
|
331
|
+
// The shared H3 gate (see `canPrompt`): stdin and stderr, never stdout.
|
|
332
|
+
isTTY: canPrompt(),
|
|
337
333
|
transport: "cli",
|
|
338
334
|
yes: mutation.yes,
|
|
339
335
|
signal: controller.signal,
|
|
@@ -420,17 +416,22 @@ export async function executeVerb(
|
|
|
420
416
|
}
|
|
421
417
|
// Real execution: spread the verb's runner options into the node
|
|
422
418
|
// interpreter (prompt handler, stamping/progress callbacks, log routing,
|
|
423
|
-
// signal). Teardown (e.g. unmount an Ink render) runs in `finally
|
|
419
|
+
// signal). Teardown (e.g. unmount an Ink render) runs in `finally` —
|
|
420
|
+
// BEFORE the outcome is rendered: an interactive session's closing frame
|
|
421
|
+
// flushes on its dispose, so printing the result first put the verb's
|
|
422
|
+
// stdout recap ABOVE a frame that then repainted stale beneath it. Tear
|
|
423
|
+
// the UI down, then report; the error path gets the same order for free.
|
|
424
424
|
const exec = mutationRuntime.exec ?? {};
|
|
425
425
|
const onSigint = (): void => controller.abort();
|
|
426
426
|
process.once("SIGINT", onSigint);
|
|
427
|
+
let value: unknown;
|
|
427
428
|
try {
|
|
428
|
-
|
|
429
|
-
return renderData(verb, flags, value, {});
|
|
429
|
+
value = await runTask(task, { onLog: logSink(flags), ...exec });
|
|
430
430
|
} finally {
|
|
431
431
|
process.removeListener("SIGINT", onSigint);
|
|
432
432
|
await exec.dispose?.();
|
|
433
433
|
}
|
|
434
|
+
return renderData(verb, flags, value, {});
|
|
434
435
|
}
|
|
435
436
|
|
|
436
437
|
const data = await Promise.resolve(
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { DETAIL_LEVELS, type DetailLevel } from "../../../constants.js";
|
|
28
|
+
import { stdoutIsCaptured } from "../../interactivity.js";
|
|
28
29
|
import type { GlobalFlags } from "../../runtime/index.js";
|
|
29
30
|
|
|
30
31
|
/** The end-of-options separator: nothing after it is a flag of this program. */
|
|
@@ -153,7 +154,9 @@ export interface OutputEnvironment {
|
|
|
153
154
|
/** Read the real stdout/env output environment. @note Impure — reads process. */
|
|
154
155
|
function readOutputEnvironment(): OutputEnvironment {
|
|
155
156
|
return {
|
|
156
|
-
|
|
157
|
+
// The OUTPUT-SHAPE question, on stdout — not the prompting one. A run whose
|
|
158
|
+
// stdout is captured may still have a terminal to prompt on (`canPrompt`).
|
|
159
|
+
isTty: !stdoutIsCaptured(),
|
|
157
160
|
noAutoLlm: Boolean(process.env.PRAGMA_NO_AUTO_LLM),
|
|
158
161
|
};
|
|
159
162
|
}
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import chalk from "chalk";
|
|
14
|
+
import { INDENT } from "../../render/vocabulary.js";
|
|
14
15
|
|
|
15
|
-
/** Two-space indent for every list body row. */
|
|
16
|
-
const INDENT = " ";
|
|
17
16
|
/** Two-space gap between a term column and its description. */
|
|
18
17
|
const GAP = " ";
|
|
19
18
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { PROGRAM_LOGO } from "../../../constants.js";
|
|
14
|
+
import type { GlobalFlags } from "../../runtime/index.js";
|
|
14
15
|
import type { VerbSpec } from "../../spec/index.js";
|
|
15
16
|
import {
|
|
16
17
|
helpColumns,
|
|
@@ -132,6 +133,27 @@ function summarizeNoun(noun: string, verbs: readonly VerbSpec[]): string {
|
|
|
132
133
|
return (first?.summary ?? `${noun} commands`).replace(/\.$/, "");
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Whether this reader is worth spending the wordmark on.
|
|
138
|
+
*
|
|
139
|
+
* An AUDIENCE question, not a data question: the art is 11 lines and ~200
|
|
140
|
+
* tokens — roughly a third of what `--help` costs an agent — and it says
|
|
141
|
+
* nothing an agent can act on. Both captured shapes are excluded:
|
|
142
|
+
* `llm` (requested, or inferred from a non-interactive stdout — the shape an
|
|
143
|
+
* agent captures) and `json`. `--format plain` therefore keeps the art even
|
|
144
|
+
* down a pipe, and `--format llm` drops it even on a terminal: explicit beats
|
|
145
|
+
* inference, the same inferred-vs-explicit split `renderErrorForFormat` uses.
|
|
146
|
+
*
|
|
147
|
+
* The DECISION is passed in as data — both call sites already hold the parsed
|
|
148
|
+
* flags — so this module still reads no `process` state and stays testable.
|
|
149
|
+
*
|
|
150
|
+
* @param flags - The invocation's global flags.
|
|
151
|
+
* @returns `true` when a human is reading and the wordmark should print.
|
|
152
|
+
*/
|
|
153
|
+
function wordmarkSuitsReader(flags: GlobalFlags): boolean {
|
|
154
|
+
return flags.llm !== true && flags.format !== "json";
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
/**
|
|
136
158
|
* Build the curated root help string.
|
|
137
159
|
*
|
|
@@ -144,6 +166,9 @@ function summarizeNoun(noun: string, verbs: readonly VerbSpec[]): string {
|
|
|
144
166
|
* read here so it is the SAME string `--version` prints: `buildProgram`
|
|
145
167
|
* resolves `options.version ?? VERSION`, and a host that overrides one must
|
|
146
168
|
* not be able to leave the other saying something else.
|
|
169
|
+
* @param globalFlags - The invocation's global flags, read only to decide who
|
|
170
|
+
* is reading (see {@link wordmarkSuitsReader}). Passed rather than probed: the
|
|
171
|
+
* help layer must not learn to read `process`.
|
|
147
172
|
* @returns The formatted, colorized help text.
|
|
148
173
|
*/
|
|
149
174
|
export function formatRootHelp(
|
|
@@ -152,6 +177,7 @@ export function formatRootHelp(
|
|
|
152
177
|
verbs: readonly VerbSpec[],
|
|
153
178
|
version: string,
|
|
154
179
|
issuesUrl: string,
|
|
180
|
+
globalFlags: GlobalFlags,
|
|
155
181
|
): string {
|
|
156
182
|
const present = nounsFrom(verbs);
|
|
157
183
|
const kernel = buildKernelGroups(programName);
|
|
@@ -181,9 +207,16 @@ export function formatRootHelp(
|
|
|
181
207
|
);
|
|
182
208
|
|
|
183
209
|
const lines: string[] = [
|
|
184
|
-
// The wordmark leads when the distribution declares one
|
|
185
|
-
// own art or none, so its front door is never
|
|
186
|
-
|
|
210
|
+
// The wordmark leads when the distribution declares one AND a human is
|
|
211
|
+
// reading it. A fork ships its own art or none, so its front door is never
|
|
212
|
+
// branded with someone else's; and a captured run drops the art entirely
|
|
213
|
+
// rather than charging an agent ~200 tokens for a picture of the name it
|
|
214
|
+
// just typed (see `wordmarkSuitsReader`). The LINES go, not their colour:
|
|
215
|
+
// chalk already emits no escapes off a TTY, so dimming would save nobody
|
|
216
|
+
// anything.
|
|
217
|
+
...(PROGRAM_LOGO.length > 0 && wordmarkSuitsReader(globalFlags)
|
|
218
|
+
? [...PROGRAM_LOGO.map(helpLogo), ""]
|
|
219
|
+
: []),
|
|
187
220
|
// `<name> v<version> — <blurb>`, the spelling `info` and `capabilities`
|
|
188
221
|
// already use, so the front door names the build the same way every other
|
|
189
222
|
// surface does. Dimmed: it answers "which build am I on" for someone who
|
|
@@ -143,6 +143,22 @@ function withDetail(
|
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The notice seam, projected into the envelope's `meta`.
|
|
148
|
+
*
|
|
149
|
+
* It existed with exactly ONE consumer: the CLI dispatcher, which writes it to
|
|
150
|
+
* stderr. An agent therefore could not tell an unbuilt store, a mistyped filter
|
|
151
|
+
* and a genuinely empty result apart — all three were
|
|
152
|
+
* `{"ok":true,"data":[],"meta":{}}` — and, later, could not tell an unambiguous
|
|
153
|
+
* lookup hit from one of three blocks sharing a name. Riding in `meta` keeps
|
|
154
|
+
* `data` its uniform shape while making both of those ≠ silence, and the CLI's
|
|
155
|
+
* `--format json` carries the same key so the two surfaces stay byte-equal.
|
|
156
|
+
*/
|
|
157
|
+
function noticeMeta(verb: VerbSpec, data: unknown): Record<string, unknown> {
|
|
158
|
+
const notice = verb.output.formatters.notice?.(data as never);
|
|
159
|
+
return notice ? { notice } : {};
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
/** The tool handler for a read verb: run, project, envelope. */
|
|
147
163
|
function readHandler(verb: VerbSpec, runtime: PragmaRuntime) {
|
|
148
164
|
return async (args: Record<string, unknown>): Promise<CallToolResult> => {
|
|
@@ -152,7 +168,10 @@ function readHandler(verb: VerbSpec, runtime: PragmaRuntime) {
|
|
|
152
168
|
const result = await Promise.resolve(
|
|
153
169
|
verb.run(params, withDetail(verb, runtime, args)) as Promise<unknown>,
|
|
154
170
|
);
|
|
155
|
-
return toolSuccess(
|
|
171
|
+
return toolSuccess(
|
|
172
|
+
JSON.parse(verb.output.formatters.json(result)),
|
|
173
|
+
noticeMeta(verb, result),
|
|
174
|
+
);
|
|
156
175
|
} catch (error) {
|
|
157
176
|
return toolError(asPragmaError(error));
|
|
158
177
|
}
|
|
@@ -85,7 +85,7 @@ function emptyBody<T>(options: RenderListOptions<T>): string {
|
|
|
85
85
|
* The empty-list notice a dispatcher routes to STDERR (exit 0): zero rows is
|
|
86
86
|
* a calm success, and stdout — the data stream — must not carry a human
|
|
87
87
|
* sentence a pipe would read as a record. Exported for the formatter seam's
|
|
88
|
-
* `
|
|
88
|
+
* `notice`; the plain renderer itself no longer prints it.
|
|
89
89
|
*/
|
|
90
90
|
export function renderListEmptyNotice<T>(
|
|
91
91
|
options: RenderListOptions<T>,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import chalk from "chalk";
|
|
15
|
+
import { stdoutIsCaptured } from "../interactivity.js";
|
|
15
16
|
|
|
16
17
|
/** The palette + gate a renderer consults to beautify the human path. */
|
|
17
18
|
export interface RenderStyle {
|
|
@@ -27,6 +28,8 @@ export interface RenderStyle {
|
|
|
27
28
|
green(text: string): string;
|
|
28
29
|
/** Tint a status that wants attention. */
|
|
29
30
|
yellow(text: string): string;
|
|
31
|
+
/** Tint a real fault — a failed check or a failed row, nothing milder. */
|
|
32
|
+
red(text: string): string;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** The identity styler — every function returns its input unchanged. */
|
|
@@ -37,6 +40,7 @@ const PLAIN_STYLE: RenderStyle = {
|
|
|
37
40
|
cyan: (text) => text,
|
|
38
41
|
green: (text) => text,
|
|
39
42
|
yellow: (text) => text,
|
|
43
|
+
red: (text) => text,
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -55,6 +59,7 @@ export function styleFor(enabled: boolean): RenderStyle {
|
|
|
55
59
|
cyan: (text) => chalk.cyan(text),
|
|
56
60
|
green: (text) => chalk.green(text),
|
|
57
61
|
yellow: (text) => chalk.yellow(text),
|
|
62
|
+
red: (text) => chalk.red(text),
|
|
58
63
|
};
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -75,8 +80,8 @@ export function forbidsColor(env: NodeJS.ProcessEnv): boolean {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// Applied to the SHARED chalk instance at module load, not inside
|
|
78
|
-
// `defaultStyle()`, because
|
|
79
|
-
// through `RenderStyle` — help formatting,
|
|
83
|
+
// `defaultStyle()`, because three modules reach for `chalk` directly rather
|
|
84
|
+
// than through `RenderStyle` — help formatting, upgrade and the colophon's
|
|
80
85
|
// Markdown renderer. Gating only the style seam would leave every one of them
|
|
81
86
|
// still emitting escapes. Zeroing the level once covers them by construction,
|
|
82
87
|
// including any site added later.
|
|
@@ -87,18 +92,36 @@ export function forbidsColor(env: NodeJS.ProcessEnv): boolean {
|
|
|
87
92
|
// worked.
|
|
88
93
|
if (forbidsColor(process.env)) chalk.level = 0;
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Whether stdout may carry color: an attended terminal AND a usable chalk
|
|
97
|
+
* level (zeroed by the module-load check above under `NO_COLOR`).
|
|
98
|
+
*
|
|
99
|
+
* The third question of the interactivity vocabulary (`kernel/interactivity.ts`
|
|
100
|
+
* holds the other two, and its docblock says why this one lives here: it needs
|
|
101
|
+
* `chalk`, and that module is on the flag-parsing fast path). Both signals are
|
|
102
|
+
* required — `supports-color` enables color off a TTY in CI, which would paint
|
|
103
|
+
* escapes into a redirected stream every byte-comparison then has to strip.
|
|
104
|
+
*
|
|
105
|
+
* @returns True on a color-capable terminal.
|
|
106
|
+
* @note Impure — reads stdout's capture state and chalk's color level.
|
|
107
|
+
*/
|
|
108
|
+
export function canColor(): boolean {
|
|
109
|
+
return !stdoutIsCaptured() && chalk.level > 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
/**
|
|
91
113
|
* The active style for this process's stdout.
|
|
92
114
|
*
|
|
93
|
-
*
|
|
94
|
-
* MCP output stays byte-stable even
|
|
95
|
-
*
|
|
96
|
-
* color level, which the module-load
|
|
97
|
-
* `NO_COLOR`.
|
|
115
|
+
* Both signals, asked as one named question ({@link canColor}): stdout must be
|
|
116
|
+
* an attended TTY, so piped / redirected / MCP output stays byte-stable even
|
|
117
|
+
* where `supports-color` fires off a TTY (CI sets `GITHUB_ACTIONS`, which chalk
|
|
118
|
+
* honors); AND chalk must report a usable color level, which the module-load
|
|
119
|
+
* check above forces to zero under `NO_COLOR`.
|
|
98
120
|
*
|
|
99
121
|
* @returns A colorizing style on a color-capable TTY, else the identity styler.
|
|
100
|
-
* @note Impure — reads
|
|
122
|
+
* @note Impure — {@link canColor} reads stdout's capture state and chalk's
|
|
123
|
+
* color level.
|
|
101
124
|
*/
|
|
102
125
|
export function defaultStyle(): RenderStyle {
|
|
103
|
-
return styleFor(
|
|
126
|
+
return styleFor(canColor());
|
|
104
127
|
}
|