pondorasti 0.1.1
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 +128 -0
- package/package.json +38 -0
- package/src/commands/brew.ts +89 -0
- package/src/commands/clone.ts +83 -0
- package/src/index.ts +22 -0
- package/src/tools/homebrew.ts +58 -0
- package/src/utils/cli-helpers.ts +28 -0
- package/src/utils/github.test.ts +93 -0
- package/src/utils/github.ts +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Pondorasti CLI
|
|
2
|
+
|
|
3
|
+
🖥️ Mission control for pondorasti - A command-line tool for automated macOS setup and configuration.
|
|
4
|
+
|
|
5
|
+
Built with [Bun](https://bun.sh), [Yargs](https://yargs.js.org/) for command parsing, and [Ink](https://github.com/vadimdemedes/ink) for rich terminal UI when needed.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🍺 **Homebrew Management**: Install, update, and manage Homebrew packages
|
|
10
|
+
- 📂 **Smart Cloning**: Clone GitHub repos to organized `~/repos/<owner>/<repo>` structure
|
|
11
|
+
- 📦 **Native Output**: Shows real brew output for transparency
|
|
12
|
+
- 🚀 **Fast**: Powered by Bun for blazing-fast execution
|
|
13
|
+
- 🔧 **Extensible**: Easy to add new commands
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- macOS (optimized for Apple Silicon)
|
|
18
|
+
- [Bun](https://bun.sh) runtime
|
|
19
|
+
- [GitHub CLI](https://cli.github.com/) (`gh`) for clone command
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Install Bun if you haven't already
|
|
25
|
+
curl -fsSL https://bun.sh/install | bash
|
|
26
|
+
|
|
27
|
+
# Clone the repository
|
|
28
|
+
git clone https://github.com/pondorasti/pondorasti.git
|
|
29
|
+
cd pondorasti
|
|
30
|
+
|
|
31
|
+
# Install dependencies
|
|
32
|
+
bun install
|
|
33
|
+
|
|
34
|
+
# Run commands
|
|
35
|
+
bun run packages/cli/src/index.ts <command>
|
|
36
|
+
|
|
37
|
+
# Or install globally
|
|
38
|
+
bun link
|
|
39
|
+
pondorasti <command>
|
|
40
|
+
# or use the short alias
|
|
41
|
+
pd <command>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Commands
|
|
47
|
+
|
|
48
|
+
#### `clone` - Clone GitHub repositories
|
|
49
|
+
|
|
50
|
+
Clones repositories to `~/repos/<owner>/<repo>` and opens a shell in the directory.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Clone using various URL formats
|
|
54
|
+
pd clone https://github.com/owner/repo
|
|
55
|
+
pd clone git@github.com:owner/repo.git
|
|
56
|
+
pd clone owner/repo
|
|
57
|
+
|
|
58
|
+
# Also works with tree/blob URLs (branch/file paths are stripped)
|
|
59
|
+
pd clone https://github.com/owner/repo/tree/main
|
|
60
|
+
pd clone https://github.com/owner/repo/blob/main/src/file.ts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### `brew` - Manage Homebrew
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Install Homebrew
|
|
67
|
+
pd brew install
|
|
68
|
+
|
|
69
|
+
# Run brew bundle from Brewfile
|
|
70
|
+
pd brew bundle
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Global Options
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
--help, -h Show help
|
|
77
|
+
--version, -v Show version
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Run in development mode
|
|
84
|
+
bun dev
|
|
85
|
+
|
|
86
|
+
# Run tests
|
|
87
|
+
bun test
|
|
88
|
+
|
|
89
|
+
# Build for production
|
|
90
|
+
bun build --compile --outfile=pondorasti src/index.ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Architecture
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
packages/cli/
|
|
97
|
+
├── src/
|
|
98
|
+
│ ├── index.ts # CLI entry with yargs
|
|
99
|
+
│ ├── commands/ # Command implementations
|
|
100
|
+
│ │ ├── brew.ts # Homebrew management
|
|
101
|
+
│ │ └── clone.ts # GitHub repo cloning
|
|
102
|
+
│ ├── tools/ # External tool wrappers
|
|
103
|
+
│ │ └── homebrew.ts # Homebrew operations
|
|
104
|
+
│ └── utils/ # Utilities
|
|
105
|
+
│ ├── cli-helpers.ts # CLI utilities
|
|
106
|
+
│ ├── github.ts # GitHub URL parsing
|
|
107
|
+
│ └── github.test.ts # Tests for GitHub utils
|
|
108
|
+
├── Brewfile # Package definitions (at repo root)
|
|
109
|
+
└── package.json # Project configuration
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Future Commands
|
|
113
|
+
|
|
114
|
+
- `dotfiles` - Manage dotfiles and configurations
|
|
115
|
+
- `macos` - Configure macOS system preferences
|
|
116
|
+
- `setup` - Full system setup wizard
|
|
117
|
+
|
|
118
|
+
## Contributing
|
|
119
|
+
|
|
120
|
+
1. Fork the repository
|
|
121
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
122
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
123
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
124
|
+
5. Open a Pull Request
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pondorasti",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI for pondorasti",
|
|
5
|
+
"author": "Alexandru Ţurcanu",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"cli",
|
|
10
|
+
"macos",
|
|
11
|
+
"homebrew",
|
|
12
|
+
"dotfiles",
|
|
13
|
+
"setup"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"pondorasti": "./src/index.ts",
|
|
20
|
+
"pd": "./src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "bun run src/index.ts",
|
|
24
|
+
"dev": "bun --watch src/index.ts",
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ink": "^6.5.1",
|
|
29
|
+
"ink-gradient": "^3.0.0",
|
|
30
|
+
"ink-spinner": "^5.0.0",
|
|
31
|
+
"react": "^19.2.0",
|
|
32
|
+
"yargs": "^18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.2.7",
|
|
36
|
+
"@types/yargs": "^17.0.35"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { $ } from "bun"
|
|
3
|
+
import { Homebrew } from "../tools/homebrew"
|
|
4
|
+
import { failHandler } from "../utils/cli-helpers"
|
|
5
|
+
import * as path from "path"
|
|
6
|
+
import * as fs from "fs"
|
|
7
|
+
|
|
8
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
9
|
+
// Subcommands
|
|
10
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const installCommand: CommandModule = {
|
|
13
|
+
command: "install",
|
|
14
|
+
describe: "Install Homebrew",
|
|
15
|
+
handler: async () => {
|
|
16
|
+
if (Homebrew.isInstalled()) {
|
|
17
|
+
const brewPath = Homebrew.getBrewPath()
|
|
18
|
+
console.log(`✓ Homebrew is already installed at ${brewPath}`)
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await $`brew --version`
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error("⚠ Homebrew is installed but not working properly")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await $`/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("✗ Failed to install Homebrew")
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const bundleCommand: CommandModule = {
|
|
39
|
+
command: "bundle",
|
|
40
|
+
describe: "Run brew bundle from Brewfile",
|
|
41
|
+
handler: async () => {
|
|
42
|
+
if (!Homebrew.isInstalled()) {
|
|
43
|
+
console.error("✗ Homebrew is not installed. Run 'pondorasti brew install' first.")
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Brewfile is always at the root of the pondorasti repo
|
|
48
|
+
const brewfilePath = path.join(__dirname, "..", "..", "..", "..", "Brewfile")
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(brewfilePath)) {
|
|
51
|
+
console.error("✗ Brewfile not found at expected location:", brewfilePath)
|
|
52
|
+
console.error("This is likely a bug in the CLI configuration.")
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await Homebrew.runBundle(brewfilePath)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("✗ Failed to run brew bundle")
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
66
|
+
// Main brew command
|
|
67
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const brewCommand: CommandModule = {
|
|
70
|
+
command: "brew",
|
|
71
|
+
describe: "Manage Homebrew and install packages",
|
|
72
|
+
builder: (yargs) => {
|
|
73
|
+
return yargs //
|
|
74
|
+
.command(installCommand)
|
|
75
|
+
.command(bundleCommand)
|
|
76
|
+
.demandCommand(1)
|
|
77
|
+
.help()
|
|
78
|
+
.strict()
|
|
79
|
+
.fail(failHandler)
|
|
80
|
+
},
|
|
81
|
+
handler: () => {
|
|
82
|
+
// This handler is called when no subcommand is provided
|
|
83
|
+
// But the fail handler above will handle it
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
export default brewCommand
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { $, spawn } from "bun"
|
|
3
|
+
import * as path from "path"
|
|
4
|
+
import * as fs from "fs"
|
|
5
|
+
import * as os from "os"
|
|
6
|
+
import { parseGitHubUrl } from "../utils/github"
|
|
7
|
+
|
|
8
|
+
const openShell = async (cwd: string) => {
|
|
9
|
+
console.log(`\nOpening shell in ${cwd}...`)
|
|
10
|
+
const shell = process.env.SHELL || "/bin/zsh"
|
|
11
|
+
const proc = spawn([shell], {
|
|
12
|
+
cwd,
|
|
13
|
+
stdin: "inherit",
|
|
14
|
+
stdout: "inherit",
|
|
15
|
+
stderr: "inherit",
|
|
16
|
+
})
|
|
17
|
+
await proc.exited
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cloneCommand: CommandModule<{}, { url: string }> = {
|
|
21
|
+
command: "clone <url>",
|
|
22
|
+
describe: "Clone a GitHub repository to ~/repos/<owner>/<repo>",
|
|
23
|
+
builder: (yargs) => {
|
|
24
|
+
return yargs.positional("url", {
|
|
25
|
+
describe: "GitHub repository URL",
|
|
26
|
+
type: "string",
|
|
27
|
+
demandOption: true,
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
handler: async (argv) => {
|
|
31
|
+
const { url } = argv
|
|
32
|
+
|
|
33
|
+
const parsed = parseGitHubUrl(url)
|
|
34
|
+
if (!parsed) {
|
|
35
|
+
console.error("✗ Invalid GitHub URL format")
|
|
36
|
+
console.error(" Expected formats:")
|
|
37
|
+
console.error(" https://github.com/owner/repo")
|
|
38
|
+
console.error(" git@github.com:owner/repo.git")
|
|
39
|
+
console.error(" owner/repo")
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { owner, repo } = parsed
|
|
44
|
+
|
|
45
|
+
// Check if gh CLI is installed
|
|
46
|
+
try {
|
|
47
|
+
await $`which gh`.quiet()
|
|
48
|
+
} catch {
|
|
49
|
+
console.error("✗ GitHub CLI (gh) is not installed")
|
|
50
|
+
console.error(" Install with: brew install gh")
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Construct the target directory
|
|
55
|
+
const reposDir = path.join(os.homedir(), "repos", owner)
|
|
56
|
+
const targetDir = path.join(reposDir, repo)
|
|
57
|
+
|
|
58
|
+
// Check if repo already exists
|
|
59
|
+
if (fs.existsSync(targetDir)) {
|
|
60
|
+
console.log(`✓ Repository already exists at ${targetDir}`)
|
|
61
|
+
await openShell(targetDir)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create the parent directory if it doesn't exist
|
|
66
|
+
if (!fs.existsSync(reposDir)) {
|
|
67
|
+
fs.mkdirSync(reposDir, { recursive: true })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clone the repository using gh CLI
|
|
71
|
+
console.log(`Cloning ${owner}/${repo}...`)
|
|
72
|
+
try {
|
|
73
|
+
await $`gh repo clone ${owner}/${repo} ${targetDir}`
|
|
74
|
+
console.log(`\n✓ Successfully cloned to ${targetDir}`)
|
|
75
|
+
await openShell(targetDir)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`\n✗ Failed to clone repository`)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default cloneCommand
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import yargs from "yargs"
|
|
3
|
+
import { hideBin } from "yargs/helpers"
|
|
4
|
+
import brewCommand from "./commands/brew"
|
|
5
|
+
import cloneCommand from "./commands/clone"
|
|
6
|
+
import { failHandler } from "./utils/cli-helpers"
|
|
7
|
+
|
|
8
|
+
yargs(hideBin(process.argv))
|
|
9
|
+
.scriptName("pondorasti")
|
|
10
|
+
.usage("$0 <command> [options]")
|
|
11
|
+
.command(brewCommand)
|
|
12
|
+
.command(cloneCommand)
|
|
13
|
+
.demandCommand(1)
|
|
14
|
+
.recommendCommands()
|
|
15
|
+
.strict()
|
|
16
|
+
.version()
|
|
17
|
+
.alias("v", "version")
|
|
18
|
+
.help()
|
|
19
|
+
.alias("h", "help")
|
|
20
|
+
.epilogue("For more information, visit https://github.com/pondorasti/pondorasti/blob/main/packages/cli/README.md")
|
|
21
|
+
.fail(failHandler)
|
|
22
|
+
.parse()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as fs from "fs"
|
|
2
|
+
import { $ } from "bun"
|
|
3
|
+
|
|
4
|
+
const BREW_PATH_M1 = "/opt/homebrew/bin/brew"
|
|
5
|
+
|
|
6
|
+
class Homebrew {
|
|
7
|
+
private static brewPath: string | null = null
|
|
8
|
+
|
|
9
|
+
static isInstalled(): boolean {
|
|
10
|
+
if (fs.existsSync(BREW_PATH_M1)) {
|
|
11
|
+
this.brewPath = BREW_PATH_M1
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = Bun.spawnSync(["which", "brew"], { stdout: "pipe", stderr: "pipe" })
|
|
17
|
+
|
|
18
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
19
|
+
this.brewPath = result.stdout.toString().trim()
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getBrewPath(): string | null {
|
|
28
|
+
if (this.brewPath) {
|
|
29
|
+
return this.brewPath
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(BREW_PATH_M1)) {
|
|
33
|
+
this.brewPath = BREW_PATH_M1
|
|
34
|
+
return this.brewPath
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = Bun.spawnSync(["which", "brew"], { stdout: "pipe", stderr: "pipe" })
|
|
39
|
+
|
|
40
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
41
|
+
this.brewPath = result.stdout.toString().trim()
|
|
42
|
+
return this.brewPath
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static async runBundle(brewfilePath?: string): Promise<void> {
|
|
50
|
+
if (brewfilePath) {
|
|
51
|
+
await $`brew bundle --file=${brewfilePath}`
|
|
52
|
+
} else {
|
|
53
|
+
await $`brew bundle`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Homebrew }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Argv } from "yargs"
|
|
2
|
+
|
|
3
|
+
function failHandler(msg: string | undefined | null, err: Error | undefined, yargs: Argv) {
|
|
4
|
+
// Handle the case where no command/subcommand was provided
|
|
5
|
+
if (!err && (!msg || msg.includes("Not enough non-option arguments"))) {
|
|
6
|
+
yargs.showHelp("log")
|
|
7
|
+
process.exit(0)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Handle unknown arguments - show error in red but help in normal color
|
|
11
|
+
if (!err && msg && msg.includes("Unknown argument")) {
|
|
12
|
+
console.error(msg)
|
|
13
|
+
console.log()
|
|
14
|
+
yargs.showHelp("log")
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (err) {
|
|
19
|
+
console.error("Error:", err.message)
|
|
20
|
+
} else if (msg) {
|
|
21
|
+
console.error(msg)
|
|
22
|
+
console.log()
|
|
23
|
+
yargs.showHelp("error")
|
|
24
|
+
}
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { failHandler }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { parseGitHubUrl } from "./github"
|
|
3
|
+
|
|
4
|
+
describe("parseGitHubUrl", () => {
|
|
5
|
+
test("parses standard HTTPS URLs", () => {
|
|
6
|
+
expect(parseGitHubUrl("https://github.com/owner/repo")).toEqual({
|
|
7
|
+
owner: "owner",
|
|
8
|
+
repo: "repo",
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test("parses HTTPS URLs with .git suffix", () => {
|
|
13
|
+
expect(parseGitHubUrl("https://github.com/owner/repo.git")).toEqual({
|
|
14
|
+
owner: "owner",
|
|
15
|
+
repo: "repo",
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("parses SSH URLs", () => {
|
|
20
|
+
expect(parseGitHubUrl("git@github.com:owner/repo.git")).toEqual({
|
|
21
|
+
owner: "owner",
|
|
22
|
+
repo: "repo",
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("parses SSH URLs without .git suffix", () => {
|
|
27
|
+
expect(parseGitHubUrl("git@github.com:owner/repo")).toEqual({
|
|
28
|
+
owner: "owner",
|
|
29
|
+
repo: "repo",
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("parses short owner/repo format", () => {
|
|
34
|
+
expect(parseGitHubUrl("owner/repo")).toEqual({
|
|
35
|
+
owner: "owner",
|
|
36
|
+
repo: "repo",
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("parses tree URLs (branch paths)", () => {
|
|
41
|
+
expect(parseGitHubUrl("https://github.com/owner/repo/tree/main")).toEqual({
|
|
42
|
+
owner: "owner",
|
|
43
|
+
repo: "repo",
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("parses tree URLs with nested paths", () => {
|
|
48
|
+
expect(parseGitHubUrl("https://github.com/owner/repo/tree/main/src/components")).toEqual({
|
|
49
|
+
owner: "owner",
|
|
50
|
+
repo: "repo",
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("parses blob URLs (file paths)", () => {
|
|
55
|
+
expect(parseGitHubUrl("https://github.com/owner/repo/blob/main/file.ts")).toEqual({
|
|
56
|
+
owner: "owner",
|
|
57
|
+
repo: "repo",
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("parses blob URLs with nested file paths", () => {
|
|
62
|
+
expect(parseGitHubUrl("https://github.com/owner/repo/blob/main/src/utils/helpers.ts")).toEqual({
|
|
63
|
+
owner: "owner",
|
|
64
|
+
repo: "repo",
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("handles trailing slashes", () => {
|
|
69
|
+
expect(parseGitHubUrl("https://github.com/owner/repo/")).toEqual({
|
|
70
|
+
owner: "owner",
|
|
71
|
+
repo: "repo",
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("handles whitespace", () => {
|
|
76
|
+
expect(parseGitHubUrl(" https://github.com/owner/repo ")).toEqual({
|
|
77
|
+
owner: "owner",
|
|
78
|
+
repo: "repo",
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("returns null for invalid URLs", () => {
|
|
83
|
+
expect(parseGitHubUrl("not-a-valid-url")).toBeNull()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("returns null for empty string", () => {
|
|
87
|
+
expect(parseGitHubUrl("")).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test("returns null for URLs without owner/repo", () => {
|
|
91
|
+
expect(parseGitHubUrl("https://github.com/")).toBeNull()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const parseGitHubUrl = (url: string): { owner: string; repo: string } | null => {
|
|
2
|
+
const normalized = url.trim().replace(/\/+$/, "")
|
|
3
|
+
|
|
4
|
+
// Strip /tree/... or /blob/... suffixes (branch/file paths)
|
|
5
|
+
const withoutTree = normalized.replace(/\/(tree|blob)\/.*$/, "")
|
|
6
|
+
|
|
7
|
+
const patterns = [
|
|
8
|
+
/github\.com[:/]([^/]+)\/([^/.]+)(\.git)?$/, // Handles https and SSH formats
|
|
9
|
+
/^([^/]+)\/([^/.]+)(\.git)?$/, // Handles owner/repo format
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
for (const pattern of patterns) {
|
|
13
|
+
const match = withoutTree.match(pattern)
|
|
14
|
+
if (match) {
|
|
15
|
+
return { owner: match[1], repo: match[2] }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null
|
|
20
|
+
}
|