@vanillaskyai/video 0.10.22 → 0.11.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 (128) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/PUBLIC-API.md +45 -438
  3. package/README.md +57 -96
  4. package/dist/builtin-metadata-OT6V7TB4.js +8 -0
  5. package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
  6. package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
  7. package/dist/chunk-4G4JBMCM.js +37 -0
  8. package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
  9. package/dist/chunk-666HGTVZ.js +41 -0
  10. package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
  11. package/dist/{chunk-5BSJLT6H.js → chunk-E7FHBLAC.js} +16 -29
  12. package/dist/chunk-HIQM4J3U.js +30 -0
  13. package/dist/chunk-ISWQ6T5X.js +36 -0
  14. package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
  15. package/dist/chunk-KMVRBUL5.js +50 -0
  16. package/dist/chunk-NM4CXXZY.js +25 -0
  17. package/dist/{chunk-44WND2VP.js → chunk-V2CP7PVY.js} +19 -63
  18. package/dist/{chunk-3O7OMMMF.js → chunk-WZESNEPT.js} +0 -1
  19. package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
  20. package/dist/cinema-media-YZ2UANJG.js +365 -0
  21. package/dist/cli.js +154 -1753
  22. package/dist/{compose-video-BH5K6XWT.js → compose-video-H2YUQDUY.js} +4 -4
  23. package/dist/{events-B4YCc4vc.d.ts → events-BZAfl0Dm.d.ts} +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +4 -6
  26. package/dist/preload-media-23JXUFCF.js +77 -0
  27. package/dist/react.d.ts +39 -15
  28. package/dist/react.js +1350 -739
  29. package/dist/scene-validation-SGLLLFY5.js +9 -0
  30. package/dist/server.d.ts +67 -117
  31. package/dist/server.js +262 -614
  32. package/dist/test.d.ts +2 -2
  33. package/dist/test.js +21 -21
  34. package/dist/{text-stream-LD364KBI.js → text-stream-SOVLYR2L.js} +2 -2
  35. package/dist/{types-BqB8zC9u.d.ts → types-CG-kPI81.d.ts} +3 -1
  36. package/dist/{types-DdZw4GRQ.d.ts → types-ht-Zw3Wv.d.ts} +1 -1
  37. package/docs/agent-integration.md +19 -64
  38. package/docs/architecture.md +82 -87
  39. package/docs/customization.md +49 -66
  40. package/docs/development.md +18 -17
  41. package/docs/errors.md +4 -4
  42. package/docs/getting-started.md +74 -93
  43. package/docs/media-and-audio.md +9 -7
  44. package/docs/performance.md +11 -6
  45. package/docs/persistence.md +3 -7
  46. package/docs/production.md +4 -15
  47. package/docs/prompt-and-input.md +1 -2
  48. package/docs/provider-integration.md +134 -181
  49. package/docs/reference/protocol.md +5 -14
  50. package/docs/reference/provider-adapters.md +21 -13
  51. package/docs/security.md +1 -14
  52. package/docs/testing.md +56 -123
  53. package/package.json +5 -27
  54. package/starters/video-chat/.env.example +12 -4
  55. package/starters/video-chat/.env.native.example +13 -0
  56. package/starters/video-chat/README.md +72 -11
  57. package/starters/video-chat/package.json +1 -1
  58. package/starters/video-chat/providers/text-native.ts +94 -0
  59. package/starters/video-chat/providers/text.ts +19 -0
  60. package/starters/video-chat/providers/transcription.ts +32 -0
  61. package/starters/video-chat/providers/video-custom.ts +45 -0
  62. package/starters/video-chat/providers/video-delivery.ts +57 -0
  63. package/starters/video-chat/providers/video-google.ts +48 -0
  64. package/starters/video-chat/providers/video-job.ts +103 -0
  65. package/starters/video-chat/providers/video-runway.ts +45 -0
  66. package/starters/video-chat/providers/video.ts +39 -63
  67. package/starters/video-chat/server.ts +3 -29
  68. package/starters/video-chat/vite.config.ts +2 -8
  69. package/styles/video-chat.css +8 -105
  70. package/dist/builtin-server-W4PLJUZ5.js +0 -8
  71. package/dist/catalog-types-WTbLP6Jh.d.ts +0 -78
  72. package/dist/check-runtime.d.ts +0 -15
  73. package/dist/check-runtime.js +0 -96
  74. package/dist/chunk-2E6T633S.js +0 -27
  75. package/dist/chunk-4YM2M62S.js +0 -13
  76. package/dist/chunk-4ZJLPHBV.js +0 -684
  77. package/dist/chunk-5JBMYQP6.js +0 -156
  78. package/dist/chunk-73NTSFFI.js +0 -81
  79. package/dist/chunk-EGVQODKU.js +0 -83
  80. package/dist/chunk-HFVNAPHZ.js +0 -35
  81. package/dist/chunk-IIN5M5HW.js +0 -697
  82. package/dist/chunk-IQMYK5DX.js +0 -133
  83. package/dist/chunk-IR44XKBI.js +0 -46
  84. package/dist/chunk-JKVOBTRO.js +0 -145
  85. package/dist/chunk-LVM5Q2DL.js +0 -68
  86. package/dist/chunk-M4QTEJTK.js +0 -709
  87. package/dist/chunk-QSBDB4J2.js +0 -16
  88. package/dist/chunk-R3XAOMKP.js +0 -29
  89. package/dist/chunk-SPVTJH3F.js +0 -24
  90. package/dist/chunk-YAHT3LST.js +0 -663
  91. package/dist/chunk-ZD2VTUYR.js +0 -28
  92. package/dist/cinema-media-HYCDG65Z.js +0 -11
  93. package/dist/comparison-XXAP4S4J.js +0 -36
  94. package/dist/editorial-timeline-VUQC6KPA.js +0 -42
  95. package/dist/key-figure-4TJK7HLT.js +0 -29
  96. package/dist/kit-DrRpdn0p.d.ts +0 -79
  97. package/dist/mobile-message-FIHAO6XC.js +0 -49
  98. package/dist/preload-media-LJXWKTWG.js +0 -57
  99. package/dist/quote-GRPIYKQJ.js +0 -32
  100. package/dist/system-prompt-AG26KJAA.js +0 -12
  101. package/dist/template-catalog.d.ts +0 -400
  102. package/dist/template-catalog.js +0 -6
  103. package/dist/templates.d.ts +0 -25
  104. package/dist/templates.js +0 -102
  105. package/dist/validate-4OUD2CLX.js +0 -10
  106. package/docs/concepts.md +0 -107
  107. package/docs/custom-templates.md +0 -346
  108. package/docs/immersive-interface.md +0 -86
  109. package/docs/maintainers/cinematic-migration.md +0 -38
  110. package/docs/motion-and-effects.md +0 -109
  111. package/docs/reference/design-system.html +0 -125
  112. package/docs/responsive-orientation.md +0 -37
  113. package/docs/streaming-protocol.md +0 -15
  114. package/examples/custom-template/README.md +0 -19
  115. package/examples/custom-template/minimal-text.tsx +0 -82
  116. package/examples/custom-template/structured-data.tsx +0 -104
  117. package/registry/items/backgrounds.json +0 -70
  118. package/registry/items/chapterTitle.json +0 -80
  119. package/registry/items/cinemaMedia.json +0 -136
  120. package/registry/items/comparison.json +0 -168
  121. package/registry/items/editorialTimeline.json +0 -172
  122. package/registry/items/keyFigure.json +0 -162
  123. package/registry/items/mobileMessage.json +0 -153
  124. package/registry/items/motion.json +0 -45
  125. package/registry/items/quote.json +0 -161
  126. package/registry/items/template-context.json +0 -31
  127. package/registry/items/theme.json +0 -47
  128. package/registry/items/typography.json +0 -47
package/dist/cli.js CHANGED
@@ -1,46 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-JKVOBTRO.js";
3
- import "./chunk-LVM5Q2DL.js";
4
- import {
5
- isTemplatePropertyFormatSupported,
6
- validateTemplateSchemaValue
7
- } from "./chunk-EGVQODKU.js";
8
- import {
9
- getBuiltinTemplateMetadata,
10
- listBuiltinTemplateMetadata
11
- } from "./chunk-IIN5M5HW.js";
12
- import "./chunk-3O7OMMMF.js";
13
- import {
14
- assertTemplateTransitionMetadata
15
- } from "./chunk-2E6T633S.js";
16
- import {
17
- BUILTIN_TEMPLATE_MANIFEST
18
- } from "./chunk-4ZJLPHBV.js";
19
- import {
20
- externalVideoBackdropCapabilitySource
21
- } from "./chunk-R3XAOMKP.js";
22
- import {
23
- getTemplateDefaults,
24
- isMeaningfullyPresentTemplateValue,
25
- summarizeTemplateVariables,
26
- templateVariableNotation
27
- } from "./chunk-73NTSFFI.js";
28
2
 
29
3
  // src/cli/index.ts
30
- import { resolve as resolve9 } from "path";
4
+ import { resolve as resolve4 } from "path";
31
5
 
32
- // src/cli/registry.ts
33
- import { randomUUID } from "crypto";
6
+ // src/cli/init.ts
7
+ import { spawn } from "child_process";
34
8
  import {
35
9
  existsSync,
36
10
  mkdirSync,
37
11
  readFileSync,
38
- readdirSync,
39
12
  renameSync,
40
13
  unlinkSync,
41
14
  writeFileSync
42
15
  } from "fs";
43
- import { basename, dirname, join as join2, resolve as resolve2 } from "path";
16
+ import { basename, dirname, isAbsolute, join as join2, resolve as resolve2 } from "path";
44
17
  import { fileURLToPath } from "url";
45
18
 
46
19
  // src/cli/safe-path.ts
@@ -84,1379 +57,6 @@ function safeProjectPath(cwd, target) {
84
57
  return destination;
85
58
  }
86
59
 
