slidemind 0.1.8 → 0.1.9

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/bin/slidemind.mjs CHANGED
@@ -60,6 +60,10 @@ 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` +
@@ -95,11 +99,12 @@ function printHelp() {
95
99
  ` --pick <n,..> 1-based selection number(s)\n` +
96
100
  ` --to <presId> target window (required if >1 open)\n` +
97
101
  ` --at <N> insert after slide N (0-based); default = append\n` +
98
- ` slidemind add-slide [opts] Fold reused slide search results into a deck as native .pptx\n` +
99
- ` --search <id> searchId from 'slidemind search'\n` +
102
+ ` slidemind add-slide [opts] Fold reused slide(s) into a deck as native .pptx\n` +
103
+ ` --search <id> searchId from 'slidemind search' (requires a host)\n` +
104
+ ` --from <file.pptx> fold slide(s) from a local .pptx instead (host-free)\n` +
100
105
  ` --pick <n,..> 1-based selection number(s)\n` +
101
106
  ` --deck <deck> deck to add the slides to\n` +
102
- ` --to <presId> target the window the search ran on (required if >1 open)\n` +
107
+ ` --to <presId> target the window the search ran on (required if >1 open; ignored with --from)\n` +
103
108
  ` --at <N> numeric prefix to start at (default: append)\n` +
104
109
  ` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/ (requires a host)\n` +
105
110
  ` --search <id> searchId from 'slidemind search'\n` +
@@ -114,7 +119,8 @@ function printHelp() {
114
119
  ` --all include retained suspended entries\n` +
115
120
  ` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
116
121
  ` --json print machine-readable status\n` +
117
- ` slidemind read [presId] Pull the current content of an open presentation (requires a host)\n` +
122
+ ` slidemind read [presId|file.pptx] Pull the content of an open presentation (requires a host),\n` +
123
+ ` or of a local .pptx path (read host-free from disk)\n` +
118
124
  ` --mode <m> summary | raw (default: raw)\n` +
119
125
  ` --to <presId> target a specific window (required if >1 open)\n` +
120
126
  ` --json print the raw content payload\n` +
@@ -149,6 +155,15 @@ function fail(msg, code = 1) {
149
155
  process.exit(code);
150
156
  }
151
157
 
