@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]="d99c091a-e57f-55a6-a666-fdc339c83aab")}catch(e){}}();
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=d99c091a-e57f-55a6-a666-fdc339c83aab
395
+ //# debugId=067e20d6-0132-565a-8a22-9c27520669d2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.63.0",
3
+ "version": "5.64.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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
+ });
@@ -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")