@tycoworks/tycoslide 0.7.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/SKILL.md +249 -0
  4. package/bin/tycoslide.js +2 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +197 -0
  7. package/dist/engine/dom.d.ts +92 -0
  8. package/dist/engine/dom.js +354 -0
  9. package/dist/engine/fillers/filler.d.ts +22 -0
  10. package/dist/engine/fillers/filler.js +53 -0
  11. package/dist/engine/fillers/image.d.ts +19 -0
  12. package/dist/engine/fillers/image.js +105 -0
  13. package/dist/engine/fillers/table.d.ts +21 -0
  14. package/dist/engine/fillers/table.js +62 -0
  15. package/dist/engine/fillers/template.d.ts +27 -0
  16. package/dist/engine/fillers/template.js +221 -0
  17. package/dist/engine/fillers/text.d.ts +28 -0
  18. package/dist/engine/fillers/text.js +29 -0
  19. package/dist/engine/generate.d.ts +51 -0
  20. package/dist/engine/generate.js +161 -0
  21. package/dist/engine/index.d.ts +8 -0
  22. package/dist/engine/index.js +7 -0
  23. package/dist/engine/types.d.ts +128 -0
  24. package/dist/engine/types.js +22 -0
  25. package/dist/index.d.ts +46 -0
  26. package/dist/index.js +146 -0
  27. package/dist/manifest.d.ts +7 -0
  28. package/dist/manifest.js +88 -0
  29. package/dist/markdown/deckCompiler.d.ts +36 -0
  30. package/dist/markdown/deckCompiler.js +289 -0
  31. package/dist/markdown/index.d.ts +13 -0
  32. package/dist/markdown/index.js +13 -0
  33. package/dist/markdown/parsers.d.ts +32 -0
  34. package/dist/markdown/parsers.js +233 -0
  35. package/dist/markdown/resolvers/code.d.ts +17 -0
  36. package/dist/markdown/resolvers/code.js +44 -0
  37. package/dist/markdown/resolvers/mermaid.d.ts +14 -0
  38. package/dist/markdown/resolvers/mermaid.js +89 -0
  39. package/dist/markdown/resolvers/mermaidTheme.d.ts +27 -0
  40. package/dist/markdown/resolvers/mermaidTheme.js +113 -0
  41. package/dist/markdown/resolvers/resolver.d.ts +42 -0
  42. package/dist/markdown/resolvers/resolver.js +52 -0
  43. package/dist/markdown/slideParser.d.ts +14 -0
  44. package/dist/markdown/slideParser.js +196 -0
  45. package/dist/markdown/textTemplate.d.ts +15 -0
  46. package/dist/markdown/textTemplate.js +75 -0
  47. package/dist/markdown/types.d.ts +239 -0
  48. package/dist/markdown/types.js +40 -0
  49. package/package.json +41 -0
  50. package/syntax.md +291 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tycoworks LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # tycoslide
