@mevdragon/vidfarm-devcli 0.2.14 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +11 -4
  2. package/SKILL.director.md +100 -0
  3. package/SKILL.platform.md +66 -6
  4. package/dist/src/cli.js +135 -1527
  5. package/package.json +5 -24
  6. package/.env.example +0 -60
  7. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +0 -275
  8. package/auto-create-hyperframe-templates/extractor-system-prompt.md +0 -128
  9. package/auto-create-hyperframe-templates/input-manifest.schema.json +0 -56
  10. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +0 -118
  11. package/auto-create-hyperframe-templates/production-graph.schema.json +0 -314
  12. package/auto-create-hyperframe-templates/runbook.md +0 -198
  13. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +0 -368
  14. package/auto-create-hyperframe-templates/template-plan.schema.json +0 -311
  15. package/auto-create-hyperframe-templates/template-planner-prompt.md +0 -144
  16. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +0 -599
  17. package/auto-create-templates/extractor-system-prompt.md +0 -119
  18. package/auto-create-templates/production-graph.schema.json +0 -276
  19. package/auto-create-templates/runbook.md +0 -140
  20. package/auto-create-templates/template-plan.schema.json +0 -301
  21. package/auto-create-templates/template-planner-prompt.md +0 -137
  22. package/dist/src/account-pages-legacy.js +0 -9338
  23. package/dist/src/account-pages-legacy.js.map +0 -1
  24. package/dist/src/account-pages.js +0 -52
  25. package/dist/src/account-pages.js.map +0 -1
  26. package/dist/src/app.js +0 -13711
  27. package/dist/src/app.js.map +0 -1
  28. package/dist/src/cli.js.map +0 -1
  29. package/dist/src/composition-runtime.js +0 -581
  30. package/dist/src/composition-runtime.js.map +0 -1
  31. package/dist/src/config.js +0 -140
  32. package/dist/src/config.js.map +0 -1
  33. package/dist/src/context.js +0 -492
  34. package/dist/src/context.js.map +0 -1
  35. package/dist/src/dev-app-legacy.js +0 -694
  36. package/dist/src/dev-app-legacy.js.map +0 -1
  37. package/dist/src/dev-app.js +0 -6
  38. package/dist/src/dev-app.js.map +0 -1
  39. package/dist/src/dev-serve.js.map +0 -1
  40. package/dist/src/domain.js +0 -2
  41. package/dist/src/domain.js.map +0 -1
  42. package/dist/src/editor-chat-history.js +0 -82
  43. package/dist/src/editor-chat-history.js.map +0 -1
  44. package/dist/src/editor-chat.js +0 -312
  45. package/dist/src/editor-chat.js.map +0 -1
  46. package/dist/src/editor-dark-theme.js +0 -1128
  47. package/dist/src/editor-dark-theme.js.map +0 -1
  48. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  49. package/dist/src/frontend/flockposter-cache-store.js.map +0 -1
  50. package/dist/src/frontend/homepage-client.js +0 -117
  51. package/dist/src/frontend/homepage-client.js.map +0 -1
  52. package/dist/src/frontend/homepage-shared.js +0 -4
  53. package/dist/src/frontend/homepage-shared.js.map +0 -1
  54. package/dist/src/frontend/homepage-store.js +0 -28
  55. package/dist/src/frontend/homepage-store.js.map +0 -1
  56. package/dist/src/frontend/homepage-view.js +0 -260
  57. package/dist/src/frontend/homepage-view.js.map +0 -1
  58. package/dist/src/frontend/page-runtime-client.js +0 -124
  59. package/dist/src/frontend/page-runtime-client.js.map +0 -1
  60. package/dist/src/frontend/page-runtime-store.js +0 -9
  61. package/dist/src/frontend/page-runtime-store.js.map +0 -1
  62. package/dist/src/frontend/sentry.js +0 -42
  63. package/dist/src/frontend/sentry.js.map +0 -1
  64. package/dist/src/frontend/template-editor-chat.js +0 -3760
  65. package/dist/src/frontend/template-editor-chat.js.map +0 -1
  66. package/dist/src/help-page.js +0 -333
  67. package/dist/src/help-page.js.map +0 -1
  68. package/dist/src/homepage.js +0 -919
  69. package/dist/src/homepage.js.map +0 -1
  70. package/dist/src/hyperframes/composition.js +0 -180
  71. package/dist/src/hyperframes/composition.js.map +0 -1
  72. package/dist/src/index.js +0 -16
  73. package/dist/src/index.js.map +0 -1
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/instrument.js.map +0 -1
  76. package/dist/src/lib/crypto.js +0 -45
  77. package/dist/src/lib/crypto.js.map +0 -1
  78. package/dist/src/lib/dev-log.js +0 -54
  79. package/dist/src/lib/dev-log.js.map +0 -1
  80. package/dist/src/lib/display-name.js +0 -11
  81. package/dist/src/lib/display-name.js.map +0 -1
  82. package/dist/src/lib/ids.js +0 -24
  83. package/dist/src/lib/ids.js.map +0 -1
  84. package/dist/src/lib/images.js +0 -19
  85. package/dist/src/lib/images.js.map +0 -1
  86. package/dist/src/lib/json.js +0 -15
  87. package/dist/src/lib/json.js.map +0 -1
  88. package/dist/src/lib/template-dna.js +0 -543
  89. package/dist/src/lib/template-dna.js.map +0 -1
  90. package/dist/src/lib/template-paths.js +0 -28
  91. package/dist/src/lib/template-paths.js.map +0 -1
  92. package/dist/src/lib/template-style-options.js +0 -203
  93. package/dist/src/lib/template-style-options.js.map +0 -1
  94. package/dist/src/lib/time.js +0 -7
  95. package/dist/src/lib/time.js.map +0 -1
  96. package/dist/src/lib/video-quality-harness.js +0 -60
  97. package/dist/src/lib/video-quality-harness.js.map +0 -1
  98. package/dist/src/page-runtime.js +0 -2
  99. package/dist/src/page-runtime.js.map +0 -1
  100. package/dist/src/page-shell.js +0 -1315
  101. package/dist/src/page-shell.js.map +0 -1
  102. package/dist/src/primitive-context.js +0 -299
  103. package/dist/src/primitive-context.js.map +0 -1
  104. package/dist/src/primitive-registry.js +0 -2181
  105. package/dist/src/primitive-registry.js.map +0 -1
  106. package/dist/src/primitive-sdk.js +0 -4
  107. package/dist/src/primitive-sdk.js.map +0 -1
  108. package/dist/src/primitives/remotion/html-image.js +0 -30
  109. package/dist/src/primitives/remotion/html-image.js.map +0 -1
  110. package/dist/src/primitives/remotion/media-slideshow.js +0 -60
  111. package/dist/src/primitives/remotion/media-slideshow.js.map +0 -1
  112. package/dist/src/react-page-shell.js +0 -35
  113. package/dist/src/react-page-shell.js.map +0 -1
  114. package/dist/src/ready-post-schedule-component.js +0 -1515
  115. package/dist/src/ready-post-schedule-component.js.map +0 -1
  116. package/dist/src/registry.js +0 -296
  117. package/dist/src/registry.js.map +0 -1
  118. package/dist/src/runtime.js +0 -31
  119. package/dist/src/runtime.js.map +0 -1
  120. package/dist/src/services/api-call-history.js +0 -249
  121. package/dist/src/services/api-call-history.js.map +0 -1
  122. package/dist/src/services/auth.js +0 -152
  123. package/dist/src/services/auth.js.map +0 -1
  124. package/dist/src/services/billing.js +0 -231
  125. package/dist/src/services/billing.js.map +0 -1
  126. package/dist/src/services/chat-threads.js +0 -92
  127. package/dist/src/services/chat-threads.js.map +0 -1
  128. package/dist/src/services/fork-access.js +0 -93
  129. package/dist/src/services/fork-access.js.map +0 -1
  130. package/dist/src/services/fork-manifest.js +0 -43
  131. package/dist/src/services/fork-manifest.js.map +0 -1
  132. package/dist/src/services/ghostcut.js +0 -132
  133. package/dist/src/services/ghostcut.js.map +0 -1
  134. package/dist/src/services/hyperframes.js +0 -1014
  135. package/dist/src/services/hyperframes.js.map +0 -1
  136. package/dist/src/services/job-capacity.js +0 -14
  137. package/dist/src/services/job-capacity.js.map +0 -1
  138. package/dist/src/services/job-logs.js +0 -197
  139. package/dist/src/services/job-logs.js.map +0 -1
  140. package/dist/src/services/jobs.js +0 -136
  141. package/dist/src/services/jobs.js.map +0 -1
  142. package/dist/src/services/media-processing.js +0 -743
  143. package/dist/src/services/media-processing.js.map +0 -1
  144. package/dist/src/services/primitive-media-lambda.js +0 -280
  145. package/dist/src/services/primitive-media-lambda.js.map +0 -1
  146. package/dist/src/services/providers.js +0 -2870
  147. package/dist/src/services/providers.js.map +0 -1
  148. package/dist/src/services/rate-limits.js +0 -237
  149. package/dist/src/services/rate-limits.js.map +0 -1
  150. package/dist/src/services/remotion.js +0 -688
  151. package/dist/src/services/remotion.js.map +0 -1
  152. package/dist/src/services/serverless-auth.js +0 -374
  153. package/dist/src/services/serverless-auth.js.map +0 -1
  154. package/dist/src/services/serverless-jobs.js +0 -1074
  155. package/dist/src/services/serverless-jobs.js.map +0 -1
  156. package/dist/src/services/serverless-provider-keys.js +0 -401
  157. package/dist/src/services/serverless-provider-keys.js.map +0 -1
  158. package/dist/src/services/serverless-records.js +0 -1088
  159. package/dist/src/services/serverless-records.js.map +0 -1
  160. package/dist/src/services/serverless-template-configs.js +0 -67
  161. package/dist/src/services/serverless-template-configs.js.map +0 -1
  162. package/dist/src/services/storage.js +0 -383
  163. package/dist/src/services/storage.js.map +0 -1
  164. package/dist/src/services/template-certification.js +0 -475
  165. package/dist/src/services/template-certification.js.map +0 -1
  166. package/dist/src/services/template-loader.js +0 -99
  167. package/dist/src/services/template-loader.js.map +0 -1
  168. package/dist/src/services/template-runtime-bundles.js +0 -217
  169. package/dist/src/services/template-runtime-bundles.js.map +0 -1
  170. package/dist/src/services/template-sources.js +0 -1017
  171. package/dist/src/services/template-sources.js.map +0 -1
  172. package/dist/src/services/video-normalization.js +0 -2
  173. package/dist/src/services/video-normalization.js.map +0 -1
  174. package/dist/src/services/webhooks.js +0 -62
  175. package/dist/src/services/webhooks.js.map +0 -1
  176. package/dist/src/template-editor-pages.js +0 -2491
  177. package/dist/src/template-editor-pages.js.map +0 -1
  178. package/dist/src/template-editor-shell.js +0 -2702
  179. package/dist/src/template-editor-shell.js.map +0 -1
  180. package/dist/src/template-sdk.js +0 -4
  181. package/dist/src/template-sdk.js.map +0 -1
  182. package/dist/src/worker.js +0 -17
  183. package/dist/src/worker.js.map +0 -1
  184. package/public/assets/homepage-app.js +0 -54
  185. package/public/assets/homepage-client-app.js +0 -80
  186. package/public/assets/page-runtime-client-app.js +0 -94
  187. package/readme.secret.md +0 -89
  188. package/templates/vidfarm_template_0000/README.md +0 -106
  189. package/templates/vidfarm_template_0000/SKILL.md +0 -266
  190. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  191. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  192. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  193. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  194. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  195. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  196. package/templates/vidfarm_template_0000/composition.json +0 -11
  197. package/templates/vidfarm_template_0000/src/lib/images.js +0 -221
  198. package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
  199. package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
  200. package/templates/vidfarm_template_0000/src/sdk.js +0 -3
  201. package/templates/vidfarm_template_0000/src/style-options.js +0 -200
  202. package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
  203. package/templates/vidfarm_template_0000/src/template.js +0 -1566
  204. package/templates/vidfarm_template_0000/template.config.json +0 -21
