slidemind 0.2.0 → 0.2.2
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/.agents/plugins/marketplace.json +29 -0
- package/.claude-plugin/marketplace.json +25 -0
- package/.claude-plugin/plugin.json +6 -3
- package/.codex-plugin/plugin.json +30 -0
- package/{CLAUDE.md → AGENTS.md} +50 -8
- package/README.md +28 -0
- package/bin/slidemind +7 -0
- package/bin/slidemind.cmd +2 -0
- package/bin/slidemind.mjs +37 -5
- package/package.json +4 -2
- package/src/bridge/bundle.mjs +2 -1
- package/src/bridge/client.mjs +2 -2
- package/src/builders/build.mjs +3 -3
- package/src/builders/resolve-skill.mjs +66 -6
- package/src/install-channel.mjs +61 -0
- package/src/installers/claude.mjs +22 -0
- package/src/pptx/fill/engine.mjs +6 -3
- package/src/pptx/fill/freeform.mjs +36 -22
- package/src/pptx/fill/parse-slides.mjs +56 -22
- package/src/pptx/fill/xml.mjs +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slidemind",
|
|
3
|
+
"interface": {
|
|
4
|
+
"displayName": "slidemind",
|
|
5
|
+
"shortDescription": "On-brand PowerPoint decks from your coding agent",
|
|
6
|
+
"longDescription": "slidemind connects Codex to the slidemind PowerPoint add-in over its local bridge, so the agent can build and edit native .pptx decks against whatever corporate design the add-in has active. It is brand-agnostic: the corporate design, the layout palette, and the OOXML render all live in the add-in, and the agent reads every brand rule from the design bundle it serves. This marketplace publishes the slidemind plugin, which carries the authoring and search skills. The slidemind CLI itself is installed separately from public npm (npm install -g slidemind); the skills explain that step when the command is missing."
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "slidemind",
|
|
11
|
+
"description": "Build and edit native, on-brand PowerPoint decks against the active corporate design.",
|
|
12
|
+
"source": {
|
|
13
|
+
"source": "local",
|
|
14
|
+
"path": "./"
|
|
15
|
+
},
|
|
16
|
+
"policy": {
|
|
17
|
+
"installation": "AVAILABLE",
|
|
18
|
+
"authentication": "ON_INSTALL"
|
|
19
|
+
},
|
|
20
|
+
"category": "Productivity",
|
|
21
|
+
"interface": {
|
|
22
|
+
"displayName": "slidemind",
|
|
23
|
+
"shortDescription": "On-brand PowerPoint decks from Codex",
|
|
24
|
+
"developerName": "statworx",
|
|
25
|
+
"longDescription": "Adds two skills. slidemind-create-slides owns the intake, plan, create and edit workflow that turns per-slide sources into an editable .pptx rendered by the add-in, plus a local HTML preview. slidemind-search discovers and reuses existing slides and brand imagery from the corporate design's engines, or from a PowerPoint file on disk. Both are brand-agnostic and read the active corporate design's rules, layout palette, and authoring workflow from the design bundle the add-in serves. Requires the slidemind CLI (npm install -g slidemind) and an open PowerPoint with the add-in for anything that renders or reads a live deck."
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slidemind",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "statworx",
|
|
5
|
+
"url": "https://www.statworx.com"
|
|
6
|
+
},
|
|
7
|
+
"description": "slidemind — on-brand PowerPoint decks from your coding agent, via the slidemind add-in",
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "slidemind",
|
|
11
|
+
"source": {
|
|
12
|
+
"source": "npm",
|
|
13
|
+
"package": "slidemind"
|
|
14
|
+
},
|
|
15
|
+
"description": "Build and edit native, on-brand PowerPoint decks against the active corporate design. Ships the slidemind CLI (on PATH inside Claude Code sessions) and both skills.",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "statworx"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/STATWORX/intern-slidemind-cli",
|
|
20
|
+
"repository": "https://github.com/STATWORX/intern-slidemind-cli",
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"category": "productivity"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Connect Claude Code
|
|
3
|
+
"version": "0.2.2",
|
|
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
|
-
"name": "statworx"
|
|
6
|
+
"name": "statworx",
|
|
7
|
+
"url": "https://www.statworx.com"
|
|
7
8
|
},
|
|
8
9
|
"homepage": "https://github.com/STATWORX/intern-slidemind-cli",
|
|
10
|
+
"repository": "https://github.com/STATWORX/intern-slidemind-cli",
|
|
11
|
+
"license": "UNLICENSED",
|
|
9
12
|
"keywords": [
|
|
10
13
|
"presentation",
|
|
11
14
|
"slides",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slidemind",
|
|
3
|
+
"version": "0.2.2",
|
|
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
|
+
"author": {
|
|
6
|
+
"name": "statworx",
|
|
7
|
+
"url": "https://www.statworx.com"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/STATWORX/intern-slidemind-cli",
|
|
10
|
+
"repository": "https://github.com/STATWORX/intern-slidemind-cli",
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"presentation",
|
|
14
|
+
"slides",
|
|
15
|
+
"powerpoint",
|
|
16
|
+
"office-addin",
|
|
17
|
+
"slidemind"
|
|
18
|
+
],
|
|
19
|
+
"skills": "./skills",
|
|
20
|
+
"interface": {
|
|
21
|
+
"displayName": "slidemind",
|
|
22
|
+
"shortDescription": "On-brand PowerPoint decks from Codex",
|
|
23
|
+
"longDescription": "Adds two skills. slidemind-create-slides owns the intake, plan, create and edit workflow that turns per-slide sources into an editable .pptx rendered by the add-in, plus a local HTML preview. slidemind-search discovers and reuses existing slides and brand imagery from the corporate design's engines, or from a PowerPoint file on disk. Both are brand-agnostic and read the active corporate design's rules, layout palette, and authoring workflow from the design bundle the add-in serves. Requires the slidemind CLI (npm install -g slidemind) and an open PowerPoint with the add-in for anything that renders or reads a live deck.",
|
|
24
|
+
"developerName": "statworx",
|
|
25
|
+
"category": "Productivity",
|
|
26
|
+
"capabilities": [
|
|
27
|
+
"Skills"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
package/{CLAUDE.md → AGENTS.md}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Guidance for coding agents working in this repository. `CLAUDE.md` is a symlink to this file — there is ONE guidance document, so edit it here and every harness sees the change. (It used to be two near-identical files kept in sync by hand, which drifted.)
|
|
4
4
|
|
|
5
5
|
## Project purpose
|
|
6
6
|
|
|
@@ -24,9 +24,25 @@ npm install -g slidemind
|
|
|
24
24
|
slidemind init --for all # claude | codex | opencode | all | none
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`npx slidemind <command>` runs it without a global install. `slidemind update` is a thin wrapper over `npm install -g slidemind@latest` (`--ref <version>` to pin, `--for <target>` to control the redeploy). The `files` whitelist in `package.json` controls what ships (`bin/ src/ skills/ .claude-plugin/
|
|
27
|
+
`npx slidemind <command>` runs it without a global install. `slidemind update` is a thin wrapper over `npm install -g slidemind@latest` (`--ref <version>` to pin, `--for <target>` to control the redeploy). The `files` whitelist in `package.json` controls what ships (`bin/ src/ skills/ .claude-plugin/ .codex-plugin/ .agents/plugins/ AGENTS.md README.md`); `archive/`, `docs/`, `scripts/`, and `test/` are excluded. Two traps live in that list:
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
- `.agents/plugins/`, **not** `.agents/` — `files` globs the filesystem rather than git, and `.agents/` also holds gitignored per-developer harness scratch that a bare entry would publish.
|
|
30
|
+
- `AGENTS.md`, **not** `CLAUDE.md` — `CLAUDE.md` is a symlink and **npm silently drops symlinks from the tarball**, so listing it would ship nothing at all. The repo keeps the symlink for local harness convenience; the package ships the real `AGENTS.md`.
|
|
31
|
+
|
|
32
|
+
A packaging test guards both.
|
|
33
|
+
|
|
34
|
+
Both agent harnesses can also install slidemind as a **plugin** — this private repo doubles as the marketplace (a public marketplace repo can be split out later; the files move as-is). The marketplace is named `slidemind`, NOT `statworx`: the team already registers a `statworx` marketplace from `STATWORX/intern-marketplace-test`, and both harnesses allow only one marketplace per name.
|
|
35
|
+
|
|
36
|
+
- **Claude Code** (one step, no `npm -g`, **no GitHub account**): `/plugin marketplace add https://unpkg.com/slidemind/.claude-plugin/marketplace.json` → `/plugin install slidemind@slidemind`. A Claude marketplace source may be a plain HTTPS URL, and an npm CDN serves files straight out of the published tarball — which ships `.claude-plugin/marketplace.json`. So the marketplace needs no repo of its own and follows releases automatically (no version in the URL, no version pin in the plugin entry). `https://cdn.jsdelivr.net/npm/slidemind/.claude-plugin/marketplace.json` is the equivalent fallback. The tarball's `bin/slidemind` + `bin/slidemind.cmd` shims land on PATH inside plugin sessions, so the plugin carries the CLI too. (`/plugin marketplace add STATWORX/intern-slidemind-cli` also works but needs read access to this private repo — prefer the URL.)
|
|
37
|
+
- **Codex**: a Codex marketplace source must be a **local path or a git remote** — no URL, no npm — so the CDN trick does NOT transfer. Instead the tarball ships `.agents/plugins/marketplace.json`, which makes the INSTALLED package a valid local marketplace: `npm install -g slidemind` → `codex plugin marketplace add "$(npm root -g)/slidemind"` → `codex plugin add slidemind@slidemind`. No GitHub account needed. (`codex plugin marketplace add STATWORX/intern-slidemind-cli` works for anyone with repo access.) Codex plugins cannot put a binary on PATH, so the CLI always comes from npm; the skills' remediation covers that.
|
|
38
|
+
|
|
39
|
+
> The Codex **admin console** validates harder than the CLI does: it rejects an import when any `interface` block lacks a non-blank `longDescription`, which `codex plugin add` never complains about. `test/plugin-manifests.test.mjs` guards it.
|
|
40
|
+
|
|
41
|
+
`slidemind init --for <target>` remains the universal fallback on all three harnesses; the Claude installer prints a dedup hint when it detects the plugin is also installed.
|
|
42
|
+
|
|
43
|
+
**Updating a plugin-installed CLI is `/plugin`, not `slidemind update`.** The Claude plugin ships its own copy of the CLI on the session's PATH, so `npm install -g slidemind@latest` drops a second, unrelated global and the session keeps running the old binary — a user following that advice would watch the update succeed and stay blocked on the add-in's version floor. `src/install-channel.mjs` detects the channel from the running code's path (plus `CLAUDE_PLUGIN_ROOT`) and every too-old-CLI remediation ends with `updateCliInstruction()`, so the message names the updater that actually replaces this binary; the `cli_update_required` rung of the readiness ladder also flips `actor` to `user` there, because the agent cannot drive `/plugin`.
|
|
44
|
+
|
|
45
|
+
> The previous account-free channel (a private Azure Blob container + shared read SAS + `scripts/install.sh` curl-bootstrap) has been **retired** — see `archive/scripts/install.sh`. npm is the sole binary channel now; the plugin marketplaces layer on top of it.
|
|
30
46
|
|
|
31
47
|
## Releasing
|
|
32
48
|
|
|
@@ -36,6 +52,8 @@ Releases are **automatic on every push to `main`**. The workflow at `.github/wor
|
|
|
36
52
|
2. Otherwise → bumps the patch by 1, commits the bumped `package.json` back to `main`, then tags the new version. (No infinite loop: GitHub doesn't trigger workflows from commits made with the default `GITHUB_TOKEN`, and the workflow also skips if the actor is `github-actions[bot]`.)
|
|
37
53
|
3. In both cases → creates a GitHub Release with auto-generated notes, then **`npm publish --access public`**.
|
|
38
54
|
|
|
55
|
+
Before tagging/publishing the workflow runs `scripts/sync-plugin-manifests.mjs`, which copies `package.json`'s version into `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` (Claude Code keys plugin updates off that version; Codex keys its plugin cache off it — the marketplace entries deliberately carry no version so they never drift). `scripts/release.mjs` runs the same sync locally, and `test/plugin-manifests.test.mjs` fails CI on drift.
|
|
56
|
+
|
|
39
57
|
The publish step is gated on the `NPM_TOKEN` repo secret (an npm automation token with publish rights to `slidemind`): until it's set the workflow warns and skips publish (so tagging/releases keep working during rollout); once set, a publish failure fails the build. So most of the time you just merge to `main` and a new patch release falls out to npm.
|
|
40
58
|
|
|
41
59
|
For a **minor or major** bump, run the local helper before pushing:
|
|
@@ -62,11 +80,17 @@ Teammates pick up the release with `npm install -g slidemind@latest` (or `slidem
|
|
|
62
80
|
slidemind/
|
|
63
81
|
├── package.json npm package metadata, single bin: slidemind
|
|
64
82
|
├── README.md end-user quickstart
|
|
65
|
-
├──
|
|
83
|
+
├── AGENTS.md this file (CLAUDE.md is a symlink to it)
|
|
66
84
|
├── .claude-plugin/
|
|
67
|
-
│
|
|
85
|
+
│ ├── plugin.json Claude Code plugin manifest (version release-synced to package.json)
|
|
86
|
+
│ └── marketplace.json Claude Code marketplace — serves the plugin from public npm, unpinned
|
|
87
|
+
├── .codex-plugin/
|
|
88
|
+
│ └── plugin.json Codex plugin manifest (version release-synced)
|
|
89
|
+
├── .agents/
|
|
90
|
+
│ └── plugins/marketplace.json Codex marketplace — serves this repo (git clone) as the plugin; NOT in the npm tarball
|
|
68
91
|
├── bin/
|
|
69
|
-
│
|
|
92
|
+
│ ├── slidemind.mjs CLI dispatcher (ready, doctor, presentations, read, download-bundle, new, init, build, build-pptx, view-draft, push, ls engines, search, insert-slide, add-slide, install, pair/whoami/use/logout)
|
|
93
|
+
│ └── slidemind + slidemind.cmd PATH shims for Claude Code plugin sessions (npm links slidemind.mjs itself)
|
|
70
94
|
├── skills/ canonical, installable skill payload — thin CLI workflow glue only
|
|
71
95
|
│ ├── create-slides/
|
|
72
96
|
│ │ └── SKILL.md authoring skill: stage-todo bootstrap + host gate + intake/plan + create flow +
|
|
@@ -86,7 +110,7 @@ slidemind/
|
|
|
86
110
|
├── archive/ retired code/assets (local exporter, icon library, brand design files) — NOT shipped
|
|
87
111
|
├── decks/ the user's working decks; sources are slides/*.jsx, design via .bundle-ref.json
|
|
88
112
|
├── docs/slidemind-cli-connect/ the connect architecture (bridge contract, bundle, engine, phasing)
|
|
89
|
-
└── test/ node:test suites (bridge client/bundle/build-and-push/resolve-skill/lint-wrapper/packaging/engine-search/native-slides/cli-dispatch/cli-download-result)
|
|
113
|
+
└── test/ node:test suites (bridge client/bundle/build-and-push/resolve-skill/lint-wrapper/packaging/plugin-manifests/engine-search/native-slides/cli-dispatch/cli-download-result)
|
|
90
114
|
```
|
|
91
115
|
|
|
92
116
|
The skills at the **root** of `skills/` are deliberate: that's the layout Claude Code, Codex (0.128+), and opencode all expect for skill auto-discovery — `<harness-root>/skills/<name>/SKILL.md`. There are **two** skills, each auto-discovered by `listSkills()` (no installer change to add one): `create-slides` (installs as `slidemind-create-slides`) owns the user-facing plan / create / edit / render workflow, and `search` (installs as `slidemind-search`) owns engine discovery + slide reuse (search → insert live / fold into a deck). Both are **brand-agnostic** — every brand rule lives in the per-CD bundle's `design.md` / `workflow/*.md`, which the skills point the agent at. (Pre-skills opencode versions used an `agent/` shim under `~/.config/opencode/`; the installer removes any leftover from those installs on every run.)
|
|
@@ -127,6 +151,24 @@ slidemind uninstall --for <target> # remove the skill (also clears the bundle
|
|
|
127
151
|
|
|
128
152
|
**The design truth lives in the add-in, not the CLI.** The `.pptx` is rendered server-side over the local bridge (no local exporter), the corporate design is fetched as a bundle, brand imagery comes from the add-in's engine, and slide reuse (extract / insert / splice) executes in the add-in too. So `create`/`build-pptx`/`push`/`ls engines`/`search`/`insert-slide`/`add-slide` all hard-require an **open PowerPoint host with the slidemind add-in and an active corporate design**. The engine-search + slide-reuse wire contract is in `docs/slidemind-cli-connect/11-engine-search-slide-reuse.md`; see `docs/slidemind-cli-connect/` for the rest.
|
|
129
153
|
|
|
154
|
+
## External skill packs (this repo ships none)
|
|
155
|
+
|
|
156
|
+
Some decks build from a **template pack** (`mode: "fill-template"` in the pack's `skill.json`) rather than from the design bundle. Those packs are distributed privately, on their own — as a harness plugin, an admin-pushed workspace skill, or a hand-installed directory. **This package must never carry, name, or hardcode anything from one.** No layout component names, no template file names, no master-fixed titles, no client names. `src/pptx/fill/` is generic machinery; every pack-specific fact is read from the pack:
|
|
157
|
+
|
|
158
|
+
| Fact | Read from |
|
|
159
|
+
|---|---|
|
|
160
|
+
| semantic name → shape id | `fields.json` |
|
|
161
|
+
| repeat groups | `repeats.json` |
|
|
162
|
+
| immutable tables, hygiene deletions, freeform placeholders | `skill.json` `policy` |
|
|
163
|
+
| layout component naming | `skill.json` `components` — `{ canonical, localized }`, regex sources; `canonical` group 1 is the slide number, `localized` group 1 the locale and group 2 the number |
|
|
164
|
+
| template-fixed slide titles | `skill.json` `policy.fixedTitles` — `{ <locale>: [{ stem, title }] }`, `stem` a case-insensitive regex on the slide file stem |
|
|
165
|
+
|
|
166
|
+
A pack that declares no `components.canonical` fails loudly in `layoutNumber()` — returning null instead would silently demote every master-backed slide to freeform and build a deck that quietly lost its template.
|
|
167
|
+
|
|
168
|
+
Packs resolve through `locateSkillDir()` (`src/builders/resolve-skill.mjs`): `$SLIDEMIND_SKILLS_DIR/<name>` → `~/.config/slidemind/skills/<name>` → `<package>/skills/<name>` → the **harness** skill and plugin dirs (`~/.claude`, `~/.codex`, opencode; both `<name>` and `slidemind-<name>`). That last tier is what makes every distribution channel work — a plugin or admin upload lands the pack in a harness directory, never in ours — and it ranks *after* the package so this CLI's own skills are never shadowed by a stale copy.
|
|
169
|
+
|
|
170
|
+
Pack-dependent integration tests belong in the pack's own repo, not here.
|
|
171
|
+
|
|
130
172
|
## Design system ground rules
|
|
131
173
|
|
|
132
174
|
The corporate design is **served by the add-in, not shipped by the CLI.** `slidemind download-bundle` materializes it (keyed by the deck's `.bundle-ref.json`) into `${XDG_CACHE_HOME:-~/.cache}/slidemind/bundles/<presFP>/`:
|
package/README.md
CHANGED
|
@@ -133,6 +133,34 @@ Run any command without installing globally:
|
|
|
133
133
|
npx slidemind <command>
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
### Install as a plugin (Claude Code / Codex)
|
|
137
|
+
|
|
138
|
+
Both harnesses can install slidemind as a **plugin** instead of the two-command npm flow.
|
|
139
|
+
|
|
140
|
+
**Claude Code — fully self-contained (no `npm install -g`, no GitHub account):**
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
/plugin marketplace add https://unpkg.com/slidemind/.claude-plugin/marketplace.json
|
|
144
|
+
/plugin install slidemind@slidemind
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The marketplace is served straight out of the published npm package — the tarball ships `.claude-plugin/marketplace.json`, and an npm CDN serves it over plain HTTPS. Nothing here needs repo access, and it follows releases automatically (no version in the URL, no version pin in the plugin entry). `https://cdn.jsdelivr.net/npm/slidemind/.claude-plugin/marketplace.json` works the same way.
|
|
148
|
+
|
|
149
|
+
The plugin ships both skills *and* the CLI: Claude Code installs the public npm package and puts `slidemind` on `PATH` inside its sessions (the headless-Chrome heads-up above applies to this install too). Outside Claude Code sessions the command is not on your `PATH` — add the global install if you also want to drive the CLI by hand. Apply updates via `/plugin`.
|
|
150
|
+
|
|
151
|
+
> `/plugin marketplace add STATWORX/intern-slidemind-cli` also works if you have read access to the repo, but the URL above is preferred — it needs no credentials. If background refreshes ever fail against the private repo, use SSH or set `CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE=1`.
|
|
152
|
+
|
|
153
|
+
**Codex — skills via plugin, CLI via npm:**
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
codex plugin marketplace add STATWORX/intern-slidemind-cli
|
|
157
|
+
codex plugin add slidemind@slidemind
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Codex plugins carry the skills only — the CLI still comes from `npm install -g slidemind` (the skills walk you through it if it's missing). Codex clones the repo for this, so it needs read access; without it, get the skills from your workspace's Codex admin skill upload and the CLI from npm. `slidemind init --for codex` remains the most robust path; it additionally writes an `~/.codex/AGENTS.md` pointer.
|
|
161
|
+
|
|
162
|
+
> Already ran `slidemind init` and switching to a plugin? Drop the copied skills once with `slidemind uninstall --for claude` (or `--for codex`) so they don't duplicate the plugin's.
|
|
163
|
+
|
|
136
164
|
---
|
|
137
165
|
|
|
138
166
|
## Connect to PowerPoint
|
package/bin/slidemind
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# PATH shim for plugin sessions: Claude Code adds the plugin's bin/ to PATH, so
|
|
3
|
+
# this makes the command literally `slidemind` (bin/slidemind.mjs would resolve
|
|
4
|
+
# as `slidemind.mjs`). npm installs never use this file — npm links
|
|
5
|
+
# bin/slidemind.mjs itself via package.json "bin".
|
|
6
|
+
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
7
|
+
exec node "$dir/slidemind.mjs" "$@"
|
package/bin/slidemind.mjs
CHANGED
|
@@ -26,6 +26,13 @@ import os from "node:os";
|
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28
28
|
|
|
29
|
+
// The only eager `src/` import: node-builtins only, and the version-floor
|
|
30
|
+
// ladder below is sync, so it cannot be lazily loaded like the rest.
|
|
31
|
+
import {
|
|
32
|
+
isClaudePluginInstall,
|
|
33
|
+
updateCliInstruction,
|
|
34
|
+
} from "../src/install-channel.mjs";
|
|
35
|
+
|
|
29
36
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
37
|
const __dirname = path.dirname(__filename);
|
|
31
38
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
@@ -461,6 +468,13 @@ async function cmdBuildPptx(rest) {
|
|
|
461
468
|
deckDir,
|
|
462
469
|
locale: skill.locale ?? "en",
|
|
463
470
|
srcDir: SRC_DIR,
|
|
471
|
+
// Both are the PACK's contract, read from its skill.json — this CLI
|
|
472
|
+
// knows no layout names and no template-fixed titles of its own.
|
|
473
|
+
components: {
|
|
474
|
+
...(skill.manifest?.components ?? {}),
|
|
475
|
+
skillDir: skill.skillDir,
|
|
476
|
+
},
|
|
477
|
+
policy: skill.manifest?.policy ?? {},
|
|
464
478
|
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
465
479
|
});
|
|
466
480
|
for (const w of ff.warnings) process.stderr.write(` warning: ${w}\n`);
|
|
@@ -1777,9 +1791,13 @@ function publicConnectionStatus(status, client) {
|
|
|
1777
1791
|
// unmet precondition, or null when the connection is fully ready. `code` is the
|
|
1778
1792
|
// machine-copeable reason an agent switches on; `actor` says who can clear it —
|
|
1779
1793
|
// only `cli_update_required` is the AGENT's to fix (it can run `slidemind
|
|
1780
|
-
// update`), every other rung needs a human at the add-in. The
|
|
1781
|
-
//
|
|
1782
|
-
//
|
|
1794
|
+
// update`), every other rung needs a human at the add-in. The one exception is
|
|
1795
|
+
// a plugin-installed CLI: `/plugin` is the only updater that replaces the
|
|
1796
|
+
// binary on the session's PATH, and the agent cannot drive it, so that rung
|
|
1797
|
+
// hands back to the user (see src/install-channel.mjs). The `message` strings
|
|
1798
|
+
// are otherwise BYTE-IDENTICAL to the old connectionNextAction ladder so
|
|
1799
|
+
// doctor's printed output never churns (connectionNextAction is now a thin
|
|
1800
|
+
// projection over this).
|
|
1783
1801
|
// `slidemind ready` layers `ambiguous_design` on top and treats `pane_suspended`
|
|
1784
1802
|
// as non-blocking (a render-ready-but-suspended window is still a design source).
|
|
1785
1803
|
function connectionBlocker(pub) {
|
|
@@ -1805,11 +1823,11 @@ function connectionBlocker(pub) {
|
|
|
1805
1823
|
if (outdated) {
|
|
1806
1824
|
return {
|
|
1807
1825
|
code: "cli_update_required",
|
|
1808
|
-
actor: "agent",
|
|
1826
|
+
actor: isClaudePluginInstall() ? "user" : "agent",
|
|
1809
1827
|
message:
|
|
1810
1828
|
`This CLI is older than the minimum the '${outdated.env}' add-in supports` +
|
|
1811
1829
|
`${outdated.minCliVersion ? ` (${outdated.minCliVersion})` : ""}. ` +
|
|
1812
|
-
|
|
1830
|
+
updateCliInstruction(),
|
|
1813
1831
|
};
|
|
1814
1832
|
}
|
|
1815
1833
|
if ((pub.presentations ?? []).length === 0) {
|
|
@@ -2926,6 +2944,20 @@ async function cmdUpdate(rest) {
|
|
|
2926
2944
|
const ui = await loadInstallerUi();
|
|
2927
2945
|
ui.banner(pkg.version, "update");
|
|
2928
2946
|
|
|
2947
|
+
// A plugin-installed CLI cannot update itself this way: npm would drop a
|
|
2948
|
+
// second copy into the global prefix while the plugin session keeps running
|
|
2949
|
+
// its own. Say so instead of letting the user believe they are now current —
|
|
2950
|
+
// the global install is still worth doing (it is what a plain terminal
|
|
2951
|
+
// outside Claude Code picks up), so this warns and continues.
|
|
2952
|
+
if (isClaudePluginInstall()) {
|
|
2953
|
+
ui.step({
|
|
2954
|
+
status: "warn",
|
|
2955
|
+
label: "plugin",
|
|
2956
|
+
value:
|
|
2957
|
+
"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",
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2929
2961
|
// Upgrade the global install straight from public npm. `@latest` (or a pinned
|
|
2930
2962
|
// version) lets npm resolve + verify the tarball — no SAS, no checksum dance.
|
|
2931
2963
|
const spec = ref
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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": {
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"src/",
|
|
20
20
|
"skills/",
|
|
21
21
|
".claude-plugin/",
|
|
22
|
-
"
|
|
22
|
+
".codex-plugin/",
|
|
23
|
+
".agents/plugins/",
|
|
24
|
+
"AGENTS.md",
|
|
23
25
|
"README.md"
|
|
24
26
|
],
|
|
25
27
|
"scripts": {
|
package/src/bridge/bundle.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
getDesignBundleForBrand,
|
|
53
53
|
getDesignBundleForBrandAnyBackend,
|
|
54
54
|
} from "./client.mjs";
|
|
55
|
+
import { updateCliInstruction } from "../install-channel.mjs";
|
|
55
56
|
|
|
56
57
|
// Highest bundle wire-contract version this CLI understands. The add-in stamps
|
|
57
58
|
// `schemaVersion` on every bundle; a higher value means the CLI is too old.
|
|
@@ -278,7 +279,7 @@ export async function materializeBundle(
|
|
|
278
279
|
throw new Error(
|
|
279
280
|
`This corporate design needs a newer slidemind (bundle schemaVersion ` +
|
|
280
281
|
`${wire.schemaVersion}; this CLI supports ${CLI_MAX_BUNDLE_SCHEMA}).\n` +
|
|
281
|
-
`
|
|
282
|
+
` ${updateCliInstruction()}`,
|
|
282
283
|
);
|
|
283
284
|
}
|
|
284
285
|
const presFP = wire.presFP;
|
package/src/bridge/client.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getSessionFor,
|
|
18
18
|
readSessions,
|
|
19
19
|
} from "./config.mjs";
|
|
20
|
+
import { updateCliInstruction } from "../install-channel.mjs";
|
|
20
21
|
|
|
21
22
|
// This CLI's own version, for the add-in's minCliVersion floor (see
|
|
22
23
|
// assertCliVersionSupported). Read lazily from the package manifest so the
|
|
@@ -57,8 +58,7 @@ export function compareCliVersions(a, b) {
|
|
|
57
58
|
function cliUpdateRequiredError(min) {
|
|
58
59
|
const err = new Error(
|
|
59
60
|
`slidemind CLI ${cliVersion()} is older than the minimum this add-in ` +
|
|
60
|
-
`supports (${min}).
|
|
61
|
-
"`npm install -g slidemind@latest`), then retry.",
|
|
61
|
+
`supports (${min}). ${updateCliInstruction()}`,
|
|
62
62
|
);
|
|
63
63
|
err.code = "cli_update_required";
|
|
64
64
|
return err;
|
package/src/builders/build.mjs
CHANGED
|
@@ -840,10 +840,10 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
840
840
|
if (!fs.existsSync(slidesDir) || !fs.statSync(slidesDir).isDirectory()) {
|
|
841
841
|
throw new Error(`Expected a 'slides/' folder in ${deckDir}`);
|
|
842
842
|
}
|
|
843
|
-
if (skill.mode === "fill-template" && !opts.
|
|
844
|
-
//
|
|
843
|
+
if (skill.mode === "fill-template" && !opts.internalFillFreeformRender) {
|
|
844
|
+
// fill-template decks are PPTX-native. The exporter may render freeform JSX
|
|
845
845
|
// inserts internally, but `slidemind build` should not create a lossy
|
|
846
|
-
// presentation.html artifact for
|
|
846
|
+
// presentation.html artifact for them.
|
|
847
847
|
throw new Error(
|
|
848
848
|
`Deck ${path.basename(deckDir)} uses skill '${skill.skillName}' (mode ` +
|
|
849
849
|
`fill-template); it has no HTML preview. Run 'slidemind build-pptx ` +
|
|
@@ -92,12 +92,15 @@ export function resolveDeckMode(deckDir) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// Skill packs can live outside the npm package:
|
|
96
|
-
// (
|
|
97
|
-
//
|
|
95
|
+
// Skill packs can live outside the npm package: private, org-internal packs
|
|
96
|
+
// (templates, brand-specific assets) are distributed on their own and this
|
|
97
|
+
// package never carries them. Lookup order:
|
|
98
98
|
// 1. $SLIDEMIND_SKILLS_DIR/<name> (tests / non-standard installs)
|
|
99
99
|
// 2. ~/.config/slidemind/skills/<name>
|
|
100
|
-
// 3. <package>/skills/<name>
|
|
100
|
+
// 3. <package>/skills/<name> (this CLI's own skills win on their names)
|
|
101
|
+
// 4. the harness skill + plugin dirs (see harnessSkillDirs)
|
|
102
|
+
// 3 precedes 4 so adding 4 is strictly additive: it is reached only for names
|
|
103
|
+
// this package does not ship, i.e. exactly the externally-distributed packs.
|
|
101
104
|
export function userSkillsRoot() {
|
|
102
105
|
return (
|
|
103
106
|
process.env.SLIDEMIND_SKILLS_DIR ||
|
|
@@ -109,12 +112,69 @@ export function userSkillsRoot() {
|
|
|
109
112
|
);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
// Every distribution channel a pack can arrive through — a Claude Code or
|
|
116
|
+
// Codex plugin, or a workspace/admin-pushed skill — installs it into a HARNESS
|
|
117
|
+
// directory, not ours. Without these the agent reads the pack's SKILL.md and
|
|
118
|
+
// the build then fails to find the pack's own templates, which looks like a
|
|
119
|
+
// broken pack rather than a lookup gap.
|
|
120
|
+
//
|
|
121
|
+
// Installers namespace copied skills as `slidemind-<name>`, so both spellings
|
|
122
|
+
// are tried. Plugin caches nest as <root>/<marketplace>/<plugin>/skills/<name>.
|
|
123
|
+
function harnessRoots() {
|
|
124
|
+
const home = os.homedir();
|
|
125
|
+
return [
|
|
126
|
+
path.join(home, ".claude"),
|
|
127
|
+
path.join(home, ".codex"),
|
|
128
|
+
// opencode keeps its config under XDG.
|
|
129
|
+
path.join(
|
|
130
|
+
process.env.XDG_CONFIG_HOME || path.join(home, ".config"),
|
|
131
|
+
"opencode",
|
|
132
|
+
),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function harnessSkillDirs(skillName) {
|
|
137
|
+
const names = [skillName, `slidemind-${skillName}`];
|
|
138
|
+
const dirs = [];
|
|
139
|
+
for (const root of harnessRoots()) {
|
|
140
|
+
for (const name of names) dirs.push(path.join(root, "skills", name));
|
|
141
|
+
// Plugin caches: <root>/plugins/cache/<marketplace>/<plugin>/skills/<name>
|
|
142
|
+
const cacheRoot = path.join(root, "plugins", "cache");
|
|
143
|
+
let marketplaces = [];
|
|
144
|
+
try {
|
|
145
|
+
marketplaces = fs.readdirSync(cacheRoot);
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
for (const mp of marketplaces) {
|
|
150
|
+
let plugins = [];
|
|
151
|
+
try {
|
|
152
|
+
plugins = fs.readdirSync(path.join(cacheRoot, mp));
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const plugin of plugins) {
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
dirs.push(path.join(cacheRoot, mp, plugin, "skills", name));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return dirs;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Exported for tests: the pack-location rules are the contract every external
|
|
167
|
+
// distribution channel depends on, and they deserve assertions that do not
|
|
168
|
+
// also have to satisfy design resolution.
|
|
169
|
+
export function locateSkillDir(skillName) {
|
|
113
170
|
const candidates = [
|
|
114
171
|
path.join(userSkillsRoot(), skillName),
|
|
115
172
|
path.join(PACKAGE_ROOT, "skills", skillName),
|
|
173
|
+
...harnessSkillDirs(skillName),
|
|
116
174
|
];
|
|
117
|
-
|
|
175
|
+
// The fallback stays the package path, so a missing pack still reports the
|
|
176
|
+
// location a bundled skill would live at rather than a harness cache dir.
|
|
177
|
+
return firstExisting(candidates) ?? path.join(PACKAGE_ROOT, "skills", skillName);
|
|
118
178
|
}
|
|
119
179
|
|
|
120
180
|
export function resolveSkill(skillName, planMeta = {}, opts = {}) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Which channel installed the CLI that is running right now?
|
|
2
|
+
//
|
|
3
|
+
// npm (`npm install -g slidemind`, or `npx`) is the default and the one every
|
|
4
|
+
// "your CLI is too old" remediation used to assume. Claude Code's plugin
|
|
5
|
+
// channel breaks that assumption: the plugin ships its OWN copy of the CLI and
|
|
6
|
+
// puts it on the plugin session's PATH, so `npm install -g slidemind@latest`
|
|
7
|
+
// installs a second, unrelated global and the session keeps running the old
|
|
8
|
+
// plugin copy. A plugin user following the npm remediation would watch the
|
|
9
|
+
// update succeed and stay blocked on the add-in's version floor forever.
|
|
10
|
+
//
|
|
11
|
+
// Detection is by the path this file lives at — where the running code came
|
|
12
|
+
// from — not by env, so it holds for any process the plugin session spawns.
|
|
13
|
+
// `CLAUDE_PLUGIN_ROOT` is honoured as a secondary signal when Claude Code
|
|
14
|
+
// exports it.
|
|
15
|
+
//
|
|
16
|
+
// Codex is deliberately NOT detected: Codex plugins carry the skills only (they
|
|
17
|
+
// cannot put a bin on PATH), so a Codex-plugin session always runs the npm CLI
|
|
18
|
+
// and the npm remediation is already correct there.
|
|
19
|
+
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const PACKAGE_ROOT = path.resolve(
|
|
24
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
25
|
+
"..",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// `~/.claude/plugins/cache/<marketplace>/<plugin>/…` — a `plugins` segment
|
|
29
|
+
// directly under `.claude`. Matching the pair (not either half alone) keeps a
|
|
30
|
+
// user's own `~/dev/.claude/plugins`-shaped checkout from being the only thing
|
|
31
|
+
// that can false-positive.
|
|
32
|
+
function underClaudePlugins(root) {
|
|
33
|
+
const segs = path.resolve(root).split(/[\\/]+/);
|
|
34
|
+
for (let i = 1; i < segs.length; i += 1) {
|
|
35
|
+
if (segs[i] === "plugins" && segs[i - 1] === ".claude") return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isClaudePluginInstall(root = PACKAGE_ROOT) {
|
|
41
|
+
const resolved = path.resolve(root);
|
|
42
|
+
if (underClaudePlugins(resolved)) return true;
|
|
43
|
+
const declared = process.env.CLAUDE_PLUGIN_ROOT;
|
|
44
|
+
if (declared) {
|
|
45
|
+
const rel = path.relative(path.resolve(declared), resolved);
|
|
46
|
+
if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The one sentence every version-floor / too-old-CLI message ends with, so the
|
|
54
|
+
// remediation always names the updater that actually replaces THIS binary.
|
|
55
|
+
export function updateCliInstruction() {
|
|
56
|
+
return isClaudePluginInstall()
|
|
57
|
+
? "This CLI came from the slidemind Claude Code plugin, so update it there: " +
|
|
58
|
+
"`/plugin` → slidemind → update (a global `npm install -g slidemind@latest` " +
|
|
59
|
+
"installs a separate copy and will NOT change the `slidemind` this session runs)."
|
|
60
|
+
: "Run `slidemind update` (or `npm install -g slidemind@latest`), then retry.";
|
|
61
|
+
}
|
|
@@ -78,6 +78,19 @@ function unregisterMcpServer() {
|
|
|
78
78
|
return false;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// ~/.claude/plugins/cache/<marketplace>/slidemind/ — presence means the Claude
|
|
82
|
+
// Code plugin is also installed; skills copied by `init` duplicate its skills.
|
|
83
|
+
function pluginInstallDetected() {
|
|
84
|
+
const cacheRoot = path.join(claudeRoot(), "plugins", "cache");
|
|
85
|
+
try {
|
|
86
|
+
return fs
|
|
87
|
+
.readdirSync(cacheRoot)
|
|
88
|
+
.some((mp) => fs.existsSync(path.join(cacheRoot, mp, NAMESPACE)));
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
export async function install({ packageRoot, flags }) {
|
|
82
95
|
const link = isLinkMode(flags);
|
|
83
96
|
const force = isForce(flags);
|
|
@@ -108,6 +121,15 @@ export async function install({ packageRoot, flags }) {
|
|
|
108
121
|
if (unregisterMcpServer()) {
|
|
109
122
|
ui.step({ status: "remove", label: "mcp server", value: "removed legacy registration (slidemind is a CLI)" });
|
|
110
123
|
}
|
|
124
|
+
|
|
125
|
+
if (pluginInstallDetected()) {
|
|
126
|
+
ui.step({
|
|
127
|
+
status: "info",
|
|
128
|
+
label: "plugin",
|
|
129
|
+
value:
|
|
130
|
+
"the slidemind Claude Code plugin is also installed — these copied skills duplicate its skills; plugin users can drop them with `slidemind uninstall --for claude`",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
111
133
|
}
|
|
112
134
|
|
|
113
135
|
export async function uninstall() {
|
package/src/pptx/fill/engine.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The fill-template deck builder — the host-free `build-pptx` path for
|
|
2
|
-
//
|
|
2
|
+
// template packs (mode: "fill-template" in the pack's skill.json).
|
|
3
3
|
//
|
|
4
4
|
// Assembly direction: the template .pptx is cloned WHOLE (masters, layouts,
|
|
5
5
|
// media, tags, embeddings), then only declared shapes inside the clone are
|
|
@@ -74,7 +74,7 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
74
74
|
problems.push(err.message);
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
-
const layout = layoutNumber(parsed.component, locale);
|
|
77
|
+
const layout = layoutNumber(parsed.component, locale, pack.components);
|
|
78
78
|
if (!layout) {
|
|
79
79
|
problems.push(`${file}: <${parsed.component}> is a freeform (create-slides) slide with no ` +
|
|
80
80
|
`rendered artifact next to it — 'slidemind build-pptx' renders slides/${file.replace(/\.jsx$/, "")}.pptx ` +
|
|
@@ -190,6 +190,9 @@ function loadPack(skill) {
|
|
|
190
190
|
fields,
|
|
191
191
|
repeats: read("repeats.json", {}),
|
|
192
192
|
policy: skill.manifest.policy ?? {},
|
|
193
|
+
// The pack's layout-component naming contract (see parse-slides.mjs).
|
|
194
|
+
// skillDir rides along so a bad pattern names the pack that shipped it.
|
|
195
|
+
components: { ...(skill.manifest.components ?? {}), skillDir: skill.skillDir },
|
|
193
196
|
};
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -283,7 +286,7 @@ function checkIdentities(sequence, warnings, problems) {
|
|
|
283
286
|
problems.push(`WP count mismatch: slide 10 declares ${wpItems.length} work packages but there are ${details.length} WP-detail files (slide 11)`);
|
|
284
287
|
}
|
|
285
288
|
// Every WP detail must carry FULL bullet lists — a section left empty (or a
|
|
286
|
-
// one-liner) is a defect in a client
|
|
289
|
+
// one-liner) is a defect in a client deliverable, not a style choice.
|
|
287
290
|
details.forEach((d, i) => {
|
|
288
291
|
for (const section of ["services", "assumptions", "deliverables"]) {
|
|
289
292
|
const lines = asLines(d.props?.[section]);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// In-deck freeform slides for fill-template decks.
|
|
2
2
|
//
|
|
3
|
-
// Contract: a
|
|
3
|
+
// Contract: a fill-template deck's slides/ may carry create-slides freeform JSX
|
|
4
4
|
// (NN-name.jsx composing the design bundle's layouts) directly next to the
|
|
5
5
|
// master-backed slides — no sibling deck. The rendered native artifact lives
|
|
6
6
|
// beside its source as NN-name.pptx; the fill engine inserts that artifact at
|
|
7
7
|
// the filename position like any native slide. This module keeps the
|
|
8
8
|
// artifacts fresh: stale or missing renders go through the add-in's render
|
|
9
|
-
// path (the ONLY host-dependent step of a
|
|
9
|
+
// path (the ONLY host-dependent step of such a build) and are folded to
|
|
10
10
|
// pruned one-slide packages. Unchanged freeform slides rebuild fully offline.
|
|
11
11
|
|
|
12
12
|
import crypto from "node:crypto";
|
|
@@ -22,22 +22,26 @@ import { layoutNumber } from "./parse-slides.mjs";
|
|
|
22
22
|
|
|
23
23
|
const SIDE_CAR = ".freeform-renders.json";
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
// Some templates fix a slide's title (the master prints it, so a freeform
|
|
26
|
+
// replacement must keep it or the deck stops reading like the master). WHICH
|
|
27
|
+
// slides, and in which locale, is the pack's business — it declares them in
|
|
28
|
+
// skill.json as policy.fixedTitles:
|
|
29
|
+
//
|
|
30
|
+
// "fixedTitles": {
|
|
31
|
+
// "en": [ { "stem": "agenda", "title": "Agenda" } ],
|
|
32
|
+
// "de": [ { "stem": "agenda|ablauf", "title": "Ablauf" } ]
|
|
33
|
+
// }
|
|
34
|
+
//
|
|
35
|
+
// `stem` is a case-insensitive regex matched against the slide file's stem.
|
|
36
|
+
// A pack that declares none simply gets no title warnings.
|
|
37
|
+
function fixedTitleRules(policy, locale) {
|
|
38
|
+
const all = policy?.fixedTitles ?? {};
|
|
39
|
+
return all[locale] ?? all.en ?? [];
|
|
40
|
+
}
|
|
37
41
|
|
|
38
|
-
function isFreeformSource(src) {
|
|
42
|
+
function isFreeformSource(src, components) {
|
|
39
43
|
const comp = src.match(/<([A-Z][A-Za-z0-9]*)/)?.[1];
|
|
40
|
-
return Boolean(comp) && layoutNumber(comp, "en") === null;
|
|
44
|
+
return Boolean(comp) && layoutNumber(comp, "en", components) === null;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function contentHash(parts) {
|
|
@@ -53,9 +57,10 @@ function slideText(bytes) {
|
|
|
53
57
|
return [...(xml ?? "").matchAll(/<a:t>([^<]*)<\/a:t>/g)].map((m) => m[1]).join("\n");
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
function fixedTitleWarning(stem, bytes, locale) {
|
|
57
|
-
const
|
|
58
|
-
|
|
60
|
+
function fixedTitleWarning(stem, bytes, locale, policy) {
|
|
61
|
+
const rule = fixedTitleRules(policy, locale).find((r) =>
|
|
62
|
+
new RegExp(r.stem, "i").test(stem),
|
|
63
|
+
);
|
|
59
64
|
if (!rule) return null;
|
|
60
65
|
if (slideText(bytes).includes(rule.title)) return null;
|
|
61
66
|
return (
|
|
@@ -69,7 +74,14 @@ function fixedTitleWarning(stem, bytes, locale) {
|
|
|
69
74
|
* `{ rendered, warnings }`; throws with the offending file named when a stale
|
|
70
75
|
* render is needed but the paired backend cannot render.
|
|
71
76
|
*/
|
|
72
|
-
export async function ensureFreeformRenders({
|
|
77
|
+
export async function ensureFreeformRenders({
|
|
78
|
+
deckDir,
|
|
79
|
+
locale = "en",
|
|
80
|
+
srcDir,
|
|
81
|
+
components,
|
|
82
|
+
policy = {},
|
|
83
|
+
onLog = () => {},
|
|
84
|
+
}) {
|
|
73
85
|
const slidesDir = path.join(deckDir, "slides");
|
|
74
86
|
const jsxFiles = fs.existsSync(slidesDir)
|
|
75
87
|
? fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx")).sort()
|
|
@@ -77,7 +89,9 @@ export async function ensureFreeformRenders({ deckDir, locale = "en", srcDir, on
|
|
|
77
89
|
const items = [];
|
|
78
90
|
for (const file of jsxFiles) {
|
|
79
91
|
const src = fs.readFileSync(path.join(slidesDir, file), "utf8");
|
|
80
|
-
if (isFreeformSource(src))
|
|
92
|
+
if (isFreeformSource(src, components)) {
|
|
93
|
+
items.push({ file, stem: file.replace(/\.jsx$/, ""), src });
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
96
|
const warnings = [];
|
|
83
97
|
if (items.length === 0) return { rendered: 0, warnings };
|
|
@@ -149,7 +163,7 @@ export async function ensureFreeformRenders({ deckDir, locale = "en", srcDir, on
|
|
|
149
163
|
for (const it of items) {
|
|
150
164
|
const target = path.join(slidesDir, `${it.stem}.pptx`);
|
|
151
165
|
if (!fs.existsSync(target)) continue;
|
|
152
|
-
const warn = fixedTitleWarning(it.stem, fs.readFileSync(target), locale);
|
|
166
|
+
const warn = fixedTitleWarning(it.stem, fs.readFileSync(target), locale, policy);
|
|
153
167
|
if (warn) warnings.push(warn);
|
|
154
168
|
}
|
|
155
169
|
return { rendered: stale.length, warnings };
|
|
@@ -1,27 +1,61 @@
|
|
|
1
|
-
// Parser for
|
|
2
|
-
// one default export returning exactly one <
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Parser for fill-template slide files. Their JSX is declarative by contract —
|
|
2
|
+
// one default export returning exactly one <SomeLayout …/> element whose props
|
|
3
|
+
// are data literals. That constraint is what makes a real JSX runtime
|
|
4
|
+
// unnecessary: attributes are scanned textually and brace expressions are
|
|
5
|
+
// evaluated as plain JS literals in an empty vm sandbox.
|
|
6
|
+
//
|
|
7
|
+
// The component NAMES are the pack's business, not this engine's: a pack ships
|
|
8
|
+
// its own layouts under its own naming scheme, so it declares the scheme in
|
|
9
|
+
// skill.json and this file only applies it.
|
|
6
10
|
|
|
7
11
|
import vm from "node:vm";
|
|
8
12
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
// Compiled once per distinct pattern — layoutNumber runs per slide file.
|
|
14
|
+
const patternCache = new Map();
|
|
15
|
+
function compile(pattern, which, skillDir) {
|
|
16
|
+
let re = patternCache.get(pattern);
|
|
17
|
+
if (re) return re;
|
|
18
|
+
try {
|
|
19
|
+
re = new RegExp(pattern);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`pack ${skillDir ?? "(unknown)"}: skill.json components.${which} is not a ` +
|
|
23
|
+
`valid regular expression (${err.message})`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
patternCache.set(pattern, re);
|
|
27
|
+
return re;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Map a component name → the template slide number it fills, or null when the
|
|
31
|
+
// component is not master-backed at all (i.e. a freeform slide).
|
|
32
|
+
//
|
|
33
|
+
// `components` comes from the pack's skill.json and declares two patterns:
|
|
34
|
+
// canonical — locale-neutral spelling; capture group 1 is the slide number
|
|
35
|
+
// localized — optional generated spelling; group 1 is the locale, group 2
|
|
36
|
+
// the slide number (so a locale mismatch can be reported)
|
|
37
|
+
export function layoutNumber(component, locale, components) {
|
|
38
|
+
if (!components || !components.canonical) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"fill-template pack declares no components.canonical pattern in " +
|
|
41
|
+
"skill.json — the pack owns its layout naming contract and must " +
|
|
42
|
+
"declare it, e.g. { \"components\": { \"canonical\": \"^MyLayout(\\\\d{2})$\" } }",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
let m = component.match(compile(components.canonical, "canonical", components.skillDir));
|
|
14
46
|
if (m) return { no: Number(m[1]) };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
if (components.localized) {
|
|
48
|
+
m = component.match(compile(components.localized, "localized", components.skillDir));
|
|
49
|
+
if (m) {
|
|
50
|
+
const compLocale = String(m[1]).toLowerCase();
|
|
51
|
+
return {
|
|
52
|
+
no: Number(m[2]),
|
|
53
|
+
warn:
|
|
54
|
+
compLocale === locale
|
|
55
|
+
? `use the canonical alias for slide ${Number(m[2])}`
|
|
56
|
+
: `component locale "${compLocale}" does not match deck locale "${locale}"`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
25
59
|
}
|
|
26
60
|
return null;
|
|
27
61
|
}
|
|
@@ -81,7 +115,7 @@ function evalLiteral(expr, file) {
|
|
|
81
115
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
82
116
|
} catch (err) {
|
|
83
117
|
throw new Error(`${file}: cannot evaluate prop expression (${err.message}). ` +
|
|
84
|
-
`
|
|
118
|
+
`Slide props must be data literals — strings, numbers, arrays, objects.`);
|
|
85
119
|
}
|
|
86
120
|
}
|
|
87
121
|
|
|
@@ -136,7 +170,7 @@ export function parseSlideFile(source, file) {
|
|
|
136
170
|
const inner = closeIdx > 0 ? src.slice(i + 1, closeIdx).trim() : "";
|
|
137
171
|
if (inner) {
|
|
138
172
|
throw new Error(
|
|
139
|
-
`${file}: <${component}> has children. Master-backed
|
|
173
|
+
`${file}: <${component}> has children. Master-backed slides are ` +
|
|
140
174
|
`self-closing props-only elements; freeform content belongs to create-slides.`,
|
|
141
175
|
);
|
|
142
176
|
}
|
package/src/pptx/fill/xml.mjs
CHANGED
|
@@ -30,7 +30,7 @@ const stripHighlight = (rPr) =>
|
|
|
30
30
|
|
|
31
31
|
const SHAPE_TAGS = ["sp", "pic", "cxnSp", "graphicFrame"];
|
|
32
32
|
|
|
33
|
-
// Top-level shape blocks of one kind.
|
|
33
|
+
// Top-level shape blocks of one kind. Template masters keep all editable
|
|
34
34
|
// shapes at spTree top level (verified by inventory); grouped shapes are not
|
|
35
35
|
// addressed by the pack metadata.
|
|
36
36
|
export function* blocks(xml, tag) {
|