@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/README.md +43 -0
- package/dist/bin/index.js +696 -82
- package/dist/bin/index.js.map +1 -1
- package/dist/index.d.ts +17 -62
- package/dist/index.js +143 -11
- package/dist/index.js.map +1 -1
- package/dist/types-BmNeVNFe.d.ts +62 -0
- package/dist/video/remotion/index.d.ts +2 -0
- package/dist/video/remotion/index.js +716 -0
- package/dist/video/remotion/index.js.map +1 -0
- package/dist/video/remotion/render.d.ts +111 -0
- package/dist/video/remotion/render.js +110 -0
- package/dist/video/remotion/render.js.map +1 -0
- package/dist/video/renderer.d.ts +154 -0
- package/dist/video/renderer.js +367 -0
- package/dist/video/renderer.js.map +1 -0
- package/package.json +15 -4
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 {
|
|
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-
|
|
1270
|
+
model: "gpt-5.2",
|
|
1164
1271
|
messages: this.messages,
|
|
1165
|
-
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1277
|
-
Authorization: `Bearer ${apiKey}`
|
|
1427
|
+
...headers
|
|
1278
1428
|
},
|
|
1279
1429
|
body: JSON.stringify({
|
|
1280
1430
|
title: payload.title,
|
|
1281
|
-
|
|
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.
|
|
1306
|
-
throw new Error("Invalid API response: missing
|
|
1452
|
+
if (!data.link) {
|
|
1453
|
+
throw new Error("Invalid API response: missing link");
|
|
1307
1454
|
}
|
|
1308
1455
|
return {
|
|
1309
|
-
|
|
1310
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1459
|
-
|
|
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
|
|
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
|
-
|
|
1469
|
-
|
|
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
|
|
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
|
-
|
|
1479
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
|
2088
|
+
if (asArray(investigation.entryPoints).length > 0) {
|
|
1563
2089
|
guidelines.push("Show how users trigger/access this feature.");
|
|
1564
2090
|
}
|
|
1565
|
-
if (investigation.dataFlow
|
|
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
|
|
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
|
|
2097
|
+
if (asArray(investigation.usageExamples).length > 0) {
|
|
1572
2098
|
guidelines.push("Include code examples showing real usage.");
|
|
1573
2099
|
}
|
|
1574
|
-
|
|
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
|