@asteroidai/cli 1.9.12 → 1.9.28

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/src/http.ts DELETED
@@ -1,115 +0,0 @@
1
- import { type Auth, UNSET_API_KEY } from "./auth/credentials";
2
- import type { BuiltRequest } from "./request";
3
-
4
- export class ApiError extends Error {
5
- constructor(
6
- readonly status: number,
7
- message: string,
8
- readonly body: unknown,
9
- ) {
10
- super(message);
11
- }
12
- }
13
-
14
- export interface ApiResponse {
15
- status: number;
16
- json?: unknown;
17
- text?: string;
18
- bytes?: Uint8Array;
19
- }
20
-
21
- export const VERSION = process.env.ASTEROID_CLI_VERSION ?? "dev";
22
-
23
- function problem(status: number, body: unknown, fallback: string): string {
24
- if (body && typeof body === "object") {
25
- const record = body as Record<string, unknown>;
26
- for (const key of ["detail", "message", "error", "title"]) {
27
- if (typeof record[key] === "string" && record[key])
28
- return record[key] as string;
29
- }
30
- }
31
- return fallback.trim() || `HTTP ${status}`;
32
- }
33
-
34
- export async function send(
35
- apiUrl: string,
36
- auth: Auth,
37
- request: BuiltRequest,
38
- binary: boolean,
39
- fetchImpl: typeof fetch = fetch,
40
- ): Promise<ApiResponse> {
41
- const query = request.query.toString();
42
- const url = `${apiUrl}${request.path}${query ? `?${query}` : ""}`;
43
- const headers: Record<string, string> = {
44
- ...auth.headers,
45
- Accept: binary ? "*/*" : "application/json",
46
- "User-Agent": `asteroid-cli/${VERSION}`,
47
- "X-Request-Source": "asteroid-cli",
48
- };
49
- let body: BodyInit | undefined;
50
- if (request.form) body = request.form;
51
- else if (request.json !== undefined) {
52
- headers["Content-Type"] = "application/json";
53
- body = JSON.stringify(request.json);
54
- }
55
-
56
- let response = await fetchImpl(url, {
57
- method: request.method,
58
- headers,
59
- body,
60
- redirect: "manual",
61
- });
62
- let current = new URL(url);
63
- let hopHeaders: Record<string, string> = { ...headers };
64
- let method: string = request.method;
65
- for (let hop = 0; hop < 5; hop++) {
66
- const location = response.headers.get("location");
67
- if (response.status < 300 || response.status >= 400 || !location) break;
68
- const target = new URL(location, current);
69
- if (target.origin !== current.origin) {
70
- hopHeaders = { "User-Agent": headers["User-Agent"] };
71
- if (headers["Content-Type"])
72
- hopHeaders["Content-Type"] = headers["Content-Type"];
73
- }
74
- current = target;
75
- const keepMethod = response.status === 307 || response.status === 308;
76
- if (!keepMethod && method !== "HEAD") {
77
- method = "GET";
78
- body = undefined;
79
- delete hopHeaders["Content-Type"];
80
- }
81
- response = await fetchImpl(target, {
82
- method,
83
- headers: hopHeaders,
84
- body,
85
- redirect: "manual",
86
- });
87
- }
88
- const contentType = response.headers.get("content-type") ?? "";
89
-
90
- if (!response.ok) {
91
- const text = await response.text();
92
- let parsed: unknown = text;
93
- try {
94
- parsed = JSON.parse(text);
95
- } catch {
96
- parsed = text;
97
- }
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);
105
- }
106
- if (binary)
107
- return {
108
- status: response.status,
109
- bytes: new Uint8Array(await response.arrayBuffer()),
110
- };
111
- if (response.status === 204) return { status: 204 };
112
- if (contentType.includes("json"))
113
- return { status: response.status, json: await response.json() };
114
- return { status: response.status, text: await response.text() };
115
- }
package/src/main.ts DELETED
@@ -1,49 +0,0 @@
1
- import { spawn } from "node:child_process";
2
-
3
- import { ALL_COMMANDS } from "./commands";
4
- import { readEnv } from "./config";
5
- import { runCompletion } from "./program";
6
- import { BUILTINS } from "./reference";
7
- import { run } from "./run";
8
- import { clackUi } from "./ui";
9
-
10
- function openUrl(url: string) {
11
- if (process.env.ASTEROID_NO_BROWSER || !/^https:\/\/[^\s"'<>^&|]+$/.test(url))
12
- return;
13
- const [command, args] =
14
- process.platform === "darwin"
15
- ? ["open", [url]]
16
- : process.platform === "win32"
17
- ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
18
- : ["xdg-open", [url]];
19
- try {
20
- spawn(command, args, { stdio: "ignore", detached: true })
21
- .on("error", () => undefined)
22
- .unref();
23
- } catch {
24
- return;
25
- }
26
- }
27
-
28
- const argv = process.argv.slice(2);
29
- if (argv[0] === "complete") {
30
- await runCompletion(argv, { commands: ALL_COMMANDS, builtins: BUILTINS });
31
- process.exit(0);
32
- }
33
-
34
- const code = await run(argv, {
35
- env: readEnv(),
36
- cwd: process.cwd(),
37
- fetch,
38
- stdout: (text) => process.stdout.write(text),
39
- stderr: (text) => process.stderr.write(text),
40
- writeBytes: (bytes) => process.stdout.write(bytes),
41
- ui: process.stdin.isTTY && process.stderr.isTTY ? clackUi : undefined,
42
- stdoutIsTTY: process.stdout.isTTY === true,
43
- styled:
44
- process.stdout.isTTY === true &&
45
- !process.env.NO_COLOR &&
46
- process.env.TERM !== "dumb",
47
- openUrl,
48
- });
49
- process.exitCode = code;
@@ -1,71 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { clean, render } from "./output";
4
-
5
- describe("clean", () => {
6
- it("strips escape sequences and control characters, keeps newlines and tabs", () => {
7
- const hostile =
8
- "a\u001b[31mred\u001b[0m\u001b]0;title\u0007b\rc\u0008d\te\nf\u009bg";
9
- expect(clean(hostile)).toBe("aredbcd\te\nfg");
10
- });
11
- });
12
-
13
- describe("render", () => {
14
- it("cleans server text in tables and records", () => {
15
- const plain = render({ items: [{ id: "1", name: "x\u001b[2Jy" }] });
16
- expect(plain).toContain("xy");
17
- expect(plain).not.toContain("\u001b");
18
- expect(render({ note: "a\u0007b" })).toContain("ab");
19
- });
20
-
21
- it("draws a bordered table only when styled", () => {
22
- const rows = [{ id: "1", name: "Ada" }];
23
- expect(render(rows)).not.toContain("┌");
24
- expect(render(rows, { styled: true })).toContain("┌");
25
- });
26
-
27
- it("prints nested objects in full, not as cut JSON", () => {
28
- const out = render({
29
- workflow: { id: "3cfc3c1d-e38d-4132-a5ee-8847136d491d", name: "Nebula" },
30
- version: { version: 3, isPublished: true },
31
- });
32
- expect(out).toContain("3cfc3c1d-e38d-4132-a5ee-8847136d491d");
33
- expect(out).toMatch(/workflow:\n {2}id +3cfc3c1d/);
34
- expect(out).not.toContain("…");
35
- });
36
-
37
- it("flattens one level of nested fields into table columns", () => {
38
- const out = render([
39
- { date: "2026-10-09", outcomes: { success: 3, failure: 1 } },
40
- ]);
41
- expect(out).toContain("OUTCOMES.SUCCESS");
42
- expect(out).toMatch(/2026-10-09 +3 +1/);
43
- });
44
-
45
- it("hides nested fields that are hidden at the top level", () => {
46
- const out = render([
47
- { id: "1", execution: { agentId: "a-1", status: "failed" } },
48
- ]);
49
- expect(out).toContain("EXECUTION.STATUS");
50
- expect(out).not.toContain("AGENTID");
51
- });
52
-
53
- it("keeps long cells whole when asked", () => {
54
- const secrets = `ECW(51d1db07-a6e1-4b1d-8303-a472b78923e8)[PASSWORD=##ECW_PASSWORD##]`;
55
- const rows = [{ id: "p-1", secrets }];
56
- expect(render(rows)).toContain("…");
57
- expect(render(rows, { full: true })).toContain(secrets);
58
- });
59
-
60
- it("shows every column when asked, six otherwise", () => {
61
- const row = Object.fromEntries(
62
- ["id", "a", "b", "c", "d", "e", "f", "g"].map((key) => [
63
- key,
64
- key.toUpperCase(),
65
- ]),
66
- );
67
- const header = (out: string) => out.split("\n")[0].trim().split(/\s+/);
68
- expect(header(render([row]))).toHaveLength(6);
69
- expect(header(render([row], { full: true }))).toHaveLength(8);
70
- });
71
- });
package/src/output.ts DELETED
@@ -1,176 +0,0 @@
1
- import Table from "cli-table3";
2
- import { styleText } from "node:util";
3
-
4
- const PREFERRED = [
5
- "id",
6
- "name",
7
- "title",
8
- "workflowName",
9
- "status",
10
- "outcome",
11
- "version",
12
- "createdAt",
13
- "updatedAt",
14
- ];
15
- const HIDDEN = /^agent[A-Z]|Url$/;
16
- const MAX_COLUMNS = 6;
17
- const MAX_CELL = 60;
18
- const CONTROL =
19
- /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]?|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
20
-
21
- type Row = Record<string, unknown>;
22
-
23
- export interface RenderOptions {
24
- styled?: boolean;
25
- full?: boolean;
26
- }
27
-
28
- export function clean(text: string): string {
29
- return text.replace(CONTROL, "");
30
- }
31
-
32
- function isObject(value: unknown): boolean {
33
- return typeof value === "object" && value !== null && !Array.isArray(value);
34
- }
35
-
36
- function isScalar(value: unknown): boolean {
37
- return (
38
- value === null || ["string", "number", "boolean"].includes(typeof value)
39
- );
40
- }
41
-
42
- function indent(text: string): string {
43
- return text
44
- .split("\n")
45
- .map((line) => ` ${line}`)
46
- .join("\n");
47
- }
48
-
49
- function text(value: unknown): string {
50
- return clean(isScalar(value) ? String(value) : JSON.stringify(value));
51
- }
52
-
53
- function cell(value: unknown, options: RenderOptions = {}): string {
54
- if (value === null || value === undefined) return "";
55
- const line = text(value).replace(/\s+/g, " ");
56
- if (options.full || line.length <= MAX_CELL) return line;
57
- return `${line.slice(0, MAX_CELL - 1)}…`;
58
- }
59
-
60
- function columns(rows: Row[], options: RenderOptions): string[] {
61
- const scalarKeys: string[] = [];
62
- for (const row of rows) {
63
- for (const [key, value] of Object.entries(row)) {
64
- if (
65
- isScalar(value) &&
66
- !HIDDEN.test(key.split(".").at(-1) ?? key) &&
67
- !scalarKeys.includes(key)
68
- )
69
- scalarKeys.push(key);
70
- }
71
- }
72
- const ordered = [
73
- ...PREFERRED.filter((key) => scalarKeys.includes(key)),
74
- ...scalarKeys.filter((key) => !PREFERRED.includes(key)),
75
- ];
76
- return options.full ? ordered : ordered.slice(0, MAX_COLUMNS);
77
- }
78
-
79
- function style(
80
- format: Parameters<typeof styleText>[0],
81
- value: string,
82
- options: RenderOptions,
83
- ) {
84
- return options.styled
85
- ? styleText(format, value, { validateStream: false })
86
- : value;
87
- }
88
-
89
- function flatten(row: Row): Row {
90
- const flat: Row = {};
91
- for (const [key, value] of Object.entries(row)) {
92
- if (!isObject(value)) {
93
- flat[key] = value;
94
- continue;
95
- }
96
- for (const [inner, v] of Object.entries(value as Row))
97
- flat[`${key}.${inner}`] = v;
98
- }
99
- return flat;
100
- }
101
-
102
- export function table(input: Row[], options: RenderOptions = {}): string {
103
- if (input.length === 0) return "No results.";
104
- const rows = input.map(flatten);
105
- const keys = columns(rows, options);
106
- if (keys.length === 0) return rows.map((row) => text(row)).join("\n");
107
- const cells = rows.map((row) => keys.map((key) => cell(row[key], options)));
108
- if (options.styled) {
109
- const grid = new Table({
110
- head: keys.map((key) => style("bold", key, options)),
111
- style: { head: [], border: [] },
112
- });
113
- grid.push(...cells);
114
- return grid.toString();
115
- }
116
- const widths = keys.map((key, i) =>
117
- Math.max(key.length, ...cells.map((r) => r[i].length)),
118
- );
119
- const line = (values: string[]) =>
120
- values
121
- .map((v, i) => v.padEnd(widths[i]))
122
- .join(" ")
123
- .trimEnd();
124
- return [line(keys.map((k) => k.toUpperCase())), ...cells.map(line)].join(
125
- "\n",
126
- );
127
- }
128
-
129
- function summary(value: unknown, options: RenderOptions): string {
130
- if (Array.isArray(value))
131
- return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
132
- if (value && typeof value === "object") return cell(value, options);
133
- return text(value);
134
- }
135
-
136
- function record(value: Row, options: RenderOptions): string {
137
- const width = Math.max(...Object.keys(value).map((key) => key.length));
138
- const label = (key: string) => style("bold", key.padEnd(width), options);
139
- return Object.entries(value)
140
- .map(([key, v]) => {
141
- if (Array.isArray(v) && v.length > 0 && v.every(isObject)) {
142
- return `${style("bold", key, options)}:\n${indent(table(v as Row[], options))}`;
143
- }
144
- if (Array.isArray(v) && v.length > 0 && v.every(isScalar)) {
145
- return `${label(key)} ${v.map(text).join(`\n${" ".repeat(width + 2)}`)}`;
146
- }
147
- if (isObject(v) && Object.keys(v as Row).length > 0) {
148
- return `${style("bold", key, options)}:\n${indent(record(v as Row, options))}`;
149
- }
150
- return `${label(key)} ${isScalar(v) ? text(v) : summary(v, options)}`;
151
- })
152
- .join("\n");
153
- }
154
-
155
- export function render(value: unknown, options: RenderOptions = {}): string {
156
- if (value === undefined) return "";
157
- if (Array.isArray(value)) {
158
- return value.every(isObject)
159
- ? table(value as Row[], options)
160
- : value.map(text).join("\n");
161
- }
162
- if (isObject(value)) {
163
- const row = value as Row;
164
- if (Array.isArray(row.items)) {
165
- const rest = Object.entries(row).filter(
166
- ([k, v]) => k !== "items" && isScalar(v),
167
- );
168
- const footer = rest.length
169
- ? `\n\n${style("dim", rest.map(([k, v]) => `${k}: ${text(v)}`).join(" "), options)}`
170
- : "";
171
- return `${table(row.items as Row[], options)}${footer}`;
172
- }
173
- return record(row, options);
174
- }
175
- return text(value);
176
- }
@@ -1,155 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { COMMANDS } from "./generated/commands";
4
- import { type Invocation, parseInvocation } from "./program";
5
- import { BUILTINS } from "./reference";
6
-
7
- const parse = (argv: string[]) =>
8
- parseInvocation(argv, { commands: COMMANDS, builtins: BUILTINS });
9
-
10
- function command(invocation: Invocation) {
11
- if (invocation.kind !== "command")
12
- throw new Error(`expected a command, got ${invocation.kind}`);
13
- return invocation;
14
- }
15
-
16
- describe("parseInvocation", () => {
17
- it("collects repeated flags, globals anywhere, and positionals", async () => {
18
- const got = command(
19
- await parse([
20
- "--json",
21
- "execution",
22
- "list",
23
- "--status",
24
- "failed",
25
- "--status",
26
- "completed",
27
- "--org",
28
- "acme",
29
- ]),
30
- );
31
- expect(got.command.tool).toBe("executions_list");
32
- expect(got.parsed.flags.get("status")).toEqual(["failed", "completed"]);
33
- expect(got.parsed.json).toBe(true);
34
- expect(got.parsed.org).toBe("acme");
35
- });
36
-
37
- it("keeps dotted body flags for the request builder", async () => {
38
- const got = command(
39
- await parse([
40
- "workflow",
41
- "execute",
42
- "w-1",
43
- "--execution-options.variant-key",
44
- "canary",
45
- ]),
46
- );
47
- expect(got.parsed.positionals).toEqual(["w-1"]);
48
- expect(got.parsed.flags.get("execution-options.variant-key")).toEqual([
49
- "canary",
50
- ]);
51
- });
52
-
53
- it("rejects a dotted flag no body field owns", async () => {
54
- await expect(
55
- parse(["workflow", "list", "--nope.deep", "1"]),
56
- ).rejects.toThrow(/unknown flag: --nope.deep/);
57
- });
58
-
59
- it("reads --no-<flag> as false", async () => {
60
- const got = command(
61
- await parse(["schedule", "update", "w-1", "s-1", "--no-enabled"]),
62
- );
63
- expect(got.parsed.flags.get("enabled")).toEqual(["false"]);
64
- });
65
-
66
- it("routes account commands as builtins and checks their arguments", async () => {
67
- const use = await parse(["use", "acme"]);
68
- expect(use).toMatchObject({ kind: "builtin", name: "use" });
69
- expect(await parse(["use", "a", "b"])).toMatchObject({
70
- kind: "exit",
71
- code: 2,
72
- });
73
- expect(await parse(["logout", "--typo", "x"])).toMatchObject({
74
- kind: "exit",
75
- code: 2,
76
- });
77
- });
78
-
79
- it("shows group and command help with exit 0", async () => {
80
- const group = await parse(["workflow", "version"]);
81
- expect(group).toMatchObject({ kind: "exit", code: 0 });
82
- expect(group.kind === "exit" && group.stdout + group.stderr).toContain(
83
- "publish",
84
- );
85
- const leaf = await parse(["workflow", "get", "--help"]);
86
- expect(leaf.kind === "exit" && leaf.stdout).toContain("<workflow-id>");
87
- });
88
-
89
- it("fills a defaulted first argument when it is left out", async () => {
90
- const got = command(
91
- await parseInvocation(["workflow", "tag", "list"], {
92
- commands: COMMANDS,
93
- defaultArg: (_, arg) =>
94
- arg.name === "workflowId" ? "chat-wf" : undefined,
95
- }),
96
- );
97
- expect(got.parsed.positionals).toEqual(["chat-wf"]);
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
- });
136
-
137
- it("prints the version only as the first argument", async () => {
138
- const options = {
139
- commands: COMMANDS,
140
- builtins: BUILTINS,
141
- version: "1.2.3",
142
- };
143
-
144
- const version = await parseInvocation(["--version"], options);
145
- const create = await parseInvocation(
146
- ["workflow", "create", "--name", "W", "--version", "{}"],
147
- options,
148
- );
149
-
150
- expect(version).toMatchObject({ kind: "exit", code: 0, stdout: "1.2.3\n" });
151
- expect(create.kind).toBe("command");
152
- if (create.kind === "command")
153
- expect(create.parsed.flags.get("version")).toEqual(["{}"]);
154
- });
155
- });