checkpoint-cli 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +141 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -271,6 +271,19 @@ function launchNgrok(port) {
|
|
|
271
271
|
} });
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
|
+
async function getUserWorkspaceId(sb, userId) {
|
|
275
|
+
const { data: membership, error } = await sb
|
|
276
|
+
.from('workspace_members')
|
|
277
|
+
.select('workspace_id')
|
|
278
|
+
.eq('user_id', userId)
|
|
279
|
+
.order('created_at', { ascending: true })
|
|
280
|
+
.limit(1)
|
|
281
|
+
.maybeSingle();
|
|
282
|
+
if (error) {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
return membership?.workspace_id ?? null;
|
|
286
|
+
}
|
|
274
287
|
async function registerTunnel(sb, opts) {
|
|
275
288
|
if (!isValidUrl(opts.tunnelUrl)) {
|
|
276
289
|
console.log(chalk_1.default.yellow(' ⚠ Invalid tunnel URL'));
|
|
@@ -283,24 +296,32 @@ async function registerTunnel(sb, opts) {
|
|
|
283
296
|
}
|
|
284
297
|
const APP_URL = (0, config_js_1.getAppUrl)();
|
|
285
298
|
const userId = userData.user.id;
|
|
299
|
+
const workspaceId = await getUserWorkspaceId(sb, userId);
|
|
300
|
+
if (!workspaceId) {
|
|
301
|
+
console.log(chalk_1.default.yellow(' ⚠ No workspace membership found. Complete setup in the dashboard first.'));
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
286
304
|
// Try to find an existing tunnel with the same name for this user
|
|
287
305
|
const { data: existing } = await sb
|
|
288
306
|
.from('tunnels')
|
|
289
307
|
.select('id, share_id')
|
|
290
308
|
.eq('user_id', userId)
|
|
309
|
+
.eq('workspace_id', workspaceId)
|
|
291
310
|
.eq('name', opts.name)
|
|
292
311
|
.order('created_at', { ascending: false })
|
|
293
312
|
.limit(1)
|
|
294
313
|
.single();
|
|
295
314
|
if (existing) {
|
|
296
315
|
// Reconnect: reuse the same share_id, update the tunnel URL
|
|
316
|
+
const now = new Date().toISOString();
|
|
297
317
|
const { error } = await sb
|
|
298
318
|
.from('tunnels')
|
|
299
319
|
.update({
|
|
300
320
|
tunnel_url: opts.tunnelUrl,
|
|
301
321
|
local_port: opts.port,
|
|
302
322
|
status: 'active',
|
|
303
|
-
|
|
323
|
+
activated_at: now,
|
|
324
|
+
last_seen_at: now,
|
|
304
325
|
})
|
|
305
326
|
.eq('id', existing.id);
|
|
306
327
|
if (error) {
|
|
@@ -310,8 +331,32 @@ async function registerTunnel(sb, opts) {
|
|
|
310
331
|
console.log(chalk_1.default.green(' ✓ Reconnected to existing tunnel') + chalk_1.default.gray(` (${opts.name})`));
|
|
311
332
|
return { shareUrl: `${APP_URL}/share/${existing.share_id}`, tunnelId: existing.id };
|
|
312
333
|
}
|
|
334
|
+
// Check plan limits before creating a new tunnel
|
|
335
|
+
const { data: subscription } = await sb
|
|
336
|
+
.from('subscriptions')
|
|
337
|
+
.select('status, billing_plans(name)')
|
|
338
|
+
.eq('workspace_id', workspaceId)
|
|
339
|
+
.maybeSingle();
|
|
340
|
+
const isActiveSub = subscription?.status === 'active' || subscription?.status === 'trialing';
|
|
341
|
+
const planName = subscription?.billing_plans?.name?.toLowerCase() ?? '';
|
|
342
|
+
const isPro = planName.includes('pro') || planName.includes('enterprise');
|
|
343
|
+
const FREE_TUNNEL_LIMIT = 1;
|
|
344
|
+
if (!isActiveSub || !isPro) {
|
|
345
|
+
const { count } = await sb
|
|
346
|
+
.from('tunnels')
|
|
347
|
+
.select('id', { count: 'exact', head: true })
|
|
348
|
+
.eq('workspace_id', workspaceId);
|
|
349
|
+
if ((count ?? 0) >= FREE_TUNNEL_LIMIT) {
|
|
350
|
+
console.log('');
|
|
351
|
+
console.log(chalk_1.default.red(` ✗ Tunnel limit reached (${FREE_TUNNEL_LIMIT} tunnels on the Free plan).`));
|
|
352
|
+
console.log(chalk_1.default.gray(' Upgrade to Pro for unlimited tunnels: ') + chalk_1.default.cyan(`${(0, config_js_1.getAppUrl)()}/dashboard/settings?tab=billing`));
|
|
353
|
+
console.log('');
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
313
357
|
// No existing tunnel — create a new one
|
|
314
358
|
const shareId = generateShareId();
|
|
359
|
+
const now = new Date().toISOString();
|
|
315
360
|
const { data, error } = await sb.from('tunnels').insert({
|
|
316
361
|
name: opts.name,
|
|
317
362
|
local_port: opts.port,
|
|
@@ -319,7 +364,9 @@ async function registerTunnel(sb, opts) {
|
|
|
319
364
|
share_id: shareId,
|
|
320
365
|
status: 'active',
|
|
321
366
|
user_id: userId,
|
|
322
|
-
|
|
367
|
+
workspace_id: workspaceId,
|
|
368
|
+
activated_at: now,
|
|
369
|
+
last_seen_at: now,
|
|
323
370
|
}).select('id').single();
|
|
324
371
|
if (error || !data) {
|
|
325
372
|
console.log(chalk_1.default.yellow(` ⚠ Could not register: ${error?.message ?? 'unknown'}`));
|
|
@@ -362,35 +409,60 @@ function openBrowser(url) {
|
|
|
362
409
|
}
|
|
363
410
|
function loginWithBrowser() {
|
|
364
411
|
return new Promise((resolve, reject) => {
|
|
365
|
-
const server = http_1.default.createServer((req, res) => {
|
|
412
|
+
const server = http_1.default.createServer(async (req, res) => {
|
|
366
413
|
const url = new URL(req.url || '/', `http://localhost`);
|
|
367
414
|
if (url.pathname === '/callback') {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
<
|
|
386
|
-
<p style="
|
|
415
|
+
const code = url.searchParams.get('code');
|
|
416
|
+
if (code) {
|
|
417
|
+
try {
|
|
418
|
+
const APP_URL = (0, config_js_1.getAppUrl)();
|
|
419
|
+
const exchangeRes = await fetch(`${APP_URL}/api/cli/auth-code?code=${encodeURIComponent(code)}`);
|
|
420
|
+
if (!exchangeRes.ok)
|
|
421
|
+
throw new Error('Code exchange failed');
|
|
422
|
+
const { access_token, refresh_token } = await exchangeRes.json();
|
|
423
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
424
|
+
res.end(`
|
|
425
|
+
<html>
|
|
426
|
+
<body style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #13120A; color: #f0ede6;">
|
|
427
|
+
<div style="text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px;">
|
|
428
|
+
<svg width="32" height="32" viewBox="0 0 256 256" fill="none">
|
|
429
|
+
<circle cx="128" cy="128" r="112" fill="#22c55e"/>
|
|
430
|
+
<polyline points="88,136 112,160 168,104" stroke="#13120A" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
431
|
+
</svg>
|
|
432
|
+
<h1 style="font-size: 20px; font-weight: 500; margin: 0; color: #f0ede6;">Logged in to Checkpoint</h1>
|
|
433
|
+
<p style="color: #9e9889; margin: 0; font-size: 14px;">You can close this tab and return to your terminal.</p>
|
|
434
|
+
<div style="margin-top: 8px; padding-top: 24px; border-top: 1px dashed rgba(73,67,58,0.3);">
|
|
435
|
+
<p style="color: #9e9889; margin: 0px 0 24px; font-size: 13px;">Start sharing your localhost for feedback:</p>
|
|
436
|
+
<code style="padding: 8px 12px; background: #1D1B15; border: 1px solid rgba(73,67,58,0.4); border-radius: 6px; font-family: 'JetBrains Mono','Fira Code','SF Mono',monospace; font-size: 13px; color: #9e9889;">checkpoint start -p <span style="color: #ff5700;"><port></span></code>
|
|
437
|
+
<p style="text-align: center; margin-top: 24px; font-size: 13px; color: #9e9889;">or <a href="${APP_URL}" style="color: #ff5700; text-decoration: none;">Go to Dashboard</a></p>
|
|
438
|
+
</div>
|
|
387
439
|
</div>
|
|
388
|
-
</
|
|
389
|
-
</
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
440
|
+
</body>
|
|
441
|
+
</html>
|
|
442
|
+
`);
|
|
443
|
+
server.close();
|
|
444
|
+
resolve({ access_token, refresh_token });
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
448
|
+
res.end(`
|
|
449
|
+
<html>
|
|
450
|
+
<body style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #13120A; color: #f0ede6;">
|
|
451
|
+
<div style="text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px;">
|
|
452
|
+
<svg width="48" height="48" viewBox="0 0 256 256" fill="none">
|
|
453
|
+
<circle cx="128" cy="128" r="112" fill="#ef4444"/>
|
|
454
|
+
<line x1="96" y1="96" x2="160" y2="160" stroke="#13120A" stroke-width="16" stroke-linecap="round"/>
|
|
455
|
+
<line x1="160" y1="96" x2="96" y2="160" stroke="#13120A" stroke-width="16" stroke-linecap="round"/>
|
|
456
|
+
</svg>
|
|
457
|
+
<h1 style="font-size: 20px; font-weight: 500; margin: 0; color: #f0ede6;">Login failed</h1>
|
|
458
|
+
<p style="color: #9e9889; margin: 0; font-size: 14px;">Code exchange failed. Please try again.</p>
|
|
459
|
+
</div>
|
|
460
|
+
</body>
|
|
461
|
+
</html>
|
|
462
|
+
`);
|
|
463
|
+
server.close();
|
|
464
|
+
reject(new Error('Code exchange failed'));
|
|
465
|
+
}
|
|
394
466
|
}
|
|
395
467
|
else {
|
|
396
468
|
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -404,13 +476,13 @@ function loginWithBrowser() {
|
|
|
404
476
|
<line x1="160" y1="96" x2="96" y2="160" stroke="#13120A" stroke-width="16" stroke-linecap="round"/>
|
|
405
477
|
</svg>
|
|
406
478
|
<h1 style="font-size: 20px; font-weight: 500; margin: 0; color: #f0ede6;">Login failed</h1>
|
|
407
|
-
<p style="color: #9e9889; margin: 0; font-size: 14px;">Missing
|
|
479
|
+
<p style="color: #9e9889; margin: 0; font-size: 14px;">Missing auth code. Please try again.</p>
|
|
408
480
|
</div>
|
|
409
481
|
</body>
|
|
410
482
|
</html>
|
|
411
483
|
`);
|
|
412
484
|
server.close();
|
|
413
|
-
reject(new Error('Missing
|
|
485
|
+
reject(new Error('Missing auth code in callback'));
|
|
414
486
|
}
|
|
415
487
|
}
|
|
416
488
|
else {
|
|
@@ -431,15 +503,18 @@ function loginWithBrowser() {
|
|
|
431
503
|
console.log('');
|
|
432
504
|
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
433
505
|
rl.question(chalk_1.default.gray(' Press ENTER to open the browser...'), () => {
|
|
434
|
-
rl.close();
|
|
435
506
|
openBrowser(loginUrl);
|
|
507
|
+
// Keep stdin open so the HTTP server stays alive to receive the callback.
|
|
508
|
+
// Pause instead of closing so the event loop doesn't drain.
|
|
509
|
+
rl.close();
|
|
510
|
+
process.stdin.resume();
|
|
436
511
|
});
|
|
437
512
|
});
|
|
438
|
-
// Timeout after
|
|
513
|
+
// Timeout after 10 minutes
|
|
439
514
|
setTimeout(() => {
|
|
440
515
|
server.close();
|
|
441
|
-
reject(new Error('Login timed out (
|
|
442
|
-
},
|
|
516
|
+
reject(new Error('Login timed out (10 minutes). Please try again.'));
|
|
517
|
+
}, 10 * 60 * 1000);
|
|
443
518
|
});
|
|
444
519
|
}
|
|
445
520
|
/* ── CLI ── */
|
|
@@ -447,7 +522,7 @@ const program = new commander_1.Command();
|
|
|
447
522
|
program
|
|
448
523
|
.name('checkpoint')
|
|
449
524
|
.description('Share your localhost with reviewers — get visual feedback directly on the page.\n\nQuick start:\n 1. checkpoint login Sign in to your Checkpoint account\n 2. checkpoint start -p 3000 Start tunneling and get a share link\n\nReuse a tunnel name to keep the same share URL and preserve all comments.')
|
|
450
|
-
.version('0.5.
|
|
525
|
+
.version('0.5.1');
|
|
451
526
|
// ── checkpoint login ──
|
|
452
527
|
program
|
|
453
528
|
.command('login')
|
|
@@ -636,8 +711,36 @@ program
|
|
|
636
711
|
console.log(chalk_1.default.red(' Session expired. Run `checkpoint login` again.'));
|
|
637
712
|
process.exit(1);
|
|
638
713
|
}
|
|
714
|
+
const workspaceId = await getUserWorkspaceId(sb, userData.user.id);
|
|
715
|
+
if (!workspaceId) {
|
|
716
|
+
console.log(chalk_1.default.red(' Failed: no workspace membership found. Complete setup in the dashboard first.'));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
// Check plan limits
|
|
720
|
+
const { data: subscription } = await sb
|
|
721
|
+
.from('subscriptions')
|
|
722
|
+
.select('status, billing_plans(name)')
|
|
723
|
+
.eq('workspace_id', workspaceId)
|
|
724
|
+
.maybeSingle();
|
|
725
|
+
const isActiveSub = subscription?.status === 'active' || subscription?.status === 'trialing';
|
|
726
|
+
const planName = subscription?.billing_plans?.name?.toLowerCase() ?? '';
|
|
727
|
+
const isPro = planName.includes('pro') || planName.includes('enterprise');
|
|
728
|
+
const FREE_TUNNEL_LIMIT = 1;
|
|
729
|
+
if (!isActiveSub || !isPro) {
|
|
730
|
+
const { count } = await sb
|
|
731
|
+
.from('tunnels')
|
|
732
|
+
.select('id', { count: 'exact', head: true })
|
|
733
|
+
.eq('workspace_id', workspaceId);
|
|
734
|
+
if ((count ?? 0) >= FREE_TUNNEL_LIMIT) {
|
|
735
|
+
console.log(chalk_1.default.red(` ✗ Tunnel limit reached (${FREE_TUNNEL_LIMIT} tunnels on the Free plan).`));
|
|
736
|
+
console.log(chalk_1.default.gray(' Upgrade to Pro for unlimited tunnels: ') + chalk_1.default.cyan(`${(0, config_js_1.getAppUrl)()}/dashboard/settings?tab=billing`));
|
|
737
|
+
console.log('');
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
639
741
|
const APP_URL = (0, config_js_1.getAppUrl)();
|
|
640
742
|
const shareId = generateShareId();
|
|
743
|
+
const now = new Date().toISOString();
|
|
641
744
|
const { data, error } = await sb.from('tunnels').insert({
|
|
642
745
|
name: opts.name,
|
|
643
746
|
local_port: parseInt(opts.port, 10),
|
|
@@ -645,7 +748,9 @@ program
|
|
|
645
748
|
share_id: shareId,
|
|
646
749
|
status: 'active',
|
|
647
750
|
user_id: userData.user.id,
|
|
648
|
-
|
|
751
|
+
workspace_id: workspaceId,
|
|
752
|
+
activated_at: now,
|
|
753
|
+
last_seen_at: now,
|
|
649
754
|
}).select('id').single();
|
|
650
755
|
if (error || !data) {
|
|
651
756
|
console.log(chalk_1.default.red(` Failed: ${error?.message ?? 'unknown error'}`));
|