@vwork/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/dist/apps.js +53 -0
- package/dist/auth/oidc.js +23 -0
- package/dist/auth/store.js +63 -0
- package/dist/client.js +45 -0
- package/dist/codegen.js +140 -0
- package/dist/command.js +70 -0
- package/dist/config.js +36 -0
- package/dist/db.js +124 -0
- package/dist/functions.js +237 -0
- package/dist/index.js +35 -0
- package/dist/kv.js +73 -0
- package/dist/login.js +176 -0
- package/dist/migrations.js +76 -0
- package/dist/observability.js +20 -0
- package/dist/oclif-command.js +357 -0
- package/dist/output.js +12 -0
- package/dist/queues.js +71 -0
- package/dist/runtime.js +11 -0
- package/dist/secrets.js +16 -0
- package/package.json +31 -0
package/dist/apps.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { stringFlag } from "./config.js";
|
|
2
|
+
export async function runAppCommand(command, client) {
|
|
3
|
+
const action = command.path[1];
|
|
4
|
+
if (action === "list") {
|
|
5
|
+
const body = await client.request("GET", "/apps");
|
|
6
|
+
return { rows: body.apps.map((app) => ({ id: app.id, slug: app.slug, host: app.api_host })) };
|
|
7
|
+
}
|
|
8
|
+
if (action === "inspect") {
|
|
9
|
+
const target = requiredPosition(command, 0, "app id or slug");
|
|
10
|
+
return { value: await client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`) };
|
|
11
|
+
}
|
|
12
|
+
if (action === "create") {
|
|
13
|
+
const host = stringFlag(command.flags.host);
|
|
14
|
+
const body = {
|
|
15
|
+
name: requiredFlag(command, "name"),
|
|
16
|
+
slug: requiredFlag(command, "slug"),
|
|
17
|
+
...(host ? { api_host: host } : {}),
|
|
18
|
+
data_source_id: requiredFlag(command, "data-source"),
|
|
19
|
+
schema_name: stringFlag(command.flags.schema) ?? "public",
|
|
20
|
+
isolation_mode: stringFlag(command.flags.isolation) ?? "shared_schema"
|
|
21
|
+
};
|
|
22
|
+
return { value: await client.request("POST", "/apps", body) };
|
|
23
|
+
}
|
|
24
|
+
if (action === "delete") {
|
|
25
|
+
const target = requiredPosition(command, 0, "app id or slug");
|
|
26
|
+
const app = await client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
27
|
+
return { value: await client.request("DELETE", `/apps/${encodeURIComponent(String(app.id))}`) };
|
|
28
|
+
}
|
|
29
|
+
if (action === "bind-data-source") {
|
|
30
|
+
const target = requiredPosition(command, 0, "app id or slug");
|
|
31
|
+
const app = await client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
32
|
+
return {
|
|
33
|
+
value: await client.request("POST", `/apps/${encodeURIComponent(String(app.id))}/bind-data-source`, {
|
|
34
|
+
data_source_id: requiredFlag(command, "data-source"),
|
|
35
|
+
schema_name: stringFlag(command.flags.schema) ?? "public",
|
|
36
|
+
isolation_mode: stringFlag(command.flags.isolation) ?? "shared_schema"
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Unknown apps command: ${action ?? ""}`);
|
|
41
|
+
}
|
|
42
|
+
function requiredFlag(command, key) {
|
|
43
|
+
const value = stringFlag(command.flags[key]);
|
|
44
|
+
if (!value)
|
|
45
|
+
throw new Error(`Missing --${key}`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function requiredPosition(command, index, label) {
|
|
49
|
+
const value = command.positionals[index];
|
|
50
|
+
if (!value)
|
|
51
|
+
throw new Error(`Missing ${label}`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
export function createPkcePair() {
|
|
3
|
+
const codeVerifier = base64Url(randomBytes(32));
|
|
4
|
+
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
5
|
+
return { codeVerifier, codeChallenge };
|
|
6
|
+
}
|
|
7
|
+
export function createState() {
|
|
8
|
+
return base64Url(randomBytes(24));
|
|
9
|
+
}
|
|
10
|
+
export function buildAuthorizationUrl(input) {
|
|
11
|
+
const url = new URL(input.authorizationEndpoint);
|
|
12
|
+
url.searchParams.set("client_id", input.clientId);
|
|
13
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
14
|
+
url.searchParams.set("response_type", "code");
|
|
15
|
+
url.searchParams.set("scope", input.scopes.join(" "));
|
|
16
|
+
url.searchParams.set("state", input.state);
|
|
17
|
+
url.searchParams.set("code_challenge", input.codeChallenge);
|
|
18
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
19
|
+
return url.toString();
|
|
20
|
+
}
|
|
21
|
+
function base64Url(bytes) {
|
|
22
|
+
return Buffer.from(bytes).toString("base64url");
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
export function resolveCredentialPath(env, home) {
|
|
6
|
+
if (env.VWORK_HOME)
|
|
7
|
+
return join(env.VWORK_HOME, "credentials.json");
|
|
8
|
+
return join(home, ".vwork", "credentials.json");
|
|
9
|
+
}
|
|
10
|
+
export function resolveCompatibilityCredentialPath(home) {
|
|
11
|
+
return join(home, ".memhub", "credentials.json");
|
|
12
|
+
}
|
|
13
|
+
export class FileCredentialStore {
|
|
14
|
+
root;
|
|
15
|
+
constructor(root) {
|
|
16
|
+
this.root = root;
|
|
17
|
+
}
|
|
18
|
+
async saveApiKey(input) {
|
|
19
|
+
const credentials = {
|
|
20
|
+
base_url: input.baseUrl,
|
|
21
|
+
auth_mode: "api_key",
|
|
22
|
+
api_key: input.apiKey
|
|
23
|
+
};
|
|
24
|
+
const path = join(this.root, "credentials.json");
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
await writeFile(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
|
|
27
|
+
await chmod(path, 0o600);
|
|
28
|
+
}
|
|
29
|
+
async loadCredentials() {
|
|
30
|
+
const path = join(this.root, "credentials.json");
|
|
31
|
+
if (!existsSync(path))
|
|
32
|
+
return null;
|
|
33
|
+
return parseCredentials(await readFile(path, "utf8"));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function loadDefaultCredentials(env = process.env, home = homedir()) {
|
|
37
|
+
const path = resolveCredentialPath(env, home);
|
|
38
|
+
if (existsSync(path))
|
|
39
|
+
return parseCredentials(await readFile(path, "utf8"));
|
|
40
|
+
if (env.VWORK_HOME)
|
|
41
|
+
return null;
|
|
42
|
+
const compatibilityPath = resolveCompatibilityCredentialPath(home);
|
|
43
|
+
if (existsSync(compatibilityPath))
|
|
44
|
+
return parseCredentials(await readFile(compatibilityPath, "utf8"));
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function parseCredentials(raw) {
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
50
|
+
throw new Error("Invalid VWork credentials file.");
|
|
51
|
+
}
|
|
52
|
+
const credentials = parsed;
|
|
53
|
+
if (typeof credentials.base_url !== "string" ||
|
|
54
|
+
credentials.auth_mode !== "api_key" ||
|
|
55
|
+
typeof credentials.api_key !== "string") {
|
|
56
|
+
throw new Error("Invalid VWork credentials file.");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
base_url: credentials.base_url,
|
|
60
|
+
auth_mode: "api_key",
|
|
61
|
+
api_key: credentials.api_key
|
|
62
|
+
};
|
|
63
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class PlatformApiError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
requestId;
|
|
5
|
+
constructor(input) {
|
|
6
|
+
super(input.message);
|
|
7
|
+
this.code = input.code;
|
|
8
|
+
this.status = input.status;
|
|
9
|
+
this.requestId = input.requestId;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class PlatformClient {
|
|
13
|
+
apiUrl;
|
|
14
|
+
trustedUserId;
|
|
15
|
+
fetchImpl;
|
|
16
|
+
constructor(input) {
|
|
17
|
+
this.apiUrl = input.apiUrl.replace(/\/+$/, "");
|
|
18
|
+
this.trustedUserId = input.trustedUserId;
|
|
19
|
+
this.fetchImpl = input.fetch ?? fetch;
|
|
20
|
+
}
|
|
21
|
+
async request(method, path, body) {
|
|
22
|
+
const headers = new Headers({ accept: "application/json" });
|
|
23
|
+
if (body !== undefined)
|
|
24
|
+
headers.set("content-type", "application/json");
|
|
25
|
+
if (this.trustedUserId)
|
|
26
|
+
headers.set("x-vwork-cli-user-id", this.trustedUserId);
|
|
27
|
+
const response = await this.fetchImpl(new Request(`${this.apiUrl}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers,
|
|
30
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
31
|
+
}));
|
|
32
|
+
const text = await response.text();
|
|
33
|
+
const parsed = text ? JSON.parse(text) : null;
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const error = parsed && typeof parsed === "object" && "error" in parsed ? parsed.error : {};
|
|
36
|
+
throw new PlatformApiError({
|
|
37
|
+
code: error.code ?? "PLATFORM_API_ERROR",
|
|
38
|
+
message: error.message ?? `Platform API request failed with status ${response.status}`,
|
|
39
|
+
status: response.status,
|
|
40
|
+
requestId: response.headers.get("x-request-id")
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { stringFlag } from "./config.js";
|
|
4
|
+
export async function runCodegenCommand(command, client, config) {
|
|
5
|
+
const group = command.path[1];
|
|
6
|
+
const action = command.path[2];
|
|
7
|
+
if (group === "skill" && action === "install-curl") {
|
|
8
|
+
const baseUrl = stringFlag(command.flags["base-url"]) ?? "https://raw.githubusercontent.com/vwork/vwork/main";
|
|
9
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
10
|
+
return {
|
|
11
|
+
value: {
|
|
12
|
+
skill: "vwork-codegen",
|
|
13
|
+
install_command: `curl -fsSL ${normalizedBaseUrl}/scripts/install-vwork-codegen-skill.sh | bash`,
|
|
14
|
+
install_path: "${CODEX_HOME:-$HOME/.codex}/skills/vwork-codegen"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (group === "mcp" && action === "config-snippet") {
|
|
19
|
+
return { value: mcpConfigSnippet(command, config) };
|
|
20
|
+
}
|
|
21
|
+
const app = await resolveApp(command, client, config);
|
|
22
|
+
const appPath = `/apps/${encodeURIComponent(String(app.id))}`;
|
|
23
|
+
if (group === "mobile" && action === "from-schema") {
|
|
24
|
+
const tableNames = arrayFlag(command.flags.table);
|
|
25
|
+
return {
|
|
26
|
+
value: await client.request("POST", `${appPath}/generate-mobile-crud-from-schema`, {
|
|
27
|
+
schema_name: stringFlag(command.flags.schema) ?? config.schema,
|
|
28
|
+
...(tableNames.length > 0 ? { table_names: tableNames } : {}),
|
|
29
|
+
...(stringFlag(command.flags.prompt) ? { prompt: stringFlag(command.flags.prompt) } : {})
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (group === "function" && action === "generate") {
|
|
34
|
+
const functionName = requiredFlag(command, "function");
|
|
35
|
+
const prompt = requiredFlag(command, "prompt");
|
|
36
|
+
const bindingNames = arrayFlag(command.flags.binding);
|
|
37
|
+
const sourceCode = stringFlag(command.flags["source-code"]);
|
|
38
|
+
const draftInvoke = draftInvokeInput(command);
|
|
39
|
+
return {
|
|
40
|
+
value: await client.request("POST", `${appPath}/functions/${encodeURIComponent(functionName)}/codegen`, {
|
|
41
|
+
prompt,
|
|
42
|
+
auth_required: command.flags["no-auth"] === true ? false : true,
|
|
43
|
+
...(bindingNames.length > 0 ? { binding_names: bindingNames } : {}),
|
|
44
|
+
...(sourceCode ? { source_code: sourceCode } : {}),
|
|
45
|
+
...(command.flags.repair === true ? { repair: { enabled: true, max_attempts: 2 } } : {}),
|
|
46
|
+
...(draftInvoke ? { draft_invoke: draftInvoke } : {})
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (group === "runs" && action === "list") {
|
|
51
|
+
const body = await client.request("GET", `${appPath}/code-generations`);
|
|
52
|
+
return { rows: body.code_generations };
|
|
53
|
+
}
|
|
54
|
+
if (group === "runs" && action === "download") {
|
|
55
|
+
const runId = requiredFlag(command, "run");
|
|
56
|
+
const out = requiredFlag(command, "out");
|
|
57
|
+
const bundle = await client.requestBytes("GET", `${appPath}/code-generations/${encodeURIComponent(runId)}/bundle`);
|
|
58
|
+
const bytes = extractBytes(bundle);
|
|
59
|
+
const path = resolve(process.cwd(), out);
|
|
60
|
+
await mkdir(dirname(path), { recursive: true });
|
|
61
|
+
await writeFile(path, bytes);
|
|
62
|
+
return { value: { path, bytes: bytes.length } };
|
|
63
|
+
}
|
|
64
|
+
if (group === "runs" && action === "verify") {
|
|
65
|
+
const runId = requiredFlag(command, "run");
|
|
66
|
+
const body = await client.request("GET", `${appPath}/code-generations`);
|
|
67
|
+
const exists = body.code_generations.some((run) => String(run.id) === runId);
|
|
68
|
+
if (!exists)
|
|
69
|
+
throw new Error(`Missing code generation run: ${runId}`);
|
|
70
|
+
const bundle = await client.requestBytes("GET", `${appPath}/code-generations/${encodeURIComponent(runId)}/bundle`);
|
|
71
|
+
const bytes = extractBytes(bundle);
|
|
72
|
+
return { value: { run_id: runId, bundle_downloadable: true, bytes: bytes.length } };
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Unknown codegen command: ${command.path.join(" ")}`);
|
|
75
|
+
}
|
|
76
|
+
async function resolveApp(command, client, config) {
|
|
77
|
+
const target = stringFlag(command.flags.app) ?? config.appId;
|
|
78
|
+
if (!target)
|
|
79
|
+
throw new Error("Missing --app");
|
|
80
|
+
return client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
81
|
+
}
|
|
82
|
+
function arrayFlag(value) {
|
|
83
|
+
if (Array.isArray(value))
|
|
84
|
+
return value;
|
|
85
|
+
if (typeof value === "string")
|
|
86
|
+
return [value];
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
function requiredFlag(command, key) {
|
|
90
|
+
const value = stringFlag(command.flags[key]);
|
|
91
|
+
if (!value)
|
|
92
|
+
throw new Error(`Missing --${key}`);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function draftInvokeInput(command) {
|
|
96
|
+
const body = optionalJsonObjectFlag(command, "draft-invoke-body");
|
|
97
|
+
const bindings = optionalJsonObjectFlag(command, "draft-invoke-bindings");
|
|
98
|
+
if (!body && !bindings)
|
|
99
|
+
return undefined;
|
|
100
|
+
return {
|
|
101
|
+
enabled: true,
|
|
102
|
+
...(body ? { body } : {}),
|
|
103
|
+
...(bindings ? { bindings } : {})
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function optionalJsonObjectFlag(command, key) {
|
|
107
|
+
const value = stringFlag(command.flags[key]);
|
|
108
|
+
if (!value)
|
|
109
|
+
return undefined;
|
|
110
|
+
const parsed = JSON.parse(value);
|
|
111
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
112
|
+
throw new Error(`--${key} must be a JSON object`);
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
function mcpConfigSnippet(command, config) {
|
|
117
|
+
const workspace = stringFlag(command.flags.workspace) ?? process.cwd();
|
|
118
|
+
const appId = stringFlag(command.flags.app) ?? config.appId;
|
|
119
|
+
return {
|
|
120
|
+
server: "mcp-vwork",
|
|
121
|
+
install_hint: "pnpm --filter @vwork/mcp-vwork build",
|
|
122
|
+
codex_config: {
|
|
123
|
+
mcp_servers: {
|
|
124
|
+
vwork: {
|
|
125
|
+
command: "pnpm",
|
|
126
|
+
args: ["--dir", workspace, "--filter", "@vwork/mcp-vwork", "dev"],
|
|
127
|
+
env: {
|
|
128
|
+
VWORK_API_URL: stringFlag(command.flags["api-url"]) ?? config.apiUrl,
|
|
129
|
+
VWORK_MCP_WRITE_MODE: "codegen",
|
|
130
|
+
...(appId ? { VWORK_APP_ID: appId } : {}),
|
|
131
|
+
VWORK_SCHEMA: stringFlag(command.flags.schema) ?? config.schema
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function extractBytes(response) {
|
|
139
|
+
return response instanceof Uint8Array ? response : response.bytes;
|
|
140
|
+
}
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const COMMAND_ROOTS = new Set(["apps", "db", "migrations", "functions", "codegen", "secrets", "kv", "queues", "runtime", "observability"]);
|
|
2
|
+
const BOOLEAN_FLAGS = new Set(["yes", "verbose", "help", "no-auth", "repair"]);
|
|
3
|
+
export function parseCommand(argv) {
|
|
4
|
+
const path = [];
|
|
5
|
+
const positionals = [];
|
|
6
|
+
const flags = {};
|
|
7
|
+
let commandStarted = false;
|
|
8
|
+
let commandFlagSeen = false;
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const token = argv[index] ?? "";
|
|
11
|
+
if (token === "--") {
|
|
12
|
+
positionals.push(...argv.slice(index + 1));
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
if (token.startsWith("--")) {
|
|
16
|
+
if (commandStarted)
|
|
17
|
+
commandFlagSeen = true;
|
|
18
|
+
const raw = token.slice(2);
|
|
19
|
+
const eqIndex = raw.indexOf("=");
|
|
20
|
+
const key = eqIndex >= 0 ? raw.slice(0, eqIndex) : raw;
|
|
21
|
+
const inlineValue = eqIndex >= 0 ? raw.slice(eqIndex + 1) : undefined;
|
|
22
|
+
const next = argv[index + 1];
|
|
23
|
+
const value = inlineValue ?? (BOOLEAN_FLAGS.has(key) ? true : next && !next.startsWith("--") ? next : true);
|
|
24
|
+
if (inlineValue === undefined && value !== true)
|
|
25
|
+
index += 1;
|
|
26
|
+
appendFlag(flags, key, value);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!commandStarted) {
|
|
30
|
+
if (COMMAND_ROOTS.has(token)) {
|
|
31
|
+
commandStarted = true;
|
|
32
|
+
path.push(token);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
positionals.push(token);
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!commandFlagSeen && path.length < maxPathLength(path)) {
|
|
40
|
+
path.push(token);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
positionals.push(token);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { path, positionals, flags };
|
|
47
|
+
}
|
|
48
|
+
function maxPathLength(path) {
|
|
49
|
+
if (path[0] === "db")
|
|
50
|
+
return 3;
|
|
51
|
+
if (path[0] === "codegen")
|
|
52
|
+
return 3;
|
|
53
|
+
if (path[0] === "kv")
|
|
54
|
+
return 3;
|
|
55
|
+
if (path[0] === "functions" && path[1] === "previews")
|
|
56
|
+
return 3;
|
|
57
|
+
if (path[0] === "functions" && path[1] === "routes")
|
|
58
|
+
return 3;
|
|
59
|
+
if (path[0] === "functions" && path[1] === "queue-bindings")
|
|
60
|
+
return 3;
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
function appendFlag(target, key, value) {
|
|
64
|
+
const existing = target[key];
|
|
65
|
+
if (existing === undefined) {
|
|
66
|
+
target[key] = value;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
target[key] = Array.isArray(existing) ? [...existing, String(value)] : [String(existing), String(value)];
|
|
70
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseCommand } from "./command.js";
|
|
4
|
+
export const DEFAULT_CLI_TRUSTED_USER_ID = "user_cli_dev";
|
|
5
|
+
export function resolveCliConfig(argv, env, cwd) {
|
|
6
|
+
const command = parseCommand(argv);
|
|
7
|
+
return resolveCliConfigFromFlags(command.flags, env, cwd);
|
|
8
|
+
}
|
|
9
|
+
export function resolveCliConfigFromFlags(flags, env, cwd, defaultApiUrl = "http://127.0.0.1:8787") {
|
|
10
|
+
const project = readProjectConfig(cwd);
|
|
11
|
+
const apiUrl = stringFlag(flags["api-url"]) ?? env.VWORK_API_URL ?? project.apiUrl ?? defaultApiUrl;
|
|
12
|
+
const appId = stringFlag(flags.app) ?? env.VWORK_APP_ID ?? project.appId ?? null;
|
|
13
|
+
const schema = stringFlag(flags.schema) ?? env.VWORK_SCHEMA ?? project.schema ?? "public";
|
|
14
|
+
const output = outputFormat(stringFlag(flags.format) ?? env.VWORK_OUTPUT ?? project.output ?? "table");
|
|
15
|
+
const trustedUserId = env.VWORK_CLI_TRUSTED_USER_ID ?? project.trustedUserId ?? DEFAULT_CLI_TRUSTED_USER_ID;
|
|
16
|
+
return { apiUrl: apiUrl.replace(/\/+$/, ""), appId, schema, output, trustedUserId };
|
|
17
|
+
}
|
|
18
|
+
function readProjectConfig(cwd) {
|
|
19
|
+
const path = join(cwd, ".vwork", "config.json");
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
return {};
|
|
22
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
23
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
24
|
+
}
|
|
25
|
+
export function stringFlag(value) {
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return value;
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.at(-1);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function outputFormat(value) {
|
|
33
|
+
if (value === "json" || value === "table")
|
|
34
|
+
return value;
|
|
35
|
+
throw new Error(`Unsupported output format: ${value}`);
|
|
36
|
+
}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { stringFlag } from "./config.js";
|
|
2
|
+
export async function runDbCommand(command, client, config) {
|
|
3
|
+
const group = command.path[1];
|
|
4
|
+
const action = command.path[2];
|
|
5
|
+
const app = await resolveApp(command, client, config);
|
|
6
|
+
const schema = stringFlag(command.flags.schema) ?? config.schema;
|
|
7
|
+
if (group === "tables" && action === "list") {
|
|
8
|
+
const response = await client.request("GET", withQuery(`/apps/${encodeURIComponent(String(app.id))}/database/tables`, { schema }));
|
|
9
|
+
return { rows: response.tables.map((table) => ({ name: table.name, columns: table.columns?.length ?? 0 })) };
|
|
10
|
+
}
|
|
11
|
+
if (group === "sql" && (action === "exec" || action === "preview")) {
|
|
12
|
+
const sql = requiredFlag(command, "sql");
|
|
13
|
+
const response = await client.request("POST", `/apps/${encodeURIComponent(String(app.id))}/sql/${action === "preview" ? "preview" : "execute"}`, { schema_name: schema, sql });
|
|
14
|
+
return { rows: response.rows ?? [] };
|
|
15
|
+
}
|
|
16
|
+
if (group === "rows") {
|
|
17
|
+
return runRowCommand(command, client, app, schema);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Unknown db command: ${command.path.join(" ")}`);
|
|
20
|
+
}
|
|
21
|
+
async function runRowCommand(command, client, app, schema) {
|
|
22
|
+
const action = command.path[2];
|
|
23
|
+
const table = requiredPosition(command, 0, "table name");
|
|
24
|
+
const basePath = `/apps/${encodeURIComponent(String(app.id))}/tables/${encodeURIComponent(table)}/rows`;
|
|
25
|
+
if (action === "select") {
|
|
26
|
+
const response = await client.request("GET", withQuery(basePath, rowSelectQuery(command, schema)));
|
|
27
|
+
return { rows: response.rows ?? [] };
|
|
28
|
+
}
|
|
29
|
+
if (action === "insert") {
|
|
30
|
+
const response = await client.request("POST", basePath, { schema_name: schema, row: jsonObjectFlag(command, "data") });
|
|
31
|
+
return { rows: response.rows ?? [] };
|
|
32
|
+
}
|
|
33
|
+
if (action === "update") {
|
|
34
|
+
const response = await client.request("PATCH", basePath, {
|
|
35
|
+
schema_name: schema,
|
|
36
|
+
primary_key: primaryKeyFlag(command),
|
|
37
|
+
patch: jsonObjectFlag(command, "data")
|
|
38
|
+
});
|
|
39
|
+
return { rows: response.rows ?? [] };
|
|
40
|
+
}
|
|
41
|
+
if (action === "delete") {
|
|
42
|
+
const response = await client.request("DELETE", basePath, {
|
|
43
|
+
schema_name: schema,
|
|
44
|
+
primary_key: primaryKeyFlag(command)
|
|
45
|
+
});
|
|
46
|
+
return { rows: response.rows ?? [] };
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Unknown db rows command: ${action ?? ""}`);
|
|
49
|
+
}
|
|
50
|
+
async function resolveApp(command, client, config) {
|
|
51
|
+
const target = stringFlag(command.flags.app) ?? config.appId;
|
|
52
|
+
if (!target)
|
|
53
|
+
throw new Error("Missing --app");
|
|
54
|
+
return client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
55
|
+
}
|
|
56
|
+
function rowSelectQuery(command, schema) {
|
|
57
|
+
const query = { schema };
|
|
58
|
+
const limit = stringFlag(command.flags.limit);
|
|
59
|
+
const offset = stringFlag(command.flags.offset);
|
|
60
|
+
const order = stringFlag(command.flags.order);
|
|
61
|
+
if (limit)
|
|
62
|
+
query.limit = limit;
|
|
63
|
+
if (offset)
|
|
64
|
+
query.offset = offset;
|
|
65
|
+
const filters = arrayFlag(command.flags.filter);
|
|
66
|
+
if (filters.length > 0)
|
|
67
|
+
query.filter = filters;
|
|
68
|
+
if (order) {
|
|
69
|
+
const [column, direction] = order.split(".");
|
|
70
|
+
query.order_by = column;
|
|
71
|
+
query.order_direction = direction === "desc" ? "desc" : "asc";
|
|
72
|
+
}
|
|
73
|
+
return query;
|
|
74
|
+
}
|
|
75
|
+
function withQuery(path, query) {
|
|
76
|
+
const searchParams = new URLSearchParams();
|
|
77
|
+
for (const [key, value] of Object.entries(query)) {
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
searchParams.set(key, value);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const item of value)
|
|
85
|
+
searchParams.append(key, item);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const queryString = searchParams.toString();
|
|
89
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
90
|
+
}
|
|
91
|
+
function jsonObjectFlag(command, key) {
|
|
92
|
+
const value = requiredFlag(command, key);
|
|
93
|
+
const parsed = JSON.parse(value);
|
|
94
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
95
|
+
throw new Error(`--${key} must be a JSON object`);
|
|
96
|
+
}
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
function primaryKeyFlag(command) {
|
|
100
|
+
const value = requiredFlag(command, "pk");
|
|
101
|
+
const separator = value.indexOf("=");
|
|
102
|
+
if (separator <= 0)
|
|
103
|
+
throw new Error("--pk must use column=value");
|
|
104
|
+
return { column: value.slice(0, separator), value: value.slice(separator + 1) };
|
|
105
|
+
}
|
|
106
|
+
function arrayFlag(value) {
|
|
107
|
+
if (Array.isArray(value))
|
|
108
|
+
return value;
|
|
109
|
+
if (typeof value === "string")
|
|
110
|
+
return [value];
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
function requiredFlag(command, key) {
|
|
114
|
+
const value = stringFlag(command.flags[key]);
|
|
115
|
+
if (!value)
|
|
116
|
+
throw new Error(`Missing --${key}`);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function requiredPosition(command, index, label) {
|
|
120
|
+
const value = command.positionals[index];
|
|
121
|
+
if (!value)
|
|
122
|
+
throw new Error(`Missing ${label}`);
|
|
123
|
+
return value;
|
|
124
|
+
}
|