vmlive 1.0.8 → 1.0.10
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/package.json +1 -1
- package/src/cli.js +195 -192
package/package.json
CHANGED
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
|
};
|
|
@@ -62,7 +62,10 @@ const buildProxyDispatcher = (functions, workspaceId, projectId, includeDashboar
|
|
|
62
62
|
const status = res.status;
|
|
63
63
|
const color = status >= 500 ? "\\x1b[31m" : status >= 400 ? "\\x1b[33m" : "\\x1b[32m";
|
|
64
64
|
|
|
65
|
+
const isMuted = path.startsWith('/.internal/') || path.includes('devtools');
|
|
66
|
+
if (!isMuted) {
|
|
65
67
|
console.log(\`\\x1b[90m[\${targetName}]\\x1b[0m \${method} \${path} \${color}\${status}\\x1b[0m\`);
|
|
68
|
+
}
|
|
66
69
|
return res;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
@@ -86,35 +89,35 @@ const runInit = async () => {
|
|
|
86
89
|
const configPath = path.join(os.homedir(), '.vm-config.json');
|
|
87
90
|
let jwtToken = process.env.VM_API_TOKEN;
|
|
88
91
|
if (!jwtToken && fs.existsSync(configPath)) {
|
|
89
|
-
|
|
92
|
+
jwtToken = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
if (!jwtToken) {
|
|
93
|
-
|
|
94
|
-
|
|
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);
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
const GATEKEEPER_URL = process.env.GATEKEEPER_URL || '
|
|
100
|
+
const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'https://api.vm.live';
|
|
98
101
|
|
|
99
102
|
// 2. Extract Workspace Architecture
|
|
100
103
|
console.log('\x1b[36mFetching workspaces...\x1b[0m');
|
|
101
104
|
const wsRes = await fetch(`${GATEKEEPER_URL}/api/workspaces`, {
|
|
102
|
-
|
|
105
|
+
headers: { 'Authorization': `Bearer ${jwtToken}` }
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
if (!wsRes.ok) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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);
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
let workspaces = await wsRes.json();
|
|
115
118
|
if (!Array.isArray(workspaces)) {
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve workspaces.');
|
|
120
|
+
process.exit(1);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// 3. Interactive Target Lock
|
|
@@ -122,104 +125,104 @@ const runInit = async () => {
|
|
|
122
125
|
workspaceChoices.push({ name: '+ Create New Workspace', value: '__CREATE_WORKSPACE__' });
|
|
123
126
|
|
|
124
127
|
let workspaceSlug = await select({
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
message: 'Which workspace would you like to use?',
|
|
129
|
+
choices: workspaceChoices
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
if (workspaceSlug === '__CREATE_WORKSPACE__') {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
// 4. Extract Project Containers
|
|
173
176
|
const projRes = await fetch(`${GATEKEEPER_URL}/api/${workspaceSlug}/projects`, {
|
|
174
|
-
|
|
177
|
+
headers: { 'Authorization': `Bearer ${jwtToken}` }
|
|
175
178
|
});
|
|
176
179
|
|
|
177
180
|
if (!projRes.ok) {
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to logically retrieve project constraints from Production.');
|
|
182
|
+
process.exit(1);
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
let projects = await projRes.json();
|
|
183
186
|
if (!Array.isArray(projects)) {
|
|
184
|
-
|
|
185
|
-
|
|
187
|
+
console.error('\x1b[31m❌ Sandbox Error:\x1b[0m Failed to retrieve projects.');
|
|
188
|
+
process.exit(1);
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
const projectChoices = projects.map(p => ({ name: p.name, value: p.slug }));
|
|
189
192
|
projectChoices.push({ name: '+ Create New Project', value: '__CREATE_PROJECT__' });
|
|
190
193
|
|
|
191
194
|
let projectSlug = await select({
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
message: 'Which project would you like to use?',
|
|
196
|
+
choices: projectChoices
|
|
194
197
|
});
|
|
195
198
|
|
|
196
199
|
if (projectSlug === '__CREATE_PROJECT__') {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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`);
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
const functionNameRaw = await input({
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
message: 'What do you want to name this function?',
|
|
220
|
+
default: 'api'
|
|
218
221
|
});
|
|
219
222
|
const functionName = functionNameRaw.trim().toLowerCase().replace(/[^a-z0-9-]/g, '') || 'api';
|
|
220
223
|
|
|
221
224
|
if (!fs.existsSync(path.resolve('src'))) {
|
|
222
|
-
|
|
225
|
+
fs.mkdirSync(path.resolve('src'), { recursive: true });
|
|
223
226
|
}
|
|
224
227
|
const funcFile = path.resolve('src', `${functionName}.ts`);
|
|
225
228
|
|
|
@@ -278,14 +281,14 @@ export default {
|
|
|
278
281
|
fs.writeFileSync(pkgPath, defaultPackageJson);
|
|
279
282
|
} else {
|
|
280
283
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} catch(e) {
|
|
288
|
-
|
|
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
|
|
289
292
|
}
|
|
290
293
|
}
|
|
291
294
|
|
|
@@ -307,22 +310,22 @@ export default {
|
|
|
307
310
|
fs.writeFileSync(tsConfigPath, defaultTsConfig);
|
|
308
311
|
} else {
|
|
309
312
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} 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) { }
|
|
318
321
|
}
|
|
319
322
|
|
|
320
323
|
console.log('\x1b[36mInstalling dependencies...\x1b[0m');
|
|
321
324
|
await new Promise((resolve) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
});
|
|
326
329
|
});
|
|
327
330
|
|
|
328
331
|
console.log('\x1b[32mProject created successfully.\x1b[0m');
|
|
@@ -331,45 +334,45 @@ export default {
|
|
|
331
334
|
|
|
332
335
|
const runAdd = async () => {
|
|
333
336
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
336
339
|
}
|
|
337
|
-
|
|
340
|
+
|
|
338
341
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
339
|
-
|
|
342
|
+
|
|
340
343
|
const functionNameRaw = await input({
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
message: 'What do you want to name this new function?',
|
|
345
|
+
default: 'webhooks'
|
|
343
346
|
});
|
|
344
347
|
const functionName = functionNameRaw.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
345
348
|
if (!functionName) process.exit(1);
|
|
346
|
-
|
|
349
|
+
|
|
347
350
|
if (config.functions.some(f => f.name === functionName)) {
|
|
348
|
-
|
|
349
|
-
|
|
351
|
+
console.error(`\x1b[31m❌ Error:\x1b[0m Function '${functionName}' already exists in this project.`);
|
|
352
|
+
process.exit(1);
|
|
350
353
|
}
|
|
351
|
-
|
|
354
|
+
|
|
352
355
|
if (!fs.existsSync(path.resolve('src'))) {
|
|
353
|
-
|
|
356
|
+
fs.mkdirSync(path.resolve('src'), { recursive: true });
|
|
354
357
|
}
|
|
355
|
-
|
|
358
|
+
|
|
356
359
|
const targetFile = path.resolve('src', `${functionName}.ts`);
|
|
357
360
|
if (!fs.existsSync(targetFile)) {
|
|
358
|
-
|
|
361
|
+
const defaultApiTs = `import type { Env, Context } from '@vmlive/types';
|
|
359
362
|
export default {
|
|
360
363
|
async fetch(request: Request, env: Env, ctx: Context) {
|
|
361
364
|
return new Response("Hello from ${functionName}!", { status: 200 });
|
|
362
365
|
}
|
|
363
366
|
};
|
|
364
367
|
`;
|
|
365
|
-
|
|
368
|
+
fs.writeFileSync(targetFile, defaultApiTs);
|
|
366
369
|
}
|
|
367
|
-
|
|
370
|
+
|
|
368
371
|
config.functions.push({
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
name: functionName,
|
|
373
|
+
entry: `src/${functionName}.ts`
|
|
371
374
|
});
|
|
372
|
-
|
|
375
|
+
|
|
373
376
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
374
377
|
console.log(`\x1b[32m✔ Function '${functionName}' securely scaffolded successfully!\x1b[0m`);
|
|
375
378
|
};
|
|
@@ -380,14 +383,14 @@ const runDev = async () => {
|
|
|
380
383
|
}
|
|
381
384
|
|
|
382
385
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
383
|
-
|
|
386
|
+
|
|
384
387
|
if (!fs.existsSync(WORK_DIR)) fs.mkdirSync(WORK_DIR, { recursive: true });
|
|
385
388
|
if (!fs.existsSync(DATA_DIR)) {
|
|
386
389
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
387
390
|
}
|
|
388
391
|
|
|
389
|
-
const sharedBindings = fs.existsSync(path.resolve('.env'))
|
|
390
|
-
? dotenv.parse(fs.readFileSync(path.resolve('.env')))
|
|
392
|
+
const sharedBindings = fs.existsSync(path.resolve('.env'))
|
|
393
|
+
? dotenv.parse(fs.readFileSync(path.resolve('.env')))
|
|
391
394
|
: {};
|
|
392
395
|
|
|
393
396
|
const resourcesConfig = buildResourcesConfig();
|
|
@@ -411,7 +414,7 @@ const runDev = async () => {
|
|
|
411
414
|
format: 'esm',
|
|
412
415
|
outdir: WORK_DIR,
|
|
413
416
|
outExtension: { '.js': '.mjs' },
|
|
414
|
-
external: ['cloudflare:*'],
|
|
417
|
+
external: ['cloudflare:*'],
|
|
415
418
|
logLevel: 'warning'
|
|
416
419
|
});
|
|
417
420
|
|
|
@@ -459,9 +462,9 @@ const runDev = async () => {
|
|
|
459
462
|
}
|
|
460
463
|
}))
|
|
461
464
|
];
|
|
462
|
-
const mf = new Emulator({
|
|
463
|
-
workers: miniflareWorkers,
|
|
464
|
-
port: mfPort,
|
|
465
|
+
const mf = new Emulator({
|
|
466
|
+
workers: miniflareWorkers,
|
|
467
|
+
port: mfPort,
|
|
465
468
|
cachePersist: path.join(WORK_DIR, 'cache'),
|
|
466
469
|
d1Persist: path.join(DATA_DIR, 'db'),
|
|
467
470
|
kvPersist: path.join(DATA_DIR, 'kv'),
|
|
@@ -469,10 +472,10 @@ const runDev = async () => {
|
|
|
469
472
|
});
|
|
470
473
|
await mf.ready;
|
|
471
474
|
console.log('\nLocal endpoints available:');
|
|
472
|
-
|
|
475
|
+
|
|
473
476
|
const dashboardUrl = `http://dashboard.localhost:${mfPort}`;
|
|
474
477
|
console.log(` \x1b]8;;${dashboardUrl}\x1b\\\x1b[32m[dashboard]\x1b[0m\x1b]8;;\x1b\\ ${dashboardUrl}`);
|
|
475
|
-
|
|
478
|
+
|
|
476
479
|
config.functions.forEach(fn => {
|
|
477
480
|
const endpointUrl = `http://${workspaceId}-${projectId}-${fn.name}.localhost:${mfPort}`;
|
|
478
481
|
const padName = `[${fn.name}]`.padEnd(13, ' ');
|
|
@@ -480,12 +483,12 @@ const runDev = async () => {
|
|
|
480
483
|
});
|
|
481
484
|
|
|
482
485
|
setInterval(() => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
}
|
|
489
492
|
}, 10000);
|
|
490
493
|
|
|
491
494
|
let debounceTimeout = null;
|
|
@@ -496,15 +499,15 @@ const runDev = async () => {
|
|
|
496
499
|
try {
|
|
497
500
|
console.log(`\nReloading ${filename}...`);
|
|
498
501
|
await builder.rebuild();
|
|
499
|
-
|
|
502
|
+
|
|
500
503
|
config.functions.forEach(fn => {
|
|
501
504
|
const relativeTarget = `./${fn.name}-out.mjs`;
|
|
502
505
|
fs.writeFileSync(path.join(WORK_DIR, `${fn.name}-shim.mjs`), generateShim(relativeTarget, workspaceId, projectId, fn.name));
|
|
503
506
|
});
|
|
504
507
|
|
|
505
|
-
await mf.setOptions({
|
|
506
|
-
workers: miniflareWorkers,
|
|
507
|
-
port: mfPort,
|
|
508
|
+
await mf.setOptions({
|
|
509
|
+
workers: miniflareWorkers,
|
|
510
|
+
port: mfPort,
|
|
508
511
|
cachePersist: path.join(WORK_DIR, 'cache'),
|
|
509
512
|
d1Persist: path.join(DATA_DIR, 'db'),
|
|
510
513
|
kvPersist: path.join(DATA_DIR, 'kv'),
|
|
@@ -529,16 +532,16 @@ const runDev = async () => {
|
|
|
529
532
|
|
|
530
533
|
const runDeploy = async () => {
|
|
531
534
|
console.log('\x1b[36mDeploying...\x1b[0m');
|
|
532
|
-
|
|
535
|
+
|
|
533
536
|
const configPath = path.join(os.homedir(), '.vm-config.json');
|
|
534
537
|
let token = process.env.VM_API_TOKEN;
|
|
535
538
|
if (!token && fs.existsSync(configPath)) {
|
|
536
|
-
|
|
539
|
+
token = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
|
|
537
540
|
}
|
|
538
|
-
|
|
541
|
+
|
|
539
542
|
if (!token) {
|
|
540
|
-
|
|
541
|
-
|
|
543
|
+
console.error('\x1b[31m❌ Unauthorized. Please run `vm login` first.\x1b[0m');
|
|
544
|
+
process.exit(1);
|
|
542
545
|
}
|
|
543
546
|
|
|
544
547
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
@@ -552,52 +555,52 @@ const runDeploy = async () => {
|
|
|
552
555
|
// Dotenv Cascading: Extract explicitly configured variables mapping them tightly
|
|
553
556
|
let userEnv = {};
|
|
554
557
|
if (fs.existsSync('.env')) {
|
|
555
|
-
|
|
556
|
-
|
|
558
|
+
const baseEnv = dotenv.parse(fs.readFileSync('.env'));
|
|
559
|
+
userEnv = { ...userEnv, ...baseEnv };
|
|
557
560
|
}
|
|
558
561
|
if (fs.existsSync('.env.production')) {
|
|
559
|
-
|
|
560
|
-
|
|
562
|
+
const prodEnv = dotenv.parse(fs.readFileSync('.env.production'));
|
|
563
|
+
userEnv = { ...userEnv, ...prodEnv }; // Strict Override True
|
|
561
564
|
}
|
|
562
|
-
|
|
565
|
+
|
|
563
566
|
console.log(`\x1b[35m[Config]\x1b[0m Successfully extracted ${Object.keys(userEnv).length} Environment Variables.`);
|
|
564
567
|
|
|
565
568
|
for (const fn of functions) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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`);
|
|
601
604
|
}
|
|
602
605
|
};
|
|
603
606
|
|
|
@@ -607,8 +610,8 @@ const runLogin = async () => {
|
|
|
607
610
|
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
608
611
|
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
609
612
|
|
|
610
|
-
const GATEKEEPER_URL = process.env.GATEKEEPER_URL || '
|
|
611
|
-
|
|
613
|
+
const GATEKEEPER_URL = process.env.GATEKEEPER_URL || 'https://api.vm.live';
|
|
614
|
+
|
|
612
615
|
const server = http.createServer(async (req, res) => {
|
|
613
616
|
const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
|
|
614
617
|
const authCode = url.searchParams.get('code');
|
|
@@ -648,18 +651,18 @@ const runLogin = async () => {
|
|
|
648
651
|
</body></html>
|
|
649
652
|
`);
|
|
650
653
|
console.log('\x1b[32m✔ Successfully authenticated.\x1b[0m');
|
|
651
|
-
|
|
654
|
+
|
|
652
655
|
setTimeout(() => {
|
|
653
656
|
server.close();
|
|
654
657
|
process.exit(0);
|
|
655
658
|
}, 1500); // 1.5s flush buffer TCP protection
|
|
656
|
-
|
|
657
|
-
|
|
659
|
+
|
|
660
|
+
|
|
658
661
|
} catch (e) {
|
|
659
662
|
console.error('\x1b[31m❌ Failed to exchange code:\x1b[0m', e.message);
|
|
660
663
|
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
661
664
|
res.end('<html><body><h1>Authentication Failed</h1></body></html>');
|
|
662
|
-
|
|
665
|
+
|
|
663
666
|
setTimeout(() => {
|
|
664
667
|
server.close();
|
|
665
668
|
process.exit(1);
|
|
@@ -669,7 +672,7 @@ const runLogin = async () => {
|
|
|
669
672
|
|
|
670
673
|
server.listen(0, '127.0.0.1', () => {
|
|
671
674
|
const PORT = server.address().port;
|
|
672
|
-
const EDITOR_URL = process.env.EDITOR_URL || '
|
|
675
|
+
const EDITOR_URL = process.env.EDITOR_URL || 'https://dash.vm.live';
|
|
673
676
|
const authUrl = `${EDITOR_URL}/?challenge=${codeChallenge}&port=${PORT}`;
|
|
674
677
|
console.log(`\x1b[34mOpening browser to: \x1b]8;;${authUrl}\x1b\\${authUrl}\x1b]8;;\x1b\\\x1b[0m`);
|
|
675
678
|
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
@@ -694,7 +697,7 @@ const main = async () => {
|
|
|
694
697
|
console.log(`vmlive CLI v${pkg.version}`);
|
|
695
698
|
} else {
|
|
696
699
|
console.error(`\x1b[31m❌ Unknown command: ${command || 'missing'}\x1b[0m`);
|
|
697
|
-
console.log('Usage:
|
|
700
|
+
console.log('Usage: vmlive init | vmlive add | vmlive dev | vmlive login | vmlive deploy | vmlive which');
|
|
698
701
|
process.exit(1);
|
|
699
702
|
}
|
|
700
703
|
};
|