@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,98 @@
1
+ import { buildFieldObject } from "../api/parse-fields.js";
2
+ import { readInputJson } from "../api/read-input.js";
3
+ import { httpRequest } from "../client/client.js";
4
+ import { resolveConfig } from "../config/resolve.js";
5
+ import { CliError, EXIT_CODE } from "../errors.js";
6
+ import { renderApiResponse } from "./api-render.js";
7
+ import { getProfileFlag } from "./shared/profile-flag.js";
8
+ const METHODS_WITH_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]);
9
+ const VALID_METHODS = new Set([
10
+ "GET",
11
+ "HEAD",
12
+ "POST",
13
+ "PUT",
14
+ "PATCH",
15
+ "DELETE",
16
+ ]);
17
+ export function registerApi(program) {
18
+ program
19
+ .command("api <method> <path>")
20
+ .description("Make an authenticated request to the Duvo public API")
21
+ .option("-f, --field <key=value>", "string field; repeat for multiple (body on POST/PUT/PATCH/DELETE, query on GET/HEAD)", collect, [])
22
+ .option("-F, --raw-field <key=value>", "typed field (boolean, number, null, or @file for JSON); repeat for multiple", collect, [])
23
+ .option("--input <path>", 'raw JSON body from a file, or "-" for stdin (mutually exclusive with -f / -F)')
24
+ .option("--json", "output raw JSON instead of the formatted view")
25
+ .action(async function apiAction(method, path, options) {
26
+ await runApi({
27
+ method,
28
+ path,
29
+ options,
30
+ }, { profileFlag: getProfileFlag(this) });
31
+ });
32
+ }
33
+ async function runApi(args, resolveOptions) {
34
+ const resolved = await resolveConfig(resolveOptions);
35
+ if (!resolved.apiKey) {
36
+ throw new CliError("Not logged in. Run `duvo login` to save an API key.", EXIT_CODE.AUTH);
37
+ }
38
+ const method = args.method.toUpperCase();
39
+ if (!VALID_METHODS.has(method)) {
40
+ throw new CliError(`Invalid HTTP method "${args.method}". Expected one of: ${[...VALID_METHODS].join(", ")}.`);
41
+ }
42
+ const { body, searchParams } = prepareRequestData(method, args.options);
43
+ const response = await httpRequest({
44
+ method,
45
+ path: args.path,
46
+ apiKey: resolved.apiKey,
47
+ apiBaseUrl: resolved.apiBaseUrl,
48
+ body,
49
+ searchParams,
50
+ });
51
+ renderApiResponse(response, args.options.json ?? false);
52
+ }
53
+ function prepareRequestData(method, options) {
54
+ const hasFieldArgs = (options.field?.length ?? 0) > 0 || (options.rawField?.length ?? 0) > 0;
55
+ if (options.input !== undefined && hasFieldArgs) {
56
+ throw new CliError("Cannot combine --input with -f / -F flags.");
57
+ }
58
+ if (options.input !== undefined) {
59
+ if (!METHODS_WITH_BODY.has(method)) {
60
+ throw new CliError(`--input is only supported for ${[...METHODS_WITH_BODY].join(", ")} requests.`);
61
+ }
62
+ return { body: readInputJson(options.input), searchParams: undefined };
63
+ }
64
+ if (!hasFieldArgs) {
65
+ return { body: undefined, searchParams: undefined };
66
+ }
67
+ const fields = buildFieldObject(options.field, options.rawField);
68
+ if (METHODS_WITH_BODY.has(method)) {
69
+ return { body: fields, searchParams: undefined };
70
+ }
71
+ return { body: undefined, searchParams: fieldsToSearchParams(fields) };
72
+ }
73
+ function collect(value, previous) {
74
+ return [...previous, value];
75
+ }
76
+ /**
77
+ * Encodes typed fields as query string values. Scalars pass through as
78
+ * their string form; objects and arrays are JSON-stringified so an
79
+ * `-F filter=@file.json` round-trips to a usable `?filter=…` rather
80
+ * than `[object Object]`. `null` / `undefined` keys are dropped — query
81
+ * strings have no native null encoding.
82
+ */
83
+ function fieldsToSearchParams(fields) {
84
+ const params = new URLSearchParams();
85
+ for (const [key, value] of Object.entries(fields)) {
86
+ if (value === null || value === undefined) {
87
+ continue;
88
+ }
89
+ if (typeof value === "string" ||
90
+ typeof value === "number" ||
91
+ typeof value === "boolean") {
92
+ params.append(key, String(value));
93
+ continue;
94
+ }
95
+ params.append(key, JSON.stringify(value));
96
+ }
97
+ return params;
98
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerLogin(program: Command): void;
@@ -0,0 +1,80 @@
1
+ import { isCancel, password, text } from "@clack/prompts";
2
+ import { DEFAULT_API_BASE_URL } from "../config/defaults.js";
3
+ import { addProfile, isValidProfileName } from "../config/profiles.js";
4
+ import { readConfig, writeConfig } from "../config/store.js";
5
+ import { AuthCliError, CancelCliError, CliError } from "../errors.js";
6
+ export function registerLogin(program) {
7
+ program
8
+ .command("login")
9
+ .description("Store an API key under a named profile")
10
+ .option("--name <profile>", "profile name to store this key under (prompted if omitted)")
11
+ .option("--api-key <key>", "API key (non-interactive; skips the masked prompt)")
12
+ .option("--base-url <url>", `API base URL for this profile (default: ${DEFAULT_API_BASE_URL})`)
13
+ .action(async (options) => {
14
+ await runLogin(options);
15
+ });
16
+ }
17
+ async function runLogin(options) {
18
+ const config = await readConfig();
19
+ const existingCount = Object.keys(config.profiles).length;
20
+ const name = await resolveProfileName(options.name, existingCount);
21
+ const apiKey = await resolveApiKey(options.apiKey);
22
+ const apiBaseUrl = options.baseUrl ?? DEFAULT_API_BASE_URL;
23
+ const next = addProfile(config, name, { apiKey, apiBaseUrl });
24
+ await writeConfig(next);
25
+ const isNew = !(name in config.profiles);
26
+ const verb = isNew ? "Saved" : "Updated";
27
+ console.log(`${verb} profile "${name}".`);
28
+ if (next.defaultProfile === name && config.defaultProfile !== name) {
29
+ console.log(`Set "${name}" as the default profile.`);
30
+ }
31
+ }
32
+ async function resolveProfileName(flag, existingCount) {
33
+ if (flag !== undefined) {
34
+ if (!isValidProfileName(flag)) {
35
+ throw new CliError(`Invalid profile name "${flag}". Use lowercase letters, digits, ` +
36
+ `".", "-" or "_", max 40 chars.`);
37
+ }
38
+ return flag;
39
+ }
40
+ const placeholder = existingCount === 0 ? "default" : "";
41
+ const answer = await text({
42
+ message: "Profile name",
43
+ placeholder: placeholder || "e.g. acme",
44
+ defaultValue: placeholder,
45
+ validate: (value) => {
46
+ if (!value || value.length === 0) {
47
+ return "Profile name is required.";
48
+ }
49
+ if (!isValidProfileName(value)) {
50
+ return 'Use lowercase letters, digits, ".", "-" or "_" only (max 40 chars).';
51
+ }
52
+ return undefined;
53
+ },
54
+ });
55
+ if (isCancel(answer)) {
56
+ throw new CancelCliError("Login cancelled.");
57
+ }
58
+ return answer;
59
+ }
60
+ async function resolveApiKey(flag) {
61
+ if (flag !== undefined) {
62
+ if (flag.length === 0) {
63
+ throw new AuthCliError("--api-key cannot be empty.");
64
+ }
65
+ return flag;
66
+ }
67
+ const answer = await password({
68
+ message: "API key",
69
+ validate: (value) => {
70
+ if (!value || value.length === 0) {
71
+ return "API key is required.";
72
+ }
73
+ return undefined;
74
+ },
75
+ });
76
+ if (isCancel(answer)) {
77
+ throw new CancelCliError("Login cancelled.");
78
+ }
79
+ return answer;
80
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerLogout(program: Command): void;
@@ -0,0 +1,38 @@
1
+ import { removeProfile } from "../config/profiles.js";
2
+ import { readConfig, writeConfig } from "../config/store.js";
3
+ import { confirmDestructive } from "./shared/confirm.js";
4
+ export function registerLogout(program) {
5
+ program
6
+ .command("logout")
7
+ .description("Remove a stored profile (default: the default one)")
8
+ .option("--name <profile>", "profile to remove (defaults to the default profile)")
9
+ .option("-y, --yes", "skip the confirmation prompt")
10
+ .action(async (options) => {
11
+ await runLogout(options);
12
+ });
13
+ }
14
+ async function runLogout(options) {
15
+ const config = await readConfig();
16
+ const target = options.name ?? config.defaultProfile;
17
+ if (!target) {
18
+ console.log("No profile to remove — you are not logged in.");
19
+ return;
20
+ }
21
+ if (!(target in config.profiles)) {
22
+ console.log(`Profile "${target}" does not exist.`);
23
+ return;
24
+ }
25
+ await confirmDestructive({
26
+ message: `Remove profile "${target}"?`,
27
+ skip: options.yes ?? false,
28
+ });
29
+ const next = removeProfile(config, target);
30
+ await writeConfig(next);
31
+ console.log(`Removed profile "${target}".`);
32
+ if (next.defaultProfile && next.defaultProfile !== config.defaultProfile) {
33
+ console.log(`Default profile is now "${next.defaultProfile}".`);
34
+ }
35
+ else if (!next.defaultProfile) {
36
+ console.log("No profiles remaining.");
37
+ }
38
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerProfiles(program: Command): void;
@@ -0,0 +1,13 @@
1
+ import { registerList } from "./list.js";
2
+ import { registerRemove } from "./remove.js";
3
+ import { registerRename } from "./rename.js";
4
+ import { registerUse } from "./use.js";
5
+ export function registerProfiles(program) {
6
+ const profiles = program
7
+ .command("profiles")
8
+ .description("Manage stored API-key profiles");
9
+ registerList(profiles);
10
+ registerUse(profiles);
11
+ registerRename(profiles);
12
+ registerRemove(profiles);
13
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerList(profiles: Command): void;
@@ -0,0 +1,22 @@
1
+ import { listProfiles } from "../../config/profiles.js";
2
+ import { readConfig } from "../../config/store.js";
3
+ import { renderProfileJson, renderProfileTable } from "./render-list.js";
4
+ export function registerList(profiles) {
5
+ profiles
6
+ .command("list", { isDefault: true })
7
+ .description("List stored profiles; mark the default one with ▶")
8
+ .option("--json", "output raw JSON")
9
+ .action(async (options) => {
10
+ const config = await readConfig();
11
+ const items = listProfiles(config);
12
+ if (options.json) {
13
+ console.log(renderProfileJson(items));
14
+ return;
15
+ }
16
+ if (items.length === 0) {
17
+ console.log("No profiles stored. Run `duvo login` to add one.");
18
+ return;
19
+ }
20
+ console.log(renderProfileTable(items));
21
+ });
22
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerRemove(profiles: Command): void;
@@ -0,0 +1,30 @@
1
+ import { removeProfile } from "../../config/profiles.js";
2
+ import { readConfig, writeConfig } from "../../config/store.js";
3
+ import { confirmDestructive } from "../shared/confirm.js";
4
+ export function registerRemove(profiles) {
5
+ profiles
6
+ .command("remove <name>")
7
+ .description("Remove a stored profile (alias of `duvo logout --name`)")
8
+ .option("-y, --yes", "skip the confirmation prompt")
9
+ .action(async (name, options) => {
10
+ const config = await readConfig();
11
+ if (!(name in config.profiles)) {
12
+ console.log(`Profile "${name}" does not exist.`);
13
+ return;
14
+ }
15
+ await confirmDestructive({
16
+ message: `Remove profile "${name}"?`,
17
+ skip: options.yes ?? false,
18
+ });
19
+ const next = removeProfile(config, name);
20
+ await writeConfig(next);
21
+ console.log(`Removed profile "${name}".`);
22
+ if (next.defaultProfile &&
23
+ next.defaultProfile !== config.defaultProfile) {
24
+ console.log(`Default profile is now "${next.defaultProfile}".`);
25
+ }
26
+ else if (!next.defaultProfile) {
27
+ console.log("No profiles remaining.");
28
+ }
29
+ });
30
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerRename(profiles: Command): void;
@@ -0,0 +1,20 @@
1
+ import { renameProfile } from "../../config/profiles.js";
2
+ import { readConfig, writeConfig } from "../../config/store.js";
3
+ import { CliError, EXIT_CODE } from "../../errors.js";
4
+ export function registerRename(profiles) {
5
+ profiles
6
+ .command("rename <old> <new>")
7
+ .description("Rename a stored profile, preserving its credentials")
8
+ .action(async (oldName, newName) => {
9
+ const config = await readConfig();
10
+ try {
11
+ const next = renameProfile(config, oldName, newName);
12
+ await writeConfig(next);
13
+ console.log(`Renamed profile "${oldName}" to "${newName}".`);
14
+ }
15
+ catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ throw new CliError(message, EXIT_CODE.GENERIC);
18
+ }
19
+ });
20
+ }
@@ -0,0 +1,12 @@
1
+ import { type ProfileSummary } from "../../config/profiles.js";
2
+ /**
3
+ * Format the profile list as an aligned text table. Header sits behind
4
+ * two spaces so it lines up with the single-char marker + space that
5
+ * prefixes each data row.
6
+ */
7
+ export declare function renderProfileTable(items: readonly ProfileSummary[]): string;
8
+ /**
9
+ * JSON output mirrors the text view's masked keys — never emit raw
10
+ * credentials from a listing command.
11
+ */
12
+ export declare function renderProfileJson(items: readonly ProfileSummary[]): string;
@@ -0,0 +1,37 @@
1
+ import { padRight, truncateApiKey } from "../../output/format.js";
2
+ const DEFAULT_MARKER = "▶";
3
+ const NON_DEFAULT_MARKER = " ";
4
+ function toRow(item) {
5
+ return {
6
+ marker: item.isDefault ? DEFAULT_MARKER : NON_DEFAULT_MARKER,
7
+ name: item.name,
8
+ apiKey: truncateApiKey(item.apiKey),
9
+ apiBaseUrl: item.apiBaseUrl,
10
+ };
11
+ }
12
+ /**
13
+ * Format the profile list as an aligned text table. Header sits behind
14
+ * two spaces so it lines up with the single-char marker + space that
15
+ * prefixes each data row.
16
+ */
17
+ export function renderProfileTable(items) {
18
+ const rows = items.map(toRow);
19
+ const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
20
+ const keyWidth = Math.max(7, ...rows.map((r) => r.apiKey.length));
21
+ const lines = [];
22
+ lines.push(` ${padRight("NAME", nameWidth)} ${padRight("KEY", keyWidth)} BASE URL`);
23
+ for (const row of rows) {
24
+ lines.push(`${row.marker} ${padRight(row.name, nameWidth)} ${padRight(row.apiKey, keyWidth)} ${row.apiBaseUrl}`);
25
+ }
26
+ return lines.join("\n");
27
+ }
28
+ /**
29
+ * JSON output mirrors the text view's masked keys — never emit raw
30
+ * credentials from a listing command.
31
+ */
32
+ export function renderProfileJson(items) {
33
+ const masked = items.map((item) => {
34
+ return { ...item, apiKey: truncateApiKey(item.apiKey) };
35
+ });
36
+ return JSON.stringify(masked, null, 2);
37
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerUse(profiles: Command): void;
@@ -0,0 +1,17 @@
1
+ import { setDefaultProfile } from "../../config/profiles.js";
2
+ import { readConfig, writeConfig } from "../../config/store.js";
3
+ import { AuthCliError } from "../../errors.js";
4
+ export function registerUse(profiles) {
5
+ profiles
6
+ .command("use <name>")
7
+ .description("Switch the default profile")
8
+ .action(async (name) => {
9
+ const config = await readConfig();
10
+ if (!(name in config.profiles)) {
11
+ throw new AuthCliError(`Profile "${name}" does not exist. Run \`duvo profiles list\` to see available profiles.`);
12
+ }
13
+ const next = setDefaultProfile(config, name);
14
+ await writeConfig(next);
15
+ console.log(`Default profile is now "${name}".`);
16
+ });
17
+ }
@@ -0,0 +1,19 @@
1
+ type ConfirmOptions = {
2
+ /** Question shown to the user, e.g. `Remove profile "acme"?`. */
3
+ message: string;
4
+ /** Caller already collected consent (e.g. via `--yes`). */
5
+ skip: boolean;
6
+ };
7
+ /**
8
+ * Prompt for [y/N] confirmation on a destructive action. Skipped when
9
+ * the caller passes `skip: true` (typically from a `--yes` flag).
10
+ *
11
+ * In non-interactive shells (no TTY on stdin) we refuse rather than
12
+ * silently treat piped input as consent. The caller must opt in via
13
+ * `--yes` to proceed without a prompt.
14
+ *
15
+ * Throws instead of calling `process.exit` so the top-level error
16
+ * handler remains the single chokepoint for process termination.
17
+ */
18
+ export declare function confirmDestructive(options: ConfirmOptions): Promise<void>;
19
+ export {};
@@ -0,0 +1,28 @@
1
+ import { confirm, isCancel } from "@clack/prompts";
2
+ import { CancelCliError, CliError } from "../../errors.js";
3
+ /**
4
+ * Prompt for [y/N] confirmation on a destructive action. Skipped when
5
+ * the caller passes `skip: true` (typically from a `--yes` flag).
6
+ *
7
+ * In non-interactive shells (no TTY on stdin) we refuse rather than
8
+ * silently treat piped input as consent. The caller must opt in via
9
+ * `--yes` to proceed without a prompt.
10
+ *
11
+ * Throws instead of calling `process.exit` so the top-level error
12
+ * handler remains the single chokepoint for process termination.
13
+ */
14
+ export async function confirmDestructive(options) {
15
+ if (options.skip) {
16
+ return;
17
+ }
18
+ if (!process.stdin.isTTY) {
19
+ throw new CliError(`${options.message} Pass --yes to confirm without an interactive prompt.`);
20
+ }
21
+ const answer = await confirm({
22
+ message: options.message,
23
+ initialValue: false,
24
+ });
25
+ if (isCancel(answer) || !answer) {
26
+ throw new CancelCliError();
27
+ }
28
+ }
@@ -0,0 +1,8 @@
1
+ type ParseLimitOptions = {
2
+ defaultValue: number;
3
+ min: number;
4
+ max: number;
5
+ };
6
+ export declare function parseLimit(raw: string | undefined, { defaultValue, min, max }: ParseLimitOptions): number;
7
+ export declare function parseOffset(raw: string | undefined): number;
8
+ export {};
@@ -0,0 +1,24 @@
1
+ import { CliError } from "../../errors.js";
2
+ const POSITIVE_INT = /^\d+$/;
3
+ export function parseLimit(raw, { defaultValue, min, max }) {
4
+ if (raw === undefined) {
5
+ return defaultValue;
6
+ }
7
+ if (!POSITIVE_INT.test(raw)) {
8
+ throw new CliError(`--limit must be an integer between ${min} and ${max}`);
9
+ }
10
+ const n = Number(raw);
11
+ if (n < min || n > max) {
12
+ throw new CliError(`--limit must be an integer between ${min} and ${max}`);
13
+ }
14
+ return n;
15
+ }
16
+ export function parseOffset(raw) {
17
+ if (raw === undefined) {
18
+ return 0;
19
+ }
20
+ if (!POSITIVE_INT.test(raw)) {
21
+ throw new CliError("--offset must be a non-negative integer");
22
+ }
23
+ return Number(raw);
24
+ }
@@ -0,0 +1,8 @@
1
+ import { type Command } from "commander";
2
+ /**
3
+ * Pulls the global `--profile` option off the root command. Commander
4
+ * merges parent options into `opts()` for subcommands, but the value
5
+ * lives on the root — this helper walks up to find it regardless of how
6
+ * deep the subcommand sits.
7
+ */
8
+ export declare function getProfileFlag(command: Command): string | undefined;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Pulls the global `--profile` option off the root command. Commander
3
+ * merges parent options into `opts()` for subcommands, but the value
4
+ * lives on the root — this helper walks up to find it regardless of how
5
+ * deep the subcommand sits.
6
+ */
7
+ export function getProfileFlag(command) {
8
+ let node = command;
9
+ while (node) {
10
+ const opts = node.opts();
11
+ if (opts.profile) {
12
+ return opts.profile;
13
+ }
14
+ node = node.parent ?? null;
15
+ }
16
+ return undefined;
17
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerWhoami(program: Command): void;
@@ -0,0 +1,21 @@
1
+ import { resolveConfig } from "../config/resolve.js";
2
+ import { AuthCliError } from "../errors.js";
3
+ import { truncateApiKey } from "../output/format.js";
4
+ import { getProfileFlag } from "./shared/profile-flag.js";
5
+ export function registerWhoami(program) {
6
+ program
7
+ .command("whoami")
8
+ .description("Print the default profile, truncated API key, and base URL")
9
+ .action(async function whoamiAction() {
10
+ const resolved = await resolveConfig({
11
+ profileFlag: getProfileFlag(this),
12
+ });
13
+ if (!resolved.apiKey) {
14
+ throw new AuthCliError("Not logged in. Run `duvo login` to save an API key.");
15
+ }
16
+ const profileLabel = resolved.profileName ?? "(env-only, no stored profile)";
17
+ console.log(`Profile ${profileLabel}`);
18
+ console.log(`API key ${truncateApiKey(resolved.apiKey)}`);
19
+ console.log(`Base URL ${resolved.apiBaseUrl}`);
20
+ });
21
+ }
@@ -0,0 +1,3 @@
1
+ import { type Config } from "../config/types.js";
2
+ export declare const DEFAULT_API_BASE_URL = "https://api.duvo.ai";
3
+ export declare function emptyConfig(): Config;
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_API_BASE_URL = "https://api.duvo.ai";
2
+ export function emptyConfig() {
3
+ return { defaultProfile: null, profiles: {} };
4
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ declare const CliEnvSchema: z.ZodObject<{
3
+ DUVO_PROFILE: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
4
+ DUVO_API_KEY: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
5
+ DUVO_API_BASE_URL: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ DUVO_PROFILE?: string | undefined;
8
+ DUVO_API_KEY?: string | undefined;
9
+ DUVO_API_BASE_URL?: string | undefined;
10
+ }, {
11
+ DUVO_PROFILE?: string | undefined;
12
+ DUVO_API_KEY?: string | undefined;
13
+ DUVO_API_BASE_URL?: string | undefined;
14
+ }>;
15
+ export type CliEnv = z.output<typeof CliEnvSchema>;
16
+ export declare function parseCliEnv(env: NodeJS.ProcessEnv): CliEnv;
17
+ export {};
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ const optionalEnvString = z
3
+ .string()
4
+ .optional()
5
+ .transform((value) => value === undefined || value === "" ? undefined : value);
6
+ const CliEnvSchema = z.object({
7
+ DUVO_PROFILE: optionalEnvString,
8
+ DUVO_API_KEY: optionalEnvString,
9
+ DUVO_API_BASE_URL: optionalEnvString,
10
+ });
11
+ export function parseCliEnv(env) {
12
+ return CliEnvSchema.parse(env);
13
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getConfigDir(): string;
2
+ export declare function getConfigPath(): string;
@@ -0,0 +1,14 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ const CONFIG_DIR_NAME = "duvo";
4
+ const CONFIG_FILE_NAME = "config.json";
5
+ export function getConfigDir() {
6
+ const xdg = process.env.XDG_CONFIG_HOME;
7
+ if (xdg && xdg.length > 0) {
8
+ return join(xdg, CONFIG_DIR_NAME);
9
+ }
10
+ return join(homedir(), ".config", CONFIG_DIR_NAME);
11
+ }
12
+ export function getConfigPath() {
13
+ return join(getConfigDir(), CONFIG_FILE_NAME);
14
+ }
@@ -0,0 +1,24 @@
1
+ import { type Config, type Profile } from "../config/types.js";
2
+ export declare function isValidProfileName(name: string): boolean;
3
+ export declare function assertValidProfileName(name: string): void;
4
+ /**
5
+ * Add (or replace) a profile. The first profile added becomes the
6
+ * default automatically. Returns the updated config without mutating
7
+ * the input.
8
+ */
9
+ export declare function addProfile(config: Config, name: string, profile: Profile): Config;
10
+ /**
11
+ * Remove a profile. If the removed profile was the default, picks
12
+ * another profile alphabetically as the new default, or clears it if
13
+ * none remain.
14
+ */
15
+ export declare function removeProfile(config: Config, name: string): Config;
16
+ export declare function renameProfile(config: Config, oldName: string, newName: string): Config;
17
+ export declare function setDefaultProfile(config: Config, name: string): Config;
18
+ export type ProfileSummary = {
19
+ name: string;
20
+ apiKey: string;
21
+ apiBaseUrl: string;
22
+ isDefault: boolean;
23
+ };
24
+ export declare function listProfiles(config: Config): readonly ProfileSummary[];