vmlive 1.0.9 → 1.0.11

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +210 -190
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vmlive",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Local development VM for custom Serverless PaaS",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -22,7 +22,7 @@ const CONFIG_PATH = path.resolve('vm.json');
22
22
 
23
23
  const buildResourcesConfig = () => {
24
24
  return {
25
- d1Databases: ["DB"],
25
+ d1Databases: ["DB"],
26
26
  kvNamespaces: { "__RAW_KV": "local_store" },
27
27
  r2Buckets: ["R2"]
28
28
  };
@@ -89,12 +89,12 @@ const runInit = async () => {
89
89
  const configPath = path.join(os.homedir(), '.vm-config.json');
90
90
  let jwtToken = process.env.VM_API_TOKEN;
91
91
  if (!jwtToken && fs.existsSync(configPath)) {
92
- jwtToken = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
92
+ jwtToken = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
93
93
  }
94
94
 
95
95
  if (!jwtToken) {
96
- console.error('\x1b[31m❌ Unauthorized.\x1b[0m Please run \x1b[36mnpx vmlive login\x1b[0m first to securely pair this CLI.');
97
- process.exit(1);
96
+ console.error('\x1b[31m❌ Unauthorized.\x1b[0m Please run \x1b[36mnpx vmlive login\x1b[0m first to securely pair this CLI.');
97
+ process.exit(1);
98
98
  }
99
99
 
100
100
  const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'https://api.vm.live';
@@ -102,22 +102,22 @@ const runInit = async () => {
102
102
  // 2. Extract Workspace Architecture
103
103
  console.log('\x1b[36mFetching workspaces...\x1b[0m');
104
104
  const wsRes = await fetch(`${GATEKEEPER_URL}/api/workspaces`, {
105
- headers: { 'Authorization': `Bearer ${jwtToken}` }
105
+ headers: { 'Authorization': `Bearer ${jwtToken}` }
106
106
  });
107
107
 
108
108
  if (!wsRes.ok) {
109
- if (wsRes.status === 401) {
110
- console.error('\x1b[31m❌ Session Expired:\x1b[0m Your authentication token has expired. Please run \x1b[36mnpx vmlive login\x1b[0m to securely re-authenticate.');
111
- } else {
112
- console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to securely retrieve workspace architecture from Production. HTTP ' + wsRes.status);
113
- }
114
- process.exit(1);
109
+ if (wsRes.status === 401) {
110
+ console.error('\x1b[31m❌ Session Expired:\x1b[0m Your authentication token has expired. Please run \x1b[36mnpx vmlive login\x1b[0m to securely re-authenticate.');
111
+ } else {
112
+ console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to securely retrieve workspace architecture from Production. HTTP ' + wsRes.status);
113
+ }
114
+ process.exit(1);
115
115
  }
116
116
 
117
117
  let workspaces = await wsRes.json();
118
118
  if (!Array.isArray(workspaces)) {
119
- console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve workspaces.');
120
- process.exit(1);
119
+ console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve workspaces.');
120
+ process.exit(1);
121
121
  }
122
122
 
123
123
  // 3. Interactive Target Lock
@@ -125,104 +125,104 @@ const runInit = async () => {
125
125
  workspaceChoices.push({ name: '+ Create New Workspace', value: '__CREATE_WORKSPACE__' });
126
126
 
127
127
  let workspaceSlug = await select({
128
- message: 'Which workspace would you like to use?',
129
- choices: workspaceChoices
128
+ message: 'Which workspace would you like to use?',
129
+ choices: workspaceChoices
130
130
  });
131
131
 
132
132
  if (workspaceSlug === '__CREATE_WORKSPACE__') {
133
- const wsName = await input({ message: 'What do you want to name your new workspace?' });
134
- console.log('\x1b[36mCreating workspace...\x1b[0m');
135
-
136
- const wsCreateRes = await fetch(`${GATEKEEPER_URL}/api/workspaces`, {
137
- method: 'POST',
138
- headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
139
- body: JSON.stringify({ name: wsName })
140
- });
141
-
142
- if (!wsCreateRes.ok) {
143
- console.error('\x1b[31m❌ Creation Failed:\x1b[0m', await wsCreateRes.text());
144
- process.exit(1);
145
- }
146
- const wsData = await wsCreateRes.json();
147
- workspaceSlug = wsData.workspace.slug;
148
- console.log(`\x1b[32m✔ Workspace created.\x1b[0m\n`);
149
-
150
- const wantsVerify = await confirm({
151
- message: 'Anti-abuse: Would you like to securely verify your identity with Stripe? (You will not be charged)',
152
- default: true
153
- });
154
-
155
- if (wantsVerify) {
156
- console.log('\x1b[36mGenerating verification link...\x1b[0m');
157
- const stripeRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/stripe/setup`, {
158
- method: 'POST',
159
- headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
160
- body: JSON.stringify({ return_url: 'https://vm.live/dashboard' })
161
- });
162
-
163
- if (stripeRes.ok) {
164
- const stripeData = await stripeRes.json();
165
- console.log(`\n\x1b]8;;${stripeData.url}\x1b\\\x1b[34m${stripeData.url}\x1b[0m\x1b]8;;\x1b\\\n`);
166
- const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
167
- exec(`${openCmd} "${stripeData.url}"`);
168
- await input({ message: 'Press Enter once verified to continue:' });
169
- } else {
170
- console.error('\x1b[31m❌ Failed to generate Stripe link:\x1b[0m', await stripeRes.text());
171
- }
172
- }
133
+ const wsName = await input({ message: 'What do you want to name your new workspace?' });
134
+ console.log('\x1b[36mCreating workspace...\x1b[0m');
135
+
136
+ const wsCreateRes = await fetch(`${GATEKEEPER_URL}/api/workspaces`, {
137
+ method: 'POST',
138
+ headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
139
+ body: JSON.stringify({ name: wsName })
140
+ });
141
+
142
+ if (!wsCreateRes.ok) {
143
+ console.error('\x1b[31m❌ Creation Failed:\x1b[0m', await wsCreateRes.text());
144
+ process.exit(1);
145
+ }
146
+ const wsData = await wsCreateRes.json();
147
+ workspaceSlug = wsData.workspace.slug;
148
+ console.log(`\x1b[32m✔ Workspace created.\x1b[0m\n`);
149
+
150
+ const wantsVerify = await confirm({
151
+ message: 'Anti-abuse: Would you like to securely verify your identity with Stripe? (You will not be charged)',
152
+ default: true
153
+ });
154
+
155
+ if (wantsVerify) {
156
+ console.log('\x1b[36mGenerating verification link...\x1b[0m');
157
+ const stripeRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/stripe/setup`, {
158
+ method: 'POST',
159
+ headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ return_url: 'https://vm.live/dashboard' })
161
+ });
162
+
163
+ if (stripeRes.ok) {
164
+ const stripeData = await stripeRes.json();
165
+ console.log(`\n\x1b]8;;${stripeData.url}\x1b\\\x1b[34m${stripeData.url}\x1b[0m\x1b]8;;\x1b\\\n`);
166
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
167
+ exec(`${openCmd} "${stripeData.url}"`);
168
+ await input({ message: 'Press Enter once verified to continue:' });
169
+ } else {
170
+ console.error('\x1b[31m❌ Failed to generate Stripe link:\x1b[0m', await stripeRes.text());
171
+ }
172
+ }
173
173
  }
174
174
 
175
175
  // 4. Extract Project Containers
176
176
  const projRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/projects`, {
177
- headers: { 'Authorization': `Bearer ${jwtToken}` }
177
+ headers: { 'Authorization': `Bearer ${jwtToken}` }
178
178
  });
179
179
 
180
180
  if (!projRes.ok) {
181
- console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to logically retrieve project constraints from Production.');
182
- process.exit(1);
181
+ console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to logically retrieve project constraints from Production.');
182
+ process.exit(1);
183
183
  }
184
184
 
185
185
  let projects = await projRes.json();
186
186
  if (!Array.isArray(projects)) {
187
- console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve projects.');
188
- process.exit(1);
187
+ console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve projects.');
188
+ process.exit(1);
189
189
  }
190
190
 
191
191
  const projectChoices = projects.map(p => ({ name: p.name, value: p.slug }));
192
192
  projectChoices.push({ name: '+ Create New Project', value: '__CREATE_PROJECT__' });
193
193
 
194
194
  let projectSlug = await select({
195
- message: 'Which project would you like to use?',
196
- choices: projectChoices
195
+ message: 'Which project would you like to use?',
196
+ choices: projectChoices
197
197
  });
198
198
 
199
199
  if (projectSlug === '__CREATE_PROJECT__') {
200
- const projName = await input({ message: 'What do you want to name your new project?' });
201
- console.log('\x1b[36mCreating project...\x1b[0m');
202
-
203
- const projCreateRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/projects`, {
204
- method: 'POST',
205
- headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
206
- body: JSON.stringify({ name: projName })
207
- });
208
-
209
- if (!projCreateRes.ok) {
210
- console.error('\x1b[31m❌ Creation Failed:\x1b[0m', await projCreateRes.text());
211
- process.exit(1);
212
- }
213
- const projData = await projCreateRes.json();
214
- projectSlug = projData.project.slug;
215
- console.log(`\x1b[32m✔ Project created.\x1b[0m\n`);
200
+ const projName = await input({ message: 'What do you want to name your new project?' });
201
+ console.log('\x1b[36mCreating project...\x1b[0m');
202
+
203
+ const projCreateRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/projects`, {
204
+ method: 'POST',
205
+ headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' },
206
+ body: JSON.stringify({ name: projName })
207
+ });
208
+
209
+ if (!projCreateRes.ok) {
210
+ console.error('\x1b[31m❌ Creation Failed:\x1b[0m', await projCreateRes.text());
211
+ process.exit(1);
212
+ }
213
+ const projData = await projCreateRes.json();
214
+ projectSlug = projData.project.slug;
215
+ console.log(`\x1b[32m✔ Project created.\x1b[0m\n`);
216
216
  }
217
217
 
218
218
  const functionNameRaw = await input({
219
- message: 'What do you want to name this function?',
220
- default: 'api'
219
+ message: 'What do you want to name this function?',
220
+ default: 'api'
221
221
  });
222
222
  const functionName = functionNameRaw.trim().toLowerCase().replace(/[^a-z0-9-]/g, '') || 'api';
223
223
 
224
224
  if (!fs.existsSync(path.resolve('src'))) {
225
- fs.mkdirSync(path.resolve('src'), { recursive: true });
225
+ fs.mkdirSync(path.resolve('src'), { recursive: true });
226
226
  }
227
227
  const funcFile = path.resolve('src', `${functionName}.ts`);
228
228
 
@@ -281,14 +281,14 @@ export default {
281
281
  fs.writeFileSync(pkgPath, defaultPackageJson);
282
282
  } else {
283
283
  try {
284
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
285
- pkg.devDependencies = pkg.devDependencies || {};
286
- if (!pkg.devDependencies['@vmlive/types']) {
287
- pkg.devDependencies['@vmlive/types'] = '*';
288
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
289
- }
290
- } catch(e) {
291
- // gracefully ignore malformed package.json
284
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
285
+ pkg.devDependencies = pkg.devDependencies || {};
286
+ if (!pkg.devDependencies['@vmlive/types']) {
287
+ pkg.devDependencies['@vmlive/types'] = '*';
288
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
289
+ }
290
+ } catch (e) {
291
+ // gracefully ignore malformed package.json
292
292
  }
293
293
  }
294
294
 
@@ -310,22 +310,22 @@ export default {
310
310
  fs.writeFileSync(tsConfigPath, defaultTsConfig);
311
311
  } else {
312
312
  try {
313
- const tsconfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf8'));
314
- tsconfig.compilerOptions = tsconfig.compilerOptions || {};
315
- tsconfig.compilerOptions.types = tsconfig.compilerOptions.types || [];
316
- if (!tsconfig.compilerOptions.types.includes('@vmlive/types')) {
317
- tsconfig.compilerOptions.types.push('@vmlive/types');
318
- fs.writeFileSync(tsConfigPath, JSON.stringify(tsconfig, null, 2));
319
- }
320
- } catch(e) {}
313
+ const tsconfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf8'));
314
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
315
+ tsconfig.compilerOptions.types = tsconfig.compilerOptions.types || [];
316
+ if (!tsconfig.compilerOptions.types.includes('@vmlive/types')) {
317
+ tsconfig.compilerOptions.types.push('@vmlive/types');
318
+ fs.writeFileSync(tsConfigPath, JSON.stringify(tsconfig, null, 2));
319
+ }
320
+ } catch (e) { }
321
321
  }
322
322
 
323
323
  console.log('\x1b[36mInstalling dependencies...\x1b[0m');
324
324
  await new Promise((resolve) => {
325
- exec('npm install', (err) => {
326
- if (err) console.error('\x1b[33m⚠️ Warning: npm install failed. You may need to run it manually to resolve TS errors.\x1b[0m');
327
- resolve();
328
- });
325
+ exec('npm install', (err) => {
326
+ if (err) console.error('\x1b[33m⚠️ Warning: npm install failed. You may need to run it manually to resolve TS errors.\x1b[0m');
327
+ resolve();
328
+ });
329
329
  });
330
330
 
331
331
  console.log('\x1b[32mProject created successfully.\x1b[0m');
@@ -334,45 +334,45 @@ export default {
334
334
 
335
335
  const runAdd = async () => {
336
336
  if (!fs.existsSync(CONFIG_PATH)) {
337
- console.error('\x1b[31m❌ Error:\x1b[0m Cannot add function. No vm.json found in this directory. Run `npx vmlive init` first.');
338
- process.exit(1);
337
+ console.error('\x1b[31m❌ Error:\x1b[0m Cannot add function. No vm.json found in this directory. Run `npx vmlive init` first.');
338
+ process.exit(1);
339
339
  }
340
-
340
+
341
341
  const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
342
-
342
+
343
343
  const functionNameRaw = await input({
344
- message: 'What do you want to name this new function?',
345
- default: 'webhooks'
344
+ message: 'What do you want to name this new function?',
345
+ default: 'webhooks'
346
346
  });
347
347
  const functionName = functionNameRaw.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
348
348
  if (!functionName) process.exit(1);
349
-
349
+
350
350
  if (config.functions.some(f => f.name === functionName)) {
351
- console.error(`\x1b[31m❌ Error:\x1b[0m Function '${functionName}' already exists in this project.`);
352
- process.exit(1);
351
+ console.error(`\x1b[31m❌ Error:\x1b[0m Function '${functionName}' already exists in this project.`);
352
+ process.exit(1);
353
353
  }
354
-
354
+
355
355
  if (!fs.existsSync(path.resolve('src'))) {
356
- fs.mkdirSync(path.resolve('src'), { recursive: true });
356
+ fs.mkdirSync(path.resolve('src'), { recursive: true });
357
357
  }
358
-
358
+
359
359
  const targetFile = path.resolve('src', `${functionName}.ts`);
360
360
  if (!fs.existsSync(targetFile)) {
361
- const defaultApiTs = `import type { Env, Context } from '@vmlive/types';
361
+ const defaultApiTs = `import type { Env, Context } from '@vmlive/types';
362
362
  export default {
363
363
  async fetch(request: Request, env: Env, ctx: Context) {
364
364
  return new Response("Hello from ${functionName}!", { status: 200 });
365
365
  }
366
366
  };
367
367
  `;
368
- fs.writeFileSync(targetFile, defaultApiTs);
368
+ fs.writeFileSync(targetFile, defaultApiTs);
369
369
  }
370
-
370
+
371
371
  config.functions.push({
372
- name: functionName,
373
- entry: `src/${functionName}.ts`
372
+ name: functionName,
373
+ entry: `src/${functionName}.ts`
374
374
  });
375
-
375
+
376
376
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
377
377
  console.log(`\x1b[32m✔ Function '${functionName}' securely scaffolded successfully!\x1b[0m`);
378
378
  };
@@ -383,14 +383,14 @@ const runDev = async () => {
383
383
  }
384
384
 
385
385
  const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
386
-
386
+
387
387
  if (!fs.existsSync(WORK_DIR)) fs.mkdirSync(WORK_DIR, { recursive: true });
388
388
  if (!fs.existsSync(DATA_DIR)) {
389
389
  fs.mkdirSync(DATA_DIR, { recursive: true });
390
390
  }
391
391
 
392
- const sharedBindings = fs.existsSync(path.resolve('.env'))
393
- ? dotenv.parse(fs.readFileSync(path.resolve('.env')))
392
+ const sharedBindings = fs.existsSync(path.resolve('.env'))
393
+ ? dotenv.parse(fs.readFileSync(path.resolve('.env')))
394
394
  : {};
395
395
 
396
396
  const resourcesConfig = buildResourcesConfig();
@@ -414,7 +414,7 @@ const runDev = async () => {
414
414
  format: 'esm',
415
415
  outdir: WORK_DIR,
416
416
  outExtension: { '.js': '.mjs' },
417
- external: ['cloudflare:*'],
417
+ external: ['cloudflare:*'],
418
418
  logLevel: 'warning'
419
419
  });
420
420
 
@@ -462,9 +462,9 @@ const runDev = async () => {
462
462
  }
463
463
  }))
464
464
  ];
465
- const mf = new Emulator({
466
- workers: miniflareWorkers,
467
- port: mfPort,
465
+ const mf = new Emulator({
466
+ workers: miniflareWorkers,
467
+ port: mfPort,
468
468
  cachePersist: path.join(WORK_DIR, 'cache'),
469
469
  d1Persist: path.join(DATA_DIR, 'db'),
470
470
  kvPersist: path.join(DATA_DIR, 'kv'),
@@ -472,10 +472,10 @@ const runDev = async () => {
472
472
  });
473
473
  await mf.ready;
474
474
  console.log('\nLocal endpoints available:');
475
-
475
+
476
476
  const dashboardUrl = `http://dashboard.localhost:${mfPort}`;
477
477
  console.log(` \x1b]8;;${dashboardUrl}\x1b\\\x1b[32m[dashboard]\x1b[0m\x1b]8;;\x1b\\ ${dashboardUrl}`);
478
-
478
+
479
479
  config.functions.forEach(fn => {
480
480
  const endpointUrl = `http://${workspaceId}-${projectId}-${fn.name}.localhost:${mfPort}`;
481
481
  const padName = `[${fn.name}]`.padEnd(13, ' ');
@@ -483,12 +483,12 @@ const runDev = async () => {
483
483
  });
484
484
 
485
485
  setInterval(() => {
486
- const memoryData = process.memoryUsage();
487
- const heapMb = Math.round(memoryData.heapUsed / 1024 / 1024);
488
- if (heapMb > 110) {
489
- console.warn(`\n\x1b[33m⚠️ [VM-CLI] CAUTION: HIGH MEMORY USAGE (${heapMb}MB)\x1b[0m`);
490
- console.warn(`\x1b[33mYour worker environment is approaching the 128MB production limit. Local memory profiling is bloated due to dev-tooling overhead, but ensure your production build avoids massive synchronous allocations.\x1b[0m`);
491
- }
486
+ const memoryData = process.memoryUsage();
487
+ const heapMb = Math.round(memoryData.heapUsed / 1024 / 1024);
488
+ if (heapMb > 110) {
489
+ console.warn(`\n\x1b[33m⚠️ [VM-CLI] CAUTION: HIGH MEMORY USAGE (${heapMb}MB)\x1b[0m`);
490
+ console.warn(`\x1b[33mYour worker environment is approaching the 128MB production limit. Local memory profiling is bloated due to dev-tooling overhead, but ensure your production build avoids massive synchronous allocations.\x1b[0m`);
491
+ }
492
492
  }, 10000);
493
493
 
494
494
  let debounceTimeout = null;
@@ -499,15 +499,15 @@ const runDev = async () => {
499
499
  try {
500
500
  console.log(`\nReloading ${filename}...`);
501
501
  await builder.rebuild();
502
-
502
+
503
503
  config.functions.forEach(fn => {
504
504
  const relativeTarget = `./${fn.name}-out.mjs`;
505
505
  fs.writeFileSync(path.join(WORK_DIR, `${fn.name}-shim.mjs`), generateShim(relativeTarget, workspaceId, projectId, fn.name));
506
506
  });
507
507
 
508
- await mf.setOptions({
509
- workers: miniflareWorkers,
510
- port: mfPort,
508
+ await mf.setOptions({
509
+ workers: miniflareWorkers,
510
+ port: mfPort,
511
511
  cachePersist: path.join(WORK_DIR, 'cache'),
512
512
  d1Persist: path.join(DATA_DIR, 'db'),
513
513
  kvPersist: path.join(DATA_DIR, 'kv'),
@@ -532,16 +532,16 @@ const runDev = async () => {
532
532
 
533
533
  const runDeploy = async () => {
534
534
  console.log('\x1b[36mDeploying...\x1b[0m');
535
-
535
+
536
536
  const configPath = path.join(os.homedir(), '.vm-config.json');
537
537
  let token = process.env.VM_API_TOKEN;
538
538
  if (!token && fs.existsSync(configPath)) {
539
- token = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
539
+ token = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
540
540
  }
541
-
541
+
542
542
  if (!token) {
543
- console.error('\x1b[31m❌ Unauthorized. Please run `vm login` first.\x1b[0m');
544
- process.exit(1);
543
+ console.error('\x1b[31m❌ Unauthorized. Please run `vm login` first.\x1b[0m');
544
+ process.exit(1);
545
545
  }
546
546
 
547
547
  if (!fs.existsSync(CONFIG_PATH)) {
@@ -555,52 +555,52 @@ const runDeploy = async () => {
555
555
  // Dotenv Cascading: Extract explicitly configured variables mapping them tightly
556
556
  let userEnv = {};
557
557
  if (fs.existsSync('.env')) {
558
- const baseEnv = dotenv.parse(fs.readFileSync('.env'));
559
- userEnv = { ...userEnv, ...baseEnv };
558
+ const baseEnv = dotenv.parse(fs.readFileSync('.env'));
559
+ userEnv = { ...userEnv, ...baseEnv };
560
560
  }
561
561
  if (fs.existsSync('.env.production')) {
562
- const prodEnv = dotenv.parse(fs.readFileSync('.env.production'));
563
- userEnv = { ...userEnv, ...prodEnv }; // Strict Override True
562
+ const prodEnv = dotenv.parse(fs.readFileSync('.env.production'));
563
+ userEnv = { ...userEnv, ...prodEnv }; // Strict Override True
564
564
  }
565
-
565
+
566
566
  console.log(`\x1b[35m[Config]\x1b[0m Successfully extracted ${Object.keys(userEnv).length} Environment Variables.`);
567
567
 
568
568
  for (const fn of functions) {
569
- console.log(`\x1b[36m[esbuild]\x1b[0m Bundling ${fn.name}...`);
570
- const outPath = path.join(WORK_DIR, `${fn.name}-out.mjs`);
571
-
572
- await esbuild.build({
573
- entryPoints: [path.resolve(fn.entry)],
574
- bundle: true,
575
- format: 'esm',
576
- outfile: outPath,
577
- external: ['cloudflare:*', 'node:*'],
578
- });
579
-
580
- const code = fs.readFileSync(outPath, 'utf8');
581
-
582
- const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'https://api.vm.live';
583
- const target = `${GATEKEEPER_URL}/api/${workspaceId}/projects/${projectId}/functions/${fn.name}/deploy`;
584
-
585
- console.log(`\x1b[36m[POST]\x1b[0m Uploading...`);
586
-
587
- const res = await fetch(target, {
588
- method: 'POST',
589
- headers: {
590
- 'Authorization': `Bearer ${token}`,
591
- 'Content-Type': 'application/json'
592
- },
593
- body: JSON.stringify({ code, envVars: userEnv })
594
- });
595
-
596
- if (!res.ok) {
597
- console.error(`\x1b[31m❌ Deployment failed: ${res.status}\x1b[0m`);
598
- console.error(await res.text());
599
- process.exit(1);
600
- }
601
-
602
- const jsonRes = await res.json();
603
- console.log(`\x1b[32mDeployed to: ${jsonRes.public_url || 'Success!'}\x1b[0m`);
569
+ console.log(`\x1b[36m[esbuild]\x1b[0m Bundling ${fn.name}...`);
570
+ const outPath = path.join(WORK_DIR, `${fn.name}-out.mjs`);
571
+
572
+ await esbuild.build({
573
+ entryPoints: [path.resolve(fn.entry)],
574
+ bundle: true,
575
+ format: 'esm',
576
+ outfile: outPath,
577
+ external: ['cloudflare:*', 'node:*'],
578
+ });
579
+
580
+ const code = fs.readFileSync(outPath, 'utf8');
581
+
582
+ const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'https://api.vm.live';
583
+ const target = `${GATEKEEPER_URL}/api/${workspaceId}/projects/${projectId}/functions/${fn.name}/deploy`;
584
+
585
+ console.log(`\x1b[36m[POST]\x1b[0m Uploading...`);
586
+
587
+ const res = await fetch(target, {
588
+ method: 'POST',
589
+ headers: {
590
+ 'Authorization': `Bearer ${token}`,
591
+ 'Content-Type': 'application/json'
592
+ },
593
+ body: JSON.stringify({ code, envVars: userEnv })
594
+ });
595
+
596
+ if (!res.ok) {
597
+ console.error(`\x1b[31m❌ Deployment failed: ${res.status}\x1b[0m`);
598
+ console.error(await res.text());
599
+ process.exit(1);
600
+ }
601
+
602
+ const jsonRes = await res.json();
603
+ console.log(`\x1b[32mDeployed to: ${jsonRes.public_url || 'Success!'}\x1b[0m`);
604
604
  }
605
605
  };
606
606
 
@@ -611,7 +611,7 @@ const runLogin = async () => {
611
611
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
612
612
 
613
613
  const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'https://api.vm.live';
614
-
614
+
615
615
  const server = http.createServer(async (req, res) => {
616
616
  const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
617
617
  const authCode = url.searchParams.get('code');
@@ -651,18 +651,18 @@ const runLogin = async () => {
651
651
  </body></html>
652
652
  `);
653
653
  console.log('\x1b[32m✔ Successfully authenticated.\x1b[0m');
654
-
654
+
655
655
  setTimeout(() => {
656
656
  server.close();
657
657
  process.exit(0);
658
658
  }, 1500); // 1.5s flush buffer TCP protection
659
-
660
-
659
+
660
+
661
661
  } catch (e) {
662
662
  console.error('\x1b[31m❌ Failed to exchange code:\x1b[0m', e.message);
663
663
  res.writeHead(500, { 'Content-Type': 'text/html' });
664
664
  res.end('<html><body><h1>Authentication Failed</h1></body></html>');
665
-
665
+
666
666
  setTimeout(() => {
667
667
  server.close();
668
668
  process.exit(1);
@@ -672,7 +672,7 @@ const runLogin = async () => {
672
672
 
673
673
  server.listen(0, '127.0.0.1', () => {
674
674
  const PORT = server.address().port;
675
- const EDITOR_URL = process.env.EDITOR_URL || 'http://localhost:5174';
675
+ const EDITOR_URL = process.env.EDITOR_URL || 'https://dash.vm.live';
676
676
  const authUrl = `${EDITOR_URL}/?challenge=${codeChallenge}&port=${PORT}`;
677
677
  console.log(`\x1b[34mOpening browser to: \x1b]8;;${authUrl}\x1b\\${authUrl}\x1b]8;;\x1b\\\x1b[0m`);
678
678
  const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
@@ -680,6 +680,24 @@ const runLogin = async () => {
680
680
  });
681
681
  };
682
682
 
683
+ const runUpdate = async () => {
684
+ console.log('\x1b[36mUpdating vmlive to the latest version...\x1b[0m');
685
+ const { spawn } = await import('child_process');
686
+
687
+ const updateProcess = spawn('npm', ['install', 'vmlive@latest'], {
688
+ stdio: 'inherit'
689
+ });
690
+
691
+ updateProcess.on('exit', (code) => {
692
+ if (code === 0) {
693
+ console.log('\n\x1b[32m✔ vmlive updated successfully!\x1b[0m');
694
+ } else {
695
+ console.error(`\n\x1b[31m❌ Update failed with exit code ${code}\x1b[0m`);
696
+ }
697
+ process.exit(code || 0);
698
+ });
699
+ };
700
+
683
701
  const main = async () => {
684
702
  const command = process.argv[2];
685
703
  if (command === 'init') {
@@ -692,12 +710,14 @@ const main = async () => {
692
710
  await runLogin();
693
711
  } else if (command === 'deploy') {
694
712
  await runDeploy();
713
+ } else if (command === 'update') {
714
+ await runUpdate();
695
715
  } else if (command === 'which' || command === '-v' || command === '--version' || command === 'v') {
696
716
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
697
717
  console.log(`vmlive CLI v${pkg.version}`);
698
718
  } else {
699
719
  console.error(`\x1b[31m❌ Unknown command: ${command || 'missing'}\x1b[0m`);
700
- console.log('Usage: vmlive init | vmlive add | vmlive dev | vmlive login | vmlive deploy | vmlive which');
720
+ console.log('Usage: vmlive init | vmlive add | vmlive dev | vmlive login | vmlive deploy | vmlive update | vmlive which');
701
721
  process.exit(1);
702
722
  }
703
723
  };