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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/package.json +2 -2
  3. 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 `index.ts`.
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
- - Runs functions locally via `http://<function-name>.localhost:8787`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vmlive",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local development VM for custom Serverless PaaS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,4 +18,4 @@
18
18
  "devDependencies": {
19
19
  "vitest": "^3.2.4"
20
20
  }
21
- }
21
+ }
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\\nUse format: http://<function-name>.localhost:8787\`,
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[36m⏳ Orchestrating live Platform boundaries...\x1b[0m');
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
- const workspaces = await wsRes.json();
99
- if (!Array.isArray(workspaces) || workspaces.length === 0) {
100
- console.error('\x1b[31m❌ Sandbox Error:\x1b[0m You do not possess any authorized Workspaces. Please map one on the dashboard first!');
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 workspaceSlug = await select({
106
- message: 'Select exactly which Platform Workspace you want this Sandbox to dynamically bind to:',
107
- choices: workspaces.map(w => ({ name: w.name, value: w.slug }))
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
- const projects = await projRes.json();
121
- if (!Array.isArray(projects) || projects.length === 0) {
122
- console.error('\x1b[31m❌ Sandbox Error:\x1b[0m No projects mathematically exist in this Workspace constraint.');
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 projectSlug = await select({
127
- message: 'Select exactly which Edge Project you want to natively emulate offline:',
128
- choices: projects.map(p => ({ name: p.name, value: p.slug }))
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 conceptually name this initial serverless function?',
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(API_FILE)) {
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(API_FILE, defaultApiTs);
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": "index.ts" }
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[36m📦 Installing platform types and dependencies...\x1b[0m');
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[32m✨ Extensible Project Scaffolded Successfully!\x1b[0m');
240
- console.log('You can now natively run \x1b[36mnpx vmlive dev\x1b[0m to initiate the offline sandbox proxy.');
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[32m✨ Platform Engine Initialized.\x1b[0m');
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(`🚀 Extensible Serverless VM running natively on http://*.localhost:${mfPort}`);
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(`\n🔄 ${filename} mutated. Syncing Engine...`);
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[32m✨ Engine Hot-Reloaded.\x1b[0m');
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('\n🛑 Suspending Platform VM...');
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[36m🚀 Initiating Deployment pipeline...\x1b[0m');
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 to Gatekeeper Dispatch Edge...`);
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[32m✨ Extracted Function Deployed to: ${jsonRes.public_url || 'Success!'}\x1b[0m`);
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[36m✨ Initiating vm.live PKCE Authentication...\x1b[0m');
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('Exchanging PKCE Authorization Code...');
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 Successfully authenticated! Saved strictly to ~/.vm-config.json\x1b[0m');
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
  };