package/dist/src/cli.js CHANGED
@@ -1,1572 +1,180 @@
1
1
  #!/usr/bin/env node
2
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { execFile } from "node:child_process";
4
- import { createHash, randomUUID } from "node:crypto";
5
- import os from "node:os";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
3
  import path from "node:path";
7
4
  import { parseArgs } from "node:util";
8
- import { promisify } from "node:util";
9
- import dotenv from "dotenv";
10
- import { CAPTION_STANDARD_VERSION, TIKTOK_CAPTION_SHADOWS, STARTER_TEMPLATE_FONT_OPTIONS, STARTER_TEMPLATE_TEXT_BACKGROUND_COLOR_OPTIONS, TIKTOK_CAPTION_RENDERING_RULES, TIKTOK_CAPTION_STYLE_PRESETS, TIKTOK_TEXT_BACKGROUND_RENDERING } from "./lib/template-style-options.js";
11
- import { analyzeTemplateDna, hasTemplatePreviewMedia, stageTemplateDnaInputs, syncTemplateDnaModule } from "./lib/template-dna.js";
12
- import { assertTemplateFolderNameHasPrefix, defaultSkillPathForTemplateModule, deriveTemplateRootDirFromModulePath } from "./lib/template-paths.js";
13
- const execFileAsync = promisify(execFile);
14
- function printDeprecationBanner() {
15
- const red = "\x1b[31m";
16
- const yellow = "\x1b[33m";
17
- const bold = "\x1b[1m";
18
- const reset = "\x1b[0m";
19
- const line = `${red}${bold}${"═".repeat(78)}${reset}`;
20
- console.warn("");
21
- console.warn(line);
22
- console.warn(`${red}${bold} ⚠ DEPRECATED: @mevdragon/vidfarm-devcli is no longer maintained.${reset}`);
23
- console.warn(line);
24
- console.warn(`${yellow} Vidfarm has migrated from the old REST-API template model to the${reset}`);
25
- console.warn(`${yellow} Trackpad Editor (timeline-based composition editor in the browser).${reset}`);
26
- console.warn("");
27
- console.warn(`${yellow} This CLI was built around the OLD paradigm — REST-API templates with${reset}`);
28
- console.warn(`${yellow} src/template.ts, workflows, operations, viral DNA extraction, and the${reset}`);
29
- console.warn(`${yellow} hosted publish/approve/activate release flow. That paradigm is gone.${reset}`);
30
- console.warn("");
31
- console.warn(`${yellow} Directors: open ${bold}https://vidfarm.cc/editor/${reset}${yellow} to edit compositions.${reset}`);
32
- console.warn(`${yellow} Template authors: see SKILL.platform.md and auto-create-hyperframe-templates/.${reset}`);
33
- console.warn("");
34
- console.warn(`${yellow} Commands may partially work but are not supported. No fixes will land.${reset}`);
35
- console.warn(line);
36
- console.warn("");
37
- }
5
+ // vidfarm-devcli thin CLI that pairs a local composition directory with the
6
+ // hosted Trackpad Editor at https://vidfarm.cc/editor/. Point it at a template
7
+ // id, and it: resolves the user's fork, downloads the current composition,
8
+ // starts a local dev-serve, and prints the editor URL to open in the browser.
9
+ const DEFAULT_HOST = "https://vidfarm.cc";
10
+ const DEFAULT_PORT = 4321;
11
+ const HELP = `vidfarm-devcli — local editor bridge for the Vidfarm Trackpad Editor
12
+
13
+ Usage:
14
+ vidfarm-devcli <template_id> [options] Start local editor session
15
+ vidfarm-devcli dev-serve [options] Serve an existing local dir only
16
+ vidfarm-devcli --help Show this help
17
+
18
+ Options (edit mode):
19
+ --port <n> Local dev-serve port (default: ${DEFAULT_PORT})
20
+ --dir <path> Local composition directory (default: .vidfarm/<template_id>)
21
+ --host <url> Vidfarm host to fetch from (default: ${DEFAULT_HOST})
22
+ --fork <id> Fork id to fetch (default: auto-resolve for the current user)
23
+ --api-key <key> Vidfarm API key (or set VIDFARM_API_KEY)
24
+ --share <token> Share token for a shared fork (public read)
25
+ --refetch Overwrite local composition files with the latest remote copy
26
+
27
+ Options (dev-serve mode):
28
+ --dir <path> Directory to serve (default: current dir)
29
+ --port <n> Port to listen on (default: ${DEFAULT_PORT})
30
+ `;
38
31
  void main().catch((error) => {
39
32
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
40
33
  process.exit(1);
41
34
  });
