@tycoworks/tycoslide 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # tycoslide
2
2
 
3
- Let AI agents build slides from existing PowerPoint (`.pptx`) files.
3
+ Create editable, on-brand PowerPoint slides from markdown.
4
4
 
5
5
  > **Early release** — tycoslide is under active development.
6
6
 
7
7
  ## How it works
8
8
 
9
- 1. **tycoslide wraps your PowerPoint file as an agent skill.**
10
- 2. **Your agents use the skill to write slides in markdown.**
11
- 3. **tycoslide builds a finished PowerPoint file from the markdown.**
9
+ 1. **tycoslide wraps existing PowerPoint files as reusable templates.**
10
+ 2. **You (or an agent) write slides in markdown.**
11
+ 3. **tycoslide builds new PowerPoint files.**
12
12
 
13
13
  ## Quick Start
14
14
 
@@ -42,16 +42,15 @@ title: Highlights
42
42
  Build:
43
43
 
44
44
  ```bash
45
- tycoslide build deck.md # → deck.pptx
45
+ npx tycoslide build deck.md # → deck.pptx
46
46
  ```
47
47
 
48
48
  ## CLI
49
49
 
50
50
  ```bash
51
- tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
52
- tycoslide build deck.md --no-notes # omit speaker notes from the output
53
- tycoslide plugin # generate AI agent plugin package
54
- tycoslide manifest # print layout + asset catalog to stdout
51
+ npx tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
52
+ npx tycoslide build deck.md --no-notes # omit speaker notes from the output
53
+ npx tycoslide package # regenerate skill.md/syntax.md/manifest.json + zip the whole theme into a self-contained <package-name>.zip
55
54
  ```
56
55
 
57
56
  ## Theme Structure
@@ -65,6 +64,10 @@ my-theme/
65
64
  assets/icons/
66
65
  theme.json
67
66
  package.json
67
+ skill.md # generated by `tycoslide package`
68
+ syntax.md # generated by `tycoslide package`
69
+ manifest.json # generated by `tycoslide package`
70
+ <package-name>.zip # uploadable Agent Skill bundle
68
71
  ```
69
72
 
70
73
  **Template** — the PPTX file with named shapes that tycoslide fills.
package/SKILL.md CHANGED
@@ -1,17 +1,15 @@
1
1
  ---
2
2
  name: slides
3
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."
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."
8
6
  ---
9
7
 
10
8
  # slides
11
9
 
12
10
  ## Setup
13
11
 
14
- Before first use, install dependencies from the plugin root:
12
+ Before first use, install dependencies from the theme root:
15
13
 
16
14
  ```bash
17
15
  npm install
@@ -48,7 +46,7 @@ A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
48
46
 
49
47
  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
48
 
51
- Study each layout's `whenToUse` and `limit`s before writing any slides. These are your primary guide for matching content to layouts.
49
+ Study each layout's `slots` and `limit`s before writing any slides.
52
50
 
53
51
  ---
54
52
 
@@ -131,7 +129,7 @@ The deck is written to your current working directory (not inside the skill).
131
129
 
132
130
  ### For Each Slide
133
131
 
134
- **Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides.
132
+ **Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides. Put the takeaway in the headline, then support it.
135
133
 
136
134
  Check each layout's `limit` in the manifest for content density constraints. When content overflows, split across slides.
137
135
 
@@ -143,6 +141,7 @@ Check each layout's `limit` in the manifest for content density constraints. Whe
143
141
  - **Don't open with a body/content layout** -- use the Title layout for impact
144
142
  - **Don't skip section dividers** -- for decks over 5 slides, use Section title layouts to group sections
145
143
  - **Don't restyle the layout** -- the theme owns all design; you only fill slots
144
+ - **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration; if the build warns an image shrank to a small %, swap it for a simpler one
146
145
  - **Don't invent layout or asset names** -- only use what exists in the manifest
147
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
148
147
  - **Don't mix dark and light** -- keep one variant across the deck
package/dist/cli.js CHANGED
@@ -1,20 +1,20 @@
1
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
2
  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
6
  import { generateManifest } from "./manifest.js";
7
7
  import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
8
+ import { renameSkill, zipDir } from "./skillZip.js";
8
9
  const DEFAULT_CONFIG = "theme.json";
