@stll/oxlint-config 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +97 -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 +25 -15
  24. package/dist/toolchain-changed.cjs.map +1 -1
  25. package/dist/toolchain-changed.mjs +27 -17
  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 +72 -48
  30. package/dist/toolchain-guard.cjs.map +1 -1
  31. package/dist/toolchain-guard.mjs +79 -55
  32. package/dist/toolchain-guard.mjs.map +1 -1
  33. package/dist/toolchain-inputs.cjs +803 -40
  34. package/dist/toolchain-inputs.cjs.map +1 -1
  35. package/dist/toolchain-inputs.mjs +768 -41
  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_refresh_for postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
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_refresh_for "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
197
201
  apt_install "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
198
202
  rm -rf -- "$scratch"
199
203
  fi
@@ -257,18 +261,267 @@ 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
+ apt_refresh_for() {
302
+ # Skip the refresh only when the install downloads nothing: then no package
303
+ # source is contacted and stale cached indexes cannot point at missing files.
304
+ local uris
305
+ if uris="$(DEBIAN_FRONTEND=noninteractive apt-get --print-uris -qq -y --no-install-recommends install "$@" 2>/dev/null)" && [[ -z "$uris" ]]; then
306
+ return 0
307
+ fi
308
+ apt_network update
309
+ }
310
+ `;
311
+ //#endregion
312
+ //#region src/cloud-setup-artifacts.ts
313
+ const wasmBindgen0126 = {
314
+ version: "0.2.126",
315
+ amd64: {
316
+ 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",
317
+ sha256: "064948d58e2d6c0a745216477a639ba696216d6309aaa902939d1b865b1d869d",
318
+ archive: "tar.gz",
319
+ binaries: {
320
+ "wasm-bindgen-test-runner": "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm-bindgen-test-runner",
321
+ wasm2es6js: "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm2es6js",
322
+ "wasm-bindgen": "wasm-bindgen-0.2.126-x86_64-unknown-linux-musl/wasm-bindgen"
323
+ }
324
+ },
325
+ arm64: {
326
+ url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.126/wasm-bindgen-0.2.126-aarch64-unknown-linux-musl.tar.gz",
327
+ sha256: "2245120254a9f6c9a9adf3601f3d52bb31309219e9ceab7696e74e24885c440a",
328
+ archive: "tar.gz",
329
+ binaries: {
330
+ "wasm-bindgen-test-runner": "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm-bindgen-test-runner",
331
+ wasm2es6js: "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm2es6js",
332
+ "wasm-bindgen": "wasm-bindgen-0.2.126-aarch64-unknown-linux-musl/wasm-bindgen"
333
+ }
334
+ }
335
+ };
336
+ /** Pinned upstream Linux artifacts, verified against their published SHA-256 sums. */
337
+ const cloudSetupArtifacts = {
338
+ "rustup-init": {
339
+ version: "1.29.1",
340
+ amd64: {
341
+ url: "https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init",
342
+ sha256: "dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71",
343
+ archive: "binary",
344
+ binaries: { "rustup-init": "rustup-init" }
345
+ },
346
+ arm64: {
347
+ url: "https://static.rust-lang.org/rustup/archive/1.29.1/aarch64-unknown-linux-gnu/rustup-init",
348
+ sha256: "15f6e4ce9f583b929c996c91562bad6d4454f3281de858b02cdfdef615fac433",
349
+ archive: "binary",
350
+ binaries: { "rustup-init": "rustup-init" }
351
+ }
352
+ },
353
+ "cargo-nextest": {
354
+ version: "0.9.148",
355
+ amd64: {
356
+ 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",
357
+ sha256: "bf92cb81450872d960ffa13857a83084d557ec0cdc141663d263bb23ef756f97",
358
+ archive: "tar.gz",
359
+ binaries: { "cargo-nextest": "cargo-nextest" }
360
+ },
361
+ arm64: {
362
+ 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",
363
+ sha256: "94676506ceaed01746d78cfe9ed124ee841b2932fd18d9d10f438a347bd9ba5e",
364
+ archive: "tar.gz",
365
+ binaries: { "cargo-nextest": "cargo-nextest" }
366
+ }
367
+ },
368
+ "wasm-bindgen-cli": {
369
+ version: "0.2.129",
370
+ amd64: {
371
+ 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",
372
+ sha256: "82d12bb940e2d4e72e0d5605387fc1b8ca179044e012b620f0ce4e7440e8320e",
373
+ archive: "tar.gz",
374
+ binaries: {
375
+ "wasm-bindgen-test-runner": "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm-bindgen-test-runner",
376
+ wasm2es6js: "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm2es6js",
377
+ "wasm-bindgen": "wasm-bindgen-0.2.129-x86_64-unknown-linux-musl/wasm-bindgen"
378
+ }
379
+ },
380
+ arm64: {
381
+ url: "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.129/wasm-bindgen-0.2.129-aarch64-unknown-linux-musl.tar.gz",
382
+ sha256: "2ed4351c35dd9440308bbb02767d47ea278efe851a52465300f3c94f5b6c2a87",
383
+ archive: "tar.gz",
384
+ binaries: {
385
+ "wasm-bindgen-test-runner": "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm-bindgen-test-runner",
386
+ wasm2es6js: "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm2es6js",
387
+ "wasm-bindgen": "wasm-bindgen-0.2.129-aarch64-unknown-linux-musl/wasm-bindgen"
388
+ }
389
+ }
390
+ },
391
+ uv: {
392
+ version: "0.13.1",
393
+ amd64: {
394
+ url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-x86_64-unknown-linux-musl.tar.gz",
395
+ sha256: "6c86040a8f045ab9adc9e3ec4cdafa03fd00e9a988ed2bf28971c7fc2d0debd0",
396
+ archive: "tar.gz",
397
+ binaries: {
398
+ uv: "uv-x86_64-unknown-linux-musl/uv",
399
+ uvx: "uv-x86_64-unknown-linux-musl/uvx"
400
+ }
401
+ },
402
+ arm64: {
403
+ url: "https://github.com/astral-sh/uv/releases/download/0.13.1/uv-aarch64-unknown-linux-musl.tar.gz",
404
+ sha256: "345a8308e4d1835cdfaadf692836fd3137a6d45864773f05ad7bf59442b1ba16",
405
+ archive: "tar.gz",
406
+ binaries: {
407
+ uv: "uv-aarch64-unknown-linux-musl/uv",
408
+ uvx: "uv-aarch64-unknown-linux-musl/uvx"
409
+ }
410
+ }
411
+ }
412
+ };
413
+ /** Explicit releases whose CLI must match a project's wasm-bindgen crate exactly. */
414
+ const wasmBindgenArtifactReleases = [wasmBindgen0126, cloudSetupArtifacts["wasm-bindgen-cli"]];
415
+ /** Every supported version participates in lookup and catalog validation. */
416
+ const cloudSetupArtifactVersions = {
417
+ "rustup-init": [cloudSetupArtifacts["rustup-init"]],
418
+ "cargo-nextest": [cloudSetupArtifacts["cargo-nextest"]],
419
+ "wasm-bindgen-cli": wasmBindgenArtifactReleases,
420
+ uv: [cloudSetupArtifacts.uv]
421
+ };
422
+ /** A new version requires an explicit, verified catalog entry; never build from source. */
423
+ const getCloudSetupArtifact = (tool, version) => {
424
+ const releases = cloudSetupArtifactVersions[tool];
425
+ const artifact = releases.find((release) => release.version === version);
426
+ if (artifact === void 0) throw new Error(`Unsupported ${tool} version ${version}; supported versions: ${releases.map((release) => release.version).join(", ")}`);
427
+ return artifact;
428
+ };
429
+ /** Ubuntu 24.04 packages; apt resolves them from the image's configured sources. */
430
+ const cloudSetupPackages = { time: {
431
+ version: "1.9",
432
+ package: "time",
433
+ binary: "/usr/bin/time",
434
+ identity: "time (GNU Time) "
435
+ } };
436
+ /** Ubuntu 24.04 ships one release per package; other versions are unsupported. */
437
+ const getCloudSetupPackage = (tool, version) => {
438
+ const release = cloudSetupPackages[tool];
439
+ if (release.version !== version) throw new Error(`Unsupported ${tool} version ${version}; supported versions: ${release.version}`);
440
+ return release;
441
+ };
442
+ //#endregion
260
443
  //#region src/cloud-setup-schema.ts
261
444
  const cloudServices = ["postgres", "valkey"];
445
+ const cloudArtifactTools = Object.keys(cloudSetupArtifacts).filter((tool) => tool !== "rustup-init" && Object.hasOwn(cloudSetupArtifacts, tool));
446
+ const cloudPackageTools = Object.keys(cloudSetupPackages).filter((tool) => Object.hasOwn(cloudSetupPackages, tool));
447
+ const cloudExtraTools = [...cloudArtifactTools, ...cloudPackageTools];
448
+ const isCloudPackageTool = (tool) => cloudPackageTools.some((candidate) => candidate === tool);
449
+ const object = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
450
+ const exactPin = (input) => typeof input === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(input);
451
+ const exactToolPin = (tool, input) => isCloudPackageTool(tool) ? typeof input === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))?$/.test(input) : exactPin(input);
452
+ const cloudRustComponentBinaries = {
453
+ clippy: ["cargo-clippy", "clippy-driver"],
454
+ rustfmt: ["rustfmt", "cargo-fmt"],
455
+ "rust-src": [],
456
+ "rust-analyzer": ["rust-analyzer"]
457
+ };
458
+ const parseCloudRustNames = (input, kind) => {
459
+ const label = `cloud Rust ${kind}`;
460
+ if (input === void 0) return [];
461
+ if (!Array.isArray(input)) throw new Error(`${label} must be an array`);
462
+ const result = input.map((value) => {
463
+ if (typeof value !== "string" || !/^[a-z][a-z0-9_-]*$/.test(value)) throw new Error(`${label} requires canonical names`);
464
+ if (kind === "components" && !Object.hasOwn(cloudRustComponentBinaries, value)) throw new Error(`${label} contains an unsupported component: ${value}`);
465
+ return value;
466
+ });
467
+ if (new Set(result).size !== result.length) throw new Error(`${label} contains duplicates`);
468
+ return result;
469
+ };
470
+ const parseRust = (input) => {
471
+ if (input === void 0) return void 0;
472
+ if (!object(input) || Object.keys(input).some((key) => ![
473
+ "toolchain",
474
+ "components",
475
+ "targets"
476
+ ].includes(key))) throw new Error("cloud.rust accepts only toolchain, components and targets");
477
+ const toolchain = input["toolchain"];
478
+ if (toolchain !== void 0 && !exactPin(toolchain)) throw new Error("cloud.rust.toolchain must be an exact stable release");
479
+ return {
480
+ ...toolchain === void 0 ? {} : { toolchain },
481
+ components: parseCloudRustNames(input["components"], "components"),
482
+ targets: parseCloudRustNames(input["targets"], "targets")
483
+ };
484
+ };
485
+ const parseTools = (input) => {
486
+ if (input === void 0) return void 0;
487
+ if (!object(input) || Object.keys(input).some((key) => !cloudExtraTools.some((tool) => tool === key))) throw new Error(`cloud.tools accepts only ${cloudExtraTools.join(", ")}`);
488
+ const result = {};
489
+ for (const tool of cloudExtraTools) {
490
+ const pin = input[tool];
491
+ if (pin === void 0) {
492
+ if (Object.hasOwn(input, tool)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
493
+ continue;
494
+ }
495
+ if (!exactToolPin(tool, pin)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
496
+ result[tool] = pin;
497
+ }
498
+ return result;
499
+ };
262
500
  const cloudInstallCommand = "bun install --frozen-lockfile";
501
+ const cloudInstallCommands = [cloudInstallCommand, `${cloudInstallCommand} --ignore-scripts`];
502
+ const cloudInstall = (value) => cloudInstallCommands.some((command) => value === command);
263
503
  const cloudService = (value) => cloudServices.some((service) => value === service);
504
+ const parseEnvFile = (input) => {
505
+ if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
506
+ const envFile = input.envFile;
507
+ 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");
508
+ return envFile;
509
+ };
264
510
  /** Parse an explicit declaration without assuming services for undeclared repositories. */
265
511
  const parseCloudSetup = (input) => {
266
512
  if (input === void 0) return void 0;
267
513
  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}`);
