@indigoai-us/hq-cli 5.345.173 → 5.345.174

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.174] — 2026-10-27
6
+ - `hq channels delete <name> --yes` deletes a channel you own, including its
7
+ messages. Group DMs stay undeletable. Without `--yes` the command refuses.
8
+ - New `hq deploy domains base add | status | remove` sets up a company deploy domain, so every app of the company serves at `<app>.<your-domain>`. `add` shows the monthly price for your company (included on Enterprise) and the two DNS records to create. `status --wait` checks every 15 seconds until the domain is live and gives up after 15 minutes (`--timeout` sets up to 60). `remove` releases the domain.
9
+ - Any signed-in HQ user can now create a bot with `--hq-fleet-runtime`: the CLI downloads the signed HQ Fleet Local release through short-lived links from HQ instead of a private GitHub release, and still checks every file against its pinned hash and the release signature. GitHub access is only used when HQ has no download for the pinned version.
10
+ - A bot created with `--hq-fleet-runtime` now runs HQ's own hooks: its tool calls, prompts and turn ends go through the HQ folder's `.claude` hooks, so it is refused the same core edits, secret dumps and unbound company reads a Claude Code session is, and it binds a company for its conversation before working on that company's files. Messages that arrive while the bot is answering now wait for that answer instead of stalling its inbox. Pins hq-fleet-local v0.4.6.
11
+
5
12
  ## [5.345.173] — 2026-10-27
6
13
  - The sync-core reconcile now counts the conflict and merge actions in a vault's read-only plan as writes: a vault is held read-only while its plan would write more than 100 conflict or merge copies, the count is recorded next to the plan's upload, download and delete counts in `~/.hq/sync-core/rollout.json`, and `hq sync-core status` prints it.
7
14
  - A machine can keep one vault off the new sync engine while its company switch is on: `~/.hq/sync-core/skip.json` with `{"vaults": {"<vault uid>": "<reason>"}}` makes the reconcile leave that vault on the current sync (stopping its engine unit if one ran), record the uid and reason under `skipped` in `rollout.json`, and show it in `hq sync-core status`. A malformed file is reported as a warning and skips nothing.
@@ -4365,6 +4365,31 @@ export declare const COMMAND_CATALOG: readonly [{
4365
4365
  readonly hidden: false;
4366
4366
  }];
4367
4367
  readonly subcommands: readonly [];
4368
+ }, {
4369
+ readonly name: "delete";
4370
+ readonly description: "Delete a channel you own, including its messages. Group DMs cannot be deleted.";
4371
+ readonly aliases: readonly [];
4372
+ readonly hidden: false;
4373
+ readonly helpOption: true;
4374
+ readonly helpCommand: false;
4375
+ readonly usage: "[options] <channel>";
4376
+ readonly helpText: readonly [];
4377
+ readonly arguments: readonly [{
4378
+ readonly name: "channel";
4379
+ readonly required: true;
4380
+ readonly variadic: false;
4381
+ readonly description: "";
4382
+ }];
4383
+ readonly options: readonly [{
4384
+ readonly flags: "--json";
4385
+ readonly description: "Output { ok, deleted } as JSON.";
4386
+ readonly hidden: false;
4387
+ }, {
4388
+ readonly flags: "--yes";
4389
+ readonly description: "Confirm the delete. Required.";
4390
+ readonly hidden: false;
4391
+ }];
4392
+ readonly subcommands: readonly [];
4368
4393
  }];
