@mevdragon/vidfarm-devcli 0.2.1 → 0.2.3

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 (40) hide show
  1. package/.env.example +6 -39
  2. package/GETTING_STARTED.developers.md +87 -0
  3. package/README.md +94 -238
  4. package/SKILL.developer.md +430 -104
  5. package/dist/src/account-pages.js +1 -1
  6. package/dist/src/app.js +93 -5
  7. package/dist/src/cli.js +456 -8
  8. package/dist/src/config.js +3 -2
  9. package/dist/src/context.js +30 -11
  10. package/dist/src/db.js +2 -57
  11. package/dist/src/dev-app.js +0 -1
  12. package/dist/src/index.js +4 -2
  13. package/dist/src/lib/template-paths.js +21 -0
  14. package/dist/src/runtime.js +3 -1
  15. package/dist/src/services/auth.js +4 -4
  16. package/dist/src/services/job-logs.js +186 -0
  17. package/dist/src/services/jobs.js +3 -2
  18. package/dist/src/services/providers.js +14 -6
  19. package/dist/src/services/storage.js +85 -2
  20. package/dist/src/services/template-sources.js +29 -3
  21. package/dist/templates/template_0000/src/lib/images.js +46 -86
  22. package/dist/templates/template_0000/src/template.js +277 -53
  23. package/package.json +5 -6
  24. package/templates/template_0000/README.md +8 -52
  25. package/templates/template_0000/SKILL.md +35 -3
  26. package/templates/template_0000/package.json +3 -6
  27. package/templates/template_0000/src/lib/images.js +46 -86
  28. package/templates/template_0000/src/lib/images.ts +55 -98
  29. package/templates/template_0000/src/template-dna.js +9 -0
  30. package/templates/template_0000/src/template.js +523 -199
  31. package/templates/template_0000/src/template.ts +356 -61
  32. package/templates/template_0000/template.config.json +7 -12
  33. package/AWS_REMOTION_HANDOFF.md +0 -311
  34. package/PLATFORM_SPEC.md +0 -1039
  35. package/SKILL.director.md +0 -599
  36. package/dist/infra/cdk/bin/vidfarm-prod.js +0 -59
  37. package/dist/infra/cdk/lib/vidfarm-prod-stack.js +0 -212
  38. package/templates/template_0000/package-lock.json +0 -5505
  39. package/templates/template_0000/scripts/create-site.mjs +0 -27
  40. package/templates/template_0000/scripts/render-cloud.mjs +0 -72
package/dist/src/cli.js CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { execFile } from "node:child_process";
3
4
  import { randomUUID } from "node:crypto";
5
+ import os from "node:os";
4
6
  import path from "node:path";
5
7
  import { parseArgs } from "node:util";
8
+ import { promisify } from "node:util";
6
9
  import dotenv from "dotenv";
7
10
  import { STARTER_TEMPLATE_FONT_OPTIONS, STARTER_TEMPLATE_TEXT_BACKGROUND_COLOR_OPTIONS } from "./lib/template-style-options.js";
8
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);
9
14
  void main().catch((error) => {
10
15
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
11
16
  process.exit(1);
@@ -28,10 +33,22 @@ async function main() {
28
33
  await runImportSourceCommand(process.argv.slice(3));
29
34
  return;
30
35
  }
36
+ if (command === "import-source-prod") {
37
+ await runImportSourceProdCommand(process.argv.slice(3));
38
+ return;
39
+ }
40
+ if (command === "deploy-template-cycle") {
41
+ await runDeployTemplateCycleCommand(process.argv.slice(3));
42
+ return;
43
+ }
31
44
  if (command === "generate-template") {
32
45
  await runGenerateTemplateCommand(process.argv.slice(3));
33
46
  return;
34
47
  }
48
+ if (command === "copy-reference-template") {
49
+ await runCopyReferenceTemplateCommand(process.argv.slice(3));
50
+ return;
51
+ }
35
52
  if (command === "analyze-viral-dna") {
36
53
  await runAnalyzeTemplateDnaCommand("viral", process.argv.slice(3));
37
54
  return;
@@ -40,6 +57,10 @@ async function main() {
40
57
  await runAnalyzeTemplateDnaCommand("visual", process.argv.slice(3));
41
58
  return;
42
59
  }
60
+ if (command === "presign-preview-media") {
61
+ await runPresignPreviewMediaCommand(process.argv.slice(3));
62
+ return;
63
+ }
43
64
  throw new Error(`Unknown command: ${command}`);
44
65
  }
45
66
  async function runDevCommand(argv) {
@@ -191,7 +212,7 @@ async function runImportSourceCommand(argv) {
191
212
  "repo-url": { type: "string" },
192
213
  branch: { type: "string", default: "production" },
193
214
  "template-module-path": { type: "string" },
194
- "skill-path": { type: "string", default: "SKILL.md" },
215
+ "skill-path": { type: "string" },
195
216
  "install-command": { type: "string", default: "npm install" },
196
217
  "build-command": { type: "string", default: "npm run build" },
197
218
  "commit-sha": { type: "string" },
@@ -205,6 +226,10 @@ async function runImportSourceCommand(argv) {
205
226
  if (!templateId || !slugId || !repoUrl || !templateModulePath) {
206
227
  throw new Error("import-source requires --template-id, --slug-id, --repo-url, and --template-module-path.");
207
228
  }
229
+ if (!/(^|\/)template\.ts$/i.test(templateModulePath)) {
230
+ throw new Error("import-source requires --template-module-path to point at the TypeScript entrypoint, usually src/template.ts.");
231
+ }
232
+ const skillPath = parsed.values["skill-path"] ?? defaultSkillPathForTemplateModule(templateModulePath);
208
233
  const [{ TemplateSourceService }, { templateRegistry }] = await Promise.all([
209
234
  import("./services/template-sources.js"),
210
235
  import("./registry.js")
@@ -216,7 +241,7 @@ async function runImportSourceCommand(argv) {
216
241
  repoUrl,
217
242
  branch: parsed.values.branch,
218
243
  templateModulePath,
219
- skillPath: parsed.values["skill-path"],
244
+ skillPath,
220
245
  installCommand: parsed.values["install-command"],
221
246
  buildCommand: parsed.values["build-command"]
222
247
  });
@@ -265,6 +290,8 @@ async function runGenerateTemplateCommand(argv) {
265
290
  const root = process.cwd();
266
291
  const starterDir = resolveStarterTemplateDir();
267
292
  const destinationDir = path.resolve(root, templateDir);
293
+ const destinationFolderName = path.basename(destinationDir);
294
+ assertTemplateFolderNameHasPrefix(destinationFolderName);
268
295
  if (!existsSync(starterDir)) {
269
296
  throw new Error(`Starter template not found: ${starterDir}`);
270
297
  }
@@ -281,7 +308,6 @@ async function runGenerateTemplateCommand(argv) {
281
308
  return ![".git", "node_modules", "dist", "package-lock.json"].includes(name);
282
309
  }
283
310
  });
284
- const destinationFolderName = path.basename(destinationDir);
285
311
  const projectName = parsed.values["project-name"] ?? toProjectName(destinationFolderName);
286
312
  const siteName = parsed.values["site-name"] ?? projectName.replace(/_/g, "-");
287
313
  const githubRepo = parsed.values["github-repo"] ?? `mevdragon/${projectName}`;
@@ -332,6 +358,91 @@ async function runGenerateTemplateCommand(argv) {
332
358
  dna_analysis_runs: dnaRuns
333
359
  }, null, 2));
334
360
  }
361
+ async function runImportSourceProdCommand(argv) {
362
+ const input = parseProdTemplateCommandArgs(argv, {
363
+ envFile: ".env.production",
364
+ stackName: "VidfarmProdStack",
365
+ activate: true
366
+ });
367
+ const result = await importSourceIntoProd(input);
368
+ console.log(JSON.stringify({
369
+ mode: "prod-import-source",
370
+ stack_name: input.stackName,
371
+ instance_id: result.instanceId,
372
+ source: result.payload.source,
373
+ release: result.payload.release,
374
+ activated_release: result.payload.activated_release ?? null
375
+ }, null, 2));
376
+ }
377
+ async function runDeployTemplateCycleCommand(argv) {
378
+ const input = parseProdTemplateCommandArgs(argv, {
379
+ envFile: ".env.production",
380
+ stackName: "VidfarmProdStack",
381
+ activate: true
382
+ });
383
+ const importResult = await importSourceIntoProd(input);
384
+ const currentImage = await getProdCurrentImage({
385
+ stackName: input.stackName,
386
+ envFile: input.envFile
387
+ });
388
+ await runLocalCommand("bash", [
389
+ "scripts/deploy-prod-inplace.sh",
390
+ "--env-file", input.envFile,
391
+ "--stack-name", input.stackName,
392
+ "--image-uri", currentImage.image,
393
+ "--skip-check",
394
+ "--skip-build",
395
+ "--tag-suffix", `template-${input.slugId}`
396
+ ]);
397
+ console.log(JSON.stringify({
398
+ mode: "deploy-template-cycle",
399
+ stack_name: input.stackName,
400
+ instance_id: importResult.instanceId,
401
+ release: importResult.payload.activated_release ?? importResult.payload.release,
402
+ restart_image: currentImage.image
403
+ }, null, 2));
404
+ }
405
+ async function runCopyReferenceTemplateCommand(argv) {
406
+ const parsed = parseArgs({
407
+ args: argv,
408
+ options: {
409
+ "template-dir": { type: "string" },
410
+ force: { type: "boolean", default: false }
411
+ }
412
+ });
413
+ const templateDir = parsed.values["template-dir"];
414
+ if (!templateDir) {
415
+ throw new Error("copy-reference-template requires --template-dir.");
416
+ }
417
+ const root = process.cwd();
418
+ const starterDir = resolveStarterTemplateDir();
419
+ const destinationDir = path.resolve(root, templateDir);
420
+ const destinationFolderName = path.basename(destinationDir);
421
+ assertTemplateFolderNameHasPrefix(destinationFolderName);
422
+ if (!existsSync(starterDir)) {
423
+ throw new Error(`Starter template not found: ${starterDir}`);
424
+ }
425
+ if (existsSync(destinationDir)) {
426
+ if (!parsed.values.force) {
427
+ throw new Error(`Destination already exists: ${destinationDir}`);
428
+ }
429
+ rmSync(destinationDir, { recursive: true, force: true });
430
+ }
431
+ cpSync(starterDir, destinationDir, {
432
+ recursive: true,
433
+ filter: (entry) => {
434
+ const name = path.basename(entry);
435
+ return ![".git", "node_modules", "dist"].includes(name);
436
+ }
437
+ });
438
+ console.log(JSON.stringify({
439
+ copied_from: starterDir,
440
+ template_dir: destinationDir,
441
+ template_id: "4c7a7e1a-7f35-4f30-9f86-9c8a63c7f2db",
442
+ slug_id: "template_0000",
443
+ rewritten: false
444
+ }, null, 2));
445
+ }
335
446
  async function runAnalyzeTemplateDnaCommand(mode, argv) {
336
447
  const parsed = parseArgs({
337
448
  args: argv,
@@ -359,6 +470,67 @@ async function runAnalyzeTemplateDnaCommand(mode, argv) {
359
470
  });
360
471
  console.log(JSON.stringify(result, null, 2));
361
472
  }
473
+ async function runPresignPreviewMediaCommand(argv) {
474
+ const parsed = parseArgs({
475
+ args: argv,
476
+ options: {
477
+ file: { type: "string" },
478
+ "file-name": { type: "string" },
479
+ "content-type": { type: "string" },
480
+ directory: { type: "string" },
481
+ "base-url": { type: "string" },
482
+ "api-key": { type: "string" },
483
+ "env-file": { type: "string", default: ".env" }
484
+ }
485
+ });
486
+ loadOptionalEnvFile(parsed.values["env-file"]);
487
+ const filePath = parsed.values.file ? path.resolve(process.cwd(), parsed.values.file) : null;
488
+ if (filePath && !existsSync(filePath)) {
489
+ throw new Error(`Preview media file not found: ${filePath}`);
490
+ }
491
+ const fileName = sanitizeCliUploadFileName(parsed.values["file-name"]
492
+ ?? (filePath ? path.basename(filePath) : undefined));
493
+ if (!fileName) {
494
+ throw new Error("presign-preview-media requires --file or --file-name.");
495
+ }
496
+ const contentType = parsed.values["content-type"]
497
+ ?? inferUploadContentType(fileName);
498
+ const baseUrl = normalizeBaseUrl(parsed.values["base-url"] ?? process.env.VIDFARM_BASE_URL ?? "https://vidfarm.cloud.zoomgtm.com");
499
+ const apiKey = parsed.values["api-key"] ?? process.env.VIDFARM_API_KEY;
500
+ if (!apiKey) {
501
+ throw new Error("Missing Vidfarm API key. Pass --api-key or set VIDFARM_API_KEY in your env file.");
502
+ }
503
+ const response = await fetch(`${baseUrl}/api/v1/user/me/developer/preview-media/presign`, {
504
+ method: "POST",
505
+ headers: {
506
+ "content-type": "application/json",
507
+ "vidfarm-api-key": apiKey
508
+ },
509
+ body: JSON.stringify({
510
+ file_name: fileName,
511
+ content_type: contentType,
512
+ directory: parsed.values.directory
513
+ })
514
+ });
515
+ const payload = await response.json();
516
+ if (!response.ok) {
517
+ throw new Error(typeof payload.error === "string" ? payload.error : `Preview-media presign failed with ${response.status}.`);
518
+ }
519
+ console.log(JSON.stringify({
520
+ base_url: baseUrl,
521
+ file_path: filePath,
522
+ file_name: payload.file_name,
523
+ content_type: payload.content_type,
524
+ storage_key: payload.storage_key,
525
+ preview_media_url: payload.preview_media_url,
526
+ upload: payload.upload,
527
+ curl_upload_example: [
528
+ `curl -X ${payload.upload.method} '${payload.upload.url}' \\`,
529
+ ` -H 'content-type: ${payload.content_type}' \\`,
530
+ " --upload-file <local-preview-file>"
531
+ ].join("\n")
532
+ }, null, 2));
533
+ }
362
534
  async function runSessionCommand(argv) {
363
535
  const parsed = parseArgs({
364
536
  args: argv,
@@ -377,13 +549,11 @@ async function runSessionCommand(argv) {
377
549
  starter_template_file: "templates/template_0000/src/style-options.ts"
378
550
  },
379
551
  headers: {
380
- "vidfarm-user-id": session.customerId,
381
552
  "vidfarm-api-key": session.apiKey,
382
553
  "content-type": "application/json"
383
554
  },
384
555
  curl_example: [
385
556
  `curl -X POST ${session.baseUrl}/api/v1/templates/template_0000/operations/create_slideshow \\`,
386
- ` -H 'vidfarm-user-id: ${session.customerId}' \\`,
387
557
  ` -H 'vidfarm-api-key: ${session.apiKey}' \\`,
388
558
  " -H 'content-type: application/json' \\",
389
559
  " -d '{\"tracer\":\"local-test\",\"payload\":{\"slides\":[[\"a cinematic founder at a desk\",\"Exact text on slide one\",2400]],\"meta_details_prompt\":\"Keep it native, casual, and curiosity-driven.\"}}'"
@@ -422,8 +592,8 @@ function printStartupBanner(input) {
422
592
  console.log("Vidfarm local dev runtime");
423
593
  console.log(`Base URL: ${input.baseUrl}`);
424
594
  console.log(`Dev user: ${input.email}`);
425
- console.log(`vidfarm-user-id: ${input.customerId}`);
426
595
  console.log(`vidfarm-api-key: ${input.apiKey}`);
596
+ console.log(`customer-id (derived via /api/v1/user/me): ${input.customerId}`);
427
597
  console.log(`Session file: ${input.sessionPath}`);
428
598
  console.log(`Data dir: ${input.dataDir}`);
429
599
  console.log(`SQLite: ${input.dbPath}`);
@@ -462,5 +632,283 @@ function toProjectName(folderName) {
462
632
  .toLowerCase();
463
633
  }
464
634
  function resolveStarterTemplateDir() {
465
- return path.resolve(import.meta.dirname, "..", "..", "templates", "template_0000");
635
+ const candidates = [
636
+ path.resolve(import.meta.dirname, "..", "templates", "template_0000"),
637
+ path.resolve(import.meta.dirname, "..", "..", "templates", "template_0000")
638
+ ];
639
+ for (const candidate of candidates) {
640
+ if (existsSync(candidate)) {
641
+ return candidate;
642
+ }
643
+ }
644
+ return candidates[0];
645
+ }
646
+ function parseProdTemplateCommandArgs(argv, defaults) {
647
+ const parsed = parseArgs({
648
+ args: argv,
649
+ options: {
650
+ "template-id": { type: "string" },
651
+ "slug-id": { type: "string" },
652
+ "repo-url": { type: "string" },
653
+ branch: { type: "string", default: "production" },
654
+ "template-module-path": { type: "string" },
655
+ "skill-path": { type: "string" },
656
+ "install-command": { type: "string", default: "npm install" },
657
+ "build-command": { type: "string", default: "npm run build" },
658
+ "commit-sha": { type: "string" },
659
+ "env-file": { type: "string", default: defaults.envFile },
660
+ "stack-name": { type: "string", default: defaults.stackName }
661
+ }
662
+ });
663
+ const templateId = parsed.values["template-id"];
664
+ const slugId = parsed.values["slug-id"];
665
+ const repoUrl = parsed.values["repo-url"];
666
+ const templateModulePath = parsed.values["template-module-path"];
667
+ if (!templateId || !slugId || !repoUrl || !templateModulePath) {
668
+ throw new Error("Command requires --template-id, --slug-id, --repo-url, and --template-module-path.");
669
+ }
670
+ deriveTemplateRootDirFromModulePath(templateModulePath);
671
+ return {
672
+ templateId,
673
+ slugId,
674
+ repoUrl,
675
+ branch: parsed.values.branch,
676
+ templateModulePath,
677
+ skillPath: parsed.values["skill-path"] ?? defaultSkillPathForTemplateModule(templateModulePath),
678
+ installCommand: parsed.values["install-command"],
679
+ buildCommand: parsed.values["build-command"],
680
+ commitSha: parsed.values["commit-sha"] ?? undefined,
681
+ envFile: parsed.values["env-file"],
682
+ stackName: parsed.values["stack-name"],
683
+ activate: defaults.activate
684
+ };
685
+ }
686
+ async function importSourceIntoProd(input) {
687
+ loadEnvFile(input.envFile);
688
+ const instanceId = await resolveStackOutput(input.stackName, "VidfarmInstanceId");
689
+ const containerScript = buildContainerImportCommand(input);
690
+ const hostScript = [
691
+ "set -euo pipefail",
692
+ `sudo docker exec -e GITHUB_TOKEN=${shellQuote(resolveGithubToken())} vidfarm /bin/bash -lc ${shellQuote(containerScript)}`
693
+ ].join("\n");
694
+ const stdout = await runSsmScript({
695
+ instanceId,
696
+ comment: `Vidfarm import ${input.slugId}`,
697
+ script: hostScript
698
+ });
699
+ return {
700
+ instanceId,
701
+ payload: extractLastJson(stdout)
702
+ };
703
+ }
704
+ function buildContainerImportCommand(input) {
705
+ const args = [
706
+ "node",
707
+ "/app/dist/src/cli.js",
708
+ "import-source",
709
+ "--template-id", input.templateId,
710
+ "--slug-id", input.slugId,
711
+ "--repo-url", input.repoUrl,
712
+ "--branch", input.branch,
713
+ "--template-module-path", input.templateModulePath,
714
+ "--skill-path", input.skillPath,
715
+ "--install-command", input.installCommand,
716
+ "--build-command", input.buildCommand
717
+ ];
718
+ if (input.commitSha) {
719
+ args.push("--commit-sha", input.commitSha);
720
+ }
721
+ if (!input.activate) {
722
+ throw new Error("Non-activating prod import is not implemented.");
723
+ }
724
+ const lines = [
725
+ "set -euo pipefail",
726
+ "if [ -n \"${GITHUB_TOKEN:-}\" ]; then",
727
+ " git config --global url.\"https://x-access-token:${GITHUB_TOKEN}@github.com/\".insteadOf \"https://github.com/\"",
728
+ "fi",
729
+ args.map(shellQuote).join(" ")
730
+ ];
731
+ return lines.join("\n");
732
+ }
733
+ function loadEnvFile(envFile) {
734
+ const resolved = path.resolve(process.cwd(), envFile);
735
+ if (!existsSync(resolved)) {
736
+ throw new Error(`Missing env file: ${resolved}`);
737
+ }
738
+ dotenv.config({
739
+ path: resolved,
740
+ override: true
741
+ });
742
+ }
743
+ function loadOptionalEnvFile(envFile) {
744
+ const resolved = path.resolve(process.cwd(), envFile);
745
+ if (!existsSync(resolved)) {
746
+ return;
747
+ }
748
+ dotenv.config({
749
+ path: resolved,
750
+ override: false
751
+ });
752
+ }
753
+ function sanitizeCliUploadFileName(value) {
754
+ if (!value) {
755
+ return "";
756
+ }
757
+ const normalized = path.basename(value).replace(/[^\w.-]+/g, "_");
758
+ return normalized.length ? normalized : "upload.bin";
759
+ }
760
+ function normalizeBaseUrl(value) {
761
+ return value.replace(/\/+$/, "");
762
+ }
763
+ function inferUploadContentType(fileName) {
764
+ switch (path.extname(fileName).toLowerCase()) {
765
+ case ".png":
766
+ return "image/png";
767
+ case ".gif":
768
+ return "image/gif";
769
+ case ".jpg":
770
+ case ".jpeg":
771
+ return "image/jpeg";
772
+ case ".webp":
773
+ return "image/webp";
774
+ case ".svg":
775
+ return "image/svg+xml";
776
+ case ".mp4":
777
+ case ".m4v":
778
+ return "video/mp4";
779
+ case ".mov":
780
+ return "video/quicktime";
781
+ case ".webm":
782
+ return "video/webm";
783
+ case ".mp3":
784
+ return "audio/mpeg";
785
+ case ".wav":
786
+ return "audio/wav";
787
+ case ".m4a":
788
+ return "audio/mp4";
789
+ case ".aac":
790
+ return "audio/aac";
791
+ case ".ogg":
792
+ return "audio/ogg";
793
+ case ".pdf":
794
+ return "application/pdf";
795
+ case ".md":
796
+ return "text/markdown; charset=utf-8";
797
+ case ".txt":
798
+ return "text/plain; charset=utf-8";
799
+ default:
800
+ return "application/octet-stream";
801
+ }
802
+ }
803
+ function resolveGithubToken() {
804
+ return process.env.VIDFARM_GITHUB_TOKEN
805
+ ?? process.env.GITHUB_TOKEN
806
+ ?? process.env.GH_TOKEN
807
+ ?? "";
808
+ }
809
+ async function resolveStackOutput(stackName, outputKey) {
810
+ const { stdout } = await runLocalCommand("aws", [
811
+ "cloudformation",
812
+ "describe-stacks",
813
+ "--stack-name", stackName,
814
+ "--query", `Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue`,
815
+ "--output", "text"
816
+ ]);
817
+ const value = stdout.trim();
818
+ if (!value || value === "None") {
819
+ throw new Error(`Could not resolve ${outputKey} from stack ${stackName}.`);
820
+ }
821
+ return value;
822
+ }
823
+ async function getProdCurrentImage(input) {
824
+ loadEnvFile(input.envFile);
825
+ const instanceId = await resolveStackOutput(input.stackName, "VidfarmInstanceId");
826
+ const stdout = await runSsmScript({
827
+ instanceId,
828
+ comment: "Read current Vidfarm image",
829
+ script: [
830
+ "set -euo pipefail",
831
+ "sudo docker ps -a --filter name=^/vidfarm$ --format '{{.Image}}' | head -n 1",
832
+ "sudo systemctl is-active vidfarm || true"
833
+ ].join("\n")
834
+ });
835
+ const [image, status] = stdout.trim().split(/\n+/);
836
+ if (!image) {
837
+ throw new Error("Could not determine the current live Docker image.");
838
+ }
839
+ return {
840
+ instanceId,
841
+ image,
842
+ status: status ?? ""
843
+ };
844
+ }
845
+ async function runSsmScript(input) {
846
+ const tempDir = mkTempDir();
847
+ try {
848
+ const commandPath = path.join(tempDir, "commands.json");
849
+ const scriptB64 = Buffer.from(input.script, "utf8").toString("base64");
850
+ writeFileSync(commandPath, JSON.stringify({
851
+ commands: [
852
+ `echo ${shellQuote(scriptB64)} | base64 --decode > /tmp/vidfarm-operator.sh`,
853
+ "bash /tmp/vidfarm-operator.sh"
854
+ ]
855
+ }));
856
+ const { stdout: sendStdout } = await runLocalCommand("aws", [
857
+ "ssm",
858
+ "send-command",
859
+ "--instance-ids", input.instanceId,
860
+ "--document-name", "AWS-RunShellScript",
861
+ "--comment", input.comment,
862
+ "--parameters", `file://${commandPath}`,
863
+ "--query", "Command.CommandId",
864
+ "--output", "text"
865
+ ]);
866
+ const commandId = sendStdout.trim();
867
+ if (!commandId) {
868
+ throw new Error(`Unable to start SSM command for ${input.comment}.`);
869
+ }
870
+ while (true) {
871
+ const { stdout } = await runLocalCommand("aws", [
872
+ "ssm",
873
+ "get-command-invocation",
874
+ "--command-id", commandId,
875
+ "--instance-id", input.instanceId
876
+ ]);
877
+ const response = JSON.parse(stdout);
878
+ if (response.Status === "Success") {
879
+ return response.StandardOutputContent ?? "";
880
+ }
881
+ if (["Failed", "Cancelled", "TimedOut", "Cancelling"].includes(response.Status)) {
882
+ throw new Error((response.StandardErrorContent ?? "").trim()
883
+ || (response.StandardOutputContent ?? "").trim()
884
+ || `SSM command failed with status ${response.Status}.`);
885
+ }
886
+ await sleep(2000);
887
+ }
888
+ }
889
+ finally {
890
+ rmSync(tempDir, { recursive: true, force: true });
891
+ }
892
+ }
893
+ async function runLocalCommand(command, args) {
894
+ return execFileAsync(command, args, {
895
+ cwd: process.cwd(),
896
+ env: process.env,
897
+ maxBuffer: 10 * 1024 * 1024
898
+ });
899
+ }
900
+ function shellQuote(value) {
901
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
902
+ }
903
+ function mkTempDir() {
904
+ return mkdtempSync(path.join(os.tmpdir(), "vidfarm-operator-"));
905
+ }
906
+ function extractLastJson(stdout) {
907
+ const trimmed = stdout.trim();
908
+ const start = trimmed.lastIndexOf("\n{");
909
+ const jsonText = start >= 0 ? trimmed.slice(start + 1) : trimmed;
910
+ return JSON.parse(jsonText);
911
+ }
912
+ function sleep(ms) {
913
+ return new Promise((resolve) => setTimeout(resolve, ms));
466
914
  }
@@ -41,7 +41,7 @@ const schema = z.object({
41
41
  REMOTION_AWS_ACCESS_KEY_ID: z.string().optional(),
42
42
  REMOTION_AWS_SECRET_ACCESS_KEY: z.string().optional(),
43
43
  REMOTION_MODE: z.enum(["auto", "mock", "local", "lambda"]).default("auto"),
44
- MOCK_PROVIDER_RESPONSES: z.string().default("true"),
44
+ MOCK_PROVIDER_RESPONSES: z.string().optional(),
45
45
  VIDFARM_ADMIN_EMAILS: z.string().default(""),
46
46
  VIDFARM_DEVELOPER_EMAILS: z.string().default(""),
47
47
  TEMPLATE_SOURCE_ROOT: z.string().default("./data/template-sources")
@@ -59,7 +59,8 @@ export const config = {
59
59
  TEMPLATE_SOURCE_ROOT: templateSourceRoot,
60
60
  PUBLIC_BASE_URL: publicBaseUrl,
61
61
  isProduction: parsed.NODE_ENV === "production",
62
- mockProviders: parsed.MOCK_PROVIDER_RESPONSES !== "false",
62
+ mockProviders: parsed.MOCK_PROVIDER_RESPONSES === "true" ||
63
+ (parsed.MOCK_PROVIDER_RESPONSES == null && parsed.NODE_ENV !== "production"),
63
64
  s3PublicRead: parsed.AWS_S3_PUBLIC_READ === "true",
64
65
  adminEmails: parsed.VIDFARM_ADMIN_EMAILS
65
66
  .split(",")