514
+ 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}`);
515
+ const rust = parseRust("rust" in input ? input.rust : void 0);
516
+ const tools = parseTools("tools" in input ? input.tools : void 0);
517
+ const extensions = {
518
+ ...rust === void 0 ? {} : { rust },
519
+ ...tools === void 0 ? {} : { tools }
520
+ };
269
521
  const selected = "services" in input ? input.services : [];
270
522
  if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
271
- if (!("install" in input) || input.install !== "bun install --frozen-lockfile") throw new Error(`cloud.install must be exactly ${cloudInstallCommand}`);
523
+ if (!("install" in input) || !cloudInstall(input.install)) throw new Error(`cloud.install must be exactly one of: ${cloudInstallCommands.join(", ")}`);
524
+ const install = input.install;
272
525
  const seen = /* @__PURE__ */ new Set();
273
526
  const services = Array.from(selected, (service) => {
274
527
  if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
@@ -276,20 +529,12 @@ const parseCloudSetup = (input) => {
276
529
  seen.add(service);
277
530
  return service;
278
531
  });
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");
532
+ if (services.length === 0 && "envFile" in input) throw new Error("cloud.envFile is not allowed without services");
289
533
  return {
290
534
  services,
291
- install: cloudInstallCommand,
292
- envFile
535
+ install,
536
+ ...extensions,
537
+ ...services.length === 0 ? {} : { envFile: parseEnvFile(input) }
293
538
  };
294
539
  };
295
540
  //#endregion
@@ -356,7 +601,7 @@ service_prune_pid() {
356
601
  }
357
602
  `;
