lazyslides 0.1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -0
  3. package/_includes/slides/_nested-list.njk +26 -0
  4. package/_includes/slides/agenda.njk +17 -0
  5. package/_includes/slides/center.njk +16 -0
  6. package/_includes/slides/code.njk +15 -0
  7. package/_includes/slides/columns.njk +35 -0
  8. package/_includes/slides/comparison.njk +29 -0
  9. package/_includes/slides/content.njk +22 -0
  10. package/_includes/slides/footer.njk +58 -0
  11. package/_includes/slides/funnel.njk +30 -0
  12. package/_includes/slides/hero.njk +27 -0
  13. package/_includes/slides/image-overlay.njk +21 -0
  14. package/_includes/slides/metrics.njk +27 -0
  15. package/_includes/slides/quote.njk +17 -0
  16. package/_includes/slides/section.njk +6 -0
  17. package/_includes/slides/split-wide.njk +30 -0
  18. package/_includes/slides/split.njk +30 -0
  19. package/_includes/slides/table.njk +31 -0
  20. package/_includes/slides/timeline.njk +30 -0
  21. package/_includes/slides/title.njk +17 -0
  22. package/_layouts/default.njk +20 -0
  23. package/_layouts/presentation.njk +240 -0
  24. package/assets/css/themes/default.css +62 -0
  25. package/assets/css/vendor/glightbox.min.css +1 -0
  26. package/assets/js/vendor/glightbox.min.js +1 -0
  27. package/assets/reveal.js/LICENSE +19 -0
  28. package/assets/reveal.js/dist/reset.css +30 -0
  29. package/assets/reveal.js/dist/reveal.css +8 -0
  30. package/assets/reveal.js/dist/reveal.esm.js +9 -0
  31. package/assets/reveal.js/dist/reveal.esm.js.map +1 -0
  32. package/assets/reveal.js/dist/reveal.js +9 -0
  33. package/assets/reveal.js/dist/reveal.js.map +1 -0
  34. package/assets/reveal.js/dist/theme/beige.css +366 -0
  35. package/assets/reveal.js/dist/theme/black-contrast.css +362 -0
  36. package/assets/reveal.js/dist/theme/black.css +359 -0
  37. package/assets/reveal.js/dist/theme/blood.css +392 -0
  38. package/assets/reveal.js/dist/theme/dracula.css +385 -0
  39. package/assets/reveal.js/dist/theme/fonts/league-gothic/LICENSE +2 -0
  40. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.css +10 -0
  41. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.eot +0 -0
  42. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.ttf +0 -0
  43. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.woff +0 -0
  44. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/LICENSE +45 -0
  45. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.eot +0 -0
  46. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.ttf +0 -0
  47. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.woff +0 -0
  48. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.eot +0 -0
  49. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.ttf +0 -0
  50. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.woff +0 -0
  51. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.eot +0 -0
  52. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.ttf +0 -0
  53. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.woff +0 -0
  54. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.eot +0 -0
  55. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.ttf +0 -0
  56. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.woff +0 -0
  57. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro.css +39 -0
  58. package/assets/reveal.js/dist/theme/league.css +368 -0
  59. package/assets/reveal.js/dist/theme/moon.css +362 -0
  60. package/assets/reveal.js/dist/theme/night.css +360 -0
  61. package/assets/reveal.js/dist/theme/serif.css +363 -0
  62. package/assets/reveal.js/dist/theme/simple.css +362 -0
  63. package/assets/reveal.js/dist/theme/sky.css +370 -0
  64. package/assets/reveal.js/dist/theme/solarized.css +363 -0
  65. package/assets/reveal.js/dist/theme/white-contrast.css +362 -0
  66. package/assets/reveal.js/dist/theme/white.css +359 -0
  67. package/assets/reveal.js/dist/theme/white_contrast_compact_verbatim_headers.css +360 -0
  68. package/assets/reveal.js/plugin/highlight/highlight.esm.js +5 -0
  69. package/assets/reveal.js/plugin/highlight/highlight.js +5 -0
  70. package/assets/reveal.js/plugin/highlight/monokai.css +71 -0
  71. package/assets/reveal.js/plugin/highlight/plugin.js +439 -0
  72. package/assets/reveal.js/plugin/highlight/zenburn.css +80 -0
  73. package/assets/reveal.js/plugin/markdown/markdown.esm.js +7 -0
  74. package/assets/reveal.js/plugin/markdown/markdown.js +7 -0
  75. package/assets/reveal.js/plugin/markdown/plugin.js +491 -0
  76. package/assets/reveal.js/plugin/notes/notes.esm.js +1 -0
  77. package/assets/reveal.js/plugin/notes/notes.js +1 -0
  78. package/assets/reveal.js/plugin/notes/plugin.js +267 -0
  79. package/assets/reveal.js/plugin/notes/speaker-view.html +898 -0
  80. package/cli.js +80 -0
  81. package/data/site.json +3 -0
  82. package/index.js +153 -0
  83. package/lib/export-pdf.js +137 -0
  84. package/lib/init.js +154 -0
  85. package/lib/renumber.js +181 -0
  86. package/lib/validate.js +196 -0
  87. package/package.json +76 -0
  88. package/scaffold/CLAUDE.md +283 -0
  89. package/scaffold/README.md +29 -0
  90. package/scaffold/_template/index.md +78 -0
  91. package/scaffold/_template/outline.md +32 -0
  92. package/scaffold/claude-commands/add-slide.md +61 -0
  93. package/scaffold/claude-commands/create-outline.md +75 -0
  94. package/scaffold/claude-commands/new-presentation.md +218 -0
  95. package/scaffold/claude-commands/refine-slides.md +62 -0
  96. package/scaffold/claude-commands/research-topic.md +66 -0
  97. package/scaffold/claude-commands/validate.md +47 -0
  98. package/scaffold/claude-settings.json +7 -0
  99. package/scaffold/eleventy.config.js +11 -0
  100. package/scaffold/gitignore +5 -0
  101. package/scaffold/my-first-deck/index.md +26 -0
  102. package/scaffold/nvmrc +1 -0
  103. package/scaffold/package.json.tmpl +25 -0
  104. package/scaffold/presentations.11tydata.js +11 -0
  105. package/scaffold/styles.css +3 -0
  106. package/src/styles.css +2077 -0