9
- const SKILL_DIR = "skills/slides";
10
- const PLUGIN_DIR = ".claude-plugin";
11
- const PLUGIN_FILE = "plugin.json";
12
10
  const MANIFEST_FILE = "manifest.json";
13
- const SKILL_FILE = "SKILL.md";
11
+ // The theme skill is written as lowercase skill.md (copied from tycoslide's own
12
+ // SKILL.md), so the skill folder can live at the theme repo root.
13
+ const SKILL_FILE = "skill.md";
14
14
  const SYNTAX_FILE = "syntax.md";
15
15
  const BUILD_COMMAND = "npx tycoslide build";
16
16
  const sdkDir = dirname(fileURLToPath(import.meta.url));
17
- const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
17
+ const skillMdPath = resolve(sdkDir, "..", "SKILL.md");
18
18
  const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
19
19
  const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
20
20
  const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
@@ -50,49 +50,36 @@ program
50
50
  await buildDeck(deck, config, { excludeNotes: !opts.notes });
51
51
  });
52
52
  program
53
- .command("manifest")
54
- .description("Generate manifest.json from theme config")
53
+ .command("package")
54
+ .description("Generate the Agent Skill (manifest.json, SKILL.md, syntax.md) for AI agents")
55
55
  .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
56
- .option(`-o, --out <file>`, "write to file instead of stdout")
57
56
  .action(async (opts) => {
58
57
  const config = loadThemeConfig(resolve(process.cwd(), opts.config));
59
- const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
60
- if (opts.out) {
61
- writeFileSync(resolve(process.cwd(), opts.out), `${json}\n`);
62
- console.log(`WROTE ${opts.out}`);
58
+ const themePkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
59
+ if (!themePkg.name) {
60
+ throw new Error('Cannot name the skill: the theme\'s package.json has no "name" field.');
63
61
  }
64
- else {
65
- process.stdout.write(`${json}\n`);
62
+ // basename drops any npm scope, e.g. "@acme/mz-slides" -> "mz-slides".
63
+ const skillName = basename(themePkg.name);
64
+ const manifestJson = `${generateManifest(config, { build: { command: BUILD_COMMAND } })}\n`;
65
+ writeFileSync(resolve(process.cwd(), MANIFEST_FILE), manifestJson);
66
+ console.log(`WROTE ${MANIFEST_FILE}`);
67
+ let skillMd;
68
+ try {
69
+ skillMd = renameSkill(readFileSync(skillMdPath, "utf-8"), skillName);
66
70
  }
67
- });
68
- program
69
- .command("plugin")
70
- .description("Generate plugin package (plugin.json, manifest.json, SKILL.md, syntax.md) for AI agents")
71
- .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
72
- .action(async (opts) => {
73
- const config = loadThemeConfig(resolve(process.cwd(), opts.config));
74
- const cwd = process.cwd();
75
- const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
76
- const pluginMeta = {
77
- name: pkg.name,
78
- version: pkg.version,
79
- description: pkg.description ?? "",
80
- skills: "./skills",
81
- };
82
- if (pkg.author)
83
- pluginMeta.author = pkg.author;
84
- const pluginDir = resolve(cwd, PLUGIN_DIR);
85
- mkdirSync(pluginDir, { recursive: true });
86
- writeFileSync(resolve(pluginDir, PLUGIN_FILE), `${JSON.stringify(pluginMeta, null, 2)}\n`);
87
- console.log(`WROTE ${PLUGIN_DIR}/${PLUGIN_FILE}`);
88
- const skillDir = resolve(cwd, SKILL_DIR);
89
- mkdirSync(skillDir, { recursive: true });
90
- const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
91
- writeFileSync(resolve(skillDir, MANIFEST_FILE), `${json}\n`);
92
- console.log(`WROTE ${SKILL_DIR}/${MANIFEST_FILE}`);
93
- copyFileSync(skillMdPath, resolve(skillDir, SKILL_FILE));
94
- console.log(`WROTE ${SKILL_DIR}/${SKILL_FILE}`);
95
- copyFileSync(syntaxMdPath, resolve(skillDir, SYNTAX_FILE));
96
- console.log(`WROTE ${SKILL_DIR}/${SYNTAX_FILE}`);
71
+ catch (err) {
72
+ throw new Error(`${skillMdPath}: ${err.message}`);
73
+ }
74
+ writeFileSync(resolve(process.cwd(), SKILL_FILE), skillMd);
75
+ console.log(`WROTE ${SKILL_FILE}`);
76
+ const syntaxMd = readFileSync(syntaxMdPath, "utf-8");
77
+ writeFileSync(resolve(process.cwd(), SYNTAX_FILE), syntaxMd);
78
+ console.log(`WROTE ${SYNTAX_FILE}`);
79
+ // Bundle the WHOLE theme so the skill is self-contained: unzip ->
80
+ // `npm install` (pulls the engine + its deps) -> `npx tycoslide build`.
81
+ const zipFile = `${skillName}.zip`;
82
+ writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName));
83
+ console.log(`WROTE ${zipFile}`);
97
84
  });
