commitshow 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/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # commitshow
2
+
3
+ > Audit any vibe-coded project from your terminal.
4
+
5
+ `commitshow` pulls the latest [commit.show](https://commit.show) audit report for
6
+ a project and renders it inline — Audit / Scout / Community scores, 3 strengths
7
+ + 2 concerns, current season rank, delta since the last snapshot. Local runs
8
+ also save a `.commitshow/audit.md` file so your AI coding agent can read the
9
+ report in the next turn and iterate.
10
+
11
+ ```bash
12
+ npx commitshow audit
13
+ # or audit any public project by URL — no cd required
14
+ npx commitshow audit github.com/owner/repo
15
+ ```
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ # one-shot
21
+ npx commitshow audit <target>
22
+
23
+ # or global
24
+ npm i -g commitshow
25
+ commitshow audit <target>
26
+ ```
27
+
28
+ Node 20+.
29
+
30
+ ## Usage
31
+
32
+ | Command | What it does |
33
+ |---|---|
34
+ | `commitshow audit [target]` | Fetch + render the latest audit, write `.commitshow/audit.md` in local mode |
35
+ | `commitshow status [target]` | Same render, no re-run |
36
+ | `commitshow submit [target]` | Audition a project (coming soon · needs login) |
37
+ | `commitshow install <pack>` | Install a Library artifact (coming soon) |
38
+ | `commitshow login` | Device-flow sign-in (coming soon) |
39
+ | `commitshow whoami` | Print the linked account |
40
+
41
+ ### Target forms
42
+
43
+ `audit` and `status` accept a positional target that auto-detects:
44
+
45
+ | Form | Example |
46
+ |---|---|
47
+ | cwd (omitted) | `commitshow audit` · infers from `git remote get-url origin` |
48
+ | Local path | `commitshow audit ./my-repo` |
49
+ | Remote URL | `commitshow audit github.com/owner/repo` · `commitshow audit https://github.com/owner/repo` |
50
+ | SSH remote | `commitshow audit git@github.com:owner/repo.git` (auto-converted) |
51
+ | Shorthand | `commitshow audit owner/repo` |
52
+
53
+ Remote-URL mode works from any directory, which makes one-line X posts
54
+ (`npx commitshow audit <their-url>`) trivial.
55
+
56
+ ## The AI-coding loop
57
+
58
+ `commitshow audit` in local mode writes to `.commitshow/audit.md` **and**
59
+ `.commitshow/audit.json` after every run. Point your coding agent at them
60
+ and it picks up exactly what the audit flagged, with no prompt engineering:
61
+
62
+ ```
63
+ You are pairing on <repo>. Read .commitshow/audit.md before each turn.
64
+ Pick the top concern and propose a minimal change; I'll run
65
+ `commitshow audit` again to check the delta.
66
+ ```
67
+
68
+ ## For agents: `--json`
69
+
70
+ `commitshow` is built on a simple idea — **CLI + stable JSON is the universal
71
+ contract** between agent ecosystems. No SDK, no MCP server, no vendor lock.
72
+ Any agent that can shell out to a subprocess can use commit.show.
73
+
74
+ ```bash
75
+ # Human
76
+ commitshow audit github.com/owner/repo
77
+
78
+ # Agent
79
+ commitshow audit github.com/owner/repo --json | jq '.concerns[].bullet'
80
+ ```
81
+
82
+ ### Example agent workflow
83
+
84
+ > "Check my commit.show score and fix anything under 80."
85
+
86
+ ```
87
+ score=$(commitshow audit --json | jq '.score.total')
88
+ if [ "$score" -lt 80 ]; then
89
+ commitshow audit --json | jq -r '.concerns[0].bullet'
90
+ # → agent reads this concern, picks a fix, applies edits, re-audits
91
+ fi
92
+ ```
93
+
94
+ ### JSON shape (v1 schema)
95
+
96
+ Stable by contract — additive fields don't bump `schema_version`; breaking
97
+ changes do. Known keys: `project`, `score`, `standing`, `strengths`, `concerns`,
98
+ `snapshot`. See `commitshow audit --json` output for the canonical example.
99
+
100
+ ### Works with
101
+
102
+ - **Claude Code**, **Cursor**, **Windsurf** — any agent with shell access
103
+ - **GitHub Actions** — gate PRs on score band or axis scores
104
+ - **n8n / Zapier** — trigger workflows when scores move
105
+ - **AutoGPT / crewAI / LangChain** — subprocess tool node
106
+ - **Your own script** — 10 lines of bash + jq is the whole integration
107
+
108
+ ## What's in the report
109
+
110
+ - **Score** · total out of 100, colored by threshold (teal ≥ 75 · gold 50–74 · scarlet < 50)
111
+ - **3-axis bars** · Audit / Scout / Community
112
+ - **3 strengths + 2 concerns** · asymmetric by design — concerns don't dominate
113
+ - **Rank + projected tier** · where you stand in the current season
114
+ - **Δ** · movement since the parent snapshot
115
+
116
+ ## Roadmap
117
+
118
+ - `0.1` — ✓ read-only audit · status · `--json` · target auto-detect · sidecar files
119
+ - `0.2` — device-flow login · `commitshow submit` · `--watch` mode · CI exit-code gate
120
+ - `0.3` — `commitshow install <pack>` with {{VARIABLE}} substitution
121
+ - `0.4` — MCP server variant (Cursor / Claude Desktop can call commit.show tools directly · §15-C.6)
122
+
123
+ ## Links
124
+
125
+ - Home: <https://commit.show>
126
+ - Source: <https://github.com/hans1329/vibe/tree/main/packages/cli>
127
+ - Issues: <https://github.com/hans1329/vibe/issues>
128
+
129
+ MIT © 2026 commit.show
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js').then(m => m.main(process.argv.slice(2))).catch(err => {
3
+ console.error(err?.message ?? err)
4
+ process.exit(1)
5
+ })
@@ -0,0 +1,115 @@
1
+ import { resolveTarget, TargetError } from '../lib/target.js';
2
+ import { findProjectByGithubUrl, fetchLatestSnapshot, fetchStanding, runPreviewAudit, waitForPreviewSnapshot, } from '../lib/api.js';
3
+ import { renderAudit, renderMarkdown, renderJson, renderUpsell, writeAuditMarkdown, writeAuditJson, } from '../lib/render.js';
4
+ import { c } from '../lib/colors.js';
5
+ export async function audit(args) {
6
+ const asJson = args.includes('--json');
7
+ const positional = args.find(a => !a.startsWith('--'));
8
+ let target;
9
+ try {
10
+ target = resolveTarget(positional);
11
+ }
12
+ catch (err) {
13
+ if (err instanceof TargetError) {
14
+ emitError(asJson, 'bad_target', err.message, positional);
15
+ return 2;
16
+ }
17
+ throw err;
18
+ }
19
+ if (!asJson)
20
+ console.log(c.dim(`Auditing ${target.slug}…`));
21
+ // 1. Try cached/registered flow first — avoid re-running Claude if we
22
+ // already have the snapshot. Covers all full-audition projects.
23
+ const project = await findProjectByGithubUrl(target.github_url);
24
+ if (project) {
25
+ const [snapshot, standing] = await Promise.all([
26
+ fetchLatestSnapshot(project.id),
27
+ fetchStanding(project.id),
28
+ ]);
29
+ const view = { project, snapshot, standing };
30
+ if (asJson) {
31
+ process.stdout.write(renderJson(view) + '\n');
32
+ }
33
+ else {
34
+ console.log('');
35
+ console.log(renderAudit(view));
36
+ if (project.status === 'preview') {
37
+ console.log('');
38
+ console.log(renderUpsell());
39
+ }
40
+ console.log('');
41
+ }
42
+ if (target.kind === 'local') {
43
+ const mdPath = writeAuditMarkdown(target.localPath, renderMarkdown(view));
44
+ const jsonPath = writeAuditJson(target.localPath, renderJson(view));
45
+ if (!asJson) {
46
+ if (mdPath)
47
+ console.log(c.dim(` Saved → ${mdPath}`));
48
+ if (jsonPath)
49
+ console.log(c.dim(` Saved → ${jsonPath}`));
50
+ }
51
+ }
52
+ return 0;
53
+ }
54
+ // 2. Unregistered repo — kick off a preview audit. Full Claude depth,
55
+ // no season entry. Rate-limited server-side.
56
+ if (!asJson)
57
+ console.log(c.dim('First time on commit.show for this repo — running a preview audit…'));
58
+ const result = await runPreviewAudit(target.github_url);
59
+ // Error envelope
60
+ if ('error' in result) {
61
+ const err = result;
62
+ if (err.error === 'rate_limited') {
63
+ emitError(asJson, 'rate_limited', err.message ?? 'Rate limit hit. Try again tomorrow or sign in.', target.github_url);
64
+ return 1;
65
+ }
66
+ emitError(asJson, err.error, err.message ?? 'Preview audit failed.', target.github_url);
67
+ return 1;
68
+ }
69
+ // Background job — poll until the snapshot lands.
70
+ let envelope;
71
+ if ('status' in result && result.status === 'running') {
72
+ const pending = result;
73
+ if (!asJson)
74
+ console.log(c.dim(' This runs the full Claude audit · ~60-90 seconds. Hang tight.'));
75
+ const waited = await waitForPreviewSnapshot(pending.project_id);
76
+ if (!waited) {
77
+ emitError(asJson, 'timeout', 'Preview audit is taking longer than expected. Try `commitshow status <repo>` in a minute.', target.github_url);
78
+ return 1;
79
+ }
80
+ envelope = waited;
81
+ }
82
+ else {
83
+ envelope = result;
84
+ }
85
+ const view = { project: envelope.project, snapshot: envelope.snapshot, standing: null };
86
+ if (asJson) {
87
+ process.stdout.write(renderJson(view) + '\n');
88
+ }
89
+ else {
90
+ console.log('');
91
+ console.log(renderAudit(view));
92
+ console.log('');
93
+ console.log(renderUpsell());
94
+ console.log('');
95
+ }
96
+ if (target.kind === 'local') {
97
+ const mdPath = writeAuditMarkdown(target.localPath, renderMarkdown(view));
98
+ const jsonPath = writeAuditJson(target.localPath, renderJson(view));
99
+ if (!asJson) {
100
+ if (mdPath)
101
+ console.log(c.dim(` Saved → ${mdPath}`));
102
+ if (jsonPath)
103
+ console.log(c.dim(` Saved → ${jsonPath}`));
104
+ }
105
+ }
106
+ return 0;
107
+ }
108
+ function emitError(asJson, code, message, target) {
109
+ if (asJson) {
110
+ process.stdout.write(JSON.stringify({ error: code, message, target: target ?? null }) + '\n');
111
+ }
112
+ else {
113
+ console.error(c.scarlet(message));
114
+ }
115
+ }
@@ -0,0 +1,11 @@
1
+ import { c } from '../lib/colors.js';
2
+ export async function install(_args) {
3
+ console.log('');
4
+ console.log(c.cream('Install is not yet available in CLI 0.1.'));
5
+ console.log('');
6
+ console.log(c.muted(' `commitshow install <pack>` will write MCP / IDE rule files directly into'));
7
+ console.log(c.muted(' the cwd. Requires login + GitHub OAuth for Apply-to-my-repo PRs.'));
8
+ console.log('');
9
+ console.log(c.dim(' Use Apply-to-my-repo on the web → https://commit.show/library'));
10
+ return 1;
11
+ }
@@ -0,0 +1,16 @@
1
+ // Device flow placeholder. Needs the /cli/link web page + token-exchange
2
+ // Edge Function on the server side (V1 backend work · see CLAUDE.md §15-C.4
3
+ // rollout). Until that lands, we fail fast with a clear message so the user
4
+ // knows their audit/status commands still work read-only.
5
+ import { c } from '../lib/colors.js';
6
+ export async function login(_args) {
7
+ console.log('');
8
+ console.log(c.cream('Login is not yet available in 0.1.'));
9
+ console.log('');
10
+ console.log(c.muted(' Read-only commands (audit, status, whoami) already work against public data.'));
11
+ console.log(c.muted(' Write commands (submit, re-audit, install) unlock once the device-flow'));
12
+ console.log(c.muted(' endpoint ships — tracked as a V1 item in the roadmap.'));
13
+ console.log('');
14
+ console.log(c.dim(' Sign in on the web for now → https://commit.show'));
15
+ return 1;
16
+ }
@@ -0,0 +1,48 @@
1
+ // Same render as `audit`, but never re-runs analysis — just reads the latest
2
+ // cached snapshot. Cheap · offline-ish · safe to poll. Honors --json the
3
+ // same way audit does.
4
+ import { resolveTarget, TargetError } from '../lib/target.js';
5
+ import { findProjectByGithubUrl, fetchLatestSnapshot, fetchStanding } from '../lib/api.js';
6
+ import { renderAudit, renderJson } from '../lib/render.js';
7
+ import { c } from '../lib/colors.js';
8
+ export async function status(args) {
9
+ const asJson = args.includes('--json');
10
+ const positional = args.find(a => !a.startsWith('--'));
11
+ let target;
12
+ try {
13
+ target = resolveTarget(positional);
14
+ }
15
+ catch (err) {
16
+ if (err instanceof TargetError) {
17
+ emitError(asJson, 'bad_target', err.message, positional);
18
+ return 2;
19
+ }
20
+ throw err;
21
+ }
22
+ const project = await findProjectByGithubUrl(target.github_url);
23
+ if (!project) {
24
+ emitError(asJson, 'not_found', `No audition yet for ${target.slug}.`, target.github_url);
25
+ return 1;
26
+ }
27
+ const [snapshot, standing] = await Promise.all([
28
+ fetchLatestSnapshot(project.id),
29
+ fetchStanding(project.id),
30
+ ]);
31
+ const view = { project, snapshot, standing };
32
+ if (asJson) {
33
+ process.stdout.write(renderJson(view) + '\n');
34
+ }
35
+ else {
36
+ console.log('');
37
+ console.log(renderAudit(view));
38
+ }
39
+ return 0;
40
+ }
41
+ function emitError(asJson, code, message, target) {
42
+ if (asJson) {
43
+ process.stdout.write(JSON.stringify({ error: code, message, target: target ?? null }) + '\n');
44
+ }
45
+ else {
46
+ console.error(c.scarlet(message));
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ import { c } from '../lib/colors.js';
2
+ export async function submit(_args) {
3
+ console.log('');
4
+ console.log(c.cream('Submit is not yet available in CLI 0.1.'));
5
+ console.log('');
6
+ console.log(c.muted(' Core Intent + screenshots + Brief upload needs login,'));
7
+ console.log(c.muted(' which ships alongside the device-flow endpoint.'));
8
+ console.log('');
9
+ console.log(c.dim(' Submit on the web for now → https://commit.show/submit'));
10
+ return 1;
11
+ }
@@ -0,0 +1,14 @@
1
+ import { readConfig } from '../lib/config.js';
2
+ import { c } from '../lib/colors.js';
3
+ export async function whoami(_args) {
4
+ const cfg = readConfig();
5
+ if (!cfg.token || !cfg.display_name) {
6
+ console.log(c.muted('Not signed in.'));
7
+ console.log(c.dim(' Read-only commands still work. Login coming in the next CLI release.'));
8
+ return 1;
9
+ }
10
+ console.log(c.cream(cfg.display_name));
11
+ if (cfg.member_id)
12
+ console.log(c.muted(` ${cfg.member_id}`));
13
+ return 0;
14
+ }
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ import { audit } from './commands/audit.js';
2
+ import { submit } from './commands/submit.js';
3
+ import { install } from './commands/install.js';
4
+ import { status } from './commands/status.js';
5
+ import { login } from './commands/login.js';
6
+ import { whoami } from './commands/whoami.js';
7
+ import { c } from './lib/colors.js';
8
+ const VERSION = '0.1.0';
9
+ const USAGE = `
10
+ ${c.bold(c.gold('commitshow'))} ${c.dim(`v${VERSION}`)} ${c.muted('—')} ${c.cream('audit any vibe-coded project from your terminal.')}
11
+
12
+ ${c.muted('USAGE')}
13
+ ${c.cream('commitshow')} ${c.gold('<command>')} [target] [flags]
14
+
15
+ ${c.muted('COMMANDS')}
16
+ ${c.gold('audit')} [target] run audit and render the report
17
+ ${c.gold('status')} [target] latest score, no re-run
18
+ ${c.gold('submit')} [target] audition a project (requires login · coming soon)
19
+ ${c.gold('install')} <pack> install a library artifact (coming soon)
20
+ ${c.gold('login')} device-flow sign-in (coming soon)
21
+ ${c.gold('whoami')} who am I signed in as
22
+
23
+ ${c.muted('FLAGS')}
24
+ ${c.gold('--json')} stable machine-readable output (for agents · CI · jq pipes)
25
+
26
+ ${c.muted('TARGET FORMS')} ${c.dim('(default: cwd)')}
27
+ ${c.cream('commitshow audit')} ${c.dim('# cwd · git remote origin')}
28
+ ${c.cream('commitshow audit ./my-repo')} ${c.dim('# local path')}
29
+ ${c.cream('commitshow audit github.com/owner/repo')} ${c.dim('# remote shorthand')}
30
+ ${c.cream('commitshow audit https://github.com/o/r')} ${c.dim('# full URL')}
31
+ ${c.cream('commitshow audit owner/repo')} ${c.dim('# last-ditch shorthand')}
32
+
33
+ ${c.muted('FOR AGENTS')}
34
+ ${c.cream('commitshow audit github.com/owner/repo --json | jq .concerns')}
35
+ ${c.dim(' → agent reads concerns, picks a fix target, applies edits, re-audits')}
36
+
37
+ ${c.muted('LEARN MORE')}
38
+ https://commit.show
39
+ `;
40
+ export async function main(argv) {
41
+ const [cmd, ...rest] = argv;
42
+ let code = 0;
43
+ try {
44
+ switch (cmd) {
45
+ case 'audit':
46
+ code = await audit(rest);
47
+ break;
48
+ case 'status':
49
+ code = await status(rest);
50
+ break;
51
+ case 'submit':
52
+ code = await submit(rest);
53
+ break;
54
+ case 'install':
55
+ code = await install(rest);
56
+ break;
57
+ case 'login':
58
+ code = await login(rest);
59
+ break;
60
+ case 'whoami':
61
+ code = await whoami(rest);
62
+ break;
63
+ case '-v':
64
+ case '--version':
65
+ console.log(VERSION);
66
+ code = 0;
67
+ break;
68
+ case undefined:
69
+ case '-h':
70
+ case '--help':
71
+ console.log(USAGE);
72
+ code = 0;
73
+ break;
74
+ default:
75
+ console.error(c.scarlet(`Unknown command: ${cmd}`));
76
+ console.error(USAGE);
77
+ code = 2;
78
+ }
79
+ }
80
+ catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ console.error(c.scarlet(`\n ${msg}`));
83
+ code = 1;
84
+ }
85
+ process.exit(code);
86
+ }
@@ -0,0 +1,111 @@
1
+ // Minimal Supabase REST client — no SDK, no dependencies.
2
+ //
3
+ // Design choice (CLAUDE.md §15-C.4): ship as a thin fetch wrapper to stay
4
+ // under the 1 MB bundle budget. We only hit PostgREST + Edge Functions, so
5
+ // the full @supabase/supabase-js isn't worth the weight.
6
+ //
7
+ // Anon-key access is enough for V0.1:
8
+ // · projects (public read)
9
+ // · analysis_snapshots (public read)
10
+ // · season_standings view (public read)
11
+ // Write paths (submit / link / install with PR) require a member JWT and
12
+ // will be enabled when the device-flow endpoint lands.
13
+ import { readConfig } from './config.js';
14
+ // Baked-in defaults for the official commit.show instance. Self-hosters can
15
+ // override via `base_url` in ~/.commitshow/config.json.
16
+ const DEFAULT_BASE_URL = 'https://tekemubwihsjdzittoqf.supabase.co';
17
+ const DEFAULT_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRla2VtdWJ3aWhzamR6aXR0b3FmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzQ1NzUsImV4cCI6MjA5MjAxMDU3NX0.n2K-3lFVvlXQx-bV9evdNRSQCtG5oC4uQushxB2ja9Y';
18
+ function baseUrl() {
19
+ return readConfig().base_url ?? DEFAULT_BASE_URL;
20
+ }
21
+ function headers(extra = {}) {
22
+ const cfg = readConfig();
23
+ return {
24
+ apikey: DEFAULT_ANON_KEY,
25
+ Authorization: `Bearer ${cfg.token ?? DEFAULT_ANON_KEY}`,
26
+ 'Content-Type': 'application/json',
27
+ ...extra,
28
+ };
29
+ }
30
+ async function rest(path, init = {}) {
31
+ const res = await fetch(`${baseUrl()}/rest/v1${path}`, {
32
+ ...init,
33
+ headers: { ...headers(), ...(init.headers ?? {}) },
34
+ });
35
+ if (!res.ok)
36
+ throw new Error(`API ${res.status} on ${path}: ${await res.text()}`);
37
+ return res.json();
38
+ }
39
+ // ── Lookups ──────────────────────────────────────────────────────────
40
+ // PostgREST rejects `SELECT *` when anon has per-column grants (our email-privacy
41
+ // migration · §18.2). We enumerate the public columns we actually render.
42
+ const PROJECT_COLS = 'id,project_name,github_url,live_url,score_total,score_auto,score_forecast,' +
43
+ 'score_community,status,creator_name,creator_grade,last_analysis_at';
44
+ /** Find a project by its canonical GitHub URL. Returns the first match or null. */
45
+ export async function findProjectByGithubUrl(url) {
46
+ // DB stores some URLs with .git or trailing slash · match loosely with ilike on owner/repo.
47
+ const canonical = url.replace(/\.git$/, '').replace(/\/+$/, '');
48
+ const ilikePattern = `${canonical.replace(/\*/g, '')}%`;
49
+ const rows = await rest(`/projects?select=${PROJECT_COLS}&github_url=ilike.${encodeURIComponent(ilikePattern)}&limit=1`);
50
+ return rows[0] ?? null;
51
+ }
52
+ export async function findProjectById(id) {
53
+ const rows = await rest(`/projects?select=${PROJECT_COLS}&id=eq.${id}&limit=1`);
54
+ return rows[0] ?? null;
55
+ }
56
+ export async function fetchLatestSnapshot(projectId) {
57
+ const rows = await rest(`/analysis_snapshots?project_id=eq.${projectId}&order=created_at.desc&limit=1`);
58
+ return rows[0] ?? null;
59
+ }
60
+ export async function fetchStanding(projectId) {
61
+ const rows = await rest(`/season_standings?project_id=eq.${projectId}&limit=1`);
62
+ return rows[0] ?? null;
63
+ }
64
+ /** Kicks off (or returns cached) a preview audit. 202 → poll; 200 → done. */
65
+ export async function runPreviewAudit(githubUrl, liveUrl) {
66
+ const res = await fetch(`${baseUrl()}/functions/v1/audit-preview`, {
67
+ method: 'POST',
68
+ headers: headers(),
69
+ body: JSON.stringify({ github_url: githubUrl, live_url: liveUrl }),
70
+ });
71
+ const body = await res.json().catch(() => ({ error: 'invalid_json' }));
72
+ if (res.status === 202)
73
+ return body;
74
+ if (!res.ok)
75
+ return body;
76
+ return body;
77
+ }
78
+ /** Poll a preview job until the snapshot lands or we time out. */
79
+ export async function waitForPreviewSnapshot(projectId, timeoutMs = 180_000, intervalMs = 4_000) {
80
+ const deadline = Date.now() + timeoutMs;
81
+ while (Date.now() < deadline) {
82
+ const project = await findProjectById(projectId);
83
+ if (project && project.last_analysis_at) {
84
+ const snapshot = await fetchLatestSnapshot(projectId);
85
+ return {
86
+ project,
87
+ snapshot,
88
+ standing: null,
89
+ is_preview: project.status === 'preview',
90
+ cache_hit: false,
91
+ };
92
+ }
93
+ await new Promise(r => setTimeout(r, intervalMs));
94
+ }
95
+ return null;
96
+ }
97
+ // ── Trigger reaudit (requires member token · V1 backend) ─────────────
98
+ export async function triggerReaudit(projectId) {
99
+ const cfg = readConfig();
100
+ if (!cfg.token) {
101
+ throw new Error('Re-audit requires login. Run `commitshow login` once device-flow is available.');
102
+ }
103
+ const res = await fetch(`${baseUrl()}/functions/v1/analyze-project`, {
104
+ method: 'POST',
105
+ headers: headers(),
106
+ body: JSON.stringify({ project_id: projectId, trigger_type: 'resubmit' }),
107
+ });
108
+ if (!res.ok)
109
+ throw new Error(`Re-audit failed: ${res.status} · ${await res.text()}`);
110
+ return res.json();
111
+ }
@@ -0,0 +1,38 @@
1
+ // Brand palette — mirrors src/index.css tokens.
2
+ // Uses kleur (no dependencies) for terminal coloring with graceful fallback.
3
+ import kleur from 'kleur';
4
+ // Hex reference (for 24-bit terminals):
5
+ // navy-900 #060C1A gold-500 #F0C040 cream #F8F5EE
6
+ // teal-500 #00D4AA scarlet #C8102E muted #6B7280
7
+ // True-color escapes — kleur doesn't expose setRgb, so we wrap manually.
8
+ // Terminals without truecolor still see readable ANSI (dim fallback chain).
9
+ function rgb(r, g, b) {
10
+ return (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`;
11
+ }
12
+ export const c = {
13
+ gold: rgb(0xF0, 0xC0, 0x40),
14
+ cream: rgb(0xF8, 0xF5, 0xEE),
15
+ teal: rgb(0x00, 0xD4, 0xAA),
16
+ scarlet: rgb(0xC8, 0x10, 0x2E),
17
+ muted: rgb(0x6B, 0x72, 0x80),
18
+ blue: rgb(0x60, 0xA5, 0xFA),
19
+ violet: rgb(0xA7, 0x8B, 0xFA),
20
+ dim: kleur.dim,
21
+ bold: kleur.bold,
22
+ };
23
+ /** Pick a color based on a 0–100 score, matching web UI thresholds. */
24
+ export function scoreTone(score) {
25
+ if (score >= 75)
26
+ return c.teal;
27
+ if (score >= 50)
28
+ return c.gold;
29
+ return c.scarlet;
30
+ }
31
+ /** Pick a color for delta (+/- vs parent snapshot). */
32
+ export function deltaTone(delta) {
33
+ if (delta > 0)
34
+ return c.teal;
35
+ if (delta < 0)
36
+ return c.scarlet;
37
+ return c.muted;
38
+ }
@@ -0,0 +1,29 @@
1
+ // Local CLI config — ~/.commitshow/config.json
2
+ //
3
+ // Holds the Supabase JWT once login lands (V1 backend work). For now the
4
+ // file mostly stores cached preferences, but we keep the path stable so the
5
+ // device-flow (when added) can drop tokens without migration.
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ const CONFIG_DIR = join(homedir(), '.commitshow');
10
+ const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
11
+ export function readConfig() {
12
+ if (!existsSync(CONFIG_PATH))
13
+ return {};
14
+ try {
15
+ const raw = readFileSync(CONFIG_PATH, 'utf8');
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ export function writeConfig(next) {
23
+ if (!existsSync(CONFIG_DIR))
24
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
25
+ writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2), { mode: 0o600 });
26
+ }
27
+ export function clearConfig() {
28
+ writeConfig({});
29
+ }
@@ -0,0 +1,307 @@
1
+ // Terminal render — the canonical output spec from CLAUDE.md §15-C.2.
2
+ //
3
+ // Two output modes:
4
+ // · Pretty (default) — fixed-width 58-col terminal screenshot layout with
5
+ // ANSI colors, brand palette, strengths/concerns box. Human target.
6
+ // · JSON (`--json`) — stable machine-readable shape with `schema_version`.
7
+ // Agent target: Claude Code, AutoGPT, n8n, Zapier, GitHub Actions, etc.
8
+ // The JSON shape is the universal contract — no SDK or MCP needed to
9
+ // integrate. Agent workflow: shell out to commitshow, pipe to jq, act.
10
+ //
11
+ // Both modes share:
12
+ // · 3 strengths + 2 concerns asymmetry (§15-C.2 content contract)
13
+ // · self-delta only (no peer-vs-peer drama in V0.1)
14
+ // · `.commitshow/audit.{md,json}` side-effect for AI re-read loop
15
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { c, scoreTone, deltaTone } from './colors.js';
18
+ const BAR_WIDTH = 20;
19
+ /** Single filled/empty bar for a 0..max score. */
20
+ function scoreBar(value, max) {
21
+ const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round((value / max) * BAR_WIDTH)));
22
+ const empty = BAR_WIDTH - filled;
23
+ const tone = scoreTone(Math.round((value / max) * 100));
24
+ return tone('▰'.repeat(filled)) + c.muted('▱'.repeat(empty));
25
+ }
26
+ function pad(s, w) {
27
+ return s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length);
28
+ }
29
+ function centerPad(s, w) {
30
+ if (s.length >= w)
31
+ return s.slice(0, w);
32
+ const total = w - s.length;
33
+ const left = Math.floor(total / 2);
34
+ return ' '.repeat(left) + s + ' '.repeat(total - left);
35
+ }
36
+ /** Strip ANSI for width math on colored strings. */
37
+ function visibleLength(s) {
38
+ // eslint-disable-next-line no-control-regex
39
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
40
+ }
41
+ function asStringArray(raw, take) {
42
+ if (!Array.isArray(raw))
43
+ return [];
44
+ return raw.slice(0, take).map(row => {
45
+ if (typeof row === 'string')
46
+ return row;
47
+ if (row && typeof row === 'object') {
48
+ const r = row;
49
+ return String(r.bullet ?? r.finding ?? r.text ?? r.summary ?? r.title ?? '');
50
+ }
51
+ return '';
52
+ }).filter(Boolean);
53
+ }
54
+ export function renderAudit(view) {
55
+ const { project: p, snapshot, standing } = view;
56
+ const total = p.score_total ?? 0;
57
+ // Header
58
+ const bar = '─'.repeat(58);
59
+ const lines = [];
60
+ lines.push(c.muted('┌' + bar + '┐'));
61
+ lines.push(c.muted('│ ') + c.bold(c.gold('commit.show')) + c.muted(' · ') + c.cream('Audit report') + ' '.repeat(58 - 29) + c.muted('│'));
62
+ lines.push(c.muted('└' + bar + '┘'));
63
+ lines.push('');
64
+ // Project title line
65
+ const name = p.project_name ?? 'untitled';
66
+ const slug = p.github_url?.replace(/^https?:\/\//, '') ?? '';
67
+ lines.push(' ' + c.bold(c.cream(name)) + ' ' + c.muted(slug));
68
+ lines.push('');
69
+ // Hero score card
70
+ const scoreText = `${total} / 100`;
71
+ const scoreTinted = scoreTone(total)(scoreText);
72
+ const cardW = 14;
73
+ lines.push(' ' + ' '.repeat(20) + c.muted('╔' + '═'.repeat(cardW) + '╗'));
74
+ lines.push(' ' + ' '.repeat(20) + c.muted('║') + centerPadAnsi(scoreTinted, cardW) + c.muted('║'));
75
+ lines.push(' ' + ' '.repeat(20) + c.muted('╚' + '═'.repeat(cardW) + '╝'));
76
+ lines.push('');
77
+ // 3-axis bars
78
+ const auditLine = ` Audit ${pad(`${p.score_auto}/50`, 7)} ${scoreBar(p.score_auto, 50)}`;
79
+ const scoutLine = ` Scout ${pad(`${p.score_forecast}/30`, 7)} ${scoreBar(p.score_forecast, 30)}`;
80
+ const communityLine = ` Comm. ${pad(`${p.score_community}/20`, 7)} ${scoreBar(p.score_community, 20)}`;
81
+ lines.push(' ' + auditLine);
82
+ lines.push(' ' + scoutLine);
83
+ lines.push(' ' + communityLine);
84
+ lines.push('');
85
+ // 3 strengths + 2 concerns from scout_brief · §15-C.2 content contract.
86
+ // Web surfaces the full 5+3; the CLI keeps it tight for terminal screenshots.
87
+ const strengths = asStringArray(snapshot?.rich_analysis?.scout_brief?.strengths, 3);
88
+ const concerns = asStringArray(snapshot?.rich_analysis?.scout_brief?.weaknesses, 2);
89
+ if (strengths.length > 0 || concerns.length > 0) {
90
+ lines.push(' ' + c.muted('┌' + '─'.repeat(56) + '┐'));
91
+ for (const s of strengths) {
92
+ lines.push(' ' + c.muted('│ ') + c.teal('↑ ') + truncate(s, 52) + fill(s, 52) + c.muted(' │'));
93
+ }
94
+ for (const s of concerns) {
95
+ lines.push(' ' + c.muted('│ ') + c.scarlet('↓ ') + truncate(s, 52) + fill(s, 52) + c.muted(' │'));
96
+ }
97
+ lines.push(' ' + c.muted('└' + '─'.repeat(56) + '┘'));
98
+ lines.push('');
99
+ }
100
+ // Standings + delta
101
+ if (standing) {
102
+ const rank = `#${standing.rank} of ${standing.total_in_season}`;
103
+ const tier = standing.projected_tier ?? '—';
104
+ const pct = `(top ${Math.max(1, Math.round(standing.percentile))}%)`;
105
+ lines.push(' ' + c.muted('Ranked ') + c.cream(pad(rank, 12)) + c.muted(` season`));
106
+ lines.push(' ' + c.muted('Tier ') + c.gold(pad(tier, 12)) + c.muted(` ${pct}`));
107
+ }
108
+ if (snapshot?.score_total_delta != null && snapshot.score_total_delta !== 0) {
109
+ const d = snapshot.score_total_delta;
110
+ const sign = d > 0 ? '+' : '';
111
+ lines.push(' ' + c.muted('Δ ') + deltaTone(d)(pad(`${sign}${d}`, 12)) + c.muted(` since last audit`));
112
+ }
113
+ lines.push('');
114
+ // Footer URLs
115
+ const url = `https://commit.show/projects/${p.id}`;
116
+ lines.push(' ' + c.muted('→ ') + c.cream(url));
117
+ const footerPad = Math.max(0, 58 - 'commit.show'.length - 2);
118
+ lines.push(' '.repeat(footerPad) + c.gold('commit.show'));
119
+ return lines.join('\n');
120
+ }
121
+ function truncate(s, w) {
122
+ const vl = s.length;
123
+ if (vl <= w)
124
+ return s;
125
+ return s.slice(0, w - 1) + '…';
126
+ }
127
+ function fill(s, w) {
128
+ return ' '.repeat(Math.max(0, w - Math.min(w, s.length)));
129
+ }
130
+ function centerPadAnsi(s, w) {
131
+ const vl = visibleLength(s).length;
132
+ if (vl >= w)
133
+ return s;
134
+ const total = w - vl;
135
+ const left = Math.floor(total / 2);
136
+ return ' '.repeat(left) + s + ' '.repeat(total - left);
137
+ }
138
+ // ─────────── Markdown sibling for .commitshow/audit.md ───────────
139
+ export function renderMarkdown(view) {
140
+ const { project: p, snapshot, standing } = view;
141
+ const strengths = asStringArray(snapshot?.rich_analysis?.scout_brief?.strengths, 3);
142
+ const concerns = asStringArray(snapshot?.rich_analysis?.scout_brief?.weaknesses, 2);
143
+ const delta = snapshot?.score_total_delta;
144
+ const lines = [];
145
+ lines.push(`# commit.show · Audit report`);
146
+ lines.push('');
147
+ lines.push(`**${p.project_name}**`);
148
+ if (p.github_url)
149
+ lines.push(`_${p.github_url}_`);
150
+ lines.push('');
151
+ lines.push(`## Score · ${p.score_total} / 100`);
152
+ lines.push('');
153
+ lines.push(`- Audit: ${p.score_auto}/50`);
154
+ lines.push(`- Scout: ${p.score_forecast}/30`);
155
+ lines.push(`- Community: ${p.score_community}/20`);
156
+ if (delta != null && delta !== 0) {
157
+ lines.push(`- **Δ ${delta > 0 ? '+' : ''}${delta}** since last audit`);
158
+ }
159
+ if (standing) {
160
+ lines.push(`- Ranked #${standing.rank} of ${standing.total_in_season} — projected **${standing.projected_tier ?? '—'}** (top ${Math.round(standing.percentile)}%)`);
161
+ }
162
+ lines.push('');
163
+ if (strengths.length > 0) {
164
+ lines.push(`## Strengths`);
165
+ for (const s of strengths)
166
+ lines.push(`- ${s}`);
167
+ lines.push('');
168
+ }
169
+ if (concerns.length > 0) {
170
+ lines.push(`## Concerns`);
171
+ for (const s of concerns)
172
+ lines.push(`- ${s}`);
173
+ lines.push('');
174
+ }
175
+ lines.push(`---`);
176
+ lines.push(`Auditioned on commit.show · https://commit.show/projects/${p.id}`);
177
+ lines.push('');
178
+ return lines.join('\n');
179
+ }
180
+ /** Persist markdown side-effect to .commitshow/audit.md in the target dir. */
181
+ export function writeAuditMarkdown(dir, md) {
182
+ if (!dir)
183
+ return null;
184
+ try {
185
+ const cshow = join(dir, '.commitshow');
186
+ if (!existsSync(cshow))
187
+ mkdirSync(cshow, { recursive: true });
188
+ const path = join(cshow, 'audit.md');
189
+ writeFileSync(path, md, 'utf8');
190
+ return path;
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
196
+ function bandFor(score) {
197
+ if (score >= 75)
198
+ return 'strong';
199
+ if (score >= 50)
200
+ return 'mid';
201
+ return 'weak';
202
+ }
203
+ function asObjectArray(raw, take) {
204
+ if (!Array.isArray(raw))
205
+ return [];
206
+ const out = [];
207
+ for (const row of raw.slice(0, take)) {
208
+ if (typeof row === 'string' && row.trim().length > 0) {
209
+ out.push({ axis: null, bullet: row });
210
+ }
211
+ else if (row && typeof row === 'object') {
212
+ const r = row;
213
+ const bullet = String(r.bullet ?? r.finding ?? r.text ?? r.summary ?? r.title ?? '').trim();
214
+ if (bullet)
215
+ out.push({ axis: r.axis ?? null, bullet });
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+ export function toAgentShape(view) {
221
+ const { project: p, snapshot, standing } = view;
222
+ return {
223
+ schema_version: '1',
224
+ generated_at: new Date().toISOString(),
225
+ project: {
226
+ id: p.id,
227
+ name: p.project_name,
228
+ github_url: p.github_url,
229
+ live_url: p.live_url,
230
+ status: p.status,
231
+ creator: { name: p.creator_name, grade: p.creator_grade },
232
+ url: `https://commit.show/projects/${p.id}`,
233
+ },
234
+ score: {
235
+ total: p.score_total,
236
+ total_max: 100,
237
+ audit: p.score_auto,
238
+ audit_max: 50,
239
+ scout: p.score_forecast,
240
+ scout_max: 30,
241
+ community: p.score_community,
242
+ community_max: 20,
243
+ delta_since_last: snapshot?.score_total_delta ?? null,
244
+ band: bandFor(p.score_total),
245
+ },
246
+ standing: standing
247
+ ? {
248
+ rank: standing.rank,
249
+ total_in_season: standing.total_in_season,
250
+ percentile: standing.percentile,
251
+ projected_tier: standing.projected_tier,
252
+ live_url_ok: standing.live_url_ok ?? null,
253
+ snapshots_ok: standing.snapshots_ok ?? null,
254
+ brief_ok: standing.brief_ok ?? null,
255
+ }
256
+ : null,
257
+ strengths: asObjectArray(snapshot?.rich_analysis?.scout_brief?.strengths, 3),
258
+ concerns: asObjectArray(snapshot?.rich_analysis?.scout_brief?.weaknesses, 2),
259
+ snapshot: snapshot
260
+ ? { id: snapshot.id, created_at: snapshot.created_at, trigger_type: snapshot.trigger_type }
261
+ : null,
262
+ };
263
+ }
264
+ export function renderJson(view) {
265
+ return JSON.stringify(toAgentShape(view), null, 2);
266
+ }
267
+ // ── Upsell panel (CLI-only · appended to preview audits) ─────────────
268
+ // Shown after a preview audit render so Creator sees what a real audition
269
+ // unlocks. Intentionally NOT shown for registered projects — they already
270
+ // have access to everything listed here.
271
+ export function renderUpsell() {
272
+ const lines = [];
273
+ const bar = '─'.repeat(58);
274
+ lines.push(' ' + c.muted('┌' + bar + '┐'));
275
+ lines.push(' ' + c.muted('│ ') + c.bold(c.gold('Preview')) + c.muted(' · ') + c.cream('not entered in the season') + ' '.repeat(58 - 35) + c.muted('│'));
276
+ lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
277
+ lines.push(' ' + c.muted('│ ') + c.cream('Audition to unlock:') + ' '.repeat(58 - 20) + c.muted('│'));
278
+ const items = [
279
+ 'Scout forecasts · human verdicts (30% of score)',
280
+ 'Season ranking · top 20% graduate each 3-week cycle',
281
+ 'Hall of Fame · permanent archive + public badge',
282
+ 'Live applauds · notifications when reviewers react',
283
+ 'Recommit loop · weekly delta + trajectory share card',
284
+ ];
285
+ for (const it of items) {
286
+ lines.push(' ' + c.muted('│ ') + c.teal('→ ') + c.cream(pad(it, 54)) + c.muted('│'));
287
+ }
288
+ lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
289
+ lines.push(' ' + c.muted('│ ') + c.gold('→ https://commit.show/submit') + ' '.repeat(58 - 30) + c.muted('│'));
290
+ lines.push(' ' + c.muted('└' + bar + '┘'));
291
+ return lines.join('\n');
292
+ }
293
+ export function writeAuditJson(dir, json) {
294
+ if (!dir)
295
+ return null;
296
+ try {
297
+ const cshow = join(dir, '.commitshow');
298
+ if (!existsSync(cshow))
299
+ mkdirSync(cshow, { recursive: true });
300
+ const path = join(cshow, 'audit.json');
301
+ writeFileSync(path, json, 'utf8');
302
+ return path;
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ }
@@ -0,0 +1,87 @@
1
+ // Target detection — turns the CLI positional arg into a canonical
2
+ // { kind: 'remote-url', github_url } or { kind: 'local', path, github_url? }.
3
+ //
4
+ // Accepted inputs (all resolve to a GitHub HTTPS URL):
5
+ // · (omitted) → cwd · read `git remote get-url origin`
6
+ // · ./my-repo · /abs/path → local dir · same remote inference
7
+ // · github.com/owner/repo → bare host shorthand
8
+ // · https://github.com/owner/repo → full URL
9
+ // · git@github.com:owner/repo.git → ssh form (common in `git remote`)
10
+ // · owner/repo → last-ditch shorthand (2 segments, no dot)
11
+ import { execSync } from 'node:child_process';
12
+ import { existsSync, statSync } from 'node:fs';
13
+ import { resolve } from 'node:path';
14
+ export class TargetError extends Error {
15
+ }
16
+ const GITHUB_URL_RE = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i;
17
+ const GITHUB_HOST_RE = /^github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i;
18
+ const GITHUB_SSH_RE = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i;
19
+ const SLUG_RE = /^([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*)$/;
20
+ function canonical(owner, repo) {
21
+ return `https://github.com/${owner}/${repo.replace(/\.git$/, '')}`;
22
+ }
23
+ function matchUrl(raw) {
24
+ const s = raw.trim();
25
+ const urlMatch = s.match(GITHUB_URL_RE) ?? s.match(GITHUB_HOST_RE) ?? s.match(GITHUB_SSH_RE);
26
+ if (urlMatch)
27
+ return { owner: urlMatch[1], repo: urlMatch[2] };
28
+ const slug = s.match(SLUG_RE);
29
+ if (slug && !slug[2].includes('.'))
30
+ return { owner: slug[1], repo: slug[2] };
31
+ return null;
32
+ }
33
+ function gitRemoteOrigin(cwd) {
34
+ try {
35
+ const out = execSync('git remote get-url origin', {
36
+ cwd,
37
+ encoding: 'utf8',
38
+ stdio: ['ignore', 'pipe', 'ignore'],
39
+ }).trim();
40
+ if (!out)
41
+ return null;
42
+ // Strip embedded userinfo (credentials) before parsing — safer and matches
43
+ // how the canonical URL should look. e.g. https://x-access-token:ghp_…@github.com/foo/bar
44
+ return out.replace(/^(https?:\/\/)[^@\/]+@/, '$1');
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ export function resolveTarget(rawArg) {
51
+ // 1 · Explicit URL forms
52
+ if (rawArg) {
53
+ const m = matchUrl(rawArg);
54
+ if (m) {
55
+ return {
56
+ kind: 'remote-url',
57
+ github_url: canonical(m.owner, m.repo),
58
+ slug: `${m.owner}/${m.repo.replace(/\.git$/, '')}`,
59
+ };
60
+ }
61
+ }
62
+ // 2 · Local path (arg resolves to a directory) or cwd
63
+ const path = resolve(rawArg ?? '.');
64
+ if (!existsSync(path) || !statSync(path).isDirectory()) {
65
+ throw new TargetError(`Not a repo I can audit: "${rawArg ?? path}".\n` +
66
+ ` Expected: github URL, owner/repo shorthand, or a local git repo path.`);
67
+ }
68
+ const remote = gitRemoteOrigin(path);
69
+ if (!remote) {
70
+ throw new TargetError(`No git remote found in ${path}.\n` +
71
+ ` Either run this inside a git repo with a GitHub remote, or pass the URL directly:\n` +
72
+ ` commitshow audit github.com/owner/repo`);
73
+ }
74
+ const m = matchUrl(remote);
75
+ if (!m) {
76
+ // Don't echo `remote` — it may contain credentials. Ask for explicit target instead.
77
+ throw new TargetError(`Couldn't parse the git remote for ${path} as a GitHub repo.\n` +
78
+ ` commitshow currently supports GitHub-hosted projects only.\n` +
79
+ ` Try passing the URL directly: commitshow audit github.com/owner/repo`);
80
+ }
81
+ return {
82
+ kind: 'local',
83
+ github_url: canonical(m.owner, m.repo),
84
+ localPath: path,
85
+ slug: `${m.owner}/${m.repo.replace(/\.git$/, '')}`,
86
+ };
87
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "commitshow",
3
+ "version": "0.1.0",
4
+ "description": "commit.show CLI — audit any vibe-coded project from your terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "commitshow": "./bin/commitshow.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p .",
16
+ "dev": "tsc -p . --watch",
17
+ "prepare": "npm run build"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "keywords": [
23
+ "commit.show",
24
+ "audit",
25
+ "vibe-coding",
26
+ "cli"
27
+ ],
28
+ "author": "commit.show",
29
+ "license": "MIT",
30
+ "homepage": "https://commit.show",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/hans1329/vibe",
34
+ "directory": "packages/cli"
35
+ },
36
+ "dependencies": {
37
+ "kleur": "^4.1.5"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.19.39",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }