@tycoworks/tycoslide 0.15.1 → 0.16.1

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.
Files changed (63) hide show
  1. package/README.md +9 -5
  2. package/dist/agents/catalog.d.ts +32 -0
  3. package/dist/agents/catalog.js +37 -0
  4. package/dist/agents/commands.d.ts +9 -0
  5. package/dist/agents/commands.js +57 -0
  6. package/dist/agents/extract.d.ts +22 -0
  7. package/dist/agents/extract.js +54 -0
  8. package/dist/{files.d.ts → agents/files.d.ts} +14 -3
  9. package/dist/{files.js → agents/files.js} +20 -5
  10. package/dist/agents/inventory.d.ts +83 -0
  11. package/dist/agents/inventory.js +568 -0
  12. package/dist/agents/manifest.d.ts +3 -0
  13. package/dist/{manifest.js → agents/manifest.js} +3 -14
  14. package/dist/agents/media.d.ts +38 -0
  15. package/dist/agents/media.js +83 -0
  16. package/dist/agents/pptx.d.ts +48 -0
  17. package/dist/agents/pptx.js +123 -0
  18. package/dist/agents/skill.d.ts +43 -0
  19. package/dist/agents/skill.js +134 -0
  20. package/dist/cli.js +8 -44
  21. package/dist/engine/dom.d.ts +3 -0
  22. package/dist/engine/dom.js +4 -0
  23. package/dist/engine/fillers/image.d.ts +6 -5
  24. package/dist/engine/fillers/image.js +24 -7
  25. package/dist/engine/generate.d.ts +2 -2
  26. package/dist/engine/generate.js +2 -2
  27. package/dist/engine/index.d.ts +3 -0
  28. package/dist/engine/index.js +2 -0
  29. package/dist/engine/notes.js +20 -62
  30. package/dist/engine/ooxml.d.ts +36 -0
  31. package/dist/engine/ooxml.js +56 -0
  32. package/dist/engine/types.d.ts +4 -0
  33. package/dist/index.d.ts +4 -11
  34. package/dist/index.js +2 -15
  35. package/dist/markdown/blocks/code.js +1 -2
  36. package/dist/markdown/blocks/image.d.ts +23 -0
  37. package/dist/markdown/blocks/image.js +65 -3
  38. package/dist/markdown/blocks/mermaid.d.ts +1 -1
  39. package/dist/markdown/blocks/mermaid.js +6 -4
  40. package/dist/markdown/blocks/table.js +7 -6
  41. package/dist/markdown/blocks/text.js +9 -8
  42. package/dist/markdown/deckCompiler.d.ts +2 -13
  43. package/dist/markdown/deckCompiler.js +7 -78
  44. package/dist/markdown/index.d.ts +4 -2
  45. package/dist/markdown/index.js +3 -1
  46. package/dist/markdown/inline.d.ts +2 -0
  47. package/dist/markdown/inline.js +4 -3
  48. package/dist/markdown/schema/themeConfigSchema.d.ts +2 -11
  49. package/dist/markdown/schema/themeConfigSchema.js +6 -16
  50. package/dist/markdown/slideParser.d.ts +2 -0
  51. package/dist/markdown/slideParser.js +6 -3
  52. package/dist/markdown/types.d.ts +4 -29
  53. package/dist/markdown/types.js +0 -11
  54. package/{theme-package/syntax.md → docs/markdown.md} +60 -28
  55. package/docs/theme.md +144 -0
  56. package/package.json +5 -4
  57. package/theme-package/SKILL.template.md +161 -0
  58. package/dist/engine/rels.d.ts +0 -18
  59. package/dist/engine/rels.js +0 -55
  60. package/dist/manifest.d.ts +0 -5
  61. package/dist/skillZip.d.ts +0 -49
  62. package/dist/skillZip.js +0 -177
  63. package/theme-package/SKILL.md +0 -226
