@indigoai-us/hq-cli 5.57.0 → 5.58.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.
@@ -6,25 +6,21 @@
6
6
  * hq people search <keyword> [--company <slug>] [--json]
7
7
  * hq people resolve <name> [--company <slug>] [--json]
8
8
  *
9
- * Source of truth is `companies/<company>/people/<person>/meta.yaml`. Every
10
- * subcommand operates on exactly ONE company (the active company, or the one
11
- * named by `--company`); nothing reads across company boundaries.
9
+ * Local records from `companies/<company>/people/<person>/meta.yaml` are the
10
+ * curated primary source. On misses, commands may fall back to the membership
11
+ * roster for exactly ONE company (the active company, or the one named by
12
+ * `--company`); nothing reads across company boundaries.
12
13
  */
13
14
 
14
15
  import * as fs from "fs";
15
16
  import { Command, Option } from "commander";
16
17
  import chalk from "chalk";
17
- import { VaultClient } from "@indigoai-us/hq-cloud";
18
18
  import * as yaml from "js-yaml";
19
19
  import { findHqRoot } from "../utils/manifest.js";
20
20
  import { manifestPath, type ManifestDoc } from "./cloud-provision.js";
21
- import {
22
- DEFAULT_COGNITO,
23
- buildVaultConfig,
24
- ensureCognitoToken,
25
- } from "../utils/cognito-session.js";
21
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
26
22
  import { getCompanyUid } from "../utils/vault-api.js";
