slidemind 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +10 -6
- package/README.md +19 -7
- package/bin/slidemind.mjs +819 -86
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +70 -18
- package/skills/search/SKILL.md +25 -6
- package/src/bridge/build-and-push.mjs +11 -0
- package/src/bridge/bundle.mjs +36 -7
- package/src/bridge/capture.mjs +20 -2
- package/src/bridge/client.mjs +513 -7
- package/src/bridge/engine-download.mjs +229 -5
- package/src/bridge/image-dimensions.mjs +181 -0
- package/src/builders/build.mjs +53 -23
- package/src/builders/gate-server.mjs +163 -22
- package/src/builders/plan-markdown-edit.mjs +120 -0
- package/src/builders/plan-overlay.js +1261 -230
- package/src/builders/plan.mjs +146 -25
- package/src/builders/red-thread-backbone.mjs +194 -0
- package/src/builders/resolve-skill.mjs +45 -1
- package/src/builders/review-overlay.js +44 -7
- package/src/builders/review.mjs +76 -11
- package/src/builders/storyboard-markdown.mjs +118 -0
- package/src/builders/storyboard-schema.mjs +180 -0
- package/src/builders/visual-families.mjs +32 -0
- package/src/pptx/app-props.mjs +222 -0
- package/src/pptx/fill/engine.mjs +1273 -0
- package/src/pptx/fill/freeform.mjs +156 -0
- package/src/pptx/fill/parse-slides.mjs +154 -0
- package/src/pptx/fill/xml.mjs +486 -0
- package/src/pptx/hidden-ole.mjs +125 -0
- package/src/pptx/image-extensions.mjs +190 -0
- package/src/pptx/ooxml.mjs +176 -0
- package/src/pptx/read-local.mjs +206 -0
- package/src/pptx/slide-swap.mjs +429 -0
- package/src/pptx/zip.mjs +30 -0
package/bin/slidemind.mjs
CHANGED
|
@@ -60,11 +60,15 @@ function printHelp() {
|
|
|
60
60
|
` as a markdown editor + live preview + overall feedback box,\n` +
|
|
61
61
|
` blocks until the user decides, prints DECISION: approved | comments\n` +
|
|
62
62
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
63
|
+
` slidemind concepts <deck> After plan sign-off, generate one concept mockup PNG per planned\n` +
|
|
64
|
+
` slide into <deck>/concepts/concept-slide-NN.png (requires a host).\n` +
|
|
65
|
+
` Read each as a design target, then rebuild it as slide JSX.\n` +
|
|
66
|
+
` Optional aid — skips cleanly (exit 0) if unavailable.\n` +
|
|
63
67
|
` slidemind build <deck> Build self-contained HTML\n` +
|
|
64
68
|
` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
|
|
65
69
|
` approves or leaves comments (host-free)\n` +
|
|
66
70
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
67
|
-
` slidemind build-pptx <deck> Export native .pptx via the add-in (
|
|
71
|
+
` slidemind build-pptx <deck> Export native .pptx — via the add-in for compose decks (host), LOCALLY for template packs (host-free)\n` +
|
|
68
72
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
69
73
|
` --out <path> output path (default <deck>/<deck>.pptx)\n` +
|
|
70
74
|
` slidemind view-draft <deck> Render the deck's slides to per-slide PNGs (no push, no .pptx)\n` +
|
|
@@ -73,8 +77,10 @@ function printHelp() {
|
|
|
73
77
|
` --to <presId> window to fetch the bundle from (only if not cached)\n` +
|
|
74
78
|
` --out <dir> output dir (default <deck>/.draft-preview/)\n` +
|
|
75
79
|
` --open open each rendered PNG\n` +
|
|
76
|
-
` slidemind search "<query>" Search an engine for images or slides (requires an open host)\n` +
|
|
77
|
-
` --engine <id> engine
|
|
80
|
+
` slidemind search "<query>" Search an engine or scope for images or slides (requires an open host)\n` +
|
|
81
|
+
` --engine <id> engine, or scope:all | scope:sharepoint to search unfiltered\n` +
|
|
82
|
+
` (default: first image engine; scope:all for slide intents)\n` +
|
|
83
|
+
` --result-type <t> slide | image — required for a scope:* target\n` +
|
|
78
84
|
` --intent <i> auto | content | visual (slide engines)\n` +
|
|
79
85
|
` --top <N> max results\n` +
|
|
80
86
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
@@ -84,8 +90,8 @@ function printHelp() {
|
|
|
84
90
|
` --pick <n> 1-based selection number to view\n` +
|
|
85
91
|
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
86
92
|
` --out <dir> output dir (default: a slidemind temp dir)\n` +
|
|
87
|
-
` slidemind ls engines List the live engine catalog for an active pane\n` +
|
|
88
|
-
` --type <t> collection | search\n` +
|
|
93
|
+
` slidemind ls engines List the live engine catalog + search scopes for an active pane\n` +
|
|
94
|
+
` --type <t> collection | search | scope\n` +
|
|
89
95
|
` --subtype <s> image | slide | template | content\n` +
|
|
90
96
|
` --to <presId> target a specific window/backend\n` +
|
|
91
97
|
` --all include every paired backend\n` +
|
|
@@ -95,11 +101,12 @@ function printHelp() {
|
|
|
95
101
|
` --pick <n,..> 1-based selection number(s)\n` +
|
|
96
102
|
` --to <presId> target window (required if >1 open)\n` +
|
|
97
103
|
` --at <N> insert after slide N (0-based); default = append\n` +
|
|
98
|
-
` slidemind add-slide [opts] Fold reused slide
|
|
99
|
-
` --search <id> searchId from 'slidemind search'\n` +
|
|
104
|
+
` slidemind add-slide [opts] Fold reused slide(s) into a deck as native .pptx\n` +
|
|
105
|
+
` --search <id> searchId from 'slidemind search' (requires a host)\n` +
|
|
106
|
+
` --from <file.pptx> fold slide(s) from a local .pptx instead (host-free)\n` +
|
|
100
107
|
` --pick <n,..> 1-based selection number(s)\n` +
|
|
101
108
|
` --deck <deck> deck to add the slides to\n` +
|
|
102
|
-
` --to <presId> target the window the search ran on (required if >1 open)\n` +
|
|
109
|
+
` --to <presId> target the window the search ran on (required if >1 open; ignored with --from)\n` +
|
|
103
110
|
` --at <N> numeric prefix to start at (default: append)\n` +
|
|
104
111
|
` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/ (requires a host)\n` +
|
|
105
112
|
` --search <id> searchId from 'slidemind search'\n` +
|
|
@@ -107,6 +114,12 @@ function printHelp() {
|
|
|
107
114
|
` --deck <deck> deck to write the asset(s) into\n` +
|
|
108
115
|
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
109
116
|
` --icon-color <hex[,hex,..]> bake a colour into SVG result(s) (one for all, or one per --pick)\n` +
|
|
117
|
+
` slidemind imagen "<prompt>" Generate ONE image with the add-in's image model into a deck's assets/ (paired backend, pane-free)\n` +
|
|
118
|
+
` --deck <deck> bound deck to write the image into (required)\n` +
|
|
119
|
+
` --out <name> file name under assets/ (default imagen-N.png)\n` +
|
|
120
|
+
` --size <s> auto|1024x1024|1536x1024|1024x1536 (default auto)\n` +
|
|
121
|
+
` --quality <q> low|medium|high|auto\n` +
|
|
122
|
+
` --brand-style condition on the CD's reference samples\n` +
|
|
110
123
|
` slidemind download-bundle [deck] Materialize the active corporate design's bundle locally\n` +
|
|
111
124
|
` --to <presId> target a specific window\n` +
|
|
112
125
|
` --force re-download even if the cached bundle is current\n` +
|
|
@@ -114,7 +127,8 @@ function printHelp() {
|
|
|
114
127
|
` --all include retained suspended entries\n` +
|
|
115
128
|
` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
|
|
116
129
|
` --json print machine-readable status\n` +
|
|
117
|
-
` slidemind read [presId]
|
|
130
|
+
` slidemind read [presId|file.pptx] Pull the content of an open presentation (requires a host),\n` +
|
|
131
|
+
` or of a local .pptx path (read host-free from disk)\n` +
|
|
118
132
|
` --mode <m> summary | raw (default: raw)\n` +
|
|
119
133
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
120
134
|
` --json print the raw content payload\n` +
|
|
@@ -149,6 +163,15 @@ function fail(msg, code = 1) {
|
|
|
149
163
|
process.exit(code);
|
|
150
164
|
}
|
|
151
165
|
|
|
166
|
+
// The local PowerPoint files `read` and `add-slide --from` accept host-free.
|
|
167
|
+
// All are OPC (zip) OOXML packages with the same ppt/presentation.xml +
|
|
168
|
+
// ppt/slides/ structure — presentation (.pptx/.pptm), template (.potx/.potm),
|
|
169
|
+
// slideshow (.ppsx/.ppsm) — so the src/pptx/ reader/swapper handle them
|
|
170
|
+
// identically. Presentation IDs are opaque and never carry these extensions,
|
|
171
|
+
// so this stays a safe discriminator for "positional is a local file, not a
|
|
172
|
+
// presId".
|
|
173
|
+
const LOCAL_PPTX_RE = /\.(pptx|pptm|potx|potm|ppsx|ppsm)$/i;
|
|
174
|
+
|
|
152
175
|
function resolveDecksDir() {
|
|
153
176
|
if (process.env.SLIDEMIND_DECKS) {
|
|
154
177
|
return path.resolve(process.env.SLIDEMIND_DECKS);
|
|
@@ -168,12 +191,25 @@ function resolveDeckArg(arg) {
|
|
|
168
191
|
return path.join(decksDir, arg);
|
|
169
192
|
}
|
|
170
193
|
|
|
171
|
-
// Best-effort human name of a materialized design
|
|
172
|
-
// the
|
|
173
|
-
//
|
|
174
|
-
//
|
|
194
|
+
// Best-effort human name of a materialized design. Prefers the live `name`
|
|
195
|
+
// the wire carries (persisted into bundle.json by materializeBundle) — the
|
|
196
|
+
// owner-set brand name, which survives renames. Falls back to the first `# `
|
|
197
|
+
// heading in design.md (frozen at authoring time, so it may still show the
|
|
198
|
+
// original auto codename), with the " — design rules" suffix stripped.
|
|
199
|
+
// Returns null on any failure so callers fall back to the raw presFP.
|
|
200
|
+
// Shared by `ready` and `new`'s auto-bind.
|
|
175
201
|
function readDesignName(bundleDir) {
|
|
176
202
|
if (!bundleDir) return null;
|
|
203
|
+
try {
|
|
204
|
+
const meta = JSON.parse(
|
|
205
|
+
fs.readFileSync(path.join(bundleDir, "bundle.json"), "utf8"),
|
|
206
|
+
);
|
|
207
|
+
if (typeof meta.name === "string" && meta.name.trim()) {
|
|
208
|
+
return meta.name.trim();
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
/* no meta or unreadable — fall through to the design.md heading */
|
|
212
|
+
}
|
|
177
213
|
try {
|
|
178
214
|
const md = fs.readFileSync(path.join(bundleDir, "design.md"), "utf8");
|
|
179
215
|
for (const raw of md.split("\n")) {
|
|
@@ -278,6 +314,103 @@ function renderProgressPrinter() {
|
|
|
278
314
|
};
|
|
279
315
|
}
|
|
280
316
|
|
|
317
|
+
// Generate one regular image via the paired add-in's image model and write it
|
|
318
|
+
// into the deck's assets/. Pane-free: routes by the deck's bound design
|
|
319
|
+
// (.bundle-ref.json), so it works with PowerPoint backgrounded or closed.
|
|
320
|
+
async function cmdImagen(rest) {
|
|
321
|
+
let prompt = null;
|
|
322
|
+
let deck = null;
|
|
323
|
+
let outName = null;
|
|
324
|
+
let size = null;
|
|
325
|
+
let quality = null;
|
|
326
|
+
let brandStyle = false;
|
|
327
|
+
for (let i = 0; i < rest.length; i++) {
|
|
328
|
+
const a = rest[i];
|
|
329
|
+
if (a === "--deck") {
|
|
330
|
+
deck = rest[++i];
|
|
331
|
+
if (!deck) fail("--deck expects a deck");
|
|
332
|
+
} else if (a === "--out" || a === "-o") {
|
|
333
|
+
outName = rest[++i];
|
|
334
|
+
if (!outName) fail("--out expects a file name");
|
|
335
|
+
} else if (a === "--size") {
|
|
336
|
+
size = rest[++i];
|
|
337
|
+
if (!["auto", "1024x1024", "1536x1024", "1024x1536"].includes(size ?? "")) {
|
|
338
|
+
fail("--size expects auto|1024x1024|1536x1024|1024x1536");
|
|
339
|
+
}
|
|
340
|
+
} else if (a === "--quality") {
|
|
341
|
+
quality = rest[++i];
|
|
342
|
+
if (!["low", "medium", "high", "auto"].includes(quality ?? "")) {
|
|
343
|
+
fail("--quality expects low|medium|high|auto");
|
|
344
|
+
}
|
|
345
|
+
} else if (a === "--brand-style") {
|
|
346
|
+
brandStyle = true;
|
|
347
|
+
} else if (a.startsWith("-")) {
|
|
348
|
+
fail(`unknown flag: ${a}`);
|
|
349
|
+
} else if (prompt === null) {
|
|
350
|
+
prompt = a;
|
|
351
|
+
} else {
|
|
352
|
+
fail(`unexpected argument: ${a} (quote the prompt as one argument)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!prompt || !prompt.trim() || !deck) {
|
|
356
|
+
fail(
|
|
357
|
+
'usage: slidemind imagen "<prompt>" --deck <deck> [--out <name.png>] ' +
|
|
358
|
+
"[--size auto|1024x1024|1536x1024|1024x1536] [--quality low|medium|high|auto] [--brand-style]",
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const deckDir = resolveDeckArg(deck);
|
|
362
|
+
const bundleMod = await import(
|
|
363
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
364
|
+
);
|
|
365
|
+
const ref = bundleMod.readDeckBundleRef(deckDir);
|
|
366
|
+
if (!ref?.presFP) {
|
|
367
|
+
fail(
|
|
368
|
+
`deck ${path.basename(deckDir)} is not bound to a design yet — run ` +
|
|
369
|
+
`'slidemind ready ${deck}' first (imagen routes by the deck's bound design).`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const client = await import(
|
|
373
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
374
|
+
);
|
|
375
|
+
process.stdout.write(" generating image (this can take a minute)…\n");
|
|
376
|
+
let result;
|
|
377
|
+
try {
|
|
378
|
+
result = await client.generateImageForBrand(ref.presFP, {
|
|
379
|
+
prompt,
|
|
380
|
+
size,
|
|
381
|
+
quality,
|
|
382
|
+
brandStyle,
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (err?.code === "imagen_route_unavailable") {
|
|
386
|
+
fail(
|
|
387
|
+
`${err.message}\n The paired add-in does not offer image generation yet — ` +
|
|
388
|
+
`it ships with the next add-in update; until then add pictures to assets/ by hand ` +
|
|
389
|
+
`or via 'slidemind download-result'.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
const m = /^data:image\/(png|jpe?g);base64,(.+)$/.exec(result?.image ?? "");
|
|
395
|
+
if (!m) fail("backend returned no image data");
|
|
396
|
+
const ext = m[1] === "png" ? "png" : "jpeg";
|
|
397
|
+
const assetsDir = path.join(deckDir, "assets");
|
|
398
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
399
|
+
let file = outName;
|
|
400
|
+
if (!file) {
|
|
401
|
+
let n = 1;
|
|
402
|
+
while (fs.existsSync(path.join(assetsDir, `imagen-${n}.${ext}`))) n++;
|
|
403
|
+
file = `imagen-${n}.${ext}`;
|
|
404
|
+
}
|
|
405
|
+
const outPath = path.join(assetsDir, file);
|
|
406
|
+
fs.writeFileSync(outPath, Buffer.from(m[2], "base64"));
|
|
407
|
+
const styled = result.styleReferences > 0 ? ` (styled on ${result.styleReferences} brand sample(s))` : "";
|
|
408
|
+
process.stdout.write(
|
|
409
|
+
`Wrote ${path.relative(process.cwd(), outPath)}${styled}\n` +
|
|
410
|
+
` Reference it from slide props as "assets/${file}" (e.g. summaryImage, illustration).\n`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
281
414
|
async function cmdBuildPptx(rest) {
|
|
282
415
|
const { deck, passthrough } = splitDeckArg(rest, "build-pptx");
|
|
283
416
|
let to = null;
|
|
@@ -300,6 +433,57 @@ async function cmdBuildPptx(rest) {
|
|
|
300
433
|
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
301
434
|
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
302
435
|
}
|
|
436
|
+
|
|
437
|
+
// Template packs (mode: fill-template) build LOCALLY: the deck's slides are
|
|
438
|
+
// declarative fills over a template .pptx that ships in the pack, so no
|
|
439
|
+
// add-in render is involved and no PowerPoint host is needed. Compose decks
|
|
440
|
+
// take the bridge path below UNTOUCHED — the mode probe never throws, so a
|
|
441
|
+
// compose deck with an unmaterialized bundle still self-heals in the render.
|
|
442
|
+
const resolveMod = await import(
|
|
443
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "resolve-skill.mjs")).href
|
|
444
|
+
);
|
|
445
|
+
if (resolveMod.resolveDeckMode(deckDir).mode === "fill-template") {
|
|
446
|
+
const skill = resolveMod.resolveSkillForDeck(deckDir);
|
|
447
|
+
if (to) {
|
|
448
|
+
fail(
|
|
449
|
+
"--to targets a PowerPoint window, but fill-template decks build " +
|
|
450
|
+
"locally from the pack's template — drop --to (use 'slidemind push' " +
|
|
451
|
+
"to get slides into a window).",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
// In-deck freeform slides (create-slides JSX next to the master-backed
|
|
455
|
+
// ones) render to sibling NN.pptx artifacts first — the only step that
|
|
456
|
+
// talks to the add-in; unchanged freeform slides keep the build offline.
|
|
457
|
+
const freeform = await import(
|
|
458
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "freeform.mjs")).href
|
|
459
|
+
);
|
|
460
|
+
const ff = await freeform.ensureFreeformRenders({
|
|
461
|
+
deckDir,
|
|
462
|
+
locale: skill.locale ?? "en",
|
|
463
|
+
srcDir: SRC_DIR,
|
|
464
|
+
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
465
|
+
});
|
|
466
|
+
for (const w of ff.warnings) process.stderr.write(` warning: ${w}\n`);
|
|
467
|
+
|
|
468
|
+
const engine = await import(
|
|
469
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "engine.mjs")).href
|
|
470
|
+
);
|
|
471
|
+
const result = await engine.buildFillTemplateDeck({
|
|
472
|
+
deckDir,
|
|
473
|
+
skill,
|
|
474
|
+
outPath,
|
|
475
|
+
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
476
|
+
});
|
|
477
|
+
process.stdout.write(
|
|
478
|
+
`Wrote ${result.outPath} (${result.slideCount} slides) from template ` +
|
|
479
|
+
`'${skill.templateKey}' — host-free\n`,
|
|
480
|
+
);
|
|
481
|
+
for (const w of result.warnings ?? []) {
|
|
482
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
303
487
|
// The .pptx is rendered server-side by the add-in (one exporter, one truth).
|
|
304
488
|
// A bound deck renders pane-free over the brand routes — no PowerPoint
|
|
305
489
|
// window needed; unbound decks (or --to) resolve through the pane registry.
|
|
@@ -405,10 +589,96 @@ async function cmdViewDraft(rest) {
|
|
|
405
589
|
}
|
|
406
590
|
}
|
|
407
591
|
|
|
592
|
+
// Turn the add-in's deliberately terse search failures into something actionable.
|
|
593
|
+
// It withholds detail on purpose: the raw message used to carry the built OData
|
|
594
|
+
// filter and physical index names, and "Engine not found" doubles as a permission
|
|
595
|
+
// denial so engine existence stays undisclosed. So the CLI can't diagnose — it can
|
|
596
|
+
// only name the next move.
|
|
597
|
+
function searchFailureHint(error, engineId) {
|
|
598
|
+
const raw = error ?? "unknown error";
|
|
599
|
+
if (/engine not found/i.test(raw)) {
|
|
600
|
+
return (
|
|
601
|
+
`${raw} — '${engineId}' is unknown or not visible to you. ` +
|
|
602
|
+
`Re-run 'slidemind ls engines' (ids can be retired) and pick from the live list, ` +
|
|
603
|
+
`or search 'scope:all' to skip engines entirely.`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (/^search failed$/i.test(raw)) {
|
|
607
|
+
return (
|
|
608
|
+
`${raw} — the add-in withholds the reason (it can leak index internals); ` +
|
|
609
|
+
`the detail is in the add-in's logs. Retry once, then try 'scope:all'.`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return raw;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Zero results is now a routine, ambiguous outcome: the add-in answers "success,
|
|
616
|
+
// nothing" for a corpus that isn't connected, one that is still embedding, and a
|
|
617
|
+
// folder filter that matches nothing — all indistinguishable on the wire. Say that
|
|
618
|
+
// instead of letting an empty list read as "this content does not exist".
|
|
619
|
+
function writeEmptySearchDiagnostic(engineId, bundleMod) {
|
|
620
|
+
const isScope = bundleMod.isSearchScopeId(engineId);
|
|
621
|
+
const lines = [
|
|
622
|
+
` No results — this does NOT mean the content doesn't exist. Any of these looks identical here:`,
|
|
623
|
+
` · nothing is connected yet, so there is no corpus to search`,
|
|
624
|
+
` · the corpus is still indexing (content appears progressively)`,
|
|
625
|
+
];
|
|
626
|
+
if (!isScope) {
|
|
627
|
+
lines.push(
|
|
628
|
+
` · this engine is a saved FOLDER FILTER and the content sits outside it`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
lines.push(` Next:`);
|
|
632
|
+
if (!isScope) {
|
|
633
|
+
lines.push(
|
|
634
|
+
` · widen to the unfiltered corpus: slidemind search "<query>" --engine ${bundleMod.SCOPE_ALL} --result-type slide`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
lines.push(
|
|
638
|
+
` · re-word the query (content search matches phrasing, so a synonym can decide a hit)`,
|
|
639
|
+
` · slidemind ls engines — see every engine and scope you can reach`,
|
|
640
|
+
);
|
|
641
|
+
if (isScope) {
|
|
642
|
+
lines.push(
|
|
643
|
+
` · if the unfiltered corpus is also empty, report it as "not found" and name what you searched — don't author a replacement as if reuse had been tried`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Write the search's numbered collage to a file and tell the agent to look at it.
|
|
650
|
+
//
|
|
651
|
+
// The add-in composes ONE bounded 800x600 image showing every candidate with its
|
|
652
|
+
// selection number burned in — the same picture the in-app agent sees. That is the
|
|
653
|
+
// intended way to choose a visual result: one look at one file, rather than N
|
|
654
|
+
// per-result previews (which is also why the relay ack carries no per-result bytes
|
|
655
|
+
// — a corpus image is multi-megabyte and N of them can't cross the relay).
|
|
656
|
+
//
|
|
657
|
+
// Best-effort by design: a missing/…unparseable grid must never fail a search that
|
|
658
|
+
// otherwise returned usable results. Content-intent searches have no grid at all —
|
|
659
|
+
// their signal is the printed text.
|
|
660
|
+
function writeSearchGrid(gridDataUri, searchId, resolveExt) {
|
|
661
|
+
if (typeof gridDataUri !== "string" || !gridDataUri.startsWith("data:")) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
const m = /^data:([^;]+);base64,(.+)$/.exec(gridDataUri);
|
|
666
|
+
if (!m) return null;
|
|
667
|
+
const bytes = Buffer.from(m[2], "base64");
|
|
668
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-grid-"));
|
|
669
|
+
const target = path.join(dir, `grid-${searchId}.${resolveExt(m[1], bytes)}`);
|
|
670
|
+
fs.writeFileSync(target, bytes);
|
|
671
|
+
return target;
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
408
677
|
async function cmdSearch(rest) {
|
|
409
678
|
let asJson = false;
|
|
410
679
|
let to = null;
|
|
411
680
|
let engineId = null;
|
|
681
|
+
let resultType = null;
|
|
412
682
|
let intent = null;
|
|
413
683
|
let top = null;
|
|
414
684
|
const queryParts = [];
|
|
@@ -422,6 +692,12 @@ async function cmdSearch(rest) {
|
|
|
422
692
|
} else if (a === "--engine" || a === "-e" || a === "--engineId") {
|
|
423
693
|
engineId = rest[++i];
|
|
424
694
|
if (!engineId) fail("--engine expects an engineId");
|
|
695
|
+
} else if (a === "--result-type" || a === "--resultType") {
|
|
696
|
+
resultType = rest[++i];
|
|
697
|
+
if (!resultType) fail("--result-type expects slide|image");
|
|
698
|
+
if (!["slide", "image"].includes(resultType)) {
|
|
699
|
+
fail(`--result-type expects slide|image, got '${resultType}'`);
|
|
700
|
+
}
|
|
425
701
|
} else if (a === "--intent") {
|
|
426
702
|
intent = rest[++i];
|
|
427
703
|
if (!intent) fail("--intent expects auto|content|visual");
|
|
@@ -444,7 +720,7 @@ async function cmdSearch(rest) {
|
|
|
444
720
|
const query = queryParts.join(" ").trim();
|
|
445
721
|
if (!query) {
|
|
446
722
|
fail(
|
|
447
|
-
`usage: slidemind search "<query>" [--engine <id>] [--intent auto|content|visual] [--top <N>] [--to <presId>] [--json]`,
|
|
723
|
+
`usage: slidemind search "<query>" [--engine <id|scope:all|scope:sharepoint>] [--result-type slide|image] [--intent auto|content|visual] [--top <N>] [--to <presId>] [--json]`,
|
|
448
724
|
);
|
|
449
725
|
}
|
|
450
726
|
|
|
@@ -456,6 +732,9 @@ async function cmdSearch(rest) {
|
|
|
456
732
|
const bundle = await import(
|
|
457
733
|
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
458
734
|
);
|
|
735
|
+
const { resolveAssetExt } = await import(
|
|
736
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
737
|
+
);
|
|
459
738
|
const registry = await client.requirePaneHost();
|
|
460
739
|
// The engine is per-user, not window-bound — so search just attaches to any
|
|
461
740
|
// connected window (preferring a live one). No "which window?" prompt; pass
|
|
@@ -465,33 +744,64 @@ async function cmdSearch(rest) {
|
|
|
465
744
|
const presId = entry.presId;
|
|
466
745
|
const backendURL = entry.backendURL ?? null;
|
|
467
746
|
|
|
468
|
-
//
|
|
469
|
-
// only applies to image/visual intent. content/visual on a slide engine is
|
|
470
|
-
// the caller's choice via --engine.
|
|
747
|
+
// An explicit content/visual intent means "search slides", not imagery.
|
|
471
748
|
const slideIntent = intent === "content" || intent === "visual";
|
|
472
749
|
// Thread the engine subtype on the wire whenever the CLI can determine it:
|
|
473
750
|
// - the pickImageEngine default branch is, by construction, an image engine;
|
|
474
|
-
// - an explicit slide intent (--intent content|visual) targets
|
|
475
|
-
// engine.
|
|
751
|
+
// - an explicit slide intent (--intent content|visual) targets slides.
|
|
476
752
|
// This is belt-and-suspenders for an add-in regression where a missing
|
|
477
753
|
// subtype no longer defaulted to "image"; when the subtype is unknown we omit
|
|
478
754
|
// it and let the add-in resolve it from the snapshot.
|
|
479
755
|
let subtype = slideIntent ? "slide" : null;
|
|
480
756
|
if (!engineId) {
|
|
481
757
|
if (slideIntent) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
758
|
+
// Slide search with no engine named: search the corpus UNFILTERED rather
|
|
759
|
+
// than refusing. An engine is a saved folder filter over the add-in's
|
|
760
|
+
// shared corpus, so a workspace can hold a full corpus and zero slide
|
|
761
|
+
// engines — demanding --engine here dead-ended exactly that case.
|
|
762
|
+
engineId = bundle.SCOPE_ALL;
|
|
763
|
+
resultType = resultType ?? "slide";
|
|
764
|
+
} else {
|
|
765
|
+
// Imagery: resolve a real image engine (a scope is not a brand image
|
|
766
|
+
// source). Ask the LIVE catalog first — the same one the add-in's own
|
|
767
|
+
// design agent reads — and fall back to the materialized bundle only when
|
|
768
|
+
// the host can't answer. Bundle-first would be stale: engine ids can be
|
|
769
|
+
// retired server-side, and a dead id surfaces as "Engine not found".
|
|
770
|
+
const live = await client
|
|
771
|
+
.listEngines({ subtype: "image", backendURL })
|
|
772
|
+
.catch(() => ({ engines: [] }));
|
|
773
|
+
let pick = bundle.pickImageEngine(live.engines);
|
|
774
|
+
if (!pick) {
|
|
775
|
+
// Best-effort: a bundle that can't be materialized (e.g. an incomplete
|
|
776
|
+
// one) must not replace the actionable "wire an image engine" message
|
|
777
|
+
// below with a bundle error — the live catalog is the real answer here.
|
|
778
|
+
const mat = await bundle
|
|
779
|
+
.materializeBundle(presId, { backendURL })
|
|
780
|
+
.catch(() => null);
|
|
781
|
+
if (mat) pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
|
|
782
|
+
}
|
|
783
|
+
if (!pick) {
|
|
784
|
+
fail(
|
|
785
|
+
"This corporate design has no image engines to search. Run 'slidemind ls engines' to see what exists, or ask the add-in owner to wire an image/icon engine.",
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
engineId = pick.id;
|
|
789
|
+
subtype = "image";
|
|
492
790
|
}
|
|
493
|
-
|
|
494
|
-
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// A scope has no subtype of its own, so the index must be named explicitly —
|
|
794
|
+
// the add-in rejects a scope search without it. Default to slides: reusing an
|
|
795
|
+
// existing slide is what an unfiltered corpus search is overwhelmingly for.
|
|
796
|
+
if (bundle.isSearchScopeId(engineId)) {
|
|
797
|
+
resultType = resultType ?? "slide";
|
|
798
|
+
subtype = resultType;
|
|
799
|
+
} else if (resultType) {
|
|
800
|
+
// Harmless but meaningless on an engine, whose own subtype is authoritative.
|
|
801
|
+
process.stderr.write(
|
|
802
|
+
`note: --result-type is ignored for engine '${engineId}' (it applies to scope:* targets); the engine's own type decides.\n`,
|
|
803
|
+
);
|
|
804
|
+
resultType = null;
|
|
495
805
|
}
|
|
496
806
|
|
|
497
807
|
// Window-agnostic: tries the picked pane, then fails over to any other live
|
|
@@ -503,6 +813,7 @@ async function cmdSearch(rest) {
|
|
|
503
813
|
engineId,
|
|
504
814
|
query,
|
|
505
815
|
subtype,
|
|
816
|
+
resultType,
|
|
506
817
|
intent: intent ?? null,
|
|
507
818
|
top: top ?? null,
|
|
508
819
|
});
|
|
@@ -513,7 +824,7 @@ async function cmdSearch(rest) {
|
|
|
513
824
|
return;
|
|
514
825
|
}
|
|
515
826
|
if (!res?.ok) {
|
|
516
|
-
process.stderr.write(`search failed: ${res?.error
|
|
827
|
+
process.stderr.write(`search failed: ${searchFailureHint(res?.error, engineId)}\n`);
|
|
517
828
|
return;
|
|
518
829
|
}
|
|
519
830
|
// Prefer the new `results` array (image + slide engines); fall back to the
|
|
@@ -522,11 +833,18 @@ async function cmdSearch(rest) {
|
|
|
522
833
|
? res.results
|
|
523
834
|
: (res.thumbnails ?? []).map((t, i) => ({ selection: i + 1, title: t.title }));
|
|
524
835
|
const count = res.resultCount ?? results.length;
|
|
836
|
+
const targetLabel = bundle.isSearchScopeId(engineId)
|
|
837
|
+
? `scope ${engineId}`
|
|
838
|
+
: `engine ${engineId}`;
|
|
525
839
|
process.stdout.write(
|
|
526
|
-
`searchId ${res.searchId} — ${count} result(s) from
|
|
840
|
+
`searchId ${res.searchId} — ${count} result(s) from ${targetLabel}` +
|
|
527
841
|
(res.intent ? ` (${res.intent})` : "") +
|
|
528
842
|
`\n`,
|
|
529
843
|
);
|
|
844
|
+
if (count === 0) {
|
|
845
|
+
writeEmptySearchDiagnostic(engineId, bundle);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
530
848
|
const isContent = res.intent === "content";
|
|
531
849
|
results.forEach((r, i) => {
|
|
532
850
|
const sel = r.selection ?? i + 1;
|
|
@@ -540,10 +858,27 @@ async function cmdSearch(rest) {
|
|
|
540
858
|
}
|
|
541
859
|
}
|
|
542
860
|
} else {
|
|
543
|
-
// Visual / image results: the numbered list.
|
|
544
|
-
|
|
861
|
+
// Visual / image results: the numbered list. An image result carries its
|
|
862
|
+
// intrinsic size from the engine index — show it, so the box can be sized
|
|
863
|
+
// to the real aspect ratio at authoring time rather than after the fetch.
|
|
864
|
+
const dims =
|
|
865
|
+
r.kind === "image" && Number.isFinite(r.width) && Number.isFinite(r.height)
|
|
866
|
+
? ` ${r.width}×${r.height}`
|
|
867
|
+
: "";
|
|
868
|
+
process.stdout.write(` ${sel}. ${r.title ?? "(untitled)"}${dims}\n`);
|
|
545
869
|
}
|
|
546
870
|
});
|
|
871
|
+
// Visual results: point at the ONE collage rather than making the caller pull
|
|
872
|
+
// each preview separately. The numbers burned into it match the list above.
|
|
873
|
+
if (!isContent) {
|
|
874
|
+
const gridPath = writeSearchGrid(res.gridDataUri, res.searchId, resolveAssetExt);
|
|
875
|
+
if (gridPath) {
|
|
876
|
+
process.stdout.write(
|
|
877
|
+
`Contact sheet of all ${count} result(s) — LOOK AT THIS FILE to choose, ` +
|
|
878
|
+
`don't fetch previews one by one: ${gridPath}\n`,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
547
882
|
// Tailor the next-step hint to the result kind: slide verbs (insert-slide /
|
|
548
883
|
// add-slide) for slide results, the image-download hint otherwise. The result
|
|
549
884
|
// subtype is authoritative; fall back to a per-result `kind` for older
|
|
@@ -572,7 +907,7 @@ async function cmdSearch(rest) {
|
|
|
572
907
|
async function cmdLs(rest) {
|
|
573
908
|
const [sub, ...subRest] = rest;
|
|
574
909
|
if (sub !== "engines") {
|
|
575
|
-
fail("usage: slidemind ls engines [--type collection|search] [--subtype image|slide|template|content] [--json]");
|
|
910
|
+
fail("usage: slidemind ls engines [--type collection|search|scope] [--subtype image|slide|template|content] [--json]");
|
|
576
911
|
}
|
|
577
912
|
let type = null;
|
|
578
913
|
let subtype = null;
|
|
@@ -583,9 +918,10 @@ async function cmdLs(rest) {
|
|
|
583
918
|
const a = subRest[i];
|
|
584
919
|
if (a === "--type") {
|
|
585
920
|
type = subRest[++i];
|
|
586
|
-
if (!type) fail("--type expects collection|search");
|
|
587
|
-
|
|
588
|
-
|
|
921
|
+
if (!type) fail("--type expects collection|search|scope");
|
|
922
|
+
// "scope" narrows to the non-engine targets (scope:all / scope:sharepoint).
|
|
923
|
+
if (!["collection", "search", "scope"].includes(type)) {
|
|
924
|
+
fail(`--type expects collection|search|scope, got '${type}'`);
|
|
589
925
|
}
|
|
590
926
|
} else if (a === "--subtype") {
|
|
591
927
|
subtype = subRest[++i];
|
|
@@ -622,7 +958,18 @@ async function cmdLs(rest) {
|
|
|
622
958
|
return;
|
|
623
959
|
}
|
|
624
960
|
if (engines.length === 0) {
|
|
625
|
-
|
|
961
|
+
// Deliberately not "nothing to search": an engine is a saved folder filter
|
|
962
|
+
// over the add-in's shared corpus, so an empty catalog says only that nobody
|
|
963
|
+
// saved a filter. An older add-in that predates scopes lands here too.
|
|
964
|
+
const { SCOPE_ALL } = await import(
|
|
965
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
966
|
+
);
|
|
967
|
+
process.stderr.write(
|
|
968
|
+
"No engines or scopes matched. An engine is a saved folder filter over the corpus, " +
|
|
969
|
+
"so an empty list does NOT mean there is nothing to search — try " +
|
|
970
|
+
`'slidemind search "<query>" --engine ${SCOPE_ALL} --result-type slide' ` +
|
|
971
|
+
"to search it unfiltered.\n",
|
|
972
|
+
);
|
|
626
973
|
return;
|
|
627
974
|
}
|
|
628
975
|
// Show the env tag only when more than one environment contributed engines.
|
|
@@ -631,7 +978,15 @@ async function cmdLs(rest) {
|
|
|
631
978
|
const kind = [e.type, e.subtype].filter(Boolean).join("/");
|
|
632
979
|
const env = multiEnv && e.env ? `[${e.env}] ` : "";
|
|
633
980
|
const desc = e.description ? ` — ${e.description}` : "";
|
|
634
|
-
|
|
981
|
+
// A scope takes --result-type instead of having a subtype of its own; spell
|
|
982
|
+
// that out so the agent doesn't have to infer it from the missing subtype.
|
|
983
|
+
const hint =
|
|
984
|
+
e.type === "scope"
|
|
985
|
+
? ` (needs --result-type ${(e.capabilities?.resultTypes ?? ["slide", "image"]).join("|")})`
|
|
986
|
+
: "";
|
|
987
|
+
process.stdout.write(
|
|
988
|
+
`${env}${e.id} · ${e.label ?? e.id} · ${kind}${hint}${desc}\n`,
|
|
989
|
+
);
|
|
635
990
|
}
|
|
636
991
|
}
|
|
637
992
|
|
|
@@ -713,6 +1068,7 @@ async function cmdAddSlide(rest) {
|
|
|
713
1068
|
let deck = null;
|
|
714
1069
|
let at = null;
|
|
715
1070
|
let to = null;
|
|
1071
|
+
let from = null;
|
|
716
1072
|
for (let i = 0; i < rest.length; i++) {
|
|
717
1073
|
const a = rest[i];
|
|
718
1074
|
if (a === "--search") {
|
|
@@ -720,6 +1076,9 @@ async function cmdAddSlide(rest) {
|
|
|
720
1076
|
const n = parseInt(next, 10);
|
|
721
1077
|
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
722
1078
|
searchId = n;
|
|
1079
|
+
} else if (a === "--from") {
|
|
1080
|
+
from = rest[++i];
|
|
1081
|
+
if (!from) fail("--from expects a path to a .pptx file");
|
|
723
1082
|
} else if (a === "--pick") {
|
|
724
1083
|
picks = parsePicks(rest[++i]);
|
|
725
1084
|
} else if (a === "--deck") {
|
|
@@ -741,8 +1100,69 @@ async function cmdAddSlide(rest) {
|
|
|
741
1100
|
fail(`unexpected argument: ${a}`);
|
|
742
1101
|
}
|
|
743
1102
|
}
|
|
1103
|
+
|
|
1104
|
+
// Host-free local source: fold picked slides from a .pptx on disk (no bridge
|
|
1105
|
+
// client, no requirePaneHost). Mutually exclusive with --search; --to is
|
|
1106
|
+
// accepted-but-ignored here (symmetric with the other reuse commands — there's
|
|
1107
|
+
// no window to target when the source is a local file).
|
|
1108
|
+
if (from !== null) {
|
|
1109
|
+
if (searchId !== null) fail("--from and --search are mutually exclusive");
|
|
1110
|
+
if (!picks || !deck) {
|
|
1111
|
+
fail("usage: slidemind add-slide --from <file.pptx> --pick <n[,n,...]> --deck <deck> [--at <N>]");
|
|
1112
|
+
}
|
|
1113
|
+
const fromPath = path.isAbsolute(from)
|
|
1114
|
+
? from
|
|
1115
|
+
: path.resolve(process.cwd(), from);
|
|
1116
|
+
let st = null;
|
|
1117
|
+
try {
|
|
1118
|
+
st = fs.statSync(fromPath);
|
|
1119
|
+
} catch {
|
|
1120
|
+
st = null;
|
|
1121
|
+
}
|
|
1122
|
+
if (!st || !st.isFile() || !LOCAL_PPTX_RE.test(fromPath)) {
|
|
1123
|
+
fail(`--from: '${from}' is not an existing PowerPoint file (.pptx/.pptm/.potx/.potm/.ppsx/.ppsm)`);
|
|
1124
|
+
}
|
|
1125
|
+
const deckDir = resolveDeckArg(deck);
|
|
1126
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
1127
|
+
fs.mkdirSync(slidesDir, { recursive: true });
|
|
1128
|
+
|
|
1129
|
+
const { foldPickedSlides } = await import(
|
|
1130
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "slide-swap.mjs")).href
|
|
1131
|
+
);
|
|
1132
|
+
let results;
|
|
1133
|
+
try {
|
|
1134
|
+
results = foldPickedSlides(fs.readFileSync(fromPath), picks, {
|
|
1135
|
+
sourceName: path.basename(fromPath),
|
|
1136
|
+
});
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
fail(`add-slide failed: ${err.message}`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Same filename math + write loop as the --search branch; results already
|
|
1142
|
+
// carry a `.slideId` and `.bytes` (a Uint8Array of a one-slide package).
|
|
1143
|
+
const reused = await import(
|
|
1144
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
|
|
1145
|
+
);
|
|
1146
|
+
const existing = fs
|
|
1147
|
+
.readdirSync(slidesDir)
|
|
1148
|
+
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"));
|
|
1149
|
+
const targets = reused.reusedSlideTargets(existing, results, at);
|
|
1150
|
+
const written = [];
|
|
1151
|
+
results.forEach((r, i) => {
|
|
1152
|
+
const target = path.join(slidesDir, targets[i].filename);
|
|
1153
|
+
fs.writeFileSync(target, Buffer.from(r.bytes));
|
|
1154
|
+
written.push(path.relative(process.cwd(), target));
|
|
1155
|
+
});
|
|
1156
|
+
process.stdout.write(
|
|
1157
|
+
`Added ${written.length} reused slide(s) to ${path.relative(process.cwd(), deckDir)}:\n` +
|
|
1158
|
+
written.map((w) => ` ${w}`).join("\n") +
|
|
1159
|
+
`\nThey are native .pptx in slides/ — re-run 'slidemind build-pptx ${deck}' or 'slidemind push ${deck}' to render the deck with them spliced in.\n`,
|
|
1160
|
+
);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
744
1164
|
if (searchId === null || !picks || !deck) {
|
|
745
|
-
fail("usage: slidemind add-slide --search <id> --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]");
|
|
1165
|
+
fail("usage: slidemind add-slide (--search <id> | --from <file.pptx>) --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]");
|
|
746
1166
|
}
|
|
747
1167
|
const deckDir = resolveDeckArg(deck);
|
|
748
1168
|
const slidesDir = path.join(deckDir, "slides");
|
|
@@ -765,18 +1185,61 @@ async function cmdAddSlide(rest) {
|
|
|
765
1185
|
const reused = await import(
|
|
766
1186
|
pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
|
|
767
1187
|
);
|
|
1188
|
+
const { createTransferProgress } = await import(
|
|
1189
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
1190
|
+
);
|
|
1191
|
+
// Stream each pick to a provisional file: the play-order filename can only be
|
|
1192
|
+
// computed once every pick is in hand, and buffering a multi-megabyte slide in
|
|
1193
|
+
// memory just to name it would defeat the streaming path.
|
|
768
1194
|
const extracted = [];
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
searchId,
|
|
773
|
-
selection: pick,
|
|
774
|
-
backendURL,
|
|
775
|
-
});
|
|
776
|
-
if (!res?.ok || !res.base64) {
|
|
777
|
-
fail(`add-slide failed for selection ${pick}: ${res?.error ?? "no slide bytes returned"}`);
|
|
1195
|
+
const cleanupTemps = () => {
|
|
1196
|
+
for (const s of extracted) {
|
|
1197
|
+
if (s.tempPath) fs.rmSync(s.tempPath, { force: true });
|
|
778
1198
|
}
|
|
779
|
-
|
|
1199
|
+
};
|
|
1200
|
+
try {
|
|
1201
|
+
for (const pick of picks) {
|
|
1202
|
+
const tempPath = path.join(slidesDir, `.reused-${searchId}-${pick}.part`);
|
|
1203
|
+
if (typeof client.extractSlideToFile === "function") {
|
|
1204
|
+
try {
|
|
1205
|
+
const res = await client.extractSlideToFile({
|
|
1206
|
+
presId,
|
|
1207
|
+
searchId,
|
|
1208
|
+
selection: pick,
|
|
1209
|
+
destPath: tempPath,
|
|
1210
|
+
backendURL,
|
|
1211
|
+
onProgress: createTransferProgress(`extracting slide ${pick}`),
|
|
1212
|
+
});
|
|
1213
|
+
if (!res?.bytesWritten) throw new Error("stream produced no bytes");
|
|
1214
|
+
extracted.push({ selection: pick, slideId: res.slideId ?? null, tempPath });
|
|
1215
|
+
continue;
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
fs.rmSync(tempPath, { force: true });
|
|
1218
|
+
// Route absent (bodyless 404 / 501) → this add-in predates the streaming
|
|
1219
|
+
// route; fall through to the capped base64 ack path below.
|
|
1220
|
+
const routeMissing =
|
|
1221
|
+
(err?.status === 404 && !err?.code) || err?.status === 501;
|
|
1222
|
+
if (!routeMissing) {
|
|
1223
|
+
cleanupTemps();
|
|
1224
|
+
fail(`add-slide failed for selection ${pick}: ${err.message}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const res = await client.extractSlide({
|
|
1229
|
+
presId,
|
|
1230
|
+
searchId,
|
|
1231
|
+
selection: pick,
|
|
1232
|
+
backendURL,
|
|
1233
|
+
});
|
|
1234
|
+
if (!res?.ok || !res.base64) {
|
|
1235
|
+
cleanupTemps();
|
|
1236
|
+
fail(`add-slide failed for selection ${pick}: ${res?.error ?? "no slide bytes returned"}`);
|
|
1237
|
+
}
|
|
1238
|
+
extracted.push({ selection: pick, slideId: res.slideId ?? null, base64: res.base64 });
|
|
1239
|
+
}
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
cleanupTemps();
|
|
1242
|
+
throw err;
|
|
780
1243
|
}
|
|
781
1244
|
const existing = fs
|
|
782
1245
|
.readdirSync(slidesDir)
|
|
@@ -785,7 +1248,11 @@ async function cmdAddSlide(rest) {
|
|
|
785
1248
|
const written = [];
|
|
786
1249
|
extracted.forEach((slide, i) => {
|
|
787
1250
|
const target = path.join(slidesDir, targets[i].filename);
|
|
788
|
-
|
|
1251
|
+
if (slide.tempPath) {
|
|
1252
|
+
fs.renameSync(slide.tempPath, target);
|
|
1253
|
+
} else {
|
|
1254
|
+
fs.writeFileSync(target, Buffer.from(slide.base64, "base64"));
|
|
1255
|
+
}
|
|
789
1256
|
written.push(path.relative(process.cwd(), target));
|
|
790
1257
|
});
|
|
791
1258
|
process.stdout.write(
|
|
@@ -855,7 +1322,7 @@ async function cmdViewSlide(rest) {
|
|
|
855
1322
|
const client = await import(
|
|
856
1323
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
857
1324
|
);
|
|
858
|
-
const {
|
|
1325
|
+
const { resolveAssetExt } = await import(
|
|
859
1326
|
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
860
1327
|
);
|
|
861
1328
|
const registry = await client.requirePaneHost();
|
|
@@ -877,8 +1344,15 @@ async function cmdViewSlide(rest) {
|
|
|
877
1344
|
? path.resolve(process.cwd(), outDir)
|
|
878
1345
|
: fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-view-"));
|
|
879
1346
|
fs.mkdirSync(dir, { recursive: true });
|
|
880
|
-
|
|
881
|
-
|
|
1347
|
+
// Extension from the BYTES, not the declared MIME — engines inherit a
|
|
1348
|
+
// Content-Type from upstream and it is demonstrably wrong for some corpus
|
|
1349
|
+
// images (PNG served as image/jpeg).
|
|
1350
|
+
const bytes = Buffer.from(m[2], "base64");
|
|
1351
|
+
const target = path.join(
|
|
1352
|
+
dir,
|
|
1353
|
+
`slide-${searchId}-${pick}.${resolveAssetExt(m[1], bytes)}`,
|
|
1354
|
+
);
|
|
1355
|
+
fs.writeFileSync(target, bytes);
|
|
882
1356
|
process.stdout.write(
|
|
883
1357
|
`Wrote preview of result ${pick} (searchId ${searchId}) to ${target}\n`,
|
|
884
1358
|
);
|
|
@@ -956,7 +1430,11 @@ async function cmdDownloadResult(rest) {
|
|
|
956
1430
|
? r.value
|
|
957
1431
|
: { selection: picks[i], error: r.reason?.message ?? String(r.reason) },
|
|
958
1432
|
);
|
|
1433
|
+
const { formatSize } = await import(
|
|
1434
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "image-dimensions.mjs")).href
|
|
1435
|
+
);
|
|
959
1436
|
let ok = 0;
|
|
1437
|
+
let anySized = false;
|
|
960
1438
|
const lines = [];
|
|
961
1439
|
for (const r of results) {
|
|
962
1440
|
if (r.error) {
|
|
@@ -965,16 +1443,197 @@ async function cmdDownloadResult(rest) {
|
|
|
965
1443
|
}
|
|
966
1444
|
ok++;
|
|
967
1445
|
const note = r.recolor?.note ? ` — ${r.recolor.note}` : "";
|
|
968
|
-
|
|
1446
|
+
// The intrinsic size is the agent's only handle on the asset's aspect
|
|
1447
|
+
// ratio — without it the <img> box is a guess and the render stretches.
|
|
1448
|
+
const dims = formatSize(r.size);
|
|
1449
|
+
anySized ||= Boolean(dims);
|
|
1450
|
+
const size = dims ? ` ${dims}` : " (size unknown)";
|
|
1451
|
+
lines.push(` ${r.reference}${size} (${r.recolor?.mode ?? "?"})${note}`);
|
|
969
1452
|
}
|
|
970
1453
|
process.stdout.write(
|
|
971
1454
|
`Downloaded ${ok}/${results.length} result(s) into ${path.relative(process.cwd(), assetsDir)}:\n` +
|
|
972
1455
|
lines.join("\n") +
|
|
973
|
-
`\nReference each from slide JSX as <img src="./assets/...">.\n
|
|
1456
|
+
`\nReference each from slide JSX as <img src="./assets/...">.\n` +
|
|
1457
|
+
(anySized
|
|
1458
|
+
? `Sizes above are the assets' intrinsic width×height — give each <img> a box ` +
|
|
1459
|
+
`with that aspect ratio, or the render stretches it to whatever box you wrote.\n`
|
|
1460
|
+
: ""),
|
|
974
1461
|
);
|
|
975
1462
|
if (ok === 0) process.exit(1);
|
|
976
1463
|
}
|
|
977
1464
|
|
|
1465
|
+
// Strip the `data:<mime>;base64,` prefix off a data URI and return the raw
|
|
1466
|
+
// base64 payload (or null when the URI isn't a base64 data URI we can decode).
|
|
1467
|
+
function base64FromDataUri(dataUri) {
|
|
1468
|
+
if (typeof dataUri !== "string") return null;
|
|
1469
|
+
const m = /^data:[^;,]*;base64,(.*)$/s.exec(dataUri.trim());
|
|
1470
|
+
return m ? m[1] : null;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
async function cmdConceptImages(rest) {
|
|
1474
|
+
let deck = null;
|
|
1475
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1476
|
+
const a = rest[i];
|
|
1477
|
+
if (a === "--deck") {
|
|
1478
|
+
deck = rest[++i];
|
|
1479
|
+
if (!deck) fail("--deck expects a deck name or path");
|
|
1480
|
+
} else if (a.startsWith("-")) {
|
|
1481
|
+
fail(`unknown flag: ${a}`);
|
|
1482
|
+
} else if (deck === null) {
|
|
1483
|
+
deck = a;
|
|
1484
|
+
} else {
|
|
1485
|
+
fail(`unexpected argument: ${a}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (!deck) fail("usage: slidemind concepts <deck>");
|
|
1489
|
+
|
|
1490
|
+
const deckDir = resolveDeckArg(deck);
|
|
1491
|
+
const planPath = path.join(deckDir, "plan.md");
|
|
1492
|
+
if (!fs.existsSync(planPath)) {
|
|
1493
|
+
fail(
|
|
1494
|
+
`no plan.md in ${path.relative(process.cwd(), deckDir) || deckDir}. ` +
|
|
1495
|
+
`Run \`slidemind plan ${deck}\` to compose and sign off a plan first.`,
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const planMarkdown = fs.readFileSync(planPath, "utf8");
|
|
1499
|
+
|
|
1500
|
+
const bundle = await import(
|
|
1501
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
1502
|
+
);
|
|
1503
|
+
const brandId = bundle.readDeckBundleRef(deckDir)?.presFP ?? null;
|
|
1504
|
+
if (!brandId) {
|
|
1505
|
+
fail(
|
|
1506
|
+
`deck ${deck} is not bound to a corporate design (no .bundle-ref.json). ` +
|
|
1507
|
+
`Run \`slidemind ready ${deck}\` to bind it first.`,
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Plan sign-off is advisory here, not a hard gate: warn but proceed if the
|
|
1512
|
+
// plan wasn't approved, so an agent can still fetch mockups while iterating.
|
|
1513
|
+
const plan = await import(
|
|
1514
|
+
pathToFileURL(path.join(BUILDERS_DIR, "plan.mjs")).href
|
|
1515
|
+
);
|
|
1516
|
+
const planState = plan.loadPlanState(deckDir);
|
|
1517
|
+
if (planState?.decision !== "approved") {
|
|
1518
|
+
process.stderr.write(
|
|
1519
|
+
`slidemind: note — ${deck}'s plan is not signed off yet ` +
|
|
1520
|
+
`(run \`slidemind plan ${deck}\`); generating concept mockups from the current plan.md anyway.\n`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const client = await import(
|
|
1525
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1526
|
+
);
|
|
1527
|
+
|
|
1528
|
+
const skipNote = () => {
|
|
1529
|
+
process.stdout.write(
|
|
1530
|
+
"no concept mockups generated — continuing without " +
|
|
1531
|
+
"(this add-in doesn't generate concept mockups; author from the storyboards).\n",
|
|
1532
|
+
);
|
|
1533
|
+
process.exit(0);
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
// First request learns slideCount (and pins the serving backend for the rest).
|
|
1537
|
+
let first;
|
|
1538
|
+
try {
|
|
1539
|
+
first = await client.generateConceptImagesForBrand(brandId, {
|
|
1540
|
+
planMarkdown,
|
|
1541
|
+
slideNumbers: [1],
|
|
1542
|
+
});
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
if (err?.code === "concept_route_unavailable") skipNote();
|
|
1545
|
+
throw err;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const backendURL = first.backendURL ?? null;
|
|
1549
|
+
const slideCount = Number.isFinite(first.slideCount) ? first.slideCount : 0;
|
|
1550
|
+
const images = [];
|
|
1551
|
+
const collect = (res) => {
|
|
1552
|
+
if (Array.isArray(res?.images)) images.push(...res.images);
|
|
1553
|
+
};
|
|
1554
|
+
collect(first);
|
|
1555
|
+
|
|
1556
|
+
// Family variants depend on their lowest-numbered anchor. Run independent
|
|
1557
|
+
// slides + anchors first, then variants; the server cache remains a safety
|
|
1558
|
+
// net for older clients and retries.
|
|
1559
|
+
const families = await import(
|
|
1560
|
+
pathToFileURL(path.join(BUILDERS_DIR, "visual-families.mjs")).href
|
|
1561
|
+
);
|
|
1562
|
+
const waves = families.conceptGenerationWaves(planMarkdown, slideCount);
|
|
1563
|
+
const CONCURRENCY = 3;
|
|
1564
|
+
async function runWave(slides) {
|
|
1565
|
+
let cursor = 0;
|
|
1566
|
+
async function worker() {
|
|
1567
|
+
for (;;) {
|
|
1568
|
+
const idx = cursor++;
|
|
1569
|
+
if (idx >= slides.length) return;
|
|
1570
|
+
const n = slides[idx];
|
|
1571
|
+
try {
|
|
1572
|
+
const res = await client.generateConceptImagesForBrand(brandId, {
|
|
1573
|
+
planMarkdown,
|
|
1574
|
+
slideNumbers: [n],
|
|
1575
|
+
backendURL,
|
|
1576
|
+
});
|
|
1577
|
+
collect(res);
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
process.stderr.write(
|
|
1580
|
+
`slidemind: concept mockup for slide ${n} failed: ${err?.message ?? err}\n`,
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
await Promise.all(
|
|
1586
|
+
Array.from({ length: Math.min(CONCURRENCY, slides.length) }, () => worker()),
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
await runWave(waves.firstWave);
|
|
1590
|
+
await runWave(waves.variants);
|
|
1591
|
+
|
|
1592
|
+
if (images.length === 0) {
|
|
1593
|
+
process.stdout.write(
|
|
1594
|
+
"no concept mockups generated — continuing without.\n",
|
|
1595
|
+
);
|
|
1596
|
+
process.exit(0);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const conceptsDir = path.join(deckDir, "concepts");
|
|
1600
|
+
fs.mkdirSync(conceptsDir, { recursive: true });
|
|
1601
|
+
const written = [];
|
|
1602
|
+
for (const img of images) {
|
|
1603
|
+
const n = Number(img?.slideNumber);
|
|
1604
|
+
if (!Number.isFinite(n) || n < 1) continue;
|
|
1605
|
+
const b64 = base64FromDataUri(img?.dataUri);
|
|
1606
|
+
if (!b64) continue;
|
|
1607
|
+
const name = `concept-slide-${String(n).padStart(2, "0")}.png`;
|
|
1608
|
+
const abs = path.join(conceptsDir, name);
|
|
1609
|
+
fs.writeFileSync(abs, Buffer.from(b64, "base64"));
|
|
1610
|
+
written.push(abs);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Drop stale mockups from a previous, longer plan — any concept-slide-NN.png
|
|
1614
|
+
// whose slide number now exceeds the plan's slideCount.
|
|
1615
|
+
for (const entry of fs.readdirSync(conceptsDir)) {
|
|
1616
|
+
const m = /^concept-slide-(\d+)\.png$/.exec(entry);
|
|
1617
|
+
if (m && Number(m[1]) > slideCount) {
|
|
1618
|
+
fs.rmSync(path.join(conceptsDir, entry), { force: true });
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
if (written.length === 0) {
|
|
1623
|
+
process.stdout.write(
|
|
1624
|
+
"no concept mockups generated — continuing without.\n",
|
|
1625
|
+
);
|
|
1626
|
+
process.exit(0);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
written.sort();
|
|
1630
|
+
process.stdout.write(
|
|
1631
|
+
written.join("\n") +
|
|
1632
|
+
`\nWrote ${written.length} concept mockup(s) to ${conceptsDir} — ` +
|
|
1633
|
+
"Read each, then rebuild as slide JSX (workflow/create.md → Preconditions).\n",
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
978
1637
|
async function cmdDownloadBundle(rest) {
|
|
979
1638
|
let to = null;
|
|
980
1639
|
let force = false;
|
|
@@ -1287,6 +1946,10 @@ async function cmdReady(rest) {
|
|
|
1287
1946
|
const client = await import(
|
|
1288
1947
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1289
1948
|
);
|
|
1949
|
+
// Same opt-out as doctor: `ready` OWNS the cli_update_required blocker (it is
|
|
1950
|
+
// terminal, `actor: "agent"`, with the update command as its nextAction), so
|
|
1951
|
+
// the raw header throw would only replace a structured answer with a worse one.
|
|
1952
|
+
client.setDiagnosticMode(true);
|
|
1290
1953
|
|
|
1291
1954
|
// --- Network (one parallel round): readiness ladder + the merged registry. ---
|
|
1292
1955
|
const [statusRaw, detailed] = await Promise.all([
|
|
@@ -1604,6 +2267,9 @@ async function cmdDoctor(rest) {
|
|
|
1604
2267
|
const client = await import(
|
|
1605
2268
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1606
2269
|
);
|
|
2270
|
+
// doctor diagnoses skew — it must never be killed BY skew. The version floor
|
|
2271
|
+
// still reaches the reader, as the `cli_update_required` rung of the ladder.
|
|
2272
|
+
client.setDiagnosticMode(true);
|
|
1607
2273
|
if (sessions.length === 0) {
|
|
1608
2274
|
const pub = {
|
|
1609
2275
|
paired: false,
|
|
@@ -1652,14 +2318,54 @@ async function cmdDoctor(rest) {
|
|
|
1652
2318
|
process.stdout.write(`Next action: ${status.nextAction}\n`);
|
|
1653
2319
|
}
|
|
1654
2320
|
|
|
2321
|
+
// Print a read result — the `{ ok, mode, slideCount, slides[] }` shape, whether
|
|
2322
|
+
// it came from the add-in (open presentation) or a local .pptx. Shared by both
|
|
2323
|
+
// `cmdRead` branches so their output is byte-for-byte identical.
|
|
2324
|
+
function printReadResult(res, asJson) {
|
|
2325
|
+
if (asJson) {
|
|
2326
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const slides = Array.isArray(res.slides) ? res.slides : [];
|
|
2330
|
+
if (slides.length === 0) {
|
|
2331
|
+
process.stderr.write("(presentation has no slides)\n");
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
if (res.mode === "summary") {
|
|
2335
|
+
for (const s of slides) {
|
|
2336
|
+
const label = s.title || "(untitled)";
|
|
2337
|
+
const sub = s.subtitle ? ` — ${s.subtitle}` : "";
|
|
2338
|
+
const layout = s.layout ? ` [${s.layout}]` : "";
|
|
2339
|
+
process.stdout.write(
|
|
2340
|
+
`${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
} else {
|
|
2344
|
+
slides.forEach((s, i) => {
|
|
2345
|
+
if (i > 0) process.stdout.write("\n");
|
|
2346
|
+
const layout = s.layout ? ` (${s.layout})` : "";
|
|
2347
|
+
process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
|
|
2348
|
+
process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
1655
2353
|
// Pull the current content of an open presentation. Content is window-bound, so
|
|
1656
2354
|
// pick exactly one window (positional presId or --to; pickPresentationEntry errors
|
|
1657
2355
|
// listing the windows if several are open and none was named). `--mode summary`
|
|
1658
2356
|
// prints a per-slide outline; default `raw` prints each slide's plain text.
|
|
2357
|
+
//
|
|
2358
|
+
// A positional arg with a local PowerPoint extension (LOCAL_PPTX_RE: .pptx and
|
|
2359
|
+
// its .pptm/.potx/.potm/.ppsx/.ppsm siblings) AND resolving to an existing
|
|
2360
|
+
// regular file is read host-free from disk instead (no bridge). The gate is
|
|
2361
|
+
// strict: a nonexistent path or a real presId falls through to the host path
|
|
2362
|
+
// unchanged, and it is skipped when --to is given (--to targets an open window).
|
|
1659
2363
|
async function cmdRead(rest) {
|
|
1660
2364
|
let presId = null;
|
|
1661
2365
|
let mode = "raw";
|
|
1662
2366
|
let asJson = false;
|
|
2367
|
+
let toGiven = false;
|
|
2368
|
+
let fileArg = null; // the positional (a candidate local .pptx path)
|
|
1663
2369
|
for (let i = 0; i < rest.length; i++) {
|
|
1664
2370
|
const a = rest[i];
|
|
1665
2371
|
if (a === "--mode") {
|
|
@@ -1671,16 +2377,47 @@ async function cmdRead(rest) {
|
|
|
1671
2377
|
} else if (a === "--to") {
|
|
1672
2378
|
presId = rest[++i];
|
|
1673
2379
|
if (!presId) fail("--to expects a presId");
|
|
2380
|
+
toGiven = true;
|
|
1674
2381
|
} else if (a === "--json") {
|
|
1675
2382
|
asJson = true;
|
|
1676
2383
|
} else if (a.startsWith("-")) {
|
|
1677
2384
|
fail(`unknown flag: ${a}`);
|
|
1678
2385
|
} else if (!presId) {
|
|
1679
|
-
presId = a; // positional presId
|
|
2386
|
+
presId = a; // positional presId (or local .pptx path)
|
|
2387
|
+
fileArg = a;
|
|
1680
2388
|
} else {
|
|
1681
2389
|
fail(`unexpected argument: ${a}`);
|
|
1682
2390
|
}
|
|
1683
2391
|
}
|
|
2392
|
+
|
|
2393
|
+
// Local-file gate: applies ONLY to the positional, and never when --to is set.
|
|
2394
|
+
// The positional must end in .pptx and resolve to an existing regular file;
|
|
2395
|
+
// anything else (missing path, real presId) falls through to the host path.
|
|
2396
|
+
if (fileArg && !toGiven && LOCAL_PPTX_RE.test(fileArg)) {
|
|
2397
|
+
const p = path.isAbsolute(fileArg)
|
|
2398
|
+
? fileArg
|
|
2399
|
+
: path.resolve(process.cwd(), fileArg);
|
|
2400
|
+
let st = null;
|
|
2401
|
+
try {
|
|
2402
|
+
st = fs.statSync(p);
|
|
2403
|
+
} catch {
|
|
2404
|
+
st = null;
|
|
2405
|
+
}
|
|
2406
|
+
if (st && st.isFile()) {
|
|
2407
|
+
const { readLocalPptx } = await import(
|
|
2408
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "read-local.mjs")).href
|
|
2409
|
+
);
|
|
2410
|
+
let res;
|
|
2411
|
+
try {
|
|
2412
|
+
res = readLocalPptx(fs.readFileSync(p), { mode });
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
fail(`read failed: ${err.message}`);
|
|
2415
|
+
}
|
|
2416
|
+
printReadResult(res, asJson);
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
1684
2421
|
const client = await import(
|
|
1685
2422
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1686
2423
|
);
|
|
@@ -1694,32 +2431,7 @@ async function cmdRead(rest) {
|
|
|
1694
2431
|
if (!res?.ok) {
|
|
1695
2432
|
fail(`read failed: ${res?.error ?? "unknown error"}`);
|
|
1696
2433
|
}
|
|
1697
|
-
|
|
1698
|
-
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
const slides = Array.isArray(res.slides) ? res.slides : [];
|
|
1702
|
-
if (slides.length === 0) {
|
|
1703
|
-
process.stderr.write("(presentation has no slides)\n");
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
if (res.mode === "summary") {
|
|
1707
|
-
for (const s of slides) {
|
|
1708
|
-
const label = s.title || "(untitled)";
|
|
1709
|
-
const sub = s.subtitle ? ` — ${s.subtitle}` : "";
|
|
1710
|
-
const layout = s.layout ? ` [${s.layout}]` : "";
|
|
1711
|
-
process.stdout.write(
|
|
1712
|
-
`${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
|
-
} else {
|
|
1716
|
-
slides.forEach((s, i) => {
|
|
1717
|
-
if (i > 0) process.stdout.write("\n");
|
|
1718
|
-
const layout = s.layout ? ` (${s.layout})` : "";
|
|
1719
|
-
process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
|
|
1720
|
-
process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
2434
|
+
printReadResult(res, asJson);
|
|
1723
2435
|
}
|
|
1724
2436
|
|
|
1725
2437
|
async function loadBridgeModule(name) {
|
|
@@ -2221,7 +2933,22 @@ async function cmdUpdate(rest) {
|
|
|
2221
2933
|
: "slidemind@latest";
|
|
2222
2934
|
await ui.section("release", ref ? `pinned ${spec}` : "latest");
|
|
2223
2935
|
ui.step({ label: "install", value: spec, note: "global (npm)" });
|
|
2224
|
-
|
|
2936
|
+
try {
|
|
2937
|
+
await spawnInherit(npmBin(), ["install", "-g", "--loglevel=error", spec]);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
console.error(
|
|
2940
|
+
"\nslidemind update could not write to npm's global package directory.\n" +
|
|
2941
|
+
"This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
|
|
2942
|
+
"Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
|
|
2943
|
+
" mkdir -p ~/.npm-global\n" +
|
|
2944
|
+
" npm config set prefix ~/.npm-global\n" +
|
|
2945
|
+
" echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
|
|
2946
|
+
" source ~/.zshrc\n" +
|
|
2947
|
+
" npm install -g slidemind\n\n" +
|
|
2948
|
+
"Then run `slidemind init --for all`.\n",
|
|
2949
|
+
);
|
|
2950
|
+
throw err;
|
|
2951
|
+
}
|
|
2225
2952
|
|
|
2226
2953
|
if (target !== "none") {
|
|
2227
2954
|
await spawnInherit("slidemind", ["init", "--for", target]);
|
|
@@ -2275,6 +3002,9 @@ async function main(rawArgv) {
|
|
|
2275
3002
|
case "review":
|
|
2276
3003
|
await cmdReview(rest);
|
|
2277
3004
|
return 0;
|
|
3005
|
+
case "imagen":
|
|
3006
|
+
await cmdImagen(rest);
|
|
3007
|
+
break;
|
|
2278
3008
|
case "build-pptx":
|
|
2279
3009
|
await cmdBuildPptx(rest);
|
|
2280
3010
|
return 0;
|
|
@@ -2299,6 +3029,9 @@ async function main(rawArgv) {
|
|
|
2299
3029
|
case "download-result":
|
|
2300
3030
|
await cmdDownloadResult(rest);
|
|
2301
3031
|
return 0;
|
|
3032
|
+
case "concepts":
|
|
3033
|
+
await cmdConceptImages(rest);
|
|
3034
|
+
return 0;
|
|
2302
3035
|
case "download-bundle":
|
|
2303
3036
|
await cmdDownloadBundle(rest);
|
|
2304
3037
|
return 0;
|