@cuppacue/cli 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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/ai/__tests__/generate.test.d.ts +2 -0
  4. package/dist/ai/__tests__/generate.test.d.ts.map +1 -0
  5. package/dist/ai/__tests__/generate.test.js +129 -0
  6. package/dist/ai/__tests__/generate.test.js.map +1 -0
  7. package/dist/ai/__tests__/images.test.d.ts +2 -0
  8. package/dist/ai/__tests__/images.test.d.ts.map +1 -0
  9. package/dist/ai/__tests__/images.test.js +186 -0
  10. package/dist/ai/__tests__/images.test.js.map +1 -0
  11. package/dist/ai/__tests__/prompt.test.d.ts +2 -0
  12. package/dist/ai/__tests__/prompt.test.d.ts.map +1 -0
  13. package/dist/ai/__tests__/prompt.test.js +98 -0
  14. package/dist/ai/__tests__/prompt.test.js.map +1 -0
  15. package/dist/ai/__tests__/refine.test.d.ts +2 -0
  16. package/dist/ai/__tests__/refine.test.d.ts.map +1 -0
  17. package/dist/ai/__tests__/refine.test.js +87 -0
  18. package/dist/ai/__tests__/refine.test.js.map +1 -0
  19. package/dist/ai/client.d.ts +4 -0
  20. package/dist/ai/client.d.ts.map +1 -0
  21. package/dist/ai/client.js +36 -0
  22. package/dist/ai/client.js.map +1 -0
  23. package/dist/ai/example.d.ts +6 -0
  24. package/dist/ai/example.d.ts.map +1 -0
  25. package/dist/ai/example.js +284 -0
  26. package/dist/ai/example.js.map +1 -0
  27. package/dist/ai/generate.d.ts +15 -0
  28. package/dist/ai/generate.d.ts.map +1 -0
  29. package/dist/ai/generate.js +120 -0
  30. package/dist/ai/generate.js.map +1 -0
  31. package/dist/ai/images.d.ts +8 -0
  32. package/dist/ai/images.d.ts.map +1 -0
  33. package/dist/ai/images.js +71 -0
  34. package/dist/ai/images.js.map +1 -0
  35. package/dist/ai/index.d.ts +7 -0
  36. package/dist/ai/index.d.ts.map +1 -0
  37. package/dist/ai/index.js +7 -0
  38. package/dist/ai/index.js.map +1 -0
  39. package/dist/ai/prompt.d.ts +10 -0
  40. package/dist/ai/prompt.d.ts.map +1 -0
  41. package/dist/ai/prompt.js +119 -0
  42. package/dist/ai/prompt.js.map +1 -0
  43. package/dist/ai/refine.d.ts +6 -0
  44. package/dist/ai/refine.d.ts.map +1 -0
  45. package/dist/ai/refine.js +22 -0
  46. package/dist/ai/refine.js.map +1 -0
  47. package/dist/ai/schema.d.ts +5 -0
  48. package/dist/ai/schema.d.ts.map +1 -0
  49. package/dist/ai/schema.js +292 -0
  50. package/dist/ai/schema.js.map +1 -0
  51. package/dist/commands/__tests__/backstage.test.d.ts +2 -0
  52. package/dist/commands/__tests__/backstage.test.d.ts.map +1 -0
  53. package/dist/commands/__tests__/backstage.test.js +45 -0
  54. package/dist/commands/__tests__/backstage.test.js.map +1 -0
  55. package/dist/commands/__tests__/export.test.d.ts +2 -0
  56. package/dist/commands/__tests__/export.test.d.ts.map +1 -0
  57. package/dist/commands/__tests__/export.test.js +50 -0
  58. package/dist/commands/__tests__/export.test.js.map +1 -0
  59. package/dist/commands/ai.d.ts +2 -0
  60. package/dist/commands/ai.d.ts.map +1 -0
  61. package/dist/commands/ai.js +113 -0
  62. package/dist/commands/ai.js.map +1 -0
  63. package/dist/commands/build.d.ts +2 -0
  64. package/dist/commands/build.d.ts.map +1 -0
  65. package/dist/commands/build.js +121 -0
  66. package/dist/commands/build.js.map +1 -0
  67. package/dist/commands/export-pdf.d.ts +9 -0
  68. package/dist/commands/export-pdf.d.ts.map +1 -0
  69. package/dist/commands/export-pdf.js +109 -0
  70. package/dist/commands/export-pdf.js.map +1 -0
  71. package/dist/commands/export.d.ts +2 -0
  72. package/dist/commands/export.d.ts.map +1 -0
  73. package/dist/commands/export.js +147 -0
  74. package/dist/commands/export.js.map +1 -0
  75. package/dist/commands/init.d.ts +2 -0
  76. package/dist/commands/init.d.ts.map +1 -0
  77. package/dist/commands/init.js +54 -0
  78. package/dist/commands/init.js.map +1 -0
  79. package/dist/commands/preview.d.ts +2 -0
  80. package/dist/commands/preview.d.ts.map +1 -0
  81. package/dist/commands/preview.js +78 -0
  82. package/dist/commands/preview.js.map +1 -0
  83. package/dist/commands/serve.d.ts +7 -0
  84. package/dist/commands/serve.d.ts.map +1 -0
  85. package/dist/commands/serve.js +773 -0
  86. package/dist/commands/serve.js.map +1 -0
  87. package/dist/commands/validate.d.ts +2 -0
  88. package/dist/commands/validate.d.ts.map +1 -0
  89. package/dist/commands/validate.js +39 -0
  90. package/dist/commands/validate.js.map +1 -0
  91. package/dist/github/__tests__/fetcher.test.d.ts +2 -0
  92. package/dist/github/__tests__/fetcher.test.d.ts.map +1 -0
  93. package/dist/github/__tests__/fetcher.test.js +102 -0
  94. package/dist/github/__tests__/fetcher.test.js.map +1 -0
  95. package/dist/github/__tests__/rewrite-images.test.d.ts +2 -0
  96. package/dist/github/__tests__/rewrite-images.test.d.ts.map +1 -0
  97. package/dist/github/__tests__/rewrite-images.test.js +79 -0
  98. package/dist/github/__tests__/rewrite-images.test.js.map +1 -0
  99. package/dist/github/__tests__/url-parser.test.d.ts +2 -0
  100. package/dist/github/__tests__/url-parser.test.d.ts.map +1 -0
  101. package/dist/github/__tests__/url-parser.test.js +96 -0
  102. package/dist/github/__tests__/url-parser.test.js.map +1 -0
  103. package/dist/github/fetcher.d.ts +10 -0
  104. package/dist/github/fetcher.d.ts.map +1 -0
  105. package/dist/github/fetcher.js +53 -0
  106. package/dist/github/fetcher.js.map +1 -0
  107. package/dist/github/index.d.ts +5 -0
  108. package/dist/github/index.d.ts.map +1 -0
  109. package/dist/github/index.js +4 -0
  110. package/dist/github/index.js.map +1 -0
  111. package/dist/github/rewrite-images.d.ts +7 -0
  112. package/dist/github/rewrite-images.d.ts.map +1 -0
  113. package/dist/github/rewrite-images.js +26 -0
  114. package/dist/github/rewrite-images.js.map +1 -0
  115. package/dist/github/url-parser.d.ts +12 -0
  116. package/dist/github/url-parser.d.ts.map +1 -0
  117. package/dist/github/url-parser.js +60 -0
  118. package/dist/github/url-parser.js.map +1 -0
  119. package/dist/index.d.ts +3 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +95 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/markdown/__tests__/auto-slide.test.d.ts +2 -0
  124. package/dist/markdown/__tests__/auto-slide.test.d.ts.map +1 -0
  125. package/dist/markdown/__tests__/auto-slide.test.js +325 -0
  126. package/dist/markdown/__tests__/auto-slide.test.js.map +1 -0
  127. package/dist/markdown/__tests__/compiler.test.d.ts +2 -0
  128. package/dist/markdown/__tests__/compiler.test.d.ts.map +1 -0
  129. package/dist/markdown/__tests__/compiler.test.js +142 -0
  130. package/dist/markdown/__tests__/compiler.test.js.map +1 -0
  131. package/dist/markdown/__tests__/custom-parsers.test.d.ts +2 -0
  132. package/dist/markdown/__tests__/custom-parsers.test.d.ts.map +1 -0
  133. package/dist/markdown/__tests__/custom-parsers.test.js +182 -0
  134. package/dist/markdown/__tests__/custom-parsers.test.js.map +1 -0
  135. package/dist/markdown/__tests__/frontmatter.test.d.ts +2 -0
  136. package/dist/markdown/__tests__/frontmatter.test.d.ts.map +1 -0
  137. package/dist/markdown/__tests__/frontmatter.test.js +162 -0
  138. package/dist/markdown/__tests__/frontmatter.test.js.map +1 -0
  139. package/dist/markdown/__tests__/layout.test.d.ts +2 -0
  140. package/dist/markdown/__tests__/layout.test.d.ts.map +1 -0
  141. package/dist/markdown/__tests__/layout.test.js +85 -0
  142. package/dist/markdown/__tests__/layout.test.js.map +1 -0
  143. package/dist/markdown/__tests__/parser.test.d.ts +2 -0
  144. package/dist/markdown/__tests__/parser.test.d.ts.map +1 -0
  145. package/dist/markdown/__tests__/parser.test.js +646 -0
  146. package/dist/markdown/__tests__/parser.test.js.map +1 -0
  147. package/dist/markdown/__tests__/timesheet-gen.test.d.ts +2 -0
  148. package/dist/markdown/__tests__/timesheet-gen.test.d.ts.map +1 -0
  149. package/dist/markdown/__tests__/timesheet-gen.test.js +90 -0
  150. package/dist/markdown/__tests__/timesheet-gen.test.js.map +1 -0
  151. package/dist/markdown/auto-slide.d.ts +16 -0
  152. package/dist/markdown/auto-slide.d.ts.map +1 -0
  153. package/dist/markdown/auto-slide.js +144 -0
  154. package/dist/markdown/auto-slide.js.map +1 -0
  155. package/dist/markdown/compiler.d.ts +12 -0
  156. package/dist/markdown/compiler.d.ts.map +1 -0
  157. package/dist/markdown/compiler.js +47 -0
  158. package/dist/markdown/compiler.js.map +1 -0
  159. package/dist/markdown/custom-parsers.d.ts +69 -0
  160. package/dist/markdown/custom-parsers.d.ts.map +1 -0
  161. package/dist/markdown/custom-parsers.js +227 -0
  162. package/dist/markdown/custom-parsers.js.map +1 -0
  163. package/dist/markdown/frontmatter.d.ts +33 -0
  164. package/dist/markdown/frontmatter.d.ts.map +1 -0
  165. package/dist/markdown/frontmatter.js +83 -0
  166. package/dist/markdown/frontmatter.js.map +1 -0
  167. package/dist/markdown/id.d.ts +4 -0
  168. package/dist/markdown/id.d.ts.map +1 -0
  169. package/dist/markdown/id.js +15 -0
  170. package/dist/markdown/id.js.map +1 -0
  171. package/dist/markdown/index.d.ts +8 -0
  172. package/dist/markdown/index.d.ts.map +1 -0
  173. package/dist/markdown/index.js +6 -0
  174. package/dist/markdown/index.js.map +1 -0
  175. package/dist/markdown/parser.d.ts +35 -0
  176. package/dist/markdown/parser.d.ts.map +1 -0
  177. package/dist/markdown/parser.js +605 -0
  178. package/dist/markdown/parser.js.map +1 -0
  179. package/dist/markdown/timesheet-gen.d.ts +6 -0
  180. package/dist/markdown/timesheet-gen.d.ts.map +1 -0
  181. package/dist/markdown/timesheet-gen.js +223 -0
  182. package/dist/markdown/timesheet-gen.js.map +1 -0
  183. package/dist/player/CuePlayer.d.ts +58 -0
  184. package/dist/player/CuePlayer.d.ts.map +1 -0
  185. package/dist/player/NavBar.d.ts +31 -0
  186. package/dist/player/NavBar.d.ts.map +1 -0
  187. package/dist/player/PresenterView.d.ts +25 -0
  188. package/dist/player/PresenterView.d.ts.map +1 -0
  189. package/dist/player/SlideOverview.d.ts +16 -0
  190. package/dist/player/SlideOverview.d.ts.map +1 -0
  191. package/dist/player/__mocks__/chart.d.ts +29 -0
  192. package/dist/player/__mocks__/chart.d.ts.map +1 -0
  193. package/dist/player/__mocks__/mermaid.d.ts +8 -0
  194. package/dist/player/__mocks__/mermaid.d.ts.map +1 -0
  195. package/dist/player/__tests__/animator.test.d.ts +2 -0
  196. package/dist/player/__tests__/animator.test.d.ts.map +1 -0
  197. package/dist/player/__tests__/layout.test.d.ts +2 -0
  198. package/dist/player/__tests__/layout.test.d.ts.map +1 -0
  199. package/dist/player/__tests__/overview.test.d.ts +2 -0
  200. package/dist/player/__tests__/overview.test.d.ts.map +1 -0
  201. package/dist/player/__tests__/timeline.test.d.ts +2 -0
  202. package/dist/player/__tests__/timeline.test.d.ts.map +1 -0
  203. package/dist/player/animator.d.ts +4 -0
  204. package/dist/player/animator.d.ts.map +1 -0
  205. package/dist/player/cuppacue-player.css +1 -0
  206. package/dist/player/cuppacue-player.js +3247 -0
  207. package/dist/player/index.d.ts +14 -0
  208. package/dist/player/index.d.ts.map +1 -0
  209. package/dist/player/loader.d.ts +3 -0
  210. package/dist/player/loader.d.ts.map +1 -0
  211. package/dist/player/main.d.ts +2 -0
  212. package/dist/player/main.d.ts.map +1 -0
  213. package/dist/player/navigator.d.ts +27 -0
  214. package/dist/player/navigator.d.ts.map +1 -0
  215. package/dist/player/renderer.d.ts +9 -0
  216. package/dist/player/renderer.d.ts.map +1 -0
  217. package/dist/player/renderers/cards.d.ts +3 -0
  218. package/dist/player/renderers/cards.d.ts.map +1 -0
  219. package/dist/player/renderers/chart.d.ts +3 -0
  220. package/dist/player/renderers/chart.d.ts.map +1 -0
  221. package/dist/player/renderers/icons.d.ts +6 -0
  222. package/dist/player/renderers/icons.d.ts.map +1 -0
  223. package/dist/player/renderers/mermaid.d.ts +3 -0
  224. package/dist/player/renderers/mermaid.d.ts.map +1 -0
  225. package/dist/player/renderers/stats.d.ts +3 -0
  226. package/dist/player/renderers/stats.d.ts.map +1 -0
  227. package/dist/player/renderers/terminal.d.ts +3 -0
  228. package/dist/player/renderers/terminal.d.ts.map +1 -0
  229. package/dist/player/themes/dark.d.ts +2 -0
  230. package/dist/player/themes/dark.d.ts.map +1 -0
  231. package/dist/player/themes/light.d.ts +2 -0
  232. package/dist/player/themes/light.d.ts.map +1 -0
  233. package/dist/player/timeline.d.ts +43 -0
  234. package/dist/player/timeline.d.ts.map +1 -0
  235. package/dist/serve.d.ts +2 -0
  236. package/dist/serve.d.ts.map +1 -0
  237. package/dist/serve.js +171 -0
  238. package/dist/serve.js.map +1 -0
  239. package/dist/templates/discover.d.ts +20 -0
  240. package/dist/templates/discover.d.ts.map +1 -0
  241. package/dist/templates/discover.js +86 -0
  242. package/dist/templates/discover.js.map +1 -0
  243. package/dist/templates/loader.d.ts +17 -0
  244. package/dist/templates/loader.d.ts.map +1 -0
  245. package/dist/templates/loader.js +40 -0
  246. package/dist/templates/loader.js.map +1 -0
  247. package/package.json +62 -0
