slidemind 0.1.7 → 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/CLAUDE.md +10 -7
- package/README.md +20 -7
- package/bin/slidemind.mjs +393 -39
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +68 -16
- package/skills/search/SKILL.md +15 -2
- package/src/bridge/build-and-push.mjs +11 -0
- package/src/bridge/bundle.mjs +15 -4
- package/src/bridge/capture.mjs +20 -2
- package/src/bridge/client.mjs +99 -0
- package/src/builders/build.mjs +83 -11
- package/src/builders/gate-server.mjs +364 -0
- package/src/builders/plan-markdown-edit.mjs +120 -0
- package/src/builders/plan-md.mjs +216 -0
- package/src/builders/plan-overlay.js +1778 -0
- package/src/builders/plan.mjs +696 -0
- package/src/builders/red-thread-backbone.mjs +194 -0
- package/src/builders/resolve-skill.mjs +40 -5
- package/src/builders/review-overlay.js +191 -13
- package/src/builders/review.mjs +196 -68
- 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/hidden-ole.mjs +125 -0
- package/src/pptx/image-extensions.mjs +190 -0
- package/src/pptx/ooxml.mjs +169 -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
|
@@ -56,6 +56,14 @@ function printHelp() {
|
|
|
56
56
|
` slidemind new <name> Scaffold ./decks/<name>/ (auto-binds to the open design)\n` +
|
|
57
57
|
` --to <presId> bind to a specific open window\n` +
|
|
58
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` +
|
|
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` +
|
|
59
67
|
` slidemind build <deck> Build self-contained HTML\n` +
|
|
60
68
|
` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
|
|
61
69
|
` approves or leaves comments (host-free)\n` +
|
|
@@ -91,11 +99,12 @@ function printHelp() {
|
|
|
91
99
|
` --pick <n,..> 1-based selection number(s)\n` +
|
|
92
100
|
` --to <presId> target window (required if >1 open)\n` +
|
|
93
101
|
` --at <N> insert after slide N (0-based); default = append\n` +
|
|
94
|
-
` slidemind add-slide [opts] Fold reused slide
|
|
95
|
-
` --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` +
|
|
96
105
|
` --pick <n,..> 1-based selection number(s)\n` +
|
|
97
106
|
` --deck <deck> deck to add the slides to\n` +
|
|
98
|
-
` --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` +
|
|
99
108
|
` --at <N> numeric prefix to start at (default: append)\n` +
|
|
100
109
|
` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/ (requires a host)\n` +
|
|
101
110
|
` --search <id> searchId from 'slidemind search'\n` +
|
|
@@ -110,7 +119,8 @@ function printHelp() {
|
|
|
110
119
|
` --all include retained suspended entries\n` +
|
|
111
120
|
` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
|
|
112
121
|
` --json print machine-readable status\n` +
|
|
113
|
-
` slidemind read [presId]
|
|
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` +
|
|
114
124
|
` --mode <m> summary | raw (default: raw)\n` +
|
|
115
125
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
116
126
|
` --json print the raw content payload\n` +
|
|
@@ -145,6 +155,15 @@ function fail(msg, code = 1) {
|
|
|
145
155
|
process.exit(code);
|
|
146
156
|
}
|
|
147
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
|
+
|
|
148
167
|
function resolveDecksDir() {
|
|
149
168
|
if (process.env.SLIDEMIND_DECKS) {
|
|
150
169
|
return path.resolve(process.env.SLIDEMIND_DECKS);
|
|
@@ -164,12 +183,25 @@ function resolveDeckArg(arg) {
|
|
|
164
183
|
return path.join(decksDir, arg);
|
|
165
184
|
}
|
|
166
185
|
|
|
167
|
-
// Best-effort human name of a materialized design
|
|
168
|
-
// the
|
|
169
|
-
//
|
|
170
|
-
//
|
|
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.
|
|
171
193
|
function readDesignName(bundleDir) {
|
|
172
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
|
+
}
|
|
173
205
|
try {
|
|
174
206
|
const md = fs.readFileSync(path.join(bundleDir, "design.md"), "utf8");
|
|
175
207
|
for (const raw of md.split("\n")) {
|
|
@@ -229,6 +261,15 @@ async function cmdBuild(rest) {
|
|
|
229
261
|
]);
|
|
230
262
|
}
|
|
231
263
|
|
|
264
|
+
async function cmdPlan(rest) {
|
|
265
|
+
const { deck, passthrough } = splitDeckArg(rest, "plan");
|
|
266
|
+
await runNode(path.join(BUILDERS_DIR, "plan.mjs"), [
|
|
267
|
+
"--deck",
|
|
268
|
+
resolveDeckArg(deck),
|
|
269
|
+
...passthrough,
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
272
|
+
|
|
232
273
|
async function cmdReview(rest) {
|
|
233
274
|
const { deck, passthrough } = splitDeckArg(rest, "review");
|
|
234
275
|
await runNode(path.join(BUILDERS_DIR, "review.mjs"), [
|
|
@@ -471,10 +512,19 @@ async function cmdSearch(rest) {
|
|
|
471
512
|
);
|
|
472
513
|
}
|
|
473
514
|
const mat = await bundle.materializeBundle(presId, { backendURL });
|
|
474
|
-
|
|
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
|
+
}
|
|
475
525
|
if (!pick) {
|
|
476
526
|
fail(
|
|
477
|
-
"This corporate design has no
|
|
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.",
|
|
478
528
|
);
|
|
479
529
|
}
|
|
480
530
|
engineId = pick.id;
|
|
@@ -700,6 +750,7 @@ async function cmdAddSlide(rest) {
|
|
|
700
750
|
let deck = null;
|
|
701
751
|
let at = null;
|
|
702
752
|
let to = null;
|
|
753
|
+
let from = null;
|
|
703
754
|
for (let i = 0; i < rest.length; i++) {
|
|
704
755
|
const a = rest[i];
|
|
705
756
|
if (a === "--search") {
|
|
@@ -707,6 +758,9 @@ async function cmdAddSlide(rest) {
|
|
|
707
758
|
const n = parseInt(next, 10);
|
|
708
759
|
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
709
760
|
searchId = n;
|
|
761
|
+
} else if (a === "--from") {
|
|
762
|
+
from = rest[++i];
|
|
763
|
+
if (!from) fail("--from expects a path to a .pptx file");
|
|
710
764
|
} else if (a === "--pick") {
|
|
711
765
|
picks = parsePicks(rest[++i]);
|
|
712
766
|
} else if (a === "--deck") {
|
|
@@ -728,8 +782,69 @@ async function cmdAddSlide(rest) {
|
|
|
728
782
|
fail(`unexpected argument: ${a}`);
|
|
729
783
|
}
|
|
730
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
|
+
|
|
731
846
|
if (searchId === null || !picks || !deck) {
|
|
732
|
-
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>]");
|
|
733
848
|
}
|
|
734
849
|
const deckDir = resolveDeckArg(deck);
|
|
735
850
|
const slidesDir = path.join(deckDir, "slides");
|
|
@@ -962,6 +1077,178 @@ async function cmdDownloadResult(rest) {
|
|
|
962
1077
|
if (ok === 0) process.exit(1);
|
|
963
1078
|
}
|
|
964
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
|
+
|
|
965
1252
|
async function cmdDownloadBundle(rest) {
|
|
966
1253
|
let to = null;
|
|
967
1254
|
let force = false;
|
|
@@ -1639,14 +1926,54 @@ async function cmdDoctor(rest) {
|
|
|
1639
1926
|
process.stdout.write(`Next action: ${status.nextAction}\n`);
|
|
1640
1927
|
}
|
|
1641
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
|
+
|
|
1642
1961
|
// Pull the current content of an open presentation. Content is window-bound, so
|
|
1643
1962
|
// pick exactly one window (positional presId or --to; pickPresentationEntry errors
|
|
1644
1963
|
// listing the windows if several are open and none was named). `--mode summary`
|
|
1645
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).
|
|
1646
1971
|
async function cmdRead(rest) {
|
|
1647
1972
|
let presId = null;
|
|
1648
1973
|
let mode = "raw";
|
|
1649
1974
|
let asJson = false;
|
|
1975
|
+
let toGiven = false;
|
|
1976
|
+
let fileArg = null; // the positional (a candidate local .pptx path)
|
|
1650
1977
|
for (let i = 0; i < rest.length; i++) {
|
|
1651
1978
|
const a = rest[i];
|
|
1652
1979
|
if (a === "--mode") {
|
|
@@ -1658,16 +1985,47 @@ async function cmdRead(rest) {
|
|
|
1658
1985
|
} else if (a === "--to") {
|
|
1659
1986
|
presId = rest[++i];
|
|
1660
1987
|
if (!presId) fail("--to expects a presId");
|
|
1988
|
+
toGiven = true;
|
|
1661
1989
|
} else if (a === "--json") {
|
|
1662
1990
|
asJson = true;
|
|
1663
1991
|
} else if (a.startsWith("-")) {
|
|
1664
1992
|
fail(`unknown flag: ${a}`);
|
|
1665
1993
|
} else if (!presId) {
|
|
1666
|
-
presId = a; // positional presId
|
|
1994
|
+
presId = a; // positional presId (or local .pptx path)
|
|
1995
|
+
fileArg = a;
|
|
1667
1996
|
} else {
|
|
1668
1997
|
fail(`unexpected argument: ${a}`);
|
|
1669
1998
|
}
|
|
1670
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
|
+
|
|
1671
2029
|
const client = await import(
|
|
1672
2030
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1673
2031
|
);
|
|
@@ -1681,32 +2039,7 @@ async function cmdRead(rest) {
|
|
|
1681
2039
|
if (!res?.ok) {
|
|
1682
2040
|
fail(`read failed: ${res?.error ?? "unknown error"}`);
|
|
1683
2041
|
}
|
|
1684
|
-
|
|
1685
|
-
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1686
|
-
return;
|
|
1687
|
-
}
|
|
1688
|
-
const slides = Array.isArray(res.slides) ? res.slides : [];
|
|
1689
|
-
if (slides.length === 0) {
|
|
1690
|
-
process.stderr.write("(presentation has no slides)\n");
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
if (res.mode === "summary") {
|
|
1694
|
-
for (const s of slides) {
|
|
1695
|
-
const label = s.title || "(untitled)";
|
|
1696
|
-
const sub = s.subtitle ? ` — ${s.subtitle}` : "";
|
|
1697
|
-
const layout = s.layout ? ` [${s.layout}]` : "";
|
|
1698
|
-
process.stdout.write(
|
|
1699
|
-
`${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
|
|
1700
|
-
);
|
|
1701
|
-
}
|
|
1702
|
-
} else {
|
|
1703
|
-
slides.forEach((s, i) => {
|
|
1704
|
-
if (i > 0) process.stdout.write("\n");
|
|
1705
|
-
const layout = s.layout ? ` (${s.layout})` : "";
|
|
1706
|
-
process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
|
|
1707
|
-
process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
2042
|
+
printReadResult(res, asJson);
|
|
1710
2043
|
}
|
|
1711
2044
|
|
|
1712
2045
|
async function loadBridgeModule(name) {
|
|
@@ -2208,7 +2541,22 @@ async function cmdUpdate(rest) {
|
|
|
2208
2541
|
: "slidemind@latest";
|
|
2209
2542
|
await ui.section("release", ref ? `pinned ${spec}` : "latest");
|
|
2210
2543
|
ui.step({ label: "install", value: spec, note: "global (npm)" });
|
|
2211
|
-
|
|
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
|
+
}
|
|
2212
2560
|
|
|
2213
2561
|
if (target !== "none") {
|
|
2214
2562
|
await spawnInherit("slidemind", ["init", "--for", target]);
|
|
@@ -2256,6 +2604,9 @@ async function main(rawArgv) {
|
|
|
2256
2604
|
case "build":
|
|
2257
2605
|
await cmdBuild(rest);
|
|
2258
2606
|
return 0;
|
|
2607
|
+
case "plan":
|
|
2608
|
+
await cmdPlan(rest);
|
|
2609
|
+
return 0;
|
|
2259
2610
|
case "review":
|
|
2260
2611
|
await cmdReview(rest);
|
|
2261
2612
|
return 0;
|
|
@@ -2283,6 +2634,9 @@ async function main(rawArgv) {
|
|
|
2283
2634
|
case "download-result":
|
|
2284
2635
|
await cmdDownloadResult(rest);
|
|
2285
2636
|
return 0;
|
|
2637
|
+
case "concepts":
|
|
2638
|
+
await cmdConceptImages(rest);
|
|
2639
|
+
return 0;
|
|
2286
2640
|
case "download-bundle":
|
|
2287
2641
|
await cmdDownloadBundle(rest);
|
|
2288
2642
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|