@indigoai-us/hq-cli 5.60.0 → 5.62.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.
Files changed (98) hide show
  1. package/dist/commands/agents.d.ts +109 -0
  2. package/dist/commands/agents.js +385 -0
  3. package/dist/commands/db-migrate.d.ts +6 -0
  4. package/dist/commands/db-migrate.js +42 -0
  5. package/dist/commands/db-provision.d.ts +15 -0
  6. package/dist/commands/db-provision.js +78 -0
  7. package/dist/commands/db-sql.d.ts +9 -0
  8. package/dist/commands/db-sql.js +81 -0
  9. package/dist/commands/db-status.d.ts +7 -0
  10. package/dist/commands/db-status.js +70 -0
  11. package/dist/commands/db.d.ts +9 -0
  12. package/dist/commands/db.js +23 -0
  13. package/dist/commands/integrations.d.ts +78 -0
  14. package/dist/commands/integrations.js +309 -0
  15. package/dist/commands/members.js +4 -4
  16. package/dist/commands/outposts.d.ts +60 -0
  17. package/dist/commands/outposts.js +255 -0
  18. package/dist/commands/pack-install.d.ts +7 -1
  19. package/dist/commands/pack-install.js +86 -15
  20. package/dist/commands/packs.d.ts +2 -1
  21. package/dist/commands/packs.js +13 -8
  22. package/dist/commands/secrets.d.ts +13 -0
  23. package/dist/commands/secrets.js +149 -10
  24. package/dist/commands/skill.d.ts +153 -0
  25. package/dist/commands/skill.js +593 -0
  26. package/dist/commands/workers.d.ts +48 -0
  27. package/dist/commands/workers.js +229 -0
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.js +14 -240
  30. package/dist/lib/db/control-plane.d.ts +45 -0
  31. package/dist/lib/db/control-plane.js +81 -0
  32. package/dist/lib/db/local.d.ts +49 -0
  33. package/dist/lib/db/local.js +106 -0
  34. package/dist/lib/db/migrate.d.ts +41 -0
  35. package/dist/lib/db/migrate.js +104 -0
  36. package/dist/lib/db/paths.d.ts +56 -0
  37. package/dist/lib/db/paths.js +103 -0
  38. package/dist/lib/db/remote-engine.d.ts +58 -0
  39. package/dist/lib/db/remote-engine.js +90 -0
  40. package/dist/lib/db/remote-sql.d.ts +22 -0
  41. package/dist/lib/db/remote-sql.js +39 -0
  42. package/dist/lib/db/sql.d.ts +49 -0
  43. package/dist/lib/db/sql.js +132 -0
  44. package/dist/main.d.ts +7 -0
  45. package/dist/main.js +272 -0
  46. package/dist/utils/cognito-session.js +3 -3
  47. package/dist/utils/sandbox-runner-client.d.ts +13 -0
  48. package/dist/utils/sandbox-runner-client.js +83 -6
  49. package/dist/utils/version-check.d.ts +6 -0
  50. package/dist/utils/version-check.js +78 -2
  51. package/package.json +9 -1
  52. package/pnpm-workspace.yaml +2 -0
  53. package/src/commands/agents.test.ts +297 -0
  54. package/src/commands/agents.ts +561 -0
  55. package/src/commands/db-migrate.ts +55 -0
  56. package/src/commands/db-provision.ts +102 -0
  57. package/src/commands/db-sql.ts +124 -0
  58. package/src/commands/db-status.ts +100 -0
  59. package/src/commands/db.ts +26 -0
  60. package/src/commands/integrations.test.ts +284 -0
  61. package/src/commands/integrations.ts +438 -0
  62. package/src/commands/members.ts +2 -2
  63. package/src/commands/outposts.test.ts +177 -0
  64. package/src/commands/outposts.ts +338 -0
  65. package/src/commands/pack-install.ts +115 -18
  66. package/src/commands/pack-update-cache.test.ts +149 -0
  67. package/src/commands/packs.ts +28 -7
  68. package/src/commands/secrets.parse-destination.test.ts +38 -0
  69. package/src/commands/secrets.test.ts +342 -0
  70. package/src/commands/secrets.ts +227 -13
  71. package/src/commands/skill.test.ts +770 -0
  72. package/src/commands/skill.ts +796 -0
  73. package/src/commands/workers.test.ts +158 -0
  74. package/src/commands/workers.ts +298 -0
  75. package/src/index.test.ts +32 -0
  76. package/src/index.ts +11 -274
  77. package/src/lib/db/control-plane.test.ts +59 -0
  78. package/src/lib/db/control-plane.ts +113 -0
  79. package/src/lib/db/local.test.ts +81 -0
  80. package/src/lib/db/local.ts +148 -0
  81. package/src/lib/db/migrate.test.ts +133 -0
  82. package/src/lib/db/migrate.ts +137 -0
  83. package/src/lib/db/paths.test.ts +112 -0
  84. package/src/lib/db/paths.ts +128 -0
  85. package/src/lib/db/remote-engine.test.ts +44 -0
  86. package/src/lib/db/remote-engine.ts +148 -0
  87. package/src/lib/db/remote-sql.test.ts +32 -0
  88. package/src/lib/db/remote-sql.ts +62 -0
  89. package/src/lib/db/sql.test.ts +106 -0
  90. package/src/lib/db/sql.ts +192 -0
  91. package/src/main.ts +314 -0
  92. package/src/utils/cognito-session.ts +1 -1
  93. package/src/utils/sandbox-runner-client.test.ts +128 -0
  94. package/src/utils/sandbox-runner-client.ts +100 -4
  95. package/src/utils/version-check.test.ts +30 -0
  96. package/src/utils/version-check.ts +72 -0
  97. package/test/commands/db-tenant-isolation.test.ts +94 -0
  98. package/test/commands/db.test.ts +85 -0
