@indigoai-us/hq-cli 5.17.0 → 5.18.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.
@@ -13,10 +13,12 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
  import { Command } from "commander";
16
- import { type ConflictStrategy } from "@indigoai-us/hq-cloud";
16
+ import { type ConflictStrategy, type MembershipSyncConfig } from "@indigoai-us/hq-cloud";
17
+ import { type BannerLevel } from "../lib/narrow-hint-banner.js";
17
18
  export interface PullAllVaultClient {
18
19
  listMyMemberships(): Promise<Array<{
19
20
  companyUid: string;
21
+ membershipKey?: string;
20
22
  }>>;
21
23
  listPersonEntities(): Promise<Array<{
22
24
  uid: string;
@@ -29,6 +31,13 @@ export interface PullAllVaultClient {
29
31
  slug?: string;
30
32
  name?: string;
31
33
  } | null>;
34
+ /**
35
+ * US-011: optional — when present, `pullAll` calls it once per
36
+ * membership to surface the narrow-hint banner for all-mode owners.
37
+ * Absent on legacy adapters (push-all et al.) where the banner is not
38
+ * applicable.
39
+ */
40
+ getMembershipSyncConfig?: (membershipId: string) => Promise<MembershipSyncConfig>;
32
41
  }
33
42
  export interface SyncCallOptions {
34
43
  company: string;
@@ -52,6 +61,22 @@ export interface PullAllDeps {
52
61
  export interface PullAllOptions {
53
62
  hqRoot: string;
54
63
  onConflict?: ConflictStrategy;
64
+ /**
65
+ * US-011: banner level for the narrow-hint nudge. Defaults to `'hint'`
66
+ * — see `resolveBannerLevel` for the env-driven override. The
67
+ * `'strict'` level causes `pullAll` to refuse to sync any membership
68
+ * still on `syncMode: 'all'` unless `modeAllOverride` is true.
69
+ *
70
+ * TODO(hq-core-staging release N+2): default flips to 'warning'.
71
+ * TODO(hq-core-staging release N+3): default flips to 'strict'.
72
+ */
73
+ narrowHintLevel?: BannerLevel;
74
+ /**
75
+ * US-011: when `true`, strict-mode does NOT refuse all-mode
76
+ * memberships — the operator has explicitly opted into keeping the
77
+ * legacy behavior for this run via `--mode-all`.
78
+ */
79
+ modeAllOverride?: boolean;
55
80
  }
56
81
  export interface PullAllRow {
57
82
  slug: string;
@@ -13,12 +13,13 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
 
16
- !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]="7bc55d66-7b02-5b45-9444-710bb6d8dfd6")}catch(e){}}();
16
+ !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]="2eb7da1d-e84f-591c-b930-b84c5c96986b")}catch(e){}}();
17
17
  import chalk from "chalk";
18
18
  import * as fs from "fs";
19
19
  import * as path from "path";
20
20
  import { share, sync, readJournal, getJournalPath, loadCachedTokens, VaultClient, computePersonalVaultPaths, } from "@indigoai-us/hq-cloud";
21
21
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
22
+ import { emitNarrowHint, isStrictRefusal, resolveBannerLevel, } from "../lib/narrow-hint-banner.js";
22
23
  // Oldest-first by createdAt, ties broken by uid lexicographic — matches
23
24
  // `pickCanonicalPersonEntity` in @indigoai-us/hq-cloud so the CLI lands on
24
25
  // the same person bucket that `hq-sync-runner` picks.
