@indigoai-us/hq-cli 5.6.0 → 5.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,10 +55,12 @@ export interface ProvisionResult {
55
55
  manifest_patched: boolean;
56
56
  config_written: boolean;
57
57
  initial_sync: {
58
- ok: boolean;
58
+ ok?: boolean;
59
59
  files_uploaded?: number;
60
60
  bytes_uploaded?: number;
61
61
  error?: string;
62
+ /** True if the caller passed --skip-initial-sync; ok/files/bytes will be absent. */
63
+ skipped?: boolean;
62
64
  };
63
65
  }
64
66
  /** Options for the high-level `provisionCompany` orchestrator. */
@@ -68,6 +70,15 @@ export interface ProvisionCompanyOptions {
68
70
  ownerUid?: string;
69
71
  hqRoot: string;
70
72
  vaultApiUrl: string;
73
+ /**
74
+ * Skip the initial-sync step. The vault entity, manifest patch, and
75
+ * `.hq/config.json` write still happen; the post-provision `share()` call
76
+ * is no-op'd. Use this when the caller has its own upload pipeline (e.g.
77
+ * AppBar HQ Sync's `first_push_company` with STS-vended credentials and
78
+ * Tauri progress events) and would otherwise double-upload the same files.
79
+ * When true, `initial_sync` in the result is `{ skipped: true }`.
80
+ */
81
+ skipInitialSync?: boolean;
71
82
  /** Injected vault HTTP client (override for tests). */
72
83
  vaultClient?: VaultClient;
73
84
  /** Injected access-token resolver (override for tests). */
@@ -25,7 +25,7 @@
25
25
  * `initial_sync.ok=false`). Manifest + config may have been written.
26
26
  */
27
27
 
28
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="301e6769-89c3-53e0-9b81-42840d6d59f7")}catch(e){}}();
28
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="aacb5bf2-8362-5336-b864-43296c30a1d6")}catch(e){}}();
29
29
  import chalk from "chalk";
30
30
  import * as fs from "node:fs";
31
31
  import * as path from "node:path";
@@ -180,7 +180,7 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
180
180
  };
181
181
  return {
182
182
  async findCompanyBySlug(slug) {
183
- const url = `${apiUrl.replace(/\/$/, "")}/v1/entities/by-slug/company/${encodeURIComponent(slug)}`;
183
+ const url = `${apiUrl.replace(/\/$/, "")}/entity/by-slug/company/${encodeURIComponent(slug)}`;
184
184
  const res = await fetch(url, { method: "GET", headers });
185
185
  if (res.status === 404)
186
186
  return null;
@@ -195,7 +195,7 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
195
195
  return data.entity;
196
196
  },
197
197
  async createCompanyEntity(input) {
198
- const url = `${apiUrl.replace(/\/$/, "")}/v1/entities`;
198
+ const url = `${apiUrl.replace(/\/$/, "")}/entity`;
199
199
  const body = {
200
200
  type: "company",
201
201
  slug: input.slug,
@@ -213,11 +213,11 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
213
213
  // 409 means a concurrent client created it between our GET and POST —
214
214
  // surface it as a vault error. The orchestrator is responsible for
215
215
  // retrying GET if it wants idempotency on collisions.
216
- throw new ProvisionError(1, `Vault POST /v1/entities failed: ${res.status} ${res.statusText} — ${text}`);
216
+ throw new ProvisionError(1, `Vault POST /entity failed: ${res.status} ${res.statusText} — ${text}`);
217
217
  }
218
218
  const data = (await res.json());
219
219
  if (!data.entity) {
220
- throw new ProvisionError(1, `Vault POST /v1/entities returned ${res.status} with no entity body`);
220
+ throw new ProvisionError(1, `Vault POST /entity returned ${res.status} with no entity body`);
221
221
  }
222
222
  return data.entity;
223
223
  },
@@ -317,39 +317,47 @@ export async function provisionCompany(options) {
317
317
  vaultApiUrl: options.vaultApiUrl,
318
318
  });
319
319
  log(`wrote companies/${options.slug}/.hq/config.json`);
