@mevdragon/vidfarm-devcli 0.2.4 → 0.2.6

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/README.md CHANGED
@@ -51,7 +51,7 @@ The local CLI runs the same template contract used by the hosted platform, but w
51
51
 
52
52
  Use `vidfarm session` to print reusable auth headers and a sample `curl` request for the local REST API.
53
53
 
54
- For hosted preview-media uploads, use `vidfarm-devcli presign-preview-media`. It calls the authenticated Vidfarm API with `VIDFARM_API_KEY`, mints a presigned PUT URL under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns the resulting Vidfarm media URL.
54
+ For hosted preview-media uploads, use `vidfarm-devcli presign-preview-media`. It calls the authenticated Vidfarm API with `VIDFARM_API_KEY`, mints a presigned PUT URL under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns a public-read media URL for the uploaded object. Public readability is granted by bucket policy for `developer/*`, not by public write or list access.
55
55
 
56
56
  ## What The Hosted Platform Expects From Templates
57
57
 
@@ -26,6 +26,24 @@ Do not use this skill for:
26
26
  - production deploys, release admin, or shared cloud infrastructure
27
27
  - asking for AWS, S3, Remotion cloud, or other platform secrets
28
28
 
29
+ ## Third-Party Publish Rule
30
+
31
+ Third-party template developers publish template code by pushing git commits to GitHub. That is the only code-distribution step they own.
32
+
33
+ The third-party developer responsibility is:
34
+
35
+ - build and validate the template locally
36
+ - commit and push the template folder to GitHub
37
+ - register or update the hosted template source metadata so Vidfarm knows which repo, branch, and template folder to import from
38
+
39
+ The third-party developer should not:
40
+
41
+ - run `import-source-prod`
42
+ - use AWS CloudFormation, SSM, EC2, or shared prod credentials
43
+ - treat platform import/activation as part of template authoring
44
+
45
+ `import-source-prod` and `deploy-template-cycle` are platform-operator commands. They are for internal admin flow, not for external template authors.
46
+
29
47
  For third-party developers, Remotion is local and storage should be accessed through Vidfarm helpers. Do not require cloud Remotion setup or hand-written S3 integration for normal template work.
30
48
 
31
49
  ## Agent Operating Rule
@@ -101,17 +119,36 @@ Notes:
101
119
  - template authoring is TypeScript-first; create and edit template source as `src/template.ts`, not `src/template.js`
102
120
  - keep exploratory scripts, one-off test runners, scratch validation files, and temporary job-driving code out of `src/`; place them in a repo-local tmp folder that is a sibling to `src` instead, for example `templates/vidfarm_template_0007/tmp/`
103
121
  - when registering a hosted template source, set `template_module_path` to the repo-relative TypeScript entrypoint inside the template folder, for example `templates/vidfarm_template_example/src/template.ts`; the platform build/import flow resolves the compiled `src/template.js` sibling after `npm run build`
122
+ - hosted template registration is git metadata only: repo URL, branch, template entrypoint path, and optionally an explicit commit SHA for a specific import
123
+ - if a template moves folders or switches branches, update the registration metadata; if the template code changed but the location stayed the same, just push a new Git commit
124
+ - the platform determines whether an update exists by comparing the registered branch head against the latest imported release commit
104
125
  - do not ask for platform secrets like `ENCRYPTION_SECRET`, `API_KEY_SALT`, `WEBHOOK_SECRET`, admin emails, S3 config, or generic AWS credentials
105
126
  - do not ask for `REMOTION_AWS_ACCESS_KEY_ID` or `REMOTION_AWS_SECRET_ACCESS_KEY`
106
127
 
107
- ## Template Deploy Cycle
128
+ ## Hosted Registration Flow
108
129
 
109
- If the user says "do a template deploy cycle", interpret that as:
130
+ For a third-party developer, the hosted publish handoff is:
110
131
 
111
132
  1. push the repo changes that contain the target template folder
