@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,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated root `--help` for `pragma`.
|
|
3
|
+
*
|
|
4
|
+
* cli-core's generic `formatHelp` derives a description per top-level noun from
|
|
5
|
+
* its commands, but noun groups that only have verbs (e.g. `block list`) have
|
|
6
|
+
* no command to carry one — so it emits placeholder text like "block commands".
|
|
7
|
+
* The root help is also the CLI's front door, so its content and grouping are
|
|
8
|
+
* worth hand-curating here rather than auto-generating.
|
|
9
|
+
*
|
|
10
|
+
* This renderer:
|
|
11
|
+
* - groups nouns into task-oriented sections with real one-line summaries,
|
|
12
|
+
* - derives each section's live nouns from the registered commands (so a noun
|
|
13
|
+
* that disappears is dropped, and a new, uncatalogued one still shows up
|
|
14
|
+
* under "Other" instead of vanishing),
|
|
15
|
+
* - expands `create` to show its scaffolding subtypes, including the component
|
|
16
|
+
* frameworks pulled straight from the command's parameter choices.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { CommandDefinition } from "@canonical/cli-core";
|
|
20
|
+
import chalk from "chalk";
|
|
21
|
+
|
|
22
|
+
interface NounSummary {
|
|
23
|
+
readonly noun: string;
|
|
24
|
+
readonly summary: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HelpGroup {
|
|
28
|
+
readonly title: string;
|
|
29
|
+
readonly nouns: readonly NounSummary[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Task-oriented grouping with curated summaries. Order is intentional: the
|
|
34
|
+
* things people reach for most (exploring the system, generating code) come
|
|
35
|
+
* first; agent/integration tooling comes last.
|
|
36
|
+
*/
|
|
37
|
+
const HELP_GROUPS: readonly HelpGroup[] = [
|
|
38
|
+
{
|
|
39
|
+
title: "Explore the design system",
|
|
40
|
+
nouns: [
|
|
41
|
+
{
|
|
42
|
+
noun: "block",
|
|
43
|
+
summary: "Inspect components & patterns and their anatomy",
|
|
44
|
+
},
|
|
45
|
+
{ noun: "token", summary: "Look up design tokens and their values" },
|
|
46
|
+
{ noun: "modifier", summary: "Inspect modifiers and modifier families" },
|
|
47
|
+
{ noun: "standard", summary: "Browse code standards and categories" },
|
|
48
|
+
{
|
|
49
|
+
noun: "ontology",
|
|
50
|
+
summary: "Explore the loaded ontologies (classes, properties)",
|
|
51
|
+
},
|
|
52
|
+
{ noun: "tier", summary: "List the design-system tiers" },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: "Generate code",
|
|
57
|
+
nouns: [
|
|
58
|
+
{ noun: "create", summary: "Scaffold components, packages, and apps" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Query & serve the graph",
|
|
63
|
+
nouns: [
|
|
64
|
+
{
|
|
65
|
+
noun: "graph",
|
|
66
|
+
summary: "Run SPARQL queries or inspect a URI directly",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
noun: "graphql",
|
|
70
|
+
summary: "Compile and serve a GraphQL API over the ontologies",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Set up & maintain",
|
|
76
|
+
nouns: [
|
|
77
|
+
{ noun: "doctor", summary: "Check environment health" },
|
|
78
|
+
{
|
|
79
|
+
noun: "setup",
|
|
80
|
+
summary: "Configure MCP, skills, completions, and the LSP",
|
|
81
|
+
},
|
|
82
|
+
{ noun: "config", summary: "Read and write pragma.config.json" },
|
|
83
|
+
{ noun: "tokens", summary: "Generate a Terrazzo tokens config" },
|
|
84
|
+
{
|
|
85
|
+
noun: "info",
|
|
86
|
+
summary: "Show version, config, update status, and store summary",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
noun: "upgrade",
|
|
90
|
+
summary: "Upgrade the pragma CLI to the latest version",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "For AI agents",
|
|
96
|
+
nouns: [
|
|
97
|
+
{
|
|
98
|
+
noun: "capabilities",
|
|
99
|
+
summary: "Discover conventions, tools, and the discovery sequence",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
noun: "llm",
|
|
103
|
+
summary: "LLM orientation: context, decision trees, command reference",
|
|
104
|
+
},
|
|
105
|
+
{ noun: "mcp", summary: "Start the MCP server over stdio" },
|
|
106
|
+
{
|
|
107
|
+
noun: "skill",
|
|
108
|
+
summary: "Browse agent skills from design-system packages",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/** All distinct top-level nouns present in the registered commands. */
|
|
115
|
+
function nounsFrom(commands: readonly CommandDefinition[]): Set<string> {
|
|
116
|
+
const nouns = new Set<string>();
|
|
117
|
+
for (const cmd of commands) {
|
|
118
|
+
const noun = cmd.path[0];
|
|
119
|
+
if (noun) nouns.add(noun);
|
|
120
|
+
}
|
|
121
|
+
return nouns;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The verbs (second path segment) registered under a noun, in order. */
|
|
125
|
+
function verbsOf(
|
|
126
|
+
noun: string,
|
|
127
|
+
commands: readonly CommandDefinition[],
|
|
128
|
+
): CommandDefinition[] {
|
|
129
|
+
return commands.filter((c) => c.path[0] === noun && c.path.length > 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The framework values offered by `create component`, e.g. `react · svelte ·
|
|
134
|
+
* lit`, read from the command's `framework` parameter choices so the help
|
|
135
|
+
* can never drift from what the generator actually accepts.
|
|
136
|
+
*/
|
|
137
|
+
function componentFrameworks(
|
|
138
|
+
commands: readonly CommandDefinition[],
|
|
139
|
+
): string | undefined {
|
|
140
|
+
const component = commands.find(
|
|
141
|
+
(c) => c.path[0] === "create" && c.path[1] === "component",
|
|
142
|
+
);
|
|
143
|
+
const choices = component?.parameters.find(
|
|
144
|
+
(p) => p.name === "framework",
|
|
145
|
+
)?.choices;
|
|
146
|
+
if (!choices || choices.length === 0) return undefined;
|
|
147
|
+
return choices.map((c) => c.label).join(" · ");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Render the `create` subtypes as indented sub-lines. */
|
|
151
|
+
function createSubtypes(
|
|
152
|
+
commands: readonly CommandDefinition[],
|
|
153
|
+
subWidth: number,
|
|
154
|
+
): string[] {
|
|
155
|
+
const lines: string[] = [];
|
|
156
|
+
const frameworks = componentFrameworks(commands);
|
|
157
|
+
for (const verb of verbsOf("create", commands)) {
|
|
158
|
+
const name = verb.path[1] as string;
|
|
159
|
+
const note =
|
|
160
|
+
name === "component" && frameworks
|
|
161
|
+
? frameworks
|
|
162
|
+
: name === "package"
|
|
163
|
+
? "a new package for the monorepo"
|
|
164
|
+
: verb.description;
|
|
165
|
+
lines.push(
|
|
166
|
+
` ${chalk.dim("·")} ${chalk.cyan(name.padEnd(subWidth))} ${chalk.dim(note)}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return lines;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build the curated root help string.
|
|
174
|
+
*
|
|
175
|
+
* @param programName - The CLI binary name (`pragma`).
|
|
176
|
+
* @param description - The program description shown in the header.
|
|
177
|
+
* @param commands - All registered command definitions.
|
|
178
|
+
* @returns The formatted, colorized help text.
|
|
179
|
+
*/
|
|
180
|
+
export default function formatRootHelp(
|
|
181
|
+
programName: string,
|
|
182
|
+
description: string,
|
|
183
|
+
commands: readonly CommandDefinition[],
|
|
184
|
+
): string {
|
|
185
|
+
const present = nounsFrom(commands);
|
|
186
|
+
// `mcp` is attached to Commander directly (not via the command list), but is
|
|
187
|
+
// always available — surface it so the front door is complete.
|
|
188
|
+
present.add("mcp");
|
|
189
|
+
const catalogued = new Set(
|
|
190
|
+
HELP_GROUPS.flatMap((g) => g.nouns.map((n) => n.noun)),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Any live noun we didn't curate is surfaced so nothing silently disappears.
|
|
194
|
+
const uncatalogued = [...present].filter((n) => !catalogued.has(n)).sort();
|
|
195
|
+
const groups: HelpGroup[] = [
|
|
196
|
+
...HELP_GROUPS.map((g) => ({
|
|
197
|
+
...g,
|
|
198
|
+
nouns: g.nouns.filter((n) => present.has(n.noun)),
|
|
199
|
+
})),
|
|
200
|
+
...(uncatalogued.length > 0
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
title: "Other",
|
|
204
|
+
nouns: uncatalogued.map((noun) => ({
|
|
205
|
+
noun,
|
|
206
|
+
summary:
|
|
207
|
+
verbsOf(noun, commands)[0]?.description ?? `${noun} commands`,
|
|
208
|
+
})),
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
: []),
|
|
212
|
+
].filter((g) => g.nouns.length > 0);
|
|
213
|
+
|
|
214
|
+
// One shared column width so summaries line up across every group.
|
|
215
|
+
const nounWidth = Math.max(
|
|
216
|
+
...groups.flatMap((g) => g.nouns.map((n) => n.noun.length)),
|
|
217
|
+
0,
|
|
218
|
+
);
|
|
219
|
+
const subWidth = Math.max(
|
|
220
|
+
...verbsOf("create", commands).map((v) => (v.path[1] as string).length),
|
|
221
|
+
0,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const lines: string[] = [
|
|
225
|
+
`${chalk.bold(programName)} — ${description}`,
|
|
226
|
+
"",
|
|
227
|
+
`${chalk.dim("Usage:")} ${programName} ${chalk.cyan("<command>")} ${chalk.dim("[subcommand] [flags]")}`,
|
|
228
|
+
"",
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
for (const group of groups) {
|
|
232
|
+
lines.push(chalk.bold(group.title));
|
|
233
|
+
for (const { noun, summary } of group.nouns) {
|
|
234
|
+
lines.push(
|
|
235
|
+
` ${chalk.cyan(noun.padEnd(nounWidth))} ${chalk.dim(summary)}`,
|
|
236
|
+
);
|
|
237
|
+
if (noun === "create") {
|
|
238
|
+
lines.push(...createSubtypes(commands, subWidth));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
lines.push("");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
lines.push(chalk.bold("Global flags"));
|
|
245
|
+
const flags: [string, string][] = [
|
|
246
|
+
["--llm", "Condensed Markdown output for agents"],
|
|
247
|
+
["--format json", "Machine-readable JSON output"],
|
|
248
|
+
["--verbose", "Diagnostic output on stderr"],
|
|
249
|
+
["--help", "Show help (works on any command)"],
|
|
250
|
+
["--version", "Show the CLI version"],
|
|
251
|
+
];
|
|
252
|
+
const flagWidth = Math.max(...flags.map(([f]) => f.length));
|
|
253
|
+
for (const [flag, desc] of flags) {
|
|
254
|
+
lines.push(` ${chalk.cyan(flag.padEnd(flagWidth))} ${chalk.dim(desc)}`);
|
|
255
|
+
}
|
|
256
|
+
lines.push("");
|
|
257
|
+
|
|
258
|
+
lines.push(
|
|
259
|
+
chalk.dim(
|
|
260
|
+
`Run \`${programName} <command> --help\` for details, or \`${programName} capabilities\` to get oriented.`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
}
|
|
@@ -33,6 +33,7 @@ vi.mock("./mapExitCode.js", () => ({
|
|
|
33
33
|
vi.mock("./parseGlobalFlags.js", () => ({
|
|
34
34
|
default: parseGlobalFlagsMock,
|
|
35
35
|
stripGlobalFlags: (argv: readonly string[]) => [...argv],
|
|
36
|
+
readRawFormat: () => undefined,
|
|
36
37
|
}));
|
|
37
38
|
|
|
38
39
|
vi.mock("./resolveCommandKind.js", () => ({
|
|
@@ -109,10 +110,14 @@ describe("runCli orchestration", () => {
|
|
|
109
110
|
collectCommandsMock.mockReturnValue([] as CommandDefinition[]);
|
|
110
111
|
createProgramMock.mockReturnValue({ parseAsync });
|
|
111
112
|
|
|
113
|
+
// Completion queries are classified before the root-help shortcut, so
|
|
114
|
+
// resolveCommandKind runs first; a non-completion result still falls
|
|
115
|
+
// through to root help without booting the store.
|
|
116
|
+
resolveCommandKindMock.mockReturnValue({ kind: "store-required" });
|
|
117
|
+
|
|
112
118
|
const { default: runCli } = await import("./runCli.js");
|
|
113
119
|
await runCli(["node", "pragma", "--verbose"]);
|
|
114
120
|
|
|
115
|
-
expect(resolveCommandKindMock).not.toHaveBeenCalled();
|
|
116
121
|
expect(bootPragmaMock).not.toHaveBeenCalled();
|
|
117
122
|
expect(parseAsync).toHaveBeenCalledWith(["node", "pragma", "--help"]);
|
|
118
123
|
});
|
|
@@ -257,19 +262,30 @@ describe("runCli orchestration", () => {
|
|
|
257
262
|
expect(dispose).toHaveBeenCalledTimes(1);
|
|
258
263
|
});
|
|
259
264
|
|
|
260
|
-
it("
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
it("prints the version directly and never reaches the program", async () => {
|
|
266
|
+
// --version is a global flag handled before command dispatch, so it neither
|
|
267
|
+
// boots the store nor builds a Commander program.
|
|
268
|
+
const stdout: string[] = [];
|
|
269
|
+
const stdoutSpy = vi
|
|
270
|
+
.spyOn(process.stdout, "write")
|
|
271
|
+
.mockImplementation((chunk) => {
|
|
272
|
+
stdout.push(String(chunk));
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
264
275
|
|
|
265
276
|
resolveCommandKindMock.mockReturnValue({ kind: "store-skip" });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
createProgramMock.mockClear();
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const { default: runCli } = await import("./runCli.js");
|
|
281
|
+
await runCli(["node", "pragma", "--version"]);
|
|
282
|
+
} finally {
|
|
283
|
+
stdoutSpy.mockRestore();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
expect(stdout.join("")).toContain(".");
|
|
287
|
+
expect(createProgramMock).not.toHaveBeenCalled();
|
|
288
|
+
expect(process.exitCode).toBeUndefined();
|
|
273
289
|
});
|
|
274
290
|
|
|
275
291
|
it("renders pragma errors from the program using mapped exit codes", async () => {
|
|
@@ -4,9 +4,17 @@ import createProgram from "./createProgram.js";
|
|
|
4
4
|
import parseGlobalFlags from "./parseGlobalFlags.js";
|
|
5
5
|
|
|
6
6
|
describe("parseGlobalFlags", () => {
|
|
7
|
-
it("parses defaults", () => {
|
|
8
|
-
const flags = parseGlobalFlags(["node", "pragma"]
|
|
9
|
-
|
|
7
|
+
it("parses defaults on an interactive terminal", () => {
|
|
8
|
+
const flags = parseGlobalFlags(["node", "pragma"], {
|
|
9
|
+
isTty: true,
|
|
10
|
+
noAutoLlm: false,
|
|
11
|
+
});
|
|
12
|
+
expect(flags).toEqual({
|
|
13
|
+
llm: false,
|
|
14
|
+
autoLlm: false,
|
|
15
|
+
format: "text",
|
|
16
|
+
verbose: false,
|
|
17
|
+
});
|
|
10
18
|
});
|
|
11
19
|
|
|
12
20
|
it("parses --llm", () => {
|
|
@@ -33,7 +41,12 @@ describe("parseGlobalFlags", () => {
|
|
|
33
41
|
"json",
|
|
34
42
|
"--verbose",
|
|
35
43
|
]);
|
|
36
|
-
expect(flags).toEqual({
|
|
44
|
+
expect(flags).toEqual({
|
|
45
|
+
llm: true,
|
|
46
|
+
autoLlm: false,
|
|
47
|
+
format: "json",
|
|
48
|
+
verbose: true,
|
|
49
|
+
});
|
|
37
50
|
});
|
|
38
51
|
});
|
|
39
52
|
|
package/src/pipeline/runCli.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { GlobalFlags } from "@canonical/cli-core";
|
|
2
2
|
import { CommanderError } from "commander";
|
|
3
|
+
import { PROGRAM_NAME, VERSION } from "../constants.js";
|
|
4
|
+
import { commands as createCommands } from "../domains/create/index.js";
|
|
3
5
|
import { commands as graphqlCommands } from "../domains/graphql/index.js";
|
|
4
6
|
import { buildCapabilitiesCommand } from "../domains/llm/index.js";
|
|
5
7
|
import { commands as refsCommands } from "../domains/refs/index.js";
|
|
@@ -12,8 +14,12 @@ import { commands as traceCommands } from "../domains/trace/index.js";
|
|
|
12
14
|
import { PragmaError } from "../error/index.js";
|
|
13
15
|
import collectCommands from "./collectCommands.js";
|
|
14
16
|
import createProgram from "./createProgram.js";
|
|
17
|
+
import ensureFirstRun from "./firstRun.js";
|
|
15
18
|
import mapExitCode from "./mapExitCode.js";
|
|
16
|
-
import parseGlobalFlags, {
|
|
19
|
+
import parseGlobalFlags, {
|
|
20
|
+
readRawFormat,
|
|
21
|
+
stripGlobalFlags,
|
|
22
|
+
} from "./parseGlobalFlags.js";
|
|
17
23
|
import {
|
|
18
24
|
renderErrorJson,
|
|
19
25
|
renderErrorLlm,
|
|
@@ -67,8 +73,23 @@ async function handleCompletionsServer(): Promise<void> {
|
|
|
67
73
|
await startCompletionsServer();
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
async function handleDoctor(
|
|
76
|
+
async function handleDoctor(
|
|
77
|
+
argv: readonly string[],
|
|
78
|
+
globalFlags: GlobalFlags,
|
|
79
|
+
): Promise<void> {
|
|
71
80
|
const { doctorCommand } = await import("../domains/doctor/commands/index.js");
|
|
81
|
+
|
|
82
|
+
// `doctor` runs before Commander, so honour --help/-h here rather than
|
|
83
|
+
// executing the health check when the user only asked for usage.
|
|
84
|
+
const wantsHelp = argv
|
|
85
|
+
.slice(2)
|
|
86
|
+
.some((arg) => arg === "--help" || arg === "-h");
|
|
87
|
+
if (wantsHelp) {
|
|
88
|
+
const { formatVerbHelp } = await import("@canonical/cli-core");
|
|
89
|
+
process.stdout.write(`${formatVerbHelp(PROGRAM_NAME, doctorCommand)}\n`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
const ctx = { cwd: process.cwd(), globalFlags };
|
|
73
94
|
const result = await doctorCommand.execute({}, ctx);
|
|
74
95
|
if (result.tag === "output") {
|
|
@@ -91,7 +112,7 @@ async function handleRootHelp(globalFlags: GlobalFlags): Promise<void> {
|
|
|
91
112
|
try {
|
|
92
113
|
await program.parseAsync(["node", "pragma", "--help"]);
|
|
93
114
|
} catch (err) {
|
|
94
|
-
handleProgramError(err, globalFlags);
|
|
115
|
+
await handleProgramError(err, globalFlags, ["node", "pragma", "--help"]);
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
|
|
@@ -151,6 +172,8 @@ async function bootAndRun(
|
|
|
151
172
|
return;
|
|
152
173
|
}
|
|
153
174
|
|
|
175
|
+
await emitSchemaArtifact(runtime, globalFlags);
|
|
176
|
+
|
|
154
177
|
try {
|
|
155
178
|
const ctx: PragmaContext = {
|
|
156
179
|
...runtime,
|
|
@@ -161,12 +184,48 @@ async function bootAndRun(
|
|
|
161
184
|
const program = createProgram(commands, ctx);
|
|
162
185
|
await program.parseAsync(stripGlobalFlags(argv));
|
|
163
186
|
} catch (err) {
|
|
164
|
-
handleProgramError(err, globalFlags);
|
|
187
|
+
await handleProgramError(err, globalFlags, argv);
|
|
165
188
|
} finally {
|
|
166
189
|
runtime.dispose();
|
|
167
190
|
}
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Keep the `schema.graphql` artifact current on every booted run.
|
|
195
|
+
*
|
|
196
|
+
* The freshness probe is a source hash against the artifact's header, so
|
|
197
|
+
* the common case costs one hash + one small read; compilation runs only
|
|
198
|
+
* when the resolved TTL sources actually changed (or on first use).
|
|
199
|
+
* Never fatal — a failed compile warns and the command proceeds.
|
|
200
|
+
*/
|
|
201
|
+
async function emitSchemaArtifact(
|
|
202
|
+
runtime: PragmaRuntime,
|
|
203
|
+
globalFlags: GlobalFlags,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
try {
|
|
206
|
+
const { ensureSchemaArtifact } = await import(
|
|
207
|
+
"../domains/graphql/operations/index.js"
|
|
208
|
+
);
|
|
209
|
+
const result = await ensureSchemaArtifact(runtime);
|
|
210
|
+
if (result.status === "written") {
|
|
211
|
+
process.stderr.write(
|
|
212
|
+
`Updated GraphQL schema artifact (sources changed): ${result.path}\n`,
|
|
213
|
+
);
|
|
214
|
+
} else if (result.status === "failed" && globalFlags.verbose) {
|
|
215
|
+
process.stderr.write(
|
|
216
|
+
`Warning: GraphQL schema compilation failed; ${result.path} not updated. Run \`pragma graphql check\` for diagnostics.\n`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (globalFlags.verbose) {
|
|
221
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`Warning: could not update GraphQL schema artifact: ${reason}\n`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
170
229
|
async function runStoreSkip(
|
|
171
230
|
argv: readonly string[],
|
|
172
231
|
globalFlags: GlobalFlags,
|
|
@@ -185,6 +244,7 @@ async function runStoreSkip(
|
|
|
185
244
|
...refsCommands(),
|
|
186
245
|
...traceCommands(),
|
|
187
246
|
...graphqlCommands(),
|
|
247
|
+
...createCommands(),
|
|
188
248
|
buildCapabilitiesCommand(),
|
|
189
249
|
];
|
|
190
250
|
const program = createProgram(commands, stubCtx);
|
|
@@ -192,11 +252,53 @@ async function runStoreSkip(
|
|
|
192
252
|
try {
|
|
193
253
|
await program.parseAsync(stripGlobalFlags(argv));
|
|
194
254
|
} catch (err) {
|
|
195
|
-
handleProgramError(err, globalFlags);
|
|
255
|
+
await handleProgramError(err, globalFlags, argv);
|
|
196
256
|
}
|
|
197
257
|
}
|
|
198
258
|
|
|
199
|
-
|
|
259
|
+
/**
|
|
260
|
+
* First non-flag token in argv — the command noun, if any.
|
|
261
|
+
*
|
|
262
|
+
* Global flags are stripped first so a flag *value* (e.g. the `json` in
|
|
263
|
+
* `--format json`) is never mistaken for the noun; otherwise
|
|
264
|
+
* `pragma --format json standard frobnicate` would scan `json` and fail to
|
|
265
|
+
* suggest `standard`'s verbs.
|
|
266
|
+
*/
|
|
267
|
+
function findNoun(argv: readonly string[]): string | undefined {
|
|
268
|
+
return stripGlobalFlags(argv)
|
|
269
|
+
.slice(2)
|
|
270
|
+
.find((arg) => !arg.startsWith("-"));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* When an unknown command/verb is entered under a known noun, print that noun's
|
|
275
|
+
* valid verbs so the user can recover — "defer to the category that exists".
|
|
276
|
+
*
|
|
277
|
+
* @note Impure — writes the noun's verb list to stderr.
|
|
278
|
+
*/
|
|
279
|
+
async function suggestNounVerbs(argv: readonly string[]): Promise<void> {
|
|
280
|
+
const noun = findNoun(argv);
|
|
281
|
+
if (!noun) return;
|
|
282
|
+
const stubCtx: PragmaContext = {
|
|
283
|
+
store: {} as PragmaRuntime["store"],
|
|
284
|
+
...(await resolveHelpConfig()),
|
|
285
|
+
cwd: process.cwd(),
|
|
286
|
+
dispose: () => {},
|
|
287
|
+
globalFlags: { llm: false, autoLlm: false, format: "text", verbose: false },
|
|
288
|
+
promptSession: createInteractivePromptSession,
|
|
289
|
+
};
|
|
290
|
+
const commands = collectCommands(stubCtx);
|
|
291
|
+
const hasNoun = commands.some((cmd) => cmd.path.at(0) === noun);
|
|
292
|
+
if (!hasNoun) return;
|
|
293
|
+
const { formatNounHelp } = await import("@canonical/cli-core");
|
|
294
|
+
process.stderr.write(`\n${formatNounHelp(PROGRAM_NAME, noun, commands)}\n`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function handleProgramError(
|
|
298
|
+
err: unknown,
|
|
299
|
+
globalFlags: GlobalFlags,
|
|
300
|
+
argv: readonly string[],
|
|
301
|
+
): Promise<void> {
|
|
200
302
|
if (err instanceof CommanderError) {
|
|
201
303
|
if (
|
|
202
304
|
err.code === "commander.helpDisplayed" ||
|
|
@@ -205,6 +307,9 @@ function handleProgramError(err: unknown, globalFlags: GlobalFlags): void {
|
|
|
205
307
|
process.exitCode = 0;
|
|
206
308
|
return;
|
|
207
309
|
}
|
|
310
|
+
if (err.code === "commander.unknownCommand") {
|
|
311
|
+
await suggestNounVerbs(argv);
|
|
312
|
+
}
|
|
208
313
|
process.exitCode = err.exitCode;
|
|
209
314
|
return;
|
|
210
315
|
}
|
|
@@ -240,26 +345,64 @@ function handleProgramError(err: unknown, globalFlags: GlobalFlags): void {
|
|
|
240
345
|
*/
|
|
241
346
|
export default async function runCli(argv: readonly string[]): Promise<void> {
|
|
242
347
|
const globalFlags = parseGlobalFlags(argv);
|
|
243
|
-
|
|
348
|
+
|
|
349
|
+
const commandKind = resolveCommandKind(argv);
|
|
350
|
+
|
|
351
|
+
// Completion queries must be handled before the root-help shortcut:
|
|
352
|
+
// `pragma --completions` (empty first-Tab partial) has no non-flag command
|
|
353
|
+
// arg, so it would otherwise fall through to root help and dump the help
|
|
354
|
+
// banner into the shell's completion buffer.
|
|
355
|
+
if (commandKind.kind === "completions-client") {
|
|
356
|
+
return handleCompletionsClient(commandKind.partial);
|
|
357
|
+
}
|
|
358
|
+
if (commandKind.kind === "completions-server") {
|
|
359
|
+
return handleCompletionsServer();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// First-run onboarding: when the global config does not exist yet, greet on
|
|
363
|
+
// stderr and create it with defaults. After the completions early-exits so
|
|
364
|
+
// the note can never leak into a shell completion buffer; stderr-only so
|
|
365
|
+
// stdout (command output, --format json, MCP stdio) stays untouched; and
|
|
366
|
+
// failure-tolerant — onboarding must never break an invocation.
|
|
367
|
+
await ensureFirstRun();
|
|
368
|
+
|
|
369
|
+
// `--version`/`-V` is a global flag: print the version and exit regardless
|
|
370
|
+
// of where it appears (root or after a command/verb), so `block list -V`
|
|
371
|
+
// behaves like the root `pragma --version` instead of erroring.
|
|
372
|
+
if (argv.slice(2).some((arg) => arg === "--version" || arg === "-V")) {
|
|
373
|
+
process.stdout.write(`${VERSION}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const explicitHelp = argv
|
|
244
378
|
.slice(2)
|
|
245
|
-
.some(
|
|
246
|
-
(arg) =>
|
|
247
|
-
arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V",
|
|
248
|
-
);
|
|
379
|
+
.some((arg) => arg === "--help" || arg === "-h");
|
|
249
380
|
|
|
250
|
-
|
|
251
|
-
|
|
381
|
+
// Reject an unknown --format value early (completion queries and --version
|
|
382
|
+
// are handled above, so neither errors). Skip when the user asked for help —
|
|
383
|
+
// it should print regardless of a bad --format. Only text/json are supported.
|
|
384
|
+
const rawFormat = readRawFormat(argv);
|
|
385
|
+
if (
|
|
386
|
+
!explicitHelp &&
|
|
387
|
+
rawFormat !== undefined &&
|
|
388
|
+
rawFormat !== "text" &&
|
|
389
|
+
rawFormat !== "json"
|
|
390
|
+
) {
|
|
391
|
+
const error = PragmaError.invalidInput("format", rawFormat, {
|
|
392
|
+
validOptions: ["text", "json"],
|
|
393
|
+
});
|
|
394
|
+
process.stderr.write(`${renderError(error, globalFlags)}\n`);
|
|
395
|
+
process.exitCode = mapExitCode(error.code);
|
|
396
|
+
return;
|
|
252
397
|
}
|
|
253
398
|
|
|
254
|
-
|
|
399
|
+
if (!hasCommandArg(argv) && !explicitHelp) {
|
|
400
|
+
return handleRootHelp(globalFlags);
|
|
401
|
+
}
|
|
255
402
|
|
|
256
403
|
switch (commandKind.kind) {
|
|
257
|
-
case "completions-client":
|
|
258
|
-
return handleCompletionsClient(commandKind.partial);
|
|
259
|
-
case "completions-server":
|
|
260
|
-
return handleCompletionsServer();
|
|
261
404
|
case "doctor":
|
|
262
|
-
return handleDoctor(globalFlags);
|
|
405
|
+
return handleDoctor(argv, globalFlags);
|
|
263
406
|
case "store-skip":
|
|
264
407
|
return runStoreSkip(argv, globalFlags);
|
|
265
408
|
case "store-required":
|
package/src/testing/cli.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
import {
|
|
9
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
10
12
|
|
|
11
13
|
/** Captured output from a CLI subprocess invocation. */
|
|
12
14
|
interface CommandResult {
|
|
@@ -15,21 +17,47 @@ interface CommandResult {
|
|
|
15
17
|
exitCode: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* XDG home whose global config already exists, shared by all default
|
|
22
|
+
* invocations in this file's worker.
|
|
23
|
+
*
|
|
24
|
+
* Subprocesses would otherwise trigger the first-run greeting in whichever
|
|
25
|
+
* test happens to spawn first (the setup-file XDG is fresh per file), making
|
|
26
|
+
* stderr assertions ordering-dependent. Seeding the config keeps every
|
|
27
|
+
* default `runCommand` past first-run; tests that WANT the first-run path
|
|
28
|
+
* pass their own fresh XDG via `env`.
|
|
29
|
+
*/
|
|
30
|
+
let seededXdg: string | undefined;
|
|
31
|
+
function seededXdgHome(): string {
|
|
32
|
+
if (!seededXdg) {
|
|
33
|
+
seededXdg = mkdtempSync(join(tmpdir(), "pragma-cli-xdg-"));
|
|
34
|
+
mkdirSync(join(seededXdg, "pragma"), { recursive: true });
|
|
35
|
+
writeFileSync(join(seededXdg, "pragma", "config.json"), "{}\n");
|
|
36
|
+
}
|
|
37
|
+
return seededXdg;
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
/**
|
|
19
41
|
* Spawn the pragma CLI binary synchronously and capture its output.
|
|
20
42
|
*
|
|
21
43
|
* @param args - CLI arguments to pass after the binary path.
|
|
22
44
|
* @param cwd - Optional working directory for the subprocess.
|
|
45
|
+
* @param env - Extra environment overrides (merged over the seeded default).
|
|
23
46
|
* @returns Captured stdout, stderr, and exit code.
|
|
24
47
|
*
|
|
25
48
|
* @note Impure
|
|
26
49
|
*/
|
|
27
|
-
function runCommand(
|
|
50
|
+
function runCommand(
|
|
51
|
+
args: string[],
|
|
52
|
+
cwd?: string,
|
|
53
|
+
env?: Record<string, string>,
|
|
54
|
+
): CommandResult {
|
|
28
55
|
const binPath = resolve(import.meta.dirname, "../bin.ts");
|
|
29
56
|
const result = spawnSync("bun", ["run", binPath, ...args], {
|
|
30
57
|
cwd,
|
|
31
58
|
encoding: "utf-8",
|
|
32
59
|
timeout: 20_000,
|
|
60
|
+
env: { ...process.env, XDG_CONFIG_HOME: seededXdgHome(), ...env },
|
|
33
61
|
});
|
|
34
62
|
|
|
35
63
|
return {
|