vslides 1.0.5 → 1.0.7

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 +340 -0
  2. package/dist/cli.js +299 -44
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # vslides CLI
2
+
3
+ Command-line interface for creating and managing Vercel Slides presentations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g vslides
9
+ ```
10
+
11
+ Or run directly with npx:
12
+
13
+ ```bash
14
+ npx vslides <command>
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # First time: Login to save credentials (valid for 7 days)
21
+ vslides login
22
+
23
+ # Create a new presentation
24
+ vslides init
25
+
26
+ # Wait for sandbox to be ready
27
+ vslides check --wait
28
+
29
+ # Edit slides.md, then push changes
30
+ vslides push
31
+
32
+ # Open preview in browser
33
+ vslides preview --open
34
+ ```
35
+
36
+ ## Authentication
37
+
38
+ The CLI uses persistent authentication so you don't need to re-authenticate for every new presentation.
39
+
40
+ ### Commands
41
+
42
+ #### `vslides login`
43
+
44
+ Authenticate with your Vercel account. Opens a browser for OAuth authentication. Credentials are cached locally and valid for 7 days.
45
+
46
+ ```bash
47
+ vslides login
48
+ # Opens browser for Vercel OAuth
49
+ # Credentials saved to ~/.vslides/auth.json
50
+ ```
51
+
52
+ #### `vslides logout`
53
+
54
+ Sign out and revoke your cached credentials.
55
+
56
+ ```bash
57
+ vslides logout
58
+ ```
59
+
60
+ #### `vslides whoami`
61
+
62
+ Show your current authentication status.
63
+
64
+ ```bash
65
+ vslides whoami
66
+ # Output: Logged in as user@vercel.com
67
+ # Expires: 2/4/2026 (7 days remaining)
68
+
69
+ # Validate token with server
70
+ vslides whoami --validate
71
+ ```
72
+
73
+ ### How Authentication Works
74
+
75
+ 1. **First-time setup**: Run `vslides login` to authenticate via Vercel OAuth
76
+ 2. **Credentials cached**: Token stored in `~/.vslides/auth.json` (valid for 7 days)
77
+ 3. **Automatic use**: `vslides init` uses cached credentials to skip OAuth flow
78
+ 4. **Expiration**: After 7 days, run `vslides login` again
79
+
80
+ ### Security
81
+
82
+ - Credentials stored in `~/.vslides/auth.json` with restricted permissions (600)
83
+ - Directory `~/.vslides/` created with mode 700
84
+ - Tokens can be revoked server-side via `vslides logout`
85
+ - Only @vercel.com email addresses are authorized
86
+
87
+ ## Session Management
88
+
89
+ #### `vslides init`
90
+
91
+ Create a new presentation session. If you're logged in, the session is created immediately without OAuth. Otherwise, you'll need to authenticate in the browser.
92
+
93
+ ```bash
94
+ vslides init
95
+ # If logged in: Creates authenticated session immediately
96
+ # If not logged in: Provides AUTH_URL for browser authentication
97
+ ```
98
+
99
+ **Output files created:**
100
+ - `.vslides.json` - Session configuration (do not commit)
101
+ - `.vslides-guide.md` - Layout reference guide
102
+ - `slides.md` - Your presentation (starter template if none exists)
103
+
104
+ #### `vslides check`
105
+
106
+ Check session status. Use `--wait` to poll until the sandbox is ready.
107
+
108
+ ```bash
109
+ # Check current status
110
+ vslides check
111
+
112
+ # Wait for sandbox to be ready (60s timeout)
113
+ vslides check --wait
114
+
115
+ # Run in background
116
+ vslides check --wait &
117
+ ```
118
+
119
+ #### `vslides preview`
120
+
121
+ Show or open the preview URL.
122
+
123
+ ```bash
124
+ # Print preview URL
125
+ vslides preview
126
+
127
+ # Open in browser
128
+ vslides preview --open
129
+ ```
130
+
131
+ ## Content Management
132
+
133
+ #### `vslides push`
134
+
135
+ Upload your local `slides.md` to the server.
136
+
137
+ ```bash
138
+ # Push changes
139
+ vslides push
140
+
141
+ # Force push (bypass version check)
142
+ vslides push --force
143
+ ```
144
+
145
+ #### `vslides get`
146
+
147
+ Download the current slides from the server.
148
+
149
+ ```bash
150
+ vslides get
151
+ ```
152
+
153
+ #### `vslides sync`
154
+
155
+ Smart bidirectional sync. Checks for remote changes before pushing.
156
+
157
+ ```bash
158
+ vslides sync
159
+ ```
160
+
161
+ #### `vslides guide`
162
+
163
+ Print the slide layout guide.
164
+
165
+ ```bash
166
+ # Print cached guide
167
+ vslides guide
168
+
169
+ # Force refresh from server
170
+ vslides guide --refresh
171
+ ```
172
+
173
+ ## Collaboration
174
+
175
+ #### `vslides share`
176
+
177
+ Get a join URL for collaborators.
178
+
179
+ ```bash
180
+ vslides share
181
+ # Output: JOIN_URL: https://...
182
+ ```
183
+
184
+ #### `vslides join <url>`
185
+
186
+ Join a shared session.
187
+
188
+ ```bash
189
+ vslides join https://slidev-server.vercel.app/join/abc123
190
+ ```
191
+
192
+ ## Assets
193
+
194
+ #### `vslides upload <file>`
195
+
196
+ Upload an image or media file.
197
+
198
+ ```bash
199
+ vslides upload logo.png
200
+ # Output: Use in slides as:
201
+ # image: /assets/logo.png
202
+ ```
203
+
204
+ Supported formats: `.jpg`, `.jpeg`, `.png`, `.gif`, `.svg`, `.webp`, `.ico`
205
+
206
+ ## Export
207
+
208
+ #### `vslides export <format>`
209
+
210
+ Export presentation to PDF or PPTX.
211
+
212
+ ```bash
213
+ vslides export pdf
214
+ vslides export pptx
215
+ ```
216
+
217
+ ## Version History
218
+
219
+ #### `vslides history`
220
+
221
+ List saved versions.
222
+
223
+ ```bash
224
+ vslides history
225
+ # Output:
226
+ # VERSION TIMESTAMP
227
+ # 3 2026-01-28 10:30:00
228
+ # 2 2026-01-28 10:15:00
229
+ # 1 2026-01-28 10:00:00
230
+ ```
231
+
232
+ #### `vslides revert <version>`
233
+
234
+ Revert to a previous version.
235
+
236
+ ```bash
237
+ vslides revert 2
238
+ ```
239
+
240
+ ## Configuration
241
+
242
+ ### Environment Variables
243
+
244
+ | Variable | Description | Default |
245
+ |----------|-------------|---------|
246
+ | `VSLIDES_API_URL` | API server URL | `https://slidev-server.vercel.app` |
247
+
248
+ ### Files
249
+
250
+ | File | Location | Description |
251
+ |------|----------|-------------|
252
+ | `auth.json` | `~/.vslides/` | Cached authentication credentials |
253
+ | `.vslides.json` | Project directory | Session configuration |
254
+ | `.vslides-guide.md` | Project directory | Layout reference guide |
255
+ | `slides.md` | Project directory | Your presentation |
256
+
257
+ ## Exit Codes
258
+
259
+ | Code | Meaning |
260
+ |------|---------|
261
+ | 0 | Success |
262
+ | 1 | Conflict (version mismatch, not running) |
263
+ | 2 | Authentication required |
264
+ | 3 | Network error |
265
+ | 4 | Validation error |
266
+
267
+ ## Troubleshooting
268
+
269
+ ### "Session expired" error
270
+
271
+ Your authentication has expired. Run:
272
+
273
+ ```bash
274
+ vslides login
275
+ vslides init
276
+ ```
277
+
278
+ ### "Not authenticated" error
279
+
280
+ You need to complete the OAuth flow:
281
+
282
+ ```bash
283
+ vslides check --wait &
284
+ # Open AUTH_URL in browser and authenticate
285
+ ```
286
+
287
+ ### Version conflict
288
+
289
+ Another collaborator pushed changes. Run:
290
+
291
+ ```bash
292
+ vslides get
293
+ # Review changes in slides.md
294
+ vslides push
295
+ ```
296
+
297
+ Or use sync for automatic handling:
298
+
299
+ ```bash
300
+ vslides sync
301
+ ```
302
+
303
+ ### Sandbox timeout
304
+
305
+ Sessions expire after 45 minutes of inactivity. Create a new session:
306
+
307
+ ```bash
308
+ rm .vslides.json
309
+ vslides init
310
+ ```
311
+
312
+ ## Slide Format
313
+
314
+ Slides use Markdown with YAML frontmatter:
315
+
316
+ ```markdown
317
+ ---
318
+ title: My Presentation
319
+ ---
320
+
321
+ ---
322
+ layout: 1-title
323
+ variant: title
324
+ ---
325
+
326
+ # Welcome
327
+
328
+ Your presentation starts here
329
+
330
+ ---
331
+ layout: 2-statement
332
+ variant: large
333
+ ---
334
+
335
+ # Make a Statement
336
+
337
+ This is where your content goes
338
+ ```
339
+
340
+ Run `vslides guide` for the complete layout reference.
package/dist/cli.js CHANGED
@@ -3074,6 +3074,65 @@ var {
3074
3074
  Help
3075
3075
  } = import_index.default;