2
+
3
+ Generate slides from markdown using your real PowerPoint templates.
4
+
5
+ > **Early release** — tycoslide is under active development.
6
+
7
+ ## Why tycoslide?
8
+
9
+ AI can write great slide content, but it can never get things on-brand. No matter what you try, fonts, logos, and colors end up slightly wrong, and you spend hours fixing it by hand.
10
+
11
+ tycoslide helps AI agents build presentations using your real slide templates, so they're always on-brand. You define a theme with your .pptx files, layouts and design assets, from which agents can quickly build new presentations using markdown.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm install @tycoworks/tycoslide
17
+ ```
18
+
19
+ Create `deck.md`:
20
+
21
+ ```markdown
22
+ ---
23
+ theme: ./theme.json
24
+ ---
25
+
26
+ ---
27
+ layout: Title
28
+ title: Quarterly Review
29
+ name: Jane Doe
30
+ jobTitle: Engineering
31
+ ---
32
+
33
+ ---
34
+ layout: Body
35
+ title: Highlights
36
+ ---
37
+
38
+ - Revenue up 12% quarter-over-quarter
39
+ - Three major product launches completed
40
+ ```
41
+
42
+ Build:
43
+
44
+ ```bash
45
+ tycoslide build deck.md # → deck.pptx
46
+ ```
47
+
48
+ ## CLI
49
+
50
+ ```bash
51
+ tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
52
+ tycoslide smoke # one slide per layout → smoke-all.pptx
53
+ tycoslide plugin # generate AI agent plugin package
54
+ tycoslide manifest # print layout + asset catalog to stdout
55
+ ```
56
+
57
+ ## Theme Structure
58
+
59
+ A theme packages a PPTX template, design assets, and a config file into one directory.
60
+
61
+ ```
62
+ my-theme/
63
+ template/corp-template.pptx
64
+ assets/logos/
65
+ assets/icons/
66
+ theme.json
67
+ package.json
68
+ ```
69
+
70
+ **Template** — the PPTX file with named shapes that tycoslide fills.
71
+ **Layout** — a slide pattern in the template (Title, Body, Quote, etc.).
72
+ **Theme** — the directory that bundles a template, assets, and config.
73
+ **Manifest** — a machine-readable catalog of layouts and assets for AI agents.
package/SKILL.md ADDED
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: slides
3
+ description: >
4
+ Use this skill any time the user wants to create branded slides, presentations,
5
+ pitch decks, or sales collateral as a .pptx. Trigger whenever the user mentions "deck," "slides,"
6
+ "presentation," "pitch," or .pptx output. Also trigger when the user says "build me a deck,"
7
+ "make slides about X," or "turn this into a presentation."
8
+ ---
9
+
10
+ # slides
11
+
12
+ ## Setup
13
+
14
+ Before first use, install dependencies from the plugin root:
15
+
16
+ ```bash
17
+ npm install
18
+ ```
19
+
20
+ This installs the tycoslide engine and its dependencies. You only need to do this once.
21
+
22
+ ## Overview
23
+
24
+ This skill builds on-brand 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.
25
+
26
+ For brand voice and naming guidelines, read `brand.md` if it exists alongside this skill.
27
+
28
+ ## Quick Reference
29
+
30
+ | Task | Guide |
31
+ |------|-------|
32
+ | Discover layouts and assets | Read `manifest.json` |
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 contains:
41
+
42
+ - **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions), each with `type` and optionally `required`, `fit`, `limit`, `codeTheme`, `mermaidVariant`, plus documentation (`whenToUse`, `whenNotToUse`)
43
+ - **assets** -- brand logos, client logos, illustrations, and icons (`description`, `whenToUse`)
44
+
45
+ A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
46
+ - **parameters** -- one value on a frontmatter line. Types: `template`, `image`. Fill by putting a value under the parameter's key in the slide frontmatter.
47
+ - **slots** -- a multi-line region in the body. Types: `text`, `table`, `code`, `mermaid`. Fill as the default body region (after the closing `---`) or a `::name::` region.
48
+
49
+ A single physical slide may back multiple layouts. When two manifest entries share the same `slideNumber`, they render into the same underlying PPTX shapes but declare their fill differently -- e.g. one layout exposing the fill as an `image` parameter and a sibling exposing it as a `mermaid` slot. Pick between them by naming the layout you want in frontmatter (`layout: Full bleed diagram` vs `layout: Full bleed image`); the declaration is unambiguous per layout, so the compiler always knows how to interpret the content you provide.
50
+
51
+ Study each layout's `whenToUse` and `limit`s before writing any slides. These are your primary guide for matching content to layouts.
52
+
53
+ ---
54
+
55
+ ## Creating Slides
56
+
57
+ Write a deck file in markdown. The file starts with a global frontmatter block declaring the theme, followed by slides separated by `---`.
58
+
59
+ ```markdown
60
+ ---
61
+ theme: ./theme.json
62
+ ---
63
+
64
+ ---
65
+ layout: Title
66
+ title: Q2 Business Review
67
+ subtitle: Engineering Division
68
+ ---
69
+
70
+ ---
71
+ layout: Quote dark
72
+ quote: Great products are built by great teams.
73
+ attributionName: Jane Smith
74
+ attributionTitle: CEO, Acme Corp
75
+ logo: assets/clients/acme.png
76
+ ---
77
+ ```
78
+
79
+ A text shape that holds several lines (e.g. an attribution with a name over a title) surfaces as one key per line -- fill each as its own scalar (`attributionName`, `attributionTitle` above), never as a YAML list.
80
+
81
+ ### File structure
82
+
83
+ A deck file has three parts:
84
+
85
+ 1. **Global frontmatter** (required) -- the first `---`-delimited block. Must contain `theme:` pointing to the theme config file. May also contain `output:` to set the filename (defaults to `deck.md` → `deck.pptx`).
86
+ 2. **Slides** -- each begins with a `---` separator. A slide's frontmatter sits between `---` delimiters. Body content follows the closing `---`.
87
+ 3. **Slide separators** -- a `---` on its own line separates slides.
88
+
89
+ ### Slide frontmatter
90
+
91
+ Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters** (template and image inputs).
92
+
93
+ ```yaml
94
+ ---
95
+ layout: Body
96
+ title: Key Achievements
97
+ subtitle: This Quarter
98
+ ---
99
+ ```
100
+
101
+ - `layout` is required and consumed by the compiler (not forwarded as content).
102
+ - All other frontmatter keys fill parameters: `title` fills the `title` template parameter, `subtitle` fills the `subtitle` parameter, `hero` fills the `hero` image parameter, etc. A multi-line text shape surfaces as several keys (e.g. `name` + `jobTitle`); fill each as its own scalar line.
103
+ - Slots (`text`, `table`, `code`, `mermaid`) are filled by body regions, not frontmatter -- see below.
104
+
105
+ ### Body content, slots, and formatting
106
+
107
+ See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::name::` markers), the parameter/slot split (parameters: template, image; slots: text, table, code, mermaid), and image parameters.
108
+
109
+ ### Build
110
+
111
+ Run the command from `manifest.json`'s `build.command`:
112
+
113
+ ```bash
114
+ # e.g.: npx tycoslide build deck.md
115
+ ```
116
+
117
+ The deck is written to your current working directory (not inside the skill).
118
+
119
+ ---
120
+
121
+ ## Layout Selection
122
+
123
+ **Don't create boring decks.** Repeating the same layout on every slide makes a forgettable presentation. Use variety and match content shape to layout purpose.
124
+
125
+ ### Before Starting
126
+
127
+ 1. **Read the manifest thoroughly.** Each layout has `whenToUse`, `whenNotToUse`, and `limit`s. Respect all three.
128
+ 2. **Match content shape to layout purpose.** A comparison belongs in a two/three-column layout, quantified proof belongs in stat blocks, a customer voice belongs in a quote or testimonial layout. Don't force content into the wrong layout.
129
+ 3. **Plan narrative arc first.** Decide the sequence of ideas before picking layouts. Then assign each idea to its best-fit layout from the manifest. Keep one variant (all dark or all light) across the deck.
130
+
131
+ ### For Each Slide
132
+
133
+ **Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides.
134
+
135
+ Check each layout's `limit` in the manifest for content density constraints. When content overflows, split across slides.
136
+
137
+ ### Avoid (Common Mistakes)
138
+
139
+ - **Don't repeat the same layout** -- vary layouts for visual rhythm
140
+ - **Don't dump all content on one slide** -- two clear slides beat one crowded slide
141
+ - **Don't ignore layout limits** -- if a slot says max 4 stats, use 4 or fewer
142
+ - **Don't open with a body/content layout** -- use the Title layout for impact
143
+ - **Don't skip section dividers** -- for decks over 5 slides, use Section title layouts to group sections
144
+ - **Don't restyle the layout** -- the theme owns all design; you only fill slots
145
+ - **Don't invent layout or asset names** -- only use what exists in the manifest
146
+ - **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image parameter you care about
147
+ - **Don't mix dark and light** -- keep one variant across the deck
148
+
149
+ ---
150
+
151
+ ## QA (Required)
152
+
153
+ **Assume the first build will fail. Your job is to fix it.**
154
+
155
+ 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.
156
+
157
+ ### Build
158
+
159
+ Run the command from `manifest.json`'s `build.command`:
160
+
161
+ ```bash
162
+ # e.g.: npx tycoslide build deck.md
163
+ ```
164
+
165
+ Read output carefully. Common errors and fixes:
166
+
167
+ | Error | Fix |
168
+ |-------|-----|
169
+ | `Unknown layout: 'xyz'` | Check layout names in `manifest.json` |
170
+ | A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
171
+ | An image didn't swap / placeholder remains | Use the image parameter's key name in frontmatter, and an asset path that exists in `manifest.json` |
172
+ | YAML parse error | Fix the YAML syntax in the slide's frontmatter |
173
+ | `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
174
+ | `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
175
+ | `mermaid-cli is required` | Install mermaid-cli: `npm i -D @mermaid-js/mermaid-cli` |
176
+ | `no "mermaid" block` | The theme has no mermaid color config -- add a `mermaid` section to theme.json |
177
+
178
+ ### Verification Loop
179
+
180
+ 1. Write the deck file → Build
181
+ 2. **Read every error** -- fix all of them
182
+ 3. Rebuild
183
+ 4. **If content overflows**: reduce content or split into two slides
184
+ 5. Repeat until the build exits cleanly
185
+
186
+ **Do not declare success until you've completed at least one build-fix cycle.**
187
+
188
+ ### Visual Check
189
+
190
+ After a clean build, render the `.pptx` to PNGs and inspect them:
191
+
192
+ ```bash
193
+ soffice --headless --convert-to pdf --outdir . <deck>.pptx
194
+ pdftoppm -png -r 96 <deck>.pdf <name>
195
+ ```
196
+
197
+ Read each slide image and check for:
198
+
199
+ - **Word wrapping** -- text that breaks mid-word or overflows its container
200
+ - **Cramped text** -- content too dense for the slide area
201
+ - **Leftover placeholders** -- dummy text ("Lorem ipsum", "Firstname Lastname") or a placeholder logo that should have been swapped
202
+ - **Cut-off content** -- text or images clipped at slide edges
203
+
204
+ If you spot issues, reduce content, switch layouts, or split into multiple slides. Rebuild and re-check.
205
+
206
+ ### Content Review (Use Subagents)
207
+
208
+ **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.
209
+
210
+ After a successful build, spawn a subagent:
211
+
212
+ ```
213
+ Review this deck. Assume there are issues -- find them.
214
+
215
+ Check for:
216
+ - Slides that are too dense (>7 bullets, >5 paragraphs, too many stats/rows)
217
+ - Same layout repeated multiple times with no variety
218
+ - Content that doesn't match layout purpose (check whenToUse in manifest.json)
219
+ - Narrative that doesn't flow logically
220
+ - Missing opening (Title) or closing (Thank you) slide
221
+ - Slides that are too sparse (a single bullet doesn't need its own slide)
222
+ - Leftover placeholder logos or dummy text in the rendered images
223
+
224
+ For each issue, suggest a specific fix.
225
+
226
+ Read: /path/to/deck.md and the rendered PNGs in the working directory
227
+ Also read: manifest.json (for layout documentation)
228
+ ```
229
+
230
+ If the subagent finds issues, fix them and rebuild.
231
+
232
+ ---
233
+
234
+ ## Full Example
235
+
236
+ See [syntax.md](syntax.md#full-example) for a complete multi-slide deck example.
237
+
238
+ ---
239
+
240
+ ## Core Commands
241
+
242
+ ```bash
243
+ # Build a deck (use command from manifest.json's build.command)
244
+ # e.g.: npx tycoslide build deck.md
245
+
246
+ # Render to images for visual QA
247
+ soffice --headless --convert-to pdf --outdir . <deck>.pptx
248
+ pdftoppm -png -r 96 <deck>.pdf <name>
249
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js");
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,197 @@
1
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Command } from "commander";
5
+ import { FitMode, generate, SlotType } from "./engine/index.js";
6
+ import { buildDeck, toEngineConfig } from "./index.js";
7
+ import { generateManifest } from "./manifest.js";
8
+ import { toImageFill } from "./markdown/deckCompiler.js";
9
+ import { CompilerSlotType, compileDeck, ParameterType, parseSlideDocument, RESERVED_KEY, } from "./markdown/index.js";
10
+ import { templateKeys, templateToSegments } from "./markdown/textTemplate.js";
11
+ // ── Smoke content fixtures (used by the `smoke` subcommand) ──────────────────
12
+ const line = (text) => ({ runs: [{ text }] });
13
+ const bulletLine = (text, level = 0) => ({ runs: [{ text }], bullet: { level } });
14
+ const SMOKE_TABLE = {
15
+ headers: ["Feature", "Starter", "Pro", "Business", "Enterprise"].map(line),
16
+ rows: [
17
+ ["Users", "5", "25", "100", "Unlimited"].map(line),
18
+ ["Storage", "1 GB", "10 GB", "100 GB", "1 TB"].map(line),
19
+ ["Support", "Email", "Priority", "24/7", "Dedicated"].map(line),
20
+ ],
21
+ };
22
+ const SMOKE_CODE = [
23
+ { runs: [{ text: "SELECT name, total", color: "FF7B72" }] },
24
+ { runs: [{ text: "FROM orders", color: "FF7B72" }] },
25
+ { runs: [{ text: "WHERE created_at > now();", color: "FF7B72" }] },
26
+ ];
27
+ const SMOKE_PROSE = [
28
+ line("Sample intro line for this block."),
29
+ bulletLine("First point"),
30
+ bulletLine("Second point"),
31
+ ];
32
+ function pickFirstAsset(config) {
33
+ for (const group of Object.values(config.assets)) {
34
+ for (const entry of Object.values(group))
35
+ return entry.path;
36
+ }
37
+ return undefined;
38
+ }
39
+ function smokeSteps(config) {
40
+ const firstAsset = pickFirstAsset(config);
41
+ const absAsset = firstAsset ? resolve(config.rootDir, firstAsset) : undefined;
42
+ return config.layouts.map((layout) => {
43
+ const content = {};
44
+ // Parameters (frontmatter): text → a placeholder-filled template, image → the first asset.
45
+ for (const p of layout.parameters) {
46
+ switch (p.type) {
47
+ case ParameterType.Image: {
48
+ if (!absAsset)
49
+ continue;
50
+ content[p.key] = toImageFill(p, absAsset);
51
+ break;
52
+ }
53
+ case ParameterType.Template: {
54
+ // Text shapes carry no key — the engine slot is keyed by shapeName.
55
+ const values = new Map(templateKeys(p.template).map((k) => [k, "Sample"]));
56
+ content[p.shapeName] = { lines: templateToSegments(p.template, values, p.shapeName) };
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ // Slots (body regions): text → prose, table → table, code → code, mermaid → image.
62
+ for (const s of layout.slots) {
63
+ switch (s.type) {
64
+ case CompilerSlotType.Table:
65
+ content[s.key] = SMOKE_TABLE;
66
+ break;
67
+ case CompilerSlotType.Code:
68
+ content[s.key] = { paragraphs: SMOKE_CODE };
69
+ break;
70
+ case CompilerSlotType.Mermaid: {
71
+ // Mermaid slots don't declare fit; smoke fills them with a fixed
72
+ // contained ImageFill so the projected engine slot (Image, contain)
73
+ // stays consistent with the real renderer's output.
74
+ if (!absAsset)
75
+ continue;
76
+ content[s.key] = { type: SlotType.Image, path: absAsset, fit: FitMode.Contain };
77
+ break;
78
+ }
79
+ case CompilerSlotType.Text:
80
+ content[s.key] = { paragraphs: SMOKE_PROSE };
81
+ break;
82
+ }
83
+ }
84
+ return { layout: layout.name, content };
85
+ });
86
+ }
87
+ const DEFAULT_CONFIG = "theme.json";
88
+ const DEFAULT_SMOKE_OUTPUT = "smoke-all.pptx";
89
+ const SKILL_DIR = "skills/slides";
90
+ const PLUGIN_DIR = ".claude-plugin";
91
+ const PLUGIN_FILE = "plugin.json";
92
+ const MANIFEST_FILE = "manifest.json";
93
+ const SKILL_FILE = "SKILL.md";
94
+ const SYNTAX_FILE = "syntax.md";
95
+ const BUILD_COMMAND = "npx tycoslide build";
96
+ const sdkDir = dirname(fileURLToPath(import.meta.url));
97
+ const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
98
+ const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
99
+ function loadConfig(absPath) {
100
+ let raw;
101
+ try {
102
+ raw = JSON.parse(readFileSync(absPath, "utf-8"));
103
+ }
104
+ catch {
105
+ throw new Error(`Config file not found or invalid JSON: ${absPath}`);
106
+ }
107
+ return { ...raw, rootDir: dirname(absPath) };
108
+ }
109
+ const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
110
+ const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
111
+ program
112
+ .command("build")
113
+ .description("Build a PPTX deck from a Markdown spec")
114
+ .argument("<deck>", "path to deck markdown file")
115
+ .option(`-c, --config <path>`, "override theme config path (default: read from frontmatter)")
116
+ .action(async (deckPath, opts) => {
117
+ const absDeckPath = resolve(process.cwd(), deckPath);
118
+ let source;
119
+ try {
120
+ source = readFileSync(absDeckPath, "utf-8");
121
+ }
122
+ catch {
123
+ throw new Error(`Deck file not found: ${deckPath}`);
124
+ }
125
+ const doc = parseSlideDocument(source);
126
+ const themePath = doc.global.theme;
127
+ const absConfigPath = opts.config
128
+ ? resolve(process.cwd(), opts.config)
129
+ : themePath
130
+ ? resolve(dirname(absDeckPath), String(themePath))
131
+ : undefined;
132
+ if (!absConfigPath) {
133
+ throw new Error(`${basename(deckPath)}: missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
134
+ }
135
+ const config = loadConfig(absConfigPath);
136
+ const deck = compileDeck(doc, config.layouts, config.rootDir);
137
+ if (!deck.output)
138
+ deck.output = basename(deckPath).replace(/\.md$/, ".pptx");
139
+ await buildDeck(deck, config);
140
+ });
141
+ program
142
+ .command("manifest")
143
+ .description("Generate manifest.json from theme config")
144
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
145
+ .option(`-o, --out <file>`, "write to file instead of stdout")
146
+ .action(async (opts) => {
147
+ const config = loadConfig(resolve(process.cwd(), opts.config));
148
+ const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
149
+ if (opts.out) {
150
+ writeFileSync(resolve(process.cwd(), opts.out), `${json}\n`);
151
+ console.log(`WROTE ${opts.out}`);
152
+ }
153
+ else {
154
+ process.stdout.write(`${json}\n`);
155
+ }
156
+ });
157
+ program
158
+ .command("smoke")
159
+ .description("Generate one smoke-test slide per layout")
160
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
161
+ .option(`-o, --out <file>`, "output PPTX filename", DEFAULT_SMOKE_OUTPUT)
162
+ .action(async (opts) => {
163
+ const config = loadConfig(resolve(process.cwd(), opts.config));
164
+ const steps = smokeSteps(config);
165
+ await generate({ theme: opts.config, output: opts.out, steps }, toEngineConfig(config));
166
+ });
167
+ program
168
+ .command("plugin")
169
+ .description("Generate plugin package (plugin.json, manifest.json, SKILL.md, syntax.md) for AI agents")
170
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
171
+ .action(async (opts) => {
172
+ const config = loadConfig(resolve(process.cwd(), opts.config));
173
+ const cwd = process.cwd();
174
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
175
+ const pluginMeta = {
176
+ name: pkg.name,
177
+ version: pkg.version,
178
+ description: pkg.description ?? "",
179
+ skills: "./skills",
180
+ };
181
+ if (pkg.author)
182
+ pluginMeta.author = pkg.author;
183
+ const pluginDir = resolve(cwd, PLUGIN_DIR);
184
+ mkdirSync(pluginDir, { recursive: true });
185
+ writeFileSync(resolve(pluginDir, PLUGIN_FILE), `${JSON.stringify(pluginMeta, null, 2)}\n`);
186
+ console.log(`WROTE ${PLUGIN_DIR}/${PLUGIN_FILE}`);
187
+ const skillDir = resolve(cwd, SKILL_DIR);
188
+ mkdirSync(skillDir, { recursive: true });
189
+ const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
190
+ writeFileSync(resolve(skillDir, MANIFEST_FILE), `${json}\n`);
191
+ console.log(`WROTE ${SKILL_DIR}/${MANIFEST_FILE}`);
192
+ copyFileSync(skillMdPath, resolve(skillDir, SKILL_FILE));
193
+ console.log(`WROTE ${SKILL_DIR}/${SKILL_FILE}`);
194
+ copyFileSync(syntaxMdPath, resolve(skillDir, SYNTAX_FILE));
195
+ console.log(`WROTE ${SKILL_DIR}/${SYNTAX_FILE}`);
196
+ });
197
+ await program.parseAsync(process.argv);
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared shape / DOM helpers used by every fill primitive.
3
+ *
4
+ * Two layers live here. First the low-level slide-XML substrate: `Tag` names the
5
+ * DrawingML / PresentationML elements, plus node utilities (element collection,
6
+ * run text access, run and paragraph builders) and hyperlink relationship
7
+ * management. Second, the higher-level StyledParagraph-rebuild machinery shared
8
+ * by fillText and fillTable: harvest specimen (pPr, rPr) buckets grouped by
9
+ * (bullet kind, level), detach the specimens, then build fresh paragraphs
10
+ * cloning the appropriate bucket. No fill strategy lives here — this is the
11
+ * substrate the fill modules build on.
12
+ */
13
+ import type { StyledParagraph, TextRun } from "./types.js";
14
+ /** DrawingML and PresentationML tag names used when manipulating slide XML. */
15
+ export declare const Tag: {
16
+ readonly RUN: "a:r";
17
+ readonly TEXT: "a:t";
18
+ readonly PARAGRAPH: "a:p";
19
+ readonly RUN_PROPS: "a:rPr";
20
+ readonly PARA_PROPS: "a:pPr";
21
+ readonly BULLET_CHAR: "a:buChar";
22
+ readonly BULLET_AUTO: "a:buAutoNum";
23
+ readonly BULLET_NONE: "a:buNone";
24
+ readonly LINE_BREAK: "a:br";
25
+ readonly END_PARA_RUN_PROPS: "a:endParaRPr";
26
+ readonly OFFSET: "a:off";
27
+ readonly EXTENT: "a:ext";
28
+ readonly BLIP: "a:blip";
29
+ readonly SRC_RECT: "a:srcRect";
30
+ readonly BLIP_FILL: "p:blipFill";
31
+ readonly SPACE_BEFORE: "a:spcBef";
32
+ readonly HLINK_CLICK: "a:hlinkClick";
33
+ readonly RELATIONSHIP: "Relationship";
34
+ readonly TABLE: "a:tbl";
35
+ readonly TABLE_ROW: "a:tr";
36
+ readonly TABLE_CELL: "a:tc";
37
+ readonly TX_BODY: "a:txBody";
38
+ readonly SOLID_FILL: "a:solidFill";
39
+ readonly SRGB_CLR: "a:srgbClr";
40
+ };
41
+ /**
42
+ * OOXML attribute names and fixed attribute values. Element names live in `Tag`;
43
+ * this is the same discipline for the attributes and enumerated values written
44
+ * onto them, so no bare string literal leaks into the fill code.
45
+ */
46
+ export declare const Attr: {
47
+ readonly BOLD: "b";
48
+ readonly ITALIC: "i";
49
+ readonly STRIKE: "strike";
50
+ readonly UNDERLINE: "u";
51
+ readonly ON: "1";
52
+ readonly STRIKE_SINGLE: "sngStrike";
53
+ readonly UNDERLINE_SINGLE: "sng";
54
+ readonly VALUE: "val";
55
+ readonly ID: "Id";
56
+ readonly TYPE: "Type";
57
+ readonly TARGET: "Target";
58
+ readonly TARGET_MODE: "TargetMode";
59
+ readonly EXTERNAL: "External";
60
+ readonly HLINK_ID: "r:id";
61
+ readonly LEVEL: "lvl";
62
+ readonly X: "x";
63
+ readonly Y: "y";
64
+ readonly CX: "cx";
65
+ readonly CY: "cy";
66
+ readonly LEFT: "l";
67
+ readonly TOP: "t";
68
+ readonly RIGHT: "r";
69
+ readonly BOTTOM: "b";
70
+ readonly XML_SPACE: "xml:space";
71
+ readonly PRESERVE: "preserve";
72
+ };
73
+ /** True for a non-null, non-array object — the shared prefix of every `isXFill`. */
74
+ export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
75
+ export declare function collectElements(parent: any, tagName: string): any[];
76
+ export declare function childrenByTag(parent: any, tagName: string): any[];
77
+ export declare function detach(node: any): void;
78
+ export declare function runText(run: any): string;
79
+ export declare function setRunTextPreservingStyle(run: any, text: string): void;
80
+ export declare function leadingDecorativePrefix(text: string): string;
81
+ export declare function buildRun(doc: any, cloneRPr: any | null, text: string): any;
82
+ export declare function buildParagraph(doc: any, clonePPr: any | null, run: any): any;
83
+ export declare const HYPERLINK_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
84
+ export declare function addRelationship(relation: any, url: string): string;
85
+ /**
86
+ * Replace all runs in a paragraph with a sequence of styled runs, cloning the
87
+ * first existing run's rPr as the template and layering per-run marks on top.
88
+ *
89
+ * Exported for tests; not part of the public engine surface (index.ts).
90
+ */
91
+ export declare function setRichRuns(para: any, runs: TextRun[], relation?: any): void;
92
+ export declare function rebuildParagraphs(shape: any, paragraphs: StyledParagraph[], startIndex: number, relation?: any, shapeName?: string): void;