112
- 2. import that template commit into the hosted platform as a template source release
113
- 3. approve and activate the new release
114
- 4. run the prod deploy script if platform code changed, or reuse the current prod image for a formal restart if the change was template-only
133
+ 2. register the source location if it is new, or update the existing registration if the repo, branch, or template path changed
134
+ 3. tell the platform operator which branch head or commit SHA should be imported
135
+
136
+ Registration data should be treated like this:
137
+
138
+ - `repo_url`: the GitHub repo that contains the template code
139
+ - `branch`: the branch Vidfarm should watch for the latest version, usually `main`
140
+ - `template_module_path`: the repo-relative path to the template TypeScript entrypoint, for example `src/vidfarm_template_funnychat/src/template.ts`
141
+ - `commit_sha`: optional and only needed when you want to import a specific commit instead of the current branch head
142
+
143
+ Do not tell third-party developers to run AWS-backed operator commands just to publish template code. If the task is only "make my updated template available to Vidfarm", the correct answer is GitHub push plus source registration metadata.
144
+
145
+ ## Template Deploy Cycle
146
+
147
+ If the user explicitly says "template deploy cycle", interpret that as the internal platform-operator sequence:
148
+
149
+ 1. import that template commit into the hosted platform as a template source release
150
+ 2. approve and activate the new release
151
+ 3. run the prod deploy script if platform code changed, or reuse the current prod image for a formal restart if the change was template-only
115
152
 
116
153
  Do not assume "template deploy cycle" means "deploy the entire dirty root repo". If the root worktree has unrelated changes and the release is template-only, prefer reusing the current prod image with:
117
154
 
@@ -221,7 +258,7 @@ drafts/
221
258
 
222
259
  This is the preferred handoff shape for agent-driven template creation. A Vidfarm developer should be able to drag media into `drafts/preview/`, add rough notes to `drafts/source_notes.md`, then tell the agent to create a template from those relative paths.
223
260
 
224
- If the source media is not already present in the repo, upload it into the authenticated developer storage namespace first instead of inventing ad hoc external hosting. Use:
261
+ If the source media is not already present in the repo, upload it into the authenticated developer storage namespace first instead of inventing ad hoc external hosting. An agent can take a developer-provided local file path and run this directly:
225
262
 
226
263
  ```bash
227
264
  npx @mevdragon/vidfarm-devcli presign-preview-media \
@@ -229,7 +266,7 @@ npx @mevdragon/vidfarm-devcli presign-preview-media \
229
266
  --directory drafts/preview
230
267
  ```
231
268
 
232
- That command calls the hosted Vidfarm API with the user's `VIDFARM_API_KEY`, mints a presigned PUT URL scoped under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns both the `storage_key` and a Vidfarm `preview_media_url`. Agents should prefer that path for hosted preview uploads.
269
+ That command calls the hosted Vidfarm API with the user's `VIDFARM_API_KEY`, mints a presigned PUT URL scoped under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns both the `storage_key` and a public-read `preview_media_url`. Public readability for `developer/<user_id>/*` comes from Vidfarm bucket policy, not from public write access, so objects may be read directly by URL but should not be treated as listable or writable without authenticated API access. Agents should prefer that path for hosted preview uploads whenever the input is a local media file path.
233
270
 
234
271
  ### 2. Start local runtime
235
272
 
package/dist/src/app.js CHANGED
@@ -15,6 +15,7 @@ import { AuthService } from "./services/auth.js";
15
15
  import { JobsService } from "./services/jobs.js";
16
16
  import { StorageService } from "./services/storage.js";
17
17
  import { TemplateSourceService } from "./services/template-sources.js";
18
+ import { TemplateSourceAlreadyRegisteredError } from "./services/template-sources.js";
18
19
  const auth = new AuthService();
19
20
  const jobs = new JobsService();
20
21
  const storage = new StorageService();
@@ -986,11 +987,13 @@ app.post(`${USER_PREFIX}/me/developer/preview-media/presign`, async (c) => {
986
987
  publicRead: false,
987
988
  expiresIn: 3600
988
989
  });
