ignitify 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/ignitify.cjs +1066 -256
  3. package/package.json +1 -1
package/ignitify.cjs CHANGED
@@ -1136,14 +1136,18 @@ function fill(pattern, ...params) {
1136
1136
  if (i !== params.length) throw new Error(`route ${pattern}: takes ${i} parameter(s), given ${params.length}`);
1137
1137
  return out2;
1138
1138
  }
1139
+ function isValidIdempotencyKey(key) {
1140
+ return IDEMPOTENCY_KEY_PATTERN.test(key);
1141
+ }
1139
1142
  function isTerminalRunStatus(status) {
1140
1143
  return TERMINAL_RUN_STATUSES.includes(status);
1141
1144
  }
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"() {
1145
+ var DOT_SEGMENTS, TENANT_ROUTES, KEY_EXPIRED_ERROR, TENANT_PATHS, API_VERSION, API_VERSION_HEADER, CLIENT_FLOOR_HEADER, RETRY_AFTER_HEADER, CLI_VERSION_HEADER, IDEMPOTENCY_KEY_HEADER, IDEMPOTENT_REPLAYED_HEADER, IDEMPOTENCY_KEY_PATTERN, TERMINAL_RUN_STATUSES, init_api_routes = __esm({ "../../packages/contract/api-routes.ts"() {
1143
1146
  "use strict";
1144
1147
  init_define_IGNITIFY_CLIENT_FILES();
1145
1148
  DOT_SEGMENTS = /* @__PURE__ */ new Set([".", ".."]);
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"];
1149
+ 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", USAGE_SERIES: "/usage/series", RUN_LOGS: "/runs/:id/logs", RUN_TIMELINE: "/runs/:id/timeline", RUN_FAILURES: "/runs/failures", 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", NOTIFICATIONS: "/notifications", USERS: "/users", USER_BY_ID: "/users/:id", GROUPS: "/groups", GROUP_BY_ID: "/groups/:id", GROUP_PERMISSION: "/groups/:id/permissions/:permission", GROUP_USER: "/groups/:id/users/:user", KEYS: "/keys", KEY_BY_ID: "/keys/:id" }, 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), RUN_TIMELINE: (id) => fill(TENANT_ROUTES.RUN_TIMELINE, id), RUN_FAILURES: TENANT_ROUTES.RUN_FAILURES, 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, USAGE_SERIES: TENANT_ROUTES.USAGE_SERIES, 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, NOTIFICATIONS: TENANT_ROUTES.NOTIFICATIONS, USERS: TENANT_ROUTES.USERS, USER_BY_ID: (name) => fill(TENANT_ROUTES.USER_BY_ID, name), GROUPS: TENANT_ROUTES.GROUPS, GROUP_BY_ID: (name) => fill(TENANT_ROUTES.GROUP_BY_ID, name), GROUP_PERMISSION: (name, permission) => fill(TENANT_ROUTES.GROUP_PERMISSION, name, permission), GROUP_USER: (name, user) => fill(TENANT_ROUTES.GROUP_USER, name, user), KEYS: TENANT_ROUTES.KEYS, KEY_BY_ID: (id) => fill(TENANT_ROUTES.KEY_BY_ID, id) }, API_VERSION = "1", API_VERSION_HEADER = "X-Api-Version", CLIENT_FLOOR_HEADER = "X-Client-Floor", RETRY_AFTER_HEADER = "Retry-After", CLI_VERSION_HEADER = "X-Ignitify-Cli", IDEMPOTENCY_KEY_HEADER = "Idempotency-Key", IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed", IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{8,255}$/;
1150
+ TERMINAL_RUN_STATUSES = ["COMPLETED", "FAILED", "CANCELLED"];
1147
1151
  } });