98
85
  await program.parseAsync(process.argv);
@@ -4,8 +4,10 @@ import { type BlockHandler } from "../types.js";
4
4
  * Recognize a fenced code block (any language except mermaid) at a region's top
5
5
  * level, folding it to a Text fill, and compile it by Shiki-highlighting its
6
6
  * source into a TextFill. Theme resolution is strict: the theme MUST declare a
7
- * `codeTheme` (one style per theme — the design-system framing); a deck with
8
- * code fences but no theme-level `codeTheme` throws, naming the offending slot.
7
+ * `codeTheme` — either one Shiki id (one style per theme — the design-system
8
+ * framing) or a `{ light, dark }` pair the layout's `variant` selects (a pair
9
+ * with no layout `variant` throws). A deck with code fences but no theme-level
10
+ * `codeTheme` throws, naming the offending slot.
9
11
  */
10
12
  export declare const CODE: BlockHandler;
11
13
  /**
@@ -5,8 +5,10 @@ import { MERMAID_LANG } from "./mermaid.js";
5
5
  * Recognize a fenced code block (any language except mermaid) at a region's top
6
6
  * level, folding it to a Text fill, and compile it by Shiki-highlighting its
7
7
  * source into a TextFill. Theme resolution is strict: the theme MUST declare a
8
- * `codeTheme` (one style per theme — the design-system framing); a deck with
9
- * code fences but no theme-level `codeTheme` throws, naming the offending slot.
8
+ * `codeTheme` — either one Shiki id (one style per theme — the design-system
9
+ * framing) or a `{ light, dark }` pair the layout's `variant` selects (a pair
10
+ * with no layout `variant` throws). A deck with code fences but no theme-level
11
+ * `codeTheme` throws, naming the offending slot.
10
12
  */
11
13
  export const CODE = {
12
14
  match: (node) => node.type === MdastType.Code && node.lang !== MERMAID_LANG,
@@ -19,11 +21,24 @@ export const CODE = {
19
21
  throw new Error(`Slide ${ctx.slideIdx}: layout "${ctx.layoutName}" slot content (from ${ctx.source}) has a code fence ` +
20
22
  "with no language; add one after the opening ``` (e.g. ```sql).");
21
23
  }
22
- const theme = ctx.config.codeTheme;
23
- if (!theme) {
24
+ const codeTheme = ctx.config.codeTheme;
25
+ if (!codeTheme) {
24
26
  throw new Error(`Layout "${ctx.layoutName}" slot content (from ${ctx.source}): deck contains a code fence but the theme ` +
25
27
  'declares no "codeTheme". Add a theme-level "codeTheme" (a Shiki theme id) to theme.json.');
26
28
  }
29
+ // A single string is one style for every code layout; a `{ light, dark }`
30
+ // pair requires the layout to declare which via `variant` — no default.
31
+ let theme;
32
+ if (typeof codeTheme === "string") {
33
+ theme = codeTheme;
34
+ }
35
+ else if (ctx.layoutVariant) {
36
+ theme = codeTheme[ctx.layoutVariant];
37
+ }
38
+ else {
39
+ throw new Error(`Layout "${ctx.layoutName}" slot content (from ${ctx.source}): the theme's "codeTheme" is a ` +
40
+ '{ light, dark } pair, but this layout declares no "variant". Add variant: "light" or "dark" to the layout.');
41
+ }
27
42
  return { paragraphs: await highlightCode(code.value, code.lang, theme) };
28
43
  },
29
44
  };
@@ -223,6 +223,7 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
223
223
  slideIdx: index,
224
224
  source: "body content",
225
225
  config,
226
+ layoutVariant: layoutDef.variant,
226
227
  });
227
228
  // Validate the slot accepts this region's type BEFORE running the (possibly
