@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 +3 -0
- package/dist/cli.js +4 -9
- package/dist/files.d.ts +22 -0
- package/dist/files.js +25 -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 +14 -0
- package/dist/skillZip.js +94 -16
- package/package.json +1 -1
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 {
|
|
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, "..",
|
|
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,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 {
|
|
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,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
|
|
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
|
|
@@ -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
|
}
|