158
+ // The local PowerPoint files `read` and `add-slide --from` accept host-free.
159
+ // All are OPC (zip) OOXML packages with the same ppt/presentation.xml +
160
+ // ppt/slides/ structure — presentation (.pptx/.pptm), template (.potx/.potm),
161
+ // slideshow (.ppsx/.ppsm) — so the src/pptx/ reader/swapper handle them
162
+ // identically. Presentation IDs are opaque and never carry these extensions,
163
+ // so this stays a safe discriminator for "positional is a local file, not a
164
+ // presId".
165
+ const LOCAL_PPTX_RE = /\.(pptx|pptm|potx|potm|ppsx|ppsm)$/i;
166
+
152
167
  function resolveDecksDir() {
153
168
  if (process.env.SLIDEMIND_DECKS) {
154
169
  return path.resolve(process.env.SLIDEMIND_DECKS);
@@ -168,12 +183,25 @@ function resolveDeckArg(arg) {
168
183
  return path.join(decksDir, arg);
169
184
  }
170
185
 
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.
186
+ // Best-effort human name of a materialized design. Prefers the live `name`
187
+ // the wire carries (persisted into bundle.json by materializeBundle) — the
188
+ // owner-set brand name, which survives renames. Falls back to the first `# `
189
+ // heading in design.md (frozen at authoring time, so it may still show the
190
+ // original auto codename), with the " — design rules" suffix stripped.
191
+ // Returns null on any failure so callers fall back to the raw presFP.
192
+ // Shared by `ready` and `new`'s auto-bind.
175
193
  function readDesignName(bundleDir) {
176
194
  if (!bundleDir) return null;
195
+ try {
196
+ const meta = JSON.parse(
197
+ fs.readFileSync(path.join(bundleDir, "bundle.json"), "utf8"),
198
+ );
199
+ if (typeof meta.name === "string" && meta.name.trim()) {
200
+ return meta.name.trim();
201
+ }
202
+ } catch {
203
+ /* no meta or unreadable — fall through to the design.md heading */
204
+ }
177
205
  try {
178
206
  const md = fs.readFileSync(path.join(bundleDir, "design.md"), "utf8");
179
207
  for (const raw of md.split("\n")) {
@@ -484,10 +512,19 @@ async function cmdSearch(rest) {
484
512
  );
485
513
  }
486
514
  const mat = await bundle.materializeBundle(presId, { backendURL });
487
- const pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
515
+ let pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
516
+ if (!pick) {
517
+ // New backends no longer ship engines[] on the bundle wire — discovery
518
+ // is live-only. Fall back to the catalog the add-in's own design agent
519
+ // uses (its list_engines tool ≙ `slidemind ls engines`).
520
+ const live = await client
521
+ .listEngines({ subtype: "image", backendURL })
522
+ .catch(() => ({ engines: [] }));
523
+ pick = bundle.pickImageEngine(live.engines);
524
+ }
488
525
  if (!pick) {
489
526
  fail(
490
- "This corporate design has no brand engines to search. Ask the add-in owner to wire an image/icon engine.",
527
+ "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.",
491
528
  );
492
529
  }
493
530
  engineId = pick.id;
@@ -713,6 +750,7 @@ async function cmdAddSlide(rest) {
713
750
  let deck = null;
714
751
  let at = null;
715
752
  let to = null;
753
+ let from = null;
716
754
  for (let i = 0; i < rest.length; i++) {
717
755
  const a = rest[i];
718
756
  if (a === "--search") {
@@ -720,6 +758,9 @@ async function cmdAddSlide(rest) {
720
758
  const n = parseInt(next, 10);
721
759
  if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
722
760
  searchId = n;
761
+ } else if (a === "--from") {
762
+ from = rest[++i];
763
+ if (!from) fail("--from expects a path to a .pptx file");
723
764
  } else if (a === "--pick") {
724
765
  picks = parsePicks(rest[++i]);
725
766
  } else if (a === "--deck") {
@@ -741,8 +782,69 @@ async function cmdAddSlide(rest) {
741
782
  fail(`unexpected argument: ${a}`);
742
783
  }
743
784
  }
785
+
786
+ // Host-free local source: fold picked slides from a .pptx on disk (no bridge
787
+ // client, no requirePaneHost). Mutually exclusive with --search; --to is
788
+ // accepted-but-ignored here (symmetric with the other reuse commands — there's
789
+ // no window to target when the source is a local file).
790
+ if (from !== null) {
791
+ if (searchId !== null) fail("--from and --search are mutually exclusive");
792
+ if (!picks || !deck) {
793
+ fail("usage: slidemind add-slide --from <file.pptx> --pick <n[,n,...]> --deck <deck> [--at <N>]");
794
+ }
795
+ const fromPath = path.isAbsolute(from)
796
+ ? from
797
+ : path.resolve(process.cwd(), from);
798
+ let st = null;
799
+ try {
800
+ st = fs.statSync(fromPath);
801
+ } catch {
802
+ st = null;
803
+ }
804
+ if (!st || !st.isFile() || !LOCAL_PPTX_RE.test(fromPath)) {
805
+ fail(`--from: '${from}' is not an existing PowerPoint file (.pptx/.pptm/.potx/.potm/.ppsx/.ppsm)`);
806
+ }
807
+ const deckDir = resolveDeckArg(deck);
808
+ const slidesDir = path.join(deckDir, "slides");
809
+ fs.mkdirSync(slidesDir, { recursive: true });
810
+
811
+ const { foldPickedSlides } = await import(
812
+ pathToFileURL(path.join(SRC_DIR, "pptx", "slide-swap.mjs")).href
813
+ );
814
+ let results;
815
+ try {
816
+ results = foldPickedSlides(fs.readFileSync(fromPath), picks, {
817
+ sourceName: path.basename(fromPath),
818
+ });
819
+ } catch (err) {
820
+ fail(`add-slide failed: ${err.message}`);
821
+ }
822
+
823
+ // Same filename math + write loop as the --search branch; results already
824
+ // carry a `.slideId` and `.bytes` (a Uint8Array of a one-slide package).
825
+ const reused = await import(
826
+ pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
827
+ );
828
+ const existing = fs
829
+ .readdirSync(slidesDir)
830
+ .filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"));
831
+ const targets = reused.reusedSlideTargets(existing, results, at);
832
+ const written = [];
833
+ results.forEach((r, i) => {
834
+ const target = path.join(slidesDir, targets[i].filename);
835
+ fs.writeFileSync(target, Buffer.from(r.bytes));
836
+ written.push(path.relative(process.cwd(), target));
837
+ });
838
+ process.stdout.write(
839
+ `Added ${written.length} reused slide(s) to ${path.relative(process.cwd(), deckDir)}:\n` +
840
+ written.map((w) => ` ${w}`).join("\n") +
841
+ `\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`,
842
+ );
843
+ return;
844
+ }
845
+
744
846
  if (searchId === null || !picks || !deck) {
745
- fail("usage: slidemind add-slide --search <id> --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]");
847
+ fail("usage: slidemind add-slide (--search <id> | --from <file.pptx>) --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]");
746
848
  }
747
849
  const deckDir = resolveDeckArg(deck);
748
850
  const slidesDir = path.join(deckDir, "slides");
@@ -975,6 +1077,178 @@ async function cmdDownloadResult(rest) {
975
1077
  if (ok === 0) process.exit(1);
976
1078
  }
977
1079
 
1080
+ // Strip the `data:<mime>;base64,` prefix off a data URI and return the raw
1081
+ // base64 payload (or null when the URI isn't a base64 data URI we can decode).
1082
+ function base64FromDataUri(dataUri) {
1083
+ if (typeof dataUri !== "string") return null;
1084
+ const m = /^data:[^;,]*;base64,(.*)$/s.exec(dataUri.trim());
1085
+ return m ? m[1] : null;
1086
+ }
1087
+
1088
+ async function cmdConceptImages(rest) {
1089
+ let deck = null;
1090
+ for (let i = 0; i < rest.length; i++) {
1091
+ const a = rest[i];
1092
+ if (a === "--deck") {
1093
+ deck = rest[++i];
1094
+ if (!deck) fail("--deck expects a deck name or path");
1095
+ } else if (a.startsWith("-")) {
1096
+ fail(`unknown flag: ${a}`);
1097
+ } else if (deck === null) {
1098
+ deck = a;
1099
+ } else {
1100
+ fail(`unexpected argument: ${a}`);
1101
+ }
1102
+ }
1103
+ if (!deck) fail("usage: slidemind concepts <deck>");
1104
+
1105
+ const deckDir = resolveDeckArg(deck);
1106
+ const planPath = path.join(deckDir, "plan.md");
1107
+ if (!fs.existsSync(planPath)) {
1108
+ fail(
1109
+ `no plan.md in ${path.relative(process.cwd(), deckDir) || deckDir}. ` +
1110
+ `Run \`slidemind plan ${deck}\` to compose and sign off a plan first.`,
1111
+ );
1112
+ }
1113
+ const planMarkdown = fs.readFileSync(planPath, "utf8");
1114
+
1115
+ const bundle = await import(
1116
+ pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
1117
+ );
1118
+ const brandId = bundle.readDeckBundleRef(deckDir)?.presFP ?? null;
1119
+ if (!brandId) {
1120
+ fail(
1121
+ `deck ${deck} is not bound to a corporate design (no .bundle-ref.json). ` +
1122
+ `Run \`slidemind ready ${deck}\` to bind it first.`,
1123
+ );
1124
+ }
1125
+
1126
+ // Plan sign-off is advisory here, not a hard gate: warn but proceed if the
1127
+ // plan wasn't approved, so an agent can still fetch mockups while iterating.
1128
+ const plan = await import(
1129
+ pathToFileURL(path.join(BUILDERS_DIR, "plan.mjs")).href
1130
+ );
1131
+ const planState = plan.loadPlanState(deckDir);
1132
+ if (planState?.decision !== "approved") {
1133
+ process.stderr.write(
1134
+ `slidemind: note — ${deck}'s plan is not signed off yet ` +
1135
+ `(run \`slidemind plan ${deck}\`); generating concept mockups from the current plan.md anyway.\n`,
1136
+ );
1137
+ }
1138
+
1139
+ const client = await import(
1140
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
1141
+ );
1142
+
1143
+ const skipNote = () => {
1144
+ process.stdout.write(
1145
+ "no concept mockups generated — continuing without " +
1146
+ "(this add-in doesn't generate concept mockups; author from the storyboards).\n",
1147
+ );
1148
+ process.exit(0);
1149
+ };
1150
+
1151
+ // First request learns slideCount (and pins the serving backend for the rest).
1152
+ let first;
1153
+ try {
1154
+ first = await client.generateConceptImagesForBrand(brandId, {
1155
+ planMarkdown,
1156
+ slideNumbers: [1],
1157
+ });
1158
+ } catch (err) {
1159
+ if (err?.code === "concept_route_unavailable") skipNote();
1160
+ throw err;
1161
+ }
1162
+
1163
+ const backendURL = first.backendURL ?? null;
1164
+ const slideCount = Number.isFinite(first.slideCount) ? first.slideCount : 0;
1165
+ const images = [];
1166
+ const collect = (res) => {
1167
+ if (Array.isArray(res?.images)) images.push(...res.images);
1168
+ };
1169
+ collect(first);
1170
+
1171
+ // Family variants depend on their lowest-numbered anchor. Run independent
1172
+ // slides + anchors first, then variants; the server cache remains a safety
1173
+ // net for older clients and retries.
1174
+ const families = await import(
1175
+ pathToFileURL(path.join(BUILDERS_DIR, "visual-families.mjs")).href
1176
+ );
1177
+ const waves = families.conceptGenerationWaves(planMarkdown, slideCount);
1178
+ const CONCURRENCY = 3;
1179
+ async function runWave(slides) {
1180
+ let cursor = 0;
1181
+ async function worker() {
1182
+ for (;;) {
1183
+ const idx = cursor++;
1184
+ if (idx >= slides.length) return;
1185
+ const n = slides[idx];
1186
+ try {
1187
+ const res = await client.generateConceptImagesForBrand(brandId, {
1188
+ planMarkdown,
1189
+ slideNumbers: [n],
1190
+ backendURL,
1191
+ });
1192
+ collect(res);
1193
+ } catch (err) {
1194
+ process.stderr.write(
1195
+ `slidemind: concept mockup for slide ${n} failed: ${err?.message ?? err}\n`,
1196
+ );
1197
+ }
1198
+ }
1199
+ }
1200
+ await Promise.all(
1201
+ Array.from({ length: Math.min(CONCURRENCY, slides.length) }, () => worker()),
1202
+ );
1203
+ }
1204
+ await runWave(waves.firstWave);
1205
+ await runWave(waves.variants);
1206
+
1207
+ if (images.length === 0) {
1208
+ process.stdout.write(
1209
+ "no concept mockups generated — continuing without.\n",
1210
+ );
1211
+ process.exit(0);
1212
+ }
1213
+
1214
+ const conceptsDir = path.join(deckDir, "concepts");
1215
+ fs.mkdirSync(conceptsDir, { recursive: true });
1216
+ const written = [];
1217
+ for (const img of images) {
1218
+ const n = Number(img?.slideNumber);
1219
+ if (!Number.isFinite(n) || n < 1) continue;
1220
+ const b64 = base64FromDataUri(img?.dataUri);
1221
+ if (!b64) continue;
1222
+ const name = `concept-slide-${String(n).padStart(2, "0")}.png`;
1223
+ const abs = path.join(conceptsDir, name);
1224
+ fs.writeFileSync(abs, Buffer.from(b64, "base64"));
1225
+ written.push(abs);
1226
+ }
1227
+
1228
+ // Drop stale mockups from a previous, longer plan — any concept-slide-NN.png
1229
+ // whose slide number now exceeds the plan's slideCount.
1230
+ for (const entry of fs.readdirSync(conceptsDir)) {
1231
+ const m = /^concept-slide-(\d+)\.png$/.exec(entry);
1232
+ if (m && Number(m[1]) > slideCount) {
1233
+ fs.rmSync(path.join(conceptsDir, entry), { force: true });
1234
+ }
1235
+ }
1236
+
1237
+ if (written.length === 0) {
1238
+ process.stdout.write(
1239
+ "no concept mockups generated — continuing without.\n",
1240
+ );
1241
+ process.exit(0);
1242
+ }
1243
+
1244
+ written.sort();
1245
+ process.stdout.write(
1246
+ written.join("\n") +
1247
+ `\nWrote ${written.length} concept mockup(s) to ${conceptsDir} — ` +
1248
+ "Read each, then rebuild as slide JSX (workflow/create.md → Preconditions).\n",
1249
+ );
1250
+ }
1251
+
978
1252
  async function cmdDownloadBundle(rest) {
979
1253
  let to = null;
980
1254
  let force = false;
@@ -1652,14 +1926,54 @@ async function cmdDoctor(rest) {
1652
1926
  process.stdout.write(`Next action: ${status.nextAction}\n`);
1653
1927
  }
1654
1928
 
1929
+ // Print a read result — the `{ ok, mode, slideCount, slides[] }` shape, whether
1930
+ // it came from the add-in (open presentation) or a local .pptx. Shared by both
1931
+ // `cmdRead` branches so their output is byte-for-byte identical.
1932
+ function printReadResult(res, asJson) {
1933
+ if (asJson) {
1934
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
1935
+ return;
1936
+ }
1937
+ const slides = Array.isArray(res.slides) ? res.slides : [];
1938
+ if (slides.length === 0) {
1939
+ process.stderr.write("(presentation has no slides)\n");
1940
+ return;
1941
+ }
1942
+ if (res.mode === "summary") {
1943
+ for (const s of slides) {
1944
+ const label = s.title || "(untitled)";
1945
+ const sub = s.subtitle ? ` — ${s.subtitle}` : "";
1946
+ const layout = s.layout ? ` [${s.layout}]` : "";
1947
+ process.stdout.write(
1948
+ `${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
1949
+ );
1950
+ }
1951
+ } else {
1952
+ slides.forEach((s, i) => {
1953
+ if (i > 0) process.stdout.write("\n");
1954
+ const layout = s.layout ? ` (${s.layout})` : "";
1955
+ process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
1956
+ process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
1957
+ });
1958
+ }
1959
+ }
1960
+
1655
1961
  // Pull the current content of an open presentation. Content is window-bound, so
1656
1962
  // pick exactly one window (positional presId or --to; pickPresentationEntry errors
1657
1963
  // listing the windows if several are open and none was named). `--mode summary`
1658
1964
  // prints a per-slide outline; default `raw` prints each slide's plain text.
1965
+ //
1966
+ // A positional arg with a local PowerPoint extension (LOCAL_PPTX_RE: .pptx and
1967
+ // its .pptm/.potx/.potm/.ppsx/.ppsm siblings) AND resolving to an existing
1968
+ // regular file is read host-free from disk instead (no bridge). The gate is
1969
+ // strict: a nonexistent path or a real presId falls through to the host path
1970
+ // unchanged, and it is skipped when --to is given (--to targets an open window).
1659
1971
  async function cmdRead(rest) {
1660
1972
  let presId = null;
1661
1973
  let mode = "raw";
1662
1974
  let asJson = false;
1975
+ let toGiven = false;
1976
+ let fileArg = null; // the positional (a candidate local .pptx path)
1663
1977
  for (let i = 0; i < rest.length; i++) {
1664
1978
  const a = rest[i];
1665
1979
  if (a === "--mode") {
@@ -1671,16 +1985,47 @@ async function cmdRead(rest) {
1671
1985
  } else if (a === "--to") {
1672
1986
  presId = rest[++i];
1673
1987
  if (!presId) fail("--to expects a presId");
1988
+ toGiven = true;
1674
1989
  } else if (a === "--json") {
1675
1990
  asJson = true;
1676
1991
  } else if (a.startsWith("-")) {
1677
1992
  fail(`unknown flag: ${a}`);
1678
1993
  } else if (!presId) {
1679
- presId = a; // positional presId
1994
+ presId = a; // positional presId (or local .pptx path)
1995
+ fileArg = a;
1680
1996
  } else {
1681
1997
  fail(`unexpected argument: ${a}`);
1682
1998
  }
1683
1999
  }
2000
+
2001
+ // Local-file gate: applies ONLY to the positional, and never when --to is set.
2002
+ // The positional must end in .pptx and resolve to an existing regular file;
2003
+ // anything else (missing path, real presId) falls through to the host path.
2004
+ if (fileArg && !toGiven && LOCAL_PPTX_RE.test(fileArg)) {
2005
+ const p = path.isAbsolute(fileArg)
2006
+ ? fileArg
2007
+ : path.resolve(process.cwd(), fileArg);
2008
+ let st = null;
2009
+ try {
2010
+ st = fs.statSync(p);
2011
+ } catch {
2012
+ st = null;
2013
+ }
2014
+ if (st && st.isFile()) {
2015
+ const { readLocalPptx } = await import(
2016
+ pathToFileURL(path.join(SRC_DIR, "pptx", "read-local.mjs")).href
2017
+ );
2018
+ let res;
2019
+ try {
2020
+ res = readLocalPptx(fs.readFileSync(p), { mode });
2021
+ } catch (err) {
2022
+ fail(`read failed: ${err.message}`);
2023
+ }
2024
+ printReadResult(res, asJson);
2025
+ return;
2026
+ }
2027
+ }
2028
+
1684
2029
  const client = await import(
1685
2030
  pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
1686
2031
  );
@@ -1694,32 +2039,7 @@ async function cmdRead(rest) {
1694
2039
  if (!res?.ok) {
1695
2040
  fail(`read failed: ${res?.error ?? "unknown error"}`);
1696
2041
  }
1697
- if (asJson) {
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
- }
2042
+ printReadResult(res, asJson);
1723
2043
  }
1724
2044
 
1725
2045
  async function loadBridgeModule(name) {
@@ -2221,7 +2541,22 @@ async function cmdUpdate(rest) {
2221
2541
  : "slidemind@latest";
2222
2542
  await ui.section("release", ref ? `pinned ${spec}` : "latest");
2223
2543
  ui.step({ label: "install", value: spec, note: "global (npm)" });
2224
- await spawnInherit(npmBin(), ["install", "-g", "--loglevel=error", spec]);
2544
+ try {
2545
+ await spawnInherit(npmBin(), ["install", "-g", "--loglevel=error", spec]);
2546
+ } catch (err) {
2547
+ console.error(
2548
+ "\nslidemind update could not write to npm's global package directory.\n" +
2549
+ "This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
2550
+ "Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
2551
+ " mkdir -p ~/.npm-global\n" +
2552
+ " npm config set prefix ~/.npm-global\n" +
2553
+ " echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
2554
+ " source ~/.zshrc\n" +
2555
+ " npm install -g slidemind\n\n" +
2556
+ "Then run `slidemind init --for all`.\n",
2557
+ );
2558
+ throw err;
2559
+ }
2225
2560
 
2226
2561
  if (target !== "none") {
2227
2562
  await spawnInherit("slidemind", ["init", "--for", target]);
@@ -2299,6 +2634,9 @@ async function main(rawArgv) {
2299
2634
  case "download-result":
2300
2635
  await cmdDownloadResult(rest);
2301
2636
  return 0;
2637
+ case "concepts":
2638
+ await cmdConceptImages(rest);
2639
+ return 0;
2302
2640
  case "download-bundle":
2303
2641
  await cmdDownloadBundle(rest);
2304
2642
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@clack/prompts": "^1.3.0",
34
+ "fflate": "^0.8.3",
34
35
  "puppeteer": "24.43.0"
35
36
  },
36
37
  "engines": {