package/docs/theme.md ADDED
@@ -0,0 +1,144 @@
1
+ # theme.json reference
2
+
3
+ `theme.json` sits at the root of a theme package next to `template/`. It maps author-facing names onto shapes in the template `.pptx`: each layout names one slide, each parameter or slot names one shape on it. The template is never edited. At build time the engine clones the named slide, rewrites the named shapes, and removes any optional shape the author left unfilled. Every object is strict: an unknown key fails the load with `theme.json: invalid theme config` and `Unknown key(s): x. Valid keys: ...`.
4
+
5
+ The one trap is `slideNumber`. It is the number in the file name `ppt/slides/slideN.xml`, not the slide's position in the deck. Reordering slides in PowerPoint changes positions but not file names. Always use the file number in `slideNumber` and `sourceSlide`.
6
+
7
+ ## `template`
8
+
9
+ | Field | Meaning | Error when wrong |
10
+ |---|---|---|
11
+ | `template` | File name of the `.pptx`, resolved inside the theme's `template/` folder. Required. | `File not found: <path>` at build; `Theme declares "template/<name>", but no such file exists` at package. |
12
+
13
+ ## `layouts[]`
14
+
15
+ One entry per slide the author may pick. Two layouts may not share a `slideNumber`; the load fails with `Layouts "A" and "B" share slideNumber N`. A slide with more than one arrangement becomes one layout with a multi-accept slot, never two layouts.
16
+
17
+ | Field | Meaning | Error when wrong |
18
+ |---|---|---|
19
+ | `name` | The value authors write as `layout:` in slide frontmatter. Required, unique. | `unknown layout "xyz"` at deck compile, listing the valid names. |
20
+ | `slideNumber` | File number of the slide to clone. Required. | Wrong slide is cloned; no error, wrong content. |
21
+ | `description` | Neutral prose about the arrangement, shown to deck authors. Optional. | None. |
22
+ | `variant` | `"light"` or `"dark"`: the surface the code panel sits on. Optional, required when `codeTheme` is a pair and the layout takes code. | `the theme's "codeTheme" is a { light, dark } pair, but this layout declares no "variant"`. |
23
+ | `parameters` | Single-value text shapes filled from frontmatter. Required, may be `[]`. | |
24
+ | `slots` | Body regions filled from `::key::` markdown regions. Required, may be `[]`. | |
25
+
26
+ ### `parameters[]`
27
+
28
+ A parameter is a styled text shape whose text is rebuilt from a template string. The `{key}` placeholders are the frontmatter keys authors fill. There is no separate key field. A key may appear in only one parameter per layout: `key "title" (parameter "...") is declared twice`. Parameter shape names and slot keys share one namespace: `name "body" (slot) collides with another parameter or slot`.
29
+
30
+ | Field | Meaning | Error when wrong |
31
+ |---|---|---|
32
+ | `shapeName` | Exact name of the text shape on the layout's slide. Required. | The build prints `Can't find element on slide N in source:` and leaves the placeholder text in place. |
33
+ | `template` | The shape's full text with `{key}` placeholders; `\n` is a line break. Required. | `template has 2 lines but the shape has only 1 visual line(s)`; or `its styled sample text "..." does not fit the template` when styles inside a line do not match the placeholders. |
34
+ | `required` | Whether the author must supply the keys. Optional, default `false`; an unfilled optional parameter has its shape removed. | `parameter "X" is marked required but its template has no keys to fill`; at compile, `requires parameter "X" (keys: ...); none provided`. |
35
+
36
+ Keys match `[A-Za-z0-9_]+`. Write `{{` or `}}` for a literal brace. A shape with one line and one style is the safe case. A line that mixes styles must place a literal or style boundary between two placeholders, or the load fails with `variables "{a}" and "{b}" are adjacent with no separator`.
37
+
38
+ ### `slots[]`
39
+
40
+ A slot is a body region. The author's markdown shape picks which accepted block fills it: prose or a code fence routes to `text`, a GFM table to `table`, an image reference or mermaid fence to `image`.
41
+
42
+ | Field | Meaning | Error when wrong |
43
+ |---|---|---|
44
+ | `key` | The `::key::` region name. Required, unique within the layout. | `unknown slot "::x::" in layout "..."` at compile. |
45
+ | `accepts` | The blocks this slot can become, at most one per `type`. Required. | `accepts two text blocks; each content type may appear once.` |
46
+ | `frame` | `{x, y, cx, cy}` in EMU: the region a transplanted shape is positioned into. Required when any block's `sourceSlide` differs from `slideNumber`, otherwise optional. | `Layout "X" slot "y": a transplant block (sourceSlide ≠ N) requires a "frame"`. |
47
+ | `required` | Whether the author must fill the region. Optional, default `false`; an unfilled optional slot has its shape removed. | `layout "X" requires slot "y"; none provided`. |
48
+
49
+ ### Blocks inside `accepts`
50
+
51
+ Each block is discriminated by `type` and carries only its own option. A `bodyRows` on a text block or a `startAt` on a table block is an unknown key and fails the load.
52
+
53
+ | Field | Meaning | Error when wrong |
54
+ |---|---|---|
55
+ | `type` | `"text"`, `"table"` or `"image"`. Required. | Schema error naming the invalid value. |
56
+ | `sourceSlide` | File number of the slide carrying the specimen shape. Required. | Wrong shape cloned, or `Can't find element on slide N`. |
57
+ | `shapeName` | Exact name of the specimen shape on `sourceSlide`. Required. | `Can't find element on slide N in source:` and the placeholder remains. |
58
+ | `startAt` | Text only. Leave the first N specimen paragraphs untouched and rebuild from paragraph N. Optional, default 0. | `startAt 3 is past the last paragraph (shape has 2)`. |
59
+ | `bodyRows` | Table only. `[start, end]`, 0-based inclusive: the rows that repeat for data. Required. Row 0 is always the header. Rows above `start` and below `end` render once, as fixed rows. | `bodyRows [s, e] is out of range; require 1 <= start <= end <= R-1 (row 0 is the header)`; `has no <a:tbl> element (is it actually a table?)`. |
60
+
61
+ For an image block the shape must be a real picture: `is not a picture (missing <a:off>, <a:ext>, <p:blipFill>, or <p:cNvPr>)`.
62
+
63
+ Transplants: a block whose `sourceSlide` differs from the layout's `slideNumber` is copied from that other slide onto the cloned slide, positioned into the slot's `frame`. The shape the slot replaces on the base slide is removed. This is how one slot accepts both text and a table when the styled table lives on a different slide, as the worked example's `Content` layout does with a table from slide 15. The frame is the observed `<a:xfrm>` of the shape being replaced, so a text block in the same slot fills in place and the table lands exactly where the text was. The transplanted shape is positioned and sized to the frame, so its own size on the source slide does not matter.
64
+
65
+ ## `fonts`
66
+
67
+ Optional array of `@font-face` entries used only when rendering mermaid diagrams. PPTX text keeps the template's own fonts. Declare weights 400 and 700 for each family to avoid synthetic bold.
68
+
69
+ | Field | Meaning | Error when wrong |
70
+ |---|---|---|
71
+ | `family` | CSS family name; must equal the mermaid variant's `fontFamily`. Required. | Warning `fontFamily "X" matches none of the declared theme fonts`, then a substitute font. |
72
+ | `path` | A package specifier such as `@fontsource/inter/files/inter-latin-400-normal.woff2`, or a `./` or `/` file path. Required. | `could not resolve "path" from <rootDir>`; `file not found at ...`; `unsupported format ".otf"`. Supported: woff2, woff, ttf. |
73
+ | `weight` | OpenType weight. Optional, default 400. | None. |
74
+
75
+ ## `codeTheme`
76
+
77
+ Optional. A Shiki theme id such as `"github-light"`, or a pair `{ "light": "...", "dark": "..." }`. Required as soon as any deck uses a code fence: `deck contains a code fence but the theme declares no "codeTheme"`. A pair requires `variant` on every layout that takes code; each layout's `variant` picks the arm.
78
+
79
+ ## `mermaid`
80
+
81
+ Optional record of named color variants. The record keys are free; every variant must carry all eleven fields. Colors are hex strings.
82
+
83
+ | Key | Meaning |
84
+ |---|---|
85
+ | `primary` | Node fill color. |
86
+ | `primaryContrast` | Text color on `primary` nodes. |
87
+ | `text` | Default text, title and node label color. |
88
+ | `line` | Edge and arrow color. |
89
+ | `surface` | Subgraph, secondary node and edge-label background. |
90
+ | `surfaceBorder` | Border color for nodes and subgraphs. |
91
+ | `fontFamily` | CSS family name; match a `fonts` entry. |
92
+ | `accents` | Array of colors assigned in order to `class` groups in a flowchart. |
93
+ | `accentOpacity` | Percent, 0 to 100, applied to accent and subgraph fills. |
94
+ | `accentTextColor` | Text color inside accent-classed nodes. |
95
+ | `groupCornerRadius` | Subgraph corner radius in pixels; 0 for square. |
96
+
97
+ A missing block fails with `theme has no "mermaid" block`.
98
+
99
+ ## `mermaidVariant`
100
+
101
+ Optional string naming a key of `mermaid`. Required as soon as any deck has a mermaid fence: `declares no "mermaidVariant"`. A name that is not a key fails with `mermaid variant "x" not found in theme. Available variants: ...`. One variant per theme; pick the one matching most diagram layouts.
102
+
103
+ ## Minimal complete example
104
+
105
+ ```json
106
+ {
107
+ "template": "composition.pptx",
108
+ "layouts": [
109
+ {
110
+ "name": "Composed",
111
+ "slideNumber": 1,
112
+ "description": "Title over one body region.",
113
+ "parameters": [
114
+ { "shapeName": "Title 1", "template": "{title}", "required": true }
115
+ ],
116
+ "slots": [
117
+ {
118
+ "key": "body",
119
+ "frame": { "x": 457200, "y": 1371600, "cx": 8229600, "cy": 2743200 },
120
+ "accepts": [
121
+ { "type": "text", "sourceSlide": 1, "shapeName": "Text 1" },
122
+ { "type": "table", "sourceSlide": 2, "shapeName": "Table 0", "bodyRows": [1, 1] },
123
+ { "type": "image", "sourceSlide": 3, "shapeName": "Image 0" }
124
+ ]
125
+ }
126
+ ]
127
+ },
128
+ {
129
+ "name": "TextOnly",
130
+ "slideNumber": 4,
131
+ "parameters": [],
132
+ "slots": [
133
+ { "key": "body", "accepts": [{ "type": "text", "sourceSlide": 4, "shapeName": "Text 1" }] }
134
+ ]
135
+ }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ `TextOnly` has no `frame` because its only block lives on its own slide. `Composed` needs one because two blocks are transplants.
141
+
142
+ ## Full example
143
+
144
+ The tycoworks theme is a complete 18-layout theme with a multi-accept slot, `variant`, a `codeTheme` pair, light and dark `mermaid` variants, and `fonts`: https://github.com/tycoworks/tycoworks-theme/blob/main/theme.json
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.15.1",
4
- "description": "Create editable, on-brand PowerPoint slides from markdown, using your existing .pptx templates.",
3
+ "version": "0.16.1",
4
+ "description": "Create editable PowerPoint slides from markdown, using your existing .pptx templates.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "bin",
14
+ "docs",
14
15
  "theme-package"
15
16
  ],
16
17
  "engines": {
@@ -25,9 +26,9 @@
25
26
  "format": "biome format --write ."
26
27
  },
27
28
  "dependencies": {
28
- "@xmldom/xmldom": "^0.9.10",
29
+ "@xmldom/xmldom": "^0.9.12",
29
30
  "commander": "^15.0.0",
30
- "image-size": "^2.0.2",
31
+ "image-size": "^2.0.4",
31
32
  "jszip": "^3.10.1",
32
33
  "mermaid": "^11.16.1",
33
34
  "playwright-core": "^1.62.1",
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: slides
3
+ description: >
4
+ Build branded slides, presentations, pitch decks, or sales collateral as a .pptx.
5
+ Trigger on "deck," "slides," "presentation," "pitch," ".pptx," or "build me a deck."
6
+ compatibility: Needs Node 23.6+ and network access to npm on first use. LibreOffice (soffice) and poppler (pdftoppm) to preview slides. Chrome only if decks contain mermaid diagrams.
7
+ ---
8
+
9
+ # slides
10
+
11
+ ## Setup
12
+
13
+ Before first use, install dependencies from the theme root:
14
+
15
+ ```bash
16
+ npm install
17
+ unzip -nq assets.dat
18
+ ```
19
+
20
+ This installs the tycoslide engine and its dependencies, and unpacks the theme's images so you can copy them into decks. You only need to do this once. If there is no `assets.dat`, skip the unzip. Run every `npx tycoslide` command from this folder too, since that is where the engine is installed and npx finds it nowhere else.
21
+
22
+
23
+ ## Overview
24
+
25
+ This skill builds decks from a markdown deck file. The theme provides slide layouts that control design. Your job: pick the right layouts, fill them with content, and build. You never restyle the layout; the engine clones the real slides, so brand, layout, fonts, and chrome come for free.
26
+
27
+ ## Quick Reference
28
+
29
+ | Task | Guide |
30
+ |------|-------|
31
+ | Discover layouts | Read `manifest.json` |
32
+ | Find a logo, illustration or icon | Search `assets.json`, then copy it next to the deck (see [Images](#images)) |
33
+ | Write a deck (structure, slots, assets) | See [Creating Slides](#creating-slides) below |
34
+ | Fix build errors | See [QA](#qa-required) below |
35
+
36
+ ---
37
+
38
+ ## Layout Discovery
39
+
40
+ Before writing anything, read `manifest.json`. It lists the theme's **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions). A layout is identified by its `name`; every parameter and slot by its `key`. Slots carry `accepts`, and either may be `required`.
41
+
42
+ A layout's inputs split two ways, parameters and slots; see [markdown.md](node_modules/@tycoworks/tycoslide/docs/markdown.md#parameters-and-slots) for how to fill each. Unfilled ones are dropped, so fill as many of a layout's numbered slots (sections, stats, columns) as you have content for.
43
+
44
+ Study each layout's `slots` before writing any slides.
45
+
46
+ ### Images
47
+
48
+ Images live in `assets.json`: every logo, illustration and icon the theme offers, keyed by category and name, each with a `path`, a `fit` and a `description`. **Search it, do not read it whole** -- an icon set alone can run to thousands of entries. **Search for what the icon depicts, not what you mean by it**: a catalog is indexed by image, so "freshness" finds nothing while `grep -i -B2 "clock" assets.json` and `grep -i -B2 "bolt" assets.json` find the icon you wanted, with its path and fit on the lines above.
49
+
50
+ To use an image, copy it into your deck's folder at the same relative path, then write its `path` and `fit` in the image:
51
+
52
+ ```bash
53
+ mkdir -p <deck dir>/assets && cp <theme dir>/assets/hub.png <deck dir>/assets/
54
+ ```
55
+
56
+ ```markdown
57
+ ![Central hub connecting three services](assets/hub.png "fit: scale-down")
58
+ ```
59
+
60
+ If your deck is in the theme folder, the file is already there, so skip the copy. An image the user gives you also goes in the deck's folder; choose its fit by looking at it (`contain` unless it is full-bleed art that may crop). Write alt text for this slide, describing what the image shows here rather than repeating the catalog description.
61
+
62
+ ---
63
+
64
+ ## Creating Slides
65
+
66
+ Write a deck file in markdown, as [markdown.md](node_modules/@tycoworks/tycoslide/docs/markdown.md) describes (see its [full example](node_modules/@tycoworks/tycoslide/docs/markdown.md#full-example)), and build it as its [Build](node_modules/@tycoworks/tycoslide/docs/markdown.md#build) section shows.
67
+
68
+ ---
69
+
70
+ ## Layout Selection
71
+
72
+ **Don't create boring decks.** Repeating the same layout on every slide makes a forgettable presentation. Use variety and match content shape to the layout's slots.
73
+
74
+ ### Before Starting
75
+
76
+ 1. **Read the manifest thoroughly.** Each layout declares its `slots` (and the `accepts` types each one takes) -- the shape of what it can hold. Respect them.
77
+ 2. **Match content shape to the layout's slots.** A comparison fits a layout with two or three text columns; quantified proof fits one with stat slots; a customer voice fits a quote slot. The slots tell you what fits -- don't force content into the wrong shape.
78
+ 3. **Assign each idea to its best-fit layout** from the manifest. Some layouts come in more than one variant (e.g. a dark and a light version, shown in the name) — consider which suits the deck.
79
+
80
+ ### For Each Slide
81
+
82
+ **Every slide communicates one idea.** Put the takeaway in the headline, then support it.
83
+
84
+ Keep each slot's content to what its region comfortably holds. When content overflows, split it across slides.
85
+
86
+ ### Avoid (Common Mistakes)
87
+
88
+ - **Don't repeat the same layout** -- vary layouts for visual rhythm
89
+ - **Don't overstuff a slot** -- keep content to what its region comfortably holds; split across slides when there's too much
90
+ - **Don't restyle the layout** -- the theme owns all design; you only fill slots
91
+ - **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration. If you get a `shrunk to X%` warning, look at the rendered slide: if the image is now too small to make out, use a simpler one. A `leaves X% of the frame empty` warning is the opposite: the image is a different shape from the slot. Neither fails the build -- judge both from the rendered slide.
92
+ - **Don't skip alt text** -- describe what a meaningful image shows and why it's there, not "image of". Leave it empty only for decoration, such as backgrounds and icons beside a heading.
93
+ - **Don't crop what can't be cropped** -- copy the `fit` from `assets.json`. For your own images, never `fit: cover` a diagram, chart, screenshot or logo.
94
+ - **Don't invent layout names or image paths** -- layouts come from `manifest.json`, and theme images from `assets.json`, copied next to the deck
95
+ - **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image slot you care about. If you don't have a suitable image, ask the user for one.
96
+
97
+ ---
98
+
99
+ ## QA (Required)
100
+
101
+ **Assume the first build will fail. Your job is to fix it.**
102
+
103
+ Your first draft almost never comes out clean. Approach QA as a debugging session, not a confirmation step. If you haven't run at least one build-fix cycle, you're not done.
104
+
105
+ Build the deck again ([Build](node_modules/@tycoworks/tycoslide/docs/markdown.md#build)) and read the output carefully. Common errors and fixes are in [markdown.md](node_modules/@tycoworks/tycoslide/docs/markdown.md#build), and layout names are in `manifest.json`. Also:
106
+
107
+ | Error | Fix |
108
+ |-------|-----|
109
+ | An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `![alt](path "fit: …")` with the path relative to the deck |
110
+ | `image "…" not found at …` | For a theme image, copy it into the deck's folder at the path you wrote (search `assets.json`; did you unzip `assets.dat`?) |
111
+
112
+ ### Verification Loop
113
+
114
+ 1. Write the deck file &rarr; Build
115
+ 2. **Fix the error** -- the build stops at the first one, so expect several rounds
116
+ 3. Rebuild
117
+ 4. **If content overflows**: reduce content or split into two slides
118
+ 5. Repeat until the build exits cleanly
119
+
120
+ **Do not declare success until you've completed at least one build-fix cycle.**
121
+
122
+ ### Visual Check
123
+
124
+ After a clean build, render the `.pptx` to PNGs and inspect them:
125
+
126
+ ```bash
127
+ soffice --headless --convert-to pdf --outdir . <deck>.pptx
128
+ pdftoppm -png -r 96 <deck>.pdf <name>
129
+ ```
130
+
131
+ LibreOffice substitutes any font it can't find, so line breaks in the images can differ slightly from PowerPoint's. Read each slide image and check for:
132
+
133
+ - **Word wrapping** -- text that breaks mid-word or overflows its container
134
+ - **Cramped text** -- content too dense for the slide area
135
+ - **Leftover placeholders** -- dummy text ("Lorem ipsum", "Firstname Lastname") or a placeholder logo that should have been swapped
136
+ - **Cut-off content** -- text or images clipped at slide edges
137
+
138
+ If you spot issues, reduce content, switch layouts, or split into multiple slides. Rebuild and re-check.
139
+
140
+ ### Content Review (Use Subagents)
141
+
142
+ **Use subagents for review** -- even for short decks. You've been staring at the content and will see what you expect, not what's there. Subagents have fresh eyes.
143
+
144
+ After a successful build, spawn a subagent:
145
+
146
+ ```
147
+ Review this deck. Assume there are issues -- find them.
148
+
149
+ Check for:
150
+ - Slides where content overflows its slot (more bullets/stats/rows than the region holds)
151
+ - Same layout repeated multiple times with no variety
152
+ - Content that doesn't match the layout's slots (check `accepts` in manifest.json)
153
+ - Leftover placeholder logos or dummy text in the rendered images
154
+
155
+ For each issue, suggest a specific fix.
156
+
157
+ Read: /path/to/deck.md and the rendered PNGs in the working directory
158
+ Also read: manifest.json (for layout documentation); search assets.json for images
159
+ ```
160
+
161
+ If the subagent finds issues, fix them and rebuild.
@@ -1,18 +0,0 @@
1
- /**
2
- * Post-write sweep of relationships whose target part no longer exists.
3
- *
4
- * pptx-automizer's `cleanup` removes media parts that no shape embeds any more,
5
- * and `removeExistingSlides` drops the root template's original slide parts.
6
- * Neither touches the `.rels` files that named them, so a filled picture leaves
7
- * two dead entries behind (the template's placeholder image and automizer's
8
- * copy of it) and every removed template slide leaves an orphan rels file.
9
- * PowerPoint treats a relationship to a missing part as a damaged package.
10
- *
11
- * Runs on the written file: cleanup is the last thing automizer does during
12
- * `write()`, and it offers no hook after it.
13
- */
14
- export type PruneResult = {
15
- removedEntries: number;
16
- removedParts: number;
17
- };
18
- export declare function pruneDanglingRels(outputPath: string): Promise<PruneResult>;
@@ -1,55 +0,0 @@
1
- /**
2
- * Post-write sweep of relationships whose target part no longer exists.
3
- *
4
- * pptx-automizer's `cleanup` removes media parts that no shape embeds any more,
5
- * and `removeExistingSlides` drops the root template's original slide parts.
6
- * Neither touches the `.rels` files that named them, so a filled picture leaves
7
- * two dead entries behind (the template's placeholder image and automizer's
8
- * copy of it) and every removed template slide leaves an orphan rels file.
9
- * PowerPoint treats a relationship to a missing part as a damaged package.
10
- *
11
- * Runs on the written file: cleanup is the last thing automizer does during
12
- * `write()`, and it offers no hook after it.
13
- */
14
- import { readFileSync, writeFileSync } from "node:fs";
15
- import { posix } from "node:path";
16
- import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
17
- import JSZip from "jszip";
18
- import { Attr, Tag } from "./dom.js";
19
- const SLIDE_RELS = /^ppt\/slides\/_rels\/(slide\d+\.xml)\.rels$/;
20
- const SLIDES_DIR = "ppt/slides";
21
- export async function pruneDanglingRels(outputPath) {
22
- const zip = await JSZip.loadAsync(readFileSync(outputPath));
23
- const parts = new Set(Object.keys(zip.files).filter((name) => !zip.files[name].dir));
24
- const result = { removedEntries: 0, removedParts: 0 };
25
- for (const relsPart of [...parts]) {
26
- const match = SLIDE_RELS.exec(relsPart);
27
- if (!match)
28
- continue;
29
- if (!parts.has(`${SLIDES_DIR}/${match[1]}`)) {
30
- zip.remove(relsPart);
31
- result.removedParts++;
32
- continue;
33
- }
34
- const doc = new DOMParser().parseFromString(await zip.file(relsPart).async("string"), "text/xml");
35
- const rels = doc.getElementsByTagName(Tag.RELATIONSHIP);
36
- const dead = [];
37
- for (let i = 0; i < rels.length; i++) {
38
- const target = rels[i].getAttribute(Attr.TARGET);
39
- if (!target || rels[i].getAttribute("TargetMode") === "External")
40
- continue;
41
- if (!parts.has(posix.normalize(posix.join(SLIDES_DIR, target))))
42
- dead.push(rels[i]);
43
- }
44
- if (dead.length === 0)
45
- continue;
46
- for (const rel of dead)
47
- rel.parentNode?.removeChild(rel);
48
- result.removedEntries += dead.length;
49
- zip.file(relsPart, new XMLSerializer().serializeToString(doc));
50
- }
51
- if (result.removedEntries > 0 || result.removedParts > 0) {
52
- writeFileSync(outputPath, await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }));
53
- }
54
- return result;
55
- }
@@ -1,5 +0,0 @@
1
- import type { CompilerThemeConfig } from "./markdown/types.js";
2
- /** The layouts document: read whole, so it carries no open-ended list. */
3
- export declare function generateManifest(config: CompilerThemeConfig): string;
4
- /** The catalog document: searched by name, never read whole. */
5
- export declare function generateAssetCatalog(config: CompilerThemeConfig): string;
@@ -1,49 +0,0 @@
1
- import type { CompilerThemeConfig } from "./markdown/types.js";
2
- /**
3
- * Pack `paths` (theme-relative, POSIX) into one archive, reading each through
4
- * `read`. Entries keep their declared paths, so expanding reproduces the layout
5
- * `theme.json` already refers to.
6
- */
7
- export declare function packAssets(paths: string[], read: (rel: string) => Buffer): Promise<Buffer>;
8
- /**
9
- * Expand a packaged theme's archive into its directory, so the files the catalog
10
- * names are on disk before anything fills with them. Idempotent per file; loose
11
- * files win, so a stale archive never overwrites a theme's real assets; the
12
- * archive itself stays put. A theme with no archive -- every theme under
13
- * development -- returns immediately.
14
- */
15
- export declare function expandAssets(rootDir: string): Promise<void>;
16
- /**
17
- * Rewrite the `name:` value in a SKILL.md's leading YAML frontmatter so the
18
- * packaged skill is named after the consuming theme, not the source template.
19
- * Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
20
- * Throws if there is no frontmatter or no `name:` line — the caller names the file.
21
- */
22
- export declare function renameSkill(md: string, name: string): string;
23
- /**
24
- * The `package.json` a packaged skill installs from — deliberately NOT the theme's
25
- * own. A theme repo's manifest is a development document: it carries the script
26
- * that regenerates the skill, and lists the engine as a devDependency because the
27
- * repo builds with it rather than shipping it.
28
- *
29
- * Copying that verbatim breaks the consumer twice. The build script runs as a
30
- * postinstall inside their container, so anything it touches that is read-only
31
- * fails their whole `npm install`. And under `--omit=dev` the engine is never
32
- * installed, so neither the postinstall nor `npx tycoslide build` can find it.
33
- *
34
- * What ships instead declares only what the skill needs to RUN: the theme's own
35
- * dependencies plus the engine, as runtime dependencies, and no scripts at all.
36
- */
37
- export declare function skillPackageJson(theme: Record<string, unknown>, engine: {
38
- name: string;
39
- version: string;
40
- }): string;
41
- /**
42
- * Zip a theme into an uploadable Agent Skill archive whose entries all live
43
- * under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
44
- * custom-skill format. `generated` names the files the caller just wrote (the
45
- * config, manifest, SKILL.md, syntax.md); `packageJson` is the authored manifest
46
- * from `skillPackageJson`. Optional support files are skipped when absent;
47
- * anything the config declares but that is missing is an error.
48
- */
49
- export declare function zipDir(rootDir: string, folderName: string, config: CompilerThemeConfig, generated: string[], packageJson: string): Promise<Buffer>;
package/dist/skillZip.js DELETED
@@ -1,177 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import JSZip from "jszip";
4
- import { TEMPLATE_DIR } from "./engine/index.js";
5
- import { ASSETS_ARCHIVE, PACKAGE_JSON } from "./files.js";
6
- /** Entries are stored, not deflated: assets are already-compressed images. */
7
- const NO_COMPRESSION = { type: "nodebuffer", compression: "STORE" };
8
- /**
9
- * Pack `paths` (theme-relative, POSIX) into one archive, reading each through
10
- * `read`. Entries keep their declared paths, so expanding reproduces the layout
11
- * `theme.json` already refers to.
12
- */
13
- export async function packAssets(paths, read) {
14
- const archive = new JSZip();
15
- for (const rel of paths)
16
- archive.file(rel, read(rel));
17
- return archive.generateAsync(NO_COMPRESSION);
18
- }
19
- /**
20
- * Expand a packaged theme's archive into its directory, so the files the catalog
21
- * names are on disk before anything fills with them. Idempotent per file; loose
22
- * files win, so a stale archive never overwrites a theme's real assets; the
23
- * archive itself stays put. A theme with no archive -- every theme under
24
- * development -- returns immediately.
25
- */
26
- export async function expandAssets(rootDir) {
27
- // An empty rootDir is a supported value elsewhere ("resolve nothing"), and it
28
- // would expand into the process working directory. Refuse rather than guess.
29
- if (!rootDir)
30
- return;
31
- const archivePath = join(rootDir, ASSETS_ARCHIVE);
32
- if (!existsSync(archivePath))
33
- return;
34
- const archive = await JSZip.loadAsync(readFileSync(archivePath));
35
- for (const [rel, entry] of Object.entries(archive.files)) {
36
- if (entry.dir)
37
- continue;
38
- // JSZip collapses `..` and a leading `/` on load, but a backslash survives
39
- // verbatim and traverses on Windows. We wrote this archive, so an entry that
40
- // is not a plain theme-relative path means it was tampered with.
41
- if (rel.includes("\\")) {
42
- throw new Error(`${ASSETS_ARCHIVE} entry "${rel}" is not a theme-relative path`);
43
- }
44
- const abs = join(rootDir, ...rel.split("/"));
45
- if (existsSync(abs))
46
- continue;
47
- mkdirSync(dirname(abs), { recursive: true });
48
- // Write-then-rename. A plain write is not atomic: a build killed partway
49
- // through 2,000 icons leaves a truncated file that `existsSync` then skips
50
- // forever. Rename is atomic within a filesystem, so a reader sees a whole
51
- // file or none, which also makes two concurrent builds in one directory safe.
52
- const partial = `${abs}.${process.pid}.tmp`;
53
- writeFileSync(partial, await entry.async("nodebuffer"));
54
- renameSync(partial, abs);
55
- }
56
- }
57
- const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
58
- const NAME_LINE = /^name:[ \t]*.*$/m;
59
- /**
60
- * Rewrite the `name:` value in a SKILL.md's leading YAML frontmatter so the
61
- * packaged skill is named after the consuming theme, not the source template.
62
- * Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
63
- * Throws if there is no frontmatter or no `name:` line — the caller names the file.
64
- */
65
- export function renameSkill(md, name) {
66
- const block = md.match(FRONTMATTER);
67
- if (!block)
68
- throw new Error("SKILL.md has no YAML frontmatter block");
69
- if (!NAME_LINE.test(block[1]))
70
- throw new Error('SKILL.md frontmatter has no "name:" line');
71
- return md.replace(block[0], block[0].replace(NAME_LINE, `name: ${name}`));
72
- }
73
- /**
74
- * Files a packaged skill needs beyond the theme's own declarations. Only the
75
- * lockfile: `package.json` is authored by `skillPackageJson` rather than taken
76
- * from the theme directory.
77
- */
78
- const SUPPORT_FILES = ["package-lock.json"];
79
- /**
80
- * The `package.json` a packaged skill installs from — deliberately NOT the theme's
81
- * own. A theme repo's manifest is a development document: it carries the script
82
- * that regenerates the skill, and lists the engine as a devDependency because the
83
- * repo builds with it rather than shipping it.
84
- *
85
- * Copying that verbatim breaks the consumer twice. The build script runs as a
86
- * postinstall inside their container, so anything it touches that is read-only
87
- * fails their whole `npm install`. And under `--omit=dev` the engine is never
88
- * installed, so neither the postinstall nor `npx tycoslide build` can find it.
89
- *
90
- * What ships instead declares only what the skill needs to RUN: the theme's own
91
- * dependencies plus the engine, as runtime dependencies, and no scripts at all.
92
- */
93
- export function skillPackageJson(theme, engine) {
94
- const dependencies = {
95
- ...(theme.dependencies ?? {}),
96
- [engine.name]: `^${engine.version}`,
97
- };
98
- const skill = {
99
- name: theme.name,
100
- version: theme.version,
101
- description: theme.description,
102
- private: true,
103
- dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))),
104
- };
105
- return `${JSON.stringify(skill, null, 2)}\n`;
106
- }
107
- /**
108
- * Every path a packaged theme needs, relative to `rootDir` and POSIX-separated,
109
- * split by how it ships.
110
- *
111
- * Derived from the theme config rather than filtered out of a directory walk:
112
- * the config already declares its template and its whole asset catalog, so an
113
- * allowlist stays correct no matter what else sits in the working directory --
114
- * built decks, PDFs, slide PNGs, scratch files. Font paths are deliberately
115
- * absent when they name a package -- those resolve from node_modules, which
116
- * `npm install` restores -- but a `./`- or `/`-prefixed font path is a file the
117
- * theme owns, and mermaid reads it during COMPILE, before any archive is
118
- * expanded. Those ship plain.
119
- *
120
- * `archived` is the asset catalog, which collapses to one archive because hosts
121
- * cap how many FILES a skill may contain. `plain` is everything read before or
122
- * without an expansion, including the catalog itself.
123
- */
124
- function skillPaths(config, generated) {
125
- const archived = Object.values(config.assets).flatMap((category) => Object.values(category).map((entry) => entry.path));
126
- const localFonts = (config.fonts ?? []).map((f) => f.path).filter((p) => p.startsWith(".") || p.startsWith("/"));
127
- return {
128
- plain: [...SUPPORT_FILES, ...generated, `${TEMPLATE_DIR}/${config.template}`, ...localFonts],
129
- archived,
130
- };
131
- }
132
- /**
133
- * Zip a theme into an uploadable Agent Skill archive whose entries all live
134
- * under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
135
- * custom-skill format. `generated` names the files the caller just wrote (the
136
- * config, manifest, SKILL.md, syntax.md); `packageJson` is the authored manifest
137
- * from `skillPackageJson`. Optional support files are skipped when absent;
138
- * anything the config declares but that is missing is an error.
139
- */
140
- export async function zipDir(rootDir, folderName, config, generated, packageJson) {
141
- const zip = new JSZip();
142
- const folder = zip.folder(folderName);
143
- if (!folder)
144
- throw new Error(`Failed to create zip folder: ${folderName}`);
145
- folder.file(PACKAGE_JSON, packageJson);
146
- const { plain, archived } = skillPaths(config, generated);
147
- const optional = new Set(SUPPORT_FILES);
148
- let count = 1;
149
- // `optional` is a plain-bucket concept (a lockfile a theme may not have). An
150
- // asset the catalog declares is never optional, so the archived loop calls
151
- // `required` and a missing one throws rather than silently vanishing.
152
- const required = (rel) => {
153
- const abs = join(rootDir, ...rel.split("/"));
154
- if (existsSync(abs))
155
- return readFileSync(abs);
156
- throw new Error(`Theme declares "${rel}", but no such file exists`);
157
- };
158
- const read = (rel) => {
159
- if (optional.has(rel) && !existsSync(join(rootDir, ...rel.split("/"))))
160
- return null;
161
- return required(rel);
162
- };
163
- for (const rel of plain) {
164
- const content = read(rel);
165
- if (content === null)
166
- continue;
167
- folder.file(rel, content);
168
- count++;
169
- }
170
- if (archived.length > 0) {
171
- folder.file(ASSETS_ARCHIVE, await packAssets(archived, required));
172
- count++;
173
- }
174
- if (count === 0)
175
- throw new Error(`No files to zip in directory: ${rootDir}`);
176
- return zip.generateAsync({ type: "nodebuffer", compression: "STORE" });
177
- }