@stll/oxlint-config 1.1.0 → 1.2.1

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.
@@ -174,7 +174,7 @@ const postgresInstall = String.raw`
174
174
  rm -- "$key"
175
175
  printf '%s\n' '@PGDG_SOURCE@' > /etc/apt/sources.list.d/stll-pgdg.list
176
176
  fi
177
- apt_network update
177
+ apt_refresh_for postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
178
178
  apt_install postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
179
179
  fi
180
180
  postgres_ready || fail 'PostgreSQL toolchain is incomplete or differs from the policy major'
@@ -197,7 +197,7 @@ const valkeyInstall = String.raw`
197
197
  [[ "$(dpkg-deb -f "$scratch/$package.deb" Version)" == '@VALKEY@-1.noble' ]] || fail 'Valkey artifact version mismatch'
198
198
  [[ "$(dpkg-deb -f "$scratch/$package.deb" Architecture)" == "$ARCH" ]] || fail 'Valkey artifact architecture mismatch'
199
199
  done
200
- apt_network update
200
+ apt_refresh_for "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
201
201
  apt_install "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
202
202
  rm -rf -- "$scratch"
203
203
  fi
@@ -298,6 +298,15 @@ apt_network() {
298
298
  [[ -n "$hosts" ]] || hosts='configured APT hosts'
299
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
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
+ }
301
310
  `;
302
311
  //#endregion
303
312
  //#region src/cloud-setup-artifacts.ts
@@ -417,12 +426,29 @@ const getCloudSetupArtifact = (tool, version) => {
417
426
  if (artifact === void 0) throw new Error(`Unsupported ${tool} version ${version}; supported versions: ${releases.map((release) => release.version).join(", ")}`);
418
427
  return artifact;
419
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
+ };
420
442
  //#endregion
421
443
  //#region src/cloud-setup-schema.ts
422
444
  const cloudServices = ["postgres", "valkey"];
423
- const cloudExtraTools = Object.keys(cloudSetupArtifacts).filter((tool) => tool !== "rustup-init" && Object.hasOwn(cloudSetupArtifacts, tool));
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);
424
449
  const object = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
425
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);
426
452
  const cloudRustComponentBinaries = {
427
453
  clippy: ["cargo-clippy", "clippy-driver"],
428
454
  rustfmt: ["rustfmt", "cargo-fmt"],
@@ -466,12 +492,14 @@ const parseTools = (input) => {
466
492
  if (Object.hasOwn(input, tool)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
467
493
  continue;
468
494
  }
469
- if (!exactPin(pin)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
495
+ if (!exactToolPin(tool, pin)) throw new Error(`cloud.tools.${tool} must be an exact stable release`);
470
496
  result[tool] = pin;
471
497
  }
472
498
  return result;
473
499
  };
474
500
  const cloudInstallCommand = "bun install --frozen-lockfile";
501
+ const cloudInstallCommands = [cloudInstallCommand, `${cloudInstallCommand} --ignore-scripts`];
502
+ const cloudInstall = (value) => cloudInstallCommands.some((command) => value === command);
475
503
  const cloudService = (value) => cloudServices.some((service) => value === service);
476
504
  const parseEnvFile = (input) => {
477
505
  if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
@@ -492,7 +520,8 @@ const parseCloudSetup = (input) => {
492
520
  };
493
521
  const selected = "services" in input ? input.services : [];
494
522
  if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
495
- 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;
496
525
  const seen = /* @__PURE__ */ new Set();
497
526
  const services = Array.from(selected, (service) => {
498
527
  if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
@@ -503,7 +532,7 @@ const parseCloudSetup = (input) => {
503
532
  if (services.length === 0 && "envFile" in input) throw new Error("cloud.envFile is not allowed without services");
504
533
  return {
505
534
  services,
506
- install: cloudInstallCommand,
535
+ install,
507
536
  ...extensions,
508
537
  ...services.length === 0 ? {} : { envFile: parseEnvFile(input) }
509
538
  };
@@ -765,9 +794,12 @@ const resolveCloudToolchains = ({ cloud, files, rust, rustExceptions }) => {
765
794
  }
766
795
  const components = [.../* @__PURE__ */ new Set([...parseCloudRustNames(declared?.["components"], "components"), ...cloud.rust?.components ?? []])].sort();
767
796
  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 [];
797
+ const resolveTool = (tool, version) => {
798
+ if (isCloudPackageTool(tool)) return {
799
+ type: "package",
800
+ tool,
801
+ release: getCloudSetupPackage(tool, version)
802
+ };
771
803
  if (tool === "cargo-nextest" && !rustEnabled) throw new Error("cloud.tools.cargo-nextest requires a Rust toolchain");
772
804
  if (tool === "wasm-bindgen-cli") {
773
805
  const lock = files["Cargo.lock"];
@@ -777,10 +809,15 @@ const resolveCloudToolchains = ({ cloud, files, rust, rustExceptions }) => {
777
809
  const versions = new Set(packages.flatMap((entry) => record$2(entry) && entry["name"] === "wasm-bindgen" ? [entry["version"]] : []));
778
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"})`);
779
811
  }