87
- // src/cli/registry.ts
88
- function defaultRegistryDir() {
89
- const sourceCandidate = fileURLToPath(new URL("../../registry/items/", import.meta.url));
90
- const distributionCandidate = fileURLToPath(new URL("../registry/items/", import.meta.url));
91
- return existsSync(sourceCandidate) ? sourceCandidate : distributionCandidate;
92
- }
93
- function readItem(registryDir, name) {
94
- if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) throw new Error(`Invalid registry item: ${name}`);
95
- const path = join2(registryDir, `${name}.json`);
96
- if (!existsSync(path)) throw new Error(`Unknown template or registry item: ${name}`);
97
- return JSON.parse(readFileSync(path, "utf8"));
98
- }
99
- function readTemplateItem(registryDir, name) {
100
- let item2;
101
- try {
102
- item2 = readItem(registryDir, name);
103
- } catch (error) {
104
- if (error instanceof Error && error.message.startsWith("Unknown template or registry item:")) {
105
- throw new Error(`Unknown template: ${name}`);
106
- }
107
- if (error instanceof Error && error.message.startsWith("Invalid registry item:")) {
108
- throw new Error(`Invalid template: ${name}`);
109
- }
110
- throw error;
111
- }
112
- if (item2.meta?.vanillasky?.layer !== "template") throw new Error(`${item2.name} is not a template`);
113
- return item2;
114
- }
115
- function dependencyName(value) {
116
- return value.startsWith("@vanillasky/") ? value.slice("@vanillasky/".length) : value;
117
- }
118
- function safeDestination(cwd, target) {
119
- return safeProjectPath(cwd, target);
120
- }
121
- function planCustomerFile(planned, target, content) {
122
- const existing = planned.get(target);
123
- if (existing != null && existing !== content) {
124
- throw new Error(`Registry items provide conflicting content for ${target}`);
125
- }
126
- planned.set(target, content);
127
- }
128
- function registryTarget(file) {
129
- if (file.target) return file.target;
130
- return file.path.replace(/^src\/lib\//, "vanillasky/");
131
- }
132
- function primaryTemplateFile(item2) {
133
- return item2.files?.find((file) => /scene-templates\/.*\.tsx$/.test(file.path));
134
- }
135
- function templateDefinition(item2) {
136
- const templateFile = primaryTemplateFile(item2);
137
- if (!templateFile) throw new Error(`Template ${item2.name} has no render component`);
138
- const metadata = item2.meta?.vanillasky ?? {};
139
- const canonical = getBuiltinTemplateMetadata(item2.name);
140
- const schema = canonical?.schema ?? metadata.schema;
141
- if (!schema) throw new Error(`Template ${item2.name} has no JSON Schema contract`);
142
- return canonical ?? {
143
- id: item2.name,
144
- label: item2.title ?? item2.name,
145
- description: item2.description ?? "",
146
- useWhen: String(metadata.useWhen ?? item2.description ?? ""),
147
- usesGlobalTextEffect: false,
148
- usesGlobalTransition: false,
149
- usesGlobalBackgroundEffect: false,
150
- schema,
151
- ...metadata.jobs == null ? {} : { jobs: metadata.jobs },
152
- ...metadata.register == null ? {} : { register: metadata.register },
153
- ...metadata.textCanvas == null ? {} : { textCanvas: metadata.textCanvas },
154
- ...metadata.minDuration == null ? {} : { minDuration: metadata.minDuration },
155
- ...metadata.preferredDuration == null ? {} : { preferredDuration: metadata.preferredDuration }
156
- };
157
- }
158
- function materializedTemplateMetadata(item2) {
159
- const definition = templateDefinition(item2);
160
- const {
161
- schema,
162
- label = definition.id,
163
- description = "",
164
- usesGlobalTextEffect = false,
165
- usesGlobalTransition = false,
166
- usesGlobalBackgroundEffect = false,
167
- ...definitionWithoutDefaults
168
- } = definition;
169
- return {
170
- label,
171
- description,
172
- usesGlobalTextEffect,
173
- usesGlobalTransition,
174
- usesGlobalBackgroundEffect,
175
- ...definitionWithoutDefaults,
176
- schema: {
177
- ...schema,
178
- additionalProperties: schema.additionalProperties ?? false
179
- }
180
- };
181
- }
182
- function templateSource(item2) {
183
- const templateFile = primaryTemplateFile(item2);
184
- if (!templateFile) throw new Error(`Template ${item2.name} has no render component`);
185
- const component = templateFile.content.match(/export const (\w+Template)\b/)?.[1];
186
- if (!component) throw new Error(`Cannot identify the component exported by ${item2.name}`);
187
- const definition = templateDefinition(item2);
188
- const properties = definition.schema.properties;
189
- const mediaTypes = properties.mediaType?.enum;
190
- const supportsExternalVideoBackdrop = !!properties.mediaUrl && (mediaTypes == null || mediaTypes.includes("video") || mediaTypes.includes("auto"));
191
- const definitionLines = JSON.stringify(definition, null, 2).split("\n").slice(1, -1);
192
- definitionLines[definitionLines.length - 1] += ",";
193
- let componentSource = templateFile.content.replace(/(from\s+["'])\.\//g, "$1../scene-templates/").replace(/(import\(\s*["'])\.\//g, "$1../scene-templates/").replace(
194
- new RegExp(`export const ${component}: React\\.FC<SceneTemplateProps> = \\(\\{([\\s\\S]*?)\\n\\}\\) => \\{`),
195
- `export const ${component} = ({$1
196
- }: SceneTemplateProps) => {`
197
- );
198
- if (!componentSource.includes("React.")) {
199
- componentSource = componentSource.replace(/^import (?:\* as )?React from ["']react["'];\n/m, "");
200
- }
201
- return [
202
- `import { defineTemplate } from "@vanillaskyai/video/templates";`,
203
- "",
204
- componentSource.trim(),
205
- "",
206
- `export const ${item2.name}Template = defineTemplate({`,
207
- ...definitionLines,
208
- ` component: ${component},`,
209
- "});",
210
- ...supportsExternalVideoBackdrop ? [
211
- `Object.defineProperty(${component}, ${externalVideoBackdropCapabilitySource()}, { value: true });`
212
- ] : [],
213
- ""
214
- ].join("\n");
215
- }
216
- function listRegistryTemplates(registryDir = defaultRegistryDir()) {
217
- 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) => {
218
- const canonical = getBuiltinTemplateMetadata(item2.name);
219
- return {
220
- id: item2.name,
221
- title: item2.title ?? item2.name,
222
- description: canonical?.useWhen ?? item2.description ?? ""
223
- };
224
- }).sort((left, right) => left.id.localeCompare(right.id));
225
- }
226
- function listInstalledTemplates(cwd) {
227
- const templatesDirectory = join2(resolve2(cwd), "vanillasky/templates");
228
- if (!existsSync(templatesDirectory)) return [];
229
- 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));
230
- }
231
- function plannedChanges(cwd, planned) {
232
- const changes = [];
233
- for (const [path, after] of planned) {
234
- const destination = safeDestination(cwd, path);
235
- if (!existsSync(destination)) {
236
- changes.push({ path, action: "create", after });
237
- continue;
238
- }
239
- const before = readFileSync(destination, "utf8");
240
- if (before !== after) changes.push({ path, action: "update", before, after });
241
- }
242
- return changes;
243
- }
244
- function applyChanges(cwd, changes) {
245
- for (const change of changes) {
246
- const destination = safeDestination(cwd, change.path);
247
- mkdirSync(dirname(destination), { recursive: true });
248
- safeProjectPath(cwd, destination);
249
- const temporary = join2(
250
- dirname(destination),
251
- `.${basename(destination)}.vanillasky-tmp-${randomUUID()}`
252
- );
253
- try {
254
- writeFileSync(temporary, change.after, { encoding: "utf8", flag: "wx" });
255
- safeProjectPath(cwd, destination);
256
- renameSync(temporary, destination);
257
- } finally {
258
- if (existsSync(temporary)) unlinkSync(temporary);
259
- }
260
- }
261
- }
262
- function addRegistryTemplates(options) {
263
- const cwd = resolve2(options.cwd);
264
- const registryDir = options.registryDir ?? defaultRegistryDir();
265
- const requested = [...new Set(options.names)];
266
- const requestedItems = requested.map((name) => readTemplateItem(registryDir, name));
267
- const installed = new Set(listInstalledTemplates(cwd));
268
- const planned = /* @__PURE__ */ new Map();
269
- const visited = /* @__PURE__ */ new Set();
270
- const installItem = (name) => {
271
- if (visited.has(name)) return;
272
- visited.add(name);
273
- const item2 = readItem(registryDir, name);
274
- const primary = item2.meta?.vanillasky?.layer === "template" ? primaryTemplateFile(item2) : void 0;
275
- for (const dependency of item2.registryDependencies ?? []) {
276
- installItem(dependencyName(dependency));
277
- }
278
- for (const file of item2.files ?? []) {
279
- if (file === primary) continue;
280
- const target = registryTarget(file);
281
- planCustomerFile(planned, target, file.content);
282
- }
283
- };
284
- for (const item2 of requestedItems) {
285
- installItem(item2.name);
286
- planCustomerFile(
287
- planned,
288
- `vanillasky/templates/${item2.name}.tsx`,
289
- templateSource(item2)
290
- );
291
- }
292
- const changes = plannedChanges(cwd, planned);
293
- const updates = changes.filter((change) => change.action === "update");
294
- if (!options.dryRun && !options.overwrite && updates.length > 0) {
295
- throw new Error(`Refusing to overwrite customer-owned file: ${updates[0]?.path}`);
296
- }
297
- if (!options.dryRun) applyChanges(cwd, changes);
298
- const templateChanges = new Set(changes.filter((change) => /^vanillasky\/templates\/.*\.tsx$/.test(change.path)).map((change) => change.path.slice("vanillasky/templates/".length, -".tsx".length)));
299
- const added = requested.filter((name) => !installed.has(name) && templateChanges.has(name));
300
- const updated = requested.filter((name) => installed.has(name) && templateChanges.has(name));
301
- return {
302
- added,
303
- updated,
304
- files: changes.map(({ path }) => path),
305
- changes,
306
- previewTemplates: requestedItems.map((item2) => ({
307
- sourcePath: `vanillasky/templates/${item2.name}.tsx`,
308
- exportName: `${item2.name}Template`,
309
- importPath: `./templates/${item2.name}`,
310
- metadata: materializedTemplateMetadata(item2)
311
- }))
312
- };
313
- }
314
-
315
- // src/cli/sync.ts
316
- import { randomUUID as randomUUID2 } from "crypto";
317
- import { lstatSync as lstatSync2 } from "fs";
318
- import { mkdir, open, readFile, rename, unlink } from "fs/promises";
319
- import { basename as basename3, dirname as dirname2, join as join4, relative as relative3, resolve as resolve4 } from "path";
320
-
321
- // src/cli/project-templates.ts
322
- import { realpathSync as realpathSync2 } from "fs";
323
- import { readdir } from "fs/promises";
324
- import { createRequire } from "module";
325
- import { basename as basename2, join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
326
- import { pathToFileURL } from "url";
327
-
328
- // src/cli/trusted-source-process.ts
329
- import { spawn } from "child_process";
330
- var DEFAULT_TRUSTED_SOURCE_TIMEOUT_MS = 1e4;
331
- var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
332
- var TrustedSourceProcessError = class extends Error {
333
- kind;
334
- constructor(kind, message) {
335
- super(message);
336
- this.name = "TrustedSourceProcessError";
337
- this.kind = kind;
338
- }
339
- };
340
- function minimalEnvironment() {
341
- const names = ["PATH", "SYSTEMROOT", "WINDIR", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL"];
342
- return Object.fromEntries(names.flatMap((name) => process.env[name] == null ? [] : [[name, process.env[name]]]));
343
- }
344
- function delay(milliseconds) {
345
- return new Promise((resolve10) => setTimeout(resolve10, milliseconds));
346
- }
347
- function signalProcessGroup(pid, signal) {
348
- try {
349
- process.kill(-pid, signal);
350
- return true;
351
- } catch (error) {
352
- const code = error.code;
353
- if (code === "ESRCH" || code === "EPERM") return false;
354
- throw error;
355
- }
356
- }
357
- async function stopProcessGroup(pid) {
358
- if (!signalProcessGroup(pid, "SIGTERM")) return;
359
- await delay(100);
360
- if (signalProcessGroup(pid, 0)) {
361
- signalProcessGroup(pid, "SIGKILL");
362
- await delay(25);
363
- }
364
- }
365
- async function runTrustedSourceProcess(args, options) {
366
- if (process.platform === "win32") {
367
- throw new Error(
368
- "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."
369
- );
370
- }
371
- const timeoutMs = options.timeoutMs ?? DEFAULT_TRUSTED_SOURCE_TIMEOUT_MS;
372
- const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
373
- const operation = options.operation ?? "loading";
374
- let cleanup;
375
- let processId;
376
- const result = new Promise((resolve10, reject) => {
377
- const child = spawn(process.execPath, [...args], {
378
- cwd: options.cwd,
379
- detached: true,
380
- env: minimalEnvironment(),
381
- stdio: ["ignore", "pipe", "pipe", "pipe"]
382
- });
383
- processId = child.pid;
384
- const protocol = [];
385
- const diagnostics = [];
386
- let outputBytes = 0;
387
- let failure;
388
- let settled = false;
389
- const fail = (error) => {
390
- if (failure) return;
391
- failure = error;
392
- if (child.pid != null) cleanup = stopProcessGroup(child.pid);
393
- };
394
- const collect = (target) => (chunk) => {
395
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
396
- outputBytes += buffer.byteLength;
397
- if (outputBytes > maxOutputBytes) {
398
- fail(new TrustedSourceProcessError("output", `${operation} output exceeded ${maxOutputBytes} bytes`));
399
- return;
400
- }
401
- target.push(buffer);
402
- };
403
- child.stdout?.on("data", collect(diagnostics));
404
- child.stderr?.on("data", collect(diagnostics));
405
- const protocolStream = child.stdio[3];
406
- if (protocolStream && "on" in protocolStream) protocolStream.on("data", collect(protocol));
407
- const timer = setTimeout(() => {
408
- fail(new TrustedSourceProcessError("timeout", `${operation} timed out after ${timeoutMs}ms`));
409
- }, timeoutMs);
410
- child.on("error", (error) => {
411
- clearTimeout(timer);
412
- if (settled) return;
413
- settled = true;
414
- reject(error);
415
- });
416
- child.on("close", (code, signal) => {
417
- clearTimeout(timer);
418
- if (settled) return;
419
- settled = true;
420
- if (failure) {
421
- reject(failure);
422
- return;
423
- }
424
- if (code !== 0) {
425
- const detail = Buffer.concat(diagnostics).toString("utf8").trim();
426
- reject(new TrustedSourceProcessError(
427
- "exit",
428
- detail || `template loader exited with ${code ?? signal ?? "an error"}`
429
- ));
430
- return;
431
- }
432
- resolve10(Buffer.concat(protocol).toString("utf8"));
433
- });
434
- });
435
- try {
436
- return await result;
437
- } finally {
438
- if (processId != null) await (cleanup ?? stopProcessGroup(processId));
439
- }
440
- }
441
-
442
- // src/cli/project-templates.ts
443
- var nodeModulesMarker = `${sep2}node_modules${sep2}`;
444
- var MAX_TEMPLATE_SOURCES = 128;
445
- var LOADER_CONCURRENCY = 4;
446
- var TEMPLATE_LOADER = String.raw`
447
- const sourceUrl = process.argv[1];
448
- const { writeSync } = await import("node:fs");
449
- const writeProtocol = (value) => writeSync(3, value);
450
- for (const method of ["log", "info", "warn", "error", "debug", "dir", "trace"]) console[method] = () => undefined;
451
- const loaded = await import(sourceUrl + "?vanillasky=loader");
452
- const isTemplate = (value) =>
453
- typeof value === "object" && value != null &&
454
- typeof value.id === "string" && typeof value.component === "function" &&
455
- typeof value.schema === "object" && value.schema != null &&
456
- value.schema.type === "object" && typeof value.schema.properties === "object";
457
- const entries = Object.entries(loaded);
458
- if (loaded.default && typeof loaded.default === "object" && !isTemplate(loaded.default)) {
459
- for (const entry of Object.entries(loaded.default)) {
460
- if (!entries.some(([name]) => name === entry[0])) entries.push(entry);
461
- }
462
- }
463
- const templates = entries.filter(([, value]) => isTemplate(value));
464
- if (templates.length !== 1) {
465
- throw new Error("must export exactly one template created with defineTemplate (found " + templates.length + ")");
466
- }
467
- const [exportName, template] = templates[0];
468
- const { component, examples, ...rawMetadata } = template;
469
- const metadata = Object.fromEntries(Object.entries(rawMetadata).filter(([, value]) => value !== undefined));
470
- const seen = new Set();
471
- function assertJson(value, path) {
472
- if (value === null || typeof value === "string" || typeof value === "boolean") return;
473
- if (typeof value === "number") {
474
- if (Number.isFinite(value)) return;
475
- throw new Error(path + " must contain only finite JSON numbers");
476
- }
477
- if (typeof value !== "object") throw new Error(path + " contains a non-serializable " + typeof value);
478
- if (seen.has(value)) throw new Error(path + " contains a circular value");
479
- seen.add(value);
480
- if (Array.isArray(value)) value.forEach((entry, index) => assertJson(entry, path + "[" + index + "]"));
481
- else {
482
- if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
483
- throw new Error(path + " contains a non-serializable object");
484
- }
485
- for (const [key, entry] of Object.entries(value)) assertJson(entry, path + "." + key);
486
- }
487
- seen.delete(value);
488
- }
489
- assertJson(metadata, "template metadata");
490
- if (examples !== undefined) assertJson(examples, "template examples");
491
- writeProtocol(JSON.stringify({ exportName, metadata, examples }));
492
- `;
493
- async function sourceFiles(templatesDirectory) {
494
- let entries;
495
- try {
496
- entries = await readdir(templatesDirectory, { withFileTypes: true });
497
- } catch (error) {
498
- if (error.code === "ENOENT") return [];
499
- throw error;
500
- }
501
- const linkedSource = entries.find((entry) => entry.isSymbolicLink() && /\.(?:ts|tsx)$/.test(entry.name));
502
- if (linkedSource) throw new Error(`Template source cannot be a symbolic link: ${linkedSource.name}`);
503
- const files = entries.filter((entry) => entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name)).map((entry) => join3(templatesDirectory, entry.name)).sort((left, right) => left.localeCompare(right));
504
- if (files.length > MAX_TEMPLATE_SOURCES) {
505
- throw new Error(`A project may contain at most ${MAX_TEMPLATE_SOURCES} template source files (found ${files.length}).`);
506
- }
507
- return files;
508
- }
509
- function resolveProjectTsxRuntime(resolveModule = (specifier) => createRequire(import.meta.url).resolve(specifier)) {
510
- let resolvedLoader;
511
- try {
512
- resolvedLoader = resolveModule("tsx");
513
- } catch {
514
- throw new Error(
515
- "Source-owned template commands require the optional TSX compiler. Install it in your project with: npm install --save-dev tsx"
516
- );
517
- }
518
- const nodeModulesIndex = resolvedLoader.lastIndexOf(nodeModulesMarker);
519
- const dependencyRoot = nodeModulesIndex < 0 ? resolve3(resolvedLoader, "..") : resolvedLoader.slice(0, nodeModulesIndex + nodeModulesMarker.length - 1);
520
- return { dependencyRoot, loader: pathToFileURL(resolvedLoader).href };
521
- }
522
- function sanitizeTrustedSourceDiagnostic(value, cwd, dependencyRoot) {
523
- const roots = [...new Set([resolve3(cwd), realpathSync2(cwd), dependencyRoot].filter((root) => Boolean(root)))].sort((left, right) => right.length - left.length);
524
- let safe = value;
525
- for (const root of roots) {
526
- const replacement = root === dependencyRoot ? "<dependency>" : ".";
527
- safe = safe.split(root).join(replacement).split(pathToFileURL(root).href).join(replacement);
528
- }
529
- return safe;
530
- }
531
- async function mapWithConcurrency(values, concurrency, transform) {
532
- if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("Loader concurrency must be a positive integer");
533
- const results = new Array(values.length);
534
- let next = 0;
535
- const worker = async () => {
536
- while (next < values.length) {
537
- const index = next;
538
- next += 1;
539
- results[index] = await transform(values[index], index);
540
- }
541
- };
542
- await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker));
543
- return results;
544
- }
545
- async function loadTemplate(filePath, cwd, options, runtime) {
546
- const displayPath = relative2(cwd, filePath);
547
- let serialized;
548
- try {
549
- const sourceUrl = pathToFileURL(filePath).href;
550
- const stdout = await runTrustedSourceProcess([
551
- "--import",
552
- runtime.loader,
553
- "--input-type=module",
554
- "--eval",
555
- TEMPLATE_LOADER,
556
- sourceUrl
557
- ], { cwd, timeoutMs: options.timeoutMs, maxOutputBytes: options.maxOutputBytes });
558
- serialized = JSON.parse(stdout);
559
- } catch (error) {
560
- const detail = error instanceof TrustedSourceProcessError || error instanceof Error ? error.message : String(error);
561
- throw new Error(`Could not load ${displayPath}: ${sanitizeTrustedSourceDiagnostic(detail, cwd, runtime.dependencyRoot)}`);
562
- }
563
- return {
564
- exportName: serialized.exportName,
565
- filePath,
566
- importPath: `./templates/${basename2(filePath).replace(/\.(?:ts|tsx)$/, "")}`,
567
- metadata: serialized.metadata,
568
- ...Object.hasOwn(serialized, "examples") ? { examples: serialized.examples } : {}
569
- };
570
- }
571
- async function discoverProjectTemplates(cwd, options = {}) {
572
- const root = resolve3(cwd);
573
- const templatesDirectory = safeProjectPath(root, "vanillasky/templates");
574
- const excluded = new Set((options.excludeSourcePaths ?? []).map((path) => resolve3(path)));
575
- const files = (await sourceFiles(templatesDirectory)).filter((file) => !excluded.has(resolve3(file)));
576
- for (const file of files) safeProjectPath(root, file);
577
- if (files.length === 0) return [];
578
- const runtime = resolveProjectTsxRuntime();
579
- const discovered = await mapWithConcurrency(
580
- files,
581
- LOADER_CONCURRENCY,
582
- (file) => loadTemplate(file, root, options, runtime)
583
- );
584
- const byId = /* @__PURE__ */ new Map();
585
- for (const template of discovered) {
586
- const duplicate = byId.get(template.metadata.id);
587
- if (duplicate) {
588
- throw new Error(
589
- `Duplicate template id ${JSON.stringify(template.metadata.id)} in ${relative2(cwd, duplicate.filePath)} and ${relative2(cwd, template.filePath)}`
590
- );
591
- }
592
- byId.set(template.metadata.id, template);
593
- }
594
- return discovered;
595
- }
596
-
597
- // src/cli/sync.ts
598
- var GENERATED_NOTICE = "// Generated by `vanillasky templates sync`. Do not edit.\n";
599
- function browserSource(templates) {
600
- 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)};`);
601
- const names = templates.map((_, index) => `template${index}`);
602
- return [
603
- GENERATED_NOTICE.trimEnd(),
604
- 'import { createTemplateRegistry } from "@vanillaskyai/video/templates";',
605
- ...imports,
606
- "",
607
- `export const definitions = [${names.join(", ")}];`,
608
- "export const templates = createTemplateRegistry({ definitions });",
609
- ""
610
- ].join("\n");
611
- }
612
- function serverSource(templates) {
613
- const metadata = JSON.stringify(templates.map(({ metadata: value }) => value), null, 2);
614
- return [
615
- GENERATED_NOTICE.trimEnd(),
616
- 'import { createServerTemplateRegistry } from "@vanillaskyai/video/server";',
617
- 'import type { ServerTemplateMetadata } from "@vanillaskyai/video/server";',
618
- "",
619
- `const templateMetadata: ServerTemplateMetadata[] = ${metadata};`,
620
- "export const templates = createServerTemplateRegistry({ templates: templateMetadata });",
621
- ""
622
- ].join("\n");
623
- }
624
- async function currentSource(path) {
625
- try {
626
- return await readFile(path, "utf8");
627
- } catch (error) {
628
- if (error.code === "ENOENT") return void 0;
629
- throw error;
630
- }
631
- }
632
- function generatedOutputs(cwd, discovered) {
633
- const vanillaskyDirectory = join4(cwd, "vanillasky");
634
- return [
635
- { path: join4(vanillaskyDirectory, "index.ts"), source: browserSource(discovered) },
636
- { path: join4(vanillaskyDirectory, "server.ts"), source: serverSource(discovered) }
637
- ];
638
- }
639
- async function previewTemplateSync(options) {
640
- const cwd = resolve4(options.cwd);
641
- const replacements = new Set(options.templates.map(({ sourcePath }) => safeProjectPath(cwd, sourcePath)));
642
- const installed = await discoverProjectTemplates(cwd, { excludeSourcePaths: [...replacements] });
643
- const proposed = options.templates.map((template) => ({
644
- ...template,
645
- filePath: safeProjectPath(cwd, template.sourcePath)
646
- }));
647
- const templates = [...installed, ...proposed].sort((left, right) => left.filePath.localeCompare(right.filePath));
648
- const byId = /* @__PURE__ */ new Map();
649
- for (const template of templates) {
650
- const duplicate = byId.get(template.metadata.id);
651
- if (duplicate) {
652
- throw new Error(
653
- `Duplicate template id ${JSON.stringify(template.metadata.id)} in ${relative3(cwd, duplicate.filePath)} and ${relative3(cwd, template.filePath)}`
654
- );
655
- }
656
- byId.set(template.metadata.id, template);
657
- }
658
- const changes = [];
659
- for (const output of generatedOutputs(cwd, templates)) {
660
- safeProjectPath(cwd, output.path);
661
- const before = await currentSource(output.path);
662
- if (before === output.source) continue;
663
- changes.push({
664
- path: relative3(cwd, output.path),
665
- action: before == null ? "create" : "update",
666
- ...before == null ? {} : { before },
667
- after: output.source
668
- });
669
- }
670
- return changes;
671
- }
672
- async function inspectGeneratedTemplateStatus(cwd, discovered) {
673
- const root = resolve4(cwd);
674
- const stale = [];
675
- for (const output of generatedOutputs(root, discovered)) {
676
- safeProjectPath(root, output.path);
677
- if (await currentSource(output.path) !== output.source) stale.push(relative3(root, output.path));
678
- }
679
- return {
680
- browser: { path: "vanillasky/index.ts", current: !stale.includes("vanillasky/index.ts") },
681
- server: { path: "vanillasky/server.ts", current: !stale.includes("vanillasky/server.ts") },
682
- current: stale.length === 0,
683
- stale
684
- };
685
- }
686
- async function unlinkIfPresent(path) {
687
- try {
688
- await unlink(path);
689
- } catch (error) {
690
- if (error.code !== "ENOENT") throw error;
691
- }
692
- }
693
- async function writeStagedFile(path, source) {
694
- const handle = await open(path, "wx", 420);
695
- try {
696
- await handle.writeFile(source, "utf8");
697
- await handle.sync();
698
- } finally {
699
- await handle.close();
700
- }
701
- }
702
- async function writeOutputsTransactionally(cwd, outputs, renameFile, unlinkFile) {
703
- const planned = outputs.map((output) => {
704
- safeProjectPath(cwd, output.path);
705
- const nonce = randomUUID2();
706
- const directory = dirname2(output.path);
707
- return {
708
- ...output,
709
- temporary: join4(directory, `.${basename3(output.path)}.vanillasky-stage-${nonce}`),
710
- backup: join4(directory, `.${basename3(output.path)}.vanillasky-backup-${nonce}`),
711
- hadPrevious: lstatSync2(output.path, { throwIfNoEntry: false }) != null,
712
- backupCreated: false,
713
- installed: false
714
- };
715
- });
716
- try {
717
- for (const output of planned) {
718
- safeProjectPath(cwd, output.temporary);
719
- safeProjectPath(cwd, output.backup);
720
- await writeStagedFile(output.temporary, output.source);
721
- }
722
- for (const output of planned) {
723
- safeProjectPath(cwd, output.path);
724
- if (output.hadPrevious) {
725
- await renameFile(output.path, output.backup);
726
- output.backupCreated = true;
727
- }
728
- await renameFile(output.temporary, output.path);
729
- output.installed = true;
730
- }
731
- } catch (error) {
732
- for (const output of [...planned].reverse()) {
733
- if (output.installed) await unlinkIfPresent(output.path);
734
- if (output.backupCreated) await rename(output.backup, output.path);
735
- await unlinkIfPresent(output.temporary);
736
- }
737
- throw error;
738
- }
739
- const warnings = [];
740
- for (const output of planned) {
741
- try {
742
- await unlinkFile(output.backup);
743
- } catch (error) {
744
- if (error.code !== "ENOENT") {
745
- warnings.push(`Could not remove transaction backup: ${relative3(cwd, output.backup)}`);
746
- }
747
- }
748
- }
749
- return warnings;
750
- }
751
- async function syncTemplates(options = {}) {
752
- const cwd = resolve4(options.cwd ?? process.cwd());
753
- const vanillaskyDirectory = join4(cwd, "vanillasky");
754
- const discovered = await discoverProjectTemplates(cwd, { timeoutMs: options.timeoutMs });
755
- const outputs = generatedOutputs(cwd, discovered);
756
- for (const output of outputs) safeProjectPath(cwd, output.path);
757
- const drifted = [];
758
- let warnings = [];
759
- for (const output of outputs) {
760
- if (await currentSource(output.path) !== output.source) drifted.push(relative3(cwd, output.path));
761
- }
762
- if (options.check && drifted.length > 0) {
763
- throw new Error(`Generated template files are out of date: ${drifted.join(", ")}. Run \`vanillasky templates sync\`.`);
764
- }
765
- if (!options.check && drifted.length > 0) {
766
- await mkdir(vanillaskyDirectory, { recursive: true });
767
- const changedOutputs = outputs.filter((output) => drifted.includes(relative3(cwd, output.path)));
768
- for (const output of changedOutputs) safeProjectPath(cwd, output.path);
769
- warnings = await writeOutputsTransactionally(cwd, changedOutputs, options.renameFile ?? rename, options.unlinkFile ?? unlink);
770
- }
771
- return {
772
- changed: drifted.length > 0,
773
- files: drifted,
774
- templates: discovered.map(({ metadata }) => metadata.id),
775
- warnings
776
- };
777
- }
778
-
779
- // src/cli/check.ts
780
- import { createHash } from "crypto";
781
- import { join as join5, relative as relative4, resolve as resolve5 } from "path";
782
- import { pathToFileURL as pathToFileURL2 } from "url";
783
- var DEFAULT_TIMEOUT_MS = 15e3;
784
- var checkRuntimeUrl = new URL("./check-runtime.js", import.meta.url).href;
785
- var JOBS = new Set(BUILTIN_TEMPLATE_MANIFEST.flatMap(({ jobs }) => jobs));
786
- var REGISTERS = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ register }) => register));
787
- var TEXT_CANVASES = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ textCanvas }) => textCanvas));
788
- var FAMILIES = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ family }) => family));
789
- var CONTENT_UNITS = new Set(BUILTIN_TEMPLATE_MANIFEST.map(({ timing }) => timing.contentUnit));
790
- var PROPERTY_KEYS = /* @__PURE__ */ new Set([
791
- "type",
792
- "format",
793
- "enum",
794
- "title",
795
- "description",
796
- "default",
797
- "examples",
798
- "minItems",
799
- "maxItems",
800
- "minLength",
801
- "maxLength",
802
- "minimum",
803
- "maximum",
804
- "items",
805
- "properties",
806
- "required",
807
- "additionalProperties"
808
- ]);
809
- var SCHEMA_KEYS = /* @__PURE__ */ new Set(["type", "properties", "required", "additionalProperties", "x-vanillasky"]);
810
- var SCHEMA_EXTENSION_KEYS = /* @__PURE__ */ new Set(["requiresStat", "allowsStockMedia", "requiredAnyOf"]);
811
- var METADATA_KEYS = /* @__PURE__ */ new Set([
812
- "id",
813
- "label",
814
- "description",
815
- "category",
816
- "family",
817
- "jobs",
818
- "register",
819
- "useWhen",
820
- "avoidWhen",
821
- "usesGlobalTextEffect",
822
- "usesGlobalTransition",
823
- "transitionTiming",
824
- "usesGlobalBackgroundEffect",
825
- "textCanvas",
826
- "schema",
827
- "minDuration",
828
- "preferredDuration",
829
- "timing"
830
- ]);
831
- function assertJson(value, path, seen = /* @__PURE__ */ new Set()) {
832
- if (value === null || typeof value === "string" || typeof value === "boolean") return;
833
- if (typeof value === "number") {
834
- if (Number.isFinite(value)) return;
835
- throw new Error(`${path} must contain only finite JSON numbers`);
836
- }
837
- if (typeof value !== "object") throw new Error(`${path} contains a non-serializable ${typeof value}`);
838
- if (seen.has(value)) throw new Error(`${path} contains a circular value`);
839
- seen.add(value);
840
- if (Array.isArray(value)) value.forEach((entry, index) => assertJson(entry, `${path}[${index}]`, seen));
841
- else {
842
- const prototype = Object.getPrototypeOf(value);
843
- if (prototype !== Object.prototype && prototype !== null) throw new Error(`${path} contains a non-serializable object`);
844
- for (const [key, entry] of Object.entries(value)) assertJson(entry, `${path}.${key}`, seen);
845
- }
846
- seen.delete(value);
847
- }
848
- function validateRequired(required, properties, path) {
849
- const seen = /* @__PURE__ */ new Set();
850
- for (const key of required ?? []) {
851
- if (typeof key !== "string" || !Object.hasOwn(properties, key)) throw new Error(`${path} references undeclared property ${JSON.stringify(key)}`);
852
- if (seen.has(key)) throw new Error(`${path} contains duplicate property ${JSON.stringify(key)}`);
853
- seen.add(key);
854
- }
855
- }
856
- function validateProperty(property, path, optional) {
857
- if (!property || typeof property !== "object" || Array.isArray(property)) throw new Error(`${path} must be an object`);
858
- for (const key of Object.keys(property)) if (!PROPERTY_KEYS.has(key)) throw new Error(`${path}.${key} is not supported`);
859
- if (!property.type || !["string", "number", "integer", "boolean", "array", "object"].includes(property.type)) {
860
- throw new Error(`${path}.type must be a supported JSON Schema type`);
861
- }
862
- if (property.format !== void 0 && !isTemplatePropertyFormatSupported(property.format, property.type)) {
863
- throw new Error(`${path}.format is not supported`);
864
- }
865
- for (const key of ["title", "description"]) {
866
- if (property[key] !== void 0 && typeof property[key] !== "string") throw new Error(`${path}.${key} must be string`);
867
- }
868
- if (property.examples !== void 0 && !Array.isArray(property.examples)) throw new Error(`${path}.examples must be an array`);
869
- if (property.type !== "string") {
870
- for (const key of ["enum", "minLength", "maxLength"]) if (property[key] !== void 0) throw new Error(`${path}.${key} is only supported for strings`);
871
- }
872
- if (property.type !== "number" && property.type !== "integer") {
873
- for (const key of ["minimum", "maximum"]) if (property[key] !== void 0) throw new Error(`${path}.${key} is only supported for numbers`);
874
- }
875
- if (property.type !== "array") {
876
- for (const key of ["minItems", "maxItems"]) if (property[key] !== void 0) throw new Error(`${path}.${key} is only supported for arrays`);
877
- }
878
- if (property.enum !== void 0 && (!Array.isArray(property.enum) || property.enum.length === 0 || property.enum.some((value) => typeof value !== "string"))) {
879
- throw new Error(`${path}.enum must contain one or more strings`);
880
- }
881
- for (const key of ["minItems", "maxItems", "minLength", "maxLength"]) {
882
- const value = property[key];
883
- if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new Error(`${path}.${key} must be a non-negative integer`);
884
- }
885
- for (const key of ["minimum", "maximum"]) {
886
- const value = property[key];
887
- if (value !== void 0 && !Number.isFinite(value)) throw new Error(`${path}.${key} must be finite`);
888
- }
889
- if (property.minItems != null && property.maxItems != null && property.minItems > property.maxItems) throw new Error(`${path}.minItems must not exceed maxItems`);
890
- if (property.minLength != null && property.maxLength != null && property.minLength > property.maxLength) throw new Error(`${path}.minLength must not exceed maxLength`);
891
- if (property.minimum != null && property.maximum != null && property.minimum > property.maximum) throw new Error(`${path}.minimum must not exceed maximum`);
892
- if (property.type === "array") {
893
- if (!property.items) throw new Error(`${path}.items is required for arrays`);
894
- validateProperty(property.items, `${path}.items`, false);
895
- } else if (property.items !== void 0) throw new Error(`${path}.items is only supported for arrays`);
896
- if (property.type === "object") {
897
- if (!property.properties || typeof property.properties !== "object" || Array.isArray(property.properties)) throw new Error(`${path}.properties must be an object`);
898
- if (property.required !== void 0 && !Array.isArray(property.required)) throw new Error(`${path}.required must be an array`);
899
- if (property.additionalProperties !== void 0 && typeof property.additionalProperties !== "boolean") throw new Error(`${path}.additionalProperties must be boolean`);
900
- validateRequired(property.required, property.properties, `${path}.required`);
901
- const required = new Set(property.required ?? []);
902
- for (const [name, child] of Object.entries(property.properties)) validateProperty(child, `${path}.properties.${name}`, !required.has(name));
903
- } else if (property.properties !== void 0 || property.required !== void 0 || property.additionalProperties !== void 0) {
904
- throw new Error(`${path} object keywords require type object`);
905
- }
906
- if (property.default !== void 0) {
907
- assertJson(property.default, `${path}.default`);
908
- validateTemplateSchemaValue(property, property.default, `${path}.default`, {
909
- allowEmptyOptionalMedia: true,
910
- optional
911
- });
912
- }
913
- for (const [index, example] of (property.examples ?? []).entries()) {
914
- assertJson(example, `${path}.examples[${index}]`);
915
- validateTemplateSchemaValue(property, example, `${path}.examples[${index}]`, {
916
- allowEmptyOptionalMedia: true,
917
- optional
918
- });
919
- }
920
- }
921
- function validateSchema(schema) {
922
- for (const key of Object.keys(schema)) if (!SCHEMA_KEYS.has(key)) throw new Error(`schema.${key} is not supported`);
923
- if (schema.type !== "object" || !schema.properties || typeof schema.properties !== "object" || Array.isArray(schema.properties)) {
924
- throw new Error("schema must describe an object with properties");
925
- }
926
- if (schema.required !== void 0 && !Array.isArray(schema.required)) throw new Error("schema.required must be an array");
927
- validateRequired(schema.required, schema.properties, "schema.required");
928
- if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") throw new Error("schema.additionalProperties must be boolean");
929
- const extension = schema["x-vanillasky"];
930
- if (extension !== void 0) {
931
- if (!extension || typeof extension !== "object" || Array.isArray(extension)) throw new Error("schema.x-vanillasky must be an object");
932
- for (const key of Object.keys(extension)) if (!SCHEMA_EXTENSION_KEYS.has(key)) throw new Error(`schema.x-vanillasky.${key} is not supported`);
933
- for (const key of ["requiresStat", "allowsStockMedia"]) {
934
- if (extension[key] !== void 0 && typeof extension[key] !== "boolean") throw new Error(`schema.x-vanillasky.${key} must be boolean`);
935
- }
936
- if (extension.requiredAnyOf !== void 0) {
937
- if (!Array.isArray(extension.requiredAnyOf)) throw new Error("schema.x-vanillasky.requiredAnyOf must be an array");
938
- for (const [groupIndex, group] of extension.requiredAnyOf.entries()) {
939
- const path = `schema.x-vanillasky.requiredAnyOf[${groupIndex}]`;
940
- if (!Array.isArray(group) || group.length === 0) throw new Error(`${path} must contain at least one property`);
941
- for (const field of group) {
942
- if (typeof field !== "string" || !Object.hasOwn(schema.properties, field)) {
943
- throw new Error(`${path} references undeclared property ${JSON.stringify(field)}`);
944
- }
945
- }
946
- }
947
- }
948
- }
949
- const required = new Set(schema.required ?? []);
950
- for (const [name, property] of Object.entries(schema.properties)) validateProperty(property, `schema.properties.${name}`, !required.has(name));
951
- }
952
- function validateVariables(schema, variables, path, enforceRequiredAnyOf) {
953
- assertJson(variables, path);
954
- for (const required2 of schema.required ?? []) if (!Object.hasOwn(variables, required2)) throw new Error(`${path}.${required2} is required`);
955
- if (schema.additionalProperties === false) {
956
- for (const key of Object.keys(variables)) if (!Object.hasOwn(schema.properties, key)) throw new Error(`${path}.${key} is not declared`);
957
- }
958
- const required = new Set(schema.required ?? []);
959
- for (const [key, property] of Object.entries(schema.properties)) {
960
- if (Object.hasOwn(variables, key)) validateTemplateSchemaValue(property, variables[key], `${path}.${key}`, {
961
- allowEmptyOptionalMedia: true,
962
- optional: !required.has(key)
963
- });
964
- }
965
- if (enforceRequiredAnyOf) {
966
- for (const group of schema["x-vanillasky"]?.requiredAnyOf ?? []) {
967
- if (group.some((field) => Object.hasOwn(variables, field) && isMeaningfullyPresentTemplateValue(variables[field]))) continue;
968
- throw new Error(`${path} requires a non-empty value for ${group.join(" or ")}`);
969
- }
970
- }
971
- }
972
- function validateDiscoveredTemplate(template, cwd) {
973
- const metadata = template.metadata;
974
- const file = relative4(cwd, template.filePath);
975
- const fail = (message) => {
976
- throw new Error(`${file}: ${message}`);
977
- };
978
- try {
979
- for (const key of Object.keys(metadata)) if (!METADATA_KEYS.has(key)) throw new Error(`metadata.${key} is not supported`);
980
- if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(metadata.id)) throw new Error(`invalid template id ${JSON.stringify(metadata.id)}`);
981
- if (typeof metadata.useWhen !== "string" || !metadata.useWhen.trim()) throw new Error("useWhen must be a non-empty string");
982
- for (const key of ["label", "description", "avoidWhen"]) if (metadata[key] !== void 0 && typeof metadata[key] !== "string") throw new Error(`${key} must be string`);
983
- if (metadata.category !== void 0 && metadata.category !== null && typeof metadata.category !== "string") throw new Error("category must be string or null");
984
- for (const key of ["usesGlobalTextEffect", "usesGlobalTransition", "usesGlobalBackgroundEffect"]) {
985
- if (typeof metadata[key] !== "boolean") throw new Error(`${key} must be boolean`);
986
- }
987
- assertTemplateTransitionMetadata(metadata);
988
- if (metadata.jobs !== void 0) {
989
- if (!Array.isArray(metadata.jobs) || metadata.jobs.some((job) => !JOBS.has(job))) throw new Error("jobs contains an unsupported value");
990
- const jobs = /* @__PURE__ */ new Set();
991
- for (const job of metadata.jobs) {
992
- if (jobs.has(job)) throw new Error(`jobs contains duplicate value ${JSON.stringify(job)}`);
993
- jobs.add(job);
994
- }
995
- }
996
- if (metadata.register !== void 0 && !REGISTERS.has(metadata.register)) throw new Error("register contains an unsupported value");
997
- if (metadata.textCanvas !== void 0 && !TEXT_CANVASES.has(metadata.textCanvas)) throw new Error("textCanvas contains an unsupported value");
998
- if (metadata.family !== void 0 && !FAMILIES.has(metadata.family)) throw new Error("family contains an unsupported value");
999
- for (const key of ["minDuration", "preferredDuration"]) {
1000
- const duration = metadata[key];
1001
- if (duration !== void 0 && (!Number.isFinite(duration) || duration <= 0)) throw new Error(`${key} must be finite and positive`);
1002
- }
1003
- if (metadata.minDuration != null && metadata.preferredDuration != null && metadata.minDuration > metadata.preferredDuration) {
1004
- throw new Error("minDuration must not exceed preferredDuration");
1005
- }
1006
- validateSchema(metadata.schema);
1007
- if (metadata.timing !== void 0) {
1008
- if (!metadata.timing || typeof metadata.timing !== "object" || Array.isArray(metadata.timing)) throw new Error("timing must be an object");
1009
- for (const key of Object.keys(metadata.timing)) if (!(/* @__PURE__ */ new Set(["contentFields", "contentUnit", "revealSeconds", "holdSeconds", "exitSeconds"])).has(key)) throw new Error(`timing.${key} is not supported`);
1010
- if (!Array.isArray(metadata.timing.contentFields)) throw new Error("timing.contentFields must be an array of schema properties");
1011
- for (const key of ["revealSeconds", "holdSeconds", "exitSeconds"]) {
1012
- const value = metadata.timing[key];
1013
- if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) throw new Error(`timing.${key} must be a finite non-negative number`);
1014
- }
1015
- const timingFields = /* @__PURE__ */ new Set();
1016
- for (const field of metadata.timing.contentFields) {
1017
- if (typeof field !== "string" || !Object.hasOwn(metadata.schema.properties, field)) throw new Error(`timing.contentFields references undeclared property ${JSON.stringify(field)}`);
1018
- if (timingFields.has(field)) throw new Error(`timing.contentFields contains duplicate property ${JSON.stringify(field)}`);
1019
- timingFields.add(field);
1020
- }
1021
- if (!CONTENT_UNITS.has(metadata.timing.contentUnit)) throw new Error("timing.contentUnit contains an unsupported value");
1022
- }
1023
- const defaults = getTemplateDefaults(metadata.schema);
1024
- const smokeVariables = { ...defaults };
1025
- for (const field of metadata.schema.required ?? []) {
1026
- const example = metadata.schema.properties[field]?.examples?.[0];
1027
- if (!(field in smokeVariables) && example !== void 0) smokeVariables[field] = example;
1028
- }
1029
- if (template.examples !== void 0 && !Array.isArray(template.examples)) throw new Error("examples must be an array");
1030
- const supplied = template.examples ?? [];
1031
- for (const [index, value] of supplied.entries()) {
1032
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`examples[${index}] must be a plain object`);
1033
- for (const key of Object.keys(value)) if (!(/* @__PURE__ */ new Set(["name", "variables"])).has(key)) throw new Error(`examples[${index}].${key} is not supported`);
1034
- const candidate = value;
1035
- if (!candidate.variables || typeof candidate.variables !== "object" || Array.isArray(candidate.variables)) {
1036
- throw new Error(`example ${JSON.stringify(candidate.name)} variables must be a plain object`);
1037
- }
1038
- }
1039
- const examples = supplied.length > 0 ? supplied.map(({ name, variables }) => ({
1040
- name,
1041
- variables: { ...defaults, ...variables },
1042
- enforceRequiredAnyOf: true
1043
- })) : [{ name: "defaults", variables: smokeVariables, enforceRequiredAnyOf: false }];
1044
- const names = /* @__PURE__ */ new Set();
1045
- for (const example of examples) {
1046
- if (typeof example.name !== "string" || !example.name.trim()) throw new Error("example names must be non-empty strings");
1047
- if (names.has(example.name)) throw new Error(`duplicate example name ${JSON.stringify(example.name)}`);
1048
- names.add(example.name);
1049
- validateVariables(
1050
- metadata.schema,
1051
- example.variables,
1052
- `example ${JSON.stringify(example.name)}`,
1053
- example.enforceRequiredAnyOf
1054
- );
1055
- }
1056
- return examples;
1057
- } catch (error) {
1058
- return fail(error instanceof Error ? error.message : String(error));
1059
- }
1060
- }
1061
- var RENDER_LOADER = String.raw`
1062
- const [runtimeUrl, payload] = process.argv.slice(1);
1063
- const { writeSync } = await import("node:fs");
1064
- for (const method of ["log", "info", "warn", "error", "debug", "dir", "trace"]) console[method] = () => undefined;
1065
- const { renderTemplateChecks } = await import(runtimeUrl);
1066
- const result = await renderTemplateChecks(JSON.parse(payload));
1067
- writeSync(3, JSON.stringify(result));
1068
- `;
1069
- async function renderExample(template, example, cwd, timeoutMs, maxOutputBytes, runtime) {
1070
- try {
1071
- const payload = JSON.stringify({
1072
- sourceUrl: pathToFileURL2(template.filePath).href,
1073
- exportName: template.exportName,
1074
- templateId: template.metadata.id,
1075
- exampleName: example.name,
1076
- variables: example.variables,
1077
- duration: template.metadata.preferredDuration ?? template.metadata.minDuration ?? 5,
1078
- timeoutMs
1079
- });
1080
- const output = await runTrustedSourceProcess([
1081
- "--import",
1082
- runtime.loader,
1083
- "--input-type=module",
1084
- "--eval",
1085
- RENDER_LOADER,
1086
- checkRuntimeUrl,
1087
- payload
1088
- ], { cwd, timeoutMs, maxOutputBytes, operation: "template rendering" });
1089
- return JSON.parse(output).renders;
1090
- } catch (error) {
1091
- const detail = error instanceof Error ? error.message : String(error);
1092
- throw new Error(`Could not render ${relative4(cwd, template.filePath)}: ${sanitizeTrustedSourceDiagnostic(detail, cwd, runtime.dependencyRoot)}`);
1093
- }
1094
- }
1095
- var REGISTRY_LOADER = String.raw`
1096
- const [browserUrl, serverUrl] = process.argv.slice(1);
1097
- const { writeSync } = await import("node:fs");
1098
- for (const method of ["log", "info", "warn", "error", "debug", "dir", "trace"]) console[method] = () => undefined;
1099
- const browser = await import(browserUrl + "?check=" + Date.now());
1100
- const server = await import(serverUrl + "?check=" + Date.now());
1101
- writeSync(3, JSON.stringify({
1102
- browser: browser.templates.listTemplateMetadata(),
1103
- server: server.templates.listTemplateMetadata(),
1104
- }));
1105
- `;
1106
- function stable(value) {
1107
- if (value === null || typeof value !== "object") return JSON.stringify(value);
1108
- if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
1109
- return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`).join(",")}}`;
1110
- }
1111
- async function verifyGeneratedParity(cwd, expected, timeoutMs, maxOutputBytes, runtime) {
1112
- try {
1113
- const output = await runTrustedSourceProcess([
1114
- "--import",
1115
- runtime.loader,
1116
- "--input-type=module",
1117
- "--eval",
1118
- REGISTRY_LOADER,
1119
- pathToFileURL2(join5(cwd, "vanillasky/index.ts")).href,
1120
- pathToFileURL2(join5(cwd, "vanillasky/server.ts")).href
1121
- ], { cwd, timeoutMs, maxOutputBytes, operation: "generated registry parity" });
1122
- const loaded = JSON.parse(output);
1123
- const expectedFingerprint = createHash("sha256").update(stable(expected)).digest("hex");
1124
- for (const [entrypoint, metadata] of Object.entries(loaded)) {
1125
- const fingerprint = createHash("sha256").update(stable(metadata)).digest("hex");
1126
- if (fingerprint !== expectedFingerprint) {
1127
- throw new Error(`${entrypoint} registry metadata/IDs/order fingerprint differs from template sources`);
1128
- }
1129
- }
1130
- } catch (error) {
1131
- const detail = error instanceof Error ? error.message : String(error);
1132
- throw new Error(`Generated registry parity failed: ${sanitizeTrustedSourceDiagnostic(detail, cwd, runtime.dependencyRoot)}`);
1133
- }
1134
- }
1135
- async function checkTemplates(options = {}) {
1136
- const cwd = resolve5(options.cwd ?? process.cwd());
1137
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1138
- const templates = await discoverProjectTemplates(cwd, {
1139
- timeoutMs,
1140
- maxOutputBytes: options.maxOutputBytes
1141
- });
1142
- if (templates.length === 0) throw new Error("No templates found in vanillasky/templates.");
1143
- const runtime = resolveProjectTsxRuntime();
1144
- const examples = templates.map((template) => ({
1145
- template,
1146
- examples: validateDiscoveredTemplate(template, cwd)
1147
- }));
1148
- const generated = await inspectGeneratedTemplateStatus(cwd, templates);
1149
- if (!generated.current) {
1150
- throw new Error(`Generated template files are out of date: ${generated.stale.join(", ")}. Run \`vanillasky templates sync\`.`);
1151
- }
1152
- let renders = 0;
1153
- for (const item2 of examples) {
1154
- for (const example of item2.examples) {
1155
- renders += await renderExample(item2.template, example, cwd, timeoutMs, options.maxOutputBytes, runtime);
1156
- }
1157
- }
1158
- await verifyGeneratedParity(cwd, templates.map(({ metadata }) => metadata), timeoutMs, options.maxOutputBytes, runtime);
1159
- return {
1160
- templates: templates.length,
1161
- examples: examples.reduce((count, item2) => count + item2.examples.length, 0),
1162
- renders
1163
- };
1164
- }
1165
-
1166
- // src/cli/create.ts
1167
- import { link, lstat, mkdir as mkdir2, open as open2, rmdir, unlink as unlink2 } from "fs/promises";
1168
- import { join as join6, relative as relative5, resolve as resolve6 } from "path";
1169
- var TEMPLATE_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
1170
- var MAX_TEMPLATE_ID_BYTES = 128;
1171
- var WINDOWS_RESERVED_BASENAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
1172
- var temporaryFileSequence = 0;
1173
- function titleFor(id) {
1174
- 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(" ");
1175
- }
1176
- function sourceFor(id) {
1177
- const title = titleFor(id);
1178
- const titleLiteral = JSON.stringify(title);
1179
- return `import { defineTemplate } from "@vanillaskyai/video/templates";
1180
-
1181
- export const template = defineTemplate({
1182
- id: ${JSON.stringify(id)},
1183
- label: ${titleLiteral},
1184
- description: "A focused headline and supporting detail for a grounded visual response.",
1185
- useWhen: "Use when the response should emphasize ${title} with one concise supporting detail.",
1186
- schema: {
1187
- type: "object",
1188
- properties: {
1189
- title: {
1190
- type: "string",
1191
- title: "Headline",
1192
- description: "The main grounded point the viewer should remember.",
1193
- minLength: 1,
1194
- maxLength: 72,
1195
- default: ${titleLiteral},
1196
- },
1197
- subtitle: {
1198
- type: "string",
1199
- title: "Supporting detail",
1200
- description: "One concise detail that explains or grounds the headline.",
1201
- minLength: 1,
1202
- maxLength: 140,
1203
- default: "A visual response grounded in the available context.",
1204
- },
1205
- },
1206
- required: ["title", "subtitle"],
1207
- additionalProperties: false,
1208
- } as const,
1209
- examples: [{
1210
- name: "${title} update",
1211
- variables: {
1212
- title: ${titleLiteral},
1213
- subtitle: "A visual response grounded in the available context.",
1214
- },
1215
- }],
1216
- minDuration: 3,
1217
- preferredDuration: 5,
1218
- component: ({ variables, progress, width, height, safeZone }) => {
1219
- const isPortrait = height >= width;
1220
- const reveal = Math.max(0, Math.min(1, progress * 1.5));
1221
- const contentWidth = isPortrait
1222
- ? width - safeZone.left - safeZone.right
1223
- : Math.min(width * 0.72, 960);
1224
-
1225
- return <section style={{
1226
- boxSizing: "border-box",
1227
- width,
1228
- height,
1229
- padding: safeZone.top + "px " + safeZone.right + "px " + safeZone.bottom + "px " + safeZone.left + "px",
1230
- display: "grid",
1231
- placeItems: "center",
1232
- overflow: "hidden",
1233
- color: "#ffffff",
1234
- background: "linear-gradient(145deg, #15112b 0%, #382f73 55%, #7658d6 100%)",
1235
- fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
1236
- }}>
1237
- <div style={{
1238
- width: contentWidth,
1239
- opacity: reveal,
1240
- transform: "translateY(" + Math.round((1 - reveal) * 24) + "px)",
1241
- textAlign: isPortrait ? "left" : "center",
1242
- }}>
1243
- <h1 style={{
1244
- margin: 0,
1245
- fontSize: Math.max(34, Math.min(width * (isPortrait ? 0.1 : 0.065), 88)),
1246
- lineHeight: 0.98,
1247
- letterSpacing: "-0.045em",
1248
- }}>
1249
- {variables.title}
1250
- </h1>
1251
- <p style={{
1252
- margin: Math.max(18, height * 0.035) + "px 0 0",
1253
- maxWidth: isPortrait ? "100%" : 760,
1254
- fontSize: Math.max(18, Math.min(width * 0.03, 34)),
1255
- lineHeight: 1.35,
1256
- color: "rgba(255, 255, 255, 0.78)",
1257
- }}>
1258
- {variables.subtitle}
1259
- </p>
1260
- </div>
1261
- </section>;
1262
- },
1263
- });
1264
- `;
1265
- }
1266
- async function status(path) {
1267
- try {
1268
- return await lstat(path);
1269
- } catch (error) {
1270
- if (error.code === "ENOENT") return void 0;
1271
- throw error;
1272
- }
1273
- }
1274
- async function assertDirectoryOrMissing(path, displayPath) {
1275
- const existing = await status(path);
1276
- if (!existing) return false;
1277
- if (existing.isSymbolicLink()) throw new Error(`Refusing to create through symbolic link: ${displayPath}`);
1278
- if (!existing.isDirectory()) throw new Error(`Template path ancestor is not a directory: ${displayPath}`);
1279
- return true;
1280
- }
1281
- async function removeCreatedDirectories(paths) {
1282
- for (const path of [...paths].reverse()) {
1283
- try {
1284
- await rmdir(path);
1285
- } catch (error) {
1286
- const code = error.code;
1287
- if (code !== "ENOENT" && code !== "ENOTEMPTY") throw error;
1288
- }
1289
- }
1290
- }
1291
- async function createTemplate(options) {
1292
- const cwd = resolve6(options.cwd ?? process.cwd());
1293
- const { id } = options;
1294
- if (!TEMPLATE_ID.test(id)) {
1295
- throw new Error(`Invalid template id: ${JSON.stringify(id)}. Use a letter first, followed by letters, numbers, _ or -.`);
1296
- }
1297
- if (Buffer.byteLength(id, "utf8") > MAX_TEMPLATE_ID_BYTES) {
1298
- throw new Error(`Invalid template id: template ids must be at most ${MAX_TEMPLATE_ID_BYTES} UTF-8 bytes.`);
1299
- }
1300
- const windowsBasename = `${id}.tsx`.replace(/[ .]+$/u, "").split(".", 1)[0];
1301
- if (WINDOWS_RESERVED_BASENAME.test(windowsBasename)) {
1302
- throw new Error(`Invalid template id: ${JSON.stringify(id)}. This name is reserved on Windows.`);
1303
- }
1304
- const vanillaskyDirectory = join6(cwd, "vanillasky");
1305
- const templatesDirectory = join6(vanillaskyDirectory, "templates");
1306
- const destination = join6(templatesDirectory, `${id}.tsx`);
1307
- const destinationPath = relative5(cwd, destination);
1308
- const createdDirectories = [];
1309
- for (const target of [destination, join6(vanillaskyDirectory, "index.ts"), join6(vanillaskyDirectory, "server.ts")]) {
1310
- safeProjectPath(cwd, target);
1311
- }
1312
- for (const [path, displayPath] of [
1313
- [vanillaskyDirectory, "vanillasky"],
1314
- [templatesDirectory, "vanillasky/templates"]
1315
- ]) {
1316
- if (!await assertDirectoryOrMissing(path, displayPath)) {
1317
- try {
1318
- await mkdir2(path);
1319
- createdDirectories.push(path);
1320
- } catch (error) {
1321
- if (error.code !== "EEXIST") throw error;
1322
- await assertDirectoryOrMissing(path, displayPath);
1323
- }
1324
- }
1325
- safeProjectPath(cwd, destination);
1326
- }
1327
- const existingDestination = await status(destination);
1328
- if (existingDestination?.isSymbolicLink()) {
1329
- await removeCreatedDirectories(createdDirectories);
1330
- throw new Error(`Refusing to replace symbolic link: ${destinationPath}`);
1331
- }
1332
- if (existingDestination) {
1333
- await removeCreatedDirectories(createdDirectories);
1334
- throw new Error(`Template already exists: ${destinationPath}`);
1335
- }
1336
- const temporary = join6(templatesDirectory, `.${id}.${process.pid}.${temporaryFileSequence += 1}.tmp`);
1337
- let temporaryExists = false;
1338
- let createdIdentity;
1339
- try {
1340
- const handle = await open2(temporary, "wx", 420);
1341
- temporaryExists = true;
1342
- try {
1343
- await handle.writeFile(sourceFor(id), "utf8");
1344
- await handle.sync();
1345
- } finally {
1346
- await handle.close();
1347
- }
1348
- try {
1349
- await link(temporary, destination);
1350
- } catch (error) {
1351
- if (error.code === "EEXIST") {
1352
- throw new Error(`Template already exists: ${destinationPath}`);
1353
- }
1354
- throw error;
1355
- }
1356
- const created = await lstat(destination, { bigint: true });
1357
- createdIdentity = { dev: created.dev, ino: created.ino };
1358
- await unlink2(temporary);
1359
- temporaryExists = false;
1360
- const result = await (options.sync ?? syncTemplates)({ cwd });
1361
- const templates = typeof result === "object" && result != null && "templates" in result && Array.isArray(result.templates) ? result.templates.length : 1;
1362
- const warnings = typeof result === "object" && result != null && "warnings" in result && Array.isArray(result.warnings) ? result.warnings.filter((warning) => typeof warning === "string") : [];
1363
- return { path: destinationPath, templates, ...warnings.length > 0 ? { warnings } : {} };
1364
- } catch (error) {
1365
- if (temporaryExists) {
1366
- try {
1367
- await unlink2(temporary);
1368
- } catch (cleanupError) {
1369
- if (cleanupError.code !== "ENOENT") throw cleanupError;
1370
- }
1371
- }
1372
- if (createdIdentity) {
1373
- const current = await status(destination);
1374
- if (current && !current.isSymbolicLink()) {
1375
- const identity = await lstat(destination, { bigint: true });
1376
- if (identity.dev === createdIdentity.dev && identity.ino === createdIdentity.ino) await unlink2(destination);
1377
- }
1378
- }
1379
- await removeCreatedDirectories(createdDirectories);
1380
- throw error;
1381
- }
1382
- }
1383
-
1384
- // src/cli/catalog.ts
1385
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
1386
- import { join as join7 } from "path";
1387
- function item(metadata, origin, generated) {
1388
- const useWhen = metadata.useWhen ?? metadata.description ?? "";
1389
- const current = generated == null ? null : generated.browser && generated.server;
1390
- return {
1391
- id: metadata.id,
1392
- title: metadata.label ?? metadata.id,
1393
- origin,
1394
- status: origin === "built-in" ? "available" : current ? "current" : "stale",
1395
- useWhen,
1396
- summary: metadata.description ?? "",
1397
- planner: {
1398
- useWhen,
1399
- ...metadata.avoidWhen == null ? {} : { avoidWhen: metadata.avoidWhen }
1400
- },
1401
- schema: metadata.schema,
1402
- schemaSummary: summarizeTemplateVariables(metadata.schema),
1403
- ...metadata.family == null ? {} : { family: metadata.family },
1404
- ...metadata.jobs == null ? {} : { jobs: metadata.jobs },
1405
- ...metadata.register == null ? {} : { register: metadata.register },
1406
- duration: {
1407
- ...metadata.minDuration == null ? {} : { min: metadata.minDuration },
1408
- ...metadata.preferredDuration == null ? {} : { preferred: metadata.preferredDuration }
1409
- },
1410
- generated: {
1411
- browser: { path: "vanillasky/index.ts", current: generated?.browser ?? null },
1412
- server: { path: "vanillasky/server.ts", current: generated?.server ?? null },
1413
- current
1414
- },
1415
- wiring: { applicationImportsInspected: false, verified: false }
1416
- };
1417
- }
1418
- function builtinTemplateCatalog() {
1419
- return listBuiltinTemplateMetadata().map((metadata) => item(metadata, "built-in", null)).sort((left, right) => left.id.localeCompare(right.id));
1420
- }
1421
- function hasProjectTemplateSources(cwd) {
1422
- const directory = join7(cwd, "vanillasky/templates");
1423
- if (!existsSync2(directory)) return false;
1424
- return readdirSync2(directory, { withFileTypes: true }).some((entry) => (entry.isFile() || entry.isSymbolicLink()) && /\.(?:ts|tsx)$/.test(entry.name));
1425
- }
1426
- async function effectiveTemplateCatalog(cwd) {
1427
- const builtins = builtinTemplateCatalog();
1428
- const discovered = await discoverProjectTemplates(cwd);
1429
- if (discovered.length === 0) return builtins;
1430
- const generated = await inspectGeneratedTemplateStatus(cwd, discovered);
1431
- const byId = new Map(builtins.map((entry) => [entry.id, entry]));
1432
- for (const template of discovered) {
1433
- byId.set(template.metadata.id, item(template.metadata, "project", {
1434
- browser: generated.browser.current,
1435
- server: generated.server.current
1436
- }));
1437
- }
1438
- return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
1439
- }
1440
- function findBuiltinTemplate(id) {
1441
- return builtinTemplateCatalog().find((entry) => entry.id === id);
1442
- }
1443
- async function findEffectiveTemplate(cwd, id) {
1444
- return (await effectiveTemplateCatalog(cwd)).find((entry) => entry.id === id);
1445
- }
1446
-
1447
- // src/cli/init.ts
1448
- import { spawn as spawn2 } from "child_process";
1449
- import {
1450
- existsSync as existsSync3,
1451
- mkdirSync as mkdirSync2,
1452
- readFileSync as readFileSync2,
1453
- renameSync as renameSync2,
1454
- unlinkSync as unlinkSync2,
1455
- writeFileSync as writeFileSync2
1456
- } from "fs";
1457
- import { basename as basename4, dirname as dirname3, isAbsolute, join as join8, resolve as resolve7 } from "path";
1458
- import { fileURLToPath as fileURLToPath2 } from "url";
1459
-
1460
60
  // src/cli/gitignore.ts
1461
61
  var ROOT_ENVIRONMENT_FILE = ".env.local";
1462
62
  function escapeRegex(character) {
@@ -1523,23 +123,23 @@ var SCAFFOLD_FILES = [
1523
123
  ];
1524
124
  var DEFAULT_GITIGNORE = ["node_modules", "dist", ".env.local", ""].join("\n");
1525
125
  function locateStarterRoot() {
1526
- const moduleDirectory = dirname3(fileURLToPath2(import.meta.url));
126
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
1527
127
  const candidates = [
1528
- resolve7(moduleDirectory, "../../starters/video-chat"),
1529
- resolve7(moduleDirectory, "../starters/video-chat")
128
+ resolve2(moduleDirectory, "../../starters/video-chat"),
129
+ resolve2(moduleDirectory, "../starters/video-chat")
1530
130
  ];
1531
- const found = candidates.find((candidate) => existsSync3(join8(candidate, "package.json")));
131
+ const found = candidates.find((candidate) => existsSync(join2(candidate, "package.json")));
1532
132
  if (!found) throw new Error("The packaged video-chat starter is missing. Reinstall @vanillaskyai/video.");
1533
133
  return found;
1534
134
  }
1535
135
  function readJson(path) {
1536
136
  try {
1537
- const value = JSON.parse(readFileSync2(path, "utf8"));
137
+ const value = JSON.parse(readFileSync(path, "utf8"));
1538
138
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected an object");
1539
139
  return value;
1540
140
  } catch (cause) {
1541
141
  const detail = cause instanceof Error ? cause.message : String(cause);
1542
- throw new Error(`Cannot read ${basename4(path)}: ${detail}`);
142
+ throw new Error(`Cannot read ${basename(path)}: ${detail}`);
1543
143
  }
1544
144
  }
1545
145
  function stringMap(value) {
@@ -1550,19 +150,23 @@ function resolvedSdkSpec(starterRoot, invokedSpec) {
1550
150
  const spec = invokedSpec?.trim();
1551
151
  if (!spec) return void 0;
1552
152
  if (spec === "@vanillaskyai/video" || spec.startsWith("@vanillaskyai/video@")) {
1553
- const packageManifest = readJson(resolve7(starterRoot, "../..", "package.json"));
153
+ const packageManifest = readJson(resolve2(starterRoot, "../..", "package.json"));
1554
154
  return typeof packageManifest.version === "string" ? packageManifest.version : void 0;
1555
155
  }
1556
156
  if (spec.startsWith("file:")) return spec;
1557
157
  return isAbsolute(spec) ? `file:${spec}` : void 0;
1558
158
  }
1559
- function mergedManifest(cwd, starterRoot, invokedSpec) {
1560
- const destination = join8(cwd, "package.json");
1561
- const existing = existsSync3(destination) ? readJson(destination) : {};
1562
- const starter = readJson(join8(starterRoot, "package.json"));
1563
- const packageName = typeof existing.name === "string" && existing.name.trim() ? existing.name : basename4(cwd).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
159
+ function mergedManifest(cwd, starterRoot, invokedSpec, native) {
160
+ const destination = join2(cwd, "package.json");
161
+ const existing = existsSync(destination) ? readJson(destination) : {};
162
+ const starter = readJson(join2(starterRoot, "package.json"));
163
+ const packageName = typeof existing.name === "string" && existing.name.trim() ? existing.name : basename(cwd).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
1564
164
  const existingDependencies = stringMap(existing.dependencies);
1565
165
  const starterDependencies = stringMap(starter.dependencies);
166
+ if (native || existing.vanillasky?.textProvider === "custom") {
167
+ delete starterDependencies.ai;
168
+ delete starterDependencies["@ai-sdk/anthropic"];
169
+ }
1566
170
  const existingScripts = stringMap(existing.scripts);
1567
171
  const starterScripts = stringMap(starter.scripts);
1568
172
  for (const [name, command] of Object.entries(starterScripts)) {
@@ -1592,7 +196,8 @@ function mergedManifest(cwd, starterRoot, invokedSpec) {
1592
196
  type: "module",
1593
197
  scripts: { ...existingScripts, ...starterScripts },
1594
198
  dependencies,
1595
- devDependencies
199
+ devDependencies,
200
+ vanillasky: { ...existing.vanillasky, textProvider: existing.vanillasky?.textProvider ?? (native ? "native" : "vercel") }
1596
201
  };
1597
202
  return `${JSON.stringify(manifest, null, 2)}
1598
203
  `;
@@ -1608,18 +213,18 @@ function mergeGitignore(existing) {
1608
213
  `;
1609
214
  }
1610
215
  function atomicWrite(path, contents) {
1611
- mkdirSync2(dirname3(path), { recursive: true });
216
+ mkdirSync(dirname(path), { recursive: true });
1612
217
  const temporary = `${path}.vanillasky-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
1613
218
  try {
1614
- writeFileSync2(temporary, contents, { encoding: "utf8", flag: "wx", mode: 420 });
1615
- renameSync2(temporary, path);
219
+ writeFileSync(temporary, contents, { encoding: "utf8", flag: "wx", mode: 420 });
220
+ renameSync(temporary, path);
1616
221
  } finally {
1617
- if (existsSync3(temporary)) unlinkSync2(temporary);
222
+ if (existsSync(temporary)) unlinkSync(temporary);
1618
223
  }
1619
224
  }
1620
225
  function defaultInstall(cwd) {
1621
226
  return new Promise((resolvePromise, reject) => {
1622
- const child = spawn2("npm", ["install", "--no-audit", "--no-fund"], {
227
+ const child = spawn("npm", ["install", "--no-audit", "--no-fund"], {
1623
228
  cwd,
1624
229
  shell: false,
1625
230
  stdio: "inherit"
@@ -1632,31 +237,38 @@ function defaultInstall(cwd) {
1632
237
  });
1633
238
  }
1634
239
  async function initVideoChatApp(options) {
1635
- const cwd = resolve7(options.cwd);
1636
- const starterRoot = resolve7(options.starterRoot ?? locateStarterRoot());
240
+ const cwd = resolve2(options.cwd);
241
+ const starterRoot = resolve2(options.starterRoot ?? locateStarterRoot());
242
+ const manifestPath = safeProjectPath(cwd, "package.json");
243
+ const existing = existsSync(manifestPath) ? readJson(manifestPath) : {};
244
+ if (options.native && existing.vanillasky?.textProvider === "vercel") throw new Error("Text provider already configured. Edit providers/text.ts and vanillasky.textProvider manually to switch.");
245
+ const native = options.native ?? existing.vanillasky?.textProvider === "native";
1637
246
  const planned = /* @__PURE__ */ new Map();
1638
247
  for (const path of SCAFFOLD_FILES) {
1639
- planned.set(path, readFileSync2(join8(starterRoot, path), "utf8"));
248
+ planned.set(path, readFileSync(join2(starterRoot, path), "utf8"));
1640
249
  }
250
+ if (native) planned.set(".env.example", readFileSync(join2(starterRoot, ".env.native.example"), "utf8"));
251
+ const textPath = safeProjectPath(cwd, "providers/text.ts");
252
+ if (!existsSync(textPath)) planned.set("providers/text.ts", readFileSync(join2(starterRoot, native ? "providers/text-native.ts" : "providers/text.ts"), "utf8"));
1641
253
  const providersPath = safeProjectPath(cwd, "providers.ts");
1642
- if (!existsSync3(providersPath)) {
1643
- planned.set("providers.ts", readFileSync2(join8(starterRoot, "providers.ts"), "utf8"));
254
+ if (!existsSync(providersPath)) {
255
+ planned.set("providers.ts", readFileSync(join2(starterRoot, "providers.ts"), "utf8"));
1644
256
  }
1645
- planned.set("package.json", mergedManifest(cwd, starterRoot, options.sdkSpec));
257
+ planned.set("package.json", mergedManifest(cwd, starterRoot, options.sdkSpec, native));
1646
258
  planned.set(".gitignore", mergeGitignore(
1647
- existsSync3(join8(cwd, ".gitignore")) ? readFileSync2(join8(cwd, ".gitignore"), "utf8") : void 0
259
+ existsSync(join2(cwd, ".gitignore")) ? readFileSync(join2(cwd, ".gitignore"), "utf8") : void 0
1648
260
  ));
1649
- if (!existsSync3(join8(cwd, ".env.local"))) {
261
+ if (!existsSync(join2(cwd, ".env.local"))) {
1650
262
  planned.set(".env.local", planned.get(".env.example"));
1651
263
  }
1652
264
  let changed = false;
1653
265
  for (const [path, contents] of planned) {
1654
266
  const destination = safeProjectPath(cwd, path);
1655
- if (!existsSync3(destination)) {
267
+ if (!existsSync(destination)) {
1656
268
  changed = true;
1657
269
  continue;
1658
270
  }
1659
- const current = readFileSync2(destination, "utf8");
271
+ const current = readFileSync(destination, "utf8");
1660
272
  if (current === contents) continue;
1661
273
  if (path === "package.json" || path === ".gitignore") {
1662
274
  changed = true;
@@ -1666,7 +278,7 @@ async function initVideoChatApp(options) {
1666
278
  }
1667
279
  for (const [path, contents] of planned) {
1668
280
  const destination = safeProjectPath(cwd, path);
1669
- if (!existsSync3(destination) || readFileSync2(destination, "utf8") !== contents) atomicWrite(destination, contents);
281
+ if (!existsSync(destination) || readFileSync(destination, "utf8") !== contents) atomicWrite(destination, contents);
1670
282
  }
1671
283
  try {
1672
284
  await (options.installDependencies ?? defaultInstall)(cwd);
@@ -1678,12 +290,13 @@ async function initVideoChatApp(options) {
1678
290
  }
1679
291
 
1680
292
  // src/cli/providers.ts
1681
- import { randomUUID as randomUUID3 } from "crypto";
1682
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
1683
- import { dirname as dirname4, join as join9 } from "path";
293
+ import { randomUUID } from "crypto";
294
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
295
+ import { dirname as dirname2, join as join3 } from "path";
1684
296
  var PROVIDERS = {
1685
- speech: { dependency: "@ai-sdk/xai", version: "^4.0.52", key: "XAI_API_KEY", export: "speechProvider" },
1686
- video: { dependency: "@fal-ai/client", version: "1.10.1", key: "FAL_KEY", export: "videoProvider" }
297
+ speech: { dependencies: { "@ai-sdk/xai": "^4.0.52", ai: "^7.0.77" }, key: "XAI_API_KEY", export: "speechProvider" },
298
+ video: { dependencies: {}, key: "FAL_KEY", export: "videoProvider" },
299
+ transcription: { dependencies: {}, key: "FAL_KEY", export: "transcriptionProvider" }
1687
300
  };
1688
301
  function registry(enabled) {
1689
302
  const imports = enabled.map((name) => `import { ${PROVIDERS[name].export} } from "./providers/${name}";`).join("\n");
@@ -1697,25 +310,34 @@ export const providers: Pick<VideoChatHandlerOptions, "generateSpeech" | "genera
1697
310
  `;
1698
311
  }
1699
312
  async function addVideoChatProvider(name, options) {
1700
- if (name !== "speech" && name !== "video") throw new Error("Choose a provider capability: speech or video.");
313
+ if (name !== "speech" && name !== "video" && name !== "transcription") throw new Error("Choose a provider capability: speech, video, or transcription.");
314
+ if (options.vendor && (name !== "video" || !["fal", "google", "runway", "custom"].includes(options.vendor))) throw new Error("Choose a video vendor: fal, google, runway, or custom.");
1701
315
  const manifestPath = safeProjectPath(options.cwd, "package.json");
1702
316
  const registryPath = safeProjectPath(options.cwd, "providers.ts");
1703
- if (!existsSync4(manifestPath) || !existsSync4(registryPath)) throw new Error("Run vanillasky init before adding providers.");
1704
- const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
317
+ if (!existsSync2(manifestPath) || !existsSync2(registryPath)) throw new Error("Run vanillasky init before adding providers.");
318
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
1705
319
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw new Error("Expected an application package.json object.");
1706
320
  const config = manifest.vanillasky ?? {};
1707
321
  if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Expected vanillasky configuration to be an object.");
1708
322
  const previous = config.providers ?? [];
1709
- if (!Array.isArray(previous) || previous.some((value) => value !== "speech" && value !== "video") || new Set(previous).size !== previous.length) {
323
+ if (!Array.isArray(previous) || previous.some((value) => value !== "speech" && value !== "video" && value !== "transcription") || new Set(previous).size !== previous.length) {
1710
324
  throw new Error("Invalid vanillasky.providers configuration.");
1711
325
  }
1712
326
  const enabled = [...previous].sort();
1713
- if (readFileSync3(registryPath, "utf8") !== registry(enabled)) {
327
+ if (readFileSync2(registryPath, "utf8") !== registry(enabled)) {
1714
328
  throw new Error("providers.ts has custom wiring. Preserve it and add the provider manually, or restore the generated wiring before using this command.");
1715
329
  }
1716
330
  const adapterPath = safeProjectPath(options.cwd, `providers/${name}.ts`);
1717
- const adapter = readFileSync3(join9(options.starterRoot ?? locateStarterRoot(), "providers", `${name}.ts`), "utf8");
1718
- if (!enabled.includes(name) && existsSync4(adapterPath) && readFileSync3(adapterPath, "utf8") !== adapter) {
331
+ const previousVendor = config.videoVendor;
332
+ if (previousVendor !== void 0 && (typeof previousVendor !== "string" || !["fal", "google", "runway", "custom"].includes(previousVendor))) throw new Error("Invalid vanillasky.videoVendor configuration.");
333
+ const vendor = options.vendor ?? previousVendor ?? "fal";
334
+ if (name === "video" && enabled.includes(name) && options.vendor && options.vendor !== (previousVendor ?? "fal")) {
335
+ throw new Error("A video adapter is already installed. Change providers/video.ts and vanillasky.videoVendor manually; no files were changed.");
336
+ }
337
+ const source = name === "video" && vendor !== "fal" ? `video-${vendor}` : name;
338
+ const starterProviders = join3(options.starterRoot ?? locateStarterRoot(), "providers");
339
+ const adapter = readFileSync2(join3(starterProviders, `${source}.ts`), "utf8");
340
+ if (!enabled.includes(name) && existsSync2(adapterPath) && readFileSync2(adapterPath, "utf8") !== adapter) {
1719
341
  throw new Error(`providers/${name}.ts already contains customer source; no files were changed.`);
1720
342
  }
1721
343
  const dependencies = manifest.dependencies ?? {};
@@ -1723,22 +345,30 @@ async function addVideoChatProvider(name, options) {
1723
345
  if (!enabled.includes(name)) enabled.push(name);
1724
346
  enabled.sort();
1725
347
  const provider = PROVIDERS[name];
1726
- manifest.dependencies = { ...dependencies, [provider.dependency]: dependencies[provider.dependency] ?? provider.version };
1727
- manifest.vanillasky = { ...config, providers: enabled };
348
+ manifest.dependencies = { ...provider.dependencies, ...dependencies };
349
+ manifest.vanillasky = { ...config, providers: enabled, ...name === "video" ? { videoVendor: vendor } : {} };
1728
350
  const writes = /* @__PURE__ */ new Map([
1729
351
  [manifestPath, `${JSON.stringify(manifest, null, 2)}
1730
352
  `],
1731
353
  [registryPath, registry(enabled)]
1732
354
  ]);
1733
- if (!existsSync4(adapterPath)) writes.set(adapterPath, adapter);
1734
- const staged = [...writes].map(([path, contents]) => ({ path, contents, temporary: safeProjectPath(options.cwd, `${path.slice(options.cwd.length + 1)}.${randomUUID3()}.tmp`) }));
355
+ if (!existsSync2(adapterPath)) writes.set(adapterPath, adapter);
356
+ for (const helper of name === "video" ? ["video-job", "video-delivery"] : name === "transcription" ? ["video-job"] : []) {
357
+ const path = safeProjectPath(options.cwd, `providers/${helper}.ts`);
358
+ const contents = readFileSync2(join3(starterProviders, `${helper}.ts`), "utf8");
359
+ if (existsSync2(path) && !previous.includes("video") && !previous.includes("transcription") && readFileSync2(path, "utf8") !== contents) {
360
+ throw new Error(`providers/${helper}.ts already contains customer source; no files were changed.`);
361
+ }
362
+ if (!existsSync2(path)) writes.set(path, contents);
363
+ }
364
+ const staged = [...writes].map(([path, contents]) => ({ path, contents, temporary: safeProjectPath(options.cwd, `${path.slice(options.cwd.length + 1)}.${randomUUID()}.tmp`) }));
1735
365
  for (const { path, contents, temporary } of staged) {
1736
- mkdirSync3(dirname4(path), { recursive: true });
1737
- writeFileSync3(temporary, contents, { flag: "wx" });
366
+ mkdirSync2(dirname2(path), { recursive: true });
367
+ writeFileSync2(temporary, contents, { flag: "wx" });
1738
368
  try {
1739
- renameSync3(temporary, path);
369
+ renameSync2(temporary, path);
1740
370
  } finally {
1741
- if (existsSync4(temporary)) unlinkSync3(temporary);
371
+ if (existsSync2(temporary)) unlinkSync2(temporary);
1742
372
  }
1743
373
  }
1744
374
  try {
@@ -1746,17 +376,19 @@ async function addVideoChatProvider(name, options) {
1746
376
  } catch {
1747
377
  throw new Error(`Provider files are ready. Rerun npx vanillasky providers add ${name} to finish installation.`);
1748
378
  }
1749
- return `Enabled ${name}. Add ${provider.key} to .env.local, then restart npm run dev.`;
379
+ const key = name === "video" ? { fal: "FAL_KEY", google: "GEMINI_API_KEY", runway: "RUNWAY_API_KEY", custom: "your provider credentials" }[vendor] : provider.key;
380
+ return `Enabled ${name}${name === "video" ? ` (${vendor})` : ""}. Add ${key} to .env.local${name === "video" ? " and configure your app-owned providers/video-delivery.ts callback" : ""}, then restart npm run dev.`;
1750
381
  }
1751
382
 
1752
383
  // src/cli/doctor.ts
1753
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1754
- import { join as join10, resolve as resolve8 } from "path";
384
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
385
+ import { join as join4, resolve as resolve3 } from "path";
1755
386
  var REQUIRED_FILES = [
1756
387
  ".env.local",
1757
388
  "index.html",
1758
389
  "server.ts",
1759
390
  "providers.ts",
391
+ "providers/text.ts",
1760
392
  "src/main.tsx",
1761
393
  "stock.ts",
1762
394
  "tsconfig.json",
@@ -1764,8 +396,6 @@ var REQUIRED_FILES = [
1764
396
  ];
1765
397
  var REQUIRED_DEPENDENCIES = [
1766
398
  "@vanillaskyai/video",
1767
- "@ai-sdk/anthropic",
1768
- "ai",
1769
399
  "react",
1770
400
  "react-dom",
1771
401
  "@types/node",
@@ -1775,7 +405,7 @@ var REQUIRED_DEPENDENCIES = [
1775
405
  "typescript",
1776
406
  "vite"
1777
407
  ];
1778
- var PROVIDER_KEYS = /* @__PURE__ */ new Set(["ANTHROPIC_API_KEY", "XAI_API_KEY", "FAL_KEY", "PEXELS_API_KEY"]);
408
+ var PROVIDER_KEYS = /* @__PURE__ */ new Set(["ANTHROPIC_API_KEY", "GEMINI_API_KEY", "RUNWAY_API_KEY", "XAI_API_KEY", "FAL_KEY", "PEXELS_API_KEY", "VIDEO_STORAGE_TOKEN"]);
1779
409
  var CLIENT_ENV_PREFIXES = ["VITE", "NEXT_PUBLIC"].map((prefix) => `${prefix}_`);
1780
410
  function parseEnvironment(source) {
1781
411
  const result = /* @__PURE__ */ new Map();
@@ -1802,18 +432,18 @@ function isJsonObject(value) {
1802
432
  return value != null && typeof value === "object" && !Array.isArray(value);
1803
433
  }
1804
434
  function doctorVideoChatApp(cwdInput) {
1805
- const cwd = resolve8(cwdInput);
435
+ const cwd = resolve3(cwdInput);
1806
436
  const lines = [];
1807
437
  let ok = true;
1808
438
  for (const path of REQUIRED_FILES) {
1809
- if (!existsSync5(join10(cwd, path))) {
439
+ if (!existsSync3(join4(cwd, path))) {
1810
440
  ok = false;
1811
441
  lines.push(`MISSING ${path}`);
1812
442
  }
1813
443
  }
1814
444
  let manifest = {};
1815
445
  try {
1816
- const parsed = JSON.parse(readFileSync4(join10(cwd, "package.json"), "utf8"));
446
+ const parsed = JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf8"));
1817
447
  if (!isJsonObject(parsed)) throw new Error("expected an object");
1818
448
  manifest = parsed;
1819
449
  } catch {
@@ -1821,13 +451,16 @@ function doctorVideoChatApp(cwdInput) {
1821
451
  lines.push("MISSING valid package.json");
1822
452
  }
1823
453
  const dependencies = { ...stringMap2(manifest.dependencies), ...stringMap2(manifest.devDependencies) };
1824
- const missingDependencies = REQUIRED_DEPENDENCIES.filter((name) => !dependencies[name]);
454
+ const configuration = isJsonObject(manifest.vanillasky) ? manifest.vanillasky : {};
455
+ const textProvider = configuration.textProvider ?? "vercel";
456
+ const requiredDependencies = [...REQUIRED_DEPENDENCIES, ...textProvider === "vercel" ? ["ai", "@ai-sdk/anthropic"] : []];
457
+ const missingDependencies = requiredDependencies.filter((name) => !dependencies[name]);
1825
458
  for (const name of missingDependencies) {
1826
459
  ok = false;
1827
460
  lines.push(`MISSING dependency ${name}`);
1828
461
  }
1829
- const installed = (name) => existsSync5(join10(cwd, "node_modules", name, "package.json"));
1830
- for (const name of REQUIRED_DEPENDENCIES.filter((name2) => dependencies[name2] && !installed(name2))) {
462
+ const installed = (name) => existsSync3(join4(cwd, "node_modules", name, "package.json"));
463
+ for (const name of requiredDependencies.filter((name2) => dependencies[name2] && !installed(name2))) {
1831
464
  ok = false;
1832
465
  lines.push(`NOT INSTALLED ${name} \u2014 run npm install`);
1833
466
  }
@@ -1836,45 +469,61 @@ function doctorVideoChatApp(cwdInput) {
1836
469
  ok = false;
1837
470
  lines.push("MISSING video-chat dev/build scripts");
1838
471
  }
1839
- const ignore = existsSync5(join10(cwd, ".gitignore")) ? readFileSync4(join10(cwd, ".gitignore"), "utf8") : "";
472
+ const ignore = existsSync3(join4(cwd, ".gitignore")) ? readFileSync3(join4(cwd, ".gitignore"), "utf8") : "";
1840
473
  if (!effectivelyIgnoresLocalEnvironment(ignore)) {
1841
474
  ok = false;
1842
475
  lines.push("UNSAFE .env.local is not ignored");
1843
476
  }
1844
- const environment = existsSync5(join10(cwd, ".env.local")) ? parseEnvironment(readFileSync4(join10(cwd, ".env.local"), "utf8")) : /* @__PURE__ */ new Map();
477
+ const environment = existsSync3(join4(cwd, ".env.local")) ? parseEnvironment(readFileSync3(join4(cwd, ".env.local"), "utf8")) : /* @__PURE__ */ new Map();
478
+ const privateKeys = /* @__PURE__ */ new Set([...PROVIDER_KEYS, ...Array.isArray(configuration.requiredEnv) ? configuration.requiredEnv.filter((value) => typeof value === "string") : []]);
1845
479
  for (const name of environment.keys()) {
1846
- if (CLIENT_ENV_PREFIXES.some((prefix) => name.startsWith(prefix) && PROVIDER_KEYS.has(name.slice(prefix.length)))) {
480
+ if (CLIENT_ENV_PREFIXES.some((prefix) => name.startsWith(prefix) && privateKeys.has(name.slice(prefix.length)))) {
1847
481
  ok = false;
1848
482
  lines.push(`UNSAFE ${name} exposes a provider key to the browser`);
1849
483
  }
1850
484
  }
1851
- lines.push("READY templates + browser voice");
1852
- if (environment.get("ANTHROPIC_API_KEY")) lines.push("READY ANTHROPIC_API_KEY");
1853
- else {
1854
- ok = false;
1855
- lines.push("MISSING ANTHROPIC_API_KEY in .env.local");
485
+ lines.push("READY video chat + browser voice");
486
+ const requiredEnv = Array.isArray(configuration.requiredEnv) ? configuration.requiredEnv.filter((key) => typeof key === "string" && /^[A-Z][A-Z0-9_]*$/.test(key)) : textProvider === "native" ? ["GEMINI_API_KEY"] : textProvider === "vercel" ? ["ANTHROPIC_API_KEY"] : [];
487
+ for (const key of requiredEnv) {
488
+ if (environment.get(key)) lines.push(`READY ${key}`);
489
+ else {
490
+ ok = false;
491
+ lines.push(`MISSING ${key} in .env.local`);
492
+ }
1856
493
  }
1857
- const configuration = isJsonObject(manifest.vanillasky) ? manifest.vanillasky : {};
494
+ if (textProvider !== "native" && textProvider !== "vercel") lines.push("CHECK app-owned text callbacks and vanillasky.requiredEnv");
1858
495
  const enabled = Array.isArray(configuration.providers) ? configuration.providers : [];
496
+ const videoVendor = typeof configuration.videoVendor === "string" ? configuration.videoVendor : "fal";
497
+ const videoKey = { fal: "FAL_KEY", google: "GEMINI_API_KEY", runway: "RUNWAY_API_KEY" }[videoVendor];
1859
498
  for (const provider of [
1860
- { id: "speech", label: "generated speech", dependency: "@ai-sdk/xai", key: "XAI_API_KEY" },
1861
- { id: "video", label: "generated video + transcription", dependency: "@fal-ai/client", key: "FAL_KEY" }
499
+ { id: "speech", label: "generated speech", dependencies: ["@ai-sdk/xai", "ai"], key: "XAI_API_KEY" },
500
+ { id: "video", label: "generated video", dependencies: [], key: videoKey },
501
+ { id: "transcription", label: "transcription", dependencies: [], key: "FAL_KEY" }
1862
502
  ]) {
1863
- const configured = enabled.includes(provider.id) && existsSync5(join10(cwd, "providers.ts")) && existsSync5(join10(cwd, "providers", `${provider.id}.ts`)) && dependencies[provider.dependency] && installed(provider.dependency);
503
+ const configured = enabled.includes(provider.id) && existsSync3(join4(cwd, "providers.ts")) && existsSync3(join4(cwd, "providers", `${provider.id}.ts`)) && provider.dependencies.every((name) => dependencies[name] && installed(name));
1864
504
  if (!configured) {
1865
505
  lines.push(`OPTIONAL ${provider.label} \u2014 run npx vanillasky providers add ${provider.id}`);
1866
- } else if (!environment.get(provider.key)) {
506
+ } else if (provider.id === "video" && !videoKey) {
507
+ lines.push("CHECK generated video \u2014 verify your custom callback, model policy and delivery in providers/video.ts");
508
+ } else if (provider.key && !environment.get(provider.key)) {
1867
509
  lines.push(`OPTIONAL ${provider.label} \u2014 add ${provider.key} to .env.local`);
510
+ } else if (provider.id === "video" && (!validUploadUrl(environment.get("VIDEO_UPLOAD_URL")) || !environment.get("VIDEO_STORAGE_TOKEN"))) {
511
+ lines.push("CHECK generated video \u2014 configure VIDEO_UPLOAD_URL + VIDEO_STORAGE_TOKEN or your app-owned delivery callback");
1868
512
  } else {
1869
- lines.push(`READY ${provider.label}`);
513
+ lines.push(`READY ${provider.label}${provider.id === "video" ? ` (${videoVendor}; configuration only, no paid probe)` : ""}`);
1870
514
  }
1871
515
  }
1872
516
  lines.push(environment.get("PEXELS_API_KEY") ? "READY stock media" : "OPTIONAL stock media \u2014 add PEXELS_API_KEY");
1873
- if (existsSync5(join10(cwd, "vanillasky", "templates"))) {
1874
- lines.push("CHECK custom templates with npx vanillasky templates check");
1875
- }
1876
517
  return { ok, lines };
1877
518
  }
519
+ function validUploadUrl(value) {
520
+ try {
521
+ const url = new URL(value ?? "");
522
+ return url.protocol === "https:" && !url.username && !url.password;
523
+ } catch {
524
+ return false;
525
+ }
526
+ }
1878
527
 
1879
528
  // src/cli/index.ts
1880
529
  function help() {
@@ -1882,33 +531,10 @@ function help() {
1882
531
  "VanillaSky video chat",
1883
532
  "",
1884
533
  "Usage:",
1885
- " vanillasky init",
534
+ " vanillasky init [--native]",
1886
535
  " vanillasky doctor",
1887
- " vanillasky providers add <speech|video>",
1888
- " vanillasky templates <command>",
1889
- "",
1890
- "Template commands:",
1891
- " vanillasky templates list [--builtin] [--json]",
1892
- " vanillasky templates describe <template> [--builtin] [--json]",
1893
- " vanillasky templates add <template...> [--dry-run|--diff] [--overwrite]",
1894
- " vanillasky templates add --all [--dry-run|--diff] [--overwrite]",
1895
- " vanillasky templates sync [--check]",
1896
- " vanillasky templates check",
1897
- " vanillasky templates create <id>"
1898
- ].join("\n");
1899
- }
1900
- function templateHelp() {
1901
- return [
1902
- "VanillaSky source-owned templates",
1903
- "",
1904
- "Usage:",
1905
- " vanillasky templates list [--builtin] [--json]",
1906
- " vanillasky templates describe <template> [--builtin] [--json]",
1907
- " vanillasky templates add <template...> [--dry-run|--diff] [--overwrite]",
1908
- " vanillasky templates add --all [--dry-run|--diff] [--overwrite]",
1909
- " vanillasky templates sync [--check]",
1910
- " vanillasky templates check",
1911
- " vanillasky templates create <id>"
536
+ " vanillasky providers add <speech|transcription>",
537
+ " vanillasky providers add video [fal|google|runway|custom]"
1912
538
  ].join("\n");
1913
539
  }
1914
540
  function unknownOption(command, args, allowed) {
@@ -1916,23 +542,6 @@ function unknownOption(command, args, allowed) {
1916
542
  const unknown = args.find((arg) => arg.startsWith("-") && !known.has(arg));
1917
543
  return unknown ? `Unknown ${command} option: ${unknown}` : void 0;
1918
544
  }
1919
- function compactDiff(change) {
1920
- const before = change.before?.split("\n") ?? [];
1921
- const after = change.after.split("\n");
1922
- let prefix = 0;
1923
- while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix += 1;
1924
- let suffix = 0;
1925
- while (suffix < before.length - prefix && suffix < after.length - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix += 1;
1926
- const removed = before.slice(prefix, before.length - suffix);
1927
- const added = after.slice(prefix, after.length - suffix);
1928
- return [
1929
- `--- ${change.path}`,
1930
- `+++ ${change.path}`,
1931
- `@@ -${prefix + 1},${removed.length} +${prefix + 1},${added.length} @@`,
1932
- ...removed.map((line) => `-${line}`),
1933
- ...added.map((line) => `+${line}`)
1934
- ].join("\n");
1935
- }
1936
545
  function sanitizeTerminalOutput(value) {
1937
546
  return [...value].filter((character) => {
1938
547
  const code = character.charCodeAt(0);
@@ -1942,24 +551,27 @@ function sanitizeTerminalOutput(value) {
1942
551
  function runVanillaSkyCli(argv, environment = {}) {
1943
552
  const output = environment.write ?? console.log;
1944
553
  const write = (line) => output(sanitizeTerminalOutput(line));
1945
- const cwd = resolve9(environment.cwd ?? process.cwd());
554
+ const cwd = resolve4(environment.cwd ?? process.cwd());
1946
555
  const [rootCommand, ...rootArgs] = argv;
1947
556
  if (rootCommand === "init") {
1948
557
  return (async () => {
1949
558
  try {
1950
- const unknown = unknownOption("init", rootArgs, []);
559
+ const unknown = unknownOption("init", rootArgs, ["--native"]);
1951
560
  if (unknown) throw new Error(unknown);
1952
- if (rootArgs.length > 0) throw new Error(`Unexpected init argument: ${rootArgs[0]}`);
561
+ const unexpected = rootArgs.find((argument) => argument !== "--native");
562
+ if (unexpected || rootArgs.length > 1) throw new Error(`Unexpected init argument: ${unexpected ?? rootArgs[1]}`);
1953
563
  const result = await initVideoChatApp({
1954
564
  cwd,
565
+ native: rootArgs.includes("--native") ? true : void 0,
1955
566
  starterRoot: environment.starterRoot,
1956
567
  sdkSpec: environment.sdkSpec ?? process.env.npm_config_package,
1957
568
  installDependencies: environment.installDependencies
1958
569
  });
1959
- write(result.initialized ? "Video chat initialized with packaged templates and browser voice." : "Video chat is already initialized; dependencies checked.");
570
+ write(result.initialized ? "Video chat initialized with chapter introductions and browser voice." : "Video chat is already initialized; dependencies checked.");
1960
571
  const health = doctorVideoChatApp(cwd);
1961
572
  health.lines.forEach(write);
1962
- write(health.ok ? "Ready. Run: npm run dev" : health.lines.some((line) => line === "MISSING ANTHROPIC_API_KEY in .env.local") ? "Add ANTHROPIC_API_KEY to .env.local. Then run: npm run dev" : "Next: fix the missing setup items above, then run npm run dev.");
573
+ const requiredKey = health.lines.find((line) => /^MISSING {2}(ANTHROPIC_API_KEY|GEMINI_API_KEY) in \.env\.local$/.test(line))?.split(" ")[1].split(" ")[0];
574
+ write(health.ok ? "Ready. Run: npm run dev" : requiredKey ? `Add ${requiredKey} to .env.local. Then run: npm run dev` : "Next: fix the missing setup items above, then run npm run dev.");
1963
575
  return 0;
1964
576
  } catch (error) {
1965
577
  write(error instanceof Error ? error.message : String(error));
@@ -1970,8 +582,16 @@ function runVanillaSkyCli(argv, environment = {}) {
1970
582
  if (rootCommand === "providers") {
1971
583
  return (async () => {
1972
584
  try {
1973
- if (rootArgs.length !== 2 || rootArgs[0] !== "add") throw new Error("Usage: vanillasky providers add <speech|video>");
1974
- write(await addVideoChatProvider(rootArgs[1], { cwd, starterRoot: environment.starterRoot, installDependencies: environment.installDependencies }));
585
+ const usage = "Usage: vanillasky providers add <speech|transcription> or vanillasky providers add video [fal|google|runway|custom]";
586
+ if (rootArgs[0] !== "add" || rootArgs.length < 2 || rootArgs.length > 3) throw new Error(usage);
587
+ const vendor = rootArgs[2];
588
+ if (vendor !== void 0 && (rootArgs[1] !== "video" || !["fal", "google", "runway", "custom"].includes(vendor))) throw new Error(usage);
589
+ write(await addVideoChatProvider(rootArgs[1], {
590
+ cwd,
591
+ starterRoot: environment.starterRoot,
592
+ installDependencies: environment.installDependencies,
593
+ vendor
594
+ }));
1975
595
  return 0;
1976
596
  } catch (error) {
1977
597
  write(error instanceof Error ? error.message : String(error));
@@ -1993,227 +613,8 @@ function runVanillaSkyCli(argv, environment = {}) {
1993
613
  for (const line of result.lines) write(line);
1994
614
  return result.ok ? 0 : 1;
1995
615
  }
1996
- if (rootCommand !== "templates") {
1997
- const removed = /* @__PURE__ */ new Set(["list", "describe", "add", "sync", "check", "create"]);
1998
- if (rootCommand && removed.has(rootCommand)) {
1999
- write(`Template commands moved. Use: vanillasky templates ${argv.join(" ")}`);
2000
- return 1;
2001
- }
2002
- write(help());
2003
- return rootCommand == null || rootCommand === "help" || rootCommand === "--help" || rootCommand === "-h" ? 0 : 1;
2004
- }
2005
- const [command, ...args] = rootArgs;
2006
- if (command == null || command === "help" || command === "--help" || command === "-h") {
2007
- write(templateHelp());
2008
- return 0;
2009
- }
2010
- if (command === "create") {
2011
- return (async () => {
2012
- try {
2013
- const unknown = unknownOption("create", args, []);
2014
- if (unknown) throw new Error(unknown);
2015
- if (args.length === 0) throw new Error("Choose one template id. Usage: vanillasky templates create <id>.");
2016
- if (args.length > 1) throw new Error(`Unexpected create argument: ${args[1]}`);
2017
- const id = args[0];
2018
- const result = await createTemplate({ cwd, id });
2019
- write(`Created template: ${result.path}`);
2020
- write(`Synced ${result.templates} template${result.templates === 1 ? "" : "s"} to vanillasky/index.ts and vanillasky/server.ts.`);
2021
- write(`Source: ${result.path}`);
2022
- write("Validate templates: vanillasky templates check");
2023
- for (const warning of result.warnings ?? []) write(`Warning: ${warning}`);
2024
- return 0;
2025
- } catch (error) {
2026
- write(error instanceof Error ? error.message : String(error));
2027
- return 1;
2028
- }
2029
- })();
2030
- }
2031
- if (command === "check") {
2032
- return (async () => {
2033
- try {
2034
- const unknown = unknownOption("check", args, []);
2035
- if (unknown) throw new Error(unknown);
2036
- if (args.length > 0) throw new Error(`Unexpected check argument: ${args[0]}`);
2037
- const result = await checkTemplates({ cwd });
2038
- write(`Checked ${result.templates} template${result.templates === 1 ? "" : "s"}, ${result.examples} example${result.examples === 1 ? "" : "s"}, and ${result.renders} deterministic renders.`);
2039
- write("Generated browser/server registries match. Application imports were not inspected.");
2040
- return 0;
2041
- } catch (error) {
2042
- write(error instanceof Error ? error.message : String(error));
2043
- return 1;
2044
- }
2045
- })();
2046
- }
2047
- if (command === "sync") {
2048
- return (async () => {
2049
- try {
2050
- const unknown = unknownOption("sync", args, ["--check"]);
2051
- if (unknown) throw new Error(unknown);
2052
- const result = await syncTemplates({ cwd, check: args.includes("--check") });
2053
- if (args.includes("--check")) {
2054
- write("Template entrypoints are up to date.");
2055
- } else {
2056
- const count = result.templates.length;
2057
- write(`Synced ${count} template${count === 1 ? "" : "s"} to vanillasky/index.ts and vanillasky/server.ts.`);
2058
- }
2059
- for (const warning of result.warnings) write(`Warning: ${warning}`);
2060
- return 0;
2061
- } catch (error) {
2062
- write(error instanceof Error ? error.message : String(error));
2063
- return 1;
2064
- }
2065
- })();
2066
- }
2067
- if (command === "list") {
2068
- const unknown = unknownOption("list", args, ["--builtin", "--json"]);
2069
- if (unknown) {
2070
- write(unknown);
2071
- return 1;
2072
- }
2073
- const positional = args.filter((arg) => !["--builtin", "--json"].includes(arg));
2074
- if (positional.length > 0) {
2075
- write(`Unexpected list argument: ${positional[0]}`);
2076
- return 1;
2077
- }
2078
- const render = (items) => {
2079
- if (args.includes("--json")) write(JSON.stringify(items.map((item2) => ({
2080
- id: item2.id,
2081
- title: item2.title,
2082
- origin: item2.origin,
2083
- status: item2.status,
2084
- useWhen: item2.useWhen,
2085
- summary: item2.summary
2086
- })), null, 2));
2087
- else {
2088
- write("ID ORIGIN STATUS USE WHEN");
2089
- for (const item2 of items) write(`${item2.id} ${item2.origin} ${item2.status} ${item2.useWhen}`);
2090
- }
2091
- return 0;
2092
- };
2093
- if (args.includes("--builtin") || !hasProjectTemplateSources(cwd)) return render(builtinTemplateCatalog());
2094
- return effectiveTemplateCatalog(cwd).then(render).catch((error) => {
2095
- write(error instanceof Error ? error.message : String(error));
2096
- return 1;
2097
- });
2098
- }
2099
- if (command === "add") {
2100
- const allowed = ["--all", "--dry-run", "--diff", "--overwrite"];
2101
- const unknown = unknownOption("add", args, allowed);
2102
- if (unknown) {
2103
- write(unknown);
2104
- return 1;
2105
- }
2106
- const dryRun = args.includes("--dry-run");
2107
- const showDiff = args.includes("--diff");
2108
- if (dryRun && showDiff) {
2109
- write("Choose either --dry-run or --diff.");
2110
- return 1;
2111
- }
2112
- const addAll = args.includes("--all");
2113
- const positional = args.filter((arg) => !allowed.includes(arg));
2114
- if (addAll && positional.length > 0) {
2115
- write("Use either named templates or --all.");
2116
- return 1;
2117
- }
2118
- const names = addAll ? listRegistryTemplates().map(({ id }) => id) : positional;
2119
- if (names.length === 0) {
2120
- write("Choose at least one template. Run `vanillasky templates list` to see the catalog.");
2121
- return 1;
2122
- }
2123
- return (async () => {
2124
- try {
2125
- const preview = dryRun || showDiff;
2126
- const result = addRegistryTemplates({
2127
- cwd,
2128
- names,
2129
- dryRun: preview,
2130
- overwrite: args.includes("--overwrite")
2131
- });
2132
- const changes = preview ? [...result.changes, ...await previewTemplateSync({ cwd, templates: result.previewTemplates })] : result.changes;
2133
- if (showDiff) {
2134
- if (changes.length === 0) write("No changes.");
2135
- else for (const change of changes) write(compactDiff(change));
2136
- return 0;
2137
- }
2138
- if (dryRun) {
2139
- if (result.added.length > 0) write(`Would add ${result.added.join(", ")}.`);
2140
- if (result.updated.length > 0) write(`Would update ${result.updated.join(", ")}.`);
2141
- if (changes.length === 0) write("No changes.");
2142
- else for (const change of changes) write(`${change.action} ${change.path}`);
2143
- return 0;
2144
- }
2145
- await syncTemplates({ cwd });
2146
- if (result.added.length > 0) write(`Added ${result.added.join(", ")}.`);
2147
- if (result.updated.length > 0) write(`Updated ${result.updated.join(", ")}.`);
2148
- if (result.added.length === 0 && result.updated.length === 0) {
2149
- write("Those templates are already installed.");
2150
- }
2151
- const templateSources = result.files.filter((file) => /^vanillasky\/templates\/.*\.tsx$/.test(file));
2152
- if (templateSources.length > 0) {
2153
- write(`${templateSources.length === 1 ? "Template source" : "Template sources"}: ${templateSources.join(", ")}`);
2154
- }
2155
- return 0;
2156
- } catch (error) {
2157
- write(error instanceof Error ? error.message : String(error));
2158
- return 1;
2159
- }
2160
- })();
2161
- }
2162
- if (command === "describe") {
2163
- const unknown = unknownOption("describe", args, ["--builtin", "--json"]);
2164
- if (unknown) {
2165
- write(unknown);
2166
- return 1;
2167
- }
2168
- const names = args.filter((arg) => arg !== "--json" && arg !== "--builtin");
2169
- if (names.length > 1) {
2170
- write(`Unexpected describe argument: ${names[1]}`);
2171
- return 1;
2172
- }
2173
- const name = names[0];
2174
- const render = (item2) => {
2175
- if (!item2) {
2176
- write(`Unknown template: ${name ?? ""}. Run \`vanillasky templates list\` to see the catalog.`);
2177
- return 1;
2178
- }
2179
- if (args.includes("--json")) {
2180
- write(JSON.stringify(item2, null, 2));
2181
- return 0;
2182
- }
2183
- write(`${item2.id} \u2014 ${item2.title}`);
2184
- if (item2.summary) write(item2.summary);
2185
- write(`Origin ${item2.origin}`);
2186
- write(`Status ${item2.status}`);
2187
- write(`Use when ${item2.planner.useWhen}`);
2188
- if (item2.planner.avoidWhen) write(`Avoid when ${item2.planner.avoidWhen}`);
2189
- if (item2.family) write(`Family ${item2.family}`);
2190
- if (item2.jobs) write(`Jobs ${item2.jobs.join(", ")}`);
2191
- if (item2.register) write(`Register ${item2.register}`);
2192
- const duration = [
2193
- item2.duration.min == null ? void 0 : `${item2.duration.min}s minimum`,
2194
- item2.duration.preferred == null ? void 0 : `${item2.duration.preferred}s preferred`
2195
- ].filter(Boolean).join(", ");
2196
- if (duration) write(`Duration ${duration}`);
2197
- write(`Generated browser ${item2.generated.browser.path} ${item2.generated.browser.current == null ? "not applicable" : item2.generated.browser.current ? "current" : "stale"}`);
2198
- write(`Generated server ${item2.generated.server.path} ${item2.generated.server.current == null ? "not applicable" : item2.generated.server.current ? "current" : "stale"}`);
2199
- write(`Generated files ${item2.generated.current == null ? "not applicable to packaged templates" : item2.generated.current ? "current" : "stale"}`);
2200
- write("Application wiring not inspected or verified");
2201
- for (const [fieldName, property] of Object.entries(item2.schema.properties)) {
2202
- const required = item2.schema.required?.includes(fieldName) === true;
2203
- const notation = templateVariableNotation(property, required).replace(/!$/, "");
2204
- write(`${fieldName} ${notation} ${required ? "required" : "optional"} ${property.description ?? ""}`);
2205
- }
2206
- return 0;
2207
- };
2208
- if (!name) return render(void 0);
2209
- if (args.includes("--builtin") || !hasProjectTemplateSources(cwd)) return render(findBuiltinTemplate(name));
2210
- return findEffectiveTemplate(cwd, name).then(render).catch((error) => {
2211
- write(error instanceof Error ? error.message : String(error));
2212
- return 1;
2213
- });
2214
- }
2215
- write(templateHelp());
2216
- return 1;
616
+ write(help());
617
+ return rootCommand == null || rootCommand === "help" || rootCommand === "--help" || rootCommand === "-h" ? 0 : 1;
2217
618
  }
2218
619
 
2219
620
  // src/cli.ts