@tycoworks/tycoslide 0.11.4 → 0.13.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.
package/README.md CHANGED
@@ -54,7 +54,7 @@ npx tycoslide build deck.md # → deck.pptx
54
54
  ```bash
55
55
  npx tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
56
56
  npx tycoslide build deck.md --no-notes # omit speaker notes from the output
57
- npx tycoslide package # regenerate skill.md/syntax.md/manifest.json + zip the theme into a self-contained <package-name>.zip
57
+ npx tycoslide package # regenerate the skill files + zip the theme into a self-contained <package-name>.zip
58
58
  ```
59
59
 
60
60
  ## Theme Structure
@@ -70,14 +70,16 @@ my-theme/
70
70
  package.json
71
71
  skill.md # generated by `tycoslide package`
72
72
  syntax.md # generated by `tycoslide package`
73
- manifest.json # generated by `tycoslide package`
73
+ manifest.json # generated by `tycoslide package` -- the theme's layouts
74
+ assets.json # generated by `tycoslide package` -- the theme's picture catalog
74
75
  <package-name>.zip # uploadable Agent Skill bundle
75
76
  ```
76
77
 
77
78
  **Template** — the PPTX file with named shapes that tycoslide fills.
78
79
  **Layout** — a slide pattern in the template (Title, Body, Quote, etc.).
79
80
  **Theme** — the directory that bundles a template, assets, and config.
80
- **Manifest** — a machine-readable catalog of layouts and assets for AI agents.
81
+ **Manifest** — the machine-readable list of layouts, for an AI agent to read.
82
+ **Catalog** — the machine-readable list of assets, for an AI agent to search.
81
83
 
82
84
  ## Diagrams
83
85
 
package/SKILL.md CHANGED
@@ -26,7 +26,8 @@ This skill builds on-brand decks from a markdown deck file. The theme provides s
26
26
 
27
27
  | Task | Guide |
28
28
  |------|-------|
29
- | Discover layouts and assets | Read `manifest.json` |
29
+ | Discover layouts | Read `manifest.json` |
30
+ | Find a logo, illustration or icon | Search `assets.json` |
30
31
  | Write a deck (structure, slots, assets) | See [Creating Slides](#creating-slides) below |
31
32
  | Fix build errors | See [QA](#qa-required) below |
32
33
 
@@ -34,13 +35,12 @@ This skill builds on-brand decks from a markdown deck file. The theme provides s
34
35
 
35
36
  ## Layout Discovery
36
37
 
37
- Before writing anything, read `manifest.json`. It contains:
38
+ 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`. Parameters carry a `type`, slots carry `accepts`, and either may be `required`.
38
39
 
39
- - **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`. Parameters carry a `type`, slots carry `accepts`, and either may be `required`
40
- - **assets** -- brand logos, client logos, illustrations, and icons (`description`)
40
+ Pictures live in `assets.json`: every logo, illustration and icon the theme offers, keyed by category and name. **Search it, do not read it whole** -- an icon set alone can run to thousands of entries. Grep for the concept you want (`grep -i "arrow" assets.json`) and use the `$category.name` you find.
41
41
 
42
42
  A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
43
- - **parameters** -- one value on a frontmatter line. Types: `template`, `image`. Fill by putting a value under the parameter's key in the slide frontmatter.
43
+ - **parameters** -- one value on a frontmatter line. Fill by putting a value under the parameter's key in the slide frontmatter.
44
44
  - **slots** -- a multi-line region in the body. Accept types: `text`, `table`, `image`. Fill as a `::key::` region, using the slot's `key` from the manifest; a fenced code block routes to a `text` slot, a fenced mermaid block to an `image` slot.
45
45
 
46
46
  A slot may accept more than one content type -- the manifest lists each slot's `accepts`, and the shape of the content you write selects which one.
@@ -89,7 +89,7 @@ A deck file has three parts:
89
89
 
90
90
  ### Slide frontmatter
91
91
 
92
- Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters** (template and image inputs).
92
+ Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters**.
93
93
 
94
94
  ```yaml
95
95
  ---
@@ -100,13 +100,13 @@ subtitle: This Quarter
100
100
  ```
101
101
 
102
102
  - `layout` is required and consumed by the compiler (not forwarded as content).
103
- - 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
+ - All other frontmatter keys fill parameters: `title` fills the `title` parameter, `subtitle` fills the `subtitle` parameter, etc. A multi-line text shape surfaces as several keys (e.g. `name` + `jobTitle`); fill each as its own scalar line.
104
104
  - Slots (accepting `text`, `table`, `image`) are filled by body regions, not frontmatter -- see below.
105
105
  - A slide may also carry a `notes:` block in frontmatter -- plain-text speaker notes for the slide's notes page (see [syntax.md](syntax.md#speaker-notes)). It is slide-level metadata, not a parameter or slot.
106
106
 
107
107
  ### Body content, slots, and formatting
108
108
 
109
- See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), the parameter/slot split (parameters: template, image; slots accept: text, table, image), and image parameters.
109
+ See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), and the parameter/slot split (slots accept: text, table, image).
110
110
 
111
111
  ### Build
112
112
 
@@ -142,8 +142,8 @@ Keep each slot's content to what its region comfortably holds. When content over
142
142
  - **Don't overstuff a slot** -- keep content to what its region comfortably holds; split across slides when there's too much
143
143
  - **Don't restyle the layout** -- the theme owns all design; you only fill slots
144
144
  - **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.
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
145
+ - **Don't invent layout or asset names** -- only use layouts from `manifest.json` and assets from `assets.json`
146
+ - **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
147
147
 
148
148
  ---
149
149
 
@@ -159,7 +159,7 @@ Build the deck again ([Build](#build)) and read the output carefully. Common err
159
159
  |-------|-----|
160
160
  | `unknown layout "xyz"` | Check layout names in `manifest.json` |
161
161
  | A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
162
- | 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` |
162
+ | An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `![]($category.name)` from `assets.json` |
163
163
  | YAML parse error | Fix the YAML syntax in the slide's frontmatter |
164
164
  | `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
165
165
  | `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
@@ -211,7 +211,7 @@ Check for:
211
211
  For each issue, suggest a specific fix.
212
212
 
213
213
  Read: /path/to/deck.md and the rendered PNGs in the working directory
