@indigoai-us/hq-cli 5.63.0 → 5.64.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.
|
@@ -73,5 +73,26 @@ export declare function getOutpostStatus(token: string, outpostId?: string): Pro
|
|
|
73
73
|
export declare function enableCodex(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|
|
74
74
|
export declare function regenerateLoginUrl(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|
|
75
75
|
export declare function destroyOutpost(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|
|
76
|
+
/** Result of `POST /outpost/exec` — a terminal SSM invocation on the box. */
|
|
77
|
+
export interface OutpostExecResult {
|
|
78
|
+
ok: true;
|
|
79
|
+
outpostId: string;
|
|
80
|
+
instanceId: string;
|
|
81
|
+
commandId: string;
|
|
82
|
+
/** SSM invocation status (Success | Failed | Cancelled). */
|
|
83
|
+
status: string;
|
|
84
|
+
/** Remote process exit code, or null when SSM reported none. */
|
|
85
|
+
exitCode: number | null;
|
|
86
|
+
stdout: string;
|
|
87
|
+
stderr: string;
|
|
88
|
+
/** True when SSM clipped stdout/stderr at its inline output limit. */
|
|
89
|
+
truncated: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run a one-shot shell command on the caller's Outpost via `POST /outpost/exec`
|
|
93
|
+
* (server-brokered SSM — no SSH). Throws `OutpostHttpError` on a non-2xx, whose
|
|
94
|
+
* `step` distinguishes not-ready / platform-unsupported / timeout / ssm.
|
|
95
|
+
*/
|
|
96
|
+
export declare function execOutpost(token: string, command: string, outpostId?: string): Promise<OutpostExecResult>;
|
|
76
97
|
export declare function registerOutpostsCommand(program: Command): void;
|
|
77
98
|
//# sourceMappingURL=outposts.d.ts.map
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* and status routes that exist. Renaming an Outpost is not a backend capability.
|
|
22
22
|
*/
|
|
23
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]="
|
|
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]="067e20d6-0132-565a-8a22-9c27520669d2")}catch(e){}}();
|
|
25
25
|
import chalk from "chalk";
|
|
26
26
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
27
27
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -119,6 +119,20 @@ export async function destroyOutpost(token, outpostId) {
|
|
|
119
119
|
query: outpostId ? { outpostId } : undefined,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Run a one-shot shell command on the caller's Outpost via `POST /outpost/exec`
|
|
124
|
+
* (server-brokered SSM — no SSH). Throws `OutpostHttpError` on a non-2xx, whose
|
|
125
|
+
* `step` distinguishes not-ready / platform-unsupported / timeout / ssm.
|
|
126
|
+
*/
|
|
127
|
+
export async function execOutpost(token, command, outpostId) {
|
|
128
|
+
return outpostRequest({
|
|
129
|
+
token,
|
|
130
|
+
path: "/outpost/exec",
|
|
131
|
+
method: "POST",
|
|
132
|
+
body: { command },
|
|
133
|
+
query: outpostId ? { outpostId } : undefined,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
122
136
|
// ---------------------------------------------------------------------------
|
|
123
137
|
// Command registration
|
|
124
138
|
// ---------------------------------------------------------------------------
|
|
@@ -267,6 +281,45 @@ export function registerOutpostsCommand(program) {
|
|
|
267
281
|
fail(err);
|
|
268
282
|
}
|
|
269
283
|
});
|
|
284
|
+
outposts
|
|
285
|
+
.command("exec <command...>")
|
|
286
|
+
.description("Run a shell command on an Outpost and print its output (use -- before flags meant for the remote command)")
|
|
287
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
288
|
+
.option("--json", "Emit raw JSON")
|
|
289
|
+
.action(async function (commandParts, opts) {
|
|
290
|
+
try {
|
|
291
|
+
const command = commandParts.join(" ").trim();
|
|
292
|
+
if (!command) {
|
|
293
|
+
console.error(chalk.red("No command given. Usage: hq outposts exec -- <command>"));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
const token = await ensureCognitoToken();
|
|
297
|
+
const result = await execOutpost(token, command, opts.id);
|
|
298
|
+
if (opts.json) {
|
|
299
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Stream the remote streams to ours so the command feels local, then
|
|
303
|
+
// exit with the remote exit code.
|
|
304
|
+
if (result.stdout)
|
|
305
|
+
process.stdout.write(result.stdout);
|
|
306
|
+
if (result.stderr)
|
|
307
|
+
process.stderr.write(result.stderr);
|
|
308
|
+
if (result.truncated) {
|
|
309
|
+
console.error(chalk.yellow("(output truncated by SSM's inline limit — redirect to a file on the box for full output)"));
|
|
310
|
+
}
|
|
311
|
+
if (result.status !== "Success" && result.exitCode === null) {
|
|
312
|
+
console.error(chalk.yellow(`(command ended with SSM status: ${result.status})`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Propagate the remote exit code so `hq outposts exec -- false` exits 1.
|
|
316
|
+
process.exitCode =
|
|
317
|
+
typeof result.exitCode === "number" ? result.exitCode : 0;
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
fail(err);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
270
323
|
outposts
|
|
271
324
|
.command("codex-enable")
|
|
272
325
|
.description("Enable (or retry) Codex on an Outpost")
|
|
@@ -339,4 +392,4 @@ export function registerOutpostsCommand(program) {
|
|
|
339
392
|
});
|
|
340
393
|
}
|
|
341
394
|
//# sourceMappingURL=outposts.js.map
|
|
342
|
-
//# debugId=
|
|
395
|
+
//# debugId=067e20d6-0132-565a-8a22-9c27520669d2
|
package/package.json
CHANGED
|
@@ -245,3 +245,112 @@ describe("hq outposts provision (billing gate)", () => {
|
|
|
245
245
|
expect(printed).toContain("https://checkout.stripe.com/outpost");
|
|
246
246
|
});
|
|
247
247
|
});
|
|
248
|
+
|
|
249
|
+
describe("hq outposts exec", () => {
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
process.exitCode = undefined;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("POSTs /outpost/exec with the joined command and streams stdout", async () => {
|
|
255
|
+
const stdoutSpy = vi
|
|
256
|
+
.spyOn(process.stdout, "write")
|
|
257
|
+
.mockImplementation(() => true);
|
|
258
|
+
fetchSpy.mockResolvedValueOnce(
|
|
259
|
+
jsonResponse(200, {
|
|
260
|
+
ok: true,
|
|
261
|
+
outpostId: "primary",
|
|
262
|
+
instanceId: "i-abc",
|
|
263
|
+
commandId: "cmd-1",
|
|
264
|
+
status: "Success",
|
|
265
|
+
exitCode: 0,
|
|
266
|
+
stdout: "hello world\n",
|
|
267
|
+
stderr: "",
|
|
268
|
+
truncated: false,
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
await run(["outposts", "exec", "echo", "hello", "world"]);
|
|
273
|
+
|
|
274
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
275
|
+
expect(String(url)).toContain("/outpost/exec");
|
|
276
|
+
expect(init?.method).toBe("POST");
|
|
277
|
+
expect(JSON.parse(init?.body as string)).toEqual({ command: "echo hello world" });
|
|
278
|
+
const printed = stdoutSpy.mock.calls.map((c) => String(c[0])).join("");
|
|
279
|
+
expect(printed).toContain("hello world\n");
|
|
280
|
+
expect(process.exitCode).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("propagates a non-zero remote exit code", async () => {
|
|
284
|
+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
285
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
286
|
+
fetchSpy.mockResolvedValueOnce(
|
|
287
|
+
jsonResponse(200, {
|
|
288
|
+
ok: true,
|
|
289
|
+
outpostId: "primary",
|
|
290
|
+
instanceId: "i-abc",
|
|
291
|
+
commandId: "cmd-2",
|
|
292
|
+
status: "Failed",
|
|
293
|
+
exitCode: 2,
|
|
294
|
+
stdout: "",
|
|
295
|
+
stderr: "nope\n",
|
|
296
|
+
truncated: false,
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
await run(["outposts", "exec", "exit", "2"]);
|
|
300
|
+
expect(process.exitCode).toBe(2);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("passes --id through as the outpostId query param", async () => {
|
|
304
|
+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
305
|
+
fetchSpy.mockResolvedValueOnce(
|
|
306
|
+
jsonResponse(200, {
|
|
307
|
+
ok: true,
|
|
308
|
+
outpostId: "2",
|
|
309
|
+
instanceId: "i-2",
|
|
310
|
+
commandId: "c",
|
|
311
|
+
status: "Success",
|
|
312
|
+
exitCode: 0,
|
|
313
|
+
stdout: "",
|
|
314
|
+
stderr: "",
|
|
315
|
+
truncated: false,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
await run(["outposts", "exec", "--id", "2", "uptime"]);
|
|
319
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("outpostId=2");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("exits 1 and surfaces the server message on a non-2xx (e.g. not-ready)", async () => {
|
|
323
|
+
fetchSpy.mockResolvedValueOnce(
|
|
324
|
+
jsonResponse(409, {
|
|
325
|
+
error: true,
|
|
326
|
+
step: "not-ready",
|
|
327
|
+
message: 'outpost is "provisioning", not ready — it must be running to exec',
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
await expect(
|
|
331
|
+
run(["outposts", "exec", "echo", "hi"]),
|
|
332
|
+
).rejects.toThrow("process.exit(1)");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("emits raw JSON with --json", async () => {
|
|
336
|
+
const stdoutSpy = vi
|
|
337
|
+
.spyOn(process.stdout, "write")
|
|
338
|
+
.mockImplementation(() => true);
|
|
339
|
+
fetchSpy.mockResolvedValueOnce(
|
|
340
|
+
jsonResponse(200, {
|
|
341
|
+
ok: true,
|
|
342
|
+
outpostId: "primary",
|
|
343
|
+
instanceId: "i-abc",
|
|
344
|
+
commandId: "cmd-3",
|
|
345
|
+
status: "Success",
|
|
346
|
+
exitCode: 0,
|
|
347
|
+
stdout: "x\n",
|
|
348
|
+
stderr: "",
|
|
349
|
+
truncated: false,
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
await run(["outposts", "exec", "--json", "echo", "x"]);
|
|
353
|
+
const printed = stdoutSpy.mock.calls.map((c) => String(c[0])).join("");
|
|
354
|
+
expect(printed).toContain('"commandId": "cmd-3"');
|
|
355
|
+
});
|
|
356
|
+
});
|
package/src/commands/outposts.ts
CHANGED
|
@@ -187,6 +187,41 @@ export async function destroyOutpost(
|
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** Result of `POST /outpost/exec` — a terminal SSM invocation on the box. */
|
|
191
|
+
export interface OutpostExecResult {
|
|
192
|
+
ok: true;
|
|
193
|
+
outpostId: string;
|
|
194
|
+
instanceId: string;
|
|
195
|
+
commandId: string;
|
|
196
|
+
/** SSM invocation status (Success | Failed | Cancelled). */
|
|
197
|
+
status: string;
|
|
198
|
+
/** Remote process exit code, or null when SSM reported none. */
|
|
199
|
+
exitCode: number | null;
|
|
200
|
+
stdout: string;
|
|
201
|
+
stderr: string;
|
|
202
|
+
/** True when SSM clipped stdout/stderr at its inline output limit. */
|
|
203
|
+
truncated: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Run a one-shot shell command on the caller's Outpost via `POST /outpost/exec`
|
|
208
|
+
* (server-brokered SSM — no SSH). Throws `OutpostHttpError` on a non-2xx, whose
|
|
209
|
+
* `step` distinguishes not-ready / platform-unsupported / timeout / ssm.
|
|
210
|
+
*/
|
|
211
|
+
export async function execOutpost(
|
|
212
|
+
token: string,
|
|
213
|
+
command: string,
|
|
214
|
+
outpostId?: string,
|
|
215
|
+
): Promise<OutpostExecResult> {
|
|
216
|
+
return outpostRequest({
|
|
217
|
+
token,
|
|
218
|
+
path: "/outpost/exec",
|
|
219
|
+
method: "POST",
|
|
220
|
+
body: { command },
|
|
221
|
+
query: outpostId ? { outpostId } : undefined,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
190
225
|
// ---------------------------------------------------------------------------
|
|
191
226
|
// Command registration
|
|
192
227
|
// ---------------------------------------------------------------------------
|
|
@@ -369,6 +404,51 @@ export function registerOutpostsCommand(program: Command): void {
|
|
|
369
404
|
}
|
|
370
405
|
});
|
|
371
406
|
|
|
407
|
+
outposts
|
|
408
|
+
.command("exec <command...>")
|
|
409
|
+
.description(
|
|
410
|
+
"Run a shell command on an Outpost and print its output (use -- before flags meant for the remote command)",
|
|
411
|
+
)
|
|
412
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
413
|
+
.option("--json", "Emit raw JSON")
|
|
414
|
+
.action(async function (
|
|
415
|
+
this: Command,
|
|
416
|
+
commandParts: string[],
|
|
417
|
+
opts: { id?: string; json?: boolean },
|
|
418
|
+
) {
|
|
419
|
+
try {
|
|
420
|
+
const command = commandParts.join(" ").trim();
|
|
421
|
+
if (!command) {
|
|
422
|
+
console.error(chalk.red("No command given. Usage: hq outposts exec -- <command>"));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const token = await ensureCognitoToken();
|
|
426
|
+
const result = await execOutpost(token, command, opts.id);
|
|
427
|
+
|
|
428
|
+
if (opts.json) {
|
|
429
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
430
|
+
} else {
|
|
431
|
+
// Stream the remote streams to ours so the command feels local, then
|
|
432
|
+
// exit with the remote exit code.
|
|
433
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
434
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
435
|
+
if (result.truncated) {
|
|
436
|
+
console.error(
|
|
437
|
+
chalk.yellow("(output truncated by SSM's inline limit — redirect to a file on the box for full output)"),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (result.status !== "Success" && result.exitCode === null) {
|
|
441
|
+
console.error(chalk.yellow(`(command ended with SSM status: ${result.status})`));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Propagate the remote exit code so `hq outposts exec -- false` exits 1.
|
|
445
|
+
process.exitCode =
|
|
446
|
+
typeof result.exitCode === "number" ? result.exitCode : 0;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
fail(err);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
372
452
|
outposts
|
|
373
453
|
.command("codex-enable")
|
|
374
454
|
.description("Enable (or retry) Codex on an Outpost")
|