@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.
- package/dist/api/agent-schemas.d.ts +94 -0
- package/dist/api/agent-schemas.js +16 -0
- package/dist/api/parse-fields.d.ts +25 -0
- package/dist/api/parse-fields.js +86 -0
- package/dist/api/read-input.d.ts +6 -0
- package/dist/api/read-input.js +28 -0
- package/dist/bin/create-program.d.ts +7 -0
- package/dist/bin/create-program.js +27 -0
- package/dist/bin/duvo.d.ts +2 -0
- package/dist/bin/duvo.js +8 -0
- package/dist/client/client.d.ts +23 -0
- package/dist/client/client.js +96 -0
- package/dist/commands/agents/get.d.ts +2 -0
- package/dist/commands/agents/get.js +42 -0
- package/dist/commands/agents/index.d.ts +2 -0
- package/dist/commands/agents/index.js +9 -0
- package/dist/commands/agents/list.d.ts +2 -0
- package/dist/commands/agents/list.js +67 -0
- package/dist/commands/api-render.d.ts +8 -0
- package/dist/commands/api-render.js +72 -0
- package/dist/commands/api.d.ts +2 -0
- package/dist/commands/api.js +98 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +80 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +38 -0
- package/dist/commands/profiles/index.d.ts +2 -0
- package/dist/commands/profiles/index.js +13 -0
- package/dist/commands/profiles/list.d.ts +2 -0
- package/dist/commands/profiles/list.js +22 -0
- package/dist/commands/profiles/remove.d.ts +2 -0
- package/dist/commands/profiles/remove.js +30 -0
- package/dist/commands/profiles/rename.d.ts +2 -0
- package/dist/commands/profiles/rename.js +20 -0
- package/dist/commands/profiles/render-list.d.ts +12 -0
- package/dist/commands/profiles/render-list.js +37 -0
- package/dist/commands/profiles/use.d.ts +2 -0
- package/dist/commands/profiles/use.js +17 -0
- package/dist/commands/shared/confirm.d.ts +19 -0
- package/dist/commands/shared/confirm.js +28 -0
- package/dist/commands/shared/pagination.d.ts +8 -0
- package/dist/commands/shared/pagination.js +24 -0
- package/dist/commands/shared/profile-flag.d.ts +8 -0
- package/dist/commands/shared/profile-flag.js +17 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +21 -0
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.js +4 -0
- package/dist/config/env.d.ts +17 -0
- package/dist/config/env.js +13 -0
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +14 -0
- package/dist/config/profiles.d.ts +24 -0
- package/dist/config/profiles.js +78 -0
- package/dist/config/resolve.d.ts +29 -0
- package/dist/config/resolve.js +46 -0
- package/dist/config/store.d.ts +4 -0
- package/dist/config/store.js +94 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.js +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +42 -0
- package/dist/output/format.d.ts +11 -0
- package/dist/output/format.js +26 -0
- package/dist/output/json.d.ts +6 -0
- package/dist/output/json.js +8 -0
- package/dist/output/table.d.ts +16 -0
- package/dist/output/table.js +35 -0
- package/dist/output/text.d.ts +9 -0
- package/dist/output/text.js +13 -0
- 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,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,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,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,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,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,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,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,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,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,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[];
|