@intentius/chant 0.124.22 → 0.124.24
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/rules-doc.js +1 -1
- package/dist/ci-jobs.d.ts +41 -1
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +48 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +14 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +9 -0
- package/dist/cli/handlers/emulator.d.ts +13 -2
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.js +63 -3
- package/dist/cli/handlers/graph.js +24 -1
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +7 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.js +5 -4
- package/dist/cli/handlers/run.d.ts +4 -3
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +73 -9
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +17 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -0
- package/dist/cli/registry.d.ts +12 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +45 -6
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +97 -22
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +25 -10
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -2
- package/dist/graph-ir.d.ts +43 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +27 -0
- package/dist/lexicon.d.ts +25 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +11 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +62 -6
- package/dist/lifecycle/no-record.d.ts +59 -0
- package/dist/lifecycle/no-record.d.ts.map +1 -0
- package/dist/lifecycle/no-record.js +88 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +30 -9
- package/dist/op/emulator-lifecycle.d.ts +23 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.js +39 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +10 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +7 -2
- package/dist/op/op-waves-notify.d.ts +2 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -1
- package/dist/op/op-waves-notify.js +9 -3
- package/dist/op/op-waves-run.d.ts +8 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +36 -11
- package/dist/op/op-waves.d.ts +85 -1
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +153 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +6 -0
- package/dist/op/step-console.d.ts +65 -0
- package/dist/op/step-console.d.ts.map +1 -0
- package/dist/op/step-console.js +140 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +240 -224
- package/dist/workspace/checks/links.d.ts +5 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/links.js +54 -8
- package/dist/workspace/checks.d.ts +4 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +6 -2
- package/dist/workspace/compose-graph.d.ts +26 -4
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +21 -7
- package/dist/workspace/dbt-project-reader.d.ts +36 -0
- package/dist/workspace/dbt-project-reader.d.ts.map +1 -0
- package/dist/workspace/dbt-project-reader.js +290 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +3 -1
- package/dist/workspace/graph.schema.json +109 -4
- package/dist/workspace/intent.js +7 -2
- package/dist/workspace/kind-link-readers.d.ts +84 -0
- package/dist/workspace/kind-link-readers.d.ts.map +1 -0
- package/dist/workspace/kind-link-readers.js +53 -0
- package/dist/workspace/kinds.d.ts +15 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +11 -0
- package/dist/workspace/lexicon-outputs.d.ts +63 -0
- package/dist/workspace/lexicon-outputs.d.ts.map +1 -0
- package/dist/workspace/lexicon-outputs.js +241 -0
- package/dist/workspace/links.d.ts +29 -7
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/links.js +109 -34
- package/dist/workspace/member-handles.d.ts +18 -1
- package/dist/workspace/member-handles.d.ts.map +1 -1
- package/dist/workspace/member-handles.js +31 -14
- package/dist/workspace/nested-graph.d.ts +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +5 -1
- package/dist/workspace/records-checkout.d.ts +11 -1
- package/dist/workspace/records-checkout.d.ts.map +1 -1
- package/dist/workspace/records-checkout.js +38 -2
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +3 -2
- package/dist/workspace/workspace-kinds.schema.json +5 -0
- package/package.json +1 -1
- package/src/audit/rules-doc.ts +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- package/src/cli/handlers/approve.ts +9 -0
- package/src/cli/handlers/emulator.test.ts +61 -1
- package/src/cli/handlers/emulator.ts +65 -4
- package/src/cli/handlers/graph.ts +22 -1
- package/src/cli/handlers/promote.test.ts +7 -0
- package/src/cli/handlers/promote.ts +7 -1
- package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
- package/src/cli/handlers/run-wave.ts +5 -4
- package/src/cli/handlers/run.test.ts +126 -0
- package/src/cli/handlers/run.ts +69 -9
- package/src/cli/main.test.ts +24 -0
- package/src/cli/main.ts +17 -4
- package/src/cli/parse-args.ts +21 -0
- package/src/cli/registry.ts +12 -0
- package/src/components/cli-support.test.ts +75 -0
- package/src/components/cli-support.ts +130 -25
- package/src/components/driver.test.ts +28 -0
- package/src/components/driver.ts +23 -10
- package/src/components/generate-disabled.test.ts +56 -0
- package/src/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -2
- package/src/graph-ir.test.ts +23 -1
- package/src/graph-ir.ts +65 -0
- package/src/lexicon.ts +26 -1
- package/src/lifecycle/git.ts +61 -6
- package/src/lifecycle/no-record.test.ts +175 -0
- package/src/lifecycle/no-record.ts +108 -0
- package/src/op/activities/shell.ts +28 -6
- package/src/op/emulator-lifecycle.test.ts +24 -1
- package/src/op/emulator-lifecycle.ts +58 -2
- package/src/op/gate.ts +3 -0
- package/src/op/index.ts +1 -1
- package/src/op/local-executor.ts +20 -3
- package/src/op/op-waves-jobs.test.ts +213 -0
- package/src/op/op-waves-notify.ts +11 -5
- package/src/op/op-waves-run.ts +42 -11
- package/src/op/op-waves.ts +202 -2
- package/src/op/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/wave-loop-golden.test.ts +3 -0
- package/src/workspace/checks/links.ts +57 -8
- package/src/workspace/checks.e2e.test.ts +1 -1
- package/src/workspace/checks.ts +9 -2
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +33 -9
- package/src/workspace/dbt-kind.e2e.test.ts +95 -0
- package/src/workspace/dbt-kind.test.ts +283 -0
- package/src/workspace/dbt-project-reader.ts +291 -0
- package/src/workspace/graph-cli.ts +3 -1
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +109 -4
- package/src/workspace/intent.ts +7 -2
- package/src/workspace/kind-link-readers.ts +109 -0
- package/src/workspace/kinds.ts +21 -1
- package/src/workspace/lexicon-outputs.ts +272 -0
- package/src/workspace/links.test.ts +22 -0
- package/src/workspace/links.ts +129 -34
- package/src/workspace/member-handles.ts +28 -8
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/records-checkout.ts +36 -2
- package/src/workspace/records-uncommitted.test.ts +5 -1
- package/src/workspace/runs.ts +4 -2
- package/src/workspace/status.ts +3 -2
- package/src/workspace/workspace-kinds.schema.json +5 -0
package/src/ci-jobs.ts
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* - the `setup` steps and `environment` a job may carry, checked once per
|
|
16
16
|
* forge ({@link assertSetupSteps}, {@link assertEnvironment},
|
|
17
17
|
* {@link gitlabSetupScript}, {@link assertGitlabEnvironment});
|
|
18
|
-
* - GitLab's `rules:` expressions and `id_tokens:` declaration
|
|
18
|
+
* - GitLab's `rules:` expressions and `id_tokens:` declaration;
|
|
19
|
+
* - where a job runs and the id tokens it asks for (#3914), {@link githubRunsOn},
|
|
20
|
+
* {@link gitlabTags}, {@link gitlabIdTokensDoc} and {@link idTokenAudienceEnv}.
|
|
19
21
|
*
|
|
20
22
|
* The Forgejo dialect (action refs, dropped `permissions:` and
|
|
21
23
|
* `environment:`, the header note naming what was dropped) is the forgejo
|
|
@@ -96,11 +98,66 @@ export function dirnameOf(path: string): string {
|
|
|
96
98
|
return i <= 0 ? "." : path.slice(0, i);
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Where a generated job runs (#3914): a runner label, a list of labels the
|
|
103
|
+
* runner must all carry, or on GitHub a runner group with optional labels.
|
|
104
|
+
* `runs-on` on GitHub and Forgejo, `tags` on GitLab.
|
|
105
|
+
*/
|
|
106
|
+
export type CiRunner = string | readonly string[] | { group: string; labels?: readonly string[] };
|
|
107
|
+
|
|
108
|
+
/** True when `runner` is a GitHub runner group, which only GitHub has. */
|
|
109
|
+
export function isRunnerGroup(runner: CiRunner | undefined): runner is { group: string; labels?: readonly string[] } {
|
|
110
|
+
return runner !== undefined && typeof runner === "object" && !Array.isArray(runner);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Refuse a runner group on a forge that has none (#3914). `subject` and
|
|
115
|
+
* `field` name whose runner it is, such as `Op waves "x"` and `wave "prod"
|
|
116
|
+
* runner`.
|
|
117
|
+
*/
|
|
118
|
+
export function assertRunnerLabels(subject: string, field: string, runner: CiRunner | undefined, forge: "GitLab" | "Forgejo"): void {
|
|
119
|
+
if (!isRunnerGroup(runner)) return;
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${subject}: ${field} is the runner group "${runner.group}", which only GitHub has. ` +
|
|
122
|
+
`${forge === "GitLab" ? "GitLab picks a runner by its tags" : "Forgejo picks a runner by its labels"}; give a label or a list of labels.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Each id token's audience as a variable, `<NAME>_AUDIENCE` (#3914). On
|
|
128
|
+
* GitHub and Forgejo the audience is chosen when the job requests the token,
|
|
129
|
+
* so the job reads it from there; GitLab declares it in `id_tokens:`
|
|
130
|
+
* ({@link gitlabIdTokensDoc}).
|
|
131
|
+
*/
|
|
132
|
+
export function idTokenAudienceEnv(idTokens: Readonly<Record<string, string>> | undefined): Record<string, string> {
|
|
133
|
+
return Object.fromEntries(Object.entries(idTokens ?? {}).map(([name, aud]) => [`${name}_AUDIENCE`, aud]));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A job's GitLab `id_tokens:` for `{ <variable>: <audience> }` (#3914), or undefined when there are none. */
|
|
137
|
+
export function gitlabIdTokensDoc(idTokens: Readonly<Record<string, string>> | undefined): Record<string, { aud: string }> | undefined {
|
|
138
|
+
const entries = Object.entries(idTokens ?? {});
|
|
139
|
+
return entries.length > 0 ? Object.fromEntries(entries.map(([name, aud]) => [name, { aud }])) : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A job's GitLab `tags:` for `runner` (#3914), or undefined when it names none. */
|
|
143
|
+
export function gitlabTags(runner: CiRunner | undefined): string[] | undefined {
|
|
144
|
+
if (runner === undefined || isRunnerGroup(runner)) return undefined;
|
|
145
|
+
return typeof runner === "string" ? [runner] : [...runner];
|
|
146
|
+
}
|
|
147
|
+
|
|
99
148
|
// ── GitHub Actions and Forgejo Actions ──────────────────────────────────────
|
|
100
149
|
|
|
101
150
|
/** The runner label a generated job asks for; the Forgejo dialect rewrites it to the project's label. */
|
|
102
151
|
export const GITHUB_RUNNER = "ubuntu-latest";
|
|
103
152
|
|
|
153
|
+
/** A job's `runs-on` for `runner` (#3914): {@link GITHUB_RUNNER} when it names none. */
|
|
154
|
+
export function githubRunsOn(runner: CiRunner | undefined): string | string[] | { group: string; labels?: string[] } {
|
|
155
|
+
if (runner === undefined) return GITHUB_RUNNER;
|
|
156
|
+
if (typeof runner === "string") return runner;
|
|
157
|
+
if (isRunnerGroup(runner)) return { group: runner.group, ...(runner.labels && runner.labels.length > 0 ? { labels: [...runner.labels] } : {}) };
|
|
158
|
+
return [...runner];
|
|
159
|
+
}
|
|
160
|
+
|
|
104
161
|
/** The job token, which GitHub and Forgejo both mint for every run. */
|
|
105
162
|
export const GITHUB_JOB_TOKEN = "${{ github.token }}";
|
|
106
163
|
|
|
@@ -32,6 +32,7 @@ import { loadPolicyChecks } from "../../lint/policy";
|
|
|
32
32
|
import { armSandboxPolicyExecution, runProjectPolicies } from "../../lint/policy-sandbox";
|
|
33
33
|
import { sortedJsonReplacer } from "../../utils";
|
|
34
34
|
import { rankFoldBlockers, toCollapsedFormat, type FoldRankResult } from "../../discovery/fold-rank";
|
|
35
|
+
import { foldExecutionCounts } from "../../discovery/fold-import";
|
|
35
36
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
36
37
|
import { writeFileSync, mkdirSync } from "fs";
|
|
37
38
|
import { resolve, dirname, join, relative } from "path";
|
|
@@ -396,6 +397,23 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
396
397
|
: (decision.reason ?? "fell back to run");
|
|
397
398
|
console.error(formatInfo(`[fold:${decision.mode}] ${rel} — ${detail}`));
|
|
398
399
|
}
|
|
400
|
+
// typescript-as-data#177 — the exact instrument beside the sampled one.
|
|
401
|
+
// `test/leftness` reports "0 MB of definition-library code on the path"
|
|
402
|
+
// from a `node --cpu-prof` recording, and a profile has a sampling
|
|
403
|
+
// floor: 0 MB is everything the profiler could see rather than nothing
|
|
404
|
+
// having run. L10.1's counters have no floor.
|
|
405
|
+
//
|
|
406
|
+
// Inside --verbose with the per-file lines, not beside them. The default
|
|
407
|
+
// path is deliberately quiet for the reason above, and the leftness
|
|
408
|
+
// capture already runs --verbose and greps this log for `[fold:fold]`.
|
|
409
|
+
const counts = foldExecutionCounts();
|
|
410
|
+
console.error(
|
|
411
|
+
formatInfo(
|
|
412
|
+
`[fold:counts] factoryInvocations=${counts.factoryInvocations} ` +
|
|
413
|
+
`projectFactoryInvocations=${counts.projectFactoryInvocations} ` +
|
|
414
|
+
`factoryInterpretations=${counts.factoryInterpretations}`,
|
|
415
|
+
),
|
|
416
|
+
);
|
|
399
417
|
} else {
|
|
400
418
|
console.error(formatInfo(summarizeFoldDecisions(result.foldDecisions)));
|
|
401
419
|
}
|
|
@@ -24,6 +24,7 @@ import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "..
|
|
|
24
24
|
import { PR_GATE_OP } from "../../pr-loop";
|
|
25
25
|
import { pushLifecycleReport, requireLifecycleLedger } from "../../lifecycle/git";
|
|
26
26
|
import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note";
|
|
27
|
+
import { ledgerRecordingOff, NO_RECORD_ENV } from "../../lifecycle/no-record";
|
|
27
28
|
import { formatError, formatWarning, formatSuccess, formatInfo } from "../format";
|
|
28
29
|
import type { CommandContext } from "../registry";
|
|
29
30
|
|
|
@@ -267,6 +268,14 @@ export async function recordGateApproval(
|
|
|
267
268
|
gate: string,
|
|
268
269
|
opts: GateApprovalOptions,
|
|
269
270
|
): Promise<GateApprovalOutcome> {
|
|
271
|
+
// #3955: with recording off the approval would be dropped with the process.
|
|
272
|
+
if (ledgerRecordingOff()) {
|
|
273
|
+
console.error(formatError({
|
|
274
|
+
message: `Cannot record the approval of "${gate}": ${NO_RECORD_ENV} is set, so nothing is written to chant/lifecycle`,
|
|
275
|
+
hint: `Unset ${NO_RECORD_ENV} to approve the gate.`,
|
|
276
|
+
}));
|
|
277
|
+
return { ok: false };
|
|
278
|
+
}
|
|
270
279
|
const { ops } = await discoverOps();
|
|
271
280
|
// `fan-out` is the op name every `chant components fan-out` gate is recorded
|
|
272
281
|
// under (../handlers/fan-out.ts). It is a command rather than a declaration,
|
|
@@ -42,7 +42,7 @@ vi.mock("node:child_process", async () => {
|
|
|
42
42
|
return { ...actual, exec: execWithCustom, execFile: execFileWithCustom };
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
const { runEmulator, formatEmulatorHelp, parsePortMappings } = await import("./emulator");
|
|
45
|
+
const { runEmulator, formatEmulatorHelp, parsePortMappings, parseUpOptions } = await import("./emulator");
|
|
46
46
|
|
|
47
47
|
function cap(): EmulatorCapability {
|
|
48
48
|
return {
|
|
@@ -286,3 +286,63 @@ describe("chant emulator --port and --help (#3673)", () => {
|
|
|
286
286
|
expect(formatEmulatorHelp([])).toContain("No lexicon configured for this project has a local emulator.");
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
describe("chant emulator up --image, --network, --setting, --loopback (#3952)", () => {
|
|
291
|
+
const floci = (): EmulatorCapability => ({ ...cap(), spec: { ...cap().spec, settingsPrefix: "FLOCI_" } });
|
|
292
|
+
const other = (): EmulatorCapability => ({
|
|
293
|
+
spec: { name: "chant-floci-az", image: "floci/floci-az:1.0.0", containerPort: 4577, healthPath: "/health" },
|
|
294
|
+
env: () => ({}),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
upMock.mockReset();
|
|
299
|
+
vi.restoreAllMocks();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("up passes each option to the lifecycle", async () => {
|
|
303
|
+
stderr();
|
|
304
|
+
upMock.mockResolvedValue({ endpoint: "http://localhost:4566" });
|
|
305
|
+
const exit = await runEmulator({
|
|
306
|
+
args: makeArgs({
|
|
307
|
+
path: "up", image: "floci/floci:1.6.0", network: "loom", loopback: true,
|
|
308
|
+
setting: ["FLOCI_STORAGE_MODE=memory", "FLOCI_X=a=b"], port: ["4567"],
|
|
309
|
+
}),
|
|
310
|
+
plugins: [plugin("aws", floci())],
|
|
311
|
+
serializers: [],
|
|
312
|
+
});
|
|
313
|
+
expect(exit).toBe(0);
|
|
314
|
+
expect(upMock).toHaveBeenCalledWith({
|
|
315
|
+
image: "floci/floci:1.6.0", network: "loom", loopback: true,
|
|
316
|
+
settings: { FLOCI_STORAGE_MODE: "memory", FLOCI_X: "a=b" }, port: 4567,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("a setting no selected emulator takes is refused before anything starts", async () => {
|
|
321
|
+
const err = stderr();
|
|
322
|
+
const exit = await runEmulator({
|
|
323
|
+
args: makeArgs({ path: "up", setting: ["HOME=/tmp"] }),
|
|
324
|
+
plugins: [plugin("aws", floci())],
|
|
325
|
+
serializers: [],
|
|
326
|
+
});
|
|
327
|
+
expect(exit).toBe(1);
|
|
328
|
+
expect(upMock).not.toHaveBeenCalled();
|
|
329
|
+
expect(err.join("\n")).toContain("--setting HOME: no selected emulator takes it; the selected ones take FLOCI_* (chant-floci)");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("settings go only to the emulator whose prefix matches; --image needs one emulator", () => {
|
|
333
|
+
const a = floci();
|
|
334
|
+
const b = other();
|
|
335
|
+
const opts = parseUpOptions({ setting: ["FLOCI_A=1"], network: "n" }, [a, b]);
|
|
336
|
+
expect(opts.get(a)).toEqual({ network: "n", settings: { FLOCI_A: "1" } });
|
|
337
|
+
expect(opts.get(b)).toEqual({ network: "n" });
|
|
338
|
+
expect(() => parseUpOptions({ image: "x:1" }, [a, b])).toThrow(/--image x:1 is ambiguous with 2 emulators/);
|
|
339
|
+
expect(() => parseUpOptions({ setting: ["FLOCI_A"] }, [a])).toThrow(/expected <KEY>=<VALUE>/);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("the options apply to up only", async () => {
|
|
343
|
+
const err = stderr();
|
|
344
|
+
const exit = await runEmulator({ args: makeArgs({ path: "status", loopback: true }), plugins: [plugin("aws", floci())], serializers: [] });
|
|
345
|
+
expect(exit).toBe(1);
|
|
346
|
+
expect(err.join("\n")).toContain("--loopback applies to `chant emulator up` only");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { exec, execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { emulatorLifecycle, emulatorsOf, type EmulatorCapability } from "../../op";
|
|
3
|
+
import { checkSettings, emulatorLifecycle, emulatorsOf, type EmulatorCapability, type EmulatorUpArgs } from "../../op";
|
|
4
4
|
import type { LexiconPlugin } from "../../lexicon";
|
|
5
5
|
import { formatError, formatWarning, formatSuccess } from "../format";
|
|
6
|
-
import type { CommandContext } from "../registry";
|
|
6
|
+
import type { CommandContext, ParsedArgs } from "../registry";
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -116,13 +116,60 @@ export function parsePortMappings(values: readonly string[], targets: readonly E
|
|
|
116
116
|
return ports;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** The `up` options other than the port (#3952), per selected emulator. */
|
|
120
|
+
type UpOptions = Pick<EmulatorUpArgs, "image" | "network" | "settings" | "loopback">;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `--image`, `--network`, `--setting` and `--loopback` as each selected
|
|
124
|
+
* emulator's `up` options (#3952). `--image` needs exactly one emulator
|
|
125
|
+
* selected, the way a bare `--port` does. A `--setting` goes to each emulator
|
|
126
|
+
* whose settings prefix its key starts with, and a key that no selected
|
|
127
|
+
* emulator takes is refused rather than passed to a container as plain env.
|
|
128
|
+
*/
|
|
129
|
+
export function parseUpOptions(
|
|
130
|
+
args: Pick<ParsedArgs, "image" | "network" | "setting" | "loopback">,
|
|
131
|
+
targets: readonly EmulatorCapability[],
|
|
132
|
+
): Map<EmulatorCapability, UpOptions> {
|
|
133
|
+
if (args.image !== undefined && targets.length !== 1) {
|
|
134
|
+
throw new Error(`--image ${args.image} is ambiguous with ${targets.length} emulators selected; pick one with --lexicon <name>`);
|
|
135
|
+
}
|
|
136
|
+
const settings = new Map<EmulatorCapability, Record<string, string>>();
|
|
137
|
+
for (const value of args.setting ?? []) {
|
|
138
|
+
const match = /^([^=]+)=(.*)$/s.exec(value);
|
|
139
|
+
if (!match) throw new Error(`--setting ${value}: expected <KEY>=<VALUE>, e.g. --setting FLOCI_<NAME>=<value>`);
|
|
140
|
+
const [, key, val] = match as unknown as [string, string, string];
|
|
141
|
+
const takers = targets.filter((t) => t.spec.settingsPrefix && key.startsWith(t.spec.settingsPrefix));
|
|
142
|
+
if (takers.length === 0) {
|
|
143
|
+
const prefixes = targets.filter((t) => t.spec.settingsPrefix).map((t) => `${t.spec.settingsPrefix}* (${t.spec.name})`);
|
|
144
|
+
throw new Error(`--setting ${key}: no selected emulator takes it; ${prefixes.length ? `the selected ones take ${prefixes.join(", ")}` : "the selected ones take no settings"}`);
|
|
145
|
+
}
|
|
146
|
+
for (const t of takers) {
|
|
147
|
+
const own = { ...settings.get(t), [key]: val };
|
|
148
|
+
checkSettings(t.spec, own);
|
|
149
|
+
settings.set(t, own);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const out = new Map<EmulatorCapability, UpOptions>();
|
|
153
|
+
for (const t of targets) {
|
|
154
|
+
const opts: UpOptions = {};
|
|
155
|
+
if (args.image !== undefined) opts.image = args.image;
|
|
156
|
+
if (args.network !== undefined) opts.network = args.network;
|
|
157
|
+
if (args.loopback) opts.loopback = true;
|
|
158
|
+
const own = settings.get(t);
|
|
159
|
+
if (own) opts.settings = own;
|
|
160
|
+
out.set(t, opts);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
/**
|
|
120
166
|
* `chant emulator --help` (#3673): the subcommands and flags, and for each
|
|
121
167
|
* configured lexicon's emulator the port, endpoint and sign-in a client uses.
|
|
122
168
|
*/
|
|
123
169
|
export function formatEmulatorHelp(plugins: readonly LexiconPlugin[]): string {
|
|
124
170
|
const lines = [
|
|
125
|
-
"Usage: chant emulator <up|down|status> [--lexicon <name>] [--port <container-port>=<host-port>] [--
|
|
171
|
+
"Usage: chant emulator <up|down|status> [--lexicon <name>] [--port <container-port>=<host-port>] [--image <ref>]",
|
|
172
|
+
" [--network <name>] [--setting <KEY>=<VALUE>] [--loopback] [--json]",
|
|
126
173
|
"",
|
|
127
174
|
"Boot, stop or inspect the local emulators of the project's configured lexicons.",
|
|
128
175
|
"",
|
|
@@ -137,6 +184,11 @@ export function formatEmulatorHelp(plugins: readonly LexiconPlugin[]): string {
|
|
|
137
184
|
" --lexicon <name> Act on one lexicon's emulators only.",
|
|
138
185
|
" --port <c>=<h> up: publish container port <c> on host port <h>; repeatable.",
|
|
139
186
|
" A bare <h> works when one emulator is selected.",
|
|
187
|
+
" --image <ref> up: start this image instead of the pinned default; one emulator only.",
|
|
188
|
+
" --network <name> up: join each started container to this Docker network.",
|
|
189
|
+
" --setting <K>=<V> up: pass an emulator setting as -e K=V; repeatable. The key must",
|
|
190
|
+
" start with the emulator's prefix (FLOCI_ for aws).",
|
|
191
|
+
" --loopback up: publish ports on 127.0.0.1 only, not on every interface.",
|
|
140
192
|
" --json Print the machine-readable report on stdout.",
|
|
141
193
|
"",
|
|
142
194
|
];
|
|
@@ -195,9 +247,18 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
195
247
|
}
|
|
196
248
|
|
|
197
249
|
let ports: Map<number, number>;
|
|
250
|
+
let upOptions: Map<EmulatorCapability, UpOptions>;
|
|
198
251
|
try {
|
|
199
252
|
if (args.port?.length && action !== "up") throw new Error("--port applies to `chant emulator up` only");
|
|
253
|
+
const upOnly = [
|
|
254
|
+
args.image !== undefined && "--image",
|
|
255
|
+
args.network !== undefined && "--network",
|
|
256
|
+
args.setting?.length && "--setting",
|
|
257
|
+
args.loopback && "--loopback",
|
|
258
|
+
].filter(Boolean);
|
|
259
|
+
if (upOnly.length && action !== "up") throw new Error(`${upOnly.join(", ")} applies to \`chant emulator up\` only`);
|
|
200
260
|
ports = parsePortMappings(args.port ?? [], targets.map((t) => t.cap));
|
|
261
|
+
upOptions = parseUpOptions(args, targets.map((t) => t.cap));
|
|
201
262
|
} catch (err) {
|
|
202
263
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
203
264
|
return 1;
|
|
@@ -209,7 +270,7 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
209
270
|
if (action === "up") {
|
|
210
271
|
const port = ports.get(cap.spec.containerPort);
|
|
211
272
|
try {
|
|
212
|
-
const { endpoint } = await lc.up(port === undefined ? {} : { port });
|
|
273
|
+
const { endpoint } = await lc.up({ ...upOptions.get(cap), ...(port === undefined ? {} : { port }) });
|
|
213
274
|
reports.push({ lexicon, name: cap.spec.name, endpoint, env: cap.env(endpoint) });
|
|
214
275
|
} catch (err) {
|
|
215
276
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
@@ -2,7 +2,7 @@ import { dirname, resolve } from "node:path";
|
|
|
2
2
|
import { discoverOps } from "../../op/discover";
|
|
3
3
|
import { discover } from "../../discovery/index";
|
|
4
4
|
import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
|
|
5
|
-
import { applyLexiconGraphEdges, buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LexiconGraphEdges, type LiveObservation } from "../../graph-ir";
|
|
5
|
+
import { applyLexiconGraphEdges, applyLexiconOutputs, buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LexiconGraphEdges, type LexiconWorkspaceOutputs, type LiveObservation } from "../../graph-ir";
|
|
6
6
|
import { behaviourOverlay } from "../../behaviour-overlay";
|
|
7
7
|
import type { SerializerResult } from "../../serializer";
|
|
8
8
|
import { applyBehaviourOverlay, behaviourRequestFromIr } from "../../behaviour-graph";
|
|
@@ -746,6 +746,26 @@ function withLexiconEdges(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<s
|
|
|
746
746
|
return reports.length === 0 ? ir : applyLexiconGraphEdges(ir, reports);
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Add the outputs the lexicons' `workspaceOutputs` hooks report (#3931) to
|
|
751
|
+
* the IR's `exports`: a sql table and each of its columns, which `chant
|
|
752
|
+
* workspace graph` resolves member links against. A hook that throws costs
|
|
753
|
+
* its own outputs and a warning, not the graph.
|
|
754
|
+
*/
|
|
755
|
+
function withLexiconOutputs(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<string, import("../../declarable").Declarable>): GraphIR {
|
|
756
|
+
const reports: LexiconWorkspaceOutputs[] = [];
|
|
757
|
+
for (const p of plugins) {
|
|
758
|
+
if (!p.workspaceOutputs) continue;
|
|
759
|
+
try {
|
|
760
|
+
const report = p.workspaceOutputs(entities);
|
|
761
|
+
if (report) reports.push(report);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.error(formatWarning({ message: `the ${p.name} lexicon's workspaceOutputs failed (${err instanceof Error ? err.message : String(err)}); its outputs are left out` }));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return reports.length === 0 ? ir : applyLexiconOutputs(ir, reports);
|
|
767
|
+
}
|
|
768
|
+
|
|
749
769
|
/** Returned instead of a graph when the prediction cannot be attempted honestly. */
|
|
750
770
|
const REFUSED = Symbol("behaviour-refused");
|
|
751
771
|
|
|
@@ -888,6 +908,7 @@ async function runGraphView(
|
|
|
888
908
|
? ctx.plugins
|
|
889
909
|
: await loadPlugins(await projectLexiconsOrNone(projectPath));
|
|
890
910
|
ir = withLexiconEdges(ir, plugins as LexiconPlugin[], result.entities);
|
|
911
|
+
ir = withLexiconOutputs(ir, plugins as LexiconPlugin[], result.entities);
|
|
891
912
|
const predicted = await predictOntoIr(ir, plugins as LexiconPlugin[], ctx.args, {
|
|
892
913
|
// No environment on the declared path: the file is not deployed anywhere
|
|
893
914
|
// yet, and naming one would claim it was.
|
|
@@ -39,6 +39,7 @@ vi.mock("../../config", async () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
vi.mock("../../components/cli-support", () => ({
|
|
42
|
+
isComponentList: (selector: string) => selector.includes(","),
|
|
42
43
|
resolveComponentTargets: (...args: unknown[]) => resolveTargetsMock(...args),
|
|
43
44
|
}));
|
|
44
45
|
|
|
@@ -286,6 +287,12 @@ describe("chant components rollback", () => {
|
|
|
286
287
|
expect(appendReleaseRecordMock.mock.calls[0][0]).not.toHaveProperty("promotedFrom");
|
|
287
288
|
});
|
|
288
289
|
|
|
290
|
+
test("a comma list is refused: rollback acts on one component (#3950)", async () => {
|
|
291
|
+
expect(await runComponentsRollback(rb({ component: "api,web" }))).toBe(1);
|
|
292
|
+
expect(errors.join("\n")).toMatch(/--component takes one component name, not a list/);
|
|
293
|
+
expect(ran).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
289
296
|
test("a digest the environment never recorded fails clearly", async () => {
|
|
290
297
|
expect(await runComponentsRollback(rb({ digest: "sha256:elsewhere" }))).toBe(1);
|
|
291
298
|
expect(errors.join("\n")).toMatch(/sha256:elsewhere is not recorded for "api" in "prod"/);
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type ReleaseRecord,
|
|
26
26
|
type ReleaseRecordInput,
|
|
27
27
|
} from "../../lifecycle/release-ledger";
|
|
28
|
-
import { resolveComponentTargets } from "../../components/cli-support";
|
|
28
|
+
import { isComponentList, resolveComponentTargets } from "../../components/cli-support";
|
|
29
29
|
import { applyConfigDefaults } from "../../components/config-defaults";
|
|
30
30
|
import { fanOutRegistry } from "../../components/fan-out-support";
|
|
31
31
|
import { renderDriverHuman } from "../../components/driver-output";
|
|
@@ -107,6 +107,12 @@ async function loadProject(ctx: CommandContext, selector: string) {
|
|
|
107
107
|
for (const message of paramsResolution.errors) console.error(message);
|
|
108
108
|
return 1;
|
|
109
109
|
}
|
|
110
|
+
// A comma list is `chant run --components`' selector (#3950), which pulls in
|
|
111
|
+
// dependencies; promote, rollback and redeploy act on the one named component.
|
|
112
|
+
if (isComponentList(selector)) {
|
|
113
|
+
console.error(formatError({ message: `--component takes one component name, not a list ("${selector}")` }));
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
110
116
|
const targets = await resolveComponentTargets(projectPath, selector, args.sandbox, paramsResolution.provenance);
|
|
111
117
|
if (!targets.success) {
|
|
112
118
|
console.error(formatError({ message: targets.error ?? "Could not discover components" }));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3955 — `chant run <op> --no-record` through the real CLI: the run executes,
|
|
3
|
+
* says at its start and end that it recorded nothing, and leaves
|
|
4
|
+
* `chant/lifecycle` where it was, both in the checkout and on origin. A plain
|
|
5
|
+
* run beside it records, so the unchanged ref is the flag's doing.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
11
|
+
import { cleanScratch, commitAll, git, repo, REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
12
|
+
import { pushLifecycleStatus, writeBlobToPath } from "../../lifecycle/git";
|
|
13
|
+
|
|
14
|
+
const MAIN = join(REPO, "packages/core/src/cli/main.ts");
|
|
15
|
+
const LOADER = pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href;
|
|
16
|
+
|
|
17
|
+
/** An Op entity as `Op()` exports it, written as a literal so the fixture needs no import to resolve. */
|
|
18
|
+
function opModule(name: string, steps: unknown[]): string {
|
|
19
|
+
return `export const op = ${JSON.stringify({ entityType: "Chant::Op", props: { name, overview: name, phases: [{ name: "Main", steps }] } })};\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SHELL = { kind: "activity", fn: "shellCmd", profile: "atMostOnce", args: { cmd: "echo ran" } };
|
|
23
|
+
|
|
24
|
+
let dir: string;
|
|
25
|
+
let remote: string;
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
remote = repo({}, false);
|
|
29
|
+
git(remote, "config", "core.bare", "true");
|
|
30
|
+
dir = repo(
|
|
31
|
+
{
|
|
32
|
+
"chant.config.ts": "export default { lexicons: [] };\n",
|
|
33
|
+
"ops/hello.op.ts": opModule("hello", [SHELL]),
|
|
34
|
+
"ops/gated.op.ts": opModule("gated", [{ kind: "gate", gate: "approve-prod" }, SHELL]),
|
|
35
|
+
},
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
git(dir, "config", "user.name", "Run Test");
|
|
39
|
+
git(dir, "config", "user.email", "run@example.com");
|
|
40
|
+
commitAll(dir, "c0");
|
|
41
|
+
git(dir, "remote", "add", "origin", join(remote, ".git"));
|
|
42
|
+
await writeBlobToPath("seed", "seed.json", "{}\n", "seed", { cwd: dir });
|
|
43
|
+
expect((await pushLifecycleStatus({ cwd: dir })).status).toBe("pushed");
|
|
44
|
+
});
|
|
45
|
+
afterAll(cleanScratch);
|
|
46
|
+
|
|
47
|
+
function chant(args: string[], env: Record<string, string> = {}): { status: number | null; stderr: string } {
|
|
48
|
+
const r = spawnSync(process.execPath, ["--import", LOADER, MAIN, ...args], {
|
|
49
|
+
cwd: dir,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
env: { ...process.env, NO_COLOR: "1", TSX_DISABLE_CACHE: "1", CHANT_NO_RECORD: "", ...env },
|
|
52
|
+
});
|
|
53
|
+
return { status: r.status, stderr: r.stderr };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ledgerRefs(): { local: string; origin: string } {
|
|
57
|
+
return { local: git(dir, "rev-parse", "refs/heads/chant/lifecycle"), origin: git(dir, "ls-remote", "origin", "refs/heads/chant/lifecycle").split(/\s/)[0]! };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("chant run --no-record (#3955)", () => {
|
|
61
|
+
test("runs the Op, says so at the start and the end, and leaves the ledger ref unchanged here and on origin", () => {
|
|
62
|
+
const before = ledgerRefs();
|
|
63
|
+
const r = chant(["run", "hello", "--no-record"]);
|
|
64
|
+
expect(r.status, r.stderr).toBe(0);
|
|
65
|
+
expect(r.stderr).toContain("not recording (--no-record): nothing this run writes goes on chant/lifecycle");
|
|
66
|
+
expect(r.stderr).toMatch(/not recorded \(--no-record\): nothing went on chant\/lifecycle and nothing was pushed; 1 ledger write was dropped \(1 file\)/);
|
|
67
|
+
expect(ledgerRefs()).toEqual(before);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("CHANT_NO_RECORD=1 fails a gate without writing a pending fact", () => {
|
|
71
|
+
const before = ledgerRefs();
|
|
72
|
+
const r = chant(["run", "gated"], { CHANT_NO_RECORD: "1" });
|
|
73
|
+
expect(r.status).toBe(1);
|
|
74
|
+
expect(r.stderr).toContain('gate "approve-prod" of "gated" cannot be passed under --no-record');
|
|
75
|
+
expect(r.stderr).toContain("not recorded (CHANT_NO_RECORD=1)");
|
|
76
|
+
expect(ledgerRefs()).toEqual(before);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a plain run of the same Op records its run", () => {
|
|
80
|
+
const before = ledgerRefs();
|
|
81
|
+
const r = chant(["run", "hello"]);
|
|
82
|
+
expect(r.status, r.stderr).toBe(0);
|
|
83
|
+
expect(r.stderr).not.toContain("not recording");
|
|
84
|
+
// The run record moves the local branch; it is pushed by the next write that pushes.
|
|
85
|
+
expect(ledgerRefs().local).not.toBe(before.local);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -29,16 +29,16 @@ export async function runOpWaveCommand(ctx: CommandContext): Promise<number> {
|
|
|
29
29
|
if (!specFile || args.wave === undefined) {
|
|
30
30
|
console.error(formatError({
|
|
31
31
|
message: "`chant run wave` needs --spec <file> and --wave <k>",
|
|
32
|
-
hint: "Run it as a generated Op waves pipeline does: chant run wave --spec <file> --wave <k> [--decide | --share <i>]",
|
|
32
|
+
hint: "Run it as a generated Op waves pipeline does: chant run wave --spec <file> --wave <k> [--decide | --share <i> | --group <name> [--share <i>]]",
|
|
33
33
|
}));
|
|
34
34
|
return 1;
|
|
35
35
|
}
|
|
36
|
-
if (args.decide && args.share !== undefined) {
|
|
37
|
-
console.error(formatError({ message:
|
|
36
|
+
if (args.decide && (args.share !== undefined || args.group !== undefined)) {
|
|
37
|
+
console.error(formatError({ message: `--decide and --${args.share !== undefined ? "share" : "group"} are two different jobs; pass one` }));
|
|
38
38
|
return 1;
|
|
39
39
|
}
|
|
40
40
|
let spec: OpWavesSpec | undefined;
|
|
41
|
-
const job = args.share !== undefined ? { share: args.share } : {};
|
|
41
|
+
const job = { ...(args.share !== undefined ? { share: args.share } : {}), ...(args.group !== undefined ? { group: args.group } : {}) };
|
|
42
42
|
try {
|
|
43
43
|
const path = resolve(specFile);
|
|
44
44
|
spec = parseOpWavesSpec(readFileSync(path, "utf-8"), `--spec ${specFile}`);
|
|
@@ -48,6 +48,7 @@ export async function runOpWaveCommand(ctx: CommandContext): Promise<number> {
|
|
|
48
48
|
wave: args.wave,
|
|
49
49
|
...(args.decide ? { decide: true } : {}),
|
|
50
50
|
...(args.share !== undefined ? { share: args.share } : {}),
|
|
51
|
+
...(args.group !== undefined ? { group: args.group } : {}),
|
|
51
52
|
...(args.base ? { base: args.base } : {}),
|
|
52
53
|
});
|
|
53
54
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|