@duvoai/cli 0.0.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.
Files changed (71) hide show
  1. package/dist/api/agent-schemas.d.ts +94 -0
  2. package/dist/api/agent-schemas.js +16 -0
  3. package/dist/api/parse-fields.d.ts +25 -0
  4. package/dist/api/parse-fields.js +86 -0
  5. package/dist/api/read-input.d.ts +6 -0
  6. package/dist/api/read-input.js +28 -0
  7. package/dist/bin/create-program.d.ts +7 -0
  8. package/dist/bin/create-program.js +27 -0
  9. package/dist/bin/duvo.d.ts +2 -0
  10. package/dist/bin/duvo.js +8 -0
  11. package/dist/client/client.d.ts +23 -0
  12. package/dist/client/client.js +96 -0
  13. package/dist/commands/agents/get.d.ts +2 -0
  14. package/dist/commands/agents/get.js +42 -0
  15. package/dist/commands/agents/index.d.ts +2 -0
  16. package/dist/commands/agents/index.js +9 -0
  17. package/dist/commands/agents/list.d.ts +2 -0
  18. package/dist/commands/agents/list.js +67 -0
  19. package/dist/commands/api-render.d.ts +8 -0
  20. package/dist/commands/api-render.js +72 -0
  21. package/dist/commands/api.d.ts +2 -0
  22. package/dist/commands/api.js +98 -0
  23. package/dist/commands/login.d.ts +2 -0
  24. package/dist/commands/login.js +80 -0
  25. package/dist/commands/logout.d.ts +2 -0
  26. package/dist/commands/logout.js +38 -0
  27. package/dist/commands/profiles/index.d.ts +2 -0
  28. package/dist/commands/profiles/index.js +13 -0
  29. package/dist/commands/profiles/list.d.ts +2 -0
  30. package/dist/commands/profiles/list.js +22 -0
  31. package/dist/commands/profiles/remove.d.ts +2 -0
  32. package/dist/commands/profiles/remove.js +30 -0
  33. package/dist/commands/profiles/rename.d.ts +2 -0
  34. package/dist/commands/profiles/rename.js +20 -0
  35. package/dist/commands/profiles/render-list.d.ts +12 -0
  36. package/dist/commands/profiles/render-list.js +37 -0
  37. package/dist/commands/profiles/use.d.ts +2 -0
  38. package/dist/commands/profiles/use.js +17 -0
  39. package/dist/commands/shared/confirm.d.ts +19 -0
  40. package/dist/commands/shared/confirm.js +28 -0
  41. package/dist/commands/shared/pagination.d.ts +8 -0
  42. package/dist/commands/shared/pagination.js +24 -0
  43. package/dist/commands/shared/profile-flag.d.ts +8 -0
  44. package/dist/commands/shared/profile-flag.js +17 -0
  45. package/dist/commands/whoami.d.ts +2 -0
  46. package/dist/commands/whoami.js +21 -0
  47. package/dist/config/defaults.d.ts +3 -0
  48. package/dist/config/defaults.js +4 -0
  49. package/dist/config/env.d.ts +17 -0
  50. package/dist/config/env.js +13 -0
  51. package/dist/config/paths.d.ts +2 -0
  52. package/dist/config/paths.js +14 -0
  53. package/dist/config/profiles.d.ts +24 -0
  54. package/dist/config/profiles.js +78 -0
  55. package/dist/config/resolve.d.ts +29 -0
  56. package/dist/config/resolve.js +46 -0
  57. package/dist/config/store.d.ts +4 -0
  58. package/dist/config/store.js +94 -0
  59. package/dist/config/types.d.ts +8 -0
  60. package/dist/config/types.js +1 -0
  61. package/dist/errors.d.ts +22 -0
  62. package/dist/errors.js +42 -0
  63. package/dist/output/format.d.ts +11 -0
  64. package/dist/output/format.js +26 -0
  65. package/dist/output/json.d.ts +6 -0
  66. package/dist/output/json.js +8 -0
  67. package/dist/output/table.d.ts +16 -0
  68. package/dist/output/table.js +35 -0
  69. package/dist/output/text.d.ts +9 -0
  70. package/dist/output/text.js +13 -0
  71. package/package.json +36 -0
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ declare const publicAgentSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ name: z.ZodString;
5
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
6
+ updated_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ id: string;
9
+ name: string;
10
+ created_at?: string | null | undefined;
11
+ updated_at?: string | null | undefined;
12
+ }, {
13
+ id: string;
14
+ name: string;
15
+ created_at?: string | null | undefined;
16
+ updated_at?: string | null | undefined;
17
+ }>;
18
+ export type PublicAgent = z.infer<typeof publicAgentSchema>;
19
+ export declare const listAgentsResponseSchema: z.ZodObject<{
20
+ agents: z.ZodArray<z.ZodObject<{
21
+ id: z.ZodString;
22
+ name: z.ZodString;
23
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ updated_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ id: string;
27
+ name: string;
28
+ created_at?: string | null | undefined;
29
+ updated_at?: string | null | undefined;
30
+ }, {
31
+ id: string;
32
+ name: string;
33
+ created_at?: string | null | undefined;
34
+ updated_at?: string | null | undefined;
35
+ }>, "many">;
36
+ total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
37
+ limit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
38
+ offset: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
39
+ }, "strip", z.ZodTypeAny, {
40
+ agents: {
41
+ id: string;
42
+ name: string;
43
+ created_at?: string | null | undefined;
44
+ updated_at?: string | null | undefined;
45
+ }[];
46
+ total?: number | null | undefined;
47
+ limit?: number | null | undefined;
48
+ offset?: number | null | undefined;
49
+ }, {
50
+ agents: {
51
+ id: string;
52
+ name: string;
53
+ created_at?: string | null | undefined;
54
+ updated_at?: string | null | undefined;
55
+ }[];
56
+ total?: number | null | undefined;
57
+ limit?: number | null | undefined;
58
+ offset?: number | null | undefined;
59
+ }>;
60
+ export type ListAgentsResponse = z.infer<typeof listAgentsResponseSchema>;
61
+ export declare const getAgentResponseSchema: z.ZodObject<{
62
+ agent: z.ZodObject<{
63
+ id: z.ZodString;
64
+ name: z.ZodString;
65
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ updated_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ }, "strip", z.ZodTypeAny, {
68
+ id: string;
69
+ name: string;
70
+ created_at?: string | null | undefined;
71
+ updated_at?: string | null | undefined;
72
+ }, {
73
+ id: string;
74
+ name: string;
75
+ created_at?: string | null | undefined;
76
+ updated_at?: string | null | undefined;
77
+ }>;
78
+ }, "strip", z.ZodTypeAny, {
79
+ agent: {
80
+ id: string;
81
+ name: string;
82
+ created_at?: string | null | undefined;
83
+ updated_at?: string | null | undefined;
84
+ };
85
+ }, {
86
+ agent: {
87
+ id: string;
88
+ name: string;
89
+ created_at?: string | null | undefined;
90
+ updated_at?: string | null | undefined;
91
+ };
92
+ }>;
93
+ export type GetAgentResponse = z.infer<typeof getAgentResponseSchema>;
94
+ export {};
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ const publicAgentSchema = z.object({
3
+ id: z.string(),
4
+ name: z.string(),
5
+ created_at: z.string().nullish(),
6
+ updated_at: z.string().nullish(),
7
+ });
8
+ export const listAgentsResponseSchema = z.object({
9
+ agents: z.array(publicAgentSchema),
10
+ total: z.number().nullish(),
11
+ limit: z.number().nullish(),
12
+ offset: z.number().nullish(),
13
+ });
14
+ export const getAgentResponseSchema = z.object({
15
+ agent: publicAgentSchema,
16
+ });
@@ -0,0 +1,25 @@
1
+ type Field = {
2
+ key: string;
3
+ value: unknown;
4
+ };
5
+ /**
6
+ * Parses a `-f key=value` pair. The value is always kept as a string,
7
+ * matching the `gh api` convention.
8
+ */
9
+ export declare function parseStringField(input: string): Field;
10
+ /**
11
+ * Parses a `-F key=value` pair. The value is coerced to a typed value
12
+ * when it matches one of: `null`, `true`, `false`, a number, or
13
+ * `@path/to/file.json` (inlines the file's parsed JSON). Anything else
14
+ * falls through as a string.
15
+ */
16
+ export declare function parseTypedField(input: string): Field;
17
+ /**
18
+ * Combines repeated `-f` and `-F` flag values into a single object.
19
+ * String fields (`-f`) are applied first, then typed fields (`-F`), so
20
+ * typed fields win on key collision regardless of CLI argument order.
21
+ * Commander collects the two flags into separate arrays, so the original
22
+ * interleaved order is lost by the time we get here.
23
+ */
24
+ export declare function buildFieldObject(stringFields?: readonly string[], typedFields?: readonly string[]): Record<string, unknown>;
25
+ export {};
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "node:fs";
2
+ const INTEGER_OR_DECIMAL = /^-?\d+(\.\d+)?$/;
3
+ /**
4
+ * Parses a `-f key=value` pair. The value is always kept as a string,
5
+ * matching the `gh api` convention.
6
+ */
7
+ export function parseStringField(input) {
8
+ const eq = input.indexOf("=");
9
+ if (eq === -1) {
10
+ throw new Error(`Invalid field "${input}". Expected "key=value" (e.g. -f name="Bot").`);
11
+ }
12
+ const key = input.slice(0, eq);
13
+ if (key.length === 0) {
14
+ throw new Error(`Invalid field "${input}". Key cannot be empty (e.g. -f name="Bot").`);
15
+ }
16
+ return { key, value: input.slice(eq + 1) };
17
+ }
18
+ /**
19
+ * Parses a `-F key=value` pair. The value is coerced to a typed value
20
+ * when it matches one of: `null`, `true`, `false`, a number, or
21
+ * `@path/to/file.json` (inlines the file's parsed JSON). Anything else
22
+ * falls through as a string.
23
+ */
24
+ export function parseTypedField(input) {
25
+ const eq = input.indexOf("=");
26
+ if (eq === -1) {
27
+ throw new Error(`Invalid field "${input}". Expected "key=value" (e.g. -F count=42).`);
28
+ }
29
+ const key = input.slice(0, eq);
30
+ if (key.length === 0) {
31
+ throw new Error(`Invalid field "${input}". Key cannot be empty (e.g. -F count=42).`);
32
+ }
33
+ const raw = input.slice(eq + 1);
34
+ if (raw.startsWith("@")) {
35
+ const path = raw.slice(1);
36
+ return { key, value: readJsonFile(path) };
37
+ }
38
+ if (raw === "null") {
39
+ return { key, value: null };
40
+ }
41
+ if (raw === "true") {
42
+ return { key, value: true };
43
+ }
44
+ if (raw === "false") {
45
+ return { key, value: false };
46
+ }
47
+ if (INTEGER_OR_DECIMAL.test(raw)) {
48
+ return { key, value: Number(raw) };
49
+ }
50
+ return { key, value: raw };
51
+ }
52
+ /**
53
+ * Combines repeated `-f` and `-F` flag values into a single object.
54
+ * String fields (`-f`) are applied first, then typed fields (`-F`), so
55
+ * typed fields win on key collision regardless of CLI argument order.
56
+ * Commander collects the two flags into separate arrays, so the original
57
+ * interleaved order is lost by the time we get here.
58
+ */
59
+ export function buildFieldObject(stringFields = [], typedFields = []) {
60
+ const result = {};
61
+ for (const input of stringFields) {
62
+ const { key, value } = parseStringField(input);
63
+ result[key] = value;
64
+ }
65
+ for (const input of typedFields) {
66
+ const { key, value } = parseTypedField(input);
67
+ result[key] = value;
68
+ }
69
+ return result;
70
+ }
71
+ function readJsonFile(path) {
72
+ let contents;
73
+ try {
74
+ contents = readFileSync(path, "utf8");
75
+ }
76
+ catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ throw new Error(`Cannot read "${path}": ${message}`);
79
+ }
80
+ try {
81
+ return JSON.parse(contents);
82
+ }
83
+ catch {
84
+ throw new Error(`File "${path}" is not valid JSON.`);
85
+ }
86
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Reads a raw JSON body from a file path or from stdin (when path is
3
+ * `"-"`). Returns the parsed JSON value. Throws if the input is not
4
+ * valid JSON or the file cannot be read.
5
+ */
6
+ export declare function readInputJson(path: string): unknown;
@@ -0,0 +1,28 @@
1
+ import { readFileSync } from "node:fs";
2
+ /**
3
+ * Reads a raw JSON body from a file path or from stdin (when path is
4
+ * `"-"`). Returns the parsed JSON value. Throws if the input is not
5
+ * valid JSON or the file cannot be read.
6
+ */
7
+ export function readInputJson(path) {
8
+ const contents = path === "-" ? readStdin() : readFile(path);
9
+ try {
10
+ return JSON.parse(contents);
11
+ }
12
+ catch {
13
+ const source = path === "-" ? "stdin" : `"${path}"`;
14
+ throw new Error(`Input from ${source} is not valid JSON.`);
15
+ }
16
+ }
17
+ function readFile(path) {
18
+ try {
19
+ return readFileSync(path, "utf8");
20
+ }
21
+ catch (error) {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ throw new Error(`Cannot read "${path}": ${message}`);
24
+ }
25
+ }
26
+ function readStdin() {
27
+ return readFileSync(0, "utf8");
28
+ }
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Build a fresh `Command` tree for the CLI. Shared between the real
4
+ * binary (`bin/duvo.ts`) and the test harness so tests can never
5
+ * silently drift out of sync with the production command set.
6
+ */
7
+ export declare function createProgram(): Command;
@@ -0,0 +1,27 @@
1
+ import { Command } from "commander";
2
+ import { registerAgents } from "../commands/agents/index.js";
3
+ import { registerApi } from "../commands/api.js";
4
+ import { registerLogin } from "../commands/login.js";
5
+ import { registerLogout } from "../commands/logout.js";
6
+ import { registerProfiles } from "../commands/profiles/index.js";
7
+ import { registerWhoami } from "../commands/whoami.js";
8
+ /**
9
+ * Build a fresh `Command` tree for the CLI. Shared between the real
10
+ * binary (`bin/duvo.ts`) and the test harness so tests can never
11
+ * silently drift out of sync with the production command set.
12
+ */
13
+ export function createProgram() {
14
+ const program = new Command();
15
+ program
16
+ .name("duvo")
17
+ .description("Command-line interface for the Duvo public API")
18
+ .version("0.0.0")
19
+ .option("--profile <name>", "use a specific profile for this invocation (overrides default profile)");
20
+ registerLogin(program);
21
+ registerLogout(program);
22
+ registerWhoami(program);
23
+ registerProfiles(program);
24
+ registerApi(program);
25
+ registerAgents(program);
26
+ return program;
27
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { createProgram } from "../bin/create-program.js";
3
+ import { handleCliError } from "../errors.js";
4
+ createProgram()
5
+ .parseAsync(process.argv)
6
+ .catch((error) => {
7
+ handleCliError(error);
8
+ });
@@ -0,0 +1,23 @@
1
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
2
+ export type HttpRequestOptions = {
3
+ method: string;
4
+ path: string;
5
+ apiKey: string;
6
+ apiBaseUrl: string;
7
+ /** JSON body. Omitted for GET/HEAD-style requests. */
8
+ body?: unknown;
9
+ /** Appended to the resolved URL. */
10
+ searchParams?: URLSearchParams;
11
+ /** Request timeout in ms. Defaults to {@link DEFAULT_REQUEST_TIMEOUT_MS}. */
12
+ timeoutMs?: number;
13
+ };
14
+ /**
15
+ * Makes an authenticated request to the Duvo public API and returns the
16
+ * parsed JSON response. Maps well-known status codes to `CliError` with
17
+ * the right exit code; other non-2xx responses throw a generic
18
+ * `CliError` containing the server's response body.
19
+ *
20
+ * Empty response bodies resolve to `null`. Non-JSON responses resolve to
21
+ * the raw text — the caller decides how to render it.
22
+ */
23
+ export declare function httpRequest(options: HttpRequestOptions): Promise<unknown>;
@@ -0,0 +1,96 @@
1
+ import { CliError, EXIT_CODE } from "../errors.js";
2
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
3
+ /**
4
+ * Makes an authenticated request to the Duvo public API and returns the
5
+ * parsed JSON response. Maps well-known status codes to `CliError` with
6
+ * the right exit code; other non-2xx responses throw a generic
7
+ * `CliError` containing the server's response body.
8
+ *
9
+ * Empty response bodies resolve to `null`. Non-JSON responses resolve to
10
+ * the raw text — the caller decides how to render it.
11
+ */
12
+ export async function httpRequest(options) {
13
+ const url = buildUrl(options.apiBaseUrl, options.path, options.searchParams);
14
+ const headers = {
15
+ Authorization: `Bearer ${options.apiKey}`,
16
+ Accept: "application/json",
17
+ };
18
+ const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
19
+ const init = {
20
+ method: options.method,
21
+ headers,
22
+ signal: AbortSignal.timeout(timeoutMs),
23
+ };
24
+ if (options.body !== undefined) {
25
+ headers["Content-Type"] = "application/json";
26
+ init.body = JSON.stringify(options.body);
27
+ }
28
+ try {
29
+ const response = await fetch(url, init);
30
+ return await handleResponse(response, options.method, options.path);
31
+ }
32
+ catch (error) {
33
+ if (isTimeoutError(error)) {
34
+ throw new CliError(`Request timed out after ${timeoutMs}ms: ${options.method} ${options.path}`);
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ function isTimeoutError(error) {
40
+ return (error instanceof DOMException &&
41
+ (error.name === "TimeoutError" || error.name === "AbortError"));
42
+ }
43
+ /**
44
+ * Resolves an API path against the configured base URL. The base URL must
45
+ * be an origin only (scheme + host + optional port) — any path component
46
+ * is discarded, because `new URL("/v1/agents", "https://host/foo/")`
47
+ * resolves to `https://host/v1/agents`, not `https://host/foo/v1/agents`.
48
+ */
49
+ const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//;
50
+ function buildUrl(base, path, searchParams) {
51
+ if (ABSOLUTE_URL.test(path)) {
52
+ throw new CliError(`Invalid path "${path}". Expected an API path like "/v1/agents" — absolute URLs are not allowed.`);
53
+ }
54
+ const normalized = path.startsWith("/") ? path : `/${path}`;
55
+ const url = new URL(normalized, ensureTrailingSlash(base));
56
+ if (searchParams) {
57
+ for (const [key, value] of searchParams) {
58
+ url.searchParams.append(key, value);
59
+ }
60
+ }
61
+ return url;
62
+ }
63
+ function ensureTrailingSlash(base) {
64
+ return base.endsWith("/") ? base : `${base}/`;
65
+ }
66
+ async function handleResponse(response, method, path) {
67
+ if (response.status === 401) {
68
+ throw new CliError("Unauthorized. Check that the API key is valid.", EXIT_CODE.AUTH);
69
+ }
70
+ if (response.status === 404) {
71
+ throw new CliError(`Not found: ${method} ${path}`, EXIT_CODE.NOT_FOUND);
72
+ }
73
+ if (!response.ok) {
74
+ const detail = await safeReadText(response);
75
+ const suffix = detail.length > 0 ? `: ${detail}` : "";
76
+ throw new CliError(`Request failed (${response.status} ${response.statusText})${suffix}`);
77
+ }
78
+ const text = await safeReadText(response);
79
+ if (text.length === 0) {
80
+ return null;
81
+ }
82
+ try {
83
+ return JSON.parse(text);
84
+ }
85
+ catch {
86
+ return text;
87
+ }
88
+ }
89
+ async function safeReadText(response) {
90
+ try {
91
+ return await response.text();
92
+ }
93
+ catch {
94
+ return "";
95
+ }
96
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerAgentsGet(agents: Command): void;
@@ -0,0 +1,42 @@
1
+ import { getAgentResponseSchema } from "../../api/agent-schemas.js";
2
+ import { httpRequest } from "../../client/client.js";
3
+ import { resolveConfig } from "../../config/resolve.js";
4
+ import { AuthCliError, CliError } from "../../errors.js";
5
+ import { printJson } from "../../output/json.js";
6
+ import { renderKeyValue } from "../../output/text.js";
7
+ import { getProfileFlag } from "../shared/profile-flag.js";
8
+ export function registerAgentsGet(agents) {
9
+ agents
10
+ .command("get <id>")
11
+ .description("Get an agent by ID")
12
+ .option("--json", "output raw JSON from the API")
13
+ .action(async function getAction(id, options) {
14
+ const resolved = await resolveConfig({
15
+ profileFlag: getProfileFlag(this),
16
+ });
17
+ if (!resolved.apiKey) {
18
+ throw new AuthCliError("Not logged in. Run `duvo login` to save an API key.");
19
+ }
20
+ const raw = await httpRequest({
21
+ method: "GET",
22
+ path: `/v1/agents/${encodeURIComponent(id)}`,
23
+ apiKey: resolved.apiKey,
24
+ apiBaseUrl: resolved.apiBaseUrl,
25
+ });
26
+ if (options.json) {
27
+ printJson(raw);
28
+ return;
29
+ }
30
+ const parsed = getAgentResponseSchema.safeParse(raw);
31
+ if (!parsed.success) {
32
+ throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
33
+ }
34
+ const { agent } = parsed.data;
35
+ console.log(renderKeyValue([
36
+ { label: "ID", value: agent.id },
37
+ { label: "Name", value: agent.name },
38
+ { label: "Created", value: agent.created_at ?? "" },
39
+ { label: "Updated", value: agent.updated_at ?? "" },
40
+ ]));
41
+ });
42
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerAgents(program: Command): void;
@@ -0,0 +1,9 @@
1
+ import { registerAgentsGet } from "./get.js";
2
+ import { registerAgentsList } from "./list.js";
3
+ export function registerAgents(program) {
4
+ const agents = program
5
+ .command("agents")
6
+ .description("Manage agents in the Duvo public API");
7
+ registerAgentsList(agents);
8
+ registerAgentsGet(agents);
9
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerAgentsList(agents: Command): void;
@@ -0,0 +1,67 @@
1
+ import { listAgentsResponseSchema, } from "../../api/agent-schemas.js";
2
+ import { httpRequest } from "../../client/client.js";
3
+ import { resolveConfig } from "../../config/resolve.js";
4
+ import { AuthCliError, CliError } from "../../errors.js";
5
+ import { printJson } from "../../output/json.js";
6
+ import { renderTable } from "../../output/table.js";
7
+ import { parseLimit, parseOffset } from "../shared/pagination.js";
8
+ import { getProfileFlag } from "../shared/profile-flag.js";
9
+ const DEFAULT_LIMIT = 20;
10
+ const MIN_LIMIT = 1;
11
+ const MAX_LIMIT = 100;
12
+ const TABLE_COLUMNS = [
13
+ { header: "ID", cell: (row) => row.id },
14
+ { header: "NAME", cell: (row) => row.name },
15
+ { header: "CREATED", cell: (row) => row.created_at ?? "" },
16
+ ];
17
+ export function registerAgentsList(agents) {
18
+ agents
19
+ .command("list", { isDefault: true })
20
+ .description("List agents for your team")
21
+ .option("--limit <n>", `page size (${MIN_LIMIT}-${MAX_LIMIT})`)
22
+ .option("--offset <n>", "number of agents to skip")
23
+ .option("--json", "output raw JSON from the API")
24
+ .action(async function listAction(options) {
25
+ const limit = parseLimit(options.limit, {
26
+ defaultValue: DEFAULT_LIMIT,
27
+ min: MIN_LIMIT,
28
+ max: MAX_LIMIT,
29
+ });
30
+ const offset = parseOffset(options.offset);
31
+ const resolved = await resolveConfig({
32
+ profileFlag: getProfileFlag(this),
33
+ });
34
+ if (!resolved.apiKey) {
35
+ throw new AuthCliError("Not logged in. Run `duvo login` to save an API key.");
36
+ }
37
+ const searchParams = new URLSearchParams();
38
+ searchParams.append("limit", String(limit));
39
+ searchParams.append("offset", String(offset));
40
+ const raw = await httpRequest({
41
+ method: "GET",
42
+ path: "/v1/agents",
43
+ apiKey: resolved.apiKey,
44
+ apiBaseUrl: resolved.apiBaseUrl,
45
+ searchParams,
46
+ });
47
+ if (options.json) {
48
+ printJson(raw);
49
+ return;
50
+ }
51
+ const parsed = listAgentsResponseSchema.safeParse(raw);
52
+ if (!parsed.success) {
53
+ throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
54
+ }
55
+ const response = parsed.data;
56
+ if (response.agents.length === 0) {
57
+ console.log("No agents found.");
58
+ return;
59
+ }
60
+ console.log(renderTable(TABLE_COLUMNS, response.agents));
61
+ const resolvedOffset = response.offset ?? offset;
62
+ const summary = response.total === null || response.total === undefined
63
+ ? `Showing ${response.agents.length} · offset ${resolvedOffset}`
64
+ : `Showing ${response.agents.length} of ${response.total} · offset ${resolvedOffset}`;
65
+ console.log(summary);
66
+ });
67
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Routes the response through the shared output pipeline per GUIDE.md:
3
+ * collections render as a table, single objects as a key/value block,
4
+ * everything else (and `--json`) falls back to pretty JSON. `duvo api`
5
+ * has no schema, so columns and labels come straight from the response
6
+ * keys.
7
+ */
8
+ export declare function renderApiResponse(response: unknown, asJson: boolean): void;
@@ -0,0 +1,72 @@
1
+ import { printJson } from "../output/json.js";
2
+ import { renderTable } from "../output/table.js";
3
+ import { renderKeyValue } from "../output/text.js";
4
+ const FALLBACK_TERMINAL_WIDTH = 120;
5
+ const MIN_COLUMN_WIDTH = 8;
6
+ const COLUMN_GUTTER = 2;
7
+ /**
8
+ * Routes the response through the shared output pipeline per GUIDE.md:
9
+ * collections render as a table, single objects as a key/value block,
10
+ * everything else (and `--json`) falls back to pretty JSON. `duvo api`
11
+ * has no schema, so columns and labels come straight from the response
12
+ * keys.
13
+ */
14
+ export function renderApiResponse(response, asJson) {
15
+ if (asJson) {
16
+ printJson(response);
17
+ return;
18
+ }
19
+ if (Array.isArray(response)) {
20
+ if (response.length === 0) {
21
+ console.log("No results.");
22
+ return;
23
+ }
24
+ if (response.every(isRecord)) {
25
+ renderTableResponse(response);
26
+ return;
27
+ }
28
+ }
29
+ if (isRecord(response)) {
30
+ const entries = Object.entries(response).map(([key, value]) => {
31
+ return { label: key, value: formatScalar(value) };
32
+ });
33
+ console.log(renderKeyValue(entries));
34
+ return;
35
+ }
36
+ printJson(response);
37
+ }
38
+ function renderTableResponse(rows) {
39
+ const keys = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
40
+ const maxWidth = perColumnMaxWidth(keys.length);
41
+ const columns = keys.map((key) => {
42
+ return {
43
+ header: key.toUpperCase(),
44
+ cell: (row) => formatScalar(row[key]),
45
+ maxWidth,
46
+ };
47
+ });
48
+ console.log(renderTable(columns, rows));
49
+ }
50
+ function isRecord(value) {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+ function perColumnMaxWidth(columnCount) {
54
+ if (columnCount === 0) {
55
+ return MIN_COLUMN_WIDTH;
56
+ }
57
+ const terminalWidth = process.stdout.columns ?? FALLBACK_TERMINAL_WIDTH;
58
+ const budget = terminalWidth - COLUMN_GUTTER * (columnCount - 1);
59
+ return Math.max(MIN_COLUMN_WIDTH, Math.floor(budget / columnCount));
60
+ }
61
+ function formatScalar(value) {
62
+ if (value === null || value === undefined) {
63
+ return "";
64
+ }
65
+ if (typeof value === "string") {
66
+ return value;
67
+ }
68
+ if (typeof value === "number" || typeof value === "boolean") {
69
+ return String(value);
70
+ }
71
+ return JSON.stringify(value);
72
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerApi(program: Command): void;