@stll/oxlint-config 1.0.2 → 1.2.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 +97 -4
- package/dist/cloud-setup-cli.cjs +62 -21
- package/dist/cloud-setup-cli.cjs.map +1 -1
- package/dist/cloud-setup-cli.mjs +63 -22
- package/dist/cloud-setup-cli.mjs.map +1 -1
- package/dist/consumer-compat-cli.cjs +7 -6
- package/dist/consumer-compat-cli.cjs.map +1 -1
- package/dist/consumer-compat-cli.mjs +7 -6
- package/dist/consumer-compat-cli.mjs.map +1 -1
- package/dist/git-tracked-output.cjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/publish-build-target-vite-dts-helper.mjs +1 -2
- package/dist/publish-build-target-vite-dts-helper2.cjs +5 -5
- package/dist/publish-build-target-vite-dts-helper2.cjs.map +1 -1
- package/dist/publish-contract-cli.cjs +17 -16
- package/dist/publish-contract-cli.cjs.map +1 -1
- package/dist/publish-contract-cli.mjs +6 -5
- package/dist/publish-contract-cli.mjs.map +1 -1
- package/dist/publish-contract.cjs +1 -1
- package/dist/publish-contract.cjs.map +1 -1
- package/dist/publish-contract.mjs +1 -1
- package/dist/rolldown-runtime.cjs +1 -1
- package/dist/toolchain-changed.cjs +25 -15
- package/dist/toolchain-changed.cjs.map +1 -1
- package/dist/toolchain-changed.mjs +27 -17
- package/dist/toolchain-changed.mjs.map +1 -1
- package/dist/toolchain-check-cli.cjs.map +1 -1
- package/dist/toolchain-check-cli.mjs.map +1 -1
- package/dist/toolchain-guard.cjs +72 -48
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +79 -55
- package/dist/toolchain-guard.mjs.map +1 -1
- package/dist/toolchain-inputs.cjs +803 -40
- package/dist/toolchain-inputs.cjs.map +1 -1
- package/dist/toolchain-inputs.mjs +768 -41
- package/dist/toolchain-inputs.mjs.map +1 -1
- package/dist/toolchain-schema.cjs +1 -1
- package/dist/toolchain-schema.cjs.map +1 -1
- package/dist/toolchain-schema.mjs +2 -2
- package/dist/typecheck-parity-cli.cjs.map +1 -1
- package/dist/typecheck-parity-cli.mjs.map +1 -1
- package/dist/typecheck-parity.cjs +8 -7
- package/dist/typecheck-parity.cjs.map +1 -1
- package/dist/typecheck-parity.mjs +6 -5
- package/dist/typecheck-parity.mjs.map +1 -1
- package/dist/typecheck-probe-cli.cjs +1 -1
- package/dist/typecheck-probe-cli.cjs.map +1 -1
- package/dist/typecheck-probe-cli.mjs +1 -1
- package/package.json +6 -6
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { subset, valid, validRange } from "semver";
|
|
4
|
+
import { parse as parse$1 } from "smol-toml";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import { parseDocument } from "yaml";
|
|
3
7
|
import picomatch from "picomatch";
|
|
4
8
|
//#region src/cloud-setup-install.ts
|
|
5
9
|
const packageInstall = String.raw`
|
|
@@ -35,7 +39,7 @@ apt_install() (
|
|
|
35
39
|
policy_state=installed
|
|
36
40
|
printf '%s\n' '#!/bin/sh' 'exit 101' > /usr/sbin/policy-rc.d
|
|
37
41
|
chmod 755 /usr/sbin/policy-rc.d
|
|
38
|
-
DEBIAN_FRONTEND=noninteractive
|
|
42
|
+
DEBIAN_FRONTEND=noninteractive apt_network install -y --no-install-recommends "$@"
|
|
39
43
|
)
|
|
40
44
|
|
|
41
45
|
`;
|
|
@@ -159,7 +163,7 @@ const postgresInstall = String.raw`
|
|
|
159
163
|
if [[ "$PGDG_SOURCE_STATE" == missing ]]; then
|
|
160
164
|
local key key_fingerprint
|
|
161
165
|
key="$(mktemp)"
|
|
162
|
-
|
|
166
|
+
download_file https://www.postgresql.org/media/keys/ACCC4CF8.asc "$key" 45
|
|
163
167
|
key_fingerprint="$(pgdg_key_fingerprint "$key")"
|
|
164
168
|
[[ "$key_fingerprint" == @PGDG_FINGERPRINT@ ]] || fail 'PostgreSQL signing key mismatch'
|
|
165
169
|
install -d -m 755 /usr/share/keyrings
|
|
@@ -167,7 +171,7 @@ const postgresInstall = String.raw`
|
|
|
167
171
|
rm -- "$key"
|
|
168
172
|
printf '%s\n' '@PGDG_SOURCE@' > /etc/apt/sources.list.d/stll-pgdg.list
|
|
169
173
|
fi
|
|
170
|
-
|
|
174
|
+
apt_refresh_for postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
|
|
171
175
|
apt_install postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
|
|
172
176
|
fi
|
|
173
177
|
postgres_ready || fail 'PostgreSQL toolchain is incomplete or differs from the policy major'
|
|
@@ -182,15 +186,15 @@ const valkeyInstall = String.raw`
|
|
|
182
186
|
arm64) server_url='@ARM64_SERVER_URL@'; server_sha='@ARM64_SERVER_SHA@'; tools_url='@ARM64_TOOLS_URL@'; tools_sha='@ARM64_TOOLS_SHA@' ;;
|
|
183
187
|
*) fail 'Unsupported Valkey architecture' ;;
|
|
184
188
|
esac
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
download_file "$server_url" "$scratch/valkey-server.deb" 120
|
|
190
|
+
download_file "$tools_url" "$scratch/valkey-tools.deb" 120
|
|
187
191
|
(cd "$scratch"; printf '%s %s\n' "$server_sha" valkey-server.deb "$tools_sha" valkey-tools.deb | sha256sum --strict -c -)
|
|
188
192
|
for package in valkey-server valkey-tools; do
|
|
189
193
|
[[ "$(dpkg-deb -f "$scratch/$package.deb" Package)" == "$package" ]] || fail 'Valkey artifact package mismatch'
|
|
190
194
|
[[ "$(dpkg-deb -f "$scratch/$package.deb" Version)" == '@VALKEY@-1.noble' ]] || fail 'Valkey artifact version mismatch'
|
|
191
195
|
[[ "$(dpkg-deb -f "$scratch/$package.deb" Architecture)" == "$ARCH" ]] || fail 'Valkey artifact architecture mismatch'
|
|
192
196
|
done
|
|
193
|
-
|
|
197
|
+
apt_refresh_for "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
|
|
194
198
|
apt_install "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
|
|
195
199
|
rm -rf -- "$scratch"
|
|
196
200
|
fi
|
|
@@ -254,18 +258,267 @@ const renderCloudServiceInstall = ({ services, postgres, valkey, valkeyArtifacts
|
|
|
254
258
|
return `${packageInstall}\n${services.includes("postgres") ? postgresSources.replaceAll("@PGDG_FINGERPRINT@", postgresSigningFingerprint) : ""}\n${services.length === 0 ? "prepare_state() { :; }\n" : state}\ninstall_services() {\n${user}${parts.join("")}\n :\n}\n`;
|
|
255
259
|
};
|
|
256
260
|
//#endregion
|
|
261
|
+
//#region src/cloud-setup-network.ts
|
|
262
|
+
/** Shared deadlines cover retries and package-manager subprocesses. */
|
|
263
|
+
const renderCloudNetwork = () => String.raw`
|
|
264
|
+
network_run() (
|
|
265
|
+
local host="$1" seconds="$2" status=0
|
|
266
|
+
shift 2
|
|
267
|
+
set -o pipefail
|
|
268
|
+
network_log="$(mktemp)" || return "$?"
|
|
269
|
+
trap 'rm -f -- "$network_log"' EXIT
|
|
270
|
+
trap 'exit 129' HUP
|
|
271
|
+
trap 'exit 130' INT
|
|
272
|
+
trap 'exit 143' TERM
|
|
273
|
+
if timeout --signal=TERM --kill-after=5s "$seconds"s "$@" 2>&1 | tee "$network_log"; then
|
|
274
|
+
return 0
|
|
275
|
+
else
|
|
276
|
+
# Scalar Bash array expansion reads PIPESTATUS[0] before assignment resets it.
|
|
277
|
+
status="$PIPESTATUS"
|
|
278
|
+
fi
|
|
279
|
+
[[ "$status" != 0 ]] || fail 'Cannot capture network command output'
|
|
280
|
+
if [[ "$status" == 124 || "$status" == 137 || "$status" == 28 ]] || grep -Eiq 'timed[[:space:]-]+out|time[[:space:]-]+out|timeout[[:space:]]+(was[[:space:]]+)?(reached|expired)|(connection|operation|socket)[[:space:]-]+timeout' "$network_log"; then
|
|
281
|
+
fail "download timed out: $host"
|
|
282
|
+
fi
|
|
283
|
+
return "$status"
|
|
284
|
+
)
|
|
285
|
+
download_file() {
|
|
286
|
+
local url="$1" destination="$2" seconds="$3" host
|
|
287
|
+
host="$(printf '%s\n' "$url" | cut -d/ -f3 | sed 's/.*@//')"
|
|
288
|
+
[[ "$url" == https://* && -n "$host" ]] || fail 'Downloads require an HTTPS host'
|
|
289
|
+
network_run "$host" "$seconds" curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 20 --max-time "$seconds" --retry 3 --retry-all-errors --retry-max-time "$seconds" "$url" -o "$destination"
|
|
290
|
+
}
|
|
291
|
+
apt_network() {
|
|
292
|
+
local hosts
|
|
293
|
+
# --print-uris reads configured indexes without contacting their servers.
|
|
294
|
+
hosts="$(set -o pipefail; apt-get --print-uris update | awk -F/ '/^\047https?:\/\// { host=$3; sub(/.*@/, "", host); if (!seen[host]++) { printf "%s%s", separator, host; separator="," } }')" || return "$?"
|
|
295
|
+
[[ -n "$hosts" ]] || hosts='configured APT hosts'
|
|
296
|
+
network_run "$hosts" 180 apt-get -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 -o Acquire::Retries=3 -o APT::Update::Error-Mode=any "$@"
|
|
297
|
+
}
|
|
298
|
+
apt_refresh_for() {
|
|
299
|
+
# Skip the refresh only when the install downloads nothing: then no package
|
|
300
|
+
# source is contacted and stale cached indexes cannot point at missing files.
|
|
301
|
+
local uris
|
|
302
|
+
if uris="$(DEBIAN_FRONTEND=noninteractive apt-get --print-uris -qq -y --no-install-recommends install "$@" 2>/dev/null)" && [[ -z "$uris" ]]; then
|
|
303
|
+
return 0
|
|
304
|
+
fi
|
|
305
|
+
apt_network update
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/cloud-setup-artifacts.ts
|
|
310
|
+
const wasmBindgen0126 = {
|
|
311
|
+
version: "0.2.126",
|
|
312
|
+
amd64: {
|
|
313
|
+
url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.126/wasm-bindgen-0.2.126-x86_64-unknown-linux-musl.tar.gz",
|
|
314
|
+
sha256: "064948d58e2d6c0a745216477a639ba696216d6309aaa902939d1b865b1d869d",
|
|
315
|
+
archive: "tar.gz",
|
|
316
|
+
binaries: {
|
|
317
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
318
|
+
wasm2es6js: "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm2es6js",
|
|
319
|
+
"wasm-bindgen": "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm-bindgen"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
arm64: {
|
|
323
|
+
url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.126/wasm-bindgen-0.2.126-aarch64-unknown-linux-musl.tar.gz",
|
|
324
|
+
sha256: "2245120254a9f6c9a9adf3601f3d52bb31309219e9ceab7696e74e24885c440a",
|
|
325
|
+
archive: "tar.gz",
|
|
326
|
+
binaries: {
|
|
327
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
328
|
+
wasm2es6js: "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm2es6js",
|
|
329
|
+
"wasm-bindgen": "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm-bindgen"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
/** Pinned upstream Linux artifacts, verified against their published SHA-256 sums. */
|
|
334
|
+
const cloudSetupArtifacts = {
|
|
335
|
+
"rustup-init": {
|
|
336
|
+
version: "1.29.1",
|
|
337
|
+
amd64: {
|
|
338
|
+
url: "https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init",
|
|
339
|
+
sha256: "dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71",
|
|
340
|
+
archive: "binary",
|
|
341
|
+
binaries: { "rustup-init": "rustup-init" }
|
|
342
|
+
},
|
|
343
|
+
arm64: {
|
|
344
|
+
url: "https://static.rust-lang.org/rustup/archive/1.29.1/aarch64-unknown-linux-gnu/rustup-init",
|
|
345
|
+
sha256: "15f6e4ce9f583b929c996c91562bad6d4454f3281de858b02cdfdef615fac433",
|
|
346
|
+
archive: "binary",
|
|
347
|
+
binaries: { "rustup-init": "rustup-init" }
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
"cargo-nextest": {
|
|
351
|
+
version: "0.9.148",
|
|
352
|
+
amd64: {
|
|
353
|
+
url: "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.148/cargo-nextest-0.9.148-x86_64-unknown-linux-musl.tar.gz",
|
|
354
|
+
sha256: "bf92cb81450872d960ffa13857a83084d557ec0cdc141663d263bb23ef756f97",
|
|
355
|
+
archive: "tar.gz",
|
|
356
|
+
binaries: { "cargo-nextest": "cargo-nextest" }
|
|
357
|
+
},
|
|
358
|
+
arm64: {
|
|
359
|
+
url: "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.148/cargo-nextest-0.9.148-aarch64-unknown-linux-musl.tar.gz",
|
|
360
|
+
sha256: "94676506ceaed01746d78cfe9ed124ee841b2932fd18d9d10f438a347bd9ba5e",
|
|
361
|
+
archive: "tar.gz",
|
|
362
|
+
binaries: { "cargo-nextest": "cargo-nextest" }
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
"wasm-bindgen-cli": {
|
|
366
|
+
version: "0.2.129",
|
|
367
|
+
amd64: {
|
|
368
|
+
url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.129/wasm-bindgen-0.2.129-x86_64-unknown-linux-musl.tar.gz",
|
|
369
|
+
sha256: "82d12bb940e2d4e72e0d5605387fc1b8ca179044e012b620f0ce4e7440e8320e",
|
|
370
|
+
archive: "tar.gz",
|
|
371
|
+
binaries: {
|
|
372
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
373
|
+
wasm2es6js: "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm2es6js",
|
|
374
|
+
"wasm-bindgen": "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm-bindgen"
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
arm64: {
|
|
378
|
+
url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.129/wasm-bindgen-0.2.129-aarch64-unknown-linux-musl.tar.gz",
|
|
379
|
+
sha256: "2ed4351c35dd9440308bbb02767d47ea278efe851a52465300f3c94f5b6c2a87",
|
|
380
|
+
archive: "tar.gz",
|
|
381
|
+
binaries: {
|
|
382
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
383
|
+
wasm2es6js: "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm2es6js",
|
|
384
|
+
"wasm-bindgen": "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm-bindgen"
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
uv: {
|
|
389
|
+
version: "0.13.1",
|
|
390
|
+
amd64: {
|
|
391
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-x86_64-unknown-linux-musl.tar.gz",
|
|
392
|
+
sha256: "6c86040a8f045ab9adc9e3ec4cdafa03fd00e9a988ed2bf28971c7fc2d0debd0",
|
|
393
|
+
archive: "tar.gz",
|
|
394
|
+
binaries: {
|
|
395
|
+
uv: "uv-x86_64-unknown-linux-musl/uv",
|
|
396
|
+
uvx: "uv-x86_64-unknown-linux-musl/uvx"
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
arm64: {
|
|
400
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-aarch64-unknown-linux-musl.tar.gz",
|
|
401
|
+
sha256: "345a8308e4d1835cdfaadf692836fd3137a6d45864773f05ad7bf59442b1ba16",
|
|
402
|
+
archive: "tar.gz",
|
|
403
|
+
binaries: {
|
|
404
|
+
uv: "uv-aarch64-unknown-linux-musl/uv",
|
|
405
|
+
uvx: "uv-aarch64-unknown-linux-musl/uvx"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
/** Explicit releases whose CLI must match a project's wasm-bindgen crate exactly. */
|
|
411
|
+
const wasmBindgenArtifactReleases = [wasmBindgen0126, cloudSetupArtifacts["wasm-bindgen-cli"]];
|
|
412
|
+
/** Every supported version participates in lookup and catalog validation. */
|
|
413
|
+
const cloudSetupArtifactVersions = {
|
|
414
|
+
"rustup-init": [cloudSetupArtifacts["rustup-init"]],
|
|
415
|
+
"cargo-nextest": [cloudSetupArtifacts["cargo-nextest"]],
|
|
416
|
+
"wasm-bindgen-cli": wasmBindgenArtifactReleases,
|
|
417
|
+
uv: [cloudSetupArtifacts.uv]
|
|
418
|
+
};
|
|
419
|
+
/** A new version requires an explicit, verified catalog entry; never build from source. */
|
|
420
|
+
const getCloudSetupArtifact = (tool, version) => {
|
|
421
|
+
const releases = cloudSetupArtifactVersions[tool];
|
|
422
|
+
const artifact = releases.find((release) => release.version === version);
|
|
423
|
+
if (artifact === void 0) throw new Error(`Unsupported ${tool} version ${version}; supported versions: ${releases.map((release) => release.version).join(", ")}`);
|
|
424
|
+
return artifact;
|
|
425
|
+
};
|
|
426
|
+
/** Ubuntu 24.04 packages; apt resolves them from the image's configured sources. */
|
|
427
|
+
const cloudSetupPackages = { time: {
|
|
428
|
+
version: "1.9",
|
|
429
|
+
package: "time",
|
|
430
|
+
binary: "/usr/bin/time",
|
|
431
|
+
identity: "time (GNU Time) "
|
|
432
|
+
} };
|
|
433
|
+
/** Ubuntu 24.04 ships one release per package; other versions are unsupported. */
|
|
434
|
+
const getCloudSetupPackage = (tool, version) => {
|
|
435
|
+
const release = cloudSetupPackages[tool];
|
|
436
|
+
if (release.version !== version) throw new Error(`Unsupported ${tool} version ${version}; supported versions: ${release.version}`);
|
|
437
|
+
return release;
|
|
438
|
+
};
|
|
439
|
+
//#endregion
|
|
257
440
|
//#region src/cloud-setup-schema.ts
|
|
258
441
|
const cloudServices = ["postgres", "valkey"];
|
|
442
|
+
const cloudArtifactTools = Object.keys(cloudSetupArtifacts).filter((tool) => tool !== "rustup-init" && Object.hasOwn(cloudSetupArtifacts, tool));
|
|
443
|
+
const cloudPackageTools = Object.keys(cloudSetupPackages).filter((tool) => Object.hasOwn(cloudSetupPackages, tool));
|
|
444
|
+
const cloudExtraTools = [...cloudArtifactTools, ...cloudPackageTools];
|
|
445
|
+
const isCloudPackageTool = (tool) => cloudPackageTools.some((candidate) => candidate === tool);
|
|
446
|
+
const object = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
|
|
447
|
+
const exactPin = (input) => typeof input === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(input);
|
|
448
|
+
const exactToolPin = (tool, input) => isCloudPackageTool(tool) ? typeof input === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))?$/.test(input) : exactPin(input);
|
|
449
|
+
const cloudRustComponentBinaries = {
|
|
450
|
+
clippy: ["cargo-clippy", "clippy-driver"],
|
|
451
|
+
rustfmt: ["rustfmt", "cargo-fmt"],
|
|
452
|
+
"rust-src": [],
|
|
453
|
+
"rust-analyzer": ["rust-analyzer"]
|
|
454
|
+
};
|
|
455
|
+
const parseCloudRustNames = (input, kind) => {
|
|
456
|
+
const label = `cloud Rust ${kind}`;
|
|
457
|
+
if (input === void 0) return [];
|
|
458
|
+
if (!Array.isArray(input)) throw new Error(`${label} must be an array`);
|
|
459
|
+
const result = input.map((value) => {
|
|
460
|
+
if (typeof value !== "string" || !/^[a-z][a-z0-9_-]*$/.test(value)) throw new Error(`${label} requires canonical names`);
|
|
461
|
+
if (kind === "components" && !Object.hasOwn(cloudRustComponentBinaries, value)) throw new Error(`${label} contains an unsupported component: ${value}`);
|
|
462
|
+
return value;
|
|
463
|
+
});
|
|
464
|
+
if (new Set(result).size !== result.length) throw new Error(`${label} contains duplicates`);
|
|
465
|
+
return result;
|
|
466
|
+
};
|
|
467
|
+
const parseRust = (input) => {
|
|
468
|
+
if (input === void 0) return void 0;
|
|
469
|
+
if (!object(input) || Object.keys(input).some((key) => ![
|
|
470
|
+
"toolchain",
|
|
471
|
+
"components",
|
|
472
|
+
"targets"
|
|
473
|
+
].includes(key))) throw new Error("cloud.rust accepts only toolchain, components and targets");
|
|
474
|
+
const toolchain = input["toolchain"];
|
|
475
|
+
if (toolchain !== void 0 && !exactPin(toolchain)) throw new Error("cloud.rust.toolchain must be an exact stable release");
|
|
476
|
+
return {
|
|
477
|
+
...toolchain === void 0 ? {} : { toolchain },
|
|
478
|
+
components: parseCloudRustNames(input["components"], "components"),
|
|
479
|
+
targets: parseCloudRustNames(input["targets"], "targets")
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
const parseTools = (input) => {
|
|
483
|
+
if (input === void 0) return void 0;
|
|
484
|
+
if (!object(input) || Object.keys(input).some((key) => !cloudExtraTools.some((tool) => tool === key))) throw new Error(`cloud.tools accepts only ${cloudExtraTools.join(", ")}`);
|
|
485
|
+
const result = {};
|
|
486
|
+
for (const tool of cloudExtraTools) {
|
|
487
|
+
const pin = input[tool];
|
|
488
|
+
if (pin === void 0) {
|
|
489
|
+
if (Object.hasOwn(input, tool)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!exactToolPin(tool, pin)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
|
|
493
|
+
result[tool] = pin;
|
|
494
|
+
}
|
|
495
|
+
return result;
|
|
496
|
+
};
|
|
259
497
|
const cloudInstallCommand = "bun install --frozen-lockfile";
|
|
498
|
+
const cloudInstallCommands = [cloudInstallCommand, `${cloudInstallCommand} --ignore-scripts`];
|
|
499
|
+
const cloudInstall = (value) => cloudInstallCommands.some((command) => value === command);
|
|
260
500
|
const cloudService = (value) => cloudServices.some((service) => value === service);
|
|
501
|
+
const parseEnvFile = (input) => {
|
|
502
|
+
if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
503
|
+
const envFile = input.envFile;
|
|
504
|
+
if (envFile.startsWith("/") || /^[A-Za-z]:/.test(envFile) || envFile.includes("\\") || /\p{Cc}/u.test(envFile) || envFile.split("/").some((part) => part === "" || part === "." || part === "..")) throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
505
|
+
return envFile;
|
|
506
|
+
};
|
|
261
507
|
/** Parse an explicit declaration without assuming services for undeclared repositories. */
|
|
262
508
|
const parseCloudSetup = (input) => {
|
|
263
509
|
if (input === void 0) return void 0;
|
|
264
510
|
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("cloud must be an object");
|
|
265
|
-
for (const key of Object.keys(input)) if (key !== "services" && key !== "install" && key !== "envFile") throw new Error(`unsupported cloud key: ${key}`);
|
|
511
|
+
for (const key of Object.keys(input)) if (key !== "services" && key !== "install" && key !== "envFile" && key !== "rust" && key !== "tools") throw new Error(`unsupported cloud key: ${key}`);
|
|
512
|
+
const rust = parseRust("rust" in input ? input.rust : void 0);
|
|
513
|
+
const tools = parseTools("tools" in input ? input.tools : void 0);
|
|
514
|
+
const extensions = {
|
|
515
|
+
...rust === void 0 ? {} : { rust },
|
|
516
|
+
...tools === void 0 ? {} : { tools }
|
|
517
|
+
};
|
|
266
518
|
const selected = "services" in input ? input.services : [];
|
|
267
519
|
if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
|
|
268
|
-
if (!("install" in input) || input.install
|
|
520
|
+
if (!("install" in input) || !cloudInstall(input.install)) throw new Error(`cloud.install must be exactly one of: ${cloudInstallCommands.join(", ")}`);
|
|
521
|
+
const install = input.install;
|
|
269
522
|
const seen = /* @__PURE__ */ new Set();
|
|
270
523
|
const services = Array.from(selected, (service) => {
|
|
271
524
|
if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
|
|
@@ -273,20 +526,12 @@ const parseCloudSetup = (input) => {
|
|
|
273
526
|
seen.add(service);
|
|
274
527
|
return service;
|
|
275
528
|
});
|
|
276
|
-
if (services.length === 0)
|
|
277
|
-
if ("envFile" in input) throw new Error("cloud.envFile is not allowed without services");
|
|
278
|
-
return {
|
|
279
|
-
services,
|
|
280
|
-
install: cloudInstallCommand
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
284
|
-
const envFile = input.envFile;
|
|
285
|
-
if (envFile.startsWith("/") || /^[A-Za-z]:/.test(envFile) || envFile.includes("\\") || /\p{Cc}/u.test(envFile) || envFile.split("/").some((part) => part === "" || part === "." || part === "..")) throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
529
|
+
if (services.length === 0 && "envFile" in input) throw new Error("cloud.envFile is not allowed without services");
|
|
286
530
|
return {
|
|
287
531
|
services,
|
|
288
|
-
install
|
|
289
|
-
|
|
532
|
+
install,
|
|
533
|
+
...extensions,
|
|
534
|
+
...services.length === 0 ? {} : { envFile: parseEnvFile(input) }
|
|
290
535
|
};
|
|
291
536
|
};
|
|
292
537
|
//#endregion
|
|
@@ -353,7 +598,7 @@ service_prune_pid() {
|
|
|
353
598
|
}
|
|
354
599
|
`;
|
|
355
600
|
const postgresStart = String.raw`
|
|
356
|
-
local pg_bin='/usr/lib/postgresql/@POSTGRES@/bin' pg_data="$STATE/postgres" pg_password pg_details pg_pid pg_version
|
|
601
|
+
local pg_bin='/usr/lib/postgresql/@POSTGRES@/bin' pg_data="$STATE/postgres" pg_password pg_details pg_pid pg_version pg_state='already running'
|
|
357
602
|
service_password "$STATE/postgres.password"
|
|
358
603
|
pg_password="$(cat "$STATE/postgres.password")"
|
|
359
604
|
service_directory "$pg_data"
|
|
@@ -372,6 +617,7 @@ const postgresStart = String.raw`
|
|
|
372
617
|
service_prune_pid "$pg_data/postmaster.pid" "$pg_bin/postgres" postgres
|
|
373
618
|
if ! service_run env PGHOST=127.0.0.1 PGHOSTADDR=127.0.0.1 "$pg_bin/pg_ctl" -D "$pg_data" status >/dev/null 2>&1; then
|
|
374
619
|
service_run env PGHOST=127.0.0.1 PGHOSTADDR=127.0.0.1 "$pg_bin/pg_ctl" -D "$pg_data" -l "$pg_data/cloud.log" -o "-h 127.0.0.1 -p 55432 -c unix_socket_directories=" -t 10 -w start >/dev/null || fail "PostgreSQL start failed; port 55432 must be available"
|
|
620
|
+
pg_state=started
|
|
375
621
|
fi
|
|
376
622
|
pg_details="$(env -u PGSERVICE -u PGSERVICEFILE -u PGOPTIONS PGHOSTADDR=127.0.0.1 PGPASSWORD="$pg_password" PGCONNECT_TIMEOUT=2 "$pg_bin/psql" -X -h 127.0.0.1 -p 55432 -U stll_cloud -d postgres -At -v ON_ERROR_STOP=1 -c "SELECT current_setting('data_directory'), current_setting('listen_addresses'), current_setting('port'), current_setting('server_version_num'), current_setting('unix_socket_directories')")" || fail "PostgreSQL authenticated readiness failed"
|
|
377
623
|
pg_version="$(printf '%s\n' "$pg_details" | cut -d '|' -f4)"
|
|
@@ -381,9 +627,10 @@ const postgresStart = String.raw`
|
|
|
381
627
|
service_process "$pg_pid" "$pg_bin/postgres"
|
|
382
628
|
[[ "$(env -u PGSERVICE -u PGSERVICEFILE -u PGOPTIONS PGHOSTADDR=127.0.0.1 PGPASSWORD="$pg_password" PGCONNECT_TIMEOUT=2 "$pg_bin/psql" -X -h 127.0.0.1 -p 55432 -U stll_cloud -d postgres -At -v ON_ERROR_STOP=1 -c "SELECT count(*) FROM pg_database WHERE datname = 'cloud_test'")" == 1 ]] || env -u PGSERVICE -u PGSERVICEFILE -u PGOPTIONS PGHOSTADDR=127.0.0.1 PGPASSWORD="$pg_password" PGCONNECT_TIMEOUT=2 "$pg_bin/createdb" -h 127.0.0.1 -p 55432 -U stll_cloud cloud_test || fail "PostgreSQL test database creation failed"
|
|
383
629
|
DATABASE_URL="postgresql://stll_cloud:$pg_password@127.0.0.1:55432/cloud_test"
|
|
630
|
+
printf 'postgres: %s\n' "$pg_state"
|
|
384
631
|
`;
|
|
385
632
|
const valkeyStart = String.raw`
|
|
386
|
-
local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0
|
|
633
|
+
local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0 vk_state='already running'
|
|
387
634
|
service_password "$STATE/valkey.password"
|
|
388
635
|
vk_password="$(cat "$STATE/valkey.password")"
|
|
389
636
|
service_directory "$vk_data"
|
|
@@ -403,7 +650,8 @@ const valkeyStart = String.raw`
|
|
|
403
650
|
[[ "$(cat "$vk_config")" == "$vk_expected" ]] || fail "Valkey configuration differs from managed settings"
|
|
404
651
|
service_prune_pid "$vk_data/valkey.pid" "$(command -v valkey-server)" valkey
|
|
405
652
|
if [[ ! -e "$vk_data/valkey.pid" ]]; then
|
|
406
|
-
service_run valkey-server "$vk_config" || fail "Valkey start failed; port 56379 must be available"
|
|
653
|
+
service_run valkey-server "$vk_config" >/dev/null || fail "Valkey start failed; port 56379 must be available"
|
|
654
|
+
vk_state=started
|
|
407
655
|
fi
|
|
408
656
|
for ((attempt=0; attempt<20; attempt++)); do
|
|
409
657
|
if vk_info="$(VALKEYCLI_AUTH="$vk_password" timeout 0.2 valkey-cli -h 127.0.0.1 -p 56379 --raw INFO server 2>/dev/null)"; then
|
|
@@ -419,6 +667,7 @@ const valkeyStart = String.raw`
|
|
|
419
667
|
[[ "$(VALKEYCLI_AUTH="$vk_password" timeout 2 valkey-cli -h 127.0.0.1 -p 56379 --raw CONFIG GET bind)" == $'bind\n127.0.0.1' ]] || fail "Valkey endpoint must bind only loopback"
|
|
420
668
|
[[ "$(VALKEYCLI_AUTH="$vk_password" timeout 2 valkey-cli -h 127.0.0.1 -p 56379 --raw CONFIG GET dir)" == "$(printf 'dir\n%s' "$vk_data")" ]] || fail "Valkey endpoint has a different data directory"
|
|
421
669
|
REDIS_URL="redis://:$vk_password@127.0.0.1:56379/0"
|
|
670
|
+
printf 'valkey: %s\n' "$vk_state"
|
|
422
671
|
`;
|
|
423
672
|
/** Render only declared services; installation and environment-file ownership stay with the caller. */
|
|
424
673
|
const renderCloudServiceStart = ({ services, postgres, valkey }) => {
|
|
@@ -434,6 +683,300 @@ const renderCloudServiceStart = ({ services, postgres, valkey }) => {
|
|
|
434
683
|
return `${common}\nstart_services() {\n local SERVICE_UID file attempt\n SERVICE_UID="$(id -u stll-cloud)"\n [[ -d "$STATE" && ! -L "$STATE" && "$(stat -c '%u' "$STATE")" == 0 ]] || fail "Invalid cloud service state directory"\n${bodies.join("")}\n}\n`;
|
|
435
684
|
};
|
|
436
685
|
//#endregion
|
|
686
|
+
//#region src/cloud-setup-shell.ts
|
|
687
|
+
const cloudShellProfilePath = "/etc/profile.d/stll-cloud.sh";
|
|
688
|
+
const cloudShellProfileSource = `. ${cloudShellProfilePath}`;
|
|
689
|
+
const quote$1 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
690
|
+
const renderCloudShellProfile = ({ nodeVersion, bunVersion, rust }) => {
|
|
691
|
+
const node = `/opt/stll-cloud/node/${nodeVersion}/bin`;
|
|
692
|
+
const bun = `/opt/stll-cloud/bun/${bunVersion}`;
|
|
693
|
+
return `# Generated by stll-cloud-setup; managed cloud shell environment.
|
|
694
|
+
# Remove managed entries before prepending, including repeated shell startup.
|
|
695
|
+
_stll_cloud_remaining="\${PATH-}"
|
|
696
|
+
_stll_cloud_result=''
|
|
697
|
+
_stll_cloud_separator=''
|
|
698
|
+
while :; do
|
|
699
|
+
case "$_stll_cloud_remaining" in
|
|
700
|
+
*:*) _stll_cloud_part="\${_stll_cloud_remaining%%:*}"; _stll_cloud_remaining="\${_stll_cloud_remaining#*:}"; _stll_cloud_more=yes ;;
|
|
701
|
+
*) _stll_cloud_part="$_stll_cloud_remaining"; _stll_cloud_more=no ;;
|
|
702
|
+
esac
|
|
703
|
+
case "$_stll_cloud_part" in
|
|
704
|
+
${quote$1(node)}|${quote$1(bun)}|/usr/local/bin) ;;
|
|
705
|
+
*) _stll_cloud_result="$_stll_cloud_result$_stll_cloud_separator$_stll_cloud_part"; _stll_cloud_separator=: ;;
|
|
706
|
+
esac
|
|
707
|
+
[ "$_stll_cloud_more" = yes ] || break
|
|
708
|
+
done
|
|
709
|
+
export PATH=${quote$1(`${node}:${bun}:/usr/local/bin`)}"\${_stll_cloud_separator:+:}$_stll_cloud_result"
|
|
710
|
+
unset _stll_cloud_remaining _stll_cloud_result _stll_cloud_separator _stll_cloud_part _stll_cloud_more
|
|
711
|
+
${rust === void 0 ? "" : `export RUSTUP_HOME=/opt/stll-cloud/rustup\nexport CARGO_HOME=/opt/stll-cloud/cargo\nexport RUSTUP_TOOLCHAIN=${quote$1(rust.channel)}\nexport RUSTUP_AUTO_INSTALL=0\n`}`;
|
|
712
|
+
};
|
|
713
|
+
const userStartup = String.raw`
|
|
714
|
+
set -euo pipefail
|
|
715
|
+
for name in .bashrc .profile .bash_profile .bash_login; do
|
|
716
|
+
file="$HOME/$name"
|
|
717
|
+
if [[ ! -e "$file" && ! -L "$file" && "$name" != .bashrc && "$name" != .profile ]]; then continue; fi
|
|
718
|
+
[[ ! -L "$file" ]] || { echo "cloud-setup: shell startup file must not be a symlink: $name" >&2; exit 1; }
|
|
719
|
+
if [[ -e "$file" ]]; then
|
|
720
|
+
[[ -f "$file" && "$(stat -c '%h:%u' "$file")" == "1:$(id -u)" ]] || { echo "cloud-setup: shell startup file must be owned and single-link: $name" >&2; exit 1; }
|
|
721
|
+
fi
|
|
722
|
+
temporary="$(mktemp "$HOME/.stll-cloud-shell.XXXXXX")"
|
|
723
|
+
trap 'rm -f -- "$temporary"' EXIT
|
|
724
|
+
if [[ "$name" == .bashrc ]]; then
|
|
725
|
+
printf '%s\n' '# stll-cloud: early environment' '@SOURCE@' > "$temporary"
|
|
726
|
+
fi
|
|
727
|
+
if [[ -f "$file" ]]; then
|
|
728
|
+
# Remove only exact managed blocks; preserve unrelated startup lines and permissions.
|
|
729
|
+
awk '
|
|
730
|
+
$0 == "# stll-cloud: early environment" || $0 == "# stll-cloud: session environment" {
|
|
731
|
+
if ((getline following) <= 0 || following != "@SOURCE@") exit 1
|
|
732
|
+
next
|
|
733
|
+
}
|
|
734
|
+
{ print }
|
|
735
|
+
' "$file" >> "$temporary" || { echo "cloud-setup: managed shell startup block differs: $name" >&2; exit 1; }
|
|
736
|
+
fi
|
|
737
|
+
printf '%s\n' '# stll-cloud: session environment' '@SOURCE@' >> "$temporary"
|
|
738
|
+
if [[ -f "$file" ]] && cmp -s "$temporary" "$file"; then rm -- "$temporary"; continue; fi
|
|
739
|
+
if [[ -f "$file" ]]; then chmod --reference="$file" "$temporary"; else chmod 644 "$temporary"; fi
|
|
740
|
+
mv -f -- "$temporary" "$file"
|
|
741
|
+
done
|
|
742
|
+
`;
|
|
743
|
+
const renderCloudShellInstall = (options) => {
|
|
744
|
+
const profile = renderCloudShellProfile(options);
|
|
745
|
+
return `install_shell_environment() (
|
|
746
|
+
# Subshell scope retains temporary for EXIT cleanup after a failed command.
|
|
747
|
+
[[ ! -L ${quote$1(cloudShellProfilePath)} ]] || fail 'Managed shell profile must not be a symlink'
|
|
748
|
+
if [[ -e ${quote$1(cloudShellProfilePath)} ]]; then
|
|
749
|
+
[[ -f ${quote$1(cloudShellProfilePath)} && "$(stat -c '%h:%u' ${quote$1(cloudShellProfilePath)})" == 1:0 ]] || fail 'Managed shell profile must be a root-owned single-link file'
|
|
750
|
+
fi
|
|
751
|
+
temporary="$(mktemp)"
|
|
752
|
+
trap 'rm -f -- "$temporary"' EXIT
|
|
753
|
+
cat > "$temporary" <<'STLL_CLOUD_PROFILE'
|
|
754
|
+
${profile}STLL_CLOUD_PROFILE
|
|
755
|
+
if ! cmp -s "$temporary" ${quote$1(cloudShellProfilePath)}; then install -m 644 "$temporary" ${quote$1(cloudShellProfilePath)}; fi
|
|
756
|
+
runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" bash -c ${quote$1(userStartup.replaceAll("@SOURCE@", cloudShellProfileSource))}
|
|
757
|
+
)
|
|
758
|
+
verify_shell_environment() {
|
|
759
|
+
[[ -f ${quote$1(cloudShellProfilePath)} && ! -L ${quote$1(cloudShellProfilePath)} && "$(stat -c '%h:%u' ${quote$1(cloudShellProfilePath)})" == 1:0 ]] || fail 'Managed shell environment is unavailable; run install'
|
|
760
|
+
[[ "$(sha256sum -- ${quote$1(cloudShellProfilePath)} | cut -d ' ' -f1)" == ${quote$1(createHash("sha256").update(profile).digest("hex"))} ]] || fail 'Managed shell environment differs; run install'
|
|
761
|
+
}
|
|
762
|
+
`;
|
|
763
|
+
};
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/cloud-setup-toolchains.ts
|
|
766
|
+
const record$2 = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
|
|
767
|
+
/** Resolve the same tracked snapshot for generation and the mandatory drift check. */
|
|
768
|
+
const resolveCloudToolchains = ({ cloud, files, rust, rustExceptions }) => {
|
|
769
|
+
const source = files["rust-toolchain.toml"];
|
|
770
|
+
const selector = files["rust-toolchain"];
|
|
771
|
+
if (source !== void 0 && selector !== void 0) throw new Error("cloud Rust requires one root rust-toolchain.toml or rust-toolchain file");
|
|
772
|
+
let declared;
|
|
773
|
+
if (source !== void 0 || selector?.trimStart().startsWith("[")) {
|
|
774
|
+
const parsed = parse$1(source ?? selector ?? "");
|
|
775
|
+
if (!record$2(parsed["toolchain"])) throw new Error("cloud Rust requires [toolchain]");
|
|
776
|
+
declared = parsed["toolchain"];
|
|
777
|
+
if (Object.keys(declared).some((key) => ![
|
|
778
|
+
"channel",
|
|
779
|
+
"components",
|
|
780
|
+
"targets",
|
|
781
|
+
"profile"
|
|
782
|
+
].includes(key))) throw new Error("cloud Rust supports only channel, components, targets and profile");
|
|
783
|
+
if (declared["profile"] !== void 0 && declared["profile"] !== "minimal") throw new Error("cloud Rust requires profile = minimal; declare required components explicitly");
|
|
784
|
+
} else if (selector !== void 0) declared = { channel: selector.trim() };
|
|
785
|
+
const rustEnabled = declared !== void 0 || cloud.rust !== void 0;
|
|
786
|
+
const channel = declared === void 0 ? cloud.rust?.toolchain ?? rust : declared["channel"];
|
|
787
|
+
if (rustEnabled) {
|
|
788
|
+
if (typeof channel !== "string" || !/^\d+\.\d+\.\d+$/.test(channel)) throw new Error("cloud Rust requires an exact stable toolchain");
|
|
789
|
+
if (channel !== rust && !rustExceptions.some(({ pin }) => pin === channel)) throw new Error(`cloud Rust must use shared Rust ${rust} or a recorded rustExceptions pin`);
|
|
790
|
+
if (cloud.rust?.toolchain !== void 0 && cloud.rust.toolchain !== channel) throw new Error("cloud.rust.toolchain conflicts with root Rust toolchain");
|
|
791
|
+
}
|
|
792
|
+
const components = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["components"], "components"), ...cloud.rust?.components ?? []])].sort();
|
|
793
|
+
const targets = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["targets"], "targets"), ...cloud.rust?.targets ?? []])].sort();
|
|
794
|
+
const resolveTool = (tool, version) => {
|
|
795
|
+
if (isCloudPackageTool(tool)) return {
|
|
796
|
+
type: "package",
|
|
797
|
+
tool,
|
|
798
|
+
release: getCloudSetupPackage(tool, version)
|
|
799
|
+
};
|
|
800
|
+
if (tool === "cargo-nextest" && !rustEnabled) throw new Error("cloud.tools.cargo-nextest requires a Rust toolchain");
|
|
801
|
+
if (tool === "wasm-bindgen-cli") {
|
|
802
|
+
const lock = files["Cargo.lock"];
|
|
803
|
+
if (lock === void 0) throw new Error("wasm-bindgen-cli requires tracked root Cargo.lock");
|
|
804
|
+
const packages = parse$1(lock)["package"];
|
|
805
|
+
if (!Array.isArray(packages)) throw new Error("Cargo.lock requires package entries for wasm-bindgen-cli");
|
|
806
|
+
const versions = new Set(packages.flatMap((entry) => record$2(entry) && entry["name"] === "wasm-bindgen" ? [entry["version"]] : []));
|
|
807
|
+
if (versions.size !== 1 || !versions.has(version)) throw new Error(`cloud.tools.wasm-bindgen-cli ${version} must equal the unique wasm-bindgen crate version in Cargo.lock (found ${[...versions].join(", ") || "none"})`);
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
type: "artifact",
|
|
811
|
+
tool,
|
|
812
|
+
release: getCloudSetupArtifact(tool, version)
|
|
813
|
+
};
|
|
814
|
+
};
|
|
815
|
+
const tools = cloudExtraTools.flatMap((tool) => {
|
|
816
|
+
const version = cloud.tools?.[tool];
|
|
817
|
+
return version === void 0 ? [] : [resolveTool(tool, version)];
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
rust: rustEnabled && typeof channel === "string" ? {
|
|
821
|
+
channel,
|
|
822
|
+
components,
|
|
823
|
+
targets,
|
|
824
|
+
installer: cloudSetupArtifacts["rustup-init"]
|
|
825
|
+
} : void 0,
|
|
826
|
+
tools
|
|
827
|
+
};
|
|
828
|
+
};
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/cloud-setup-tools-install.ts
|
|
831
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
832
|
+
const artifactSelection = (release) => `case "$ARCH" in
|
|
833
|
+
${["amd64", "arm64"].map((arch) => ` ${arch}) artifact_url=${quote(release[arch].url)}; artifact_sha=${quote(release[arch].sha256)} ;;`).join("\n")}
|
|
834
|
+
esac`;
|
|
835
|
+
const helpers = String.raw`
|
|
836
|
+
fetch_verified() {
|
|
837
|
+
download_file "$1" "$3" 120
|
|
838
|
+
printf '%s %s\n' "$2" "$3" | sha256sum --strict -c - >/dev/null
|
|
839
|
+
}
|
|
840
|
+
`;
|
|
841
|
+
const rustInstall = (rust) => {
|
|
842
|
+
const args = [...rust.components.flatMap((name) => ["--component", name]), ...rust.targets.flatMap((name) => ["--target", name])].map(quote).join(" ");
|
|
843
|
+
return String.raw`
|
|
844
|
+
export RUSTUP_HOME=/opt/stll-cloud/rustup
|
|
845
|
+
export CARGO_HOME=/opt/stll-cloud/cargo
|
|
846
|
+
export RUSTUP_TOOLCHAIN='@RUST@'
|
|
847
|
+
export RUSTUP_AUTO_INSTALL=0
|
|
848
|
+
rust_run() {
|
|
849
|
+
local mode="$1"
|
|
850
|
+
shift
|
|
851
|
+
set -- runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" RUSTUP_HOME="$RUSTUP_HOME" CARGO_HOME="$CARGO_HOME" RUSTUP_AUTO_INSTALL=0 RUSTUP_TOOLCHAIN=@RUST@ RUSTUP_DIST_SERVER=https://static.rust-lang.org RUSTUP_UPDATE_ROOT=https://static.rust-lang.org/rustup PATH="$PATH" "$@"
|
|
852
|
+
case "$mode" in
|
|
853
|
+
local) "$@" ;;
|
|
854
|
+
network) network_run static.rust-lang.org 180 "$@" ;;
|
|
855
|
+
*) fail 'Invalid Rust execution mode' ;;
|
|
856
|
+
esac
|
|
857
|
+
}
|
|
858
|
+
rust_ready() {
|
|
859
|
+
local directory
|
|
860
|
+
for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do
|
|
861
|
+
[[ -d "$directory" && ! -L "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || return 1
|
|
862
|
+
done
|
|
863
|
+
[[ -x "$CARGO_HOME/bin/rustup" ]] || return 1
|
|
864
|
+
[[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" == 'rustup @RUSTUP@ '* ]] || return 1
|
|
865
|
+
[[ "$(rust_run local "$CARGO_HOME/bin/rustc" +@RUST@ --version)" == 'rustc @RUST@ '* ]] || return 1
|
|
866
|
+
local host
|
|
867
|
+
host="$(rust_run local "$CARGO_HOME/bin/rustc" +@RUST@ -vV | awk '$1 == "host:" { print $2 }')" || return 1
|
|
868
|
+
@VERIFY_COMPONENTS@
|
|
869
|
+
@VERIFY_TARGETS@
|
|
870
|
+
return 0
|
|
871
|
+
}
|
|
872
|
+
verify_rust() { rust_ready || fail 'Pinned Rust, components or targets are unavailable; run install'; }
|
|
873
|
+
install_rust() (
|
|
874
|
+
if ! command -v cc >/dev/null || ! command -v ld >/dev/null || [[ ! -f /usr/include/stdlib.h ]]; then
|
|
875
|
+
apt_refresh_for build-essential
|
|
876
|
+
apt_install build-essential
|
|
877
|
+
fi
|
|
878
|
+
local artifact_url artifact_sha
|
|
879
|
+
# Subshell scope retains scratch for EXIT cleanup after a failed command.
|
|
880
|
+
scratch="$(mktemp -d)"
|
|
881
|
+
trap 'rm -rf -- "$scratch"' EXIT
|
|
882
|
+
for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do
|
|
883
|
+
[[ ! -L "$directory" ]] || fail 'Rust home must not be a symlink'
|
|
884
|
+
if [[ -e "$directory" ]]; then
|
|
885
|
+
[[ -d "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || fail 'Rust home ownership differs from the invoking user'
|
|
886
|
+
else
|
|
887
|
+
install -d -m 755 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$directory"
|
|
888
|
+
fi
|
|
889
|
+
done
|
|
890
|
+
if [[ ! -x "$CARGO_HOME/bin/rustup" ]] || [[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" != 'rustup @RUSTUP@ '* ]]; then
|
|
891
|
+
@RUSTUP_ARTIFACT@
|
|
892
|
+
fetch_verified "$artifact_url" "$artifact_sha" "$scratch/rustup-init"
|
|
893
|
+
chmod 755 "$scratch" "$scratch/rustup-init"
|
|
894
|
+
rust_run network "$scratch/rustup-init" -y --no-modify-path --default-toolchain none --profile minimal
|
|
895
|
+
fi
|
|
896
|
+
rust_run local "$CARGO_HOME/bin/rustup" set auto-self-update disable
|
|
897
|
+
if ! rust_ready; then
|
|
898
|
+
rust_run network "$CARGO_HOME/bin/rustup" toolchain install @RUST@ --profile minimal --no-self-update @RUST_ARGS@
|
|
899
|
+
fi
|
|
900
|
+
rust_run local "$CARGO_HOME/bin/rustup" default @RUST@
|
|
901
|
+
verify_rust
|
|
902
|
+
for tool in rustup cargo rustc rustdoc @RUST_PROXIES@; do
|
|
903
|
+
# Launchers persist the system homes in fresh shells without profile sourcing.
|
|
904
|
+
printf '%s\n' '#!/bin/sh' 'export RUSTUP_HOME=/opt/stll-cloud/rustup' 'export CARGO_HOME=/opt/stll-cloud/cargo' 'export RUSTUP_AUTO_INSTALL=0' 'export RUSTUP_TOOLCHAIN=@RUST@' "exec /opt/stll-cloud/cargo/bin/$tool \"\$@\"" > "$scratch/$tool"
|
|
905
|
+
if ! cmp -s "$scratch/$tool" "/usr/local/bin/$tool"; then
|
|
906
|
+
install -m 755 "$scratch/$tool" "/usr/local/bin/$tool"
|
|
907
|
+
fi
|
|
908
|
+
done
|
|
909
|
+
)
|
|
910
|
+
`.replaceAll("@RUSTUP@", rust.installer.version).replaceAll("@RUST@", rust.channel).replace("@RUSTUP_ARTIFACT@", artifactSelection(rust.installer)).replace("@RUST_ARGS@", args).replace("@RUST_PROXIES@", Object.entries(cloudRustComponentBinaries).flatMap(([component, binaries]) => rust.components.includes(component) ? Array.from(binaries) : []).join(" ")).replace("@VERIFY_COMPONENTS@", rust.components.map((name) => `rust_run local "$CARGO_HOME/bin/rustup" component list --toolchain ${quote(rust.channel)} --installed | awk -v name=${quote(name)} -v host="$host" '$1 == name || $1 == name "-" host { found=1 } END { exit !found }' || return 1`).join("\n ")).replace("@VERIFY_TARGETS@", rust.targets.map((name) => `rust_run local "$CARGO_HOME/bin/rustup" target list --toolchain ${quote(rust.channel)} --installed | awk -v name=${quote(name)} '$1 == name { found=1 } END { exit !found }' || return 1`).join("\n "));
|
|
911
|
+
};
|
|
912
|
+
const packageToolInstall = ({ tool, release }) => {
|
|
913
|
+
const name = tool.replaceAll("-", "_");
|
|
914
|
+
return {
|
|
915
|
+
name,
|
|
916
|
+
text: `verify_${name}() {
|
|
917
|
+
[[ -x ${quote(release.binary)} && "$(${quote(release.binary)} --version 2>/dev/null)" == ${quote(release.identity)}* ]] || return 1
|
|
918
|
+
[[ "$(dpkg-query -W -f='\${Version}' ${quote(release.package)} 2>/dev/null)" == ${quote(`${release.version}-`)}* ]]
|
|
919
|
+
}
|
|
920
|
+
install_${name}() {
|
|
921
|
+
if ! verify_${name}; then
|
|
922
|
+
apt_network update
|
|
923
|
+
apt_install ${quote(release.package)}
|
|
924
|
+
verify_${name} || fail ${quote(`Installed ${tool} version differs from its pin`)}
|
|
925
|
+
fi
|
|
926
|
+
}
|
|
927
|
+
`
|
|
928
|
+
};
|
|
929
|
+
};
|
|
930
|
+
const artifactToolInstall = ({ tool, release }) => {
|
|
931
|
+
const directory = `/opt/stll-cloud/tools/${tool}/${release.version}`;
|
|
932
|
+
const command = tool === "wasm-bindgen-cli" ? "wasm-bindgen" : tool;
|
|
933
|
+
const marker = `${tool === "cargo-nextest" ? "cargo-nextest" : command} ${release.version}`;
|
|
934
|
+
return {
|
|
935
|
+
name: tool.replaceAll("-", "_"),
|
|
936
|
+
text: `verify_${tool.replaceAll("-", "_")}() {
|
|
937
|
+
${Object.keys(release.amd64.binaries).map((binary) => `[[ -x ${quote(`${directory}/${binary}`)} ]] || return 1`).join("\n ")}
|
|
938
|
+
[[ "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(marker)} || "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(`${marker} `)}* ]]
|
|
939
|
+
}
|
|
940
|
+
install_${tool.replaceAll("-", "_")}() (
|
|
941
|
+
local artifact_url artifact_sha
|
|
942
|
+
# Subshell scope retains scratch for EXIT cleanup after a failed command.
|
|
943
|
+
if ! verify_${tool.replaceAll("-", "_")}; then
|
|
944
|
+
scratch="$(mktemp -d)"
|
|
945
|
+
trap 'rm -rf -- "$scratch"' EXIT
|
|
946
|
+
${artifactSelection(release)}
|
|
947
|
+
fetch_verified "$artifact_url" "$artifact_sha" "$scratch/archive.tar.gz"
|
|
948
|
+
tar -xzf "$scratch/archive.tar.gz" -C "$scratch"
|
|
949
|
+
install -d -m 755 ${quote(directory)}
|
|
950
|
+
case "$ARCH" in
|
|
951
|
+
${["amd64", "arm64"].map((arch) => ` ${arch})\n${Object.entries(release[arch].binaries).map(([binary, path]) => ` install -m 755 "$scratch/${path}" ${quote(`${directory}/${binary}`)}`).join("\n")}\n ;;`).join("\n")}
|
|
952
|
+
esac
|
|
953
|
+
verify_${tool.replaceAll("-", "_")} || fail ${quote(`Installed ${tool} version differs from its pin`)}
|
|
954
|
+
fi
|
|
955
|
+
${Object.keys(release.amd64.binaries).map((binary) => ` ln -sfnT -- ${quote(`${directory}/${binary}`)} ${quote(`/usr/local/bin/${binary}`)}`).join("\n")}
|
|
956
|
+
)
|
|
957
|
+
`
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
const renderCloudToolInstall = ({ rust, tools }) => {
|
|
961
|
+
const installs = tools.map((entry) => {
|
|
962
|
+
switch (entry.type) {
|
|
963
|
+
case "package": return packageToolInstall(entry);
|
|
964
|
+
case "artifact": return artifactToolInstall(entry);
|
|
965
|
+
default: throw new Error(`Unsupported cloud tool: ${String(entry)}`);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
return `${helpers}${rust === void 0 ? "" : rustInstall(rust)}\n${installs.map(({ text }) => text).join("\n")}
|
|
969
|
+
install_tools() {
|
|
970
|
+
${rust === void 0 ? ":" : "install_rust"}
|
|
971
|
+
${installs.map(({ name }) => `install_${name}`).join("\n ")}
|
|
972
|
+
}
|
|
973
|
+
verify_tools() {
|
|
974
|
+
${rust === void 0 ? ":" : "verify_rust"}
|
|
975
|
+
${installs.map(({ name }) => `verify_${name} || fail 'Pinned ${name} is unavailable; run install'`).join("\n ")}
|
|
976
|
+
}
|
|
977
|
+
`;
|
|
978
|
+
};
|
|
979
|
+
//#endregion
|
|
437
980
|
//#region src/toolchain-node.ts
|
|
438
981
|
const numericComponent = "(?:0|[1-9]\\d*)";
|
|
439
982
|
const policyPattern = new RegExp(`^${numericComponent}\\.x$`);
|
|
@@ -471,6 +1014,14 @@ usage() { echo 'Usage: cloud-setup.sh install|start' >&2; exit 2; }
|
|
|
471
1014
|
case "$1" in install|start) MODE="$1" ;; *) usage ;; esac
|
|
472
1015
|
fail() { echo "cloud-setup: $*" >&2; exit 1; }
|
|
473
1016
|
trap 'echo "cloud-setup: command failed at line $LINENO" >&2' ERR
|
|
1017
|
+
# Each phase logs its start and duration so a stalled phase is visible in the task log.
|
|
1018
|
+
step() {
|
|
1019
|
+
local name="$1" started="$SECONDS"
|
|
1020
|
+
shift
|
|
1021
|
+
echo "cloud-setup: $name started" >&2
|
|
1022
|
+
"$@"
|
|
1023
|
+
echo "cloud-setup: $name done in $((SECONDS - started))s" >&2
|
|
1024
|
+
}
|
|
474
1025
|
[[ -f /etc/os-release ]] || fail 'Ubuntu 24.04 is required'
|
|
475
1026
|
. /etc/os-release
|
|
476
1027
|
[[ "$ID" == ubuntu && "$VERSION_ID" == 24.04 && "$VERSION_CODENAME" == noble ]] || fail 'Only Ubuntu 24.04 (noble) is supported'
|
|
@@ -506,22 +1057,24 @@ case "$(uname -m)" in
|
|
|
506
1057
|
*) fail 'Only amd64 and arm64 are supported' ;;
|
|
507
1058
|
esac
|
|
508
1059
|
|
|
1060
|
+
@NETWORK@
|
|
1061
|
+
|
|
509
1062
|
runtime_ready() {
|
|
510
1063
|
[[ -x "$NODE_DIR/bin/node" && "$($NODE_DIR/bin/node --version)" == "v$NODE_VERSION" ]] || fail 'Pinned Node is unavailable; run install'
|
|
511
1064
|
[[ -x "$BUN_DIR/bun" && "$($BUN_DIR/bun --version)" == "$BUN_VERSION" ]] || fail 'Pinned Bun is unavailable; run install'
|
|
512
1065
|
}
|
|
513
1066
|
install_runtimes() {
|
|
514
|
-
if ! command -v curl >/dev/null || ! command -v unzip >/dev/null || ! command -v xz >/dev/null || ! command -v openssl >/dev/null || ! command -v gpg >/dev/null; then
|
|
515
|
-
|
|
516
|
-
DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux
|
|
1067
|
+
if ! command -v curl >/dev/null || ! command -v unzip >/dev/null || ! command -v xz >/dev/null || ! command -v openssl >/dev/null || ! command -v gpg >/dev/null || ! ldconfig -p | awk '$1 == "libatomic.so.1" { found=1 } END { exit !found }'; then
|
|
1068
|
+
apt_refresh_for ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
|
|
1069
|
+
DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
|
|
517
1070
|
fi
|
|
518
1071
|
local scratch node_archive bun_archive bun_folder
|
|
519
1072
|
scratch="$(mktemp -d)"
|
|
520
1073
|
trap 'rm -rf -- "$scratch"' RETURN
|
|
521
1074
|
if [[ ! -x "$NODE_DIR/bin/node" ]] || [[ "$($NODE_DIR/bin/node --version)" != "v$NODE_VERSION" ]]; then
|
|
522
1075
|
node_archive="node-v$NODE_VERSION-linux-$NODE_ARCH.tar.xz"
|
|
523
|
-
|
|
524
|
-
|
|
1076
|
+
download_file "https://nodejs.org/dist/v$NODE_VERSION/$node_archive" "$scratch/$node_archive" 120
|
|
1077
|
+
download_file "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt" "$scratch/node-sums" 45
|
|
525
1078
|
(cd "$scratch"; awk -v name="$node_archive" '$2 == name { print; count++ } END { if (count != 1) exit 1 }' node-sums | sha256sum --strict -c -)
|
|
526
1079
|
install -d -m 755 "$NODE_DIR"
|
|
527
1080
|
tar -xJf "$scratch/$node_archive" --strip-components=1 -C "$NODE_DIR"
|
|
@@ -529,8 +1082,8 @@ install_runtimes() {
|
|
|
529
1082
|
if [[ ! -x "$BUN_DIR/bun" ]] || [[ "$($BUN_DIR/bun --version)" != "$BUN_VERSION" ]]; then
|
|
530
1083
|
bun_folder="bun-linux-$BUN_ARCH"
|
|
531
1084
|
bun_archive="$bun_folder.zip"
|
|
532
|
-
|
|
533
|
-
|
|
1085
|
+
download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/$bun_archive" "$scratch/$bun_archive" 120
|
|
1086
|
+
download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/SHASUMS256.txt" "$scratch/bun-sums" 45
|
|
534
1087
|
(cd "$scratch"; awk -v name="$bun_archive" '$2 == name || $2 == "*" name { print $1 " " name; count++ } END { if (count != 1) exit 1 }' bun-sums | sha256sum --strict -c -)
|
|
535
1088
|
unzip -q "$scratch/$bun_archive" -d "$scratch/bun"
|
|
536
1089
|
install -d -m 755 "$BUN_DIR"
|
|
@@ -546,7 +1099,7 @@ install_runtimes() {
|
|
|
546
1099
|
}
|
|
547
1100
|
|
|
548
1101
|
dependency_install() {
|
|
549
|
-
local directory
|
|
1102
|
+
local directory download_hosts
|
|
550
1103
|
for directory in /var/cache/stll-cloud /var/cache/stll-cloud/bun; do
|
|
551
1104
|
[[ ! -L "$directory" ]] || fail 'Dependency cache parent must not be a symlink'
|
|
552
1105
|
if [[ -e "$directory" ]]; then
|
|
@@ -559,24 +1112,52 @@ dependency_install() {
|
|
|
559
1112
|
[[ -d "$BUN_CACHE" && "$(stat -c '%u' "$BUN_CACHE")" == "$OUTPUT_UID" ]] || fail 'Dependency cache ownership differs from the invoking user'
|
|
560
1113
|
fi
|
|
561
1114
|
install -d -m 700 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$BUN_CACHE"
|
|
562
|
-
|
|
1115
|
+
# Report hostnames only: lockfile/config URLs may contain credentials.
|
|
1116
|
+
# These are known install hosts; lifecycle scripts can contact other hosts.
|
|
1117
|
+
download_hosts="$(HOME="$OUTPUT_HOME" "$NODE_DIR/bin/node" -e '
|
|
1118
|
+
const fs = require("node:fs");
|
|
1119
|
+
const path = require("node:path");
|
|
1120
|
+
const hosts = new Set(["registry.npmjs.org"]);
|
|
1121
|
+
const inputs = [process.env.BUN_CONFIG_REGISTRY ?? "", process.env.NPM_CONFIG_REGISTRY ?? "", process.env.npm_config_registry ?? ""];
|
|
1122
|
+
for (const file of ["bun.lock", ".npmrc", "bunfig.toml", path.join(process.env.HOME, ".npmrc"), path.join(process.env.HOME, ".bunfig.toml"), path.join(process.env.XDG_CONFIG_HOME ?? process.env.HOME, ".bunfig.toml")]) {
|
|
1123
|
+
if (fs.existsSync(file)) inputs.push(fs.readFileSync(file, "utf8"));
|
|
1124
|
+
}
|
|
1125
|
+
for (const input of inputs) {
|
|
1126
|
+
for (const match of input.matchAll(/(?:git\+)?https?:\/\/[^\s"\x27<>\\]+/g)) {
|
|
1127
|
+
try { hosts.add(new URL(match[0].replace(/^git\+/, "")).hostname); } catch {}
|
|
1128
|
+
}
|
|
1129
|
+
if (/github:/.test(input)) { hosts.add("github.com"); hosts.add("codeload.github.com"); }
|
|
1130
|
+
}
|
|
1131
|
+
process.stdout.write([...hosts].sort().join(","));
|
|
1132
|
+
')"
|
|
1133
|
+
# Bun 1.4.3 socket inactivity timeout, in seconds; the outer bound includes scripts.
|
|
1134
|
+
network_run "$download_hosts" 240 runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" BUN_INSTALL_CACHE_DIR="$BUN_CACHE" PATH="$PATH" BUN_CONFIG_HTTP_IDLE_TIMEOUT=20 "$BUN_DIR/bun" @INSTALL_ARGS@
|
|
563
1135
|
}
|
|
564
1136
|
|
|
565
1137
|
@SERVICE_INSTALL@
|
|
566
1138
|
@SERVICE_START@
|
|
567
1139
|
|
|
1140
|
+
@TOOL_INSTALL@
|
|
1141
|
+
|
|
1142
|
+
@INPUT_VALIDATION@
|
|
1143
|
+
|
|
568
1144
|
@ENV_FUNCTIONS@
|
|
569
1145
|
|
|
570
1146
|
if [[ "$MODE" == install ]]; then
|
|
571
|
-
install_runtimes
|
|
572
|
-
install_services
|
|
573
|
-
|
|
1147
|
+
step runtimes install_runtimes
|
|
1148
|
+
step services install_services
|
|
1149
|
+
step tools install_tools
|
|
1150
|
+
step shell install_shell_environment
|
|
1151
|
+
step dependencies dependency_install
|
|
574
1152
|
exit 0
|
|
575
1153
|
fi
|
|
576
|
-
runtime_ready
|
|
1154
|
+
step runtime-check runtime_ready
|
|
1155
|
+
step tool-check verify_tools
|
|
1156
|
+
step shell install_shell_environment
|
|
1157
|
+
step shell-check verify_shell_environment
|
|
577
1158
|
@ENV_VALIDATE@
|
|
578
|
-
prepare_state
|
|
579
|
-
start_services
|
|
1159
|
+
step state prepare_state
|
|
1160
|
+
step service-start start_services
|
|
580
1161
|
@ENV_WRITE@
|
|
581
1162
|
`;
|
|
582
1163
|
const environmentFunctions = String.raw`
|
|
@@ -608,13 +1189,36 @@ write_environment() {
|
|
|
608
1189
|
}
|
|
609
1190
|
`;
|
|
610
1191
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
611
|
-
const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
1192
|
+
const generateCloudSetup = ({ policy, cloud, nodeVersion, files = {}, rustExceptions = [] }) => {
|
|
612
1193
|
const selected = parseCloudSetup(cloud);
|
|
613
1194
|
if (selected === void 0) throw new Error("cloud declaration is required");
|
|
614
1195
|
const exactNode = nodeVersion.trim();
|
|
615
1196
|
if (valid(exactNode) !== exactNode || !nodeSelectorMatches(exactNode, policy.node)) throw new Error("cloud requires a tracked exact .node-version within the shared Node series");
|
|
1197
|
+
const toolchains = resolveCloudToolchains({
|
|
1198
|
+
cloud: selected,
|
|
1199
|
+
files,
|
|
1200
|
+
rust: policy.rust,
|
|
1201
|
+
rustExceptions
|
|
1202
|
+
});
|
|
616
1203
|
const services = cloudServices.filter((service) => selected.services.includes(service));
|
|
617
1204
|
const replacements = /* @__PURE__ */ new Map([
|
|
1205
|
+
["@INSTALL_ARGS@", selected.install.replace(/^bun /, "")],
|
|
1206
|
+
["@NETWORK@", renderCloudNetwork()],
|
|
1207
|
+
["@TOOL_INSTALL@", renderCloudToolInstall(toolchains) + renderCloudShellInstall({
|
|
1208
|
+
nodeVersion: exactNode,
|
|
1209
|
+
bunVersion: policy.bun,
|
|
1210
|
+
rust: toolchains.rust
|
|
1211
|
+
})],
|
|
1212
|
+
["@INPUT_VALIDATION@", [
|
|
1213
|
+
"rust-toolchain.toml",
|
|
1214
|
+
"rust-toolchain",
|
|
1215
|
+
...selected.tools?.["wasm-bindgen-cli"] === void 0 ? [] : ["Cargo.lock"]
|
|
1216
|
+
].flatMap((file) => {
|
|
1217
|
+
const content = files[file];
|
|
1218
|
+
if (content === void 0) return [`[[ ! -e ${shellQuote(file)} && ! -L ${shellQuote(file)} ]] || fail ${shellQuote(`${file} appeared; regenerate cloud-setup.sh`)}`];
|
|
1219
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
1220
|
+
return [`[[ -f ${shellQuote(file)} && ! -L ${shellQuote(file)} ]] && [[ "$(sha256sum -- ${shellQuote(file)} | cut -d ' ' -f1)" == ${shellQuote(hash)} ]] || fail ${shellQuote(`${file} changed; regenerate cloud-setup.sh`)}`];
|
|
1221
|
+
}).join("\n")],
|
|
618
1222
|
["@BUN@", policy.bun],
|
|
619
1223
|
["@NODE@", exactNode],
|
|
620
1224
|
["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
|
|
@@ -640,6 +1244,129 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
|
640
1244
|
});
|
|
641
1245
|
};
|
|
642
1246
|
//#endregion
|
|
1247
|
+
//#region src/cloud-setup-claude.ts
|
|
1248
|
+
const cloudClaudeSettingsPath = ".claude/settings.json";
|
|
1249
|
+
const sessionStartHook = (script) => ({
|
|
1250
|
+
matcher: "",
|
|
1251
|
+
hooks: [{
|
|
1252
|
+
type: "command",
|
|
1253
|
+
command: `if [ "\${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
|
|
1254
|
+
${script} start || exit "$?"
|
|
1255
|
+
if [ -n "\${CLAUDE_ENV_FILE:-}" ]; then
|
|
1256
|
+
if [ ! -f ${cloudShellProfilePath} ]; then
|
|
1257
|
+
printf '%s\\n' 'cloud-setup: cloud shell profile is unavailable; run install' >&2
|
|
1258
|
+
exit 1
|
|
1259
|
+
fi
|
|
1260
|
+
printf '\\n' >> "$CLAUDE_ENV_FILE" || exit "$?"
|
|
1261
|
+
cat ${cloudShellProfilePath} >> "$CLAUDE_ENV_FILE"
|
|
1262
|
+
fi
|
|
1263
|
+
fi`
|
|
1264
|
+
}]
|
|
1265
|
+
});
|
|
1266
|
+
const cloudSessionStartHook = sessionStartHook(`bash "$CLAUDE_PROJECT_DIR/${cloudSetupPath}"`);
|
|
1267
|
+
const previousCloudSessionStartHooks = [sessionStartHook(`bash ${cloudSetupPath}`)];
|
|
1268
|
+
const cloudAttribution = {
|
|
1269
|
+
commit: "",
|
|
1270
|
+
pr: ""
|
|
1271
|
+
};
|
|
1272
|
+
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1273
|
+
const array = (value) => Array.isArray(value);
|
|
1274
|
+
const parseSettings = (text) => {
|
|
1275
|
+
try {
|
|
1276
|
+
return JSON.parse(text, (_key, value) => {
|
|
1277
|
+
if (typeof value === "number" && (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value))) throw new Error("settings contains a number that cannot be safely preserved");
|
|
1278
|
+
return value;
|
|
1279
|
+
});
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (error instanceof SyntaxError) throw new Error("settings must contain valid JSON");
|
|
1282
|
+
throw error;
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
const readSettings = (text) => {
|
|
1286
|
+
const settings = text === void 0 ? {} : parseSettings(text);
|
|
1287
|
+
if (text !== void 0 && parseDocument(text, { uniqueKeys: true }).errors.length > 0) throw new Error("settings contains duplicate or ambiguous JSON keys");
|
|
1288
|
+
if (!record$1(settings)) throw new Error("settings must be a JSON object");
|
|
1289
|
+
if (settings["disableAllHooks"] === true) throw new Error("disableAllHooks prevents SessionStart from running");
|
|
1290
|
+
const hooks = settings["hooks"] === void 0 ? {} : settings["hooks"];
|
|
1291
|
+
if (!record$1(hooks)) throw new Error("hooks must be a JSON object");
|
|
1292
|
+
const sessionStart = hooks["SessionStart"] === void 0 ? [] : hooks["SessionStart"];
|
|
1293
|
+
if (!array(sessionStart) || sessionStart.some((entry) => !record$1(entry) || !array(entry["hooks"]))) throw new Error("hooks.SessionStart must be an array of hook groups");
|
|
1294
|
+
return {
|
|
1295
|
+
settings,
|
|
1296
|
+
hooks,
|
|
1297
|
+
sessionStart
|
|
1298
|
+
};
|
|
1299
|
+
};
|
|
1300
|
+
const cloudHookGroups = (sessionStart) => sessionStart.filter((entry) => record$1(entry) && array(entry["hooks"]) && entry["hooks"].some((hook) => record$1(hook) && typeof hook["command"] === "string" && hook["command"].includes(".agents/cloud-setup.sh")));
|
|
1301
|
+
const cloudSessionStartFix = `preserve unrelated settings in ${cloudClaudeSettingsPath}, repair its JSON/hooks structure and set disableAllHooks to false if needed, and ensure hooks.SessionStart contains exactly this cloud setup entry (replace conflicting cloud setup entries):\n` + JSON.stringify(cloudSessionStartHook, null, 2);
|
|
1302
|
+
const cloudAttributionFix = `preserve unrelated settings in ${cloudClaudeSettingsPath} and set its top-level attribution to exactly:\n` + JSON.stringify({ attribution: cloudAttribution }, null, 2);
|
|
1303
|
+
const sessionStartState = (sessionStart) => {
|
|
1304
|
+
const groups = cloudHookGroups(sessionStart);
|
|
1305
|
+
const group = groups.at(0);
|
|
1306
|
+
if (group === void 0) return { type: "missing" };
|
|
1307
|
+
if (groups.length === 1 && isDeepStrictEqual(group, cloudSessionStartHook)) return { type: "current" };
|
|
1308
|
+
if (groups.length === 1 && previousCloudSessionStartHooks.some((hook) => isDeepStrictEqual(group, hook))) return {
|
|
1309
|
+
type: "previous",
|
|
1310
|
+
group
|
|
1311
|
+
};
|
|
1312
|
+
return { type: "conflict" };
|
|
1313
|
+
};
|
|
1314
|
+
const attributionState = (settings) => {
|
|
1315
|
+
const attribution = settings["attribution"];
|
|
1316
|
+
if (attribution === void 0) return "missing";
|
|
1317
|
+
return isDeepStrictEqual(attribution, cloudAttribution) ? "current" : "conflict";
|
|
1318
|
+
};
|
|
1319
|
+
const mergeError = (reason, fix) => /* @__PURE__ */ new Error(`cannot safely merge ${cloudClaudeSettingsPath}: ${reason}; ${fix}`);
|
|
1320
|
+
const readMergeableSettings = (text) => {
|
|
1321
|
+
try {
|
|
1322
|
+
return readSettings(text);
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
throw mergeError(error instanceof Error ? error.message : String(error), cloudSessionStartFix);
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
/** Add the cloud hook and attribution; never overwrite conflicting values. */
|
|
1328
|
+
const mergeCloudClaudeSettings = (text) => {
|
|
1329
|
+
const { settings, hooks, sessionStart } = readMergeableSettings(text);
|
|
1330
|
+
const hook = sessionStartState(sessionStart);
|
|
1331
|
+
if (hook.type === "conflict") throw mergeError("existing cloud setup SessionStart hook differs", cloudSessionStartFix);
|
|
1332
|
+
const attribution = attributionState(settings);
|
|
1333
|
+
if (attribution === "conflict") throw mergeError("existing attribution differs", cloudAttributionFix);
|
|
1334
|
+
if (hook.type === "current" && attribution === "current" && text !== void 0) return text;
|
|
1335
|
+
const mergedSessionStart = (() => {
|
|
1336
|
+
switch (hook.type) {
|
|
1337
|
+
case "current": return sessionStart;
|
|
1338
|
+
case "missing": return [...sessionStart, cloudSessionStartHook];
|
|
1339
|
+
case "previous": return sessionStart.map((entry) => entry === hook.group ? cloudSessionStartHook : entry);
|
|
1340
|
+
default: throw new Error(`Unsupported hook state: ${String(hook)}`);
|
|
1341
|
+
}
|
|
1342
|
+
})();
|
|
1343
|
+
return `${JSON.stringify({
|
|
1344
|
+
...settings,
|
|
1345
|
+
attribution: cloudAttribution,
|
|
1346
|
+
hooks: {
|
|
1347
|
+
...hooks,
|
|
1348
|
+
SessionStart: mergedSessionStart
|
|
1349
|
+
}
|
|
1350
|
+
}, null, 2)}\n`;
|
|
1351
|
+
};
|
|
1352
|
+
/** Settings formatting and unrelated hooks do not affect the generated contract. */
|
|
1353
|
+
const hasCloudSessionStartHook = (text) => {
|
|
1354
|
+
if (text === void 0) return false;
|
|
1355
|
+
try {
|
|
1356
|
+
return sessionStartState(readSettings(text).sessionStart).type === "current";
|
|
1357
|
+
} catch {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
const hasCloudAttribution = (text) => {
|
|
1362
|
+
if (text === void 0) return false;
|
|
1363
|
+
try {
|
|
1364
|
+
return attributionState(readSettings(text).settings) === "current";
|
|
1365
|
+
} catch {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
//#endregion
|
|
643
1370
|
//#region src/toolchain-workspaces.ts
|
|
644
1371
|
/** Match manifest directories with the same positive and negative workspace patterns. */
|
|
645
1372
|
const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
@@ -806,13 +1533,13 @@ const isDockerDefinitionPath = (file) => {
|
|
|
806
1533
|
const isShellScript = (file, text = "") => !excludedInputPath(file) && (/\.(?:sh|bash|zsh)$/.test(file) || /^#![ \t]*(?:\/(?:[^ \t\r\n]+\/)*(?:bash|dash|ksh|zsh|sh)|\/usr\/bin\/env[ \t]+(?:-S[ \t]+)?(?:bash|dash|ksh|zsh|sh))(?=[ \t\r\n]|$)/.test(text));
|
|
807
1534
|
/** Lockfiles establish ecosystem presence without reading their dependency graphs. */
|
|
808
1535
|
const toolchainInputKind = (file, text = "") => {
|
|
809
|
-
if (file === ".agents/cloud-setup.sh") return "config";
|
|
1536
|
+
if (file === ".agents/cloud-setup.sh" || file === ".claude/settings.json" || file === "Cargo.lock") return "config";
|
|
810
1537
|
if (githubAutomationFileKind(file) !== void 0) return "config";
|
|
811
1538
|
if (excludedInputPath(file)) return void 0;
|
|
812
1539
|
if (pythonDependencyManifestKind(file) === "pipfile-lock" || javascriptDependencyLockfiles.some((name) => file.split("/").at(-1) === name) || /(?:^|\/)uv\.lock$/.test(file)) return "presence";
|
|
813
1540
|
if (githubAutomationFileKind(file) !== void 0 || isShellScript(file, text) || /(?:^|\/)ci-service-images\.json$/.test(file) || isMiseConfigPath(file) || isPythonDependencyManifest(file) || isDockerDefinitionPath(file) || isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file) || /(?:^|\/)(?:package\.json|consumer-compat\.json|pnpm-workspace\.yaml|stll-toolchain\.json|\.bun-version|\.node-version|\.nvmrc|\.python-version|rust-toolchain(?:\.toml)?|\.tool-versions|\.?mise\.toml|uv\.toml|\.uv\.toml|\.github\/dependabot\.ya?ml|Cargo\.toml)$/.test(file)) return "config";
|
|
814
1541
|
};
|
|
815
1542
|
//#endregion
|
|
816
|
-
export { nodeSelectorMatches as _, isPythonDependencyManifest as a, pythonDependencyManifestKind as c, isComposeDefinitionPath as d, isKubernetesDefinitionPath as f,
|
|
1543
|
+
export { nodeSelectorMatches as C, nodePolicyValid as S, parseCloudSetup as T, hasCloudAttribution as _, isPythonDependencyManifest as a, cloudSetupPath as b, pythonDependencyManifestKind as c, isComposeDefinitionPath as d, isKubernetesDefinitionPath as f, cloudSessionStartFix as g, cloudClaudeSettingsPath as h, isMiseConfigPath as i, toolchainInputKind as l, cloudAttributionFix as m, isDependabotGithubActionsPath as n, isShellScript as o, workspaceContains as p, isDockerDefinitionPath as r, javascriptDependencyLockfiles as s, githubAutomationFileKind as t, containerDocumentImages as u, hasCloudSessionStartHook as v, nodeSupportRangeMatches as w, generateCloudSetup as x, mergeCloudClaudeSettings as y };
|
|
817
1544
|
|
|
818
1545
|
//# sourceMappingURL=toolchain-inputs.mjs.map
|