@indigoai-us/hq-cli 5.75.0 → 5.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents.js +8 -3
- package/dist/commands/files.d.ts +61 -0
- package/dist/commands/files.js +274 -0
- package/dist/commands/mcp-registration.d.ts +4 -5
- package/dist/commands/mcp-registration.js +5 -4
- package/dist/commands/outposts.d.ts +20 -4
- package/dist/commands/outposts.js +79 -10
- package/dist/commands/pack-install.d.ts +14 -17
- package/dist/commands/pack-install.js +53 -29
- package/dist/commands/pkg-install.js +3 -1
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +9 -3
- package/dist/commands/secrets.js +189 -87
- package/dist/run/hq-plugin.js +94 -31
- package/dist/utils/billing-gate.d.ts +15 -0
- package/dist/utils/billing-gate.js +35 -0
- package/dist/utils/sandbox-runner-client.d.ts +1 -0
- package/dist/utils/sandbox-runner-client.js +1 -0
- package/dist/utils/secrets-cache.d.ts +4 -5
- package/dist/utils/secrets-cache.js +5 -8
- package/package.json +3 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/commands/agents.test.ts +41 -0
- package/src/commands/agents.ts +7 -3
- package/src/commands/files-recovery.test.ts +361 -0
- package/src/commands/files.ts +410 -0
- package/src/commands/mcp-registration.ts +9 -9
- package/src/commands/outposts.test.ts +155 -24
- package/src/commands/outposts.ts +199 -45
- package/src/commands/pack-install-secret-authorization.test.ts +115 -0
- package/src/commands/pack-install.test.ts +5 -1
- package/src/commands/pack-install.ts +67 -29
- package/src/commands/pkg-install.ts +3 -1
- package/src/commands/run.test.ts +45 -0
- package/src/commands/run.ts +20 -4
- package/src/commands/secrets.test.ts +366 -25
- package/src/commands/secrets.ts +222 -96
- package/src/run/hq-plugin.test.ts +186 -10
- package/src/run/hq-plugin.ts +102 -32
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +23 -0
- package/src/utils/billing-gate.ts +46 -0
- package/src/utils/pack-contributions.test.ts +90 -31
- package/src/utils/sandbox-runner-client.test.ts +28 -0
- package/src/utils/sandbox-runner-client.ts +2 -0
- package/src/utils/secrets-cache.ts +5 -8
- package/test/commands/signals.test.ts +2 -2
- package/test/commands/sources.test.ts +2 -2
- package/test/helpers/vault-service-mock.ts +76 -17
- package/test/sources-signals/smoke.test.ts +2 -2
package/src/commands/files.ts
CHANGED
|
@@ -512,6 +512,100 @@ export function registerFilesCommand(program: Command): Command {
|
|
|
512
512
|
},
|
|
513
513
|
);
|
|
514
514
|
|
|
515
|
+
files
|
|
516
|
+
.command("versions <path>")
|
|
517
|
+
.description(
|
|
518
|
+
"List prior content versions and delete markers for one exact vault key. Use --personal for your personal vault.",
|
|
519
|
+
)
|
|
520
|
+
.option(
|
|
521
|
+
"--personal",
|
|
522
|
+
"Target your own personal vault instead of a company vault (mutually exclusive with --company)",
|
|
523
|
+
)
|
|
524
|
+
.action(async (path: string, opts: { personal?: boolean }) => {
|
|
525
|
+
try {
|
|
526
|
+
const companySlug = files.opts().company as string | undefined;
|
|
527
|
+
const personal = opts.personal === true;
|
|
528
|
+
assertRecoveryScope(personal, companySlug);
|
|
529
|
+
await runFilesVersions({ key: path, personal, companySlug });
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.error(
|
|
532
|
+
chalk.red("Error:"),
|
|
533
|
+
err instanceof Error ? err.message : String(err),
|
|
534
|
+
);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
files
|
|
540
|
+
.command("restore <path>")
|
|
541
|
+
.description(
|
|
542
|
+
"Restore a prior version or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.",
|
|
543
|
+
)
|
|
544
|
+
.option(
|
|
545
|
+
"--personal",
|
|
546
|
+
"Target your own personal vault instead of a company vault (mutually exclusive with --company)",
|
|
547
|
+
)
|
|
548
|
+
.option("--version <id>", "Content version ID to restore")
|
|
549
|
+
.option("-y, --yes", "Skip the overwrite confirmation prompt (for scripts)")
|
|
550
|
+
.action(
|
|
551
|
+
async (
|
|
552
|
+
path: string,
|
|
553
|
+
opts: { personal?: boolean; yes?: boolean; version?: string },
|
|
554
|
+
) => {
|
|
555
|
+
try {
|
|
556
|
+
const companySlug = files.opts().company as string | undefined;
|
|
557
|
+
const personal = opts.personal === true;
|
|
558
|
+
assertRecoveryScope(personal, companySlug);
|
|
559
|
+
await runFilesRestore({
|
|
560
|
+
key: path,
|
|
561
|
+
versionId: opts.version,
|
|
562
|
+
yes: opts.yes === true,
|
|
563
|
+
personal,
|
|
564
|
+
companySlug,
|
|
565
|
+
});
|
|
566
|
+
} catch (err) {
|
|
567
|
+
console.error(
|
|
568
|
+
chalk.red("Error:"),
|
|
569
|
+
err instanceof Error ? err.message : String(err),
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
files
|
|
577
|
+
.command("trash")
|
|
578
|
+
.description(
|
|
579
|
+
"List deleted vault keys retained as tombstones. Use --personal for your personal vault.",
|
|
580
|
+
)
|
|
581
|
+
.option(
|
|
582
|
+
"--personal",
|
|
583
|
+
"Target your own personal vault instead of a company vault (mutually exclusive with --company)",
|
|
584
|
+
)
|
|
585
|
+
.option("--prefix <prefix>", "Literal key prefix to filter tombstones")
|
|
586
|
+
.option("--cursor <cursor>", "Continue from an opaque tombstone cursor")
|
|
587
|
+
.action(
|
|
588
|
+
async (opts: { personal?: boolean; prefix?: string; cursor?: string }) => {
|
|
589
|
+
try {
|
|
590
|
+
const companySlug = files.opts().company as string | undefined;
|
|
591
|
+
const personal = opts.personal === true;
|
|
592
|
+
assertRecoveryScope(personal, companySlug);
|
|
593
|
+
await runFilesTrash({
|
|
594
|
+
prefix: opts.prefix ?? "",
|
|
595
|
+
cursor: opts.cursor,
|
|
596
|
+
personal,
|
|
597
|
+
companySlug,
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error(
|
|
601
|
+
chalk.red("Error:"),
|
|
602
|
+
err instanceof Error ? err.message : String(err),
|
|
603
|
+
);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
|
|
515
609
|
// Return the `files` Commander group so callers (src/index.ts) can attach
|
|
516
610
|
// additional subcommands (e.g. `hq files browse`/`hq files cat` from
|
|
517
611
|
// files-browse.ts) onto the same group without re-creating it.
|
|
@@ -1034,3 +1128,319 @@ export async function runFilesDelete(
|
|
|
1034
1128
|
);
|
|
1035
1129
|
}
|
|
1036
1130
|
}
|
|
1131
|
+
|
|
1132
|
+
// ---------------------------------------------------------------------------
|
|
1133
|
+
// Version history + recovery commands
|
|
1134
|
+
// ---------------------------------------------------------------------------
|
|
1135
|
+
|
|
1136
|
+
export interface FileVersionRow {
|
|
1137
|
+
versionId: string;
|
|
1138
|
+
isLatest: boolean;
|
|
1139
|
+
lastModified: string;
|
|
1140
|
+
size: number;
|
|
1141
|
+
isDeleteMarker?: boolean;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export interface FilesVersionsResponse {
|
|
1145
|
+
key: string;
|
|
1146
|
+
versions: FileVersionRow[];
|
|
1147
|
+
computedAt: string;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export interface FileTombstoneRow {
|
|
1151
|
+
key: string;
|
|
1152
|
+
deletedAt: string;
|
|
1153
|
+
deletedBy: string;
|
|
1154
|
+
deletedPrefix: string;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export interface FilesTrashResponse {
|
|
1158
|
+
companyUid?: string;
|
|
1159
|
+
personal?: true;
|
|
1160
|
+
tombstones: FileTombstoneRow[];
|
|
1161
|
+
cursor?: string | null;
|
|
1162
|
+
truncated: boolean;
|
|
1163
|
+
computedAt: string;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export interface FilesRestoreResponse {
|
|
1167
|
+
key: string;
|
|
1168
|
+
wasDeleted: boolean;
|
|
1169
|
+
restoredFromVersionId: string;
|
|
1170
|
+
newVersionId: string;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export class FilesRecoveryHttpError extends Error {
|
|
1174
|
+
constructor(
|
|
1175
|
+
public readonly status: number,
|
|
1176
|
+
message: string,
|
|
1177
|
+
public readonly code?: string,
|
|
1178
|
+
) {
|
|
1179
|
+
super(message);
|
|
1180
|
+
this.name = "FilesRecoveryHttpError";
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function assertRecoveryScope(personal: boolean, companySlug: string | undefined): void {
|
|
1185
|
+
if (personal && companySlug) {
|
|
1186
|
+
throw new Error("Pass either --personal or --company, not both.");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function assertExactRecoveryKey(key: string): void {
|
|
1191
|
+
if (!key || key.includes("*")) {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
"Restore and version history require one exact, wildcard-free vault key.",
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function quoteRecoveryShellArg(value: string): string {
|
|
1199
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function formatBytes(bytes: number): string {
|
|
1203
|
+
if (bytes < 1024) return String(bytes) + " B";
|
|
1204
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KiB";
|
|
1205
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
1206
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MiB";
|
|
1207
|
+
}
|
|
1208
|
+
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + " GiB";
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** Render the exact-key version history, including S3 delete markers. */
|
|
1212
|
+
export function formatFilesVersionsTable(rows: FileVersionRow[]): string {
|
|
1213
|
+
if (rows.length === 0) {
|
|
1214
|
+
return "No version history exists for that key.";
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const cols = ["VERSION", "TYPE", "LATEST", "MODIFIED", "SIZE"];
|
|
1218
|
+
const data = rows.map((row) => [
|
|
1219
|
+
row.versionId,
|
|
1220
|
+
row.isDeleteMarker ? "delete marker" : "content",
|
|
1221
|
+
row.isLatest ? "yes" : "",
|
|
1222
|
+
row.lastModified,
|
|
1223
|
+
row.isDeleteMarker ? "—" : formatBytes(row.size),
|
|
1224
|
+
]);
|
|
1225
|
+
const widths = cols.map((col, index) =>
|
|
1226
|
+
Math.max(col.length, ...data.map((row) => row[index].length)),
|
|
1227
|
+
);
|
|
1228
|
+
const renderRow = (row: string[]): string =>
|
|
1229
|
+
row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
|
1230
|
+
return [
|
|
1231
|
+
chalk.bold(renderRow(cols)),
|
|
1232
|
+
chalk.dim(renderRow(widths.map((width) => "─".repeat(width)))),
|
|
1233
|
+
...data.map(renderRow),
|
|
1234
|
+
].join("\n");
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/** Render durable delete tombstones without implying the objects still exist. */
|
|
1238
|
+
export function formatFilesTrashTable(rows: FileTombstoneRow[]): string {
|
|
1239
|
+
if (rows.length === 0) {
|
|
1240
|
+
return "Trash is empty.";
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const cols = ["KEY", "DELETED", "DELETED BY", "DELETE PREFIX"];
|
|
1244
|
+
const data = rows.map((row) => [
|
|
1245
|
+
row.key,
|
|
1246
|
+
row.deletedAt,
|
|
1247
|
+
row.deletedBy,
|
|
1248
|
+
row.deletedPrefix,
|
|
1249
|
+
]);
|
|
1250
|
+
const widths = cols.map((col, index) =>
|
|
1251
|
+
Math.max(col.length, ...data.map((row) => row[index].length)),
|
|
1252
|
+
);
|
|
1253
|
+
const renderRow = (row: string[]): string =>
|
|
1254
|
+
row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
|
1255
|
+
return [
|
|
1256
|
+
chalk.bold(renderRow(cols)),
|
|
1257
|
+
chalk.dim(renderRow(widths.map((width) => "─".repeat(width)))),
|
|
1258
|
+
...data.map(renderRow),
|
|
1259
|
+
].join("\n");
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function throwRecoveryHttpError(
|
|
1263
|
+
status: number,
|
|
1264
|
+
body: Record<string, string>,
|
|
1265
|
+
statusText: string,
|
|
1266
|
+
): never {
|
|
1267
|
+
throw new FilesRecoveryHttpError(
|
|
1268
|
+
status,
|
|
1269
|
+
body.message ?? body.error ?? statusText,
|
|
1270
|
+
body.code,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function formatFilesRecoveryError(err: FilesRecoveryHttpError, key?: string): string {
|
|
1275
|
+
if (err.status === 401) {
|
|
1276
|
+
return "Not authenticated — please run hq login";
|
|
1277
|
+
}
|
|
1278
|
+
if (err.status === 403) {
|
|
1279
|
+
return key
|
|
1280
|
+
? "Not authorized to modify '" + key + "' — you need write access on it"
|
|
1281
|
+
: "Not authorized to view this vault path";
|
|
1282
|
+
}
|
|
1283
|
+
if (err.code === "FILES_RESTORE_VERSION_NOT_FOUND") {
|
|
1284
|
+
return "That version is unavailable or is a delete marker.";
|
|
1285
|
+
}
|
|
1286
|
+
if (err.code === "FILES_RESTORE_NO_PRIOR_VERSION") {
|
|
1287
|
+
return "There is no prior content version to restore.";
|
|
1288
|
+
}
|
|
1289
|
+
if (err.code === "FILES_RESTORE_NO_RECOVERABLE_VERSION") {
|
|
1290
|
+
return "There is no recoverable content version for this key.";
|
|
1291
|
+
}
|
|
1292
|
+
if (err.status === 400) {
|
|
1293
|
+
return "Invalid request: " + err.message;
|
|
1294
|
+
}
|
|
1295
|
+
if (err.status >= 500) {
|
|
1296
|
+
return "Server error: " + err.message;
|
|
1297
|
+
}
|
|
1298
|
+
return err.message || "Request failed (" + String(err.status) + ")";
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
interface RecoveryScope {
|
|
1302
|
+
token: string;
|
|
1303
|
+
companyUid?: string;
|
|
1304
|
+
personal: boolean;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function resolveRecoveryScope(params: {
|
|
1308
|
+
personal: boolean;
|
|
1309
|
+
companySlug: string | undefined;
|
|
1310
|
+
}): Promise<RecoveryScope> {
|
|
1311
|
+
assertRecoveryScope(params.personal, params.companySlug);
|
|
1312
|
+
const token = await ensureCognitoToken();
|
|
1313
|
+
if (params.personal) {
|
|
1314
|
+
return { token, personal: true };
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
token,
|
|
1318
|
+
personal: false,
|
|
1319
|
+
companyUid: await getCompanyUid(token, params.companySlug),
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export async function runFilesVersions(params: {
|
|
1324
|
+
key: string;
|
|
1325
|
+
personal: boolean;
|
|
1326
|
+
companySlug: string | undefined;
|
|
1327
|
+
}): Promise<FilesVersionsResponse> {
|
|
1328
|
+
assertExactRecoveryKey(params.key);
|
|
1329
|
+
const scope = await resolveRecoveryScope(params);
|
|
1330
|
+
const res = await vaultApiFetch({
|
|
1331
|
+
token: scope.token,
|
|
1332
|
+
path: "/v1/files/versions",
|
|
1333
|
+
query: scope.personal
|
|
1334
|
+
? { personal: "1", key: params.key }
|
|
1335
|
+
: { company: scope.companyUid!, key: params.key },
|
|
1336
|
+
});
|
|
1337
|
+
if (!res.ok) {
|
|
1338
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
1339
|
+
throwRecoveryHttpError(res.status, body, res.statusText);
|
|
1340
|
+
}
|
|
1341
|
+
const data = (await res.json()) as FilesVersionsResponse;
|
|
1342
|
+
console.log(chalk.green("Versions for '" + data.key + "':"));
|
|
1343
|
+
console.log(formatFilesVersionsTable(data.versions));
|
|
1344
|
+
return data;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export async function runFilesRestore(
|
|
1348
|
+
params: {
|
|
1349
|
+
key: string;
|
|
1350
|
+
versionId?: string;
|
|
1351
|
+
yes: boolean;
|
|
1352
|
+
personal: boolean;
|
|
1353
|
+
companySlug: string | undefined;
|
|
1354
|
+
},
|
|
1355
|
+
deps: { confirm?: ConfirmFn } = {},
|
|
1356
|
+
): Promise<FilesRestoreResponse | undefined> {
|
|
1357
|
+
assertExactRecoveryKey(params.key);
|
|
1358
|
+
if (params.versionId !== undefined && params.versionId.trim() === "") {
|
|
1359
|
+
throw new Error("versionId must not be empty.");
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const confirm = deps.confirm ?? realConfirm;
|
|
1363
|
+
if (!params.yes) {
|
|
1364
|
+
const ok = await confirm(
|
|
1365
|
+
params.personal
|
|
1366
|
+
? "Restore '" + params.key + "' in your personal vault? This overwrites current content."
|
|
1367
|
+
: "Restore '" + params.key + "'? This overwrites current content in the shared vault.",
|
|
1368
|
+
);
|
|
1369
|
+
if (!ok) {
|
|
1370
|
+
console.log(chalk.dim("Aborted — nothing was restored."));
|
|
1371
|
+
return undefined;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const scope = await resolveRecoveryScope(params);
|
|
1376
|
+
const body: Record<string, unknown> = scope.personal
|
|
1377
|
+
? { personal: true, key: params.key }
|
|
1378
|
+
: { company: scope.companyUid, key: params.key };
|
|
1379
|
+
if (params.versionId !== undefined) body.versionId = params.versionId;
|
|
1380
|
+
const res = await vaultApiFetch({
|
|
1381
|
+
token: scope.token,
|
|
1382
|
+
path: "/v1/files/restore",
|
|
1383
|
+
method: "POST",
|
|
1384
|
+
body,
|
|
1385
|
+
});
|
|
1386
|
+
if (!res.ok) {
|
|
1387
|
+
const errBody = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
1388
|
+
const err = new FilesRecoveryHttpError(
|
|
1389
|
+
res.status,
|
|
1390
|
+
errBody.message ?? errBody.error ?? res.statusText,
|
|
1391
|
+
errBody.code,
|
|
1392
|
+
);
|
|
1393
|
+
throw new Error(formatFilesRecoveryError(err, params.key));
|
|
1394
|
+
}
|
|
1395
|
+
const data = (await res.json()) as FilesRestoreResponse;
|
|
1396
|
+
const verb = data.wasDeleted ? "Undeleted" : "Restored";
|
|
1397
|
+
console.log(
|
|
1398
|
+
chalk.green(
|
|
1399
|
+
verb + " '" + data.key + "' from version '" + data.restoredFromVersionId + "'.",
|
|
1400
|
+
),
|
|
1401
|
+
);
|
|
1402
|
+
console.log(chalk.dim("New version: " + data.newVersionId));
|
|
1403
|
+
return data;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
export async function runFilesTrash(params: {
|
|
1407
|
+
prefix: string;
|
|
1408
|
+
cursor?: string;
|
|
1409
|
+
personal: boolean;
|
|
1410
|
+
companySlug: string | undefined;
|
|
1411
|
+
}): Promise<FilesTrashResponse> {
|
|
1412
|
+
if (params.prefix.includes("*")) {
|
|
1413
|
+
throw new Error("Trash prefix must be literal; wildcard expansion is not supported.");
|
|
1414
|
+
}
|
|
1415
|
+
const scope = await resolveRecoveryScope(params);
|
|
1416
|
+
const query: Record<string, string> = scope.personal
|
|
1417
|
+
? { personal: "1" }
|
|
1418
|
+
: { company: scope.companyUid! };
|
|
1419
|
+
if (params.prefix) query.prefix = params.prefix;
|
|
1420
|
+
if (params.cursor) query.cursor = params.cursor;
|
|
1421
|
+
const res = await vaultApiFetch({
|
|
1422
|
+
token: scope.token,
|
|
1423
|
+
path: "/v1/files/tombstones",
|
|
1424
|
+
query,
|
|
1425
|
+
});
|
|
1426
|
+
if (!res.ok) {
|
|
1427
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
1428
|
+
throwRecoveryHttpError(res.status, body, res.statusText);
|
|
1429
|
+
}
|
|
1430
|
+
const data = (await res.json()) as FilesTrashResponse;
|
|
1431
|
+
console.log(formatFilesTrashTable(data.tombstones));
|
|
1432
|
+
if (data.truncated && data.cursor) {
|
|
1433
|
+
let continuation = "hq files";
|
|
1434
|
+
if (params.companySlug) {
|
|
1435
|
+
continuation += " --company " + quoteRecoveryShellArg(params.companySlug);
|
|
1436
|
+
}
|
|
1437
|
+
continuation += " trash";
|
|
1438
|
+
if (params.personal) continuation += " --personal";
|
|
1439
|
+
if (params.prefix) {
|
|
1440
|
+
continuation += " --prefix " + quoteRecoveryShellArg(params.prefix);
|
|
1441
|
+
}
|
|
1442
|
+
continuation += " --cursor " + quoteRecoveryShellArg(data.cursor);
|
|
1443
|
+
console.log(chalk.dim("More results: " + continuation));
|
|
1444
|
+
}
|
|
1445
|
+
return data;
|
|
1446
|
+
}
|
|
@@ -1071,7 +1071,8 @@ function restoreSafely(realTarget: string, backupDir: string): boolean {
|
|
|
1071
1071
|
//
|
|
1072
1072
|
// SECRET SAFETY (the hard rule from the PRD authModel / US-002 AC):
|
|
1073
1073
|
// - `${secret:NAME}` resolves ONLY here, at emit, via the injected
|
|
1074
|
-
// {@link SecretResolver}
|
|
1074
|
+
// {@link SecretResolver}. Production injects values returned by a fresh
|
|
1075
|
+
// server authorization; the encrypted disk cache is never a value fallback.
|
|
1075
1076
|
// - The RESOLVED value lands in `~/.claude.json` — a 0600 user-global file that
|
|
1076
1077
|
// is NOT synced/committed — because the runtime needs the real header to work.
|
|
1077
1078
|
// - The resolved value is REDACTED from every return value and never echoed; we
|
|
@@ -1105,11 +1106,10 @@ export interface McpManifest {
|
|
|
1105
1106
|
/**
|
|
1106
1107
|
* Resolve a `${secret:NAME}` reference to its plaintext value, or return `null`
|
|
1107
1108
|
* when the secret is unavailable (un-minted / TTL-expired / no company context).
|
|
1108
|
-
* Production binds this to
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
* reference at all.
|
|
1109
|
+
* Production binds this to an in-memory map populated by a fresh vault `/load`
|
|
1110
|
+
* authorization response; tests inject a pure map. Returning `null` for a
|
|
1111
|
+
* referenced secret is a hard error at emit (we refuse to write a broken
|
|
1112
|
+
* header), distinct from a name with no reference at all.
|
|
1113
1113
|
*/
|
|
1114
1114
|
export type SecretResolver = (name: string) => string | null;
|
|
1115
1115
|
|
|
@@ -1146,9 +1146,9 @@ export function resolveSecretRefs(
|
|
|
1146
1146
|
const resolved = resolve(name);
|
|
1147
1147
|
if (resolved === null || resolved === undefined) {
|
|
1148
1148
|
throw new McpManifestError(
|
|
1149
|
-
`cannot resolve \${secret:${name}} at emit — the secret
|
|
1150
|
-
'
|
|
1151
|
-
'write a half-resolved header.',
|
|
1149
|
+
`cannot resolve \${secret:${name}} at emit — the secret was not available ` +
|
|
1150
|
+
'after online vault authorization (check login, company scope, and access, ' +
|
|
1151
|
+
'then re-install). HQ refuses to write a half-resolved header.',
|
|
1152
1152
|
);
|
|
1153
1153
|
}
|
|
1154
1154
|
if (resolved.length > 0) secretSink.add(resolved);
|