@vibeframe/cli 0.27.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 (420) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +21 -0
  3. package/.turbo/turbo-test.log +689 -0
  4. package/dist/agent/adapters/claude.d.ts +15 -0
  5. package/dist/agent/adapters/claude.d.ts.map +1 -0
  6. package/dist/agent/adapters/claude.js +119 -0
  7. package/dist/agent/adapters/claude.js.map +1 -0
  8. package/dist/agent/adapters/gemini.d.ts +15 -0
  9. package/dist/agent/adapters/gemini.d.ts.map +1 -0
  10. package/dist/agent/adapters/gemini.js +132 -0
  11. package/dist/agent/adapters/gemini.js.map +1 -0
  12. package/dist/agent/adapters/index.d.ts +27 -0
  13. package/dist/agent/adapters/index.d.ts.map +1 -0
  14. package/dist/agent/adapters/index.js +38 -0
  15. package/dist/agent/adapters/index.js.map +1 -0
  16. package/dist/agent/adapters/ollama.d.ts +20 -0
  17. package/dist/agent/adapters/ollama.d.ts.map +1 -0
  18. package/dist/agent/adapters/ollama.js +186 -0
  19. package/dist/agent/adapters/ollama.js.map +1 -0
  20. package/dist/agent/adapters/openai.d.ts +15 -0
  21. package/dist/agent/adapters/openai.d.ts.map +1 -0
  22. package/dist/agent/adapters/openai.js +92 -0
  23. package/dist/agent/adapters/openai.js.map +1 -0
  24. package/dist/agent/adapters/xai.d.ts +15 -0
  25. package/dist/agent/adapters/xai.d.ts.map +1 -0
  26. package/dist/agent/adapters/xai.js +95 -0
  27. package/dist/agent/adapters/xai.js.map +1 -0
  28. package/dist/agent/index.d.ts +69 -0
  29. package/dist/agent/index.d.ts.map +1 -0
  30. package/dist/agent/index.js +180 -0
  31. package/dist/agent/index.js.map +1 -0
  32. package/dist/agent/memory/index.d.ts +70 -0
  33. package/dist/agent/memory/index.d.ts.map +1 -0
  34. package/dist/agent/memory/index.js +132 -0
  35. package/dist/agent/memory/index.js.map +1 -0
  36. package/dist/agent/prompts/system.d.ts +6 -0
  37. package/dist/agent/prompts/system.d.ts.map +1 -0
  38. package/dist/agent/prompts/system.js +103 -0
  39. package/dist/agent/prompts/system.js.map +1 -0
  40. package/dist/agent/tools/ai-editing.d.ts +15 -0
  41. package/dist/agent/tools/ai-editing.d.ts.map +1 -0
  42. package/dist/agent/tools/ai-editing.js +763 -0
  43. package/dist/agent/tools/ai-editing.js.map +1 -0
  44. package/dist/agent/tools/ai-generation.d.ts +13 -0
  45. package/dist/agent/tools/ai-generation.d.ts.map +1 -0
  46. package/dist/agent/tools/ai-generation.js +973 -0
  47. package/dist/agent/tools/ai-generation.js.map +1 -0
  48. package/dist/agent/tools/ai-pipeline.d.ts +14 -0
  49. package/dist/agent/tools/ai-pipeline.d.ts.map +1 -0
  50. package/dist/agent/tools/ai-pipeline.js +961 -0
  51. package/dist/agent/tools/ai-pipeline.js.map +1 -0
  52. package/dist/agent/tools/ai.d.ts +13 -0
  53. package/dist/agent/tools/ai.d.ts.map +1 -0
  54. package/dist/agent/tools/ai.js +19 -0
  55. package/dist/agent/tools/ai.js.map +1 -0
  56. package/dist/agent/tools/batch.d.ts +6 -0
  57. package/dist/agent/tools/batch.d.ts.map +1 -0
  58. package/dist/agent/tools/batch.js +383 -0
  59. package/dist/agent/tools/batch.js.map +1 -0
  60. package/dist/agent/tools/e2e.test.d.ts +26 -0
  61. package/dist/agent/tools/e2e.test.d.ts.map +1 -0
  62. package/dist/agent/tools/e2e.test.js +397 -0
  63. package/dist/agent/tools/e2e.test.js.map +1 -0
  64. package/dist/agent/tools/export.d.ts +6 -0
  65. package/dist/agent/tools/export.d.ts.map +1 -0
  66. package/dist/agent/tools/export.js +171 -0
  67. package/dist/agent/tools/export.js.map +1 -0
  68. package/dist/agent/tools/filesystem.d.ts +6 -0
  69. package/dist/agent/tools/filesystem.d.ts.map +1 -0
  70. package/dist/agent/tools/filesystem.js +212 -0
  71. package/dist/agent/tools/filesystem.js.map +1 -0
  72. package/dist/agent/tools/index.d.ts +65 -0
  73. package/dist/agent/tools/index.d.ts.map +1 -0
  74. package/dist/agent/tools/index.js +120 -0
  75. package/dist/agent/tools/index.js.map +1 -0
  76. package/dist/agent/tools/integration.test.d.ts +11 -0
  77. package/dist/agent/tools/integration.test.d.ts.map +1 -0
  78. package/dist/agent/tools/integration.test.js +659 -0
  79. package/dist/agent/tools/integration.test.js.map +1 -0
  80. package/dist/agent/tools/media.d.ts +6 -0
  81. package/dist/agent/tools/media.d.ts.map +1 -0
  82. package/dist/agent/tools/media.js +616 -0
  83. package/dist/agent/tools/media.js.map +1 -0
  84. package/dist/agent/tools/project.d.ts +6 -0
  85. package/dist/agent/tools/project.d.ts.map +1 -0
  86. package/dist/agent/tools/project.js +284 -0
  87. package/dist/agent/tools/project.js.map +1 -0
  88. package/dist/agent/tools/timeline.d.ts +6 -0
  89. package/dist/agent/tools/timeline.d.ts.map +1 -0
  90. package/dist/agent/tools/timeline.js +873 -0
  91. package/dist/agent/tools/timeline.js.map +1 -0
  92. package/dist/agent/types.d.ts +59 -0
  93. package/dist/agent/types.d.ts.map +1 -0
  94. package/dist/agent/types.js +5 -0
  95. package/dist/agent/types.js.map +1 -0
  96. package/dist/commands/agent.d.ts +21 -0
  97. package/dist/commands/agent.d.ts.map +1 -0
  98. package/dist/commands/agent.js +290 -0
  99. package/dist/commands/agent.js.map +1 -0
  100. package/dist/commands/ai-analyze.d.ts +106 -0
  101. package/dist/commands/ai-analyze.d.ts.map +1 -0
  102. package/dist/commands/ai-analyze.js +327 -0
  103. package/dist/commands/ai-analyze.js.map +1 -0
  104. package/dist/commands/ai-animated-caption.d.ts +64 -0
  105. package/dist/commands/ai-animated-caption.d.ts.map +1 -0
  106. package/dist/commands/ai-animated-caption.js +272 -0
  107. package/dist/commands/ai-animated-caption.js.map +1 -0
  108. package/dist/commands/ai-audio.d.ts +20 -0
  109. package/dist/commands/ai-audio.d.ts.map +1 -0
  110. package/dist/commands/ai-audio.js +808 -0
  111. package/dist/commands/ai-audio.js.map +1 -0
  112. package/dist/commands/ai-broll.d.ts +15 -0
  113. package/dist/commands/ai-broll.d.ts.map +1 -0
  114. package/dist/commands/ai-broll.js +406 -0
  115. package/dist/commands/ai-broll.js.map +1 -0
  116. package/dist/commands/ai-edit-cli.d.ts +14 -0
  117. package/dist/commands/ai-edit-cli.d.ts.map +1 -0
  118. package/dist/commands/ai-edit-cli.js +579 -0
  119. package/dist/commands/ai-edit-cli.js.map +1 -0
  120. package/dist/commands/ai-edit.d.ts +398 -0
  121. package/dist/commands/ai-edit.d.ts.map +1 -0
  122. package/dist/commands/ai-edit.js +1019 -0
  123. package/dist/commands/ai-edit.js.map +1 -0
  124. package/dist/commands/ai-fill-gaps.d.ts +14 -0
  125. package/dist/commands/ai-fill-gaps.d.ts.map +1 -0
  126. package/dist/commands/ai-fill-gaps.js +451 -0
  127. package/dist/commands/ai-fill-gaps.js.map +1 -0
  128. package/dist/commands/ai-helpers.d.ts +20 -0
  129. package/dist/commands/ai-helpers.d.ts.map +1 -0
  130. package/dist/commands/ai-helpers.js +59 -0
  131. package/dist/commands/ai-helpers.js.map +1 -0
  132. package/dist/commands/ai-highlights.d.ts +127 -0
  133. package/dist/commands/ai-highlights.d.ts.map +1 -0
  134. package/dist/commands/ai-highlights.js +1026 -0
  135. package/dist/commands/ai-highlights.js.map +1 -0
  136. package/dist/commands/ai-image.d.ts +34 -0
  137. package/dist/commands/ai-image.d.ts.map +1 -0
  138. package/dist/commands/ai-image.js +653 -0
  139. package/dist/commands/ai-image.js.map +1 -0
  140. package/dist/commands/ai-motion.d.ts +50 -0
  141. package/dist/commands/ai-motion.d.ts.map +1 -0
  142. package/dist/commands/ai-motion.js +271 -0
  143. package/dist/commands/ai-motion.js.map +1 -0
  144. package/dist/commands/ai-narrate.d.ts +66 -0
  145. package/dist/commands/ai-narrate.d.ts.map +1 -0
  146. package/dist/commands/ai-narrate.js +329 -0
  147. package/dist/commands/ai-narrate.js.map +1 -0
  148. package/dist/commands/ai-review.d.ts +57 -0
  149. package/dist/commands/ai-review.d.ts.map +1 -0
  150. package/dist/commands/ai-review.js +251 -0
  151. package/dist/commands/ai-review.js.map +1 -0
  152. package/dist/commands/ai-script-pipeline-cli.d.ts +9 -0
  153. package/dist/commands/ai-script-pipeline-cli.d.ts.map +1 -0
  154. package/dist/commands/ai-script-pipeline-cli.js +1494 -0
  155. package/dist/commands/ai-script-pipeline-cli.js.map +1 -0
  156. package/dist/commands/ai-script-pipeline.d.ts +259 -0
  157. package/dist/commands/ai-script-pipeline.d.ts.map +1 -0
  158. package/dist/commands/ai-script-pipeline.js +1027 -0
  159. package/dist/commands/ai-script-pipeline.js.map +1 -0
  160. package/dist/commands/ai-suggest-edit.d.ts +14 -0
  161. package/dist/commands/ai-suggest-edit.d.ts.map +1 -0
  162. package/dist/commands/ai-suggest-edit.js +220 -0
  163. package/dist/commands/ai-suggest-edit.js.map +1 -0
  164. package/dist/commands/ai-video-fx.d.ts +14 -0
  165. package/dist/commands/ai-video-fx.d.ts.map +1 -0
  166. package/dist/commands/ai-video-fx.js +395 -0
  167. package/dist/commands/ai-video-fx.js.map +1 -0
  168. package/dist/commands/ai-video.d.ts +15 -0
  169. package/dist/commands/ai-video.d.ts.map +1 -0
  170. package/dist/commands/ai-video.js +785 -0
  171. package/dist/commands/ai-video.js.map +1 -0
  172. package/dist/commands/ai-viral.d.ts +15 -0
  173. package/dist/commands/ai-viral.d.ts.map +1 -0
  174. package/dist/commands/ai-viral.js +519 -0
  175. package/dist/commands/ai-viral.js.map +1 -0
  176. package/dist/commands/ai-visual-fx.d.ts +14 -0
  177. package/dist/commands/ai-visual-fx.d.ts.map +1 -0
  178. package/dist/commands/ai-visual-fx.js +505 -0
  179. package/dist/commands/ai-visual-fx.js.map +1 -0
  180. package/dist/commands/ai.d.ts +38 -0
  181. package/dist/commands/ai.d.ts.map +1 -0
  182. package/dist/commands/ai.js +225 -0
  183. package/dist/commands/ai.js.map +1 -0
  184. package/dist/commands/ai.test.d.ts +2 -0
  185. package/dist/commands/ai.test.d.ts.map +1 -0
  186. package/dist/commands/ai.test.js +554 -0
  187. package/dist/commands/ai.test.js.map +1 -0
  188. package/dist/commands/analyze.d.ts +16 -0
  189. package/dist/commands/analyze.d.ts.map +1 -0
  190. package/dist/commands/analyze.js +247 -0
  191. package/dist/commands/analyze.js.map +1 -0
  192. package/dist/commands/audio.d.ts +18 -0
  193. package/dist/commands/audio.d.ts.map +1 -0
  194. package/dist/commands/audio.js +539 -0
  195. package/dist/commands/audio.js.map +1 -0
  196. package/dist/commands/batch.d.ts +3 -0
  197. package/dist/commands/batch.d.ts.map +1 -0
  198. package/dist/commands/batch.js +366 -0
  199. package/dist/commands/batch.js.map +1 -0
  200. package/dist/commands/batch.test.d.ts +2 -0
  201. package/dist/commands/batch.test.d.ts.map +1 -0
  202. package/dist/commands/batch.test.js +203 -0
  203. package/dist/commands/batch.test.js.map +1 -0
  204. package/dist/commands/detect.d.ts +3 -0
  205. package/dist/commands/detect.d.ts.map +1 -0
  206. package/dist/commands/detect.js +273 -0
  207. package/dist/commands/detect.js.map +1 -0
  208. package/dist/commands/doctor.d.ts +6 -0
  209. package/dist/commands/doctor.d.ts.map +1 -0
  210. package/dist/commands/doctor.js +191 -0
  211. package/dist/commands/doctor.js.map +1 -0
  212. package/dist/commands/edit-cmd.d.ts +26 -0
  213. package/dist/commands/edit-cmd.d.ts.map +1 -0
  214. package/dist/commands/edit-cmd.js +870 -0
  215. package/dist/commands/edit-cmd.js.map +1 -0
  216. package/dist/commands/export.d.ts +39 -0
  217. package/dist/commands/export.d.ts.map +1 -0
  218. package/dist/commands/export.js +730 -0
  219. package/dist/commands/export.js.map +1 -0
  220. package/dist/commands/generate.d.ts +25 -0
  221. package/dist/commands/generate.d.ts.map +1 -0
  222. package/dist/commands/generate.js +1885 -0
  223. package/dist/commands/generate.js.map +1 -0
  224. package/dist/commands/media.d.ts +3 -0
  225. package/dist/commands/media.d.ts.map +1 -0
  226. package/dist/commands/media.js +165 -0
  227. package/dist/commands/media.js.map +1 -0
  228. package/dist/commands/output.d.ts +45 -0
  229. package/dist/commands/output.d.ts.map +1 -0
  230. package/dist/commands/output.js +122 -0
  231. package/dist/commands/output.js.map +1 -0
  232. package/dist/commands/pipeline.d.ts +19 -0
  233. package/dist/commands/pipeline.d.ts.map +1 -0
  234. package/dist/commands/pipeline.js +345 -0
  235. package/dist/commands/pipeline.js.map +1 -0
  236. package/dist/commands/project.d.ts +3 -0
  237. package/dist/commands/project.d.ts.map +1 -0
  238. package/dist/commands/project.js +139 -0
  239. package/dist/commands/project.js.map +1 -0
  240. package/dist/commands/project.test.d.ts +2 -0
  241. package/dist/commands/project.test.d.ts.map +1 -0
  242. package/dist/commands/project.test.js +105 -0
  243. package/dist/commands/project.test.js.map +1 -0
  244. package/dist/commands/sanitize.d.ts +21 -0
  245. package/dist/commands/sanitize.d.ts.map +1 -0
  246. package/dist/commands/sanitize.js +56 -0
  247. package/dist/commands/sanitize.js.map +1 -0
  248. package/dist/commands/schema.d.ts +11 -0
  249. package/dist/commands/schema.d.ts.map +1 -0
  250. package/dist/commands/schema.js +101 -0
  251. package/dist/commands/schema.js.map +1 -0
  252. package/dist/commands/setup.d.ts +6 -0
  253. package/dist/commands/setup.d.ts.map +1 -0
  254. package/dist/commands/setup.js +440 -0
  255. package/dist/commands/setup.js.map +1 -0
  256. package/dist/commands/timeline.d.ts +3 -0
  257. package/dist/commands/timeline.d.ts.map +1 -0
  258. package/dist/commands/timeline.js +469 -0
  259. package/dist/commands/timeline.js.map +1 -0
  260. package/dist/commands/timeline.test.d.ts +2 -0
  261. package/dist/commands/timeline.test.d.ts.map +1 -0
  262. package/dist/commands/timeline.test.js +320 -0
  263. package/dist/commands/timeline.test.js.map +1 -0
  264. package/dist/commands/validate.d.ts +32 -0
  265. package/dist/commands/validate.d.ts.map +1 -0
  266. package/dist/commands/validate.js +63 -0
  267. package/dist/commands/validate.js.map +1 -0
  268. package/dist/config/config.test.d.ts +2 -0
  269. package/dist/config/config.test.d.ts.map +1 -0
  270. package/dist/config/config.test.js +164 -0
  271. package/dist/config/config.test.js.map +1 -0
  272. package/dist/config/index.d.ts +35 -0
  273. package/dist/config/index.d.ts.map +1 -0
  274. package/dist/config/index.js +101 -0
  275. package/dist/config/index.js.map +1 -0
  276. package/dist/config/schema.d.ts +43 -0
  277. package/dist/config/schema.d.ts.map +1 -0
  278. package/dist/config/schema.js +42 -0
  279. package/dist/config/schema.js.map +1 -0
  280. package/dist/engine/index.d.ts +3 -0
  281. package/dist/engine/index.d.ts.map +1 -0
  282. package/dist/engine/index.js +2 -0
  283. package/dist/engine/index.js.map +1 -0
  284. package/dist/engine/project.d.ts +84 -0
  285. package/dist/engine/project.d.ts.map +1 -0
  286. package/dist/engine/project.js +355 -0
  287. package/dist/engine/project.js.map +1 -0
  288. package/dist/engine/project.test.d.ts +2 -0
  289. package/dist/engine/project.test.d.ts.map +1 -0
  290. package/dist/engine/project.test.js +599 -0
  291. package/dist/engine/project.test.js.map +1 -0
  292. package/dist/index.d.ts +7 -0
  293. package/dist/index.d.ts.map +1 -0
  294. package/dist/index.js +131 -0
  295. package/dist/index.js.map +1 -0
  296. package/dist/utils/api-key.d.ts +36 -0
  297. package/dist/utils/api-key.d.ts.map +1 -0
  298. package/dist/utils/api-key.js +211 -0
  299. package/dist/utils/api-key.js.map +1 -0
  300. package/dist/utils/api-key.test.d.ts +2 -0
  301. package/dist/utils/api-key.test.d.ts.map +1 -0
  302. package/dist/utils/api-key.test.js +35 -0
  303. package/dist/utils/api-key.test.js.map +1 -0
  304. package/dist/utils/audio.d.ts +23 -0
  305. package/dist/utils/audio.d.ts.map +1 -0
  306. package/dist/utils/audio.js +79 -0
  307. package/dist/utils/audio.js.map +1 -0
  308. package/dist/utils/exec-safe.d.ts +22 -0
  309. package/dist/utils/exec-safe.d.ts.map +1 -0
  310. package/dist/utils/exec-safe.js +62 -0
  311. package/dist/utils/exec-safe.js.map +1 -0
  312. package/dist/utils/first-run.d.ts +13 -0
  313. package/dist/utils/first-run.d.ts.map +1 -0
  314. package/dist/utils/first-run.js +48 -0
  315. package/dist/utils/first-run.js.map +1 -0
  316. package/dist/utils/provider-resolver.d.ts +15 -0
  317. package/dist/utils/provider-resolver.d.ts.map +1 -0
  318. package/dist/utils/provider-resolver.js +42 -0
  319. package/dist/utils/provider-resolver.js.map +1 -0
  320. package/dist/utils/remotion.d.ts +210 -0
  321. package/dist/utils/remotion.d.ts.map +1 -0
  322. package/dist/utils/remotion.js +731 -0
  323. package/dist/utils/remotion.js.map +1 -0
  324. package/dist/utils/subtitle.d.ts +65 -0
  325. package/dist/utils/subtitle.d.ts.map +1 -0
  326. package/dist/utils/subtitle.js +135 -0
  327. package/dist/utils/subtitle.js.map +1 -0
  328. package/dist/utils/subtitle.test.d.ts +2 -0
  329. package/dist/utils/subtitle.test.d.ts.map +1 -0
  330. package/dist/utils/subtitle.test.js +175 -0
  331. package/dist/utils/subtitle.test.js.map +1 -0
  332. package/dist/utils/tty.d.ts +45 -0
  333. package/dist/utils/tty.d.ts.map +1 -0
  334. package/dist/utils/tty.js +172 -0
  335. package/dist/utils/tty.js.map +1 -0
  336. package/package.json +102 -0
  337. package/src/agent/adapters/claude.ts +143 -0
  338. package/src/agent/adapters/gemini.ts +159 -0
  339. package/src/agent/adapters/index.ts +61 -0
  340. package/src/agent/adapters/ollama.ts +231 -0
  341. package/src/agent/adapters/openai.ts +116 -0
  342. package/src/agent/adapters/xai.ts +119 -0
  343. package/src/agent/index.ts +251 -0
  344. package/src/agent/memory/index.ts +151 -0
  345. package/src/agent/prompts/system.ts +106 -0
  346. package/src/agent/tools/ai-editing.ts +845 -0
  347. package/src/agent/tools/ai-generation.ts +1073 -0
  348. package/src/agent/tools/ai-pipeline.ts +1055 -0
  349. package/src/agent/tools/ai.ts +21 -0
  350. package/src/agent/tools/batch.ts +429 -0
  351. package/src/agent/tools/e2e.test.ts +545 -0
  352. package/src/agent/tools/export.ts +184 -0
  353. package/src/agent/tools/filesystem.ts +237 -0
  354. package/src/agent/tools/index.ts +150 -0
  355. package/src/agent/tools/integration.test.ts +775 -0
  356. package/src/agent/tools/media.ts +697 -0
  357. package/src/agent/tools/project.ts +313 -0
  358. package/src/agent/tools/timeline.ts +951 -0
  359. package/src/agent/types.ts +68 -0
  360. package/src/commands/agent.ts +340 -0
  361. package/src/commands/ai-analyze.ts +429 -0
  362. package/src/commands/ai-animated-caption.ts +390 -0
  363. package/src/commands/ai-audio.ts +941 -0
  364. package/src/commands/ai-broll.ts +490 -0
  365. package/src/commands/ai-edit-cli.ts +658 -0
  366. package/src/commands/ai-edit.ts +1542 -0
  367. package/src/commands/ai-fill-gaps.ts +566 -0
  368. package/src/commands/ai-helpers.ts +65 -0
  369. package/src/commands/ai-highlights.ts +1303 -0
  370. package/src/commands/ai-image.ts +761 -0
  371. package/src/commands/ai-motion.ts +347 -0
  372. package/src/commands/ai-narrate.ts +451 -0
  373. package/src/commands/ai-review.ts +309 -0
  374. package/src/commands/ai-script-pipeline-cli.ts +1710 -0
  375. package/src/commands/ai-script-pipeline.ts +1365 -0
  376. package/src/commands/ai-suggest-edit.ts +264 -0
  377. package/src/commands/ai-video-fx.ts +445 -0
  378. package/src/commands/ai-video.ts +915 -0
  379. package/src/commands/ai-viral.ts +595 -0
  380. package/src/commands/ai-visual-fx.ts +601 -0
  381. package/src/commands/ai.test.ts +627 -0
  382. package/src/commands/ai.ts +307 -0
  383. package/src/commands/analyze.ts +282 -0
  384. package/src/commands/audio.ts +644 -0
  385. package/src/commands/batch.test.ts +279 -0
  386. package/src/commands/batch.ts +440 -0
  387. package/src/commands/detect.ts +329 -0
  388. package/src/commands/doctor.ts +237 -0
  389. package/src/commands/edit-cmd.ts +1014 -0
  390. package/src/commands/export.ts +918 -0
  391. package/src/commands/generate.ts +2146 -0
  392. package/src/commands/media.ts +177 -0
  393. package/src/commands/output.ts +142 -0
  394. package/src/commands/pipeline.ts +398 -0
  395. package/src/commands/project.test.ts +127 -0
  396. package/src/commands/project.ts +149 -0
  397. package/src/commands/sanitize.ts +60 -0
  398. package/src/commands/schema.ts +130 -0
  399. package/src/commands/setup.ts +509 -0
  400. package/src/commands/timeline.test.ts +499 -0
  401. package/src/commands/timeline.ts +529 -0
  402. package/src/commands/validate.ts +77 -0
  403. package/src/config/config.test.ts +197 -0
  404. package/src/config/index.ts +125 -0
  405. package/src/config/schema.ts +82 -0
  406. package/src/engine/index.ts +2 -0
  407. package/src/engine/project.test.ts +702 -0
  408. package/src/engine/project.ts +439 -0
  409. package/src/index.ts +146 -0
  410. package/src/utils/api-key.test.ts +41 -0
  411. package/src/utils/api-key.ts +247 -0
  412. package/src/utils/audio.ts +83 -0
  413. package/src/utils/exec-safe.ts +75 -0
  414. package/src/utils/first-run.ts +52 -0
  415. package/src/utils/provider-resolver.ts +56 -0
  416. package/src/utils/remotion.ts +951 -0
  417. package/src/utils/subtitle.test.ts +227 -0
  418. package/src/utils/subtitle.ts +169 -0
  419. package/src/utils/tty.ts +196 -0
  420. package/tsconfig.json +20 -0