320
- // Step 8: trigger initial sync (failure ⇒ exit 3 with cloud_uid populated)
321
- const runner = options.runInitialSync ?? defaultRunInitialSync;
320
+ // Step 8: trigger initial sync (failure ⇒ exit 3 with cloud_uid populated).
321
+ // Skipped when caller passed --skip-initial-sync (e.g. AppBar HQ Sync, which
322
+ // owns its own STS-credentialed upload pipeline + Tauri progress events).
322
323
  let initialSync;
323
- try {
324
- log(`triggering initial sync via share()`);
325
- const sync = await runner({
326
- slug: options.slug,
327
- hqRoot: options.hqRoot,
328
- accessToken,
329
- vaultApiUrl: options.vaultApiUrl,
330
- });
331
- initialSync = {
332
- ok: true,
333
- files_uploaded: sync.filesUploaded,
334
- bytes_uploaded: sync.bytesUploaded,
335
- };
336
- log(`initial sync complete — files=${sync.filesUploaded} bytes=${sync.bytesUploaded}`);
324
+ if (options.skipInitialSync) {
325
+ log(`skipping initial sync (--skip-initial-sync)`);
326
+ initialSync = { skipped: true };
337
327
  }
338
- catch (err) {
339
- const msg = err instanceof Error ? err.message : String(err);
340
- log(`initial sync failed: ${msg}`);
341
- throw new ProvisionError(3, `Initial sync failed: ${msg}`, {
342
- ok: false,
343
- company_slug: options.slug,
344
- cloud_uid: cloudUid,
345
- bucket_name: bucketName,
346
- vault_api_url: options.vaultApiUrl,
347
- kms_key_id: kmsKeyId,
348
- created_entity: createdEntity,
349
- manifest_patched: true,
350
- config_written: true,
351
- initial_sync: { ok: false, error: msg },
352
- });
328
+ else {
329
+ const runner = options.runInitialSync ?? defaultRunInitialSync;
330
+ try {
331
+ log(`triggering initial sync via share()`);
332
+ const sync = await runner({
333
+ slug: options.slug,
334
+ hqRoot: options.hqRoot,
335
+ accessToken,
336
+ vaultApiUrl: options.vaultApiUrl,
337
+ });
338
+ initialSync = {
339
+ ok: true,
340
+ files_uploaded: sync.filesUploaded,
341
+ bytes_uploaded: sync.bytesUploaded,
342
+ };
343
+ log(`initial sync complete — files=${sync.filesUploaded} bytes=${sync.bytesUploaded}`);
344
+ }
345
+ catch (err) {
346
+ const msg = err instanceof Error ? err.message : String(err);
347
+ log(`initial sync failed: ${msg}`);
348
+ throw new ProvisionError(3, `Initial sync failed: ${msg}`, {
349
+ ok: false,
350
+ company_slug: options.slug,
351
+ cloud_uid: cloudUid,
352
+ bucket_name: bucketName,
353
+ vault_api_url: options.vaultApiUrl,
354
+ kms_key_id: kmsKeyId,
355
+ created_entity: createdEntity,
356
+ manifest_patched: true,
357
+ config_written: true,
358
+ initial_sync: { ok: false, error: msg },
359
+ });
360
+ }
353
361
  }
354
362
  return {
355
363
  ok: true,
@@ -385,6 +393,9 @@ export function registerCloudProvisionCommands(program) {
385
393
  .option("--owner <uid>", "Owner person UID (default: current Cognito user sub)")
386
394
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
387
395
  .option("--vault-api-url <url>", `Vault API URL (default: ${DEFAULT_VAULT_API_URL})`, DEFAULT_VAULT_API_URL)
396
+ .option("--skip-initial-sync", "Skip the post-provision share() initial sync. Use when the caller " +
397
+ "(e.g. AppBar HQ Sync) has its own upload pipeline. Result includes " +
398
+ "{ initial_sync: { skipped: true } } when set.")
388
399
  .action(async (slug, options) => {
389
400
  try {
390
401
  const result = await provisionCompany({
@@ -393,6 +404,7 @@ export function registerCloudProvisionCommands(program) {
393
404
  ownerUid: options.owner,
394
405
  hqRoot: options.hqRoot,
395
406
  vaultApiUrl: options.vaultApiUrl,
407
+ skipInitialSync: options.skipInitialSync,
396
408
  });
397
409
  // Final stdout line — single JSON document for downstream consumers
398
410
  process.stdout.write(JSON.stringify(result) + "\n");
@@ -414,4 +426,4 @@ export function registerCloudProvisionCommands(program) {
414
426
  });
415
427
  }
416
428
  //# sourceMappingURL=cloud-provision.js.map
417
- //# debugId=301e6769-89c3-53e0-9b81-42840d6d59f7
429
+ //# debugId=aacb5bf2-8362-5336-b864-43296c30a1d6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.6.0",
3
+ "version": "5.6.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -374,7 +374,7 @@ describe("createDefaultVaultClient", () => {
374
374
  expect(out).toEqual(entity);
375
375
  // Verify request shape
376
376
  const call = fetchSpy.mock.calls[0];
377
- expect(call[0]).toBe(`${apiUrl}/v1/entities`);
377
+ expect(call[0]).toBe(`${apiUrl}/entity`);
378
378
  expect((call[1] as RequestInit)?.method).toBe("POST");
379
379
  const body = JSON.parse(((call[1] as RequestInit)?.body as string) ?? "{}");
380
380
  expect(body).toEqual({ type: "company", slug: "indigo", name: "Indigo" });
@@ -649,4 +649,75 @@ describe("provisionCompany", () => {
649
649
  ownerUid: undefined,
650
650
  });
651
651
  });
652
+
653
+ it("skipInitialSync=true — runner is NOT called; result has initial_sync.skipped", async () => {
654
+ setupValid();
655
+ const entity: VaultEntity = {
656
+ uid: "cmp_01H",
657
+ type: "company",
658
+ slug: "indigo",
659
+ name: "Indigo",
660
+ bucketName: "hq-vault-cmp-01H",
661
+ };
662
+ const vaultClient = makeVaultClient({
663
+ findCompanyBySlug: vi.fn().mockResolvedValue(null),
664
+ createCompanyEntity: vi.fn().mockResolvedValue(entity),
665
+ });
666
+ const runInitialSync = vi.fn();
667
+
668
+ const result = await provisionCompany({
669
+ slug: "indigo",
670
+ hqRoot: tmpRoot,
671
+ vaultApiUrl,
672
+ vaultClient,
673
+ resolveAccessToken: async () => accessToken,
674
+ runInitialSync,
675
+ skipInitialSync: true,
676
+ log: () => {},
677
+ });
678
+
679
+ expect(runInitialSync).not.toHaveBeenCalled();
680
+ expect(result.initial_sync).toEqual({ skipped: true });
681
+ expect(result.ok).toBe(true);
682
+ expect(result.cloud_uid).toBe("cmp_01H");
683
+ expect(result.manifest_patched).toBe(true);
684
+ expect(result.config_written).toBe(true);
685
+ });
686
+
687
+ it("skipInitialSync=true — manifest + .hq/config.json are STILL written", async () => {
688
+ setupValid();
689
+ const entity: VaultEntity = {
690
+ uid: "cmp_SKIP",
691
+ type: "company",
692
+ slug: "indigo",
693
+ name: "Indigo",
694
+ bucketName: "hq-vault-cmp-SKIP",
695
+ };
696
+ const vaultClient = makeVaultClient({
697
+ findCompanyBySlug: vi.fn().mockResolvedValue(null),
698
+ createCompanyEntity: vi.fn().mockResolvedValue(entity),
699
+ });
700
+ await provisionCompany({
701
+ slug: "indigo",
702
+ hqRoot: tmpRoot,
703
+ vaultApiUrl,
704
+ vaultClient,
705
+ resolveAccessToken: async () => accessToken,
706
+ runInitialSync: vi.fn(),
707
+ skipInitialSync: true,
708
+ log: () => {},
709
+ });
710
+ // Manifest patched on disk
711
+ const m = yaml.load(fs.readFileSync(manifestPath(tmpRoot), "utf-8")) as {
712
+ companies: Record<string, Record<string, unknown>>;
713
+ };
714
+ expect(m.companies.indigo.cloud_uid).toBe("cmp_SKIP");
715
+ expect(m.companies.indigo.bucket_name).toBe("hq-vault-cmp-SKIP");
716
+ // .hq/config.json written
717
+ const c = JSON.parse(
718
+ fs.readFileSync(companyConfigPath(tmpRoot, "indigo"), "utf-8"),
719
+ );
720
+ expect(c.companyUid).toBe("cmp_SKIP");
721
+ expect(c.bucketName).toBe("hq-vault-cmp-SKIP");
722
+ });
652
723
  });
@@ -74,10 +74,12 @@ export interface ProvisionResult {
74
74
  manifest_patched: boolean;
75
75
  config_written: boolean;
76
76
  initial_sync: {
77
- ok: boolean;
77
+ ok?: boolean;
78
78
  files_uploaded?: number;
79
79
  bytes_uploaded?: number;
80
80
  error?: string;
81
+ /** True if the caller passed --skip-initial-sync; ok/files/bytes will be absent. */
82
+ skipped?: boolean;
81
83
  };
82
84
  }
83
85
 
@@ -88,6 +90,15 @@ export interface ProvisionCompanyOptions {
88
90
  ownerUid?: string;
89
91
  hqRoot: string;
90
92
  vaultApiUrl: string;
93
+ /**
94
+ * Skip the initial-sync step. The vault entity, manifest patch, and
95
+ * `.hq/config.json` write still happen; the post-provision `share()` call
96
+ * is no-op'd. Use this when the caller has its own upload pipeline (e.g.
97
+ * AppBar HQ Sync's `first_push_company` with STS-vended credentials and
98
+ * Tauri progress events) and would otherwise double-upload the same files.
99
+ * When true, `initial_sync` in the result is `{ skipped: true }`.
100
+ */
101
+ skipInitialSync?: boolean;
91
102
  /** Injected vault HTTP client (override for tests). */
92
103
  vaultClient?: VaultClient;
93
104
  /** Injected access-token resolver (override for tests). */
@@ -335,7 +346,7 @@ export function createDefaultVaultClient(
335
346
  };
336
347
  return {
337
348
  async findCompanyBySlug(slug: string): Promise<VaultEntity | null> {
338
- const url = `${apiUrl.replace(/\/$/, "")}/v1/entities/by-slug/company/${encodeURIComponent(
349
+ const url = `${apiUrl.replace(/\/$/, "")}/entity/by-slug/company/${encodeURIComponent(
339
350
  slug,
340
351
  )}`;
341
352
  const res = await fetch(url, { method: "GET", headers });
@@ -361,7 +372,7 @@ export function createDefaultVaultClient(
361
372
  name: string;
362
373
  ownerUid?: string;
363
374
  }): Promise<VaultEntity> {
364
- const url = `${apiUrl.replace(/\/$/, "")}/v1/entities`;
375
+ const url = `${apiUrl.replace(/\/$/, "")}/entity`;
365
376
  const body: Record<string, unknown> = {
366
377
  type: "company",
367
378
  slug: input.slug,
@@ -380,14 +391,14 @@ export function createDefaultVaultClient(
380
391
  // retrying GET if it wants idempotency on collisions.
381
392
  throw new ProvisionError(
382
393
  1,
383
- `Vault POST /v1/entities failed: ${res.status} ${res.statusText} — ${text}`,
394
+ `Vault POST /entity failed: ${res.status} ${res.statusText} — ${text}`,
384
395
  );
385
396
  }
386
397
  const data = (await res.json()) as { entity?: VaultEntity };
387
398
  if (!data.entity) {
388
399
  throw new ProvisionError(
389
400
  1,
390
- `Vault POST /v1/entities returned ${res.status} with no entity body`,
401
+ `Vault POST /entity returned ${res.status} with no entity body`,
391
402
  );
392
403
  }
393
404
  return data.entity;
@@ -510,40 +521,47 @@ export async function provisionCompany(
510
521
  });
511
522
  log(`wrote companies/${options.slug}/.hq/config.json`);
512
523
 
513
- // Step 8: trigger initial sync (failure ⇒ exit 3 with cloud_uid populated)
514
- const runner = options.runInitialSync ?? defaultRunInitialSync;
524
+ // Step 8: trigger initial sync (failure ⇒ exit 3 with cloud_uid populated).
525
+ // Skipped when caller passed --skip-initial-sync (e.g. AppBar HQ Sync, which
526
+ // owns its own STS-credentialed upload pipeline + Tauri progress events).
515
527
  let initialSync: ProvisionResult["initial_sync"];
516
- try {
517
- log(`triggering initial sync via share()`);
518
- const sync = await runner({
519
- slug: options.slug,
520
- hqRoot: options.hqRoot,
521
- accessToken,
522
- vaultApiUrl: options.vaultApiUrl,
523
- });
524
- initialSync = {
525
- ok: true,
526
- files_uploaded: sync.filesUploaded,
527
- bytes_uploaded: sync.bytesUploaded,
528
- };
529
- log(
530
- `initial sync complete — files=${sync.filesUploaded} bytes=${sync.bytesUploaded}`,
531
- );
532
- } catch (err) {
533
- const msg = err instanceof Error ? err.message : String(err);
534
- log(`initial sync failed: ${msg}`);
535
- throw new ProvisionError(3, `Initial sync failed: ${msg}`, {
536
- ok: false,
537
- company_slug: options.slug,
538
- cloud_uid: cloudUid,
539
- bucket_name: bucketName,
540
- vault_api_url: options.vaultApiUrl,
541
- kms_key_id: kmsKeyId,
542
- created_entity: createdEntity,
543
- manifest_patched: true,
544
- config_written: true,
545
- initial_sync: { ok: false, error: msg },
546
- });
528
+ if (options.skipInitialSync) {
529
+ log(`skipping initial sync (--skip-initial-sync)`);
530
+ initialSync = { skipped: true };
531
+ } else {
532
+ const runner = options.runInitialSync ?? defaultRunInitialSync;
533
+ try {
534
+ log(`triggering initial sync via share()`);
535
+ const sync = await runner({
536
+ slug: options.slug,
537
+ hqRoot: options.hqRoot,
538
+ accessToken,
539
+ vaultApiUrl: options.vaultApiUrl,
540
+ });
541
+ initialSync = {
542
+ ok: true,
543
+ files_uploaded: sync.filesUploaded,
544
+ bytes_uploaded: sync.bytesUploaded,
545
+ };
546
+ log(
547
+ `initial sync complete files=${sync.filesUploaded} bytes=${sync.bytesUploaded}`,
548
+ );
549
+ } catch (err) {
550
+ const msg = err instanceof Error ? err.message : String(err);
551
+ log(`initial sync failed: ${msg}`);
552
+ throw new ProvisionError(3, `Initial sync failed: ${msg}`, {
553
+ ok: false,
554
+ company_slug: options.slug,
555
+ cloud_uid: cloudUid,
556
+ bucket_name: bucketName,
557
+ vault_api_url: options.vaultApiUrl,
558
+ kms_key_id: kmsKeyId,
559
+ created_entity: createdEntity,
560
+ manifest_patched: true,
561
+ config_written: true,
562
+ initial_sync: { ok: false, error: msg },
563
+ });
564
+ }
547
565
  }
548
566
 
549
567
  return {
@@ -593,6 +611,12 @@ export function registerCloudProvisionCommands(program: Command): void {
593
611
  `Vault API URL (default: ${DEFAULT_VAULT_API_URL})`,
594
612
  DEFAULT_VAULT_API_URL,
595
613
  )
614
+ .option(
615
+ "--skip-initial-sync",
616
+ "Skip the post-provision share() initial sync. Use when the caller " +
617
+ "(e.g. AppBar HQ Sync) has its own upload pipeline. Result includes " +
618
+ "{ initial_sync: { skipped: true } } when set.",
619
+ )
596
620
  .action(
597
621
  async (
598
622
  slug: string,
@@ -601,6 +625,7 @@ export function registerCloudProvisionCommands(program: Command): void {
601
625
  owner?: string;
602
626
  hqRoot: string;
603
627
  vaultApiUrl: string;
628
+ skipInitialSync?: boolean;
604
629
  },
605
630
  ) => {
606
631
  try {
@@ -610,6 +635,7 @@ export function registerCloudProvisionCommands(program: Command): void {
610
635
  ownerUid: options.owner,
611
636
  hqRoot: options.hqRoot,
612
637
  vaultApiUrl: options.vaultApiUrl,
638
+ skipInitialSync: options.skipInitialSync,
613
639
  });
614
640
  // Final stdout line — single JSON document for downstream consumers
615
641
  process.stdout.write(JSON.stringify(result) + "\n");