358
603
  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
604
+ 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
605
  service_password "$STATE/postgres.password"
361
606
  pg_password="$(cat "$STATE/postgres.password")"
362
607
  service_directory "$pg_data"
@@ -375,6 +620,7 @@ const postgresStart = String.raw`
375
620
  service_prune_pid "$pg_data/postmaster.pid" "$pg_bin/postgres" postgres
376
621
  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
622
  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"
623
+ pg_state=started
378
624
  fi
379
625
  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
626
  pg_version="$(printf '%s\n' "$pg_details" | cut -d '|' -f4)"
@@ -384,9 +630,10 @@ const postgresStart = String.raw`
384
630
  service_process "$pg_pid" "$pg_bin/postgres"
385
631
  [[ "$(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
632
  DATABASE_URL="postgresql://stll_cloud:$pg_password@127.0.0.1:55432/cloud_test"
633
+ printf 'postgres: %s\n' "$pg_state"
387
634
  `;
388
635
  const valkeyStart = String.raw`
389
- local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0
636
+ local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0 vk_state='already running'
390
637
  service_password "$STATE/valkey.password"
391
638
  vk_password="$(cat "$STATE/valkey.password")"
392
639
  service_directory "$vk_data"
@@ -406,7 +653,8 @@ const valkeyStart = String.raw`
406
653
  [[ "$(cat "$vk_config")" == "$vk_expected" ]] || fail "Valkey configuration differs from managed settings"
407
654
  service_prune_pid "$vk_data/valkey.pid" "$(command -v valkey-server)" valkey
408
655
  if [[ ! -e "$vk_data/valkey.pid" ]]; then
409
- service_run valkey-server "$vk_config" || fail "Valkey start failed; port 56379 must be available"
656
+ service_run valkey-server "$vk_config" >/dev/null || fail "Valkey start failed; port 56379 must be available"
657
+ vk_state=started
410
658
  fi
411
659
  for ((attempt=0; attempt<20; attempt++)); do
412
660
  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 +670,7 @@ const valkeyStart = String.raw`
422
670
  [[ "$(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
671
  [[ "$(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
672
  REDIS_URL="redis://:$vk_password@127.0.0.1:56379/0"
673
+ printf 'valkey: %s\n' "$vk_state"
425
674
  `;
426
675
  /** Render only declared services; installation and environment-file ownership stay with the caller. */
427
676
  const renderCloudServiceStart = ({ services, postgres, valkey }) => {
@@ -437,6 +686,300 @@ const renderCloudServiceStart = ({ services, postgres, valkey }) => {
437
686
  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
687
  };
439
688
  //#endregion
689
+ //#region src/cloud-setup-shell.ts
690
+ const cloudShellProfilePath = "/etc/profile.d/stll-cloud.sh";
691
+ const cloudShellProfileSource = `. ${cloudShellProfilePath}`;
692
+ const quote$1 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
693
+ const renderCloudShellProfile = ({ nodeVersion, bunVersion, rust }) => {
694
+ const node = `/opt/stll-cloud/node/${nodeVersion}/bin`;
695
+ const bun = `/opt/stll-cloud/bun/${bunVersion}`;
696
+ return `# Generated by stll-cloud-setup; managed cloud shell environment.
697
+ # Remove managed entries before prepending, including repeated shell startup.
698
+ _stll_cloud_remaining="\${PATH-}"
699
+ _stll_cloud_result=''
700
+ _stll_cloud_separator=''
701
+ while :; do
702
+ case "$_stll_cloud_remaining" in
703
+ *:*) _stll_cloud_part="\${_stll_cloud_remaining%%:*}"; _stll_cloud_remaining="\${_stll_cloud_remaining#*:}"; _stll_cloud_more=yes ;;
704
+ *) _stll_cloud_part="$_stll_cloud_remaining"; _stll_cloud_more=no ;;
705
+ esac
706
+ case "$_stll_cloud_part" in
707
+ ${quote$1(node)}|${quote$1(bun)}|/usr/local/bin) ;;
708
+ *) _stll_cloud_result="$_stll_cloud_result$_stll_cloud_separator$_stll_cloud_part"; _stll_cloud_separator=: ;;
709
+ esac
710
+ [ "$_stll_cloud_more" = yes ] || break
711
+ done
712
+ export PATH=${quote$1(`${node}:${bun}:/usr/local/bin`)}"\${_stll_cloud_separator:+:}$_stll_cloud_result"
713
+ unset _stll_cloud_remaining _stll_cloud_result _stll_cloud_separator _stll_cloud_part _stll_cloud_more
714
+ ${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`}`;
715
+ };
716
+ const userStartup = String.raw`
717
+ set -euo pipefail
718
+ for name in .bashrc .profile .bash_profile .bash_login; do
719
+ file="$HOME/$name"
720
+ if [[ ! -e "$file" && ! -L "$file" && "$name" != .bashrc && "$name" != .profile ]]; then continue; fi
721
+ [[ ! -L "$file" ]] || { echo "cloud-setup: shell startup file must not be a symlink: $name" >&2; exit 1; }
722
+ if [[ -e "$file" ]]; then
723
+ [[ -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; }
724
+ fi
725
+ temporary="$(mktemp "$HOME/.stll-cloud-shell.XXXXXX")"
726
+ trap 'rm -f -- "$temporary"' EXIT
727
+ if [[ "$name" == .bashrc ]]; then
728
+ printf '%s\n' '# stll-cloud: early environment' '@SOURCE@' > "$temporary"
729
+ fi
730
+ if [[ -f "$file" ]]; then
731
+ # Remove only exact managed blocks; preserve unrelated startup lines and permissions.
732
+ awk '
733
+ $0 == "# stll-cloud: early environment" || $0 == "# stll-cloud: session environment" {
734
+ if ((getline following) <= 0 || following != "@SOURCE@") exit 1
735
+ next
736
+ }
737
+ { print }
738
+ ' "$file" >> "$temporary" || { echo "cloud-setup: managed shell startup block differs: $name" >&2; exit 1; }
739
+ fi
740
+ printf '%s\n' '# stll-cloud: session environment' '@SOURCE@' >> "$temporary"
741
+ if [[ -f "$file" ]] && cmp -s "$temporary" "$file"; then rm -- "$temporary"; continue; fi
742
+ if [[ -f "$file" ]]; then chmod --reference="$file" "$temporary"; else chmod 644 "$temporary"; fi
743
+ mv -f -- "$temporary" "$file"
744
+ done
745
+ `;
746
+ const renderCloudShellInstall = (options) => {
747
+ const profile = renderCloudShellProfile(options);
748
+ return `install_shell_environment() (
749
+ # Subshell scope retains temporary for EXIT cleanup after a failed command.
750
+ [[ ! -L ${quote$1(cloudShellProfilePath)} ]] || fail 'Managed shell profile must not be a symlink'
751
+ if [[ -e ${quote$1(cloudShellProfilePath)} ]]; then
752
+ [[ -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'
753
+ fi
754
+ temporary="$(mktemp)"
755
+ trap 'rm -f -- "$temporary"' EXIT
756
+ cat > "$temporary" <<'STLL_CLOUD_PROFILE'
757
+ ${profile}STLL_CLOUD_PROFILE
758
+ if ! cmp -s "$temporary" ${quote$1(cloudShellProfilePath)}; then install -m 644 "$temporary" ${quote$1(cloudShellProfilePath)}; fi
759
+ runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" bash -c ${quote$1(userStartup.replaceAll("@SOURCE@", cloudShellProfileSource))}
760
+ )
761
+ verify_shell_environment() {
762
+ [[ -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'
763
+ [[ "$(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'
764
+ }
765
+ `;
766
+ };
767
+ //#endregion
768
+ //#region src/cloud-setup-toolchains.ts
769
+ const record$2 = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
770
+ /** Resolve the same tracked snapshot for generation and the mandatory drift check. */
771
+ const resolveCloudToolchains = ({ cloud, files, rust, rustExceptions }) => {
772
+ const source = files["rust-toolchain.toml"];
773
+ const selector = files["rust-toolchain"];
774
+ if (source !== void 0 && selector !== void 0) throw new Error("cloud Rust requires one root rust-toolchain.toml or rust-toolchain file");
775
+ let declared;
776
+ if (source !== void 0 || selector?.trimStart().startsWith("[")) {
777
+ const parsed = (0, smol_toml.parse)(source ?? selector ?? "");
778
+ if (!record$2(parsed["toolchain"])) throw new Error("cloud Rust requires [toolchain]");
779
+ declared = parsed["toolchain"];
780
+ if (Object.keys(declared).some((key) => ![
781
+ "channel",
782
+ "components",
783
+ "targets",
784
+ "profile"
785
+ ].includes(key))) throw new Error("cloud Rust supports only channel, components, targets and profile");
786
+ if (declared["profile"] !== void 0 && declared["profile"] !== "minimal") throw new Error("cloud Rust requires profile = minimal; declare required components explicitly");
787
+ } else if (selector !== void 0) declared = { channel: selector.trim() };
788
+ const rustEnabled = declared !== void 0 || cloud.rust !== void 0;
789
+ const channel = declared === void 0 ? cloud.rust?.toolchain ?? rust : declared["channel"];
790
+ if (rustEnabled) {
791
+ if (typeof channel !== "string" || !/^\d+\.\d+\.\d+$/.test(channel)) throw new Error("cloud Rust requires an exact stable toolchain");
792
+ if (channel !== rust && !rustExceptions.some(({ pin }) => pin === channel)) throw new Error(`cloud Rust must use shared Rust ${rust} or a recorded rustExceptions pin`);
793
+ if (cloud.rust?.toolchain !== void 0 && cloud.rust.toolchain !== channel) throw new Error("cloud.rust.toolchain conflicts with root Rust toolchain");
794
+ }
795
+ const components = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["components"], "components"), ...cloud.rust?.components ?? []])].sort();
796
+ const targets = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["targets"], "targets"), ...cloud.rust?.targets ?? []])].sort();
797
+ const resolveTool = (tool, version) => {
798
+ if (isCloudPackageTool(tool)) return {
799
+ type: "package",
800
+ tool,
801
+ release: getCloudSetupPackage(tool, version)
802
+ };
803
+ if (tool === "cargo-nextest" && !rustEnabled) throw new Error("cloud.tools.cargo-nextest requires a Rust toolchain");
804
+ if (tool === "wasm-bindgen-cli") {
805
+ const lock = files["Cargo.lock"];
806
+ if (lock === void 0) throw new Error("wasm-bindgen-cli requires tracked root Cargo.lock");
807
+ const packages = (0, smol_toml.parse)(lock)["package"];
808
+ if (!Array.isArray(packages)) throw new Error("Cargo.lock requires package entries for wasm-bindgen-cli");
809
+ const versions = new Set(packages.flatMap((entry) => record$2(entry) && entry["name"] === "wasm-bindgen" ? [entry["version"]] : []));
810
+ 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"})`);
811
+ }
812
+ return {
813
+ type: "artifact",
814
+ tool,
815
+ release: getCloudSetupArtifact(tool, version)
816
+ };
817
+ };
818
+ const tools = cloudExtraTools.flatMap((tool) => {
819
+ const version = cloud.tools?.[tool];
820
+ return version === void 0 ? [] : [resolveTool(tool, version)];
821
+ });
822
+ return {
823
+ rust: rustEnabled && typeof channel === "string" ? {
824
+ channel,
825
+ components,
826
+ targets,
827
+ installer: cloudSetupArtifacts["rustup-init"]
828
+ } : void 0,
829
+ tools
830
+ };
831
+ };
832
+ //#endregion
833
+ //#region src/cloud-setup-tools-install.ts
834
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
835
+ const artifactSelection = (release) => `case "$ARCH" in
836
+ ${["amd64", "arm64"].map((arch) => ` ${arch}) artifact_url=${quote(release[arch].url)}; artifact_sha=${quote(release[arch].sha256)} ;;`).join("\n")}
837
+ esac`;
838
+ const helpers = String.raw`
839
+ fetch_verified() {
840
+ download_file "$1" "$3" 120
841
+ printf '%s %s\n' "$2" "$3" | sha256sum --strict -c - >/dev/null
842
+ }
843
+ `;
844
+ const rustInstall = (rust) => {
845
+ const args = [...rust.components.flatMap((name) => ["--component", name]), ...rust.targets.flatMap((name) => ["--target", name])].map(quote).join(" ");
846
+ return String.raw`
847
+ export RUSTUP_HOME=/opt/stll-cloud/rustup
848
+ export CARGO_HOME=/opt/stll-cloud/cargo
849
+ export RUSTUP_TOOLCHAIN='@RUST@'
850
+ export RUSTUP_AUTO_INSTALL=0
851
+ rust_run() {
852
+ local mode="$1"
853
+ shift
854
+ 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" "$@"
855
+ case "$mode" in
856
+ local) "$@" ;;
857
+ network) network_run static.rust-lang.org 180 "$@" ;;
858
+ *) fail 'Invalid Rust execution mode' ;;
859
+ esac
860
+ }
861
+ rust_ready() {
862
+ local directory
863
+ for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do
864
+ [[ -d "$directory" && ! -L "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || return 1
865
+ done
866
+ [[ -x "$CARGO_HOME/bin/rustup" ]] || return 1
867
+ [[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" == 'rustup @RUSTUP@ '* ]] || return 1
868
+ [[ "$(rust_run local "$CARGO_HOME/bin/rustc" +@RUST@ --version)" == 'rustc @RUST@ '* ]] || return 1
869
+ local host
870
+ host="$(rust_run local "$CARGO_HOME/bin/rustc" +@RUST@ -vV | awk '$1 == "host:" { print $2 }')" || return 1
871
+ @VERIFY_COMPONENTS@
872
+ @VERIFY_TARGETS@
873
+ return 0
874
+ }
875
+ verify_rust() { rust_ready || fail 'Pinned Rust, components or targets are unavailable; run install'; }
876
+ install_rust() (
877
+ if ! command -v cc >/dev/null || ! command -v ld >/dev/null || [[ ! -f /usr/include/stdlib.h ]]; then
878
+ apt_refresh_for build-essential
879
+ apt_install build-essential
880
+ fi
881
+ local artifact_url artifact_sha
882
+ # Subshell scope retains scratch for EXIT cleanup after a failed command.
883
+ scratch="$(mktemp -d)"
884
+ trap 'rm -rf -- "$scratch"' EXIT
885
+ for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do
886
+ [[ ! -L "$directory" ]] || fail 'Rust home must not be a symlink'
887
+ if [[ -e "$directory" ]]; then
888
+ [[ -d "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || fail 'Rust home ownership differs from the invoking user'
889
+ else
890
+ install -d -m 755 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$directory"
891
+ fi
892
+ done
893
+ if [[ ! -x "$CARGO_HOME/bin/rustup" ]] || [[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" != 'rustup @RUSTUP@ '* ]]; then
894
+ @RUSTUP_ARTIFACT@
895
+ fetch_verified "$artifact_url" "$artifact_sha" "$scratch/rustup-init"
896
+ chmod 755 "$scratch" "$scratch/rustup-init"
897
+ rust_run network "$scratch/rustup-init" -y --no-modify-path --default-toolchain none --profile minimal
898
+ fi
899
+ rust_run local "$CARGO_HOME/bin/rustup" set auto-self-update disable
900
+ if ! rust_ready; then
901
+ rust_run network "$CARGO_HOME/bin/rustup" toolchain install @RUST@ --profile minimal --no-self-update @RUST_ARGS@
902
+ fi
903
+ rust_run local "$CARGO_HOME/bin/rustup" default @RUST@
904
+ verify_rust
905
+ for tool in rustup cargo rustc rustdoc @RUST_PROXIES@; do
906
+ # Launchers persist the system homes in fresh shells without profile sourcing.
907
+ 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"
908
+ if ! cmp -s "$scratch/$tool" "/usr/local/bin/$tool"; then
909
+ install -m 755 "$scratch/$tool" "/usr/local/bin/$tool"
910
+ fi
911
+ done
912
+ )
913
+ `.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 "));
914
+ };
915
+ const packageToolInstall = ({ tool, release }) => {
916
+ const name = tool.replaceAll("-", "_");
917
+ return {
918
+ name,
919
+ text: `verify_${name}() {
920
+ [[ -x ${quote(release.binary)} && "$(${quote(release.binary)} --version 2>/dev/null)" == ${quote(release.identity)}* ]] || return 1
921
+ [[ "$(dpkg-query -W -f='\${Version}' ${quote(release.package)} 2>/dev/null)" == ${quote(`${release.version}-`)}* ]]
922
+ }
923
+ install_${name}() {
924
+ if ! verify_${name}; then
925
+ apt_network update
926
+ apt_install ${quote(release.package)}
927
+ verify_${name} || fail ${quote(`Installed ${tool} version differs from its pin`)}
928
+ fi
929
+ }
930
+ `
931
+ };
932
+ };
933
+ const artifactToolInstall = ({ tool, release }) => {
934
+ const directory = `/opt/stll-cloud/tools/${tool}/${release.version}`;
935
+ const command = tool === "wasm-bindgen-cli" ? "wasm-bindgen" : tool;
936
+ const marker = `${tool === "cargo-nextest" ? "cargo-nextest" : command} ${release.version}`;
937
+ return {
938
+ name: tool.replaceAll("-", "_"),
939
+ text: `verify_${tool.replaceAll("-", "_")}() {
940
+ ${Object.keys(release.amd64.binaries).map((binary) => `[[ -x ${quote(`${directory}/${binary}`)} ]] || return 1`).join("\n ")}
941
+ [[ "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(marker)} || "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(`${marker} `)}* ]]
942
+ }
943
+ install_${tool.replaceAll("-", "_")}() (
944
+ local artifact_url artifact_sha
945
+ # Subshell scope retains scratch for EXIT cleanup after a failed command.
946
+ if ! verify_${tool.replaceAll("-", "_")}; then
947
+ scratch="$(mktemp -d)"
948
+ trap 'rm -rf -- "$scratch"' EXIT
949
+ ${artifactSelection(release)}
950
+ fetch_verified "$artifact_url" "$artifact_sha" "$scratch/archive.tar.gz"
951
+ tar -xzf "$scratch/archive.tar.gz" -C "$scratch"
952
+ install -d -m 755 ${quote(directory)}
953
+ case "$ARCH" in
954
+ ${["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")}
955
+ esac
956
+ verify_${tool.replaceAll("-", "_")} || fail ${quote(`Installed ${tool} version differs from its pin`)}
957
+ fi
958
+ ${Object.keys(release.amd64.binaries).map((binary) => ` ln -sfnT -- ${quote(`${directory}/${binary}`)} ${quote(`/usr/local/bin/${binary}`)}`).join("\n")}
959
+ )
960
+ `
961
+ };
962
+ };
963
+ const renderCloudToolInstall = ({ rust, tools }) => {
964
+ const installs = tools.map((entry) => {
965
+ switch (entry.type) {
966
+ case "package": return packageToolInstall(entry);
967
+ case "artifact": return artifactToolInstall(entry);
968
+ default: throw new Error(`Unsupported cloud tool: ${String(entry)}`);
969
+ }
970
+ });
971
+ return `${helpers}${rust === void 0 ? "" : rustInstall(rust)}\n${installs.map(({ text }) => text).join("\n")}
972
+ install_tools() {
973
+ ${rust === void 0 ? ":" : "install_rust"}
974
+ ${installs.map(({ name }) => `install_${name}`).join("\n ")}
975
+ }
976
+ verify_tools() {
977
+ ${rust === void 0 ? ":" : "verify_rust"}
978
+ ${installs.map(({ name }) => `verify_${name} || fail 'Pinned ${name} is unavailable; run install'`).join("\n ")}
979
+ }
980
+ `;
981
+ };
982
+ //#endregion
440
983
  //#region src/toolchain-node.ts
441
984
  const numericComponent = "(?:0|[1-9]\\d*)";
442
985
  const policyPattern = new RegExp(`^${numericComponent}\\.x$`);
@@ -474,6 +1017,14 @@ usage() { echo 'Usage: cloud-setup.sh install|start' >&2; exit 2; }
474
1017
  case "$1" in install|start) MODE="$1" ;; *) usage ;; esac
475
1018
  fail() { echo "cloud-setup: $*" >&2; exit 1; }
476
1019
  trap 'echo "cloud-setup: command failed at line $LINENO" >&2' ERR
1020
+ # Each phase logs its start and duration so a stalled phase is visible in the task log.
1021
+ step() {
1022
+ local name="$1" started="$SECONDS"
1023
+ shift
1024
+ echo "cloud-setup: $name started" >&2
1025
+ "$@"
1026
+ echo "cloud-setup: $name done in $((SECONDS - started))s" >&2
1027
+ }
477
1028
  [[ -f /etc/os-release ]] || fail 'Ubuntu 24.04 is required'
478
1029
  . /etc/os-release
479
1030
  [[ "$ID" == ubuntu && "$VERSION_ID" == 24.04 && "$VERSION_CODENAME" == noble ]] || fail 'Only Ubuntu 24.04 (noble) is supported'
