@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,78 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const EXIT_CODE = {
|
|
2
|
+
SUCCESS: 0,
|
|
3
|
+
GENERIC: 1,
|
|
4
|
+
AUTH: 2,
|
|
5
|
+
NOT_FOUND: 3,
|
|
6
|
+
};
|
|
7
|
+
export class CliError extends Error {
|
|
8
|
+
exitCode;
|
|
9
|
+
constructor(message, exitCode = EXIT_CODE.GENERIC) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "CliError";
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class AuthCliError extends CliError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, EXIT_CODE.AUTH);
|
|
18
|
+
this.name = "AuthCliError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class NotFoundCliError extends CliError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message, EXIT_CODE.NOT_FOUND);
|
|
24
|
+
this.name = "NotFoundCliError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Signals a cooperative user-driven cancel (Ctrl-C, answering "no"). */
|
|
28
|
+
export class CancelCliError extends CliError {
|
|
29
|
+
constructor(message = "Cancelled.") {
|
|
30
|
+
super(message, EXIT_CODE.GENERIC);
|
|
31
|
+
this.name = "CancelCliError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function handleCliError(error) {
|
|
35
|
+
if (error instanceof CliError) {
|
|
36
|
+
process.stderr.write(`${error.message}\n`);
|
|
37
|
+
process.exit(error.exitCode);
|
|
38
|
+
}
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
process.stderr.write(`error: ${message}\n`);
|
|
41
|
+
process.exit(EXIT_CODE.GENERIC);
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mask the secret portion of an API key, leaving the non-secret prefix
|
|
3
|
+
* visible so users can distinguish keys in listings.
|
|
4
|
+
*
|
|
5
|
+
* Keys have the shape `dv_<prefix>-<secret>`. Everything up to and
|
|
6
|
+
* including the first `-` is the public id; everything after is masked.
|
|
7
|
+
* Keys without a dash (shouldn't happen in practice) fall back to a
|
|
8
|
+
* fixed-length prefix so we never leak the tail regardless of format.
|
|
9
|
+
*/
|
|
10
|
+
export declare function truncateApiKey(key: string): string;
|
|
11
|
+
export declare function padRight(value: string, width: number): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const FALLBACK_PREFIX_LEN = 11;
|
|
2
|
+
/**
|
|
3
|
+
* Mask the secret portion of an API key, leaving the non-secret prefix
|
|
4
|
+
* visible so users can distinguish keys in listings.
|
|
5
|
+
*
|
|
6
|
+
* Keys have the shape `dv_<prefix>-<secret>`. Everything up to and
|
|
7
|
+
* including the first `-` is the public id; everything after is masked.
|
|
8
|
+
* Keys without a dash (shouldn't happen in practice) fall back to a
|
|
9
|
+
* fixed-length prefix so we never leak the tail regardless of format.
|
|
10
|
+
*/
|
|
11
|
+
export function truncateApiKey(key) {
|
|
12
|
+
const dashIdx = key.indexOf("-");
|
|
13
|
+
const visibleLen = dashIdx >= 0 ? dashIdx + 1 : FALLBACK_PREFIX_LEN;
|
|
14
|
+
if (key.length <= visibleLen) {
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
const visible = key.slice(0, visibleLen);
|
|
18
|
+
const masked = "*".repeat(key.length - visibleLen);
|
|
19
|
+
return `${visible}${masked}`;
|
|
20
|
+
}
|
|
21
|
+
export function padRight(value, width) {
|
|
22
|
+
if (value.length >= width) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return value + " ".repeat(width - value.length);
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prints a value as pretty JSON (2-space indent) to stdout. This is the
|
|
3
|
+
* stable output contract — scripts should always be able to `| jq` the
|
|
4
|
+
* result. No colour, no truncation.
|
|
5
|
+
*/
|
|
6
|
+
export function printJson(data) {
|
|
7
|
+
console.log(JSON.stringify(data, null, 2));
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Column<T> = {
|
|
2
|
+
header: string;
|
|
3
|
+
cell: (row: T) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Optional cap on the rendered width of the column. Cells wider than
|
|
6
|
+
* this are truncated with a trailing `…` to keep each row on one line.
|
|
7
|
+
* The header is truncated too so the column never exceeds the cap.
|
|
8
|
+
*/
|
|
9
|
+
maxWidth?: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Renders a fixed-width ASCII table. Columns auto-size to the widest
|
|
13
|
+
* cell (including the header) but never exceed `maxWidth` when set.
|
|
14
|
+
* No colour — non-TTY safe by default.
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderTable<T>(columns: readonly Column<T>[], rows: readonly T[]): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a fixed-width ASCII table. Columns auto-size to the widest
|
|
3
|
+
* cell (including the header) but never exceed `maxWidth` when set.
|
|
4
|
+
* No colour — non-TTY safe by default.
|
|
5
|
+
*/
|
|
6
|
+
export function renderTable(columns, rows) {
|
|
7
|
+
if (columns.length === 0) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
const cells = rows.map((row) => columns.map((col) => col.cell(row)));
|
|
11
|
+
const widths = columns.map((col, i) => {
|
|
12
|
+
const contentWidth = Math.max(col.header.length, ...cells.map((row) => row[i].length));
|
|
13
|
+
return col.maxWidth ? Math.min(col.maxWidth, contentWidth) : contentWidth;
|
|
14
|
+
});
|
|
15
|
+
const headerLine = columns
|
|
16
|
+
.map((col, i) => pad(truncate(col.header, widths[i]), widths[i]))
|
|
17
|
+
.join(" ");
|
|
18
|
+
const bodyLines = cells.map((row) => row.map((cell, i) => pad(truncate(cell, widths[i]), widths[i])).join(" "));
|
|
19
|
+
return [headerLine, ...bodyLines].join("\n");
|
|
20
|
+
}
|
|
21
|
+
function pad(value, width) {
|
|
22
|
+
if (value.length >= width) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return value + " ".repeat(width - value.length);
|
|
26
|
+
}
|
|
27
|
+
function truncate(value, width) {
|
|
28
|
+
if (value.length <= width) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (width <= 1) {
|
|
32
|
+
return "…".slice(0, width);
|
|
33
|
+
}
|
|
34
|
+
return `${value.slice(0, width - 1)}…`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type KeyValueEntry = {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Renders a labelled key/value block for single-resource views.
|
|
7
|
+
* Labels are left-padded to the widest label so values line up.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderKeyValue(entries: readonly KeyValueEntry[]): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a labelled key/value block for single-resource views.
|
|
3
|
+
* Labels are left-padded to the widest label so values line up.
|
|
4
|
+
*/
|
|
5
|
+
export function renderKeyValue(entries) {
|
|
6
|
+
if (entries.length === 0) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
const labelWidth = Math.max(...entries.map((e) => e.label.length));
|
|
10
|
+
return entries
|
|
11
|
+
.map((entry) => `${entry.label.padEnd(labelWidth)} ${entry.value}`)
|
|
12
|
+
.join("\n");
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duvoai/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Command-line interface for the Duvo public API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/bin/duvo.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"duvo": "./dist/bin/duvo.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
15
|
+
"dev": "tsx src/bin/duvo.ts",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"test": "vitest run --passWithNoTests",
|
|
19
|
+
"test:watch": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@clack/prompts": "^0.8.2",
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"zod": "^3.25.76"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.3.0",
|
|
28
|
+
"tsc-alias": "^1.8.16",
|
|
29
|
+
"tsx": "^4.20.3",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"vitest": "^3.2.4"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.22.0"
|
|
35
|
+
}
|
|
36
|
+
}
|