@tycoworks/tycoslide 0.13.1 → 0.13.3

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
@@ -72,6 +72,7 @@ my-theme/
72
72
  syntax.md # generated by `tycoslide package`
73
73
  manifest.json # generated by `tycoslide package` -- the theme's layouts
74
74
  assets.json # generated by `tycoslide package` -- the theme's picture catalog
75
+ assets.dat # generated by `tycoslide package` -- every declared asset, in one file
75
76
  <package-name>.zip # uploadable Agent Skill bundle
76
77
  ```
77
78
 
@@ -81,6 +82,8 @@ my-theme/
81
82
  **Manifest** — the machine-readable list of layouts, for an AI agent to read.
82
83
  **Catalog** — the machine-readable list of assets, for an AI agent to search.
83
84
 
85
+ A packaged skill ships its assets as one `assets.dat` archive. `tycoslide build` expands it automatically before filling; the catalog stays a plain file.
86
+
84
87
  ## Diagrams
85
88
 
86
89
  Mermaid blocks are rendered with a headless Chrome. tycoslide does not download one.
package/dist/cli.js CHANGED
@@ -2,18 +2,13 @@ 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
+ import { ASSETS_FILE, MANIFEST_FILE, SKILL_FILE, SYNTAX_FILE, THEME_CONFIG } from "./files.js";
5
6
  import { buildDeck } from "./index.js";
6
- import { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
7
+ import { generateAssetCatalog, generateManifest } from "./manifest.js";
7
8
  import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
8
9
  import { renameSkill, skillPackageJson, zipDir } from "./skillZip.js";
9
- const DEFAULT_CONFIG = "theme.json";
10
- const MANIFEST_FILE = "manifest.json";
11
- // SKILL.md, uppercase: the Agent Skills format requires that exact filename at the
12
- // root of a skill folder, and a case-sensitive filesystem will not find any other.
13
- const SKILL_FILE = "SKILL.md";
14
- const SYNTAX_FILE = "syntax.md";
15
10
  const sdkDir = dirname(fileURLToPath(import.meta.url));
16
- const skillMdPath = resolve(sdkDir, "..", "SKILL.md");
11
+ const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
17
12
  const syntaxMdPath = resolve(sdkDir, "..", SYNTAX_FILE);
18
13
  const pkg = JSON.parse(readFileSync(resolve(sdkDir, "..", "package.json"), "utf-8"));
19
14
  const program = new Command().name("tycoslide").description("PPTX template engine CLI").version(pkg.version);
@@ -53,7 +48,7 @@ program
53
48
  program
54
49
  .command("package")
55
50
  .description("Generate the Agent Skill (manifest.json, SKILL.md, syntax.md) for AI agents")
56
- .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
51
+ .option(`-c, --config <path>`, "path to theme config file", THEME_CONFIG)
57
52
  .action(async (opts) => {
58
53
  const config = loadThemeConfig(resolve(process.cwd(), opts.config));
59
54
  const themePkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The file names of a packaged theme (Agent Skill), in one place so the layout
3
+ * of a skill reads on one screen. The engine's TEMPLATE_DIR is not here: it is
4
+ * an engine concern, and the engine stays product-generic.
5
+ */
6
+ export declare const THEME_CONFIG = "theme.json";
7
+ /** The layouts document: read whole, so it carries no open-ended list. */
8
+ export declare const MANIFEST_FILE = "manifest.json";
9
+ /** The searchable asset catalog, named by the manifest that points at it. */
10
+ export declare const ASSETS_FILE = "assets.json";
11
+ /**
12
+ * The one archive a packaged theme's declared assets ship inside. Entries are
13
+ * stored at theme-relative POSIX paths and never rewritten: packaging writes
14
+ * them, building expands them. Zip format, deliberately NOT named .zip: the
15
+ * upload host rejects nested .zip entries by extension while accepting .pptx,
16
+ * which is also a zip -- so the archive travels under a neutral name.
17
+ */
18
+ export declare const ASSETS_ARCHIVE = "assets.dat";
19
+ export declare const SKILL_FILE = "SKILL.md";
20
+ export declare const SYNTAX_FILE = "syntax.md";
21
+ /** The manifest a packaged skill installs from, authored rather than copied. */
22
+ export declare const PACKAGE_JSON = "package.json";
package/dist/files.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The file names of a packaged theme (Agent Skill), in one place so the layout
3
+ * of a skill reads on one screen. The engine's TEMPLATE_DIR is not here: it is
4
+ * an engine concern, and the engine stays product-generic.
5
+ */
6
+ export const THEME_CONFIG = "theme.json";
7
+ /** The layouts document: read whole, so it carries no open-ended list. */
8
+ export const MANIFEST_FILE = "manifest.json";
9
+ /** The searchable asset catalog, named by the manifest that points at it. */
10
+ export const ASSETS_FILE = "assets.json";
11
+ /**
12
+ * The one archive a packaged theme's declared assets ship inside. Entries are
13
+ * stored at theme-relative POSIX paths and never rewritten: packaging writes
14
+ * them, building expands them. Zip format, deliberately NOT named .zip: the
15
+ * upload host rejects nested .zip entries by extension while accepting .pptx,
16
+ * which is also a zip -- so the archive travels under a neutral name.
17
+ */
18
+ export const ASSETS_ARCHIVE = "assets.dat";
19
+ // SKILL.md, uppercase: the Agent Skills format requires that exact filename at
20
+ // the root of a skill folder, and a case-sensitive filesystem will not find any
21
+ // other.
22
+ export const SKILL_FILE = "SKILL.md";
23
+ export const SYNTAX_FILE = "syntax.md";
24
+ /** The manifest a packaged skill installs from, authored rather than copied. */
25
+ export const PACKAGE_JSON = "package.json";
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ 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 { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
37
+ export { ASSETS_ARCHIVE, ASSETS_FILE } from "./files.js";
38
+ export { generateAssetCatalog, generateManifest } from "./manifest.js";
38
39
  export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
39
40
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
41
+ export { expandAssets } from "./skillZip.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { generate, SlotType, } from "./engine/index.js";
2
2
  import { AcceptType, } from "./markdown/types.js";
3
+ import { expandAssets } from "./skillZip.js";
3
4
  /**
4
5
  * A frontmatter parameter always fills one physical shape on the layout's own
5
6
  * slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
@@ -112,12 +113,18 @@ export async function buildDeck(deck, config, options = {}) {
112
113
  if (deck.output === undefined) {
113
114
  throw new Error('buildDeck: deck.output is not set. Set it (e.g. "deck.pptx") before calling buildDeck.');
114
115
  }
116
+ // A packaged theme ships its assets as one archive, because hosts cap how many
117
+ // files a skill may contain. Expand it here rather than at compile: the catalog
118
+ // is what an author reads, and only filling needs the bytes.
119
+ await expandAssets(config.rootDir);
115
120
  const engineDeck = { theme: deck.theme, output: deck.output, steps: deck.steps };
116
121
  await generate(engineDeck, toEngineConfig(config), options);
117
122
  }
118
123
  // Engine — primitives-only public surface.
119
124
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
125
+ export { ASSETS_ARCHIVE, ASSETS_FILE } from "./files.js";
120
126
  // Authoring
121
- export { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
127
+ export { generateAssetCatalog, generateManifest } from "./manifest.js";
122
128
  // Markdown / Compiler
123
129
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
130
+ export { expandAssets } from "./skillZip.js";
@@ -1,6 +1,4 @@
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
2
  /** The layouts document: read whole, so it carries no open-ended list. */
5
3
  export declare function generateManifest(config: CompilerConfig): string;
6
4
  /** The catalog document: searched by name, never read whole. */
package/dist/manifest.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ASSETS_FILE } from "./files.js";
1
2
  import { templateKeys } from "./markdown/textTemplate.js";
2
3
  /**
3
4
  * Flatten a compiler parameter to the manifest entries advertised to AI authors.
@@ -18,8 +19,6 @@ function stripSlot(slot) {
18
19
  result.required = true;
19
20
  return result;
20
21
  }
21
- /** Filename of the searchable asset catalog, named by the manifest that points at it. */
22
- export const ASSETS_FILE = "assets.json";
23
22
  /** The layouts document: read whole, so it carries no open-ended list. */
24
23
  export function generateManifest(config) {
25
24
  const layouts = config.layouts.map((layout) => {
@@ -1,4 +1,18 @@
1
1
  import type { CompilerThemeConfig } from "./markdown/types.js";
2
+ /**
3
+ * Pack `paths` (theme-relative, POSIX) into one archive, reading each through
4
+ * `read`. Entries keep their declared paths, so expanding reproduces the layout
5
+ * `theme.json` already refers to.
6
+ */
7
+ export declare function packAssets(paths: string[], read: (rel: string) => Buffer): Promise<Buffer>;
8
+ /**
9
+ * Expand a packaged theme's archive into its directory, so the files the catalog
10
+ * names are on disk before anything fills with them. Idempotent per file; loose
11
+ * files win, so a stale archive never overwrites a theme's real assets; the
12
+ * archive itself stays put. A theme with no archive -- every theme under
13
+ * development -- returns immediately.
14
+ */
15
+ export declare function expandAssets(rootDir: string): Promise<void>;
2
16
  /**
3
17
  * Rewrite the `name:` value in a SKILL.md's leading YAML frontmatter so the
4
18
  * packaged skill is named after the consuming theme, not the source template.
package/dist/skillZip.js CHANGED
@@ -1,7 +1,59 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
3
  import JSZip from "jszip";
4
4
  import { TEMPLATE_DIR } from "./engine/index.js";
5
+ import { ASSETS_ARCHIVE, PACKAGE_JSON } from "./files.js";
6
+ /** Entries are stored, not deflated: assets are already-compressed images. */
7
+ const NO_COMPRESSION = { type: "nodebuffer", compression: "STORE" };
8
+ /**
9
+ * Pack `paths` (theme-relative, POSIX) into one archive, reading each through
10
+ * `read`. Entries keep their declared paths, so expanding reproduces the layout
11
+ * `theme.json` already refers to.
12
+ */
13
+ export async function packAssets(paths, read) {
14
+ const archive = new JSZip();
15
+ for (const rel of paths)
16
+ archive.file(rel, read(rel));
17
+ return archive.generateAsync(NO_COMPRESSION);
18
+ }
19
+ /**
20
+ * Expand a packaged theme's archive into its directory, so the files the catalog
21
+ * names are on disk before anything fills with them. Idempotent per file; loose
22
+ * files win, so a stale archive never overwrites a theme's real assets; the
23
+ * archive itself stays put. A theme with no archive -- every theme under
24
+ * development -- returns immediately.
25
+ */
26
+ export async function expandAssets(rootDir) {
27
+ // An empty rootDir is a supported value elsewhere ("resolve nothing"), and it
28
+ // would expand into the process working directory. Refuse rather than guess.
29
+ if (!rootDir)
30
+ return;
31
+ const archivePath = join(rootDir, ASSETS_ARCHIVE);
32
+ if (!existsSync(archivePath))
33
+ return;
34
+ const archive = await JSZip.loadAsync(readFileSync(archivePath));
35
+ for (const [rel, entry] of Object.entries(archive.files)) {
36
+ if (entry.dir)
37
+ continue;
38
+ // JSZip collapses `..` and a leading `/` on load, but a backslash survives
39
+ // verbatim and traverses on Windows. We wrote this archive, so an entry that
40
+ // is not a plain theme-relative path means it was tampered with.
41
+ if (rel.includes("\\")) {
42
+ throw new Error(`${ASSETS_ARCHIVE} entry "${rel}" is not a theme-relative path`);
43
+ }
44
+ const abs = join(rootDir, ...rel.split("/"));
45
+ if (existsSync(abs))
46
+ continue;
47
+ mkdirSync(dirname(abs), { recursive: true });
48
+ // Write-then-rename. A plain write is not atomic: a build killed partway
49
+ // through 2,000 icons leaves a truncated file that `existsSync` then skips
50
+ // forever. Rename is atomic within a filesystem, so a reader sees a whole
51
+ // file or none, which also makes two concurrent builds in one directory safe.
52
+ const partial = `${abs}.${process.pid}.tmp`;
53
+ writeFileSync(partial, await entry.async("nodebuffer"));
54
+ renameSync(partial, abs);
55
+ }
56
+ }
5
57
  const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
6
58
  const NAME_LINE = /^name:[ \t]*.*$/m;
7
59
  /**
@@ -18,8 +70,6 @@ export function renameSkill(md, name) {
18
70
  throw new Error('SKILL.md frontmatter has no "name:" line');
19
71
  return md.replace(block[0], block[0].replace(NAME_LINE, `name: ${name}`));
20
72
  }
21
- /** The manifest a packaged skill installs from, authored rather than copied. */
22
- const PACKAGE_JSON = "package.json";
23
73
  /**
24
74
  * Files a packaged skill needs beyond the theme's own declarations. Only the
25
75
  * lockfile: `package.json` is authored by `skillPackageJson` rather than taken
@@ -55,17 +105,29 @@ export function skillPackageJson(theme, engine) {
55
105
  return `${JSON.stringify(skill, null, 2)}\n`;
56
106
  }
57
107
  /**
58
- * Every path a packaged theme needs, relative to `rootDir` and POSIX-separated.
108
+ * Every path a packaged theme needs, relative to `rootDir` and POSIX-separated,
109
+ * split by how it ships.
59
110
  *
60
111
  * Derived from the theme config rather than filtered out of a directory walk:
61
112
  * the config already declares its template and its whole asset catalog, so an
62
113
  * allowlist stays correct no matter what else sits in the working directory --
63
114
  * built decks, PDFs, slide PNGs, scratch files. Font paths are deliberately
64
- * absent: they resolve from node_modules, which `npm install` restores.
115
+ * absent when they name a package -- those resolve from node_modules, which
116
+ * `npm install` restores -- but a `./`- or `/`-prefixed font path is a file the
117
+ * theme owns, and mermaid reads it during COMPILE, before any archive is
118
+ * expanded. Those ship plain.
119
+ *
120
+ * `archived` is the asset catalog, which collapses to one archive because hosts
121
+ * cap how many FILES a skill may contain. `plain` is everything read before or
122
+ * without an expansion, including the catalog itself.
65
123
  */
66
124
  function skillPaths(config, generated) {
67
- const assets = Object.values(config.assets).flatMap((category) => Object.values(category).map((entry) => entry.path));
68
- return [...SUPPORT_FILES, ...generated, `${TEMPLATE_DIR}/${config.template}`, ...assets];
125
+ const archived = Object.values(config.assets).flatMap((category) => Object.values(category).map((entry) => entry.path));
126
+ const localFonts = (config.fonts ?? []).map((f) => f.path).filter((p) => p.startsWith(".") || p.startsWith("/"));
127
+ return {
128
+ plain: [...SUPPORT_FILES, ...generated, `${TEMPLATE_DIR}/${config.template}`, ...localFonts],
129
+ archived,
130
+ };
69
131
  }
70
132
  /**
71
133
  * Zip a theme into an uploadable Agent Skill archive whose entries all live
@@ -81,19 +143,35 @@ export async function zipDir(rootDir, folderName, config, generated, packageJson
81
143
  if (!folder)
82
144
  throw new Error(`Failed to create zip folder: ${folderName}`);
83
145
  folder.file(PACKAGE_JSON, packageJson);
146
+ const { plain, archived } = skillPaths(config, generated);
84
147
  const optional = new Set(SUPPORT_FILES);
85
148
  let count = 1;
86
- for (const rel of skillPaths(config, generated)) {
149
+ // `optional` is a plain-bucket concept (a lockfile a theme may not have). An
150
+ // asset the catalog declares is never optional, so the archived loop calls
151
+ // `required` and a missing one throws rather than silently vanishing.
152
+ const required = (rel) => {
87
153
  const abs = join(rootDir, ...rel.split("/"));
88
- if (!existsSync(abs)) {
89
- if (optional.has(rel))
90
- continue;
91
- throw new Error(`Theme declares "${rel}", but no such file exists`);
92
- }
93
- folder.file(rel, readFileSync(abs));
154
+ if (existsSync(abs))
155
+ return readFileSync(abs);
156
+ throw new Error(`Theme declares "${rel}", but no such file exists`);
157
+ };
158
+ const read = (rel) => {
159
+ if (optional.has(rel) && !existsSync(join(rootDir, ...rel.split("/"))))
160
+ return null;
161
+ return required(rel);
162
+ };
163
+ for (const rel of plain) {
164
+ const content = read(rel);
165
+ if (content === null)
166
+ continue;
167
+ folder.file(rel, content);
168
+ count++;
169
+ }
170
+ if (archived.length > 0) {
171
+ folder.file(ASSETS_ARCHIVE, await packAssets(archived, required));
94
172
  count++;
95
173
  }
96
174
  if (count === 0)
97
175
  throw new Error(`No files to zip in directory: ${rootDir}`);
98
- return zip.generateAsync({ type: "nodebuffer" });
176
+ return zip.generateAsync({ type: "nodebuffer", compression: "STORE" });
99
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "Create editable, on-brand PowerPoint slides from markdown.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",