lastgen 1.2.0 → 1.2.1

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/bin/lastgen ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const { run } = await import(join(__dirname, '..', 'dist', 'cli.mjs'));
10
+
11
+ try {
12
+ await run(process.argv.slice(2));
13
+ } catch (err) {
14
+ const message = err instanceof Error ? err.message : String(err);
15
+ process.stderr.write(`Error: ${message}\n`);
16
+ process.exitCode = 1;
17
+ }
package/dist/cli.mjs ADDED
@@ -0,0 +1,673 @@
1
+ import { parseArgs, styleText } from "node:util";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, extname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createHash } from "node:crypto";
6
+ import { createServer } from "node:http";
7
+ //#region src/core/types.ts
8
+ /**
9
+ * @fileoverview Shared interfaces, constants, and type definitions for lastgen.
10
+ */
11
+ const CUTOFF_DATE = "2025-02-21T00:00:00Z";
12
+ const ERAS = {
13
+ LAST_GEN: {
14
+ title: "Last Generation Coder",
15
+ description: "Wrote code before AI agents shipped"
16
+ },
17
+ AI_NATIVE: {
18
+ title: "AI Native Coder",
19
+ description: "First verifiable commit after AI agents shipped"
20
+ }
21
+ };
22
+ const CERTIFICATE_SALT = "lastgen_v1";
23
+ const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
24
+ //#endregion
25
+ //#region src/core/github.ts
26
+ const GITHUB_API = "https://api.github.com";
27
+ const USER_AGENT = "lastgen";
28
+ function buildHeaders(token) {
29
+ const headers = {
30
+ Accept: "application/vnd.github.v3+json",
31
+ "User-Agent": USER_AGENT
32
+ };
33
+ if (token) headers["Authorization"] = `token ${token}`;
34
+ return headers;
35
+ }
36
+ async function githubFetch(url, token, extraHeaders) {
37
+ const response = await fetch(url, { headers: {
38
+ ...buildHeaders(token),
39
+ ...extraHeaders
40
+ } });
41
+ if (response.status === 403) {
42
+ if (response.headers.get("x-ratelimit-remaining") === "0") {
43
+ const resetTimestamp = response.headers.get("x-ratelimit-reset");
44
+ const resetDate = resetTimestamp ? (/* @__PURE__ */ new Date(Number(resetTimestamp) * 1e3)).toLocaleTimeString() : "soon";
45
+ throw new Error(`GitHub API rate limit exceeded. Resets at ${resetDate}.`);
46
+ }
47
+ }
48
+ if (response.status === 404) {
49
+ const userMatch = url.match(/\/users\/([^/?]+)/);
50
+ if (userMatch?.[1]) throw new Error(`GitHub user '${decodeURIComponent(userMatch[1])}' not found. Check the spelling?`);
51
+ throw new Error(`Not found: ${url}`);
52
+ }
53
+ if (!response.ok) throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
54
+ return response;
55
+ }
56
+ async function fetchUser(username, token) {
57
+ const data = await (await githubFetch(`${GITHUB_API}/users/${encodeURIComponent(username)}`, token)).json();
58
+ return {
59
+ login: data.login,
60
+ id: data.id,
61
+ name: data.name ?? null,
62
+ createdAt: data.created_at
63
+ };
64
+ }
65
+ async function fetchFirstCommit(username, token) {
66
+ const commit = await searchFirstCommit(username, token);
67
+ if (commit?.repo) commit.repoCreatedAt = await fetchRepoCreatedAt(commit.repo, token);
68
+ return commit;
69
+ }
70
+ async function fetchRepoCreatedAt(repoFullName, token) {
71
+ try {
72
+ return (await (await githubFetch(`${GITHUB_API}/repos/${repoFullName}`, token)).json()).created_at ?? void 0;
73
+ } catch {
74
+ return;
75
+ }
76
+ }
77
+ async function searchFirstCommitByQuery(query, token, order = "asc") {
78
+ try {
79
+ const item = (await (await githubFetch(`${GITHUB_API}/search/commits?q=${encodeURIComponent(query)}&sort=committer-date&order=${order}&per_page=1`, token, { Accept: "application/vnd.github.cloak-preview+json" })).json()).items?.[0];
80
+ if (!item) return null;
81
+ const commit = item.commit;
82
+ const author = commit.author;
83
+ const commitCommitter = commit.committer;
84
+ const repo = item.repository;
85
+ return {
86
+ date: author.date,
87
+ repo: repo.full_name ?? "",
88
+ sha: item.sha,
89
+ message: (commit.message ?? "").split("\n")[0] ?? "",
90
+ committerDate: commitCommitter?.date ?? void 0
91
+ };
92
+ } catch (err) {
93
+ if (err instanceof Error && err.message.includes("rate limit")) throw err;
94
+ return null;
95
+ }
96
+ }
97
+ async function searchFirstCommit(username, token) {
98
+ const cutoffDate = CUTOFF_DATE.slice(0, 10);
99
+ return await searchFirstCommitByQuery(`author:${username} user:${username} committer-date:<${cutoffDate}`, token, "desc") ?? await searchFirstCommitByQuery(`author:${username} committer-date:<${cutoffDate}`, token, "desc") ?? await searchFirstCommitByQuery(`author:${username}`, token);
100
+ }
101
+ async function fetchCommit(repoFullName, sha, token) {
102
+ const data = await (await githubFetch(`${GITHUB_API}/repos/${repoFullName}/commits/${sha}`, token)).json();
103
+ const commit = data.commit;
104
+ const commitAuthor = commit.author;
105
+ const commitCommitter = commit.committer;
106
+ const verification = commit.verification;
107
+ const author = data.author;
108
+ const committer = data.committer;
109
+ const parents = data.parents;
110
+ return {
111
+ sha: data.sha,
112
+ authorLogin: author?.login ?? null,
113
+ committerLogin: committer?.login ?? null,
114
+ authorEmail: commitAuthor.email ?? null,
115
+ authorDate: commitAuthor.date ?? null,
116
+ committerDate: commitCommitter?.date ?? null,
117
+ authorId: author?.id ?? null,
118
+ verificationReason: verification?.reason ?? null,
119
+ isRootCommit: Array.isArray(parents) && parents.length === 0,
120
+ message: (commit.message ?? "").split("\n")[0] ?? "",
121
+ verified: Boolean(verification?.verified)
122
+ };
123
+ }
124
+ //#endregion
125
+ //#region src/core/proof.ts
126
+ function classifyEra(proofDate) {
127
+ const cutoff = new Date(CUTOFF_DATE).getTime();
128
+ return new Date(proofDate).getTime() < cutoff ? "LAST_GEN" : "AI_NATIVE";
129
+ }
130
+ function resolveProofDate(user, firstCommit) {
131
+ if (!firstCommit) return (/* @__PURE__ */ new Date()).toISOString();
132
+ const effectiveDate = getEffectiveCommitDate(firstCommit);
133
+ const commitTime = new Date(effectiveDate).getTime();
134
+ const accountTime = new Date(user.createdAt).getTime();
135
+ if (commitTime < (firstCommit.repoCreatedAt ? new Date(firstCommit.repoCreatedAt).getTime() : Infinity)) return firstCommit.repoCreatedAt ?? user.createdAt;
136
+ return commitTime < accountTime ? effectiveDate : user.createdAt;
137
+ }
138
+ function getEffectiveCommitDate(commit) {
139
+ if (!commit.committerDate) return commit.date;
140
+ const authorTime = new Date(commit.date).getTime();
141
+ if (new Date(commit.committerDate).getTime() - authorTime > 2592e6) return commit.committerDate;
142
+ return commit.date;
143
+ }
144
+ async function generateCertificateHash(hashFn, username, githubId, proofDate, era) {
145
+ return hashFn(JSON.stringify({
146
+ username,
147
+ githubId,
148
+ proofDate,
149
+ era,
150
+ salt: CERTIFICATE_SALT
151
+ }));
152
+ }
153
+ function generateCertificateNumber(hash) {
154
+ const prefix = hash.slice(0, 4).toUpperCase();
155
+ const numericPart = parseInt(hash.slice(4, 12), 16) % 1e6;
156
+ return `LGC-${prefix}-${String(numericPart).padStart(6, "0")}`;
157
+ }
158
+ async function createCertificate(hashFn, user, firstCommit) {
159
+ const proofDate = resolveProofDate(user, firstCommit);
160
+ const era = classifyEra(proofDate);
161
+ const hash = await generateCertificateHash(hashFn, user.login, user.id, proofDate, era);
162
+ const certificateNumber = generateCertificateNumber(hash);
163
+ return {
164
+ version: "1.0",
165
+ type: "LASTGEN_CERTIFICATE",
166
+ identity: {
167
+ username: user.login,
168
+ githubId: user.id,
169
+ name: user.name
170
+ },
171
+ proof: {
172
+ accountCreated: user.createdAt,
173
+ firstCommit: firstCommit ?? {
174
+ date: user.createdAt,
175
+ repo: "",
176
+ sha: "",
177
+ message: "(no public commits found - using account creation date)"
178
+ },
179
+ proofDate
180
+ },
181
+ era,
182
+ verification: {
183
+ hash: `sha256:${hash}`,
184
+ salt: CERTIFICATE_SALT
185
+ },
186
+ certificateNumber,
187
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString()
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/hash.ts
192
+ /**
193
+ * @fileoverview Node.js SHA-256 hash implementation using node:crypto.
194
+ */
195
+ const nodeHash = async (data) => {
196
+ return createHash("sha256").update(data).digest("hex");
197
+ };
198
+ //#endregion
199
+ //#region src/display.ts
200
+ /**
201
+ * @fileoverview Terminal output formatting with ASCII box-drawing for certificates and verification.
202
+ */
203
+ function shouldUseColor() {
204
+ if (process.env["NO_COLOR"] !== void 0) return false;
205
+ if (process.env["TERM"] === "dumb") return false;
206
+ if (process.argv.includes("--no-color")) return false;
207
+ return process.stdout.isTTY === true;
208
+ }
209
+ function style(format, text) {
210
+ if (!shouldUseColor()) return text;
211
+ return styleText(format, text);
212
+ }
213
+ function boxRule() {
214
+ return style("dim", "+" + "-".repeat(52) + "+");
215
+ }
216
+ function boxLine(content, rawLength) {
217
+ const pad = 50 - rawLength;
218
+ return style("dim", "|") + " " + content + " ".repeat(Math.max(pad, 0)) + " " + style("dim", "|");
219
+ }
220
+ function boxEmpty() {
221
+ return boxLine("", 0);
222
+ }
223
+ const LABEL_WIDTH = 13;
224
+ function labelLine(label, value, styledValue, lines) {
225
+ const max = 50 - LABEL_WIDTH;
226
+ if (value.length <= max) {
227
+ lines.push(boxLine(style("dim", label) + styledValue, LABEL_WIDTH + value.length));
228
+ return;
229
+ }
230
+ const indent = " ".repeat(LABEL_WIDTH);
231
+ let remaining = value;
232
+ let isFirst = true;
233
+ while (remaining.length > 0) {
234
+ const chunk = remaining.slice(0, max);
235
+ remaining = remaining.slice(max);
236
+ const prefix = isFirst ? style("dim", label) : indent;
237
+ lines.push(boxLine(prefix + chunk, LABEL_WIDTH + chunk.length));
238
+ isFirst = false;
239
+ }
240
+ }
241
+ function displayCertificate(cert) {
242
+ const out = process.stdout;
243
+ const isLastGen = cert.era === "LAST_GEN";
244
+ const eraInfo = ERAS[cert.era];
245
+ const lines = [];
246
+ lines.push(boxRule());
247
+ const title = style("bold", "LASTGEN CERTIFICATE");
248
+ const titlePadL = Math.floor(31 / 2);
249
+ const titlePadR = 31 - titlePadL;
250
+ lines.push(boxLine(" ".repeat(titlePadL) + title + " ".repeat(titlePadR), 50));
251
+ lines.push(boxRule());
252
+ labelLine("Certificate ", cert.certificateNumber, cert.certificateNumber, lines);
253
+ const issuedDate = new Date(cert.issuedAt).toISOString().slice(0, 10);
254
+ labelLine("Issued ", issuedDate, issuedDate, lines);
255
+ lines.push(boxEmpty());
256
+ const devValue = cert.identity.name ? `${cert.identity.username} (${cert.identity.name})` : cert.identity.username;
257
+ labelLine("Developer ", devValue, devValue, lines);
258
+ const eraColor = isLastGen ? "green" : "cyan";
259
+ labelLine("Era ", eraInfo.title, style(eraColor, eraInfo.title), lines);
260
+ labelLine(" ", eraInfo.description, style("dim", eraInfo.description), lines);
261
+ if (cert.proof.firstCommit.sha) {
262
+ lines.push(boxEmpty());
263
+ const commitDate = new Date(cert.proof.firstCommit.date).toISOString().slice(0, 10);
264
+ const repo = cert.proof.firstCommit.repo;
265
+ labelLine("Proof Commit ", repo, repo, lines);
266
+ const commitMsg = `${cert.proof.firstCommit.sha.slice(0, 7)} ${cert.proof.firstCommit.message.replace(/\n/g, " ")}`;
267
+ labelLine(" ", commitMsg, style("dim", commitMsg), lines);
268
+ labelLine("Commit Date ", commitDate, commitDate, lines);
269
+ }
270
+ lines.push(boxEmpty());
271
+ const hash = cert.verification.hash;
272
+ labelLine("Hash ", hash, style("dim", hash), lines);
273
+ lines.push(boxRule());
274
+ out.write("\n" + lines.join("\n") + "\n\n");
275
+ }
276
+ function displayBadgeMarkdown(cert) {
277
+ const out = process.stdout;
278
+ const badgeUrl = `https://img.shields.io/badge/lastgen-${cert.era === "LAST_GEN" ? "Last%20Gen" : "AI%20Native"}-${cert.era === "LAST_GEN" ? "blue" : "brightgreen"}?style=for-the-badge`;
279
+ out.write("\n");
280
+ out.write(style("bold", " Add to your GitHub README:") + "\n");
281
+ out.write("\n");
282
+ out.write(` [![Last Gen Coder](${badgeUrl})](https://github.com/pgagnidze/lastgen)\n`);
283
+ out.write("\n");
284
+ }
285
+ function displayJson(cert) {
286
+ process.stdout.write(JSON.stringify(cert, null, 2) + "\n");
287
+ }
288
+ function info(message) {
289
+ process.stderr.write(style("dim", ` ${message}`) + "\n");
290
+ }
291
+ function error(message) {
292
+ process.stderr.write(style("red", ` ${message}`) + "\n");
293
+ }
294
+ //#endregion
295
+ //#region src/core/verify.ts
296
+ function isValidCertificate(data) {
297
+ if (typeof data !== "object" || data === null) return false;
298
+ const cert = data;
299
+ return cert.type === "LASTGEN_CERTIFICATE" && typeof cert.version === "string" && typeof cert.identity === "object" && typeof cert.proof === "object" && typeof cert.verification === "object" && typeof cert.certificateNumber === "string";
300
+ }
301
+ function matchesNoreplyEmail(email, username) {
302
+ const lower = email.toLowerCase();
303
+ const user = username.toLowerCase();
304
+ return new RegExp(`^(\\d+\\+)?${escapeRegex(user)}@users\\.noreply\\.github\\.com$`).test(lower);
305
+ }
306
+ function escapeRegex(str) {
307
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
308
+ }
309
+ async function verifyCertificateData(cert, hashFn, token) {
310
+ const results = [];
311
+ const expectedHash = await generateCertificateHash(hashFn, cert.identity.username, cert.identity.githubId, cert.proof.proofDate, cert.era);
312
+ const actualHash = cert.verification.hash.replace("sha256:", "");
313
+ results.push({
314
+ check: "Hash integrity",
315
+ passed: expectedHash === actualHash,
316
+ detail: expectedHash === actualHash ? "Certificate hash is valid" : "Certificate hash does not match - data may have been tampered with"
317
+ });
318
+ const proofDate = new Date(cert.proof.proofDate);
319
+ const cutoff = new Date(CUTOFF_DATE);
320
+ const expectedEra = proofDate.getTime() < cutoff.getTime() ? "LAST_GEN" : "AI_NATIVE";
321
+ results.push({
322
+ check: "Era classification",
323
+ passed: cert.era === expectedEra,
324
+ detail: cert.era === expectedEra ? `Era ${cert.era} is correct for proof date ${cert.proof.proofDate}` : `Era should be ${expectedEra} but certificate claims ${cert.era}`
325
+ });
326
+ const expectedProofDate = resolveProofDate({
327
+ login: cert.identity.username,
328
+ id: cert.identity.githubId,
329
+ name: cert.identity.name,
330
+ createdAt: cert.proof.accountCreated
331
+ }, cert.proof.firstCommit.sha ? cert.proof.firstCommit : null);
332
+ const proofDateMatch = Math.abs(new Date(expectedProofDate).getTime() - proofDate.getTime()) < 6e4;
333
+ results.push({
334
+ check: "Proof date",
335
+ passed: proofDateMatch,
336
+ detail: proofDateMatch ? `Proof date ${cert.proof.proofDate} is consistent with commit and account data` : `Proof date should be ${expectedProofDate} but certificate claims ${cert.proof.proofDate}`
337
+ });
338
+ if (cert.proof.firstCommit.sha) try {
339
+ const commitDetail = await fetchCommit(cert.proof.firstCommit.repo, cert.proof.firstCommit.sha, token);
340
+ const username = cert.identity.username.toLowerCase();
341
+ const authorMatch = (commitDetail.authorLogin ?? "").toLowerCase() === username;
342
+ const committerMatch = (commitDetail.committerLogin ?? "").toLowerCase() === username;
343
+ const emailMatch = commitDetail.authorEmail ? matchesNoreplyEmail(commitDetail.authorEmail, cert.identity.username) : false;
344
+ const identityMatch = authorMatch || committerMatch || emailMatch;
345
+ const matchMethods = [];
346
+ if (authorMatch) matchMethods.push("author login");
347
+ if (committerMatch) matchMethods.push("committer login");
348
+ if (emailMatch) matchMethods.push("noreply email");
349
+ results.push({
350
+ check: "Identity",
351
+ passed: identityMatch,
352
+ detail: identityMatch ? `Matched via: ${matchMethods.join(", ")}` : `Commit author (${commitDetail.authorLogin}) does not match ${cert.identity.username}`
353
+ });
354
+ const isSelfOwned = (cert.proof.firstCommit.repo.split("/")[0] ?? "").toLowerCase() === cert.identity.username.toLowerCase();
355
+ results.push({
356
+ check: "Repo ownership",
357
+ passed: true,
358
+ detail: isSelfOwned ? `Commit is in a repo owned by ${cert.identity.username}` : `Commit is in a third-party repo (${cert.proof.firstCommit.repo})`
359
+ });
360
+ if (commitDetail.authorId !== null) {
361
+ const idMatch = commitDetail.authorId === cert.identity.githubId;
362
+ results.push({
363
+ check: "GitHub ID",
364
+ passed: idMatch,
365
+ detail: idMatch ? `GitHub ID ${commitDetail.authorId} matches certificate` : `Commit author ID ${commitDetail.authorId} does not match certificate ID ${cert.identity.githubId}`
366
+ });
367
+ }
368
+ const commitDate = new Date(commitDetail.authorDate ?? "");
369
+ const certDate = new Date(cert.proof.firstCommit.date);
370
+ const datesClose = Math.abs(commitDate.getTime() - certDate.getTime()) < 6e4;
371
+ results.push({
372
+ check: "Commit date",
373
+ passed: datesClose,
374
+ detail: datesClose ? `Commit date matches certificate (${commitDetail.authorDate})` : `Commit date ${commitDetail.authorDate} differs from certificate ${cert.proof.firstCommit.date}`
375
+ });
376
+ if (commitDetail.authorDate && commitDetail.committerDate) {
377
+ const authorTime = new Date(commitDetail.authorDate).getTime();
378
+ const committerTime = new Date(commitDetail.committerDate).getTime();
379
+ const driftMs = Math.abs(committerTime - authorTime);
380
+ const driftDays = Math.round(driftMs / (1440 * 60 * 1e3));
381
+ const consistent = driftMs <= THIRTY_DAYS_MS;
382
+ results.push({
383
+ check: "Date consistency",
384
+ passed: consistent,
385
+ detail: consistent ? `Author/committer date drift: ${driftDays}d (within 30d threshold)` : `Author/committer date drift: ${driftDays}d exceeds 30d - author date may be forged`
386
+ });
387
+ }
388
+ if (commitDetail.isRootCommit) results.push({
389
+ check: "Root commit",
390
+ passed: true,
391
+ detail: "Commit has no parents (first commit in repo - higher trust)"
392
+ });
393
+ if (commitDetail.verified) {
394
+ const reason = commitDetail.verificationReason;
395
+ const reasonDetail = reason && reason !== "valid" ? ` (${reason})` : "";
396
+ results.push({
397
+ check: "GPG signature",
398
+ passed: true,
399
+ detail: `Commit is GPG-signed${reasonDetail}`
400
+ });
401
+ }
402
+ } catch (fetchError) {
403
+ const message = fetchError instanceof Error ? fetchError.message : String(fetchError);
404
+ results.push({
405
+ check: "Commit verification",
406
+ passed: false,
407
+ detail: `Could not fetch commit from GitHub: ${message}`
408
+ });
409
+ }
410
+ return {
411
+ valid: results.every((r) => r.passed),
412
+ results,
413
+ certificateNumber: cert.certificateNumber,
414
+ username: cert.identity.username
415
+ };
416
+ }
417
+ //#endregion
418
+ //#region src/verify-cli.ts
419
+ /**
420
+ * @fileoverview CLI wrapper for certificate verification. Handles file I/O and display.
421
+ */
422
+ async function verifyCertificate(filePath, hashFn, token) {
423
+ let raw;
424
+ try {
425
+ raw = readFileSync(filePath, "utf-8");
426
+ } catch {
427
+ error(`Cannot read file: ${filePath}`);
428
+ return false;
429
+ }
430
+ let cert;
431
+ try {
432
+ cert = JSON.parse(raw);
433
+ } catch {
434
+ error("Invalid JSON in certificate file.");
435
+ return false;
436
+ }
437
+ if (!isValidCertificate(cert)) {
438
+ error("File is not a valid lastgen certificate.");
439
+ return false;
440
+ }
441
+ info(`Verifying certificate ${cert.certificateNumber}...`);
442
+ info(`Developer: ${cert.identity.username}`);
443
+ info("");
444
+ if (cert.proof.firstCommit.sha) info(`Fetching commit ${cert.proof.firstCommit.sha.slice(0, 7)} from GitHub...`);
445
+ const { valid, results } = await verifyCertificateData(cert, hashFn, token);
446
+ const out = process.stdout;
447
+ const lines = [];
448
+ lines.push(boxRule());
449
+ const title = style("bold", "VERIFICATION");
450
+ const titlePadL = Math.floor(38 / 2);
451
+ const titlePadR = 38 - titlePadL;
452
+ lines.push(boxLine(" ".repeat(titlePadL) + title + " ".repeat(titlePadR), 50));
453
+ lines.push(boxRule());
454
+ for (const result of results) {
455
+ const checkLine = `${result.passed ? style("green", "PASS") : style("red", "FAIL")} ${style("bold", result.check)}`;
456
+ lines.push(boxLine(checkLine, 6 + result.check.length));
457
+ const indent = 6;
458
+ const maxLen = 50 - indent;
459
+ let remaining = result.detail;
460
+ while (remaining.length > 0) {
461
+ const chunk = remaining.slice(0, maxLen);
462
+ remaining = remaining.slice(maxLen);
463
+ const line = " ".repeat(indent) + style("dim", chunk);
464
+ lines.push(boxLine(line, indent + chunk.length));
465
+ }
466
+ }
467
+ lines.push(boxRule());
468
+ if (valid) {
469
+ const msg = style("green", "Certificate is valid.");
470
+ lines.push(boxLine(msg, 21));
471
+ } else {
472
+ const msg = style("red", "Certificate verification failed.");
473
+ lines.push(boxLine(msg, 32));
474
+ }
475
+ lines.push(boxRule());
476
+ out.write("\n" + lines.join("\n") + "\n\n");
477
+ return valid;
478
+ }
479
+ //#endregion
480
+ //#region src/serve.ts
481
+ /**
482
+ * @fileoverview Static HTTP server for the pre-built web frontend.
483
+ * Zero dependencies — uses node:http and node:fs.
484
+ */
485
+ const MIME_TYPES = {
486
+ ".html": "text/html; charset=utf-8",
487
+ ".css": "text/css; charset=utf-8",
488
+ ".js": "application/javascript; charset=utf-8",
489
+ ".json": "application/json; charset=utf-8",
490
+ ".svg": "image/svg+xml",
491
+ ".png": "image/png",
492
+ ".ico": "image/x-icon"
493
+ };
494
+ function serve(port = 3e3) {
495
+ const distDir = join(dirname(fileURLToPath(import.meta.url)), "..", "web", "dist");
496
+ if (!existsSync(distDir)) {
497
+ process.stderr.write(` Web assets not found at ${distDir}\n Run "cd web && npm install && npm run build" first.
498
+ `);
499
+ process.exitCode = 1;
500
+ return;
501
+ }
502
+ createServer((req, res) => {
503
+ let urlPath = req.url ?? "/";
504
+ const qIndex = urlPath.indexOf("?");
505
+ if (qIndex !== -1) urlPath = urlPath.slice(0, qIndex);
506
+ if (urlPath.startsWith("/lastgen/")) urlPath = urlPath.slice(8);
507
+ if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
508
+ const filePath = join(distDir, urlPath);
509
+ if (!filePath.startsWith(distDir)) {
510
+ res.writeHead(403);
511
+ res.end("Forbidden");
512
+ return;
513
+ }
514
+ try {
515
+ const content = readFileSync(filePath);
516
+ const contentType = MIME_TYPES[extname(filePath)] ?? "application/octet-stream";
517
+ res.writeHead(200, { "Content-Type": contentType });
518
+ res.end(content);
519
+ } catch {
520
+ try {
521
+ const indexContent = readFileSync(join(distDir, "index.html"));
522
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
523
+ res.end(indexContent);
524
+ } catch {
525
+ res.writeHead(404);
526
+ res.end("Not found");
527
+ }
528
+ }
529
+ }).listen(port, "127.0.0.1", () => {
530
+ process.stderr.write(`\n lastgen web UI running at http://localhost:${port}/\n\n`);
531
+ });
532
+ }
533
+ //#endregion
534
+ //#region src/cli.ts
535
+ /**
536
+ * @fileoverview CLI argument parsing and command routing using built-in parseArgs.
537
+ */
538
+ function getVersion() {
539
+ try {
540
+ const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
541
+ return JSON.parse(readFileSync(packagePath, "utf-8")).version ?? "unknown";
542
+ } catch {
543
+ return "unknown";
544
+ }
545
+ }
546
+ const HELP_BRIEF = `
547
+ _ _
548
+ | | __ _ ___ | |_ __ _ ___ _ __
549
+ | |/ _\` / __|| __/ _\` |/ _ \\ '_ \\
550
+ | | (_| \\__ \\| || (_| | __/ | | |
551
+ |_|\\__,_|___/ \\__\\__, |\\___|_| |_|
552
+ |___/
553
+ Check if you started coding before or after AI agents.
554
+
555
+ Usage:
556
+ lastgen <username> Classify a GitHub user
557
+ lastgen verify <file.json> Verify a saved certificate
558
+ lastgen serve [--port <port>] Launch web UI
559
+
560
+ Options:
561
+ --token <token> GitHub personal access token
562
+ --json Output as JSON
563
+ --badge Output as README badge markdown
564
+ --port <port> Port for web UI (default: 3000)
565
+ --no-color Disable colors
566
+ -h, --help Show this help
567
+ -v, --version Show version
568
+
569
+ Environment:
570
+ GITHUB_TOKEN GitHub token (alternative to --token)
571
+ NO_COLOR Disable colors (any value)
572
+
573
+ Examples:
574
+ npx lastgen torvalds
575
+ npx lastgen --json torvalds > proof.json
576
+ npx lastgen verify proof.json
577
+ npx lastgen --badge torvalds
578
+ npx lastgen serve
579
+ `;
580
+ function parseCli(argv) {
581
+ const { values, positionals } = parseArgs({
582
+ args: argv,
583
+ options: {
584
+ token: { type: "string" },
585
+ json: {
586
+ type: "boolean",
587
+ default: false
588
+ },
589
+ badge: {
590
+ type: "boolean",
591
+ default: false
592
+ },
593
+ port: { type: "string" },
594
+ help: {
595
+ type: "boolean",
596
+ short: "h",
597
+ default: false
598
+ },
599
+ version: {
600
+ type: "boolean",
601
+ short: "v",
602
+ default: false
603
+ },
604
+ "no-color": {
605
+ type: "boolean",
606
+ default: false
607
+ }
608
+ },
609
+ allowPositionals: true,
610
+ strict: false
611
+ });
612
+ const first = positionals[0] ?? "";
613
+ const isVerify = first === "verify";
614
+ return {
615
+ command: isVerify ? "verify" : first === "serve" ? "serve" : first ? "lookup" : "",
616
+ target: isVerify ? positionals[1] ?? "" : first,
617
+ token: values.token ?? process.env["GITHUB_TOKEN"],
618
+ port: Number(values.port) || 3e3,
619
+ json: Boolean(values.json),
620
+ badge: Boolean(values.badge),
621
+ help: Boolean(values.help),
622
+ version: Boolean(values.version)
623
+ };
624
+ }
625
+ async function run(argv) {
626
+ const opts = parseCli(argv);
627
+ if (opts.version) {
628
+ process.stdout.write(`lastgen ${getVersion()}\n`);
629
+ return;
630
+ }
631
+ if (opts.help || !opts.command) {
632
+ process.stdout.write(HELP_BRIEF);
633
+ return;
634
+ }
635
+ switch (opts.command) {
636
+ case "lookup":
637
+ await handleLookup(opts);
638
+ break;
639
+ case "verify":
640
+ await handleVerify(opts);
641
+ break;
642
+ case "serve":
643
+ serve(opts.port);
644
+ break;
645
+ default:
646
+ error(`Unknown command: ${opts.command}`);
647
+ process.stdout.write(HELP_BRIEF);
648
+ process.exitCode = 2;
649
+ }
650
+ }
651
+ async function handleLookup(opts) {
652
+ if (!opts.target) {
653
+ error("Username required. Usage: lastgen <username>");
654
+ process.exitCode = 2;
655
+ return;
656
+ }
657
+ if (!opts.json && !opts.badge) info(`Looking up ${opts.target} on GitHub...`);
658
+ const [user, firstCommit] = await Promise.all([fetchUser(opts.target, opts.token), fetchFirstCommit(opts.target, opts.token)]);
659
+ const cert = await createCertificate(nodeHash, user, firstCommit);
660
+ if (opts.badge) displayBadgeMarkdown(cert);
661
+ else if (opts.json) displayJson(cert);
662
+ else displayCertificate(cert);
663
+ }
664
+ async function handleVerify(opts) {
665
+ if (!opts.target) {
666
+ error("Certificate file required. Usage: lastgen verify <file.json>");
667
+ process.exitCode = 2;
668
+ return;
669
+ }
670
+ if (!await verifyCertificate(opts.target, nodeHash, opts.token)) process.exitCode = 1;
671
+ }
672
+ //#endregion
673
+ export { parseCli, run };