@intentius/chant 0.79.0 → 0.80.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/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +38 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +14 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +27 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +17 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +9 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +211 -0
- package/dist/components/promote.d.ts.map +1 -0
- package/dist/components/unbound-gate-approval.d.ts +24 -0
- package/dist/components/unbound-gate-approval.d.ts.map +1 -0
- package/dist/config.d.ts +82 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/convergence.d.ts +97 -0
- package/dist/discovery/convergence.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +41 -2
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +13 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +65 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +24 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +26 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +25 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
- package/dist/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/workspace/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +57 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +90 -12
- package/src/audit/terraform-state.test.ts +86 -1
- package/src/audit/terraform-state.ts +26 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-walk-warnings.test.ts +156 -0
- package/src/cli/commands/audit.ts +93 -19
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/doctor.ts +3 -0
- package/src/cli/commands/import-agents.ts +11 -4
- package/src/cli/commands/init.ts +57 -25
- package/src/cli/commands/lint.test.ts +58 -1
- package/src/cli/commands/lint.ts +48 -18
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +5 -1
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- package/src/cli/handlers/build.test.ts +11 -0
- package/src/cli/handlers/build.ts +1 -1
- package/src/cli/handlers/dev.ts +3 -0
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.ts +2 -1
- package/src/cli/handlers/promote.test.ts +261 -0
- package/src/cli/handlers/promote.ts +325 -0
- package/src/cli/handlers/run-generate.test.ts +160 -0
- package/src/cli/handlers/run-generate.ts +133 -0
- package/src/cli/handlers/run.ts +13 -1
- package/src/cli/main.test.ts +33 -1
- package/src/cli/main.ts +69 -16
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +44 -6
- package/src/cli/registry.ts +14 -1
- package/src/components/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +3 -2
- package/src/components/discover.ts +26 -2
- package/src/components/driver.test.ts +12 -11
- package/src/components/driver.ts +2 -0
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +371 -0
- package/src/components/promote.ts +537 -0
- package/src/components/unbound-gate-approval.test.ts +82 -0
- package/src/components/unbound-gate-approval.ts +51 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +128 -11
- package/src/discovery/convergence.test.ts +231 -0
- package/src/discovery/convergence.ts +399 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +97 -9
- package/src/discovery/fold-import.ts +83 -9
- package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
- package/src/graph-ir.ts +14 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +117 -0
- package/src/lexicon.ts +26 -0
- package/src/lifecycle/build-ledger-store.ts +11 -1
- package/src/lifecycle/legacy-digest.test.ts +114 -0
- package/src/lifecycle/legacy-digest.ts +62 -0
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- package/src/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/discover-convergence.test.ts +70 -0
- package/src/op/discover.ts +4 -0
- package/src/op/gate-approval.ts +3 -1
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/runtimes/local.ts +2 -1
- package/src/workspace/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +126 -0
- package/src/workspace/records-contract.test.ts +88 -0
- package/src/workspace/records.schema.json +120 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +383 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant run --generate <provider>` (#2533). Discovery is stubbed so the
|
|
3
|
+
* scheduled Ops are fixed; the github, gitlab and forgejo lexicon plugins are
|
|
4
|
+
* the real ones, so "identical to calling `generateOpsPipeline` directly" is
|
|
5
|
+
* checked against the real generators.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
9
|
+
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { ParsedArgs } from "../registry";
|
|
13
|
+
import type { OpConfig } from "../../op/types";
|
|
14
|
+
|
|
15
|
+
const discovered = new Map<string, { config: Partial<OpConfig>; filePath: string; exportName: string }>();
|
|
16
|
+
|
|
17
|
+
vi.mock("../../op/discover", () => ({
|
|
18
|
+
discoverOps: async () => ({ ops: discovered, errors: [] }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { runOp } = await import("./run");
|
|
22
|
+
const { generateOpsPipeline } = await import("../../op/generate-pipeline");
|
|
23
|
+
const { GENERATED_MARKER } = await import("../../discovery/files");
|
|
24
|
+
const { parseArgs } = await import("../main");
|
|
25
|
+
|
|
26
|
+
function ctx(argv: string[]) {
|
|
27
|
+
const args = parseArgs(argv) as ParsedArgs;
|
|
28
|
+
return { args, plugins: [], serializers: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let dir: string;
|
|
32
|
+
let errSpy: ReturnType<typeof vi.spyOn>;
|
|
33
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
dir = mkdtempSync(join(tmpdir(), "chant-run-generate-"));
|
|
37
|
+
discovered.clear();
|
|
38
|
+
discovered.set("nightly-audit", {
|
|
39
|
+
config: { name: "nightly-audit", phases: [], schedule: { cron: "0 6 * * *" } } as Partial<OpConfig>,
|
|
40
|
+
filePath: "nightly-audit.op.ts",
|
|
41
|
+
exportName: "default",
|
|
42
|
+
});
|
|
43
|
+
discovered.set("weekly-report", {
|
|
44
|
+
config: { name: "weekly-report", phases: [], schedule: { cron: "0 7 * * 1" } } as Partial<OpConfig>,
|
|
45
|
+
filePath: "weekly-report.op.ts",
|
|
46
|
+
exportName: "default",
|
|
47
|
+
});
|
|
48
|
+
discovered.set("deploy", {
|
|
49
|
+
config: { name: "deploy", phases: [] } as Partial<OpConfig>,
|
|
50
|
+
filePath: "deploy.op.ts",
|
|
51
|
+
exportName: "default",
|
|
52
|
+
});
|
|
53
|
+
errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
54
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
errSpy.mockRestore();
|
|
59
|
+
logSpy.mockRestore();
|
|
60
|
+
rmSync(dir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("chant run --generate <provider>", () => {
|
|
64
|
+
test.each(["github", "gitlab", "forgejo"])(
|
|
65
|
+
"%s: each file is the generator's own output under one marker line",
|
|
66
|
+
async (provider) => {
|
|
67
|
+
const out = join(dir, provider);
|
|
68
|
+
expect(await runOp(ctx(["run", "--generate", provider, "--output", out]))).toBe(0);
|
|
69
|
+
|
|
70
|
+
const direct = await generateOpsPipeline([{ name: "nightly-audit" }, { name: "weekly-report" }], provider);
|
|
71
|
+
expect(direct.success).toBe(true);
|
|
72
|
+
const header = `# ${GENERATED_MARKER}. Regenerate with: chant run --generate ${provider}\n`;
|
|
73
|
+
|
|
74
|
+
expect(readdirSync(out).sort()).toEqual(direct.files!.map((f) => f.name).sort());
|
|
75
|
+
for (const file of direct.files!) {
|
|
76
|
+
expect(readFileSync(join(out, file.name), "utf-8")).toBe(header + file.yaml);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
test("github and forgejo get one file per scheduled Op; an Op with no schedule is left out", async () => {
|
|
82
|
+
for (const provider of ["github", "forgejo"]) {
|
|
83
|
+
const out = join(dir, provider);
|
|
84
|
+
expect(await runOp(ctx(["run", "--generate", provider, "--output", out]))).toBe(0);
|
|
85
|
+
expect(readdirSync(out).sort()).toEqual(["nightly-audit.yml", "weekly-report.yml"]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("each github workflow is named after its Op (#2580); forgejo workflows carry no name", async () => {
|
|
90
|
+
for (const provider of ["github", "forgejo"]) {
|
|
91
|
+
expect(await runOp(ctx(["run", "--generate", provider, "--output", join(dir, provider)]))).toBe(0);
|
|
92
|
+
}
|
|
93
|
+
const header = `# ${GENERATED_MARKER}. Regenerate with: chant run --generate github\n`;
|
|
94
|
+
for (const op of ["nightly-audit", "weekly-report"]) {
|
|
95
|
+
const github = readFileSync(join(dir, "github", `${op}.yml`), "utf-8");
|
|
96
|
+
expect(github.startsWith(`${header}name: ${op}\n\non:\n`)).toBe(true);
|
|
97
|
+
const forgejo = readFileSync(join(dir, "forgejo", `${op}.yml`), "utf-8");
|
|
98
|
+
expect(forgejo).not.toMatch(/^name:/m);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("--spec passes its ops and options to the generator unchanged", async () => {
|
|
103
|
+
const specFile = join(dir, "ops.json");
|
|
104
|
+
const spec = {
|
|
105
|
+
ops: [
|
|
106
|
+
{ name: "deploy", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
|
|
107
|
+
{ name: "nightly-audit" },
|
|
108
|
+
],
|
|
109
|
+
options: { opsStage: "chant" },
|
|
110
|
+
};
|
|
111
|
+
writeFileSync(specFile, JSON.stringify(spec));
|
|
112
|
+
const out = join(dir, "gitlab");
|
|
113
|
+
expect(await runOp(ctx(["run", "--generate", "gitlab", "--spec", specFile, "--output", out]))).toBe(0);
|
|
114
|
+
|
|
115
|
+
const direct = await generateOpsPipeline(spec.ops as never, "gitlab", spec.options);
|
|
116
|
+
expect(direct.files!.map((f) => f.name)).toEqual(["chant.gitlab-ci.yml"]);
|
|
117
|
+
const written = readFileSync(join(out, "chant.gitlab-ci.yml"), "utf-8");
|
|
118
|
+
expect(written).toBe(
|
|
119
|
+
`# ${GENERATED_MARKER}. Regenerate with: chant run --generate gitlab --spec ${specFile}\n` + direct.files![0].yaml,
|
|
120
|
+
);
|
|
121
|
+
expect(written).toContain('$CI_PIPELINE_SOURCE == "merge_request_event"');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("--format json prints the files and jobs and writes nothing", async () => {
|
|
125
|
+
const out = join(dir, "none");
|
|
126
|
+
expect(await runOp(ctx(["run", "--generate", "github", "--output", out, "--format", "json"]))).toBe(0);
|
|
127
|
+
const printed = JSON.parse(String(logSpy.mock.calls[0][0]));
|
|
128
|
+
expect(printed.files.map((f: { path: string }) => f.path)).toEqual([
|
|
129
|
+
join(out, "nightly-audit.yml"),
|
|
130
|
+
join(out, "weekly-report.yml"),
|
|
131
|
+
]);
|
|
132
|
+
expect(printed.files[0].content.startsWith(`# ${GENERATED_MARKER}`)).toBe(true);
|
|
133
|
+
expect(printed.jobs).toHaveLength(2);
|
|
134
|
+
expect(() => readdirSync(out)).toThrow();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("with no scheduled Op and no --spec, it fails and says what to do", async () => {
|
|
138
|
+
discovered.delete("nightly-audit");
|
|
139
|
+
discovered.delete("weekly-report");
|
|
140
|
+
expect(await runOp(ctx(["run", "--generate", "github", "--output", dir]))).toBe(1);
|
|
141
|
+
expect(String(errSpy.mock.calls.flat().join("\n"))).toMatch(/No Op declares a schedule/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("a provider with no Op generator fails by name", async () => {
|
|
145
|
+
expect(await runOp(ctx(["run", "--generate", "aws", "--output", dir]))).toBe(1);
|
|
146
|
+
expect(String(errSpy.mock.calls.flat().join("\n"))).toMatch(/does not support Op generate mode/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a --spec entry without a name is refused", async () => {
|
|
150
|
+
const specFile = join(dir, "bad.json");
|
|
151
|
+
writeFileSync(specFile, JSON.stringify([{ trigger: { kind: "push" } }]));
|
|
152
|
+
expect(await runOp(ctx(["run", "--generate", "github", "--spec", specFile, "--output", dir]))).toBe(1);
|
|
153
|
+
expect(String(errSpy.mock.calls.flat().join("\n"))).toMatch(/entry 0 has no "name"/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("--components with --generate on run is refused", async () => {
|
|
157
|
+
expect(await runOp(ctx(["run", "--components", "--generate", "github"]))).toBe(1);
|
|
158
|
+
expect(String(errSpy.mock.calls.flat().join("\n"))).toMatch(/build --components --generate/);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant run --generate <provider>` (#2533): the CLI for `generateOpsPipeline`
|
|
3
|
+
* (../../op/generate-pipeline.ts), the Op counterpart to `chant build
|
|
4
|
+
* --components --generate <lexicon>`.
|
|
5
|
+
*
|
|
6
|
+
* The generator is a library function with no caller of its own. A pipeline
|
|
7
|
+
* committed to a repository needs a named command that regenerates it, so a
|
|
8
|
+
* drift check (#2524 D14) can rerun that command and compare. This handler is
|
|
9
|
+
* that command and adds nothing to the generator's output beyond one header
|
|
10
|
+
* line: every file it writes is the generator's own `yaml`, byte for byte,
|
|
11
|
+
* under a first line carrying {@link GENERATED_MARKER} and the command that
|
|
12
|
+
* produced it.
|
|
13
|
+
*
|
|
14
|
+
* Where the specs come from:
|
|
15
|
+
*
|
|
16
|
+
* - by default, every discovered Op that declares its own `schedule`, as a
|
|
17
|
+
* bare `{ name }` spec. `generateOpsPipeline` fills the cron in from the Op
|
|
18
|
+
* (`withOpSchedules`), so the result is exactly what a direct call with
|
|
19
|
+
* those names returns;
|
|
20
|
+
* - with `--spec <file>`, a JSON file holding either a `ScheduledOpSpec[]`
|
|
21
|
+
* or `{ ops, options }`, for Ops triggered by a pull request or a push, a
|
|
22
|
+
* finding mode, setup steps, or generator options. The file is passed to
|
|
23
|
+
* the generator unchanged.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join, resolve } from "node:path";
|
|
28
|
+
import { discoverOps } from "../../op/discover";
|
|
29
|
+
import { generateOpsPipeline } from "../../op/generate-pipeline";
|
|
30
|
+
import { GENERATED_MARKER } from "../../discovery/files";
|
|
31
|
+
import type { ComponentPipelineOptions, ScheduledOpSpec } from "../../lexicon";
|
|
32
|
+
import { formatBold, formatError, formatSuccess } from "../format";
|
|
33
|
+
import type { CommandContext } from "../registry";
|
|
34
|
+
|
|
35
|
+
/** Where each forge reads its pipeline files from, relative to the repository root. `--output` overrides it. */
|
|
36
|
+
export const DEFAULT_OP_PIPELINE_DIRS: Record<string, string> = {
|
|
37
|
+
github: ".github/workflows",
|
|
38
|
+
forgejo: ".forgejo/workflows",
|
|
39
|
+
gitlab: ".",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The first line of every file this command writes. */
|
|
43
|
+
export function opPipelineHeader(provider: string, specFile?: string): string {
|
|
44
|
+
const command = `chant run --generate ${provider}${specFile ? ` --spec ${specFile}` : ""}`;
|
|
45
|
+
return `# ${GENERATED_MARKER}. Regenerate with: ${command}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SpecFile {
|
|
49
|
+
ops: ScheduledOpSpec[];
|
|
50
|
+
options?: ComponentPipelineOptions;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Read `--spec`: a `ScheduledOpSpec[]`, or `{ ops, options }`. Throws with the file named on anything else. */
|
|
54
|
+
export function readOpSpecFile(path: string): SpecFile {
|
|
55
|
+
let parsed: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new Error(`--spec ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
const shape = "an array of Op specs, or { \"ops\": [...], \"options\": {...} }";
|
|
62
|
+
const ops = Array.isArray(parsed) ? parsed : (parsed as { ops?: unknown } | null)?.ops;
|
|
63
|
+
if (!Array.isArray(ops)) throw new Error(`--spec ${path} must hold ${shape}`);
|
|
64
|
+
for (const [i, spec] of ops.entries()) {
|
|
65
|
+
if (typeof (spec as { name?: unknown })?.name !== "string") {
|
|
66
|
+
throw new Error(`--spec ${path}: entry ${i} has no "name". Each entry names the Op it generates a pipeline for.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const options = Array.isArray(parsed) ? undefined : (parsed as { options?: ComponentPipelineOptions }).options;
|
|
70
|
+
return { ops: ops as ScheduledOpSpec[], ...(options ? { options } : {}) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runOpGenerate(ctx: CommandContext): Promise<number> {
|
|
74
|
+
const { args } = ctx;
|
|
75
|
+
const provider = args.generate as string;
|
|
76
|
+
const specPath = args.opsSpec;
|
|
77
|
+
|
|
78
|
+
let ops: ScheduledOpSpec[];
|
|
79
|
+
let options: ComponentPipelineOptions | undefined;
|
|
80
|
+
if (specPath) {
|
|
81
|
+
try {
|
|
82
|
+
({ ops, options } = readOpSpecFile(resolve(specPath)));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const discovered = await discoverOps();
|
|
89
|
+
if (discovered.errors.length > 0) {
|
|
90
|
+
for (const e of discovered.errors) console.error(formatError({ message: e }));
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
ops = [...discovered.ops.entries()]
|
|
94
|
+
.filter(([, op]) => op.config.schedule)
|
|
95
|
+
.map(([name]) => name)
|
|
96
|
+
.sort()
|
|
97
|
+
.map((name) => ({ name }));
|
|
98
|
+
if (ops.length === 0) {
|
|
99
|
+
console.error(formatError({
|
|
100
|
+
message: "No Op declares a schedule, so there is nothing to generate",
|
|
101
|
+
hint: "Give an Op a `schedule`, or pass --spec <file.json> naming the Ops and their triggers.",
|
|
102
|
+
}));
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await generateOpsPipeline(ops, provider, options);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
console.error(formatError({ message: result.error ?? "Failed to generate Op pipelines" }));
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const header = opPipelineHeader(provider, specPath);
|
|
114
|
+
const outDir = args.output ?? DEFAULT_OP_PIPELINE_DIRS[provider] ?? ".";
|
|
115
|
+
const files = (result.files ?? []).map((f) => ({
|
|
116
|
+
name: f.name,
|
|
117
|
+
path: join(outDir, f.name),
|
|
118
|
+
content: header + f.yaml,
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
if (args.format === "json") {
|
|
122
|
+
console.log(JSON.stringify({ files, jobs: result.jobs ?? [] }, null, 2));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const target = resolve(file.path);
|
|
128
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
129
|
+
writeFileSync(target, file.content);
|
|
130
|
+
console.error(formatSuccess(`wrote ${formatBold(file.path)}`));
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lexiconNames } from "../../lexicon-module";
|
|
1
2
|
import { resolve, dirname } from "node:path";
|
|
2
3
|
import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
4
|
import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
|
|
@@ -20,6 +21,7 @@ import { ndjsonProgressSink } from "../../components/run-progress";
|
|
|
20
21
|
import { maybeRecordAutoRelease } from "../../components/auto-release";
|
|
21
22
|
import { maybePersistBuildManifest } from "../../components/manifest-persistence";
|
|
22
23
|
import type { DriverComponentResult } from "../../components/driver";
|
|
24
|
+
import { runOpGenerate } from "./run-generate";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The exit code a run that stopped at an unapproved gate uses (#2119).
|
|
@@ -124,7 +126,7 @@ async function resolveOpRuntime(ctx: CommandContext): Promise<OpRuntimeProvider
|
|
|
124
126
|
// long wait to be told a name is wrong.
|
|
125
127
|
let configured: string[] = [];
|
|
126
128
|
try {
|
|
127
|
-
configured = (await loadChantConfig(resolve("."))).config.lexicons ?? [];
|
|
129
|
+
configured = lexiconNames((await loadChantConfig(resolve("."))).config.lexicons ?? []);
|
|
128
130
|
} catch {
|
|
129
131
|
// No/unreadable chant.config.ts — the error below says so by listing nothing.
|
|
130
132
|
}
|
|
@@ -524,6 +526,16 @@ function refusesPolicyGateUnderSandbox(ctx: CommandContext, config: OpConfig, op
|
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
529
|
+
if (ctx.args.generate) {
|
|
530
|
+
if (ctx.args.components) {
|
|
531
|
+
console.error(formatError({
|
|
532
|
+
message: "`chant run --components --generate` is not a command",
|
|
533
|
+
hint: "Generate a component pipeline with `chant build --components --generate <lexicon>`, or Op pipelines with `chant run --generate <provider>`.",
|
|
534
|
+
}));
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
return runOpGenerate(ctx);
|
|
538
|
+
}
|
|
527
539
|
if (ctx.args.components && ctx.args.report) {
|
|
528
540
|
console.error(formatError({
|
|
529
541
|
message: "--report is not supported with --components",
|
package/src/cli/main.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { commandRegistry, parseArgs, waitForStreamDrain } from "./main";
|
|
4
6
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
5
7
|
|
|
6
8
|
describe("parseArgs", () => {
|
|
@@ -298,6 +300,12 @@ describe("parseArgs", () => {
|
|
|
298
300
|
|
|
299
301
|
// ── --no-release-record (#597) ───────────────────────────────────────────
|
|
300
302
|
|
|
303
|
+
test("--promote-to <env> sets promoteTo, and a missing value is refused (#2575)", () => {
|
|
304
|
+
expect(parseArgs(["build", "--components", "--generate", "github", "--promote-to", "prod"]).promoteTo).toBe("prod");
|
|
305
|
+
expect(() => parseArgs(["build", "--components", "--generate", "github", "--promote-to"])).toThrow(/--promote-to needs an environment/);
|
|
306
|
+
expect(() => parseArgs(["build", "--promote-to", "--env", "x"])).toThrow(/--promote-to needs an environment/);
|
|
307
|
+
});
|
|
308
|
+
|
|
301
309
|
test("parses --no-release-record for run --components", () => {
|
|
302
310
|
const result = parseArgs(["run", "--components", "search-service", "--env", "staging", "--no-release-record"]);
|
|
303
311
|
expect(result.components).toBe(true);
|
|
@@ -633,6 +641,30 @@ describe("parseArgs — run flags", () => {
|
|
|
633
641
|
});
|
|
634
642
|
});
|
|
635
643
|
|
|
644
|
+
describe("workspace records (#2546)", () => {
|
|
645
|
+
test("parses --kind, --current, --at and --json", () => {
|
|
646
|
+
const args = parseArgs(["workspace", "records", "--kind", "docs/k.kind.mjs", "--current", "--at", "HEAD~1", "--json"]);
|
|
647
|
+
expect(args).toMatchObject({ command: "workspace", path: "records", kind: "docs/k.kind.mjs", current: true, at: "HEAD~1", json: true });
|
|
648
|
+
expect(parseArgs(["workspace", "records", "--kind=docs/k.kind.mjs"]).kind).toBe("docs/k.kind.mjs");
|
|
649
|
+
expect(parseArgs(["workspace", "records"]).current).toBeUndefined();
|
|
650
|
+
expect(() => parseArgs(["workspace", "records", "--kind"])).toThrow(/--kind needs a kind file/);
|
|
651
|
+
expect(() => parseArgs(["workspace", "records", "--kind", "--json"])).toThrow(/--kind needs a kind file/);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("resolves to the records command, and anything else under workspace to its fallback", () => {
|
|
655
|
+
expect(resolveCommand(parseArgs(["workspace", "records", "--kind", "k.mjs"]), commandRegistry)?.def.name).toBe("workspace records");
|
|
656
|
+
expect(resolveCommand(parseArgs(["workspace", "ls"]), commandRegistry)?.def.name).toBe("workspace");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// #2525 rule 5: the workspace code loads only when a workspace command runs.
|
|
660
|
+
// The level-0 goldens catch it at run time; this catches it in review.
|
|
661
|
+
test("main.ts never imports workspace code statically", () => {
|
|
662
|
+
const source = readFileSync(join(import.meta.dirname, "main.ts"), "utf-8");
|
|
663
|
+
expect(source).not.toMatch(/^import[^;]*from\s+["'][^"']*workspace/m);
|
|
664
|
+
expect(source).toMatch(/await import\("\.\.\/workspace\/records-cli"\)/);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
636
668
|
describe("waitForStreamDrain", () => {
|
|
637
669
|
// Minimal writable stub — just the surface waitForStreamDrain reads.
|
|
638
670
|
function fakeStream(len: number): NodeJS.WriteStream & { writableLength: number } {
|
package/src/cli/main.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { runCarveStatus } from "./handlers/carve-status";
|
|
|
27
27
|
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleTeardown, runLifecycleWhoami, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
28
28
|
import { runComponentsStatus, runComponentsReleaseRecord, runComponentsExport, runComponentsUnknown } from "./handlers/components";
|
|
29
29
|
import { runComponentsFanOut } from "./handlers/fan-out";
|
|
30
|
+
import { runComponentsPromote, runComponentsRollback } from "./handlers/promote";
|
|
30
31
|
import { runScenarioCheck, runScenarioUnknown } from "./handlers/scenario";
|
|
31
32
|
import { runGraph } from "./handlers/graph";
|
|
32
33
|
import { runExplain } from "./handlers/explain";
|
|
@@ -93,6 +94,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
93
94
|
"--fail-on-drift",
|
|
94
95
|
"--durable-requests",
|
|
95
96
|
"--skip-mcp",
|
|
97
|
+
"--current",
|
|
96
98
|
]);
|
|
97
99
|
|
|
98
100
|
/**
|
|
@@ -205,8 +207,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
205
207
|
if (v !== "source" && v !== "live") throw new Error(`--overlay-anchor must be 'source' or 'live', got '${v}'`);
|
|
206
208
|
result.overlayAnchor = v;
|
|
207
209
|
} else if (arg === "--from") {
|
|
208
|
-
// Shared by `migrate --from <lexicon
|
|
209
|
-
//
|
|
210
|
+
// Shared by `migrate --from <lexicon>`, `import --from <env>` and
|
|
211
|
+
// `components promote --from <env>`; the commands never run together,
|
|
212
|
+
// so one field carries all three.
|
|
210
213
|
result.migrateFrom = args[++i];
|
|
211
214
|
} else if (arg === "--kustomize") {
|
|
212
215
|
// `chant import --kustomize <dir>` (#1548): render the overlay, import
|
|
@@ -265,6 +268,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
265
268
|
result.src = args[++i];
|
|
266
269
|
} else if (arg === "--env") {
|
|
267
270
|
result.env = args[++i];
|
|
271
|
+
} else if (arg === "--promote-to") {
|
|
272
|
+
result.promoteTo = args[++i];
|
|
273
|
+
if (!result.promoteTo || result.promoteTo.startsWith("-")) throw new Error("--promote-to needs an environment: --promote-to <env>");
|
|
268
274
|
} else if (arg === "--tier") {
|
|
269
275
|
result.tier = args[++i];
|
|
270
276
|
} else if (arg === "--agents") {
|
|
@@ -277,6 +283,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
277
283
|
result.runtime = args[++i];
|
|
278
284
|
} else if (arg === "--fail-on") {
|
|
279
285
|
result.failOn = args[++i];
|
|
286
|
+
} else if (arg === "--max-files") {
|
|
287
|
+
// Validated by the audit handler, like `--limit` below.
|
|
288
|
+
result.maxFiles = Number(args[++i]);
|
|
280
289
|
} else if (arg === "--theme") {
|
|
281
290
|
result.theme = args[++i];
|
|
282
291
|
} else if (arg === "--stacks") {
|
|
@@ -285,6 +294,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
285
294
|
result.components = true;
|
|
286
295
|
} else if (arg === "--generate") {
|
|
287
296
|
result.generate = args[++i];
|
|
297
|
+
} else if (arg === "--spec") {
|
|
298
|
+
result.opsSpec = args[++i];
|
|
288
299
|
} else if (arg === "--dump-outputs") {
|
|
289
300
|
result.dumpOutputs = args[++i];
|
|
290
301
|
} else if (arg === "--seed-outputs") {
|
|
@@ -342,6 +353,12 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
342
353
|
result.deep = true;
|
|
343
354
|
} else if (arg === "--at") {
|
|
344
355
|
result.at = args[++i];
|
|
356
|
+
} else if (arg === "--kind") {
|
|
357
|
+
// `chant workspace records --kind <kind file>` (#2546)
|
|
358
|
+
result.kind = args[++i];
|
|
359
|
+
if (!result.kind || result.kind.startsWith("-")) throw new Error("--kind needs a kind file: --kind <path>");
|
|
360
|
+
} else if (arg === "--current") {
|
|
361
|
+
result.current = true;
|
|
345
362
|
} else if (arg === "--ambient") {
|
|
346
363
|
result.ambient = true;
|
|
347
364
|
} else if (arg === "--check-live") {
|
|
@@ -491,10 +508,11 @@ Commands:
|
|
|
491
508
|
init Initialize a new chant project
|
|
492
509
|
init lexicon <name> Scaffold a new lexicon plugin project
|
|
493
510
|
build Build infrastructure from specification files
|
|
494
|
-
(--components --generate gitlab:
|
|
495
|
-
|
|
496
|
-
discovered component's
|
|
497
|
-
instead of a normal
|
|
511
|
+
(--components --generate github|gitlab|forgejo:
|
|
512
|
+
generate mode (#563), which synthesizes a thin CI
|
|
513
|
+
pipeline that triggers each discovered component's
|
|
514
|
+
own deploy in wave order, instead of a normal
|
|
515
|
+
lexicon build)
|
|
498
516
|
lint Check specifications for issues
|
|
499
517
|
list List discovered entities
|
|
500
518
|
describe Show the effective config for one component
|
|
@@ -509,7 +527,8 @@ Commands:
|
|
|
509
527
|
audit [path|url] Audit a repo's CI YAML for security issues
|
|
510
528
|
(--format stylish|json|sarif|markdown|html, -o <file>,
|
|
511
529
|
--tier merge-worthy|all, --fail-on merge-worthy|warning|error|none,
|
|
512
|
-
--template <file> / --theme <file> for the html report
|
|
530
|
+
--template <file> / --theme <file> for the html report,
|
|
531
|
+
--max-files <n> to walk more than 1000 files)
|
|
513
532
|
--agents audits this machine's agent configuration —
|
|
514
533
|
instruction files, MCP servers, skills, plugins,
|
|
515
534
|
permissions — instead of a repository
|
|
@@ -556,6 +575,11 @@ Ops:
|
|
|
556
575
|
run approve <op> <gate> Record a gate's resolution and wake the runtime
|
|
557
576
|
run cancel <name> Cancel the active run (requires --force)
|
|
558
577
|
run log <name> Show run history for an Op
|
|
578
|
+
run --generate <provider> Write one CI pipeline file per scheduled Op for
|
|
579
|
+
github, gitlab or forgejo (#2533), from every Op that
|
|
580
|
+
declares a schedule or from --spec <file.json>.
|
|
581
|
+
Files go to the forge's own directory unless
|
|
582
|
+
--output <dir>; --format json prints them instead
|
|
559
583
|
run --components <name|all> Run discovered Component(s) through the interpret
|
|
560
584
|
driver on the local executor (--env <env>; #585).
|
|
561
585
|
On success, auto-emits a release-ledger record per
|
|
@@ -615,6 +639,13 @@ Ops:
|
|
|
615
639
|
predicts the account. The level reaches the engine
|
|
616
640
|
verbatim; without the flag nothing is asked (#2377)
|
|
617
641
|
|
|
642
|
+
Workspace (first-test slice, #2546):
|
|
643
|
+
workspace records --kind <kind file> [--current] [--at <rev>] [--json]
|
|
644
|
+
Read the records a record kind locates, validated
|
|
645
|
+
against its schema, with reason codes for invalid
|
|
646
|
+
ones. --current leaves out superseded records; --at
|
|
647
|
+
reads a commit's git objects. Needs no workspace file
|
|
648
|
+
|
|
618
649
|
Lifecycle (alias: lc):
|
|
619
650
|
lifecycle snapshot <env> Query API, save metadata to orphan branch
|
|
620
651
|
--deep: also record each resource's property tree,
|
|
@@ -670,6 +701,15 @@ Component release ledger + status:
|
|
|
670
701
|
[--digest <manifestDigest>] -o <dir> [--json]);
|
|
671
702
|
copies every image/template/asset/sbom entry
|
|
672
703
|
byte-for-byte plus a self-describing manifest.json
|
|
704
|
+
components promote Deploy the digests one environment's release
|
|
705
|
+
ledger records to another, without a build
|
|
706
|
+
(--from <env> --to <env> [--component <name>
|
|
707
|
+
[--digest <sha256:...>]]; --dry-run prints the
|
|
708
|
+
plan; the target's gates still apply)
|
|
709
|
+
components rollback <env> Redeploy an earlier release of an environment
|
|
710
|
+
from its recorded digest (--component <name>
|
|
711
|
+
[--digest <sha256:...>]; defaults to the release
|
|
712
|
+
before the current one; --dry-run prints the plan)
|
|
673
713
|
|
|
674
714
|
Lexicon development:
|
|
675
715
|
dev generate Generate lexicon artifacts (+ validate + coverage)
|
|
@@ -744,9 +784,14 @@ Options:
|
|
|
744
784
|
--use-composites Rewrite to composite calls when patterns match (migrate)
|
|
745
785
|
--components Target discovered Component declarations instead of
|
|
746
786
|
lexicon resources (list, describe, graph, build, run)
|
|
747
|
-
--generate <lexicon> Generate mode
|
|
748
|
-
|
|
749
|
-
|
|
787
|
+
--generate <lexicon> Generate mode: synthesize CI YAML for <lexicon> (github,
|
|
788
|
+
gitlab or forgejo) instead of running. build
|
|
789
|
+
--components: one pipeline for the components; run:
|
|
790
|
+
one pipeline file per scheduled Op
|
|
791
|
+
--spec <file> (run --generate) JSON Op specs: an array, or
|
|
792
|
+
{ ops, options }
|
|
793
|
+
--promote-to <env> (build --components --generate) Add a job that
|
|
794
|
+
promotes the deployed releases to <env>
|
|
750
795
|
--no-release-record Skip auto-emitting a release-ledger record after a
|
|
751
796
|
successful \`run --components\` deploy (default: on;
|
|
752
797
|
also settable via chant.config.ts's
|
|
@@ -778,18 +823,18 @@ Options:
|
|
|
778
823
|
project source; network egress is NOT blocked (see
|
|
779
824
|
docs). Default: off (also settable via
|
|
780
825
|
chant.config.ts's build.sandbox: true; #1045)
|
|
781
|
-
--param <name=value> (build, graph, run --components, components fan-out
|
|
782
|
-
Bind a
|
|
783
|
-
build-time parameter (chant.config.ts's buildParams)
|
|
826
|
+
--param <name=value> (build, graph, run --components, components fan-out,
|
|
827
|
+
components promote, components rollback) Bind a
|
|
828
|
+
declared build-time parameter (chant.config.ts's buildParams)
|
|
784
829
|
to a value, for source to read as params.<name>
|
|
785
830
|
(#1064) instead of process.env — repeatable.
|
|
786
831
|
Distinct from the AWS lexicon's deploy-time
|
|
787
832
|
Parameter(): this resolves before synthesis, so it
|
|
788
833
|
can change which resources are produced at all.
|
|
789
834
|
Highest precedence.
|
|
790
|
-
--params-file <path> (build, graph, run --components, components fan-out
|
|
791
|
-
JSON file
|
|
792
|
-
{ "name": value } build-time parameter values
|
|
835
|
+
--params-file <path> (build, graph, run --components, components fan-out,
|
|
836
|
+
components promote, components rollback) JSON file
|
|
837
|
+
of { "name": value } build-time parameter values
|
|
793
838
|
(#1064). Second precedence, after --param.
|
|
794
839
|
|
|
795
840
|
Examples:
|
|
@@ -800,6 +845,7 @@ Examples:
|
|
|
800
845
|
chant build ./infra/ --fold
|
|
801
846
|
chant build ./infra/ --components --generate gitlab
|
|
802
847
|
chant build ./infra/ --components --generate gitlab --output .gitlab-ci.yml
|
|
848
|
+
chant run --generate github
|
|
803
849
|
chant run --components search-service --env staging
|
|
804
850
|
chant run --components all --env production
|
|
805
851
|
chant components export prod --component search-service -o ./dist/search-service
|
|
@@ -994,6 +1040,10 @@ export const commandRegistry: CommandDef[] = [
|
|
|
994
1040
|
{ name: "graph", handler: runGraph },
|
|
995
1041
|
{ name: "vendor", handler: runVendor },
|
|
996
1042
|
|
|
1043
|
+
// Workspace reads (#2524). Imported on first use, so a level-0 command never
|
|
1044
|
+
// loads anything under workspace/ (#2525 rule 5, pinned by #2526's goldens).
|
|
1045
|
+
{ name: "workspace records", handler: async (ctx) => (await import("../workspace/records-cli")).runWorkspaceRecords(ctx) },
|
|
1046
|
+
|
|
997
1047
|
// State subcommands
|
|
998
1048
|
{ name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
|
|
999
1049
|
{ name: "lifecycle show", handler: runLifecycleShow },
|
|
@@ -1015,6 +1065,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1015
1065
|
{ name: "components status", requiresPlugins: true, handler: runComponentsStatus },
|
|
1016
1066
|
{ name: "components release", handler: runComponentsReleaseRecord },
|
|
1017
1067
|
{ name: "components export", handler: runComponentsExport },
|
|
1068
|
+
{ name: "components promote", handler: runComponentsPromote },
|
|
1069
|
+
{ name: "components rollback", handler: runComponentsRollback },
|
|
1018
1070
|
|
|
1019
1071
|
// Local emulators of configured lexicons (#920). Compound so the action word
|
|
1020
1072
|
// lands in args.path (not consumed as a project dir) and projectPath is forced ".".
|
|
@@ -1034,6 +1086,7 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1034
1086
|
{ name: "dev", handler: runDevUnknown },
|
|
1035
1087
|
{ name: "serve", handler: runServeUnknown },
|
|
1036
1088
|
{ name: "components", handler: runComponentsUnknown },
|
|
1089
|
+
{ name: "workspace", handler: async (ctx) => (await import("../workspace/records-cli")).runWorkspaceUnknown(ctx) },
|
|
1037
1090
|
];
|
|
1038
1091
|
|
|
1039
1092
|
/**
|
package/src/cli/mcp/op-tools.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* run.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { lexiconNames } from "../../lexicon-module";
|
|
17
18
|
import { resolve } from "node:path";
|
|
18
19
|
import { discoverOps } from "../../op/discover";
|
|
19
20
|
import { createLocalOpRuntime } from "../../op/runtimes/local";
|
|
@@ -39,7 +40,7 @@ async function runtimeFor(name: unknown): Promise<OpRuntimeProvider> {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const configured = await loadChantConfig(resolve("."))
|
|
42
|
-
.then(({ config }) => config.lexicons ?? [])
|
|
43
|
+
.then(({ config }) => lexiconNames(config.lexicons ?? []))
|
|
43
44
|
.catch(() => [] as string[]);
|
|
44
45
|
if (!configured.includes(wanted)) {
|
|
45
46
|
throw new Error(
|