214
- Also read: manifest.json (for layout documentation)
214
+ Also read: manifest.json (for layout documentation); search assets.json for pictures
215
215
  ```
216
216
 
217
217
  If the subagent finds issues, fix them and rebuild.
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@ import { basename, dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { Command } from "commander";
5
5
  import { buildDeck } from "./index.js";
6
- import { generateManifest } from "./manifest.js";
6
+ import { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
7
7
  import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
8
- import { renameSkill, zipDir } from "./skillZip.js";
8
+ import { renameSkill, skillPackageJson, zipDir } from "./skillZip.js";
9
9
  const DEFAULT_CONFIG = "theme.json";
10
10
  const MANIFEST_FILE = "manifest.json";
11
11
  // The theme skill is written as lowercase skill.md (copied from tycoslide's own
@@ -60,11 +60,12 @@ program
60
60
  if (!themePkg.name) {
61
61
  throw new Error('Cannot name the skill: the theme\'s package.json has no "name" field.');
62
62
  }
63
- // basename drops any npm scope, e.g. "@acme/mz-slides" -> "mz-slides".
63
+ // basename drops any npm scope, e.g. "@acme/acme-slides" -> "acme-slides".
64
64
  const skillName = basename(themePkg.name);
65
- const manifestJson = `${generateManifest(config)}\n`;
66
- writeFileSync(resolve(process.cwd(), MANIFEST_FILE), manifestJson);
65
+ writeFileSync(resolve(process.cwd(), MANIFEST_FILE), `${generateManifest(config)}\n`);
67
66
  console.log(`WROTE ${MANIFEST_FILE}`);
67
+ writeFileSync(resolve(process.cwd(), ASSETS_FILE), `${generateAssetCatalog(config)}\n`);
68
+ console.log(`WROTE ${ASSETS_FILE}`);
68
69
  let skillMd;
69
70
  try {
70
71
  skillMd = renameSkill(readFileSync(skillMdPath, "utf-8"), skillName);
@@ -80,8 +81,9 @@ program
80
81
  // Bundle the WHOLE theme so the skill is self-contained: unzip ->
81
82
  // `npm install` (pulls the engine + its deps) -> `npx tycoslide build`.
82
83
  const zipFile = `${skillName}.zip`;
83
- const generated = [opts.config, MANIFEST_FILE, SKILL_FILE, SYNTAX_FILE];
84
- writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName, config, generated));
84
+ const generated = [opts.config, MANIFEST_FILE, ASSETS_FILE, SKILL_FILE, SYNTAX_FILE];
85
+ const skillPkg = skillPackageJson(themePkg, { name: pkg.name, version: pkg.version });
86
+ writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName, config, generated, skillPkg));
85
87
  console.log(`WROTE ${zipFile}`);
86
88
  });
87
89
  // Everything below the CLI throws plain Errors carrying a written-for-humans
package/dist/index.d.ts CHANGED
@@ -34,6 +34,6 @@ export declare function toEngineConfig(config: CompilerConfig): Config;
34
34
  export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig, options?: GenerateOptions): Promise<void>;
35
35
  export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
36
36
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
37
- export { generateManifest } from "./manifest.js";
37
+ export { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
38
38
  export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
39
- export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
39
+ export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { generate, SlotType, } from "./engine/index.js";
2
- import { AcceptType, ParameterType, } from "./markdown/types.js";
2
+ import { AcceptType, } from "./markdown/types.js";
3
3
  /**
4
4
  * A frontmatter parameter always fills one physical shape on the layout's own
5
5
  * slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
@@ -7,24 +7,15 @@ import { AcceptType, ParameterType, } from "./markdown/types.js";
7
7
  * placeholder; only body slots with a transplant block carry a real frame.
8
8
  */
9
9
  const NO_FRAME = { x: 0, y: 0, cx: 0, cy: 0 };
10
+ // A text shape carries no top-level key — its template placeholders are the keys.
11
+ // The compiler emits its expanded content under shapeName, so the engine slot is
12
+ // keyed by shapeName too.
10
13
  function paramToEngineSlot(param, baseSlide) {
11
- switch (param.type) {
12
- case ParameterType.Template:
13
- // A text shape carries no top-level key — its template placeholders are the keys. The
14
- // compiler emits its expanded content under shapeName, so the engine slot
15
- // is keyed by shapeName too.
16
- return {
17
- key: param.shapeName,
18
- frame: NO_FRAME,
19
- accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
20
- };
21
- case ParameterType.Image:
22
- return {
23
- key: param.key,
24
- frame: NO_FRAME,
25
- accepts: [{ type: SlotType.Image, sourceSlide: baseSlide, shapeName: param.shapeName }],
26
- };
27
- }
14
+ return {
15
+ key: param.shapeName,
16
+ frame: NO_FRAME,
17
+ accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
18
+ };
28
19
  }
29
20
  /**
30
21
  * Project a body slot's real `accepts` to engine `Block[]` and pass its `frame`
@@ -127,6 +118,6 @@ export async function buildDeck(deck, config, options = {}) {
127
118
  // Engine — primitives-only public surface.
128
119
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
129
120
  // Authoring
130
- export { generateManifest } from "./manifest.js";
121
+ export { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
131
122
  // Markdown / Compiler
132
- export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";
123
+ export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
@@ -1,2 +1,7 @@
1
1
  import type { CompilerConfig } from "./markdown/types.js";
2
+ /** Filename of the searchable asset catalog, named by the manifest that points at it. */
3
+ export declare const ASSETS_FILE = "assets.json";
4
+ /** The layouts document: read whole, so it carries no open-ended list. */
2
5
  export declare function generateManifest(config: CompilerConfig): string;
6
+ /** The catalog document: searched by name, never read whole. */
7
+ export declare function generateAssetCatalog(config: CompilerConfig): string;
package/dist/manifest.js CHANGED
@@ -1,27 +1,16 @@
1
1
  import { templateKeys } from "./markdown/textTemplate.js";
2
- import { ParameterType } from "./markdown/types.js";
3
2
  /**
4
3
  * Flatten a compiler parameter to the manifest entries advertised to AI authors.
5
- * A template parameter has no top-level key — its template's keys are the keys, so it
6
- * flattens to one entry per key (shapeName/template stay manifest-internal). An
7
- * image parameter is a single key.
4
+ * A parameter has no top-level key — its template's placeholders are the keys, so
5
+ * it flattens to one entry per key (shapeName/template stay manifest-internal).
8
6
  */
9
7
  function stripParameter(param) {
10
- switch (param.type) {
11
- case ParameterType.Template:
12
- return templateKeys(param.template).map((key) => {
13
- const result = { key, type: param.type };
14
- if (param.required)
15
- result.required = true;
16
- return result;
17
- });
18
- case ParameterType.Image: {
19
- const result = { key: param.key, type: param.type };
20
- if (param.required)
21
- result.required = true;
22
- return [result];
23
- }
24
- }
8
+ return templateKeys(param.template).map((key) => {
9
+ const result = { key };
10
+ if (param.required)
11
+ result.required = true;
12
+ return result;
13
+ });
25
14
  }
26
15
  function stripSlot(slot) {
27
16
  const result = { key: slot.key, accepts: slot.accepts.map((b) => b.type) };
@@ -29,6 +18,9 @@ function stripSlot(slot) {
29
18
  result.required = true;
30
19
  return result;
31
20
  }
21
+ /** Filename of the searchable asset catalog, named by the manifest that points at it. */
22
+ export const ASSETS_FILE = "assets.json";
23
+ /** The layouts document: read whole, so it carries no open-ended list. */
32
24
  export function generateManifest(config) {
33
25
  const layouts = config.layouts.map((layout) => {
34
26
  const ml = {
@@ -41,22 +33,17 @@ export function generateManifest(config) {
41
33
  ml.description = layout.description;
42
34
  return ml;
43
35
  });
36
+ const manifest = { layouts, assets: ASSETS_FILE };
37
+ return JSON.stringify(manifest, null, 2);
38
+ }
39
+ /** The catalog document: searched by name, never read whole. */
40
+ export function generateAssetCatalog(config) {
44
41
  const assets = {};
45
42
  for (const [category, entries] of Object.entries(config.assets)) {
46
43
  assets[category] = {};
47
44
  for (const [name, entry] of Object.entries(entries)) {
48
- const manifestEntry = {
49
- path: entry.path,
50
- type: entry.type,
51
- description: entry.description,
52
- };
53
- assets[category][name] = manifestEntry;
45
+ assets[category][name] = { path: entry.path, type: entry.type, description: entry.description };
54
46
  }
55
47
  }
56
- const manifest = {
57
- version: 1,
58
- layouts,
59
- assets,
60
- };
61
- return JSON.stringify(manifest, null, 2);
48
+ return JSON.stringify(assets, null, 2);
62
49
  }
@@ -13,12 +13,11 @@ export declare function toImageFill(path: string, type: AssetType): ImageFill;
13
13
  * code (Shiki) and rendered mermaid (PNG) — so the returned deck is
14
14
  * engine-shaped, ready for `buildDeck`.
15
15
  *
16
- * `config.rootDir` is the base directory for resolving relative image paths
17
- * declared in the deck's frontmatter or named slots. When empty, image paths are
18
- * returned unchanged — callers that already produce absolute paths (or don't need
19
- * resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
20
- * are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
21
- * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` feed
22
- * the code and mermaid compiles.
16
+ * `config.rootDir` is the base directory for resolving the asset catalog's
17
+ * relative image paths. When empty, image paths are returned unchanged — callers
18
+ * that already produce absolute paths (or don't need resolution, e.g. unit tests)
19
+ * rely on the pass-through. When set, relative paths are resolved to absolute via
20
+ * `path.resolve(rootDir, path)`; absolute paths pass through. `config.codeTheme` /
21
+ * `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
23
22
  */
24
23
  export declare function compileDeck(doc: ParsedDocument, config: CompilerConfig): Promise<CompilerDeck>;
@@ -3,7 +3,7 @@ import { ImageFit, SlotType } from "../engine/index.js";
3
3
  import { parseSlotContent } from "./blocks/registry.js";
4
4
  import { validateSlideFrontmatter } from "./schema/deckSchema.js";
5
5
  import { templateKeys, templateToSegments } from "./textTemplate.js";
6
- import { AssetType, ParameterType, RESERVED_KEY, } from "./types.js";
6
+ import { AssetType, RESERVED_KEY, } from "./types.js";
7
7
  /** Map each semantic asset type to the engine's object-fit directive. */
8
8
  const FIT_FOR = {
9
9
  [AssetType.Icon]: ImageFit.ScaleDown,
@@ -37,7 +37,7 @@ function assertSlotRegion(slot, got, layoutName, slideNo, source) {
37
37
  }
38
38
  }
39
39
  /**
40
- * Resolve a user-supplied image path against the deck's root directory.
40
+ * Resolve a catalog image path against the deck's root directory.
41
41
  * When `rootDir` is empty, the path is returned unchanged so callers that
42
42
  * already produce absolute paths (or callers that don't care about
43
43
  * resolution) can opt out. Absolute paths always pass through.
@@ -73,12 +73,11 @@ function assertUniqueSlideNumbers(layouts) {
73
73
  * be collision-free — otherwise a fill silently clobbers or throws a misleading
74
74
  * error:
75
75
  *
76
- * - **Author keys** — what a frontmatter line addresses: every template-parameter key
77
- * and every image-parameter key must be distinct, so a line routes to exactly
78
- * one parameter.
79
- * - **Content keys** what `step.content` is addressed by: every image key, every
80
- * template parameter's `shapeName`, and every slot key must be distinct, so no two
81
- * overwrite each other in the content map.
76
+ * - **Author keys** — what a frontmatter line addresses: every parameter's placeholder
77
+ * keys must be distinct across the layout, so a line routes to exactly one parameter.
78
+ * - **Content keys** — what `step.content` is addressed by: every parameter's
79
+ * `shapeName` and every slot key must be distinct, so no two overwrite each other in
80
+ * the content map.
82
81
  *
83
82
  * Also rejects a required template parameter whose template has no keys — it declares
84
83
  * no way to be filled, so `required` on it is unsatisfiable.
@@ -95,34 +94,25 @@ function validateLayout(layout) {
95
94
  const claimContentKey = (key, owner) => {
96
95
  if (contentKeys.has(key)) {
97
96
  throw new Error(`Layout "${layout.name}": name "${key}" (${owner}) collides with another parameter or slot; ` +
98
- "each template parameter's shape, image key, and slot key must be distinct.");
97
+ "each parameter's shape and each slot key must be distinct.");
99
98
  }
100
99
  contentKeys.add(key);
101
100
  };
102
101
  for (const param of layout.parameters) {
103
- switch (param.type) {
104
- case ParameterType.Image:
105
- claimAuthorKey(param.key, "image parameter");
106
- claimContentKey(param.key, "image parameter");
107
- break;
108
- case ParameterType.Template: {
109
- const keys = templateKeys(param.template);
110
- if (param.required && keys.length === 0) {
111
- throw new Error(`Layout "${layout.name}": template parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
112
- }
113
- for (const key of keys)
114
- claimAuthorKey(key, `template parameter "${param.shapeName}"`);
115
- claimContentKey(param.shapeName, "template parameter");
116
- break;
117
- }
102
+ const keys = templateKeys(param.template);
103
+ if (param.required && keys.length === 0) {
104
+ throw new Error(`Layout "${layout.name}": parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
118
105
  }
106
+ for (const key of keys)
107
+ claimAuthorKey(key, `parameter "${param.shapeName}"`);
108
+ claimContentKey(param.shapeName, "parameter");
119
109
  }
120
110
  for (const slot of layout.slots) {
121
111
  claimContentKey(slot.key, "slot");
122
112
  }
123
113
  }
124
- async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
125
- const { layouts, rootDir } = config;
114
+ async function compileStep(slide, config, resolveAssetRef) {
115
+ const { layouts } = config;
126
116
  const { frontmatter, slots, index } = slide;
127
117
  // Slide numbers in errors are 1-based, matching how an author counts slides in
128
118
  // the deck file. Derive it once: every message below, and the parse-time errors
@@ -148,44 +138,22 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
148
138
  // layout/notes stripped first). The per-layout strict schema IS the unknown-key
149
139
  // check — it fires before the resolution loop, so that loop only sees valid keys.
150
140
  validateSlideFrontmatter(frontmatter, layoutDef, slideNo);
151
- // Map each author-facing key to its owning parameter: template keys the template
152
- // parameter that declares them, image keys the image parameter. validateLayout
153
- // (run once per layout in compileDeck) has already proven these key spaces are
141
+ // Map each author-facing key to the parameter that declares it. validateLayout
142
+ // (run once per layout in compileDeck) has already proven these keys are
154
143
  // collision-free, so a later lookup is unambiguous.
155
- const templateParams = [];
156
- const imageByKey = new Map();
157
144
  const templateParamByKey = new Map();
158
145
  for (const param of layoutDef.parameters) {
159
- switch (param.type) {
160
- case ParameterType.Image:
161
- imageByKey.set(param.key, param);
162
- break;
163
- case ParameterType.Template:
164
- templateParams.push(param);
165
- for (const key of templateKeys(param.template))
166
- templateParamByKey.set(key, param);
167
- break;
168
- }
146
+ for (const key of templateKeys(param.template))
147
+ templateParamByKey.set(key, param);
169
148
  }
170
149
  const slotsByKey = new Map(layoutDef.slots.map((s) => [s.key, s]));
171
150
  const content = {};
172
- // Frontmatter lines fill image parameters (by key) or template-parameter keys
173
- // (gathered per parameter, expanded together once every line is read).
151
+ // Frontmatter lines fill template-parameter keys, gathered per parameter and
152
+ // expanded together once every line is read.
174
153
  const valuesByTemplateParam = new Map();
175
154
  for (const [key, value] of Object.entries(frontmatter)) {
176
155
  if (key === RESERVED_KEY.LAYOUT || key === RESERVED_KEY.NOTES)
177
156
  continue;
178
- const image = imageByKey.get(key);
179
- if (image) {
180
- const imgPath = resolveImagePath(rootDir, String(value));
181
- const assetType = assetTypeByPath.get(imgPath);
182
- if (assetType === undefined) {
183
- throw new Error(`Slide image "${image.key}": "${value}" has no asset-catalog entry, so no type. ` +
184
- `Add it to the theme's assets with a type (icon | image | background).`);
185
- }
186
- content[image.key] = toImageFill(imgPath, assetType);
187
- continue;
188
- }
189
157
  const templateParam = templateParamByKey.get(key);
