slidemind 0.1.5 → 0.1.7
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/.claude-plugin/plugin.json +1 -7
- package/CLAUDE.md +22 -14
- package/README.md +33 -15
- package/bin/slidemind.mjs +1124 -34
- package/package.json +2 -5
- package/skills/create-slides/SKILL.md +95 -47
- package/skills/search/SKILL.md +29 -26
- package/src/bridge/build-and-push.mjs +353 -66
- package/src/bridge/bundle.mjs +49 -7
- package/src/bridge/capture.mjs +2 -2
- package/src/bridge/client.mjs +572 -43
- package/src/bridge/engine-download.mjs +89 -0
- package/src/{mcp → bridge}/recolor.mjs +10 -9
- package/src/builders/build.mjs +209 -13
- package/src/builders/lint.mjs +2 -2
- package/src/builders/review-overlay.js +22 -7
- package/src/builders/review.mjs +8 -15
- package/src/installers/claude.mjs +10 -17
- package/src/installers/codex.mjs +41 -36
- package/src/installers/opencode.mjs +10 -18
- package/src/lib/reused-slides.mjs +2 -4
- package/src/mcp/server.mjs +0 -810
package/bin/slidemind.mjs
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Subcommands:
|
|
5
5
|
// slidemind init [--for <target>] set up slidemind in a coding harness
|
|
6
|
-
// (
|
|
6
|
+
// (deploy the skill);
|
|
7
7
|
// targets: claude | codex | opencode | all
|
|
8
8
|
// (`install` is a back-compat alias)
|
|
9
9
|
// slidemind new <name> scaffold ./decks/<name>/{plan.md,slides/}
|
|
10
10
|
// slidemind build <deck> build self-contained HTML deck
|
|
11
11
|
// slidemind review <deck> spatial preview-review gate (blocks for approve/comments)
|
|
12
12
|
// slidemind build-pptx <deck> render the deck as native .pptx (via the add-in)
|
|
13
|
+
// slidemind view-draft <deck> render the deck's slides to per-slide PNGs (no push)
|
|
13
14
|
// slidemind uninstall --for <target>
|
|
14
15
|
// slidemind update [--for <target>] [--ref <version>]
|
|
15
16
|
// npm install -g slidemind@latest + redeploy
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
|
|
22
23
|
import { spawn } from "node:child_process";
|
|
23
24
|
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
24
26
|
import path from "node:path";
|
|
25
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
28
|
|
|
@@ -43,24 +45,46 @@ function printHelp() {
|
|
|
43
45
|
`slidemind ${pkg.version} — drive the slidemind PowerPoint add-in to build native, on-brand decks\n` +
|
|
44
46
|
`\n` +
|
|
45
47
|
`Usage:\n` +
|
|
46
|
-
` slidemind
|
|
48
|
+
` slidemind ready [deck] One-command session setup — run this FIRST each session.\n` +
|
|
49
|
+
` Verifies pairing/connection/CLI version, resolves the open\n` +
|
|
50
|
+
` design, materializes its bundle, and (with a deck) binds it.\n` +
|
|
51
|
+
` Exit 0 = ready; non-zero prints one actionable blocker.\n` +
|
|
52
|
+
` --to <presId> bind to a specific open window\n` +
|
|
53
|
+
` --force re-download the bundle even if current\n` +
|
|
54
|
+
` --connect-only check the connection only (no design/bundle)\n` +
|
|
55
|
+
` --json machine-readable readiness payload\n` +
|
|
56
|
+
` slidemind new <name> Scaffold ./decks/<name>/ (auto-binds to the open design)\n` +
|
|
57
|
+
` --to <presId> bind to a specific open window\n` +
|
|
58
|
+
` --no-bind skip the auto-bind\n` +
|
|
47
59
|
` slidemind build <deck> Build self-contained HTML\n` +
|
|
48
60
|
` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
|
|
49
61
|
` approves or leaves comments (host-free)\n` +
|
|
50
|
-
` --no-open don't auto-open the browser\n` +
|
|
51
62
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
52
63
|
` slidemind build-pptx <deck> Export native .pptx via the add-in (requires an open host)\n` +
|
|
53
64
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
54
65
|
` --out <path> output path (default <deck>/<deck>.pptx)\n` +
|
|
66
|
+
` slidemind view-draft <deck> Render the deck's slides to per-slide PNGs (no push, no .pptx)\n` +
|
|
67
|
+
` so the agent can look before review; host-free once the\n` +
|
|
68
|
+
` bundle is materialized\n` +
|
|
69
|
+
` --to <presId> window to fetch the bundle from (only if not cached)\n` +
|
|
70
|
+
` --out <dir> output dir (default <deck>/.draft-preview/)\n` +
|
|
71
|
+
` --open open each rendered PNG\n` +
|
|
55
72
|
` slidemind search "<query>" Search an engine for images or slides (requires an open host)\n` +
|
|
56
73
|
` --engine <id> engine to search (default: first image engine; required for slides)\n` +
|
|
57
74
|
` --intent <i> auto | content | visual (slide engines)\n` +
|
|
58
75
|
` --top <N> max results\n` +
|
|
59
76
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
60
77
|
` --json print the raw result payload\n` +
|
|
61
|
-
` slidemind
|
|
78
|
+
` slidemind view-slide [opts] Fetch a slide search result's preview image to a file (requires a host)\n` +
|
|
79
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
80
|
+
` --pick <n> 1-based selection number to view\n` +
|
|
81
|
+
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
82
|
+
` --out <dir> output dir (default: a slidemind temp dir)\n` +
|
|
83
|
+
` slidemind ls engines List the live engine catalog for an active pane\n` +
|
|
62
84
|
` --type <t> collection | search\n` +
|
|
63
85
|
` --subtype <s> image | slide | template | content\n` +
|
|
86
|
+
` --to <presId> target a specific window/backend\n` +
|
|
87
|
+
` --all include every paired backend\n` +
|
|
64
88
|
` --json print the raw engine list\n` +
|
|
65
89
|
` slidemind insert-slide [opts] Insert reused slide search results live into a window (requires a host)\n` +
|
|
66
90
|
` --search <id> searchId from 'slidemind search'\n` +
|
|
@@ -73,10 +97,23 @@ function printHelp() {
|
|
|
73
97
|
` --deck <deck> deck to add the slides to\n` +
|
|
74
98
|
` --to <presId> target the window the search ran on (required if >1 open)\n` +
|
|
75
99
|
` --at <N> numeric prefix to start at (default: append)\n` +
|
|
100
|
+
` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/ (requires a host)\n` +
|
|
101
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
102
|
+
` --pick <n,..> 1-based selection number(s)\n` +
|
|
103
|
+
` --deck <deck> deck to write the asset(s) into\n` +
|
|
104
|
+
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
105
|
+
` --icon-color <hex[,hex,..]> bake a colour into SVG result(s) (one for all, or one per --pick)\n` +
|
|
76
106
|
` slidemind download-bundle [deck] Materialize the active corporate design's bundle locally\n` +
|
|
77
107
|
` --to <presId> target a specific window\n` +
|
|
78
108
|
` --force re-download even if the cached bundle is current\n` +
|
|
79
|
-
` slidemind presentations List PowerPoint
|
|
109
|
+
` slidemind presentations List active PowerPoint panes registered with the add-in\n` +
|
|
110
|
+
` --all include retained suspended entries\n` +
|
|
111
|
+
` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
|
|
112
|
+
` --json print machine-readable status\n` +
|
|
113
|
+
` slidemind read [presId] Pull the current content of an open presentation (requires a host)\n` +
|
|
114
|
+
` --mode <m> summary | raw (default: raw)\n` +
|
|
115
|
+
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
116
|
+
` --json print the raw content payload\n` +
|
|
80
117
|
` slidemind push <deck> [opts] Build deck and push slides into a running PowerPoint\n` +
|
|
81
118
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
82
119
|
` --at <index> insert after slide N (0-based); default = append\n` +
|
|
@@ -88,7 +125,7 @@ function printHelp() {
|
|
|
88
125
|
` slidemind whoami Show every paired environment (* = default)\n` +
|
|
89
126
|
` slidemind use <url|env> Set the default environment\n` +
|
|
90
127
|
` slidemind logout [url|env] Unpair one environment (--all to unpair every one)\n` +
|
|
91
|
-
` slidemind init --for <t> Set up slidemind in a harness (
|
|
128
|
+
` slidemind init --for <t> Set up slidemind in a harness (deploys the skill; clears the bundle cache)\n` +
|
|
92
129
|
` slidemind uninstall --for <t> Remove a previously-installed skill (clears the bundle cache)\n` +
|
|
93
130
|
` slidemind update [--for <t>] Fetch the latest release and redeploy\n` +
|
|
94
131
|
` --for <t> harness to redeploy into (default: all)\n` +
|
|
@@ -127,6 +164,31 @@ function resolveDeckArg(arg) {
|
|
|
127
164
|
return path.join(decksDir, arg);
|
|
128
165
|
}
|
|
129
166
|
|
|
167
|
+
// Best-effort human name of a materialized design: the first `# ` heading in
|
|
168
|
+
// the bundle's design.md, with the add-in's trailing " — design rules" suffix
|
|
169
|
+
// stripped. Returns null on any failure (missing file, no heading) so callers
|
|
170
|
+
// fall back to the raw presFP. Shared by `ready` and `new`'s auto-bind.
|
|
171
|
+
function readDesignName(bundleDir) {
|
|
172
|
+
if (!bundleDir) return null;
|
|
173
|
+
try {
|
|
174
|
+
const md = fs.readFileSync(path.join(bundleDir, "design.md"), "utf8");
|
|
175
|
+
for (const raw of md.split("\n")) {
|
|
176
|
+
const line = raw.trim();
|
|
177
|
+
if (line.startsWith("# ")) {
|
|
178
|
+
const name = line
|
|
179
|
+
.slice(2)
|
|
180
|
+
.trim()
|
|
181
|
+
.replace(/\s+[—-]\s+design rules\s*$/i, "")
|
|
182
|
+
.trim();
|
|
183
|
+
return name || null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
130
192
|
function runNode(script, args) {
|
|
131
193
|
return new Promise((resolve, reject) => {
|
|
132
194
|
const child = spawn(process.execPath, [script, ...args], {
|
|
@@ -225,8 +287,9 @@ async function cmdBuildPptx(rest) {
|
|
|
225
287
|
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
226
288
|
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
227
289
|
}
|
|
228
|
-
// The .pptx is rendered server-side by the add-in (one exporter, one truth)
|
|
229
|
-
//
|
|
290
|
+
// The .pptx is rendered server-side by the add-in (one exporter, one truth).
|
|
291
|
+
// A bound deck renders pane-free over the brand routes — no PowerPoint
|
|
292
|
+
// window needed; unbound decks (or --to) resolve through the pane registry.
|
|
230
293
|
const mod = await import(
|
|
231
294
|
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
232
295
|
);
|
|
@@ -237,7 +300,8 @@ async function cmdBuildPptx(rest) {
|
|
|
237
300
|
onProgress: renderProgressPrinter(),
|
|
238
301
|
});
|
|
239
302
|
process.stdout.write(
|
|
240
|
-
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in
|
|
303
|
+
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in` +
|
|
304
|
+
`${result.windowless ? " (no PowerPoint window needed)" : ""}\n`,
|
|
241
305
|
);
|
|
242
306
|
for (const w of result.warnings ?? []) {
|
|
243
307
|
process.stderr.write(` warning: ${w}\n`);
|
|
@@ -247,6 +311,87 @@ async function cmdBuildPptx(rest) {
|
|
|
247
311
|
}
|
|
248
312
|
}
|
|
249
313
|
|
|
314
|
+
function dataUriToPng(dataUri) {
|
|
315
|
+
const m = /^data:image\/png;base64,(.+)$/.exec(dataUri || "");
|
|
316
|
+
return m ? Buffer.from(m[1], "base64") : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function cmdViewDraft(rest) {
|
|
320
|
+
const { deck, passthrough } = splitDeckArg(rest, "view-draft");
|
|
321
|
+
let to = null;
|
|
322
|
+
let outDir = null;
|
|
323
|
+
let open = false;
|
|
324
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
325
|
+
const a = passthrough[i];
|
|
326
|
+
if (a === "--to") {
|
|
327
|
+
to = passthrough[++i];
|
|
328
|
+
if (!to) fail("--to expects a presId");
|
|
329
|
+
} else if (a === "--out" || a === "-o") {
|
|
330
|
+
outDir = passthrough[++i];
|
|
331
|
+
if (!outDir) fail("--out expects a directory");
|
|
332
|
+
} else if (a === "--open") {
|
|
333
|
+
open = true;
|
|
334
|
+
} else if (a.startsWith("-")) {
|
|
335
|
+
fail(`unknown flag: ${a}`);
|
|
336
|
+
} else {
|
|
337
|
+
fail(`unexpected argument: ${a}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const deckDir = resolveDeckArg(deck);
|
|
341
|
+
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
342
|
+
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
343
|
+
}
|
|
344
|
+
// Draft previews are produced locally by the add-in's shipped capture engine
|
|
345
|
+
// (no push, no .pptx) — the same module that hosts the render+push path.
|
|
346
|
+
const mod = await import(
|
|
347
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
348
|
+
);
|
|
349
|
+
const result = await mod.viewDraft(deckDir, {
|
|
350
|
+
presentationId: to,
|
|
351
|
+
onProgress: renderProgressPrinter(),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const previewDir = outDir
|
|
355
|
+
? path.resolve(process.cwd(), outDir)
|
|
356
|
+
: path.join(deckDir, ".draft-preview");
|
|
357
|
+
fs.mkdirSync(previewDir, { recursive: true });
|
|
358
|
+
|
|
359
|
+
const written = [];
|
|
360
|
+
for (const slide of result.slides ?? []) {
|
|
361
|
+
const png = slide.dataUri ? dataUriToPng(slide.dataUri) : null;
|
|
362
|
+
if (png) {
|
|
363
|
+
const stem = slide.fileName.replace(/\.[^.]+$/, "");
|
|
364
|
+
const file = path.join(previewDir, `${stem}.png`);
|
|
365
|
+
fs.writeFileSync(file, png);
|
|
366
|
+
written.push(file);
|
|
367
|
+
const flag = slide.status === "degraded" ? " (DEGRADED)" : "";
|
|
368
|
+
process.stdout.write(` ${slide.fileName}${flag} → ${file}\n`);
|
|
369
|
+
if (slide.note) process.stderr.write(` note: ${slide.note}\n`);
|
|
370
|
+
} else {
|
|
371
|
+
process.stdout.write(` ${slide.fileName} — ${slide.note ?? slide.status}\n`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (written.length === 0) {
|
|
375
|
+
process.stdout.write(
|
|
376
|
+
"No draft preview images were produced (no freeform slides to capture).\n",
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
process.stdout.write(
|
|
380
|
+
`Wrote ${written.length} draft preview${written.length === 1 ? "" : "s"} to ${previewDir} — NOT pushed. ` +
|
|
381
|
+
"Fix anything visibly wrong in the JSX, then re-run, or 'slidemind review' / 'push' when sound.\n",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
for (const w of result.warnings ?? []) {
|
|
385
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
386
|
+
}
|
|
387
|
+
if (open && written.length > 0) {
|
|
388
|
+
const { openInBrowser } = await import(
|
|
389
|
+
pathToFileURL(path.join(BUILDERS_DIR, "build.mjs")).href
|
|
390
|
+
);
|
|
391
|
+
for (const file of written) openInBrowser(file);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
250
395
|
async function cmdSearch(rest) {
|
|
251
396
|
let asJson = false;
|
|
252
397
|
let to = null;
|
|
@@ -298,7 +443,7 @@ async function cmdSearch(rest) {
|
|
|
298
443
|
const bundle = await import(
|
|
299
444
|
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
300
445
|
);
|
|
301
|
-
const registry = await client.
|
|
446
|
+
const registry = await client.requirePaneHost();
|
|
302
447
|
// The engine is per-user, not window-bound — so search just attaches to any
|
|
303
448
|
// connected window (preferring a live one). No "which window?" prompt; pass
|
|
304
449
|
// --to only to pin a specific one. The chosen window is printed below so
|
|
@@ -402,8 +547,11 @@ async function cmdSearch(rest) {
|
|
|
402
547
|
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${ranPresId}\n`,
|
|
403
548
|
);
|
|
404
549
|
} else {
|
|
550
|
+
// The searchId is bound to the window the search ran on — download must
|
|
551
|
+
// target that same window, so spell out its presId in the next-step command.
|
|
405
552
|
process.stdout.write(
|
|
406
|
-
`
|
|
553
|
+
`searchId ${res.searchId} ran on window ${ranPresId}${envTag} — download image result(s) into a deck:\n` +
|
|
554
|
+
` slidemind download-result --search ${res.searchId} --pick <n[,n,...]> --deck <deck> --to ${ranPresId} [--icon-color <hex>]\n`,
|
|
407
555
|
);
|
|
408
556
|
}
|
|
409
557
|
}
|
|
@@ -416,6 +564,7 @@ async function cmdLs(rest) {
|
|
|
416
564
|
let type = null;
|
|
417
565
|
let subtype = null;
|
|
418
566
|
let to = null;
|
|
567
|
+
let all = false;
|
|
419
568
|
let asJson = false;
|
|
420
569
|
for (let i = 0; i < subRest.length; i++) {
|
|
421
570
|
const a = subRest[i];
|
|
@@ -432,10 +581,10 @@ async function cmdLs(rest) {
|
|
|
432
581
|
fail(`--subtype expects image|slide|template|content, got '${subtype}'`);
|
|
433
582
|
}
|
|
434
583
|
} else if (a === "--to") {
|
|
435
|
-
// Accepted for symmetry with other commands; engines are backend-scoped,
|
|
436
|
-
// not presId-scoped, so it does not change the catalog.
|
|
437
584
|
to = subRest[++i];
|
|
438
585
|
if (!to) fail("--to expects a presId");
|
|
586
|
+
} else if (a === "--all") {
|
|
587
|
+
all = true;
|
|
439
588
|
} else if (a === "--json") {
|
|
440
589
|
asJson = true;
|
|
441
590
|
} else if (a.startsWith("-")) {
|
|
@@ -444,11 +593,17 @@ async function cmdLs(rest) {
|
|
|
444
593
|
fail(`unexpected argument: ${a}`);
|
|
445
594
|
}
|
|
446
595
|
}
|
|
447
|
-
void to;
|
|
448
596
|
const client = await import(
|
|
449
597
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
450
598
|
);
|
|
451
|
-
|
|
599
|
+
if (all && to) fail("--all cannot be combined with --to");
|
|
600
|
+
let backendURL = null;
|
|
601
|
+
if (!all) {
|
|
602
|
+
const registry = await client.requirePaneHost();
|
|
603
|
+
const entry = client.pickAnyPresentationEntry(registry, to);
|
|
604
|
+
backendURL = entry.backendURL ?? null;
|
|
605
|
+
}
|
|
606
|
+
const { engines } = await client.listEngines({ type, subtype, backendURL });
|
|
452
607
|
if (asJson) {
|
|
453
608
|
process.stdout.write(`${JSON.stringify(engines, null, 2)}\n`);
|
|
454
609
|
return;
|
|
@@ -522,7 +677,7 @@ async function cmdInsertSlide(rest) {
|
|
|
522
677
|
const client = await import(
|
|
523
678
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
524
679
|
);
|
|
525
|
-
const registry = await client.
|
|
680
|
+
const registry = await client.requirePaneHost();
|
|
526
681
|
const entry = client.pickPresentationEntry(registry, to);
|
|
527
682
|
const res = await client.insertSlideLive({
|
|
528
683
|
presId: entry.presId,
|
|
@@ -583,7 +738,7 @@ async function cmdAddSlide(rest) {
|
|
|
583
738
|
const client = await import(
|
|
584
739
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
585
740
|
);
|
|
586
|
-
const registry = await client.
|
|
741
|
+
const registry = await client.requirePaneHost();
|
|
587
742
|
// The searchId is presentation-scoped, so --to targets the window the search
|
|
588
743
|
// ran on. Without it, a registry with >1 open window is a dead-end.
|
|
589
744
|
const entry = client.pickPresentationEntry(registry, to);
|
|
@@ -592,8 +747,8 @@ async function cmdAddSlide(rest) {
|
|
|
592
747
|
|
|
593
748
|
// Extract each picked slide's bytes over the bridge, then let the shared
|
|
594
749
|
// helper compute the play-order filenames (NN-reused-<shortid>.pptx). The
|
|
595
|
-
// prefix math
|
|
596
|
-
//
|
|
750
|
+
// prefix math lives in one shared unit (src/lib/reused-slides.mjs). Existing
|
|
751
|
+
// files are not renumbered.
|
|
597
752
|
const reused = await import(
|
|
598
753
|
pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
|
|
599
754
|
);
|
|
@@ -627,6 +782,186 @@ async function cmdAddSlide(rest) {
|
|
|
627
782
|
);
|
|
628
783
|
}
|
|
629
784
|
|
|
785
|
+
// Map a `--icon-color` value onto the `--pick` list. Accepts either one colour
|
|
786
|
+
// (applied to every pick) or exactly one per pick (positional); an empty
|
|
787
|
+
// segment means "no colour for this pick". Returns an array aligned to picks.
|
|
788
|
+
function parseIconColors(raw, pickCount) {
|
|
789
|
+
if (!raw) return new Array(pickCount).fill(null);
|
|
790
|
+
const colors = raw
|
|
791
|
+
.split(",")
|
|
792
|
+
.map((s) => s.trim())
|
|
793
|
+
.map((s) => (s.length ? s : null));
|
|
794
|
+
for (const c of colors) {
|
|
795
|
+
if (c !== null && !/^#[0-9a-fA-F]{3,8}$/.test(c)) {
|
|
796
|
+
fail(`--icon-color expects hex colours like #0A7E8C, got '${c}'`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (colors.length === 1) return new Array(pickCount).fill(colors[0]);
|
|
800
|
+
if (colors.length !== pickCount) {
|
|
801
|
+
fail(
|
|
802
|
+
`--icon-color takes one colour (applied to all) or exactly ${pickCount} (one per --pick); got ${colors.length}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return colors;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function cmdViewSlide(rest) {
|
|
809
|
+
let searchId = null;
|
|
810
|
+
let pick = null;
|
|
811
|
+
let to = null;
|
|
812
|
+
let outDir = null;
|
|
813
|
+
for (let i = 0; i < rest.length; i++) {
|
|
814
|
+
const a = rest[i];
|
|
815
|
+
if (a === "--search") {
|
|
816
|
+
const next = rest[++i];
|
|
817
|
+
const n = parseInt(next, 10);
|
|
818
|
+
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
819
|
+
searchId = n;
|
|
820
|
+
} else if (a === "--pick") {
|
|
821
|
+
const next = rest[++i];
|
|
822
|
+
const n = parseInt(next, 10);
|
|
823
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
824
|
+
fail(`--pick expects a positive selection number, got '${next}'`);
|
|
825
|
+
}
|
|
826
|
+
pick = n;
|
|
827
|
+
} else if (a === "--to") {
|
|
828
|
+
to = rest[++i];
|
|
829
|
+
if (!to) fail("--to expects a presId");
|
|
830
|
+
} else if (a === "--out" || a === "-o") {
|
|
831
|
+
outDir = rest[++i];
|
|
832
|
+
if (!outDir) fail("--out expects a directory");
|
|
833
|
+
} else if (a.startsWith("-")) {
|
|
834
|
+
fail(`unknown flag: ${a}`);
|
|
835
|
+
} else {
|
|
836
|
+
fail(`unexpected argument: ${a}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (searchId === null || pick === null) {
|
|
840
|
+
fail("usage: slidemind view-slide --search <id> --pick <n> --to <presId> [--out <dir>]");
|
|
841
|
+
}
|
|
842
|
+
const client = await import(
|
|
843
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
844
|
+
);
|
|
845
|
+
const { extFromMime } = await import(
|
|
846
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
847
|
+
);
|
|
848
|
+
const registry = await client.requirePaneHost();
|
|
849
|
+
// The searchId is presentation-scoped, so --to targets the window the search
|
|
850
|
+
// ran on. Without it, a registry with >1 open window is a dead-end.
|
|
851
|
+
const entry = client.pickPresentationEntry(registry, to);
|
|
852
|
+
const res = await client.viewSlide({
|
|
853
|
+
presId: entry.presId,
|
|
854
|
+
searchId,
|
|
855
|
+
selection: pick,
|
|
856
|
+
backendURL: entry.backendURL ?? null,
|
|
857
|
+
});
|
|
858
|
+
if (!res?.ok || !res.dataUri) {
|
|
859
|
+
fail(`view-slide failed: ${res?.error ?? "no image returned"}`);
|
|
860
|
+
}
|
|
861
|
+
const m = /^data:([^;]+);base64,(.+)$/.exec(res.dataUri);
|
|
862
|
+
if (!m) fail("view-slide returned an unparseable data URI");
|
|
863
|
+
const dir = outDir
|
|
864
|
+
? path.resolve(process.cwd(), outDir)
|
|
865
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-view-"));
|
|
866
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
867
|
+
const target = path.join(dir, `slide-${searchId}-${pick}.${extFromMime(m[1])}`);
|
|
868
|
+
fs.writeFileSync(target, Buffer.from(m[2], "base64"));
|
|
869
|
+
process.stdout.write(
|
|
870
|
+
`Wrote preview of result ${pick} (searchId ${searchId}) to ${target}\n`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function cmdDownloadResult(rest) {
|
|
875
|
+
let searchId = null;
|
|
876
|
+
let picks = null;
|
|
877
|
+
let deck = null;
|
|
878
|
+
let to = null;
|
|
879
|
+
let iconColorsRaw = null;
|
|
880
|
+
for (let i = 0; i < rest.length; i++) {
|
|
881
|
+
const a = rest[i];
|
|
882
|
+
if (a === "--search") {
|
|
883
|
+
const next = rest[++i];
|
|
884
|
+
const n = parseInt(next, 10);
|
|
885
|
+
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
886
|
+
searchId = n;
|
|
887
|
+
} else if (a === "--pick") {
|
|
888
|
+
picks = parsePicks(rest[++i]);
|
|
889
|
+
} else if (a === "--deck") {
|
|
890
|
+
deck = rest[++i];
|
|
891
|
+
if (!deck) fail("--deck expects a deck name or path");
|
|
892
|
+
} else if (a === "--to") {
|
|
893
|
+
to = rest[++i];
|
|
894
|
+
if (!to) fail("--to expects a presId");
|
|
895
|
+
} else if (a === "--icon-color" || a === "--icon-colour") {
|
|
896
|
+
iconColorsRaw = rest[++i];
|
|
897
|
+
if (!iconColorsRaw) fail("--icon-color expects a hex colour (or comma-separated list aligned to --pick)");
|
|
898
|
+
} else if (a.startsWith("-")) {
|
|
899
|
+
fail(`unknown flag: ${a}`);
|
|
900
|
+
} else {
|
|
901
|
+
fail(`unexpected argument: ${a}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (searchId === null || !picks || !deck) {
|
|
905
|
+
fail(
|
|
906
|
+
"usage: slidemind download-result --search <id> --pick <n[,n,...]> --deck <deck> --to <presId> [--icon-color <hex[,hex,...]>]",
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const iconColors = parseIconColors(iconColorsRaw, picks.length);
|
|
910
|
+
const deckDir = resolveDeckArg(deck);
|
|
911
|
+
const assetsDir = path.join(deckDir, "assets");
|
|
912
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
913
|
+
|
|
914
|
+
const client = await import(
|
|
915
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
916
|
+
);
|
|
917
|
+
// Shared image/icon download logic (src/bridge/engine-download.mjs).
|
|
918
|
+
const { downloadOneResult } = await import(
|
|
919
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
920
|
+
);
|
|
921
|
+
const registry = await client.requirePaneHost();
|
|
922
|
+
// The searchId is presentation-scoped, so --to targets the window the search
|
|
923
|
+
// ran on. Without it, a registry with >1 open window is a dead-end.
|
|
924
|
+
const entry = client.pickPresentationEntry(registry, to);
|
|
925
|
+
|
|
926
|
+
// Concurrent — each download is its own bridge round-trip; allSettled so one
|
|
927
|
+
// bad selection doesn't sink the rest of the batch.
|
|
928
|
+
const settled = await Promise.allSettled(
|
|
929
|
+
picks.map((selection, i) =>
|
|
930
|
+
downloadOneResult({
|
|
931
|
+
client,
|
|
932
|
+
presId: entry.presId,
|
|
933
|
+
backendURL: entry.backendURL ?? null,
|
|
934
|
+
searchId,
|
|
935
|
+
assetsDir,
|
|
936
|
+
selection,
|
|
937
|
+
iconColor: iconColors[i],
|
|
938
|
+
}),
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
const results = settled.map((r, i) =>
|
|
942
|
+
r.status === "fulfilled"
|
|
943
|
+
? r.value
|
|
944
|
+
: { selection: picks[i], error: r.reason?.message ?? String(r.reason) },
|
|
945
|
+
);
|
|
946
|
+
let ok = 0;
|
|
947
|
+
const lines = [];
|
|
948
|
+
for (const r of results) {
|
|
949
|
+
if (r.error) {
|
|
950
|
+
lines.push(` ✗ selection ${r.selection}: ${r.error}`);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
ok++;
|
|
954
|
+
const note = r.recolor?.note ? ` — ${r.recolor.note}` : "";
|
|
955
|
+
lines.push(` ${r.reference} (${r.recolor?.mode ?? "?"})${note}`);
|
|
956
|
+
}
|
|
957
|
+
process.stdout.write(
|
|
958
|
+
`Downloaded ${ok}/${results.length} result(s) into ${path.relative(process.cwd(), assetsDir)}:\n` +
|
|
959
|
+
lines.join("\n") +
|
|
960
|
+
`\nReference each from slide JSX as <img src="./assets/...">.\n`,
|
|
961
|
+
);
|
|
962
|
+
if (ok === 0) process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
|
|
630
965
|
async function cmdDownloadBundle(rest) {
|
|
631
966
|
let to = null;
|
|
632
967
|
let force = false;
|
|
@@ -656,7 +991,7 @@ async function cmdDownloadBundle(rest) {
|
|
|
656
991
|
const deckRef = positional[0]
|
|
657
992
|
? bundle.readDeckBundleRef(resolveDeckArg(positional[0]))
|
|
658
993
|
: null;
|
|
659
|
-
const registry = await client.
|
|
994
|
+
const registry = await client.requireRenderHost();
|
|
660
995
|
const entry = client.pickPresentationEntry(registry, to, {
|
|
661
996
|
preferBrandId: deckRef?.presFP ?? null,
|
|
662
997
|
});
|
|
@@ -695,26 +1030,682 @@ async function cmdDownloadBundle(rest) {
|
|
|
695
1030
|
|
|
696
1031
|
async function cmdPresentations(rest) {
|
|
697
1032
|
const asJson = rest.includes("--json");
|
|
1033
|
+
const showAll = rest.includes("--all");
|
|
698
1034
|
const mod = await import(
|
|
699
1035
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
700
1036
|
);
|
|
701
|
-
|
|
1037
|
+
// `--all` is a raw immediate inspection snapshot (no wait). Otherwise wait out
|
|
1038
|
+
// the brief re-registration window after a pane reload / PowerPoint wake so a
|
|
1039
|
+
// freshly-suspended pane doesn't read as an empty list. Either way we LIST
|
|
1040
|
+
// every entry with its status — suspended panes are honest render/push targets
|
|
1041
|
+
// and hiding them made the CLI say "nothing" during the flaky window.
|
|
1042
|
+
const list = showAll
|
|
1043
|
+
? await mod.listOpenPresentations()
|
|
1044
|
+
: (await mod.listReadyPresentationsWithWait()).all;
|
|
1045
|
+
// Connected/pane-ready panes first, then suspended/stale ones (stable sort
|
|
1046
|
+
// keeps each group's incoming order).
|
|
1047
|
+
const sorted = [...list].sort(
|
|
1048
|
+
(a, b) =>
|
|
1049
|
+
(mod.isPaneReadyPresentation(b) ? 1 : 0) -
|
|
1050
|
+
(mod.isPaneReadyPresentation(a) ? 1 : 0),
|
|
1051
|
+
);
|
|
702
1052
|
if (asJson) {
|
|
703
1053
|
process.stdout.write(
|
|
704
|
-
JSON.stringify(
|
|
1054
|
+
JSON.stringify(sorted.map(mod.toPublicPresentation), null, 2) + "\n",
|
|
705
1055
|
);
|
|
706
1056
|
return;
|
|
707
1057
|
}
|
|
708
|
-
if (
|
|
1058
|
+
if (sorted.length === 0) {
|
|
709
1059
|
process.stderr.write(
|
|
710
|
-
"No PowerPoint
|
|
1060
|
+
"No active PowerPoint panes registered with the slidemind add-in.\n",
|
|
711
1061
|
);
|
|
712
1062
|
return;
|
|
713
1063
|
}
|
|
714
|
-
for (const p of
|
|
1064
|
+
for (const p of sorted) {
|
|
715
1065
|
const label = p.title || p.fileName || "(untitled)";
|
|
716
1066
|
const env = p.env ? `[${p.env}]\t` : "";
|
|
717
|
-
|
|
1067
|
+
const state =
|
|
1068
|
+
p.connectionState ||
|
|
1069
|
+
(p.connected === false || p.suspendedAt ? "suspended" : "connected");
|
|
1070
|
+
const design = p.designState ? `\tcd:${p.designState}` : "";
|
|
1071
|
+
process.stdout.write(`${p.presId}\t${env}${label}\t${state}${design}\n`);
|
|
1072
|
+
// A pane that can't take live ops yet (suspended/stale/hydrating) is still
|
|
1073
|
+
// listed — with the one actionable line to wake it — so the reader sees an
|
|
1074
|
+
// honest state instead of an empty registry.
|
|
1075
|
+
if (!mod.isPaneReadyPresentation(p)) {
|
|
1076
|
+
const hint =
|
|
1077
|
+
p.nextAction && typeof p.nextAction.message === "string"
|
|
1078
|
+
? p.nextAction.message
|
|
1079
|
+
: "bring PowerPoint forward and click the slidemind pane";
|
|
1080
|
+
process.stdout.write(`\t${hint}\n`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function publicConnectionStatus(status, client) {
|
|
1086
|
+
return {
|
|
1087
|
+
paired: status.paired,
|
|
1088
|
+
backendCount: status.backendCount,
|
|
1089
|
+
backends: (status.backends ?? []).map((b) => ({
|
|
1090
|
+
env: b.env,
|
|
1091
|
+
ok: b.ok,
|
|
1092
|
+
readinessV1: b.readinessV1 === true,
|
|
1093
|
+
capabilities: b.capabilities ?? null,
|
|
1094
|
+
minCliVersion: b.minCliVersion ?? null,
|
|
1095
|
+
cliUpdateRequired: b.cliUpdateRequired === true,
|
|
1096
|
+
error: b.error,
|
|
1097
|
+
summary: b.summary ?? null,
|
|
1098
|
+
presentations: (b.presentations ?? []).map(client.toPublicPresentation),
|
|
1099
|
+
})),
|
|
1100
|
+
presentations: (status.presentations ?? []).map(client.toPublicPresentation),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// The ordered connection-readiness ladder as a STRUCTURED blocker: the first
|
|
1105
|
+
// unmet precondition, or null when the connection is fully ready. `code` is the
|
|
1106
|
+
// machine-copeable reason an agent switches on; `actor` says who can clear it —
|
|
1107
|
+
// only `cli_update_required` is the AGENT's to fix (it can run `slidemind
|
|
1108
|
+
// update`), every other rung needs a human at the add-in. The `message` strings
|
|
1109
|
+
// are BYTE-IDENTICAL to the old connectionNextAction ladder so doctor's printed
|
|
1110
|
+
// output never churns (connectionNextAction is now a thin projection over this).
|
|
1111
|
+
// `slidemind ready` layers `ambiguous_design` on top and treats `pane_suspended`
|
|
1112
|
+
// as non-blocking (a render-ready-but-suspended window is still a design source).
|
|
1113
|
+
function connectionBlocker(pub) {
|
|
1114
|
+
if (!pub.paired) {
|
|
1115
|
+
return {
|
|
1116
|
+
code: "not_paired",
|
|
1117
|
+
actor: "user",
|
|
1118
|
+
message:
|
|
1119
|
+
"Not paired. In the add-in open Settings → General → Connect CLI, press Pair, then run the shown `slidemind pair <code>`.",
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if ((pub.backends ?? []).every((b) => !b.ok)) {
|
|
1123
|
+
return {
|
|
1124
|
+
code: "all_unreachable",
|
|
1125
|
+
actor: "user",
|
|
1126
|
+
message:
|
|
1127
|
+
"Every paired backend is unreachable. Check network/VPN/backend status, or re-pair if the environment moved.",
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
// Version skew explains most downstream failures — surface it before pane
|
|
1131
|
+
// readiness so the user fixes the right thing first.
|
|
1132
|
+
const outdated = (pub.backends ?? []).find((b) => b.cliUpdateRequired);
|
|
1133
|
+
if (outdated) {
|
|
1134
|
+
return {
|
|
1135
|
+
code: "cli_update_required",
|
|
1136
|
+
actor: "agent",
|
|
1137
|
+
message:
|
|
1138
|
+
`This CLI is older than the minimum the '${outdated.env}' add-in supports` +
|
|
1139
|
+
`${outdated.minCliVersion ? ` (${outdated.minCliVersion})` : ""}. ` +
|
|
1140
|
+
"Run `slidemind update` (or `npm install -g slidemind@latest`), then retry.",
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
if ((pub.presentations ?? []).length === 0) {
|
|
1144
|
+
return {
|
|
1145
|
+
code: "no_window",
|
|
1146
|
+
actor: "user",
|
|
1147
|
+
message:
|
|
1148
|
+
"No PowerPoint window is registered. If PowerPoint is already open with the slidemind pane, bring it to the foreground (click the pane) and re-run. Otherwise open PowerPoint, load the slidemind add-in, and activate or create a corporate design.",
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
const renderReady = pub.presentations.some(
|
|
1152
|
+
(p) =>
|
|
1153
|
+
p.capabilities?.render === true ||
|
|
1154
|
+
(!p.connectionState && p.connected !== false) ||
|
|
1155
|
+
p.designState === "ready",
|
|
1156
|
+
);
|
|
1157
|
+
if (!renderReady) {
|
|
1158
|
+
return {
|
|
1159
|
+
code: "not_render_ready",
|
|
1160
|
+
actor: "user",
|
|
1161
|
+
message:
|
|
1162
|
+
"PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.",
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
const paneReady = pub.presentations.some(
|
|
1166
|
+
(p) =>
|
|
1167
|
+
p.capabilities?.paneOps === true ||
|
|
1168
|
+
((!p.connectionState || p.connectionState === "connected") &&
|
|
1169
|
+
p.connected !== false &&
|
|
1170
|
+
!p.suspendedAt),
|
|
1171
|
+
);
|
|
1172
|
+
if (!paneReady) {
|
|
1173
|
+
return {
|
|
1174
|
+
code: "pane_suspended",
|
|
1175
|
+
actor: "user",
|
|
1176
|
+
message:
|
|
1177
|
+
"Rendering is available, but pane operations are suspended. Bring PowerPoint forward and click/reopen the slidemind pane before push/search/read.",
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Back-compat projection for doctor: just the blocker's message, or "Ready."
|
|
1184
|
+
// when the ladder is clear. Keeps the ladder authored in ONE place.
|
|
1185
|
+
function connectionNextAction(pub) {
|
|
1186
|
+
return connectionBlocker(pub)?.message ?? "Ready.";
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// The one-command agent preflight. Collapses the old 4-call session start
|
|
1190
|
+
// (presentations → pick → download-bundle → download-bundle-to-bind) into a
|
|
1191
|
+
// single verb: check pairing/reachability/CLI-version/windows, resolve the OPEN
|
|
1192
|
+
// design (the add-in is the source of truth), materialize its bundle, bind the
|
|
1193
|
+
// deck, and print exactly one actionable blocker when it can't. Exit 0 iff
|
|
1194
|
+
// ready; every blocker → exit 1 with a machine-copeable code. Never prints a
|
|
1195
|
+
// backendURL (env label only).
|
|
1196
|
+
async function cmdReady(rest) {
|
|
1197
|
+
// --- Local arg parse: unknown flags fail BEFORE any network (cli-dispatch
|
|
1198
|
+
// convention). ---
|
|
1199
|
+
let to = null;
|
|
1200
|
+
let asJson = false;
|
|
1201
|
+
let force = false;
|
|
1202
|
+
let connectOnly = false;
|
|
1203
|
+
const positional = [];
|
|
1204
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1205
|
+
const a = rest[i];
|
|
1206
|
+
if (a === "--to") {
|
|
1207
|
+
to = rest[++i];
|
|
1208
|
+
if (!to) fail("--to expects a presId");
|
|
1209
|
+
} else if (a === "--json") {
|
|
1210
|
+
asJson = true;
|
|
1211
|
+
} else if (a === "--force") {
|
|
1212
|
+
force = true;
|
|
1213
|
+
} else if (a === "--connect-only") {
|
|
1214
|
+
connectOnly = true;
|
|
1215
|
+
} else if (a.startsWith("-")) {
|
|
1216
|
+
fail(`unknown flag: ${a}`);
|
|
1217
|
+
} else {
|
|
1218
|
+
positional.push(a);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// --- Local: resolve the deck dir (never create it) and read its existing
|
|
1223
|
+
// bundle ref. deckPresFP feeds ONLY the mirror tie-break + `suggested`; it
|
|
1224
|
+
// never auto-picks a design. ---
|
|
1225
|
+
const deckArg = positional[0] ?? null;
|
|
1226
|
+
const bundle = await import(
|
|
1227
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
1228
|
+
);
|
|
1229
|
+
let deckDir = null;
|
|
1230
|
+
let deckSlug = null;
|
|
1231
|
+
let deckPresFP = null;
|
|
1232
|
+
if (deckArg) {
|
|
1233
|
+
deckDir = resolveDeckArg(deckArg);
|
|
1234
|
+
if (!fs.existsSync(deckDir)) {
|
|
1235
|
+
fail(
|
|
1236
|
+
`deck '${deckArg}' not found — run \`slidemind ready\` (no deck) to set up, then \`slidemind new ${deckArg}\``,
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
deckSlug = path.basename(deckDir);
|
|
1240
|
+
deckPresFP = bundle.readDeckBundleRef(deckDir)?.presFP ?? null;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Emit a structured blocker (JSON to stdout / human to stderr) and exit 1.
|
|
1244
|
+
// `humanLines` overrides the default `slidemind: <message>` for the richer
|
|
1245
|
+
// ambiguous / unknown-window shapes. Mirrors doctor's stream conventions:
|
|
1246
|
+
// machine JSON on stdout, human prose on stderr.
|
|
1247
|
+
const finishBlocker = ({ blocker, extras = {}, humanLines = null }) => {
|
|
1248
|
+
if (asJson) {
|
|
1249
|
+
process.stdout.write(
|
|
1250
|
+
JSON.stringify(
|
|
1251
|
+
{ ready: false, blocker, nextAction: blocker.message, ...extras },
|
|
1252
|
+
null,
|
|
1253
|
+
2,
|
|
1254
|
+
) + "\n",
|
|
1255
|
+
);
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
if (humanLines) {
|
|
1259
|
+
process.stderr.write(
|
|
1260
|
+
humanLines.endsWith("\n") ? humanLines : `${humanLines}\n`,
|
|
1261
|
+
);
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
fail(blocker.message);
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// --- Local: not paired short-circuits before any network. ---
|
|
1268
|
+
const cfg = await loadBridgeModule("config");
|
|
1269
|
+
if (cfg.readSessions().length === 0) {
|
|
1270
|
+
finishBlocker({ blocker: connectionBlocker({ paired: false }) });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const client = await import(
|
|
1275
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
// --- Network (one parallel round): readiness ladder + the merged registry. ---
|
|
1279
|
+
const [statusRaw, detailed] = await Promise.all([
|
|
1280
|
+
client.connectionStatus(),
|
|
1281
|
+
client.listOpenPresentationsDetailed(),
|
|
1282
|
+
]);
|
|
1283
|
+
const pub = publicConnectionStatus(statusRaw, client);
|
|
1284
|
+
let registry = detailed.presentations;
|
|
1285
|
+
|
|
1286
|
+
// Terminal blockers: waiting can't fix an unreachable backend or a stale CLI.
|
|
1287
|
+
const terminal = connectionBlocker(pub);
|
|
1288
|
+
if (
|
|
1289
|
+
terminal &&
|
|
1290
|
+
(terminal.code === "all_unreachable" || terminal.code === "cli_update_required")
|
|
1291
|
+
) {
|
|
1292
|
+
finishBlocker({ blocker: terminal });
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// --- Bounded hydration wait when nothing is render-ready yet: covers the
|
|
1297
|
+
// just-clicked / just-woke pane still hydrating brand metadata. Polls even on
|
|
1298
|
+
// an empty registry (that's the flaky window this papers over). ---
|
|
1299
|
+
const isRR = client.isRenderReadyPresentation;
|
|
1300
|
+
if (!registry.some(isRR)) {
|
|
1301
|
+
const waitMs =
|
|
1302
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_PRESENTATIONS_WAIT_MS || "6000", 10) ||
|
|
1303
|
+
6000;
|
|
1304
|
+
const pollMs =
|
|
1305
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_READY_POLL_MS || "1500", 10) || 1500;
|
|
1306
|
+
const deadline = Date.now() + waitMs;
|
|
1307
|
+
while (!registry.some(isRR) && Date.now() < deadline) {
|
|
1308
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1309
|
+
registry = (await client.listOpenPresentationsDetailed()).presentations;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (!registry.some(isRR)) {
|
|
1313
|
+
// --- Pane-free rung: a BOUND deck readies over the brand routes with no
|
|
1314
|
+
// window at all — the design identity (presFP == brandId) lives in the
|
|
1315
|
+
// deck's .bundle-ref.json, and the render half never needed the pane.
|
|
1316
|
+
// Degraded: build-pptx/build/review/view-draft work; push/search/read
|
|
1317
|
+
// still need a pane. Skipped for --connect-only (search needs a pane) and
|
|
1318
|
+
// --to (an explicit window must resolve through the registry). Any miss
|
|
1319
|
+
// (old add-in, legacy ref, revoked grant) falls through to the ladder.
|
|
1320
|
+
if (deckDir && deckPresFP && !to && !connectOnly) {
|
|
1321
|
+
let result = null;
|
|
1322
|
+
try {
|
|
1323
|
+
result = await bundle.materializeBundle(null, {
|
|
1324
|
+
force,
|
|
1325
|
+
brandId: deckPresFP,
|
|
1326
|
+
});
|
|
1327
|
+
} catch {
|
|
1328
|
+
/* fall through to the standard no_window / not_render_ready blocker */
|
|
1329
|
+
}
|
|
1330
|
+
if (result) {
|
|
1331
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
1332
|
+
presFP: result.presFP,
|
|
1333
|
+
etag: result.etag,
|
|
1334
|
+
});
|
|
1335
|
+
const designName = readDesignName(result.dir);
|
|
1336
|
+
const env = result.backendURL
|
|
1337
|
+
? cfg.deriveEnvLabel(result.backendURL)
|
|
1338
|
+
: null;
|
|
1339
|
+
const nextAction =
|
|
1340
|
+
"Ready (no PowerPoint window — build-pptx/build/review/view-draft work; push/search/read need a pane).";
|
|
1341
|
+
if (asJson) {
|
|
1342
|
+
process.stdout.write(
|
|
1343
|
+
JSON.stringify(
|
|
1344
|
+
{
|
|
1345
|
+
ready: true,
|
|
1346
|
+
windowless: true,
|
|
1347
|
+
deck: deckSlug,
|
|
1348
|
+
bound: true,
|
|
1349
|
+
env,
|
|
1350
|
+
presId: null,
|
|
1351
|
+
presFP: result.presFP,
|
|
1352
|
+
designName,
|
|
1353
|
+
bundleDir: result.dir,
|
|
1354
|
+
bundleEtag: result.etag ?? null,
|
|
1355
|
+
paneSuspended: true,
|
|
1356
|
+
warnings: result.warnings ?? [],
|
|
1357
|
+
nextAction,
|
|
1358
|
+
},
|
|
1359
|
+
null,
|
|
1360
|
+
2,
|
|
1361
|
+
) + "\n",
|
|
1362
|
+
);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const designLabel = designName
|
|
1366
|
+
? `${designName} (${result.presFP})`
|
|
1367
|
+
: result.presFP;
|
|
1368
|
+
process.stdout.write(
|
|
1369
|
+
`Ready — design ${designLabel}${env ? ` [${env}]` : ""} (no PowerPoint window)\n`,
|
|
1370
|
+
);
|
|
1371
|
+
process.stdout.write(
|
|
1372
|
+
` bundle: ${result.dir} (etag ${result.etag ?? "?"})\n`,
|
|
1373
|
+
);
|
|
1374
|
+
process.stdout.write(` deck ${deckSlug} bound\n`);
|
|
1375
|
+
process.stdout.write(
|
|
1376
|
+
" Note: build-pptx/build/review/view-draft work without a window; push/search/read need a pane.\n",
|
|
1377
|
+
);
|
|
1378
|
+
for (const w of result.warnings ?? []) {
|
|
1379
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
// Only the no-window / not-render-ready rungs are reachable here (terminal
|
|
1385
|
+
// blockers already returned; a render-ready window would have exited the
|
|
1386
|
+
// poll). Reuse the ladder's exact strings over the freshly-polled registry.
|
|
1387
|
+
const latePub = {
|
|
1388
|
+
paired: true,
|
|
1389
|
+
backendCount: pub.backendCount,
|
|
1390
|
+
backends: pub.backends,
|
|
1391
|
+
presentations: registry.map(client.toPublicPresentation),
|
|
1392
|
+
};
|
|
1393
|
+
const b =
|
|
1394
|
+
connectionBlocker(latePub) ?? {
|
|
1395
|
+
code: "not_render_ready",
|
|
1396
|
+
actor: "user",
|
|
1397
|
+
message:
|
|
1398
|
+
"PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.",
|
|
1399
|
+
};
|
|
1400
|
+
finishBlocker({ blocker: b });
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// --- --connect-only: the connection is up with ≥1 render-ready window; the
|
|
1405
|
+
// search skill needs nothing more (no design resolution, no bundle). ---
|
|
1406
|
+
if (connectOnly) {
|
|
1407
|
+
if (asJson) {
|
|
1408
|
+
process.stdout.write(
|
|
1409
|
+
JSON.stringify(
|
|
1410
|
+
{
|
|
1411
|
+
ready: true,
|
|
1412
|
+
connectOnly: true,
|
|
1413
|
+
nextAction: "Ready.",
|
|
1414
|
+
backends: (pub.backends ?? []).map((b) => ({ env: b.env, ok: b.ok })),
|
|
1415
|
+
},
|
|
1416
|
+
null,
|
|
1417
|
+
2,
|
|
1418
|
+
) + "\n",
|
|
1419
|
+
);
|
|
1420
|
+
} else {
|
|
1421
|
+
const windows = registry.filter(isRR).length;
|
|
1422
|
+
process.stdout.write(
|
|
1423
|
+
`Connected. ${pub.backendCount} backend(s), ${windows} window(s) registered.\n`,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// --- Local: resolve WHICH design to bind against. ---
|
|
1430
|
+
const resolution = client.resolveDesignSource(registry, {
|
|
1431
|
+
explicitPresId: to,
|
|
1432
|
+
deckPresFP,
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
if (resolution.status === "none") {
|
|
1436
|
+
// Defensive: a render-ready window exists (poll passed) so this is rare.
|
|
1437
|
+
finishBlocker({
|
|
1438
|
+
blocker: {
|
|
1439
|
+
code: "not_render_ready",
|
|
1440
|
+
actor: "user",
|
|
1441
|
+
message:
|
|
1442
|
+
"PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.",
|
|
1443
|
+
},
|
|
1444
|
+
});
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (resolution.status === "unknown_presid") {
|
|
1449
|
+
const windows = registry.filter(isRR).map(client.toPublicPresentation);
|
|
1450
|
+
const candidates = windows.map((p) => ({
|
|
1451
|
+
presId: p.presId,
|
|
1452
|
+
env: p.env ?? null,
|
|
1453
|
+
title: p.title ?? null,
|
|
1454
|
+
fileName: p.fileName ?? null,
|
|
1455
|
+
}));
|
|
1456
|
+
const blocker = {
|
|
1457
|
+
code: "unknown_presid",
|
|
1458
|
+
actor: "user",
|
|
1459
|
+
message: `No render-ready PowerPoint window has presId='${to}'. Re-run 'slidemind ready --to <presId>' with one of the open windows.`,
|
|
1460
|
+
};
|
|
1461
|
+
const rows = windows.map(
|
|
1462
|
+
(p) =>
|
|
1463
|
+
` ${p.presId} ${p.env ? `[${p.env}] ` : ""}${p.title || p.fileName || "(untitled)"}`,
|
|
1464
|
+
);
|
|
1465
|
+
finishBlocker({
|
|
1466
|
+
blocker,
|
|
1467
|
+
extras: { candidates },
|
|
1468
|
+
humanLines:
|
|
1469
|
+
`slidemind: ${blocker.message}\n` +
|
|
1470
|
+
`${rows.join("\n")}`,
|
|
1471
|
+
});
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (resolution.status === "ambiguous") {
|
|
1476
|
+
const cand = resolution.candidates;
|
|
1477
|
+
const blocker = {
|
|
1478
|
+
code: "ambiguous_design",
|
|
1479
|
+
actor: "user",
|
|
1480
|
+
message:
|
|
1481
|
+
"Multiple corporate designs are open — ask the user which to build against, then re-run: slidemind ready --to <presId>.",
|
|
1482
|
+
};
|
|
1483
|
+
const rows = cand.map((c) => {
|
|
1484
|
+
const env = c.env ? ` [${c.env}]` : "";
|
|
1485
|
+
const label = c.title || c.fileName || "(untitled)";
|
|
1486
|
+
return ` ${c.presId}${env} ${label} (design ${c.brandId ?? "?"})`;
|
|
1487
|
+
});
|
|
1488
|
+
let human =
|
|
1489
|
+
"slidemind: multiple corporate designs are open — ask the user which to build against:\n" +
|
|
1490
|
+
`${rows.join("\n")}\n`;
|
|
1491
|
+
if (resolution.suggested) {
|
|
1492
|
+
const s = cand.find((c) => c.presId === resolution.suggested);
|
|
1493
|
+
const sLabel = s?.title || s?.fileName || "(untitled)";
|
|
1494
|
+
human += `(this deck was last built against ${s?.brandId ?? deckPresFP} — ${sLabel})\n`;
|
|
1495
|
+
}
|
|
1496
|
+
human += "Then re-run: slidemind ready --to <presId>";
|
|
1497
|
+
finishBlocker({
|
|
1498
|
+
blocker,
|
|
1499
|
+
extras: {
|
|
1500
|
+
candidates: cand.map((c) => ({
|
|
1501
|
+
presId: c.presId,
|
|
1502
|
+
env: c.env ?? null,
|
|
1503
|
+
title: c.title ?? null,
|
|
1504
|
+
fileName: c.fileName ?? null,
|
|
1505
|
+
presFP: c.brandId ?? null,
|
|
1506
|
+
})),
|
|
1507
|
+
suggested: resolution.suggested,
|
|
1508
|
+
},
|
|
1509
|
+
humanLines: human,
|
|
1510
|
+
});
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// --- Network: materialize the resolved design's bundle. Always called —
|
|
1515
|
+
// incremental etag-gated download makes an unchanged bundle near-free and a
|
|
1516
|
+
// changed one refreshes. ---
|
|
1517
|
+
const entry = resolution.entry;
|
|
1518
|
+
const result = await bundle.materializeBundle(entry.presId, {
|
|
1519
|
+
force,
|
|
1520
|
+
backendURL: entry.backendURL ?? null,
|
|
1521
|
+
preferBrandId: entry.brandId ?? null,
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
// --- Local: bind the deck (kills the second download-bundle). ---
|
|
1525
|
+
let bound = false;
|
|
1526
|
+
if (deckDir) {
|
|
1527
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
1528
|
+
presFP: result.presFP,
|
|
1529
|
+
etag: result.etag,
|
|
1530
|
+
});
|
|
1531
|
+
bound = true;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const designName = readDesignName(result.dir);
|
|
1535
|
+
const paneSuspended = !client.isPaneReadyPresentation(entry);
|
|
1536
|
+
|
|
1537
|
+
if (asJson) {
|
|
1538
|
+
process.stdout.write(
|
|
1539
|
+
JSON.stringify(
|
|
1540
|
+
{
|
|
1541
|
+
ready: true,
|
|
1542
|
+
deck: deckSlug,
|
|
1543
|
+
bound,
|
|
1544
|
+
env: entry.env ?? null,
|
|
1545
|
+
presId: entry.presId,
|
|
1546
|
+
presFP: result.presFP,
|
|
1547
|
+
designName,
|
|
1548
|
+
bundleDir: result.dir,
|
|
1549
|
+
bundleEtag: result.etag ?? null,
|
|
1550
|
+
presentation: {
|
|
1551
|
+
title: entry.title ?? null,
|
|
1552
|
+
fileName: entry.fileName ?? null,
|
|
1553
|
+
},
|
|
1554
|
+
paneSuspended,
|
|
1555
|
+
warnings: result.warnings ?? [],
|
|
1556
|
+
nextAction: "Ready.",
|
|
1557
|
+
},
|
|
1558
|
+
null,
|
|
1559
|
+
2,
|
|
1560
|
+
) + "\n",
|
|
1561
|
+
);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// --- Human success: compact multi-line summary on stdout. ---
|
|
1566
|
+
const envTag = entry.env ? ` [${entry.env}]` : "";
|
|
1567
|
+
const presLabel = entry.title || entry.fileName || "(untitled)";
|
|
1568
|
+
const designLabel = designName
|
|
1569
|
+
? `${designName} (${result.presFP})`
|
|
1570
|
+
: result.presFP;
|
|
1571
|
+
process.stdout.write(`Ready — design ${designLabel}\n`);
|
|
1572
|
+
process.stdout.write(` presentation: ${presLabel}${envTag}\n`);
|
|
1573
|
+
process.stdout.write(
|
|
1574
|
+
` bundle: ${result.dir} (etag ${result.etag ?? "?"})\n`,
|
|
1575
|
+
);
|
|
1576
|
+
if (bound) process.stdout.write(` deck ${deckSlug} bound\n`);
|
|
1577
|
+
if (paneSuspended) {
|
|
1578
|
+
process.stdout.write(
|
|
1579
|
+
" Note: pane operations (push/search/read) will wait for a pane click.\n",
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
for (const w of result.warnings ?? []) {
|
|
1583
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
async function cmdDoctor(rest) {
|
|
1588
|
+
const asJson = rest.includes("--json");
|
|
1589
|
+
const cfg = await loadBridgeModule("config");
|
|
1590
|
+
const sessions = cfg.readSessions();
|
|
1591
|
+
const client = await import(
|
|
1592
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1593
|
+
);
|
|
1594
|
+
if (sessions.length === 0) {
|
|
1595
|
+
const pub = {
|
|
1596
|
+
paired: false,
|
|
1597
|
+
backendCount: 0,
|
|
1598
|
+
backends: [],
|
|
1599
|
+
presentations: [],
|
|
1600
|
+
nextAction: connectionNextAction({ paired: false }),
|
|
1601
|
+
};
|
|
1602
|
+
process[asJson ? "stdout" : "stderr"].write(
|
|
1603
|
+
asJson ? `${JSON.stringify(pub, null, 2)}\n` : `${pub.nextAction}\n`,
|
|
1604
|
+
);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const status = publicConnectionStatus(await client.connectionStatus(), client);
|
|
1608
|
+
status.nextAction = connectionNextAction(status);
|
|
1609
|
+
if (asJson) {
|
|
1610
|
+
process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
process.stdout.write(`Paired backends: ${status.backendCount}\n`);
|
|
1614
|
+
for (const b of status.backends) {
|
|
1615
|
+
process.stdout.write(
|
|
1616
|
+
` ${b.env}: ${b.ok ? "reachable" : `unreachable — ${b.error}`}` +
|
|
1617
|
+
(b.readinessV1 ? " (readiness)" : " (legacy)") +
|
|
1618
|
+
`\n`,
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (status.presentations.length === 0) {
|
|
1622
|
+
process.stdout.write("Open presentations: none\n");
|
|
1623
|
+
} else {
|
|
1624
|
+
process.stdout.write("Open presentations:\n");
|
|
1625
|
+
for (const p of status.presentations) {
|
|
1626
|
+
const label = p.title || p.fileName || "(untitled)";
|
|
1627
|
+
const state =
|
|
1628
|
+
p.connectionState ||
|
|
1629
|
+
(p.connected === false || p.suspendedAt ? "suspended" : "connected");
|
|
1630
|
+
const design = p.designState || (p.brandId ? "ready" : "unknown");
|
|
1631
|
+
const caps = p.capabilities
|
|
1632
|
+
? ` render:${p.capabilities.render === true ? "yes" : "no"} pane:${p.capabilities.paneOps === true ? "yes" : "no"}`
|
|
1633
|
+
: "";
|
|
1634
|
+
process.stdout.write(
|
|
1635
|
+
` ${p.presId}\t${p.env ? `[${p.env}] ` : ""}${label}\t${state}\tcd:${design}${caps}\n`,
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
process.stdout.write(`Next action: ${status.nextAction}\n`);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Pull the current content of an open presentation. Content is window-bound, so
|
|
1643
|
+
// pick exactly one window (positional presId or --to; pickPresentationEntry errors
|
|
1644
|
+
// listing the windows if several are open and none was named). `--mode summary`
|
|
1645
|
+
// prints a per-slide outline; default `raw` prints each slide's plain text.
|
|
1646
|
+
async function cmdRead(rest) {
|
|
1647
|
+
let presId = null;
|
|
1648
|
+
let mode = "raw";
|
|
1649
|
+
let asJson = false;
|
|
1650
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1651
|
+
const a = rest[i];
|
|
1652
|
+
if (a === "--mode") {
|
|
1653
|
+
const next = rest[++i];
|
|
1654
|
+
if (next !== "summary" && next !== "raw") {
|
|
1655
|
+
fail(`--mode expects 'summary' or 'raw', got '${next ?? ""}'`);
|
|
1656
|
+
}
|
|
1657
|
+
mode = next;
|
|
1658
|
+
} else if (a === "--to") {
|
|
1659
|
+
presId = rest[++i];
|
|
1660
|
+
if (!presId) fail("--to expects a presId");
|
|
1661
|
+
} else if (a === "--json") {
|
|
1662
|
+
asJson = true;
|
|
1663
|
+
} else if (a.startsWith("-")) {
|
|
1664
|
+
fail(`unknown flag: ${a}`);
|
|
1665
|
+
} else if (!presId) {
|
|
1666
|
+
presId = a; // positional presId
|
|
1667
|
+
} else {
|
|
1668
|
+
fail(`unexpected argument: ${a}`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const client = await import(
|
|
1672
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1673
|
+
);
|
|
1674
|
+
const registry = await client.requirePaneHost();
|
|
1675
|
+
const entry = client.pickPresentationEntry(registry, presId);
|
|
1676
|
+
const res = await client.getPresentationContent({
|
|
1677
|
+
presId: entry.presId,
|
|
1678
|
+
mode,
|
|
1679
|
+
backendURL: entry.backendURL ?? null,
|
|
1680
|
+
});
|
|
1681
|
+
if (!res?.ok) {
|
|
1682
|
+
fail(`read failed: ${res?.error ?? "unknown error"}`);
|
|
1683
|
+
}
|
|
1684
|
+
if (asJson) {
|
|
1685
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const slides = Array.isArray(res.slides) ? res.slides : [];
|
|
1689
|
+
if (slides.length === 0) {
|
|
1690
|
+
process.stderr.write("(presentation has no slides)\n");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (res.mode === "summary") {
|
|
1694
|
+
for (const s of slides) {
|
|
1695
|
+
const label = s.title || "(untitled)";
|
|
1696
|
+
const sub = s.subtitle ? ` — ${s.subtitle}` : "";
|
|
1697
|
+
const layout = s.layout ? ` [${s.layout}]` : "";
|
|
1698
|
+
process.stdout.write(
|
|
1699
|
+
`${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
} else {
|
|
1703
|
+
slides.forEach((s, i) => {
|
|
1704
|
+
if (i > 0) process.stdout.write("\n");
|
|
1705
|
+
const layout = s.layout ? ` (${s.layout})` : "";
|
|
1706
|
+
process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
|
|
1707
|
+
process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
|
|
1708
|
+
});
|
|
718
1709
|
}
|
|
719
1710
|
}
|
|
720
1711
|
|
|
@@ -765,8 +1756,30 @@ async function cmdPair(rest) {
|
|
|
765
1756
|
others > 0
|
|
766
1757
|
? ` (${sessions.length} environments connected — commands now fan out across all of them)`
|
|
767
1758
|
: "";
|
|
1759
|
+
let verify = "";
|
|
1760
|
+
try {
|
|
1761
|
+
const client = await import(
|
|
1762
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1763
|
+
);
|
|
1764
|
+
const deadline = Date.now() + 8000;
|
|
1765
|
+
for (;;) {
|
|
1766
|
+
const status = await client.connectionStatus();
|
|
1767
|
+
if ((status.presentations ?? []).length > 0) {
|
|
1768
|
+
verify = " Pane is registered.";
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
if (Date.now() >= deadline) {
|
|
1772
|
+
verify =
|
|
1773
|
+
" No pane is registered yet; if PowerPoint is already open, reopen or click the slidemind task pane, then run 'slidemind doctor'.";
|
|
1774
|
+
break;
|
|
1775
|
+
}
|
|
1776
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
// Pairing itself succeeded; diagnostics remain available via doctor.
|
|
1780
|
+
}
|
|
768
1781
|
process.stdout.write(
|
|
769
|
-
`Paired with ${label || env}${suffix}
|
|
1782
|
+
`Paired with ${label || env}${suffix}.${verify} Run 'slidemind doctor' to verify.\n`,
|
|
770
1783
|
);
|
|
771
1784
|
}
|
|
772
1785
|
|
|
@@ -934,8 +1947,8 @@ async function cmdPush(rest) {
|
|
|
934
1947
|
}
|
|
935
1948
|
if (result.paneSuspended) {
|
|
936
1949
|
process.stdout.write(
|
|
937
|
-
`
|
|
938
|
-
`
|
|
1950
|
+
`The deck rendered, but no PowerPoint window was ready to receive it.\n` +
|
|
1951
|
+
`Open PowerPoint with the slidemind pane (or click the pane once to wake it), ` +
|
|
939
1952
|
`then re-run: slidemind push ${deck}\n` +
|
|
940
1953
|
`The rendered deck was saved to ${path.relative(process.cwd(), result.fallbackFile)} in the meantime.\n`,
|
|
941
1954
|
);
|
|
@@ -947,7 +1960,22 @@ async function cmdPush(rest) {
|
|
|
947
1960
|
}
|
|
948
1961
|
|
|
949
1962
|
async function cmdNew(rest) {
|
|
950
|
-
|
|
1963
|
+
let to = null;
|
|
1964
|
+
let noBind = false;
|
|
1965
|
+
const positional = [];
|
|
1966
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1967
|
+
const a = rest[i];
|
|
1968
|
+
if (a === "--to") {
|
|
1969
|
+
to = rest[++i];
|
|
1970
|
+
if (!to) fail("--to expects a presId");
|
|
1971
|
+
} else if (a === "--no-bind") {
|
|
1972
|
+
noBind = true;
|
|
1973
|
+
} else if (a.startsWith("-")) {
|
|
1974
|
+
fail(`unknown flag: ${a}`);
|
|
1975
|
+
} else {
|
|
1976
|
+
positional.push(a);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
951
1979
|
const name = positional[0];
|
|
952
1980
|
if (!name) fail("usage: slidemind new <name>");
|
|
953
1981
|
if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
@@ -970,6 +1998,48 @@ async function cmdNew(rest) {
|
|
|
970
1998
|
process.stdout.write(
|
|
971
1999
|
`Next: ask your agent to author "${name}", then \`slidemind build ${name}\`\n`,
|
|
972
2000
|
);
|
|
2001
|
+
|
|
2002
|
+
// Stateless auto-bind: bind the fresh deck to whatever design is OPEN so the
|
|
2003
|
+
// agent skips a follow-up `download-bundle`. Best-effort — a bind miss (no
|
|
2004
|
+
// host, ambiguous designs, bundle not yet materialized, any network error)
|
|
2005
|
+
// NEVER fails the scaffold; it just drops a soft hint. Fully offline when
|
|
2006
|
+
// unpaired: no network attempted, no hint. `--no-bind` opts out entirely.
|
|
2007
|
+
if (noBind) return;
|
|
2008
|
+
const cfg = await loadBridgeModule("config");
|
|
2009
|
+
if (cfg.readSessions().length === 0) return;
|
|
2010
|
+
let boundMsg = null;
|
|
2011
|
+
try {
|
|
2012
|
+
const client = await import(
|
|
2013
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
2014
|
+
);
|
|
2015
|
+
const bundle = await import(
|
|
2016
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
2017
|
+
);
|
|
2018
|
+
const { presentations } = await client.listOpenPresentationsDetailed();
|
|
2019
|
+
const resolution = client.resolveDesignSource(presentations, {
|
|
2020
|
+
explicitPresId: to,
|
|
2021
|
+
});
|
|
2022
|
+
if (resolution.status === "resolved") {
|
|
2023
|
+
const entry = resolution.entry;
|
|
2024
|
+
const meta = bundle.readMaterializedBundleMeta(entry.brandId);
|
|
2025
|
+
if (meta?.presFP) {
|
|
2026
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
2027
|
+
presFP: meta.presFP,
|
|
2028
|
+
etag: meta.etag ?? null,
|
|
2029
|
+
});
|
|
2030
|
+
const label =
|
|
2031
|
+
readDesignName(bundle.bundleDirForPresFP(meta.presFP)) ?? meta.presFP;
|
|
2032
|
+
boundMsg = `bound to design ${label}`;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
} catch {
|
|
2036
|
+
/* best-effort — fall through to the soft hint */
|
|
2037
|
+
}
|
|
2038
|
+
process.stdout.write(
|
|
2039
|
+
boundMsg
|
|
2040
|
+
? `${boundMsg}\n`
|
|
2041
|
+
: `not bound to a design yet — run \`slidemind ready ${name}\` when a window is open\n`,
|
|
2042
|
+
);
|
|
973
2043
|
}
|
|
974
2044
|
|
|
975
2045
|
function parseTargetFlag(rest) {
|
|
@@ -1090,9 +2160,9 @@ function spawnInherit(file, args) {
|
|
|
1090
2160
|
// ---------- distribution (public npm) ----------
|
|
1091
2161
|
//
|
|
1092
2162
|
// slidemind is published to public npm as `slidemind`. First install is
|
|
1093
|
-
// `npm install -g slidemind` (then `slidemind init` to
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
2163
|
+
// `npm install -g slidemind` (then `slidemind init` to deploy the skill;
|
|
2164
|
+
// `install` stays as a back-compat alias); `slidemind update` is a thin
|
|
2165
|
+
// wrapper over `npm install -g slidemind@latest`.
|
|
1096
2166
|
|
|
1097
2167
|
// Resolve the `npm` that sits next to the node running this process, so an
|
|
1098
2168
|
// update lands the global into the same prefix slidemind was launched from.
|
|
@@ -1192,21 +2262,33 @@ async function main(rawArgv) {
|
|
|
1192
2262
|
case "build-pptx":
|
|
1193
2263
|
await cmdBuildPptx(rest);
|
|
1194
2264
|
return 0;
|
|
2265
|
+
case "view-draft":
|
|
2266
|
+
await cmdViewDraft(rest);
|
|
2267
|
+
return 0;
|
|
1195
2268
|
case "search":
|
|
1196
2269
|
await cmdSearch(rest);
|
|
1197
2270
|
return 0;
|
|
1198
2271
|
case "ls":
|
|
1199
2272
|
await cmdLs(rest);
|
|
1200
2273
|
return 0;
|
|
2274
|
+
case "view-slide":
|
|
2275
|
+
await cmdViewSlide(rest);
|
|
2276
|
+
return 0;
|
|
1201
2277
|
case "insert-slide":
|
|
1202
2278
|
await cmdInsertSlide(rest);
|
|
1203
2279
|
return 0;
|
|
1204
2280
|
case "add-slide":
|
|
1205
2281
|
await cmdAddSlide(rest);
|
|
1206
2282
|
return 0;
|
|
2283
|
+
case "download-result":
|
|
2284
|
+
await cmdDownloadResult(rest);
|
|
2285
|
+
return 0;
|
|
1207
2286
|
case "download-bundle":
|
|
1208
2287
|
await cmdDownloadBundle(rest);
|
|
1209
2288
|
return 0;
|
|
2289
|
+
case "ready":
|
|
2290
|
+
await cmdReady(rest);
|
|
2291
|
+
return 0;
|
|
1210
2292
|
case "new":
|
|
1211
2293
|
await cmdNew(rest);
|
|
1212
2294
|
return 0;
|
|
@@ -1225,12 +2307,20 @@ async function main(rawArgv) {
|
|
|
1225
2307
|
case "presentations":
|
|
1226
2308
|
await cmdPresentations(rest);
|
|
1227
2309
|
return 0;
|
|
2310
|
+
case "doctor":
|
|
2311
|
+
case "connection":
|
|
2312
|
+
case "status":
|
|
2313
|
+
await cmdDoctor(rest);
|
|
2314
|
+
return 0;
|
|
2315
|
+
case "read":
|
|
2316
|
+
await cmdRead(rest);
|
|
2317
|
+
return 0;
|
|
1228
2318
|
case "push":
|
|
1229
2319
|
await cmdPush(rest);
|
|
1230
2320
|
return 0;
|
|
1231
2321
|
case "init":
|
|
1232
2322
|
case "install":
|
|
1233
|
-
// Set slidemind up in a coding harness (
|
|
2323
|
+
// Set slidemind up in a coding harness (deploy the skill).
|
|
1234
2324
|
// `install` stays as a back-compat alias for the documented `init`.
|
|
1235
2325
|
await cmdInstall(rest);
|
|
1236
2326
|
return 0;
|