42
35
  async function main() {
43
- printDeprecationBanner();
44
- const command = process.argv[2] ?? "dev";
45
- if (command === "session") {
46
- await runSessionCommand(process.argv.slice(3));
47
- return;
48
- }
49
- if (command === "dev") {
50
- await runDevCommand(process.argv.slice(3));
36
+ const argv = process.argv.slice(2);
37
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
38
+ process.stdout.write(HELP);
51
39
  return;
52
40
  }
41
+ const command = argv[0];
53
42
  if (command === "dev-serve") {
54
43
  const { runDevServeCommand } = await import("./dev-serve.js");
55
- await runDevServeCommand(process.argv.slice(3));
56
- return;
57
- }
58
- if (command === "validate-template") {
59
- await runValidateTemplateCommand(process.argv.slice(3));
60
- return;
61
- }
62
- if (command === "import-source") {
63
- await runImportSourceCommand(process.argv.slice(3));
64
- return;
65
- }
66
- if (command === "import-source-prod") {
67
- await runImportSourceProdCommand(process.argv.slice(3));
68
- return;
69
- }
70
- if (command === "register-source-hosted") {
71
- await runRegisterSourceHostedCommand(process.argv.slice(3));
72
- return;
73
- }
74
- if (command === "publish-update-hosted") {
75
- await runPublishUpdateHostedCommand(process.argv.slice(3));
76
- return;
77
- }
78
- if (command === "register-source-prod") {
79
- await runRegisterSourceProdCommand(process.argv.slice(3));
80
- return;
81
- }
82
- if (command === "deploy-template-cycle") {
83
- await runDeployTemplateCycleCommand(process.argv.slice(3));
84
- return;
85
- }
86
- if (command === "generate-template") {
87
- await runGenerateTemplateCommand(process.argv.slice(3));
88
- return;
89
- }
90
- if (command === "autocreate-template" || command === "autocreate") {
91
- await runAutocreateTemplateCommand(process.argv.slice(3));
92
- return;
93
- }
94
- if (command === "copy-reference-template") {
95
- await runCopyReferenceTemplateCommand(process.argv.slice(3));
96
- return;
97
- }
98
- if (command === "analyze-viral-dna") {
99
- await runAnalyzeTemplateDnaCommand("viral", process.argv.slice(3));
44
+ await runDevServeCommand(argv.slice(1));
100
45
  return;
101
46
  }
102
- if (command === "analyze-visual-dna") {
103
- await runAnalyzeTemplateDnaCommand("visual", process.argv.slice(3));
47
+ if (command === "edit") {
48
+ await runEditCommand(argv.slice(1));
104
49
  return;
105
50
  }
106
- if (command === "presign-preview-media") {
107
- await runPresignPreviewMediaCommand(process.argv.slice(3));
51
+ if (!command.startsWith("-")) {
52
+ // Positional template id → default `edit` command.
53
+ await runEditCommand(argv);
108
54
  return;
109
55
  }
110
- throw new Error(`Unknown command: ${command}`);
111
- }
112
- async function runDevCommand(argv) {
113
- const parsed = parseArgs({
114
- args: argv,
115
- options: {
116
- port: { type: "string", default: "3000" },
117
- "data-dir": { type: "string", default: ".vidfarm/local" },
118
- "env-file": { type: "string", default: ".env" },
119
- email: { type: "string", default: "dev@vidfarm.local" },
120
- name: { type: "string", default: "Vidfarm Local Dev" },
121
- "api-key": { type: "string" },
122
- reset: { type: "boolean", default: false },
123
- "mock-ai": { type: "boolean" }
124
- }
125
- });
126
- const port = Number(parsed.values.port);
127
- const root = process.cwd();
128
- const dataDir = path.resolve(root, parsed.values["data-dir"]);
129
- const sessionPath = path.join(dataDir, "session.json");
130
- dotenv.config({
131
- path: path.resolve(root, parsed.values["env-file"]),
132
- override: false
133
- });
134
- if (parsed.values.reset) {
135
- rmSync(dataDir, { recursive: true, force: true });
136
- }
137
- mkdirSync(dataDir, { recursive: true });
138
- const hasProviderEnvKey = Boolean(process.env.OPENAI_API_KEY ||
139
- process.env.GEMINI_API_KEY ||
140
- process.env.OPENROUTER_API_KEY ||
141
- process.env.PERPLEXITY_API_KEY);
142
- process.env.NODE_ENV = process.env.NODE_ENV || "development";
143
- process.env.PORT = String(port);
144
- process.env.VIDFARM_DATA_DIR = dataDir;
145
- process.env.VIDFARM_RUNTIME_BACKEND = "serverless";
146
- process.env.STORAGE_DRIVER = "local";
147
- process.env.PUBLIC_BASE_URL = `http://127.0.0.1:${port}`;
148
- process.env.REMOTION_MODE = "local";
149
- process.env.VIDFARM_ADMIN_EMAILS = process.env.VIDFARM_ADMIN_EMAILS || parsed.values.email;
150
- process.env.MOCK_PROVIDER_RESPONSES =
151
- parsed.values["mock-ai"] ? "true" : process.env.MOCK_PROVIDER_RESPONSES ?? (hasProviderEnvKey ? "false" : "true");
152
- const [{ startRuntime }, { ServerlessAuthService }, { ServerlessProviderKeyService }, { config }, { createId }, crypto] = await Promise.all([
153
- import("./runtime.js"),
154
- import("./services/serverless-auth.js"),
155
- import("./services/serverless-provider-keys.js"),
156
- import("./config.js"),
157
- import("./lib/ids.js"),
158
- import("./lib/crypto.js")
159
- ]);
160
- const auth = new ServerlessAuthService();
161
- const providerKeys = new ServerlessProviderKeyService();
162
- const customer = await auth.getCustomerByEmailPublic(parsed.values.email) ?? await auth.upsertCustomer({
163
- id: createId("cus"),
164
- email: parsed.values.email,
165
- name: parsed.values.name,
166
- isDeveloper: true,
167
- isPaidPlan: true,
168
- planTier: "standalone"
169
- });
170
- const rawApiKey = parsed.values["api-key"] ?? `vf_local_${Math.random().toString(36).slice(2, 12)}`;
171
- await auth.insertApiKey({
172
- id: createId("api"),
173
- customerId: customer.id,
174
- keyHash: crypto.hashSecret(rawApiKey + config.API_KEY_SALT),
175
- rawValue: rawApiKey,
176
- label: "Local CLI session"
177
- });
178
- await seedEnvProviderKeys({
179
- customerId: customer.id,
180
- providerKeys,
181
- createId,
182
- encryptString: crypto.encryptString,
183
- encryptionSecret: config.ENCRYPTION_SECRET,
184
- allowMockPlaceholders: config.mockProviders
185
- });
186
- writeFileSync(sessionPath, JSON.stringify({
187
- baseUrl: process.env.PUBLIC_BASE_URL,
188
- customerId: customer.id,
189
- apiKey: rawApiKey,
190
- email: customer.email,
191
- dataDir
192
- }, null, 2));
193
- printStartupBanner({
194
- baseUrl: process.env.PUBLIC_BASE_URL,
195
- customerId: customer.id,
196
- apiKey: rawApiKey,
197
- email: customer.email,
198
- dataDir,
199
- sessionPath,
200
- mockProviders: config.mockProviders
201
- });
202
- const runtime = await startRuntime();
203
- for (const signal of ["SIGINT", "SIGTERM"]) {
204
- process.on(signal, () => {
205
- runtime.shutdown();
206
- process.exit(0);
207
- });
208
- }
209
- }
210
- async function runValidateTemplateCommand(argv) {
211
- const parsed = parseArgs({
212
- args: argv,
213
- options: {
214
- "template-id": { type: "string" },
215
- "module-path": { type: "string" },
216
- "skill-path": { type: "string" }
217
- }
218
- });
219
- const [{ templateRegistry }, { TemplateCertificationService }, { loadTemplateFromModule }] = await Promise.all([
220
- import("./registry.js"),
221
- import("./services/template-certification.js"),
222
- import("./services/template-loader.js")
223
- ]);
224
- const certification = new TemplateCertificationService();
225
- let template;
226
- let skillPath;
227
- if (parsed.values["module-path"]) {
228
- const modulePath = path.resolve(process.cwd(), parsed.values["module-path"]);
229
- template = await loadTemplateFromModule(modulePath);
230
- skillPath = path.resolve(process.cwd(), parsed.values["skill-path"] ?? "SKILL.md");
231
- const report = await certification.certify({ template, skillPath, modulePath });
232
- console.log(JSON.stringify(report, null, 2));
233
- if (!report.passed) {
234
- process.exitCode = 1;
235
- }
236
- return;
237
- }
238
- else {
239
- await templateRegistry.ensureInitialized();
240
- const templateId = parsed.values["template-id"];
241
- if (!templateId) {
242
- throw new Error("Provide --template-id or --module-path.");
243
- }
244
- template = templateRegistry.get(templateId);
245
- if (!template) {
246
- throw new Error(`Unknown template: ${templateId}`);
247
- }
248
- skillPath = template.skillPath;
249
- }
250
- if (!template || !skillPath) {
251
- throw new Error("Template or skill path not found for certification.");
252
- }
253
- const report = await certification.certify({ template, skillPath });
254
- console.log(JSON.stringify(report, null, 2));
255
- if (!report.passed) {
256
- process.exitCode = 1;
257
- }
258
- }
259
- async function runImportSourceCommand(argv) {
260
- const parsed = parseArgs({
261
- args: argv,
262
- options: {
263
- "template-id": { type: "string" },
264
- "slug-id": { type: "string" },
265
- "repo-url": { type: "string" },
266
- branch: { type: "string", default: "production" },
267
- "template-module-path": { type: "string" },
268
- "skill-path": { type: "string" },
269
- "install-command": { type: "string", default: "npm install" },
270
- "build-command": { type: "string", default: "npm run build" },
271
- "commit-sha": { type: "string" },
272
- activate: { type: "boolean", default: true }
273
- }
274
- });
275
- const templateId = parsed.values["template-id"];
276
- const slugId = parsed.values["slug-id"];
277
- const repoUrl = parsed.values["repo-url"];
278
- const templateModulePath = parsed.values["template-module-path"];
279
- if (!templateId || !slugId || !repoUrl || !templateModulePath) {
280
- throw new Error("import-source requires --template-id, --slug-id, --repo-url, and --template-module-path.");
281
- }
282
- if (!/(^|\/)template\.ts$/i.test(templateModulePath)) {
283
- throw new Error("import-source requires --template-module-path to point at the TypeScript entrypoint, usually src/template.ts.");
284
- }
285
- const skillPath = parsed.values["skill-path"] ?? defaultSkillPathForTemplateModule(templateModulePath);
286
- const [{ TemplateSourceService }, { templateRegistry }] = await Promise.all([
287
- import("./services/template-sources.js"),
288
- import("./registry.js")
289
- ]);
290
- const sources = new TemplateSourceService();
291
- const registration = await sources.registerSource({
292
- templateId,
293
- slugId,
294
- repoUrl,
295
- branch: parsed.values.branch,
296
- templateModulePath,
297
- skillPath,
298
- installCommand: parsed.values["install-command"],
299
- buildCommand: parsed.values["build-command"]
300
- });
301
- const release = await sources.importRelease({
302
- sourceId: registration.source.id,
303
- commitSha: parsed.values["commit-sha"] ?? null
304
- });
305
- let sourceRecord = registration.source;
306
- let activated = null;
307
- if (parsed.values.activate) {
308
- sourceRecord = sources.approveSource({ sourceId: registration.source.id });
309
- sources.approveRelease({ releaseId: release.id });
310
- const result = await sources.activateRelease({ releaseId: release.id });
311
- templateRegistry.registerRuntimeTemplate(result.template);
312
- activated = result.release;
313
- }
314
- console.log(JSON.stringify({
315
- source: sourceRecord,
316
- registration_action: registration.action,
317
- sync: registration.sync,
318
- release,
319
- activated_release: activated
320
- }, null, 2));
321
- }
322
- async function runGenerateTemplateCommand(argv) {
323
- const parsed = parseArgs({
324
- args: argv,
325
- options: {
326
- "template-id": { type: "string" },
327
- "slug-id": { type: "string" },
328
- "template-dir": { type: "string" },
329
- "project-name": { type: "string" },
330
- "github-repo": { type: "string" },
331
- "site-name": { type: "string" },
332
- "link-to-original": { type: "string" },
333
- "source-notes-path": { type: "string" },
334
- "source-preview-dir": { type: "string" },
335
- "dna-env-file": { type: "string", default: ".env" },
336
- "dna-model": { type: "string" },
337
- "skip-dna-analysis": { type: "boolean", default: false },
338
- branch: { type: "string", default: "production" },
339
- force: { type: "boolean", default: false }
340
- }
341
- });
342
- const result = await generateTemplateFromStarter({
343
- templateId: parsed.values["template-id"],
344
- slugId: parsed.values["slug-id"],
345
- templateDir: parsed.values["template-dir"],
346
- projectName: parsed.values["project-name"],
347
- githubRepo: parsed.values["github-repo"],
348
- siteName: parsed.values["site-name"],
349
- linkToOriginal: parsed.values["link-to-original"],
350
- sourceNotesPath: parsed.values["source-notes-path"],
351
- sourcePreviewDir: parsed.values["source-preview-dir"],
352
- dnaEnvFile: parsed.values["dna-env-file"],
353
- dnaModel: parsed.values["dna-model"],
354
- skipDnaAnalysis: parsed.values["skip-dna-analysis"],
355
- branch: parsed.values.branch,
356
- force: parsed.values.force
357
- });
358
- console.log(JSON.stringify(result, null, 2));
359
- }
360
- async function generateTemplateFromStarter(input) {
361
- const templateId = input.templateId ?? randomUUID();
362
- const slugId = input.slugId;
363
- const templateDir = input.templateDir;
364
- if (!slugId || !templateDir) {
365
- throw new Error("generate-template requires --slug-id and --template-dir. --template-id is optional and defaults to a new UUIDv4.");
366
- }
367
- const root = process.cwd();
368
- const starterDir = resolveStarterTemplateDir();
369
- const destinationDir = path.resolve(root, templateDir);
370
- const destinationFolderName = path.basename(destinationDir);
371
- assertTemplateFolderNameHasPrefix(destinationFolderName);
372
- if (!existsSync(starterDir)) {
373
- throw new Error(`Starter template not found: ${starterDir}`);
374
- }
375
- if (existsSync(destinationDir)) {
376
- if (!input.force) {
377
- throw new Error(`Destination already exists: ${destinationDir}`);
378
- }
379
- rmSync(destinationDir, { recursive: true, force: true });
380
- }
381
- cpSync(starterDir, destinationDir, {
382
- recursive: true,
383
- filter: (entry) => {
384
- const name = path.basename(entry);
385
- return ![".git", "node_modules", "dist", "package-lock.json"].includes(name);
386
- }
387
- });
388
- const projectName = input.projectName ?? toProjectName(destinationFolderName);
389
- const compositionId = slugId.replace(/_/g, "-");
390
- const siteName = input.siteName ?? compositionId;
391
- const githubRepo = input.githubRepo ?? `mevdragon/${projectName}`;
392
- rewriteTemplateStarter(destinationDir, [
393
- ["mevdragon/vidfarm_template_0000", githubRepo],
394
- ["vidfarm_template_0000", projectName],
395
- ["vidfarm-template-0000", siteName],
396
- ["template_0000", slugId],
397
- ["template-0000", compositionId],
398
- ["4c7a7e1a-7f35-4f30-9f86-9c8a63c7f2db", templateId],
399
- ['"production"', JSON.stringify(input.branch ?? "production")]
400
- ]);
401
- const stagedInputs = stageTemplateDnaInputs({
402
- templateDir: destinationDir,
403
- sourceNotesPath: input.sourceNotesPath,
404
- sourcePreviewDir: input.sourcePreviewDir
405
- });
406
- const moduleSync = syncTemplateDnaModule({
407
- templateDir: destinationDir,
408
- linkToOriginal: input.linkToOriginal
409
- });
410
- const dnaRuns = [];
411
- if (!input.skipDnaAnalysis && hasTemplatePreviewMedia(destinationDir)) {
412
- dnaRuns.push(await analyzeTemplateDna({
413
- mode: "viral",
414
- templateDir: destinationDir,
415
- envFile: input.dnaEnvFile ?? ".env",
416
- model: input.dnaModel,
417
- linkToOriginal: input.linkToOriginal
418
- }));
419
- dnaRuns.push(await analyzeTemplateDna({
420
- mode: "visual",
421
- templateDir: destinationDir,
422
- envFile: input.dnaEnvFile ?? ".env",
423
- model: input.dnaModel,
424
- linkToOriginal: input.linkToOriginal
425
- }));
426
- }
427
- return {
428
- template_id: templateId,
429
- slug_id: slugId,
430
- template_dir: destinationDir,
431
- project_name: projectName,
432
- github_repo: githubRepo,
433
- site_name: siteName,
434
- research: stagedInputs,
435
- template_dna_module: moduleSync,
436
- dna_analysis_runs: dnaRuns,
437
- required_template_contract: {
438
- template_fields: [
439
- "id",
440
- "slugId",
441
- "version",
442
- "about.title",
443
- "about.description",
444
- "about.difficulty",
445
- "about.viral_dna",
446
- "about.visual_dna",
447
- "about.preview_media",
448
- "about.link_to_original",
449
- "about.links_to_inspiration",
450
- "about.sound",
451
- "about.sound.sound_title",
452
- "about.sound.sound_url",
453
- "about.proposal_generator",
454
- "about.skeleton_prompt",
455
- "about.sample_prompts",
456
- "examples",
457
- "configSchema",
458
- ],
459
- operation_fields: [
460
- "operations.<name>.workflow",
461
- "operations.<name>.smokeTestPayload",
462
- "jobs.<workflow>",
463
- ],
464
- files: [
465
- "src/template.ts",
466
- "SKILL.md",
467
- ],
468
- },
469
- next_steps: [
470
- "Review src/template.ts and replace the starter proposal_generator with template-specific proposal instructions.",
471
- "Review src/template.ts and replace the starter skeleton_prompt with a fill-in-the-blanks prompt that fits the real format.",
472
- "Review src/template.ts and replace the starter sample_prompts with prompts that fit the new format.",
473
- "Verify AI calls use ctx.providers.* so the platform resolves an active customer provider key before generation. Operation payload provider fields must be optional explicit overrides, not z.enum(...).default(...), and parser helpers must not silently fall back to a provider the customer may not have.",
474
- "If DNA analysis was skipped or the source research changed, run analyze-viral-dna and analyze-visual-dna.",
475
- "Update SKILL.md so the usage examples match the template's real operations and payloads.",
476
- `Run validate-template against ${slugId} and launch at least one real local job before treating the template as ready.`,
477
- ],
478
- };
56
+ throw new Error(`Unknown option before command: ${command}\n\n${HELP}`);
479
57
  }
480
- async function runAutocreateTemplateCommand(argv) {
58
+ async function runEditCommand(argv) {
481
59
  const parsed = parseArgs({
482
60
  args: argv,
483
61
  allowPositionals: true,
484
62
  options: {
485
- "input-path": { type: "string" },
486
- "output-path": { type: "string" },
487
- "run-id": { type: "string" },
488
- "runs-dir": { type: "string", default: "auto-create-templates/runs" },
489
- "harness-dir": { type: "string" },
490
- "slug-id": { type: "string" },
491
- "template-id": { type: "string" },
492
- "project-name": { type: "string" },
493
- "github-repo": { type: "string" },
494
- "site-name": { type: "string" },
495
- "link-to-original": { type: "string" },
496
- "dna-env-file": { type: "string", default: ".env" },
497
- "dna-model": { type: "string" },
498
- "skip-dna-analysis": { type: "boolean", default: false },
499
- "skip-scaffold": { type: "boolean", default: false },
500
- "skip-media-probes": { type: "boolean", default: false },
501
- "skip-harness-snapshot": { type: "boolean", default: false },
502
- branch: { type: "string", default: "production" },
503
- force: { type: "boolean", default: false }
504
- }
505
- });
506
- const positionalInput = parsed.positionals[0];
507
- const positionalOutput = parsed.positionals[1];
508
- const inputPathRaw = parsed.values["input-path"] ?? positionalInput;
509
- const outputPathRaw = parsed.values["output-path"] ?? positionalOutput;
510
- if (!inputPathRaw || !outputPathRaw) {
511
- throw new Error("autocreate-template requires --input-path and --output-path, or positional args: autocreate-template <input_path> <output_path>.");
512
- }
513
- const root = process.cwd();
514
- const inputPath = path.resolve(root, inputPathRaw);
515
- const outputPath = path.resolve(root, outputPathRaw);
516
- if (!existsSync(inputPath)) {
517
- throw new Error(`Input path not found: ${inputPath}`);
518
- }
519
- const outputFolderName = path.basename(outputPath);
520
- assertTemplateFolderNameHasPrefix(outputFolderName);
521
- const slugId = parsed.values["slug-id"] ?? toSlugId(outputFolderName);
522
- const runId = parsed.values["run-id"] ?? `${toProjectName(slugId)}-${formatUtcTimestamp(new Date())}`;
523
- const runsDir = path.resolve(root, parsed.values["runs-dir"]);
524
- const runDir = path.join(runsDir, runId);
525
- const sourceDir = path.join(runDir, "source");
526
- const previewDir = path.join(sourceDir, "preview");
527
- const harnessDir = parsed.values["harness-dir"]
528
- ? path.resolve(root, parsed.values["harness-dir"])
529
- : resolveAutoCreateHarnessDir();
530
- createAutoCreateRunFolders(runDir);
531
- stageAutocreateSource({
532
- inputPath,
533
- sourceDir,
534
- previewDir,
535
- linkToOriginal: parsed.values["link-to-original"]
536
- });
537
- const mediaProbe = parsed.values["skip-media-probes"]
538
- ? { skipped: true, reason: "skip-media-probes was set" }
539
- : await prepareAutocreateMediaArtifacts({ runDir, previewDir });
540
- const harnessSnapshot = parsed.values["skip-harness-snapshot"]
541
- ? null
542
- : snapshotAutoCreateHarness({ harnessDir, runDir });
543
- const sourceNotesPath = path.join(sourceDir, "source.md");
544
- const scaffold = parsed.values["skip-scaffold"]
545
- ? null
546
- : await generateTemplateFromStarter({
547
- templateId: parsed.values["template-id"],
548
- slugId,
549
- templateDir: path.relative(root, outputPath),
550
- projectName: parsed.values["project-name"],
551
- githubRepo: parsed.values["github-repo"],
552
- siteName: parsed.values["site-name"],
553
- linkToOriginal: parsed.values["link-to-original"],
554
- sourceNotesPath,
555
- sourcePreviewDir: previewDir,
556
- dnaEnvFile: parsed.values["dna-env-file"],
557
- dnaModel: parsed.values["dna-model"],
558
- skipDnaAnalysis: parsed.values["skip-dna-analysis"],
559
- branch: parsed.values.branch,
560
- force: parsed.values.force
561
- });
562
- const agentPromptPath = path.join(runDir, "agent_prompt.md");
563
- const manifest = {
564
- mode: "autocreate-template",
565
- harness_entry: path.join(harnessDir, "AUTO_CREATE_TEMPLATES.md"),
566
- harness_snapshot: harnessSnapshot,
567
- developer_skill_context: harnessSnapshot?.developer_skill_path ?? resolveDeveloperSkillPath(),
568
- input_path: inputPath,
569
- output_path: outputPath,
570
- run_id: runId,
571
- run_dir: runDir,
572
- source_dir: sourceDir,
573
- source_notes_path: sourceNotesPath,
574
- source_preview_dir: previewDir,
575
- slug_id: slugId,
576
- scaffold,
577
- media_probe: mediaProbe,
578
- required_artifacts: {
579
- viral_steering_and_visual_preservation: path.join(runDir, "dna", "preservation_decision.json"),
580
- production_graph: path.join(runDir, "extraction", "production_graph.json"),
581
- template_plan: path.join(runDir, "plan", "template_plan.json")
582
- }
583
- };
584
- writeFileSync(path.join(runDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
585
- writeFileSync(agentPromptPath, renderAutocreateAgentPrompt(manifest), "utf8");
586
- console.log(JSON.stringify({
587
- ...manifest,
588
- agent_prompt_path: agentPromptPath,
589
- next_command_for_agent: `Use ${manifest.harness_entry} as the harness. Analyze ${sourceDir} as the source format, use run artifacts in ${runDir}, generate the production graph and template plan, then scaffold and implement a runnable Vidfarm template to ${outputPath}.`
590
- }, null, 2));
591
- }
592
- async function runRegisterSourceProdCommand(argv) {
593
- const input = parseProdTemplateCommandArgs(argv, {
594
- envFile: ".env.production",
595
- baseUrl: "https://vidfarm.cc",
596
- activate: false
597
- });
598
- const registration = await registerSourceViaApi(input);
599
- console.log(JSON.stringify({
600
- mode: "prod-register-source",
601
- base_url: input.baseUrl,
602
- source: registration.source,
603
- registration_action: registration.registration_action,
604
- sync: registration.sync ?? null
605
- }, null, 2));
606
- }
607
- async function runRegisterSourceHostedCommand(argv) {
608
- const input = parseProdTemplateCommandArgs(argv, {
609
- envFile: ".env",
610
- baseUrl: process.env.VIDFARM_BASE_URL ?? "https://vidfarm.cc",
611
- activate: false
612
- });
613
- const registration = await registerSourceViaApi(input);
614
- console.log(JSON.stringify({
615
- mode: "hosted-register-source",
616
- base_url: input.baseUrl,
617
- source: registration.source,
618
- registration_action: registration.registration_action,
619
- sync: registration.sync ?? null,
620
- next_step: "Platform admin reviews the source, imports a release, approves it, and activates it."
621
- }, null, 2));
622
- }
623
- async function runPublishUpdateHostedCommand(argv) {
624
- const input = parseProdTemplateCommandArgs(argv, {
625
- envFile: ".env",
626
- baseUrl: process.env.VIDFARM_BASE_URL ?? "https://vidfarm.cc",
627
- activate: false
628
- });
629
- if (!input.commitSha) {
630
- throw new Error("publish-update-hosted requires --commit-sha.");
631
- }
632
- const registration = await registerSourceViaApi(input);
633
- const artifact = await packageTemplateArtifact(input.repoDir, input.commitSha);
634
- const presign = await postProdTemplateApi(input, `/api/v1/templates/sources/${registration.source.id}/artifacts/presign`, {
635
- method: "POST",
636
- body: JSON.stringify({
637
- commit_sha: input.commitSha
638
- })
639
- });
640
- await uploadArtifactArchive(presign, artifact.buffer);
641
- const payload = await postProdTemplateApi(input, `/api/v1/templates/sources/${registration.source.id}/publish`, {
642
- method: "POST",
643
- body: JSON.stringify({
644
- commit_sha: input.commitSha,
645
- artifact_sha256: artifact.sha256
646
- })
647
- });
648
- console.log(JSON.stringify({
649
- mode: "hosted-publish-update",
650
- base_url: input.baseUrl,
651
- source: payload.source,
652
- release: payload.release,
653
- artifact: {
654
- artifact_key: presign.artifact_key,
655
- artifact_manifest_key: presign.artifact_manifest_key,
656
- sha256: artifact.sha256
657
- },
658
- publish_action: payload.publish_action,
659
- developer_handoff: payload.developer_handoff ?? null
660
- }, null, 2));
661
- }
662
- async function runImportSourceProdCommand(argv) {
663
- const input = parseProdTemplateCommandArgs(argv, {
664
- envFile: ".env.production",
665
- baseUrl: "https://vidfarm.cc",
666
- activate: true
667
- });
668
- const result = await importSourceIntoProd(input);
669
- console.log(JSON.stringify({
670
- mode: "prod-import-source",
671
- base_url: input.baseUrl,
672
- source: result.payload.source,
673
- release: result.payload.release,
674
- activated_release: result.payload.activated_release ?? null
675
- }, null, 2));
676
- }
677
- async function runDeployTemplateCycleCommand(argv) {
678
- const input = parseProdTemplateCommandArgs(argv, {
679
- envFile: ".env.production",
680
- baseUrl: "https://vidfarm.cc",
681
- stackName: "VidfarmProdStack",
682
- activate: true
683
- });
684
- if (!input.stackName) {
685
- throw new Error("deploy-template-cycle requires --stack-name.");
686
- }
687
- const importResult = await importSourceIntoProd(input);
688
- const currentImage = await getProdCurrentImage({
689
- stackName: input.stackName,
690
- envFile: input.envFile
691
- });
692
- await runLocalCommand("bash", [
693
- "scripts/deploy-prod-inplace.sh",
694
- "--env-file", input.envFile,
695
- "--stack-name", input.stackName,
696
- "--image-uri", currentImage.image,
697
- "--skip-check",
698
- "--skip-build",
699
- "--tag-suffix", `template-${input.slugId}`
700
- ]);
701
- console.log(JSON.stringify({
702
- mode: "deploy-template-cycle",
703
- stack_name: input.stackName,
704
- release: importResult.payload.activated_release ?? importResult.payload.release,
705
- restart_image: currentImage.image
706
- }, null, 2));
707
- }
708
- async function runCopyReferenceTemplateCommand(argv) {
709
- const parsed = parseArgs({
710
- args: argv,
711
- options: {
712
- "template-dir": { type: "string" },
713
- force: { type: "boolean", default: false }
714
- }
715
- });
716
- const templateDir = parsed.values["template-dir"];
717
- if (!templateDir) {
718
- throw new Error("copy-reference-template requires --template-dir.");
719
- }
720
- const root = process.cwd();
721
- const starterDir = resolveStarterTemplateDir();
722
- const destinationDir = path.resolve(root, templateDir);
723
- const destinationFolderName = path.basename(destinationDir);
724
- assertTemplateFolderNameHasPrefix(destinationFolderName);
725
- if (!existsSync(starterDir)) {
726
- throw new Error(`Starter template not found: ${starterDir}`);
727
- }
728
- if (existsSync(destinationDir)) {
729
- if (!parsed.values.force) {
730
- throw new Error(`Destination already exists: ${destinationDir}`);
731
- }
732
- rmSync(destinationDir, { recursive: true, force: true });
733
- }
734
- cpSync(starterDir, destinationDir, {
735
- recursive: true,
736
- filter: (entry) => {
737
- const name = path.basename(entry);
738
- return ![".git", "node_modules", "dist"].includes(name);
739
- }
740
- });
741
- console.log(JSON.stringify({
742
- copied_from: starterDir,
743
- template_dir: destinationDir,
744
- template_id: "4c7a7e1a-7f35-4f30-9f86-9c8a63c7f2db",
745
- slug_id: "template_0000",
746
- rewritten: false,
747
- required_template_contract: {
748
- template_fields: [
749
- "id",
750
- "slugId",
751
- "version",
752
- "about.title",
753
- "about.description",
754
- "about.difficulty",
755
- "about.viral_dna",
756
- "about.visual_dna",
757
- "about.preview_media",
758
- "about.link_to_original",
759
- "about.links_to_inspiration",
760
- "about.sound",
761
- "about.sound.sound_title",
762
- "about.sound.sound_url",
763
- "about.proposal_generator",
764
- "about.skeleton_prompt",
765
- "about.sample_prompts",
766
- "examples",
767
- "configSchema",
768
- ],
769
- operation_fields: [
770
- "operations.<name>.workflow",
771
- "operations.<name>.smokeTestPayload",
772
- "jobs.<workflow>",
773
- ],
774
- files: [
775
- "src/template.ts",
776
- "SKILL.md",
777
- ],
778
- },
779
- next_steps: [
780
- "Treat src/template.ts as the canonical entrypoint and review the required about metadata before copying patterns into a new template.",
781
- "Use the starter proposal_generator, skeleton_prompt, and sample_prompts as placeholders only; rewrite them for the real format you are building.",
782
- "Keep the template-local SKILL.md aligned with the operations and payloads in code.",
783
- ],
784
- }, null, 2));
785
- }
786
- async function runAnalyzeTemplateDnaCommand(mode, argv) {
787
- const parsed = parseArgs({
788
- args: argv,
789
- options: {
790
- "template-dir": { type: "string", default: "." },
791
- "env-file": { type: "string", default: ".env" },
792
- model: { type: "string" },
793
- "notes-path": { type: "string" },
794
- "preview-dir": { type: "string" },
795
- "output-path": { type: "string" },
796
- "link-to-original": { type: "string" },
797
- "sync-template-module": { type: "boolean", default: true }
798
- }
799
- });
800
- const result = await analyzeTemplateDna({
801
- mode,
802
- templateDir: parsed.values["template-dir"],
803
- envFile: parsed.values["env-file"],
804
- model: parsed.values.model,
805
- notesPath: parsed.values["notes-path"],
806
- previewDir: parsed.values["preview-dir"],
807
- outputPath: parsed.values["output-path"],
808
- linkToOriginal: parsed.values["link-to-original"],
809
- syncTemplateModule: parsed.values["sync-template-module"]
810
- });
811
- console.log(JSON.stringify(result, null, 2));
812
- }
813
- async function runPresignPreviewMediaCommand(argv) {
814
- const parsed = parseArgs({
815
- args: argv,
816
- options: {
817
- file: { type: "string" },
818
- "file-name": { type: "string" },
819
- "content-type": { type: "string" },
820
- directory: { type: "string" },
821
- "base-url": { type: "string" },
63
+ port: { type: "string", default: String(DEFAULT_PORT) },
64
+ dir: { type: "string" },
65
+ host: { type: "string", default: DEFAULT_HOST },
66
+ fork: { type: "string" },
822
67
  "api-key": { type: "string" },
823
- "env-file": { type: "string", default: ".env" }
68
+ share: { type: "string" },
69
+ refetch: { type: "boolean", default: false }
824
70
  }
825
71
  });
826
- loadOptionalEnvFile(parsed.values["env-file"]);
827
- const filePath = parsed.values.file ? path.resolve(process.cwd(), parsed.values.file) : null;
828
- if (filePath && !existsSync(filePath)) {
829
- throw new Error(`Preview media file not found: ${filePath}`);
72
+ const templateId = parsed.positionals[0];
73
+ if (!templateId) {
74
+ throw new Error(`edit requires a template id.\n\n${HELP}`);
830
75
  }
831
- const fileName = sanitizeCliUploadFileName(parsed.values["file-name"]
832
- ?? (filePath ? path.basename(filePath) : undefined));
833
- if (!fileName) {
834
- throw new Error("presign-preview-media requires --file or --file-name.");
76
+ const port = Number(parsed.values.port);
77
+ if (!Number.isFinite(port) || port <= 0) {
78
+ throw new Error(`Invalid --port: ${parsed.values.port}`);
835
79
  }
836
- const contentType = parsed.values["content-type"]
837
- ?? inferUploadContentType(fileName);
838
- const baseUrl = normalizeBaseUrl(parsed.values["base-url"] ?? process.env.VIDFARM_BASE_URL ?? "https://vidfarm.cc");
80
+ const host = trimTrailingSlash(parsed.values.host);
839
81
  const apiKey = parsed.values["api-key"] ?? process.env.VIDFARM_API_KEY;
840
- if (!apiKey) {
841
- throw new Error("Missing Vidfarm API key. Pass --api-key or set VIDFARM_API_KEY in your env file.");
842
- }
843
- const response = await fetch(`${baseUrl}/api/v1/user/me/developer/preview-media/presign`, {
844
- method: "POST",
845
- headers: {
846
- "content-type": "application/json",
847
- "vidfarm-api-key": apiKey
848
- },
849
- body: JSON.stringify({
850
- file_name: fileName,
851
- content_type: contentType,
852
- directory: parsed.values.directory
853
- })
854
- });
855
- const payload = await response.json();
856
- if (!response.ok) {
857
- throw new Error(typeof payload.error === "string" ? payload.error : `Preview-media presign failed with ${response.status}.`);
858
- }
859
- let uploadResult = null;
860
- if (filePath) {
861
- const body = readFileSync(filePath);
862
- const uploadResponse = await fetch(String(payload.upload?.url), {
863
- method: String(payload.upload?.method ?? "PUT"),
864
- headers: payload.upload?.headers,
865
- body
866
- });
867
- if (!uploadResponse.ok) {
868
- const errorText = await uploadResponse.text().catch(() => "");
869
- throw new Error(`Preview-media upload failed with ${uploadResponse.status}${errorText ? `: ${errorText}` : "."}`);
82
+ const shareToken = parsed.values.share;
83
+ const rootDir = path.resolve(process.cwd(), parsed.values.dir ?? path.join(".vidfarm", templateId));
84
+ mkdirSync(rootDir, { recursive: true });
85
+ const forkId = parsed.values.fork ?? await resolveForkId({ host, templateId, apiKey, shareToken });
86
+ if (!forkId) {
87
+ throw new Error(`Could not resolve a fork id for template ${templateId}. Pass --fork <id>, or open ${host}/editor/${templateId} once so a fork is created.`);
88
+ }
89
+ await fetchCompositionFiles({
90
+ host,
91
+ forkId,
92
+ dir: rootDir,
93
+ apiKey,
94
+ shareToken,
95
+ refetch: parsed.values.refetch
96
+ });
97
+ const devSource = `http://localhost:${port}`;
98
+ const editorUrl = `${host}/editor/${encodeURIComponent(templateId)}?fork=${encodeURIComponent(forkId)}&dev=${encodeURIComponent(devSource)}`;
99
+ printEditorBanner({ templateId, forkId, dir: rootDir, port, editorUrl });
100
+ const { runDevServeCommand } = await import("./dev-serve.js");
101
+ await runDevServeCommand(["--dir", rootDir, "--port", String(port)]);
102
+ }
103
+ async function resolveForkId(input) {
104
+ // /editor/<template_id> issues a 302 → /editor/<template_id>?fork=<id>
105
+ // whenever the caller (or the template's default fork) has one. Follow the
106
+ // redirect once by hand so we can read the fork id off the query string.
107
+ const url = `${input.host}/editor/${encodeURIComponent(input.templateId)}`;
108
+ const res = await fetch(url, {
109
+ method: "GET",
110
+ redirect: "manual",
111
+ headers: buildAuthHeaders(input)
112
+ });
113
+ if (res.status >= 300 && res.status < 400) {
114
+ const location = res.headers.get("location");
115
+ if (location) {
116
+ try {
117
+ const target = new URL(location, input.host);
118
+ const fork = target.searchParams.get("fork");
119
+ if (fork)
120
+ return fork;
121
+ }
122
+ catch {
123
+ // fall through
124
+ }
870
125
  }
871
- uploadResult = {
872
- uploaded: true,
873
- size_bytes: statSync(filePath).size,
874
- method: payload.upload?.method ?? "PUT",
875
- status: uploadResponse.status
876
- };
877
126
  }
878
- console.log(JSON.stringify({
879
- base_url: baseUrl,
880
- file_path: filePath,
881
- file_name: payload.file_name,
882
- content_type: payload.content_type,
883
- storage_key: payload.storage_key,
884
- preview_media_url: payload.preview_media_url,
885
- public_url: payload.public_url ?? payload.preview_media_url,
886
- upload_result: uploadResult,
887
- upload: payload.upload,
888
- curl_upload_example: [
889
- `curl -X ${payload.upload.method} '${payload.upload.url}' \\`,
890
- ` -H 'content-type: ${payload.content_type}' \\`,
891
- " --upload-file <local-preview-file>"
892
- ].join("\n")
893
- }, null, 2));
894
- }
895
- async function runSessionCommand(argv) {
896
- const parsed = parseArgs({
897
- args: argv,
898
- options: {
899
- "data-dir": { type: "string", default: ".vidfarm/local" }
900
- }
901
- });
902
- const sessionPath = path.resolve(process.cwd(), parsed.values["data-dir"], "session.json");
903
- const session = JSON.parse(readFileSync(sessionPath, "utf8"));
904
- console.log(JSON.stringify({
905
- base_url: session.baseUrl,
906
- email: session.email,
907
- starter_style_options: {
908
- caption_standard_version: CAPTION_STANDARD_VERSION,
909
- fonts: STARTER_TEMPLATE_FONT_OPTIONS,
910
- text_background_colors: STARTER_TEMPLATE_TEXT_BACKGROUND_COLOR_OPTIONS,
911
- text_background_rendering: TIKTOK_TEXT_BACKGROUND_RENDERING,
912
- caption_shadows: TIKTOK_CAPTION_SHADOWS,
913
- caption_rendering_rules: TIKTOK_CAPTION_RENDERING_RULES,
914
- presets: TIKTOK_CAPTION_STYLE_PRESETS,
915
- package_dependencies: [
916
- "@fontsource/tiktok-sans",
917
- "@fontsource/montserrat",
918
- "@fontsource/source-code-pro",
919
- "@fontsource/libre-baskerville"
920
- ],
921
- standard_file: "src/lib/template-style-options.ts",
922
- starter_template_file: "demos/vidfarm_template_0000/src/style-options.ts"
923
- },
924
- headers: {
925
- "vidfarm-api-key": session.apiKey,
926
- "content-type": "application/json"
927
- },
928
- curl_example: [
929
- `curl -X POST ${session.baseUrl}/api/v1/primitives/image/operations/run \\`,
930
- ` -H 'vidfarm-api-key: ${session.apiKey}' \\`,
931
- " -H 'content-type: application/json' \\",
932
- " -d '{\"tracer\":\"local-test\",\"payload\":{\"prompt\":\"a cinematic founder at a desk\",\"width\":1080,\"height\":1920}}'"
933
- ].join("\n")
934
- }, null, 2));
127
+ return null;
935
128
  }
936
- async function seedEnvProviderKeys(input) {
937
- const entries = [
938
- { provider: "openai", secret: process.env.OPENAI_API_KEY ?? "", placeholder: "mock-openai-key" },
939
- { provider: "gemini", secret: process.env.GEMINI_API_KEY ?? "", placeholder: "mock-gemini-key" },
940
- { provider: "openrouter", secret: process.env.OPENROUTER_API_KEY ?? "", placeholder: "mock-openrouter-key" },
941
- { provider: "perplexity", secret: process.env.PERPLEXITY_API_KEY ?? "", placeholder: "mock-perplexity-key" }
942
- ];
943
- const existing = await input.providerKeys.listProviderKeys(input.customerId);
944
- for (const entry of entries) {
945
- const secret = entry.secret || (input.allowMockPlaceholders ? entry.placeholder : "");
946
- if (!secret) {
129
+ async function fetchCompositionFiles(input) {
130
+ const files = ["composition.html", "composition.json", "manifest.json"];
131
+ for (const filename of files) {
132
+ const target = path.join(input.dir, filename);
133
+ if (existsSync(target) && !input.refetch) {
134
+ console.log(`[vidfarm] keep local ${filename} (use --refetch to overwrite)`);
947
135
  continue;
948
136
  }
949
- const alreadySeeded = existing.some((row) => row.provider === entry.provider && row.label === "Local CLI env key");
950
- if (alreadySeeded) {
137
+ const url = `${input.host}/api/v1/compositions/${encodeURIComponent(input.forkId)}/${filename}`;
138
+ const res = await fetch(url, { headers: buildAuthHeaders(input) });
139
+ if (!res.ok) {
140
+ console.warn(`[vidfarm] skip ${filename}: ${res.status} ${res.statusText} (${url})`);
951
141
  continue;
952
142
  }
953
- await input.providerKeys.createProviderKey({
954
- id: input.createId("pkey"),
955
- customerId: input.customerId,
956
- provider: entry.provider,
957
- label: "Local CLI env key",
958
- encryptedSecret: input.encryptString(secret, input.encryptionSecret)
959
- });
143
+ const body = Buffer.from(await res.arrayBuffer());
144
+ writeFileSync(target, body);
145
+ console.log(`[vidfarm] fetched ${filename} (${body.length} bytes)`);
960
146
  }
961
147
  }
962
- function printStartupBanner(input) {
963
- console.log("");
964
- console.log("Vidfarm local dev runtime");
965
- console.log(`Base URL: ${input.baseUrl}`);
966
- console.log(`Dev user: ${input.email}`);
967
- console.log(`vidfarm-api-key: ${input.apiKey}`);
968
- console.log(`customer-id (derived via /api/v1/user/me): ${input.customerId}`);
969
- console.log(`Session file: ${input.sessionPath}`);
970
- console.log(`Data dir: ${input.dataDir}`);
971
- console.log("Runtime state: DynamoDB-backed services");
972
- console.log(`AI mode: ${input.mockProviders ? "mock provider responses" : "real provider env keys"}`);
148
+ function buildAuthHeaders(input) {
149
+ const headers = { accept: "text/html,application/json" };
150
+ if (input.apiKey)
151
+ headers.authorization = `Bearer ${input.apiKey}`;
152
+ if (input.shareToken)
153
+ headers["x-vidfarm-share-token"] = input.shareToken;
154
+ return headers;
155
+ }
156
+ function printEditorBanner(input) {
157
+ const bold = "\x1b[1m";
158
+ const green = "\x1b[32m";
159
+ const cyan = "\x1b[36m";
160
+ const dim = "\x1b[2m";
161
+ const reset = "\x1b[0m";
162
+ const line = `${dim}${"─".repeat(74)}${reset}`;
973
163
  console.log("");
974
- console.log("REST API:");
975
- console.log(` ${input.baseUrl}/health`);
976
- console.log(` ${input.baseUrl}/dev`);
164
+ console.log(line);
165
+ console.log(`${bold}${green} Vidfarm local editor${reset}`);
166
+ console.log(line);
167
+ console.log(` template ${input.templateId}`);
168
+ console.log(` fork ${input.forkId}`);
169
+ console.log(` local dir ${input.dir}`);
170
+ console.log(` dev-serve http://localhost:${input.port}`);
977
171
  console.log("");
978
- console.log("Get reusable auth headers:");
979
- console.log(" vidfarm session");
172
+ console.log(` ${bold}Open in browser:${reset}`);
173
+ console.log(` ${cyan}${input.editorUrl}${reset}`);
174
+ console.log(line);
980
175
  console.log("");
981
176
  }
982
- function rewriteTemplateStarter(root, replacements) {
983
- for (const entry of readdirSync(root, { withFileTypes: true })) {
984
- const target = path.join(root, entry.name);
985
- if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "dist") {
986
- continue;
987
- }
988
- if (entry.isDirectory()) {
989
- rewriteTemplateStarter(target, replacements);
990
- continue;
991
- }
992
- const original = readFileSync(target, "utf8");
993
- const next = replacements.reduce((value, [from, to]) => value.split(from).join(to), original);
994
- if (next !== original) {
995
- writeFileSync(target, next);
996
- }
997
- }
998
- }
999
- function toProjectName(folderName) {
1000
- return folderName
1001
- .replace(/[^a-zA-Z0-9_-]+/g, "_")
1002
- .replace(/-+/g, "_")
1003
- .replace(/^_+|_+$/g, "")
1004
- .toLowerCase();
1005
- }
1006
- function resolveStarterTemplateDir() {
1007
- const candidates = [
1008
- path.resolve(import.meta.dirname, "..", "templates", "vidfarm_template_0000"),
1009
- path.resolve(import.meta.dirname, "..", "..", "templates", "vidfarm_template_0000"),
1010
- path.resolve(import.meta.dirname, "..", "demos", "vidfarm_template_0000"),
1011
- path.resolve(import.meta.dirname, "..", "..", "demos", "vidfarm_template_0000")
1012
- ];
1013
- for (const candidate of candidates) {
1014
- if (existsSync(candidate)) {
1015
- return candidate;
1016
- }
1017
- }
1018
- return candidates[0];
1019
- }
1020
- function resolveAutoCreateHarnessDir() {
1021
- const candidates = [
1022
- path.resolve(process.cwd(), "auto-create-templates"),
1023
- path.resolve(import.meta.dirname, "..", "auto-create-templates"),
1024
- path.resolve(import.meta.dirname, "..", "..", "auto-create-templates")
1025
- ];
1026
- for (const candidate of candidates) {
1027
- if (existsSync(path.join(candidate, "AUTO_CREATE_TEMPLATES.md"))) {
1028
- return candidate;
1029
- }
1030
- }
1031
- return candidates[0];
1032
- }
1033
- function resolveDeveloperSkillPath() {
1034
- const candidates = [
1035
- path.resolve(process.cwd(), "SKILL.platform.md"),
1036
- path.resolve(import.meta.dirname, "..", "SKILL.platform.md"),
1037
- path.resolve(import.meta.dirname, "..", "..", "SKILL.platform.md")
1038
- ];
1039
- for (const candidate of candidates) {
1040
- if (existsSync(candidate)) {
1041
- return candidate;
1042
- }
1043
- }
1044
- return candidates[0];
1045
- }
1046
- function createAutoCreateRunFolders(runDir) {
1047
- for (const name of ["source", "frames", "audio", "transcripts", "ocr", "motion", "dna", "extraction", "plan"]) {
1048
- mkdirSync(path.join(runDir, name), { recursive: true });
1049
- }
1050
- }
1051
- function stageAutocreateSource(input) {
1052
- mkdirSync(input.sourceDir, { recursive: true });
1053
- mkdirSync(input.previewDir, { recursive: true });
1054
- const stats = statSync(input.inputPath);
1055
- if (stats.isDirectory()) {
1056
- const inputHadPreviewDir = existsSync(path.join(input.inputPath, "preview"));
1057
- copyDirectoryContentsShallow(input.inputPath, input.sourceDir);
1058
- const existingPreviewDir = path.join(input.sourceDir, "preview");
1059
- if (!inputHadPreviewDir) {
1060
- mkdirSync(existingPreviewDir, { recursive: true });
1061
- for (const mediaFile of listMediaFiles(input.sourceDir)) {
1062
- cpSync(mediaFile, path.join(existingPreviewDir, path.basename(mediaFile)));
1063
- }
1064
- }
1065
- }
1066
- else {
1067
- cpSync(input.inputPath, path.join(input.previewDir, path.basename(input.inputPath)));
1068
- }
1069
- const notesPath = path.join(input.sourceDir, "source.md");
1070
- if (!existsSync(notesPath)) {
1071
- writeFileSync(notesPath, [
1072
- "# Source Notes",
1073
- "",
1074
- `- input path: ${input.inputPath}`,
1075
- `- original format URL: ${input.linkToOriginal ?? ""}`,
1076
- "- creator/account:",
1077
- "- product/category being adapted:",
1078
- "- what appears repeatable:",
1079
- "- constraints:",
1080
- ""
1081
- ].join("\n"), "utf8");
1082
- }
1083
- }
1084
- async function prepareAutocreateMediaArtifacts(input) {
1085
- const videoFile = listMediaFiles(input.previewDir).find((filePath) => isVideoFile(filePath));
1086
- if (!videoFile) {
1087
- return {
1088
- skipped: true,
1089
- reason: "No video file found in source preview media.",
1090
- preview_files: listMediaFiles(input.previewDir)
1091
- };
1092
- }
1093
- const ffprobePath = path.join(input.runDir, "source", "ffprobe.json");
1094
- const contactSheetPath = path.join(input.runDir, "frames", "contact-sheet.jpg");
1095
- const audioPath = path.join(input.runDir, "audio", "source.wav");
1096
- const steps = [];
1097
- steps.push(await runOptionalLocalCommand("ffprobe", [
1098
- "-v", "error",
1099
- "-show_entries", "format=duration:stream=codec_type,width,height,avg_frame_rate",
1100
- "-of", "json",
1101
- videoFile
1102
- ], ffprobePath));
1103
- steps.push(await runOptionalLocalCommand("ffmpeg", [
1104
- "-y",
1105
- "-i", videoFile,
1106
- "-vf", "fps=1/2,scale=240:-1,tile=3x4",
1107
- "-frames:v", "1",
1108
- contactSheetPath
1109
- ]));
1110
- steps.push(await runOptionalLocalCommand("ffmpeg", [
1111
- "-y",
1112
- "-i", videoFile,
1113
- "-vn",
1114
- "-ac", "1",
1115
- "-ar", "16000",
1116
- audioPath
1117
- ]));
1118
- const result = {
1119
- skipped: false,
1120
- video_file: videoFile,
1121
- ffprobe_path: existsSync(ffprobePath) ? ffprobePath : null,
1122
- contact_sheet_path: existsSync(contactSheetPath) ? contactSheetPath : null,
1123
- audio_path: existsSync(audioPath) ? audioPath : null,
1124
- steps
1125
- };
1126
- writeFileSync(path.join(input.runDir, "source", "media_artifacts.json"), JSON.stringify(result, null, 2) + "\n", "utf8");
1127
- return result;
1128
- }
1129
- function snapshotAutoCreateHarness(input) {
1130
- if (!existsSync(path.join(input.harnessDir, "AUTO_CREATE_TEMPLATES.md"))) {
1131
- throw new Error(`Auto-create harness not found: ${input.harnessDir}`);
1132
- }
1133
- const destinationDir = path.join(input.runDir, "harness");
1134
- mkdirSync(destinationDir, { recursive: true });
1135
- for (const fileName of [
1136
- "AUTO_CREATE_TEMPLATES.md",
1137
- "runbook.md",
1138
- "extractor-system-prompt.md",
1139
- "production-graph.schema.json",
1140
- "template-planner-prompt.md",
1141
- "template-plan.schema.json"
1142
- ]) {
1143
- const source = path.join(input.harnessDir, fileName);
1144
- if (existsSync(source)) {
1145
- cpSync(source, path.join(destinationDir, fileName));
1146
- }
1147
- }
1148
- const developerSkillPath = resolveDeveloperSkillPath();
1149
- const developerSkillDestination = path.join(destinationDir, "SKILL.platform.md");
1150
- if (existsSync(developerSkillPath)) {
1151
- cpSync(developerSkillPath, developerSkillDestination);
1152
- }
1153
- return {
1154
- harness_dir: destinationDir,
1155
- entry: path.join(destinationDir, "AUTO_CREATE_TEMPLATES.md"),
1156
- developer_skill_path: existsSync(developerSkillDestination)
1157
- ? developerSkillDestination
1158
- : developerSkillPath
1159
- };
1160
- }
1161
- function renderAutocreateAgentPrompt(manifest) {
1162
- return [
1163
- "# Vidfarm Autocreate Template Handoff",
1164
- "",
1165
- "Cacheable context, read first and keep before run-specific artifacts in your working context:",
1166
- "",
1167
- `1. Developer standard: ${manifest.developer_skill_context}`,
1168
- `2. Auto-create harness: ${manifest.harness_snapshot?.entry ?? manifest.harness_entry}`,
1169
- manifest.harness_snapshot?.harness_dir
1170
- ? `3. Harness snapshot directory: ${manifest.harness_snapshot.harness_dir}`
1171
- : "3. Harness snapshot directory: not created; use the live harness path above.",
1172
- "",
1173
- "These files are stable across runs and are intended to be prompt-cache friendly. Read them before loading source media, analyzer outputs, or run-specific JSON.",
1174
- "",
1175
- `Use ${manifest.harness_snapshot?.entry ?? manifest.harness_entry} as the harness.`,
1176
- `Use ${manifest.developer_skill_context} as the developer template standard context.`,
1177
- `Analyze ${manifest.source_dir} as the source format.`,
1178
- `Use the run artifacts in ${manifest.run_dir}.`,
1179
- `Output the runnable Vidfarm template to ${manifest.output_path}.`,
1180
- "",
1181
- "Required work:",
1182
- "",
1183
- "1. Inspect source notes and preview media.",
1184
- "2. Use or create analyzer artifacts in the run folder.",
1185
- "3. Run or consume viral and visual DNA analysis.",
1186
- "4. Create `dna/preservation_decision.json`.",
1187
- "5. Create `extraction/production_graph.json` matching the production graph schema.",
1188
- "6. Create `plan/template_plan.json` matching the template plan schema.",
1189
- "7. Implement the template operations, jobs, metadata, prompts, storage outputs, and template-local `SKILL.md`.",
1190
- "8. Validate storage boundaries: use only `ctx.storage` with job-relative keys; do not generate direct AWS/S3 clients, generic `AWS_*` credential handling, `REMOTION_AWS_*` handling, or absolute keys like `templates/${templateId}/users/${ctx.customer.id}/...`.",
1191
- "9. Run `vidfarm-devcli validate-template` and at least one real local job before calling it done.",
1192
- "",
1193
- "Manifest:",
1194
- "",
1195
- "```json",
1196
- JSON.stringify(manifest, null, 2),
1197
- "```",
1198
- ""
1199
- ].join("\n");
1200
- }
1201
- async function runOptionalLocalCommand(command, args, outputPath) {
1202
- try {
1203
- const result = await execFileAsync(command, args, {
1204
- maxBuffer: 20 * 1024 * 1024
1205
- });
1206
- if (outputPath) {
1207
- writeFileSync(outputPath, result.stdout, "utf8");
1208
- }
1209
- return {
1210
- command,
1211
- ok: true,
1212
- output_path: outputPath ?? null,
1213
- stderr: result.stderr.trim() || null
1214
- };
1215
- }
1216
- catch (error) {
1217
- return {
1218
- command,
1219
- ok: false,
1220
- output_path: outputPath ?? null,
1221
- error: error instanceof Error ? error.message : String(error)
1222
- };
1223
- }
1224
- }
1225
- function copyDirectoryContentsShallow(sourceDir, destinationDir) {
1226
- mkdirSync(destinationDir, { recursive: true });
1227
- for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
1228
- if (!entry.name.startsWith(".")) {
1229
- cpSync(path.join(sourceDir, entry.name), path.join(destinationDir, entry.name), { recursive: true });
1230
- }
1231
- }
1232
- }
1233
- function listMediaFiles(dir) {
1234
- if (!existsSync(dir)) {
1235
- return [];
1236
- }
1237
- const files = [];
1238
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1239
- const filePath = path.join(dir, entry.name);
1240
- if (entry.name.startsWith(".")) {
1241
- continue;
1242
- }
1243
- if (entry.isDirectory()) {
1244
- files.push(...listMediaFiles(filePath));
1245
- continue;
1246
- }
1247
- if (entry.isFile() && isMediaFile(filePath)) {
1248
- files.push(filePath);
1249
- }
1250
- }
1251
- return files.sort((a, b) => a.localeCompare(b));
1252
- }
1253
- function isMediaFile(filePath) {
1254
- return [".png", ".jpg", ".jpeg", ".webp", ".mp4", ".mov", ".webm", ".m4v"].includes(path.extname(filePath).toLowerCase());
1255
- }
1256
- function isVideoFile(filePath) {
1257
- return [".mp4", ".mov", ".webm", ".m4v"].includes(path.extname(filePath).toLowerCase());
1258
- }
1259
- function toSlugId(folderName) {
1260
- return toProjectName(folderName).replace(/^vidfarm_template_/, "template_");
1261
- }
1262
- function formatUtcTimestamp(date) {
1263
- return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
1264
- }
1265
- function parseProdTemplateCommandArgs(argv, defaults) {
1266
- const parsed = parseArgs({
1267
- args: argv,
1268
- options: {
1269
- "template-id": { type: "string" },
1270
- "slug-id": { type: "string" },
1271
- "repo-url": { type: "string" },
1272
- branch: { type: "string", default: "production" },
1273
- "template-module-path": { type: "string" },
1274
- "skill-path": { type: "string" },
1275
- "install-command": { type: "string", default: "npm install" },
1276
- "build-command": { type: "string", default: "npm run build" },
1277
- "commit-sha": { type: "string" },
1278
- "repo-dir": { type: "string", default: "." },
1279
- "env-file": { type: "string", default: defaults.envFile },
1280
- "base-url": { type: "string", default: defaults.baseUrl },
1281
- "api-key": { type: "string" },
1282
- "stack-name": { type: "string", default: defaults.stackName }
1283
- }
1284
- });
1285
- const templateId = parsed.values["template-id"];
1286
- const slugId = parsed.values["slug-id"];
1287
- const repoUrl = parsed.values["repo-url"];
1288
- const templateModulePath = parsed.values["template-module-path"];
1289
- if (!templateId || !slugId || !repoUrl || !templateModulePath) {
1290
- throw new Error("Command requires --template-id, --slug-id, --repo-url, and --template-module-path.");
1291
- }
1292
- deriveTemplateRootDirFromModulePath(templateModulePath);
1293
- loadEnvFile(parsed.values["env-file"]);
1294
- const apiKey = parsed.values["api-key"] ?? process.env.VIDFARM_API_KEY;
1295
- if (!apiKey) {
1296
- throw new Error("Missing Vidfarm API key. Pass --api-key or set VIDFARM_API_KEY in the selected env file.");
1297
- }
1298
- return {
1299
- templateId,
1300
- slugId,
1301
- repoUrl,
1302
- branch: parsed.values.branch,
1303
- templateModulePath,
1304
- skillPath: parsed.values["skill-path"] ?? defaultSkillPathForTemplateModule(templateModulePath),
1305
- installCommand: parsed.values["install-command"],
1306
- buildCommand: parsed.values["build-command"],
1307
- commitSha: parsed.values["commit-sha"] ?? undefined,
1308
- repoDir: path.resolve(process.cwd(), parsed.values["repo-dir"]),
1309
- envFile: parsed.values["env-file"],
1310
- baseUrl: normalizeBaseUrl(parsed.values["base-url"]),
1311
- apiKey,
1312
- stackName: parsed.values["stack-name"] || undefined,
1313
- activate: defaults.activate
1314
- };
1315
- }
1316
- async function importSourceIntoProd(input) {
1317
- const registration = await registerSourceViaApi(input);
1318
- const source = await postProdTemplateApi(input, `/api/v1/templates/sources/${registration.source.id}/approve`, { method: "POST" });
1319
- const releaseResponse = await postProdTemplateApi(input, `/api/v1/templates/sources/${registration.source.id}/import`, {
1320
- method: "POST",
1321
- body: JSON.stringify({
1322
- commit_sha: input.commitSha ?? undefined
1323
- })
1324
- });
1325
- const approvedRelease = await postProdTemplateApi(input, `/api/v1/templates/releases/${releaseResponse.release.id}/approve`, { method: "POST" });
1326
- const activatedRelease = input.activate
1327
- ? await postProdTemplateApi(input, `/api/v1/templates/releases/${releaseResponse.release.id}/activate`, { method: "POST" })
1328
- : null;
1329
- return {
1330
- payload: {
1331
- source: source.source,
1332
- release: approvedRelease.release,
1333
- activated_release: activatedRelease?.release ?? null
1334
- }
1335
- };
1336
- }
1337
- async function registerSourceViaApi(input) {
1338
- return postProdTemplateApi(input, "/api/v1/templates/sources", {
1339
- method: "POST",
1340
- body: JSON.stringify({
1341
- template_id: input.templateId,
1342
- slug_id: input.slugId,
1343
- repo_url: input.repoUrl,
1344
- branch: input.branch,
1345
- template_module_path: input.templateModulePath,
1346
- skill_path: input.skillPath,
1347
- install_command: input.installCommand,
1348
- build_command: input.buildCommand
1349
- })
1350
- });
1351
- }
1352
- async function postProdTemplateApi(input, pathname, init) {
1353
- const response = await fetch(`${input.baseUrl}${pathname}`, {
1354
- ...init,
1355
- headers: {
1356
- "content-type": "application/json",
1357
- "vidfarm-api-key": input.apiKey,
1358
- ...(init.headers ?? {})
1359
- }
1360
- });
1361
- const payload = await response.json().catch(() => ({}));
1362
- if (!response.ok) {
1363
- throw new Error(typeof payload.error === "string" ? payload.error : `Request failed with ${response.status}.`);
1364
- }
1365
- return payload;
1366
- }
1367
- async function packageTemplateArtifact(repoDir, commitSha) {
1368
- const tempDir = mkdtempSync(path.join(os.tmpdir(), "vidfarm-template-artifact-"));
1369
- const repoRoot = (await execFileAsync("git", ["-C", repoDir, "rev-parse", "--show-toplevel"])).stdout.trim();
1370
- const artifactPath = path.join(tempDir, "template-source.tar.gz");
1371
- try {
1372
- await execFileAsync("git", ["-C", repoRoot, "archive", "--format=tar.gz", `--output=${artifactPath}`, commitSha], {
1373
- cwd: repoRoot
1374
- });
1375
- const buffer = readFileSync(artifactPath);
1376
- return {
1377
- buffer,
1378
- sha256: createHash("sha256").update(buffer).digest("hex")
1379
- };
1380
- }
1381
- finally {
1382
- rmSync(tempDir, { recursive: true, force: true });
1383
- }
1384
- }
1385
- async function uploadArtifactArchive(presign, buffer) {
1386
- const response = await fetch(presign.upload.url, {
1387
- method: presign.upload.method,
1388
- headers: presign.upload.headers,
1389
- body: new Uint8Array(buffer)
1390
- });
1391
- if (!response.ok) {
1392
- const body = await response.text().catch(() => "");
1393
- throw new Error(body || `Artifact upload failed with ${response.status}.`);
1394
- }
1395
- }
1396
- function loadEnvFile(envFile) {
1397
- const resolved = path.resolve(process.cwd(), envFile);
1398
- if (!existsSync(resolved)) {
1399
- throw new Error(`Missing env file: ${resolved}`);
1400
- }
1401
- dotenv.config({
1402
- path: resolved,
1403
- override: true
1404
- });
1405
- }
1406
- function loadOptionalEnvFile(envFile) {
1407
- const resolved = path.resolve(process.cwd(), envFile);
1408
- if (!existsSync(resolved)) {
1409
- return;
1410
- }
1411
- dotenv.config({
1412
- path: resolved,
1413
- override: false
1414
- });
1415
- }
1416
- function sanitizeCliUploadFileName(value) {
1417
- if (!value) {
1418
- return "";
1419
- }
1420
- const normalized = path.basename(value).replace(/[^\w.-]+/g, "_");
1421
- return normalized.length ? normalized : "upload.bin";
1422
- }
1423
- function normalizeBaseUrl(value) {
177
+ function trimTrailingSlash(value) {
1424
178
  return value.replace(/\/+$/, "");
1425
179
  }
1426
- function inferUploadContentType(fileName) {
1427
- switch (path.extname(fileName).toLowerCase()) {
1428
- case ".png":
1429
- return "image/png";
1430
- case ".gif":
1431
- return "image/gif";
1432
- case ".jpg":
1433
- case ".jpeg":
1434
- return "image/jpeg";
1435
- case ".webp":
1436
- return "image/webp";
1437
- case ".svg":
1438
- return "image/svg+xml";
1439
- case ".mp4":
1440
- case ".m4v":
1441
- return "video/mp4";
1442
- case ".mov":
1443
- return "video/quicktime";
1444
- case ".webm":
1445
- return "video/webm";
1446
- case ".mp3":
1447
- return "audio/mpeg";
1448
- case ".wav":
1449
- return "audio/wav";
1450
- case ".m4a":
1451
- return "audio/mp4";
1452
- case ".aac":
1453
- return "audio/aac";
1454
- case ".ogg":
1455
- return "audio/ogg";
1456
- case ".pdf":
1457
- return "application/pdf";
1458
- case ".md":
1459
- return "text/markdown; charset=utf-8";
1460
- case ".txt":
1461
- return "text/plain; charset=utf-8";
1462
- default:
1463
- return "application/octet-stream";
1464
- }
1465
- }
1466
- async function resolveStackOutput(stackName, outputKey) {
1467
- const { stdout } = await runLocalCommand("aws", [
1468
- "cloudformation",
1469
- "describe-stacks",
1470
- "--stack-name", stackName,
1471
- "--query", `Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue`,
1472
- "--output", "text"
1473
- ]);
1474
- const value = stdout.trim();
1475
- if (!value || value === "None") {
1476
- throw new Error(`Could not resolve ${outputKey} from stack ${stackName}.`);
1477
- }
1478
- return value;
1479
- }
1480
- async function getProdCurrentImage(input) {
1481
- loadEnvFile(input.envFile);
1482
- const instanceId = await resolveStackOutput(input.stackName, "VidfarmInstanceId");
1483
- const stdout = await runSsmScript({
1484
- instanceId,
1485
- comment: "Read current Vidfarm image",
1486
- script: [
1487
- "set -euo pipefail",
1488
- "sudo docker ps -a --filter name=^/vidfarm$ --format '{{.Image}}' | head -n 1",
1489
- "sudo systemctl is-active vidfarm || true"
1490
- ].join("\n")
1491
- });
1492
- const [image, status] = stdout.trim().split(/\n+/);
1493
- if (!image) {
1494
- throw new Error("Could not determine the current live Docker image.");
1495
- }
1496
- return {
1497
- instanceId,
1498
- image,
1499
- status: status ?? ""
1500
- };
1501
- }
1502
- async function runSsmScript(input) {
1503
- const tempDir = mkTempDir();
1504
- try {
1505
- const commandPath = path.join(tempDir, "commands.json");
1506
- const scriptB64 = Buffer.from(input.script, "utf8").toString("base64");
1507
- writeFileSync(commandPath, JSON.stringify({
1508
- commands: [
1509
- `echo ${shellQuote(scriptB64)} | base64 --decode > /tmp/vidfarm-operator.sh`,
1510
- "bash /tmp/vidfarm-operator.sh"
1511
- ]
1512
- }));
1513
- const { stdout: sendStdout } = await runLocalCommand("aws", [
1514
- "ssm",
1515
- "send-command",
1516
- "--instance-ids", input.instanceId,
1517
- "--document-name", "AWS-RunShellScript",
1518
- "--comment", input.comment,
1519
- "--parameters", `file://${commandPath}`,
1520
- "--query", "Command.CommandId",
1521
- "--output", "text"
1522
- ]);
1523
- const commandId = sendStdout.trim();
1524
- if (!commandId) {
1525
- throw new Error(`Unable to start SSM command for ${input.comment}.`);
1526
- }
1527
- while (true) {
1528
- const { stdout } = await runLocalCommand("aws", [
1529
- "ssm",
1530
- "get-command-invocation",
1531
- "--command-id", commandId,
1532
- "--instance-id", input.instanceId
1533
- ]);
1534
- const response = JSON.parse(stdout);
1535
- if (response.Status === "Success") {
1536
- return response.StandardOutputContent ?? "";
1537
- }
1538
- if (["Failed", "Cancelled", "TimedOut", "Cancelling"].includes(response.Status)) {
1539
- throw new Error((response.StandardErrorContent ?? "").trim()
1540
- || (response.StandardOutputContent ?? "").trim()
1541
- || `SSM command failed with status ${response.Status}.`);
1542
- }
1543
- await sleep(2000);
1544
- }
1545
- }
1546
- finally {
1547
- rmSync(tempDir, { recursive: true, force: true });
1548
- }
1549
- }
1550
- async function runLocalCommand(command, args) {
1551
- return execFileAsync(command, args, {
1552
- cwd: process.cwd(),
1553
- env: process.env,
1554
- maxBuffer: 10 * 1024 * 1024
1555
- });
1556
- }
1557
- function shellQuote(value) {
1558
- return `'${value.replace(/'/g, `'\"'\"'`)}'`;
1559
- }
1560
- function mkTempDir() {
1561
- return mkdtempSync(path.join(os.tmpdir(), "vidfarm-operator-"));
1562
- }
1563
- function extractLastJson(stdout) {
1564
- const trimmed = stdout.trim();
1565
- const start = trimmed.lastIndexOf("\n{");
1566
- const jsonText = start >= 0 ? trimmed.slice(start + 1) : trimmed;
1567
- return JSON.parse(jsonText);
1568
- }
1569
- function sleep(ms) {
1570
- return new Promise((resolve) => setTimeout(resolve, ms));
1571
- }
1572
180
  //# sourceMappingURL=cli.js.map