190
158
  if (templateParam) {
191
159
  let bucket = valuesByTemplateParam.get(templateParam);
@@ -196,17 +164,17 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
196
164
  bucket.set(key, String(value));
197
165
  }
198
166
  // Unreachable: validateSlideFrontmatter (above) already rejected any key that is
199
- // neither an image key nor a template key, so every key here routes to a parameter.
167
+ // not a reserved key, so every key here routes to a parameter.
200
168
  }
201
169
  // Expand each template parameter whose keys were supplied. Filling any key fills the
202
170
  // parameter as a whole — a missing key throws (fail-fast in templateToSegments). A
203
171
  // parameter with no supplied keys stays designer-sample unless required. Content
204
172
  // is keyed by shapeName: one parameter → one entry, regardless of key count.
205
- for (const templateParam of templateParams) {
173
+ for (const templateParam of layoutDef.parameters) {
206
174
  const supplied = valuesByTemplateParam.get(templateParam);
207
175
  if (!supplied) {
208
176
  if (templateParam.required) {
209
- throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires template parameter "${templateParam.shapeName}" ` +
177
+ throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${templateParam.shapeName}" ` +
210
178
  `(keys: ${templateKeys(templateParam.template).join(", ")}); none provided`);
211
179
  }
212
180
  continue;
@@ -231,17 +199,11 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
231
199
  config,
232
200
  layoutVariant: layoutDef.variant,
233
201
  });
