@liustack/pptfast 0.8.0 → 0.11.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/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  resolveSpecThemeId,
11
11
  specJsonSchema,
12
12
  validateSpec
13
- } from "./chunk-HBRSZW67.js";
13
+ } from "./chunk-WCBFCHOZ.js";
14
14
  import {
15
15
  installNodePlatform
16
16
  } from "./chunk-HFRNKYZ6.js";
@@ -35,7 +35,7 @@ import {
35
35
  sniffImageFormat,
36
36
  styleJsonSchema,
37
37
  validateIr
38
- } from "./chunk-4NKLJ7OK.js";
38
+ } from "./chunk-ONO7V2SF.js";
39
39
  import {
40
40
  getPlatform
41
41
  } from "./chunk-L524YK63.js";
@@ -573,7 +573,10 @@ var JS = `
573
573
  // skills/pptfast/SKILL.md's phase-6 revision-request handling). Zero
574
574
  // network/storage \u2014 an in-memory Blob + a synthetic <a download> click,
575
575
  // the standard browser-only download pattern.
576
- exportBtn.addEventListener('click', function () {
576
+ // pptfast serve (src/cli/serve.ts) depends on this function's
577
+ // callable-and-synchronous contract and return type \u2014 think before
578
+ // changing.
579
+ function buildExportBlob() {
577
580
  var requests = []
578
581
  Object.keys(annotations).forEach(function (key) {
579
582
  var i = parseInt(key, 10)
@@ -584,7 +587,16 @@ var JS = `
584
587
  })
585
588
  var deckTitle = document.getElementById('pf-title').textContent
586
589
  var payload = { version: '1', deck: deckTitle, requests: requests }
