vslides 1.0.4 → 1.0.6

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 +279 -33
  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();
@@ -3418,6 +3509,40 @@ async function init() {
3418
3509
  }
3419
3510
  return;
3420
3511
  }
3512
+ const cachedAuth = getCachedAuth();
3513
+ if (cachedAuth && isAuthValid(cachedAuth)) {
3514
+ const result2 = await createSession({ cliAuthToken: cachedAuth.token });
3515
+ if (result2.ok && result2.data.authenticated && result2.data.token) {
3516
+ const { slug: slug2, pollSecret: pollSecret2, previewUrl: previewUrl2, token } = result2.data;
3517
+ writeConfig({
3518
+ slug: slug2,
3519
+ pollSecret: pollSecret2,
3520
+ previewUrl: previewUrl2,
3521
+ token
3522
+ });
3523
+ try {
3524
+ const guideResult = await getGuide();
3525
+ if (guideResult.ok && typeof guideResult.data === "string") {
3526
+ writeGuide(guideResult.data);
3527
+ }
3528
+ } catch {
3529
+ }
3530
+ if (!slidesExist()) {
3531
+ writeSlides(STARTER_SLIDES);
3532
+ info("Created slides.md with starter template");
3533
+ }
3534
+ success(`Authenticated as ${cachedAuth.email}`);
3535
+ url("PREVIEW_URL", previewUrl2);
3536
+ newline();
3537
+ info("Sandbox is starting in the background.");
3538
+ instructions([
3539
+ "Run: vslides check --wait (wait for sandbox to be ready)",
3540
+ "Run: vslides push (upload your slides)"
3541
+ ]);
3542
+ return;
3543
+ }
3544
+ clearCLIAuth();
3545
+ }
3421
3546
  const result = await createSession();
