@intentius/chant 0.58.0 → 0.59.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/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/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/verbs/run-agent.d.ts +1 -7
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/lexicon.d.ts +170 -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/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 +62 -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 +43 -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/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/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 +14 -0
- 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 +146 -1
- package/src/cli/commands/lint.ts +82 -7
- 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/__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/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/detectLexicon.ts +18 -1
- package/src/discovery/fold-import.test.ts +1 -1
- package/src/fold/foldable-helpers.ts +1 -1
- package/src/graph-ops.test.ts +1 -1
- package/src/lexicon.ts +177 -3
- package/src/lifecycle/gate-ledger.ts +12 -1
- 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 +2 -2
- package/src/op/activities/reconcile.test.ts +82 -2
- package/src/op/activities/reconcile.ts +169 -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.ts +84 -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/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
package/src/cli/commands/lint.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
5
|
import { runLint, parseDisableComments } from "../../lint/engine";
|
|
6
6
|
import type { LintRule, LintDiagnostic, LintFix } from "../../lint/rule";
|
|
7
|
-
import type { IntrinsicDef } from "../../lexicon";
|
|
7
|
+
import type { IntrinsicDef, LexiconPlugin } from "../../lexicon";
|
|
8
8
|
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
9
9
|
import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
|
|
10
10
|
import { loadLocalRules } from "../../lint/rule-loader";
|
|
@@ -20,10 +20,12 @@ import { rule } from "../../lint/declarative";
|
|
|
20
20
|
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
21
21
|
import { formatError, formatInfo } from "../format";
|
|
22
22
|
import { GENERATED_MARKER } from "../../discovery/files";
|
|
23
|
+
import { isNoLexiconDetected } from "../../detectLexicon";
|
|
23
24
|
|
|
24
25
|
// Import config loader
|
|
25
26
|
import { loadConfig, resolveRulesForFile, resolveConfiguredSeverity, findProjectRoot } from "../../lint/config";
|
|
26
27
|
import { loadChantConfig, resolveKnowledgeDir } from "../../config";
|
|
28
|
+
import { findProjectConfig } from "../../project-root";
|
|
27
29
|
import type { LintProjectConfig } from "../../lint/rule";
|
|
28
30
|
import { loadOkfBundle, type OkfBundle } from "../../okf-read";
|
|
29
31
|
|
|
@@ -91,6 +93,40 @@ export async function loadPluginRules(
|
|
|
91
93
|
return pluginRules;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The diagnostic id `chant lint` reports a lexicon it could not resolve under
|
|
98
|
+
* (chant #2222).
|
|
99
|
+
*
|
|
100
|
+
* Deliberately outside the COR/EVL/COMP/OPS families: those are *rules*, each
|
|
101
|
+
* with a `check()` the engine runs per file, a documented page, a configurable
|
|
102
|
+
* severity and a `chant-disable` escape hatch. This is none of those. It is
|
|
103
|
+
* the lint run reporting that it could not assemble the rule set the project
|
|
104
|
+
* asked for, so it is not silenceable through `rules: { ... : "off" }` or a
|
|
105
|
+
* disable comment. Silencing it would put back the exact "green on a broken
|
|
106
|
+
* project" this id exists to prevent.
|
|
107
|
+
*/
|
|
108
|
+
export const LEXICON_RESOLUTION_RULE_ID = "LEX001";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Turn a lexicon-resolution failure into the error diagnostic `chant lint`
|
|
112
|
+
* reports it as. The message is the underlying error's own, unwrapped, so it
|
|
113
|
+
* is character-for-character what `chant build` prints after `error: ` for the
|
|
114
|
+
* same project (`loadPluginsOrExit`, ../main.ts): one failure, one wording.
|
|
115
|
+
*
|
|
116
|
+
* Attributed to the project's `chant.config.*` where there is one, since that
|
|
117
|
+
* is the file that names the lexicon; to the project root otherwise.
|
|
118
|
+
*/
|
|
119
|
+
function lexiconResolutionDiagnostic(projectRoot: string, error: Error): LintDiagnostic {
|
|
120
|
+
return {
|
|
121
|
+
file: findProjectConfig(projectRoot).configPath ?? projectRoot,
|
|
122
|
+
line: 1,
|
|
123
|
+
column: 1,
|
|
124
|
+
ruleId: LEXICON_RESOLUTION_RULE_ID,
|
|
125
|
+
severity: "error",
|
|
126
|
+
message: error.message,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
94
130
|
/**
|
|
95
131
|
* Load all lint rules: core COR/EVL rules, then lexicon plugin rules.
|
|
96
132
|
*
|
|
@@ -103,7 +139,7 @@ export async function loadPluginRules(
|
|
|
103
139
|
*/
|
|
104
140
|
async function loadAllPluginRules(
|
|
105
141
|
projectPath: string,
|
|
106
|
-
): Promise<{ rules: Map<string, LintRule>; intrinsics: IntrinsicDef[] }> {
|
|
142
|
+
): Promise<{ rules: Map<string, LintRule>; intrinsics: IntrinsicDef[]; lexiconError?: Error }> {
|
|
107
143
|
const rules = new Map<string, LintRule>();
|
|
108
144
|
|
|
109
145
|
// Load core COR/EVL rules directly
|
|
@@ -111,16 +147,47 @@ async function loadAllPluginRules(
|
|
|
111
147
|
rules.set(r.id, r);
|
|
112
148
|
}
|
|
113
149
|
|
|
114
|
-
// Resolve project lexicons (e.g. ["aws"]) from config or detection
|
|
150
|
+
// Resolve project lexicons (e.g. ["aws"]) from config or detection, then
|
|
151
|
+
// load their plugins.
|
|
152
|
+
//
|
|
153
|
+
// chant #2222: a failure in either step is handed back to `lintCommand`
|
|
154
|
+
// (which reports it as {@link LEXICON_RESOLUTION_RULE_ID}) rather than
|
|
155
|
+
// swallowed or thrown. Both steps fail on the same project state: a
|
|
156
|
+
// `chant.config.ts` that names a lexicon whose package is not installed
|
|
157
|
+
// throws out of `loadPlugins`, and one that *imports* that package throws
|
|
158
|
+
// out of `resolveProjectLexicons` before the names are ever read. Before
|
|
159
|
+
// this, the first case crashed `chant lint` with a stack trace and the
|
|
160
|
+
// second was caught here and discarded, so `chant lint` printed "No
|
|
161
|
+
// problems found" and exited 0 on a project `chant build` refuses to
|
|
162
|
+
// build. A CI job that lints before it builds reported green on a project
|
|
163
|
+
// whose lexicon was missing.
|
|
164
|
+
//
|
|
165
|
+
// The one failure that stays quiet is the one the original `catch` was
|
|
166
|
+
// written for and named in its comment: a project that declares no
|
|
167
|
+
// `lexicons` and imports none from its source files, where the detection
|
|
168
|
+
// fallback throws `NO_LEXICON_DETECTED_MESSAGE`. That project lints under
|
|
169
|
+
// the core rules alone and always has. It is told apart by the exported
|
|
170
|
+
// sentinel rather than by a string literal copied to this file, so the two
|
|
171
|
+
// cannot drift apart.
|
|
115
172
|
let lexiconNames: string[] = [];
|
|
173
|
+
let lexiconError: Error | undefined;
|
|
116
174
|
try {
|
|
117
175
|
lexiconNames = await resolveProjectLexicons(projectPath);
|
|
118
|
-
} catch {
|
|
119
|
-
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (!isNoLexiconDetected(err)) {
|
|
178
|
+
lexiconError = err instanceof Error ? err : new Error(String(err));
|
|
179
|
+
}
|
|
120
180
|
}
|
|
121
181
|
|
|
122
182
|
// Load only project lexicon plugins (no "chant" injection)
|
|
123
|
-
|
|
183
|
+
let plugins: LexiconPlugin[] = [];
|
|
184
|
+
if (!lexiconError) {
|
|
185
|
+
try {
|
|
186
|
+
plugins = await loadPlugins(lexiconNames);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
lexiconError = err instanceof Error ? err : new Error(String(err));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
124
191
|
|
|
125
192
|
// chant #1106 — the same plugins' registered intrinsics (`Ref`, `GetAtt`,
|
|
126
193
|
// ...), so EVL001 can answer "does this call fold?" exactly like fold()
|
|
@@ -150,7 +217,7 @@ async function loadAllPluginRules(
|
|
|
150
217
|
rules.set(r.id, r);
|
|
151
218
|
}
|
|
152
219
|
|
|
153
|
-
return { rules, intrinsics };
|
|
220
|
+
return { rules, intrinsics, ...(lexiconError ? { lexiconError } : {}) };
|
|
154
221
|
}
|
|
155
222
|
|
|
156
223
|
/**
|
|
@@ -695,6 +762,14 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
695
762
|
suppressed.push(...postOpResult.suppressed);
|
|
696
763
|
}
|
|
697
764
|
|
|
765
|
+
// chant #2222: a lexicon the project declared but this run could not
|
|
766
|
+
// resolve. Appended here, after the `--fix` re-lint block above has finished
|
|
767
|
+
// reassigning `diagnostics`, so it survives every path; it is a property of
|
|
768
|
+
// the run, not of any file a fix could touch.
|
|
769
|
+
if (loaded.lexiconError) {
|
|
770
|
+
diagnostics.push(lexiconResolutionDiagnostic(projectRoot, loaded.lexiconError));
|
|
771
|
+
}
|
|
772
|
+
|
|
698
773
|
// Count errors and warnings
|
|
699
774
|
let errorCount = 0;
|
|
700
775
|
let warningCount = 0;
|
|
@@ -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);
|