@@ -0,0 +1,529 @@
1
+ import { Command } from "commander";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { resolve, basename, extname } from "node:path";
4
+ import chalk from "chalk";
5
+ import ora from "ora";
6
+ import { Project, type ProjectFile } from "../engine/index.js";
7
+ import type { MediaType } from "@vibeframe/core/timeline";
8
+ import { validateResourceId } from "./validate.js";
9
+
10
+ export const timelineCommand = new Command("timeline")
11
+ .description("Timeline editing commands");
12
+
13
+ timelineCommand
14
+ .command("add-source")
15
+ .description("Add a media source to the project")
16
+ .argument("<project>", "Project file path")
17
+ .argument("<media>", "Media file path")
18
+ .option("-n, --name <name>", "Source name (defaults to filename)")
19
+ .option("-t, --type <type>", "Media type (video, audio, image)")
20
+ .option("-d, --duration <seconds>", "Duration in seconds (required for images)")
21
+ .action(async (projectPath: string, mediaPath: string, options) => {
22
+ const spinner = ora("Adding source...").start();
23
+
24
+ try {
25
+ const filePath = resolve(process.cwd(), projectPath);
26
+ const content = await readFile(filePath, "utf-8");
27
+ const data: ProjectFile = JSON.parse(content);
28
+ const project = Project.fromJSON(data);
29
+
30
+ const absMediaPath = resolve(process.cwd(), mediaPath);
31
+ const mediaName = options.name || basename(mediaPath);
32
+ const mediaType = options.type || detectMediaType(mediaPath);
33
+ const duration = parseFloat(options.duration) || 5; // Default 5s for images
34
+
35
+ const source = project.addSource({
36
+ name: mediaName,
37
+ type: mediaType,
38
+ url: absMediaPath,
39
+ duration,
40
+ });
41
+
42
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
43
+
44
+ spinner.succeed(chalk.green(`Source added: ${source.id}`));
45
+ console.log();
46
+ console.log(chalk.dim(" Name:"), mediaName);
47
+ console.log(chalk.dim(" Type:"), mediaType);
48
+ console.log(chalk.dim(" Path:"), absMediaPath);
49
+ console.log(chalk.dim(" Duration:"), duration, "s");
50
+ } catch (error) {
51
+ spinner.fail(chalk.red("Failed to add source"));
52
+ console.error(error);
53
+ process.exit(1);
54
+ }
55
+ });
56
+
57
+ timelineCommand
58
+ .command("add-clip")
59
+ .description("Add a clip to the timeline")
60
+ .argument("<project>", "Project file path")
61
+ .argument("<source-id>", "Source ID to use")
62
+ .option("-t, --track <id>", "Track ID (defaults to first matching track)")
63
+ .option("-s, --start <seconds>", "Start time in timeline", "0")
64
+ .option("-d, --duration <seconds>", "Clip duration (defaults to source duration)")
65
+ .option("--offset <seconds>", "Source start offset", "0")
66
+ .action(async (projectPath: string, sourceId: string, options) => {
67
+ const spinner = ora("Adding clip...").start();
68
+
69
+ try {
70
+ validateResourceId(sourceId);
71
+ if (options.track) validateResourceId(options.track);
72
+
73
+ const filePath = resolve(process.cwd(), projectPath);
74
+ const content = await readFile(filePath, "utf-8");
75
+ const data: ProjectFile = JSON.parse(content);
76
+ const project = Project.fromJSON(data);
77
+
78
+ const source = project.getSource(sourceId);
79
+ if (!source) {
80
+ spinner.fail(chalk.red(`Source not found: ${sourceId}`));
81
+ process.exit(1);
82
+ }
83
+
84
+ // Find track (images use video track, like REPL does)
85
+ let trackId = options.track;
86
+ if (!trackId) {
87
+ const trackType = source.type === "audio" ? "audio" : "video";
88
+ const tracks = project.getTracksByType(trackType);
89
+ if (tracks.length === 0) {
90
+ spinner.fail(chalk.red(`No ${trackType} track found. Create one first.`));
91
+ process.exit(1);
92
+ }
93
+ trackId = tracks[0].id;
94
+ }
95
+
96
+ const startTime = parseFloat(options.start);
97
+ const sourceOffset = parseFloat(options.offset);
98
+ const duration = options.duration ? parseFloat(options.duration) : source.duration;
99
+
100
+ const clip = project.addClip({
101
+ sourceId,
102
+ trackId,
103
+ startTime,
104
+ duration,
105
+ sourceStartOffset: sourceOffset,
106
+ sourceEndOffset: sourceOffset + duration,
107
+ });
108
+
109
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
110
+
111
+ spinner.succeed(chalk.green(`Clip added: ${clip.id}`));
112
+ console.log();
113
+ console.log(chalk.dim(" Source:"), source.name);
114
+ console.log(chalk.dim(" Track:"), trackId);
115
+ console.log(chalk.dim(" Start:"), startTime, "s");
116
+ console.log(chalk.dim(" Duration:"), duration, "s");
117
+ } catch (error) {
118
+ spinner.fail(chalk.red("Failed to add clip"));
119
+ console.error(error);
120
+ process.exit(1);
121
+ }
122
+ });
123
+
124
+ timelineCommand
125
+ .command("add-track")
126
+ .description("Add a new track")
127
+ .argument("<project>", "Project file path")
128
+ .argument("<type>", "Track type (video, audio)")
129
+ .option("-n, --name <name>", "Track name")
130
+ .action(async (projectPath: string, type: string, options) => {
131
+ const spinner = ora("Adding track...").start();
132
+
133
+ try {
134
+ const filePath = resolve(process.cwd(), projectPath);
135
+ const content = await readFile(filePath, "utf-8");
136
+ const data: ProjectFile = JSON.parse(content);
137
+ const project = Project.fromJSON(data);
138
+
139
+ const existingTracks = project.getTracksByType(type as MediaType);
140
+ const trackName = options.name || `${type.charAt(0).toUpperCase() + type.slice(1)} ${existingTracks.length + 1}`;
141
+ const order = project.getTracks().length;
142
+
143
+ const track = project.addTrack({
144
+ name: trackName,
145
+ type: type as MediaType,
146
+ order,
147
+ isMuted: false,
148
+ isLocked: false,
149
+ isVisible: true,
150
+ });
151
+
152
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
153
+
154
+ spinner.succeed(chalk.green(`Track added: ${track.id}`));
155
+ console.log();
156
+ console.log(chalk.dim(" Name:"), track.name);
157
+ console.log(chalk.dim(" Type:"), track.type);
158
+ } catch (error) {
159
+ spinner.fail(chalk.red("Failed to add track"));
160
+ console.error(error);
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ timelineCommand
166
+ .command("add-effect")
167
+ .description("Add an effect to a clip")
168
+ .argument("<project>", "Project file path")
169
+ .argument("<clip-id>", "Clip ID")
170
+ .argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)")
171
+ .option("-s, --start <seconds>", "Effect start time (relative to clip)", "0")
172
+ .option("-d, --duration <seconds>", "Effect duration (defaults to clip duration)")
173
+ .option("-p, --params <json>", "Effect parameters as JSON", "{}")
174
+ .action(async (projectPath: string, clipId: string, effectType: string, options) => {
175
+ const spinner = ora("Adding effect...").start();
176
+
177
+ try {
178
+ validateResourceId(clipId);
179
+
180
+ const filePath = resolve(process.cwd(), projectPath);
181
+ const content = await readFile(filePath, "utf-8");
182
+ const data: ProjectFile = JSON.parse(content);
183
+ const project = Project.fromJSON(data);
184
+
185
+ const clip = project.getClip(clipId);
186
+ if (!clip) {
187
+ spinner.fail(chalk.red(`Clip not found: ${clipId}`));
188
+ process.exit(1);
189
+ }
190
+
191
+ const startTime = parseFloat(options.start);
192
+ const duration = options.duration ? parseFloat(options.duration) : clip.duration;
193
+ const params = JSON.parse(options.params);
194
+
195
+ const effect = project.addEffect(clipId, {
196
+ type: effectType as any,
197
+ startTime,
198
+ duration,
199
+ params,
200
+ });
201
+
202
+ if (!effect) {
203
+ spinner.fail(chalk.red("Failed to add effect"));
204
+ process.exit(1);
205
+ }
206
+
207
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
208
+
209
+ spinner.succeed(chalk.green(`Effect added: ${effect.id}`));
210
+ console.log();
211
+ console.log(chalk.dim(" Type:"), effectType);
212
+ console.log(chalk.dim(" Start:"), startTime, "s");
213
+ console.log(chalk.dim(" Duration:"), duration, "s");
214
+ } catch (error) {
215
+ spinner.fail(chalk.red("Failed to add effect"));
216
+ console.error(error);
217
+ process.exit(1);
218
+ }
219
+ });
220
+
221
+ timelineCommand
222
+ .command("trim")
223
+ .description("Trim a clip")
224
+ .argument("<project>", "Project file path")
225
+ .argument("<clip-id>", "Clip ID")
226
+ .option("--start <seconds>", "New start time")
227
+ .option("--duration <seconds>", "New duration")
228
+ .action(async (projectPath: string, clipId: string, options) => {
229
+ const spinner = ora("Trimming clip...").start();
230
+
231
+ try {
232
+ validateResourceId(clipId);
233
+
234
+ const filePath = resolve(process.cwd(), projectPath);
235
+ const content = await readFile(filePath, "utf-8");
236
+ const data: ProjectFile = JSON.parse(content);
237
+ const project = Project.fromJSON(data);
238
+
239
+ const clip = project.getClip(clipId);
240
+ if (!clip) {
241
+ spinner.fail(chalk.red(`Clip not found: ${clipId}`));
242
+ process.exit(1);
243
+ }
244
+
245
+ if (options.start !== undefined) {
246
+ project.trimClipStart(clipId, parseFloat(options.start));
247
+ }
248
+ if (options.duration !== undefined) {
249
+ project.trimClipEnd(clipId, parseFloat(options.duration));
250
+ }
251
+
252
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
253
+
254
+ const updatedClip = project.getClip(clipId)!;
255
+ spinner.succeed(chalk.green("Clip trimmed"));
256
+ console.log();
257
+ console.log(chalk.dim(" Start:"), updatedClip.startTime, "s");
258
+ console.log(chalk.dim(" Duration:"), updatedClip.duration, "s");
259
+ } catch (error) {
260
+ spinner.fail(chalk.red("Failed to trim clip"));
261
+ console.error(error);
262
+ process.exit(1);
263
+ }
264
+ });
265
+
266
+ timelineCommand
267
+ .command("list")
268
+ .description("List timeline contents")
269
+ .argument("<project>", "Project file path")
270
+ .option("--sources", "List sources only")
271
+ .option("--tracks", "List tracks only")
272
+ .option("--clips", "List clips only")
273
+ .action(async (projectPath: string, options) => {
274
+ try {
275
+ const filePath = resolve(process.cwd(), projectPath);
276
+ const content = await readFile(filePath, "utf-8");
277
+ const data: ProjectFile = JSON.parse(content);
278
+ const project = Project.fromJSON(data);
279
+
280
+ const showAll = !options.sources && !options.tracks && !options.clips;
281
+
282
+ if (showAll || options.sources) {
283
+ console.log();
284
+ console.log(chalk.bold.cyan("Sources"));
285
+ console.log(chalk.dim("─".repeat(60)));
286
+ const sources = project.getSources();
287
+ if (sources.length === 0) {
288
+ console.log(chalk.dim(" (none)"));
289
+ } else {
290
+ for (const source of sources) {
291
+ console.log(` ${chalk.yellow(source.id)}`);
292
+ console.log(` ${source.name} (${source.type}, ${source.duration}s)`);
293
+ }
294
+ }
295
+ }
296
+
297
+ if (showAll || options.tracks) {
298
+ console.log();
299
+ console.log(chalk.bold.cyan("Tracks"));
300
+ console.log(chalk.dim("─".repeat(60)));
301
+ const tracks = project.getTracks();
302
+ for (const track of tracks) {
303
+ const status = [
304
+ track.isMuted ? "muted" : null,
305
+ track.isLocked ? "locked" : null,
306
+ !track.isVisible ? "hidden" : null,
307
+ ].filter(Boolean).join(", ");
308
+ console.log(` ${chalk.yellow(track.id)}`);
309
+ console.log(` ${track.name} (${track.type})${status ? ` [${status}]` : ""}`);
310
+ }
311
+ }
312
+
313
+ if (showAll || options.clips) {
314
+ console.log();
315
+ console.log(chalk.bold.cyan("Clips"));
316
+ console.log(chalk.dim("─".repeat(60)));
317
+ const clips = project.getClips();
318
+ if (clips.length === 0) {
319
+ console.log(chalk.dim(" (none)"));
320
+ } else {
321
+ for (const clip of clips) {
322
+ const source = project.getSource(clip.sourceId);
323
+ console.log(` ${chalk.yellow(clip.id)}`);
324
+ console.log(` ${source?.name || "unknown"} @ ${clip.startTime}s (${clip.duration}s)`);
325
+ if (clip.effects.length > 0) {
326
+ console.log(` Effects: ${clip.effects.map((e) => e.type).join(", ")}`);
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ console.log();
333
+ } catch (error) {
334
+ console.error(chalk.red("Failed to list timeline"));
335
+ console.error(error);
336
+ process.exit(1);
337
+ }
338
+ });
339
+
340
+ timelineCommand
341
+ .command("split")
342
+ .description("Split a clip at a specific time")
343
+ .argument("<project>", "Project file path")
344
+ .argument("<clip-id>", "Clip ID to split")
345
+ .option("-t, --time <seconds>", "Split time relative to clip start", "0")
346
+ .action(async (projectPath: string, clipId: string, options) => {
347
+ const spinner = ora("Splitting clip...").start();
348
+
349
+ try {
350
+ validateResourceId(clipId);
351
+
352
+ const filePath = resolve(process.cwd(), projectPath);
353
+ const content = await readFile(filePath, "utf-8");
354
+ const data: ProjectFile = JSON.parse(content);
355
+ const project = Project.fromJSON(data);
356
+
357
+ const clip = project.getClip(clipId);
358
+ if (!clip) {
359
+ spinner.fail(chalk.red(`Clip not found: ${clipId}`));
360
+ process.exit(1);
361
+ }
362
+
363
+ const splitTime = parseFloat(options.time);
364
+ if (splitTime <= 0 || splitTime >= clip.duration) {
365
+ spinner.fail(chalk.red(`Invalid split time. Must be between 0 and ${clip.duration}s`));
366
+ process.exit(1);
367
+ }
368
+
369
+ const result = project.splitClip(clipId, splitTime);
370
+ if (!result) {
371
+ spinner.fail(chalk.red("Failed to split clip"));
372
+ process.exit(1);
373
+ }
374
+
375
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
376
+
377
+ const [first, second] = result;
378
+ spinner.succeed(chalk.green("Clip split successfully"));
379
+ console.log();
380
+ console.log(chalk.dim(" First clip:"), first.id, `(${first.duration.toFixed(2)}s)`);
381
+ console.log(chalk.dim(" Second clip:"), second.id, `(${second.duration.toFixed(2)}s)`);
382
+ } catch (error) {
383
+ spinner.fail(chalk.red("Failed to split clip"));
384
+ console.error(error);
385
+ process.exit(1);
386
+ }
387
+ });
388
+
389
+ timelineCommand
390
+ .command("duplicate")
391
+ .description("Duplicate a clip")
392
+ .argument("<project>", "Project file path")
393
+ .argument("<clip-id>", "Clip ID to duplicate")
394
+ .option("-t, --time <seconds>", "Start time for duplicate (default: after original)")
395
+ .action(async (projectPath: string, clipId: string, options) => {
396
+ const spinner = ora("Duplicating clip...").start();
397
+
398
+ try {
399
+ validateResourceId(clipId);
400
+
401
+ const filePath = resolve(process.cwd(), projectPath);
402
+ const content = await readFile(filePath, "utf-8");
403
+ const data: ProjectFile = JSON.parse(content);
404
+ const project = Project.fromJSON(data);
405
+
406
+ const clip = project.getClip(clipId);
407
+ if (!clip) {
408
+ spinner.fail(chalk.red(`Clip not found: ${clipId}`));
409
+ process.exit(1);
410
+ }
411
+
412
+ const offsetTime = options.time ? parseFloat(options.time) : undefined;
413
+ const duplicated = project.duplicateClip(clipId, offsetTime);
414
+
415
+ if (!duplicated) {
416
+ spinner.fail(chalk.red("Failed to duplicate clip"));
417
+ process.exit(1);
418
+ }
419
+
420
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
421
+
422
+ spinner.succeed(chalk.green(`Clip duplicated: ${duplicated.id}`));
423
+ console.log();
424
+ console.log(chalk.dim(" Start:"), duplicated.startTime, "s");
425
+ console.log(chalk.dim(" Duration:"), duplicated.duration, "s");
426
+ } catch (error) {
427
+ spinner.fail(chalk.red("Failed to duplicate clip"));
428
+ console.error(error);
429
+ process.exit(1);
430
+ }
431
+ });
432
+
433
+ timelineCommand
434
+ .command("delete")
435
+ .description("Delete a clip from the timeline")
436
+ .argument("<project>", "Project file path")
437
+ .argument("<clip-id>", "Clip ID to delete")
438
+ .action(async (projectPath: string, clipId: string) => {
439
+ const spinner = ora("Deleting clip...").start();
440
+
441
+ try {
442
+ validateResourceId(clipId);
443
+
444
+ const filePath = resolve(process.cwd(), projectPath);
445
+ const content = await readFile(filePath, "utf-8");
446
+ const data: ProjectFile = JSON.parse(content);
447
+ const project = Project.fromJSON(data);
448
+
449
+ const clip = project.getClip(clipId);
450
+ if (!clip) {
451
+ spinner.fail(chalk.red(`Clip not found: ${clipId}`));
452
+ process.exit(1);
453
+ }
454
+
455
+ const removed = project.removeClip(clipId);
456
+ if (!removed) {
457
+ spinner.fail(chalk.red("Failed to delete clip"));
458
+ process.exit(1);
459
+ }
460
+
461
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
462
+
463
+ spinner.succeed(chalk.green("Clip deleted"));
464
+ } catch (error) {
465
+ spinner.fail(chalk.red("Failed to delete clip"));
466
+ console.error(error);
467
+ process.exit(1);
468
+ }
469
+ });
470
+
471
+ timelineCommand
472
+ .command("move")
473
+ .description("Move a clip to a new position")
474
+ .argument("<project>", "Project file path")
475
+ .argument("<clip-id>", "Clip ID to move")
476
+ .option("-t, --time <seconds>", "New start time")
477
+ .option("--track <track-id>", "Move to different track")
478
+ .action(async (projectPath: string, clipId: string, options) => {
479
+ const spinner = ora("Moving clip...").start();
480
+
481
+ try {
482
+ validateResourceId(clipId);
483
+ if (options.track) validateResourceId(options.track);
484
+
485
+ const filePath = resolve(process.cwd(), projectPath);
486
+ const content = await readFile(filePath, "utf-8");
487
+ const data: ProjectFile = JSON.parse(content);
488
+ const project = Project.fromJSON(data);
489
+
490
+ const clip = project.getClip(clipId);
491
+ if (!clip) {
492
+ spinner.fail(chalk.red(`Clip not found: ${clipId}`));
493
+ process.exit(1);
494
+ }
495
+
496
+ const newTime = options.time !== undefined ? parseFloat(options.time) : clip.startTime;
497
+ const newTrack = options.track || clip.trackId;
498
+
499
+ const moved = project.moveClip(clipId, newTrack, newTime);
500
+ if (!moved) {
501
+ spinner.fail(chalk.red("Failed to move clip"));
502
+ process.exit(1);
503
+ }
504
+
505
+ await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
506
+
507
+ const updated = project.getClip(clipId)!;
508
+ spinner.succeed(chalk.green("Clip moved"));
509
+ console.log();
510
+ console.log(chalk.dim(" Track:"), updated.trackId);
511
+ console.log(chalk.dim(" Start:"), updated.startTime, "s");
512
+ } catch (error) {
513
+ spinner.fail(chalk.red("Failed to move clip"));
514
+ console.error(error);
515
+ process.exit(1);
516
+ }
517
+ });
518
+
519
+ function detectMediaType(path: string): MediaType {
520
+ const ext = extname(path).toLowerCase();
521
+ const videoExts = [".mp4", ".mov", ".webm", ".avi", ".mkv"];
522
+ const audioExts = [".mp3", ".wav", ".ogg", ".m4a", ".aac"];
523
+ const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
524
+
525
+ if (videoExts.includes(ext)) return "video";
526
+ if (audioExts.includes(ext)) return "audio";
527
+ if (imageExts.includes(ext)) return "image";
528
+ return "video"; // Default
529
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @module validate
3
+ * @description Input validation layer for CLI commands.
4
+ * Guards against path traversal, control characters, and invalid resource IDs.
5
+ */
6
+
7
+ import { resolve, relative, isAbsolute } from "node:path";
8
+
9
+ /**
10
+ * Validate and resolve an output path, ensuring it stays within the working directory.
11
+ * Prevents path traversal attacks (e.g., --output ../../etc/passwd).
12
+ */
13
+ export function validateOutputPath(path: string, cwd = process.cwd()): string {
14
+ rejectControlChars(path);
15
+ const resolved = resolve(cwd, path);
16
+ const rel = relative(cwd, resolved);
17
+ if (rel.startsWith("..") || isAbsolute(rel)) {
18
+ throw new Error(
19
+ `Output path "${path}" escapes the working directory. Use a path within "${cwd}".`
20
+ );
21
+ }
22
+ return resolved;
23
+ }
24
+
25
+ /**
26
+ * Validate a resource ID (source-*, clip-*, track-*, effect-*).
27
+ * Only allows alphanumeric characters, hyphens, and underscores.
28
+ */
29
+ export function validateResourceId(id: string): string {
30
+ rejectControlChars(id);
31
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
32
+ throw new Error(
33
+ `Invalid resource ID "${id}". Only alphanumeric characters, hyphens, and underscores are allowed.`
34
+ );
35
+ }
36
+ if (id.length > 128) {
37
+ throw new Error(`Resource ID too long (max 128 characters).`);
38
+ }
39
+ return id;
40
+ }
41
+
42
+ /**
43
+ * Reject strings containing control characters (U+0000–U+001F, U+007F–U+009F).
44
+ * Prevents terminal injection and other control-char exploits.
45
+ */
46
+ export function rejectControlChars(input: string): string {
47
+ // eslint-disable-next-line no-control-regex
48
+ if (/[\x00-\x1f\x7f-\x9f]/.test(input)) {
49
+ throw new Error("Input contains invalid control characters.");
50
+ }
51
+ return input;
52
+ }
53
+
54
+ /**
55
+ * Validate a media file path and check its extension against an allowlist.
56
+ */
57
+ export function validateMediaFile(
58
+ path: string,
59
+ allowedExts: string[]
60
+ ): string {
61
+ rejectControlChars(path);
62
+ const ext = path.toLowerCase().split(".").pop();
63
+ if (!ext || !allowedExts.includes(`.${ext}`)) {
64
+ throw new Error(
65
+ `Unsupported file type ".${ext}". Allowed: ${allowedExts.join(", ")}`
66
+ );
67
+ }
68
+ return path;
69
+ }
70
+
71
+ /** Common media file extensions */
72
+ export const MEDIA_EXTS = {
73
+ video: [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".flv"],
74
+ audio: [".mp3", ".wav", ".aac", ".ogg", ".m4a", ".flac", ".wma"],
75
+ image: [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".svg"],
76
+ subtitle: [".srt", ".vtt", ".ass", ".ssa"],
77
+ };