@@ -0,0 +1,773 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import os from "node:os";
6
+ import { writeCupDirectory, parseCupDirectory, parseCupFile } from "@cuppacue/format";
7
+ import { compileMarkdown } from "../markdown/index.js";
8
+ import { resolveImages } from "../ai/images.js";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const MIME_TYPES = {
11
+ ".html": "text/html",
12
+ ".css": "text/css",
13
+ ".js": "application/javascript",
14
+ ".json": "application/json",
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".svg": "image/svg+xml",
18
+ ".woff2": "font/woff2",
19
+ ".woff": "font/woff",
20
+ ".gif": "image/gif",
21
+ ".webp": "image/webp",
22
+ ".ico": "image/x-icon",
23
+ ".map": "application/json",
24
+ };
25
+ export function applyNavAction(state, action, index) {
26
+ const max = Math.max(state.totalScenes - 1, 0);
27
+ switch (action) {
28
+ case "next":
29
+ return Math.min(state.currentIndex + 1, max);
30
+ case "prev":
31
+ return Math.max(state.currentIndex - 1, 0);
32
+ case "goto":
33
+ if (typeof index === "number") {
34
+ return Math.max(0, Math.min(index, max));
35
+ }
36
+ return state.currentIndex;
37
+ default:
38
+ return state.currentIndex;
39
+ }
40
+ }
41
+ // ─── HTML templates ───
42
+ function getServeTemplate(title, backstageEnabled) {
43
+ const navSync = backstageEnabled
44
+ ? `
45
+ // ── Backstage sync ──
46
+ let _navFromSSE = false;
47
+
48
+ es.addEventListener('nav', (e) => {
49
+ const { index } = JSON.parse(e.data);
50
+ if (_currentIndex !== index) {
51
+ _navFromSSE = true;
52
+ player.goToScene(index);
53
+ }
54
+ });
55
+
56
+ let _currentIndex = 0;
57
+ player.on('scenechange', (data) => {
58
+ _currentIndex = data.index;
59
+ if (!_navFromSSE) {
60
+ fetch('/__nav', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ action: 'goto', index: data.index }),
64
+ }).catch(() => {});
65
+ }
66
+ _navFromSSE = false;
67
+ });`
68
+ : "";
69
+ return `<!DOCTYPE html>
70
+ <html lang="en">
71
+ <head>
72
+ <meta charset="UTF-8" />
73
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
74
+ <title>${title} — CuppaCue</title>
75
+ <script type="importmap">
76
+ {
77
+ "imports": {
78
+ "shiki": "https://esm.sh/shiki@3",
79
+ "mermaid": "https://esm.sh/mermaid@11",
80
+ "chart.js": "https://esm.sh/chart.js@4"
81
+ }
82
+ }
83
+ </script>
84
+ <link rel="stylesheet" href="/player/cuppacue-player.css" />
85
+ <style>
86
+ * { margin: 0; padding: 0; box-sizing: border-box; }
87
+ html, body, #cue-root { width: 100%; height: 100%; overflow: hidden; background: #0f172a; }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <div id="cue-root"></div>
92
+ <script type="module">
93
+ import { CuePlayer } from '/player/cuppacue-player.js';
94
+
95
+ const root = document.getElementById('cue-root');
96
+ const player = new CuePlayer(root);
97
+
98
+ player.on('load', () => console.log('[CuppaCue] Loaded'));
99
+ player.on('end', () => console.log('[CuppaCue] Ended'));
100
+
101
+ await player.load('/data/');
102
+ player.goToScene(0);
103
+
104
+ // SSE hot reload
105
+ const es = new EventSource('/__sse');
106
+ es.addEventListener('reload', () => {
107
+ console.log('[CuppaCue] Reloading...');
108
+ window.location.reload();
109
+ });
110
+ es.onerror = () => {
111
+ console.log('[CuppaCue] SSE disconnected, will retry...');
112
+ };
113
+ ${navSync}
114
+
115
+ window.cuePlayer = player;
116
+ </script>
117
+ </body>
118
+ </html>`;
119
+ }
120
+ function getBackstageTemplate(title) {
121
+ return `<!DOCTYPE html>
122
+ <html lang="en">
123
+ <head>
124
+ <meta charset="UTF-8" />
125
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
126
+ <title>Backstage — ${title}</title>
127
+ <script type="importmap">
128
+ {
129
+ "imports": {
130
+ "shiki": "https://esm.sh/shiki@3",
131
+ "mermaid": "https://esm.sh/mermaid@11",
132
+ "chart.js": "https://esm.sh/chart.js@4"
133
+ }
134
+ }
135
+ </script>
136
+ <link rel="stylesheet" href="/player/cuppacue-player.css" />
137
+ <style>
138
+ * { margin: 0; padding: 0; box-sizing: border-box; }
139
+ html { height: 100%; }
140
+ body {
141
+ height: 100%;
142
+ background: #0a0e1a;
143
+ color: #e2e8f0;
144
+ font-family: 'Inter', system-ui, sans-serif;
145
+ display: flex;
146
+ flex-direction: column;
147
+ overflow: hidden;
148
+ -webkit-user-select: none;
149
+ user-select: none;
150
+ }
151
+
152
+ /* ── Header ── */
153
+ .bs-header {
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: center;
157
+ padding: 10px 16px;
158
+ border-bottom: 1px solid rgba(255,255,255,0.1);
159
+ flex-shrink: 0;
160
+ }
161
+ .bs-title {
162
+ font-size: 14px;
163
+ font-weight: 600;
164
+ opacity: 0.8;
165
+ white-space: nowrap;
166
+ overflow: hidden;
167
+ text-overflow: ellipsis;
168
+ max-width: 60%;
169
+ }
170
+ .bs-counter {
171
+ font-size: 14px;
172
+ font-variant-numeric: tabular-nums;
173
+ opacity: 0.6;
174
+ }
175
+
176
+ /* ── Content area ── */
177
+ .bs-content {
178
+ flex: 1;
179
+ overflow-y: auto;
180
+ -webkit-overflow-scrolling: touch;
181
+ padding: 12px 16px;
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 12px;
185
+ min-height: 0;
186
+ }
187
+
188
+ /* ── Slide previews ── */
189
+ .bs-label {
190
+ font-size: 11px;
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.1em;
193
+ opacity: 0.4;
194
+ font-weight: 600;
195
+ }
196
+ .bs-preview {
197
+ position: relative;
198
+ border: 1px solid rgba(255,255,255,0.12);
199
+ border-radius: 8px;
200
+ overflow: hidden;
201
+ background: rgba(0,0,0,0.3);
202
+ aspect-ratio: 16 / 9;
203
+ flex-shrink: 0;
204
+ }
205
+ .bs-preview .cue-scene {
206
+ position: absolute;
207
+ inset: 0;
208
+ }
209
+ .bs-preview--current { border-color: rgba(56,189,248,0.4); }
210
+ .bs-preview--next { opacity: 0.6; }
211
+ .bs-next-row {
212
+ display: flex;
213
+ align-items: flex-start;
214
+ gap: 12px;
215
+ }
216
+ .bs-next-row .bs-preview {
217
+ width: 40%;
218
+ flex-shrink: 0;
219
+ }
220
+
221
+ /* ── Notes ── */
222
+ .bs-notes {
223
+ flex: 1;
224
+ min-height: 80px;
225
+ }
226
+ .bs-notes-text {
227
+ font-size: 16px;
228
+ line-height: 1.65;
229
+ white-space: pre-wrap;
230
+ opacity: 0.85;
231
+ margin-top: 6px;
232
+ }
233
+ .bs-notes-empty { opacity: 0.3; font-style: italic; }
234
+
235
+ /* ── Footer / controls ── */
236
+ .bs-footer {
237
+ flex-shrink: 0;
238
+ border-top: 1px solid rgba(255,255,255,0.1);
239
+ padding: 10px 16px;
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 10px;
243
+ }
244
+ .bs-nav-row {
245
+ display: flex;
246
+ gap: 10px;
247
+ }
248
+ .bs-nav-btn {
249
+ flex: 1;
250
+ padding: 14px 0;
251
+ border: 1px solid rgba(255,255,255,0.2);
252
+ border-radius: 10px;
253
+ background: rgba(255,255,255,0.06);
254
+ color: #e2e8f0;
255
+ font-size: 16px;
256
+ font-weight: 600;
257
+ cursor: pointer;
258
+ transition: background 0.15s ease;
259
+ touch-action: manipulation;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ gap: 6px;
264
+ }
265
+ .bs-nav-btn:active { background: rgba(255,255,255,0.18); }
266
+ .bs-nav-btn svg { width: 20px; height: 20px; }
267
+ .bs-info-row {
268
+ display: flex;
269
+ justify-content: space-between;
270
+ align-items: center;
271
+ font-size: 13px;
272
+ opacity: 0.5;
273
+ }
274
+ .bs-timer {
275
+ font-variant-numeric: tabular-nums;
276
+ color: #38bdf8;
277
+ font-weight: 600;
278
+ font-size: 16px;
279
+ opacity: 1;
280
+ }
281
+
282
+ /* ── Landscape ── */
283
+ @media (orientation: landscape) and (max-height: 500px) {
284
+ .bs-content { flex-direction: row; }
285
+ .bs-preview--current { width: 55%; }
286
+ .bs-next-row { flex-direction: column; width: 45%; }
287
+ .bs-next-row .bs-preview { width: 100%; }
288
+ }
289
+ </style>
290
+ </head>
291
+ <body>
292
+ <div class="bs-header">
293
+ <div class="bs-title" id="bs-title"></div>
294
+ <div class="bs-counter" id="bs-counter">– / –</div>
295
+ </div>
296
+
297
+ <div class="bs-content" id="bs-swipe-area">
298
+ <div class="bs-label">Current</div>
299
+ <div class="bs-preview bs-preview--current" id="bs-current"></div>
300
+
301
+ <div class="bs-next-row">
302
+ <div>
303
+ <div class="bs-label">Next</div>
304
+ <div class="bs-preview bs-preview--next" id="bs-next"></div>
305
+ </div>
306
+ <div class="bs-notes">
307
+ <div class="bs-label">Speaker Notes</div>
308
+ <div class="bs-notes-text" id="bs-notes"></div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <div class="bs-footer">
314
+ <div class="bs-nav-row">
315
+ <button class="bs-nav-btn" id="bs-prev">
316
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
317
+ Prev
318
+ </button>
319
+ <button class="bs-nav-btn" id="bs-next-btn">
320
+ Next
321
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
322
+ </button>
323
+ </div>
324
+ <div class="bs-info-row">
325
+ <span class="bs-timer" id="bs-timer">00:00</span>
326
+ <span id="bs-info-counter">– / –</span>
327
+ </div>
328
+ </div>
329
+
330
+ <script type="module">
331
+ import { loadFromUrl, renderScene } from '/player/cuppacue-player.js';
332
+
333
+ // ── Load presentation data ──
334
+ const cupFile = await loadFromUrl('/data/');
335
+ const scenes = cupFile.content.scenes;
336
+ const theme = cupFile.theme;
337
+
338
+ document.getElementById('bs-title').textContent = cupFile.manifest.title || 'Untitled';
339
+
340
+ let currentIndex = 0;
341
+
342
+ // ── Rendering helpers ──
343
+ async function renderPreview(containerId, scene) {
344
+ const container = document.getElementById(containerId);
345
+ if (!container || !scene) return;
346
+ try {
347
+ const rendered = await renderScene(scene, theme);
348
+ for (const [, el] of rendered.elementMap) el.style.opacity = '1';
349
+ container.innerHTML = '';
350
+ container.appendChild(rendered.container);
351
+ } catch {
352
+ container.innerHTML = '<div style="padding:1rem;opacity:0.3;">Unable to render</div>';
353
+ }
354
+ }
355
+
356
+ function updateDisplay(index) {
357
+ currentIndex = index;
358
+ const scene = scenes[index];
359
+ const nextScene = index + 1 < scenes.length ? scenes[index + 1] : null;
360
+
361
+ renderPreview('bs-current', scene);
362
+
363
+ if (nextScene) {
364
+ renderPreview('bs-next', nextScene);
365
+ } else {
366
+ document.getElementById('bs-next').innerHTML =
367
+ '<div style="display:flex;align-items:center;justify-content:center;height:100%;opacity:0.3;font-style:italic;">End</div>';
368
+ }
369
+
370
+ const notesEl = document.getElementById('bs-notes');
371
+ if (scene.notes) {
372
+ notesEl.textContent = scene.notes;
373
+ notesEl.classList.remove('bs-notes-empty');
374
+ } else {
375
+ notesEl.textContent = '(No notes for this scene)';
376
+ notesEl.classList.add('bs-notes-empty');
377
+ }
378
+
379
+ const counterText = (index + 1) + ' / ' + scenes.length;
380
+ document.getElementById('bs-counter').textContent = counterText;
381
+ document.getElementById('bs-info-counter').textContent = counterText;
382
+ }
383
+
384
+ // ── Navigation ──
385
+ async function nav(action, index) {
386
+ try {
387
+ const body = index !== undefined
388
+ ? JSON.stringify({ action, index })
389
+ : JSON.stringify({ action });
390
+ const res = await fetch('/__nav', {
391
+ method: 'POST',
392
+ headers: { 'Content-Type': 'application/json' },
393
+ body,
394
+ });
395
+ const data = await res.json();
396
+ updateDisplay(data.index);
397
+ } catch { /* ignore */ }
398
+ }
399
+
400
+ document.getElementById('bs-prev').addEventListener('click', () => nav('prev'));
401
+ document.getElementById('bs-next-btn').addEventListener('click', () => nav('next'));
402
+
403
+ // ── Keyboard ──
404
+ document.addEventListener('keydown', (e) => {
405
+ if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); nav('next'); }
406
+ if (e.key === 'ArrowLeft') { e.preventDefault(); nav('prev'); }
407
+ });
408
+
409
+ // ── Swipe gestures ──
410
+ let touchStartX = 0;
411
+ const swipeArea = document.getElementById('bs-swipe-area');
412
+ swipeArea.addEventListener('touchstart', (e) => {
413
+ touchStartX = e.touches[0].clientX;
414
+ }, { passive: true });
415
+ swipeArea.addEventListener('touchend', (e) => {
416
+ const dx = e.changedTouches[0].clientX - touchStartX;
417
+ if (Math.abs(dx) > 50) {
418
+ if (dx < 0) nav('next');
419
+ else nav('prev');
420
+ }
421
+ }, { passive: true });
422
+
423
+ // ── SSE for sync with main display ──
424
+ const es = new EventSource('/__sse');
425
+ es.addEventListener('nav', (e) => {
426
+ const { index } = JSON.parse(e.data);
427
+ if (currentIndex !== index) updateDisplay(index);
428
+ });
429
+ es.addEventListener('reload', () => window.location.reload());
430
+
431
+ // ── Timer ──
432
+ const startTime = Date.now();
433
+ setInterval(() => {
434
+ const elapsed = Date.now() - startTime;
435
+ const mins = Math.floor(elapsed / 60000);
436
+ const secs = Math.floor((elapsed % 60000) / 1000);
437
+ document.getElementById('bs-timer').textContent =
438
+ String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
439
+ }, 1000);
440
+
441
+ // ── Initial state ──
442
+ try {
443
+ const res = await fetch('/__nav', {
444
+ method: 'POST',
445
+ headers: { 'Content-Type': 'application/json' },
446
+ body: JSON.stringify({ action: 'state' }),
447
+ });
448
+ const data = await res.json();
449
+ updateDisplay(data.index);
450
+ } catch {
451
+ updateDisplay(0);
452
+ }
453
+ </script>
454
+ </body>
455
+ </html>`;
456
+ }
457
+ function getFlag(args, flag) {
458
+ const idx = args.indexOf(flag);
459
+ if (idx >= 0 && args[idx + 1])
460
+ return args[idx + 1];
461
+ return undefined;
462
+ }
463
+ function hasFlag(args, flag) {
464
+ return args.includes(flag);
465
+ }
466
+ function rebuildMarkdown(inputPath, outDir) {
467
+ try {
468
+ const markdown = fs.readFileSync(inputPath, "utf-8");
469
+ const { cupFile, warnings } = compileMarkdown(markdown);
470
+ for (const w of warnings)
471
+ console.warn(` [warn] ${w}`);
472
+ writeCupDirectory(cupFile, outDir);
473
+ console.log(` [rebuild] ${path.basename(inputPath)} → ${outDir}`);
474
+ }
475
+ catch (err) {
476
+ console.error(` [error] ${err}`);
477
+ }
478
+ }
479
+ function getLanAddress() {
480
+ const interfaces = os.networkInterfaces();
481
+ for (const name of Object.keys(interfaces)) {
482
+ for (const iface of interfaces[name] ?? []) {
483
+ if (iface.family === "IPv4" && !iface.internal) {
484
+ return iface.address;
485
+ }
486
+ }
487
+ }
488
+ return null;
489
+ }
490
+ function readBody(req) {
491
+ return new Promise((resolve) => {
492
+ const chunks = [];
493
+ req.on("data", (chunk) => chunks.push(chunk));
494
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
495
+ });
496
+ }
497
+ export async function serve(args) {
498
+ const input = args[0];
499
+ if (!input) {
500
+ console.error("Usage: cuppacue serve <dir|file.md|file.cup> [--port <port>] [--backstage]");
501
+ process.exit(1);
502
+ }
503
+ let port = 3000;
504
+ const portStr = getFlag(args, "--port");
505
+ if (portStr)
506
+ port = parseInt(portStr, 10);
507
+ const backstageEnabled = hasFlag(args, "--backstage");
508
+ const inputPath = path.resolve(input);
509
+ const isCupFile = inputPath.endsWith(".cup");
510
+ const isMarkdown = inputPath.endsWith(".md");
511
+ let presentationDir;
512
+ let markdownSource = null;
513
+ let cleanupTempDir = null;
514
+ if (isCupFile) {
515
+ if (!fs.existsSync(inputPath)) {
516
+ console.error(`File not found: ${inputPath}`);
517
+ process.exit(1);
518
+ }
519
+ const tempDir = path.join(os.tmpdir(), `cuppacue-serve-${path.basename(inputPath, ".cup")}-${Date.now()}`);
520
+ const { cupFile, resources } = await parseCupFile(inputPath);
521
+ await writeCupDirectory(cupFile, tempDir);
522
+ // Write resources to temp dir
523
+ if (resources.size > 0) {
524
+ const resDir = path.join(tempDir, "resources");
525
+ fs.mkdirSync(resDir, { recursive: true });
526
+ for (const [name, data] of resources) {
527
+ const dest = path.join(resDir, name);
528
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
529
+ fs.writeFileSync(dest, data);
530
+ }
531
+ }
532
+ presentationDir = tempDir;
533
+ cleanupTempDir = () => {
534
+ try {
535
+ fs.rmSync(tempDir, { recursive: true, force: true });
536
+ }
537
+ catch { /* ignore */ }
538
+ };
539
+ console.log(` Extracted: ${inputPath} → ${tempDir}`);
540
+ }
541
+ else if (isMarkdown) {
542
+ if (!fs.existsSync(inputPath)) {
543
+ console.error(`File not found: ${inputPath}`);
544
+ process.exit(1);
545
+ }
546
+ markdownSource = inputPath;
547
+ presentationDir = inputPath.replace(/\.md$/, "");
548
+ // Initial build
549
+ rebuildMarkdown(inputPath, presentationDir);
550
+ }
551
+ else {
552
+ presentationDir = inputPath;
553
+ if (!fs.existsSync(presentationDir)) {
554
+ console.error(`Directory not found: ${presentationDir}`);
555
+ process.exit(1);
556
+ }
557
+ }
558
+ // Resolve placeholder images if an API key is available
559
+ try {
560
+ const { cupFile } = await parseCupDirectory(presentationDir);
561
+ const hasPlaceholders = cupFile.content.scenes.some((s) => s.elements.some((el) => el.type === "image" &&
562
+ el.placeholder));
563
+ if (hasPlaceholders) {
564
+ await resolveImages(cupFile);
565
+ await writeCupDirectory(cupFile, presentationDir);
566
+ }
567
+ }
568
+ catch {
569
+ // Non-fatal — continue serving with placeholders
570
+ }
571
+ // Find the built player bundle (bundled into dist/player/ during build)
572
+ const playerDistDir = path.resolve(__dirname, "../player");
573
+ // Read manifest for title and scene count
574
+ let title = "CuppaCue Presentation";
575
+ const navState = { currentIndex: 0, totalScenes: 0 };
576
+ try {
577
+ const manifest = JSON.parse(fs.readFileSync(path.join(presentationDir, "manifest.json"), "utf-8"));
578
+ title = manifest.title || title;
579
+ navState.totalScenes = manifest.sceneCount || 0;
580
+ }
581
+ catch {
582
+ // ignore
583
+ }
584
+ const sseClients = [];
585
+ function notifyClients(filename) {
586
+ console.log(` [reload] ${filename} changed`);
587
+ for (const client of sseClients) {
588
+ try {
589
+ client.res.write(`event: reload\ndata: ${filename}\n\n`);
590
+ }
591
+ catch {
592
+ // client disconnected
593
+ }
594
+ }
595
+ }
596
+ function broadcastNav() {
597
+ const data = JSON.stringify({ index: navState.currentIndex });
598
+ for (const client of sseClients) {
599
+ try {
600
+ client.res.write(`event: nav\ndata: ${data}\n\n`);
601
+ }
602
+ catch {
603
+ // client disconnected
604
+ }
605
+ }
606
+ }
607
+ // Watch output directory for changes (triggers SSE reload)
608
+ const outputWatcher = fs.watch(presentationDir, { recursive: true }, (_event, filename) => {
609
+ if (!filename)
610
+ return;
611
+ // Re-read manifest on rebuild to update scene count
612
+ if (filename === "manifest.json") {
613
+ try {
614
+ const manifest = JSON.parse(fs.readFileSync(path.join(presentationDir, "manifest.json"), "utf-8"));
615
+ title = manifest.title || title;
616
+ navState.totalScenes = manifest.sceneCount || navState.totalScenes;
617
+ }
618
+ catch { /* ignore */ }
619
+ }
620
+ notifyClients(filename);
621
+ });
622
+ // If markdown source, also watch it for rebuilds
623
+ let sourceWatcher;
624
+ if (markdownSource) {
625
+ sourceWatcher = fs.watch(markdownSource, (_event) => {
626
+ rebuildMarkdown(markdownSource, presentationDir);
627
+ });
628
+ }
629
+ const server = http.createServer(async (req, res) => {
630
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
631
+ const pathname = url.pathname;
632
+ // ── Navigation command endpoint ──
633
+ if (req.method === "POST" && pathname === "/__nav") {
634
+ const body = await readBody(req);
635
+ let parsed;
636
+ try {
637
+ parsed = JSON.parse(body);
638
+ }
639
+ catch {
640
+ res.writeHead(400, { "Content-Type": "application/json" });
641
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
642
+ return;
643
+ }
644
+ if (parsed.action !== "state") {
645
+ navState.currentIndex = applyNavAction(navState, parsed.action, parsed.index);
646
+ }
647
+ // Broadcast state to all connected clients
648
+ if (parsed.action !== "state") {
649
+ broadcastNav();
650
+ }
651
+ res.writeHead(200, {
652
+ "Content-Type": "application/json",
653
+ "Access-Control-Allow-Origin": "*",
654
+ });
655
+ res.end(JSON.stringify({ index: navState.currentIndex }));
656
+ return;
657
+ }
658
+ // CORS preflight for /__nav
659
+ if (req.method === "OPTIONS" && pathname === "/__nav") {
660
+ res.writeHead(204, {
661
+ "Access-Control-Allow-Origin": "*",
662
+ "Access-Control-Allow-Methods": "POST",
663
+ "Access-Control-Allow-Headers": "Content-Type",
664
+ });
665
+ res.end();
666
+ return;
667
+ }
668
+ // SSE endpoint for hot reload + nav sync
669
+ if (pathname === "/__sse") {
670
+ res.writeHead(200, {
671
+ "Content-Type": "text/event-stream",
672
+ "Cache-Control": "no-cache",
673
+ Connection: "keep-alive",
674
+ "Access-Control-Allow-Origin": "*",
675
+ });
676
+ res.write("data: connected\n\n");
677
+ const client = { res };
678
+ sseClients.push(client);
679
+ req.on("close", () => {
680
+ const idx = sseClients.indexOf(client);
681
+ if (idx >= 0)
682
+ sseClients.splice(idx, 1);
683
+ });
684
+ return;
685
+ }
686
+ // ── Backstage route ──
687
+ if (backstageEnabled && (pathname === "/backstage" || pathname === "/backstage/")) {
688
+ // Re-read title
689
+ try {
690
+ const manifest = JSON.parse(fs.readFileSync(path.join(presentationDir, "manifest.json"), "utf-8"));
691
+ title = manifest.title || title;
692
+ }
693
+ catch { /* ignore */ }
694
+ res.writeHead(200, { "Content-Type": "text/html" });
695
+ res.end(getBackstageTemplate(title));
696
+ return;
697
+ }
698
+ // Serve index
699
+ if (pathname === "/" || pathname === "/index.html") {
700
+ // Re-read title on each request in case it changed
701
+ try {
702
+ const manifest = JSON.parse(fs.readFileSync(path.join(presentationDir, "manifest.json"), "utf-8"));
703
+ title = manifest.title || title;
704
+ }
705
+ catch {
706
+ // ignore
707
+ }
708
+ res.writeHead(200, { "Content-Type": "text/html" });
709
+ res.end(getServeTemplate(title, backstageEnabled));
710
+ return;
711
+ }
712
+ // Serve presentation data
713
+ if (pathname.startsWith("/data/")) {
714
+ const filePath = path.join(presentationDir, pathname.slice("/data/".length));
715
+ return serveFile(filePath, res);
716
+ }
717
+ // Serve player bundle from dist
718
+ if (pathname.startsWith("/player/")) {
719
+ const filePath = path.join(playerDistDir, pathname.slice("/player/".length));
720
+ return serveFile(filePath, res);
721
+ }
722
+ // 404
723
+ res.writeHead(404, { "Content-Type": "text/plain" });
724
+ res.end("Not found");
725
+ });
726
+ server.listen(port, () => {
727
+ console.log(`\n CuppaCue Dev Server`);
728
+ console.log(` ───────────────────`);
729
+ if (markdownSource) {
730
+ console.log(` Source: ${markdownSource}`);
731
+ }
732
+ console.log(` Presentation: ${presentationDir}`);
733
+ console.log(` Slides: http://localhost:${port}/`);
734
+ console.log(` Hot reload: enabled`);
735
+ if (backstageEnabled) {
736
+ const lanIp = getLanAddress();
737
+ const lanUrl = lanIp ? `http://${lanIp}:${port}` : null;
738
+ console.log(`\n Backstage: http://localhost:${port}/backstage`);
739
+ if (lanUrl) {
740
+ console.log(` LAN: ${lanUrl}/backstage`);
741
+ console.log(`\n Open the LAN URL on your phone to control the presentation remotely.`);
742
+ }
743
+ }
744
+ console.log(`\n Press Ctrl+C to stop.\n`);
745
+ });
746
+ process.on("SIGINT", () => {
747
+ outputWatcher.close();
748
+ sourceWatcher?.close();
749
+ cleanupTempDir?.();
750
+ server.close();
751
+ process.exit(0);
752
+ });
753
+ }
754
+ function serveFile(filePath, res) {
755
+ try {
756
+ const stat = fs.statSync(filePath);
757
+ if (!stat.isFile())
758
+ throw new Error("Not a file");
759
+ const ext = path.extname(filePath);
760
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
761
+ res.writeHead(200, {
762
+ "Content-Type": contentType,
763
+ "Content-Length": stat.size,
764
+ "Cache-Control": "no-cache",
765
+ });
766
+ fs.createReadStream(filePath).pipe(res);
767
+ }
768
+ catch {
769
+ res.writeHead(404, { "Content-Type": "text/plain" });
770
+ res.end("Not found");
771
+ }
772
+ }
773
+ //# sourceMappingURL=serve.js.map