@vanillaskyai/video 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 (162) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +201 -0
  3. package/PUBLIC-API.md +276 -0
  4. package/README.md +152 -0
  5. package/SECURITY.md +22 -0
  6. package/SUPPORT.md +28 -0
  7. package/bin/vanillasky.js +3 -0
  8. package/dist/bg-confetti-WNQXS7ZS.js +89 -0
  9. package/dist/bg-emoji-ETI47GLS.js +150 -0
  10. package/dist/bg-media-SVUZAMGX.js +99 -0
  11. package/dist/brand-message-F3RD4P3P.js +253 -0
  12. package/dist/builtin-server-F7YXRVCH.js +8 -0
  13. package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
  14. package/dist/chart-bar-CNSC7BNK.js +304 -0
  15. package/dist/chart-counter-MCRHKN77.js +299 -0
  16. package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
  17. package/dist/check-runtime.d.ts +15 -0
  18. package/dist/check-runtime.js +102 -0
  19. package/dist/chunk-2E6T633S.js +27 -0
  20. package/dist/chunk-352ZZCET.js +83 -0
  21. package/dist/chunk-73NTSFFI.js +81 -0
  22. package/dist/chunk-A2X45FET.js +46 -0
  23. package/dist/chunk-ABPKM4GK.js +187 -0
  24. package/dist/chunk-ABZACD4G.js +216 -0
  25. package/dist/chunk-BCRUUJ6A.js +756 -0
  26. package/dist/chunk-CHLGW22F.js +3551 -0
  27. package/dist/chunk-CJ7F2CE6.js +104 -0
  28. package/dist/chunk-CR7FE7BW.js +109 -0
  29. package/dist/chunk-E7CL7UPB.js +109 -0
  30. package/dist/chunk-EE6PK6XC.js +18 -0
  31. package/dist/chunk-ERY4S6QV.js +385 -0
  32. package/dist/chunk-EVG2ZYIY.js +125 -0
  33. package/dist/chunk-FNAEQ7QO.js +155 -0
  34. package/dist/chunk-FQ47WXAJ.js +38 -0
  35. package/dist/chunk-FSG2PYWG.js +336 -0
  36. package/dist/chunk-GHJEAP4O.js +504 -0
  37. package/dist/chunk-GRMUV7QY.js +262 -0
  38. package/dist/chunk-IFOW65Z6.js +205 -0
  39. package/dist/chunk-IXCUY3B3.js +786 -0
  40. package/dist/chunk-L5BTTCZV.js +97 -0
  41. package/dist/chunk-LMG3GHLL.js +107 -0
  42. package/dist/chunk-OOPIETLW.js +101 -0
  43. package/dist/chunk-P22HHFDK.js +43 -0
  44. package/dist/chunk-QAAO7OAY.js +19 -0
  45. package/dist/chunk-S7HFLYNN.js +35 -0
  46. package/dist/chunk-SPVTJH3F.js +24 -0
  47. package/dist/chunk-W2XJKG6C.js +3177 -0
  48. package/dist/chunk-XB7LH4J2.js +301 -0
  49. package/dist/chunk-XGRA2MUV.js +242 -0
  50. package/dist/chunk-XTOXFMLM.js +105 -0
  51. package/dist/chunk-YC5MFXA5.js +132 -0
  52. package/dist/cli.d.ts +1 -0
  53. package/dist/cli.js +1681 -0
  54. package/dist/compose-video-CN3VSWST.js +13 -0
  55. package/dist/cta-logo-SVA5GP2A.js +209 -0
  56. package/dist/cta-media-E7VLLK42.js +215 -0
  57. package/dist/events-_wfjbmEp.d.ts +81 -0
  58. package/dist/incoming-call-62MFZL3Z.js +320 -0
  59. package/dist/index.d.ts +15 -0
  60. package/dist/index.js +14 -0
  61. package/dist/infographic-before-after-AXCDAWND.js +460 -0
  62. package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
  63. package/dist/infographic-problem-solution-7NURJANA.js +340 -0
  64. package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
  65. package/dist/infographic-steps-VSZM6F5M.js +400 -0
  66. package/dist/kit-BMNcHqa9.d.ts +79 -0
  67. package/dist/prompt-input-LOUCRXCY.js +236 -0
  68. package/dist/react.d.ts +81 -0
  69. package/dist/react.js +835 -0
  70. package/dist/reaction-QG4CYZLQ.js +119 -0
  71. package/dist/server.d.ts +141 -0
  72. package/dist/server.js +498 -0
  73. package/dist/showcase-code-RUTFZR3H.js +351 -0
  74. package/dist/showcase-phone-6CP6XEU5.js +272 -0
  75. package/dist/showcase-terminal-MGAEQWPR.js +306 -0
  76. package/dist/showcase-web-Y4W2HVRJ.js +390 -0
  77. package/dist/social-conversation-75SD2637.js +695 -0
  78. package/dist/social-milestone-XI2QE5BO.js +292 -0
  79. package/dist/social-notification-2UOHQCUY.js +244 -0
  80. package/dist/social-review-stack-FDYZGZTJ.js +239 -0
  81. package/dist/social-testimonial-OQ7N52GJ.js +284 -0
  82. package/dist/social-tweet-5OLCNPPC.js +295 -0
  83. package/dist/state-DZcKuS32.d.ts +3 -0
  84. package/dist/system-prompt-A6UXNTPK.js +8 -0
  85. package/dist/template-catalog.d.ts +24 -0
  86. package/dist/template-catalog.js +6 -0
  87. package/dist/templates.d.ts +25 -0
  88. package/dist/templates.js +19 -0
  89. package/dist/test.d.ts +149 -0
  90. package/dist/test.js +272 -0
  91. package/dist/text-stream-6XHC7J47.js +10 -0
  92. package/dist/types-B-PZR6G_.d.ts +208 -0
  93. package/dist/validate-T7GBU2YF.js +8 -0
  94. package/docs/agent-integration.md +39 -0
  95. package/docs/architecture.md +84 -0
  96. package/docs/branding-and-personalization.md +87 -0
  97. package/docs/concepts.md +106 -0
  98. package/docs/custom-templates.md +329 -0
  99. package/docs/customization.md +141 -0
  100. package/docs/errors.md +70 -0
  101. package/docs/getting-started.md +111 -0
  102. package/docs/input-and-first-scene.md +41 -0
  103. package/docs/integrate-nextjs.md +79 -0
  104. package/docs/media-and-audio.md +69 -0
  105. package/docs/motion-and-effects.md +107 -0
  106. package/docs/persistence.md +95 -0
  107. package/docs/production.md +122 -0
  108. package/docs/prompt-and-input.md +185 -0
  109. package/docs/provider-integration.md +115 -0
  110. package/docs/reference/protocol.md +96 -0
  111. package/docs/reference/provider-adapters.md +158 -0
  112. package/docs/responsive-orientation.md +37 -0
  113. package/docs/security.md +38 -0
  114. package/docs/streaming-protocol.md +15 -0
  115. package/docs/testing.md +137 -0
  116. package/docs/use-cases.md +59 -0
  117. package/examples/custom-template/README.md +19 -0
  118. package/examples/custom-template/minimal-text.tsx +82 -0
  119. package/examples/custom-template/structured-data.tsx +104 -0
  120. package/examples/custom-template/supplied-media.tsx +92 -0
  121. package/examples/nextjs-quickstart/.env.example +2 -0
  122. package/examples/nextjs-quickstart/README.md +22 -0
  123. package/examples/nextjs-quickstart/next-env.d.ts +4 -0
  124. package/examples/nextjs-quickstart/package.json +24 -0
  125. package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
  126. package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
  127. package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
  128. package/examples/nextjs-quickstart/tsconfig.json +26 -0
  129. package/package.json +167 -0
  130. package/registry/items/backgrounds.json +52 -0
  131. package/registry/items/barChart.json +237 -0
  132. package/registry/items/beforeAfter.json +180 -0
  133. package/registry/items/bigNumber.json +225 -0
  134. package/registry/items/brandMessage.json +158 -0
  135. package/registry/items/cardList.json +206 -0
  136. package/registry/items/chatMessenger.json +151 -0
  137. package/registry/items/chatWhatsapp.json +151 -0
  138. package/registry/items/codeEditor.json +184 -0
  139. package/registry/items/confetti.json +107 -0
  140. package/registry/items/ctaLogo.json +160 -0
  141. package/registry/items/ctaMedia.json +193 -0
  142. package/registry/items/emojiBurst.json +101 -0
  143. package/registry/items/incomingCall.json +170 -0
  144. package/registry/items/media.json +179 -0
  145. package/registry/items/milestone.json +205 -0
  146. package/registry/items/motion.json +45 -0
  147. package/registry/items/notification.json +182 -0
  148. package/registry/items/phoneMockup.json +254 -0
  149. package/registry/items/problemSolution.json +186 -0
  150. package/registry/items/progressRing.json +199 -0
  151. package/registry/items/promptInput.json +148 -0
  152. package/registry/items/reaction.json +170 -0
  153. package/registry/items/reviewStack.json +224 -0
  154. package/registry/items/steps.json +207 -0
  155. package/registry/items/template-context.json +31 -0
  156. package/registry/items/terminal.json +212 -0
  157. package/registry/items/testimonial.json +164 -0
  158. package/registry/items/theme.json +47 -0
  159. package/registry/items/tripleStats.json +228 -0
  160. package/registry/items/tweet.json +203 -0
  161. package/registry/items/typography.json +47 -0
  162. package/registry/items/webMockup.json +273 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1681 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-YC5MFXA5.js";
