@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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generator packs — the write-side analog of story packs.
|
|
3
|
+
*
|
|
4
|
+
* A generator pack is a set of summon generators (a package's `generators`
|
|
5
|
+
* export). This compiler enumerates them, groups by noun, and emits `create
|
|
6
|
+
* <noun>` CLI commands and `create_<noun>` MCP tools — the same data-driven
|
|
7
|
+
* surface story packs give read commands. The core stops hardcoding which
|
|
8
|
+
* generators exist; a package's full set is exposed automatically.
|
|
9
|
+
*
|
|
10
|
+
* Key convention (summon's own): `"<noun>/<variant>"` (e.g. `component/react`)
|
|
11
|
+
* groups under `<noun>` with a `<variant>` discriminator; a bare `"<noun>"`
|
|
12
|
+
* (e.g. `package`) is a single generator. A noun with one variant collapses to
|
|
13
|
+
* `create <noun>`; a noun with several becomes `create <noun> <variant>`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type CommandDefinition,
|
|
18
|
+
executeGenerator,
|
|
19
|
+
generatorToCommand,
|
|
20
|
+
type ParameterDefinition,
|
|
21
|
+
} from "@canonical/cli-core";
|
|
22
|
+
import type { AnyGenerator, PromptDefinition } from "@canonical/summon-core";
|
|
23
|
+
import { PragmaError } from "#error";
|
|
24
|
+
import type { ToolParamDef, ToolSpec } from "../../shared/ToolSpec.js";
|
|
25
|
+
import renderGeneratorUi from "../renderGeneratorUi.js";
|
|
26
|
+
|
|
27
|
+
/** A package's generator set: generator key → generator. */
|
|
28
|
+
export type GeneratorSet = Readonly<Record<string, AnyGenerator>>;
|
|
29
|
+
|
|
30
|
+
/** Compiled surfaces for one generator pack. */
|
|
31
|
+
export interface CompiledGeneratorPack {
|
|
32
|
+
readonly commands: readonly CommandDefinition[];
|
|
33
|
+
readonly specs: readonly ToolSpec[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A generator grouped under a noun, with its optional variant. */
|
|
37
|
+
interface GroupedGenerator {
|
|
38
|
+
readonly variant?: string;
|
|
39
|
+
readonly gen: AnyGenerator;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Split a generator key into `{ noun, variant? }` on the first slash. */
|
|
43
|
+
function parseGeneratorKey(key: string): { noun: string; variant?: string } {
|
|
44
|
+
const slash = key.indexOf("/");
|
|
45
|
+
if (slash === -1) return { noun: key };
|
|
46
|
+
return { noun: key.slice(0, slash), variant: key.slice(slash + 1) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Group a generator set by noun, preserving declaration order. */
|
|
50
|
+
function groupByNoun(
|
|
51
|
+
generators: GeneratorSet,
|
|
52
|
+
): Map<string, GroupedGenerator[]> {
|
|
53
|
+
const groups = new Map<string, GroupedGenerator[]>();
|
|
54
|
+
for (const [key, gen] of Object.entries(generators)) {
|
|
55
|
+
const { noun, variant } = parseGeneratorKey(key);
|
|
56
|
+
const group = groups.get(noun) ?? [];
|
|
57
|
+
group.push(variant !== undefined ? { variant, gen } : { gen });
|
|
58
|
+
groups.set(noun, group);
|
|
59
|
+
}
|
|
60
|
+
return groups;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Prompt type → tool param type, mirroring cli-core's `promptToParameter`. */
|
|
64
|
+
const PROMPT_TYPE_MAP: Record<PromptDefinition["type"], ToolParamDef["type"]> =
|
|
65
|
+
{
|
|
66
|
+
text: "string",
|
|
67
|
+
confirm: "boolean",
|
|
68
|
+
select: "string",
|
|
69
|
+
multiselect: "string[]",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Map a summon prompt to a neutral MCP tool parameter. */
|
|
73
|
+
function promptToToolParam(prompt: PromptDefinition): ToolParamDef {
|
|
74
|
+
const enumValues =
|
|
75
|
+
prompt.type === "confirm"
|
|
76
|
+
? undefined
|
|
77
|
+
: prompt.choices?.map((choice) => choice.value);
|
|
78
|
+
return {
|
|
79
|
+
type: PROMPT_TYPE_MAP[prompt.type],
|
|
80
|
+
description: prompt.message,
|
|
81
|
+
// A prompt with a default or a `when` condition is answerable without
|
|
82
|
+
// input; only an unconditional, defaultless prompt is required.
|
|
83
|
+
optional: prompt.default !== undefined || prompt.when !== undefined,
|
|
84
|
+
...(enumValues ? { enum: enumValues } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generator answers the MCP tool runner pins to fixed values.
|
|
90
|
+
*
|
|
91
|
+
* MCP create tools always produce a dry-run plan, so `runInstall` is forced
|
|
92
|
+
* off regardless of input. This one map drives BOTH the schema (pinned
|
|
93
|
+
* answers are excluded from the advertised params — a tool must not
|
|
94
|
+
* advertise a parameter it ignores) and the runtime override in
|
|
95
|
+
* {@link runGeneratorTool}, so the two can never drift.
|
|
96
|
+
*/
|
|
97
|
+
const PINNED_TOOL_ANSWERS: Readonly<Record<string, unknown>> = {
|
|
98
|
+
runInstall: false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the MCP param map for a generator from its prompts, omitting
|
|
103
|
+
* prompts whose answers are pinned by {@link PINNED_TOOL_ANSWERS}.
|
|
104
|
+
*/
|
|
105
|
+
function toolParamsFor(gen: AnyGenerator): Record<string, ToolParamDef> {
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
gen.prompts
|
|
108
|
+
.filter((prompt) => !(prompt.name in PINNED_TOOL_ANSWERS))
|
|
109
|
+
.map((prompt) => [prompt.name, promptToToolParam(prompt)]),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run one generator as an MCP tool call: json-mode dry-run plan.
|
|
115
|
+
*
|
|
116
|
+
* A generator that throws (e.g. an unmet hard requirement such as
|
|
117
|
+
* application's ssr/router pairing) is mapped to a typed INVALID_INPUT
|
|
118
|
+
* carrying the generator's own message as the recovery hint — the generic
|
|
119
|
+
* replacement for the per-generator validation the hand-written specs did.
|
|
120
|
+
*/
|
|
121
|
+
async function runGeneratorTool(
|
|
122
|
+
gen: AnyGenerator,
|
|
123
|
+
rt: { cwd: string },
|
|
124
|
+
params: Record<string, unknown>,
|
|
125
|
+
discriminator?: string,
|
|
126
|
+
): Promise<{ data: unknown }> {
|
|
127
|
+
const batchCtx = {
|
|
128
|
+
cwd: rt.cwd,
|
|
129
|
+
globalFlags: { llm: false, format: "json" as const, verbose: false },
|
|
130
|
+
};
|
|
131
|
+
// Apply the pinned answers (never install during a dry-run plan); the
|
|
132
|
+
// discriminator is dispatch metadata, not a generator answer.
|
|
133
|
+
const genParams: Record<string, unknown> = {
|
|
134
|
+
...params,
|
|
135
|
+
...PINNED_TOOL_ANSWERS,
|
|
136
|
+
};
|
|
137
|
+
if (discriminator) delete genParams[discriminator];
|
|
138
|
+
try {
|
|
139
|
+
const result = await executeGenerator(gen, genParams, batchCtx);
|
|
140
|
+
if (result.tag === "output") {
|
|
141
|
+
// json mode: result.value is already the structured plan.
|
|
142
|
+
return { data: result.value };
|
|
143
|
+
}
|
|
144
|
+
throw PragmaError.invalidInput("parameters", "(no plan produced)", {
|
|
145
|
+
recovery: { message: `The ${gen.meta.name} generator rejected them.` },
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof PragmaError) throw error;
|
|
149
|
+
throw PragmaError.invalidInput("parameters", "(rejected)", {
|
|
150
|
+
recovery: {
|
|
151
|
+
message: error instanceof Error ? error.message : String(error),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the variant → generator resolver shared by the CLI and MCP dispatch.
|
|
159
|
+
*
|
|
160
|
+
* @returns A function resolving a raw `variant` param to its generator.
|
|
161
|
+
* @throws PragmaError with code INVALID_INPUT for a missing/unknown variant.
|
|
162
|
+
*/
|
|
163
|
+
function buildVariantResolver(
|
|
164
|
+
group: readonly GroupedGenerator[],
|
|
165
|
+
variants: readonly string[],
|
|
166
|
+
): (variant: unknown) => AnyGenerator {
|
|
167
|
+
const byVariant = new Map(group.map((g) => [g.variant, g.gen]));
|
|
168
|
+
return (variant) => {
|
|
169
|
+
const gen =
|
|
170
|
+
typeof variant === "string" ? byVariant.get(variant) : undefined;
|
|
171
|
+
if (!gen) {
|
|
172
|
+
throw PragmaError.invalidInput(
|
|
173
|
+
"variant",
|
|
174
|
+
typeof variant === "string" ? variant : "(missing)",
|
|
175
|
+
{ validOptions: [...variants] },
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return gen;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Compile a package's generator set into `create <noun>` commands and
|
|
184
|
+
* `create_<noun>` MCP tools.
|
|
185
|
+
*
|
|
186
|
+
* @note Impure — warns on stderr when a degenerate group (duplicate noun
|
|
187
|
+
* with mixed bare/variant keys) forces generators to be skipped.
|
|
188
|
+
*/
|
|
189
|
+
export default function compileGeneratorPack(
|
|
190
|
+
generators: GeneratorSet,
|
|
191
|
+
): CompiledGeneratorPack {
|
|
192
|
+
const commands: CommandDefinition[] = [];
|
|
193
|
+
const specs: ToolSpec[] = [];
|
|
194
|
+
|
|
195
|
+
for (const [noun, group] of groupByNoun(generators)) {
|
|
196
|
+
const [first, ...rest] = group;
|
|
197
|
+
if (!first) continue; // groupByNoun never emits empty groups
|
|
198
|
+
|
|
199
|
+
const variants = group
|
|
200
|
+
.map((g) => g.variant)
|
|
201
|
+
.filter((v): v is string => v !== undefined);
|
|
202
|
+
|
|
203
|
+
if (rest.length > 0 && variants.length === group.length) {
|
|
204
|
+
commands.push(buildVariantCommand(noun, first.gen, group, variants));
|
|
205
|
+
specs.push(buildVariantSpec(noun, first.gen, group, variants));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (rest.length > 0) {
|
|
210
|
+
// Mixed bare/variant keys under one noun is a malformed set — expose
|
|
211
|
+
// the first generator rather than none, but never drop the rest
|
|
212
|
+
// silently.
|
|
213
|
+
process.stderr.write(
|
|
214
|
+
`Warning: generator noun "${noun}" mixes bare and variant keys; ` +
|
|
215
|
+
`only "${first.gen.meta.name}" is exposed.\n`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
commands.push(buildSingleCommand(noun, first.gen));
|
|
219
|
+
specs.push(buildSingleSpec(noun, first.gen));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { commands, specs };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** `create <noun>` for a single generator, with summon's rich Ink UI. */
|
|
226
|
+
function buildSingleCommand(
|
|
227
|
+
noun: string,
|
|
228
|
+
gen: AnyGenerator,
|
|
229
|
+
): CommandDefinition {
|
|
230
|
+
const base = generatorToCommand(["create", noun], gen);
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
execute: (params, ctx) => renderGeneratorUi(gen, params, ctx),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* `create <noun> <variant>` dispatch over several generators.
|
|
239
|
+
*
|
|
240
|
+
* Help and parameters project from the reference (first-declared) generator —
|
|
241
|
+
* variants of one noun are assumed prompt-compatible, the same assumption the
|
|
242
|
+
* hand-written component command made.
|
|
243
|
+
*/
|
|
244
|
+
function buildVariantCommand(
|
|
245
|
+
noun: string,
|
|
246
|
+
reference: AnyGenerator,
|
|
247
|
+
group: readonly GroupedGenerator[],
|
|
248
|
+
variants: readonly string[],
|
|
249
|
+
): CommandDefinition {
|
|
250
|
+
const resolve = buildVariantResolver(group, variants);
|
|
251
|
+
const base = generatorToCommand(["create", noun], reference);
|
|
252
|
+
const variantParam: ParameterDefinition = {
|
|
253
|
+
name: "variant",
|
|
254
|
+
description: `Which ${noun} to scaffold (${variants.join(", ")})`,
|
|
255
|
+
type: "select",
|
|
256
|
+
choices: variants.map((v) => ({ label: v, value: v })),
|
|
257
|
+
positional: true,
|
|
258
|
+
required: true,
|
|
259
|
+
};
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
parameters: [variantParam, ...base.parameters],
|
|
263
|
+
// async so an unknown variant rejects instead of throwing synchronously.
|
|
264
|
+
execute: async (params, ctx) =>
|
|
265
|
+
renderGeneratorUi(resolve(params.variant), params, ctx),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** `create_<noun>` MCP tool for a single generator. */
|
|
270
|
+
function buildSingleSpec(noun: string, gen: AnyGenerator): ToolSpec {
|
|
271
|
+
return {
|
|
272
|
+
name: `create_${noun}`,
|
|
273
|
+
description: `${gen.meta.description} Returns a JSON generation plan (dry-run).`,
|
|
274
|
+
params: toolParamsFor(gen),
|
|
275
|
+
readOnly: false,
|
|
276
|
+
destructive: false,
|
|
277
|
+
execute: (rt, params) => runGeneratorTool(gen, rt, params),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* `create_<noun>` MCP tool with a `variant` discriminator.
|
|
283
|
+
*
|
|
284
|
+
* The param schema projects from the reference generator's prompts (see
|
|
285
|
+
* {@link buildVariantCommand} for the compatibility assumption).
|
|
286
|
+
*/
|
|
287
|
+
function buildVariantSpec(
|
|
288
|
+
noun: string,
|
|
289
|
+
reference: AnyGenerator,
|
|
290
|
+
group: readonly GroupedGenerator[],
|
|
291
|
+
variants: readonly string[],
|
|
292
|
+
): ToolSpec {
|
|
293
|
+
const resolve = buildVariantResolver(group, variants);
|
|
294
|
+
return {
|
|
295
|
+
name: `create_${noun}`,
|
|
296
|
+
description: `${reference.meta.description} Returns a JSON generation plan (dry-run).`,
|
|
297
|
+
params: {
|
|
298
|
+
variant: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: `Which ${noun} to scaffold`,
|
|
301
|
+
optional: false,
|
|
302
|
+
enum: [...variants],
|
|
303
|
+
},
|
|
304
|
+
...toolParamsFor(reference),
|
|
305
|
+
},
|
|
306
|
+
readOnly: false,
|
|
307
|
+
destructive: false,
|
|
308
|
+
// async so an unknown variant rejects instead of throwing synchronously.
|
|
309
|
+
execute: async (rt, params) =>
|
|
310
|
+
runGeneratorTool(resolve(params.variant), rt, params, "variant"),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Create domain — generator-pack-driven `create <noun>` commands.
|
|
3
|
+
*
|
|
4
|
+
* The create surface is compiled from bundled generator packs (a package's
|
|
5
|
+
* `generators` export), not hardcoded per generator. Every generator each
|
|
6
|
+
* bundled package ships is exposed as a `create <noun>` command and a
|
|
7
|
+
* `create_<noun>` MCP tool. See {@link ./generatorPack/compileGeneratorPack}.
|
|
8
|
+
*/
|
|
2
9
|
|
|
3
10
|
import type { CommandDefinition } from "@canonical/cli-core";
|
|
4
|
-
import
|
|
11
|
+
import bundledGeneratorPacks from "./generatorPack/bundled.js";
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
|
-
* Return all create command definitions
|
|
8
|
-
*
|
|
9
|
-
* @returns An array of CommandDefinitions for component and package generators.
|
|
14
|
+
* Return all create command definitions, compiled from the bundled
|
|
15
|
+
* generator packs.
|
|
10
16
|
*/
|
|
11
|
-
export function commands(): CommandDefinition[] {
|
|
12
|
-
return
|
|
17
|
+
export function commands(): readonly CommandDefinition[] {
|
|
18
|
+
return bundledGeneratorPacks().commands;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export { specs as mcpSpecs } from "./mcp/index.js";
|
|
@@ -1,168 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP tool specs for create domain
|
|
2
|
+
* MCP tool specs for the create domain.
|
|
3
|
+
*
|
|
4
|
+
* Compiled from the bundled generator packs — one `create_<noun>` tool per
|
|
5
|
+
* generator noun — rather than hand-written per generator.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
import { executeGenerator } from "@canonical/cli-core";
|
|
6
|
-
import type { AnyGenerator } from "@canonical/summon-core";
|
|
7
|
-
import { generators as packageGenerators } from "@canonical/summon-package";
|
|
8
|
-
import { PragmaError } from "#error";
|
|
9
8
|
import type { ToolSpec } from "../../shared/ToolSpec.js";
|
|
10
|
-
import
|
|
9
|
+
import bundledGeneratorPacks from "../generatorPack/bundled.js";
|
|
11
10
|
|
|
12
|
-
const specs: readonly ToolSpec[] =
|
|
13
|
-
{
|
|
14
|
-
name: "create_component",
|
|
15
|
-
description:
|
|
16
|
-
"Generate a design system component with TypeScript, tests, stories, and styles. Returns a JSON generation plan (dry-run).",
|
|
17
|
-
params: {
|
|
18
|
-
framework: {
|
|
19
|
-
type: "string",
|
|
20
|
-
description: "Component framework",
|
|
21
|
-
optional: false,
|
|
22
|
-
enum: ["react", "svelte", "lit"],
|
|
23
|
-
},
|
|
24
|
-
componentPath: {
|
|
25
|
-
type: "string",
|
|
26
|
-
description:
|
|
27
|
-
"Path for the component directory (e.g. 'src/components/Button')",
|
|
28
|
-
optional: false,
|
|
29
|
-
},
|
|
30
|
-
withStyles: {
|
|
31
|
-
type: "boolean",
|
|
32
|
-
description: "Include CSS styles (default: true)",
|
|
33
|
-
optional: true,
|
|
34
|
-
},
|
|
35
|
-
withStories: {
|
|
36
|
-
type: "boolean",
|
|
37
|
-
description: "Include Storybook stories (default: true)",
|
|
38
|
-
optional: true,
|
|
39
|
-
},
|
|
40
|
-
withSsrTests: {
|
|
41
|
-
type: "boolean",
|
|
42
|
-
description: "Include SSR tests (default: true)",
|
|
43
|
-
optional: true,
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
readOnly: false,
|
|
47
|
-
destructive: false,
|
|
48
|
-
async execute(rt, params) {
|
|
49
|
-
const batchCtx = {
|
|
50
|
-
cwd: rt.cwd,
|
|
51
|
-
globalFlags: {
|
|
52
|
-
llm: false,
|
|
53
|
-
format: "json" as const,
|
|
54
|
-
verbose: false,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const framework = params.framework as string;
|
|
59
|
-
const gen = COMPONENT_GENERATORS[framework];
|
|
60
|
-
if (!gen) {
|
|
61
|
-
throw PragmaError.invalidInput("framework", framework, {
|
|
62
|
-
validOptions: Object.keys(COMPONENT_GENERATORS),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const genParams: Record<string, unknown> = {
|
|
67
|
-
componentPath: params.componentPath,
|
|
68
|
-
...(params.withStyles !== undefined && {
|
|
69
|
-
withStyles: params.withStyles,
|
|
70
|
-
}),
|
|
71
|
-
...(params.withStories !== undefined && {
|
|
72
|
-
withStories: params.withStories,
|
|
73
|
-
}),
|
|
74
|
-
...(params.withSsrTests !== undefined && {
|
|
75
|
-
withSsrTests: params.withSsrTests,
|
|
76
|
-
}),
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const result = await executeGenerator(gen, genParams, batchCtx);
|
|
80
|
-
if (result.tag === "output") {
|
|
81
|
-
const text = result.render.plain(result.value);
|
|
82
|
-
return { data: JSON.parse(text) };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
throw PragmaError.invalidInput(
|
|
86
|
-
"componentPath",
|
|
87
|
-
String(params.componentPath),
|
|
88
|
-
);
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: "create_package",
|
|
93
|
-
description:
|
|
94
|
-
"Generate a new npm package with proper configuration. Returns a JSON generation plan (dry-run).",
|
|
95
|
-
params: {
|
|
96
|
-
name: {
|
|
97
|
-
type: "string",
|
|
98
|
-
description: "Full package name (e.g. '@canonical/my-package')",
|
|
99
|
-
optional: false,
|
|
100
|
-
},
|
|
101
|
-
type: {
|
|
102
|
-
type: "string",
|
|
103
|
-
description: "Package type",
|
|
104
|
-
optional: false,
|
|
105
|
-
enum: ["tool-ts", "library", "css"],
|
|
106
|
-
},
|
|
107
|
-
description: {
|
|
108
|
-
type: "string",
|
|
109
|
-
description: "Package description",
|
|
110
|
-
optional: true,
|
|
111
|
-
},
|
|
112
|
-
withReact: {
|
|
113
|
-
type: "boolean",
|
|
114
|
-
description: "Add React dependencies (default: false)",
|
|
115
|
-
optional: true,
|
|
116
|
-
},
|
|
117
|
-
withStorybook: {
|
|
118
|
-
type: "boolean",
|
|
119
|
-
description: "Add Storybook config (default: false)",
|
|
120
|
-
optional: true,
|
|
121
|
-
},
|
|
122
|
-
withCli: {
|
|
123
|
-
type: "boolean",
|
|
124
|
-
description: "Add CLI entry point (default: false)",
|
|
125
|
-
optional: true,
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
readOnly: false,
|
|
129
|
-
destructive: false,
|
|
130
|
-
async execute(rt, params) {
|
|
131
|
-
const batchCtx = {
|
|
132
|
-
cwd: rt.cwd,
|
|
133
|
-
globalFlags: {
|
|
134
|
-
llm: false,
|
|
135
|
-
format: "json" as const,
|
|
136
|
-
verbose: false,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const gen = packageGenerators.package as AnyGenerator | undefined;
|
|
141
|
-
if (!gen) throw PragmaError.internalError("Package generator not found");
|
|
142
|
-
|
|
143
|
-
const genParams: Record<string, unknown> = {
|
|
144
|
-
name: params.name,
|
|
145
|
-
type: params.type,
|
|
146
|
-
...(params.description !== undefined && {
|
|
147
|
-
description: params.description,
|
|
148
|
-
}),
|
|
149
|
-
...(params.withReact !== undefined && { withReact: params.withReact }),
|
|
150
|
-
...(params.withStorybook !== undefined && {
|
|
151
|
-
withStorybook: params.withStorybook,
|
|
152
|
-
}),
|
|
153
|
-
...(params.withCli !== undefined && { withCli: params.withCli }),
|
|
154
|
-
runInstall: false,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const result = await executeGenerator(gen, genParams, batchCtx);
|
|
158
|
-
if (result.tag === "output") {
|
|
159
|
-
const text = result.render.plain(result.value);
|
|
160
|
-
return { data: JSON.parse(text) };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
throw PragmaError.invalidInput("name", String(params.name));
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
];
|
|
11
|
+
const specs: readonly ToolSpec[] = bundledGeneratorPacks().specs;
|
|
167
12
|
|
|
168
13
|
export default specs;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { CommandContext } from "@canonical/cli-core";
|
|
2
|
+
import type { GeneratorDefinition } from "@canonical/summon-core";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const executeGeneratorMock = vi.fn(async () => ({
|
|
6
|
+
tag: "output" as const,
|
|
7
|
+
value: "",
|
|
8
|
+
render: { plain: () => "" },
|
|
9
|
+
}));
|
|
10
|
+
const renderAppMock = vi.fn(async () => {});
|
|
11
|
+
|
|
12
|
+
vi.mock("@canonical/cli-core", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import("@canonical/cli-core")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
executeGenerator: executeGeneratorMock,
|
|
17
|
+
createGeneratorStamp: () => ({ generatorName: "g", version: "1" }),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock("@canonical/summon/ui", () => ({ renderApp: renderAppMock }));
|
|
22
|
+
|
|
23
|
+
const gen = {
|
|
24
|
+
meta: {},
|
|
25
|
+
prompts: [],
|
|
26
|
+
generate: () => ({}),
|
|
27
|
+
} as unknown as GeneratorDefinition;
|
|
28
|
+
|
|
29
|
+
function makeCtx(overrides: Partial<CommandContext["globalFlags"]> = {}) {
|
|
30
|
+
return {
|
|
31
|
+
cwd: "/tmp",
|
|
32
|
+
globalFlags: { llm: false, format: "text", verbose: false, ...overrides },
|
|
33
|
+
} as CommandContext;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function withTty<T>(
|
|
37
|
+
stdin: boolean,
|
|
38
|
+
stdout: boolean,
|
|
39
|
+
run: () => Promise<T>,
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
const inPrev = process.stdin.isTTY;
|
|
42
|
+
const outPrev = process.stdout.isTTY;
|
|
43
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
44
|
+
value: stdin,
|
|
45
|
+
configurable: true,
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
48
|
+
value: stdout,
|
|
49
|
+
configurable: true,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
return await run();
|
|
53
|
+
} finally {
|
|
54
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
55
|
+
value: inPrev,
|
|
56
|
+
configurable: true,
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
59
|
+
value: outPrev,
|
|
60
|
+
configurable: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("renderGeneratorUi", () => {
|
|
66
|
+
afterEach(() => vi.clearAllMocks());
|
|
67
|
+
|
|
68
|
+
it("renders summon's Ink UI on an interactive terminal", async () => {
|
|
69
|
+
const { default: renderGeneratorUi } = await import(
|
|
70
|
+
"./renderGeneratorUi.js"
|
|
71
|
+
);
|
|
72
|
+
const result = await withTty(true, true, () =>
|
|
73
|
+
renderGeneratorUi(gen, {}, makeCtx()),
|
|
74
|
+
);
|
|
75
|
+
expect(renderAppMock).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(executeGeneratorMock).not.toHaveBeenCalled();
|
|
77
|
+
expect(result).toEqual({ tag: "exit", code: 0 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("falls back to executeGenerator on a non-interactive terminal", async () => {
|
|
81
|
+
const { default: renderGeneratorUi } = await import(
|
|
82
|
+
"./renderGeneratorUi.js"
|
|
83
|
+
);
|
|
84
|
+
await withTty(false, false, () => renderGeneratorUi(gen, {}, makeCtx()));
|
|
85
|
+
expect(executeGeneratorMock).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(renderAppMock).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it.each([
|
|
90
|
+
["--yes", { yes: true }],
|
|
91
|
+
["--dry-run", { dryRun: true }],
|
|
92
|
+
["--undo", { undo: true }],
|
|
93
|
+
["--llm", { llm: true }],
|
|
94
|
+
["--format json", { format: "json" }],
|
|
95
|
+
])("falls back to executeGenerator for %s even on a TTY", async (_label, params) => {
|
|
96
|
+
const { default: renderGeneratorUi } = await import(
|
|
97
|
+
"./renderGeneratorUi.js"
|
|
98
|
+
);
|
|
99
|
+
await withTty(true, true, () => renderGeneratorUi(gen, params, makeCtx()));
|
|
100
|
+
expect(executeGeneratorMock).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(renderAppMock).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route a generator run to the right front-end.
|
|
3
|
+
*
|
|
4
|
+
* On an interactive terminal (no `--yes`, not a machine-output or preview-only
|
|
5
|
+
* mode), render summon's Ink `App` — the same colored prompt sequence,
|
|
6
|
+
* operations recap, and "Proceed?" confirmation the `summon` binary shows — so
|
|
7
|
+
* `pragma create`/`setup` match summon exactly without duplicating the UI. In
|
|
8
|
+
* every other case (piped/CI, `--yes`, `--llm`/`--format json`, `--dry-run`,
|
|
9
|
+
* `--undo`) fall back to the shared UI-free {@link executeGenerator}, which owns
|
|
10
|
+
* the batch, preview, and machine-readable paths.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type CommandContext,
|
|
15
|
+
type CommandResult,
|
|
16
|
+
createExitResult,
|
|
17
|
+
createGeneratorStamp,
|
|
18
|
+
executeGenerator,
|
|
19
|
+
} from "@canonical/cli-core";
|
|
20
|
+
import type { GeneratorDefinition } from "@canonical/summon-core";
|
|
21
|
+
|
|
22
|
+
/** Whether both stdin and stdout are attached to an interactive terminal. */
|
|
23
|
+
function isInteractiveTerminal(): boolean {
|
|
24
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run a generator, preferring summon's rich Ink UI when interactive.
|
|
29
|
+
*
|
|
30
|
+
* @param gen - The generator to run.
|
|
31
|
+
* @param params - Parsed CLI params (flags + positional answers).
|
|
32
|
+
* @param ctx - The command context (global flags, cwd).
|
|
33
|
+
* @returns A CommandResult; the Ink path exits 0 after the UI closes.
|
|
34
|
+
* @note Impure — may take over the terminal via Ink and perform the run.
|
|
35
|
+
*/
|
|
36
|
+
export default async function renderGeneratorUi(
|
|
37
|
+
gen: GeneratorDefinition,
|
|
38
|
+
params: Record<string, unknown>,
|
|
39
|
+
ctx: CommandContext,
|
|
40
|
+
): Promise<CommandResult> {
|
|
41
|
+
const isLlm = params.llm === true || ctx.globalFlags.llm;
|
|
42
|
+
const isJson = params.format === "json" || ctx.globalFlags.format === "json";
|
|
43
|
+
const wantsBatch =
|
|
44
|
+
params.yes === true ||
|
|
45
|
+
params.dryRun === true ||
|
|
46
|
+
params.undo === true ||
|
|
47
|
+
isLlm ||
|
|
48
|
+
isJson;
|
|
49
|
+
|
|
50
|
+
// Machine/preview/batch modes and non-interactive terminals stay on the
|
|
51
|
+
// UI-free executor.
|
|
52
|
+
if (wantsBatch || !isInteractiveTerminal()) {
|
|
53
|
+
return executeGenerator(gen, params, ctx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { renderApp } = await import("@canonical/summon/ui");
|
|
57
|
+
const stampEnabled = params.generatedStamp !== false;
|
|
58
|
+
await renderApp({
|
|
59
|
+
generator: gen,
|
|
60
|
+
preview: params.preview !== false,
|
|
61
|
+
verbose: ctx.globalFlags.verbose === true,
|
|
62
|
+
stamp: stampEnabled ? createGeneratorStamp(gen) : undefined,
|
|
63
|
+
});
|
|
64
|
+
return createExitResult(0);
|
|
65
|
+
}
|