@asteroidai/cli 1.8.193 → 1.8.204

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/dist/asteroid.js CHANGED
@@ -4875,16 +4875,25 @@ function defineCommands(root, options, dotted = /* @__PURE__ */ new Map(), onInv
4875
4875
  const defaults = command.args.map(
4876
4876
  (arg) => options.defaultArg?.(command, arg)
4877
4877
  );
4878
- command.args.forEach((arg, i) => {
4878
+ const defaulted = defaults[0] !== void 0;
4879
+ command.args.forEach((arg) => {
4879
4880
  leaf.argument(
4880
- defaults[i] === void 0 ? `<${arg.flag}>` : `[${arg.flag}]`,
4881
+ defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
4881
4882
  arg.description ?? ""
4882
4883
  );
4883
4884
  });
4884
- if (defaults.some((value2) => value2 !== void 0) && options.defaultArgNote) {
4885
- leaf.addHelpText("after", `
4886
- ${options.defaultArgNote}`);
4885
+ if (defaulted) {
4886
+ const names = command.args.map(
4887
+ (arg, i) => i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`
4888
+ );
4889
+ leaf.usage(`[options] ${names.join(" ")}`);
4887
4890
  }
4891
+ const notes = [
4892
+ defaults.some((value2) => value2 !== void 0) ? options.defaultArgNote : void 0,
4893
+ options.helpNote?.(command)
4894
+ ].filter(Boolean);
4895
+ if (notes.length > 0) leaf.addHelpText("after", `
4896
+ ${notes.join("\n")}`);
4888
4897
  leafOptions(leaf, command);
4889
4898
  leaf.action((...args) => {
4890
4899
  const cmd = args.at(-1);
@@ -4893,7 +4902,7 @@ ${options.defaultArgNote}`);
4893
4902
  positionals = [defaults[0], ...positionals];
4894
4903
  }
4895
4904
  if (positionals.length !== command.args.length) {
4896
- const expected = command.args.map((a) => `<${a.flag}>`).join(" ");
4905
+ const expected = command.args.map((a, i) => i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
4897
4906
  throw new UsageError(
4898
4907
  `expected ${expected}, got ${cmd.args.length} argument(s)`
4899
4908
  );
@@ -5192,6 +5201,8 @@ var CredentialStore = class {
5192
5201
  // src/auth/credentials.ts
5193
5202
  var AuthError = class extends Error {
5194
5203
  };
5204
+ var API_KEY_OVERRIDES = "ASTEROID_API_KEY is set, so the CLI uses that key and ignores `asteroid login`.";
5205
+ var UNSET_API_KEY = "Run `unset ASTEROID_API_KEY` to use your login instead.";
5195
5206
  function orgFromToken(token) {
5196
5207
  const claims = tokenClaims(token);
5197
5208
  if (!claims.org_id) return void 0;
@@ -5301,7 +5312,7 @@ var ApiError = class extends Error {
5301
5312
  status;
5302
5313
  body;
5303
5314
  };
5304
- var VERSION = "1.8.193";
5315
+ var VERSION = "1.8.204";
5305
5316
  function problem(status, body, fallback) {
5306
5317
  if (body && typeof body === "object") {
5307
5318
  const record2 = body;
@@ -5368,11 +5379,13 @@ async function send(apiUrl, auth, request, binary, fetchImpl = fetch) {
5368
5379
  } catch {
5369
5380
  parsed = text2;
5370
5381
  }
5371
- throw new ApiError(
5372
- response.status,
5373
- problem(response.status, parsed, text2.slice(0, 200)),
5374
- parsed
5375
- );
5382
+ let message = problem(response.status, parsed, text2.slice(0, 200));
5383
+ const sentKey = "X-Api-Key" in hopHeaders;
5384
+ if (sentKey && response.status === 401)
5385
+ message = `ASTEROID_API_KEY was rejected: ${message}. Check the key. ${UNSET_API_KEY}`;
5386
+ else if (sentKey && response.status === 403)
5387
+ message = `${message} (sent with ASTEROID_API_KEY. ${UNSET_API_KEY})`;
5388
+ throw new ApiError(response.status, message, parsed);
5376
5389
  }
5377
5390
  if (binary)
5378
5391
  return {
@@ -5401,7 +5414,8 @@ async function chooseOrg(deps, orgs2) {
5401
5414
  async function login(deps) {
5402
5415
  if (deps.env.apiKey)
5403
5416
  deps.stderr(
5404
- "Note: ASTEROID_API_KEY is set and takes precedence over this sign-in.\n"
5417
+ `Warning: ${API_KEY_OVERRIDES} Every command will use the key, not this sign-in. ${UNSET_API_KEY}
5418
+ `
5405
5419
  );
5406
5420
  const client = {
5407
5421
  clientId: deps.env.workosClientId,
@@ -5467,7 +5481,7 @@ async function logout(deps) {
5467
5481
  async function orgs(deps, flagOrg, json) {
5468
5482
  if (deps.env.apiKey)
5469
5483
  throw new UsageError(
5470
- "ASTEROID_API_KEY acts in one organization. Run `asteroid whoami` to see it."
5484
+ `${API_KEY_OVERRIDES} The key acts in one organization; \`asteroid whoami\` shows it. ${UNSET_API_KEY}`
5471
5485
  );
5472
5486
  const config = loadConfig(deps.env);
5473
5487
  const auth = await resolveAuth(
@@ -5497,7 +5511,7 @@ async function orgs(deps, flagOrg, json) {
5497
5511
  async function use(deps, ref) {
5498
5512
  if (deps.env.apiKey)
5499
5513
  throw new UsageError(
5500
- "ASTEROID_API_KEY is pinned to one organization; `use` needs `asteroid login`."
5514
+ `${API_KEY_OVERRIDES} \`use\` only switches the login's organization. ${UNSET_API_KEY}`
5501
5515
  );
5502
5516
  const config = loadConfig(deps.env);
5503
5517
  if (!ref && deps.ui && (config.orgs?.length ?? 0) > 0) {
@@ -5556,6 +5570,7 @@ async function whoami(deps, flagOrg, json) {
5556
5570
  as: person?.email ?? body.id,
5557
5571
  kind: body.kind,
5558
5572
  credential: body.credential,
5573
+ source: auth.kind === "api-key" ? "ASTEROID_API_KEY" : "asteroid login",
5559
5574
  organization,
5560
5575
  ...minutes === void 0 ? {} : { tokenExpiresIn: `${minutes}m` }
5561
5576
  })}
@@ -5765,7 +5780,7 @@ function rejectUnknown(flags) {
5765
5780
  function rejectOrgWithApiKey(deps, flagOrg) {
5766
5781
  if (deps.env.apiKey && (flagOrg || deps.env.org)) {
5767
5782
  throw new UsageError(
5768
- "ASTEROID_API_KEY acts in its own organization. Unset --org and ASTEROID_ORG, or sign in with `asteroid login`."
5783
+ `${API_KEY_OVERRIDES} The key acts in its own organization, so drop --org and ASTEROID_ORG. ${UNSET_API_KEY}`
5769
5784
  );
5770
5785
  }
5771
5786
  }
package/dist/core.js CHANGED
@@ -4808,16 +4808,25 @@ function defineCommands(root, options, dotted = /* @__PURE__ */ new Map(), onInv
4808
4808
  const defaults = command.args.map(
4809
4809
  (arg) => options.defaultArg?.(command, arg)
4810
4810
  );
4811
- command.args.forEach((arg, i) => {
4811
+ const defaulted = defaults[0] !== void 0;
4812
+ command.args.forEach((arg) => {
4812
4813
  leaf.argument(
4813
- defaults[i] === void 0 ? `<${arg.flag}>` : `[${arg.flag}]`,
4814
+ defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
4814
4815
  arg.description ?? ""
4815
4816
  );
4816
4817
  });
4817
- if (defaults.some((value2) => value2 !== void 0) && options.defaultArgNote) {
4818
- leaf.addHelpText("after", `
4819
- ${options.defaultArgNote}`);
4818
+ if (defaulted) {
4819
+ const names = command.args.map(
4820
+ (arg, i) => i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`
4821
+ );
4822
+ leaf.usage(`[options] ${names.join(" ")}`);
4820
4823
  }
4824
+ const notes = [
4825
+ defaults.some((value2) => value2 !== void 0) ? options.defaultArgNote : void 0,
4826
+ options.helpNote?.(command)
4827
+ ].filter(Boolean);
4828
+ if (notes.length > 0) leaf.addHelpText("after", `
4829
+ ${notes.join("\n")}`);
4821
4830
  leafOptions(leaf, command);
4822
4831
  leaf.action((...args) => {
4823
4832
  const cmd = args.at(-1);
@@ -4826,7 +4835,7 @@ ${options.defaultArgNote}`);
4826
4835
  positionals = [defaults[0], ...positionals];
4827
4836
  }
4828
4837
  if (positionals.length !== command.args.length) {
4829
- const expected = command.args.map((a) => `<${a.flag}>`).join(" ");
4838
+ const expected = command.args.map((a, i) => i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
4830
4839
  throw new UsageError(
4831
4840
  `expected ${expected}, got ${cmd.args.length} argument(s)`
4832
4841
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteroidai/cli",
3
- "version": "1.8.193",
3
+ "version": "1.8.204",
4
4
  "description": "The asteroid command-line interface for the Asteroid platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/account.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { UsageError } from "./args";
2
2
  import {
3
+ API_KEY_OVERRIDES,
3
4
  AuthError,
4
5
  orgFromToken,
5
6
  rememberOrg,
6
7
  resolveAuth,
8
+ UNSET_API_KEY,
7
9
  } from "./auth/credentials";
8
10
  import { CredentialStore } from "./auth/store";
9
11
  import {
@@ -40,7 +42,7 @@ async function chooseOrg(deps: Deps, orgs: WorkosOrg[]): Promise<WorkosOrg> {
40
42
  export async function login(deps: Deps): Promise<number> {
41
43
  if (deps.env.apiKey)
42
44
  deps.stderr(
43
- "Note: ASTEROID_API_KEY is set and takes precedence over this sign-in.\n",
45
+ `Warning: ${API_KEY_OVERRIDES} Every command will use the key, not this sign-in. ${UNSET_API_KEY}\n`,
44
46
  );
45
47
  const client = {
46
48
  clientId: deps.env.workosClientId,
@@ -116,7 +118,7 @@ export async function orgs(
116
118
  ): Promise<number> {
117
119
  if (deps.env.apiKey)
118
120
  throw new UsageError(
119
- "ASTEROID_API_KEY acts in one organization. Run `asteroid whoami` to see it.",
121
+ `${API_KEY_OVERRIDES} The key acts in one organization; \`asteroid whoami\` shows it. ${UNSET_API_KEY}`,
120
122
  );
121
123
  const config = loadConfig(deps.env);
122
124
  const auth = await resolveAuth(
@@ -148,7 +150,7 @@ export async function use(
148
150
  ): Promise<number> {
149
151
  if (deps.env.apiKey)
150
152
  throw new UsageError(
151
- "ASTEROID_API_KEY is pinned to one organization; `use` needs `asteroid login`.",
153
+ `${API_KEY_OVERRIDES} \`use\` only switches the login's organization. ${UNSET_API_KEY}`,
152
154
  );
153
155
  const config = loadConfig(deps.env);
154
156
  if (!ref && deps.ui && (config.orgs?.length ?? 0) > 0) {
@@ -213,6 +215,7 @@ export async function whoami(
213
215
  as: person?.email ?? body.id,
214
216
  kind: body.kind,
215
217
  credential: body.credential,
218
+ source: auth.kind === "api-key" ? "ASTEROID_API_KEY" : "asteroid login",
216
219
  organization,
217
220
  ...(minutes === undefined ? {} : { tokenExpiresIn: `${minutes}m` }),
218
221
  })}\n`,
@@ -12,6 +12,11 @@ import { refresh, tokenClaims, WorkosError } from "./workos";
12
12
 
13
13
  export class AuthError extends Error {}
14
14
 
15
+ export const API_KEY_OVERRIDES =
16
+ "ASTEROID_API_KEY is set, so the CLI uses that key and ignores `asteroid login`.";
17
+ export const UNSET_API_KEY =
18
+ "Run `unset ASTEROID_API_KEY` to use your login instead.";
19
+
15
20
  export interface Auth {
16
21
  kind: "api-key" | "bearer";
17
22
  headers: Record<string, string>;
package/src/http.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Auth } from "./auth/credentials";
1
+ import { type Auth, UNSET_API_KEY } from "./auth/credentials";
2
2
  import type { BuiltRequest } from "./request";
3
3
 
4
4
  export class ApiError extends Error {
@@ -95,11 +95,13 @@ export async function send(
95
95
  } catch {
96
96
  parsed = text;
97
97
  }
98
- throw new ApiError(
99
- response.status,
100
- problem(response.status, parsed, text.slice(0, 200)),
101
- parsed,
102
- );
98
+ let message = problem(response.status, parsed, text.slice(0, 200));
99
+ const sentKey = "X-Api-Key" in hopHeaders;
100
+ if (sentKey && response.status === 401)
101
+ message = `ASTEROID_API_KEY was rejected: ${message}. Check the key. ${UNSET_API_KEY}`;
102
+ else if (sentKey && response.status === 403)
103
+ message = `${message} (sent with ASTEROID_API_KEY. ${UNSET_API_KEY})`;
104
+ throw new ApiError(response.status, message, parsed);
103
105
  }
104
106
  if (binary)
105
107
  return {
@@ -96,4 +96,41 @@ describe("parseInvocation", () => {
96
96
  );
97
97
  expect(got.parsed.positionals).toEqual(["chat-wf"]);
98
98
  });
99
+
100
+ it("adds a per-command help note", async () => {
101
+ const help = await parseInvocation(["workflow", "get", "--help"], {
102
+ commands: COMMANDS,
103
+ helpNote: (command) =>
104
+ command.tool === "workflow_get" ? "Note for get." : undefined,
105
+ });
106
+
107
+ expect(help.kind === "exit" && help.stdout).toContain("Note for get.");
108
+ });
109
+
110
+ it("fills a defaulted first argument ahead of a required one", async () => {
111
+ const options = {
112
+ commands: COMMANDS,
113
+ defaultArg: (_: unknown, arg: { name: string }) =>
114
+ arg.name === "workflowId" ? "chat-wf" : undefined,
115
+ };
116
+
117
+ const short = command(
118
+ await parseInvocation(["schedule", "get", "s-1"], options),
119
+ );
120
+ expect(short.parsed.positionals).toEqual(["chat-wf", "s-1"]);
121
+
122
+ const full = command(
123
+ await parseInvocation(["schedule", "get", "wf-2", "s-1"], options),
124
+ );
125
+ expect(full.parsed.positionals).toEqual(["wf-2", "s-1"]);
126
+
127
+ const help = await parseInvocation(["schedule", "get", "--help"], options);
128
+ expect(help.kind === "exit" && help.stdout).toContain(
129
+ "[workflow-id] <schedule-id>",
130
+ );
131
+
132
+ await expect(parseInvocation(["schedule", "get"], options)).rejects.toThrow(
133
+ "expected [workflow-id] <schedule-id>, got 0 argument(s)",
134
+ );
135
+ });
99
136
  });
package/src/program.ts CHANGED
@@ -16,6 +16,7 @@ export interface ProgramOptions {
16
16
  description?: string;
17
17
  defaultArg?: (command: CliCommand, arg: Option) => string | undefined;
18
18
  defaultArgNote?: string;
19
+ helpNote?: (command: CliCommand) => string | undefined;
19
20
  }
20
21
 
21
22
  const DOTTED = /^--([a-z0-9-]+\.[a-z0-9.-]+)(?:=(.*))?$/;
@@ -155,18 +156,26 @@ export function defineCommands(
155
156
  const defaults = command.args.map((arg) =>
156
157
  options.defaultArg?.(command, arg),
157
158
  );
158
- command.args.forEach((arg, i) => {
159
+ const defaulted = defaults[0] !== undefined;
160
+ command.args.forEach((arg) => {
159
161
  leaf.argument(
160
- defaults[i] === undefined ? `<${arg.flag}>` : `[${arg.flag}]`,
162
+ defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
161
163
  arg.description ?? "",
162
164
  );
163
165
  });
164
- if (
165
- defaults.some((value) => value !== undefined) &&
166
- options.defaultArgNote
167
- ) {
168
- leaf.addHelpText("after", `\n${options.defaultArgNote}`);
166
+ if (defaulted) {
167
+ const names = command.args.map((arg, i) =>
168
+ i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`,
169
+ );
170
+ leaf.usage(`[options] ${names.join(" ")}`);
169
171
  }
172
+ const notes = [
173
+ defaults.some((value) => value !== undefined)
174
+ ? options.defaultArgNote
175
+ : undefined,
176
+ options.helpNote?.(command),
177
+ ].filter(Boolean);
178
+ if (notes.length > 0) leaf.addHelpText("after", `\n${notes.join("\n")}`);
170
179
  leafOptions(leaf, command);
171
180
  leaf.action((...args: unknown[]) => {
172
181
  const cmd = args.at(-1) as Command;
@@ -178,7 +187,9 @@ export function defineCommands(
178
187
  positionals = [defaults[0], ...positionals];
179
188
  }
180
189
  if (positionals.length !== command.args.length) {
181
- const expected = command.args.map((a) => `<${a.flag}>`).join(" ");
190
+ const expected = command.args
191
+ .map((a, i) => (i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`))
192
+ .join(" ");
182
193
  throw new UsageError(
183
194
  `expected ${expected}, got ${cmd.args.length} argument(s)`,
184
195
  );
package/src/run.test.ts CHANGED
@@ -184,6 +184,24 @@ describe("guards", () => {
184
184
  expect(h.calls).toHaveLength(0);
185
185
  });
186
186
 
187
+ it("tells a signed-in user that the API key overrides use", async () => {
188
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, () => json({}));
189
+ await new CredentialStore(h.deps.env).writeLogin({ refreshToken: "r1" });
190
+ expect(await run(["use", "org_A"], h.deps)).toBe(2);
191
+ expect(h.err.join("")).toContain("ignores `asteroid login`");
192
+ expect(h.err.join("")).toContain("unset ASTEROID_API_KEY");
193
+ });
194
+
195
+ it("names the API key when the API rejects it", async () => {
196
+ const h = harness({ ASTEROID_API_KEY: "sk_bad" }, () =>
197
+ json({ detail: "invalid API key" }, 401),
198
+ );
199
+ expect(await run(["workflow", "list"], h.deps)).toBe(1);
200
+ expect(h.err.join("")).toContain(
201
+ "401: ASTEROID_API_KEY was rejected: invalid API key.",
202
+ );
203
+ });
204
+
187
205
  it("rejects extra arguments and unknown flags on account commands", async () => {
188
206
  const h = harness({}, () => json({}));
189
207
  expect(await run(["use", "a", "b"], h.deps)).toBe(2);
@@ -209,6 +227,22 @@ describe("guards", () => {
209
227
  ).toBeUndefined();
210
228
  });
211
229
 
230
+ it("does not blame the API key for an error after a cross-host redirect", async () => {
231
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, (call) =>
232
+ call.url.startsWith("https://odyssey")
233
+ ? new Response(null, {
234
+ status: 303,
235
+ headers: { location: "https://storage.test/file" },
236
+ })
237
+ : new Response("expired", { status: 403 }),
238
+ );
239
+ expect(
240
+ await run(["execution", "file", "download", "e1", "f1"], h.deps),
241
+ ).toBe(1);
242
+ expect(h.err.join("")).toContain("403: expired");
243
+ expect(h.err.join("")).not.toContain("ASTEROID_API_KEY");
244
+ });
245
+
212
246
  it("keeps the method and body on a 307", async () => {
213
247
  const h = harness({ ASTEROID_API_KEY: "sk_test" }, (call) =>
214
248
  call.url.endsWith("/validate")
package/src/run.ts CHANGED
@@ -2,7 +2,13 @@ import { writeFileSync } from "node:fs";
2
2
 
3
3
  import { login, logout, orgs, use, whoami } from "./account";
4
4
  import { type Parsed, UsageError } from "./args";
5
- import { type Auth, AuthError, resolveAuth } from "./auth/credentials";
5
+ import {
6
+ API_KEY_OVERRIDES,
7
+ type Auth,
8
+ AuthError,
9
+ resolveAuth,
10
+ UNSET_API_KEY,
11
+ } from "./auth/credentials";
6
12
  import { WorkosError } from "./auth/workos";
7
13
  import { type Env, loadConfig, orgLabel, requestedOrg } from "./config";
8
14
  import { COMMANDS } from "./generated/commands";
@@ -30,7 +36,7 @@ export interface Deps {
30
36
  function rejectOrgWithApiKey(deps: Deps, flagOrg: string | undefined) {
31
37
  if (deps.env.apiKey && (flagOrg || deps.env.org)) {
32
38
  throw new UsageError(
33
- "ASTEROID_API_KEY acts in its own organization. Unset --org and ASTEROID_ORG, or sign in with `asteroid login`.",
39
+ `${API_KEY_OVERRIDES} The key acts in its own organization, so drop --org and ASTEROID_ORG. ${UNSET_API_KEY}`,
34
40
  );
35
41
  }
36
42
  }