3
+ import "./chunk-FQ47WXAJ.js";
4
+ import "./chunk-EVG2ZYIY.js";
5
+ import {
6
+ isTemplatePropertyFormatSupported,
7
+ validateTemplateSchemaValue
8
+ } from "./chunk-352ZZCET.js";
9
+ import {
10
+ getBuiltinTemplateMetadata,
11
+ listBuiltinTemplateMetadata
12
+ } from "./chunk-CHLGW22F.js";
13
+ import {
14
+ assertTemplateTransitionMetadata
15
+ } from "./chunk-2E6T633S.js";
16
+ import {
17
+ BUILTIN_TEMPLATE_MANIFEST
18
+ } from "./chunk-W2XJKG6C.js";
19
+ import {
20
+ getTemplateDefaults,
21
+ isMeaningfullyPresentTemplateValue,
22
+ summarizeTemplateVariables,
23
+ templateVariableNotation
24
+ } from "./chunk-73NTSFFI.js";
25
+
26
+ // src/cli/index.ts
27
+ import { resolve as resolve7 } from "path";
28
+
29
+ // src/cli/registry.ts
30
+ import { randomUUID } from "crypto";
31
+ import {
32
+ existsSync,
33
+ mkdirSync,
34
+ readFileSync,
35
+ readdirSync,
36
+ renameSync,
37
+ unlinkSync,
38
+ writeFileSync
39
+ } from "fs";
40
+ import { basename, dirname, join as join2, resolve as resolve2 } from "path";
41
+ import { fileURLToPath } from "url";
42
+
43
+ // src/cli/safe-path.ts
44
+ import { lstatSync, realpathSync } from "fs";
45
+ import { join, relative, resolve, sep } from "path";
46
+ function lstatIfPresent(path) {
47
+ try {
48
+ return lstatSync(path);
49
+ } catch (error) {
50
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return void 0;
51
+ throw error;
52
+ }
53
+ }
54
+ function safeProjectPath(cwd, target) {
55
+ const root = resolve(cwd);
56
+ const destination = resolve(root, target);
57
+ const pathFromRoot = relative(root, destination);
58
+ if (pathFromRoot === ".." || pathFromRoot.startsWith(`..${sep}`)) {
59
+ throw new Error(`Write target escapes the project: ${target}`);
60
+ }
61
+ const rootStat = lstatIfPresent(root);
62
+ if (!rootStat) throw new Error(`Project directory does not exist: ${root}`);
63
+ if (rootStat.isSymbolicLink()) throw new Error("Project writes cannot use a symbolic-link project root");
64
+ if (!rootStat.isDirectory()) throw new Error(`Project path is not a directory: ${root}`);
65
+ const realRoot = realpathSync(root);
66
+ const segments = pathFromRoot.split(sep).filter(Boolean);
67
+ let current = root;
68
+ for (const segment of segments) {
69
+ current = join(current, segment);
70
+ const stat = lstatIfPresent(current);
71
+ if (!stat) return destination;
72
+ if (stat.isSymbolicLink()) {
73
+ throw new Error(`Write target contains a symbolic link: ${relative(root, current)}`);
74
+ }
75
+ const realCurrent = realpathSync(current);
76
+ const realPathFromRoot = relative(realRoot, realCurrent);
77
+ if (realPathFromRoot === ".." || realPathFromRoot.startsWith(`..${sep}`)) {
78
+ throw new Error(`Write target escapes the project: ${relative(root, current)}`);
79
+ }
80
+ }
81
+ return destination;
82
+ }
83
+
84
+ // src/cli/registry.ts
85
+ function defaultRegistryDir() {
86
+ const sourceCandidate = fileURLToPath(new URL("../../registry/items/", import.meta.url));
87
+ const distributionCandidate = fileURLToPath(new URL("../registry/items/", import.meta.url));
88
+ return existsSync(sourceCandidate) ? sourceCandidate : distributionCandidate;
89
+ }
90
+ function readItem(registryDir, name) {
91
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) throw new Error(`Invalid registry item: ${name}`);
92
+ const path = join2(registryDir, `${name}.json`);
93
+ if (!existsSync(path)) throw new Error(`Unknown template or registry item: ${name}`);
94
+ return JSON.parse(readFileSync(path, "utf8"));
95
+ }
96
+ function readTemplateItem(registryDir, name) {
97
+ let item2;
98
+ try {
99
+ item2 = readItem(registryDir, name);
100
+ } catch (error) {
101
+ if (error instanceof Error && error.message.startsWith("Unknown template or registry item:")) {
102
+ throw new Error(`Unknown template: ${name}`);
103
+ }
104
+ if (error instanceof Error && error.message.startsWith("Invalid registry item:")) {
105
+ throw new Error(`Invalid template: ${name}`);
106
+ }
107
+ throw error;
108
+ }
109
+ if (item2.meta?.vanillasky?.layer !== "template") throw new Error(`${item2.name} is not a template`);
110
+ return item2;
111
+ }
112
+ function dependencyName(value) {
113
+ return value.startsWith("@vanillasky/") ? value.slice("@vanillasky/".length) : value;
114
+ }
115
+ function safeDestination(cwd, target) {
116
+ return safeProjectPath(cwd, target);
117
+ }
118
+ function planCustomerFile(planned, target, content) {
119
+ const existing = planned.get(target);
120
+ if (existing != null && existing !== content) {
121
+ throw new Error(`Registry items provide conflicting content for ${target}`);
122
+ }
123
+ planned.set(target, content);
124
+ }
125
+ function registryTarget(file) {
126
+ if (file.target) return file.target;
127
+ return file.path.replace(/^src\/lib\//, "vanillasky/");
128
+ }
129
+ function primaryTemplateFile(item2) {
130
+ return item2.files?.find((file) => /scene-templates\/.*\.tsx$/.test(file.path));
131
+ }
132
+ function templateDefinition(item2) {
133
+ const templateFile = primaryTemplateFile(item2);
134
+ if (!templateFile) throw new Error(`Template ${item2.name} has no render component`);
135
+ const metadata = item2.meta?.vanillasky ?? {};
136
+ const canonical = getBuiltinTemplateMetadata(item2.name);
137
+ const schema = canonical?.schema ?? metadata.schema;
138
+ if (!schema) throw new Error(`Template ${item2.name} has no JSON Schema contract`);
139
+ return canonical ?? {
140
+ id: item2.name,
141
+ label: item2.title ?? item2.name,
142
+ description: item2.description ?? "",
143
+ useWhen: String(metadata.useWhen ?? item2.description ?? ""),
144
+ usesGlobalTextEffect: false,
145
+ usesGlobalTransition: false,
146
+ usesGlobalBackgroundEffect: false,
147
+ schema,
148
+ ...metadata.jobs == null ? {} : { jobs: metadata.jobs },
149
+ ...metadata.register == null ? {} : { register: metadata.register },
150
+ ...metadata.textCanvas == null ? {} : { textCanvas: metadata.textCanvas },
151
+ ...metadata.minDuration == null ? {} : { minDuration: metadata.minDuration },
152
+ ...metadata.preferredDuration == null ? {} : { preferredDuration: metadata.preferredDuration }
153
+ };
154
+ }
155
+ function materializedTemplateMetadata(item2) {
156
+ const definition = templateDefinition(item2);
157
+ const {
158
+ schema,
159
+ label = definition.id,
160
+ description = "",
161
+ usesGlobalTextEffect = false,
162
+ usesGlobalTransition = false,
163
+ usesGlobalBackgroundEffect = false,
164
+ ...definitionWithoutDefaults
165
+ } = definition;
166
+ return {
167
+ label,
168
+ description,
169
+ usesGlobalTextEffect,
170
+ usesGlobalTransition,
171
+ usesGlobalBackgroundEffect,
172
+ ...definitionWithoutDefaults,
173
+ schema: {
174
+ ...schema,
175
+ additionalProperties: schema.additionalProperties ?? false
176
+ }
177
+ };
178
+ }
179
+ function templateSource(item2) {
180
+ const templateFile = primaryTemplateFile(item2);
181
+ if (!templateFile) throw new Error(`Template ${item2.name} has no render component`);
182
+ const component = templateFile.content.match(/export const (\w+Template)\b/)?.[1];
183
+ if (!component) throw new Error(`Cannot identify the component exported by ${item2.name}`);
184
+ const definition = templateDefinition(item2);
185
+ const definitionLines = JSON.stringify(definition, null, 2).split("\n").slice(1, -1);
186
+ definitionLines[definitionLines.length - 1] += ",";
187
+ let componentSource = templateFile.content.replace(/(from\s+["'])\.\//g, "$1../scene-templates/").replace(/(import\(\s*["'])\.\//g, "$1../scene-templates/").replace(
188
+ new RegExp(`export const ${component}: React\\.FC<SceneTemplateProps> = \\(\\{([\\s\\S]*?)\\n\\}\\) => \\{`),
189
+ `export const ${component} = ({$1
190
+ }: SceneTemplateProps) => {`
191
+ );
192
+ if (!componentSource.includes("React.")) {
193
+ componentSource = componentSource.replace(/^import (?:\* as )?React from ["']react["'];\n/m, "");
194
+ }
195
+ return [
196
+ `import { defineTemplate } from "@vanillaskyai/video/templates";`,
197
+ "",
198
+ componentSource.trim(),
199
+ "",
200
+ `export const ${item2.name}Template = defineTemplate({`,
201
+ ...definitionLines,
202
+ ` component: ${component},`,
203
+ "});",
204
+ ""
205
+ ].join("\n");
206
+ }
207
+ function listRegistryTemplates(registryDir = defaultRegistryDir()) {
208
+ return readdirSync(registryDir).filter((name) => name.endsWith(".json")).map((name) => readItem(registryDir, name.slice(0, -5))).filter((item2) => item2.meta?.vanillasky?.layer === "template").map((item2) => {
209
+ const canonical = getBuiltinTemplateMetadata(item2.name);
210
+ return {
211
+ id: item2.name,
212
+ title: item2.title ?? item2.name,
213
+ description: canonical?.useWhen ?? item2.description ?? ""
214
+ };
215
+ }).sort((left, right) => left.id.localeCompare(right.id));
216
+ }
217
+ function listInstalledTemplates(cwd) {
218
+ const templatesDirectory = join2(resolve2(cwd), "vanillasky/templates");
219
+ if (!existsSync(templatesDirectory)) return [];
220
+ return [...new Set(readdirSync(templatesDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /^[A-Za-z][A-Za-z0-9_-]*\.(?:ts|tsx)$/.test(entry.name)).map((entry) => entry.name.replace(/\.(?:ts|tsx)$/, "")))].sort((left, right) => left.localeCompare(right));
221
+ }
222
+ function plannedChanges(cwd, planned) {
223
+ const changes = [];
224
+ for (const [path, after] of planned) {
225
+ const destination = safeDestination(cwd, path);
226
+ if (!existsSync(destination)) {
227
+ changes.push({ path, action: "create", after });
228
+ continue;
229
+ }
230
+ const before = readFileSync(destination, "utf8");
231
+ if (before !== after) changes.push({ path, action: "update", before, after });
232
+ }
233
+ return changes;
234
+ }
235
+ function applyChanges(cwd, changes) {
236
+ for (const change of changes) {
237
+ const destination = safeDestination(cwd, change.path);
238
+ mkdirSync(dirname(destination), { recursive: true });
239
+ safeProjectPath(cwd, destination);
240
+ const temporary = join2(
241
+ dirname(destination),
242
+ `.${basename(destination)}.vanillasky-tmp-${randomUUID()}`
243
+ );
244
+ try {
245
+ writeFileSync(temporary, change.after, { encoding: "utf8", flag: "wx" });
246
+ safeProjectPath(cwd, destination);
247
+ renameSync(temporary, destination);
248
+ } finally {
249
+ if (existsSync(temporary)) unlinkSync(temporary);
250
+ }
251
+ }
252
+ }
253
+ function addRegistryTemplates(options) {
254
+ const cwd = resolve2(options.cwd);
255
+ const registryDir = options.registryDir ?? defaultRegistryDir();
256
+ const requested = [...new Set(options.names)];
257
+ const requestedItems = requested.map((name) => readTemplateItem(registryDir, name));
258
+ const installed = new Set(listInstalledTemplates(cwd));
259
+ const planned = /* @__PURE__ */ new Map();
260
+ const visited = /* @__PURE__ */ new Set();
261
+ const installItem = (name) => {
262
+ if (visited.has(name)) return;
263
+ visited.add(name);
264
+ const item2 = readItem(registryDir, name);
265
+ const primary = item2.meta?.vanillasky?.layer === "template" ? primaryTemplateFile(item2) : void 0;
266
+ for (const dependency of item2.registryDependencies ?? []) {
267
+ installItem(dependencyName(dependency));
268
+ }
269
+ for (const file of item2.files ?? []) {
270
+ if (file === primary) continue;
271
+ const target = registryTarget(file);
272
+ planCustomerFile(planned, target, file.content);
273
+ }
274
+ };
275
+ for (const item2 of requestedItems) {
276
+ installItem(item2.name);
277
+ planCustomerFile(
278
+ planned,
279
+ `vanillasky/templates/${item2.name}.tsx`,
280
+ templateSource(item2)
281
+ );
282
+ }
283
+ const changes = plannedChanges(cwd, planned);
284
+ const updates = changes.filter((change) => change.action === "update");
285
+ if (!options.dryRun && !options.overwrite && updates.length > 0) {
286
+ throw new Error(`Refusing to overwrite customer-owned file: ${updates[0]?.path}`);
287
+ }
288
+ if (!options.dryRun) applyChanges(cwd, changes);
289
+ const templateChanges = new Set(changes.filter((change) => /^vanillasky\/templates\/.*\.tsx$/.test(change.path)).map((change) => change.path.slice("vanillasky/templates/".length, -".tsx".length)));
290
+ const added = requested.filter((name) => !installed.has(name) && templateChanges.has(name));
291
+ const updated = requested.filter((name) => installed.has(name) && templateChanges.has(name));
292
+ return {
293
+ added,
294
+ updated,
295
+ files: changes.map(({ path }) => path),
296
+ changes,
297
+ previewTemplates: requestedItems.map((item2) => ({
298
+ sourcePath: `vanillasky/templates/${item2.name}.tsx`,
299
+ exportName: `${item2.name}Template`,
300
+ importPath: `./templates/${item2.name}`,
301
+ metadata: materializedTemplateMetadata(item2)
302
+ }))
303
+ };
304
+ }
305
+
306
+ // src/cli/sync.ts
307
+ import { randomUUID as randomUUID2 } from "crypto";
308
+ import { lstatSync as lstatSync2 } from "fs";
309
+ import { mkdir, open, readFile, rename, unlink } from "fs/promises";
310
+ import { basename as basename3, dirname as dirname2, join as join4, relative as relative3, resolve as resolve4 } from "path";
311
+
312
+ // src/cli/project-templates.ts
313
+ import { realpathSync as realpathSync2 } from "fs";
314
+ import { readdir } from "fs/promises";
315
+ import { createRequire } from "module";
316
+ import { basename as basename2, join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
317
+ import { pathToFileURL } from "url";
318
+
319
+ // src/cli/trusted-source-process.ts
320
+ import { spawn } from "child_process";
321
+ var DEFAULT_TIMEOUT_MS = 1e4;
322
+ var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
323
+ var TrustedSourceProcessError = class extends Error {
324
+ kind;
325
+ constructor(kind, message) {
326
+ super(message);
327
+ this.name = "TrustedSourceProcessError";
328
+ this.kind = kind;
329
+ }
330
+ };
331
+ function minimalEnvironment() {
332
+ const names = ["PATH", "SYSTEMROOT", "WINDIR", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL"];
333
+ return Object.fromEntries(names.flatMap((name) => process.env[name] == null ? [] : [[name, process.env[name]]]));
334
+ }
335
+ function delay(milliseconds) {
336
+ return new Promise((resolve8) => setTimeout(resolve8, milliseconds));
337
+ }
338
+ function signalProcessGroup(pid, signal) {
339
+ try {
340
+ process.kill(-pid, signal);
341
+ return true;
342
+ } catch (error) {
343
+ const code = error.code;
344
+ if (code === "ESRCH" || code === "EPERM") return false;
345
+ throw error;
346
+ }
347
+ }
348
+ async function stopProcessGroup(pid) {
349
+ if (!signalProcessGroup(pid, "SIGTERM")) return;
350
+ await delay(100);
351
+ if (signalProcessGroup(pid, 0)) {
352
+ signalProcessGroup(pid, "SIGKILL");
353
+ await delay(25);
354
+ }
355
+ }
356
+ async function runTrustedSourceProcess(args, options) {
357
+ if (process.platform === "win32") {
358
+ throw new Error(
359
+ "Project template execution is not supported on Windows because portable process-tree cleanup cannot be guaranteed. Use --builtin from Windows or run project template commands in WSL."
360
+ );
361
+ }
362
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
363
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
364
+ const operation = options.operation ?? "loading";
365
+ let cleanup;
366
+ let processId;
367
+ const result = new Promise((resolve8, reject) => {
368
+ const child = spawn(process.execPath, [...args], {
369
+ cwd: options.cwd,
370
+ detached: true,
371
+ env: minimalEnvironment(),
372
+ stdio: ["ignore", "pipe", "pipe", "pipe"]
373
+ });
374
+ processId = child.pid;
375
+ const protocol = [];
376
+ const diagnostics = [];
377
+ let outputBytes = 0;
378
+ let failure;
379
+ let settled = false;
380
+ const fail = (error) => {
381
+ if (failure) return;
382
+ failure = error;
383
+ if (child.pid != null) cleanup = stopProcessGroup(child.pid);
384
+ };
385
+ const collect = (target) => (chunk) => {
386
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
387
+ outputBytes += buffer.byteLength;
388
+ if (outputBytes > maxOutputBytes) {
389
+ fail(new TrustedSourceProcessError("output", `${operation} output exceeded ${maxOutputBytes} bytes`));
390
+ return;
391
+ }
392
+ target.push(buffer);
393
+ };
394
+ child.stdout?.on("data", collect(diagnostics));
395
+ child.stderr?.on("data", collect(diagnostics));
396
+ const protocolStream = child.stdio[3];
397
+ if (protocolStream && "on" in protocolStream) protocolStream.on("data", collect(protocol));
398
+ const timer = setTimeout(() => {
399
+ fail(new TrustedSourceProcessError("timeout", `${operation} timed out after ${timeoutMs}ms`));
400
+ }, timeoutMs);
401
+ child.on("error", (error) => {
402
+ clearTimeout(timer);
403
+ if (settled) return;
404
+ settled = true;
405
+ reject(error);
406
+ });
407
+ child.on("close", (code, signal) => {
408
+ clearTimeout(timer);
409
+ if (settled) return;
410
+ settled = true;
411
+ if (failure) {
412
+ reject(failure);
413
+ return;
414
+ }
415
+ if (code !== 0) {
416
+ const detail = Buffer.concat(diagnostics).toString("utf8").trim();
417
+ reject(new TrustedSourceProcessError(
418
+ "exit",
419
+ detail || `template loader exited with ${code ?? signal ?? "an error"}`
420
+ ));
421
+ return;
422
+ }
423
+ resolve8(Buffer.concat(protocol).toString("utf8"));
424
+ });
425
+ });
426
+ try {
427
+ return await result;
428
+ } finally {
429
+ if (processId != null) await (cleanup ?? stopProcessGroup(processId));
430
+ }
431
+ }
432
+
433
+ // src/cli/project-templates.ts
434
+ var nodeModulesMarker = `${sep2}node_modules${sep2}`;
435
+ var MAX_TEMPLATE_SOURCES = 128;
436
+ var LOADER_CONCURRENCY = 4;
437
+ var TEMPLATE_LOADER = String.raw`
438
+ const sourceUrl = process.argv[1];
439
+ const { writeSync } = await import("node:fs");
440
+ const writeProtocol = (value) => writeSync(3, value);
441
+ for (const method of ["log", "info", "warn", "error", "debug", "dir", "trace"]) console[method] = () => undefined;
442
+ const loaded = await import(sourceUrl + "?vanillasky=loader");
443
+ const isTemplate = (value) =>
444
+ typeof value === "object" && value != null &&
445
+ typeof value.id === "string" && typeof value.component === "function" &&
446
+ typeof value.schema === "object" && value.schema != null &&
447
+ value.schema.type === "object" && typeof value.schema.properties === "object";
448
+ const entries = Object.entries(loaded);
449
+ if (loaded.default && typeof loaded.default === "object" && !isTemplate(loaded.default)) {
450
+ for (const entry of Object.entries(loaded.default)) {
451
+ if (!entries.some(([name]) => name === entry[0])) entries.push(entry);
452
+ }
453
+ }
454
+ const templates = entries.filter(([, value]) => isTemplate(value));
455
+ if (templates.length !== 1) {
456
+ throw new Error("must export exactly one template created with defineTemplate (found " + templates.length + ")");
457
+ }
458
+ const [exportName, template] = templates[0];
459
+ const { component, examples, ...rawMetadata } = template;
460
+ const metadata = Object.fromEntries(Object.entries(rawMetadata).filter(([, value]) => value !== undefined));
461
+ const seen = new Set();
462
+ function assertJson(value, path) {
463
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
464
+ if (typeof value === "number") {
465
+ if (Number.isFinite(value)) return;
466
+ throw new Error(path + " must contain only finite JSON numbers");
467
+ }
468
+ if (typeof value !== "object") throw new Error(path + " contains a non-serializable " + typeof value);
469
+ if (seen.has(value)) throw new Error(path + " contains a circular value");
470
+ seen.add(value);
471
+ if (Array.isArray(value)) value.forEach((entry, index) => assertJson(entry, path + "[" + index + "]"));
472
+ else {
473
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
474
+ throw new Error(path + " contains a non-serializable object");
475
+ }
476
+ for (const [key, entry] of Object.entries(value)) assertJson(entry, path + "." + key);
477
+ }
478
+ seen.delete(value);
479
+ }
480
+ assertJson(metadata, "template metadata");
481
+ if (examples !== undefined) assertJson(examples, "template examples");
482
+ writeProtocol(JSON.stringify({ exportName, metadata, examples }));
483
+ `;
484
+ async function sourceFiles(templatesDirectory) {
485
+ let entries;
486
+ try {
487
+ entries = await readdir(templatesDirectory, { withFileTypes: true });
488
+ } catch (error) {
489
+ if (error.code === "ENOENT") return [];
490
+ throw error;
491
+ }
492
+ const linkedSource = entries.find((entry) => entry.isSymbolicLink() && /\.(?:ts|tsx)$/.test(entry.name));
493
+ if (linkedSource) throw new Error(`Template source cannot be a symbolic link: ${linkedSource.name}`);
494
+ const files = entries.filter((entry) => entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name)).map((entry) => join3(templatesDirectory, entry.name)).sort((left, right) => left.localeCompare(right));
495
+ if (files.length > MAX_TEMPLATE_SOURCES) {
496
+ throw new Error(`A project may contain at most ${MAX_TEMPLATE_SOURCES} template source files (found ${files.length}).`);
497
+ }
498
+ return files;
499
+ }
500
+ function resolveProjectTsxRuntime(resolveModule = (specifier) => createRequire(import.meta.url).resolve(specifier)) {
501
+ let resolvedLoader;
502
+ try {
503
+ resolvedLoader = resolveModule("tsx");
504
+ } catch {
505
+ throw new Error(
506
+ "Source-owned template commands require the optional TSX compiler. Install it in your project with: npm install --save-dev tsx"
507
+ );
508
+ }
509
+ const nodeModulesIndex = resolvedLoader.lastIndexOf(nodeModulesMarker);
510
+ const dependencyRoot = nodeModulesIndex < 0 ? resolve3(resolvedLoader, "..") : resolvedLoader.slice(0, nodeModulesIndex + nodeModulesMarker.length - 1);
511
+ return { dependencyRoot, loader: pathToFileURL(resolvedLoader).href };
512
+ }
513
+ function sanitizeTrustedSourceDiagnostic(value, cwd, dependencyRoot) {
514
+ const roots = [...new Set([resolve3(cwd), realpathSync2(cwd), dependencyRoot].filter((root) => Boolean(root)))].sort((left, right) => right.length - left.length);
515
+ let safe = value;
516
+ for (const root of roots) {
517
+ const replacement = root === dependencyRoot ? "<dependency>" : ".";
518
+ safe = safe.split(root).join(replacement).split(pathToFileURL(root).href).join(replacement);
519
+ }
520
+ return safe;
521
+ }
522
+ async function mapWithConcurrency(values, concurrency, transform) {
523
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("Loader concurrency must be a positive integer");
524
+ const results = new Array(values.length);
525
+ let next = 0;
526
+ const worker = async () => {
527
+ while (next < values.length) {
528
+ const index = next;
529
+ next += 1;
530
+ results[index] = await transform(values[index], index);
531
+ }
532
+ };
533
+ await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker));
534
+ return results;
535
+ }
536
+ async function loadTemplate(filePath, cwd, options, runtime) {
537
+ const displayPath = relative2(cwd, filePath);
538
+ let serialized;
539
+ try {
540
+ const sourceUrl = pathToFileURL(filePath).href;
541
+ const stdout = await runTrustedSourceProcess([
542
+ "--import",
543
+ runtime.loader,
544
+ "--input-type=module",
545
+ "--eval",
546
+ TEMPLATE_LOADER,
547
+ sourceUrl
548
+ ], { cwd, timeoutMs: options.timeoutMs, maxOutputBytes: options.maxOutputBytes });
549
+ serialized = JSON.parse(stdout);
550
+ } catch (error) {
551
+ const detail = error instanceof TrustedSourceProcessError || error instanceof Error ? error.message : String(error);
552
+ throw new Error(`Could not load ${displayPath}: ${sanitizeTrustedSourceDiagnostic(detail, cwd, runtime.dependencyRoot)}`);
553
+ }
554
+ return {
555
+ exportName: serialized.exportName,
556
+ filePath,
557
+ importPath: `./templates/${basename2(filePath).replace(/\.(?:ts|tsx)$/, "")}`,
558
+ metadata: serialized.metadata,
559
+ ...Object.hasOwn(serialized, "examples") ? { examples: serialized.examples } : {}
560
+ };
561
+ }
562
+ async function discoverProjectTemplates(cwd, options = {}) {
563
+ const root = resolve3(cwd);
564
+ const templatesDirectory = safeProjectPath(root, "vanillasky/templates");
565
+ const excluded = new Set((options.excludeSourcePaths ?? []).map((path) => resolve3(path)));
566
+ const files = (await sourceFiles(templatesDirectory)).filter((file) => !excluded.has(resolve3(file)));
567
+ for (const file of files) safeProjectPath(root, file);
568
+ if (files.length === 0) return [];
569
+ const runtime = resolveProjectTsxRuntime();
570
+ const discovered = await mapWithConcurrency(
571
+ files,
572
+ LOADER_CONCURRENCY,
573
+ (file) => loadTemplate(file, root, options, runtime)
574
+ );
575
+ const byId = /* @__PURE__ */ new Map();
576
+ for (const template of discovered) {
577
+ const duplicate = byId.get(template.metadata.id);
578
+ if (duplicate) {
579
+ throw new Error(
580
+ `Duplicate template id ${JSON.stringify(template.metadata.id)} in ${relative2(cwd, duplicate.filePath)} and ${relative2(cwd, template.filePath)}`
581
+ );
582
+ }
583
+ byId.set(template.metadata.id, template);
584
+ }
585
+ return discovered;
586
+ }
587
+
588
+ // src/cli/sync.ts
589
+ var GENERATED_NOTICE = "// Generated by `vanillasky sync`. Do not edit.\n";
590
+ function browserSource(templates) {
591
+ const imports = templates.map(({ exportName, importPath }, index) => exportName === "default" ? `import template${index} from ${JSON.stringify(importPath)};` : `import { ${exportName} as template${index} } from ${JSON.stringify(importPath)};`);
592
+ const names = templates.map((_, index) => `template${index}`);
593
+ return [
594
+ GENERATED_NOTICE.trimEnd(),
595
+ 'import { createTemplateRegistry } from "@vanillaskyai/video/templates";',
596
+ ...imports,
597
+ "",
598
+ `export const definitions = [${names.join(", ")}];`,
599
+ "export const templates = createTemplateRegistry({ definitions });",
600
+ ""
601
+ ].join("\n");
602
+ }
603
+ function serverSource(templates) {
604
+ const metadata = JSON.stringify(templates.map(({ metadata: value }) => value), null, 2);
605
+ return [
606
+ GENERATED_NOTICE.trimEnd(),
607
+ 'import { createServerTemplateRegistry } from "@vanillaskyai/video/server";',
608
+ 'import type { ServerTemplateMetadata } from "@vanillaskyai/video/server";',
609
+ "",
610
+ `const templateMetadata: ServerTemplateMetadata[] = ${metadata};`,
611
+ "export const templates = createServerTemplateRegistry({ templates: templateMetadata });",
612
+ ""
613
+ ].join("\n");
614
+ }
615
+ async function currentSource(path) {
616
+ try {
617
+ return await readFile(path, "utf8");
618
+ } catch (error) {
619
+ if (error.code === "ENOENT") return void 0;
620
+ throw error;
621
+ }
622
+ }
623
+ function generatedOutputs(cwd, discovered) {
624
+ const vanillaskyDirectory = join4(cwd, "vanillasky");
625
+ return [
626
+ { path: join4(vanillaskyDirectory, "index.ts"), source: browserSource(discovered) },
627
+ { path: join4(vanillaskyDirectory, "server.ts"), source: serverSource(discovered) }
628
+ ];
629
+ }
630
+ async function previewTemplateSync(options) {
631
+ const cwd = resolve4(options.cwd);
632
+ const replacements = new Set(options.templates.map(({ sourcePath }) => safeProjectPath(cwd, sourcePath)));
633
+ const installed = await discoverProjectTemplates(cwd, { excludeSourcePaths: [...replacements] });
634
+ const proposed = options.templates.map((template) => ({
635
+ ...template,
636
+ filePath: safeProjectPath(cwd, template.sourcePath)
637
+ }));
638
+ const templates = [...installed, ...proposed].sort((left, right) => left.filePath.localeCompare(right.filePath));
639
+ const byId = /* @__PURE__ */ new Map();
640
+ for (const template of templates) {
641
+ const duplicate = byId.get(template.metadata.id);
642
+ if (duplicate) {
643
+ throw new Error(
644
+ `Duplicate template id ${JSON.stringify(template.metadata.id)} in ${relative3(cwd, duplicate.filePath)} and ${relative3(cwd, template.filePath)}`
645
+ );
646
+ }
647
+ byId.set(template.metadata.id, template);
648
+ }
649
+ const changes = [];
650
+ for (const output of generatedOutputs(cwd, templates)) {
651
+ safeProjectPath(cwd, output.path);
652
+ const before = await currentSource(output.path);
653
+ if (before === output.source) continue;
654
+ changes.push({
655
+ path: relative3(cwd, output.path),
656
+ action: before == null ? "create" : "update",
657
+ ...before == null ? {} : { before },
658
+ after: output.source
659
+ });
660
+ }
661
+ return changes;
662
+ }
663
+ async function inspectGeneratedTemplateStatus(cwd, discovered) {
664
+ const root = resolve4(cwd);
665
+ const stale = [];
666
+ for (const output of generatedOutputs(root, discovered)) {
667
+ safeProjectPath(root, output.path);
668
+ if (await currentSource(output.path) !== output.source) stale.push(relative3(root, output.path));
669
+ }
670
+ return {
671
+ browser: { path: "vanillasky/index.ts", current: !stale.includes("vanillasky/index.ts") },
672
+ server: { path: "vanillasky/server.ts", current: !stale.includes("vanillasky/server.ts") },
673
+ current: stale.length === 0,
674
+ stale
675
+ };
676
+ }
677
+ async function unlinkIfPresent(path) {
678
+ try {
679
+ await unlink(path);
680
+ } catch (error) {
681
+ if (error.code !== "ENOENT") throw error;
682
+ }
683
+ }
684
+ async function writeStagedFile(path, source) {
685
+ const handle = await open(path, "wx", 420);
686
+ try {
687
+ await handle.writeFile(source, "utf8");
688
+ await handle.sync();
689
+ } finally {
690
+ await handle.close();
691
+ }
692
+ }
693
+ async function writeOutputsTransactionally(cwd, outputs, renameFile, unlinkFile) {
694
+ const planned = outputs.map((output) => {
695
+ safeProjectPath(cwd, output.path);
696
+ const nonce = randomUUID2();
697
+ const directory = dirname2(output.path);
698
+ return {
699
+ ...output,
700
+ temporary: join4(directory, `.${basename3(output.path)}.vanillasky-stage-${nonce}`),
701
+ backup: join4(directory, `.${basename3(output.path)}.vanillasky-backup-${nonce}`),
702
+ hadPrevious: lstatSync2(output.path, { throwIfNoEntry: false }) != null,
703
+ backupCreated: false,
704
+ installed: false
705
+ };
706
+ });
707
+ try {
708
+ for (const output of planned) {
709
+ safeProjectPath(cwd, output.temporary);
710
+ safeProjectPath(cwd, output.backup);
711
+ await writeStagedFile(output.temporary, output.source);
712
+ }
713
+ for (const output of planned) {
714
+ safeProjectPath(cwd, output.path);
715
+ if (output.hadPrevious) {
716
+ await renameFile(output.path, output.backup);
717
+ output.backupCreated = true;
718
+ }
719
+ await renameFile(output.temporary, output.path);
720
+ output.installed = true;
721
+ }
722
+ } catch (error) {
723
+ for (const output of [...planned].reverse()) {
724
+ if (output.installed) await unlinkIfPresent(output.path);
725
+ if (output.backupCreated) await rename(output.backup, output.path);
726
+ await unlinkIfPresent(output.temporary);
727
+ }
728
+ throw error;
729
+ }
730
+ const warnings = [];
731
+ for (const output of planned) {
732
+ try {
733
+ await unlinkFile(output.backup);
734
+ } catch (error) {
735
+ if (error.code !== "ENOENT") {
736
+ warnings.push(`Could not remove transaction backup: ${relative3(cwd, output.backup)}`);
737
+ }
738
+ }
739
+ }
740
+ return warnings;
741
+ }
742
+ async function syncTemplates(options = {}) {
743
+ const cwd = resolve4(options.cwd ?? process.cwd());
744
+ const vanillaskyDirectory = join4(cwd, "vanillasky");
745
+ const discovered = await discoverProjectTemplates(cwd, { timeoutMs: options.timeoutMs });
746
+ const outputs = generatedOutputs(cwd, discovered);
747
+ for (const output of outputs) safeProjectPath(cwd, output.path);
748
+ const drifted = [];
749
+ let warnings = [];
750
+ for (const output of outputs) {
751
+ if (await currentSource(output.path) !== output.source) drifted.push(relative3(cwd, output.path));
752
+ }
753
+ if (options.check && drifted.length > 0) {
754
+ throw new Error(`Generated template files are out of date: ${drifted.join(", ")}. Run \`vanillasky sync\`.`);
755
+ }
756
+ if (!options.check && drifted.length > 0) {
757
+ await mkdir(vanillaskyDirectory, { recursive: true });
758
+ const changedOutputs = outputs.filter((output) => drifted.includes(relative3(cwd, output.path)));
759
+ for (const output of changedOutputs) safeProjectPath(cwd, output.path);
760
+ warnings = await writeOutputsTransactionally(cwd, changedOutputs, options.renameFile ?? rename, options.unlinkFile ?? unlink);
761
+ }
762
+ return {
763
+ changed: drifted.length > 0,
764
+ files: drifted,
765
+ templates: discovered.map(({ metadata }) => metadata.id),
766
+ warnings
767
+ };
768
+ }
769
+
770
+ // src/cli/check.ts
771
+ import { createHash } from "crypto";
772
+ import { join as join5, relative as relative4, resolve as resolve5 } from "path";
773
+ import { pathToFileURL as pathToFileURL2 } from "url";
774
+ var DEFAULT_TIMEOUT_MS2 = 15e3;
775
+ var checkRuntimeUrl = new URL("./check-runtime.js", import.meta.url).href;
776
+ var JOBS = new Set(BUILTIN_TEMPLATE_MANIFEST.flatMap(({ jobs }) => jobs));
777
+ var REGISTERS = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ register }) => register));
778
+ var TEXT_CANVASES = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ textCanvas }) => textCanvas));
779
+ var FAMILIES = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ family }) => family));
780
+ var CONTENT_UNITS = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ timing }) => timing.contentUnit));
781
+ var PROPERTY_KEYS = /* @__PURE__ */ new Set([
782
+ "type",
783
+ "format",
784
+ "enum",
785
+ "title",
786
+ "description",
787
+ "default",
788
+ "examples",
789
+ "minItems",
790
+ "maxItems",
791
+ "minLength",
792
+ "maxLength",
793
+ "minimum",
794
+ "maximum",
795
+ "items",
796
+ "properties",
797
+ "required",
798
+ "additionalProperties"
799
+ ]);
800
+ var SCHEMA_KEYS = /* @__PURE__ */ new Set(["type", "properties", "required", "additionalProperties", "x-vanillasky"]);
801
+ var SCHEMA_EXTENSION_KEYS = /* @__PURE__ */ new Set(["requiresStat", "allowsStockMedia", "requiredAnyOf"]);
802
+ var METADATA_KEYS = /* @__PURE__ */ new Set([
803
+ "id",
804
+ "label",
805
+ "description",
806
+ "category",
807
+ "family",
808
+ "jobs",
809
+ "register",
810
+ "useWhen",
811
+ "avoidWhen",
812
+ "usesGlobalTextEffect",
813
+ "usesGlobalTransition",
814
+ "transitionTiming",
815
+ "usesGlobalBackgroundEffect",
816
+ "textCanvas",
817
+ "schema",
818
+ "minDuration",
819
+ "preferredDuration",
820
+ "timing"
821
+ ]);
822
+ function assertJson(value, path, seen = /* @__PURE__ */ new Set()) {
823
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
824
+ if (typeof value === "number") {
825
+ if (Number.isFinite(value)) return;
826
+ throw new Error(`${path} must contain only finite JSON numbers`);
827
+ }
828
+ if (typeof value !== "object") throw new Error(`${path} contains a non-serializable ${typeof value}`);
829
+ if (seen.has(value)) throw new Error(`${path} contains a circular value`);
830
+ seen.add(value);
831
+ if (Array.isArray(value)) value.forEach((entry, index) => assertJson(entry, `${path}[${index}]`, seen));
832
+ else {
833
+ const prototype = Object.getPrototypeOf(value);
834
+ if (prototype !== Object.prototype && prototype !== null) throw new Error(`${path} contains a non-serializable object`);
835
+ for (const [key, entry] of Object.entries(value)) assertJson(entry, `${path}.${key}`, seen);
836
+ }
837
+ seen.delete(value);
838
+ }
839
+ function validateRequired(required, properties, path) {
840
+ const seen = /* @__PURE__ */ new Set();
841
+ for (const key of required ?? []) {
842
+ if (typeof key !== "string" || !Object.hasOwn(properties, key)) throw new Error(`${path} references undeclared property ${JSON.stringify(key)}`);
843
+ if (seen.has(key)) throw new Error(`${path} contains duplicate property ${JSON.stringify(key)}`);
844
+ seen.add(key);
845
+ }
846
+ }
847
+ function validateProperty(property, path, optional) {
848
+ if (!property || typeof property !== "object" || Array.isArray(property)) throw new Error(`${path} must be an object`);
849
+ for (const key of Object.keys(property)) if (!PROPERTY_KEYS.has(key)) throw new Error(`${path}.${key} is not supported`);
850
+ if (!property.type || !["string", "number", "integer", "boolean", "array", "object"].includes(property.type)) {
851
+ throw new Error(`${path}.type must be a supported JSON Schema type`);
852
+ }
853
+ if (property.format !== void 0 && !isTemplatePropertyFormatSupported(property.format, property.type)) {
854
+ throw new Error(`${path}.format is not supported`);
855
+ }
856
+ for (const key of ["title", "description"]) {
857
+ if (property[key] !== void 0 && typeof property[key] !== "string") throw new Error(`${path}.${key} must be string`);
858
+ }
859
+ if (property.examples !== void 0 && !Array.isArray(property.examples)) throw new Error(`${path}.examples must be an array`);
860
+ if (property.type !== "string") {
861
+ for (const key of ["enum", "minLength", "maxLength"]) if (property[key] !== void 0) throw new Error(`${path}.${key} is only supported for strings`);
862
+ }
863
+ if (property.type !== "number" && property.type !== "integer") {
864
+ for (const key of ["minimum", "maximum"]) if (property[key] !== void 0) throw new Error(`${path}.${key} is only supported for numbers`);
865
+ }
866
+ if (property.type !== "array") {
867
+ for (const key of ["minItems", "maxItems"]) if (property[key] !== void 0) throw new Error(`${path}.${key} is only supported for arrays`);
868
+ }
869
+ if (property.enum !== void 0 && (!Array.isArray(property.enum) || property.enum.length === 0 || property.enum.some((value) => typeof value !== "string"))) {
870
+ throw new Error(`${path}.enum must contain one or more strings`);
871
+ }
872
+ for (const key of ["minItems", "maxItems", "minLength", "maxLength"]) {
873
+ const value = property[key];
874
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new Error(`${path}.${key} must be a non-negative integer`);
875
+ }
876
+ for (const key of ["minimum", "maximum"]) {
877
+ const value = property[key];
878
+ if (value !== void 0 && !Number.isFinite(value)) throw new Error(`${path}.${key} must be finite`);
879
+ }
880
+ if (property.minItems != null && property.maxItems != null && property.minItems > property.maxItems) throw new Error(`${path}.minItems must not exceed maxItems`);
881
+ if (property.minLength != null && property.maxLength != null && property.minLength > property.maxLength) throw new Error(`${path}.minLength must not exceed maxLength`);
882
+ if (property.minimum != null && property.maximum != null && property.minimum > property.maximum) throw new Error(`${path}.minimum must not exceed maximum`);
883
+ if (property.type === "array") {
884
+ if (!property.items) throw new Error(`${path}.items is required for arrays`);
885
+ validateProperty(property.items, `${path}.items`, false);
886
+ } else if (property.items !== void 0) throw new Error(`${path}.items is only supported for arrays`);
887
+ if (property.type === "object") {
888
+ if (!property.properties || typeof property.properties !== "object" || Array.isArray(property.properties)) throw new Error(`${path}.properties must be an object`);
889
+ if (property.required !== void 0 && !Array.isArray(property.required)) throw new Error(`${path}.required must be an array`);
890
+ if (property.additionalProperties !== void 0 && typeof property.additionalProperties !== "boolean") throw new Error(`${path}.additionalProperties must be boolean`);
891
+ validateRequired(property.required, property.properties, `${path}.required`);
892
+ const required = new Set(property.required ?? []);
893
+ for (const [name, child] of Object.entries(property.properties)) validateProperty(child, `${path}.properties.${name}`, !required.has(name));
894
+ } else if (property.properties !== void 0 || property.required !== void 0 || property.additionalProperties !== void 0) {
895
+ throw new Error(`${path} object keywords require type object`);
896
+ }
897
+ if (property.default !== void 0) {
898
+ assertJson(property.default, `${path}.default`);
899
+ validateTemplateSchemaValue(property, property.default, `${path}.default`, {
900
+ allowEmptyOptionalMedia: true,
901
+ optional
902
+ });
903
+ }
904
+ for (const [index, example] of (property.examples ?? []).entries()) {
905
+ assertJson(example, `${path}.examples[${index}]`);
906
+ validateTemplateSchemaValue(property, example, `${path}.examples[${index}]`, {
907
+ allowEmptyOptionalMedia: true,
908
+ optional
909
+ });
910
+ }
911
+ }
912
+ function validateSchema(schema) {
913
+ for (const key of Object.keys(schema)) if (!SCHEMA_KEYS.has(key)) throw new Error(`schema.${key} is not supported`);
914
+ if (schema.type !== "object" || !schema.properties || typeof schema.properties !== "object" || Array.isArray(schema.properties)) {
915
+ throw new Error("schema must describe an object with properties");
916
+ }
917
+ if (schema.required !== void 0 && !Array.isArray(schema.required)) throw new Error("schema.required must be an array");
918
+ validateRequired(schema.required, schema.properties, "schema.required");
919
+ if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") throw new Error("schema.additionalProperties must be boolean");
920
+ const extension = schema["x-vanillasky"];
921
+ if (extension !== void 0) {
922
+ if (!extension || typeof extension !== "object" || Array.isArray(extension)) throw new Error("schema.x-vanillasky must be an object");
923
+ for (const key of Object.keys(extension)) if (!SCHEMA_EXTENSION_KEYS.has(key)) throw new Error(`schema.x-vanillasky.${key} is not supported`);
924
+ for (const key of ["requiresStat", "allowsStockMedia"]) {
925
+ if (extension[key] !== void 0 && typeof extension[key] !== "boolean") throw new Error(`schema.x-vanillasky.${key} must be boolean`);
926
+ }
927
+ if (extension.requiredAnyOf !== void 0) {
928
+ if (!Array.isArray(extension.requiredAnyOf)) throw new Error("schema.x-vanillasky.requiredAnyOf must be an array");
929
+ for (const [groupIndex, group] of extension.requiredAnyOf.entries()) {
930
+ const path = `schema.x-vanillasky.requiredAnyOf[${groupIndex}]`;
931
+ if (!Array.isArray(group) || group.length === 0) throw new Error(`${path} must contain at least one property`);
932
+ for (const field of group) {
933
+ if (typeof field !== "string" || !Object.hasOwn(schema.properties, field)) {
934
+ throw new Error(`${path} references undeclared property ${JSON.stringify(field)}`);
935
+ }
936
+ }
937
+ }
938
+ }
939
+ }
940
+ const required = new Set(schema.required ?? []);
941
+ for (const [name, property] of Object.entries(schema.properties)) validateProperty(property, `schema.properties.${name}`, !required.has(name));
942
+ }
943
+ function validateVariables(schema, variables, path, enforceRequiredAnyOf) {
944
+ assertJson(variables, path);
945
+ for (const required2 of schema.required ?? []) if (!Object.hasOwn(variables, required2)) throw new Error(`${path}.${required2} is required`);
946
+ if (schema.additionalProperties === false) {
947
+ for (const key of Object.keys(variables)) if (!Object.hasOwn(schema.properties, key)) throw new Error(`${path}.${key} is not declared`);
948
+ }
949
+ const required = new Set(schema.required ?? []);
950
+ for (const [key, property] of Object.entries(schema.properties)) {
951
+ if (Object.hasOwn(variables, key)) validateTemplateSchemaValue(property, variables[key], `${path}.${key}`, {
952
+ allowEmptyOptionalMedia: true,
953
+ optional: !required.has(key)
954
+ });
955
+ }
956
+ if (enforceRequiredAnyOf) {
957
+ for (const group of schema["x-vanillasky"]?.requiredAnyOf ?? []) {
958
+ if (group.some((field) => Object.hasOwn(variables, field) && isMeaningfullyPresentTemplateValue(variables[field]))) continue;
959
+ throw new Error(`${path} requires a non-empty value for ${group.join(" or ")}`);
960
+ }
961
+ }
962
+ }
963
+ function validateTemplate(template, cwd) {
964
+ const metadata = template.metadata;
965
+ const file = relative4(cwd, template.filePath);
966
+ const fail = (message) => {
967
+ throw new Error(`${file}: ${message}`);
968
+ };
969
+ try {
970
+ for (const key of Object.keys(metadata)) if (!METADATA_KEYS.has(key)) throw new Error(`metadata.${key} is not supported`);
971
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(metadata.id)) throw new Error(`invalid template id ${JSON.stringify(metadata.id)}`);
972
+ if (typeof metadata.useWhen !== "string" || !metadata.useWhen.trim()) throw new Error("useWhen must be a non-empty string");
973
+ for (const key of ["label", "description", "avoidWhen"]) if (metadata[key] !== void 0 && typeof metadata[key] !== "string") throw new Error(`${key} must be string`);
974
+ if (metadata.category !== void 0 && metadata.category !== null && typeof metadata.category !== "string") throw new Error("category must be string or null");
975
+ for (const key of ["usesGlobalTextEffect", "usesGlobalTransition", "usesGlobalBackgroundEffect"]) {
976
+ if (typeof metadata[key] !== "boolean") throw new Error(`${key} must be boolean`);
977
+ }
978
+ assertTemplateTransitionMetadata(metadata);
979
+ if (metadata.jobs !== void 0) {
980
+ if (!Array.isArray(metadata.jobs) || metadata.jobs.some((job) => !JOBS.has(job))) throw new Error("jobs contains an unsupported value");
981
+ const jobs = /* @__PURE__ */ new Set();
982
+ for (const job of metadata.jobs) {
983
+ if (jobs.has(job)) throw new Error(`jobs contains duplicate value ${JSON.stringify(job)}`);
984
+ jobs.add(job);
985
+ }
986
+ }
987
+ if (metadata.register !== void 0 && !REGISTERS.has(metadata.register)) throw new Error("register contains an unsupported value");
988
+ if (metadata.textCanvas !== void 0 && !TEXT_CANVASES.has(metadata.textCanvas)) throw new Error("textCanvas contains an unsupported value");
989
+ if (metadata.family !== void 0 && !FAMILIES.has(metadata.family)) throw new Error("family contains an unsupported value");
990
+ for (const key of ["minDuration", "preferredDuration"]) {
991
+ const duration = metadata[key];
992
+ if (duration !== void 0 && (!Number.isFinite(duration) || duration <= 0)) throw new Error(`${key} must be finite and positive`);
993
+ }
994
+ if (metadata.minDuration != null && metadata.preferredDuration != null && metadata.minDuration > metadata.preferredDuration) {
995
+ throw new Error("minDuration must not exceed preferredDuration");
996
+ }
997
+ validateSchema(metadata.schema);
998
+ if (metadata.timing !== void 0) {
999
+ if (!metadata.timing || typeof metadata.timing !== "object" || Array.isArray(metadata.timing)) throw new Error("timing must be an object");
1000
+ for (const key of Object.keys(metadata.timing)) if (!(/* @__PURE__ */ new Set(["contentFields", "contentUnit"])).has(key)) throw new Error(`timing.${key} is not supported`);
1001
+ if (!Array.isArray(metadata.timing.contentFields) || metadata.timing.contentFields.length === 0) throw new Error("timing.contentFields must contain at least one schema property");
1002
+ const timingFields = /* @__PURE__ */ new Set();
1003
+ for (const field of metadata.timing.contentFields) {
1004
+ if (typeof field !== "string" || !Object.hasOwn(metadata.schema.properties, field)) throw new Error(`timing.contentFields references undeclared property ${JSON.stringify(field)}`);
1005
+ if (timingFields.has(field)) throw new Error(`timing.contentFields contains duplicate property ${JSON.stringify(field)}`);
1006
+ timingFields.add(field);
1007
+ }
1008
+ if (!CONTENT_UNITS.has(metadata.timing.contentUnit)) throw new Error("timing.contentUnit contains an unsupported value");
1009
+ }
1010
+ const defaults = getTemplateDefaults(metadata.schema);
1011
+ if (template.examples !== void 0 && !Array.isArray(template.examples)) throw new Error("examples must be an array");
1012
+ const supplied = template.examples ?? [];
1013
+ for (const [index, value] of supplied.entries()) {
1014
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`examples[${index}] must be a plain object`);
1015
+ for (const key of Object.keys(value)) if (!(/* @__PURE__ */ new Set(["name", "variables"])).has(key)) throw new Error(`examples[${index}].${key} is not supported`);
1016
+ const candidate = value;
1017
+ if (!candidate.variables || typeof candidate.variables !== "object" || Array.isArray(candidate.variables)) {
1018
+ throw new Error(`example ${JSON.stringify(candidate.name)} variables must be a plain object`);
1019
+ }
1020
+ }
1021
+ const examples = supplied.length > 0 ? supplied.map(({ name, variables }) => ({
1022
+ name,
1023
+ variables: { ...defaults, ...variables },
1024
+ enforceRequiredAnyOf: true
1025
+ })) : [{ name: "defaults", variables: defaults, enforceRequiredAnyOf: false }];
1026
+ const names = /* @__PURE__ */ new Set();
1027
+ for (const example of examples) {
1028
+ if (typeof example.name !== "string" || !example.name.trim()) throw new Error("example names must be non-empty strings");
1029
+ if (names.has(example.name)) throw new Error(`duplicate example name ${JSON.stringify(example.name)}`);
1030
+ names.add(example.name);
1031
+ validateVariables(
1032
+ metadata.schema,
1033
+ example.variables,
1034
+ `example ${JSON.stringify(example.name)}`,
1035
+ example.enforceRequiredAnyOf
1036
+ );
1037
+ }
1038
+ return examples;
1039
+ } catch (error) {
1040
+ return fail(error instanceof Error ? error.message : String(error));
1041
+ }
1042
+ }
1043
+ var RENDER_LOADER = String.raw`
1044
+ const [runtimeUrl, payload] = process.argv.slice(1);
1045
+ const { writeSync } = await import("node:fs");
1046
+ for (const method of ["log", "info", "warn", "error", "debug", "dir", "trace"]) console[method] = () => undefined;
1047
+ const { renderTemplateChecks } = await import(runtimeUrl);
1048
+ const result = await renderTemplateChecks(JSON.parse(payload));
1049
+ writeSync(3, JSON.stringify(result));
1050
+ `;
1051
+ async function renderExample(template, example, cwd, timeoutMs, maxOutputBytes, runtime) {
1052
+ try {
1053
+ const payload = JSON.stringify({
1054
+ sourceUrl: pathToFileURL2(template.filePath).href,
1055
+ exportName: template.exportName,
1056
+ templateId: template.metadata.id,
1057
+ exampleName: example.name,
1058
+ variables: example.variables,
1059
+ duration: template.metadata.preferredDuration ?? template.metadata.minDuration ?? 5,
1060
+ timeoutMs
1061
+ });
1062
+ const output = await runTrustedSourceProcess([
1063
+ "--import",
1064
+ runtime.loader,
1065
+ "--input-type=module",
1066
+ "--eval",
1067
+ RENDER_LOADER,
1068
+ checkRuntimeUrl,
1069
+ payload
1070
+ ], { cwd, timeoutMs, maxOutputBytes, operation: "template rendering" });
1071
+ return JSON.parse(output).renders;
1072
+ } catch (error) {
1073
+ const detail = error instanceof Error ? error.message : String(error);
1074
+ throw new Error(`Could not render ${relative4(cwd, template.filePath)}: ${sanitizeTrustedSourceDiagnostic(detail, cwd, runtime.dependencyRoot)}`);
1075
+ }
1076
+ }
1077
+ var REGISTRY_LOADER = String.raw`
1078
+ const [browserUrl, serverUrl] = process.argv.slice(1);
1079
+ const { writeSync } = await import("node:fs");
1080
+ for (const method of ["log", "info", "warn", "error", "debug", "dir", "trace"]) console[method] = () => undefined;
1081
+ const browser = await import(browserUrl + "?check=" + Date.now());
1082
+ const server = await import(serverUrl + "?check=" + Date.now());
1083
+ writeSync(3, JSON.stringify({
1084
+ browser: browser.templates.listTemplateMetadata(),
1085
+ server: server.templates.listTemplateMetadata(),
1086
+ }));
1087
+ `;
1088
+ function stable(value) {
1089
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1090
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
1091
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`).join(",")}}`;
1092
+ }
1093
+ async function verifyGeneratedParity(cwd, expected, timeoutMs, maxOutputBytes, runtime) {
1094
+ try {
1095
+ const output = await runTrustedSourceProcess([
1096
+ "--import",
1097
+ runtime.loader,
1098
+ "--input-type=module",
1099
+ "--eval",
1100
+ REGISTRY_LOADER,
1101
+ pathToFileURL2(join5(cwd, "vanillasky/index.ts")).href,
1102
+ pathToFileURL2(join5(cwd, "vanillasky/server.ts")).href
1103
+ ], { cwd, timeoutMs, maxOutputBytes, operation: "generated registry parity" });
1104
+ const loaded = JSON.parse(output);
1105
+ const expectedFingerprint = createHash("sha256").update(stable(expected)).digest("hex");
1106
+ for (const [entrypoint, metadata] of Object.entries(loaded)) {
1107
+ const fingerprint = createHash("sha256").update(stable(metadata)).digest("hex");
1108
+ if (fingerprint !== expectedFingerprint) {
1109
+ throw new Error(`${entrypoint} registry metadata/IDs/order fingerprint differs from template sources`);
1110
+ }
1111
+ }
1112
+ } catch (error) {
1113
+ const detail = error instanceof Error ? error.message : String(error);
1114
+ throw new Error(`Generated registry parity failed: ${sanitizeTrustedSourceDiagnostic(detail, cwd, runtime.dependencyRoot)}`);
1115
+ }
1116
+ }
1117
+ async function checkTemplates(options = {}) {
1118
+ const cwd = resolve5(options.cwd ?? process.cwd());
1119
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1120
+ const templates = await discoverProjectTemplates(cwd, {
1121
+ timeoutMs,
1122
+ maxOutputBytes: options.maxOutputBytes
1123
+ });
1124
+ if (templates.length === 0) throw new Error("No templates found in vanillasky/templates.");
1125
+ const runtime = resolveProjectTsxRuntime();
1126
+ const examples = templates.map((template) => ({ template, examples: validateTemplate(template, cwd) }));
1127
+ await syncTemplates({ cwd, check: true, timeoutMs });
1128
+ let renders = 0;
1129
+ for (const item2 of examples) {
1130
+ for (const example of item2.examples) {
1131
+ renders += await renderExample(item2.template, example, cwd, timeoutMs, options.maxOutputBytes, runtime);
1132
+ }
1133
+ }
1134
+ await verifyGeneratedParity(cwd, templates.map(({ metadata }) => metadata), timeoutMs, options.maxOutputBytes, runtime);
1135
+ return {
1136
+ templates: templates.length,
1137
+ examples: examples.reduce((count, item2) => count + item2.examples.length, 0),
1138
+ renders
1139
+ };
1140
+ }
1141
+
1142
+ // src/cli/create.ts
1143
+ import { link, lstat, mkdir as mkdir2, open as open2, rmdir, unlink as unlink2 } from "fs/promises";
1144
+ import { join as join6, relative as relative5, resolve as resolve6 } from "path";
1145
+ var TEMPLATE_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
1146
+ var MAX_TEMPLATE_ID_BYTES = 128;
1147
+ var WINDOWS_RESERVED_BASENAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
1148
+ var temporaryFileSequence = 0;
1149
+ function titleFor(id) {
1150
+ return id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").split(" ").filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
1151
+ }
1152
+ function sourceFor(id) {
1153
+ const title = titleFor(id);
1154
+ const titleLiteral = JSON.stringify(title);
1155
+ return `import { defineTemplate } from "@vanillaskyai/video/templates";
1156
+
1157
+ export const template = defineTemplate({
1158
+ id: ${JSON.stringify(id)},
1159
+ label: ${titleLiteral},
1160
+ description: "A focused headline and supporting detail for a grounded visual response.",
1161
+ useWhen: "Use when the response should emphasize ${title} with one concise supporting detail.",
1162
+ schema: {
1163
+ type: "object",
1164
+ properties: {
1165
+ title: {
1166
+ type: "string",
1167
+ title: "Headline",
1168
+ description: "The main grounded point the viewer should remember.",
1169
+ minLength: 1,
1170
+ maxLength: 72,
1171
+ default: ${titleLiteral},
1172
+ },
1173
+ subtitle: {
1174
+ type: "string",
1175
+ title: "Supporting detail",
1176
+ description: "One concise detail that explains or grounds the headline.",
1177
+ minLength: 1,
1178
+ maxLength: 140,
1179
+ default: "A visual response grounded in the available context.",
1180
+ },
1181
+ },
1182
+ required: ["title", "subtitle"],
1183
+ additionalProperties: false,
1184
+ } as const,
1185
+ examples: [{
1186
+ name: "${title} update",
1187
+ variables: {
1188
+ title: ${titleLiteral},
1189
+ subtitle: "A visual response grounded in the available context.",
1190
+ },
1191
+ }],
1192
+ minDuration: 3,
1193
+ preferredDuration: 5,
1194
+ component: ({ variables, progress, width, height, safeZone }) => {
1195
+ const isPortrait = height >= width;
1196
+ const reveal = Math.max(0, Math.min(1, progress * 1.5));
1197
+ const contentWidth = isPortrait
1198
+ ? width - safeZone.left - safeZone.right
1199
+ : Math.min(width * 0.72, 960);
1200
+
1201
+ return <section style={{
1202
+ boxSizing: "border-box",
1203
+ width,
1204
+ height,
1205
+ padding: safeZone.top + "px " + safeZone.right + "px " + safeZone.bottom + "px " + safeZone.left + "px",
1206
+ display: "grid",
1207
+ placeItems: "center",
1208
+ overflow: "hidden",
1209
+ color: "#ffffff",
1210
+ background: "linear-gradient(145deg, #15112b 0%, #382f73 55%, #7658d6 100%)",
1211
+ fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
1212
+ }}>
1213
+ <div style={{
1214
+ width: contentWidth,
1215
+ opacity: reveal,
1216
+ transform: "translateY(" + Math.round((1 - reveal) * 24) + "px)",
1217
+ textAlign: isPortrait ? "left" : "center",
1218
+ }}>
1219
+ <h1 style={{
1220
+ margin: 0,
1221
+ fontSize: Math.max(34, Math.min(width * (isPortrait ? 0.1 : 0.065), 88)),
1222
+ lineHeight: 0.98,
1223
+ letterSpacing: "-0.045em",
1224
+ }}>
1225
+ {variables.title}
1226
+ </h1>
1227
+ <p style={{
1228
+ margin: Math.max(18, height * 0.035) + "px 0 0",
1229
+ maxWidth: isPortrait ? "100%" : 760,
1230
+ fontSize: Math.max(18, Math.min(width * 0.03, 34)),
1231
+ lineHeight: 1.35,
1232
+ color: "rgba(255, 255, 255, 0.78)",
1233
+ }}>
1234
+ {variables.subtitle}
1235
+ </p>
1236
+ </div>
1237
+ </section>;
1238
+ },
1239
+ });
1240
+ `;
1241
+ }
1242
+ async function status(path) {
1243
+ try {
1244
+ return await lstat(path);
1245
+ } catch (error) {
1246
+ if (error.code === "ENOENT") return void 0;
1247
+ throw error;
1248
+ }
1249
+ }
1250
+ async function assertDirectoryOrMissing(path, displayPath) {
1251
+ const existing = await status(path);
1252
+ if (!existing) return false;
1253
+ if (existing.isSymbolicLink()) throw new Error(`Refusing to create through symbolic link: ${displayPath}`);
1254
+ if (!existing.isDirectory()) throw new Error(`Template path ancestor is not a directory: ${displayPath}`);
1255
+ return true;
1256
+ }
1257
+ async function removeCreatedDirectories(paths) {
1258
+ for (const path of [...paths].reverse()) {
1259
+ try {
1260
+ await rmdir(path);
1261
+ } catch (error) {
1262
+ const code = error.code;
1263
+ if (code !== "ENOENT" && code !== "ENOTEMPTY") throw error;
1264
+ }
1265
+ }
1266
+ }
1267
+ async function createTemplate(options) {
1268
+ const cwd = resolve6(options.cwd ?? process.cwd());
1269
+ const { id } = options;
1270
+ if (!TEMPLATE_ID.test(id)) {
1271
+ throw new Error(`Invalid template id: ${JSON.stringify(id)}. Use a letter first, followed by letters, numbers, _ or -.`);
1272
+ }
1273
+ if (Buffer.byteLength(id, "utf8") > MAX_TEMPLATE_ID_BYTES) {
1274
+ throw new Error(`Invalid template id: template ids must be at most ${MAX_TEMPLATE_ID_BYTES} UTF-8 bytes.`);
1275
+ }
1276
+ const windowsBasename = `${id}.tsx`.replace(/[ .]+$/u, "").split(".", 1)[0];
1277
+ if (WINDOWS_RESERVED_BASENAME.test(windowsBasename)) {
1278
+ throw new Error(`Invalid template id: ${JSON.stringify(id)}. This name is reserved on Windows.`);
1279
+ }
1280
+ const vanillaskyDirectory = join6(cwd, "vanillasky");
1281
+ const templatesDirectory = join6(vanillaskyDirectory, "templates");
1282
+ const destination = join6(templatesDirectory, `${id}.tsx`);
1283
+ const destinationPath = relative5(cwd, destination);
1284
+ const createdDirectories = [];
1285
+ for (const target of [destination, join6(vanillaskyDirectory, "index.ts"), join6(vanillaskyDirectory, "server.ts")]) {
1286
+ safeProjectPath(cwd, target);
1287
+ }
1288
+ for (const [path, displayPath] of [
1289
+ [vanillaskyDirectory, "vanillasky"],
1290
+ [templatesDirectory, "vanillasky/templates"]
1291
+ ]) {
1292
+ if (!await assertDirectoryOrMissing(path, displayPath)) {
1293
+ try {
1294
+ await mkdir2(path);
1295
+ createdDirectories.push(path);
1296
+ } catch (error) {
1297
+ if (error.code !== "EEXIST") throw error;
1298
+ await assertDirectoryOrMissing(path, displayPath);
1299
+ }
1300
+ }
1301
+ safeProjectPath(cwd, destination);
1302
+ }
1303
+ const existingDestination = await status(destination);
1304
+ if (existingDestination?.isSymbolicLink()) {
1305
+ await removeCreatedDirectories(createdDirectories);
1306
+ throw new Error(`Refusing to replace symbolic link: ${destinationPath}`);
1307
+ }
1308
+ if (existingDestination) {
1309
+ await removeCreatedDirectories(createdDirectories);
1310
+ throw new Error(`Template already exists: ${destinationPath}`);
1311
+ }
1312
+ const temporary = join6(templatesDirectory, `.${id}.${process.pid}.${temporaryFileSequence += 1}.tmp`);
1313
+ let temporaryExists = false;
1314
+ let createdIdentity;
1315
+ try {
1316
+ const handle = await open2(temporary, "wx", 420);
1317
+ temporaryExists = true;
1318
+ try {
1319
+ await handle.writeFile(sourceFor(id), "utf8");
1320
+ await handle.sync();
1321
+ } finally {
1322
+ await handle.close();
1323
+ }
1324
+ try {
1325
+ await link(temporary, destination);
1326
+ } catch (error) {
1327
+ if (error.code === "EEXIST") {
1328
+ throw new Error(`Template already exists: ${destinationPath}`);
1329
+ }
1330
+ throw error;
1331
+ }
1332
+ const created = await lstat(destination, { bigint: true });
1333
+ createdIdentity = { dev: created.dev, ino: created.ino };
1334
+ await unlink2(temporary);
1335
+ temporaryExists = false;
1336
+ const result = await (options.sync ?? syncTemplates)({ cwd });
1337
+ const templates = typeof result === "object" && result != null && "templates" in result && Array.isArray(result.templates) ? result.templates.length : 1;
1338
+ const warnings = typeof result === "object" && result != null && "warnings" in result && Array.isArray(result.warnings) ? result.warnings.filter((warning) => typeof warning === "string") : [];
1339
+ return { path: destinationPath, templates, ...warnings.length > 0 ? { warnings } : {} };
1340
+ } catch (error) {
1341
+ if (temporaryExists) {
1342
+ try {
1343
+ await unlink2(temporary);
1344
+ } catch (cleanupError) {
1345
+ if (cleanupError.code !== "ENOENT") throw cleanupError;
1346
+ }
1347
+ }
1348
+ if (createdIdentity) {
1349
+ const current = await status(destination);
1350
+ if (current && !current.isSymbolicLink()) {
1351
+ const identity = await lstat(destination, { bigint: true });
1352
+ if (identity.dev === createdIdentity.dev && identity.ino === createdIdentity.ino) await unlink2(destination);
1353
+ }
1354
+ }
1355
+ await removeCreatedDirectories(createdDirectories);
1356
+ throw error;
1357
+ }
1358
+ }
1359
+
1360
+ // src/cli/catalog.ts
1361
+ import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
1362
+ import { join as join7 } from "path";
1363
+ function item(metadata, origin, generated) {
1364
+ const useWhen = metadata.useWhen ?? metadata.description ?? "";
1365
+ const current = generated == null ? null : generated.browser && generated.server;
1366
+ return {
1367
+ id: metadata.id,
1368
+ title: metadata.label ?? metadata.id,
1369
+ origin,
1370
+ status: origin === "built-in" ? "available" : current ? "current" : "stale",
1371
+ useWhen,
1372
+ summary: metadata.description ?? "",
1373
+ planner: {
1374
+ useWhen,
1375
+ ...metadata.avoidWhen == null ? {} : { avoidWhen: metadata.avoidWhen }
1376
+ },
1377
+ schema: metadata.schema,
1378
+ schemaSummary: summarizeTemplateVariables(metadata.schema),
1379
+ ...metadata.family == null ? {} : { family: metadata.family },
1380
+ ...metadata.jobs == null ? {} : { jobs: metadata.jobs },
1381
+ ...metadata.register == null ? {} : { register: metadata.register },
1382
+ duration: {
1383
+ ...metadata.minDuration == null ? {} : { min: metadata.minDuration },
1384
+ ...metadata.preferredDuration == null ? {} : { preferred: metadata.preferredDuration }
1385
+ },
1386
+ generated: {
1387
+ browser: { path: "vanillasky/index.ts", current: generated?.browser ?? null },
1388
+ server: { path: "vanillasky/server.ts", current: generated?.server ?? null },
1389
+ current
1390
+ },
1391
+ wiring: { applicationImportsInspected: false, verified: false }
1392
+ };
1393
+ }
1394
+ function builtinTemplateCatalog() {
1395
+ return listBuiltinTemplateMetadata().map((metadata) => item(metadata, "built-in", null)).sort((left, right) => left.id.localeCompare(right.id));
1396
+ }
1397
+ function hasProjectTemplateSources(cwd) {
1398
+ const directory = join7(cwd, "vanillasky/templates");
1399
+ if (!existsSync2(directory)) return false;
1400
+ return readdirSync2(directory, { withFileTypes: true }).some((entry) => (entry.isFile() || entry.isSymbolicLink()) && /\.(?:ts|tsx)$/.test(entry.name));
1401
+ }
1402
+ async function effectiveTemplateCatalog(cwd) {
1403
+ const builtins = builtinTemplateCatalog();
1404
+ const discovered = await discoverProjectTemplates(cwd);
1405
+ if (discovered.length === 0) return builtins;
1406
+ const generated = await inspectGeneratedTemplateStatus(cwd, discovered);
1407
+ const byId = new Map(builtins.map((entry) => [entry.id, entry]));
1408
+ for (const template of discovered) {
1409
+ byId.set(template.metadata.id, item(template.metadata, "project", {
1410
+ browser: generated.browser.current,
1411
+ server: generated.server.current
1412
+ }));
1413
+ }
1414
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
1415
+ }
1416
+ function findBuiltinTemplate(id) {
1417
+ return builtinTemplateCatalog().find((entry) => entry.id === id);
1418
+ }
1419
+ async function findEffectiveTemplate(cwd, id) {
1420
+ return (await effectiveTemplateCatalog(cwd)).find((entry) => entry.id === id);
1421
+ }
1422
+
1423
+ // src/cli/index.ts
1424
+ function help() {
1425
+ return [
1426
+ "VanillaSky source-owned templates",
1427
+ "",
1428
+ "Usage:",
1429
+ " vanillasky list [--builtin] [--json]",
1430
+ " vanillasky describe <template> [--builtin] [--json]",
1431
+ " vanillasky add <template...> [--dry-run|--diff] [--overwrite]",
1432
+ " vanillasky add --all [--dry-run|--diff] [--overwrite]",
1433
+ " vanillasky sync [--check]",
1434
+ " vanillasky check",
1435
+ " vanillasky create <id>"
1436
+ ].join("\n");
1437
+ }
1438
+ function unknownOption(command, args, allowed) {
1439
+ const known = new Set(allowed);
1440
+ const unknown = args.find((arg) => arg.startsWith("-") && !known.has(arg));
1441
+ return unknown ? `Unknown ${command} option: ${unknown}` : void 0;
1442
+ }
1443
+ function compactDiff(change) {
1444
+ const before = change.before?.split("\n") ?? [];
1445
+ const after = change.after.split("\n");
1446
+ let prefix = 0;
1447
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix += 1;
1448
+ let suffix = 0;
1449
+ while (suffix < before.length - prefix && suffix < after.length - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix += 1;
1450
+ const removed = before.slice(prefix, before.length - suffix);
1451
+ const added = after.slice(prefix, after.length - suffix);
1452
+ return [
1453
+ `--- ${change.path}`,
1454
+ `+++ ${change.path}`,
1455
+ `@@ -${prefix + 1},${removed.length} +${prefix + 1},${added.length} @@`,
1456
+ ...removed.map((line) => `-${line}`),
1457
+ ...added.map((line) => `+${line}`)
1458
+ ].join("\n");
1459
+ }
1460
+ function sanitizeTerminalOutput(value) {
1461
+ return [...value].filter((character) => {
1462
+ const code = character.charCodeAt(0);
1463
+ return code > 159 || code > 31 && code < 127 || code === 9 || code === 10;
1464
+ }).join("");
1465
+ }
1466
+ function runVanillaSkyCli(argv, environment = {}) {
1467
+ const output = environment.write ?? console.log;
1468
+ const write = (line) => output(sanitizeTerminalOutput(line));
1469
+ const cwd = resolve7(environment.cwd ?? process.cwd());
1470
+ const [command, ...args] = argv;
1471
+ if (command === "create") {
1472
+ return (async () => {
1473
+ try {
1474
+ const unknown = unknownOption("create", args, []);
1475
+ if (unknown) throw new Error(unknown);
1476
+ if (args.length === 0) throw new Error("Choose one template id. Usage: vanillasky create <id>.");
1477
+ if (args.length > 1) throw new Error(`Unexpected create argument: ${args[1]}`);
1478
+ const id = args[0];
1479
+ const result = await createTemplate({ cwd, id });
1480
+ write(`Created template: ${result.path}`);
1481
+ write(`Synced ${result.templates} template${result.templates === 1 ? "" : "s"} to vanillasky/index.ts and vanillasky/server.ts.`);
1482
+ write(`Source: ${result.path}`);
1483
+ write("Validate templates: vanillasky check");
1484
+ for (const warning of result.warnings ?? []) write(`Warning: ${warning}`);
1485
+ return 0;
1486
+ } catch (error) {
1487
+ write(error instanceof Error ? error.message : String(error));
1488
+ return 1;
1489
+ }
1490
+ })();
1491
+ }
1492
+ if (command === "check") {
1493
+ return (async () => {
1494
+ try {
1495
+ const unknown = unknownOption("check", args, []);
1496
+ if (unknown) throw new Error(unknown);
1497
+ if (args.length > 0) throw new Error(`Unexpected check argument: ${args[0]}`);
1498
+ const result = await checkTemplates({ cwd });
1499
+ write(`Checked ${result.templates} template${result.templates === 1 ? "" : "s"}, ${result.examples} example${result.examples === 1 ? "" : "s"}, and ${result.renders} deterministic renders.`);
1500
+ write("Generated browser/server registries match. Application imports were not inspected.");
1501
+ return 0;
1502
+ } catch (error) {
1503
+ write(error instanceof Error ? error.message : String(error));
1504
+ return 1;
1505
+ }
1506
+ })();
1507
+ }
1508
+ if (command === "sync") {
1509
+ return (async () => {
1510
+ try {
1511
+ const unknown = unknownOption("sync", args, ["--check"]);
1512
+ if (unknown) throw new Error(unknown);
1513
+ const result = await syncTemplates({ cwd, check: args.includes("--check") });
1514
+ if (args.includes("--check")) {
1515
+ write("Template entrypoints are up to date.");
1516
+ } else {
1517
+ const count = result.templates.length;
1518
+ write(`Synced ${count} template${count === 1 ? "" : "s"} to vanillasky/index.ts and vanillasky/server.ts.`);
1519
+ }
1520
+ for (const warning of result.warnings) write(`Warning: ${warning}`);
1521
+ return 0;
1522
+ } catch (error) {
1523
+ write(error instanceof Error ? error.message : String(error));
1524
+ return 1;
1525
+ }
1526
+ })();
1527
+ }
1528
+ if (command === "list") {
1529
+ const unknown = unknownOption("list", args, ["--builtin", "--json"]);
1530
+ if (unknown) {
1531
+ write(unknown);
1532
+ return 1;
1533
+ }
1534
+ const positional = args.filter((arg) => !["--builtin", "--json"].includes(arg));
1535
+ if (positional.length > 0) {
1536
+ write(`Unexpected list argument: ${positional[0]}`);
1537
+ return 1;
1538
+ }
1539
+ const render = (items) => {
1540
+ if (args.includes("--json")) write(JSON.stringify(items.map((item2) => ({
1541
+ id: item2.id,
1542
+ title: item2.title,
1543
+ origin: item2.origin,
1544
+ status: item2.status,
1545
+ useWhen: item2.useWhen,
1546
+ summary: item2.summary
1547
+ })), null, 2));
1548
+ else {
1549
+ write("ID ORIGIN STATUS USE WHEN");
1550
+ for (const item2 of items) write(`${item2.id} ${item2.origin} ${item2.status} ${item2.useWhen}`);
1551
+ }
1552
+ return 0;
1553
+ };
1554
+ if (args.includes("--builtin") || !hasProjectTemplateSources(cwd)) return render(builtinTemplateCatalog());
1555
+ return effectiveTemplateCatalog(cwd).then(render).catch((error) => {
1556
+ write(error instanceof Error ? error.message : String(error));
1557
+ return 1;
1558
+ });
1559
+ }
1560
+ if (command === "add") {
1561
+ const allowed = ["--all", "--dry-run", "--diff", "--overwrite"];
1562
+ const unknown = unknownOption("add", args, allowed);
1563
+ if (unknown) {
1564
+ write(unknown);
1565
+ return 1;
1566
+ }
1567
+ const dryRun = args.includes("--dry-run");
1568
+ const showDiff = args.includes("--diff");
1569
+ if (dryRun && showDiff) {
1570
+ write("Choose either --dry-run or --diff.");
1571
+ return 1;
1572
+ }
1573
+ const addAll = args.includes("--all");
1574
+ const positional = args.filter((arg) => !allowed.includes(arg));
1575
+ if (addAll && positional.length > 0) {
1576
+ write("Use either named templates or --all.");
1577
+ return 1;
1578
+ }
1579
+ const names = addAll ? listRegistryTemplates().map(({ id }) => id) : positional;
1580
+ if (names.length === 0) {
1581
+ write("Choose at least one template. Run `vanillasky list` to see the catalog.");
1582
+ return 1;
1583
+ }
1584
+ return (async () => {
1585
+ try {
1586
+ const preview = dryRun || showDiff;
1587
+ const result = addRegistryTemplates({
1588
+ cwd,
1589
+ names,
1590
+ dryRun: preview,
1591
+ overwrite: args.includes("--overwrite")
1592
+ });
1593
+ const changes = preview ? [...result.changes, ...await previewTemplateSync({ cwd, templates: result.previewTemplates })] : result.changes;
1594
+ if (showDiff) {
1595
+ if (changes.length === 0) write("No changes.");
1596
+ else for (const change of changes) write(compactDiff(change));
1597
+ return 0;
1598
+ }
1599
+ if (dryRun) {
1600
+ if (result.added.length > 0) write(`Would add ${result.added.join(", ")}.`);
1601
+ if (result.updated.length > 0) write(`Would update ${result.updated.join(", ")}.`);
1602
+ if (changes.length === 0) write("No changes.");
1603
+ else for (const change of changes) write(`${change.action} ${change.path}`);
1604
+ return 0;
1605
+ }
1606
+ await syncTemplates({ cwd });
1607
+ if (result.added.length > 0) write(`Added ${result.added.join(", ")}.`);
1608
+ if (result.updated.length > 0) write(`Updated ${result.updated.join(", ")}.`);
1609
+ if (result.added.length === 0 && result.updated.length === 0) {
1610
+ write("Those templates are already installed.");
1611
+ }
1612
+ const templateSources = result.files.filter((file) => /^vanillasky\/templates\/.*\.tsx$/.test(file));
1613
+ if (templateSources.length > 0) {
1614
+ write(`${templateSources.length === 1 ? "Template source" : "Template sources"}: ${templateSources.join(", ")}`);
1615
+ }
1616
+ return 0;
1617
+ } catch (error) {
1618
+ write(error instanceof Error ? error.message : String(error));
1619
+ return 1;
1620
+ }
1621
+ })();
1622
+ }
1623
+ if (command === "describe") {
1624
+ const unknown = unknownOption("describe", args, ["--builtin", "--json"]);
1625
+ if (unknown) {
1626
+ write(unknown);
1627
+ return 1;
1628
+ }
1629
+ const names = args.filter((arg) => arg !== "--json" && arg !== "--builtin");
1630
+ if (names.length > 1) {
1631
+ write(`Unexpected describe argument: ${names[1]}`);
1632
+ return 1;
1633
+ }
1634
+ const name = names[0];
1635
+ const render = (item2) => {
1636
+ if (!item2) {
1637
+ write(`Unknown template: ${name ?? ""}. Run \`vanillasky list\` to see the catalog.`);
1638
+ return 1;
1639
+ }
1640
+ if (args.includes("--json")) {
1641
+ write(JSON.stringify(item2, null, 2));
1642
+ return 0;
1643
+ }
1644
+ write(`${item2.id} \u2014 ${item2.title}`);
1645
+ if (item2.summary) write(item2.summary);
1646
+ write(`Origin ${item2.origin}`);
1647
+ write(`Status ${item2.status}`);
1648
+ write(`Use when ${item2.planner.useWhen}`);
1649
+ if (item2.planner.avoidWhen) write(`Avoid when ${item2.planner.avoidWhen}`);
1650
+ if (item2.family) write(`Family ${item2.family}`);
1651
+ if (item2.jobs) write(`Jobs ${item2.jobs.join(", ")}`);
1652
+ if (item2.register) write(`Register ${item2.register}`);
1653
+ const duration = [
1654
+ item2.duration.min == null ? void 0 : `${item2.duration.min}s minimum`,
1655
+ item2.duration.preferred == null ? void 0 : `${item2.duration.preferred}s preferred`
1656
+ ].filter(Boolean).join(", ");
1657
+ if (duration) write(`Duration ${duration}`);
1658
+ write(`Generated browser ${item2.generated.browser.path} ${item2.generated.browser.current == null ? "not applicable" : item2.generated.browser.current ? "current" : "stale"}`);
1659
+ write(`Generated server ${item2.generated.server.path} ${item2.generated.server.current == null ? "not applicable" : item2.generated.server.current ? "current" : "stale"}`);
1660
+ write(`Generated files ${item2.generated.current == null ? "not applicable to packaged templates" : item2.generated.current ? "current" : "stale"}`);
1661
+ write("Application wiring not inspected or verified");
1662
+ for (const [fieldName, property] of Object.entries(item2.schema.properties)) {
1663
+ const required = item2.schema.required?.includes(fieldName) === true;
1664
+ const notation = templateVariableNotation(property, required).replace(/!$/, "");
1665
+ write(`${fieldName} ${notation} ${required ? "required" : "optional"} ${property.description ?? ""}`);
1666
+ }
1667
+ return 0;
1668
+ };
1669
+ if (!name) return render(void 0);
1670
+ if (args.includes("--builtin") || !hasProjectTemplateSources(cwd)) return render(findBuiltinTemplate(name));
1671
+ return findEffectiveTemplate(cwd, name).then(render).catch((error) => {
1672
+ write(error instanceof Error ? error.message : String(error));
1673
+ return 1;
1674
+ });
1675
+ }
1676
+ write(help());
1677
+ return command == null || command === "help" || command === "--help" || command === "-h" ? 0 : 1;
1678
+ }
1679
+
1680
+ // src/cli.ts
1681
+ process.exitCode = await runVanillaSkyCli(process.argv.slice(2));