780
- return [{
812
+ return {
813
+ type: "artifact",
781
814
  tool,
782
815
  release: getCloudSetupArtifact(tool, version)
783
- }];
816
+ };
817
+ };
818
+ const tools = cloudExtraTools.flatMap((tool) => {
819
+ const version = cloud.tools?.[tool];
820
+ return version === void 0 ? [] : [resolveTool(tool, version)];
784
821
  });
785
822
  return {
786
823
  rust: rustEnabled && typeof channel === "string" ? {
@@ -838,7 +875,7 @@ rust_ready() {
838
875
  verify_rust() { rust_ready || fail 'Pinned Rust, components or targets are unavailable; run install'; }
839
876
  install_rust() (
840
877
  if ! command -v cc >/dev/null || ! command -v ld >/dev/null || [[ ! -f /usr/include/stdlib.h ]]; then
841
- apt_network update
878
+ apt_refresh_for build-essential
842
879
  apt_install build-essential
843
880
  fi
844
881
  local artifact_url artifact_sha
@@ -875,14 +912,31 @@ install_rust() (
875
912
  )
876
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 "));
877
914
  };
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("-", "_")}() {
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("-", "_")}() {
886
940
  ${Object.keys(release.amd64.binaries).map((binary) => `[[ -x ${quote(`${directory}/${binary}`)} ]] || return 1`).join("\n ")}
887
941
  [[ "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(marker)} || "$( ${quote(`${directory}/${command}`)} --version)" == ${quote(`${marker} `)}* ]]
888
942
  }
@@ -904,7 +958,15 @@ ${["amd64", "arm64"].map((arch) => ` ${arch})\n${Object.entries(release[arc
904
958
  ${Object.keys(release.amd64.binaries).map((binary) => ` ln -sfnT -- ${quote(`${directory}/${binary}`)} ${quote(`/usr/local/bin/${binary}`)}`).join("\n")}
905
959
  )
906
960
  `
907
- };
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
+ }
908
970
  });
909
971
  return `${helpers}${rust === void 0 ? "" : rustInstall(rust)}\n${installs.map(({ text }) => text).join("\n")}
910
972
  install_tools() {
@@ -955,6 +1017,14 @@ usage() { echo 'Usage: cloud-setup.sh install|start' >&2; exit 2; }
955
1017
  case "$1" in install|start) MODE="$1" ;; *) usage ;; esac
956
1018
  fail() { echo "cloud-setup: $*" >&2; exit 1; }
957
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
+ }
958
1028
  [[ -f /etc/os-release ]] || fail 'Ubuntu 24.04 is required'
959
1029
  . /etc/os-release
960
1030
  [[ "$ID" == ubuntu && "$VERSION_ID" == 24.04 && "$VERSION_CODENAME" == noble ]] || fail 'Only Ubuntu 24.04 (noble) is supported'
@@ -998,7 +1068,7 @@ runtime_ready() {
998
1068
  }
999
1069
  install_runtimes() {
1000
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
1001
- apt_network update
1071
+ apt_refresh_for ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
1002
1072
  DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1
1003
1073
  fi
1004
1074
  local scratch node_archive bun_archive bun_folder
@@ -1064,7 +1134,7 @@ for (const input of inputs) {
1064
1134
  process.stdout.write([...hosts].sort().join(","));
1065
1135
  ')"
1066
1136
  # 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
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@
1068
1138
  }
1069
1139
 
1070
1140
  @SERVICE_INSTALL@
@@ -1077,20 +1147,20 @@ process.stdout.write([...hosts].sort().join(","));
1077
1147
  @ENV_FUNCTIONS@
1078
1148
 
1079
1149
  if [[ "$MODE" == install ]]; then
1080
- install_runtimes
1081
- install_services
1082
- install_tools
1083
- install_shell_environment
1084
- 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
1085
1155
  exit 0
1086
1156
  fi
1087
- runtime_ready
1088
- verify_tools
1089
- install_shell_environment
1090
- verify_shell_environment
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
1091
1161
  @ENV_VALIDATE@
1092
- prepare_state
1093
- start_services
1162
+ step state prepare_state
1163
+ step service-start start_services
1094
1164
  @ENV_WRITE@
1095
1165
  `;
1096
1166
  const environmentFunctions = String.raw`
@@ -1135,6 +1205,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion, files = {}, rustExcept
1135
1205
  });
1136
1206
  const services = cloudServices.filter((service) => selected.services.includes(service));
1137
1207
  const replacements = /* @__PURE__ */ new Map([
1208
+ ["@INSTALL_ARGS@", selected.install.replace(/^bun /, "")],
1138
1209
  ["@NETWORK@", renderCloudNetwork()],
1139
1210
  ["@TOOL_INSTALL@", renderCloudToolInstall(toolchains) + renderCloudShellInstall({
1140
1211
  nodeVersion: exactNode,
@@ -1178,12 +1249,12 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion, files = {}, rustExcept
1178
1249
  //#endregion
1179
1250
  //#region src/cloud-setup-claude.ts
1180
1251
  const cloudClaudeSettingsPath = ".claude/settings.json";
1181
- const cloudSessionStartHook = {
1252
+ const sessionStartHook = (script) => ({
1182
1253
  matcher: "",
1183
1254
  hooks: [{
1184
1255
  type: "command",
1185
1256
  command: `if [ "\${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
1186
- bash ${cloudSetupPath} start || exit "$?"
1257
+ ${script} start || exit "$?"
1187
1258
  if [ -n "\${CLAUDE_ENV_FILE:-}" ]; then
1188
1259
  if [ ! -f ${cloudShellProfilePath} ]; then
1189
1260
  printf '%s\\n' 'cloud-setup: cloud shell profile is unavailable; run install' >&2
@@ -1194,6 +1265,12 @@ const cloudSessionStartHook = {
1194
1265
  fi
1195
1266
  fi`
1196
1267
  }]
1268
+ });
1269
+ const cloudSessionStartHook = sessionStartHook(`bash "$CLAUDE_PROJECT_DIR/${cloudSetupPath}"`);
1270
+ const previousCloudSessionStartHooks = [sessionStartHook(`bash ${cloudSetupPath}`)];
1271
+ const cloudAttribution = {
1272
+ commit: "",
1273
+ pr: ""
1197
1274
  };
1198
1275
  const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1199
1276
  const array = (value) => Array.isArray(value);
@@ -1225,33 +1302,69 @@ const readSettings = (text) => {
1225
1302
  };
1226
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")));
1227
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);
1228
- /** Append only when the existing settings have no conflicting cloud startup. */
1229
- const mergeCloudSessionStartHook = (text) => {
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) => {
1230
1324
  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`;
1325
+ return readSettings(text);
1244
1326
  } catch (error) {
1245
- throw new Error(`cannot safely merge ${cloudClaudeSettingsPath}: ${error instanceof Error ? error.message : String(error)}; ${cloudSessionStartFix}`);
1327
+ throw mergeError(error instanceof Error ? error.message : String(error), cloudSessionStartFix);
1246
1328
  }
1247
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
+ };
1248
1355
  /** Settings formatting and unrelated hooks do not affect the generated contract. */
1249
1356
  const hasCloudSessionStartHook = (text) => {
1250
1357
  if (text === void 0) return false;
1251
1358
  try {
1252
- const { sessionStart } = readSettings(text);
1253
- const cloudGroups = cloudHookGroups(sessionStart);
1254
- return cloudGroups.length === 1 && (0, node_util.isDeepStrictEqual)(cloudGroups.at(0), cloudSessionStartHook);
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";
1255
1368
  } catch {
1256
1369
  return false;
1257
1370
  }
@@ -1430,6 +1543,12 @@ const toolchainInputKind = (file, text = "") => {
1430
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";
1431
1544
  };
1432
1545
  //#endregion
1546
+ Object.defineProperty(exports, "cloudAttributionFix", {
1547
+ enumerable: true,
1548
+ get: function() {
1549
+ return cloudAttributionFix;
1550
+ }
1551
+ });
1433
1552
  Object.defineProperty(exports, "cloudClaudeSettingsPath", {
1434
1553
  enumerable: true,
1435
1554
  get: function() {
@@ -1466,6 +1585,12 @@ Object.defineProperty(exports, "githubAutomationFileKind", {
1466
1585
  return githubAutomationFileKind;
1467
1586
  }
1468
1587
  });
1588
+ Object.defineProperty(exports, "hasCloudAttribution", {
1589
+ enumerable: true,
1590
+ get: function() {
1591
+ return hasCloudAttribution;
1592
+ }
1593
+ });
1469
1594
  Object.defineProperty(exports, "hasCloudSessionStartHook", {
1470
1595
  enumerable: true,
1471
1596
  get: function() {
@@ -1520,10 +1645,10 @@ Object.defineProperty(exports, "javascriptDependencyLockfiles", {
1520
1645
  return javascriptDependencyLockfiles;
1521
1646
  }
1522
1647
  });
1523
- Object.defineProperty(exports, "mergeCloudSessionStartHook", {
1648
+ Object.defineProperty(exports, "mergeCloudClaudeSettings", {
1524
1649
  enumerable: true,
1525
1650
  get: function() {
1526
- return mergeCloudSessionStartHook;
1651
+ return mergeCloudClaudeSettings;
1527
1652
  }
1528
1653
  });
1529
1654
  Object.defineProperty(exports, "nodePolicyValid", {