supaslidev 0.1.4 → 0.2.0
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/app/app.config.ts +9 -0
- package/app/assets/css/main.css +90 -0
- package/app/components/AppHeader.vue +429 -0
- package/app/components/CreatePresentationDialog.vue +236 -0
- package/app/components/EmptyState.vue +37 -0
- package/app/components/ImportPresentationDialog.vue +865 -0
- package/app/components/PresentationCard.vue +343 -0
- package/app/components/PresentationListItem.vue +242 -0
- package/app/composables/useServers.ts +148 -0
- package/app/layouts/default.vue +49 -0
- package/app/pages/index.vue +542 -0
- package/dist/cli/index.js +183751 -137
- package/dist/config.d.ts +8 -0
- package/dist/config.js +16 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +3 -0
- package/dist/module.d.ts +6 -0
- package/dist/module.js +9168 -0
- package/dist/prompt.js +847 -0
- package/nuxt.config.ts +53 -0
- package/package.json +26 -19
- package/server/api/export/[id].post.ts +67 -0
- package/server/api/open-editor/[id].post.ts +28 -0
- package/server/api/presentations/import.post.ts +139 -0
- package/server/api/presentations/index.get.ts +18 -0
- package/server/api/presentations/index.post.ts +175 -0
- package/server/api/presentations/upload.post.ts +174 -0
- package/server/api/presentations/validate.post.ts +14 -0
- package/server/api/servers/[id].delete.ts +15 -0
- package/server/api/servers/[id].post.ts +17 -0
- package/server/api/servers/index.delete.ts +5 -0
- package/server/api/servers/index.get.ts +5 -0
- package/server/api/servers/stop-all.post.ts +5 -0
- package/server/plugins/generate.ts +12 -0
- package/server/plugins/shutdown.ts +16 -0
- package/server/routes/exports/[...path].get.ts +25 -0
- package/server/utils/config.ts +13 -0
- package/server/utils/process-manager.ts +119 -0
- package/src/cli/commands/create.ts +125 -0
- package/src/cli/commands/deploy.ts +90 -0
- package/src/cli/commands/dev.ts +116 -0
- package/src/cli/commands/export.ts +63 -0
- package/src/cli/commands/import.ts +178 -0
- package/src/cli/commands/present.ts +111 -0
- package/src/cli/index.ts +87 -0
- package/src/cli/utils.ts +94 -0
- package/src/config.ts +21 -0
- package/src/index.ts +2 -0
- package/src/module.ts +12 -0
- package/src/shared/catalog.ts +94 -0
- package/src/shared/copy.ts +28 -0
- package/src/shared/index.ts +29 -0
- package/{scripts/generate-presentations.mjs → src/shared/presentations.ts} +23 -46
- package/src/shared/types.ts +29 -0
- package/src/shared/validation.ts +111 -0
- package/dist/assets/index-BerY9FcI.js +0 -49
- package/dist/assets/index-CVzsY-on.css +0 -1
- package/dist/index.html +0 -24
- package/server/api.js +0 -1225
- /package/{dist → public}/apple-touch-icon.png +0 -0
- /package/{dist → public}/favicon-96x96.png +0 -0
- /package/{dist → public}/favicon.ico +0 -0
- /package/{dist → public}/favicon.svg +0 -0
- /package/{dist → public}/site.webmanifest +0 -0
- /package/{dist → public}/ssl-logo.png +0 -0
- /package/{dist → public}/web-app-manifest-192x192.png +0 -0
- /package/{dist → public}/web-app-manifest-512x512.png +0 -0
package/server/api.js
DELETED
|
@@ -1,1225 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'http';
|
|
2
|
-
import { spawn, execSync } from 'child_process';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, join, resolve, basename, extname, relative } from 'path';
|
|
5
|
-
import {
|
|
6
|
-
existsSync,
|
|
7
|
-
mkdirSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
statSync,
|
|
11
|
-
cpSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
createReadStream,
|
|
14
|
-
} from 'fs';
|
|
15
|
-
|
|
16
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
|
|
20
|
-
function resolveProjectRoot() {
|
|
21
|
-
if (process.env.SUPASLIDEV_PROJECT_ROOT) {
|
|
22
|
-
return process.env.SUPASLIDEV_PROJECT_ROOT;
|
|
23
|
-
}
|
|
24
|
-
return join(__dirname, '..', '..', '..');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function resolvePresentationsDir() {
|
|
28
|
-
if (process.env.SUPASLIDEV_PRESENTATIONS_DIR) {
|
|
29
|
-
return process.env.SUPASLIDEV_PRESENTATIONS_DIR;
|
|
30
|
-
}
|
|
31
|
-
return join(resolveProjectRoot(), 'presentations');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const projectRoot = resolveProjectRoot();
|
|
35
|
-
const presentationsDir = resolvePresentationsDir();
|
|
36
|
-
const packageDir = join(__dirname, '..');
|
|
37
|
-
const presentationsJsonPath = join(projectRoot, '.supaslidev', 'presentations.json');
|
|
38
|
-
const dashboardDir = process.env.SUPASLIDEV_DASHBOARD_DIR;
|
|
39
|
-
|
|
40
|
-
const MIME_TYPES = {
|
|
41
|
-
'.html': 'text/html',
|
|
42
|
-
'.js': 'application/javascript',
|
|
43
|
-
'.css': 'text/css',
|
|
44
|
-
'.json': 'application/json',
|
|
45
|
-
'.png': 'image/png',
|
|
46
|
-
'.jpg': 'image/jpeg',
|
|
47
|
-
'.svg': 'image/svg+xml',
|
|
48
|
-
'.ico': 'image/x-icon',
|
|
49
|
-
'.webmanifest': 'application/manifest+json',
|
|
50
|
-
'.pdf': 'application/pdf',
|
|
51
|
-
'.woff': 'font/woff',
|
|
52
|
-
'.woff2': 'font/woff2',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const runningServers = new Map();
|
|
56
|
-
|
|
57
|
-
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
58
|
-
|
|
59
|
-
const CATALOG_DEPENDENCIES = [
|
|
60
|
-
'@slidev/cli',
|
|
61
|
-
'@slidev/theme-default',
|
|
62
|
-
'@slidev/theme-seriph',
|
|
63
|
-
'@slidev/theme-apple-basic',
|
|
64
|
-
'vue',
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
function hasSharedPackage() {
|
|
68
|
-
const sharedPackagePath = join(projectRoot, 'packages', 'shared', 'package.json');
|
|
69
|
-
return existsSync(sharedPackagePath);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function addSharedAddonToSlides(slidesPath) {
|
|
73
|
-
const content = readFileSync(slidesPath, 'utf-8');
|
|
74
|
-
const frontmatterMatch = content.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
75
|
-
if (!frontmatterMatch) return;
|
|
76
|
-
|
|
77
|
-
const [fullMatch, openDelim, frontmatter, closeDelim] = frontmatterMatch;
|
|
78
|
-
const restOfFile = content.slice(fullMatch.length);
|
|
79
|
-
const sharedAddon = '@supaslidev/shared';
|
|
80
|
-
|
|
81
|
-
if (frontmatter.includes(sharedAddon)) return;
|
|
82
|
-
|
|
83
|
-
let updatedFrontmatter = frontmatter;
|
|
84
|
-
|
|
85
|
-
const addonsMatch = frontmatter.match(/^(addons:\s*)(\[.*?\])?$/m);
|
|
86
|
-
if (addonsMatch) {
|
|
87
|
-
if (addonsMatch[2]) {
|
|
88
|
-
const arrayContent = addonsMatch[2].slice(1, -1).trim();
|
|
89
|
-
if (arrayContent === '') {
|
|
90
|
-
updatedFrontmatter = frontmatter.replace(addonsMatch[0], `addons: ['${sharedAddon}']`);
|
|
91
|
-
} else {
|
|
92
|
-
updatedFrontmatter = frontmatter.replace(
|
|
93
|
-
addonsMatch[0],
|
|
94
|
-
`addons: [${arrayContent}, '${sharedAddon}']`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
const addonsBlockMatch = frontmatter.match(/^addons:\s*\n((?: - .+\n?)*)/m);
|
|
99
|
-
if (addonsBlockMatch) {
|
|
100
|
-
const existingBlock = addonsBlockMatch[0].trimEnd();
|
|
101
|
-
updatedFrontmatter = frontmatter.replace(
|
|
102
|
-
existingBlock,
|
|
103
|
-
`${existingBlock}\n - '${sharedAddon}'`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
const themeMatch = frontmatter.match(/^(theme:\s*.+)$/m);
|
|
109
|
-
if (themeMatch) {
|
|
110
|
-
updatedFrontmatter = frontmatter.replace(
|
|
111
|
-
themeMatch[1],
|
|
112
|
-
`${themeMatch[1]}\naddons:\n - '${sharedAddon}'`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (updatedFrontmatter !== frontmatter) {
|
|
118
|
-
writeFileSync(slidesPath, `${openDelim}${updatedFrontmatter}\n${closeDelim}${restOfFile}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function addSharedDependencyToPackageJson(packageJson) {
|
|
123
|
-
if (!packageJson.dependencies) {
|
|
124
|
-
packageJson.dependencies = {};
|
|
125
|
-
}
|
|
126
|
-
if (!packageJson.dependencies['@supaslidev/shared']) {
|
|
127
|
-
packageJson.dependencies['@supaslidev/shared'] = 'workspace:*';
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function convertToCatalogDependencies(dependencies) {
|
|
132
|
-
if (!dependencies || typeof dependencies !== 'object') {
|
|
133
|
-
return {};
|
|
134
|
-
}
|
|
135
|
-
const converted = { ...dependencies };
|
|
136
|
-
for (const dep of CATALOG_DEPENDENCIES) {
|
|
137
|
-
if (dep in converted) {
|
|
138
|
-
converted[dep] = 'catalog:';
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return converted;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parseFrontmatter(content) {
|
|
145
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
146
|
-
if (!frontmatterMatch) return {};
|
|
147
|
-
|
|
148
|
-
const frontmatter = frontmatterMatch[1];
|
|
149
|
-
const result = {};
|
|
150
|
-
|
|
151
|
-
let currentKey = null;
|
|
152
|
-
let currentValue = [];
|
|
153
|
-
let inMultiline = false;
|
|
154
|
-
|
|
155
|
-
const lines = frontmatter.split('\n');
|
|
156
|
-
|
|
157
|
-
for (const line of lines) {
|
|
158
|
-
if (inMultiline) {
|
|
159
|
-
if (line.match(/^[a-zA-Z]/)) {
|
|
160
|
-
result[currentKey] = currentValue.join('\n').trim();
|
|
161
|
-
inMultiline = false;
|
|
162
|
-
currentKey = null;
|
|
163
|
-
currentValue = [];
|
|
164
|
-
} else {
|
|
165
|
-
currentValue.push(line.replace(/^ /, ''));
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const match = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
171
|
-
if (match) {
|
|
172
|
-
const [, key, value] = match;
|
|
173
|
-
|
|
174
|
-
if (value === '|' || value === '>') {
|
|
175
|
-
currentKey = key;
|
|
176
|
-
currentValue = [];
|
|
177
|
-
inMultiline = true;
|
|
178
|
-
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
179
|
-
result[key] = value.slice(1, -1);
|
|
180
|
-
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
181
|
-
result[key] = value.slice(1, -1);
|
|
182
|
-
} else {
|
|
183
|
-
result[key] = value;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (inMultiline && currentKey) {
|
|
189
|
-
result[currentKey] = currentValue.join('\n').trim();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function extractDescription(info) {
|
|
196
|
-
if (!info) return '';
|
|
197
|
-
return info
|
|
198
|
-
.replace(/^##?\s+.*$/gm, '')
|
|
199
|
-
.replace(/\*\*/g, '')
|
|
200
|
-
.trim()
|
|
201
|
-
.split('\n')
|
|
202
|
-
.filter(Boolean)
|
|
203
|
-
.join(' ');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function regeneratePresentationsJson() {
|
|
207
|
-
console.log(`[regenerate] presentationsDir: ${presentationsDir}`);
|
|
208
|
-
console.log(`[regenerate] presentationsJsonPath: ${presentationsJsonPath}`);
|
|
209
|
-
|
|
210
|
-
if (!existsSync(presentationsDir)) {
|
|
211
|
-
console.log(`[regenerate] presentationsDir does not exist!`);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const allDirs = readdirSync(presentationsDir);
|
|
216
|
-
console.log(`[regenerate] All items in presentationsDir: ${allDirs.join(', ')}`);
|
|
217
|
-
|
|
218
|
-
const dirs = allDirs.filter((name) => {
|
|
219
|
-
const fullPath = join(presentationsDir, name);
|
|
220
|
-
const isDir = statSync(fullPath).isDirectory();
|
|
221
|
-
const hasSlides = existsSync(join(fullPath, 'slides.md'));
|
|
222
|
-
console.log(`[regenerate] ${name}: isDir=${isDir}, hasSlides=${hasSlides}`);
|
|
223
|
-
return isDir && hasSlides;
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const presentations = dirs
|
|
227
|
-
.map((name) => {
|
|
228
|
-
const slidesPath = join(presentationsDir, name, 'slides.md');
|
|
229
|
-
const content = readFileSync(slidesPath, 'utf-8');
|
|
230
|
-
const frontmatter = parseFrontmatter(content);
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
id: name,
|
|
234
|
-
title: frontmatter.title || name,
|
|
235
|
-
description: extractDescription(frontmatter.info) || '',
|
|
236
|
-
theme: frontmatter.theme || 'default',
|
|
237
|
-
background: frontmatter.background || '',
|
|
238
|
-
duration: frontmatter.duration || '',
|
|
239
|
-
};
|
|
240
|
-
})
|
|
241
|
-
.sort((a, b) => a.title.localeCompare(b.title));
|
|
242
|
-
|
|
243
|
-
const outputDir = dirname(presentationsJsonPath);
|
|
244
|
-
if (!existsSync(outputDir)) {
|
|
245
|
-
mkdirSync(outputDir, { recursive: true });
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
writeFileSync(presentationsJsonPath, JSON.stringify(presentations, null, 2));
|
|
249
|
-
console.log(`[regenerate] Updated presentations.json with ${presentations.length} presentations`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function isValidPresentationId(id) {
|
|
253
|
-
return typeof id === 'string' && id.length > 0 && id.length <= 100 && SLUG_REGEX.test(id);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function getNextPort() {
|
|
257
|
-
const usedPorts = new Set([...runningServers.values()].map((s) => s.port));
|
|
258
|
-
let port = 3030;
|
|
259
|
-
while (usedPorts.has(port)) {
|
|
260
|
-
port++;
|
|
261
|
-
}
|
|
262
|
-
return port;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function startServer(presentationId) {
|
|
266
|
-
if (!isValidPresentationId(presentationId)) {
|
|
267
|
-
return { success: false, error: 'Invalid presentation id' };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (runningServers.has(presentationId)) {
|
|
271
|
-
return { success: true, port: runningServers.get(presentationId).port, alreadyRunning: true };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const presentationPath = join(projectRoot, 'presentations', presentationId);
|
|
275
|
-
const slidevBin = join(presentationPath, 'node_modules', '.bin', 'slidev');
|
|
276
|
-
|
|
277
|
-
if (!existsSync(presentationPath)) {
|
|
278
|
-
return { success: false, error: 'Presentation not found' };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (!existsSync(slidevBin)) {
|
|
282
|
-
return { success: false, error: 'Dependencies not installed. Run pnpm install first.' };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const port = getNextPort();
|
|
286
|
-
|
|
287
|
-
const child = spawn(slidevBin, ['--port', String(port), '--open', 'false'], {
|
|
288
|
-
cwd: presentationPath,
|
|
289
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
290
|
-
detached: !IS_WINDOWS,
|
|
291
|
-
shell: IS_WINDOWS,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
child.unref();
|
|
295
|
-
|
|
296
|
-
runningServers.set(presentationId, { process: child, port });
|
|
297
|
-
|
|
298
|
-
child.stderr.on('data', (data) => {
|
|
299
|
-
console.error(`[${presentationId}] stderr:`, data.toString());
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
child.on('close', (code) => {
|
|
303
|
-
console.log(`[${presentationId}] process exited with code ${code}`);
|
|
304
|
-
runningServers.delete(presentationId);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
child.on('error', (err) => {
|
|
308
|
-
console.error(`Failed to start server for ${presentationId}:`, err);
|
|
309
|
-
runningServers.delete(presentationId);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
return { success: true, port, alreadyRunning: false };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function stopServer(presentationId) {
|
|
316
|
-
const server = runningServers.get(presentationId);
|
|
317
|
-
if (!server) {
|
|
318
|
-
return { success: false, error: 'Server not running' };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (IS_WINDOWS) {
|
|
322
|
-
try {
|
|
323
|
-
execSync(`taskkill /pid ${server.process.pid} /T /F`, { stdio: 'ignore' });
|
|
324
|
-
} catch {
|
|
325
|
-
// Process may have already exited
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
server.process.kill('SIGTERM');
|
|
329
|
-
}
|
|
330
|
-
runningServers.delete(presentationId);
|
|
331
|
-
return { success: true };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function stopAllServers() {
|
|
335
|
-
const stopped = [];
|
|
336
|
-
for (const [id] of runningServers) {
|
|
337
|
-
const result = stopServer(id);
|
|
338
|
-
if (result.success) {
|
|
339
|
-
stopped.push(id);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return { success: true, stopped };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function getStatus() {
|
|
346
|
-
const servers = {};
|
|
347
|
-
for (const [id, server] of runningServers) {
|
|
348
|
-
servers[id] = { port: server.port };
|
|
349
|
-
}
|
|
350
|
-
return servers;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function exportPresentation(presentationId) {
|
|
354
|
-
return new Promise((resolve) => {
|
|
355
|
-
if (!isValidPresentationId(presentationId)) {
|
|
356
|
-
resolve({ success: false, error: 'Invalid presentation id' });
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const presentationPath = join(projectRoot, 'presentations', presentationId);
|
|
361
|
-
const exportsDir = join(projectRoot, 'exports');
|
|
362
|
-
const outputPath = join(exportsDir, `${presentationId}.pdf`);
|
|
363
|
-
|
|
364
|
-
if (!existsSync(presentationPath)) {
|
|
365
|
-
resolve({ success: false, error: 'Presentation not found' });
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!existsSync(exportsDir)) {
|
|
370
|
-
mkdirSync(exportsDir, { recursive: true });
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
console.log(`[export] Starting export for ${presentationId}`);
|
|
374
|
-
console.log(`[export] Output: ${outputPath}`);
|
|
375
|
-
|
|
376
|
-
const child = spawn('npx', ['slidev', 'export', '--output', outputPath], {
|
|
377
|
-
cwd: presentationPath,
|
|
378
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
379
|
-
shell: true,
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
let stdout = '';
|
|
383
|
-
let stderr = '';
|
|
384
|
-
|
|
385
|
-
child.stdout.on('data', (data) => {
|
|
386
|
-
stdout += data.toString();
|
|
387
|
-
console.log(`[export] ${data.toString().trim()}`);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
child.stderr.on('data', (data) => {
|
|
391
|
-
stderr += data.toString();
|
|
392
|
-
console.error(`[export] ${data.toString().trim()}`);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
child.on('error', (err) => {
|
|
396
|
-
console.error(`[export] Failed to export ${presentationId}:`, err);
|
|
397
|
-
resolve({ success: false, error: `Export failed: ${err.message}` });
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
child.on('close', (code) => {
|
|
401
|
-
if (code === 0) {
|
|
402
|
-
console.log(`[export] Export complete for ${presentationId}`);
|
|
403
|
-
resolve({
|
|
404
|
-
success: true,
|
|
405
|
-
pdfPath: `/exports/${presentationId}.pdf`,
|
|
406
|
-
filename: `${presentationId}.pdf`,
|
|
407
|
-
});
|
|
408
|
-
} else {
|
|
409
|
-
console.error(`[export] Export failed with code ${code}`);
|
|
410
|
-
resolve({ success: false, error: `Export failed with exit code ${code}. ${stderr}` });
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function createPresentation({ name, title, description, template = 'default' }) {
|
|
417
|
-
return new Promise((resolve) => {
|
|
418
|
-
const presentationPath = join(presentationsDir, name);
|
|
419
|
-
|
|
420
|
-
if (existsSync(presentationPath)) {
|
|
421
|
-
resolve({
|
|
422
|
-
success: false,
|
|
423
|
-
field: 'name',
|
|
424
|
-
message: 'A presentation with this name already exists',
|
|
425
|
-
});
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (!SLUG_REGEX.test(name)) {
|
|
430
|
-
resolve({
|
|
431
|
-
success: false,
|
|
432
|
-
field: 'name',
|
|
433
|
-
message: 'Name must be a valid slug (lowercase letters, numbers, hyphens only)',
|
|
434
|
-
});
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const child = spawn('pnpm', ['create', 'slidev', name], {
|
|
439
|
-
cwd: presentationsDir,
|
|
440
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
441
|
-
shell: true,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
let stderr = '';
|
|
445
|
-
let scaffoldingDone = false;
|
|
446
|
-
let pollInterval = null;
|
|
447
|
-
const slidesPath = join(presentationPath, 'slides.md');
|
|
448
|
-
|
|
449
|
-
const checkScaffoldingComplete = () => {
|
|
450
|
-
if (existsSync(slidesPath) && !scaffoldingDone) {
|
|
451
|
-
scaffoldingDone = true;
|
|
452
|
-
if (pollInterval) clearInterval(pollInterval);
|
|
453
|
-
child.kill('SIGTERM');
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
pollInterval = setInterval(checkScaffoldingComplete, 200);
|
|
458
|
-
|
|
459
|
-
const scaffoldTimeout = setTimeout(() => {
|
|
460
|
-
if (!scaffoldingDone) {
|
|
461
|
-
if (pollInterval) clearInterval(pollInterval);
|
|
462
|
-
child.kill('SIGTERM');
|
|
463
|
-
}
|
|
464
|
-
}, 60000);
|
|
465
|
-
|
|
466
|
-
child.stderr.on('data', (data) => {
|
|
467
|
-
stderr += data.toString();
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
child.on('error', (err) => {
|
|
471
|
-
if (pollInterval) clearInterval(pollInterval);
|
|
472
|
-
clearTimeout(scaffoldTimeout);
|
|
473
|
-
resolve({ success: false, message: `Failed to create presentation: ${err.message}` });
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
child.on('close', () => {
|
|
477
|
-
if (pollInterval) clearInterval(pollInterval);
|
|
478
|
-
clearTimeout(scaffoldTimeout);
|
|
479
|
-
|
|
480
|
-
if (!existsSync(slidesPath)) {
|
|
481
|
-
resolve({ success: false, message: `Slidev CLI failed. ${stderr}` });
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
let slidesContent = readFileSync(slidesPath, 'utf-8');
|
|
486
|
-
|
|
487
|
-
slidesContent = slidesContent.replace(
|
|
488
|
-
/^(---\n[\s\S]*?)theme:\s*\S+/m,
|
|
489
|
-
`$1theme: ${template}`,
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
if (title) {
|
|
493
|
-
slidesContent = slidesContent.replace(/^(---\n[\s\S]*?)title:\s*.+$/m, `$1title: ${title}`);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (description) {
|
|
497
|
-
slidesContent = slidesContent.replace(
|
|
498
|
-
/^(---\n[\s\S]*?)info:\s*\|[\s\S]*?(?=\n[a-zA-Z]|\n---)/m,
|
|
499
|
-
`$1info: |\n ${description}\n`,
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
writeFileSync(slidesPath, slidesContent);
|
|
504
|
-
|
|
505
|
-
const sharedExists = hasSharedPackage();
|
|
506
|
-
if (sharedExists) {
|
|
507
|
-
addSharedAddonToSlides(slidesPath);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const frontmatter = parseFrontmatter(readFileSync(slidesPath, 'utf-8'));
|
|
511
|
-
|
|
512
|
-
const packageJsonPath = join(presentationPath, 'package.json');
|
|
513
|
-
const catalogPackageJson = {
|
|
514
|
-
name: `@supaslidev/${name}`,
|
|
515
|
-
private: true,
|
|
516
|
-
type: 'module',
|
|
517
|
-
scripts: {
|
|
518
|
-
build: 'slidev build',
|
|
519
|
-
dev: 'slidev --open',
|
|
520
|
-
export: 'slidev export',
|
|
521
|
-
},
|
|
522
|
-
dependencies: {
|
|
523
|
-
'@slidev/cli': 'catalog:',
|
|
524
|
-
'@slidev/theme-default': 'catalog:',
|
|
525
|
-
'@slidev/theme-seriph': 'catalog:',
|
|
526
|
-
'@slidev/theme-apple-basic': 'catalog:',
|
|
527
|
-
vue: 'catalog:',
|
|
528
|
-
},
|
|
529
|
-
devDependencies: {},
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
if (sharedExists) {
|
|
533
|
-
catalogPackageJson.dependencies['@supaslidev/shared'] = 'workspace:*';
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
writeFileSync(packageJsonPath, JSON.stringify(catalogPackageJson, null, 2) + '\n');
|
|
537
|
-
|
|
538
|
-
regeneratePresentationsJson();
|
|
539
|
-
|
|
540
|
-
const presentation = {
|
|
541
|
-
id: name,
|
|
542
|
-
title: frontmatter.title || name,
|
|
543
|
-
description: extractDescription(frontmatter.info) || '',
|
|
544
|
-
theme: template || 'default',
|
|
545
|
-
background: frontmatter.background || 'https://cover.sli.dev',
|
|
546
|
-
duration: frontmatter.duration || '',
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
const install = spawn('pnpm', ['install'], {
|
|
550
|
-
cwd: projectRoot,
|
|
551
|
-
stdio: 'inherit',
|
|
552
|
-
shell: true,
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
install.on('close', (installCode) => {
|
|
556
|
-
if (installCode !== 0) {
|
|
557
|
-
console.error(`[create] pnpm install failed with code ${installCode}`);
|
|
558
|
-
}
|
|
559
|
-
resolve({ success: true, presentation });
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
install.on('error', (err) => {
|
|
563
|
-
console.error(`[create] pnpm install error:`, err);
|
|
564
|
-
resolve({ success: true, presentation });
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const IGNORE_PATTERNS = [
|
|
571
|
-
'node_modules',
|
|
572
|
-
'.git',
|
|
573
|
-
'dist',
|
|
574
|
-
'.nuxt',
|
|
575
|
-
'.output',
|
|
576
|
-
'pnpm-lock.yaml',
|
|
577
|
-
'package-lock.json',
|
|
578
|
-
'yarn.lock',
|
|
579
|
-
'.DS_Store',
|
|
580
|
-
];
|
|
581
|
-
|
|
582
|
-
function shouldIgnore(name) {
|
|
583
|
-
return IGNORE_PATTERNS.includes(name);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function copyDirectorySelective(source, destination) {
|
|
587
|
-
mkdirSync(destination, { recursive: true });
|
|
588
|
-
const entries = readdirSync(source);
|
|
589
|
-
|
|
590
|
-
for (const entry of entries) {
|
|
591
|
-
if (shouldIgnore(entry)) {
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const sourcePath = join(source, entry);
|
|
596
|
-
const destPath = join(destination, entry);
|
|
597
|
-
const stat = statSync(sourcePath);
|
|
598
|
-
|
|
599
|
-
if (stat.isDirectory()) {
|
|
600
|
-
cpSync(sourcePath, destPath, { recursive: true });
|
|
601
|
-
} else {
|
|
602
|
-
cpSync(sourcePath, destPath);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function validateSourceDirectory(sourcePath) {
|
|
608
|
-
try {
|
|
609
|
-
if (!existsSync(sourcePath)) {
|
|
610
|
-
return { isValid: false, error: 'Source directory does not exist' };
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (!statSync(sourcePath).isDirectory()) {
|
|
614
|
-
return { isValid: false, error: 'Source path is not a directory' };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const slidesPath = join(sourcePath, 'slides.md');
|
|
618
|
-
if (!existsSync(slidesPath)) {
|
|
619
|
-
return { isValid: false, error: 'No slides.md found in source directory' };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const packageJsonPath = join(sourcePath, 'package.json');
|
|
623
|
-
if (!existsSync(packageJsonPath)) {
|
|
624
|
-
return { isValid: false, error: 'No package.json found in source directory' };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return { isValid: true };
|
|
628
|
-
} catch (err) {
|
|
629
|
-
return { isValid: false, error: `Validation error: ${err.message}` };
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function validatePath(path) {
|
|
634
|
-
const sourcePath = resolve(path);
|
|
635
|
-
const validation = validateSourceDirectory(sourcePath);
|
|
636
|
-
|
|
637
|
-
if (!validation.isValid) {
|
|
638
|
-
return {
|
|
639
|
-
path,
|
|
640
|
-
isValid: false,
|
|
641
|
-
suggestedName: null,
|
|
642
|
-
error: validation.error,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const suggestedName = basename(sourcePath)
|
|
647
|
-
.toLowerCase()
|
|
648
|
-
.replace(/[^a-z0-9-]/g, '-');
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
path,
|
|
652
|
-
isValid: true,
|
|
653
|
-
suggestedName,
|
|
654
|
-
error: null,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function validatePaths(paths) {
|
|
659
|
-
return paths.map(validatePath);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function importPresentation({ source, name }) {
|
|
663
|
-
return new Promise((resolvePromise) => {
|
|
664
|
-
const sourcePath = resolve(source);
|
|
665
|
-
const validation = validateSourceDirectory(sourcePath);
|
|
666
|
-
|
|
667
|
-
if (!validation.isValid) {
|
|
668
|
-
resolvePromise({
|
|
669
|
-
success: false,
|
|
670
|
-
field: 'source',
|
|
671
|
-
message: validation.error,
|
|
672
|
-
});
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const presentationName =
|
|
677
|
-
name ||
|
|
678
|
-
basename(sourcePath)
|
|
679
|
-
.toLowerCase()
|
|
680
|
-
.replace(/[^a-z0-9-]/g, '-');
|
|
681
|
-
|
|
682
|
-
if (!SLUG_REGEX.test(presentationName)) {
|
|
683
|
-
resolvePromise({
|
|
684
|
-
success: false,
|
|
685
|
-
field: 'name',
|
|
686
|
-
message: 'Name must be a valid slug (lowercase letters, numbers, hyphens only)',
|
|
687
|
-
});
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const destinationPath = join(presentationsDir, presentationName);
|
|
692
|
-
|
|
693
|
-
if (existsSync(destinationPath)) {
|
|
694
|
-
resolvePromise({
|
|
695
|
-
success: false,
|
|
696
|
-
field: 'name',
|
|
697
|
-
message: 'A presentation with this name already exists',
|
|
698
|
-
});
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
console.log(`[import] Importing from: ${sourcePath}`);
|
|
703
|
-
console.log(`[import] Destination: ${destinationPath}`);
|
|
704
|
-
|
|
705
|
-
copyDirectorySelective(sourcePath, destinationPath);
|
|
706
|
-
|
|
707
|
-
const sourcePackageJsonPath = join(sourcePath, 'package.json');
|
|
708
|
-
const packageJsonContent = readFileSync(sourcePackageJsonPath, 'utf-8');
|
|
709
|
-
const packageJson = JSON.parse(packageJsonContent);
|
|
710
|
-
|
|
711
|
-
packageJson.name = `@supaslidev/${presentationName}`;
|
|
712
|
-
packageJson.private = true;
|
|
713
|
-
packageJson.scripts = {
|
|
714
|
-
dev: 'slidev --open',
|
|
715
|
-
build: 'slidev build',
|
|
716
|
-
export: 'slidev export',
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
if (packageJson.dependencies) {
|
|
720
|
-
packageJson.dependencies = convertToCatalogDependencies(packageJson.dependencies);
|
|
721
|
-
}
|
|
722
|
-
if (packageJson.devDependencies) {
|
|
723
|
-
packageJson.devDependencies = convertToCatalogDependencies(packageJson.devDependencies);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const sharedExists = hasSharedPackage();
|
|
727
|
-
if (sharedExists) {
|
|
728
|
-
addSharedDependencyToPackageJson(packageJson);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
writeFileSync(
|
|
732
|
-
join(destinationPath, 'package.json'),
|
|
733
|
-
JSON.stringify(packageJson, null, 2) + '\n',
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
if (sharedExists) {
|
|
737
|
-
const slidesPath = join(destinationPath, 'slides.md');
|
|
738
|
-
if (existsSync(slidesPath)) {
|
|
739
|
-
addSharedAddonToSlides(slidesPath);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
console.log('[import] Files copied successfully');
|
|
744
|
-
console.log('[import] Running pnpm install...');
|
|
745
|
-
|
|
746
|
-
const install = spawn('pnpm', ['install'], {
|
|
747
|
-
cwd: projectRoot,
|
|
748
|
-
stdio: 'inherit',
|
|
749
|
-
shell: true,
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
install.on('close', (code) => {
|
|
753
|
-
if (code === 0) {
|
|
754
|
-
console.log('[import] pnpm install completed');
|
|
755
|
-
regeneratePresentationsJson();
|
|
756
|
-
resolvePromise({
|
|
757
|
-
success: true,
|
|
758
|
-
presentation: {
|
|
759
|
-
id: presentationName,
|
|
760
|
-
title: presentationName,
|
|
761
|
-
description: '',
|
|
762
|
-
theme: 'default',
|
|
763
|
-
background: 'https://cover.sli.dev',
|
|
764
|
-
duration: '',
|
|
765
|
-
},
|
|
766
|
-
});
|
|
767
|
-
} else {
|
|
768
|
-
console.error(`[import] pnpm install failed with code ${code}`);
|
|
769
|
-
resolvePromise({
|
|
770
|
-
success: false,
|
|
771
|
-
field: 'install',
|
|
772
|
-
message: `Failed to install dependencies (exit code ${code})`,
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
install.on('error', (err) => {
|
|
778
|
-
console.error('[import] pnpm install error:', err);
|
|
779
|
-
resolvePromise({
|
|
780
|
-
success: false,
|
|
781
|
-
field: 'install',
|
|
782
|
-
message: `Failed to install dependencies: ${err.message}`,
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function uploadPresentation({ files, name, folderName }) {
|
|
789
|
-
return new Promise((resolvePromise) => {
|
|
790
|
-
if (!Array.isArray(files) || files.length === 0) {
|
|
791
|
-
resolvePromise({
|
|
792
|
-
success: false,
|
|
793
|
-
field: 'files',
|
|
794
|
-
message: 'No files provided',
|
|
795
|
-
});
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const hasSlides = files.some((f) => f.path === 'slides.md');
|
|
800
|
-
const hasPackageJson = files.some((f) => f.path === 'package.json');
|
|
801
|
-
|
|
802
|
-
if (!hasSlides) {
|
|
803
|
-
resolvePromise({
|
|
804
|
-
success: false,
|
|
805
|
-
field: 'files',
|
|
806
|
-
message: 'No slides.md found in uploaded files',
|
|
807
|
-
});
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
if (!hasPackageJson) {
|
|
812
|
-
resolvePromise({
|
|
813
|
-
success: false,
|
|
814
|
-
field: 'files',
|
|
815
|
-
message: 'No package.json found in uploaded files',
|
|
816
|
-
});
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const presentationName =
|
|
821
|
-
name || (folderName || 'presentation').toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
822
|
-
|
|
823
|
-
if (!SLUG_REGEX.test(presentationName)) {
|
|
824
|
-
resolvePromise({
|
|
825
|
-
success: false,
|
|
826
|
-
field: 'name',
|
|
827
|
-
message: 'Name must be a valid slug (lowercase letters, numbers, hyphens only)',
|
|
828
|
-
});
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
const destinationPath = join(presentationsDir, presentationName);
|
|
833
|
-
|
|
834
|
-
if (existsSync(destinationPath)) {
|
|
835
|
-
resolvePromise({
|
|
836
|
-
success: false,
|
|
837
|
-
field: 'name',
|
|
838
|
-
message: 'A presentation with this name already exists',
|
|
839
|
-
});
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
console.log(`[upload] Creating presentation: ${presentationName}`);
|
|
844
|
-
console.log(`[upload] Destination: ${destinationPath}`);
|
|
845
|
-
console.log(`[upload] Files to write: ${files.length}`);
|
|
846
|
-
|
|
847
|
-
mkdirSync(destinationPath, { recursive: true });
|
|
848
|
-
|
|
849
|
-
for (const file of files) {
|
|
850
|
-
if (shouldIgnore(file.path.split('/')[0])) {
|
|
851
|
-
continue;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
const filePath = join(destinationPath, file.path);
|
|
855
|
-
const fileDir = dirname(filePath);
|
|
856
|
-
|
|
857
|
-
if (!existsSync(fileDir)) {
|
|
858
|
-
mkdirSync(fileDir, { recursive: true });
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (file.encoding === 'base64') {
|
|
862
|
-
writeFileSync(filePath, Buffer.from(file.content, 'base64'));
|
|
863
|
-
} else {
|
|
864
|
-
writeFileSync(filePath, file.content, 'utf-8');
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const packageJsonPath = join(destinationPath, 'package.json');
|
|
869
|
-
const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
|
|
870
|
-
const packageJson = JSON.parse(packageJsonContent);
|
|
871
|
-
|
|
872
|
-
packageJson.name = `@supaslidev/${presentationName}`;
|
|
873
|
-
packageJson.private = true;
|
|
874
|
-
packageJson.scripts = {
|
|
875
|
-
dev: 'slidev --open',
|
|
876
|
-
build: 'slidev build',
|
|
877
|
-
export: 'slidev export',
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
if (packageJson.dependencies) {
|
|
881
|
-
packageJson.dependencies = convertToCatalogDependencies(packageJson.dependencies);
|
|
882
|
-
}
|
|
883
|
-
if (packageJson.devDependencies) {
|
|
884
|
-
packageJson.devDependencies = convertToCatalogDependencies(packageJson.devDependencies);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const sharedExists = hasSharedPackage();
|
|
888
|
-
if (sharedExists) {
|
|
889
|
-
addSharedDependencyToPackageJson(packageJson);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
893
|
-
|
|
894
|
-
if (sharedExists) {
|
|
895
|
-
const slidesPath = join(destinationPath, 'slides.md');
|
|
896
|
-
if (existsSync(slidesPath)) {
|
|
897
|
-
addSharedAddonToSlides(slidesPath);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
console.log('[upload] Files written successfully');
|
|
902
|
-
console.log('[upload] Running pnpm install...');
|
|
903
|
-
|
|
904
|
-
const install = spawn('pnpm', ['install'], {
|
|
905
|
-
cwd: projectRoot,
|
|
906
|
-
stdio: 'inherit',
|
|
907
|
-
shell: true,
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
install.on('close', (code) => {
|
|
911
|
-
if (code === 0) {
|
|
912
|
-
console.log('[upload] pnpm install completed');
|
|
913
|
-
regeneratePresentationsJson();
|
|
914
|
-
resolvePromise({
|
|
915
|
-
success: true,
|
|
916
|
-
presentation: {
|
|
917
|
-
id: presentationName,
|
|
918
|
-
title: presentationName,
|
|
919
|
-
description: '',
|
|
920
|
-
theme: 'default',
|
|
921
|
-
background: 'https://cover.sli.dev',
|
|
922
|
-
duration: '',
|
|
923
|
-
},
|
|
924
|
-
});
|
|
925
|
-
} else {
|
|
926
|
-
console.error(`[upload] pnpm install failed with code ${code}`);
|
|
927
|
-
resolvePromise({
|
|
928
|
-
success: false,
|
|
929
|
-
field: 'install',
|
|
930
|
-
message: `Failed to install dependencies (exit code ${code})`,
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
install.on('error', (err) => {
|
|
936
|
-
console.error('[upload] pnpm install error:', err);
|
|
937
|
-
resolvePromise({
|
|
938
|
-
success: false,
|
|
939
|
-
field: 'install',
|
|
940
|
-
message: `Failed to install dependencies: ${err.message}`,
|
|
941
|
-
});
|
|
942
|
-
});
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
const server = createServer(async (req, res) => {
|
|
947
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
948
|
-
const path = url.pathname;
|
|
949
|
-
|
|
950
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
951
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
952
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
953
|
-
|
|
954
|
-
if (path.startsWith('/api/')) {
|
|
955
|
-
res.setHeader('Content-Type', 'application/json');
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (req.method === 'OPTIONS') {
|
|
959
|
-
res.writeHead(204);
|
|
960
|
-
res.end();
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (path === '/api/presentations' && req.method === 'GET') {
|
|
965
|
-
try {
|
|
966
|
-
if (existsSync(presentationsJsonPath)) {
|
|
967
|
-
const data = readFileSync(presentationsJsonPath, 'utf-8');
|
|
968
|
-
res.writeHead(200);
|
|
969
|
-
res.end(data);
|
|
970
|
-
} else {
|
|
971
|
-
res.writeHead(200);
|
|
972
|
-
res.end('[]');
|
|
973
|
-
}
|
|
974
|
-
} catch {
|
|
975
|
-
res.writeHead(500);
|
|
976
|
-
res.end(JSON.stringify({ error: 'Failed to read presentations' }));
|
|
977
|
-
}
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (path === '/api/servers' && req.method === 'GET') {
|
|
982
|
-
res.writeHead(200);
|
|
983
|
-
res.end(JSON.stringify(getStatus()));
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (path === '/api/servers' && req.method === 'DELETE') {
|
|
988
|
-
const result = stopAllServers();
|
|
989
|
-
res.writeHead(200);
|
|
990
|
-
res.end(JSON.stringify(result));
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
if (path === '/api/servers/stop-all' && req.method === 'POST') {
|
|
995
|
-
const result = stopAllServers();
|
|
996
|
-
res.writeHead(200);
|
|
997
|
-
res.end(JSON.stringify(result));
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (path.startsWith('/api/servers/') && req.method === 'POST') {
|
|
1002
|
-
const presentationId = path.split('/api/servers/')[1];
|
|
1003
|
-
const result = startServer(presentationId);
|
|
1004
|
-
res.writeHead(result.success ? 200 : 500);
|
|
1005
|
-
res.end(JSON.stringify(result));
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (path.startsWith('/api/servers/') && req.method === 'DELETE') {
|
|
1010
|
-
const presentationId = path.split('/api/servers/')[1];
|
|
1011
|
-
const result = stopServer(presentationId);
|
|
1012
|
-
res.writeHead(result.success ? 200 : 404);
|
|
1013
|
-
res.end(JSON.stringify(result));
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (path === '/api/presentations' && req.method === 'POST') {
|
|
1018
|
-
let body = '';
|
|
1019
|
-
req.on('data', (chunk) => {
|
|
1020
|
-
body += chunk;
|
|
1021
|
-
});
|
|
1022
|
-
req.on('end', async () => {
|
|
1023
|
-
try {
|
|
1024
|
-
const data = JSON.parse(body);
|
|
1025
|
-
const result = await createPresentation(data);
|
|
1026
|
-
if (result.success) {
|
|
1027
|
-
res.writeHead(201);
|
|
1028
|
-
res.end(JSON.stringify(result.presentation));
|
|
1029
|
-
} else {
|
|
1030
|
-
res.writeHead(400);
|
|
1031
|
-
res.end(JSON.stringify({ field: result.field, message: result.message }));
|
|
1032
|
-
}
|
|
1033
|
-
} catch {
|
|
1034
|
-
res.writeHead(400);
|
|
1035
|
-
res.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
1036
|
-
}
|
|
1037
|
-
});
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if (path === '/api/presentations/validate' && req.method === 'POST') {
|
|
1042
|
-
let body = '';
|
|
1043
|
-
req.on('data', (chunk) => {
|
|
1044
|
-
body += chunk;
|
|
1045
|
-
});
|
|
1046
|
-
req.on('end', () => {
|
|
1047
|
-
let data;
|
|
1048
|
-
try {
|
|
1049
|
-
data = JSON.parse(body);
|
|
1050
|
-
} catch {
|
|
1051
|
-
res.writeHead(400);
|
|
1052
|
-
res.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (!Array.isArray(data.paths)) {
|
|
1057
|
-
res.writeHead(400);
|
|
1058
|
-
res.end(JSON.stringify({ message: 'paths must be an array' }));
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
try {
|
|
1063
|
-
const results = validatePaths(data.paths);
|
|
1064
|
-
res.writeHead(200);
|
|
1065
|
-
res.end(JSON.stringify(results));
|
|
1066
|
-
} catch (err) {
|
|
1067
|
-
console.error('[validate] Error validating paths:', err);
|
|
1068
|
-
res.writeHead(500);
|
|
1069
|
-
res.end(JSON.stringify({ message: `Validation failed: ${err.message}` }));
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (path === '/api/presentations/import' && req.method === 'POST') {
|
|
1076
|
-
let body = '';
|
|
1077
|
-
req.on('data', (chunk) => {
|
|
1078
|
-
body += chunk;
|
|
1079
|
-
});
|
|
1080
|
-
req.on('end', async () => {
|
|
1081
|
-
try {
|
|
1082
|
-
const data = JSON.parse(body);
|
|
1083
|
-
const result = await importPresentation(data);
|
|
1084
|
-
if (result.success) {
|
|
1085
|
-
res.writeHead(201);
|
|
1086
|
-
res.end(JSON.stringify(result.presentation));
|
|
1087
|
-
} else {
|
|
1088
|
-
res.writeHead(400);
|
|
1089
|
-
res.end(JSON.stringify({ field: result.field, message: result.message }));
|
|
1090
|
-
}
|
|
1091
|
-
} catch {
|
|
1092
|
-
res.writeHead(400);
|
|
1093
|
-
res.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
1094
|
-
}
|
|
1095
|
-
});
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
if (path === '/api/presentations/upload' && req.method === 'POST') {
|
|
1100
|
-
let body = '';
|
|
1101
|
-
req.on('data', (chunk) => {
|
|
1102
|
-
body += chunk;
|
|
1103
|
-
});
|
|
1104
|
-
req.on('end', async () => {
|
|
1105
|
-
try {
|
|
1106
|
-
const data = JSON.parse(body);
|
|
1107
|
-
const result = await uploadPresentation(data);
|
|
1108
|
-
if (result.success) {
|
|
1109
|
-
res.writeHead(201);
|
|
1110
|
-
res.end(JSON.stringify(result.presentation));
|
|
1111
|
-
} else {
|
|
1112
|
-
res.writeHead(400);
|
|
1113
|
-
res.end(JSON.stringify({ field: result.field, message: result.message }));
|
|
1114
|
-
}
|
|
1115
|
-
} catch {
|
|
1116
|
-
res.writeHead(400);
|
|
1117
|
-
res.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
if (path.startsWith('/api/export/') && req.method === 'POST') {
|
|
1124
|
-
const presentationId = path.split('/api/export/')[1];
|
|
1125
|
-
const result = await exportPresentation(presentationId);
|
|
1126
|
-
res.writeHead(result.success ? 200 : 400);
|
|
1127
|
-
res.end(JSON.stringify(result));
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
if (path.startsWith('/api/open-editor/') && req.method === 'POST') {
|
|
1132
|
-
const presentationId = path.split('/api/open-editor/')[1];
|
|
1133
|
-
if (!isValidPresentationId(presentationId)) {
|
|
1134
|
-
res.writeHead(400);
|
|
1135
|
-
res.end(JSON.stringify({ success: false, error: 'Invalid presentation id' }));
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
const slidesPath = join(projectRoot, 'presentations', presentationId, 'slides.md');
|
|
1139
|
-
if (!existsSync(slidesPath)) {
|
|
1140
|
-
res.writeHead(404);
|
|
1141
|
-
res.end(JSON.stringify({ success: false, error: 'Presentation not found' }));
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
spawn('code', [slidesPath], { detached: true, stdio: 'ignore' }).unref();
|
|
1145
|
-
res.writeHead(200);
|
|
1146
|
-
res.end(JSON.stringify({ success: true }));
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
if (path.startsWith('/exports/') && req.method === 'GET') {
|
|
1151
|
-
const exportsDir = join(projectRoot, 'exports');
|
|
1152
|
-
const filePath = join(exportsDir, path.slice('/exports/'.length));
|
|
1153
|
-
const rel = relative(exportsDir, filePath);
|
|
1154
|
-
if (
|
|
1155
|
-
!rel.startsWith('..') &&
|
|
1156
|
-
filePath.startsWith(exportsDir) &&
|
|
1157
|
-
existsSync(filePath) &&
|
|
1158
|
-
filePath.endsWith('.pdf')
|
|
1159
|
-
) {
|
|
1160
|
-
res.setHeader('Content-Type', 'application/pdf');
|
|
1161
|
-
res.setHeader('Content-Disposition', `inline; filename="${basename(filePath)}"`);
|
|
1162
|
-
createReadStream(filePath).pipe(res);
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
if (dashboardDir) {
|
|
1168
|
-
const requestedFile = path === '/' ? '/index.html' : path;
|
|
1169
|
-
const filePath = join(dashboardDir, requestedFile);
|
|
1170
|
-
const normalizedPath = resolve(filePath);
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
normalizedPath.startsWith(resolve(dashboardDir)) &&
|
|
1174
|
-
existsSync(filePath) &&
|
|
1175
|
-
statSync(filePath).isFile()
|
|
1176
|
-
) {
|
|
1177
|
-
const ext = extname(filePath);
|
|
1178
|
-
res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
|
|
1179
|
-
createReadStream(filePath).pipe(res);
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
const indexPath = join(dashboardDir, 'index.html');
|
|
1184
|
-
if (existsSync(indexPath)) {
|
|
1185
|
-
res.setHeader('Content-Type', 'text/html');
|
|
1186
|
-
createReadStream(indexPath).pipe(res);
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
res.writeHead(404);
|
|
1192
|
-
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
const API_PORT = dashboardDir ? 3000 : 7777;
|
|
1196
|
-
|
|
1197
|
-
server.listen(API_PORT, () => {
|
|
1198
|
-
if (dashboardDir) {
|
|
1199
|
-
console.log(`Supaslidev dashboard: http://localhost:${API_PORT}`);
|
|
1200
|
-
const openCmd =
|
|
1201
|
-
process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1202
|
-
spawn(openCmd, [`http://localhost:${API_PORT}`], {
|
|
1203
|
-
detached: true,
|
|
1204
|
-
stdio: 'ignore',
|
|
1205
|
-
shell: process.platform === 'win32',
|
|
1206
|
-
}).unref();
|
|
1207
|
-
} else {
|
|
1208
|
-
console.log(`API server running on http://localhost:${API_PORT}`);
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
|
-
|
|
1212
|
-
process.on('SIGINT', () => {
|
|
1213
|
-
console.log('\nShutting down servers...');
|
|
1214
|
-
for (const [id] of runningServers) {
|
|
1215
|
-
stopServer(id);
|
|
1216
|
-
}
|
|
1217
|
-
process.exit(0);
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
process.on('SIGTERM', () => {
|
|
1221
|
-
for (const [id] of runningServers) {
|
|
1222
|
-
stopServer(id);
|
|
1223
|
-
}
|
|
1224
|
-
process.exit(0);
|
|
1225
|
-
});
|