@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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { URI } from "@canonical/ke";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
4
4
|
import type { BlockSummary } from "../../shared/types/index.js";
|
|
5
5
|
import formatters from "./list.js";
|
|
6
6
|
|
|
7
7
|
const BUTTON: BlockSummary = {
|
|
8
|
-
uri: `${
|
|
8
|
+
uri: `${DEFAULT_PREFIX_MAP.ds}button` as URI,
|
|
9
9
|
name: "Button",
|
|
10
10
|
tier: "global",
|
|
11
11
|
modifiers: ["importance", "density"],
|
|
@@ -18,7 +18,7 @@ const BUTTON: BlockSummary = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const CARD: BlockSummary = {
|
|
21
|
-
uri: `${
|
|
21
|
+
uri: `${DEFAULT_PREFIX_MAP.ds}card` as URI,
|
|
22
22
|
name: "Card",
|
|
23
23
|
tier: "global",
|
|
24
24
|
modifiers: [],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { URI } from "@canonical/ke";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
4
4
|
import type { BlockDetailed } from "../../shared/types/index.js";
|
|
5
5
|
import type { AspectFlags } from "../types.js";
|
|
6
6
|
import formatters from "./lookup.js";
|
|
7
7
|
|
|
8
8
|
const BUTTON_DETAILED: BlockDetailed = {
|
|
9
|
-
uri: `${
|
|
9
|
+
uri: `${DEFAULT_PREFIX_MAP.ds}global.component.button` as URI,
|
|
10
10
|
name: "Button",
|
|
11
11
|
type: "component",
|
|
12
12
|
tier: "global",
|
|
@@ -40,7 +40,7 @@ const BUTTON_DETAILED: BlockDetailed = {
|
|
|
40
40
|
],
|
|
41
41
|
modifierFamilies: [
|
|
42
42
|
{
|
|
43
|
-
uri: `${
|
|
43
|
+
uri: `${DEFAULT_PREFIX_MAP.ds}modifier_family.importance` as URI,
|
|
44
44
|
name: "importance",
|
|
45
45
|
values: ["default", "primary", "secondary"],
|
|
46
46
|
},
|
|
@@ -56,7 +56,7 @@ const BUTTON_DETAILED: BlockDetailed = {
|
|
|
56
56
|
],
|
|
57
57
|
subcomponents: [
|
|
58
58
|
{
|
|
59
|
-
uri: `${
|
|
59
|
+
uri: `${DEFAULT_PREFIX_MAP.ds}global.subcomponent.button.icon` as URI,
|
|
60
60
|
name: "Button Icon",
|
|
61
61
|
},
|
|
62
62
|
],
|
|
@@ -65,7 +65,7 @@ const BUTTON_DETAILED: BlockDetailed = {
|
|
|
65
65
|
],
|
|
66
66
|
tokens: [
|
|
67
67
|
{
|
|
68
|
-
uri: `${
|
|
68
|
+
uri: `${DEFAULT_PREFIX_MAP.ds}token.color.primary` as URI,
|
|
69
69
|
name: "color.primary",
|
|
70
70
|
},
|
|
71
71
|
],
|
|
@@ -14,12 +14,15 @@ export default async function enrichBlocks(
|
|
|
14
14
|
filters: FilterConfig,
|
|
15
15
|
disclosure: Disclosure,
|
|
16
16
|
): Promise<readonly (BlockListDigest | BlockDetailed)[]> {
|
|
17
|
+
// Look up by each summary's stable unique URI, not its display name: names
|
|
18
|
+
// recur across tiers/packages, so a by-name lookup returns every same-named
|
|
19
|
+
// block and taking the first match collapses distinct blocks onto one.
|
|
17
20
|
const nested = await Promise.all(
|
|
18
|
-
summaries.map((summary) => lookupBlock(store, summary.
|
|
21
|
+
summaries.map((summary) => lookupBlock(store, summary.uri, filters)),
|
|
19
22
|
);
|
|
20
|
-
// lookupBlock returns BlockDetailed[] —
|
|
23
|
+
// lookupBlock returns BlockDetailed[] — the URI resolves to exactly one match
|
|
21
24
|
const details: BlockDetailed[] = nested.flatMap((matches) =>
|
|
22
|
-
matches
|
|
25
|
+
matches.at(0) ? [matches[0] as BlockDetailed] : [],
|
|
23
26
|
);
|
|
24
27
|
|
|
25
28
|
if (disclosure.level === "detailed") {
|
|
@@ -113,9 +113,15 @@ export const blockLookupStory: LookupStory<BlockDetailed, BlockLookupInput> = {
|
|
|
113
113
|
complete: async (partial, cmdCtx) => {
|
|
114
114
|
const ctx = requirePragmaContext(cmdCtx);
|
|
115
115
|
const all = await listBlocks(ctx.store, ctx.config);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const lower = partial.toLowerCase();
|
|
117
|
+
// Block names recur across tiers/packages; dedupe so completion offers each
|
|
118
|
+
// name once.
|
|
119
|
+
const unique = new Set(
|
|
120
|
+
all
|
|
121
|
+
.map((block) => block.name)
|
|
122
|
+
.filter((name) => name.toLowerCase().startsWith(lower)),
|
|
123
|
+
);
|
|
124
|
+
return [...unique];
|
|
119
125
|
},
|
|
120
126
|
detailedParam: {
|
|
121
127
|
description:
|
|
@@ -60,6 +60,22 @@ describe("config channel command", () => {
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("rejects a value combined with --reset", async () => {
|
|
64
|
+
const ctx = makeCtx(dir);
|
|
65
|
+
const cmd = buildChannelCommand(ctx);
|
|
66
|
+
await expect(
|
|
67
|
+
cmd.execute({ value: "experimental", reset: true }, ctx),
|
|
68
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects an explicit empty-string value", async () => {
|
|
72
|
+
const ctx = makeCtx(dir);
|
|
73
|
+
const cmd = buildChannelCommand(ctx);
|
|
74
|
+
await expect(cmd.execute({ value: "" }, ctx)).rejects.toMatchObject({
|
|
75
|
+
code: "INVALID_INPUT",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
63
79
|
it("resets channel via --reset flag", async () => {
|
|
64
80
|
writeFileSync(
|
|
65
81
|
join(dir, "pragma.config.json"),
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@canonical/cli-core";
|
|
6
6
|
import { runTask } from "@canonical/task/node";
|
|
7
7
|
import { readConfig } from "#config";
|
|
8
|
+
import { PragmaError } from "#error";
|
|
8
9
|
import type { PragmaContext } from "../../shared/context.js";
|
|
9
10
|
import { selectFormatter } from "../../shared/formatters.js";
|
|
10
11
|
import { channelFormatters } from "../formatters/index.js";
|
|
@@ -56,6 +57,16 @@ export default function buildChannelCommand(
|
|
|
56
57
|
const reset = params.reset === true;
|
|
57
58
|
const value = params.value as string | undefined;
|
|
58
59
|
|
|
60
|
+
// Contradictory input: a value AND --reset. Reject rather than silently
|
|
61
|
+
// letting one win.
|
|
62
|
+
if (reset && value !== undefined) {
|
|
63
|
+
throw PragmaError.invalidInput("channel", value, {
|
|
64
|
+
recovery: {
|
|
65
|
+
message: "Pass either a channel value or --reset, not both.",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
if (reset) {
|
|
60
71
|
const result = await runTask(setChannelTask(ctx.cwd, undefined, scope));
|
|
61
72
|
const format = selectFormatter(ctx, channelFormatters.reset);
|
|
@@ -65,7 +76,9 @@ export default function buildChannelCommand(
|
|
|
65
76
|
);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
// No value at all → query the current channel. An explicit empty string
|
|
80
|
+
// is invalid input, not a query.
|
|
81
|
+
if (value === undefined) {
|
|
69
82
|
const config = readConfig(ctx.cwd);
|
|
70
83
|
const format = selectFormatter(ctx, channelFormatters.query);
|
|
71
84
|
return createOutputResult(config.channel, {
|
|
@@ -62,6 +62,20 @@ describe("config framework command", () => {
|
|
|
62
62
|
expect(readConfig(dir).framework).toBe("lit");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("rejects a value combined with --reset", async () => {
|
|
66
|
+
const ctx = makeCtx(dir);
|
|
67
|
+
await expect(
|
|
68
|
+
buildFrameworkCommand(ctx).execute({ value: "react", reset: true }, ctx),
|
|
69
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects an explicit empty-string value", async () => {
|
|
73
|
+
const ctx = makeCtx(dir);
|
|
74
|
+
await expect(
|
|
75
|
+
buildFrameworkCommand(ctx).execute({ value: "" }, ctx),
|
|
76
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
77
|
+
});
|
|
78
|
+
|
|
65
79
|
it("rejects an invalid framework", async () => {
|
|
66
80
|
const ctx = makeCtx(dir);
|
|
67
81
|
await expect(
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@canonical/cli-core";
|
|
6
6
|
import { runTask } from "@canonical/task/node";
|
|
7
7
|
import { readConfig } from "#config";
|
|
8
|
+
import { PragmaError } from "#error";
|
|
8
9
|
import type { PragmaContext } from "../../shared/context.js";
|
|
9
10
|
import { selectFormatter } from "../../shared/formatters.js";
|
|
10
11
|
import { frameworkFormatters } from "../formatters/index.js";
|
|
@@ -59,6 +60,16 @@ export default function buildFrameworkCommand(
|
|
|
59
60
|
const reset = params.reset === true;
|
|
60
61
|
const value = params.value as string | undefined;
|
|
61
62
|
|
|
63
|
+
// Contradictory input: a value AND --reset. Reject rather than silently
|
|
64
|
+
// letting one win.
|
|
65
|
+
if (reset && value !== undefined) {
|
|
66
|
+
throw PragmaError.invalidInput("framework", value, {
|
|
67
|
+
recovery: {
|
|
68
|
+
message: "Pass either a framework value or --reset, not both.",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
if (reset) {
|
|
63
74
|
const result = await runTask(
|
|
64
75
|
setFrameworkTask(ctx.cwd, undefined, scope),
|
|
@@ -70,7 +81,8 @@ export default function buildFrameworkCommand(
|
|
|
70
81
|
);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
// No value at all → query. An explicit empty string is invalid input.
|
|
85
|
+
if (value === undefined) {
|
|
74
86
|
const config = readConfig(ctx.cwd);
|
|
75
87
|
const format = selectFormatter(ctx, frameworkFormatters.query);
|
|
76
88
|
return createOutputResult(config.framework ?? "none", {
|
|
@@ -92,4 +92,11 @@ describe("config tier command", () => {
|
|
|
92
92
|
expect(text).toBe("No tier set (all tiers visible).");
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
it("rejects a tier path combined with --reset", async () => {
|
|
97
|
+
const ctx = makeCtx(dir);
|
|
98
|
+
await expect(
|
|
99
|
+
buildTierCommand(ctx).execute({ path: "apps/lxd", reset: true }, ctx),
|
|
100
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
101
|
+
});
|
|
95
102
|
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@canonical/cli-core";
|
|
6
6
|
import { runTask } from "@canonical/task/node";
|
|
7
7
|
import { readConfig } from "#config";
|
|
8
|
+
import { PragmaError } from "#error";
|
|
8
9
|
import type { PragmaContext } from "../../shared/context.js";
|
|
9
10
|
import { selectFormatter } from "../../shared/formatters.js";
|
|
10
11
|
import { tierFormatters } from "../formatters/index.js";
|
|
@@ -53,6 +54,16 @@ export default function buildTierCommand(
|
|
|
53
54
|
const reset = params.reset === true;
|
|
54
55
|
const tierPath = params.path as string | undefined;
|
|
55
56
|
|
|
57
|
+
// Contradictory input: a tier path AND --reset. Reject rather than
|
|
58
|
+
// silently letting one win.
|
|
59
|
+
if (reset && tierPath !== undefined) {
|
|
60
|
+
throw PragmaError.invalidInput("tier", tierPath, {
|
|
61
|
+
recovery: {
|
|
62
|
+
message: "Pass either a tier path or --reset, not both.",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
if (reset) {
|
|
57
68
|
const result = await runTask(setTierTask(ctx.cwd, undefined, scope));
|
|
58
69
|
const format = selectFormatter(ctx, tierFormatters.reset);
|
|
@@ -62,7 +73,9 @@ export default function buildTierCommand(
|
|
|
62
73
|
);
|
|
63
74
|
}
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
// No path at all → query. An explicit empty string is invalid input and
|
|
77
|
+
// is rejected by validateTier below.
|
|
78
|
+
if (tierPath === undefined) {
|
|
66
79
|
const config = readConfig(ctx.cwd);
|
|
67
80
|
const format = selectFormatter(ctx, tierFormatters.query);
|
|
68
81
|
return createOutputResult(config.tier, {
|
|
@@ -30,6 +30,15 @@ describe("validateTier", () => {
|
|
|
30
30
|
expect(entry.path).toBe("global");
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it("matches case- and whitespace-insensitively", async () => {
|
|
34
|
+
// The documented lowercase slash-path must match regardless of the label's
|
|
35
|
+
// casing in the ontology (e.g. real graph uses `Apps/LXD`).
|
|
36
|
+
const upper = await validateTier(store, "APPS/LXD");
|
|
37
|
+
expect(upper.path).toBe("apps/lxd");
|
|
38
|
+
const padded = await validateTier(store, " Apps/Lxd ");
|
|
39
|
+
expect(padded.path).toBe("apps/lxd");
|
|
40
|
+
});
|
|
41
|
+
|
|
33
42
|
it("throws INVALID_INPUT for a nonexistent tier", async () => {
|
|
34
43
|
await expect(validateTier(store, "apps/nonexistent")).rejects.toThrow(
|
|
35
44
|
'Invalid tier "apps/nonexistent"',
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { Store } from "@canonical/ke";
|
|
2
2
|
import { PragmaError } from "#error";
|
|
3
|
+
import { listTiers } from "../../shared/listTiers.js";
|
|
3
4
|
import type { TierEntry } from "../../shared/types/index.js";
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
/** Normalise a tier path for comparison: trim and lowercase. */
|
|
7
|
+
function normalizeTierPath(path: string): string {
|
|
8
|
+
return path.trim().toLowerCase();
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Validate a tier path against the ke store ontology.
|
|
@@ -17,7 +22,10 @@ export default async function validateTier(
|
|
|
17
22
|
tierPath: string,
|
|
18
23
|
): Promise<TierEntry> {
|
|
19
24
|
const tiers = await listTiers(store);
|
|
20
|
-
|
|
25
|
+
// Compare case/format-insensitively so the documented lowercase slash-path
|
|
26
|
+
// (`apps/lxd`) matches the ontology's title-case label (`Apps/LXD`).
|
|
27
|
+
const target = normalizeTierPath(tierPath);
|
|
28
|
+
const match = tiers.find((t) => normalizeTierPath(t.path) === target);
|
|
21
29
|
|
|
22
30
|
if (!match) {
|
|
23
31
|
const validPaths = tiers.map((t) => t.path);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled generator packs.
|
|
3
|
+
*
|
|
4
|
+
* The generator sets pragma ships by default, statically imported so the
|
|
5
|
+
* single-file build embeds them. Each package's full generator set is exposed —
|
|
6
|
+
* every generator, always — grouped into `create <noun>` commands and
|
|
7
|
+
* `create_<noun>` MCP tools by {@link compileGeneratorPack}.
|
|
8
|
+
*
|
|
9
|
+
* This is the write-side counterpart of the bundled story packs. In a later
|
|
10
|
+
* slice these move into the packages themselves (declared, dynamically
|
|
11
|
+
* resolved) so the core ships no generator wiring at all; today they are
|
|
12
|
+
* bundled transitional entries, like the story packs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { generators as applicationGenerators } from "@canonical/summon-application";
|
|
16
|
+
import { generators as componentGenerators } from "@canonical/summon-component";
|
|
17
|
+
import { generators as packageGenerators } from "@canonical/summon-package";
|
|
18
|
+
import compileGeneratorPack, {
|
|
19
|
+
type CompiledGeneratorPack,
|
|
20
|
+
type GeneratorSet,
|
|
21
|
+
} from "./compileGeneratorPack.js";
|
|
22
|
+
|
|
23
|
+
/** Every generator set pragma bundles, in surface order. */
|
|
24
|
+
const BUNDLED_GENERATOR_SETS: readonly GeneratorSet[] = [
|
|
25
|
+
componentGenerators,
|
|
26
|
+
packageGenerators,
|
|
27
|
+
applicationGenerators,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
let cached: CompiledGeneratorPack | undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compile every bundled generator pack into its CLI commands and MCP specs.
|
|
34
|
+
*
|
|
35
|
+
* Memoized — the generator sets are static, and both the CLI and MCP
|
|
36
|
+
* registration paths call this.
|
|
37
|
+
*
|
|
38
|
+
* @returns The merged commands and specs across all bundled packs.
|
|
39
|
+
*/
|
|
40
|
+
export default function bundledGeneratorPacks(): CompiledGeneratorPack {
|
|
41
|
+
cached ??= BUNDLED_GENERATOR_SETS.map(compileGeneratorPack).reduce(
|
|
42
|
+
(merged, pack) => ({
|
|
43
|
+
commands: [...merged.commands, ...pack.commands],
|
|
44
|
+
specs: [...merged.specs, ...pack.specs],
|
|
45
|
+
}),
|
|
46
|
+
{ commands: [], specs: [] },
|
|
47
|
+
);
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { AnyGenerator } from "@canonical/summon-core";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { PragmaRuntime } from "../../shared/types/index.js";
|
|
4
|
+
import bundledGeneratorPacks from "./bundled.js";
|
|
5
|
+
import compileGeneratorPack from "./compileGeneratorPack.js";
|
|
6
|
+
|
|
7
|
+
describe("compileGeneratorPack (bundled)", () => {
|
|
8
|
+
const { commands, specs } = bundledGeneratorPacks();
|
|
9
|
+
const cliPaths = commands.map((c) => c.path.join(" "));
|
|
10
|
+
const toolNames = specs.map((s) => s.name);
|
|
11
|
+
|
|
12
|
+
it("exposes every generator from every bundled package as a create command", () => {
|
|
13
|
+
// component (react/svelte/lit → one noun), package, and the four
|
|
14
|
+
// application-package generators.
|
|
15
|
+
expect(cliPaths).toEqual([
|
|
16
|
+
"create component",
|
|
17
|
+
"create package",
|
|
18
|
+
"create application",
|
|
19
|
+
"create domain",
|
|
20
|
+
"create route",
|
|
21
|
+
"create wrapper",
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("emits a matching MCP tool per noun", () => {
|
|
26
|
+
expect(toolNames).toEqual([
|
|
27
|
+
"create_component",
|
|
28
|
+
"create_package",
|
|
29
|
+
"create_application",
|
|
30
|
+
"create_domain",
|
|
31
|
+
"create_route",
|
|
32
|
+
"create_wrapper",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("dispatches a multi-variant noun via a `variant` positional select", () => {
|
|
37
|
+
const component = commands.find(
|
|
38
|
+
(c) => c.path.join(" ") === "create component",
|
|
39
|
+
);
|
|
40
|
+
const variant = component?.parameters.find((p) => p.name === "variant");
|
|
41
|
+
expect(variant?.type).toBe("select");
|
|
42
|
+
expect(variant?.positional).toBe(true);
|
|
43
|
+
expect(variant?.choices?.map((ch) => ch.value)).toEqual([
|
|
44
|
+
"react",
|
|
45
|
+
"svelte",
|
|
46
|
+
"lit",
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("gives the multi-variant MCP tool a `variant` enum parameter", () => {
|
|
51
|
+
const component = specs.find((s) => s.name === "create_component");
|
|
52
|
+
expect(component?.params?.variant?.enum).toEqual([
|
|
53
|
+
"react",
|
|
54
|
+
"svelte",
|
|
55
|
+
"lit",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("collapses a single-variant noun to a plain `create <noun>` (no variant arg)", () => {
|
|
60
|
+
const application = commands.find(
|
|
61
|
+
(c) => c.path.join(" ") === "create application",
|
|
62
|
+
);
|
|
63
|
+
expect(application?.parameters.some((p) => p.name === "variant")).toBe(
|
|
64
|
+
false,
|
|
65
|
+
);
|
|
66
|
+
// Its prompts still project — appPath comes from the generator.
|
|
67
|
+
expect(application?.parameters.some((p) => p.name === "appPath")).toBe(
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("derives single-generator MCP params from the generator's prompts", () => {
|
|
73
|
+
const pkg = specs.find((s) => s.name === "create_package");
|
|
74
|
+
expect(pkg?.params).toBeDefined();
|
|
75
|
+
// The package generator prompts for a package name.
|
|
76
|
+
expect(Object.keys(pkg?.params ?? {}).length).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("never advertises params the runner pins (runInstall is forced off)", () => {
|
|
80
|
+
// The runner always executes a dry-run plan with runInstall: false, so
|
|
81
|
+
// no create tool may advertise the parameter it would ignore.
|
|
82
|
+
for (const spec of specs) {
|
|
83
|
+
expect(spec.params).not.toHaveProperty("runInstall");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Minimal synthetic generator for edge-case tests. */
|
|
89
|
+
function makeGenerator(name: string, generate?: () => never): AnyGenerator {
|
|
90
|
+
return {
|
|
91
|
+
meta: {
|
|
92
|
+
name,
|
|
93
|
+
displayName: name,
|
|
94
|
+
description: `Test generator ${name}`,
|
|
95
|
+
version: "0.0.0",
|
|
96
|
+
},
|
|
97
|
+
prompts: [],
|
|
98
|
+
generate:
|
|
99
|
+
generate ??
|
|
100
|
+
(() => {
|
|
101
|
+
throw new Error("not exercised");
|
|
102
|
+
}),
|
|
103
|
+
} as unknown as AnyGenerator;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("compileGeneratorPack — edge cases", () => {
|
|
107
|
+
let warnings: string[];
|
|
108
|
+
let warn: ReturnType<typeof vi.spyOn>;
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
warnings = [];
|
|
112
|
+
warn = vi
|
|
113
|
+
.spyOn(process.stderr, "write")
|
|
114
|
+
.mockImplementation((chunk: string | Uint8Array) => {
|
|
115
|
+
warnings.push(String(chunk));
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
warn.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("warns and exposes only the first generator of a mixed bare/variant noun", () => {
|
|
125
|
+
const { commands, specs } = compileGeneratorPack({
|
|
126
|
+
thing: makeGenerator("thing"),
|
|
127
|
+
"thing/extra": makeGenerator("thing/extra"),
|
|
128
|
+
});
|
|
129
|
+
expect(commands.map((c) => c.path.join(" "))).toEqual(["create thing"]);
|
|
130
|
+
expect(specs.map((s) => s.name)).toEqual(["create_thing"]);
|
|
131
|
+
expect(warnings.join("")).toContain("mixes bare and variant keys");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("maps a generator that throws to INVALID_INPUT with its message as recovery", async () => {
|
|
135
|
+
const throwing = makeGenerator("boom", () => {
|
|
136
|
+
throw new Error("boom requires --both flags");
|
|
137
|
+
});
|
|
138
|
+
const { specs } = compileGeneratorPack({ boom: throwing });
|
|
139
|
+
const spec = specs.at(0);
|
|
140
|
+
const rt = { cwd: "/tmp" } as PragmaRuntime;
|
|
141
|
+
await expect(spec?.execute(rt, {})).rejects.toMatchObject({
|
|
142
|
+
code: "INVALID_INPUT",
|
|
143
|
+
recovery: { message: "boom requires --both flags" },
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("rejects an unknown variant with INVALID_INPUT and the valid options", async () => {
|
|
148
|
+
const { specs } = compileGeneratorPack({
|
|
149
|
+
"widget/a": makeGenerator("widget/a"),
|
|
150
|
+
"widget/b": makeGenerator("widget/b"),
|
|
151
|
+
});
|
|
152
|
+
const spec = specs.at(0);
|
|
153
|
+
const rt = { cwd: "/tmp" } as PragmaRuntime;
|
|
154
|
+
await expect(spec?.execute(rt, { variant: "c" })).rejects.toMatchObject({
|
|
155
|
+
code: "INVALID_INPUT",
|
|
156
|
+
validOptions: ["a", "b"],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|