990
+ const publicUrl = storage.getPublicUrl(storageKey) ?? buildAbsoluteUrl(c, `/template-media?key=${encodeURIComponent(storageKey)}`);
989
991
  return c.json({
990
992
  file_name: fileName,
991
993
  content_type: contentType,
992
994
  storage_key: storageKey,
993
- preview_media_url: buildAbsoluteUrl(c, `/template-media?key=${encodeURIComponent(storageKey)}`),
995
+ preview_media_url: publicUrl,
996
+ public_url: publicUrl,
994
997
  upload: {
995
998
  method: upload.method,
996
999
  url: upload.url,
@@ -1063,7 +1066,7 @@ app.post(`${TEMPLATES_PREFIX}/sources`, async (c) => {
1063
1066
  install_command: z.string().min(1).default("npm install"),
1064
1067
  build_command: z.string().min(1).default("npm run build")
1065
1068
  }).parse(await c.req.json());
1066
- const source = templateSources.registerSource({
1069
+ const registration = await templateSources.registerSource({
1067
1070
  templateId: body.template_id,
1068
1071
  slugId: body.slug_id,
1069
1072
  repoUrl: body.repo_url,
@@ -1073,9 +1076,21 @@ app.post(`${TEMPLATES_PREFIX}/sources`, async (c) => {
1073
1076
  installCommand: body.install_command,
1074
1077
  buildCommand: body.build_command
1075
1078
  });
1076
- return c.json({ source }, 201);
1079
+ return c.json({
1080
+ source: registration.source,
1081
+ registration_action: registration.action,
1082
+ sync: registration.sync
1083
+ }, registration.action === "created" ? 201 : 200);
1077
1084
  }
1078
1085
  catch (error) {
1086
+ if (error instanceof TemplateSourceAlreadyRegisteredError) {
1087
+ return c.json({
1088
+ error: error.message,
1089
+ registration_status: error.existingSource.status,
1090
+ existing_source: error.existingSource,
1091
+ conflict_field: error.conflictField
1092
+ }, 409);
1093
+ }
1079
1094
  const message = error instanceof Error ? error.message : "Unable to register template source.";
1080
1095
  const status = /already exists/i.test(message) ? 409 : 400;
1081
1096
  return c.json({ error: message }, status);
package/dist/src/cli.js CHANGED
@@ -235,7 +235,7 @@ async function runImportSourceCommand(argv) {
235
235
  import("./registry.js")
236
236
  ]);
237
237
  const sources = new TemplateSourceService();
238
- const source = sources.registerSource({
238
+ const registration = await sources.registerSource({
239
239
  templateId,
240
240
  slugId,
241
241
  repoUrl,
@@ -246,7 +246,7 @@ async function runImportSourceCommand(argv) {
246
246
  buildCommand: parsed.values["build-command"]
247
247
  });
248
248
  const release = await sources.importRelease({
249
- sourceId: source.id,
249
+ sourceId: registration.source.id,
250
250
  commitSha: parsed.values["commit-sha"] ?? null
251
251
  });
252
252
  let activated = null;
@@ -256,7 +256,9 @@ async function runImportSourceCommand(argv) {
256
256
  activated = result.release;
257
257
  }
258
258
  console.log(JSON.stringify({
259
- source,
259
+ source: registration.source,
260
+ registration_action: registration.action,
261
+ sync: registration.sync,
260
262
  release,
261
263
  activated_release: activated
262
264
  }, null, 2));
@@ -542,6 +544,7 @@ async function runPresignPreviewMediaCommand(argv) {
542
544
  content_type: payload.content_type,
543
545
  storage_key: payload.storage_key,
544
546
  preview_media_url: payload.preview_media_url,
547
+ public_url: payload.public_url ?? payload.preview_media_url,
545
548
  upload_result: uploadResult,
546
549
  upload: payload.upload,
547
550
  curl_upload_example: [
package/dist/src/db.js CHANGED
@@ -845,6 +845,35 @@ export const database = {
845
845
  });
846
846
  return this.getTemplateSourceByTemplateId(record.templateId);
847
847
  },
848
+ updateTemplateSource(input) {
849
+ const current = this.getTemplateSource(input.id);
850
+ if (!current) {
851
+ throw new Error("Template source not found.");
852
+ }
853
+ db.prepare(`
854
+ update template_sources
855
+ set repo_url = @repo_url,
856
+ branch = @branch,
857
+ template_module_path = @template_module_path,
858
+ skill_path = @skill_path,
859
+ install_command = @install_command,
860
+ build_command = @build_command,
861
+ status = @status,
862
+ updated_at = @updated_at
863
+ where id = @id
864
+ `).run({
865
+ id: input.id,
866
+ repo_url: input.repoUrl,
867
+ branch: input.branch,
868
+ template_module_path: input.templateModulePath,
869
+ skill_path: input.skillPath,
870
+ install_command: input.installCommand,
871
+ build_command: input.buildCommand,
872
+ status: input.status ?? current.status,
873
+ updated_at: nowIso()
874
+ });
875
+ return this.getTemplateSource(input.id);
876
+ },
848
877
  getTemplateSource(id) {
849
878
  const row = db.prepare(`select * from template_sources where id = ?`).get(id);
850
879
  return row ? mapTemplateSource(row) : null;
@@ -857,6 +886,15 @@ export const database = {
857
886
  const row = db.prepare(`select * from template_sources where slug_id = ?`).get(slugId);
858
887
  return row ? mapTemplateSource(row) : null;
859
888
  },
889
+ getTemplateSourceByLocation(repoUrl, branch, templateModulePath) {
890
+ const row = db.prepare(`
891
+ select *
892
+ from template_sources
893
+ where repo_url = ? and branch = ? and template_module_path = ?
894
+ limit 1
895
+ `).get(repoUrl, branch, templateModulePath);
896
+ return row ? mapTemplateSource(row) : null;
897
+ },
860
898
  listTemplateSources() {
861
899
  return db.prepare(`select * from template_sources order by template_id asc`).all().map(mapTemplateSource);
862
900
  },
@@ -903,6 +941,16 @@ export const database = {
903
941
  const row = db.prepare(`select * from template_releases where source_id = ? and commit_sha = ?`).get(sourceId, commitSha);
904
942
  return row ? mapTemplateRelease(row) : null;
905
943
  },
944
+ getLatestTemplateReleaseForSource(sourceId) {
945
+ const row = db.prepare(`
946
+ select *
947
+ from template_releases
948
+ where source_id = ?
949
+ order by created_at desc
950
+ limit 1
951
+ `).get(sourceId);
952
+ return row ? mapTemplateRelease(row) : null;
953
+ },
906
954
  listTemplateReleases(templateId) {
907
955
  const rows = templateId
908
956
  ? db.prepare(`select * from template_releases where template_id = ? order by created_at desc`).all(templateId)
@@ -10,6 +10,16 @@ import { nowIso } from "../lib/time.js";
10
10
  import { loadTemplateFromModule } from "./template-loader.js";
11
11
  import { TemplateCertificationService } from "./template-certification.js";
12
12
  const execFileAsync = promisify(execFile);
13
+ export class TemplateSourceAlreadyRegisteredError extends Error {
14
+ existingSource;
15
+ conflictField;
16
+ constructor(input) {
17
+ super(input.message);
18
+ this.name = "TemplateSourceAlreadyRegisteredError";
19
+ this.existingSource = input.existingSource;
20
+ this.conflictField = input.conflictField;
21
+ }
22
+ }
13
23
  export class TemplateSourceService {
14
24
  certification = new TemplateCertificationService();
15
25
  listSources() {
@@ -18,28 +28,97 @@ export class TemplateSourceService {
18
28
  listReleases(templateId) {
19
29
  return database.listTemplateReleases(templateId);
20
30
  }
21
- registerSource(input) {
31
+ async registerSource(input) {
22
32
  deriveTemplateRootDirFromModulePath(input.templateModulePath);
33
+ const branch = input.branch ?? "production";
23
34
  const existingByTemplateId = database.getTemplateSourceByTemplateId(input.templateId);
24
- if (existingByTemplateId) {
25
- throw new Error("A template with this template_id already exists. Generate a new UUIDv4 and try again.");
35
+ if (existingByTemplateId && existingByTemplateId.slugId !== input.slugId) {
36
+ throw new TemplateSourceAlreadyRegisteredError({
37
+ message: `Template ${input.templateId} is already registered with slug ${existingByTemplateId.slugId}.`,
38
+ existingSource: existingByTemplateId,
39
+ conflictField: "template_id"
40
+ });
26
41
  }
27
42
  const existingBySlugId = database.getTemplateSourceBySlugId(input.slugId);
28
- if (existingBySlugId) {
29
- throw new Error(`A template with slug_id ${input.slugId} already exists.`);
30
- }
31
- return database.createTemplateSource({
32
- id: createId("tsrc"),
33
- templateId: input.templateId,
34
- slugId: input.slugId,
35
- repoUrl: input.repoUrl,
36
- branch: input.branch ?? "production",
37
- templateModulePath: input.templateModulePath,
38
- skillPath: input.skillPath ?? defaultSkillPathForTemplateModule(input.templateModulePath),
39
- installCommand: input.installCommand ?? "npm install",
40
- buildCommand: input.buildCommand ?? "npm run build",
41
- status: "active"
42
- });
43
+ if (existingBySlugId && existingBySlugId.templateId !== input.templateId) {
44
+ throw new TemplateSourceAlreadyRegisteredError({
45
+ message: `Template slug ${input.slugId} is already registered to template ${existingBySlugId.templateId}.`,
46
+ existingSource: existingBySlugId,
47
+ conflictField: "slug_id"
48
+ });
49
+ }
50
+ const existingByLocation = database.getTemplateSourceByLocation(input.repoUrl, branch, input.templateModulePath);
51
+ if (existingByLocation &&
52
+ (existingByLocation.templateId !== input.templateId || existingByLocation.slugId !== input.slugId)) {
53
+ throw new TemplateSourceAlreadyRegisteredError({
54
+ message: `Template source ${input.repoUrl}#${branch}:${input.templateModulePath} is already registered to ${existingByLocation.templateId}/${existingByLocation.slugId}.`,
55
+ existingSource: existingByLocation,
56
+ conflictField: "source_location"
57
+ });
58
+ }
59
+ const existingSource = existingByTemplateId ?? existingBySlugId ?? existingByLocation ?? null;
60
+ const installCommand = input.installCommand ?? "npm install";
61
+ const buildCommand = input.buildCommand ?? "npm run build";
62
+ const skillPath = input.skillPath ?? defaultSkillPathForTemplateModule(input.templateModulePath);
63
+ if (existingSource) {
64
+ const nextSource = existingSource.repoUrl !== input.repoUrl ||
65
+ existingSource.branch !== branch ||
66
+ existingSource.templateModulePath !== input.templateModulePath ||
67
+ existingSource.skillPath !== skillPath ||
68
+ existingSource.installCommand !== installCommand ||
69
+ existingSource.buildCommand !== buildCommand
70
+ ? database.updateTemplateSource({
71
+ id: existingSource.id,
72
+ repoUrl: input.repoUrl,
73
+ branch,
74
+ templateModulePath: input.templateModulePath,
75
+ skillPath,
76
+ installCommand,
77
+ buildCommand
78
+ })
79
+ : existingSource;
80
+ return {
81
+ source: nextSource,
82
+ action: nextSource.updatedAt === existingSource.updatedAt ? "unchanged" : "updated",
83
+ sync: await this.getSourceSyncStatus(nextSource.id)
84
+ };
85
+ }
86
+ try {
87
+ const source = database.createTemplateSource({
88
+ id: createId("tsrc"),
89
+ templateId: input.templateId,
90
+ slugId: input.slugId,
91
+ repoUrl: input.repoUrl,
92
+ branch,
93
+ templateModulePath: input.templateModulePath,
94
+ skillPath,
95
+ installCommand,
96
+ buildCommand,
97
+ status: "active"
98
+ });
99
+ return {
100
+ source,
101
+ action: "created",
102
+ sync: await this.getSourceSyncStatus(source.id)
103
+ };
104
+ }
105
+ catch (error) {
106
+ const existingSource = database.getTemplateSourceByTemplateId(input.templateId) ??
107
+ database.getTemplateSourceBySlugId(input.slugId) ??
108
+ database.getTemplateSourceByLocation(input.repoUrl, branch, input.templateModulePath);
109
+ if (existingSource) {
110
+ throw new TemplateSourceAlreadyRegisteredError({
111
+ message: `Template registration already exists with status ${existingSource.status}.`,
112
+ existingSource,
113
+ conflictField: existingSource.templateId === input.templateId
114
+ ? "template_id"
115
+ : existingSource.slugId === input.slugId
116
+ ? "slug_id"
117
+ : "source_location"
118
+ });
119
+ }
120
+ throw error;
121
+ }
43
122
  }
44
123
  async importRelease(input) {
45
124
  const source = database.getTemplateSource(input.sourceId);
@@ -120,6 +199,32 @@ export class TemplateSourceService {
120
199
  }
121
200
  };
122
201
  }
202
+ async getSourceSyncStatus(sourceId) {
203
+ const source = database.getTemplateSource(sourceId);
204
+ if (!source) {
205
+ throw new Error("Template source not found.");
206
+ }
207
+ const latestRelease = database.getLatestTemplateReleaseForSource(source.id);
208
+ try {
209
+ const headCommitSha = await this.resolveBranchHead(source.repoUrl, source.branch);
210
+ return {
211
+ headCommitSha,
212
+ latestReleaseCommitSha: latestRelease?.commitSha ?? null,
213
+ importTargetCommitSha: headCommitSha,
214
+ hasUnimportedUpdate: latestRelease ? latestRelease.commitSha !== headCommitSha : true,
215
+ headLookupError: null
216
+ };
217
+ }
218
+ catch (error) {
219
+ return {
220
+ headCommitSha: null,
221
+ latestReleaseCommitSha: latestRelease?.commitSha ?? null,
222
+ importTargetCommitSha: latestRelease?.commitSha ?? null,
223
+ hasUnimportedUpdate: null,
224
+ headLookupError: error instanceof Error ? error.message : String(error)
225
+ };
226
+ }
227
+ }
123
228
  async resolveBranchHead(repoUrl, branch) {
124
229
  const { stdout } = await execFileAsync("git", ["ls-remote", repoUrl, branch], { cwd: process.cwd() });
125
230
  const line = stdout.trim().split("\n").find(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Developer CLI for running the Vidfarm local template platform.",
5
5
  "type": "module",
6
6
  "bin": {