@stll/oxlint-config 0.10.0 → 0.12.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/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # @stll/oxlint-config
2
+
3
+ Shared oxlint presets, JavaScript plugins, and toolchain policy for Stella
4
+ packages. Repository-specific rules and exceptions belong in the consuming
5
+ repository's configuration.
6
+
7
+ ## Install and configure
8
+
9
+ ```sh
10
+ bun add -d @stll/oxlint-config
11
+ ```
12
+
13
+ Install `oxlint`, `oxlint-tsgolint`, and any other shared tools at the exact
14
+ versions in the exported `@stll/oxlint-config/toolchain.json`. That file is the
15
+ source for current pins; peer dependency ranges describe supported versions.
16
+
17
+ Use the library preset in `oxlint.config.ts`:
18
+
19
+ ```ts
20
+ import { library } from "@stll/oxlint-config";
21
+
22
+ export default library({
23
+ ignorePatterns: ["dist/"],
24
+ overrides: [
25
+ {
26
+ files: ["scripts/**"],
27
+ rules: { "no-console": "off" },
28
+ },
29
+ ],
30
+ });
31
+ ```
32
+
33
+ The preset includes the shared `stella-lowercase` and `no-raw-colors`
34
+ JavaScript plugins. The separate `@stll/oxlint-plugin` package provides
35
+ portable safety and route-query rules. See the
36
+ [repository usage guide](https://github.com/stella/tooling#usage) for other
37
+ presets, CommonJS configuration, and plugin composition.
38
+
39
+ ## Shared toolchain and Bun
40
+
41
+ `@stll/oxlint-config/toolchain.json` is a versioned policy for package pins,
42
+ Bun, TypeScript install layouts, Node, Python, Rust, GitHub Actions, and
43
+ Dependabot. Inherit the fields for tools your repository uses. The checker
44
+ does not require tools absent from the repository.
45
+
46
+ Set `packageManager` to `bun@` followed by the policy's `bun` value, and pin
47
+ `bun-types` to that same value. Dependency pins can live in default or named
48
+ catalogs, including Bun's `workspaces` catalogs. Bun image tags, version
49
+ files, and mise/asdf entries also inherit the policy pin.
50
+
51
+ Configure `oven-sh/setup-bun` with a tracked manifest as its version source:
52
+
53
+ ```yaml
54
+ with:
55
+ bun-version-file: package.json
56
+ ```
57
+
58
+ The action itself must use the policy's approved SHA and version comment.
59
+ A prefixed version-file selector can use a preceding checkout in its job:
60
+ use the exact `${{ job.workflow_repository }}` / `${{ job.workflow_sha }}` pair,
61
+ or a same-repository checkout with no `ref`. For no-ref checkouts, omit
62
+ `repository`, name this repository literally, or use `${{ github.repository }}`.
63
+ The checkout must omit both `if` and `continue-on-error`, and its normalized
64
+ path must be unique among every checkout in that job, including later steps.
65
+ Unknown checkout destinations prevent mapped selectors from proving provenance.
66
+ Sparse checkouts must explicitly list the selected repository-relative file,
67
+ without the checkout prefix, in `sparse-checkout`. This applies in either cone
68
+ mode and to delegated sources. Dynamic, empty, glob, negation, or unsupported
69
+ sparse configurations fail; a cone-mode input alone is insufficient. Directory
70
+ entries cannot substitute for the selected file.
71
+ Other nonempty, nonconstant GitHub expression refs on same-repository checkouts
72
+ (including `${{ job.workflow_repository }}`) delegate a safe static version-file selector
73
+ to that checkout. The CLI reports delegation on stdout without validating the
74
+ current source's version file. Literal refs and unpaired `${{ github.sha }}` /
75
+ `${{ job.workflow_sha }}` remain rejected.
76
+ For snapshot-bound sources, the guard maps the selector to its tracked source
77
+ file. Literal commit SHAs,
78
+ branches, tags, unpaired snapshot contexts, foreign repositories, and traversal
79
+ paths fail.
80
+ Direct `bun-version` inputs are forbidden. For snapshot-bound sources, the
81
+ manifest referenced by `bun-version-file` must declare the shared
82
+ `packageManager` value. Delegated commits own their version validation.
83
+
84
+ The Node policy is `26.x`. Runtime selectors (`.node-version`, `.nvmrc`,
85
+ setup-node's `node-version-file`, Node image tags and mise/asdf entries)
86
+ must select major 26: bare major, major wildcard, minor and stable patch
87
+ selectors within that major are allowed. Open ranges, floating aliases,
88
+ other majors and prereleases fail. Use `node-version-file` for setup-node.
89
+ `engines.node` remains a support range and must include every stable release
90
+ in major 26; `>=26`, `^26` and `^24 || ^26` pass, while ranges that omit
91
+ part of major 26 fail.
92
+ Release and publish workflows use exact patch version-file selectors for
93
+ reproducible builds; this repository pins `.node-version` to `26.10.0`.
94
+ The series policy accepts that patch without weakening release validation.
95
+ GitHub JavaScript action host runtimes (`runs.using`) are platform-managed
96
+ and do not select the project toolchain Node version.
97
+
98
+ A published package can test the minimum Node major in its `engines.node`
99
+ support range in one declared workflow job. Add a scoped `engineFloors` entry
100
+ to tracked root `stll-toolchain.json`:
101
+
102
+ ```json
103
+ {
104
+ "engineFloors": [
105
+ {
106
+ "package": "packages/library",
107
+ "workflow": ".github/workflows/ci.yml",
108
+ "job": "node-floor"
109
+ }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ That job may use a literal exact stable patch in setup-node's `node-version`
115
+ whose major matches the range's minimum major and whose release satisfies
116
+ the range (for example, `20.10.0` for `>=20.10.0`). The approved action SHA, version comment and input validation
117
+ also apply. Other jobs and runtime files follow the shared Node series.
118
+ Missing packages or jobs, private packages, absent setup-node selectors and
119
+ floor mismatches fail configuration validation. `optOuts` is optional when
120
+ only `engineFloors` is declared.
121
+
122
+ Run the installed checker from the repository root in CI:
123
+
124
+ ```sh
125
+ bunx --no-install stll-toolchain-check
126
+ ```
127
+
128
+ The CLI checks tracked files, prints `path:line: [rule] message` diagnostics,
129
+ and exits with status 1 on a mismatch. Updating the package updates the
130
+ policy; update consumer pins and run the checker together.
131
+
132
+ `typescriptInstallLayouts` declares the compiler package, exact specifier,
133
+ and typecheck command for each supported layout. Prefer the direct layout.
134
+ Use the split compatibility layout for the compiler-API consumers and peer
135
+ blockers listed in `typescript6Compatibility`; it retains the current
136
+ compiler for typechecking while providing the compatibility package.
137
+
138
+ An exception requires a tracked root `stll-toolchain.json` with the named
139
+ rule and a nonempty reason:
140
+
141
+ ```json
142
+ {
143
+ "optOuts": [{ "rule": "bun-pins", "reason": "This repository uses npm." }]
144
+ }
145
+ ```
146
+
147
+ Workspace TypeScript toolchains combine the root and declared members' devDependencies.
148
+ An included member may declare the exact TypeScript 6 compatibility API across
149
+ dependencies, devDependencies, and peerDependencies when that shared install uses
150
+ the complete split layout. Ambiguous or partial layouts do not grant this allowance.
151
+ Peer ranges declare support: they must include the policy compiler release and the
152
+ compatibility release when the selected layout uses it. Ranges do not contribute
153
+ to the installed compiler inventory.
154
+
155
+ JavaScript update roots come from the repository root, declared workspace membership,
156
+ and directories with their own `bun.lock`, `package-lock.json`, `yarn.lock`, or
157
+ `pnpm-lock.yaml`. Other package manifests do not create update roots; directory names
158
+ have no special meaning. Workspace members share their owner's update entry.
159
+
160
+ `node-engine` and `cloud-setup-drift` are mandatory: opt-outs cannot bypass the
161
+ shared Node support range or generated cloud setup requirements.
162
+
163
+ Opt-outs apply to the entire repository for that rule. Unknown or duplicate
164
+ rules, empty reasons, and malformed configuration fail validation. See the
165
+ [shared toolchain reference](https://github.com/stella/tooling#shared-toolchain)
166
+ for the schema, rule list, runtime support ranges, and generated Dependabot
167
+ template.
168
+
169
+ ## Typecheck parity project selection
170
+
171
+ Run `stll-typecheck-parity` from the repository root. Without flags, it checks
172
+ the root `tsconfig.json` and recursively discovers project references in build
173
+ mode. Repeat `--project` to check exactly the selected configs:
174
+
175
+ ```sh
176
+ stll-typecheck-parity --project packages/api/tsconfig.json --project packages/web/tsconfig.json
177
+ ```
178
+
179
+ Selected configs run independently using the repository-root compiler
180
+ installation; child packages need no compiler declaration and a root tsconfig
181
+ is unnecessary. Their original reference metadata is preserved without
182
+ discovering or building referenced projects. Missing, unreadable, and zero-input
183
+ selected configs fail the check. `--help` prints usage and exits successfully;
184
+ unknown arguments fail.
185
+
186
+ ## Shared cloud setup
187
+
188
+ Add an explicit declaration to tracked root `stll-toolchain.json`:
189
+
190
+ ```json
191
+ {
192
+ "optOuts": [],
193
+ "cloud": {
194
+ "services": ["postgres", "valkey"],
195
+ "install": "bun install --frozen-lockfile",
196
+ "envFile": ".env.cloud"
197
+ }
198
+ }
199
+ ```
200
+
201
+ Services are optional within the declared list: use `[]` for runtimes and
202
+ dependencies only. No `cloud` declaration means no cloud setup script.
203
+ `envFile` must be a canonical repository-relative path; its existing parent
204
+ must be safe at runtime. Track an exact stable Node patch in root
205
+ `.node-version` within the shared Node series. Generate and commit the script:
206
+
207
+ ```sh
208
+ bunx --no-install stll-cloud-setup
209
+ ```
210
+
211
+ Generation uses the installed policy's Bun, Node, PostgreSQL, and Valkey pins.
212
+ PostgreSQL uses the PGDG major-version packages; minor updates within the pinned
213
+ major are accepted for the disposable database. Before adding PGDG, provisioning
214
+ reuses an existing source only after verifying its signing-key fingerprint;
215
+ conflicts identify the source file. Existing matching-major binaries are reused. Valkey uses official versioned
216
+ binary DEBs for the declared release and architecture, verifies the policy's
217
+ SHA256 pins, and installs those local files with apt. It does not resolve Valkey
218
+ from a mutable package index.
219
+ The command writes `.agents/cloud-setup.sh` with executable permissions and
220
+ refuses symlink parents or destinations and hard-linked destinations. `stll-toolchain-check` rejects missing,
221
+ drifted, or undeclared tracked scripts with mandatory `cloud-setup-drift`.
222
+ Regenerate after changing the policy or declaration. `stll-cloud-setup --help`
223
+ prints usage without reading repository configuration.
224
+
225
+ | Environment phase | Command |
226
+ | ----------------------- | ------------------------------------- |
227
+ | Networked install | `bash .agents/cloud-setup.sh install` |
228
+ | Offline session startup | `bash .agents/cloud-setup.sh start` |
229
+
230
+ Both commands can be rerun. Missing or unknown subcommands print usage and
231
+ exit 2; there is no implicit lifecycle mode. The script supports Ubuntu 24.04
232
+ with root access or passwordless sudo and fails when required capabilities
233
+ or pinned installations are unavailable. `install` provisions runtimes,
234
+ dependencies, and declared services; `start` restores local services after
235
+ cached environments lose running processes. Services bind only to localhost.
236
+ System provisioning runs as root; frozen dependencies run as the invoking user
237
+ with that user's home and a private, user-owned Bun cache. Package installation
238
+ does not permit automatic downgrades.
239
+ If a newer Valkey package is already installed, provisioning fails rather than
240
+ downgrading it; use an environment compatible with the pinned release.
241
+ The environment file contains `NODE_ENV=test` and declared local connection
242
+ URLs, carries a generated ownership marker, and refuses an unowned file or
243
+ symlink. Configure the repository's test runner to load that file; migration
244
+ commands and repository adoption are separate configuration steps.
245
+
246
+ Environments may cache filesystems without preserving service processes.
247
+ Run `start` at every session start, including sessions that reuse installed files.
248
+ The supported OS, privileges, and pinned runtime requirements are checked explicitly.
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_toolchain_guard = require("./toolchain-guard.cjs");
4
+ let node_fs = require("node:fs");
5
+ let node_path = require("node:path");
6
+ let node_fs_promises = require("node:fs/promises");
7
+ //#region src/cloud-setup-cli.ts
8
+ const usage = "Usage: stll-cloud-setup (run from repository root)\n stll-cloud-setup --help";
9
+ const optionalStat = async (path) => {
10
+ try {
11
+ return await (0, node_fs_promises.lstat)(path);
12
+ } catch (error) {
13
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
14
+ throw error;
15
+ }
16
+ };
17
+ const main = async () => {
18
+ const args = process.argv.slice(2);
19
+ if (args.length === 1 && args.at(0) === "--help") {
20
+ process.stdout.write(`${usage}\n`);
21
+ return;
22
+ }
23
+ if (args.length !== 0) throw new Error(usage);
24
+ const root = await (0, node_fs_promises.realpath)(process.cwd());
25
+ const { files, diagnostics } = require_toolchain_guard.readToolchainInputs(root);
26
+ if (diagnostics.length > 0) throw new Error(diagnostics.map(({ path, message }) => `${path}: ${message}`).join("\n"));
27
+ const text = files["stll-toolchain.json"];
28
+ if (text === void 0) throw new Error("requires tracked stll-toolchain.json");
29
+ const { cloud } = require_toolchain_guard.parseToolchainConfiguration(JSON.parse(text));
30
+ if (cloud === void 0) throw new Error("requires an explicit cloud declaration");
31
+ const nodeVersion = files[".node-version"];
32
+ if (nodeVersion === void 0) throw new Error("requires tracked root .node-version");
33
+ const script = require_toolchain_guard.generateCloudSetup({
34
+ policy: require_toolchain_guard.parseToolchainPolicy(JSON.parse(await (0, node_fs_promises.readFile)(new URL("../toolchain.json", require("url").pathToFileURL(__filename).href), "utf8"))),
35
+ cloud,
36
+ nodeVersion: nodeVersion.trim()
37
+ });
38
+ const destination = (0, node_path.join)(root, require_toolchain_guard.cloudSetupPath);
39
+ const parent = (0, node_path.dirname)(destination);
40
+ const parentStat = await optionalStat(parent);
41
+ if (parentStat === void 0) await (0, node_fs_promises.mkdir)(parent);
42
+ else if (parentStat.isSymbolicLink() || !parentStat.isDirectory()) throw new Error("cloud setup parent must be a real repository directory");
43
+ const destinationStat = await optionalStat(destination);
44
+ if (destinationStat !== void 0 && (destinationStat.isSymbolicLink() || !destinationStat.isFile() || destinationStat.nlink !== 1)) throw new Error("cloud setup destination must be a single-link regular repository file");
45
+ const output = await (0, node_fs_promises.open)(destination, node_fs.constants.O_WRONLY | node_fs.constants.O_CREAT | node_fs.constants.O_NOFOLLOW, 493);
46
+ try {
47
+ const opened = await output.stat();
48
+ if (!opened.isFile() || opened.nlink !== 1) throw new Error("cloud setup destination must be a single-link regular repository file");
49
+ await output.truncate(0);
50
+ await output.writeFile(script, "utf8");
51
+ await output.chmod(493);
52
+ } finally {
53
+ await output.close();
54
+ }
55
+ process.stdout.write(`Generated ${require_toolchain_guard.cloudSetupPath}\n`);
56
+ };
57
+ main().catch((error) => {
58
+ process.stderr.write(`cloud-setup: ${error instanceof Error ? error.message : String(error)}\n`);
59
+ process.exitCode = 1;
60
+ });
61
+ //#endregion
62
+
63
+ //# sourceMappingURL=cloud-setup-cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-setup-cli.cjs","names":["readToolchainInputs","parseToolchainConfiguration","generateCloudSetup","parseToolchainPolicy","cloudSetupPath","constants"],"sources":["../src/cloud-setup-cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { constants } from \"node:fs\";\nimport { lstat, mkdir, open, readFile, realpath } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { cloudSetupPath, generateCloudSetup } from \"./cloud-setup\";\nimport {\n parseToolchainConfiguration,\n readToolchainInputs,\n} from \"./toolchain-guard\";\nimport { parseToolchainPolicy } from \"./toolchain-schema\";\n\nconst usage =\n \"Usage: stll-cloud-setup (run from repository root)\\n stll-cloud-setup --help\";\nconst optionalStat = async (path: string) => {\n try {\n return await lstat(path);\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\")\n return undefined;\n throw error;\n }\n};\n\nconst main = async () => {\n const args = process.argv.slice(2);\n if (args.length === 1 && args.at(0) === \"--help\") {\n process.stdout.write(`${usage}\\n`);\n return;\n }\n if (args.length !== 0) throw new Error(usage);\n const root = await realpath(process.cwd());\n const { files, diagnostics } = readToolchainInputs(root);\n if (diagnostics.length > 0)\n throw new Error(\n diagnostics.map(({ path, message }) => `${path}: ${message}`).join(\"\\n\"),\n );\n const text = files[\"stll-toolchain.json\"];\n if (text === undefined)\n throw new Error(\"requires tracked stll-toolchain.json\");\n const { cloud } = parseToolchainConfiguration(JSON.parse(text));\n if (cloud === undefined)\n throw new Error(\"requires an explicit cloud declaration\");\n const nodeVersion = files[\".node-version\"];\n if (nodeVersion === undefined)\n throw new Error(\"requires tracked root .node-version\");\n const policy = parseToolchainPolicy(\n JSON.parse(\n await readFile(new URL(\"../toolchain.json\", import.meta.url), \"utf8\"),\n ),\n );\n const script = generateCloudSetup({\n policy,\n cloud,\n nodeVersion: nodeVersion.trim(),\n });\n const destination = join(root, cloudSetupPath);\n const parent = dirname(destination);\n const parentStat = await optionalStat(parent);\n if (parentStat === undefined) await mkdir(parent);\n else if (parentStat.isSymbolicLink() || !parentStat.isDirectory())\n throw new Error(\"cloud setup parent must be a real repository directory\");\n const destinationStat = await optionalStat(destination);\n if (\n destinationStat !== undefined &&\n (destinationStat.isSymbolicLink() ||\n !destinationStat.isFile() ||\n destinationStat.nlink !== 1)\n )\n throw new Error(\n \"cloud setup destination must be a single-link regular repository file\",\n );\n const output = await open(\n destination,\n constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW,\n 0o755,\n );\n try {\n const opened = await output.stat();\n if (!opened.isFile() || opened.nlink !== 1)\n throw new Error(\n \"cloud setup destination must be a single-link regular repository file\",\n );\n await output.truncate(0);\n await output.writeFile(script, \"utf8\");\n await output.chmod(0o755);\n } finally {\n await output.close();\n }\n process.stdout.write(`Generated ${cloudSetupPath}\\n`);\n};\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `cloud-setup: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;;AAYA,MAAM,QACJ;AACF,MAAM,eAAe,OAAO,SAAiB;CAC3C,IAAI;EACF,OAAO,OAAA,GAAA,iBAAA,MAAA,CAAY,IAAI;CACzB,SAAS,OAAO;EACd,IAAI,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,UAC9D,OAAO,KAAA;EACT,MAAM;CACR;AACF;AAEA,MAAM,OAAO,YAAY;CACvB,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;CACjC,IAAI,KAAK,WAAW,KAAK,KAAK,GAAG,CAAC,MAAM,UAAU;EAChD,QAAQ,OAAO,MAAM,GAAG,MAAM,GAAG;EACjC;CACF;CACA,IAAI,KAAK,WAAW,GAAG,MAAM,IAAI,MAAM,KAAK;CAC5C,MAAM,OAAO,OAAA,GAAA,iBAAA,SAAA,CAAe,QAAQ,IAAI,CAAC;CACzC,MAAM,EAAE,OAAO,gBAAgBA,wBAAAA,oBAAoB,IAAI;CACvD,IAAI,YAAY,SAAS,GACvB,MAAM,IAAI,MACR,YAAY,KAAK,EAAE,MAAM,cAAc,GAAG,KAAK,IAAI,SAAS,CAAC,CAAC,KAAK,IAAI,CACzE;CACF,MAAM,OAAO,MAAM;CACnB,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,sCAAsC;CACxD,MAAM,EAAE,UAAUC,wBAAAA,4BAA4B,KAAK,MAAM,IAAI,CAAC;CAC9D,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,wCAAwC;CAC1D,MAAM,cAAc,MAAM;CAC1B,IAAI,gBAAgB,KAAA,GAClB,MAAM,IAAI,MAAM,qCAAqC;CAMvD,MAAM,SAASC,wBAAAA,mBAAmB;EAChC,QANaC,wBAAAA,qBACb,KAAK,MACH,OAAA,GAAA,iBAAA,SAAA,CAAe,IAAI,IAAI,qBAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAAoC,GAAG,MAAM,CACtE,CAGK;EACL;EACA,aAAa,YAAY,KAAK;CAChC,CAAC;CACD,MAAM,eAAA,GAAA,UAAA,KAAA,CAAmB,MAAMC,wBAAAA,cAAc;CAC7C,MAAM,UAAA,GAAA,UAAA,QAAA,CAAiB,WAAW;CAClC,MAAM,aAAa,MAAM,aAAa,MAAM;CAC5C,IAAI,eAAe,KAAA,GAAW,OAAA,GAAA,iBAAA,MAAA,CAAY,MAAM;MAC3C,IAAI,WAAW,eAAe,KAAK,CAAC,WAAW,YAAY,GAC9D,MAAM,IAAI,MAAM,wDAAwD;CAC1E,MAAM,kBAAkB,MAAM,aAAa,WAAW;CACtD,IACE,oBAAoB,KAAA,MACnB,gBAAgB,eAAe,KAC9B,CAAC,gBAAgB,OAAO,KACxB,gBAAgB,UAAU,IAE5B,MAAM,IAAI,MACR,uEACF;CACF,MAAM,SAAS,OAAA,GAAA,iBAAA,KAAA,CACb,aACAC,QAAAA,UAAU,WAAWA,QAAAA,UAAU,UAAUA,QAAAA,UAAU,YACnD,GACF;CACA,IAAI;EACF,MAAM,SAAS,MAAM,OAAO,KAAK;EACjC,IAAI,CAAC,OAAO,OAAO,KAAK,OAAO,UAAU,GACvC,MAAM,IAAI,MACR,uEACF;EACF,MAAM,OAAO,SAAS,CAAC;EACvB,MAAM,OAAO,UAAU,QAAQ,MAAM;EACrC,MAAM,OAAO,MAAM,GAAK;CAC1B,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;CACA,QAAQ,OAAO,MAAM,aAAaD,wBAAAA,eAAe,GAAG;AACtD;AAEA,KAAK,CAAC,CAAC,OAAO,UAAmB;CAC/B,QAAQ,OAAO,MACb,gBAAgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GACzE;CACA,QAAQ,WAAW;AACrB,CAAC"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { a as cloudSetupPath, i as parseToolchainPolicy, n as parseToolchainConfiguration, o as generateCloudSetup, r as readToolchainInputs } from "./toolchain-guard.mjs";
3
+ import { constants } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { lstat, mkdir, open, readFile, realpath } from "node:fs/promises";
6
+ //#region src/cloud-setup-cli.ts
7
+ const usage = "Usage: stll-cloud-setup (run from repository root)\n stll-cloud-setup --help";
8
+ const optionalStat = async (path) => {
9
+ try {
10
+ return await lstat(path);
11
+ } catch (error) {
12
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
13
+ throw error;
14
+ }
15
+ };
16
+ const main = async () => {
17
+ const args = process.argv.slice(2);
18
+ if (args.length === 1 && args.at(0) === "--help") {
19
+ process.stdout.write(`${usage}\n`);
20
+ return;
21
+ }
22
+ if (args.length !== 0) throw new Error(usage);
23
+ const root = await realpath(process.cwd());
24
+ const { files, diagnostics } = readToolchainInputs(root);
25
+ if (diagnostics.length > 0) throw new Error(diagnostics.map(({ path, message }) => `${path}: ${message}`).join("\n"));
26
+ const text = files["stll-toolchain.json"];
27
+ if (text === void 0) throw new Error("requires tracked stll-toolchain.json");
28
+ const { cloud } = parseToolchainConfiguration(JSON.parse(text));
29
+ if (cloud === void 0) throw new Error("requires an explicit cloud declaration");
30
+ const nodeVersion = files[".node-version"];
31
+ if (nodeVersion === void 0) throw new Error("requires tracked root .node-version");
32
+ const script = generateCloudSetup({
33
+ policy: parseToolchainPolicy(JSON.parse(await readFile(new URL("../toolchain.json", import.meta.url), "utf8"))),
34
+ cloud,
35
+ nodeVersion: nodeVersion.trim()
36
+ });
37
+ const destination = join(root, cloudSetupPath);
38
+ const parent = dirname(destination);
39
+ const parentStat = await optionalStat(parent);
40
+ if (parentStat === void 0) await mkdir(parent);
41
+ else if (parentStat.isSymbolicLink() || !parentStat.isDirectory()) throw new Error("cloud setup parent must be a real repository directory");
42
+ const destinationStat = await optionalStat(destination);
43
+ if (destinationStat !== void 0 && (destinationStat.isSymbolicLink() || !destinationStat.isFile() || destinationStat.nlink !== 1)) throw new Error("cloud setup destination must be a single-link regular repository file");
44
+ const output = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW, 493);
45
+ try {
46
+ const opened = await output.stat();
47
+ if (!opened.isFile() || opened.nlink !== 1) throw new Error("cloud setup destination must be a single-link regular repository file");
48
+ await output.truncate(0);
49
+ await output.writeFile(script, "utf8");
50
+ await output.chmod(493);
51
+ } finally {
52
+ await output.close();
53
+ }
54
+ process.stdout.write(`Generated ${cloudSetupPath}\n`);
55
+ };
56
+ main().catch((error) => {
57
+ process.stderr.write(`cloud-setup: ${error instanceof Error ? error.message : String(error)}\n`);
58
+ process.exitCode = 1;
59
+ });
60
+ //#endregion
61
+ export {};
62
+
63
+ //# sourceMappingURL=cloud-setup-cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-setup-cli.mjs","names":[],"sources":["../src/cloud-setup-cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { constants } from \"node:fs\";\nimport { lstat, mkdir, open, readFile, realpath } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { cloudSetupPath, generateCloudSetup } from \"./cloud-setup\";\nimport {\n parseToolchainConfiguration,\n readToolchainInputs,\n} from \"./toolchain-guard\";\nimport { parseToolchainPolicy } from \"./toolchain-schema\";\n\nconst usage =\n \"Usage: stll-cloud-setup (run from repository root)\\n stll-cloud-setup --help\";\nconst optionalStat = async (path: string) => {\n try {\n return await lstat(path);\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\")\n return undefined;\n throw error;\n }\n};\n\nconst main = async () => {\n const args = process.argv.slice(2);\n if (args.length === 1 && args.at(0) === \"--help\") {\n process.stdout.write(`${usage}\\n`);\n return;\n }\n if (args.length !== 0) throw new Error(usage);\n const root = await realpath(process.cwd());\n const { files, diagnostics } = readToolchainInputs(root);\n if (diagnostics.length > 0)\n throw new Error(\n diagnostics.map(({ path, message }) => `${path}: ${message}`).join(\"\\n\"),\n );\n const text = files[\"stll-toolchain.json\"];\n if (text === undefined)\n throw new Error(\"requires tracked stll-toolchain.json\");\n const { cloud } = parseToolchainConfiguration(JSON.parse(text));\n if (cloud === undefined)\n throw new Error(\"requires an explicit cloud declaration\");\n const nodeVersion = files[\".node-version\"];\n if (nodeVersion === undefined)\n throw new Error(\"requires tracked root .node-version\");\n const policy = parseToolchainPolicy(\n JSON.parse(\n await readFile(new URL(\"../toolchain.json\", import.meta.url), \"utf8\"),\n ),\n );\n const script = generateCloudSetup({\n policy,\n cloud,\n nodeVersion: nodeVersion.trim(),\n });\n const destination = join(root, cloudSetupPath);\n const parent = dirname(destination);\n const parentStat = await optionalStat(parent);\n if (parentStat === undefined) await mkdir(parent);\n else if (parentStat.isSymbolicLink() || !parentStat.isDirectory())\n throw new Error(\"cloud setup parent must be a real repository directory\");\n const destinationStat = await optionalStat(destination);\n if (\n destinationStat !== undefined &&\n (destinationStat.isSymbolicLink() ||\n !destinationStat.isFile() ||\n destinationStat.nlink !== 1)\n )\n throw new Error(\n \"cloud setup destination must be a single-link regular repository file\",\n );\n const output = await open(\n destination,\n constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW,\n 0o755,\n );\n try {\n const opened = await output.stat();\n if (!opened.isFile() || opened.nlink !== 1)\n throw new Error(\n \"cloud setup destination must be a single-link regular repository file\",\n );\n await output.truncate(0);\n await output.writeFile(script, \"utf8\");\n await output.chmod(0o755);\n } finally {\n await output.close();\n }\n process.stdout.write(`Generated ${cloudSetupPath}\\n`);\n};\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `cloud-setup: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;AAYA,MAAM,QACJ;AACF,MAAM,eAAe,OAAO,SAAiB;CAC3C,IAAI;EACF,OAAO,MAAM,MAAM,IAAI;CACzB,SAAS,OAAO;EACd,IAAI,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,UAC9D,OAAO,KAAA;EACT,MAAM;CACR;AACF;AAEA,MAAM,OAAO,YAAY;CACvB,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;CACjC,IAAI,KAAK,WAAW,KAAK,KAAK,GAAG,CAAC,MAAM,UAAU;EAChD,QAAQ,OAAO,MAAM,GAAG,MAAM,GAAG;EACjC;CACF;CACA,IAAI,KAAK,WAAW,GAAG,MAAM,IAAI,MAAM,KAAK;CAC5C,MAAM,OAAO,MAAM,SAAS,QAAQ,IAAI,CAAC;CACzC,MAAM,EAAE,OAAO,gBAAgB,oBAAoB,IAAI;CACvD,IAAI,YAAY,SAAS,GACvB,MAAM,IAAI,MACR,YAAY,KAAK,EAAE,MAAM,cAAc,GAAG,KAAK,IAAI,SAAS,CAAC,CAAC,KAAK,IAAI,CACzE;CACF,MAAM,OAAO,MAAM;CACnB,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,sCAAsC;CACxD,MAAM,EAAE,UAAU,4BAA4B,KAAK,MAAM,IAAI,CAAC;CAC9D,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,wCAAwC;CAC1D,MAAM,cAAc,MAAM;CAC1B,IAAI,gBAAgB,KAAA,GAClB,MAAM,IAAI,MAAM,qCAAqC;CAMvD,MAAM,SAAS,mBAAmB;EAChC,QANa,qBACb,KAAK,MACH,MAAM,SAAS,IAAI,IAAI,qBAAqB,OAAO,KAAK,GAAG,GAAG,MAAM,CACtE,CAGK;EACL;EACA,aAAa,YAAY,KAAK;CAChC,CAAC;CACD,MAAM,cAAc,KAAK,MAAM,cAAc;CAC7C,MAAM,SAAS,QAAQ,WAAW;CAClC,MAAM,aAAa,MAAM,aAAa,MAAM;CAC5C,IAAI,eAAe,KAAA,GAAW,MAAM,MAAM,MAAM;MAC3C,IAAI,WAAW,eAAe,KAAK,CAAC,WAAW,YAAY,GAC9D,MAAM,IAAI,MAAM,wDAAwD;CAC1E,MAAM,kBAAkB,MAAM,aAAa,WAAW;CACtD,IACE,oBAAoB,KAAA,MACnB,gBAAgB,eAAe,KAC9B,CAAC,gBAAgB,OAAO,KACxB,gBAAgB,UAAU,IAE5B,MAAM,IAAI,MACR,uEACF;CACF,MAAM,SAAS,MAAM,KACnB,aACA,UAAU,WAAW,UAAU,UAAU,UAAU,YACnD,GACF;CACA,IAAI;EACF,MAAM,SAAS,MAAM,OAAO,KAAK;EACjC,IAAI,CAAC,OAAO,OAAO,KAAK,OAAO,UAAU,GACvC,MAAM,IAAI,MACR,uEACF;EACF,MAAM,OAAO,SAAS,CAAC;EACvB,MAAM,OAAO,UAAU,QAAQ,MAAM;EACrC,MAAM,OAAO,MAAM,GAAK;CAC1B,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;CACA,QAAQ,OAAO,MAAM,aAAa,eAAe,GAAG;AACtD;AAEA,KAAK,CAAC,CAAC,OAAO,UAAmB;CAC/B,QAAQ,OAAO,MACb,gBAAgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GACzE;CACA,QAAQ,WAAW;AACrB,CAAC"}
@@ -1,209 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- //#region \0rolldown/runtime.js
4
- var __create = Object.create;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
- var __getOwnPropNames = Object.getOwnPropertyNames;
8
- var __getProtoOf = Object.getPrototypeOf;
9
- var __hasOwnProp = Object.prototype.hasOwnProperty;
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
- key = keys[i];
13
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
- get: ((k) => from[k]).bind(null, key),
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
- });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
- value: mod,
22
- enumerable: true
23
- }) : target, mod));
24
- //#endregion
3
+ const require_toolchain_guard = require("./toolchain-guard.cjs");
25
4
  let node_fs = require("node:fs");
