@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.
- package/CHANGELOG.md +74 -1
- package/PUBLIC-API.md +45 -438
- package/README.md +57 -96
- package/dist/builtin-metadata-OT6V7TB4.js +8 -0
- package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
- package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
- package/dist/chunk-4G4JBMCM.js +37 -0
- package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
- package/dist/chunk-666HGTVZ.js +41 -0
- package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
- package/dist/{chunk-5BSJLT6H.js → chunk-E7FHBLAC.js} +16 -29
- package/dist/chunk-HIQM4J3U.js +30 -0
- package/dist/chunk-ISWQ6T5X.js +36 -0
- package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
- package/dist/chunk-KMVRBUL5.js +50 -0
- package/dist/chunk-NM4CXXZY.js +25 -0
- package/dist/{chunk-44WND2VP.js → chunk-V2CP7PVY.js} +19 -63
- package/dist/{chunk-3O7OMMMF.js → chunk-WZESNEPT.js} +0 -1
- package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
- package/dist/cinema-media-YZ2UANJG.js +365 -0
- package/dist/cli.js +154 -1753
- package/dist/{compose-video-BH5K6XWT.js → compose-video-H2YUQDUY.js} +4 -4
- package/dist/{events-B4YCc4vc.d.ts → events-BZAfl0Dm.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -6
- package/dist/preload-media-23JXUFCF.js +77 -0
- package/dist/react.d.ts +39 -15
- package/dist/react.js +1350 -739
- package/dist/scene-validation-SGLLLFY5.js +9 -0
- package/dist/server.d.ts +67 -117
- package/dist/server.js +262 -614
- package/dist/test.d.ts +2 -2
- package/dist/test.js +21 -21
- package/dist/{text-stream-LD364KBI.js → text-stream-SOVLYR2L.js} +2 -2
- package/dist/{types-BqB8zC9u.d.ts → types-CG-kPI81.d.ts} +3 -1
- package/dist/{types-DdZw4GRQ.d.ts → types-ht-Zw3Wv.d.ts} +1 -1
- package/docs/agent-integration.md +19 -64
- package/docs/architecture.md +82 -87
- package/docs/customization.md +49 -66
- package/docs/development.md +18 -17
- package/docs/errors.md +4 -4
- package/docs/getting-started.md +74 -93
- package/docs/media-and-audio.md +9 -7
- package/docs/performance.md +11 -6
- package/docs/persistence.md +3 -7
- package/docs/production.md +4 -15
- package/docs/prompt-and-input.md +1 -2
- package/docs/provider-integration.md +134 -181
- package/docs/reference/protocol.md +5 -14
- package/docs/reference/provider-adapters.md +21 -13
- package/docs/security.md +1 -14
- package/docs/testing.md +56 -123
- package/package.json +5 -27
- package/starters/video-chat/.env.example +12 -4
- package/starters/video-chat/.env.native.example +13 -0
- package/starters/video-chat/README.md +72 -11
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/providers/text-native.ts +94 -0
- package/starters/video-chat/providers/text.ts +19 -0
- package/starters/video-chat/providers/transcription.ts +32 -0
- package/starters/video-chat/providers/video-custom.ts +45 -0
- package/starters/video-chat/providers/video-delivery.ts +57 -0
- package/starters/video-chat/providers/video-google.ts +48 -0
- package/starters/video-chat/providers/video-job.ts +103 -0
- package/starters/video-chat/providers/video-runway.ts +45 -0
- package/starters/video-chat/providers/video.ts +39 -63
- package/starters/video-chat/server.ts +3 -29
- package/starters/video-chat/vite.config.ts +2 -8
- package/styles/video-chat.css +8 -105
- package/dist/builtin-server-W4PLJUZ5.js +0 -8
- package/dist/catalog-types-WTbLP6Jh.d.ts +0 -78
- package/dist/check-runtime.d.ts +0 -15
- package/dist/check-runtime.js +0 -96
- package/dist/chunk-2E6T633S.js +0 -27
- package/dist/chunk-4YM2M62S.js +0 -13
- package/dist/chunk-4ZJLPHBV.js +0 -684
- package/dist/chunk-5JBMYQP6.js +0 -156
- package/dist/chunk-73NTSFFI.js +0 -81
- package/dist/chunk-EGVQODKU.js +0 -83
- package/dist/chunk-HFVNAPHZ.js +0 -35
- package/dist/chunk-IIN5M5HW.js +0 -697
- package/dist/chunk-IQMYK5DX.js +0 -133
- package/dist/chunk-IR44XKBI.js +0 -46
- package/dist/chunk-JKVOBTRO.js +0 -145
- package/dist/chunk-LVM5Q2DL.js +0 -68
- package/dist/chunk-M4QTEJTK.js +0 -709
- package/dist/chunk-QSBDB4J2.js +0 -16
- package/dist/chunk-R3XAOMKP.js +0 -29
- package/dist/chunk-SPVTJH3F.js +0 -24
- package/dist/chunk-YAHT3LST.js +0 -663
- package/dist/chunk-ZD2VTUYR.js +0 -28
- package/dist/cinema-media-HYCDG65Z.js +0 -11
- package/dist/comparison-XXAP4S4J.js +0 -36
- package/dist/editorial-timeline-VUQC6KPA.js +0 -42
- package/dist/key-figure-4TJK7HLT.js +0 -29
- package/dist/kit-DrRpdn0p.d.ts +0 -79
- package/dist/mobile-message-FIHAO6XC.js +0 -49
- package/dist/preload-media-LJXWKTWG.js +0 -57
- package/dist/quote-GRPIYKQJ.js +0 -32
- package/dist/system-prompt-AG26KJAA.js +0 -12
- package/dist/template-catalog.d.ts +0 -400
- package/dist/template-catalog.js +0 -6
- package/dist/templates.d.ts +0 -25
- package/dist/templates.js +0 -102
- package/dist/validate-4OUD2CLX.js +0 -10
- package/docs/concepts.md +0 -107
- package/docs/custom-templates.md +0 -346
- package/docs/immersive-interface.md +0 -86
- package/docs/maintainers/cinematic-migration.md +0 -38
- package/docs/motion-and-effects.md +0 -109
- package/docs/reference/design-system.html +0 -125
- package/docs/responsive-orientation.md +0 -37
- package/docs/streaming-protocol.md +0 -15
- package/examples/custom-template/README.md +0 -19
- package/examples/custom-template/minimal-text.tsx +0 -82
- package/examples/custom-template/structured-data.tsx +0 -104
- package/registry/items/backgrounds.json +0 -70
- package/registry/items/chapterTitle.json +0 -80
- package/registry/items/cinemaMedia.json +0 -136
- package/registry/items/comparison.json +0 -168
- package/registry/items/editorialTimeline.json +0 -172
- package/registry/items/keyFigure.json +0 -162
- package/registry/items/mobileMessage.json +0 -153
- package/registry/items/motion.json +0 -45
- package/registry/items/quote.json +0 -161
- package/registry/items/template-context.json +0 -31
- package/registry/items/theme.json +0 -47
- 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
|
|
4
|
+
import { resolve as resolve4 } from "path";
|
|
31
5
|
|
|
32
|
-
// src/cli/
|
|
33
|
-
import {
|
|
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 =
|
|
126
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
1527
127
|
const candidates = [
|
|
1528
|
-
|
|
1529
|
-
|
|
128
|
+
resolve2(moduleDirectory, "../../starters/video-chat"),
|
|
129
|
+
resolve2(moduleDirectory, "../starters/video-chat")
|
|
1530
130
|
];
|
|
1531
|
-
const found = candidates.find((candidate) =>
|
|
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(
|
|
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 ${
|
|
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(
|
|
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 =
|
|
1561
|
-
const existing =
|
|
1562
|
-
const starter = readJson(
|
|
1563
|
-
const packageName = typeof existing.name === "string" && existing.name.trim() ? existing.name :
|
|
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
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
219
|
+
writeFileSync(temporary, contents, { encoding: "utf8", flag: "wx", mode: 420 });
|
|
220
|
+
renameSync(temporary, path);
|
|
1616
221
|
} finally {
|
|
1617
|
-
if (
|
|
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 =
|
|
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 =
|
|
1636
|
-
const starterRoot =
|
|
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,
|
|
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 (!
|
|
1643
|
-
planned.set("providers.ts",
|
|
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
|
-
|
|
259
|
+
existsSync(join2(cwd, ".gitignore")) ? readFileSync(join2(cwd, ".gitignore"), "utf8") : void 0
|
|
1648
260
|
));
|
|
1649
|
-
if (!
|
|
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 (!
|
|
267
|
+
if (!existsSync(destination)) {
|
|
1656
268
|
changed = true;
|
|
1657
269
|
continue;
|
|
1658
270
|
}
|
|
1659
|
-
const current =
|
|
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 (!
|
|
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
|
|
1682
|
-
import { existsSync as
|
|
1683
|
-
import { dirname as
|
|
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: {
|
|
1686
|
-
video: {
|
|
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
|
|
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 (!
|
|
1704
|
-
const manifest = JSON.parse(
|
|
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 (
|
|
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
|
|
1718
|
-
if (
|
|
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,
|
|
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 (!
|
|
1734
|
-
const
|
|
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
|
-
|
|
1737
|
-
|
|
366
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
367
|
+
writeFileSync2(temporary, contents, { flag: "wx" });
|
|
1738
368
|
try {
|
|
1739
|
-
|
|
369
|
+
renameSync2(temporary, path);
|
|
1740
370
|
} finally {
|
|
1741
|
-
if (
|
|
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
|
-
|
|
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
|
|
1754
|
-
import { join as
|
|
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 =
|
|
435
|
+
const cwd = resolve3(cwdInput);
|
|
1806
436
|
const lines = [];
|
|
1807
437
|
let ok = true;
|
|
1808
438
|
for (const path of REQUIRED_FILES) {
|
|
1809
|
-
if (!
|
|
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(
|
|
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
|
|
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) =>
|
|
1830
|
-
for (const name of
|
|
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 =
|
|
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 =
|
|
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) &&
|
|
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
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
-
|
|
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",
|
|
1861
|
-
{ id: "video", label: "generated video
|
|
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) &&
|
|
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 (
|
|
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|
|
|
1888
|
-
" vanillasky
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1997
|
-
|
|
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
|