@@ -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]="99545a82-414c-5fed-a478-ea8e63009643")}catch(e){}}();
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]="f7058fc6-251e-5e11-af6c-de15f6e256f3")}catch(e){}}();
3
3
  import * as fs from "fs";
4
4
  import * as os from "os";
5
5
  import * as path from "path";
@@ -9,10 +9,15 @@ import { CLI_VERSION } from "../cli-version.js";
9
9
  const PACKAGE_NAME = "@indigoai-us/hq-cli";
10
10
  const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
11
11
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12
+ const CACHE_TTL_JITTER_MS = 60 * 60 * 1000;
12
13
  const FETCH_TIMEOUT_MS = 3_000;
14
+ const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
13
15
  function cachePath() {
14
16
  return path.join(os.homedir(), ".hq", "version-check.json");
15
17
  }
18
+ function lockPath() {
19
+ return path.join(os.homedir(), ".hq", "version-check.lock");
20
+ }
16
21
  function isOptedOut() {
17
22
  return process.env.HQ_NO_UPDATE_CHECK === "1";
18
23
  }
@@ -40,6 +45,59 @@ function writeCache(entry) {
40
45
  // best-effort; never break the CLI on cache write failure
41
46
  }
42
47
  }
48
+ function freshEnough(entry, now = Date.now()) {
49
+ const jitter = Math.floor(Math.random() * CACHE_TTL_JITTER_MS);
50
+ return now - entry.fetchedAt <= CACHE_TTL_MS - jitter;
51
+ }
52
+ function isKnownNoninteractiveStatusProbe(argv = process.argv) {
53
+ const args = argv.slice(2);
54
+ const positional = args.filter((arg) => !arg.startsWith("-"));
55
+ const json = args.includes("--json") || !process.stdout.isTTY;
56
+ if (!json)
57
+ return false;
58
+ if (positional[0] === "mcp" && positional[1] === "status")
59
+ return true;
60
+ if (positional[0] === "packs" && (positional[1] === "list" || positional[1] === "ls"))
61
+ return true;
62
+ if (positional[0] === "packages" &&
63
+ positional[1] === "packs" &&
64
+ (positional[2] === "list" || positional[2] === "ls")) {
65
+ return true;
66
+ }
67
+ if ((positional[0] === "sources" || positional[0] === "signals") && positional[1] === "list") {
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+ function acquireRefreshLock(now = Date.now()) {
73
+ const dir = lockPath();
74
+ try {
75
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
76
+ fs.mkdirSync(dir);
77
+ fs.writeFileSync(path.join(dir, "owner"), `${process.pid}\n${now}\n`);
78
+ return () => {
79
+ try {
80
+ fs.rmSync(dir, { recursive: true, force: true });
81
+ }
82
+ catch {
83
+ // best-effort lock cleanup
84
+ }
85
+ };
86
+ }
87
+ catch {
88
+ try {
89
+ const stat = fs.statSync(dir);
90
+ if (now - stat.mtimeMs > REFRESH_LOCK_STALE_MS) {
91
+ fs.rmSync(dir, { recursive: true, force: true });
92
+ return acquireRefreshLock(now);
93
+ }
94
+ }
95
+ catch {
96
+ // ignore lock inspection failures
97
+ }
98
+ return null;
99
+ }
100
+ }
43
101
  export function maybeWarnNewVersion() {
44
102
  if (isOptedOut())
45
103
  return;
@@ -60,7 +118,18 @@ export function maybeWarnNewVersion() {
60
118
  export async function refreshVersionCache() {
61
119
  if (isOptedOut())
62
120
  return;
121
+ if (isKnownNoninteractiveStatusProbe())
122
+ return;
123
+ const existing = readCache();
124
+ if (existing && freshEnough(existing))
125
+ return;
126
+ const releaseLock = acquireRefreshLock();
127
+ if (!releaseLock)
128
+ return;
63
129
  try {
130
+ const lockedExisting = readCache();
131
+ if (lockedExisting && freshEnough(lockedExisting))
132
+ return;
64
133
  const res = await fetch(REGISTRY_URL, {
65
134
  headers: { Accept: "application/json" },
66
135
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -75,6 +144,13 @@ export async function refreshVersionCache() {
75
144
  catch {
76
145
  // best-effort; offline / registry down / timeout — silent
77
146
  }
147
+ finally {
148
+ releaseLock();
149
+ }
78
150
  }
151
+ export const __test__ = {
152
+ CACHE_TTL_MS,
153
+ isKnownNoninteractiveStatusProbe,
154
+ };
79
155
  //# sourceMappingURL=version-check.js.map
80
- //# debugId=99545a82-414c-5fed-a478-ea8e63009643
156
+ //# debugId=f7058fc6-251e-5e11-af6c-de15f6e256f3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.60.0",
3
+ "version": "5.62.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "typecheck": "tsc --noEmit",
13
13
  "lint": "eslint .",
14
14
  "test": "vitest run",
15
+ "test:db": "vitest run src/lib/db test/commands/db.test.ts test/commands/db-tenant-isolation.test.ts",
15
16
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
16
17
  "coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
17
18
  "vitest": "vitest",
@@ -21,6 +22,7 @@
21
22
  "@indigoai-us/hq-cloud": "^6.12.1",
22
23
  "@indigoai-us/hq-onboarding": "^0.1.0",
23
24
  "@sentry/node": "^10.49.0",
25
+ "better-sqlite3": "^12.11.1",
24
26
  "chalk": "^5.3.0",
25
27
  "commander": "^12.1.0",
26
28
  "js-yaml": "^4.1.0",
@@ -33,6 +35,7 @@
33
35
  "devDependencies": {
34
36
  "@aws-sdk/client-s3": "^3.1049.0",
35
37
  "@eslint/js": "^10.0.1",
38
+ "@types/better-sqlite3": "^7.6.13",
36
39
  "@types/js-yaml": "^4.0.9",
37
40
  "@types/node": "^22.0.0",
38
41
  "@types/semver": "^7.5.8",
@@ -58,5 +61,10 @@
58
61
  "type": "module",
59
62
  "engines": {
60
63
  "node": ">=20.0.0"
64
+ },
65
+ "pnpm": {
66
+ "onlyBuiltDependencies": [
67
+ "better-sqlite3"
68
+ ]
61
69
  }
62
70
  }
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ better-sqlite3: true
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Unit tests for `hq agents` (agents.ts).
3
+ *
4
+ * Mirrors company.test.ts / members.test.ts: mock ensureCognitoToken +
5
+ * getCompanyUid, spy on global fetch, drive through a Commander program, and
6
+ * assert the /v1/agents request shape (method + path + body) plus the guard /
7
+ * validation / exit behavior. `vaultApiFetch` itself is NOT mocked — the spied
8
+ * global fetch verifies the real URL + init the helper produces.
9
+ */
10
+
11
+ import { Command } from "commander";
12
+ import {
13
+ afterEach,
14
+ beforeEach,
15
+ describe,
16
+ expect,
17
+ it,
18
+ vi,
19
+ type MockInstance,
20
+ } from "vitest";
21
+
22
+ vi.mock("../utils/cognito-session.js", async (importOriginal) => {
23
+ const original =
24
+ await importOriginal<typeof import("../utils/cognito-session.js")>();
25
+ return {
26
+ ...original,
27
+ ensureCognitoToken: vi.fn(async () => "test-token"),
28
+ };
29
+ });
30
+
31
+ vi.mock("../utils/vault-api.js", async (importOriginal) => {
32
+ const original = await importOriginal<typeof import("../utils/vault-api.js")>();
33
+ return {
34
+ ...original,
35
+ getCompanyUid: vi.fn(async () => "cmp_acme"),
36
+ };
37
+ });
38
+
39
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
40
+ import { getCompanyUid } from "../utils/vault-api.js";
41
+ import { registerAgentsCommand } from "./agents.js";
42
+
43
+ function jsonResponse(status: number, body: unknown): Response {
44
+ return new Response(JSON.stringify(body), {
45
+ status,
46
+ headers: { "Content-Type": "application/json" },
47
+ });
48
+ }
49
+
50
+ let fetchSpy: MockInstance<typeof fetch>;
51
+ let exitSpy: MockInstance<typeof process.exit>;
52
+ let logSpy: MockInstance<typeof console.log>;
53
+ const mockEnsureCognitoToken = vi.mocked(ensureCognitoToken);
54
+ const mockGetCompanyUid = vi.mocked(getCompanyUid);
55
+
56
+ beforeEach(() => {
57
+ vi.clearAllMocks();
58
+ fetchSpy = vi.spyOn(globalThis, "fetch");
59
+ mockEnsureCognitoToken.mockResolvedValue("test-token");
60
+ mockGetCompanyUid.mockResolvedValue("cmp_acme");
61
+ exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: number) => {
62
+ throw new Error(`process.exit(${code})`);
63
+ }) as unknown as MockInstance<typeof process.exit>;
64
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
65
+ vi.spyOn(console, "error").mockImplementation(() => {});
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.restoreAllMocks();
70
+ });
71
+
72
+ function buildProgram(): Command {
73
+ const program = new Command();
74
+ program.name("hq").exitOverride();
75
+ registerAgentsCommand(program);
76
+ return program;
77
+ }
78
+
79
+ async function run(args: string[]): Promise<void> {
80
+ await buildProgram().parseAsync(["node", "hq", ...args]);
81
+ }
82
+
83
+ describe("hq agents list", () => {
84
+ it("GETs /v1/agents with the resolved companyUid", async () => {
85
+ fetchSpy.mockResolvedValueOnce(
86
+ jsonResponse(200, {
87
+ agents: [
88
+ { uid: "agt_1", name: "Ops", slug: "ops", agentStatus: "running" },
89
+ ],
90
+ }),
91
+ );
92
+
93
+ await run(["agents", "--company", "acme", "list"]);
94
+
95
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
96
+ const [url, init] = fetchSpy.mock.calls[0];
97
+ expect(String(url)).toContain("/v1/agents");
98
+ expect(String(url)).toContain("companyUid=cmp_acme");
99
+ expect(init?.method ?? "GET").toBe("GET");
100
+ });
101
+
102
+ it("accepts --company on the subcommand too", async () => {
103
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { agents: [] }));
104
+ await run(["agents", "list", "--company", "acme"]);
105
+ expect(mockGetCompanyUid).toHaveBeenCalledWith("test-token", "acme");
106
+ });
107
+
108
+ it("emits raw JSON with --json", async () => {
109
+ const stdoutSpy = vi
110
+ .spyOn(process.stdout, "write")
111
+ .mockImplementation(() => true);
112
+ fetchSpy.mockResolvedValueOnce(
113
+ jsonResponse(200, { agents: [{ uid: "agt_1", name: "Ops", slug: "ops" }] }),
114
+ );
115
+ await run(["agents", "--company", "acme", "list", "--json"]);
116
+ const printed = stdoutSpy.mock.calls.map((c) => String(c[0])).join("\n");
117
+ expect(printed).toContain('"uid": "agt_1"');
118
+ });
119
+
120
+ it("prints a friendly message when agents are not enabled (404)", async () => {
121
+ fetchSpy.mockResolvedValueOnce(jsonResponse(404, { error: "not found" }));
122
+ await run(["agents", "--company", "acme", "list"]);
123
+ const printed = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
124
+ expect(printed).toMatch(/agents feature isn't enabled/i);
125
+ expect(exitSpy).not.toHaveBeenCalled();
126
+ });
127
+ });
128
+
129
+ describe("hq agents rename", () => {
130
+ it("PATCHes the profile with displayName", async () => {
131
+ fetchSpy.mockResolvedValueOnce(
132
+ jsonResponse(200, {
133
+ uid: "agt_1",
134
+ profile: { displayName: "New" },
135
+ slackUpdated: false,
136
+ }),
137
+ );
138
+
139
+ await run(["agents", "--company", "acme", "rename", "agt_1", "New"]);
140
+
141
+ const [url, init] = fetchSpy.mock.calls[0];
142
+ expect(String(url)).toContain("/v1/agents/agt_1/profile");
143
+ expect(init?.method).toBe("PATCH");
144
+ expect(JSON.parse(init?.body as string)).toEqual({ displayName: "New" });
145
+ });
146
+ });
147
+
148
+ describe("hq agents set", () => {
149
+ it("PATCHes only the supplied profile fields", async () => {
150
+ fetchSpy.mockResolvedValueOnce(
151
+ jsonResponse(200, { uid: "agt_1", profile: {}, slackUpdated: true }),
152
+ );
153
+
154
+ await run([
155
+ "agents",
156
+ "--company",
157
+ "acme",
158
+ "set",
159
+ "agt_1",
160
+ "--title",
161
+ "Chief of Staff",
162
+ ]);
163
+
164
+ const [url, init] = fetchSpy.mock.calls[0];
165
+ expect(String(url)).toContain("/v1/agents/agt_1/profile");
166
+ expect(init?.method).toBe("PATCH");
167
+ const sent = JSON.parse(init?.body as string);
168
+ expect(sent).toEqual({ title: "Chief of Staff" });
169
+ });
170
+
171
+ it("requires at least one field", async () => {
172
+ await expect(
173
+ run(["agents", "--company", "acme", "set", "agt_1"]),
174
+ ).rejects.toThrow("process.exit(1)");
175
+ expect(fetchSpy).not.toHaveBeenCalled();
176
+ });
177
+ });
178
+
179
+ describe("hq agents config", () => {
180
+ it("PATCHes runtime-config with model/effort/tier", async () => {
181
+ fetchSpy.mockResolvedValueOnce(
182
+ jsonResponse(200, { uid: "agt_1", codexModel: "gpt-5.5", applied: true }),
183
+ );
184
+
185
+ await run([
186
+ "agents",
187
+ "--company",
188
+ "acme",
189
+ "config",
190
+ "agt_1",
191
+ "--model",
192
+ "gpt-5.5",
193
+ "--effort",
194
+ "high",
195
+ "--tier",
196
+ "priority",
197
+ ]);
198
+
199
+ const [url, init] = fetchSpy.mock.calls[0];
200
+ expect(String(url)).toContain("/v1/agents/agt_1/runtime-config");
201
+ expect(init?.method).toBe("PATCH");
202
+ expect(JSON.parse(init?.body as string)).toEqual({
203
+ codexModel: "gpt-5.5",
204
+ codexReasoningEffort: "high",
205
+ codexServiceTier: "priority",
206
+ });
207
+ });
208
+
209
+ it("rejects an invalid --effort", async () => {
210
+ await expect(
211
+ run([
212
+ "agents",
213
+ "--company",
214
+ "acme",
215
+ "config",
216
+ "agt_1",
217
+ "--effort",
218
+ "turbo",
219
+ ]),
220
+ ).rejects.toThrow("process.exit(1)");
221
+ expect(fetchSpy).not.toHaveBeenCalled();
222
+ });
223
+
224
+ it("rejects an invalid --tier", async () => {
225
+ await expect(
226
+ run([
227
+ "agents",
228
+ "--company",
229
+ "acme",
230
+ "config",
231
+ "agt_1",
232
+ "--tier",
233
+ "turbo",
234
+ ]),
235
+ ).rejects.toThrow("process.exit(1)");
236
+ expect(fetchSpy).not.toHaveBeenCalled();
237
+ });
238
+
239
+ it("requires at least one field", async () => {
240
+ await expect(
241
+ run(["agents", "--company", "acme", "config", "agt_1"]),
242
+ ).rejects.toThrow("process.exit(1)");
243
+ expect(fetchSpy).not.toHaveBeenCalled();
244
+ });
245
+ });
246
+
247
+ describe("hq agents start/stop/retry", () => {
248
+ it("POSTs /start", async () => {
249
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { uid: "agt_1" }));
250
+ await run(["agents", "--company", "acme", "start", "agt_1"]);
251
+ const [url, init] = fetchSpy.mock.calls[0];
252
+ expect(String(url)).toContain("/v1/agents/agt_1/start");
253
+ expect(init?.method).toBe("POST");
254
+ });
255
+
256
+ it("POSTs /stop", async () => {
257
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { uid: "agt_1" }));
258
+ await run(["agents", "--company", "acme", "stop", "agt_1"]);
259
+ expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/agents/agt_1/stop");
260
+ });
261
+
262
+ it("POSTs /retry", async () => {
263
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { uid: "agt_1" }));
264
+ await run(["agents", "--company", "acme", "retry", "agt_1"]);
265
+ expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/agents/agt_1/retry");
266
+ });
267
+ });
268
+
269
+ describe("hq agents rm", () => {
270
+ it("refuses without --yes and does not call the API", async () => {
271
+ await expect(
272
+ run(["agents", "--company", "acme", "rm", "agt_1"]),
273
+ ).rejects.toThrow("process.exit(1)");
274
+ expect(fetchSpy).not.toHaveBeenCalled();
275
+ });
276
+
277
+ it("DELETEs the agent with --yes", async () => {
278
+ fetchSpy.mockResolvedValueOnce(
279
+ jsonResponse(200, { uid: "agt_1", terminal: true }),
280
+ );
281
+ await run(["agents", "--company", "acme", "rm", "agt_1", "--yes"]);
282
+ const [url, init] = fetchSpy.mock.calls[0];
283
+ expect(String(url)).toContain("/v1/agents/agt_1");
284
+ expect(init?.method).toBe("DELETE");
285
+ });
286
+ });
287
+
288
+ describe("hq agents status", () => {
289
+ it("exits 1 on a non-2xx and surfaces the error", async () => {
290
+ fetchSpy.mockResolvedValueOnce(
291
+ jsonResponse(403, { error: "forbidden" }),
292
+ );
293
+ await expect(
294
+ run(["agents", "--company", "acme", "status", "agt_1"]),
295
+ ).rejects.toThrow("process.exit(1)");
296
+ });
297
+ });