@indigoai-us/hq-cli 5.61.0 → 5.62.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/CHANGELOG.md +7 -0
- package/dist/commands/agents.d.ts +109 -0
- package/dist/commands/agents.js +385 -0
- package/dist/commands/db-migrate.d.ts +6 -0
- package/dist/commands/db-migrate.js +42 -0
- package/dist/commands/db-provision.d.ts +15 -0
- package/dist/commands/db-provision.js +85 -0
- package/dist/commands/db-sql.d.ts +9 -0
- package/dist/commands/db-sql.js +81 -0
- package/dist/commands/db-status.d.ts +7 -0
- package/dist/commands/db-status.js +74 -0
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +23 -0
- package/dist/commands/integrations.d.ts +78 -0
- package/dist/commands/integrations.js +309 -0
- package/dist/commands/members.js +4 -4
- package/dist/commands/outposts.d.ts +60 -0
- package/dist/commands/outposts.js +255 -0
- package/dist/commands/secrets.d.ts +8 -0
- package/dist/commands/secrets.js +23 -8
- package/dist/commands/skill.d.ts +153 -0
- package/dist/commands/skill.js +593 -0
- package/dist/commands/workers.d.ts +48 -0
- package/dist/commands/workers.js +229 -0
- package/dist/lib/db/control-plane.d.ts +45 -0
- package/dist/lib/db/control-plane.js +81 -0
- package/dist/lib/db/local.d.ts +49 -0
- package/dist/lib/db/local.js +106 -0
- package/dist/lib/db/migrate.d.ts +41 -0
- package/dist/lib/db/migrate.js +104 -0
- package/dist/lib/db/paths.d.ts +56 -0
- package/dist/lib/db/paths.js +103 -0
- package/dist/lib/db/remote-engine.d.ts +58 -0
- package/dist/lib/db/remote-engine.js +90 -0
- package/dist/lib/db/remote-sql.d.ts +22 -0
- package/dist/lib/db/remote-sql.js +39 -0
- package/dist/lib/db/sql.d.ts +49 -0
- package/dist/lib/db/sql.js +132 -0
- package/dist/main.js +27 -2
- package/dist/utils/cognito-session.js +3 -3
- package/dist/utils/sandbox-runner-client.js +3 -3
- package/package.json +9 -1
- package/pnpm-workspace.yaml +2 -0
- package/src/commands/agents.test.ts +297 -0
- package/src/commands/agents.ts +561 -0
- package/src/commands/db-migrate.ts +55 -0
- package/src/commands/db-provision.ts +114 -0
- package/src/commands/db-sql.ts +124 -0
- package/src/commands/db-status.ts +108 -0
- package/src/commands/db.ts +26 -0
- package/src/commands/integrations.test.ts +284 -0
- package/src/commands/integrations.ts +438 -0
- package/src/commands/members.ts +2 -2
- package/src/commands/outposts.test.ts +177 -0
- package/src/commands/outposts.ts +338 -0
- package/src/commands/secrets.parse-destination.test.ts +38 -0
- package/src/commands/secrets.test.ts +24 -0
- package/src/commands/secrets.ts +30 -10
- package/src/commands/skill.test.ts +770 -0
- package/src/commands/skill.ts +796 -0
- package/src/commands/workers.test.ts +158 -0
- package/src/commands/workers.ts +298 -0
- package/src/lib/db/control-plane.test.ts +59 -0
- package/src/lib/db/control-plane.ts +113 -0
- package/src/lib/db/local.test.ts +81 -0
- package/src/lib/db/local.ts +148 -0
- package/src/lib/db/migrate.test.ts +133 -0
- package/src/lib/db/migrate.ts +137 -0
- package/src/lib/db/paths.test.ts +112 -0
- package/src/lib/db/paths.ts +128 -0
- package/src/lib/db/remote-engine.test.ts +44 -0
- package/src/lib/db/remote-engine.ts +148 -0
- package/src/lib/db/remote-sql.test.ts +32 -0
- package/src/lib/db/remote-sql.ts +62 -0
- package/src/lib/db/sql.test.ts +106 -0
- package/src/lib/db/sql.ts +192 -0
- package/src/main.ts +31 -0
- package/src/utils/cognito-session.ts +1 -1
- package/src/utils/sandbox-runner-client.ts +1 -1
- package/test/commands/db-tenant-isolation.test.ts +94 -0
- package/test/commands/db.test.ts +85 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq outposts` — manage your personal HQ Outposts (EC2 boxes) from the
|
|
3
|
+
* terminal instead of the web console. Targets the hq-pro `/outpost/*`
|
|
4
|
+
* control plane on `DEFAULT_VAULT_API_URL` via the shared `vaultApiFetch`
|
|
5
|
+
* helper — the same routes the console's outpost panel calls.
|
|
6
|
+
*
|
|
7
|
+
* Outposts are PERSONAL / caller-scoped: hq-pro keys every `/outpost/*` route
|
|
8
|
+
* on the caller's Cognito sub, so there is no `--company`. `--id <outpostId>`
|
|
9
|
+
* selects a specific box (passed as the `outpostId` query param); when omitted
|
|
10
|
+
* hq-pro targets the caller's primary slot.
|
|
11
|
+
*
|
|
12
|
+
* Subcommands:
|
|
13
|
+
* hq outposts list — every Outpost you own (row summaries)
|
|
14
|
+
* hq outposts status [--id] — live detail for one box
|
|
15
|
+
* hq outposts codex-enable [--id] — enable / retry Codex on the box
|
|
16
|
+
* hq outposts login [--id] — request a fresh login URL
|
|
17
|
+
* hq outposts destroy [--id] --yes — tear the box down (destructive; flag-guarded)
|
|
18
|
+
*
|
|
19
|
+
* NOTE: hq-pro exposes no rename or settings-mutation route for Outposts (the
|
|
20
|
+
* web console can't rename them either), so this CLI wraps only the lifecycle
|
|
21
|
+
* and status routes that exist. Renaming an Outpost is not a backend capability.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
!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]="b01c6828-502e-5f7a-b15c-acafef4fa143")}catch(e){}}();
|
|
25
|
+
import chalk from "chalk";
|
|
26
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
27
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
28
|
+
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
29
|
+
export class OutpostHttpError extends Error {
|
|
30
|
+
status;
|
|
31
|
+
step;
|
|
32
|
+
constructor(status, message, step) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "OutpostHttpError";
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.step = step;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Authenticated JSON round-trip against the outpost control plane. Throws
|
|
41
|
+
* `OutpostHttpError` on any non-2xx (never swallows — hq-never-swallow-errors),
|
|
42
|
+
* decoding hq-pro's `{ error | message, step }` envelope for the reason. The
|
|
43
|
+
* `step` is preserved so callers can recognise the `destroy` route's
|
|
44
|
+
* `teardown-incomplete` 409 (which means "retry", not "failed").
|
|
45
|
+
*/
|
|
46
|
+
export async function outpostRequest(opts) {
|
|
47
|
+
const res = await vaultApiFetch(opts);
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const body = (await res.json().catch(() => ({})));
|
|
50
|
+
const message = typeof body.message === "string"
|
|
51
|
+
? body.message
|
|
52
|
+
: typeof body.error === "string"
|
|
53
|
+
? body.error
|
|
54
|
+
: res.statusText;
|
|
55
|
+
throw new OutpostHttpError(res.status, message, body.step);
|
|
56
|
+
}
|
|
57
|
+
return (await res.json());
|
|
58
|
+
}
|
|
59
|
+
export async function listOutposts(token) {
|
|
60
|
+
const data = await outpostRequest({
|
|
61
|
+
token,
|
|
62
|
+
path: "/outpost/list",
|
|
63
|
+
});
|
|
64
|
+
return data.outposts ?? [];
|
|
65
|
+
}
|
|
66
|
+
export async function getOutpostStatus(token, outpostId) {
|
|
67
|
+
return outpostRequest({
|
|
68
|
+
token,
|
|
69
|
+
path: "/outpost/status",
|
|
70
|
+
query: outpostId ? { outpostId } : undefined,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function enableCodex(token, outpostId) {
|
|
74
|
+
return outpostRequest({
|
|
75
|
+
token,
|
|
76
|
+
path: "/outpost/codex/enable",
|
|
77
|
+
method: "POST",
|
|
78
|
+
query: outpostId ? { outpostId } : undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export async function regenerateLoginUrl(token, outpostId) {
|
|
82
|
+
return outpostRequest({
|
|
83
|
+
token,
|
|
84
|
+
path: "/outpost/regenerate-login-url",
|
|
85
|
+
method: "POST",
|
|
86
|
+
query: outpostId ? { outpostId } : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function destroyOutpost(token, outpostId) {
|
|
90
|
+
return outpostRequest({
|
|
91
|
+
token,
|
|
92
|
+
path: "/outpost/destroy",
|
|
93
|
+
method: "POST",
|
|
94
|
+
query: outpostId ? { outpostId } : undefined,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Command registration
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
function fail(err) {
|
|
101
|
+
if (err instanceof OutpostHttpError) {
|
|
102
|
+
console.error(chalk.red(err.message));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
106
|
+
}
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
/** Print a top-level object as `key: value`, JSON-ifying nested values. */
|
|
110
|
+
function printKeyValues(obj) {
|
|
111
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
112
|
+
const rendered = v && typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
113
|
+
console.log(`${chalk.bold(k)}: ${rendered}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function registerOutpostsCommand(program) {
|
|
117
|
+
const outposts = program
|
|
118
|
+
.command("outposts")
|
|
119
|
+
.description("Manage your personal HQ Outposts (EC2 boxes)");
|
|
120
|
+
outposts
|
|
121
|
+
.command("list")
|
|
122
|
+
.description("List every Outpost you own")
|
|
123
|
+
.option("--json", "Emit raw JSON")
|
|
124
|
+
.action(async function (opts) {
|
|
125
|
+
try {
|
|
126
|
+
const token = await ensureCognitoToken();
|
|
127
|
+
const rows = await listOutposts(token);
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (rows.length === 0) {
|
|
133
|
+
console.log(chalk.gray("You don't own any Outposts yet."));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const idW = Math.max(2, ...rows.map((r) => (r.outpostId ?? "").length));
|
|
137
|
+
const stateW = Math.max(5, ...rows.map((r) => (r.state ?? "").length));
|
|
138
|
+
const nameW = Math.max(4, ...rows.map((r) => (r.instanceName ?? "").length));
|
|
139
|
+
const regionW = Math.max(6, ...rows.map((r) => (r.region ?? "").length));
|
|
140
|
+
const rtW = Math.max(7, ...rows.map((r) => (r.agentRuntime ?? "").length));
|
|
141
|
+
console.log(chalk.bold([
|
|
142
|
+
"ID".padEnd(idW),
|
|
143
|
+
"STATE".padEnd(stateW),
|
|
144
|
+
"INSTANCE".padEnd(nameW),
|
|
145
|
+
"REGION".padEnd(regionW),
|
|
146
|
+
"RUNTIME".padEnd(rtW),
|
|
147
|
+
"PLATFORM",
|
|
148
|
+
].join(" ")));
|
|
149
|
+
for (const r of rows) {
|
|
150
|
+
console.log([
|
|
151
|
+
(r.outpostId ?? "").padEnd(idW),
|
|
152
|
+
(r.state ?? "").padEnd(stateW),
|
|
153
|
+
(r.instanceName ?? "").padEnd(nameW),
|
|
154
|
+
(r.region ?? "").padEnd(regionW),
|
|
155
|
+
(r.agentRuntime ?? "").padEnd(rtW),
|
|
156
|
+
r.platform ?? "",
|
|
157
|
+
].join(" "));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
fail(err);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
outposts
|
|
165
|
+
.command("status")
|
|
166
|
+
.description("Show live detail for one Outpost")
|
|
167
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
168
|
+
.option("--json", "Emit raw JSON")
|
|
169
|
+
.action(async function (opts) {
|
|
170
|
+
try {
|
|
171
|
+
const token = await ensureCognitoToken();
|
|
172
|
+
const status = await getOutpostStatus(token, opts.id);
|
|
173
|
+
if (opts.json) {
|
|
174
|
+
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
printKeyValues(status);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
fail(err);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
outposts
|
|
184
|
+
.command("codex-enable")
|
|
185
|
+
.description("Enable (or retry) Codex on an Outpost")
|
|
186
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
187
|
+
.option("--json", "Emit raw JSON")
|
|
188
|
+
.action(async function (opts) {
|
|
189
|
+
try {
|
|
190
|
+
const token = await ensureCognitoToken();
|
|
191
|
+
const result = await enableCodex(token, opts.id);
|
|
192
|
+
if (opts.json) {
|
|
193
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
console.log(chalk.green("Codex enablement requested for the Outpost."));
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
fail(err);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
outposts
|
|
203
|
+
.command("login")
|
|
204
|
+
.description("Request a fresh login URL for an Outpost")
|
|
205
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
206
|
+
.option("--json", "Emit raw JSON")
|
|
207
|
+
.action(async function (opts) {
|
|
208
|
+
try {
|
|
209
|
+
const token = await ensureCognitoToken();
|
|
210
|
+
const result = await regenerateLoginUrl(token, opts.id);
|
|
211
|
+
if (opts.json) {
|
|
212
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
console.log(chalk.green("Login-URL regeneration requested. The box mints a fresh URL shortly — " +
|
|
216
|
+
"check `hq outposts status` to pick it up."));
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
fail(err);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
outposts
|
|
223
|
+
.command("destroy")
|
|
224
|
+
.description("Tear down (permanently destroy) an Outpost")
|
|
225
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
226
|
+
.option("--yes", "Confirm the irreversible teardown (required)")
|
|
227
|
+
.action(async function (opts) {
|
|
228
|
+
const target = opts.id ?? "your primary Outpost";
|
|
229
|
+
if (!opts.yes) {
|
|
230
|
+
console.error(chalk.yellow(`This will permanently destroy ${target} and delete its cloud resources. ` +
|
|
231
|
+
`This cannot be undone.\n` +
|
|
232
|
+
`Re-run with --yes to confirm: hq outposts destroy${opts.id ? ` --id ${opts.id}` : ""} --yes`));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const token = await ensureCognitoToken();
|
|
237
|
+
await destroyOutpost(token, opts.id);
|
|
238
|
+
console.log(chalk.green(`Destroyed ${target}.`));
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
// A 409 teardown-incomplete is not a failure — the gateway's 30s cap
|
|
242
|
+
// fired while the Lambda keeps working. The row is preserved and the
|
|
243
|
+
// operation is idempotent, so tell the caller to retry.
|
|
244
|
+
if (err instanceof OutpostHttpError &&
|
|
245
|
+
(err.status === 409 || err.step === "teardown-incomplete")) {
|
|
246
|
+
console.log(chalk.yellow(`Teardown still in progress for ${target} — this is expected for large boxes. ` +
|
|
247
|
+
`Re-run the same destroy command to finish (it's idempotent).`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
fail(err);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=outposts.js.map
|
|
255
|
+
//# debugId=b01c6828-502e-5f7a-b15c-acafef4fa143
|
|
@@ -41,6 +41,14 @@ export interface SecretInjectionRecipe {
|
|
|
41
41
|
scheme: "raw" | "bearer";
|
|
42
42
|
extraHeaders?: Record<string, string>;
|
|
43
43
|
}
|
|
44
|
+
export declare function parseDestinationUrl(raw: string): {
|
|
45
|
+
ok: true;
|
|
46
|
+
url: string;
|
|
47
|
+
hostname: string;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
};
|
|
51
|
+
export declare function collectSecretNames(value: string, previous?: string[]): string[];
|
|
44
52
|
export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
|
|
45
53
|
export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage): Promise<Map<string, string>>;
|
|
46
54
|
export declare function registerSecretsCommand(program: Command): void;
|
package/dist/commands/secrets.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]="136bd272-0091-5fce-9a93-a2a375f98b38")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -192,7 +192,7 @@ function parseAuthStyle(authStyle) {
|
|
|
192
192
|
// (src/vault-service/handlers/secrets.ts) so a malformed URL is caught
|
|
193
193
|
// locally with an actionable message rather than a round trip — the server
|
|
194
194
|
// re-validates and remains authoritative.
|
|
195
|
-
function parseDestinationUrl(raw) {
|
|
195
|
+
export function parseDestinationUrl(raw) {
|
|
196
196
|
let parsed;
|
|
197
197
|
try {
|
|
198
198
|
parsed = new URL(raw);
|
|
@@ -209,6 +209,13 @@ function parseDestinationUrl(raw) {
|
|
|
209
209
|
console.error(chalk.red(`Invalid --destination '${raw}': missing hostname`));
|
|
210
210
|
return { ok: false };
|
|
211
211
|
}
|
|
212
|
+
// Reject an embedded `user:pass@host` credential segment explicitly rather
|
|
213
|
+
// than letting `new URL()` silently drop it (mirrors hq-pro's authoritative
|
|
214
|
+
// `validateDestinations`; the server re-validates and remains authoritative).
|
|
215
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
216
|
+
console.error(chalk.red(`Invalid --destination '${raw}': must not contain embedded userinfo (user:pass@) credentials`));
|
|
217
|
+
return { ok: false };
|
|
218
|
+
}
|
|
212
219
|
if ((parsed.pathname !== "" && parsed.pathname !== "/") ||
|
|
213
220
|
parsed.search !== "" ||
|
|
214
221
|
parsed.hash !== "") {
|
|
@@ -252,8 +259,16 @@ async function buildSecretUsage(channel, scriptPath, scriptId, attestationLevel
|
|
|
252
259
|
},
|
|
253
260
|
};
|
|
254
261
|
}
|
|
262
|
+
// Commander collector for `--only`: accumulates across REPEATED flags instead
|
|
263
|
+
// of the last one silently winning, and each value may still be
|
|
264
|
+
// comma-separated. So `--only A,B`, `--only A --only B`, and
|
|
265
|
+
// `--only A,B --only C` all resolve to the full list.
|
|
266
|
+
export function collectSecretNames(value, previous) {
|
|
267
|
+
const parsed = value.split(",").map((k) => k.trim()).filter(Boolean);
|
|
268
|
+
return (previous ?? []).concat(parsed);
|
|
269
|
+
}
|
|
255
270
|
function parseSecretNameList(input) {
|
|
256
|
-
const keys = input
|
|
271
|
+
const keys = input ?? [];
|
|
257
272
|
if (keys.length === 0) {
|
|
258
273
|
console.error(chalk.red("Error: --only requires at least one secret name."));
|
|
259
274
|
process.exit(1);
|
|
@@ -997,11 +1012,11 @@ export function registerSecretsCommand(program) {
|
|
|
997
1012
|
.description("Run a command in the hosted sandbox with named secrets injected as env vars; open egress, secrets never touch this machine")
|
|
998
1013
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
999
1014
|
.option("--personal", "Operate on the caller's personal vault (no sharing)")
|
|
1000
|
-
.option("--only <keys>", "
|
|
1015
|
+
.option("--only <keys>", "Secret names to inject (comma-separated; may be repeated) (required)", collectSecretNames)
|
|
1001
1016
|
.allowUnknownOption(true)
|
|
1002
1017
|
.action(async (opts, cmd) => {
|
|
1003
1018
|
try {
|
|
1004
|
-
if (!opts.only || opts.only.
|
|
1019
|
+
if (!opts.only || opts.only.length === 0) {
|
|
1005
1020
|
console.error(chalk.red("Error: --only is required and must name at least one secret."));
|
|
1006
1021
|
process.exit(1);
|
|
1007
1022
|
}
|
|
@@ -1044,7 +1059,7 @@ export function registerSecretsCommand(program) {
|
|
|
1044
1059
|
secrets
|
|
1045
1060
|
.command("exec")
|
|
1046
1061
|
.description("Run a command with secrets injected as env vars")
|
|
1047
|
-
.requiredOption("--only <keys>", "
|
|
1062
|
+
.requiredOption("--only <keys>", "Secret names to inject (comma-separated; may be repeated) (required)", collectSecretNames)
|
|
1048
1063
|
.option("--script <path>", "Attach local script identity for script-locked secrets")
|
|
1049
1064
|
.allowUnknownOption(true)
|
|
1050
1065
|
.action(async (_opts, cmd) => {
|
|
@@ -1100,7 +1115,7 @@ export function registerSecretsCommand(program) {
|
|
|
1100
1115
|
secrets
|
|
1101
1116
|
.command("env")
|
|
1102
1117
|
.description("Print 'export KEY=VALUE' lines suitable for: source <(hq secrets env --only K1,K2)")
|
|
1103
|
-
.requiredOption("--only <keys>", "
|
|
1118
|
+
.requiredOption("--only <keys>", "Secret names to print (comma-separated; may be repeated) (required)", collectSecretNames)
|
|
1104
1119
|
.option("--script <path>", "Attach local script identity for script-locked secrets")
|
|
1105
1120
|
.action(async (opts) => {
|
|
1106
1121
|
try {
|
|
@@ -1382,4 +1397,4 @@ export function registerSecretsCommand(program) {
|
|
|
1382
1397
|
});
|
|
1383
1398
|
}
|
|
1384
1399
|
//# sourceMappingURL=secrets.js.map
|
|
1385
|
-
//# debugId=
|
|
1400
|
+
//# debugId=136bd272-0091-5fce-9a93-a2a375f98b38
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq skill` subcommand group (US-017).
|
|
3
|
+
*
|
|
4
|
+
* Terminal / scriptable access to the skill collaboration loop:
|
|
5
|
+
*
|
|
6
|
+
* hq skill suggest <uid|path> Propose a change to a skill from working edits.
|
|
7
|
+
* hq skill list-suggestions Show the review inbox (suggestions on skills you own).
|
|
8
|
+
* hq skill review <sgn_…> Accept (merge) or decline a pending suggestion.
|
|
9
|
+
*
|
|
10
|
+
* This is a THIN front-end over the SAME wired hq-pro routes the MCP surface
|
|
11
|
+
* (US-007) and the console merge path (US-009) use — there is NO forked
|
|
12
|
+
* suggestion or merge logic here. The CLI reads the local working SKILL.md,
|
|
13
|
+
* computes the proposed content (+ its base for a diff), and POSTs to:
|
|
14
|
+
*
|
|
15
|
+
* CREATE POST /v1/files/skills/company/{slug}/{skillUid}/suggestions
|
|
16
|
+
* LIST POST /v1/files/skills/company/{slug}/suggestions/list
|
|
17
|
+
* ACCEPT POST /v1/files/skills/company/{slug}/{skillUid}/suggestions/{id}/accept
|
|
18
|
+
* DECLINE POST /v1/files/skills/company/{slug}/{skillUid}/suggestions/{id}/decline
|
|
19
|
+
*
|
|
20
|
+
* The skill routes are keyed on the company SLUG (path param, resolved
|
|
21
|
+
* server-side via findEntityBySlug) — NOT the companyUid the vault/ACL routes
|
|
22
|
+
* use — so this module resolves a slug (from `--company` or the active company)
|
|
23
|
+
* and passes it straight through.
|
|
24
|
+
*
|
|
25
|
+
* Lock semantics (AC3): a suggest against a skill the caller cannot WRITE still
|
|
26
|
+
* SUCCEEDS as a proposal. The CREATE route is MEMBER-gated (never write-gated),
|
|
27
|
+
* so this command performs NO client-side lock/permission pre-check — it always
|
|
28
|
+
* posts and renders whatever the server returns. A locked-out member lands a
|
|
29
|
+
* suggestion, never a hard permission error.
|
|
30
|
+
*
|
|
31
|
+
* Attribution (AC4): the invoking identity (from the Cognito JWT) is the server-
|
|
32
|
+
* derived `authorPersonUid`; the CLI never sends an author. An optional
|
|
33
|
+
* `--note` rides as `authorNote` (the change's rationale / failure context).
|
|
34
|
+
*/
|
|
35
|
+
import { Command } from "commander";
|
|
36
|
+
/**
|
|
37
|
+
* A `skl_…` argument to `suggest` is a skill UID (resolve the local file from
|
|
38
|
+
* it); anything else is a filesystem path. Lenient on the suffix (the strict
|
|
39
|
+
* `skl_<ulid>` shape is `isSkillUid` on the server) so a hand-typed / fixture
|
|
40
|
+
* uid still routes to the uid branch.
|
|
41
|
+
*/
|
|
42
|
+
export declare const SKILL_UID_PATTERN: RegExp;
|
|
43
|
+
/** A `sgn_…` suggestion id — the `review` target and the LIST row key. */
|
|
44
|
+
export declare const SUGGESTION_ID_PATTERN: RegExp;
|
|
45
|
+
/**
|
|
46
|
+
* Read the top-level `skill_uid` from a SKILL.md's YAML frontmatter. Mirrors
|
|
47
|
+
* hq-pro's `parseSkillFrontmatter` (block extraction → YAML parse → read the
|
|
48
|
+
* top-level `skill_uid` string). Returns `undefined` when there is no
|
|
49
|
+
* frontmatter, it fails to parse, or `skill_uid` is absent / not a `skl_…`
|
|
50
|
+
* string. Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseSkillUid(md: string): string | undefined;
|
|
53
|
+
/** sha256 hex of a string — the base-version fingerprint the server records (AC2). */
|
|
54
|
+
export declare function sha256Hex(content: string): string;
|
|
55
|
+
export interface SuggestionCreateBody {
|
|
56
|
+
proposedContent: string;
|
|
57
|
+
baseContent?: string;
|
|
58
|
+
baseContentHash?: string;
|
|
59
|
+
authorNote?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the CREATE request body from the resolved proposed content and an
|
|
63
|
+
* optional base. Mirrors the server's two payload shapes:
|
|
64
|
+
* - `baseContent` present → the server derives the unified diff + base hash
|
|
65
|
+
* (the diff-by-default path). A no-op (base === proposed) is rejected here
|
|
66
|
+
* rather than round-tripped to an EmptySuggestion 400.
|
|
67
|
+
* - no base → a `full-file` proposal; the server REQUIRES a `baseContentHash`,
|
|
68
|
+
* so we fingerprint the proposed content (a "here is my whole file" proposal
|
|
69
|
+
* with no base to diff against).
|
|
70
|
+
* An empty / whitespace-only note is dropped (kept absent, not blank).
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildSuggestionCreateBody(input: {
|
|
73
|
+
proposedContent: string;
|
|
74
|
+
baseContent?: string;
|
|
75
|
+
note?: string;
|
|
76
|
+
}): SuggestionCreateBody;
|
|
77
|
+
/**
|
|
78
|
+
* Map an hq-pro skill route error to a single user-facing line. Pure so the
|
|
79
|
+
* status → copy mapping is unit-tested independently of the network. Prefers the
|
|
80
|
+
* server's own `error` / `message` (they carry the actionable specifics — e.g.
|
|
81
|
+
* "Skill not found", "You need write access to this skill…").
|
|
82
|
+
*/
|
|
83
|
+
export declare function mapSkillError(status: number, body: Record<string, unknown>): string;
|
|
84
|
+
/** LIST inbox row shape returned by `suggestionToWire` on the server. */
|
|
85
|
+
export interface SuggestionRow {
|
|
86
|
+
suggestionId: string;
|
|
87
|
+
skillUid: string;
|
|
88
|
+
authorPersonUid: string;
|
|
89
|
+
status: string;
|
|
90
|
+
baseChanged: boolean;
|
|
91
|
+
presentation?: string;
|
|
92
|
+
unifiedDiff?: string;
|
|
93
|
+
fullContent?: string;
|
|
94
|
+
baseContentHash?: string;
|
|
95
|
+
currentContentHash?: string;
|
|
96
|
+
authorNote?: string;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
path: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Render the review inbox as a table (one row per suggestion), optionally
|
|
102
|
+
* printing each suggestion's unified diff (or full proposed file, when the base
|
|
103
|
+
* drifted / the proposal is full-file) beneath its row. Pure → snapshot-testable.
|
|
104
|
+
*/
|
|
105
|
+
export declare function formatSuggestionsList(suggestions: SuggestionRow[], opts?: {
|
|
106
|
+
showDiff?: boolean;
|
|
107
|
+
}): string;
|
|
108
|
+
/** Read `.hq/config.json`'s `activeCompany` (mirrors signals/sources). */
|
|
109
|
+
export declare function readActiveCompanySlug(hqRoot: string): string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* The skill routes are keyed on the company SLUG. Precedence: explicit
|
|
112
|
+
* `--company` → `.hq/config.json` activeCompany. Throws with actionable copy
|
|
113
|
+
* when neither is available.
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveCompanySlug(flag: string | undefined, hqRoot?: string): string;
|
|
116
|
+
export interface ResolvedSkillTarget {
|
|
117
|
+
/** Absolute path to the SKILL.md. */
|
|
118
|
+
filePath: string;
|
|
119
|
+
/** The `skl_…` uid read from its frontmatter (or the uid arg). */
|
|
120
|
+
skillUid: string;
|
|
121
|
+
/** The working-tree SKILL.md content — the proposed content. */
|
|
122
|
+
content: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Recursively find a SKILL.md whose frontmatter `skill_uid` equals `uid`,
|
|
126
|
+
* searching each root breadth-first with a bounded depth (skips VCS / build /
|
|
127
|
+
* dependency dirs). Returns the first match's absolute path, or null.
|
|
128
|
+
*/
|
|
129
|
+
export declare function findSkillFileByUid(roots: string[], uid: string, maxDepth?: number): string | null;
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a `suggest` target (a `skl_…` uid OR a filesystem path) to the local
|
|
132
|
+
* SKILL.md, its uid, and its content. A uid is resolved by scanning the company
|
|
133
|
+
* skills dir and the cwd; a path is read directly (a directory → its SKILL.md),
|
|
134
|
+
* with the uid read from the file's frontmatter.
|
|
135
|
+
*/
|
|
136
|
+
export declare function resolveSkillTarget(target: string, deps: {
|
|
137
|
+
cwd: string;
|
|
138
|
+
hqRoot: string;
|
|
139
|
+
companySlug: string;
|
|
140
|
+
}): ResolvedSkillTarget;
|
|
141
|
+
/**
|
|
142
|
+
* Read the committed (HEAD) version of a file from its git repo — the diff base
|
|
143
|
+
* for "propose my working changes". Returns null when the file is untracked, not
|
|
144
|
+
* in a repo, or git is unavailable (the caller then falls back to full-file, or
|
|
145
|
+
* errors under `--diff`). Never throws.
|
|
146
|
+
*/
|
|
147
|
+
export declare function readGitBase(filePath: string): Promise<string | null>;
|
|
148
|
+
/** Injectable git-base seam so `suggest` is testable without a real repo. */
|
|
149
|
+
export type GitBaseReader = (filePath: string) => Promise<string | null>;
|
|
150
|
+
export declare function registerSkillCommand(program: Command, deps?: {
|
|
151
|
+
gitBase?: GitBaseReader;
|
|
152
|
+
}): Command;
|
|
153
|
+
//# sourceMappingURL=skill.d.ts.map
|