@indigoai-us/hq-cli 5.150.0 → 5.151.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.151.0] — 2026-09-23
6
+
7
+ - `hq report publish` no longer fails when another app it cannot see holds the report's address. It publishes at the next free address (`<slug>-2`, then `-3`) and saves that address on the report.
8
+
5
9
  ## [5.150.0] — 2026-09-23
6
10
 
7
11
  ### Fixed
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch } from "../lib/report-publish.js";
2
+ import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch, ReportPublishError } from "../lib/report-publish.js";
3
3
  type ChannelRow = {
4
4
  channelId?: string;
5
5
  scope?: string;
@@ -17,7 +17,21 @@ export declare function withChannelCompany(report: ChannelReport, channels: Chan
17
17
  /** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
18
18
  export declare function resolveReport(token: string, reportId: string, requestedChannelId?: string): Promise<ChannelReport>;
19
19
  export declare function patchReport(token: string, report: ChannelReport, patch: ReportPatchInput): Promise<ChannelReport>;
20
+ /** hq-deploy refused the subdomain: another app, not visible to this caller, holds it. */
21
+ export declare class SubdomainTakenError extends ReportPublishError {
22
+ constructor();
23
+ }
24
+ /**
25
+ * Subdomains to try, in order. The first is the report's own slug; the rest
26
+ * are used only when an app the caller cannot see already holds it (for
27
+ * example, one left by an earlier publish under a different account).
28
+ */
29
+ export declare function candidateSlugs(slug: string, attempts?: number): string[];
20
30
  export declare function resolveCompanySlug(token: string, companyUid: string): Promise<string>;
31
+ export declare function findOrCreateApp(token: string, slug: string, org: ReturnType<typeof deployOrgContext>): Promise<{
32
+ id: string;
33
+ subdomain: string;
34
+ }>;
21
35
  export declare function deployReportStatic(token: string, input: {
22
36
  slug: string;
23
37
  files: Parameters<typeof buildReportTarball>[0];
@@ -232,10 +232,28 @@ async function deployJson(token, route, method, body, extraHeaders = {}) {
232
232
  catch {
233
233
  throw new ReportPublishError("Could not reach hq-deploy.");
234
234
  }
235
- if (!res.ok)
235
+ if (!res.ok) {
236
+ const failure = await res.json().catch(() => ({}));
237
+ if (failure?.error?.code === "SUBDOMAIN_DUPLICATE")
238
+ throw new SubdomainTakenError();
236
239
  throw new ReportPublishError("hq-deploy rejected the report deployment.");
240
+ }
237
241
  return await res.json();
238
242
  }
243
+ /** hq-deploy refused the subdomain: another app, not visible to this caller, holds it. */
244
+ export class SubdomainTakenError extends ReportPublishError {
245
+ constructor() {
246
+ super("hq-deploy rejected the report deployment.");
247
+ }
248
+ }
249
+ /**
250
+ * Subdomains to try, in order. The first is the report's own slug; the rest
251
+ * are used only when an app the caller cannot see already holds it (for
252
+ * example, one left by an earlier publish under a different account).
253
+ */
254
+ export function candidateSlugs(slug, attempts = 3) {
255
+ return [slug, ...Array.from({ length: attempts }, (_, i) => `${slug}-${i + 2}`)];
256
+ }
239
257
  export async function resolveCompanySlug(token, companyUid) {
240
258
  const res = await vaultApiFetch({ token, path: `/entity/${encodeURIComponent(companyUid)}` });
241
259
  if (!res.ok)
@@ -250,12 +268,29 @@ async function orgFor(token, report) {
250
268
  const slug = report.companyUid ? await resolveCompanySlug(token, report.companyUid) : undefined;
251
269
  return deployOrgContext(report, slug);
252
270
  }
253
- async function findOrCreateApp(token, slug, org) {
271
+ export async function findOrCreateApp(token, slug, org) {
254
272
  const listed = await deployJson(token, "/api/apps", "GET", undefined, org.headers);
255
- const existing = Array.isArray(listed.apps)
256
- ? listed.apps.find((app) => !!app && typeof app === "object" && (app.subdomain === slug || app.name === slug))
257
- : undefined;
258
- const app = existing ?? await deployJson(token, "/api/apps", "POST", { name: slug, type: "static" }, org.headers);
273
+ const apps = Array.isArray(listed.apps) ? listed.apps : [];
274
+ const mine = (name) => apps.find((app) => !!app && typeof app === "object" && (app.subdomain === name || app.name === name));
275
+ let app;
276
+ for (const name of candidateSlugs(slug)) {
277
+ const existing = mine(name);
278
+ if (existing) {
279
+ app = existing;
280
+ break;
281
+ }
282
+ try {
283
+ app = await deployJson(token, "/api/apps", "POST", { name, type: "static" }, org.headers);
284
+ break;
285
+ }
286
+ catch (error) {
287
+ if (error instanceof SubdomainTakenError)
288
+ continue;
289
+ throw error;
290
+ }
291
+ }
292
+ if (!app)
293
+ throw new ReportPublishError("Every address for this report is taken on hq-deploy.");
259
294
  const id = typeof app.id === "string" ? app.id : undefined;
260
295
  const subdomain = typeof app.subdomain === "string" ? app.subdomain : undefined;
261
296
  if (!id || !subdomain)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.150.0",
3
+ "version": "5.151.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {