@duvoai/cli 0.1.0 → 0.2.0
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/README.md +71 -11
- package/dist/bin/duvo.js +11726 -6
- package/dist/bin/duvo.js.map +7 -0
- package/package.json +10 -8
- package/dist/api/agent-schemas.d.ts +0 -521
- package/dist/api/agent-schemas.js +0 -36
- package/dist/api/parse-fields.d.ts +0 -25
- package/dist/api/parse-fields.js +0 -86
- package/dist/api/read-input.d.ts +0 -6
- package/dist/api/read-input.js +0 -28
- package/dist/api/run-schemas.d.ts +0 -621
- package/dist/api/run-schemas.js +0 -91
- package/dist/bin/create-program.d.ts +0 -7
- package/dist/bin/create-program.js +0 -34
- package/dist/bin/duvo.d.ts +0 -2
- package/dist/client/client.d.ts +0 -23
- package/dist/client/client.js +0 -96
- package/dist/commands/agents/create.d.ts +0 -2
- package/dist/commands/agents/create.js +0 -182
- package/dist/commands/agents/get.d.ts +0 -2
- package/dist/commands/agents/get.js +0 -36
- package/dist/commands/agents/index.d.ts +0 -2
- package/dist/commands/agents/index.js +0 -11
- package/dist/commands/agents/list.d.ts +0 -2
- package/dist/commands/agents/list.js +0 -61
- package/dist/commands/api-render.d.ts +0 -8
- package/dist/commands/api-render.js +0 -72
- package/dist/commands/api.d.ts +0 -2
- package/dist/commands/api.js +0 -90
- package/dist/commands/login.d.ts +0 -2
- package/dist/commands/login.js +0 -80
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.js +0 -38
- package/dist/commands/profiles/index.d.ts +0 -2
- package/dist/commands/profiles/index.js +0 -13
- package/dist/commands/profiles/list.d.ts +0 -2
- package/dist/commands/profiles/list.js +0 -22
- package/dist/commands/profiles/remove.d.ts +0 -2
- package/dist/commands/profiles/remove.js +0 -30
- package/dist/commands/profiles/rename.d.ts +0 -2
- package/dist/commands/profiles/rename.js +0 -20
- package/dist/commands/profiles/render-list.d.ts +0 -12
- package/dist/commands/profiles/render-list.js +0 -37
- package/dist/commands/profiles/use.d.ts +0 -2
- package/dist/commands/profiles/use.js +0 -17
- package/dist/commands/runs/get.d.ts +0 -2
- package/dist/commands/runs/get.js +0 -59
- package/dist/commands/runs/index.d.ts +0 -2
- package/dist/commands/runs/index.js +0 -17
- package/dist/commands/runs/messages.d.ts +0 -2
- package/dist/commands/runs/messages.js +0 -80
- package/dist/commands/runs/respond.d.ts +0 -2
- package/dist/commands/runs/respond.js +0 -105
- package/dist/commands/runs/send-message.d.ts +0 -2
- package/dist/commands/runs/send-message.js +0 -41
- package/dist/commands/runs/start.d.ts +0 -2
- package/dist/commands/runs/start.js +0 -54
- package/dist/commands/runs/stop.d.ts +0 -2
- package/dist/commands/runs/stop.js +0 -41
- package/dist/commands/shared/confirm.d.ts +0 -19
- package/dist/commands/shared/confirm.js +0 -28
- package/dist/commands/shared/pagination.d.ts +0 -8
- package/dist/commands/shared/pagination.js +0 -24
- package/dist/commands/shared/profile-flag.d.ts +0 -8
- package/dist/commands/shared/profile-flag.js +0 -17
- package/dist/commands/shared/require-auth.d.ts +0 -12
- package/dist/commands/shared/require-auth.js +0 -18
- package/dist/commands/whoami.d.ts +0 -2
- package/dist/commands/whoami.js +0 -14
- package/dist/config/defaults.d.ts +0 -3
- package/dist/config/defaults.js +0 -4
- package/dist/config/env.d.ts +0 -17
- package/dist/config/env.js +0 -13
- package/dist/config/paths.d.ts +0 -2
- package/dist/config/paths.js +0 -14
- package/dist/config/profiles.d.ts +0 -24
- package/dist/config/profiles.js +0 -78
- package/dist/config/resolve.d.ts +0 -29
- package/dist/config/resolve.js +0 -46
- package/dist/config/store.d.ts +0 -4
- package/dist/config/store.js +0 -94
- package/dist/config/types.d.ts +0 -8
- package/dist/config/types.js +0 -1
- package/dist/errors.d.ts +0 -22
- package/dist/errors.js +0 -42
- package/dist/output/format.d.ts +0 -11
- package/dist/output/format.js +0 -26
- package/dist/output/green.d.ts +0 -5
- package/dist/output/green.js +0 -18
- package/dist/output/json.d.ts +0 -6
- package/dist/output/json.js +0 -8
- package/dist/output/table.d.ts +0 -16
- package/dist/output/table.js +0 -35
- package/dist/output/text.d.ts +0 -9
- package/dist/output/text.js +0 -13
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { startRunResponseSchema } from "../../api/run-schemas.js";
|
|
2
|
-
import { httpRequest } from "../../client/client.js";
|
|
3
|
-
import { CliError } from "../../errors.js";
|
|
4
|
-
import { green } from "../../output/green.js";
|
|
5
|
-
import { printJson } from "../../output/json.js";
|
|
6
|
-
import { renderKeyValue } from "../../output/text.js";
|
|
7
|
-
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
8
|
-
export function registerRunsStart(runs) {
|
|
9
|
-
runs
|
|
10
|
-
.command("start")
|
|
11
|
-
.description("Start a new agent run")
|
|
12
|
-
.requiredOption("--agent <id>", "ID of the agent to run")
|
|
13
|
-
.option("--message <text>", "optional initial message to start the run with")
|
|
14
|
-
.option("--sandbox-id <id>", "optional sandbox ID with pre-uploaded files")
|
|
15
|
-
.option("--webhook-url <url>", "optional webhook URL to POST run events to")
|
|
16
|
-
.option("--json", "output raw JSON from the API")
|
|
17
|
-
.action(async function startAction(options) {
|
|
18
|
-
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
19
|
-
const body = {
|
|
20
|
-
agent_id: options.agent,
|
|
21
|
-
...(options.message !== undefined ? { message: options.message } : {}),
|
|
22
|
-
...(options.sandboxId !== undefined
|
|
23
|
-
? { sandbox_id: options.sandboxId }
|
|
24
|
-
: {}),
|
|
25
|
-
...(options.webhookUrl !== undefined
|
|
26
|
-
? { webhook_url: options.webhookUrl }
|
|
27
|
-
: {}),
|
|
28
|
-
};
|
|
29
|
-
const raw = await httpRequest({
|
|
30
|
-
method: "POST",
|
|
31
|
-
path: "/v1/runs",
|
|
32
|
-
apiKey,
|
|
33
|
-
apiBaseUrl,
|
|
34
|
-
body,
|
|
35
|
-
});
|
|
36
|
-
if (options.json) {
|
|
37
|
-
printJson(raw);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const parsed = startRunResponseSchema.safeParse(raw);
|
|
41
|
-
if (!parsed.success) {
|
|
42
|
-
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
43
|
-
}
|
|
44
|
-
const { run } = parsed.data;
|
|
45
|
-
console.log(green("Run started!"));
|
|
46
|
-
console.log(renderKeyValue([
|
|
47
|
-
{ label: "ID", value: run.id },
|
|
48
|
-
{ label: "Agent", value: run.agent_id },
|
|
49
|
-
{ label: "Build", value: run.build_id },
|
|
50
|
-
{ label: "Status", value: run.status },
|
|
51
|
-
{ label: "Created", value: run.created_at },
|
|
52
|
-
]));
|
|
53
|
-
});
|
|
54
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { stopRunResponseSchema } from "../../api/run-schemas.js";
|
|
2
|
-
import { httpRequest } from "../../client/client.js";
|
|
3
|
-
import { CliError } from "../../errors.js";
|
|
4
|
-
import { green } from "../../output/green.js";
|
|
5
|
-
import { printJson } from "../../output/json.js";
|
|
6
|
-
import { confirmDestructive } from "../shared/confirm.js";
|
|
7
|
-
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
8
|
-
export function registerRunsStop(runs) {
|
|
9
|
-
runs
|
|
10
|
-
.command("stop <id>")
|
|
11
|
-
.description("Stop a running agent run")
|
|
12
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
13
|
-
.option("--json", "output raw JSON from the API")
|
|
14
|
-
.action(async function stopAction(id, options) {
|
|
15
|
-
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
16
|
-
await confirmDestructive({
|
|
17
|
-
message: `Stop run "${id}"?`,
|
|
18
|
-
skip: options.yes ?? false,
|
|
19
|
-
});
|
|
20
|
-
const raw = await httpRequest({
|
|
21
|
-
method: "POST",
|
|
22
|
-
path: `/v1/runs/${encodeURIComponent(id)}/stop`,
|
|
23
|
-
apiKey,
|
|
24
|
-
apiBaseUrl,
|
|
25
|
-
});
|
|
26
|
-
if (options.json) {
|
|
27
|
-
printJson(raw);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const parsed = stopRunResponseSchema.safeParse(raw);
|
|
31
|
-
if (!parsed.success) {
|
|
32
|
-
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
33
|
-
}
|
|
34
|
-
const { success, message } = parsed.data;
|
|
35
|
-
if (!success) {
|
|
36
|
-
throw new CliError(message ?? `Failed to stop run "${id}".`);
|
|
37
|
-
}
|
|
38
|
-
const label = message ?? `Run "${id}" stopped.`;
|
|
39
|
-
console.log(green(label));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
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;
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type Command } from "commander";
|
|
2
|
-
import { type ResolvedConfig } from "../../config/resolve.js";
|
|
3
|
-
export type AuthenticatedConfig = ResolvedConfig & {
|
|
4
|
-
apiKey: string;
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Resolves the effective profile for this invocation and guarantees an
|
|
8
|
-
* API key is present. Intended for every command that hits the API —
|
|
9
|
-
* throws `AuthCliError` (exit code 2) if no key is stored or passed via
|
|
10
|
-
* env, so callers can assume `apiKey` is a non-null string.
|
|
11
|
-
*/
|
|
12
|
-
export declare function requireAuthenticatedConfig(command: Command): Promise<AuthenticatedConfig>;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { resolveConfig } from "../../config/resolve.js";
|
|
2
|
-
import { AuthCliError } from "../../errors.js";
|
|
3
|
-
import { getProfileFlag } from "./profile-flag.js";
|
|
4
|
-
/**
|
|
5
|
-
* Resolves the effective profile for this invocation and guarantees an
|
|
6
|
-
* API key is present. Intended for every command that hits the API —
|
|
7
|
-
* throws `AuthCliError` (exit code 2) if no key is stored or passed via
|
|
8
|
-
* env, so callers can assume `apiKey` is a non-null string.
|
|
9
|
-
*/
|
|
10
|
-
export async function requireAuthenticatedConfig(command) {
|
|
11
|
-
const resolved = await resolveConfig({
|
|
12
|
-
profileFlag: getProfileFlag(command),
|
|
13
|
-
});
|
|
14
|
-
if (!resolved.apiKey) {
|
|
15
|
-
throw new AuthCliError("Not logged in. Run `duvo login` to save an API key.");
|
|
16
|
-
}
|
|
17
|
-
return { ...resolved, apiKey: resolved.apiKey };
|
|
18
|
-
}
|
package/dist/commands/whoami.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { truncateApiKey } from "../output/format.js";
|
|
2
|
-
import { requireAuthenticatedConfig } from "./shared/require-auth.js";
|
|
3
|
-
export function registerWhoami(program) {
|
|
4
|
-
program
|
|
5
|
-
.command("whoami")
|
|
6
|
-
.description("Print the default profile, truncated API key, and base URL")
|
|
7
|
-
.action(async function whoamiAction() {
|
|
8
|
-
const resolved = await requireAuthenticatedConfig(this);
|
|
9
|
-
const profileLabel = resolved.profileName ?? "(env-only, no stored profile)";
|
|
10
|
-
console.log(`Profile ${profileLabel}`);
|
|
11
|
-
console.log(`API key ${truncateApiKey(resolved.apiKey)}`);
|
|
12
|
-
console.log(`Base URL ${resolved.apiBaseUrl}`);
|
|
13
|
-
});
|
|
14
|
-
}
|
package/dist/config/defaults.js
DELETED
package/dist/config/env.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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 {};
|
package/dist/config/env.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/config/paths.d.ts
DELETED
package/dist/config/paths.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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[];
|
package/dist/config/profiles.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
const PROFILE_NAME_PATTERN = /^[a-z0-9._-]+$/;
|
|
2
|
-
const MAX_PROFILE_NAME_LENGTH = 40;
|
|
3
|
-
export function isValidProfileName(name) {
|
|
4
|
-
if (name.length === 0 || name.length > MAX_PROFILE_NAME_LENGTH) {
|
|
5
|
-
return false;
|
|
6
|
-
}
|
|
7
|
-
return PROFILE_NAME_PATTERN.test(name);
|
|
8
|
-
}
|
|
9
|
-
export function assertValidProfileName(name) {
|
|
10
|
-
if (!isValidProfileName(name)) {
|
|
11
|
-
throw new Error(`Invalid profile name "${name}". Use lowercase letters, digits, ` +
|
|
12
|
-
`".", "-" or "_", max ${MAX_PROFILE_NAME_LENGTH} chars.`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Add (or replace) a profile. The first profile added becomes the
|
|
17
|
-
* default automatically. Returns the updated config without mutating
|
|
18
|
-
* the input.
|
|
19
|
-
*/
|
|
20
|
-
export function addProfile(config, name, profile) {
|
|
21
|
-
assertValidProfileName(name);
|
|
22
|
-
const profiles = { ...config.profiles, [name]: profile };
|
|
23
|
-
const defaultProfile = config.defaultProfile === null ? name : config.defaultProfile;
|
|
24
|
-
return { defaultProfile, profiles };
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Remove a profile. If the removed profile was the default, picks
|
|
28
|
-
* another profile alphabetically as the new default, or clears it if
|
|
29
|
-
* none remain.
|
|
30
|
-
*/
|
|
31
|
-
export function removeProfile(config, name) {
|
|
32
|
-
if (!(name in config.profiles)) {
|
|
33
|
-
return config;
|
|
34
|
-
}
|
|
35
|
-
const profiles = { ...config.profiles };
|
|
36
|
-
delete profiles[name];
|
|
37
|
-
if (config.defaultProfile !== name) {
|
|
38
|
-
return { defaultProfile: config.defaultProfile, profiles };
|
|
39
|
-
}
|
|
40
|
-
const remaining = Object.keys(profiles).sort();
|
|
41
|
-
const defaultProfile = remaining.length > 0 ? remaining[0] : null;
|
|
42
|
-
return { defaultProfile, profiles };
|
|
43
|
-
}
|
|
44
|
-
export function renameProfile(config, oldName, newName) {
|
|
45
|
-
if (!(oldName in config.profiles)) {
|
|
46
|
-
throw new Error(`Profile "${oldName}" does not exist.`);
|
|
47
|
-
}
|
|
48
|
-
if (oldName === newName) {
|
|
49
|
-
return config;
|
|
50
|
-
}
|
|
51
|
-
assertValidProfileName(newName);
|
|
52
|
-
if (newName in config.profiles) {
|
|
53
|
-
throw new Error(`Profile "${newName}" already exists.`);
|
|
54
|
-
}
|
|
55
|
-
const profiles = { ...config.profiles };
|
|
56
|
-
profiles[newName] = profiles[oldName];
|
|
57
|
-
delete profiles[oldName];
|
|
58
|
-
const defaultProfile = config.defaultProfile === oldName ? newName : config.defaultProfile;
|
|
59
|
-
return { defaultProfile, profiles };
|
|
60
|
-
}
|
|
61
|
-
export function setDefaultProfile(config, name) {
|
|
62
|
-
if (!(name in config.profiles)) {
|
|
63
|
-
throw new Error(`Profile "${name}" does not exist.`);
|
|
64
|
-
}
|
|
65
|
-
return { ...config, defaultProfile: name };
|
|
66
|
-
}
|
|
67
|
-
export function listProfiles(config) {
|
|
68
|
-
return Object.entries(config.profiles)
|
|
69
|
-
.map(([name, profile]) => {
|
|
70
|
-
return {
|
|
71
|
-
name,
|
|
72
|
-
apiKey: profile.apiKey,
|
|
73
|
-
apiBaseUrl: profile.apiBaseUrl,
|
|
74
|
-
isDefault: config.defaultProfile === name,
|
|
75
|
-
};
|
|
76
|
-
})
|
|
77
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
78
|
-
}
|
package/dist/config/resolve.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type CliEnv } from "../config/env.js";
|
|
2
|
-
import { type Config } from "../config/types.js";
|
|
3
|
-
import { AuthCliError } from "../errors.js";
|
|
4
|
-
export type ResolvedConfig = {
|
|
5
|
-
apiKey: string | null;
|
|
6
|
-
apiBaseUrl: string;
|
|
7
|
-
/** Name of the profile we resolved to, or null if we fell through to env-only. */
|
|
8
|
-
profileName: string | null;
|
|
9
|
-
};
|
|
10
|
-
export type ResolveOptions = {
|
|
11
|
-
/** From the --profile CLI flag. Takes precedence over env and stored default. */
|
|
12
|
-
profileFlag?: string | undefined;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Selection precedence:
|
|
16
|
-
* profile: --profile flag > DUVO_PROFILE env > config.defaultProfile
|
|
17
|
-
* apiKey: DUVO_API_KEY env > profile.apiKey
|
|
18
|
-
* baseUrl: DUVO_API_BASE_URL env > profile.apiBaseUrl > default
|
|
19
|
-
*
|
|
20
|
-
* If --profile is given but unknown, this throws (caller maps to exit 2).
|
|
21
|
-
* DUVO_PROFILE being unset/unknown falls through silently so users can use
|
|
22
|
-
* env overrides without breaking other shells.
|
|
23
|
-
*/
|
|
24
|
-
export declare function resolveConfig(options?: ResolveOptions): Promise<ResolvedConfig>;
|
|
25
|
-
export declare function selectEffectiveConfig(config: Config, options: ResolveOptions, env: CliEnv): ResolvedConfig;
|
|
26
|
-
export declare class UnknownProfileError extends AuthCliError {
|
|
27
|
-
readonly profileName: string;
|
|
28
|
-
constructor(profileName: string);
|
|
29
|
-
}
|
package/dist/config/resolve.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_API_BASE_URL } from "../config/defaults.js";
|
|
2
|
-
import { parseCliEnv } from "../config/env.js";
|
|
3
|
-
import { readConfig } from "../config/store.js";
|
|
4
|
-
import { AuthCliError } from "../errors.js";
|
|
5
|
-
/**
|
|
6
|
-
* Selection precedence:
|
|
7
|
-
* profile: --profile flag > DUVO_PROFILE env > config.defaultProfile
|
|
8
|
-
* apiKey: DUVO_API_KEY env > profile.apiKey
|
|
9
|
-
* baseUrl: DUVO_API_BASE_URL env > profile.apiBaseUrl > default
|
|
10
|
-
*
|
|
11
|
-
* If --profile is given but unknown, this throws (caller maps to exit 2).
|
|
12
|
-
* DUVO_PROFILE being unset/unknown falls through silently so users can use
|
|
13
|
-
* env overrides without breaking other shells.
|
|
14
|
-
*/
|
|
15
|
-
export async function resolveConfig(options = {}) {
|
|
16
|
-
const config = await readConfig();
|
|
17
|
-
return selectEffectiveConfig(config, options, parseCliEnv(process.env));
|
|
18
|
-
}
|
|
19
|
-
export function selectEffectiveConfig(config, options, env) {
|
|
20
|
-
const profileName = selectProfileName(config, options, env);
|
|
21
|
-
const profile = profileName ? (config.profiles[profileName] ?? null) : null;
|
|
22
|
-
const apiKey = env.DUVO_API_KEY ?? profile?.apiKey ?? null;
|
|
23
|
-
const apiBaseUrl = env.DUVO_API_BASE_URL ?? profile?.apiBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
24
|
-
return { apiKey, apiBaseUrl, profileName };
|
|
25
|
-
}
|
|
26
|
-
function selectProfileName(config, options, env) {
|
|
27
|
-
const flag = options.profileFlag;
|
|
28
|
-
if (flag && flag.length > 0) {
|
|
29
|
-
if (!(flag in config.profiles)) {
|
|
30
|
-
throw new UnknownProfileError(flag);
|
|
31
|
-
}
|
|
32
|
-
return flag;
|
|
33
|
-
}
|
|
34
|
-
if (env.DUVO_PROFILE && env.DUVO_PROFILE in config.profiles) {
|
|
35
|
-
return env.DUVO_PROFILE;
|
|
36
|
-
}
|
|
37
|
-
return config.defaultProfile;
|
|
38
|
-
}
|
|
39
|
-
export class UnknownProfileError extends AuthCliError {
|
|
40
|
-
profileName;
|
|
41
|
-
constructor(profileName) {
|
|
42
|
-
super(`Profile "${profileName}" does not exist. Run \`duvo profiles list\` to see available profiles.`);
|
|
43
|
-
this.profileName = profileName;
|
|
44
|
-
this.name = "UnknownProfileError";
|
|
45
|
-
}
|
|
46
|
-
}
|
package/dist/config/store.d.ts
DELETED
package/dist/config/store.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { DEFAULT_API_BASE_URL, emptyConfig } from "../config/defaults.js";
|
|
4
|
-
import { getConfigPath } from "../config/paths.js";
|
|
5
|
-
const FILE_MODE = 0o600;
|
|
6
|
-
const DIR_MODE = 0o700;
|
|
7
|
-
export async function readConfig() {
|
|
8
|
-
const path = getConfigPath();
|
|
9
|
-
try {
|
|
10
|
-
const raw = await readFile(path, "utf8");
|
|
11
|
-
const parsed = JSON.parse(raw);
|
|
12
|
-
return normalizeConfig(parsed);
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
if (isNotFound(error)) {
|
|
16
|
-
return emptyConfig();
|
|
17
|
-
}
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export async function writeConfig(config) {
|
|
22
|
-
const path = getConfigPath();
|
|
23
|
-
await mkdir(dirname(path), { recursive: true, mode: DIR_MODE });
|
|
24
|
-
await writeFile(path, JSON.stringify(config, null, 2), {
|
|
25
|
-
mode: FILE_MODE,
|
|
26
|
-
encoding: "utf8",
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
export async function deleteConfig() {
|
|
30
|
-
const path = getConfigPath();
|
|
31
|
-
try {
|
|
32
|
-
await rm(path);
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (isNotFound(error)) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Accepts the legacy single-profile shape ({ apiKey, apiBaseUrl }) and
|
|
44
|
-
* migrates it into the named-profile shape under the "default" name. Any
|
|
45
|
-
* fields we don't recognize are dropped.
|
|
46
|
-
*/
|
|
47
|
-
function normalizeConfig(parsed) {
|
|
48
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
49
|
-
return emptyConfig();
|
|
50
|
-
}
|
|
51
|
-
const record = parsed;
|
|
52
|
-
// Legacy single-profile migration.
|
|
53
|
-
if (typeof record.apiKey === "string" &&
|
|
54
|
-
record.profiles === undefined &&
|
|
55
|
-
record.defaultProfile === undefined) {
|
|
56
|
-
const apiBaseUrl = typeof record.apiBaseUrl === "string"
|
|
57
|
-
? record.apiBaseUrl
|
|
58
|
-
: DEFAULT_API_BASE_URL;
|
|
59
|
-
return {
|
|
60
|
-
defaultProfile: "default",
|
|
61
|
-
profiles: {
|
|
62
|
-
default: { apiKey: record.apiKey, apiBaseUrl },
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const profiles = {};
|
|
67
|
-
const rawProfiles = record.profiles && typeof record.profiles === "object"
|
|
68
|
-
? record.profiles
|
|
69
|
-
: {};
|
|
70
|
-
for (const [name, value] of Object.entries(rawProfiles)) {
|
|
71
|
-
if (value === null || typeof value !== "object") {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const profile = value;
|
|
75
|
-
if (typeof profile.apiKey !== "string") {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const apiBaseUrl = typeof profile.apiBaseUrl === "string"
|
|
79
|
-
? profile.apiBaseUrl
|
|
80
|
-
: DEFAULT_API_BASE_URL;
|
|
81
|
-
profiles[name] = { apiKey: profile.apiKey, apiBaseUrl };
|
|
82
|
-
}
|
|
83
|
-
const defaultProfile = typeof record.defaultProfile === "string" &&
|
|
84
|
-
record.defaultProfile in profiles
|
|
85
|
-
? record.defaultProfile
|
|
86
|
-
: null;
|
|
87
|
-
return { defaultProfile, profiles };
|
|
88
|
-
}
|
|
89
|
-
function isNotFound(error) {
|
|
90
|
-
return (typeof error === "object" &&
|
|
91
|
-
error !== null &&
|
|
92
|
-
"code" in error &&
|
|
93
|
-
error.code === "ENOENT");
|
|
94
|
-
}
|
package/dist/config/types.d.ts
DELETED
package/dist/config/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/errors.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export declare const EXIT_CODE: {
|
|
2
|
-
readonly SUCCESS: 0;
|
|
3
|
-
readonly GENERIC: 1;
|
|
4
|
-
readonly AUTH: 2;
|
|
5
|
-
readonly NOT_FOUND: 3;
|
|
6
|
-
};
|
|
7
|
-
export type ExitCode = (typeof EXIT_CODE)[keyof typeof EXIT_CODE];
|
|
8
|
-
export declare class CliError extends Error {
|
|
9
|
-
readonly exitCode: ExitCode;
|
|
10
|
-
constructor(message: string, exitCode?: ExitCode);
|
|
11
|
-
}
|
|
12
|
-
export declare class AuthCliError extends CliError {
|
|
13
|
-
constructor(message: string);
|
|
14
|
-
}
|
|
15
|
-
export declare class NotFoundCliError extends CliError {
|
|
16
|
-
constructor(message: string);
|
|
17
|
-
}
|
|
18
|
-
/** Signals a cooperative user-driven cancel (Ctrl-C, answering "no"). */
|
|
19
|
-
export declare class CancelCliError extends CliError {
|
|
20
|
-
constructor(message?: string);
|
|
21
|
-
}
|
|
22
|
-
export declare function handleCliError(error: unknown): never;
|