@indigoai-us/hq-cli 5.17.0 → 5.18.1
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/dist/commands/cloud.d.ts +63 -1
- package/dist/commands/cloud.js +214 -11
- package/dist/commands/files-browse.d.ts +178 -0
- package/dist/commands/files-browse.js +348 -0
- package/dist/commands/files.d.ts +1 -1
- package/dist/commands/files.js +6 -2
- package/dist/commands/sync-mode.d.ts +115 -0
- package/dist/commands/sync-mode.js +249 -0
- package/dist/commands/sync-narrow.d.ts +154 -0
- package/dist/commands/sync-narrow.js +327 -0
- package/dist/index.js +11 -3
- package/dist/lib/local-tree-diff.d.ts +94 -0
- package/dist/lib/local-tree-diff.js +244 -0
- package/dist/lib/narrow-hint-banner.d.ts +102 -0
- package/dist/lib/narrow-hint-banner.js +144 -0
- package/package.json +2 -2
- package/src/commands/cloud.pull-all.test.ts +170 -1
- package/src/commands/cloud.pull-per-company.test.ts +188 -0
- package/src/commands/cloud.ts +327 -5
- package/src/commands/files-browse.test.ts +475 -0
- package/src/commands/files-browse.ts +561 -0
- package/src/commands/files.ts +6 -1
- package/src/commands/sync-mode.test.ts +366 -0
- package/src/commands/sync-mode.ts +387 -0
- package/src/commands/sync-narrow.test.ts +573 -0
- package/src/commands/sync-narrow.ts +541 -0
- package/src/index.ts +9 -1
- package/src/lib/hq-cloud-dep.smoke.test.ts +75 -0
- package/src/lib/local-tree-diff.test.ts +262 -0
- package/src/lib/local-tree-diff.ts +330 -0
- package/src/lib/narrow-hint-banner.test.ts +235 -0
- package/src/lib/narrow-hint-banner.ts +212 -0
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
|
|
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]="7a4467c5-bef0-5f15-99f9-9db157497caa")}catch(e){}}();
|
|
33
|
+
import chalk from "chalk";
|
|
34
|
+
import * as fs from "node:fs";
|
|
35
|
+
import * as path from "node:path";
|
|
36
|
+
import { pipeline } from "node:stream/promises";
|
|
37
|
+
import { S3Client, ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
|
|
38
|
+
import { VaultClient, } from "@indigoai-us/hq-cloud";
|
|
39
|
+
import { DEFAULT_HQ_ROOT, DEFAULT_COGNITO, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
40
|
+
import { getCompanyUid } from "../utils/vault-api.js";
|
|
41
|
+
// ── Pure helpers ────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Parse the company slug from a vault prefix. Vault paths are anchored at
|
|
44
|
+
* `companies/<slug>/...`; anything else is rejected so we never try to
|
|
45
|
+
* browse a non-company tree (e.g. `personal/`) with a company-vend.
|
|
46
|
+
*/
|
|
47
|
+
export function parseCompanySlugFromPath(prefix) {
|
|
48
|
+
const normalized = prefix.replace(/^\/+/, "");
|
|
49
|
+
const parts = normalized.split("/");
|
|
50
|
+
if (parts.length < 2 || parts[0] !== "companies" || !parts[1]) {
|
|
51
|
+
throw new Error(`Invalid browse path '${prefix}'. Expected a path starting with 'companies/<slug>/'.`);
|
|
52
|
+
}
|
|
53
|
+
return parts[1];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Classify a single S3 key against the caller's explicit-grant list. Any
|
|
57
|
+
* grant whose `path` is a prefix of the key contributes `shared-with-you`;
|
|
58
|
+
* otherwise the key is only visible via role-bypass on the vend call.
|
|
59
|
+
*
|
|
60
|
+
* Grant paths and S3 keys live in the same canonical form ("companies/<slug>/…");
|
|
61
|
+
* `coalescePrefixes` would shrink the list further but isn't required for
|
|
62
|
+
* correctness — `startsWith` already short-circuits on the first match.
|
|
63
|
+
*/
|
|
64
|
+
export function classifyAclSource(key, grants) {
|
|
65
|
+
for (const g of grants) {
|
|
66
|
+
if (g.path && key.startsWith(g.path))
|
|
67
|
+
return "shared-with-you";
|
|
68
|
+
}
|
|
69
|
+
return "role-bypass";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Bright-line guard for `--out`: refuse to write any byte beneath
|
|
73
|
+
* `<hqRoot>/companies/`. We do NOT enumerate `companies/manifest.yaml`
|
|
74
|
+
* slug-by-slug — `companies/` is the entire surface hq-sync owns, so a
|
|
75
|
+
* containment check on that parent suffices and avoids drift with the
|
|
76
|
+
* manifest file. Returns the resolved absolute output path on success;
|
|
77
|
+
* throws when the destination would land inside the protected tree.
|
|
78
|
+
*/
|
|
79
|
+
export function assertOutPathOutsideCompanies(outPath, hqRoot) {
|
|
80
|
+
const absOut = path.resolve(outPath);
|
|
81
|
+
const protectedRoot = path.resolve(hqRoot, "companies") + path.sep;
|
|
82
|
+
if (absOut === path.resolve(hqRoot, "companies") || absOut.startsWith(protectedRoot)) {
|
|
83
|
+
throw new Error(`Refusing to write '${absOut}': bytes peeked via 'hq files cat' must not land under ` +
|
|
84
|
+
`'${path.resolve(hqRoot, "companies")}'. Pick an --out path outside the HQ companies tree.`);
|
|
85
|
+
}
|
|
86
|
+
return absOut;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Render a browse listing as a padded table. Mirrors the chalk + padEnd
|
|
90
|
+
* pattern used by `hq sync mode --show` so the CLI surface stays
|
|
91
|
+
* stylistically consistent.
|
|
92
|
+
*/
|
|
93
|
+
export function formatBrowseTable(rows) {
|
|
94
|
+
if (rows.length === 0) {
|
|
95
|
+
return "No objects under that prefix.";
|
|
96
|
+
}
|
|
97
|
+
const cols = ["KEY", "SIZE", "MODIFIED", "ACL"];
|
|
98
|
+
const data = rows.map((r) => [
|
|
99
|
+
r.key,
|
|
100
|
+
String(r.size),
|
|
101
|
+
r.lastModified ? r.lastModified.toISOString() : "—",
|
|
102
|
+
r.aclSource,
|
|
103
|
+
]);
|
|
104
|
+
const widths = cols.map((c, i) => Math.max(c.length, ...data.map((row) => row[i].length)));
|
|
105
|
+
const renderRow = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
|
|
106
|
+
const lines = [
|
|
107
|
+
chalk.bold(renderRow(cols)),
|
|
108
|
+
chalk.dim(renderRow(widths.map((w) => "─".repeat(w)))),
|
|
109
|
+
...data.map(renderRow),
|
|
110
|
+
];
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* `hq files browse <path>` orchestrator.
|
|
115
|
+
*
|
|
116
|
+
* 1. Parse slug from prefix (or use override).
|
|
117
|
+
* 2. Resolve companyUid + bucketName via VaultClient.entity.
|
|
118
|
+
* 3. Vend with `purpose: 'browse'`, `operations: 'read-only'`, paths: [prefix].
|
|
119
|
+
* 4. Construct S3Client from vended creds, paginate ListObjectsV2.
|
|
120
|
+
* 5. Fetch explicit grants once, classify each key.
|
|
121
|
+
*
|
|
122
|
+
* Pure-ish: no console output, no process.exit — caller renders + exits.
|
|
123
|
+
*/
|
|
124
|
+
export async function runBrowse(input) {
|
|
125
|
+
const { pathPrefix, vaultClient, s3Factory, region } = input;
|
|
126
|
+
const slug = input.companySlug ?? parseCompanySlugFromPath(pathPrefix);
|
|
127
|
+
const entity = await vaultClient.entity.findInMyNamespace("company", slug);
|
|
128
|
+
if (!entity) {
|
|
129
|
+
throw new Error(`No company found for slug '${slug}' in your namespace. Confirm you have an active membership.`);
|
|
130
|
+
}
|
|
131
|
+
if (!entity.bucketName) {
|
|
132
|
+
throw new Error(`Company '${slug}' (${entity.uid}) has no provisioned bucket.`);
|
|
133
|
+
}
|
|
134
|
+
const companyUid = entity.uid;
|
|
135
|
+
const bucket = entity.bucketName;
|
|
136
|
+
// Distinct vend call from sync — `purpose: 'browse'` opts the request
|
|
137
|
+
// into the role-bypass-allowed code path on the server (US-009).
|
|
138
|
+
const vend = await vaultClient.vend({
|
|
139
|
+
paths: [pathPrefix],
|
|
140
|
+
operations: "read-only",
|
|
141
|
+
purpose: "browse",
|
|
142
|
+
});
|
|
143
|
+
const s3 = s3Factory({
|
|
144
|
+
region,
|
|
145
|
+
credentials: {
|
|
146
|
+
accessKeyId: vend.credentials.accessKeyId,
|
|
147
|
+
secretAccessKey: vend.credentials.secretAccessKey,
|
|
148
|
+
sessionToken: vend.credentials.sessionToken,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
// Pull the caller's explicit-grant graph once so per-key classification
|
|
152
|
+
// is O(grants) without N round-trips.
|
|
153
|
+
const grants = await vaultClient.listMyExplicitGrants(companyUid);
|
|
154
|
+
const rows = [];
|
|
155
|
+
let continuationToken;
|
|
156
|
+
do {
|
|
157
|
+
const resp = (await s3.send(new ListObjectsV2Command({
|
|
158
|
+
Bucket: bucket,
|
|
159
|
+
Prefix: pathPrefix,
|
|
160
|
+
ContinuationToken: continuationToken,
|
|
161
|
+
})));
|
|
162
|
+
for (const obj of resp.Contents ?? []) {
|
|
163
|
+
if (!obj.Key)
|
|
164
|
+
continue;
|
|
165
|
+
// Skip S3 "directory marker" objects (0-byte, trailing slash).
|
|
166
|
+
if (obj.Key.endsWith("/") && (obj.Size ?? 0) === 0)
|
|
167
|
+
continue;
|
|
168
|
+
rows.push({
|
|
169
|
+
key: obj.Key,
|
|
170
|
+
size: obj.Size ?? 0,
|
|
171
|
+
lastModified: obj.LastModified,
|
|
172
|
+
aclSource: classifyAclSource(obj.Key, grants),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
continuationToken = resp.NextContinuationToken ?? undefined;
|
|
176
|
+
} while (continuationToken);
|
|
177
|
+
return { rows, vend };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* `hq files cat <path>` orchestrator. Vends with `purpose: 'browse'`, then
|
|
181
|
+
* streams the object body either to stdout or to `--out` (after the
|
|
182
|
+
* containment guard). Refuses ahead of any I/O when `--out` is unsafe.
|
|
183
|
+
*/
|
|
184
|
+
export async function runCat(input) {
|
|
185
|
+
const { key, vaultClient, s3Factory, region, hqRoot } = input;
|
|
186
|
+
const slug = input.companySlug ?? parseCompanySlugFromPath(key);
|
|
187
|
+
// Acceptance 5: refuse BEFORE vending — no point pulling credentials
|
|
188
|
+
// for a request we're already going to abort.
|
|
189
|
+
let absOut;
|
|
190
|
+
if (input.out !== undefined) {
|
|
191
|
+
absOut = assertOutPathOutsideCompanies(input.out, hqRoot);
|
|
192
|
+
}
|
|
193
|
+
const entity = await vaultClient.entity.findInMyNamespace("company", slug);
|
|
194
|
+
if (!entity) {
|
|
195
|
+
throw new Error(`No company found for slug '${slug}' in your namespace. Confirm you have an active membership.`);
|
|
196
|
+
}
|
|
197
|
+
if (!entity.bucketName) {
|
|
198
|
+
throw new Error(`Company '${slug}' (${entity.uid}) has no provisioned bucket.`);
|
|
199
|
+
}
|
|
200
|
+
const vend = await vaultClient.vend({
|
|
201
|
+
paths: [key],
|
|
202
|
+
operations: "read-only",
|
|
203
|
+
purpose: "browse",
|
|
204
|
+
});
|
|
205
|
+
const s3 = s3Factory({
|
|
206
|
+
region,
|
|
207
|
+
credentials: {
|
|
208
|
+
accessKeyId: vend.credentials.accessKeyId,
|
|
209
|
+
secretAccessKey: vend.credentials.secretAccessKey,
|
|
210
|
+
sessionToken: vend.credentials.sessionToken,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
const resp = (await s3.send(new GetObjectCommand({ Bucket: entity.bucketName, Key: key })));
|
|
214
|
+
if (!resp.Body) {
|
|
215
|
+
throw new Error(`GetObject for '${key}' returned no body.`);
|
|
216
|
+
}
|
|
217
|
+
// The SDK Body type in node is a Readable (it can also be a
|
|
218
|
+
// ReadableStream/Blob in other runtimes but those don't apply to the
|
|
219
|
+
// CLI). Cast through unknown so the type checker accepts the narrowing.
|
|
220
|
+
const body = resp.Body;
|
|
221
|
+
let bytesWritten = 0;
|
|
222
|
+
body.on("data", (chunk) => {
|
|
223
|
+
bytesWritten += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
224
|
+
});
|
|
225
|
+
if (absOut !== undefined) {
|
|
226
|
+
// Ensure the parent directory exists — but ONLY if it's also outside
|
|
227
|
+
// the protected tree (the guard already validated absOut itself; the
|
|
228
|
+
// parent of an outside-tree path is by definition outside too).
|
|
229
|
+
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
230
|
+
await pipeline(body, fs.createWriteStream(absOut));
|
|
231
|
+
return {
|
|
232
|
+
bytesWritten,
|
|
233
|
+
destination: { kind: "file", absPath: absOut },
|
|
234
|
+
vend,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
await pipeline(body, input.stdout ?? process.stdout);
|
|
238
|
+
return { bytesWritten, destination: { kind: "stdout" }, vend };
|
|
239
|
+
}
|
|
240
|
+
// ── CLI registration ────────────────────────────────────────────────────────
|
|
241
|
+
const defaultS3Factory = ({ region, credentials }) => new S3Client({ region, credentials });
|
|
242
|
+
/**
|
|
243
|
+
* Wire `hq files browse` + `hq files cat` onto an existing `files`
|
|
244
|
+
* Commander group. `registerFilesCommand` in files.ts builds the group
|
|
245
|
+
* and registers `share`/`unshare`/`acl`; this function appends the two
|
|
246
|
+
* new browse-vs-sync subcommands so they share the `--company` switch.
|
|
247
|
+
*/
|
|
248
|
+
export function registerFilesBrowseCommands(filesCmd) {
|
|
249
|
+
filesCmd
|
|
250
|
+
.command("browse <path>")
|
|
251
|
+
.description("List vault objects under <path> without syncing them locally. Uses the browse-vend path (role-bypass allowed).")
|
|
252
|
+
.option("--company <slug>", "Company slug (defaults to the slug parsed from <path>)")
|
|
253
|
+
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
254
|
+
.action(async (pathArg, options) => {
|
|
255
|
+
try {
|
|
256
|
+
const accessToken = await ensureCognitoToken();
|
|
257
|
+
const vaultConfig = buildVaultConfig(accessToken);
|
|
258
|
+
const client = new VaultClient(vaultConfig);
|
|
259
|
+
// Resolve slug — CLI flag wins, otherwise parse from path arg.
|
|
260
|
+
const slug = options.company ?? parseCompanySlugFromPath(pathArg);
|
|
261
|
+
// If the user passed `--company` AND the path doesn't begin with
|
|
262
|
+
// companies/<that-slug>/, refuse — we'd otherwise vend creds for
|
|
263
|
+
// one company and list keys from another tree, which never makes
|
|
264
|
+
// sense (defense in depth against operator typos).
|
|
265
|
+
if (options.company !== undefined) {
|
|
266
|
+
const fromPath = (() => {
|
|
267
|
+
try {
|
|
268
|
+
return parseCompanySlugFromPath(pathArg);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
})();
|
|
274
|
+
if (fromPath && fromPath !== options.company) {
|
|
275
|
+
throw new Error(`--company '${options.company}' disagrees with path slug '${fromPath}'.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Confirm the slug resolves to a known membership — same pattern
|
|
279
|
+
// sync-mode/sync-narrow use to surface "you're not a member" early.
|
|
280
|
+
await getCompanyUid(accessToken, slug);
|
|
281
|
+
const result = await runBrowse({
|
|
282
|
+
pathPrefix: pathArg,
|
|
283
|
+
companySlug: slug,
|
|
284
|
+
vaultClient: client,
|
|
285
|
+
s3Factory: defaultS3Factory,
|
|
286
|
+
region: DEFAULT_COGNITO.region,
|
|
287
|
+
});
|
|
288
|
+
console.log(formatBrowseTable(result.rows));
|
|
289
|
+
if (result.rows.length > 0) {
|
|
290
|
+
const bypassCount = result.rows.filter((r) => r.aclSource === "role-bypass").length;
|
|
291
|
+
if (bypassCount > 0) {
|
|
292
|
+
console.log("");
|
|
293
|
+
console.log(chalk.yellow(`Heads-up: ${bypassCount} object(s) visible only via role-bypass (no explicit grant covers them).`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
filesCmd
|
|
303
|
+
.command("cat <path>")
|
|
304
|
+
.description("Stream a single vault object to stdout (or --out <file>) without syncing it. Uses the browse-vend path.")
|
|
305
|
+
.option("--out <file>", "Write the object body to <file> instead of stdout. Refused under <hqRoot>/companies/.")
|
|
306
|
+
.option("--company <slug>", "Company slug (defaults to the slug parsed from <path>)")
|
|
307
|
+
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
308
|
+
.action(async (keyArg, options) => {
|
|
309
|
+
try {
|
|
310
|
+
const accessToken = await ensureCognitoToken();
|
|
311
|
+
const vaultConfig = buildVaultConfig(accessToken);
|
|
312
|
+
const client = new VaultClient(vaultConfig);
|
|
313
|
+
const slug = options.company ?? parseCompanySlugFromPath(keyArg);
|
|
314
|
+
if (options.company !== undefined) {
|
|
315
|
+
const fromPath = (() => {
|
|
316
|
+
try {
|
|
317
|
+
return parseCompanySlugFromPath(keyArg);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
})();
|
|
323
|
+
if (fromPath && fromPath !== options.company) {
|
|
324
|
+
throw new Error(`--company '${options.company}' disagrees with path slug '${fromPath}'.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
await getCompanyUid(accessToken, slug);
|
|
328
|
+
const result = await runCat({
|
|
329
|
+
key: keyArg,
|
|
330
|
+
out: options.out,
|
|
331
|
+
hqRoot: options.hqRoot,
|
|
332
|
+
companySlug: slug,
|
|
333
|
+
vaultClient: client,
|
|
334
|
+
s3Factory: defaultS3Factory,
|
|
335
|
+
region: DEFAULT_COGNITO.region,
|
|
336
|
+
});
|
|
337
|
+
if (result.destination.kind === "file") {
|
|
338
|
+
console.error(chalk.green("✓"), `Wrote ${result.bytesWritten} bytes to ${result.destination.absPath}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=files-browse.js.map
|
|
348
|
+
//# debugId=7a4467c5-bef0-5f15-99f9-9db157497caa
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -40,5 +40,5 @@ export declare class ShareSessionHttpError extends Error {
|
|
|
40
40
|
* stay in sync.
|
|
41
41
|
*/
|
|
42
42
|
export declare function formatShareSessionError(err: ShareSessionHttpError): string;
|
|
43
|
-
export declare function registerFilesCommand(program: Command):
|
|
43
|
+
export declare function registerFilesCommand(program: Command): Command;
|
|
44
44
|
//# sourceMappingURL=files.d.ts.map
|
package/dist/commands/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!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]="
|
|
2
|
+
!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]="c72d8e20-42a6-5818-b5d3-985e53954430")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -315,6 +315,10 @@ export function registerFilesCommand(program) {
|
|
|
315
315
|
process.exit(1);
|
|
316
316
|
}
|
|
317
317
|
});
|
|
318
|
+
// Return the `files` Commander group so callers (src/index.ts) can attach
|
|
319
|
+
// additional subcommands (e.g. `hq files browse`/`hq files cat` from
|
|
320
|
+
// files-browse.ts) onto the same group without re-creating it.
|
|
321
|
+
return files;
|
|
318
322
|
}
|
|
319
323
|
async function runDirectGrant(params) {
|
|
320
324
|
const canonicalPrefix = normalizeFilePrefix(params.prefix);
|
|
@@ -456,4 +460,4 @@ async function runShareSession(params) {
|
|
|
456
460
|
}
|
|
457
461
|
}
|
|
458
462
|
//# sourceMappingURL=files.js.map
|
|
459
|
-
//# debugId=
|
|
463
|
+
//# debugId=c72d8e20-42a6-5818-b5d3-985e53954430
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq sync mode <mode>` (US-006) — flip a membership's syncMode for a company.
|
|
3
|
+
*
|
|
4
|
+
* Subcommand of the `hq sync` group. Three call shapes:
|
|
5
|
+
*
|
|
6
|
+
* hq sync mode shared|all|custom [--company <slug>]
|
|
7
|
+
* Resolves the target membership for the given company (or the cwd's
|
|
8
|
+
* active company from `.hq/config.json` if `--company` is omitted) and
|
|
9
|
+
* calls `VaultClient.setMembershipSyncConfig`. Prints membershipId +
|
|
10
|
+
* previous mode → new mode as a chat audit trail.
|
|
11
|
+
*
|
|
12
|
+
* hq sync mode --show
|
|
13
|
+
* No positional. Prints a table of every membership the caller has
|
|
14
|
+
* with its company slug, current sync-mode, and last-updated stamp.
|
|
15
|
+
*
|
|
16
|
+
* hq sync mode custom --paths a/,b/ (planned follow-up)
|
|
17
|
+
* For the `custom` mode the server requires `customPaths`. This command
|
|
18
|
+
* accepts `--paths` as a comma-separated list to forward to the API; if
|
|
19
|
+
* omitted on `custom` the server validation will reject.
|
|
20
|
+
*
|
|
21
|
+
* Auto-detect of `--company` from cwd: best-effort via the active-company
|
|
22
|
+
* slug in `<hq-root>/.hq/config.json`. If absent, the caller must pass
|
|
23
|
+
* `--company` explicitly — there's no cwd-walk to a `companies/<slug>/`
|
|
24
|
+
* folder in this initial implementation (follow-up US could add it).
|
|
25
|
+
*
|
|
26
|
+
* Cross-package note: this command depends on `VaultClient`
|
|
27
|
+
* (`getMembershipSyncConfig` / `setMembershipSyncConfig`) added in
|
|
28
|
+
* hq-cloud US-004 (commit 41e5ee1). While that hq-cloud release is
|
|
29
|
+
* unpublished, hq-cli pins `@indigoai-us/hq-cloud` to `file:../hq-cloud`
|
|
30
|
+
* in package.json — revert that line to `^5.20.0` (or whatever the
|
|
31
|
+
* published cut is) once US-004 ships to npm.
|
|
32
|
+
*/
|
|
33
|
+
import { Command } from "commander";
|
|
34
|
+
import { type MembershipSyncConfig, type Membership, type SyncMode } from "@indigoai-us/hq-cloud";
|
|
35
|
+
export declare const LEGAL_SYNC_MODES: readonly SyncMode[];
|
|
36
|
+
/** Subset of VaultClient surface this command exercises (test seam). */
|
|
37
|
+
export interface SyncModeVaultClient {
|
|
38
|
+
listMyMemberships(): Promise<Membership[]>;
|
|
39
|
+
getMembershipSyncConfig(membershipId: string): Promise<MembershipSyncConfig>;
|
|
40
|
+
setMembershipSyncConfig(membershipId: string, partial: {
|
|
41
|
+
syncMode: SyncMode;
|
|
42
|
+
customPaths?: string[];
|
|
43
|
+
}): Promise<MembershipSyncConfig>;
|
|
44
|
+
entity: {
|
|
45
|
+
get(uid: string): Promise<{
|
|
46
|
+
uid: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface SetSyncModeOptions {
|
|
53
|
+
/** Validated mode — already known to be a legal SyncMode. */
|
|
54
|
+
mode: SyncMode;
|
|
55
|
+
/** Company slug (caller-provided or resolved from cwd). Required. */
|
|
56
|
+
companySlug: string;
|
|
57
|
+
/** Required when `mode === "custom"`. Server rejects otherwise. */
|
|
58
|
+
customPaths?: string[];
|
|
59
|
+
/** Injected vault client (real impl built from access token by default). */
|
|
60
|
+
vaultClient: SyncModeVaultClient;
|
|
61
|
+
}
|
|
62
|
+
export interface SetSyncModeResult {
|
|
63
|
+
membershipId: string;
|
|
64
|
+
companySlug: string;
|
|
65
|
+
previousMode: SyncMode;
|
|
66
|
+
newMode: SyncMode;
|
|
67
|
+
previousWasDefault: boolean;
|
|
68
|
+
newConfig: MembershipSyncConfig;
|
|
69
|
+
}
|
|
70
|
+
export interface ShowSyncModesOptions {
|
|
71
|
+
vaultClient: SyncModeVaultClient;
|
|
72
|
+
}
|
|
73
|
+
export interface ShowSyncModesRow {
|
|
74
|
+
companySlug: string;
|
|
75
|
+
companyName?: string;
|
|
76
|
+
membershipId: string;
|
|
77
|
+
syncMode: SyncMode;
|
|
78
|
+
isDefault: boolean;
|
|
79
|
+
updatedAt?: string;
|
|
80
|
+
}
|
|
81
|
+
/** Throws a helpful Error if `mode` is not a legal SyncMode. */
|
|
82
|
+
export declare function validateMode(mode: string): SyncMode;
|
|
83
|
+
/**
|
|
84
|
+
* Read the active company slug from `<hq-root>/.hq/config.json`. Returns
|
|
85
|
+
* undefined when the file is missing or `activeCompany` isn't set. Never
|
|
86
|
+
* throws — auto-detect is best-effort.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readActiveCompanySlug(hqRoot: string): string | undefined;
|
|
89
|
+
/** Parse `--paths a/,b/c/` into trimmed non-empty entries. */
|
|
90
|
+
export declare function parseCustomPaths(raw: string | undefined): string[] | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a membership by company slug, then PUT the new sync-mode. Returns
|
|
93
|
+
* the membershipId, the previous mode (read via GET first), and the server's
|
|
94
|
+
* fresh config (which sets `isDefault: false` once a row exists).
|
|
95
|
+
*/
|
|
96
|
+
export declare function setSyncMode(options: SetSyncModeOptions): Promise<SetSyncModeResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Fetch every membership the caller has, resolve company slugs, and look up
|
|
99
|
+
* each effective sync-config in parallel. Returns rows sorted by slug for
|
|
100
|
+
* stable table output.
|
|
101
|
+
*/
|
|
102
|
+
export declare function showSyncModes(options: ShowSyncModesOptions): Promise<ShowSyncModesRow[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Render the `--show` table as plain text. No external dep — hq-cli doesn't
|
|
105
|
+
* use cli-table3, so we hand-format columns to match the existing chalk +
|
|
106
|
+
* padEnd pattern used by `hq members list`.
|
|
107
|
+
*/
|
|
108
|
+
export declare function formatShowTable(rows: ShowSyncModesRow[]): string;
|
|
109
|
+
/**
|
|
110
|
+
* Wire `hq sync mode` onto an existing `sync` Commander group. The caller
|
|
111
|
+
* (`src/index.ts`) constructs the `sync` group and calls this after
|
|
112
|
+
* `registerCloudCommands` so push/pull/status/mode all coexist.
|
|
113
|
+
*/
|
|
114
|
+
export declare function registerSyncModeCommand(syncCmd: Command): void;
|
|
115
|
+
//# sourceMappingURL=sync-mode.d.ts.map
|