587
- var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
590
+ return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
591
+ }
592
+ // The one sanctioned cross-module seam this file exposes (see this
593
+ // module's own doc comment) \u2014 a plain function reference, not data, so
594
+ // the caller always gets this build's live annotation state, never a
595
+ // stale snapshot.
596
+ window.__pptfastBuildExportBlob = buildExportBlob
597
+
598
+ exportBtn.addEventListener('click', function () {
599
+ var blob = buildExportBlob()
588
600
  var url = URL.createObjectURL(blob)
589
601
  var a = document.createElement('a')
590
602
  a.href = url
@@ -695,10 +707,11 @@ async function loadDeckTarget(arg, cwd, projectHit, userHit) {
695
707
  const target = await resolveDeckTarget(arg, resolveDecksDirSource(projectHit, userHit), cwd);
696
708
  if (await isDeckDirectory(target)) {
697
709
  const { ir, deckDir } = await readDeckDir(target);
698
- return { raw: ir, baseDir: deckDir, isDir: true };
710
+ return { raw: ir, baseDir: deckDir, isDir: true, resolvedTarget: deckDir };
699
711
  }
700
712
  const raw = await loadIrFile(target);
701
- return { raw, baseDir: dirname2(resolve5(target)), isDir: false };
713
+ const resolvedFile = resolve5(target);
714
+ return { raw, baseDir: dirname2(resolvedFile), isDir: false, resolvedTarget: resolvedFile };
702
715
  }
703
716
  async function runRender(irPath, opts) {
704
717
  const cwd = opts.cwd ?? process.cwd();
@@ -799,7 +812,10 @@ async function runSpecValidate(specPath) {
799
812
  }
800
813
  const spec2 = v.spec;
801
814
  const axes = resolveNarrative(spec2.narrative);
802
- return `OK \u2014 ${spec2.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme "${resolveSpecThemeId(spec2)}"`;
815
+ const ok = `OK \u2014 ${spec2.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme "${resolveSpecThemeId(spec2)}"`;
816
+ const aliasNote = normalizedNote(v.normalized);
817
+ return aliasNote ? `${ok}
818
+ ${aliasNote}` : ok;
803
819
  }
804
820
  function runSchema(mode) {
805
821
  const schema = mode === "style" ? styleJsonSchema() : mode === "spec" ? specJsonSchema() : irJsonSchema();
@@ -850,50 +866,61 @@ async function runInit(cwd = process.cwd()) {
850
866
  }
851
867
  return `wrote ${target} \u2014 themes: \`pptfast themes\`, style schema: \`pptfast schema --style\``;
852
868
  }
853
- async function runPreview(irPath, outDir, opts = {}) {
869
+ async function renderDeckSlides(target, opts = {}) {
854
870
  const cwd = opts.cwd ?? process.cwd();
855
871
  const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
856
- const { raw, baseDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
872
+ const { raw, baseDir, isDir, resolvedTarget } = await loadDeckTarget(target, cwd, projectHit, userHit);
857
873
  await applyDeckConfig(raw, { cwd, projectHit, userHit });
858
874
  const v = validateIr(raw);
859
875
  if (!v.ok) throw new PptfastError(`invalid IR:
860
876
  ${formatIssues(v.errors)}`);
861
877
  await resolveLocalAssets(v.ir, baseDir);
862
- await mkdir2(outDir, { recursive: true });
863
878
  const ir = v.ir;
864
- const svgs = [];
879
+ const svgs = ir.slides.map((_, i) => renderSlideSvg(ir, i));
880
+ return { ir, svgs, resolvedTarget, isDir, normalized: v.normalized };
881
+ }
882
+ function buildDeckAuditAndHtml(ir, svgs) {
883
+ const hasPlaceholder = ir.slides.some((slide) => slide.placeholder);
884
+ const auditReport = hasPlaceholder ? void 0 : auditDeck(ir);
885
+ const findings = auditReport?.findings ?? [];
886
+ const html = buildPreviewHtml({
887
+ title: ir.filename,
888
+ slides: ir.slides.map((slide, i) => ({
889
+ index: i,
890
+ id: slide.id,
891
+ type: slide.type,
892
+ svg: svgs[i],
893
+ placeholder: slide.placeholder
894
+ })),
895
+ findings: findings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),
896
+ auditNote: hasPlaceholder ? "audit overlay skipped \u2014 deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings" : void 0,
897
+ checks: auditReport?.checks
898
+ });
899
+ return { html, findings, checks: auditReport?.checks };
900
+ }
901
+ async function buildDeckPreview(target, opts = {}) {
902
+ const rendered = await renderDeckSlides(target, opts);
903
+ const { html, findings, checks } = buildDeckAuditAndHtml(rendered.ir, rendered.svgs);
904
+ return { ...rendered, html, findings, checks };
905
+ }
906
+ async function runPreview(irPath, outDir, opts = {}) {
907
+ const { ir, svgs, normalized } = await renderDeckSlides(irPath, { cwd: opts.cwd });
908
+ await mkdir2(outDir, { recursive: true });
865
909
  for (let i = 0; i < ir.slides.length; i++) {
866
- const svg = renderSlideSvg(ir, i);
867
- svgs.push(svg);
868
910
  const name = `${String(i + 1).padStart(3, "0")}-${ir.slides[i].type}.svg`;
869
- await writeFile2(join4(outDir, name), svg);
911
+ await writeFile2(join4(outDir, name), svgs[i]);
870
912
  }
871
913
  const ok = `wrote ${ir.slides.length} SVG files to ${outDir}`;
872
914
  const notes = [];
873
- const aliasNote = normalizedNote(v.normalized);
915
+ const aliasNote = normalizedNote(normalized);
874
916
  if (aliasNote) notes.push(aliasNote);
875
917
  if (opts.htmlOut) {
918
+ const { html, findings } = buildDeckAuditAndHtml(ir, svgs);
876
919
  const htmlPath = join4(outDir, "preview.html");
877
- const hasPlaceholder = ir.slides.some((slide) => slide.placeholder);
878
- const auditReport = hasPlaceholder ? void 0 : auditDeck(ir);
879
- const auditFindings = auditReport?.findings ?? [];
880
- const html = buildPreviewHtml({
881
- title: ir.filename,
882
- slides: ir.slides.map((slide, i) => ({
883
- index: i,
884
- id: slide.id,
885
- type: slide.type,
886
- svg: svgs[i],
887
- placeholder: slide.placeholder
888
- })),
889
- findings: auditFindings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),
890
- auditNote: hasPlaceholder ? "audit overlay skipped \u2014 deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings" : void 0,
891
- checks: auditReport?.checks
892
- });
893
920
  await writeFile2(htmlPath, html);
894
921
  notes.push(`note: wrote self-contained preview to ${htmlPath}`);
895
- if (auditFindings.length > 0) {
896
- notes.push(`note: audit found ${auditFindings.length} finding${auditFindings.length === 1 ? "" : "s"} \u2014 see preview.html`);
922
+ if (findings.length > 0) {
923
+ notes.push(`note: audit found ${findings.length} finding${findings.length === 1 ? "" : "s"} \u2014 see preview.html`);
897
924
  }
898
925
  }
899
926
  return notes.length > 0 ? `${ok}
@@ -1040,6 +1067,341 @@ ${detail}`);
1040
1067
  return `wrote ${outPath} (migrated IR v3 \u2192 v4)`;
1041
1068
  }
1042
1069
 
1070
+ // src/cli/serve.ts
1071
+ import { spawn } from "child_process";
1072
+ import { randomUUID } from "crypto";
1073
+ import { watch } from "fs";
1074
+ import { rename, unlink, writeFile as writeFile3 } from "fs/promises";
1075
+ import { createServer } from "http";
1076
+ import { platform as osPlatform } from "os";
1077
+ import { dirname as dirname3, join as join5 } from "path";
1078
+ var DEFAULT_PORT = 4400;
1079
+ var DEBOUNCE_MS = 200;
1080
+ var HEARTBEAT_MS = 3e4;
1081
+ var REVISION_REQUEST_FILENAME = "revision-request.json";
1082
+ var MAX_REVISION_REQUEST_BYTES = 1024 * 1024;
1083
+ function watchRoots(resolvedTarget, isDir) {
1084
+ if (!isDir) return [resolvedTarget];
1085
+ return [join5(resolvedTarget, SPEC_FILENAME), join5(resolvedTarget, PAGES_DIRNAME), join5(resolvedTarget, ASSETS_DIRNAME)];
1086
+ }
1087
+ var SERVE_CLIENT_SCRIPT_ID = "pptfast-serve-client";
1088
+ var SERVE_CLIENT_JS = `
1089
+ (function () {
1090
+ // Each of the two jobs below is independently wrapped (own function, own
1091
+ // try/catch at its call site at the bottom) so a failure in one \u2014 an
1092
+ // EventSource construction that throws in some unusual embedding, a
1093
+ // future buildPreviewHtml markup change that drops #pf-export-btn \u2014 can
1094
+ // never take the other down with it; a single un-isolated top-level
1095
+ // throw would otherwise abort the rest of this IIFE.
1096
+
1097
+ function setUpLiveReload() {
1098
+ var es = new EventSource('/events')
1099
+ es.addEventListener('reload', function () { location.reload() })
1100
+
1101
+ var banner = document.createElement('div')
1102
+ banner.id = 'pptfast-serve-error-banner'
1103
+ banner.setAttribute('role', 'alert')
1104
+ banner.style.cssText =
1105
+ 'display:none;position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
1106
+ 'background:#dc2626;color:#fff;font:13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;' +
1107
+ 'padding:8px 16px;text-align:center'
1108
+ document.body.appendChild(banner)
1109
+
1110
+ function showBanner(message) {
1111
+ banner.textContent = 'pptfast serve: ' + message
1112
+ banner.style.display = 'block'
1113
+ }
1114
+
1115
+ es.addEventListener('error', function (e) {
1116
+ if (!e || typeof e.data !== 'string') return // a real connection hiccup, not the server's own rebuild-failed frame
1117
+ var message = 'rebuild failed'
1118
+ try {
1119
+ var parsed = JSON.parse(e.data)
1120
+ if (parsed && typeof parsed.message === 'string') message = parsed.message
1121
+ } catch (err) {}
1122
+ showBanner(message)
1123
+ })
1124
+ }
1125
+
1126
+ function setUpRevisionRequestSubmit() {
1127
+ var originalBtn = document.getElementById('pf-export-btn')
1128
+ if (!originalBtn) return
1129
+
1130
+ var submitBtn = originalBtn.cloneNode(true)
1131
+ submitBtn.textContent = 'Submit revision request'
1132
+ originalBtn.replaceWith(submitBtn)
1133
+
1134
+ var status = document.createElement('span')
1135
+ status.id = 'pptfast-serve-submit-status'
1136
+ status.setAttribute('aria-live', 'polite')
1137
+ status.style.cssText = 'margin-left:8px;font-size:12px'
1138
+
1139
+ var downloadLink = document.createElement('a')
1140
+ downloadLink.id = 'pptfast-serve-download-fallback'
1141
+ downloadLink.href = '#'
1142
+ downloadLink.textContent = 'download a copy instead'
1143
+ downloadLink.style.cssText = 'margin-left:8px;font-size:12px;color:#2563eb'
1144
+
1145
+ submitBtn.insertAdjacentElement('afterend', status)
1146
+ status.insertAdjacentElement('afterend', downloadLink)
1147
+
1148
+ submitBtn.addEventListener('click', function () {
1149
+ if (typeof window.__pptfastBuildExportBlob !== 'function') {
1150
+ status.style.color = '#dc2626'
1151
+ status.textContent = 'failed to submit revision request (export function unavailable \u2014 try reloading the page)'
1152
+ return
1153
+ }
1154
+ status.style.color = ''
1155
+ status.textContent = 'submitting\u2026'
1156
+ // The extra Promise.resolve().then(...) wrapper (rather than calling
1157
+ // window.__pptfastBuildExportBlob() directly and chaining off its
1158
+ // result) means a synchronous throw from that call lands in the same
1159
+ // .catch below as an async rejection or a network failure \u2014 every
1160
+ // failure mode this chain can hit surfaces as the same inline
1161
+ // status-line feedback, none of them silent.
1162
+ Promise.resolve()
1163
+ .then(function () {
1164
+ return window.__pptfastBuildExportBlob()
1165
+ })
1166
+ .then(function (blob) {
1167
+ return blob.text()
1168
+ })
1169
+ .then(function (text) {
1170
+ return fetch('/revision-request', {
1171
+ method: 'POST',
1172
+ headers: { 'Content-Type': 'application/json' },
1173
+ body: text,
1174
+ })
1175
+ })
1176
+ .then(function (res) {
1177
+ if (!res.ok) throw new Error('server responded ' + res.status)
1178
+ status.style.color = '#16a34a'
1179
+ status.textContent = 'Revision request saved to the deck directory'
1180
+ })
1181
+ .catch(function (err) {
1182
+ status.style.color = '#dc2626'
1183
+ status.textContent = 'failed to submit revision request' + (err && err.message ? ' (' + err.message + ')' : '')
1184
+ })
1185
+ })
1186
+
1187
+ downloadLink.addEventListener('click', function (e) {
1188
+ e.preventDefault()
1189
+ originalBtn.click()
1190
+ })
1191
+ }
1192
+
1193
+ try {
1194
+ setUpLiveReload()
1195
+ } catch (e) {
1196
+ console.error('pptfast serve: failed to set up live reload', e)
1197
+ }
1198
+ try {
1199
+ setUpRevisionRequestSubmit()
1200
+ } catch (e) {
1201
+ console.error('pptfast serve: failed to set up revision-request submit', e)
1202
+ }
1203
+ })()
1204
+ `.trim();
1205
+ function buildServeClientScriptTag() {
1206
+ return `<script id="${SERVE_CLIENT_SCRIPT_ID}">${SERVE_CLIENT_JS}</script>`;
1207
+ }
1208
+ function injectServeClient(html) {
1209
+ if (html.includes(SERVE_CLIENT_SCRIPT_ID)) return html;
1210
+ return html.replace("</body>", `${buildServeClientScriptTag()}
1211
+ </body>`);
1212
+ }
1213
+ async function atomicWriteFile(targetPath, data) {
1214
+ const tmpPath = `${targetPath}.${randomUUID()}.tmp`;
1215
+ await writeFile3(tmpPath, data, "utf8");
1216
+ try {
1217
+ await rename(tmpPath, targetPath);
1218
+ } catch (e) {
1219
+ await unlink(tmpPath).catch(() => {
1220
+ });
1221
+ throw e;
1222
+ }
1223
+ }
1224
+ async function createServeServer(options) {
1225
+ const cwd = options.cwd ?? process.cwd();
1226
+ const requestedPort = options.port ?? DEFAULT_PORT;
1227
+ if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
1228
+ throw new PptfastError(`invalid port ${requestedPort} \u2014 expected an integer between 0 and 65535`);
1229
+ }
1230
+ const initial = await buildDeckPreview(options.target, { cwd });
1231
+ let cachedHtml = injectServeClient(initial.html);
1232
+ const sseClients = /* @__PURE__ */ new Set();
1233
+ const revisionRequestPath = initial.isDir ? join5(initial.resolvedTarget, REVISION_REQUEST_FILENAME) : join5(dirname3(initial.resolvedTarget), REVISION_REQUEST_FILENAME);
1234
+ function writeToAll(chunk) {
1235
+ for (const res of sseClients) {
1236
+ try {
1237
+ res.write(chunk);
1238
+ } catch {
1239
+ }
1240
+ }
1241
+ }
1242
+ function broadcast(event, data) {
1243
+ writeToAll(`event: ${event}
1244
+ data: ${JSON.stringify(data)}
1245
+
1246
+ `);
1247
+ }
1248
+ async function rebuild() {
1249
+ try {
1250
+ const result = await buildDeckPreview(options.target, { cwd });
1251
+ cachedHtml = injectServeClient(result.html);
1252
+ broadcast("reload", {});
1253
+ } catch (e) {
1254
+ broadcast("error", { message: e instanceof Error ? e.message : String(e) });
1255
+ }
1256
+ }
1257
+ async function handleRevisionRequestPost(req, res) {
1258
+ const chunks = [];
1259
+ let total = 0;
1260
+ try {
1261
+ for await (const chunk of req) {
1262
+ total += chunk.length;
1263
+ if (total > MAX_REVISION_REQUEST_BYTES) {
1264
+ res.writeHead(413, { "Content-Type": "text/plain; charset=utf-8" });
1265
+ res.end(`revision request body exceeds the ${MAX_REVISION_REQUEST_BYTES}-byte limit`);
1266
+ req.destroy();
1267
+ return;
1268
+ }
1269
+ chunks.push(chunk);
1270
+ }
1271
+ } catch {
1272
+ return;
1273
+ }
1274
+ const body = Buffer.concat(chunks).toString("utf8");
1275
+ try {
1276
+ JSON.parse(body);
1277
+ } catch {
1278
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1279
+ res.end("invalid JSON body");
1280
+ return;
1281
+ }
1282
+ try {
1283
+ await atomicWriteFile(revisionRequestPath, body);
1284
+ } catch (e) {
1285
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
1286
+ res.end(`failed to write ${REVISION_REQUEST_FILENAME}: ${e instanceof Error ? e.message : String(e)}`);
1287
+ return;
1288
+ }
1289
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1290
+ res.end(JSON.stringify({ ok: true }));
1291
+ }
1292
+ const server = createServer((req, res) => {
1293
+ const pathname = (req.url ?? "/").split("?")[0];
1294
+ if (req.method === "GET" && pathname === "/") {
1295
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1296
+ res.end(cachedHtml);
1297
+ return;
1298
+ }
1299
+ if (req.method === "GET" && pathname === "/events") {
1300
+ res.writeHead(200, {
1301
+ "Content-Type": "text/event-stream",
1302
+ "Cache-Control": "no-cache, no-transform",
1303
+ Connection: "keep-alive"
1304
+ });
1305
+ res.write("retry: 2000\n\n");
1306
+ sseClients.add(res);
1307
+ res.on("close", () => sseClients.delete(res));
1308
+ res.on("error", () => sseClients.delete(res));
1309
+ return;
1310
+ }
1311
+ if (pathname === "/revision-request") {
1312
+ if (req.method !== "POST") {
1313
+ res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8", Allow: "POST" });
1314
+ res.end("method not allowed \u2014 only POST is accepted on /revision-request");
1315
+ return;
1316
+ }
1317
+ void handleRevisionRequestPost(req, res);
1318
+ return;
1319
+ }
1320
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1321
+ res.end("not found");
1322
+ });
1323
+ const heartbeat = setInterval(() => writeToAll(": heartbeat\n\n"), HEARTBEAT_MS);
1324
+ let debounceTimer;
1325
+ function scheduleRebuild() {
1326
+ if (debounceTimer) clearTimeout(debounceTimer);
1327
+ debounceTimer = setTimeout(() => {
1328
+ debounceTimer = void 0;
1329
+ void rebuild();
1330
+ }, DEBOUNCE_MS);
1331
+ }
1332
+ const watchers = [];
1333
+ for (const path of watchRoots(initial.resolvedTarget, initial.isDir)) {
1334
+ try {
1335
+ watchers.push(watch(path, () => scheduleRebuild()));
1336
+ } catch (e) {
1337
+ if (e.code !== "ENOENT") throw e;
1338
+ }
1339
+ }
1340
+ function teardownWatchersAndTimers() {
1341
+ clearInterval(heartbeat);
1342
+ if (debounceTimer) clearTimeout(debounceTimer);
1343
+ for (const w of watchers) w.close();
1344
+ }
1345
+ try {
1346
+ await new Promise((resolveListen, rejectListen) => {
1347
+ const onError = (err) => {
1348
+ server.removeListener("listening", onListening);
1349
+ rejectListen(err);
1350
+ };
1351
+ const onListening = () => {
1352
+ server.removeListener("error", onError);
1353
+ resolveListen();
1354
+ };
1355
+ server.once("error", onError);
1356
+ server.once("listening", onListening);
1357
+ server.listen(requestedPort, "127.0.0.1");
1358
+ });
1359
+ } catch (e) {
1360
+ teardownWatchersAndTimers();
1361
+ if (e.code === "EADDRINUSE") {
1362
+ throw new PptfastError(`port ${requestedPort} is already in use \u2014 pick a different one with --port`);
1363
+ }
1364
+ throw e;
1365
+ }
1366
+ const address = server.address();
1367
+ const actualPort = typeof address === "object" && address !== null ? address.port : requestedPort;
1368
+ let closed = false;
1369
+ async function close() {
1370
+ if (closed) return;
1371
+ closed = true;
1372
+ teardownWatchersAndTimers();
1373
+ for (const res of sseClients) res.end();
1374
+ sseClients.clear();
1375
+ if (typeof server.closeIdleConnections === "function") server.closeIdleConnections();
1376
+ if (typeof server.closeAllConnections === "function") server.closeAllConnections();
1377
+ await new Promise((resolveClose, rejectClose) => {
1378
+ server.close((err) => err ? rejectClose(err) : resolveClose());
1379
+ });
1380
+ }
1381
+ return { server, rebuild, close, url: `http://127.0.0.1:${actualPort}`, port: actualPort };
1382
+ }
1383
+ function openBrowser(url) {
1384
+ const command = osPlatform() === "darwin" ? "open" : "xdg-open";
1385
+ try {
1386
+ const child = spawn(command, [url], { stdio: "ignore", detached: true });
1387
+ child.on("error", () => {
1388
+ });
1389
+ child.unref();
1390
+ } catch {
1391
+ }
1392
+ }
1393
+ async function runServe(target, opts = {}) {
1394
+ const handle = await createServeServer({ target, port: opts.port, cwd: opts.cwd });
1395
+ console.log(`pptfast serve: ${handle.url} (Ctrl+C to stop)`);
1396
+ if (opts.open !== false) openBrowser(handle.url);
1397
+ process.on("SIGINT", () => {
1398
+ void handle.close().then(
1399
+ () => process.exit(0),
1400
+ () => process.exit(1)
1401
+ );
1402
+ });
1403
+ }
1404
+
1043
1405
  // src/cli/update.ts
1044
1406
  import { execFile } from "child_process";
1045
1407
  var PACKAGE_NAME = "@liustack/pptfast";
@@ -1211,6 +1573,20 @@ program.command("preview").description("Render each slide to an SVG file for vis
1211
1573
  fail(e);
1212
1574
  }
1213
1575
  });
1576
+ program.command("serve").description("Serve a live-reloading HTML preview of an IR JSON file, deck project directory, or bare deck name over HTTP").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").option("--port <number>", `port to listen on (default ${DEFAULT_PORT})`).option("--no-open", "do not open the URL in a browser after starting").action(async (target, opts) => {
1577
+ try {
1578
+ let port;
1579
+ if (opts.port !== void 0) {
1580
+ port = Number(opts.port);
1581
+ if (!Number.isInteger(port)) {
1582
+ fail(new Error(`invalid --port value "${opts.port}" \u2014 expected an integer`));
1583
+ }
1584
+ }
1585
+ await runServe(target, { port, open: opts.open });
1586
+ } catch (e) {
1587
+ fail(e);
1588
+ }
1589
+ });
1214
1590
  program.command("check-update").description("Check npm for a newer pptfast release").action(async () => {
1215
1591
  const info = await checkForUpdate({ currentVersion: VERSION });
1216
1592
  if (!info.checked) fail(new Error(`update check failed: ${info.error}`));