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.
- package/README.md +340 -0
- package/dist/cli.js +279 -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)}`);
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
3851
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
3866
|
-
const content = (0,
|
|
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
|
|
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,
|
|
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(
|
|
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)));
|