@intentius/chant 0.124.15 → 0.124.16
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/cli/flush.d.ts +16 -0
- package/dist/cli/flush.d.ts.map +1 -0
- package/dist/cli/flush.js +42 -0
- package/dist/cli/handlers/approve.d.ts +98 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -0
- package/dist/cli/handlers/approve.js +495 -0
- package/dist/cli/handlers/operator.d.ts +1 -95
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/operator.js +4 -488
- package/dist/cli/handlers/override-approve.js +1 -1
- package/dist/cli/main.d.ts +4 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +4 -1062
- package/dist/cli/parse-args.d.ts +6 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +1028 -0
- package/dist/components/fan-out-record.d.ts +1 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +3 -7
- package/dist/components/fan-out-run.d.ts +5 -3
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +7 -5
- package/dist/gated-waves.d.ts +25 -6
- package/dist/gated-waves.d.ts.map +1 -1
- package/dist/gated-waves.js +21 -6
- package/dist/op/activities/activity-contracts.d.ts +4 -4
- package/dist/op/op-waves-run.d.ts +1 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +5 -6
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +4 -8
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +10 -0
- package/dist/wave-engine.d.ts +62 -0
- package/dist/wave-engine.d.ts.map +1 -0
- package/dist/wave-engine.js +59 -0
- package/dist/wave-layout.d.ts +29 -0
- package/dist/wave-layout.d.ts.map +1 -0
- package/dist/wave-layout.js +41 -0
- package/dist/wave-runtime.d.ts +29 -0
- package/dist/wave-runtime.d.ts.map +1 -0
- package/dist/wave-runtime.js +103 -0
- package/dist/wave-runtime.mjs +331 -0
- package/dist/workspace/ajv2020.d.ts +5 -0
- package/dist/workspace/ajv2020.d.ts.map +1 -0
- package/dist/workspace/ajv2020.js +14 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +4 -8
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +2 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/points.js +2 -5
- package/dist/workspace/principal-classes.d.ts.map +1 -1
- package/dist/workspace/principal-classes.js +2 -5
- package/package.json +5 -3
- package/src/cli/flush.ts +44 -0
- package/src/cli/handlers/approve.ts +618 -0
- package/src/cli/handlers/operator.ts +10 -608
- package/src/cli/handlers/override-approve.test.ts +1 -1
- package/src/cli/handlers/override-approve.ts +1 -1
- package/src/cli/main.ts +5 -816
- package/src/cli/parse-args.ts +781 -0
- package/src/components/fan-out-record.ts +3 -7
- package/src/components/fan-out-run.ts +7 -5
- package/src/gate-decision-bundle.test.ts +1 -0
- package/src/gated-waves.ts +28 -6
- package/src/op/op-waves-run.ts +5 -6
- package/src/op/op-waves.ts +4 -8
- package/src/package-version.ts +11 -0
- package/src/wave-engine.test.ts +107 -0
- package/src/wave-engine.ts +98 -0
- package/src/wave-layout.ts +44 -0
- package/src/wave-runtime-bundle.test.ts +70 -0
- package/src/wave-runtime.e2e.test.ts +177 -0
- package/src/wave-runtime.ts +104 -0
- package/src/workspace/ajv2020.ts +17 -0
- package/src/workspace/declaration.ts +4 -10
- package/src/workspace/kinds.ts +2 -5
- package/src/workspace/points.ts +2 -5
- package/src/workspace/principal-classes.ts +2 -5
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wave-job runtime runs alone (#3896). `dist/wave-runtime.mjs` is copied
|
|
3
|
+
* into an empty directory, the way a product's CI image carries it, and run
|
|
4
|
+
* with plain node, nothing installed beside it, through a gated wave's whole
|
|
5
|
+
* life: `run wave` waits at a ledger gate, `approve` records the resolution,
|
|
6
|
+
* `run resume --dry-run` finds it, `run wave` applies; then a wave whose gate
|
|
7
|
+
* asks a decision point waits, `workspace points` lists the open question,
|
|
8
|
+
* `workspace points answer` answers it, and `run wave` applies.
|
|
9
|
+
*
|
|
10
|
+
* End to end because it builds the bundle and spawns node for each command.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
14
|
+
import { copyFileSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
19
|
+
// @ts-expect-error: a plain .mjs script with no type declarations
|
|
20
|
+
import { buildWaveRuntime } from "../scripts/build-wave-runtime.mjs";
|
|
21
|
+
import { REPO } from "./workspace/__fixtures__/decide-workspace";
|
|
22
|
+
|
|
23
|
+
const POINTS = {
|
|
24
|
+
points: {
|
|
25
|
+
"prod-apply": {
|
|
26
|
+
title: "Apply this plan to prod",
|
|
27
|
+
question: { type: "noul", instructions: "Should this plan be applied to prod?", criteria: { true: "Apply it.", false: "Do not apply it." } },
|
|
28
|
+
inputs: { "gate.component": "the Op the gate belongs to", "gate.planDigest": "the plan the gate binds" },
|
|
29
|
+
deciders: [{ kind: "quorum", count: 1 }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SPEC = {
|
|
35
|
+
name: "smoke",
|
|
36
|
+
op: "smoke",
|
|
37
|
+
plan: ["node", "plan.mjs", "{target}", "{plan}"],
|
|
38
|
+
apply: ["node", "apply.mjs", "{target}", "{plan}"],
|
|
39
|
+
waves: [
|
|
40
|
+
{ name: "dev", runs: [{ target: "dev" }], gate: "always" },
|
|
41
|
+
{ name: "prod", runs: [{ target: "prod" }], gate: "always", point: "prod-apply" },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let scratch: string;
|
|
46
|
+
let runtimeDir: string;
|
|
47
|
+
let runtime: string;
|
|
48
|
+
let repo: string;
|
|
49
|
+
|
|
50
|
+
/** The job's environment: no CI variables, so nothing names a CI job to resume. */
|
|
51
|
+
function jobEnv(): NodeJS.ProcessEnv {
|
|
52
|
+
const env: NodeJS.ProcessEnv = {};
|
|
53
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
54
|
+
if (/^(CI|GITHUB_|GITLAB_|FORGEJO_|GITEA_|CHANT_|NODE_OPTIONS$|VITEST)/.test(k)) continue;
|
|
55
|
+
env[k] = v;
|
|
56
|
+
}
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function chant(...args: string[]): { status: number | null; stdout: string; stderr: string } {
|
|
61
|
+
const r = spawnSync(process.execPath, [runtime, ...args], { cwd: repo, env: jobEnv(), encoding: "utf-8" });
|
|
62
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const git = (...args: string[]) =>
|
|
66
|
+
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: repo, stdio: "ignore" });
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
scratch = realpathSync(mkdtempSync(join(tmpdir(), "chant-wave-runtime-")));
|
|
70
|
+
const built = join(scratch, "build", "wave-runtime.mjs");
|
|
71
|
+
mkdirSync(dirname(built), { recursive: true });
|
|
72
|
+
await buildWaveRuntime({ outfile: built });
|
|
73
|
+
runtimeDir = join(scratch, "image");
|
|
74
|
+
mkdirSync(runtimeDir);
|
|
75
|
+
runtime = join(runtimeDir, "wave-runtime.mjs");
|
|
76
|
+
copyFileSync(built, runtime);
|
|
77
|
+
|
|
78
|
+
repo = join(scratch, "repo");
|
|
79
|
+
const files: Record<string, string> = {
|
|
80
|
+
"chant.workspace.json": JSON.stringify({
|
|
81
|
+
name: "wave-runtime",
|
|
82
|
+
schema: 1,
|
|
83
|
+
members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
|
|
84
|
+
records: [{ kind: "answers/answer.kind.mjs" }],
|
|
85
|
+
}),
|
|
86
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
87
|
+
"decisions/points.json": JSON.stringify(POINTS),
|
|
88
|
+
"plan.mjs": `import { writeFileSync } from "node:fs";\nwriteFileSync(process.argv[3], JSON.stringify({ planDigest: "sha256:" + (process.argv[2] === "dev" ? "a" : "b").repeat(64) }));\n`,
|
|
89
|
+
"apply.mjs": `import { appendFileSync } from "node:fs";\nappendFileSync("applied.log", process.argv[2] + "\\n");\n`,
|
|
90
|
+
"waves.json": JSON.stringify(SPEC),
|
|
91
|
+
};
|
|
92
|
+
for (const [path, text] of Object.entries(files)) {
|
|
93
|
+
mkdirSync(dirname(join(repo, path)), { recursive: true });
|
|
94
|
+
writeFileSync(join(repo, path), text);
|
|
95
|
+
}
|
|
96
|
+
cpSync(join(REPO, "reference-workspace", "answers", "answer.kind.mjs"), join(repo, "answers", "answer.kind.mjs"));
|
|
97
|
+
cpSync(join(REPO, "reference-workspace", "answers", "answer.schema.json"), join(repo, "answers", "answer.schema.json"));
|
|
98
|
+
git("init", "-q", "-b", "main");
|
|
99
|
+
git("add", "-A");
|
|
100
|
+
git("commit", "-qm", "spec");
|
|
101
|
+
// The gate policy is read at HEAD^1, so the spec needs a commit under the head.
|
|
102
|
+
writeFileSync(join(repo, ".gitignore"), ".chant/\napplied.log\n");
|
|
103
|
+
git("add", "-A");
|
|
104
|
+
git("commit", "-qm", "ignore");
|
|
105
|
+
}, 120_000);
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("the wave-job runtime, alone in a directory (#3896)", () => {
|
|
112
|
+
test("is one file, with no package beside it to resolve", () => {
|
|
113
|
+
expect(readdirSync(runtimeDir)).toEqual(["wave-runtime.mjs"]);
|
|
114
|
+
const resolveBeside = createRequire(runtime).resolve;
|
|
115
|
+
for (const pkg of ["@intentius/chant", "typescript", "esbuild", "tsx", "zod", "ajv"]) {
|
|
116
|
+
expect(() => resolveBeside(pkg), pkg).toThrow();
|
|
117
|
+
}
|
|
118
|
+
const version = (JSON.parse(readFileSync(join(REPO, "packages", "core", "package.json"), "utf-8")) as { version: string }).version;
|
|
119
|
+
expect(chant("--version").stdout.trim()).toBe(version);
|
|
120
|
+
const other = chant("build");
|
|
121
|
+
expect(other.status).toBe(1);
|
|
122
|
+
expect(other.stderr).toContain("is not part of chant's wave-job runtime");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a ledger gate: run wave waits, approve, run resume --dry-run, run wave applies", () => {
|
|
126
|
+
const waiting = chant("run", "wave", "--spec", "waves.json", "--wave", "1", "--json");
|
|
127
|
+
expect(waiting.status, waiting.stderr).toBe(3);
|
|
128
|
+
const decision = (JSON.parse(waiting.stdout) as { decision: { status: string; gate: string; digest: string } }).decision;
|
|
129
|
+
expect(decision).toMatchObject({ status: "waiting", gate: "smoke-wave-1" });
|
|
130
|
+
|
|
131
|
+
const approved = chant("approve", "smoke", "smoke-wave-1", "--actor", "alice");
|
|
132
|
+
expect(approved.status, approved.stderr).toBe(0);
|
|
133
|
+
expect(approved.stderr).toContain(`Gate "smoke-wave-1" on "smoke" resolved by alice`);
|
|
134
|
+
expect(approved.stderr).toContain(`This approves the plan ${decision.digest}`);
|
|
135
|
+
|
|
136
|
+
const resume = chant("run", "resume", "--dry-run");
|
|
137
|
+
expect(resume.status, resume.stderr).toBe(0);
|
|
138
|
+
expect(resume.stderr).toContain("smoke / smoke-wave-1, approved by alice");
|
|
139
|
+
|
|
140
|
+
const applied = chant("run", "wave", "--spec", "waves.json", "--wave", "1");
|
|
141
|
+
expect(applied.status, applied.stderr).toBe(0);
|
|
142
|
+
expect(applied.stderr).toContain("approved by alice");
|
|
143
|
+
expect(readFileSync(join(repo, "applied.log"), "utf-8")).toBe("dev\n");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a gate that asks a decision point: run wave waits, points lists and answers, run wave applies", () => {
|
|
147
|
+
const waiting = chant("run", "wave", "--spec", "waves.json", "--wave", "2", "--json");
|
|
148
|
+
expect(waiting.status, waiting.stderr).toBe(3);
|
|
149
|
+
const id = (JSON.parse(waiting.stdout) as { decision: { point: { question: { id: string } } } }).decision.point.question.id;
|
|
150
|
+
expect(id).toMatch(/^prod-apply-[0-9a-f]{12}$/);
|
|
151
|
+
|
|
152
|
+
const open = chant("workspace", "points", "--open");
|
|
153
|
+
expect(open.status, open.stderr).toBe(0);
|
|
154
|
+
expect(open.stdout).toContain(`${id} escalated`);
|
|
155
|
+
|
|
156
|
+
const answered = chant("workspace", "points", "answer", id, "--answer", "true", "--by", "alice");
|
|
157
|
+
expect(answered.status, answered.stderr).toBe(0);
|
|
158
|
+
expect(readFileSync(join(repo, "answers", `${id}.md`), "utf-8")).toMatch(/state: "answered"\nanswer: true/);
|
|
159
|
+
|
|
160
|
+
const applied = chant("run", "wave", "--spec", "waves.json", "--wave", "2");
|
|
161
|
+
expect(applied.status, applied.stderr).toBe(0);
|
|
162
|
+
expect(readFileSync(join(repo, "applied.log"), "utf-8")).toBe("dev\nprod\n");
|
|
163
|
+
|
|
164
|
+
// The ledger the runtime wrote is the full CLI's: pending, resolution, pending citing the question, resolution citing the answer.
|
|
165
|
+
const ledger = execFileSync("git", ["show", "chant/lifecycle:_gates/smoke.jsonl"], { cwd: repo, encoding: "utf-8" })
|
|
166
|
+
.trim()
|
|
167
|
+
.split("\n")
|
|
168
|
+
.map((l) => JSON.parse(l) as { kind?: string; gate: string; resolvedBy?: string; answer?: { id: string } });
|
|
169
|
+
expect(ledger.map((l) => [l.gate, l.kind ?? "resolution", l.resolvedBy ?? null])).toEqual([
|
|
170
|
+
["smoke-wave-1", "pending", null],
|
|
171
|
+
["smoke-wave-1", "resolution", "alice"],
|
|
172
|
+
["smoke-wave-2", "pending", null],
|
|
173
|
+
["smoke-wave-2", "resolution", "alice"],
|
|
174
|
+
]);
|
|
175
|
+
expect(ledger[3]!.answer?.id).toBe(id);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slim wave-job runtime (#3896): the four commands a generated Op waves
|
|
3
|
+
* pipeline runs, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* chant run wave --spec <file> --wave <k> [--decide | --share <i> | --record-plans]
|
|
6
|
+
* chant run resume [--gate <op>/<gate>] [--dry-run]
|
|
7
|
+
* chant approve <op> <gate> [...]
|
|
8
|
+
* chant workspace points ask|answer|list|... [...]
|
|
9
|
+
*
|
|
10
|
+
* `scripts/build-runtime.mjs` bundles this file with esbuild into
|
|
11
|
+
* `dist/wave-runtime.mjs`: one self-contained ES module whose only imports are
|
|
12
|
+
* `node:` builtins, with no TypeScript compiler, esbuild, tsx or zod in it. A
|
|
13
|
+
* product copies that one file into its CI image and runs it with plain node,
|
|
14
|
+
* or runs it from an installed `@intentius/chant`
|
|
15
|
+
* (`node node_modules/@intentius/chant/dist/wave-runtime.mjs run wave ...`).
|
|
16
|
+
*
|
|
17
|
+
* The handlers are the full CLI's own (`./cli/main.ts` dispatches to the same
|
|
18
|
+
* functions), parsed by the same `parseArgs`, so flags, output, exit codes and
|
|
19
|
+
* the ledgers each command writes are the same. What the runtime does not do:
|
|
20
|
+
* evaluate `chant.config.ts` (which needs a TypeScript loader). None of the
|
|
21
|
+
* four commands reads the config; the full CLI loads it only to check `--env`
|
|
22
|
+
* against the declared `environments`, a check the runtime skips.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { parseArgs } from "./cli/parse-args";
|
|
26
|
+
import { resolveCommand, type CommandDef, type ParsedArgs } from "./cli/registry";
|
|
27
|
+
import { formatError } from "./cli/format";
|
|
28
|
+
import { flushAndExit } from "./cli/flush";
|
|
29
|
+
import { CHANT_VERSION } from "./cli/version";
|
|
30
|
+
import { isEntryPoint } from "./cli/is-entry-point";
|
|
31
|
+
import { ENV_VAR } from "./env";
|
|
32
|
+
import { initRuntime } from "./runtime-adapter";
|
|
33
|
+
import { runOpWaveCommand } from "./cli/handlers/run-wave";
|
|
34
|
+
import { runResumeCommand } from "./cli/handlers/gate-resume";
|
|
35
|
+
import { runApprove } from "./cli/handlers/approve";
|
|
36
|
+
import { runWorkspacePoints } from "./workspace/points-cli";
|
|
37
|
+
|
|
38
|
+
/** The commands the runtime carries, under the names the full CLI registers them by. */
|
|
39
|
+
export const RUNTIME_COMMANDS: CommandDef[] = [
|
|
40
|
+
{ name: "run wave", handler: runOpWaveCommand },
|
|
41
|
+
{ name: "run resume", handler: runResumeCommand },
|
|
42
|
+
{ name: "approve", handler: runApprove },
|
|
43
|
+
{ name: "workspace points", handler: runWorkspacePoints },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const HELP = `
|
|
47
|
+
chant runtime ${CHANT_VERSION} - the wave-job commands of chant, in one file
|
|
48
|
+
|
|
49
|
+
Usage: node wave-runtime.mjs <command> [options]
|
|
50
|
+
|
|
51
|
+
Commands:
|
|
52
|
+
run wave --spec <file> --wave <k> Plan, gate and apply wave k of an Op waves spec
|
|
53
|
+
(--decide, --share <i>, --record-plans, --base <ref>, --json)
|
|
54
|
+
run resume Start the CI jobs that wait at gates whose approval arrived
|
|
55
|
+
(--gate <op>/<gate>, --dry-run, --json)
|
|
56
|
+
approve <op> <gate> Record a gate's resolution on the lifecycle ledger
|
|
57
|
+
(--actor, --plan, --note, --url, --sign, --expire, --resume, ...)
|
|
58
|
+
workspace points <verb> Ask, answer and list decision points
|
|
59
|
+
|
|
60
|
+
Flags, output, exit codes and ledger formats are the full chant CLI's;
|
|
61
|
+
see "chant --help" in @intentius/chant for every flag. This runtime does
|
|
62
|
+
not evaluate chant.config.ts, so --env is not checked against the
|
|
63
|
+
declared environments.
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
/** Parse, dispatch and return the exit code. Exported for tests. */
|
|
67
|
+
export async function runRuntime(argv: string[]): Promise<number> {
|
|
68
|
+
let args: ParsedArgs;
|
|
69
|
+
try {
|
|
70
|
+
args = parseArgs(argv);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const match = resolveCommand(args, RUNTIME_COMMANDS);
|
|
76
|
+
if (args.version && (!args.command || match)) {
|
|
77
|
+
console.log(CHANT_VERSION);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
if (args.help || !args.command) {
|
|
81
|
+
console.log(HELP);
|
|
82
|
+
return args.help ? 0 : 1;
|
|
83
|
+
}
|
|
84
|
+
if (!match) {
|
|
85
|
+
const name = args.path && args.path !== "." ? `${args.command} ${args.path}` : args.command;
|
|
86
|
+
console.error(formatError({
|
|
87
|
+
message: `"${name}" is not part of chant's wave-job runtime`,
|
|
88
|
+
hint: "It runs run wave, run resume, approve and workspace points. Every other command needs the full @intentius/chant CLI.",
|
|
89
|
+
}));
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
if (args.env) process.env[ENV_VAR] = args.env;
|
|
93
|
+
initRuntime();
|
|
94
|
+
return match.def.handler({ args, plugins: [], serializers: [] });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isEntryPoint(process.argv[1], import.meta.url)) {
|
|
98
|
+
runRuntime(process.argv.slice(2)).then(flushAndExit, async (err) => {
|
|
99
|
+
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
100
|
+
if (verbose && err instanceof Error && err.stack) console.error(err.stack);
|
|
101
|
+
else console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
102
|
+
await flushAndExit(1);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Ajv 2020-12 validator class, loaded on first use: ajv costs start-up
|
|
3
|
+
* time, and most commands validate nothing with it. One loader for every
|
|
4
|
+
* schema check in workspace/, so the slim wave-job runtime (#3896,
|
|
5
|
+
* `../../scripts/build-wave-runtime.mjs`) can swap in a bundled ajv here
|
|
6
|
+
* instead of a `require` resolved next to an installed chant.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
|
|
10
|
+
export type Ajv2020Class = new (opts: object) => { compile(schema: object): unknown };
|
|
11
|
+
|
|
12
|
+
export function loadAjv2020(): Ajv2020Class {
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
// ajv is CommonJS; its class is the default export, or that export's own default.
|
|
15
|
+
const mod = require("ajv/dist/2020") as { default?: unknown };
|
|
16
|
+
return (mod.default ?? mod) as Ajv2020Class;
|
|
17
|
+
}
|
|
@@ -16,15 +16,14 @@
|
|
|
16
16
|
* runs. The level-0 goldens (#2526) fail if a level-0 command loads it.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { readFileSync } from "node:fs";
|
|
20
|
-
import { createRequire } from "node:module";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
19
|
import schema from "./declaration.schema.json" with { type: "json" };
|
|
23
20
|
import { expandGlob } from "./glob";
|
|
24
21
|
import { EXAMPLES_KIND, holdsChantProject } from "./kinds";
|
|
25
22
|
import { parseJsonText, pointerToken, type TextLocation } from "./jsonc";
|
|
26
23
|
import type { ReasonCode } from "./reason-codes";
|
|
27
24
|
import { joinPath, type WorkspaceTree } from "./tree";
|
|
25
|
+
import { loadAjv2020 } from "./ajv2020";
|
|
26
|
+
import { readChantPackageVersion } from "../package-version";
|
|
28
27
|
|
|
29
28
|
export const DECLARATION_SCHEMA_ID = schema.$id;
|
|
30
29
|
export const DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"] as const;
|
|
@@ -601,9 +600,7 @@ let readerVersionCache: string | undefined;
|
|
|
601
600
|
* well under a millisecond.
|
|
602
601
|
*/
|
|
603
602
|
export function readerVersion(): string {
|
|
604
|
-
readerVersionCache ??= (
|
|
605
|
-
JSON.parse(readFileSync(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf-8")) as { version: string }
|
|
606
|
-
).version;
|
|
603
|
+
readerVersionCache ??= readChantPackageVersion();
|
|
607
604
|
return readerVersionCache;
|
|
608
605
|
}
|
|
609
606
|
|
|
@@ -638,10 +635,7 @@ type Validate = ((data: unknown) => boolean) & { errors?: AjvError[] | null };
|
|
|
638
635
|
let compiled: Validate | undefined;
|
|
639
636
|
function validator(): Validate {
|
|
640
637
|
if (compiled) return compiled;
|
|
641
|
-
const
|
|
642
|
-
// ajv is CommonJS; its class is the default export, or that export's own default.
|
|
643
|
-
const mod = require("ajv/dist/2020") as { default?: unknown };
|
|
644
|
-
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
|
|
638
|
+
const Ajv = loadAjv2020() as new (opts: object) => { compile(s: object): Validate };
|
|
645
639
|
compiled = new Ajv({ allErrors: true, strict: true }).compile(schema);
|
|
646
640
|
return compiled;
|
|
647
641
|
}
|
package/src/workspace/kinds.ts
CHANGED
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
|
-
import { createRequire } from "node:module";
|
|
22
21
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
23
22
|
import schema from "./workspace-kinds.schema.json" with { type: "json" };
|
|
24
23
|
import type { WorkspaceTree } from "./tree";
|
|
25
24
|
import { joinPath, skippedDir } from "./tree";
|
|
26
25
|
import { checkPinIntegrity } from "./pin-integrity";
|
|
27
26
|
import { dataExportTarget, findInstalledPackage } from "./class-data";
|
|
27
|
+
import { loadAjv2020 } from "./ajv2020";
|
|
28
28
|
|
|
29
29
|
export const KINDS_SCHEMA_ID = schema.$id;
|
|
30
30
|
|
|
@@ -353,10 +353,7 @@ type Validate = ((data: unknown) => boolean) & { errors?: AjvError[] | null };
|
|
|
353
353
|
let compiled: Validate | undefined;
|
|
354
354
|
function validator(): Validate {
|
|
355
355
|
if (compiled) return compiled;
|
|
356
|
-
const
|
|
357
|
-
// ajv is CommonJS; its class is the default export, or that export's own default.
|
|
358
|
-
const mod = require("ajv/dist/2020") as { default?: unknown };
|
|
359
|
-
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
|
|
356
|
+
const Ajv = loadAjv2020() as new (opts: object) => { compile(s: object): Validate };
|
|
360
357
|
compiled = new Ajv({ allErrors: true, strict: true }).compile(schema);
|
|
361
358
|
return compiled;
|
|
362
359
|
}
|
package/src/workspace/points.ts
CHANGED
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { createHash } from "node:crypto";
|
|
36
|
-
import { createRequire } from "node:module";
|
|
37
36
|
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
38
37
|
import schema from "./decision-points.schema.json" with { type: "json" };
|
|
39
38
|
import type { ReasonCode } from "./reason-codes";
|
|
40
39
|
import type { RecordSource } from "./record-source";
|
|
41
40
|
import type { LoadedRecordKind, ReadRecordsOptions, RecordEntry } from "./records";
|
|
41
|
+
import { loadAjv2020 } from "./ajv2020";
|
|
42
42
|
|
|
43
43
|
export const DECISION_POINTS_SCHEMA_ID = schema.$id;
|
|
44
44
|
|
|
@@ -200,10 +200,7 @@ type Validate = ((data: unknown) => boolean) & { errors?: AjvError[] | null };
|
|
|
200
200
|
let compiled: Validate | undefined;
|
|
201
201
|
function validator(): Validate {
|
|
202
202
|
if (compiled) return compiled;
|
|
203
|
-
const
|
|
204
|
-
// ajv is CommonJS; its class is the default export, or that export's own default.
|
|
205
|
-
const mod = require("ajv/dist/2020") as { default?: unknown };
|
|
206
|
-
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
|
|
203
|
+
const Ajv = loadAjv2020() as new (opts: object) => { compile(s: object): Validate };
|
|
207
204
|
compiled = new Ajv({ allErrors: true, strict: false }).compile(schema);
|
|
208
205
|
return compiled;
|
|
209
206
|
}
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
* installed package, which must be at the version the base pins.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { createRequire } from "node:module";
|
|
26
25
|
import { checkClassData, diskFiles, mergePinClasses, readClassFiles, type ClassData, type PackageFiles, type PinClassRead } from "./class-data";
|
|
27
26
|
import { pinnedPackageDir, type KindLoadProblem, type PinLike } from "./kinds";
|
|
28
27
|
import schema from "./workspace-principals.schema.json" with { type: "json" };
|
|
29
28
|
import type { TrustPolicy } from "./trust/policy";
|
|
30
29
|
import { joinPath, type WorkspaceTree } from "./tree";
|
|
31
30
|
import { CORE_CLASSES, CORE_PRINCIPAL_CLASSES, grantedClasses, type PrincipalClass } from "./identity-gate-rule";
|
|
31
|
+
import { loadAjv2020 } from "./ajv2020";
|
|
32
32
|
|
|
33
33
|
export const PRINCIPALS_SCHEMA_ID = schema.$id;
|
|
34
34
|
|
|
@@ -99,10 +99,7 @@ type Validate = ((data: unknown) => boolean) & { errors?: AjvError[] | null };
|
|
|
99
99
|
let compiled: Validate | undefined;
|
|
100
100
|
function validator(): Validate {
|
|
101
101
|
if (compiled) return compiled;
|
|
102
|
-
const
|
|
103
|
-
// ajv is CommonJS; its class is the default export, or that export's own default.
|
|
104
|
-
const mod = require("ajv/dist/2020") as { default?: unknown };
|
|
105
|
-
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
|
|
102
|
+
const Ajv = loadAjv2020() as new (opts: object) => { compile(s: object): Validate };
|
|
106
103
|
compiled = new Ajv({ allErrors: true, strict: true }).compile(schema);
|
|
107
104
|
return compiled;
|
|
108
105
|
}
|