@canonical/pragma-cli 0.30.0 → 0.31.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 +64 -35
- package/dist/pragma +0 -0
- package/package.json +15 -13
- package/src/completions/constants.ts +7 -0
- package/src/completions/handleQuery.test.ts +32 -0
- package/src/completions/handleQuery.ts +20 -1
- package/src/completions/queryCompletions.test.ts +5 -0
- package/src/completions/queryCompletions.ts +17 -3
- package/src/completions/querySocket.test.ts +31 -0
- package/src/completions/querySocket.ts +36 -9
- package/src/config/parseConfigValues.ts +2 -0
- package/src/config/readConfigLayers.test.ts +2 -0
- package/src/config/readConfigLayers.ts +3 -0
- package/src/config/types.ts +7 -0
- package/src/constants.ts +3 -1
- package/src/domains/block/commands/sample.ts +2 -1
- package/src/domains/block/formatters/list.test.ts +3 -3
- package/src/domains/block/formatters/lookup.test.ts +5 -5
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/stories.ts +9 -3
- package/src/domains/config/commands/channel.test.ts +16 -0
- package/src/domains/config/commands/channel.ts +14 -1
- package/src/domains/config/commands/framework.test.ts +14 -0
- package/src/domains/config/commands/framework.ts +13 -1
- package/src/domains/config/commands/tier.test.ts +7 -0
- package/src/domains/config/commands/tier.ts +14 -1
- package/src/domains/config/operations/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- package/src/domains/create/generatorPack/bundled.ts +49 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
- package/src/domains/create/index.ts +13 -7
- package/src/domains/create/mcp/specs.ts +6 -161
- package/src/domains/create/renderGeneratorUi.test.ts +103 -0
- package/src/domains/create/renderGeneratorUi.ts +65 -0
- package/src/domains/doctor/formatters/doctor.test.ts +50 -2
- package/src/domains/doctor/formatters/doctor.ts +90 -38
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
- package/src/domains/doctor/operations/checks.test.ts +6 -4
- package/src/domains/doctor/operations/types.ts +23 -1
- package/src/domains/graph/commands/inspect.test.ts +2 -2
- package/src/domains/graph/commands/query.test.ts +5 -3
- package/src/domains/graph/formatters/inspect.ts +7 -7
- package/src/domains/graph/formatters/query.test.ts +87 -0
- package/src/domains/graph/formatters/query.ts +42 -5
- package/src/domains/graph/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/serve.test.ts +12 -6
- package/src/domains/graphql/commands/serve.ts +16 -6
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
- package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
- package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
- package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
- package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/formatters/upgrade.test.ts +7 -4
- package/src/domains/info/formatters/upgrade.ts +3 -1
- package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
- package/src/domains/info/operations/collectEntityCounts.ts +123 -0
- package/src/domains/info/operations/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +23 -3
- package/src/domains/llm/commands/capabilities.ts +12 -3
- package/src/domains/llm/commands/llm.ts +5 -1
- package/src/domains/llm/data/commandReference.ts +20 -11
- package/src/domains/llm/data/decisionTrees.ts +10 -5
- package/src/domains/llm/data/toolCatalog.ts +49 -18
- package/src/domains/llm/formatters/orientation.ts +5 -1
- package/src/domains/llm/llmCommand.test.ts +11 -3
- package/src/domains/llm/mcp/specs.ts +3 -4
- package/src/domains/llm/operations/collectContext.test.ts +64 -0
- package/src/domains/llm/operations/collectContext.ts +7 -17
- package/src/domains/modifier/commands/sample.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/stories.ts +9 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/show.test.ts +85 -9
- package/src/domains/ontology/formatters/index.ts +1 -0
- package/src/domains/ontology/formatters/list.ts +37 -12
- package/src/domains/ontology/formatters/show.ts +358 -67
- package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
- package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
- package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
- package/src/domains/ontology/helpers/index.ts +14 -0
- package/src/domains/ontology/helpers/queryClasses.ts +32 -16
- package/src/domains/ontology/helpers/queryProperties.ts +39 -6
- package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
- package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
- package/src/domains/ontology/operations/listOntologies.ts +126 -30
- package/src/domains/ontology/operations/showOntology.test.ts +131 -12
- package/src/domains/ontology/operations/showOntology.ts +289 -7
- package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
- package/src/domains/ontology/stories.ts +53 -8
- package/src/domains/refs/operations/updateRefs.ts +0 -1
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- package/src/domains/setup/index.ts +7 -3
- package/src/domains/setup/operations/setupLsp.test.ts +3 -2
- package/src/domains/setup/operations/setupLsp.ts +7 -2
- package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +56 -20
- package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
- package/src/domains/shared/extractLocalName.test.ts +3 -3
- package/src/domains/shared/formatters.ts +23 -2
- package/src/domains/shared/listTiers.ts +40 -0
- package/src/domains/shared/loaders/bundledLoader.ts +48 -0
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/localLoader.ts +2 -18
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +46 -8
- package/src/domains/shared/lookupMany.test.ts +15 -0
- package/src/domains/shared/lookupMany.ts +22 -6
- package/src/domains/shared/namespaces.ts +13 -5
- package/src/domains/shared/parseSampleCount.test.ts +47 -0
- package/src/domains/shared/parseSampleCount.ts +45 -0
- package/src/domains/shared/prefixes.test.ts +211 -0
- package/src/domains/shared/prefixes.ts +182 -15
- package/src/domains/shared/renderers.ts +5 -5
- package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +17 -2
- package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
- package/src/domains/shared/stories/compileLookupTool.ts +1 -1
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
- package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
- package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
- package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
- package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
- package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
- package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
- package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
- package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
- package/src/domains/shared/stories/pack/index.ts +14 -0
- package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
- package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
- package/src/domains/shared/stories/pack/types.ts +123 -3
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
- package/src/domains/shared/stories/types.ts +9 -1
- package/src/domains/shared/types/index.ts +5 -9
- package/src/domains/shared/types/ontology.ts +119 -24
- package/src/domains/shared/types/standard.ts +9 -52
- package/src/domains/token/commands/lookup.test.ts +8 -0
- package/src/domains/token/commands/lookup.ts +12 -0
- package/src/domains/token/commands/sample.ts +2 -1
- package/src/domains/token/featureFlag.ts +17 -0
- package/src/domains/token/index.ts +5 -0
- package/src/domains/token/mcp/specs.ts +13 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +2 -2
- package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
- package/src/mcp/tools/index.ts +18 -10
- package/src/mcp/tools/registerTools.test.ts +106 -30
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +34 -19
- package/src/pipeline/createProgram.test.ts +5 -1
- package/src/pipeline/createProgram.ts +2 -2
- package/src/pipeline/firstRun.test.ts +80 -0
- package/src/pipeline/firstRun.ts +99 -0
- package/src/pipeline/parseGlobalFlags.test.ts +102 -1
- package/src/pipeline/parseGlobalFlags.ts +98 -9
- package/src/pipeline/resolveCommandKind.test.ts +12 -0
- package/src/pipeline/resolveCommandKind.ts +4 -0
- package/src/pipeline/rootHelp.test.ts +87 -0
- package/src/pipeline/rootHelp.ts +265 -0
- package/src/pipeline/runCli.orchestration.test.ts +28 -12
- package/src/pipeline/runCli.test.ts +17 -4
- package/src/pipeline/runCli.ts +162 -19
- package/src/testing/cli.ts +30 -2
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +1 -4
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +6 -4
- package/src/testing/integration/executor-golden.test.ts +2 -2
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +119 -70
- package/src/testing/setupXdgIsolation.ts +16 -5
- package/src/testing/standardParityFixtures.ts +26 -83
- package/src/testing/store.ts +4 -4
- package/src/domains/create/commands/component.test.ts +0 -166
- package/src/domains/create/commands/component.ts +0 -123
- package/src/domains/create/commands/index.ts +0 -3
- package/src/domains/create/commands/package.test.ts +0 -35
- package/src/domains/create/commands/package.ts +0 -21
- package/src/domains/create/generators.ts +0 -8
- package/src/domains/standard/commands/categories.test.ts +0 -70
- package/src/domains/standard/commands/categories.ts +0 -48
- package/src/domains/standard/commands/index.ts +0 -6
- package/src/domains/standard/commands/list.test.ts +0 -105
- package/src/domains/standard/commands/list.ts +0 -11
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -14
- package/src/domains/standard/commands/sample.ts +0 -57
- package/src/domains/standard/formatters/categories.test.ts +0 -44
- package/src/domains/standard/formatters/categories.ts +0 -38
- package/src/domains/standard/formatters/index.ts +0 -10
- package/src/domains/standard/formatters/list.test.ts +0 -169
- package/src/domains/standard/formatters/list.ts +0 -184
- package/src/domains/standard/formatters/lookup.test.ts +0 -95
- package/src/domains/standard/formatters/lookup.ts +0 -101
- package/src/domains/standard/formatters/sample.ts +0 -15
- package/src/domains/standard/formatters/types.ts +0 -31
- package/src/domains/standard/index.ts +0 -40
- package/src/domains/standard/mcp/index.ts +0 -2
- package/src/domains/standard/mcp/specs.ts +0 -89
- package/src/domains/standard/operations/categories.test.ts +0 -61
- package/src/domains/standard/operations/categories.ts +0 -38
- package/src/domains/standard/operations/index.ts +0 -6
- package/src/domains/standard/operations/list.test.ts +0 -75
- package/src/domains/standard/operations/list.ts +0 -64
- package/src/domains/standard/operations/lookup.test.ts +0 -58
- package/src/domains/standard/operations/lookup.ts +0 -151
- package/src/domains/standard/operations/sample.ts +0 -35
- package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
- package/src/domains/standard/orchestration/index.ts +0 -4
- package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
- package/src/domains/standard/standardConfig.ts +0 -42
- package/src/domains/standard/stories.test.ts +0 -24
- package/src/domains/standard/stories.ts +0 -151
- package/src/domains/standard/types.ts +0 -15
- package/src/domains/tier/commands/index.ts +0 -3
- package/src/domains/tier/commands/list.test.ts +0 -67
- package/src/domains/tier/commands/list.ts +0 -9
- package/src/domains/tier/formatters/index.ts +0 -3
- package/src/domains/tier/formatters/list.test.ts +0 -33
- package/src/domains/tier/formatters/list.ts +0 -45
- package/src/domains/tier/index.ts +0 -17
- package/src/domains/tier/mcp/index.ts +0 -2
- package/src/domains/tier/mcp/specs.ts +0 -12
- package/src/domains/tier/operations/index.ts +0 -3
- package/src/domains/tier/operations/list.test.ts +0 -32
- package/src/domains/tier/operations/list.ts +0 -33
- package/src/domains/tier/stories.test.ts +0 -22
- package/src/domains/tier/stories.ts +0 -52
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-run onboarding — welcome note + global config creation.
|
|
3
|
+
*
|
|
4
|
+
* On the first invocation of pragma (detected by the absence of the global
|
|
5
|
+
* config file), print a short welcome to stderr and create
|
|
6
|
+
* `$XDG_CONFIG_HOME/pragma/config.json` with defaults. Modeled as a
|
|
7
|
+
* {@link Task} so the effects (exists/mkdir/write/log) stay declarative and
|
|
8
|
+
* testable, mirroring the config domain's write tasks.
|
|
9
|
+
*
|
|
10
|
+
* The banner goes to STDERR: stdout belongs to command output (`--format
|
|
11
|
+
* json` consumers, MCP stdio), and the note must never corrupt it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
$,
|
|
17
|
+
exists,
|
|
18
|
+
gen,
|
|
19
|
+
info,
|
|
20
|
+
mkdir,
|
|
21
|
+
type Task,
|
|
22
|
+
writeFile,
|
|
23
|
+
} from "@canonical/task";
|
|
24
|
+
import { runTask } from "@canonical/task/node";
|
|
25
|
+
import { resolveGlobalConfigPath } from "../config/index.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Seed content for a fresh global config: an empty object, so every field
|
|
29
|
+
* keeps its built-in default and `config show` reports honest provenance
|
|
30
|
+
* (nothing is pinned that the user did not choose).
|
|
31
|
+
*/
|
|
32
|
+
const SEED_CONFIG = "{}\n";
|
|
33
|
+
|
|
34
|
+
/** Build the welcome note. The resolved path is shown so it is copyable. */
|
|
35
|
+
function welcomeLines(path: string): string[] {
|
|
36
|
+
return [
|
|
37
|
+
"Hello! Thanks for taking the time to try the pre-release pragma CLI.",
|
|
38
|
+
"Please file issues, suggestions, and feedback at https://github.com/canonical/pragma/issues.",
|
|
39
|
+
`Pragma stores its configuration in ${path} (just created with defaults).`,
|
|
40
|
+
"A `pragma.config.json` in the current directory or any directory above it overrides it per project.",
|
|
41
|
+
"",
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The first-run task: create the global config, announcing it on first run.
|
|
47
|
+
*
|
|
48
|
+
* @returns Task yielding whether the config was created and its path.
|
|
49
|
+
* @note Impure — checks/creates the global config file, emits log effects.
|
|
50
|
+
*/
|
|
51
|
+
export function firstRunTask(): Task<{ created: boolean; path: string }> {
|
|
52
|
+
return gen(function* () {
|
|
53
|
+
const path = resolveGlobalConfigPath();
|
|
54
|
+
|
|
55
|
+
if (yield* $(exists(path))) {
|
|
56
|
+
return { created: false, path };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create the config first, then greet: if creation fails the effects throw
|
|
60
|
+
// before any line is emitted, so onboarding degrades to the single stderr
|
|
61
|
+
// warning ensureFirstRun writes — never a "just created" banner alongside a
|
|
62
|
+
// failure. `mkdir` is recursive (its default), so a missing XDG parent is
|
|
63
|
+
// created rather than being treated as an error.
|
|
64
|
+
yield* $(mkdir(dirname(path), true));
|
|
65
|
+
yield* $(writeFile(path, SEED_CONFIG));
|
|
66
|
+
|
|
67
|
+
for (const line of welcomeLines(path)) {
|
|
68
|
+
yield* $(info(line));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { created: true, path };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run the first-run task, tolerating every failure.
|
|
77
|
+
*
|
|
78
|
+
* Onboarding must never break the CLI: a read-only home, a weird XDG path,
|
|
79
|
+
* or a race with a concurrent first run degrade to a single stderr warning
|
|
80
|
+
* and the invocation proceeds normally (the config layers treat a missing
|
|
81
|
+
* global file as defaults anyway).
|
|
82
|
+
*
|
|
83
|
+
* @param write - Stderr sink, injectable for tests.
|
|
84
|
+
* @note Impure — filesystem effects + stderr.
|
|
85
|
+
*/
|
|
86
|
+
export default async function ensureFirstRun(
|
|
87
|
+
write: (line: string) => void = (line) => process.stderr.write(`${line}\n`),
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
try {
|
|
90
|
+
await runTask(firstRunTask(), {
|
|
91
|
+
// Route log effects to stderr without the interpreter's [INFO] prefix —
|
|
92
|
+
// this is a greeting, not diagnostics.
|
|
93
|
+
onLog: (_level, message) => write(message),
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
97
|
+
write(`Warning: could not create the global pragma config — ${reason}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import parseGlobalFlags, {
|
|
2
|
+
import parseGlobalFlags, {
|
|
3
|
+
readRawFormat,
|
|
4
|
+
stripGlobalFlags,
|
|
5
|
+
} from "./parseGlobalFlags.js";
|
|
3
6
|
|
|
4
7
|
describe("parseGlobalFlags", () => {
|
|
5
8
|
it("extracts --llm", () => {
|
|
@@ -29,6 +32,20 @@ describe("parseGlobalFlags", () => {
|
|
|
29
32
|
expect(flags.format).toBe("text");
|
|
30
33
|
});
|
|
31
34
|
|
|
35
|
+
it("accepts the --format=json equals form", () => {
|
|
36
|
+
const flags = parseGlobalFlags(["node", "pragma", "info", "--format=json"]);
|
|
37
|
+
expect(flags.format).toBe("json");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("treats an unknown --format value as text", () => {
|
|
41
|
+
expect(
|
|
42
|
+
parseGlobalFlags(["node", "pragma", "info", "--format", "xml"]).format,
|
|
43
|
+
).toBe("text");
|
|
44
|
+
expect(
|
|
45
|
+
parseGlobalFlags(["node", "pragma", "info", "--format=xml"]).format,
|
|
46
|
+
).toBe("text");
|
|
47
|
+
});
|
|
48
|
+
|
|
32
49
|
it("extracts --verbose", () => {
|
|
33
50
|
const flags = parseGlobalFlags([
|
|
34
51
|
"node",
|
|
@@ -39,6 +56,52 @@ describe("parseGlobalFlags", () => {
|
|
|
39
56
|
]);
|
|
40
57
|
expect(flags.verbose).toBe(true);
|
|
41
58
|
});
|
|
59
|
+
|
|
60
|
+
describe("auto-LLM detection", () => {
|
|
61
|
+
const tty = { isTty: true, noAutoLlm: false };
|
|
62
|
+
const piped = { isTty: false, noAutoLlm: false };
|
|
63
|
+
|
|
64
|
+
it("stays plain on an interactive terminal", () => {
|
|
65
|
+
expect(parseGlobalFlags(["node", "pragma", "info"], tty).llm).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("defaults to llm when stdout is not a TTY, flagged as auto", () => {
|
|
69
|
+
const flags = parseGlobalFlags(["node", "pragma", "info"], piped);
|
|
70
|
+
expect(flags.llm).toBe(true);
|
|
71
|
+
expect(flags.autoLlm).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("marks explicit --llm as not auto", () => {
|
|
75
|
+
const flags = parseGlobalFlags(
|
|
76
|
+
["node", "pragma", "--llm", "info"],
|
|
77
|
+
piped,
|
|
78
|
+
);
|
|
79
|
+
expect(flags.llm).toBe(true);
|
|
80
|
+
expect(flags.autoLlm).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not auto-enable llm when --format json is requested", () => {
|
|
84
|
+
expect(
|
|
85
|
+
parseGlobalFlags(["node", "pragma", "info", "--format", "json"], piped)
|
|
86
|
+
.llm,
|
|
87
|
+
).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("respects an explicit --llm even on a TTY", () => {
|
|
91
|
+
expect(
|
|
92
|
+
parseGlobalFlags(["node", "pragma", "--llm", "info"], tty).llm,
|
|
93
|
+
).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("honours PRAGMA_NO_AUTO_LLM to stay plain when piped", () => {
|
|
97
|
+
expect(
|
|
98
|
+
parseGlobalFlags(["node", "pragma", "info"], {
|
|
99
|
+
isTty: false,
|
|
100
|
+
noAutoLlm: true,
|
|
101
|
+
}).llm,
|
|
102
|
+
).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
42
105
|
});
|
|
43
106
|
|
|
44
107
|
describe("stripGlobalFlags", () => {
|
|
@@ -89,4 +152,42 @@ describe("stripGlobalFlags", () => {
|
|
|
89
152
|
"list",
|
|
90
153
|
]);
|
|
91
154
|
});
|
|
155
|
+
|
|
156
|
+
it("removes the equals form of every global flag", () => {
|
|
157
|
+
expect(
|
|
158
|
+
stripGlobalFlags([
|
|
159
|
+
"node",
|
|
160
|
+
"pragma",
|
|
161
|
+
"--llm=",
|
|
162
|
+
"--verbose=",
|
|
163
|
+
"--format=json",
|
|
164
|
+
"block",
|
|
165
|
+
"list",
|
|
166
|
+
]),
|
|
167
|
+
).toEqual(["node", "pragma", "block", "list"]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("readRawFormat", () => {
|
|
172
|
+
it("reads the space form", () => {
|
|
173
|
+
expect(readRawFormat(["node", "pragma", "--format", "json"])).toBe("json");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("reads the equals form", () => {
|
|
177
|
+
expect(readRawFormat(["node", "pragma", "--format=text"])).toBe("text");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns undefined when --format is absent", () => {
|
|
181
|
+
expect(readRawFormat(["node", "pragma", "info"])).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("returns an empty string for a bare --format at end of argv", () => {
|
|
185
|
+
// Not undefined — the caller must reject it rather than fall through to
|
|
186
|
+
// root help with exit code 0.
|
|
187
|
+
expect(readRawFormat(["node", "pragma", "--format"])).toBe("");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns an empty string when --format is followed by another flag", () => {
|
|
191
|
+
expect(readRawFormat(["node", "pragma", "--format", "--llm"])).toBe("");
|
|
192
|
+
});
|
|
92
193
|
});
|
|
@@ -1,21 +1,102 @@
|
|
|
1
1
|
import type { GlobalFlags } from "@canonical/cli-core";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Read the value of a `--flag` that accepts both the space form
|
|
5
|
+
* (`--flag value`) and the equals form (`--flag=value`).
|
|
6
|
+
*
|
|
7
|
+
* @param argv - Raw process.argv array.
|
|
8
|
+
* @param flag - The flag name including leading dashes (e.g. `--format`).
|
|
9
|
+
* @returns The flag's value, or undefined when the flag is absent.
|
|
10
|
+
*/
|
|
11
|
+
function readFlagValue(
|
|
12
|
+
argv: readonly string[],
|
|
13
|
+
flag: string,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const equalsPrefix = `${flag}=`;
|
|
16
|
+
const equalsArg = argv.find((arg) => arg.startsWith(equalsPrefix));
|
|
17
|
+
if (equalsArg !== undefined) {
|
|
18
|
+
return equalsArg.slice(equalsPrefix.length);
|
|
19
|
+
}
|
|
20
|
+
const spaceIndex = argv.indexOf(flag);
|
|
21
|
+
return spaceIndex === -1 ? undefined : argv[spaceIndex + 1];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the raw `--format` value exactly as the user typed it (both space and
|
|
26
|
+
* equals forms), for validation. Unlike {@link parseGlobalFlags}, this does not
|
|
27
|
+
* coerce unknown values to `text` — the caller decides how to reject them.
|
|
28
|
+
*
|
|
29
|
+
* A bare `--format` with no following value (either at the end of argv or
|
|
30
|
+
* immediately followed by another flag) is reported as an empty string rather
|
|
31
|
+
* than `undefined`, so the caller rejects it as invalid input instead of
|
|
32
|
+
* silently falling through to the root-help path with exit code 0.
|
|
33
|
+
*
|
|
34
|
+
* @param argv - Raw process.argv array.
|
|
35
|
+
* @returns The raw format value, an empty string when `--format` is present
|
|
36
|
+
* without a value, or undefined when `--format` is absent.
|
|
37
|
+
*/
|
|
38
|
+
export function readRawFormat(argv: readonly string[]): string | undefined {
|
|
39
|
+
const equalsArg = argv.find((arg) => arg.startsWith("--format="));
|
|
40
|
+
if (equalsArg !== undefined) return equalsArg.slice("--format=".length);
|
|
41
|
+
|
|
42
|
+
const spaceIndex = argv.indexOf("--format");
|
|
43
|
+
if (spaceIndex === -1) return undefined;
|
|
44
|
+
|
|
45
|
+
const value = argv[spaceIndex + 1];
|
|
46
|
+
// A missing value or another flag means the user gave no format at all.
|
|
47
|
+
return value !== undefined && !value.startsWith("-") ? value : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Environment probe used to decide the auto-LLM default. Injectable so the
|
|
52
|
+
* detection can be unit-tested without touching the real process.
|
|
53
|
+
*/
|
|
54
|
+
export interface OutputEnvironment {
|
|
55
|
+
/** Whether stdout is attached to an interactive terminal. */
|
|
56
|
+
readonly isTty: boolean;
|
|
57
|
+
/** Whether the auto-LLM default is explicitly disabled. */
|
|
58
|
+
readonly noAutoLlm: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read the real stdout/env output environment. @note Impure — reads process. */
|
|
62
|
+
function readOutputEnvironment(): OutputEnvironment {
|
|
63
|
+
return {
|
|
64
|
+
isTty: process.stdout.isTTY === true,
|
|
65
|
+
noAutoLlm: Boolean(process.env.PRAGMA_NO_AUTO_LLM),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
3
69
|
/**
|
|
4
70
|
* Extract global flags (--llm, --format, --verbose) from raw argv.
|
|
5
71
|
*
|
|
6
|
-
* Performs simple
|
|
7
|
-
*
|
|
72
|
+
* Performs simple scanning rather than full argument parsing so it can run
|
|
73
|
+
* before Commander processes the command tree. Recognises both the space form
|
|
74
|
+
* (`--format json`) and the equals form (`--format=json`).
|
|
75
|
+
*
|
|
76
|
+
* Auto-LLM: when no `--llm`/`--format` is given and stdout is not an
|
|
77
|
+
* interactive terminal (piped or redirected — the shape an agent captures),
|
|
78
|
+
* `llm` defaults to true so agents get condensed Markdown without passing a
|
|
79
|
+
* flag. Interactive terminals stay in rich mode. `--llm`, `--format json`, and
|
|
80
|
+
* `PRAGMA_NO_AUTO_LLM` all override this.
|
|
8
81
|
*
|
|
9
82
|
* @param argv - Raw process.argv array.
|
|
83
|
+
* @param env - Output environment probe (defaults to the real process).
|
|
10
84
|
* @returns Parsed global flags.
|
|
85
|
+
* @note Impure by default — reads process.stdout/env unless `env` is injected.
|
|
11
86
|
*/
|
|
12
|
-
export default function parseGlobalFlags(
|
|
87
|
+
export default function parseGlobalFlags(
|
|
88
|
+
argv: readonly string[],
|
|
89
|
+
env: OutputEnvironment = readOutputEnvironment(),
|
|
90
|
+
): GlobalFlags {
|
|
91
|
+
const explicitLlm = argv.includes("--llm");
|
|
92
|
+
const format = readFlagValue(argv, "--format") === "json" ? "json" : "text";
|
|
93
|
+
const formatRequested = readFlagValue(argv, "--format") !== undefined;
|
|
94
|
+
const autoLlm =
|
|
95
|
+
!explicitLlm && !formatRequested && !env.isTty && !env.noAutoLlm;
|
|
13
96
|
return {
|
|
14
|
-
llm:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
? "json"
|
|
18
|
-
: "text",
|
|
97
|
+
llm: explicitLlm || autoLlm,
|
|
98
|
+
autoLlm,
|
|
99
|
+
format,
|
|
19
100
|
verbose: argv.includes("--verbose"),
|
|
20
101
|
};
|
|
21
102
|
}
|
|
@@ -36,8 +117,16 @@ export function stripGlobalFlags(argv: readonly string[]): string[] {
|
|
|
36
117
|
for (let i = 0; i < argv.length; i++) {
|
|
37
118
|
const arg = argv[i];
|
|
38
119
|
if (arg === "--llm" || arg === "--verbose") continue;
|
|
120
|
+
// Equals form (`--format=json`, `--llm=…`) is a single token — drop it.
|
|
121
|
+
if (
|
|
122
|
+
arg?.startsWith("--format=") ||
|
|
123
|
+
arg?.startsWith("--llm=") ||
|
|
124
|
+
arg?.startsWith("--verbose=")
|
|
125
|
+
) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
39
128
|
if (arg === "--format") {
|
|
40
|
-
i += 1; // skip the value too
|
|
129
|
+
i += 1; // skip the space-form value too
|
|
41
130
|
continue;
|
|
42
131
|
}
|
|
43
132
|
result.push(arg as string);
|
|
@@ -40,6 +40,18 @@ describe("resolveCommandKind", () => {
|
|
|
40
40
|
expect(result).toEqual({ kind: "store-skip", command: "mcp" });
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it("detects store-skip for create (pure scaffolding, no store)", () => {
|
|
44
|
+
const result = resolveCommandKind([
|
|
45
|
+
"node",
|
|
46
|
+
"pragma",
|
|
47
|
+
"create",
|
|
48
|
+
"component",
|
|
49
|
+
"react",
|
|
50
|
+
"Accordion2",
|
|
51
|
+
]);
|
|
52
|
+
expect(result).toEqual({ kind: "store-skip", command: "create" });
|
|
53
|
+
});
|
|
54
|
+
|
|
43
55
|
it("defaults to store-required for regular commands", () => {
|
|
44
56
|
const result = resolveCommandKind(["node", "pragma", "component", "list"]);
|
|
45
57
|
expect(result).toEqual({ kind: "store-required" });
|
|
@@ -15,6 +15,10 @@ const STORE_SKIP_COMMANDS = new Set([
|
|
|
15
15
|
"capabilities",
|
|
16
16
|
"trace",
|
|
17
17
|
"graphql",
|
|
18
|
+
// `create` is pure scaffolding (summon generators) — it never queries the
|
|
19
|
+
// design-system graph, so booting the shared store just couples it to the
|
|
20
|
+
// installed packages and makes it fail when they are missing or malformed.
|
|
21
|
+
"create",
|
|
18
22
|
]);
|
|
19
23
|
|
|
20
24
|
function findCommandArg(argv: readonly string[]): string | undefined {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CommandDefinition } from "@canonical/cli-core";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import formatRootHelp from "./rootHelp.js";
|
|
4
|
+
|
|
5
|
+
/** Minimal command stub — only the fields the help renderer reads. */
|
|
6
|
+
function cmd(
|
|
7
|
+
path: string[],
|
|
8
|
+
extra: Partial<CommandDefinition> = {},
|
|
9
|
+
): CommandDefinition {
|
|
10
|
+
return {
|
|
11
|
+
path,
|
|
12
|
+
description: `${path.join(" ")} description`,
|
|
13
|
+
parameters: [],
|
|
14
|
+
execute: async () => ({ tag: "output" }) as never,
|
|
15
|
+
...extra,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const commands: CommandDefinition[] = [
|
|
20
|
+
cmd(["block", "list"]),
|
|
21
|
+
cmd(["token", "list"]),
|
|
22
|
+
cmd(["create", "component"], {
|
|
23
|
+
parameters: [
|
|
24
|
+
{
|
|
25
|
+
name: "framework",
|
|
26
|
+
description: "Component framework",
|
|
27
|
+
type: "select",
|
|
28
|
+
positional: true,
|
|
29
|
+
required: true,
|
|
30
|
+
choices: [
|
|
31
|
+
{ label: "react", value: "react" },
|
|
32
|
+
{ label: "svelte", value: "svelte" },
|
|
33
|
+
{ label: "lit", value: "lit" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
cmd(["create", "package"]),
|
|
39
|
+
cmd(["doctor"]),
|
|
40
|
+
cmd(["mystery", "verb"], { description: "an uncatalogued thing" }),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
describe("formatRootHelp", () => {
|
|
44
|
+
const out = formatRootHelp("pragma", "Design system CLI", commands);
|
|
45
|
+
|
|
46
|
+
it("groups nouns under task-oriented headings instead of a flat list", () => {
|
|
47
|
+
expect(out).toContain("Explore the design system");
|
|
48
|
+
expect(out).toContain("Generate code");
|
|
49
|
+
expect(out).toContain("Set up & maintain");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("gives real summaries, never the `<noun> commands` placeholder", () => {
|
|
53
|
+
expect(out).toContain("Look up design tokens and their values");
|
|
54
|
+
expect(out).not.toContain("block commands");
|
|
55
|
+
expect(out).not.toContain("token commands");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("expands create with its subtypes and derived frameworks", () => {
|
|
59
|
+
expect(out).toContain("component");
|
|
60
|
+
expect(out).toContain("react · svelte · lit");
|
|
61
|
+
expect(out).toContain("package");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("surfaces uncatalogued nouns under Other instead of dropping them", () => {
|
|
65
|
+
expect(out).toContain("Other");
|
|
66
|
+
expect(out).toContain("mystery");
|
|
67
|
+
expect(out).toContain("an uncatalogued thing");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("always lists the mcp server command even without a definition", () => {
|
|
71
|
+
const withoutMcp = commands.filter((c) => c.path[0] !== "mcp");
|
|
72
|
+
const rendered = formatRootHelp("pragma", "d", withoutMcp);
|
|
73
|
+
expect(rendered).toContain("mcp");
|
|
74
|
+
expect(rendered).toContain("Start the MCP server over stdio");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("hides nouns that are catalogued but not registered", () => {
|
|
78
|
+
// `standard` is catalogued but absent from this command set.
|
|
79
|
+
expect(out).not.toContain("Browse code standards");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("lists the global flags and an orientation hint", () => {
|
|
83
|
+
expect(out).toContain("Global flags");
|
|
84
|
+
expect(out).toContain("--format json");
|
|
85
|
+
expect(out).toContain("pragma capabilities");
|
|
86
|
+
});
|
|
87
|
+
});
|