1148
1152
  var CALLBACK_PATH_PREFIX, init_callback_path = __esm({ "../../packages/contract/callback-path.ts"() {
1149
1153
  "use strict";
@@ -1158,6 +1162,10 @@ var init_limits = __esm({ "../../packages/contract/limits.ts"() {
1158
1162
  "use strict";
1159
1163
  init_define_IGNITIFY_CLIENT_FILES();
1160
1164
  } });
1165
+ var init_currencies = __esm({ "../../packages/contract/currencies.ts"() {
1166
+ "use strict";
1167
+ init_define_IGNITIFY_CLIENT_FILES();
1168
+ } });
1161
1169
  var TASK_CONFIG_FILENAMES, RUNTIME_REGISTRY, RUNTIME_IDS, LIVE_RUNTIME_IDS, isRuntimeId, TASK_CONFIG_AJV_OPTIONS, TASK_CONFIG_SCHEMA, BUILD_REFUSAL_STAGES, BUILD_STAGES, init_task_config = __esm({ "../../packages/contract/task-config.ts"() {
1162
1170
  "use strict";
1163
1171
  init_define_IGNITIFY_CLIENT_FILES();
@@ -1302,10 +1310,10 @@ var init_package_advice = __esm({ "../../packages/contract/package-advice.ts"()
1302
1310
  "use strict";
1303
1311
  init_define_IGNITIFY_CLIENT_FILES();
1304
1312
  } });
1305
- var USAGE_AXES, USAGE_DETAIL_COMMAND, RESETS_AT_NEXT_RENEWAL, init_usage = __esm({ "../../packages/contract/usage.ts"() {
1313
+ var USAGE_AXES, AXIS_UNIT, USAGE_DETAIL_COMMAND, RESETS_AT_NEXT_RENEWAL, init_usage = __esm({ "../../packages/contract/usage.ts"() {
1306
1314
  "use strict";
1307
1315
  init_define_IGNITIFY_CLIENT_FILES();
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";
1316
+ USAGE_AXES = ["compute", "network", "network_in", "disk", "scratch", "runs", "pin", "build"], AXIS_UNIT = { compute: "ms", network: "bytes", network_in: "bytes", disk: "byte-hours", scratch: "byte-hours", runs: "runs", pin: "ms", build: "ms" }, USAGE_DETAIL_COMMAND = "ignitify quota", RESETS_AT_NEXT_RENEWAL = "your next paid renewal";
1309
1317
  } });
1310
1318
  function formatUserCode(code) {
1311
1319
  if (code.length !== 8) return code;
@@ -1324,12 +1332,65 @@ var init_backoff = __esm({ "../../packages/contract/backoff.ts"() {
1324
1332
  "use strict";
1325
1333
  init_define_IGNITIFY_CLIENT_FILES();
1326
1334
  } });
1335
+ var init_tenant_account = __esm({ "../../packages/contract/tenant-account.ts"() {
1336
+ "use strict";
1337
+ init_define_IGNITIFY_CLIENT_FILES();
1338
+ } });
1339
+ function retryAfterMs(value, nowMs) {
1340
+ if (value === null || value.trim() === "") return null;
1341
+ let text = value.trim(), ms = /^\d+$/.test(text) ? Number(text) * 1e3 : /[a-z]/i.test(text) ? Date.parse(text) - nowMs : Number.NaN;
1342
+ return Number.isNaN(ms) ? null : Math.min(Math.max(2e3, ms), WEB_POLL.BACKOFF_CEILING_MS);
1343
+ }
1344
+ var WEB_POLL, NO_TASK_NAME_TEXT, OBS_QUERY, FAILURE_CLASSES, FAILURE_CLASS_TEXT, OBS_REFUSAL, init_tenant_telemetry = __esm({ "../../packages/contract/tenant-telemetry.ts"() {
1345
+ "use strict";
1346
+ init_define_IGNITIFY_CLIENT_FILES();
1347
+ WEB_POLL = { LIVE_MS: 5e3, SUMMARY_MS: 6e4, JITTER: 0.1, BACKOFF_CEILING_MS: 6e4 };
1348
+ NO_TASK_NAME_TEXT = "(no task name)", OBS_QUERY = { CURSOR: "cursor", WAIT_SEC: "wait_sec", FROM_OFFSET: "from_offset", STATUS: "status", DAYS: "days" }, FAILURE_CLASSES = ["task", "limit", "cancelled", "platform"], FAILURE_CLASS_TEXT = { task: { label: "Your task", hint: "The code or the deployment failed: an exit code, a crash, a timeout." }, limit: { label: "A limit", hint: "A limit of your plan stopped the run. You can change it." }, cancelled: { label: "Cancelled", hint: "Someone asked the run to stop." }, platform: { label: "The platform", hint: "Something on our side failed. We see these too." } }, OBS_REFUSAL = { NEVER_RAN: { error: "run-never-ran", detail: "this run was not dispatched to a VM" }, INVALID_CURSOR: { error: "invalid-cursor", detail: "from_offset must be 0 or a next_offset that this run's output returned" }, AGENT_OUTDATED_CURSOR: { error: "agent-outdated-cursor", detail: "the host that ran this run cannot read output from an offset; read it without from_offset" }, HOST_GONE: { error: "host-gone", detail: "the host that ran this VM is no longer in the fleet" }, OUTPUT_GONE: { error: "no-file", detail: "the host no longer holds this run's output" }, INVALID_TIMELINE_CURSOR: { error: "invalid-timeline-cursor", detail: "cursor must be a cursor that this run's timeline returned" }, INVALID_DAYS: { error: "invalid-days", detail: "days must be a whole number of days, 1 or more" }, INVALID_WAIT: { error: "invalid-wait", detail: "wait_sec must be whole seconds from 0 to 20" }, WAIT_REFUSED: { error: "wait-refused", detail: "too many open waits; retry after the Retry-After seconds" }, UNAVAILABLE: { error: "obs-unavailable", detail: "the read cannot be served now; retry after the Retry-After seconds" } };
1349
+ } });
1350
+ function isPermission(value) {
1351
+ return typeof value == "string" && PERMISSIONS.has(value);
1352
+ }
1353
+ var PERMISSION, PERMISSIONS, PERMISSION_DESCRIPTION, ACCESS_MARKER, ACCESS_REFUSAL, ACCESS_NAME_PATTERN, ACCESS_NAME_RULE, KEY_LIST_QUERY, init_tenant_access = __esm({ "../../packages/contract/tenant-access.ts"() {
1354
+ "use strict";
1355
+ init_define_IGNITIFY_CLIENT_FILES();
1356
+ PERMISSION = { TASKS_READ: "tasks.read", TASKS_WRITE: "tasks.write", DEPLOYMENTS_READ: "deployments.read", DEPLOYMENTS_WRITE: "deployments.write", RUNS_READ: "runs.read", RUNS_WRITE: "runs.write", SECRETS_READ: "secrets.read", SECRETS_WRITE: "secrets.write", HOOKS_READ: "hooks.read", HOOKS_WRITE: "hooks.write", USAGE_READ: "usage.read" }, PERMISSIONS = new Set(Object.values(PERMISSION));
1357
+ PERMISSION_DESCRIPTION = { [PERMISSION.TASKS_READ]: "Read tasks", [PERMISSION.TASKS_WRITE]: "Create and delete tasks", [PERMISSION.DEPLOYMENTS_READ]: "Read deployments and build logs", [PERMISSION.DEPLOYMENTS_WRITE]: "Deploy, delete a deployment, ask for package advice", [PERMISSION.RUNS_READ]: "Read runs, logs, timeline, failures, schedules, VMs", [PERMISSION.RUNS_WRITE]: "Start, cancel, delete runs; cancel schedules; stop VMs", [PERMISSION.SECRETS_READ]: "List secret names (values are never readable)", [PERMISSION.SECRETS_WRITE]: "Set and delete secrets", [PERMISSION.HOOKS_READ]: "List hooks", [PERMISSION.HOOKS_WRITE]: "Create and delete hooks; set notify", [PERMISSION.USAGE_READ]: "Usage, series, notifications" }, ACCESS_MARKER = { ANY_KEY: "any-key", OWNER: "owner" }, ACCESS_REFUSAL = { PERMISSION_DENIED: "permission-denied", OWNER_REQUIRED: "owner-required", USER_NOT_FOUND: "user-not-found", GROUP_NOT_FOUND: "group-not-found", NAME_TAKEN: "name-taken", UNKNOWN_PERMISSION: "unknown-permission" }, ACCESS_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/, ACCESS_NAME_RULE = "1 to 63 lower case letters, digits and '-', starting with a letter or digit", KEY_LIST_QUERY = { USER: "user" };
1358
+ } });
1359
+ var NOTIFICATION_KINDS, NOTIFICATION_KIND_INFO, ALERT_KINDS, CRITICAL_ALERT_KINDS, NOTIFICATION_FILTERS, NOTIFICATION_RETENTION_DAYS, NOTIFICATION_PAGE_MAX, NOTIFICATION_PAGE_DEFAULT, NOTIFICATION_QUERY, NOTIFICATION_REFUSAL, init_tenant_notifications = __esm({ "../../packages/contract/tenant-notifications.ts"() {
1360
+ "use strict";
1361
+ init_define_IGNITIFY_CLIENT_FILES();
1362
+ init_api_routes();
1363
+ init_tenant_telemetry();
1364
+ NOTIFICATION_KINDS = ["limit.reached", "limit.near", "runs.failing", "callback.failing", "schedule.missed", "pin.lost", "plan.changed"], NOTIFICATION_KIND_INFO = { "limit.reached": { class: "alert", severity: "critical", label: "Monthly limit reached" }, "limit.near": { class: "notification", severity: "warning", label: "Monthly limit near" }, "runs.failing": { class: "alert", severity: "warning", label: "Runs failing" }, "callback.failing": { class: "alert", severity: "warning", label: "Run callbacks failing" }, "schedule.missed": { class: "notification", severity: "warning", label: "Schedule did not run" }, "pin.lost": { class: "notification", severity: "warning", label: "Pinned VM lost" }, "plan.changed": { class: "notification", severity: "info", label: "Plan changed" } }, ALERT_KINDS = NOTIFICATION_KINDS.filter((k) => NOTIFICATION_KIND_INFO[k].class === "alert"), CRITICAL_ALERT_KINDS = ALERT_KINDS.filter((k) => NOTIFICATION_KIND_INFO[k].severity === "critical"), NOTIFICATION_FILTERS = ["all", "unread", "open"], NOTIFICATION_RETENTION_DAYS = 30, NOTIFICATION_PAGE_MAX = 100, NOTIFICATION_PAGE_DEFAULT = 25, NOTIFICATION_QUERY = { FILTER: "filter", KIND: "kind", CURSOR: OBS_QUERY.CURSOR, LIMIT: "limit" }, NOTIFICATION_REFUSAL = { INVALID_FILTER: { error: "invalid-filter", detail: `filter must be one of: ${NOTIFICATION_FILTERS.join(", ")}` }, INVALID_KIND: { error: "invalid-kind", detail: `kind must be one of: ${NOTIFICATION_KINDS.join(", ")}` }, INVALID_LIMIT: { error: "invalid-limit", detail: "limit must be a whole number, 1 or more" }, INVALID_CURSOR: { error: "invalid-cursor", detail: "cursor must be a cursor that this list returned" } };
1365
+ } });
1366
+ var SITE_LINKS, init_web_nav = __esm({ "../../packages/contract/web-nav.ts"() {
1367
+ "use strict";
1368
+ init_define_IGNITIFY_CLIENT_FILES();
1369
+ SITE_LINKS = { DOCS: "https://ignitify.dev/docs", CLI_REFERENCE: "https://ignitify.dev/docs/cli/all", PRICING: "https://ignitify.dev/pricing" };
1370
+ } });
1371
+ var init_web_frame = __esm({ "../../packages/contract/web-frame.ts"() {
1372
+ "use strict";
1373
+ init_define_IGNITIFY_CLIENT_FILES();
1374
+ init_api_routes();
1375
+ init_tenant_account();
1376
+ init_web_nav();
1377
+ } });
1378
+ function stripControl(text, keep = KEEP_NONE) {
1379
+ return text.replace(CONTROL_UNIT, (unit) => keep(unit) ? unit : unit.startsWith("\r") ? `
1380
+ ` : "");
1381
+ }
1382
+ var CONTROL_UNIT, KEEP_NONE, init_control_text = __esm({ "../../packages/contract/control-text.ts"() {
1383
+ "use strict";
1384
+ init_define_IGNITIFY_CLIENT_FILES();
1385
+ CONTROL_UNIT = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[PX^_][^\x1b]*(?:\x1b\\)?|[ -/]*[0-~])?|\r\n?|[\u0000-\u0008\u000b\u000c\u000e-\u001a\u001c-\u001f\u007f-\u009f]/g, KEEP_NONE = () => false;
1386
+ } });
1327
1387
  var init_tenant = __esm({ "../../packages/contract/tenant/index.ts"() {
1328
1388
  "use strict";
1329
1389
  init_define_IGNITIFY_CLIENT_FILES();
1330
1390
  init_api_routes();
1331
1391
  init_callback_path();
1332
1392
  init_limits();
1393
+ init_currencies();
1333
1394
  init_task_config();
1334
1395
  init_source_manifest();
1335
1396
  init_system_packages();
@@ -1340,21 +1401,28 @@ var init_tenant = __esm({ "../../packages/contract/tenant/index.ts"() {
1340
1401
  init_digest();
1341
1402
  init_device_code();
1342
1403
  init_backoff();
1404
+ init_tenant_account();
1405
+ init_tenant_telemetry();
1406
+ init_tenant_access();
1407
+ init_tenant_notifications();
1408
+ init_web_nav();
1409
+ init_web_frame();
1410
+ init_control_text();
1343
1411
  } });
1344
1412
  function valueFlags(map2) {
1345
1413
  return Object.fromEntries(Object.keys(map2).map((flag) => [flag, "value"]));
1346
1414
  }
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"() {
1415
+ var COMMANDS, IGNORE_FILE, STATE, CREDENTIALS, LOGIN, LOGIN_VIEW, NETWORK, ENV, DEFAULT_TASKS_DIR, RELEASE_BUILD, DEFAULTS, UPGRADE_COMMAND, PAYLOAD_LIMIT_FALLBACK_BYTES, RESULT_LIMIT_FALLBACK_BYTES, RUN_LIST_FLAG_PARAMS, VM_LIST_FLAG_PARAMS, GLOBAL_FLAGS, SHORT_FLAGS, VERSION_TOKENS, VALIDATE, ADVICE, INTEGRATION, NO_FLAGS, DIR, PAGE, YES, RAW, FOLLOW_FLAG, DAYS_FLAG, USAGE_FLAG_PARAMS, USAGE_FILTERS, NOTIFICATION_LIST_FLAG_PARAMS, COMMAND_META, VERSION_META, KEY_REVOKE_COMMAND, MESSAGES, ACCESS, RUN_STATUS, ACTIVE_RUN_STATUSES, DEPLOYMENT_STATUS, POLL, WAIT, FOLLOW, MS_PER_SECOND, BUSY_READ, FOLLOW_MESSAGES, init_constants = __esm({ "src/constants/index.ts"() {
1348
1416
  "use strict";
1349
1417
  init_define_IGNITIFY_CLIENT_FILES();
1350
1418
  init_http_client();
1351
1419
  init_tenant();
1352
1420
  init_tenant();
1353
1421
  init_tenant();
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
1422
+ 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_TIMELINE: "run:timeline", RUN_FAILURES: "run:failures", 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", USAGE_SERIES: "usage:series", NOTIFICATION_LIST: "notification:list", PERMISSION_LIST: "permission:list", USER_CREATE: "user:create", USER_LIST: "user:list", USER_GET: "user:get", USER_REMOVE: "user:remove", GROUP_CREATE: "group:create", GROUP_LIST: "group:list", GROUP_GET: "group:get", GROUP_GRANT: "group:grant", GROUP_REVOKE: "group:revoke", GROUP_ADD_USER: "group:add-user", GROUP_REMOVE_USER: "group:remove-user", GROUP_DELETE: "group:delete", KEY_CREATE: "key:create", KEY_LIST: "key:list", KEY_REVOKE: "key:revoke" }, IGNORE_FILE = ".ignitifyignore", STATE = { DIR: ".ignitify", FILE: "state.json", GITIGNORE_FILE: ".gitignore", GITIGNORE_TEXT: `# written by ignitify: local state, never commit
1355
1423
  *
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"];
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.
1424
+ ` }, 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", DEV_API_URL: "IGNITIFY_DEV_API_URL" }, DEFAULT_TASKS_DIR = "tasks", RELEASE_BUILD = true, 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" }, SHORT_FLAGS = { h: "help", y: "yes" }, VERSION_TOKENS = ["--version", "-v"];
1425
+ 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" }, FOLLOW_FLAG = { follow: "bool" }, DAYS_FLAG = { days: "value" }, USAGE_FLAG_PARAMS = { period: "period", axis: "axis", vm: "vm", run: "run" }, USAGE_FILTERS = valueFlags(USAGE_FLAG_PARAMS), NOTIFICATION_LIST_FLAG_PARAMS = { kind: NOTIFICATION_QUERY.KIND, limit: NOTIFICATION_QUERY.LIMIT }, COMMAND_META = { [COMMANDS.HELP]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.LOGIN]: { flags: { "no-browser": "bool", name: "value" }, positionals: 0, confirms: false }, [COMMANDS.LOGOUT]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.WHOAMI]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.QUOTA]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.TASK_CREATE]: { flags: { ...DIR, lang: "value", local: "bool", "no-init": "bool", "no-scaffold": "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_LIST]: { flags: PAGE, positionals: 0, confirms: false }, [COMMANDS.TASK_GET]: { flags: DIR, positionals: 1, confirms: false }, [COMMANDS.TASK_DELETE]: { flags: { ...DIR, ...YES }, positionals: 1, confirms: true }, [COMMANDS.TASK_VALIDATE]: { flags: { ...DIR, [ADVICE.FLAG]: "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_PACK]: { flags: DIR, positionals: 1, confirms: false }, [COMMANDS.TASK_DEPLOY]: { flags: { ...DIR, check: "bool" }, positionals: 1, confirms: false }, [COMMANDS.TASK_RUN]: { flags: { deployment: "value", payload: "value", "timeout-sec": "value", "on-busy": "value", at: "value", "idempotency-key": "value", wait: "bool", ...DIR }, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_LIST]: { flags: PAGE, positionals: 0, confirms: false }, [COMMANDS.DEPLOYMENT_GET]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_LOGS]: { flags: RAW, positionals: 1, confirms: false }, [COMMANDS.DEPLOYMENT_DELETE]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.RUN_LIST]: { flags: valueFlags(RUN_LIST_FLAG_PARAMS), positionals: 0, confirms: false }, [COMMANDS.RUN_GET]: { flags: { full: "bool" }, positionals: 1, confirms: false }, [COMMANDS.RUN_LOGS]: { flags: { ...RAW, ...FOLLOW_FLAG, tail: "value" }, positionals: 1, confirms: false }, [COMMANDS.RUN_TIMELINE]: { flags: FOLLOW_FLAG, positionals: 1, confirms: false }, [COMMANDS.RUN_FAILURES]: { flags: DAYS_FLAG, positionals: 0, confirms: false }, [COMMANDS.RUN_DELETE]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.RUN_CANCEL]: { flags: { ...YES, force: "bool", grace: "value" }, positionals: 1, confirms: true }, [COMMANDS.VM_LIST]: { flags: { ...valueFlags(VM_LIST_FLAG_PARAMS), pinned: "bool" }, positionals: 0, confirms: false }, [COMMANDS.VM_GET]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.VM_DELETE]: { flags: { ...YES, force: "bool" }, positionals: 1, confirms: true }, [COMMANDS.SECRET_SET]: { flags: { stdin: "bool", value: "value" }, positionals: 1, confirms: false, secretArgv: true }, [COMMANDS.SECRET_LIST]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.SECRET_DELETE]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.SCHEDULE_LIST]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.SCHEDULE_CANCEL]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.HOOK_CREATE]: { flags: { "task-name": "value" }, positionals: 1, confirms: false }, [COMMANDS.HOOK_LIST]: { flags: PAGE, positionals: 1, confirms: false }, [COMMANDS.HOOK_DELETE]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.NOTIFY_SET]: { 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: { ...USAGE_FILTERS, runs: "bool", vms: "bool" }, positionals: 0, confirms: false }, [COMMANDS.USAGE_EXPORT]: { flags: { ...USAGE_FILTERS, csv: "bool" }, positionals: 0, confirms: false }, [COMMANDS.USAGE_SERIES]: { flags: DAYS_FLAG, positionals: 0, confirms: false }, [COMMANDS.NOTIFICATION_LIST]: { flags: { ...valueFlags(NOTIFICATION_LIST_FLAG_PARAMS), unread: "bool", open: "bool" }, positionals: 0, confirms: false }, [COMMANDS.PERMISSION_LIST]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.USER_CREATE]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.USER_LIST]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.USER_GET]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.USER_REMOVE]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.GROUP_CREATE]: { flags: { permissions: "value" }, positionals: 1, confirms: false }, [COMMANDS.GROUP_LIST]: { flags: NO_FLAGS, positionals: 0, confirms: false }, [COMMANDS.GROUP_GET]: { flags: NO_FLAGS, positionals: 1, confirms: false }, [COMMANDS.GROUP_GRANT]: { flags: NO_FLAGS, positionals: 2, confirms: false }, [COMMANDS.GROUP_REVOKE]: { flags: NO_FLAGS, positionals: 2, confirms: false }, [COMMANDS.GROUP_ADD_USER]: { flags: NO_FLAGS, positionals: 2, confirms: false }, [COMMANDS.GROUP_REMOVE_USER]: { flags: NO_FLAGS, positionals: 2, confirms: false }, [COMMANDS.GROUP_DELETE]: { flags: YES, positionals: 1, confirms: true }, [COMMANDS.KEY_CREATE]: { flags: { user: "value", label: "value", out: "value" }, positionals: 0, confirms: false }, [COMMANDS.KEY_LIST]: { flags: { user: "value" }, positionals: 0, confirms: false }, [COMMANDS.KEY_REVOKE]: { flags: YES, positionals: 1, confirms: true } }, VERSION_META = { flags: NO_FLAGS, positionals: 0, confirms: false }, KEY_REVOKE_COMMAND = (id) => `ignitify ${COMMANDS.KEY_REVOKE} ${id}`, 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'.`, 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}`, TASK_NOT_REGISTERED: (taskId, dirFlag) => `this folder is not registered yet (no task_id in .ignitify/state.json). Run: ignitify task:create ${taskId} --no-scaffold${dirFlag === void 0 ? "" : ` --dir ${dirFlag}`}, then deploy again.`, 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.
1358
1426
  ignitify secret:set <NAME> --stdin < value-file
1359
1427
  printf %s "$VALUE" | ignitify secret:set <NAME> --stdin
1360
1428
  \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.
@@ -1362,14 +1430,15 @@ usage: ignitify hook:create <deployment-id> --task-name <name>`, HOOK_LIST_USAGE
1362
1430
  ignitify notify:set <deployment-id> --clear
1363
1431
  --secret-stdin reads the signing secret (at least ${32} characters) from stdin; it is
1364
1432
  required with --url. There is no --secret flag: a value on argv is visible in \`ps\`
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.
1433
+ 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_SECRET_REDACTED: (id) => `the server answered without the secret (a replay of an earlier request), and it is not shown again. Delete ${id} with 'ignitify hook:delete ${id}' and create another to get a new secret.`, 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)", DAYS_INVALID: (raw) => `--days must be a whole number of days, 1 or more (got ${JSON.stringify(raw)})`, NOTIFICATION_ONE_VIEW: "Choose --unread or --open, not both.", NOTIFICATION_MORE: (max) => `Older notifications exist. Raise --limit (at most ${max}), or open Notifications in the app.`, NOTIFICATION_MORE_IN_APP: "Older notifications exist. Open Notifications in the app to see them.", 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'.", IDEMPOTENCY_KEY_INVALID: (key) => `--idempotency-key must be 8 to 255 characters of A-Z a-z 0-9 . _ : -, got: ${key}`, RUN_REPLAYED: "this is the answer to an earlier request with the same --idempotency-key; no new run was started.", IDEMPOTENCY_KEY_REUSED_HINT: "Use a new --idempotency-key for a different run. Reuse a key only to retry the same run.", IDEMPOTENCY_IN_PROGRESS_HINT: "If an earlier request with this key is still running, retry with the same key shortly.", SECRET_STORED: (name) => `secret ${name} stored`, SECRET_REMOVED: (name) => `secret ${name} removed`, ACCESS_NAME_INVALID: (kind, name) => `${kind} name ${JSON.stringify(name)} is not valid: use ${ACCESS_NAME_RULE}`, PERMISSION_UNKNOWN: (permission) => `unknown permission ${JSON.stringify(permission)}. Run 'ignitify permission:list' to see the permissions.`, PERMISSION_NOT_GRANTABLE: (marker) => `${JSON.stringify(marker)} is not a permission and cannot be granted: only owner keys manage access.`, PERMISSIONS_REQUIRED: "--permissions <p1,p2,\u2026> is required: name at least one permission, comma-separated.", KEY_USER_REQUIRED: "--user <name> is required: a key belongs to one user. Owner keys come from ignitify login.", KEY_LABEL_REQUIRED: "--label <text> is required: it tells you later where the key is used.", KEY_SHOWN_ONCE: "\u26A0\uFE0F The key below is shown ONCE and cannot be read back. Hand it over now; if it is lost, revoke it and create another.", KEY_OUT_EXISTS: (file) => `${file} already exists. key:create --out writes a new file only and never follows a link. Choose another name; no key was created.`, KEY_OUT_FAILED: (file, reason) => `cannot create ${file} (${reason}); no key was created.`, KEY_OUT_WRITE_FAILED: (file, reason, id) => `cannot write ${file} (${reason}). Key ${id} was created but its value is lost: revoke it with '${KEY_REVOKE_COMMAND(id)}' and create another.`, KEY_WRITTEN: (id, file) => `key ${id} written to ${file} (mode 0600). Hand the file over, then delete your copy.`, KEY_REDACTED: (id) => `the server answered without the key (a replay of an earlier request). Revoke ${id} with '${KEY_REVOKE_COMMAND(id)}' and create another.`, USER_CREATED: (name) => `user ${name} created`, USER_REMOVED: (name, keys) => `user ${name} removed; ${keys} key(s) revoked`, GROUP_CREATED: (name) => `group ${name} created`, GROUP_GRANTED: (name, permission) => `group ${name} now has ${permission}`, GROUP_REVOKED: (name, permission) => `group ${name} no longer has ${permission}`, GROUP_USER_ADDED: (group, user) => `user ${user} added to group ${group}`, GROUP_USER_REMOVED: (group, user) => `user ${user} removed from group ${group}`, GROUP_DELETED: (name) => `group ${name} deleted`, KEY_REVOKED: (id) => `key ${id} revoked`, ACCESS_PERMISSION_DENIED: (needed) => needed === null ? "permission denied: this key lacks the permission this command needs. Ask the owner to grant it to one of your groups." : `permission denied: this key needs the ${needed} permission. Ask the owner to grant it to one of your groups.`, ACCESS_OWNER_REQUIRED: "owner key required: only owner keys manage users, groups and keys. This is a user key.", ACCESS_REFUSED: (error, reason) => `${error} (${reason})` }, ACCESS = { OWNER: "owner", SEPARATOR: ",", USER: "user", GROUP: "group", TASKS_WRITE_NOTE: "tasks.write can delete deployments and runs too: deleting a task deletes its deployments, cancels and deletes their runs and stops their VMs. Grant it only where deployments.write and runs.write are acceptable too.", SECRETS_READ_NOTE: "deployments.write plus runs.write can read secret values in practice: deploy code that prints its environment, then run it.", HOOKS_WRITE_NOTE: "hooks.write can start runs (a hook trigger starts a run) and can send run results to any URL. Grant it only where runs.write is acceptable too." }, 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.
1366
1434
  Check it with: ignitify deployment:get ${deploymentId}`, INTERRUPTED_RUN: (runId) => `Stopped waiting. The run keeps going on the server \u2014 nothing was cancelled.
1367
- Check it with: ignitify run:get ${runId}` };
1435
+ Check it with: ignitify run:get ${runId}` }, FOLLOW = { WAIT_SEC: 20, IDLE_MS: 2e3, RETRY_FALLBACK_MS: 2e3 }, MS_PER_SECOND = 1e3, BUSY_READ = { MAX_RETRIES: 3, NOTE: (seconds) => `server busy, retrying in ${seconds}s` }, FOLLOW_MESSAGES = { AGENT_OUTDATED: "the host is running an older agent; try again after the update, or read it without --follow or --tail", API_NO_CURSOR: "this API cannot follow output yet; run the command without --follow or --tail", TAIL_INVALID: (raw) => `--tail must be a whole number of lines, 1 or more (got ${JSON.stringify(raw)})`, INTERRUPTED: (runId) => `Stopped following. The run keeps going on the server \u2014 nothing was cancelled.
1436
+ Check it with: ignitify run:get ${runId}`, STILL_LIVE: (status) => `Run ${status} \u2014 still live; add --follow to watch it` };
1368
1437
  } });
1369
1438
  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"() {
1370
1439
  "use strict";
1371
1440
  init_define_IGNITIFY_CLIENT_FILES();
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" };
1441
+ 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>" }, USERS: { message: "No users yet.", next: "Create one with: ignitify user:create <name>" }, GROUPS: { message: "No groups yet.", next: "Create one with: ignitify group:create <name> --permissions <p1,p2>" }, KEYS: { message: "No live keys." } }, DISPLAY_CAPS = { RESULT_MAX_LINES: 40, BUILD_LOG_TAIL_LINES: 30, ERROR_MAX_CHARS: 2e3 }, TITLES = { TENANT_PROFILE: "Tenant Profile", KEY_ACCESS: "This Key", 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.`, USER_REMOVE_PREVIEW: (name) => `Will remove user ${name} and revoke every key of the user. The keys stop working at once.`, GROUP_DELETE_PREVIEW: (name) => `Will delete group ${name}. Its users lose the permissions it grants; users and keys stay.`, KEY_REVOKE_PREVIEW: (id) => `Will revoke key ${id}. A program that uses it is refused from then on.`, KEY_CREATED_TITLE: "Key Created", KEY_RAW_KEY: "key (shown ONCE)", USER_KEYS_TITLE: "Keys", NONE: "-", 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" };
1373
1442
  } });
1374
1443
  function makeStyle(o) {
1375
1444
  let p = (...codes) => paint(o.colour, ...codes), columns = Math.max(o.width, 1);
@@ -1399,15 +1468,15 @@ function paintStatus(st, status) {
1399
1468
  let s = String(status).toUpperCase(), tone = STATUS_TONE[s];
1400
1469
  return tone === "ok" ? st.green(s) : tone === "busy" ? st.yellow(s) : tone === "bad" ? st.red(s) : st.dim(s);
1401
1470
  }
1402
- var paint, PLAIN, resolveStyle, plain, plainLines, TERMINAL_CONTROL, SGR, terminalSafe, stripAnsi, visibleLength, padVisible, cut, STATUS_TONE, init_style = __esm({ "src/output/style.ts"() {
1471
+ var paint, PLAIN, resolveStyle, plain, plainLines, SGR, isColour, terminalSafe, stripAnsi, visibleLength, padVisible, cut, STATUS_TONE, init_style = __esm({ "src/output/style.ts"() {
1403
1472
  "use strict";
1404
1473
  init_define_IGNITIFY_CLIENT_FILES();
1474
+ init_tenant();
1405
1475
  init_constants2();
1406
1476
  paint = (on, ...codes) => (s) => on && s !== "" ? `${codes.join("")}${s}${ANSI.reset}` : s;
1407
1477
  PLAIN = makeStyle({ colour: false, unicode: true, width: LAYOUT.MAX_WIDTH });
1408
1478
  resolveStyle = (f) => makeStyle({ colour: wantsColour(f), unicode: wantsUnicode(f), width: f.columns !== void 0 && f.columns > 0 ? f.columns : LAYOUT.MAX_WIDTH });
1409
- plain = (s) => s.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""), plainLines = (s) => s.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, ""), TERMINAL_CONTROL = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[PX^_][^\x1b]*(?:\x1b\\)?|[ -/]*[0-~])?|\r\n?|[\u0000-\u0008\u000b\u000c\u000e-\u001a\u001c-\u001f\u007f-\u009f]/g, SGR = /^\x1b\[[0-9;:]*m$/, terminalSafe = (s, keepColour) => s.replace(TERMINAL_CONTROL, (m) => keepColour && SGR.test(m) ? m : m.startsWith("\r") ? `
1410
- ` : ""), stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, ""), visibleLength = (s) => stripAnsi(s).length, padVisible = (s, width) => s + " ".repeat(Math.max(0, width - visibleLength(s))), cut = (s, max, ellipsis) => s.length <= max || max <= ellipsis.length ? s : `${s.slice(0, max - ellipsis.length)}${ellipsis}`;
1479
+ plain = (s) => s.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""), plainLines = (s) => s.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, ""), SGR = /^\x1b\[[0-9;:]*m$/, isColour = (unit) => SGR.test(unit), terminalSafe = (s, keepColour) => keepColour ? stripControl(s, isColour) : stripControl(s), stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, ""), visibleLength = (s) => stripAnsi(s).length, padVisible = (s, width) => s + " ".repeat(Math.max(0, width - visibleLength(s))), cut = (s, max, ellipsis) => s.length <= max || max <= ellipsis.length ? s : `${s.slice(0, max - ellipsis.length)}${ellipsis}`;
1411
1480
  STATUS_TONE = { COMPLETED: "ok", READY: "ok", ACTIVE: "ok", DONE: "ok", OK: "ok", PENDING: "busy", PROVISIONING: "busy", RUNNING: "busy", RETRYING: "busy", BUILDING: "busy", FAILED: "bad" };
1412
1481
  } });
1413
1482
  function cellText(v) {
@@ -1501,6 +1570,94 @@ var heading, successLine, errorMark, warnMark, noteLine, emptyState, init_blocks
1501
1570
  heading = (st, title, ok2 = false) => ok2 ? st.bold(st.green(`${st.glyphs.ok} ${title}`)) : st.bold(title), successLine = (st, text) => `${st.green(st.glyphs.ok)} ${text}`, errorMark = (st, text) => `${st.red(st.glyphs.err)} ${text}`, warnMark = (st, text) => `${st.yellow(st.glyphs.warn)} ${text}`, noteLine = (st, text) => st.yellow(text), emptyState = (st, message, next) => next === void 0 ? st.dim(message) : `${st.dim(message)}
1502
1571
  ${st.dim(next)}`;
1503
1572
  } });
1573
+ function checkAccessName(kind, name) {
1574
+ return ACCESS_NAME_PATTERN.test(name) ? { ok: true, value: name } : { ok: false, message: MESSAGES.ACCESS_NAME_INVALID(kind, name) };
1575
+ }
1576
+ function checkPermission(permission) {
1577
+ return isPermission(permission) ? { ok: true, value: permission } : { ok: false, message: MARKERS.includes(permission) ? MESSAGES.PERMISSION_NOT_GRANTABLE(permission) : MESSAGES.PERMISSION_UNKNOWN(permission) };
1578
+ }
1579
+ function parsePermissions(raw) {
1580
+ let names = (raw ?? "").split(ACCESS.SEPARATOR).map((p) => p.trim()).filter((p) => p !== "");
1581
+ if (names.length === 0) return { ok: false, message: MESSAGES.PERMISSIONS_REQUIRED };
1582
+ let out2 = [];
1583
+ for (let name of names) {
1584
+ let checked = checkPermission(name);
1585
+ if (!checked.ok) return checked;
1586
+ out2.includes(checked.value) || out2.push(checked.value);
1587
+ }
1588
+ return { ok: true, value: out2 };
1589
+ }
1590
+ function permissionNote(permission) {
1591
+ if (permission === PERMISSION.TASKS_WRITE) return ACCESS.TASKS_WRITE_NOTE;
1592
+ if (permission === PERMISSION.SECRETS_READ) return ACCESS.SECRETS_READ_NOTE;
1593
+ if (permission === PERMISSION.HOOKS_WRITE) return ACCESS.HOOKS_WRITE_NOTE;
1594
+ }
1595
+ function permissionRows() {
1596
+ return CATALOGUE.map((permission) => {
1597
+ let note = permissionNote(permission);
1598
+ return { permission, description: PERMISSION_DESCRIPTION[permission], ...note === void 0 ? {} : { note } };
1599
+ });
1600
+ }
1601
+ function accessRefusalText(e) {
1602
+ let body = httpErrorJson(e)?.body, reason = body?.reason;
1603
+ if (typeof reason != "string" || !REFUSALS.includes(reason)) return null;
1604
+ if (reason === ACCESS_REFUSAL.PERMISSION_DENIED) {
1605
+ let needed = body?.needed;
1606
+ return MESSAGES.ACCESS_PERMISSION_DENIED(typeof needed == "string" && needed !== "" ? needed : null);
1607
+ }
1608
+ if (reason === ACCESS_REFUSAL.OWNER_REQUIRED) return MESSAGES.ACCESS_OWNER_REQUIRED;
1609
+ let error = body?.error;
1610
+ return MESSAGES.ACCESS_REFUSED(typeof error == "string" && error !== "" ? error : reason, reason);
1611
+ }
1612
+ var CATALOGUE, MARKERS, REFUSALS, keyUser, init_access = __esm({ "src/core/access.ts"() {
1613
+ "use strict";
1614
+ init_define_IGNITIFY_CLIENT_FILES();
1615
+ init_http_client();
1616
+ init_tenant();
1617
+ init_constants();
1618
+ CATALOGUE = Object.values(PERMISSION), MARKERS = Object.values(ACCESS_MARKER), REFUSALS = Object.values(ACCESS_REFUSAL);
1619
+ keyUser = (user) => user ?? ACCESS.OWNER;
1620
+ } });
1621
+ function renderPermissions(st, rows) {
1622
+ return renderTable(st, [{ header: "permission", keep: true }, { header: "allows", wrap: true }], rows.map((r) => [r.permission, r.note === void 0 ? r.description : `${r.description}. Note: ${r.note}`]));
1623
+ }
1624
+ function renderUsers(st, users) {
1625
+ return users.length === 0 ? emptyState(st, EMPTY_LISTS.USERS.message, EMPTY_LISTS.USERS.next) : renderTable(st, [{ header: "name", keep: true }, { header: "resource_id", keep: true }, { header: "created_at", keep: true }], users.map((u) => [u.name, u.resource_id, u.created_at]));
1626
+ }
1627
+ function renderKeys(st, keys) {
1628
+ return keys.length === 0 ? emptyState(st, EMPTY_LISTS.KEYS.message) : renderTable(st, [{ header: "resource_id", keep: true }, { header: "user", keep: true }, { header: "label" }, { header: "created_at", keep: true }, { header: "last_used_at", keep: true }], keys.map((k) => [k.resource_id, keyUser(k.user), k.label, k.created_at, k.last_used_at]));
1629
+ }
1630
+ function renderUser(st, user) {
1631
+ return `${renderFields(st, { name: user.name, resource_id: user.resource_id, created_at: user.created_at, groups: joined(user.groups), permissions: joined(user.permissions) })}
1632
+
1633
+ ${heading(st, RESULT_MESSAGES.USER_KEYS_TITLE)}
1634
+ ${renderKeys(st, user.keys)}`;
1635
+ }
1636
+ function renderGroups(st, groups) {
1637
+ return groups.length === 0 ? emptyState(st, EMPTY_LISTS.GROUPS.message, EMPTY_LISTS.GROUPS.next) : renderTable(st, [{ header: "name", keep: true }, { header: "permissions", wrap: true }, { header: "created_at", keep: true }], groups.map((g) => [g.name, joined(g.permissions), g.created_at]));
1638
+ }
1639
+ function renderGroup(st, group) {
1640
+ return renderFields(st, { name: group.name, resource_id: group.resource_id, created_at: group.created_at, permissions: joined(group.permissions), users: joined(group.users) });
1641
+ }
1642
+ function renderKeyAccess(st, access) {
1643
+ return renderFields(st, { key: access.key, user: keyUser(access.user), groups: joined(access.groups), permissions: joined(access.permissions) });
1644
+ }
1645
+ function renderKeyCreated(st, created) {
1646
+ let { raw_key, secret_redacted: _redacted, ...rest } = created, rows = { ...rest, [RESULT_MESSAGES.KEY_RAW_KEY]: raw_key }, paint2 = { [RESULT_MESSAGES.KEY_RAW_KEY]: (s) => st.bold(st.yellow(s)) };
1647
+ return `
1648
+ ${heading(st, RESULT_MESSAGES.KEY_CREATED_TITLE, true)}
1649
+ ${renderFields(st, rows, void 0, paint2)}
1650
+ `;
1651
+ }
1652
+ var joined, init_access2 = __esm({ "src/output/access.ts"() {
1653
+ "use strict";
1654
+ init_define_IGNITIFY_CLIENT_FILES();
1655
+ init_access();
1656
+ init_blocks();
1657
+ init_constants2();
1658
+ init_table();
1659
+ joined = (items) => items.length === 0 ? RESULT_MESSAGES.NONE : items.join(", ");
1660
+ } });
1504
1661
  function isStorageMib(value) {
1505
1662
  return typeof value == "number" && Number.isInteger(value) && value > 0;
1506
1663
  }
@@ -1636,6 +1793,7 @@ var sectionHeader, layout, filterScalars, filterDisplayKeys, BYTE_UNITS2, format
1636
1793
  init_table();
1637
1794
  init_blocks();
1638
1795
  init_style();
1796
+ init_access2();
1639
1797
  init_constants2();
1640
1798
  init_tenant();
1641
1799
  init_execution();
@@ -1652,10 +1810,10 @@ ${heading(st, title, success)}
1652
1810
  return `${exp === 0 ? value : value.toFixed(1)} ${BYTE_UNITS2[exp]}`;
1653
1811
  }, 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) => {
1654
1812
  if (!data) return "";
1655
- let { limits, usage, tier, policy, bound_by, status_reason, policy_scope, enforced, unit_weights, capacity, ...identity } = data, identityRows = { ...identity };
1813
+ let { limits, usage, tier, policy, bound_by, status_reason, policy_scope, enforced, unit_weights, capacity, access, ...identity } = data, identityRows = { ...identity };
1656
1814
  typeof status_reason == "string" && status_reason !== "" && (identityRows.status_reason = status_reason), tier && (identityRows.tier = `${tier.name} v${tier.version}`), bound_by && (identityRows.bound_by = bound_by);
1657
1815
  let out2 = layout(sectionHeader(st, TITLES.TENANT_PROFILE), renderFields(st, filterDisplayKeys(identityRows)));
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;
1816
+ return access && (out2 += layout(sectionHeader(st, TITLES.KEY_ACCESS), renderKeyAccess(st, access))), (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;
1659
1817
  }, 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) => {
1660
1818
  let enforcedText = (r) => {
1661
1819
  let value = showEnforced(r.enforced);
@@ -1879,7 +2037,7 @@ ${notice()}
1879
2037
  process.removeListener("SIGINT", handler);
1880
2038
  }
1881
2039
  }
1882
- var spinnerGlyph, realEvery, NOOP, init_progress = __esm({ "src/output/progress.ts"() {
2040
+ var spinnerGlyph, realEvery, NOOP, NO_PROGRESS, init_progress = __esm({ "src/output/progress.ts"() {
1883
2041
  "use strict";
1884
2042
  init_define_IGNITIFY_CLIENT_FILES();
1885
2043
  init_constants2();
@@ -1895,13 +2053,15 @@ var spinnerGlyph, realEvery, NOOP, init_progress = __esm({ "src/output/progress.
1895
2053
  }, fail() {
1896
2054
  }, stop() {
1897
2055
  } };
2056
+ NO_PROGRESS = { stop: () => {
2057
+ } };
1898
2058
  } });
1899
- var AXES, DIR2, YES2, RAW2, PAGE2, VERSION_HELP, COMMAND_HELP, HELP_PAGE, init_help = __esm({ "src/constants/help.ts"() {
2059
+ var OWNER_ONLY, PERMISSION_NOTES, AXES, DIR2, YES2, RAW2, SERIES_DAYS, FAILURES_DAYS, PAGE2, VERSION_HELP, COMMAND_HELP, HELP_PAGE, init_help = __esm({ "src/constants/help.ts"() {
1900
2060
  "use strict";
1901
2061
  init_define_IGNITIFY_CLIENT_FILES();
1902
2062
  init_tenant();
1903
2063
  init_constants();
1904
- AXES = USAGE_AXES.join("|"), DIR2 = { dir: { text: "Folder that holds your task folders.", default: `$${ENV.TASKS_DIR}, else ${DEFAULT_TASKS_DIR}` } }, YES2 = { yes: { text: "Skip the question (needed when not interactive)." } }, RAW2 = { raw: { text: "Print the bytes exactly as received. Without it, the colours are kept on a colour terminal and every other control code is removed." } }, PAGE2 = { limit: { text: "Rows on the page. The server sets the default and the largest value." }, "after-id": { text: "Page cursor: the id of the last row of the page before." }, after: { text: "Only rows after this ISO 8601 time." } }, VERSION_HELP = { summary: "Print the client version, and the API version if the server answers.", usage: ["ignitify --version"], notes: ["Needs no key. Warns when this client is older than the API supports."] }, COMMAND_HELP = { [COMMANDS.HELP]: { summary: "Print the usage of every command, or of one command.", usage: ["ignitify help [<command>]"], args: { command: "A command name. Leave it out to print every command." }, notes: ["`ignitify <command> --help` prints the same page."], examples: ["ignitify help task:run"] }, [COMMANDS.LOGIN]: { summary: "Sign this machine in and save its API key.", usage: ["ignitify login [--name <label>] [--no-browser]"], flags: { name: { text: "A label for this machine's key; keeps two machines with one hostname apart.", default: "the hostname" }, "no-browser": { text: "Print the sign-in page, but do not open it." } }, notes: ["The key is saved in ~/.config/ignitify/credentials.json (mode 0600)."], examples: ["ignitify login", "ignitify login --name build-server --no-browser"] }, [COMMANDS.LOGOUT]: { summary: "Revoke this machine's key and delete the saved file.", usage: ["ignitify logout"] }, [COMMANDS.WHOAMI]: { summary: "Show who your key belongs to, your limits and usage.", usage: ["ignitify whoami"] }, [COMMANDS.QUOTA]: { summary: "Show what you hold now against your limits.", usage: ["ignitify quota"] }, [COMMANDS.TASK_CREATE]: { summary: "Register a task name and write its starter files.", usage: ["ignitify task:create <task-id> [--lang node|python|go|rust] [--local] [--no-init] [--no-scaffold] [--dir <base-dir>]"], args: { "task-id": "The task name: also the folder name and the slug in task.yaml." }, flags: { lang: { text: "Language of the starter files.", default: "node" }, local: { text: "Write the files only. Calls no server and needs no key." }, "no-init": { text: "Write the short starter that has only run(), without init code." }, "no-scaffold": { text: "Register the task name but write no files." }, ...DIR2 }, examples: ["ignitify task:create image-resize", "ignitify task:create image-resize --lang python --local"] }, [COMMANDS.TASK_LIST]: { summary: "List your tasks, one page at a time.", usage: ["ignitify task:list [--limit N] [--after-id <id>] [--after <iso>]"], flags: PAGE2 }, [COMMANDS.TASK_GET]: { summary: "Report a task's deploys, runs and next-step verdicts.", usage: ["ignitify task:get <task-id> [--dir <base-dir>]"], args: { "task-id": "The task folder name." }, flags: DIR2, notes: ["Shows whether the next deploy builds a new image and the next run boots a new VM, with the reasons."] }, [COMMANDS.TASK_VALIDATE]: { summary: "Check task.yaml on your machine; builds nothing.", usage: ["ignitify task:validate <task-id> [--dir <base-dir>] [--review-packages]"], args: { "task-id": "The task folder name." }, flags: { ...DIR2, "review-packages": { text: "Then ask the server for an AI review of build.system_packages." } }, notes: ["The review is advice only: the exit code never changes.", "Only package names and the runtime id leave this machine; never code, slug or secrets.", "It also checks that task.yaml still covers each @ignitify-integrations/* package in package.json. A listed package that is not in node_modules stops the check: run npm install first.", "With integrations installed, it sends their package names to the public npm registry (registry.npmjs.org) to find obsolete versions: a warning only. With no network, that warning is skipped with a note."] }, [COMMANDS.TASK_PACK]: { summary: "List the files task:deploy would upload; builds nothing.", usage: ["ignitify task:pack <task-id> [--dir <base-dir>]"], args: { "task-id": "The task folder name." }, flags: DIR2 }, [COMMANDS.TASK_DEPLOY]: { summary: "Upload the task source and wait until the image is built.", usage: ["ignitify task:deploy <task-id> [--dir <base-dir>] [--check]"], args: { "task-id": "The task folder name." }, flags: { ...DIR2, check: { text: "Run every local check, then stop. Nothing is uploaded." } }, notes: ["Ctrl+C stops waiting, not the build."], examples: ["ignitify task:deploy image-resize", "ignitify task:deploy image-resize --check"] }, [COMMANDS.TASK_RUN]: { summary: "Start one run of a task from its latest deployment.", usage: ["ignitify task:run <task-id> [--dir <base-dir>] [--deployment <id>] [--payload <json>] [--timeout-sec <n>] [--on-busy queue|reject] [--at <iso>] [--wait]"], args: { "task-id": "The task folder name." }, flags: { ...DIR2, deployment: { text: "Run this deployment instead of the task's latest deploy.", default: "the task's latest deploy" }, payload: { text: "JSON input for this run only.", default: "run.payload from task.yaml" }, "timeout-sec": { text: "Time limit in seconds for this run only.", default: "run.timeout_sec from task.yaml" }, "on-busy": { text: "When the run cannot start at once: queue waits for room (a limited time), reject fails at once.", default: "queue" }, at: { text: "Schedule the run for this ISO 8601 time. No run exists until it fires." }, wait: { text: "Follow the run to its end, then print it with its result." } }, notes: ["With --wait the exit code is 0 only when the run is COMPLETED. Ctrl+C stops waiting, not the run.", "--payload and --timeout-sec apply to this run only; task.yaml is never written.", "A bidirectional task runs one VM at a time; --on-busy covers that case too."], examples: ["ignitify task:run image-resize --wait", `ignitify task:run image-resize --payload '{"width":320}'`, "ignitify task:run image-resize --at 2027-01-15T03:00:00Z"] }, [COMMANDS.TASK_DELETE]: { summary: "Delete a task with all its deployments and runs.", usage: ["ignitify task:delete <task-id> [--dir <base-dir>] [--yes] [--json]"], args: { "task-id": "The task folder name, or the task id." }, flags: { ...DIR2, ...YES2, json: { text: "Print the plan and the result as one JSON document." } }, notes: ["Also removes the .ignitify/ link of every local folder linked to it; your files are kept."] }, [COMMANDS.DEPLOYMENT_LIST]: { summary: "List your deployments with the size of each image.", usage: ["ignitify deployment:list [--limit N] [--after-id <id>] [--after <iso>]"], flags: PAGE2 }, [COMMANDS.DEPLOYMENT_GET]: { summary: "Show one deployment: status, size, integrity.", usage: ["ignitify deployment:get <deployment-id>"], args: { "deployment-id": "The deployment id, from task:deploy or deployment:list." } }, [COMMANDS.DEPLOYMENT_LOGS]: { summary: "Print the build log of a deployment, as the build wrote it.", usage: ["ignitify deployment:logs <deployment-id> [--raw]"], args: { "deployment-id": "The deployment id." }, flags: RAW2, notes: ["The output goes to stdout; a truncated answer says so on stderr."] }, [COMMANDS.DEPLOYMENT_DELETE]: { summary: "Delete a deployment together with its runs.", usage: ["ignitify deployment:delete <deployment-id> [--yes]"], args: { "deployment-id": "The deployment id." }, flags: YES2 }, [COMMANDS.RUN_LIST]: { summary: "List your runs, with filters.", usage: ["ignitify run:list [--status S] [--deployment-id D] [--task-name N] [--preset P] [--correlation-id C] [--vm-id V] [--limit N] [--after-id <id>] [--after <iso>]"], flags: { status: { text: "Only runs in this status, for example FAILED." }, "deployment-id": { text: "Only runs of this deployment." }, "task-name": { text: "Only runs of this task." }, preset: { text: "Only runs on this instance class." }, "correlation-id": { text: "Only runs with this correlation id." }, "vm-id": { text: "Only runs that used this VM." }, ...PAGE2 } }, [COMMANDS.RUN_GET]: { summary: "Show one run: status, timing and the result of the task.", usage: ["ignitify run:get <run-id> [--full]"], args: { "run-id": "The run id, from task:run or run:list." }, flags: { full: { text: "Print the whole run as JSON, every field the API returns." } } }, [COMMANDS.RUN_LOGS]: { summary: "Print the console output of a run, as the task wrote it.", usage: ["ignitify run:logs <run-id> [--raw]"], args: { "run-id": "The run id." }, flags: RAW2, notes: ["The output goes to stdout; a truncated answer says so on stderr."] }, [COMMANDS.RUN_DELETE]: { summary: "Delete the record of a run; does not stop a live run.", usage: ["ignitify run:delete <run-id> [--yes]"], args: { "run-id": "The run id." }, flags: YES2 }, [COMMANDS.RUN_CANCEL]: { summary: "Stop a live run, gracefully by default.", usage: ["ignitify run:cancel <run-id> [--yes] [--force] [--grace <ms>]"], args: { "run-id": "The run id." }, flags: { ...YES2, force: { text: "Kill the run now. Also needed when the run is on a pinned VM." }, grace: { text: "Ask for a longer wind-down, in milliseconds. The server limits it." } } }, [COMMANDS.VM_LIST]: { summary: "List your live VMs with their state, age and idle time.", usage: ["ignitify vm:list [--pinned] [--deployment-id D] [--task-name N] [--preset P] [--older-than 24h] [--limit N] [--after-id <vm-id>] [--after <iso>]"], flags: { pinned: { text: "Only pinned VMs: they stay up until you delete them." }, "deployment-id": { text: "Only VMs of this deployment." }, "task-name": { text: "Only VMs of this task." }, preset: { text: "Only VMs of this instance class." }, "older-than": { text: "Only VMs older than this age, for example 24h." }, limit: PAGE2.limit, "after-id": { text: "Page cursor: the VM id of the last row of the page before." }, after: { text: "Only VMs booted after this ISO 8601 time." } } }, [COMMANDS.VM_GET]: { summary: "Show one VM: residency, state, age and its task nature.", usage: ["ignitify vm:get <vm-id>"], args: { "vm-id": "The VM id, from vm:list." } }, [COMMANDS.VM_DELETE]: { summary: "Tear down a VM by hand.", usage: ["ignitify vm:delete <vm-id> [--yes] [--force]"], args: { "vm-id": "The VM id." }, flags: { ...YES2, force: { text: "Allow the delete of a pinned VM. It does not skip the question." } } }, [COMMANDS.SECRET_SET]: { summary: "Store a secret value under a name, read from stdin.", usage: ["ignitify secret:set <NAME> --stdin"], args: { NAME: "The secret name. List it in `secrets` in task.yaml to use it." }, flags: { stdin: { text: "Read the value from stdin. A value on the command line is refused." } }, examples: ['printf %s "$VALUE" | ignitify secret:set DB_PASSWORD --stdin'] }, [COMMANDS.SECRET_LIST]: { summary: "List the names of your secrets.", usage: ["ignitify secret:list"] }, [COMMANDS.SECRET_DELETE]: { summary: "Delete a secret.", usage: ["ignitify secret:delete <NAME> [--yes]"], args: { NAME: "The secret name." }, flags: YES2 }, [COMMANDS.INTEGRATION_ADD]: { summary: "Add an integration package to a Node task and write its settings into task.yaml.", usage: ["ignitify integration:add <name> --task <slug> [--yes | --dry-run] [--dir <base-dir>]"], args: { name: "The integration name. The npm package is @ignitify-integrations/<name>." }, flags: { task: { text: "The task folder name to add the integration to." }, yes: { text: "Apply the plan without the question (needed when not interactive)." }, "dry-run": { text: "Show the plan, then stop. Nothing in the task is changed." }, ...DIR2 }, notes: ["It downloads the latest @ignitify-integrations/<name> with npm pack into a temporary folder and reads the plan from that file. Nothing in the task changes before you answer y.", "The plan shows every task.yaml change, then what you add yourself: secret values and hosts that differ per task.", "After y (or with --yes), it runs npm install @ignitify-integrations/<name>@<that version> in the task folder (a normal dependency, with --ignore-scripts as the build uses), then writes task.yaml. If the install fails, task.yaml is not written.", "An N, --dry-run or a refusal changes nothing: no package.json, lockfile, node_modules or task.yaml change.", "A setting that conflicts with task.yaml, or more egress hosts than the platform allows, is refused and nothing is written.", "Adding the same integration again changes nothing. To upgrade, add it again. To remove it, run npm uninstall; the settings it added stay yours to delete."], examples: ["ignitify integration:add s3-storage --task launch-event", "ignitify integration:add stream --task launch-event --dry-run"] }, [COMMANDS.SCHEDULE_LIST]: { summary: "List runs that wait for a time, soonest first.", usage: ["ignitify schedule:list"] }, [COMMANDS.SCHEDULE_CANCEL]: { summary: "Cancel one scheduled run so that it never fires.", usage: ["ignitify schedule:cancel <sch-id> [--yes]"], args: { "sch-id": "The schedule id, from task:run --at or schedule:list." }, flags: YES2, notes: ['"0 rows" means it is already firing or fired.'] }, [COMMANDS.HOOK_CREATE]: { summary: "Create a webhook that starts a run of one task.", usage: ["ignitify hook:create <deployment-id> --task-name <name>"], args: { "deployment-id": "The deployment the webhook belongs to." }, flags: { "task-name": { text: "The task that each run starts. Required." } }, notes: ["The reply carries a `secret` shown ONCE: hook:list never returns it."] }, [COMMANDS.HOOK_LIST]: { summary: "List the webhooks of a deployment.", usage: ["ignitify hook:list <deployment-id> [--limit N] [--after-id <id>] [--after <iso>]"], args: { "deployment-id": "The deployment id." }, flags: PAGE2 }, [COMMANDS.HOOK_DELETE]: { summary: "Delete a webhook.", usage: ["ignitify hook:delete <hook-resource-id> [--yes]"], args: { "hook-resource-id": "The HOOK's id (hok_\u2026), not the deployment's." }, flags: YES2 }, [COMMANDS.NOTIFY_SET]: { summary: "Set or clear the callback URL of a deployment.", usage: ["ignitify notify:set <deployment-id> --url <https://\u2026> --secret-stdin", "ignitify notify:set <deployment-id> --clear"], args: { "deployment-id": "The deployment id." }, flags: { url: { text: "The HTTPS address to call. Needs --secret-stdin." }, "secret-stdin": { text: "Read the signing secret from stdin. Needed with --url." }, clear: { text: "Remove the callback URL. Cannot be used with --url." } }, notes: ["There is no --secret flag: a value on argv is in `ps` and in your shell history."] }, [COMMANDS.USAGE]: { summary: "Show what you used in one month, by axis.", usage: [`ignitify usage [--period YYYY-MM] [--axis ${AXES}] [--vm <vm-id>] [--run <run-id>] [--runs|--vms] [--json]`], flags: { period: { text: "The month to show, as YYYY-MM.", default: "this month" }, axis: { text: "Show one axis only." }, vm: { text: "Only the usage of this VM." }, run: { text: "Only the usage of this run." }, runs: { text: "Add the lines behind the totals, one per run." }, vms: { text: "Add the lines behind the totals, one per VM." }, json: { text: "Print the report as JSON." } } }, [COMMANDS.USAGE_EXPORT]: { summary: "Export the usage lines of a month as JSON or CSV.", usage: [`ignitify usage:export [--period YYYY-MM] [--axis ${AXES}] [--vm <vm-id>] [--run <run-id>] (--json|--csv)`], flags: { period: { text: "The month to export, as YYYY-MM.", default: "this month" }, axis: { text: "Export one axis only." }, vm: { text: "Only the lines of this VM." }, run: { text: "Only the lines of this run." }, json: { text: "Print the lines as a JSON array." }, csv: { text: "Print the lines as CSV." } }, notes: ["Name exactly one format: --json or --csv."] } }, HELP_PAGE = { TITLE: "ignitify", TAGLINE: "create, deploy and run tasks", USAGE: "ignitify <command> [arguments] [flags]", USAGE_NOTE: "Every command is <resource>:<verb>.", QUICK_START: [{ command: "ignitify login", text: "Sign this machine in." }, { command: "ignitify task:create my-task", text: "Register a task and write its files." }, { command: "ignitify task:deploy my-task", text: "Build it on the platform." }, { command: "ignitify task:run my-task --wait", text: "Run it and print the result." }], GROUPS: [{ title: "Tasks", resources: ["task"] }, { title: "Deployments", resources: ["deployment"] }, { title: "Runs", resources: ["run"] }, { title: "VMs", resources: ["vm"] }, { title: "Secrets", resources: ["secret"] }, { title: "Integrations", resources: ["integration"] }, { title: "Schedules", resources: ["schedule"] }, { title: "Webhooks", resources: ["hook", "notify"] }, { title: "Account and usage", resources: ["login", "logout", "whoami", "quota", "usage", "help"] }], GLOBAL_FLAGS: [{ flag: "--json", text: "Print the result as one JSON document on stdout." }, { flag: "-h, --help", text: "Print help for the command." }, { flag: "-y, --yes", text: "Answer yes to a delete's question (for scripts)." }, { flag: "-v, --version", text: "Print the client and API version (first word only)." }], ENV: [{ name: ENV.KEY, text: "Your API key, for CI. Wins over the saved login." }, { name: ENV.TASKS_DIR, text: `Folder that holds your task folders (default: ${DEFAULT_TASKS_DIR}).` }, { name: "NO_COLOR", text: "Set to any value to turn colour off." }], NOTES: ["Delete commands always ask first; --force unlocks a refusal, it does not skip the question.", "A flag or argument a command does not take is refused, not ignored.", "On error the CLI prints a clean message and exits non-zero."], DOCS: "https://ignitify.dev/docs", MORE: "Run 'ignitify <command> --help' for the flags of one command.", H: { USAGE: "Usage", QUICK_START: "Quick start", GLOBAL_FLAGS: "Global flags", ENV: "Environment", NOTES: "Good to know", DOCS: "Docs", DESCRIPTION: "Description", ARGUMENTS: "Arguments", FLAGS: "Flags", EXAMPLES: "Examples" }, COLUMNS: { COMMAND: "COMMAND", WHAT: "WHAT IT DOES", FLAG: "FLAG", VALUE: "VALUE", DEFAULT: "DEFAULT", DESCRIPTION: "DESCRIPTION", NAME: "NAME", ARG: "ARGUMENT" }, SWITCH: "switch", DEFAULT: (value) => `Default: ${value}.`, REQUIRED: "required" };
2064
+ OWNER_ONLY = "Needs an owner key (from ignitify login). A user key is refused with owner-required.", PERMISSION_NOTES = [ACCESS.TASKS_WRITE_NOTE, ACCESS.SECRETS_READ_NOTE, ACCESS.HOOKS_WRITE_NOTE], AXES = USAGE_AXES.join("|"), DIR2 = { dir: { text: "Folder that holds your task folders.", default: `$${ENV.TASKS_DIR}, else ${DEFAULT_TASKS_DIR}` } }, YES2 = { yes: { text: "Skip the question (needed when not interactive)." } }, RAW2 = { raw: { text: "Print the bytes exactly as received. Without it, the colours are kept on a colour terminal and every other control code is removed." } }, SERIES_DAYS = { days: { text: `How many UTC days to show, today included. More than ${92} is read as ${92}.`, default: `${30}` } }, FAILURES_DAYS = { days: { text: `How many UTC days to show, today included. More than ${31} is read as ${31}.`, default: `${30}` } }, PAGE2 = { limit: { text: "Rows on the page. The server sets the default and the largest value." }, "after-id": { text: "Page cursor: the id of the last row of the page before." }, after: { text: "Only rows after this ISO 8601 time." } }, VERSION_HELP = { summary: "Print the client version, and the API version if the server answers.", usage: ["ignitify --version"], notes: ["Needs no key. Warns when this client is older than the API supports."] }, COMMAND_HELP = { [COMMANDS.HELP]: { summary: "Print the usage of every command, or of one command.", usage: ["ignitify help [<command>]"], args: { command: "A command name. Leave it out to print every command." }, notes: ["`ignitify <command> --help` prints the same page."], examples: ["ignitify help task:run"] }, [COMMANDS.LOGIN]: { summary: "Sign this machine in and save its API key.", usage: ["ignitify login [--name <label>] [--no-browser]"], flags: { name: { text: "A label for this machine's key; keeps two machines with one hostname apart.", default: "the hostname" }, "no-browser": { text: "Print the sign-in page, but do not open it." } }, notes: ["The key is saved in ~/.config/ignitify/credentials.json (mode 0600)."], examples: ["ignitify login", "ignitify login --name build-server --no-browser"] }, [COMMANDS.LOGOUT]: { summary: "Revoke this machine's key and delete the saved file.", usage: ["ignitify logout"] }, [COMMANDS.WHOAMI]: { summary: "Show who your key belongs to, your limits and usage.", usage: ["ignitify whoami"] }, [COMMANDS.QUOTA]: { summary: "Show what you hold now against your limits.", usage: ["ignitify quota"] }, [COMMANDS.TASK_CREATE]: { summary: "Register a task name and write its starter files.", usage: ["ignitify task:create <task-id> [--lang node|python|go|rust] [--local] [--no-init] [--no-scaffold] [--dir <base-dir>]"], args: { "task-id": "The task name: also the folder name and the slug in task.yaml." }, flags: { lang: { text: "Language of the starter files.", default: "node" }, local: { text: "Write the files only. Calls no server and needs no key." }, "no-init": { text: "Write the short starter that has only run(), without init code." }, "no-scaffold": { text: "Register the task name but write no files." }, ...DIR2 }, examples: ["ignitify task:create image-resize", "ignitify task:create image-resize --lang python --local"] }, [COMMANDS.TASK_LIST]: { summary: "List your tasks, one page at a time.", usage: ["ignitify task:list [--limit N] [--after-id <id>] [--after <iso>]"], flags: PAGE2 }, [COMMANDS.TASK_GET]: { summary: "Report a task's deploys, runs and next-step verdicts.", usage: ["ignitify task:get <task-id> [--dir <base-dir>]"], args: { "task-id": "The task folder name." }, flags: DIR2, notes: ["Shows whether the next deploy builds a new image and the next run boots a new VM, with the reasons."] }, [COMMANDS.TASK_VALIDATE]: { summary: "Check task.yaml on your machine; builds nothing.", usage: ["ignitify task:validate <task-id> [--dir <base-dir>] [--review-packages]"], args: { "task-id": "The task folder name." }, flags: { ...DIR2, "review-packages": { text: "Then ask the server for an AI review of build.system_packages." } }, notes: ["The review is advice only: the exit code never changes.", "Only package names and the runtime id leave this machine; never code, slug or secrets.", "It also checks that task.yaml still covers each @ignitify-integrations/* package in package.json. A listed package that is not in node_modules stops the check: run npm install first.", "With integrations installed, it sends their package names to the public npm registry (registry.npmjs.org) to find obsolete versions: a warning only. With no network, that warning is skipped with a note."] }, [COMMANDS.TASK_PACK]: { summary: "List the files task:deploy would upload; builds nothing.", usage: ["ignitify task:pack <task-id> [--dir <base-dir>]"], args: { "task-id": "The task folder name." }, flags: DIR2 }, [COMMANDS.TASK_DEPLOY]: { summary: "Upload the task source and wait until the image is built.", usage: ["ignitify task:deploy <task-id> [--dir <base-dir>] [--check]"], args: { "task-id": "The task folder name." }, flags: { ...DIR2, check: { text: "Run every local check, then stop. Nothing is uploaded." } }, notes: ["Ctrl+C stops waiting, not the build."], examples: ["ignitify task:deploy image-resize", "ignitify task:deploy image-resize --check"] }, [COMMANDS.TASK_RUN]: { summary: "Start one run of a task from its latest deployment.", usage: ["ignitify task:run <task-id> [--dir <base-dir>] [--deployment <id>] [--payload <json>] [--timeout-sec <n>] [--on-busy queue|reject] [--at <iso>] [--idempotency-key <key>] [--wait]"], args: { "task-id": "The task folder name." }, flags: { ...DIR2, deployment: { text: "Run this deployment instead of the task's latest deploy.", default: "the task's latest deploy" }, payload: { text: "JSON input for this run only.", default: "run.payload from task.yaml" }, "timeout-sec": { text: "Time limit in seconds for this run only.", default: "run.timeout_sec from task.yaml" }, "on-busy": { text: "When the run cannot start at once: queue waits for room (a limited time), reject fails at once.", default: "queue" }, at: { text: "Schedule the run for this ISO 8601 time. No run exists until it fires." }, "idempotency-key": { text: "Start at most one run for this key (8 to 255 characters of A-Z a-z 0-9 . _ : -). A retry with the same key gets the first answer again." }, wait: { text: "Follow the run to its end, then print it with its result." } }, notes: ["With --wait the exit code is 0 only when the run is COMPLETED. Ctrl+C stops waiting, not the run.", "--payload and --timeout-sec apply to this run only; task.yaml is never written.", "A bidirectional task runs one VM at a time; --on-busy covers that case too.", "Use --idempotency-key when a script retries after a network error. Use a new key for each new run; the same key with a different run is refused. Pin --deployment in such a script: a new deploy between two tries changes the request."], examples: ["ignitify task:run image-resize --wait", `ignitify task:run image-resize --payload '{"width":320}'`, "ignitify task:run image-resize --at 2027-01-15T03:00:00Z"] }, [COMMANDS.TASK_DELETE]: { summary: "Delete a task with all its deployments and runs.", usage: ["ignitify task:delete <task-id> [--dir <base-dir>] [--yes] [--json]"], args: { "task-id": "The task folder name, or the task id." }, flags: { ...DIR2, ...YES2, json: { text: "Print the plan and the result as one JSON document." } }, notes: ["Also removes the .ignitify/ link of every local folder linked to it; your files are kept."] }, [COMMANDS.DEPLOYMENT_LIST]: { summary: "List your deployments with the size of each image.", usage: ["ignitify deployment:list [--limit N] [--after-id <id>] [--after <iso>]"], flags: PAGE2 }, [COMMANDS.DEPLOYMENT_GET]: { summary: "Show one deployment: status, size, integrity.", usage: ["ignitify deployment:get <deployment-id>"], args: { "deployment-id": "The deployment id, from task:deploy or deployment:list." } }, [COMMANDS.DEPLOYMENT_LOGS]: { summary: "Print the build log of a deployment, as the build wrote it.", usage: ["ignitify deployment:logs <deployment-id> [--raw]"], args: { "deployment-id": "The deployment id." }, flags: RAW2, notes: ["The output goes to stdout; a truncated answer says so on stderr."] }, [COMMANDS.DEPLOYMENT_DELETE]: { summary: "Delete a deployment together with its runs.", usage: ["ignitify deployment:delete <deployment-id> [--yes]"], args: { "deployment-id": "The deployment id." }, flags: YES2 }, [COMMANDS.RUN_LIST]: { summary: "List your runs, with filters.", usage: ["ignitify run:list [--status S] [--deployment-id D] [--task-name N] [--preset P] [--correlation-id C] [--vm-id V] [--limit N] [--after-id <id>] [--after <iso>]"], flags: { status: { text: "Only runs in this status, for example FAILED." }, "deployment-id": { text: "Only runs of this deployment." }, "task-name": { text: "Only runs of this task." }, preset: { text: "Only runs on this instance class." }, "correlation-id": { text: "Only runs with this correlation id." }, "vm-id": { text: "Only runs that used this VM." }, ...PAGE2 } }, [COMMANDS.RUN_GET]: { summary: "Show one run: status, timing and the result of the task.", usage: ["ignitify run:get <run-id> [--full]"], args: { "run-id": "The run id, from task:run or run:list." }, flags: { full: { text: "Print the whole run as JSON, every field the API returns." } } }, [COMMANDS.RUN_LOGS]: { summary: "Print the console output of a run, as the task wrote it.", usage: ["ignitify run:logs <run-id> [--follow] [--tail N] [--raw]"], args: { "run-id": "The run id." }, flags: { ...RAW2, follow: { text: "Keep printing new output until the run has ended." }, tail: { text: "Print only the last N lines of the output so far. With --follow, go on from there." } }, notes: ["Output is best-effort. The host keeps it for 30 days at most. A read that is cut at the size cap says so on stderr. Use --follow to watch a live run.", "The output goes to stdout. With --json and --follow or --tail, each chunk is one JSON line with the offset to read from next.", "Ctrl+C stops following, not the run."], examples: ["ignitify run:logs <run-id> --follow", "ignitify run:logs <run-id> --tail 50"] }, [COMMANDS.RUN_TIMELINE]: { summary: "Show what happened to a run: one line per event, then how it ended.", usage: ["ignitify run:timeline <run-id> [--follow]"], args: { "run-id": "The run id." }, flags: { follow: { text: "Keep printing new events until the run has ended." } }, notes: ["When the run has ended, the exit code is 0 only when it is COMPLETED. A live run read without --follow exits 0.", "With --json --follow, each page of events is one JSON line. Ctrl+C stops following, not the run."], examples: ["ignitify run:timeline <run-id> --follow"] }, [COMMANDS.RUN_FAILURES]: { summary: "Show how your runs failed: by class, then the tasks that fail most.", usage: ["ignitify run:failures [--days N] [--json]"], flags: { ...FAILURES_DAYS, json: { text: "Print the report as JSON." } }, notes: ["A failure is a run that ended FAILED or CANCELLED. The task list leaves out cancelled runs.", "Classes: task (your code or deployment), limit (a plan limit), cancelled, platform (our side)."], examples: ["ignitify run:failures", "ignitify run:failures --days 7"] }, [COMMANDS.NOTIFICATION_LIST]: { summary: "List your notifications and alerts, newest first.", usage: ["ignitify notification:list [--unread | --open] [--kind <kind>] [--limit N] [--json]"], flags: { unread: { text: "Only the notifications you have not read." }, open: { text: "Only the alerts that are still open." }, kind: { text: `Only this kind: ${NOTIFICATION_KINDS.join(", ")}.` }, limit: { text: `How many to show. More than ${NOTIFICATION_PAGE_MAX} is read as ${NOTIFICATION_PAGE_MAX}.`, default: `${NOTIFICATION_PAGE_DEFAULT}` }, json: { text: "Print the page as JSON." } }, notes: [`We keep notifications for ${NOTIFICATION_RETENTION_DAYS} days. Mark them read in the app; this command does not change them.`], examples: ["ignitify notification:list --unread", "ignitify notification:list --open --json"] }, [COMMANDS.RUN_DELETE]: { summary: "Delete the record of a run; does not stop a live run.", usage: ["ignitify run:delete <run-id> [--yes]"], args: { "run-id": "The run id." }, flags: YES2 }, [COMMANDS.RUN_CANCEL]: { summary: "Stop a live run, gracefully by default.", usage: ["ignitify run:cancel <run-id> [--yes] [--force] [--grace <ms>]"], args: { "run-id": "The run id." }, flags: { ...YES2, force: { text: "Kill the run now. Also needed when the run is on a pinned VM." }, grace: { text: "Ask for a longer wind-down, in milliseconds. The server limits it." } } }, [COMMANDS.VM_LIST]: { summary: "List your live VMs with their state, age and idle time.", usage: ["ignitify vm:list [--pinned] [--deployment-id D] [--task-name N] [--preset P] [--older-than 24h] [--limit N] [--after-id <vm-id>] [--after <iso>]"], flags: { pinned: { text: "Only pinned VMs: they stay up until you delete them." }, "deployment-id": { text: "Only VMs of this deployment." }, "task-name": { text: "Only VMs of this task." }, preset: { text: "Only VMs of this instance class." }, "older-than": { text: "Only VMs older than this age, for example 24h." }, limit: PAGE2.limit, "after-id": { text: "Page cursor: the VM id of the last row of the page before." }, after: { text: "Only VMs booted after this ISO 8601 time." } } }, [COMMANDS.VM_GET]: { summary: "Show one VM: residency, state, age and its task nature.", usage: ["ignitify vm:get <vm-id>"], args: { "vm-id": "The VM id, from vm:list." } }, [COMMANDS.VM_DELETE]: { summary: "Tear down a VM by hand.", usage: ["ignitify vm:delete <vm-id> [--yes] [--force]"], args: { "vm-id": "The VM id." }, flags: { ...YES2, force: { text: "Allow the delete of a pinned VM. It does not skip the question." } } }, [COMMANDS.SECRET_SET]: { summary: "Store a secret value under a name, read from stdin.", usage: ["ignitify secret:set <NAME> --stdin"], args: { NAME: "The secret name. List it in `secrets` in task.yaml to use it." }, flags: { stdin: { text: "Read the value from stdin. A value on the command line is refused." } }, examples: ['printf %s "$VALUE" | ignitify secret:set DB_PASSWORD --stdin'] }, [COMMANDS.SECRET_LIST]: { summary: "List the names of your secrets.", usage: ["ignitify secret:list"] }, [COMMANDS.SECRET_DELETE]: { summary: "Delete a secret.", usage: ["ignitify secret:delete <NAME> [--yes]"], args: { NAME: "The secret name." }, flags: YES2 }, [COMMANDS.INTEGRATION_ADD]: { summary: "Add an integration package to a Node task and write its settings into task.yaml.", usage: ["ignitify integration:add <name> --task <slug> [--yes | --dry-run] [--dir <base-dir>]"], args: { name: "The integration name. The npm package is @ignitify-integrations/<name>." }, flags: { task: { text: "The task folder name to add the integration to." }, yes: { text: "Apply the plan without the question (needed when not interactive)." }, "dry-run": { text: "Show the plan, then stop. Nothing in the task is changed." }, ...DIR2 }, notes: ["It downloads the latest @ignitify-integrations/<name> with npm pack into a temporary folder and reads the plan from that file. Nothing in the task changes before you answer y.", "The plan shows every task.yaml change, then what you add yourself: secret values and hosts that differ per task.", "After y (or with --yes), it runs npm install @ignitify-integrations/<name>@<that version> in the task folder (a normal dependency, with --ignore-scripts as the build uses), then writes task.yaml. If the install fails, task.yaml is not written.", "An N, --dry-run or a refusal changes nothing: no package.json, lockfile, node_modules or task.yaml change.", "A setting that conflicts with task.yaml, or more egress hosts than the platform allows, is refused and nothing is written.", "Adding the same integration again changes nothing. To upgrade, add it again. To remove it, run npm uninstall; the settings it added stay yours to delete."], examples: ["ignitify integration:add s3-storage --task launch-event", "ignitify integration:add stream --task launch-event --dry-run"] }, [COMMANDS.SCHEDULE_LIST]: { summary: "List runs that wait for a time, soonest first.", usage: ["ignitify schedule:list"] }, [COMMANDS.SCHEDULE_CANCEL]: { summary: "Cancel one scheduled run so that it never fires.", usage: ["ignitify schedule:cancel <sch-id> [--yes]"], args: { "sch-id": "The schedule id, from task:run --at or schedule:list." }, flags: YES2, notes: ['"0 rows" means it is already firing or fired.'] }, [COMMANDS.HOOK_CREATE]: { summary: "Create a webhook that starts a run of one task.", usage: ["ignitify hook:create <deployment-id> --task-name <name>"], args: { "deployment-id": "The deployment the webhook belongs to." }, flags: { "task-name": { text: "The task that each run starts. Required." } }, notes: ["The reply carries a `secret` shown ONCE: hook:list never returns it."] }, [COMMANDS.HOOK_LIST]: { summary: "List the webhooks of a deployment.", usage: ["ignitify hook:list <deployment-id> [--limit N] [--after-id <id>] [--after <iso>]"], args: { "deployment-id": "The deployment id." }, flags: PAGE2 }, [COMMANDS.HOOK_DELETE]: { summary: "Delete a webhook.", usage: ["ignitify hook:delete <hook-resource-id> [--yes]"], args: { "hook-resource-id": "The HOOK's id (hok_\u2026), not the deployment's." }, flags: YES2 }, [COMMANDS.NOTIFY_SET]: { summary: "Set or clear the callback URL of a deployment.", usage: ["ignitify notify:set <deployment-id> --url <https://\u2026> --secret-stdin", "ignitify notify:set <deployment-id> --clear"], args: { "deployment-id": "The deployment id." }, flags: { url: { text: "The HTTPS address to call. Needs --secret-stdin." }, "secret-stdin": { text: "Read the signing secret from stdin. Needed with --url." }, clear: { text: "Remove the callback URL. Cannot be used with --url." } }, notes: ["There is no --secret flag: a value on argv is in `ps` and in your shell history."] }, [COMMANDS.USAGE]: { summary: "Show what you used in one month, by axis.", usage: [`ignitify usage [--period YYYY-MM] [--axis ${AXES}] [--vm <vm-id>] [--run <run-id>] [--runs|--vms] [--json]`], flags: { period: { text: "The month to show, as YYYY-MM.", default: "this month" }, axis: { text: "Show one axis only." }, vm: { text: "Only the usage of this VM." }, run: { text: "Only the usage of this run." }, runs: { text: "Add the lines behind the totals, one per run." }, vms: { text: "Add the lines behind the totals, one per VM." }, json: { text: "Print the report as JSON." } } }, [COMMANDS.USAGE_EXPORT]: { summary: "Export the usage lines of a month as JSON or CSV.", usage: [`ignitify usage:export [--period YYYY-MM] [--axis ${AXES}] [--vm <vm-id>] [--run <run-id>] (--json|--csv)`], flags: { period: { text: "The month to export, as YYYY-MM.", default: "this month" }, axis: { text: "Export one axis only." }, vm: { text: "Only the lines of this VM." }, run: { text: "Only the lines of this run." }, json: { text: "Print the lines as a JSON array." }, csv: { text: "Print the lines as CSV." } }, notes: ["Name exactly one format: --json or --csv."] }, [COMMANDS.USAGE_SERIES]: { summary: "Show your usage per day: runs, run durations, compute and egress.", usage: ["ignitify usage:series [--days N] [--json]"], flags: { ...SERIES_DAYS, json: { text: "Print the series as JSON." } }, notes: ["Days are UTC. A run counts on the day it ended. Compute counts when the VM closes, so a running VM shows none yet."], examples: ["ignitify usage:series", "ignitify usage:series --days 7 --json"] }, [COMMANDS.PERMISSION_LIST]: { summary: "List every permission a group can grant, and what it allows.", usage: ["ignitify permission:list [--json]"], notes: ["Needs no key and no server: the list is built into the CLI.", ...PERMISSION_NOTES] }, [COMMANDS.USER_CREATE]: { summary: "Create a user: a name that holds keys and is in groups.", usage: ["ignitify user:create <name> [--json]"], args: { name: "1 to 63 lower case letters, digits and -, unique in your account." }, notes: ["A user has no password and cannot sign in. Add it to a group to give it permissions.", OWNER_ONLY], examples: ["ignitify user:create github-ci"] }, [COMMANDS.USER_LIST]: { summary: "List your users.", usage: ["ignitify user:list [--json]"], notes: [OWNER_ONLY] }, [COMMANDS.USER_GET]: { summary: "Show one user: its groups, permissions and live keys.", usage: ["ignitify user:get <name> [--json]"], args: { name: "The user name." }, notes: [OWNER_ONLY] }, [COMMANDS.USER_REMOVE]: { summary: "Remove a user and revoke all of its keys.", usage: ["ignitify user:remove <name> [--yes]"], args: { name: "The user name." }, flags: YES2, notes: ["Every key of the user stops working at once. The name can be used again.", OWNER_ONLY] }, [COMMANDS.GROUP_CREATE]: { summary: "Create a group with a set of permissions.", usage: ["ignitify group:create <name> --permissions <p1,p2,\u2026> [--json]"], args: { name: "1 to 63 lower case letters, digits and -, unique in your account." }, flags: { permissions: { text: "The permissions the group grants, comma-separated. Required. See permission:list." } }, notes: [...PERMISSION_NOTES, OWNER_ONLY], examples: ["ignitify group:create readers --permissions tasks.read,deployments.read,runs.read,usage.read", "ignitify group:create ci --permissions tasks.read,deployments.read,deployments.write,runs.read,runs.write"] }, [COMMANDS.GROUP_LIST]: { summary: "List your groups and their permissions.", usage: ["ignitify group:list [--json]"], notes: [OWNER_ONLY] }, [COMMANDS.GROUP_GET]: { summary: "Show one group: its permissions and users.", usage: ["ignitify group:get <name> [--json]"], args: { name: "The group name." }, notes: [OWNER_ONLY] }, [COMMANDS.GROUP_GRANT]: { summary: "Add one permission to a group.", usage: ["ignitify group:grant <name> <permission>"], args: { name: "The group name.", permission: "One permission from permission:list." }, notes: [...PERMISSION_NOTES, OWNER_ONLY], examples: ["ignitify group:grant readers secrets.read"] }, [COMMANDS.GROUP_REVOKE]: { summary: "Remove one permission from a group.", usage: ["ignitify group:revoke <name> <permission>"], args: { name: "The group name.", permission: "The permission to remove." }, notes: ["The users of the group lose it at their next request.", OWNER_ONLY] }, [COMMANDS.GROUP_ADD_USER]: { summary: "Put a user in a group.", usage: ["ignitify group:add-user <group> <user>"], args: { group: "The group name.", user: "The user name." }, notes: [OWNER_ONLY], examples: ["ignitify group:add-user ci github-ci"] }, [COMMANDS.GROUP_REMOVE_USER]: { summary: "Take a user out of a group.", usage: ["ignitify group:remove-user <group> <user>"], args: { group: "The group name.", user: "The user name." }, notes: ["The user and its keys stay; they lose what the group granted.", OWNER_ONLY] }, [COMMANDS.GROUP_DELETE]: { summary: "Delete a group. Its users and keys stay.", usage: ["ignitify group:delete <name> [--yes]"], args: { name: "The group name." }, flags: YES2, notes: ["The users of the group lose the permissions it granted.", OWNER_ONLY] }, [COMMANDS.KEY_CREATE]: { summary: "Create an API key for a user.", usage: ["ignitify key:create --user <name> --label <text> [--out <file>] [--json]"], flags: { user: { text: "The user the key belongs to. Required." }, label: { text: "A name for the key, such as where it is used. Required." }, out: { text: "Write the key to this new file (mode 0600) and print only its id. Fails when the file exists." } }, notes: ["The key is shown once. Without --out it is printed; hand it over by hand and do not keep a copy.", "Owner keys come from ignitify login. This command makes user keys only.", OWNER_ONLY], examples: ["ignitify key:create --user github-ci --label github --out ./github-ci.key"] }, [COMMANDS.KEY_LIST]: { summary: "List the live keys of your account. Never a raw key.", usage: ["ignitify key:list [--user <name>] [--json]"], flags: { user: { text: "Only the keys of this user." } }, notes: ["The user column says owner for an owner key.", OWNER_ONLY] }, [COMMANDS.KEY_REVOKE]: { summary: "Revoke a key. It stops working at once.", usage: ["ignitify key:revoke <key-resource-id> [--yes]"], args: { "key-resource-id": "The key id, from key:list." }, flags: YES2, notes: [OWNER_ONLY] } }, HELP_PAGE = { TITLE: "ignitify", TAGLINE: "create, deploy and run tasks", USAGE: "ignitify <command> [arguments] [flags]", USAGE_NOTE: "Every command is <resource>:<verb>.", QUICK_START: [{ command: "ignitify login", text: "Sign this machine in." }, { command: "ignitify task:create my-task", text: "Register a task and write its files." }, { command: "ignitify task:deploy my-task", text: "Build it on the platform." }, { command: "ignitify task:run my-task --wait", text: "Run it and print the result." }], GROUPS: [{ title: "Tasks", resources: ["task"] }, { title: "Deployments", resources: ["deployment"] }, { title: "Runs", resources: ["run"] }, { title: "VMs", resources: ["vm"] }, { title: "Secrets", resources: ["secret"] }, { title: "Integrations", resources: ["integration"] }, { title: "Schedules", resources: ["schedule"] }, { title: "Webhooks", resources: ["hook", "notify"] }, { title: "Access", resources: ["permission", "user", "group", "key"] }, { title: "Account and usage", resources: ["login", "logout", "whoami", "quota", "usage", "notification", "help"] }], GLOBAL_FLAGS: [{ flag: "--json", text: "Print the result as one JSON document on stdout." }, { flag: "-h, --help", text: "Print help for the command." }, { flag: "-y, --yes", text: "Answer yes to a delete's question (for scripts)." }, { flag: "-v, --version", text: "Print the client and API version (first word only)." }], ENV: [{ name: ENV.KEY, text: "Your API key, for CI. Wins over the saved login." }, { name: ENV.TASKS_DIR, text: `Folder that holds your task folders (default: ${DEFAULT_TASKS_DIR}).` }, { name: "NO_COLOR", text: "Set to any value to turn colour off." }], NOTES: ["Delete commands always ask first; --force unlocks a refusal, it does not skip the question.", "A flag or argument a command does not take is refused, not ignored.", "On error the CLI prints a clean message and exits non-zero."], DOCS: SITE_LINKS.DOCS, MORE: "Run 'ignitify <command> --help' for the flags of one command.", H: { USAGE: "Usage", QUICK_START: "Quick start", GLOBAL_FLAGS: "Global flags", ENV: "Environment", NOTES: "Good to know", DOCS: "Docs", DESCRIPTION: "Description", ARGUMENTS: "Arguments", FLAGS: "Flags", EXAMPLES: "Examples" }, COLUMNS: { COMMAND: "COMMAND", WHAT: "WHAT IT DOES", FLAG: "FLAG", VALUE: "VALUE", DEFAULT: "DEFAULT", DESCRIPTION: "DESCRIPTION", NAME: "NAME", ARG: "ARGUMENT" }, SWITCH: "switch", DEFAULT: (value) => `Default: ${value}.`, REQUIRED: "required" };
1905
2065
  } });
1906
2066
  function entryOf(name) {
1907
2067
  if (name === VERSION_NAME) return { help: VERSION_HELP, meta: VERSION_META };
@@ -1942,7 +2102,7 @@ function placeholderOf(forms, flag) {
1942
2102
  }
1943
2103
  function flagRows(help, meta) {
1944
2104
  let words = help.flags ?? {}, known = { ...meta.flags, ...GLOBAL_FLAGS }, shortOf = Object.fromEntries(Object.entries(SHORT_FLAGS).map(([s, l]) => [l, s]));
1945
- return Object.entries(words).filter(([flag]) => Object.hasOwn(known, flag) && !HIDDEN_FLAGS.includes(flag)).map(([flag, w]) => {
2105
+ return Object.entries(words).filter(([flag]) => Object.hasOwn(known, flag)).map(([flag, w]) => {
1946
2106
  let short = shortOf[flag], value = known[flag] === "value" ? placeholderOf(help.usage, flag) : HELP_PAGE.SWITCH, text = w.default === void 0 ? w.text : `${w.text} ${HELP_PAGE.DEFAULT(w.default)}`;
1947
2107
  return [short === void 0 ? `--${flag}` : `-${short}, --${flag}`, value, text];
1948
2108
  });
@@ -2068,11 +2228,17 @@ function explainLimitRefusal(e) {
2068
2228
  return typeof summary != "string" || summary === "" ? null : [`\u2717 Run refused \u2014 a monthly limit is reached (${typeof reason == "string" ? reason : "429"}).`, ` ${summary}`].join(`
2069
2229
  `);
2070
2230
  }
2071
- var isPayloadTooLarge, init_run_refusal = __esm({ "src/output/run-refusal.ts"() {
2231
+ function idempotencyRefusal(e) {
2232
+ let refused2 = httpErrorJson(e), hint = refused2 ? IDEMPOTENCY_REFUSAL_HINTS[refused2.status] : void 0, error = refused2?.body.error;
2233
+ return !refused2 || hint === void 0 || typeof error != "string" || error === "" ? null : { error, hint };
2234
+ }
2235
+ var IDEMPOTENCY_REFUSAL_HINTS, isPayloadTooLarge, init_run_refusal = __esm({ "src/output/run-refusal.ts"() {
2072
2236
  "use strict";
2073
2237
  init_define_IGNITIFY_CLIENT_FILES();
2074
2238
  init_http_client();
2239
+ init_constants();
2075
2240
  init_tenant();
2241
+ IDEMPOTENCY_REFUSAL_HINTS = { 409: MESSAGES.IDEMPOTENCY_IN_PROGRESS_HINT, 422: MESSAGES.IDEMPOTENCY_KEY_REUSED_HINT };
2076
2242
  isPayloadTooLarge = (e) => e instanceof HttpError && e.status === 413;
2077
2243
  } });
2078
2244
  function renderUsageReport(st, r, opts = { lines: false }) {
@@ -2096,6 +2262,77 @@ var init_usage2 = __esm({ "src/output/usage.ts"() {
2096
2262
  init_constants2();
2097
2263
  init_table();
2098
2264
  } });
2265
+ function attrText(value) {
2266
+ return value === null || typeof value != "object" ? typeof value == "string" && !/^[\w.:/@+-]+$/.test(value) ? JSON.stringify(value) : String(value) : JSON.stringify(value.text);
2267
+ }
2268
+ function timelineLine(event) {
2269
+ let attrs = Object.entries(event.attrs).filter(([key, value]) => key !== CAUSE_ATTR && value !== null).map(([key, value]) => `${key}=${attrText(value)}`), line = [event.at, event.label, ...attrs.length > 0 ? [attrs.join(" ")] : []].join(" ");
2270
+ return terminalSafe(line, false).replace(/[\n\t]/g, " ");
2271
+ }
2272
+ var CAUSE_ATTR, init_timeline = __esm({ "src/output/timeline.ts"() {
2273
+ "use strict";
2274
+ init_define_IGNITIFY_CLIENT_FILES();
2275
+ init_style();
2276
+ CAUSE_ATTR = "cause";
2277
+ } });
2278
+ function renderUsageSeries(st, r) {
2279
+ let I2 = LAYOUT.INDENT, statusColumns = TERMINAL_RUN_STATUSES.map((s) => ({ header: s, keep: true })), table = renderTable(st, [{ header: "DAY", keep: true }, ...statusColumns, { header: "P50", keep: true }, { header: "P95", keep: true }, { header: "COMPUTE", keep: true }, { header: "EGRESS", keep: true }], r.series.map((d) => [d.day, ...TERMINAL_RUN_STATUSES.map((s) => runsIn(d, s)), durationText(st, d.duration_ms_p50), durationText(st, d.duration_ms_p95), formatUsageQuantity(d.compute_ms, AXIS_UNIT.compute), formatUsageQuantity(d.egress_bytes, AXIS_UNIT.network)]), { indent: I2 });
2280
+ return [heading(st, `usage per day, ${windowText(r.days)}`), "", table, "", st.dim(`${I2}note: runs count on the day they ended; P50 and P95 are the run durations of that day.`), st.dim(`${I2} \`compute\` = VM time, counted when the VM closes. The egress of a past day can still grow.`)].join(`
2281
+ `);
2282
+ }
2283
+ function renderFailures(st, r) {
2284
+ let I2 = LAYOUT.INDENT, out2 = [heading(st, `failed and cancelled runs, ${windowText(r.days)}`), ""];
2285
+ return out2.push(renderTable(st, [{ header: "CLASS", keep: true }, { header: "RUNS", keep: true }, { header: "MEANING" }], FAILURE_CLASSES.map((c) => [c, r.by_class[c], FAILURE_CLASS_TEXT[c].hint]), { indent: I2 }), ""), r.top_tasks.length === 0 ? (out2.push(st.dim(`${I2}No task failed in this window.`)), out2.join(`
2286
+ `)) : (out2.push(renderTable(st, [{ header: "TASK" }, { header: "FAILURES", keep: true }], r.top_tasks.map((t) => [t.task_name === null ? NO_TASK_NAME_TEXT : terminalSafe(t.task_name, false), t.failures]), { indent: I2 }), "", st.dim(`${I2}note: the task list leaves out cancelled runs.`)), out2.join(`
2287
+ `));
2288
+ }
2289
+ var windowText, runsIn, durationText, init_reports = __esm({ "src/output/reports.ts"() {
2290
+ "use strict";
2291
+ init_define_IGNITIFY_CLIENT_FILES();
2292
+ init_tenant();
2293
+ init_http_client();
2294
+ init_blocks();
2295
+ init_constants2();
2296
+ init_style();
2297
+ init_table();
2298
+ windowText = (days) => `last ${days} ${days === 1 ? "day" : "days"} (UTC)`, runsIn = (day, status) => day.runs[status] ?? 0, durationText = (st, ms) => ms === null ? st.glyphs.dash : formatUsageQuantity(ms, AXIS_UNIT.compute);
2299
+ } });
2300
+ function severityWord(st, s) {
2301
+ let word = s.toUpperCase();
2302
+ return s === "critical" ? st.red(word) : s === "warning" ? st.yellow(word) : st.dim(word);
2303
+ }
2304
+ function factsOf(n) {
2305
+ let facts = [];
2306
+ return n.read || facts.push("unread"), n.state === "open" && facts.push("open"), n.state === "resolved" && n.resolved_at !== null && facts.push(`resolved ${terminalSafe(n.resolved_at, false)}`), n.occurrences > 1 && facts.push(`${n.occurrences} times`), facts;
2307
+ }
2308
+ function commandOf(a) {
2309
+ return a.type === "open-run" ? `ignitify ${COMMANDS.RUN_GET} ${terminalSafe(a.run_id, false)}` : a.type === "usage" ? `ignitify ${COMMANDS.QUOTA}` : null;
2310
+ }
2311
+ function card(st, n) {
2312
+ let I2 = LAYOUT.INDENT, title = terminalSafe(n.title, false), facts = factsOf(n), head = `${I2}${severityWord(st, n.severity)} ${n.read ? title : st.bold(title)}${facts.length > 0 ? st.dim(` (${facts.join(", ")})`) : ""}`, text = wrapText(terminalSafe(n.text, false), st.width - 2 * I2.length).map((l) => `${I2}${I2}${l}`), ids = [n.id, n.last_at, ...n.subject === null ? [] : [`${n.subject.type} ${n.subject.id}`]].map((s) => terminalSafe(s, false)), next = (n.actions ?? []).flatMap((a) => {
2313
+ let cmd = commandOf(a);
2314
+ return cmd === null ? [] : [`${I2}${I2}next: ${cmd}`];
2315
+ });
2316
+ return [head, ...text, st.dim(`${I2}${I2}${ids.join(" ")}`), ...next].join(`
2317
+ `);
2318
+ }
2319
+ function renderNotifications(st, r) {
2320
+ let I2 = LAYOUT.INDENT;
2321
+ return r.items.length === 0 ? `${heading(st, "notifications")}
2322
+
2323
+ ${st.dim(`${I2}No notifications.`)}` : [heading(st, "notifications, newest first"), "", r.items.map((n) => card(st, n)).join(`
2324
+
2325
+ `)].join(`
2326
+ `);
2327
+ }
2328
+ var init_notifications = __esm({ "src/output/notifications.ts"() {
2329
+ "use strict";
2330
+ init_define_IGNITIFY_CLIENT_FILES();
2331
+ init_constants();
2332
+ init_blocks();
2333
+ init_constants2();
2334
+ init_style();
2335
+ } });
2099
2336
  var init_output = __esm({ "src/output/index.ts"() {
2100
2337
  "use strict";
2101
2338
  init_define_IGNITIFY_CLIENT_FILES();
@@ -2113,6 +2350,10 @@ var init_output = __esm({ "src/output/index.ts"() {
2113
2350
  init_deletion_plan();
2114
2351
  init_run_refusal();
2115
2352
  init_usage2();
2353
+ init_timeline();
2354
+ init_reports();
2355
+ init_notifications();
2356
+ init_access2();
2116
2357
  } });
2117
2358
  function planAdvice(config) {
2118
2359
  let declared = config.build?.system_packages ?? [];
@@ -2132,127 +2373,31 @@ var init_package_advice3 = __esm({ "src/core/package-advice.ts"() {
2132
2373
  init_constants();
2133
2374
  } });
2134
2375
  var version, init_package = __esm({ "package.json"() {
2135
- version = "1.2.2";
2136
- } });
2137
- var byCreatedAtDesc, init_sort = __esm({ "src/utils/sort.ts"() {
2138
- "use strict";
2139
- init_define_IGNITIFY_CLIENT_FILES();
2140
- byCreatedAtDesc = (rows) => [...rows].sort((a, b) => (b.created_at ?? "").localeCompare(a.created_at ?? ""));
2376
+ version = "2.0.0";
2141
2377
  } });
2142
- function apiUrlOf(args) {
2143
- return strOpt(args, "api") ?? DEFAULTS.API_URL;
2144
- }
2145
- function boolFlag(args, key) {
2146
- return args.options[key] === true;
2147
- }
2148
- function requiredArg(args) {
2149
- return args.positionals[0];
2150
- }
2151
- function requireArg(args, label) {
2152
- let id = requiredArg(args);
2153
- return id || (fail(MESSAGES.ARG_REQUIRED(label)), null);
2154
- }
2155
- var init_args2 = __esm({ "src/utils/args.ts"() {
2156
- "use strict";
2157
- init_define_IGNITIFY_CLIENT_FILES();
2158
- init_constants();
2159
- init_output();
2160
- init_http_client();
2161
- init_http_client();
2162
- } });
2163
- function traceLine(e) {
2164
- return e instanceof HttpError && e.traceId ? MESSAGES.SERVER_TRACE_ID(e.traceId) : null;
2165
- }
2166
- function keyExpiredHint(e) {
2167
- let refused2 = httpErrorJson(e);
2168
- return refused2?.status === 401 && refused2.body.error === KEY_EXPIRED_ERROR ? MESSAGES.KEY_EXPIRED_HINT : null;
2378
+ function apiUrlFor(env, release = RELEASE_BUILD) {
2379
+ return release ? DEFAULTS.API_URL : env[ENV.DEV_API_URL] || DEFAULTS.API_URL;
2169
2380
  }
2170
- function printError(e) {
2171
- fail(errText(e), { hint: keyExpiredHint(e), trace: traceLine(e) });
2172
- }
2173
- async function guard(body) {
2174
- return await guardValue(body) ?? 1;
2175
- }
2176
- async function guardValue(body) {
2177
- try {
2178
- return await body();
2179
- } catch (e) {
2180
- printError(e);
2181
- return;
2381
+ function apiUrlRefusal(url) {
2382
+ switch (keyUrlRefusal(url)) {
2383
+ case "not-url":
2384
+ return MESSAGES.API_URL_NOT_URL(url);
2385
+ case "insecure":
2386
+ return MESSAGES.API_URL_NOT_HTTPS(url);
2387
+ case null:
2388
+ return null;
2182
2389
  }
2183
2390
  }
2184
- var init_guard = __esm({ "src/utils/guard.ts"() {
2185
- "use strict";
2186
- init_define_IGNITIFY_CLIENT_FILES();
2187
- init_http_client();
2188
- init_tenant();
2189
- init_constants();
2190
- init_output();
2191
- } });
2192
- var import_node_readline, ttyPrompter, init_prompt = __esm({ "src/io/prompt.ts"() {
2193
- "use strict";
2194
- init_define_IGNITIFY_CLIENT_FILES();
2195
- import_node_readline = __toESM(require("node:readline"), 1);
2196
- init_output();
2197
- ttyPrompter = { async confirm(question) {
2198
- if (!process.stdin.isTTY) return fail("Refusing to proceed: not a TTY. Re-run with --yes to confirm non-interactively."), false;
2199
- let rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
2200
- try {
2201
- let answer = await new Promise((resolve) => rl.question(`${question} [y/N] `, resolve));
2202
- return /^y(es)?$/i.test(answer.trim());
2203
- } finally {
2204
- rl.close();
2205
- }
2206
- } };
2207
- } });
2208
- async function confirmOrAbort(args, prompter = ttyPrompter, question = MESSAGES.CONFIRM_PROCEED) {
2209
- return boolFlag(args, "yes") || await prompter.confirm(question) ? true : (say(MESSAGES.ABORTED), false);
2210
- }
2211
- var init_prompt_flow = __esm({ "src/utils/prompt-flow.ts"() {
2212
- "use strict";
2213
- init_define_IGNITIFY_CLIENT_FILES();
2214
- init_prompt();
2215
- init_constants();
2216
- init_args2();
2217
- init_manager();
2218
- } });
2219
- var REAL_CLOCK, init_clock = __esm({ "src/utils/clock.ts"() {
2220
- "use strict";
2221
- init_define_IGNITIFY_CLIENT_FILES();
2222
- REAL_CLOCK = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now: () => Date.now() };
2223
- } });
2224
- function snakeCaseKeys(value) {
2225
- return Array.isArray(value) ? value.map(snakeCaseKeys) : typeof value != "object" || value === null ? value : Object.fromEntries(Object.entries(value).map(([k, v]) => [k.replace(/([a-z0-9])([A-Z])/g, (_, a, b) => `${a}_${b.toLowerCase()}`), snakeCaseKeys(v)]));
2226
- }
2227
- var init_snake_case = __esm({ "src/utils/snake-case.ts"() {
2228
- "use strict";
2229
- init_define_IGNITIFY_CLIENT_FILES();
2230
- } });
2231
- function nextPageNoteFor2(args, list2, rows) {
2232
- return nextPageNoteFor(PROGRAM, args, { ...list2, flags: [...Object.keys(list2.flags), ...REPEATED_GLOBAL_FLAGS] }, rows, effectivePageLimit(strOpt(args, "limit")));
2233
- }
2234
- function limitRefusal(args) {
2235
- let raw = args.options.limit;
2236
- return typeof raw == "string" ? pageLimitRefusal(raw) : null;
2391
+ function assertApiUrl(url) {
2392
+ let refusal = apiUrlRefusal(url);
2393
+ if (refusal !== null) throw new Error(refusal);
2394
+ return url;
2237
2395
  }
2238
- var PROGRAM, REPEATED_GLOBAL_FLAGS, init_page2 = __esm({ "src/utils/page.ts"() {
2396
+ var init_api_url = __esm({ "src/core/api-url.ts"() {
2239
2397
  "use strict";
2240
2398
  init_define_IGNITIFY_CLIENT_FILES();
2241
- init_tenant();
2242
2399
  init_http_client();
2243
- init_args2();
2244
- PROGRAM = "ignitify", REPEATED_GLOBAL_FLAGS = ["api"];
2245
- } });
2246
- var init_utils = __esm({ "src/utils/index.ts"() {
2247
- "use strict";
2248
- init_define_IGNITIFY_CLIENT_FILES();
2249
- init_sort();
2250
- init_args2();
2251
- init_guard();
2252
- init_prompt_flow();
2253
- init_clock();
2254
- init_snake_case();
2255
- init_page2();
2400
+ init_constants();
2256
2401
  } });
2257
2402
  function floorWarning(client, floor) {
2258
2403
  if (!floor) return null;
@@ -2280,8 +2425,8 @@ async function describeServer(baseUrl, fetchImpl = fetch, check = noteServerVers
2280
2425
  let floor = res.headers.get(CLIENT_FLOOR_HEADER);
2281
2426
  return `API: ${api} at ${baseUrl}${floor ? ` (minimum client ${floor})` : ""}`;
2282
2427
  }
2283
- async function runVersion(args, fetchImpl = fetch) {
2284
- let baseUrl = apiUrlOf(args), st = outStyle();
2428
+ async function runVersion(fetchImpl = fetch, env = process.env) {
2429
+ let baseUrl = apiUrlFor(env), st = outStyle();
2285
2430
  return out(`${st.bold(`ignitify ${CLIENT_VERSION}`)} ${st.dim(`(built for API ${API_VERSION})`)}`), out(st.dim(await describeServer(baseUrl, fetchImpl))), 0;
2286
2431
  }
2287
2432
  var CLIENT_VERSION, PROBE_TIMEOUT_MS, noteServerVersion, init_version = __esm({ "src/io/version.ts"() {
@@ -2291,7 +2436,7 @@ var CLIENT_VERSION, PROBE_TIMEOUT_MS, noteServerVersion, init_version = __esm({
2291
2436
  init_tenant();
2292
2437
  init_constants();
2293
2438
  init_semver();
2294
- init_utils();
2439
+ init_api_url();
2295
2440
  init_output();
2296
2441
  CLIENT_VERSION = version, PROBE_TIMEOUT_MS = 3e3;
2297
2442
  noteServerVersion = createStalenessCheck(CLIENT_VERSION, (msg) => warn(msg));
@@ -2360,16 +2505,25 @@ function readCredentials(file = credentialsPath(), fsx = import_node_fs2.default
2360
2505
  fsx.closeSync(fd);
2361
2506
  }
2362
2507
  }
2363
- function writeCredentialsAtomic(creds, file = credentialsPath(), fsx = import_node_fs2.default) {
2364
- secureCredentialsDir(import_node_path.default.dirname(file), fsx);
2365
- let temp = `${file}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`, fd = fsx.openSync(temp, "wx", CREDENTIALS.FILE_MODE);
2508
+ function createPrivateFile(file, fsx = import_node_fs2.default) {
2509
+ return fsx.openSync(file, "wx", CREDENTIALS.FILE_MODE);
2510
+ }
2511
+ function discardPrivateFile(fd, file, fsx = import_node_fs2.default) {
2512
+ fsx.closeSync(fd), fsx.unlinkSync(file);
2513
+ }
2514
+ function fillPrivateFile(fd, file, text, fsx = import_node_fs2.default) {
2366
2515
  try {
2367
- fsx.writeSync(fd, `${JSON.stringify(creds, null, 2)}
2368
- `), fsx.fsyncSync(fd);
2516
+ fsx.writeSync(fd, text), fsx.fsyncSync(fd);
2369
2517
  } catch (e) {
2370
- throw fsx.closeSync(fd), fsx.unlinkSync(temp), e;
2518
+ throw discardPrivateFile(fd, file, fsx), e;
2371
2519
  }
2372
2520
  fsx.closeSync(fd);
2521
+ }
2522
+ function writeCredentialsAtomic(creds, file = credentialsPath(), fsx = import_node_fs2.default) {
2523
+ secureCredentialsDir(import_node_path.default.dirname(file), fsx);
2524
+ let temp = `${file}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
2525
+ fillPrivateFile(createPrivateFile(temp, fsx), temp, `${JSON.stringify(creds, null, 2)}
2526
+ `, fsx);
2373
2527
  try {
2374
2528
  fsx.renameSync(temp, file);
2375
2529
  } catch (e) {
@@ -2440,29 +2594,8 @@ var UnreachableError, init_unreachable = __esm({ "src/core/unreachable.ts"() {
2440
2594
  }
2441
2595
  };
2442
2596
  } });
2443
- function apiUrlRefusal(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;
2451
- }
2452
- }
2453
- function assertApiUrl(url) {
2454
- let refusal = apiUrlRefusal(url);
2455
- if (refusal !== null) throw new Error(refusal);
2456
- return url;
2457
- }
2458
- var init_api_url = __esm({ "src/core/api-url.ts"() {
2459
- "use strict";
2460
- init_define_IGNITIFY_CLIENT_FILES();
2461
- init_http_client();
2462
- init_constants();
2463
- } });
2464
- function resolveApiConfig(apiOverride, env = process.env, read = () => readCredentials(), note = noteEnvOverride) {
2465
- let baseUrl = assertApiUrl(apiOverride ?? DEFAULTS.API_URL), envKey = env[ENV.KEY];
2597
+ function resolveApiConfig(env = process.env, read = () => readCredentials(), note = noteEnvOverride) {
2598
+ let baseUrl = assertApiUrl(apiUrlFor(env)), envKey = env[ENV.KEY];
2466
2599
  if (envKey) return envOverridesLogin(baseUrl, readOrNone(read)) && note(MESSAGES.ENV_KEY_OVERRIDES_LOGIN), { baseUrl, token: envKey };
2467
2600
  let saved = read();
2468
2601
  if (saved.kind === "none") throw new Error(MESSAGES.NOT_SIGNED_IN);
@@ -2536,9 +2669,11 @@ async function postDeployment(cfg, taskId, manifest, tar, fetchImpl) {
2536
2669
  let form = new FormData();
2537
2670
  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 });
2538
2671
  }
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);
2672
+ async function postRun(cfg, run2, fetchImpl) {
2673
+ 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 }, replayed = false;
2674
+ return { answer: await apiPost(cfg, TENANT_PATHS.RUNS, body, fetchImpl, void 0, { headers: idempotencyHeaders(run2.idempotencyKey), onHeaders: (headers) => {
2675
+ replayed = isReplayedAnswer(headers);
2676
+ } }), replayed };
2542
2677
  }
2543
2678
  async function listSchedules(cfg, fetchImpl) {
2544
2679
  return apiGet(cfg, TENANT_PATHS.SCHEDULES, fetchImpl);
@@ -2549,8 +2684,10 @@ function deleteSchedule(cfg, scheduleId, fetchImpl) {
2549
2684
  function apiGet(cfg, path12, fetchImpl) {
2550
2685
  return request(`${cfg.baseUrl}${path12}`, { headers: authHeaders(cfg), fetchImpl: viaApi(fetchImpl), ...readHeaders });
2551
2686
  }
2552
- function apiPost(cfg, path12, body, fetchImpl, timeoutMs) {
2553
- return request(`${cfg.baseUrl}${path12}`, { method: "POST", headers: { ...authHeaders(cfg), "content-type": "application/json" }, body, fetchImpl: viaApi(fetchImpl), timeoutMs, ...readHeaders });
2687
+ function apiPost(cfg, path12, body, fetchImpl, timeoutMs, extras = {}) {
2688
+ return request(`${cfg.baseUrl}${path12}`, { method: "POST", headers: { ...authHeaders(cfg), "content-type": "application/json", ...extras.headers }, body, fetchImpl: viaApi(fetchImpl), timeoutMs, onHeaders: (headers) => {
2689
+ readHeaders.onHeaders(headers), extras.onHeaders?.(headers);
2690
+ } });
2554
2691
  }
2555
2692
  function apiDelete(cfg, path12, fetchImpl) {
2556
2693
  return request(`${cfg.baseUrl}${path12}`, { method: "DELETE", headers: authHeaders(cfg), fetchImpl: viaApi(fetchImpl), ...readHeaders });
@@ -2564,6 +2701,15 @@ function getWhoami(cfg, fetchImpl) {
2564
2701
  function getUsage(cfg, filters2 = {}, fetchImpl) {
2565
2702
  return apiGet(cfg, `${TENANT_PATHS.USAGE}${buildQuery(filters2)}`, fetchImpl);
2566
2703
  }
2704
+ function getUsageSeries(cfg, days, fetchImpl) {
2705
+ return apiGet(cfg, `${TENANT_PATHS.USAGE_SERIES}${daysQuery(days)}`, fetchImpl);
2706
+ }
2707
+ function getRunFailures(cfg, days, fetchImpl) {
2708
+ return apiGet(cfg, `${TENANT_PATHS.RUN_FAILURES}${daysQuery(days)}`, fetchImpl);
2709
+ }
2710
+ function listNotifications(cfg, params, fetchImpl) {
2711
+ return apiGet(cfg, `${TENANT_PATHS.NOTIFICATIONS}${buildQuery(params)}`, fetchImpl);
2712
+ }
2567
2713
  function setSecret(cfg, name, value, fetchImpl) {
2568
2714
  return apiPost(cfg, TENANT_PATHS.SECRETS, { name, value }, fetchImpl);
2569
2715
  }
@@ -2585,6 +2731,52 @@ function deleteHook(cfg, hookId, fetchImpl) {
2585
2731
  function setNotify(cfg, deploymentId, body, fetchImpl) {
2586
2732
  return apiPut(cfg, TENANT_PATHS.DEPLOYMENT_NOTIFY(deploymentId), body, fetchImpl);
2587
2733
  }
2734
+ function createUser(cfg, name, fetchImpl) {
2735
+ return apiPost(cfg, TENANT_PATHS.USERS, { name }, fetchImpl);
2736
+ }
2737
+ function listUsers(cfg, fetchImpl) {
2738
+ return apiGet(cfg, TENANT_PATHS.USERS, fetchImpl);
2739
+ }
2740
+ function getUser(cfg, name, fetchImpl) {
2741
+ return apiGet(cfg, TENANT_PATHS.USER_BY_ID(name), fetchImpl);
2742
+ }
2743
+ function deleteUser(cfg, name, fetchImpl) {
2744
+ return apiDelete(cfg, TENANT_PATHS.USER_BY_ID(name), fetchImpl);
2745
+ }
2746
+ function createGroup(cfg, name, permissions, fetchImpl) {
2747
+ return apiPost(cfg, TENANT_PATHS.GROUPS, { name, permissions }, fetchImpl);
2748
+ }
2749
+ function listGroups(cfg, fetchImpl) {
2750
+ return apiGet(cfg, TENANT_PATHS.GROUPS, fetchImpl);
2751
+ }
2752
+ function getGroup(cfg, name, fetchImpl) {
2753
+ return apiGet(cfg, TENANT_PATHS.GROUP_BY_ID(name), fetchImpl);
2754
+ }
2755
+ function deleteGroup(cfg, name, fetchImpl) {
2756
+ return apiDelete(cfg, TENANT_PATHS.GROUP_BY_ID(name), fetchImpl);
2757
+ }
2758
+ function grantGroupPermission(cfg, name, permission, fetchImpl) {
2759
+ return apiPut(cfg, TENANT_PATHS.GROUP_PERMISSION(name, permission), void 0, fetchImpl);
2760
+ }
2761
+ function revokeGroupPermission(cfg, name, permission, fetchImpl) {
2762
+ return apiDelete(cfg, TENANT_PATHS.GROUP_PERMISSION(name, permission), fetchImpl);
2763
+ }
2764
+ function addGroupUser(cfg, name, user, fetchImpl) {
2765
+ return apiPut(cfg, TENANT_PATHS.GROUP_USER(name, user), void 0, fetchImpl);
2766
+ }
2767
+ function removeGroupUser(cfg, name, user, fetchImpl) {
2768
+ return apiDelete(cfg, TENANT_PATHS.GROUP_USER(name, user), fetchImpl);
2769
+ }
2770
+ function createKey(cfg, body, fetchImpl) {
2771
+ return apiPost(cfg, TENANT_PATHS.KEYS, body, fetchImpl);
2772
+ }
2773
+ function listKeys(cfg, user, fetchImpl) {
2774
+ let q = buildQuery({ [KEY_LIST_QUERY.USER]: user });
2775
+ return apiGet(cfg, `${TENANT_PATHS.KEYS}${q}`, fetchImpl);
2776
+ }
2777
+ function deleteKey(cfg, keyId, fetchImpl) {
2778
+ return apiDelete(cfg, TENANT_PATHS.KEY_BY_ID(keyId), fetchImpl);
2779
+ }
2588
2780
  async function postPackageAdvice(cfg, body, fetchImpl) {
2589
2781
  try {
2590
2782
  return { kind: "ok", response: await apiPost(cfg, TENANT_PATHS.PACKAGE_ADVICE, body, fetchImpl, ADVICE.TIMEOUT_MS) };
@@ -2632,46 +2824,192 @@ function getApiRun(cfg, runId, fetchImpl) {
2632
2824
  function getApiRunLogs(cfg, runId, fetchImpl) {
2633
2825
  return apiGet(cfg, TENANT_PATHS.RUN_LOGS(runId), fetchImpl);
2634
2826
  }
2827
+ async function apiFollowGet(cfg, path12, fetchImpl) {
2828
+ let retryAfter = null;
2829
+ try {
2830
+ return { ok: true, data: await request(`${cfg.baseUrl}${path12}`, { headers: authHeaders(cfg), fetchImpl: viaApi(fetchImpl), onHeaders: (headers) => {
2831
+ readHeaders.onHeaders(headers), retryAfter = headers.get(RETRY_AFTER_HEADER);
2832
+ } }) };
2833
+ } catch (error) {
2834
+ return { ok: false, error, retryAfterMs: retryAfterMs(retryAfter, Date.now()) };
2835
+ }
2836
+ }
2837
+ function readRunTimeline(cfg, runId, query, fetchImpl) {
2838
+ let q = buildQuery({ [OBS_QUERY.CURSOR]: query.cursor ?? void 0, [OBS_QUERY.WAIT_SEC]: query.waitSec === void 0 ? void 0 : String(query.waitSec) });
2839
+ return apiFollowGet(cfg, `${TENANT_PATHS.RUN_TIMELINE(runId)}${q}`, fetchImpl);
2840
+ }
2841
+ function readRunOutput(cfg, runId, fromOffset, fetchImpl) {
2842
+ return apiFollowGet(cfg, `${TENANT_PATHS.RUN_LOGS(runId)}${buildQuery({ [OBS_QUERY.FROM_OFFSET]: String(fromOffset) })}`, fetchImpl);
2843
+ }
2844
+ function readRunList(cfg, params = {}, fetchImpl) {
2845
+ return apiFollowGet(cfg, `${TENANT_PATHS.RUNS}${buildQuery(params)}`, fetchImpl);
2846
+ }
2847
+ function readRunForFollow(cfg, runId, fetchImpl) {
2848
+ return apiFollowGet(cfg, TENANT_PATHS.RUN_BY_ID(runId), fetchImpl);
2849
+ }
2635
2850
  function deleteRun(cfg, runId, fetchImpl) {
2636
2851
  return apiDelete(cfg, TENANT_PATHS.RUN_BY_ID(runId), fetchImpl);
2637
2852
  }
2638
- function cancelApiRun(cfg, runId, force = false, graceMs, fetchImpl) {
2639
- let params = new URLSearchParams();
2640
- force && params.set("force", "true"), graceMs !== void 0 && params.set("grace_ms", String(graceMs));
2641
- let query = params.size > 0 ? `?${params}` : "";
2642
- return apiPost(cfg, `${TENANT_PATHS.RUN_CANCEL(runId)}${query}`, void 0, fetchImpl);
2853
+ function cancelApiRun(cfg, runId, force = false, graceMs, fetchImpl) {
2854
+ let params = new URLSearchParams();
2855
+ force && params.set("force", "true"), graceMs !== void 0 && params.set("grace_ms", String(graceMs));
2856
+ let query = params.size > 0 ? `?${params}` : "";
2857
+ return apiPost(cfg, `${TENANT_PATHS.RUN_CANCEL(runId)}${query}`, void 0, fetchImpl);
2858
+ }
2859
+ async function listApiVms(cfg, params = {}, fetchImpl) {
2860
+ return apiGet(cfg, `${TENANT_PATHS.VMS}${buildQuery(params)}`, fetchImpl);
2861
+ }
2862
+ async function getApiVm(cfg, vmId, fetchImpl) {
2863
+ return apiGet(cfg, TENANT_PATHS.VM_BY_ID(vmId), fetchImpl);
2864
+ }
2865
+ function deleteVm(cfg, vmId, force = false, fetchImpl) {
2866
+ return apiDelete(cfg, `${TENANT_PATHS.VM_BY_ID(vmId)}${forceQuery(force)}`, fetchImpl);
2867
+ }
2868
+ var readHeaders, idempotencyHeaders, isReplayedAnswer, noteEnvOverride, versionHeader, BadAnswerError, daysQuery, init_api = __esm({ "src/io/api.ts"() {
2869
+ "use strict";
2870
+ init_define_IGNITIFY_CLIENT_FILES();
2871
+ init_http_client();
2872
+ init_output();
2873
+ init_constants();
2874
+ init_package_advice3();
2875
+ init_version();
2876
+ init_credentials();
2877
+ init_unreachable();
2878
+ init_api_url();
2879
+ init_tenant();
2880
+ readHeaders = { onHeaders: noteServerVersion }, idempotencyHeaders = (key) => key === void 0 ? {} : { [IDEMPOTENCY_KEY_HEADER]: key }, isReplayedAnswer = (headers) => headers.get(IDEMPOTENT_REPLAYED_HEADER) === "true";
2881
+ noteEnvOverride = /* @__PURE__ */ (() => {
2882
+ let noted = false;
2883
+ return (msg) => {
2884
+ noted || (noted = true, warn(msg));
2885
+ };
2886
+ })();
2887
+ versionHeader = { [CLI_VERSION_HEADER]: CLIENT_VERSION };
2888
+ BadAnswerError = class extends Error {
2889
+ };
2890
+ daysQuery = (days) => buildQuery({ [OBS_QUERY.DAYS]: days === null ? void 0 : String(days) });
2891
+ } });
2892
+ var byCreatedAtDesc, init_sort = __esm({ "src/utils/sort.ts"() {
2893
+ "use strict";
2894
+ init_define_IGNITIFY_CLIENT_FILES();
2895
+ byCreatedAtDesc = (rows) => [...rows].sort((a, b) => (b.created_at ?? "").localeCompare(a.created_at ?? ""));
2896
+ } });
2897
+ function parseWholeCount(raw) {
2898
+ if (!/^[1-9][0-9]*$/.test(raw)) return null;
2899
+ let n = Number(raw);
2900
+ return Number.isSafeInteger(n) ? n : null;
2901
+ }
2902
+ function boolFlag(args, key) {
2903
+ return args.options[key] === true;
2904
+ }
2905
+ function requiredArg(args) {
2906
+ return args.positionals[0];
2907
+ }
2908
+ function requireArg(args, label) {
2909
+ let id = requiredArg(args);
2910
+ return id || (fail(MESSAGES.ARG_REQUIRED(label)), null);
2911
+ }
2912
+ var init_args2 = __esm({ "src/utils/args.ts"() {
2913
+ "use strict";
2914
+ init_define_IGNITIFY_CLIENT_FILES();
2915
+ init_constants();
2916
+ init_output();
2917
+ init_http_client();
2918
+ } });
2919
+ function traceLine(e) {
2920
+ return e instanceof HttpError && e.traceId ? MESSAGES.SERVER_TRACE_ID(e.traceId) : null;
2921
+ }
2922
+ function keyExpiredHint(e) {
2923
+ let refused2 = httpErrorJson(e);
2924
+ return refused2?.status === 401 && refused2.body.error === KEY_EXPIRED_ERROR ? MESSAGES.KEY_EXPIRED_HINT : null;
2643
2925
  }
2644
- async function listApiVms(cfg, params = {}, fetchImpl) {
2645
- return apiGet(cfg, `${TENANT_PATHS.VMS}${buildQuery(params)}`, fetchImpl);
2926
+ function printError(e) {
2927
+ fail(accessRefusalText(e) ?? errText(e), { hint: keyExpiredHint(e), trace: traceLine(e) });
2646
2928
  }
2647
- async function getApiVm(cfg, vmId, fetchImpl) {
2648
- return apiGet(cfg, TENANT_PATHS.VM_BY_ID(vmId), fetchImpl);
2929
+ async function guard(body) {
2930
+ return await guardValue(body) ?? 1;
2649
2931
  }
2650
- function deleteVm(cfg, vmId, force = false, fetchImpl) {
2651
- return apiDelete(cfg, `${TENANT_PATHS.VM_BY_ID(vmId)}${forceQuery(force)}`, fetchImpl);
2932
+ async function guardValue(body) {
2933
+ try {
2934
+ return await body();
2935
+ } catch (e) {
2936
+ printError(e);
2937
+ return;
2938
+ }
2652
2939
  }
2653
- var readHeaders, noteEnvOverride, versionHeader, BadAnswerError, init_api = __esm({ "src/io/api.ts"() {
2940
+ var init_guard = __esm({ "src/utils/guard.ts"() {
2654
2941
  "use strict";
2655
2942
  init_define_IGNITIFY_CLIENT_FILES();
2656
2943
  init_http_client();
2944
+ init_tenant();
2945
+ init_constants();
2946
+ init_output();
2947
+ init_access();
2948
+ } });
2949
+ var import_node_readline, ttyPrompter, init_prompt = __esm({ "src/io/prompt.ts"() {
2950
+ "use strict";
2951
+ init_define_IGNITIFY_CLIENT_FILES();
2952
+ import_node_readline = __toESM(require("node:readline"), 1);
2657
2953
  init_output();
2954
+ ttyPrompter = { async confirm(question) {
2955
+ if (!process.stdin.isTTY) return fail("Refusing to proceed: not a TTY. Re-run with --yes to confirm non-interactively."), false;
2956
+ let rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
2957
+ try {
2958
+ let answer = await new Promise((resolve) => rl.question(`${question} [y/N] `, resolve));
2959
+ return /^y(es)?$/i.test(answer.trim());
2960
+ } finally {
2961
+ rl.close();
2962
+ }
2963
+ } };
2964
+ } });
2965
+ async function confirmOrAbort(args, prompter = ttyPrompter, question = MESSAGES.CONFIRM_PROCEED) {
2966
+ return boolFlag(args, "yes") || await prompter.confirm(question) ? true : (say(MESSAGES.ABORTED), false);
2967
+ }
2968
+ var init_prompt_flow = __esm({ "src/utils/prompt-flow.ts"() {
2969
+ "use strict";
2970
+ init_define_IGNITIFY_CLIENT_FILES();
2971
+ init_prompt();
2658
2972
  init_constants();
2659
- init_package_advice3();
2660
- init_version();
2661
- init_credentials();
2662
- init_unreachable();
2663
- init_api_url();
2973
+ init_args2();
2974
+ init_manager();
2975
+ } });
2976
+ var REAL_CLOCK, init_clock = __esm({ "src/utils/clock.ts"() {
2977
+ "use strict";
2978
+ init_define_IGNITIFY_CLIENT_FILES();
2979
+ REAL_CLOCK = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now: () => Date.now() };
2980
+ } });
2981
+ function snakeCaseKeys(value) {
2982
+ return Array.isArray(value) ? value.map(snakeCaseKeys) : typeof value != "object" || value === null ? value : Object.fromEntries(Object.entries(value).map(([k, v]) => [k.replace(/([a-z0-9])([A-Z])/g, (_, a, b) => `${a}_${b.toLowerCase()}`), snakeCaseKeys(v)]));
2983
+ }
2984
+ var init_snake_case = __esm({ "src/utils/snake-case.ts"() {
2985
+ "use strict";
2986
+ init_define_IGNITIFY_CLIENT_FILES();
2987
+ } });
2988
+ function nextPageNoteFor2(args, list2, rows) {
2989
+ return nextPageNoteFor(PROGRAM, args, { ...list2, flags: Object.keys(list2.flags) }, rows, effectivePageLimit(strOpt(args, "limit")));
2990
+ }
2991
+ function limitRefusal(args) {
2992
+ let raw = args.options.limit;
2993
+ return typeof raw == "string" ? pageLimitRefusal(raw) : null;
2994
+ }
2995
+ var PROGRAM, init_page2 = __esm({ "src/utils/page.ts"() {
2996
+ "use strict";
2997
+ init_define_IGNITIFY_CLIENT_FILES();
2664
2998
  init_tenant();
2665
- readHeaders = { onHeaders: noteServerVersion };
2666
- noteEnvOverride = /* @__PURE__ */ (() => {
2667
- let noted = false;
2668
- return (msg) => {
2669
- noted || (noted = true, warn(msg));
2670
- };
2671
- })();
2672
- versionHeader = { [CLI_VERSION_HEADER]: CLIENT_VERSION };
2673
- BadAnswerError = class extends Error {
2674
- };
2999
+ init_http_client();
3000
+ init_args2();
3001
+ PROGRAM = "ignitify";
3002
+ } });
3003
+ var init_utils = __esm({ "src/utils/index.ts"() {
3004
+ "use strict";
3005
+ init_define_IGNITIFY_CLIENT_FILES();
3006
+ init_sort();
3007
+ init_args2();
3008
+ init_guard();
3009
+ init_prompt_flow();
3010
+ init_clock();
3011
+ init_snake_case();
3012
+ init_page2();
2675
3013
  } });
2676
3014
  function payloadRefusal(payload, limit) {
2677
3015
  let bytes = payloadBytes(payload);
@@ -2685,8 +3023,19 @@ var payloadBytes, limitOrFallback, formatLimit, limitsNote, init_payload_limit =
2685
3023
  payloadBytes = (payload) => Buffer.byteLength(JSON.stringify(payload) ?? "", "utf8"), limitOrFallback = (limit, fallback) => typeof limit == "number" && Number.isFinite(limit) && limit > 0 ? limit : fallback;
2686
3024
  formatLimit = (bytes) => formatBytes(bytes).replace(/\.0 /, " "), limitsNote = () => MESSAGES.TASK_LIMITS(formatLimit(PAYLOAD_LIMIT_FALLBACK_BYTES), formatLimit(RESULT_LIMIT_FALLBACK_BYTES));
2687
3025
  } });
2688
- function apiConfig(args) {
2689
- return resolveApiConfig(strOpt(args, "api"));
3026
+ function apiConfig() {
3027
+ return resolveApiConfig();
3028
+ }
3029
+ function accessNameArg(raw, kind, label) {
3030
+ if (raw === void 0 || raw === "") return fail(MESSAGES.ARG_REQUIRED(label)), null;
3031
+ let checked = checkAccessName(kind, raw);
3032
+ return checked.ok ? checked.value : (fail(checked.message), null);
3033
+ }
3034
+ function daysOption(args) {
3035
+ let raw = strOpt(args, "days");
3036
+ if (raw === void 0) return { ok: true, days: null };
3037
+ let days = parseWholeCount(raw);
3038
+ return days === null ? (fail(MESSAGES.DAYS_INVALID(raw)), { ok: false }) : { ok: true, days };
2690
3039
  }
2691
3040
  async function payloadLimit(cfg) {
2692
3041
  let whoami = await getWhoami(cfg).catch(() => null);
@@ -2696,6 +3045,10 @@ function consoleLogText(bytes, out2) {
2696
3045
  return out2.raw ? bytes : terminalSafe(bytes, out2.colour);
2697
3046
  }
2698
3047
  function printConsoleLog(data, raw, colour = outStyle().colour) {
3048
+ if (isJsonOutput()) {
3049
+ emit(data);
3050
+ return;
3051
+ }
2699
3052
  if (writeRaw(consoleLogText(typeof data?.bytes == "string" ? data.bytes : "", { raw, colour })), data?.truncated === true) {
2700
3053
  let lines = typeof data.lines == "number" ? data.lines : 0;
2701
3054
  tell(`
@@ -2709,7 +3062,7 @@ function noteNextPage(args, list2, rows) {
2709
3062
  async function runPagedList(args, verb) {
2710
3063
  let badLimit = limitRefusal(args);
2711
3064
  return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
2712
- let data = await verb.fetchPage(apiConfig(args), { ...flagParams(args, verb.list.flags), ...verb.extra });
3065
+ let data = await verb.fetchPage(apiConfig(), { ...flagParams(args, verb.list.flags), ...verb.extra });
2713
3066
  return verb.show(data), noteNextPage(args, verb.list, verb.rowsOf(data)), 0;
2714
3067
  });
2715
3068
  }
@@ -2718,19 +3071,20 @@ var init_shared = __esm({ "src/commands/shared.ts"() {
2718
3071
  init_define_IGNITIFY_CLIENT_FILES();
2719
3072
  init_api();
2720
3073
  init_payload_limit();
3074
+ init_access();
2721
3075
  init_constants();
2722
3076
  init_utils();
2723
3077
  init_output();
2724
3078
  } });
2725
3079
  async function runWhoami(args) {
2726
3080
  return guard(async () => {
2727
- let cfg = apiConfig(args);
3081
+ let cfg = apiConfig();
2728
3082
  return CliOutputManager.render("whoami", await getWhoami(cfg)), 0;
2729
3083
  });
2730
3084
  }
2731
3085
  async function runUsage(args) {
2732
3086
  return guard(async () => {
2733
- let cfg = apiConfig(args);
3087
+ let cfg = apiConfig();
2734
3088
  return CliOutputManager.render("quota", await getWhoami(cfg)), 0;
2735
3089
  });
2736
3090
  }
@@ -2752,14 +3106,16 @@ function decidePollError(e, failuresInARow, failingForMs) {
2752
3106
  return isTransientError(e) ? failuresInARow >= POLL.ERROR.MIN_TRIES && failingForMs >= POLL.ERROR.GRACE_MS ? "give-up" : "retry" : "give-up";
2753
3107
  }
2754
3108
  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;
3109
+ let sleep = loop.clock?.sleep ?? REAL_CLOCK.sleep, now = loop.clock?.now ?? REAL_CLOCK.now, startedAt = now(), decideError = loop.decideError === void 0 ? fixedRetry(loop.retryDelayMs) : loop.decideError, failures = 0, firstFailureAt = 0;
2756
3110
  for (let attempt = 0; ; ) {
2757
3111
  let view;
2758
3112
  try {
2759
3113
  view = await loop.fetch();
2760
3114
  } 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);
3115
+ failures === 0 && (firstFailureAt = now()), failures += 1;
3116
+ let next = decideError(error, failures, now() - firstFailureAt);
3117
+ if (next.kind === "give-up") return { kind: "lost", error };
3118
+ next.counted || (failures = 0), loop.onRetry?.(error, next.delayMs), await sleep(next.delayMs);
2763
3119
  continue;
2764
3120
  }
2765
3121
  failures = 0, loop.onView?.(view);
@@ -2768,7 +3124,7 @@ async function pollUntil(loop) {
2768
3124
  loop.onWait?.(view, attempt), await sleep(action.delayMs), attempt += 1;
2769
3125
  }
2770
3126
  }
2771
- var isWait, isLost, init_poll = __esm({ "src/core/poll.ts"() {
3127
+ var isWait, isLost, fixedRetry, init_poll = __esm({ "src/core/poll.ts"() {
2772
3128
  "use strict";
2773
3129
  init_define_IGNITIFY_CLIENT_FILES();
2774
3130
  init_http_client();
@@ -2776,6 +3132,7 @@ var isWait, isLost, init_poll = __esm({ "src/core/poll.ts"() {
2776
3132
  init_constants();
2777
3133
  init_utils();
2778
3134
  isWait = (action) => action.kind === "wait", isLost = (end) => "kind" in end && end.kind === "lost";
3135
+ fixedRetry = (delayMs) => (error, failuresInARow, failingForMs) => decidePollError(error, failuresInARow, failingForMs) === "give-up" ? { kind: "give-up" } : { kind: "retry", delayMs, counted: true };
2779
3136
  } });
2780
3137
  function shortenHome(file, home) {
2781
3138
  if (home === "" || !file.startsWith(home)) return file;
@@ -2867,7 +3224,7 @@ async function pollForKey(baseUrl, started, deadline, deps) {
2867
3224
  }
2868
3225
  async function runLogin(args, deps = {}) {
2869
3226
  return guard(async () => {
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) => {
3227
+ let clock = deps.clock ?? REAL_CLOCK, baseUrl = assertApiUrl(apiUrlFor(deps.env ?? process.env)), 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) => {
2871
3228
  throw e instanceof HttpError && e.status === 429 ? new Error(MESSAGES.LOGIN_TOO_MANY, { cause: e }) : e;
2872
3229
  }), env = deps.env ?? process.env, st = screenStyle(deps.screen, env), url = started.verification_uri_complete;
2873
3230
  if (say(renderLoginStart(st, { code: formatUserCode(started.user_code), url })), say(""), url !== null) {
@@ -2935,7 +3292,7 @@ function selectLines(lines, runs, vms) {
2935
3292
  }
2936
3293
  async function runUsageShow(args) {
2937
3294
  return guard(async () => {
2938
- let report = await getUsage(apiConfig(args), filters(args));
3295
+ let report = await getUsage(apiConfig(), filters(args));
2939
3296
  if (boolFlag(args, "json")) return emit(report), 0;
2940
3297
  let runs = boolFlag(args, "runs"), vms = boolFlag(args, "vms"), shown = { ...report, lines: selectLines(report.lines, runs, vms) };
2941
3298
  return out(renderUsageReport(outStyle(), shown, { lines: runs || vms })), 0;
@@ -2945,10 +3302,18 @@ async function runUsageExport(args) {
2945
3302
  return guard(async () => {
2946
3303
  let json2 = boolFlag(args, "json"), csv = boolFlag(args, "csv");
2947
3304
  if (json2 === csv) return tell(MESSAGES.USAGE_EXPORT_USAGE), 1;
2948
- let report = await getUsage(apiConfig(args), filters(args));
3305
+ let report = await getUsage(apiConfig(), filters(args));
2949
3306
  return out(json2 ? JSON.stringify(report.lines, null, 2) : toCsv(report)), 0;
2950
3307
  });
2951
3308
  }
3309
+ async function runUsageSeries(args) {
3310
+ return guard(async () => {
3311
+ let days = daysOption(args);
3312
+ if (!days.ok) return 1;
3313
+ let series = await getUsageSeries(apiConfig(), days.days);
3314
+ return emit(series, (st) => renderUsageSeries(st, series)), 0;
3315
+ });
3316
+ }
2952
3317
  var filters, init_usage_report = __esm({ "src/commands/usage-report.ts"() {
2953
3318
  "use strict";
2954
3319
  init_define_IGNITIFY_CLIENT_FILES();
@@ -3001,8 +3366,10 @@ function parseRunOptions(args, config) {
3001
3366
  if (onBusy !== void 0 && onBusy !== "queue" && onBusy !== "reject") return { ok: false, error: `--on-busy must be 'queue' or 'reject', got: ${onBusy}` };
3002
3367
  let at = strOpt(args, "at");
3003
3368
  if (at !== void 0 && Number.isNaN(new Date(at).getTime())) return { ok: false, error: `--at is not a valid ISO-8601 timestamp: ${at}` };
3369
+ let idempotencyKey = strOpt(args, "idempotency-key");
3370
+ if (idempotencyKey !== void 0 && !isValidIdempotencyKey(idempotencyKey)) return { ok: false, error: MESSAGES.IDEMPOTENCY_KEY_INVALID(idempotencyKey) };
3004
3371
  let wait = boolFlag(args, "wait");
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 } };
3372
+ 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, idempotencyKey } };
3006
3373
  }
3007
3374
  var VALUE_FLAGS, init_run_options = __esm({ "src/core/run-options.ts"() {
3008
3375
  "use strict";
@@ -9015,6 +9382,71 @@ var import_fs3, import_path3, init_scaffold = __esm({ "src/scaffold/index.ts"()
9015
9382
  init_templates();
9016
9383
  init_vendor();
9017
9384
  } });
9385
+ function replyOrThrow(reply) {
9386
+ if (!reply.ok) throw new FollowReadError(reply.error, reply.retryAfterMs);
9387
+ return reply.data;
9388
+ }
9389
+ function followStopOf(e) {
9390
+ let code = refusalCode(e);
9391
+ return code === OBS_REFUSAL.AGENT_OUTDATED_CURSOR.error ? "agent-outdated" : code === OBS_REFUSAL.NEVER_RAN.error ? "not-started" : "error";
9392
+ }
9393
+ function decidePacedRetry(e, retryAfterMs2, failuresInARow, failingForMs, fallbackMs) {
9394
+ return decidePollError(e, failuresInARow, failingForMs) === "give-up" ? { kind: "give-up" } : { kind: "retry", delayMs: isBusyAnswer(e) ? retryAfterMs2 ?? fallbackMs : fallbackMs, counted: true };
9395
+ }
9396
+ function decideBusyReadError(e, retryAfterMs2, failuresInARow, failingForMs) {
9397
+ return !isBusyAnswer(e) || failuresInARow > BUSY_READ.MAX_RETRIES ? { kind: "give-up" } : decidePacedRetry(e, retryAfterMs2, failuresInARow, failingForMs, FOLLOW.RETRY_FALLBACK_MS);
9398
+ }
9399
+ function decideFollowError(e, retryAfterMs2, failuresInARow, failingForMs) {
9400
+ return followStopOf(e) !== "error" ? { kind: "give-up" } : isWaitRefused(e) ? { kind: "retry", delayMs: retryAfterMs2 ?? FOLLOW.RETRY_FALLBACK_MS, counted: false } : decidePacedRetry(e, retryAfterMs2, failuresInARow, failingForMs, FOLLOW.RETRY_FALLBACK_MS);
9401
+ }
9402
+ function placeOutputPage(fromOffset, reply) {
9403
+ let next = reply.next_offset, bytes = typeof reply.bytes == "string" ? reply.bytes : "";
9404
+ return !isOffset(next) || next < fromOffset || bytes !== "" && next === fromOffset ? { kind: "no-cursor" } : { kind: "page", bytes, nextOffset: next, full: reply.truncated === true && next > fromOffset };
9405
+ }
9406
+ function decideOutputStep(page, endedAt, readAt) {
9407
+ return page.full ? "read-now" : page.bytes === "" && endedAt !== null && readAt - endedAt >= FOLLOW.IDLE_MS ? "stop" : "wait";
9408
+ }
9409
+ function timelineDone(reply, follow, held) {
9410
+ return reply.complete ? true : !follow && nextCursor(held, reply) === held;
9411
+ }
9412
+ function eventsToPrint(page, printedTail) {
9413
+ let fresh = (e) => !printedTail.has(e.id), tail = page.tail.filter(fresh), settledIds = new Set(page.events.map((e) => e.id));
9414
+ return { print: [...page.events.filter(fresh), ...tail], printedTail: new Set([...printedTail, ...tail.map((e) => e.id)].filter((id) => !settledIds.has(id))) };
9415
+ }
9416
+ function lastLines(text, n) {
9417
+ if (n <= 0) return "";
9418
+ let at = text.endsWith(`
9419
+ `) ? text.length - 1 : text.length;
9420
+ for (let seen = 0; seen < n; seen += 1) {
9421
+ let nl = at > 0 ? text.lastIndexOf(`
9422
+ `, at - 1) : -1;
9423
+ if (nl < 0) return text;
9424
+ at = nl;
9425
+ }
9426
+ return text.slice(at + 1);
9427
+ }
9428
+ var FollowReadError, isOffset, refusalCode, isWaitRefused, isBusyAnswer, nextCursor, idleRestMs, init_follow = __esm({ "src/core/follow.ts"() {
9429
+ "use strict";
9430
+ init_define_IGNITIFY_CLIENT_FILES();
9431
+ init_http_client();
9432
+ init_tenant();
9433
+ init_constants();
9434
+ init_poll();
9435
+ FollowReadError = class extends Error {
9436
+ error;
9437
+ retryAfterMs;
9438
+ constructor(error, retryAfterMs2) {
9439
+ super(error instanceof Error ? error.message : String(error)), this.name = "FollowReadError", this.error = error, this.retryAfterMs = retryAfterMs2;
9440
+ }
9441
+ };
9442
+ isOffset = (v) => typeof v == "number" && Number.isSafeInteger(v) && v >= 0, refusalCode = (e) => {
9443
+ let code = httpErrorJson(e)?.body.error;
9444
+ return typeof code == "string" ? code : null;
9445
+ }, isWaitRefused = (e) => refusalCode(e) === OBS_REFUSAL.WAIT_REFUSED.error;
9446
+ isBusyAnswer = (e) => e instanceof HttpError && (e.status === 429 || e.status === 503);
9447
+ nextCursor = (held, reply) => reply.cursor ?? held;
9448
+ idleRestMs = (startedAt, now) => Math.max(0, FOLLOW.IDLE_MS - (now - startedAt));
9449
+ } });
9018
9450
  function decidePoll(status, elapsedMs, attempt, failureReason) {
9019
9451
  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) };
9020
9452
  }
@@ -9041,14 +9473,19 @@ var BUILDING, READY, FAILED, init_deploy_poll = __esm({ "src/core/deploy-poll.ts
9041
9473
  function decideRunPoll(status, elapsedMs, attempt) {
9042
9474
  return isTerminalRunStatus(status) ? { kind: "settled", status } : elapsedMs >= POLL.RUN.TIMEOUT_MS ? { kind: "timeout", status } : { kind: "wait", delayMs: backoffDelayMs(attempt, POLL.RUN) };
9043
9475
  }
9476
+ function decideRunPollError(e, failuresInARow, failingForMs) {
9477
+ let [error, retryAfterMs2] = e instanceof FollowReadError ? [e.error, e.retryAfterMs] : [e, null];
9478
+ return decidePacedRetry(error, retryAfterMs2, failuresInARow, failingForMs, POLL.RUN.MAX_MS);
9479
+ }
9044
9480
  async function pollUntilSettled(deps) {
9045
- let statusOf = (run2) => run2.status ?? "UNKNOWN", end = await pollUntil({ fetch: deps.fetchRun, retryDelayMs: POLL.RUN.MAX_MS, onRetry: deps.onRetry, decide: (run2, elapsedMs, attempt) => decideRunPoll(statusOf(run2), elapsedMs, attempt), onView: (run2) => deps.onStatus?.(statusOf(run2)), clock: deps });
9046
- return isLost(end) ? end : { action: end.action, run: end.view };
9481
+ let statusOf = (run2) => run2.status ?? "UNKNOWN", end = await pollUntil({ fetch: deps.fetchRun, decideError: decideRunPollError, onRetry: deps.onRetry, decide: (run2, elapsedMs, attempt) => decideRunPoll(statusOf(run2), elapsedMs, attempt), onView: (run2) => deps.onStatus?.(statusOf(run2)), clock: deps });
9482
+ return isLost(end) ? end.error instanceof FollowReadError ? { kind: "lost", error: end.error.error } : end : { action: end.action, run: end.view };
9047
9483
  }
9048
9484
  var init_run_poll = __esm({ "src/core/run-poll.ts"() {
9049
9485
  "use strict";
9050
9486
  init_define_IGNITIFY_CLIENT_FILES();
9051
9487
  init_constants();
9488
+ init_follow();
9052
9489
  init_poll();
9053
9490
  } });
9054
9491
  async function waitForBuild(cfg, deploymentId, progress, reused = false) {
@@ -9076,7 +9513,7 @@ ${st.bold(LABELS.BUILD_LOG)}
9076
9513
  return built.kind === "timeout" ? (progress.fail(WAIT.BUILD_TIMEOUT), fail(WAIT.BUILD_TIMEOUT_DETAIL(deploymentId)), 1) : (progress.succeed(reused && !building ? WAIT.REUSED : WAIT.BUILT), 0);
9077
9514
  }
9078
9515
  async function waitForRun(cfg, runId) {
9079
- let progress = startProgress(WAIT.RUN_SUBMITTED(runId)), lastStatus, settled = await withInterruptNotice(progress, () => WAIT.INTERRUPTED_RUN(runId), () => pollUntilSettled({ fetchRun: () => getApiRun(cfg, runId), onStatus: (status) => {
9516
+ let progress = startProgress(WAIT.RUN_SUBMITTED(runId)), lastStatus, settled = await withInterruptNotice(progress, () => WAIT.INTERRUPTED_RUN(runId), () => pollUntilSettled({ fetchRun: async () => replyOrThrow(await readRunForFollow(cfg, runId)), onStatus: (status) => {
9080
9517
  lastStatus = status, progress.update(WAIT.RUN(runId, status), WAIT.HINT_RUN);
9081
9518
  }, onRetry: () => progress.update(lastStatus === void 0 ? WAIT.RUN_SUBMITTED(runId) : WAIT.RUN(runId, lastStatus), WAIT.HINT_RETRYING) }));
9082
9519
  if (isLost(settled)) return progress.fail(WAIT.LOST), fail(WAIT.LOST_RUN_DETAIL(runId, errText(settled.error))), 1;
@@ -9089,6 +9526,7 @@ var init_wait = __esm({ "src/commands/wait.ts"() {
9089
9526
  "use strict";
9090
9527
  init_define_IGNITIFY_CLIENT_FILES();
9091
9528
  init_api();
9529
+ init_follow();
9092
9530
  init_deploy_poll();
9093
9531
  init_run_poll();
9094
9532
  init_poll();
@@ -13318,7 +13756,7 @@ async function runIntegrationAdd(args, cwd = process.cwd(), deps = REAL_INTEGRAT
13318
13756
  if (!task) return 1;
13319
13757
  if (printWarnings([...task.warnings]), task.config.runtime !== INTEGRATION.RUNTIME) return fail(INTEGRATION.NOT_NODE(task.config.runtime)), 1;
13320
13758
  let taskDir = import_node_fs9.default.realpathSync(task.taskDir);
13321
- if (readTaskPackageJson(taskDir) === null) return fail(INTEGRATION.NO_PACKAGE_JSON(taskDir)), 1;
13759
+ if (readTaskPackageJson(taskDir) === null) return fail(INTEGRATION.NO_PACKAGE_JSON(task.taskDir)), 1;
13322
13760
  let pkg = packageName(name);
13323
13761
  return guard(() => withPackDir((packDir) => addIntegration(args, taskDir, task.config, pkg, packDir, dryRun, deps)));
13324
13762
  }
@@ -13400,7 +13838,7 @@ async function printPackageAdvice(args, config, fetchImpl) {
13400
13838
  let plan = planAdvice(config);
13401
13839
  if (plan.kind === "skip") return say(ADVICE.SKIPPED(plan.reason));
13402
13840
  try {
13403
- let outcome = await postPackageAdvice(apiConfig(args), plan.request, fetchImpl), { lines, fault } = renderAdvice(outcome, screenStyle());
13841
+ let outcome = await postPackageAdvice(apiConfig(), plan.request, fetchImpl), { lines, fault } = renderAdvice(outcome, screenStyle());
13404
13842
  for (let line of lines) (fault ? tell : say)(line);
13405
13843
  } catch (e) {
13406
13844
  fail(errText(e));
@@ -13413,12 +13851,12 @@ async function runTaskValidate(args, cwd = process.cwd(), fetchImpl) {
13413
13851
  async function runTaskGet(args, cwd = process.cwd()) {
13414
13852
  let task = await prepareTask(args, cwd);
13415
13853
  if (!task) return 1;
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 });
13854
+ let { slug: slug2 } = task, state = readState(task.taskDir), check = checkSourceUpload(task.taskDir), localContentHash = check.ok ? check.source.contentHash : null, cfg = offlineTolerantConfig(), 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 });
13417
13855
  return emit(snakeCaseKeys(report), () => formatTaskReport(report)), 0;
13418
13856
  }
13419
- function offlineTolerantConfig(args) {
13857
+ function offlineTolerantConfig() {
13420
13858
  try {
13421
- return apiConfig(args);
13859
+ return apiConfig();
13422
13860
  } catch (e) {
13423
13861
  return warn(RESULT_MESSAGES.TASK_GET_OFFLINE(errText(e))), null;
13424
13862
  }
@@ -13436,8 +13874,8 @@ async function runTaskPack(args, cwd = process.cwd()) {
13436
13874
  return CliOutputManager.render("task:pack", { task_id: config.slug, files: entries.map((e) => ({ path: e.path, size: e.size })), total_bytes: totalBytes, ignored, content_hash: contentHash }), 0;
13437
13875
  }
13438
13876
  async function runTaskDeploy(args, cwd = process.cwd()) {
13439
- let taskDir = getTaskDir(args, cwd);
13440
- if (!taskDir) return 1;
13877
+ let taskDir = getTaskDir(args, cwd), { taskId, dirFlag } = getTaskPath(args, cwd);
13878
+ if (!taskDir || !taskId) return 1;
13441
13879
  let prep = args.options.check ? checkSourceUpload(taskDir) : prepareSourceUpload(taskDir);
13442
13880
  if (printWarnings([...prep.warnings]), !prep.ok) return printErrors([...prep.errors]), 1;
13443
13881
  let left = "upload" in prep ? prep.upload.ignored : prep.source.ignored;
@@ -13446,9 +13884,9 @@ async function runTaskDeploy(args, cwd = process.cwd()) {
13446
13884
  return emit({ task_id: config.slug, valid: true, total_bytes: totalBytes, content_hash: contentHash }, (st) => successLine(st, VALIDATE.VALID)), 0;
13447
13885
  }
13448
13886
  let state = readState(taskDir);
13449
- if (!state.task_id) return fail("task_id not found in .ignitify/state.json \u2014 run `ignitify task:create <task-id>` first"), 1;
13887
+ if (!state.task_id) return fail(MESSAGES.TASK_NOT_REGISTERED(taskId, dirFlag)), 1;
13450
13888
  let { upload } = prep;
13451
- return uploadAndWait(apiConfig(args), taskDir, state.task_id, upload.manifest, upload.tar, upload.slug);
13889
+ return uploadAndWait(apiConfig(), taskDir, state.task_id, upload.manifest, upload.tar, upload.slug);
13452
13890
  }
13453
13891
  async function uploadAndWait(cfg, taskDir, taskId, manifest, tar, slug2) {
13454
13892
  let progress = startProgress(WAIT.UPLOADING(slug2, formatBytes(tar.length))), res, whoami;
@@ -13467,7 +13905,7 @@ async function runTaskRun(args, cwd = process.cwd()) {
13467
13905
  printWarnings([...task.warnings]);
13468
13906
  let opts = parseRunOptions(args, task.config);
13469
13907
  if (!opts.ok) return fail(opts.error), 1;
13470
- let { run: run2 } = opts, taskName = task.slug, cfg = apiConfig(args);
13908
+ let { run: run2 } = opts, taskName = task.slug, cfg = apiConfig();
13471
13909
  if (payloadBytes(run2.payload) > PAYLOAD_LIMIT_FALLBACK_BYTES) {
13472
13910
  let refusal = payloadRefusal(run2.payload, await payloadLimit(cfg));
13473
13911
  if (refusal !== null) return fail(refusal), 1;
@@ -13480,17 +13918,21 @@ async function runTaskRun(args, cwd = process.cwd()) {
13480
13918
  if (refusal !== null) return fail(refusal), 1;
13481
13919
  }
13482
13920
  return resolved.source === "server" && say(`Using deployment ${deploymentId} (resolved from the server \u2014 no local state).`), await guard(async () => {
13483
- let res;
13921
+ let posted;
13484
13922
  try {
13485
- res = await postRun(cfg, { deploymentId, taskName, ...run2 });
13923
+ posted = await postRun(cfg, { deploymentId, taskName, ...run2 });
13486
13924
  } catch (e) {
13487
13925
  if (isJsonOutput()) throw e;
13488
13926
  let trace = traceLine(e);
13489
13927
  if (isPayloadTooLarge(e)) return fail(MESSAGES.PAYLOAD_REFUSED_BY_SERVER(await payloadLimit(cfg)), { trace }), 1;
13928
+ let keyed = run2.idempotencyKey === void 0 ? null : idempotencyRefusal(e);
13929
+ if (keyed) return fail(keyed.error, { hint: keyed.hint, trace }), 1;
13490
13930
  let explained = explainRunRefusal(e) ?? explainLimitRefusal(e);
13491
13931
  if (!explained) throw e;
13492
13932
  return tell(explained), trace !== null && tell(errStyle().dim(trace)), 1;
13493
13933
  }
13934
+ let res = posted.answer;
13935
+ posted.replayed && warn(MESSAGES.RUN_REPLAYED);
13494
13936
  let answer = classifyRunAnswer(res, run2.at, opts.wait);
13495
13937
  switch (answer.kind) {
13496
13938
  case "skew-ran-now":
@@ -13532,7 +13974,7 @@ async function runTaskCreate(args, cwd = process.cwd()) {
13532
13974
  return err2 ? (fail(err2), 1) : (emit({ slug: taskId, runtime: lang, local_dir: taskDir, registered: false }, (st) => successLine(st, RESULT_MESSAGES.TASK_CREATED_LOCAL(lang, taskId, taskDir))), say(screenStyle().dim(limitsNote())), 0);
13533
13975
  }
13534
13976
  return guard(async () => {
13535
- let cfg = apiConfig(args), res = await createTask(cfg, taskId);
13977
+ let cfg = apiConfig(), res = await createTask(cfg, taskId);
13536
13978
  writeState(taskDir, { task_id: res.task_id, task_resource_id: res.resource_id });
13537
13979
  let local_dir;
13538
13980
  if (!args.options["no-scaffold"]) {
@@ -13549,11 +13991,11 @@ async function runTaskDelete(args, cwd = process.cwd(), prompter = ttyPrompter)
13549
13991
  let id = requiredArg(args);
13550
13992
  if (!id) return fail(MESSAGES.TASK_ID_REQUIRED), 1;
13551
13993
  let plan = await guardValue(async () => {
13552
- let baseDir = tasksBaseDir(args, cwd), cfg = apiConfig(args), { taskId, runs, deploymentCount, runCount } = await surveyTaskContents(cfg, id), folders = readTaskFolders(baseDir);
13994
+ let baseDir = tasksBaseDir(args, cwd), cfg = apiConfig(), { taskId, runs, deploymentCount, runCount } = await surveyTaskContents(cfg, id), folders = readTaskFolders(baseDir);
13553
13995
  return planTaskDeletion({ taskSlug: id, taskId, runs, deploymentCount, runCount, folders });
13554
13996
  });
13555
13997
  return plan === void 0 || (printDeletionPlan(plan), !await confirmOrAbort(args, prompter)) ? 1 : guard(async () => {
13556
- let cfg = apiConfig(args), d = (await deleteTask(cfg, id)).deleted;
13998
+ let cfg = apiConfig(), d = (await deleteTask(cfg, id)).deleted;
13557
13999
  d ? sayOk(`${MESSAGES.TASK_DELETED}: ${d.task_id} (${deletedCounts([["deployments", d.deployments], ["runs", d.runs], ["vms", d.vms], ["forfeited pins", d.forfeited_pins]])})`) : sayOk(MESSAGES.TASK_DELETED);
13558
14000
  for (let dir of plan.unlinkDirs) {
13559
14001
  let link = taskLinkPath(dir);
@@ -13599,18 +14041,18 @@ function runDeploymentList(args) {
13599
14041
  async function runDeploymentGet(args) {
13600
14042
  let id = requireArg(args, "deployment-id");
13601
14043
  return id ? guard(async () => {
13602
- let cfg = apiConfig(args);
14044
+ let cfg = apiConfig();
13603
14045
  return CliOutputManager.render("deployment:get", await getApiDeployment(cfg, id)), 0;
13604
14046
  }) : 1;
13605
14047
  }
13606
14048
  async function runDeploymentLogs(args) {
13607
14049
  let id = requireArg(args, "deployment-id");
13608
- return id ? guard(async () => (printConsoleLog(await getApiDeploymentLogs(apiConfig(args), id), boolFlag(args, "raw")), 0)) : 1;
14050
+ return id ? guard(async () => (printConsoleLog(await getApiDeploymentLogs(apiConfig(), id), boolFlag(args, "raw")), 0)) : 1;
13609
14051
  }
13610
14052
  async function runDeploymentDelete(args, prompter = ttyPrompter) {
13611
14053
  let id = requireArg(args, "deployment-id");
13612
14054
  return id ? guard(async () => {
13613
- let cfg = apiConfig(args);
14055
+ let cfg = apiConfig();
13614
14056
  if (say(RESULT_MESSAGES.DEPLOYMENT_DELETE_PREVIEW(id)), !await confirmOrAbort(args, prompter)) return 1;
13615
14057
  let res = await deleteDeployment(cfg, id);
13616
14058
  emit(res);
@@ -13639,27 +14081,122 @@ var WHOLE_MS, init_grace = __esm({ "src/core/grace.ts"() {
13639
14081
  init_constants();
13640
14082
  WHOLE_MS = /^\d+$/;
13641
14083
  } });
14084
+ async function readWithRetry(read, clock) {
14085
+ let wait = true, end = await pollUntil({ fetch: async () => replyOrThrow(await read(wait)), decideError: (e, failures, failingForMs) => e instanceof FollowReadError ? decideFollowError(e.error, e.retryAfterMs, failures, failingForMs) : { kind: "give-up" }, onRetry: (e) => {
14086
+ e instanceof FollowReadError && isWaitRefused(e.error) && (wait = false);
14087
+ }, decide: () => ({ kind: "read" }), clock });
14088
+ if (!isLost(end)) return { ok: true, data: end.view };
14089
+ let error = end.error instanceof FollowReadError ? end.error.error : end.error, stop = followStopOf(error);
14090
+ return { ok: false, failure: stop === "agent-outdated" ? { kind: stop } : { kind: stop, error } };
14091
+ }
14092
+ async function readBusyRetried(read, clock, onRetry = () => {
14093
+ }) {
14094
+ let end = await pollUntil({ fetch: async () => replyOrThrow(await read()), decideError: (e, failures, failingForMs) => e instanceof FollowReadError ? decideBusyReadError(e.error, e.retryAfterMs, failures, failingForMs) : { kind: "give-up" }, onRetry: (_e, delayMs) => onRetry(delayMs), decide: () => ({ kind: "read" }), clock });
14095
+ if (!isLost(end)) return end.view;
14096
+ throw end.error instanceof FollowReadError ? end.error.error : end.error;
14097
+ }
14098
+ async function readTimeline(loop) {
14099
+ let cursor = null;
14100
+ for (; ; ) {
14101
+ let held = cursor, startedAt = loop.clock.now(), read = await readWithRetry((wait) => loop.read(held, loop.follow && wait), loop.clock);
14102
+ if (!read.ok) return { ok: false, failure: read.failure };
14103
+ if (loop.onPage(read.data), cursor = nextCursor(held, read.data), timelineDone(read.data, loop.follow, held)) return { ok: true, last: read.data };
14104
+ if (loop.follow && read.data.events.length === 0) {
14105
+ let rest = idleRestMs(startedAt, loop.clock.now());
14106
+ rest > 0 && await loop.clock.sleep(rest);
14107
+ }
14108
+ }
14109
+ }
14110
+ async function readOutput(loop) {
14111
+ let offset = 0, held = loop.tail === null ? null : "", runId = "", checkRun = loop.follow, endedAt = null;
14112
+ for (; ; ) {
14113
+ if (checkRun && endedAt === null) {
14114
+ let run2 = await readWithRetry(loop.readRun, loop.clock);
14115
+ if (!run2.ok) return { ok: false, failure: run2.failure };
14116
+ isTerminalRunStatus(run2.data.status) && (endedAt = loop.clock.now());
14117
+ }
14118
+ let from = offset, readAt = loop.clock.now(), read = await readWithRetry(() => loop.readPage(from), loop.clock);
14119
+ if (!read.ok && !(loop.follow && read.failure.kind === "not-started")) return { ok: false, failure: read.failure };
14120
+ let placed = read.ok ? placeOutputPage(from, read.data) : { kind: "page", bytes: "", nextOffset: from, full: false };
14121
+ if (placed.kind === "no-cursor") return { ok: false, failure: { kind: "no-cursor" } };
14122
+ if (read.ok && (runId = read.data.run_id), offset = placed.nextOffset, held !== null && loop.tail !== null ? (held = lastLines(held + placed.bytes, loop.tail), placed.full || (held !== "" && loop.onChunk({ runId, bytes: held, nextOffset: offset }), held = null)) : placed.bytes !== "" && loop.onChunk({ runId, bytes: placed.bytes, nextOffset: offset }), !loop.follow) {
14123
+ if (placed.full) continue;
14124
+ return { ok: true };
14125
+ }
14126
+ let step = decideOutputStep(placed, endedAt, readAt);
14127
+ if (step === "stop") return { ok: true };
14128
+ checkRun = step === "wait", step === "wait" && await loop.clock.sleep(FOLLOW.IDLE_MS);
14129
+ }
14130
+ }
14131
+ var init_run_follow = __esm({ "src/commands/run-follow.ts"() {
14132
+ "use strict";
14133
+ init_define_IGNITIFY_CLIENT_FILES();
14134
+ init_constants();
14135
+ init_follow();
14136
+ init_poll();
14137
+ } });
13642
14138
  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) });
14139
+ return runPagedList(args, { list: { command: COMMANDS.RUN_LIST, flags: RUN_LIST_FLAG_PARAMS }, fetchPage: (cfg, params) => readBusyRetried(() => readRunList(cfg, params), REAL_CLOCK, noteBusyRetry), rowsOf: (data) => data.runs, show: (data) => CliOutputManager.render("run:list", data.runs) });
13644
14140
  }
13645
14141
  async function runRunGet(args) {
13646
14142
  let id = requireArg(args, "run-id");
13647
14143
  return id ? guard(async () => {
13648
- let cfg = apiConfig(args), data = await getApiRun(cfg, id);
14144
+ let cfg = apiConfig(), data = await readBusyRetried(() => readRunForFollow(cfg, id), REAL_CLOCK, noteBusyRetry);
13649
14145
  return boolFlag(args, "full") ? out(JSON.stringify(data, null, 2)) : CliOutputManager.render("run:get", data), 0;
13650
14146
  }) : 1;
13651
14147
  }
13652
14148
  async function runRunLogs(args) {
13653
14149
  let id = requireArg(args, "run-id");
13654
- return id ? guard(async () => {
13655
- let cfg = apiConfig(args);
13656
- return printConsoleLog(await getApiRunLogs(cfg, id), boolFlag(args, "raw")), 0;
13657
- }) : 1;
14150
+ if (!id) return 1;
14151
+ let follow = boolFlag(args, "follow"), raw = boolFlag(args, "raw");
14152
+ return guard(async () => {
14153
+ let tailRaw = strOpt(args, "tail"), tail = tailRaw === void 0 ? null : parseWholeCount(tailRaw);
14154
+ if (tailRaw !== void 0 && tail === null) return fail(FOLLOW_MESSAGES.TAIL_INVALID(tailRaw)), 1;
14155
+ let cfg = apiConfig(), json2 = isJsonOutput();
14156
+ if (!follow && tail === null) return printConsoleLog(await getApiRunLogs(cfg, id), raw), 0;
14157
+ let colour = outStyle().colour, read = () => readOutput({ readPage: (fromOffset) => readRunOutput(cfg, id, fromOffset), readRun: () => readRunForFollow(cfg, id), onChunk: (chunk) => json2 ? out(JSON.stringify(outputDocument(chunk))) : writeRaw(consoleLogText(chunk.bytes, { raw, colour })), follow, tail, clock: REAL_CLOCK }), end = follow ? await withInterruptNotice(NO_PROGRESS, () => FOLLOW_MESSAGES.INTERRUPTED(id), read) : await read();
14158
+ return end.ok ? 0 : failFollow(end.failure);
14159
+ });
14160
+ }
14161
+ function failFollow(failure) {
14162
+ if (failure.kind === "agent-outdated") fail(FOLLOW_MESSAGES.AGENT_OUTDATED);
14163
+ else if (failure.kind === "no-cursor") fail(FOLLOW_MESSAGES.API_NO_CURSOR);
14164
+ else throw failure.error;
14165
+ return 1;
14166
+ }
14167
+ async function runRunTimeline(args) {
14168
+ let id = requireArg(args, "run-id");
14169
+ if (!id) return 1;
14170
+ let follow = boolFlag(args, "follow");
14171
+ return guard(async () => {
14172
+ let cfg = apiConfig(), json2 = isJsonOutput(), events = [], printedTail = /* @__PURE__ */ new Set(), onPage = (page) => {
14173
+ if (json2 && follow) (page.events.length > 0 || page.complete) && out(JSON.stringify(page));
14174
+ else if (json2) events.push(...page.events);
14175
+ else {
14176
+ let next = eventsToPrint(page, printedTail);
14177
+ printedTail = next.printedTail;
14178
+ for (let event of next.print) out(timelineLine(event));
14179
+ }
14180
+ }, read = () => readTimeline({ read: (cursor, wait) => readRunTimeline(cfg, id, { cursor, waitSec: wait ? FOLLOW.WAIT_SEC : void 0 }), onPage, follow, clock: REAL_CLOCK }), end = follow ? await withInterruptNotice(NO_PROGRESS, () => FOLLOW_MESSAGES.INTERRUPTED(id), read) : await read();
14181
+ if (!end.ok) return failFollow(end.failure);
14182
+ let { run: run2 } = end.last;
14183
+ json2 && !follow && emit({ ...end.last, events });
14184
+ let ended = isTerminalRunStatus(run2.status), status = terminalSafe(run2.status, false);
14185
+ return json2 || out(ended ? WAIT.RUN_ENDED(status) : FOLLOW_MESSAGES.STILL_LIVE(status)), !ended || run2.status === RUN_STATUS.COMPLETED ? 0 : 1;
14186
+ });
14187
+ }
14188
+ async function runRunFailures(args) {
14189
+ return guard(async () => {
14190
+ let days = daysOption(args);
14191
+ if (!days.ok) return 1;
14192
+ let report = await getRunFailures(apiConfig(), days.days);
14193
+ return emit(report, (st) => renderFailures(st, report)), 0;
14194
+ });
13658
14195
  }
13659
14196
  async function runRunDelete(args, prompter = ttyPrompter) {
13660
14197
  let id = requireArg(args, "run-id");
13661
14198
  return id ? guard(async () => {
13662
- let cfg = apiConfig(args);
14199
+ let cfg = apiConfig();
13663
14200
  return say(RESULT_MESSAGES.RUN_DELETE_PREVIEW(id)), await confirmOrAbort(args, prompter) ? (emit(await deleteRun(cfg, id)), sayOk(`${MESSAGES.RUN_DELETED}: ${id}`), 0) : 1;
13664
14201
  }) : 1;
13665
14202
  }
@@ -13670,7 +14207,10 @@ async function runRunCancel(args, prompter = ttyPrompter) {
13670
14207
  return guard(async () => {
13671
14208
  let grace = parseGraceMs(strOpt(args, "grace"), force);
13672
14209
  if (!grace.ok) return fail(grace.error), 1;
13673
- let graceMs = grace.graceMs, cfg = apiConfig(args), run2 = await getApiRun(cfg, id);
14210
+ let graceMs = grace.graceMs, cfg = apiConfig(), run2 = await getApiRun(cfg, id).catch((e) => {
14211
+ if (isTransientError(e)) return null;
14212
+ throw e;
14213
+ });
13674
14214
  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
14215
  if (say(RESULT_MESSAGES.RUN_CANCEL_PREVIEW(id, force)), !await confirmOrAbort(args, prompter)) return 1;
13676
14216
  let res = await cancelApiRun(cfg, id, force, graceMs);
@@ -13683,7 +14223,7 @@ async function runRunCancel(args, prompter = ttyPrompter) {
13683
14223
  return sayOk(RESULT_MESSAGES.RUN_CANCELLED(id, was)), 0;
13684
14224
  });
13685
14225
  }
13686
- var init_run = __esm({ "src/commands/run.ts"() {
14226
+ var noteBusyRetry, outputDocument, init_run = __esm({ "src/commands/run.ts"() {
13687
14227
  "use strict";
13688
14228
  init_define_IGNITIFY_CLIENT_FILES();
13689
14229
  init_prompt();
@@ -13692,7 +14232,15 @@ var init_run = __esm({ "src/commands/run.ts"() {
13692
14232
  init_utils();
13693
14233
  init_output();
13694
14234
  init_grace();
14235
+ init_follow();
14236
+ init_poll();
13695
14237
  init_shared();
14238
+ init_run_follow();
14239
+ noteBusyRetry = (delayMs) => {
14240
+ tell(BUSY_READ.NOTE(Math.ceil(delayMs / MS_PER_SECOND)));
14241
+ };
14242
+ outputDocument = (chunk) => ({ run_id: chunk.runId, bytes: chunk.bytes, lines: chunk.bytes.split(`
14243
+ `).length - 1, truncated: false, next_offset: chunk.nextOffset });
13696
14244
  } });
13697
14245
  function decideVmDelete(vm, force, vmId) {
13698
14246
  if (vm.residency === "pinned" && !force) {
@@ -13714,7 +14262,7 @@ function runVmList(args) {
13714
14262
  async function runVmGet(args) {
13715
14263
  let id = requireArg(args, "vm-id");
13716
14264
  return id ? guard(async () => {
13717
- let cfg = apiConfig(args), data = await getApiVm(cfg, id);
14265
+ let cfg = apiConfig(), data = await getApiVm(cfg, id);
13718
14266
  return CliOutputManager.render("vm:get", { ...data }), 0;
13719
14267
  }) : 1;
13720
14268
  }
@@ -13723,7 +14271,7 @@ async function runVmDelete(args, prompter = ttyPrompter) {
13723
14271
  if (!id) return 1;
13724
14272
  let force = boolFlag(args, "force");
13725
14273
  return guard(async () => {
13726
- let cfg = apiConfig(args), vm = await getApiVm(cfg, id), decision = decideVmDelete(vm, force, id);
14274
+ let cfg = apiConfig(), vm = await getApiVm(cfg, id), decision = decideVmDelete(vm, force, id);
13727
14275
  if (decision.refuse) return fail(decision.message), 1;
13728
14276
  if (say(decision.notice), !await confirmOrAbort(args, prompter)) return 1;
13729
14277
  let res = await deleteVm(cfg, id, force);
@@ -13751,20 +14299,20 @@ async function runSecretSet(args) {
13751
14299
  if (args.options.stdin !== true) return tell(MESSAGES.SECRET_SET_USAGE), 1;
13752
14300
  let value = import_fs5.default.readFileSync(0, "utf8").replace(/\n$/, "");
13753
14301
  return value === "" ? (fail(MESSAGES.SECRET_VALUE_EMPTY), 1) : guard(async () => {
13754
- let cfg = apiConfig(args), res = await setSecret(cfg, name, value);
14302
+ let cfg = apiConfig(), res = await setSecret(cfg, name, value);
13755
14303
  return emit(res), sayOk(MESSAGES.SECRET_STORED(name)), 0;
13756
14304
  });
13757
14305
  }
13758
14306
  async function runSecretList(args) {
13759
14307
  return guard(async () => {
13760
- let cfg = apiConfig(args), data = await listSecrets(cfg);
14308
+ let cfg = apiConfig(), data = await listSecrets(cfg);
13761
14309
  return emit(data, (st) => data.secrets.length === 0 ? emptyState(st, RESULT_MESSAGES.NO_SECRETS, RESULT_MESSAGES.NO_SECRETS_NEXT) : renderTable(st, [{ header: "name", keep: true }, { header: "updated_at", keep: true }], data.secrets.map((x) => [x.name, x.updated_at]))), 0;
13762
14310
  });
13763
14311
  }
13764
14312
  async function runSecretDelete(args, prompter = ttyPrompter) {
13765
14313
  let name = args.positionals[0];
13766
14314
  return name ? guard(async () => {
13767
- let cfg = apiConfig(args);
14315
+ let cfg = apiConfig();
13768
14316
  if (tell(RESULT_MESSAGES.SECRET_DELETE_PREVIEW(name)), !await confirmOrAbort(args, prompter)) return 1;
13769
14317
  let res = await rmSecret(cfg, name);
13770
14318
  return emit(res), sayOk(MESSAGES.SECRET_REMOVED(name)), 0;
@@ -13784,14 +14332,14 @@ var import_fs5, secretValueOnArgv, init_secret = __esm({ "src/commands/secret.ts
13784
14332
  } });
13785
14333
  async function runScheduleList(args) {
13786
14334
  return guard(async () => {
13787
- let cfg = apiConfig(args), res = await listSchedules(cfg);
14335
+ let cfg = apiConfig(), res = await listSchedules(cfg);
13788
14336
  return CliOutputManager.render("schedule:list", res.schedules.map((row) => ({ ...row }))), 0;
13789
14337
  });
13790
14338
  }
13791
14339
  async function runScheduleCancel(args, prompter = ttyPrompter) {
13792
14340
  let id = requireArg(args, "schedule-id");
13793
14341
  return id ? guard(async () => {
13794
- let cfg = apiConfig(args);
14342
+ let cfg = apiConfig();
13795
14343
  if (say(RESULT_MESSAGES.SCHEDULE_CANCEL_PREVIEW(id)), !await confirmOrAbort(args, prompter)) return 1;
13796
14344
  try {
13797
14345
  await deleteSchedule(cfg, id);
@@ -13812,12 +14360,37 @@ var init_schedule = __esm({ "src/commands/schedule.ts"() {
13812
14360
  init_output();
13813
14361
  init_shared();
13814
14362
  } });
14363
+ function notificationView(unread, open) {
14364
+ return unread && open ? null : unread ? "unread" : open ? "open" : "all";
14365
+ }
14366
+ function notificationMoreNote(rawLimit) {
14367
+ return (rawLimit === void 0 ? NOTIFICATION_PAGE_DEFAULT : Number(rawLimit)) < NOTIFICATION_PAGE_MAX ? MESSAGES.NOTIFICATION_MORE(NOTIFICATION_PAGE_MAX) : MESSAGES.NOTIFICATION_MORE_IN_APP;
14368
+ }
14369
+ async function runNotificationList(args) {
14370
+ let view = notificationView(boolFlag(args, "unread"), boolFlag(args, "open"));
14371
+ if (view === null) return fail(MESSAGES.NOTIFICATION_ONE_VIEW), 1;
14372
+ let badLimit = limitRefusal(args);
14373
+ return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
14374
+ let page = await listNotifications(apiConfig(), { ...flagParams(args, NOTIFICATION_LIST_FLAG_PARAMS), [NOTIFICATION_QUERY.FILTER]: view });
14375
+ return emit(page, (st) => renderNotifications(st, page)), page.next_cursor !== null && warn(notificationMoreNote(strOpt(args, "limit"))), 0;
14376
+ });
14377
+ }
14378
+ var init_notification = __esm({ "src/commands/notification.ts"() {
14379
+ "use strict";
14380
+ init_define_IGNITIFY_CLIENT_FILES();
14381
+ init_tenant();
14382
+ init_api();
14383
+ init_utils();
14384
+ init_output();
14385
+ init_constants();
14386
+ init_shared();
14387
+ } });
13815
14388
  function withAbsoluteUrl(created, baseUrl) {
13816
14389
  let { url } = created;
13817
14390
  return typeof url != "string" || !url.startsWith("/") ? created : { ...created, url: `${baseUrl.replace(/\/+$/, "")}${url}` };
13818
14391
  }
13819
14392
  function renderHookCreated(st, created) {
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)) };
14393
+ let { secret, secret_redacted: _redacted, ...rest } = created, rows = { ...rest, [RESULT_MESSAGES.HOOK_SECRET_KEY]: secret }, paint2 = { [RESULT_MESSAGES.HOOK_SECRET_KEY]: (s) => st.bold(st.yellow(s)) };
13821
14394
  return `
13822
14395
  ${heading(st, RESULT_MESSAGES.HOOK_CREATED_TITLE, true)}
13823
14396
  ${renderFields(st, rows, void 0, paint2)}
@@ -13828,8 +14401,8 @@ async function runHookCreate(args) {
13828
14401
  if (!deploymentId) return tell(MESSAGES.HOOK_CREATE_USAGE), 1;
13829
14402
  let taskName = args.options["task-name"];
13830
14403
  return typeof taskName != "string" || taskName.trim() === "" ? (fail(MESSAGES.HOOK_TASK_NAME_REQUIRED), 1) : guard(async () => {
13831
- let cfg = apiConfig(args), created = withAbsoluteUrl(await createHook(cfg, deploymentId, { task_name: taskName }), cfg.baseUrl);
13832
- return warn(MESSAGES.HOOK_SECRET_SHOWN_ONCE), tell(MESSAGES.HOOK_PAYLOAD_LIMIT(formatLimit(await payloadLimit(cfg)))), emit(created, (st) => renderHookCreated(st, created)), 0;
14404
+ let cfg = apiConfig(), created = withAbsoluteUrl(await createHook(cfg, deploymentId, { task_name: taskName }), cfg.baseUrl);
14405
+ return created.secret === null ? (fail(MESSAGES.HOOK_SECRET_REDACTED(created.resource_id)), emit(created), 1) : (warn(MESSAGES.HOOK_SECRET_SHOWN_ONCE), tell(MESSAGES.HOOK_PAYLOAD_LIMIT(formatLimit(await payloadLimit(cfg)))), emit(created, (st) => renderHookCreated(st, created)), 0);
13833
14406
  });
13834
14407
  }
13835
14408
  async function runHookList(args) {
@@ -13837,14 +14410,14 @@ async function runHookList(args) {
13837
14410
  if (!deploymentId) return tell(MESSAGES.HOOK_LIST_USAGE), 1;
13838
14411
  let badLimit = limitRefusal(args);
13839
14412
  return badLimit !== null ? (fail(badLimit), 1) : guard(async () => {
13840
- let cfg = apiConfig(args), res = await listHooks(cfg, deploymentId, flagParams(args, PAGE_FLAG_PARAMS)), hooks = Array.isArray(res.hooks) ? res.hooks : [];
14413
+ let cfg = apiConfig(), res = await listHooks(cfg, deploymentId, flagParams(args, PAGE_FLAG_PARAMS)), hooks = Array.isArray(res.hooks) ? res.hooks : [];
13841
14414
  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;
13842
14415
  });
13843
14416
  }
13844
14417
  async function runHookDelete(args, prompter = ttyPrompter) {
13845
14418
  let hookId = args.positionals[0];
13846
14419
  return hookId ? guard(async () => {
13847
- let cfg = apiConfig(args);
14420
+ let cfg = apiConfig();
13848
14421
  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;
13849
14422
  }) : (tell(MESSAGES.HOOK_DELETE_USAGE), 1);
13850
14423
  }
@@ -13856,7 +14429,7 @@ async function runNotifySet(args) {
13856
14429
  if (refusal !== null) return refuseNotifySet(refusal);
13857
14430
  let secret = args.options["secret-stdin"] === true ? import_fs6.default.readFileSync(0, "utf8").replace(/\n$/, "") : void 0;
13858
14431
  return secret === "" ? refuseNotifySet(MESSAGES.NOTIFY_SECRET_EMPTY) : guard(async () => {
13859
- let cfg = apiConfig(args), target = clear || typeof url != "string" ? null : url;
14432
+ let cfg = apiConfig(), target = clear || typeof url != "string" ? null : url;
13860
14433
  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;
13861
14434
  });
13862
14435
  }
@@ -13879,6 +14452,198 @@ var import_fs6, init_hook = __esm({ "src/commands/hook.ts"() {
13879
14452
  init_shared();
13880
14453
  init_output();
13881
14454
  } });
14455
+ async function runPermissionList(_args) {
14456
+ let permissions = permissionRows();
14457
+ return emit({ permissions }, (st) => renderPermissions(st, permissions)), 0;
14458
+ }
14459
+ var init_permission = __esm({ "src/commands/permission.ts"() {
14460
+ "use strict";
14461
+ init_define_IGNITIFY_CLIENT_FILES();
14462
+ init_access();
14463
+ init_output();
14464
+ } });
14465
+ async function runUserCreate(args) {
14466
+ let name = accessNameArg(args.positionals[0], ACCESS.USER, "name");
14467
+ return name === null ? 1 : guard(async () => {
14468
+ let created = await createUser(apiConfig(), name);
14469
+ return emit(created, (st) => renderFields(st, { ...created })), sayOk(MESSAGES.USER_CREATED(created.name)), 0;
14470
+ });
14471
+ }
14472
+ async function runUserList(_args) {
14473
+ return guard(async () => {
14474
+ let data = await listUsers(apiConfig());
14475
+ return emit(data, (st) => renderUsers(st, data.users)), 0;
14476
+ });
14477
+ }
14478
+ async function runUserGet(args) {
14479
+ let name = accessNameArg(args.positionals[0], ACCESS.USER, "name");
14480
+ return name === null ? 1 : guard(async () => {
14481
+ let user = await getUser(apiConfig(), name);
14482
+ return emit(user, (st) => renderUser(st, user)), 0;
14483
+ });
14484
+ }
14485
+ async function runUserRemove(args, prompter = ttyPrompter) {
14486
+ let name = accessNameArg(args.positionals[0], ACCESS.USER, "name");
14487
+ return name === null ? 1 : guard(async () => {
14488
+ let cfg = apiConfig();
14489
+ if (tell(RESULT_MESSAGES.USER_REMOVE_PREVIEW(name)), !await confirmOrAbort(args, prompter)) return 1;
14490
+ let res = await deleteUser(cfg, name);
14491
+ return emit(res), sayOk(MESSAGES.USER_REMOVED(res.removed, res.revoked_keys.length)), 0;
14492
+ });
14493
+ }
14494
+ var init_user = __esm({ "src/commands/user.ts"() {
14495
+ "use strict";
14496
+ init_define_IGNITIFY_CLIENT_FILES();
14497
+ init_api();
14498
+ init_prompt();
14499
+ init_constants();
14500
+ init_utils();
14501
+ init_output();
14502
+ init_shared();
14503
+ } });
14504
+ async function groupWrite(write, done) {
14505
+ return guard(async () => {
14506
+ let group = await write(apiConfig());
14507
+ return emit(group, (st) => renderGroup(st, group)), sayOk(done), 0;
14508
+ });
14509
+ }
14510
+ function permissionArg(raw) {
14511
+ if (raw === void 0 || raw === "") return fail(MESSAGES.ARG_REQUIRED("permission")), null;
14512
+ let checked = checkPermission(raw);
14513
+ return checked.ok ? checked.value : (fail(checked.message), null);
14514
+ }
14515
+ async function runGroupCreate(args) {
14516
+ let name = accessNameArg(args.positionals[0], ACCESS.GROUP, "name");
14517
+ if (name === null) return 1;
14518
+ let permissions = parsePermissions(strOpt(args, "permissions"));
14519
+ return permissions.ok ? groupWrite((cfg) => createGroup(cfg, name, permissions.value), MESSAGES.GROUP_CREATED(name)) : (fail(permissions.message), 1);
14520
+ }
14521
+ async function runGroupList(_args) {
14522
+ return guard(async () => {
14523
+ let data = await listGroups(apiConfig());
14524
+ return emit(data, (st) => renderGroups(st, data.groups)), 0;
14525
+ });
14526
+ }
14527
+ async function runGroupGet(args) {
14528
+ let name = accessNameArg(args.positionals[0], ACCESS.GROUP, "name");
14529
+ return name === null ? 1 : guard(async () => {
14530
+ let group = await getGroup(apiConfig(), name);
14531
+ return emit(group, (st) => renderGroup(st, group)), 0;
14532
+ });
14533
+ }
14534
+ async function runGroupGrant(args) {
14535
+ let name = accessNameArg(args.positionals[0], ACCESS.GROUP, "name");
14536
+ if (name === null) return 1;
14537
+ let permission = permissionArg(args.positionals[1]);
14538
+ return permission === null ? 1 : groupWrite((cfg) => grantGroupPermission(cfg, name, permission), MESSAGES.GROUP_GRANTED(name, permission));
14539
+ }
14540
+ async function runGroupRevoke(args) {
14541
+ let name = accessNameArg(args.positionals[0], ACCESS.GROUP, "name");
14542
+ if (name === null) return 1;
14543
+ let permission = permissionArg(args.positionals[1]);
14544
+ return permission === null ? 1 : groupWrite((cfg) => revokeGroupPermission(cfg, name, permission), MESSAGES.GROUP_REVOKED(name, permission));
14545
+ }
14546
+ async function runGroupAddUser(args) {
14547
+ let group = accessNameArg(args.positionals[0], ACCESS.GROUP, "group");
14548
+ if (group === null) return 1;
14549
+ let user = accessNameArg(args.positionals[1], ACCESS.USER, "user");
14550
+ return user === null ? 1 : groupWrite((cfg) => addGroupUser(cfg, group, user), MESSAGES.GROUP_USER_ADDED(group, user));
14551
+ }
14552
+ async function runGroupRemoveUser(args) {
14553
+ let group = accessNameArg(args.positionals[0], ACCESS.GROUP, "group");
14554
+ if (group === null) return 1;
14555
+ let user = accessNameArg(args.positionals[1], ACCESS.USER, "user");
14556
+ return user === null ? 1 : groupWrite((cfg) => removeGroupUser(cfg, group, user), MESSAGES.GROUP_USER_REMOVED(group, user));
14557
+ }
14558
+ async function runGroupDelete(args, prompter = ttyPrompter) {
14559
+ let name = accessNameArg(args.positionals[0], ACCESS.GROUP, "name");
14560
+ return name === null ? 1 : guard(async () => {
14561
+ let cfg = apiConfig();
14562
+ if (tell(RESULT_MESSAGES.GROUP_DELETE_PREVIEW(name)), !await confirmOrAbort(args, prompter)) return 1;
14563
+ let res = await deleteGroup(cfg, name);
14564
+ return emit(res), sayOk(MESSAGES.GROUP_DELETED(res.deleted)), 0;
14565
+ });
14566
+ }
14567
+ var init_group = __esm({ "src/commands/group.ts"() {
14568
+ "use strict";
14569
+ init_define_IGNITIFY_CLIENT_FILES();
14570
+ init_api();
14571
+ init_prompt();
14572
+ init_constants();
14573
+ init_access();
14574
+ init_utils();
14575
+ init_output();
14576
+ init_shared();
14577
+ } });
14578
+ function keyFileDocument(created, out2) {
14579
+ let { raw_key: _raw, ...rest } = created;
14580
+ return { ...rest, out: out2 };
14581
+ }
14582
+ function openOutFile(out2) {
14583
+ try {
14584
+ return { fd: createPrivateFile(out2), path: out2 };
14585
+ } catch (e) {
14586
+ return fail(errCode(e) === "EEXIST" ? MESSAGES.KEY_OUT_EXISTS(out2) : MESSAGES.KEY_OUT_FAILED(out2, errText(e))), null;
14587
+ }
14588
+ }
14589
+ async function runKeyCreate(args) {
14590
+ let rawUser = strOpt(args, "user");
14591
+ if (rawUser === void 0 || rawUser === "") return fail(MESSAGES.KEY_USER_REQUIRED), 1;
14592
+ let user = accessNameArg(rawUser, ACCESS.USER, "--user");
14593
+ if (user === null) return 1;
14594
+ let label = strOpt(args, "label")?.trim() ?? "";
14595
+ if (label === "") return fail(MESSAGES.KEY_LABEL_REQUIRED), 1;
14596
+ let out2 = strOpt(args, "out");
14597
+ return guard(async () => {
14598
+ let cfg = apiConfig(), file = null;
14599
+ if (out2 !== void 0 && (file = openOutFile(out2), file === null)) return 1;
14600
+ let created;
14601
+ try {
14602
+ created = await createKey(cfg, { user, label });
14603
+ } catch (e) {
14604
+ throw file !== null && discardPrivateFile(file.fd, file.path), e;
14605
+ }
14606
+ if (created.raw_key === null) return file !== null && discardPrivateFile(file.fd, file.path), fail(MESSAGES.KEY_REDACTED(created.resource_id)), 1;
14607
+ if (file !== null) {
14608
+ try {
14609
+ fillPrivateFile(file.fd, file.path, `${created.raw_key}
14610
+ `);
14611
+ } catch (e) {
14612
+ return fail(MESSAGES.KEY_OUT_WRITE_FAILED(file.path, errText(e), created.resource_id)), 1;
14613
+ }
14614
+ return emit(keyFileDocument(created, file.path)), sayOk(MESSAGES.KEY_WRITTEN(created.resource_id, file.path)), 0;
14615
+ }
14616
+ return warn(MESSAGES.KEY_SHOWN_ONCE), emit(created, (st) => renderKeyCreated(st, created)), 0;
14617
+ });
14618
+ }
14619
+ async function runKeyList(args) {
14620
+ let rawUser = strOpt(args, "user"), user = rawUser === void 0 ? void 0 : accessNameArg(rawUser, ACCESS.USER, "--user");
14621
+ return user === null ? 1 : guard(async () => {
14622
+ let data = await listKeys(apiConfig(), user);
14623
+ return emit(data, (st) => renderKeys(st, data.keys)), 0;
14624
+ });
14625
+ }
14626
+ async function runKeyRevoke(args, prompter = ttyPrompter) {
14627
+ let id = requireArg(args, "key-resource-id");
14628
+ return id ? guard(async () => {
14629
+ let cfg = apiConfig();
14630
+ if (tell(RESULT_MESSAGES.KEY_REVOKE_PREVIEW(id)), !await confirmOrAbort(args, prompter)) return 1;
14631
+ let res = await deleteKey(cfg, id);
14632
+ return emit(res), sayOk(MESSAGES.KEY_REVOKED(res.revoked)), 0;
14633
+ }) : 1;
14634
+ }
14635
+ var init_key = __esm({ "src/commands/key.ts"() {
14636
+ "use strict";
14637
+ init_define_IGNITIFY_CLIENT_FILES();
14638
+ init_tenant();
14639
+ init_api();
14640
+ init_credentials();
14641
+ init_prompt();
14642
+ init_constants();
14643
+ init_utils();
14644
+ init_output();
14645
+ init_shared();
14646
+ } });
13882
14647
  var init_commands = __esm({ "src/commands/index.ts"() {
13883
14648
  "use strict";
13884
14649
  init_define_IGNITIFY_CLIENT_FILES();
@@ -13892,7 +14657,12 @@ var init_commands = __esm({ "src/commands/index.ts"() {
13892
14657
  init_secret();
13893
14658
  init_schedule();
13894
14659
  init_integration();
14660
+ init_notification();
13895
14661
  init_hook();
14662
+ init_permission();
14663
+ init_user();
14664
+ init_group();
14665
+ init_key();
13896
14666
  init_decide_vm_delete();
13897
14667
  } });
13898
14668
  function metaFor(command) {
@@ -13924,7 +14694,7 @@ function checkInvocation(command, meta, pass) {
13924
14694
  for (let key of Object.keys(args.options)) {
13925
14695
  if (Object.hasOwn(flags, key)) continue;
13926
14696
  let short = typed[key];
13927
- return short !== void 0 ? MESSAGES.UNKNOWN_SHORT_FLAG(short, key, command) : unknownFlagRefusal(key, command, Object.keys(meta.flags), Object.keys(GLOBAL_FLAGS), HIDDEN_FLAGS);
14697
+ return short !== void 0 ? MESSAGES.UNKNOWN_SHORT_FLAG(short, key, command) : unknownFlagRefusal(key, command, Object.keys(meta.flags), Object.keys(GLOBAL_FLAGS));
13928
14698
  }
13929
14699
  let badShort = pass.unknown[0];
13930
14700
  return badShort !== void 0 ? unknownFlagText(badShort, command) : meta.secretArgv === true ? null : shapeRefusal(command, args, flags, meta.positionals);
@@ -13962,7 +14732,7 @@ async function dispatch(plan) {
13962
14732
  case "unknown":
13963
14733
  return fail(`${MESSAGES.UNKNOWN_COMMAND}: ${plan.command}`, { hint: MESSAGES.SEE_HELP }), 1;
13964
14734
  case "version":
13965
- return runVersion(plan.args);
14735
+ return runVersion();
13966
14736
  case "run":
13967
14737
  return route(plan.command, plan.args);
13968
14738
  }
@@ -13993,6 +14763,8 @@ function route(command, args) {
13993
14763
  return runUsageShow(args);
13994
14764
  case COMMANDS.USAGE_EXPORT:
13995
14765
  return runUsageExport(args);
14766
+ case COMMANDS.USAGE_SERIES:
14767
+ return runUsageSeries(args);
13996
14768
  case COMMANDS.TASK_CREATE:
13997
14769
  return runTaskCreate(args);
13998
14770
  case COMMANDS.TASK_LIST:
@@ -14013,6 +14785,10 @@ function route(command, args) {
14013
14785
  return runRunGet(args);
14014
14786
  case COMMANDS.RUN_LOGS:
14015
14787
  return runRunLogs(args);
14788
+ case COMMANDS.RUN_TIMELINE:
14789
+ return runRunTimeline(args);
14790
+ case COMMANDS.RUN_FAILURES:
14791
+ return runRunFailures(args);
14016
14792
  case COMMANDS.RUN_DELETE:
14017
14793
  return runRunDelete(args);
14018
14794
  case COMMANDS.RUN_CANCEL:
@@ -14043,6 +14819,40 @@ function route(command, args) {
14043
14819
  return runNotifySet(args);
14044
14820
  case COMMANDS.INTEGRATION_ADD:
14045
14821
  return runIntegrationAdd(args);
14822
+ case COMMANDS.NOTIFICATION_LIST:
14823
+ return runNotificationList(args);
14824
+ case COMMANDS.PERMISSION_LIST:
14825
+ return runPermissionList(args);
14826
+ case COMMANDS.USER_CREATE:
14827
+ return runUserCreate(args);
14828
+ case COMMANDS.USER_LIST:
14829
+ return runUserList(args);
14830
+ case COMMANDS.USER_GET:
14831
+ return runUserGet(args);
14832
+ case COMMANDS.USER_REMOVE:
14833
+ return runUserRemove(args);
14834
+ case COMMANDS.GROUP_CREATE:
14835
+ return runGroupCreate(args);
14836
+ case COMMANDS.GROUP_LIST:
14837
+ return runGroupList(args);
14838
+ case COMMANDS.GROUP_GET:
14839
+ return runGroupGet(args);
14840
+ case COMMANDS.GROUP_GRANT:
14841
+ return runGroupGrant(args);
14842
+ case COMMANDS.GROUP_REVOKE:
14843
+ return runGroupRevoke(args);
14844
+ case COMMANDS.GROUP_ADD_USER:
14845
+ return runGroupAddUser(args);
14846
+ case COMMANDS.GROUP_REMOVE_USER:
14847
+ return runGroupRemoveUser(args);
14848
+ case COMMANDS.GROUP_DELETE:
14849
+ return runGroupDelete(args);
14850
+ case COMMANDS.KEY_CREATE:
14851
+ return runKeyCreate(args);
14852
+ case COMMANDS.KEY_LIST:
14853
+ return runKeyList(args);
14854
+ case COMMANDS.KEY_REVOKE:
14855
+ return runKeyRevoke(args);
14046
14856
  }
14047
14857
  }
14048
14858
  async function main(argv) {