vmlive 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/package.json +2 -2
- package/src/cli.js +160 -42
package/README.md
CHANGED
|
@@ -20,12 +20,15 @@ Authenticates the CLI with your vm.live account via PKCE OAuth. Saves the access
|
|
|
20
20
|
### `npx vmlive init`
|
|
21
21
|
Scaffolds a new project in the current directory.
|
|
22
22
|
- Prompts for Workspace and Project selection.
|
|
23
|
-
- Generates `vm.json` (configuration manifest) and `
|
|
23
|
+
- Generates `vm.json` (configuration manifest) and dynamically scopes your first endpoint to `src/`.
|
|
24
24
|
- Installs `@vmlive/types` and configures TypeScript.
|
|
25
25
|
|
|
26
|
+
### `npx vmlive add`
|
|
27
|
+
Interactively scaffolds additional serverless endpoints natively into your `src/` directory, immediately mapping them into your `vm.json` so the local proxy and production Gatekeeper seamlessly route to your independent microservices.
|
|
28
|
+
|
|
26
29
|
### `npx vmlive dev`
|
|
27
30
|
Starts the local emulation environment.
|
|
28
|
-
-
|
|
31
|
+
- Iteratively hosts your entire multi-function footprint locally via predictable slugs: `http://<workspace_id>-<project_id>-<function_name>.localhost:8787`.
|
|
29
32
|
- Provides local proxy bindings for your SQL Database (`env.DB`), Global KV (`env.KV`), and Object Storage (`env.BUCKET`) using the `.vmlive/` directory for persistent storage.
|
|
30
33
|
- Supports hot-reloading for `.js` and `.ts` files.
|
|
31
34
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import os from 'os';
|
|
|
9
9
|
import crypto from 'crypto';
|
|
10
10
|
import { exec } from 'child_process';
|
|
11
11
|
import { Miniflare } from 'miniflare';
|
|
12
|
-
import { select, input } from '@inquirer/prompts';
|
|
12
|
+
import { select, input, confirm } from '@inquirer/prompts';
|
|
13
13
|
import { generateShim } from './string-shim.js';
|
|
14
14
|
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -19,7 +19,6 @@ const __dirname = path.dirname(__filename);
|
|
|
19
19
|
const WORK_DIR = path.resolve('node_modules', '.cache', 'vmlive');
|
|
20
20
|
const DATA_DIR = path.resolve('.vmlive');
|
|
21
21
|
const CONFIG_PATH = path.resolve('vm.json');
|
|
22
|
-
const API_FILE = path.resolve('index.ts');
|
|
23
22
|
|
|
24
23
|
const buildResourcesConfig = () => {
|
|
25
24
|
return {
|
|
@@ -29,7 +28,7 @@ const buildResourcesConfig = () => {
|
|
|
29
28
|
};
|
|
30
29
|
};
|
|
31
30
|
|
|
32
|
-
const buildProxyDispatcher = (functions) => ({
|
|
31
|
+
const buildProxyDispatcher = (functions, workspaceId, projectId) => ({
|
|
33
32
|
name: "vm-gateway-proxy",
|
|
34
33
|
modules: true,
|
|
35
34
|
script: `
|
|
@@ -39,23 +38,19 @@ const buildProxyDispatcher = (functions) => ({
|
|
|
39
38
|
const hostHeader = request.headers.get("Host") || url.hostname;
|
|
40
39
|
let targetName = hostHeader.split('.')[0];
|
|
41
40
|
|
|
42
|
-
if (targetName === '127' || targetName === 'localhost') {
|
|
43
|
-
targetName = 'api';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
41
|
if (targetName && env[targetName]) {
|
|
47
42
|
return env[targetName].fetch(request);
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
return new Response(
|
|
51
|
-
\`Function Not Found: \${targetName}.\\n\\
|
|
46
|
+
\`Function Not Found: \${targetName}.\\n\\nExpected format: http://${workspaceId}-${projectId}-<function_name>.localhost\`,
|
|
52
47
|
{ status: 404 }
|
|
53
48
|
);
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
51
|
`,
|
|
57
52
|
serviceBindings: functions.reduce((acc, fn) => {
|
|
58
|
-
acc[fn.name] = fn.name;
|
|
53
|
+
acc[`${workspaceId}-${projectId}-${fn.name}`] = fn.name;
|
|
59
54
|
return acc;
|
|
60
55
|
}, {})
|
|
61
56
|
});
|
|
@@ -81,7 +76,7 @@ const runInit = async () => {
|
|
|
81
76
|
const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'http://localhost:8787';
|
|
82
77
|
|
|
83
78
|
// 2. Extract Workspace Architecture
|
|
84
|
-
console.log('\x1b[
|
|
79
|
+
console.log('\x1b[36mFetching workspaces...\x1b[0m');
|
|
85
80
|
const wsRes = await fetch(`${GATEKEEPER_URL}/api/workspaces`, {
|
|
86
81
|
headers: { 'Authorization': `Bearer ${jwtToken}` }
|
|
87
82
|
});
|
|
@@ -95,18 +90,64 @@ const runInit = async () => {
|
|
|
95
90
|
process.exit(1);
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
|
|
99
|
-
if (!Array.isArray(workspaces)
|
|
100
|
-
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m
|
|
93
|
+
let workspaces = await wsRes.json();
|
|
94
|
+
if (!Array.isArray(workspaces)) {
|
|
95
|
+
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve workspaces.');
|
|
101
96
|
process.exit(1);
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
// 3. Interactive Target Lock
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
const workspaceChoices = workspaces.map(w => ({ name: w.name, value: w.slug }));
|
|
101
|
+
workspaceChoices.push({ name: '+ Create New Workspace', value: '__CREATE_WORKSPACE__' });
|
|
102
|
+
|
|
103
|
+
let workspaceSlug = await select({
|
|
104
|
+
message: 'Which workspace would you like to use?',
|
|
105
|
+
choices: workspaceChoices
|
|
108
106
|
});
|
|
109
107
|
|
|
108
|
+
if (workspaceSlug === '__CREATE_WORKSPACE__') {
|
|
109
|
+
const wsName = await input({ message: 'What do you want to name your new workspace?' });
|
|
110
|
+
console.log('\x1b[36mCreating workspace...\x1b[0m');
|
|
111
|
+
|
|
112
|
+
const wsCreateRes = await fetch(`${GATEKEEPER_URL}/api/workspaces`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({ name: wsName })
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!wsCreateRes.ok) {
|
|
119
|
+
console.error('\x1b[31m❌ Creation Failed:\x1b[0m', await wsCreateRes.text());
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const wsData = await wsCreateRes.json();
|
|
123
|
+
workspaceSlug = wsData.workspace.slug;
|
|
124
|
+
console.log(`\x1b[32m✔ Workspace created.\x1b[0m\n`);
|
|
125
|
+
|
|
126
|
+
const wantsVerify = await confirm({
|
|
127
|
+
message: 'Anti-abuse: Would you like to securely verify your identity with Stripe? (You will not be charged)',
|
|
128
|
+
default: true
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (wantsVerify) {
|
|
132
|
+
console.log('\x1b[36mGenerating verification link...\x1b[0m');
|
|
133
|
+
const stripeRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/stripe/setup`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ return_url: 'https://vm.live/dashboard' })
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (stripeRes.ok) {
|
|
140
|
+
const stripeData = await stripeRes.json();
|
|
141
|
+
console.log(`\n\x1b[34m${stripeData.url}\x1b[0m\n`);
|
|
142
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
143
|
+
exec(`${openCmd} "${stripeData.url}"`);
|
|
144
|
+
await input({ message: 'Press Enter once verified to continue:' });
|
|
145
|
+
} else {
|
|
146
|
+
console.error('\x1b[31m❌ Failed to generate Stripe link:\x1b[0m', await stripeRes.text());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
110
151
|
// 4. Extract Project Containers
|
|
111
152
|
const projRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/projects`, {
|
|
112
153
|
headers: { 'Authorization': `Bearer ${jwtToken}` }
|
|
@@ -117,25 +158,52 @@ const runInit = async () => {
|
|
|
117
158
|
process.exit(1);
|
|
118
159
|
}
|
|
119
160
|
|
|
120
|
-
|
|
121
|
-
if (!Array.isArray(projects)
|
|
122
|
-
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m
|
|
161
|
+
let projects = await projRes.json();
|
|
162
|
+
if (!Array.isArray(projects)) {
|
|
163
|
+
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve projects.');
|
|
123
164
|
process.exit(1);
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
167
|
+
const projectChoices = projects.map(p => ({ name: p.name, value: p.slug }));
|
|
168
|
+
projectChoices.push({ name: '+ Create New Project', value: '__CREATE_PROJECT__' });
|
|
169
|
+
|
|
170
|
+
let projectSlug = await select({
|
|
171
|
+
message: 'Which project would you like to use?',
|
|
172
|
+
choices: projectChoices
|
|
129
173
|
});
|
|
130
174
|
|
|
175
|
+
if (projectSlug === '__CREATE_PROJECT__') {
|
|
176
|
+
const projName = await input({ message: 'What do you want to name your new project?' });
|
|
177
|
+
console.log('\x1b[36mCreating project...\x1b[0m');
|
|
178
|
+
|
|
179
|
+
const projCreateRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/projects`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({ name: projName })
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!projCreateRes.ok) {
|
|
186
|
+
console.error('\x1b[31m❌ Creation Failed:\x1b[0m', await projCreateRes.text());
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
const projData = await projCreateRes.json();
|
|
190
|
+
projectSlug = projData.project.slug;
|
|
191
|
+
console.log(`\x1b[32m✔ Project created.\x1b[0m\n`);
|
|
192
|
+
}
|
|
193
|
+
|
|
131
194
|
const functionNameRaw = await input({
|
|
132
|
-
message: 'What do you want to
|
|
195
|
+
message: 'What do you want to name this function?',
|
|
133
196
|
default: 'api'
|
|
134
197
|
});
|
|
135
198
|
const functionName = functionNameRaw.trim().toLowerCase().replace(/[^a-z0-9-]/g, '') || 'api';
|
|
136
199
|
|
|
200
|
+
if (!fs.existsSync(path.resolve('src'))) {
|
|
201
|
+
fs.mkdirSync(path.resolve('src'), { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
const funcFile = path.resolve('src', `${functionName}.ts`);
|
|
204
|
+
|
|
137
205
|
// 5. Instantiation
|
|
138
|
-
if (!fs.existsSync(
|
|
206
|
+
if (!fs.existsSync(funcFile)) {
|
|
139
207
|
const defaultApiTs = `import type { Env, Context } from '@vmlive/types';
|
|
140
208
|
export default {
|
|
141
209
|
async fetch(request: Request, env: Env, ctx: Context) {
|
|
@@ -143,7 +211,7 @@ export default {
|
|
|
143
211
|
}
|
|
144
212
|
};
|
|
145
213
|
`;
|
|
146
|
-
fs.writeFileSync(
|
|
214
|
+
fs.writeFileSync(funcFile, defaultApiTs);
|
|
147
215
|
}
|
|
148
216
|
|
|
149
217
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
@@ -151,7 +219,7 @@ export default {
|
|
|
151
219
|
"workspaceId": "${workspaceSlug}",
|
|
152
220
|
"projectId": "${projectSlug}",
|
|
153
221
|
"functions": [
|
|
154
|
-
{ "name": "${functionName}", "entry": "
|
|
222
|
+
{ "name": "${functionName}", "entry": "src/${functionName}.ts" }
|
|
155
223
|
]
|
|
156
224
|
}
|
|
157
225
|
`;
|
|
@@ -228,7 +296,7 @@ export default {
|
|
|
228
296
|
} catch(e) {}
|
|
229
297
|
}
|
|
230
298
|
|
|
231
|
-
console.log('\x1b[
|
|
299
|
+
console.log('\x1b[36mInstalling dependencies...\x1b[0m');
|
|
232
300
|
await new Promise((resolve) => {
|
|
233
301
|
exec('npm install', (err) => {
|
|
234
302
|
if (err) console.error('\x1b[33m⚠️ Warning: npm install failed. You may need to run it manually to resolve TS errors.\x1b[0m');
|
|
@@ -236,8 +304,53 @@ export default {
|
|
|
236
304
|
});
|
|
237
305
|
});
|
|
238
306
|
|
|
239
|
-
console.log('\x1b[
|
|
240
|
-
console.log('
|
|
307
|
+
console.log('\x1b[32mProject created successfully.\x1b[0m');
|
|
308
|
+
console.log('Run \x1b[36mnpx vmlive dev\x1b[0m to start the local environment.');
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const runAdd = async () => {
|
|
312
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
313
|
+
console.error('\x1b[31m❌ Error:\x1b[0m Cannot add function. No vm.json found in this directory. Run `npx vmlive init` first.');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
318
|
+
|
|
319
|
+
const functionNameRaw = await input({
|
|
320
|
+
message: 'What do you want to name this new function?',
|
|
321
|
+
default: 'webhooks'
|
|
322
|
+
});
|
|
323
|
+
const functionName = functionNameRaw.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
324
|
+
if (!functionName) process.exit(1);
|
|
325
|
+
|
|
326
|
+
if (config.functions.some(f => f.name === functionName)) {
|
|
327
|
+
console.error(`\x1b[31m❌ Error:\x1b[0m Function '${functionName}' already exists in this project.`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!fs.existsSync(path.resolve('src'))) {
|
|
332
|
+
fs.mkdirSync(path.resolve('src'), { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const targetFile = path.resolve('src', `${functionName}.ts`);
|
|
336
|
+
if (!fs.existsSync(targetFile)) {
|
|
337
|
+
const defaultApiTs = `import type { Env, Context } from '@vmlive/types';
|
|
338
|
+
export default {
|
|
339
|
+
async fetch(request: Request, env: Env, ctx: Context) {
|
|
340
|
+
return new Response("Hello from ${functionName}!", { status: 200 });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
`;
|
|
344
|
+
fs.writeFileSync(targetFile, defaultApiTs);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
config.functions.push({
|
|
348
|
+
name: functionName,
|
|
349
|
+
entry: `src/${functionName}.ts`
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
353
|
+
console.log(`\x1b[32m✔ Function '${functionName}' securely scaffolded successfully!\x1b[0m`);
|
|
241
354
|
};
|
|
242
355
|
|
|
243
356
|
const runDev = async () => {
|
|
@@ -282,7 +395,7 @@ const runDev = async () => {
|
|
|
282
395
|
});
|
|
283
396
|
|
|
284
397
|
await builder.rebuild();
|
|
285
|
-
console.log('\x1b[
|
|
398
|
+
console.log('\x1b[32mPlatform initialized.\x1b[0m');
|
|
286
399
|
|
|
287
400
|
// Copy Shadow Worker to ephemeral .vm directory for Miniflare Sandbox Bounds
|
|
288
401
|
const shadowSource = fs.readFileSync(path.join(__dirname, 'shadow-dos.js'), 'utf-8');
|
|
@@ -291,7 +404,7 @@ const runDev = async () => {
|
|
|
291
404
|
const mfPort = process.env.PORT ? parseInt(process.env.PORT) : (config.port || 8787);
|
|
292
405
|
|
|
293
406
|
const miniflareWorkers = [
|
|
294
|
-
buildProxyDispatcher(config.functions),
|
|
407
|
+
buildProxyDispatcher(config.functions, workspaceId, projectId),
|
|
295
408
|
{
|
|
296
409
|
name: "vm-shadow-worker",
|
|
297
410
|
modules: true,
|
|
@@ -323,7 +436,10 @@ const runDev = async () => {
|
|
|
323
436
|
r2Persist: path.join(DATA_DIR, 'bucket')
|
|
324
437
|
});
|
|
325
438
|
await mf.ready;
|
|
326
|
-
console.log(
|
|
439
|
+
console.log('\\nLocal endpoints available:');
|
|
440
|
+
config.functions.forEach(fn => {
|
|
441
|
+
console.log(` http://${workspaceId}-${projectId}-${fn.name}.localhost:${mfPort}`);
|
|
442
|
+
});
|
|
327
443
|
|
|
328
444
|
setInterval(() => {
|
|
329
445
|
const memoryData = process.memoryUsage();
|
|
@@ -340,7 +456,7 @@ const runDev = async () => {
|
|
|
340
456
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
341
457
|
debounceTimeout = setTimeout(async () => {
|
|
342
458
|
try {
|
|
343
|
-
console.log(`\
|
|
459
|
+
console.log(`\nReloading ${filename}...`);
|
|
344
460
|
await builder.rebuild();
|
|
345
461
|
|
|
346
462
|
config.functions.forEach(fn => {
|
|
@@ -357,7 +473,7 @@ const runDev = async () => {
|
|
|
357
473
|
r2Persist: path.join(DATA_DIR, 'bucket')
|
|
358
474
|
});
|
|
359
475
|
await mf.ready;
|
|
360
|
-
console.log('\x1b[
|
|
476
|
+
console.log('\x1b[32mReloaded.\x1b[0m');
|
|
361
477
|
} catch (err) {
|
|
362
478
|
console.error('\x1b[31m❌ Engine Sync Crash:\x1b[0m', err.message);
|
|
363
479
|
}
|
|
@@ -366,7 +482,7 @@ const runDev = async () => {
|
|
|
366
482
|
});
|
|
367
483
|
|
|
368
484
|
process.on('SIGINT', async () => {
|
|
369
|
-
console.log('\
|
|
485
|
+
console.log('\nStopping...');
|
|
370
486
|
await builder.dispose();
|
|
371
487
|
await mf.dispose();
|
|
372
488
|
process.exit(0);
|
|
@@ -374,7 +490,7 @@ const runDev = async () => {
|
|
|
374
490
|
};
|
|
375
491
|
|
|
376
492
|
const runDeploy = async () => {
|
|
377
|
-
console.log('\x1b[
|
|
493
|
+
console.log('\x1b[36mDeploying...\x1b[0m');
|
|
378
494
|
|
|
379
495
|
const configPath = path.join(os.homedir(), '.vm-config.json');
|
|
380
496
|
let token = process.env.VM_API_TOKEN;
|
|
@@ -425,7 +541,7 @@ const runDeploy = async () => {
|
|
|
425
541
|
const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'http://localhost:8787';
|
|
426
542
|
const target = `${GATEKEEPER_URL}/api/${workspaceId}/projects/${projectId}/functions/${fn.name}/deploy`;
|
|
427
543
|
|
|
428
|
-
console.log(`\x1b[36m[POST]\x1b[0m Uploading
|
|
544
|
+
console.log(`\x1b[36m[POST]\x1b[0m Uploading...`);
|
|
429
545
|
|
|
430
546
|
const res = await fetch(target, {
|
|
431
547
|
method: 'POST',
|
|
@@ -443,12 +559,12 @@ const runDeploy = async () => {
|
|
|
443
559
|
}
|
|
444
560
|
|
|
445
561
|
const jsonRes = await res.json();
|
|
446
|
-
console.log(`\x1b[
|
|
562
|
+
console.log(`\x1b[32mDeployed to: ${jsonRes.public_url || 'Success!'}\x1b[0m`);
|
|
447
563
|
}
|
|
448
564
|
};
|
|
449
565
|
|
|
450
566
|
const runLogin = async () => {
|
|
451
|
-
console.log('\x1b[
|
|
567
|
+
console.log('\x1b[36mAuthenticating...\x1b[0m');
|
|
452
568
|
|
|
453
569
|
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
454
570
|
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
@@ -467,7 +583,7 @@ const runLogin = async () => {
|
|
|
467
583
|
|
|
468
584
|
try {
|
|
469
585
|
// Exchange
|
|
470
|
-
console.log('
|
|
586
|
+
console.log('Exchanging authorization code...');
|
|
471
587
|
const tokenRes = await fetch(`${GATEKEEPER_URL}/api/auth/cli/exchange`, {
|
|
472
588
|
method: 'POST',
|
|
473
589
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -493,7 +609,7 @@ const runLogin = async () => {
|
|
|
493
609
|
</div>
|
|
494
610
|
</body></html>
|
|
495
611
|
`);
|
|
496
|
-
console.log('\x1b[32m
|
|
612
|
+
console.log('\x1b[32m✔ Successfully authenticated.\x1b[0m');
|
|
497
613
|
|
|
498
614
|
setTimeout(() => {
|
|
499
615
|
server.close();
|
|
@@ -527,6 +643,8 @@ const main = async () => {
|
|
|
527
643
|
const command = process.argv[2];
|
|
528
644
|
if (command === 'init') {
|
|
529
645
|
runInit();
|
|
646
|
+
} else if (command === 'add') {
|
|
647
|
+
await runAdd();
|
|
530
648
|
} else if (command === 'dev' || !command) {
|
|
531
649
|
await runDev();
|
|
532
650
|
} else if (command === 'login') {
|
|
@@ -535,7 +653,7 @@ const main = async () => {
|
|
|
535
653
|
await runDeploy();
|
|
536
654
|
} else {
|
|
537
655
|
console.error(`\x1b[31m❌ Unknown command: ${command}\x1b[0m`);
|
|
538
|
-
console.log('Usage: vm init | vm dev | vm login | vm deploy');
|
|
656
|
+
console.log('Usage: vm init | vm add | vm dev | vm login | vm deploy');
|
|
539
657
|
process.exit(1);
|
|
540
658
|
}
|
|
541
659
|
};
|