vibecodedshit 0.2.0 → 0.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/README.md CHANGED
@@ -8,9 +8,18 @@ Codebase Health benchmark. See `methodology/v1.0/METHODOLOGY.md` for how scores
8
8
  npx vibecodedshit scan .
9
9
  ```
10
10
 
11
- Needs Node 22 or later and `git`. No clone, no plugin, no repository access for anyone. With Docker and the `health:1.0` image the scan is a full run. Without them it falls back to a preview scan, prints why and which parameters it skipped (they need Docker), and writes `run.json`. The scan runs on your machine. Your code is not uploaded; it only looks up your dependencies' names and versions on the public npm and PyPI registries (and OSV, if osv-scanner is installed). To get the full run, build the image with `docker build -t health:1.0 -f <package>/docker/Dockerfile <package>/docker` (the scan prints the exact command).
11
+ Needs Node 22 or later and `git`. No clone, no plugin, no repository access for anyone. In a terminal the scan walks you through it:
12
12
 
13
- Submitting is opt-in: `npx vibecodedshit submit run.json --email <you@example.com>` prints the payload and sends it only with `--confirm`. Only full, comparable runs can be submitted.
13
+ 1. Docker not installed: asks whether to install and start it (Homebrew on macOS, get.docker.com on Linux, winget on Windows).
14
+ 2. Docker installed but not running: asks whether to start it.
15
+ 3. Scan image `health:1.0` missing or out of date: asks whether to build it (5 to 10 minutes the first time).
16
+ 4. Runs the scan and writes `run.json`. Any No above runs a preview scan instead (static layer only, not comparable).
17
+ 5. Asks whether to submit the score to the global leaderboard (full, comparable runs only). Sends your scores, a hash of the repository URL and your email; no code, no file paths.
18
+ 6. For a Vibe-coded or Fragile result, asks whether you want a quote from Peter for fixing the app. Sends your name, email, run.json and the HTML report.
19
+
20
+ Every question defaults to No, and nothing leaves your machine without a yes. The scan itself only looks up your dependencies' names and versions on the public npm and PyPI registries (and OSV, if osv-scanner is installed).
21
+
22
+ Without a terminal (CI, `CI` set, or piped output) nothing is asked: the scan falls back to a preview when Docker or the image is missing, prints the image build command, and submitting stays the explicit `npx vibecodedshit submit run.json --email <you@example.com> --confirm`.
14
23
 
15
24
  ## From a clone
16
25
 
package/dist/bin.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { run } from './cli.js';
3
+ import { terminalPrompter } from './interactive/prompt.js';
4
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI;
3
5
  const code = await run(process.argv.slice(2), {
4
6
  stdout: (text) => process.stdout.write(text),
5
7
  stderr: (text) => process.stderr.write(text),
6
8
  cwd: process.cwd(),
9
+ ...(interactive ? { prompt: terminalPrompter } : {}),
7
10
  });
8
11
  process.exitCode = code;
package/dist/cli.js CHANGED
@@ -7,7 +7,10 @@ import { footer, imageBuildCommand } from './commands/footer.js';
7
7
  import { imageDigest } from './exec/docker.js';
8
8
  import { runReport } from './commands/report.js';
9
9
  import { runRoute } from './commands/route.js';
10
- import { runSubmit } from './commands/submit.js';
10
+ import { defaultGit, runSubmit } from './commands/submit.js';
11
+ import { afterScan } from './interactive/after-scan.js';
12
+ import { setUpDocker } from './interactive/docker-setup.js';
13
+ import { hostSystem } from './interactive/system.js';
11
14
  import { summarize } from './commands/summary.js';
12
15
  import { CURRENT_METHODOLOGY_VERSION, loadMethodology, MethodologyError, packageRoot } from './methodology/load.js';
13
16
  import { invalidRun, NoLogicFilesError, runScan } from './run/scan.js';
@@ -86,20 +89,34 @@ export async function run(argv, io, deps = {}) {
86
89
  if (opts.mode === 'full' && !deps.execRunner && deps.imageDigest === undefined) {
87
90
  // No Docker, or no usable image: fall back to the static layer so `scan .` works from an npx install.
88
91
  const tag = methodology.execution.runner.image;
89
- try {
90
- opts.imageDigest = await imageDigest(tag);
92
+ if (io.prompt) {
93
+ const setup = await setUpDocker(tag, { io, prompt: io.prompt, system: deps.system ?? hostSystem, packageRoot, imageDigest: deps.inspectImage ?? imageDigest });
94
+ if ('digest' in setup)
95
+ opts.imageDigest = setup.digest;
96
+ else {
97
+ opts.mode = 'preview';
98
+ io.stderr(`${setup.previewReason}\nRunning a preview scan (static layer only, not comparable).\n`);
99
+ }
91
100
  }
92
- catch (err) {
93
- if (!(err instanceof ToolError))
94
- throw err;
95
- opts.mode = 'preview';
96
- io.stderr(`${err.message}\nRunning a preview scan (static layer only, not comparable). For a full run: ${imageBuildCommand(packageRoot, tag)}\n`);
101
+ else {
102
+ try {
103
+ opts.imageDigest = await imageDigest(tag);
104
+ }
105
+ catch (err) {
106
+ if (!(err instanceof ToolError))
107
+ throw err;
108
+ opts.mode = 'preview';
109
+ io.stderr(`${err.message}\nRunning a preview scan (static layer only, not comparable). For a full run: ${imageBuildCommand(packageRoot, tag)}\n`);
110
+ }
97
111
  }
98
112
  }
99
113
  try {
100
114
  const result = await runScan(opts, methodology);
101
115
  writeRun(outFile, result);
102
- io.stdout(summarize(result, methodology) + footer(result, outFile));
116
+ io.stdout(summarize(result, methodology) + footer(result, outFile, io.prompt !== undefined));
117
+ if (io.prompt) {
118
+ await afterScan(result, methodology, { io, prompt: io.prompt, git: deps.git ?? defaultGit, fetch: deps.fetch ?? fetch });
119
+ }
103
120
  return EXIT.ok;
104
121
  }
105
122
  catch (err) {
@@ -6,7 +6,7 @@ export function imageBuildCommand(packageRoot, tag) {
6
6
  return `docker build -t ${tag} -f ${path.join(docker, 'Dockerfile')} ${docker}`;
7
7
  }
8
8
  /** Lines printed after the scan summary: what a preview skipped, that nothing was uploaded, and how to submit. */
9
- export function footer(run, outFile) {
9
+ export function footer(run, outFile, interactive = false) {
10
10
  const lines = [];
11
11
  if (run.mode === 'preview') {
12
12
  const skipped = run.parameters.filter((p) => p.status === 'not_measured' && p.reason?.startsWith('execution layer')).map((p) => p.id);
@@ -14,6 +14,9 @@ export function footer(run, outFile) {
14
14
  lines.push(`Skipped (needs Docker): ${skipped.join(', ')}. Run a full scan with Docker to measure them.`);
15
15
  }
16
16
  lines.push(PRIVACY);
17
+ // An interactive scan asks about the leaderboard next instead of printing the command.
18
+ if (interactive)
19
+ return `${lines.join('\n')}\n`;
17
20
  if (run.valid && run.comparable && run.mode === 'full') {
18
21
  lines.push(`To compare your result on app.vibecodedshit.com, run: npx vibecodedshit submit ${outFile} --email <you@example.com>`);
19
22
  lines.push('It prints the payload first and sends nothing unless you add --confirm.');
@@ -0,0 +1,121 @@
1
+ import { renderReport } from '../report/render.js';
2
+ import { EMAIL_PATTERN, MAX_EMAIL_CHARS, MAX_PAYLOAD_BYTES, PRIVACY_URL, QUOTE_URL } from '../submit/constants.js';
3
+ import { buildPayload, PayloadError } from '../submit/payload.js';
4
+ import { repoIdentity, RepoIdentityError } from '../submit/repo-identity.js';
5
+ import { sendPayload } from '../submit/send.js';
6
+ /** Bands whose owners are offered a quote for the fixing service. */
7
+ const QUOTE_BANDS = new Set(['vibe_coded', 'fragile']);
8
+ const QUOTE_TIMEOUT_MS = 30_000;
9
+ async function askEmail(prompt, io) {
10
+ for (let attempt = 0; attempt < 3; attempt += 1) {
11
+ const email = await prompt.text('Your email:');
12
+ if (email.length <= MAX_EMAIL_CHARS && EMAIL_PATTERN.test(email))
13
+ return email;
14
+ io.stderr('That is not a valid email address.\n');
15
+ }
16
+ return null;
17
+ }
18
+ async function submitToLeaderboard(run, ctx, emailFor) {
19
+ const { io, prompt } = ctx;
20
+ if (run.mode !== 'full' || !run.comparable) {
21
+ const why = run.mode === 'full' ? (run.comparableReason ?? 'not comparable') : 'a preview scan, a full scan with Docker is needed';
22
+ io.stdout(`This run cannot go on the global leaderboard: ${why}.\n`);
23
+ return;
24
+ }
25
+ io.stdout(`The leaderboard gets your scores, a hash of your repository URL and your email. No code, no file paths. Privacy: ${PRIVACY_URL}\n`);
26
+ if (!(await prompt.confirm('Submit your score to the global leaderboard?')))
27
+ return;
28
+ const email = await emailFor();
29
+ if (email === null)
30
+ return;
31
+ try {
32
+ const identity = await repoIdentity(run.repo.path, ctx.git);
33
+ const payload = buildPayload({ run, repoHash: identity.hash, email });
34
+ if (Buffer.byteLength(JSON.stringify(payload), 'utf8') > MAX_PAYLOAD_BYTES) {
35
+ io.stderr('Not submitted: the payload is larger than 64 KB, the server limit.\n');
36
+ return;
37
+ }
38
+ const result = await sendPayload(payload, ctx.fetch);
39
+ if (!result.ok) {
40
+ io.stderr(`Not submitted. ${result.message}\n`);
41
+ return;
42
+ }
43
+ const rank = result.rank;
44
+ const rankLine = rank ? ` Rank ${rank.rank} of ${rank.total}.${rank.top10Threshold === null ? '' : ` Top 10 starts at ${rank.top10Threshold}.`}` : '';
45
+ io.stdout(`Submitted.${rankLine}\n`);
46
+ }
47
+ catch (err) {
48
+ if (err instanceof RepoIdentityError || err instanceof PayloadError) {
49
+ io.stderr(`Not submitted: ${err.message}\n`);
50
+ return;
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ async function requestQuote(run, methodology, ctx, emailFor) {
56
+ const { io, prompt } = ctx;
57
+ if (!run.health || !QUOTE_BANDS.has(run.health.band))
58
+ return;
59
+ const bandName = methodology.bands.find((b) => b.key === run.health?.band)?.name ?? run.health.band;
60
+ io.stdout(`\nYour app is ${bandName}. Peter, the creator of this scan, runs a service that fixes vibe-coded apps.\n`);
61
+ io.stdout('A quote request sends your name, email, run.json and the HTML report (scores and file paths, no source code).\n');
62
+ if (!(await prompt.confirm('Do you want to ask for a quote?')))
63
+ return;
64
+ const name = await prompt.text('Your name:');
65
+ if (name === '') {
66
+ io.stderr('A name is needed for the quote request. Nothing was sent.\n');
67
+ return;
68
+ }
69
+ const email = await emailFor();
70
+ if (email === null)
71
+ return;
72
+ const note = await prompt.text('Anything Peter should know? (optional, press Enter to skip)');
73
+ let repoHash;
74
+ try {
75
+ repoHash = (await repoIdentity(run.repo.path, ctx.git)).hash;
76
+ }
77
+ catch (err) {
78
+ if (!(err instanceof RepoIdentityError))
79
+ throw err;
80
+ io.stderr(`Nothing was sent: ${err.message}\n`);
81
+ return;
82
+ }
83
+ const body = {
84
+ contact: { name, email, ...(note === '' ? {} : { note }) },
85
+ methodologyVersion: run.methodology.version,
86
+ health: run.health.displayed,
87
+ band: run.health.band,
88
+ repoHash,
89
+ run,
90
+ report: renderReport(run, methodology),
91
+ };
92
+ try {
93
+ const response = await ctx.fetch(QUOTE_URL, {
94
+ method: 'POST',
95
+ headers: { 'content-type': 'application/json' },
96
+ body: JSON.stringify(body),
97
+ signal: AbortSignal.timeout(QUOTE_TIMEOUT_MS),
98
+ });
99
+ if (response.ok) {
100
+ io.stdout('Quote requested. Peter will reply by email.\n');
101
+ return;
102
+ }
103
+ io.stderr(`The quote request failed (${response.status}): ${(await response.text()).slice(0, 300)}. Reach Peter at https://vibecodedshit.com\n`);
104
+ }
105
+ catch (err) {
106
+ io.stderr(`The quote request failed: ${err instanceof Error ? err.message : String(err)}. Reach Peter at https://vibecodedshit.com\n`);
107
+ }
108
+ }
109
+ /** After an interactive scan: offer the leaderboard, then a quote for Vibe-coded and Fragile apps. Nothing is sent without a yes. */
110
+ export async function afterScan(run, methodology, ctx) {
111
+ if (!run.valid)
112
+ return;
113
+ let email;
114
+ const emailFor = async () => {
115
+ if (email === undefined)
116
+ email = await askEmail(ctx.prompt, ctx.io);
117
+ return email;
118
+ };
119
+ await submitToLeaderboard(run, ctx, emailFor);
120
+ await requestQuote(run, methodology, ctx, emailFor);
121
+ }
@@ -0,0 +1,97 @@
1
+ import { imageBuildCommand } from '../commands/footer.js';
2
+ import { ToolError } from '../scope/types.js';
3
+ const DOWNLOAD_URL = 'https://www.docker.com/products/docker-desktop/';
4
+ const START_TIMEOUT_MS = 180_000;
5
+ const POLL_MS = 3_000;
6
+ const ok = (code) => code === 0;
7
+ async function install(ctx) {
8
+ const { system, io } = ctx;
9
+ if (system.platform === 'darwin') {
10
+ if (!ok(await system.exec('brew', ['--version']))) {
11
+ io.stderr(`Homebrew is not installed, so Docker cannot be installed for you. Download Docker Desktop from ${DOWNLOAD_URL}\n`);
12
+ return false;
13
+ }
14
+ return ok(await system.exec('brew', ['install', '--cask', 'docker-desktop'], { inherit: true }));
15
+ }
16
+ if (system.platform === 'linux') {
17
+ return ok(await system.exec('sh', ['-c', 'curl -fsSL https://get.docker.com | sudo sh'], { inherit: true }));
18
+ }
19
+ if (system.platform === 'win32') {
20
+ const args = ['install', '-e', '--id', 'Docker.DockerDesktop', '--accept-package-agreements', '--accept-source-agreements'];
21
+ return ok(await system.exec('winget', args, { inherit: true }));
22
+ }
23
+ io.stderr(`Docker cannot be installed for you on ${system.platform}. Download it from ${DOWNLOAD_URL}\n`);
24
+ return false;
25
+ }
26
+ async function start(ctx) {
27
+ const { system, io } = ctx;
28
+ if (system.platform === 'darwin') {
29
+ await system.exec('open', ['-a', 'Docker']);
30
+ io.stdout('Starting Docker Desktop. If it asks you to accept its terms, do that in its window.\n');
31
+ }
32
+ else if (system.platform === 'linux') {
33
+ await system.exec('sudo', ['systemctl', 'start', 'docker'], { inherit: true });
34
+ }
35
+ else if (system.platform === 'win32') {
36
+ await system.exec('cmd', ['/c', 'start', '', 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe']);
37
+ io.stdout('Starting Docker Desktop. If it asks you to accept its terms, do that in its window.\n');
38
+ }
39
+ io.stdout('Waiting for Docker to start (up to 3 minutes)...\n');
40
+ for (let waited = 0; waited < START_TIMEOUT_MS; waited += POLL_MS) {
41
+ if (ok(await system.exec('docker', ['info'])))
42
+ return true;
43
+ await system.sleep(POLL_MS);
44
+ }
45
+ return false;
46
+ }
47
+ function notStarted(platform) {
48
+ const linux = platform === 'linux' ? ' If Docker runs but needs sudo, run `sudo usermod -aG docker $USER`, log in again and rerun the scan.' : '';
49
+ return `Docker did not start within 3 minutes.${linux}`;
50
+ }
51
+ /**
52
+ * Get Docker and the scan image ready for a full run, asking before each install, start or build.
53
+ * Any No, or a step that fails, returns the reason to fall back to a preview scan.
54
+ */
55
+ export async function setUpDocker(tag, ctx) {
56
+ const { system, prompt, io } = ctx;
57
+ if (!ok(await system.exec('docker', ['--version']))) {
58
+ if (!(await prompt.confirm('Docker is not installed. A full scan needs it. Install and start Docker now?'))) {
59
+ return { previewReason: 'Docker is not installed.' };
60
+ }
61
+ if (!(await install(ctx)))
62
+ return { previewReason: 'Docker could not be installed.' };
63
+ if (!(await start(ctx)))
64
+ return { previewReason: notStarted(system.platform) };
65
+ }
66
+ else if (!ok(await system.exec('docker', ['info']))) {
67
+ if (!(await prompt.confirm('Docker is installed but not running. Start it now?'))) {
68
+ return { previewReason: 'Docker is not running.' };
69
+ }
70
+ if (!(await start(ctx)))
71
+ return { previewReason: notStarted(system.platform) };
72
+ }
73
+ try {
74
+ return { digest: await ctx.imageDigest(tag) };
75
+ }
76
+ catch (err) {
77
+ if (!(err instanceof ToolError))
78
+ throw err;
79
+ const state = err.message.includes('out of date') ? 'is out of date' : 'is not built yet';
80
+ if (!(await prompt.confirm(`The scan image ${tag} ${state}. Build it now? The first build takes about 5 to 10 minutes.`))) {
81
+ return { previewReason: `The scan image ${tag} ${state}.` };
82
+ }
83
+ }
84
+ const build = imageBuildCommand(ctx.packageRoot, tag).split(' ');
85
+ if (!ok(await system.exec(build[0] ?? 'docker', build.slice(1), { inherit: true }))) {
86
+ return { previewReason: `Building the scan image failed. Retry with: ${build.join(' ')}` };
87
+ }
88
+ try {
89
+ io.stdout(`Built ${tag}.\n`);
90
+ return { digest: await ctx.imageDigest(tag) };
91
+ }
92
+ catch (err) {
93
+ if (!(err instanceof ToolError))
94
+ throw err;
95
+ return { previewReason: err.message };
96
+ }
97
+ }
@@ -0,0 +1,17 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ async function ask(question) {
3
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
4
+ try {
5
+ return (await rl.question(question)).trim();
6
+ }
7
+ finally {
8
+ rl.close();
9
+ }
10
+ }
11
+ export const terminalPrompter = {
12
+ async confirm(question) {
13
+ const answer = await ask(`${question} [y/N] `);
14
+ return /^y(es)?$/i.test(answer);
15
+ },
16
+ text: (question) => ask(`${question} `),
17
+ };
@@ -0,0 +1,10 @@
1
+ import { spawn } from 'node:child_process';
2
+ export const hostSystem = {
3
+ platform: process.platform,
4
+ exec: (command, args, opts = {}) => new Promise((resolve) => {
5
+ const child = spawn(command, args, { stdio: opts.inherit ? 'inherit' : 'ignore' });
6
+ child.on('error', () => resolve(null));
7
+ child.on('close', (code) => resolve(code));
8
+ }),
9
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
10
+ };
@@ -1,5 +1,6 @@
1
1
  export const SUBMIT_URL = 'https://app.vibecodedshit.com/api/submissions';
2
2
  export const PRIVACY_URL = 'https://app.vibecodedshit.com/privacy';
3
+ export const QUOTE_URL = 'https://app.vibecodedshit.com/api/quote-requests';
3
4
  /** The server rejects larger bodies. */
4
5
  export const MAX_PAYLOAD_BYTES = 65536;
5
6
  /** The server rejects longer summaries. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibecodedshit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Codebase Health benchmark CLI",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",