@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,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 apt-get install -y --no-install-recommends "$@"
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
- curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 https://www.postgresql.org/media/keys/ACCC4CF8.asc -o "$key"
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
- apt-get update
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
- curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "$server_url" -o "$scratch/valkey-server.deb"
189
- curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "$tools_url" -o "$scratch/valkey-tools.deb"
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
- apt-get update
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
- envFile
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
- apt-get update
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
- 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"
527
- 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"
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
- 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"
536
- 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"
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
- runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" BUN_INSTALL_CACHE_DIR="$BUN_CACHE" PATH="$PATH" "$BUN_DIR/bun" install --frozen-lockfile
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() {