@tycoworks/tycoslide 0.13.0 → 0.13.2
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 +4 -1
- package/SKILL.md +3 -3
- package/dist/cli.js +4 -9
- package/dist/files.d.ts +20 -0
- package/dist/files.js +23 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -1
- package/dist/manifest.d.ts +0 -2
- package/dist/manifest.js +1 -2
- package/dist/skillZip.d.ts +16 -2
- package/dist/skillZip.js +98 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,10 +68,11 @@ my-theme/
|
|
|
68
68
|
assets/icons/
|
|
69
69
|
theme.json
|
|
70
70
|
package.json
|
|
71
|
-
|
|
71
|
+
SKILL.md # generated by `tycoslide package`
|
|
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.zip # 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.zip`. `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/SKILL.md
CHANGED
|
@@ -37,7 +37,7 @@ This skill builds on-brand decks from a markdown deck file. The theme provides s
|
|
|
37
37
|
|
|
38
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`.
|
|
39
39
|
|
|
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
|
|
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 it and use the `$category.name` you find -- but **search for what the icon depicts, not what you mean by it**: a catalog is indexed by picture, so "freshness" finds nothing while `grep -i "clock" assets.json` and `grep -i "bolt"` find the icon you wanted.
|
|
41
41
|
|
|
42
42
|
A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
|
|
43
43
|
- **parameters** -- one value on a frontmatter line. Fill by putting a value under the parameter's key in the slide frontmatter.
|
|
@@ -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**.
|
|
92
|
+
Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters**. A value containing a colon-then-space must be quoted -- `title: "The change: compute on the difference"` -- or YAML reads it as a second key and the build fails.
|
|
93
93
|
|
|
94
94
|
```yaml
|
|
95
95
|
---
|
|
@@ -141,7 +141,7 @@ Keep each slot's content to what its region comfortably holds. When content over
|
|
|
141
141
|
- **Don't repeat the same layout** -- vary layouts for visual rhythm
|
|
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
|
-
- **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.
|
|
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. A `leaves X% of the frame empty` warning is the opposite: the image is a different shape from the slot. Neither fails the build -- judge both from the rendered slide.
|
|
145
145
|
- **Don't invent layout or asset names** -- only use layouts from `manifest.json` and assets from `assets.json`
|
|
146
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
|
|
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 {
|
|
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
|
-
// 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
|
-
const SYNTAX_FILE = "syntax.md";
|
|
15
10
|
const sdkDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const skillMdPath = resolve(sdkDir, "..",
|
|
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",
|
|
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"));
|
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ASSETS_ARCHIVE = "assets.zip";
|
|
17
|
+
export declare const SKILL_FILE = "SKILL.md";
|
|
18
|
+
export declare const SYNTAX_FILE = "syntax.md";
|
|
19
|
+
/** The manifest a packaged skill installs from, authored rather than copied. */
|
|
20
|
+
export declare const PACKAGE_JSON = "package.json";
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
export const ASSETS_ARCHIVE = "assets.zip";
|
|
17
|
+
// SKILL.md, uppercase: the Agent Skills format requires that exact filename at
|
|
18
|
+
// the root of a skill folder, and a case-sensitive filesystem will not find any
|
|
19
|
+
// other.
|
|
20
|
+
export const SKILL_FILE = "SKILL.md";
|
|
21
|
+
export const SYNTAX_FILE = "syntax.md";
|
|
22
|
+
/** The manifest a packaged skill installs from, authored rather than copied. */
|
|
23
|
+
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 {
|
|
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 {
|
|
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";
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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) => {
|
package/dist/skillZip.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { CompilerThemeConfig } from "./markdown/types.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Pack `paths` (theme-relative, POSIX) into one archive, reading each through
|
|
4
|
+
* `read`. Entries keep their declared paths, so expanding reproduces the layout
|
|
5
|
+
* `theme.json` already refers to.
|
|
6
|
+
*/
|
|
7
|
+
export declare function packAssets(paths: string[], read: (rel: string) => Buffer): Promise<Buffer>;
|
|
8
|
+
/**
|
|
9
|
+
* Expand a packaged theme's archive into its directory, so the files the catalog
|
|
10
|
+
* names are on disk before anything fills with them. Idempotent per file; loose
|
|
11
|
+
* files win, so a stale archive never overwrites a theme's real assets; the
|
|
12
|
+
* archive itself stays put. A theme with no archive -- every theme under
|
|
13
|
+
* development -- returns immediately.
|
|
14
|
+
*/
|
|
15
|
+
export declare function expandAssets(rootDir: string): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Rewrite the `name:` value in a SKILL.md's leading YAML frontmatter so the
|
|
4
18
|
* packaged skill is named after the consuming theme, not the source template.
|
|
5
19
|
* Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
|
|
6
20
|
* Throws if there is no frontmatter or no `name:` line — the caller names the file.
|
|
@@ -28,7 +42,7 @@ export declare function skillPackageJson(theme: Record<string, unknown>, engine:
|
|
|
28
42
|
* Zip a theme into an uploadable Agent Skill archive whose entries all live
|
|
29
43
|
* under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
|
|
30
44
|
* custom-skill format. `generated` names the files the caller just wrote (the
|
|
31
|
-
* config, manifest,
|
|
45
|
+
* config, manifest, SKILL.md, syntax.md); `packageJson` is the authored manifest
|
|
32
46
|
* from `skillPackageJson`. Optional support files are skipped when absent;
|
|
33
47
|
* anything the config declares but that is missing is an error.
|
|
34
48
|
*/
|
package/dist/skillZip.js
CHANGED
|
@@ -1,11 +1,63 @@
|
|
|
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
|
/**
|
|
8
|
-
* Rewrite the `name:` value in a
|
|
60
|
+
* Rewrite the `name:` value in a SKILL.md's leading YAML frontmatter so the
|
|
9
61
|
* packaged skill is named after the consuming theme, not the source template.
|
|
10
62
|
* Only the leading `---`…`---` block is touched; the body is left byte-for-byte.
|
|
11
63
|
* Throws if there is no frontmatter or no `name:` line — the caller names the file.
|
|
@@ -13,13 +65,11 @@ const NAME_LINE = /^name:[ \t]*.*$/m;
|
|
|
13
65
|
export function renameSkill(md, name) {
|
|
14
66
|
const block = md.match(FRONTMATTER);
|
|
15
67
|
if (!block)
|
|
16
|
-
throw new Error("
|
|
68
|
+
throw new Error("SKILL.md has no YAML frontmatter block");
|
|
17
69
|
if (!NAME_LINE.test(block[1]))
|
|
18
|
-
throw new Error('
|
|
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,23 +105,35 @@ 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
|
|
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
|
|
68
|
-
|
|
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
|
|
72
134
|
* under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
|
|
73
135
|
* custom-skill format. `generated` names the files the caller just wrote (the
|
|
74
|
-
* config, manifest,
|
|
136
|
+
* config, manifest, SKILL.md, syntax.md); `packageJson` is the authored manifest
|
|
75
137
|
* from `skillPackageJson`. Optional support files are skipped when absent;
|
|
76
138
|
* anything the config declares but that is missing is an error.
|
|
77
139
|
*/
|
|
@@ -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
|
-
|
|
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 (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
}
|