@vwork/cli 0.1.7 → 0.1.8
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 +23 -1
- package/dist/codegen.js +57 -10
- package/dist/command.js +7 -1
- package/dist/functions.js +180 -2
- package/dist/index.js +0 -0
- package/dist/oclif-command.js +25 -7
- package/package.json +32 -31
- package/dist/client.js +0 -45
package/dist/apps.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { stringFlag } from "./config.js";
|
|
2
|
-
export async function runAppCommand(command, client) {
|
|
4
|
+
export async function runAppCommand(command, client, cwd = process.cwd()) {
|
|
3
5
|
const action = command.path[1];
|
|
6
|
+
if (action === "firebase") {
|
|
7
|
+
const subcommand = command.path[2];
|
|
8
|
+
const target = requiredPosition(command, 0, "app id or slug");
|
|
9
|
+
const app = await client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
10
|
+
const path = `/apps/${encodeURIComponent(String(app.id))}/auth/firebase`;
|
|
11
|
+
if (subcommand === "get") {
|
|
12
|
+
return { value: await client.request("GET", path) };
|
|
13
|
+
}
|
|
14
|
+
if (subcommand === "set") {
|
|
15
|
+
return {
|
|
16
|
+
value: await client.request("PUT", path, {
|
|
17
|
+
service_account_json: readFileSync(resolve(cwd, requiredFlag(command, "file")), "utf8").trim()
|
|
18
|
+
})
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (subcommand === "delete") {
|
|
22
|
+
return { value: await client.request("DELETE", path) };
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Unknown apps firebase command: ${subcommand ?? ""}`);
|
|
25
|
+
}
|
|
4
26
|
if (action === "list") {
|
|
5
27
|
const body = await client.request("GET", "/apps");
|
|
6
28
|
return { rows: body.apps.map((app) => ({ id: app.id, slug: app.slug, host: app.api_host })) };
|
package/dist/codegen.js
CHANGED
|
@@ -52,7 +52,7 @@ export async function runCodegenCommand(command, client, config) {
|
|
|
52
52
|
};
|
|
53
53
|
try {
|
|
54
54
|
return {
|
|
55
|
-
value: await client.request("POST", `${appPath}/functions/${encodeURIComponent(functionName)}
|
|
55
|
+
value: await client.request("POST", `${appPath}/functions/${encodeURIComponent(functionName)}/${command.flags.project === true ? "codegen-project" : "codegen"}`, requestBody)
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
@@ -87,6 +87,12 @@ export async function runCodegenCommand(command, client, config) {
|
|
|
87
87
|
const bytes = extractBytes(bundle);
|
|
88
88
|
return { value: { run_id: runId, bundle_downloadable: true, bytes: bytes.length } };
|
|
89
89
|
}
|
|
90
|
+
if (group === "runs" && action === "stop") {
|
|
91
|
+
const runId = requiredFlag(command, "run");
|
|
92
|
+
return {
|
|
93
|
+
value: await client.request("POST", `${appPath}/code-generations/${encodeURIComponent(runId)}/stop`, {})
|
|
94
|
+
};
|
|
95
|
+
}
|
|
90
96
|
throw new Error(`Unknown codegen command: ${command.path.join(" ")}`);
|
|
91
97
|
}
|
|
92
98
|
async function resolveApp(command, client, config) {
|
|
@@ -149,11 +155,48 @@ function sleep(ms) {
|
|
|
149
155
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
156
|
}
|
|
151
157
|
function mcpConfigSnippet(command, config) {
|
|
152
|
-
const workspace = stringFlag(command.flags.workspace)
|
|
158
|
+
const workspace = stringFlag(command.flags.workspace);
|
|
153
159
|
const appId = stringFlag(command.flags.app) ?? config.appId;
|
|
160
|
+
const apiUrl = stringFlag(command.flags["api-url"]) ?? config.apiUrl;
|
|
161
|
+
const schema = stringFlag(command.flags.schema) ?? config.schema;
|
|
162
|
+
const env = {
|
|
163
|
+
VWORK_API_URL: apiUrl,
|
|
164
|
+
VWORK_MCP_WRITE_MODE: "codegen",
|
|
165
|
+
...(appId ? { VWORK_APP_ID: appId } : {}),
|
|
166
|
+
VWORK_SCHEMA: schema
|
|
167
|
+
};
|
|
168
|
+
if (!workspace) {
|
|
169
|
+
const remoteUrl = remoteMcpUrl(apiUrl, {
|
|
170
|
+
mode: "codegen",
|
|
171
|
+
...(appId ? { app: appId } : {}),
|
|
172
|
+
schema
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
server: "vwork-remote-mcp",
|
|
176
|
+
transport: "streamable-http",
|
|
177
|
+
remote_url: remoteUrl,
|
|
178
|
+
auth_header: "Authorization: Bearer ${VWORK_API_KEY}",
|
|
179
|
+
install_hint: "No MCP npm package is required for the default remote server.",
|
|
180
|
+
cli_install_hint: [
|
|
181
|
+
"npm install -g @vwork/cli --registry https://registry.npmjs.org",
|
|
182
|
+
"pnpm install -g @vwork/cli --registry https://registry.npmjs.org"
|
|
183
|
+
],
|
|
184
|
+
codex_config: {
|
|
185
|
+
mcp_servers: {
|
|
186
|
+
vwork: {
|
|
187
|
+
type: "http",
|
|
188
|
+
url: remoteUrl,
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: "Bearer ${VWORK_API_KEY}"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
154
197
|
return {
|
|
155
|
-
server: "mcp-vwork",
|
|
156
|
-
install_hint: "
|
|
198
|
+
server: "mcp-vwork-local",
|
|
199
|
+
install_hint: "Repository developers only. Use the remote MCP server for normal clients.",
|
|
157
200
|
cli_install_hint: [
|
|
158
201
|
"npm install -g @vwork/cli --registry https://registry.npmjs.org",
|
|
159
202
|
"pnpm install -g @vwork/cli --registry https://registry.npmjs.org"
|
|
@@ -163,17 +206,21 @@ function mcpConfigSnippet(command, config) {
|
|
|
163
206
|
vwork: {
|
|
164
207
|
command: "pnpm",
|
|
165
208
|
args: ["--dir", workspace, "--filter", "@vwork/mcp-vwork", "dev"],
|
|
166
|
-
env
|
|
167
|
-
VWORK_API_URL: stringFlag(command.flags["api-url"]) ?? config.apiUrl,
|
|
168
|
-
VWORK_MCP_WRITE_MODE: "codegen",
|
|
169
|
-
...(appId ? { VWORK_APP_ID: appId } : {}),
|
|
170
|
-
VWORK_SCHEMA: stringFlag(command.flags.schema) ?? config.schema
|
|
171
|
-
}
|
|
209
|
+
env
|
|
172
210
|
}
|
|
173
211
|
}
|
|
174
212
|
}
|
|
175
213
|
};
|
|
176
214
|
}
|
|
215
|
+
function remoteMcpUrl(apiUrl, params) {
|
|
216
|
+
const url = new URL(apiUrl);
|
|
217
|
+
url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/api$/, "") + "/mcp";
|
|
218
|
+
url.search = "";
|
|
219
|
+
for (const [key, value] of Object.entries(params)) {
|
|
220
|
+
url.searchParams.set(key, value);
|
|
221
|
+
}
|
|
222
|
+
return url.toString();
|
|
223
|
+
}
|
|
177
224
|
function extractBytes(response) {
|
|
178
225
|
return response instanceof Uint8Array ? response : response.bytes;
|
|
179
226
|
}
|
package/dist/command.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const COMMAND_ROOTS = new Set(["apps", "db", "migrations", "functions", "codegen", "secrets", "kv", "queues", "observability"]);
|
|
2
|
-
const BOOLEAN_FLAGS = new Set(["yes", "verbose", "help", "no-auth", "repair"]);
|
|
2
|
+
const BOOLEAN_FLAGS = new Set(["yes", "verbose", "help", "no-auth", "project", "repair"]);
|
|
3
3
|
export function parseCommand(argv) {
|
|
4
4
|
const path = [];
|
|
5
5
|
const positionals = [];
|
|
@@ -46,12 +46,18 @@ export function parseCommand(argv) {
|
|
|
46
46
|
return { path, positionals, flags };
|
|
47
47
|
}
|
|
48
48
|
function maxPathLength(path) {
|
|
49
|
+
if (path[0] === "apps" && path[1] === "firebase")
|
|
50
|
+
return 3;
|
|
49
51
|
if (path[0] === "db")
|
|
50
52
|
return 3;
|
|
51
53
|
if (path[0] === "codegen")
|
|
52
54
|
return 3;
|
|
53
55
|
if (path[0] === "kv")
|
|
54
56
|
return 3;
|
|
57
|
+
if (path[0] === "functions" && path[1] === "firebase-access")
|
|
58
|
+
return 3;
|
|
59
|
+
if (path[0] === "functions" && path[1] === "bundle")
|
|
60
|
+
return 2;
|
|
55
61
|
if (path[0] === "functions" && path[1] === "previews")
|
|
56
62
|
return 3;
|
|
57
63
|
if (path[0] === "functions" && path[1] === "routes")
|
package/dist/functions.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { bundleFunctionProject, collectFunctionProject } from "@vwork/function-bundler";
|
|
3
7
|
import { stringFlag } from "./config.js";
|
|
4
8
|
export async function runFunctionCommand(command, client, config, cwd) {
|
|
5
9
|
const action = command.path[1];
|
|
6
10
|
if (action === "validate") {
|
|
7
11
|
return { value: await client.request("POST", "/apps/functions/validate-source", { source_code: sourceInput(command, cwd) }) };
|
|
8
12
|
}
|
|
13
|
+
if (action === "bundle") {
|
|
14
|
+
const projectPath = requiredPosition(command, 0, "project path");
|
|
15
|
+
const absoluteProjectPath = resolve(cwd, projectPath);
|
|
16
|
+
const out = resolve(cwd, stringFlag(command.flags.out) ?? `${projectPath}/dist/index.js`);
|
|
17
|
+
const snapshot = await collectFunctionProject(absoluteProjectPath);
|
|
18
|
+
const result = await bundleSnapshot(snapshot);
|
|
19
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
20
|
+
writeFileSync(out, result.code, "utf8");
|
|
21
|
+
writeFileSync(`${out}.manifest.json`, JSON.stringify(result.manifest, null, 2), "utf8");
|
|
22
|
+
return { value: { path: out, bytes: result.codeSize, hash: result.codeHash } };
|
|
23
|
+
}
|
|
9
24
|
const app = await resolveApp(command, client, config);
|
|
10
25
|
const appPath = `/apps/${encodeURIComponent(String(app.id))}`;
|
|
11
26
|
if (action === "previews") {
|
|
@@ -17,10 +32,55 @@ export async function runFunctionCommand(command, client, config, cwd) {
|
|
|
17
32
|
if (action === "queue-bindings") {
|
|
18
33
|
return runFunctionQueueBindingsCommand(command, client, appPath);
|
|
19
34
|
}
|
|
35
|
+
if (action === "firebase-access") {
|
|
36
|
+
return runFunctionFirebaseAccessCommand(command, client, appPath);
|
|
37
|
+
}
|
|
20
38
|
if (action === "list") {
|
|
21
39
|
const body = await client.request("GET", `${appPath}/functions`);
|
|
22
40
|
return { rows: body.functions };
|
|
23
41
|
}
|
|
42
|
+
if (action === "pull") {
|
|
43
|
+
const name = requiredPosition(command, 0, "function name");
|
|
44
|
+
const out = stringFlag(command.flags.out) ?? command.positionals[1];
|
|
45
|
+
if (!out)
|
|
46
|
+
throw new Error("Missing --out or output directory");
|
|
47
|
+
const outputDir = resolve(cwd, out);
|
|
48
|
+
assertWritablePullTarget(outputDir, command.flags.yes === true);
|
|
49
|
+
const tree = await client.request("GET", functionPath(appPath, name, "working-tree"));
|
|
50
|
+
const files = await readRemoteWorkingTreeFiles(client, appPath, name, tree);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const target = resolveSourceOutputPath(outputDir, file.path);
|
|
53
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
54
|
+
writeFileSync(target, file.content, "utf8");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
value: {
|
|
58
|
+
path: outputDir,
|
|
59
|
+
function: name,
|
|
60
|
+
files: files.length,
|
|
61
|
+
base_commit_id: tree.base_commit_id ?? null,
|
|
62
|
+
dirty: Boolean(tree.dirty)
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (action === "status") {
|
|
67
|
+
const name = requiredPosition(command, 0, "function name");
|
|
68
|
+
const projectPath = command.positionals[1] ?? stringFlag(command.flags.out);
|
|
69
|
+
if (!projectPath)
|
|
70
|
+
throw new Error("Missing project directory");
|
|
71
|
+
const projectRoot = resolve(cwd, projectPath);
|
|
72
|
+
const [local, tree] = await Promise.all([
|
|
73
|
+
collectFunctionProject(projectRoot),
|
|
74
|
+
client.request("GET", functionPath(appPath, name, "working-tree"))
|
|
75
|
+
]);
|
|
76
|
+
const remoteFiles = await readRemoteWorkingTreeFiles(client, appPath, name, tree);
|
|
77
|
+
return {
|
|
78
|
+
value: functionProjectStatus(projectRoot, local.files, remoteFiles, {
|
|
79
|
+
baseCommitId: tree.base_commit_id ?? null,
|
|
80
|
+
dirty: Boolean(tree.dirty)
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
24
84
|
if (action === "inspect") {
|
|
25
85
|
const name = requiredPosition(command, 0, "function name");
|
|
26
86
|
const body = await client.request("GET", `${appPath}/functions`);
|
|
@@ -39,6 +99,26 @@ export async function runFunctionCommand(command, client, config, cwd) {
|
|
|
39
99
|
}
|
|
40
100
|
if (action === "deploy") {
|
|
41
101
|
const name = requiredPosition(command, 0, "function name");
|
|
102
|
+
const projectPath = command.positionals[1];
|
|
103
|
+
if (projectPath && !stringFlag(command.flags.bundle)) {
|
|
104
|
+
const absoluteProjectPath = resolve(cwd, projectPath);
|
|
105
|
+
const snapshot = await collectFunctionProject(absoluteProjectPath);
|
|
106
|
+
const bundled = await bundleSnapshot(snapshot);
|
|
107
|
+
return {
|
|
108
|
+
value: await client.request("POST", functionPath(appPath, name, "deploy"), {
|
|
109
|
+
source_files: snapshot.files.map((file) => ({
|
|
110
|
+
path: file.path,
|
|
111
|
+
language: file.language,
|
|
112
|
+
content: file.content
|
|
113
|
+
})),
|
|
114
|
+
bundled_code: bundled.code,
|
|
115
|
+
build_manifest: bundled.manifest
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (stringFlag(command.flags.bundle)) {
|
|
120
|
+
return { value: await client.request("POST", functionPath(appPath, name, "deploy"), { bundled_code: sourceInput(command, cwd) }) };
|
|
121
|
+
}
|
|
42
122
|
return { value: await client.request("POST", functionPath(appPath, name, "deploy"), { source_code: sourceInput(command, cwd) }) };
|
|
43
123
|
}
|
|
44
124
|
if (action === "artifact") {
|
|
@@ -63,6 +143,22 @@ export async function runFunctionCommand(command, client, config, cwd) {
|
|
|
63
143
|
if (artifact) {
|
|
64
144
|
return { value: await client.request("POST", `${functionPath(appPath, name, "artifacts")}/${encodeURIComponent(artifact)}/invoke`, { body }) };
|
|
65
145
|
}
|
|
146
|
+
const projectPath = command.positionals[1];
|
|
147
|
+
if (projectPath && !stringFlag(command.flags.file) && !stringFlag(command.flags["source-code"]) && !stringFlag(command.flags.bundle)) {
|
|
148
|
+
const absoluteProjectPath = resolve(cwd, projectPath);
|
|
149
|
+
const snapshot = await collectFunctionProject(absoluteProjectPath);
|
|
150
|
+
const bundled = await bundleSnapshot(snapshot);
|
|
151
|
+
const entrypoint = snapshot.files.find((file) => file.path === snapshot.entrypoint);
|
|
152
|
+
if (!entrypoint)
|
|
153
|
+
throw new Error(`Missing function entrypoint: ${snapshot.entrypoint}`);
|
|
154
|
+
return {
|
|
155
|
+
value: await client.request("POST", functionPath(appPath, name, "draft-invoke"), {
|
|
156
|
+
source_code: entrypoint.content,
|
|
157
|
+
bundled_code: bundled.code,
|
|
158
|
+
body
|
|
159
|
+
})
|
|
160
|
+
};
|
|
161
|
+
}
|
|
66
162
|
return { value: await client.request("POST", functionPath(appPath, name, "draft-invoke"), { source_code: sourceInput(command, cwd), body }) };
|
|
67
163
|
}
|
|
68
164
|
if (action === "delete") {
|
|
@@ -77,6 +173,15 @@ export async function runFunctionCommand(command, client, config, cwd) {
|
|
|
77
173
|
}
|
|
78
174
|
throw new Error(`Unknown functions command: ${action ?? ""}`);
|
|
79
175
|
}
|
|
176
|
+
async function bundleSnapshot(snapshot) {
|
|
177
|
+
const workspaceRoot = await mkdtemp(join(tmpdir(), "vwork-cli-function-bundle-"));
|
|
178
|
+
try {
|
|
179
|
+
return await bundleFunctionProject({ snapshot, workspaceRoot, installDependencies: true });
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await rm(workspaceRoot, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
80
185
|
async function runPreviewCommand(command, client, appPath) {
|
|
81
186
|
const action = command.path[2];
|
|
82
187
|
const name = requiredPosition(command, 0, "function name");
|
|
@@ -157,6 +262,18 @@ async function runFunctionQueueBindingsCommand(command, client, appPath) {
|
|
|
157
262
|
}
|
|
158
263
|
throw new Error(`Unknown functions queue-bindings command: ${action ?? ""}`);
|
|
159
264
|
}
|
|
265
|
+
async function runFunctionFirebaseAccessCommand(command, client, appPath) {
|
|
266
|
+
const action = command.path[2];
|
|
267
|
+
const name = requiredPosition(command, 0, "function name");
|
|
268
|
+
const basePath = functionPath(appPath, name, "auth/firebase");
|
|
269
|
+
if (action === "get") {
|
|
270
|
+
return { value: await client.request("GET", basePath) };
|
|
271
|
+
}
|
|
272
|
+
if (action === "enable" || action === "disable") {
|
|
273
|
+
return { value: await client.request("PUT", basePath, { enabled: action === "enable" }) };
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`Unknown functions firebase-access command: ${action ?? ""}`);
|
|
276
|
+
}
|
|
160
277
|
async function resolveApp(command, client, config) {
|
|
161
278
|
const target = stringFlag(command.flags.app) ?? config.appId;
|
|
162
279
|
if (!target)
|
|
@@ -166,6 +283,67 @@ async function resolveApp(command, client, config) {
|
|
|
166
283
|
function functionPath(appPath, name, suffix) {
|
|
167
284
|
return `${appPath}/functions/${encodeURIComponent(name)}/${suffix}`;
|
|
168
285
|
}
|
|
286
|
+
async function readRemoteWorkingTreeFiles(client, appPath, functionName, tree) {
|
|
287
|
+
const files = [];
|
|
288
|
+
for (const file of tree.files) {
|
|
289
|
+
const query = new URLSearchParams({ path: file.path });
|
|
290
|
+
files.push(await client.request("GET", `${functionPath(appPath, functionName, "working-tree/files")}?${query.toString()}`));
|
|
291
|
+
}
|
|
292
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
293
|
+
return files;
|
|
294
|
+
}
|
|
295
|
+
function assertWritablePullTarget(outputDir, overwrite) {
|
|
296
|
+
if (!existsSync(outputDir)) {
|
|
297
|
+
mkdirSync(outputDir, { recursive: true });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!overwrite && readdirSync(outputDir).length > 0) {
|
|
301
|
+
throw new Error(`Output directory is not empty: ${outputDir}. Pass --yes to overwrite matching files.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function resolveSourceOutputPath(root, path) {
|
|
305
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\/+/, "");
|
|
306
|
+
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || /^[a-zA-Z]:/.test(path)) {
|
|
307
|
+
throw new Error(`Invalid source path from server: ${path}`);
|
|
308
|
+
}
|
|
309
|
+
return resolve(root, normalized);
|
|
310
|
+
}
|
|
311
|
+
function functionProjectStatus(projectRoot, localFiles, remoteFiles, remoteState) {
|
|
312
|
+
const localByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
313
|
+
const remoteByPath = new Map(remoteFiles.map((file) => [file.path, file]));
|
|
314
|
+
const changed = [];
|
|
315
|
+
const missingLocal = [];
|
|
316
|
+
const extraLocal = [];
|
|
317
|
+
for (const remote of remoteFiles) {
|
|
318
|
+
const local = localByPath.get(remote.path);
|
|
319
|
+
if (!local) {
|
|
320
|
+
missingLocal.push(remote.path);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (sha256(local.content) !== sha256(remote.content)) {
|
|
324
|
+
changed.push(remote.path);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const local of localFiles) {
|
|
328
|
+
if (!remoteByPath.has(local.path)) {
|
|
329
|
+
extraLocal.push(local.path);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
path: projectRoot,
|
|
334
|
+
base_commit_id: remoteState.baseCommitId,
|
|
335
|
+
remote_dirty: remoteState.dirty,
|
|
336
|
+
local_files: localFiles.length,
|
|
337
|
+
remote_files: remoteFiles.length,
|
|
338
|
+
changed,
|
|
339
|
+
missing_local: missingLocal,
|
|
340
|
+
extra_local: extraLocal,
|
|
341
|
+
clean: changed.length === 0 && missingLocal.length === 0 && extraLocal.length === 0
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function sha256(value) {
|
|
345
|
+
return createHash("sha256").update(value).digest("hex");
|
|
346
|
+
}
|
|
169
347
|
function sourceInput(command, cwd) {
|
|
170
348
|
const inline = stringFlag(command.flags["source-code"]);
|
|
171
349
|
if (inline)
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/oclif-command.js
CHANGED
|
@@ -81,6 +81,7 @@ export class VWorkCliCommand extends Command {
|
|
|
81
81
|
yes: Flags.boolean({ description: "Confirm the operation" }),
|
|
82
82
|
verbose: Flags.boolean({ description: "Enable verbose output" }),
|
|
83
83
|
"no-auth": Flags.boolean({ description: "Disable function auth requirement" }),
|
|
84
|
+
project: Flags.boolean({ description: "Generate multi-file function project source" }),
|
|
84
85
|
repair: Flags.boolean({ description: "Enable codegen repair attempts" }),
|
|
85
86
|
"draft-invoke-body": Flags.string({ description: "Draft invocation request body JSON object for codegen validation" }),
|
|
86
87
|
"draft-invoke-bindings": Flags.string({ description: "Draft invocation binding overrides JSON object for codegen validation" }),
|
|
@@ -194,7 +195,7 @@ function parsedFlags(flags) {
|
|
|
194
195
|
}
|
|
195
196
|
async function dispatchCommand(command, client, config, cwd) {
|
|
196
197
|
if (command.path[0] === "apps")
|
|
197
|
-
return runAppCommand(command, client);
|
|
198
|
+
return runAppCommand(command, client, cwd);
|
|
198
199
|
if (command.path[0] === "codegen")
|
|
199
200
|
return runCodegenCommand(command, client, config);
|
|
200
201
|
if (command.path[0] === "db")
|
|
@@ -224,12 +225,18 @@ function normalizeApiBaseUrl(value) {
|
|
|
224
225
|
return value.replace(/\/+$/, "");
|
|
225
226
|
}
|
|
226
227
|
function maxPathLength(path) {
|
|
228
|
+
if (path[0] === "apps" && path[1] === "firebase")
|
|
229
|
+
return 3;
|
|
227
230
|
if (path[0] === "db")
|
|
228
231
|
return 3;
|
|
229
232
|
if (path[0] === "codegen")
|
|
230
233
|
return 3;
|
|
231
234
|
if (path[0] === "kv")
|
|
232
235
|
return 3;
|
|
236
|
+
if (path[0] === "functions" && path[1] === "firebase-access")
|
|
237
|
+
return 3;
|
|
238
|
+
if (path[0] === "functions" && path[1] === "bundle")
|
|
239
|
+
return 2;
|
|
233
240
|
if (path[0] === "functions" && path[1] === "previews")
|
|
234
241
|
return 3;
|
|
235
242
|
if (path[0] === "functions" && path[1] === "routes")
|
|
@@ -245,11 +252,11 @@ function helpText() {
|
|
|
245
252
|
"Usage: vwork <resource> <command> [flags]",
|
|
246
253
|
"",
|
|
247
254
|
"Resources:",
|
|
248
|
-
" apps list, inspect, create, delete, bind-data-source",
|
|
255
|
+
" apps list, inspect, create, delete, bind-data-source, firebase",
|
|
249
256
|
" db tables list, sql exec, sql preview, rows select|insert|update|delete",
|
|
250
257
|
" migrations list, runs, create, up, down, delete",
|
|
251
|
-
" functions list, inspect, create, deploy, invoke, artifacts, artifact, publish, rollback, metrics, previews, validate",
|
|
252
|
-
" codegen mobile from-schema, function generate, runs list|download|verify, skill install-curl",
|
|
258
|
+
" functions list, inspect, create, deploy, bundle, invoke, artifacts, artifact, publish, rollback, metrics, previews, firebase-access, validate",
|
|
259
|
+
" codegen mobile from-schema, function generate, runs list|download|verify|stop, skill install-curl",
|
|
253
260
|
" secrets list",
|
|
254
261
|
" kv namespaces list|create, entries list, stats, metrics",
|
|
255
262
|
" queues list, create, update, delete",
|
|
@@ -272,7 +279,10 @@ function resourceHelpText(resource) {
|
|
|
272
279
|
"inspect <app>",
|
|
273
280
|
"create --name <name> --slug <slug> [--host <host>] --data-source <id>",
|
|
274
281
|
"delete <app>",
|
|
275
|
-
"bind-data-source <app> --data-source <id>"
|
|
282
|
+
"bind-data-source <app> --data-source <id>",
|
|
283
|
+
"firebase get <app>",
|
|
284
|
+
"firebase set <app> --file <service-account.json>",
|
|
285
|
+
"firebase delete <app>"
|
|
276
286
|
],
|
|
277
287
|
db: [
|
|
278
288
|
"tables list --app <app>",
|
|
@@ -294,8 +304,12 @@ function resourceHelpText(resource) {
|
|
|
294
304
|
functions: [
|
|
295
305
|
"list --app <app>",
|
|
296
306
|
"inspect <name> --app <app>",
|
|
307
|
+
"pull <name> --app <app> --out <dir>",
|
|
308
|
+
"status <name> <path> --app <app>",
|
|
297
309
|
"create <name> --app <app> --file <path>",
|
|
310
|
+
"deploy <name> <path> --app <app>",
|
|
298
311
|
"deploy <name> --app <app> --bundle <path>",
|
|
312
|
+
"bundle <project-dir> --out <dist/index.js>",
|
|
299
313
|
"invoke <name> --app <app> --file <path>",
|
|
300
314
|
"artifact <name> --app <app> --file <path>",
|
|
301
315
|
"artifacts <name> --app <app>",
|
|
@@ -303,18 +317,22 @@ function resourceHelpText(resource) {
|
|
|
303
317
|
"rollback <name> --app <app> --artifact <id>",
|
|
304
318
|
"metrics <name> --app <app>",
|
|
305
319
|
"previews create <name> --app <app> --artifact <id>",
|
|
320
|
+
"firebase-access get <name> --app <app>",
|
|
321
|
+
"firebase-access enable <name> --app <app>",
|
|
322
|
+
"firebase-access disable <name> --app <app>",
|
|
306
323
|
"queue-bindings list <name> --app <app>",
|
|
307
324
|
"queue-bindings replace <name> --app <app> --queue <queue_id:mode>",
|
|
308
325
|
"validate --file <path>"
|
|
309
326
|
],
|
|
310
327
|
codegen: [
|
|
311
328
|
"mobile from-schema --app <app> [--schema <schema>] [--table <table>] [--prompt <text>]",
|
|
312
|
-
"function generate --app <app> --function <name> --prompt <text> [--source-code <code>] [--binding <name>...] [--repair] [--draft-invoke-body <json>] [--draft-invoke-bindings <json>]",
|
|
329
|
+
"function generate --app <app> --function <name> --prompt <text> [--project] [--source-code <code>] [--binding <name>...] [--repair] [--draft-invoke-body <json>] [--draft-invoke-bindings <json>]",
|
|
313
330
|
"skill install-curl [--base-url <raw-base-url>]",
|
|
314
331
|
"mcp config-snippet [--workspace <repo-path>] [--api-url <url>] [--app <app>] [--schema <schema>]",
|
|
315
332
|
"runs list --app <app>",
|
|
316
333
|
"runs download --app <app> --run <id> --out <zip>",
|
|
317
|
-
"runs verify --app <app> --run <id>"
|
|
334
|
+
"runs verify --app <app> --run <id>",
|
|
335
|
+
"runs stop --app <app> --run <id>"
|
|
318
336
|
],
|
|
319
337
|
secrets: [
|
|
320
338
|
"list --app <app>"
|
package/package.json
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vwork/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"files": [
|
|
6
|
-
"dist/*.js",
|
|
7
|
-
"dist/auth/*.js",
|
|
8
|
-
"package.json"
|
|
9
|
-
],
|
|
10
|
-
"publishConfig": {
|
|
11
|
-
"registry": "https://registry.npmjs.org/"
|
|
12
|
-
},
|
|
13
|
-
"bin": {
|
|
14
|
-
"vwork": "dist/index.js"
|
|
15
|
-
},
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@vwork/cli",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist/*.js",
|
|
7
|
+
"dist/auth/*.js",
|
|
8
|
+
"package.json"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org/"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"vwork": "dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@oclif/core": "^4.11.7",
|
|
18
|
+
"@vwork/platform-client": "^0.1.0",
|
|
19
|
+
"@vwork/function-bundler": "0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.10.2",
|
|
23
|
+
"tsx": "^4.19.2",
|
|
24
|
+
"typescript": "^5.7.2"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"test": "pnpm --filter @vwork/function-bundler build && pnpm --filter @vwork/platform-client build && tsx --test src/test/*.test.ts",
|
|
30
|
+
"typecheck": "pnpm --filter @vwork/function-bundler build && pnpm --filter @vwork/platform-client build && tsc -p tsconfig.json --noEmit"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/client.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
}
|