228
229
  // expensive — Shiki, Playwright) fill: a mismatched region fails fast without
@@ -238,7 +239,14 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
238
239
  `Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
239
240
  }
240
241
  const source = `::${name}::`;
241
- const parsed = parseSlotContent(text, { resolveAssetRef, layoutName, slideIdx: index, source, config });
242
+ const parsed = parseSlotContent(text, {
243
+ resolveAssetRef,
244
+ layoutName,
245
+ slideIdx: index,
246
+ source,
247
+ config,
248
+ layoutVariant: layoutDef.variant,
249
+ });
242
250
  assertSlotRegion(slot, parsed.acceptType, layoutName, index, source);
243
251
  content[name] = await parsed.fill();
244
252
  }
@@ -7,6 +7,10 @@ export declare const ThemeConfigSchema: z.ZodObject<{
7
7
  description: z.ZodOptional<z.ZodString>;
8
8
  whenToUse: z.ZodOptional<z.ZodString>;
9
9
  whenNotToUse: z.ZodOptional<z.ZodString>;
10
+ variant: z.ZodOptional<z.ZodEnum<{
11
+ light: "light";
12
+ dark: "dark";
13
+ }>>;
10
14
  parameters: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
11
15
  shapeName: z.ZodString;
12
16
  limit: z.ZodOptional<z.ZodObject<{
@@ -79,7 +83,10 @@ export declare const ThemeConfigSchema: z.ZodObject<{
79
83
  path: z.ZodString;
80
84
  weight: z.ZodOptional<z.ZodNumber>;
81
85
  }, z.core.$strict>>>;
82
- codeTheme: z.ZodOptional<z.ZodString>;
86
+ codeTheme: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
87
+ light: z.ZodString;
88
+ dark: z.ZodString;
89
+ }, z.core.$strict>]>>;
83
90
  mermaidVariant: z.ZodOptional<z.ZodString>;
84
91
  }, z.core.$strict>;
85
92
  /**
@@ -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 } from "../types.js";
4
+ import { AcceptType, AssetType, ParameterType, 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
@@ -23,6 +23,7 @@ import { strict } from "./strict.js";
23
23
  // Reuse the const-object enums as runtime values — no third copy of the literals.
24
24
  const acceptTypeSchema = z.enum(Object.values(AcceptType));
25
25
  const assetTypeSchema = z.enum(Object.values(AssetType));
26
+ const variantSchema = z.enum(Object.values(Variant));
26
27
  // Re-declared here (not imported from the engine) so the schema layer never
27
28
  // depends on the engine — mirrors engine `Frame`, guarded by `_drift`.
28
29
  const FrameSchema = strict({
@@ -98,6 +99,7 @@ const LayoutSchema = strict({
98
99
  description: z.string().optional(),
99
100
  whenToUse: z.string().optional(),
100
101
  whenNotToUse: z.string().optional(),
102
+ variant: variantSchema.optional(),
101
103
  parameters: z.array(ParameterSchema),
102
104
  slots: z.array(SlotSchema),
103
105
  });
@@ -108,7 +110,7 @@ export const ThemeConfigSchema = strict({
108
110
  outputDir: z.string().optional(),
109
111
  mermaid: MermaidConfigSchema.optional(),
110
112
  fonts: z.array(ThemeFontSchema).optional(),
111
- codeTheme: z.string().optional(),
113
+ codeTheme: z.union([z.string(), strict({ light: z.string().min(1), dark: z.string().min(1) })]).optional(),
112
114
  mermaidVariant: z.string().optional(),
113
115
  });
114
116
  const _drift = true;
@@ -54,6 +54,17 @@ export declare const AcceptType: {
54
54
  readonly Image: "image";
55
55
  };
56
56
  export type AcceptType = (typeof AcceptType)[keyof typeof AcceptType];
57
+ /**
58
+ * The tonal surface a layout sits on. A code layout on a light panel needs a
59
+ * light Shiki theme (dark syntax would wash out); one on a dark panel needs a
60
+ * dark theme. A layout declares its `variant`; the code compile resolves a
61
+ * `{ light, dark }` `codeTheme` pair against it (a pair with no `variant` throws).
62
+ */
63
+ export declare const Variant: {
64
+ readonly Light: "light";
65
+ readonly Dark: "dark";
66
+ };
67
+ export type Variant = (typeof Variant)[keyof typeof Variant];
57
68
  /**
58
69
  * The four engine content shapes a slot's compiled content can be — the value
59
70
  * `compile` produces and the engine fills. ImageFill carries a `.type`
@@ -85,6 +96,12 @@ export type BlockContext = {
85
96
  slideIdx: number;
86
97
  source: string;
87
98
  config: CompilerConfig;
99
+ /**
100
+ * The current layout's tonal surface, threaded from its `variant`. The code
101
+ * compile reads it to pick the arm of a `{ light, dark }` `codeTheme` pair;
102
+ * a pair with no `variant` throws (no default).
103
+ */
104
+ layoutVariant?: Variant;
88
105
  };
89
106
  /**
90
107
  * A block handler recognizes one content kind at the region's top level, folds
@@ -233,6 +250,11 @@ export type CompilerLayout = {
233
250
  description?: string;
234
251
  whenToUse?: string;
235
252
  whenNotToUse?: string;
253
+ /**
254
+ * The layout's tonal surface. Selects the arm of a `{ light, dark }` `codeTheme`
255
+ * pair for code fences on this layout. Required when `codeTheme` is a pair.
256
+ */
257
+ variant?: Variant;
236
258
  /** Frontmatter inputs (template, image) — one value per `key: value` line. */
237
259
  parameters: CompilerParameter[];
238
260
  /** Body regions — the default body or `::name::` regions; each `accepts` blocks. */
@@ -276,9 +298,14 @@ export type CompilerThemeConfig = {
276
298
  * Theme-level defaults for the two content types the compiler resolves before
277
299
  * the engine sees them. One code style and one mermaid style per theme (the
278
300
  * design-system framing) — they can't sit on a multi-type slot. `codeTheme` is
279
- * a Shiki theme id; `mermaidVariant` names an entry in `mermaid`.
301
+ * a Shiki theme id, or a `{ light, dark }` pair when the theme has both light
302
+ * and dark code layouts (each layout's `variant` picks the arm);
303
+ * `mermaidVariant` names an entry in `mermaid`.
280
304
  */
281
- codeTheme?: string;
305
+ codeTheme?: string | {
306
+ light: string;
307
+ dark: string;
308
+ };
282
309
  mermaidVariant?: string;
283
310
  };
284
311
  export type CompilerConfig = CompilerThemeConfig & {
@@ -37,6 +37,17 @@ export const AcceptType = {
37
37
  Table: SlotType.Table,
38
38
  Image: SlotType.Image,
39
39
  };
40
+ // ── Variant discriminator (a layout's light/dark surface) ─────────────────────
41
+ /**
42
+ * The tonal surface a layout sits on. A code layout on a light panel needs a
43
+ * light Shiki theme (dark syntax would wash out); one on a dark panel needs a
44
+ * dark theme. A layout declares its `variant`; the code compile resolves a
45
+ * `{ light, dark }` `codeTheme` pair against it (a pair with no `variant` throws).
46
+ */
47
+ export const Variant = {
48
+ Light: "light",
49
+ Dark: "dark",
50
+ };
40
51
  /**
41
52
  * Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
42
53
  * (layout, body). Exported so callers (e.g. cli.ts) reference the constants
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Rewrite the `name:` value in a skill.md's leading YAML frontmatter so the
3
+ * packaged skill is named after the consuming theme, not the source template.
4
+ * Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
5
+ * Throws if there is no frontmatter or no `name:` line — the caller names the file.
6
+ */
7
+ export declare function renameSkill(md: string, name: string): string;
8
+ /**
9
+ * Zip an entire theme directory into an uploadable Agent Skill archive whose
10
+ * entries all live under a single root folder (e.g. `mz-slides/theme.json`),
11
+ * matching Anthropic's custom-skill format. Recursively includes every file
12
+ * except node_modules, hidden entries (any name starting with `.`), and
13
+ * top-level build artifacts (.pptx/.pdf/.zip at the repo root; the template
14
+ * .pptx under template/ is kept). Subdirectory structure is preserved with
15
+ * POSIX slashes. Fails fast if nothing is left to zip.
16
+ */
17
+ export declare function zipDir(rootDir: string, folderName: string): Promise<Buffer>;
@@ -0,0 +1,64 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { extname, join, relative, sep } from "node:path";
3
+ import JSZip from "jszip";
4
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
5
+ const NAME_LINE = /^name:[ \t]*.*$/m;
6
+ /**
7
+ * Rewrite the `name:` value in a skill.md's leading YAML frontmatter so the
8
+ * packaged skill is named after the consuming theme, not the source template.
9
+ * Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
10
+ * Throws if there is no frontmatter or no `name:` line — the caller names the file.
11
+ */
12
+ export function renameSkill(md, name) {
13
+ const block = md.match(FRONTMATTER);
14
+ if (!block)
15
+ throw new Error("skill.md has no YAML frontmatter block");
16
+ if (!NAME_LINE.test(block[1]))
17
+ throw new Error('skill.md frontmatter has no "name:" line');
18
+ return md.replace(block[0], block[0].replace(NAME_LINE, `name: ${name}`));
19
+ }
20
+ // Never packaged: dependencies (npm install rebuilds them) and hidden entries
21
+ // (name starting with "." — covers VCS, tooling, caches, secrets like .env/.npmrc).
22
+ const EXCLUDE_DIRS = new Set(["node_modules"]);
23
+ // Build artifacts, dropped ONLY at the repo root: decks build to cwd
24
+ // (showcase.pptx, deck.pptx, the output .zip), while the template .pptx lives
25
+ // under template/ and must be kept — so these extensions are pruned top-level only.
26
+ const ROOT_ARTIFACT_EXTS = new Set([".pptx", ".pdf", ".zip"]);
27
+ /**
28
+ * Zip an entire theme directory into an uploadable Agent Skill archive whose
29
+ * entries all live under a single root folder (e.g. `mz-slides/theme.json`),
30
+ * matching Anthropic's custom-skill format. Recursively includes every file
31
+ * except node_modules, hidden entries (any name starting with `.`), and
32
+ * top-level build artifacts (.pptx/.pdf/.zip at the repo root; the template
33
+ * .pptx under template/ is kept). Subdirectory structure is preserved with
34
+ * POSIX slashes. Fails fast if nothing is left to zip.
35
+ */
36
+ export async function zipDir(rootDir, folderName) {
37
+ const zip = new JSZip();
38
+ const folder = zip.folder(folderName);
39
+ if (!folder)
40
+ throw new Error(`Failed to create zip folder: ${folderName}`);
41
+ let count = 0;
42
+ const walk = (dir) => {
43
+ const atRoot = dir === rootDir;
44
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
45
+ if (entry.name.startsWith("."))
46
+ continue;
47
+ const abs = join(dir, entry.name);
48
+ if (entry.isDirectory()) {
49
+ if (!EXCLUDE_DIRS.has(entry.name))
50
+ walk(abs);
51
+ }
52
+ else if (entry.isFile()) {
53
+ if (atRoot && ROOT_ARTIFACT_EXTS.has(extname(entry.name)))
54
+ continue;
55
+ folder.file(relative(rootDir, abs).split(sep).join("/"), readFileSync(abs));
56
+ count++;
57
+ }
58
+ }
59
+ };
60
+ walk(rootDir);
61
+ if (count === 0)
62
+ throw new Error(`No files to zip in directory: ${rootDir}`);
63
+ return zip.generateAsync({ type: "nodebuffer" });
64
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
+ "description": "Create editable, on-brand PowerPoint slides from markdown.",
4
5
  "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
@@ -25,6 +26,7 @@
25
26
  "@xmldom/xmldom": "^0.9.10",
26
27
  "commander": "^15.0.0",
27
28
  "image-size": "^2.0.2",
29
+ "jszip": "^3.10.1",
28
30
  "mermaid": "^11.16.1",
29
31
  "playwright": "^1.58.2",
30
32
  "pptx-automizer": "^0.8.2",
@@ -39,7 +41,6 @@
39
41
  "devDependencies": {
40
42
  "@biomejs/biome": "^2.5.1",
41
43
  "@types/node": "^22.20.0",
42
- "jszip": "^3.10.1",
43
44
  "typescript": "^5.8.0"
44
45
  }
45
46
  }
package/syntax.md CHANGED
@@ -238,7 +238,7 @@ notes: |
238
238
  ---
239
239
  ```
240
240
 
241
- To build with all speaker notes omitted, pass `--no-notes` to `tycoslide build` (see [README](README.md#cli)).
241
+ To build with all speaker notes omitted, pass `--no-notes` to `npx tycoslide build` (see [README](README.md#cli)).
242
242
 
243
243
  ---
244
244