4369
4394
  }, {
4370
4395
  readonly name: "onboard";
@@ -19837,6 +19862,111 @@ export declare const COMMAND_CATALOG: readonly [{
19837
19862
  readonly hidden: false;
19838
19863
  }];
19839
19864
  readonly subcommands: readonly [];
19865
+ }, {
19866
+ readonly name: "base";
19867
+ readonly description: "Manage the company deploy domain: one domain for every app (<app>.<domain>)";
19868
+ readonly aliases: readonly [];
19869
+ readonly hidden: false;
19870
+ readonly helpOption: true;
19871
+ readonly helpCommand: true;
19872
+ readonly usage: "[options] [command]";
19873
+ readonly helpText: readonly [];
19874
+ readonly arguments: readonly [];
19875
+ readonly options: readonly [];
19876
+ readonly subcommands: readonly [{
19877
+ readonly name: "add";
19878
+ readonly description: "Claim a company deploy domain; prints the price and the DNS records";
19879
+ readonly aliases: readonly [];
19880
+ readonly hidden: false;
19881
+ readonly helpOption: true;
19882
+ readonly helpCommand: false;
19883
+ readonly usage: "[options] <domain>";
19884
+ readonly helpText: readonly [];
19885
+ readonly arguments: readonly [{
19886
+ readonly name: "domain";
19887
+ readonly required: true;
19888
+ readonly variadic: false;
19889
+ readonly description: "";
19890
+ }];
19891
+ readonly options: readonly [{
19892
+ readonly flags: "--yes";
19893
+ readonly description: "add without a prompt (the domain can add a monthly charge)";
19894
+ readonly hidden: false;
19895
+ }, {
19896
+ readonly flags: "--company <slug>";
19897
+ readonly description: "use this company instead of the linked or active company";
19898
+ readonly hidden: false;
19899
+ }, {
19900
+ readonly flags: "--path <dir>";
19901
+ readonly description: "folder linked to an app of the company (default: current folder)";
19902
+ readonly hidden: false;
19903
+ }, {
19904
+ readonly flags: "--json";
19905
+ readonly description: "print machine-readable JSON";
19906
+ readonly hidden: false;
19907
+ }];
19908
+ readonly subcommands: readonly [];
19909
+ }, {
19910
+ readonly name: "status";
19911
+ readonly description: "Check the company deploy domain and advance it toward live (exits 2 while not live)";
19912
+ readonly aliases: readonly [];
19913
+ readonly hidden: false;
19914
+ readonly helpOption: true;
19915
+ readonly helpCommand: false;
19916
+ readonly usage: "[options]";
19917
+ readonly helpText: readonly [];
19918
+ readonly arguments: readonly [];
19919
+ readonly options: readonly [{
19920
+ readonly flags: "--wait";
19921
+ readonly description: "keep checking until it is live or the timeout passes";
19922
+ readonly hidden: false;
19923
+ }, {
19924
+ readonly flags: "--timeout <minutes>";
19925
+ readonly description: "stop waiting after this many minutes (default 15, at most 60)";
19926
+ readonly hidden: false;
19927
+ }, {
19928
+ readonly flags: "--company <slug>";
19929
+ readonly description: "use this company instead of the linked or active company";
19930
+ readonly hidden: false;
19931
+ }, {
19932
+ readonly flags: "--path <dir>";
19933
+ readonly description: "folder linked to an app of the company (default: current folder)";
19934
+ readonly hidden: false;
19935
+ }, {
19936
+ readonly flags: "--json";
19937
+ readonly description: "print machine-readable JSON";
19938
+ readonly hidden: false;
19939
+ }];
19940
+ readonly subcommands: readonly [];
19941
+ }, {
19942
+ readonly name: "remove";
19943
+ readonly description: "Release the company deploy domain";
19944
+ readonly aliases: readonly [];
19945
+ readonly hidden: false;
19946
+ readonly helpOption: true;
19947
+ readonly helpCommand: false;
19948
+ readonly usage: "[options]";
19949
+ readonly helpText: readonly [];
19950
+ readonly arguments: readonly [];
19951
+ readonly options: readonly [{
19952
+ readonly flags: "--yes";
19953
+ readonly description: "remove without a prompt";
19954
+ readonly hidden: false;
19955
+ }, {
19956
+ readonly flags: "--company <slug>";
19957
+ readonly description: "use this company instead of the linked or active company";
19958
+ readonly hidden: false;
19959
+ }, {
19960
+ readonly flags: "--path <dir>";
19961
+ readonly description: "folder linked to an app of the company (default: current folder)";
19962
+ readonly hidden: false;
19963
+ }, {
19964
+ readonly flags: "--json";
19965
+ readonly description: "print machine-readable JSON";
19966
+ readonly hidden: false;
19967
+ }];
19968
+ readonly subcommands: readonly [];
19969
+ }];
19840
19970
  }, {
19841
19971
  readonly name: "search";
19842
19972
  readonly description: "Check whether a domain can be bought, and its price";
@@ -5290,6 +5290,37 @@ export const COMMAND_CATALOG = [
5290
5290
  }
5291
5291
  ],
5292
5292
  "subcommands": []
5293
+ },
5294
+ {
5295
+ "name": "delete",
5296
+ "description": "Delete a channel you own, including its messages. Group DMs cannot be deleted.",
5297
+ "aliases": [],
5298
+ "hidden": false,
5299
+ "helpOption": true,
5300
+ "helpCommand": false,
5301
+ "usage": "[options] <channel>",
5302
+ "helpText": [],
5303
+ "arguments": [
5304
+ {
5305
+ "name": "channel",
5306
+ "required": true,
5307
+ "variadic": false,
5308
+ "description": ""
5309
+ }
5310
+ ],
5311
+ "options": [
5312
+ {
5313
+ "flags": "--json",
5314
+ "description": "Output { ok, deleted } as JSON.",
5315
+ "hidden": false
5316
+ },
5317
+ {
5318
+ "flags": "--yes",
5319
+ "description": "Confirm the delete. Required.",
5320
+ "hidden": false
5321
+ }
5322
+ ],
5323
+ "subcommands": []
5293
5324
  }
5294
5325
  ]
5295
5326
  },
@@ -24186,6 +24217,134 @@ export const COMMAND_CATALOG = [
24186
24217
  ],
24187
24218
  "subcommands": []
24188
24219
  },