234
- assertSlotRegion(slot, parsed.acceptType, layoutName, index, source);
202
+ assertSlotRegion(slot, parsed.acceptType, layoutName, slideNo, source);
235
203
  content[name] = await parsed.fill();
236
204
  }
237
- // Required image parameters (missing frontmatter key) and required slots
238
- // (missing region) throw with layout + key context. Required template parameters are
239
- // enforced during expansion above.
240
- for (const image of imageByKey.values()) {
241
- if (image.required && content[image.key] === undefined) {
242
- throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
243
- }
244
- }
205
+ // A required slot with no region throws with layout + key context. Required
206
+ // parameters are enforced during template expansion above.
245
207
  for (const slot of layoutDef.slots) {
246
208
  if (slot.required && content[slot.key] === undefined) {
247
209
  throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
@@ -260,13 +222,12 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
260
222
  * code (Shiki) and rendered mermaid (PNG) — so the returned deck is
261
223
  * engine-shaped, ready for `buildDeck`.
262
224
  *
263
- * `config.rootDir` is the base directory for resolving relative image paths
264
- * declared in the deck's frontmatter or named slots. When empty, image paths are
265
- * returned unchanged — callers that already produce absolute paths (or don't need
266
- * resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
267
- * are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
268
- * through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` feed
269
- * the code and mermaid compiles.
225
+ * `config.rootDir` is the base directory for resolving the asset catalog's
226
+ * relative image paths. When empty, image paths are returned unchanged — callers
227
+ * that already produce absolute paths (or don't need resolution, e.g. unit tests)
228
+ * rely on the pass-through. When set, relative paths are resolved to absolute via
229
+ * `path.resolve(rootDir, path)`; absolute paths pass through. `config.codeTheme` /
230
+ * `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
270
231
  */
271
232
  export async function compileDeck(doc, config) {
272
233
  const { layouts, rootDir, assets } = config;
@@ -284,18 +245,10 @@ export async function compileDeck(doc, config) {
284
245
  assertUniqueSlideNumbers(layouts);
285
246
  for (const layout of layouts)
286
247
  validateLayout(layout);
287
- // Index each catalog asset's resolved path its declared type, so an image
288
- // filled by path inherits the scaling tolerance intrinsic to its pixels.
289
- const assetTypeByPath = new Map();
290
- for (const group of Object.values(assets)) {
291
- for (const entry of Object.values(group)) {
292
- assetTypeByPath.set(resolveImagePath(rootDir, entry.path), entry.type);
293
- }
294
- }
295
- // Resolve a body/`::name::` `$category.name` reference against the theme's
296
- // curated asset catalog. Anchored ⇒ the whole ref is the reference or it is
297
- // nothing; a found entry wraps through the same path→ImageFit mapping as a
298
- // frontmatter image (`toImageFill`), so a body image has no second fit story.
248
+ // Resolve a `$category.name` reference against the theme's curated asset
249
+ // catalog. Anchored the whole ref is the reference or it is nothing; a found
250
+ // entry wraps through `toImageFill`, which maps the asset's declared type to
251
+ // its ImageFit.
299
252
  const resolveAssetRef = (ref) => {
300
253
  const match = ASSET_REF_RE.exec(ref);
301
254
  if (!match) {
@@ -304,9 +257,18 @@ export async function compileDeck(doc, config) {
304
257
  const [, category, name] = match;
305
258
  const entry = assets[category]?.[name];
306
259
  if (!entry) {
307
- const available = Object.entries(assets)
308
- .flatMap(([cat, group]) => Object.keys(group).map((n) => `$${cat}.${n}`))
309
- .join(", ");
260
+ // Every asset in the catalog is far too many to read: a theme's icon set
261
+ // alone can run to thousands.
262
+ // A known category narrows it to that category's names, which is what the
263
+ // author is choosing between; an unknown one lists the categories instead.
264
+ const group = assets[category];
265
+ const available = group
266
+ ? Object.keys(group)
267
+ .map((n) => `$${category}.${n}`)
268
+ .join(", ")
269
+ : Object.keys(assets)
270
+ .map((c) => `$${c}.*`)
271
+ .join(", ");
310
272
  throw new Error(`Unknown asset reference "${ref}". Available: ${available}`);
311
273
  }
312
274
  return toImageFill(resolveImagePath(rootDir, entry.path), entry.type);
@@ -315,7 +277,7 @@ export async function compileDeck(doc, config) {
315
277
  // bad asset ref, accept-type mismatch) fire before its own content is rendered.
316
278
  const steps = [];
317
279
  for (const slide of doc.slides) {
318
- steps.push(await compileStep(slide, config, assetTypeByPath, resolveAssetRef));
280
+ steps.push(await compileStep(slide, config, resolveAssetRef));
319
281
  }
320
282
  return { theme: String(theme), steps };
321
283
  }
@@ -9,5 +9,5 @@ export { parseRegion } from "./mdast.js";
9
9
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
10
10
  export type { ParsedDocument, RawSlide } from "./slideParser.js";
11
11
  export { parseSlideDocument } from "./slideParser.js";
12
- export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerParameter, CompilerSlot, CompilerTemplateParameter, CompilerThemeConfig, EngineFill, } from "./types.js";
13
- export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
12
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, } from "./types.js";
13
+ export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
@@ -11,4 +11,4 @@ export { parseInlineRuns } from "./inline.js";
11
11
  export { parseRegion } from "./mdast.js";
12
12
  export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
13
13
  export { parseSlideDocument } from "./slideParser.js";
14
- export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
14
+ export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
@@ -1,22 +1,17 @@
1
1
  import * as z from "zod";
2
2
  import type { CompilerLayout } from "../types.js";
3
3
  /**
4
- * Per-layout frontmatter validation for a deck `.md`. The old tycoslide validated
5
- * every slide's frontmatter against a Zod schema carried by the layout program;
6
- * current layouts are JSON DATA, so the schema is BUILT dynamically from a layout's
7
- * declared `parameters` using the same key-derivation the compiler already uses
8
- * (`deckCompiler.ts`): an image parameter contributes its `key`; a template
9
- * parameter contributes one field per `{key}` placeholder in its template (NOT its
10
- * `shapeName`, which addresses `step.content`, not frontmatter).
4
+ * Per-layout frontmatter validation for a deck `.md`. Layouts are JSON data, so
5
+ * the schema is built dynamically from a layout's `parameters` using the same
6
+ * key-derivation the compiler uses (`deckCompiler.ts`): a parameter contributes
7
+ * one field per `{key}` placeholder in its template, never its `shapeName`,
8
+ * which addresses `step.content` rather than frontmatter.
11
9
  *
12
- * Commit-1 scope is unknown-key detection ONLY zero behavior change:
13
- * - Every field is `z.coerce.string()` (values are `String()`-coerced today, so a
14
- * YAML number like `year: 2026` must still pass plain `z.string()` would regress)
15
- * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
16
- * "all a template's keys present"). Value-typing and required-encoding are deliberate
17
- * later commits.
18
- * The strict object IS the unknown-key check: a stray frontmatter key throws
19
- * instead of being silently ignored.
10
+ * Every field is `z.coerce.string()` so a YAML number like `year: 2026` passes
11
+ * (values are `String()`-coerced downstream), and `.optional()` because
12
+ * `required` is per-parameter and enforced in `compileStep`, not "all a
13
+ * template's keys present". The strict object is the point: a stray frontmatter
14
+ * key throws instead of being silently ignored.
20
15
  */
21
16
  export declare function deckFrontmatterSchema(layout: CompilerLayout): z.ZodObject<{
22
17
  [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
@@ -1,37 +1,25 @@
1
1
  import * as z from "zod";
2
2
  import { templateKeys } from "../textTemplate.js";
3
- import { ParameterType, RESERVED_KEY } from "../types.js";
3
+ import { RESERVED_KEY } from "../types.js";
4
4
  import { strict } from "./strict.js";
5
5
  /**
6
- * Per-layout frontmatter validation for a deck `.md`. The old tycoslide validated
7
- * every slide's frontmatter against a Zod schema carried by the layout program;
8
- * current layouts are JSON DATA, so the schema is BUILT dynamically from a layout's
9
- * declared `parameters` using the same key-derivation the compiler already uses
10
- * (`deckCompiler.ts`): an image parameter contributes its `key`; a template
11
- * parameter contributes one field per `{key}` placeholder in its template (NOT its
12
- * `shapeName`, which addresses `step.content`, not frontmatter).
6
+ * Per-layout frontmatter validation for a deck `.md`. Layouts are JSON data, so
7
+ * the schema is built dynamically from a layout's `parameters` using the same
8
+ * key-derivation the compiler uses (`deckCompiler.ts`): a parameter contributes
9
+ * one field per `{key}` placeholder in its template, never its `shapeName`,
10
+ * which addresses `step.content` rather than frontmatter.
13
11
  *
14
- * Commit-1 scope is unknown-key detection ONLY zero behavior change:
15
- * - Every field is `z.coerce.string()` (values are `String()`-coerced today, so a
16
- * YAML number like `year: 2026` must still pass plain `z.string()` would regress)
17
- * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
18
- * "all a template's keys present"). Value-typing and required-encoding are deliberate
19
- * later commits.
20
- * The strict object IS the unknown-key check: a stray frontmatter key throws
21
- * instead of being silently ignored.
12
+ * Every field is `z.coerce.string()` so a YAML number like `year: 2026` passes
13
+ * (values are `String()`-coerced downstream), and `.optional()` because
14
+ * `required` is per-parameter and enforced in `compileStep`, not "all a
15
+ * template's keys present". The strict object is the point: a stray frontmatter
16
+ * key throws instead of being silently ignored.
22
17
  */
23
18
  export function deckFrontmatterSchema(layout) {
24
19
  const shape = {};
25
20
  for (const param of layout.parameters) {
26
- switch (param.type) {
27
- case ParameterType.Image:
28
- shape[param.key] = z.coerce.string().optional();
29
- break;
30
- case ParameterType.Template:
31
- for (const key of templateKeys(param.template)) {
32
- shape[key] = z.coerce.string().optional();
33
- }
34
- break;
21
+ for (const key of templateKeys(param.template)) {
22
+ shape[key] = z.coerce.string().optional();
35
23
  }
36
24
  }
37
25
  return strict(shape);
@@ -9,17 +9,11 @@ export declare const ThemeConfigSchema: z.ZodObject<{
9
9
  light: "light";
10
10
  dark: "dark";
11
11
  }>>;
12
- parameters: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
12
+ parameters: z.ZodArray<z.ZodObject<{
13
13
  shapeName: z.ZodString;
14
14
  required: z.ZodOptional<z.ZodBoolean>;
15
- type: z.ZodLiteral<"template">;
16
15
  template: z.ZodString;
17
- }, z.core.$strict>, z.ZodObject<{
18
- shapeName: z.ZodString;
19
- required: z.ZodOptional<z.ZodBoolean>;
20
- key: z.ZodString;
21
- type: z.ZodLiteral<"image">;
22
- }, z.core.$strict>], "type">>;
16
+ }, z.core.$strict>>;
23
17
  slots: z.ZodArray<z.ZodObject<{
24
18
  key: z.ZodString;
25
19
  accepts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -49,8 +43,8 @@ export declare const ThemeConfigSchema: z.ZodObject<{
49
43
  assets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
50
44
  path: z.ZodString;
51
45
  type: z.ZodEnum<{
52
- icon: "icon";
53
46
  image: "image";
47
+ icon: "icon";
54
48
  background: "background";
55
49
  }>;
56
50
  description: z.ZodString;
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, dirname } from "node:path";
3
3
  import * as z from "zod";
4
- import { AcceptType, AssetType, ParameterType, Variant, } from "../types.js";
4
+ import { AcceptType, AssetType, Variant } from "../types.js";
5
5
  import { strict } from "./strict.js";
6
6
  /**
7
7
  * Fail-fast runtime validation for a `theme.json`. The hand-written types in
@@ -63,19 +63,11 @@ const ThemeFontSchema = strict({
63
63
  path: z.string(),
64
64
  weight: z.number().optional(),
65
65
  });
66
- const TemplateParamSchema = strict({
66
+ const ParameterSchema = strict({
67
67
  shapeName: z.string(),
68
68
  required: z.boolean().optional(),
69
- type: z.literal(ParameterType.Template),
70
69
  template: z.string(),
71
70
  });
72
- const ImageParamSchema = strict({
73
- shapeName: z.string(),
74
- required: z.boolean().optional(),
75
- key: z.string(),
76
- type: z.literal(ParameterType.Image),
77
- });
78
- const ParameterSchema = z.discriminatedUnion("type", [TemplateParamSchema, ImageParamSchema]);
79
71
  // One arm per accept type, discriminated by `type`, mirroring `CompilerBlock`:
80
72
  // `startAt` lives only on the text arm, and `bodyRows` only on the table arm (a
81
73
  // table block MUST declare its repeatable-row range; text/image arms have no
@@ -27,18 +27,6 @@ export type AssetEntry = {
27
27
  };
28
28
  /** Two-level catalog: `{ category: { name: AssetEntry } }`. */
29
29
  export type AssetCatalog = Record<string, Record<string, AssetEntry>>;
30
- /**
31
- * Discriminator for a layout's frontmatter *parameters* — inputs the author
32
- * writes as a single `key: value` line. Two kinds: `Template` (fills a styled
33
- * shape's runs via fillTemplate) and `Image` (a filesystem path filled via
34
- * fillImage). Both share their value with the engine's `SlotType`, since a
35
- * parameter maps straight to an engine slot with no resolution step.
36
- */
37
- export declare const ParameterType: {
38
- readonly Template: "template";
39
- readonly Image: "image";
40
- };
41
- export type ParameterType = (typeof ParameterType)[keyof typeof ParameterType];
42
30
  /**
43
31
  * The engine content types a slot may `accept`. A subset of the engine's
44
32
  * `SlotType`: `Template` is parameter-only, never a body block. There is no
@@ -143,14 +131,14 @@ export type CompilerDeck = {
143
131
  steps: CompilerDeckStep[];
144
132
  };
145
133
  /**
146
- * Template parameter: a styled text shape filled by expanding a `template` into the
134
+ * Parameter: a styled text shape filled by expanding a `template` into the
147
135
  * shape's paragraphs via fillTemplate. The template is one string with `{key}`
148
136
  * placeholders and newlines for line breaks (`"{lastname}, {firstname} -
149
137
  * {company}"`, or `"{name}\n{jobTitle}"`); a shape's fillable keys are the
150
138
  * placeholders in its template. The shape carries no top-level `key`; its
151
139
  * placeholders are the keys the author fills in frontmatter.
152
140
  */
153
- export type CompilerTemplateParameter = {
141
+ export type CompilerParameter = {
154
142
  shapeName: string;
155
143
  /**
156
144
  * Whether the parameter may be omitted from a slide. Optional (defaults to
@@ -159,29 +147,9 @@ export type CompilerTemplateParameter = {
159
147
  * from the slide.
160
148
  */
161
149
  required?: boolean;
162
- type: typeof ParameterType.Template;
163
150
  /** The shape's text as one template with `{key}` placeholders; newlines are line breaks. */
164
151
  template: string;
165
152
  };
166
- /** Image parameter: one frontmatter path filled by fillImage. Sizing/crop
167
- * behaviour comes from the resolved asset's `type`, not the slot. */
168
- export type CompilerImageParameter = {
169
- shapeName: string;
170
- /**
171
- * Whether the parameter may be omitted from a slide. Optional (defaults to
172
- * false): a required one with no value causes the compiler to throw with
173
- * layout + key names; an optional one left unfilled has its shape removed
174
- * from the slide.
175
- */
176
- required?: boolean;
177
- key: string;
178
- type: typeof ParameterType.Image;
179
- };
180
- /**
181
- * Compiler-facing parameter. A layout's frontmatter inputs — each written as a
182
- * single `key: value` line, resolved against the layout's `parameters` list.
183
- */
184
- export type CompilerParameter = CompilerTemplateParameter | CompilerImageParameter;
185
153
  /**
186
154
  * Reserved keys in a deck's frontmatter — global (theme) and per-slide
187
155
  * (layout). Exported so callers (e.g. cli.ts) reference the constants
@@ -10,18 +10,6 @@ export const AssetType = {
10
10
  Image: "image",
11
11
  Background: "background",
12
12
  };
13
- // ── ParameterType discriminator (frontmatter, one value) ──────────────────────
14
- /**
15
- * Discriminator for a layout's frontmatter *parameters* — inputs the author
16
- * writes as a single `key: value` line. Two kinds: `Template` (fills a styled
17
- * shape's runs via fillTemplate) and `Image` (a filesystem path filled via
18
- * fillImage). Both share their value with the engine's `SlotType`, since a
19
- * parameter maps straight to an engine slot with no resolution step.
20
- */
21
- export const ParameterType = {
22
- Template: SlotType.Template,
23
- Image: SlotType.Image,
24
- };
25
13
  // ── AcceptType discriminator (what a slot accepts) ────────────────────────────
26
14
  /**
27
15
  * The engine content types a slot may `accept`. A subset of the engine's
@@ -6,11 +6,30 @@ import type { CompilerThemeConfig } from "./markdown/types.js";
6
6
  * Throws if there is no frontmatter or no `name:` line — the caller names the file.
7
7
  */
8
8
  export declare function renameSkill(md: string, name: string): string;
9
+ /**
10
+ * The `package.json` a packaged skill installs from — deliberately NOT the theme's
11
+ * own. A theme repo's manifest is a development document: it carries the script
12
+ * that regenerates the skill, and lists the engine as a devDependency because the
13
+ * repo builds with it rather than shipping it.
14
+ *
15
+ * Copying that verbatim breaks the consumer twice. The build script runs as a
16
+ * postinstall inside their container, so anything it touches that is read-only
17
+ * fails their whole `npm install`. And under `--omit=dev` the engine is never
18
+ * installed, so neither the postinstall nor `npx tycoslide build` can find it.
19
+ *
20
+ * What ships instead declares only what the skill needs to RUN: the theme's own
21
+ * dependencies plus the engine, as runtime dependencies, and no scripts at all.
22
+ */
23
+ export declare function skillPackageJson(theme: Record<string, unknown>, engine: {
24
+ name: string;
25
+ version: string;
26
+ }): string;
9
27
  /**
10
28
  * Zip a theme into an uploadable Agent Skill archive whose entries all live
11
- * under a single root folder (e.g. `mz-slides/theme.json`), matching Anthropic's
29
+ * under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
12
30
  * custom-skill format. `generated` names the files the caller just wrote (the
13
- * config, manifest, skill.md, syntax.md). Optional support files are skipped
14
- * when absent; anything the config declares but that is missing is an error.
31
+ * config, manifest, skill.md, syntax.md); `packageJson` is the authored manifest
32
+ * from `skillPackageJson`. Optional support files are skipped when absent;
33
+ * anything the config declares but that is missing is an error.
15
34
  */
16
- export declare function zipDir(rootDir: string, folderName: string, config: CompilerThemeConfig, generated: string[]): Promise<Buffer>;
35
+ export declare function zipDir(rootDir: string, folderName: string, config: CompilerThemeConfig, generated: string[], packageJson: string): Promise<Buffer>;
package/dist/skillZip.js CHANGED
@@ -18,13 +18,42 @@ export function renameSkill(md, name) {
18
18
  throw new Error('skill.md frontmatter has no "name:" line');
19
19
  return md.replace(block[0], block[0].replace(NAME_LINE, `name: ${name}`));
20
20
  }
21
+ /** The manifest a packaged skill installs from, authored rather than copied. */
22
+ const PACKAGE_JSON = "package.json";
21
23
  /**
22
- * Files a packaged skill needs beyond the theme's own declarations. `package.json`
23
- * matters most: the unzip flow is `npm install` -> `npx tycoslide build`, so it
24
- * restores the engine and any npm-resolved brand fonts. The lockfile is taken
25
- * when present so that install is reproducible.
24
+ * Files a packaged skill needs beyond the theme's own declarations. Only the
25
+ * lockfile: `package.json` is authored by `skillPackageJson` rather than taken
26
+ * from the theme directory.
26
27
  */
27
- const SUPPORT_FILES = ["package.json", "package-lock.json"];
28
+ const SUPPORT_FILES = ["package-lock.json"];
29
+ /**
30
+ * The `package.json` a packaged skill installs from — deliberately NOT the theme's
31
+ * own. A theme repo's manifest is a development document: it carries the script
32
+ * that regenerates the skill, and lists the engine as a devDependency because the
33
+ * repo builds with it rather than shipping it.
34
+ *
35
+ * Copying that verbatim breaks the consumer twice. The build script runs as a
36
+ * postinstall inside their container, so anything it touches that is read-only
37
+ * fails their whole `npm install`. And under `--omit=dev` the engine is never
38
+ * installed, so neither the postinstall nor `npx tycoslide build` can find it.
39
+ *
40
+ * What ships instead declares only what the skill needs to RUN: the theme's own
41
+ * dependencies plus the engine, as runtime dependencies, and no scripts at all.
42
+ */
43
+ export function skillPackageJson(theme, engine) {
44
+ const dependencies = {
45
+ ...(theme.dependencies ?? {}),
46
+ [engine.name]: `^${engine.version}`,
47
+ };
48
+ const skill = {
49
+ name: theme.name,
50
+ version: theme.version,
51
+ description: theme.description,
52
+ private: true,
53
+ dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))),
54
+ };
55
+ return `${JSON.stringify(skill, null, 2)}\n`;
56
+ }
28
57
  /**
29
58
  * Every path a packaged theme needs, relative to `rootDir` and POSIX-separated.
30
59
  *
@@ -40,18 +69,20 @@ function skillPaths(config, generated) {
40
69
  }
41
70
  /**
42
71
  * Zip a theme into an uploadable Agent Skill archive whose entries all live
43
- * under a single root folder (e.g. `mz-slides/theme.json`), matching Anthropic's
72
+ * under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
44
73
  * custom-skill format. `generated` names the files the caller just wrote (the
45
- * config, manifest, skill.md, syntax.md). Optional support files are skipped
46
- * when absent; anything the config declares but that is missing is an error.
74
+ * config, manifest, skill.md, syntax.md); `packageJson` is the authored manifest
75
+ * from `skillPackageJson`. Optional support files are skipped when absent;
76
+ * anything the config declares but that is missing is an error.
47
77
  */
48
- export async function zipDir(rootDir, folderName, config, generated) {
78
+ export async function zipDir(rootDir, folderName, config, generated, packageJson) {
49
79
  const zip = new JSZip();
50
80
  const folder = zip.folder(folderName);
51
81
  if (!folder)
52
82
  throw new Error(`Failed to create zip folder: ${folderName}`);
83
+ folder.file(PACKAGE_JSON, packageJson);
53
84
  const optional = new Set(SUPPORT_FILES);
54
- let count = 0;
85
+ let count = 1;
55
86
  for (const rel of skillPaths(config, generated)) {
56
87
  const abs = join(rootDir, ...rel.split("/"));
57
88
  if (!existsSync(abs)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.11.4",
3
+ "version": "0.13.0",
4
4
  "description": "Create editable, on-brand PowerPoint slides from markdown.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,8 +16,8 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "tsc --build",
19
- "test": "tsc --build && node --test --experimental-transform-types test/**/*.test.ts",
20
- "typecheck": "tsc --noEmit",
19
+ "test": "tsc --build && tsc -p tsconfig.test.json && node --test --experimental-transform-types test/**/*.test.ts",
20
+ "typecheck": "tsc --build && tsc -p tsconfig.test.json",
21
21
  "lint": "biome check .",
22
22
  "lint:fix": "biome check --write .",
23
23
  "format": "biome format --write ."
package/syntax.md CHANGED
@@ -118,11 +118,6 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
118
118
  jobTitle: CEO, Acme Corp
119
119
  ```
120
120
  The engine substitutes each value into the run that carries its style, so if the designer made the name bold and the job title grey, the filled name stays bold and the filled title stays grey.
121
- - **`image`** -- a picture placeholder. Set it in frontmatter with the image path (from an asset catalog entry, or an absolute path). How it is scaled and cropped is set by the **asset's `type`** in the catalog (`icon` never enlarges past native and never crops; `image` fits the whole picture without cropping; `background` fills the frame and center-crops).
122
- ```yaml
123
- hero: assets/diagrams/architecture.png
124
- ```
125
-
126
121
  ### Body content shapes
127
122
 
128
123
  Fill a slot by writing a `::key::` region in the body; the marker maps to the slot with that `key`. A slot's manifest entry lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
@@ -139,7 +134,13 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
139
134
  ````
140
135
  The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting, using the theme's `codeTheme` (set once in `theme.json`, not per slot). Colors are applied as native text runs in the output, not images.
141
136
  - **table** (slots that accept `table`) -- a GFM table. Write it in the slot region between `|`-delimited headers and rows; cells inherit inline formatting (bold, italic, links).
142
- - **image** (slots that accept `image`) -- a picture. A fenced `mermaid` block renders to a themed PNG and fills it (see below).
137
+ - **image** (slots that accept `image`) -- a picture from the theme's asset catalog, written as `![]($category.name)`:
138
+ ```markdown
139
+ ::logo::
140
+
141
+ ![]($brand.primaryDarkWordmark)
142
+ ```
143
+ The categories and names are catalogued in `assets.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
143
144
 
144
145
  ---
145
146
 
@@ -200,20 +201,9 @@ The theme owns all styling. These directives are rejected at build time:
200
201
 
201
202
  ---
202
203
 
203
- ## Image parameters
204
-
205
- An image is a **parameter** -- one value (a path) on a frontmatter line. Reference it using the parameter key directly:
206
-
207
- ```yaml
208
- ---
209
- layout: ImageSlide
210
- title: Architecture Diagram
211
- hero: assets/diagrams/architecture.png
212
- ---
213
- ```
204
+ ## Layout declarations
214
205
 
215
206
  Each parameter or slot in the layout definition may declare:
216
- - **`type`** (parameters, required) -- `template` or `image`.
217
207
  - **`accepts`** (slots, required) -- an array of `text`, `table`, `image`.
218
208
  - **`required: true`** -- the slide has no usable default and the build fails if the parameter/slot has no value (e.g. team-member photos, icon-grid icons, the quote logo). If you don't have a suitable image, ask the user for one.
219
209
  - **optional (the default)** -- a parameter or slot you leave unfilled is dropped from the slide (its shape is removed), so a layout with numbered slots (e.g. up to six sections, up to four stats) renders only the ones you fill.
@@ -309,6 +299,9 @@ Infrastructure:
309
299
  ---
310
300
  layout: ImageSlide
311
301
  title: Office Map
312
- hero: assets/images/office-floor-plan.png
313
302
  ---
303
+
304
+ ::hero::
305
+
306
+ ![]($images.officeFloorPlan)
314
307
  ```