ignitify 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ignitify.cjs CHANGED
@@ -942,10 +942,25 @@ function bareValueFlag(args, valueFlags2) {
942
942
  function needsValueMessage(flag) {
943
943
  return `--${flag} needs a value`;
944
944
  }
945
+ function strOpt(args, key) {
946
+ let bare = bareValueFlag(args, [key]);
947
+ if (bare !== void 0) throw new Error(needsValueMessage(bare));
948
+ let v = args.options[key];
949
+ return typeof v == "string" ? v : void 0;
950
+ }
951
+ function flagParams(args, map2) {
952
+ return Object.fromEntries(Object.entries(map2).flatMap(([flag, param]) => {
953
+ let v = strOpt(args, flag);
954
+ return v === void 0 ? [] : [[param, v]];
955
+ }));
956
+ }
945
957
  var init_args = __esm({ "../../packages/http-client/args.ts"() {
946
958
  "use strict";
947
959
  init_define_IGNITIFY_CLIENT_FILES();
948
960
  } });
961
+ function boolFlagSpec(flags) {
962
+ return { boolFlags: new Set(Object.keys(flags).filter((flag) => flags[flag] === "bool")) };
963
+ }
949
964
  function unknownFlagText(flag, command) {
950
965
  return `unknown flag ${flag} for ${command}`;
951
966
  }
@@ -978,13 +993,49 @@ function shapeRefusal(command, args, flags, maxPositionals) {
978
993
  function isHelpRequest(args) {
979
994
  return HELP_TOKENS.includes(args.command) || args.options.help === true;
980
995
  }
981
- var didYouMean, itsFlags, flagTakesNoValue, tooManyArgs, HELP_TOKENS, init_invocation = __esm({ "../../packages/http-client/invocation.ts"() {
996
+ var looksShort, didYouMean, itsFlags, flagTakesNoValue, tooManyArgs, HELP_TOKENS, init_invocation = __esm({ "../../packages/http-client/invocation.ts"() {
982
997
  "use strict";
983
998
  init_define_IGNITIFY_CLIENT_FILES();
984
999
  init_args();
1000
+ looksShort = (tok) => tok.length > 1 && tok.startsWith("-") && !tok.startsWith("--");
985
1001
  didYouMean = (flag) => `Did you mean --${flag}?`, itsFlags = (own, global) => `Its flags: ${own.length === 0 ? "(none)" : own.map((f) => `--${f}`).join(" ")}. Global: ${global.map((f) => `--${f}`).join(" ")}.`, flagTakesNoValue = (flag) => `--${flag} takes no value`, tooManyArgs = (command, max, extra, got) => `${command} takes ${max} argument${max === 1 ? "" : "s"}; got ${got} (${extra.map((e) => JSON.stringify(e)).join(", ")} ${extra.length === 1 ? "is" : "are"} extra)`;
986
1002
  HELP_TOKENS = ["", "help", "--help", "-h"];
987
1003
  } });
1004
+ function isLoopbackHost(hostname) {
1005
+ return hostname === "localhost" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
1006
+ }
1007
+ function keyUrlRefusal(url) {
1008
+ let u;
1009
+ try {
1010
+ u = new URL(url);
1011
+ } catch {
1012
+ return "not-url";
1013
+ }
1014
+ return u.protocol === "https:" || u.protocol === "http:" && isLoopbackHost(u.hostname) ? null : "insecure";
1015
+ }
1016
+ var init_key_url = __esm({ "../../packages/http-client/key-url.ts"() {
1017
+ "use strict";
1018
+ init_define_IGNITIFY_CLIENT_FILES();
1019
+ } });
1020
+ function scale(n, units) {
1021
+ let step = (v, i) => v >= 1024 && i < units.length - 1 ? step(v / 1024, i + 1) : i === 0 ? `${n} ${units[0]}` : `${v.toFixed(1)} ${units[i]}`;
1022
+ return step(n, 0);
1023
+ }
1024
+ function duration(ms) {
1025
+ if (ms < 1e3) return `${ms} ms`;
1026
+ let s = Math.round(ms / 1e3);
1027
+ if (s < 60) return `${s} s`;
1028
+ let m = Math.floor(s / 60);
1029
+ return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`;
1030
+ }
1031
+ function formatUsageQuantity(n, unit) {
1032
+ return unit === "bytes" ? scale(n, BYTE_UNITS) : unit === "ms" ? duration(n) : unit === "byte-hours" ? scale(n, BYTE_HOUR_UNITS) : `${n}`;
1033
+ }
1034
+ var BYTE_UNITS, BYTE_HOUR_UNITS, init_usage_format = __esm({ "../../packages/http-client/usage-format.ts"() {
1035
+ "use strict";
1036
+ init_define_IGNITIFY_CLIENT_FILES();
1037
+ BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"], BYTE_HOUR_UNITS = ["B\xB7h", "KiB\xB7h", "MiB\xB7h", "GiB\xB7h", "TiB\xB7h", "PiB\xB7h"];
1038
+ } });
988
1039
  function csvRow(values) {
989
1040
  return values.map((v) => `"${v.replace(/"/g, '""')}"`).join(",");
990
1041
  }
@@ -1027,6 +1078,16 @@ var PAGE_CURSOR_FLAG, PAGE_FLAG_PARAMS, init_page = __esm({ "../../packages/http
1027
1078
  init_define_IGNITIFY_CLIENT_FILES();
1028
1079
  PAGE_CURSOR_FLAG = "after-id", PAGE_FLAG_PARAMS = { limit: "limit", after: "after", "after-id": "after_id" };
1029
1080
  } });
1081
+ function httpErrorJson(e) {
1082
+ if (!(e instanceof HttpError)) return null;
1083
+ let body;
1084
+ try {
1085
+ body = JSON.parse(e.body);
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ return typeof body != "object" || body === null || Array.isArray(body) ? null : { status: e.status, body };
1090
+ }
1030
1091
  async function parseJson(res) {
1031
1092
  let text = await res.text();
1032
1093
  if (!res.ok) throw new HttpError(res.status, text, res.statusText, res.headers.get(TRACE_HEADER));
@@ -1052,6 +1113,8 @@ var DEFAULT_REQUEST_TIMEOUT_MS, TRACE_HEADER, HttpError, init_http_client = __es
1052
1113
  init_define_IGNITIFY_CLIENT_FILES();
1053
1114
  init_args();
1054
1115
  init_invocation();
1116
+ init_key_url();
1117
+ init_usage_format();
1055
1118
  init_csv();
1056
1119
  init_page();
1057
1120
  DEFAULT_REQUEST_TIMEOUT_MS = 12e4, TRACE_HEADER = "x-correlation-id", HttpError = class extends Error {
@@ -1073,11 +1136,14 @@ function fill(pattern, ...params) {
1073
1136
  if (i !== params.length) throw new Error(`route ${pattern}: takes ${i} parameter(s), given ${params.length}`);
1074
1137
  return out2;
1075
1138
  }
1076
- var DOT_SEGMENTS, TENANT_ROUTES, KEY_EXPIRED_ERROR, TENANT_PATHS, API_VERSION, API_VERSION_HEADER, CLIENT_FLOOR_HEADER, CLI_VERSION_HEADER, init_api_routes = __esm({ "../../packages/contract/api-routes.ts"() {
1139
+ function isTerminalRunStatus(status) {
1140
+ return TERMINAL_RUN_STATUSES.includes(status);
1141
+ }
1142
+ var DOT_SEGMENTS, TENANT_ROUTES, KEY_EXPIRED_ERROR, TENANT_PATHS, API_VERSION, API_VERSION_HEADER, CLIENT_FLOOR_HEADER, CLI_VERSION_HEADER, TERMINAL_RUN_STATUSES, init_api_routes = __esm({ "../../packages/contract/api-routes.ts"() {
1077
1143
  "use strict";
1078
1144
  init_define_IGNITIFY_CLIENT_FILES();
1079
1145
  DOT_SEGMENTS = /* @__PURE__ */ new Set([".", ".."]);
1080
- TENANT_ROUTES = { TASKS: "/tasks", TASK_BY_ID: "/tasks/:id", TASK_DELETION_PREVIEW: "/tasks/:id/deletion-preview", DEPLOYMENTS: "/deployments", DEPLOYMENT_BY_ID: "/deployments/:id", RUNS: "/runs", RUN_BY_ID: "/runs/:id", RUN_CANCEL: "/runs/:id/cancel", WHOAMI: "/whoami", PACKAGE_ADVICE: "/packages/advice", VMS: "/vms", VM_BY_ID: "/vms/:id", HOOKS: "/hooks", TRIGGER: "/hooks/trigger", TRIGGER_HOOK: "/hooks/trigger/:hookId", SECRETS: "/secrets", SECRET_BY_NAME: "/secrets/:name", SCHEDULES: "/schedules", SCHEDULE_BY_ID: "/schedules/:id", USAGE: "/usage", RUN_LOGS: "/runs/:id/logs", DEPLOYMENT_LOGS: "/deployments/:id/logs", DEPLOYMENT_HOOKS: "/deployments/:id/hooks", DEPLOYMENT_NOTIFY: "/deployments/:id/notify", HOOK_BY_ID: "/hooks/:id", READYZ: "/readyz", LIVEZ: "/livez", AUTH_DEVICE: "/auth/device", AUTH_DEVICE_TOKEN: "/auth/device/token", AUTH_KEY: "/auth/key" }, KEY_EXPIRED_ERROR = "API key has expired", TENANT_PATHS = { TASKS: TENANT_ROUTES.TASKS, TASK_BY_ID: (id) => fill(TENANT_ROUTES.TASK_BY_ID, id), TASK_DELETION_PREVIEW: (id) => fill(TENANT_ROUTES.TASK_DELETION_PREVIEW, id), DEPLOYMENTS: TENANT_ROUTES.DEPLOYMENTS, DEPLOYMENT_BY_ID: (id) => fill(TENANT_ROUTES.DEPLOYMENT_BY_ID, id), DEPLOYMENT_LOGS: (id) => fill(TENANT_ROUTES.DEPLOYMENT_LOGS, id), RUNS: TENANT_ROUTES.RUNS, RUN_BY_ID: (id) => fill(TENANT_ROUTES.RUN_BY_ID, id), RUN_CANCEL: (id) => fill(TENANT_ROUTES.RUN_CANCEL, id), RUN_LOGS: (id) => fill(TENANT_ROUTES.RUN_LOGS, id), WHOAMI: TENANT_ROUTES.WHOAMI, PACKAGE_ADVICE: TENANT_ROUTES.PACKAGE_ADVICE, VMS: TENANT_ROUTES.VMS, VM_BY_ID: (id) => fill(TENANT_ROUTES.VM_BY_ID, id), HOOKS: TENANT_ROUTES.HOOKS, TRIGGER: TENANT_ROUTES.TRIGGER, TRIGGER_HOOK: (hookId) => fill(TENANT_ROUTES.TRIGGER_HOOK, hookId), SECRETS: TENANT_ROUTES.SECRETS, SECRET_BY_NAME: (name) => fill(TENANT_ROUTES.SECRET_BY_NAME, name), SCHEDULES: TENANT_ROUTES.SCHEDULES, SCHEDULE_BY_ID: (id) => fill(TENANT_ROUTES.SCHEDULE_BY_ID, id), USAGE: TENANT_ROUTES.USAGE, DEPLOYMENT_HOOKS: (id) => fill(TENANT_ROUTES.DEPLOYMENT_HOOKS, id), DEPLOYMENT_NOTIFY: (id) => fill(TENANT_ROUTES.DEPLOYMENT_NOTIFY, id), HOOK_BY_ID: (id) => fill(TENANT_ROUTES.HOOK_BY_ID, id), READYZ: TENANT_ROUTES.READYZ, LIVEZ: TENANT_ROUTES.LIVEZ, AUTH_DEVICE: TENANT_ROUTES.AUTH_DEVICE, AUTH_DEVICE_TOKEN: TENANT_ROUTES.AUTH_DEVICE_TOKEN, AUTH_KEY: TENANT_ROUTES.AUTH_KEY }, API_VERSION = "1", API_VERSION_HEADER = "X-Api-Version", CLIENT_FLOOR_HEADER = "X-Client-Floor", CLI_VERSION_HEADER = "X-Ignitify-Cli";
1146
+ TENANT_ROUTES = { TASKS: "/tasks", TASK_BY_ID: "/tasks/:id", TASK_DELETION_PREVIEW: "/tasks/:id/deletion-preview", DEPLOYMENTS: "/deployments", DEPLOYMENT_BY_ID: "/deployments/:id", RUNS: "/runs", RUN_BY_ID: "/runs/:id", RUN_CANCEL: "/runs/:id/cancel", WHOAMI: "/whoami", PACKAGE_ADVICE: "/packages/advice", VMS: "/vms", VM_BY_ID: "/vms/:id", HOOKS: "/hooks", TRIGGER: "/hooks/trigger", TRIGGER_HOOK: "/hooks/trigger/:hookId", SECRETS: "/secrets", SECRET_BY_NAME: "/secrets/:name", SCHEDULES: "/schedules", SCHEDULE_BY_ID: "/schedules/:id", USAGE: "/usage", RUN_LOGS: "/runs/:id/logs", DEPLOYMENT_LOGS: "/deployments/:id/logs", DEPLOYMENT_HOOKS: "/deployments/:id/hooks", DEPLOYMENT_NOTIFY: "/deployments/:id/notify", HOOK_BY_ID: "/hooks/:id", READYZ: "/readyz", LIVEZ: "/livez", AUTH_DEVICE: "/auth/device", AUTH_DEVICE_TOKEN: "/auth/device/token", AUTH_KEY: "/auth/key" }, KEY_EXPIRED_ERROR = "API key has expired", TENANT_PATHS = { TASKS: TENANT_ROUTES.TASKS, TASK_BY_ID: (id) => fill(TENANT_ROUTES.TASK_BY_ID, id), TASK_DELETION_PREVIEW: (id) => fill(TENANT_ROUTES.TASK_DELETION_PREVIEW, id), DEPLOYMENTS: TENANT_ROUTES.DEPLOYMENTS, DEPLOYMENT_BY_ID: (id) => fill(TENANT_ROUTES.DEPLOYMENT_BY_ID, id), DEPLOYMENT_LOGS: (id) => fill(TENANT_ROUTES.DEPLOYMENT_LOGS, id), RUNS: TENANT_ROUTES.RUNS, RUN_BY_ID: (id) => fill(TENANT_ROUTES.RUN_BY_ID, id), RUN_CANCEL: (id) => fill(TENANT_ROUTES.RUN_CANCEL, id), RUN_LOGS: (id) => fill(TENANT_ROUTES.RUN_LOGS, id), WHOAMI: TENANT_ROUTES.WHOAMI, PACKAGE_ADVICE: TENANT_ROUTES.PACKAGE_ADVICE, VMS: TENANT_ROUTES.VMS, VM_BY_ID: (id) => fill(TENANT_ROUTES.VM_BY_ID, id), HOOKS: TENANT_ROUTES.HOOKS, TRIGGER: TENANT_ROUTES.TRIGGER, TRIGGER_HOOK: (hookId) => fill(TENANT_ROUTES.TRIGGER_HOOK, hookId), SECRETS: TENANT_ROUTES.SECRETS, SECRET_BY_NAME: (name) => fill(TENANT_ROUTES.SECRET_BY_NAME, name), SCHEDULES: TENANT_ROUTES.SCHEDULES, SCHEDULE_BY_ID: (id) => fill(TENANT_ROUTES.SCHEDULE_BY_ID, id), USAGE: TENANT_ROUTES.USAGE, DEPLOYMENT_HOOKS: (id) => fill(TENANT_ROUTES.DEPLOYMENT_HOOKS, id), DEPLOYMENT_NOTIFY: (id) => fill(TENANT_ROUTES.DEPLOYMENT_NOTIFY, id), HOOK_BY_ID: (id) => fill(TENANT_ROUTES.HOOK_BY_ID, id), READYZ: TENANT_ROUTES.READYZ, LIVEZ: TENANT_ROUTES.LIVEZ, AUTH_DEVICE: TENANT_ROUTES.AUTH_DEVICE, AUTH_DEVICE_TOKEN: TENANT_ROUTES.AUTH_DEVICE_TOKEN, AUTH_KEY: TENANT_ROUTES.AUTH_KEY }, API_VERSION = "1", API_VERSION_HEADER = "X-Api-Version", CLIENT_FLOOR_HEADER = "X-Client-Floor", CLI_VERSION_HEADER = "X-Ignitify-Cli", TERMINAL_RUN_STATUSES = ["COMPLETED", "FAILED", "CANCELLED"];
1081
1147
  } });
1082
1148
  var CALLBACK_PATH_PREFIX, init_callback_path = __esm({ "../../packages/contract/callback-path.ts"() {
1083
1149
  "use strict";
@@ -1097,14 +1163,49 @@ var TASK_CONFIG_FILENAMES, RUNTIME_REGISTRY, RUNTIME_IDS, LIVE_RUNTIME_IDS, isRu
1097
1163
  init_define_IGNITIFY_CLIENT_FILES();
1098
1164
  TASK_CONFIG_FILENAMES = ["task.yaml", "task.yml", "task.json"], RUNTIME_REGISTRY = Object.freeze({ node: Object.freeze({ entryFile: "index.ts", depManifest: "package.json", lockfile: "package-lock.json", wireRuntime: "node22", status: "live" }), python: Object.freeze({ entryFile: "main.py", depManifest: "requirements.txt", lockfile: "requirements.txt", wireRuntime: "python3.11", status: "live" }), go: Object.freeze({ entryFile: "main.go", depManifest: "go.mod", lockfile: "go.sum", wireRuntime: "go", status: "live" }), rust: Object.freeze({ entryFile: "src/main.rs", depManifest: "Cargo.toml", lockfile: "Cargo.lock", wireRuntime: "rust", status: "live" }) }), RUNTIME_IDS = Object.keys(RUNTIME_REGISTRY), LIVE_RUNTIME_IDS = RUNTIME_IDS.filter((id) => RUNTIME_REGISTRY[id].status === "live"), isRuntimeId = (v) => typeof v == "string" && Object.prototype.hasOwnProperty.call(RUNTIME_REGISTRY, v), TASK_CONFIG_AJV_OPTIONS = Object.freeze({ allErrors: true, strict: true, useDefaults: false }), TASK_CONFIG_SCHEMA = Object.freeze({ $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://ignitify.dev/schemas/task-config/v1.json", title: "Ignitify task configuration", type: "object", required: ["schema_version", "slug", "runtime"], additionalProperties: false, properties: { schema_version: { const: 1 }, slug: { type: "string", minLength: 1 }, runtime: { enum: [...RUNTIME_IDS] }, entry: { type: "string", minLength: 1 }, execution: { type: "object" }, secrets: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true }, run: { type: "object", additionalProperties: false, properties: { payload: { type: "object" }, preset: { type: "string", minLength: 1 }, timeout_sec: { type: "integer", exclusiveMinimum: 0 } } }, build: { type: "object", additionalProperties: false, properties: { allowed_hosts: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true }, ship: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true }, system_packages: { type: "array", items: { type: "string", minLength: 1 } } } } } }), BUILD_REFUSAL_STAGES = ["read", "fetch", "build", "smoke"], BUILD_STAGES = ["done", ...BUILD_REFUSAL_STAGES, "builder", "image", "cap"];
1099
1165
  } });
1166
+ function parseJsonText(text) {
1167
+ try {
1168
+ return { ok: true, value: JSON.parse(text) };
1169
+ } catch {
1170
+ return { ok: false };
1171
+ }
1172
+ }
1173
+ var isRecord, init_wire_json = __esm({ "../../packages/contract/wire-json.ts"() {
1174
+ "use strict";
1175
+ init_define_IGNITIFY_CLIENT_FILES();
1176
+ isRecord = (v) => typeof v == "object" && v !== null && !Array.isArray(v);
1177
+ } });
1178
+ function errText(err2) {
1179
+ if (err2 instanceof Error) return err2.message;
1180
+ try {
1181
+ return String(err2);
1182
+ } catch {
1183
+ return "unprintable error";
1184
+ }
1185
+ }
1186
+ function errCode(err2) {
1187
+ let code = isRecord(err2) ? err2.code : void 0;
1188
+ return typeof code == "string" ? code : "error";
1189
+ }
1190
+ var isMissing, init_err_text = __esm({ "../../packages/contract/err-text.ts"() {
1191
+ "use strict";
1192
+ init_define_IGNITIFY_CLIENT_FILES();
1193
+ init_wire_json();
1194
+ isMissing = (err2) => errCode(err2) === "ENOENT";
1195
+ } });
1196
+ var SHA256_HEX, SHA256_PREFIX, isSha256Hex, stripSha256Prefix, init_digest = __esm({ "../../packages/contract/digest.ts"() {
1197
+ "use strict";
1198
+ init_define_IGNITIFY_CLIENT_FILES();
1199
+ SHA256_HEX = /^[0-9a-f]{64}$/, SHA256_PREFIX = "sha256:", isSha256Hex = (v) => typeof v == "string" && SHA256_HEX.test(v), stripSha256Prefix = (s) => s.startsWith(SHA256_PREFIX) ? s.slice(SHA256_PREFIX.length) : s;
1200
+ } });
1100
1201
  function rootConfigName(paths) {
1101
1202
  return TASK_CONFIG_FILENAMES.find((n) => paths.includes(n)) ?? null;
1102
1203
  }
1103
1204
  function configKeyText(value) {
1104
- let top = value !== null && typeof value == "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).filter(([k]) => !RUN_ONLY_CONFIG_KEYS.includes(k))) : value, parts = [], bytes2 = 0, over = () => {
1205
+ let top = value !== null && typeof value == "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).filter(([k]) => !RUN_ONLY_CONFIG_KEYS.includes(k))) : value, parts = [], bytes = 0, over = () => {
1105
1206
  throw new Error(`its build settings are over ${CONFIG_KEY_TEXT_MAX_BYTES / 1024} KiB as canonical JSON (a YAML alias is expanded each time it is used)`);
1106
1207
  }, emit2 = (s) => {
1107
- bytes2 += utf8Bytes(s), bytes2 > CONFIG_KEY_TEXT_MAX_BYTES && over(), parts.push(s);
1208
+ bytes += utf8Bytes(s), bytes > CONFIG_KEY_TEXT_MAX_BYTES && over(), parts.push(s);
1108
1209
  }, walk = (v, depth) => {
1109
1210
  if (depth > CONFIG_KEY_MAX_DEPTH) throw new Error(`it is nested deeper than ${CONFIG_KEY_MAX_DEPTH} levels`);
1110
1211
  if (v === null || typeof v == "boolean") return emit2(String(v));
@@ -1150,39 +1251,39 @@ function privateKeyFileWarning(relPath) {
1150
1251
  let base = relPath.slice(relPath.lastIndexOf("/") + 1).toLowerCase();
1151
1252
  return base.endsWith(".pem") || base.endsWith(".key") || base.startsWith("id_rsa") && !base.endsWith(".pub") ? `${relPath} looks like a private key and is uploaded with the task, then stored in the image. If it is a secret, remove it (secrets go through \`secret:set\`); to keep a harmless file out, list it in \`.ignitifyignore\`` : null;
1152
1253
  }
1153
- function sourceManifest(entries, config) {
1254
+ function checkSourceManifest(entries, config) {
1154
1255
  let expected = rootConfigName(entries.map((e) => e.path));
1155
- if ((config?.name ?? null) !== expected) throw new Error(`source manifest: the task config in the key must be ${expected ?? "none"}, not ${config?.name ?? "none"}`);
1256
+ if ((config?.name ?? null) !== expected) return refused(`the task config in the key must be ${expected ?? "none"}, not ${config?.name ?? "none"}`);
1156
1257
  let configLine = [];
1157
1258
  if (config !== null) try {
1158
1259
  configLine = [`config ${config.name} ${configKeyText(config.value)}`];
1159
1260
  } catch (e) {
1160
- throw new Error(`source manifest: ${config.name}: ${e.message}`, { cause: e });
1261
+ return refused(`${config.name}: ${errText(e)}`);
1161
1262
  }
1162
1263
  let seen = /* @__PURE__ */ new Set();
1163
1264
  for (let e of entries) {
1164
- if (typeof e.path != "string" || e.path === "") throw new Error("source manifest: empty path");
1165
- if (e.path.includes("\\")) throw new Error(`source manifest: path contains a backslash: ${JSON.stringify(e.path)}. Separators must be normalised to "/" before hashing, or the same tree hashes differently per platform.`);
1166
- if (e.path.startsWith("/")) throw new Error(`source manifest: absolute path: ${e.path}`);
1167
- if (!isCanonicalRelativePath(e.path)) throw new Error(`source manifest: non-canonical path: ${e.path}`);
1168
- if (e.path.includes(`
1169
- `) || e.path.includes(" ")) throw new Error(`source manifest: path contains a tab or newline: ${JSON.stringify(e.path)}`);
1170
- if (!Number.isSafeInteger(e.size) || e.size < 0) throw new Error(`source manifest: bad size for ${e.path}: ${String(e.size)}`);
1171
- if (!HEX64.test(e.sha256)) throw new Error(`source manifest: sha256 for ${e.path} must be 64 lowercase hex chars`);
1172
- if (seen.has(e.path)) throw new Error(`source manifest: duplicate path: ${e.path}`);
1265
+ let bad = entryRefusal(e, seen);
1266
+ if (bad !== null) return refused(bad);
1173
1267
  seen.add(e.path);
1174
1268
  }
1175
1269
  let lines = entries.filter((e) => e.path !== config?.name).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((e) => `${e.exec ? "755" : "644"} ${e.size} ${e.sha256} ${e.path}`);
1176
- return [`ignitify-source/${SOURCE_MANIFEST_VERSION}`, ...configLine, ...lines, ""].join(`
1177
- `);
1270
+ return { ok: true, text: [`ignitify-source/${SOURCE_MANIFEST_VERSION}`, ...configLine, ...lines, ""].join(`
1271
+ `) };
1178
1272
  }
1179
- var SOURCE_MANIFEST_VERSION, RUN_ONLY_CONFIG_KEYS, CONFIG_KEY_TEXT_MAX_BYTES, UPLOAD_MAX_BYTES, CONFIG_KEY_MAX_DEPTH, SOURCE_EXCLUDED_DIRS, SOURCE_EXCLUDED_ROOT_DIRS, SOURCE_EXCLUDED_FILE_PREFIXES, CREDENTIAL_KEYS, CREDENTIAL_CHECKED_FILES, HEX64, isCanonicalRelativePath, init_source_manifest = __esm({ "../../packages/contract/source-manifest.ts"() {
1273
+ function entryRefusal(e, seen) {
1274
+ return typeof e.path != "string" || e.path === "" ? "empty path" : e.path.includes("\\") ? `path contains a backslash: ${JSON.stringify(e.path)}. Separators must be normalised to "/" before hashing, or the same tree hashes differently per platform.` : e.path.startsWith("/") ? `absolute path: ${e.path}` : isCanonicalRelativePath(e.path) ? e.path.includes(`
1275
+ `) || e.path.includes(" ") ? `path contains a tab or newline: ${JSON.stringify(e.path)}` : !Number.isSafeInteger(e.size) || e.size < 0 ? `bad size for ${e.path}: ${String(e.size)}` : isSha256Hex(e.sha256) ? seen.has(e.path) ? `duplicate path: ${e.path}` : null : `sha256 for ${e.path} must be 64 lowercase hex chars` : `non-canonical path: ${e.path}`;
1276
+ }
1277
+ var SOURCE_MANIFEST_VERSION, RUN_ONLY_CONFIG_KEYS, CONFIG_KEY_TEXT_MAX_BYTES, UPLOAD_MAX_BYTES, CONFIG_KEY_MAX_DEPTH, SOURCE_EXCLUDED_DIRS, SOURCE_EXCLUDED_ROOT_DIRS, SOURCE_EXCLUDED_FILE_PREFIXES, CREDENTIAL_KEYS, CREDENTIAL_CHECKED_FILES, isCanonicalRelativePath, refused, init_source_manifest = __esm({ "../../packages/contract/source-manifest.ts"() {
1180
1278
  "use strict";
1181
1279
  init_define_IGNITIFY_CLIENT_FILES();
1182
1280
  init_task_config();
1281
+ init_err_text();
1282
+ init_digest();
1183
1283
  SOURCE_MANIFEST_VERSION = "v2", RUN_ONLY_CONFIG_KEYS = Object.freeze(["execution", "secrets", "run"]), CONFIG_KEY_TEXT_MAX_BYTES = 64 * 1024, UPLOAD_MAX_BYTES = 64 * 1024 * 1024, CONFIG_KEY_MAX_DEPTH = 64;
1184
1284
  SOURCE_EXCLUDED_DIRS = Object.freeze([".git", ".ignitify", "node_modules", "__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]), SOURCE_EXCLUDED_ROOT_DIRS = Object.freeze(["target", "dist"]), SOURCE_EXCLUDED_FILE_PREFIXES = Object.freeze([".env"]), CREDENTIAL_KEYS = Object.freeze({ ".npmrc": /^[ \t]*(?![#;])[^=\n]*_(?:authToken|auth|password)[ \t]*=[ \t]*(?!\$\{\w+\}[ \t]*$)\S/m, ".yarnrc.yml": /^[ \t]*npmAuth(?:Token|Ident)[ \t]*:[ \t]*(?!["']?\$\{\w+\}["']?[ \t]*$)\S/m }), CREDENTIAL_CHECKED_FILES = Object.freeze(Object.keys(CREDENTIAL_KEYS));
1185
- HEX64 = /^[0-9a-f]{64}$/, isCanonicalRelativePath = (p) => !p.startsWith("/") && !p.includes("\\") && p.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
1285
+ isCanonicalRelativePath = (p) => !p.startsWith("/") && !p.includes("\\") && p.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
1286
+ refused = (reason) => ({ ok: false, reason });
1186
1287
  } });
1187
1288
  function normaliseSystemPackages(declared) {
1188
1289
  return [...new Set(declared)].sort();
@@ -1206,6 +1307,23 @@ var USAGE_AXES, USAGE_DETAIL_COMMAND, RESETS_AT_NEXT_RENEWAL, init_usage = __esm
1206
1307
  init_define_IGNITIFY_CLIENT_FILES();
1207
1308
  USAGE_AXES = ["compute", "network", "network_in", "disk", "scratch", "runs", "pin", "build"], USAGE_DETAIL_COMMAND = "ignitify quota", RESETS_AT_NEXT_RENEWAL = "your next paid renewal";
1208
1309
  } });
1310
+ function formatUserCode(code) {
1311
+ if (code.length !== 8) return code;
1312
+ let half = 8 / 2;
1313
+ return `${code.slice(0, half)}-${code.slice(half)}`;
1314
+ }
1315
+ var init_device_code = __esm({ "../../packages/contract/device-code.ts"() {
1316
+ "use strict";
1317
+ init_define_IGNITIFY_CLIENT_FILES();
1318
+ } });
1319
+ function cappedExponentialMs(step, baseMs, maxMs) {
1320
+ let n = Number.isNaN(step) ? 0 : Math.min(Math.max(0, Math.floor(step)), 32);
1321
+ return Math.min(baseMs * 2 ** n, maxMs);
1322
+ }
1323
+ var init_backoff = __esm({ "../../packages/contract/backoff.ts"() {
1324
+ "use strict";
1325
+ init_define_IGNITIFY_CLIENT_FILES();
1326
+ } });
1209
1327
  var init_tenant = __esm({ "../../packages/contract/tenant/index.ts"() {
1210
1328
  "use strict";
1211
1329
  init_define_IGNITIFY_CLIENT_FILES();
@@ -1217,23 +1335,26 @@ var init_tenant = __esm({ "../../packages/contract/tenant/index.ts"() {
1217
1335
  init_system_packages();
1218
1336
  init_package_advice();
1219
1337
  init_usage();
1338
+ init_wire_json();
1339
+ init_err_text();
1340
+ init_digest();
1341
+ init_device_code();
1342
+ init_backoff();
1220
1343
  } });
1221
1344
  function valueFlags(map2) {
1222
1345
  return Object.fromEntries(Object.keys(map2).map((flag) => [flag, "value"]));
1223
1346
  }
1224
- function isTerminalRunStatus(status) {
1225
- return TERMINAL_RUN_STATUSES.includes(status);
1226
- }
1227
- var COMMANDS, IGNORE_FILE, STATE, CREDENTIALS, LOGIN, LOGIN_VIEW, NETWORK, ENV, DEFAULT_TASKS_DIR, DEFAULTS, UPGRADE_COMMAND, PAYLOAD_LIMIT_FALLBACK_BYTES, RESULT_LIMIT_FALLBACK_BYTES, RUN_LIST_FLAG_PARAMS, VM_LIST_FLAG_PARAMS, GLOBAL_FLAGS, SERVER_FLAGS, HIDDEN_FLAGS, SHORT_FLAGS, VERSION_TOKENS, VALIDATE, ADVICE, INTEGRATION, NO_FLAGS, DIR, PAGE, YES, RAW, USAGE_FILTERS, COMMAND_META, VERSION_META, MESSAGES, RUN_STATUS, TERMINAL_RUN_STATUSES, ACTIVE_RUN_STATUSES, DEPLOYMENT_STATUS, POLL, WAIT, init_constants = __esm({ "src/constants/index.ts"() {
1347
+ var COMMANDS, IGNORE_FILE, STATE, CREDENTIALS, LOGIN, LOGIN_VIEW, NETWORK, ENV, DEFAULT_TASKS_DIR, DEFAULTS, UPGRADE_COMMAND, PAYLOAD_LIMIT_FALLBACK_BYTES, RESULT_LIMIT_FALLBACK_BYTES, RUN_LIST_FLAG_PARAMS, VM_LIST_FLAG_PARAMS, GLOBAL_FLAGS, SERVER_FLAGS, HIDDEN_FLAGS, SHORT_FLAGS, VERSION_TOKENS, VALIDATE, ADVICE, INTEGRATION, NO_FLAGS, DIR, PAGE, YES, RAW, USAGE_FLAG_PARAMS, USAGE_FILTERS, COMMAND_META, VERSION_META, MESSAGES, RUN_STATUS, ACTIVE_RUN_STATUSES, DEPLOYMENT_STATUS, POLL, WAIT, init_constants = __esm({ "src/constants/index.ts"() {
1228
1348
  "use strict";
1229
1349
  init_define_IGNITIFY_CLIENT_FILES();
1230
1350
  init_http_client();
1231
1351
  init_tenant();
1232
1352
  init_tenant();
1353
+ init_tenant();
1233
1354
  COMMANDS = { HELP: "help", LOGIN: "login", LOGOUT: "logout", WHOAMI: "whoami", QUOTA: "quota", TASK_CREATE: "task:create", TASK_LIST: "task:list", TASK_GET: "task:get", TASK_DELETE: "task:delete", TASK_VALIDATE: "task:validate", TASK_PACK: "task:pack", TASK_DEPLOY: "task:deploy", TASK_RUN: "task:run", DEPLOYMENT_LIST: "deployment:list", DEPLOYMENT_GET: "deployment:get", DEPLOYMENT_LOGS: "deployment:logs", DEPLOYMENT_DELETE: "deployment:delete", RUN_LIST: "run:list", RUN_GET: "run:get", RUN_LOGS: "run:logs", RUN_DELETE: "run:delete", RUN_CANCEL: "run:cancel", VM_LIST: "vm:list", VM_GET: "vm:get", VM_DELETE: "vm:delete", SECRET_SET: "secret:set", SECRET_LIST: "secret:list", SECRET_DELETE: "secret:delete", SCHEDULE_LIST: "schedule:list", SCHEDULE_CANCEL: "schedule:cancel", HOOK_CREATE: "hook:create", HOOK_LIST: "hook:list", HOOK_DELETE: "hook:delete", NOTIFY_SET: "notify:set", INTEGRATION_ADD: "integration:add", USAGE: "usage", USAGE_EXPORT: "usage:export" }, IGNORE_FILE = ".ignitifyignore", STATE = { DIR: ".ignitify", FILE: "state.json", GITIGNORE_FILE: ".gitignore", GITIGNORE_TEXT: `# written by ignitify: local state, never commit
1234
1355
  *
1235
1356
  ` }, CREDENTIALS = { DIR_NAME: "ignitify", FILE_NAME: "credentials.json", DIR_MODE: 448, FILE_MODE: 384 }, LOGIN = { MAX_RETRIES: 3, KEY_TTL_DAYS: 90, MIN_POLL_INTERVAL_SEC: 5, BROWSER_SCHEMES: ["https:", "http:"] }, LOGIN_VIEW = { TITLE: "Sign in to Ignitify", OPEN_PAGE: "Open this page:", ASK_OPERATOR: "Give this code to your operator, who approves it:", CHECK_CODE: "Check this code", SAME_CODE: "The page must show the same code.", WAITING_STATIC: (minutes) => `Waiting for approval. The code expires in ${minutes} minutes.`, WAITING_LIVE: (left) => `Waiting for approval... code expires in ${left}`, CANCELLED: "Sign-in cancelled. Nothing was saved.", SIGNED_IN: "Signed in", SIGNED_OUT: "Signed out", KEY_EXPIRES: (days) => `${days} days after last use; each use renews it`, YES: "yes", ROWS: { ACCOUNT: "Account", ACCOUNT_ID: "Account id", KEY_LABEL: "Key label", KEY_EXPIRES: "Key expires", SAVED_TO: "Saved to", REVOKED: "Revoked", DELETED: "Deleted" } }, NETWORK = { REASONS: { ECONNREFUSED: "nothing accepts connections there", ENOTFOUND: "the host name is unknown", EAI_AGAIN: "the host name lookup failed", ETIMEDOUT: "the connection timed out", UND_ERR_CONNECT_TIMEOUT: "the connection timed out", ECONNRESET: "the connection was reset" }, TLS_WORDS: "the TLS certificate was not accepted", TLS_CODES: ["CERT_HAS_EXPIRED", "CERT_NOT_YET_VALID", "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"], TLS_CODE_PREFIXES: ["ERR_TLS_", "ERR_SSL_"], CODE_PATTERN: /^[A-Z][A-Z0-9_]*$/, DETAIL_MAX_CHARS: 80 }, ENV = { TASKS_DIR: "IGNITIFY_TASKS_DIR", KEY: "IGNITIFY_API_KEY" }, DEFAULT_TASKS_DIR = "tasks", DEFAULTS = { API_URL: "https://api.ignitify.dev" }, UPGRADE_COMMAND = "npm install --global ignitify@latest", PAYLOAD_LIMIT_FALLBACK_BYTES = 10240, RESULT_LIMIT_FALLBACK_BYTES = 10240, RUN_LIST_FLAG_PARAMS = { status: "status", "deployment-id": "deployment_id", "task-name": "task_name", preset: "preset", "correlation-id": "correlation_id", "vm-id": "vm_id", ...PAGE_FLAG_PARAMS }, VM_LIST_FLAG_PARAMS = { "deployment-id": "deployment_id", "task-name": "task_name", preset: "preset", "older-than": "older_than", ...PAGE_FLAG_PARAMS }, GLOBAL_FLAGS = { json: "bool", help: "bool" }, SERVER_FLAGS = { api: "value" }, HIDDEN_FLAGS = ["api"], SHORT_FLAGS = { h: "help", y: "yes" }, VERSION_TOKENS = ["--version", "-v"];
1236
- VALIDATE = { VALID: "task config valid" }, ADVICE = { FLAG: "review-packages", TIMEOUT_MS: 6e4, FAULT_STATUSES: [400, 401, 403], SECONDS_ONLY_BELOW: 60, SECONDS_PER_MINUTE: 60, NO_PACKAGES: "this task lists no build.system_packages", SKIPPED: (reason) => `AI check skipped: ${reason}`, RATE_LIMITED: (retryAfterS) => `rate limited, try again in ${retryAfterS} s`, UNAVAILABLE: (reason) => `the review is unavailable (${reason})`, NO_ANSWER: (status, message) => status === null ? message : `server answered ${status}`, FAULT: (status, body) => `AI check failed (HTTP ${status}): ${body}`, BAD_NAMES: (bad) => `not valid package names: ${bad.join(", ")}`, TOO_MANY: (count, max) => `${count} packages listed, the most is ${max}`, BASIS: { measured: "measured", model: "model estimate" }, NO_FIT: (driveMib, capMib) => `does not fit: ${driveMib} MiB of packages against the ${capMib} MiB image store of your tier`, TITLE: "Package review", NOTE: "AI advice only. A suggestion is not checked against Debian.", COLUMNS: { PACKAGE: "Package", VERDICT: "Verdict", SIZE: "Size", NOTE: "Note", SUGGEST: "Suggest" }, NOT_DOUBLE_CHECKED: "not double-checked by the second model", RESULT_CLEAN: (total) => `No concerns found in ${total} package${total === 1 ? "" : "s"}.`, RESULT_FLAGGED: (total, flagged) => `${total} package${total === 1 ? "" : "s"} checked, ${flagged} to look at.`, NOT_ANSWERED: (names) => `No AI answer for: ${names.join(", ")}. The review could not judge ${names.length === 1 ? "it" : "them"}.`, BUILD_TITLE: "Build time", BUILD_LABELS: { ADDED: "Added", APPLIES: "Applies to", SIZE: "Packages size", LEFT_OUT: "Left out" }, BUILD_NONE: "nothing: your tenant already holds the drive for this list", BUILD_ADDED: (range, basis) => `about ${range} (${basis})`, BUILD_APPLIES: "the first deploy of this list. A later code-only redeploy reuses the drive.", BUILD_SIZE: (mib, capMib) => capMib === null ? `${mib} MiB` : `${mib} MiB of ${capMib} MiB (the image store of your tier)`, BUILD_LEFT_OUT: (names) => `${names.join(", ")} (unusable, or size unknown)` }, INTEGRATION = { PACKAGE_JSON: "package.json", NODE_MODULES: "node_modules", RUNTIME: "node", NPM: "npm", TARBALL_EXT: ".tgz", PACKED_ROOT: "package", PACK_TMP_PREFIX: "ignitify-integration-pack-", PACK_ARGS: (pkg, destDir, taskDir) => ["pack", pkg, "--pack-destination", destDir, "--prefix", taskDir], INSTALL_ARGS: (spec, taskDir) => ["install", spec, "--save-prod", "--ignore-scripts", "--prefix", taskDir], PACKED_MAX_BYTES: 8 * 1024 * 1024, ZLIB_TOO_BIG: "ERR_BUFFER_TOO_LARGE", VERSION_PATTERN: /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, QUESTION: "Apply?", REGISTRY_URL: "https://registry.npmjs.org", REGISTRY_ACCEPT: "application/vnd.npm.install-v1+json", REGISTRY_TIMEOUT_MS: 5e3, NOTED_FIELDS: ["execution.network.type", "run.preset"], USAGE: "usage: ignitify integration:add <name> --task <slug> [--yes | --dry-run] [--dir <base-dir>]", TASK_REQUIRED: "--task <slug> is required: the task folder to add the integration to", YES_WITH_DRY_RUN: "--yes and --dry-run cannot be used together", BAD_NAME: (name, pattern) => `integration name ${JSON.stringify(name)} must match ${pattern}`, NOT_NODE: (runtime) => `integrations work with Node tasks only; this task has runtime "${runtime}"`, NO_PACKAGE_JSON: (taskDir) => `${taskDir} has no package.json: run npm init in the task folder first; nothing was changed`, NOT_INTERACTIVE: "Refusing to proceed: not a TTY. Re-run with --yes to apply the plan, or with --dry-run to see it.", PACK_FAILED: (pkg, code) => `npm pack ${pkg} failed${code === null ? "" : ` (exit ${code})`}; nothing in the task was changed`, INSTALL_FAILED: (spec, code) => `npm install ${spec} failed${code === null ? "" : ` (exit ${code})`}; task.yaml was not changed`, INSTALLED: (spec) => `${spec} installed`, BAD_VERSION: (pkg, version2) => `${pkg}: its package.json version ${JSON.stringify(version2)} is not a plain x.y.z version; nothing in the task was changed`, PACKED_TOO_BIG: (file, max) => `${file} unpacks to more than ${max} bytes: refused, nothing in the task was changed`, INSTALL_ONLY: (spec) => `installs ${spec}; task.yaml unchanged`, NOT_AN_INTEGRATION: (pkg, file) => `${pkg} has no ${file}: it is not an integration package`, BAD_MANIFEST: (pkg, issue) => `${pkg} integration.yaml: ${issue}`, UNREADABLE: (file, reason) => `cannot read ${file}: ${reason}`, WRITE_MISMATCH: (file) => `the edited ${file} does not read back as the plan; nothing was written`, YOU_ADD: "you add:", NO_CHANGES: "task.yaml already has every setting this integration needs; nothing to write", DRY_RUN: "dry run: task.yaml was not changed", WRITTEN: (file) => `${file} updated`, NOT_INSTALLED: (pkgs) => `${pkgs.join(", ")} ${pkgs.length === 1 ? "is" : "are"} in package.json but not in node_modules: run npm install, then task:validate again`, OBSOLETE: (pkg, version2, latest) => `${pkg}@${version2} is obsolete: use ${latest}`, OBSOLETE_SKIPPED: (reason) => `obsolete-version check skipped: ${reason}`, REGISTRY_UNREACHABLE: "the npm registry did not answer (offline?)", REGISTRY_NOT_FOUND: (pkg) => `${pkg} is not on the npm registry` }, NO_FLAGS = {}, DIR = { dir: "value" }, PAGE = valueFlags(PAGE_FLAG_PARAMS), YES = { yes: "bool" }, RAW = { raw: "bool" }, USAGE_FILTERS = { period: "value", axis: "value", vm: "value", run: "value" }, COMMAND_META = { [COMMANDS.HELP]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.LOGIN]: { flags: { ...SERVER_FLAGS, "no-browser": "bool", name: "value" }, positionals: 0, confirms: false }, [COMMANDS.LOGOUT]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.WHOAMI]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.QUOTA]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.TASK_CREATE]: { flags: { ...SERVER_FLAGS, ...DIR, lang: "value", local: "bool", "no-init": "bool", "no-scaffold": "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_LIST]: { flags: { ...SERVER_FLAGS, ...PAGE }, positionals: 0, confirms: false }, [COMMANDS.TASK_GET]: { flags: { ...SERVER_FLAGS, ...DIR }, positionals: 1, confirms: false }, [COMMANDS.TASK_DELETE]: { flags: { ...SERVER_FLAGS, ...DIR, ...YES }, positionals: 1, confirms: true }, [COMMANDS.TASK_VALIDATE]: { flags: { ...SERVER_FLAGS, ...DIR, [ADVICE.FLAG]: "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_PACK]: { flags: DIR, positionals: 1, confirms: false }, [COMMANDS.TASK_DEPLOY]: { flags: { ...SERVER_FLAGS, ...DIR, check: "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_RUN]: { flags: { deployment: "value", payload: "value", "timeout-sec": "value", "on-busy": "value", at: "value", wait: "bool", ...DIR, ...SERVER_FLAGS }, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_LIST]: { flags: { ...SERVER_FLAGS, ...PAGE }, positionals: 0, confirms: false }, [COMMANDS.DEPLOYMENT_GET]: { flags: SERVER_FLAGS, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_LOGS]: { flags: { ...SERVER_FLAGS, ...RAW }, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.RUN_LIST]: { flags: { ...SERVER_FLAGS, ...valueFlags(RUN_LIST_FLAG_PARAMS) }, positionals: 0, confirms: false }, [COMMANDS.RUN_GET]: { flags: { ...SERVER_FLAGS, full: "bool" }, positionals: 1, confirms: false }, [COMMANDS.RUN_LOGS]: { flags: { ...SERVER_FLAGS, ...RAW }, positionals: 1, confirms: false }, [COMMANDS.RUN_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.RUN_CANCEL]: { flags: { ...SERVER_FLAGS, ...YES, force: "bool", grace: "value" }, positionals: 1, confirms: true }, [COMMANDS.VM_LIST]: { flags: { ...SERVER_FLAGS, ...valueFlags(VM_LIST_FLAG_PARAMS), pinned: "bool" }, positionals: 0, confirms: false }, [COMMANDS.VM_GET]: { flags: SERVER_FLAGS, positionals: 1, confirms: false }, [COMMANDS.VM_DELETE]: { flags: { ...SERVER_FLAGS, ...YES, force: "bool" }, positionals: 1, confirms: true }, [COMMANDS.SECRET_SET]: { flags: { ...SERVER_FLAGS, stdin: "bool", value: "value" }, positionals: 1, confirms: false, secretArgv: true }, [COMMANDS.SECRET_LIST]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.SECRET_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.SCHEDULE_LIST]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.SCHEDULE_CANCEL]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.HOOK_CREATE]: { flags: { ...SERVER_FLAGS, "task-name": "value" }, positionals: 1, confirms: false }, [COMMANDS.HOOK_LIST]: { flags: { ...SERVER_FLAGS, ...PAGE }, positionals: 1, confirms: false }, [COMMANDS.HOOK_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.NOTIFY_SET]: { flags: { ...SERVER_FLAGS, url: "value", clear: "bool", "secret-stdin": "bool", secret: "value" }, positionals: 1, confirms: false, secretArgv: true }, [COMMANDS.INTEGRATION_ADD]: { flags: { ...DIR, ...YES, task: "value", "dry-run": "bool" }, positionals: 1, confirms: true }, [COMMANDS.USAGE]: { flags: { ...SERVER_FLAGS, ...USAGE_FILTERS, runs: "bool", vms: "bool" }, positionals: 0, confirms: false }, [COMMANDS.USAGE_EXPORT]: { flags: { ...SERVER_FLAGS, ...USAGE_FILTERS, csv: "bool" }, positionals: 0, confirms: false } }, VERSION_META = { flags: SERVER_FLAGS, positionals: 0, confirms: false }, MESSAGES = { NOT_SIGNED_IN: "Not signed in. Run 'ignitify login', or set the IGNITIFY_API_KEY environment variable.", CREDENTIALS_SYMLINK: (file) => `${file} is a symbolic link. Remove it and run 'ignitify login' again.`, CREDENTIALS_NOT_FILE: (file) => `${file} is not a regular file. Remove it and run 'ignitify login' again.`, CREDENTIALS_NOT_OWNED: (file) => `${file} belongs to another user. Remove it and run 'ignitify login' again.`, CREDENTIALS_TOO_OPEN: (file, mode) => `${file} can be read by other users (mode 0${mode}). Run: chmod 600 ${file}`, CREDENTIALS_UNREADABLE: (file) => `${file} is not a valid credentials file. Remove it and run 'ignitify login' again.`, CREDENTIALS_OTHER_URL: (fileUrl, url) => `The saved key is for ${fileUrl}, not ${url}. Run 'ignitify login --api ${url}'.`, CREDENTIALS_DIR_NOT_DIR: (dir) => `${dir} is not a folder. Remove it and run 'ignitify login' again.`, API_URL_NOT_URL: (url) => `The API address "${url}" is not a URL.`, API_URL_NOT_HTTPS: (url) => `The API address ${url} is not https. The CLI sends your key only over https, or over plain http to this machine (localhost, 127.0.0.1, ::1).`, LOGIN_REPLACED: (label) => `Note: replaced the key "${label}" of another machine. Use 'ignitify login --name <other-label>' to keep both.`, LOGIN_EXPIRED: (code) => `The code ${code} expired before it was approved. Run 'ignitify login' again.`, LOGIN_REFUSED: (reason) => `The sign-in was refused: ${reason}.`, LOGIN_UNKNOWN: "The sign-in code is unknown or was already used. Run 'ignitify login' again.", LOGIN_BAD_ANSWER: "The server sent a sign-in answer with no usable key. Nothing was saved.", LOGIN_BAD_START: "The server sent a sign-in start that the CLI cannot use (no code, or no wait times). Nothing was started.", ENV_KEY_OVERRIDES_LOGIN: "Note: the IGNITIFY_API_KEY environment variable is set, so it is used instead of your saved login.", KEY_EXPIRED_HINT: "Run 'ignitify login' to sign in again.", LOGIN_TOO_MANY: "Too many sign-in attempts from this network. Try again later.", LOGIN_CAP: (limit) => `This account already has the most API keys its plan allows${limit === null ? "" : ` (${limit})`}. Remove one, or approve with "replace", then run 'ignitify login' again.`, CANNOT_REACH: (url, detail) => `Cannot reach ${url}${detail === null ? "" : ` (${detail})`}. Check your network and run 'ignitify login' again.`, UNREACHABLE: (origin, detail) => `Cannot reach ${origin}${detail === null ? "" : ` (${detail})`}.`, LOGOUT_NOTHING: "Not signed in. There are no saved credentials.", LOGOUT_ENV_NOTE: "Note: the IGNITIFY_API_KEY environment variable is still set. Logout does not change it.", LOGOUT_NOT_REVOKED: (label) => `Warning: the server did not revoke the key, so it may still work. The saved credentials were deleted. Ask your operator to revoke the key "${label}".`, SLUG_MISSING: (source) => `'slug' is required in ${source}`, SLUG_INVALID: (source, reason) => `${source}: 'slug' ${reason}`, PRESET_INVALID: "run.preset must be a non-empty string", PRESET_UNKNOWN: (source, name, valid) => `${source}: unknown run.preset "${name}" (valid: ${valid.join(", ")}). An unrecognised preset would silently fall back to the platform default, giving you a different instance class than you declared.`, TIMEOUT_INVALID: "run.timeout_sec must be a positive number", TASK_ID_REQUIRED: "a <task-id> is required", TASK_ID_INVALID: (id, reason) => `task id ${JSON.stringify(id)} ${reason}`, ERROR: (message) => `Error: ${message}`, ERROR_PREFIX: "Error:", SERVER_TRACE_ID: (id) => `server trace id: ${id}`, ARG_REQUIRED: (label) => `${label} is required`, UNKNOWN_COMMAND: "unknown command", SEE_HELP: "Run 'ignitify help' to see every command.", UNKNOWN_SHORT_FLAG: (short, long, command) => `unknown flag ${short} (--${long}) for ${command}`, GRACE_INVALID: (got) => `--grace must be a whole number of milliseconds, 0 or more (got ${JSON.stringify(got)})`, GRACE_WITH_FORCE: "--grace has no effect with --force (force stops the run now)", TASK_LIMITS: (payload, result) => `payload max ${payload}, result max ${result}; over the limit the run fails`, PAYLOAD_OVER_LIMIT: (bytes2, limit) => `payload is ${bytes2} bytes; the limit is ${limit} bytes`, PAYLOAD_REFUSED_BY_SERVER: (limit) => `the server refused the payload as too large; the limit is ${limit} bytes`, DEFAULT_PAYLOAD_OVER_LIMIT: (source, bytes2, limit) => `${source}: run.payload is ${bytes2} bytes; the offline limit is ${limit} bytes (the server may allow more)`, IGNORED_BY_FILE: `ignored by ${IGNORE_FILE}`, IGNORED_COUNT: (n) => `${n} path(s) left out by ${IGNORE_FILE}`, TASK_DELETED: "task deleted", TASK_LINK_REMOVED: "Removed local link", TASK_LINK_NOT_REMOVED: (link, reason) => `Warning: the task is deleted, but its local link was not removed (${reason}). Remove ${link} by hand.`, DEPLOY_DELETED: "deployment deleted", RUN_DELETED: "run deleted", ABORTED: "Aborted.", CONFIRM_PROCEED: "Proceed?", SECRET_SET_USAGE: "usage: ignitify secret:set <NAME> --stdin (the value is read from stdin)", SECRET_VALUE_EMPTY: "secret:set read an empty value from stdin \u2014 nothing was stored (pipe the value in: `--stdin < value-file`)", SECRET_VALUE_ON_ARGV: `secret:set takes no value on argv: it is visible in \`ps\` and is written to your shell history. Pipe it on stdin, e.g.
1357
+ VALIDATE = { VALID: "task config valid" }, ADVICE = { FLAG: "review-packages", TIMEOUT_MS: 6e4, FAULT_STATUSES: [400, 401, 403], SECONDS_ONLY_BELOW: 60, SECONDS_PER_MINUTE: 60, NO_PACKAGES: "this task lists no build.system_packages", SKIPPED: (reason) => `AI check skipped: ${reason}`, RATE_LIMITED: (retryAfterS) => `rate limited, try again in ${retryAfterS} s`, UNAVAILABLE: (reason) => `the review is unavailable (${reason})`, NO_ANSWER: (status, message) => status === null ? message : `server answered ${status}`, FAULT: (status, body) => `AI check failed (HTTP ${status}): ${body}`, BAD_NAMES: (bad) => `not valid package names: ${bad.join(", ")}`, TOO_MANY: (count, max) => `${count} packages listed, the most is ${max}`, BASIS: { measured: "measured", model: "model estimate" }, NO_FIT: (driveMib, capMib) => `does not fit: ${driveMib} MiB of packages against the ${capMib} MiB image store of your tier`, TITLE: "Package review", NOTE: "AI advice only. A suggestion is not checked against Debian.", COLUMNS: { PACKAGE: "Package", VERDICT: "Verdict", SIZE: "Size", NOTE: "Note", SUGGEST: "Suggest" }, NOT_DOUBLE_CHECKED: "not double-checked by the second model", RESULT_CLEAN: (total) => `No concerns found in ${total} package${total === 1 ? "" : "s"}.`, RESULT_FLAGGED: (total, flagged) => `${total} package${total === 1 ? "" : "s"} checked, ${flagged} to look at.`, NOT_ANSWERED: (names) => `No AI answer for: ${names.join(", ")}. The review could not judge ${names.length === 1 ? "it" : "them"}.`, BUILD_TITLE: "Build time", BUILD_LABELS: { ADDED: "Added", APPLIES: "Applies to", SIZE: "Packages size", LEFT_OUT: "Left out" }, BUILD_NONE: "nothing: your tenant already holds the drive for this list", BUILD_ADDED: (range, basis) => `about ${range} (${basis})`, BUILD_APPLIES: "the first deploy of this list. A later code-only redeploy reuses the drive.", BUILD_SIZE: (mib, capMib) => capMib === null ? `${mib} MiB` : `${mib} MiB of ${capMib} MiB (the image store of your tier)`, BUILD_LEFT_OUT: (names) => `${names.join(", ")} (unusable, or size unknown)` }, INTEGRATION = { PACKAGE_JSON: "package.json", NODE_MODULES: "node_modules", RUNTIME: "node", NPM: "npm", TARBALL_EXT: ".tgz", PACKED_ROOT: "package", PACK_TMP_PREFIX: "ignitify-integration-pack-", PACK_ARGS: (pkg, destDir, taskDir) => ["pack", pkg, "--pack-destination", destDir, "--prefix", taskDir], INSTALL_ARGS: (spec, taskDir) => ["install", spec, "--save-prod", "--ignore-scripts", "--prefix", taskDir], PACKED_MAX_BYTES: 8 * 1024 * 1024, ZLIB_TOO_BIG: "ERR_BUFFER_TOO_LARGE", VERSION_PATTERN: /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, QUESTION: "Apply?", REGISTRY_URL: "https://registry.npmjs.org", REGISTRY_ACCEPT: "application/vnd.npm.install-v1+json", REGISTRY_TIMEOUT_MS: 5e3, NOTED_FIELDS: ["execution.network.type", "run.preset"], USAGE: "usage: ignitify integration:add <name> --task <slug> [--yes | --dry-run] [--dir <base-dir>]", TASK_REQUIRED: "--task <slug> is required: the task folder to add the integration to", YES_WITH_DRY_RUN: "--yes and --dry-run cannot be used together", BAD_NAME: (name, pattern) => `integration name ${JSON.stringify(name)} must match ${pattern}`, NOT_NODE: (runtime) => `integrations work with Node tasks only; this task has runtime "${runtime}"`, NO_PACKAGE_JSON: (taskDir) => `${taskDir} has no package.json: run npm init in the task folder first; nothing was changed`, NOT_INTERACTIVE: "Refusing to proceed: not a TTY. Re-run with --yes to apply the plan, or with --dry-run to see it.", PACK_FAILED: (pkg, code) => `npm pack ${pkg} failed${code === null ? "" : ` (exit ${code})`}; nothing in the task was changed`, INSTALL_FAILED: (spec, code) => `npm install ${spec} failed${code === null ? "" : ` (exit ${code})`}; task.yaml was not changed`, INSTALLED: (spec) => `${spec} installed`, BAD_VERSION: (pkg, version2) => `${pkg}: its package.json version ${JSON.stringify(version2)} is not a plain x.y.z version; nothing in the task was changed`, PACKED_TOO_BIG: (file, max) => `${file} unpacks to more than ${max} bytes: refused, nothing in the task was changed`, INSTALL_ONLY: (spec) => `installs ${spec}; task.yaml unchanged`, NOT_AN_INTEGRATION: (pkg, file) => `${pkg} has no ${file}: it is not an integration package`, BAD_MANIFEST: (pkg, issue) => `${pkg} integration.yaml: ${issue}`, UNREADABLE: (file, reason) => `cannot read ${file}: ${reason}`, WRITE_MISMATCH: (file) => `the edited ${file} does not read back as the plan; nothing was written`, YOU_ADD: "you add:", NO_CHANGES: "task.yaml already has every setting this integration needs; nothing to write", DRY_RUN: "dry run: task.yaml was not changed", WRITTEN: (file) => `${file} updated`, NOT_INSTALLED: (pkgs) => `${pkgs.join(", ")} ${pkgs.length === 1 ? "is" : "are"} in package.json but not in node_modules: run npm install, then task:validate again`, OBSOLETE: (pkg, version2, latest) => `${pkg}@${version2} is obsolete: use ${latest}`, OBSOLETE_SKIPPED: (reason) => `obsolete-version check skipped: ${reason}`, REGISTRY_UNREACHABLE: "the npm registry did not answer (offline?)", REGISTRY_NOT_FOUND: (pkg) => `${pkg} is not on the npm registry` }, NO_FLAGS = {}, DIR = { dir: "value" }, PAGE = valueFlags(PAGE_FLAG_PARAMS), YES = { yes: "bool" }, RAW = { raw: "bool" }, USAGE_FLAG_PARAMS = { period: "period", axis: "axis", vm: "vm", run: "run" }, USAGE_FILTERS = valueFlags(USAGE_FLAG_PARAMS), COMMAND_META = { [COMMANDS.HELP]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.LOGIN]: { flags: { ...SERVER_FLAGS, "no-browser": "bool", name: "value" }, positionals: 0, confirms: false }, [COMMANDS.LOGOUT]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.WHOAMI]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.QUOTA]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.TASK_CREATE]: { flags: { ...SERVER_FLAGS, ...DIR, lang: "value", local: "bool", "no-init": "bool", "no-scaffold": "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_LIST]: { flags: { ...SERVER_FLAGS, ...PAGE }, positionals: 0, confirms: false }, [COMMANDS.TASK_GET]: { flags: { ...SERVER_FLAGS, ...DIR }, positionals: 1, confirms: false }, [COMMANDS.TASK_DELETE]: { flags: { ...SERVER_FLAGS, ...DIR, ...YES }, positionals: 1, confirms: true }, [COMMANDS.TASK_VALIDATE]: { flags: { ...SERVER_FLAGS, ...DIR, [ADVICE.FLAG]: "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_PACK]: { flags: DIR, positionals: 1, confirms: false }, [COMMANDS.TASK_DEPLOY]: { flags: { ...SERVER_FLAGS, ...DIR, check: "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_RUN]: { flags: { deployment: "value", payload: "value", "timeout-sec": "value", "on-busy": "value", at: "value", wait: "bool", ...DIR, ...SERVER_FLAGS }, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_LIST]: { flags: { ...SERVER_FLAGS, ...PAGE }, positionals: 0, confirms: false }, [COMMANDS.DEPLOYMENT_GET]: { flags: SERVER_FLAGS, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_LOGS]: { flags: { ...SERVER_FLAGS, ...RAW }, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.RUN_LIST]: { flags: { ...SERVER_FLAGS, ...valueFlags(RUN_LIST_FLAG_PARAMS) }, positionals: 0, confirms: false }, [COMMANDS.RUN_GET]: { flags: { ...SERVER_FLAGS, full: "bool" }, positionals: 1, confirms: false }, [COMMANDS.RUN_LOGS]: { flags: { ...SERVER_FLAGS, ...RAW }, positionals: 1, confirms: false }, [COMMANDS.RUN_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.RUN_CANCEL]: { flags: { ...SERVER_FLAGS, ...YES, force: "bool", grace: "value" }, positionals: 1, confirms: true }, [COMMANDS.VM_LIST]: { flags: { ...SERVER_FLAGS, ...valueFlags(VM_LIST_FLAG_PARAMS), pinned: "bool" }, positionals: 0, confirms: false }, [COMMANDS.VM_GET]: { flags: SERVER_FLAGS, positionals: 1, confirms: false }, [COMMANDS.VM_DELETE]: { flags: { ...SERVER_FLAGS, ...YES, force: "bool" }, positionals: 1, confirms: true }, [COMMANDS.SECRET_SET]: { flags: { ...SERVER_FLAGS, stdin: "bool", value: "value" }, positionals: 1, confirms: false, secretArgv: true }, [COMMANDS.SECRET_LIST]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.SECRET_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.SCHEDULE_LIST]: { flags: SERVER_FLAGS, positionals: 0, confirms: false }, [COMMANDS.SCHEDULE_CANCEL]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.HOOK_CREATE]: { flags: { ...SERVER_FLAGS, "task-name": "value" }, positionals: 1, confirms: false }, [COMMANDS.HOOK_LIST]: { flags: { ...SERVER_FLAGS, ...PAGE }, positionals: 1, confirms: false }, [COMMANDS.HOOK_DELETE]: { flags: { ...SERVER_FLAGS, ...YES }, positionals: 1, confirms: true }, [COMMANDS.NOTIFY_SET]: { flags: { ...SERVER_FLAGS, url: "value", clear: "bool", "secret-stdin": "bool", secret: "value" }, positionals: 1, confirms: false, secretArgv: true }, [COMMANDS.INTEGRATION_ADD]: { flags: { ...DIR, ...YES, task: "value", "dry-run": "bool" }, positionals: 1, confirms: true }, [COMMANDS.USAGE]: { flags: { ...SERVER_FLAGS, ...USAGE_FILTERS, runs: "bool", vms: "bool" }, positionals: 0, confirms: false }, [COMMANDS.USAGE_EXPORT]: { flags: { ...SERVER_FLAGS, ...USAGE_FILTERS, csv: "bool" }, positionals: 0, confirms: false } }, VERSION_META = { flags: SERVER_FLAGS, positionals: 0, confirms: false }, MESSAGES = { NOT_SIGNED_IN: "Not signed in. Run 'ignitify login', or set the IGNITIFY_API_KEY environment variable.", CREDENTIALS_SYMLINK: (file) => `${file} is a symbolic link. Remove it and run 'ignitify login' again.`, CREDENTIALS_NOT_FILE: (file) => `${file} is not a regular file. Remove it and run 'ignitify login' again.`, CREDENTIALS_NOT_OWNED: (file) => `${file} belongs to another user. Remove it and run 'ignitify login' again.`, CREDENTIALS_TOO_OPEN: (file, mode) => `${file} can be read by other users (mode 0${mode}). Run: chmod 600 ${file}`, CREDENTIALS_UNREADABLE: (file) => `${file} is not a valid credentials file. Remove it and run 'ignitify login' again.`, CREDENTIALS_OTHER_URL: (fileUrl, url) => `The saved key is for ${fileUrl}, not ${url}. Run 'ignitify login --api ${url}'.`, CREDENTIALS_DIR_NOT_DIR: (dir) => `${dir} is not a folder. Remove it and run 'ignitify login' again.`, API_URL_NOT_URL: (url) => `The API address "${url}" is not a URL.`, API_URL_NOT_HTTPS: (url) => `The API address ${url} is not https. The CLI sends your key only over https, or over plain http to this machine (localhost, 127.0.0.1, ::1).`, LOGIN_REPLACED: (label) => `Note: replaced the key "${label}" of another machine. Use 'ignitify login --name <other-label>' to keep both.`, LOGIN_EXPIRED: (code) => `The code ${code} expired before it was approved. Run 'ignitify login' again.`, LOGIN_REFUSED: (reason) => `The sign-in was refused: ${reason}.`, LOGIN_UNKNOWN: "The sign-in code is unknown or was already used. Run 'ignitify login' again.", LOGIN_BAD_ANSWER: "The server sent a sign-in answer with no usable key. Nothing was saved.", LOGIN_BAD_START: "The server sent a sign-in start that the CLI cannot use (no code, or no wait times). Nothing was started.", ENV_KEY_OVERRIDES_LOGIN: "Note: the IGNITIFY_API_KEY environment variable is set, so it is used instead of your saved login.", KEY_EXPIRED_HINT: "Run 'ignitify login' to sign in again.", LOGIN_TOO_MANY: "Too many sign-in attempts from this network. Try again later.", LOGIN_CAP: (limit) => `This account already has the most API keys its plan allows${limit === null ? "" : ` (${limit})`}. Remove one, or approve with "replace", then run 'ignitify login' again.`, CANNOT_REACH: (url, detail) => `Cannot reach ${url}${detail === null ? "" : ` (${detail})`}. Check your network and run 'ignitify login' again.`, UNREACHABLE: (origin, detail) => `Cannot reach ${origin}${detail === null ? "" : ` (${detail})`}.`, LOGOUT_NOTHING: "Not signed in. There are no saved credentials.", LOGOUT_ENV_NOTE: "Note: the IGNITIFY_API_KEY environment variable is still set. Logout does not change it.", LOGOUT_NOT_REVOKED: (label) => `Warning: the server did not revoke the key, so it may still work. The saved credentials were deleted. Ask your operator to revoke the key "${label}".`, SLUG_MISSING: (source) => `'slug' is required in ${source}`, SLUG_INVALID: (source, reason) => `${source}: 'slug' ${reason}`, PRESET_INVALID: "run.preset must be a non-empty string", PRESET_UNKNOWN: (source, name, valid) => `${source}: unknown run.preset "${name}" (valid: ${valid.join(", ")}). An unrecognised preset would silently fall back to the platform default, giving you a different instance class than you declared.`, TIMEOUT_INVALID: "run.timeout_sec must be a positive number", TASK_ID_REQUIRED: "a <task-id> is required", TASK_ID_INVALID: (id, reason) => `task id ${JSON.stringify(id)} ${reason}`, ERROR: (message) => `Error: ${message}`, ERROR_PREFIX: "Error:", SERVER_TRACE_ID: (id) => `server trace id: ${id}`, ARG_REQUIRED: (label) => `${label} is required`, UNKNOWN_COMMAND: "unknown command", SEE_HELP: "Run 'ignitify help' to see every command.", UNKNOWN_SHORT_FLAG: (short, long, command) => `unknown flag ${short} (--${long}) for ${command}`, GRACE_INVALID: (got) => `--grace must be a whole number of milliseconds, 0 or more (got ${JSON.stringify(got)})`, GRACE_WITH_FORCE: "--grace has no effect with --force (force stops the run now)", TASK_LIMITS: (payload, result) => `payload max ${payload}, result max ${result}; over the limit the run fails`, PAYLOAD_OVER_LIMIT: (bytes, limit) => `payload is ${bytes} bytes; the limit is ${limit} bytes`, PAYLOAD_REFUSED_BY_SERVER: (limit) => `the server refused the payload as too large; the limit is ${limit} bytes`, DEFAULT_PAYLOAD_OVER_LIMIT: (source, bytes, limit) => `${source}: run.payload is ${bytes} bytes; the offline limit is ${limit} bytes (the server may allow more)`, IGNORED_BY_FILE: `ignored by ${IGNORE_FILE}`, IGNORED_COUNT: (n) => `${n} path(s) left out by ${IGNORE_FILE}`, TASK_DELETED: "task deleted", TASK_LINK_REMOVED: "Removed local link", TASK_LINK_NOT_REMOVED: (link, reason) => `Warning: the task is deleted, but its local link was not removed (${reason}). Remove ${link} by hand.`, DEPLOY_DELETED: "deployment deleted", RUN_DELETED: "run deleted", ABORTED: "Aborted.", CONFIRM_PROCEED: "Proceed?", SECRET_SET_USAGE: "usage: ignitify secret:set <NAME> --stdin (the value is read from stdin)", SECRET_VALUE_EMPTY: "secret:set read an empty value from stdin \u2014 nothing was stored (pipe the value in: `--stdin < value-file`)", SECRET_VALUE_ON_ARGV: `secret:set takes no value on argv: it is visible in \`ps\` and is written to your shell history. Pipe it on stdin, e.g.
1237
1358
  ignitify secret:set <NAME> --stdin < value-file
1238
1359
  printf %s "$VALUE" | ignitify secret:set <NAME> --stdin
1239
1360
  \u26A0\uFE0F The value you just typed is already in your history \u2014 rotate it.`, NOTIFY_SECRET_ON_ARGV: '--secret is not accepted: a value on argv is visible in `ps` and is written to your shell history. Use --secret-stdin, e.g.\n printf %s "$SECRET" | ignitify notify:set <deployment-id> --url <https://\u2026> --secret-stdin\n\u26A0\uFE0F The value you just typed is already in your history \u2014 rotate it.', HOOK_CREATE_USAGE: "usage: ignitify hook:create <deployment-id> --task-name <name>", HOOK_TASK_NAME_REQUIRED: `--task-name <name> is required: a hook starts a run of ONE task in the deployment, and a run without a task name fails.
@@ -1241,15 +1362,14 @@ usage: ignitify hook:create <deployment-id> --task-name <name>`, HOOK_LIST_USAGE
1241
1362
  ignitify notify:set <deployment-id> --clear
1242
1363
  --secret-stdin reads the signing secret (at least ${32} characters) from stdin; it is
1243
1364
  required with --url. There is no --secret flag: a value on argv is visible in \`ps\`
1244
- and lands in your shell history.`, NOTIFY_URL_NEEDS_SECRET: "--url needs --secret-stdin: the server refuses a notify target with no signing secret.", NOTIFY_SECRET_EMPTY: "--secret-stdin read an empty value from stdin.", NOTIFY_URL_WITH_CLEAR: "--url and --clear cannot be used together.", NOTIFY_SECRET_STDIN_VALUE: "--secret-stdin takes no value: pipe the secret on stdin.", HOOK_SECRET_SHOWN_ONCE: "\u26A0\uFE0F The secret below is shown ONCE and cannot be read back \u2014 hook:list never returns it. Store it now; if it is lost, delete the hook and create a new one.", HOOK_PAYLOAD_LIMIT: (limit) => `A webhook body over ${limit} is refused with 413 before the signature is checked.`, SECRET_DELETE_USAGE: "usage: ignitify secret:delete <NAME>", USAGE_EXPORT_USAGE: "usage: ignitify usage:export [--period YYYY-MM] (--json|--csv) (name exactly one format)", LATEST_DEPLOY_BUILDING: (task, deploymentId) => `the latest deploy of ${task} is still building (${deploymentId}). Wait for it ('ignitify deployment:get ${deploymentId}'), or pass --deployment <id> to run an older one.`, LATEST_DEPLOY_FAILED: (task, reason) => `the latest deploy of ${task} failed: ${reason ?? "no reason recorded"}. Fix the task and run 'ignitify task:deploy ${task}' again, or pass --deployment <id> to run an older one.`, RUN_AT_WITH_WAIT: "--wait cannot be used with --at: a scheduled run has no run id until it fires. Drop --wait; follow it later with 'ignitify schedule:list' and 'ignitify run:list'.", SECRET_STORED: (name) => `secret ${name} stored`, SECRET_REMOVED: (name) => `secret ${name} removed` }, RUN_STATUS = { PENDING: "PENDING", PROVISIONING: "PROVISIONING", RUNNING: "RUNNING", RETRYING: "RETRYING", COMPLETED: "COMPLETED", FAILED: "FAILED", CANCELLED: "CANCELLED" }, TERMINAL_RUN_STATUSES = [RUN_STATUS.COMPLETED, RUN_STATUS.FAILED, RUN_STATUS.CANCELLED];
1245
- ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([RUN_STATUS.PENDING, RUN_STATUS.PROVISIONING, RUN_STATUS.RUNNING, RUN_STATUS.RETRYING]), DEPLOYMENT_STATUS = { BUILDING: "BUILDING", READY: "READY", FAILED: "FAILED", ARCHIVED: "ARCHIVED" }, POLL = { DEPLOY: { BASE_MS: 500, MAX_MS: 1e3, TIMEOUT_MS: 900 * 1e3 }, RUN: { BASE_MS: 500, MAX_MS: 3e3, TIMEOUT_MS: 1800 * 1e3 }, ERROR: { MIN_TRIES: 3, GRACE_MS: 6e4 } }, WAIT = { UPLOADING: (slug2, size) => `Uploading ${slug2}${size ? ` (${size})` : ""}`, UPLOAD_FAILED: "Upload failed", BUILDING: (deploymentId) => `Building image ${deploymentId}`, BUILT: "Image built", REUSED: "Image already built \u2014 reused", BUILD_FAILED: "Build failed", BUILD_FAILED_DETAIL: (deploymentId, reason) => `deployment ${deploymentId} failed to build \u2014 ${reason}`, BUILD_TIMEOUT: "Still building after 15 minutes \u2014 stopped waiting", BUILD_TIMEOUT_DETAIL: (deploymentId) => `deployment ${deploymentId} is still building after 15 minutes. The build has NOT been cancelled \u2014 check it with 'ignitify deployment:get ${deploymentId}'.`, RUN: (runId, status) => `Run ${runId} ${status}`, RUN_SUBMITTED: (runId) => `Run ${runId} submitted`, RUN_ENDED: (status) => `Run ${status}`, RUN_TIMEOUT: (status) => `Run is still ${status} after 30 minutes \u2014 stopped waiting`, RUN_TIMEOUT_DETAIL: (runId) => `The run has NOT been cancelled \u2014 check it with 'ignitify run:get ${runId}'.`, HINT_RETRYING: "lost contact with the server \u2014 retrying", LOST: "Lost contact with the server \u2014 stopped waiting", LOST_BUILD_DETAIL: (deploymentId, cause) => `stopped waiting for deployment ${deploymentId}: ${cause}. The build has NOT been cancelled \u2014 check it with 'ignitify deployment:get ${deploymentId}'.`, LOST_RUN_DETAIL: (runId, cause) => `stopped waiting for run ${runId}: ${cause}. The run has NOT been cancelled \u2014 check it with 'ignitify run:get ${runId}'.`, HINT_BUILD: "Ctrl+C stops waiting, not the build", HINT_RUN: "Ctrl+C stops waiting, not the run", INTERRUPTED_BUILD: (deploymentId, started) => `Stopped waiting. The build ${started ? "keeps running" : "is queued"} on the server \u2014 nothing was cancelled.
1365
+ and lands in your shell history.`, NOTIFY_URL_NEEDS_SECRET: "--url needs --secret-stdin: the server refuses a notify target with no signing secret.", NOTIFY_SECRET_EMPTY: "--secret-stdin read an empty value from stdin.", NOTIFY_URL_WITH_CLEAR: "--url and --clear cannot be used together.", NOTIFY_SECRET_STDIN_VALUE: "--secret-stdin takes no value: pipe the secret on stdin.", HOOK_SECRET_SHOWN_ONCE: "\u26A0\uFE0F The secret below is shown ONCE and cannot be read back \u2014 hook:list never returns it. Store it now; if it is lost, delete the hook and create a new one.", HOOK_PAYLOAD_LIMIT: (limit) => `A webhook body over ${limit} is refused with 413 before the signature is checked.`, SECRET_DELETE_USAGE: "usage: ignitify secret:delete <NAME>", USAGE_EXPORT_USAGE: "usage: ignitify usage:export [--period YYYY-MM] (--json|--csv) (name exactly one format)", LATEST_DEPLOY_BUILDING: (task, deploymentId) => `the latest deploy of ${task} is still building (${deploymentId}). Wait for it ('ignitify deployment:get ${deploymentId}'), or pass --deployment <id> to run an older one.`, LATEST_DEPLOY_FAILED: (task, reason) => `the latest deploy of ${task} failed: ${reason ?? "no reason recorded"}. Fix the task and run 'ignitify task:deploy ${task}' again, or pass --deployment <id> to run an older one.`, RUN_AT_WITH_WAIT: "--wait cannot be used with --at: a scheduled run has no run id until it fires. Drop --wait; follow it later with 'ignitify schedule:list' and 'ignitify run:list'.", SECRET_STORED: (name) => `secret ${name} stored`, SECRET_REMOVED: (name) => `secret ${name} removed` }, RUN_STATUS = { PENDING: "PENDING", PROVISIONING: "PROVISIONING", RUNNING: "RUNNING", RETRYING: "RETRYING", COMPLETED: "COMPLETED", FAILED: "FAILED", CANCELLED: "CANCELLED" }, ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([RUN_STATUS.PENDING, RUN_STATUS.PROVISIONING, RUN_STATUS.RUNNING, RUN_STATUS.RETRYING]), DEPLOYMENT_STATUS = { BUILDING: "BUILDING", READY: "READY", FAILED: "FAILED", ARCHIVED: "ARCHIVED" }, POLL = { DEPLOY: { BASE_MS: 500, MAX_MS: 1e3, TIMEOUT_MS: 900 * 1e3 }, RUN: { BASE_MS: 500, MAX_MS: 3e3, TIMEOUT_MS: 1800 * 1e3 }, ERROR: { MIN_TRIES: 3, GRACE_MS: 6e4 } }, WAIT = { UPLOADING: (slug2, size) => `Uploading ${slug2}${size ? ` (${size})` : ""}`, UPLOAD_FAILED: "Upload failed", BUILDING: (deploymentId) => `Building image ${deploymentId}`, BUILT: "Image built", REUSED: "Image already built \u2014 reused", BUILD_FAILED: "Build failed", BUILD_FAILED_DETAIL: (deploymentId, reason) => `deployment ${deploymentId} failed to build \u2014 ${reason}`, BUILD_TIMEOUT: "Still building after 15 minutes \u2014 stopped waiting", BUILD_TIMEOUT_DETAIL: (deploymentId) => `deployment ${deploymentId} is still building after 15 minutes. The build has NOT been cancelled \u2014 check it with 'ignitify deployment:get ${deploymentId}'.`, RUN: (runId, status) => `Run ${runId} ${status}`, RUN_SUBMITTED: (runId) => `Run ${runId} submitted`, RUN_ENDED: (status) => `Run ${status}`, RUN_TIMEOUT: (status) => `Run is still ${status} after 30 minutes \u2014 stopped waiting`, RUN_TIMEOUT_DETAIL: (runId) => `The run has NOT been cancelled \u2014 check it with 'ignitify run:get ${runId}'.`, HINT_RETRYING: "lost contact with the server \u2014 retrying", LOST: "Lost contact with the server \u2014 stopped waiting", LOST_BUILD_DETAIL: (deploymentId, cause) => `stopped waiting for deployment ${deploymentId}: ${cause}. The build has NOT been cancelled \u2014 check it with 'ignitify deployment:get ${deploymentId}'.`, LOST_RUN_DETAIL: (runId, cause) => `stopped waiting for run ${runId}: ${cause}. The run has NOT been cancelled \u2014 check it with 'ignitify run:get ${runId}'.`, HINT_BUILD: "Ctrl+C stops waiting, not the build", HINT_RUN: "Ctrl+C stops waiting, not the run", INTERRUPTED_BUILD: (deploymentId, started) => `Stopped waiting. The build ${started ? "keeps running" : "is queued"} on the server \u2014 nothing was cancelled.
1246
1366
  Check it with: ignitify deployment:get ${deploymentId}`, INTERRUPTED_RUN: (runId) => `Stopped waiting. The run keeps going on the server \u2014 nothing was cancelled.
1247
1367
  Check it with: ignitify run:get ${runId}` };
1248
1368
  } });
1249
1369
  var ANSI, GLYPHS, LAYOUT, LABELS, EMPTY_LISTS, DISPLAY_CAPS, TITLES, PER_BRAIN_MARKER, PER_BRAIN_FOOTNOTE, POLICY_UNIT_NOTES, HIDDEN_FIELDS, RESULT_MESSAGES, RUN_CANCEL_OUTCOME, init_constants2 = __esm({ "src/output/constants.ts"() {
1250
1370
  "use strict";
1251
1371
  init_define_IGNITIFY_CLIENT_FILES();
1252
- ANSI = { reset: "\x1B[0m", bold: "\x1B[1m", dim: "\x1B[2m", underline: "\x1B[4m", red: "\x1B[31m", green: "\x1B[32m", yellow: "\x1B[33m", cyan: "\x1B[36m", clearLine: "\r\x1B[2K", hideCursor: "\x1B[?25l", showCursor: "\x1B[?25h" }, GLYPHS = { unicode: { ok: "\u2713", err: "\u2717", warn: "\u26A0", bullet: "\u2022", ellipsis: "\u2026", dash: "\u2014", rule: "\u2500", box: { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", h: "\u2500", v: "\u2502" }, frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] }, ascii: { ok: "OK", err: "x", warn: "!", bullet: "*", ellipsis: "...", dash: "-", rule: "-", box: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" }, frames: ["-", "\\", "|", "/"] } }, LAYOUT = { MAX_WIDTH: 80, INDENT: " ", GAP: " ", MIN_COLUMN: 8, SPINNER_MS: 100, NO_UNICODE_TERMS: ["dumb", "linux"] }, LABELS = { NEXT_STEP: "Next Step:", SYSTEM_RESOURCES: "System Resources:", RESULT: "Result:", MORE_LINES: (n) => `\u2026 ${n} more lines (run:get --full shows all)`, BUILD_LOG: "Build log:", PLATFORM_BUILT_IMAGE: "Image: built and stored by the platform; there is nothing for you to download.", EARLIER_LINES: (n) => `\u2026 ${n} earlier lines (deployment:get <id> shows all)`, NO_RESULTS: "(No results)", EMPTY: "(empty)" }, EMPTY_LISTS = { ANY: { message: "(No results)" }, TASKS: { message: "No tasks yet.", next: "Create one with: ignitify task:create <task-id>" }, DEPLOYMENTS: { message: "No deployments yet.", next: "Build one with: ignitify task:deploy <task-id>" }, RUNS: { message: "No runs yet.", next: "Start one with: ignitify task:run <task-id>" }, VMS: { message: "No live VMs." }, SCHEDULES: { message: "No scheduled runs.", next: "Schedule one with: ignitify task:run <task-id> --at <iso>" }, HOOKS: { message: "No webhooks yet.", next: "Create one with: ignitify hook:create <deployment-id> --task-name <name>" } }, DISPLAY_CAPS = { RESULT_MAX_LINES: 40, BUILD_LOG_TAIL_LINES: 30, ERROR_MAX_CHARS: 2e3 }, TITLES = { TENANT_PROFILE: "Tenant Profile", TASK_REGISTERED: "Task Registered", SOURCE_PACK: "Source Upload (nothing built, nothing sent)", DEPLOYMENT_CREATED: "Deployment Created", RUN_SUBMITTED: "Run Submitted", RUN_DETAILS: "Run Details", DEPLOYMENT_DETAILS: "Deployment Details", VM_DETAILS: "VM Details", USAGE: "Usage & Limits", UNIT_WEIGHTS: "Units per run (by preset)", ENTITLEMENTS: "Entitlements (tier policy)", ENFORCED: "Enforced Limits (what this server applies)" }, PER_BRAIN_MARKER = "(per server)", PER_BRAIN_FOOTNOTE = " (per server) \u2014 each server applies this limit on its own, so the limit across all servers is at least this.", POLICY_UNIT_NOTES = { max_compute_seconds_month: "(run seconds, not the VM time `usage` shows as compute)" }, HIDDEN_FIELDS = { SCALAR: ["payload", "result", "slug"], LIST: ["payload", "result", "execution"] }, RESULT_MESSAGES = { TASK_GET_OFFLINE: (reason) => `Note: ${reason.replace(/\.$/, "")}. The server sections show UNKNOWN.`, TASK_CREATED_LOCAL: (lang, taskId, dir) => `Created ${lang} task ${taskId} at ${dir} (local only \u2014 not registered)`, HOOK_CREATED_TITLE: "Webhook Created", HOOK_SECRET_KEY: "secret (shown ONCE)", HOOK_DELETED: (id) => `Webhook deleted: ${id}`, NOTIFY_SET: (deploymentId, url) => `Notify URL for ${deploymentId} set to ${url}`, NOTIFY_CLEARED: (deploymentId) => `Notify URL for ${deploymentId} cleared`, SCHEDULE_CANCELLED: (id) => `Schedule cancelled: ${id}`, NO_SECRETS: "(no secrets)", NO_SECRETS_NEXT: "Store one with: ignitify secret:set <NAME> --stdin" }, RUN_CANCEL_OUTCOME = { ALREADY_TERMINAL: "already-terminal", DRAINING: "draining", CANCELLED: "cancelled" };
1372
+ ANSI = { reset: "\x1B[0m", bold: "\x1B[1m", dim: "\x1B[2m", underline: "\x1B[4m", red: "\x1B[31m", green: "\x1B[32m", yellow: "\x1B[33m", cyan: "\x1B[36m", clearLine: "\r\x1B[2K", hideCursor: "\x1B[?25l", showCursor: "\x1B[?25h" }, GLYPHS = { unicode: { ok: "\u2713", err: "\u2717", warn: "\u26A0", bullet: "\u2022", ellipsis: "\u2026", dash: "\u2014", rule: "\u2500", box: { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", h: "\u2500", v: "\u2502" }, frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] }, ascii: { ok: "OK", err: "x", warn: "!", bullet: "*", ellipsis: "...", dash: "-", rule: "-", box: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" }, frames: ["-", "\\", "|", "/"] } }, LAYOUT = { MAX_WIDTH: 80, INDENT: " ", GAP: " ", MIN_COLUMN: 8, SPINNER_MS: 100, NO_UNICODE_TERMS: ["dumb", "linux"] }, LABELS = { NEXT_STEP: "Next Step:", SYSTEM_RESOURCES: "System Resources:", RESULT: "Result:", MORE_LINES: (n) => `\u2026 ${n} more lines (run:get --full shows all)`, BUILD_LOG: "Build log:", PLATFORM_BUILT_IMAGE: "Image: built and stored by the platform; there is nothing for you to download.", EARLIER_LINES: (n) => `\u2026 ${n} earlier lines (deployment:get <id> shows all)`, NO_RESULTS: "(No results)", EMPTY: "(empty)" }, EMPTY_LISTS = { ANY: { message: "(No results)" }, TASKS: { message: "No tasks yet.", next: "Create one with: ignitify task:create <task-id>" }, DEPLOYMENTS: { message: "No deployments yet.", next: "Build one with: ignitify task:deploy <task-id>" }, RUNS: { message: "No runs yet.", next: "Start one with: ignitify task:run <task-id>" }, VMS: { message: "No live VMs." }, SCHEDULES: { message: "No scheduled runs.", next: "Schedule one with: ignitify task:run <task-id> --at <iso>" }, HOOKS: { message: "No webhooks yet.", next: "Create one with: ignitify hook:create <deployment-id> --task-name <name>" } }, DISPLAY_CAPS = { RESULT_MAX_LINES: 40, BUILD_LOG_TAIL_LINES: 30, ERROR_MAX_CHARS: 2e3 }, TITLES = { TENANT_PROFILE: "Tenant Profile", TASK_REGISTERED: "Task Registered", SOURCE_PACK: "Source Upload (nothing built, nothing sent)", DEPLOYMENT_CREATED: "Deployment Created", RUN_SUBMITTED: "Run Submitted", RUN_DETAILS: "Run Details", DEPLOYMENT_DETAILS: "Deployment Details", VM_DETAILS: "VM Details", USAGE: "Usage & Limits", UNIT_WEIGHTS: "Units per run (by preset)", ENTITLEMENTS: "Entitlements (tier policy)", ENFORCED: "Enforced Limits (what this server applies)" }, PER_BRAIN_MARKER = "(per server)", PER_BRAIN_FOOTNOTE = " (per server) \u2014 each server applies this limit on its own, so the limit across all servers is at least this.", POLICY_UNIT_NOTES = { max_compute_seconds_month: "(run seconds, not the VM time `usage` shows as compute)" }, HIDDEN_FIELDS = { SCALAR: ["payload", "result", "slug"], LIST: ["payload", "result", "execution"] }, RESULT_MESSAGES = { TASK_GET_OFFLINE: (reason) => `Note: ${reason.replace(/\.$/, "")}. The server sections show UNKNOWN.`, TASK_CREATED_LOCAL: (lang, taskId, dir) => `Created ${lang} task ${taskId} at ${dir} (local only \u2014 not registered)`, HOOK_CREATED_TITLE: "Webhook Created", HOOK_SECRET_KEY: "secret (shown ONCE)", HOOK_DELETED: (id) => `Webhook deleted: ${id}`, NOTIFY_SET: (deploymentId, url) => `Notify URL for ${deploymentId} set to ${url}`, NOTIFY_CLEARED: (deploymentId) => `Notify URL for ${deploymentId} cleared`, SCHEDULE_CANCELLED: (id) => `Schedule cancelled: ${id}`, SCHEDULE_CANCEL_PREVIEW: (id) => `Will cancel schedule ${id} \u2014 it will never fire.`, SCHEDULE_NOT_CANCELLED: (id) => `Schedule ${id} was NOT cancelled: not found, already fired, or currently firing.`, HOOK_DELETE_PREVIEW: (id) => `Will delete webhook ${id}. A sender that posts to it is refused from then on.`, SECRET_DELETE_PREVIEW: (name) => `Will delete secret ${name}. Its value cannot be read back.`, DEPLOYMENT_DELETE_PREVIEW: (id) => `Will soft-delete deployment ${id} and all its runs (cascade).`, RUN_DELETE_PREVIEW: (id) => `Will soft-delete run ${id}.`, RUN_ALREADY_OVER: (id, status) => `Run ${id} is already ${status} \u2014 nothing to cancel.`, RUN_CANCEL_PREVIEW: (id, force) => force ? `Will cancel run ${id} immediately (force: destroys the VM even if it is pinned).` : `Will ask run ${id} to stop gracefully; it is killed if it does not finish in time.`, RUN_STOPPING: (id, grace, was) => `Run ${id} is stopping${grace}${was} \u2014 still RUNNING until it finishes or the grace expires.`, RUN_CANCELLED: (id, was) => `Run cancelled: ${id}${was}`, LOG_TRUNCATED: (lines) => `truncated \u2014 ${lines} line(s) shown; the log was cut at the size cap.`, NO_SECRETS: "(no secrets)", NO_SECRETS_NEXT: "Store one with: ignitify secret:set <NAME> --stdin" }, RUN_CANCEL_OUTCOME = { ALREADY_TERMINAL: "already-terminal", DRAINING: "draining", CANCELLED: "cancelled" };
1253
1373
  } });
1254
1374
  function makeStyle(o) {
1255
1375
  let p = (...codes) => paint(o.colour, ...codes), columns = Math.max(o.width, 1);
@@ -1509,7 +1629,7 @@ var init_execution = __esm({ "../../packages/execution/index.ts"() {
1509
1629
  init_egress_match();
1510
1630
  init_ingress_hostname();
1511
1631
  } });
1512
- var sectionHeader, layout, filterScalars, filterDisplayKeys, BYTE_UNITS, formatBytes, quotaLine, storeLine, renderWhoami, hasUnits, renderUnitWeights, showEnforced, renderEnforcedBlock, renderPolicyValue, renderPolicyBlock, renderUsageBlocks, renderUsage, renderTaskCreate, renderPack, renderDeploy, renderRunSubmit, renderRunNotices, renderCapacity, capacityBlock, renderRunGet, renderList, imageSize, renderDeploymentList, packagesLine, renderDeploymentGet, formatDuration, renderVmList, pinLines, renderVmRows, renderVmGet, nextStepBanner, init_formatters = __esm({ "src/output/formatters.ts"() {
1632
+ var sectionHeader, layout, filterScalars, filterDisplayKeys, BYTE_UNITS2, formatBytes, quotaLine, storeLine, renderWhoami, hasUnits, renderUnitWeights, showEnforced, renderEnforcedBlock, renderPolicyValue, renderPolicyBlock, renderUsageBlocks, renderUsage, renderTaskCreate, renderPack, renderDeploy, renderRunSubmit, renderRunNotices, isStorageClamped, renderCapacity, capacityBlock, renderRunGet, renderList, imageSize, renderDeploymentList, packagesLine, renderDeploymentGet, formatDuration, renderVmList, pinLines, renderVmRows, renderVmGet, nextStepBanner, init_formatters = __esm({ "src/output/formatters.ts"() {
1513
1633
  "use strict";
1514
1634
  init_define_IGNITIFY_CLIENT_FILES();
1515
1635
  init_constants();
@@ -1523,17 +1643,13 @@ var sectionHeader, layout, filterScalars, filterDisplayKeys, BYTE_UNITS, formatB
1523
1643
  ${heading(st, title, success)}
1524
1644
  `, layout = (header, content, footer = "") => `${header}${content}${footer ? `
1525
1645
  ` + footer : `
1526
- `}`, filterScalars = (data) => {
1527
- let filtered = {};
1528
- for (let [k, v] of Object.entries(data)) typeof v != "object" && !HIDDEN_FIELDS.SCALAR.includes(k) && (filtered[k] = v);
1529
- return filtered;
1530
- }, filterDisplayKeys = (data) => {
1646
+ `}`, filterScalars = (data) => Object.fromEntries(Object.entries(data).filter(([k, v]) => typeof v != "object" && !HIDDEN_FIELDS.SCALAR.includes(k))), filterDisplayKeys = (data) => {
1531
1647
  let { slug: _slug, ...rest } = data;
1532
1648
  return rest;
1533
- }, BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"], formatBytes = (bytes2) => {
1534
- if (!Number.isFinite(bytes2) || bytes2 <= 0) return "0 B";
1535
- let exp = Math.min(Math.floor(Math.log2(bytes2) / 10), BYTE_UNITS.length - 1), value = bytes2 / 2 ** (10 * exp);
1536
- return `${exp === 0 ? value : value.toFixed(1)} ${BYTE_UNITS[exp]}`;
1649
+ }, BYTE_UNITS2 = ["B", "KiB", "MiB", "GiB", "TiB"], formatBytes = (bytes) => {
1650
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
1651
+ let exp = Math.min(Math.floor(Math.log2(bytes) / 10), BYTE_UNITS2.length - 1), value = bytes / 2 ** (10 * exp);
1652
+ return `${exp === 0 ? value : value.toFixed(1)} ${BYTE_UNITS2[exp]}`;
1537
1653
  }, quotaLine = (used, max) => `${used} / ${max ?? "unlimited"}`, storeLine = (used, max) => max === void 0 ? formatBytes(used) : `${formatBytes(used)} / ${max === null ? "unlimited" : formatBytes(max)}`, renderWhoami = (data, st = PLAIN) => {
1538
1654
  if (!data) return "";
1539
1655
  let { limits, usage, tier, policy, bound_by, status_reason, policy_scope, enforced, unit_weights, capacity, ...identity } = data, identityRows = { ...identity };
@@ -1541,20 +1657,16 @@ ${heading(st, title, success)}
1541
1657
  let out2 = layout(sectionHeader(st, TITLES.TENANT_PROFILE), renderFields(st, filterDisplayKeys(identityRows)));
1542
1658
  return (limits || usage) && (out2 += renderUsageBlocks(st, limits, usage)), out2 += capacityBlock(st, capacity), hasUnits(limits) && unit_weights && (out2 += renderUnitWeights(st, unit_weights)), policy && (out2 += renderPolicyBlock(st, policy, policy_scope?.per_brain ?? [])), enforced && enforced.length > 0 && (out2 += renderEnforcedBlock(st, enforced)), out2;
1543
1659
  }, hasUnits = (limits) => typeof limits?.max_units == "number", renderUnitWeights = (st, weights) => layout(sectionHeader(st, TITLES.UNIT_WEIGHTS), renderFields(st, Object.fromEntries(Object.entries(weights).map(([p, w]) => [p, String(w)])))), showEnforced = (v) => v === null ? "\u2014" : String(v), renderEnforcedBlock = (st, rows) => {
1544
- let out2 = {};
1545
- for (let r of rows) {
1660
+ let enforcedText = (r) => {
1546
1661
  let value = showEnforced(r.enforced);
1547
- out2[r.column] = r.winner === "host-clamp" ? `${value} (platform cap; tier asked ${showEnforced(r.tier)})` : r.winner === "host-default" ? `${value} (platform default)` : value;
1548
- }
1662
+ return r.winner === "host-clamp" ? `${value} (platform cap; tier asked ${showEnforced(r.tier)})` : r.winner === "host-default" ? `${value} (platform default)` : value;
1663
+ }, out2 = Object.fromEntries(rows.map((r) => [r.column, enforcedText(r)]));
1549
1664
  return layout(sectionHeader(st, TITLES.ENFORCED), renderFields(st, out2));
1550
1665
  }, renderPolicyValue = (key, v) => key === "allowed_presets" ? Array.isArray(v) ? v.join(", ") : "all" : v == null ? "unlimited" : typeof v == "boolean" ? v ? "yes" : "no" : String(v), renderPolicyBlock = (st, policy, perBrain = []) => {
1551
- let scoped = new Set(perBrain), rows = {};
1552
- for (let [k, v] of Object.entries(policy)) {
1553
- if (k === "tier_id" || k === "tier_name" || k === "tier_version") continue;
1666
+ let scoped = new Set(perBrain), policyText = (k, v) => {
1554
1667
  let value = POLICY_UNIT_NOTES[k] ? `${renderPolicyValue(k, v)} ${POLICY_UNIT_NOTES[k]}` : renderPolicyValue(k, v);
1555
- rows[k] = scoped.has(k) ? `${value} ${PER_BRAIN_MARKER}` : value;
1556
- }
1557
- let block = layout(sectionHeader(st, TITLES.ENTITLEMENTS), renderFields(st, rows));
1668
+ return scoped.has(k) ? `${value} ${PER_BRAIN_MARKER}` : value;
1669
+ }, rows = Object.fromEntries(Object.entries(policy).filter(([k]) => k !== "tier_id" && k !== "tier_name" && k !== "tier_version").map(([k, v]) => [k, policyText(k, v)])), block = layout(sectionHeader(st, TITLES.ENTITLEMENTS), renderFields(st, rows));
1558
1670
  return scoped.size === 0 ? block : `${block}
1559
1671
  ${st.dim(PER_BRAIN_FOOTNOTE)}
1560
1672
  `;
@@ -1581,12 +1693,13 @@ ${bullets(st, [LABELS.PLATFORM_BUILT_IMAGE])}
1581
1693
  return layout(sectionHeader(st, TITLES.RUN_SUBMITTED, true), renderFields(st, filterDisplayKeys(rest))) + (noticeText ? `
1582
1694
  ${st.yellow(noticeText)}
1583
1695
  ` : "") + capacityBlock(st, capacity);
1584
- }, renderRunNotices = (notices) => Array.isArray(notices) ? notices.map((n) => n.code === "storage-clamped" ? STORAGE_MESSAGES.clamped(n.requested, n.preset, n.effective) : "").filter((line) => line !== "").join(`
1585
- `) : "", renderCapacity = (capacity, st = PLAIN) => {
1586
- let c = capacity;
1587
- if (!c || !Array.isArray(c.limits) || c.limits.length === 0) return "";
1588
- let lines = c.limits.map((l) => `Monthly ${l.text}`), since = typeof c.period_start == "string" ? `Since ${c.period_start} \xB7 ` : "";
1589
- return lines.push(st.dim(`${since}Resets ${c.resets_at ?? `at ${RESETS_AT_NEXT_RENEWAL}`} \xB7 details: ${c.usage_command ?? USAGE_DETAIL_COMMAND}`)), lines.join(`
1696
+ }, renderRunNotices = (notices) => Array.isArray(notices) ? notices.filter(isStorageClamped).map((n) => STORAGE_MESSAGES.clamped(n.requested, n.preset, n.effective)).join(`
1697
+ `) : "", isStorageClamped = (n) => isRecord(n) && n.code === "storage-clamped" && typeof n.preset == "string" && typeof n.requested == "number" && typeof n.effective == "number", renderCapacity = (capacity, st = PLAIN) => {
1698
+ if (!isRecord(capacity) || !Array.isArray(capacity.limits)) return "";
1699
+ let texts = capacity.limits.flatMap((l) => isRecord(l) && typeof l.text == "string" ? [l.text] : []);
1700
+ if (texts.length === 0) return "";
1701
+ let { period_start: periodStart, resets_at: resetsAt, usage_command: usageCommand } = capacity, since = typeof periodStart == "string" ? `Since ${periodStart} \xB7 ` : "", resets = typeof resetsAt == "string" ? resetsAt : `at ${RESETS_AT_NEXT_RENEWAL}`, details = typeof usageCommand == "string" ? usageCommand : USAGE_DETAIL_COMMAND;
1702
+ return [...texts.map((t) => `Monthly ${t}`), st.dim(`${since}Resets ${resets} \xB7 details: ${details}`)].join(`
1590
1703
  `);
1591
1704
  }, capacityBlock = (st, capacity) => {
1592
1705
  let text = renderCapacity(capacity, st);
@@ -1608,7 +1721,7 @@ ${emptyState(st, empty.message, empty.next)}
1608
1721
  return `
1609
1722
  ${renderTable(st, columns, rows)}
1610
1723
  `;
1611
- }, imageSize = (bytes2, present) => present === false ? "(missing)" : bytes2 === null ? "unknown" : formatBytes(Number(bytes2)), renderDeploymentList = (data, st = PLAIN) => {
1724
+ }, imageSize = (bytes, present) => present === false ? "(missing)" : bytes === null ? "unknown" : formatBytes(Number(bytes)), renderDeploymentList = (data, st = PLAIN) => {
1612
1725
  let rows = data.deployments ?? [], total = data.tenant_total_bytes, limit = data.tenant_limit_bytes, display = rows.map(({ image_bytes, image_present, ...rest }) => ({ ...rest, ...image_bytes !== void 0 ? { size: imageSize(image_bytes, image_present) } : {} })), out2 = renderList(display, st, EMPTY_LISTS.DEPLOYMENTS);
1613
1726
  if (total !== void 0 && rows.length > 0) {
1614
1727
  let of = typeof limit == "number" ? ` of ${formatBytes(limit)}` : "";
@@ -1617,10 +1730,10 @@ ${renderTable(st, columns, rows)}
1617
1730
  }
1618
1731
  return out2;
1619
1732
  }, packagesLine = (v) => {
1620
- if (typeof v != "object" || v === null) return;
1733
+ if (!isRecord(v)) return;
1621
1734
  let { names, drive } = v;
1622
1735
  if (!Array.isArray(names) || names.length === 0) return;
1623
- let d = drive, size = d != null && typeof d.bytes == "number" ? `drive ${formatBytes(d.bytes)} (snapshot ${String(d.snapshot)})` : "drive not built yet";
1736
+ let size = isRecord(drive) && typeof drive.bytes == "number" ? `drive ${formatBytes(drive.bytes)} (snapshot ${String(drive.snapshot)})` : "drive not built yet";
1624
1737
  return `${names.join(", ")} \xB7 ${size}`;
1625
1738
  }, renderDeploymentGet = (data, st = PLAIN) => {
1626
1739
  if (!data) return "";
@@ -1647,10 +1760,11 @@ ${st.dim("Command template for next step:")} ${st.bold(LABELS.NEXT_STEP)} ${temp
1647
1760
  ${st.dim("Full command:")} ${st.cyan(actual)}
1648
1761
  `;
1649
1762
  } });
1650
- var str, slug, WORKFLOW, isRecord, resolveNextStep, init_next_step = __esm({ "src/output/next-step.ts"() {
1763
+ var str, slug, WORKFLOW, resolveNextStep, init_next_step = __esm({ "src/output/next-step.ts"() {
1651
1764
  "use strict";
1652
1765
  init_define_IGNITIFY_CLIENT_FILES();
1653
- str = (v) => typeof v == "string" && v !== "" ? v : void 0, slug = (data) => str(data.slug) ?? str(data.task_id) ?? "<task_id>", WORKFLOW = { "task:create": { template: "ignitify task:deploy <task_id>", resolve: (d) => `ignitify task:deploy ${slug(d)}` }, "task:pack": { template: "ignitify task:deploy <task_id>", resolve: (d) => `ignitify task:deploy ${slug(d)}` }, "task:deploy": { template: "ignitify task:run <task_id>", resolve: (d) => `ignitify task:run ${slug(d)}` }, "task:run": { template: "ignitify run:get <run_id>", resolve: (d) => `ignitify run:get ${str(d.resource_id) ?? str(d.run_id) ?? "<run_id>"}` } }, isRecord = (v) => typeof v == "object" && v !== null && !Array.isArray(v), resolveNextStep = (command, data) => {
1766
+ init_tenant();
1767
+ str = (v) => typeof v == "string" && v !== "" ? v : void 0, slug = (data) => str(data.slug) ?? str(data.task_id) ?? "<task_id>", WORKFLOW = { "task:create": { template: "ignitify task:deploy <task_id>", resolve: (d) => `ignitify task:deploy ${slug(d)}` }, "task:pack": { template: "ignitify task:deploy <task_id>", resolve: (d) => `ignitify task:deploy ${slug(d)}` }, "task:deploy": { template: "ignitify task:run <task_id>", resolve: (d) => `ignitify task:run ${slug(d)}` }, "task:run": { template: "ignitify run:get <run_id>", resolve: (d) => `ignitify run:get ${str(d.resource_id) ?? str(d.run_id) ?? "<run_id>"}` } }, resolveNextStep = (command, data) => {
1654
1768
  let step = WORKFLOW[command];
1655
1769
  return step ? { template: step.template, actual: step.resolve(isRecord(data) ? data : {}) } : null;
1656
1770
  };
@@ -1696,8 +1810,8 @@ var RENDERERS, jsonMode, sayStream, outStyle, errStyle, writeRaw, out, sayOk, wa
1696
1810
  RENDERERS = { whoami: renderWhoami, quota: renderUsage, "task:create": renderTaskCreate, "task:pack": renderPack, "task:deploy": renderDeploy, "task:run": renderRunSubmit, "run:get": renderRunGet, "task:list": (d, st) => renderList(d, st, EMPTY_LISTS.TASKS), "deployment:list": renderDeploymentList, "deployment:get": renderDeploymentGet, "run:list": (d, st) => renderList(d, st, EMPTY_LISTS.RUNS), "vm:list": renderVmList, "vm:get": renderVmGet, "schedule:list": (d, st) => renderList(d, st, EMPTY_LISTS.SCHEDULES) }, jsonMode = false;
1697
1811
  sayStream = () => jsonMode ? process.stderr : process.stdout;
1698
1812
  outStyle = () => streamStyle(process.stdout), errStyle = () => streamStyle(process.stderr);
1699
- writeRaw = (bytes2) => {
1700
- process.stdout.write(bytes2);
1813
+ writeRaw = (bytes) => {
1814
+ process.stdout.write(bytes);
1701
1815
  }, out = (text) => console.log(text), sayOk = (text) => say(successLine(screenStyle(), text));
1702
1816
  warn = (text) => console.error(noteLine(errStyle(), text)), tell = (text) => console.error(text), printWarnings = (ws) => ws.forEach((w) => console.error(warnMark(errStyle(), w))), printErrors = (es) => es.forEach((e) => console.error(errorMark(errStyle(), e)));
1703
1817
  CliOutputManager = { render(command, data) {
@@ -1936,66 +2050,38 @@ var FATE_TEXT, printDeletionPlan, init_deletion_plan = __esm({ "src/output/delet
1936
2050
  FATE_TEXT = { "cancelled-via-vm-teardown": "its VM is destroyed and the run is recorded as CANCELLED", "deleted-while-pending": "never dispatched \u2014 deleted before it could start", "unknown-status": "unrecognised status; this CLI cannot say what happens to it" };
1937
2051
  printDeletionPlan = (plan) => say(renderDeletionPlan(screenStyle(), plan));
1938
2052
  } });
1939
- function explainRunRefusal(message) {
1940
- let body = apiErrorBody(message)?.body, pinned = body?.reason === "bidirectional-busy" ? body.pinned === true : message.includes("bidirectional deployment runs one VM at a time") ? message.includes("PINNED") : null;
2053
+ function explainRunRefusal(e) {
2054
+ let body = httpErrorJson(e)?.body, message = errText(e), pinned = body?.reason === "bidirectional-busy" ? body.pinned === true : message.includes("bidirectional deployment runs one VM at a time") ? message.includes("PINNED") : null;
1941
2055
  if (pinned === null) return null;
1942
2056
  let lines = ["\u2717 Run refused \u2014 this bidirectional deployment already has a live VM.", "", " Concurrent VMs for bidirectional-network tasks are not supported", " (one VM at a time)."];
1943
2057
  if (pinned) {
1944
- let vmId = typeof body?.vm_id == "string" && body.vm_id !== "" ? body.vm_id : "<vm-id>";
2058
+ let vmField = body?.vm_id, vmId = typeof vmField == "string" && vmField !== "" ? vmField : "<vm-id>";
1945
2059
  lines.push("", " Why it was not queued, even if you asked for --on-busy=queue:", " A PINNED VM has no idle timeout \u2014 it stays up until it is explicitly", " removed, so queueing behind it could wait forever.", "", " What you can do now:", " \u2022 Reuse the pinned VM \u2014 submit the run against it instead", ' \u2022 Use network.type "egress" if this task needs no inbound traffic', ` \u2022 Remove it yourself: ignitify vm:delete ${vmId} --force`);
1946
2060
  } else lines.push("", " Retry when the live VM finishes, or omit --on-busy=reject to queue instead.");
1947
2061
  return lines.join(`
1948
2062
  `);
1949
2063
  }
1950
- function explainLimitRefusal(message) {
1951
- let refused = apiErrorBody(message);
1952
- if (!refused || refused.status !== 429) return null;
1953
- let body = refused.body;
1954
- return typeof body.summary != "string" || body.summary === "" ? null : [`\u2717 Run refused \u2014 a monthly limit is reached (${body.reason ?? "429"}).`, ` ${body.summary}`].join(`
2064
+ function explainLimitRefusal(e) {
2065
+ let refused2 = httpErrorJson(e);
2066
+ if (!refused2 || refused2.status !== 429) return null;
2067
+ let { summary, reason } = refused2.body;
2068
+ return typeof summary != "string" || summary === "" ? null : [`\u2717 Run refused \u2014 a monthly limit is reached (${typeof reason == "string" ? reason : "429"}).`, ` ${summary}`].join(`
1955
2069
  `);
1956
2070
  }
1957
- function apiErrorBody(message) {
1958
- let m = /^API (\d{3}): (\{[\s\S]*\})$/.exec(message.trim());
1959
- if (!m) return null;
1960
- try {
1961
- let body = JSON.parse(m[2]);
1962
- return typeof body != "object" || body === null || Array.isArray(body) ? null : { status: Number(m[1]), body };
1963
- } catch {
1964
- return null;
1965
- }
1966
- }
1967
2071
  var isPayloadTooLarge, init_run_refusal = __esm({ "src/output/run-refusal.ts"() {
1968
2072
  "use strict";
1969
2073
  init_define_IGNITIFY_CLIENT_FILES();
1970
- isPayloadTooLarge = (message) => apiErrorBody(message)?.status === 413;
2074
+ init_http_client();
2075
+ init_tenant();
2076
+ isPayloadTooLarge = (e) => e instanceof HttpError && e.status === 413;
1971
2077
  } });
1972
- function quantity(n, unit) {
1973
- return unit === "bytes" ? bytes(n) : unit === "ms" ? duration(n) : unit === "byte-hours" ? byteHours(n) : `${n}`;
1974
- }
1975
- function byteHours(n) {
1976
- let u = ["B\xB7h", "KiB\xB7h", "MiB\xB7h", "GiB\xB7h", "TiB\xB7h", "PiB\xB7h"], i = 0, v = n;
1977
- for (; v >= 1024 && i < u.length - 1; ) v /= 1024, i += 1;
1978
- return i === 0 ? `${n} B\xB7h` : `${v.toFixed(1)} ${u[i]}`;
1979
- }
1980
- function bytes(n) {
1981
- let u = ["B", "KB", "MB", "GB", "TB"], i = 0, v = n;
1982
- for (; v >= 1024 && i < u.length - 1; ) v /= 1024, i += 1;
1983
- return i === 0 ? `${n} B` : `${v.toFixed(1)} ${u[i]}`;
1984
- }
1985
- function duration(ms) {
1986
- if (ms < 1e3) return `${ms} ms`;
1987
- let s = Math.round(ms / 1e3);
1988
- if (s < 60) return `${s} s`;
1989
- let m = Math.floor(s / 60);
1990
- return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`;
1991
- }
1992
2078
  function renderUsageReport(st, r, opts = { lines: false }) {
1993
2079
  let I2 = LAYOUT.INDENT, out2 = [`${heading(st, `usage for ${r.period}`)}${r.filtered ? st.dim(" (filtered)") : ""}`, ""];
1994
- return out2.push(renderTable(st, [{ header: "AXIS", keep: true }, { header: "QUANTITY", keep: true }, { header: "LINES", keep: true }, { header: "RECONCILIATION" }], r.totals.map((t) => [t.axis, quantity(t.quantity, t.unit), t.lines, reconcileNote(t)]), { indent: I2 })), r.totals.some((t) => t.axis === "disk" || t.axis === "scratch") && (out2.push("", st.dim(`${I2}note: \`disk\` = your build images \xD7 the hours each was held this month (still-held images included);`)), out2.push(st.dim(`${I2} \`scratch\` = each VM's scratch drive \xD7 the hours the VM ran. 1 GiB\xB7h = 1 GiB for one hour.`))), r.totals.some((t) => t.axis === "compute") && out2.push("", st.dim(`${I2}note: \`compute\` = VM time (boot to teardown). Your monthly compute limit counts run seconds instead.`)), out2.push("", `${I2}runs started ${r.counters.runs_started} deployments created ${r.counters.deployments_created} API requests ${r.counters.api_requests_approx} (approx)`), opts.lines && out2.push("", renderTable(st, [{ header: "AXIS", keep: true }, { header: "QUANTITY", keep: true }, { header: "VM / RUN", keep: true }, { header: "DETAIL" }], r.lines.map((l) => [l.axis, quantity(l.quantity, l.unit), l.run_id ?? l.vm_id ?? l.deployment_id ?? st.glyphs.dash, l.detail ?? ""]), { indent: I2 })), out2.join(`
2080
+ return out2.push(renderTable(st, [{ header: "AXIS", keep: true }, { header: "QUANTITY", keep: true }, { header: "LINES", keep: true }, { header: "RECONCILIATION" }], r.totals.map((t) => [t.axis, formatUsageQuantity(t.quantity, t.unit), t.lines, reconcileNote(t)]), { indent: I2 })), r.totals.some((t) => t.axis === "disk" || t.axis === "scratch") && (out2.push("", st.dim(`${I2}note: \`disk\` = your build images \xD7 the hours each was held this month (still-held images included);`)), out2.push(st.dim(`${I2} \`scratch\` = each VM's scratch drive \xD7 the hours the VM ran. 1 GiB\xB7h = 1 GiB for one hour.`))), r.totals.some((t) => t.axis === "compute") && out2.push("", st.dim(`${I2}note: \`compute\` = VM time (boot to teardown). Your monthly compute limit counts run seconds instead.`)), out2.push("", `${I2}runs started ${r.counters.runs_started} deployments created ${r.counters.deployments_created} API requests ${r.counters.api_requests_approx} (approx)`), opts.lines && out2.push("", renderTable(st, [{ header: "AXIS", keep: true }, { header: "QUANTITY", keep: true }, { header: "VM / RUN", keep: true }, { header: "DETAIL" }], r.lines.map((l) => [l.axis, formatUsageQuantity(l.quantity, l.unit), l.run_id ?? l.vm_id ?? l.deployment_id ?? st.glyphs.dash, l.detail ?? ""]), { indent: I2 })), out2.join(`
1995
2081
  `);
1996
2082
  }
1997
2083
  function reconcileNote(t) {
1998
- return !t.reconcilable || t.counter === null || t.drift === null ? t.reconciliation === "none" ? "\u2014" : "not checked (filtered)" : t.drift === 0 ? "agrees with the meter" : t.reconciliation === "inequality" && t.drift > 0 ? `${t.drift} still in flight` : `meter differs by ${quantity(Math.abs(t.drift), t.unit)}`;
2084
+ return !t.reconcilable || t.counter === null || t.drift === null ? t.reconciliation === "none" ? "\u2014" : "not checked (filtered)" : t.drift === 0 ? "agrees with the meter" : t.reconciliation === "inequality" && t.drift > 0 ? `${t.drift} still in flight` : `meter differs by ${formatUsageQuantity(Math.abs(t.drift), t.unit)}`;
1999
2085
  }
2000
2086
  function toCsv(r) {
2001
2087
  let head = "axis,vm_id,run_id,deployment_id,window_start,window_end,quantity,unit,detail", rows = r.lines.map((l) => csvRow([l.axis, l.vm_id ?? "", l.run_id ?? "", l.deployment_id ?? "", l.window_start ?? "", l.window_end ?? "", String(l.quantity), l.unit, l.detail ?? ""]));
@@ -2034,50 +2120,27 @@ function planAdvice(config) {
2034
2120
  let checked = checkSystemPackages(declared);
2035
2121
  return checked.ok ? { kind: "call", request: { names: checked.names, runtime: config.runtime } } : { kind: "skip", reason: checked.reason === "bad-names" ? ADVICE.BAD_NAMES(checked.bad) : ADVICE.TOO_MANY(checked.count, 64) };
2036
2122
  }
2037
- function jsonBody(body) {
2038
- try {
2039
- let v = JSON.parse(body);
2040
- return typeof v == "object" && v !== null ? v : {};
2041
- } catch {
2042
- return {};
2043
- }
2044
- }
2045
- function adviceFailure(status, body) {
2046
- let parsed = jsonBody(body);
2123
+ function adviceFailure(e) {
2124
+ let { status, body } = e, parsed = httpErrorJson(e)?.body ?? {};
2047
2125
  return status === 429 && typeof parsed.retry_after_s == "number" ? { kind: "rate-limited", retryAfterS: parsed.retry_after_s } : status === 503 && typeof parsed.reason == "string" ? { kind: "unavailable", reason: parsed.reason } : { kind: "error", status, message: body };
2048
2126
  }
2049
2127
  var init_package_advice3 = __esm({ "src/core/package-advice.ts"() {
2050
2128
  "use strict";
2051
2129
  init_define_IGNITIFY_CLIENT_FILES();
2130
+ init_http_client();
2052
2131
  init_tenant();
2053
2132
  init_constants();
2054
2133
  } });
2055
2134
  var version, init_package = __esm({ "package.json"() {
2056
- version = "1.2.0";
2135
+ version = "1.2.2";
2057
2136
  } });
2058
2137
  var byCreatedAtDesc, init_sort = __esm({ "src/utils/sort.ts"() {
2059
2138
  "use strict";
2060
2139
  init_define_IGNITIFY_CLIENT_FILES();
2061
2140
  byCreatedAtDesc = (rows) => [...rows].sort((a, b) => (b.created_at ?? "").localeCompare(a.created_at ?? ""));
2062
2141
  } });
2063
- var normalizeContentHash, init_hash = __esm({ "src/utils/hash.ts"() {
2064
- "use strict";
2065
- init_define_IGNITIFY_CLIENT_FILES();
2066
- normalizeContentHash = (hash) => hash.replace(/^sha256:/, "");
2067
- } });
2068
- function strOpt(args, key) {
2069
- let bare = bareValueFlag(args, [key]);
2070
- if (bare !== void 0) throw new Error(needsValueMessage(bare));
2071
- let v = args.options[key];
2072
- return typeof v == "string" ? v : void 0;
2073
- }
2074
- function flagParams(args, map2) {
2075
- let params = {};
2076
- for (let [flag, param] of Object.entries(map2)) {
2077
- let v = strOpt(args, flag);
2078
- v !== void 0 && (params[param] = v);
2079
- }
2080
- return params;
2142
+ function apiUrlOf(args) {
2143
+ return strOpt(args, "api") ?? DEFAULTS.API_URL;
2081
2144
  }
2082
2145
  function boolFlag(args, key) {
2083
2146
  return args.options[key] === true;
@@ -2092,33 +2155,23 @@ function requireArg(args, label) {
2092
2155
  var init_args2 = __esm({ "src/utils/args.ts"() {
2093
2156
  "use strict";
2094
2157
  init_define_IGNITIFY_CLIENT_FILES();
2095
- init_http_client();
2096
2158
  init_constants();
2097
2159
  init_output();
2160
+ init_http_client();
2161
+ init_http_client();
2098
2162
  } });
2099
- function errorMessage(e) {
2100
- return e instanceof Error ? e.message : String(e);
2101
- }
2102
2163
  function traceLine(e) {
2103
2164
  return e instanceof HttpError && e.traceId ? MESSAGES.SERVER_TRACE_ID(e.traceId) : null;
2104
2165
  }
2105
2166
  function keyExpiredHint(e) {
2106
- if (!(e instanceof HttpError) || e.status !== 401) return null;
2107
- try {
2108
- return JSON.parse(e.body)?.error === KEY_EXPIRED_ERROR ? MESSAGES.KEY_EXPIRED_HINT : null;
2109
- } catch {
2110
- return null;
2111
- }
2167
+ let refused2 = httpErrorJson(e);
2168
+ return refused2?.status === 401 && refused2.body.error === KEY_EXPIRED_ERROR ? MESSAGES.KEY_EXPIRED_HINT : null;
2112
2169
  }
2113
2170
  function printError(e) {
2114
- fail(errorMessage(e), { hint: keyExpiredHint(e), trace: traceLine(e) });
2171
+ fail(errText(e), { hint: keyExpiredHint(e), trace: traceLine(e) });
2115
2172
  }
2116
2173
  async function guard(body) {
2117
- try {
2118
- return await body();
2119
- } catch (e) {
2120
- return printError(e), 1;
2121
- }
2174
+ return await guardValue(body) ?? 1;
2122
2175
  }
2123
2176
  async function guardValue(body) {
2124
2177
  try {
@@ -2194,7 +2247,6 @@ var init_utils = __esm({ "src/utils/index.ts"() {
2194
2247
  "use strict";
2195
2248
  init_define_IGNITIFY_CLIENT_FILES();
2196
2249
  init_sort();
2197
- init_hash();
2198
2250
  init_args2();
2199
2251
  init_guard();
2200
2252
  init_prompt_flow();
@@ -2229,7 +2281,7 @@ async function describeServer(baseUrl, fetchImpl = fetch, check = noteServerVers
2229
2281
  return `API: ${api} at ${baseUrl}${floor ? ` (minimum client ${floor})` : ""}`;
2230
2282
  }
2231
2283
  async function runVersion(args, fetchImpl = fetch) {
2232
- let baseUrl = strOpt(args, "api") ?? DEFAULTS.API_URL, st = outStyle();
2284
+ let baseUrl = apiUrlOf(args), st = outStyle();
2233
2285
  return out(`${st.bold(`ignitify ${CLIENT_VERSION}`)} ${st.dim(`(built for API ${API_VERSION})`)}`), out(st.dim(await describeServer(baseUrl, fetchImpl))), 0;
2234
2286
  }
2235
2287
  var CLIENT_VERSION, PROBE_TIMEOUT_MS, noteServerVersion, init_version = __esm({ "src/io/version.ts"() {
@@ -2249,7 +2301,7 @@ function openNoFollow(file, fsx = import_node_fs.default) {
2249
2301
  try {
2250
2302
  fd = fsx.openSync(file, NO_FOLLOW_READ_FLAGS);
2251
2303
  } catch (e) {
2252
- let code = e?.code;
2304
+ let code = errCode(e);
2253
2305
  if (code === "ENOENT") return { kind: "missing" };
2254
2306
  if (code === "ELOOP") return { kind: "symlink" };
2255
2307
  throw e;
@@ -2263,7 +2315,9 @@ function openNoFollow(file, fsx = import_node_fs.default) {
2263
2315
  var import_node_fs, NO_FOLLOW_READ_FLAGS, init_no_follow = __esm({ "src/io/no-follow.ts"() {
2264
2316
  "use strict";
2265
2317
  init_define_IGNITIFY_CLIENT_FILES();
2266
- import_node_fs = __toESM(require("node:fs"), 1), NO_FOLLOW_READ_FLAGS = import_node_fs.default.constants.O_RDONLY | (import_node_fs.default.constants.O_NOFOLLOW ?? 0) | (import_node_fs.default.constants.O_NONBLOCK ?? 0);
2318
+ import_node_fs = __toESM(require("node:fs"), 1);
2319
+ init_tenant();
2320
+ NO_FOLLOW_READ_FLAGS = import_node_fs.default.constants.O_RDONLY | (import_node_fs.default.constants.O_NOFOLLOW ?? 0) | (import_node_fs.default.constants.O_NONBLOCK ?? 0);
2267
2321
  } });
2268
2322
  function credentialsPath(env = process.env, home = import_node_os.default.homedir()) {
2269
2323
  let xdg = env.XDG_CONFIG_HOME, base = xdg && import_node_path.default.isAbsolute(xdg) ? xdg : import_node_path.default.join(home, ".config");
@@ -2288,10 +2342,9 @@ function parseCredentials(text) {
2288
2342
  } catch {
2289
2343
  return null;
2290
2344
  }
2291
- let d = doc;
2292
- if (!d || typeof d != "object" || d.version !== 1) return null;
2293
- for (let k of ["api_url", "api_key", "key_id", "label", "saved_at"]) if (typeof d[k] != "string" || d[k] === "") return null;
2294
- return d;
2345
+ if (!isRecord(doc) || doc.version !== 1) return null;
2346
+ let { api_url, api_key, key_id, label, saved_at } = doc;
2347
+ return !isFilled(api_url) || !isFilled(api_key) || !isFilled(key_id) || !isFilled(label) || !isFilled(saved_at) ? null : { version: 1, api_url, api_key, key_id, label, saved_at };
2295
2348
  }
2296
2349
  function readCredentials(file = credentialsPath(), fsx = import_node_fs2.default) {
2297
2350
  let opened = openNoFollow(file, fsx);
@@ -2331,13 +2384,15 @@ function deleteCredentials(file = credentialsPath(), fsx = import_node_fs2.defau
2331
2384
  throw e;
2332
2385
  }
2333
2386
  }
2334
- var import_node_fs2, import_node_os, import_node_path, import_node_crypto, myUid, isMissing, init_credentials = __esm({ "src/io/credentials.ts"() {
2387
+ var import_node_fs2, import_node_os, import_node_path, import_node_crypto, isFilled, myUid, init_credentials = __esm({ "src/io/credentials.ts"() {
2335
2388
  "use strict";
2336
2389
  init_define_IGNITIFY_CLIENT_FILES();
2337
2390
  import_node_fs2 = __toESM(require("node:fs"), 1), import_node_os = __toESM(require("node:os"), 1), import_node_path = __toESM(require("node:path"), 1), import_node_crypto = require("node:crypto");
2391
+ init_tenant();
2338
2392
  init_constants();
2339
2393
  init_no_follow();
2340
- myUid = () => typeof process.getuid == "function" ? process.getuid() : null, isMissing = (e) => e?.code === "ENOENT";
2394
+ isFilled = (v) => typeof v == "string" && v !== "";
2395
+ myUid = () => typeof process.getuid == "function" ? process.getuid() : null;
2341
2396
  } });
2342
2397
  function codeOf(cause) {
2343
2398
  let code = cause?.code;
@@ -2385,17 +2440,15 @@ var UnreachableError, init_unreachable = __esm({ "src/core/unreachable.ts"() {
2385
2440
  }
2386
2441
  };
2387
2442
  } });
2388
- function isLoopbackHost(hostname) {
2389
- return hostname === "localhost" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
2390
- }
2391
2443
  function apiUrlRefusal(url) {
2392
- let u;
2393
- try {
2394
- u = new URL(url);
2395
- } catch {
2396
- return MESSAGES.API_URL_NOT_URL(url);
2444
+ switch (keyUrlRefusal(url)) {
2445
+ case "not-url":
2446
+ return MESSAGES.API_URL_NOT_URL(url);
2447
+ case "insecure":
2448
+ return MESSAGES.API_URL_NOT_HTTPS(url);
2449
+ case null:
2450
+ return null;
2397
2451
  }
2398
- return u.protocol === "https:" || u.protocol === "http:" && isLoopbackHost(u.hostname) ? null : MESSAGES.API_URL_NOT_HTTPS(url);
2399
2452
  }
2400
2453
  function assertApiUrl(url) {
2401
2454
  let refusal = apiUrlRefusal(url);
@@ -2405,6 +2458,7 @@ function assertApiUrl(url) {
2405
2458
  var init_api_url = __esm({ "src/core/api-url.ts"() {
2406
2459
  "use strict";
2407
2460
  init_define_IGNITIFY_CLIENT_FILES();
2461
+ init_http_client();
2408
2462
  init_constants();
2409
2463
  } });
2410
2464
  function resolveApiConfig(apiOverride, env = process.env, read = () => readCredentials(), note = noteEnvOverride) {
@@ -2456,7 +2510,7 @@ async function pollDeviceLogin(cfg, deviceCode, fetchImpl) {
2456
2510
  try {
2457
2511
  body = await request(`${cfg.baseUrl}${TENANT_PATHS.AUTH_DEVICE_TOKEN}`, { method: "POST", headers: { "content-type": "application/json", ...versionHeader }, body: { device_code: deviceCode }, fetchImpl: viaApi(fetchImpl), ...readHeaders });
2458
2512
  } catch (e) {
2459
- let wait = e instanceof HttpError && e.status === 429 ? slowDownOf(e.body) : null;
2513
+ let wait = slowDownOf(e);
2460
2514
  if (wait === null) throw e;
2461
2515
  return { kind: "wait", interval: wait };
2462
2516
  }
@@ -2466,13 +2520,11 @@ async function pollDeviceLogin(cfg, deviceCode, fetchImpl) {
2466
2520
  if (!key || typeof key.api_key != "string" || key.api_key === "" || typeof key.key_id != "string" || typeof key.label != "string" || typeof key.tenant?.resource_id != "string") throw new BadAnswerError(MESSAGES.LOGIN_BAD_ANSWER);
2467
2521
  return { kind: "key", key };
2468
2522
  }
2469
- function slowDownOf(text) {
2470
- try {
2471
- let b = JSON.parse(text);
2472
- return b.status === "slow_down" && typeof b.interval == "number" ? b.interval : null;
2473
- } catch {
2474
- return null;
2475
- }
2523
+ function slowDownOf(e) {
2524
+ let refused2 = httpErrorJson(e);
2525
+ if (refused2?.status !== 429 || refused2.body.status !== "slow_down") return null;
2526
+ let interval = refused2.body.interval;
2527
+ return typeof interval == "number" ? interval : null;
2476
2528
  }
2477
2529
  function revokeOwnKey(cfg, fetchImpl) {
2478
2530
  return apiDelete(cfg, TENANT_PATHS.AUTH_KEY, fetchImpl);
@@ -2484,8 +2536,9 @@ async function postDeployment(cfg, taskId, manifest, tar, fetchImpl) {
2484
2536
  let form = new FormData();
2485
2537
  return form.set("task_id", taskId), form.set("manifest", JSON.stringify(manifest)), form.set("bundle", new Blob([new Uint8Array(tar)], { type: "application/x-tar" }), "bundle.tar"), await request(`${cfg.baseUrl}${TENANT_PATHS.DEPLOYMENTS}`, { method: "POST", headers: authHeaders(cfg), body: form, fetchImpl: viaApi(fetchImpl), timeoutMs: 3e5, ...readHeaders });
2486
2538
  }
2487
- async function postRun(cfg, run2, fetchImpl) {
2488
- return await request(`${cfg.baseUrl}${TENANT_PATHS.RUNS}`, { method: "POST", headers: { ...authHeaders(cfg), "content-type": "application/json" }, body: { deployment_id: run2.deploymentId, task_name: run2.taskName, payload: run2.payload, preset: run2.preset, timeout_sec: run2.timeoutSec, execution: run2.execution, at: run2.at }, fetchImpl: viaApi(fetchImpl), ...readHeaders });
2539
+ function postRun(cfg, run2, fetchImpl) {
2540
+ let body = { deployment_id: run2.deploymentId, task_name: run2.taskName, payload: run2.payload, preset: run2.preset, timeout_sec: run2.timeoutSec, execution: run2.execution, at: run2.at };
2541
+ return apiPost(cfg, TENANT_PATHS.RUNS, body, fetchImpl);
2489
2542
  }
2490
2543
  async function listSchedules(cfg, fetchImpl) {
2491
2544
  return apiGet(cfg, TENANT_PATHS.SCHEDULES, fetchImpl);
@@ -2536,7 +2589,7 @@ async function postPackageAdvice(cfg, body, fetchImpl) {
2536
2589
  try {
2537
2590
  return { kind: "ok", response: await apiPost(cfg, TENANT_PATHS.PACKAGE_ADVICE, body, fetchImpl, ADVICE.TIMEOUT_MS) };
2538
2591
  } catch (e) {
2539
- return e instanceof HttpError ? adviceFailure(e.status, e.body) : { kind: "error", status: null, message: e instanceof Error ? e.message : String(e) };
2592
+ return e instanceof HttpError ? adviceFailure(e) : { kind: "error", status: null, message: errText(e) };
2540
2593
  }
2541
2594
  }
2542
2595
  async function createTask(cfg, slug2, fetchImpl) {
@@ -2621,8 +2674,8 @@ var readHeaders, noteEnvOverride, versionHeader, BadAnswerError, init_api = __es
2621
2674
  };
2622
2675
  } });
2623
2676
  function payloadRefusal(payload, limit) {
2624
- let bytes2 = payloadBytes(payload);
2625
- return bytes2 > limit ? MESSAGES.PAYLOAD_OVER_LIMIT(bytes2, limit) : null;
2677
+ let bytes = payloadBytes(payload);
2678
+ return bytes > limit ? MESSAGES.PAYLOAD_OVER_LIMIT(bytes, limit) : null;
2626
2679
  }
2627
2680
  var payloadBytes, limitOrFallback, formatLimit, limitsNote, init_payload_limit = __esm({ "src/core/payload-limit.ts"() {
2628
2681
  "use strict";
@@ -2630,7 +2683,7 @@ var payloadBytes, limitOrFallback, formatLimit, limitsNote, init_payload_limit =
2630
2683
  init_constants();
2631
2684
  init_output();
2632
2685
  payloadBytes = (payload) => Buffer.byteLength(JSON.stringify(payload) ?? "", "utf8"), limitOrFallback = (limit, fallback) => typeof limit == "number" && Number.isFinite(limit) && limit > 0 ? limit : fallback;
2633
- formatLimit = (bytes2) => formatBytes(bytes2).replace(/\.0 /, " "), limitsNote = () => MESSAGES.TASK_LIMITS(formatLimit(PAYLOAD_LIMIT_FALLBACK_BYTES), formatLimit(RESULT_LIMIT_FALLBACK_BYTES));
2686
+ formatLimit = (bytes) => formatBytes(bytes).replace(/\.0 /, " "), limitsNote = () => MESSAGES.TASK_LIMITS(formatLimit(PAYLOAD_LIMIT_FALLBACK_BYTES), formatLimit(RESULT_LIMIT_FALLBACK_BYTES));
2634
2687
  } });
2635
2688
  function apiConfig(args) {
2636
2689
  return resolveApiConfig(strOpt(args, "api"));
@@ -2639,20 +2692,27 @@ async function payloadLimit(cfg) {
2639
2692
  let whoami = await getWhoami(cfg).catch(() => null);
2640
2693
  return limitOrFallback(whoami?.limits.max_payload_bytes, PAYLOAD_LIMIT_FALLBACK_BYTES);
2641
2694
  }
2642
- function consoleLogText(bytes2, out2) {
2643
- return out2.raw ? bytes2 : terminalSafe(bytes2, out2.colour);
2695
+ function consoleLogText(bytes, out2) {
2696
+ return out2.raw ? bytes : terminalSafe(bytes, out2.colour);
2644
2697
  }
2645
2698
  function printConsoleLog(data, raw, colour = outStyle().colour) {
2646
2699
  if (writeRaw(consoleLogText(typeof data?.bytes == "string" ? data.bytes : "", { raw, colour })), data?.truncated === true) {
2647
2700
  let lines = typeof data.lines == "number" ? data.lines : 0;
2648
2701
  tell(`
2649
- ${warnMark(errStyle(), `truncated \u2014 ${lines} line(s) shown; the log was cut at the size cap.`)}`);
2702
+ ${warnMark(errStyle(), RESULT_MESSAGES.LOG_TRUNCATED(lines))}`);
2650
2703
  }
2651
2704
  }
2652
2705
  function noteNextPage(args, list2, rows) {
2653
2706
  let note = nextPageNoteFor2(args, list2, rows);
2654
2707
  note !== void 0 && warn(note);
2655
2708
  }
2709
+ async function runPagedList(args, verb) {
2710
+ let badLimit = limitRefusal(args);
2711
+ return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
2712
+ let data = await verb.fetchPage(apiConfig(args), { ...flagParams(args, verb.list.flags), ...verb.extra });
2713
+ return verb.show(data), noteNextPage(args, verb.list, verb.rowsOf(data)), 0;
2714
+ });
2715
+ }
2656
2716
  var init_shared = __esm({ "src/commands/shared.ts"() {
2657
2717
  "use strict";
2658
2718
  init_define_IGNITIFY_CLIENT_FILES();
@@ -2682,6 +2742,41 @@ var init_identity = __esm({ "src/commands/identity.ts"() {
2682
2742
  init_output();
2683
2743
  init_shared();
2684
2744
  } });
2745
+ function backoffDelayMs(attempt, cadence) {
2746
+ return cappedExponentialMs(attempt, cadence.BASE_MS, cadence.MAX_MS);
2747
+ }
2748
+ function isTransientError(e) {
2749
+ return e instanceof HttpError ? e.status === 408 || e.status === 429 || e.status >= 500 && e.status <= 599 : e instanceof TypeError ? true : e instanceof Error && isRecord(e.cause) && e.cause.name === "TimeoutError";
2750
+ }
2751
+ function decidePollError(e, failuresInARow, failingForMs) {
2752
+ return isTransientError(e) ? failuresInARow >= POLL.ERROR.MIN_TRIES && failingForMs >= POLL.ERROR.GRACE_MS ? "give-up" : "retry" : "give-up";
2753
+ }
2754
+ async function pollUntil(loop) {
2755
+ let sleep = loop.clock?.sleep ?? REAL_CLOCK.sleep, now = loop.clock?.now ?? REAL_CLOCK.now, startedAt = now(), failures = 0, firstFailureAt = 0;
2756
+ for (let attempt = 0; ; ) {
2757
+ let view;
2758
+ try {
2759
+ view = await loop.fetch();
2760
+ } catch (error) {
2761
+ if (failures === 0 && (firstFailureAt = now()), failures += 1, decidePollError(error, failures, now() - firstFailureAt) === "give-up") return { kind: "lost", error };
2762
+ loop.onRetry?.(error), await sleep(loop.retryDelayMs);
2763
+ continue;
2764
+ }
2765
+ failures = 0, loop.onView?.(view);
2766
+ let action = loop.decide(view, now() - startedAt, attempt);
2767
+ if (!isWait(action)) return { action, view };
2768
+ loop.onWait?.(view, attempt), await sleep(action.delayMs), attempt += 1;
2769
+ }
2770
+ }
2771
+ var isWait, isLost, init_poll = __esm({ "src/core/poll.ts"() {
2772
+ "use strict";
2773
+ init_define_IGNITIFY_CLIENT_FILES();
2774
+ init_http_client();
2775
+ init_tenant();
2776
+ init_constants();
2777
+ init_utils();
2778
+ isWait = (action) => action.kind === "wait", isLost = (end) => "kind" in end && end.kind === "lost";
2779
+ } });
2685
2780
  function shortenHome(file, home) {
2686
2781
  if (home === "" || !file.startsWith(home)) return file;
2687
2782
  let rest = file.slice(home.length);
@@ -2710,9 +2805,6 @@ var INDENT, renderWaitingStatic, init_login_view = __esm({ "src/commands/login-v
2710
2805
  INDENT = " ";
2711
2806
  renderWaitingStatic = (minutes) => LOGIN_VIEW.WAITING_STATIC(minutes);
2712
2807
  } });
2713
- function formatUserCode(code) {
2714
- return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
2715
- }
2716
2808
  function replacedKeyNote(replacedKeyId, ownKeyId, label) {
2717
2809
  return !replacedKeyId || replacedKeyId === ownKeyId ? null : MESSAGES.LOGIN_REPLACED(label);
2718
2810
  }
@@ -2745,15 +2837,8 @@ async function withRetries(baseUrl, clock, waitMs, call) {
2745
2837
  await clock.sleep(waitMs);
2746
2838
  }
2747
2839
  }
2748
- function bodyOf(e) {
2749
- try {
2750
- return JSON.parse(e.body);
2751
- } catch {
2752
- return {};
2753
- }
2754
- }
2755
2840
  function pollFailure(e, userCode) {
2756
- let body = bodyOf(e);
2841
+ let body = httpErrorJson(e)?.body ?? {};
2757
2842
  switch (e.status) {
2758
2843
  case 410:
2759
2844
  return MESSAGES.LOGIN_EXPIRED(formatUserCode(userCode));
@@ -2782,7 +2867,7 @@ async function pollForKey(baseUrl, started, deadline, deps) {
2782
2867
  }
2783
2868
  async function runLogin(args, deps = {}) {
2784
2869
  return guard(async () => {
2785
- let clock = deps.clock ?? REAL_CLOCK, baseUrl = assertApiUrl(strOpt(args, "api") ?? DEFAULTS.API_URL), label = strOpt(args, "name") ?? (deps.hostname ?? import_node_os2.default.hostname)(), file = deps.file ?? credentialsPath(deps.env), started = await withRetries(baseUrl, clock, START_RETRY_MS, () => startDeviceLogin({ baseUrl }, { label, cli_version: CLIENT_VERSION }, deps.fetchImpl)).catch((e) => {
2870
+ let clock = deps.clock ?? REAL_CLOCK, baseUrl = assertApiUrl(apiUrlOf(args)), label = strOpt(args, "name") ?? (deps.hostname ?? import_node_os2.default.hostname)(), file = deps.file ?? credentialsPath(deps.env), started = await withRetries(baseUrl, clock, START_RETRY_MS, () => startDeviceLogin({ baseUrl }, { label, cli_version: CLIENT_VERSION }, deps.fetchImpl)).catch((e) => {
2786
2871
  throw e instanceof HttpError && e.status === 429 ? new Error(MESSAGES.LOGIN_TOO_MANY, { cause: e }) : e;
2787
2872
  }), env = deps.env ?? process.env, st = screenStyle(deps.screen, env), url = started.verification_uri_complete;
2788
2873
  if (say(renderLoginStart(st, { code: formatUserCode(started.user_code), url })), say(""), url !== null) {
@@ -2830,35 +2915,28 @@ var import_node_os2, import_node_child_process, START_RETRY_MS, retryable, reach
2830
2915
  "use strict";
2831
2916
  init_define_IGNITIFY_CLIENT_FILES();
2832
2917
  import_node_os2 = __toESM(require("node:os"), 1), import_node_child_process = require("node:child_process");
2918
+ init_tenant();
2833
2919
  init_http_client();
2920
+ init_tenant();
2834
2921
  init_api();
2835
2922
  init_credentials();
2836
2923
  init_version();
2837
2924
  init_unreachable();
2925
+ init_poll();
2838
2926
  init_api_url();
2839
2927
  init_constants();
2840
2928
  init_utils();
2841
2929
  init_output();
2842
2930
  init_login_view();
2843
- START_RETRY_MS = 1e3, retryable = (e) => !(e instanceof BadAnswerError) && (!(e instanceof HttpError) || e.status >= 500), reachDetail = (e) => e instanceof UnreachableError ? e.detail : null;
2844
- } });
2845
- function filters(args) {
2846
- let q = {}, period = strOpt(args, "period");
2847
- period !== void 0 && (q.period = period);
2848
- let axis = strOpt(args, "axis");
2849
- axis !== void 0 && (q.axis = axis);
2850
- let vm = strOpt(args, "vm");
2851
- vm !== void 0 && (q.vm = vm);
2852
- let run2 = strOpt(args, "run");
2853
- return run2 !== void 0 && (q.run = run2), q;
2854
- }
2931
+ START_RETRY_MS = 1e3, retryable = (e) => isTransientError(e) && !(e instanceof HttpError && e.status === 429), reachDetail = (e) => e instanceof UnreachableError ? e.detail : isTransientError(e) && !(e instanceof HttpError) && !(e instanceof TypeError) ? errText(e) : null;
2932
+ } });
2855
2933
  function selectLines(lines, runs, vms) {
2856
2934
  return lines.filter((l) => runs && l.run_id !== null || vms && l.vm_id !== null);
2857
2935
  }
2858
2936
  async function runUsageShow(args) {
2859
2937
  return guard(async () => {
2860
2938
  let report = await getUsage(apiConfig(args), filters(args));
2861
- if (boolFlag(args, "json")) return out(JSON.stringify(report, null, 2)), 0;
2939
+ if (boolFlag(args, "json")) return emit(report), 0;
2862
2940
  let runs = boolFlag(args, "runs"), vms = boolFlag(args, "vms"), shown = { ...report, lines: selectLines(report.lines, runs, vms) };
2863
2941
  return out(renderUsageReport(outStyle(), shown, { lines: runs || vms })), 0;
2864
2942
  });
@@ -2871,7 +2949,7 @@ async function runUsageExport(args) {
2871
2949
  return out(json2 ? JSON.stringify(report.lines, null, 2) : toCsv(report)), 0;
2872
2950
  });
2873
2951
  }
2874
- var init_usage_report = __esm({ "src/commands/usage-report.ts"() {
2952
+ var filters, init_usage_report = __esm({ "src/commands/usage-report.ts"() {
2875
2953
  "use strict";
2876
2954
  init_define_IGNITIFY_CLIENT_FILES();
2877
2955
  init_api();
@@ -2880,10 +2958,10 @@ var init_usage_report = __esm({ "src/commands/usage-report.ts"() {
2880
2958
  init_usage2();
2881
2959
  init_output();
2882
2960
  init_constants();
2961
+ filters = (args) => flagParams(args, USAGE_FLAG_PARAMS);
2883
2962
  } });
2884
2963
  function planTaskDeletion(input) {
2885
- let affected = [];
2886
- for (let run2 of input.runs) isTerminalRunStatus(run2.status) || affected.push({ runId: run2.id, status: run2.status, ...run2.vm_id ? { vmId: run2.vm_id } : {}, fate: fateOf(run2.status) });
2964
+ let affected = input.runs.filter((run2) => !isTerminalRunStatus(run2.status)).map((run2) => ({ runId: run2.id, status: run2.status, ...run2.vm_id ? { vmId: run2.vm_id } : {}, fate: fateOf(run2.status) }));
2887
2965
  return { taskSlug: input.taskSlug, deploymentCount: input.deploymentCount, runCount: input.runCount, affected, unlinkDirs: linkedDirs(input.taskId, input.folders), hasInFlight: affected.length > 0 };
2888
2966
  }
2889
2967
  function linkedDirs(taskId, folders) {
@@ -2915,7 +2993,7 @@ var init_resolve_deployment = __esm({ "src/core/resolve-deployment.ts"() {
2915
2993
  function parseRunOptions(args, config) {
2916
2994
  let bare = bareValueFlag(args, VALUE_FLAGS);
2917
2995
  if (bare !== void 0) return { ok: false, error: needsValueMessage(bare) };
2918
- let payloadArg = strOpt(args, "payload"), payload = payloadArg === void 0 ? { ok: true, value: config.run?.payload ?? {} } : parseJson2(payloadArg);
2996
+ let payloadArg = strOpt(args, "payload"), payload = payloadArg === void 0 ? { ok: true, value: config.run?.payload ?? {} } : parseJsonText(payloadArg);
2919
2997
  if (!payload.ok) return { ok: false, error: `--payload is not valid JSON: ${payloadArg}` };
2920
2998
  let timeoutArg = strOpt(args, "timeout-sec"), timeoutOverride = timeoutArg === void 0 ? void 0 : Number(timeoutArg);
2921
2999
  if (timeoutOverride !== void 0 && !(Number.isFinite(timeoutOverride) && timeoutOverride > 0)) return { ok: false, error: `--timeout-sec must be a positive number, got: ${timeoutArg}` };
@@ -2926,21 +3004,35 @@ function parseRunOptions(args, config) {
2926
3004
  let wait = boolFlag(args, "wait");
2927
3005
  return at !== void 0 && wait ? { ok: false, error: MESSAGES.RUN_AT_WITH_WAIT } : { ok: true, deploymentFlag: strOpt(args, "deployment"), wait, run: { payload: payload.value, preset: config.run?.preset, timeoutSec: timeoutOverride ?? config.run?.timeout_sec, execution: onBusy === void 0 ? config.execution : { ...config.execution ?? {}, on_busy: onBusy }, at } };
2928
3006
  }
2929
- function parseJson2(text) {
2930
- try {
2931
- return { ok: true, value: JSON.parse(text) };
2932
- } catch {
2933
- return { ok: false };
2934
- }
2935
- }
2936
3007
  var VALUE_FLAGS, init_run_options = __esm({ "src/core/run-options.ts"() {
2937
3008
  "use strict";
2938
3009
  init_define_IGNITIFY_CLIENT_FILES();
3010
+ init_tenant();
2939
3011
  init_http_client();
2940
3012
  init_constants();
2941
3013
  init_utils();
2942
3014
  VALUE_FLAGS = Object.entries(COMMAND_META[COMMANDS.TASK_RUN].flags).filter(([, kind]) => kind === "value").map(([flag]) => flag);
2943
3015
  } });
3016
+ function classifyRunAnswer(res, at, wait) {
3017
+ return at !== void 0 ? res.schedule_id ? { kind: "scheduled", scheduleId: res.schedule_id, firesAt: res.fires_at ?? at } : res.run_id ? { kind: "skew-ran-now", runId: res.run_id } : { kind: "skew-no-schedule" } : wait && res.run_id ? { kind: "wait", runId: res.run_id } : { kind: "submitted" };
3018
+ }
3019
+ var init_run_answer = __esm({ "src/core/run-answer.ts"() {
3020
+ "use strict";
3021
+ init_define_IGNITIFY_CLIENT_FILES();
3022
+ } });
3023
+ function writeFileAtomic(file, text) {
3024
+ let exists = import_node_fs3.default.existsSync(file), target = exists ? import_node_fs3.default.realpathSync(file) : file, mode = exists ? import_node_fs3.default.statSync(target).mode & 511 : NEW_FILE_MODE, name = `.${import_node_path2.default.basename(target)}.${process.pid}.${(0, import_node_crypto2.randomBytes)(6).toString("hex")}.tmp`, tmp = import_node_path2.default.join(import_node_path2.default.dirname(target), name);
3025
+ try {
3026
+ import_node_fs3.default.writeFileSync(tmp, text, { mode, flag: "wx" }), exists && import_node_fs3.default.chmodSync(tmp, mode), import_node_fs3.default.renameSync(tmp, target);
3027
+ } finally {
3028
+ import_node_fs3.default.rmSync(tmp, { force: true });
3029
+ }
3030
+ }
3031
+ var import_node_crypto2, import_node_fs3, import_node_path2, NEW_FILE_MODE, init_fs_atomic = __esm({ "src/io/fs-atomic.ts"() {
3032
+ "use strict";
3033
+ init_define_IGNITIFY_CLIENT_FILES();
3034
+ import_node_crypto2 = require("node:crypto"), import_node_fs3 = __toESM(require("node:fs"), 1), import_node_path2 = __toESM(require("node:path"), 1), NEW_FILE_MODE = 438;
3035
+ } });
2944
3036
  function parseState(raw) {
2945
3037
  let parsed;
2946
3038
  try {
@@ -2948,9 +3040,12 @@ function parseState(raw) {
2948
3040
  } catch {
2949
3041
  return {};
2950
3042
  }
2951
- if (typeof parsed != "object" || parsed === null) return {};
2952
- let obj = parsed, out2 = {};
2953
- for (let key of STATE_KEYS) typeof obj[key] == "string" && (out2[key] = obj[key]);
3043
+ if (!isRecord(parsed)) return {};
3044
+ let out2 = {};
3045
+ for (let key of STATE_KEYS) {
3046
+ let value = parsed[key];
3047
+ typeof value == "string" && (out2[key] = value);
3048
+ }
2954
3049
  return out2;
2955
3050
  }
2956
3051
  function serializeState(state) {
@@ -2964,19 +3059,19 @@ function writeState(taskDir, patch2) {
2964
3059
  let merged = { ...readState(taskDir), ...patch2 }, statePath = import_path.default.join(taskDir, STATE.DIR, STATE.FILE);
2965
3060
  import_fs.default.mkdirSync(import_path.default.dirname(statePath), { recursive: true });
2966
3061
  let ignorePath = import_path.default.join(import_path.default.dirname(statePath), STATE.GITIGNORE_FILE);
2967
- import_fs.default.existsSync(ignorePath) || import_fs.default.writeFileSync(ignorePath, STATE.GITIGNORE_TEXT);
2968
- let tmpPath = `${statePath}.tmp`;
2969
- return import_fs.default.writeFileSync(tmpPath, serializeState(merged)), import_fs.default.renameSync(tmpPath, statePath), merged;
3062
+ return import_fs.default.existsSync(ignorePath) || import_fs.default.writeFileSync(ignorePath, STATE.GITIGNORE_TEXT), writeFileAtomic(statePath, serializeState(merged)), merged;
2970
3063
  }
2971
3064
  var import_fs, import_path, STATE_KEYS, init_state = __esm({ "src/io/state.ts"() {
2972
3065
  "use strict";
2973
3066
  init_define_IGNITIFY_CLIENT_FILES();
2974
3067
  import_fs = __toESM(require("fs"), 1), import_path = __toESM(require("path"), 1);
3068
+ init_tenant();
3069
+ init_fs_atomic();
2975
3070
  init_constants();
2976
3071
  STATE_KEYS = ["task_id", "task_resource_id", "deployment_id"];
2977
3072
  } });
2978
3073
  function buildTaskReport(inputs) {
2979
- let { slug: slug2, deployments, runs, usage } = inputs, taskDeployments = deployments === null ? null : deployments.filter((d) => d.task_slug === slug2), firstPage = (rows) => rows.length >= inputs.pageLimit ? ` (first ${inputs.pageLimit} shown; more may exist)` : "", newDeployment = decideNewDeployment(inputs.localContentHash, taskDeployments), newVm = decideNewVm(inputs.declared, inputs.declaredPreset, inputs.stateDeploymentId, newDeployment, runs), deployed = taskDeployments === null ? { yes: "unknown", detail: inputs.stateDeploymentId ? `server unreachable; .ignitify/state.json points at ${inputs.stateDeploymentId}` : "server unreachable; no local deployment pointer either" } : taskDeployments.length > 0 ? { yes: true, detail: `${taskDeployments.length} live deployment(s)${firstPage(taskDeployments)}${inputs.stateDeploymentId && !taskDeployments.some((d) => d.id === inputs.stateDeploymentId) ? ` \u2014 WARNING: state.json points at ${inputs.stateDeploymentId}, which is not among them (deleted?)` : ""}` } : { yes: false, detail: "no live deployments for this task" }, deploymentSummaries = taskDeployments === null ? "unknown" : byCreatedAtDesc(taskDeployments).map((d) => ({ id: d.id ?? "?", created_at: d.created_at ?? "?", status: d.status ?? "?", content_hash: normalizeContentHash(d.content_hash ?? "").slice(0, 12), image_bytes: d.image_bytes ?? 0, detailCommand: `ignitify deployment:get ${d.id ?? "<id>"}` })), runsSummary = runs === null ? "unknown" : { total: runs.length, firstPageOnly: runs.length >= inputs.pageLimit, byStatus: runs.reduce((acc, r) => {
3074
+ let { slug: slug2, deployments, runs, usage } = inputs, taskDeployments = deployments === null ? null : deployments.filter((d) => d.task_slug === slug2), firstPage = (rows) => rows.length >= inputs.pageLimit ? ` (first ${inputs.pageLimit} shown; more may exist)` : "", newDeployment = decideNewDeployment(inputs.localContentHash, taskDeployments), newVm = decideNewVm(inputs.declared, inputs.declaredPreset, inputs.stateDeploymentId, newDeployment, runs), deployed = taskDeployments === null ? { yes: "unknown", detail: inputs.stateDeploymentId ? `server unreachable; .ignitify/state.json points at ${inputs.stateDeploymentId}` : "server unreachable; no local deployment pointer either" } : taskDeployments.length > 0 ? { yes: true, detail: `${taskDeployments.length} live deployment(s)${firstPage(taskDeployments)}${inputs.stateDeploymentId && !taskDeployments.some((d) => d.id === inputs.stateDeploymentId) ? ` \u2014 WARNING: state.json points at ${inputs.stateDeploymentId}, which is not among them (deleted?)` : ""}` } : { yes: false, detail: "no live deployments for this task" }, deploymentSummaries = taskDeployments === null ? "unknown" : byCreatedAtDesc(taskDeployments).map((d) => ({ id: d.id ?? "?", created_at: d.created_at ?? "?", status: d.status ?? "?", content_hash: stripSha256Prefix(d.content_hash ?? "").slice(0, 12), image_bytes: d.image_bytes ?? 0, detailCommand: `ignitify deployment:get ${d.id ?? "<id>"}` })), runsSummary = runs === null ? "unknown" : { total: runs.length, firstPageOnly: runs.length >= inputs.pageLimit, byStatus: runs.reduce((acc, r) => {
2980
3075
  let s = r.status ?? "?";
2981
3076
  return acc[s] = (acc[s] ?? 0) + 1, acc;
2982
3077
  }, /* @__PURE__ */ Object.create(null)), recent: byCreatedAtDesc(runs).slice(0, 5).map((r) => ({ id: r.id ?? "?", created_at: r.created_at ?? "?", status: r.status ?? "?", detailCommand: `ignitify run:get ${r.id ?? "<id>"}` })), listCommand: `ignitify run:list --task-name ${slug2}` };
@@ -2984,7 +3079,7 @@ function buildTaskReport(inputs) {
2984
3079
  }
2985
3080
  function formatTaskReport(r) {
2986
3081
  let lines = [];
2987
- if (lines.push(`Task report: ${r.slug}`), lines.push(""), lines.push(`Deployed: ${r.deployed.yes === "unknown" ? "UNKNOWN" : r.deployed.yes ? "YES" : "NO"} \u2014 ${r.deployed.detail}`), lines.push(""), lines.push("Resources:"), lines.push(` task images on disk: ${r.resources.taskImageBytes === "unknown" ? "unknown (server unreachable)" : formatBytes(r.resources.taskImageBytes)}`), r.resources.tenant) {
3082
+ if (lines.push(`Task report: ${r.slug}`), lines.push(""), lines.push(`Deployed: ${yesNoUnknown(r.deployed.yes)} \u2014 ${r.deployed.detail}`), lines.push(""), lines.push("Resources:"), lines.push(` task images on disk: ${r.resources.taskImageBytes === "unknown" ? "unknown (server unreachable)" : formatBytes(r.resources.taskImageBytes)}`), r.resources.tenant) {
2988
3083
  let u = r.resources.tenant;
2989
3084
  lines.push(` tenant-wide (whoami): tasks ${u.task_count ?? "?"}, active runs ${u.active_runs ?? "?"}, deployments ${u.deployment_count ?? "?"}, images ${formatBytes(u.total_image_bytes ?? 0)}, egress this month ${formatBytes(u.net_egress_bytes ?? 0)}`), lines.push(" more: ignitify quota");
2990
3085
  }
@@ -3007,27 +3102,25 @@ function formatTaskReport(r) {
3007
3102
  }
3008
3103
  lines.push("");
3009
3104
  }
3010
- lines.push(`New deployment needed: ${verdictLine(r.newDeployment)}`);
3105
+ lines.push(`New deployment needed: ${yesNoUnknown(r.newDeployment.needed)}`);
3011
3106
  for (let reason of r.newDeployment.reasons) lines.push(` - ${reason}`);
3012
- lines.push(""), lines.push(`New VM on next run: ${verdictLine(r.newVm)}`);
3107
+ lines.push(""), lines.push(`New VM on next run: ${yesNoUnknown(r.newVm.needed)}`);
3013
3108
  for (let reason of r.newVm.reasons) lines.push(` - ${reason}`);
3014
3109
  return lines.join(`
3015
3110
  `);
3016
3111
  }
3017
- var FINGERPRINT_AXES, resolveOrNull, runNature, fingerprintDiffs, presetIndeterminate, decideNewDeployment, decideNewVm, decideInbound, verdictLine, init_report = __esm({ "src/core/report.ts"() {
3112
+ var FINGERPRINT_AXES, resolveOrNull, DEFAULT_NATURE, runNature, fingerprintDiffs, presetIndeterminate, decideNewDeployment, decideNewVm, decideInbound, yesNoUnknown, init_report = __esm({ "src/core/report.ts"() {
3018
3113
  "use strict";
3019
3114
  init_define_IGNITIFY_CLIENT_FILES();
3020
3115
  init_execution();
3021
3116
  init_output();
3117
+ init_tenant();
3022
3118
  init_utils();
3023
3119
  init_constants();
3024
3120
  FINGERPRINT_AXES = ["network", "storage_mib", "cardinality", "state", "interaction", "duration", "residency"], resolveOrNull = (declared) => {
3025
3121
  let r = resolveNature(declared, PLATFORM_DEFAULT_NATURE);
3026
3122
  return r.ok ? r.nature : null;
3027
- }, runNature = (run2) => {
3028
- let declared = run2.execution ?? void 0;
3029
- return resolveOrNull(declared) ?? resolveNature(void 0, PLATFORM_DEFAULT_NATURE).nature;
3030
- }, fingerprintDiffs = (prev, next, prevPreset, nextPreset) => {
3123
+ }, DEFAULT_NATURE = resolveOrNull(void 0) ?? PLATFORM_DEFAULT_NATURE, runNature = (run2) => resolveOrNull(run2.execution ?? void 0) ?? DEFAULT_NATURE, fingerprintDiffs = (prev, next, prevPreset, nextPreset) => {
3031
3124
  let diffs = [];
3032
3125
  prevPreset !== void 0 && nextPreset !== void 0 && prevPreset !== nextPreset && diffs.push(`preset ${prevPreset}\u2192${nextPreset}`);
3033
3126
  for (let axis of FINGERPRINT_AXES) prev[axis] !== next[axis] && diffs.push(`${axis} ${String(prev[axis] ?? "default")}\u2192${String(next[axis] ?? "default")}`);
@@ -3036,7 +3129,7 @@ var FINGERPRINT_AXES, resolveOrNull, runNature, fingerprintDiffs, presetIndeterm
3036
3129
  if (taskDeployments === null) return { needed: "unknown", reasons: ["server unreachable \u2014 cannot compare against live deployments"] };
3037
3130
  if (taskDeployments.length === 0) return { needed: true, reasons: ["no live deployment exists for this task (never deployed, or all deleted)"] };
3038
3131
  if (localContentHash === null) return { needed: "unknown", reasons: ["the local content hash could not be computed \u2014 the upload would be refused (run `ignitify task:pack` to see why)"] };
3039
- let localHash = normalizeContentHash(localContentHash), match = taskDeployments.find((d) => normalizeContentHash(d.content_hash ?? "") === localHash);
3132
+ let localHash = stripSha256Prefix(localContentHash), match = taskDeployments.find((d) => stripSha256Prefix(d.content_hash ?? "") === localHash);
3040
3133
  if (match) {
3041
3134
  let rebuild = match.image_present === false;
3042
3135
  return { needed: false, reasons: [`local code is identical to deployment ${match.id ?? "?"} (content_hash dedup \u2014 deploy would return reused: true)`, ...rebuild ? ["note: its image is no longer in the store (archived by the GC) \u2014 a deploy would rebuild the image under the SAME deployment id"] : []] };
@@ -3057,10 +3150,10 @@ var FINGERPRINT_AXES, resolveOrNull, runNature, fingerprintDiffs, presetIndeterm
3057
3150
  }, decideInbound = (declared, taskDeployments, runs) => {
3058
3151
  let nature = resolveOrNull(declared);
3059
3152
  if (nature === null || nature.network !== "bidirectional" || nature.inbound_port === void 0) return null;
3060
- let serving = (runs ?? []).some((r) => ACTIVE_RUN_STATUSES.has(r.status ?? "")), routes = taskDeployments === null ? "unknown" : taskDeployments.filter((d) => d.id).map((d) => ({ deploymentId: d.id, routeId: deriveRouteId(d.id) }));
3153
+ let serving = (runs ?? []).some((r) => ACTIVE_RUN_STATUSES.has(r.status ?? "")), routes = taskDeployments === null ? "unknown" : taskDeployments.flatMap((d) => d.id ? [{ deploymentId: d.id, routeId: deriveRouteId(d.id) }] : []);
3061
3154
  return { port: nature.inbound_port, serving, routes };
3062
3155
  };
3063
- verdictLine = (v) => v.needed === "unknown" ? "UNKNOWN" : v.needed ? "YES" : "NO";
3156
+ yesNoUnknown = (answer) => answer === "unknown" ? "UNKNOWN" : answer ? "YES" : "NO";
3064
3157
  } });
3065
3158
  function getDefaultExportFromCjs(x) {
3066
3159
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x.default : x;
@@ -3721,7 +3814,7 @@ function requireLoader() {
3721
3814
  function mergeMappings(state, destination, source, overridableKeys) {
3722
3815
  common2.isObject(source) || throwError(state, "cannot merge mappings; the provided source object is unacceptable"), chargeMergeWork(state);
3723
3816
  let sourceKeys = Object.keys(source);
3724
- for (let index = 0, quantity2 = sourceKeys.length; index < quantity2; index += 1) {
3817
+ for (let index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
3725
3818
  let key = sourceKeys[index];
3726
3819
  chargeMergeWork(state), _hasOwnProperty.call(destination, key) || (setProperty(destination, key, source[key]), overridableKeys[key] = true);
3727
3820
  }
@@ -3729,11 +3822,11 @@ function requireLoader() {
3729
3822
  function storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valueNode, startLine, startLineStart, startPos) {
3730
3823
  if (Array.isArray(keyNode)) {
3731
3824
  keyNode = Array.prototype.slice.call(keyNode);
3732
- for (let index = 0, quantity2 = keyNode.length; index < quantity2; index += 1) Array.isArray(keyNode[index]) && throwError(state, "nested arrays are not supported inside keys"), typeof keyNode == "object" && _class(keyNode[index]) === "[object Object]" && (keyNode[index] = "[object Object]");
3825
+ for (let index = 0, quantity = keyNode.length; index < quantity; index += 1) Array.isArray(keyNode[index]) && throwError(state, "nested arrays are not supported inside keys"), typeof keyNode == "object" && _class(keyNode[index]) === "[object Object]" && (keyNode[index] = "[object Object]");
3733
3826
  }
3734
3827
  if (typeof keyNode == "object" && _class(keyNode) === "[object Object]" && (keyNode = "[object Object]"), keyNode = String(keyNode), _result === null && (_result = {}), keyTag === "tag:yaml.org,2002:merge") if (Array.isArray(valueNode)) {
3735
3828
  valueNode.length > 100 && throwError(state, "abnormal merge sequence size");
3736
- for (let index = 0, quantity2 = valueNode.length; index < quantity2; index += 1) mergeMappings(state, _result, valueNode[index], overridableKeys);
3829
+ for (let index = 0, quantity = valueNode.length; index < quantity; index += 1) mergeMappings(state, _result, valueNode[index], overridableKeys);
3737
3830
  } else mergeMappings(state, _result, valueNode, overridableKeys);
3738
3831
  else !state.json && !_hasOwnProperty.call(overridableKeys, keyNode) && _hasOwnProperty.call(_result, keyNode) && (state.line = startLine || state.line, state.lineStart = startLineStart || state.lineStart, state.position = startPos || state.position, throwError(state, "duplicated mapping key")), setProperty(_result, keyNode, valueNode), delete overridableKeys[keyNode];
3739
3832
  return _result;
@@ -8200,8 +8293,8 @@ function validateConfig(config, source = "task.yaml", payloadLimit2 = PAYLOAD_LI
8200
8293
  let run2 = config.run;
8201
8294
  if (run2) {
8202
8295
  run2.preset !== void 0 && (typeof run2.preset != "string" || run2.preset.trim() === "") ? errors.push(MESSAGES.PRESET_INVALID) : run2.preset !== void 0 && !isKnownPreset(run2.preset) && errors.push(MESSAGES.PRESET_UNKNOWN(source, run2.preset, PRESET_NAMES)), run2.timeout_sec !== void 0 && !(typeof run2.timeout_sec == "number" && run2.timeout_sec > 0) && errors.push(MESSAGES.TIMEOUT_INVALID);
8203
- let bytes2 = run2.payload === void 0 ? 0 : payloadBytes(run2.payload);
8204
- bytes2 > payloadLimit2 && errors.push(MESSAGES.DEFAULT_PAYLOAD_OVER_LIMIT(source, bytes2, payloadLimit2));
8296
+ let bytes = run2.payload === void 0 ? 0 : payloadBytes(run2.payload);
8297
+ bytes > payloadLimit2 && errors.push(MESSAGES.DEFAULT_PAYLOAD_OVER_LIMIT(source, bytes, payloadLimit2));
8205
8298
  }
8206
8299
  let resolved = resolveNature(config.execution, PLATFORM_DEFAULT_NATURE);
8207
8300
  return resolved.ok ? warnings.push(...resolved.warnings) : errors.push(resolved.error), { ok: errors.length === 0, errors, warnings };
@@ -8223,26 +8316,26 @@ function checkTaskConfigText(text, source) {
8223
8316
  return errors.push(...shared.errors), errors.length > 0 ? { ok: false, errors, warnings: shared.warnings } : { ok: true, config, errors, warnings: shared.warnings };
8224
8317
  }
8225
8318
  function slugDirRefusal(slug2, taskDir) {
8226
- let dirName = import_node_path2.default.basename(taskDir);
8319
+ let dirName = import_node_path3.default.basename(taskDir);
8227
8320
  return slug2 === dirName ? null : `task.yaml slug "${slug2}" must match the task directory name "${dirName}". Every local lookup names the task by its directory, but runs dispatch by slug. Set slug: "${dirName}" or rename the directory.`;
8228
8321
  }
8229
8322
  function findTaskConfigFile(taskDir) {
8230
- let name = import_node_fs3.default.existsSync(taskDir) ? rootConfigName(import_node_fs3.default.readdirSync(taskDir)) : null;
8231
- return name === null ? null : import_node_path2.default.join(taskDir, name);
8323
+ let name = import_node_fs4.default.existsSync(taskDir) ? rootConfigName(import_node_fs4.default.readdirSync(taskDir)) : null;
8324
+ return name === null ? null : import_node_path3.default.join(taskDir, name);
8232
8325
  }
8233
8326
  function readTaskConfigFile(taskDir) {
8234
8327
  let file = findTaskConfigFile(taskDir);
8235
8328
  if (!file) return { ok: false, file: null, errors: [`No ${TASK_CONFIG_FILENAMES.join(" / ")} found in ${taskDir}`], warnings: [] };
8236
- let parsed = checkTaskConfigText(import_node_fs3.default.readFileSync(file, "utf8"), import_node_path2.default.basename(file)), slugError = parsed.ok ? slugDirRefusal(parsed.config.slug, taskDir) : null;
8329
+ let parsed = checkTaskConfigText(import_node_fs4.default.readFileSync(file, "utf8"), import_node_path3.default.basename(file)), slugError = parsed.ok ? slugDirRefusal(parsed.config.slug, taskDir) : null;
8237
8330
  return slugError !== null ? { ok: false, file, errors: [slugError], warnings: parsed.warnings } : { ...parsed, file };
8238
8331
  }
8239
8332
  function resolvedEntry(config) {
8240
8333
  return config.entry ?? RUNTIME_REGISTRY[config.runtime].entryFile;
8241
8334
  }
8242
- var import_node_fs3, import_node_path2, init_task_config3 = __esm({ "src/io/task-config.ts"() {
8335
+ var import_node_fs4, import_node_path3, init_task_config3 = __esm({ "src/io/task-config.ts"() {
8243
8336
  "use strict";
8244
8337
  init_define_IGNITIFY_CLIENT_FILES();
8245
- import_node_fs3 = __toESM(require("node:fs"), 1), import_node_path2 = __toESM(require("node:path"), 1);
8338
+ import_node_fs4 = __toESM(require("node:fs"), 1), import_node_path3 = __toESM(require("node:path"), 1);
8246
8339
  init_tenant();
8247
8340
  init_task_config2();
8248
8341
  init_validate2();
@@ -8256,7 +8349,7 @@ function readTaskFolders(baseDir) {
8256
8349
  try {
8257
8350
  return { dir, state: readState(dir) };
8258
8351
  } catch (e) {
8259
- throw new Error(`cannot read ${taskLinkPath(dir)}: ${errorMessage(e)}`, { cause: e });
8352
+ throw new Error(`cannot read ${taskLinkPath(dir)}: ${errText(e)}`, { cause: e });
8260
8353
  }
8261
8354
  }) : [];
8262
8355
  }
@@ -8279,7 +8372,7 @@ async function prepareTask(args, cwd, taskId = requiredArg(args)) {
8279
8372
  for (let e of read.errors) fail(e);
8280
8373
  return null;
8281
8374
  }
8282
- return { taskDir, config: read.config, warnings: read.warnings };
8375
+ return { taskDir, config: read.config, slug: read.config.slug ?? import_path2.default.basename(taskDir), warnings: read.warnings };
8283
8376
  }
8284
8377
  var import_fs2, import_path2, init_task_path = __esm({ "src/io/task-path.ts"() {
8285
8378
  "use strict";
@@ -8290,7 +8383,7 @@ var import_fs2, import_path2, init_task_path = __esm({ "src/io/task-path.ts"() {
8290
8383
  init_output();
8291
8384
  init_constants();
8292
8385
  init_args2();
8293
- init_guard();
8386
+ init_tenant();
8294
8387
  } });
8295
8388
  function compile(body, anchored) {
8296
8389
  let segs = body.split("/");
@@ -8337,11 +8430,11 @@ var UNSUPPORTED_CHARS, escapeRegex, segmentSource, init_ignore_file = __esm({ "s
8337
8430
  } });
8338
8431
  function scanSourceTree(root, maxBytes = UPLOAD_MAX_BYTES, ignoreRules = []) {
8339
8432
  let entries = [], data = /* @__PURE__ */ new Map(), errors = [], warnings = [], ignored = [], totalBytes = 0, over = false;
8340
- if (!import_node_fs4.default.existsSync(root)) return { entries, data, errors: [`Task directory does not exist: ${root}`], warnings, ignored, totalBytes, truncated: false };
8433
+ if (!import_node_fs5.default.existsSync(root)) return { entries, data, errors: [`Task directory does not exist: ${root}`], warnings, ignored, totalBytes, truncated: false };
8341
8434
  let walk = (dir) => {
8342
- for (let dirent of import_node_fs4.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
8435
+ for (let dirent of import_node_fs5.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
8343
8436
  if (over) return;
8344
- let abs = import_node_path3.default.join(dir, dirent.name), rel = import_node_path3.default.relative(root, abs).split(import_node_path3.default.sep).join("/"), skip = excludeReason(rel);
8437
+ let abs = import_node_path4.default.join(dir, dirent.name), rel = import_node_path4.default.relative(root, abs).split(import_node_path4.default.sep).join("/"), skip = excludeReason(rel);
8345
8438
  if (skip) {
8346
8439
  dirent.isDirectory() || warnings.push(`excluded ${rel} \u2014 ${skip}`);
8347
8440
  continue;
@@ -8369,7 +8462,7 @@ function scanSourceTree(root, maxBytes = UPLOAD_MAX_BYTES, ignoreRules = []) {
8369
8462
  errors.push(symlinkRefusal(rel));
8370
8463
  continue;
8371
8464
  }
8372
- let { fd, st } = opened, bytes2;
8465
+ let { fd, st } = opened, bytes;
8373
8466
  try {
8374
8467
  if (!st.isFile()) {
8375
8468
  errors.push(notRegularRefusal(rel));
@@ -8379,34 +8472,35 @@ function scanSourceTree(root, maxBytes = UPLOAD_MAX_BYTES, ignoreRules = []) {
8379
8472
  over = true, errors.push(`the task directory holds more than ${maxBytes / MIB} MiB of files (reached at ${rel}); the server refuses a larger upload`);
8380
8473
  return;
8381
8474
  }
8382
- bytes2 = import_node_fs4.default.readFileSync(fd);
8475
+ bytes = import_node_fs5.default.readFileSync(fd);
8383
8476
  } finally {
8384
- import_node_fs4.default.closeSync(fd);
8477
+ import_node_fs5.default.closeSync(fd);
8385
8478
  }
8386
8479
  if (CREDENTIAL_CHECKED_FILES.includes(dirent.name)) {
8387
- let cred = credentialReason(rel, bytes2.toString("utf8"));
8480
+ let cred = credentialReason(rel, bytes.toString("utf8"));
8388
8481
  if (cred !== null) {
8389
8482
  errors.push(cred);
8390
8483
  continue;
8391
8484
  }
8392
8485
  }
8393
8486
  let keyWarning = privateKeyFileWarning(rel);
8394
- keyWarning !== null && warnings.push(keyWarning), entries.push({ path: rel, exec: (st.mode & 73) !== 0, size: bytes2.length, sha256: import_node_crypto2.default.createHash("sha256").update(bytes2).digest("hex") }), data.set(rel, bytes2), totalBytes += bytes2.length;
8487
+ keyWarning !== null && warnings.push(keyWarning), entries.push({ path: rel, exec: (st.mode & 73) !== 0, size: bytes.length, sha256: import_node_crypto3.default.createHash("sha256").update(bytes).digest("hex") }), data.set(rel, bytes), totalBytes += bytes.length;
8395
8488
  }
8396
8489
  };
8397
8490
  return walk(root), { entries, data, errors, warnings, ignored, totalBytes, truncated: over };
8398
8491
  }
8399
8492
  function computeSourceHash(entries, config) {
8400
- return `sha256:${import_node_crypto2.default.createHash("sha256").update(sourceManifest(entries, config), "utf8").digest("hex")}`;
8493
+ let manifest = checkSourceManifest(entries, config);
8494
+ return manifest.ok ? { ok: true, hash: `sha256:${import_node_crypto3.default.createHash("sha256").update(manifest.text, "utf8").digest("hex")}` } : manifest;
8401
8495
  }
8402
8496
  function checkLockfile(entries, runtime) {
8403
8497
  let required = RUNTIME_REGISTRY[runtime].lockfile;
8404
8498
  return entries.some((e) => e.path === required) ? null : `${required} is missing. A ${runtime} task must upload its lockfile: without one the build is not reproducible and two deployments of the same source can resolve different versions.`;
8405
8499
  }
8406
- var import_node_fs4, import_node_path3, import_node_crypto2, MIB, symlinkRefusal, notRegularRefusal, init_source_tree = __esm({ "src/io/source-tree.ts"() {
8500
+ var import_node_fs5, import_node_path4, import_node_crypto3, MIB, symlinkRefusal, notRegularRefusal, init_source_tree = __esm({ "src/io/source-tree.ts"() {
8407
8501
  "use strict";
8408
8502
  init_define_IGNITIFY_CLIENT_FILES();
8409
- import_node_fs4 = __toESM(require("node:fs"), 1), import_node_path3 = __toESM(require("node:path"), 1), import_node_crypto2 = __toESM(require("node:crypto"), 1);
8503
+ import_node_fs5 = __toESM(require("node:fs"), 1), import_node_path4 = __toESM(require("node:path"), 1), import_node_crypto3 = __toESM(require("node:crypto"), 1);
8410
8504
  init_tenant();
8411
8505
  init_ignore_file();
8412
8506
  init_constants();
@@ -8504,8 +8598,8 @@ var NAME_BYTES, MODE_FILE, MODE_EXEC, TYPE_FILE, TYPE_PAX, PAX_HEADER_NAME, TarF
8504
8598
  };
8505
8599
  } });
8506
8600
  function readIgnoreRules(taskDir, protectedPaths) {
8507
- let file = import_node_path4.default.join(taskDir, IGNORE_FILE);
8508
- return import_node_fs5.default.existsSync(file) ? parseIgnoreFile(import_node_fs5.default.readFileSync(file, "utf8"), IGNORE_FILE, protectedPaths) : { ok: true, rules: [] };
8601
+ let file = import_node_path5.default.join(taskDir, IGNORE_FILE);
8602
+ return import_node_fs6.default.existsSync(file) ? parseIgnoreFile(import_node_fs6.default.readFileSync(file, "utf8"), IGNORE_FILE, protectedPaths) : { ok: true, rules: [] };
8509
8603
  }
8510
8604
  function checkSourceUpload(taskDir) {
8511
8605
  let read = readTaskConfigFile(taskDir), warnings = [...read.warnings];
@@ -8520,13 +8614,8 @@ function checkSourceUpload(taskDir) {
8520
8614
  scan.entries.some((e) => e.path === entry) || errors.push(`the entry file "${entry}" is not in the upload.`);
8521
8615
  }
8522
8616
  if (errors.length > 0) return { ok: false, errors, warnings };
8523
- let contentHash;
8524
- try {
8525
- contentHash = computeSourceHash(scan.entries, { name: import_node_path4.default.basename(read.file ?? ""), value: config });
8526
- } catch (e) {
8527
- return { ok: false, errors: [e.message.replace(/^source manifest: /, "")], warnings };
8528
- }
8529
- return { ok: true, source: { config, entries: scan.entries, data: scan.data, totalBytes: scan.totalBytes, ignored: scan.ignored, contentHash }, warnings };
8617
+ let key = computeSourceHash(scan.entries, { name: import_node_path5.default.basename(read.file ?? ""), value: config });
8618
+ return key.ok ? { ok: true, source: { config, entries: scan.entries, data: scan.data, totalBytes: scan.totalBytes, ignored: scan.ignored, contentHash: key.hash }, warnings } : { ok: false, errors: [key.reason], warnings };
8530
8619
  }
8531
8620
  function prepareSourceUpload(taskDir) {
8532
8621
  let check = checkSourceUpload(taskDir);
@@ -8534,10 +8623,10 @@ function prepareSourceUpload(taskDir) {
8534
8623
  let { warnings } = check, { config, entries, data, totalBytes, ignored, contentHash } = check.source, tar = packTarball(entries.map((e) => ({ name: e.path, data: data.get(e.path), exec: e.exec })));
8535
8624
  return { ok: true, upload: { manifest: { contentHash, format: "source", ...config.execution ? { execution: config.execution } : {}, ...config.secrets?.length ? { secrets: [...config.secrets] } : {} }, tar, slug: config.slug, files: entries.length, totalBytes, ignored }, warnings };
8536
8625
  }
8537
- var import_node_fs5, import_node_path4, init_source_upload = __esm({ "src/io/source-upload.ts"() {
8626
+ var import_node_fs6, import_node_path5, init_source_upload = __esm({ "src/io/source-upload.ts"() {
8538
8627
  "use strict";
8539
8628
  init_define_IGNITIFY_CLIENT_FILES();
8540
- import_node_fs5 = __toESM(require("node:fs"), 1), import_node_path4 = __toESM(require("node:path"), 1);
8629
+ import_node_fs6 = __toESM(require("node:fs"), 1), import_node_path5 = __toESM(require("node:path"), 1);
8541
8630
  init_tenant();
8542
8631
  init_ignore_file();
8543
8632
  init_constants();
@@ -8798,9 +8887,9 @@ function packagesRoot() {
8798
8887
  }
8799
8888
  function checkoutReader(root = packagesRoot()) {
8800
8889
  return (pkg, rel) => {
8801
- let abs = import_node_path5.default.join(root, pkg, rel);
8890
+ let abs = import_node_path6.default.join(root, pkg, rel);
8802
8891
  try {
8803
- return import_node_fs6.default.readFileSync(abs, "utf8");
8892
+ return import_node_fs7.default.readFileSync(abs, "utf8");
8804
8893
  } catch {
8805
8894
  throw new Error(`the runtime client source ${abs} is not readable. A Go or Rust scaffold carries the client from packages/${pkg}, so this CLI must run from a checkout of the repository.`);
8806
8895
  }
@@ -8878,10 +8967,10 @@ function rustScaffoldLock(pkgName, read = clientReader()) {
8878
8967
  `) + `
8879
8968
  `;
8880
8969
  }
8881
- var import_node_fs6, import_node_path5, import_node_sea, import_node_url, VENDOR_DIR, CLIENT_FILES, CARGO_RESERVED, init_vendor = __esm({ "src/scaffold/vendor.ts"() {
8970
+ var import_node_fs7, import_node_path6, import_node_sea, import_node_url, VENDOR_DIR, CLIENT_FILES, CARGO_RESERVED, init_vendor = __esm({ "src/scaffold/vendor.ts"() {
8882
8971
  "use strict";
8883
8972
  init_define_IGNITIFY_CLIENT_FILES();
8884
- import_node_fs6 = __toESM(require("node:fs"), 1), import_node_path5 = __toESM(require("node:path"), 1), import_node_sea = __toESM(require("node:sea"), 1), import_node_url = require("node:url"), VENDOR_DIR = "ignitify", CLIENT_FILES = { go: { pkg: "runtime-go", files: ["go.mod", "runtime.go"] }, rust: { pkg: "runtime-rust", files: ["Cargo.toml", "src/lib.rs"] } };
8973
+ import_node_fs7 = __toESM(require("node:fs"), 1), import_node_path6 = __toESM(require("node:path"), 1), import_node_sea = __toESM(require("node:sea"), 1), import_node_url = require("node:url"), VENDOR_DIR = "ignitify", CLIENT_FILES = { go: { pkg: "runtime-go", files: ["go.mod", "runtime.go"] }, rust: { pkg: "runtime-rust", files: ["Cargo.toml", "src/lib.rs"] } };
8885
8974
  CARGO_RESERVED = /* @__PURE__ */ new Set(["build", "deps", "examples", "incremental"]);
8886
8975
  } });
8887
8976
  function templateFiles(taskId, runtime, withInit, dirFlag) {
@@ -8906,7 +8995,7 @@ function scaffoldTaskFiles(taskId, taskDir, withInit, dirFlag, runtime = "node")
8906
8995
  try {
8907
8996
  files = templateFiles(taskId, runtime, withInit, dirFlag);
8908
8997
  } catch (e) {
8909
- return e instanceof Error ? e.message : String(e);
8998
+ return errText(e);
8910
8999
  }
8911
9000
  if (import_fs3.default.existsSync(taskDir)) {
8912
9001
  let clashes = [.../* @__PURE__ */ new Set([...TASK_CONFIG_FILENAMES, ...Object.keys(files)])].filter((f) => import_fs3.default.existsSync(import_path3.default.join(taskDir, f)));
@@ -8926,40 +9015,6 @@ var import_fs3, import_path3, init_scaffold = __esm({ "src/scaffold/index.ts"()
8926
9015
  init_templates();
8927
9016
  init_vendor();
8928
9017
  } });
8929
- function backoffDelayMs(attempt, cadence) {
8930
- return Math.min(cadence.BASE_MS * 2 ** Math.max(0, attempt), cadence.MAX_MS);
8931
- }
8932
- function isTransientPollError(e) {
8933
- return e instanceof HttpError ? e.status === 408 || e.status === 429 || e.status >= 500 && e.status <= 599 : e instanceof TypeError ? true : e instanceof Error && e.cause?.name === "TimeoutError";
8934
- }
8935
- function decidePollError(e, failuresInARow, failingForMs) {
8936
- return isTransientPollError(e) ? failuresInARow >= POLL.ERROR.MIN_TRIES && failingForMs >= POLL.ERROR.GRACE_MS ? "give-up" : "retry" : "give-up";
8937
- }
8938
- async function pollUntil(loop) {
8939
- let sleep = loop.clock?.sleep ?? REAL_CLOCK.sleep, now = loop.clock?.now ?? REAL_CLOCK.now, startedAt = now(), failures = 0, firstFailureAt = 0;
8940
- for (let attempt = 0; ; ) {
8941
- let view;
8942
- try {
8943
- view = await loop.fetch();
8944
- } catch (error) {
8945
- if (failures === 0 && (firstFailureAt = now()), failures += 1, decidePollError(error, failures, now() - firstFailureAt) === "give-up") return { kind: "lost", error };
8946
- loop.onRetry?.(error), await sleep(loop.retryDelayMs);
8947
- continue;
8948
- }
8949
- failures = 0, loop.onView?.(view);
8950
- let action = loop.decide(view, now() - startedAt, attempt);
8951
- if (!isWait(action)) return { action, view };
8952
- loop.onWait?.(view, attempt), await sleep(action.delayMs), attempt += 1;
8953
- }
8954
- }
8955
- var isWait, isLost, init_poll = __esm({ "src/core/poll.ts"() {
8956
- "use strict";
8957
- init_define_IGNITIFY_CLIENT_FILES();
8958
- init_http_client();
8959
- init_constants();
8960
- init_utils();
8961
- isWait = (action) => action.kind === "wait", isLost = (end) => "kind" in end && end.kind === "lost";
8962
- } });
8963
9018
  function decidePoll(status, elapsedMs, attempt, failureReason) {
8964
9019
  return status === READY ? { kind: "ready" } : status === FAILED ? { kind: "failed", reason: failureReason ?? "the build failed without recording a reason" } : status !== BUILDING ? { kind: "failed", reason: `deployment entered an unexpected status: ${status}` } : elapsedMs >= POLL.DEPLOY.TIMEOUT_MS ? { kind: "timeout" } : { kind: "wait", delayMs: nextDelayMs(attempt) };
8965
9020
  }
@@ -9001,14 +9056,14 @@ async function waitForBuild(cfg, deploymentId, progress, reused = false) {
9001
9056
  let view = await getApiDeploymentStatus(cfg, deploymentId);
9002
9057
  return retrying && (retrying = false, progress.update(WAIT.BUILDING(deploymentId), WAIT.HINT_BUILD)), view;
9003
9058
  }, fetchLog: async () => {
9004
- let { bytes: bytes2 } = await getApiDeploymentLogs(cfg, deploymentId);
9005
- return typeof bytes2 == "string" && bytes2 !== "" ? bytes2 : null;
9059
+ let { bytes } = await getApiDeploymentLogs(cfg, deploymentId);
9060
+ return typeof bytes == "string" && bytes !== "" ? bytes : null;
9006
9061
  }, onBuilding: () => {
9007
9062
  building = true, progress.update(WAIT.BUILDING(deploymentId), WAIT.HINT_BUILD);
9008
9063
  }, onRetry: () => {
9009
9064
  retrying = true, progress.update(WAIT.BUILDING(deploymentId), WAIT.HINT_RETRYING);
9010
9065
  } }));
9011
- if (built.kind === "lost") return progress.fail(WAIT.LOST), fail(WAIT.LOST_BUILD_DETAIL(deploymentId, errorMessage(built.error))), 1;
9066
+ if (built.kind === "lost") return progress.fail(WAIT.LOST), fail(WAIT.LOST_BUILD_DETAIL(deploymentId, errText(built.error))), 1;
9012
9067
  if (built.kind === "failed") {
9013
9068
  if (progress.fail(WAIT.BUILD_FAILED), fail(WAIT.BUILD_FAILED_DETAIL(deploymentId, built.reason)), built.log) {
9014
9069
  let st = errStyle();
@@ -9024,7 +9079,7 @@ async function waitForRun(cfg, runId) {
9024
9079
  let progress = startProgress(WAIT.RUN_SUBMITTED(runId)), lastStatus, settled = await withInterruptNotice(progress, () => WAIT.INTERRUPTED_RUN(runId), () => pollUntilSettled({ fetchRun: () => getApiRun(cfg, runId), onStatus: (status) => {
9025
9080
  lastStatus = status, progress.update(WAIT.RUN(runId, status), WAIT.HINT_RUN);
9026
9081
  }, onRetry: () => progress.update(lastStatus === void 0 ? WAIT.RUN_SUBMITTED(runId) : WAIT.RUN(runId, lastStatus), WAIT.HINT_RETRYING) }));
9027
- if (isLost(settled)) return progress.fail(WAIT.LOST), fail(WAIT.LOST_RUN_DETAIL(runId, errorMessage(settled.error))), 1;
9082
+ if (isLost(settled)) return progress.fail(WAIT.LOST), fail(WAIT.LOST_RUN_DETAIL(runId, errText(settled.error))), 1;
9028
9083
  let { action, run: run2 } = settled;
9029
9084
  if (action.kind === "timeout") return progress.fail(WAIT.RUN_TIMEOUT(action.status)), warn(WAIT.RUN_TIMEOUT_DETAIL(runId)), 1;
9030
9085
  let completed = action.status === RUN_STATUS.COMPLETED;
@@ -9039,7 +9094,7 @@ var init_wait = __esm({ "src/commands/wait.ts"() {
9039
9094
  init_poll();
9040
9095
  init_output();
9041
9096
  init_constants();
9042
- init_utils();
9097
+ init_tenant();
9043
9098
  } });
9044
9099
  var MANIFEST_FILE, PACKAGE_SCOPE, RUNTIMES, NETWORK_TYPES, PRESETS, DURATIONS, INTERACTIONS, NAME_PATTERN, CALLBACK_ROUTE_PREFIX, EGRESS_HINT_PATTERN, DNS_LABEL, HOST_PATTERN, HOST_MAX_LENGTH, init_constants3 = __esm({ "../../node_modules/@ignitify-integrations/core/dist/constants.js"() {
9045
9100
  init_define_IGNITIFY_CLIENT_FILES();
@@ -12715,8 +12770,8 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
12715
12770
  case "scalar":
12716
12771
  case "single-quoted-scalar":
12717
12772
  case "double-quoted-scalar": {
12718
- let fs13 = this.flowScalar(this.type);
12719
- atNextItem || it.value ? (map2.items.push({ start, key: fs13, sep: [] }), this.onKeyLine = true) : it.sep ? this.stack.push(fs13) : (Object.assign(it, { key: fs13, sep: [] }), this.onKeyLine = true);
12773
+ let fs14 = this.flowScalar(this.type);
12774
+ atNextItem || it.value ? (map2.items.push({ start, key: fs14, sep: [] }), this.onKeyLine = true) : it.sep ? this.stack.push(fs14) : (Object.assign(it, { key: fs14, sep: [] }), this.onKeyLine = true);
12720
12775
  return;
12721
12776
  }
12722
12777
  default: {
@@ -12805,8 +12860,8 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
12805
12860
  case "scalar":
12806
12861
  case "single-quoted-scalar":
12807
12862
  case "double-quoted-scalar": {
12808
- let fs13 = this.flowScalar(this.type);
12809
- !it || it.value ? fc.items.push({ start: [], key: fs13, sep: [] }) : it.sep ? this.stack.push(fs13) : Object.assign(it, { key: fs13, sep: [] });
12863
+ let fs14 = this.flowScalar(this.type);
12864
+ !it || it.value ? fc.items.push({ start: [], key: fs14, sep: [] }) : it.sep ? this.stack.push(fs14) : Object.assign(it, { key: fs14, sep: [] });
12810
12865
  return;
12811
12866
  }
12812
12867
  case "flow-map-end":
@@ -12981,21 +13036,25 @@ var ok, err, init_result2 = __esm({ "../../node_modules/@ignitify-integrations/r
12981
13036
  var init_ctx = __esm({ "../../node_modules/@ignitify-integrations/runtime/dist/ctx.js"() {
12982
13037
  init_define_IGNITIFY_CLIENT_FILES();
12983
13038
  } });
12984
- var SECRET_NAME_PATTERN, init_secrets = __esm({ "../../node_modules/@ignitify-integrations/runtime/dist/secrets.js"() {
13039
+ var SECRET_NAME_MAX_LENGTH, SECRET_NAME_PATTERN, init_secrets = __esm({ "../../node_modules/@ignitify-integrations/runtime/dist/secrets.js"() {
12985
13040
  init_define_IGNITIFY_CLIENT_FILES();
12986
13041
  init_result2();
12987
- SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
13042
+ SECRET_NAME_MAX_LENGTH = 64, SECRET_NAME_PATTERN = new RegExp(`^[A-Z][A-Z0-9_]{0,${SECRET_NAME_MAX_LENGTH - 1}}$`);
12988
13043
  } });
12989
13044
  var ROUTE_PREFIX_PATTERN, init_platform = __esm({ "../../node_modules/@ignitify-integrations/runtime/dist/platform.js"() {
12990
13045
  init_define_IGNITIFY_CLIENT_FILES();
12991
13046
  ROUTE_PREFIX_PATTERN = "^/[a-z0-9._~-]+(/[a-z0-9._~-]+)*/$";
12992
13047
  } });
13048
+ var init_stop = __esm({ "../../node_modules/@ignitify-integrations/runtime/dist/stop.js"() {
13049
+ init_define_IGNITIFY_CLIENT_FILES();
13050
+ } });
12993
13051
  var init_dist = __esm({ "../../node_modules/@ignitify-integrations/runtime/dist/index.js"() {
12994
13052
  init_define_IGNITIFY_CLIENT_FILES();
12995
13053
  init_result2();
12996
13054
  init_ctx();
12997
13055
  init_secrets();
12998
13056
  init_platform();
13057
+ init_stop();
12999
13058
  } });
13000
13059
  var list, MANIFEST_MESSAGES, PLAN_MESSAGES, COVERAGE_MESSAGES, init_messages = __esm({ "../../node_modules/@ignitify-integrations/core/dist/messages.js"() {
13001
13060
  init_define_IGNITIFY_CLIENT_FILES();
@@ -13006,7 +13065,7 @@ var nonEmptyString, uniqueStrings, closed, network, MANIFEST_SCHEMA, init_schema
13006
13065
  init_define_IGNITIFY_CLIENT_FILES();
13007
13066
  init_dist();
13008
13067
  init_constants3();
13009
- nonEmptyString = { type: "string", minLength: 1 }, uniqueStrings = { type: "array", items: nonEmptyString, uniqueItems: true }, closed = (properties, required = []) => ({ type: "object", additionalProperties: false, properties, required }), network = closed({ type: { enum: NETWORK_TYPES }, egress_allow: { type: "array", items: { type: "string", minLength: 1, maxLength: HOST_MAX_LENGTH }, uniqueItems: true, maxItems: 32 }, egress_hint: { type: "string", minLength: 1, maxLength: 200, pattern: EGRESS_HINT_PATTERN }, inbound_port: { type: "integer", minimum: 1024, maximum: 65535 }, inbound_routes: { type: "array", minItems: 1, items: closed({ prefix: { type: "string", pattern: ROUTE_PREFIX_PATTERN } }, ["prefix"]) }, inbound_max_concurrent: { type: "integer", minimum: 1 } }, ["type"]), MANIFEST_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", title: "ignitify integration manifest", ...closed({ schema_version: { const: 1 }, name: { type: "string", pattern: NAME_PATTERN }, runtimes: { type: "array", items: { enum: RUNTIMES }, minItems: 1, uniqueItems: true }, secrets: { type: "array", items: { type: "string", pattern: SECRET_NAME_PATTERN.source }, uniqueItems: true }, network, execution: closed({ duration: { enum: DURATIONS }, interaction: { enum: INTERACTIONS } }), system_packages: uniqueStrings, min_preset: { enum: PRESETS } }, ["schema_version", "name", "runtimes"]) };
13068
+ nonEmptyString = { type: "string", minLength: 1 }, uniqueStrings = { type: "array", items: nonEmptyString, uniqueItems: true }, closed = (properties, required = []) => ({ type: "object", additionalProperties: false, properties, required }), network = closed({ type: { enum: NETWORK_TYPES }, egress_allow: { type: "array", items: { type: "string", minLength: 1, maxLength: HOST_MAX_LENGTH }, uniqueItems: true, maxItems: 32 }, egress_hint: { type: "string", minLength: 1, maxLength: 200, pattern: EGRESS_HINT_PATTERN }, inbound_port: { type: "integer", minimum: 1024, maximum: 65535 }, inbound_routes: { type: "array", minItems: 1, maxItems: 32, items: closed({ prefix: { type: "string", maxLength: 256, pattern: ROUTE_PREFIX_PATTERN } }, ["prefix"]) }, inbound_max_concurrent: { type: "integer", minimum: 1 } }, ["type"]), MANIFEST_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", title: "ignitify integration manifest", ...closed({ schema_version: { const: 1 }, name: { type: "string", pattern: NAME_PATTERN }, runtimes: { type: "array", items: { enum: RUNTIMES }, minItems: 1, uniqueItems: true }, secrets: { type: "array", items: { type: "string", maxLength: SECRET_NAME_MAX_LENGTH, pattern: SECRET_NAME_PATTERN.source }, uniqueItems: true }, network, execution: closed({ duration: { enum: DURATIONS }, interaction: { enum: INTERACTIONS } }), system_packages: uniqueStrings, min_preset: { enum: PRESETS } }, ["schema_version", "name", "runtimes"]) };
13010
13069
  } });
13011
13070
  var import__2, import_yaml, INBOUND_KEYS, EGRESS_KEYS, ROOT_PATH, NUMERIC_LABEL, validate, isHostName, loadYaml, formatError, semanticIssues, parseManifest, init_manifest = __esm({ "../../node_modules/@ignitify-integrations/core/dist/manifest.js"() {
13012
13071
  init_define_IGNITIFY_CLIENT_FILES();
@@ -13041,7 +13100,7 @@ var packageName, hasCode, rank, maxOf, union, optional, init_utils2 = __esm({ ".
13041
13100
  init_constants3();
13042
13101
  packageName = (name) => `${PACKAGE_SCOPE}/${name}`, hasCode = (packageJson) => typeof packageJson == "object" && packageJson !== null && "exports" in packageJson, rank = (order, value) => order.indexOf(value), maxOf = (order, a, b) => rank(order, a) >= rank(order, b) ? a : b, union = (a, b) => [.../* @__PURE__ */ new Set([...a, ...b])], optional = (key, value) => value === void 0 ? {} : { [key]: value };
13043
13102
  } });
13044
- var EXECUTION_FIELDS, INBOUND_FIELDS, needsOf, planHeadline, pick, unionRoutes, egressFor, networkFor, presetFor, planAdd, coverageIssues, checkCoverage, init_plan = __esm({ "../../node_modules/@ignitify-integrations/core/dist/plan.js"() {
13103
+ var EXECUTION_FIELDS, INBOUND_FIELDS, needsOf, planHeadline, pick, prefixOf, unionRoutes, egressFor, networkFor, presetFor, planAdd, coverageIssues, checkCoverage, init_plan = __esm({ "../../node_modules/@ignitify-integrations/core/dist/plan.js"() {
13045
13104
  init_define_IGNITIFY_CLIENT_FILES();
13046
13105
  init_dist();
13047
13106
  init_constants3();
@@ -13050,7 +13109,7 @@ var EXECUTION_FIELDS, INBOUND_FIELDS, needsOf, planHeadline, pick, unionRoutes,
13050
13109
  EXECUTION_FIELDS = ["duration", "interaction"], INBOUND_FIELDS = ["inbound_port", "inbound_max_concurrent"], needsOf = (manifest) => ({ hosts: manifest.network?.egress_allow ?? [], secrets: manifest.secrets ?? [], networkType: manifest.network?.type ?? "none", inbound_port: manifest.network?.inbound_port, inbound_max_concurrent: manifest.network?.inbound_max_concurrent, inboundRoutes: manifest.network?.inbound_routes ?? [], execution: manifest.execution ?? {}, systemPackages: manifest.system_packages ?? [], minPreset: manifest.min_preset }), planHeadline = (manifest, version2, hasCode2) => {
13051
13110
  let packages = manifest.system_packages ?? [];
13052
13111
  return [`${packageName(manifest.name)}@${version2}`, hasCode2 ? PLAN_MESSAGES.settingsAndCode : PLAN_MESSAGES.settingsOnly, ...packages.length > 0 ? [PLAN_MESSAGES.runs(packages)] : []].join(" \xB7 ");
13053
- }, pick = (field, current, wanted, label) => wanted === void 0 || current === wanted ? { value: current } : current === void 0 ? { value: wanted } : { value: current, conflict: PLAN_MESSAGES.conflict(field, current, wanted, label) }, unionRoutes = (current, wanted) => [...current, ...wanted.filter((route2) => !current.some((have) => have.prefix === route2.prefix))], egressFor = (current, needs, notes) => {
13112
+ }, pick = (field, current, wanted, label) => wanted === void 0 || current === wanted ? { value: current } : current === void 0 ? { value: wanted } : { value: current, conflict: PLAN_MESSAGES.conflict(field, current, wanted, label) }, prefixOf = (route2) => "prefix" in route2 ? route2.prefix : void 0, unionRoutes = (current, wanted) => [...current, ...wanted.filter((route2) => !current.some((have) => prefixOf(have) === route2.prefix))], egressFor = (current, needs, notes) => {
13054
13113
  if (current?.egress_allow !== void 0) return union(current.egress_allow, needs.hosts);
13055
13114
  if ((current?.type ?? "none") !== "none") {
13056
13115
  needs.hosts.length > 0 && notes.push(PLAN_MESSAGES.egressAnywhere(needs.hosts));
@@ -13088,7 +13147,7 @@ var EXECUTION_FIELDS, INBOUND_FIELDS, needsOf, planHeadline, pick, unionRoutes,
13088
13147
  }), ...INBOUND_FIELDS.flatMap((field) => {
13089
13148
  let need = needs[field];
13090
13149
  return need === void 0 || network2?.[field] === need ? [] : [COVERAGE_MESSAGES.fieldMismatch(label, `execution.network.${field}`, network2?.[field], need)];
13091
- }), ...needs.inboundRoutes.filter((route2) => !routes.some((present) => present.prefix === route2.prefix)).map((route2) => COVERAGE_MESSAGES.routeMissing(label, route2.prefix)), ...needs.systemPackages.filter((name) => !packages.includes(name)).map((name) => COVERAGE_MESSAGES.packageMissing(label, name)), ...needs.minPreset !== void 0 && (preset === void 0 || presetRank >= 0 && presetRank < rank(PRESETS, needs.minPreset)) ? [COVERAGE_MESSAGES.presetTooSmall(label, preset, needs.minPreset)] : []];
13150
+ }), ...needs.inboundRoutes.filter((route2) => !routes.some((present) => prefixOf(present) === route2.prefix)).map((route2) => COVERAGE_MESSAGES.routeMissing(label, route2.prefix)), ...needs.systemPackages.filter((name) => !packages.includes(name)).map((name) => COVERAGE_MESSAGES.packageMissing(label, name)), ...needs.minPreset !== void 0 && (preset === void 0 || presetRank >= 0 && presetRank < rank(PRESETS, needs.minPreset)) ? [COVERAGE_MESSAGES.presetTooSmall(label, preset, needs.minPreset)] : []];
13092
13151
  }, checkCoverage = (config, manifests) => manifests.flatMap((manifest) => coverageIssues(config, packageName(manifest.name), needsOf(manifest)));
13093
13152
  } });
13094
13153
  var init_dist2 = __esm({ "../../node_modules/@ignitify-integrations/core/dist/index.js"() {
@@ -13105,31 +13164,38 @@ function diffLines(before, after, path12 = []) {
13105
13164
  let had = Array.isArray(before) ? before : [], added = after.filter((item) => !had.some((old2) => (0, import_node_util.isDeepStrictEqual)(old2, item)));
13106
13165
  return added.length > 0 ? [`+ ${here}: ${added.map(itemText).join(", ")}`] : [];
13107
13166
  }
13108
- if (!isPlainObject(after)) return before === void 0 || (0, import_node_util.isDeepStrictEqual)(before, after) || INTEGRATION.NOTED_FIELDS.includes(here) ? [] : [`${here}: ${String(before)} -> ${String(after)}`];
13109
- let old = isPlainObject(before) ? before : {}, newScalars = [], nested = [];
13167
+ if (!isRecord(after)) return before === void 0 || (0, import_node_util.isDeepStrictEqual)(before, after) || INTEGRATION.NOTED_FIELDS.includes(here) ? [] : [`${here}: ${String(before)} -> ${String(after)}`];
13168
+ let old = isRecord(before) ? before : {}, newScalars = [], nested = [];
13110
13169
  for (let [key, value] of Object.entries(after)) {
13111
13170
  let fieldPath = [...path12, key];
13112
- old[key] === void 0 && !Array.isArray(value) && !isPlainObject(value) ? INTEGRATION.NOTED_FIELDS.includes(fieldPath.join(".")) || newScalars.push(`${key} ${String(value)}`) : nested.push(...diffLines(old[key], value, fieldPath));
13171
+ old[key] === void 0 && !Array.isArray(value) && !isRecord(value) ? INTEGRATION.NOTED_FIELDS.includes(fieldPath.join(".")) || newScalars.push(`${key} ${String(value)}`) : nested.push(...diffLines(old[key], value, fieldPath));
13113
13172
  }
13114
13173
  return [...newScalars.length > 0 ? [`+ ${here}: ${newScalars.join(", ")}`] : [], ...nested];
13115
13174
  }
13116
13175
  function planIntegrationAdd(config, manifest, version2, hasCode2) {
13117
- let before = asCoreConfig(config), headline = planHeadline(manifest, version2, hasCode2), outcome = planAdd(before, manifest);
13176
+ let headline = planHeadline(manifest, version2, hasCode2), outcome = planAdd(config, manifest);
13118
13177
  if (!outcome.ok) return { ok: false, headline, refusals: outcome.error };
13119
13178
  let after = outcome.value.config;
13120
- return { ok: true, headline, changes: [...outcome.value.notes, ...diffLines(before, after)], youAdd: outcome.value.youAdd, config: after, changed: !(0, import_node_util.isDeepStrictEqual)(before, after) };
13179
+ return { ok: true, headline, changes: [...outcome.value.notes, ...diffLines(config, after)], youAdd: outcome.value.youAdd, config: after, changed: !(0, import_node_util.isDeepStrictEqual)(config, after) };
13121
13180
  }
13122
13181
  function renderAddPlan(plan) {
13123
13182
  return [plan.headline, ...plan.changes.map((line) => ` ${line}`), ...plan.youAdd.length > 0 ? [INTEGRATION.YOU_ADD, ...plan.youAdd.map((line) => ` ${line}`)] : []];
13124
13183
  }
13125
- var import_node_util, asCoreConfig, isPlainObject, itemText, init_integration_plan = __esm({ "src/core/integration-plan.ts"() {
13184
+ var import_node_util, itemText, init_integration_plan = __esm({ "src/core/integration-plan.ts"() {
13126
13185
  "use strict";
13127
13186
  init_define_IGNITIFY_CLIENT_FILES();
13128
13187
  import_node_util = require("node:util");
13129
13188
  init_dist2();
13189
+ init_tenant();
13130
13190
  init_constants();
13131
- asCoreConfig = (config) => config, isPlainObject = (v) => typeof v == "object" && v !== null && !Array.isArray(v), itemText = (item) => isPlainObject(item) && typeof item.prefix == "string" ? item.prefix : typeof item == "string" ? item : JSON.stringify(item);
13191
+ itemText = (item) => isRecord(item) && typeof item.prefix == "string" ? item.prefix : isRecord(item) && typeof item.path == "string" ? item.path : typeof item == "string" ? item : JSON.stringify(item);
13132
13192
  } });
13193
+ function assessIntegrations(installed, config) {
13194
+ let missing = installed.filter((entry) => entry.read.kind === "missing").map((entry) => entry.pkg);
13195
+ if (missing.length > 0) return { ok: false, missing };
13196
+ let integrations = installed.flatMap(({ pkg, read }) => read.kind === "installed" ? [{ pkg, version: read.version, manifest: parseManifest(read.manifestText) }] : []), manifests = integrations.flatMap(({ manifest }) => manifest.ok ? [manifest.value] : []);
13197
+ return { ok: true, errors: [...integrations.flatMap(({ pkg, manifest }) => manifest.ok ? [] : manifest.error.map((issue) => INTEGRATION.BAD_MANIFEST(pkg, issue))), ...checkCoverage(config, manifests)], versions: integrations.map(({ pkg, version: version2 }) => ({ pkg, version: version2 })) };
13198
+ }
13133
13199
  function integrationDeps(packageJson) {
13134
13200
  let deps = packageJson?.dependencies;
13135
13201
  return typeof deps != "object" || deps === null ? [] : Object.keys(deps).filter((name) => name.startsWith(`${PACKAGE_SCOPE}/`));
@@ -13151,7 +13217,7 @@ var init_integration_coverage = __esm({ "src/core/integration-coverage.ts"() {
13151
13217
  function patch(doc, before, after, path12) {
13152
13218
  if ((0, import_node_util2.isDeepStrictEqual)(before, after)) return;
13153
13219
  let node = path12.length === 0 ? doc.contents : doc.getIn(path12, true);
13154
- if (isPlainObject2(after) && isPlainObject2(before) && (0, import_yaml2.isMap)(node)) {
13220
+ if (isRecord(after) && isRecord(before) && (0, import_yaml2.isMap)(node)) {
13155
13221
  for (let [key, value] of Object.entries(after)) patch(doc, before[key], value, [...path12, key]);
13156
13222
  return;
13157
13223
  }
@@ -13159,7 +13225,7 @@ function patch(doc, before, after, path12) {
13159
13225
  for (let item of after.slice(before.length)) node.add(doc.createNode(item));
13160
13226
  return;
13161
13227
  }
13162
- (0, import_yaml2.isScalar)(node) && !Array.isArray(after) && !isPlainObject2(after) ? node.value = after : doc.setIn(path12, doc.createNode(after));
13228
+ (0, import_yaml2.isScalar)(node) && !Array.isArray(after) && !isRecord(after) ? node.value = after : doc.setIn(path12, doc.createNode(after));
13163
13229
  }
13164
13230
  function renderTaskConfigText(text, before, after, isJson) {
13165
13231
  if ((0, import_node_util2.isDeepStrictEqual)(before, after)) return text;
@@ -13168,10 +13234,11 @@ function renderTaskConfigText(text, before, after, isJson) {
13168
13234
  let doc = (0, import_yaml2.parseDocument)(text, { schema: "core" });
13169
13235
  return patch(doc, before, after, []), doc.toString({ flowCollectionPadding: false });
13170
13236
  }
13171
- var import_node_util2, import_yaml2, isPlainObject2, init_task_yaml_edit = __esm({ "src/core/task-yaml-edit.ts"() {
13237
+ var import_node_util2, import_yaml2, init_task_yaml_edit = __esm({ "src/core/task-yaml-edit.ts"() {
13172
13238
  "use strict";
13173
13239
  init_define_IGNITIFY_CLIENT_FILES();
13174
- import_node_util2 = require("node:util"), import_yaml2 = __toESM(require_dist(), 1), isPlainObject2 = (v) => typeof v == "object" && v !== null && !Array.isArray(v);
13240
+ import_node_util2 = require("node:util"), import_yaml2 = __toESM(require_dist(), 1);
13241
+ init_tenant();
13175
13242
  } });
13176
13243
  function integrationPackage(manifestText, packageJson) {
13177
13244
  if (manifestText === null) return { kind: "not-integration" };
@@ -13180,25 +13247,25 @@ function integrationPackage(manifestText, packageJson) {
13180
13247
  }
13181
13248
  function readInstalledPackage(taskDir, pkg) {
13182
13249
  let dir = packageDir(taskDir, pkg);
13183
- if (!import_node_fs7.default.existsSync(dir)) return { kind: "missing" };
13184
- let manifestFile = import_node_path6.default.join(dir, MANIFEST_FILE);
13185
- return import_node_fs7.default.existsSync(manifestFile) ? integrationPackage(readText(manifestFile), readJson(import_node_path6.default.join(dir, INTEGRATION.PACKAGE_JSON))) : { kind: "not-integration" };
13250
+ if (!import_node_fs8.default.existsSync(dir)) return { kind: "missing" };
13251
+ let manifestFile = import_node_path7.default.join(dir, MANIFEST_FILE);
13252
+ return import_node_fs8.default.existsSync(manifestFile) ? integrationPackage(readText(manifestFile), readJson(import_node_path7.default.join(dir, INTEGRATION.PACKAGE_JSON))) : { kind: "not-integration" };
13186
13253
  }
13187
13254
  function readPackedPackage(packDir) {
13188
- let tarball = import_node_fs7.default.readdirSync(packDir).find((f) => f.endsWith(INTEGRATION.TARBALL_EXT));
13255
+ let tarball = import_node_fs8.default.readdirSync(packDir).find((f) => f.endsWith(INTEGRATION.TARBALL_EXT));
13189
13256
  if (tarball === void 0) return { kind: "missing" };
13190
- let file = import_node_path6.default.join(packDir, tarball);
13257
+ let file = import_node_path7.default.join(packDir, tarball);
13191
13258
  try {
13192
- let members = readTar((0, import_node_zlib.gunzipSync)(import_node_fs7.default.readFileSync(file), { maxOutputLength: INTEGRATION.PACKED_MAX_BYTES })), text = (name) => members.find((m) => m.kind === "file" && m.name === `${INTEGRATION.PACKED_ROOT}/${name}`)?.data.toString("utf8") ?? null, packageJson = text(INTEGRATION.PACKAGE_JSON);
13259
+ let members = readTar((0, import_node_zlib.gunzipSync)(import_node_fs8.default.readFileSync(file), { maxOutputLength: INTEGRATION.PACKED_MAX_BYTES })), text = (name) => members.find((m) => m.kind === "file" && m.name === `${INTEGRATION.PACKED_ROOT}/${name}`)?.data.toString("utf8") ?? null, packageJson = text(INTEGRATION.PACKAGE_JSON);
13193
13260
  return integrationPackage(text(MANIFEST_FILE), packageJson === null ? null : JSON.parse(packageJson));
13194
13261
  } catch (e) {
13195
13262
  let tooBig = e.code === INTEGRATION.ZLIB_TOO_BIG;
13196
- throw new Error(tooBig ? INTEGRATION.PACKED_TOO_BIG(file, INTEGRATION.PACKED_MAX_BYTES) : INTEGRATION.UNREADABLE(file, errorMessage(e)), { cause: e });
13263
+ throw new Error(tooBig ? INTEGRATION.PACKED_TOO_BIG(file, INTEGRATION.PACKED_MAX_BYTES) : INTEGRATION.UNREADABLE(file, errText(e)), { cause: e });
13197
13264
  }
13198
13265
  }
13199
13266
  function readTaskPackageJson(taskDir) {
13200
- let file = import_node_path6.default.join(taskDir, INTEGRATION.PACKAGE_JSON);
13201
- return import_node_fs7.default.existsSync(file) ? readJson(file) : null;
13267
+ let file = import_node_path7.default.join(taskDir, INTEGRATION.PACKAGE_JSON);
13268
+ return import_node_fs8.default.existsSync(file) ? readJson(file) : null;
13202
13269
  }
13203
13270
  async function fetchPackument(pkg, fetchImpl = fetch) {
13204
13271
  try {
@@ -13208,19 +13275,11 @@ async function fetchPackument(pkg, fetchImpl = fetch) {
13208
13275
  return { ok: false, reason: INTEGRATION.REGISTRY_UNREACHABLE };
13209
13276
  }
13210
13277
  }
13211
- function writeFileAtomic(file, text) {
13212
- let target = import_node_fs7.default.realpathSync(file), mode = import_node_fs7.default.statSync(target).mode & 511, name = `.${import_node_path6.default.basename(target)}.${process.pid}.${(0, import_node_crypto3.randomBytes)(6).toString("hex")}.tmp`, tmp = import_node_path6.default.join(import_node_path6.default.dirname(target), name);
13213
- try {
13214
- import_node_fs7.default.writeFileSync(tmp, text, { mode, flag: "wx" }), import_node_fs7.default.chmodSync(tmp, mode), import_node_fs7.default.renameSync(tmp, target);
13215
- } finally {
13216
- import_node_fs7.default.rmSync(tmp, { force: true });
13217
- }
13218
- }
13219
13278
  function readText(file) {
13220
13279
  try {
13221
- return import_node_fs7.default.readFileSync(file, "utf8");
13280
+ return import_node_fs8.default.readFileSync(file, "utf8");
13222
13281
  } catch (e) {
13223
- throw new Error(INTEGRATION.UNREADABLE(file, errorMessage(e)), { cause: e });
13282
+ throw new Error(INTEGRATION.UNREADABLE(file, errText(e)), { cause: e });
13224
13283
  }
13225
13284
  }
13226
13285
  function readJson(file) {
@@ -13228,19 +13287,23 @@ function readJson(file) {
13228
13287
  try {
13229
13288
  return JSON.parse(text);
13230
13289
  } catch (e) {
13231
- throw new Error(INTEGRATION.UNREADABLE(file, errorMessage(e)), { cause: e });
13290
+ throw new Error(INTEGRATION.UNREADABLE(file, errText(e)), { cause: e });
13232
13291
  }
13233
13292
  }
13234
- var import_node_child_process2, import_node_crypto3, import_node_fs7, import_node_path6, import_node_zlib, spawnNpm, packageDir, init_integrations = __esm({ "src/io/integrations.ts"() {
13293
+ var import_node_child_process2, import_node_fs8, import_node_path7, import_node_zlib, NPM_OUTPUT_MAX_BYTES, npmOutputToShow, spawnNpm, packageDir, init_integrations = __esm({ "src/io/integrations.ts"() {
13235
13294
  "use strict";
13236
13295
  init_define_IGNITIFY_CLIENT_FILES();
13237
- import_node_child_process2 = require("node:child_process"), import_node_crypto3 = require("node:crypto"), import_node_fs7 = __toESM(require("node:fs"), 1), import_node_path6 = __toESM(require("node:path"), 1), import_node_zlib = require("node:zlib");
13296
+ import_node_child_process2 = require("node:child_process"), import_node_fs8 = __toESM(require("node:fs"), 1), import_node_path7 = __toESM(require("node:path"), 1), import_node_zlib = require("node:zlib");
13238
13297
  init_dist2();
13239
13298
  init_tar();
13240
13299
  init_constants();
13241
13300
  init_integration_coverage();
13242
- init_guard();
13243
- spawnNpm = (cwd, args) => (0, import_node_child_process2.spawnSync)(INTEGRATION.NPM, args, { cwd, stdio: ["ignore", 2, 2] }).status, packageDir = (taskDir, pkg) => import_node_path6.default.join(taskDir, INTEGRATION.NODE_MODULES, ...pkg.split("/"));
13301
+ init_manager();
13302
+ init_tenant();
13303
+ NPM_OUTPUT_MAX_BYTES = 16 * 1024 * 1024, npmOutputToShow = (status, output) => status === 0 ? "" : output, spawnNpm = (cwd, args) => {
13304
+ let r = (0, import_node_child_process2.spawnSync)(INTEGRATION.NPM, args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", maxBuffer: NPM_OUTPUT_MAX_BYTES }), shown = npmOutputToShow(r.status, `${r.stdout ?? ""}${r.stderr ?? ""}`);
13305
+ return shown !== "" && tell(shown.trimEnd()), r.status;
13306
+ }, packageDir = (taskDir, pkg) => import_node_path7.default.join(taskDir, INTEGRATION.NODE_MODULES, ...pkg.split("/"));
13244
13307
  } });
13245
13308
  async function runIntegrationAdd(args, cwd = process.cwd(), deps = REAL_INTEGRATION_ADD_DEPS) {
13246
13309
  let name = requiredArg(args);
@@ -13254,16 +13317,17 @@ async function runIntegrationAdd(args, cwd = process.cwd(), deps = REAL_INTEGRAT
13254
13317
  let task = await prepareTask(args, cwd, slug2);
13255
13318
  if (!task) return 1;
13256
13319
  if (printWarnings([...task.warnings]), task.config.runtime !== INTEGRATION.RUNTIME) return fail(INTEGRATION.NOT_NODE(task.config.runtime)), 1;
13257
- if (readTaskPackageJson(task.taskDir) === null) return fail(INTEGRATION.NO_PACKAGE_JSON(task.taskDir)), 1;
13320
+ let taskDir = import_node_fs9.default.realpathSync(task.taskDir);
13321
+ if (readTaskPackageJson(taskDir) === null) return fail(INTEGRATION.NO_PACKAGE_JSON(taskDir)), 1;
13258
13322
  let pkg = packageName(name);
13259
- return guard(() => withPackDir((packDir) => addIntegration(args, task.taskDir, task.config, pkg, packDir, dryRun, deps)));
13323
+ return guard(() => withPackDir((packDir) => addIntegration(args, taskDir, task.config, pkg, packDir, dryRun, deps)));
13260
13324
  }
13261
13325
  async function withPackDir(fn) {
13262
- let packDir = import_node_fs8.default.mkdtempSync(import_node_path7.default.join(import_node_os3.default.tmpdir(), INTEGRATION.PACK_TMP_PREFIX));
13326
+ let packDir = import_node_fs9.default.mkdtempSync(import_node_path8.default.join(import_node_os3.default.tmpdir(), INTEGRATION.PACK_TMP_PREFIX));
13263
13327
  try {
13264
13328
  return await fn(packDir);
13265
13329
  } finally {
13266
- import_node_fs8.default.rmSync(packDir, { recursive: true, force: true });
13330
+ import_node_fs9.default.rmSync(packDir, { recursive: true, force: true });
13267
13331
  }
13268
13332
  }
13269
13333
  async function addIntegration(args, taskDir, config, pkg, packDir, dryRun, deps) {
@@ -13288,26 +13352,19 @@ async function addIntegration(args, taskDir, config, pkg, packDir, dryRun, deps)
13288
13352
  if (installCode !== 0) return fail(INTEGRATION.INSTALL_FAILED(spec, installCode)), 1;
13289
13353
  sayOk(INTEGRATION.INSTALLED(spec));
13290
13354
  }
13291
- return edit === null ? (emit({ ...result, written: false }), 0) : (writeFileAtomic(edit.file, edit.text), sayOk(INTEGRATION.WRITTEN(import_node_path7.default.basename(edit.file))), emit({ ...result, written: true }), 0);
13355
+ return edit === null ? (emit({ ...result, written: false }), 0) : (writeFileAtomic(edit.file, edit.text), sayOk(INTEGRATION.WRITTEN(import_node_path8.default.basename(edit.file))), emit({ ...result, written: true }), 0);
13292
13356
  }
13293
13357
  function taskConfigEdit(taskDir, config, planned) {
13294
13358
  let file = findTaskConfigFile(taskDir);
13295
13359
  if (file === null) throw new Error(INTEGRATION.UNREADABLE(taskDir, "no task.yaml"));
13296
- let source = import_node_path7.default.basename(file), text = renderTaskConfigText(import_node_fs8.default.readFileSync(file, "utf8"), config, planned, source.endsWith(".json")), check = checkTaskConfigText(text, source);
13360
+ let source = import_node_path8.default.basename(file), text = renderTaskConfigText(import_node_fs9.default.readFileSync(file, "utf8"), config, planned, source.endsWith(".json")), check = checkTaskConfigText(text, source);
13297
13361
  return !check.ok || !(0, import_node_util3.isDeepStrictEqual)(check.config, planned) ? (printErrors(check.errors), fail(INTEGRATION.WRITE_MISMATCH(source)), { ok: false }) : { ok: true, file, text };
13298
13362
  }
13299
13363
  async function checkIntegrations(taskDir, config, fetchImpl) {
13300
13364
  let pkgs = integrationDeps(readTaskPackageJson(taskDir));
13301
13365
  if (pkgs.length === 0) return true;
13302
- let installed = pkgs.map((pkg) => ({ pkg, read: readInstalledPackage(taskDir, pkg) })), missing = installed.filter((entry) => entry.read.kind === "missing").map((entry) => entry.pkg);
13303
- if (missing.length > 0) return fail(INTEGRATION.NOT_INSTALLED(missing)), false;
13304
- let manifests = [], errors = [], versions = [];
13305
- for (let { pkg, read } of installed) {
13306
- if (read.kind !== "installed") continue;
13307
- let manifest = parseManifest(read.manifestText);
13308
- manifest.ok ? manifests.push(manifest.value) : errors.push(...manifest.error.map((issue) => INTEGRATION.BAD_MANIFEST(pkg, issue))), versions.push({ pkg, version: read.version });
13309
- }
13310
- return await warnObsolete(versions, fetchImpl), errors.push(...checkCoverage(asCoreConfig(config), manifests)), printErrors(errors), errors.length === 0;
13366
+ let installed = pkgs.map((pkg) => ({ pkg, read: readInstalledPackage(taskDir, pkg) })), assessed = assessIntegrations(installed, config);
13367
+ return assessed.ok ? (await warnObsolete(assessed.versions, fetchImpl), printErrors(assessed.errors), assessed.errors.length === 0) : (fail(INTEGRATION.NOT_INSTALLED(assessed.missing)), false);
13311
13368
  }
13312
13369
  async function warnObsolete(versions, fetchImpl) {
13313
13370
  let lookups = await Promise.all(versions.map(async (entry) => ({ ...entry, lookup: await fetchPackument(entry.pkg, fetchImpl) }))), skipped = /* @__PURE__ */ new Set();
@@ -13321,16 +13378,17 @@ async function warnObsolete(versions, fetchImpl) {
13321
13378
  }
13322
13379
  for (let reason of skipped) warn(INTEGRATION.OBSOLETE_SKIPPED(reason));
13323
13380
  }
13324
- var import_node_fs8, import_node_os3, import_node_path7, import_node_util3, REAL_INTEGRATION_ADD_DEPS, NAME_RE, init_integration = __esm({ "src/commands/integration.ts"() {
13381
+ var import_node_fs9, import_node_os3, import_node_path8, import_node_util3, REAL_INTEGRATION_ADD_DEPS, NAME_RE, init_integration = __esm({ "src/commands/integration.ts"() {
13325
13382
  "use strict";
13326
13383
  init_define_IGNITIFY_CLIENT_FILES();
13327
- import_node_fs8 = __toESM(require("node:fs"), 1), import_node_os3 = __toESM(require("node:os"), 1), import_node_path7 = __toESM(require("node:path"), 1), import_node_util3 = require("node:util");
13384
+ import_node_fs9 = __toESM(require("node:fs"), 1), import_node_os3 = __toESM(require("node:os"), 1), import_node_path8 = __toESM(require("node:path"), 1), import_node_util3 = require("node:util");
13328
13385
  init_dist2();
13329
13386
  init_constants();
13330
13387
  init_integration_plan();
13331
13388
  init_integration_coverage();
13332
13389
  init_task_yaml_edit();
13333
13390
  init_integrations();
13391
+ init_fs_atomic();
13334
13392
  init_prompt();
13335
13393
  init_task_config3();
13336
13394
  init_task_path();
@@ -13345,24 +13403,24 @@ async function printPackageAdvice(args, config, fetchImpl) {
13345
13403
  let outcome = await postPackageAdvice(apiConfig(args), plan.request, fetchImpl), { lines, fault } = renderAdvice(outcome, screenStyle());
13346
13404
  for (let line of lines) (fault ? tell : say)(line);
13347
13405
  } catch (e) {
13348
- fail(errorMessage(e));
13406
+ fail(errText(e));
13349
13407
  }
13350
13408
  }
13351
13409
  async function runTaskValidate(args, cwd = process.cwd(), fetchImpl) {
13352
13410
  let task = await prepareTask(args, cwd);
13353
- return !task || (printWarnings([...task.warnings]), !await checkIntegrations(task.taskDir, task.config, fetchImpl)) ? 1 : (emit({ task_id: task.config.slug ?? import_path4.default.basename(task.taskDir), valid: true }, (st) => successLine(st, VALIDATE.VALID)), boolFlag(args, ADVICE.FLAG) && await printPackageAdvice(args, task.config, fetchImpl), 0);
13411
+ return !task || (printWarnings([...task.warnings]), !await checkIntegrations(task.taskDir, task.config, fetchImpl)) ? 1 : (emit({ task_id: task.slug, valid: true }, (st) => successLine(st, VALIDATE.VALID)), boolFlag(args, ADVICE.FLAG) && await printPackageAdvice(args, task.config, fetchImpl), 0);
13354
13412
  }
13355
13413
  async function runTaskGet(args, cwd = process.cwd()) {
13356
13414
  let task = await prepareTask(args, cwd);
13357
13415
  if (!task) return 1;
13358
- let slug2 = task.config.slug ?? import_path4.default.basename(task.taskDir), state = readState(task.taskDir), check = checkSourceUpload(task.taskDir), localContentHash = check.ok ? check.source.contentHash : null, cfg = offlineTolerantConfig(args), orNull = (p) => p.catch(() => null), [whoami, deploymentsRes, runsRes] = cfg ? await Promise.all([orNull(getWhoami(cfg)), orNull(listApiDeploymentsForTask(cfg, slug2, void 0, { limit: TASK_REPORT_PAGE })), orNull(listApiRuns(cfg, { task_name: slug2, limit: String(TASK_REPORT_PAGE) }))]) : [null, null, null], report = buildTaskReport({ slug: slug2, declared: task.config.execution, declaredPreset: task.config.run?.preset, stateDeploymentId: state.deployment_id ?? null, localContentHash, deployments: deploymentsRes?.deployments ?? null, runs: runsRes?.runs ?? null, pageLimit: TASK_REPORT_PAGE, usage: whoami?.usage ?? null });
13416
+ let { slug: slug2 } = task, state = readState(task.taskDir), check = checkSourceUpload(task.taskDir), localContentHash = check.ok ? check.source.contentHash : null, cfg = offlineTolerantConfig(args), orNull = (p) => p.catch(() => null), [whoami, deploymentsRes, runsRes] = cfg ? await Promise.all([orNull(getWhoami(cfg)), orNull(listApiDeploymentsForTask(cfg, slug2, void 0, { limit: TASK_REPORT_PAGE })), orNull(listApiRuns(cfg, { task_name: slug2, limit: String(TASK_REPORT_PAGE) }))]) : [null, null, null], report = buildTaskReport({ slug: slug2, declared: task.config.execution, declaredPreset: task.config.run?.preset, stateDeploymentId: state.deployment_id ?? null, localContentHash, deployments: deploymentsRes?.deployments ?? null, runs: runsRes?.runs ?? null, pageLimit: TASK_REPORT_PAGE, usage: whoami?.usage ?? null });
13359
13417
  return emit(snakeCaseKeys(report), () => formatTaskReport(report)), 0;
13360
13418
  }
13361
13419
  function offlineTolerantConfig(args) {
13362
13420
  try {
13363
13421
  return apiConfig(args);
13364
13422
  } catch (e) {
13365
- return warn(RESULT_MESSAGES.TASK_GET_OFFLINE(errorMessage(e))), null;
13423
+ return warn(RESULT_MESSAGES.TASK_GET_OFFLINE(errText(e))), null;
13366
13424
  }
13367
13425
  }
13368
13426
  async function surveyTaskContents(cfg, taskRef) {
@@ -13409,7 +13467,7 @@ async function runTaskRun(args, cwd = process.cwd()) {
13409
13467
  printWarnings([...task.warnings]);
13410
13468
  let opts = parseRunOptions(args, task.config);
13411
13469
  if (!opts.ok) return fail(opts.error), 1;
13412
- let { run: run2 } = opts, taskName = task.config.slug ?? import_path4.default.basename(task.taskDir), cfg = apiConfig(args);
13470
+ let { run: run2 } = opts, taskName = task.slug, cfg = apiConfig(args);
13413
13471
  if (payloadBytes(run2.payload) > PAYLOAD_LIMIT_FALLBACK_BYTES) {
13414
13472
  let refusal = payloadRefusal(run2.payload, await payloadLimit(cfg));
13415
13473
  if (refusal !== null) return fail(refusal), 1;
@@ -13428,26 +13486,37 @@ async function runTaskRun(args, cwd = process.cwd()) {
13428
13486
  } catch (e) {
13429
13487
  if (isJsonOutput()) throw e;
13430
13488
  let trace = traceLine(e);
13431
- if (isPayloadTooLarge(errorMessage(e))) return fail(MESSAGES.PAYLOAD_REFUSED_BY_SERVER(await payloadLimit(cfg)), { trace }), 1;
13432
- let explained = explainRunRefusal(errorMessage(e)) ?? explainLimitRefusal(errorMessage(e));
13489
+ if (isPayloadTooLarge(e)) return fail(MESSAGES.PAYLOAD_REFUSED_BY_SERVER(await payloadLimit(cfg)), { trace }), 1;
13490
+ let explained = explainRunRefusal(e) ?? explainLimitRefusal(e);
13433
13491
  if (!explained) throw e;
13434
13492
  return tell(explained), trace !== null && tell(errStyle().dim(trace)), 1;
13435
13493
  }
13436
- if (run2.at !== void 0) {
13437
- if (!res.schedule_id) return fail(res.run_id ? `this server does not support scheduling \u2014 it IGNORED --at and ran the job NOW (run ${res.run_id}). Cancel it with 'ignitify run:cancel ${res.run_id}' if that is not what you wanted.` : "this server does not support scheduling (no schedule_id in the response)."), 1;
13438
- if (isJsonOutput()) return emit(res), 0;
13439
- let st = outStyle();
13440
- out(successLine(st, `Scheduled: ${res.schedule_id} fires at ${res.fires_at ?? run2.at}`)), out(st.dim(`No run exists until it fires. List with 'ignitify schedule:list'; cancel with 'ignitify schedule:cancel ${res.schedule_id}'.`));
13441
- let scheduledNotices = renderRunNotices(res.notices);
13442
- scheduledNotices && out(st.yellow(scheduledNotices));
13443
- let capacity2 = renderCapacity(res.capacity, st);
13444
- return capacity2 && out(capacity2), 0;
13445
- }
13446
- if (!opts.wait || !res.run_id) return CliOutputManager.render("task:run", { ...res }), 0;
13447
- let notices = isJsonOutput() ? "" : renderRunNotices(res.notices);
13448
- notices && out(outStyle().yellow(notices));
13449
- let capacity = isJsonOutput() ? "" : renderCapacity(res.capacity, outStyle());
13450
- return capacity && out(capacity), waitForRun(cfg, res.run_id);
13494
+ let answer = classifyRunAnswer(res, run2.at, opts.wait);
13495
+ switch (answer.kind) {
13496
+ case "skew-ran-now":
13497
+ return fail(`this server does not support scheduling \u2014 it IGNORED --at and ran the job NOW (run ${answer.runId}). Cancel it with 'ignitify run:cancel ${answer.runId}' if that is not what you wanted.`), 1;
13498
+ case "skew-no-schedule":
13499
+ return fail("this server does not support scheduling (no schedule_id in the response)."), 1;
13500
+ case "scheduled": {
13501
+ if (isJsonOutput()) return emit(res), 0;
13502
+ let st = outStyle();
13503
+ out(successLine(st, `Scheduled: ${answer.scheduleId} fires at ${answer.firesAt}`)), out(st.dim(`No run exists until it fires. List with 'ignitify schedule:list'; cancel with 'ignitify schedule:cancel ${answer.scheduleId}'.`));
13504
+ let scheduledNotices = renderRunNotices(res.notices);
13505
+ scheduledNotices && out(st.yellow(scheduledNotices));
13506
+ let capacity = renderCapacity(res.capacity, st);
13507
+ return capacity && out(capacity), 0;
13508
+ }
13509
+ case "submitted":
13510
+ return CliOutputManager.render("task:run", { ...res }), 0;
13511
+ case "wait": {
13512
+ let notices = isJsonOutput() ? "" : renderRunNotices(res.notices);
13513
+ notices && out(outStyle().yellow(notices));
13514
+ let capacity = isJsonOutput() ? "" : renderCapacity(res.capacity, outStyle());
13515
+ return capacity && out(capacity), waitForRun(cfg, answer.runId);
13516
+ }
13517
+ default:
13518
+ return answer;
13519
+ }
13451
13520
  });
13452
13521
  }
13453
13522
  async function runTaskCreate(args, cwd = process.cwd()) {
@@ -13473,12 +13542,8 @@ async function runTaskCreate(args, cwd = process.cwd()) {
13473
13542
  return CliOutputManager.render("task:create", { ...res, slug: taskId, ...local_dir ? { local_dir } : {} }), say(screenStyle().dim(limitsNote())), 0;
13474
13543
  });
13475
13544
  }
13476
- async function runTaskList(args, _cwd = process.cwd()) {
13477
- let badLimit = limitRefusal(args);
13478
- return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
13479
- let cfg = apiConfig(args), data = await listApiTasks(cfg, flagParams(args, PAGE_FLAG_PARAMS));
13480
- return CliOutputManager.render("task:list", data.tasks), noteNextPage(args, { command: COMMANDS.TASK_LIST, flags: PAGE_FLAG_PARAMS }, data.tasks), 0;
13481
- });
13545
+ function runTaskList(args, _cwd = process.cwd()) {
13546
+ return runPagedList(args, { list: { command: COMMANDS.TASK_LIST, flags: PAGE_FLAG_PARAMS }, fetchPage: listApiTasks, rowsOf: (data) => data.tasks, show: (data) => CliOutputManager.render("task:list", data.tasks) });
13482
13547
  }
13483
13548
  async function runTaskDelete(args, cwd = process.cwd(), prompter = ttyPrompter) {
13484
13549
  let id = requiredArg(args);
@@ -13495,21 +13560,22 @@ async function runTaskDelete(args, cwd = process.cwd(), prompter = ttyPrompter)
13495
13560
  try {
13496
13561
  import_fs4.default.rmSync(link, { recursive: true, force: true }), say(`${MESSAGES.TASK_LINK_REMOVED}: ${link}`);
13497
13562
  } catch (e) {
13498
- warn(MESSAGES.TASK_LINK_NOT_REMOVED(link, errorMessage(e)));
13563
+ warn(MESSAGES.TASK_LINK_NOT_REMOVED(link, errText(e)));
13499
13564
  }
13500
13565
  }
13501
13566
  return emit({ plan: snakeCaseKeys(plan), deleted: d ?? null }), 0;
13502
13567
  });
13503
13568
  }
13504
- var import_fs4, import_path4, TASK_REPORT_PAGE, init_task = __esm({ "src/commands/task.ts"() {
13569
+ var import_fs4, TASK_REPORT_PAGE, init_task = __esm({ "src/commands/task.ts"() {
13505
13570
  "use strict";
13506
13571
  init_define_IGNITIFY_CLIENT_FILES();
13507
- import_fs4 = __toESM(require("fs"), 1), import_path4 = __toESM(require("path"), 1);
13572
+ import_fs4 = __toESM(require("fs"), 1);
13508
13573
  init_prompt();
13509
13574
  init_api();
13510
13575
  init_plan_task_delete();
13511
13576
  init_resolve_deployment();
13512
13577
  init_run_options();
13578
+ init_run_answer();
13513
13579
  init_state();
13514
13580
  init_constants();
13515
13581
  init_payload_limit();
@@ -13527,12 +13593,8 @@ var import_fs4, import_path4, TASK_REPORT_PAGE, init_task = __esm({ "src/command
13527
13593
  init_shared();
13528
13594
  TASK_REPORT_PAGE = 100;
13529
13595
  } });
13530
- async function runDeploymentList(args) {
13531
- let badLimit = limitRefusal(args);
13532
- return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
13533
- let cfg = apiConfig(args), data = await listApiDeployments(cfg, flagParams(args, PAGE_FLAG_PARAMS));
13534
- return CliOutputManager.render("deployment:list", data), noteNextPage(args, { command: COMMANDS.DEPLOYMENT_LIST, flags: PAGE_FLAG_PARAMS }, data.deployments), 0;
13535
- });
13596
+ function runDeploymentList(args) {
13597
+ return runPagedList(args, { list: { command: COMMANDS.DEPLOYMENT_LIST, flags: PAGE_FLAG_PARAMS }, fetchPage: listApiDeployments, rowsOf: (data) => data.deployments, show: (data) => CliOutputManager.render("deployment:list", data) });
13536
13598
  }
13537
13599
  async function runDeploymentGet(args) {
13538
13600
  let id = requireArg(args, "deployment-id");
@@ -13549,7 +13611,7 @@ async function runDeploymentDelete(args, prompter = ttyPrompter) {
13549
13611
  let id = requireArg(args, "deployment-id");
13550
13612
  return id ? guard(async () => {
13551
13613
  let cfg = apiConfig(args);
13552
- if (say(`Will soft-delete deployment ${id} and all its runs (cascade).`), !await confirmOrAbort(args, prompter)) return 1;
13614
+ if (say(RESULT_MESSAGES.DEPLOYMENT_DELETE_PREVIEW(id)), !await confirmOrAbort(args, prompter)) return 1;
13553
13615
  let res = await deleteDeployment(cfg, id);
13554
13616
  emit(res);
13555
13617
  let d = res.deleted;
@@ -13577,12 +13639,8 @@ var WHOLE_MS, init_grace = __esm({ "src/core/grace.ts"() {
13577
13639
  init_constants();
13578
13640
  WHOLE_MS = /^\d+$/;
13579
13641
  } });
13580
- async function runRunList(args) {
13581
- let badLimit = limitRefusal(args);
13582
- return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
13583
- let cfg = apiConfig(args), data = await listApiRuns(cfg, flagParams(args, RUN_LIST_FLAG_PARAMS));
13584
- return CliOutputManager.render("run:list", data.runs), noteNextPage(args, { command: COMMANDS.RUN_LIST, flags: RUN_LIST_FLAG_PARAMS }, data.runs), 0;
13585
- });
13642
+ function runRunList(args) {
13643
+ return runPagedList(args, { list: { command: COMMANDS.RUN_LIST, flags: RUN_LIST_FLAG_PARAMS }, fetchPage: listApiRuns, rowsOf: (data) => data.runs, show: (data) => CliOutputManager.render("run:list", data.runs) });
13586
13644
  }
13587
13645
  async function runRunGet(args) {
13588
13646
  let id = requireArg(args, "run-id");
@@ -13602,7 +13660,7 @@ async function runRunDelete(args, prompter = ttyPrompter) {
13602
13660
  let id = requireArg(args, "run-id");
13603
13661
  return id ? guard(async () => {
13604
13662
  let cfg = apiConfig(args);
13605
- return say(`Will soft-delete run ${id}.`), await confirmOrAbort(args, prompter) ? (emit(await deleteRun(cfg, id)), sayOk(`${MESSAGES.RUN_DELETED}: ${id}`), 0) : 1;
13663
+ return say(RESULT_MESSAGES.RUN_DELETE_PREVIEW(id)), await confirmOrAbort(args, prompter) ? (emit(await deleteRun(cfg, id)), sayOk(`${MESSAGES.RUN_DELETED}: ${id}`), 0) : 1;
13606
13664
  }) : 1;
13607
13665
  }
13608
13666
  async function runRunCancel(args, prompter = ttyPrompter) {
@@ -13613,16 +13671,16 @@ async function runRunCancel(args, prompter = ttyPrompter) {
13613
13671
  let grace = parseGraceMs(strOpt(args, "grace"), force);
13614
13672
  if (!grace.ok) return fail(grace.error), 1;
13615
13673
  let graceMs = grace.graceMs, cfg = apiConfig(args), run2 = await getApiRun(cfg, id);
13616
- if (run2?.status && isTerminalRunStatus(run2.status)) return emit({ run_id: id, outcome: RUN_CANCEL_OUTCOME.ALREADY_TERMINAL, status: run2.status }), say(`Run ${id} is already ${run2.status} \u2014 nothing to cancel.`), 0;
13617
- if (say(force ? `Will cancel run ${id} immediately (force: destroys the VM even if it is pinned).` : `Will ask run ${id} to stop gracefully; it is killed if it does not finish in time.`), !await confirmOrAbort(args, prompter)) return 1;
13674
+ if (run2?.status && isTerminalRunStatus(run2.status)) return emit({ run_id: id, outcome: RUN_CANCEL_OUTCOME.ALREADY_TERMINAL, status: run2.status }), say(RESULT_MESSAGES.RUN_ALREADY_OVER(id, run2.status)), 0;
13675
+ if (say(RESULT_MESSAGES.RUN_CANCEL_PREVIEW(id, force)), !await confirmOrAbort(args, prompter)) return 1;
13618
13676
  let res = await cancelApiRun(cfg, id, force, graceMs);
13619
13677
  emit({ run_id: id, outcome: res?.cancelled?.draining ? RUN_CANCEL_OUTCOME.DRAINING : RUN_CANCEL_OUTCOME.CANCELLED, ...res });
13620
13678
  let prior = res?.cancelled?.prior_state, was = prior ? ` (was ${prior})` : "";
13621
13679
  if (res?.cancelled?.draining) {
13622
13680
  let armed = res.cancelled.grace_ms, window = armed === void 0 ? "" : ` (grace ${armed} ms)`;
13623
- return say(`Run ${id} is stopping${window}${was} \u2014 still RUNNING until it finishes or the grace expires.`), 0;
13681
+ return say(RESULT_MESSAGES.RUN_STOPPING(id, window, was)), 0;
13624
13682
  }
13625
- return sayOk(`Run cancelled: ${id}${was}`), 0;
13683
+ return sayOk(RESULT_MESSAGES.RUN_CANCELLED(id, was)), 0;
13626
13684
  });
13627
13685
  }
13628
13686
  var init_run = __esm({ "src/commands/run.ts"() {
@@ -13638,8 +13696,8 @@ var init_run = __esm({ "src/commands/run.ts"() {
13638
13696
  } });
13639
13697
  function decideVmDelete(vm, force, vmId) {
13640
13698
  if (vm.residency === "pinned" && !force) {
13641
- let ageH = vm.age_ms !== void 0 ? (vm.age_ms / 36e5).toFixed(1) : "?";
13642
- return { refuse: true, message: `Refusing to delete ${vmId}: this VM is PINNED (permanent standby, alive ${ageH}h).
13699
+ let age = vm.age_ms !== void 0 ? formatDuration(vm.age_ms) : "?";
13700
+ return { refuse: true, message: `Refusing to delete ${vmId}: this VM is PINNED (permanent standby, alive ${age}).
13643
13701
  Deleting it is exactly what "permanent" protects against \u2014 re-run with --force if you mean it.` };
13644
13702
  }
13645
13703
  let pinnedNote = vm.residency === "pinned" ? " (PINNED \u2014 --force given)" : "", busyNote = vm.state === "busy" ? " \u2014 BUSY: its in-flight run will FAIL" : "";
@@ -13648,15 +13706,10 @@ Deleting it is exactly what "permanent" protects against \u2014 re-run with --fo
13648
13706
  var init_decide_vm_delete = __esm({ "src/core/decide-vm-delete.ts"() {
13649
13707
  "use strict";
13650
13708
  init_define_IGNITIFY_CLIENT_FILES();
13709
+ init_output();
13651
13710
  } });
13652
- async function runVmList(args) {
13653
- let badLimit = limitRefusal(args);
13654
- return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
13655
- let cfg = apiConfig(args), params = flagParams(args, VM_LIST_FLAG_PARAMS);
13656
- boolFlag(args, "pinned") && (params.residency = "pinned");
13657
- let data = await listApiVms(cfg, params);
13658
- return CliOutputManager.render("vm:list", data), noteNextPage(args, VM_LIST_PAGE, data.vms ?? []), 0;
13659
- });
13711
+ function runVmList(args) {
13712
+ return runPagedList(args, { list: VM_LIST_PAGE, fetchPage: listApiVms, extra: boolFlag(args, "pinned") ? { residency: "pinned" } : {}, rowsOf: (data) => data.vms ?? [], show: (data) => CliOutputManager.render("vm:list", data) });
13660
13713
  }
13661
13714
  async function runVmGet(args) {
13662
13715
  let id = requireArg(args, "vm-id");
@@ -13712,7 +13765,7 @@ async function runSecretDelete(args, prompter = ttyPrompter) {
13712
13765
  let name = args.positionals[0];
13713
13766
  return name ? guard(async () => {
13714
13767
  let cfg = apiConfig(args);
13715
- if (tell(`Will delete secret ${name}. Its value cannot be read back.`), !await confirmOrAbort(args, prompter)) return 1;
13768
+ if (tell(RESULT_MESSAGES.SECRET_DELETE_PREVIEW(name)), !await confirmOrAbort(args, prompter)) return 1;
13716
13769
  let res = await rmSecret(cfg, name);
13717
13770
  return emit(res), sayOk(MESSAGES.SECRET_REMOVED(name)), 0;
13718
13771
  }) : (tell(MESSAGES.SECRET_DELETE_USAGE), 1);
@@ -13739,11 +13792,11 @@ async function runScheduleCancel(args, prompter = ttyPrompter) {
13739
13792
  let id = requireArg(args, "schedule-id");
13740
13793
  return id ? guard(async () => {
13741
13794
  let cfg = apiConfig(args);
13742
- if (say(`Will cancel schedule ${id} \u2014 it will never fire.`), !await confirmOrAbort(args, prompter)) return 1;
13795
+ if (say(RESULT_MESSAGES.SCHEDULE_CANCEL_PREVIEW(id)), !await confirmOrAbort(args, prompter)) return 1;
13743
13796
  try {
13744
13797
  await deleteSchedule(cfg, id);
13745
13798
  } catch (e) {
13746
- if (e instanceof HttpError && e.status === 404) return fail(`Schedule ${id} was NOT cancelled: not found, already fired, or currently firing.`), 1;
13799
+ if (e instanceof HttpError && e.status === 404) return fail(RESULT_MESSAGES.SCHEDULE_NOT_CANCELLED(id)), 1;
13747
13800
  throw e;
13748
13801
  }
13749
13802
  return emit({ schedule_id: id, cancelled: true }), sayOk(RESULT_MESSAGES.SCHEDULE_CANCELLED(id)), 0;
@@ -13760,11 +13813,11 @@ var init_schedule = __esm({ "src/commands/schedule.ts"() {
13760
13813
  init_shared();
13761
13814
  } });
13762
13815
  function withAbsoluteUrl(created, baseUrl) {
13763
- let url = created.url;
13816
+ let { url } = created;
13764
13817
  return typeof url != "string" || !url.startsWith("/") ? created : { ...created, url: `${baseUrl.replace(/\/+$/, "")}${url}` };
13765
13818
  }
13766
13819
  function renderHookCreated(st, created) {
13767
- let { secret, ...rest } = created, rows = secret === void 0 ? rest : { ...rest, [RESULT_MESSAGES.HOOK_SECRET_KEY]: secret }, paint2 = { [RESULT_MESSAGES.HOOK_SECRET_KEY]: (s) => st.bold(st.yellow(s)) };
13820
+ let { secret, ...rest } = created, rows = { ...rest, [RESULT_MESSAGES.HOOK_SECRET_KEY]: secret }, paint2 = { [RESULT_MESSAGES.HOOK_SECRET_KEY]: (s) => st.bold(st.yellow(s)) };
13768
13821
  return `
13769
13822
  ${heading(st, RESULT_MESSAGES.HOOK_CREATED_TITLE, true)}
13770
13823
  ${renderFields(st, rows, void 0, paint2)}
@@ -13775,7 +13828,7 @@ async function runHookCreate(args) {
13775
13828
  if (!deploymentId) return tell(MESSAGES.HOOK_CREATE_USAGE), 1;
13776
13829
  let taskName = args.options["task-name"];
13777
13830
  return typeof taskName != "string" || taskName.trim() === "" ? (fail(MESSAGES.HOOK_TASK_NAME_REQUIRED), 1) : guard(async () => {
13778
- let cfg = apiConfig(args), created = withAbsoluteUrl(asRecord(await createHook(cfg, deploymentId, { task_name: taskName })), cfg.baseUrl);
13831
+ let cfg = apiConfig(args), created = withAbsoluteUrl(await createHook(cfg, deploymentId, { task_name: taskName }), cfg.baseUrl);
13779
13832
  return warn(MESSAGES.HOOK_SECRET_SHOWN_ONCE), tell(MESSAGES.HOOK_PAYLOAD_LIMIT(formatLimit(await payloadLimit(cfg)))), emit(created, (st) => renderHookCreated(st, created)), 0;
13780
13833
  });
13781
13834
  }
@@ -13784,15 +13837,15 @@ async function runHookList(args) {
13784
13837
  if (!deploymentId) return tell(MESSAGES.HOOK_LIST_USAGE), 1;
13785
13838
  let badLimit = limitRefusal(args);
13786
13839
  return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
13787
- let cfg = apiConfig(args), res = asRecord(await listHooks(cfg, deploymentId, flagParams(args, PAGE_FLAG_PARAMS))), hooks = Array.isArray(res.hooks) ? res.hooks : [];
13788
- return emit(res, (st) => renderList(hooks, st, EMPTY_LISTS.HOOKS)), noteNextPage(args, { command: COMMANDS.HOOK_LIST, flags: PAGE_FLAG_PARAMS }, hooks), 0;
13840
+ let cfg = apiConfig(args), res = await listHooks(cfg, deploymentId, flagParams(args, PAGE_FLAG_PARAMS)), hooks = Array.isArray(res.hooks) ? res.hooks : [];
13841
+ return emit(res, (st) => renderList(hooks.map((h) => ({ ...h })), st, EMPTY_LISTS.HOOKS)), noteNextPage(args, { command: COMMANDS.HOOK_LIST, flags: PAGE_FLAG_PARAMS }, hooks), 0;
13789
13842
  });
13790
13843
  }
13791
13844
  async function runHookDelete(args, prompter = ttyPrompter) {
13792
13845
  let hookId = args.positionals[0];
13793
13846
  return hookId ? guard(async () => {
13794
13847
  let cfg = apiConfig(args);
13795
- return tell(`Will delete webhook ${hookId}. A sender that posts to it is refused from then on.`), await confirmOrAbort(args, prompter) ? (emit(await deleteHook(cfg, hookId)), sayOk(RESULT_MESSAGES.HOOK_DELETED(hookId)), 0) : 1;
13848
+ return tell(RESULT_MESSAGES.HOOK_DELETE_PREVIEW(hookId)), await confirmOrAbort(args, prompter) ? (emit(await deleteHook(cfg, hookId)), sayOk(RESULT_MESSAGES.HOOK_DELETED(hookId)), 0) : 1;
13796
13849
  }) : (tell(MESSAGES.HOOK_DELETE_USAGE), 1);
13797
13850
  }
13798
13851
  async function runNotifySet(args) {
@@ -13801,10 +13854,10 @@ async function runNotifySet(args) {
13801
13854
  if (args.options.secret !== void 0) return fail(MESSAGES.NOTIFY_SECRET_ON_ARGV), 1;
13802
13855
  let refusal = notifySetRefusal(args, url !== void 0, clear);
13803
13856
  if (refusal !== null) return refuseNotifySet(refusal);
13804
- let secret;
13805
- return args.options["secret-stdin"] === true && (secret = import_fs6.default.readFileSync(0, "utf8").replace(/\n$/, ""), secret === "") ? refuseNotifySet(MESSAGES.NOTIFY_SECRET_EMPTY) : guard(async () => {
13806
- let cfg = apiConfig(args), body = { url: clear ? null : url };
13807
- return secret !== void 0 && (body.secret = secret), emit(await setNotify(cfg, deploymentId, body)), sayOk(body.url === null ? RESULT_MESSAGES.NOTIFY_CLEARED(deploymentId) : RESULT_MESSAGES.NOTIFY_SET(deploymentId, body.url)), 0;
13857
+ let secret = args.options["secret-stdin"] === true ? import_fs6.default.readFileSync(0, "utf8").replace(/\n$/, "") : void 0;
13858
+ return secret === "" ? refuseNotifySet(MESSAGES.NOTIFY_SECRET_EMPTY) : guard(async () => {
13859
+ let cfg = apiConfig(args), target = clear || typeof url != "string" ? null : url;
13860
+ return emit(await setNotify(cfg, deploymentId, { url: target, ...secret !== void 0 ? { secret } : {} })), sayOk(target === null ? RESULT_MESSAGES.NOTIFY_CLEARED(deploymentId) : RESULT_MESSAGES.NOTIFY_SET(deploymentId, target)), 0;
13808
13861
  });
13809
13862
  }
13810
13863
  function notifySetRefusal(args, hasUrl, clear) {
@@ -13814,7 +13867,7 @@ function notifySetRefusal(args, hasUrl, clear) {
13814
13867
  function refuseNotifySet(reason) {
13815
13868
  return fail(reason), tell(MESSAGES.NOTIFY_SET_USAGE), 1;
13816
13869
  }
13817
- var import_fs6, asRecord, init_hook = __esm({ "src/commands/hook.ts"() {
13870
+ var import_fs6, init_hook = __esm({ "src/commands/hook.ts"() {
13818
13871
  "use strict";
13819
13872
  init_define_IGNITIFY_CLIENT_FILES();
13820
13873
  import_fs6 = __toESM(require("fs"), 1);
@@ -13825,7 +13878,6 @@ var import_fs6, asRecord, init_hook = __esm({ "src/commands/hook.ts"() {
13825
13878
  init_utils();
13826
13879
  init_shared();
13827
13880
  init_output();
13828
- asRecord = (v) => typeof v == "object" && v !== null && !Array.isArray(v) ? v : {};
13829
13881
  } });
13830
13882
  var init_commands = __esm({ "src/commands/index.ts"() {
13831
13883
  "use strict";
@@ -13850,9 +13902,7 @@ function allowedFlags(meta) {
13850
13902
  return { ...meta.flags, ...GLOBAL_FLAGS };
13851
13903
  }
13852
13904
  function parserSpec(meta) {
13853
- let bools = new Set(Object.keys(GLOBAL_FLAGS));
13854
- if (meta !== void 0 && meta.secretArgv !== true) for (let [flag, kind] of Object.entries(meta.flags)) kind === "bool" && bools.add(flag);
13855
- return { boolFlags: bools };
13905
+ return boolFlagSpec(meta === void 0 || meta.secretArgv === true ? GLOBAL_FLAGS : allowedFlags(meta));
13856
13906
  }
13857
13907
  function parseInvocation(argv) {
13858
13908
  return parseArgs([...argv], parserSpec(metaFor(argv[0] ?? "")));
@@ -13893,12 +13943,11 @@ function planInvocation(argv) {
13893
13943
  let refusal = checkInvocation(command, meta, pass);
13894
13944
  return refusal === null ? { kind: "run", command, args } : { kind: "error", message: refusal };
13895
13945
  }
13896
- var looksShort, init_invocation2 = __esm({ "src/core/invocation.ts"() {
13946
+ var init_invocation2 = __esm({ "src/core/invocation.ts"() {
13897
13947
  "use strict";
13898
13948
  init_define_IGNITIFY_CLIENT_FILES();
13899
13949
  init_http_client();
13900
13950
  init_constants();
13901
- looksShort = (tok) => tok.length > 1 && tok.startsWith("-") && !tok.startsWith("--");
13902
13951
  } });
13903
13952
  var index_exports = {};
13904
13953
  __export(index_exports, { main: () => main, run: () => run });