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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { isClaudePluginInstall } from "../install-channel.mjs";
|
|
5
|
+
import { npmBin } from "../update-check.mjs";
|
|
6
|
+
import { readPackageJson, fail, PACKAGE_ROOT, SRC_DIR, INSTALLERS_DIR } from "./common.mjs";
|
|
7
|
+
|
|
8
|
+
export const HARNESS_TARGETS = ["claude", "codex", "opencode"];
|
|
9
|
+
|
|
10
|
+
export function parseTargetFlag(rest, { allowAuto = false } = {}) {
|
|
11
|
+
const idx = rest.findIndex((a) => a === "--for" || a === "-f");
|
|
12
|
+
if (idx < 0 || !rest[idx + 1]) {
|
|
13
|
+
fail(
|
|
14
|
+
`missing --for <claude|codex|opencode|all${allowAuto ? "|auto" : ""}>`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const target = rest[idx + 1];
|
|
18
|
+
const allowed = allowAuto
|
|
19
|
+
? [...HARNESS_TARGETS, "all", "auto"]
|
|
20
|
+
: [...HARNESS_TARGETS, "all"];
|
|
21
|
+
if (!allowed.includes(target)) {
|
|
22
|
+
fail(`unknown install target: ${target}`);
|
|
23
|
+
}
|
|
24
|
+
return target;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Which coding agents are actually on this machine, and which of them already
|
|
28
|
+
// get their skills from a plugin. This is what lets `slidemind setup` finish the
|
|
29
|
+
// job on its own: it deploys where a harness exists and stays out of the way
|
|
30
|
+
// where a plugin already supplies the skills, so a separate `init` is no longer
|
|
31
|
+
// part of onboarding.
|
|
32
|
+
export async function detectHarnesses() {
|
|
33
|
+
const found = [];
|
|
34
|
+
for (const target of HARNESS_TARGETS) {
|
|
35
|
+
const mod = await import(
|
|
36
|
+
pathToFileURL(path.join(INSTALLERS_DIR, `${target}.mjs`)).href
|
|
37
|
+
);
|
|
38
|
+
const info =
|
|
39
|
+
typeof mod.detect === "function"
|
|
40
|
+
? mod.detect()
|
|
41
|
+
: { present: false, pluginInstalled: false, root: null };
|
|
42
|
+
found.push({ target, ...info });
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Expand a `--for` value into the concrete harness list to act on. */
|
|
48
|
+
|
|
49
|
+
export async function resolveInstallTargets(target) {
|
|
50
|
+
if (target === "all") return [...HARNESS_TARGETS];
|
|
51
|
+
if (target !== "auto") return [target];
|
|
52
|
+
return (await detectHarnesses())
|
|
53
|
+
.filter((h) => h.present && !h.pluginInstalled)
|
|
54
|
+
.map((h) => h.target);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runInstaller(target, action, rest) {
|
|
58
|
+
const targets = await resolveInstallTargets(target);
|
|
59
|
+
for (const t of targets) {
|
|
60
|
+
const mod = await import(
|
|
61
|
+
pathToFileURL(path.join(INSTALLERS_DIR, `${t}.mjs`)).href
|
|
62
|
+
);
|
|
63
|
+
const fn = mod[action];
|
|
64
|
+
if (typeof fn !== "function") {
|
|
65
|
+
fail(`installer for '${t}' does not implement '${action}'`);
|
|
66
|
+
}
|
|
67
|
+
await fn({
|
|
68
|
+
packageRoot: PACKAGE_ROOT,
|
|
69
|
+
flags: rest,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return targets;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadInstallerUi() {
|
|
76
|
+
const mod = await import(
|
|
77
|
+
pathToFileURL(path.join(INSTALLERS_DIR, "lib.mjs")).href
|
|
78
|
+
);
|
|
79
|
+
return mod.ui;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function cmdInstall(rest) {
|
|
83
|
+
const target = parseTargetFlag(rest, { allowAuto: true });
|
|
84
|
+
const ui = await loadInstallerUi();
|
|
85
|
+
ui.banner(readPackageJson().version, "init");
|
|
86
|
+
const deployed = await runInstaller(target, "install", rest);
|
|
87
|
+
if (deployed.length === 0) {
|
|
88
|
+
ui.step({
|
|
89
|
+
status: "warn",
|
|
90
|
+
label: "harnesses",
|
|
91
|
+
value: "none detected",
|
|
92
|
+
note: "install a coding agent, then rerun with --for <claude|codex|opencode>",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Force every design bundle to re-download from the add-in on the next
|
|
97
|
+
// build/push/download by wiping the materialized cache. init is the
|
|
98
|
+
// deliberate refresh point (and `slidemind update` redeploys through it), so
|
|
99
|
+
// a stale binary or an orphaned per-presentation dir never outlives a re-init.
|
|
100
|
+
const bundle = await import(
|
|
101
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
102
|
+
);
|
|
103
|
+
const cleared = bundle.clearBundleCache();
|
|
104
|
+
await ui.section("cache", "design bundles");
|
|
105
|
+
ui.step({
|
|
106
|
+
label: "clear",
|
|
107
|
+
value: cleared ? `${cleared} bundle${cleared === 1 ? "" : "s"}` : "empty",
|
|
108
|
+
note: "re-downloads on next build",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
ui.done(
|
|
112
|
+
deployed.length === 0
|
|
113
|
+
? "no coding agent found — the CLI is installed, skills are not"
|
|
114
|
+
: deployed.length === HARNESS_TARGETS.length
|
|
115
|
+
? "ready — ask your agent to plan, create, or edit a deck"
|
|
116
|
+
: `ready — ${deployed.join(", ")} can now plan, create, or edit decks`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function cmdUninstall(rest) {
|
|
121
|
+
const target = parseTargetFlag(rest);
|
|
122
|
+
const ui = await loadInstallerUi();
|
|
123
|
+
ui.banner(readPackageJson().version, "uninstall");
|
|
124
|
+
await runInstaller(target, "uninstall", rest);
|
|
125
|
+
|
|
126
|
+
// Drop the materialized bundle cache too — uninstall is a teardown, so leaving
|
|
127
|
+
// a per-presentation dir (with brand assets) behind would be stale residue.
|
|
128
|
+
const bundle = await import(
|
|
129
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
130
|
+
);
|
|
131
|
+
const cleared = bundle.clearBundleCache();
|
|
132
|
+
await ui.section("cache", "design bundles");
|
|
133
|
+
ui.step({
|
|
134
|
+
status: "remove",
|
|
135
|
+
label: "clear",
|
|
136
|
+
value: cleared ? `${cleared} bundle${cleared === 1 ? "" : "s"}` : "empty",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ui.done(target === "all" ? "removed from all harnesses" : `removed from ${target}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// On Windows we must spawn through the shell (Node refuses to exec .cmd
|
|
143
|
+
// shims directly since CVE-2024-27980), and `shell: true` joins file + args
|
|
144
|
+
// into one cmd.exe command string WITHOUT quoting — so an unquoted
|
|
145
|
+
// `C:\Program Files\nodejs\npm.cmd` splits at the space and cmd tries to run
|
|
146
|
+
// `C:\Program`. Quote anything containing whitespace before handing it over.
|
|
147
|
+
export function cmdQuote(s) {
|
|
148
|
+
return /\s/.test(s) ? `"${s}"` : s;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function spawnInherit(file, args) {
|
|
152
|
+
const useShell = process.platform === "win32";
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const child = spawn(
|
|
155
|
+
useShell ? cmdQuote(file) : file,
|
|
156
|
+
useShell ? args.map(cmdQuote) : args,
|
|
157
|
+
{
|
|
158
|
+
stdio: "inherit",
|
|
159
|
+
env: process.env,
|
|
160
|
+
shell: useShell,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
child.on("error", reject);
|
|
164
|
+
child.on("exit", (code) => {
|
|
165
|
+
if (code === 0) resolve();
|
|
166
|
+
else reject(new Error(`${file} ${args.join(" ")} exited with code ${code}`));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Like spawnInherit, but resolves with the child's exit code instead of
|
|
172
|
+
// rejecting — for a step whose failure is a message to the user rather than a
|
|
173
|
+
// failure of the command that ran it (see `setup` → `ready`).
|
|
174
|
+
export function spawnStatus(file, args) {
|
|
175
|
+
return spawnInherit(file, args).then(
|
|
176
|
+
() => 0,
|
|
177
|
+
() => 1,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//
|
|
182
|
+
// slidemind is published to public npm as `slidemind`. First install is
|
|
183
|
+
// `npm install -g slidemind` (then `slidemind init` to deploy the skill;
|
|
184
|
+
// `install` stays as a back-compat alias); `slidemind update` is a thin
|
|
185
|
+
// wrapper over `npm install -g slidemind@latest`.
|
|
186
|
+
|
|
187
|
+
export async function cmdUpdate(rest) {
|
|
188
|
+
let target = "all";
|
|
189
|
+
let ref = null;
|
|
190
|
+
for (let i = 0; i < rest.length; i++) {
|
|
191
|
+
const a = rest[i];
|
|
192
|
+
if (a === "--for" || a === "-f") {
|
|
193
|
+
target = rest[++i];
|
|
194
|
+
if (!target) fail("--for expects a target");
|
|
195
|
+
if (!["claude", "codex", "opencode", "all", "none"].includes(target)) {
|
|
196
|
+
fail(`unknown install target: ${target}`);
|
|
197
|
+
}
|
|
198
|
+
} else if (a === "--ref") {
|
|
199
|
+
ref = rest[++i];
|
|
200
|
+
if (!ref) fail("--ref expects a version tag (e.g. v0.1.0)");
|
|
201
|
+
} else {
|
|
202
|
+
fail(`unknown flag: ${a}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const pkg = readPackageJson();
|
|
207
|
+
const ui = await loadInstallerUi();
|
|
208
|
+
ui.banner(pkg.version, "update");
|
|
209
|
+
|
|
210
|
+
// A plugin-installed CLI cannot update itself this way: npm would drop a
|
|
211
|
+
// second copy into the global prefix while the plugin session keeps running
|
|
212
|
+
// its own. Say so instead of letting the user believe they are now current —
|
|
213
|
+
// the global install is still worth doing (it is what a plain terminal
|
|
214
|
+
// outside Claude Code picks up), so this warns and continues.
|
|
215
|
+
if (isClaudePluginInstall()) {
|
|
216
|
+
ui.step({
|
|
217
|
+
status: "warn",
|
|
218
|
+
label: "plugin",
|
|
219
|
+
value:
|
|
220
|
+
"this slidemind came from the Claude Code plugin — update it with `/plugin` → slidemind → update; the global install below does NOT replace it inside plugin sessions",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Upgrade the global install straight from public npm. `@latest` (or a pinned
|
|
225
|
+
// version) lets npm resolve + verify the tarball — no SAS, no checksum dance.
|
|
226
|
+
const spec = ref
|
|
227
|
+
? `slidemind@${ref.replace(/^v/, "")}`
|
|
228
|
+
: "slidemind@latest";
|
|
229
|
+
await ui.section("release", ref ? `pinned ${spec}` : "latest");
|
|
230
|
+
ui.step({ label: "install", value: spec, note: "global (npm)" });
|
|
231
|
+
try {
|
|
232
|
+
await spawnInherit(npmBin(), ["install", "-g", "--loglevel=error", spec]);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error(
|
|
235
|
+
"\nslidemind update could not write to npm's global package directory.\n" +
|
|
236
|
+
"This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
|
|
237
|
+
"Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
|
|
238
|
+
" mkdir -p ~/.npm-global\n" +
|
|
239
|
+
" npm config set prefix ~/.npm-global\n" +
|
|
240
|
+
" echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
|
|
241
|
+
" source ~/.zshrc\n" +
|
|
242
|
+
" npm install -g slidemind\n\n" +
|
|
243
|
+
"Then run `slidemind init --for all`.\n",
|
|
244
|
+
);
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (target !== "none") {
|
|
249
|
+
await spawnInherit("slidemind", ["init", "--for", target]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
ui.done(
|
|
253
|
+
target === "none"
|
|
254
|
+
? "updated — skill files unchanged (run 'slidemind init --for all' when ready)"
|
|
255
|
+
: `updated and redeployed into ${target === "all" ? "all harnesses" : target}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
package/src/cli/read.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { fail, LOCAL_PPTX_RE, SRC_DIR } from "./common.mjs";
|
|
5
|
+
|
|
6
|
+
// Print a read result — the `{ ok, mode, slideCount, slides[] }` shape, whether
|
|
7
|
+
// it came from the add-in (open presentation) or a local .pptx. Shared by both
|
|
8
|
+
// `cmdRead` branches so their output is byte-for-byte identical.
|
|
9
|
+
export function printReadResult(res, asJson) {
|
|
10
|
+
if (asJson) {
|
|
11
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const slides = Array.isArray(res.slides) ? res.slides : [];
|
|
15
|
+
if (slides.length === 0) {
|
|
16
|
+
process.stderr.write("(presentation has no slides)\n");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (res.mode === "summary") {
|
|
20
|
+
for (const s of slides) {
|
|
21
|
+
const label = s.title || "(untitled)";
|
|
22
|
+
const sub = s.subtitle ? ` — ${s.subtitle}` : "";
|
|
23
|
+
const layout = s.layout ? ` [${s.layout}]` : "";
|
|
24
|
+
process.stdout.write(
|
|
25
|
+
`${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
slides.forEach((s, i) => {
|
|
30
|
+
if (i > 0) process.stdout.write("\n");
|
|
31
|
+
const layout = s.layout ? ` (${s.layout})` : "";
|
|
32
|
+
process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
|
|
33
|
+
process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Pull the current content of an open presentation. Content is window-bound, so
|
|
39
|
+
// pick exactly one window (positional presId or --to; pickPresentationEntry errors
|
|
40
|
+
// listing the windows if several are open and none was named). `--mode summary`
|
|
41
|
+
// prints a per-slide outline; default `raw` prints each slide's plain text.
|
|
42
|
+
//
|
|
43
|
+
// A positional arg with a local PowerPoint extension (LOCAL_PPTX_RE: .pptx and
|
|
44
|
+
// its .pptm/.potx/.potm/.ppsx/.ppsm siblings) AND resolving to an existing
|
|
45
|
+
// regular file is read host-free from disk instead (no bridge). The gate is
|
|
46
|
+
// strict: a nonexistent path or a real presId falls through to the host path
|
|
47
|
+
// unchanged, and it is skipped when --to is given (--to targets an open window).
|
|
48
|
+
export async function cmdRead(rest) {
|
|
49
|
+
let presId = null;
|
|
50
|
+
let mode = "raw";
|
|
51
|
+
let asJson = false;
|
|
52
|
+
let toGiven = false;
|
|
53
|
+
let fileArg = null; // the positional (a candidate local .pptx path)
|
|
54
|
+
for (let i = 0; i < rest.length; i++) {
|
|
55
|
+
const a = rest[i];
|
|
56
|
+
if (a === "--mode") {
|
|
57
|
+
const next = rest[++i];
|
|
58
|
+
if (next !== "summary" && next !== "raw") {
|
|
59
|
+
fail(`--mode expects 'summary' or 'raw', got '${next ?? ""}'`);
|
|
60
|
+
}
|
|
61
|
+
mode = next;
|
|
62
|
+
} else if (a === "--to") {
|
|
63
|
+
presId = rest[++i];
|
|
64
|
+
if (!presId) fail("--to expects a presId");
|
|
65
|
+
toGiven = true;
|
|
66
|
+
} else if (a === "--json") {
|
|
67
|
+
asJson = true;
|
|
68
|
+
} else if (a.startsWith("-")) {
|
|
69
|
+
fail(`unknown flag: ${a}`);
|
|
70
|
+
} else if (!presId) {
|
|
71
|
+
presId = a; // positional presId (or local .pptx path)
|
|
72
|
+
fileArg = a;
|
|
73
|
+
} else {
|
|
74
|
+
fail(`unexpected argument: ${a}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Local-file gate: applies ONLY to the positional, and never when --to is set.
|
|
79
|
+
// The positional must end in .pptx and resolve to an existing regular file;
|
|
80
|
+
// anything else (missing path, real presId) falls through to the host path.
|
|
81
|
+
if (fileArg && !toGiven && LOCAL_PPTX_RE.test(fileArg)) {
|
|
82
|
+
const p = path.isAbsolute(fileArg)
|
|
83
|
+
? fileArg
|
|
84
|
+
: path.resolve(process.cwd(), fileArg);
|
|
85
|
+
let st = null;
|
|
86
|
+
try {
|
|
87
|
+
st = fs.statSync(p);
|
|
88
|
+
} catch {
|
|
89
|
+
st = null;
|
|
90
|
+
}
|
|
91
|
+
if (st && st.isFile()) {
|
|
92
|
+
const { readLocalPptx } = await import(
|
|
93
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "read-local.mjs")).href
|
|
94
|
+
);
|
|
95
|
+
let res;
|
|
96
|
+
try {
|
|
97
|
+
res = readLocalPptx(fs.readFileSync(p), { mode });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
fail(`read failed: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
printReadResult(res, asJson);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const client = await import(
|
|
107
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
108
|
+
);
|
|
109
|
+
const registry = await client.requirePaneHost();
|
|
110
|
+
const entry = client.pickPresentationEntry(registry, presId);
|
|
111
|
+
const res = await client.getPresentationContent({
|
|
112
|
+
presId: entry.presId,
|
|
113
|
+
mode,
|
|
114
|
+
backendURL: entry.backendURL ?? null,
|
|
115
|
+
});
|
|
116
|
+
if (!res?.ok) {
|
|
117
|
+
fail(`read failed: ${res?.error ?? "unknown error"}`);
|
|
118
|
+
}
|
|
119
|
+
printReadResult(res, asJson);
|
|
120
|
+
}
|