24220
+ {
24221
+ "name": "base",
24222
+ "description": "Manage the company deploy domain: one domain for every app (<app>.<domain>)",
24223
+ "aliases": [],
24224
+ "hidden": false,
24225
+ "helpOption": true,
24226
+ "helpCommand": true,
24227
+ "usage": "[options] [command]",
24228
+ "helpText": [],
24229
+ "arguments": [],
24230
+ "options": [],
24231
+ "subcommands": [
24232
+ {
24233
+ "name": "add",
24234
+ "description": "Claim a company deploy domain; prints the price and the DNS records",
24235
+ "aliases": [],
24236
+ "hidden": false,
24237
+ "helpOption": true,
24238
+ "helpCommand": false,
24239
+ "usage": "[options] <domain>",
24240
+ "helpText": [],
24241
+ "arguments": [
24242
+ {
24243
+ "name": "domain",
24244
+ "required": true,
24245
+ "variadic": false,
24246
+ "description": ""
24247
+ }
24248
+ ],
24249
+ "options": [
24250
+ {
24251
+ "flags": "--yes",
24252
+ "description": "add without a prompt (the domain can add a monthly charge)",
24253
+ "hidden": false
24254
+ },
24255
+ {
24256
+ "flags": "--company <slug>",
24257
+ "description": "use this company instead of the linked or active company",
24258
+ "hidden": false
24259
+ },
24260
+ {
24261
+ "flags": "--path <dir>",
24262
+ "description": "folder linked to an app of the company (default: current folder)",
24263
+ "hidden": false
24264
+ },
24265
+ {
24266
+ "flags": "--json",
24267
+ "description": "print machine-readable JSON",
24268
+ "hidden": false
24269
+ }
24270
+ ],
24271
+ "subcommands": []
24272
+ },
24273
+ {
24274
+ "name": "status",
24275
+ "description": "Check the company deploy domain and advance it toward live (exits 2 while not live)",
24276
+ "aliases": [],
24277
+ "hidden": false,
24278
+ "helpOption": true,
24279
+ "helpCommand": false,
24280
+ "usage": "[options]",
24281
+ "helpText": [],
24282
+ "arguments": [],
24283
+ "options": [
24284
+ {
24285
+ "flags": "--wait",
24286
+ "description": "keep checking until it is live or the timeout passes",
24287
+ "hidden": false
24288
+ },
24289
+ {
24290
+ "flags": "--timeout <minutes>",
24291
+ "description": "stop waiting after this many minutes (default 15, at most 60)",
24292
+ "hidden": false
24293
+ },
24294
+ {
24295
+ "flags": "--company <slug>",
24296
+ "description": "use this company instead of the linked or active company",
24297
+ "hidden": false
24298
+ },
24299
+ {
24300
+ "flags": "--path <dir>",
24301
+ "description": "folder linked to an app of the company (default: current folder)",
24302
+ "hidden": false
24303
+ },
24304
+ {
24305
+ "flags": "--json",
24306
+ "description": "print machine-readable JSON",
24307
+ "hidden": false
24308
+ }
24309
+ ],
24310
+ "subcommands": []
24311
+ },
24312
+ {
24313
+ "name": "remove",
24314
+ "description": "Release the company deploy domain",
24315
+ "aliases": [],
24316
+ "hidden": false,
24317
+ "helpOption": true,
24318
+ "helpCommand": false,
24319
+ "usage": "[options]",
24320
+ "helpText": [],
24321
+ "arguments": [],
24322
+ "options": [
24323
+ {
24324
+ "flags": "--yes",
24325
+ "description": "remove without a prompt",
24326
+ "hidden": false
24327
+ },
24328
+ {
24329
+ "flags": "--company <slug>",
24330
+ "description": "use this company instead of the linked or active company",
24331
+ "hidden": false
24332
+ },
24333
+ {
24334
+ "flags": "--path <dir>",
24335
+ "description": "folder linked to an app of the company (default: current folder)",
24336
+ "hidden": false
24337
+ },
24338
+ {
24339
+ "flags": "--json",
24340
+ "description": "print machine-readable JSON",
24341
+ "hidden": false
24342
+ }
24343
+ ],
24344
+ "subcommands": []
24345
+ }
24346
+ ]
24347
+ },
24189
24348
  {
24190
24349
  "name": "search",
24191
24350
  "description": "Check whether a domain can be bought, and its price",
@@ -212,6 +212,88 @@ async function runChannelsMembers(channelName, opts) {
212
212
  process.exit(1);
213
213
  }
214
214
  }