@@ -35,6 +36,8 @@ function pickCanonicalPerson(persons) {
35
36
  export async function pullAll(options, deps) {
36
37
  const memberships = await deps.vaultClient.listMyMemberships();
37
38
  const persons = await deps.vaultClient.listPersonEntities();
39
+ const narrowHintLevel = options.narrowHintLevel ?? "hint";
40
+ const getSyncConfig = deps.vaultClient.getMembershipSyncConfig;
38
41
  const plan = [];
39
42
  for (const m of memberships) {
40
43
  let slug = m.companyUid;
@@ -48,6 +51,8 @@ export async function pullAll(options, deps) {
48
51
  }
49
52
  plan.push({
50
53
  slug,
54
+ companyUid: m.companyUid,
55
+ ...(m.membershipKey ? { membershipKey: m.membershipKey } : {}),
51
56
  syncOptions: {
52
57
  company: m.companyUid,
53
58
  hqRoot: options.hqRoot,
@@ -78,12 +83,57 @@ export async function pullAll(options, deps) {
78
83
  };
79
84
  for (const entry of plan) {
80
85
  result.attempted += 1;
86
+ // US-011: resolve the membership's effective sync mode so we can
87
+ // either nudge an all-mode owner toward `hq sync narrow` OR refuse
88
+ // the leg outright when strict-mode is on and the operator didn't
89
+ // pass `--mode-all`. Sync-config lookup is best-effort — a 404 or
90
+ // network blip should never block the sync itself, so we fall back
91
+ // to syncMode='all' (the legacy default) and skip the banner.
92
+ let resolvedMode;
93
+ if (entry.membershipKey && getSyncConfig) {
94
+ try {
95
+ const cfg = await getSyncConfig(entry.membershipKey);
96
+ resolvedMode = cfg.syncMode;
97
+ }
98
+ catch {
99
+ resolvedMode = undefined;
100
+ }
101
+ }
102
+ if (resolvedMode === "all" &&
103
+ isStrictRefusal(resolvedMode, narrowHintLevel) &&
104
+ !options.modeAllOverride &&
105
+ entry.companyUid) {
106
+ // Emit the strict-level banner once, then mark the leg as errored
107
+ // without invoking sync(). The operator either narrows the
108
+ // membership (`hq sync narrow --apply`) or passes `--mode-all` to
109
+ // opt back in.
110
+ emitNarrowHint({
111
+ companyUid: entry.companyUid,
112
+ syncMode: resolvedMode,
113
+ level: narrowHintLevel,
114
+ });
115
+ const message = "Refusing to pull all-mode membership in strict mode. " +
116
+ "Run `hq sync narrow --apply` to migrate, or re-run with --mode-all.";
117
+ result.errors.push({ company: entry.slug, message });
118
+ result.perCompany.push({ slug: entry.slug, error: message });
119
+ continue;
120
+ }
81
121
  try {
82
122
  const r = await deps.sync(entry.syncOptions);
83
123
  result.filesDownloaded += r.filesDownloaded;
84
124
  result.bytesDownloaded += r.bytesDownloaded;
85
125
  result.conflicts += r.conflicts;
86
126
  result.perCompany.push({ slug: entry.slug, result: r });
127
+ // Banner emitted AFTER the leg succeeds so it appears alongside
128
+ // the per-company summary line and doesn't get scrolled off by
129
+ // sync chatter.
130
+ if (resolvedMode === "all" && entry.companyUid) {
131
+ emitNarrowHint({
132
+ companyUid: entry.companyUid,
133
+ syncMode: resolvedMode,
134
+ level: narrowHintLevel,
135
+ });
136
+ }
87
137
  }
88
138
  catch (err) {
89
139
  const message = err instanceof Error ? err.message : String(err);
@@ -407,6 +457,10 @@ export function registerCloudCommands(program) {
407
457
  "(no companies/<slug>/ prefix). Resolves the person UID automatically " +
408
458
  "from the cached Cognito session. Mutually exclusive with --company " +
409
459
  "and --all.")
460
+ .option("--mode-all", "US-011: opt out of the strict narrow-hint refusal for this run. " +
461
+ "Has no effect today (default narrow-hint level is 'hint'); " +
462
+ "wired so future hq-core-staging releases can flip the default to " +
463
+ "'strict' without re-touching this command.")
410
464
  .action(async (options) => {
411
465
  try {
412
466
  assertSingleSelector(options, "pull");
@@ -416,7 +470,7 @@ export function registerCloudCommands(program) {
416
470
  process.exit(1);
417
471
  }
418
472
  if (options.all) {
419
- await runPullAll(options.hqRoot, options.onConflict);
473
+ await runPullAll(options.hqRoot, options.onConflict, options.modeAll === true);
420
474
  return;
421
475
  }
422
476
  if (options.personal) {
@@ -502,14 +556,17 @@ export function registerCloudCommands(program) {
502
556
  "--personal.")
503
557
  .option("--personal", "Sync the caller's canonical personal vault bidirectionally. " +
504
558
  "Mutually exclusive with --company and --all.")
559
+ .option("--mode-all", "US-011: opt out of the strict narrow-hint refusal for this run. " +
560
+ "No-op today; wired so future hq-core-staging releases can flip " +
561
+ "the default narrow-hint level to 'strict'.")
505
562
  .action(async (options) => {
506
563
  try {
507
564
  assertSingleSelector(options, "now");
508
565
  if (options.all) {
509
- await runNowAll(options.hqRoot, options.message, options.onConflict);
566
+ await runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true);
510
567
  return;
511
568
  }
512
- await runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict);
569
+ await runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true);
513
570
  }
514
571
  catch (err) {
515
572
  console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
@@ -517,7 +574,7 @@ export function registerCloudCommands(program) {
517
574
  }
518
575
  });
519
576
  }
520
- async function runPullAll(hqRoot, onConflict) {
577
+ async function runPullAll(hqRoot, onConflict, modeAllOverride) {
521
578
  console.log(chalk.bold("\nHQ Sync — Pull (all)"));
522
579
  console.log(` HQ root: ${hqRoot}`);
523
580
  console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
@@ -537,8 +594,14 @@ async function runPullAll(hqRoot, onConflict) {
537
594
  return null;
538
595
  }
539
596
  },
597
+ getMembershipSyncConfig: (id) => realClient.getMembershipSyncConfig(id),
540
598
  };
541
- result = await pullAll({ hqRoot, ...(onConflict ? { onConflict } : {}) }, {
599
+ result = await pullAll({
600
+ hqRoot,
601
+ ...(onConflict ? { onConflict } : {}),
602
+ narrowHintLevel: resolveBannerLevel(),
603
+ ...(modeAllOverride ? { modeAllOverride: true } : {}),
604
+ }, {
542
605
  vaultClient: adapter,
543
606
  sync: (opts) => sync({
544
607
  company: opts.company,
@@ -687,7 +750,7 @@ async function runPushAll(hqRoot, message, onConflict) {
687
750
  if (errored > 0)
688
751
  process.exit(1);
689
752
  }
690
- async function runNowSingle(hqRoot, company, personal, message, onConflict) {
753
+ async function runNowSingle(hqRoot, company, personal, message, onConflict, modeAllOverride) {
691
754
  console.log(chalk.bold("\nHQ Sync — Now"));
692
755
  console.log(` HQ root: ${hqRoot}`);
693
756
  console.log(` Target: ${personal ? "(personal)" : (company ?? "(active company)")}`);
@@ -752,6 +815,49 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict) {
752
815
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
753
816
  process.exit(1);
754
817
  }
818
+ // US-011: resolve membership sync-config so we can either nudge an
819
+ // all-mode owner or refuse the pull when strict-mode is on. Skipped
820
+ // for personal targets (personal vault has no membership row) and
821
+ // for resolution failures (best-effort — never block sync). The
822
+ // lookup runs BEFORE the pull leg so strict refusal can short-circuit
823
+ // without burning a sync.
824
+ const narrowHintLevel = resolveBannerLevel();
825
+ let resolvedMode;
826
+ let resolvedCompanyUid;
827
+ if (!personalMode && targetCompany) {
828
+ try {
829
+ const client = new VaultClient(vaultConfig);
830
+ const memberships = await client.listMyMemberships();
831
+ const match = memberships.find((m) => m.companyUid === targetCompany || m.membershipKey === targetCompany);
832
+ if (match) {
833
+ resolvedCompanyUid = match.companyUid;
834
+ try {
835
+ const cfg = await client.getMembershipSyncConfig(match.membershipKey);
836
+ resolvedMode = cfg.syncMode;
837
+ }
838
+ catch {
839
+ resolvedMode = undefined;
840
+ }
841
+ }
842
+ }
843
+ catch {
844
+ resolvedMode = undefined;
845
+ }
846
+ }
847
+ if (resolvedMode === "all" &&
848
+ isStrictRefusal(resolvedMode, narrowHintLevel) &&
849
+ !modeAllOverride &&
850
+ resolvedCompanyUid) {
851
+ emitNarrowHint({
852
+ companyUid: resolvedCompanyUid,
853
+ syncMode: resolvedMode,
854
+ level: narrowHintLevel,
855
+ });
856
+ console.error(chalk.red("\n✗ Sync now refused: strict narrow-hint mode is on and this " +
857
+ "membership still pulls everything. Run `hq sync narrow --apply` " +
858
+ "to migrate, or re-run with --mode-all."));
859
+ process.exit(1);
860
+ }
755
861
  console.log(chalk.dim(" → pull leg"));
756
862
  const pullResult = await sync({
757
863
  company: targetCompany,
@@ -769,6 +875,15 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict) {
769
875
  console.log(chalk.yellow("\n⚠ Sync now finished with pull leg aborted."));
770
876
  process.exit(1);
771
877
  }
878
+ // US-011: emit the hint banner after a successful pull so it
879
+ // appears at the bottom of the summary rather than mid-stream.
880
+ if (resolvedMode === "all" && resolvedCompanyUid) {
881
+ emitNarrowHint({
882
+ companyUid: resolvedCompanyUid,
883
+ syncMode: resolvedMode,
884
+ level: narrowHintLevel,
885
+ });
886
+ }
772
887
  console.log(chalk.green("\n✓ Sync now complete"));
773
888
  }
774
889
  catch (err) {
@@ -776,7 +891,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict) {
776
891
  process.exit(1);
777
892
  }
778
893
  }
779
- async function runNowAll(hqRoot, message, onConflict) {
894
+ async function runNowAll(hqRoot, message, onConflict, modeAllOverride) {
780
895
  console.log(chalk.bold("\nHQ Sync — Now (all)"));
781
896
  console.log(` HQ root: ${hqRoot}`);
782
897
  console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
@@ -786,7 +901,10 @@ async function runNowAll(hqRoot, message, onConflict) {
786
901
  console.log(chalk.dim("→ push --all"));
787
902
  await runPushAll(hqRoot, message, onConflict);
788
903
  console.log(chalk.dim("\n→ pull --all"));
789
- await runPullAll(hqRoot, onConflict);
904
+ // US-011: forward --mode-all so the strict refusal applies to the
905
+ // pull leg (push doesn't need a narrow-hint — the narrow ritual is
906
+ // pull-side).
907
+ await runPullAll(hqRoot, onConflict, modeAllOverride);
790
908
  }
791
909
  /**
792
910
  * Best-effort read of the active company slug from `<hqRoot>/.hq/config.json`.
@@ -860,4 +978,4 @@ function resolveUploadAuthorFromCache() {
860
978
  }
861
979
  }
862
980
  //# sourceMappingURL=cloud.js.map
863
- //# debugId=7bc55d66-7b02-5b45-9444-710bb6d8dfd6
981
+ //# debugId=2eb7da1d-e84f-591c-b930-b84c5c96986b
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `hq files browse <path>` + `hq files cat <path> [--out <file>]` (US-008).
3
+ *
4
+ * Peek at a company's vault files **without** ever materialising them under
5
+ * `companies/{co}/` in the local HQ tree. Distinct from the sync path:
6
+ *
7
+ * - `browse` — `ListObjectsV2` under the given prefix, prints
8
+ * `{key, size, lastModified, aclSource}` rows. The
9
+ * `aclSource` hint distinguishes prefixes the caller can
10
+ * see via an EXPLICIT grant (`shared-with-you`) from
11
+ * prefixes they can see only because owner/admin
12
+ * role-bypass widened the vended policy (`role-bypass`).
13
+ * - `cat` — `GetObject`, stream the body to stdout. With `--out
14
+ * <file>` write the body to a path the user picked, but
15
+ * only after a bright-line guard refuses any destination
16
+ * inside `<hqRoot>/companies/` — that's the exact tree
17
+ * `hq sync` owns, and writing a peeked object there would
18
+ * silently re-import it into the sync envelope.
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.
25
+ *
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.
30
+ */
31
+ import { Command } from "commander";
32
+ import { ListObjectsV2Command, GetObjectCommand, type ListObjectsV2CommandOutput, type GetObjectCommandOutput } from "@aws-sdk/client-s3";
33
+ import { type VendResult, type ExplicitGrant } from "@indigoai-us/hq-cloud";
34
+ /**
35
+ * Subset of `VaultClient` this command actually uses — exposed so tests
36
+ * can stub vend + grants without standing up a real `VaultClient`.
37
+ */
38
+ export interface FilesBrowseVaultClient {
39
+ vend(input: {
40
+ paths: string[];
41
+ operations: "read-only" | "read-write" | "staged-write";
42
+ purpose: "sync" | "browse";
43
+ duration?: number;
44
+ }): Promise<VendResult>;
45
+ listMyExplicitGrants(companyUid: string): Promise<ExplicitGrant[]>;
46
+ entity: {
47
+ get(uid: string): Promise<{
48
+ uid: string;
49
+ slug: string;
50
+ name?: string;
51
+ bucketName?: string;
52
+ }>;
53
+ findInMyNamespace(type: string, slug: string): Promise<{
54
+ uid: string;
55
+ slug: string;
56
+ name?: string;
57
+ bucketName?: string;
58
+ } | null>;
59
+ };
60
+ }
61
+ /** Subset of `S3Client` this command actually uses — for test stubs. */
62
+ export interface FilesBrowseS3Client {
63
+ send(cmd: ListObjectsV2Command): Promise<ListObjectsV2CommandOutput>;
64
+ send(cmd: GetObjectCommand): Promise<GetObjectCommandOutput>;
65
+ }
66
+ /** Factory for an S3 client given vended credentials. Injectable for tests. */
67
+ export type S3ClientFactory = (input: {
68
+ region: string;
69
+ credentials: {
70
+ accessKeyId: string;
71
+ secretAccessKey: string;
72
+ sessionToken: string;
73
+ };
74
+ }) => FilesBrowseS3Client;
75
+ /** ACL provenance for a single listed key. */
76
+ export type AclSource = "shared-with-you" | "role-bypass";
77
+ export interface BrowseRow {
78
+ key: string;
79
+ size: number;
80
+ lastModified: Date | undefined;
81
+ aclSource: AclSource;
82
+ }
83
+ /**
84
+ * Parse the company slug from a vault prefix. Vault paths are anchored at
85
+ * `companies/<slug>/...`; anything else is rejected so we never try to
86
+ * browse a non-company tree (e.g. `personal/`) with a company-vend.
87
+ */
88
+ export declare function parseCompanySlugFromPath(prefix: string): string;
89
+ /**
90
+ * Classify a single S3 key against the caller's explicit-grant list. Any
91
+ * grant whose `path` is a prefix of the key contributes `shared-with-you`;
92
+ * otherwise the key is only visible via role-bypass on the vend call.
93
+ *
94
+ * Grant paths and S3 keys live in the same canonical form ("companies/<slug>/…");
95
+ * `coalescePrefixes` would shrink the list further but isn't required for
96
+ * correctness — `startsWith` already short-circuits on the first match.
97
+ */
98
+ export declare function classifyAclSource(key: string, grants: ExplicitGrant[]): AclSource;
99
+ /**
100
+ * Bright-line guard for `--out`: refuse to write any byte beneath
101
+ * `<hqRoot>/companies/`. We do NOT enumerate `companies/manifest.yaml`
102
+ * slug-by-slug — `companies/` is the entire surface hq-sync owns, so a
103
+ * containment check on that parent suffices and avoids drift with the
104
+ * manifest file. Returns the resolved absolute output path on success;
105
+ * throws when the destination would land inside the protected tree.
106
+ */
107
+ export declare function assertOutPathOutsideCompanies(outPath: string, hqRoot: string): string;
108
+ /**
109
+ * Render a browse listing as a padded table. Mirrors the chalk + padEnd
110
+ * pattern used by `hq sync mode --show` so the CLI surface stays
111
+ * stylistically consistent.
112
+ */
113
+ export declare function formatBrowseTable(rows: BrowseRow[]): string;
114
+ export interface RunBrowseInput {
115
+ /** Vault path prefix, e.g. `companies/indigo/scratch/`. */
116
+ pathPrefix: string;
117
+ /** Caller-overridden company slug (defaults to slug parsed from path). */
118
+ companySlug?: string;
119
+ vaultClient: FilesBrowseVaultClient;
120
+ s3Factory: S3ClientFactory;
121
+ region: string;
122
+ }
123
+ export interface RunBrowseResult {
124
+ rows: BrowseRow[];
125
+ vend: VendResult;
126
+ }
127
+ /**
128
+ * `hq files browse <path>` orchestrator.
129
+ *
130
+ * 1. Parse slug from prefix (or use override).
131
+ * 2. Resolve companyUid + bucketName via VaultClient.entity.
132
+ * 3. Vend with `purpose: 'browse'`, `operations: 'read-only'`, paths: [prefix].
133
+ * 4. Construct S3Client from vended creds, paginate ListObjectsV2.
134
+ * 5. Fetch explicit grants once, classify each key.
135
+ *
136
+ * Pure-ish: no console output, no process.exit — caller renders + exits.
137
+ */
138
+ export declare function runBrowse(input: RunBrowseInput): Promise<RunBrowseResult>;
139
+ export interface RunCatInput {
140
+ /** Single vault key, e.g. `companies/indigo/scratch/foo.txt`. */
141
+ key: string;
142
+ /**
143
+ * Where to write the body. `undefined` ⇒ stdout. Bright-line-guarded
144
+ * against `<hqRoot>/companies/` by `assertOutPathOutsideCompanies`.
145
+ */
146
+ out?: string;
147
+ hqRoot: string;
148
+ companySlug?: string;
149
+ vaultClient: FilesBrowseVaultClient;
150
+ s3Factory: S3ClientFactory;
151
+ region: string;
152
+ /** Destination stream for the stdout path. Injectable for tests. */
153
+ stdout?: NodeJS.WritableStream;
154
+ }
155
+ export interface RunCatResult {
156
+ bytesWritten: number;
157
+ destination: {
158
+ kind: "stdout";
159
+ } | {
160
+ kind: "file";
161
+ absPath: string;
162
+ };
163
+ vend: VendResult;
164
+ }
165
+ /**
166
+ * `hq files cat <path>` orchestrator. Vends with `purpose: 'browse'`, then
167
+ * streams the object body either to stdout or to `--out` (after the
168
+ * containment guard). Refuses ahead of any I/O when `--out` is unsafe.
169
+ */
170
+ export declare function runCat(input: RunCatInput): Promise<RunCatResult>;
171
+ /**
172
+ * Wire `hq files browse` + `hq files cat` onto an existing `files`
173
+ * Commander group. `registerFilesCommand` in files.ts builds the group
174
+ * and registers `share`/`unshare`/`acl`; this function appends the two
175
+ * new browse-vs-sync subcommands so they share the `--company` switch.
176
+ */
177
+ export declare function registerFilesBrowseCommands(filesCmd: Command): void;
178
+ //# sourceMappingURL=files-browse.d.ts.map