commitshow 0.3.25 → 0.3.27

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.
@@ -3,6 +3,7 @@ import { findProjectByGithubUrl, fetchLatestSnapshot, fetchStanding, runPreviewA
3
3
  import { renderAudit, renderMarkdown, renderJson, renderUpsell, renderStarCta, renderQuotaFooter, renderRateLimitDeny, renderAuditError, writeAuditMarkdown, writeAuditJson, } from '../lib/render.js';
4
4
  import { c } from '../lib/colors.js';
5
5
  import { Spinner } from '../lib/spinner.js';
6
+ import { maybeOfferCi } from '../lib/ci-prompt.js';
6
7
  export async function audit(args) {
7
8
  const asJson = args.includes('--json');
8
9
  const force = args.includes('--refresh') || args.includes('--force');
@@ -123,6 +124,12 @@ export async function audit(args) {
123
124
  if (jsonPath)
124
125
  console.log(c.dim(` Saved → ${jsonPath}`));
125
126
  }
127
+ // Post-audit CI nudge · only in interactive non-JSON mode.
128
+ // Skipped silently if not in a git repo / no GitHub remote /
129
+ // workflow already exists.
130
+ if (!asJson && target.localPath) {
131
+ await maybeOfferCi(target.localPath);
132
+ }
126
133
  }
127
134
  return 0;
128
135
  }
@@ -248,6 +255,10 @@ export async function audit(args) {
248
255
  if (jsonPath)
249
256
  console.log(c.dim(` Saved → ${jsonPath}`));
250
257
  }
258
+ // Post-audit CI nudge · only in interactive non-JSON mode.
259
+ if (!asJson && target.localPath) {
260
+ await maybeOfferCi(target.localPath);
261
+ }
251
262
  }
252
263
  return 0;
253
264
  }
@@ -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
- export async function install(_args) {
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.cream('Install is not yet available in CLI 0.1.'));
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.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.'));
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
- console.log(c.dim(' Use Apply-to-my-repo on the web → https://commit.show/library'));
10
- return 1;
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')} <pack> install a library artifact (coming soon)
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
 
@@ -0,0 +1,93 @@
1
+ // Post-audit nudge · ask the user whether to wire the audit into CI
2
+ // by writing .github/workflows/audit.yml in their repo. Highest-intent
3
+ // moment (they just saw their score and concerns) and the cheapest
4
+ // possible install path (one file, one commit).
5
+ //
6
+ // Skipped silently when:
7
+ // · stdout/stdin isn't a TTY (CI, scripted use)
8
+ // · target isn't a git repo
9
+ // · the repo has no github.com remote
10
+ // · the workflow file already exists (don't overwrite)
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { createInterface } from 'node:readline/promises';
14
+ import { c } from './colors.js';
15
+ const WORKFLOW_REL_PATH = '.github/workflows/audit.yml';
16
+ const WORKFLOW_BODY = `name: audit
17
+ on:
18
+ pull_request:
19
+
20
+ permissions:
21
+ pull-requests: write
22
+
23
+ jobs:
24
+ audit:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: commitshow/audit-action@v1
28
+ `;
29
+ function isGitRepo(path) {
30
+ return existsSync(join(path, '.git'));
31
+ }
32
+ function hasGithubRemote(path) {
33
+ try {
34
+ const cfg = readFileSync(join(path, '.git', 'config'), 'utf8');
35
+ return /url\s*=\s*[^\n]*github\.com/i.test(cfg);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ function existingWorkflow(path) {
42
+ return existsSync(join(path, WORKFLOW_REL_PATH));
43
+ }
44
+ async function ask(question, defaultYes = true) {
45
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
46
+ try {
47
+ const raw = await rl.question(question + ' ');
48
+ const ans = raw.trim().toLowerCase();
49
+ if (ans === '')
50
+ return defaultYes;
51
+ return ans === 'y' || ans === 'yes';
52
+ }
53
+ finally {
54
+ rl.close();
55
+ }
56
+ }
57
+ export async function maybeOfferCi(localPath) {
58
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
59
+ return;
60
+ if (!isGitRepo(localPath))
61
+ return;
62
+ if (!hasGithubRemote(localPath))
63
+ return;
64
+ if (existingWorkflow(localPath))
65
+ return;
66
+ console.log('');
67
+ console.log(c.muted(' Want this audit to run on every pull request?'));
68
+ console.log(c.muted(` Drops a single file at ${WORKFLOW_REL_PATH} that uses commitshow/audit-action@v1.`));
69
+ console.log(c.muted(' The action posts a sticky comment on each PR so regressions surface in review.'));
70
+ let yes;
71
+ try {
72
+ yes = await ask(c.gold(' Add the workflow? [Y/n]'), true);
73
+ }
74
+ catch {
75
+ // Reading from stdin can throw on unusual terminal states (e.g. piped
76
+ // input that closes mid-read). Treat as "no" rather than crashing.
77
+ return;
78
+ }
79
+ if (!yes) {
80
+ console.log(c.dim(' Skipped. Run again later or copy the YAML from https://github.com/commitshow/audit-action.'));
81
+ return;
82
+ }
83
+ try {
84
+ const dir = join(localPath, '.github', 'workflows');
85
+ mkdirSync(dir, { recursive: true });
86
+ writeFileSync(join(localPath, WORKFLOW_REL_PATH), WORKFLOW_BODY, 'utf8');
87
+ console.log(c.gold(` ✓ Wrote ${WORKFLOW_REL_PATH}`));
88
+ console.log(c.muted(' Next: commit and push, then open a pull request — the score will post automatically.'));
89
+ }
90
+ catch (e) {
91
+ console.log(c.scarlet(` Could not write ${WORKFLOW_REL_PATH}: ${e.message}`));
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "commitshow",
3
- "version": "0.3.25",
4
- "description": "commit.show CLI audit any vibe-coded project from your terminal.",
3
+ "version": "0.3.27",
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"