-
Notifications
You must be signed in to change notification settings - Fork 0
Polish npx shotfix for first-run experience #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,15 +3,38 @@ import { createServer } from 'node:http'; | |||||||||||||||||||||||||||||
| import { request as httpsRequest } from 'node:https'; | ||||||||||||||||||||||||||||||
| import { mkdir, writeFile, readdir, unlink, copyFile, stat, readFile } from 'node:fs/promises'; | ||||||||||||||||||||||||||||||
| import { join, relative, resolve } from 'node:path'; | ||||||||||||||||||||||||||||||
| import { execSync, spawnSync } from 'node:child_process'; | ||||||||||||||||||||||||||||||
| import { spawnSync } from 'node:child_process'; | ||||||||||||||||||||||||||||||
| import { createInterface } from 'node:readline'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const PORT = 2847; | ||||||||||||||||||||||||||||||
| const VERSION = '0.1.0'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||
| const args = process.argv.slice(2); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // --help | ||||||||||||||||||||||||||||||
| if (args.includes('--help') || args.includes('-h')) { | ||||||||||||||||||||||||||||||
| console.log(` | ||||||||||||||||||||||||||||||
| shotfix v${VERSION} — Visual AI input for developers | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Usage: | ||||||||||||||||||||||||||||||
| shotfix [options] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Options: | ||||||||||||||||||||||||||||||
| --port <n> Server port (default: 2847) | ||||||||||||||||||||||||||||||
| --dir <path> Captures directory (default: .shotfix/captures) | ||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove Line 24 advertises a configurable port, but this service contract is required to stay on 🔧 Proposed fix- --port <n> Server port (default: 2847)
--dir <path> Captures directory (default: .shotfix/captures)And remove As per coding guidelines: "cli/src/**/*.{js,ts}: Fixed port 2847 must be used for the local dev server health check and capture endpoint". 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| --no-watch Disable auto-fix watch mode | ||||||||||||||||||||||||||||||
| --help, -h Show this help | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Environment: | ||||||||||||||||||||||||||||||
| GEMINI_API_KEY Gemini API key for auto-fix (prompted on first run) | ||||||||||||||||||||||||||||||
| `); | ||||||||||||||||||||||||||||||
| process.exit(0); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const portArg = args.find((_, i) => args[i - 1] === '--port'); | ||||||||||||||||||||||||||||||
| const dirArg = args.find((_, i) => args[i - 1] === '--dir'); | ||||||||||||||||||||||||||||||
| const watchMode = args.includes('--watch'); | ||||||||||||||||||||||||||||||
| const watchMode = !args.includes('--no-watch'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const port = portArg ? parseInt(portArg, 10) : PORT; | ||||||||||||||||||||||||||||||
| const capturesDir = join(process.cwd(), dirArg || '.shotfix/captures'); | ||||||||||||||||||||||||||||||
|
|
@@ -22,9 +45,15 @@ async function main() { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Write .gitignore for .shotfix/ | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| await writeFile(join(shotfixDir, '.gitignore'), 'captures/\n', { flag: 'wx' }); | ||||||||||||||||||||||||||||||
| await writeFile(join(shotfixDir, '.gitignore'), 'captures/\n.env\n', { flag: 'wx' }); | ||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||
| // Already exists, fine | ||||||||||||||||||||||||||||||
| // Already exists — make sure .env is in it | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const existing = await readFile(join(shotfixDir, '.gitignore'), 'utf-8'); | ||||||||||||||||||||||||||||||
| if (!existing.includes('.env')) { | ||||||||||||||||||||||||||||||
| await writeFile(join(shotfixDir, '.gitignore'), existing.trimEnd() + '\n.env\n'); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Backfill both Lines 53-55 only append 🛠️ Proposed fix- const existing = await readFile(join(shotfixDir, '.gitignore'), 'utf-8');
- if (!existing.includes('.env')) {
- await writeFile(join(shotfixDir, '.gitignore'), existing.trimEnd() + '\n.env\n');
- }
+ const gitignorePath = join(shotfixDir, '.gitignore');
+ const existing = await readFile(gitignorePath, 'utf-8');
+ const lines = new Set(existing.split(/\r?\n/).filter(Boolean));
+ lines.add('captures/');
+ lines.add('.env');
+ await writeFile(gitignorePath, `${Array.from(lines).join('\n')}\n`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } catch {} | ||||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nested try {
const existing = await readFile(join(shotfixDir, '.gitignore'), 'utf-8');
if (!existing.includes('.env')) {
await writeFile(join(shotfixDir, '.gitignore'), existing.trimEnd() + '\n.env\n');
}
} catch (e) {
console.warn(`[shotfix] Failed to update .shotfix/.gitignore: ${(e as Error).message}`);
} |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Cleanup old captures (>1 hour) | ||||||||||||||||||||||||||||||
|
|
@@ -51,13 +80,38 @@ async function main() { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Resolve API key from env or noxkey | ||||||||||||||||||||||||||||||
| // Resolve API key: env var → .shotfix/.env → interactive prompt | ||||||||||||||||||||||||||||||
| let apiKey = process.env.GEMINI_API_KEY || ''; | ||||||||||||||||||||||||||||||
| if (!apiKey && watchMode) { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||
| const envPath = join(shotfixDir, '.env'); | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| apiKey = execSync('noxkey get shared/GEMINI_API_KEY --raw', { encoding: 'utf-8', stdio: 'pipe' }).trim(); | ||||||||||||||||||||||||||||||
| const envContent = await readFile(envPath, 'utf-8'); | ||||||||||||||||||||||||||||||
| const match = envContent.match(/^GEMINI_API_KEY=(.+)$/m); | ||||||||||||||||||||||||||||||
| if (match) apiKey = match[1].trim(); | ||||||||||||||||||||||||||||||
|
Comment on lines
+90
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regular expression for parsing the const match = envContent.match(/^\s*GEMINI_API_KEY=(.*?)(\s*#.*)?$/m);
if (match) apiKey = match[1].trim().replace(/^['"]|['"]$/g, ''); |
||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||
| // Will warn at startup | ||||||||||||||||||||||||||||||
| // No .env file yet | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!apiKey && watchMode && process.stdin.isTTY) { | ||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||
| console.log(' 🔑 Shotfix needs a Gemini API key for auto-fix.'); | ||||||||||||||||||||||||||||||
| console.log(' Get one free at: https://aistudio.google.com/apikey'); | ||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefix new console logs with Lines 99-101, 114, 483, and 507-520 use unprefixed console output; changed logs should follow the project logging prefix rule. As per coding guidelines: "Applies to /{widget,cli}//*.{js,ts} : Use Also applies to: 114-114, 483-483, 507-520 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const rl = createInterface({ input: process.stdin, output: process.stdout }); | ||||||||||||||||||||||||||||||
| apiKey = await new Promise<string>((resolve) => { | ||||||||||||||||||||||||||||||
| rl.question(' Enter your API key: ', (answer) => { | ||||||||||||||||||||||||||||||
| rl.close(); | ||||||||||||||||||||||||||||||
| resolve(answer.trim()); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (apiKey) { | ||||||||||||||||||||||||||||||
| await mkdir(shotfixDir, { recursive: true }); | ||||||||||||||||||||||||||||||
| await writeFile(join(shotfixDir, '.env'), `GEMINI_API_KEY=${apiKey}\n`); | ||||||||||||||||||||||||||||||
| console.log(' ✅ Saved to .shotfix/.env\n'); | ||||||||||||||||||||||||||||||
|
Comment on lines
+111
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify .env write call and whether explicit restrictive mode is set.
rg -n "writeFile\\(\\s*join\\(shotfixDir, '\\.env'\\)" cli/src/cli.ts -C4Repository: No-Box-Dev/shotfix Length of output: 317 🏁 Script executed: # Check if there are other instances of writeFile in this file that might have the same issue
rg -n "writeFile" cli/src/cli.tsRepository: No-Box-Dev/shotfix Length of output: 772 Write Line 113 saves the API key without explicit file mode; default permissions (typically 0o644 with common umask settings) make the file readable by other users on the system. Fix- await writeFile(join(shotfixDir, '.env'), `GEMINI_API_KEY=${apiKey}\n`);
+ await writeFile(
+ join(shotfixDir, '.env'),
+ `GEMINI_API_KEY=${apiKey}\n`,
+ { mode: 0o600 }
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -323,9 +377,25 @@ async function main() { | |||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Serve the widget JS | ||||||||||||||||||||||||||||||
| if (url.pathname === '/widget.js' && req.method === 'GET') { | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const widgetPath = new URL('../widget/dist/shotfix.min.js', import.meta.url); | ||||||||||||||||||||||||||||||
| const widgetContent = await readFile(widgetPath); | ||||||||||||||||||||||||||||||
| res.writeHead(200, { | ||||||||||||||||||||||||||||||
| 'Content-Type': 'application/javascript', | ||||||||||||||||||||||||||||||
| 'Cache-Control': 'no-cache', | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| res.end(widgetContent); | ||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||||||||||||||||||||||||||||||
| res.end('Widget not found'); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+390
to
+393
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The } catch (err) {
console.error(`[shotfix] Error serving widget.js: ${(err as Error).message}`);
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Widget not found');
} |
||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (url.pathname === '/cancel' && req.method === 'POST') { | ||||||||||||||||||||||||||||||
| cancelRequested = true; | ||||||||||||||||||||||||||||||
| // Clear the queue too | ||||||||||||||||||||||||||||||
| const cleared = fixQueue.length; | ||||||||||||||||||||||||||||||
| fixQueue.length = 0; | ||||||||||||||||||||||||||||||
| console.log(` 🚫 Cancel requested (${cleared} queued items cleared)`); | ||||||||||||||||||||||||||||||
|
|
@@ -410,7 +480,7 @@ async function main() { | |||||||||||||||||||||||||||||
| await cleanOldCaptures(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const title = captureJson.title; | ||||||||||||||||||||||||||||||
| console.log(`[${timeLabel}] ⚡ Captured: ${title}`); | ||||||||||||||||||||||||||||||
| console.log(` ⚡ Captured: ${title}`); | ||||||||||||||||||||||||||||||
| broadcast('capture', { title, timestamp: timeLabel }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||
|
|
@@ -434,16 +504,22 @@ async function main() { | |||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| server.listen(port, () => { | ||||||||||||||||||||||||||||||
| console.log(`⚡ Shotfix dev server running on port ${port}`); | ||||||||||||||||||||||||||||||
| console.log(` Captures saved to ${capturesDir}`); | ||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||
| console.log(` ⚡ Shotfix v${VERSION}`); | ||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||
| console.log(' Add to your HTML:'); | ||||||||||||||||||||||||||||||
| console.log(` <script src="http://localhost:${port}/widget.js"><\/script>`); | ||||||||||||||||||||||||||||||
| console.log(' <script>Shotfix.init();<\/script>'); | ||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||
| console.log(` Server: http://localhost:${port}`); | ||||||||||||||||||||||||||||||
| if (watchMode && apiKey) { | ||||||||||||||||||||||||||||||
| console.log(` 🤖 Watch mode: captures will auto-fix via Gemini Flash`); | ||||||||||||||||||||||||||||||
| console.log(' Watch: ON (Gemini Flash)'); | ||||||||||||||||||||||||||||||
| } else if (watchMode) { | ||||||||||||||||||||||||||||||
| console.log(` ⚠️ Watch mode enabled but no GEMINI_API_KEY found`); | ||||||||||||||||||||||||||||||
| console.log(` Set env var or store with: noxkey set shared/GEMINI_API_KEY`); | ||||||||||||||||||||||||||||||
| console.log(' Watch: ON (no API key — captures only)'); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| console.log(' Watch: OFF'); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||
| console.log('Tip: Add to CLAUDE.md: "Check .shotfix/captures/latest.json and latest.png for bug captures"'); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
build:widgetscript relies on shell commands (cd,mkdir -p,cp) that may not be available or behave consistently across all operating systems, particularly on standard Windows command prompts. This can hinder cross-platform development and usage. For better portability, consider using Node.js-based utilities for file system operations, either through a library likefs-extraor by creating a small Node script that uses the built-infs/promisesmodule.