@makeitvisible/cli 0.1.0 → 0.2.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.
package/dist/bin/index.js CHANGED
@@ -4,9 +4,10 @@ import chalk from 'chalk';
4
4
  import ora from 'ora';
5
5
  import { execSync } from 'child_process';
6
6
  import OpenAI from 'openai';
7
- import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
8
- import { join, relative, basename } from 'path';
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync } from 'fs';
8
+ import { dirname, resolve, join, relative, basename } from 'path';
9
9
  import { glob } from 'glob';
10
+ import { fileURLToPath, pathToFileURL } from 'url';
10
11
 
11
12
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
13
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -67,6 +68,92 @@ function getCommits(range) {
67
68
  function getDiffContent(range) {
68
69
  return git(`diff ${range}`);
69
70
  }
71
+ function extractCodeSnippets(diffContent, maxSnippets = 6) {
72
+ const snippets = [];
73
+ const lines = diffContent.split("\n");
74
+ let currentFile = "";
75
+ let currentHunk = [];
76
+ let currentStartLine = 0;
77
+ let inHunk = false;
78
+ for (const line of lines) {
79
+ if (line.startsWith("diff --git")) {
80
+ if (currentHunk.length > 0 && currentFile) {
81
+ snippets.push(createSnippetFromHunk(currentFile, currentHunk, currentStartLine));
82
+ if (snippets.length >= maxSnippets) break;
83
+ }
84
+ currentHunk = [];
85
+ inHunk = false;
86
+ continue;
87
+ }
88
+ if (line.startsWith("+++ b/")) {
89
+ currentFile = line.slice(6);
90
+ continue;
91
+ }
92
+ if (line.startsWith("@@")) {
93
+ if (currentHunk.length > 0 && currentFile) {
94
+ snippets.push(createSnippetFromHunk(currentFile, currentHunk, currentStartLine));
95
+ if (snippets.length >= maxSnippets) break;
96
+ }
97
+ const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)/);
98
+ currentStartLine = match ? parseInt(match[1], 10) : 1;
99
+ currentHunk = [];
100
+ inHunk = true;
101
+ continue;
102
+ }
103
+ if (inHunk && currentHunk.length < 15) {
104
+ currentHunk.push(line);
105
+ }
106
+ }
107
+ if (currentHunk.length > 0 && currentFile && snippets.length < maxSnippets) {
108
+ snippets.push(createSnippetFromHunk(currentFile, currentHunk, currentStartLine));
109
+ }
110
+ return snippets;
111
+ }
112
+ function createSnippetFromHunk(file, hunkLines, startLine) {
113
+ const hasAdditions = hunkLines.some((l) => l.startsWith("+"));
114
+ const hasDeletions = hunkLines.some((l) => l.startsWith("-"));
115
+ let changeType;
116
+ if (hasAdditions && hasDeletions) {
117
+ changeType = "modified";
118
+ } else if (hasAdditions) {
119
+ changeType = "added";
120
+ } else if (hasDeletions) {
121
+ changeType = "deleted";
122
+ } else {
123
+ changeType = "context";
124
+ }
125
+ const code = hunkLines.map((line) => {
126
+ if (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) {
127
+ return line.slice(1);
128
+ }
129
+ return line;
130
+ }).join("\n");
131
+ const ext = file.split(".").pop()?.toLowerCase() || "";
132
+ const langMap = {
133
+ ts: "typescript",
134
+ tsx: "typescript",
135
+ js: "javascript",
136
+ jsx: "javascript",
137
+ vue: "vue",
138
+ py: "python",
139
+ rb: "ruby",
140
+ go: "go",
141
+ rs: "rust",
142
+ java: "java",
143
+ css: "css",
144
+ scss: "scss",
145
+ html: "html",
146
+ json: "json"
147
+ };
148
+ return {
149
+ file,
150
+ code,
151
+ startLine,
152
+ language: langMap[ext] || "text",
153
+ description: `${changeType === "added" ? "Added" : changeType === "deleted" ? "Removed" : "Changed"} in ${file}`,
154
+ changeType
155
+ };
156
+ }
70
157
  function detectBreakingChanges(commits, diffContent) {
71
158
  const breakingPatterns = [
72
159
  /BREAKING CHANGE/i,
@@ -217,12 +304,14 @@ async function analyzeDiff(range) {
217
304
  breakingChanges,
218
305
  keywords
219
306
  };
307
+ const codeSnippets = extractCodeSnippets(diffContent);
220
308
  return {
221
309
  title,
222
310
  description,
223
311
  source,
224
312
  context,
225
- guidelines
313
+ guidelines,
314
+ codeSnippets
226
315
  };
227
316
  }
228
317
  async function analyzePR(options) {
@@ -1095,6 +1184,14 @@ Your mission is to investigate a codebase based on a user's query and produce a
1095
1184
 
1096
1185
  6. **Document Technical Details**: Note any important patterns, validations, error handling, or edge cases.
1097
1186
 
1187
+ ## Efficiency Rules (Important)
1188
+
1189
+ - Be efficient: use as few tools as possible (aim for 8-12 tool calls).
1190
+ - Prefer targeted \`search_files\` in content or filename over broad directory listing.
1191
+ - Read only the most relevant files; avoid reading the same file multiple times.
1192
+ - If you have enough information to explain the feature, **complete the investigation** even if some sections are partial.
1193
+ - Use empty arrays for unknown sections rather than continuing to search.
1194
+
1098
1195
  ## Tool Usage Tips
1099
1196
 
1100
1197
  - Use \`search_files\` with searchIn="filename" first for broad discovery
@@ -1138,6 +1235,7 @@ var DetectiveAgent = class {
1138
1235
  this.context = { projectRoot: options.projectRoot };
1139
1236
  this.options = {
1140
1237
  maxIterations: 25,
1238
+ maxToolCalls: 18,
1141
1239
  ...options
1142
1240
  };
1143
1241
  this.messages = [{ role: "system", content: SYSTEM_PROMPT }];
@@ -1156,14 +1254,23 @@ Use the available tools to explore the codebase, understand the feature, and the
1156
1254
  });
1157
1255
  let iterations = 0;
1158
1256
  const maxIterations = this.options.maxIterations;
1257
+ const maxToolCalls = this.options.maxToolCalls;
1159
1258
  while (iterations < maxIterations) {
1160
1259
  iterations++;
1161
1260
  try {
1261
+ const shouldForceCompletion = this.toolCallCount >= maxToolCalls || iterations === maxIterations;
1262
+ if (shouldForceCompletion) {
1263
+ this.messages.push({
1264
+ role: "user",
1265
+ content: "Finalize the investigation now using ONLY the information already gathered. Do not call any tools except complete_investigation. Use empty arrays for any section you could not verify."
1266
+ });
1267
+ }
1268
+ const tools = shouldForceCompletion ? AGENT_TOOLS.filter((tool) => tool.function.name === "complete_investigation") : AGENT_TOOLS;
1162
1269
  const response = await this.openai.chat.completions.create({
1163
- model: "gpt-4o",
1270
+ model: "gpt-5.2",
1164
1271
  messages: this.messages,
1165
- tools: AGENT_TOOLS,
1166
- tool_choice: "auto",
1272
+ tools,
1273
+ tool_choice: shouldForceCompletion ? "required" : "auto",
1167
1274
  temperature: 0.1
1168
1275
  });
1169
1276
  const message = response.choices[0].message;
@@ -1171,15 +1278,41 @@ Use the available tools to explore the codebase, understand the feature, and the
1171
1278
  if (message.content && this.options.onThinking) {
1172
1279
  this.options.onThinking(message.content);
1173
1280
  }
1174
- if (!message.tool_calls || message.tool_calls.length === 0) {
1175
- return {
1176
- success: false,
1177
- error: "Agent finished without completing investigation",
1178
- toolCalls: this.toolCallCount
1179
- };
1281
+ let toolCalls = message.tool_calls;
1282
+ if (!toolCalls || toolCalls.length === 0) {
1283
+ if (!shouldForceCompletion) {
1284
+ this.messages.push({
1285
+ role: "user",
1286
+ content: "Complete the investigation now using the current context. Call complete_investigation with best-effort details and empty arrays where needed."
1287
+ });
1288
+ const forced = await this.openai.chat.completions.create({
1289
+ model: "gpt-5.2",
1290
+ messages: this.messages,
1291
+ tools: AGENT_TOOLS.filter((tool) => tool.function.name === "complete_investigation"),
1292
+ tool_choice: "required",
1293
+ temperature: 0.1
1294
+ });
1295
+ const forcedMessage = forced.choices[0].message;
1296
+ this.messages.push(forcedMessage);
1297
+ if (forcedMessage.tool_calls && forcedMessage.tool_calls.length > 0) {
1298
+ toolCalls = forcedMessage.tool_calls;
1299
+ } else {
1300
+ return {
1301
+ success: false,
1302
+ error: forcedMessage.content ? `Agent finished without tool calls: ${forcedMessage.content}` : "Agent finished without completing investigation",
1303
+ toolCalls: this.toolCallCount
1304
+ };
1305
+ }
1306
+ } else {
1307
+ return {
1308
+ success: false,
1309
+ error: message.content ? `Agent finished without tool calls: ${message.content}` : "Agent finished without completing investigation",
1310
+ toolCalls: this.toolCallCount
1311
+ };
1312
+ }
1180
1313
  }
1181
1314
  const toolResults = [];
1182
- for (const toolCall of message.tool_calls) {
1315
+ for (const toolCall of toolCalls ?? []) {
1183
1316
  const toolName = toolCall.function.name;
1184
1317
  let args;
1185
1318
  try {
@@ -1254,34 +1387,48 @@ function getProjectRoot() {
1254
1387
 
1255
1388
  // src/api/client.ts
1256
1389
  function getConfig() {
1257
- const baseUrl = process.env.VISIBLE_API_BASE_URL;
1390
+ const baseUrl = (process.env.VISIBLE_API_BASE_URL || "http://localhost:3000/api").replace(
1391
+ /\/$/,
1392
+ ""
1393
+ );
1258
1394
  const apiKey = process.env.VISIBLE_API_KEY;
1259
- if (!baseUrl) {
1260
- throw new Error(
1261
- "Missing VISIBLE_API_BASE_URL environment variable.\nSet it to your Visible API endpoint (e.g., https://api.visible.dev)"
1262
- );
1263
- }
1264
- if (!apiKey) {
1395
+ if (!apiKey && !isLocalApi(baseUrl)) {
1265
1396
  throw new Error(
1266
1397
  "Missing VISIBLE_API_KEY environment variable.\nGet your API key from the Visible dashboard."
1267
1398
  );
1268
1399
  }
1269
- return { baseUrl: baseUrl.replace(/\/$/, ""), apiKey };
1400
+ return { baseUrl, apiKey };
1401
+ }
1402
+ function isLocalApi(baseUrl) {
1403
+ return baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1") || baseUrl.includes("0.0.0.0");
1404
+ }
1405
+ function buildRawChangelog(payload) {
1406
+ const details = payload.context.technicalDetails || [];
1407
+ const lines = [
1408
+ payload.description,
1409
+ details.length > 0 ? "" : void 0,
1410
+ details.length > 0 ? "Technical details:" : void 0,
1411
+ ...details.map((detail) => `- ${detail}`)
1412
+ ].filter((line) => typeof line === "string" && line.length > 0);
1413
+ return lines.join("\n");
1270
1414
  }
1271
1415
  async function postArtifact(payload) {
1272
1416
  const { baseUrl, apiKey } = getConfig();
1273
- const response = await fetch(`${baseUrl}/api/artifacts`, {
1417
+ const apiBase = baseUrl.endsWith("/api") ? baseUrl : `${baseUrl}/api`;
1418
+ const headers = {
1419
+ "Content-Type": "application/json"
1420
+ };
1421
+ if (apiKey) {
1422
+ headers.Authorization = `Bearer ${apiKey}`;
1423
+ }
1424
+ const response = await fetch(`${apiBase}/generate`, {
1274
1425
  method: "POST",
1275
1426
  headers: {
1276
- "Content-Type": "application/json",
1277
- Authorization: `Bearer ${apiKey}`
1427
+ ...headers
1278
1428
  },
1279
1429
  body: JSON.stringify({
1280
1430
  title: payload.title,
1281
- description: payload.description,
1282
- source: payload.source,
1283
- context: payload.context,
1284
- guidelines: payload.guidelines
1431
+ raw_changelog: buildRawChangelog(payload)
1285
1432
  })
1286
1433
  });
1287
1434
  if (!response.ok) {
@@ -1302,19 +1449,374 @@ async function postArtifact(payload) {
1302
1449
  throw new Error(errorMessage);
1303
1450
  }
1304
1451
  const data = await response.json();
1305
- if (!data.id || !data.url) {
1306
- throw new Error("Invalid API response: missing id or url");
1452
+ if (!data.link) {
1453
+ throw new Error("Invalid API response: missing link");
1307
1454
  }
1308
1455
  return {
1309
- id: data.id,
1310
- url: data.url
1456
+ link: data.link
1457
+ };
1458
+ }
1459
+
1460
+ // src/video/types.ts
1461
+ var RESOLUTIONS = {
1462
+ "1080p": { width: 1920, height: 1080 },
1463
+ "720p": { width: 1280, height: 720 },
1464
+ "480p": { width: 854, height: 480 },
1465
+ square: { width: 1080, height: 1080 },
1466
+ vertical: { width: 1080, height: 1920 }
1467
+ };
1468
+ var DEFAULT_THEME = {
1469
+ primaryColor: "#10B981",
1470
+ // Green accent
1471
+ backgroundColor: "#0A0A0B",
1472
+ // Dark background
1473
+ foregroundColor: "#FAFAFA",
1474
+ // Light text
1475
+ accentColor: "#10B981",
1476
+ // Green accent
1477
+ mutedColor: "#A1A1A6",
1478
+ // Secondary text
1479
+ cardColor: "#141415",
1480
+ // Card background
1481
+ borderColor: "#1C1C1E",
1482
+ // Border
1483
+ fontFamily: "Inter, system-ui, sans-serif"
1484
+ };
1485
+
1486
+ // src/video/storyboard.ts
1487
+ var FPS = 30;
1488
+ var DURATIONS = {
1489
+ title: 4,
1490
+ overview: 6,
1491
+ walkthrough: 8,
1492
+ code: 6,
1493
+ outro: 4
1494
+ };
1495
+ function generateStoryboard(artifact, theme = {}, codeSnippets, uiScenes) {
1496
+ const mergedTheme = { ...DEFAULT_THEME, ...theme };
1497
+ const scenes = [];
1498
+ const snippets = codeSnippets || artifact.codeSnippets || [];
1499
+ scenes.push(createTitleScene(artifact));
1500
+ scenes.push(createOverviewScene(artifact));
1501
+ const walkthroughScenes = createWalkthroughScenes(artifact);
1502
+ scenes.push(...walkthroughScenes);
1503
+ if (snippets.length > 0) {
1504
+ const codeHighlightScenes = createCodeHighlightScenes(snippets);
1505
+ scenes.push(...codeHighlightScenes);
1506
+ }
1507
+ if (artifact.context.affectedComponents.length > 0) {
1508
+ scenes.push(createCodeScene(artifact));
1509
+ }
1510
+ scenes.push(createOutroScene(artifact));
1511
+ const totalDurationInFrames = scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
1512
+ const enhancedArtifact = {
1513
+ ...artifact,
1514
+ codeSnippets: snippets
1515
+ };
1516
+ return {
1517
+ title: artifact.title,
1518
+ totalDurationInFrames,
1519
+ scenes,
1520
+ artifact: enhancedArtifact,
1521
+ theme: mergedTheme
1522
+ };
1523
+ }
1524
+ function createTitleScene(artifact) {
1525
+ return {
1526
+ id: "title",
1527
+ type: "title",
1528
+ title: artifact.title,
1529
+ content: artifact.description,
1530
+ durationInFrames: DURATIONS.title * FPS,
1531
+ props: {
1532
+ source: artifact.source,
1533
+ keywords: artifact.context.keywords.slice(0, 5)
1534
+ }
1535
+ };
1536
+ }
1537
+ function createOverviewScene(artifact) {
1538
+ return {
1539
+ id: "overview",
1540
+ type: "overview",
1541
+ title: "What Changed",
1542
+ content: artifact.context.summary,
1543
+ durationInFrames: DURATIONS.overview * FPS,
1544
+ props: {
1545
+ breakingChanges: artifact.context.breakingChanges,
1546
+ componentCount: artifact.context.affectedComponents.length
1547
+ }
1548
+ };
1549
+ }
1550
+ function createWalkthroughScenes(artifact) {
1551
+ const scenes = [];
1552
+ const details = artifact.context.technicalDetails;
1553
+ const chunks = chunkArray(details, 3);
1554
+ chunks.forEach((chunk, index) => {
1555
+ scenes.push({
1556
+ id: `walkthrough-${index + 1}`,
1557
+ type: "walkthrough",
1558
+ title: index === 0 ? "How It Works" : `Details (${index + 1})`,
1559
+ content: chunk.join("\n\n"),
1560
+ durationInFrames: DURATIONS.walkthrough * FPS,
1561
+ props: {
1562
+ steps: chunk,
1563
+ stepIndex: index
1564
+ }
1565
+ });
1566
+ });
1567
+ if (scenes.length === 0) {
1568
+ scenes.push({
1569
+ id: "walkthrough-1",
1570
+ type: "walkthrough",
1571
+ title: "How It Works",
1572
+ content: artifact.context.summary,
1573
+ durationInFrames: DURATIONS.walkthrough * FPS,
1574
+ props: {
1575
+ steps: [artifact.context.summary],
1576
+ stepIndex: 0
1577
+ }
1578
+ });
1579
+ }
1580
+ return scenes;
1581
+ }
1582
+ function createCodeHighlightScenes(snippets) {
1583
+ const scenes = [];
1584
+ const displaySnippets = snippets.slice(0, 6);
1585
+ const chunkedSnippets = chunkArray(displaySnippets, 2);
1586
+ chunkedSnippets.forEach((chunk, index) => {
1587
+ scenes.push({
1588
+ id: `code-highlight-${index + 1}`,
1589
+ type: "code-highlight",
1590
+ title: index === 0 ? "Code Changes" : `More Changes (${index + 1})`,
1591
+ content: chunk.map((s) => s.description || s.file).join(", "),
1592
+ durationInFrames: DURATIONS.code * FPS,
1593
+ props: {
1594
+ snippets: chunk.map((s) => ({
1595
+ file: s.file,
1596
+ code: s.code,
1597
+ language: s.language || detectLanguage(s.file),
1598
+ description: s.description,
1599
+ changeType: s.changeType,
1600
+ startLine: s.startLine
1601
+ }))
1602
+ }
1603
+ });
1604
+ });
1605
+ return scenes;
1606
+ }
1607
+ function detectLanguage(filename) {
1608
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
1609
+ const langMap = {
1610
+ ts: "typescript",
1611
+ tsx: "typescript",
1612
+ js: "javascript",
1613
+ jsx: "javascript",
1614
+ vue: "vue",
1615
+ py: "python",
1616
+ rb: "ruby",
1617
+ go: "go",
1618
+ rs: "rust",
1619
+ java: "java",
1620
+ css: "css",
1621
+ scss: "scss",
1622
+ html: "html",
1623
+ json: "json",
1624
+ md: "markdown",
1625
+ yaml: "yaml",
1626
+ yml: "yaml"
1627
+ };
1628
+ return langMap[ext] || "text";
1629
+ }
1630
+ function createCodeScene(artifact) {
1631
+ const components = artifact.context.affectedComponents;
1632
+ const displayComponents = components.slice(0, 8);
1633
+ return {
1634
+ id: "code",
1635
+ type: "code",
1636
+ title: "Files Changed",
1637
+ content: `${components.length} file${components.length !== 1 ? "s" : ""} affected`,
1638
+ durationInFrames: DURATIONS.code * FPS,
1639
+ props: {
1640
+ files: displayComponents,
1641
+ totalFiles: components.length,
1642
+ hasMore: components.length > 8
1643
+ }
1644
+ };
1645
+ }
1646
+ function createOutroScene(artifact) {
1647
+ const cta = artifact.guidelines[0] || "Learn more about this feature";
1648
+ return {
1649
+ id: "outro",
1650
+ type: "outro",
1651
+ title: "Summary",
1652
+ content: cta,
1653
+ durationInFrames: DURATIONS.outro * FPS,
1654
+ props: {
1655
+ guidelines: artifact.guidelines.slice(0, 3),
1656
+ source: artifact.source
1657
+ }
1311
1658
  };
1312
1659
  }
1660
+ function chunkArray(array, size) {
1661
+ const chunks = [];
1662
+ for (let i = 0; i < array.length; i += size) {
1663
+ chunks.push(array.slice(i, i + size));
1664
+ }
1665
+ return chunks;
1666
+ }
1667
+ function getVideoDurationSeconds(storyboard) {
1668
+ return storyboard.totalDurationInFrames / FPS;
1669
+ }
1670
+ function formatDuration(seconds) {
1671
+ const mins = Math.floor(seconds / 60);
1672
+ const secs = Math.floor(seconds % 60);
1673
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1674
+ }
1675
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
1676
+ async function generateVideo(input) {
1677
+ const startTime = Date.now();
1678
+ try {
1679
+ const { artifact, codeSnippets, config, theme = {}, verbose } = input;
1680
+ const mergedTheme = { ...DEFAULT_THEME, ...theme };
1681
+ const storyboard = generateStoryboard(artifact, mergedTheme, codeSnippets);
1682
+ if (verbose) {
1683
+ console.log(`
1684
+ Storyboard generated:`);
1685
+ console.log(` - Scenes: ${storyboard.scenes.length}`);
1686
+ console.log(` - Duration: ${formatDuration(getVideoDurationSeconds(storyboard))}`);
1687
+ }
1688
+ const outputDir = resolve(config.outputDir);
1689
+ if (!existsSync(outputDir)) {
1690
+ mkdirSync(outputDir, { recursive: true });
1691
+ }
1692
+ const storyboardPath = join(outputDir, "storyboard.json");
1693
+ writeFileSync(storyboardPath, JSON.stringify(storyboard, null, 2));
1694
+ if (verbose) {
1695
+ console.log(` - Storyboard saved: ${storyboardPath}`);
1696
+ }
1697
+ const propsPath = join(outputDir, "video-props.json");
1698
+ const videoProps = {
1699
+ storyboard,
1700
+ resolution: config.resolution || RESOLUTIONS["1080p"],
1701
+ fps: config.fps || 30
1702
+ };
1703
+ writeFileSync(propsPath, JSON.stringify(videoProps, null, 2));
1704
+ if (verbose) {
1705
+ console.log(` - Props saved: ${propsPath}`);
1706
+ }
1707
+ const filename = config.filename || "video";
1708
+ const format = config.format || "mp4";
1709
+ const videoPath = join(outputDir, `${filename}.${format}`);
1710
+ const renderResult = await tryRenderWithRemotion({
1711
+ storyboard,
1712
+ outputPath: videoPath,
1713
+ config,
1714
+ verbose
1715
+ });
1716
+ if (renderResult.success) {
1717
+ return {
1718
+ success: true,
1719
+ videoPath: renderResult.videoPath,
1720
+ posterPath: renderResult.posterPath,
1721
+ durationMs: Date.now() - startTime
1722
+ };
1723
+ }
1724
+ const durationMs = Date.now() - startTime;
1725
+ return {
1726
+ success: true,
1727
+ videoPath: propsPath,
1728
+ durationMs
1729
+ };
1730
+ } catch (error) {
1731
+ return {
1732
+ success: false,
1733
+ error: error instanceof Error ? error.message : String(error),
1734
+ durationMs: Date.now() - startTime
1735
+ };
1736
+ }
1737
+ }
1738
+ async function tryRenderWithRemotion(options) {
1739
+ const { storyboard, outputPath, config, verbose } = options;
1740
+ const remotionAvailable = await isRemotionAvailable();
1741
+ if (!remotionAvailable) {
1742
+ if (verbose) {
1743
+ console.log("\n \u26A0\uFE0F Remotion dependencies not installed.");
1744
+ console.log(" To enable automatic video rendering, install Remotion in the CLI:");
1745
+ console.log("");
1746
+ console.log(" cd $(npm root -g)/@makeitvisible/cli && npm install");
1747
+ console.log("");
1748
+ console.log(" Or install locally:");
1749
+ console.log(" npm install @remotion/bundler @remotion/renderer @remotion/cli remotion react react-dom");
1750
+ console.log("");
1751
+ console.log(" For now, you can render manually with the generated video-props.json");
1752
+ }
1753
+ return { success: false, error: "Remotion not available" };
1754
+ }
1755
+ try {
1756
+ const renderPath = pathToFileURL(join(__dirname$1, "remotion", "render.js")).href;
1757
+ const { renderVideo } = await import(
1758
+ /* @vite-ignore */
1759
+ renderPath
1760
+ );
1761
+ const resolution = config.resolution || RESOLUTIONS["1080p"];
1762
+ let resolutionKey = "1080p";
1763
+ if (resolution.width === RESOLUTIONS["720p"].width) {
1764
+ resolutionKey = "720p";
1765
+ } else if (resolution.width === RESOLUTIONS.square.width && resolution.height === RESOLUTIONS.square.height) {
1766
+ resolutionKey = "square";
1767
+ } else if (resolution.width === RESOLUTIONS.vertical.width && resolution.height === RESOLUTIONS.vertical.height) {
1768
+ resolutionKey = "vertical";
1769
+ }
1770
+ if (verbose) {
1771
+ console.log(`
1772
+ Rendering video with Remotion...`);
1773
+ console.log(` Resolution: ${resolution.width}x${resolution.height}`);
1774
+ }
1775
+ const result = await renderVideo({
1776
+ storyboard,
1777
+ outputPath,
1778
+ resolution: resolutionKey,
1779
+ codec: "h264",
1780
+ verbose,
1781
+ onProgress: (progress) => {
1782
+ if (verbose) {
1783
+ process.stdout.write(`\r Rendering: ${Math.round(progress * 100)}%`);
1784
+ }
1785
+ }
1786
+ });
1787
+ if (verbose && result.success) {
1788
+ console.log("\n");
1789
+ }
1790
+ return result;
1791
+ } catch (error) {
1792
+ if (verbose) {
1793
+ console.log(`
1794
+ Remotion render failed: ${error instanceof Error ? error.message : error}`);
1795
+ }
1796
+ return {
1797
+ success: false,
1798
+ error: error instanceof Error ? error.message : String(error)
1799
+ };
1800
+ }
1801
+ }
1802
+ async function isRemotionAvailable() {
1803
+ try {
1804
+ await import('@remotion/bundler');
1805
+ await import('@remotion/renderer');
1806
+ await import('remotion');
1807
+ return true;
1808
+ } catch (error) {
1809
+ if (process.env.DEBUG_REMOTION) {
1810
+ console.error("Remotion availability check failed:", error);
1811
+ }
1812
+ return false;
1813
+ }
1814
+ }
1313
1815
 
1314
1816
  // src/commands/analyze.ts
1315
1817
  var analyzeCommand = new Command("analyze").description("Analyze code changes and create artifacts").addCommand(createDiffCommand()).addCommand(createPRCommand()).addCommand(createPromptCommand());
1316
1818
  function createDiffCommand() {
1317
- return new Command("diff").description("Analyze git diff between commits").argument("[range]", "Git commit range (e.g., HEAD~1..HEAD)", "HEAD~1..HEAD").option("--no-post", "Skip posting artifact to API").action(async (range, options) => {
1819
+ return new Command("diff").description("Analyze git diff between commits").argument("[range]", "Git commit range (e.g., HEAD~1..HEAD)", "HEAD~1..HEAD").option("--no-post", "Skip posting artifact to API").option("--video [output]", "Generate video from artifact (optional: output directory)").option("--video-format <format>", "Video format: mp4 or webm", "mp4").option("--video-resolution <res>", "Resolution: 1080p, 720p, 480p, square, vertical", "1080p").action(async (range, options) => {
1318
1820
  const spinner = ora("Analyzing diff...").start();
1319
1821
  try {
1320
1822
  const result = await analyzeDiff(range);
@@ -1325,11 +1827,22 @@ function createDiffCommand() {
1325
1827
  console.log(chalk.cyan("Description:"), result.description);
1326
1828
  console.log(chalk.cyan("Files changed:"), result.context.affectedComponents.length);
1327
1829
  console.log(chalk.cyan("Breaking changes:"), result.context.breakingChanges ? "Yes" : "No");
1830
+ if (options.video !== void 0) {
1831
+ const snippets = (result.codeSnippets || []).map((s) => ({
1832
+ file: s.file,
1833
+ code: s.code,
1834
+ startLine: s.startLine,
1835
+ language: s.language,
1836
+ description: s.description,
1837
+ changeType: s.changeType
1838
+ }));
1839
+ await handleVideoGeneration(result, options, spinner, snippets);
1840
+ }
1328
1841
  if (options.post) {
1329
1842
  spinner.start("Posting artifact to Visible API...");
1330
1843
  const response = await postArtifact(result);
1331
1844
  spinner.succeed("Artifact posted successfully");
1332
- console.log(chalk.green("\n\u2713 Artifact created:"), response.url);
1845
+ console.log(chalk.green("\n\u2713 Artifact created:"), response.link);
1333
1846
  } else {
1334
1847
  console.log(chalk.yellow("\nSkipped posting to API (--no-post)"));
1335
1848
  console.log(chalk.dim("Artifact payload:"));
@@ -1343,7 +1856,7 @@ function createDiffCommand() {
1343
1856
  });
1344
1857
  }
1345
1858
  function createPRCommand() {
1346
- return new Command("pr").description("Analyze a pull request").option("--url <url>", "Pull request URL (e.g., https://github.com/owner/repo/pull/123)").option("--number <number>", "Pull request number (uses current repo)").option("--no-post", "Skip posting artifact to API").action(async (options) => {
1859
+ return new Command("pr").description("Analyze a pull request").option("--url <url>", "Pull request URL (e.g., https://github.com/owner/repo/pull/123)").option("--number <number>", "Pull request number (uses current repo)").option("--no-post", "Skip posting artifact to API").option("--video [output]", "Generate video from artifact (optional: output directory)").option("--video-format <format>", "Video format: mp4 or webm", "mp4").option("--video-resolution <res>", "Resolution: 1080p, 720p, 480p, square, vertical", "1080p").action(async (options) => {
1347
1860
  const spinner = ora("Analyzing pull request...").start();
1348
1861
  try {
1349
1862
  if (!options.url && !options.number) {
@@ -1362,11 +1875,22 @@ function createPRCommand() {
1362
1875
  console.log(chalk.cyan("Description:"), result.description);
1363
1876
  console.log(chalk.cyan("Files changed:"), result.context.affectedComponents.length);
1364
1877
  console.log(chalk.cyan("Breaking changes:"), result.context.breakingChanges ? "Yes" : "No");
1878
+ if (options.video !== void 0) {
1879
+ const snippets = (result.codeSnippets || []).map((s) => ({
1880
+ file: s.file,
1881
+ code: s.code,
1882
+ startLine: s.startLine,
1883
+ language: s.language,
1884
+ description: s.description,
1885
+ changeType: s.changeType
1886
+ }));
1887
+ await handleVideoGeneration(result, options, spinner, snippets);
1888
+ }
1365
1889
  if (options.post) {
1366
1890
  spinner.start("Posting artifact to Visible API...");
1367
1891
  const response = await postArtifact(result);
1368
1892
  spinner.succeed("Artifact posted successfully");
1369
- console.log(chalk.green("\n\u2713 Artifact created:"), response.url);
1893
+ console.log(chalk.green("\n\u2713 Artifact created:"), response.link);
1370
1894
  } else {
1371
1895
  console.log(chalk.yellow("\nSkipped posting to API (--no-post)"));
1372
1896
  console.log(chalk.dim("Artifact payload:"));
@@ -1380,10 +1904,11 @@ function createPRCommand() {
1380
1904
  });
1381
1905
  }
1382
1906
  function createPromptCommand() {
1383
- return new Command("prompt").description("Explore codebase with an AI agent").argument("<query>", "Natural language prompt describing what to analyze").option("--no-post", "Skip posting artifact to API").option("-v, --verbose", "Show detailed agent activity").action(async (query, options) => {
1384
- console.log(chalk.bold("\n\u{1F50D} The Detective is investigating...\n"));
1907
+ return new Command("prompt").description("Explore codebase with an AI agent").argument("<query>", "Natural language prompt describing what to analyze").option("--no-post", "Skip posting artifact to API").option("-v, --verbose", "Show detailed agent activity").option("--video [output]", "Generate video from artifact (optional: output directory)").option("--video-format <format>", "Video format: mp4 or webm", "mp4").option("--video-resolution <res>", "Resolution: 1080p, 720p, 480p, square, vertical", "1080p").action(async (query, options) => {
1908
+ console.log(chalk.bold("\n\u{1F50D} The Detective is investigating... (hot)\n"));
1385
1909
  console.log(chalk.dim(`Query: "${query}"
1386
1910
  `));
1911
+ const asArray = (value) => Array.isArray(value) ? value : [];
1387
1912
  let spinner = null;
1388
1913
  let toolCount = 0;
1389
1914
  try {
@@ -1451,53 +1976,63 @@ function createPromptCommand() {
1451
1976
  console.log(chalk.dim(` ${ep.description}`));
1452
1977
  }
1453
1978
  }
1454
- if (investigation.dataFlow && investigation.dataFlow.length > 0) {
1979
+ if (asArray(investigation.dataFlow).length > 0) {
1455
1980
  console.log(chalk.cyan("\nData Flow:"));
1456
- for (const step of investigation.dataFlow) {
1981
+ for (const step of asArray(investigation.dataFlow)) {
1457
1982
  console.log(chalk.dim(` ${step.step}. ${step.description}`));
1458
- if (step.files.length > 0) {
1459
- console.log(chalk.dim(` Files: ${step.files.join(", ")}`));
1983
+ const stepFiles = asArray(step.files);
1984
+ if (stepFiles.length > 0) {
1985
+ console.log(chalk.dim(` Files: ${stepFiles.join(", ")}`));
1460
1986
  }
1461
1987
  }
1462
1988
  }
1463
- if (investigation.keyFiles && investigation.keyFiles.length > 0) {
1989
+ if (asArray(investigation.keyFiles).length > 0) {
1464
1990
  console.log(chalk.cyan("\nKey Files:"));
1465
- for (const file of investigation.keyFiles) {
1991
+ for (const file of asArray(investigation.keyFiles)) {
1466
1992
  console.log(chalk.dim(` \u2022 ${file.path}`));
1467
1993
  console.log(chalk.dim(` Purpose: ${file.purpose}`));
1468
- if (file.keyExports.length > 0) {
1469
- console.log(chalk.dim(` Exports: ${file.keyExports.join(", ")}`));
1994
+ const keyExports = asArray(file.keyExports);
1995
+ if (keyExports.length > 0) {
1996
+ console.log(chalk.dim(` Exports: ${keyExports.join(", ")}`));
1470
1997
  }
1471
1998
  }
1472
1999
  }
1473
- if (investigation.dataStructures && investigation.dataStructures.length > 0) {
2000
+ if (asArray(investigation.dataStructures).length > 0) {
1474
2001
  console.log(chalk.cyan("\nData Structures:"));
1475
- for (const ds of investigation.dataStructures) {
2002
+ for (const ds of asArray(investigation.dataStructures)) {
1476
2003
  console.log(chalk.dim(` \u2022 ${ds.name} (${ds.file})`));
1477
2004
  console.log(chalk.dim(` ${ds.description}`));
1478
- if (ds.fields.length > 0) {
1479
- console.log(chalk.dim(` Fields: ${ds.fields.slice(0, 5).join(", ")}${ds.fields.length > 5 ? "..." : ""}`));
2005
+ const fields = asArray(ds.fields);
2006
+ if (fields.length > 0) {
2007
+ console.log(
2008
+ chalk.dim(
2009
+ ` Fields: ${fields.slice(0, 5).join(", ")}${fields.length > 5 ? "..." : ""}`
2010
+ )
2011
+ );
1480
2012
  }
1481
2013
  }
1482
2014
  }
1483
- if (investigation.usageExamples && investigation.usageExamples.length > 0) {
2015
+ if (asArray(investigation.usageExamples).length > 0) {
1484
2016
  console.log(chalk.cyan("\nUsage Examples:"));
1485
- for (const example of investigation.usageExamples) {
2017
+ for (const example of asArray(investigation.usageExamples)) {
1486
2018
  console.log(chalk.dim(` \u2022 ${example.description} (${example.file})`));
1487
2019
  }
1488
2020
  }
1489
- if (investigation.technicalNotes && investigation.technicalNotes.length > 0) {
2021
+ if (asArray(investigation.technicalNotes).length > 0) {
1490
2022
  console.log(chalk.cyan("\nTechnical Notes:"));
1491
- for (const note of investigation.technicalNotes) {
2023
+ for (const note of asArray(investigation.technicalNotes)) {
1492
2024
  console.log(chalk.dim(` \u2022 ${note}`));
1493
2025
  }
1494
2026
  }
1495
- const artifact = investigationToArtifact(query, investigation);
2027
+ const { artifact, codeSnippets } = investigationToArtifact(query, investigation);
2028
+ if (options.video !== void 0) {
2029
+ await handleVideoGeneration(artifact, options, null, codeSnippets);
2030
+ }
1496
2031
  if (options.post) {
1497
2032
  const postSpinner = ora("Posting artifact to Visible API...").start();
1498
2033
  const response = await postArtifact(artifact);
1499
2034
  postSpinner.succeed("Artifact posted successfully");
1500
- console.log(chalk.green("\n\u2713 Artifact created:"), response.url);
2035
+ console.log(chalk.green("\n\u2713 Artifact created:"), response.link);
1501
2036
  } else {
1502
2037
  console.log(chalk.yellow("\nSkipped posting to API (--no-post)"));
1503
2038
  console.log(chalk.dim("\nArtifact payload:"));
@@ -1511,6 +2046,7 @@ function createPromptCommand() {
1511
2046
  });
1512
2047
  }
1513
2048
  function investigationToArtifact(query, investigation) {
2049
+ const asArray = (value) => Array.isArray(value) ? value : [];
1514
2050
  const source = {
1515
2051
  type: "code-section",
1516
2052
  reference: query
@@ -1525,29 +2061,19 @@ function investigationToArtifact(query, investigation) {
1525
2061
  technicalDetails.push(...investigation.technicalNotes);
1526
2062
  }
1527
2063
  const affectedComponents = [];
1528
- if (investigation.keyFiles) {
1529
- affectedComponents.push(...investigation.keyFiles.map((f) => f.path));
1530
- }
1531
- if (investigation.entryPoints) {
1532
- affectedComponents.push(...investigation.entryPoints.map((e) => e.file));
1533
- }
1534
- if (investigation.dataStructures) {
1535
- affectedComponents.push(...investigation.dataStructures.map((d) => d.file));
1536
- }
1537
- if (investigation.usageExamples) {
1538
- affectedComponents.push(...investigation.usageExamples.map((e) => e.file));
1539
- }
2064
+ affectedComponents.push(...asArray(investigation.keyFiles).map((f) => f.path));
2065
+ affectedComponents.push(...asArray(investigation.entryPoints).map((e) => e.file));
2066
+ affectedComponents.push(...asArray(investigation.dataStructures).map((d) => d.file));
2067
+ affectedComponents.push(...asArray(investigation.usageExamples).map((e) => e.file));
1540
2068
  const uniqueComponents = [...new Set(affectedComponents)];
1541
2069
  const keywords = [];
1542
- if (investigation.dataStructures) {
1543
- keywords.push(...investigation.dataStructures.map((d) => d.name.toLowerCase()));
1544
- }
1545
- if (investigation.keyFiles) {
1546
- keywords.push(...investigation.keyFiles.flatMap((f) => f.keyExports.map((e) => e.toLowerCase())));
1547
- }
1548
- if (investigation.relatedFeatures) {
1549
- keywords.push(...investigation.relatedFeatures.map((f) => f.toLowerCase()));
1550
- }
2070
+ keywords.push(...asArray(investigation.dataStructures).map((d) => d.name.toLowerCase()));
2071
+ keywords.push(
2072
+ ...asArray(investigation.keyFiles).flatMap(
2073
+ (f) => asArray(f.keyExports).map((e) => e.toLowerCase())
2074
+ )
2075
+ );
2076
+ keywords.push(...asArray(investigation.relatedFeatures).map((f) => f.toLowerCase()));
1551
2077
  const uniqueKeywords = [...new Set(keywords)].slice(0, 20);
1552
2078
  const context = {
1553
2079
  summary: investigation.summary || "",
@@ -1559,25 +2085,113 @@ function investigationToArtifact(query, investigation) {
1559
2085
  const guidelines = [
1560
2086
  "Explain the feature from the user's perspective first."
1561
2087
  ];
1562
- if (investigation.entryPoints && investigation.entryPoints.length > 0) {
2088
+ if (asArray(investigation.entryPoints).length > 0) {
1563
2089
  guidelines.push("Show how users trigger/access this feature.");
1564
2090
  }
1565
- if (investigation.dataFlow && investigation.dataFlow.length > 0) {
2091
+ if (asArray(investigation.dataFlow).length > 0) {
1566
2092
  guidelines.push("Walk through the data flow step by step.");
1567
2093
  }
1568
- if (investigation.dataStructures && investigation.dataStructures.length > 0) {
2094
+ if (asArray(investigation.dataStructures).length > 0) {
1569
2095
  guidelines.push("Define the key data structures and their fields.");
1570
2096
  }
1571
- if (investigation.usageExamples && investigation.usageExamples.length > 0) {
2097
+ if (asArray(investigation.usageExamples).length > 0) {
1572
2098
  guidelines.push("Include code examples showing real usage.");
1573
2099
  }
1574
- return {
2100
+ const codeSnippets = [];
2101
+ for (const example of asArray(investigation.usageExamples)) {
2102
+ if (example.codeSnippet) {
2103
+ codeSnippets.push({
2104
+ file: example.file,
2105
+ code: example.codeSnippet,
2106
+ description: example.description,
2107
+ changeType: "context"
2108
+ });
2109
+ }
2110
+ }
2111
+ for (const ds of asArray(investigation.dataStructures)) {
2112
+ if (ds.fields && ds.fields.length > 0) {
2113
+ const typeCode = `interface ${ds.name} {
2114
+ ${ds.fields.slice(0, 8).join("\n ")}
2115
+ }`;
2116
+ codeSnippets.push({
2117
+ file: ds.file,
2118
+ code: typeCode,
2119
+ description: ds.description,
2120
+ changeType: "context"
2121
+ });
2122
+ }
2123
+ }
2124
+ const artifact = {
1575
2125
  title: investigation.title || `Feature: ${query}`,
1576
2126
  description: investigation.summary || `Investigation of: ${query}`,
1577
2127
  source,
1578
2128
  context,
1579
2129
  guidelines
1580
2130
  };
2131
+ return { artifact, codeSnippets };
2132
+ }
2133
+ async function handleVideoGeneration(artifact, options, existingSpinner, codeSnippets) {
2134
+ const spinner = existingSpinner || ora();
2135
+ spinner.start("Generating video...");
2136
+ const outputDir = typeof options.video === "string" ? options.video : process.cwd();
2137
+ const resolutionKey = options.videoResolution || "1080p";
2138
+ const resolution = RESOLUTIONS[resolutionKey] || RESOLUTIONS["1080p"];
2139
+ const format = options.videoFormat === "webm" ? "webm" : "mp4";
2140
+ const config = {
2141
+ outputDir,
2142
+ filename: sanitizeFilename(artifact.title),
2143
+ format,
2144
+ resolution,
2145
+ fps: 30,
2146
+ generatePoster: true
2147
+ };
2148
+ const storyboard = generateStoryboard(artifact, {}, codeSnippets);
2149
+ const duration = getVideoDurationSeconds(storyboard);
2150
+ const snippetCount = codeSnippets?.length || 0;
2151
+ spinner.text = `Generating video (${storyboard.scenes.length} scenes, ${snippetCount} code snippets, ${formatDuration(duration)})...`;
2152
+ const result = await generateVideo({
2153
+ artifact,
2154
+ codeSnippets,
2155
+ config,
2156
+ verbose: false
2157
+ });
2158
+ const isActualVideo = result.videoPath?.endsWith(".mp4") || result.videoPath?.endsWith(".webm");
2159
+ if (result.success && isActualVideo) {
2160
+ spinner.succeed("Video rendered successfully");
2161
+ console.log(chalk.bold("\n\u{1F3AC} Video Output:"));
2162
+ console.log(chalk.dim("\u2500".repeat(50)));
2163
+ console.log(chalk.cyan("Scenes:"), storyboard.scenes.length);
2164
+ console.log(chalk.cyan("Duration:"), formatDuration(duration));
2165
+ console.log(chalk.cyan("Resolution:"), `${resolution.width}x${resolution.height}`);
2166
+ console.log(chalk.cyan("Output:"), result.videoPath);
2167
+ if (result.posterPath) {
2168
+ console.log(chalk.cyan("Poster:"), result.posterPath);
2169
+ }
2170
+ console.log(chalk.dim(`
2171
+ Render time: ${result.durationMs}ms`));
2172
+ } else if (result.success) {
2173
+ spinner.warn("Storyboard saved (Remotion not installed for rendering)");
2174
+ console.log(chalk.bold("\n\u{1F4CB} Storyboard Output:"));
2175
+ console.log(chalk.dim("\u2500".repeat(50)));
2176
+ console.log(chalk.cyan("Scenes:"), storyboard.scenes.length);
2177
+ console.log(chalk.cyan("Duration:"), formatDuration(duration));
2178
+ console.log(chalk.cyan("Resolution:"), `${resolution.width}x${resolution.height}`);
2179
+ console.log(chalk.cyan("Props file:"), result.videoPath);
2180
+ console.log(chalk.yellow("\n\u26A0\uFE0F To render the actual video, install Remotion:"));
2181
+ console.log(chalk.dim(""));
2182
+ console.log(chalk.dim(" npm install @remotion/bundler @remotion/renderer @remotion/cli remotion react react-dom"));
2183
+ console.log(chalk.dim(""));
2184
+ console.log(chalk.yellow("Then run the command again, or render manually:"));
2185
+ console.log(chalk.dim(` # From the CLI package directory:`));
2186
+ console.log(chalk.dim(` cd $(npm root -g)/@makeitvisible/cli`));
2187
+ console.log(chalk.dim(` npx remotion render src/video/remotion/index.tsx Main ${outputDir}/video.mp4 --props=${result.videoPath}`));
2188
+ } else {
2189
+ spinner.fail("Video generation failed");
2190
+ console.log(chalk.red("\nError:"), result.error);
2191
+ }
2192
+ }
2193
+ function sanitizeFilename(name) {
2194
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
1581
2195
  }
1582
2196
 
1583
2197
  // src/bin/index.ts