lastgen 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "lastgen",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Check if you started coding before or after AI agents",
5
5
  "type": "module",
6
6
  "bin": {
7
- "lastgen": "./src/index.ts"
7
+ "lastgen": "./bin/lastgen"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=22.18.0"
11
11
  },
12
12
  "files": [
13
- "src",
13
+ "dist",
14
+ "bin",
14
15
  "web/dist",
15
16
  "README.md"
16
17
  ],
@@ -21,8 +22,9 @@
21
22
  "format": "prettier --write src/",
22
23
  "format:check": "prettier --check src/",
23
24
  "test": "node --test src/**/*.test.ts",
25
+ "build": "tsdown",
24
26
  "start": "node src/index.ts",
25
- "prepublishOnly": "cd web && npm ci && npm run build"
27
+ "prepublishOnly": "npm run build && cd web && npm ci && npm run build"
26
28
  },
27
29
  "keywords": [
28
30
  "developer",
@@ -54,6 +56,7 @@
54
56
  "eslint-plugin-prettier": "^5.5.5",
55
57
  "prettier": "^3.8.1",
56
58
  "semantic-release": "^25.0.3",
59
+ "tsdown": "^0.21.1",
57
60
  "typescript": "^5.8.0"
58
61
  }
59
62
  }
package/src/cli.ts DELETED
@@ -1,179 +0,0 @@
1
- /**
2
- * @fileoverview CLI argument parsing and command routing using built-in parseArgs.
3
- */
4
-
5
- import { parseArgs } from 'node:util';
6
- import { readFileSync } from 'node:fs';
7
- import { dirname, join } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- import { fetchFirstCommit, fetchUser } from './core/github.ts';
11
- import { createCertificate } from './core/proof.ts';
12
- import { nodeHash } from './hash.ts';
13
- import { displayBadgeMarkdown, displayCertificate, displayJson, error, info } from './display.ts';
14
- import { verifyCertificate } from './verify-cli.ts';
15
- import { serve } from './serve.ts';
16
-
17
- function getVersion(): string {
18
- try {
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
- const packagePath = join(__dirname, '..', 'package.json');
22
- const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
23
- return pkg.version ?? 'unknown';
24
- } catch {
25
- return 'unknown';
26
- }
27
- }
28
-
29
- const HELP_BRIEF = `
30
- _ _
31
- | | __ _ ___ | |_ __ _ ___ _ __
32
- | |/ _\` / __|| __/ _\` |/ _ \\ '_ \\
33
- | | (_| \\__ \\| || (_| | __/ | | |
34
- |_|\\__,_|___/ \\__\\__, |\\___|_| |_|
35
- |___/
36
- Check if you started coding before or after AI agents.
37
-
38
- Usage:
39
- lastgen <username> Classify a GitHub user
40
- lastgen verify <file.json> Verify a saved certificate
41
- lastgen serve [--port <port>] Launch web UI
42
-
43
- Options:
44
- --token <token> GitHub personal access token
45
- --json Output as JSON
46
- --badge Output as README badge markdown
47
- --port <port> Port for web UI (default: 3000)
48
- --no-color Disable colors
49
- -h, --help Show this help
50
- -v, --version Show version
51
-
52
- Environment:
53
- GITHUB_TOKEN GitHub token (alternative to --token)
54
- NO_COLOR Disable colors (any value)
55
-
56
- Examples:
57
- npx lastgen torvalds
58
- npx lastgen --json torvalds > proof.json
59
- npx lastgen verify proof.json
60
- npx lastgen --badge torvalds
61
- npx lastgen serve
62
- `;
63
-
64
- interface CliOptions {
65
- command: string;
66
- target: string;
67
- token?: string;
68
- port: number;
69
- json: boolean;
70
- badge: boolean;
71
- help: boolean;
72
- version: boolean;
73
- }
74
-
75
- export function parseCli(argv: string[]): CliOptions {
76
- const { values, positionals } = parseArgs({
77
- args: argv,
78
- options: {
79
- token: { type: 'string' },
80
- json: { type: 'boolean', default: false },
81
- badge: { type: 'boolean', default: false },
82
- port: { type: 'string' },
83
- help: { type: 'boolean', short: 'h', default: false },
84
- version: { type: 'boolean', short: 'v', default: false },
85
- 'no-color': { type: 'boolean', default: false },
86
- },
87
- allowPositionals: true,
88
- strict: false,
89
- });
90
-
91
- const first = positionals[0] ?? '';
92
- const isVerify = first === 'verify';
93
- const isServe = first === 'serve';
94
-
95
- return {
96
- command: isVerify ? 'verify' : isServe ? 'serve' : first ? 'lookup' : '',
97
- target: isVerify ? (positionals[1] ?? '') : first,
98
- token: (values.token as string | undefined) ?? process.env['GITHUB_TOKEN'],
99
- port: Number(values.port) || 3000,
100
- json: Boolean(values.json),
101
- badge: Boolean(values.badge),
102
- help: Boolean(values.help),
103
- version: Boolean(values.version),
104
- };
105
- }
106
-
107
- export async function run(argv: string[]): Promise<void> {
108
- const opts = parseCli(argv);
109
-
110
- if (opts.version) {
111
- process.stdout.write(`lastgen ${getVersion()}\n`);
112
- return;
113
- }
114
-
115
- if (opts.help || !opts.command) {
116
- process.stdout.write(HELP_BRIEF);
117
- return;
118
- }
119
-
120
- switch (opts.command) {
121
- case 'lookup': {
122
- await handleLookup(opts);
123
- break;
124
- }
125
- case 'verify': {
126
- await handleVerify(opts);
127
- break;
128
- }
129
- case 'serve': {
130
- serve(opts.port);
131
- break;
132
- }
133
- default: {
134
- error(`Unknown command: ${opts.command}`);
135
- process.stdout.write(HELP_BRIEF);
136
- process.exitCode = 2;
137
- }
138
- }
139
- }
140
-
141
- async function handleLookup(opts: CliOptions): Promise<void> {
142
- if (!opts.target) {
143
- error('Username required. Usage: lastgen <username>');
144
- process.exitCode = 2;
145
- return;
146
- }
147
-
148
- if (!opts.json && !opts.badge) {
149
- info(`Looking up ${opts.target} on GitHub...`);
150
- }
151
-
152
- const [user, firstCommit] = await Promise.all([
153
- fetchUser(opts.target, opts.token),
154
- fetchFirstCommit(opts.target, opts.token),
155
- ]);
156
-
157
- const cert = await createCertificate(nodeHash, user, firstCommit);
158
-
159
- if (opts.badge) {
160
- displayBadgeMarkdown(cert);
161
- } else if (opts.json) {
162
- displayJson(cert);
163
- } else {
164
- displayCertificate(cert);
165
- }
166
- }
167
-
168
- async function handleVerify(opts: CliOptions): Promise<void> {
169
- if (!opts.target) {
170
- error('Certificate file required. Usage: lastgen verify <file.json>');
171
- process.exitCode = 2;
172
- return;
173
- }
174
-
175
- const valid = await verifyCertificate(opts.target, nodeHash, opts.token);
176
- if (!valid) {
177
- process.exitCode = 1;
178
- }
179
- }
@@ -1,177 +0,0 @@
1
- /**
2
- * @fileoverview GitHub API client using built-in fetch. Zero dependencies.
3
- */
4
-
5
- import type { GitHubUser, FirstCommit, CommitDetail } from './types.ts';
6
- import { CUTOFF_DATE } from './types.ts';
7
-
8
- const GITHUB_API = 'https://api.github.com';
9
- const USER_AGENT = 'lastgen';
10
-
11
- function buildHeaders(token?: string): Record<string, string> {
12
- const headers: Record<string, string> = {
13
- Accept: 'application/vnd.github.v3+json',
14
- 'User-Agent': USER_AGENT,
15
- };
16
- if (token) {
17
- headers['Authorization'] = `token ${token}`;
18
- }
19
- return headers;
20
- }
21
-
22
- async function githubFetch(
23
- url: string,
24
- token?: string,
25
- extraHeaders?: Record<string, string>,
26
- ): Promise<Response> {
27
- const response = await fetch(url, { headers: { ...buildHeaders(token), ...extraHeaders } });
28
-
29
- if (response.status === 403) {
30
- const remaining = response.headers.get('x-ratelimit-remaining');
31
- if (remaining === '0') {
32
- const resetTimestamp = response.headers.get('x-ratelimit-reset');
33
- const resetDate = resetTimestamp
34
- ? new Date(Number(resetTimestamp) * 1000).toLocaleTimeString()
35
- : 'soon';
36
- throw new Error(`GitHub API rate limit exceeded. Resets at ${resetDate}.`);
37
- }
38
- }
39
-
40
- if (response.status === 404) {
41
- const userMatch = url.match(/\/users\/([^/?]+)/);
42
- if (userMatch?.[1]) {
43
- throw new Error(
44
- `GitHub user '${decodeURIComponent(userMatch[1])}' not found. Check the spelling?`,
45
- );
46
- }
47
- throw new Error(`Not found: ${url}`);
48
- }
49
-
50
- if (!response.ok) {
51
- throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
52
- }
53
-
54
- return response;
55
- }
56
-
57
- export async function fetchUser(username: string, token?: string): Promise<GitHubUser> {
58
- const response = await githubFetch(`${GITHUB_API}/users/${encodeURIComponent(username)}`, token);
59
- const data = (await response.json()) as Record<string, unknown>;
60
-
61
- return {
62
- login: data.login as string,
63
- id: data.id as number,
64
- name: (data.name as string | null) ?? null,
65
- createdAt: data.created_at as string,
66
- };
67
- }
68
-
69
- export async function fetchFirstCommit(
70
- username: string,
71
- token?: string,
72
- ): Promise<FirstCommit | null> {
73
- const commit = await searchFirstCommit(username, token);
74
-
75
- if (commit?.repo) {
76
- commit.repoCreatedAt = await fetchRepoCreatedAt(commit.repo, token);
77
- }
78
-
79
- return commit;
80
- }
81
-
82
- async function fetchRepoCreatedAt(
83
- repoFullName: string,
84
- token?: string,
85
- ): Promise<string | undefined> {
86
- try {
87
- const response = await githubFetch(`${GITHUB_API}/repos/${repoFullName}`, token);
88
- const data = (await response.json()) as Record<string, unknown>;
89
- return (data.created_at as string | undefined) ?? undefined;
90
- } catch {
91
- return undefined;
92
- }
93
- }
94
-
95
- async function searchFirstCommitByQuery(
96
- query: string,
97
- token?: string,
98
- order: 'asc' | 'desc' = 'asc',
99
- ): Promise<FirstCommit | null> {
100
- try {
101
- const url = `${GITHUB_API}/search/commits?q=${encodeURIComponent(query)}&sort=committer-date&order=${order}&per_page=1`;
102
- const response = await githubFetch(url, token, {
103
- Accept: 'application/vnd.github.cloak-preview+json',
104
- });
105
-
106
- const data = (await response.json()) as Record<string, unknown>;
107
- const items = data.items as Array<Record<string, unknown>> | undefined;
108
- const item = items?.[0];
109
- if (!item) return null;
110
-
111
- const commit = item.commit as Record<string, unknown>;
112
- const author = commit.author as Record<string, unknown>;
113
- const commitCommitter = commit.committer as Record<string, unknown> | undefined;
114
- const repo = item.repository as Record<string, unknown>;
115
-
116
- return {
117
- date: author.date as string,
118
- repo: (repo.full_name as string) ?? '',
119
- sha: item.sha as string,
120
- message: ((commit.message as string) ?? '').split('\n')[0] ?? '',
121
- committerDate: (commitCommitter?.date as string | undefined) ?? undefined,
122
- };
123
- } catch (err) {
124
- if (err instanceof Error && err.message.includes('rate limit')) throw err;
125
- return null;
126
- }
127
- }
128
-
129
- async function searchFirstCommit(username: string, token?: string): Promise<FirstCommit | null> {
130
- const cutoffDate = CUTOFF_DATE.slice(0, 10);
131
-
132
- return (
133
- (await searchFirstCommitByQuery(
134
- `author:${username} user:${username} committer-date:<${cutoffDate}`,
135
- token,
136
- 'desc',
137
- )) ??
138
- (await searchFirstCommitByQuery(
139
- `author:${username} committer-date:<${cutoffDate}`,
140
- token,
141
- 'desc',
142
- )) ??
143
- (await searchFirstCommitByQuery(`author:${username}`, token))
144
- );
145
- }
146
-
147
- export async function fetchCommit(
148
- repoFullName: string,
149
- sha: string,
150
- token?: string,
151
- ): Promise<CommitDetail> {
152
- const url = `${GITHUB_API}/repos/${repoFullName}/commits/${sha}`;
153
- const response = await githubFetch(url, token);
154
- const data = (await response.json()) as Record<string, unknown>;
155
-
156
- const commit = data.commit as Record<string, unknown>;
157
- const commitAuthor = commit.author as Record<string, unknown>;
158
- const commitCommitter = commit.committer as Record<string, unknown> | undefined;
159
- const verification = commit.verification as Record<string, unknown> | undefined;
160
- const author = data.author as Record<string, unknown> | null;
161
- const committer = data.committer as Record<string, unknown> | null;
162
- const parents = data.parents as Array<unknown> | undefined;
163
-
164
- return {
165
- sha: data.sha as string,
166
- authorLogin: (author?.login as string | undefined) ?? null,
167
- committerLogin: (committer?.login as string | undefined) ?? null,
168
- authorEmail: (commitAuthor.email as string | undefined) ?? null,
169
- authorDate: (commitAuthor.date as string | undefined) ?? null,
170
- committerDate: (commitCommitter?.date as string | undefined) ?? null,
171
- authorId: (author?.id as number | undefined) ?? null,
172
- verificationReason: (verification?.reason as string | undefined) ?? null,
173
- isRootCommit: Array.isArray(parents) && parents.length === 0,
174
- message: ((commit.message as string) ?? '').split('\n')[0] ?? '',
175
- verified: Boolean(verification?.verified),
176
- };
177
- }
package/src/core/proof.ts DELETED
@@ -1,111 +0,0 @@
1
- /**
2
- * @fileoverview Era classification, hashing, and certificate generation.
3
- * Platform-agnostic: hash function is injected via HashFn.
4
- */
5
-
6
- import type { Certificate, EraKey, FirstCommit, GitHubUser, HashFn } from './types.ts';
7
-
8
- import { CERTIFICATE_SALT, CERTIFICATE_VERSION, CUTOFF_DATE, THIRTY_DAYS_MS } from './types.ts';
9
-
10
- export function classifyEra(proofDate: string): EraKey {
11
- const cutoff = new Date(CUTOFF_DATE).getTime();
12
- const date = new Date(proofDate).getTime();
13
- return date < cutoff ? 'LAST_GEN' : 'AI_NATIVE';
14
- }
15
-
16
- export function resolveProofDate(user: GitHubUser, firstCommit: FirstCommit | null): string {
17
- if (!firstCommit) {
18
- return new Date().toISOString();
19
- }
20
-
21
- const effectiveDate = getEffectiveCommitDate(firstCommit);
22
- const commitTime = new Date(effectiveDate).getTime();
23
- const accountTime = new Date(user.createdAt).getTime();
24
- const repoTime = firstCommit.repoCreatedAt
25
- ? new Date(firstCommit.repoCreatedAt).getTime()
26
- : Infinity;
27
-
28
- if (commitTime < repoTime) {
29
- return firstCommit.repoCreatedAt ?? user.createdAt;
30
- }
31
-
32
- return commitTime < accountTime ? effectiveDate : user.createdAt;
33
- }
34
-
35
- function getEffectiveCommitDate(commit: FirstCommit): string {
36
- if (!commit.committerDate) {
37
- return commit.date;
38
- }
39
-
40
- const authorTime = new Date(commit.date).getTime();
41
- const committerTime = new Date(commit.committerDate).getTime();
42
-
43
- if (committerTime - authorTime > THIRTY_DAYS_MS) {
44
- return commit.committerDate;
45
- }
46
-
47
- return commit.date;
48
- }
49
-
50
- export async function generateCertificateHash(
51
- hashFn: HashFn,
52
- username: string,
53
- githubId: number,
54
- proofDate: string,
55
- era: EraKey,
56
- ): Promise<string> {
57
- const payload = JSON.stringify({
58
- username,
59
- githubId,
60
- proofDate,
61
- era,
62
- salt: CERTIFICATE_SALT,
63
- });
64
-
65
- return hashFn(payload);
66
- }
67
-
68
- export function generateCertificateNumber(hash: string): string {
69
- const prefix = hash.slice(0, 4).toUpperCase();
70
- const numericPart = parseInt(hash.slice(4, 12), 16) % 1000000;
71
- const padded = String(numericPart).padStart(6, '0');
72
- return `LGC-${prefix}-${padded}`;
73
- }
74
-
75
- export async function createCertificate(
76
- hashFn: HashFn,
77
- user: GitHubUser,
78
- firstCommit: FirstCommit | null,
79
- ): Promise<Certificate> {
80
- const proofDate = resolveProofDate(user, firstCommit);
81
- const era = classifyEra(proofDate);
82
- const hash = await generateCertificateHash(hashFn, user.login, user.id, proofDate, era);
83
- const certificateNumber = generateCertificateNumber(hash);
84
-
85
- return {
86
- version: CERTIFICATE_VERSION,
87
- type: 'LASTGEN_CERTIFICATE',
88
- identity: {
89
- username: user.login,
90
- githubId: user.id,
91
- name: user.name,
92
- },
93
- proof: {
94
- accountCreated: user.createdAt,
95
- firstCommit: firstCommit ?? {
96
- date: user.createdAt,
97
- repo: '',
98
- sha: '',
99
- message: '(no public commits found - using account creation date)',
100
- },
101
- proofDate,
102
- },
103
- era,
104
- verification: {
105
- hash: `sha256:${hash}`,
106
- salt: CERTIFICATE_SALT,
107
- },
108
- certificateNumber,
109
- issuedAt: new Date().toISOString(),
110
- };
111
- }
package/src/core/types.ts DELETED
@@ -1,89 +0,0 @@
1
- /**
2
- * @fileoverview Shared interfaces, constants, and type definitions for lastgen.
3
- */
4
-
5
- export const CUTOFF_DATE = '2025-02-21T00:00:00Z';
6
-
7
- export const ERAS = {
8
- LAST_GEN: {
9
- title: 'Last Generation Coder',
10
- description: 'Wrote code before AI agents shipped',
11
- },
12
- AI_NATIVE: {
13
- title: 'AI Native Coder',
14
- description: 'First verifiable commit after AI agents shipped',
15
- },
16
- } as const;
17
-
18
- export type EraKey = keyof typeof ERAS;
19
-
20
- export const CERTIFICATE_VERSION = '1.0';
21
- export const CERTIFICATE_SALT = 'lastgen_v1';
22
- export const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
23
-
24
- export interface GitHubUser {
25
- login: string;
26
- id: number;
27
- name: string | null;
28
- createdAt: string;
29
- }
30
-
31
- export interface FirstCommit {
32
- date: string;
33
- repo: string;
34
- sha: string;
35
- message: string;
36
- repoCreatedAt?: string;
37
- committerDate?: string;
38
- }
39
-
40
- export interface CertificateIdentity {
41
- username: string;
42
- githubId: number;
43
- name: string | null;
44
- }
45
-
46
- export interface CertificateProof {
47
- accountCreated: string;
48
- firstCommit: FirstCommit;
49
- proofDate: string;
50
- }
51
-
52
- export interface CertificateVerification {
53
- hash: string;
54
- salt: string;
55
- }
56
-
57
- export interface Certificate {
58
- version: string;
59
- type: 'LASTGEN_CERTIFICATE';
60
- identity: CertificateIdentity;
61
- proof: CertificateProof;
62
- era: EraKey;
63
- verification: CertificateVerification;
64
- certificateNumber: string;
65
- issuedAt: string;
66
- }
67
-
68
- export interface CommitDetail {
69
- sha: string;
70
- authorLogin: string | null;
71
- committerLogin: string | null;
72
- authorEmail: string | null;
73
- authorDate: string | null;
74
- committerDate: string | null;
75
- authorId: number | null;
76
- verificationReason: string | null;
77
- isRootCommit: boolean;
78
- message: string;
79
- verified: boolean;
80
- }
81
-
82
- export interface VerifyResult {
83
- check: string;
84
- passed: boolean;
85
- detail: string;
86
- }
87
-
88
- /** Platform-agnostic SHA-256 hash function. */
89
- export type HashFn = (data: string) => Promise<string>;