@mevdragon/vidfarm-devcli 0.2.13 → 0.3.0

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