@@ -509,22 +1060,24 @@ case "$(uname -m)" in
509
1060
  *) fail 'Only amd64 and arm64 are supported' ;;
510
1061
  esac
511
1062
 
1063
+ @NETWORK@
1064
+
512
1065
  runtime_ready() {
513
1066
  [[ -x "$NODE_DIR/bin/node" && "$($NODE_DIR/bin/node --version)" == "v$NODE_VERSION" ]] || fail 'Pinned Node is unavailable; run install'
514
1067
  [[ -x "$BUN_DIR/bun" && "$($BUN_DIR/bun --version)" == "$BUN_VERSION" ]] || fail 'Pinned Bun is unavailable; run install'
515
1068
  }
516
1069
  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
1070
+ 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
1071
+ apt_refresh_for ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
1072
+ DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
520
1073
  fi
521
1074
  local scratch node_archive bun_archive bun_folder
522
1075
  scratch="$(mktemp -d)"
523
1076
  trap 'rm -rf -- "$scratch"' RETURN
524
1077
  if [[ ! -x "$NODE_DIR/bin/node" ]] || [[ "$($NODE_DIR/bin/node --version)" != "v$NODE_VERSION" ]]; then
525
1078
  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"
1079
+ download_file "https://nodejs.org/dist/v$NODE_VERSION/$node_archive" "$scratch/$node_archive" 120
1080
+ download_file "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt" "$scratch/node-sums" 45
528
1081
  (cd "$scratch"; awk -v name="$node_archive" '$2 == name { print; count++ } END { if (count != 1) exit 1 }' node-sums | sha256sum --strict -c -)