3422
3547
  if (!result.ok) {
3423
3548
  error(`Failed to create session: ${JSON.stringify(result.data)}`);
@@ -3438,8 +3563,10 @@ async function init() {
3438
3563
  }
3439
3564
  if (!slidesExist()) {
3440
3565
  writeSlides(STARTER_SLIDES);
3566
+ info("Created slides.md with starter template");
3441
3567
  }
3442
- url("AUTH_URL", verifyUrl);
3568
+ const authUrlWithCli = `${verifyUrl}?cliAuth=true`;
3569
+ url("AUTH_URL", authUrlWithCli);
3443
3570
  url("PREVIEW_URL", previewUrl);
3444
3571
  instructions([
3445
3572
  "Run in background: vslides check --wait &",
@@ -3463,11 +3590,21 @@ async function check(options = {}) {
3463
3590
  error(`Failed to check session: ${JSON.stringify(result.data)}`);
3464
3591
  process.exit(ExitCode.NetworkError);
3465
3592
  }
3466
- const { status: sessionStatus, token } = result.data;
3593
+ const { status: sessionStatus, token, cliAuthToken } = result.data;
3467
3594
  if (sessionStatus === "running") {
3468
3595
  if (token) {
3469
3596
  updateConfig({ token });
3470
3597
  }
3598
+ if (cliAuthToken) {
3599
+ try {
3600
+ const validateResult = await validateCLIAuth(cliAuthToken);
3601
+ if (validateResult.ok && validateResult.data.valid && validateResult.data.email && validateResult.data.expiresAt) {
3602
+ saveCLIAuth(cliAuthToken, validateResult.data.email, validateResult.data.expiresAt);
3603
+ info(`CLI auth token saved (valid for 7 days)`);
3604
+ }
3605
+ } catch {
3606
+ }
3607
+ }
3471
3608
  status("running");
3472
3609
  process.exit(ExitCode.Success);
3473
3610
  }
@@ -3488,7 +3625,7 @@ async function check(options = {}) {
3488
3625
  }
3489
3626
 
3490
3627
  // src/commands/join.ts
3491
- async function join2(urlOrCode) {
3628
+ async function join3(urlOrCode) {
3492
3629
  let code = urlOrCode;
3493
3630
  if (urlOrCode.includes("/join/")) {
3494
3631
  const match = urlOrCode.match(/\/join\/([^/?]+)/);
@@ -3847,23 +3984,23 @@ async function sync() {
3847
3984
  }
3848
3985
 
3849
3986
  // src/commands/upload.ts
3850
- var import_node_fs2 = require("node:fs");
3851
- var import_node_path2 = require("node:path");
3987
+ var import_node_fs3 = require("node:fs");
3988
+ var import_node_path3 = require("node:path");
3852
3989
  var ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".ico"];
3853
3990
  async function upload(file) {
3854
3991
  const { config: cfg, token } = requireToken();
3855
- if (!(0, import_node_fs2.existsSync)(file)) {
3992
+ if (!(0, import_node_fs3.existsSync)(file)) {
3856
3993
  error(`File not found: ${file}`);
3857
3994
  process.exit(ExitCode.ValidationError);
3858
3995
  }
3859
- const ext = (0, import_node_path2.extname)(file).toLowerCase();
3996
+ const ext = (0, import_node_path3.extname)(file).toLowerCase();
3860
3997
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
3861
3998
  error(`Invalid file type: ${ext}`);
3862
3999
  info(`Allowed types: ${ALLOWED_EXTENSIONS.join(", ")}`);
3863
4000
  process.exit(ExitCode.ValidationError);
3864
4001
  }
3865
- const filename = (0, import_node_path2.basename)(file);
3866
- const content = (0, import_node_fs2.readFileSync)(file);
4002
+ const filename = (0, import_node_path3.basename)(file);
4003
+ const content = (0, import_node_fs3.readFileSync)(file);
3867
4004
  const result = await uploadAsset(cfg.slug, token, filename, content);
3868
4005
  if (!result.ok) {
3869
4006
  error(`Failed to upload: ${JSON.stringify(result.data)}`);
@@ -3877,7 +4014,7 @@ async function upload(file) {
3877
4014
  }
3878
4015
 
3879
4016
  // src/commands/export.ts
3880
- var import_node_fs3 = require("node:fs");
4017
+ var import_node_fs4 = require("node:fs");
3881
4018
  async function exportSlides2(format) {
3882
4019
  if (format !== "pdf" && format !== "pptx") {
3883
4020
  error(`Invalid format: ${format}`);
@@ -3891,7 +4028,7 @@ async function exportSlides2(format) {
3891
4028
  process.exit(ExitCode.NetworkError);
3892
4029
  }
3893
4030
  const filename = result.filename || `slides.${format}`;
3894
- (0, import_node_fs3.writeFileSync)(filename, result.data);
4031
+ (0, import_node_fs4.writeFileSync)(filename, result.data);
3895
4032
  info(`EXPORTED: ${filename}`);
3896
4033
  }
3897
4034
 
@@ -3940,19 +4077,128 @@ async function revert(version) {
3940
4077
  info(`REVERTED: Now at version ${newVersion} (content from version ${revertedFrom})`);
3941
4078
  }
3942
4079
 
4080
+ // src/commands/login.ts
4081
+ var POLL_INTERVAL2 = 2e3;
4082
+ var POLL_TIMEOUT2 = 12e4;
4083
+ function sleep3(ms) {
4084
+ return new Promise((resolve) => setTimeout(resolve, ms));
4085
+ }
4086
+ async function login() {
4087
+ const cachedAuth = getCachedAuth();
4088
+ if (cachedAuth && isAuthValid(cachedAuth)) {
4089
+ const daysLeft = Math.ceil((cachedAuth.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3));
4090
+ info(`Already logged in as ${cachedAuth.email} (${daysLeft} days remaining)`);
4091
+ info("Run `vslides logout` to sign out first.");
4092
+ return;
4093
+ }
4094
+ const createResult = await createSession();
4095
+ if (!createResult.ok) {
4096
+ error(`Failed to create session: ${JSON.stringify(createResult.data)}`);
4097
+ process.exit(ExitCode.NetworkError);
4098
+ }
4099
+ const { slug, pollSecret, verifyUrl } = createResult.data;
4100
+ const authUrl = `${verifyUrl}?cliAuth=true`;
4101
+ info("Please authenticate in your browser:");
4102
+ url("AUTH_URL", authUrl);
4103
+ newline();
4104
+ info("Waiting for authentication...");
4105
+ const startTime = Date.now();
4106
+ while (true) {
4107
+ const result = await getSession(slug, pollSecret);
4108
+ if (!result.ok) {
4109
+ error(`Failed to check session: ${JSON.stringify(result.data)}`);
4110
+ process.exit(ExitCode.NetworkError);
4111
+ }
4112
+ const { status: sessionStatus, cliAuthToken } = result.data;
4113
+ if (cliAuthToken && (sessionStatus === "authenticated" || sessionStatus === "running")) {
4114
+ const validateResult = await validateCLIAuth(cliAuthToken);
4115
+ if (validateResult.ok && validateResult.data.valid && validateResult.data.email && validateResult.data.expiresAt) {
4116
+ saveCLIAuth(cliAuthToken, validateResult.data.email, validateResult.data.expiresAt);
4117
+ newline();
4118
+ success(`Logged in as ${validateResult.data.email} (valid for 7 days)`);
4119
+ process.exit(ExitCode.Success);
4120
+ }
4121
+ }
4122
+ if (Date.now() - startTime > POLL_TIMEOUT2) {
4123
+ newline();
4124
+ error("Timeout waiting for authentication");
4125
+ instructions([
4126
+ "Open the AUTH_URL in your browser",
4127
+ "Sign in with your @vercel.com account",
4128
+ "Run `vslides login` again"
4129
+ ]);
4130
+ process.exit(ExitCode.Conflict);
4131
+ }
4132
+ await sleep3(POLL_INTERVAL2);
4133
+ }
4134
+ }
4135
+
4136
+ // src/commands/logout.ts
4137
+ async function logout() {
4138
+ const cachedAuth = getCachedAuth();
4139
+ if (!cachedAuth) {
4140
+ info("Not logged in");
4141
+ return;
4142
+ }
4143
+ try {
4144
+ await revokeCLIAuth(cachedAuth.token);
4145
+ } catch {
4146
+ }
4147
+ clearCLIAuth();
4148
+ success("Logged out successfully");
4149
+ }
4150
+
4151
+ // src/commands/whoami.ts
4152
+ async function whoami(options = {}) {
4153
+ const cachedAuth = getCachedAuth();
4154
+ if (!cachedAuth) {
4155
+ info("Not logged in");
4156
+ instructions(["Run `vslides login` to authenticate"]);
4157
+ process.exit(ExitCode.AuthRequired);
4158
+ }
4159
+ if (!isAuthValid(cachedAuth)) {
4160
+ info(`Logged in as ${cachedAuth.email} (expired)`);
4161
+ instructions(["Run `vslides login` to re-authenticate"]);
4162
+ process.exit(ExitCode.AuthRequired);
4163
+ }
4164
+ if (options.validate) {
4165
+ const result = await validateCLIAuth(cachedAuth.token);
4166
+ if (!result.ok || !result.data.valid) {
4167
+ info(`Logged in as ${cachedAuth.email} (invalid - token revoked)`);
4168
+ instructions(["Run `vslides login` to re-authenticate"]);
4169
+ process.exit(ExitCode.AuthRequired);
4170
+ }
4171
+ }
4172
+ const daysLeft = Math.ceil((cachedAuth.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3));
4173
+ const expiresDate = new Date(cachedAuth.expiresAt).toLocaleDateString();
4174
+ info(`Logged in as ${cachedAuth.email}`);
4175
+ info(`Expires: ${expiresDate} (${daysLeft} day${daysLeft === 1 ? "" : "s"} remaining)`);
4176
+ }
4177
+
3943
4178
  // src/cli.ts
3944
4179
  function wrapCommand(fn) {
3945
4180
  return (...args) => {
3946
4181
  fn(...args).catch((err) => {
4182
+ if (err instanceof AuthExpiredError) {
4183
+ error(err.message);
4184
+ instructions([
4185
+ "Run `vslides login` to re-authenticate",
4186
+ "Then run `vslides init` to start a new session"
4187
+ ]);
4188
+ process.exit(ExitCode.AuthRequired);
4189
+ }
3947
4190
  error(err.message);
3948
4191
  process.exit(ExitCode.NetworkError);
3949
4192
  });
3950
4193
  };
3951
4194
  }
3952
4195
  program.name("vslides").description("CLI for Vercel Slides API").version("1.0.0");
4196
+ program.command("login").description("Authenticate with Vercel (valid for 7 days)").action(wrapCommand(login));
4197
+ program.command("logout").description("Sign out and revoke credentials").action(wrapCommand(logout));
4198
+ program.command("whoami").description("Show current authentication status").option("--validate", "Validate token with server").action(wrapCommand((options) => whoami(options)));
3953
4199
  program.command("init").description("Create a new session").action(wrapCommand(init));
3954
4200
  program.command("check").description("Check session status").option("--wait", "Poll until running (60s timeout)").action(wrapCommand((options) => check(options)));
3955
- program.command("join <url>").description("Join a shared session").action(wrapCommand(join2));
4201
+ program.command("join <url>").description("Join a shared session").action(wrapCommand(join3));
3956
4202
  program.command("share").description("Get join URL for collaborators").action(wrapCommand(share));
3957
4203
  program.command("preview").description("Print or open the preview URL").option("--open", "Open in browser").action(wrapCommand((options) => preview(options)));
3958
4204
  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.4",
3
+ "version": "1.0.6",
4
4
  "description": "CLI for Vercel Slides API",
5
5
  "license": "MIT",
6
6
  "author": "Vercel",