26
- let node_child_process = require("node:child_process");
27
- let node_path = require("node:path");
28
- node_path = __toESM(node_path, 1);
29
- //#region src/toolchain-check.ts
30
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
31
- const scalar = (value) => value.trim().replace(/\s+#.*$/, "").replace(/^["']|["']$/g, "");
32
- const trackedPath = (file) => !file.split("/").some((part) => part === "node_modules" || part === "vendor");
33
- /** Check tracked consumer files without traversing dependencies or the working tree. */
34
- const checkToolchain = ({ root, bunVersion, allowNonBunPackageManagers = [] }) => {
35
- const files = (0, node_child_process.execFileSync)("git", [
36
- "ls-files",
37
- "-z",
38
- "--cached"
39
- ], {
40
- cwd: root,
41
- encoding: "utf8"
42
- }).split("\0").filter((file) => file !== "" && trackedPath(file));
43
- const tracked = new Set(files);
44
- const diagnostics = [];
45
- const add = (file, line, message) => {
46
- diagnostics.push({
47
- path: file,
48
- line,
49
- message
50
- });
51
- };
52
- const read = (file) => {
53
- try {
54
- return (0, node_fs.readFileSync)(node_path.default.join(root, file), "utf8");
55
- } catch {
56
- add(file, 1, "cannot read tracked file");
57
- return;
58
- }
59
- };
60
- const checkVersion = (file, line, value, label) => {
61
- if (value !== bunVersion) add(file, line, `${label} must be ${bunVersion}, found ${String(value)}`);
62
- };
63
- const packages = /* @__PURE__ */ new Map();
64
- for (const file of files.filter((entry) => node_path.default.basename(entry) === "package.json")) {
65
- const text = read(file);
66
- if (text === void 0) continue;
67
- let json;
68
- try {
69
- json = JSON.parse(text);
70
- } catch {
71
- add(file, 1, "invalid package.json");
72
- continue;
73
- }
74
- if (!isRecord(json)) {
75
- add(file, 1, "package.json must contain an object");
76
- continue;
77
- }
78
- packages.set(file, {
79
- manager: json["packageManager"],
80
- json,
81
- text
82
- });
83
- }
84
- const catalogSource = (json) => isRecord(json["workspaces"]) ? json["workspaces"] : json;
85
- const rootCatalogs = catalogSource(packages.get("package.json")?.json ?? {});
86
- for (const [file, { json, text }] of packages) {
87
- const lines = text.split(/\r?\n/);
88
- const reportedLines = /* @__PURE__ */ new Map();
89
- const propertyLine = (key, value) => {
90
- const serialized = value === void 0 ? void 0 : JSON.stringify(value);
91
- const matchKey = `${key}:${serialized ?? ""}`;
92
- const previous = reportedLines.get(matchKey) ?? -1;
93
- const index = lines.findIndex((line, lineIndex) => lineIndex > previous && line.includes(`"${key}"`) && (serialized === void 0 || line.includes(serialized)));
94
- if (index >= 0) reportedLines.set(matchKey, index);
95
- return Math.max(1, index + 1);
96
- };
97
- if (json["packageManager"] !== void 0) {
98
- if (typeof json["packageManager"] === "string" && json["packageManager"].startsWith("bun@")) checkVersion(file, propertyLine("packageManager"), json["packageManager"].slice(4), "packageManager Bun version");
99
- else if (!allowNonBunPackageManagers.includes(file)) add(file, propertyLine("packageManager"), "packageManager is not Bun; use --allow-non-bun-package-manager with this path to allow it");
100
- }
101
- const checkDependencies = (value, resolveCatalog = false) => {
102
- if (!isRecord(value) || value["bun-types"] === void 0) return;
103
- const specifier = value["bun-types"];
104
- const resolveVersion = () => {
105
- if (!resolveCatalog || typeof specifier !== "string" || !specifier.startsWith("catalog:")) return specifier;
106
- const catalogName = specifier.slice(8);
107
- const source = file === "package.json" ? catalogSource(json) : rootCatalogs;
108
- const catalog = catalogName === "" ? source["catalog"] : isRecord(source["catalogs"]) && source["catalogs"][catalogName];
109
- return isRecord(catalog) ? catalog["bun-types"] : void 0;
110
- };
111
- checkVersion(file, propertyLine("bun-types", specifier), resolveVersion(), "bun-types");
112
- };
113
- checkDependencies(json["dependencies"], true);
114
- checkDependencies(json["devDependencies"], true);
115
- checkDependencies(json["catalog"]);
116
- if (isRecord(json["catalogs"])) for (const catalog of Object.values(json["catalogs"])) checkDependencies(catalog);
117
- if (isRecord(json["workspaces"])) {
118
- checkDependencies(json["workspaces"]["catalog"]);
119
- if (isRecord(json["workspaces"]["catalogs"])) for (const catalog of Object.values(json["workspaces"]["catalogs"])) checkDependencies(catalog);
120
- }
121
- }
122
- for (const file of files) {
123
- const name = node_path.default.basename(file);
124
- const workflow = /^\.github\/workflows\/[^/]+\.ya?ml$/.test(file) || /^\.github\/actions\/.+\/action\.ya?ml$/.test(file);
125
- const dockerfile = name.startsWith("Dockerfile") || name === "Containerfile";
126
- const versionFile = name === ".bun-version" || name === ".tool-versions";
127
- const mise = name === "mise.toml" || name === ".mise.toml";
128
- if (!workflow && !dockerfile && !versionFile && !mise) continue;
129
- const text = read(file);
130
- if (text === void 0) continue;
131
- if (name === ".bun-version") checkVersion(file, 1, scalar(text), ".bun-version");
132
- let section = "";
133
- for (const [index, original] of text.split(/\r?\n/).entries()) {
134
- const line = index + 1;
135
- if (/^\s*#/.test(original) || original.trim() === "") continue;
136
- if (workflow) {
137
- if (original.match(/(?:^\s*(?:-\s*)?|[{,]\s*)["']?bun-version["']?\s*:/) !== null) add(file, line, "bun-version literals are forbidden; use bun-version-file pointing to a package.json with the shared Bun pin");
138
- const reference = original.match(/(?:^\s*(?:-\s*)?|[{,]\s*)["']?bun-version-file["']?\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,}]*))/);
139
- if (reference !== null) {
140
- const target = scalar(reference[1] ?? reference[2] ?? reference[3] ?? "");
141
- const normalized = node_path.default.posix.normalize(target);
142
- const pkg = packages.get(normalized);
143
- if (target === "" || node_path.default.posix.isAbsolute(target) || normalized.startsWith("../") || !tracked.has(normalized) || node_path.default.basename(normalized) !== "package.json" || pkg?.manager !== `bun@${bunVersion}`) add(file, line, `bun-version-file must reference a tracked package.json with packageManager bun@${bunVersion}, found ${target}`);
144
- }
145
- }
146
- if (dockerfile) {
147
- const image = original.match(/^\s*FROM\s+(?:--\S+\s+)*oven\/bun(?::([^\s@]+))?(?:@\S+)?(?:\s|$)/i);
148
- if (image !== null) {
149
- const tag = image[1];
150
- if (tag !== void 0) {
151
- const version = tag.match(/^(\d+\.\d+\.\d+)(?:-|$)/)?.[1];
152
- checkVersion(file, line, version ?? tag, "oven/bun version");
153
- } else if (!/oven\/bun@/i.test(original)) add(file, line, `oven/bun must use a ${bunVersion} version tag`);
154
- }
155
- }
156
- if (name === ".tool-versions") {
157
- const entry = original.match(/^\s*bun\s+(.+)$/);
158
- if (entry !== null) checkVersion(file, line, scalar(entry[1] ?? ""), ".tool-versions Bun version");
159
- }
160
- if (mise) {
161
- const heading = original.match(/^\s*\[([^\]]+)\]/);
162
- if (heading !== null) section = heading[1] ?? "";
163
- const checkMiseVersion = (value) => {
164
- const inlineVersion = [...value.matchAll(/[,{]\s*([\w-]+|"[^"]+"|'[^']+')\s*=\s*("(?:\\.|[^"\\])*"|'[^']*'|[^,{}]+)\s*(?=[,}])/g)].find((field) => scalar(field[1] ?? "") === "version");
165
- checkVersion(file, line, scalar(inlineVersion?.[2] ?? value), "mise Bun version");
166
- };
167
- const entry = original.match(/^\s*["']?bun["']?\s*=\s*(.+)$/);
168
- if (entry !== null && section === "tools") checkMiseVersion(entry[1] ?? "");
169
- const dotted = original.match(/^\s*tools\.bun(?:\.version)?\s*=\s*(.+)$/);
170
- if (dotted !== null && section === "") checkMiseVersion(dotted[1] ?? "");
171
- const tableVersion = original.match(/^\s*version\s*=\s*(.+)$/);
172
- if (tableVersion !== null && section === "tools.bun") checkMiseVersion(tableVersion[1] ?? "");
173
- const inlineTools = original.match(/^\s*tools\s*=\s*\{(.*)\}/);
174
- if (inlineTools !== null && section === "") {
175
- const inlineBun = inlineTools[1]?.match(/(?:^|,)\s*["']?bun["']?\s*=\s*(\{[^}]*\}|[^,}]+)/);
176
- if (inlineBun !== null && inlineBun !== void 0) checkMiseVersion(inlineBun[1] ?? "");
177
- }
178
- }
179
- }
180
- }
181
- return diagnostics;
182
- };
183
- //#endregion
184
5
  //#region src/toolchain-check-cli.ts
185
6
  try {
186
- const allowNonBunPackageManagers = [];
187
7
  const args = process.argv.slice(2);
188
- for (let index = 0; index < args.length; index += 1) {
189
- const argument = args[index];
190
- if (argument === "--help") {
191
- process.stdout.write("Usage: stll-toolchain-check [--allow-non-bun-package-manager <tracked package.json path>]\n");
192
- process.exit(0);
193
- }
194
- const value = args.at(index + 1);
195
- if (argument !== "--allow-non-bun-package-manager" || value === void 0 || value.startsWith("--")) throw new Error(`unexpected or incomplete argument: ${argument}`);
196
- allowNonBunPackageManagers.push(value);
197
- index += 1;
8
+ if (args.length === 1 && args[0] === "--help") {
9
+ process.stdout.write("Usage: stll-toolchain-check (opt-outs: tracked stll-toolchain.json)\n");
10
+ process.exit(0);
198
11
  }
199
- const toolchain = JSON.parse((0, node_fs.readFileSync)(new URL("../toolchain.json", require("url").pathToFileURL(__filename).href), "utf8"));
200
- if (typeof toolchain !== "object" || toolchain === null || !("bun" in toolchain) || typeof toolchain.bun !== "string") throw new Error("installed toolchain.json must define a Bun version");
201
- const diagnostics = checkToolchain({
12
+ if (args.length !== 0) throw new Error(`unexpected argument: ${args.at(0)}`);
13
+ const policy = require_toolchain_guard.parseToolchainPolicy(JSON.parse((0, node_fs.readFileSync)(new URL("../toolchain.json", require("url").pathToFileURL(__filename).href), "utf8")));
14
+ const diagnostics = require_toolchain_guard.checkToolchain({
202
15
  root: process.cwd(),
203
- bunVersion: toolchain.bun,
204
- allowNonBunPackageManagers
16
+ policy,
17
+ onDelegated: ({ path, line, tool, selector, ref }) => process.stdout.write(`${path}:${line}: [runtime-delegated] setup-${tool} ${selector} delegates its version to checkout ref ${ref}\n`)
205
18
  });
206
- for (const diagnostic of diagnostics) process.stderr.write(`${diagnostic.path}:${diagnostic.line}: ${diagnostic.message}\n`);
19
+ for (const diagnostic of diagnostics) process.stderr.write(`${diagnostic.path}:${diagnostic.line}: [${diagnostic.rule}] ${diagnostic.message}\n`);
207
20
  if (diagnostics.length > 0) process.exitCode = 1;
208
21
  } catch (error) {
209
22
  process.stderr.write(`toolchain-check:1: ${error instanceof Error ? error.message : String(error)}\n`);