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.
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+
4
+ const args = new Map();
5
+ for (let index = 2; index < process.argv.length; index += 1) {
6
+ const arg = process.argv[index];
7
+ if (arg.startsWith("--")) {
8
+ const [key, inlineValue] = arg.split("=", 2);
9
+ const value = inlineValue ?? process.argv[index + 1];
10
+ args.set(key, value);
11
+ if (inlineValue === undefined) index += 1;
12
+ }
13
+ }
14
+
15
+ const repo = args.get("--repo") ?? "howong217-ui/repotrailer";
16
+ const goal = Number(args.get("--goal") ?? 100);
17
+ const launchDate = new Date(args.get("--launch-date") ?? "2026-06-24T15:28:23Z");
18
+ const launchDays = Number(args.get("--days") ?? 7);
19
+ const outputJson = process.argv.includes("--json");
20
+
21
+ function run(command, commandArgs) {
22
+ try {
23
+ return execFileSync(command, commandArgs, {
24
+ encoding: "utf8",
25
+ stdio: ["ignore", "pipe", "pipe"],
26
+ }).trim();
27
+ } catch {
28
+ return "";
29
+ }
30
+ }
31
+
32
+ function runJson(command, commandArgs) {
33
+ const output = run(command, commandArgs);
34
+ if (!output) return null;
35
+ try {
36
+ return JSON.parse(output);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function parseRepo(input) {
43
+ const [owner, name] = input.split("/");
44
+ if (!owner || !name) {
45
+ throw new Error(`Expected owner/name repository, received: ${input}`);
46
+ }
47
+ return { owner, name };
48
+ }
49
+
50
+ async function fetchJson(path) {
51
+ const response = await fetch(`https://api.github.com/${path}`, {
52
+ headers: { "User-Agent": "repotrailer-growth-check" },
53
+ });
54
+ if (response.status === 404) return null;
55
+ if (!response.ok) {
56
+ throw new Error(`GitHub API request failed: ${response.status} ${path}`);
57
+ }
58
+ return response.json();
59
+ }
60
+
61
+ async function fetchLatestRelease(repository) {
62
+ const release = await fetchJson(`repos/${repository}/releases/latest`);
63
+ if (!release) return null;
64
+ return {
65
+ name: release.name,
66
+ tagName: release.tag_name,
67
+ url: release.html_url,
68
+ body: release.body ?? "",
69
+ };
70
+ }
71
+
72
+ async function getRepoMetrics() {
73
+ const ghMetrics = runJson("gh", [
74
+ "repo",
75
+ "view",
76
+ repo,
77
+ "--json",
78
+ "stargazerCount,forkCount,watchers,issues,latestRelease,url,pushedAt,defaultBranchRef,homepageUrl,repositoryTopics",
79
+ ]);
80
+ if (ghMetrics) {
81
+ if (!ghMetrics.latestRelease) {
82
+ ghMetrics.latestRelease = await fetchLatestRelease(repo).catch(() => null);
83
+ }
84
+ return ghMetrics;
85
+ }
86
+
87
+ const data = await fetchJson(`repos/${repo}`);
88
+ if (!data) throw new Error(`Repository not found: ${repo}`);
89
+ return {
90
+ stargazerCount: data.stargazers_count,
91
+ forkCount: data.forks_count,
92
+ watchers: { totalCount: data.subscribers_count ?? data.watchers_count },
93
+ issues: { totalCount: data.open_issues_count },
94
+ latestRelease: await fetchLatestRelease(repo).catch(() => null),
95
+ url: data.html_url,
96
+ homepageUrl: data.homepage,
97
+ repositoryTopics: data.topics ?? [],
98
+ pushedAt: data.pushed_at,
99
+ defaultBranchRef: { name: data.default_branch },
100
+ };
101
+ }
102
+
103
+ async function getPagesQuality(homepageUrl) {
104
+ const pages = runJson("gh", ["api", `repos/${repo}/pages`])
105
+ ?? await fetchJson(`repos/${repo}/pages`).catch(() => null);
106
+ const url = pages?.html_url ?? homepageUrl ?? null;
107
+ if (!url) return null;
108
+
109
+ const result = {
110
+ url,
111
+ githubStatus: pages?.status ?? null,
112
+ source: pages?.source ?? null,
113
+ homepageUrl: homepageUrl ?? null,
114
+ httpStatus: null,
115
+ hasStarCallToAction: false,
116
+ status: "missing",
117
+ };
118
+
119
+ for (let attempt = 0; attempt < 3; attempt += 1) {
120
+ const output = run("curl", ["-L", "-s", "-w", "\n%{http_code}", url]);
121
+ const splitAt = output.lastIndexOf("\n");
122
+ if (splitAt === -1) continue;
123
+
124
+ const html = output.slice(0, splitAt);
125
+ result.httpStatus = Number.parseInt(output.slice(splitAt + 1), 10) || null;
126
+ result.hasStarCallToAction =
127
+ /Star on GitHub|github\.com\/howong217-ui\/repotrailer/i.test(html);
128
+ if (result.httpStatus === 200 && result.hasStarCallToAction) {
129
+ break;
130
+ }
131
+ }
132
+ result.status = result.httpStatus === 200 && result.hasStarCallToAction
133
+ ? "ready"
134
+ : "needs-work";
135
+ return result;
136
+ }
137
+
138
+ async function getReleaseQuality(latestRelease) {
139
+ if (!latestRelease?.tagName) return null;
140
+ let details = null;
141
+ for (let attempt = 0; attempt < 3; attempt += 1) {
142
+ details = runJson("gh", [
143
+ "release",
144
+ "view",
145
+ latestRelease.tagName,
146
+ "--repo",
147
+ repo,
148
+ "--json",
149
+ "body",
150
+ ]);
151
+ if (details?.body) break;
152
+ }
153
+ const release = details?.body
154
+ ? details
155
+ : await fetchJson(`repos/${repo}/releases/tags/${latestRelease.tagName}`)
156
+ .catch(() => null);
157
+ const body = release?.body ?? latestRelease.body ?? "";
158
+ const bodyCharacters = body.length;
159
+ const hasInstallCommand = /npx|npm|pnpm|yarn|bun/i.test(body);
160
+ const hasExamplesLink = /examples|demo|gallery/i.test(body);
161
+ return {
162
+ bodyCharacters,
163
+ hasInstallCommand,
164
+ hasExamplesLink,
165
+ status: bodyCharacters >= 500 && hasInstallCommand && hasExamplesLink
166
+ ? "ready"
167
+ : "thin",
168
+ };
169
+ }
170
+
171
+ function getLocalState(defaultBranch) {
172
+ if (run("git", ["rev-parse", "--is-inside-work-tree"]) !== "true") {
173
+ return null;
174
+ }
175
+ const branch = run("git", ["branch", "--show-current"]) || null;
176
+ const head = run("git", ["rev-parse", "--short", "HEAD"]) || null;
177
+ const remoteRef = `origin/${defaultBranch || "main"}`;
178
+ const ahead = Number.parseInt(
179
+ run("git", ["rev-list", "--count", `${remoteRef}..HEAD`]),
180
+ 10,
181
+ ) || 0;
182
+ const behind = Number.parseInt(
183
+ run("git", ["rev-list", "--count", `HEAD..${remoteRef}`]),
184
+ 10,
185
+ ) || 0;
186
+ return {
187
+ branch,
188
+ head,
189
+ remoteRef,
190
+ ahead,
191
+ behind,
192
+ hasUncommittedChanges: run("git", ["status", "--short"]).length > 0,
193
+ };
194
+ }
195
+
196
+ async function getRuns() {
197
+ const ghRuns = runJson("gh", [
198
+ "run",
199
+ "list",
200
+ "--repo",
201
+ repo,
202
+ "--limit",
203
+ "5",
204
+ "--json",
205
+ "workflowName,status,conclusion,headBranch,event,createdAt,databaseId",
206
+ ]);
207
+ if (ghRuns) return ghRuns;
208
+
209
+ const runs = await fetchJson(`repos/${repo}/actions/runs?per_page=5`).catch(() => null);
210
+ if (!runs?.workflow_runs) return [];
211
+ return runs.workflow_runs.map((run) => ({
212
+ workflowName: run.name,
213
+ status: run.status,
214
+ conclusion: run.conclusion,
215
+ headBranch: run.head_branch,
216
+ event: run.event,
217
+ createdAt: run.created_at,
218
+ databaseId: run.id,
219
+ }));
220
+ }
221
+
222
+ function round(value) {
223
+ return Math.round(value * 10) / 10;
224
+ }
225
+
226
+ parseRepo(repo);
227
+ const metrics = await getRepoMetrics();
228
+ const runs = await getRuns();
229
+ const releaseQuality = await getReleaseQuality(metrics.latestRelease);
230
+ const pagesQuality = await getPagesQuality(metrics.homepageUrl);
231
+ const localState = getLocalState(metrics.defaultBranchRef?.name);
232
+ const now = new Date();
233
+ const elapsedDays = Math.max((now - launchDate) / 86400000, 0);
234
+ const remainingDays = Math.max(launchDays - elapsedDays, 0);
235
+ const stars = metrics.stargazerCount ?? 0;
236
+ const gap = Math.max(goal - stars, 0);
237
+ const pace = elapsedDays > 0 ? stars / elapsedDays : 0;
238
+ const requiredDaily = remainingDays > 0 ? gap / remainingDays : gap;
239
+ const status =
240
+ gap === 0
241
+ ? "done"
242
+ : pace >= requiredDaily
243
+ ? "on-track"
244
+ : stars > 0
245
+ ? "behind"
246
+ : "needs-launch";
247
+
248
+ const report = {
249
+ repo,
250
+ url: metrics.url,
251
+ generatedAt: now.toISOString(),
252
+ launchDate: launchDate.toISOString(),
253
+ goal,
254
+ launchDays,
255
+ stars,
256
+ forks: metrics.forkCount ?? 0,
257
+ watchers: metrics.watchers?.totalCount ?? 0,
258
+ openIssues: metrics.issues?.totalCount ?? 0,
259
+ gap,
260
+ elapsedDays: round(elapsedDays),
261
+ remainingDays: round(remainingDays),
262
+ starsPerDay: round(pace),
263
+ requiredStarsPerDay: round(requiredDaily),
264
+ status,
265
+ latestRelease: metrics.latestRelease
266
+ ? {
267
+ name: metrics.latestRelease.name,
268
+ tagName: metrics.latestRelease.tagName,
269
+ url: metrics.latestRelease.url,
270
+ }
271
+ : null,
272
+ releaseQuality,
273
+ pagesQuality,
274
+ topics: metrics.repositoryTopics?.map((topic) => topic.name ?? topic) ?? [],
275
+ localState,
276
+ recentRuns: runs.map((run) => ({
277
+ workflow: run.workflowName,
278
+ status: run.status,
279
+ conclusion: run.conclusion,
280
+ branch: run.headBranch,
281
+ event: run.event,
282
+ createdAt: run.createdAt,
283
+ id: run.databaseId,
284
+ })),
285
+ };
286
+
287
+ if (outputJson) {
288
+ console.log(JSON.stringify(report, null, 2));
289
+ } else {
290
+ console.log(`# RepoTrailer growth check`);
291
+ console.log("");
292
+ console.log(`Repo: ${report.url}`);
293
+ console.log(`Generated: ${report.generatedAt}`);
294
+ console.log("");
295
+ console.log(`- Stars: ${stars}/${goal} (${gap} to go)`);
296
+ console.log(`- Forks: ${report.forks}`);
297
+ console.log(`- Watchers: ${report.watchers}`);
298
+ console.log(`- Open issues: ${report.openIssues}`);
299
+ console.log(`- Elapsed launch days: ${report.elapsedDays}/${launchDays}`);
300
+ console.log(`- Current pace: ${report.starsPerDay} stars/day`);
301
+ console.log(`- Required pace: ${report.requiredStarsPerDay} stars/day`);
302
+ console.log(`- Status: ${status}`);
303
+ if (report.latestRelease) {
304
+ console.log(`- Latest release: ${report.latestRelease.tagName}`);
305
+ if (report.releaseQuality) {
306
+ console.log(
307
+ `- Release page: ${report.releaseQuality.status} `
308
+ + `(${report.releaseQuality.bodyCharacters} chars)`,
309
+ );
310
+ }
311
+ }
312
+ if (report.localState) {
313
+ console.log(`- Local branch: ${report.localState.branch ?? "detached"} @ ${report.localState.head}`);
314
+ console.log(
315
+ `- Local ahead/behind ${report.localState.remoteRef}: `
316
+ + `${report.localState.ahead}/${report.localState.behind}`,
317
+ );
318
+ console.log(
319
+ `- Uncommitted changes: ${
320
+ report.localState.hasUncommittedChanges ? "yes" : "no"
321
+ }`,
322
+ );
323
+ }
324
+ if (report.pagesQuality) {
325
+ console.log(
326
+ `- Pages: ${report.pagesQuality.status} `
327
+ + `(${report.pagesQuality.httpStatus ?? "no HTTP"})`,
328
+ );
329
+ }
330
+ if (report.recentRuns.length > 0) {
331
+ console.log("");
332
+ console.log("Recent runs:");
333
+ for (const run of report.recentRuns) {
334
+ console.log(`- ${run.workflow}: ${run.status}/${run.conclusion ?? "pending"} (${run.branch})`);
335
+ }
336
+ }
337
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+
4
+ const requiredFiles = new Set([
5
+ "bin/repotrailer.js",
6
+ "src/cli.js",
7
+ "src/index.js",
8
+ "README.md",
9
+ "LICENSE",
10
+ "llms.txt",
11
+ ".codex-plugin/plugin.json",
12
+ "action.yml",
13
+ ]);
14
+
15
+ function run(command, args, options = {}) {
16
+ return execFileSync(command, args, {
17
+ encoding: "utf8",
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ timeout: options.timeoutMs ?? 60000,
20
+ }).trim();
21
+ }
22
+
23
+ function tryRun(command, args, options) {
24
+ try {
25
+ return { ok: true, stdout: run(command, args, options), stderr: "" };
26
+ } catch (error) {
27
+ const stdout = error.stdout?.trim?.() ?? "";
28
+ const stderr = error.stderr?.trim?.() || error.message;
29
+ return {
30
+ ok: false,
31
+ stdout,
32
+ stderr,
33
+ };
34
+ }
35
+ }
36
+
37
+ function classifyRegistryResult(result) {
38
+ const output = `${result.stdout}\n${result.stderr}`;
39
+ if (result.ok) {
40
+ return { status: "exists", output };
41
+ }
42
+ if (/E404|404 Not Found|not found/i.test(output)) {
43
+ return { status: "available", output };
44
+ }
45
+ if (/ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|network|proxy|socket hang up/i.test(output)) {
46
+ return { status: "unknown", output };
47
+ }
48
+ return { status: "unknown", output };
49
+ }
50
+
51
+ function fail(message) {
52
+ console.error(`publish readiness failed: ${message}`);
53
+ process.exitCode = 1;
54
+ }
55
+
56
+ function readJson(command, args) {
57
+ return JSON.parse(run(command, args));
58
+ }
59
+
60
+ const packageInfo = readJson("node", [
61
+ "-e",
62
+ "console.log(JSON.stringify(JSON.parse(require('fs').readFileSync('package.json', 'utf8'))))",
63
+ ]);
64
+
65
+ if (packageInfo.name !== "repotrailer") {
66
+ fail(`package name must be repotrailer, received ${packageInfo.name}`);
67
+ }
68
+ if (!packageInfo.bin?.repotrailer) {
69
+ fail("package must expose the repotrailer bin");
70
+ }
71
+ if (!packageInfo.homepage?.includes("howong217-ui.github.io/repotrailer")) {
72
+ fail("package homepage must point to the GitHub Pages landing page");
73
+ }
74
+
75
+ const pack = readJson("npm", ["pack", "--dry-run", "--json"])[0];
76
+ const packedFiles = new Set(pack.files.map((file) => file.path));
77
+ for (const file of requiredFiles) {
78
+ if (!packedFiles.has(file)) {
79
+ fail(`npm tarball is missing ${file}`);
80
+ }
81
+ }
82
+
83
+ const registry = tryRun("npm", ["view", packageInfo.name, "name", "--json"], {
84
+ timeoutMs: 30000,
85
+ });
86
+ const registryStatus = classifyRegistryResult(registry);
87
+ const nameAvailable = registryStatus.status === "available";
88
+ if (registryStatus.status === "exists") {
89
+ fail(`npm package name appears to exist: ${packageInfo.name}`);
90
+ } else if (!nameAvailable) {
91
+ fail(`could not verify npm package availability due to registry error: ${registryStatus.output.split("\n").find(Boolean) ?? "unknown error"}`);
92
+ }
93
+
94
+ if (process.exitCode) {
95
+ process.exit(process.exitCode);
96
+ }
97
+
98
+ console.log(JSON.stringify({
99
+ package: `${packageInfo.name}@${packageInfo.version}`,
100
+ nameAvailable,
101
+ packedFiles: pack.entryCount,
102
+ unpackedSize: pack.unpackedSize,
103
+ status: "ready",
104
+ }, null, 2));
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: repotrailer
3
+ description: Create README-ready launch assets, a social card, browser preview, release copy, and optional video from the current local repository or a GitHub URL. Use when the user wants to promote, launch, showcase, announce, or make a demo for an open-source project.
4
+ ---
5
+
6
+ # RepoTrailer
7
+
8
+ Turn repository facts into launch assets. Never invent stars, downloads,
9
+ benchmarks, contributors, or adoption claims.
10
+
11
+ ## Workflow
12
+
13
+ 1. Identify the source repository. Use the current directory unless the user
14
+ supplies a local path or GitHub URL.
15
+ 2. Run the local RepoTrailer CLI:
16
+
17
+ ```bash
18
+ npx repotrailer@latest . --out ./repotrailer-out
19
+ ```
20
+
21
+ During local plugin development, run:
22
+
23
+ ```bash
24
+ node bin/repotrailer.js . --out ./repotrailer-out
25
+ ```
26
+
27
+ 3. Review `repotrailer-out/repotrailer.json`. Check that the title, tagline,
28
+ features, install command, and repository URL are accurate.
29
+ 4. If metadata needs correction, rerun with `--title`, `--tagline`, or
30
+ `--install`. Do not edit generated metrics into stronger claims.
31
+ 5. Open `repotrailer-out/index.html` in the browser and inspect the visual
32
+ hierarchy. When video rendering is available, render only after the static
33
+ preview is correct.
34
+ 6. Return links to the generated preview, social card, launch copy, manifest,
35
+ and video.
36
+
37
+ ## Output Contract
38
+
39
+ - `index.html`: static scene-by-scene preview
40
+ - `social-card.svg`: 1200x630 README and social image
41
+ - `launch-copy.md`: short post, Show HN draft, README snippet, topics
42
+ - `repotrailer.json`: source-grounded repository metadata and storyboard
43
+ - `trailer.mp4`: optional rendered video when HyperFrames is available
44
+
45
+ ## Guardrails
46
+
47
+ - Prefer the repository README and package metadata over creative inference.
48
+ - Phrase missing facts as unknown; never fill them with plausible numbers.
49
+ - Keep the trailer under 25 seconds unless the user requests a longer format.
50
+ - Lead with one benefit, show at most three features, then show how to try it.
51
+ - Treat generated launch copy as a draft. Do not post it without the user's
52
+ explicit request.