@stll/oxlint-config 1.0.2 → 1.1.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 +85 -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 +19 -14
- package/dist/toolchain-changed.cjs.map +1 -1
- package/dist/toolchain-changed.mjs +21 -16
- 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 +66 -50
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +73 -57
- package/dist/toolchain-guard.mjs.map +1 -1
- package/dist/toolchain-inputs.cjs +670 -32
- package/dist/toolchain-inputs.cjs.map +1 -1
- package/dist/toolchain-inputs.mjs +647 -33
- 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,7 +1,11 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime.cjs");
|
|
2
2
|
let node_path = require("node:path");
|
|
3
3
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
4
|
+
let node_crypto = require("node:crypto");
|
|
4
5
|
let semver = require("semver");
|
|
6
|
+
let smol_toml = require("smol-toml");
|
|
7
|
+
let node_util = require("node:util");
|
|
8
|
+
let yaml = require("yaml");
|
|
5
9
|
let picomatch = require("picomatch");
|
|
6
10
|
picomatch = require_rolldown_runtime.__toESM(picomatch, 1);
|
|
7
11
|
//#region src/cloud-setup-install.ts
|
|
@@ -38,7 +42,7 @@ apt_install() (
|
|
|
38
42
|
policy_state=installed
|
|
39
43
|
printf '%s\n' '#!/bin/sh' 'exit 101' > /usr/sbin/policy-rc.d
|
|
40
44
|
chmod 755 /usr/sbin/policy-rc.d
|
|
41
|
-
DEBIAN_FRONTEND=noninteractive
|
|
45
|
+
DEBIAN_FRONTEND=noninteractive apt_network install -y --no-install-recommends "$@"
|
|
42
46
|
)
|
|
43
47
|
|
|
44
48
|
`;
|
|
@@ -162,7 +166,7 @@ const postgresInstall = String.raw`
|
|
|
162
166
|
if [[ "$PGDG_SOURCE_STATE" == missing ]]; then
|
|
163
167
|
local key key_fingerprint
|
|
164
168
|
key="$(mktemp)"
|
|
165
|
-
|
|
169
|
+
download_file https://www.postgresql.org/media/keys/ACCC4CF8.asc "$key" 45
|
|
166
170
|
key_fingerprint="$(pgdg_key_fingerprint "$key")"
|
|
167
171
|
[[ "$key_fingerprint" == @PGDG_FINGERPRINT@ ]] || fail 'PostgreSQL signing key mismatch'
|
|
168
172
|
install -d -m 755 /usr/share/keyrings
|
|
@@ -170,7 +174,7 @@ const postgresInstall = String.raw`
|
|
|
170
174
|
rm -- "$key"
|
|
171
175
|
printf '%s\n' '@PGDG_SOURCE@' > /etc/apt/sources.list.d/stll-pgdg.list
|
|
172
176
|
fi
|
|
173
|
-
|
|
177
|
+
apt_network update
|
|
174
178
|
apt_install postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
|
|
175
179
|
fi
|
|
176
180
|
postgres_ready || fail 'PostgreSQL toolchain is incomplete or differs from the policy major'
|
|
@@ -185,15 +189,15 @@ const valkeyInstall = String.raw`
|
|
|
185
189
|
arm64) server_url='@ARM64_SERVER_URL@'; server_sha='@ARM64_SERVER_SHA@'; tools_url='@ARM64_TOOLS_URL@'; tools_sha='@ARM64_TOOLS_SHA@' ;;
|
|
186
190
|
*) fail 'Unsupported Valkey architecture' ;;
|
|
187
191
|
esac
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
download_file "$server_url" "$scratch/valkey-server.deb" 120
|
|
193
|
+
download_file "$tools_url" "$scratch/valkey-tools.deb" 120
|
|
190
194
|
(cd "$scratch"; printf '%s %s\n' "$server_sha" valkey-server.deb "$tools_sha" valkey-tools.deb | sha256sum --strict -c -)
|
|
191
195
|
for package in valkey-server valkey-tools; do
|
|
192
196
|
[[ "$(dpkg-deb -f "$scratch/$package.deb" Package)" == "$package" ]] || fail 'Valkey artifact package mismatch'
|
|
193
197
|
[[ "$(dpkg-deb -f "$scratch/$package.deb" Version)" == '@VALKEY@-1.noble' ]] || fail 'Valkey artifact version mismatch'
|
|
194
198
|
[[ "$(dpkg-deb -f "$scratch/$package.deb" Architecture)" == "$ARCH" ]] || fail 'Valkey artifact architecture mismatch'
|
|
195
199
|
done
|
|
196
|
-
|
|
200
|
+
apt_network update
|
|
197
201
|
apt_install "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
|
|
198
202
|
rm -rf -- "$scratch"
|
|
199
203
|
fi
|
|
@@ -257,15 +261,235 @@ const renderCloudServiceInstall = ({ services, postgres, valkey, valkeyArtifacts
|
|
|
257
261
|
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`;
|
|
258
262
|
};
|
|
259
263
|
//#endregion
|
|
264
|
+
//#region src/cloud-setup-network.ts
|
|
265
|
+
/** Shared deadlines cover retries and package-manager subprocesses. */
|
|
266
|
+
const renderCloudNetwork = () => String.raw`
|
|
267
|
+
network_run() (
|
|
268
|
+
local host="$1" seconds="$2" status=0
|
|
269
|
+
shift 2
|
|
270
|
+
set -o pipefail
|
|
271
|
+
network_log="$(mktemp)" || return "$?"
|
|
272
|
+
trap 'rm -f -- "$network_log"' EXIT
|
|
273
|
+
trap 'exit 129' HUP
|
|
274
|
+
trap 'exit 130' INT
|
|
275
|
+
trap 'exit 143' TERM
|
|
276
|
+
if timeout --signal=TERM --kill-after=5s "$seconds"s "$@" 2>&1 | tee "$network_log"; then
|
|
277
|
+
return 0
|
|
278
|
+
else
|
|
279
|
+
# Scalar Bash array expansion reads PIPESTATUS[0] before assignment resets it.
|
|
280
|
+
status="$PIPESTATUS"
|
|
281
|
+
fi
|
|
282
|
+
[[ "$status" != 0 ]] || fail 'Cannot capture network command output'
|
|
283
|
+
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
|
|
284
|
+
fail "download timed out: $host"
|
|
285
|
+
fi
|
|
286
|
+
return "$status"
|
|
287
|
+
)
|
|
288
|
+
download_file() {
|
|
289
|
+
local url="$1" destination="$2" seconds="$3" host
|
|
290
|
+
host="$(printf '%s\n' "$url" | cut -d/ -f3 | sed 's/.*@//')"
|
|
291
|
+
[[ "$url" == https://* && -n "$host" ]] || fail 'Downloads require an HTTPS host'
|
|
292
|
+
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"
|
|
293
|
+
}
|
|
294
|
+
apt_network() {
|
|
295
|
+
local hosts
|
|
296
|
+
# --print-uris reads configured indexes without contacting their servers.
|
|
297
|
+
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 "$?"
|
|
298
|
+
[[ -n "$hosts" ]] || hosts='configured APT hosts'
|
|
299
|
+
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 "$@"
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/cloud-setup-artifacts.ts
|
|
304
|
+
const wasmBindgen0126 = {
|
|
305
|
+
version: "0.2.126",
|
|
306
|
+
amd64: {
|
|
307
|
+
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",
|
|
308
|
+
sha256: "064948d58e2d6c0a745216477a639ba696216d6309aaa902939d1b865b1d869d",
|
|
309
|
+
archive: "tar.gz",
|
|
310
|
+
binaries: {
|
|
311
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
312
|
+
wasm2es6js: "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm2es6js",
|
|
313
|
+
"wasm-bindgen": "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm-bindgen"
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
arm64: {
|
|
317
|
+
url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.126/wasm-bindgen-0.2.126-aarch64-unknown-linux-musl.tar.gz",
|
|
318
|
+
sha256: "2245120254a9f6c9a9adf3601f3d52bb31309219e9ceab7696e74e24885c440a",
|
|
319
|
+
archive: "tar.gz",
|
|
320
|
+
binaries: {
|
|
321
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
322
|
+
wasm2es6js: "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm2es6js",
|
|
323
|
+
"wasm-bindgen": "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm-bindgen"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
/** Pinned upstream Linux artifacts, verified against their published SHA-256 sums. */
|
|
328
|
+
const cloudSetupArtifacts = {
|
|
329
|
+
"rustup-init": {
|
|
330
|
+
version: "1.29.1",
|
|
331
|
+
amd64: {
|
|
332
|
+
url: "https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init",
|
|
333
|
+
sha256: "dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71",
|
|
334
|
+
archive: "binary",
|
|
335
|
+
binaries: { "rustup-init": "rustup-init" }
|
|
336
|
+
},
|
|
337
|
+
arm64: {
|
|
338
|
+
url: "https://static.rust-lang.org/rustup/archive/1.29.1/aarch64-unknown-linux-gnu/rustup-init",
|
|
339
|
+
sha256: "15f6e4ce9f583b929c996c91562bad6d4454f3281de858b02cdfdef615fac433",
|
|
340
|
+
archive: "binary",
|
|
341
|
+
binaries: { "rustup-init": "rustup-init" }
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"cargo-nextest": {
|
|
345
|
+
version: "0.9.148",
|
|
346
|
+
amd64: {
|
|
347
|
+
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",
|
|
348
|
+
sha256: "bf92cb81450872d960ffa13857a83084d557ec0cdc141663d263bb23ef756f97",
|
|
349
|
+
archive: "tar.gz",
|
|
350
|
+
binaries: { "cargo-nextest": "cargo-nextest" }
|
|
351
|
+
},
|
|
352
|
+
arm64: {
|
|
353
|
+
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",
|
|
354
|
+
sha256: "94676506ceaed01746d78cfe9ed124ee841b2932fd18d9d10f438a347bd9ba5e",
|
|
355
|
+
archive: "tar.gz",
|
|
356
|
+
binaries: { "cargo-nextest": "cargo-nextest" }
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"wasm-bindgen-cli": {
|
|
360
|
+
version: "0.2.129",
|
|
361
|
+
amd64: {
|
|
362
|
+
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",
|
|
363
|
+
sha256: "82d12bb940e2d4e72e0d5605387fc1b8ca179044e012b620f0ce4e7440e8320e",
|
|
364
|
+
archive: "tar.gz",
|
|
365
|
+
binaries: {
|
|
366
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
367
|
+
wasm2es6js: "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm2es6js",
|
|
368
|
+
"wasm-bindgen": "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm-bindgen"
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
arm64: {
|
|
372
|
+
url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.129/wasm-bindgen-0.2.129-aarch64-unknown-linux-musl.tar.gz",
|
|
373
|
+
sha256: "2ed4351c35dd9440308bbb02767d47ea278efe851a52465300f3c94f5b6c2a87",
|
|
374
|
+
archive: "tar.gz",
|
|
375
|
+
binaries: {
|
|
376
|
+
"wasm-bindgen-test-runner": "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm-bindgen-test-runner",
|
|
377
|
+
wasm2es6js: "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm2es6js",
|
|
378
|
+
"wasm-bindgen": "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm-bindgen"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
uv: {
|
|
383
|
+
version: "0.13.1",
|
|
384
|
+
amd64: {
|
|
385
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-x86_64-unknown-linux-musl.tar.gz",
|
|
386
|
+
sha256: "6c86040a8f045ab9adc9e3ec4cdafa03fd00e9a988ed2bf28971c7fc2d0debd0",
|
|
387
|
+
archive: "tar.gz",
|
|
388
|
+
binaries: {
|
|
389
|
+
uv: "uv-x86_64-unknown-linux-musl/uv",
|
|
390
|
+
uvx: "uv-x86_64-unknown-linux-musl/uvx"
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
arm64: {
|
|
394
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-aarch64-unknown-linux-musl.tar.gz",
|
|
395
|
+
sha256: "345a8308e4d1835cdfaadf692836fd3137a6d45864773f05ad7bf59442b1ba16",
|
|
396
|
+
archive: "tar.gz",
|
|
397
|
+
binaries: {
|
|
398
|
+
uv: "uv-aarch64-unknown-linux-musl/uv",
|
|
399
|
+
uvx: "uv-aarch64-unknown-linux-musl/uvx"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
/** Explicit releases whose CLI must match a project's wasm-bindgen crate exactly. */
|
|
405
|
+
const wasmBindgenArtifactReleases = [wasmBindgen0126, cloudSetupArtifacts["wasm-bindgen-cli"]];
|
|
406
|
+
/** Every supported version participates in lookup and catalog validation. */
|
|
407
|
+
const cloudSetupArtifactVersions = {
|
|
408
|
+
"rustup-init": [cloudSetupArtifacts["rustup-init"]],
|
|
409
|
+
"cargo-nextest": [cloudSetupArtifacts["cargo-nextest"]],
|
|
410
|
+
"wasm-bindgen-cli": wasmBindgenArtifactReleases,
|
|
411
|
+
uv: [cloudSetupArtifacts.uv]
|
|
412
|
+
};
|
|
413
|
+
/** A new version requires an explicit, verified catalog entry; never build from source. */
|
|
414
|
+
const getCloudSetupArtifact = (tool, version) => {
|
|
415
|
+
const releases = cloudSetupArtifactVersions[tool];
|
|
416
|
+
const artifact = releases.find((release) => release.version === version);
|
|
417
|
+
if (artifact === void 0) throw new Error(`Unsupported ${tool} version ${version}; supported versions: ${releases.map((release) => release.version).join(", ")}`);
|
|
418
|
+
return artifact;
|
|
419
|
+
};
|
|
420
|
+
//#endregion
|
|
260
421
|
//#region src/cloud-setup-schema.ts
|
|
261
422
|
const cloudServices = ["postgres", "valkey"];
|
|
423
|
+
const cloudExtraTools = Object.keys(cloudSetupArtifacts).filter((tool) => tool !== "rustup-init" && Object.hasOwn(cloudSetupArtifacts, tool));
|
|
424
|
+
const object = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
|
|
425
|
+
const exactPin = (input) => typeof input === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(input);
|
|
426
|
+
const cloudRustComponentBinaries = {
|
|
427
|
+
clippy: ["cargo-clippy", "clippy-driver"],
|
|
428
|
+
rustfmt: ["rustfmt", "cargo-fmt"],
|
|
429
|
+
"rust-src": [],
|
|
430
|
+
"rust-analyzer": ["rust-analyzer"]
|
|
431
|
+
};
|
|
432
|
+
const parseCloudRustNames = (input, kind) => {
|
|
433
|
+
const label = `cloud Rust ${kind}`;
|
|
434
|
+
if (input === void 0) return [];
|
|
435
|
+
if (!Array.isArray(input)) throw new Error(`${label} must be an array`);
|
|
436
|
+
const result = input.map((value) => {
|
|
437
|
+
if (typeof value !== "string" || !/^[a-z][a-z0-9_-]*$/.test(value)) throw new Error(`${label} requires canonical names`);
|
|
438
|
+
if (kind === "components" && !Object.hasOwn(cloudRustComponentBinaries, value)) throw new Error(`${label} contains an unsupported component: ${value}`);
|
|
439
|
+
return value;
|
|
440
|
+
});
|
|
441
|
+
if (new Set(result).size !== result.length) throw new Error(`${label} contains duplicates`);
|
|
442
|
+
return result;
|
|
443
|
+
};
|
|
444
|
+
const parseRust = (input) => {
|
|
445
|
+
if (input === void 0) return void 0;
|
|
446
|
+
if (!object(input) || Object.keys(input).some((key) => ![
|
|
447
|
+
"toolchain",
|
|
448
|
+
"components",
|
|
449
|
+
"targets"
|
|
450
|
+
].includes(key))) throw new Error("cloud.rust accepts only toolchain, components and targets");
|
|
451
|
+
const toolchain = input["toolchain"];
|
|
452
|
+
if (toolchain !== void 0 && !exactPin(toolchain)) throw new Error("cloud.rust.toolchain must be an exact stable release");
|
|
453
|
+
return {
|
|
454
|
+
...toolchain === void 0 ? {} : { toolchain },
|
|
455
|
+
components: parseCloudRustNames(input["components"], "components"),
|
|
456
|
+
targets: parseCloudRustNames(input["targets"], "targets")
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
const parseTools = (input) => {
|
|
460
|
+
if (input === void 0) return void 0;
|
|
461
|
+
if (!object(input) || Object.keys(input).some((key) => !cloudExtraTools.some((tool) => tool === key))) throw new Error(`cloud.tools accepts only ${cloudExtraTools.join(", ")}`);
|
|
462
|
+
const result = {};
|
|
463
|
+
for (const tool of cloudExtraTools) {
|
|
464
|
+
const pin = input[tool];
|
|
465
|
+
if (pin === void 0) {
|
|
466
|
+
if (Object.hasOwn(input, tool)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!exactPin(pin)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
|
|
470
|
+
result[tool] = pin;
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
};
|
|
262
474
|
const cloudInstallCommand = "bun install --frozen-lockfile";
|
|
263
475
|
const cloudService = (value) => cloudServices.some((service) => value === service);
|
|
476
|
+
const parseEnvFile = (input) => {
|
|
477
|
+
if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
478
|
+
const envFile = input.envFile;
|
|
479
|
+
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");
|
|
480
|
+
return envFile;
|
|
481
|
+
};
|
|
264
482
|
/** Parse an explicit declaration without assuming services for undeclared repositories. */
|
|
265
483
|
const parseCloudSetup = (input) => {
|
|
266
484
|
if (input === void 0) return void 0;
|
|
267
485
|
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("cloud must be an object");
|
|
268
|
-
for (const key of Object.keys(input)) if (key !== "services" && key !== "install" && key !== "envFile") throw new Error(`unsupported cloud key: ${key}`);
|
|
486
|
+
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}`);
|
|
487
|
+
const rust = parseRust("rust" in input ? input.rust : void 0);
|
|
488
|
+
const tools = parseTools("tools" in input ? input.tools : void 0);
|
|
489
|
+
const extensions = {
|
|
490
|
+
...rust === void 0 ? {} : { rust },
|
|
491
|
+
...tools === void 0 ? {} : { tools }
|
|
492
|
+
};
|
|
269
493
|
const selected = "services" in input ? input.services : [];
|
|
270
494
|
if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
|
|
271
495
|
if (!("install" in input) || input.install !== "bun install --frozen-lockfile") throw new Error(`cloud.install must be exactly ${cloudInstallCommand}`);
|
|
@@ -276,20 +500,12 @@ const parseCloudSetup = (input) => {
|
|
|
276
500
|
seen.add(service);
|
|
277
501
|
return service;
|
|
278
502
|
});
|
|
279
|
-
if (services.length === 0)
|
|
280
|
-
if ("envFile" in input) throw new Error("cloud.envFile is not allowed without services");
|
|
281
|
-
return {
|
|
282
|
-
services,
|
|
283
|
-
install: cloudInstallCommand
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
287
|
-
const envFile = input.envFile;
|
|
288
|
-
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");
|
|
503
|
+
if (services.length === 0 && "envFile" in input) throw new Error("cloud.envFile is not allowed without services");
|
|
289
504
|
return {
|
|
290
505
|
services,
|
|
291
506
|
install: cloudInstallCommand,
|
|
292
|
-
|
|
507
|
+
...extensions,
|
|
508
|
+
...services.length === 0 ? {} : { envFile: parseEnvFile(input) }
|
|
293
509
|
};
|
|
294
510
|
};
|
|
295
511
|
//#endregion
|
|
@@ -356,7 +572,7 @@ service_prune_pid() {
|
|
|
356
572
|
}
|
|
357
573
|
`;
|
|
358
574
|
const postgresStart = String.raw`
|
|
359
|
-
local pg_bin='/usr/lib/postgresql/@POSTGRES@/bin' pg_data="$STATE/postgres" pg_password pg_details pg_pid pg_version
|
|
575
|
+
local pg_bin='/usr/lib/postgresql/@POSTGRES@/bin' pg_data="$STATE/postgres" pg_password pg_details pg_pid pg_version pg_state='already running'
|
|
360
576
|
service_password "$STATE/postgres.password"
|
|
361
577
|
pg_password="$(cat "$STATE/postgres.password")"
|
|
362
578
|
service_directory "$pg_data"
|
|
@@ -375,6 +591,7 @@ const postgresStart = String.raw`
|
|
|
375
591
|
service_prune_pid "$pg_data/postmaster.pid" "$pg_bin/postgres" postgres
|
|
376
592
|
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
|
|
377
593
|
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"
|
|
594
|
+
pg_state=started
|
|
378
595
|
fi
|
|
379
596
|
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"
|
|
380
597
|
pg_version="$(printf '%s\n' "$pg_details" | cut -d '|' -f4)"
|
|
@@ -384,9 +601,10 @@ const postgresStart = String.raw`
|
|
|
384
601
|
service_process "$pg_pid" "$pg_bin/postgres"
|
|
385
602
|
[[ "$(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"
|
|
386
603
|
DATABASE_URL="postgresql://stll_cloud:$pg_password@127.0.0.1:55432/cloud_test"
|
|
604
|
+
printf 'postgres: %s\n' "$pg_state"
|
|
387
605
|
`;
|
|
388
606
|
const valkeyStart = String.raw`
|
|
389
|
-
local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0
|
|
607
|
+
local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0 vk_state='already running'
|
|
390
608
|
service_password "$STATE/valkey.password"
|
|
391
609
|
vk_password="$(cat "$STATE/valkey.password")"
|
|
392
610
|
service_directory "$vk_data"
|
|
@@ -406,7 +624,8 @@ const valkeyStart = String.raw`
|
|
|
406
624
|
[[ "$(cat "$vk_config")" == "$vk_expected" ]] || fail "Valkey configuration differs from managed settings"
|
|
407
625
|
service_prune_pid "$vk_data/valkey.pid" "$(command -v valkey-server)" valkey
|
|
408
626
|
if [[ ! -e "$vk_data/valkey.pid" ]]; then
|
|
409
|
-
service_run valkey-server "$vk_config" || fail "Valkey start failed; port 56379 must be available"
|
|
627
|
+
service_run valkey-server "$vk_config" >/dev/null || fail "Valkey start failed; port 56379 must be available"
|
|
628
|
+
vk_state=started
|
|
410
629
|
fi
|
|
411
630
|
for ((attempt=0; attempt<20; attempt++)); do
|
|
412
631
|
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
|
|
@@ -422,6 +641,7 @@ const valkeyStart = String.raw`
|
|
|
422
641
|
[[ "$(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"
|
|
423
642
|
[[ "$(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"
|
|
424
643
|
REDIS_URL="redis://:$vk_password@127.0.0.1:56379/0"
|
|
644
|
+
printf 'valkey: %s\n' "$vk_state"
|
|
425
645
|
`;
|
|
426
646
|
/** Render only declared services; installation and environment-file ownership stay with the caller. */
|
|
427
647
|
const renderCloudServiceStart = ({ services, postgres, valkey }) => {
|
|
@@ -437,6 +657,267 @@ const renderCloudServiceStart = ({ services, postgres, valkey }) => {
|
|
|
437
657
|
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`;
|
|
438
658
|
};
|
|
439
659
|
//#endregion
|
|
660
|
+
//#region src/cloud-setup-shell.ts
|
|
661
|
+
const cloudShellProfilePath = "/etc/profile.d/stll-cloud.sh";
|
|
662
|
+
const cloudShellProfileSource = `. ${cloudShellProfilePath}`;
|
|
663
|
+
const quote$1 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
664
|
+
const renderCloudShellProfile = ({ nodeVersion, bunVersion, rust }) => {
|
|
665
|
+
const node = `/opt/stll-cloud/node/${nodeVersion}/bin`;
|
|
666
|
+
const bun = `/opt/stll-cloud/bun/${bunVersion}`;
|
|
667
|
+
return `# Generated by stll-cloud-setup; managed cloud shell environment.
|
|
668
|
+
# Remove managed entries before prepending, including repeated shell startup.
|
|
669
|
+
_stll_cloud_remaining="\${PATH-}"
|
|
670
|
+
_stll_cloud_result=''
|
|
671
|
+
_stll_cloud_separator=''
|
|
672
|
+
while :; do
|
|
673
|
+
case "$_stll_cloud_remaining" in
|
|
674
|
+
*:*) _stll_cloud_part="\${_stll_cloud_remaining%%:*}"; _stll_cloud_remaining="\${_stll_cloud_remaining#*:}"; _stll_cloud_more=yes ;;
|
|
675
|
+
*) _stll_cloud_part="$_stll_cloud_remaining"; _stll_cloud_more=no ;;
|
|
676
|
+
esac
|
|
677
|
+
case "$_stll_cloud_part" in
|
|
678
|
+
${quote$1(node)}|${quote$1(bun)}|/usr/local/bin) ;;
|
|
679
|
+
*) _stll_cloud_result="$_stll_cloud_result$_stll_cloud_separator$_stll_cloud_part"; _stll_cloud_separator=: ;;
|
|
680
|
+
esac
|
|
681
|
+
[ "$_stll_cloud_more" = yes ] || break
|
|
682
|
+
done
|
|
683
|
+
export PATH=${quote$1(`${node}:${bun}:/usr/local/bin`)}"\${_stll_cloud_separator:+:}$_stll_cloud_result"
|
|
684
|
+
unset _stll_cloud_remaining _stll_cloud_result _stll_cloud_separator _stll_cloud_part _stll_cloud_more
|
|
685
|
+
${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`}`;
|
|
686
|
+
};
|
|
687
|
+
const userStartup = String.raw`
|
|
688
|
+
set -euo pipefail
|
|
689
|
+
for name in .bashrc .profile .bash_profile .bash_login; do
|
|
690
|
+
file="$HOME/$name"
|
|
691
|
+
if [[ ! -e "$file" && ! -L "$file" && "$name" != .bashrc && "$name" != .profile ]]; then continue; fi
|
|
692
|
+
[[ ! -L "$file" ]] || { echo "cloud-setup: shell startup file must not be a symlink: $name" >&2; exit 1; }
|
|
693
|
+
if [[ -e "$file" ]]; then
|
|
694
|
+
[[ -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; }
|
|
695
|
+
fi
|
|
696
|
+
temporary="$(mktemp "$HOME/.stll-cloud-shell.XXXXXX")"
|
|
697
|
+
trap 'rm -f -- "$temporary"' EXIT
|
|
698
|
+
if [[ "$name" == .bashrc ]]; then
|
|
699
|
+
printf '%s\n' '# stll-cloud: early environment' '@SOURCE@' > "$temporary"
|
|
700
|
+
fi
|
|
701
|
+
if [[ -f "$file" ]]; then
|
|
702
|
+
# Remove only exact managed blocks; preserve unrelated startup lines and permissions.
|
|
703
|
+
awk '
|
|
704
|
+
$0 == "# stll-cloud: early environment" || $0 == "# stll-cloud: session environment" {
|
|
705
|
+
if ((getline following) <= 0 || following != "@SOURCE@") exit 1
|
|
706
|
+
next
|
|
707
|
+
}
|
|
708
|
+
{ print }
|
|
709
|
+
' "$file" >> "$temporary" || { echo "cloud-setup: managed shell startup block differs: $name" >&2; exit 1; }
|
|
710
|
+
fi
|
|
711
|
+
printf '%s\n' '# stll-cloud: session environment' '@SOURCE@' >> "$temporary"
|
|
712
|
+
if [[ -f "$file" ]] && cmp -s "$temporary" "$file"; then rm -- "$temporary"; continue; fi
|
|
713
|
+
if [[ -f "$file" ]]; then chmod --reference="$file" "$temporary"; else chmod 644 "$temporary"; fi
|
|
714
|
+
mv -f -- "$temporary" "$file"
|
|
715
|
+
done
|
|
716
|
+
`;
|
|
717
|
+
const renderCloudShellInstall = (options) => {
|
|
718
|
+
const profile = renderCloudShellProfile(options);
|
|
719
|
+
return `install_shell_environment() (
|
|
720
|
+
# Subshell scope retains temporary for EXIT cleanup after a failed command.
|
|
721
|
+
[[ ! -L ${quote$1(cloudShellProfilePath)} ]] || fail 'Managed shell profile must not be a symlink'
|
|
722
|
+
if [[ -e ${quote$1(cloudShellProfilePath)} ]]; then
|
|
723
|
+
[[ -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'
|
|
724
|
+
fi
|
|
725
|
+
temporary="$(mktemp)"
|
|
726
|
+
trap 'rm -f -- "$temporary"' EXIT
|
|
727
|
+
cat > "$temporary" <<'STLL_CLOUD_PROFILE'
|
|
728
|
+
${profile}STLL_CLOUD_PROFILE
|
|
729
|
+
if ! cmp -s "$temporary" ${quote$1(cloudShellProfilePath)}; then install -m 644 "$temporary" ${quote$1(cloudShellProfilePath)}; fi
|
|
730
|
+
runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" bash -c ${quote$1(userStartup.replaceAll("@SOURCE@", cloudShellProfileSource))}
|
|
731
|
+
)
|
|
732
|
+
verify_shell_environment() {
|
|
733
|
+
[[ -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'
|
|
734
|
+
[[ "$(sha256sum -- ${quote$1(cloudShellProfilePath)} | cut -d ' ' -f1)" == ${quote$1((0, node_crypto.createHash)("sha256").update(profile).digest("hex"))} ]] || fail 'Managed shell environment differs; run install'
|
|
735
|
+
}
|
|
736
|
+
`;
|
|
737
|
+
};
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region src/cloud-setup-toolchains.ts
|
|
740
|
+
const record$2 = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
|
|
741
|
+
/** Resolve the same tracked snapshot for generation and the mandatory drift check. */
|
|
742
|
+
const resolveCloudToolchains = ({ cloud, files, rust, rustExceptions }) => {
|
|
743
|
+
const source = files["rust-toolchain.toml"];
|
|
744
|
+
const selector = files["rust-toolchain"];
|
|
745
|
+
if (source !== void 0 && selector !== void 0) throw new Error("cloud Rust requires one root rust-toolchain.toml or rust-toolchain file");
|
|
746
|
+
let declared;
|
|
747
|
+
if (source !== void 0 || selector?.trimStart().startsWith("[")) {
|
|
748
|
+
const parsed = (0, smol_toml.parse)(source ?? selector ?? "");
|
|
749
|
+
if (!record$2(parsed["toolchain"])) throw new Error("cloud Rust requires [toolchain]");
|
|
750
|
+
declared = parsed["toolchain"];
|
|
751
|
+
if (Object.keys(declared).some((key) => ![
|
|
752
|
+
"channel",
|
|
753
|
+
"components",
|
|
754
|
+
"targets",
|
|
755
|
+
"profile"
|
|
756
|
+
].includes(key))) throw new Error("cloud Rust supports only channel, components, targets and profile");
|
|
757
|
+
if (declared["profile"] !== void 0 && declared["profile"] !== "minimal") throw new Error("cloud Rust requires profile = minimal; declare required components explicitly");
|
|
758
|
+
} else if (selector !== void 0) declared = { channel: selector.trim() };
|
|
759
|
+
const rustEnabled = declared !== void 0 || cloud.rust !== void 0;
|
|
760
|
+
const channel = declared === void 0 ? cloud.rust?.toolchain ?? rust : declared["channel"];
|
|
761
|
+
if (rustEnabled) {
|
|
762
|
+
if (typeof channel !== "string" || !/^\d+\.\d+\.\d+$/.test(channel)) throw new Error("cloud Rust requires an exact stable toolchain");
|
|
763
|
+
if (channel !== rust && !rustExceptions.some(({ pin }) => pin === channel)) throw new Error(`cloud Rust must use shared Rust ${rust} or a recorded rustExceptions pin`);
|
|
764
|
+
if (cloud.rust?.toolchain !== void 0 && cloud.rust.toolchain !== channel) throw new Error("cloud.rust.toolchain conflicts with root Rust toolchain");
|
|
765
|
+
}
|
|
766
|
+
const components = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["components"], "components"), ...cloud.rust?.components ?? []])].sort();
|
|
767
|
+
const targets = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["targets"], "targets"), ...cloud.rust?.targets ?? []])].sort();
|
|
768
|
+
const tools = cloudExtraTools.flatMap((tool) => {
|
|
769
|
+
const version = cloud.tools?.[tool];
|
|
770
|
+
if (version === void 0) return [];
|
|
771
|
+
if (tool === "cargo-nextest" && !rustEnabled) throw new Error("cloud.tools.cargo-nextest requires a Rust toolchain");
|
|
772
|
+
if (tool === "wasm-bindgen-cli") {
|
|
773
|
+
const lock = files["Cargo.lock"];
|
|
774
|
+
if (lock === void 0) throw new Error("wasm-bindgen-cli requires tracked root Cargo.lock");
|
|
775
|
+
const packages = (0, smol_toml.parse)(lock)["package"];
|
|
776
|
+
if (!Array.isArray(packages)) throw new Error("Cargo.lock requires package entries for wasm-bindgen-cli");
|
|
777
|
+
const versions = new Set(packages.flatMap((entry) => record$2(entry) && entry["name"] === "wasm-bindgen" ? [entry["version"]] : []));
|
|
778
|
+
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"})`);
|
|
779
|
+
}
|
|
780
|
+
return [{
|
|
781
|
+
tool,
|
|
782
|
+
release: getCloudSetupArtifact(tool, version)
|
|
783
|
+
}];
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
rust: rustEnabled && typeof channel === "string" ? {
|
|
787
|
+
channel,
|
|
788
|
+
components,
|
|
789
|
+
targets,
|
|
790
|
+
installer: cloudSetupArtifacts["rustup-init"]
|
|
791
|
+
} : void 0,
|
|
792
|
+
tools
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/cloud-setup-tools-install.ts
|
|
797
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
798
|
+
const artifactSelection = (release) => `case "$ARCH" in
|
|
799
|
+
${["amd64", "arm64"].map((arch) => ` ${arch}) artifact_url=${quote(release[arch].url)}; artifact_sha=${quote(release[arch].sha256)} ;;`).join("\n")}
|
|
800
|
+
esac`;
|
|
801
|
+
const helpers = String.raw`
|
|
802
|
+
fetch_verified() {
|
|
803
|
+
download_file "$1" "$3" 120
|
|
804
|
+
printf '%s %s\n' "$2" "$3" | sha256sum --strict -c - >/dev/null
|
|
805
|
+
}
|
|
806
|
+
`;
|
|
807
|
+
const rustInstall = (rust) => {
|
|
808
|
+
const args = [...rust.components.flatMap((name) => ["--component", name]), ...rust.targets.flatMap((name) => ["--target", name])].map(quote).join(" ");
|
|
809
|
+
return String.raw`
|
|
810
|
+
export RUSTUP_HOME=/opt/stll-cloud/rustup
|
|
811
|
+
export CARGO_HOME=/opt/stll-cloud/cargo
|
|
812
|
+
export RUSTUP_TOOLCHAIN='@RUST@'
|
|
813
|
+
export RUSTUP_AUTO_INSTALL=0
|
|
814
|
+
rust_run() {
|
|
815
|
+
local mode="$1"
|
|
816
|
+
shift
|
|
817
|
+
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" "$@"
|
|
818
|
+
case "$mode" in
|
|
819
|
+
local) "$@" ;;
|
|
820
|
+
network) network_run static.rust-lang.org 180 "$@" ;;
|
|
821
|
+
*) fail 'Invalid Rust execution mode' ;;
|
|
822
|
+
esac
|
|
823
|
+
}
|
|
824
|
+
rust_ready() {
|
|
825
|
+
local directory
|
|
826
|
+
for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do
|
|
827
|
+
[[ -d "$directory" && ! -L "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || return 1
|
|
828
|
+
done
|
|
829
|
+
[[ -x "$CARGO_HOME/bin/rustup" ]] || return 1
|
|
830
|
+
[[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" == 'rustup @RUSTUP@ '* ]] || return 1
|
|
831
|
+
[[ "$(rust_run local "$CARGO_HOME/bin/rustc" +@RUST@ --version)" == 'rustc @RUST@ '* ]] || return 1
|
|
832
|
+
local host
|
|
833
|
+
host="$(rust_run local "$CARGO_HOME/bin/rustc" +@RUST@ -vV | awk '$1 == "host:" { print $2 }')" || return 1
|
|
834
|
+
@VERIFY_COMPONENTS@
|
|
835
|
+
@VERIFY_TARGETS@
|
|
836
|
+
return 0
|
|
837
|
+
}
|
|
838
|
+
verify_rust() { rust_ready || fail 'Pinned Rust, components or targets are unavailable; run install'; }
|
|
839
|
+
install_rust() (
|
|
840
|
+
if ! command -v cc >/dev/null || ! command -v ld >/dev/null || [[ ! -f /usr/include/stdlib.h ]]; then
|
|
841
|
+
apt_network update
|
|
842
|
+
apt_install build-essential
|
|
843
|
+
fi
|
|
844
|
+
local artifact_url artifact_sha
|
|
845
|
+
# Subshell scope retains scratch for EXIT cleanup after a failed command.
|
|
846
|
+
scratch="$(mktemp -d)"
|
|
847
|
+
trap 'rm -rf -- "$scratch"' EXIT
|
|
848
|
+
for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do
|
|
849
|
+
[[ ! -L "$directory" ]] || fail 'Rust home must not be a symlink'
|
|
850
|
+
if [[ -e "$directory" ]]; then
|
|
851
|
+
[[ -d "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || fail 'Rust home ownership differs from the invoking user'
|
|
852
|
+
else
|
|
853
|
+
install -d -m 755 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$directory"
|
|
854
|
+
fi
|
|
855
|
+
done
|
|
856
|
+
if [[ ! -x "$CARGO_HOME/bin/rustup" ]] || [[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" != 'rustup @RUSTUP@ '* ]]; then
|
|
857
|
+
@RUSTUP_ARTIFACT@
|
|
858
|
+
fetch_verified "$artifact_url" "$artifact_sha" "$scratch/rustup-init"
|
|
859
|
+
chmod 755 "$scratch" "$scratch/rustup-init"
|
|
860
|
+
rust_run network "$scratch/rustup-init" -y --no-modify-path --default-toolchain none --profile minimal
|
|
861
|
+
fi
|
|
862
|
+
rust_run local "$CARGO_HOME/bin/rustup" set auto-self-update disable
|
|
863
|
+
if ! rust_ready; then
|
|
864
|
+
rust_run network "$CARGO_HOME/bin/rustup" toolchain install @RUST@ --profile minimal --no-self-update @RUST_ARGS@
|
|
865
|
+
fi
|
|
866
|
+
rust_run local "$CARGO_HOME/bin/rustup" default @RUST@
|
|
867
|
+
verify_rust
|
|
868
|
+
for tool in rustup cargo rustc rustdoc @RUST_PROXIES@; do
|
|
869
|
+
# Launchers persist the system homes in fresh shells without profile sourcing.
|
|
870
|
+
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"
|
|
871
|
+
if ! cmp -s "$scratch/$tool" "/usr/local/bin/$tool"; then
|
|
872
|
+
install -m 755 "$scratch/$tool" "/usr/local/bin/$tool"
|
|
873
|
+
fi
|
|
874
|
+
done
|
|
875
|
+
)
|
|
876
|
+
`.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 "));
|
|
877
|
+
};
|
|
878
|
+
const renderCloudToolInstall = ({ rust, tools }) => {
|
|
879
|
+
const installs = tools.map(({ tool, release }) => {
|
|
880
|
+
const directory = `/opt/stll-cloud/tools/${tool}/${release.version}`;
|
|
881
|
+
const command = tool === "wasm-bindgen-cli" ? "wasm-bindgen" : tool;
|
|
882
|
+
const marker = `${tool === "cargo-nextest" ? "cargo-nextest" : command} ${release.version}`;
|
|
883
|
+
return {
|
|
884
|
+
name: tool.replaceAll("-", "_"),
|
|
885
|
+
text: `verify_${tool.replaceAll("-", "_")}() {
|
|
886
|
+
${Object.keys(release.amd64.binaries).map((binary) => `[[ -x ${quote(`${directory}/${binary}`)} ]] || return 1`).join("\n ")}
|
|
887
|
+
[[ "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(marker)} || "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(`${marker} `)}* ]]
|
|
888
|
+
}
|
|
889
|
+
install_${tool.replaceAll("-", "_")}() (
|
|
890
|
+
local artifact_url artifact_sha
|
|
891
|
+
# Subshell scope retains scratch for EXIT cleanup after a failed command.
|
|
892
|
+
if ! verify_${tool.replaceAll("-", "_")}; then
|
|
893
|
+
scratch="$(mktemp -d)"
|
|
894
|
+
trap 'rm -rf -- "$scratch"' EXIT
|
|
895
|
+
${artifactSelection(release)}
|
|
896
|
+
fetch_verified "$artifact_url" "$artifact_sha" "$scratch/archive.tar.gz"
|
|
897
|
+
tar -xzf "$scratch/archive.tar.gz" -C "$scratch"
|
|
898
|
+
install -d -m 755 ${quote(directory)}
|
|
899
|
+
case "$ARCH" in
|
|
900
|
+
${["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")}
|
|
901
|
+
esac
|
|
902
|
+
verify_${tool.replaceAll("-", "_")} || fail ${quote(`Installed ${tool} version differs from its pin`)}
|
|
903
|
+
fi
|
|
904
|
+
${Object.keys(release.amd64.binaries).map((binary) => ` ln -sfnT -- ${quote(`${directory}/${binary}`)} ${quote(`/usr/local/bin/${binary}`)}`).join("\n")}
|
|
905
|
+
)
|
|
906
|
+
`
|
|
907
|
+
};
|
|
908
|
+
});
|
|
909
|
+
return `${helpers}${rust === void 0 ? "" : rustInstall(rust)}\n${installs.map(({ text }) => text).join("\n")}
|
|
910
|
+
install_tools() {
|
|
911
|
+
${rust === void 0 ? ":" : "install_rust"}
|
|
912
|
+
${installs.map(({ name }) => `install_${name}`).join("\n ")}
|
|
913
|
+
}
|
|
914
|
+
verify_tools() {
|
|
915
|
+
${rust === void 0 ? ":" : "verify_rust"}
|
|
916
|
+
${installs.map(({ name }) => `verify_${name} || fail 'Pinned ${name} is unavailable; run install'`).join("\n ")}
|
|
917
|
+
}
|
|
918
|
+
`;
|
|
919
|
+
};
|
|
920
|
+
//#endregion
|
|
440
921
|
//#region src/toolchain-node.ts
|
|
441
922
|
const numericComponent = "(?:0|[1-9]\\d*)";
|
|
442
923
|
const policyPattern = new RegExp(`^${numericComponent}\\.x$`);
|
|
@@ -509,22 +990,24 @@ case "$(uname -m)" in
|
|
|
509
990
|
*) fail 'Only amd64 and arm64 are supported' ;;
|
|
510
991
|
esac
|
|
511
992
|
|
|
993
|
+
@NETWORK@
|
|
994
|
+
|
|
512
995
|
runtime_ready() {
|
|
513
996
|
[[ -x "$NODE_DIR/bin/node" && "$($NODE_DIR/bin/node --version)" == "v$NODE_VERSION" ]] || fail 'Pinned Node is unavailable; run install'
|
|
514
997
|
[[ -x "$BUN_DIR/bun" && "$($BUN_DIR/bun --version)" == "$BUN_VERSION" ]] || fail 'Pinned Bun is unavailable; run install'
|
|
515
998
|
}
|
|
516
999
|
install_runtimes() {
|
|
517
|
-
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
|
|
518
|
-
|
|
519
|
-
DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux
|
|
1000
|
+
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
|
|
1001
|
+
apt_network update
|
|
1002
|
+
DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
|
|
520
1003
|
fi
|
|
521
1004
|
local scratch node_archive bun_archive bun_folder
|
|
522
1005
|
scratch="$(mktemp -d)"
|
|
523
1006
|
trap 'rm -rf -- "$scratch"' RETURN
|
|
524
1007
|
if [[ ! -x "$NODE_DIR/bin/node" ]] || [[ "$($NODE_DIR/bin/node --version)" != "v$NODE_VERSION" ]]; then
|
|
525
1008
|
node_archive="node-v$NODE_VERSION-linux-$NODE_ARCH.tar.xz"
|
|
526
|
-
|
|
527
|
-
|
|
1009
|
+
download_file "https://nodejs.org/dist/v$NODE_VERSION/$node_archive" "$scratch/$node_archive" 120
|
|
1010
|
+
download_file "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt" "$scratch/node-sums" 45
|
|
528
1011
|
(cd "$scratch"; awk -v name="$node_archive" '$2 == name { print; count++ } END { if (count != 1) exit 1 }' node-sums | sha256sum --strict -c -)
|
|
529
1012
|
install -d -m 755 "$NODE_DIR"
|
|
530
1013
|
tar -xJf "$scratch/$node_archive" --strip-components=1 -C "$NODE_DIR"
|
|
@@ -532,8 +1015,8 @@ install_runtimes() {
|
|
|
532
1015
|
if [[ ! -x "$BUN_DIR/bun" ]] || [[ "$($BUN_DIR/bun --version)" != "$BUN_VERSION" ]]; then
|
|
533
1016
|
bun_folder="bun-linux-$BUN_ARCH"
|
|
534
1017
|
bun_archive="$bun_folder.zip"
|
|
535
|
-
|
|
536
|
-
|
|
1018
|
+
download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/$bun_archive" "$scratch/$bun_archive" 120
|
|
1019
|
+
download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/SHASUMS256.txt" "$scratch/bun-sums" 45
|
|
537
1020
|
(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 -)
|
|
538
1021
|
unzip -q "$scratch/$bun_archive" -d "$scratch/bun"
|
|
539
1022
|
install -d -m 755 "$BUN_DIR"
|
|
@@ -549,7 +1032,7 @@ install_runtimes() {
|
|
|
549
1032
|
}
|
|
550
1033
|
|
|
551
1034
|
dependency_install() {
|
|
552
|
-
local directory
|
|
1035
|
+
local directory download_hosts
|
|
553
1036
|
for directory in /var/cache/stll-cloud /var/cache/stll-cloud/bun; do
|
|
554
1037
|
[[ ! -L "$directory" ]] || fail 'Dependency cache parent must not be a symlink'
|
|
555
1038
|
if [[ -e "$directory" ]]; then
|
|
@@ -562,21 +1045,49 @@ dependency_install() {
|
|
|
562
1045
|
[[ -d "$BUN_CACHE" && "$(stat -c '%u' "$BUN_CACHE")" == "$OUTPUT_UID" ]] || fail 'Dependency cache ownership differs from the invoking user'
|
|
563
1046
|
fi
|
|
564
1047
|
install -d -m 700 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$BUN_CACHE"
|
|
565
|
-
|
|
1048
|
+
# Report hostnames only: lockfile/config URLs may contain credentials.
|
|
1049
|
+
# These are known install hosts; lifecycle scripts can contact other hosts.
|
|
1050
|
+
download_hosts="$(HOME="$OUTPUT_HOME" "$NODE_DIR/bin/node" -e '
|
|
1051
|
+
const fs = require("node:fs");
|
|
1052
|
+
const path = require("node:path");
|
|
1053
|
+
const hosts = new Set(["registry.npmjs.org"]);
|
|
1054
|
+
const inputs = [process.env.BUN_CONFIG_REGISTRY ?? "", process.env.NPM_CONFIG_REGISTRY ?? "", process.env.npm_config_registry ?? ""];
|
|
1055
|
+
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")]) {
|
|
1056
|
+
if (fs.existsSync(file)) inputs.push(fs.readFileSync(file, "utf8"));
|
|
1057
|
+
}
|
|
1058
|
+
for (const input of inputs) {
|
|
1059
|
+
for (const match of input.matchAll(/(?:git\+)?https?:\/\/[^\s"\x27<>\\]+/g)) {
|
|
1060
|
+
try { hosts.add(new URL(match[0].replace(/^git\+/, "")).hostname); } catch {}
|
|
1061
|
+
}
|
|
1062
|
+
if (/github:/.test(input)) { hosts.add("github.com"); hosts.add("codeload.github.com"); }
|
|
1063
|
+
}
|
|
1064
|
+
process.stdout.write([...hosts].sort().join(","));
|
|
1065
|
+
')"
|
|
1066
|
+
# Bun 1.4.3 socket inactivity timeout, in seconds; the outer bound includes scripts.
|
|
1067
|
+
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 --frozen-lockfile
|
|
566
1068
|
}
|
|
567
1069
|
|
|
568
1070
|
@SERVICE_INSTALL@
|
|
569
1071
|
@SERVICE_START@
|
|
570
1072
|
|
|
1073
|
+
@TOOL_INSTALL@
|
|
1074
|
+
|
|
1075
|
+
@INPUT_VALIDATION@
|
|
1076
|
+
|
|
571
1077
|
@ENV_FUNCTIONS@
|
|
572
1078
|
|
|
573
1079
|
if [[ "$MODE" == install ]]; then
|
|
574
1080
|
install_runtimes
|
|
575
1081
|
install_services
|
|
1082
|
+
install_tools
|
|
1083
|
+
install_shell_environment
|
|
576
1084
|
dependency_install
|
|
577
1085
|
exit 0
|
|
578
1086
|
fi
|
|
579
1087
|
runtime_ready
|
|
1088
|
+
verify_tools
|
|
1089
|
+
install_shell_environment
|
|
1090
|
+
verify_shell_environment
|
|
580
1091
|
@ENV_VALIDATE@
|
|
581
1092
|
prepare_state
|
|
582
1093
|
start_services
|
|
@@ -611,13 +1122,35 @@ write_environment() {
|
|
|
611
1122
|
}
|
|
612
1123
|
`;
|
|
613
1124
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
614
|
-
const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
1125
|
+
const generateCloudSetup = ({ policy, cloud, nodeVersion, files = {}, rustExceptions = [] }) => {
|
|
615
1126
|
const selected = parseCloudSetup(cloud);
|
|
616
1127
|
if (selected === void 0) throw new Error("cloud declaration is required");
|
|
617
1128
|
const exactNode = nodeVersion.trim();
|
|
618
1129
|
if ((0, semver.valid)(exactNode) !== exactNode || !nodeSelectorMatches(exactNode, policy.node)) throw new Error("cloud requires a tracked exact .node-version within the shared Node series");
|
|
1130
|
+
const toolchains = resolveCloudToolchains({
|
|
1131
|
+
cloud: selected,
|
|
1132
|
+
files,
|
|
1133
|
+
rust: policy.rust,
|
|
1134
|
+
rustExceptions
|
|
1135
|
+
});
|
|
619
1136
|
const services = cloudServices.filter((service) => selected.services.includes(service));
|
|
620
1137
|
const replacements = /* @__PURE__ */ new Map([
|
|
1138
|
+
["@NETWORK@", renderCloudNetwork()],
|
|
1139
|
+
["@TOOL_INSTALL@", renderCloudToolInstall(toolchains) + renderCloudShellInstall({
|
|
1140
|
+
nodeVersion: exactNode,
|
|
1141
|
+
bunVersion: policy.bun,
|
|
1142
|
+
rust: toolchains.rust
|
|
1143
|
+
})],
|
|
1144
|
+
["@INPUT_VALIDATION@", [
|
|
1145
|
+
"rust-toolchain.toml",
|
|
1146
|
+
"rust-toolchain",
|
|
1147
|
+
...selected.tools?.["wasm-bindgen-cli"] === void 0 ? [] : ["Cargo.lock"]
|
|
1148
|
+
].flatMap((file) => {
|
|
1149
|
+
const content = files[file];
|
|
1150
|
+
if (content === void 0) return [`[[ ! -e ${shellQuote(file)} && ! -L ${shellQuote(file)} ]] || fail ${shellQuote(`${file} appeared; regenerate cloud-setup.sh`)}`];
|
|
1151
|
+
const hash = (0, node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
1152
|
+
return [`[[ -f ${shellQuote(file)} && ! -L ${shellQuote(file)} ]] && [[ "$(sha256sum -- ${shellQuote(file)} | cut -d ' ' -f1)" == ${shellQuote(hash)} ]] || fail ${shellQuote(`${file} changed; regenerate cloud-setup.sh`)}`];
|
|
1153
|
+
}).join("\n")],
|
|
621
1154
|
["@BUN@", policy.bun],
|
|
622
1155
|
["@NODE@", exactNode],
|
|
623
1156
|
["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
|
|
@@ -643,6 +1176,87 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
|
643
1176
|
});
|
|
644
1177
|
};
|
|
645
1178
|
//#endregion
|
|
1179
|
+
//#region src/cloud-setup-claude.ts
|
|
1180
|
+
const cloudClaudeSettingsPath = ".claude/settings.json";
|
|
1181
|
+
const cloudSessionStartHook = {
|
|
1182
|
+
matcher: "",
|
|
1183
|
+
hooks: [{
|
|
1184
|
+
type: "command",
|
|
1185
|
+
command: `if [ "\${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
|
|
1186
|
+
bash ${cloudSetupPath} start || exit "$?"
|
|
1187
|
+
if [ -n "\${CLAUDE_ENV_FILE:-}" ]; then
|
|
1188
|
+
if [ ! -f ${cloudShellProfilePath} ]; then
|
|
1189
|
+
printf '%s\\n' 'cloud-setup: cloud shell profile is unavailable; run install' >&2
|
|
1190
|
+
exit 1
|
|
1191
|
+
fi
|
|
1192
|
+
printf '\\n' >> "$CLAUDE_ENV_FILE" || exit "$?"
|
|
1193
|
+
cat ${cloudShellProfilePath} >> "$CLAUDE_ENV_FILE"
|
|
1194
|
+
fi
|
|
1195
|
+
fi`
|
|
1196
|
+
}]
|
|
1197
|
+
};
|
|
1198
|
+
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1199
|
+
const array = (value) => Array.isArray(value);
|
|
1200
|
+
const parseSettings = (text) => {
|
|
1201
|
+
try {
|
|
1202
|
+
return JSON.parse(text, (_key, value) => {
|
|
1203
|
+
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");
|
|
1204
|
+
return value;
|
|
1205
|
+
});
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
if (error instanceof SyntaxError) throw new Error("settings must contain valid JSON");
|
|
1208
|
+
throw error;
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
const readSettings = (text) => {
|
|
1212
|
+
const settings = text === void 0 ? {} : parseSettings(text);
|
|
1213
|
+
if (text !== void 0 && (0, yaml.parseDocument)(text, { uniqueKeys: true }).errors.length > 0) throw new Error("settings contains duplicate or ambiguous JSON keys");
|
|
1214
|
+
if (!record$1(settings)) throw new Error("settings must be a JSON object");
|
|
1215
|
+
if (settings["disableAllHooks"] === true) throw new Error("disableAllHooks prevents SessionStart from running");
|
|
1216
|
+
const hooks = settings["hooks"] === void 0 ? {} : settings["hooks"];
|
|
1217
|
+
if (!record$1(hooks)) throw new Error("hooks must be a JSON object");
|
|
1218
|
+
const sessionStart = hooks["SessionStart"] === void 0 ? [] : hooks["SessionStart"];
|
|
1219
|
+
if (!array(sessionStart) || sessionStart.some((entry) => !record$1(entry) || !array(entry["hooks"]))) throw new Error("hooks.SessionStart must be an array of hook groups");
|
|
1220
|
+
return {
|
|
1221
|
+
settings,
|
|
1222
|
+
hooks,
|
|
1223
|
+
sessionStart
|
|
1224
|
+
};
|
|
1225
|
+
};
|
|
1226
|
+
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")));
|
|
1227
|
+
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);
|
|
1228
|
+
/** Append only when the existing settings have no conflicting cloud startup. */
|
|
1229
|
+
const mergeCloudSessionStartHook = (text) => {
|
|
1230
|
+
try {
|
|
1231
|
+
const { settings, hooks, sessionStart } = readSettings(text);
|
|
1232
|
+
const cloudGroups = cloudHookGroups(sessionStart);
|
|
1233
|
+
if (cloudGroups.length !== 0) {
|
|
1234
|
+
if (cloudGroups.length === 1 && (0, node_util.isDeepStrictEqual)(cloudGroups.at(0), cloudSessionStartHook) && text !== void 0) return text;
|
|
1235
|
+
throw new Error("existing cloud setup SessionStart hook differs");
|
|
1236
|
+
}
|
|
1237
|
+
return `${JSON.stringify({
|
|
1238
|
+
...settings,
|
|
1239
|
+
hooks: {
|
|
1240
|
+
...hooks,
|
|
1241
|
+
SessionStart: [...sessionStart, cloudSessionStartHook]
|
|
1242
|
+
}
|
|
1243
|
+
}, null, 2)}\n`;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
throw new Error(`cannot safely merge ${cloudClaudeSettingsPath}: ${error instanceof Error ? error.message : String(error)}; ${cloudSessionStartFix}`);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
/** Settings formatting and unrelated hooks do not affect the generated contract. */
|
|
1249
|
+
const hasCloudSessionStartHook = (text) => {
|
|
1250
|
+
if (text === void 0) return false;
|
|
1251
|
+
try {
|
|
1252
|
+
const { sessionStart } = readSettings(text);
|
|
1253
|
+
const cloudGroups = cloudHookGroups(sessionStart);
|
|
1254
|
+
return cloudGroups.length === 1 && (0, node_util.isDeepStrictEqual)(cloudGroups.at(0), cloudSessionStartHook);
|
|
1255
|
+
} catch {
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
//#endregion
|
|
646
1260
|
//#region src/toolchain-workspaces.ts
|
|
647
1261
|
/** Match manifest directories with the same positive and negative workspace patterns. */
|
|
648
1262
|
const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
@@ -809,13 +1423,25 @@ const isDockerDefinitionPath = (file) => {
|
|
|
809
1423
|
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));
|
|
810
1424
|
/** Lockfiles establish ecosystem presence without reading their dependency graphs. */
|
|
811
1425
|
const toolchainInputKind = (file, text = "") => {
|
|
812
|
-
if (file === ".agents/cloud-setup.sh") return "config";
|
|
1426
|
+
if (file === ".agents/cloud-setup.sh" || file === ".claude/settings.json" || file === "Cargo.lock") return "config";
|
|
813
1427
|
if (githubAutomationFileKind(file) !== void 0) return "config";
|
|
814
1428
|
if (excludedInputPath(file)) return void 0;
|
|
815
1429
|
if (pythonDependencyManifestKind(file) === "pipfile-lock" || javascriptDependencyLockfiles.some((name) => file.split("/").at(-1) === name) || /(?:^|\/)uv\.lock$/.test(file)) return "presence";
|
|
816
1430
|
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";
|
|
817
1431
|
};
|
|
818
1432
|
//#endregion
|
|
1433
|
+
Object.defineProperty(exports, "cloudClaudeSettingsPath", {
|
|
1434
|
+
enumerable: true,
|
|
1435
|
+
get: function() {
|
|
1436
|
+
return cloudClaudeSettingsPath;
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
Object.defineProperty(exports, "cloudSessionStartFix", {
|
|
1440
|
+
enumerable: true,
|
|
1441
|
+
get: function() {
|
|
1442
|
+
return cloudSessionStartFix;
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
819
1445
|
Object.defineProperty(exports, "cloudSetupPath", {
|
|
820
1446
|
enumerable: true,
|
|
821
1447
|
get: function() {
|
|
@@ -840,6 +1466,12 @@ Object.defineProperty(exports, "githubAutomationFileKind", {
|
|
|
840
1466
|
return githubAutomationFileKind;
|
|
841
1467
|
}
|
|
842
1468
|
});
|
|
1469
|
+
Object.defineProperty(exports, "hasCloudSessionStartHook", {
|
|
1470
|
+
enumerable: true,
|
|
1471
|
+
get: function() {
|
|
1472
|
+
return hasCloudSessionStartHook;
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
843
1475
|
Object.defineProperty(exports, "isComposeDefinitionPath", {
|
|
844
1476
|
enumerable: true,
|
|
845
1477
|
get: function() {
|
|
@@ -888,6 +1520,12 @@ Object.defineProperty(exports, "javascriptDependencyLockfiles", {
|
|
|
888
1520
|
return javascriptDependencyLockfiles;
|
|
889
1521
|
}
|
|
890
1522
|
});
|
|
1523
|
+
Object.defineProperty(exports, "mergeCloudSessionStartHook", {
|
|
1524
|
+
enumerable: true,
|
|
1525
|
+
get: function() {
|
|
1526
|
+
return mergeCloudSessionStartHook;
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
891
1529
|
Object.defineProperty(exports, "nodePolicyValid", {
|
|
892
1530
|
enumerable: true,
|
|
893
1531
|
get: function() {
|