slidemind 0.1.3 → 0.1.4
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 +31 -19
- package/README.md +31 -6
- package/bin/slidemind.mjs +437 -18
- package/package.json +2 -6
- package/skills/create-slides/SKILL.md +14 -8
- package/skills/search/SKILL.md +75 -0
- package/src/bridge/build-and-push.mjs +178 -34
- package/src/bridge/bundle.mjs +325 -99
- package/src/bridge/capture.mjs +187 -0
- package/src/bridge/client.mjs +628 -47
- package/src/builders/build.mjs +75 -26
- package/src/builders/lint.mjs +112 -0
- package/src/builders/resolve-skill.mjs +16 -21
- package/src/builders/review-overlay.js +821 -0
- package/src/builders/review.mjs +388 -0
- package/src/lib/reused-slides.mjs +67 -0
- package/src/lib/slide-order.mjs +4 -1
- package/src/mcp/server.mjs +368 -27
- package/src/builders/lint-slides.mjs +0 -796
- package/src/builders/primitives.json +0 -19
package/src/builders/build.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import fs from "node:fs";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
34
|
|
|
35
|
-
import { lintSlidesOrThrow } from "./lint
|
|
35
|
+
import { lintSlidesOrThrow } from "./lint.mjs";
|
|
36
36
|
import { resolveSkillForDeck } from "./resolve-skill.mjs";
|
|
37
37
|
import { slideOrderWarning, sortSlideFilenames } from "../lib/slide-order.mjs";
|
|
38
38
|
|
|
@@ -50,7 +50,10 @@ function safeInlineJson(data) {
|
|
|
50
50
|
return JSON.stringify(data).replace(/<\/script>/g, "<\\/script>");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// Exported for tests: the regex keeps the line's trailing newline, so line
|
|
54
|
+
// numbers stay aligned to the author's pristine file — which the browser-side
|
|
55
|
+
// data-sm-loc instrumentation depends on.
|
|
56
|
+
export function stripImports(src) {
|
|
54
57
|
// Remove `import ... from "..."` lines. Layouts live in global scope in
|
|
55
58
|
// the generated HTML, so slide files never need to import them explicitly.
|
|
56
59
|
return src.replace(
|
|
@@ -354,9 +357,49 @@ __RUNTIME_APPLIER__
|
|
|
354
357
|
}).join("");
|
|
355
358
|
}
|
|
356
359
|
|
|
360
|
+
// Stamp every intrinsic JSX element with data-sm-loc="<prefix>:<line>:<col>"
|
|
361
|
+
// so the preview-review overlay (review-overlay.js) can resolve a click back
|
|
362
|
+
// to the exact slide source location. Runs as a Babel plugin BEFORE the react
|
|
363
|
+
// preset (plugins run before presets) so it sees JSX nodes, not createElement
|
|
364
|
+
// calls. Inert without the overlay. Mirrors the add-in's instrument-source-loc.
|
|
365
|
+
function makeSourceLocPlugin(locPrefix) {
|
|
366
|
+
return function (babel) {
|
|
367
|
+
var t = babel.types;
|
|
368
|
+
return {
|
|
369
|
+
visitor: {
|
|
370
|
+
JSXOpeningElement: function (path) {
|
|
371
|
+
var node = path.node;
|
|
372
|
+
var nameNode = node.name;
|
|
373
|
+
// Intrinsic elements only: a lowercase JSXIdentifier. Skip
|
|
374
|
+
// components (<SlideCover>), member/namespaced names, fragments.
|
|
375
|
+
if (!nameNode || nameNode.type !== "JSXIdentifier") return;
|
|
376
|
+
if (!/^[a-z]/.test(nameNode.name)) return;
|
|
377
|
+
for (var i = 0; i < node.attributes.length; i++) {
|
|
378
|
+
var a = node.attributes[i];
|
|
379
|
+
if (a.type === "JSXAttribute" && a.name &&
|
|
380
|
+
a.name.name === "data-sm-loc") return;
|
|
381
|
+
}
|
|
382
|
+
var start = node.loc && node.loc.start;
|
|
383
|
+
if (!start) return;
|
|
384
|
+
var value = locPrefix + ":" + start.line + ":" + (start.column + 1);
|
|
385
|
+
node.attributes.push(
|
|
386
|
+
t.jsxAttribute(t.jsxIdentifier("data-sm-loc"),
|
|
387
|
+
t.stringLiteral(value))
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
357
395
|
function evalJsx(src, filename, extra) {
|
|
396
|
+
// The inliner wraps each source as "\\n" + src + "\\n" inside the <script>
|
|
397
|
+
// block; strip that one leading newline so Babel's line numbers (and thus
|
|
398
|
+
// data-sm-loc) align with the author's pristine workspace file.
|
|
399
|
+
if (src.charAt(0) === "\\n") src = src.slice(1);
|
|
358
400
|
var result = Babel.transform(src, {
|
|
359
401
|
presets: [["react", { runtime: "classic" }]],
|
|
402
|
+
plugins: [makeSourceLocPlugin(filename)],
|
|
360
403
|
sourceType: "module",
|
|
361
404
|
filename: filename
|
|
362
405
|
});
|
|
@@ -466,7 +509,11 @@ __RUNTIME_APPLIER__
|
|
|
466
509
|
document.querySelectorAll('script[data-kind="layout"]').forEach(function (s) {
|
|
467
510
|
var name = s.dataset.binding || layoutBindingName(s.dataset.name);
|
|
468
511
|
try {
|
|
469
|
-
LAYOUTS[name] = evalJsx(
|
|
512
|
+
LAYOUTS[name] = evalJsx(
|
|
513
|
+
s.textContent,
|
|
514
|
+
"layouts/" + s.dataset.name + ".jsx",
|
|
515
|
+
{}
|
|
516
|
+
);
|
|
470
517
|
window[name] = LAYOUTS[name];
|
|
471
518
|
} catch (err) {
|
|
472
519
|
console.error("Layout " + name + " failed to load:", err);
|
|
@@ -481,7 +528,7 @@ __RUNTIME_APPLIER__
|
|
|
481
528
|
try {
|
|
482
529
|
var Component = evalJsx(
|
|
483
530
|
s.textContent,
|
|
484
|
-
name + ".jsx",
|
|
531
|
+
"slides/" + name + ".jsx",
|
|
485
532
|
LAYOUTS
|
|
486
533
|
);
|
|
487
534
|
slides[idx] = { name: name, Component: Component };
|
|
@@ -591,7 +638,7 @@ __RUNTIME_APPLIER__
|
|
|
591
638
|
</html>
|
|
592
639
|
`;
|
|
593
640
|
|
|
594
|
-
export function buildDeck(deckDir, opts = {}) {
|
|
641
|
+
export async function buildDeck(deckDir, opts = {}) {
|
|
595
642
|
const skill = resolveSkillForDeck(deckDir);
|
|
596
643
|
const slidesDir = path.join(deckDir, "slides");
|
|
597
644
|
if (!fs.existsSync(slidesDir) || !fs.statSync(slidesDir).isDirectory()) {
|
|
@@ -640,17 +687,18 @@ export function buildDeck(deckDir, opts = {}) {
|
|
|
640
687
|
throw new Error(`No layouts in ${skill.layoutsDir}`);
|
|
641
688
|
}
|
|
642
689
|
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
690
|
+
// Lint with the add-in's engine from the materialized bundle (the CLI owns
|
|
691
|
+
// no lint source — see src/builders/lint.mjs). The registry derives layout
|
|
692
|
+
// component names from the RAW layout JSX (not the rewriteAssetsPath-mutated
|
|
693
|
+
// copy inlineLayout produces); the primitives registry rides next to the
|
|
694
|
+
// engine in the bundle cache.
|
|
695
|
+
const layoutSources = new Map(
|
|
696
|
+
[...layoutByStem].map(([stem, file]) => [
|
|
697
|
+
`${stem}.jsx`,
|
|
698
|
+
fs.readFileSync(file, "utf8"),
|
|
699
|
+
]),
|
|
700
|
+
);
|
|
701
|
+
await lintSlidesOrThrow({ deckDir, slideFiles, layoutSources });
|
|
654
702
|
|
|
655
703
|
copyDesignSystem(deckDir, skill);
|
|
656
704
|
|
|
@@ -673,13 +721,16 @@ export function buildDeck(deckDir, opts = {}) {
|
|
|
673
721
|
return { out, slideCount: slideFiles.length };
|
|
674
722
|
}
|
|
675
723
|
|
|
676
|
-
|
|
724
|
+
// Open a local file path OR an http URL in the user's default browser. The
|
|
725
|
+
// platform openers (open / start / xdg-open) accept both, so review.mjs reuses
|
|
726
|
+
// this to launch the local review server URL.
|
|
727
|
+
export function openInBrowser(target) {
|
|
677
728
|
const opener =
|
|
678
729
|
process.platform === "darwin"
|
|
679
|
-
? { cmd: "open", args: [
|
|
730
|
+
? { cmd: "open", args: [target] }
|
|
680
731
|
: process.platform === "win32"
|
|
681
|
-
? { cmd: "cmd", args: ["/c", "start", "",
|
|
682
|
-
: { cmd: "xdg-open", args: [
|
|
732
|
+
? { cmd: "cmd", args: ["/c", "start", "", target] }
|
|
733
|
+
: { cmd: "xdg-open", args: [target] };
|
|
683
734
|
spawn(opener.cmd, opener.args, { stdio: "ignore", detached: true }).unref();
|
|
684
735
|
}
|
|
685
736
|
|
|
@@ -709,20 +760,18 @@ function parseArgs(argv) {
|
|
|
709
760
|
return args;
|
|
710
761
|
}
|
|
711
762
|
|
|
712
|
-
function main() {
|
|
763
|
+
async function main() {
|
|
713
764
|
const args = parseArgs(process.argv.slice(2));
|
|
714
765
|
const deckDir = path.resolve(args.deck);
|
|
715
|
-
const { out, slideCount } = buildDeck(deckDir);
|
|
766
|
+
const { out, slideCount } = await buildDeck(deckDir);
|
|
716
767
|
const rel = path.relative(process.cwd(), out) || out;
|
|
717
768
|
process.stdout.write(`Built ${rel} (${slideCount} slides)\n`);
|
|
718
769
|
if (args.open) openInBrowser(out);
|
|
719
770
|
}
|
|
720
771
|
|
|
721
772
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
722
|
-
|
|
723
|
-
main();
|
|
724
|
-
} catch (err) {
|
|
773
|
+
main().catch((err) => {
|
|
725
774
|
process.stderr.write(`build: ${err.message || err}\n`);
|
|
726
775
|
process.exit(1);
|
|
727
|
-
}
|
|
776
|
+
});
|
|
728
777
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Slide lint — thin wrapper over the add-in's lint engine.
|
|
2
|
+
//
|
|
3
|
+
// This CLI owns NO lint source. The add-in is the single source of truth: its
|
|
4
|
+
// capability-table engine (the same one that gates `build_and_push` server-
|
|
5
|
+
// side) is served on the design-bundle wire as a self-contained ESM artifact
|
|
6
|
+
// and materialized into the bundle cache as `lint-engine.mjs` next to its
|
|
7
|
+
// `primitives.json` registry (see src/bridge/bundle.mjs). This module resolves
|
|
8
|
+
// the deck's bundle, dynamic-imports the cached engine, and mirrors the old
|
|
9
|
+
// lintSlidesOrThrow contract: findings to stderr, throw on errors.
|
|
10
|
+
//
|
|
11
|
+
// Lint therefore works fully offline once the bundle is materialized, and
|
|
12
|
+
// rule changes propagate on the next bundle fetch — no CLI release needed.
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
19
|
+
|
|
20
|
+
// Resolve the cached engine for a deck and explain every miss actionably.
|
|
21
|
+
// Returns { enginePath, version, primitives }.
|
|
22
|
+
function resolveEngine(deckDir) {
|
|
23
|
+
const bundleDir = resolveBundleDirForDeck(deckDir);
|
|
24
|
+
if (!bundleDir) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`No materialized design bundle for deck ${path.basename(deckDir)}.\n` +
|
|
27
|
+
` Run 'slidemind download-bundle ${path.basename(deckDir)}' first.`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const enginePath = path.join(bundleDir, "lint-engine.mjs");
|
|
31
|
+
const metaPath = path.join(bundleDir, "bundle.json");
|
|
32
|
+
let meta = {};
|
|
33
|
+
try {
|
|
34
|
+
meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
35
|
+
} catch {
|
|
36
|
+
/* treated as a pre-engine cache below */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(enginePath)) {
|
|
40
|
+
if (meta.lintEngineVersion === null) {
|
|
41
|
+
// The backend answered but served no engine: it predates the feature.
|
|
42
|
+
throw new Error(
|
|
43
|
+
`The paired slidemind add-in does not serve a lint engine yet.\n` +
|
|
44
|
+
` Update/redeploy the add-in backend, then re-run ` +
|
|
45
|
+
`'slidemind download-bundle ${path.basename(deckDir)}'.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
// Cache written before the engine existed (meta lacks the key), or the
|
|
49
|
+
// file went missing — refetch repairs both.
|
|
50
|
+
throw new Error(
|
|
51
|
+
`This bundle cache predates the lint engine.\n` +
|
|
52
|
+
` Run 'slidemind download-bundle ${path.basename(deckDir)}'.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let primitives = {};
|
|
57
|
+
try {
|
|
58
|
+
primitives = JSON.parse(
|
|
59
|
+
fs.readFileSync(path.join(bundleDir, "primitives.json"), "utf8"),
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
// The registry is closed and deliberately empty today — lint still works,
|
|
63
|
+
// but flag it so a broken materialization doesn't pass silently.
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
"warning: bundle has no readable primitives.json — linting with an empty registry\n",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { enginePath, version: meta.lintEngineVersion ?? "0", primitives };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Lint slide files with the add-in's engine. `layoutSources` is the registry
|
|
73
|
+
// input the engine derives layout component names from: Map<"Cover.jsx", raw
|
|
74
|
+
// JSX source> — same shape the add-in's in-app lint feeds it.
|
|
75
|
+
// Prints findings to stderr; throws when any [lint:error] is present.
|
|
76
|
+
export async function lintSlidesOrThrow({ deckDir, slideFiles, layoutSources }) {
|
|
77
|
+
const { enginePath, version, primitives } = resolveEngine(deckDir);
|
|
78
|
+
|
|
79
|
+
// `?v=` busts Node's per-process module cache so a long-lived process (the
|
|
80
|
+
// MCP server) picks up a refreshed engine at the same path.
|
|
81
|
+
const engine = await import(
|
|
82
|
+
`${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
|
|
83
|
+
);
|
|
84
|
+
if (typeof engine.lintSlidesInMemory !== "function") {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Cached lint engine is corrupt (no lintSlidesInMemory export).\n` +
|
|
87
|
+
` Run 'slidemind download-bundle ${path.basename(deckDir)}'.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const slides = slideFiles.map((file) => ({
|
|
92
|
+
fileName: path.basename(file),
|
|
93
|
+
source: fs.readFileSync(file, "utf8"),
|
|
94
|
+
}));
|
|
95
|
+
const result = engine.lintSlidesInMemory(slides, {
|
|
96
|
+
layoutSources,
|
|
97
|
+
primitives,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for (const line of result.lines) {
|
|
101
|
+
process.stderr.write(`${line}\n`);
|
|
102
|
+
}
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
const summary =
|
|
105
|
+
`slide lint: ${result.errors} error${result.errors === 1 ? "" : "s"}` +
|
|
106
|
+
(result.warnings
|
|
107
|
+
? `, ${result.warnings} warning${result.warnings === 1 ? "" : "s"}`
|
|
108
|
+
: "");
|
|
109
|
+
throw new Error(`${summary}\n${result.lines.join("\n")}`);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Discover which skill a deck was authored against, and return the paths the
|
|
2
|
-
// rest of the local preview pipeline (build.mjs
|
|
3
|
-
//
|
|
2
|
+
// rest of the local preview pipeline (build.mjs) needs to load layouts,
|
|
3
|
+
// tokens and assets from. (The lint registry's primitives.json now rides in
|
|
4
|
+
// the materialized bundle next to the add-in's lint engine — see
|
|
5
|
+
// src/builders/lint.mjs.)
|
|
4
6
|
//
|
|
5
7
|
// Post-connect, the corporate design (css, layouts, assets) is no longer
|
|
6
8
|
// shipped inside the skill — it lives in the slidemind add-in and is
|
|
@@ -31,12 +33,6 @@ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
|
31
33
|
|
|
32
34
|
const DEFAULT_SKILL = "create-slides";
|
|
33
35
|
|
|
34
|
-
// Shipped fallback layout-contract registry for the LOCAL HTML preview.
|
|
35
|
-
// The add-in derives layout contracts from the bundle's layouts directly, so
|
|
36
|
-
// this is only consulted by the local preview when neither the deck nor the
|
|
37
|
-
// materialized bundle carries its own primitives.json.
|
|
38
|
-
const SHIPPED_PRIMITIVES = path.join(__dirname, "primitives.json");
|
|
39
|
-
|
|
40
36
|
function firstExisting(candidates) {
|
|
41
37
|
for (const c of candidates) {
|
|
42
38
|
if (c && fs.existsSync(c)) return c;
|
|
@@ -104,9 +100,6 @@ export function resolveSkill(skillName, planMeta = {}, opts = {}) {
|
|
|
104
100
|
|
|
105
101
|
const deckLayouts = opts.deckDir ? path.join(opts.deckDir, "layouts") : null;
|
|
106
102
|
const deckAssets = opts.deckDir ? path.join(opts.deckDir, "assets") : null;
|
|
107
|
-
const deckPrimitives = opts.deckDir
|
|
108
|
-
? path.join(opts.deckDir, "primitives.json")
|
|
109
|
-
: null;
|
|
110
103
|
|
|
111
104
|
const cssFile = firstExistingOr(
|
|
112
105
|
designRoots.map((r) => path.join(r, cssName)),
|
|
@@ -124,23 +117,26 @@ export function resolveSkill(skillName, planMeta = {}, opts = {}) {
|
|
|
124
117
|
[deckAssets, ...designRoots.map((r) => path.join(r, "assets"))],
|
|
125
118
|
path.join(designRoots[0], "assets"),
|
|
126
119
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
120
|
+
// Brand assets ALWAYS resolve from the design root (the bundle): logos, decor,
|
|
121
|
+
// and the per-layout chrome backgrounds (`assets/chrome-*`). They must be
|
|
122
|
+
// resolved INDEPENDENTLY of the deck — `assetsDir` above prefers the deck's own
|
|
123
|
+
// `assets/` (engine downloads), so if `brand.assetsDir` mirrored it, a deck that
|
|
124
|
+
// already has any local asset would collapse both layers to the deck dir and
|
|
125
|
+
// copyDesignSystem would never copy the bundle's brand assets (chrome included)
|
|
126
|
+
// into the preview. Keeping the brand layer on the bundle lets the deck's assets
|
|
127
|
+
// layer ON TOP and override by name, without suppressing the brand chrome.
|
|
128
|
+
const brandAssetsDir = firstExistingOr(
|
|
129
|
+
designRoots.map((r) => path.join(r, "assets")),
|
|
130
|
+
path.join(designRoots[0], "assets"),
|
|
134
131
|
);
|
|
135
132
|
// One truth: brand paths mirror the resolved design roots (the bundle IS the
|
|
136
133
|
// brand source; there is no separate brand skill anymore).
|
|
137
134
|
const brand = {
|
|
138
135
|
skillDir: designRoots[0],
|
|
139
|
-
assetsDir,
|
|
136
|
+
assetsDir: brandAssetsDir,
|
|
140
137
|
cssFile,
|
|
141
138
|
layoutsDir,
|
|
142
139
|
layoutsCssFile,
|
|
143
|
-
primitivesPath,
|
|
144
140
|
};
|
|
145
141
|
|
|
146
142
|
// The PPTX slide-master template only fed the now-archived local exporter;
|
|
@@ -167,7 +163,6 @@ export function resolveSkill(skillName, planMeta = {}, opts = {}) {
|
|
|
167
163
|
cssFile,
|
|
168
164
|
layoutsDir,
|
|
169
165
|
assetsDir,
|
|
170
|
-
primitivesPath,
|
|
171
166
|
brand,
|
|
172
167
|
templateKey,
|
|
173
168
|
templatePath,
|