529
1082
  install -d -m 755 "$NODE_DIR"
530
1083
  tar -xJf "$scratch/$node_archive" --strip-components=1 -C "$NODE_DIR"
@@ -532,8 +1085,8 @@ install_runtimes() {
532
1085
  if [[ ! -x "$BUN_DIR/bun" ]] || [[ "$($BUN_DIR/bun --version)" != "$BUN_VERSION" ]]; then
533
1086
  bun_folder="bun-linux-$BUN_ARCH"
534
1087
  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"
1088
+ download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/$bun_archive" "$scratch/$bun_archive" 120
1089
+ download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/SHASUMS256.txt" "$scratch/bun-sums" 45
537
1090
  (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
1091
  unzip -q "$scratch/$bun_archive" -d "$scratch/bun"
539
1092
  install -d -m 755 "$BUN_DIR"
@@ -549,7 +1102,7 @@ install_runtimes() {
549
1102
  }
550
1103
 
551
1104
  dependency_install() {
552
- local directory
1105
+ local directory download_hosts
553
1106
  for directory in /var/cache/stll-cloud /var/cache/stll-cloud/bun; do
554
1107
  [[ ! -L "$directory" ]] || fail 'Dependency cache parent must not be a symlink'
555
1108
  if [[ -e "$directory" ]]; then
@@ -562,24 +1115,52 @@ dependency_install() {
562
1115
  [[ -d "$BUN_CACHE" && "$(stat -c '%u' "$BUN_CACHE")" == "$OUTPUT_UID" ]] || fail 'Dependency cache ownership differs from the invoking user'
563
1116
  fi
564
1117
  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
1118
+ # Report hostnames only: lockfile/config URLs may contain credentials.
1119
+ # These are known install hosts; lifecycle scripts can contact other hosts.
1120
+ download_hosts="$(HOME="$OUTPUT_HOME" "$NODE_DIR/bin/node" -e '
1121
+ const fs = require("node:fs");
1122
+ const path = require("node:path");
1123
+ const hosts = new Set(["registry.npmjs.org"]);
1124
+ const inputs = [process.env.BUN_CONFIG_REGISTRY ?? "", process.env.NPM_CONFIG_REGISTRY ?? "", process.env.npm_config_registry ?? ""];
1125
+ 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")]) {
1126
+ if (fs.existsSync(file)) inputs.push(fs.readFileSync(file, "utf8"));
1127
+ }
1128
+ for (const input of inputs) {
1129
+ for (const match of input.matchAll(/(?:git\+)?https?:\/\/[^\s"\x27<>\\]+/g)) {
1130
+ try { hosts.add(new URL(match[0].replace(/^git\+/, "")).hostname); } catch {}
1131
+ }
1132
+ if (/github:/.test(input)) { hosts.add("github.com"); hosts.add("codeload.github.com"); }
1133
+ }
1134
+ process.stdout.write([...hosts].sort().join(","));
1135
+ ')"
1136
+ # Bun 1.4.3 socket inactivity timeout, in seconds; the outer bound includes scripts.
1137
+ network_run "$download_hosts" 240 runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" BUN_INSTALL_CACHE_DIR="$BUN_CACHE" PATH="$PATH" BUN_CONFIG_HTTP_IDLE_TIMEOUT=20 "$BUN_DIR/bun" @INSTALL_ARGS@
566
1138
  }
567
1139
 
568
1140
  @SERVICE_INSTALL@
569
1141
  @SERVICE_START@
570
1142
 
1143
+ @TOOL_INSTALL@
1144
+
1145
+ @INPUT_VALIDATION@
1146
+
571
1147
  @ENV_FUNCTIONS@
572
1148
 
573
1149
  if [[ "$MODE" == install ]]; then
574
- install_runtimes
575
- install_services
576
- dependency_install
1150
+ step runtimes install_runtimes
1151
+ step services install_services
1152
+ step tools install_tools
1153
+ step shell install_shell_environment
1154
+ step dependencies dependency_install
577
1155
  exit 0
578
1156
  fi
579
- runtime_ready
1157
+ step runtime-check runtime_ready
1158
+ step tool-check verify_tools
1159
+ step shell install_shell_environment
1160
+ step shell-check verify_shell_environment
580
1161
  @ENV_VALIDATE@
581
- prepare_state
582
- start_services
1162
+ step state prepare_state
1163
+ step service-start start_services
583
1164
  @ENV_WRITE@
584
1165
  `;
585
1166
  const environmentFunctions = String.raw`
@@ -611,13 +1192,36 @@ write_environment() {
611
1192
  }
612
1193
  `;
613
1194
  const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
614
- const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
1195
+ const generateCloudSetup = ({ policy, cloud, nodeVersion, files = {}, rustExceptions = [] }) => {
615
1196
  const selected = parseCloudSetup(cloud);
616
1197
  if (selected === void 0) throw new Error("cloud declaration is required");
617
1198
  const exactNode = nodeVersion.trim();
618
1199
  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");
1200
+ const toolchains = resolveCloudToolchains({
1201
+ cloud: selected,
1202
+ files,
1203
+ rust: policy.rust,
1204
+ rustExceptions
1205
+ });
619
1206
  const services = cloudServices.filter((service) => selected.services.includes(service));
620
1207
  const replacements = /* @__PURE__ */ new Map([
1208
+ ["@INSTALL_ARGS@", selected.install.replace(/^bun /, "")],
1209
+ ["@NETWORK@", renderCloudNetwork()],
1210
+ ["@TOOL_INSTALL@", renderCloudToolInstall(toolchains) + renderCloudShellInstall({
1211
+ nodeVersion: exactNode,
1212
+ bunVersion: policy.bun,
1213
+ rust: toolchains.rust
1214
+ })],
1215
+ ["@INPUT_VALIDATION@", [
1216
+ "rust-toolchain.toml",
1217
+ "rust-toolchain",
1218
+ ...selected.tools?.["wasm-bindgen-cli"] === void 0 ? [] : ["Cargo.lock"]
1219
+ ].flatMap((file) => {
1220
+ const content = files[file];
1221
+ if (content === void 0) return [`[[ ! -e ${shellQuote(file)} && ! -L ${shellQuote(file)} ]] || fail ${shellQuote(`${file} appeared; regenerate cloud-setup.sh`)}`];
1222
+ const hash = (0, node_crypto.createHash)("sha256").update(content).digest("hex");
1223
+ return [`[[ -f ${shellQuote(file)} && ! -L ${shellQuote(file)} ]] && [[ "$(sha256sum -- ${shellQuote(file)} | cut -d ' ' -f1)" == ${shellQuote(hash)} ]] || fail ${shellQuote(`${file} changed; regenerate cloud-setup.sh`)}`];
1224
+ }).join("\n")],
621
1225
  ["@BUN@", policy.bun],
622
1226
  ["@NODE@", exactNode],
623
1227
  ["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
@@ -643,6 +1247,129 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
643
1247
  });
644
1248
  };
645
1249
  //#endregion
1250
+ //#region src/cloud-setup-claude.ts
1251
+ const cloudClaudeSettingsPath = ".claude/settings.json";
1252
+ const sessionStartHook = (script) => ({
1253
+ matcher: "",
1254
+ hooks: [{
1255
+ type: "command",
1256
+ command: `if [ "\${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
1257
+ ${script} start || exit "$?"
1258
+ if [ -n "\${CLAUDE_ENV_FILE:-}" ]; then
1259
+ if [ ! -f ${cloudShellProfilePath} ]; then
1260
+ printf '%s\\n' 'cloud-setup: cloud shell profile is unavailable; run install' >&2
1261
+ exit 1
1262
+ fi
1263
+ printf '\\n' >> "$CLAUDE_ENV_FILE" || exit "$?"
1264
+ cat ${cloudShellProfilePath} >> "$CLAUDE_ENV_FILE"
1265
+ fi
1266
+ fi`
1267
+ }]
1268
+ });
1269
+ const cloudSessionStartHook = sessionStartHook(`bash "$CLAUDE_PROJECT_DIR/${cloudSetupPath}"`);
1270
+ const previousCloudSessionStartHooks = [sessionStartHook(`bash ${cloudSetupPath}`)];
1271
+ const cloudAttribution = {
1272
+ commit: "",
1273
+ pr: ""
1274
+ };
1275
+ const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1276
+ const array = (value) => Array.isArray(value);
1277
+ const parseSettings = (text) => {
1278
+ try {
1279
+ return JSON.parse(text, (_key, value) => {
1280
+ 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");
1281
+ return value;
1282
+ });
1283
+ } catch (error) {
1284
+ if (error instanceof SyntaxError) throw new Error("settings must contain valid JSON");
1285
+ throw error;
1286
+ }
1287
+ };
1288
+ const readSettings = (text) => {
1289
+ const settings = text === void 0 ? {} : parseSettings(text);
1290
+ if (text !== void 0 && (0, yaml.parseDocument)(text, { uniqueKeys: true }).errors.length > 0) throw new Error("settings contains duplicate or ambiguous JSON keys");
1291
+ if (!record$1(settings)) throw new Error("settings must be a JSON object");
1292
+ if (settings["disableAllHooks"] === true) throw new Error("disableAllHooks prevents SessionStart from running");
1293
+ const hooks = settings["hooks"] === void 0 ? {} : settings["hooks"];
1294
+ if (!record$1(hooks)) throw new Error("hooks must be a JSON object");
1295
+ const sessionStart = hooks["SessionStart"] === void 0 ? [] : hooks["SessionStart"];
1296
+ if (!array(sessionStart) || sessionStart.some((entry) => !record$1(entry) || !array(entry["hooks"]))) throw new Error("hooks.SessionStart must be an array of hook groups");
1297
+ return {
1298
+ settings,
1299
+ hooks,
1300
+ sessionStart
1301
+ };
1302
+ };
1303
+ 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")));
1304
+ 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);
1305
+ const cloudAttributionFix = `preserve unrelated settings in ${cloudClaudeSettingsPath} and set its top-level attribution to exactly:\n` + JSON.stringify({ attribution: cloudAttribution }, null, 2);
1306
+ const sessionStartState = (sessionStart) => {
1307
+ const groups = cloudHookGroups(sessionStart);
1308
+ const group = groups.at(0);
1309
+ if (group === void 0) return { type: "missing" };
1310
+ if (groups.length === 1 && (0, node_util.isDeepStrictEqual)(group, cloudSessionStartHook)) return { type: "current" };
1311
+ if (groups.length === 1 && previousCloudSessionStartHooks.some((hook) => (0, node_util.isDeepStrictEqual)(group, hook))) return {
1312
+ type: "previous",
1313
+ group
1314
+ };
1315
+ return { type: "conflict" };
1316
+ };
1317
+ const attributionState = (settings) => {
1318
+ const attribution = settings["attribution"];
1319
+ if (attribution === void 0) return "missing";
1320
+ return (0, node_util.isDeepStrictEqual)(attribution, cloudAttribution) ? "current" : "conflict";
1321
+ };
1322
+ const mergeError = (reason, fix) => /* @__PURE__ */ new Error(`cannot safely merge ${cloudClaudeSettingsPath}: ${reason}; ${fix}`);
1323
+ const readMergeableSettings = (text) => {
1324
+ try {
1325
+ return readSettings(text);
1326
+ } catch (error) {
1327
+ throw mergeError(error instanceof Error ? error.message : String(error), cloudSessionStartFix);
1328
+ }
1329
+ };
1330
+ /** Add the cloud hook and attribution; never overwrite conflicting values. */
1331
+ const mergeCloudClaudeSettings = (text) => {
1332
+ const { settings, hooks, sessionStart } = readMergeableSettings(text);
1333
+ const hook = sessionStartState(sessionStart);
1334
+ if (hook.type === "conflict") throw mergeError("existing cloud setup SessionStart hook differs", cloudSessionStartFix);
1335
+ const attribution = attributionState(settings);
1336
+ if (attribution === "conflict") throw mergeError("existing attribution differs", cloudAttributionFix);
1337
+ if (hook.type === "current" && attribution === "current" && text !== void 0) return text;
1338
+ const mergedSessionStart = (() => {
1339
+ switch (hook.type) {
1340
+ case "current": return sessionStart;
1341
+ case "missing": return [...sessionStart, cloudSessionStartHook];
1342
+ case "previous": return sessionStart.map((entry) => entry === hook.group ? cloudSessionStartHook : entry);
1343
+ default: throw new Error(`Unsupported hook state: ${String(hook)}`);
1344
+ }
1345
+ })();
1346
+ return `${JSON.stringify({
1347
+ ...settings,
1348
+ attribution: cloudAttribution,
1349
+ hooks: {
1350
+ ...hooks,
1351
+ SessionStart: mergedSessionStart
1352
+ }
1353
+ }, null, 2)}\n`;
1354
+ };
1355
+ /** Settings formatting and unrelated hooks do not affect the generated contract. */
1356
+ const hasCloudSessionStartHook = (text) => {
1357
+ if (text === void 0) return false;
1358
+ try {
1359
+ return sessionStartState(readSettings(text).sessionStart).type === "current";
1360
+ } catch {
1361
+ return false;
1362
+ }
1363
+ };
1364
+ const hasCloudAttribution = (text) => {
1365
+ if (text === void 0) return false;
1366
+ try {
1367
+ return attributionState(readSettings(text).settings) === "current";
1368
+ } catch {
1369
+ return false;
1370
+ }
1371
+ };
1372
+ //#endregion
646
1373
  //#region src/toolchain-workspaces.ts
647
1374
  /** Match manifest directories with the same positive and negative workspace patterns. */
648
1375
  const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
@@ -809,13 +1536,31 @@ const isDockerDefinitionPath = (file) => {
809
1536
  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
1537
  /** Lockfiles establish ecosystem presence without reading their dependency graphs. */
811
1538
  const toolchainInputKind = (file, text = "") => {
812
- if (file === ".agents/cloud-setup.sh") return "config";
1539
+ if (file === ".agents/cloud-setup.sh" || file === ".claude/settings.json" || file === "Cargo.lock") return "config";
813
1540
  if (githubAutomationFileKind(file) !== void 0) return "config";
814
1541
  if (excludedInputPath(file)) return void 0;
815
1542
  if (pythonDependencyManifestKind(file) === "pipfile-lock" || javascriptDependencyLockfiles.some((name) => file.split("/").at(-1) === name) || /(?:^|\/)uv\.lock$/.test(file)) return "presence";
816
1543
  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
1544
  };
818
1545
  //#endregion
1546
+ Object.defineProperty(exports, "cloudAttributionFix", {
1547
+ enumerable: true,
1548
+ get: function() {
1549
+ return cloudAttributionFix;
1550
+ }
1551
+ });
1552
+ Object.defineProperty(exports, "cloudClaudeSettingsPath", {
1553
+ enumerable: true,
1554
+ get: function() {
1555
+ return cloudClaudeSettingsPath;
1556
+ }
1557
+ });
1558
+ Object.defineProperty(exports, "cloudSessionStartFix", {
1559
+ enumerable: true,
1560
+ get: function() {
1561
+ return cloudSessionStartFix;
1562
+ }
1563
+ });
819
1564
  Object.defineProperty(exports, "cloudSetupPath", {
820
1565
  enumerable: true,
821
1566
  get: function() {
@@ -840,6 +1585,18 @@ Object.defineProperty(exports, "githubAutomationFileKind", {
840
1585
  return githubAutomationFileKind;
841
1586
  }
842
1587
  });
1588
+ Object.defineProperty(exports, "hasCloudAttribution", {
1589
+ enumerable: true,
1590
+ get: function() {
1591
+ return hasCloudAttribution;
1592
+ }
1593
+ });
1594
+ Object.defineProperty(exports, "hasCloudSessionStartHook", {
1595
+ enumerable: true,
1596
+ get: function() {
1597
+ return hasCloudSessionStartHook;
1598
+ }
1599
+ });
843
1600
  Object.defineProperty(exports, "isComposeDefinitionPath", {
844
1601
  enumerable: true,
845
1602
  get: function() {
@@ -888,6 +1645,12 @@ Object.defineProperty(exports, "javascriptDependencyLockfiles", {
888
1645
  return javascriptDependencyLockfiles;
889
1646
  }
890
1647
  });
1648
+ Object.defineProperty(exports, "mergeCloudClaudeSettings", {
1649
+ enumerable: true,
1650
+ get: function() {
1651
+ return mergeCloudClaudeSettings;
1652
+ }
1653
+ });
891
1654
  Object.defineProperty(exports, "nodePolicyValid", {
892
1655
  enumerable: true,
893
1656
  get: function() {