gitrunbykaru 1.0.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,94 @@
1
+ # gitrunbykaru
2
+
3
+ Run any GitHub repository locally in seconds — no setup, no friction.
4
+
5
+ ```
6
+ gitrunbykaru https://github.com/user/repo
7
+ ```
8
+
9
+ That's it. GitRun clones the repo, detects what kind of project it is, installs dependencies, and runs it. You get a `localhost` link.
10
+
11
+ ---
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g gitrunbykaru
17
+ ```
18
+
19
+ Node.js 18+ required.
20
+
21
+ ---
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Run a repo
27
+ gitrunbykaru https://github.com/user/repo
28
+
29
+ # Don't auto-open the browser
30
+ gitrunbykaru https://github.com/user/repo --no-open
31
+
32
+ # Keep the cloned directory after exit (for debugging)
33
+ gitrunbykaru https://github.com/user/repo --keep
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Supported stacks
39
+
40
+ | Stack | Detected by | Install / Package Manager | Run |
41
+ | -------------- | ----------------------------- | ---------------------------------- | ---------------------------------------------- |
42
+ | Node.js / JS | `package.json` | npm, yarn, pnpm, bun (auto-detect) | `dev` → `start` → `serve` → `build && preview` |
43
+ | Next.js | `package.json` + next dep | matching lockfile (`npm/yarn/...`) | `dev` |
44
+ | Vite / React | `package.json` + vite dep | matching lockfile (`npm/yarn/...`) | `dev` |
45
+ | Python / Flask | `requirements.txt` + `app.py` | `pip install -r requirements.txt` | `python app.py` |
46
+ | Django | `manage.py` | `pip install -r requirements.txt` | `python manage.py runserver` |
47
+ | FastAPI | `requirements.txt` + fastapi | `pip install -r requirements.txt` | `python main.py` |
48
+ | Static HTML | `index.html` only | — | `npx serve .` |
49
+
50
+ ---
51
+
52
+ ## 🔥 Magic Features (Why it's smart)
53
+
54
+ - **Auto-Environment Mocking:** If a repo requires a `.env` file but only provides a `.env.example` or `.env.sample`, GitRun automatically clones it and injects dummy keys so the app doesn't crash on startup!
55
+ - **Interactive Fallback:** If a repository is too weird to auto-detect, GitRun won't just crash. It pauses and gives you an interactive menu to manually type the run command.
56
+ - **Auto-Downloads Missing Tools:** If a repo strictly uses `yarn`, `pnpm`, or `bun`, but you don't have them installed globally, GitRun will automatically fetch them securely via `npx` just for that run.
57
+ - **Windows Path Fixes:** Bypasses legacy Windows 8.3 short-path bugs so strict bundlers like Vite and React never crash.
58
+
59
+ ---
60
+
61
+ ## Philosophy
62
+
63
+ - **Speed over perfection** — works for 80% of repos without any config
64
+ - **Zero effort** — you provide the URL, GitRun handles everything else
65
+ - **Local first** — runs on your machine, no cloud, no auth
66
+ - **Opinionated** — makes decisions for you so you don't have to
67
+
68
+ ---
69
+
70
+ ## What it doesn't handle (by design)
71
+
72
+ - True database dependencies (Postgres, MySQL, Redis, etc.)
73
+ - Monorepos with complex workspace setups (e.g., `client/` and `api/` folders requiring separate terminal tabs — _coming soon_)
74
+ - Private repositories where Git needs explicit SSH keys/auth
75
+
76
+ For these, you'll need to set things up manually.
77
+
78
+ ---
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ git clone https://github.com/yourusername/gitrunbykaru
84
+ cd gitrunbykaru
85
+ npm install
86
+ npm link # makes 'gitrunbykaru' available globally
87
+ node tests/test.js
88
+ ```
89
+
90
+ ---
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { run } from '../src/index.js';
4
+ import { readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
10
+
11
+ program
12
+ .name('gitrunbykaru')
13
+ .description('Run any GitHub repo locally in seconds — no setup, no friction.')
14
+ .version(pkg.version)
15
+ .argument('<repo-url>', 'GitHub repository URL (e.g. https://github.com/user/repo)')
16
+ .option('-p, --port <port>', 'preferred port (tool will still detect from app output)')
17
+ .option('--no-open', 'skip auto-opening the browser')
18
+ .option('--keep', 'keep the cloned temp directory after exit')
19
+ .action(async (repoUrl, options) => {
20
+ await run(repoUrl, options);
21
+ });
22
+
23
+ program.parse();
@@ -0,0 +1,94 @@
1
+ <svg width="100%" viewBox="0 0 680 620" role="img" xmlns="http://www.w3.org/2000/svg">
2
+ <title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">GitRun architecture and execution flow</title>
3
+ <desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Flowchart showing GitRun CLI pipeline: input → clone → detect → strategy → install → run → output</desc>
4
+ <defs>
5
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
6
+ <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
7
+ </marker>
8
+ <mask id="imagine-text-gaps-3qah53" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="620" fill="white"/><rect x="253.8087158203125" y="31.311004638671875" width="171.7318878173828" height="21.377994537353516" fill="black" rx="2"/><rect x="299.4908752441406" y="104.31100463867188" width="81.86158752441406" height="21.377992630004883" fill="black" rx="2"/><rect x="273.89019775390625" y="125.44435119628906" width="131.92237091064453" height="19.111297607421875" fill="black" rx="2"/><rect x="270.3071594238281" y="190.31101989746094" width="139.5130615234375" height="21.377992630004883" fill="black" rx="2"/><rect x="264.3807067871094" y="211.44435119628906" width="151.1907501220703" height="19.111297607421875" fill="black" rx="2"/><rect x="308.4159851074219" y="272.4443664550781" width="62.703189849853516" height="19.111297607421875" fill="black" rx="2"/><rect x="110.91288757324219" y="331.31103515625" width="58.465335845947266" height="21.377992630004883" fill="black" rx="2"/><rect x="312.0462341308594" y="331.31103515625" width="55.907535552978516" height="21.377992630004883" fill="black" rx="2"/><rect x="492.2952880859375" y="331.31103515625" width="94.94525146484375" height="21.377992630004883" fill="black" rx="2"/><rect x="297.95611572265625" y="440.3110046386719" width="84.38046264648438" height="21.377992630004883" fill="black" rx="2"/><rect x="263.2296447753906" y="461.4443664550781" width="153.54067993164062" height="19.111297607421875" fill="black" rx="2"/><rect x="297.34222412109375" y="526.31103515625" width="85.37410736083984" height="21.377992630004883" fill="black" rx="2"/><rect x="268.84326171875" y="547.4443969726562" width="142.1863250732422" height="19.111297607421875" fill="black" rx="2"/><rect x="271.0509338378906" y="593.4443969726562" width="138.1968231201172" height="19.111297607421875" fill="black" rx="2"/></mask></defs>
9
+
10
+ <!-- Entry -->
11
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
12
+ <rect x="220" y="20" width="240" height="44" rx="8" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
13
+ <text x="340" y="42" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">gitrunbykaru &lt;repo_url&gt;</text>
14
+ </g>
15
+
16
+ <line x1="340" y1="64" x2="340" y2="94" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
17
+
18
+ <!-- Clone -->
19
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
20
+ <rect x="220" y="94" width="240" height="56" rx="8" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
21
+ <text x="340" y="115" text-anchor="middle" dominant-baseline="central" style="fill:rgb(206, 203, 246);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Clone repo</text>
22
+ <text x="340" y="135" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">git clone into temp dir</text>
23
+ </g>
24
+
25
+ <line x1="340" y1="150" x2="340" y2="180" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
26
+
27
+ <!-- Detect -->
28
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
29
+ <rect x="220" y="180" width="240" height="56" rx="8" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
30
+ <text x="340" y="201" text-anchor="middle" dominant-baseline="central" style="fill:rgb(159, 225, 203);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Detect project type</text>
31
+ <text x="340" y="221" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">scan files → pick strategy</text>
32
+ </g>
33
+
34
+ <!-- Strategy branches -->
35
+ <line x1="340" y1="236" x2="340" y2="266" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
36
+
37
+ <!-- Strategy diamond label -->
38
+ <text x="340" y="282" text-anchor="middle" dominant-baseline="central" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">strategy?</text>
39
+
40
+ <line x1="195" y1="280" x2="485" y2="280" stroke="var(--color-border-secondary)" stroke-width="0.8" mask="url(#imagine-text-gaps-3qah53)" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
41
+
42
+ <!-- Node strategy -->
43
+ <line x1="220" y1="280" x2="140" y2="280" stroke="var(--color-border-secondary)" stroke-width="0.8" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
44
+ <line x1="140" y1="280" x2="140" y2="320" marker-end="url(#arrow)" stroke="var(--color-border-secondary)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
45
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
46
+ <rect x="60" y="320" width="160" height="44" rx="8" stroke-width="0.5" style="fill:rgb(99, 56, 6);stroke:rgb(239, 159, 39);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
47
+ <text x="140" y="342" text-anchor="middle" dominant-baseline="central" style="fill:rgb(250, 199, 117);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Node.js</text>
48
+ </g>
49
+
50
+ <!-- Python strategy -->
51
+ <line x1="340" y1="290" x2="340" y2="320" marker-end="url(#arrow)" stroke="var(--color-border-secondary)" mask="url(#imagine-text-gaps-3qah53)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
52
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
53
+ <rect x="260" y="320" width="160" height="44" rx="8" stroke-width="0.5" style="fill:rgb(99, 56, 6);stroke:rgb(239, 159, 39);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
54
+ <text x="340" y="342" text-anchor="middle" dominant-baseline="central" style="fill:rgb(250, 199, 117);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Python</text>
55
+ </g>
56
+
57
+ <!-- Static strategy -->
58
+ <line x1="460" y1="280" x2="540" y2="280" stroke="var(--color-border-secondary)" stroke-width="0.8" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
59
+ <line x1="540" y1="280" x2="540" y2="320" marker-end="url(#arrow)" stroke="var(--color-border-secondary)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
60
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
61
+ <rect x="460" y="320" width="160" height="44" rx="8" stroke-width="0.5" style="fill:rgb(99, 56, 6);stroke:rgb(239, 159, 39);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
62
+ <text x="540" y="342" text-anchor="middle" dominant-baseline="central" style="fill:rgb(250, 199, 117);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Static / other</text>
63
+ </g>
64
+
65
+ <!-- Converge to install -->
66
+ <line x1="140" y1="364" x2="140" y2="410" stroke="var(--color-border-secondary)" stroke-width="0.8" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
67
+ <line x1="340" y1="364" x2="340" y2="410" stroke="var(--color-border-secondary)" stroke-width="0.8" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
68
+ <line x1="540" y1="364" x2="540" y2="410" stroke="var(--color-border-secondary)" stroke-width="0.8" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
69
+ <line x1="140" y1="410" x2="540" y2="410" stroke="var(--color-border-secondary)" stroke-width="0.8" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
70
+ <line x1="340" y1="410" x2="340" y2="430" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
71
+
72
+ <!-- Install -->
73
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
74
+ <rect x="220" y="430" width="240" height="56" rx="8" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
75
+ <text x="340" y="451" text-anchor="middle" dominant-baseline="central" style="fill:rgb(206, 203, 246);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Install deps</text>
76
+ <text x="340" y="471" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">npm install / pip install / …</text>
77
+ </g>
78
+
79
+ <line x1="340" y1="486" x2="340" y2="516" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
80
+
81
+ <!-- Run -->
82
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
83
+ <rect x="220" y="516" width="240" height="56" rx="8" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
84
+ <text x="340" y="537" text-anchor="middle" dominant-baseline="central" style="fill:rgb(159, 225, 203);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Run project</text>
85
+ <text x="340" y="557" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">spawn process, tail logs</text>
86
+ </g>
87
+
88
+ <!-- Output -->
89
+ <line x1="340" y1="572" x2="340" y2="596" marker-end="url(#arrow)" mask="url(#imagine-text-gaps-3qah53)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
90
+ <g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
91
+ <rect x="200" y="596" width="280" height="14" rx="6" stroke-width="0.5" style="fill:rgb(39, 80, 10);stroke:rgb(151, 196, 89);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
92
+ <text x="340" y="603" text-anchor="middle" dominant-baseline="central" style="fill:rgb(151, 196, 89);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">→ http://localhost:3000</text>
93
+ </g>
94
+ </svg>
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "gitrunbykaru",
3
+ "version": "1.0.0",
4
+ "description": "Run any GitHub repo locally in seconds — no setup, no friction.",
5
+ "type": "module",
6
+ "bin": {
7
+ "gitrunbykaru": "./bin/gitrunbykaru.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "scripts": {
11
+ "start": "node bin/gitrunbykaru.js",
12
+ "test": "node tests/test.js"
13
+ },
14
+ "keywords": [
15
+ "cli",
16
+ "github",
17
+ "runner",
18
+ "developer-tools",
19
+ "automation"
20
+ ],
21
+ "author": "Karthikeya Dusi",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Karthikeyadusi/gitrunbykaru.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/Karthikeyadusi/gitrunbykaru/issues"
28
+ },
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "chalk": "^5.3.0",
32
+ "commander": "^11.1.0",
33
+ "open": "^10.0.3",
34
+ "ora": "^8.0.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ }
39
+ }
package/src/clone.js ADDED
@@ -0,0 +1,48 @@
1
+ import { mkdtempSync, existsSync, realpathSync } from 'fs';
2
+ import { tmpdir } from 'os';
3
+ import { join } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { log, createSpinner } from './logger.js';
6
+
7
+ export async function cloneRepo(url) {
8
+ // Verify git is available
9
+ try {
10
+ execSync('git --version', { stdio: 'ignore' });
11
+ } catch {
12
+ throw new Error('git is not installed or not in PATH. Please install git first.');
13
+ }
14
+
15
+ // Extract repo name for a friendlier temp dir name
16
+ const repoName = url.split('/').slice(-2).join('-').replace(/[^a-zA-Z0-9-_]/g, '');
17
+ const tmpBase = tmpdir();
18
+ // Create temp dir and immediately resolve it to its real absolute path (fixes Windows 8.3 short path bugs with Vite/React)
19
+ let tmpDir = mkdtempSync(join(tmpBase, `gitrun-${repoName}-`));
20
+ if (process.platform === 'win32') {
21
+ tmpDir = realpathSync.native(tmpDir);
22
+ } else {
23
+ tmpDir = realpathSync(tmpDir);
24
+ }
25
+
26
+ const spinner = createSpinner(`Cloning ${url}`);
27
+ spinner.start();
28
+
29
+ try {
30
+ execSync(`git clone --depth 1 "${url}" "${tmpDir}"`, {
31
+ stdio: 'pipe',
32
+ timeout: 300000, // 5 minutes timeout for large repos or slower networks
33
+ });
34
+ spinner.succeed(`Cloned ${url}`);
35
+ return tmpDir;
36
+ } catch (err) {
37
+ spinner.fail('Clone failed');
38
+ // Try to give a useful error
39
+ const stderr = err.stderr?.toString() || err.message;
40
+ if (stderr.includes('not found') || stderr.includes('does not exist')) {
41
+ throw new Error(`Repository not found: ${url}\nCheck the URL is correct and the repo is public.`);
42
+ }
43
+ if (stderr.includes('Could not resolve host')) {
44
+ throw new Error('No internet connection — could not reach GitHub.');
45
+ }
46
+ throw new Error(`git clone failed:\n${stderr}`);
47
+ }
48
+ }
package/src/detect.js ADDED
@@ -0,0 +1,149 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { log } from './logger.js';
4
+
5
+ /**
6
+ * Scans a directory and returns a detection result:
7
+ * { type, label, runCommand, installCommand, port }
8
+ */
9
+ export async function detectProject(dir) {
10
+ const files = readdirSync(dir);
11
+ const has = (f) => existsSync(join(dir, f));
12
+
13
+ log.step('Detecting project type...');
14
+
15
+ // ── Node.js ──────────────────────────────────────────────────────────────
16
+ if (has('package.json')) {
17
+ let pkg = {};
18
+ try {
19
+ pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
20
+ } catch { /* malformed package.json — still treat as Node */ }
21
+
22
+ const scripts = pkg.scripts || {};
23
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
24
+
25
+ // Detect package manager
26
+ let pm = 'npm';
27
+ let installCommand = 'npm install';
28
+ if (has('yarn.lock')) {
29
+ pm = 'yarn';
30
+ installCommand = 'yarn install';
31
+ } else if (has('pnpm-lock.yaml')) {
32
+ pm = 'pnpm';
33
+ installCommand = 'pnpm install';
34
+ } else if (has('bun.lockb') || has('bun.lock')) {
35
+ pm = 'bun';
36
+ installCommand = 'bun install';
37
+ } else if (has('package-lock.json') || has('npm-shrinkwrap.json')) {
38
+ installCommand = 'npm ci --prefer-offline';
39
+ }
40
+
41
+ // Pick best run command in priority order
42
+ let runCommand = null;
43
+ let label = 'Node.js';
44
+ const runPrefix = pm === 'npm' ? 'npm run ' : `${pm} `;
45
+
46
+ if (scripts.dev) { runCommand = `${runPrefix}dev`; }
47
+ else if (scripts.start) { runCommand = pm === 'npm' ? 'npm start' : `${pm} start`; }
48
+ else if (scripts.serve) { runCommand = `${runPrefix}serve`; }
49
+ else if (scripts.build && scripts.preview) { runCommand = `${runPrefix}build && ${runPrefix}preview`; } // For Vite/Svelte sometimes
50
+ else { return null; } // Give up and fall through to manual prompt instead of crashing on 'node index.js'
51
+
52
+ // Fix for package managers via npx
53
+ if (runCommand.startsWith('yarn') || runCommand.startsWith('pnpm') || runCommand.startsWith('bun')) {
54
+ runCommand = `npx ${runCommand}`;
55
+ }
56
+
57
+ // Detect framework for a nicer label
58
+ if (deps?.next) label = `Next.js (${runCommand})`;
59
+ else if (deps?.nuxt) label = `Nuxt.js (${runCommand})`;
60
+ else if (deps?.vite) label = `Vite app (${runCommand})`;
61
+ else if (deps?.react) label = 'React app';
62
+ else if (deps?.express) label = 'Express.js server';
63
+ else if (deps?.fastify) label = 'Fastify server';
64
+ else label = `Node.js (${runCommand})`;
65
+
66
+ return {
67
+ type: 'node',
68
+ label: `${label} via ${pm}`,
69
+ runCommand,
70
+ installCommand,
71
+ port: detectPortFromEnv(dir) || 3000,
72
+ };
73
+ }
74
+
75
+ // ── Python ───────────────────────────────────────────────────────────────
76
+ if (has('requirements.txt') || has('pyproject.toml') || has('setup.py') || has('Pipfile')) {
77
+ let runCommand = 'python main.py';
78
+ let label = 'Python app';
79
+
80
+ if (has('manage.py')) {
81
+ runCommand = 'python manage.py runserver';
82
+ label = 'Django app';
83
+ } else if (has('app.py')) {
84
+ runCommand = 'python app.py';
85
+ label = 'Python/Flask app';
86
+ } else if (has('main.py')) {
87
+ runCommand = 'python main.py';
88
+ label = 'Python app (main.py)';
89
+ } else if (has('run.py')) {
90
+ runCommand = 'python run.py';
91
+ label = 'Python app (run.py)';
92
+ } else if (has('server.py')) {
93
+ runCommand = 'python server.py';
94
+ label = 'Python server (server.py)';
95
+ }
96
+
97
+ // Check pyproject.toml for scripts
98
+ if (has('pyproject.toml')) {
99
+ try {
100
+ const content = readFileSync(join(dir, 'pyproject.toml'), 'utf8');
101
+ if (content.includes('fastapi')) { label = 'FastAPI app'; }
102
+ if (content.includes('flask')) { label = 'Flask app'; }
103
+ } catch { /* ignore */ }
104
+ }
105
+
106
+ let installCommand = 'pip install -r requirements.txt';
107
+ if (!has('requirements.txt') && has('pyproject.toml')) {
108
+ installCommand = 'pip install -e .';
109
+ } else if (has('Pipfile')) {
110
+ installCommand = 'pipenv install';
111
+ }
112
+
113
+ return {
114
+ type: 'python',
115
+ label,
116
+ runCommand,
117
+ installCommand,
118
+ port: detectPortFromEnv(dir) || 8000,
119
+ };
120
+ }
121
+
122
+ // ── Static HTML ──────────────────────────────────────────────────────────
123
+ if (has('index.html') || files.some(f => f.endsWith('.html'))) {
124
+ return {
125
+ type: 'static',
126
+ label: 'Static HTML site',
127
+ runCommand: null, // handled by static strategy
128
+ installCommand: null,
129
+ port: 8080,
130
+ };
131
+ }
132
+
133
+ // ── Unknown ──────────────────────────────────────────────────────────────
134
+ log.dim(`Files found: ${files.slice(0, 10).join(', ')}`);
135
+ return null;
136
+ }
137
+
138
+ /** Try to read a .env file for a PORT hint */
139
+ function detectPortFromEnv(dir) {
140
+ try {
141
+ const envPath = join(dir, '.env.example') || join(dir, '.env');
142
+ if (existsSync(envPath)) {
143
+ const content = readFileSync(envPath, 'utf8');
144
+ const match = content.match(/^PORT\s*=\s*(\d+)/m);
145
+ if (match) return parseInt(match[1], 10);
146
+ }
147
+ } catch { /* ignore */ }
148
+ return null;
149
+ }
package/src/index.js ADDED
@@ -0,0 +1,101 @@
1
+ import { cloneRepo } from './clone.js';
2
+ import { detectProject } from './detect.js';
3
+ import { getStrategy } from './strategies/index.js';
4
+ import { spawnProject } from './runner.js';
5
+ import { log, printBanner, printSuccess, printError, printWarning } from './logger.js';
6
+ import { rmSync } from 'fs';
7
+ import * as readline from 'readline/promises';
8
+
9
+ export async function run(repoUrl, options = {}) {
10
+ printBanner();
11
+
12
+ // Validate URL
13
+ if (!repoUrl || !repoUrl.includes('github.com')) {
14
+ printError('Please provide a valid GitHub URL.');
15
+ printError('Example: gitrunbykaru https://github.com/user/repo');
16
+ process.exit(1);
17
+ }
18
+
19
+ // Normalize URL (strip .git suffix if present)
20
+ const url = repoUrl.replace(/\.git$/, '');
21
+ let tmpDir = null;
22
+
23
+ // Cleanup handler
24
+ const cleanup = () => {
25
+ if (tmpDir && !options.keep) {
26
+ try {
27
+ rmSync(tmpDir, { recursive: true, force: true });
28
+ log.dim(`Cleaned up ${tmpDir}`);
29
+ } catch {
30
+ // best effort
31
+ }
32
+ }
33
+ process.exit(0);
34
+ };
35
+
36
+ process.on('SIGINT', cleanup);
37
+ process.on('SIGTERM', cleanup);
38
+ process.on('exit', () => {
39
+ if (tmpDir && !options.keep) {
40
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best effort */ }
41
+ }
42
+ });
43
+
44
+ try {
45
+ // Step 1: Clone
46
+ tmpDir = await cloneRepo(url);
47
+
48
+ // Step 2: Detect
49
+ let detection = await detectProject(tmpDir);
50
+
51
+ if (!detection) {
52
+ printWarning("Couldn't detect project type automatically.");
53
+
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
+ console.log('\nWhat type of project is this?');
56
+ console.log('1) Node.js');
57
+ console.log('2) Python');
58
+ console.log('3) Static HTML');
59
+ console.log('0) Exit');
60
+
61
+ const answer = await rl.question('\nEnter number (1-3): ');
62
+
63
+ if (answer === '1') {
64
+ const cmd = await rl.question('Run command (default: npm start): ');
65
+ detection = { type: 'node', label: 'Node.js (manual)', runCommand: cmd || 'npm start', installCommand: 'npm install', port: 3000 };
66
+ } else if (answer === '2') {
67
+ const cmd = await rl.question('Run command (default: python main.py): ');
68
+ detection = { type: 'python', label: 'Python (manual)', runCommand: cmd || 'python main.py', installCommand: 'pip install -r requirements.txt', port: 8000 };
69
+ } else if (answer === '3') {
70
+ detection = { type: 'static', label: 'Static HTML (manual)', runCommand: null, installCommand: null, port: 8080 };
71
+ } else {
72
+ process.exit(1);
73
+ }
74
+ rl.close();
75
+ }
76
+
77
+ log.info(`Detected ${detection.label}`);
78
+
79
+ // Step 3: Get strategy
80
+ const strategy = getStrategy(detection.type);
81
+
82
+ if (!strategy) {
83
+ printWarning(`No strategy available for "${detection.type}" projects yet.`);
84
+ printWarning('Supported: Node.js, Python, Static HTML');
85
+ process.exit(1);
86
+ }
87
+
88
+ // Step 4: Install
89
+ await strategy.install(tmpDir, detection);
90
+
91
+ // Step 5: Run
92
+ await spawnProject(tmpDir, detection, strategy, options);
93
+
94
+ } catch (err) {
95
+ printError(err.message || String(err));
96
+ if (tmpDir && !options.keep) {
97
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best effort */ }
98
+ }
99
+ process.exit(1);
100
+ }
101
+ }
package/src/logger.js ADDED
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+
4
+ // ── Symbols ────────────────────────────────────────────────────────────────
5
+ const SYM = {
6
+ info: chalk.cyan('◆'),
7
+ success: chalk.green('✔'),
8
+ warn: chalk.yellow('⚠'),
9
+ error: chalk.red('✖'),
10
+ step: chalk.magenta('→'),
11
+ dim: chalk.gray('·'),
12
+ };
13
+
14
+ // ── Core log functions ─────────────────────────────────────────────────────
15
+ export const log = {
16
+ info: (msg) => console.log(` ${SYM.info} ${chalk.cyan(msg)}`),
17
+ step: (msg) => console.log(` ${SYM.step} ${chalk.white(msg)}`),
18
+ success: (msg) => console.log(` ${SYM.success} ${chalk.green(msg)}`),
19
+ warn: (msg) => console.log(` ${SYM.warn} ${chalk.yellow(msg)}`),
20
+ error: (msg) => console.log(` ${SYM.error} ${chalk.red(msg)}`),
21
+ dim: (msg) => console.log(` ${chalk.gray(msg)}`),
22
+ };
23
+
24
+ // ── Spinner ────────────────────────────────────────────────────────────────
25
+ export function createSpinner(text) {
26
+ return ora({
27
+ text: ` ${text}`,
28
+ spinner: 'dots',
29
+ color: 'magenta',
30
+ prefixText: ' ',
31
+ });
32
+ }
33
+
34
+ // ── Banner ─────────────────────────────────────────────────────────────────
35
+ export function printBanner() {
36
+ console.log('');
37
+ console.log(
38
+ chalk.bold.magenta(' GitRun') +
39
+ chalk.gray(' by karu') +
40
+ chalk.dim(' — run any GitHub repo in seconds')
41
+ );
42
+ console.log(chalk.gray(' ' + '─'.repeat(46)));
43
+ console.log('');
44
+ }
45
+
46
+ // ── Success (the big moment) ───────────────────────────────────────────────
47
+ export function printSuccess(url) {
48
+ console.log('');
49
+ console.log(chalk.gray(' ' + '─'.repeat(46)));
50
+ console.log('');
51
+ console.log(
52
+ ` ${chalk.bold.green('✔ Ready')} ${chalk.bold.underline.cyan(url)}`
53
+ );
54
+ console.log('');
55
+ console.log(chalk.gray(' Press Ctrl+C to stop'));
56
+ console.log('');
57
+ }
58
+
59
+ // ── Error ──────────────────────────────────────────────────────────────────
60
+ export function printError(msg) {
61
+ console.log('');
62
+ console.log(` ${chalk.bold.red('✖ Error')} ${chalk.red(msg)}`);
63
+ console.log('');
64
+ }
65
+
66
+ // ── Warning ───────────────────────────────────────────────────────────────
67
+ export function printWarning(msg) {
68
+ console.log(` ${chalk.bold.yellow('⚠')} ${chalk.yellow(msg)}`);
69
+ }
package/src/runner.js ADDED
@@ -0,0 +1,184 @@
1
+ import { spawn, spawnSync } from 'child_process';
2
+ import { log, printSuccess, printError, printWarning } from './logger.js';
3
+ import openBrowser from 'open';
4
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ const PORT_TIMEOUT_MS = 45000; // Wait up to 45s for the port to appear in output
8
+
9
+ export async function spawnProject(dir, detection, strategy, options = {}) {
10
+ const runCmd = strategy.getRunCommand(detection);
11
+ const portPattern = strategy.portPattern;
12
+
13
+ // Auto-mocking .env files
14
+ const envPath = join(dir, '.env');
15
+ if (!existsSync(envPath)) {
16
+ const examples = ['.env.example', '.env.local.example', '.env.sample', '.env.template', 'env.example'];
17
+ const foundExample = examples.find(ex => existsSync(join(dir, ex)));
18
+
19
+ if (foundExample) {
20
+ log.dim(`→ Auto-generating .env from ${foundExample}`);
21
+ const content = readFileSync(join(dir, foundExample), 'utf8');
22
+
23
+ // Inject dummy keys into empty variables to prevent immediate crashes
24
+ const mockedContent = content.split('\n').map(line => {
25
+ const trimmed = line.trim();
26
+ // Ignore lines that don't look like variable definitions
27
+ if (!trimmed || trimmed.startsWith('#')) return line;
28
+
29
+ // Handle "KEY=VALUE", "KEY=", "KEY" situations
30
+ const splitIdx = trimmed.indexOf('=');
31
+ // If no '=', it might just be listed in the example without a value
32
+ if (splitIdx === -1) {
33
+ const key = trimmed;
34
+ let val = 'gitrun_dummy_key_12345';
35
+ if (key.toUpperCase().includes('URL') || key.toUpperCase().includes('URI') || key.toUpperCase().includes('ENDPOINT')) {
36
+ val = 'http://localhost:9999';
37
+ }
38
+ return `${key}=${val}`;
39
+ }
40
+
41
+ const key = trimmed.slice(0, splitIdx).trim();
42
+ let val = trimmed.slice(splitIdx + 1).trim();
43
+
44
+ // Check for common indicator that it's empty / needs filling
45
+ if (!val || val === '""' || val === "''" || val === '<your-key-here>' || val.includes('<') || val.includes('[')) {
46
+ if (key.toUpperCase().includes('URL') || key.toUpperCase().includes('URI') || key.toUpperCase().includes('ENDPOINT')) {
47
+ val = 'http://localhost:9999';
48
+ } else {
49
+ val = 'gitrun_dummy_key_12345';
50
+ }
51
+ }
52
+ return `${key}=${val}`;
53
+ }).join('\n');
54
+
55
+ writeFileSync(envPath, mockedContent);
56
+ }
57
+ }
58
+
59
+ log.step(`Starting ${runCmd}`);
60
+ log.dim('─'.repeat(48));
61
+
62
+ return new Promise((resolve, reject) => {
63
+ // Check if the command is a package manager we need to run via npx
64
+ let fullCommand = runCmd;
65
+ if (runCmd.startsWith('yarn ') && !checkCommand('yarn')) fullCommand = `npx ${runCmd}`;
66
+ if (runCmd.startsWith('pnpm ') && !checkCommand('pnpm')) fullCommand = `npx ${runCmd}`;
67
+ if (runCmd.startsWith('bun ') && !checkCommand('bun')) fullCommand = `npx ${runCmd}`;
68
+
69
+ // Split command into binary + args (handles quoted paths)
70
+ const [bin, ...args] = parseCommand(fullCommand);
71
+
72
+ const child = spawn(bin, args, {
73
+ cwd: dir,
74
+ stdio: ['ignore', 'pipe', 'pipe'],
75
+ shell: process.platform === 'win32', // Windows needs shell for npx etc.
76
+ env: {
77
+ ...process.env,
78
+ // Disable color stripping — let the app output naturally
79
+ FORCE_COLOR: '1',
80
+ // Some frameworks respect this for port
81
+ PORT: String(detection.port),
82
+ },
83
+ });
84
+
85
+ let portFound = false;
86
+ let portTimer = null;
87
+
88
+ const checkForPort = (line) => {
89
+ if (portFound) return;
90
+ // Strip ANSI color codes from the terminal output before matching
91
+ const cleanLine = line.replace(/\x1b\[[0-9;]*[mGK]/g, '');
92
+ const match = cleanLine.match(portPattern);
93
+ if (match) {
94
+ const port = match[1] || match[2] || match[3] || match[4];
95
+ if (port) {
96
+ portFound = true;
97
+ clearTimeout(portTimer);
98
+ const url = `http://localhost:${port}`;
99
+ printSuccess(url);
100
+ if (options.open !== false) {
101
+ // Small delay so the server is actually ready
102
+ setTimeout(() => openBrowser(url).catch(() => {}), 800);
103
+ }
104
+ }
105
+ }
106
+ };
107
+
108
+ child.stdout.on('data', (data) => {
109
+ const text = data.toString();
110
+ process.stdout.write(text);
111
+ text.split('\n').forEach(checkForPort);
112
+ });
113
+
114
+ child.stderr.on('data', (data) => {
115
+ const text = data.toString();
116
+ process.stderr.write(text);
117
+ text.split('\n').forEach(checkForPort);
118
+ });
119
+
120
+ child.on('error', (err) => {
121
+ clearTimeout(portTimer);
122
+ if (err.code === 'ENOENT') {
123
+ reject(new Error(`Command not found: "${bin}"\nMake sure it's installed and in your PATH.`));
124
+ } else {
125
+ reject(err);
126
+ }
127
+ });
128
+
129
+ child.on('close', (code) => {
130
+ clearTimeout(portTimer);
131
+ if (code !== 0 && code !== null) {
132
+ log.dim('─'.repeat(48));
133
+ printError(`Process exited with code ${code}`);
134
+ printWarning('The project crashed. Check the output above for errors.');
135
+ }
136
+ resolve();
137
+ });
138
+
139
+ // If no port detected in time, show a helpful hint
140
+ portTimer = setTimeout(() => {
141
+ if (!portFound) {
142
+ printWarning(`No port detected after ${PORT_TIMEOUT_MS / 1000}s.`);
143
+ printWarning(`Try opening http://localhost:${detection.port} manually.`);
144
+ }
145
+ }, PORT_TIMEOUT_MS);
146
+
147
+ // Forward SIGINT to child so it can clean up
148
+ process.on('SIGINT', () => {
149
+ child.kill('SIGINT');
150
+ });
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Naive command parser: splits on spaces but respects double-quoted segments.
156
+ * Handles paths like: "C:\Program Files\Python\python" run app.py
157
+ */
158
+ function parseCommand(cmd) {
159
+ const parts = [];
160
+ let current = '';
161
+ let inQuote = false;
162
+
163
+ for (let i = 0; i < cmd.length; i++) {
164
+ const ch = cmd[i];
165
+ if (ch === '"') {
166
+ inQuote = !inQuote;
167
+ } else if (ch === ' ' && !inQuote) {
168
+ if (current) { parts.push(current); current = ''; }
169
+ } else {
170
+ current += ch;
171
+ }
172
+ }
173
+ if (current) parts.push(current);
174
+ return parts;
175
+ }
176
+
177
+ function checkCommand(cmd) {
178
+ try {
179
+ const result = spawnSync(cmd, ['--version'], { stdio: 'pipe', shell: process.platform === 'win32' });
180
+ return result.status === 0;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
@@ -0,0 +1,13 @@
1
+ import { nodeStrategy } from './node.js';
2
+ import { pythonStrategy } from './python.js';
3
+ import { staticStrategy } from './static.js';
4
+
5
+ const strategies = {
6
+ node: nodeStrategy,
7
+ python: pythonStrategy,
8
+ static: staticStrategy,
9
+ };
10
+
11
+ export function getStrategy(type) {
12
+ return strategies[type] || null;
13
+ }
@@ -0,0 +1,45 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+ import { createSpinner, log } from '../logger.js';
3
+
4
+ export const nodeStrategy = {
5
+ name: 'node',
6
+
7
+ async install(dir, detection) {
8
+ const spinner = createSpinner(`Installing dependencies...`);
9
+ spinner.start();
10
+ try {
11
+ // If they use a package manager other than npm, make sure it executes via corepack/npx if not installed
12
+ let installCmd = detection.installCommand;
13
+ if (installCmd.startsWith('yarn') && !checkCommand('yarn')) installCmd = `npx ${installCmd}`;
14
+ if (installCmd.startsWith('pnpm') && !checkCommand('pnpm')) installCmd = `npx ${installCmd}`;
15
+ if (installCmd.startsWith('bun') && !checkCommand('bun')) installCmd = `npx ${installCmd}`;
16
+
17
+ execSync(installCmd, {
18
+ cwd: dir,
19
+ stdio: 'pipe',
20
+ timeout: 600000,
21
+ });
22
+ spinner.succeed(`Installed node_modules ready`);
23
+ } catch (err) {
24
+ spinner.fail('Dependency install failed');
25
+ const stderr = err.stderr?.toString() || err.message;
26
+ throw new Error(`Dependency install failed (Exit code: ${err.status}):\n${stderr.slice(0, 1500)}`);
27
+ }
28
+ },
29
+
30
+ getRunCommand(detection) {
31
+ return detection.runCommand;
32
+ },
33
+
34
+ // Pattern to detect the running port from stdout/stderr
35
+ portPattern: /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):(\d{4,5})|(?:port|PORT|listening on)\s*:?\s*(\d{4,5})|(?:running at|started on)\s+.*?:(\d{4,5})/i,
36
+ };
37
+
38
+ function checkCommand(cmd) {
39
+ try {
40
+ const result = spawnSync(cmd, ['--version'], { stdio: 'pipe', shell: process.platform === 'win32' });
41
+ return result.status === 0;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
@@ -0,0 +1,79 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+ import { existsSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { createSpinner, log } from '../logger.js';
5
+
6
+ export const pythonStrategy = {
7
+ name: 'python',
8
+
9
+ async install(dir, detection) {
10
+ // Check python is available
11
+ const pyBin = getPythonBin();
12
+ if (!pyBin) {
13
+ throw new Error('Python is not installed or not in PATH. Please install Python 3.');
14
+ }
15
+
16
+ if (!detection.installCommand) {
17
+ log.dim('No install command needed for this Python project.');
18
+ return;
19
+ }
20
+
21
+ const spinner = createSpinner('Installing Python dependencies...');
22
+ spinner.start();
23
+
24
+ try {
25
+ // Create a venv to avoid polluting global Python
26
+ const venvDir = join(dir, '.gitrun-venv');
27
+ if (!existsSync(venvDir)) {
28
+ execSync(`${pyBin} -m venv "${venvDir}"`, { cwd: dir, stdio: 'pipe' });
29
+ }
30
+
31
+ // Use venv pip
32
+ const pipBin = process.platform === 'win32'
33
+ ? join(venvDir, 'Scripts', 'pip')
34
+ : join(venvDir, 'bin', 'pip');
35
+
36
+ const installCmd = detection.installCommand
37
+ .replace(/^pip install/, `"${pipBin}" install`)
38
+ .replace(/^pipenv install/, `"${pipBin}" install`);
39
+
40
+ execSync(installCmd, {
41
+ cwd: dir,
42
+ stdio: 'pipe',
43
+ timeout: 120000,
44
+ });
45
+
46
+ // Store venv path so runner can use the right python binary
47
+ detection._venvDir = venvDir;
48
+ detection._pyBin = process.platform === 'win32'
49
+ ? join(venvDir, 'Scripts', 'python')
50
+ : join(venvDir, 'bin', 'python');
51
+
52
+ spinner.succeed('Installed Python packages ready');
53
+ } catch (err) {
54
+ spinner.fail('pip install failed');
55
+ const stderr = err.stderr?.toString() || err.message;
56
+ throw new Error(`Python dependency install failed:\n${stderr.slice(0, 500)}`);
57
+ }
58
+ },
59
+
60
+ getRunCommand(detection) {
61
+ if (detection._pyBin) {
62
+ // Use the venv python binary
63
+ return detection.runCommand.replace(/^python/, `"${detection._pyBin}"`);
64
+ }
65
+ return detection.runCommand;
66
+ },
67
+
68
+ portPattern: /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):(\d{4,5})|(?:Running on|Serving on|Uvicorn running on)\s+.*?:(\d{4,5})|port\s+(\d{4,5})/i,
69
+ };
70
+
71
+ function getPythonBin() {
72
+ for (const bin of ['python3', 'python']) {
73
+ try {
74
+ const result = spawnSync(bin, ['--version'], { stdio: 'pipe' });
75
+ if (result.status === 0) return bin;
76
+ } catch { /* try next */ }
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,35 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+ import { createSpinner, log } from '../logger.js';
3
+
4
+ export const staticStrategy = {
5
+ name: 'static',
6
+
7
+ async install(dir, detection) {
8
+ // No install needed for static sites
9
+ log.dim('Static site — no dependencies to install.');
10
+ },
11
+
12
+ getRunCommand(detection) {
13
+ // Prefer npx serve, fall back to python http.server
14
+ const hasNode = checkCommand('node');
15
+ const hasPython = checkCommand('python3') || checkCommand('python');
16
+
17
+ if (hasNode) {
18
+ return `npx serve . -p ${detection.port} --no-clipboard`;
19
+ } else if (hasPython) {
20
+ return `python3 -m http.server ${detection.port}`;
21
+ }
22
+ throw new Error('Neither Node.js nor Python found — cannot serve static files.');
23
+ },
24
+
25
+ portPattern: /(?:localhost|127\.0\.0\.1):(\d{4,5})|Serving!\s+.*?:(\d{4,5})|port\s+(\d{4,5})/i,
26
+ };
27
+
28
+ function checkCommand(cmd) {
29
+ try {
30
+ const result = spawnSync(cmd, ['--version'], { stdio: 'pipe' });
31
+ return result.status === 0;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }