@mevdragon/vidfarm-devcli 0.2.6 → 0.2.8
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/GETTING_STARTED.developers.md +25 -0
- package/README.md +44 -3
- package/SKILL.developer.md +147 -11
- package/dist/src/account-pages.js +1 -1
- package/dist/src/app.js +380 -13
- package/dist/src/cli.js +246 -59
- package/dist/src/config.js +4 -5
- package/dist/src/context.js +16 -0
- package/dist/src/db.js +67 -13
- package/dist/src/homepage.js +33 -1
- package/dist/src/lib/template-paths.js +10 -4
- package/dist/src/registry.js +9 -2
- package/dist/src/services/auth.js +2 -6
- package/dist/src/services/job-logs.js +4 -4
- package/dist/src/services/providers.js +470 -0
- package/dist/src/services/storage.js +13 -0
- package/dist/src/services/template-certification.js +16 -2
- package/dist/src/services/template-loader.js +17 -1
- package/dist/src/services/template-sources.js +338 -17
- package/{templates/template_0000 → dist/templates/vidfarm_template_0000}/src/remotion/Root.js +1 -0
- package/dist/templates/{template_0000 → vidfarm_template_0000}/src/template.js +9 -3
- package/package.json +4 -3
- package/templates/{template_0000 → vidfarm_template_0000}/README.md +23 -0
- package/templates/{template_0000 → vidfarm_template_0000}/package.json +0 -1
- package/templates/{template_0000 → vidfarm_template_0000}/src/sdk.ts +18 -0
- package/templates/{template_0000 → vidfarm_template_0000}/src/template.js +9 -3
- package/templates/{template_0000 → vidfarm_template_0000}/src/template.ts +10 -3
- package/templates/vidfarm_template_0000/tmp/solobacterium-moorei-slideshow.request.json +31 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/lib/images.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/remotion/index.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/sdk.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/style-options.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/template-dna.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/SKILL.md +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/Abel-Regular.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/DMSerifDisplay-Regular.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/Montserrat[wght].ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/SourceCodePro[wght].ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/TikTokSans-SemiBold.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/Yesteryear-Regular.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/composition.json +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/research/preview/.gitkeep +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/research/source_notes.md +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/lib/images.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/lib/images.ts +0 -0
- /package/{dist/templates/template_0000 → templates/vidfarm_template_0000}/src/remotion/Root.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/remotion/Root.tsx +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/remotion/index.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/remotion/index.tsx +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/sdk.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/style-options.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/style-options.ts +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/template-dna.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/template-dna.ts +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/template.config.json +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/tsconfig.json +0 -0
|
@@ -115,6 +115,19 @@ export class StorageService {
|
|
|
115
115
|
}
|
|
116
116
|
return readFileSync(path.join(this.localRoot, key), "utf8");
|
|
117
117
|
}
|
|
118
|
+
async readBuffer(key) {
|
|
119
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
120
|
+
const response = await this.s3.send(new GetObjectCommand({
|
|
121
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
122
|
+
Key: key
|
|
123
|
+
}));
|
|
124
|
+
if (!response.Body) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
128
|
+
}
|
|
129
|
+
return readFileSync(path.join(this.localRoot, key));
|
|
130
|
+
}
|
|
118
131
|
async listKeys(prefix) {
|
|
119
132
|
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
120
133
|
const keys = [];
|
|
@@ -13,11 +13,17 @@ export class TemplateCertificationService {
|
|
|
13
13
|
typeof template.about.visual_dna === "string" &&
|
|
14
14
|
Array.isArray(template.about.preview_media) &&
|
|
15
15
|
template.about.preview_media.every((entry) => typeof entry === "string") &&
|
|
16
|
-
typeof template.about.link_to_original === "string"
|
|
16
|
+
typeof template.about.link_to_original === "string" &&
|
|
17
|
+
typeof template.about.proposal_generator === "string" &&
|
|
18
|
+
Array.isArray(template.about.sample_prompts) &&
|
|
19
|
+
template.about.sample_prompts.every((entry) => typeof entry === "string") &&
|
|
20
|
+
template.about.sample_prompts.length > 0), "Template must define id, slugId, version, and complete about metadata, including proposal_generator and sample prompts.", {
|
|
17
21
|
templateId: template.id,
|
|
18
22
|
slugId: template.slugId,
|
|
19
23
|
version: template.version,
|
|
20
|
-
previewMediaCount: template.about?.preview_media?.length ?? 0
|
|
24
|
+
previewMediaCount: template.about?.preview_media?.length ?? 0,
|
|
25
|
+
hasProposalGenerator: typeof template.about?.proposal_generator === "string" && template.about.proposal_generator.length > 0,
|
|
26
|
+
samplePromptCount: template.about?.sample_prompts?.length ?? 0
|
|
21
27
|
}));
|
|
22
28
|
checks.push(this.check("template.operations", Object.keys(template.operations).length > 0, "Template must define at least one operation."));
|
|
23
29
|
checks.push(this.checkSkill(skillPath));
|
|
@@ -124,6 +130,10 @@ function createSmokeContext(templateId) {
|
|
|
124
130
|
providerCalls += 1;
|
|
125
131
|
return { text: "certification text", usage: { inputTokens: 1, outputTokens: 1, costUsd: 0 } };
|
|
126
132
|
},
|
|
133
|
+
async generateSpeech() {
|
|
134
|
+
providerCalls += 1;
|
|
135
|
+
return { bytes: Uint8Array.from([0, 1, 2, 3]), contentType: "audio/mpeg", usage: { inputTokens: 1, outputTokens: 1, costUsd: 0 } };
|
|
136
|
+
},
|
|
127
137
|
async generateImage() {
|
|
128
138
|
providerCalls += 1;
|
|
129
139
|
return { bytes: MOCK_PNG_BYTES, contentType: "image/png", revisedPrompt: null };
|
|
@@ -136,6 +146,10 @@ function createSmokeContext(templateId) {
|
|
|
136
146
|
maxWidthPercent: 80,
|
|
137
147
|
justification: "Certification mock layout."
|
|
138
148
|
};
|
|
149
|
+
},
|
|
150
|
+
async transcribeSpeech() {
|
|
151
|
+
providerCalls += 1;
|
|
152
|
+
return { text: "certification transcript", usage: { inputTokens: 1, outputTokens: 1, costUsd: 0 } };
|
|
139
153
|
}
|
|
140
154
|
},
|
|
141
155
|
remotion: {
|
|
@@ -4,11 +4,22 @@ export async function loadTemplateFromModule(modulePath) {
|
|
|
4
4
|
const candidates = Object.values(loaded);
|
|
5
5
|
for (const candidate of candidates) {
|
|
6
6
|
if (isTemplateDefinition(candidate)) {
|
|
7
|
-
return candidate;
|
|
7
|
+
return normalizeTemplateDefinition(candidate);
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
throw new Error(`No template definition export found in ${modulePath}`);
|
|
11
11
|
}
|
|
12
|
+
function normalizeTemplateDefinition(template) {
|
|
13
|
+
return {
|
|
14
|
+
...template,
|
|
15
|
+
about: {
|
|
16
|
+
...template.about,
|
|
17
|
+
// Older runtime template releases may predate proposal metadata fields.
|
|
18
|
+
proposal_generator: template.about.proposal_generator ?? "",
|
|
19
|
+
sample_prompts: template.about.sample_prompts ?? []
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
12
23
|
function isTemplateDefinition(value) {
|
|
13
24
|
if (!value || typeof value !== "object") {
|
|
14
25
|
return false;
|
|
@@ -20,6 +31,9 @@ function isTemplateDefinition(value) {
|
|
|
20
31
|
const previewMedia = about && Array.isArray(about.preview_media)
|
|
21
32
|
? about.preview_media
|
|
22
33
|
: null;
|
|
34
|
+
const samplePrompts = about && Array.isArray(about.sample_prompts)
|
|
35
|
+
? about.sample_prompts
|
|
36
|
+
: null;
|
|
23
37
|
return (typeof candidate.id === "string" &&
|
|
24
38
|
typeof candidate.slugId === "string" &&
|
|
25
39
|
typeof candidate.version === "string" &&
|
|
@@ -31,6 +45,8 @@ function isTemplateDefinition(value) {
|
|
|
31
45
|
Array.isArray(previewMedia) &&
|
|
32
46
|
previewMedia.every((entry) => typeof entry === "string") &&
|
|
33
47
|
typeof about.link_to_original === "string" &&
|
|
48
|
+
(typeof about.proposal_generator === "string" || typeof about.proposal_generator === "undefined") &&
|
|
49
|
+
(samplePrompts === null || samplePrompts.every((entry) => typeof entry === "string")) &&
|
|
34
50
|
typeof candidate.configSchema === "object" &&
|
|
35
51
|
typeof candidate.operations === "object" &&
|
|
36
52
|
typeof candidate.jobs === "object");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { execFile } from "node:child_process";
|
|
4
6
|
import { promisify } from "node:util";
|
|
@@ -9,7 +11,9 @@ import { defaultSkillPathForTemplateModule, deriveTemplateRootDirFromModulePath
|
|
|
9
11
|
import { nowIso } from "../lib/time.js";
|
|
10
12
|
import { loadTemplateFromModule } from "./template-loader.js";
|
|
11
13
|
import { TemplateCertificationService } from "./template-certification.js";
|
|
14
|
+
import { StorageService } from "./storage.js";
|
|
12
15
|
const execFileAsync = promisify(execFile);
|
|
16
|
+
const storage = new StorageService();
|
|
13
17
|
export class TemplateSourceAlreadyRegisteredError extends Error {
|
|
14
18
|
existingSource;
|
|
15
19
|
conflictField;
|
|
@@ -22,11 +26,11 @@ export class TemplateSourceAlreadyRegisteredError extends Error {
|
|
|
22
26
|
}
|
|
23
27
|
export class TemplateSourceService {
|
|
24
28
|
certification = new TemplateCertificationService();
|
|
25
|
-
listSources() {
|
|
26
|
-
return database.listTemplateSources();
|
|
29
|
+
listSources(status) {
|
|
30
|
+
return database.listTemplateSources(status);
|
|
27
31
|
}
|
|
28
|
-
listReleases(
|
|
29
|
-
return database.listTemplateReleases(
|
|
32
|
+
listReleases(input) {
|
|
33
|
+
return database.listTemplateReleases(input);
|
|
30
34
|
}
|
|
31
35
|
async registerSource(input) {
|
|
32
36
|
deriveTemplateRootDirFromModulePath(input.templateModulePath);
|
|
@@ -61,12 +65,13 @@ export class TemplateSourceService {
|
|
|
61
65
|
const buildCommand = input.buildCommand ?? "npm run build";
|
|
62
66
|
const skillPath = input.skillPath ?? defaultSkillPathForTemplateModule(input.templateModulePath);
|
|
63
67
|
if (existingSource) {
|
|
64
|
-
const
|
|
68
|
+
const metadataChanged = existingSource.repoUrl !== input.repoUrl ||
|
|
65
69
|
existingSource.branch !== branch ||
|
|
66
70
|
existingSource.templateModulePath !== input.templateModulePath ||
|
|
67
71
|
existingSource.skillPath !== skillPath ||
|
|
68
72
|
existingSource.installCommand !== installCommand ||
|
|
69
|
-
existingSource.buildCommand !== buildCommand
|
|
73
|
+
existingSource.buildCommand !== buildCommand;
|
|
74
|
+
const nextSource = metadataChanged
|
|
70
75
|
? database.updateTemplateSource({
|
|
71
76
|
id: existingSource.id,
|
|
72
77
|
repoUrl: input.repoUrl,
|
|
@@ -74,7 +79,8 @@ export class TemplateSourceService {
|
|
|
74
79
|
templateModulePath: input.templateModulePath,
|
|
75
80
|
skillPath,
|
|
76
81
|
installCommand,
|
|
77
|
-
buildCommand
|
|
82
|
+
buildCommand,
|
|
83
|
+
status: existingSource.status === "disabled" ? "disabled" : "pending_review"
|
|
78
84
|
})
|
|
79
85
|
: existingSource;
|
|
80
86
|
return {
|
|
@@ -94,7 +100,7 @@ export class TemplateSourceService {
|
|
|
94
100
|
skillPath,
|
|
95
101
|
installCommand,
|
|
96
102
|
buildCommand,
|
|
97
|
-
status: "
|
|
103
|
+
status: "pending_review"
|
|
98
104
|
});
|
|
99
105
|
return {
|
|
100
106
|
source,
|
|
@@ -125,13 +131,26 @@ export class TemplateSourceService {
|
|
|
125
131
|
if (!source) {
|
|
126
132
|
throw new Error("Template source not found.");
|
|
127
133
|
}
|
|
128
|
-
const
|
|
134
|
+
const artifactManifest = input.artifactManifestKey
|
|
135
|
+
? await this.loadAndVerifyArtifactManifest(input.artifactManifestKey, source)
|
|
136
|
+
: null;
|
|
137
|
+
const commitSha = artifactManifest?.commit_sha ?? input.commitSha ?? await this.resolveBranchHead(source.repoUrl, source.branch);
|
|
129
138
|
const checkoutPath = path.join(config.TEMPLATE_SOURCE_ROOT, source.templateId, commitSha);
|
|
130
|
-
const skillPath = path.join(checkoutPath, source.skillPath);
|
|
139
|
+
const skillPath = path.join(checkoutPath, artifactManifest?.skill_path ?? source.skillPath);
|
|
140
|
+
const authRepoUrl = this.resolveGitRemoteUrl(source.repoUrl);
|
|
131
141
|
if (!existsSync(checkoutPath)) {
|
|
132
142
|
mkdirSync(path.dirname(checkoutPath), { recursive: true });
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
if (artifactManifest) {
|
|
144
|
+
await this.extractArtifactToCheckout({
|
|
145
|
+
checkoutPath,
|
|
146
|
+
artifactKey: artifactManifest.artifact_key,
|
|
147
|
+
expectedSha256: artifactManifest.artifact_sha256
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await this.runShell(["git", "clone", "--branch", source.branch, authRepoUrl, checkoutPath], process.cwd());
|
|
152
|
+
await this.runShell(["git", "checkout", commitSha], checkoutPath);
|
|
153
|
+
}
|
|
135
154
|
if (source.installCommand.trim()) {
|
|
136
155
|
await this.runCommandString(source.installCommand, checkoutPath);
|
|
137
156
|
}
|
|
@@ -148,7 +167,7 @@ export class TemplateSourceService {
|
|
|
148
167
|
throw new Error(`Imported template slug_id ${template.slugId} does not match source slug_id ${source.slugId}.`);
|
|
149
168
|
}
|
|
150
169
|
const certificationReport = await this.certification.certify({ template, skillPath });
|
|
151
|
-
const status = certificationReport.passed ? "
|
|
170
|
+
const status = certificationReport.passed ? "pending_approval" : "failed";
|
|
152
171
|
return database.createTemplateRelease({
|
|
153
172
|
id: createId("trel"),
|
|
154
173
|
sourceId: source.id,
|
|
@@ -158,18 +177,183 @@ export class TemplateSourceService {
|
|
|
158
177
|
checkoutPath,
|
|
159
178
|
skillPath,
|
|
160
179
|
modulePath,
|
|
180
|
+
artifactManifestKey: input.artifactManifestKey ?? null,
|
|
181
|
+
artifactKey: artifactManifest?.artifact_key ?? null,
|
|
161
182
|
status,
|
|
162
183
|
certificationReport,
|
|
163
184
|
activatedAt: null
|
|
164
185
|
});
|
|
165
186
|
}
|
|
187
|
+
async publishRelease(input) {
|
|
188
|
+
const source = database.getTemplateSource(input.sourceId);
|
|
189
|
+
if (!source) {
|
|
190
|
+
throw new Error("Template source not found.");
|
|
191
|
+
}
|
|
192
|
+
if (source.status !== "approved") {
|
|
193
|
+
throw new Error("Template source must be approved before a developer can publish an update.");
|
|
194
|
+
}
|
|
195
|
+
const trimmedCommitSha = input.commitSha.trim();
|
|
196
|
+
if (!trimmedCommitSha) {
|
|
197
|
+
throw new Error("commit_sha is required.");
|
|
198
|
+
}
|
|
199
|
+
const existingRelease = database.getTemplateReleaseBySourceAndCommit(source.id, trimmedCommitSha);
|
|
200
|
+
if (existingRelease &&
|
|
201
|
+
(existingRelease.status === "pending_approval"
|
|
202
|
+
|| existingRelease.status === "approved"
|
|
203
|
+
|| existingRelease.status === "active")) {
|
|
204
|
+
return {
|
|
205
|
+
source,
|
|
206
|
+
release: existingRelease,
|
|
207
|
+
publishAction: "updated"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const release = await this.importRelease({
|
|
211
|
+
sourceId: source.id,
|
|
212
|
+
commitSha: trimmedCommitSha
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
source,
|
|
216
|
+
release,
|
|
217
|
+
publishAction: existingRelease ? "updated" : "created"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async publishArtifactRelease(input) {
|
|
221
|
+
const source = database.getTemplateSource(input.sourceId);
|
|
222
|
+
if (!source) {
|
|
223
|
+
throw new Error("Template source not found.");
|
|
224
|
+
}
|
|
225
|
+
if (source.status !== "approved") {
|
|
226
|
+
throw new Error("Template source must be approved before a developer can publish an update.");
|
|
227
|
+
}
|
|
228
|
+
const trimmedCommitSha = input.commitSha.trim();
|
|
229
|
+
const trimmedArtifactSha256 = input.artifactSha256.trim().toLowerCase();
|
|
230
|
+
if (!trimmedCommitSha) {
|
|
231
|
+
throw new Error("commit_sha is required.");
|
|
232
|
+
}
|
|
233
|
+
if (!/^[0-9a-f]{64}$/i.test(trimmedArtifactSha256)) {
|
|
234
|
+
throw new Error("artifact_sha256 must be a 64-character hexadecimal SHA-256 hash.");
|
|
235
|
+
}
|
|
236
|
+
const existingRelease = database.getTemplateReleaseBySourceAndCommit(source.id, trimmedCommitSha);
|
|
237
|
+
if (existingRelease &&
|
|
238
|
+
(existingRelease.status === "pending_approval"
|
|
239
|
+
|| existingRelease.status === "approved"
|
|
240
|
+
|| existingRelease.status === "active")) {
|
|
241
|
+
return {
|
|
242
|
+
source,
|
|
243
|
+
release: existingRelease,
|
|
244
|
+
publishAction: "updated"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const { manifestKey } = await this.createSignedArtifactManifest({
|
|
248
|
+
source,
|
|
249
|
+
commitSha: trimmedCommitSha,
|
|
250
|
+
artifactSha256: trimmedArtifactSha256,
|
|
251
|
+
signedBy: input.signedBy
|
|
252
|
+
});
|
|
253
|
+
const release = await this.importRelease({
|
|
254
|
+
sourceId: source.id,
|
|
255
|
+
artifactManifestKey: manifestKey
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
source,
|
|
259
|
+
release,
|
|
260
|
+
publishAction: existingRelease ? "updated" : "created"
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async createDeveloperArtifactUpload(input) {
|
|
264
|
+
const source = database.getTemplateSource(input.sourceId);
|
|
265
|
+
if (!source) {
|
|
266
|
+
throw new Error("Template source not found.");
|
|
267
|
+
}
|
|
268
|
+
if (source.status !== "approved") {
|
|
269
|
+
throw new Error("Template source must be approved before a developer can publish an update.");
|
|
270
|
+
}
|
|
271
|
+
const trimmedCommitSha = input.commitSha.trim();
|
|
272
|
+
if (!trimmedCommitSha) {
|
|
273
|
+
throw new Error("commit_sha is required.");
|
|
274
|
+
}
|
|
275
|
+
const artifact = this.buildArtifactKeys(source, trimmedCommitSha);
|
|
276
|
+
const upload = await storage.createWriteUrl(artifact.artifactKey, {
|
|
277
|
+
contentType: "application/gzip",
|
|
278
|
+
publicRead: false,
|
|
279
|
+
expiresIn: 3600
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
...artifact,
|
|
283
|
+
upload
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
approveSource(input) {
|
|
287
|
+
const source = database.getTemplateSource(input.sourceId);
|
|
288
|
+
if (!source) {
|
|
289
|
+
throw new Error("Template source not found.");
|
|
290
|
+
}
|
|
291
|
+
return database.updateTemplateSource({
|
|
292
|
+
id: source.id,
|
|
293
|
+
repoUrl: source.repoUrl,
|
|
294
|
+
branch: source.branch,
|
|
295
|
+
templateModulePath: source.templateModulePath,
|
|
296
|
+
skillPath: source.skillPath,
|
|
297
|
+
installCommand: source.installCommand,
|
|
298
|
+
buildCommand: source.buildCommand,
|
|
299
|
+
status: "approved"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
rejectSource(input) {
|
|
303
|
+
const source = database.getTemplateSource(input.sourceId);
|
|
304
|
+
if (!source) {
|
|
305
|
+
throw new Error("Template source not found.");
|
|
306
|
+
}
|
|
307
|
+
return database.updateTemplateSource({
|
|
308
|
+
id: source.id,
|
|
309
|
+
repoUrl: source.repoUrl,
|
|
310
|
+
branch: source.branch,
|
|
311
|
+
templateModulePath: source.templateModulePath,
|
|
312
|
+
skillPath: source.skillPath,
|
|
313
|
+
installCommand: source.installCommand,
|
|
314
|
+
buildCommand: source.buildCommand,
|
|
315
|
+
status: "rejected"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
approveRelease(input) {
|
|
319
|
+
const release = database.getTemplateRelease(input.releaseId);
|
|
320
|
+
if (!release) {
|
|
321
|
+
throw new Error("Template release not found.");
|
|
322
|
+
}
|
|
323
|
+
if (release.status !== "pending_approval" && release.status !== "approved" && release.status !== "active") {
|
|
324
|
+
throw new Error("Only pending-approval releases can be approved.");
|
|
325
|
+
}
|
|
326
|
+
if (release.status === "active") {
|
|
327
|
+
return release;
|
|
328
|
+
}
|
|
329
|
+
database.updateTemplateReleaseStatus({
|
|
330
|
+
id: release.id,
|
|
331
|
+
status: "approved",
|
|
332
|
+
certificationReport: release.certificationReport,
|
|
333
|
+
activatedAt: null
|
|
334
|
+
});
|
|
335
|
+
return database.getTemplateRelease(release.id);
|
|
336
|
+
}
|
|
337
|
+
rejectRelease(input) {
|
|
338
|
+
const release = database.getTemplateRelease(input.releaseId);
|
|
339
|
+
if (!release) {
|
|
340
|
+
throw new Error("Template release not found.");
|
|
341
|
+
}
|
|
342
|
+
database.updateTemplateReleaseStatus({
|
|
343
|
+
id: release.id,
|
|
344
|
+
status: "rejected",
|
|
345
|
+
certificationReport: release.certificationReport,
|
|
346
|
+
activatedAt: null
|
|
347
|
+
});
|
|
348
|
+
return database.getTemplateRelease(release.id);
|
|
349
|
+
}
|
|
166
350
|
async activateRelease(input) {
|
|
167
351
|
const release = database.getTemplateRelease(input.releaseId);
|
|
168
352
|
if (!release) {
|
|
169
353
|
throw new Error("Template release not found.");
|
|
170
354
|
}
|
|
171
|
-
if (release.status !== "
|
|
172
|
-
throw new Error("Only
|
|
355
|
+
if (release.status !== "approved" && release.status !== "active") {
|
|
356
|
+
throw new Error("Only approved releases can be activated.");
|
|
173
357
|
}
|
|
174
358
|
const template = await loadTemplateFromModule(release.modulePath);
|
|
175
359
|
const certificationReport = release.certificationReport ?? await this.certification.certify({
|
|
@@ -226,7 +410,7 @@ export class TemplateSourceService {
|
|
|
226
410
|
}
|
|
227
411
|
}
|
|
228
412
|
async resolveBranchHead(repoUrl, branch) {
|
|
229
|
-
const { stdout } = await execFileAsync("git", ["ls-remote", repoUrl, branch], { cwd: process.cwd() });
|
|
413
|
+
const { stdout } = await execFileAsync("git", ["ls-remote", this.resolveGitRemoteUrl(repoUrl), branch], { cwd: process.cwd() });
|
|
230
414
|
const line = stdout.trim().split("\n").find(Boolean);
|
|
231
415
|
if (!line) {
|
|
232
416
|
throw new Error(`Unable to resolve branch ${branch} for ${repoUrl}`);
|
|
@@ -248,6 +432,143 @@ export class TemplateSourceService {
|
|
|
248
432
|
}
|
|
249
433
|
});
|
|
250
434
|
}
|
|
435
|
+
async loadAndVerifyArtifactManifest(manifestKey, source) {
|
|
436
|
+
const raw = await storage.readText(manifestKey);
|
|
437
|
+
if (!raw) {
|
|
438
|
+
throw new Error(`Artifact manifest not found: ${manifestKey}`);
|
|
439
|
+
}
|
|
440
|
+
const manifest = JSON.parse(raw);
|
|
441
|
+
if (manifest.version !== 1 || manifest.algorithm !== "hmac-sha256") {
|
|
442
|
+
throw new Error("Unsupported template artifact manifest version or algorithm.");
|
|
443
|
+
}
|
|
444
|
+
if (!config.ARTIFACT_SIGNING_SECRET) {
|
|
445
|
+
throw new Error("ARTIFACT_SIGNING_SECRET or SUPERAGENCY_KEY is required to verify artifact manifests.");
|
|
446
|
+
}
|
|
447
|
+
const expectedSignature = createHmac("sha256", config.ARTIFACT_SIGNING_SECRET)
|
|
448
|
+
.update(this.serializeArtifactManifestPayload({
|
|
449
|
+
version: 1,
|
|
450
|
+
algorithm: "hmac-sha256",
|
|
451
|
+
template_id: String(manifest.template_id ?? ""),
|
|
452
|
+
slug_id: String(manifest.slug_id ?? ""),
|
|
453
|
+
repo_url: String(manifest.repo_url ?? ""),
|
|
454
|
+
branch: String(manifest.branch ?? ""),
|
|
455
|
+
commit_sha: String(manifest.commit_sha ?? ""),
|
|
456
|
+
template_module_path: String(manifest.template_module_path ?? ""),
|
|
457
|
+
skill_path: String(manifest.skill_path ?? ""),
|
|
458
|
+
install_command: String(manifest.install_command ?? ""),
|
|
459
|
+
build_command: String(manifest.build_command ?? ""),
|
|
460
|
+
artifact_key: String(manifest.artifact_key ?? ""),
|
|
461
|
+
artifact_sha256: String(manifest.artifact_sha256 ?? ""),
|
|
462
|
+
created_at: String(manifest.created_at ?? ""),
|
|
463
|
+
signed_by: String(manifest.signed_by ?? "")
|
|
464
|
+
}))
|
|
465
|
+
.digest("hex");
|
|
466
|
+
if (manifest.signature !== expectedSignature) {
|
|
467
|
+
throw new Error("Template artifact manifest signature verification failed.");
|
|
468
|
+
}
|
|
469
|
+
if (manifest.template_id !== source.templateId || manifest.slug_id !== source.slugId) {
|
|
470
|
+
throw new Error("Template artifact manifest does not match the registered template identity.");
|
|
471
|
+
}
|
|
472
|
+
if (manifest.repo_url !== source.repoUrl || manifest.branch !== source.branch) {
|
|
473
|
+
throw new Error("Template artifact manifest does not match the registered repo URL or branch.");
|
|
474
|
+
}
|
|
475
|
+
if (manifest.template_module_path !== source.templateModulePath) {
|
|
476
|
+
throw new Error("Template artifact manifest does not match the registered template module path.");
|
|
477
|
+
}
|
|
478
|
+
if (manifest.skill_path !== source.skillPath) {
|
|
479
|
+
throw new Error("Template artifact manifest does not match the registered skill path.");
|
|
480
|
+
}
|
|
481
|
+
if (manifest.install_command !== source.installCommand || manifest.build_command !== source.buildCommand) {
|
|
482
|
+
throw new Error("Template artifact manifest does not match the registered install or build command.");
|
|
483
|
+
}
|
|
484
|
+
return manifest;
|
|
485
|
+
}
|
|
486
|
+
async extractArtifactToCheckout(input) {
|
|
487
|
+
const archive = await storage.readBuffer(input.artifactKey);
|
|
488
|
+
if (!archive) {
|
|
489
|
+
throw new Error(`Template artifact not found: ${input.artifactKey}`);
|
|
490
|
+
}
|
|
491
|
+
const actualSha256 = createHash("sha256").update(archive).digest("hex");
|
|
492
|
+
if (actualSha256 !== input.expectedSha256) {
|
|
493
|
+
throw new Error(`Template artifact hash mismatch for ${input.artifactKey}.`);
|
|
494
|
+
}
|
|
495
|
+
mkdirSync(input.checkoutPath, { recursive: true });
|
|
496
|
+
const tempArchivePath = path.join(os.tmpdir(), `vidfarm-template-${createId("tmp")}.tar.gz`);
|
|
497
|
+
writeFileSync(tempArchivePath, archive);
|
|
498
|
+
try {
|
|
499
|
+
await execFileAsync("tar", ["-xzf", tempArchivePath, "-C", input.checkoutPath], { cwd: process.cwd() });
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
rmSync(tempArchivePath, { force: true });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
serializeArtifactManifestPayload(input) {
|
|
506
|
+
return JSON.stringify({
|
|
507
|
+
version: input.version,
|
|
508
|
+
algorithm: input.algorithm,
|
|
509
|
+
template_id: input.template_id,
|
|
510
|
+
slug_id: input.slug_id,
|
|
511
|
+
repo_url: input.repo_url,
|
|
512
|
+
branch: input.branch,
|
|
513
|
+
commit_sha: input.commit_sha,
|
|
514
|
+
template_module_path: input.template_module_path,
|
|
515
|
+
skill_path: input.skill_path,
|
|
516
|
+
install_command: input.install_command,
|
|
517
|
+
build_command: input.build_command,
|
|
518
|
+
artifact_key: input.artifact_key,
|
|
519
|
+
artifact_sha256: input.artifact_sha256,
|
|
520
|
+
created_at: input.created_at,
|
|
521
|
+
signed_by: input.signed_by
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
buildArtifactKeys(source, commitSha) {
|
|
525
|
+
return {
|
|
526
|
+
artifactKey: `template-artifacts/${source.templateId}/${commitSha}/source.tar.gz`,
|
|
527
|
+
manifestKey: `template-artifacts/${source.templateId}/${commitSha}/manifest.json`
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
async createSignedArtifactManifest(input) {
|
|
531
|
+
const artifact = this.buildArtifactKeys(input.source, input.commitSha);
|
|
532
|
+
const createdAt = nowIso();
|
|
533
|
+
const payload = {
|
|
534
|
+
version: 1,
|
|
535
|
+
algorithm: "hmac-sha256",
|
|
536
|
+
template_id: input.source.templateId,
|
|
537
|
+
slug_id: input.source.slugId,
|
|
538
|
+
repo_url: input.source.repoUrl,
|
|
539
|
+
branch: input.source.branch,
|
|
540
|
+
commit_sha: input.commitSha,
|
|
541
|
+
template_module_path: input.source.templateModulePath,
|
|
542
|
+
skill_path: input.source.skillPath,
|
|
543
|
+
install_command: input.source.installCommand,
|
|
544
|
+
build_command: input.source.buildCommand,
|
|
545
|
+
artifact_key: artifact.artifactKey,
|
|
546
|
+
artifact_sha256: input.artifactSha256,
|
|
547
|
+
created_at: createdAt,
|
|
548
|
+
signed_by: input.signedBy
|
|
549
|
+
};
|
|
550
|
+
const signature = createHmac("sha256", config.ARTIFACT_SIGNING_SECRET)
|
|
551
|
+
.update(this.serializeArtifactManifestPayload(payload))
|
|
552
|
+
.digest("hex");
|
|
553
|
+
await storage.putText(artifact.manifestKey, JSON.stringify({
|
|
554
|
+
...payload,
|
|
555
|
+
signature
|
|
556
|
+
}, null, 2), "application/json");
|
|
557
|
+
return {
|
|
558
|
+
...artifact,
|
|
559
|
+
signature
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
resolveGitRemoteUrl(repoUrl) {
|
|
563
|
+
const githubToken = config.VIDFARM_GITHUB_TOKEN || config.GITHUB_TOKEN;
|
|
564
|
+
if (!githubToken) {
|
|
565
|
+
return repoUrl;
|
|
566
|
+
}
|
|
567
|
+
if (!/^https:\/\/github\.com\//i.test(repoUrl)) {
|
|
568
|
+
return repoUrl;
|
|
569
|
+
}
|
|
570
|
+
return repoUrl.replace(/^https:\/\/github\.com\//i, `https://x-access-token:${encodeURIComponent(githubToken)}@github.com/`);
|
|
571
|
+
}
|
|
251
572
|
resolveImportableModulePath(checkoutPath, declaredModulePath) {
|
|
252
573
|
const sourceModulePath = path.join(checkoutPath, declaredModulePath);
|
|
253
574
|
const extension = path.extname(sourceModulePath).toLowerCase();
|
package/{templates/template_0000 → dist/templates/vidfarm_template_0000}/src/remotion/Root.js
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { AbsoluteFill, Composition, Img, Sequence, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
4
|
export const RemotionRoot = () => {
|
|
4
5
|
return (_jsx(Composition, { id: "template-0000", component: TemplateVideo, width: 1080, height: 1920, fps: 30, durationInFrames: 120, defaultProps: { slides: [] }, calculateMetadata: ({ props }) => ({
|
|
@@ -100,6 +100,12 @@ export const template0000Definition = defineTemplate({
|
|
|
100
100
|
visual_dna: templateVisualDna,
|
|
101
101
|
preview_media: [...TEMPLATE_PREVIEW_MEDIA],
|
|
102
102
|
link_to_original: templateLinkToOriginal,
|
|
103
|
+
proposal_generator: "Given the director's product, customer, and angle, propose multiple slideshow concepts that fit this template's creator-native TikTok format. Return concrete post ideas that can either be used directly as single prompts or expanded into 10+ related prompts. Favor specific hooks, clear educational angles, founder stories, product proof, objections, comparisons, and repeatable series ideas.",
|
|
104
|
+
sample_prompts: [
|
|
105
|
+
"Make a 3-slide TikTok slideshow explaining why cold plunges feel so hard at first but get easier after a week. Use exact overlay text for each slide and generate matching cinematic background prompts.",
|
|
106
|
+
"Create a founder-story slideshow about launching a niche coffee brand, with one hook slide, one proof slide, and one CTA slide. Keep the tone creator-native and direct.",
|
|
107
|
+
"Build a product explainer slideshow for a posture-correcting desk setup. I want concise overlays, clean visual prompts, and output ready for optional Remotion rendering."
|
|
108
|
+
],
|
|
103
109
|
},
|
|
104
110
|
skillPath,
|
|
105
111
|
configSchema: z.object({
|
|
@@ -1407,7 +1413,7 @@ function resolveRemotionEntryPoint() {
|
|
|
1407
1413
|
function resolveTemplateRuntimeConfig() {
|
|
1408
1414
|
const candidates = [
|
|
1409
1415
|
fileURLToPath(new URL("../template.config.json", import.meta.url)),
|
|
1410
|
-
path.resolve(process.cwd(), "templates/
|
|
1416
|
+
path.resolve(process.cwd(), "templates/vidfarm_template_0000/template.config.json"),
|
|
1411
1417
|
];
|
|
1412
1418
|
for (const candidate of candidates) {
|
|
1413
1419
|
if (!existsSync(candidate)) {
|
|
@@ -1422,13 +1428,13 @@ function resolveSkillPath() {
|
|
|
1422
1428
|
if (existsSync(builtPath)) {
|
|
1423
1429
|
return builtPath;
|
|
1424
1430
|
}
|
|
1425
|
-
return path.resolve(process.cwd(), "templates/
|
|
1431
|
+
return path.resolve(process.cwd(), "templates/vidfarm_template_0000/SKILL.md");
|
|
1426
1432
|
}
|
|
1427
1433
|
function resolveOverlayFonts() {
|
|
1428
1434
|
const resolved = Object.fromEntries(TEMPLATE_FONT_OPTIONS.map((option) => {
|
|
1429
1435
|
const candidates = [
|
|
1430
1436
|
fileURLToPath(new URL(`../assets/${option.assetFile}`, import.meta.url)),
|
|
1431
|
-
path.resolve(process.cwd(), `templates/
|
|
1437
|
+
path.resolve(process.cwd(), `templates/vidfarm_template_0000/assets/${option.assetFile}`),
|
|
1432
1438
|
];
|
|
1433
1439
|
for (const candidate of candidates) {
|
|
1434
1440
|
if (existsSync(candidate)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mevdragon/vidfarm-devcli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Developer CLI for running the Vidfarm local template platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/src",
|
|
12
|
-
"dist/templates/
|
|
13
|
-
"templates/
|
|
12
|
+
"dist/templates/vidfarm_template_0000",
|
|
13
|
+
"templates/vidfarm_template_0000",
|
|
14
14
|
"README.md",
|
|
15
15
|
"SKILL.developer.md",
|
|
16
16
|
"GETTING_STARTED.developers.md",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"dev": "tsx watch src/index.ts",
|
|
31
31
|
"dev:cli": "tsx src/cli.ts dev",
|
|
32
|
+
"operator:template-artifact": "node scripts/platform-operator-template-artifact.mjs",
|
|
32
33
|
"deploy:prod-inplace": "bash scripts/deploy-prod-inplace.sh",
|
|
33
34
|
"rollback:prod-inplace": "bash scripts/rollback-prod-inplace.sh",
|
|
34
35
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
@@ -23,6 +23,22 @@ Production integration settings are checked into `template.config.json`. AI agen
|
|
|
23
23
|
- the template module path, which should be the TypeScript source entrypoint `src/template.ts`
|
|
24
24
|
- the local Remotion composition settings used by the starter template
|
|
25
25
|
|
|
26
|
+
## Required template contract
|
|
27
|
+
|
|
28
|
+
Every new template copied from `template_0000` should keep the current required `defineTemplate(...)` contract intact.
|
|
29
|
+
|
|
30
|
+
- `id`, `slugId`, and `version`
|
|
31
|
+
- `about.title` and `about.description`
|
|
32
|
+
- `about.viral_dna`, `about.visual_dna`, `about.preview_media`, and `about.link_to_original`
|
|
33
|
+
- `about.proposal_generator`, which teaches a director or agent how to turn business context into template-native post proposals
|
|
34
|
+
- `about.sample_prompts`, which shows multiple concrete high-signal prompts for the template
|
|
35
|
+
- `configSchema`
|
|
36
|
+
- at least one operation with a `smokeTestPayload`
|
|
37
|
+
- a matching workflow implementation in `jobs`
|
|
38
|
+
- a template-local `SKILL.md`
|
|
39
|
+
|
|
40
|
+
The starter already ships with example `proposal_generator` and `sample_prompts` values in `src/template.ts`. When you adapt this template, replace them with instructions and examples that fit the new format instead of leaving the starter language in place.
|
|
41
|
+
|
|
26
42
|
## Included
|
|
27
43
|
|
|
28
44
|
- `SKILL.md` for customer AI-agent usage
|
|
@@ -75,3 +91,10 @@ vidfarm analyze-visual-dna --template-dir .
|
|
|
75
91
|
```
|
|
76
92
|
|
|
77
93
|
If you scaffold a template with `vidfarm generate-template` and provide `--source-preview-dir`, the CLI will run both analyzers automatically unless `--skip-dna-analysis` is set.
|
|
94
|
+
|
|
95
|
+
After DNA analysis, review and customize the remaining human-authored metadata before calling the template ready:
|
|
96
|
+
|
|
97
|
+
- refresh `about.proposal_generator`
|
|
98
|
+
- refresh `about.sample_prompts`
|
|
99
|
+
- update `SKILL.md` to match the actual operations and payload shapes
|
|
100
|
+
- run `vidfarm validate-template --template-id <slug-or-id>` and at least one real local job
|