@vwork/cli 0.1.6 → 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 +8 -2
- package/dist/functions.js +180 -2
- package/dist/index.js +0 -0
- package/dist/oclif-command.js +25 -12
- package/dist/runtime.js +3 -9
- 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
|
-
const COMMAND_ROOTS = new Set(["apps", "db", "migrations", "functions", "codegen", "secrets", "kv", "queues", "
|
|
2
|
-
const BOOLEAN_FLAGS = new Set(["yes", "verbose", "help", "no-auth", "repair"]);
|
|
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", "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,15 +252,14 @@ 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",
|
|
256
|
-
" runtime latest, publish",
|
|
257
263
|
" observability access-logs",
|
|
258
264
|
" login authenticate with VWork",
|
|
259
265
|
" whoami show the authenticated VWork user",
|
|
@@ -273,7 +279,10 @@ function resourceHelpText(resource) {
|
|
|
273
279
|
"inspect <app>",
|
|
274
280
|
"create --name <name> --slug <slug> [--host <host>] --data-source <id>",
|
|
275
281
|
"delete <app>",
|
|
276
|
-
"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>"
|
|
277
286
|
],
|
|
278
287
|
db: [
|
|
279
288
|
"tables list --app <app>",
|
|
@@ -295,8 +304,12 @@ function resourceHelpText(resource) {
|
|
|
295
304
|
functions: [
|
|
296
305
|
"list --app <app>",
|
|
297
306
|
"inspect <name> --app <app>",
|
|
307
|
+
"pull <name> --app <app> --out <dir>",
|
|
308
|
+
"status <name> <path> --app <app>",
|
|
298
309
|
"create <name> --app <app> --file <path>",
|
|
310
|
+
"deploy <name> <path> --app <app>",
|
|
299
311
|
"deploy <name> --app <app> --bundle <path>",
|
|
312
|
+
"bundle <project-dir> --out <dist/index.js>",
|
|
300
313
|
"invoke <name> --app <app> --file <path>",
|
|
301
314
|
"artifact <name> --app <app> --file <path>",
|
|
302
315
|
"artifacts <name> --app <app>",
|
|
@@ -304,18 +317,22 @@ function resourceHelpText(resource) {
|
|
|
304
317
|
"rollback <name> --app <app> --artifact <id>",
|
|
305
318
|
"metrics <name> --app <app>",
|
|
306
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>",
|
|
307
323
|
"queue-bindings list <name> --app <app>",
|
|
308
324
|
"queue-bindings replace <name> --app <app> --queue <queue_id:mode>",
|
|
309
325
|
"validate --file <path>"
|
|
310
326
|
],
|
|
311
327
|
codegen: [
|
|
312
328
|
"mobile from-schema --app <app> [--schema <schema>] [--table <table>] [--prompt <text>]",
|
|
313
|
-
"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>]",
|
|
314
330
|
"skill install-curl [--base-url <raw-base-url>]",
|
|
315
331
|
"mcp config-snippet [--workspace <repo-path>] [--api-url <url>] [--app <app>] [--schema <schema>]",
|
|
316
332
|
"runs list --app <app>",
|
|
317
333
|
"runs download --app <app> --run <id> --out <zip>",
|
|
318
|
-
"runs verify --app <app> --run <id>"
|
|
334
|
+
"runs verify --app <app> --run <id>",
|
|
335
|
+
"runs stop --app <app> --run <id>"
|
|
319
336
|
],
|
|
320
337
|
secrets: [
|
|
321
338
|
"list --app <app>"
|
|
@@ -333,10 +350,6 @@ function resourceHelpText(resource) {
|
|
|
333
350
|
"update <queue-id> --app <app> [--batch-size <n>] [--dead-letter-enabled true|false]",
|
|
334
351
|
"delete <queue-id> --app <app>"
|
|
335
352
|
],
|
|
336
|
-
runtime: [
|
|
337
|
-
"latest",
|
|
338
|
-
"publish"
|
|
339
|
-
],
|
|
340
353
|
observability: [
|
|
341
354
|
"access-logs [--source <source>] [--app <app>] [--limit <n>]"
|
|
342
355
|
],
|
package/dist/runtime.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
export async function runRuntimeCommand(command, client) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return { value: body.runtime_config };
|
|
6
|
-
}
|
|
7
|
-
if (action === "publish") {
|
|
8
|
-
return { value: await client.request("POST", "/runtime-config/publish") };
|
|
9
|
-
}
|
|
10
|
-
throw new Error(`Unknown runtime command: ${action ?? ""}`);
|
|
2
|
+
void command;
|
|
3
|
+
void client;
|
|
4
|
+
throw new Error("Runtime commands are not exposed in VWork CLI. Use function and observability commands for user-facing runtime data.");
|
|
11
5
|
}
|
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
|
-
}
|