commitshow 0.3.24 → 0.3.26
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/dist/commands/install.js +187 -6
- package/dist/index.js +1 -1
- package/dist/lib/render.js +23 -8
- package/package.json +2 -2
package/dist/commands/install.js
CHANGED
|
@@ -1,11 +1,192 @@
|
|
|
1
|
+
// `commitshow install <slug>` — fetch a marketplace pack and run its
|
|
2
|
+
// installer in the current working directory.
|
|
3
|
+
//
|
|
4
|
+
// Flow:
|
|
5
|
+
// 1. Look up the listing by slug via PostgREST (md_library_feed)
|
|
6
|
+
// 2. Download the bundle .tar.gz from Storage public URL
|
|
7
|
+
// 3. Verify sha256 against the listing's bundle_sha256
|
|
8
|
+
// 4. Untar to ~/.commitshow/cache/<slug>-<version>/
|
|
9
|
+
// 5. Run scripts/install.sh from the untarred bundle, with $PWD set
|
|
10
|
+
// to the user's project directory. The installer itself prompts
|
|
11
|
+
// for inputs (osascript on macOS, stdin elsewhere) and runs ALL
|
|
12
|
+
// operations against the user's own credentials.
|
|
13
|
+
//
|
|
14
|
+
// commit.show is the bundle delivery channel only · no user secrets
|
|
15
|
+
// pass through any of our infrastructure.
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { createWriteStream, mkdtempSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
18
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
19
|
+
import { tmpdir, homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { Readable } from 'node:stream';
|
|
1
23
|
import { c } from '../lib/colors.js';
|
|
2
|
-
|
|
24
|
+
const PUBLIC_BASE = 'https://tekemubwihsjdzittoqf.supabase.co';
|
|
25
|
+
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRla2VtdWJ3aWhzamR6aXR0b3FmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzQ1NzUsImV4cCI6MjA5MjAxMDU3NX0.n2K-3lFVvlXQx-bV9evdNRSQCtG5oC4uQushxB2ja9Y';
|
|
26
|
+
async function fetchListing(slug) {
|
|
27
|
+
const url = `${PUBLIC_BASE}/rest/v1/md_library_feed`
|
|
28
|
+
+ `?slug=eq.${encodeURIComponent(slug)}`
|
|
29
|
+
+ `&select=id,slug,title,description,author_name,bundle_url,bundle_sha256,bundle_size_bytes,bundle_version,manifest_version,target_format`
|
|
30
|
+
+ `&limit=1`;
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
headers: {
|
|
33
|
+
apikey: ANON_KEY,
|
|
34
|
+
Authorization: `Bearer ${ANON_KEY}`,
|
|
35
|
+
Accept: 'application/json',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
throw new Error(`Library lookup failed (HTTP ${res.status})`);
|
|
40
|
+
}
|
|
41
|
+
const rows = (await res.json());
|
|
42
|
+
return rows[0] ?? null;
|
|
43
|
+
}
|
|
44
|
+
async function downloadBundle(url, dest, expectedSha) {
|
|
45
|
+
const res = await fetch(url);
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
throw new Error(`Bundle download failed (HTTP ${res.status})`);
|
|
48
|
+
if (!res.body)
|
|
49
|
+
throw new Error('Bundle download returned no body');
|
|
50
|
+
// Stream to disk while computing sha256 inline. Avoids holding the
|
|
51
|
+
// entire .tar.gz in memory, fast-fails on hash mismatch.
|
|
52
|
+
const out = createWriteStream(dest);
|
|
53
|
+
const hash = createHash('sha256');
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
const stream = Readable.fromWeb(res.body);
|
|
56
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
57
|
+
stream.on('error', reject);
|
|
58
|
+
out.on('error', reject);
|
|
59
|
+
out.on('finish', () => resolve());
|
|
60
|
+
stream.pipe(out);
|
|
61
|
+
});
|
|
62
|
+
const got = hash.digest('hex');
|
|
63
|
+
if (got !== expectedSha) {
|
|
64
|
+
throw new Error(`Bundle integrity check failed.\n expected: ${expectedSha}\n got: ${got}\n`
|
|
65
|
+
+ ` Refusing to run an install script with a mismatched hash. `
|
|
66
|
+
+ `Try again — if this persists report it to https://github.com/commitshow/cli/issues`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function untar(tarball, into) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
mkdirSync(into, { recursive: true });
|
|
72
|
+
const child = spawn('tar', ['-xzf', tarball, '-C', into], { stdio: 'inherit' });
|
|
73
|
+
child.on('error', reject);
|
|
74
|
+
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`tar exited ${code}`)));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function findInstallScript(extractRoot) {
|
|
78
|
+
// Bundles tar with the skill dir as the top-level entry, so after
|
|
79
|
+
// extraction we expect: <extractRoot>/<slug>/scripts/install.sh
|
|
80
|
+
// Handle both that shape and a flat shape where pack.yaml is at root.
|
|
81
|
+
const direct = join(extractRoot, 'scripts', 'install.sh');
|
|
82
|
+
if (existsSync(direct))
|
|
83
|
+
return direct;
|
|
84
|
+
// Find first child directory containing scripts/install.sh
|
|
85
|
+
for (const ent of readdirSync(extractRoot, { withFileTypes: true })) {
|
|
86
|
+
if (ent.isDirectory()) {
|
|
87
|
+
const cand = join(extractRoot, ent.name, 'scripts', 'install.sh');
|
|
88
|
+
if (existsSync(cand))
|
|
89
|
+
return cand;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function runInstallScript(scriptPath, projectDir) {
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
const child = spawn('bash', [scriptPath, projectDir], {
|
|
97
|
+
stdio: 'inherit',
|
|
98
|
+
env: { ...process.env },
|
|
99
|
+
});
|
|
100
|
+
child.on('exit', code => resolve(code ?? 1));
|
|
101
|
+
child.on('error', err => {
|
|
102
|
+
console.error(c.scarlet(` install.sh failed to launch: ${err.message}`));
|
|
103
|
+
resolve(1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export async function install(args) {
|
|
108
|
+
const slug = args.find(a => !a.startsWith('-'));
|
|
109
|
+
if (!slug) {
|
|
110
|
+
console.error(c.scarlet(' usage: commitshow install <slug>'));
|
|
111
|
+
console.error(c.muted(' e.g. commitshow install supabase-resend-auth'));
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
3
114
|
console.log('');
|
|
4
|
-
console.log(c.
|
|
115
|
+
console.log(c.muted(`→ Looking up `) + c.gold(slug) + c.muted(` in commit.show Library...`));
|
|
116
|
+
let listing;
|
|
117
|
+
try {
|
|
118
|
+
listing = await fetchListing(slug);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error(c.scarlet(` ${e.message}`));
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
if (!listing) {
|
|
125
|
+
console.error(c.scarlet(` No published pack with slug '${slug}'.`));
|
|
126
|
+
console.error(c.muted(` Browse https://commit.show/library to find the right slug.`));
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
if (!listing.bundle_url || !listing.bundle_sha256) {
|
|
130
|
+
console.error(c.scarlet(` '${slug}' is listed but has no installable bundle yet.`));
|
|
131
|
+
console.error(c.muted(` This pack may be content-only (Apply-to-my-repo on the web).`));
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const sizeKb = listing.bundle_size_bytes ? (listing.bundle_size_bytes / 1024).toFixed(1) : '?';
|
|
5
135
|
console.log('');
|
|
6
|
-
console.log(c.
|
|
7
|
-
|
|
136
|
+
console.log(c.cream(` ${listing.title}`) + c.muted(` v${listing.bundle_version ?? '?'}`));
|
|
137
|
+
if (listing.description) {
|
|
138
|
+
console.log(c.muted(` ${listing.description.split('\n')[0]}`));
|
|
139
|
+
}
|
|
140
|
+
console.log(c.dim(` by ${listing.author_name ?? 'unknown'} · ${sizeKb} KB · sha256 ${listing.bundle_sha256.slice(0, 12)}…`));
|
|
8
141
|
console.log('');
|
|
9
|
-
|
|
10
|
-
|
|
142
|
+
// Cache layout: ~/.commitshow/cache/<slug>-<version>/
|
|
143
|
+
const cacheRoot = join(homedir(), '.commitshow', 'cache');
|
|
144
|
+
const stagingDir = mkdtempSync(join(tmpdir(), `commitshow-${slug}-`));
|
|
145
|
+
const versionTag = listing.bundle_version || 'unversioned';
|
|
146
|
+
const finalDir = join(cacheRoot, `${slug}-${versionTag}`);
|
|
147
|
+
try {
|
|
148
|
+
await mkdir(cacheRoot, { recursive: true });
|
|
149
|
+
// If already cached with the right hash, skip download
|
|
150
|
+
const cachedScript = existsSync(finalDir) ? findInstallScript(finalDir) : null;
|
|
151
|
+
let installScript = null;
|
|
152
|
+
if (cachedScript) {
|
|
153
|
+
console.log(c.muted(` using cached bundle at ${finalDir}`));
|
|
154
|
+
installScript = cachedScript;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const tarball = join(stagingDir, 'bundle.tar.gz');
|
|
158
|
+
console.log(c.muted(`→ Downloading bundle...`));
|
|
159
|
+
await downloadBundle(listing.bundle_url, tarball, listing.bundle_sha256);
|
|
160
|
+
console.log(c.muted(` ✓ sha256 verified`));
|
|
161
|
+
console.log(c.muted(`→ Extracting to ${finalDir}`));
|
|
162
|
+
// Clear any old version of this slug+version dir
|
|
163
|
+
await rm(finalDir, { recursive: true, force: true });
|
|
164
|
+
await untar(tarball, finalDir);
|
|
165
|
+
installScript = findInstallScript(finalDir);
|
|
166
|
+
}
|
|
167
|
+
if (!installScript) {
|
|
168
|
+
console.error(c.scarlet(` Bundle extracted but no scripts/install.sh found.`));
|
|
169
|
+
console.error(c.muted(` Pack may be malformed · please report to the publisher.`));
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
console.log(c.muted(`→ Running installer in ${process.cwd()}`));
|
|
173
|
+
console.log(c.dim(` (the script prompts for YOUR Supabase + Resend credentials below)`));
|
|
174
|
+
const exitCode = await runInstallScript(installScript, process.cwd());
|
|
175
|
+
if (exitCode === 0) {
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(c.gold('✓ ') + c.cream(`${listing.title} installed`));
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.error('');
|
|
182
|
+
console.error(c.scarlet(`✗ Installer exited with code ${exitCode}`));
|
|
183
|
+
console.error(c.muted(` See the messages above. The bundle is cached at:`));
|
|
184
|
+
console.error(c.muted(` ${finalDir}`));
|
|
185
|
+
console.error(c.muted(` · re-run with: bash ${installScript}`));
|
|
186
|
+
}
|
|
187
|
+
return exitCode;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => { });
|
|
191
|
+
}
|
|
11
192
|
}
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ ${c.muted('COMMANDS')}
|
|
|
34
34
|
${c.gold('audit')} [target] run audit and render the report
|
|
35
35
|
${c.gold('status')} [target] latest score, no re-run
|
|
36
36
|
${c.gold('submit')} [target] audition a project (requires login · coming soon)
|
|
37
|
-
${c.gold('install')} <
|
|
37
|
+
${c.gold('install')} <slug> install a library pack (e.g. supabase-resend-auth)
|
|
38
38
|
${c.gold('login')} device-flow sign-in (coming soon)
|
|
39
39
|
${c.gold('whoami')} who am I signed in as
|
|
40
40
|
|
package/dist/lib/render.js
CHANGED
|
@@ -471,19 +471,34 @@ export function renderAudit(view) {
|
|
|
471
471
|
const isWalkOn = p.status === 'preview';
|
|
472
472
|
const total = p.score_total ?? 0;
|
|
473
473
|
const lines = [];
|
|
474
|
-
// Big COMMIT.SHOW ANSI Shadow banner.
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
474
|
+
// Big COMMIT.SHOW ANSI Shadow banner. Three-tier fallback by width:
|
|
475
|
+
// · cols ≥ 99 → single-line "COMMIT.SHOW"
|
|
476
|
+
// · cols ≥ 50 → stacked two-line "COMMIT." / "SHOW" so the
|
|
477
|
+
// brand still lands at standard 80-col terminals
|
|
478
|
+
// · cols < 50 → fall through to the Claude-style strip only
|
|
479
|
+
// COLUMNS env var is the fallback when stdout isn't a TTY (CI logs ·
|
|
480
|
+
// piped output).
|
|
479
481
|
const cols = process.stdout.columns
|
|
480
482
|
?? (process.env.COLUMNS ? Number(process.env.COLUMNS) : 80);
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
const single = bigText('COMMIT.SHOW');
|
|
484
|
+
const singleW = single[0].length;
|
|
485
|
+
if (cols >= singleW + 2) {
|
|
486
|
+
for (const r of single)
|
|
484
487
|
lines.push(' ' + c.gold(r));
|
|
485
488
|
lines.push('');
|
|
486
489
|
}
|
|
490
|
+
else {
|
|
491
|
+
const top = bigText('COMMIT.');
|
|
492
|
+
const bot = bigText('SHOW');
|
|
493
|
+
const stackedW = Math.max(top[0].length, bot[0].length);
|
|
494
|
+
if (cols >= stackedW + 2) {
|
|
495
|
+
for (const r of top)
|
|
496
|
+
lines.push(' ' + c.gold(r));
|
|
497
|
+
for (const r of bot)
|
|
498
|
+
lines.push(' ' + c.gold(r));
|
|
499
|
+
lines.push('');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
487
502
|
// Claude Code-style welcome strip · rounded corners + ✻ glyph. Always
|
|
488
503
|
// shown so the brand mark lands even when the big banner doesn't fit.
|
|
489
504
|
const roundTop = c.muted('╭' + '─'.repeat(INSIDE_W) + '╮');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commitshow",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "commit.show CLI
|
|
3
|
+
"version": "0.3.26",
|
|
4
|
+
"description": "commit.show CLI \u2014 audit any vibe-coded project from your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"commitshow": "./bin/commitshow.js"
|