@intentius/chant 0.58.0 → 0.60.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 +17 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +15 -4
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/build-params.d.ts +2 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +13 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +0 -16
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +19 -4
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +2 -2
- package/dist/components/verbs/run-agent.d.ts +1 -7
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/lexicon.d.ts +213 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +9 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +4 -4
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +134 -2
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +7 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/reconcile-op.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +55 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- package/dist/op/index.d.ts +6 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts +63 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/types.d.ts +18 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/params.d.ts +1 -1
- package/dist/project-root.d.ts +2 -2
- package/dist/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/core.ts +25 -3
- package/src/audit/discover.ts +122 -14
- package/src/build-params.ts +2 -2
- package/src/cli/commands/audit.test.ts +5 -3
- package/src/cli/commands/audit.ts +5 -1
- package/src/cli/commands/build.test.ts +39 -1
- package/src/cli/commands/build.ts +22 -8
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- package/src/cli/commands/lint.test.ts +297 -1
- package/src/cli/commands/lint.ts +119 -11
- package/src/cli/handlers/graph.test.ts +4 -4
- package/src/cli/handlers/graph.ts +11 -11
- package/src/cli/handlers/lint.test.ts +107 -0
- package/src/cli/handlers/lint.ts +30 -0
- package/src/cli/handlers/operator.ts +81 -0
- package/src/cli/handlers/run.test.ts +132 -3
- package/src/cli/handlers/run.ts +87 -3
- package/src/cli/main.test.ts +9 -11
- package/src/cli/main.ts +5 -27
- package/src/cli/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +14 -0
- package/src/components/README.md +2 -2
- package/src/components/SPRAWL-VALIDATION.md +5 -5
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- package/src/components/cli-support.test.ts +18 -7
- package/src/components/cli-support.ts +8 -3
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +17 -4
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +25 -5
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver.test.ts +20 -1
- package/src/components/driver.ts +12 -5
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +2 -2
- package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
- package/src/components/verbs/run-agent.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +1 -7
- package/src/config.ts +4 -4
- package/src/detectLexicon.ts +18 -1
- package/src/discovery/fold-import.test.ts +2 -2
- package/src/discovery/fold-import.ts +3 -3
- package/src/fold/foldable-helpers.ts +1 -1
- package/src/graph-ops.test.ts +1 -1
- package/src/lexicon.ts +221 -3
- package/src/lifecycle/gate-ledger.ts +12 -1
- package/src/lifecycle/observe.test.ts +2 -2
- package/src/lifecycle/observe.ts +8 -8
- package/src/lifecycle/release-ledger.test.ts +2 -2
- package/src/lint/pipeline-change-gate.test.ts +2 -2
- package/src/lint/rules/comp/comp.test.ts +26 -0
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +4 -2
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +1 -1
- package/src/op/activities/index.ts +9 -2
- package/src/op/activities/reconcile.test.ts +320 -2
- package/src/op/activities/reconcile.ts +423 -2
- package/src/op/builders.ts +3 -3
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +14 -4
- package/src/op/composites/composites.test.ts +17 -4
- package/src/op/composites/reconcile-op.ts +7 -4
- package/src/op/effect-step.test.ts +3 -3
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.test.ts +62 -0
- package/src/op/gate-summary.ts +96 -0
- package/src/op/index.ts +9 -2
- package/src/op/local-executor.test.ts +16 -1
- package/src/op/local-executor.ts +4 -3
- package/src/op/op-ir.test.ts +12 -1
- package/src/op/op-ir.ts +5 -3
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op.test.ts +2 -2
- package/src/op/operator.test.ts +368 -0
- package/src/op/operator.ts +141 -5
- package/src/op/runtimes/local.test.ts +1 -1
- package/src/op/types.ts +23 -3
- package/src/params.ts +1 -1
- package/src/project-root.ts +2 -2
- package/src/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant #2251 — `chant lint` resolves this invocation's declared build-time
|
|
3
|
+
* parameters before it lints.
|
|
4
|
+
*
|
|
5
|
+
* The gap this covers: `runLint` built its `LintOptions` without ever
|
|
6
|
+
* touching `chant.config.ts`'s `buildParams`, so the OPS* checks imported
|
|
7
|
+
* every `*.op.ts` file with `params` (`@intentius/chant/params`) still empty.
|
|
8
|
+
* An Op taking a step argument from `params.<name>` therefore read
|
|
9
|
+
* `undefined` and OPS012 reported the activity contract violated on source
|
|
10
|
+
* that builds and runs — reproducible on any project with an Op that reads a
|
|
11
|
+
* build parameter, `--param` and the declared `env` mapping alike (the
|
|
12
|
+
* resolution never ran at all, so no input could reach it).
|
|
13
|
+
*
|
|
14
|
+
* Mocks `lintCommand` and `loadChantConfigUpward` and drives the exported
|
|
15
|
+
* `runLint` dispatcher, the same shape ./build.test.ts uses for the matching
|
|
16
|
+
* #1108 gap in generate mode.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
19
|
+
import type { ParsedArgs } from "../registry";
|
|
20
|
+
|
|
21
|
+
const lintCommandMock = vi.fn();
|
|
22
|
+
const loadChantConfigUpwardMock = vi.fn();
|
|
23
|
+
|
|
24
|
+
vi.mock("../commands/lint", async () => {
|
|
25
|
+
const actual = await vi.importActual<typeof import("../commands/lint")>("../commands/lint");
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
lintCommand: (...args: unknown[]) => lintCommandMock(...args),
|
|
29
|
+
printLintResult: () => {},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
vi.mock("../../config", async () => {
|
|
33
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
34
|
+
return { ...actual, loadChantConfigUpward: (...args: unknown[]) => loadChantConfigUpwardMock(...args) };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { runLint } = await import("./lint");
|
|
38
|
+
|
|
39
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
40
|
+
return {
|
|
41
|
+
command: "lint",
|
|
42
|
+
path: ".",
|
|
43
|
+
format: "",
|
|
44
|
+
fix: false,
|
|
45
|
+
watch: false,
|
|
46
|
+
verbose: false,
|
|
47
|
+
help: false,
|
|
48
|
+
live: false,
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("runLint build-time parameters (#2251)", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
lintCommandMock.mockReset().mockResolvedValue({ success: true, errorCount: 0, warningCount: 0, diagnostics: [] });
|
|
56
|
+
loadChantConfigUpwardMock.mockReset().mockResolvedValue({ config: {} });
|
|
57
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a project declaring no buildParams lints with an empty provenance array", async () => {
|
|
65
|
+
const exit = await runLint({ args: makeArgs(), plugins: [], serializers: [] });
|
|
66
|
+
|
|
67
|
+
expect(exit).toBe(0);
|
|
68
|
+
expect(lintCommandMock.mock.calls[0][0].buildParams).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("declared buildParams resolve from their defaults and reach lintCommand", async () => {
|
|
72
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
73
|
+
config: { buildParams: { env: { type: "string", default: "local" } } },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const exit = await runLint({ args: makeArgs(), plugins: [], serializers: [] });
|
|
77
|
+
|
|
78
|
+
expect(exit).toBe(0);
|
|
79
|
+
expect(lintCommandMock.mock.calls[0][0].buildParams).toEqual([
|
|
80
|
+
{ name: "env", value: "local", source: "default" },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("--param overrides the declared default, the same precedence chant build applies", async () => {
|
|
85
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
86
|
+
config: { buildParams: { env: { type: "string", default: "local" } } },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const exit = await runLint({ args: makeArgs({ param: ["env=pr-42"] }), plugins: [], serializers: [] });
|
|
90
|
+
|
|
91
|
+
expect(exit).toBe(0);
|
|
92
|
+
expect(lintCommandMock.mock.calls[0][0].buildParams).toEqual([
|
|
93
|
+
{ name: "env", value: "pr-42", source: "cli" },
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("an unresolvable parameter exits non-zero without linting", async () => {
|
|
98
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
99
|
+
config: { buildParams: { env: { type: "string", required: true } } },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const exit = await runLint({ args: makeArgs(), plugins: [], serializers: [] });
|
|
103
|
+
|
|
104
|
+
expect(exit).toBe(1);
|
|
105
|
+
expect(lintCommandMock).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/cli/handlers/lint.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { lintCommand, lintCommandWatch, printLintResult } from "../commands/lint";
|
|
2
3
|
import { formatError, formatInfo } from "../format";
|
|
3
4
|
import type { CommandContext } from "../registry";
|
|
5
|
+
import { commandBuildParams } from "../build-params-cli";
|
|
6
|
+
import { loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* chant #2251 — `chant lint` resolves this invocation's declared build-time
|
|
10
|
+
* parameters (`chant.config.ts`'s `buildParams`) before it lints, the same
|
|
11
|
+
* way `chant build` and the lifecycle family do (`commandBuildParams`,
|
|
12
|
+
* ../build-params-cli.ts).
|
|
13
|
+
*
|
|
14
|
+
* The OPS* checks import every `*.op.ts` file to read the Op it declares
|
|
15
|
+
* (../commands/lint.ts's `runOpCheckDiagnostics`), and an Op that takes a
|
|
16
|
+
* step argument from `params.<name>` (`@intentius/chant/params`) evaluates
|
|
17
|
+
* that read at module load. With no parameters resolved, `params` is the
|
|
18
|
+
* empty object every such argument reads `undefined` out of, and OPS012
|
|
19
|
+
* reports the activity contract violated — `args.env: expected string,
|
|
20
|
+
* received undefined` — for source that builds and runs correctly. The
|
|
21
|
+
* config is loaded by walking up from the lint path, so linting a
|
|
22
|
+
* subdirectory still sees the project root's declarations.
|
|
23
|
+
*/
|
|
24
|
+
async function lintBuildParams(args: { path: string; param?: string[]; paramsFile?: string }) {
|
|
25
|
+
const { config } = await loadChantConfigUpward(resolve(args.path)).catch(() => ({ config: {} as ChantConfig }));
|
|
26
|
+
if (!config.buildParams) return [];
|
|
27
|
+
return commandBuildParams(config.buildParams, args);
|
|
28
|
+
}
|
|
4
29
|
|
|
5
30
|
export async function runLint(ctx: CommandContext): Promise<number> {
|
|
6
31
|
const { args } = ctx;
|
|
@@ -11,12 +36,16 @@ export async function runLint(ctx: CommandContext): Promise<number> {
|
|
|
11
36
|
return 1;
|
|
12
37
|
}
|
|
13
38
|
|
|
39
|
+
const buildParams = await lintBuildParams(args);
|
|
40
|
+
if (buildParams === undefined) return 1;
|
|
41
|
+
|
|
14
42
|
if (args.watch) {
|
|
15
43
|
const cleanup = lintCommandWatch({
|
|
16
44
|
path: args.path,
|
|
17
45
|
fix: args.fix,
|
|
18
46
|
format: lintFormat,
|
|
19
47
|
sandbox: args.sandbox,
|
|
48
|
+
buildParams,
|
|
20
49
|
});
|
|
21
50
|
process.on("SIGINT", () => {
|
|
22
51
|
cleanup();
|
|
@@ -31,6 +60,7 @@ export async function runLint(ctx: CommandContext): Promise<number> {
|
|
|
31
60
|
fix: args.fix,
|
|
32
61
|
format: lintFormat,
|
|
33
62
|
sandbox: args.sandbox,
|
|
63
|
+
buildParams,
|
|
34
64
|
});
|
|
35
65
|
|
|
36
66
|
printLintResult(result);
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* `loadProfiles` from the project's configured lexicons).
|
|
8
8
|
*/
|
|
9
9
|
import { loadChantConfig } from "../../config";
|
|
10
|
+
import { build } from "../../build";
|
|
11
|
+
import { isResourceDeclarable } from "../../declarable";
|
|
12
|
+
import { collectBuildRootContributors, collectChangeSubscribers } from "../plugins";
|
|
10
13
|
import { discoverOps } from "../../op/discover";
|
|
11
14
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
12
15
|
import { parseDuration } from "../../op/local-executor";
|
|
@@ -15,7 +18,10 @@ import {
|
|
|
15
18
|
runOperatorRound,
|
|
16
19
|
runOperatorForever,
|
|
17
20
|
formatRoundLine,
|
|
21
|
+
formatSignalLine,
|
|
18
22
|
DEFAULT_OPERATOR_INTERVAL_MS,
|
|
23
|
+
type ChangeSubscriber,
|
|
24
|
+
type OperatorSignalEvent,
|
|
19
25
|
type OperatorTickEvent,
|
|
20
26
|
} from "../../op/operator";
|
|
21
27
|
import { readLease, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease";
|
|
@@ -43,6 +49,76 @@ async function loadOperatorActivities() {
|
|
|
43
49
|
return Promise.all([loadActivities(lexicons), loadProfiles()]);
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Bind the change-signal seams (#1981) the daemon will keep subscribed, or an
|
|
54
|
+
* empty list.
|
|
55
|
+
*
|
|
56
|
+
* Three gates, in cost order, so a project that gains nothing from this pays
|
|
57
|
+
* nothing for it:
|
|
58
|
+
*
|
|
59
|
+
* 1. No configured lexicon implements `subscribeChanges`. Return immediately,
|
|
60
|
+
* without loading config or building anything. This is every project today.
|
|
61
|
+
* 2. No `--env`. A subscription resolves the same cluster binding a read
|
|
62
|
+
* does, and there is no binding to resolve without an environment. Said out
|
|
63
|
+
* loud rather than silently skipped, because "why did it not wake" is
|
|
64
|
+
* otherwise unanswerable.
|
|
65
|
+
* 3. The build that supplies the declared entities failed. Warn and fall back
|
|
66
|
+
* to the timer. A subscription is an optimization; a build error here must
|
|
67
|
+
* not stop the operator, which has its own per-tick build inside the tick.
|
|
68
|
+
*/
|
|
69
|
+
async function collectOperatorSubscribers(
|
|
70
|
+
ctx: CommandContext,
|
|
71
|
+
env: string | undefined,
|
|
72
|
+
): Promise<ChangeSubscriber[]> {
|
|
73
|
+
if (!ctx.plugins.some((p) => typeof p.subscribeChanges === "function")) return [];
|
|
74
|
+
|
|
75
|
+
if (!env) {
|
|
76
|
+
console.error(formatWarning({
|
|
77
|
+
message: "a change signal needs an environment to resolve its binding, so this runs on the timer alone",
|
|
78
|
+
hint: "pass --env <env> to let a lexicon's subscribeChanges wake a tick early",
|
|
79
|
+
}));
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cwd = process.cwd();
|
|
84
|
+
try {
|
|
85
|
+
const { config } = await loadChantConfig(cwd);
|
|
86
|
+
const buildRoots = collectBuildRootContributors(
|
|
87
|
+
ctx.plugins,
|
|
88
|
+
config as unknown as Record<string, unknown>,
|
|
89
|
+
cwd,
|
|
90
|
+
);
|
|
91
|
+
const buildResult = await build(config.sourceDir ?? ".", ctx.serializers, undefined, { buildRoots });
|
|
92
|
+
if (buildResult.errors.length > 0) {
|
|
93
|
+
console.error(formatWarning({
|
|
94
|
+
message: "build failed while scoping the change signal, so this runs on the timer alone",
|
|
95
|
+
}));
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The declared estate, sliced per lexicon: the same slice
|
|
100
|
+
// `takeSnapshot` hands `describeResources`, and the bound on what any
|
|
101
|
+
// subscription may watch.
|
|
102
|
+
const entities = new Map<string, Map<string, { entityType: string; props: Record<string, unknown> }>>();
|
|
103
|
+
for (const [name, entity] of buildResult.entities) {
|
|
104
|
+
if (!isResourceDeclarable(entity)) continue;
|
|
105
|
+
let perLexicon = entities.get(entity.lexicon);
|
|
106
|
+
if (!perLexicon) entities.set(entity.lexicon, (perLexicon = new Map()));
|
|
107
|
+
perLexicon.set(name, {
|
|
108
|
+
entityType: entity.entityType,
|
|
109
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return collectChangeSubscribers(ctx.plugins, { environment: env, cwd, entities });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(formatWarning({
|
|
116
|
+
message: `could not scope the change signal, so this runs on the timer alone (${err instanceof Error ? err.message : String(err)})`,
|
|
117
|
+
}));
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
46
122
|
// ── chant operator ──────────────────────────────────────────────────────────
|
|
47
123
|
|
|
48
124
|
/**
|
|
@@ -98,6 +174,9 @@ export async function runOperator(ctx: CommandContext): Promise<number> {
|
|
|
98
174
|
console.error(formatInfo(
|
|
99
175
|
`chant operator: watching ${ops.length} ConvergeOp(s) every ${intervalMs}ms (Ctrl-C to stop)`,
|
|
100
176
|
));
|
|
177
|
+
// A change signal (#1981) only ever shortens the sleep above. Every round
|
|
178
|
+
// it wakes is the round the timer would have run.
|
|
179
|
+
const subscribers = await collectOperatorSubscribers(ctx, ctx.args.env);
|
|
101
180
|
await runOperatorForever({
|
|
102
181
|
env: ctx.args.env,
|
|
103
182
|
intervalMs,
|
|
@@ -106,6 +185,8 @@ export async function runOperator(ctx: CommandContext): Promise<number> {
|
|
|
106
185
|
profiles,
|
|
107
186
|
signal: controller.signal,
|
|
108
187
|
onRound: printRound,
|
|
188
|
+
subscribers,
|
|
189
|
+
onSignalEvent: (event: OperatorSignalEvent) => console.error(formatInfo(formatSignalLine(event))),
|
|
109
190
|
});
|
|
110
191
|
return 0;
|
|
111
192
|
} finally {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import type { ParsedArgs } from "../registry";
|
|
3
6
|
|
|
4
7
|
const discoverOpsMock = vi.fn();
|
|
@@ -208,7 +211,7 @@ describe("runOp dispatcher", () => {
|
|
|
208
211
|
test("gate in local mode → exit 3 and the approve line (#2119)", async () => {
|
|
209
212
|
gateLedger = memoryGateLedgerPort();
|
|
210
213
|
discoverOpsMock.mockResolvedValue({
|
|
211
|
-
ops: new Map([localOp("gated", [{ kind: "gate",
|
|
214
|
+
ops: new Map([localOp("gated", [{ kind: "gate", gate: "approve-prod" }])]),
|
|
212
215
|
errors: [],
|
|
213
216
|
});
|
|
214
217
|
// `renderHuman` writes straight to process.stderr, not through console.error.
|
|
@@ -235,7 +238,7 @@ describe("runOp dispatcher", () => {
|
|
|
235
238
|
});
|
|
236
239
|
discoverOpsMock.mockResolvedValue({
|
|
237
240
|
ops: new Map([localOp("gated", [
|
|
238
|
-
{ kind: "gate",
|
|
241
|
+
{ kind: "gate", gate: "approve-prod" },
|
|
239
242
|
{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } },
|
|
240
243
|
])]),
|
|
241
244
|
errors: [],
|
|
@@ -266,6 +269,132 @@ describe("runOp dispatcher", () => {
|
|
|
266
269
|
});
|
|
267
270
|
});
|
|
268
271
|
|
|
272
|
+
/**
|
|
273
|
+
* chant #2243 — a gated run's exit code is the one thing `--gated-exit`
|
|
274
|
+
* remaps, and the gate is reported where CI can see it without opening a log.
|
|
275
|
+
*
|
|
276
|
+
* The motivating shape is a push-to-main terraform apply: GitHub Actions has
|
|
277
|
+
* no neutral conclusion for a `run:` step, so exit 3 paints the branch red on
|
|
278
|
+
* every merge until someone approves. `--gated-exit 0` says a pending
|
|
279
|
+
* approval is not a broken build; nothing else about the run changes, and a
|
|
280
|
+
* failure is still a failure.
|
|
281
|
+
*/
|
|
282
|
+
describe("runOp: --gated-exit (#2243)", () => {
|
|
283
|
+
const gateStep = { kind: "gate", gate: "approve-prod" };
|
|
284
|
+
|
|
285
|
+
function summaryFile(): string {
|
|
286
|
+
return join(mkdtempSync(join(tmpdir(), "chant-gated-exit-")), "summary.md");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
beforeEach(() => {
|
|
290
|
+
discoverOpsMock.mockReset();
|
|
291
|
+
loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
|
|
292
|
+
loadPluginsMock.mockReset().mockResolvedValue([]);
|
|
293
|
+
gateLedger = memoryGateLedgerPort();
|
|
294
|
+
delete process.env.GITHUB_STEP_SUMMARY;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
afterEach(() => {
|
|
298
|
+
delete process.env.GITHUB_STEP_SUMMARY;
|
|
299
|
+
vi.restoreAllMocks();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("unmapped: without the flag a gated run still exits 3", async () => {
|
|
303
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
|
|
304
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
305
|
+
const exit = await runOp({ args: makeArgs({ path: "gated" }), plugins: [], serializers: [] });
|
|
306
|
+
expect(exit).toBe(3);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("mapped: --gated-exit 0 exits 0 and writes the gate to GITHUB_STEP_SUMMARY", async () => {
|
|
310
|
+
const file = summaryFile();
|
|
311
|
+
process.env.GITHUB_STEP_SUMMARY = file;
|
|
312
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
|
|
313
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
314
|
+
const stderr = makeStderrSpy();
|
|
315
|
+
|
|
316
|
+
const exit = await runOp({
|
|
317
|
+
args: makeArgs({ path: "gated", gatedExit: 0 }),
|
|
318
|
+
plugins: [], serializers: [],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(exit).toBe(0);
|
|
322
|
+
// The run genuinely stopped: the pending fact is on the ledger, and
|
|
323
|
+
// nothing after the gate ran.
|
|
324
|
+
expect(gateLedger.appended).toHaveLength(1);
|
|
325
|
+
const summary = readFileSync(file, "utf8");
|
|
326
|
+
expect(summary).toContain("approve-prod");
|
|
327
|
+
expect(summary).toContain("chant approve gated approve-prod");
|
|
328
|
+
expect(summary).toContain("_gates/gated.jsonl");
|
|
329
|
+
// A green run that applied nothing says so on stderr too.
|
|
330
|
+
expect(stderr.join("\n")).toContain("exiting 0");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("mapped: the --json payload still reports the gate, not a success", async () => {
|
|
334
|
+
const file = summaryFile();
|
|
335
|
+
process.env.GITHUB_STEP_SUMMARY = file;
|
|
336
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
|
|
337
|
+
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
338
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
339
|
+
makeStderrSpy();
|
|
340
|
+
|
|
341
|
+
const exit = await runOp({
|
|
342
|
+
args: makeArgs({ path: "gated", gatedExit: 0, json: true }),
|
|
343
|
+
plugins: [], serializers: [],
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(exit).toBe(0);
|
|
347
|
+
const parsed = JSON.parse(stdoutWrite.mock.calls.map((c) => String(c[0])).join("").trim());
|
|
348
|
+
expect(parsed.status).toBe("gated");
|
|
349
|
+
expect(parsed.gate).toMatchObject({ name: "approve-prod" });
|
|
350
|
+
expect(parsed.approve).toBe("chant approve gated approve-prod");
|
|
351
|
+
// The summary is written whether or not stdout is machine-readable.
|
|
352
|
+
expect(readFileSync(file, "utf8")).toContain("chant approve gated approve-prod");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("a run that fails for any other reason is still red under the flag", async () => {
|
|
356
|
+
discoverOpsMock.mockResolvedValue({
|
|
357
|
+
ops: new Map([localOp("broken", [{ kind: "activity", fn: "shellCmd", args: { cmd: "exit 7" } }])]),
|
|
358
|
+
errors: [],
|
|
359
|
+
});
|
|
360
|
+
const file = summaryFile();
|
|
361
|
+
process.env.GITHUB_STEP_SUMMARY = file;
|
|
362
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
363
|
+
|
|
364
|
+
const exit = await runOp({
|
|
365
|
+
args: makeArgs({ path: "broken", gatedExit: 0 }),
|
|
366
|
+
plugins: [], serializers: [],
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(exit).toBe(1);
|
|
370
|
+
// Nothing is pending, so nothing is reported as pending.
|
|
371
|
+
expect(existsSync(file)).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("a value that is not an exit status is refused before the Op runs", async () => {
|
|
375
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
|
|
376
|
+
const stderr = makeStderrSpy();
|
|
377
|
+
const exit = await runOp({
|
|
378
|
+
args: makeArgs({ path: "gated", gatedExit: 300 }),
|
|
379
|
+
plugins: [], serializers: [],
|
|
380
|
+
});
|
|
381
|
+
expect(exit).toBe(1);
|
|
382
|
+
expect(stderr.join("\n")).toContain("--gated-exit expects a whole number from 0 to 255");
|
|
383
|
+
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("no GITHUB_STEP_SUMMARY, no file: the variable is the whole forge coupling", async () => {
|
|
387
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [gateStep])]), errors: [] });
|
|
388
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
389
|
+
const exit = await runOp({
|
|
390
|
+
args: makeArgs({ path: "gated", gatedExit: 0 }),
|
|
391
|
+
plugins: [], serializers: [],
|
|
392
|
+
});
|
|
393
|
+
expect(exit).toBe(0);
|
|
394
|
+
expect(process.env.GITHUB_STEP_SUMMARY).toBeUndefined();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
269
398
|
/**
|
|
270
399
|
* chant #2003 — `--sandbox` is a global flag and `../main.ts` arms the
|
|
271
400
|
* process-wide policy latch off it for every command, so `chant run <op>
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { OpConfig } from "../../op/types";
|
|
|
6
6
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
7
7
|
import { runOpLocally, findPolicyGateStep, OpRunFailure, type StepRecord } from "../../op/local-executor";
|
|
8
8
|
import { approveCommand } from "../../op/gate";
|
|
9
|
+
import { writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
|
|
9
10
|
import { createLocalOpRuntime } from "../../op/runtimes/local";
|
|
10
11
|
import type { OpRuntimeProvider, OpRunStatus } from "../../op/runtime";
|
|
11
12
|
import { renderHuman, renderJson } from "../../op/local-output";
|
|
@@ -28,6 +29,54 @@ import type { DriverComponentResult } from "../../components/driver";
|
|
|
28
29
|
*/
|
|
29
30
|
export const GATED_EXIT_CODE = 3;
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The exit code this invocation gives a gated run — {@link GATED_EXIT_CODE}
|
|
34
|
+
* unless `--gated-exit <code>` asked for another one (#2243).
|
|
35
|
+
*
|
|
36
|
+
* The mapping lives here rather than in a shell wrapper in every generated
|
|
37
|
+
* pipeline, so one rule covers every forge: GitHub Actions has no neutral
|
|
38
|
+
* conclusion for a `run:` step, so a push-to-main apply that stops at its gate
|
|
39
|
+
* paints the branch red on every merge until someone approves. `--gated-exit
|
|
40
|
+
* 0` is how the job that knows a pending approval is not a failure says so.
|
|
41
|
+
*
|
|
42
|
+
* Only the gated outcome is remapped. A failed run still returns 1, so the
|
|
43
|
+
* flag can never hide a broken apply. Returns `undefined` after printing the
|
|
44
|
+
* refusal when the value is not a process exit status.
|
|
45
|
+
*/
|
|
46
|
+
function resolveGatedExitCode(ctx: CommandContext): number | undefined {
|
|
47
|
+
const raw = ctx.args.gatedExit;
|
|
48
|
+
if (raw === undefined) return GATED_EXIT_CODE;
|
|
49
|
+
if (!Number.isInteger(raw) || raw < 0 || raw > 255) {
|
|
50
|
+
console.error(formatError({
|
|
51
|
+
message: "--gated-exit expects a whole number from 0 to 255",
|
|
52
|
+
hint: `Got ${Number.isNaN(raw) ? "a non-numeric value" : String(raw)}. ` +
|
|
53
|
+
`Pass --gated-exit 0 to make a run that stopped at a gate a success for CI; ` +
|
|
54
|
+
`omit the flag for the default ${GATED_EXIT_CODE}.`,
|
|
55
|
+
}));
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What a gated run leaves behind for CI, beyond the stderr block the renderers
|
|
63
|
+
* already print (#2243): the same gate, approve command and ledger path
|
|
64
|
+
* appended to whatever file `GITHUB_STEP_SUMMARY` names, so the run page says
|
|
65
|
+
* what is pending without anyone opening the log.
|
|
66
|
+
*
|
|
67
|
+
* Also says on stderr that the exit code was remapped, when it was. A job that
|
|
68
|
+
* passes `--gated-exit 0` reports success, and the one line that explains why
|
|
69
|
+
* a zero-exit run applied nothing belongs next to the gate itself.
|
|
70
|
+
*/
|
|
71
|
+
function reportGatedRun(summary: GatedRunSummary, exitCode: number): void {
|
|
72
|
+
writeGatedRunSummary(summary);
|
|
73
|
+
if (exitCode !== GATED_EXIT_CODE) {
|
|
74
|
+
console.error(formatInfo(
|
|
75
|
+
`gated: exiting ${exitCode} because --gated-exit asked for it. Nothing after the gate ran.`,
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
/**
|
|
32
81
|
* `run list/status/log/cancel --components` reported a component's *durable*
|
|
33
82
|
* run state — a run that outlives the CLI process and can be queried, signalled
|
|
@@ -613,6 +662,9 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
613
662
|
return 1;
|
|
614
663
|
}
|
|
615
664
|
|
|
665
|
+
const gatedExit = resolveGatedExitCode(ctx);
|
|
666
|
+
if (gatedExit === undefined) return 1;
|
|
667
|
+
|
|
616
668
|
const projectPath = resolve(".");
|
|
617
669
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
618
670
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
@@ -692,7 +744,17 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
692
744
|
console.error(formatInfo(`approve : ${approveCommand(gate.op, gate.gate)}`));
|
|
693
745
|
if (gate.url) console.error(formatInfo(`approve at: ${gate.url}`));
|
|
694
746
|
console.error(formatInfo(`expires : ${gate.expiresAt}`));
|
|
695
|
-
|
|
747
|
+
reportGatedRun(
|
|
748
|
+
{
|
|
749
|
+
op: gate.op,
|
|
750
|
+
gate: gate.gate,
|
|
751
|
+
...(gate.description ? { description: gate.description } : {}),
|
|
752
|
+
expiresAt: gate.expiresAt,
|
|
753
|
+
...(gate.url ? { url: gate.url } : {}),
|
|
754
|
+
},
|
|
755
|
+
gatedExit,
|
|
756
|
+
);
|
|
757
|
+
return gatedExit;
|
|
696
758
|
}
|
|
697
759
|
|
|
698
760
|
if (result.success && result.run) {
|
|
@@ -725,6 +787,9 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
725
787
|
return 1;
|
|
726
788
|
}
|
|
727
789
|
|
|
790
|
+
const gatedExit = resolveGatedExitCode(ctx);
|
|
791
|
+
if (gatedExit === undefined) return 1;
|
|
792
|
+
|
|
728
793
|
const { ops, errors } = await discoverOps();
|
|
729
794
|
for (const err of errors) console.error(formatWarning({ message: err }));
|
|
730
795
|
|
|
@@ -786,8 +851,27 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
786
851
|
|
|
787
852
|
// Exit 3 for a gated run (#2119) — a distinct code so CI can tell
|
|
788
853
|
// "waiting on a human" from a broken op, and retry the one but not the
|
|
789
|
-
// other.
|
|
790
|
-
|
|
854
|
+
// other. `--gated-exit <code>` remaps that one outcome and nothing else
|
|
855
|
+
// (#2243).
|
|
856
|
+
if (status.state === "gated") {
|
|
857
|
+
// The local runtime carries the whole pending fact on its result; a
|
|
858
|
+
// runtime that reports only a state carries the gate's name alone.
|
|
859
|
+
const pending = status.result?.gate;
|
|
860
|
+
const gate = pending?.gate ?? status.gate?.name;
|
|
861
|
+
if (gate) {
|
|
862
|
+
reportGatedRun(
|
|
863
|
+
{
|
|
864
|
+
op: opName,
|
|
865
|
+
gate,
|
|
866
|
+
...(pending?.description ? { description: pending.description } : {}),
|
|
867
|
+
...(pending?.expiresAt ? { expiresAt: pending.expiresAt } : {}),
|
|
868
|
+
...(pending?.url ? { url: pending.url } : {}),
|
|
869
|
+
},
|
|
870
|
+
gatedExit,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
return gatedExit;
|
|
874
|
+
}
|
|
791
875
|
return status.state === "completed" ? 0 : 1;
|
|
792
876
|
} catch (err) {
|
|
793
877
|
if (err instanceof OpRunFailure) {
|
package/src/cli/main.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { parseArgs, waitForStreamDrain
|
|
3
|
+
import { parseArgs, waitForStreamDrain } from "./main";
|
|
4
4
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
5
5
|
|
|
6
6
|
describe("parseArgs", () => {
|
|
@@ -590,16 +590,14 @@ describe("resolveCommand", () => {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
describe("--temporal, removed in #2116", () => {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
expect(
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
expect(usesRemovedTemporalFlag(["run", "alb-deploy", "--on", "fountain"])).toBe(false);
|
|
602
|
-
expect(() => parseArgs(["run", "alb-deploy", "--temporal"])).toThrow(/Unknown flag/);
|
|
593
|
+
// #2204 removed the bridge that caught the flag ahead of the parser and
|
|
594
|
+
// exited 2. Both spellings now take the same route any other unrecognised
|
|
595
|
+
// flag takes, and nothing in the CLI knows the word.
|
|
596
|
+
test("both spellings fail as an unknown flag, like any other unrecognised flag", () => {
|
|
597
|
+
expect(() => parseArgs(["run", "alb-deploy", "--temporal"])).toThrow(/Unknown flag: --temporal/);
|
|
598
|
+
expect(() => parseArgs(["run", "alb-deploy", "--temporal=true"])).toThrow(/Unknown flag: --temporal/);
|
|
599
|
+
// The message and the throw match what an invented flag gets.
|
|
600
|
+
expect(() => parseArgs(["run", "alb-deploy", "--nonesuch"])).toThrow(/Unknown flag: --nonesuch/);
|
|
603
601
|
});
|
|
604
602
|
});
|
|
605
603
|
|
package/src/cli/main.ts
CHANGED
|
@@ -95,27 +95,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
95
95
|
/**
|
|
96
96
|
* Parse command line arguments
|
|
97
97
|
*/
|
|
98
|
-
/**
|
|
99
|
-
* `--temporal` picked the Temporal runtime, which #2116 deleted. It is caught
|
|
100
|
-
* ahead of {@link parseArgs} — which no longer knows the flag at all — for one
|
|
101
|
-
* minor version, so an invocation that still carries it is told where the
|
|
102
|
-
* runtime went instead of getting "Unknown flag: --temporal" and a pointer at
|
|
103
|
-
* `--help`. Delete this, its test and the constants below once that version
|
|
104
|
-
* has shipped.
|
|
105
|
-
*/
|
|
106
|
-
export const REMOVED_TEMPORAL_FLAG = "--temporal was removed in #2116; use --on fountain";
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Not 1: a removed flag means the command never started, so a CI job that
|
|
110
|
-
* retries a failed run has nothing to retry here.
|
|
111
|
-
*/
|
|
112
|
-
export const REMOVED_FLAG_EXIT_CODE = 2;
|
|
113
|
-
|
|
114
|
-
/** Matches the bare flag and the joined `--temporal=…` form `splitJoinedFlags` would otherwise split. */
|
|
115
|
-
export function usesRemovedTemporalFlag(argv: string[]): boolean {
|
|
116
|
-
return argv.some((arg) => arg === "--temporal" || arg.startsWith("--temporal="));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
98
|
export function parseArgs(args: string[]): ParsedArgs {
|
|
120
99
|
// Local mutable copy — chant #1127's joined-`--flag=value` splitting below
|
|
121
100
|
// rewrites the array in place (one token becomes two), so this must not
|
|
@@ -328,6 +307,11 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
328
307
|
result.json = true;
|
|
329
308
|
} else if (arg === "--progress-json") {
|
|
330
309
|
result.progressJson = true;
|
|
310
|
+
} else if (arg === "--gated-exit") {
|
|
311
|
+
// `chant run <op> --gated-exit <code>` (#2243) — remap only the gated
|
|
312
|
+
// outcome's exit code. Parsed as a number here and range-checked in the
|
|
313
|
+
// handler, where the refusal can name the flag alongside the run.
|
|
314
|
+
result.gatedExit = Number(args[++i]);
|
|
331
315
|
} else if (arg === "--durable-requests") {
|
|
332
316
|
// `chant run approve ... --on <lexicon> --durable-requests` (#2126) —
|
|
333
317
|
// resolve the gate on the hosting runtime's own request path rather
|
|
@@ -1006,12 +990,6 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1006
990
|
async function main(): Promise<void> {
|
|
1007
991
|
const rawArgv = process.argv.slice(2);
|
|
1008
992
|
|
|
1009
|
-
if (usesRemovedTemporalFlag(rawArgv)) {
|
|
1010
|
-
console.error(formatError({ message: REMOVED_TEMPORAL_FLAG }));
|
|
1011
|
-
await flushAndExit(REMOVED_FLAG_EXIT_CODE);
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
993
|
let args: ParsedArgs;
|
|
1016
994
|
try {
|
|
1017
995
|
args = parseArgs(rawArgv);
|