3076
3076
 
3077
+ // src/lib/errors.ts
3078
+ var AuthExpiredError = class extends Error {
3079
+ constructor(message = "Session expired. Please run `vslides login` to re-authenticate.") {
3080
+ super(message);
3081
+ this.name = "AuthExpiredError";
3082
+ }
3083
+ };
3084
+
3085
+ // src/lib/cli-auth.ts
3086
+ var import_node_fs = require("node:fs");
3087
+ var import_node_path = require("node:path");
3088
+ var import_node_os = require("node:os");
3089
+ var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".vslides");
3090
+ var AUTH_FILE = (0, import_node_path.join)(AUTH_DIR, "auth.json");
3091
+ function ensureAuthDir() {
3092
+ if (!(0, import_node_fs.existsSync)(AUTH_DIR)) {
3093
+ (0, import_node_fs.mkdirSync)(AUTH_DIR, { mode: 448 });
3094
+ }
3095
+ }
3096
+ function getCachedAuth() {
3097
+ if (!(0, import_node_fs.existsSync)(AUTH_FILE)) {
3098
+ return null;
3099
+ }
3100
+ try {
3101
+ const content = (0, import_node_fs.readFileSync)(AUTH_FILE, "utf-8");
3102
+ const auth = JSON.parse(content);
3103
+ if (auth.version !== 1 || !auth.token || !auth.email || !auth.expiresAt) {
3104
+ return null;
3105
+ }
3106
+ return auth;
3107
+ } catch {
3108
+ return null;
3109
+ }
3110
+ }
3111
+ function isAuthValid(auth) {
3112
+ return Date.now() < auth.expiresAt - 5 * 60 * 1e3;
3113
+ }
3114
+ function saveCLIAuth(token, email, expiresAt) {
3115
+ ensureAuthDir();
3116
+ const auth = {
3117
+ version: 1,
3118
+ token,
3119
+ email,
3120
+ expiresAt
3121
+ };
3122
+ (0, import_node_fs.writeFileSync)(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", { mode: 384 });
3123
+ (0, import_node_fs.chmodSync)(AUTH_FILE, 384);
3124
+ }
3125
+ function clearCLIAuth() {
3126
+ if ((0, import_node_fs.existsSync)(AUTH_FILE)) {
3127
+ try {
3128
+ (0, import_node_fs.writeFileSync)(AUTH_FILE, "{}");
3129
+ const { unlinkSync } = require("node:fs");
3130
+ unlinkSync(AUTH_FILE);
3131
+ } catch {
3132
+ }
3133
+ }
3134
+ }
3135
+
3077
3136
  // src/lib/api.ts
3078
3137
  var DEFAULT_URL = "https://slidev-server.vercel.app";
3079
3138
  var BASE_URL = process.env.VSLIDES_API_URL || DEFAULT_URL;
@@ -3086,7 +3145,7 @@ function sleep(ms) {
3086
3145
  return new Promise((resolve) => setTimeout(resolve, ms));
3087
3146
  }
3088
3147
  async function request(path, options = {}) {
3089
- const { method = "GET", headers = {}, body } = options;
3148
+ const { method = "GET", headers = {}, body, skipAuthCheck = false } = options;
3090
3149
  for (let i = 0; i < MAX_RETRIES; i++) {
3091
3150
  const response = await fetch(BASE_URL + path, {
3092
3151
  method,
@@ -3100,6 +3159,10 @@ async function request(path, options = {}) {
3100
3159
  await sleep(RETRY_DELAY);
3101
3160
  continue;
3102
3161
  }
3162
+ if (response.status === 401 && !skipAuthCheck) {
3163
+ clearCLIAuth();
3164
+ throw new AuthExpiredError();
3165
+ }
3103
3166
  let data;
3104
3167
  const contentType = response.headers.get("content-type") || "";
3105
3168
  if (contentType.includes("application/json")) {
@@ -3111,10 +3174,16 @@ async function request(path, options = {}) {
3111
3174
  }
3112
3175
  throw new Error("Sandbox failed to wake up after 5 minutes");
3113
3176
  }
3114
- async function createSession() {
3177
+ async function createSession(options = {}) {
3178
+ const headers = {
3179
+ "Content-Type": "application/json"
3180
+ };
3181
+ if (options.cliAuthToken) {
3182
+ headers["X-CLI-Auth-Token"] = options.cliAuthToken;
3183
+ }
3115
3184
  return request("/api/session", {
3116
3185
  method: "POST",
3117
- headers: { "Content-Type": "application/json" },
3186
+ headers,
3118
3187
  body: JSON.stringify({})
3119
3188
  });
3120
3189
  }
@@ -3184,6 +3253,10 @@ async function exportSlides(slug, token, format) {
3184
3253
  "X-Session-Token": token
3185
3254
  }
3186
3255
  });
3256
+ if (response.status === 401) {
3257
+ clearCLIAuth();
3258
+ throw new AuthExpiredError();
3259
+ }
3187
3260
  if (!response.ok) {
3188
3261
  const text = await response.text();
3189
3262
  return {
@@ -3231,34 +3304,52 @@ async function revertToVersion(slug, token, version) {
3231
3304
  body: JSON.stringify({ version })
3232
3305
  });
3233
3306
  }
3307
+ async function validateCLIAuth(token) {
3308
+ return request("/api/auth/cli/validate", {
3309
+ method: "POST",
3310
+ headers: { "Content-Type": "application/json" },
3311
+ body: JSON.stringify({ token }),
3312
+ skipAuthCheck: true
3313
+ // Don't clear auth on validation failure
3314
+ });
3315
+ }
3316
+ async function revokeCLIAuth(token) {
3317
+ return request("/api/auth/cli/revoke", {
3318
+ method: "POST",
3319
+ headers: { "Content-Type": "application/json" },
3320
+ body: JSON.stringify({ token }),
3321
+ skipAuthCheck: true
3322
+ // Don't clear auth on revoke failure
3323
+ });
3324
+ }
3234
3325
 
3235
3326
  // src/lib/config.ts
3236
- var import_node_fs = require("node:fs");
3237
- var import_node_path = require("node:path");
3327
+ var import_node_fs2 = require("node:fs");
3328
+ var import_node_path2 = require("node:path");
3238
3329
  var CONFIG_FILE = ".vslides.json";
3239
3330
  var GUIDE_FILE = ".vslides-guide.md";
3240
3331
  var SLIDES_FILE = "slides.md";
3241
3332
  var UPSTREAM_FILE = "upstream.md";
3242
3333
  var GUIDE_CACHE_TTL = 24 * 60 * 60 * 1e3;
3243
3334
  function getConfigPath() {
3244
- return (0, import_node_path.join)(process.cwd(), CONFIG_FILE);
3335
+ return (0, import_node_path2.join)(process.cwd(), CONFIG_FILE);
3245
3336
  }
3246
3337
  function getGuidePath() {
3247
- return (0, import_node_path.join)(process.cwd(), GUIDE_FILE);
3338
+ return (0, import_node_path2.join)(process.cwd(), GUIDE_FILE);
3248
3339
  }
3249
3340
  function getSlidesPath() {
3250
- return (0, import_node_path.join)(process.cwd(), SLIDES_FILE);
3341
+ return (0, import_node_path2.join)(process.cwd(), SLIDES_FILE);
3251
3342
  }
3252
3343
  function getUpstreamPath() {
3253
- return (0, import_node_path.join)(process.cwd(), UPSTREAM_FILE);
3344
+ return (0, import_node_path2.join)(process.cwd(), UPSTREAM_FILE);
3254
3345
  }
3255
3346
  function readConfig() {
3256
3347
  const path = getConfigPath();
3257
- if (!(0, import_node_fs.existsSync)(path)) {
3348
+ if (!(0, import_node_fs2.existsSync)(path)) {
3258
3349
  return null;
3259
3350
  }
3260
3351
  try {
3261
- const content = (0, import_node_fs.readFileSync)(path, "utf-8");
3352
+ const content = (0, import_node_fs2.readFileSync)(path, "utf-8");
3262
3353
  return JSON.parse(content);
3263
3354
  } catch {
3264
3355
  return null;
@@ -3266,7 +3357,7 @@ function readConfig() {
3266
3357
  }
3267
3358
  function writeConfig(config) {
3268
3359
  const path = getConfigPath();
3269
- (0, import_node_fs.writeFileSync)(path, JSON.stringify(config, null, 2) + "\n");
3360
+ (0, import_node_fs2.writeFileSync)(path, JSON.stringify(config, null, 2) + "\n");
3270
3361
  }
3271
3362
  function updateConfig(updates) {
3272
3363
  const config = readConfig();
@@ -3277,18 +3368,18 @@ function updateConfig(updates) {
3277
3368
  }
3278
3369
  function readGuide() {
3279
3370
  const path = getGuidePath();
3280
- if (!(0, import_node_fs.existsSync)(path)) {
3371
+ if (!(0, import_node_fs2.existsSync)(path)) {
3281
3372
  return null;
3282
3373
  }
3283
- return (0, import_node_fs.readFileSync)(path, "utf-8");
3374
+ return (0, import_node_fs2.readFileSync)(path, "utf-8");
3284
3375
  }
3285
3376
  function writeGuide(content) {
3286
3377
  const path = getGuidePath();
3287
- (0, import_node_fs.writeFileSync)(path, content);
3378
+ (0, import_node_fs2.writeFileSync)(path, content);
3288
3379
  }
3289
3380
  function isGuideFresh() {
3290
3381
  const path = getGuidePath();
3291
- if (!(0, import_node_fs.existsSync)(path)) {
3382
+ if (!(0, import_node_fs2.existsSync)(path)) {
3292
3383
  return false;
3293
3384
  }
3294
3385
  try {
@@ -3299,22 +3390,22 @@ function isGuideFresh() {
3299
3390
  }
3300
3391
  }
3301
3392
  function slidesExist() {
3302
- return (0, import_node_fs.existsSync)(getSlidesPath());
3393
+ return (0, import_node_fs2.existsSync)(getSlidesPath());
3303
3394
  }
3304
3395
  function readSlides() {
3305
3396
  const path = getSlidesPath();
3306
- if (!(0, import_node_fs.existsSync)(path)) {
3397
+ if (!(0, import_node_fs2.existsSync)(path)) {
3307
3398
  return null;
3308
3399
  }
3309
- return (0, import_node_fs.readFileSync)(path, "utf-8");
3400
+ return (0, import_node_fs2.readFileSync)(path, "utf-8");
3310
3401
  }
3311
3402
  function writeSlides(content) {
3312
3403
  const path = getSlidesPath();
3313
- (0, import_node_fs.writeFileSync)(path, content);
3404
+ (0, import_node_fs2.writeFileSync)(path, content);
3314
3405
  }
3315
3406
  function writeUpstream(content) {
3316
3407
  const path = getUpstreamPath();
3317
- (0, import_node_fs.writeFileSync)(path, content);
3408
+ (0, import_node_fs2.writeFileSync)(path, content);
3318
3409
  }
3319
3410
  function requireConfig() {
3320
3411
  const config = readConfig();
@@ -3380,10 +3471,10 @@ var ExitCode = {
3380
3471
 
3381
3472
  // src/commands/init.ts
3382
3473
  var STARTER_SLIDES = `---
3474
+ theme: ./
3383
3475
  title: My Presentation
3384
- ---
3385
-
3386
- ---
3476
+ footerLogo: wordmark
3477
+ footerTitle: true
3387
3478
  layout: 1-title
3388
3479
  variant: title
3389
3480
  ---
@@ -3418,16 +3509,23 @@ async function init() {
3418
3509
  }
3419
3510
  return;
3420
3511
  }
3421
- const result = await createSession();
3422
- if (!result.ok) {
3423
- error(`Failed to create session: ${JSON.stringify(result.data)}`);
3424
- process.exit(ExitCode.NetworkError);
3512
+ const cachedAuth = getCachedAuth();
3513
+ if (!cachedAuth || !isAuthValid(cachedAuth)) {
3514
+ error("Not authenticated. Run `vslides login` to log in.");
3515
+ process.exit(ExitCode.AuthRequired);
3516
+ }
3517
+ const result = await createSession({ cliAuthToken: cachedAuth.token });
3518
+ if (!result.ok || !result.data.authenticated || !result.data.token) {
3519
+ clearCLIAuth();
3520
+ error("Not authenticated. Run `vslides login` to log in.");
3521
+ process.exit(ExitCode.AuthRequired);
3425
3522
  }
3426
- const { slug, pollSecret, verifyUrl, previewUrl } = result.data;
3523
+ const { slug, pollSecret, previewUrl, token } = result.data;
3427
3524
  writeConfig({
3428
3525
  slug,
3429
3526
  pollSecret,
3430
- previewUrl
3527
+ previewUrl,
3528
+ token
3431
3529
  });
3432
3530
  try {
3433
3531
  const guideResult = await getGuide();
@@ -3440,11 +3538,13 @@ async function init() {
3440
3538
  writeSlides(STARTER_SLIDES);
3441
3539
  info("Created slides.md with starter template");
3442
3540
  }
3443
- url("AUTH_URL", verifyUrl);
3541
+ url("SESSION", slug);
3542
+ info("CONFIG: .vslides.json created");
3543
+ url("USER", cachedAuth.email);
3444
3544
  url("PREVIEW_URL", previewUrl);
3445
3545
  instructions([
3446
- "Run in background: vslides check --wait &",
3447
- "Open the AUTH_URL to authenticate"
3546
+ "Run: vslides check --wait (wait for sandbox to be ready)",
3547
+ "Run: vslides push (upload your slides)"
3448
3548
  ]);
3449
3549
  }
3450
3550
 
@@ -3464,12 +3564,47 @@ async function check(options = {}) {
3464
3564
  error(`Failed to check session: ${JSON.stringify(result.data)}`);
3465
3565
  process.exit(ExitCode.NetworkError);
3466
3566
  }
3467
- const { status: sessionStatus, token } = result.data;
3567
+ const { status: sessionStatus, token, cliAuthToken, expiresAt } = result.data;
3468
3568
  if (sessionStatus === "running") {
3469
3569
  if (token) {
3470
3570
  updateConfig({ token });
3471
3571
  }
3572
+ if (cliAuthToken) {
3573
+ try {
3574
+ const validateResult = await validateCLIAuth(cliAuthToken);
3575
+ if (validateResult.ok && validateResult.data.valid && validateResult.data.email && validateResult.data.expiresAt) {
3576
+ saveCLIAuth(cliAuthToken, validateResult.data.email, validateResult.data.expiresAt);
3577
+ info(`CLI auth token saved (valid for 7 days)`);
3578
+ }
3579
+ } catch {
3580
+ }
3581
+ }
3472
3582
  status("running");
3583
+ url("SESSION", slug);
3584
+ const cachedAuth = getCachedAuth();
3585
+ if (cachedAuth?.email) {
3586
+ url("USER", cachedAuth.email);
3587
+ }
3588
+ const sessionToken = token || cfg.token;
3589
+ if (sessionToken) {
3590
+ try {
3591
+ const slidesResult = await getSlides(slug, sessionToken);
3592
+ if (slidesResult.ok && slidesResult.data.version !== void 0) {
3593
+ url("VERSION", String(slidesResult.data.version));
3594
+ }
3595
+ } catch {
3596
+ }
3597
+ }
3598
+ const previewUrl = cfg.previewUrl || `${getBaseUrl()}/slides/${slug}`;
3599
+ url("PREVIEW_URL", previewUrl);
3600
+ if (expiresAt) {
3601
+ const expiresDate = new Date(expiresAt);
3602
+ const now = Date.now();
3603
+ const remainingMs = expiresAt - now;
3604
+ const remainingMins = Math.max(0, Math.floor(remainingMs / 6e4));
3605
+ const dateStr = expiresDate.toISOString().slice(0, 16).replace("T", " ");
3606
+ url("EXPIRES", `${dateStr} (${remainingMins} min remaining)`);
3607
+ }
3473
3608
  process.exit(ExitCode.Success);
3474
3609
  }
3475
3610
  if (!options.wait) {
@@ -3489,7 +3624,7 @@ async function check(options = {}) {
3489
3624
  }
3490
3625
 
3491
3626
  // src/commands/join.ts
3492
- async function join2(urlOrCode) {
3627
+ async function join3(urlOrCode) {
3493
3628
  let code = urlOrCode;
3494
3629
  if (urlOrCode.includes("/join/")) {
3495
3630
  const match = urlOrCode.match(/\/join\/([^/?]+)/);
@@ -3754,10 +3889,16 @@ async function push(options = {}) {
3754
3889
  process.exit(ExitCode.NetworkError);
3755
3890
  }
3756
3891
  const pushResult = result.data;
3892
+ const cfg_updated = readConfig();
3757
3893
  if (pushResult.version !== void 0) {
3758
3894
  updateConfig({ version: pushResult.version });
3759
3895
  }
3760
3896
  success(`Version ${pushResult.version} saved`);
3897
+ if (pushResult.previewUrl) {
3898
+ url("PREVIEW_URL", pushResult.previewUrl);
3899
+ } else if (cfg_updated?.previewUrl) {
3900
+ url("PREVIEW_URL", cfg_updated.previewUrl);
3901
+ }
3761
3902
  }
3762
3903
 
3763
3904
  // src/commands/sync.ts
@@ -3845,26 +3986,31 @@ async function sync() {
3845
3986
  updateConfig({ version: pushResult.version });
3846
3987
  }
3847
3988
  success(`Version ${pushResult.version} saved`);
3989
+ if (pushResult.previewUrl) {
3990
+ url("PREVIEW_URL", pushResult.previewUrl);
3991
+ } else if (cfg.previewUrl) {
3992
+ url("PREVIEW_URL", cfg.previewUrl);
3993
+ }
3848
3994
  }
3849
3995
 
3850
3996
  // src/commands/upload.ts
3851
- var import_node_fs2 = require("node:fs");
3852
- var import_node_path2 = require("node:path");
3997
+ var import_node_fs3 = require("node:fs");
3998
+ var import_node_path3 = require("node:path");
3853
3999
  var ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".ico"];
3854
4000
  async function upload(file) {
3855
4001
  const { config: cfg, token } = requireToken();
3856
- if (!(0, import_node_fs2.existsSync)(file)) {
4002
+ if (!(0, import_node_fs3.existsSync)(file)) {
3857
4003
  error(`File not found: ${file}`);
3858
4004
  process.exit(ExitCode.ValidationError);
3859
4005
  }
3860
- const ext = (0, import_node_path2.extname)(file).toLowerCase();
4006
+ const ext = (0, import_node_path3.extname)(file).toLowerCase();
3861
4007
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
3862
4008
  error(`Invalid file type: ${ext}`);
3863
4009
  info(`Allowed types: ${ALLOWED_EXTENSIONS.join(", ")}`);
3864
4010
  process.exit(ExitCode.ValidationError);
3865
4011
  }
3866
- const filename = (0, import_node_path2.basename)(file);
3867
- const content = (0, import_node_fs2.readFileSync)(file);
4012
+ const filename = (0, import_node_path3.basename)(file);
4013
+ const content = (0, import_node_fs3.readFileSync)(file);
3868
4014
  const result = await uploadAsset(cfg.slug, token, filename, content);
3869
4015
  if (!result.ok) {
3870
4016
  error(`Failed to upload: ${JSON.stringify(result.data)}`);
@@ -3878,7 +4024,7 @@ async function upload(file) {
3878
4024
  }
3879
4025
 
3880
4026
  // src/commands/export.ts
3881
- var import_node_fs3 = require("node:fs");
4027
+ var import_node_fs4 = require("node:fs");
3882
4028
  async function exportSlides2(format) {
3883
4029
  if (format !== "pdf" && format !== "pptx") {
3884
4030
  error(`Invalid format: ${format}`);
@@ -3892,7 +4038,7 @@ async function exportSlides2(format) {
3892
4038
  process.exit(ExitCode.NetworkError);
3893
4039
  }
3894
4040
  const filename = result.filename || `slides.${format}`;
3895
- (0, import_node_fs3.writeFileSync)(filename, result.data);
4041
+ (0, import_node_fs4.writeFileSync)(filename, result.data);
3896
4042
  info(`EXPORTED: ${filename}`);
3897
4043
  }
3898
4044
 
@@ -3941,19 +4087,128 @@ async function revert(version) {
3941
4087
  info(`REVERTED: Now at version ${newVersion} (content from version ${revertedFrom})`);
3942
4088
  }
3943
4089
 
4090
+ // src/commands/login.ts
4091
+ var POLL_INTERVAL2 = 2e3;
4092
+ var POLL_TIMEOUT2 = 12e4;
4093
+ function sleep3(ms) {
4094
+ return new Promise((resolve) => setTimeout(resolve, ms));
4095
+ }
4096
+ async function login() {
4097
+ const cachedAuth = getCachedAuth();
4098
+ if (cachedAuth && isAuthValid(cachedAuth)) {
4099
+ const daysLeft = Math.ceil((cachedAuth.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3));
4100
+ info(`Already logged in as ${cachedAuth.email} (${daysLeft} days remaining)`);
4101
+ info("Run `vslides logout` to sign out first.");
4102
+ return;
4103
+ }
4104
+ const createResult = await createSession();
4105
+ if (!createResult.ok) {
4106
+ error(`Failed to create session: ${JSON.stringify(createResult.data)}`);
4107
+ process.exit(ExitCode.NetworkError);
4108
+ }
4109
+ const { slug, pollSecret, verifyUrl } = createResult.data;
4110
+ const authUrl = `${verifyUrl}?cliAuth=true`;
4111
+ info("Please authenticate in your browser:");
4112
+ url("AUTH_URL", authUrl);
4113
+ newline();
4114
+ info("Waiting for authentication...");
4115
+ const startTime = Date.now();
4116
+ while (true) {
4117
+ const result = await getSession(slug, pollSecret);
4118
+ if (!result.ok) {
4119
+ error(`Failed to check session: ${JSON.stringify(result.data)}`);
4120
+ process.exit(ExitCode.NetworkError);
4121
+ }
4122
+ const { status: sessionStatus, cliAuthToken } = result.data;
4123
+ if (cliAuthToken && (sessionStatus === "authenticated" || sessionStatus === "running")) {
4124
+ const validateResult = await validateCLIAuth(cliAuthToken);
4125
+ if (validateResult.ok && validateResult.data.valid && validateResult.data.email && validateResult.data.expiresAt) {
4126
+ saveCLIAuth(cliAuthToken, validateResult.data.email, validateResult.data.expiresAt);
4127
+ newline();
4128
+ success(`Logged in as ${validateResult.data.email} (valid for 7 days)`);
4129
+ process.exit(ExitCode.Success);
4130
+ }
4131
+ }
4132
+ if (Date.now() - startTime > POLL_TIMEOUT2) {
4133
+ newline();
4134
+ error("Timeout waiting for authentication");
4135
+ instructions([
4136
+ "Open the AUTH_URL in your browser",
4137
+ "Sign in with your @vercel.com account",
4138
+ "Run `vslides login` again"
4139
+ ]);
4140
+ process.exit(ExitCode.Conflict);
4141
+ }
4142
+ await sleep3(POLL_INTERVAL2);
4143
+ }
4144
+ }
4145
+
4146
+ // src/commands/logout.ts
4147
+ async function logout() {
4148
+ const cachedAuth = getCachedAuth();
4149
+ if (!cachedAuth) {
4150
+ info("Not logged in");
4151
+ return;
4152
+ }
4153
+ try {
4154
+ await revokeCLIAuth(cachedAuth.token);
4155
+ } catch {
4156
+ }
4157
+ clearCLIAuth();
4158
+ success("Logged out successfully");
4159
+ }
4160
+
4161
+ // src/commands/whoami.ts
4162
+ async function whoami(options = {}) {
4163
+ const cachedAuth = getCachedAuth();
4164
+ if (!cachedAuth) {
4165
+ info("Not logged in");
4166
+ instructions(["Run `vslides login` to authenticate"]);
4167
+ process.exit(ExitCode.AuthRequired);
4168
+ }
4169
+ if (!isAuthValid(cachedAuth)) {
4170
+ info(`Logged in as ${cachedAuth.email} (expired)`);
4171
+ instructions(["Run `vslides login` to re-authenticate"]);
4172
+ process.exit(ExitCode.AuthRequired);
4173
+ }
4174
+ if (options.validate) {
4175
+ const result = await validateCLIAuth(cachedAuth.token);
4176
+ if (!result.ok || !result.data.valid) {
4177
+ info(`Logged in as ${cachedAuth.email} (invalid - token revoked)`);
4178
+ instructions(["Run `vslides login` to re-authenticate"]);
4179
+ process.exit(ExitCode.AuthRequired);
4180
+ }
4181
+ }
4182
+ const daysLeft = Math.ceil((cachedAuth.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3));
4183
+ const expiresDate = new Date(cachedAuth.expiresAt).toLocaleDateString();
4184
+ info(`Logged in as ${cachedAuth.email}`);
4185
+ info(`Expires: ${expiresDate} (${daysLeft} day${daysLeft === 1 ? "" : "s"} remaining)`);
4186
+ }
4187
+
3944
4188
  // src/cli.ts
3945
4189
  function wrapCommand(fn) {
3946
4190
  return (...args) => {
3947
4191
  fn(...args).catch((err) => {
4192
+ if (err instanceof AuthExpiredError) {
4193
+ error(err.message);
4194
+ instructions([
4195
+ "Run `vslides login` to re-authenticate",
4196
+ "Then run `vslides init` to start a new session"
4197
+ ]);
4198
+ process.exit(ExitCode.AuthRequired);
4199
+ }
3948
4200
  error(err.message);
3949
4201
  process.exit(ExitCode.NetworkError);
3950
4202
  });
3951
4203
  };
3952
4204
  }
3953
4205
  program.name("vslides").description("CLI for Vercel Slides API").version("1.0.0");
4206
+ program.command("login").description("Authenticate with Vercel (valid for 7 days)").action(wrapCommand(login));
4207
+ program.command("logout").description("Sign out and revoke credentials").action(wrapCommand(logout));
4208
+ program.command("whoami").description("Show current authentication status").option("--validate", "Validate token with server").action(wrapCommand((options) => whoami(options)));
3954
4209
  program.command("init").description("Create a new session").action(wrapCommand(init));
3955
4210
  program.command("check").description("Check session status").option("--wait", "Poll until running (60s timeout)").action(wrapCommand((options) => check(options)));
3956
- program.command("join <url>").description("Join a shared session").action(wrapCommand(join2));
4211
+ program.command("join <url>").description("Join a shared session").action(wrapCommand(join3));
3957
4212
  program.command("share").description("Get join URL for collaborators").action(wrapCommand(share));
3958
4213
  program.command("preview").description("Print or open the preview URL").option("--open", "Open in browser").action(wrapCommand((options) => preview(options)));
3959
4214
  program.command("guide").description("Print the slide layout guide").option("--refresh", "Force fresh fetch").action(wrapCommand((options) => guide(options)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vslides",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "CLI for Vercel Slides API",
5
5
  "license": "MIT",
6
6
  "author": "Vercel",