27
- import { createCompanyPresignClient, runGet } from "./files-browse.js";
23
+ import { listActiveMembers, type ActiveMember } from "./members.js";
28
24
  import {
29
25
  assertSafeCompanySlug,
30
26
  listCompanyPeople,
@@ -39,13 +35,13 @@ interface PeopleScopeOpts {
39
35
  hqRoot?: string;
40
36
  }
41
37
 
42
- export type RefreshPeopleRoster = (
38
+ export type FetchPeopleRoster = (
43
39
  hqRoot: string,
44
40
  companySlug: string,
45
- ) => Promise<void>;
41
+ ) => Promise<PersonRecord[]>;
46
42
 
47
43
  interface PeopleCommandDeps {
48
- refreshRoster?: RefreshPeopleRoster;
44
+ fetchRoster?: FetchPeopleRoster;
49
45
  }
50
46
 
51
47
  interface PeopleLookupOpts {
@@ -53,23 +49,71 @@ interface PeopleLookupOpts {
53
49
  json?: boolean;
54
50
  }
55
51
 
56
- export async function refreshPeopleRosterFromCloud(
52
+ function slugify(value: string): string {
53
+ return value
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9]+/g, "-")
56
+ .replace(/-+/g, "-")
57
+ .replace(/^-|-$/g, "");
58
+ }
59
+
60
+ export function activeMemberToPersonRecord(
61
+ m: ActiveMember,
62
+ ): PersonRecord | null {
63
+ const name =
64
+ m.personName?.trim() ||
65
+ m.personEmail?.trim() ||
66
+ m.personSlug?.trim() ||
67
+ "";
68
+ if (!name) return null;
69
+
70
+ const email = m.personEmail?.trim() || undefined;
71
+ const slug = m.personSlug?.trim() || slugify(name) || m.personUid;
72
+
73
+ return {
74
+ slug,
75
+ name,
76
+ email,
77
+ role: m.role || undefined,
78
+ type: "internal",
79
+ source: `hq-pro membership: /membership/company/${m.companyUid}`,
80
+ };
81
+ }
82
+
83
+ export async function fetchMembershipRoster(
57
84
  hqRoot: string,
58
85
  companySlug: string,
59
- ): Promise<void> {
60
- const accessToken = await ensureCognitoToken();
61
- const client = new VaultClient(buildVaultConfig(accessToken));
62
- await getCompanyUid(accessToken, companySlug);
63
-
64
- await runGet({
65
- path: `companies/${companySlug}/people/`,
66
- hqRoot,
67
- companySlug,
68
- vaultClient: client,
69
- companyClient: ({ companyUid }) =>
70
- createCompanyPresignClient({ token: accessToken, companyUid }),
71
- region: DEFAULT_COGNITO.region,
72
- });
86
+ ): Promise<PersonRecord[]> {
87
+ void hqRoot;
88
+ const token = await ensureCognitoToken();
89
+ const companyUid = await getCompanyUid(token, companySlug);
90
+ const members = await listActiveMembers(token, companyUid);
91
+ return members
92
+ .map(activeMemberToPersonRecord)
93
+ .filter((r): r is PersonRecord => r !== null);
94
+ }
95
+
96
+ function personIdentityKey(person: PersonRecord): string {
97
+ return (
98
+ person.email?.toLowerCase() ||
99
+ person.slug?.toLowerCase() ||
100
+ person.name.toLowerCase()
101
+ );
102
+ }
103
+
104
+ export function mergePeople(
105
+ primary: PersonRecord[],
106
+ extra: PersonRecord[],
107
+ ): PersonRecord[] {
108
+ const seen = new Set(primary.map(personIdentityKey));
109
+ const merged = [...primary];
110
+ for (const person of extra) {
111
+ const key = personIdentityKey(person);
112
+ if (seen.has(key)) continue;
113
+ seen.add(key);
114
+ merged.push(person);
115
+ }
116
+ return merged;
73
117
  }
74
118
 
75
119
  /** Companies that still exist (anything not explicitly `status: archived`). */
@@ -159,26 +203,25 @@ function fail(message: string): never {
159
203
  process.exit(1);
160
204
  }
161
205
 
162
- function logRefreshFailure(companySlug: string, err: unknown): void {
206
+ function logRosterFetchFailure(companySlug: string, err: unknown): void {
163
207
  const message = err instanceof Error ? err.message : String(err);
164
208
  console.error(
165
209
  chalk.dim(
166
- ` Could not refresh people roster for '${companySlug}': ${message}`,
210
+ ` Could not fetch people roster for '${companySlug}': ${message}`,
167
211
  ),
168
212
  );
169
213
  }
170
214
 
171
- async function tryRefreshRoster(
172
- refreshRoster: RefreshPeopleRoster,
215
+ async function tryFetchRoster(
216
+ fetchRoster: FetchPeopleRoster,
173
217
  hqRoot: string,
174
218
  slug: string,
175
- ): Promise<boolean> {
219
+ ): Promise<PersonRecord[] | null> {
176
220
  try {
177
- await refreshRoster(hqRoot, slug);
178
- return true;
221
+ return await fetchRoster(hqRoot, slug);
179
222
  } catch (err) {
180
- logRefreshFailure(slug, err);
181
- return false;
223
+ logRosterFetchFailure(slug, err);
224
+ return null;
182
225
  }
183
226
  }
184
227
 
@@ -188,25 +231,20 @@ export async function resolvePersonWithRosterFallback(
188
231
  slug: string;
189
232
  name: string;
190
233
  opts?: PeopleLookupOpts;
191
- refreshRoster?: RefreshPeopleRoster;
234
+ fetchRoster?: FetchPeopleRoster;
192
235
  },
193
236
  ): Promise<ReturnType<typeof resolveNameToEmail>> {
194
- const local = resolveNameToEmail(
195
- listCompanyPeople(input.hqRoot, input.slug),
196
- input.name,
197
- );
237
+ const localPeople = listCompanyPeople(input.hqRoot, input.slug);
238
+ const local = resolveNameToEmail(localPeople, input.name);
198
239
  if (local.status !== "not_found" || input.opts?.localOnly) return local;
199
240
 
200
- const refreshed = await tryRefreshRoster(
201
- input.refreshRoster ?? refreshPeopleRosterFromCloud,
241
+ const roster = await tryFetchRoster(
242
+ input.fetchRoster ?? fetchMembershipRoster,
202
243
  input.hqRoot,
203
244
  input.slug,
204
245
  );
205
- if (!refreshed) return local;
206
- return resolveNameToEmail(
207
- listCompanyPeople(input.hqRoot, input.slug),
208
- input.name,
209
- );
246
+ if (!roster) return local;
247
+ return resolveNameToEmail(mergePeople(localPeople, roster), input.name);
210
248
  }
211
249
 
212
250
  export async function searchPeopleWithRosterFallback(input: {
@@ -214,21 +252,19 @@ export async function searchPeopleWithRosterFallback(input: {
214
252
  slug: string;
215
253
  keyword: string;
216
254
  opts?: PeopleLookupOpts;
217
- refreshRoster?: RefreshPeopleRoster;
255
+ fetchRoster?: FetchPeopleRoster;
218
256
  }): Promise<PersonRecord[]> {
219
- const local = searchPeople(
220
- listCompanyPeople(input.hqRoot, input.slug),
221
- input.keyword,
222
- );
257
+ const localPeople = listCompanyPeople(input.hqRoot, input.slug);
258
+ const local = searchPeople(localPeople, input.keyword);
223
259
  if (local.length > 0 || input.opts?.localOnly) return local;
224
260
 
225
- const refreshed = await tryRefreshRoster(
226
- input.refreshRoster ?? refreshPeopleRosterFromCloud,
261
+ const roster = await tryFetchRoster(
262
+ input.fetchRoster ?? fetchMembershipRoster,
227
263
  input.hqRoot,
228
264
  input.slug,
229
265
  );
230
- if (!refreshed) return local;
231
- return searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
266
+ if (!roster) return local;
267
+ return searchPeople(mergePeople(localPeople, roster), input.keyword);
232
268
  }
233
269
 
234
270
  export function registerPeopleCommand(
@@ -257,12 +293,26 @@ export function registerPeopleCommand(
257
293
  .command("list")
258
294
  .description("List all people recorded for the company")
259
295
  .option("--json", "Output JSON instead of a table")
260
- .action((opts: { json?: boolean }) => {
296
+ .option(
297
+ "--local-only",
298
+ "Skip network fallback; list only the local people roster",
299
+ )
300
+ .action(async (opts: PeopleLookupOpts) => {
261
301
  try {
262
302
  const scope = people.opts() as PeopleScopeOpts;
263
303
  const hqRoot = resolveHqRoot(scope);
264
304
  const slug = resolveCompanySlug(hqRoot, scope.company);
265
- const records = listCompanyPeople(hqRoot, slug);
305
+ let records = listCompanyPeople(hqRoot, slug);
306
+
307
+ if (!opts.localOnly) {
308
+ const roster = await tryFetchRoster(
309
+ deps.fetchRoster ?? fetchMembershipRoster,
310
+ hqRoot,
311
+ slug,
312
+ );
313
+ if (roster) records = mergePeople(records, roster);
314
+ }
315
+ records = records.sort((a, b) => a.name.localeCompare(b.name));
266
316
 
267
317
  if (opts.json) {
268
318
  console.log(JSON.stringify(records, null, 2));
@@ -288,7 +338,7 @@ export function registerPeopleCommand(
288
338
  .option("--json", "Output JSON instead of a table")
289
339
  .option(
290
340
  "--local-only",
291
- "Skip cloud fallback; search only the local people roster",
341
+ "Skip network fallback; search only the local people roster",
292
342
  )
293
343
  .action(async (keyword: string, opts: PeopleLookupOpts) => {
294
344
  try {
@@ -300,7 +350,7 @@ export function registerPeopleCommand(
300
350
  slug,
301
351
  keyword,
302
352
  opts,
303
- refreshRoster: deps.refreshRoster,
353
+ fetchRoster: deps.fetchRoster,
304
354
  });
305
355
 
306
356
  if (opts.json) {
@@ -323,7 +373,7 @@ export function registerPeopleCommand(
323
373
  .option("--json", "Output JSON instead of plain text")
324
374
  .option(
325
375
  "--local-only",
326
- "Skip cloud fallback; resolve only from the local people roster",
376
+ "Skip network fallback; resolve only from the local people roster",
327
377
  )
328
378
  .action(async (name: string, opts: PeopleLookupOpts) => {
329
379
  try {
@@ -335,7 +385,7 @@ export function registerPeopleCommand(
335
385
  slug,
336
386
  name,
337
387
  opts,
338
- refreshRoster: deps.refreshRoster,
388
+ fetchRoster: deps.fetchRoster,
339
389
  });
340
390
 
341
391
  if (opts.json) {
@@ -104,6 +104,7 @@ function jsonRes(body: unknown, status = 200): Response {
104
104
  describe("secrets sandbox", () => {
105
105
  let stdoutSpy: MockInstance<typeof process.stdout.write>;
106
106
  let stderrSpy: MockInstance<typeof process.stderr.write>;
107
+ let exitSpy: MockInstance<typeof process.exit>;
107
108
  let startJobSpy: MockInstance<SandboxRunnerClient["startJob"]>;
108
109
  let pollJobSpy: MockInstance<SandboxRunnerClient["pollJob"]>;
109
110
  let getJobSpy: MockInstance<SandboxRunnerClient["getJob"]>;
@@ -119,9 +120,14 @@ describe("secrets sandbox", () => {
119
120
  .mockResolvedValue({
120
121
  jobId: "job_123",
121
122
  status: "succeeded",
122
- stdout: "done\n",
123
+ output: "done\n",
124
+ exitCode: 0,
125
+ success: true,
123
126
  });
124
127
  getJobSpy = vi.spyOn(SandboxRunnerClient.prototype, "getJob");
128
+ exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
129
+ throw new Error("__exit__");
130
+ }) as never);
125
131
  });
126
132
 
127
133
  it("registers the sandbox channel value", () => {
@@ -129,7 +135,7 @@ describe("secrets sandbox", () => {
129
135
  expect(channel).toBe("sandbox");
130
136
  });
131
137
 
132
- it("parses --company, --only, and the -- <skill> separator", async () => {
138
+ it("parses --company, --only, and joins args after -- into a command", async () => {
133
139
  const program = buildProgram();
134
140
  await program.parseAsync([
135
141
  "node",
@@ -141,22 +147,59 @@ describe("secrets sandbox", () => {
141
147
  "--only",
142
148
  "API_KEY,OTHER_KEY",
143
149
  "--",
144
- "pull-triple-whale",
150
+ "node",
151
+ "-e",
152
+ "console.log('hi')",
145
153
  "--days",
146
154
  "7",
147
155
  ]);
148
156
 
149
157
  expect(startJobSpy).toHaveBeenCalledWith("test-token", {
150
- skillId: "pull-triple-whale",
151
158
  companyUid: "prs_alice",
152
- args: { argv: ["--days", "7"] },
153
- companySlug: "acme",
154
- only: ["API_KEY", "OTHER_KEY"],
155
- usage: { channel: "sandbox" },
159
+ secretNames: ["API_KEY", "OTHER_KEY"],
160
+ command: "node -e console.log('hi') --days 7",
156
161
  });
157
162
  expect(stdoutSpy).toHaveBeenCalledWith("done\n");
158
163
  });
159
164
 
165
+ it("requires --only", async () => {
166
+ const program = buildProgram();
167
+
168
+ await expect(
169
+ program.parseAsync([
170
+ "node",
171
+ "hq",
172
+ "secrets",
173
+ "sandbox",
174
+ "--",
175
+ "env",
176
+ ]),
177
+ ).rejects.toThrow("__exit__");
178
+
179
+ expect(exitSpy).toHaveBeenCalledWith(1);
180
+ expect(errSpy.mock.calls.flat().join(" ")).toMatch(/--only is required/i);
181
+ expect(startJobSpy).not.toHaveBeenCalled();
182
+ });
183
+
184
+ it("requires a command after --", async () => {
185
+ const program = buildProgram();
186
+
187
+ await expect(
188
+ program.parseAsync([
189
+ "node",
190
+ "hq",
191
+ "secrets",
192
+ "sandbox",
193
+ "--only",
194
+ "API_KEY",
195
+ ]),
196
+ ).rejects.toThrow("__exit__");
197
+
198
+ expect(exitSpy).toHaveBeenCalledWith(1);
199
+ expect(errSpy.mock.calls.flat().join(" ")).toMatch(/no command specified/i);
200
+ expect(startJobSpy).not.toHaveBeenCalled();
201
+ });
202
+
160
203
  it("resolves identity before posting the runner job", async () => {
161
204
  const program = buildProgram();
162
205
  await program.parseAsync([
@@ -166,8 +209,10 @@ describe("secrets sandbox", () => {
166
209
  "--company",
167
210
  "parent-co",
168
211
  "sandbox",
212
+ "--only",
213
+ "API_KEY",
169
214
  "--",
170
- "my-skill",
215
+ "env",
171
216
  ]);
172
217
 
173
218
  expect(ensureCognitoToken).toHaveBeenCalled();
@@ -175,58 +220,20 @@ describe("secrets sandbox", () => {
175
220
  personal: false,
176
221
  companySlug: "parent-co",
177
222
  });
178
- expect(startJobSpy).toHaveBeenCalledWith(
179
- "test-token",
180
- expect.objectContaining({
181
- skillId: "my-skill",
182
- companyUid: "prs_alice",
183
- companySlug: "parent-co",
184
- usage: { channel: "sandbox" },
185
- }),
186
- );
187
- });
188
-
189
- it("can attach script attestation metadata to sandbox usage", async () => {
190
- const scriptPath = join(tempDir, "sandbox-script.sh");
191
- const scriptBody = "#!/usr/bin/env bash\necho sandbox\n";
192
- writeFileSync(scriptPath, scriptBody);
193
- const expectedSha = createHash("sha256").update(scriptBody).digest("hex");
194
-
195
- const program = buildProgram();
196
- await program.parseAsync([
197
- "node",
198
- "hq",
199
- "secrets",
200
- "sandbox",
201
- "--script",
202
- scriptPath,
203
- "--",
204
- "my-skill",
205
- ]);
206
-
207
- expect(startJobSpy).toHaveBeenCalledWith(
208
- "test-token",
209
- expect.objectContaining({
210
- usage: {
211
- channel: "sandbox",
212
- script: {
213
- scriptId: scriptPath,
214
- path: scriptPath,
215
- sha256: expectedSha,
216
- attestationLevel: "self-asserted-hash",
217
- },
218
- },
219
- }),
220
- );
223
+ expect(startJobSpy).toHaveBeenCalledWith("test-token", {
224
+ companyUid: "prs_alice",
225
+ secretNames: ["API_KEY"],
226
+ command: "env",
227
+ });
221
228
  });
222
229
 
223
- it("never batch-loads or prints a plaintext secret value locally", async () => {
230
+ it("prints server-scrubbed output and never batch-loads secrets locally", async () => {
224
231
  pollJobSpy.mockResolvedValueOnce({
225
232
  jobId: "job_123",
226
233
  status: "succeeded",
227
- stdout: "token sk-test-secret-value\nAPI_KEY=sk-another-secret\n",
228
- stderr: "debug sk-stderr-secret\n",
229
- logsTail: "tail sk-log-secret\n",
234
+ output: "token [REDACTED]\nAPI_KEY=[REDACTED]\n",
235
+ exitCode: 0,
236
+ success: true,
230
237
  });
231
238
 
232
239
  const program = buildProgram();
@@ -253,37 +260,36 @@ describe("secrets sandbox", () => {
253
260
  ...logSpy.mock.calls.flat().map(String),
254
261
  ...errSpy.mock.calls.flat().map(String),
255
262
  ].join("\n");
256
- expect(rendered).not.toContain("sk-test-secret-value");
257
- expect(rendered).not.toContain("sk-another-secret");
258
- expect(rendered).not.toContain("sk-stderr-secret");
259
- expect(rendered).not.toContain("sk-log-secret");
260
263
  expect(rendered).toContain("[REDACTED]");
264
+ expect(stdoutSpy).toHaveBeenCalledWith("token [REDACTED]\nAPI_KEY=[REDACTED]\n");
261
265
  });
262
266
 
263
- it("renders scrubbed stdout, stderr, and log tails", async () => {
267
+ it("exits non-zero when the sandbox command fails", async () => {
264
268
  pollJobSpy.mockResolvedValueOnce({
265
269
  jobId: "job_123",
266
- status: "succeeded",
267
- stdout: "ok\nAPI_KEY=sk-stdout-secret\n",
268
- stderr: "warn sk-stderr-secret\n",
269
- logsTail: "tail sk-log-secret\n",
270
+ status: "failed",
271
+ output: "boom\n",
272
+ exitCode: 7,
273
+ success: false,
270
274
  });
271
275
 
272
276
  const program = buildProgram();
273
- await program.parseAsync([
274
- "node",
275
- "hq",
276
- "secrets",
277
- "sandbox",
278
- "--only",
279
- "API_KEY",
280
- "--",
281
- "my-skill",
282
- ]);
277
+ await expect(
278
+ program.parseAsync([
279
+ "node",
280
+ "hq",
281
+ "secrets",
282
+ "sandbox",
283
+ "--only",
284
+ "API_KEY",
285
+ "--",
286
+ "my-skill",
287
+ ]),
288
+ ).rejects.toThrow("__exit__");
283
289
 
284
- expect(stdoutSpy).toHaveBeenCalledWith("ok\nAPI_KEY=[REDACTED]\n");
285
- expect(stderrSpy).toHaveBeenCalledWith("warn [REDACTED]\n");
286
- expect(stderrSpy).toHaveBeenCalledWith("tail [REDACTED]\n");
290
+ expect(stdoutSpy).toHaveBeenCalledWith("boom\n");
291
+ expect(errSpy.mock.calls.flat().join(" ")).toMatch(/exit code 7/i);
292
+ expect(exitSpy).toHaveBeenCalledWith(7);
287
293
  expect(getJobSpy).not.toHaveBeenCalled();
288
294
  });
289
295
  });
@@ -368,14 +368,8 @@ export function scrubSandboxOutput(text: string, secretNames: string[] = []): st
368
368
  }
369
369
 
370
370
  function renderSandboxJobResult(job: SandboxRunnerJob, secretNames: string[]): void {
371
- if (job.stdout) {
372
- process.stdout.write(scrubSandboxOutput(job.stdout, secretNames));
373
- }
374
- if (job.stderr) {
375
- process.stderr.write(scrubSandboxOutput(job.stderr, secretNames));
376
- }
377
- if (job.logsTail) {
378
- process.stderr.write(scrubSandboxOutput(job.logsTail, secretNames));
371
+ if (job.output) {
372
+ process.stdout.write(scrubSandboxOutput(job.output, secretNames));
379
373
  }
380
374
  }
381
375
 
@@ -1249,38 +1243,41 @@ export function registerSecretsCommand(program: Command): void {
1249
1243
 
1250
1244
  secrets
1251
1245
  .command("sandbox")
1252
- .description("Run a skill in the hosted sandbox with secrets injected server-side")
1246
+ .description("Run a command in the hosted sandbox with named secrets injected as env vars; open egress, secrets never touch this machine")
1253
1247
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1254
1248
  .option(
1255
1249
  "--personal",
1256
1250
  "Operate on the caller's personal vault (no sharing)",
1257
1251
  )
1258
- .option("--only <keys>", "Comma-separated list of secret names the skill may use")
1259
- .option("--script <path>", "Attach local script identity for script-locked secrets")
1252
+ .option("--only <keys>", "Comma-separated list of secret names to inject (required)")
1260
1253
  .allowUnknownOption(true)
1261
- .action(async (opts: { company?: string; personal?: boolean; only?: string; script?: string }, cmd: Command) => {
1254
+ .action(async (opts: { company?: string; personal?: boolean; only?: string }, cmd: Command) => {
1262
1255
  try {
1263
- const dashIndex = process.argv.indexOf("--");
1264
- const rawArgs = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : cmd.args;
1265
- if (rawArgs.length === 0) {
1266
- console.error(chalk.red("Error: no skill specified. Usage: hq secrets sandbox [--company X] [--only KEY1,KEY2] -- <skill> [args...]"));
1256
+ if (!opts.only || opts.only.trim().length === 0) {
1257
+ console.error(chalk.red("Error: --only is required and must name at least one secret."));
1258
+ process.exit(1);
1259
+ }
1260
+
1261
+ const rawArgs = cmd.args;
1262
+ const command = rawArgs.join(" ").trim();
1263
+ if (command.length === 0) {
1264
+ console.error(chalk.red("Error: no command specified. Usage: hq secrets sandbox --company <slug> --only KEY1,KEY2 -- <command>"));
1265
+ process.exit(1);
1266
+ }
1267
+ if (command.length > 8192) {
1268
+ console.error(chalk.red("Error: sandbox command exceeds the 8192 character limit."));
1267
1269
  process.exit(1);
1268
1270
  }
1269
1271
 
1270
- const [skillId, ...skillArgs] = rawArgs;
1271
- const keys = opts.only ? parseSecretNameList(opts.only) : [];
1272
+ const keys = parseSecretNameList(opts.only);
1272
1273
  const token = await ensureCognitoToken();
1273
1274
  const scope = scopeOpts(mergeScopeOpts(secrets.opts(), opts));
1274
1275
  const companyUid = await getEntityUid(token, scope);
1275
- const usage = await buildSecretUsage("sandbox", opts.script);
1276
1276
  const client = new SandboxRunnerClient();
1277
1277
  const started = await client.startJob(token, {
1278
- skillId,
1279
1278
  companyUid,
1280
- args: skillArgs.length > 0 ? { argv: skillArgs } : undefined,
1281
- companySlug: scope.companySlug,
1282
- only: keys.length > 0 ? keys : undefined,
1283
- usage,
1279
+ secretNames: keys,
1280
+ command,
1284
1281
  });
1285
1282
  const job =
1286
1283
  started.status === "succeeded" || started.status === "failed"
@@ -1288,14 +1285,11 @@ export function registerSecretsCommand(program: Command): void {
1288
1285
  : await client.pollJob(token, started.jobId);
1289
1286
 
1290
1287
  renderSandboxJobResult(job, keys);
1291
- if (job.status === "failed") {
1292
- if (job.error) {
1293
- console.error(chalk.red("Sandbox job failed:"), scrubSandboxOutput(job.error, keys));
1294
- }
1295
- process.exit(job.exitCode && job.exitCode > 0 ? job.exitCode : 1);
1296
- }
1297
- if (job.exitCode && job.exitCode !== 0) {
1298
- process.exit(job.exitCode);
1288
+ const exitCode = typeof job.exitCode === "number" ? job.exitCode : undefined;
1289
+ if (job.success === false || (exitCode !== undefined && exitCode !== 0) || job.status === "failed") {
1290
+ const code = exitCode && exitCode !== 0 ? exitCode : 1;
1291
+ console.error(chalk.red(`Sandbox command failed with exit code ${code}.`));
1292
+ process.exit(code);
1299
1293
  }
1300
1294
  } catch (err) {
1301
1295
  console.error(