@mevdragon/vidfarm-devcli 0.2.0 → 0.2.2

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.
@@ -220,6 +220,40 @@ function shell(input) {
220
220
  color: var(--muted);
221
221
  background: rgba(255,255,255,0.7);
222
222
  }
223
+ .status-row {
224
+ display: flex;
225
+ flex-wrap: wrap;
226
+ gap: 10px;
227
+ align-items: center;
228
+ }
229
+ .status-pill {
230
+ display: inline-flex;
231
+ align-items: center;
232
+ gap: 8px;
233
+ padding: 8px 12px;
234
+ border: 1px solid var(--line);
235
+ background: rgba(255,255,255,0.78);
236
+ color: var(--ink);
237
+ letter-spacing: 0.08em;
238
+ text-transform: uppercase;
239
+ font-size: 12px;
240
+ }
241
+ .status-pill::before {
242
+ content: "";
243
+ width: 9px;
244
+ height: 9px;
245
+ border-radius: 999px;
246
+ background: #7a6d61;
247
+ }
248
+ .status-pill.is-active::before {
249
+ background: var(--accent);
250
+ box-shadow: 0 0 0 4px rgba(159, 61, 36, 0.12);
251
+ }
252
+ .developer-panel {
253
+ background:
254
+ linear-gradient(135deg, rgba(159, 61, 36, 0.11), rgba(255,255,255,0.68)),
255
+ rgba(255,255,255,0.62);
256
+ }
223
257
  .key-list, .attachment-list {
224
258
  display: grid;
225
259
  gap: 12px;
@@ -402,6 +436,21 @@ export function renderSettingsPage(input) {
402
436
  data-copy-content='${escapeAttribute(input.developerSkill)}'
403
437
  data-copy-label="Developer SKILL.md"
404
438
  >Developer SKILL.md</button>
439
+ ` : "";
440
+ const developerPanel = input.isDeveloper ? `
441
+ <section class="settings-panel developer-panel">
442
+ <div class="card-head">
443
+ <div>
444
+ <div class="pill">Developer Access</div>
445
+ <h2>Template developer tools are enabled</h2>
446
+ </div>
447
+ <div class="status-pill is-active">Developer</div>
448
+ </div>
449
+ <p class="helper">This account can submit template sources and use the developer workflow. The <code>Developer SKILL.md</code> prompt is available here for fast copy into your agent tooling.</p>
450
+ <div class="toolbar">
451
+ ${developerSkillButton}
452
+ </div>
453
+ </section>
405
454
  ` : "";
406
455
  return shell({
407
456
  title: "Settings",
@@ -423,7 +472,10 @@ export function renderSettingsPage(input) {
423
472
  <div class="pill">Profile</div>
424
473
  <h2>Workspace identity</h2>
425
474
  </div>
426
- <div class="tiny">${input.isPaidPlan ? "Paid plan" : "Inactive plan"}</div>
475
+ <div class="status-row">
476
+ <div class="status-pill ${input.isPaidPlan ? "is-active" : ""}">${input.isPaidPlan ? "Paid plan" : "Inactive plan"}</div>
477
+ <div class="status-pill ${input.isDeveloper ? "is-active" : ""}">${input.isDeveloper ? "Developer" : "Standard"}</div>
478
+ </div>
427
479
  </div>
428
480
  <form method="post" action="/settings/profile">
429
481
  <label for="settings-email">Email</label>
@@ -440,10 +492,11 @@ export function renderSettingsPage(input) {
440
492
  </div>
441
493
  </form>
442
494
  </section>
495
+ ${developerPanel}
443
496
  <section class="settings-panel">
444
497
  <div class="pill">Vidfarm API</div>
445
- <h2>Your control-plane key</h2>
446
- <p class="helper">Use this key with the <code>vidfarm-user-id</code> header pair when calling the API directly.</p>
498
+ <h2>Your Vidfarm API key</h2>
499
+ <p class="helper">Use this key with the <code>vidfarm-user-id</code> header pair when calling the Vidfarm API directly.</p>
447
500
  <label for="vidfarm-api-key">Current API key</label>
448
501
  <div class="secret-wrap">
449
502
  <input id="vidfarm-api-key" type="password" value="${escapeHtml(input.vidfarmApiKey)}" readonly />
@@ -457,7 +510,6 @@ export function renderSettingsPage(input) {
457
510
  data-copy-content='${escapeAttribute(input.directorSkill)}'
458
511
  data-copy-label="Director SKILL.md"
459
512
  >Director SKILL.md</button>
460
- ${developerSkillButton}
461
513
  </div>
462
514
  </section>
463
515
  <section class="settings-panel">
@@ -506,7 +558,7 @@ export function renderSettingsPage(input) {
506
558
  <section class="settings-panel">
507
559
  <div class="pill">Attachments</div>
508
560
  <h2>Shared files</h2>
509
- <p class="helper">Uploads are stored under your <code>user/*</code> storage path and can be opened via public read URLs.</p>
561
+ <p class="helper">Uploads are stored under your <code>user/&lt;user_id&gt;/*</code> namespace and can be opened via public read URLs.</p>
510
562
  <form method="post" action="/settings/attachments" enctype="multipart/form-data">
511
563
  <label for="settings-files">Images, video, audio, PDF, Markdown, or text</label>
512
564
  <input id="settings-files" type="file" name="files" multiple accept="image/*,video/*,audio/*,.pdf,.md,.txt,text/markdown,text/plain,application/pdf" />
package/dist/src/app.js CHANGED
@@ -70,6 +70,17 @@ const listJobsQuerySchema = z.object({
70
70
  function getLoginMode(value) {
71
71
  return value === "password" ? "password" : "otp";
72
72
  }
73
+ function readRootSkillFile(...filenames) {
74
+ for (const filename of filenames) {
75
+ try {
76
+ return readFileSync(path.resolve(filename), "utf8");
77
+ }
78
+ catch {
79
+ continue;
80
+ }
81
+ }
82
+ return "";
83
+ }
73
84
  const allowedAttachmentExtensions = new Set([
74
85
  ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg",
75
86
  ".mp4", ".mov", ".webm", ".m4v",
@@ -361,23 +372,9 @@ app.get("/settings", (c) => {
361
372
  if (!customer) {
362
373
  return redirect(c, "/login");
363
374
  }
364
- const directorSkill = (() => {
365
- try {
366
- return readFileSync(path.resolve("SKILL.user.md"), "utf8");
367
- }
368
- catch {
369
- return "";
370
- }
371
- })();
375
+ const directorSkill = readRootSkillFile("SKILL.director.md", "SKILL.user.md");
372
376
  const developerSkill = customer.isDeveloper
373
- ? (() => {
374
- try {
375
- return readFileSync(path.resolve("SKILL.developer.md"), "utf8");
376
- }
377
- catch {
378
- return null;
379
- }
380
- })()
377
+ ? readRootSkillFile("SKILL.developer.md") || null
381
378
  : null;
382
379
  return c.html(renderSettingsPage({
383
380
  notice: c.req.query("notice") ?? null,
@@ -476,7 +473,7 @@ app.post("/settings/attachments", async (c) => {
476
473
  const arrayBuffer = await file.arrayBuffer();
477
474
  const buffer = Buffer.from(arrayBuffer);
478
475
  const attachmentId = createId("att");
479
- const storageKey = `user/${customer.id}/attachments/${attachmentId}/${fileName}`;
476
+ const storageKey = storage.userAttachmentKey(customer.id, attachmentId, fileName);
480
477
  const stored = await storage.putBuffer(storageKey, buffer, contentType, { publicRead: true });
481
478
  database.createUserAttachment({
482
479
  id: attachmentId,
@@ -610,14 +607,17 @@ function buildAbsoluteUrl(c, pathname) {
610
607
  }
611
608
  function resolveTemplateAboutStorageKey(templateId, entry) {
612
609
  const normalizedEntry = entry.replace(/^\/+/, "");
610
+ const templateAboutPrefix = storage.templateAboutKey(templateId, "");
613
611
  if (!/^https?:\/\//i.test(entry)) {
614
- const templateAboutPrefix = `templates/${templateId}/about/`;
612
+ if (normalizedEntry.startsWith("templates/")) {
613
+ return normalizedEntry;
614
+ }
615
615
  if (normalizedEntry.startsWith(templateAboutPrefix)) {
616
616
  return normalizedEntry;
617
617
  }
618
618
  return normalizedEntry.startsWith("about/")
619
- ? `templates/${templateId}/${normalizedEntry}`
620
- : `templates/${templateId}/about/${normalizedEntry}`;
619
+ ? joinStoragePath("templates", templateId, normalizedEntry)
620
+ : storage.templateAboutKey(templateId, normalizedEntry);
621
621
  }
622
622
  try {
623
623
  const url = new URL(entry);
@@ -657,7 +657,7 @@ async function serveTemplateAboutAsset(c, templateId, assetPath) {
657
657
  if (!normalizedAssetPath) {
658
658
  return c.json({ error: "Template about asset path is required" }, 400);
659
659
  }
660
- const key = `templates/${template.id}/about/${normalizedAssetPath}`;
660
+ const key = storage.templateAboutKey(template.id, normalizedAssetPath);
661
661
  const readUrl = await storage.getReadUrl(key);
662
662
  if (config.STORAGE_DRIVER === "s3" && readUrl) {
663
663
  return c.redirect(readUrl, 302);
@@ -742,7 +742,10 @@ function resolveTemplateAboutMediaUrl(c, templateId, entry) {
742
742
  return entry;
743
743
  }
744
744
  const normalizedEntry = entry.replace(/^\/+/, "");
745
- const templateAboutPrefix = `templates/${templateId}/about/`;
745
+ if (normalizedEntry.startsWith("templates/")) {
746
+ return buildAbsoluteUrl(c, `/template-media?key=${encodeURIComponent(normalizedEntry)}`);
747
+ }
748
+ const templateAboutPrefix = storage.templateAboutKey(templateId, "");
746
749
  const aboutPath = normalizedEntry.startsWith(templateAboutPrefix)
747
750
  ? `about/${normalizedEntry.slice(templateAboutPrefix.length)}`
748
751
  : normalizedEntry.startsWith("about/")
@@ -750,6 +753,13 @@ function resolveTemplateAboutMediaUrl(c, templateId, entry) {
750
753
  : `about/${normalizedEntry}`;
751
754
  return buildAbsoluteUrl(c, `${TEMPLATES_PREFIX}/${templateId}/${aboutPath}`);
752
755
  }
756
+ function joinStoragePath(...parts) {
757
+ return parts
758
+ .flatMap((part) => part.split("/"))
759
+ .map((part) => part.trim())
760
+ .filter(Boolean)
761
+ .join("/");
762
+ }
753
763
  app.get(TEMPLATES_PREFIX, (c) => c.json({
754
764
  templates: templateRegistry.list().map((template) => ({
755
765
  ...serializeTemplate(c, template),
@@ -26,7 +26,7 @@ const schema = z.object({
26
26
  AWS_SECRET_ACCESS_KEY: z.string().optional(),
27
27
  PUBLIC_BASE_URL: z.string().optional(),
28
28
  RESEND_API_KEY: z.string().optional(),
29
- RESEND_FROM_EMAIL: z.string().default("noreply@example.com"),
29
+ RESEND_FROM_EMAIL: z.string().default("vidfarm@fwd.zoomgtm.com"),
30
30
  OPENAI_API_KEY: z.string().optional(),
31
31
  OPENROUTER_API_KEY: z.string().optional(),
32
32
  GEMINI_API_KEY: z.string().optional(),
@@ -3,7 +3,7 @@ import { database } from "./db.js";
3
3
  import { createId } from "./lib/ids.js";
4
4
  export function createTemplateJobContext(input) {
5
5
  const templateConfig = database.getTemplateConfig(input.customer.id, input.template.id);
6
- const prefix = `templates/${input.template.id}/users/${input.customer.id}/jobs/${input.job.id}`;
6
+ const prefix = input.storage.templateJobPrefix(input.template.id, input.customer.id, input.job.id);
7
7
  return {
8
8
  env: config.isProduction ? "production" : "development",
9
9
  customer: input.customer,
@@ -14,6 +14,21 @@ export class StorageService {
14
14
  constructor() {
15
15
  mkdirSync(this.localRoot, { recursive: true });
16
16
  }
17
+ userAttachmentKey(customerId, attachmentId, fileName) {
18
+ return joinStorageKey("user", customerId, "attachments", attachmentId, fileName);
19
+ }
20
+ developerAttachmentKey(customerId, attachmentId, fileName) {
21
+ return joinStorageKey("developer", customerId, "attachments", attachmentId, fileName);
22
+ }
23
+ templateJobPrefix(templateId, customerId, jobId) {
24
+ return joinStorageKey("templates", templateId, "users", customerId, "jobs", jobId);
25
+ }
26
+ templateJobKey(templateId, customerId, jobId, key) {
27
+ return joinStorageKey(this.templateJobPrefix(templateId, customerId, jobId), key);
28
+ }
29
+ templateAboutKey(templateId, assetPath) {
30
+ return joinStorageKey("templates", templateId, "about", assetPath);
31
+ }
17
32
  async putJson(key, value, options) {
18
33
  const body = JSON.stringify(value, null, 2);
19
34
  return this.putBuffer(key, Buffer.from(body, "utf8"), "application/json", options);
@@ -86,6 +101,13 @@ export class StorageService {
86
101
  return this.getPublicUrl(key);
87
102
  }
88
103
  }
104
+ function joinStorageKey(...parts) {
105
+ return parts
106
+ .flatMap((part) => part.split("/"))
107
+ .map((part) => part.trim())
108
+ .filter(Boolean)
109
+ .join("/");
110
+ }
89
111
  function inferContentType(filePath) {
90
112
  switch (path.extname(filePath).toLowerCase()) {
91
113
  case ".json":
@@ -11,7 +11,7 @@ const TEMPLATE_ID = "4c7a7e1a-7f35-4f30-9f86-9c8a63c7f2db";
11
11
  const TEMPLATE_SLUG_ID = "template_0000";
12
12
  const COMPOSITION_ID = "template-0000";
13
13
  const TEMPLATE_PREVIEW_MEDIA = [
14
- "https://vidfarmprodstack-vidfarmbucket335ee12f-0vsvtd5earqy.s3.us-east-1.amazonaws.com/templates/template-0000/about/preview-01.jpg",
14
+ "templates/template-0000/about/preview-01.jpg",
15
15
  ];
16
16
  const FRAME = { width: 1080, height: 1920 };
17
17
  const TIKTOK_SAFE_AREA = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Developer CLI for running the Vidfarm local template platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,12 +8,12 @@
8
8
  "vidfarm-devcli": "dist/src/cli.js"
9
9
  },
10
10
  "files": [
11
- "dist",
11
+ "dist/src",
12
+ "dist/templates/template_0000",
12
13
  "templates/template_0000",
13
14
  "README.md",
14
15
  "SKILL.developer.md",
15
- "PLATFORM_SPEC.md",
16
- "AWS_REMOTION_HANDOFF.md",
16
+ "GETTING_STARTED.developers.md",
17
17
  ".env.example"
18
18
  ],
19
19
  "publishConfig": {
@@ -2,12 +2,11 @@
2
2
 
3
3
  Standalone GitHub-distributable package for the Vidfarm `template_0000` starter template.
4
4
 
5
- This package exists for two operational reasons:
5
+ This package exists to give third-party developers a concrete example of the Vidfarm template contract.
6
6
 
7
- - keep the template code in its own private GitHub repo for manual admin review/import
8
- - preserve the exact checked-in config and build shape the release admin will later promote to shared Remotion AWS and the production Docker image
7
+ Developers should treat this repo as an authoring and review unit.
9
8
 
10
- Developers should treat this repo as an authoring and review unit, not as an authorized deployment unit.
9
+ When a template is ready, it is handed off for Vidfarm admin review and approval before it becomes available on the hosted platform.
11
10
 
12
11
  Every new template should keep its source-format research checked in:
13
12
 
@@ -15,33 +14,21 @@ Every new template should keep its source-format research checked in:
15
14
  - `research/preview/` for the screenshots or source video the DNA analyzers inspect
16
15
  - `src/template-dna.ts` for the generated viral and visual DNA that feed the published template metadata
17
16
 
18
- The production release flow is:
19
-
20
- 1. developer pushes template code to GitHub
21
- 2. admin reviews the repo and selects a commit from `production`
22
- 3. admin publishes the approved Remotion site bundle to shared AWS
23
- 4. admin imports and activates the approved commit in Vidfarm
24
- 5. admin rebuilds and redeploys the production Docker image
25
-
26
- Template authors do not publish directly to shared Remotion Lambda and do not directly promote templates into production Docker.
27
-
28
17
  Production integration settings are checked into `template.config.json`. AI agents and developers should treat that file as the source of truth for:
29
18
 
30
19
  - project identity
31
20
  - `template_id` as the self-issued UUIDv4 platform identifier
32
21
  - `slug_id` as the human-readable stable slug
33
- - GitHub repo target
34
- - production source branch
35
- - Remotion region/function/bucket/site/composition settings
36
- - release control expectations for admin promotion
22
+ - the template module path
23
+ - the local Remotion composition settings used by the starter template
37
24
 
38
25
  ## Included
39
26
 
40
27
  - `SKILL.md` for customer AI-agent usage
41
28
  - `src/template.ts` for the Vidfarm platform contract
42
- - `src/remotion/*` for local and cloud video rendering
29
+ - `src/remotion/*` for local video rendering
43
30
  - `src/lib/images.ts` for non-cropping portrait normalization
44
- - `composition.json` sample props for cloud render smoke tests
31
+ - `composition.json` sample props for local render tests
45
32
 
46
33
  ## Template behavior
47
34
 
@@ -87,35 +74,3 @@ vidfarm analyze-visual-dna --template-dir .
87
74
  ```
88
75
 
89
76
  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.
90
-
91
- ## Shared Remotion AWS
92
-
93
- Expected shared infra:
94
-
95
- - region: stored in `template.config.json`
96
- - function: stored in `template.config.json`
97
- - bucket: stored in `template.config.json`
98
-
99
- The only runtime secrets expected from the execution environment are AWS credentials. The production Remotion topology is intentionally checked into `template.config.json`, not spread across `.env` values.
100
-
101
- These shared-AWS operations are release-admin tasks. Template developers should not run them against the shared production AWS account.
102
-
103
- Release admin create-site:
104
-
105
- ```bash
106
- npm run admin:create-site
107
- ```
108
-
109
- Release admin cloud render smoke test:
110
-
111
- ```bash
112
- npm run admin:render:cloud
113
- ```
114
-
115
- Release admin cloud render from a real stage-1 manifest:
116
-
117
- ```bash
118
- npm run admin:render:cloud -- --manifest /abs/path/to/template-0000.json
119
- ```
120
-
121
- Use the manifest mode when you want the cloud render verification to include the actual composited slide frames with text overlays, not just the static sample props.
@@ -5,8 +5,8 @@ Use this template when the customer wants a fast vertical slideshow for TikTok-s
5
5
  Operational rule:
6
6
 
7
7
  - template developers author and test this template locally
8
- - only the release admin publishes the approved Remotion site bundle to shared AWS
9
- - only the release admin promotes the approved template into production Docker
8
+ - the hosted platform later runs the approved template behind the standard Vidfarm API wrapper
9
+ - a Vidfarm admin reviews and approves the template before it is made available in production
10
10
 
11
11
  Inputs:
12
12
 
@@ -51,7 +51,7 @@ Allowed text background colors:
51
51
  - `light_gray`
52
52
  - `dark_gray`
53
53
 
54
- REST API routes for this template:
54
+ Typical routes for this template:
55
55
 
56
56
  - `GET /templates/template_0000`
57
57
  - `GET /templates/template_0000/skill`
@@ -2,19 +2,16 @@
2
2
  "name": "vidfarm_template_0000",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "description": "Standalone Vidfarm template_0000 project for authoring, admin review, and controlled production promotion.",
5
+ "description": "Standalone Vidfarm template_0000 starter project for local template authoring.",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "check": "tsc -p tsconfig.json --noEmit",
9
9
  "build": "tsc -p tsconfig.json",
10
- "studio": "remotion studio src/remotion/index.tsx",
11
- "admin:create-site": "node ./scripts/create-site.mjs",
12
- "admin:render:cloud": "node ./scripts/render-cloud.mjs"
10
+ "studio": "remotion studio src/remotion/index.tsx"
13
11
  },
14
12
  "dependencies": {
15
13
  "@remotion/bundler": "4.0.355",
16
14
  "@remotion/cli": "4.0.355",
17
- "@remotion/lambda": "4.0.355",
18
15
  "@remotion/renderer": "4.0.355",
19
16
  "react": "^18.3.1",
20
17
  "react-dom": "^18.3.1",
@@ -22,6 +19,7 @@
22
19
  "sharp": "^0.34.2"
23
20
  },
24
21
  "devDependencies": {
22
+ "@mevdragon/vidfarm-devcli": "^0.2.2",
25
23
  "@types/node": "^24.0.1",
26
24
  "@types/react": "^18.3.23",
27
25
  "@types/react-dom": "^18.3.7",