slidemind 0.3.13 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +86 -21
- package/README.md +14 -17
- package/bin/slidemind.mjs +65 -4969
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +113 -74
- package/skills/search/SKILL.md +25 -7
- package/src/bridge/build-and-push.mjs +108 -15
- package/src/bridge/bundle.mjs +69 -0
- package/src/bridge/capture.mjs +19 -0
- package/src/bridge/client/brands.mjs +622 -0
- package/src/bridge/client/engine-routing.mjs +237 -0
- package/src/bridge/client/engine-search.mjs +310 -0
- package/src/bridge/client/engine-slides.mjs +435 -0
- package/src/bridge/client/pane-ops.mjs +335 -0
- package/src/bridge/client/presentations.mjs +587 -0
- package/src/bridge/client/render.mjs +317 -0
- package/src/bridge/client/transport.mjs +695 -0
- package/src/bridge/client.mjs +80 -3452
- package/src/bridge/config.mjs +1 -1
- package/src/bridge/engine-download.mjs +15 -2
- package/src/builders/build.mjs +11 -804
- package/src/builders/draft-check.mjs +376 -0
- package/src/builders/lint.mjs +38 -2
- package/src/builders/preview.html +799 -0
- package/src/builders/review.mjs +23 -4
- package/src/builders/visual-families.mjs +6 -8
- package/src/cli/common.mjs +179 -0
- package/src/cli/concepts.mjs +310 -0
- package/src/cli/connection.mjs +147 -0
- package/src/cli/deck.mjs +608 -0
- package/src/cli/designs.mjs +113 -0
- package/src/cli/diagnose.mjs +141 -0
- package/src/cli/download-bundle.mjs +144 -0
- package/src/cli/engine-helpers.mjs +429 -0
- package/src/cli/engine-results.mjs +320 -0
- package/src/cli/help.mjs +201 -0
- package/src/cli/install.mjs +257 -0
- package/src/cli/read.mjs +120 -0
- package/src/cli/ready.mjs +805 -0
- package/src/cli/search.mjs +608 -0
- package/src/cli/session.mjs +177 -0
- package/src/cli/setup.mjs +282 -0
- package/src/cli/slide-reuse.mjs +334 -0
- package/src/cli/status.mjs +118 -0
- package/src/cli/workspace.mjs +302 -0
- package/src/installers/codex.mjs +1 -1
- package/src/update-check.mjs +231 -0
package/src/cli/deck.mjs
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { fail, requirePlanApproved, requireReviewApproved, resolveDeckArg, runNode, splitDeckArg, renderProgressPrinter, dataUriToPng, freshenDeckBundle, withUnboundHint, SRC_DIR, BUILDERS_DIR } from "./common.mjs";
|
|
5
|
+
|
|
6
|
+
export async function cmdPlan(rest) {
|
|
7
|
+
const { deck, passthrough } = splitDeckArg(rest, "plan");
|
|
8
|
+
await runNode(path.join(BUILDERS_DIR, "plan.mjs"), [
|
|
9
|
+
"--deck",
|
|
10
|
+
resolveDeckArg(deck),
|
|
11
|
+
...passthrough,
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function cmdReview(rest) {
|
|
16
|
+
const { deck, passthrough } = splitDeckArg(rest, "review");
|
|
17
|
+
const deckDir = resolveDeckArg(deck);
|
|
18
|
+
await requirePlanApproved(deckDir);
|
|
19
|
+
await freshenDeckBundle(deckDir);
|
|
20
|
+
await runNode(path.join(BUILDERS_DIR, "review.mjs"), ["--deck", deckDir, ...passthrough]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Generate one regular image via the paired add-in's image model and write it
|
|
24
|
+
// into the deck's assets/. Pane-free: routes by the deck's bound design
|
|
25
|
+
// (.bundle-ref.json), so it works with PowerPoint backgrounded or closed.
|
|
26
|
+
export async function cmdImagen(rest) {
|
|
27
|
+
let prompt = null;
|
|
28
|
+
let deck = null;
|
|
29
|
+
let outName = null;
|
|
30
|
+
let size = null;
|
|
31
|
+
let quality = null;
|
|
32
|
+
for (let i = 0; i < rest.length; i++) {
|
|
33
|
+
const a = rest[i];
|
|
34
|
+
if (a === "--deck") {
|
|
35
|
+
deck = rest[++i];
|
|
36
|
+
if (!deck) fail("--deck expects a deck");
|
|
37
|
+
} else if (a === "--out" || a === "-o") {
|
|
38
|
+
outName = rest[++i];
|
|
39
|
+
if (!outName) fail("--out expects a file name");
|
|
40
|
+
} else if (a === "--size") {
|
|
41
|
+
size = rest[++i];
|
|
42
|
+
if (!["auto", "1024x1024", "1536x1024", "1024x1536"].includes(size ?? "")) {
|
|
43
|
+
fail("--size expects auto|1024x1024|1536x1024|1024x1536");
|
|
44
|
+
}
|
|
45
|
+
} else if (a === "--quality") {
|
|
46
|
+
quality = rest[++i];
|
|
47
|
+
if (!["low", "medium", "high", "auto"].includes(quality ?? "")) {
|
|
48
|
+
fail("--quality expects low|medium|high|auto");
|
|
49
|
+
}
|
|
50
|
+
} else if (a.startsWith("-")) {
|
|
51
|
+
fail(`unknown flag: ${a}`);
|
|
52
|
+
} else if (prompt === null) {
|
|
53
|
+
prompt = a;
|
|
54
|
+
} else {
|
|
55
|
+
fail(`unexpected argument: ${a} (quote the prompt as one argument)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!prompt || !prompt.trim() || !deck) {
|
|
59
|
+
fail(
|
|
60
|
+
'usage: slidemind imagen "<prompt>" --deck <deck> [--out <name.png>] ' +
|
|
61
|
+
"[--size auto|1024x1024|1536x1024|1024x1536] [--quality low|medium|high|auto]",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const deckDir = resolveDeckArg(deck);
|
|
65
|
+
const bundleMod = await import(
|
|
66
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
67
|
+
);
|
|
68
|
+
const ref = bundleMod.readDeckBundleRef(deckDir);
|
|
69
|
+
if (!ref?.presFP) {
|
|
70
|
+
fail(
|
|
71
|
+
`deck ${path.basename(deckDir)} is not bound to a design yet — run ` +
|
|
72
|
+
`'slidemind ready ${deck}' first (imagen routes by the deck's bound design).`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const client = await import(
|
|
76
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
77
|
+
);
|
|
78
|
+
process.stdout.write(" generating image (this can take a minute)…\n");
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await client.generateImageForBrand(ref.presFP, {
|
|
82
|
+
prompt,
|
|
83
|
+
size,
|
|
84
|
+
quality,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err?.code === "imagen_route_unavailable") {
|
|
88
|
+
fail(
|
|
89
|
+
`${err.message}\n The paired add-in does not offer image generation yet — ` +
|
|
90
|
+
`it ships with the next add-in update; until then add pictures to assets/ by hand ` +
|
|
91
|
+
`or via 'slidemind download-result'.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const m = /^data:image\/(png|jpe?g);base64,(.+)$/.exec(result?.image ?? "");
|
|
97
|
+
if (!m) fail("backend returned no image data");
|
|
98
|
+
const ext = m[1] === "png" ? "png" : "jpeg";
|
|
99
|
+
const assetsDir = path.join(deckDir, "assets");
|
|
100
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
101
|
+
let file = outName;
|
|
102
|
+
if (!file) {
|
|
103
|
+
let n = 1;
|
|
104
|
+
while (fs.existsSync(path.join(assetsDir, `imagen-${n}.${ext}`))) n++;
|
|
105
|
+
file = `imagen-${n}.${ext}`;
|
|
106
|
+
}
|
|
107
|
+
const outPath = path.join(assetsDir, file);
|
|
108
|
+
fs.writeFileSync(outPath, Buffer.from(m[2], "base64"));
|
|
109
|
+
process.stdout.write(
|
|
110
|
+
`Wrote ${path.relative(process.cwd(), outPath)}\n` +
|
|
111
|
+
` Reference it from slide props as "assets/${file}" (e.g. summaryImage, illustration).\n`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function cmdInspectTemplateDeck(rest) {
|
|
116
|
+
const { deck, passthrough } = splitDeckArg(rest, "inspect-template-deck");
|
|
117
|
+
let packDir = null;
|
|
118
|
+
let json = false;
|
|
119
|
+
let allowUnrenderedFreeform = false;
|
|
120
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
121
|
+
const a = passthrough[i];
|
|
122
|
+
if (a === "--pack") {
|
|
123
|
+
packDir = passthrough[++i];
|
|
124
|
+
if (!packDir) fail("--pack expects a directory");
|
|
125
|
+
} else if (a === "--json") {
|
|
126
|
+
json = true;
|
|
127
|
+
} else if (a === "--allow-unrendered-freeform") {
|
|
128
|
+
allowUnrenderedFreeform = true;
|
|
129
|
+
} else if (a.startsWith("-")) {
|
|
130
|
+
fail(`unknown flag: ${a}`);
|
|
131
|
+
} else {
|
|
132
|
+
fail(`unexpected argument: ${a}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!packDir) fail("inspect-template-deck requires --pack <dir>");
|
|
136
|
+
|
|
137
|
+
const deckDir = resolveDeckArg(deck);
|
|
138
|
+
const resolveMod = await import(
|
|
139
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "resolve-skill.mjs")).href
|
|
140
|
+
);
|
|
141
|
+
const skill = resolveMod.resolveSkillForDeck(deckDir, {
|
|
142
|
+
packDir: path.resolve(packDir),
|
|
143
|
+
});
|
|
144
|
+
if (skill.mode !== "fill-template") {
|
|
145
|
+
fail(`pack ${skill.skillDir} is mode ${skill.mode}, not fill-template`);
|
|
146
|
+
}
|
|
147
|
+
const engine = await import(
|
|
148
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "engine.mjs")).href
|
|
149
|
+
);
|
|
150
|
+
const result = engine.inspectFillTemplateDeck({
|
|
151
|
+
deckDir,
|
|
152
|
+
skill,
|
|
153
|
+
allowUnrenderedFreeform,
|
|
154
|
+
});
|
|
155
|
+
const payload = {
|
|
156
|
+
version: 1,
|
|
157
|
+
pack: {
|
|
158
|
+
name: skill.skillName,
|
|
159
|
+
mode: skill.mode,
|
|
160
|
+
directory: skill.skillDir,
|
|
161
|
+
template: result.templateKey,
|
|
162
|
+
locale: result.locale,
|
|
163
|
+
},
|
|
164
|
+
slides: result.sequence,
|
|
165
|
+
warnings: result.warnings,
|
|
166
|
+
problems: result.problems,
|
|
167
|
+
};
|
|
168
|
+
if (json) {
|
|
169
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
170
|
+
} else {
|
|
171
|
+
for (const item of result.sequence) {
|
|
172
|
+
const suffix = item.kind === "master" ? ` → template ${item.no}` : "";
|
|
173
|
+
process.stdout.write(`${item.file}: ${item.kind}${suffix}\n`);
|
|
174
|
+
}
|
|
175
|
+
for (const warning of result.warnings) process.stderr.write(` warning: ${warning}\n`);
|
|
176
|
+
}
|
|
177
|
+
if (result.problems.length) {
|
|
178
|
+
fail(`inspection failed:\n - ${result.problems.join("\n - ")}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function cmdBuildPptx(rest) {
|
|
183
|
+
const { deck, passthrough } = splitDeckArg(rest, "build-pptx");
|
|
184
|
+
let to = null;
|
|
185
|
+
let outPath = null;
|
|
186
|
+
let packDir = null;
|
|
187
|
+
let force = false;
|
|
188
|
+
let asJson = false;
|
|
189
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
190
|
+
const a = passthrough[i];
|
|
191
|
+
if (a === "--force") {
|
|
192
|
+
force = true;
|
|
193
|
+
} else if (a === "--json") {
|
|
194
|
+
asJson = true;
|
|
195
|
+
} else if (a === "--to") {
|
|
196
|
+
to = passthrough[++i];
|
|
197
|
+
if (!to) fail("--to expects a presId");
|
|
198
|
+
} else if (a === "--pack") {
|
|
199
|
+
packDir = passthrough[++i];
|
|
200
|
+
if (!packDir) fail("--pack expects a directory");
|
|
201
|
+
} else if (a === "--out" || a === "-o") {
|
|
202
|
+
outPath = passthrough[++i];
|
|
203
|
+
if (!outPath) fail("--out expects a path");
|
|
204
|
+
} else if (a.startsWith("-")) {
|
|
205
|
+
fail(`unknown flag: ${a}`);
|
|
206
|
+
} else {
|
|
207
|
+
fail(`unexpected argument: ${a}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const deckDir = resolveDeckArg(deck);
|
|
211
|
+
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
212
|
+
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Template packs (mode: fill-template) build LOCALLY: the deck's slides are
|
|
216
|
+
// declarative fills over a template .pptx that ships in the pack, so no
|
|
217
|
+
// add-in render is involved and no PowerPoint host is needed. Compose decks
|
|
218
|
+
// take the bridge path below UNTOUCHED — the mode probe never throws, so a
|
|
219
|
+
// compose deck with an unmaterialized bundle still self-heals in the render.
|
|
220
|
+
const resolveMod = await import(
|
|
221
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "resolve-skill.mjs")).href
|
|
222
|
+
);
|
|
223
|
+
const resolveOpts = packDir ? { packDir: path.resolve(packDir) } : {};
|
|
224
|
+
const deckMode = resolveMod.resolveDeckMode(deckDir, resolveOpts);
|
|
225
|
+
if (packDir && deckMode.mode !== "fill-template") {
|
|
226
|
+
fail(`explicit --pack ${path.resolve(packDir)} is mode ${deckMode.mode}, not fill-template`);
|
|
227
|
+
}
|
|
228
|
+
// Template packs have no HTML preview to review and carry their own plan
|
|
229
|
+
// sign-off; compose decks ship only from an approved, unchanged preview.
|
|
230
|
+
if (deckMode.mode !== "fill-template") {
|
|
231
|
+
await requireReviewApproved(deckDir);
|
|
232
|
+
await freshenDeckBundle(deckDir);
|
|
233
|
+
}
|
|
234
|
+
if (deckMode.mode === "fill-template") {
|
|
235
|
+
const skill = resolveMod.resolveSkillForDeck(deckDir, resolveOpts);
|
|
236
|
+
if (to) {
|
|
237
|
+
fail(
|
|
238
|
+
"--to targets a PowerPoint window, but fill-template decks build " +
|
|
239
|
+
"locally from the pack's template — drop --to (use 'slidemind push' " +
|
|
240
|
+
"to get slides into a window).",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
// In-deck freeform slides (create-slides JSX next to the master-backed
|
|
244
|
+
// ones) render to sibling NN.pptx artifacts first — the only step that
|
|
245
|
+
// talks to the add-in; unchanged freeform slides keep the build offline.
|
|
246
|
+
const freeform = await import(
|
|
247
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "freeform.mjs")).href
|
|
248
|
+
);
|
|
249
|
+
const ff = await freeform.ensureFreeformRenders({
|
|
250
|
+
deckDir,
|
|
251
|
+
locale: skill.locale ?? "en",
|
|
252
|
+
srcDir: SRC_DIR,
|
|
253
|
+
// Both are the PACK's contract, read from its skill.json — this CLI
|
|
254
|
+
// knows no layout names and no template-fixed titles of its own.
|
|
255
|
+
components: {
|
|
256
|
+
...(skill.manifest?.components ?? {}),
|
|
257
|
+
skillDir: skill.skillDir,
|
|
258
|
+
},
|
|
259
|
+
policy: skill.manifest?.policy ?? {},
|
|
260
|
+
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
261
|
+
});
|
|
262
|
+
for (const w of ff.warnings) process.stderr.write(` warning: ${w}\n`);
|
|
263
|
+
|
|
264
|
+
const engine = await import(
|
|
265
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "engine.mjs")).href
|
|
266
|
+
);
|
|
267
|
+
const result = await engine.buildFillTemplateDeck({
|
|
268
|
+
deckDir,
|
|
269
|
+
skill,
|
|
270
|
+
outPath,
|
|
271
|
+
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
272
|
+
});
|
|
273
|
+
if (asJson) {
|
|
274
|
+
process.stdout.write(`${JSON.stringify({ outPath: result.outPath, slideCount: result.slideCount, template: skill.templateKey, windowless: true, warnings: result.warnings ?? [] }, null, 2)}\n`);
|
|
275
|
+
} else {
|
|
276
|
+
process.stdout.write(
|
|
277
|
+
`Wrote ${result.outPath} (${result.slideCount} slides) from template ` +
|
|
278
|
+
`'${skill.templateKey}' — host-free\n`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
for (const w of result.warnings ?? []) {
|
|
282
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// The .pptx is rendered server-side by the add-in (one exporter, one truth).
|
|
288
|
+
// A bound deck renders pane-free over the brand routes — no PowerPoint
|
|
289
|
+
// window needed; unbound decks (or --to) resolve through the pane registry.
|
|
290
|
+
const mod = await import(
|
|
291
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
292
|
+
);
|
|
293
|
+
const result = await mod
|
|
294
|
+
.buildPptxFile({
|
|
295
|
+
deckDir,
|
|
296
|
+
presentationId: to,
|
|
297
|
+
outPath,
|
|
298
|
+
force,
|
|
299
|
+
onProgress: renderProgressPrinter(),
|
|
300
|
+
})
|
|
301
|
+
.catch((err) => fail(withUnboundHint(deckDir, err)));
|
|
302
|
+
if (asJson) {
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
`${JSON.stringify({ outPath: result.outPath, slideCount: result.slideCount ?? null, presId: result.presId ?? null, windowless: Boolean(result.windowless), warnings: result.warnings ?? [], skippedSlides: result.skippedSlides ?? [] }, null, 2)}\n`,
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
process.stdout.write(
|
|
308
|
+
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in` +
|
|
309
|
+
`${result.windowless ? " (no PowerPoint window needed)" : ""}\n`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
for (const w of result.warnings ?? []) {
|
|
313
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
314
|
+
}
|
|
315
|
+
for (const s of result.skippedSlides ?? []) {
|
|
316
|
+
process.stderr.write(` skipped ${s.stem}: ${s.reason}\n`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function cmdViewDraft(rest) {
|
|
321
|
+
const { deck, passthrough } = splitDeckArg(rest, "view-draft");
|
|
322
|
+
let to = null;
|
|
323
|
+
let outDir = null;
|
|
324
|
+
let open = false;
|
|
325
|
+
let onlySpec = null;
|
|
326
|
+
// The server's export gate runs by default: the 422 it would raise at
|
|
327
|
+
// build-pptx (after the user approved) surfaces here instead. "auto" skips
|
|
328
|
+
// with a note when the backend is unreachable; --translate insists, and
|
|
329
|
+
// --no-translate keeps the check purely local.
|
|
330
|
+
let translate = "auto";
|
|
331
|
+
let fix = false;
|
|
332
|
+
let lintOnly = false;
|
|
333
|
+
let asJson = false;
|
|
334
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
335
|
+
const a = passthrough[i];
|
|
336
|
+
if (a === "--fix") {
|
|
337
|
+
fix = true;
|
|
338
|
+
} else if (a === "--lint-only") {
|
|
339
|
+
lintOnly = true;
|
|
340
|
+
} else if (a === "--json") {
|
|
341
|
+
asJson = true;
|
|
342
|
+
} else if (a === "--no-translate") {
|
|
343
|
+
translate = false;
|
|
344
|
+
} else if (a === "--to") {
|
|
345
|
+
to = passthrough[++i];
|
|
346
|
+
if (!to) fail("--to expects a presId");
|
|
347
|
+
} else if (a === "--out" || a === "-o") {
|
|
348
|
+
outDir = passthrough[++i];
|
|
349
|
+
if (!outDir) fail("--out expects a directory");
|
|
350
|
+
} else if (a === "--open") {
|
|
351
|
+
open = true;
|
|
352
|
+
} else if (a === "--translate") {
|
|
353
|
+
translate = true;
|
|
354
|
+
} else if (a === "--only") {
|
|
355
|
+
onlySpec = passthrough[++i];
|
|
356
|
+
if (!onlySpec) fail("--only expects a slide file or number (comma-separated for several)");
|
|
357
|
+
} else if (a.startsWith("-")) {
|
|
358
|
+
fail(`unknown flag: ${a}`);
|
|
359
|
+
} else {
|
|
360
|
+
fail(`unexpected argument: ${a}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const deckDir = resolveDeckArg(deck);
|
|
364
|
+
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
365
|
+
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
366
|
+
}
|
|
367
|
+
// --only: a per-slide builder re-checks its own file. Accepts file names
|
|
368
|
+
// (`03-loop.jsx`) or slide numbers (`3`, `03`), resolved by numeric prefix.
|
|
369
|
+
let only = null;
|
|
370
|
+
if (onlySpec) {
|
|
371
|
+
const files = fs.readdirSync(path.join(deckDir, "slides")).filter((f) => f.endsWith(".jsx"));
|
|
372
|
+
only = onlySpec.split(",").map((t) => t.trim()).filter(Boolean).map((t) => {
|
|
373
|
+
if (files.includes(t)) return t;
|
|
374
|
+
if (/^\d{1,3}$/.test(t)) {
|
|
375
|
+
const hit = files.find((f) => Number(/^(\d{1,3})[-_.]/.exec(f)?.[1]) === Number(t));
|
|
376
|
+
if (hit) return hit;
|
|
377
|
+
}
|
|
378
|
+
fail(`--only: no slide file matches '${t}' under ${path.join(deckDir, "slides")}`);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
await requirePlanApproved(deckDir);
|
|
382
|
+
await freshenDeckBundle(deckDir);
|
|
383
|
+
// The HTML build runs FIRST — it is what lints every slide against the
|
|
384
|
+
// bundle's capability table and copies the bundle's corporate-design.css +
|
|
385
|
+
// assets into the deck, which the type-ramp check below reads. (This is the
|
|
386
|
+
// work the retired `slidemind build` command used to do in a separate call;
|
|
387
|
+
// `--fix` is its autofix, folded in with it.)
|
|
388
|
+
const { buildDeck } = await import(
|
|
389
|
+
pathToFileURL(path.join(BUILDERS_DIR, "build.mjs")).href
|
|
390
|
+
);
|
|
391
|
+
if (fix) {
|
|
392
|
+
const { fixDeckSlides } = await import(
|
|
393
|
+
pathToFileURL(path.join(BUILDERS_DIR, "lint-fix.mjs")).href
|
|
394
|
+
);
|
|
395
|
+
const fixed = fixDeckSlides(deckDir);
|
|
396
|
+
if (fixed.fixes > 0) {
|
|
397
|
+
process.stdout.write(
|
|
398
|
+
`Autofixed ${fixed.fixes} lint issue(s) in ${fixed.files} slide file(s) — re-read any file before patching it.\n`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
await buildDeck(deckDir);
|
|
403
|
+
if (lintOnly) {
|
|
404
|
+
if (asJson) process.stdout.write(`${JSON.stringify({ deck: path.basename(deckDir), lint: "clean" })}\n`);
|
|
405
|
+
else process.stdout.write("Lint clean (no render — drop --lint-only for the draft previews).\n");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// Draft previews are produced locally by the add-in's shipped capture engine
|
|
409
|
+
// (no push, no .pptx) — the same module that hosts the render+push path.
|
|
410
|
+
const mod = await import(
|
|
411
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
412
|
+
);
|
|
413
|
+
// The capture engine lints every slide again and prints the same lines the
|
|
414
|
+
// build just did; mute that second copy (its warnings still ride the result).
|
|
415
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
416
|
+
process.stderr.write = (chunk, ...args) =>
|
|
417
|
+
/^\s*\[lint:(warn|error|note)\]/.test(String(chunk)) ? true : stderrWrite(chunk, ...args);
|
|
418
|
+
let result;
|
|
419
|
+
try {
|
|
420
|
+
result = await mod.viewDraft(deckDir, {
|
|
421
|
+
presentationId: to,
|
|
422
|
+
onProgress: renderProgressPrinter(),
|
|
423
|
+
only,
|
|
424
|
+
// The server's export gate (slot-fit, diagram geometry, connector
|
|
425
|
+
// endpoints) on the captured slides, verdict only, no .pptx, no review
|
|
426
|
+
// gate — so a slide is proven buildable before hand-over.
|
|
427
|
+
translate,
|
|
428
|
+
});
|
|
429
|
+
} catch (err) {
|
|
430
|
+
process.stderr.write = stderrWrite;
|
|
431
|
+
fail(withUnboundHint(deckDir, err));
|
|
432
|
+
}
|
|
433
|
+
process.stderr.write = stderrWrite;
|
|
434
|
+
// Everything below prints through `out` so --json can hand back one payload
|
|
435
|
+
// instead of the human lines (the lint lines already went to stderr).
|
|
436
|
+
const out = asJson ? () => {} : (text) => process.stdout.write(text);
|
|
437
|
+
const slidesOut = [];
|
|
438
|
+
|
|
439
|
+
const previewDir = outDir
|
|
440
|
+
? path.resolve(process.cwd(), outDir)
|
|
441
|
+
: path.join(deckDir, ".draft-preview");
|
|
442
|
+
fs.mkdirSync(previewDir, { recursive: true });
|
|
443
|
+
|
|
444
|
+
// The measured half of the draft check — the same mechanics the add-in's
|
|
445
|
+
// see_draft runs (fill bands, inventory rows) so a low-effort agent reads
|
|
446
|
+
// defects as text instead of judging pixels. Images stay CLEAN: no overlay
|
|
447
|
+
// on the render and none on the concept — a magenta grid dominated both
|
|
448
|
+
// and hid whether the slide was actually built well; the content area is
|
|
449
|
+
// enforced by the lint and the slot-fit check, not by eye.
|
|
450
|
+
const check = await import(
|
|
451
|
+
pathToFileURL(path.join(BUILDERS_DIR, "draft-check.mjs")).href
|
|
452
|
+
);
|
|
453
|
+
const conceptsDir = path.join(deckDir, "concepts");
|
|
454
|
+
const conceptFor = (fileName) => {
|
|
455
|
+
const m = /^(\d{1,3})[-_.]/.exec(fileName);
|
|
456
|
+
if (!m) return null;
|
|
457
|
+
const file = path.join(conceptsDir, `concept-slide-${String(Number(m[1])).padStart(2, "0")}.png`);
|
|
458
|
+
return fs.existsSync(file) ? file : null;
|
|
459
|
+
};
|
|
460
|
+
const inventory = check.inventoryWarnings(
|
|
461
|
+
deckDir,
|
|
462
|
+
(result.slides ?? []).map((s) => s.fileName).filter((f) => f.endsWith(".jsx")),
|
|
463
|
+
);
|
|
464
|
+
const deckLevel = inventory.filter((l) => l.includes("] assets/"));
|
|
465
|
+
// The measured rules come from the add-in's capture engine in the bundle —
|
|
466
|
+
// the same fill / type-scale / overlap checks see_draft runs in-app.
|
|
467
|
+
const { loadDraftChecks } = await import(
|
|
468
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "capture.mjs")).href
|
|
469
|
+
);
|
|
470
|
+
const { resolveBundleDirForDeck } = await import(
|
|
471
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
472
|
+
);
|
|
473
|
+
const rules = await loadDraftChecks(resolveBundleDirForDeck(deckDir));
|
|
474
|
+
if (!rules) {
|
|
475
|
+
fail(
|
|
476
|
+
"the design bundle's capture engine exports no draft checks — the connected add-in predates them. Update the add-in deployment, then run `slidemind download-bundle <deck>` and retry.",
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
// Type ramp from the deck's own copy of the bundle css (build copies it in).
|
|
480
|
+
let typeTokens = null;
|
|
481
|
+
try {
|
|
482
|
+
typeTokens = rules.typeScaleTokens(
|
|
483
|
+
fs.readFileSync(path.join(deckDir, "corporate-design.css"), "utf8"),
|
|
484
|
+
);
|
|
485
|
+
} catch {
|
|
486
|
+
typeTokens = null;
|
|
487
|
+
}
|
|
488
|
+
const measuredLines = (stem, render) =>
|
|
489
|
+
[
|
|
490
|
+
...rules.fill([{ stem, render }]),
|
|
491
|
+
...rules.typeScale([{ stem, render }], typeTokens),
|
|
492
|
+
...rules.overlap([{ stem, render }]),
|
|
493
|
+
].map((l) => l.trim());
|
|
494
|
+
|
|
495
|
+
const written = [];
|
|
496
|
+
const record = [];
|
|
497
|
+
let measured = 0;
|
|
498
|
+
for (const slide of result.slides ?? []) {
|
|
499
|
+
const png = slide.png ?? (slide.dataUri ? dataUriToPng(slide.dataUri) : null);
|
|
500
|
+
if (png) {
|
|
501
|
+
const stem = slide.fileName.replace(/\.[^.]+$/, "");
|
|
502
|
+
const file = path.join(previewDir, `${stem}.png`);
|
|
503
|
+
fs.writeFileSync(file, png);
|
|
504
|
+
written.push(file);
|
|
505
|
+
const flag = slide.status === "degraded" ? " (DEGRADED)" : "";
|
|
506
|
+
out(` ${slide.fileName}${flag} → ${file}\n`);
|
|
507
|
+
if (slide.note) process.stderr.write(` note: ${slide.note}\n`);
|
|
508
|
+
const concept = conceptFor(slide.fileName);
|
|
509
|
+
const defects =
|
|
510
|
+
slide.status === "ok"
|
|
511
|
+
? [
|
|
512
|
+
...measuredLines(stem, slide.render),
|
|
513
|
+
...inventory.filter((l) => l.includes(` slides/${slide.fileName} `)),
|
|
514
|
+
...(slide.translateErrors ?? []),
|
|
515
|
+
]
|
|
516
|
+
: [];
|
|
517
|
+
if (concept) {
|
|
518
|
+
out(
|
|
519
|
+
` concept: ${concept} — compare against the render above: every organism present at the same relative position and share, every connector present, no region emptier than in the concept.\n`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
for (const line of defects) out(` ${line}\n`);
|
|
523
|
+
measured += defects.length;
|
|
524
|
+
slidesOut.push({ file: slide.fileName, status: slide.status, png: file, concept, defects, note: slide.note ?? null });
|
|
525
|
+
const jsxFile = path.join(deckDir, "slides", slide.fileName);
|
|
526
|
+
if (slide.status === "ok" && fs.existsSync(jsxFile)) {
|
|
527
|
+
record.push({ fileName: slide.fileName, hash: check.hashFile(jsxFile), defects });
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
out(` ${slide.fileName} — ${slide.note ?? slide.status}\n`);
|
|
531
|
+
slidesOut.push({ file: slide.fileName, status: slide.status, png: null, concept: null, defects: [], note: slide.note ?? null });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const line of deckLevel) out(` ${line}\n`);
|
|
535
|
+
measured += deckLevel.length;
|
|
536
|
+
const translateRan = translate && result.translateRan !== false;
|
|
537
|
+
if (translateRan) {
|
|
538
|
+
const failed = (result.slides ?? []).filter((s) => s.translateErrors?.length).length;
|
|
539
|
+
out(
|
|
540
|
+
failed
|
|
541
|
+
? ` TRANSLATE GATE: ${failed} slide${failed === 1 ? "" : "s"} refused by the export gate (lines above) — these are the errors build-pptx would stop on.\n`
|
|
542
|
+
: " TRANSLATE GATE: every rendered slide passed the export gate (slot-fit, diagram geometry, connector endpoints).\n",
|
|
543
|
+
);
|
|
544
|
+
for (const w of result.translateWarnings ?? []) process.stderr.write(` note: ${w}\n`);
|
|
545
|
+
}
|
|
546
|
+
const verdict = check.verdictBlocker(deckDir);
|
|
547
|
+
if (verdict) {
|
|
548
|
+
out(
|
|
549
|
+
` self-check: ${verdict.replace(/\n\s+/g, "; ")}\n` +
|
|
550
|
+
" (under each slide in asset-plan.md: `draft-check:` then `- [x] <organism>` per organism found in the render, `- [ ] <organism> — <what differs>` otherwise; `review` refuses until every organism is ticked [x])\n",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (record.length > 0 && !outDir) {
|
|
554
|
+
// Deck-level asset lines are recorded on the first slide so they stand
|
|
555
|
+
// until that slide (or the asset plan) changes.
|
|
556
|
+
if (deckLevel.length > 0) record[0].defects.push(...deckLevel);
|
|
557
|
+
// A partial run (--only) updates its slides and keeps the others' record.
|
|
558
|
+
const prior = only ? check.loadDraftCheck(deckDir)?.slides ?? {} : {};
|
|
559
|
+
const rendered = new Set(record.map((r) => r.fileName));
|
|
560
|
+
const kept = Object.entries(prior)
|
|
561
|
+
.filter(([f]) => !rendered.has(f))
|
|
562
|
+
.map(([fileName, e]) => ({ fileName, hash: e.hash, defects: e.defects ?? [] }));
|
|
563
|
+
check.saveDraftCheck(deckDir, [...record, ...kept]);
|
|
564
|
+
}
|
|
565
|
+
if (written.length === 0) {
|
|
566
|
+
out(
|
|
567
|
+
"No draft preview images were produced (no freeform slides to capture).\n",
|
|
568
|
+
);
|
|
569
|
+
} else if (measured > 0) {
|
|
570
|
+
out(
|
|
571
|
+
`DRAFT CHECK: ${measured} measured defect${measured === 1 ? "" : "s"} (above) — fix them in the JSX and re-run ` +
|
|
572
|
+
"`slidemind view-draft`; `slidemind review` does not open while they stand. " +
|
|
573
|
+
`Previews in ${previewDir} — NOT pushed.\n`,
|
|
574
|
+
);
|
|
575
|
+
} else {
|
|
576
|
+
out(
|
|
577
|
+
`DRAFT CHECK: no measured defects. Wrote ${written.length} draft preview${written.length === 1 ? "" : "s"} to ${previewDir} — NOT pushed. ` +
|
|
578
|
+
"Now diff each preview against its concept; fix anything visibly wrong in the JSX, then re-run, or 'slidemind review' / 'push' when sound.\n",
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
// The capture lints again on its own; those lines already printed above.
|
|
582
|
+
const warnings = (result.warnings ?? []).filter((w) => !/\[lint:(warn|error|note)\]/.test(w));
|
|
583
|
+
for (const w of warnings) process.stderr.write(` warning: ${w}\n`);
|
|
584
|
+
if (asJson) {
|
|
585
|
+
process.stdout.write(
|
|
586
|
+
`${JSON.stringify(
|
|
587
|
+
{
|
|
588
|
+
deck: path.basename(deckDir),
|
|
589
|
+
previewDir,
|
|
590
|
+
slides: slidesOut,
|
|
591
|
+
measuredDefects: measured,
|
|
592
|
+
translateGate: translateRan ? (slidesOut.some((s) => s.defects.some((d) => /:error\]/.test(d))) ? "failed" : "passed") : "skipped",
|
|
593
|
+
selfCheck: verdict ? "incomplete" : "ok",
|
|
594
|
+
reviewCanOpen: measured === 0 && !verdict,
|
|
595
|
+
warnings,
|
|
596
|
+
},
|
|
597
|
+
null,
|
|
598
|
+
2,
|
|
599
|
+
)}\n`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (open && written.length > 0) {
|
|
603
|
+
const { openInBrowser } = await import(
|
|
604
|
+
pathToFileURL(path.join(BUILDERS_DIR, "build.mjs")).href
|
|
605
|
+
);
|
|
606
|
+
for (const file of written) openInBrowser(file);
|
|
607
|
+
}
|
|
608
|
+
}
|