215
+ function channelDeleteError(status, err) {
216
+ const code = err.code ?? "";
217
+ if (code === "CHANNEL_NOT_OWNER" || status === 403) {
218
+ return "Only the channel owner may delete the channel.";
219
+ }
220
+ if (code === "CHANNEL_GROUP_NOT_DELETABLE" || status === 409) {
221
+ return "Group DMs cannot be deleted.";
222
+ }
223
+ if (code === "CHANNEL_DELETE_INCOMPLETE" || status === 503) {
224
+ return "Channel deletion did not finish — try again.";
225
+ }
226
+ return err.error ?? err.message ?? `Could not delete channel (HTTP ${status}).`;
227
+ }
228
+ /**
229
+ * `hq channels delete` — owner-only. The server already accepts
230
+ * DELETE /v1/notify/channels/{id} and removes messages with the channel.
231
+ * `--yes` is required so a mistyped name cannot wipe a room.
232
+ */
233
+ async function runChannelsDelete(channelName, opts) {
234
+ try {
235
+ if (!opts.yes) {
236
+ const message = "This permanently deletes the channel and its messages. Re-run with --yes to confirm.";
237
+ if (opts.json) {
238
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
239
+ }
240
+ console.error(chalk.red(message));
241
+ process.exit(1);
242
+ }
243
+ const token = await ensureCognitoToken();
244
+ const channels = await fetchChannels(token);
245
+ const matches = matchChannelsByName(channels, channelName);
246
+ if (matches.length === 0) {
247
+ const message = `No channel named '${channelName}' — run \`hq channels\` to see your channels.`;
248
+ const suggestions = suggestChannels(channels, channelName);
249
+ if (opts.json) {
250
+ console.log(JSON.stringify({ ok: false, error: message, suggestions }, null, 2));
251
+ }
252
+ console.error(chalk.red(withChannelSuggestions(message, suggestions)));
253
+ process.exit(1);
254
+ }
255
+ if (matches.length > 1) {
256
+ const scopes = matches.map((match) => match.scope ?? "?").join(", ");
257
+ console.error(chalk.red(`'${channelName}' matches ${matches.length} channels (${scopes}) — this is ambiguous.`));
258
+ process.exit(1);
259
+ }
260
+ const target = matches[0];
261
+ if (target.scope === "group") {
262
+ const message = "Group DMs cannot be deleted.";
263
+ if (opts.json) {
264
+ console.log(JSON.stringify({ ok: false, error: message, code: "CHANNEL_GROUP_NOT_DELETABLE" }, null, 2));
265
+ }
266
+ console.error(chalk.red(message));
267
+ process.exit(1);
268
+ }
269
+ const res = await vaultApiFetch({
270
+ token,
271
+ path: `/v1/notify/channels/${encodeURIComponent(target.channelId)}`,
272
+ method: "DELETE",
273
+ });
274
+ if (!res.ok) {
275
+ const err = (await res.json().catch(() => ({})));
276
+ const message = channelDeleteError(res.status, err);
277
+ if (opts.json) {
278
+ console.log(JSON.stringify({ ok: false, error: message, code: err.code ?? null, status: res.status }, null, 2));
279
+ }
280
+ console.error(chalk.red(message));
281
+ process.exit(1);
282
+ }
283
+ const body = (await res.json().catch(() => ({})));
284
+ const deleted = body.deleted ?? target.channelId;
285
+ if (opts.json) {
286
+ console.log(JSON.stringify({ ok: true, deleted }, null, 2));
287
+ }
288
+ else {
289
+ console.log(`Deleted ${target.name ?? target.slug ?? deleted}.`);
290
+ }
291
+ }
292
+ catch (err) {
293
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
294
+ process.exit(1);
295
+ }
296
+ }
215
297
  export function registerChannelsCommand(program) {
216
298
  const channels = program
217
299
  .command("channels")
@@ -239,5 +321,13 @@ export function registerChannelsCommand(program) {
239
321
  .action(async (channelName, opts) => {
240
322
  await runChannelsMembers(channelName, opts);
241
323
  });
324
+ channels
325
+ .command("delete <channel>")
326
+ .description("Delete a channel you own, including its messages. Group DMs cannot be deleted.")
327
+ .option("--json", "Output { ok, deleted } as JSON.")
328
+ .option("--yes", "Confirm the delete. Required.")
329
+ .action(async (channelName, opts) => {
330
+ await runChannelsDelete(channelName, opts);
331
+ });
242
332
  }
243
333
  //# sourceMappingURL=channels.js.map
@@ -47,6 +47,26 @@ export declare function addOnConfirmQuestion(domain: string, environment: string
47
47
  export declare const PURCHASE_TERMS_UNAVAILABLE = "The renewal and refund terms could not be loaded. Check them on the HQ pricing page before you buy.";
48
48
  /** The purchase terms published as usage.domains.purchase. Null when not published. */
49
49
  export declare function purchaseTermsLine(pricing: Record<string, unknown> | null): string | null;
50
+ /** Hard deadline for `base status --wait`, so a script never waits forever. */
51
+ export declare const BASE_WAIT_DEFAULT_MINUTES = 15;
52
+ export declare const BASE_WAIT_MAX_MINUTES = 60;
53
+ /** Clock hooks for `base status --wait`. Tests replace these. */
54
+ export declare const deployDomainsClock: {
55
+ now: () => number;
56
+ sleep: (ms: number) => Promise<void>;
57
+ };
58
+ /**
59
+ * The price line for a company deploy domain, from the server's answer:
60
+ * Enterprise includes it; a company with a price override (or comped) pays
61
+ * what the server says, $0 included; otherwise the server's price, falling
62
+ * back to the published premium price, then $500.
63
+ */
64
+ export declare function baseDomainPriceLine(body: {
65
+ billingBasis?: unknown;
66
+ monthlyPriceUsd?: unknown;
67
+ }, pricing: Record<string, unknown> | null): string | undefined;
68
+ /** The approval question for `base add`, before the server says the basis. */
69
+ export declare function baseAddConfirmQuestion(domain: string, orgSlug: string, pricing: Record<string, unknown> | null): string;
50
70
  export declare function parsePrice(raw: string): number;
51
71
  export declare function registerDeployDomainsCommand(deploy: Command): void;
52
72
  export {};
@@ -615,6 +615,223 @@ async function removeAction(domainArg, options) {
615
615
  console.log(displayText(warning));
616
616
  }
617
617
  }
618
+ // ---------------------------------------------------------------------------
619
+ // Company deploy domain (`base`): one domain for every app of the company
620
+ // (`<app>.<domain>`), served by hq-deploy's /api/orgs/:orgSlug/base-domain
621
+ // routes, the same ones the desktop Deployments panel uses. The flag,
622
+ // Enterprise and price checks run on the server (hq-pro), so this command
623
+ // prints the server's answer: billingBasis and monthlyPriceUsd.
624
+ /** Hard deadline for `base status --wait`, so a script never waits forever. */
625
+ export const BASE_WAIT_DEFAULT_MINUTES = 15;
626
+ export const BASE_WAIT_MAX_MINUTES = 60;
627
+ const BASE_POLL_INTERVAL_MS = 15_000;
628
+ /** Clock hooks for `base status --wait`. Tests replace these. */
629
+ export const deployDomainsClock = {
630
+ now: () => Date.now(),
631
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
632
+ };
633
+ const BASE_MESSAGES = {
634
+ DOMAIN_NOT_ENTITLED: "A company deploy domain is not enabled for this company yet.",
635
+ BASE_DOMAIN_NOT_FOUND: "This company has no deploy domain. Add one with 'hq deploy domains base add <domain>'.",
636
+ BASE_DOMAIN_ALREADY_SET: "This company already has a deploy domain. Remove it first with 'hq deploy domains base remove'.",
637
+ BASE_DOMAIN_CLAIM_EXPIRED: "The claim expired before it went live. Add the domain again to get new DNS records.",
638
+ BASE_DOMAIN_CLAIMED: "That domain is already the deploy domain of another company.",
639
+ ORG_ACCESS_DENIED: "You are not a member of that company.",
640
+ };
641
+ async function baseError(response, orgSlug) {
642
+ const clone = response.clone();
643
+ const body = await readBody(clone);
644
+ const code = apiCode(body, response.status);
645
+ if (BASE_MESSAGES[code] && !SERVER_WORDED_CODES.has(code)) {
646
+ return new DeployDomainsError(BASE_MESSAGES[code], code, response.status);
647
+ }
648
+ if (response.status === 409 && !DEPLOY_MESSAGES[code]) {
649
+ const serverMessage = apiField(body, "message");
650
+ if (typeof serverMessage === "string" && serverMessage.trim()) {
651
+ return new DeployDomainsError(displayText(serverMessage), code, response.status);
652
+ }
653
+ }
654
+ return deployError(response, orgSlug);
655
+ }
656
+ /**
657
+ * The price line for a company deploy domain, from the server's answer:
658
+ * Enterprise includes it; a company with a price override (or comped) pays
659
+ * what the server says, $0 included; otherwise the server's price, falling
660
+ * back to the published premium price, then $500.
661
+ */
662
+ export function baseDomainPriceLine(body, pricing) {
663
+ const basis = body.billingBasis;
664
+ const price = typeof body.monthlyPriceUsd === "number" && body.monthlyPriceUsd >= 0 ? body.monthlyPriceUsd : undefined;
665
+ if (basis === "enterprise")
666
+ return "Included with the Enterprise plan: no monthly charge.";
667
+ if (basis === "comped")
668
+ return `This company deploy domain is included for this company: ${usd(price ?? 0)}/month.`;
669
+ if (basis === "paid") {
670
+ const published = domainsBlock(pricing)?.premiumUsd;
671
+ const amount = price ?? (typeof published === "number" && published > 0 ? published : 500);
672
+ return amount === 0
673
+ ? "This company deploy domain is $0/month for this company (price override)."
674
+ : `This company deploy domain adds ${usd(amount)}/month to the company bill.`;
675
+ }
676
+ return undefined;
677
+ }
678
+ /** The approval question for `base add`, before the server says the basis. */
679
+ export function baseAddConfirmQuestion(domain, orgSlug, pricing) {
680
+ const premium = domainsBlock(pricing)?.premiumUsd;
681
+ const price = typeof premium === "number" && premium > 0 ? `${usd(premium)}/month` : "a monthly add-on";
682
+ return `Make ${displayText(domain)} the deploy domain of '${orgSlug}', so every app serves at <app>.${displayText(domain)}? ` +
683
+ `It costs ${price}, billed to '${orgSlug}', unless the company is Enterprise or has a price override.`;
684
+ }
685
+ function baseSlug(options) {
686
+ if (options.company !== undefined)
687
+ return resolveSlug(options.company, undefined);
688
+ const binding = readLinkedApp(path.resolve(options.path ?? process.cwd()));
689
+ return resolveSlug(undefined, binding?.orgSlug);
690
+ }
691
+ function baseRoute(orgSlug, suffix = "") {
692
+ return `/api/orgs/${encodeURIComponent(orgSlug)}/base-domain${suffix}`;
693
+ }
694
+ function baseState(body) {
695
+ const certificate = body.certificate && typeof body.certificate === "object" ? body.certificate : {};
696
+ const serving = body.serving && typeof body.serving === "object" ? body.serving : {};
697
+ const status = typeof body.status === "string" ? body.status : "unknown";
698
+ const servingStatus = typeof serving.status === "string" ? serving.status : null;
699
+ return {
700
+ ...(typeof body.baseDomain === "string" ? { baseDomain: body.baseDomain } : {}),
701
+ status,
702
+ certStatus: typeof certificate.status === "string" ? certificate.status : null,
703
+ servingStatus,
704
+ servingError: typeof serving.error === "string" && serving.error ? serving.error : null,
705
+ records: dnsRecords(body.records),
706
+ live: status === "live" || servingStatus === "live",
707
+ raw: body,
708
+ };
709
+ }
710
+ function describeBaseState(state) {
711
+ if (state.live)
712
+ return "live";
713
+ if (state.status === "failed")
714
+ return "failed";
715
+ if (state.certStatus !== "ISSUED")
716
+ return `waiting for DNS validation (certificate ${state.certStatus ?? "not requested"})`;
717
+ return `certificate issued, serving ${state.servingStatus ?? "not attached yet"}`;
718
+ }
719
+ async function baseAddAction(domainArg, options) {
720
+ const domain = normalizeDomain(domainArg);
721
+ const token = await ensureCognitoToken();
722
+ const orgSlug = baseSlug(options);
723
+ const pricing = await fetchPricingBody();
724
+ await confirm(baseAddConfirmQuestion(domain, orgSlug, pricing), options.yes, "add the company deploy domain");
725
+ const response = await request(token, orgSlug, baseRoute(orgSlug), "POST", { baseDomain: domain });
726
+ if (!response.ok)
727
+ throw await baseError(response, orgSlug);
728
+ const body = await readBody(response);
729
+ const state = baseState(body);
730
+ const priceLine = baseDomainPriceLine(body, pricing);
731
+ if (options.json) {
732
+ console.log(safeJson({
733
+ ok: true,
734
+ orgSlug,
735
+ baseDomain: state.baseDomain ?? domain,
736
+ status: state.status,
737
+ certificate: state.certStatus,
738
+ records: state.records,
739
+ ...(typeof body.billingBasis === "string" ? { billingBasis: body.billingBasis } : {}),
740
+ ...(typeof body.monthlyPriceUsd === "number" ? { monthlyPriceUsd: body.monthlyPriceUsd } : {}),
741
+ ...(priceLine ? { billingMessage: priceLine } : {}),
742
+ }));
743
+ return;
744
+ }
745
+ console.log(`${displayText(domain)} is now the deploy domain of '${orgSlug}'. It is not live yet.`);
746
+ if (priceLine)
747
+ console.log(priceLine);
748
+ printDnsRecords(state.records);
749
+ if (state.records.length < 2) {
750
+ console.log("The certificate's validation record can take a few seconds to appear; 'hq deploy domains base status' prints it.");
751
+ }
752
+ console.log("When the records are in place, run: hq deploy domains base status --wait");
753
+ }
754
+ async function baseStatusAction(options) {
755
+ const token = await ensureCognitoToken();
756
+ const orgSlug = baseSlug(options);
757
+ const minutes = options.timeout === undefined ? BASE_WAIT_DEFAULT_MINUTES : Number(options.timeout);
758
+ if (!Number.isFinite(minutes) || minutes <= 0 || minutes > BASE_WAIT_MAX_MINUTES) {
759
+ throw new DeployDomainsError(`--timeout must be 1 to ${BASE_WAIT_MAX_MINUTES} minutes.`, "INVALID_TIMEOUT");
760
+ }
761
+ const deadline = deployDomainsClock.now() + minutes * 60_000;
762
+ let state;
763
+ let attempts = 0;
764
+ for (;;) {
765
+ attempts += 1;
766
+ // Refresh advances the claim itself: it re-checks the certificate and,
767
+ // once issued, attaches serving and polls it to live.
768
+ const response = await request(token, orgSlug, baseRoute(orgSlug, "/refresh"), "POST");
769
+ if (!response.ok)
770
+ throw await baseError(response, orgSlug);
771
+ state = baseState(await readBody(response));
772
+ if (state.live || state.status === "failed" || !options.wait)
773
+ break;
774
+ if (!options.json)
775
+ console.error(`${new Date(deployDomainsClock.now()).toISOString()} ${describeBaseState(state)}`);
776
+ if (deployDomainsClock.now() + BASE_POLL_INTERVAL_MS > deadline)
777
+ break;
778
+ await deployDomainsClock.sleep(BASE_POLL_INTERVAL_MS);
779
+ }
780
+ const timedOut = Boolean(options.wait) && !state.live && state.status !== "failed";
781
+ if (!state.live)
782
+ process.exitCode = state.status === "failed" ? 1 : REFRESH_NOT_SERVING_EXIT_CODE;
783
+ if (options.json) {
784
+ console.log(safeJson({
785
+ ok: true,
786
+ orgSlug,
787
+ baseDomain: state.baseDomain ?? null,
788
+ status: state.status,
789
+ live: state.live,
790
+ certificate: state.certStatus,
791
+ serving: state.servingStatus,
792
+ ...(state.servingError ? { servingError: state.servingError } : {}),
793
+ records: state.live ? [] : state.records,
794
+ attempts,
795
+ ...(timedOut ? { timedOut: true } : {}),
796
+ }));
797
+ return;
798
+ }
799
+ const name = displayText(state.baseDomain ?? "The company deploy domain");
800
+ if (state.live) {
801
+ console.log(`${name} is live. Every app of '${orgSlug}' serves at <app>.${name}.`);
802
+ return;
803
+ }
804
+ if (state.status === "failed") {
805
+ console.log(`${name} failed to go live${state.servingError ? `: ${displayText(state.servingError)}` : "."} Remove it and add it again.`);
806
+ return;
807
+ }
808
+ console.log(`${name} is not live yet: ${describeBaseState(state)}.`);
809
+ printDnsRecords(state.records);
810
+ console.log(timedOut
811
+ ? `Stopped waiting after ${minutes} minutes. Run 'hq deploy domains base status --wait' again to keep waiting.`
812
+ : "Run 'hq deploy domains base status --wait' to wait until it is live.");
813
+ }
814
+ async function baseRemoveAction(options) {
815
+ const token = await ensureCognitoToken();
816
+ const orgSlug = baseSlug(options);
817
+ await confirm(`Remove the deploy domain of '${orgSlug}'? Every app stops serving on it, and if it is billed, its monthly charge stops.`, options.yes, "remove the company deploy domain");
818
+ const response = await request(token, orgSlug, baseRoute(orgSlug), "DELETE");
819
+ if (!response.ok)
820
+ throw await baseError(response, orgSlug);
821
+ const body = await readBody(response);
822
+ const removed = body.removed === true;
823
+ const billingRemovalPending = body.billingRemovalPending === true;
824
+ const warning = typeof body.warning === "string" ? body.warning : undefined;
825
+ if (options.json) {
826
+ console.log(safeJson({ ok: true, orgSlug, removed, billingRemovalPending, ...(warning ? { warning } : {}) }));
827
+ return;
828
+ }
829
+ console.log(removed ? `Removed the deploy domain of '${orgSlug}'.` : `'${orgSlug}' had no deploy domain to remove.`);
830
+ if (warning)
831
+ console.log(displayText(warning));
832
+ else if (billingRemovalPending)
833
+ console.log("Its charge could not be stopped right away. HQ keeps retrying until the charge is removed.");
834
+ }
618
835
  async function purchaseRequest(token, companyUid, suffix, body) {
619
836
  try {
620
837
  return await vaultApiFetch({
@@ -782,6 +999,26 @@ export function registerDeployDomainsCommand(deploy) {
782
999
  .description("Unlink a custom domain from an app")
783
1000
  .option("--env <name>", "pick the environment when the domain points at more than one")
784
1001
  .option("--yes", "remove without a prompt")).action((domain, options) => run(options.json, () => removeAction(domain, options)));
1002
+ const base = domains
1003
+ .command("base")
1004
+ .description("Manage the company deploy domain: one domain for every app (<app>.<domain>)");
1005
+ const baseCommon = (command) => command
1006
+ .option("--company <slug>", "use this company instead of the linked or active company")
1007
+ .option("--path <dir>", "folder linked to an app of the company (default: current folder)")
1008
+ .option("--json", "print machine-readable JSON");
1009
+ baseCommon(base
1010
+ .command("add <domain>")
1011
+ .description("Claim a company deploy domain; prints the price and the DNS records")
1012
+ .option("--yes", "add without a prompt (the domain can add a monthly charge)")).action((domain, options) => run(options.json, () => baseAddAction(domain, options)));
1013
+ baseCommon(base
1014
+ .command("status")
1015
+ .description(`Check the company deploy domain and advance it toward live (exits ${REFRESH_NOT_SERVING_EXIT_CODE} while not live)`)
1016
+ .option("--wait", "keep checking until it is live or the timeout passes")
1017
+ .option("--timeout <minutes>", `stop waiting after this many minutes (default ${BASE_WAIT_DEFAULT_MINUTES}, at most ${BASE_WAIT_MAX_MINUTES})`)).action((options) => run(options.json, () => baseStatusAction(options)));
1018
+ baseCommon(base
1019
+ .command("remove")
1020
+ .description("Release the company deploy domain")
1021
+ .option("--yes", "remove without a prompt")).action((options) => run(options.json, () => baseRemoveAction(options)));
785
1022
  domains
786
1023
  .command("search <query>")
787
1024
  .description("Check whether a domain can be bought, and its price")
@@ -56,6 +56,7 @@ export interface FleetLocalInstallDeps {
56
56
  fleetHome?: string;
57
57
  fetch?: typeof fetch;
58
58
  token?: () => string;
59
+ hqReleaseLinks?: (release: FleetLocalReleasePin) => Promise<Map<string, string> | null>;
59
60
  now?: () => Date;
60
61
  release?: FleetLocalReleasePin;
61
62
  publicKey?: string;
@@ -114,6 +115,7 @@ export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMC
114
115
  export declare function fleetHome(home?: string, env?: NodeJS.ProcessEnv): string;
115
116
  export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string): void;
116
117
  export declare function resolveFleetReleaseToken(env?: NodeJS.ProcessEnv): string;
118
+ export declare function fetchHqFleetReleaseLinks(release: FleetLocalReleasePin): Promise<Map<string, string> | null>;
117
119
  export declare const FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS = 30000;
118
120
  export declare const FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS: number;
119
121
  export declare const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS: number;
@@ -5,16 +5,18 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
7
7
  import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
8
+ import { ensureCognitoToken } from "../../utils/cognito-session.js";
9
+ import { vaultApiFetch } from "../../utils/vault-api.js";
8
10
  export const HQ_FLEET_LOCAL_RELEASE = {
9
11
  repository: "indigoai-us/hq-fleet-local",
10
- tag: "v0.4.5",
11
- version: "0.4.5",
12
- artifact: "hq-fleet-local-0.4.5.zip",
13
- artifactSha256: "5dc05b1a9c8a3fce0d748c1f642a908c4166958cf4e1c3cec8ee3799148eef80",
12
+ tag: "v0.4.6",
13
+ version: "0.4.6",
14
+ artifact: "hq-fleet-local-0.4.6.zip",
15
+ artifactSha256: "24e48887dd4450ad63eda11e61f78363b0cc53b3fc87e3fd21bfe97e4a430865",
14
16
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
15
17
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
16
- manifestSha256: "ffb81d7cbb61cdd05e91208ac3eccf045789d9e068309da3164fe002991f7780",
17
- signatureSha256: "3792d43c191c65c731e9f27a003d86334b4695a43feb8ac23a7b679f79531616",
18
+ manifestSha256: "5cc06b30a724419c22f4ab116db1ae745158d33c4aa46c7be04d29639172af1d",
19
+ signatureSha256: "b7a6c771dc674423b56db36f58b5a2a2a49eb2e27b33a41206fcc228a11d1def",
18
20
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
19
21
  };
20
22
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
@@ -92,9 +94,69 @@ export function resolveFleetReleaseToken(env = process.env) {
92
94
  return value;
93
95
  }
94
96
  catch {
95
- throw expectedError("HQ Fleet Local is a private Indigo preview. Sign in with GitHub CLI or set HQ_FLEET_RELEASE_GITHUB_TOKEN before creating this bot.");
97
+ throw expectedError("HQ could not provide the HQ Fleet Runtime download. Sign in with `hq login` and retry.");
96
98
  }
97
- throw expectedError("GitHub CLI returned an empty token for the private HQ Fleet Local release. Run gh auth login and retry.");
99
+ throw expectedError("HQ could not provide the HQ Fleet Runtime download. Sign in with `hq login` and retry.");
100
+ }
101
+ export async function fetchHqFleetReleaseLinks(release) {
102
+ let token;
103
+ try {
104
+ token = await ensureCognitoToken({ tokenSource: "person", interactive: false });
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ const response = await vaultApiFetch({
110
+ token,
111
+ path: `/v1/fleet-local/releases/${encodeURIComponent(release.version)}`,
112
+ signal: AbortSignal.timeout(FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
113
+ });
114
+ if (response.status === 401 || response.status === 403) {
115
+ throw expectedError("HQ refused the HQ Fleet Runtime download. Sign in with `hq login` and retry.");
116
+ }
117
+ if (response.status === 404)
118
+ return null;
119
+ if (!response.ok) {
120
+ throw expectedError(`Could not get the HQ Fleet Local ${release.tag} download: HQ returned HTTP ${response.status}.`);
121
+ }
122
+ const body = (await response.json());
123
+ const links = new Map();
124
+ for (const asset of body.assets ?? []) {
125
+ if (typeof asset.name === "string" && typeof asset.url === "string")
126
+ links.set(asset.name, asset.url);
127
+ }
128
+ return links.size > 0 ? links : null;
129
+ }
130
+ function trustedReleaseLink(url) {
131
+ try {
132
+ const parsed = new URL(url);
133
+ return parsed.protocol === "https:" && parsed.hostname.endsWith(".amazonaws.com");
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ async function downloadFleetLocalReleaseFromHq(release, fetcher, links) {
140
+ const get = async (name, timeoutMs) => {
141
+ const url = links.get(name);
142
+ if (!url)
143
+ throw expectedError(`HQ Fleet Local ${release.tag} is missing release asset ${name}.`);
144
+ if (!trustedReleaseLink(url)) {
145
+ throw expectedError(`HQ Fleet Local ${release.tag} returned an untrusted download link for ${name}.`);
146
+ }
147
+ const response = await fetcher(url, { signal: AbortSignal.timeout(timeoutMs) });
148
+ if (!response.ok) {
149
+ throw expectedError(`Could not download ${name}: HQ storage returned HTTP ${response.status}.`);
150
+ }
151
+ return Buffer.from(await response.arrayBuffer());
152
+ };
153
+ const [archive, runtimeArchive, manifest, signature] = await Promise.all([
154
+ get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
155
+ get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
156
+ get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
157
+ get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
158
+ ]);
159
+ return { archive, runtimeArchive, manifest, signature };
98
160
  }
99
161
  async function responseBytes(response, label) {
100
162
  if (!response.ok) {
@@ -196,8 +258,11 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
196
258
  verifyFleetLocalRelease(current, release, publicKey);
197
259
  }
198
260
  else {
199
- const token = (deps.token ?? resolveFleetReleaseToken)();
200
- const files = await downloadFleetLocalRelease(release, deps.fetch ?? fetch, token);
261
+ const fetcher = deps.fetch ?? fetch;
262
+ const links = await (deps.hqReleaseLinks ?? fetchHqFleetReleaseLinks)(release);
263
+ const files = links
264
+ ? await downloadFleetLocalReleaseFromHq(release, fetcher, links)
265
+ : await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)());
201
266
  verifyFleetLocalRelease(files, release, publicKey);
202
267
  installFiles(dir, files, release, publicKey);
203
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.173",
3
+ "version": "5.345.174",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.173",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.173",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.173",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.173",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.173",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.173"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.174",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.174",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.174",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.174",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.174",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.174"
123
123
  }
124
124
  }