slidemind 0.3.17 → 0.3.19
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/package.json +1 -1
- package/src/bridge/build-and-push.mjs +91 -37
- package/src/bridge/capture.mjs +11 -0
- package/src/builders/build.mjs +23 -23
- package/src/cli/deck.mjs +67 -6
- package/src/cli/help.mjs +5 -0
- package/src/lib/adopt.mjs +30 -24
- package/src/lib/conversions.mjs +108 -0
- package/src/lib/jsx-insert.mjs +115 -0
- package/src/lib/lint-converted.mjs +433 -0
- package/src/lib/text-fit.mjs +94 -0
- package/src/pptx/app-props.mjs +0 -4
- package/src/pptx/slide-swap.mjs +0 -3
- package/src/pptx/image-extensions.mjs +0 -190
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Connect Claude Code to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design, from the terminal.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "statworx",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Connect Codex to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design. Requires the slidemind CLI (npm install -g slidemind); the skills explain the install when it is missing.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "statworx",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
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": {
|
|
@@ -37,10 +37,11 @@ import {
|
|
|
37
37
|
writeDeckBundleRef,
|
|
38
38
|
} from "./bundle.mjs";
|
|
39
39
|
import { bundleHasCaptureEngine, captureDeckLocally } from "./capture.mjs";
|
|
40
|
+
import { lintConvertedOrThrow } from "../lib/lint-converted.mjs";
|
|
40
41
|
import { slideOrderWarning, sortSlideFilenames } from "../lib/slide-order.mjs";
|
|
41
42
|
import {
|
|
42
43
|
applyConversionGates,
|
|
43
|
-
|
|
44
|
+
conversionIntegrityErrors,
|
|
44
45
|
loadConversions,
|
|
45
46
|
rehomeConversions,
|
|
46
47
|
} from "../lib/conversions.mjs";
|
|
@@ -274,12 +275,25 @@ export async function renderDeckToBase64(
|
|
|
274
275
|
// server splices them back from. Keyed by slide file name, like `slides`.
|
|
275
276
|
const conversions = loadConversions(deckDir);
|
|
276
277
|
const hasConversions = Object.keys(conversions).length > 0;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
// A record that points at a package file that is gone, or still names the
|
|
279
|
+
// file it was renamed away from, used to be a note AFTER the render — by
|
|
280
|
+
// which time the slide had already come back on a fallback layout. It is a
|
|
281
|
+
// hard stop: the render request would carry a lie about the source.
|
|
282
|
+
const integrity = conversionIntegrityErrors(deckDir, conversions);
|
|
283
|
+
if (integrity.length > 0) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`conversion records are out of sync with the deck:\n ${integrity.join("\n ")}`,
|
|
281
286
|
);
|
|
282
287
|
}
|
|
288
|
+
// The export gate for the converted dialect. `view-draft` prints these as
|
|
289
|
+
// warnings so a half-authored deck still previews; here — the last step
|
|
290
|
+
// before a .pptx exists — a surviving master prompt, copy in a field frame
|
|
291
|
+
// or text spilling a fixed box stops the deck from shipping.
|
|
292
|
+
if (hasConversions) {
|
|
293
|
+
lintConvertedOrThrow({
|
|
294
|
+
slideFiles: Object.keys(conversions).map((f) => path.join(deckDir, "slides", f)),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
283
297
|
|
|
284
298
|
const bundleDir = resolveBundleDirForDeck(deckDir);
|
|
285
299
|
// Local capture only applies to authored JSX slides. A native-only deck (just
|
|
@@ -544,19 +558,16 @@ export async function viewDraft(
|
|
|
544
558
|
}
|
|
545
559
|
const warnings = [];
|
|
546
560
|
// Same identity reconcile as the render path — a reorder renames every file,
|
|
547
|
-
// and a stale sidecar here would mislabel renamed converted slides
|
|
548
|
-
// would then choke on their unregistered ConvertedHost layout).
|
|
561
|
+
// and a stale sidecar here would mislabel renamed converted slides.
|
|
549
562
|
warnings.push(...rehomeConversions(deckDir, reconcileSlideGuids(deckDir)));
|
|
550
563
|
// Converted slides (`slidemind adopt`) invoke the synthetic ConvertedHost
|
|
551
|
-
// layout, which no materialized bundle carries —
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
564
|
+
// layout, which no materialized bundle carries — buildCaptureInput feeds the
|
|
565
|
+
// engine a stand-in host per source canvas so they preview here like any
|
|
566
|
+
// other slide. 34 converted slides were once authored blind because this
|
|
567
|
+
// refused them.
|
|
555
568
|
const conversions = loadConversions(deckDir, { includeSource: false });
|
|
556
|
-
const convertedNote =
|
|
557
|
-
|
|
558
|
-
"bundle); use `slidemind build-pptx <deck>` to look at it";
|
|
559
|
-
const hasJsx = all.some((f) => f.endsWith(".jsx") && !conversions[f]);
|
|
569
|
+
const convertedNote = "converted slide — geometry authoritative on the server render";
|
|
570
|
+
const hasJsx = all.some((f) => f.endsWith(".jsx"));
|
|
560
571
|
|
|
561
572
|
// Resolve the materialized bundle. Local capture needs the bundle's capture
|
|
562
573
|
// engine; once it's cached this whole call is offline. Only reach for a host
|
|
@@ -593,19 +604,14 @@ export async function viewDraft(
|
|
|
593
604
|
bundleDir = resolveBundleDirForDeck(deckDir);
|
|
594
605
|
}
|
|
595
606
|
|
|
596
|
-
// Nothing left to capture — a native-only deck (just reused .pptx)
|
|
597
|
-
// converted one, or both.
|
|
607
|
+
// Nothing left to capture — a native-only deck (just reused .pptx).
|
|
598
608
|
if (!hasJsx) {
|
|
599
609
|
return {
|
|
600
|
-
slides: all.map((fileName) =>
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
status: "native",
|
|
606
|
-
note: "reused native slide — not authored JSX, no draft preview rendered",
|
|
607
|
-
},
|
|
608
|
-
),
|
|
610
|
+
slides: all.map((fileName) => ({
|
|
611
|
+
fileName,
|
|
612
|
+
status: "native",
|
|
613
|
+
note: "reused native slide — not authored JSX, no draft preview rendered",
|
|
614
|
+
})),
|
|
609
615
|
warnings,
|
|
610
616
|
};
|
|
611
617
|
}
|
|
@@ -621,16 +627,13 @@ export async function viewDraft(
|
|
|
621
627
|
|
|
622
628
|
let captured;
|
|
623
629
|
try {
|
|
624
|
-
|
|
625
|
-
// as an unregistered layout and the noise would ride the translate gate.
|
|
626
|
-
const capturable = all.filter((f) => f.endsWith(".jsx") && !conversions[f]);
|
|
630
|
+
const capturable = all.filter((f) => f.endsWith(".jsx"));
|
|
627
631
|
captured = await captureDeckLocally({ deckDir, bundleDir, onProgress, only: capturable });
|
|
628
632
|
} catch (err) {
|
|
629
633
|
throw new Error(
|
|
630
634
|
`local draft capture failed (${err instanceof Error ? err.message : String(err)}). ` +
|
|
631
635
|
"It runs a local headless Chrome; if Chrome can't launch here, " +
|
|
632
|
-
"`slidemind review <deck>` still builds and serves the HTML preview without it."
|
|
633
|
-
(Object.keys(conversions).length > 0 ? ` ${convertedNote}.` : ""),
|
|
636
|
+
"`slidemind review <deck>` still builds and serves the HTML preview without it.",
|
|
634
637
|
);
|
|
635
638
|
}
|
|
636
639
|
|
|
@@ -643,10 +646,9 @@ export async function viewDraft(
|
|
|
643
646
|
note: "reused native slide — not authored JSX, no draft preview rendered",
|
|
644
647
|
};
|
|
645
648
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return mapCapturedSlide(fileName, byFile.get(fileName), warnings);
|
|
649
|
+
const slide = mapCapturedSlide(fileName, byFile.get(fileName), warnings);
|
|
650
|
+
if (conversions[fileName] && !slide.note) slide.note = convertedNote;
|
|
651
|
+
return slide;
|
|
650
652
|
});
|
|
651
653
|
|
|
652
654
|
// --translate: run the captured slides through the server's translate gate
|
|
@@ -658,10 +660,18 @@ export async function viewDraft(
|
|
|
658
660
|
// `translate: "auto"` (the default) runs the gate when it can and reports
|
|
659
661
|
// why not otherwise — an unbound deck or a dead backend must not turn a
|
|
660
662
|
// host-free draft check into a failure; `true` (--translate) insists.
|
|
663
|
+
// Converted slides are captured for the PICTURE only. Their export gate is
|
|
664
|
+
// the server render with their conversion record, and the stand-in host they
|
|
665
|
+
// preview on names no brand layout — sending them through /translate would
|
|
666
|
+
// judge them against a layout contract the CLI invented.
|
|
667
|
+
const gateDeck = {
|
|
668
|
+
...captured,
|
|
669
|
+
slides: (captured?.slides ?? []).filter((s) => !conversions[s.fileName]),
|
|
670
|
+
};
|
|
661
671
|
let translateErrors = null;
|
|
662
672
|
let translateWarnings = [];
|
|
663
673
|
let translateRan = false;
|
|
664
|
-
if (translate &&
|
|
674
|
+
if (translate && gateDeck.slides.length) {
|
|
665
675
|
const skip = (why) => {
|
|
666
676
|
if (translate !== "auto") throw new Error(why);
|
|
667
677
|
warnings.push(`export gate skipped: ${why}`);
|
|
@@ -684,7 +694,7 @@ export async function viewDraft(
|
|
|
684
694
|
brandId: ref.presFP,
|
|
685
695
|
backendURL: mat.backendURL,
|
|
686
696
|
deckName: path.basename(deckDir),
|
|
687
|
-
captured,
|
|
697
|
+
captured: gateDeck,
|
|
688
698
|
etag: mat.etag,
|
|
689
699
|
});
|
|
690
700
|
translateErrors = new Map();
|
|
@@ -1054,6 +1064,48 @@ export async function buildAndPush({
|
|
|
1054
1064
|
};
|
|
1055
1065
|
}
|
|
1056
1066
|
|
|
1067
|
+
// After a successful build-pptx: per-slide PNGs of what was just shipped, into
|
|
1068
|
+
// `<deck>/.build-preview/NN-name.png`. The .pptx itself is opaque here, so
|
|
1069
|
+
// without this the agent hands over a deck it has never seen. Best-effort — a
|
|
1070
|
+
// missing engine or a Chrome that won't launch returns a note, never a failure:
|
|
1071
|
+
// the .pptx is already written.
|
|
1072
|
+
export async function writeBuildPreview(deckDir, onProgress = null) {
|
|
1073
|
+
const bundleDir = resolveBundleDirForDeck(deckDir);
|
|
1074
|
+
if (!bundleHasCaptureEngine(bundleDir)) {
|
|
1075
|
+
return {
|
|
1076
|
+
dir: null,
|
|
1077
|
+
files: [],
|
|
1078
|
+
note:
|
|
1079
|
+
"visual check skipped — the deck's bundle carries no capture engine " +
|
|
1080
|
+
`(run 'slidemind download-bundle ${path.basename(deckDir)}')`,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
let captured;
|
|
1084
|
+
try {
|
|
1085
|
+
captured = await captureDeckLocally({ deckDir, bundleDir, onProgress });
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
return {
|
|
1088
|
+
dir: null,
|
|
1089
|
+
files: [],
|
|
1090
|
+
note: `visual check skipped — local capture failed (${err instanceof Error ? err.message : String(err)})`,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
const dir = path.join(deckDir, ".build-preview");
|
|
1094
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1095
|
+
const files = [];
|
|
1096
|
+
for (const slide of captured?.slides ?? []) {
|
|
1097
|
+
const b64 =
|
|
1098
|
+
/^data:image\/png;base64,(.+)$/.exec(slide.previewDataUrl ?? "")?.[1] ??
|
|
1099
|
+
slide.render?.fallbackPngBase64 ??
|
|
1100
|
+
null;
|
|
1101
|
+
if (!b64) continue;
|
|
1102
|
+
const file = path.join(dir, `${path.basename(slide.fileName, ".jsx")}.png`);
|
|
1103
|
+
fs.writeFileSync(file, Buffer.from(b64, "base64"));
|
|
1104
|
+
files.push(file);
|
|
1105
|
+
}
|
|
1106
|
+
return { dir, files, note: null };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1057
1109
|
// Render a deck to a .pptx file (the `slidemind build-pptx` path). For a BOUND
|
|
1058
1110
|
// deck against a backend serving brand routes this is fully pane-free — no
|
|
1059
1111
|
// PowerPoint window needed at all (backgrounded or closed). Otherwise it needs
|
|
@@ -1092,6 +1144,7 @@ export async function buildPptxFile({
|
|
|
1092
1144
|
slideCount: detached.result.slideCount ?? null,
|
|
1093
1145
|
warnings: detached.result.warnings ?? [],
|
|
1094
1146
|
skippedSlides: detached.result.skippedSlides ?? [],
|
|
1147
|
+
preview: await writeBuildPreview(deckDir, onProgress),
|
|
1095
1148
|
};
|
|
1096
1149
|
}
|
|
1097
1150
|
}
|
|
@@ -1115,5 +1168,6 @@ export async function buildPptxFile({
|
|
|
1115
1168
|
slideCount: result.slideCount ?? null,
|
|
1116
1169
|
warnings: result.warnings ?? [],
|
|
1117
1170
|
skippedSlides: result.skippedSlides ?? [],
|
|
1171
|
+
preview: await writeBuildPreview(deckDir, onProgress),
|
|
1118
1172
|
};
|
|
1119
1173
|
}
|
package/src/bridge/capture.mjs
CHANGED
|
@@ -14,6 +14,8 @@ import fs from "node:fs";
|
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
|
|
17
|
+
import { convertedHostLayouts, loadConversions } from "../lib/conversions.mjs";
|
|
18
|
+
|
|
17
19
|
// Same launch profile as the add-in's chrome-launcher.ts, so the client browser
|
|
18
20
|
// behaves identically to the server's during capture.
|
|
19
21
|
const LAUNCH_ARGS = [
|
|
@@ -88,6 +90,15 @@ export function buildCaptureInput(deckDir, bundleDir) {
|
|
|
88
90
|
|
|
89
91
|
const slides = readJsxDir(path.join(deckDir, "slides"));
|
|
90
92
|
const layouts = readJsxDir(path.join(bundleDir, "layouts"));
|
|
93
|
+
// Converted slides (`slidemind adopt`) invoke a ConvertedHost layout the
|
|
94
|
+
// server mints per source canvas — no bundle carries it, so without a
|
|
95
|
+
// stand-in the engine rejects every one of them as an unregistered layout
|
|
96
|
+
// and the agent authors them blind. The stub is the deck's own host box;
|
|
97
|
+
// geometry stays authoritative on the server render.
|
|
98
|
+
Object.assign(
|
|
99
|
+
layouts,
|
|
100
|
+
convertedHostLayouts(deckDir, loadConversions(deckDir, { includeSource: false })),
|
|
101
|
+
);
|
|
91
102
|
|
|
92
103
|
const layoutsCssPath = path.join(bundleDir, "layouts", "layouts.css");
|
|
93
104
|
const layoutsCss = fs.existsSync(layoutsCssPath)
|
package/src/builders/build.mjs
CHANGED
|
@@ -37,8 +37,9 @@ import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
|
37
37
|
import { loadChartPreview, loadConnectorPreview } from "../bridge/capture.mjs";
|
|
38
38
|
import { FAVICON_LINK } from "./gate-server.mjs";
|
|
39
39
|
import { lintSlidesOrThrow } from "./lint.mjs";
|
|
40
|
+
import { lintConvertedOrThrow } from "../lib/lint-converted.mjs";
|
|
40
41
|
import { resolveSkillForDeck } from "./resolve-skill.mjs";
|
|
41
|
-
import { loadConversions } from "../lib/conversions.mjs";
|
|
42
|
+
import { convertedHostLayouts, loadConversions } from "../lib/conversions.mjs";
|
|
42
43
|
import { slideOrderWarning, sortSlideFilenames } from "../lib/slide-order.mjs";
|
|
43
44
|
|
|
44
45
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -88,26 +89,13 @@ function inlineLayout(file) {
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
// Preview stand-ins for the ConvertedHost layouts converted slides invoke
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
function convertedHostStubs(slideFiles, conversions) {
|
|
100
|
-
const hosts = new Map();
|
|
101
|
-
for (const file of slideFiles) {
|
|
102
|
-
if (!conversions[path.basename(file)]) continue;
|
|
103
|
-
const m = HOST_NAME_RE.exec(fs.readFileSync(file, "utf8"));
|
|
104
|
-
if (m) hosts.set(m[1], { w: emuToCssPx(m[2]), h: emuToCssPx(m[3]) });
|
|
105
|
-
}
|
|
106
|
-
return [...hosts].map(
|
|
107
|
-
([name, { w, h }]) =>
|
|
108
|
-
`<script type="text/jsx-template" data-kind="layout" data-name="${name}" data-binding="${name}">\n` +
|
|
109
|
-
`function ${name}({ background, children }) { return <div style={{ position: "relative", width: "${w}", height: "${h}", overflow: "hidden", background: background || "#FFFFFF" }}>{children}</div>; }\n` +
|
|
110
|
-
`</script>`,
|
|
92
|
+
// Preview stand-ins for the ConvertedHost layouts converted slides invoke —
|
|
93
|
+
// same sources the local capture path feeds its engine (src/lib/conversions.mjs),
|
|
94
|
+
// wrapped as inline jsx-templates for this page.
|
|
95
|
+
function convertedHostStubs(deckDir, conversions) {
|
|
96
|
+
return Object.entries(convertedHostLayouts(deckDir, conversions)).map(
|
|
97
|
+
([name, source]) =>
|
|
98
|
+
`<script type="text/jsx-template" data-kind="layout" data-name="${name.replace(/\.jsx$/, "")}" data-binding="${name.replace(/\.jsx$/, "")}">\n${source}</script>`,
|
|
111
99
|
);
|
|
112
100
|
}
|
|
113
101
|
|
|
@@ -381,12 +369,24 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
381
369
|
// Converted slides (`slidemind adopt`) invoke a synthetic ConvertedHost
|
|
382
370
|
// layout the server mints per source canvas; no bundle carries it, so the
|
|
383
371
|
// local engine would fail every one of them as an unregistered layout. They
|
|
384
|
-
//
|
|
372
|
+
// get the dialect's OWN lint instead (master prompts that survived, empty
|
|
373
|
+
// placeholders, text in a field frame, text spilling a fixed box) — without
|
|
374
|
+
// it the adopted half of a deck was checked by nothing at all.
|
|
385
375
|
const conversions = loadConversions(deckDir, { includeSource: false });
|
|
376
|
+
const convertedFiles = slideFiles.filter((f) => conversions[path.basename(f)]);
|
|
386
377
|
const authoredFiles = slideFiles.filter((f) => !conversions[path.basename(f)]);
|
|
387
378
|
if (authoredFiles.length > 0) {
|
|
388
379
|
await lintSlidesOrThrow({ deckDir, slideFiles: authoredFiles, layoutSources });
|
|
389
380
|
}
|
|
381
|
+
if (convertedFiles.length > 0) {
|
|
382
|
+
lintConvertedOrThrow({
|
|
383
|
+
slideFiles: convertedFiles,
|
|
384
|
+
strict: Boolean(opts.strict),
|
|
385
|
+
// view-draft looks at half-authored slides, so it passes false; the
|
|
386
|
+
// review gate and the render path enforce.
|
|
387
|
+
enforce: opts.enforceConverted !== false,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
390
|
|
|
391
391
|
// The <Chart> stand-in runtime rides in the same bundle cache (see
|
|
392
392
|
// loadChartPreview) — resolved after lint, which already proved the bundle
|
|
@@ -398,7 +398,7 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
398
398
|
|
|
399
399
|
const layoutsBlock = layoutFiles
|
|
400
400
|
.map(inlineLayout)
|
|
401
|
-
.concat(convertedHostStubs(
|
|
401
|
+
.concat(convertedHostStubs(deckDir, conversions))
|
|
402
402
|
.join("\n");
|
|
403
403
|
const slidesBlock = slideFiles
|
|
404
404
|
.map((file, i) => inlineSlide(file, i))
|
package/src/cli/deck.mjs
CHANGED
|
@@ -29,9 +29,24 @@ export async function cmdImagen(rest) {
|
|
|
29
29
|
let outName = null;
|
|
30
30
|
let size = null;
|
|
31
31
|
let quality = null;
|
|
32
|
+
let into = null;
|
|
33
|
+
let behind = null;
|
|
34
|
+
let rect = null;
|
|
32
35
|
for (let i = 0; i < rest.length; i++) {
|
|
33
36
|
const a = rest[i];
|
|
34
|
-
if (a === "--
|
|
37
|
+
if (a === "--into") {
|
|
38
|
+
into = rest[++i];
|
|
39
|
+
if (!into) fail("--into expects a slide file (e.g. slides/07-approach.jsx)");
|
|
40
|
+
} else if (a === "--behind") {
|
|
41
|
+
behind = rest[++i];
|
|
42
|
+
if (!/^\d+$/.test(behind ?? "")) fail("--behind expects a data-sm-source-id number");
|
|
43
|
+
} else if (a === "--rect") {
|
|
44
|
+
const parts = String(rest[++i] ?? "").split(",").map((n) => n.trim());
|
|
45
|
+
if (parts.length !== 4 || parts.some((n) => !/^-?[\d.]+$/.test(n))) {
|
|
46
|
+
fail("--rect expects <left,top,width,height> in px");
|
|
47
|
+
}
|
|
48
|
+
rect = { left: parts[0], top: parts[1], width: parts[2], height: parts[3] };
|
|
49
|
+
} else if (a === "--deck") {
|
|
35
50
|
deck = rest[++i];
|
|
36
51
|
if (!deck) fail("--deck expects a deck");
|
|
37
52
|
} else if (a === "--out" || a === "-o") {
|
|
@@ -58,9 +73,12 @@ export async function cmdImagen(rest) {
|
|
|
58
73
|
if (!prompt || !prompt.trim() || !deck) {
|
|
59
74
|
fail(
|
|
60
75
|
'usage: slidemind imagen "<prompt>" --deck <deck> [--out <name.png>] ' +
|
|
61
|
-
"[--size auto|1024x1024|1536x1024|1024x1536] [--quality low|medium|high|auto]"
|
|
76
|
+
"[--size auto|1024x1024|1536x1024|1024x1536] [--quality low|medium|high|auto] " +
|
|
77
|
+
"[--into slides/NN-name.jsx --behind <source-id>|--rect <l,t,w,h>]",
|
|
62
78
|
);
|
|
63
79
|
}
|
|
80
|
+
if ((behind || rect) && !into) fail("--behind/--rect need --into <slide file>");
|
|
81
|
+
if (into && !behind && !rect) fail("--into needs --behind <source-id> or --rect <l,t,w,h>");
|
|
64
82
|
const deckDir = resolveDeckArg(deck);
|
|
65
83
|
const bundleMod = await import(
|
|
66
84
|
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
@@ -106,9 +124,37 @@ export async function cmdImagen(rest) {
|
|
|
106
124
|
}
|
|
107
125
|
const outPath = path.join(assetsDir, file);
|
|
108
126
|
fs.writeFileSync(outPath, Buffer.from(m[2], "base64"));
|
|
127
|
+
process.stdout.write(`Wrote ${path.relative(process.cwd(), outPath)}\n`);
|
|
128
|
+
if (!into) {
|
|
129
|
+
process.stdout.write(
|
|
130
|
+
` Reference it from slide props as "assets/${file}" (e.g. summaryImage, illustration),\n` +
|
|
131
|
+
` or re-run with --into <slide> --behind <source-id> to place it for you.\n`,
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Place it: a picture the agent wires up by hand is the step that put the
|
|
136
|
+
// last one on the wrong element.
|
|
137
|
+
const slidePath = path.isAbsolute(into)
|
|
138
|
+
? into
|
|
139
|
+
: path.join(deckDir, into.startsWith("slides/") ? into : path.join("slides", into));
|
|
140
|
+
if (!fs.existsSync(slidePath)) fail(`--into: no such slide file: ${slidePath}`);
|
|
141
|
+
const { insertImageDiv } = await import(
|
|
142
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "jsx-insert.mjs")).href
|
|
143
|
+
);
|
|
144
|
+
let next;
|
|
145
|
+
try {
|
|
146
|
+
next = insertImageDiv(fs.readFileSync(slidePath, "utf8"), {
|
|
147
|
+
url: `./assets/${file}`,
|
|
148
|
+
behind,
|
|
149
|
+
rect,
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
fail(`${err.message}\n The image is on disk at assets/${file} — place it by hand.`);
|
|
153
|
+
}
|
|
154
|
+
fs.writeFileSync(slidePath, next);
|
|
109
155
|
process.stdout.write(
|
|
110
|
-
`
|
|
111
|
-
`
|
|
156
|
+
` Placed it in ${path.relative(process.cwd(), slidePath)} ` +
|
|
157
|
+
`${behind ? `behind data-sm-source-id={${behind}}` : "at the given rect"} — re-read the file before patching it.\n`,
|
|
112
158
|
);
|
|
113
159
|
}
|
|
114
160
|
|
|
@@ -301,13 +347,21 @@ export async function cmdBuildPptx(rest) {
|
|
|
301
347
|
.catch((err) => fail(withUnboundHint(deckDir, err)));
|
|
302
348
|
if (asJson) {
|
|
303
349
|
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`,
|
|
350
|
+
`${JSON.stringify({ outPath: result.outPath, slideCount: result.slideCount ?? null, presId: result.presId ?? null, windowless: Boolean(result.windowless), warnings: result.warnings ?? [], skippedSlides: result.skippedSlides ?? [], preview: result.preview ?? null }, null, 2)}\n`,
|
|
305
351
|
);
|
|
306
352
|
} else {
|
|
307
353
|
process.stdout.write(
|
|
308
354
|
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in` +
|
|
309
355
|
`${result.windowless ? " (no PowerPoint window needed)" : ""}\n`,
|
|
310
356
|
);
|
|
357
|
+
// LOOK at what shipped — the .pptx is opaque from here.
|
|
358
|
+
if (result.preview?.dir) {
|
|
359
|
+
process.stdout.write(
|
|
360
|
+
` ${result.preview.files.length} slide png(s) in ${path.relative(process.cwd(), result.preview.dir)}/ — read them before handing the deck over\n`,
|
|
361
|
+
);
|
|
362
|
+
} else if (result.preview?.note) {
|
|
363
|
+
process.stdout.write(` ${result.preview.note}\n`);
|
|
364
|
+
}
|
|
311
365
|
}
|
|
312
366
|
for (const w of result.warnings ?? []) {
|
|
313
367
|
process.stderr.write(` warning: ${w}\n`);
|
|
@@ -331,10 +385,15 @@ export async function cmdViewDraft(rest) {
|
|
|
331
385
|
let fix = false;
|
|
332
386
|
let lintOnly = false;
|
|
333
387
|
let asJson = false;
|
|
388
|
+
// Converted slides only: promote the dialect lint's warnings (an empty
|
|
389
|
+
// title/body placeholder) to errors.
|
|
390
|
+
let strict = false;
|
|
334
391
|
for (let i = 0; i < passthrough.length; i++) {
|
|
335
392
|
const a = passthrough[i];
|
|
336
393
|
if (a === "--fix") {
|
|
337
394
|
fix = true;
|
|
395
|
+
} else if (a === "--strict") {
|
|
396
|
+
strict = true;
|
|
338
397
|
} else if (a === "--lint-only") {
|
|
339
398
|
lintOnly = true;
|
|
340
399
|
} else if (a === "--json") {
|
|
@@ -399,7 +458,9 @@ export async function cmdViewDraft(rest) {
|
|
|
399
458
|
);
|
|
400
459
|
}
|
|
401
460
|
}
|
|
402
|
-
|
|
461
|
+
// Advisory here: the converted rules print as warnings while slides are
|
|
462
|
+
// still being edited, and block at `review` / `build-pptx`. --strict gates now.
|
|
463
|
+
await buildDeck(deckDir, { strict, enforceConverted: strict });
|
|
403
464
|
if (lintOnly) {
|
|
404
465
|
if (asJson) process.stdout.write(`${JSON.stringify({ deck: path.basename(deckDir), lint: "clean" })}\n`);
|
|
405
466
|
else process.stdout.write("Lint clean (no render — drop --lint-only for the draft previews).\n");
|
package/src/cli/help.mjs
CHANGED
|
@@ -63,6 +63,7 @@ export function printHelp(cmd = null) {
|
|
|
63
63
|
` --out <path> output path (default <deck>/<deck>.pptx)\n` +
|
|
64
64
|
` --force discard manual PowerPoint edits instead of reconciling them\n` +
|
|
65
65
|
` --json machine-readable result\n` +
|
|
66
|
+
` (writes per-slide PNGs of the built deck to <deck>/.build-preview/)\n` +
|
|
66
67
|
` slidemind inspect-template-deck <deck> Inspect a template deck as JSON for pack-owned validation\n` +
|
|
67
68
|
` --pack <dir> explicit template-pack directory (required)\n` +
|
|
68
69
|
` --allow-unrendered-freeform include JSX inserts without .pptx artifacts\n` +
|
|
@@ -76,6 +77,7 @@ export function printHelp(cmd = null) {
|
|
|
76
77
|
` --only <file|N>[,…] render just these slides (a per-slide builder's own check)\n` +
|
|
77
78
|
` --fix apply the deterministic lint autofixes first\n` +
|
|
78
79
|
` --lint-only lint + HTML build, no render (fast fix loop)\n` +
|
|
80
|
+
` --strict converted slides: gate here like review does (they only warn by default)\n` +
|
|
79
81
|
` --no-translate skip the export gate (purely local check)\n` +
|
|
80
82
|
` --translate insist on the export gate (fail instead of skipping when the backend is unreachable)\n` +
|
|
81
83
|
` --json machine-readable result (slides, defects, reviewCanOpen)\n` +
|
|
@@ -144,6 +146,9 @@ export function printHelp(cmd = null) {
|
|
|
144
146
|
` --out <name> file name under assets/ (default imagen-N.png)\n` +
|
|
145
147
|
` --size <s> auto|1024x1024|1536x1024|1024x1536 (default auto)\n` +
|
|
146
148
|
` --quality <q> low|medium|high|auto\n` +
|
|
149
|
+
` --into <slide> place it in this slide file instead of only writing the asset\n` +
|
|
150
|
+
` --behind <id> ... as a background div right before data-sm-source-id={id} (takes its rect)\n` +
|
|
151
|
+
` --rect <l,t,w,h> ... at this rect in px instead\n` +
|
|
147
152
|
` slidemind download-bundle [deck] Materialize or refresh a corporate design's bundle locally\n` +
|
|
148
153
|
` (pane-free for a bound deck, --brand or --all — no PowerPoint needed)\n` +
|
|
149
154
|
` --to <presId> target a specific window (forces the pane route)\n` +
|
package/src/lib/adopt.mjs
CHANGED
|
@@ -30,7 +30,14 @@ import { decodeSourceTags, mintSlideGuid, readSlideTags } from "../pptx/tags.mjs
|
|
|
30
30
|
import { foldPickedSlides } from "../pptx/slide-swap.mjs";
|
|
31
31
|
import { readDeckBundleRef, writeDeckBundleRef } from "../bridge/bundle.mjs";
|
|
32
32
|
import { loadSlideGuids, saveSlideGuids, slideGuidEntry } from "./slide-guids.mjs";
|
|
33
|
-
import {
|
|
33
|
+
import { writeConversionRecord } from "./conversions.mjs";
|
|
34
|
+
|
|
35
|
+
// Converted JSX needs a fresh baseline against the imported PPTX. Source tags
|
|
36
|
+
// alone cannot recover the original conversion evidence or preserved natives.
|
|
37
|
+
export function needsConversion(decoded) {
|
|
38
|
+
return !decoded || /<ConvertedHost\d+x\d+\b/.test(decoded.jsx) ||
|
|
39
|
+
/^\s*\/\/\s*SLIDEMIND_DIALECT:\s*converted\b/m.test(decoded.jsx);
|
|
40
|
+
}
|
|
34
41
|
|
|
35
42
|
export const LIVE_DECK_UNSUPPORTED =
|
|
36
43
|
"conversion of live presentations requires the add-in path in v1 — " +
|
|
@@ -174,21 +181,13 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
|
|
|
174
181
|
const native = [];
|
|
175
182
|
let bundle = null;
|
|
176
183
|
|
|
177
|
-
// Fold
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// that stays native, a CONVERTED slide whose preserved natives (OLE /
|
|
181
|
-
// SmartArt / unsupported chart kinds) need their verbatim source package kept
|
|
182
|
-
// for the render-time splice (design D4/D6), and a converted slide carrying
|
|
183
|
-
// the deck's own chart as `<Chart data-sm-source-id>`, which the render
|
|
184
|
-
// patches against that same package (native-charts 5.4a).
|
|
184
|
+
// Fold every converted slide in one pass. The package supplies preserved
|
|
185
|
+
// natives AND the original envelope (layouts, passive third-party tags,
|
|
186
|
+
// relationships). Tagged converted JSX alone cannot restore either safely.
|
|
185
187
|
const picks = [];
|
|
186
188
|
decodedByIndex.forEach((decoded, index) => {
|
|
187
|
-
if (decoded) return;
|
|
188
|
-
|
|
189
|
-
if (!conv || conv.preserved?.length > 0 || hasSourcedChart(conv.jsx)) {
|
|
190
|
-
picks.push(index + 1);
|
|
191
|
-
}
|
|
189
|
+
if (!needsConversion(decoded)) return;
|
|
190
|
+
picks.push(index + 1);
|
|
192
191
|
});
|
|
193
192
|
const foldByPick = new Map(
|
|
194
193
|
(picks.length
|
|
@@ -229,7 +228,7 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
|
|
|
229
228
|
}
|
|
230
229
|
|
|
231
230
|
decodedByIndex.forEach((decoded, index) => {
|
|
232
|
-
if (decoded) {
|
|
231
|
+
if (decoded && !needsConversion(decoded)) {
|
|
233
232
|
const stem = stemFor(decoded.meta.name, index, taken);
|
|
234
233
|
let jsx = decoded.jsx;
|
|
235
234
|
for (const asset of decoded.assets) {
|
|
@@ -251,11 +250,14 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
|
|
|
251
250
|
if (conv) {
|
|
252
251
|
// Deterministically converted: editable jsx in the converted dialect,
|
|
253
252
|
// plus the identity/evidence record push threads back to the server.
|
|
254
|
-
const stem = stemFor(conv.name ?? `${pad2(index + 1)}-converted`, index, taken);
|
|
253
|
+
const stem = stemFor(decoded?.meta.name ?? conv.name ?? `${pad2(index + 1)}-converted`, index, taken);
|
|
255
254
|
let jsx = conv.jsx;
|
|
256
255
|
for (const [from, to] of convAssetRenames) jsx = jsx.split(from).join(to);
|
|
257
256
|
fs.writeFileSync(path.join(slidesDir, `${stem}.jsx`), jsx, "utf8");
|
|
258
|
-
const
|
|
257
|
+
const candidateGuid = decoded?.guid ?? conv.guid;
|
|
258
|
+
const owns = !decoded || ownerIndexByGuid.get(decoded.guid)?.index === index;
|
|
259
|
+
const guid = owns && candidateGuid && !seenGuids.has(candidateGuid)
|
|
260
|
+
? candidateGuid : mintSlideGuid();
|
|
259
261
|
seenGuids.add(guid);
|
|
260
262
|
guids[`slides/${stem}.jsx`] = slideGuidEntry(guid, jsx);
|
|
261
263
|
writeConversionRecord(
|
|
@@ -277,6 +279,7 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
|
|
|
277
279
|
},
|
|
278
280
|
fold?.bytes ?? null,
|
|
279
281
|
);
|
|
282
|
+
bundle ??= decoded?.meta.bundle ?? null;
|
|
280
283
|
convertedFiles.push(`${stem}.jsx`);
|
|
281
284
|
return;
|
|
282
285
|
}
|
|
@@ -329,7 +332,10 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
/**
|
|
332
|
-
*
|
|
335
|
+
* Convert foreign slides and re-convert embedded converted slides against the
|
|
336
|
+
* CURRENT imported PPTX. This recovers a fresh baseline/evidence pair without
|
|
337
|
+
* pretending the original conversion was reviewed. Ordinary authored JSX is
|
|
338
|
+
* restored verbatim. Requests go through the add-in's
|
|
333
339
|
* `POST /api/cli/brands/:brandId/convert-pptx` route. Returns the route result
|
|
334
340
|
* (`{ slides, assets, skipped, warnings }` + the `brandId` it ran against) to
|
|
335
341
|
* hand to {@link adoptDeckFromPptx}, or null when conversion is not available
|
|
@@ -349,7 +355,7 @@ export async function convertUntaggedSlides({
|
|
|
349
355
|
const { buffer, decodedByIndex } = source ?? readSourceDeck(pptxPath);
|
|
350
356
|
const slides = [];
|
|
351
357
|
decodedByIndex.forEach((decoded, i) => {
|
|
352
|
-
if (
|
|
358
|
+
if (needsConversion(decoded)) slides.push(i + 1);
|
|
353
359
|
});
|
|
354
360
|
if (slides.length === 0) return null;
|
|
355
361
|
|
|
@@ -372,7 +378,7 @@ export async function convertUntaggedSlides({
|
|
|
372
378
|
if (brands.length !== 1) {
|
|
373
379
|
note(
|
|
374
380
|
brands.length === 0
|
|
375
|
-
? "no corporate design available to convert against —
|
|
381
|
+
? "no corporate design available to convert against — slides requiring conversion stay native"
|
|
376
382
|
: "several corporate designs available — pass `--brand <presFP>` to convert the untagged slides",
|
|
377
383
|
);
|
|
378
384
|
return null;
|
|
@@ -383,7 +389,7 @@ export async function convertUntaggedSlides({
|
|
|
383
389
|
// capability probe — do it here, so a whole .pptx is never shipped to a
|
|
384
390
|
// backend that will just 404 the route.
|
|
385
391
|
if (backendURL && !(await client.backendSupportsConvertPptx(backendURL))) {
|
|
386
|
-
note("this add-in does not convert foreign slides —
|
|
392
|
+
note("this add-in does not convert foreign slides — slides requiring conversion stay native");
|
|
387
393
|
return null;
|
|
388
394
|
}
|
|
389
395
|
}
|
|
@@ -396,15 +402,15 @@ export async function convertUntaggedSlides({
|
|
|
396
402
|
backendURL,
|
|
397
403
|
});
|
|
398
404
|
if (result?.ok === false) {
|
|
399
|
-
note("the add-in declined to convert this file —
|
|
405
|
+
note("the add-in declined to convert this file — slides requiring conversion stay native");
|
|
400
406
|
return null;
|
|
401
407
|
}
|
|
402
408
|
return { ...result, brandId: resolved };
|
|
403
409
|
} catch (err) {
|
|
404
410
|
note(
|
|
405
411
|
err?.code === "convert_route_unavailable"
|
|
406
|
-
? "this add-in does not convert foreign slides —
|
|
407
|
-
: `slide conversion unavailable (${err?.message ?? err}) —
|
|
412
|
+
? "this add-in does not convert foreign slides — slides requiring conversion stay native"
|
|
413
|
+
: `slide conversion unavailable (${err?.message ?? err}) — slides requiring conversion stay native`,
|
|
408
414
|
);
|
|
409
415
|
return null;
|
|
410
416
|
}
|