@indigoai-us/hq-cli 5.28.0 → 5.30.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.
@@ -17,25 +17,30 @@
17
17
  * `hq sync` owns, and writing a peeked object there would
18
18
  * silently re-import it into the sync envelope.
19
19
  *
20
- * Both subcommands vend via the new `purpose: 'browse'` path
21
- * (`VaultClient.vend`) shipped in hq-cloud US-009. The server treats that
22
- * purpose as the role-bypass-allowed surface sync vends NEVER widen, so
23
- * keeping browse on its own vend call is the acceptance-criteria-1
24
- * separation we need.
20
+ * Both subcommands vend through the multi-tenant STS routes
21
+ * `VaultClient.sts.vend` (`/sts/vend`, company) and `.sts.vendSelf`
22
+ * (`/sts/vend-self`, personal). These resolve the caller's per-entity bucket
23
+ * and apply role/ACL scoping server-side (owner/admin full access; member/
24
+ * guest per-prefix). The legacy `POST /vend` is deliberately NOT used: it
25
+ * assumes a single static `BUCKET_ARN` that is unset in multi-tenant prod, so
26
+ * it builds an invalid policy and STS rejects it (`MalformedPolicyDocument`).
25
27
  *
26
- * Cross-package note: depends on `VendInput`/`VendResult` + the
27
- * `VaultClient.vend` method from hq-cloud US-009 (commit 2f790c5).
28
- * hq-cli pins `@indigoai-us/hq-cloud` to `file:../hq-cloud` via
29
- * `pnpm.overrides` until that release ships to npm.
28
+ * Namespace note: company vault keys are company-relative (no
29
+ * `companies/<slug>/` prefix). The CLI speaks the anchored form for user
30
+ * familiarity and translates at the S3 boundary via `toBucketRelative` /
31
+ * `toCompanyAnchored`.
32
+ *
33
+ * Cross-package note: depends on the `VaultClient.sts.vend`/`.vendSelf`
34
+ * methods and `grantPathToPrefix` from hq-cloud.
30
35
  */
31
36
 
32
- !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]="102456e4-666a-5484-b432-7f0b73275818")}catch(e){}}();
37
+ !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]="73529786-7369-5356-a6a8-4d55bd553147")}catch(e){}}();
33
38
  import chalk from "chalk";
34
39
  import * as fs from "node:fs";
35
40
  import * as path from "node:path";
36
41
  import { pipeline } from "node:stream/promises";
37
42
  import { S3Client, ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
38
- import { VaultClient, } from "@indigoai-us/hq-cloud";
43
+ import { VaultClient, grantPathToPrefix, } from "@indigoai-us/hq-cloud";
39
44
  import { DEFAULT_HQ_ROOT, DEFAULT_COGNITO, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
40
45
  import { getCompanyUid } from "../utils/vault-api.js";
41
46
  import { resolveCanonicalPersonUid } from "./cloud.js";
@@ -54,17 +59,43 @@ export function parseCompanySlugFromPath(prefix) {
54
59
  return parts[1];
55
60
  }
56
61
  /**
57
- * Classify a single S3 key against the caller's explicit-grant list. Any
58
- * grant whose `path` is a prefix of the key contributes `shared-with-you`;
59
- * otherwise the key is only visible via role-bypass on the vend call.
62
+ * Translate a CLI `companies/<slug>/…` path into the company-relative S3 key
63
+ * the vault bucket actually stores. Company vault buckets are already
64
+ * company-scoped, so their keys carry NO `companies/<slug>/` prefix (e.g.
65
+ * `knowledge/foo.md`, not `companies/indigo/knowledge/foo.md`). The CLI speaks
66
+ * the anchored form for user familiarity; we strip the anchor at the S3
67
+ * boundary. A path without the anchor (or personal-mode, bucket-relative
68
+ * paths) passes through unchanged.
69
+ */
70
+ export function toBucketRelative(pathOrPrefix, slug) {
71
+ const anchor = `companies/${slug}/`;
72
+ const normalized = pathOrPrefix.replace(/^\/+/, "");
73
+ return normalized.startsWith(anchor)
74
+ ? normalized.slice(anchor.length)
75
+ : normalized;
76
+ }
77
+ /**
78
+ * Re-attach the `companies/<slug>/` anchor to a company-relative bucket key
79
+ * for display + `hq files cat` round-trip, so the CLI surface keeps speaking
80
+ * the anchored form the user passed in.
81
+ */
82
+ export function toCompanyAnchored(bucketRelKey, slug) {
83
+ return `companies/${slug}/${bucketRelKey}`;
84
+ }
85
+ /**
86
+ * Classify a single company-relative S3 key against the caller's
87
+ * (already-normalized) explicit-grant prefixes. Any prefix that covers the
88
+ * key contributes `shared-with-you`; otherwise the key is visible only via
89
+ * the owner/admin role-bypass the `/sts/vend` policy applied. An empty-string
90
+ * prefix is a company-wide grant and matches everything.
60
91
  *
61
- * Grant paths and S3 keys live in the same canonical form ("companies/<slug>/…");
62
- * `coalescePrefixes` would shrink the list further but isn't required for
63
- * correctness`startsWith` already short-circuits on the first match.
92
+ * Grant `path`s arrive in inconsistent/glob form; the caller normalizes them
93
+ * to company-relative `startsWith` prefixes via `grantPathToPrefix` before
94
+ * calling this keeping this helper pure and trivially testable.
64
95
  */
65
- export function classifyAclSource(key, grants) {
66
- for (const g of grants) {
67
- if (g.path && key.startsWith(g.path))
96
+ export function classifyAclSource(bucketRelKey, grantPrefixes) {
97
+ for (const p of grantPrefixes) {
98
+ if (p === "" || bucketRelKey.startsWith(p))
68
99
  return "shared-with-you";
69
100
  }
70
101
  return "role-bypass";
@@ -116,8 +147,10 @@ export function formatBrowseTable(rows) {
116
147
  *
117
148
  * 1. Parse slug from prefix (or use override).
118
149
  * 2. Resolve companyUid + bucketName via VaultClient.entity.
119
- * 3. Vend with `purpose: 'browse'`, `operations: 'read-only'`, paths: [prefix].
120
- * 4. Construct S3Client from vended creds, paginate ListObjectsV2.
150
+ * 3. Vend read creds via the multi-tenant STS route (`/sts/vend` company,
151
+ * `/sts/vend-self` personal) resolves the per-entity bucket + role/ACL.
152
+ * 4. Construct S3Client from vended creds, paginate ListObjectsV2 over the
153
+ * company-relative key space.
121
154
  * 5. Fetch explicit grants once, classify each key.
122
155
  *
123
156
  * Pure-ish: no console output, no process.exit — caller renders + exits.
@@ -127,10 +160,11 @@ export async function runBrowse(input) {
127
160
  // Branch by mode. Company mode parses slug from path and looks up by
128
161
  // namespace; personal mode resolves the entity directly by the supplied
129
162
  // person UID and skips the slug + grants machinery (a person bucket has
130
- // no grants graph — the owner is the only principal). The vend call is
131
- // identical for both modes once we have the entity in hand.
163
+ // no grants graph — the owner is the only principal).
132
164
  let bucket;
133
165
  let entityUid;
166
+ let slug;
167
+ let vend;
134
168
  if (personalMode) {
135
169
  if (!input.personalUid) {
136
170
  throw new Error("runBrowse: personalMode requires personalUid. Resolve via " +
@@ -142,9 +176,10 @@ export async function runBrowse(input) {
142
176
  }
143
177
  entityUid = entity.uid;
144
178
  bucket = entity.bucketName;
179
+ vend = await vaultClient.sts.vendSelf({ personUid: entityUid });
145
180
  }
146
181
  else {
147
- const slug = input.companySlug ?? parseCompanySlugFromPath(pathPrefix);
182
+ slug = input.companySlug ?? parseCompanySlugFromPath(pathPrefix);
148
183
  const entity = await vaultClient.entity.findInMyNamespace("company", slug);
149
184
  if (!entity) {
150
185
  throw new Error(`No company found for slug '${slug}' in your namespace. Confirm you have an active membership.`);
@@ -154,17 +189,11 @@ export async function runBrowse(input) {
154
189
  }
155
190
  entityUid = entity.uid;
156
191
  bucket = entity.bucketName;
192
+ // Multi-tenant vend: the server resolves this company's bucket + applies
193
+ // owner/admin role-bypass (full access) or member/guest ACL scoping. The
194
+ // legacy `POST /vend` is unused here — see FilesBrowseVaultClient docs.
195
+ vend = await vaultClient.sts.vend({ companyUid: entityUid });
157
196
  }
158
- // Distinct vend call from sync — `purpose: 'browse'` opts the request
159
- // into the role-bypass-allowed code path on the server (US-009). The
160
- // personal mode vends against the person entity which is owner-only by
161
- // construction; the vend response shape is identical so downstream
162
- // S3Client construction doesn't branch.
163
- const vend = await vaultClient.vend({
164
- paths: [pathPrefix],
165
- operations: "read-only",
166
- purpose: "browse",
167
- });
168
197
  const s3 = s3Factory({
169
198
  region,
170
199
  credentials: {
@@ -173,19 +202,26 @@ export async function runBrowse(input) {
173
202
  sessionToken: vend.credentials.sessionToken,
174
203
  },
175
204
  });
205
+ // Company vault keys are company-relative (no `companies/<slug>/` prefix), so
206
+ // translate the CLI's anchored prefix into the bucket-relative form before
207
+ // listing. Personal-mode paths are already bucket-relative.
208
+ const listPrefix = personalMode || slug === undefined
209
+ ? pathPrefix
210
+ : toBucketRelative(pathPrefix, slug);
176
211
  // Pull the caller's explicit-grant graph once so per-key classification
177
- // is O(grants) without N round-trips. Skipped in personal mode — the
178
- // grants graph is a company concept; a person bucket marks every row
179
- // as `"personal-vault"` directly.
180
- const grants = personalMode
212
+ // is O(grants) without N round-trips. Grant `path`s are normalized to
213
+ // company-relative `startsWith` prefixes (matching the bucket key space)
214
+ // via `grantPathToPrefix`. Skipped in personal mode — the grants graph is a
215
+ // company concept; a person bucket marks every row `"personal-vault"`.
216
+ const grantPrefixes = personalMode || slug === undefined
181
217
  ? []
182
- : await vaultClient.listMyExplicitGrants(entityUid);
218
+ : (await vaultClient.listMyExplicitGrants(entityUid)).map((g) => grantPathToPrefix(g.path, slug));
183
219
  const rows = [];
184
220
  let continuationToken;
185
221
  do {
186
222
  const resp = (await s3.send(new ListObjectsV2Command({
187
223
  Bucket: bucket,
188
- Prefix: pathPrefix,
224
+ Prefix: listPrefix,
189
225
  ContinuationToken: continuationToken,
190
226
  })));
191
227
  for (const obj of resp.Contents ?? []) {
@@ -194,11 +230,17 @@ export async function runBrowse(input) {
194
230
  // Skip S3 "directory marker" objects (0-byte, trailing slash).
195
231
  if (obj.Key.endsWith("/") && (obj.Size ?? 0) === 0)
196
232
  continue;
233
+ // `obj.Key` is company-relative. Classify in that space, then re-anchor
234
+ // for display so the CLI keeps speaking `companies/<slug>/...`.
197
235
  rows.push({
198
- key: obj.Key,
236
+ key: personalMode || slug === undefined
237
+ ? obj.Key
238
+ : toCompanyAnchored(obj.Key, slug),
199
239
  size: obj.Size ?? 0,
200
240
  lastModified: obj.LastModified,
201
- aclSource: personalMode ? "personal-vault" : classifyAclSource(obj.Key, grants),
241
+ aclSource: personalMode
242
+ ? "personal-vault"
243
+ : classifyAclSource(obj.Key, grantPrefixes),
202
244
  });
203
245
  }
204
246
  continuationToken = resp.NextContinuationToken ?? undefined;
@@ -221,6 +263,8 @@ export async function runCat(input) {
221
263
  // Same branch logic as runBrowse — see that function's doc-block for
222
264
  // the personal-vs-company rationale.
223
265
  let bucket;
266
+ let s3Key;
267
+ let vend;
224
268
  if (personalMode) {
225
269
  if (!input.personalUid) {
226
270
  throw new Error("runCat: personalMode requires personalUid. Resolve via " +
@@ -231,6 +275,8 @@ export async function runCat(input) {
231
275
  throw new Error(`Personal entity '${input.personalUid}' has no provisioned bucket.`);
232
276
  }
233
277
  bucket = entity.bucketName;
278
+ s3Key = key; // personal-mode keys are already bucket-relative
279
+ vend = await vaultClient.sts.vendSelf({ personUid: entity.uid });
234
280
  }
235
281
  else {
236
282
  const slug = input.companySlug ?? parseCompanySlugFromPath(key);
@@ -242,12 +288,10 @@ export async function runCat(input) {
242
288
  throw new Error(`Company '${slug}' (${entity.uid}) has no provisioned bucket.`);
243
289
  }
244
290
  bucket = entity.bucketName;
291
+ // Translate the anchored CLI key into the company-relative bucket key.
292
+ s3Key = toBucketRelative(key, slug);
293
+ vend = await vaultClient.sts.vend({ companyUid: entity.uid });
245
294
  }
246
- const vend = await vaultClient.vend({
247
- paths: [key],
248
- operations: "read-only",
249
- purpose: "browse",
250
- });
251
295
  const s3 = s3Factory({
252
296
  region,
253
297
  credentials: {
@@ -256,7 +300,7 @@ export async function runCat(input) {
256
300
  sessionToken: vend.credentials.sessionToken,
257
301
  },
258
302
  });
259
- const resp = (await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })));
303
+ const resp = (await s3.send(new GetObjectCommand({ Bucket: bucket, Key: s3Key })));
260
304
  if (!resp.Body) {
261
305
  throw new Error(`GetObject for '${key}' returned no body.`);
262
306
  }
@@ -356,6 +400,148 @@ export function formatSharedWithMeTable(rows) {
356
400
  ...data.map(renderRow),
357
401
  ].join("\n");
358
402
  }
403
+ /**
404
+ * `hq files search <query>` orchestrator. Lists the company (or personal)
405
+ * vault under its root via `runBrowse`, then filters to keys containing the
406
+ * query (case-insensitive). v1 is a name/path search over the listing — no
407
+ * content search. Rows carry the same ACL-source classification as browse.
408
+ *
409
+ * Pure-ish: no console output. The caller renders with `formatBrowseTable`.
410
+ */
411
+ export async function runSearch(input) {
412
+ const prefix = input.personalMode ? "" : `companies/${input.companySlug}/`;
413
+ const { rows } = await runBrowse({
414
+ pathPrefix: prefix,
415
+ companySlug: input.companySlug,
416
+ personalMode: input.personalMode,
417
+ personalUid: input.personalUid,
418
+ vaultClient: input.vaultClient,
419
+ s3Factory: input.s3Factory,
420
+ region: input.region,
421
+ });
422
+ const q = input.query.toLowerCase();
423
+ return rows.filter((r) => r.key.toLowerCase().includes(q));
424
+ }
425
+ /** Per-machine pin set path: `<hqRoot>/.hq/pins.json`. */
426
+ export function pinFilePath(hqRoot) {
427
+ return path.join(hqRoot, ".hq", "pins.json");
428
+ }
429
+ /** Read the pin set, tolerating a missing or corrupt file (→ fresh). */
430
+ export function readPins(hqRoot) {
431
+ try {
432
+ const parsed = JSON.parse(fs.readFileSync(pinFilePath(hqRoot), "utf-8"));
433
+ if (parsed && typeof parsed === "object" && parsed.pins) {
434
+ return { version: parsed.version ?? 1, pins: parsed.pins };
435
+ }
436
+ }
437
+ catch {
438
+ /* missing / unreadable / malformed → start fresh */
439
+ }
440
+ return { version: 1, pins: {} };
441
+ }
442
+ /**
443
+ * Register a company-relative prefix in the per-machine pin set. Pins are what
444
+ * keep an on-demand `hq files get` from being pruned by the next *scoped*
445
+ * sync (`syncMode: shared|custom`): the sync runner unions the company's pins
446
+ * into its pull scope. Idempotent + sorted for stable diffs.
447
+ */
448
+ export function addPin(hqRoot, companySlug, prefix) {
449
+ const pf = readPins(hqRoot);
450
+ const list = pf.pins[companySlug] ?? [];
451
+ if (!list.includes(prefix))
452
+ list.push(prefix);
453
+ list.sort();
454
+ pf.pins[companySlug] = list;
455
+ const f = pinFilePath(hqRoot);
456
+ fs.mkdirSync(path.dirname(f), { recursive: true });
457
+ fs.writeFileSync(f, JSON.stringify(pf, null, 2) + "\n");
458
+ }
459
+ /**
460
+ * `hq files get <path>` orchestrator. Materializes a vault file or prefix into
461
+ * the local HQ tree on demand. Unlike `cat` (which refuses to write under
462
+ * `companies/`), `get` deliberately writes INTO `companies/<slug>/...` by
463
+ * default — that's the point: pull a path you have access to but don't sync.
464
+ * It then registers a pin so the next scoped sync keeps it.
465
+ *
466
+ * Company mode only in v1 — materializing a personal vault would target the
467
+ * HQ root itself, which is too broad to do implicitly.
468
+ */
469
+ export async function runGet(input) {
470
+ const { path: vaultPath, vaultClient, s3Factory, region, hqRoot } = input;
471
+ const slug = input.companySlug ?? parseCompanySlugFromPath(vaultPath);
472
+ const entity = await vaultClient.entity.findInMyNamespace("company", slug);
473
+ if (!entity) {
474
+ throw new Error(`No company found for slug '${slug}' in your namespace. Confirm you have an active membership.`);
475
+ }
476
+ if (!entity.bucketName) {
477
+ throw new Error(`Company '${slug}' (${entity.uid}) has no provisioned bucket.`);
478
+ }
479
+ const bucket = entity.bucketName;
480
+ const vend = await vaultClient.sts.vend({ companyUid: entity.uid });
481
+ const s3 = s3Factory({
482
+ region,
483
+ credentials: {
484
+ accessKeyId: vend.credentials.accessKeyId,
485
+ secretAccessKey: vend.credentials.secretAccessKey,
486
+ sessionToken: vend.credentials.sessionToken,
487
+ },
488
+ });
489
+ // Company-relative prefix to list/fetch (bucket keys carry no anchor).
490
+ const bucketPrefix = toBucketRelative(vaultPath, slug);
491
+ const keys = [];
492
+ let continuationToken;
493
+ do {
494
+ const resp = (await s3.send(new ListObjectsV2Command({
495
+ Bucket: bucket,
496
+ Prefix: bucketPrefix,
497
+ ContinuationToken: continuationToken,
498
+ })));
499
+ for (const obj of resp.Contents ?? []) {
500
+ if (!obj.Key)
501
+ continue;
502
+ if (obj.Key.endsWith("/") && (obj.Size ?? 0) === 0)
503
+ continue;
504
+ keys.push(obj.Key);
505
+ }
506
+ continuationToken = resp.NextContinuationToken ?? undefined;
507
+ } while (continuationToken);
508
+ if (keys.length === 0) {
509
+ throw new Error(`No objects under '${vaultPath}'.`);
510
+ }
511
+ const destinations = [];
512
+ let bytesWritten = 0;
513
+ for (const key of keys) {
514
+ // Default: in-place under companies/<slug>/<company-relative key>.
515
+ // --into: write the path relative to the requested prefix under <into>.
516
+ let destAbs;
517
+ if (input.into !== undefined) {
518
+ const rel = key.startsWith(bucketPrefix)
519
+ ? key.slice(bucketPrefix.length)
520
+ : key;
521
+ destAbs = path.resolve(input.into, rel || path.basename(key));
522
+ }
523
+ else {
524
+ destAbs = path.join(hqRoot, "companies", slug, key);
525
+ }
526
+ const resp = (await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })));
527
+ if (!resp.Body) {
528
+ throw new Error(`GetObject for '${key}' returned no body.`);
529
+ }
530
+ const body = resp.Body;
531
+ fs.mkdirSync(path.dirname(destAbs), { recursive: true });
532
+ await pipeline(body, fs.createWriteStream(destAbs));
533
+ bytesWritten += fs.statSync(destAbs).size;
534
+ destinations.push(destAbs);
535
+ }
536
+ // Pin only the in-place case — `--into` writes outside the sync envelope, so
537
+ // there's nothing for a scoped sync to prune.
538
+ let pinned;
539
+ if (input.into === undefined) {
540
+ addPin(hqRoot, slug, bucketPrefix);
541
+ pinned = { companySlug: slug, prefix: bucketPrefix };
542
+ }
543
+ return { filesWritten: keys.length, bytesWritten, destinations, pinned };
544
+ }
359
545
  // ── CLI registration ────────────────────────────────────────────────────────
360
546
  const defaultS3Factory = ({ region, credentials }) => new S3Client({ region, credentials });
361
547
  /**
@@ -547,6 +733,98 @@ export function registerFilesBrowseCommands(filesCmd) {
547
733
  process.exit(1);
548
734
  }
549
735
  });
736
+ filesCmd
737
+ .command("search <query>")
738
+ .description("Search vault object keys (case-insensitive path/name match) under a company without downloading. Requires --company (or --personal). Content search is not supported in v1.")
739
+ .option("--company <slug>", "Company slug to search.")
740
+ .option("--personal", "Search the caller's canonical personal vault. Mutually exclusive with --company.")
741
+ .action(async (query, options) => {
742
+ try {
743
+ if (options.personal && options.company) {
744
+ throw new Error("--personal and --company are mutually exclusive. Pick one.");
745
+ }
746
+ const accessToken = await ensureCognitoToken();
747
+ const client = new VaultClient(buildVaultConfig(accessToken));
748
+ if (options.personal) {
749
+ const personalUid = await resolveCanonicalPersonUid({
750
+ listMyMemberships: () => client.listMyMemberships(),
751
+ listPersonEntities: () => client.entity.listByType("person"),
752
+ getEntity: async () => null,
753
+ });
754
+ const rows = await runSearch({
755
+ query,
756
+ companySlug: "personal",
757
+ personalMode: true,
758
+ personalUid,
759
+ vaultClient: client,
760
+ s3Factory: defaultS3Factory,
761
+ region: DEFAULT_COGNITO.region,
762
+ });
763
+ console.log(formatBrowseTable(rows));
764
+ return;
765
+ }
766
+ if (!options.company) {
767
+ throw new Error("search: --company <slug> is required (or --personal to search your personal vault).");
768
+ }
769
+ await getCompanyUid(accessToken, options.company);
770
+ const rows = await runSearch({
771
+ query,
772
+ companySlug: options.company,
773
+ vaultClient: client,
774
+ s3Factory: defaultS3Factory,
775
+ region: DEFAULT_COGNITO.region,
776
+ });
777
+ console.log(formatBrowseTable(rows));
778
+ }
779
+ catch (err) {
780
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
781
+ process.exit(1);
782
+ }
783
+ });
784
+ filesCmd
785
+ .command("get <path>")
786
+ .description("Download (materialize) a vault file or prefix into local HQ on demand. Default writes in place under <hqRoot>/companies/<slug>/<path> and registers a pin so a scoped sync (mode shared|custom) won't prune it. Use --into to write elsewhere (no pin). Company mode only.")
787
+ .option("--into <dir>", "Write into this directory instead of the in-place companies/<slug>/ location. No pin is registered.")
788
+ .option("--company <slug>", "Company slug (defaults to the slug parsed from <path>).")
789
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
790
+ .action(async (pathArg, options) => {
791
+ try {
792
+ const accessToken = await ensureCognitoToken();
793
+ const client = new VaultClient(buildVaultConfig(accessToken));
794
+ const slug = options.company ?? parseCompanySlugFromPath(pathArg);
795
+ if (options.company !== undefined) {
796
+ const fromPath = (() => {
797
+ try {
798
+ return parseCompanySlugFromPath(pathArg);
799
+ }
800
+ catch {
801
+ return undefined;
802
+ }
803
+ })();
804
+ if (fromPath && fromPath !== options.company) {
805
+ throw new Error(`--company '${options.company}' disagrees with path slug '${fromPath}'.`);
806
+ }
807
+ }
808
+ await getCompanyUid(accessToken, slug);
809
+ const result = await runGet({
810
+ path: pathArg,
811
+ into: options.into,
812
+ hqRoot: options.hqRoot,
813
+ companySlug: slug,
814
+ vaultClient: client,
815
+ s3Factory: defaultS3Factory,
816
+ region: DEFAULT_COGNITO.region,
817
+ });
818
+ console.error(chalk.green("✓"), `Materialized ${result.filesWritten} file(s), ${result.bytesWritten} bytes.`);
819
+ if (result.pinned) {
820
+ console.error(chalk.dim(`Pinned ${result.pinned.companySlug}:${result.pinned.prefix} — survives scoped sync.`));
821
+ }
822
+ }
823
+ catch (err) {
824
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
825
+ process.exit(1);
826
+ }
827
+ });
550
828
  }
551
829
  //# sourceMappingURL=files-browse.js.map
552
- //# debugId=102456e4-666a-5484-b432-7f0b73275818
830
+ //# debugId=73529786-7369-5356-a6a8-4d55bd553147
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.28.0",
3
+ "version": "5.30.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "clean": "rm -rf dist"
16
16
  },
17
17
  "dependencies": {
18
- "@indigoai-us/hq-cloud": "~5.42.0",
18
+ "@indigoai-us/hq-cloud": "~5.44.0",
19
19
  "@indigoai-us/hq-onboarding": "^0.1.0",
20
20
  "@sentry/node": "^10.49.0",
21
21
  "chalk": "^5.3.0",