@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/dist/commands/agents.js
CHANGED
|
@@ -25,7 +25,7 @@ import chalk from "chalk";
|
|
|
25
25
|
import { randomUUID } from "node:crypto";
|
|
26
26
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
27
27
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
28
|
-
import { AGENT_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload,
|
|
28
|
+
import { AGENT_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
29
29
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
30
30
|
export const VALID_EFFORTS = new Set([
|
|
31
31
|
"minimal",
|
|
@@ -64,7 +64,12 @@ export async function agentsRequest(opts) {
|
|
|
64
64
|
const res = await vaultApiFetch(opts);
|
|
65
65
|
if (!res.ok) {
|
|
66
66
|
const body = (await res.json().catch(() => ({})));
|
|
67
|
-
throw new AgentsHttpError(res.status,
|
|
67
|
+
throw new AgentsHttpError(res.status,
|
|
68
|
+
// `message` FIRST: on a payment_failed envelope it carries the friendly
|
|
69
|
+
// decline copy ("Your card was declined…") while `error` is the generic
|
|
70
|
+
// "payment required" — error-first would feed surfaceBillingBlocked the
|
|
71
|
+
// generic string and lose the decline reason (mirrors outpostRequest).
|
|
72
|
+
body.message ?? body.error ?? res.statusText, body.code, parseBillingPayload(body));
|
|
68
73
|
}
|
|
69
74
|
return (await res.json());
|
|
70
75
|
}
|
|
@@ -371,7 +376,7 @@ export function registerAgentsCommand(program) {
|
|
|
371
376
|
if (err instanceof AgentsHttpError &&
|
|
372
377
|
err.status === 402 &&
|
|
373
378
|
err.billing) {
|
|
374
|
-
await
|
|
379
|
+
await surfaceBillingBlocked(token, err.billing, err.message);
|
|
375
380
|
process.exit(1);
|
|
376
381
|
}
|
|
377
382
|
throw err;
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -92,5 +92,66 @@ export declare function stripRedundantCompanyScope(prefix: string): {
|
|
|
92
92
|
export declare function runFilesDelete(params: RunFilesDeleteParams, deps?: {
|
|
93
93
|
confirm?: ConfirmFn;
|
|
94
94
|
}): Promise<void>;
|
|
95
|
+
export interface FileVersionRow {
|
|
96
|
+
versionId: string;
|
|
97
|
+
isLatest: boolean;
|
|
98
|
+
lastModified: string;
|
|
99
|
+
size: number;
|
|
100
|
+
isDeleteMarker?: boolean;
|
|
101
|
+
}
|
|
102
|
+
export interface FilesVersionsResponse {
|
|
103
|
+
key: string;
|
|
104
|
+
versions: FileVersionRow[];
|
|
105
|
+
computedAt: string;
|
|
106
|
+
}
|
|
107
|
+
export interface FileTombstoneRow {
|
|
108
|
+
key: string;
|
|
109
|
+
deletedAt: string;
|
|
110
|
+
deletedBy: string;
|
|
111
|
+
deletedPrefix: string;
|
|
112
|
+
}
|
|
113
|
+
export interface FilesTrashResponse {
|
|
114
|
+
companyUid?: string;
|
|
115
|
+
personal?: true;
|
|
116
|
+
tombstones: FileTombstoneRow[];
|
|
117
|
+
cursor?: string | null;
|
|
118
|
+
truncated: boolean;
|
|
119
|
+
computedAt: string;
|
|
120
|
+
}
|
|
121
|
+
export interface FilesRestoreResponse {
|
|
122
|
+
key: string;
|
|
123
|
+
wasDeleted: boolean;
|
|
124
|
+
restoredFromVersionId: string;
|
|
125
|
+
newVersionId: string;
|
|
126
|
+
}
|
|
127
|
+
export declare class FilesRecoveryHttpError extends Error {
|
|
128
|
+
readonly status: number;
|
|
129
|
+
readonly code?: string | undefined;
|
|
130
|
+
constructor(status: number, message: string, code?: string | undefined);
|
|
131
|
+
}
|
|
132
|
+
/** Render the exact-key version history, including S3 delete markers. */
|
|
133
|
+
export declare function formatFilesVersionsTable(rows: FileVersionRow[]): string;
|
|
134
|
+
/** Render durable delete tombstones without implying the objects still exist. */
|
|
135
|
+
export declare function formatFilesTrashTable(rows: FileTombstoneRow[]): string;
|
|
136
|
+
export declare function runFilesVersions(params: {
|
|
137
|
+
key: string;
|
|
138
|
+
personal: boolean;
|
|
139
|
+
companySlug: string | undefined;
|
|
140
|
+
}): Promise<FilesVersionsResponse>;
|
|
141
|
+
export declare function runFilesRestore(params: {
|
|
142
|
+
key: string;
|
|
143
|
+
versionId?: string;
|
|
144
|
+
yes: boolean;
|
|
145
|
+
personal: boolean;
|
|
146
|
+
companySlug: string | undefined;
|
|
147
|
+
}, deps?: {
|
|
148
|
+
confirm?: ConfirmFn;
|
|
149
|
+
}): Promise<FilesRestoreResponse | undefined>;
|
|
150
|
+
export declare function runFilesTrash(params: {
|
|
151
|
+
prefix: string;
|
|
152
|
+
cursor?: string;
|
|
153
|
+
personal: boolean;
|
|
154
|
+
companySlug: string | undefined;
|
|
155
|
+
}): Promise<FilesTrashResponse>;
|
|
95
156
|
export {};
|
|
96
157
|
//# sourceMappingURL=files.d.ts.map
|
package/dist/commands/files.js
CHANGED
|
@@ -379,6 +379,69 @@ export function registerFilesCommand(program) {
|
|
|
379
379
|
process.exit(1);
|
|
380
380
|
}
|
|
381
381
|
});
|
|
382
|
+
files
|
|
383
|
+
.command("versions <path>")
|
|
384
|
+
.description("List prior content versions and delete markers for one exact vault key. Use --personal for your personal vault.")
|
|
385
|
+
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
386
|
+
.action(async (path, opts) => {
|
|
387
|
+
try {
|
|
388
|
+
const companySlug = files.opts().company;
|
|
389
|
+
const personal = opts.personal === true;
|
|
390
|
+
assertRecoveryScope(personal, companySlug);
|
|
391
|
+
await runFilesVersions({ key: path, personal, companySlug });
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
files
|
|
399
|
+
.command("restore <path>")
|
|
400
|
+
.description("Restore a prior version or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.")
|
|
401
|
+
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
402
|
+
.option("--version <id>", "Content version ID to restore")
|
|
403
|
+
.option("-y, --yes", "Skip the overwrite confirmation prompt (for scripts)")
|
|
404
|
+
.action(async (path, opts) => {
|
|
405
|
+
try {
|
|
406
|
+
const companySlug = files.opts().company;
|
|
407
|
+
const personal = opts.personal === true;
|
|
408
|
+
assertRecoveryScope(personal, companySlug);
|
|
409
|
+
await runFilesRestore({
|
|
410
|
+
key: path,
|
|
411
|
+
versionId: opts.version,
|
|
412
|
+
yes: opts.yes === true,
|
|
413
|
+
personal,
|
|
414
|
+
companySlug,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
files
|
|
423
|
+
.command("trash")
|
|
424
|
+
.description("List deleted vault keys retained as tombstones. Use --personal for your personal vault.")
|
|
425
|
+
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
426
|
+
.option("--prefix <prefix>", "Literal key prefix to filter tombstones")
|
|
427
|
+
.option("--cursor <cursor>", "Continue from an opaque tombstone cursor")
|
|
428
|
+
.action(async (opts) => {
|
|
429
|
+
try {
|
|
430
|
+
const companySlug = files.opts().company;
|
|
431
|
+
const personal = opts.personal === true;
|
|
432
|
+
assertRecoveryScope(personal, companySlug);
|
|
433
|
+
await runFilesTrash({
|
|
434
|
+
prefix: opts.prefix ?? "",
|
|
435
|
+
cursor: opts.cursor,
|
|
436
|
+
personal,
|
|
437
|
+
companySlug,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
382
445
|
// Return the `files` Commander group so callers (src/index.ts) can attach
|
|
383
446
|
// additional subcommands (e.g. `hq files browse`/`hq files cat` from
|
|
384
447
|
// files-browse.ts) onto the same group without re-creating it.
|
|
@@ -737,4 +800,215 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
737
800
|
console.log(chalk.dim(`${result.skipped} object(s) under this prefix you can't delete were left untouched.`));
|
|
738
801
|
}
|
|
739
802
|
}
|
|
803
|
+
export class FilesRecoveryHttpError extends Error {
|
|
804
|
+
status;
|
|
805
|
+
code;
|
|
806
|
+
constructor(status, message, code) {
|
|
807
|
+
super(message);
|
|
808
|
+
this.status = status;
|
|
809
|
+
this.code = code;
|
|
810
|
+
this.name = "FilesRecoveryHttpError";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function assertRecoveryScope(personal, companySlug) {
|
|
814
|
+
if (personal && companySlug) {
|
|
815
|
+
throw new Error("Pass either --personal or --company, not both.");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function assertExactRecoveryKey(key) {
|
|
819
|
+
if (!key || key.includes("*")) {
|
|
820
|
+
throw new Error("Restore and version history require one exact, wildcard-free vault key.");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function quoteRecoveryShellArg(value) {
|
|
824
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
825
|
+
}
|
|
826
|
+
function formatBytes(bytes) {
|
|
827
|
+
if (bytes < 1024)
|
|
828
|
+
return String(bytes) + " B";
|
|
829
|
+
if (bytes < 1024 * 1024)
|
|
830
|
+
return (bytes / 1024).toFixed(1) + " KiB";
|
|
831
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
832
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MiB";
|
|
833
|
+
}
|
|
834
|
+
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + " GiB";
|
|
835
|
+
}
|
|
836
|
+
/** Render the exact-key version history, including S3 delete markers. */
|
|
837
|
+
export function formatFilesVersionsTable(rows) {
|
|
838
|
+
if (rows.length === 0) {
|
|
839
|
+
return "No version history exists for that key.";
|
|
840
|
+
}
|
|
841
|
+
const cols = ["VERSION", "TYPE", "LATEST", "MODIFIED", "SIZE"];
|
|
842
|
+
const data = rows.map((row) => [
|
|
843
|
+
row.versionId,
|
|
844
|
+
row.isDeleteMarker ? "delete marker" : "content",
|
|
845
|
+
row.isLatest ? "yes" : "",
|
|
846
|
+
row.lastModified,
|
|
847
|
+
row.isDeleteMarker ? "—" : formatBytes(row.size),
|
|
848
|
+
]);
|
|
849
|
+
const widths = cols.map((col, index) => Math.max(col.length, ...data.map((row) => row[index].length)));
|
|
850
|
+
const renderRow = (row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
|
851
|
+
return [
|
|
852
|
+
chalk.bold(renderRow(cols)),
|
|
853
|
+
chalk.dim(renderRow(widths.map((width) => "─".repeat(width)))),
|
|
854
|
+
...data.map(renderRow),
|
|
855
|
+
].join("\n");
|
|
856
|
+
}
|
|
857
|
+
/** Render durable delete tombstones without implying the objects still exist. */
|
|
858
|
+
export function formatFilesTrashTable(rows) {
|
|
859
|
+
if (rows.length === 0) {
|
|
860
|
+
return "Trash is empty.";
|
|
861
|
+
}
|
|
862
|
+
const cols = ["KEY", "DELETED", "DELETED BY", "DELETE PREFIX"];
|
|
863
|
+
const data = rows.map((row) => [
|
|
864
|
+
row.key,
|
|
865
|
+
row.deletedAt,
|
|
866
|
+
row.deletedBy,
|
|
867
|
+
row.deletedPrefix,
|
|
868
|
+
]);
|
|
869
|
+
const widths = cols.map((col, index) => Math.max(col.length, ...data.map((row) => row[index].length)));
|
|
870
|
+
const renderRow = (row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
|
871
|
+
return [
|
|
872
|
+
chalk.bold(renderRow(cols)),
|
|
873
|
+
chalk.dim(renderRow(widths.map((width) => "─".repeat(width)))),
|
|
874
|
+
...data.map(renderRow),
|
|
875
|
+
].join("\n");
|
|
876
|
+
}
|
|
877
|
+
function throwRecoveryHttpError(status, body, statusText) {
|
|
878
|
+
throw new FilesRecoveryHttpError(status, body.message ?? body.error ?? statusText, body.code);
|
|
879
|
+
}
|
|
880
|
+
function formatFilesRecoveryError(err, key) {
|
|
881
|
+
if (err.status === 401) {
|
|
882
|
+
return "Not authenticated — please run hq login";
|
|
883
|
+
}
|
|
884
|
+
if (err.status === 403) {
|
|
885
|
+
return key
|
|
886
|
+
? "Not authorized to modify '" + key + "' — you need write access on it"
|
|
887
|
+
: "Not authorized to view this vault path";
|
|
888
|
+
}
|
|
889
|
+
if (err.code === "FILES_RESTORE_VERSION_NOT_FOUND") {
|
|
890
|
+
return "That version is unavailable or is a delete marker.";
|
|
891
|
+
}
|
|
892
|
+
if (err.code === "FILES_RESTORE_NO_PRIOR_VERSION") {
|
|
893
|
+
return "There is no prior content version to restore.";
|
|
894
|
+
}
|
|
895
|
+
if (err.code === "FILES_RESTORE_NO_RECOVERABLE_VERSION") {
|
|
896
|
+
return "There is no recoverable content version for this key.";
|
|
897
|
+
}
|
|
898
|
+
if (err.status === 400) {
|
|
899
|
+
return "Invalid request: " + err.message;
|
|
900
|
+
}
|
|
901
|
+
if (err.status >= 500) {
|
|
902
|
+
return "Server error: " + err.message;
|
|
903
|
+
}
|
|
904
|
+
return err.message || "Request failed (" + String(err.status) + ")";
|
|
905
|
+
}
|
|
906
|
+
async function resolveRecoveryScope(params) {
|
|
907
|
+
assertRecoveryScope(params.personal, params.companySlug);
|
|
908
|
+
const token = await ensureCognitoToken();
|
|
909
|
+
if (params.personal) {
|
|
910
|
+
return { token, personal: true };
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
token,
|
|
914
|
+
personal: false,
|
|
915
|
+
companyUid: await getCompanyUid(token, params.companySlug),
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
export async function runFilesVersions(params) {
|
|
919
|
+
assertExactRecoveryKey(params.key);
|
|
920
|
+
const scope = await resolveRecoveryScope(params);
|
|
921
|
+
const res = await vaultApiFetch({
|
|
922
|
+
token: scope.token,
|
|
923
|
+
path: "/v1/files/versions",
|
|
924
|
+
query: scope.personal
|
|
925
|
+
? { personal: "1", key: params.key }
|
|
926
|
+
: { company: scope.companyUid, key: params.key },
|
|
927
|
+
});
|
|
928
|
+
if (!res.ok) {
|
|
929
|
+
const body = (await res.json().catch(() => ({})));
|
|
930
|
+
throwRecoveryHttpError(res.status, body, res.statusText);
|
|
931
|
+
}
|
|
932
|
+
const data = (await res.json());
|
|
933
|
+
console.log(chalk.green("Versions for '" + data.key + "':"));
|
|
934
|
+
console.log(formatFilesVersionsTable(data.versions));
|
|
935
|
+
return data;
|
|
936
|
+
}
|
|
937
|
+
export async function runFilesRestore(params, deps = {}) {
|
|
938
|
+
assertExactRecoveryKey(params.key);
|
|
939
|
+
if (params.versionId !== undefined && params.versionId.trim() === "") {
|
|
940
|
+
throw new Error("versionId must not be empty.");
|
|
941
|
+
}
|
|
942
|
+
const confirm = deps.confirm ?? realConfirm;
|
|
943
|
+
if (!params.yes) {
|
|
944
|
+
const ok = await confirm(params.personal
|
|
945
|
+
? "Restore '" + params.key + "' in your personal vault? This overwrites current content."
|
|
946
|
+
: "Restore '" + params.key + "'? This overwrites current content in the shared vault.");
|
|
947
|
+
if (!ok) {
|
|
948
|
+
console.log(chalk.dim("Aborted — nothing was restored."));
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const scope = await resolveRecoveryScope(params);
|
|
953
|
+
const body = scope.personal
|
|
954
|
+
? { personal: true, key: params.key }
|
|
955
|
+
: { company: scope.companyUid, key: params.key };
|
|
956
|
+
if (params.versionId !== undefined)
|
|
957
|
+
body.versionId = params.versionId;
|
|
958
|
+
const res = await vaultApiFetch({
|
|
959
|
+
token: scope.token,
|
|
960
|
+
path: "/v1/files/restore",
|
|
961
|
+
method: "POST",
|
|
962
|
+
body,
|
|
963
|
+
});
|
|
964
|
+
if (!res.ok) {
|
|
965
|
+
const errBody = (await res.json().catch(() => ({})));
|
|
966
|
+
const err = new FilesRecoveryHttpError(res.status, errBody.message ?? errBody.error ?? res.statusText, errBody.code);
|
|
967
|
+
throw new Error(formatFilesRecoveryError(err, params.key));
|
|
968
|
+
}
|
|
969
|
+
const data = (await res.json());
|
|
970
|
+
const verb = data.wasDeleted ? "Undeleted" : "Restored";
|
|
971
|
+
console.log(chalk.green(verb + " '" + data.key + "' from version '" + data.restoredFromVersionId + "'."));
|
|
972
|
+
console.log(chalk.dim("New version: " + data.newVersionId));
|
|
973
|
+
return data;
|
|
974
|
+
}
|
|
975
|
+
export async function runFilesTrash(params) {
|
|
976
|
+
if (params.prefix.includes("*")) {
|
|
977
|
+
throw new Error("Trash prefix must be literal; wildcard expansion is not supported.");
|
|
978
|
+
}
|
|
979
|
+
const scope = await resolveRecoveryScope(params);
|
|
980
|
+
const query = scope.personal
|
|
981
|
+
? { personal: "1" }
|
|
982
|
+
: { company: scope.companyUid };
|
|
983
|
+
if (params.prefix)
|
|
984
|
+
query.prefix = params.prefix;
|
|
985
|
+
if (params.cursor)
|
|
986
|
+
query.cursor = params.cursor;
|
|
987
|
+
const res = await vaultApiFetch({
|
|
988
|
+
token: scope.token,
|
|
989
|
+
path: "/v1/files/tombstones",
|
|
990
|
+
query,
|
|
991
|
+
});
|
|
992
|
+
if (!res.ok) {
|
|
993
|
+
const body = (await res.json().catch(() => ({})));
|
|
994
|
+
throwRecoveryHttpError(res.status, body, res.statusText);
|
|
995
|
+
}
|
|
996
|
+
const data = (await res.json());
|
|
997
|
+
console.log(formatFilesTrashTable(data.tombstones));
|
|
998
|
+
if (data.truncated && data.cursor) {
|
|
999
|
+
let continuation = "hq files";
|
|
1000
|
+
if (params.companySlug) {
|
|
1001
|
+
continuation += " --company " + quoteRecoveryShellArg(params.companySlug);
|
|
1002
|
+
}
|
|
1003
|
+
continuation += " trash";
|
|
1004
|
+
if (params.personal)
|
|
1005
|
+
continuation += " --personal";
|
|
1006
|
+
if (params.prefix) {
|
|
1007
|
+
continuation += " --prefix " + quoteRecoveryShellArg(params.prefix);
|
|
1008
|
+
}
|
|
1009
|
+
continuation += " --cursor " + quoteRecoveryShellArg(data.cursor);
|
|
1010
|
+
console.log(chalk.dim("More results: " + continuation));
|
|
1011
|
+
}
|
|
1012
|
+
return data;
|
|
1013
|
+
}
|
|
740
1014
|
//# sourceMappingURL=files.js.map
|
|
@@ -429,11 +429,10 @@ export interface McpManifest {
|
|
|
429
429
|
/**
|
|
430
430
|
* Resolve a `${secret:NAME}` reference to its plaintext value, or return `null`
|
|
431
431
|
* when the secret is unavailable (un-minted / TTL-expired / no company context).
|
|
432
|
-
* Production binds this to
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* reference at all.
|
|
432
|
+
* Production binds this to an in-memory map populated by a fresh vault `/load`
|
|
433
|
+
* authorization response; tests inject a pure map. Returning `null` for a
|
|
434
|
+
* referenced secret is a hard error at emit (we refuse to write a broken
|
|
435
|
+
* header), distinct from a name with no reference at all.
|
|
437
436
|
*/
|
|
438
437
|
export type SecretResolver = (name: string) => string | null;
|
|
439
438
|
/** True iff `value` contains at least one `${secret:NAME}` reference. */
|
|
@@ -848,7 +848,8 @@ function restoreSafely(realTarget, backupDir) {
|
|
|
848
848
|
//
|
|
849
849
|
// SECRET SAFETY (the hard rule from the PRD authModel / US-002 AC):
|
|
850
850
|
// - `${secret:NAME}` resolves ONLY here, at emit, via the injected
|
|
851
|
-
// {@link SecretResolver}
|
|
851
|
+
// {@link SecretResolver}. Production injects values returned by a fresh
|
|
852
|
+
// server authorization; the encrypted disk cache is never a value fallback.
|
|
852
853
|
// - The RESOLVED value lands in `~/.claude.json` — a 0600 user-global file that
|
|
853
854
|
// is NOT synced/committed — because the runtime needs the real header to work.
|
|
854
855
|
// - The resolved value is REDACTED from every return value and never echoed; we
|
|
@@ -885,9 +886,9 @@ export function resolveSecretRefs(value, resolve, secretSink) {
|
|
|
885
886
|
return value.replace(SECRET_REF_RE, (_match, name) => {
|
|
886
887
|
const resolved = resolve(name);
|
|
887
888
|
if (resolved === null || resolved === undefined) {
|
|
888
|
-
throw new McpManifestError(`cannot resolve \${secret:${name}} at emit — the secret
|
|
889
|
-
'
|
|
890
|
-
'write a half-resolved header.');
|
|
889
|
+
throw new McpManifestError(`cannot resolve \${secret:${name}} at emit — the secret was not available ` +
|
|
890
|
+
'after online vault authorization (check login, company scope, and access, ' +
|
|
891
|
+
'then re-install). HQ refuses to write a half-resolved header.');
|
|
891
892
|
}
|
|
892
893
|
if (resolved.length > 0)
|
|
893
894
|
secretSink.add(resolved);
|
|
@@ -23,13 +23,27 @@
|
|
|
23
23
|
import { Command } from "commander";
|
|
24
24
|
import { spawnSync } from "node:child_process";
|
|
25
25
|
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
26
|
+
/**
|
|
27
|
+
* hq-pro's per-person cap envelope on a `409` provision block. Unlike every
|
|
28
|
+
* other `/outpost/*` failure this body carries NO `message`/`error` field —
|
|
29
|
+
* only the cap facts — so it has to be decoded structurally or the reason
|
|
30
|
+
* degrades to a bare `res.statusText` ("Conflict").
|
|
31
|
+
*/
|
|
32
|
+
export interface OutpostCappedPayload {
|
|
33
|
+
limit: number;
|
|
34
|
+
outposts: OutpostSummary[];
|
|
35
|
+
}
|
|
36
|
+
/** Decode hq-pro's `{ capped, limit, outposts }` cap envelope, if present. */
|
|
37
|
+
export declare function parseCappedPayload(body: unknown): OutpostCappedPayload | undefined;
|
|
26
38
|
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
27
39
|
export declare class OutpostHttpError extends Error {
|
|
28
40
|
status: number;
|
|
29
41
|
step?: string;
|
|
30
42
|
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
31
43
|
billing?: BillingErrorPayload;
|
|
32
|
-
|
|
44
|
+
/** hq-pro's cap envelope on a `409` provision block. */
|
|
45
|
+
capped?: OutpostCappedPayload;
|
|
46
|
+
constructor(status: number, message: string, step?: string, billing?: BillingErrorPayload, capped?: OutpostCappedPayload);
|
|
33
47
|
}
|
|
34
48
|
/** Row summary from `GET /outpost/list`. */
|
|
35
49
|
export interface OutpostSummary {
|
|
@@ -59,9 +73,11 @@ export declare function outpostRequest<T>(opts: {
|
|
|
59
73
|
/**
|
|
60
74
|
* Provision the caller's Outpost. Sends the cached Cognito refresh token so the
|
|
61
75
|
* box can authenticate AS the caller (the same body the console's
|
|
62
|
-
* `provisionMyOutpost` sends).
|
|
63
|
-
* per-person cap gets
|
|
64
|
-
*
|
|
76
|
+
* `provisionMyOutpost` sends). No duplicate is ever created: a caller already at
|
|
77
|
+
* their per-person cap gets a `409` whose body lists their existing boxes —
|
|
78
|
+
* thrown here as an `OutpostHttpError` carrying `capped` (hq-pro checks the cap
|
|
79
|
+
* BEFORE activation billing, so a capped call is never charged). The refresh
|
|
80
|
+
* token is sent over HTTPS and NEVER printed.
|
|
65
81
|
*/
|
|
66
82
|
export declare function provisionOutpost(token: string, input: {
|
|
67
83
|
refreshToken: string;
|
|
@@ -31,19 +31,34 @@ import * as yaml from "js-yaml";
|
|
|
31
31
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
32
32
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
33
33
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
34
|
-
import { OUTPOST_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload,
|
|
34
|
+
import { OUTPOST_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
35
|
+
/** Decode hq-pro's `{ capped, limit, outposts }` cap envelope, if present. */
|
|
36
|
+
export function parseCappedPayload(body) {
|
|
37
|
+
if (!body || typeof body !== "object")
|
|
38
|
+
return undefined;
|
|
39
|
+
const b = body;
|
|
40
|
+
if (b.capped !== true)
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
limit: typeof b.limit === "number" ? b.limit : 0,
|
|
44
|
+
outposts: Array.isArray(b.outposts) ? b.outposts : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
35
47
|
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
36
48
|
export class OutpostHttpError extends Error {
|
|
37
49
|
status;
|
|
38
50
|
step;
|
|
39
51
|
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
40
52
|
billing;
|
|
41
|
-
|
|
53
|
+
/** hq-pro's cap envelope on a `409` provision block. */
|
|
54
|
+
capped;
|
|
55
|
+
constructor(status, message, step, billing, capped) {
|
|
42
56
|
super(message);
|
|
43
57
|
this.name = "OutpostHttpError";
|
|
44
58
|
this.status = status;
|
|
45
59
|
this.step = step;
|
|
46
60
|
this.billing = billing;
|
|
61
|
+
this.capped = capped;
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
/**
|
|
@@ -62,16 +77,18 @@ export async function outpostRequest(opts) {
|
|
|
62
77
|
: typeof body.error === "string"
|
|
63
78
|
? body.error
|
|
64
79
|
: res.statusText;
|
|
65
|
-
throw new OutpostHttpError(res.status, message, body.step, parseBillingPayload(body));
|
|
80
|
+
throw new OutpostHttpError(res.status, message, body.step, parseBillingPayload(body), parseCappedPayload(body));
|
|
66
81
|
}
|
|
67
82
|
return (await res.json());
|
|
68
83
|
}
|
|
69
84
|
/**
|
|
70
85
|
* Provision the caller's Outpost. Sends the cached Cognito refresh token so the
|
|
71
86
|
* box can authenticate AS the caller (the same body the console's
|
|
72
|
-
* `provisionMyOutpost` sends).
|
|
73
|
-
* per-person cap gets
|
|
74
|
-
*
|
|
87
|
+
* `provisionMyOutpost` sends). No duplicate is ever created: a caller already at
|
|
88
|
+
* their per-person cap gets a `409` whose body lists their existing boxes —
|
|
89
|
+
* thrown here as an `OutpostHttpError` carrying `capped` (hq-pro checks the cap
|
|
90
|
+
* BEFORE activation billing, so a capped call is never charged). The refresh
|
|
91
|
+
* token is sent over HTTPS and NEVER printed.
|
|
75
92
|
*/
|
|
76
93
|
export async function provisionOutpost(token, input) {
|
|
77
94
|
return outpostRequest({
|
|
@@ -300,6 +317,24 @@ function ensureTrailingNewline(s) {
|
|
|
300
317
|
// ---------------------------------------------------------------------------
|
|
301
318
|
// Command registration
|
|
302
319
|
// ---------------------------------------------------------------------------
|
|
320
|
+
/**
|
|
321
|
+
* Explain a `409` per-person cap. hq-pro checks the cap BEFORE activation
|
|
322
|
+
* billing, so nothing was charged — worth saying, since the caller just
|
|
323
|
+
* confirmed a recurring charge to get here.
|
|
324
|
+
*/
|
|
325
|
+
function surfaceOutpostCapped(capped) {
|
|
326
|
+
const owned = capped.outposts.length;
|
|
327
|
+
console.error(chalk.yellow(`You're already at your Outpost limit (${owned} of ${capped.limit}). ` +
|
|
328
|
+
`No new box was provisioned and you have not been charged.`));
|
|
329
|
+
for (const o of capped.outposts) {
|
|
330
|
+
const detail = [o.state, o.instanceName, o.region]
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.join(" ");
|
|
333
|
+
console.error(` ${o.outpostId} ${detail}`);
|
|
334
|
+
}
|
|
335
|
+
console.error(chalk.dim("Inspect it: hq outposts status"));
|
|
336
|
+
console.error(chalk.dim("Or tear it down first: hq outposts destroy --id <id> --yes"));
|
|
337
|
+
}
|
|
303
338
|
function fail(err) {
|
|
304
339
|
if (err instanceof OutpostHttpError) {
|
|
305
340
|
console.error(chalk.red(err.message));
|
|
@@ -615,7 +650,17 @@ function authGitHubViaVault(deps) {
|
|
|
615
650
|
"unset GITHUB_TOKEN",
|
|
616
651
|
'printf "%s" "$TOKEN" | gh auth login --with-token',
|
|
617
652
|
].join("\n");
|
|
618
|
-
const authed = runBestEffort(deps, "hq", [
|
|
653
|
+
const authed = runBestEffort(deps, "hq", [
|
|
654
|
+
"secrets",
|
|
655
|
+
"--personal",
|
|
656
|
+
"exec",
|
|
657
|
+
"--only",
|
|
658
|
+
"GITHUB_TOKEN",
|
|
659
|
+
"--",
|
|
660
|
+
"bash",
|
|
661
|
+
"-c",
|
|
662
|
+
ghLogin,
|
|
663
|
+
], "GitHub auth via vault");
|
|
619
664
|
if (authed) {
|
|
620
665
|
// Let plain `git clone https://github.com/...` reuse gh's token so private
|
|
621
666
|
// repos don't prompt for a username/password.
|
|
@@ -670,7 +715,15 @@ async function replicaSyncOutpost(opts, deps) {
|
|
|
670
715
|
console.warn("replica-sync: auth refresh failed — skipping this cycle (token likely expired).");
|
|
671
716
|
return;
|
|
672
717
|
}
|
|
673
|
-
runBestEffort(deps, "hq", [
|
|
718
|
+
runBestEffort(deps, "hq", [
|
|
719
|
+
"sync",
|
|
720
|
+
"pull",
|
|
721
|
+
"--personal",
|
|
722
|
+
"--hq-root",
|
|
723
|
+
hqRoot,
|
|
724
|
+
"--on-conflict",
|
|
725
|
+
"keep",
|
|
726
|
+
], "personal vault pull");
|
|
674
727
|
// rescue lays down the core kernel and expects companies/ to exist.
|
|
675
728
|
deps.mkdirp(path.join(hqRoot, "companies"));
|
|
676
729
|
runBestEffort(deps, "hq", ["rescue", "--hq-root", hqRoot, "--yes"], "HQ kernel rescue");
|
|
@@ -722,9 +775,17 @@ export function registerOutpostsCommand(program, selfDeployOverrides = {}) {
|
|
|
722
775
|
.option("--yes", "Confirm the $80/month charge (required to provision)")
|
|
723
776
|
.action(async function (opts) {
|
|
724
777
|
try {
|
|
778
|
+
// Reject an unrecognized runtime rather than resolving it to claude:
|
|
779
|
+
// `--runtime codx` would otherwise hand back a silently Claude box.
|
|
780
|
+
if (opts.runtime !== undefined &&
|
|
781
|
+
opts.runtime !== "claude" &&
|
|
782
|
+
opts.runtime !== "codex") {
|
|
783
|
+
console.error(chalk.red(`Invalid --runtime '${opts.runtime}': must be 'claude' or 'codex'.`));
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
725
786
|
const agentRuntime = opts.runtime === "codex"
|
|
726
787
|
? "codex"
|
|
727
|
-
: opts.runtime
|
|
788
|
+
: opts.runtime === "claude"
|
|
728
789
|
? "claude"
|
|
729
790
|
: undefined;
|
|
730
791
|
let diskSizeGb;
|
|
@@ -764,7 +825,15 @@ export function registerOutpostsCommand(program, selfDeployOverrides = {}) {
|
|
|
764
825
|
if (err instanceof OutpostHttpError &&
|
|
765
826
|
err.status === 402 &&
|
|
766
827
|
err.billing) {
|
|
767
|
-
await
|
|
828
|
+
await surfaceBillingBlocked(token, err.billing, err.message);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
// Already at the per-person cap → say so and name the box they own,
|
|
832
|
+
// not a bare "Conflict".
|
|
833
|
+
if (err instanceof OutpostHttpError &&
|
|
834
|
+
err.status === 409 &&
|
|
835
|
+
err.capped) {
|
|
836
|
+
surfaceOutpostCapped(err.capped);
|
|
768
837
|
process.exit(1);
|
|
769
838
|
}
|
|
770
839
|
throw err;
|