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 +94 -0
- package/bin/gitrunbykaru.js +23 -0
- package/gitrun_architecture.svg +94 -0
- package/package.json +39 -0
- package/src/clone.js +48 -0
- package/src/detect.js +149 -0
- package/src/index.js +101 -0
- package/src/logger.js +69 -0
- package/src/runner.js +184 -0
- package/src/strategies/index.js +13 -0
- package/src/strategies/node.js +45 -0
- package/src/strategies/python.js +79 -0
- package/src/strategies/static.js +35 -0
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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">gitrunbykaru <repo_url></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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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
|
+
}
|