dashform-cli 0.1.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 +22 -0
- package/package.json +28 -0
- package/src/auth.ts +17 -0
- package/src/cli.ts +114 -0
- package/src/commands/auth/login.ts +28 -0
- package/src/commands/auth/logout.ts +5 -0
- package/src/commands/auth/whoami.ts +8 -0
- package/src/commands/forms/create.ts +61 -0
- package/src/commands/forms/delete.ts +31 -0
- package/src/commands/forms/get.ts +21 -0
- package/src/commands/forms/list.ts +18 -0
- package/src/commands/forms/update.ts +75 -0
- package/src/config.ts +68 -0
- package/src/http/client.ts +92 -0
- package/src/http/errors.ts +22 -0
- package/src/output.ts +62 -0
- package/src/types.ts +33 -0
- package/tests/config.test.ts +9 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# dashform-cli
|
|
2
|
+
|
|
3
|
+
Dashform CLI (TypeScript) scaffold for API-key authenticated access to Dashform APIs.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `dashform auth login --api-key <key>`
|
|
8
|
+
- `dashform auth whoami`
|
|
9
|
+
- `dashform auth logout`
|
|
10
|
+
- `dashform forms list`
|
|
11
|
+
- `dashform forms get <id>`
|
|
12
|
+
- `dashform forms create --name <name> [--description <description>] [--type structured|dynamic] [--tone <tone>]`
|
|
13
|
+
- `dashform forms update <id> [--name <name>] [--description <description>] [--type structured|dynamic] [--tone <tone>] [--clear-tone]`
|
|
14
|
+
- `dashform forms delete <id> --yes`
|
|
15
|
+
|
|
16
|
+
## Development
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm check-types
|
|
21
|
+
pnpm dev -- --help
|
|
22
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dashform-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"dashform": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=24"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "tsx src/cli.ts",
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"check-types": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"cac": "^6.7.14",
|
|
19
|
+
"picocolors": "^1.1.1",
|
|
20
|
+
"zod": "^4.3.6"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsx": "^4.21.0",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vitest": "^4.0.18",
|
|
26
|
+
"@types/node": "^25.2.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { requestJson } from "./http/client.js";
|
|
4
|
+
import { type MeResponse } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const meSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
email: z.string().email(),
|
|
9
|
+
name: z.string().nullable().optional(),
|
|
10
|
+
emailVerified: z.boolean(),
|
|
11
|
+
createdAt: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export async function whoAmI() {
|
|
15
|
+
const config = await loadConfig();
|
|
16
|
+
return requestJson(config, "/api/v1/me", meSchema) as Promise<MeResponse>;
|
|
17
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cac } from "cac";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { loginCommand } from "./commands/auth/login.js";
|
|
6
|
+
import { logoutCommand } from "./commands/auth/logout.js";
|
|
7
|
+
import { whoamiCommand } from "./commands/auth/whoami.js";
|
|
8
|
+
import { getFormCommand } from "./commands/forms/get.js";
|
|
9
|
+
import { listFormsCommand } from "./commands/forms/list.js";
|
|
10
|
+
import { createFormCommand } from "./commands/forms/create.js";
|
|
11
|
+
import { updateFormCommand } from "./commands/forms/update.js";
|
|
12
|
+
import { deleteFormCommand } from "./commands/forms/delete.js";
|
|
13
|
+
import { describeHttpError } from "./http/errors.js";
|
|
14
|
+
|
|
15
|
+
const cli = cac("dashform");
|
|
16
|
+
|
|
17
|
+
cli
|
|
18
|
+
.command("auth login", "Store a Dashform API key")
|
|
19
|
+
.option("--api-key <key>", "API key value")
|
|
20
|
+
.option("--base-url <url>", "Override Dashform base URL")
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
await loginCommand(options);
|
|
23
|
+
process.stdout.write("Saved API key. Run `dashform auth whoami` to verify.\n");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
cli.command("auth logout", "Remove the stored API key").action(async () => {
|
|
27
|
+
await logoutCommand();
|
|
28
|
+
process.stdout.write("Removed API key.\n");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
cli
|
|
32
|
+
.command("auth whoami", "Show the current authenticated user")
|
|
33
|
+
.option("--json", "Print JSON output")
|
|
34
|
+
.action(async (options) => {
|
|
35
|
+
await whoamiCommand(Boolean(options.json));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
cli
|
|
39
|
+
.command("forms list", "List your forms")
|
|
40
|
+
.option("--json", "Print JSON output")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
await listFormsCommand(Boolean(options.json));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
cli
|
|
46
|
+
.command("forms get <id>", "Get a form by ID or public ID")
|
|
47
|
+
.option("--json", "Print JSON output")
|
|
48
|
+
.action(async (id, options) => {
|
|
49
|
+
await getFormCommand(id, Boolean(options.json));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
cli
|
|
53
|
+
.command("forms create", "Create a form")
|
|
54
|
+
.option("--name <name>", "Form name")
|
|
55
|
+
.option("--description <description>", "Form description")
|
|
56
|
+
.option("--type <type>", "Form type: structured | dynamic")
|
|
57
|
+
.option("--tone <tone>", "Form tone")
|
|
58
|
+
.option("--json", "Print JSON output")
|
|
59
|
+
.action(async (options) => {
|
|
60
|
+
await createFormCommand({
|
|
61
|
+
name: options.name,
|
|
62
|
+
description: options.description,
|
|
63
|
+
type: options.type,
|
|
64
|
+
tone: options.tone,
|
|
65
|
+
json: Boolean(options.json),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
cli
|
|
70
|
+
.command("forms update <id>", "Update a form by ID or public ID")
|
|
71
|
+
.option("--name <name>", "Update form name")
|
|
72
|
+
.option("--description <description>", "Update form description")
|
|
73
|
+
.option("--type <type>", "Update form type: structured | dynamic")
|
|
74
|
+
.option("--tone <tone>", "Update form tone")
|
|
75
|
+
.option("--clear-tone", "Clear form tone")
|
|
76
|
+
.option("--json", "Print JSON output")
|
|
77
|
+
.action(async (id, options) => {
|
|
78
|
+
await updateFormCommand(id, {
|
|
79
|
+
name: options.name,
|
|
80
|
+
description: options.description,
|
|
81
|
+
type: options.type,
|
|
82
|
+
tone: options.tone,
|
|
83
|
+
clearTone: Boolean(options.clearTone),
|
|
84
|
+
json: Boolean(options.json),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
cli
|
|
89
|
+
.command("forms delete <id>", "Delete a form by ID or public ID")
|
|
90
|
+
.option("-y, --yes", "Confirm deletion")
|
|
91
|
+
.option("--json", "Print JSON output")
|
|
92
|
+
.action(async (id, options) => {
|
|
93
|
+
await deleteFormCommand(id, {
|
|
94
|
+
yes: Boolean(options.yes),
|
|
95
|
+
json: Boolean(options.json),
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
cli.command("config", "Show current config (without API key)").action(async () => {
|
|
100
|
+
const config = await loadConfig();
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
`${JSON.stringify({ ...config, apiKey: config.apiKey ? "***" : null }, null, 2)}\n`
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
cli.help();
|
|
107
|
+
cli.version("0.1.0");
|
|
108
|
+
|
|
109
|
+
cli.parse();
|
|
110
|
+
|
|
111
|
+
process.on("unhandledRejection", (error) => {
|
|
112
|
+
process.stderr.write(`${pc.red(describeHttpError(error))}\n`);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { updateConfig } from "../../config.js";
|
|
4
|
+
|
|
5
|
+
type LoginOptions = {
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function loginCommand(options: LoginOptions) {
|
|
11
|
+
const rl = createInterface({ input, output });
|
|
12
|
+
try {
|
|
13
|
+
const apiKey = options.apiKey ?? (await rl.question("Dashform API key: "));
|
|
14
|
+
const baseUrl = options.baseUrl;
|
|
15
|
+
|
|
16
|
+
if (!apiKey.trim()) {
|
|
17
|
+
throw new Error("API key is required");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await updateConfig((current) => ({
|
|
21
|
+
...current,
|
|
22
|
+
apiKey: apiKey.trim(),
|
|
23
|
+
baseUrl: baseUrl?.trim() || current.baseUrl,
|
|
24
|
+
}));
|
|
25
|
+
} finally {
|
|
26
|
+
rl.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { whoAmI } from "../../auth.js";
|
|
2
|
+
import { loadConfig } from "../../config.js";
|
|
3
|
+
import { printData } from "../../output.js";
|
|
4
|
+
|
|
5
|
+
export async function whoamiCommand(json?: boolean) {
|
|
6
|
+
const [me, config] = await Promise.all([whoAmI(), loadConfig()]);
|
|
7
|
+
printData(me, json ? "json" : config.outputFormat);
|
|
8
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "../../config.js";
|
|
3
|
+
import { requestJson } from "../../http/client.js";
|
|
4
|
+
import { printData } from "../../output.js";
|
|
5
|
+
|
|
6
|
+
const formTypeSchema = z.enum(["structured", "dynamic"]);
|
|
7
|
+
|
|
8
|
+
const formSchema = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
description: z.string(),
|
|
12
|
+
public_id: z.string(),
|
|
13
|
+
type: formTypeSchema,
|
|
14
|
+
created_at: z.string(),
|
|
15
|
+
updated_at: z.string(),
|
|
16
|
+
organization_id: z.string().optional(),
|
|
17
|
+
user_id: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type CreateFormOptions = {
|
|
21
|
+
name?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
tone?: string;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function createFormCommand(options: CreateFormOptions) {
|
|
29
|
+
const name = options.name?.trim();
|
|
30
|
+
if (!name) {
|
|
31
|
+
throw new Error("Missing required option: --name");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let type: z.infer<typeof formTypeSchema> | undefined;
|
|
35
|
+
if (options.type !== undefined) {
|
|
36
|
+
const parsedType = formTypeSchema.safeParse(options.type);
|
|
37
|
+
if (!parsedType.success) {
|
|
38
|
+
throw new Error("Invalid --type. Expected one of: structured, dynamic");
|
|
39
|
+
}
|
|
40
|
+
type = parsedType.data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const payload: Record<string, unknown> = { name };
|
|
44
|
+
if (options.description !== undefined) {
|
|
45
|
+
payload.description = options.description;
|
|
46
|
+
}
|
|
47
|
+
if (options.tone !== undefined) {
|
|
48
|
+
payload.tone = options.tone;
|
|
49
|
+
}
|
|
50
|
+
if (type !== undefined) {
|
|
51
|
+
payload.type = type;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const config = await loadConfig();
|
|
55
|
+
const form = await requestJson(config, "/api/v1/forms", formSchema, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: payload,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
printData(form, options.json ? "json" : config.outputFormat);
|
|
61
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "../../config.js";
|
|
3
|
+
import { requestJson } from "../../http/client.js";
|
|
4
|
+
import { printData } from "../../output.js";
|
|
5
|
+
|
|
6
|
+
const deleteFormSchema = z.object({
|
|
7
|
+
deleted: z.boolean(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type DeleteFormOptions = {
|
|
11
|
+
yes?: boolean;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function deleteFormCommand(id: string, options: DeleteFormOptions) {
|
|
16
|
+
if (!options.yes) {
|
|
17
|
+
throw new Error("Refusing to delete without confirmation. Re-run with --yes");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = await loadConfig();
|
|
21
|
+
const result = await requestJson(
|
|
22
|
+
config,
|
|
23
|
+
`/api/v1/forms/${encodeURIComponent(id)}`,
|
|
24
|
+
deleteFormSchema,
|
|
25
|
+
{
|
|
26
|
+
method: "DELETE",
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
printData(result, options.json ? "json" : config.outputFormat);
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "../../config.js";
|
|
3
|
+
import { requestJson } from "../../http/client.js";
|
|
4
|
+
import { printData } from "../../output.js";
|
|
5
|
+
|
|
6
|
+
const formSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
description: z.string(),
|
|
10
|
+
public_id: z.string(),
|
|
11
|
+
created_at: z.string(),
|
|
12
|
+
updated_at: z.string().optional(),
|
|
13
|
+
organization_id: z.string().optional(),
|
|
14
|
+
user_id: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function getFormCommand(id: string, json?: boolean) {
|
|
18
|
+
const config = await loadConfig();
|
|
19
|
+
const form = await requestJson(config, `/api/v1/forms/${encodeURIComponent(id)}`, formSchema);
|
|
20
|
+
printData(form, json ? "json" : config.outputFormat);
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "../../config.js";
|
|
3
|
+
import { requestJson } from "../../http/client.js";
|
|
4
|
+
import { printData } from "../../output.js";
|
|
5
|
+
|
|
6
|
+
const formListItemSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
description: z.string(),
|
|
10
|
+
public_id: z.string(),
|
|
11
|
+
created_at: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export async function listFormsCommand(json?: boolean) {
|
|
15
|
+
const config = await loadConfig();
|
|
16
|
+
const forms = await requestJson(config, "/api/v1/forms", z.array(formListItemSchema));
|
|
17
|
+
printData(forms, json ? "json" : config.outputFormat);
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "../../config.js";
|
|
3
|
+
import { requestJson } from "../../http/client.js";
|
|
4
|
+
import { printData } from "../../output.js";
|
|
5
|
+
|
|
6
|
+
const formTypeSchema = z.enum(["structured", "dynamic"]);
|
|
7
|
+
|
|
8
|
+
const formSchema = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
description: z.string(),
|
|
12
|
+
public_id: z.string(),
|
|
13
|
+
type: formTypeSchema,
|
|
14
|
+
created_at: z.string(),
|
|
15
|
+
updated_at: z.string(),
|
|
16
|
+
organization_id: z.string().optional(),
|
|
17
|
+
user_id: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type UpdateFormOptions = {
|
|
21
|
+
name?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
tone?: string;
|
|
25
|
+
clearTone?: boolean;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function updateFormCommand(id: string, options: UpdateFormOptions) {
|
|
30
|
+
if (options.tone !== undefined && options.clearTone) {
|
|
31
|
+
throw new Error("Use either --tone or --clear-tone, not both");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const payload: Record<string, unknown> = {};
|
|
35
|
+
|
|
36
|
+
if (options.name !== undefined) {
|
|
37
|
+
const name = options.name.trim();
|
|
38
|
+
if (name.length === 0) {
|
|
39
|
+
throw new Error("Invalid --name. Must be at least 1 character");
|
|
40
|
+
}
|
|
41
|
+
payload.name = name;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.description !== undefined) {
|
|
45
|
+
payload.description = options.description;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.type !== undefined) {
|
|
49
|
+
const parsedType = formTypeSchema.safeParse(options.type);
|
|
50
|
+
if (!parsedType.success) {
|
|
51
|
+
throw new Error("Invalid --type. Expected one of: structured, dynamic");
|
|
52
|
+
}
|
|
53
|
+
payload.type = parsedType.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.clearTone) {
|
|
57
|
+
payload.tone = null;
|
|
58
|
+
} else if (options.tone !== undefined) {
|
|
59
|
+
payload.tone = options.tone;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (Object.keys(payload).length === 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"No update fields provided. Use one of: --name, --description, --type, --tone, --clear-tone"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const config = await loadConfig();
|
|
69
|
+
const form = await requestJson(config, `/api/v1/forms/${encodeURIComponent(id)}`, formSchema, {
|
|
70
|
+
method: "PATCH",
|
|
71
|
+
body: payload,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
printData(form, options.json ? "json" : config.outputFormat);
|
|
75
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { type CliConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const configSchema = z.object({
|
|
8
|
+
baseUrl: z.string().url(),
|
|
9
|
+
apiKey: z.string().min(1).nullable(),
|
|
10
|
+
outputFormat: z.enum(["table", "json"]),
|
|
11
|
+
timeoutMs: z.number().int().positive(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const defaultConfig: CliConfig = {
|
|
15
|
+
baseUrl: "https://getaiform.com",
|
|
16
|
+
apiKey: null,
|
|
17
|
+
outputFormat: "table",
|
|
18
|
+
timeoutMs: 15_000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function getConfigPath() {
|
|
22
|
+
const xdgHome = process.env.XDG_CONFIG_HOME;
|
|
23
|
+
const base = xdgHome && xdgHome.length > 0 ? xdgHome : join(homedir(), ".config");
|
|
24
|
+
return join(base, "dashform", "config.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadConfig(): Promise<CliConfig> {
|
|
28
|
+
const path = getConfigPath();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const raw = await readFile(path, "utf8");
|
|
32
|
+
const parsed = configSchema.partial().parse(JSON.parse(raw));
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
baseUrl: parsed.baseUrl ?? defaultConfig.baseUrl,
|
|
36
|
+
apiKey: parsed.apiKey ?? defaultConfig.apiKey,
|
|
37
|
+
outputFormat: parsed.outputFormat ?? defaultConfig.outputFormat,
|
|
38
|
+
timeoutMs: parsed.timeoutMs ?? defaultConfig.timeoutMs,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
42
|
+
return defaultConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (error instanceof z.ZodError) {
|
|
46
|
+
throw new Error(`Invalid Dashform config at ${path}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function saveConfig(nextConfig: CliConfig) {
|
|
54
|
+
const path = getConfigPath();
|
|
55
|
+
await mkdir(dirname(path), { recursive: true });
|
|
56
|
+
const validated = configSchema.parse(nextConfig);
|
|
57
|
+
await writeFile(path, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
|
|
58
|
+
await chmod(path, 0o600);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function updateConfig(
|
|
62
|
+
updater: (current: CliConfig) => CliConfig | Promise<CliConfig>
|
|
63
|
+
) {
|
|
64
|
+
const current = await loadConfig();
|
|
65
|
+
const next = await updater(current);
|
|
66
|
+
await saveConfig(next);
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CliConfig } from "../types.js";
|
|
3
|
+
import { HttpError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
type RequestOptions = {
|
|
6
|
+
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
|
7
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function getAuthHeaders(config: CliConfig): Record<string, string> {
|
|
12
|
+
if (!config.apiKey) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
"x-api-key": config.apiKey,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildUrl(baseUrl: string, path: string, query?: RequestOptions["query"]) {
|
|
22
|
+
const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
23
|
+
|
|
24
|
+
if (query) {
|
|
25
|
+
for (const [key, value] of Object.entries(query)) {
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
url.searchParams.set(key, String(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function requestJson<T>(
|
|
38
|
+
config: CliConfig,
|
|
39
|
+
path: string,
|
|
40
|
+
schema: z.ZodType<T>,
|
|
41
|
+
options: RequestOptions = {}
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const url = buildUrl(config.baseUrl, path, options.query);
|
|
48
|
+
const init: RequestInit = {
|
|
49
|
+
method: options.method ?? "GET",
|
|
50
|
+
headers: {
|
|
51
|
+
Accept: "application/json",
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"User-Agent": "dashform-cli/dev",
|
|
54
|
+
...getAuthHeaders(config),
|
|
55
|
+
},
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (options.body !== undefined) {
|
|
60
|
+
init.body = JSON.stringify(options.body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await fetch(url, init);
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
|
|
66
|
+
let parsed: unknown = null;
|
|
67
|
+
if (text) {
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(text);
|
|
70
|
+
} catch {
|
|
71
|
+
parsed = text;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const message =
|
|
77
|
+
(typeof parsed === "object" && parsed && "error_description" in parsed
|
|
78
|
+
? (parsed as { error_description?: string }).error_description
|
|
79
|
+
: undefined) ??
|
|
80
|
+
(typeof parsed === "object" && parsed && "message" in parsed
|
|
81
|
+
? (parsed as { message?: string }).message
|
|
82
|
+
: undefined) ??
|
|
83
|
+
`Request failed for ${url.pathname}`;
|
|
84
|
+
|
|
85
|
+
throw new HttpError(message, response.status, parsed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return schema.parse(parsed);
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
readonly status: number,
|
|
5
|
+
readonly body: unknown
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "HttpError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function describeHttpError(error: unknown) {
|
|
13
|
+
if (error instanceof HttpError) {
|
|
14
|
+
return `${error.message} (HTTP ${error.status})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return error.message;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return "Unknown error";
|
|
22
|
+
}
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { type OutputFormat } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function printData(data: unknown, format: OutputFormat) {
|
|
5
|
+
if (format === "json") {
|
|
6
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (Array.isArray(data)) {
|
|
11
|
+
printArrayTable(data);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (data && typeof data === "object") {
|
|
16
|
+
for (const [key, value] of Object.entries(data)) {
|
|
17
|
+
process.stdout.write(`${pc.cyan(key)}: ${String(value ?? "")}\n`);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.stdout.write(`${String(data)}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printArrayTable(items: unknown[]) {
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
process.stdout.write("No results.\n");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const rows = items.map((item) => {
|
|
32
|
+
if (!item || typeof item !== "object") {
|
|
33
|
+
return { value: String(item) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(item).map(([key, value]) => [key, value == null ? "" : String(value)])
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
42
|
+
const widths = new Map(
|
|
43
|
+
columns.map((column) => [
|
|
44
|
+
column,
|
|
45
|
+
Math.max(column.length, ...rows.map((row) => (row[column] ?? "").length)),
|
|
46
|
+
])
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const header = columns
|
|
50
|
+
.map((column) => pc.bold(column.padEnd(widths.get(column) ?? column.length)))
|
|
51
|
+
.join(" ");
|
|
52
|
+
const divider = columns
|
|
53
|
+
.map((column) => "-".repeat(widths.get(column) ?? column.length))
|
|
54
|
+
.join(" ");
|
|
55
|
+
|
|
56
|
+
process.stdout.write(`${header}\n${divider}\n`);
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
process.stdout.write(
|
|
59
|
+
`${columns.map((column) => (row[column] ?? "").padEnd(widths.get(column) ?? column.length)).join(" ")}\n`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type OutputFormat = "table" | "json";
|
|
2
|
+
|
|
3
|
+
export type CliConfig = {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string | null;
|
|
6
|
+
outputFormat: OutputFormat;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type MeResponse = {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
name: string | null;
|
|
14
|
+
emailVerified: boolean;
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FormListItem = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
public_id: string;
|
|
23
|
+
created_at: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type FormType = "structured" | "dynamic";
|
|
27
|
+
|
|
28
|
+
export type FormDetail = FormListItem & {
|
|
29
|
+
type?: FormType;
|
|
30
|
+
organization_id?: string;
|
|
31
|
+
user_id?: string;
|
|
32
|
+
updated_at?: string;
|
|
33
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2024"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"noImplicitOverride": true,
|
|
10
|
+
"exactOptionalPropertyTypes": true,
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"rootDir": ".",
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
18
|
+
}
|