slidemind 0.1.6 → 0.1.8
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 +21 -15
- package/README.md +21 -10
- package/bin/slidemind.mjs +836 -35
- package/package.json +2 -5
- package/skills/create-slides/SKILL.md +85 -60
- package/skills/search/SKILL.md +27 -27
- package/src/bridge/build-and-push.mjs +166 -55
- package/src/bridge/bundle.mjs +49 -7
- package/src/bridge/capture.mjs +2 -2
- package/src/bridge/client.mjs +374 -44
- package/src/bridge/engine-download.mjs +89 -0
- package/src/{mcp → bridge}/recolor.mjs +10 -9
- package/src/builders/build.mjs +261 -23
- package/src/builders/gate-server.mjs +223 -0
- package/src/builders/lint.mjs +2 -2
- package/src/builders/plan-md.mjs +216 -0
- package/src/builders/plan-overlay.js +747 -0
- package/src/builders/plan.mjs +575 -0
- package/src/builders/resolve-skill.mjs +40 -5
- package/src/builders/review-overlay.js +172 -16
- package/src/builders/review.mjs +139 -80
- package/src/installers/claude.mjs +10 -17
- package/src/installers/codex.mjs +14 -77
- package/src/installers/opencode.mjs +10 -18
- package/src/lib/reused-slides.mjs +2 -4
- package/src/mcp/server.mjs +0 -958
package/bin/slidemind.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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/}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
25
26
|
import path from "node:path";
|
|
26
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
28
|
|
|
@@ -44,11 +45,24 @@ function printHelp() {
|
|
|
44
45
|
`slidemind ${pkg.version} — drive the slidemind PowerPoint add-in to build native, on-brand decks\n` +
|
|
45
46
|
`\n` +
|
|
46
47
|
`Usage:\n` +
|
|
47
|
-
` 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` +
|
|
59
|
+
` slidemind plan <deck> Plan sign-off gate on <deck>/plan.md: opens it in the browser\n` +
|
|
60
|
+
` as a markdown editor + live preview + overall feedback box,\n` +
|
|
61
|
+
` blocks until the user decides, prints DECISION: approved | comments\n` +
|
|
62
|
+
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
48
63
|
` slidemind build <deck> Build self-contained HTML\n` +
|
|
49
64
|
` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
|
|
50
65
|
` approves or leaves comments (host-free)\n` +
|
|
51
|
-
` --no-open don't auto-open the browser\n` +
|
|
52
66
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
53
67
|
` slidemind build-pptx <deck> Export native .pptx via the add-in (requires an open host)\n` +
|
|
54
68
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
@@ -65,9 +79,16 @@ function printHelp() {
|
|
|
65
79
|
` --top <N> max results\n` +
|
|
66
80
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
67
81
|
` --json print the raw result payload\n` +
|
|
68
|
-
` slidemind
|
|
82
|
+
` slidemind view-slide [opts] Fetch a slide search result's preview image to a file (requires a host)\n` +
|
|
83
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
84
|
+
` --pick <n> 1-based selection number to view\n` +
|
|
85
|
+
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
86
|
+
` --out <dir> output dir (default: a slidemind temp dir)\n` +
|
|
87
|
+
` slidemind ls engines List the live engine catalog for an active pane\n` +
|
|
69
88
|
` --type <t> collection | search\n` +
|
|
70
89
|
` --subtype <s> image | slide | template | content\n` +
|
|
90
|
+
` --to <presId> target a specific window/backend\n` +
|
|
91
|
+
` --all include every paired backend\n` +
|
|
71
92
|
` --json print the raw engine list\n` +
|
|
72
93
|
` slidemind insert-slide [opts] Insert reused slide search results live into a window (requires a host)\n` +
|
|
73
94
|
` --search <id> searchId from 'slidemind search'\n` +
|
|
@@ -80,10 +101,17 @@ function printHelp() {
|
|
|
80
101
|
` --deck <deck> deck to add the slides to\n` +
|
|
81
102
|
` --to <presId> target the window the search ran on (required if >1 open)\n` +
|
|
82
103
|
` --at <N> numeric prefix to start at (default: append)\n` +
|
|
104
|
+
` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/ (requires a host)\n` +
|
|
105
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
106
|
+
` --pick <n,..> 1-based selection number(s)\n` +
|
|
107
|
+
` --deck <deck> deck to write the asset(s) into\n` +
|
|
108
|
+
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
109
|
+
` --icon-color <hex[,hex,..]> bake a colour into SVG result(s) (one for all, or one per --pick)\n` +
|
|
83
110
|
` slidemind download-bundle [deck] Materialize the active corporate design's bundle locally\n` +
|
|
84
111
|
` --to <presId> target a specific window\n` +
|
|
85
112
|
` --force re-download even if the cached bundle is current\n` +
|
|
86
|
-
` slidemind presentations List PowerPoint
|
|
113
|
+
` slidemind presentations List active PowerPoint panes registered with the add-in\n` +
|
|
114
|
+
` --all include retained suspended entries\n` +
|
|
87
115
|
` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
|
|
88
116
|
` --json print machine-readable status\n` +
|
|
89
117
|
` slidemind read [presId] Pull the current content of an open presentation (requires a host)\n` +
|
|
@@ -101,7 +129,7 @@ function printHelp() {
|
|
|
101
129
|
` slidemind whoami Show every paired environment (* = default)\n` +
|
|
102
130
|
` slidemind use <url|env> Set the default environment\n` +
|
|
103
131
|
` slidemind logout [url|env] Unpair one environment (--all to unpair every one)\n` +
|
|
104
|
-
` slidemind init --for <t> Set up slidemind in a harness (
|
|
132
|
+
` slidemind init --for <t> Set up slidemind in a harness (deploys the skill; clears the bundle cache)\n` +
|
|
105
133
|
` slidemind uninstall --for <t> Remove a previously-installed skill (clears the bundle cache)\n` +
|
|
106
134
|
` slidemind update [--for <t>] Fetch the latest release and redeploy\n` +
|
|
107
135
|
` --for <t> harness to redeploy into (default: all)\n` +
|
|
@@ -140,6 +168,31 @@ function resolveDeckArg(arg) {
|
|
|
140
168
|
return path.join(decksDir, arg);
|
|
141
169
|
}
|
|
142
170
|
|
|
171
|
+
// Best-effort human name of a materialized design: the first `# ` heading in
|
|
172
|
+
// the bundle's design.md, with the add-in's trailing " — design rules" suffix
|
|
173
|
+
// stripped. Returns null on any failure (missing file, no heading) so callers
|
|
174
|
+
// fall back to the raw presFP. Shared by `ready` and `new`'s auto-bind.
|
|
175
|
+
function readDesignName(bundleDir) {
|
|
176
|
+
if (!bundleDir) return null;
|
|
177
|
+
try {
|
|
178
|
+
const md = fs.readFileSync(path.join(bundleDir, "design.md"), "utf8");
|
|
179
|
+
for (const raw of md.split("\n")) {
|
|
180
|
+
const line = raw.trim();
|
|
181
|
+
if (line.startsWith("# ")) {
|
|
182
|
+
const name = line
|
|
183
|
+
.slice(2)
|
|
184
|
+
.trim()
|
|
185
|
+
.replace(/\s+[—-]\s+design rules\s*$/i, "")
|
|
186
|
+
.trim();
|
|
187
|
+
return name || null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
143
196
|
function runNode(script, args) {
|
|
144
197
|
return new Promise((resolve, reject) => {
|
|
145
198
|
const child = spawn(process.execPath, [script, ...args], {
|
|
@@ -180,6 +233,15 @@ async function cmdBuild(rest) {
|
|
|
180
233
|
]);
|
|
181
234
|
}
|
|
182
235
|
|
|
236
|
+
async function cmdPlan(rest) {
|
|
237
|
+
const { deck, passthrough } = splitDeckArg(rest, "plan");
|
|
238
|
+
await runNode(path.join(BUILDERS_DIR, "plan.mjs"), [
|
|
239
|
+
"--deck",
|
|
240
|
+
resolveDeckArg(deck),
|
|
241
|
+
...passthrough,
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
|
|
183
245
|
async function cmdReview(rest) {
|
|
184
246
|
const { deck, passthrough } = splitDeckArg(rest, "review");
|
|
185
247
|
await runNode(path.join(BUILDERS_DIR, "review.mjs"), [
|
|
@@ -238,8 +300,9 @@ async function cmdBuildPptx(rest) {
|
|
|
238
300
|
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
239
301
|
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
240
302
|
}
|
|
241
|
-
// The .pptx is rendered server-side by the add-in (one exporter, one truth)
|
|
242
|
-
//
|
|
303
|
+
// The .pptx is rendered server-side by the add-in (one exporter, one truth).
|
|
304
|
+
// A bound deck renders pane-free over the brand routes — no PowerPoint
|
|
305
|
+
// window needed; unbound decks (or --to) resolve through the pane registry.
|
|
243
306
|
const mod = await import(
|
|
244
307
|
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
245
308
|
);
|
|
@@ -250,7 +313,8 @@ async function cmdBuildPptx(rest) {
|
|
|
250
313
|
onProgress: renderProgressPrinter(),
|
|
251
314
|
});
|
|
252
315
|
process.stdout.write(
|
|
253
|
-
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in
|
|
316
|
+
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in` +
|
|
317
|
+
`${result.windowless ? " (no PowerPoint window needed)" : ""}\n`,
|
|
254
318
|
);
|
|
255
319
|
for (const w of result.warnings ?? []) {
|
|
256
320
|
process.stderr.write(` warning: ${w}\n`);
|
|
@@ -496,8 +560,11 @@ async function cmdSearch(rest) {
|
|
|
496
560
|
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${ranPresId}\n`,
|
|
497
561
|
);
|
|
498
562
|
} else {
|
|
563
|
+
// The searchId is bound to the window the search ran on — download must
|
|
564
|
+
// target that same window, so spell out its presId in the next-step command.
|
|
499
565
|
process.stdout.write(
|
|
500
|
-
`
|
|
566
|
+
`searchId ${res.searchId} ran on window ${ranPresId}${envTag} — download image result(s) into a deck:\n` +
|
|
567
|
+
` slidemind download-result --search ${res.searchId} --pick <n[,n,...]> --deck <deck> --to ${ranPresId} [--icon-color <hex>]\n`,
|
|
501
568
|
);
|
|
502
569
|
}
|
|
503
570
|
}
|
|
@@ -510,6 +577,7 @@ async function cmdLs(rest) {
|
|
|
510
577
|
let type = null;
|
|
511
578
|
let subtype = null;
|
|
512
579
|
let to = null;
|
|
580
|
+
let all = false;
|
|
513
581
|
let asJson = false;
|
|
514
582
|
for (let i = 0; i < subRest.length; i++) {
|
|
515
583
|
const a = subRest[i];
|
|
@@ -526,10 +594,10 @@ async function cmdLs(rest) {
|
|
|
526
594
|
fail(`--subtype expects image|slide|template|content, got '${subtype}'`);
|
|
527
595
|
}
|
|
528
596
|
} else if (a === "--to") {
|
|
529
|
-
// Accepted for symmetry with other commands; engines are backend-scoped,
|
|
530
|
-
// not presId-scoped, so it does not change the catalog.
|
|
531
597
|
to = subRest[++i];
|
|
532
598
|
if (!to) fail("--to expects a presId");
|
|
599
|
+
} else if (a === "--all") {
|
|
600
|
+
all = true;
|
|
533
601
|
} else if (a === "--json") {
|
|
534
602
|
asJson = true;
|
|
535
603
|
} else if (a.startsWith("-")) {
|
|
@@ -538,11 +606,17 @@ async function cmdLs(rest) {
|
|
|
538
606
|
fail(`unexpected argument: ${a}`);
|
|
539
607
|
}
|
|
540
608
|
}
|
|
541
|
-
void to;
|
|
542
609
|
const client = await import(
|
|
543
610
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
544
611
|
);
|
|
545
|
-
|
|
612
|
+
if (all && to) fail("--all cannot be combined with --to");
|
|
613
|
+
let backendURL = null;
|
|
614
|
+
if (!all) {
|
|
615
|
+
const registry = await client.requirePaneHost();
|
|
616
|
+
const entry = client.pickAnyPresentationEntry(registry, to);
|
|
617
|
+
backendURL = entry.backendURL ?? null;
|
|
618
|
+
}
|
|
619
|
+
const { engines } = await client.listEngines({ type, subtype, backendURL });
|
|
546
620
|
if (asJson) {
|
|
547
621
|
process.stdout.write(`${JSON.stringify(engines, null, 2)}\n`);
|
|
548
622
|
return;
|
|
@@ -686,8 +760,8 @@ async function cmdAddSlide(rest) {
|
|
|
686
760
|
|
|
687
761
|
// Extract each picked slide's bytes over the bridge, then let the shared
|
|
688
762
|
// helper compute the play-order filenames (NN-reused-<shortid>.pptx). The
|
|
689
|
-
// prefix math
|
|
690
|
-
//
|
|
763
|
+
// prefix math lives in one shared unit (src/lib/reused-slides.mjs). Existing
|
|
764
|
+
// files are not renumbered.
|
|
691
765
|
const reused = await import(
|
|
692
766
|
pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
|
|
693
767
|
);
|
|
@@ -721,6 +795,186 @@ async function cmdAddSlide(rest) {
|
|
|
721
795
|
);
|
|
722
796
|
}
|
|
723
797
|
|
|
798
|
+
// Map a `--icon-color` value onto the `--pick` list. Accepts either one colour
|
|
799
|
+
// (applied to every pick) or exactly one per pick (positional); an empty
|
|
800
|
+
// segment means "no colour for this pick". Returns an array aligned to picks.
|
|
801
|
+
function parseIconColors(raw, pickCount) {
|
|
802
|
+
if (!raw) return new Array(pickCount).fill(null);
|
|
803
|
+
const colors = raw
|
|
804
|
+
.split(",")
|
|
805
|
+
.map((s) => s.trim())
|
|
806
|
+
.map((s) => (s.length ? s : null));
|
|
807
|
+
for (const c of colors) {
|
|
808
|
+
if (c !== null && !/^#[0-9a-fA-F]{3,8}$/.test(c)) {
|
|
809
|
+
fail(`--icon-color expects hex colours like #0A7E8C, got '${c}'`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (colors.length === 1) return new Array(pickCount).fill(colors[0]);
|
|
813
|
+
if (colors.length !== pickCount) {
|
|
814
|
+
fail(
|
|
815
|
+
`--icon-color takes one colour (applied to all) or exactly ${pickCount} (one per --pick); got ${colors.length}`,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return colors;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function cmdViewSlide(rest) {
|
|
822
|
+
let searchId = null;
|
|
823
|
+
let pick = null;
|
|
824
|
+
let to = null;
|
|
825
|
+
let outDir = null;
|
|
826
|
+
for (let i = 0; i < rest.length; i++) {
|
|
827
|
+
const a = rest[i];
|
|
828
|
+
if (a === "--search") {
|
|
829
|
+
const next = rest[++i];
|
|
830
|
+
const n = parseInt(next, 10);
|
|
831
|
+
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
832
|
+
searchId = n;
|
|
833
|
+
} else if (a === "--pick") {
|
|
834
|
+
const next = rest[++i];
|
|
835
|
+
const n = parseInt(next, 10);
|
|
836
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
837
|
+
fail(`--pick expects a positive selection number, got '${next}'`);
|
|
838
|
+
}
|
|
839
|
+
pick = n;
|
|
840
|
+
} else if (a === "--to") {
|
|
841
|
+
to = rest[++i];
|
|
842
|
+
if (!to) fail("--to expects a presId");
|
|
843
|
+
} else if (a === "--out" || a === "-o") {
|
|
844
|
+
outDir = rest[++i];
|
|
845
|
+
if (!outDir) fail("--out expects a directory");
|
|
846
|
+
} else if (a.startsWith("-")) {
|
|
847
|
+
fail(`unknown flag: ${a}`);
|
|
848
|
+
} else {
|
|
849
|
+
fail(`unexpected argument: ${a}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (searchId === null || pick === null) {
|
|
853
|
+
fail("usage: slidemind view-slide --search <id> --pick <n> --to <presId> [--out <dir>]");
|
|
854
|
+
}
|
|
855
|
+
const client = await import(
|
|
856
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
857
|
+
);
|
|
858
|
+
const { extFromMime } = await import(
|
|
859
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
860
|
+
);
|
|
861
|
+
const registry = await client.requirePaneHost();
|
|
862
|
+
// The searchId is presentation-scoped, so --to targets the window the search
|
|
863
|
+
// ran on. Without it, a registry with >1 open window is a dead-end.
|
|
864
|
+
const entry = client.pickPresentationEntry(registry, to);
|
|
865
|
+
const res = await client.viewSlide({
|
|
866
|
+
presId: entry.presId,
|
|
867
|
+
searchId,
|
|
868
|
+
selection: pick,
|
|
869
|
+
backendURL: entry.backendURL ?? null,
|
|
870
|
+
});
|
|
871
|
+
if (!res?.ok || !res.dataUri) {
|
|
872
|
+
fail(`view-slide failed: ${res?.error ?? "no image returned"}`);
|
|
873
|
+
}
|
|
874
|
+
const m = /^data:([^;]+);base64,(.+)$/.exec(res.dataUri);
|
|
875
|
+
if (!m) fail("view-slide returned an unparseable data URI");
|
|
876
|
+
const dir = outDir
|
|
877
|
+
? path.resolve(process.cwd(), outDir)
|
|
878
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-view-"));
|
|
879
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
880
|
+
const target = path.join(dir, `slide-${searchId}-${pick}.${extFromMime(m[1])}`);
|
|
881
|
+
fs.writeFileSync(target, Buffer.from(m[2], "base64"));
|
|
882
|
+
process.stdout.write(
|
|
883
|
+
`Wrote preview of result ${pick} (searchId ${searchId}) to ${target}\n`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function cmdDownloadResult(rest) {
|
|
888
|
+
let searchId = null;
|
|
889
|
+
let picks = null;
|
|
890
|
+
let deck = null;
|
|
891
|
+
let to = null;
|
|
892
|
+
let iconColorsRaw = null;
|
|
893
|
+
for (let i = 0; i < rest.length; i++) {
|
|
894
|
+
const a = rest[i];
|
|
895
|
+
if (a === "--search") {
|
|
896
|
+
const next = rest[++i];
|
|
897
|
+
const n = parseInt(next, 10);
|
|
898
|
+
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
899
|
+
searchId = n;
|
|
900
|
+
} else if (a === "--pick") {
|
|
901
|
+
picks = parsePicks(rest[++i]);
|
|
902
|
+
} else if (a === "--deck") {
|
|
903
|
+
deck = rest[++i];
|
|
904
|
+
if (!deck) fail("--deck expects a deck name or path");
|
|
905
|
+
} else if (a === "--to") {
|
|
906
|
+
to = rest[++i];
|
|
907
|
+
if (!to) fail("--to expects a presId");
|
|
908
|
+
} else if (a === "--icon-color" || a === "--icon-colour") {
|
|
909
|
+
iconColorsRaw = rest[++i];
|
|
910
|
+
if (!iconColorsRaw) fail("--icon-color expects a hex colour (or comma-separated list aligned to --pick)");
|
|
911
|
+
} else if (a.startsWith("-")) {
|
|
912
|
+
fail(`unknown flag: ${a}`);
|
|
913
|
+
} else {
|
|
914
|
+
fail(`unexpected argument: ${a}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (searchId === null || !picks || !deck) {
|
|
918
|
+
fail(
|
|
919
|
+
"usage: slidemind download-result --search <id> --pick <n[,n,...]> --deck <deck> --to <presId> [--icon-color <hex[,hex,...]>]",
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const iconColors = parseIconColors(iconColorsRaw, picks.length);
|
|
923
|
+
const deckDir = resolveDeckArg(deck);
|
|
924
|
+
const assetsDir = path.join(deckDir, "assets");
|
|
925
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
926
|
+
|
|
927
|
+
const client = await import(
|
|
928
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
929
|
+
);
|
|
930
|
+
// Shared image/icon download logic (src/bridge/engine-download.mjs).
|
|
931
|
+
const { downloadOneResult } = await import(
|
|
932
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
933
|
+
);
|
|
934
|
+
const registry = await client.requirePaneHost();
|
|
935
|
+
// The searchId is presentation-scoped, so --to targets the window the search
|
|
936
|
+
// ran on. Without it, a registry with >1 open window is a dead-end.
|
|
937
|
+
const entry = client.pickPresentationEntry(registry, to);
|
|
938
|
+
|
|
939
|
+
// Concurrent — each download is its own bridge round-trip; allSettled so one
|
|
940
|
+
// bad selection doesn't sink the rest of the batch.
|
|
941
|
+
const settled = await Promise.allSettled(
|
|
942
|
+
picks.map((selection, i) =>
|
|
943
|
+
downloadOneResult({
|
|
944
|
+
client,
|
|
945
|
+
presId: entry.presId,
|
|
946
|
+
backendURL: entry.backendURL ?? null,
|
|
947
|
+
searchId,
|
|
948
|
+
assetsDir,
|
|
949
|
+
selection,
|
|
950
|
+
iconColor: iconColors[i],
|
|
951
|
+
}),
|
|
952
|
+
),
|
|
953
|
+
);
|
|
954
|
+
const results = settled.map((r, i) =>
|
|
955
|
+
r.status === "fulfilled"
|
|
956
|
+
? r.value
|
|
957
|
+
: { selection: picks[i], error: r.reason?.message ?? String(r.reason) },
|
|
958
|
+
);
|
|
959
|
+
let ok = 0;
|
|
960
|
+
const lines = [];
|
|
961
|
+
for (const r of results) {
|
|
962
|
+
if (r.error) {
|
|
963
|
+
lines.push(` ✗ selection ${r.selection}: ${r.error}`);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
ok++;
|
|
967
|
+
const note = r.recolor?.note ? ` — ${r.recolor.note}` : "";
|
|
968
|
+
lines.push(` ${r.reference} (${r.recolor?.mode ?? "?"})${note}`);
|
|
969
|
+
}
|
|
970
|
+
process.stdout.write(
|
|
971
|
+
`Downloaded ${ok}/${results.length} result(s) into ${path.relative(process.cwd(), assetsDir)}:\n` +
|
|
972
|
+
lines.join("\n") +
|
|
973
|
+
`\nReference each from slide JSX as <img src="./assets/...">.\n`,
|
|
974
|
+
);
|
|
975
|
+
if (ok === 0) process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
|
|
724
978
|
async function cmdDownloadBundle(rest) {
|
|
725
979
|
let to = null;
|
|
726
980
|
let force = false;
|
|
@@ -789,23 +1043,38 @@ async function cmdDownloadBundle(rest) {
|
|
|
789
1043
|
|
|
790
1044
|
async function cmdPresentations(rest) {
|
|
791
1045
|
const asJson = rest.includes("--json");
|
|
1046
|
+
const showAll = rest.includes("--all");
|
|
792
1047
|
const mod = await import(
|
|
793
1048
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
794
1049
|
);
|
|
795
|
-
|
|
1050
|
+
// `--all` is a raw immediate inspection snapshot (no wait). Otherwise wait out
|
|
1051
|
+
// the brief re-registration window after a pane reload / PowerPoint wake so a
|
|
1052
|
+
// freshly-suspended pane doesn't read as an empty list. Either way we LIST
|
|
1053
|
+
// every entry with its status — suspended panes are honest render/push targets
|
|
1054
|
+
// and hiding them made the CLI say "nothing" during the flaky window.
|
|
1055
|
+
const list = showAll
|
|
1056
|
+
? await mod.listOpenPresentations()
|
|
1057
|
+
: (await mod.listReadyPresentationsWithWait()).all;
|
|
1058
|
+
// Connected/pane-ready panes first, then suspended/stale ones (stable sort
|
|
1059
|
+
// keeps each group's incoming order).
|
|
1060
|
+
const sorted = [...list].sort(
|
|
1061
|
+
(a, b) =>
|
|
1062
|
+
(mod.isPaneReadyPresentation(b) ? 1 : 0) -
|
|
1063
|
+
(mod.isPaneReadyPresentation(a) ? 1 : 0),
|
|
1064
|
+
);
|
|
796
1065
|
if (asJson) {
|
|
797
1066
|
process.stdout.write(
|
|
798
|
-
JSON.stringify(
|
|
1067
|
+
JSON.stringify(sorted.map(mod.toPublicPresentation), null, 2) + "\n",
|
|
799
1068
|
);
|
|
800
1069
|
return;
|
|
801
1070
|
}
|
|
802
|
-
if (
|
|
1071
|
+
if (sorted.length === 0) {
|
|
803
1072
|
process.stderr.write(
|
|
804
|
-
"No PowerPoint
|
|
1073
|
+
"No active PowerPoint panes registered with the slidemind add-in.\n",
|
|
805
1074
|
);
|
|
806
1075
|
return;
|
|
807
1076
|
}
|
|
808
|
-
for (const p of
|
|
1077
|
+
for (const p of sorted) {
|
|
809
1078
|
const label = p.title || p.fileName || "(untitled)";
|
|
810
1079
|
const env = p.env ? `[${p.env}]\t` : "";
|
|
811
1080
|
const state =
|
|
@@ -813,6 +1082,16 @@ async function cmdPresentations(rest) {
|
|
|
813
1082
|
(p.connected === false || p.suspendedAt ? "suspended" : "connected");
|
|
814
1083
|
const design = p.designState ? `\tcd:${p.designState}` : "";
|
|
815
1084
|
process.stdout.write(`${p.presId}\t${env}${label}\t${state}${design}\n`);
|
|
1085
|
+
// A pane that can't take live ops yet (suspended/stale/hydrating) is still
|
|
1086
|
+
// listed — with the one actionable line to wake it — so the reader sees an
|
|
1087
|
+
// honest state instead of an empty registry.
|
|
1088
|
+
if (!mod.isPaneReadyPresentation(p)) {
|
|
1089
|
+
const hint =
|
|
1090
|
+
p.nextAction && typeof p.nextAction.message === "string"
|
|
1091
|
+
? p.nextAction.message
|
|
1092
|
+
: "bring PowerPoint forward and click the slidemind pane";
|
|
1093
|
+
process.stdout.write(`\t${hint}\n`);
|
|
1094
|
+
}
|
|
816
1095
|
}
|
|
817
1096
|
}
|
|
818
1097
|
|
|
@@ -825,6 +1104,8 @@ function publicConnectionStatus(status, client) {
|
|
|
825
1104
|
ok: b.ok,
|
|
826
1105
|
readinessV1: b.readinessV1 === true,
|
|
827
1106
|
capabilities: b.capabilities ?? null,
|
|
1107
|
+
minCliVersion: b.minCliVersion ?? null,
|
|
1108
|
+
cliUpdateRequired: b.cliUpdateRequired === true,
|
|
828
1109
|
error: b.error,
|
|
829
1110
|
summary: b.summary ?? null,
|
|
830
1111
|
presentations: (b.presentations ?? []).map(client.toPublicPresentation),
|
|
@@ -833,15 +1114,52 @@ function publicConnectionStatus(status, client) {
|
|
|
833
1114
|
};
|
|
834
1115
|
}
|
|
835
1116
|
|
|
836
|
-
|
|
1117
|
+
// The ordered connection-readiness ladder as a STRUCTURED blocker: the first
|
|
1118
|
+
// unmet precondition, or null when the connection is fully ready. `code` is the
|
|
1119
|
+
// machine-copeable reason an agent switches on; `actor` says who can clear it —
|
|
1120
|
+
// only `cli_update_required` is the AGENT's to fix (it can run `slidemind
|
|
1121
|
+
// update`), every other rung needs a human at the add-in. The `message` strings
|
|
1122
|
+
// are BYTE-IDENTICAL to the old connectionNextAction ladder so doctor's printed
|
|
1123
|
+
// output never churns (connectionNextAction is now a thin projection over this).
|
|
1124
|
+
// `slidemind ready` layers `ambiguous_design` on top and treats `pane_suspended`
|
|
1125
|
+
// as non-blocking (a render-ready-but-suspended window is still a design source).
|
|
1126
|
+
function connectionBlocker(pub) {
|
|
837
1127
|
if (!pub.paired) {
|
|
838
|
-
return
|
|
1128
|
+
return {
|
|
1129
|
+
code: "not_paired",
|
|
1130
|
+
actor: "user",
|
|
1131
|
+
message:
|
|
1132
|
+
"Not paired. In the add-in open Settings → General → Connect CLI, press Pair, then run the shown `slidemind pair <code>`.",
|
|
1133
|
+
};
|
|
839
1134
|
}
|
|
840
1135
|
if ((pub.backends ?? []).every((b) => !b.ok)) {
|
|
841
|
-
return
|
|
1136
|
+
return {
|
|
1137
|
+
code: "all_unreachable",
|
|
1138
|
+
actor: "user",
|
|
1139
|
+
message:
|
|
1140
|
+
"Every paired backend is unreachable. Check network/VPN/backend status, or re-pair if the environment moved.",
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
// Version skew explains most downstream failures — surface it before pane
|
|
1144
|
+
// readiness so the user fixes the right thing first.
|
|
1145
|
+
const outdated = (pub.backends ?? []).find((b) => b.cliUpdateRequired);
|
|
1146
|
+
if (outdated) {
|
|
1147
|
+
return {
|
|
1148
|
+
code: "cli_update_required",
|
|
1149
|
+
actor: "agent",
|
|
1150
|
+
message:
|
|
1151
|
+
`This CLI is older than the minimum the '${outdated.env}' add-in supports` +
|
|
1152
|
+
`${outdated.minCliVersion ? ` (${outdated.minCliVersion})` : ""}. ` +
|
|
1153
|
+
"Run `slidemind update` (or `npm install -g slidemind@latest`), then retry.",
|
|
1154
|
+
};
|
|
842
1155
|
}
|
|
843
1156
|
if ((pub.presentations ?? []).length === 0) {
|
|
844
|
-
return
|
|
1157
|
+
return {
|
|
1158
|
+
code: "no_window",
|
|
1159
|
+
actor: "user",
|
|
1160
|
+
message:
|
|
1161
|
+
"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.",
|
|
1162
|
+
};
|
|
845
1163
|
}
|
|
846
1164
|
const renderReady = pub.presentations.some(
|
|
847
1165
|
(p) =>
|
|
@@ -850,7 +1168,12 @@ function connectionNextAction(pub) {
|
|
|
850
1168
|
p.designState === "ready",
|
|
851
1169
|
);
|
|
852
1170
|
if (!renderReady) {
|
|
853
|
-
return
|
|
1171
|
+
return {
|
|
1172
|
+
code: "not_render_ready",
|
|
1173
|
+
actor: "user",
|
|
1174
|
+
message:
|
|
1175
|
+
"PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.",
|
|
1176
|
+
};
|
|
854
1177
|
}
|
|
855
1178
|
const paneReady = pub.presentations.some(
|
|
856
1179
|
(p) =>
|
|
@@ -860,9 +1183,418 @@ function connectionNextAction(pub) {
|
|
|
860
1183
|
!p.suspendedAt),
|
|
861
1184
|
);
|
|
862
1185
|
if (!paneReady) {
|
|
863
|
-
return
|
|
1186
|
+
return {
|
|
1187
|
+
code: "pane_suspended",
|
|
1188
|
+
actor: "user",
|
|
1189
|
+
message:
|
|
1190
|
+
"Rendering is available, but pane operations are suspended. Bring PowerPoint forward and click/reopen the slidemind pane before push/search/read.",
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Back-compat projection for doctor: just the blocker's message, or "Ready."
|
|
1197
|
+
// when the ladder is clear. Keeps the ladder authored in ONE place.
|
|
1198
|
+
function connectionNextAction(pub) {
|
|
1199
|
+
return connectionBlocker(pub)?.message ?? "Ready.";
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// The one-command agent preflight. Collapses the old 4-call session start
|
|
1203
|
+
// (presentations → pick → download-bundle → download-bundle-to-bind) into a
|
|
1204
|
+
// single verb: check pairing/reachability/CLI-version/windows, resolve the OPEN
|
|
1205
|
+
// design (the add-in is the source of truth), materialize its bundle, bind the
|
|
1206
|
+
// deck, and print exactly one actionable blocker when it can't. Exit 0 iff
|
|
1207
|
+
// ready; every blocker → exit 1 with a machine-copeable code. Never prints a
|
|
1208
|
+
// backendURL (env label only).
|
|
1209
|
+
async function cmdReady(rest) {
|
|
1210
|
+
// --- Local arg parse: unknown flags fail BEFORE any network (cli-dispatch
|
|
1211
|
+
// convention). ---
|
|
1212
|
+
let to = null;
|
|
1213
|
+
let asJson = false;
|
|
1214
|
+
let force = false;
|
|
1215
|
+
let connectOnly = false;
|
|
1216
|
+
const positional = [];
|
|
1217
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1218
|
+
const a = rest[i];
|
|
1219
|
+
if (a === "--to") {
|
|
1220
|
+
to = rest[++i];
|
|
1221
|
+
if (!to) fail("--to expects a presId");
|
|
1222
|
+
} else if (a === "--json") {
|
|
1223
|
+
asJson = true;
|
|
1224
|
+
} else if (a === "--force") {
|
|
1225
|
+
force = true;
|
|
1226
|
+
} else if (a === "--connect-only") {
|
|
1227
|
+
connectOnly = true;
|
|
1228
|
+
} else if (a.startsWith("-")) {
|
|
1229
|
+
fail(`unknown flag: ${a}`);
|
|
1230
|
+
} else {
|
|
1231
|
+
positional.push(a);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// --- Local: resolve the deck dir (never create it) and read its existing
|
|
1236
|
+
// bundle ref. deckPresFP feeds ONLY the mirror tie-break + `suggested`; it
|
|
1237
|
+
// never auto-picks a design. ---
|
|
1238
|
+
const deckArg = positional[0] ?? null;
|
|
1239
|
+
const bundle = await import(
|
|
1240
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
1241
|
+
);
|
|
1242
|
+
let deckDir = null;
|
|
1243
|
+
let deckSlug = null;
|
|
1244
|
+
let deckPresFP = null;
|
|
1245
|
+
if (deckArg) {
|
|
1246
|
+
deckDir = resolveDeckArg(deckArg);
|
|
1247
|
+
if (!fs.existsSync(deckDir)) {
|
|
1248
|
+
fail(
|
|
1249
|
+
`deck '${deckArg}' not found — run \`slidemind ready\` (no deck) to set up, then \`slidemind new ${deckArg}\``,
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
deckSlug = path.basename(deckDir);
|
|
1253
|
+
deckPresFP = bundle.readDeckBundleRef(deckDir)?.presFP ?? null;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Emit a structured blocker (JSON to stdout / human to stderr) and exit 1.
|
|
1257
|
+
// `humanLines` overrides the default `slidemind: <message>` for the richer
|
|
1258
|
+
// ambiguous / unknown-window shapes. Mirrors doctor's stream conventions:
|
|
1259
|
+
// machine JSON on stdout, human prose on stderr.
|
|
1260
|
+
const finishBlocker = ({ blocker, extras = {}, humanLines = null }) => {
|
|
1261
|
+
if (asJson) {
|
|
1262
|
+
process.stdout.write(
|
|
1263
|
+
JSON.stringify(
|
|
1264
|
+
{ ready: false, blocker, nextAction: blocker.message, ...extras },
|
|
1265
|
+
null,
|
|
1266
|
+
2,
|
|
1267
|
+
) + "\n",
|
|
1268
|
+
);
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
if (humanLines) {
|
|
1272
|
+
process.stderr.write(
|
|
1273
|
+
humanLines.endsWith("\n") ? humanLines : `${humanLines}\n`,
|
|
1274
|
+
);
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1277
|
+
fail(blocker.message);
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// --- Local: not paired short-circuits before any network. ---
|
|
1281
|
+
const cfg = await loadBridgeModule("config");
|
|
1282
|
+
if (cfg.readSessions().length === 0) {
|
|
1283
|
+
finishBlocker({ blocker: connectionBlocker({ paired: false }) });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const client = await import(
|
|
1288
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
// --- Network (one parallel round): readiness ladder + the merged registry. ---
|
|
1292
|
+
const [statusRaw, detailed] = await Promise.all([
|
|
1293
|
+
client.connectionStatus(),
|
|
1294
|
+
client.listOpenPresentationsDetailed(),
|
|
1295
|
+
]);
|
|
1296
|
+
const pub = publicConnectionStatus(statusRaw, client);
|
|
1297
|
+
let registry = detailed.presentations;
|
|
1298
|
+
|
|
1299
|
+
// Terminal blockers: waiting can't fix an unreachable backend or a stale CLI.
|
|
1300
|
+
const terminal = connectionBlocker(pub);
|
|
1301
|
+
if (
|
|
1302
|
+
terminal &&
|
|
1303
|
+
(terminal.code === "all_unreachable" || terminal.code === "cli_update_required")
|
|
1304
|
+
) {
|
|
1305
|
+
finishBlocker({ blocker: terminal });
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// --- Bounded hydration wait when nothing is render-ready yet: covers the
|
|
1310
|
+
// just-clicked / just-woke pane still hydrating brand metadata. Polls even on
|
|
1311
|
+
// an empty registry (that's the flaky window this papers over). ---
|
|
1312
|
+
const isRR = client.isRenderReadyPresentation;
|
|
1313
|
+
if (!registry.some(isRR)) {
|
|
1314
|
+
const waitMs =
|
|
1315
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_PRESENTATIONS_WAIT_MS || "6000", 10) ||
|
|
1316
|
+
6000;
|
|
1317
|
+
const pollMs =
|
|
1318
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_READY_POLL_MS || "1500", 10) || 1500;
|
|
1319
|
+
const deadline = Date.now() + waitMs;
|
|
1320
|
+
while (!registry.some(isRR) && Date.now() < deadline) {
|
|
1321
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1322
|
+
registry = (await client.listOpenPresentationsDetailed()).presentations;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (!registry.some(isRR)) {
|
|
1326
|
+
// --- Pane-free rung: a BOUND deck readies over the brand routes with no
|
|
1327
|
+
// window at all — the design identity (presFP == brandId) lives in the
|
|
1328
|
+
// deck's .bundle-ref.json, and the render half never needed the pane.
|
|
1329
|
+
// Degraded: build-pptx/build/review/view-draft work; push/search/read
|
|
1330
|
+
// still need a pane. Skipped for --connect-only (search needs a pane) and
|
|
1331
|
+
// --to (an explicit window must resolve through the registry). Any miss
|
|
1332
|
+
// (old add-in, legacy ref, revoked grant) falls through to the ladder.
|
|
1333
|
+
if (deckDir && deckPresFP && !to && !connectOnly) {
|
|
1334
|
+
let result = null;
|
|
1335
|
+
try {
|
|
1336
|
+
result = await bundle.materializeBundle(null, {
|
|
1337
|
+
force,
|
|
1338
|
+
brandId: deckPresFP,
|
|
1339
|
+
});
|
|
1340
|
+
} catch {
|
|
1341
|
+
/* fall through to the standard no_window / not_render_ready blocker */
|
|
1342
|
+
}
|
|
1343
|
+
if (result) {
|
|
1344
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
1345
|
+
presFP: result.presFP,
|
|
1346
|
+
etag: result.etag,
|
|
1347
|
+
});
|
|
1348
|
+
const designName = readDesignName(result.dir);
|
|
1349
|
+
const env = result.backendURL
|
|
1350
|
+
? cfg.deriveEnvLabel(result.backendURL)
|
|
1351
|
+
: null;
|
|
1352
|
+
const nextAction =
|
|
1353
|
+
"Ready (no PowerPoint window — build-pptx/build/review/view-draft work; push/search/read need a pane).";
|
|
1354
|
+
if (asJson) {
|
|
1355
|
+
process.stdout.write(
|
|
1356
|
+
JSON.stringify(
|
|
1357
|
+
{
|
|
1358
|
+
ready: true,
|
|
1359
|
+
windowless: true,
|
|
1360
|
+
deck: deckSlug,
|
|
1361
|
+
bound: true,
|
|
1362
|
+
env,
|
|
1363
|
+
presId: null,
|
|
1364
|
+
presFP: result.presFP,
|
|
1365
|
+
designName,
|
|
1366
|
+
bundleDir: result.dir,
|
|
1367
|
+
bundleEtag: result.etag ?? null,
|
|
1368
|
+
paneSuspended: true,
|
|
1369
|
+
warnings: result.warnings ?? [],
|
|
1370
|
+
nextAction,
|
|
1371
|
+
},
|
|
1372
|
+
null,
|
|
1373
|
+
2,
|
|
1374
|
+
) + "\n",
|
|
1375
|
+
);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const designLabel = designName
|
|
1379
|
+
? `${designName} (${result.presFP})`
|
|
1380
|
+
: result.presFP;
|
|
1381
|
+
process.stdout.write(
|
|
1382
|
+
`Ready — design ${designLabel}${env ? ` [${env}]` : ""} (no PowerPoint window)\n`,
|
|
1383
|
+
);
|
|
1384
|
+
process.stdout.write(
|
|
1385
|
+
` bundle: ${result.dir} (etag ${result.etag ?? "?"})\n`,
|
|
1386
|
+
);
|
|
1387
|
+
process.stdout.write(` deck ${deckSlug} bound\n`);
|
|
1388
|
+
process.stdout.write(
|
|
1389
|
+
" Note: build-pptx/build/review/view-draft work without a window; push/search/read need a pane.\n",
|
|
1390
|
+
);
|
|
1391
|
+
for (const w of result.warnings ?? []) {
|
|
1392
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
1393
|
+
}
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
// Only the no-window / not-render-ready rungs are reachable here (terminal
|
|
1398
|
+
// blockers already returned; a render-ready window would have exited the
|
|
1399
|
+
// poll). Reuse the ladder's exact strings over the freshly-polled registry.
|
|
1400
|
+
const latePub = {
|
|
1401
|
+
paired: true,
|
|
1402
|
+
backendCount: pub.backendCount,
|
|
1403
|
+
backends: pub.backends,
|
|
1404
|
+
presentations: registry.map(client.toPublicPresentation),
|
|
1405
|
+
};
|
|
1406
|
+
const b =
|
|
1407
|
+
connectionBlocker(latePub) ?? {
|
|
1408
|
+
code: "not_render_ready",
|
|
1409
|
+
actor: "user",
|
|
1410
|
+
message:
|
|
1411
|
+
"PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.",
|
|
1412
|
+
};
|
|
1413
|
+
finishBlocker({ blocker: b });
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// --- --connect-only: the connection is up with ≥1 render-ready window; the
|
|
1418
|
+
// search skill needs nothing more (no design resolution, no bundle). ---
|
|
1419
|
+
if (connectOnly) {
|
|
1420
|
+
if (asJson) {
|
|
1421
|
+
process.stdout.write(
|
|
1422
|
+
JSON.stringify(
|
|
1423
|
+
{
|
|
1424
|
+
ready: true,
|
|
1425
|
+
connectOnly: true,
|
|
1426
|
+
nextAction: "Ready.",
|
|
1427
|
+
backends: (pub.backends ?? []).map((b) => ({ env: b.env, ok: b.ok })),
|
|
1428
|
+
},
|
|
1429
|
+
null,
|
|
1430
|
+
2,
|
|
1431
|
+
) + "\n",
|
|
1432
|
+
);
|
|
1433
|
+
} else {
|
|
1434
|
+
const windows = registry.filter(isRR).length;
|
|
1435
|
+
process.stdout.write(
|
|
1436
|
+
`Connected. ${pub.backendCount} backend(s), ${windows} window(s) registered.\n`,
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// --- Local: resolve WHICH design to bind against. ---
|
|
1443
|
+
const resolution = client.resolveDesignSource(registry, {
|
|
1444
|
+
explicitPresId: to,
|
|
1445
|
+
deckPresFP,
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
if (resolution.status === "none") {
|
|
1449
|
+
// Defensive: a render-ready window exists (poll passed) so this is rare.
|
|
1450
|
+
finishBlocker({
|
|
1451
|
+
blocker: {
|
|
1452
|
+
code: "not_render_ready",
|
|
1453
|
+
actor: "user",
|
|
1454
|
+
message:
|
|
1455
|
+
"PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.",
|
|
1456
|
+
},
|
|
1457
|
+
});
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (resolution.status === "unknown_presid") {
|
|
1462
|
+
const windows = registry.filter(isRR).map(client.toPublicPresentation);
|
|
1463
|
+
const candidates = windows.map((p) => ({
|
|
1464
|
+
presId: p.presId,
|
|
1465
|
+
env: p.env ?? null,
|
|
1466
|
+
title: p.title ?? null,
|
|
1467
|
+
fileName: p.fileName ?? null,
|
|
1468
|
+
}));
|
|
1469
|
+
const blocker = {
|
|
1470
|
+
code: "unknown_presid",
|
|
1471
|
+
actor: "user",
|
|
1472
|
+
message: `No render-ready PowerPoint window has presId='${to}'. Re-run 'slidemind ready --to <presId>' with one of the open windows.`,
|
|
1473
|
+
};
|
|
1474
|
+
const rows = windows.map(
|
|
1475
|
+
(p) =>
|
|
1476
|
+
` ${p.presId} ${p.env ? `[${p.env}] ` : ""}${p.title || p.fileName || "(untitled)"}`,
|
|
1477
|
+
);
|
|
1478
|
+
finishBlocker({
|
|
1479
|
+
blocker,
|
|
1480
|
+
extras: { candidates },
|
|
1481
|
+
humanLines:
|
|
1482
|
+
`slidemind: ${blocker.message}\n` +
|
|
1483
|
+
`${rows.join("\n")}`,
|
|
1484
|
+
});
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (resolution.status === "ambiguous") {
|
|
1489
|
+
const cand = resolution.candidates;
|
|
1490
|
+
const blocker = {
|
|
1491
|
+
code: "ambiguous_design",
|
|
1492
|
+
actor: "user",
|
|
1493
|
+
message:
|
|
1494
|
+
"Multiple corporate designs are open — ask the user which to build against, then re-run: slidemind ready --to <presId>.",
|
|
1495
|
+
};
|
|
1496
|
+
const rows = cand.map((c) => {
|
|
1497
|
+
const env = c.env ? ` [${c.env}]` : "";
|
|
1498
|
+
const label = c.title || c.fileName || "(untitled)";
|
|
1499
|
+
return ` ${c.presId}${env} ${label} (design ${c.brandId ?? "?"})`;
|
|
1500
|
+
});
|
|
1501
|
+
let human =
|
|
1502
|
+
"slidemind: multiple corporate designs are open — ask the user which to build against:\n" +
|
|
1503
|
+
`${rows.join("\n")}\n`;
|
|
1504
|
+
if (resolution.suggested) {
|
|
1505
|
+
const s = cand.find((c) => c.presId === resolution.suggested);
|
|
1506
|
+
const sLabel = s?.title || s?.fileName || "(untitled)";
|
|
1507
|
+
human += `(this deck was last built against ${s?.brandId ?? deckPresFP} — ${sLabel})\n`;
|
|
1508
|
+
}
|
|
1509
|
+
human += "Then re-run: slidemind ready --to <presId>";
|
|
1510
|
+
finishBlocker({
|
|
1511
|
+
blocker,
|
|
1512
|
+
extras: {
|
|
1513
|
+
candidates: cand.map((c) => ({
|
|
1514
|
+
presId: c.presId,
|
|
1515
|
+
env: c.env ?? null,
|
|
1516
|
+
title: c.title ?? null,
|
|
1517
|
+
fileName: c.fileName ?? null,
|
|
1518
|
+
presFP: c.brandId ?? null,
|
|
1519
|
+
})),
|
|
1520
|
+
suggested: resolution.suggested,
|
|
1521
|
+
},
|
|
1522
|
+
humanLines: human,
|
|
1523
|
+
});
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// --- Network: materialize the resolved design's bundle. Always called —
|
|
1528
|
+
// incremental etag-gated download makes an unchanged bundle near-free and a
|
|
1529
|
+
// changed one refreshes. ---
|
|
1530
|
+
const entry = resolution.entry;
|
|
1531
|
+
const result = await bundle.materializeBundle(entry.presId, {
|
|
1532
|
+
force,
|
|
1533
|
+
backendURL: entry.backendURL ?? null,
|
|
1534
|
+
preferBrandId: entry.brandId ?? null,
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
// --- Local: bind the deck (kills the second download-bundle). ---
|
|
1538
|
+
let bound = false;
|
|
1539
|
+
if (deckDir) {
|
|
1540
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
1541
|
+
presFP: result.presFP,
|
|
1542
|
+
etag: result.etag,
|
|
1543
|
+
});
|
|
1544
|
+
bound = true;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const designName = readDesignName(result.dir);
|
|
1548
|
+
const paneSuspended = !client.isPaneReadyPresentation(entry);
|
|
1549
|
+
|
|
1550
|
+
if (asJson) {
|
|
1551
|
+
process.stdout.write(
|
|
1552
|
+
JSON.stringify(
|
|
1553
|
+
{
|
|
1554
|
+
ready: true,
|
|
1555
|
+
deck: deckSlug,
|
|
1556
|
+
bound,
|
|
1557
|
+
env: entry.env ?? null,
|
|
1558
|
+
presId: entry.presId,
|
|
1559
|
+
presFP: result.presFP,
|
|
1560
|
+
designName,
|
|
1561
|
+
bundleDir: result.dir,
|
|
1562
|
+
bundleEtag: result.etag ?? null,
|
|
1563
|
+
presentation: {
|
|
1564
|
+
title: entry.title ?? null,
|
|
1565
|
+
fileName: entry.fileName ?? null,
|
|
1566
|
+
},
|
|
1567
|
+
paneSuspended,
|
|
1568
|
+
warnings: result.warnings ?? [],
|
|
1569
|
+
nextAction: "Ready.",
|
|
1570
|
+
},
|
|
1571
|
+
null,
|
|
1572
|
+
2,
|
|
1573
|
+
) + "\n",
|
|
1574
|
+
);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// --- Human success: compact multi-line summary on stdout. ---
|
|
1579
|
+
const envTag = entry.env ? ` [${entry.env}]` : "";
|
|
1580
|
+
const presLabel = entry.title || entry.fileName || "(untitled)";
|
|
1581
|
+
const designLabel = designName
|
|
1582
|
+
? `${designName} (${result.presFP})`
|
|
1583
|
+
: result.presFP;
|
|
1584
|
+
process.stdout.write(`Ready — design ${designLabel}\n`);
|
|
1585
|
+
process.stdout.write(` presentation: ${presLabel}${envTag}\n`);
|
|
1586
|
+
process.stdout.write(
|
|
1587
|
+
` bundle: ${result.dir} (etag ${result.etag ?? "?"})\n`,
|
|
1588
|
+
);
|
|
1589
|
+
if (bound) process.stdout.write(` deck ${deckSlug} bound\n`);
|
|
1590
|
+
if (paneSuspended) {
|
|
1591
|
+
process.stdout.write(
|
|
1592
|
+
" Note: pane operations (push/search/read) will wait for a pane click.\n",
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
for (const w of result.warnings ?? []) {
|
|
1596
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
864
1597
|
}
|
|
865
|
-
return "Ready.";
|
|
866
1598
|
}
|
|
867
1599
|
|
|
868
1600
|
async function cmdDoctor(rest) {
|
|
@@ -1228,8 +1960,8 @@ async function cmdPush(rest) {
|
|
|
1228
1960
|
}
|
|
1229
1961
|
if (result.paneSuspended) {
|
|
1230
1962
|
process.stdout.write(
|
|
1231
|
-
`
|
|
1232
|
-
`
|
|
1963
|
+
`The deck rendered, but no PowerPoint window was ready to receive it.\n` +
|
|
1964
|
+
`Open PowerPoint with the slidemind pane (or click the pane once to wake it), ` +
|
|
1233
1965
|
`then re-run: slidemind push ${deck}\n` +
|
|
1234
1966
|
`The rendered deck was saved to ${path.relative(process.cwd(), result.fallbackFile)} in the meantime.\n`,
|
|
1235
1967
|
);
|
|
@@ -1241,7 +1973,22 @@ async function cmdPush(rest) {
|
|
|
1241
1973
|
}
|
|
1242
1974
|
|
|
1243
1975
|
async function cmdNew(rest) {
|
|
1244
|
-
|
|
1976
|
+
let to = null;
|
|
1977
|
+
let noBind = false;
|
|
1978
|
+
const positional = [];
|
|
1979
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1980
|
+
const a = rest[i];
|
|
1981
|
+
if (a === "--to") {
|
|
1982
|
+
to = rest[++i];
|
|
1983
|
+
if (!to) fail("--to expects a presId");
|
|
1984
|
+
} else if (a === "--no-bind") {
|
|
1985
|
+
noBind = true;
|
|
1986
|
+
} else if (a.startsWith("-")) {
|
|
1987
|
+
fail(`unknown flag: ${a}`);
|
|
1988
|
+
} else {
|
|
1989
|
+
positional.push(a);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1245
1992
|
const name = positional[0];
|
|
1246
1993
|
if (!name) fail("usage: slidemind new <name>");
|
|
1247
1994
|
if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
@@ -1264,6 +2011,48 @@ async function cmdNew(rest) {
|
|
|
1264
2011
|
process.stdout.write(
|
|
1265
2012
|
`Next: ask your agent to author "${name}", then \`slidemind build ${name}\`\n`,
|
|
1266
2013
|
);
|
|
2014
|
+
|
|
2015
|
+
// Stateless auto-bind: bind the fresh deck to whatever design is OPEN so the
|
|
2016
|
+
// agent skips a follow-up `download-bundle`. Best-effort — a bind miss (no
|
|
2017
|
+
// host, ambiguous designs, bundle not yet materialized, any network error)
|
|
2018
|
+
// NEVER fails the scaffold; it just drops a soft hint. Fully offline when
|
|
2019
|
+
// unpaired: no network attempted, no hint. `--no-bind` opts out entirely.
|
|
2020
|
+
if (noBind) return;
|
|
2021
|
+
const cfg = await loadBridgeModule("config");
|
|
2022
|
+
if (cfg.readSessions().length === 0) return;
|
|
2023
|
+
let boundMsg = null;
|
|
2024
|
+
try {
|
|
2025
|
+
const client = await import(
|
|
2026
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
2027
|
+
);
|
|
2028
|
+
const bundle = await import(
|
|
2029
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
2030
|
+
);
|
|
2031
|
+
const { presentations } = await client.listOpenPresentationsDetailed();
|
|
2032
|
+
const resolution = client.resolveDesignSource(presentations, {
|
|
2033
|
+
explicitPresId: to,
|
|
2034
|
+
});
|
|
2035
|
+
if (resolution.status === "resolved") {
|
|
2036
|
+
const entry = resolution.entry;
|
|
2037
|
+
const meta = bundle.readMaterializedBundleMeta(entry.brandId);
|
|
2038
|
+
if (meta?.presFP) {
|
|
2039
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
2040
|
+
presFP: meta.presFP,
|
|
2041
|
+
etag: meta.etag ?? null,
|
|
2042
|
+
});
|
|
2043
|
+
const label =
|
|
2044
|
+
readDesignName(bundle.bundleDirForPresFP(meta.presFP)) ?? meta.presFP;
|
|
2045
|
+
boundMsg = `bound to design ${label}`;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
} catch {
|
|
2049
|
+
/* best-effort — fall through to the soft hint */
|
|
2050
|
+
}
|
|
2051
|
+
process.stdout.write(
|
|
2052
|
+
boundMsg
|
|
2053
|
+
? `${boundMsg}\n`
|
|
2054
|
+
: `not bound to a design yet — run \`slidemind ready ${name}\` when a window is open\n`,
|
|
2055
|
+
);
|
|
1267
2056
|
}
|
|
1268
2057
|
|
|
1269
2058
|
function parseTargetFlag(rest) {
|
|
@@ -1384,9 +2173,9 @@ function spawnInherit(file, args) {
|
|
|
1384
2173
|
// ---------- distribution (public npm) ----------
|
|
1385
2174
|
//
|
|
1386
2175
|
// slidemind is published to public npm as `slidemind`. First install is
|
|
1387
|
-
// `npm install -g slidemind` (then `slidemind init` to
|
|
1388
|
-
//
|
|
1389
|
-
//
|
|
2176
|
+
// `npm install -g slidemind` (then `slidemind init` to deploy the skill;
|
|
2177
|
+
// `install` stays as a back-compat alias); `slidemind update` is a thin
|
|
2178
|
+
// wrapper over `npm install -g slidemind@latest`.
|
|
1390
2179
|
|
|
1391
2180
|
// Resolve the `npm` that sits next to the node running this process, so an
|
|
1392
2181
|
// update lands the global into the same prefix slidemind was launched from.
|
|
@@ -1480,6 +2269,9 @@ async function main(rawArgv) {
|
|
|
1480
2269
|
case "build":
|
|
1481
2270
|
await cmdBuild(rest);
|
|
1482
2271
|
return 0;
|
|
2272
|
+
case "plan":
|
|
2273
|
+
await cmdPlan(rest);
|
|
2274
|
+
return 0;
|
|
1483
2275
|
case "review":
|
|
1484
2276
|
await cmdReview(rest);
|
|
1485
2277
|
return 0;
|
|
@@ -1495,15 +2287,24 @@ async function main(rawArgv) {
|
|
|
1495
2287
|
case "ls":
|
|
1496
2288
|
await cmdLs(rest);
|
|
1497
2289
|
return 0;
|
|
2290
|
+
case "view-slide":
|
|
2291
|
+
await cmdViewSlide(rest);
|
|
2292
|
+
return 0;
|
|
1498
2293
|
case "insert-slide":
|
|
1499
2294
|
await cmdInsertSlide(rest);
|
|
1500
2295
|
return 0;
|
|
1501
2296
|
case "add-slide":
|
|
1502
2297
|
await cmdAddSlide(rest);
|
|
1503
2298
|
return 0;
|
|
2299
|
+
case "download-result":
|
|
2300
|
+
await cmdDownloadResult(rest);
|
|
2301
|
+
return 0;
|
|
1504
2302
|
case "download-bundle":
|
|
1505
2303
|
await cmdDownloadBundle(rest);
|
|
1506
2304
|
return 0;
|
|
2305
|
+
case "ready":
|
|
2306
|
+
await cmdReady(rest);
|
|
2307
|
+
return 0;
|
|
1507
2308
|
case "new":
|
|
1508
2309
|
await cmdNew(rest);
|
|
1509
2310
|
return 0;
|
|
@@ -1535,7 +2336,7 @@ async function main(rawArgv) {
|
|
|
1535
2336
|
return 0;
|
|
1536
2337
|
case "init":
|
|
1537
2338
|
case "install":
|
|
1538
|
-
// Set slidemind up in a coding harness (
|
|
2339
|
+
// Set slidemind up in a coding harness (deploy the skill).
|
|
1539
2340
|
// `install` stays as a back-compat alias for the documented `init`.
|
|
1540
2341
|
await cmdInstall(rest);
|
|
1541
2342
|
return 0;
|