repotrailer 0.1.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/.codex-plugin/plugin.json +22 -0
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/action.yml +38 -0
- package/assets/cover.svg +16 -0
- package/assets/demo-preview.png +0 -0
- package/bin/repotrailer.js +9 -0
- package/llms.txt +54 -0
- package/package.json +61 -0
- package/scripts/growth-check.mjs +337 -0
- package/scripts/publish-readiness.mjs +104 -0
- package/skills/repotrailer/SKILL.md +52 -0
- package/src/analyze.js +405 -0
- package/src/cli.js +156 -0
- package/src/hyperframes.js +588 -0
- package/src/index.js +7 -0
- package/src/launch-kit.js +217 -0
- package/src/storyboard.js +71 -0
- package/src/utils.js +73 -0
package/src/analyze.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { mkdtemp, readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { run, stripMarkdown, truncate } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
const SKIP_DIRS = new Set([
|
|
9
|
+
".git",
|
|
10
|
+
".next",
|
|
11
|
+
".turbo",
|
|
12
|
+
".venv",
|
|
13
|
+
"build",
|
|
14
|
+
"coverage",
|
|
15
|
+
"dist",
|
|
16
|
+
"node_modules",
|
|
17
|
+
"target",
|
|
18
|
+
"vendor",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const LANGUAGE_BY_EXTENSION = new Map([
|
|
22
|
+
[".c", "C"],
|
|
23
|
+
[".cc", "C++"],
|
|
24
|
+
[".cpp", "C++"],
|
|
25
|
+
[".cs", "C#"],
|
|
26
|
+
[".css", "CSS"],
|
|
27
|
+
[".dart", "Dart"],
|
|
28
|
+
[".ex", "Elixir"],
|
|
29
|
+
[".exs", "Elixir"],
|
|
30
|
+
[".go", "Go"],
|
|
31
|
+
[".html", "HTML"],
|
|
32
|
+
[".java", "Java"],
|
|
33
|
+
[".js", "JavaScript"],
|
|
34
|
+
[".jsx", "JavaScript"],
|
|
35
|
+
[".kt", "Kotlin"],
|
|
36
|
+
[".lua", "Lua"],
|
|
37
|
+
[".m", "Objective-C"],
|
|
38
|
+
[".php", "PHP"],
|
|
39
|
+
[".py", "Python"],
|
|
40
|
+
[".rb", "Ruby"],
|
|
41
|
+
[".rs", "Rust"],
|
|
42
|
+
[".scala", "Scala"],
|
|
43
|
+
[".sh", "Shell"],
|
|
44
|
+
[".sol", "Solidity"],
|
|
45
|
+
[".swift", "Swift"],
|
|
46
|
+
[".ts", "TypeScript"],
|
|
47
|
+
[".tsx", "TypeScript"],
|
|
48
|
+
[".vue", "Vue"],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const STACK_SIGNALS = [
|
|
52
|
+
["next", "Next.js"],
|
|
53
|
+
["react", "React"],
|
|
54
|
+
["vue", "Vue"],
|
|
55
|
+
["svelte", "Svelte"],
|
|
56
|
+
["astro", "Astro"],
|
|
57
|
+
["vite", "Vite"],
|
|
58
|
+
["express", "Express"],
|
|
59
|
+
["fastify", "Fastify"],
|
|
60
|
+
["tailwindcss", "Tailwind CSS"],
|
|
61
|
+
["remotion", "Remotion"],
|
|
62
|
+
["hyperframes", "HyperFrames"],
|
|
63
|
+
["playwright", "Playwright"],
|
|
64
|
+
["vitest", "Vitest"],
|
|
65
|
+
["jest", "Jest"],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
async function exists(target) {
|
|
69
|
+
try {
|
|
70
|
+
await stat(target);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeGithubSource(source) {
|
|
78
|
+
if (
|
|
79
|
+
!source.startsWith(".")
|
|
80
|
+
&& !source.startsWith("/")
|
|
81
|
+
&& /^[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/.test(source)
|
|
82
|
+
) {
|
|
83
|
+
return `https://github.com/${source}.git`;
|
|
84
|
+
}
|
|
85
|
+
if (/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/?$/.test(source)) {
|
|
86
|
+
return `${source.replace(/\/$/, "")}.git`;
|
|
87
|
+
}
|
|
88
|
+
if (/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\.git$/.test(source)) {
|
|
89
|
+
return source;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function resolveRepository(source) {
|
|
95
|
+
const localPath = path.resolve(source);
|
|
96
|
+
if (await exists(localPath)) {
|
|
97
|
+
return {
|
|
98
|
+
root: localPath,
|
|
99
|
+
source,
|
|
100
|
+
temporary: false,
|
|
101
|
+
nameHint: path.basename(localPath),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const githubUrl = normalizeGithubSource(source);
|
|
106
|
+
if (!githubUrl) {
|
|
107
|
+
return {
|
|
108
|
+
root: localPath,
|
|
109
|
+
source,
|
|
110
|
+
temporary: false,
|
|
111
|
+
nameHint: path.basename(localPath),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const root = await mkdtemp(path.join(tmpdir(), "repotrailer-"));
|
|
116
|
+
const repositoryName = githubUrl
|
|
117
|
+
.replace(/\.git$/, "")
|
|
118
|
+
.split("/")
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.at(-1);
|
|
121
|
+
const clone = await run(
|
|
122
|
+
"git",
|
|
123
|
+
["clone", "--depth", "50", "--quiet", githubUrl, root],
|
|
124
|
+
{ timeout: 120_000 },
|
|
125
|
+
);
|
|
126
|
+
if (!clone.ok) {
|
|
127
|
+
throw new Error(`could not clone ${source}: ${clone.stderr}`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
root,
|
|
131
|
+
source: githubUrl.replace(/\.git$/, ""),
|
|
132
|
+
temporary: true,
|
|
133
|
+
nameHint: repositoryName || path.basename(root),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function findReadme(root) {
|
|
138
|
+
const entries = await readdir(root);
|
|
139
|
+
const candidate = entries.find((entry) => /^readme(?:\.[^.]+)?$/i.test(entry));
|
|
140
|
+
if (!candidate) {
|
|
141
|
+
return { path: null, content: "" };
|
|
142
|
+
}
|
|
143
|
+
const readmePath = path.join(root, candidate);
|
|
144
|
+
return {
|
|
145
|
+
path: candidate,
|
|
146
|
+
content: await readFile(readmePath, "utf8"),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readmeMetadata(markdown, fallbackName) {
|
|
151
|
+
const proseMarkdown = markdown.replace(/```[\s\S]*?```/g, "");
|
|
152
|
+
const introLines = proseMarkdown.split(/\r?\n/).slice(0, 40);
|
|
153
|
+
const cleanHeading = (value) => {
|
|
154
|
+
if (!value) return null;
|
|
155
|
+
const withoutImages = value.replace(/<img\b[^>]*>/gi, "");
|
|
156
|
+
const text = stripMarkdown(withoutImages.replace(/<[^>]+>/g, " ")).trim();
|
|
157
|
+
return text.length >= 2 ? text : null;
|
|
158
|
+
};
|
|
159
|
+
const heading = introLines
|
|
160
|
+
.map((line) => (
|
|
161
|
+
cleanHeading(line.match(/^#\s+(.+)$/)?.[1])
|
|
162
|
+
))
|
|
163
|
+
.find(Boolean)
|
|
164
|
+
|| cleanHeading(proseMarkdown.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]);
|
|
165
|
+
|
|
166
|
+
const paragraphs = proseMarkdown
|
|
167
|
+
.split(/\n\s*\n/)
|
|
168
|
+
.map(stripMarkdown)
|
|
169
|
+
.filter((value) => value.length >= 24)
|
|
170
|
+
.filter((value) => !/^(build|install|usage|features|license)\b/i.test(value));
|
|
171
|
+
|
|
172
|
+
const bullets = proseMarkdown.split(/\r?\n/)
|
|
173
|
+
.map((line) => line.match(/^\s*[-*]\s+(.+)$/)?.[1])
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.map(stripMarkdown)
|
|
176
|
+
.filter((value) => value.length >= 8 && value.length <= 140);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
title: truncate(heading ? stripMarkdown(heading) : fallbackName, 64),
|
|
180
|
+
description: truncate(
|
|
181
|
+
paragraphs[0] || `A project called ${fallbackName}.`,
|
|
182
|
+
180,
|
|
183
|
+
),
|
|
184
|
+
features: [...new Set(bullets)].slice(0, 4),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function walkFiles(root, current = root, result = []) {
|
|
189
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (SKIP_DIRS.has(entry.name)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const fullPath = path.join(current, entry.name);
|
|
198
|
+
if (entry.isDirectory()) {
|
|
199
|
+
await walkFiles(root, fullPath, result);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (entry.isFile()) {
|
|
203
|
+
const info = await stat(fullPath);
|
|
204
|
+
result.push({
|
|
205
|
+
path: path.relative(root, fullPath).split(path.sep).join("/"),
|
|
206
|
+
bytes: info.size,
|
|
207
|
+
extension: path.extname(entry.name).toLowerCase(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function summarizeLanguages(files) {
|
|
215
|
+
const totals = new Map();
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
const language = LANGUAGE_BY_EXTENSION.get(file.extension);
|
|
218
|
+
if (!language) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
totals.set(language, (totals.get(language) || 0) + file.bytes);
|
|
222
|
+
}
|
|
223
|
+
const sum = [...totals.values()].reduce((total, value) => total + value, 0);
|
|
224
|
+
return [...totals.entries()]
|
|
225
|
+
.sort((a, b) => b[1] - a[1])
|
|
226
|
+
.slice(0, 5)
|
|
227
|
+
.map(([name, bytes]) => ({
|
|
228
|
+
name,
|
|
229
|
+
percent: sum === 0 ? 0 : Math.max(1, Math.round((bytes / sum) * 100)),
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function packageMetadata(root) {
|
|
234
|
+
const packagePath = path.join(root, "package.json");
|
|
235
|
+
if (!(await exists(packagePath))) {
|
|
236
|
+
return { package: null, stack: [], install: null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const pkg = JSON.parse(await readFile(packagePath, "utf8"));
|
|
241
|
+
const dependencies = {
|
|
242
|
+
...pkg.dependencies,
|
|
243
|
+
...pkg.devDependencies,
|
|
244
|
+
...pkg.peerDependencies,
|
|
245
|
+
};
|
|
246
|
+
const stack = STACK_SIGNALS
|
|
247
|
+
.filter(([dependency]) => dependency in dependencies)
|
|
248
|
+
.map(([, label]) => label)
|
|
249
|
+
.slice(0, 6);
|
|
250
|
+
const install = (await exists(path.join(root, "pnpm-lock.yaml")))
|
|
251
|
+
? "pnpm install"
|
|
252
|
+
: (await exists(path.join(root, "yarn.lock")))
|
|
253
|
+
? "yarn"
|
|
254
|
+
: (await exists(path.join(root, "bun.lock")))
|
|
255
|
+
? "bun install"
|
|
256
|
+
: "npm install";
|
|
257
|
+
|
|
258
|
+
return { package: pkg, stack, install };
|
|
259
|
+
} catch {
|
|
260
|
+
return { package: null, stack: [], install: null };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function detectStack(root, files, packageInfo) {
|
|
265
|
+
const stack = [...packageInfo.stack];
|
|
266
|
+
const names = new Set(files.map((file) => file.path.toLowerCase()));
|
|
267
|
+
const add = (condition, value) => {
|
|
268
|
+
if (condition && !stack.includes(value)) {
|
|
269
|
+
stack.push(value);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
add(names.has("pyproject.toml") || names.has("requirements.txt"), "Python");
|
|
274
|
+
add(names.has("cargo.toml"), "Rust");
|
|
275
|
+
add(names.has("go.mod"), "Go");
|
|
276
|
+
add(names.has("dockerfile"), "Docker");
|
|
277
|
+
add(names.has("compose.yml") || names.has("docker-compose.yml"), "Compose");
|
|
278
|
+
add(names.has(".github/workflows/ci.yml"), "GitHub Actions");
|
|
279
|
+
|
|
280
|
+
if (stack.length === 0) {
|
|
281
|
+
stack.push(...summarizeLanguages(files).map((item) => item.name));
|
|
282
|
+
}
|
|
283
|
+
return stack.slice(0, 6);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function gitMetadata(root) {
|
|
287
|
+
const branch = await run("git", ["branch", "--show-current"], { cwd: root });
|
|
288
|
+
if (!branch.ok) {
|
|
289
|
+
return {
|
|
290
|
+
isGit: false,
|
|
291
|
+
branch: null,
|
|
292
|
+
commits: 0,
|
|
293
|
+
contributors: 0,
|
|
294
|
+
latest: [],
|
|
295
|
+
remote: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const [commitCount, contributors, latest, remote] = await Promise.all([
|
|
300
|
+
run("git", ["rev-list", "--count", "HEAD"], { cwd: root }),
|
|
301
|
+
run("git", ["shortlog", "-sne", "HEAD"], { cwd: root }),
|
|
302
|
+
run("git", ["log", "-5", "--pretty=format:%h%x09%s"], { cwd: root }),
|
|
303
|
+
run("git", ["remote", "get-url", "origin"], { cwd: root }),
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
isGit: true,
|
|
308
|
+
branch: branch.stdout || "main",
|
|
309
|
+
commits: Number.parseInt(commitCount.stdout, 10) || 0,
|
|
310
|
+
contributors: contributors.stdout
|
|
311
|
+
? contributors.stdout.split(/\r?\n/).filter(Boolean).length
|
|
312
|
+
: 0,
|
|
313
|
+
latest: latest.stdout
|
|
314
|
+
? latest.stdout.split(/\r?\n/).map((line) => {
|
|
315
|
+
const [hash, ...subject] = line.split("\t");
|
|
316
|
+
return { hash, subject: subject.join("\t") };
|
|
317
|
+
})
|
|
318
|
+
: [],
|
|
319
|
+
remote: remote.ok ? remote.stdout.replace(/\.git$/, "") : null,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function suggestedCommand(root, packageInfo, files) {
|
|
324
|
+
const names = new Set(files.map((file) => file.path.toLowerCase()));
|
|
325
|
+
if (packageInfo.package?.bin) {
|
|
326
|
+
const bin = typeof packageInfo.package.bin === "string"
|
|
327
|
+
? packageInfo.package.name
|
|
328
|
+
: Object.keys(packageInfo.package.bin)[0];
|
|
329
|
+
if (bin) {
|
|
330
|
+
return `npx ${bin}`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (packageInfo.package) {
|
|
334
|
+
return `${packageInfo.install || "npm install"} && npm run dev`;
|
|
335
|
+
}
|
|
336
|
+
if (names.has("pyproject.toml")) {
|
|
337
|
+
return "pip install -e .";
|
|
338
|
+
}
|
|
339
|
+
if (names.has("cargo.toml")) {
|
|
340
|
+
return "cargo run";
|
|
341
|
+
}
|
|
342
|
+
if (names.has("go.mod")) {
|
|
343
|
+
return "go run .";
|
|
344
|
+
}
|
|
345
|
+
return `git clone ${path.basename(root)}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function analyzeRepository(source = ".", overrides = {}) {
|
|
349
|
+
const repository = await resolveRepository(source);
|
|
350
|
+
if (!(await exists(repository.root))) {
|
|
351
|
+
throw new Error(`repository path does not exist: ${repository.root}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const fallbackName = repository.nameHint || path.basename(repository.root);
|
|
355
|
+
const [readme, files, git, packageInfo] = await Promise.all([
|
|
356
|
+
findReadme(repository.root),
|
|
357
|
+
walkFiles(repository.root),
|
|
358
|
+
gitMetadata(repository.root),
|
|
359
|
+
packageMetadata(repository.root),
|
|
360
|
+
]);
|
|
361
|
+
const readmeInfo = readmeMetadata(readme.content, fallbackName);
|
|
362
|
+
const languages = summarizeLanguages(files);
|
|
363
|
+
const stack = await detectStack(repository.root, files, packageInfo);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
schemaVersion: 1,
|
|
367
|
+
generatedAt: new Date().toISOString(),
|
|
368
|
+
source: repository.source,
|
|
369
|
+
root: repository.root,
|
|
370
|
+
temporary: repository.temporary,
|
|
371
|
+
name: overrides.title || packageInfo.package?.name || readmeInfo.title,
|
|
372
|
+
title: overrides.title || readmeInfo.title,
|
|
373
|
+
tagline: overrides.tagline || packageInfo.package?.description || readmeInfo.description,
|
|
374
|
+
description: readmeInfo.description,
|
|
375
|
+
features: readmeInfo.features.length
|
|
376
|
+
? readmeInfo.features
|
|
377
|
+
: [
|
|
378
|
+
"Built from real repository metadata",
|
|
379
|
+
"Ready to share in minutes",
|
|
380
|
+
"Runs locally with no API key",
|
|
381
|
+
],
|
|
382
|
+
stack,
|
|
383
|
+
languages,
|
|
384
|
+
installCommand: overrides.install || suggestedCommand(
|
|
385
|
+
repository.root,
|
|
386
|
+
packageInfo,
|
|
387
|
+
files,
|
|
388
|
+
),
|
|
389
|
+
files: {
|
|
390
|
+
total: files.length,
|
|
391
|
+
source: files.filter((file) => LANGUAGE_BY_EXTENSION.has(file.extension)).length,
|
|
392
|
+
},
|
|
393
|
+
readme: {
|
|
394
|
+
path: readme.path,
|
|
395
|
+
characters: readme.content.length,
|
|
396
|
+
},
|
|
397
|
+
git,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const __test = {
|
|
402
|
+
normalizeGithubSource,
|
|
403
|
+
readmeMetadata,
|
|
404
|
+
summarizeLanguages,
|
|
405
|
+
};
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { analyzeRepository } from "./analyze.js";
|
|
4
|
+
import { renderHyperframesProject } from "./hyperframes.js";
|
|
5
|
+
import { writeLaunchKit } from "./launch-kit.js";
|
|
6
|
+
import { buildStoryboard } from "./storyboard.js";
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
RepoTrailer
|
|
10
|
+
Turn a GitHub repository into a launch trailer and share kit.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
repotrailer [path | owner/repo | GitHub URL] [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-o, --out <directory> Output directory (default: ./repotrailer-out)
|
|
17
|
+
--title <text> Override the detected project title
|
|
18
|
+
--tagline <text> Override the detected tagline
|
|
19
|
+
--install <command> Override the detected install command
|
|
20
|
+
--accent <hex> Accent color for generated assets
|
|
21
|
+
--quality <level> Video quality: draft, standard, high
|
|
22
|
+
--workers <number> Render workers (default: 1)
|
|
23
|
+
--no-video Generate preview assets without rendering MP4
|
|
24
|
+
--json Print the manifest JSON path only
|
|
25
|
+
-h, --help Show help
|
|
26
|
+
-v, --version Show version
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
repotrailer .
|
|
30
|
+
repotrailer openai/openai-agents-js
|
|
31
|
+
repotrailer https://github.com/owner/repo --out ./launch
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const options = {
|
|
36
|
+
source: ".",
|
|
37
|
+
output: "repotrailer-out",
|
|
38
|
+
video: true,
|
|
39
|
+
json: false,
|
|
40
|
+
overrides: {},
|
|
41
|
+
palette: {},
|
|
42
|
+
quality: "standard",
|
|
43
|
+
workers: 1,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
47
|
+
const argument = argv[index];
|
|
48
|
+
if (!argument.startsWith("-")) {
|
|
49
|
+
options.source = argument;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (argument === "--no-video") {
|
|
53
|
+
options.video = false;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (argument === "--json") {
|
|
57
|
+
options.json = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (argument === "-h" || argument === "--help") {
|
|
61
|
+
options.help = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (argument === "-v" || argument === "--version") {
|
|
65
|
+
options.version = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const value = argv[index + 1];
|
|
70
|
+
if (!value || value.startsWith("-")) {
|
|
71
|
+
throw new Error(`missing value for ${argument}`);
|
|
72
|
+
}
|
|
73
|
+
index += 1;
|
|
74
|
+
if (argument === "-o" || argument === "--out") {
|
|
75
|
+
options.output = value;
|
|
76
|
+
} else if (argument === "--title") {
|
|
77
|
+
options.overrides.title = value;
|
|
78
|
+
} else if (argument === "--tagline") {
|
|
79
|
+
options.overrides.tagline = value;
|
|
80
|
+
} else if (argument === "--install") {
|
|
81
|
+
options.overrides.install = value;
|
|
82
|
+
} else if (argument === "--accent") {
|
|
83
|
+
options.palette.accent = value;
|
|
84
|
+
} else if (argument === "--quality") {
|
|
85
|
+
if (!["draft", "standard", "high"].includes(value)) {
|
|
86
|
+
throw new Error("--quality must be draft, standard, or high");
|
|
87
|
+
}
|
|
88
|
+
options.quality = value;
|
|
89
|
+
} else if (argument === "--workers") {
|
|
90
|
+
options.workers = Number.parseInt(value, 10);
|
|
91
|
+
if (!Number.isInteger(options.workers) || options.workers < 1 || options.workers > 8) {
|
|
92
|
+
throw new Error("--workers must be an integer from 1 to 8");
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error(`unknown option: ${argument}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function version() {
|
|
103
|
+
const packageJson = new URL("../package.json", import.meta.url);
|
|
104
|
+
const data = await import(packageJson, { with: { type: "json" } });
|
|
105
|
+
return data.default.version;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function runCli(argv) {
|
|
109
|
+
const options = parseArgs(argv);
|
|
110
|
+
if (options.help) {
|
|
111
|
+
console.log(HELP.trim());
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (options.version) {
|
|
115
|
+
console.log(await version());
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const repo = await analyzeRepository(options.source, options.overrides);
|
|
120
|
+
const scenes = buildStoryboard(repo);
|
|
121
|
+
const kit = await writeLaunchKit(
|
|
122
|
+
repo,
|
|
123
|
+
scenes,
|
|
124
|
+
path.resolve(options.output),
|
|
125
|
+
{ palette: options.palette },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (options.video) {
|
|
129
|
+
await renderHyperframesProject(
|
|
130
|
+
kit.hyperframes.project,
|
|
131
|
+
kit.files.trailer,
|
|
132
|
+
{
|
|
133
|
+
quality: options.quality,
|
|
134
|
+
workers: options.workers,
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.json) {
|
|
140
|
+
console.log(kit.files.manifest);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`\nRepoTrailer built ${repo.title}`);
|
|
145
|
+
console.log(` Preview ${kit.files.preview}`);
|
|
146
|
+
console.log(` Social card ${kit.files.socialCard}`);
|
|
147
|
+
console.log(` Launch copy ${kit.files.launchCopy}`);
|
|
148
|
+
console.log(` Manifest ${kit.files.manifest}\n`);
|
|
149
|
+
console.log(` HyperFrames ${kit.hyperframes.project}`);
|
|
150
|
+
if (options.video) {
|
|
151
|
+
console.log(` Trailer ${kit.files.trailer}`);
|
|
152
|
+
}
|
|
153
|
+
console.log("");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const __test = { parseArgs };
|