vslides 1.0.5 → 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.
- package/README.md +340 -0
- package/dist/cli.js +278 -33
- 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
|
|
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
|
|
3237
|
-
var
|
|
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,
|
|
3335
|
+
return (0, import_node_path2.join)(process.cwd(), CONFIG_FILE);
|
|
3245
3336
|
}
|
|
3246
3337
|
function getGuidePath() {
|
|
3247
|
-
return (0,
|
|
3338
|
+
return (0, import_node_path2.join)(process.cwd(), GUIDE_FILE);
|
|
3248
3339
|
}
|
|
3249
3340
|
function getSlidesPath() {
|
|
3250
|
-
return (0,
|
|
3341
|
+
return (0, import_node_path2.join)(process.cwd(), SLIDES_FILE);
|
|
3251
3342
|
}
|
|
3252
3343
|
function getUpstreamPath() {
|
|
3253
|
-
return (0,
|
|
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,
|
|
3348
|
+
if (!(0, import_node_fs2.existsSync)(path)) {
|
|
3258
3349
|
return null;
|
|
3259
3350
|
}
|
|
3260
3351
|
try {
|
|
3261
|
-
const content = (0,
|
|
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,
|
|
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,
|
|
3371
|
+
if (!(0, import_node_fs2.existsSync)(path)) {
|
|
3281
3372
|
return null;
|
|
3282
3373
|
}
|
|
3283
|
-
return (0,
|
|
3374
|
+
return (0, import_node_fs2.readFileSync)(path, "utf-8");
|
|
3284
3375
|
}
|
|
3285
3376
|
function writeGuide(content) {
|
|
3286
3377
|
const path = getGuidePath();
|
|
3287
|
-
(0,
|
|
3378
|
+
(0, import_node_fs2.writeFileSync)(path, content);
|
|
3288
3379
|
}
|
|
3289
3380
|
function isGuideFresh() {
|
|
3290
3381
|
const path = getGuidePath();
|
|
3291
|
-
if (!(0,
|
|
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,
|
|
3393
|
+
return (0, import_node_fs2.existsSync)(getSlidesPath());
|
|
3303
3394
|
}
|
|
3304
3395
|
function readSlides() {
|
|
3305
3396
|
const path = getSlidesPath();
|
|
3306
|
-
if (!(0,
|
|
3397
|
+
if (!(0, import_node_fs2.existsSync)(path)) {
|
|
3307
3398
|
return null;
|
|
3308
3399
|
}
|
|
3309
|
-
return (0,
|
|
3400
|
+
return (0, import_node_fs2.readFileSync)(path, "utf-8");
|
|
3310
3401
|
}
|
|
3311
3402
|
function writeSlides(content) {
|
|
3312
3403
|
const path = getSlidesPath();
|
|
3313
|
-
(0,
|
|
3404
|
+
(0, import_node_fs2.writeFileSync)(path, content);
|
|
3314
3405
|
}
|
|
3315
3406
|
function writeUpstream(content) {
|
|
3316
3407
|
const path = getUpstreamPath();
|
|
3317
|
-
(0,
|
|
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)}`);
|
|
@@ -3440,7 +3565,8 @@ async function init() {
|
|
|
3440
3565
|
writeSlides(STARTER_SLIDES);
|
|
3441
3566
|
info("Created slides.md with starter template");
|
|
3442
3567
|
}
|
|
3443
|
-
|
|
3568
|
+
const authUrlWithCli = `${verifyUrl}?cliAuth=true`;
|
|
3569
|
+
url("AUTH_URL", authUrlWithCli);
|
|
3444
3570
|
url("PREVIEW_URL", previewUrl);
|
|
3445
3571
|
instructions([
|
|
3446
3572
|
"Run in background: vslides check --wait &",
|
|
@@ -3464,11 +3590,21 @@ async function check(options = {}) {
|
|
|
3464
3590
|
error(`Failed to check session: ${JSON.stringify(result.data)}`);
|
|
3465
3591
|
process.exit(ExitCode.NetworkError);
|
|
3466
3592
|
}
|
|
3467
|
-
const { status: sessionStatus, token } = result.data;
|
|
3593
|
+
const { status: sessionStatus, token, cliAuthToken } = result.data;
|
|
3468
3594
|
if (sessionStatus === "running") {
|
|
3469
3595
|
if (token) {
|
|
3470
3596
|
updateConfig({ token });
|
|
3471
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
|
+
}
|
|
3472
3608
|
status("running");
|
|
3473
3609
|
process.exit(ExitCode.Success);
|
|
3474
3610
|
}
|
|
@@ -3489,7 +3625,7 @@ async function check(options = {}) {
|
|
|
3489
3625
|
}
|
|
3490
3626
|
|
|
3491
3627
|
// src/commands/join.ts
|
|
3492
|
-
async function
|
|
3628
|
+
async function join3(urlOrCode) {
|
|
3493
3629
|
let code = urlOrCode;
|
|
3494
3630
|
if (urlOrCode.includes("/join/")) {
|
|
3495
3631
|
const match = urlOrCode.match(/\/join\/([^/?]+)/);
|
|
@@ -3848,23 +3984,23 @@ async function sync() {
|
|
|
3848
3984
|
}
|
|
3849
3985
|
|
|
3850
3986
|
// src/commands/upload.ts
|
|
3851
|
-
var
|
|
3852
|
-
var
|
|
3987
|
+
var import_node_fs3 = require("node:fs");
|
|
3988
|
+
var import_node_path3 = require("node:path");
|
|
3853
3989
|
var ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".ico"];
|
|
3854
3990
|
async function upload(file) {
|
|
3855
3991
|
const { config: cfg, token } = requireToken();
|
|
3856
|
-
if (!(0,
|
|
3992
|
+
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
3857
3993
|
error(`File not found: ${file}`);
|
|
3858
3994
|
process.exit(ExitCode.ValidationError);
|
|
3859
3995
|
}
|
|
3860
|
-
const ext = (0,
|
|
3996
|
+
const ext = (0, import_node_path3.extname)(file).toLowerCase();
|
|
3861
3997
|
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
|
3862
3998
|
error(`Invalid file type: ${ext}`);
|
|
3863
3999
|
info(`Allowed types: ${ALLOWED_EXTENSIONS.join(", ")}`);
|
|
3864
4000
|
process.exit(ExitCode.ValidationError);
|
|
3865
4001
|
}
|
|
3866
|
-
const filename = (0,
|
|
3867
|
-
const content = (0,
|
|
4002
|
+
const filename = (0, import_node_path3.basename)(file);
|
|
4003
|
+
const content = (0, import_node_fs3.readFileSync)(file);
|
|
3868
4004
|
const result = await uploadAsset(cfg.slug, token, filename, content);
|
|
3869
4005
|
if (!result.ok) {
|
|
3870
4006
|
error(`Failed to upload: ${JSON.stringify(result.data)}`);
|
|
@@ -3878,7 +4014,7 @@ async function upload(file) {
|
|
|
3878
4014
|
}
|
|
3879
4015
|
|
|
3880
4016
|
// src/commands/export.ts
|
|
3881
|
-
var
|
|
4017
|
+
var import_node_fs4 = require("node:fs");
|
|
3882
4018
|
async function exportSlides2(format) {
|
|
3883
4019
|
if (format !== "pdf" && format !== "pptx") {
|
|
3884
4020
|
error(`Invalid format: ${format}`);
|
|
@@ -3892,7 +4028,7 @@ async function exportSlides2(format) {
|
|
|
3892
4028
|
process.exit(ExitCode.NetworkError);
|
|
3893
4029
|
}
|
|
3894
4030
|
const filename = result.filename || `slides.${format}`;
|
|
3895
|
-
(0,
|
|
4031
|
+
(0, import_node_fs4.writeFileSync)(filename, result.data);
|
|
3896
4032
|
info(`EXPORTED: ${filename}`);
|
|
3897
4033
|
}
|
|
3898
4034
|
|
|
@@ -3941,19 +4077,128 @@ async function revert(version) {
|
|
|
3941
4077
|
info(`REVERTED: Now at version ${newVersion} (content from version ${revertedFrom})`);
|
|
3942
4078
|
}
|
|
3943
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
|
+
|
|
3944
4178
|
// src/cli.ts
|
|
3945
4179
|
function wrapCommand(fn) {
|
|
3946
4180
|
return (...args) => {
|
|
3947
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
|
+
}
|
|
3948
4190
|
error(err.message);
|
|
3949
4191
|
process.exit(ExitCode.NetworkError);
|
|
3950
4192
|
});
|
|
3951
4193
|
};
|
|
3952
4194
|
}
|
|
3953
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)));
|
|
3954
4199
|
program.command("init").description("Create a new session").action(wrapCommand(init));
|
|
3955
4200
|
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(
|
|
4201
|
+
program.command("join <url>").description("Join a shared session").action(wrapCommand(join3));
|
|
3957
4202
|
program.command("share").description("Get join URL for collaborators").action(wrapCommand(share));
|
|
3958
4203
|
program.command("preview").description("Print or open the preview URL").option("--open", "Open in browser").action(wrapCommand((options) => preview(options)));
|
|
3959
4204
|
program.command("guide").description("Print the slide layout guide").option("--refresh", "Force fresh fetch").action(wrapCommand((options) => guide(options)));
|