package/cli.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+
6
+ const HELP = `
7
+ Usage: lazyslides <command> [options]
8
+
9
+ Commands:
10
+ init [dir] Scaffold a new LazySlides project
11
+ validate Validate presentation YAML files
12
+ renumber [file] Renumber slide comments
13
+ pdf [name] Export presentations to PDF
14
+
15
+ Options:
16
+ --help Show this help message
17
+ --version Show version number
18
+ `;
19
+
20
+ async function main() {
21
+ if (!command || command === "--help" || command === "-h") {
22
+ console.log(HELP.trim());
23
+ process.exit(0);
24
+ }
25
+
26
+ if (command === "--version" || command === "-v") {
27
+ const { createRequire } = await import("node:module");
28
+ const require = createRequire(import.meta.url);
29
+ const pkg = require("./package.json");
30
+ console.log(pkg.version);
31
+ process.exit(0);
32
+ }
33
+
34
+ switch (command) {
35
+ case "init": {
36
+ const dir = args[1];
37
+ if (!dir) {
38
+ console.error("Error: Please specify a project directory.\n");
39
+ console.error(" lazyslides init my-deck");
40
+ process.exit(1);
41
+ }
42
+ const { run } = await import("./lib/init.js");
43
+ await run({ dir });
44
+ break;
45
+ }
46
+
47
+ case "validate": {
48
+ const { run } = await import("./lib/validate.js");
49
+ const result = await run({
50
+ renumber: args.includes("--renumber"),
51
+ });
52
+ if (result.errors.length > 0) process.exit(1);
53
+ break;
54
+ }
55
+
56
+ case "renumber": {
57
+ const { run } = await import("./lib/renumber.js");
58
+ const files = args.slice(1).filter((a) => !a.startsWith("--"));
59
+ await run({ files: files.length > 0 ? files : undefined });
60
+ break;
61
+ }
62
+
63
+ case "pdf": {
64
+ const { run } = await import("./lib/export-pdf.js");
65
+ const name = args[1];
66
+ await run({ name });
67
+ break;
68
+ }
69
+
70
+ default:
71
+ console.error(`Unknown command: ${command}\n`);
72
+ console.log(HELP.trim());
73
+ process.exit(1);
74
+ }
75
+ }
76
+
77
+ main().catch((err) => {
78
+ console.error(err.message);
79
+ process.exit(1);
80
+ });
package/data/site.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "title": "LazySlides"
3
+ }
package/index.js ADDED
@@ -0,0 +1,153 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const pkgRoot = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ /**
8
+ * LazySlides Eleventy plugin.
9
+ *
10
+ * Registers layouts, templates, assets, filters, and a virtual
11
+ * presentations index page so that a user project only needs a
12
+ * six-line eleventy.config.js to get the full engine.
13
+ */
14
+ export default function lazyslides(eleventyConfig, options = {}) {
15
+ // ---------------------------------------------------------------
16
+ // 1. Filters & Nunjucks test (unchanged logic from .eleventy.js)
17
+ // ---------------------------------------------------------------
18
+ eleventyConfig.addFilter("resolveImage", (src) => {
19
+ if (!src || src.includes("://") || src.startsWith("/")) return src;
20
+ return "../" + src;
21
+ });
22
+
23
+ eleventyConfig.addFilter("isMapping", (val) => {
24
+ return val !== null && typeof val === "object" && !Array.isArray(val);
25
+ });
26
+
27
+ // ---------------------------------------------------------------
28
+ // 2. Nunjucks search paths — makes {% include "slides/…" %} and
29
+ // layout: presentation.njk resolve to files inside the package.
30
+ // User-local paths are searched first (Eleventy default) so
31
+ // users can override any template.
32
+ // ---------------------------------------------------------------
33
+ eleventyConfig.amendLibrary("njk", (env) => {
34
+ // Register the "mapping" test
35
+ env.addTest("mapping", (val) => {
36
+ return val !== null && typeof val === "object" && !Array.isArray(val);
37
+ });
38
+
39
+ // Add package paths *after* the user's local paths
40
+ env.loaders[0].searchPaths.push(path.join(pkgRoot, "_includes"));
41
+ env.loaders[0].searchPaths.push(path.join(pkgRoot, "_layouts"));
42
+ });
43
+
44
+ // ---------------------------------------------------------------
45
+ // 3. Passthrough copies — map engine assets to output
46
+ // ---------------------------------------------------------------
47
+ const realPkgRoot = fs.realpathSync(pkgRoot);
48
+
49
+ eleventyConfig.addPassthroughCopy({
50
+ [path.join(realPkgRoot, "assets/reveal.js")]: "assets/reveal.js",
51
+ });
52
+ eleventyConfig.addPassthroughCopy({
53
+ [path.join(realPkgRoot, "assets/css/themes")]: "assets/css/themes",
54
+ });
55
+ eleventyConfig.addPassthroughCopy({
56
+ [path.join(realPkgRoot, "assets/css/vendor")]: "assets/css/vendor",
57
+ });
58
+ eleventyConfig.addPassthroughCopy({
59
+ [path.join(realPkgRoot, "assets/js/vendor")]: "assets/js/vendor",
60
+ });
61
+
62
+ // User's local theme overrides
63
+ eleventyConfig.addPassthroughCopy({
64
+ "themes/": "assets/css/themes/",
65
+ });
66
+
67
+ // Per-presentation images
68
+ eleventyConfig.addPassthroughCopy("presentations/*/images");
69
+
70
+ // ---------------------------------------------------------------
71
+ // 3b. Collection — only index.md files are presentations
72
+ // ---------------------------------------------------------------
73
+ eleventyConfig.addCollection("presentations", (collectionApi) => {
74
+ return collectionApi.getFilteredByGlob("presentations/*/index.md");
75
+ });
76
+
77
+ // ---------------------------------------------------------------
78
+ // 4. Global data — site.title with option override
79
+ // ---------------------------------------------------------------
80
+ const siteData = JSON.parse(
81
+ fs.readFileSync(path.join(realPkgRoot, "data/site.json"), "utf-8")
82
+ );
83
+ eleventyConfig.addGlobalData("site", {
84
+ title: options.siteTitle || siteData.title,
85
+ ...options.siteData,
86
+ });
87
+
88
+ // ---------------------------------------------------------------
89
+ // 5. Virtual layouts — loaded from the package so users don't
90
+ // need _layouts/ locally (but can override by creating one)
91
+ // ---------------------------------------------------------------
92
+ const layoutsDir = path.join(realPkgRoot, "_layouts");
93
+ for (const file of fs.readdirSync(layoutsDir)) {
94
+ if (!file.endsWith(".njk")) continue;
95
+ const content = fs.readFileSync(path.join(layoutsDir, file), "utf-8");
96
+ eleventyConfig.addTemplate(`_layouts/${file}`, content);
97
+ }
98
+
99
+ // ---------------------------------------------------------------
100
+ // 6. Virtual template — presentations index page
101
+ // ---------------------------------------------------------------
102
+ eleventyConfig.addTemplate("presentations-index.njk", `---
103
+ layout: default.njk
104
+ title: Presentations
105
+ permalink: /presentations/
106
+ ---
107
+
108
+ <div style="text-align: center; margin-bottom: 2rem;">
109
+ <h1 style="font-family: 'Source Serif 4', Georgia, serif; font-size: 2rem; font-weight: 700; color: #0f172a; margin-bottom: 0.5rem;">LazySlides</h1>
110
+ <p style="color: #64748b; font-size: 1rem;">AI-native presentations from YAML. Powered by Eleventy + Reveal.js + Tailwind CSS.</p>
111
+ </div>
112
+
113
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; margin-bottom: 3rem;">
114
+ {% for presentation in collections.presentations %}
115
+ <a href="{{ presentation.url | url }}" style="display: block; padding: 1.5rem; border: 1px solid #e2e8f0; border-radius: 8px; text-decoration: none; transition: all 0.2s ease; background: white;">
116
+ <h2 style="font-family: 'Source Serif 4', Georgia, serif; font-size: 1.1rem; font-weight: 600; color: #0f172a; margin: 0 0 0.5rem 0;">{{ presentation.data.title }}</h2>
117
+ {% if presentation.data.description %}
118
+ <p style="font-size: 0.85rem; color: #64748b; margin: 0;">{{ presentation.data.description }}</p>
119
+ {% endif %}
120
+ </a>
121
+ {% endfor %}
122
+ </div>
123
+ `);
124
+
125
+ // ---------------------------------------------------------------
126
+ // 6b. Virtual template — root redirect to presentations index
127
+ // ---------------------------------------------------------------
128
+ eleventyConfig.addTemplate("root-redirect.njk", `---
129
+ permalink: /index.html
130
+ eleventyExcludeFromCollections: true
131
+ ---
132
+ <!DOCTYPE html>
133
+ <html>
134
+ <head><meta http-equiv="refresh" content="0; url={{ '/presentations/example/' | url }}"></head>
135
+ <body></body>
136
+ </html>
137
+ `);
138
+
139
+ // ---------------------------------------------------------------
140
+ // 7. Ignores
141
+ // ---------------------------------------------------------------
142
+ eleventyConfig.ignores.add("node_modules/**");
143
+ eleventyConfig.ignores.add("README.md");
144
+ eleventyConfig.ignores.add("CONTRIBUTING.md");
145
+ eleventyConfig.ignores.add("CLAUDE.md");
146
+ eleventyConfig.ignores.add("LICENSE");
147
+ eleventyConfig.ignores.add(".claude/**");
148
+ eleventyConfig.ignores.add("presentations/_template/**");
149
+ eleventyConfig.ignores.add("pages/**");
150
+ eleventyConfig.ignores.add("scaffold/**");
151
+ eleventyConfig.ignores.add("docs/**");
152
+ eleventyConfig.ignores.add("bin/**");
153
+ }
@@ -0,0 +1,137 @@
1
+ import { execSync, spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import http from "node:http";
5
+
6
+ const PORT = 4100;
7
+ const BASE_URL = `http://127.0.0.1:${PORT}`;
8
+ const OUTPUT_DIR = "_pdfs";
9
+ const TIMEOUT_SECONDS = 30;
10
+
11
+ let serverProcess = null;
12
+
13
+ function cleanup() {
14
+ if (serverProcess) {
15
+ console.log("\n Stopping server...");
16
+ serverProcess.kill();
17
+ serverProcess = null;
18
+ }
19
+ }
20
+
21
+ function waitForServer() {
22
+ return new Promise((resolve, reject) => {
23
+ let elapsed = 0;
24
+ const interval = setInterval(() => {
25
+ const req = http.get(BASE_URL, (res) => {
26
+ res.resume();
27
+ clearInterval(interval);
28
+ resolve();
29
+ });
30
+ req.on("error", () => {
31
+ elapsed++;
32
+ if (elapsed >= TIMEOUT_SECONDS) {
33
+ clearInterval(interval);
34
+ reject(new Error(`Server failed to start within ${TIMEOUT_SECONDS}s`));
35
+ }
36
+ });
37
+ req.end();
38
+ }, 1000);
39
+ });
40
+ }
41
+
42
+ function runDecktape(name) {
43
+ const url = `${BASE_URL}/presentations/${name}/?pdf`;
44
+ const output = path.join(OUTPUT_DIR, `${name}.pdf`);
45
+
46
+ console.log(` → Exporting: ${name}`);
47
+ try {
48
+ execSync(
49
+ `npx decktape reveal --size 1920x1080 --pause 1000 --load-pause 2000 "${url}" "${output}"`,
50
+ { stdio: ["ignore", "pipe", "pipe"] }
51
+ );
52
+ console.log(` \u2713 ${output}`);
53
+ return true;
54
+ } catch {
55
+ console.log(` \u2717 Failed: ${name}`);
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function getAvailablePresentations(cwd) {
61
+ const dir = path.join(cwd, "presentations");
62
+ return fs
63
+ .readdirSync(dir, { withFileTypes: true })
64
+ .filter((d) => d.isDirectory() && d.name !== "_template")
65
+ .map((d) => d.name);
66
+ }
67
+
68
+ /**
69
+ * Export presentations to PDF using DeckTape.
70
+ * @param {object} [opts]
71
+ * @param {string} [opts.cwd] - working directory (defaults to process.cwd())
72
+ * @param {string} [opts.name] - specific presentation name to export
73
+ */
74
+ export async function run(opts = {}) {
75
+ const cwd = opts.cwd || process.cwd();
76
+ const requestedName = opts.name;
77
+ const available = getAvailablePresentations(cwd);
78
+
79
+ if (requestedName) {
80
+ if (!available.includes(requestedName)) {
81
+ console.error(`Presentation not found: ${requestedName}`);
82
+ console.error(" Available presentations:");
83
+ for (const name of available) {
84
+ console.error(` - ${name}`);
85
+ }
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ const presentations = requestedName ? [requestedName] : available;
91
+
92
+ // Build the site
93
+ console.log("Building site...");
94
+ execSync("pnpm run build", { stdio: "inherit", cwd });
95
+
96
+ // Start server
97
+ console.log(`\nStarting server on port ${PORT}...`);
98
+ serverProcess = spawn("npx", ["eleventy", "--serve", "--port", String(PORT)], {
99
+ stdio: "ignore",
100
+ cwd,
101
+ });
102
+
103
+ process.on("exit", cleanup);
104
+ process.on("SIGINT", () => { cleanup(); process.exit(1); });
105
+ process.on("SIGTERM", () => { cleanup(); process.exit(1); });
106
+
107
+ // Wait for server
108
+ console.log("Waiting for server...");
109
+ await waitForServer();
110
+ console.log("Server ready");
111
+
112
+ // Create output directory
113
+ fs.mkdirSync(path.join(cwd, OUTPUT_DIR), { recursive: true });
114
+
115
+ // Export presentations
116
+ let success = 0;
117
+ let failure = 0;
118
+ console.log(`\nExporting ${presentations.length} presentation(s) to PDF...\n`);
119
+
120
+ for (const name of presentations) {
121
+ if (runDecktape(name)) {
122
+ success++;
123
+ } else {
124
+ failure++;
125
+ }
126
+ console.log();
127
+ }
128
+
129
+ cleanup();
130
+
131
+ // Summary
132
+ console.log("\u2501".repeat(30));
133
+ console.log(`Exported: ${success} Failed: ${failure}`);
134
+ if (failure > 0) {
135
+ process.exit(1);
136
+ }
137
+ }
package/lib/init.js ADDED
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
6
+ const scaffoldDir = path.join(pkgRoot, "scaffold");
7
+
8
+ /**
9
+ * Copy a directory recursively.
10
+ */
11
+ function copyDir(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true });
13
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
14
+ const srcPath = path.join(src, entry.name);
15
+ const destPath = path.join(dest, entry.name);
16
+ if (entry.isDirectory()) {
17
+ copyDir(srcPath, destPath);
18
+ } else {
19
+ fs.copyFileSync(srcPath, destPath);
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Scaffold a new LazySlides project.
26
+ * @param {object} opts
27
+ * @param {string} opts.dir - target directory path
28
+ */
29
+ export async function run(opts) {
30
+ const targetDir = path.resolve(opts.dir);
31
+ const projectName = path.basename(targetDir);
32
+
33
+ // Check if directory already has files
34
+ if (fs.existsSync(targetDir)) {
35
+ const existing = fs.readdirSync(targetDir);
36
+ if (existing.length > 0) {
37
+ throw new Error(`Directory "${targetDir}" is not empty.`);
38
+ }
39
+ }
40
+
41
+ fs.mkdirSync(targetDir, { recursive: true });
42
+
43
+ console.log(`\nCreating a new LazySlides project in ${targetDir}\n`);
44
+
45
+ // 1. Copy scaffold/eleventy.config.js
46
+ fs.copyFileSync(
47
+ path.join(scaffoldDir, "eleventy.config.js"),
48
+ path.join(targetDir, "eleventy.config.js")
49
+ );
50
+ console.log(" Created eleventy.config.js");
51
+
52
+ // 2. Create src/styles.css from scaffold
53
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
54
+ fs.copyFileSync(
55
+ path.join(scaffoldDir, "styles.css"),
56
+ path.join(targetDir, "src", "styles.css")
57
+ );
58
+ console.log(" Created src/styles.css");
59
+
60
+ // 3. Create presentations/ with template and starter deck
61
+ const presentationsDir = path.join(targetDir, "presentations");
62
+ fs.mkdirSync(presentationsDir, { recursive: true });
63
+
64
+ // presentations.11tydata.js
65
+ fs.copyFileSync(
66
+ path.join(scaffoldDir, "presentations.11tydata.js"),
67
+ path.join(presentationsDir, "presentations.11tydata.js")
68
+ );
69
+ console.log(" Created presentations/presentations.11tydata.js");
70
+
71
+ // _template/
72
+ copyDir(
73
+ path.join(scaffoldDir, "_template"),
74
+ path.join(presentationsDir, "_template")
75
+ );
76
+ console.log(" Created presentations/_template/");
77
+
78
+ // my-first-deck/
79
+ copyDir(
80
+ path.join(scaffoldDir, "my-first-deck"),
81
+ path.join(presentationsDir, "my-first-deck")
82
+ );
83
+ console.log(" Created presentations/my-first-deck/");
84
+
85
+ // 4. Create themes/ directory (empty, for user overrides)
86
+ fs.mkdirSync(path.join(targetDir, "themes"), { recursive: true });
87
+ // Add a .gitkeep so git tracks the empty directory
88
+ fs.writeFileSync(path.join(targetDir, "themes", ".gitkeep"), "");
89
+ console.log(" Created themes/");
90
+
91
+ // 5. package.json from template
92
+ const pkgTemplate = fs.readFileSync(
93
+ path.join(scaffoldDir, "package.json.tmpl"),
94
+ "utf-8"
95
+ );
96
+ const pkgContent = pkgTemplate.replace(/\{\{name\}\}/g, projectName);
97
+ fs.writeFileSync(path.join(targetDir, "package.json"), pkgContent);
98
+ console.log(" Created package.json");
99
+
100
+ // 6. Rename-on-copy files (.gitignore, .nvmrc)
101
+ fs.copyFileSync(
102
+ path.join(scaffoldDir, "gitignore"),
103
+ path.join(targetDir, ".gitignore")
104
+ );
105
+ console.log(" Created .gitignore");
106
+
107
+ fs.copyFileSync(
108
+ path.join(scaffoldDir, "nvmrc"),
109
+ path.join(targetDir, ".nvmrc")
110
+ );
111
+ console.log(" Created .nvmrc");
112
+
113
+ // 7. README.md
114
+ const readmeTemplate = fs.readFileSync(
115
+ path.join(scaffoldDir, "README.md"),
116
+ "utf-8"
117
+ );
118
+ const readmeContent = readmeTemplate.replace(/\{\{name\}\}/g, projectName);
119
+ fs.writeFileSync(path.join(targetDir, "README.md"), readmeContent);
120
+ console.log(" Created README.md");
121
+
122
+ // 8. CLAUDE.md (Claude Code integration)
123
+ fs.copyFileSync(
124
+ path.join(scaffoldDir, "CLAUDE.md"),
125
+ path.join(targetDir, "CLAUDE.md")
126
+ );
127
+ console.log(" Created CLAUDE.md");
128
+
129
+ // 9. .claude/ directory (commands + settings)
130
+ const claudeDir = path.join(targetDir, ".claude");
131
+ const commandsDir = path.join(claudeDir, "commands");
132
+ fs.mkdirSync(commandsDir, { recursive: true });
133
+ const scaffoldCommandsDir = path.join(scaffoldDir, "claude-commands");
134
+ for (const file of fs.readdirSync(scaffoldCommandsDir)) {
135
+ fs.copyFileSync(
136
+ path.join(scaffoldCommandsDir, file),
137
+ path.join(commandsDir, file)
138
+ );
139
+ }
140
+ fs.copyFileSync(
141
+ path.join(scaffoldDir, "claude-settings.json"),
142
+ path.join(claudeDir, "settings.json")
143
+ );
144
+ console.log(" Created .claude/ (Claude Code integration)");
145
+
146
+ // Done!
147
+ console.log(`
148
+ Done! To get started:
149
+
150
+ cd ${projectName}
151
+ pnpm install
152
+ pnpm run dev
153
+ `);
154
+ }