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.
- package/package.json +1 -1
- package/src/cli.js +210 -190
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
|
};
|
|
@@ -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
|
-
|
|
92
|
+
jwtToken = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
if (!jwtToken) {
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
+
headers: { 'Authorization': `Bearer ${jwtToken}` }
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
if (!wsRes.ok) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
129
|
-
|
|
128
|
+
message: 'Which workspace would you like to use?',
|
|
129
|
+
choices: workspaceChoices
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
if (workspaceSlug === '__CREATE_WORKSPACE__') {
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
177
|
+
headers: { 'Authorization': `Bearer ${jwtToken}` }
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
if (!projRes.ok) {
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
196
|
-
|
|
195
|
+
message: 'Which project would you like to use?',
|
|
196
|
+
choices: projectChoices
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
if (projectSlug === '__CREATE_PROJECT__') {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
} catch(e) {
|
|
291
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
+
fs.writeFileSync(targetFile, defaultApiTs);
|
|
369
369
|
}
|
|
370
|
-
|
|
370
|
+
|
|
371
371
|
config.functions.push({
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
539
|
+
token = JSON.parse(fs.readFileSync(configPath, 'utf-8')).token;
|
|
540
540
|
}
|
|
541
|
-
|
|
541
|
+
|
|
542
542
|
if (!token) {
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
559
|
-
|
|
558
|
+
const baseEnv = dotenv.parse(fs.readFileSync('.env'));
|
|
559
|
+
userEnv = { ...userEnv, ...baseEnv };
|
|
560
560
|
}
|
|
561
561
|
if (fs.existsSync('.env.production')) {
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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 || '
|
|
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
|
};
|