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