slidemind 0.3.12 → 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 +71 -8
- 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 +16 -669
- 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/bridge/bundle.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getDesignBundle,
|
|
51
51
|
getDesignBundleForBrand,
|
|
52
52
|
getDesignBundleForBrandAnyBackend,
|
|
53
|
+
listBrands,
|
|
53
54
|
} from "./client.mjs";
|
|
54
55
|
import { updateCliInstruction } from "../install-channel.mjs";
|
|
55
56
|
|
|
@@ -655,6 +656,74 @@ export function readDeckBundleRef(deckDir) {
|
|
|
655
656
|
// Resolve the materialized bundle directory a deck should read its design files
|
|
656
657
|
// from (via the deck's .bundle-ref.json). Returns null when no bundle has been
|
|
657
658
|
// materialized for the deck's CD yet.
|
|
659
|
+
// Re-sync a bound deck's design bundle to the design as it stands right now.
|
|
660
|
+
//
|
|
661
|
+
// `ready` materializes at session start, but a corporate design can change
|
|
662
|
+
// while a session runs, and every local step afterwards (HTML build, lint,
|
|
663
|
+
// capture, the draft checks, and the etag the server render is validated
|
|
664
|
+
// against) reads this cached copy. So the deck commands call this first: the
|
|
665
|
+
// sync is etag-gated and incremental, so an unchanged design downloads nothing.
|
|
666
|
+
//
|
|
667
|
+
// Returns { result, changed } — `changed` is true when the design's etag moved,
|
|
668
|
+
// in which case the deck's ref is rewritten to the new one. Returns null when
|
|
669
|
+
// the deck is unbound or the backend cannot be reached: a stale-but-present
|
|
670
|
+
// bundle is what keeps the host-free steps host-free.
|
|
671
|
+
export async function refreshDeckBundle(deckDir) {
|
|
672
|
+
if (!deckDir) return null;
|
|
673
|
+
const ref = readDeckBundleRef(deckDir);
|
|
674
|
+
if (!ref?.presFP) return null; // unbound — `ready <deck>` binds it
|
|
675
|
+
// A refresh must never make a host-free step wait like a render does: a dead
|
|
676
|
+
// backend has to fall back to the cached bundle in seconds, not the 35s
|
|
677
|
+
// default plus retries. A negative delay list filters to empty, which also
|
|
678
|
+
// turns the transient retry off — the next command retries anyway.
|
|
679
|
+
const prev = {
|
|
680
|
+
SLIDEMIND_BRIDGE_TIMEOUT_MS: process.env.SLIDEMIND_BRIDGE_TIMEOUT_MS,
|
|
681
|
+
SLIDEMIND_BRIDGE_RETRY_DELAYS_MS: process.env.SLIDEMIND_BRIDGE_RETRY_DELAYS_MS,
|
|
682
|
+
};
|
|
683
|
+
process.env.SLIDEMIND_BRIDGE_TIMEOUT_MS ??=
|
|
684
|
+
process.env.SLIDEMIND_BUNDLE_REFRESH_TIMEOUT_MS || "6000";
|
|
685
|
+
process.env.SLIDEMIND_BRIDGE_RETRY_DELAYS_MS ??= "-1";
|
|
686
|
+
try {
|
|
687
|
+
const result = await materializeBundle(null, { brandId: ref.presFP });
|
|
688
|
+
if (!result) return null;
|
|
689
|
+
const changed = Boolean(result.etag) && result.etag !== ref.etag;
|
|
690
|
+
if (changed) {
|
|
691
|
+
writeDeckBundleRef(deckDir, { presFP: result.presFP, etag: result.etag });
|
|
692
|
+
}
|
|
693
|
+
return { result, changed };
|
|
694
|
+
} catch {
|
|
695
|
+
return null; // offline / unconnected / design withdrawn — keep the cache
|
|
696
|
+
} finally {
|
|
697
|
+
for (const [k, v] of Object.entries(prev)) {
|
|
698
|
+
if (v === undefined) delete process.env[k];
|
|
699
|
+
else process.env[k] = v;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// The design an UNBOUND deck can be rendered/previewed against WITHOUT a
|
|
705
|
+
// PowerPoint window — the same ladder `ready` walks once no window is in play:
|
|
706
|
+
// the deck's own binding → the one design cached on this machine → the one
|
|
707
|
+
// design the connected backends list. Returns the presFP, or null when nothing
|
|
708
|
+
// answers unambiguously (0 or several of either; no brand route) — the caller
|
|
709
|
+
// then falls back to the window registry, which is the only remaining source
|
|
710
|
+
// of a design (an open presentation's active corporate design) and the one
|
|
711
|
+
// place that legitimately needs the pane.
|
|
712
|
+
export async function resolveWindowlessBrand(deckDir) {
|
|
713
|
+
const bound = readDeckBundleRef(deckDir)?.presFP;
|
|
714
|
+
if (bound) return bound;
|
|
715
|
+
const cached = listCachedBundles();
|
|
716
|
+
if (cached.length === 1) return cached[0].presFP;
|
|
717
|
+
if (cached.length > 1) return null;
|
|
718
|
+
let brands = [];
|
|
719
|
+
try {
|
|
720
|
+
brands = await listBrands();
|
|
721
|
+
} catch {
|
|
722
|
+
brands = [];
|
|
723
|
+
}
|
|
724
|
+
return brands.length === 1 ? brands[0].presFP : null;
|
|
725
|
+
}
|
|
726
|
+
|
|
658
727
|
export function resolveBundleDirForDeck(deckDir) {
|
|
659
728
|
const ref = readDeckBundleRef(deckDir);
|
|
660
729
|
if (!ref?.presFP) return null;
|
package/src/bridge/capture.mjs
CHANGED
|
@@ -145,8 +145,9 @@ export function buildCaptureInput(deckDir, bundleDir) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// Launch a headless Chrome, run `fn`, then tear it down — mirrors the add-in's
|
|
148
|
-
// withBrowser (waits for the process to fully exit before cleanup).
|
|
149
|
-
|
|
148
|
+
// withBrowser (waits for the process to fully exit before cleanup). Exported
|
|
149
|
+
// for the draft-check grid overlay, which reuses the same pinned Chrome.
|
|
150
|
+
export async function withBrowser(fn) {
|
|
150
151
|
// Test hook: run the engine with no real browser. Only safe with a fake
|
|
151
152
|
// engine that ignores its `browser` argument — never set in production.
|
|
152
153
|
if (process.env.SLIDEMIND_CAPTURE_BROWSERLESS === "1") {
|
|
@@ -162,11 +163,25 @@ async function withBrowser(fn) {
|
|
|
162
163
|
// the bundle-shipped fonts are identical at the same pinned version, so text
|
|
163
164
|
// measurement parity with the add-in's renderer is preserved.
|
|
164
165
|
// SLIDEMIND_CAPTURE_FULL_CHROME=1 forces the full binary for A/B debugging.
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const launch = (args) =>
|
|
167
|
+
puppeteer.launch({
|
|
168
|
+
headless: process.env.SLIDEMIND_CAPTURE_FULL_CHROME === "1" ? true : "shell",
|
|
169
|
+
protocolTimeout: 600_000,
|
|
170
|
+
args,
|
|
171
|
+
});
|
|
172
|
+
let browser;
|
|
173
|
+
try {
|
|
174
|
+
browser = await launch(LAUNCH_ARGS);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Codex's workspace-write seatbelt denies the mach-port rendezvous the
|
|
177
|
+
// browser process uses to hand off to its renderer ("bootstrap_check_in
|
|
178
|
+
// … Permission denied"), so a multi-process launch dies before the first
|
|
179
|
+
// page. Single-process keeps the renderer in-process — no rendezvous —
|
|
180
|
+
// and paints identically (same Blink/Skia, same fonts). Retried only on a
|
|
181
|
+
// launch failure, so every other environment keeps the stock launch.
|
|
182
|
+
if (!/Failed to launch the browser process/i.test(String(err?.message ?? err))) throw err;
|
|
183
|
+
browser = await launch([...LAUNCH_ARGS, "--single-process", "--no-zygote"]);
|
|
184
|
+
}
|
|
170
185
|
try {
|
|
171
186
|
return await fn(browser);
|
|
172
187
|
} finally {
|
|
@@ -199,7 +214,7 @@ async function withBrowser(fn) {
|
|
|
199
214
|
* Throws if the engine is missing/corrupt or the local browser can't launch —
|
|
200
215
|
* the caller decides whether to fall back to the server render path.
|
|
201
216
|
*/
|
|
202
|
-
export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
217
|
+
export async function captureDeckLocally({ deckDir, bundleDir, onProgress, only = null }) {
|
|
203
218
|
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|
|
204
219
|
if (!fs.existsSync(enginePath)) {
|
|
205
220
|
throw new Error(`no capture engine in bundle ${bundleDir}`);
|
|
@@ -218,6 +233,15 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
const input = buildCaptureInput(deckDir, bundleDir);
|
|
236
|
+
// `only`: capture a subset of the authored slides (a per-slide builder
|
|
237
|
+
// re-checking its own file) — the engine renders each slide on its own, so
|
|
238
|
+
// dropping the others from the input is exact, not an approximation.
|
|
239
|
+
if (Array.isArray(only) && only.length > 0) {
|
|
240
|
+
const keep = new Set(only);
|
|
241
|
+
for (const name of Object.keys(input.slides)) {
|
|
242
|
+
if (!keep.has(name)) delete input.slides[name];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
221
245
|
return withBrowser((browser) => engine.captureDeck(browser, input, { onProgress }));
|
|
222
246
|
}
|
|
223
247
|
|
|
@@ -232,6 +256,45 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
|
232
256
|
* never a silent degradation: such a bundle's lint engine (same deployment)
|
|
233
257
|
* rejects `<Chart>` as an unknown component before a page is ever built.
|
|
234
258
|
*/
|
|
259
|
+
/**
|
|
260
|
+
* The exporter's connector geometry (`connector-derive`) as a browser script
|
|
261
|
+
* exported from the same artifact (`connectorPreview: { globalName, source }`).
|
|
262
|
+
* The HTML preview's connector overlay calls it on the measured DOM rects, so
|
|
263
|
+
* the preview draws the route and arrowhead the .pptx will have. The CLI ships
|
|
264
|
+
* no connector geometry of its own. null when the bundle has no engine or the
|
|
265
|
+
* engine predates the export (the overlay then draws nothing and says so).
|
|
266
|
+
*/
|
|
267
|
+
export async function loadConnectorPreview(bundleDir) {
|
|
268
|
+
if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
|
|
269
|
+
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|
|
270
|
+
const version = readBundleMeta(bundleDir).captureEngineVersion ?? "0";
|
|
271
|
+
const engine = await import(
|
|
272
|
+
`${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
|
|
273
|
+
);
|
|
274
|
+
const preview = engine.connectorPreview;
|
|
275
|
+
if (!preview || typeof preview.source !== "string") return null;
|
|
276
|
+
return { globalName: String(preview.globalName || "SlidemindConnectorDerive"), source: preview.source };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The measured draft checks (`draftChecks: { fill, typeScale, typeScaleTokens,
|
|
281
|
+
* overlap }`) exported from the same artifact — the rules the add-in's own
|
|
282
|
+
* see_draft runs. view-draft calls them on its local capture so both harnesses
|
|
283
|
+
* teach one contract. The CLI ships no draft-check rule of its own; null when
|
|
284
|
+
* the bundle has no engine or the engine predates the export.
|
|
285
|
+
*/
|
|
286
|
+
export async function loadDraftChecks(bundleDir) {
|
|
287
|
+
if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
|
|
288
|
+
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|
|
289
|
+
const version = readBundleMeta(bundleDir).captureEngineVersion ?? "0";
|
|
290
|
+
const engine = await import(
|
|
291
|
+
`${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
|
|
292
|
+
);
|
|
293
|
+
const checks = engine.draftChecks;
|
|
294
|
+
if (!checks || typeof checks.fill !== "function") return null;
|
|
295
|
+
return checks;
|
|
296
|
+
}
|
|
297
|
+
|
|
235
298
|
export async function loadChartPreview(bundleDir) {
|
|
236
299
|
if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
|
|
237
300
|
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|