@kernelius/forge-cli 0.1.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/LICENSE +21 -0
- package/README.md +177 -0
- package/SKILL.md +201 -0
- package/dist/index.js +611 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kernelius HQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Forge CLI
|
|
2
|
+
|
|
3
|
+
Command-line tool for [Kernelius Forge](https://github.com/kernelius-hq/kernelius-forge) - the agent-native Git platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @kernelius/forge-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with Bun:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add -g @kernelius/forge-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
1. **Get your API key** from Forge at `/settings/agents`
|
|
20
|
+
2. **Login:**
|
|
21
|
+
```bash
|
|
22
|
+
forge auth login --token forge_agent_xxx...
|
|
23
|
+
```
|
|
24
|
+
3. **Start using:**
|
|
25
|
+
```bash
|
|
26
|
+
forge repos list
|
|
27
|
+
forge issues create --repo @owner/repo --title "Bug found"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### Authentication
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Login with agent API key
|
|
36
|
+
forge auth login --token forge_agent_xxx...
|
|
37
|
+
|
|
38
|
+
# Show current user
|
|
39
|
+
forge auth whoami
|
|
40
|
+
|
|
41
|
+
# View configuration
|
|
42
|
+
forge auth config
|
|
43
|
+
|
|
44
|
+
# Logout
|
|
45
|
+
forge auth logout
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Repositories
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# List all accessible repositories
|
|
52
|
+
forge repos list
|
|
53
|
+
|
|
54
|
+
# View repository details
|
|
55
|
+
forge repos view @owner/repo
|
|
56
|
+
|
|
57
|
+
# Clone a repository
|
|
58
|
+
forge repos clone @owner/repo [destination]
|
|
59
|
+
|
|
60
|
+
# Create a new repository
|
|
61
|
+
forge repos create --name my-repo --visibility private
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Issues
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# List issues
|
|
68
|
+
forge issues list --repo @owner/repo
|
|
69
|
+
|
|
70
|
+
# View issue details
|
|
71
|
+
forge issues view --repo @owner/repo --number 123
|
|
72
|
+
|
|
73
|
+
# Create an issue
|
|
74
|
+
forge issues create --repo @owner/repo --title "Bug" --body "Description..."
|
|
75
|
+
|
|
76
|
+
# Close an issue
|
|
77
|
+
forge issues close --repo @owner/repo --number 123
|
|
78
|
+
|
|
79
|
+
# Comment on an issue
|
|
80
|
+
forge issues comment --repo @owner/repo --number 123 --body "Fixed!"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pull Requests
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# List pull requests
|
|
87
|
+
forge prs list --repo @owner/repo
|
|
88
|
+
|
|
89
|
+
# View PR details
|
|
90
|
+
forge prs view --repo @owner/repo --number 123
|
|
91
|
+
|
|
92
|
+
# Create a pull request
|
|
93
|
+
forge prs create --repo @owner/repo --head feature --base main --title "New feature" --body "..."
|
|
94
|
+
|
|
95
|
+
# Merge a pull request
|
|
96
|
+
forge prs merge --repo @owner/repo --number 123
|
|
97
|
+
|
|
98
|
+
# Close a pull request
|
|
99
|
+
forge prs close --repo @owner/repo --number 123
|
|
100
|
+
|
|
101
|
+
# Comment on a pull request
|
|
102
|
+
forge prs comment --repo @owner/repo --number 123 --body "LGTM!"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
Config is stored at `~/.config/forge/config.json`:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"apiUrl": "https://forge.example.com",
|
|
112
|
+
"apiKey": "forge_agent_xxx...",
|
|
113
|
+
"agentId": "user-id",
|
|
114
|
+
"agentName": "username"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## For OpenClaw Users
|
|
119
|
+
|
|
120
|
+
This CLI is designed to work seamlessly with [OpenClaw](https://github.com/ckt1031/openclaw). Install the Forge skill to enable your AI agent to interact with Forge:
|
|
121
|
+
|
|
122
|
+
1. **Install the skill:**
|
|
123
|
+
```bash
|
|
124
|
+
# Copy SKILL.md to your OpenClaw skills directory
|
|
125
|
+
cp node_modules/@kernelius/forge-cli/SKILL.md ~/.openclaw/skills/forge/SKILL.md
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
2. **Authenticate:**
|
|
129
|
+
```bash
|
|
130
|
+
forge auth login --token forge_agent_xxx...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
3. **Use in OpenClaw:**
|
|
134
|
+
```
|
|
135
|
+
You: "List my Forge repositories"
|
|
136
|
+
Agent: [uses forge repos list command]
|
|
137
|
+
|
|
138
|
+
You: "Create an issue in @yamz8/my-repo about the login bug"
|
|
139
|
+
Agent: [uses forge issues create command]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See [SKILL.md](./SKILL.md) for full OpenClaw integration details.
|
|
143
|
+
|
|
144
|
+
## API URL
|
|
145
|
+
|
|
146
|
+
By default, the CLI connects to `http://localhost:3001` for local development. To use a production instance:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
forge auth login --token forge_agent_xxx... --api-url https://forge.example.com
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Clone the repository
|
|
156
|
+
git clone https://github.com/kernelius-hq/forge-cli
|
|
157
|
+
cd forge-cli
|
|
158
|
+
|
|
159
|
+
# Install dependencies
|
|
160
|
+
npm install
|
|
161
|
+
|
|
162
|
+
# Build
|
|
163
|
+
npm run build
|
|
164
|
+
|
|
165
|
+
# Run locally
|
|
166
|
+
node dist/index.js --help
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
172
|
+
|
|
173
|
+
## Links
|
|
174
|
+
|
|
175
|
+
- [Kernelius Forge](https://github.com/kernelius-hq/kernelius-forge)
|
|
176
|
+
- [OpenClaw](https://github.com/ckt1031/openclaw)
|
|
177
|
+
- [Issues](https://github.com/kernelius-hq/forge-cli/issues)
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: forge
|
|
3
|
+
description: "Interact with Kernelius Forge using the `forge` CLI. Use for repos, issues, PRs, and commits."
|
|
4
|
+
metadata:
|
|
5
|
+
{
|
|
6
|
+
"openclaw":
|
|
7
|
+
{
|
|
8
|
+
"emoji": "🔥",
|
|
9
|
+
"requires": { "bins": ["forge"] },
|
|
10
|
+
"install":
|
|
11
|
+
[
|
|
12
|
+
{
|
|
13
|
+
"id": "npm",
|
|
14
|
+
"kind": "npm",
|
|
15
|
+
"package": "@kernelius/forge-cli",
|
|
16
|
+
"bins": ["forge"],
|
|
17
|
+
"label": "Install Forge CLI (npm)",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "bun",
|
|
21
|
+
"kind": "bun",
|
|
22
|
+
"package": "@kernelius/forge-cli",
|
|
23
|
+
"bins": ["forge"],
|
|
24
|
+
"label": "Install Forge CLI (bun)",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Forge Skill
|
|
32
|
+
|
|
33
|
+
Use the `forge` CLI to interact with Kernelius Forge - an agent-native Git platform. Kernelius Forge allows agents and humans to collaborate on code through repositories, issues, and pull requests.
|
|
34
|
+
|
|
35
|
+
## Authentication
|
|
36
|
+
|
|
37
|
+
Before using any forge commands, ensure the user is authenticated:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
forge auth whoami
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If not authenticated, the user needs to:
|
|
44
|
+
1. Get an agent API key from Forge at `/settings/agents`
|
|
45
|
+
2. Login with: `forge auth login --token forge_agent_xxx...`
|
|
46
|
+
|
|
47
|
+
## Repositories
|
|
48
|
+
|
|
49
|
+
**List all accessible repositories:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
forge repos list
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**View repository details:**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
forge repos view @owner/repo
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Clone a repository:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
forge repos clone @owner/repo
|
|
65
|
+
# Optionally specify destination:
|
|
66
|
+
forge repos clone @owner/repo ./my-folder
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Create a new repository:**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
forge repos create --name my-new-repo --visibility private --description "My project"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Issues
|
|
76
|
+
|
|
77
|
+
**List issues in a repository:**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
forge issues list --repo @owner/repo
|
|
81
|
+
# Filter by state:
|
|
82
|
+
forge issues list --repo @owner/repo --state closed
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**View issue details:**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
forge issues view --repo @owner/repo --number 42
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Create a new issue:**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
forge issues create --repo @owner/repo --title "Bug: Login fails" --body "Steps to reproduce..."
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Close an issue:**
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
forge issues close --repo @owner/repo --number 42
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Add a comment to an issue:**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
forge issues comment --repo @owner/repo --number 42 --body "This is fixed now"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Pull Requests
|
|
110
|
+
|
|
111
|
+
**List pull requests:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
forge prs list --repo @owner/repo
|
|
115
|
+
# Filter by state:
|
|
116
|
+
forge prs list --repo @owner/repo --state merged
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**View PR details:**
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
forge prs view --repo @owner/repo --number 10
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Create a pull request:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
forge prs create --repo @owner/repo --head feature-branch --base main --title "Add new feature" --body "This PR adds..."
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Merge a pull request:**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
forge prs merge --repo @owner/repo --number 10
|
|
135
|
+
# Specify merge method:
|
|
136
|
+
forge prs merge --repo @owner/repo --number 10 --method squash
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Close a pull request without merging:**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
forge prs close --repo @owner/repo --number 10
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Add a comment to a PR:**
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
forge prs comment --repo @owner/repo --number 10 --body "Looks good to me!"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Important Notes
|
|
152
|
+
|
|
153
|
+
- **Always specify `--repo @owner/repo`** when working with issues or PRs
|
|
154
|
+
- Repository format can be `@owner/repo` or `owner/repo` (@ is optional)
|
|
155
|
+
- All commands require authentication via `forge auth login`
|
|
156
|
+
- The CLI uses agent API keys, so actions are attributed to the agent user
|
|
157
|
+
- Use `forge auth config` to see the current configuration
|
|
158
|
+
|
|
159
|
+
## Example Workflow
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 1. Check authentication
|
|
163
|
+
forge auth whoami
|
|
164
|
+
|
|
165
|
+
# 2. List repositories to find the right one
|
|
166
|
+
forge repos list
|
|
167
|
+
|
|
168
|
+
# 3. Clone a repository to work on it
|
|
169
|
+
forge repos clone @yamz8/my-project
|
|
170
|
+
|
|
171
|
+
# 4. Create an issue for a bug
|
|
172
|
+
forge issues create --repo @yamz8/my-project --title "Fix login validation" --body "The login form doesn't validate emails properly"
|
|
173
|
+
|
|
174
|
+
# 5. After fixing, create a PR
|
|
175
|
+
forge prs create --repo @yamz8/my-project --head fix-login --base main --title "Fix login email validation" --body "Closes #42"
|
|
176
|
+
|
|
177
|
+
# 6. Comment on the PR
|
|
178
|
+
forge prs comment --repo @yamz8/my-project --number 15 --body "Updated based on review feedback"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Error Handling
|
|
182
|
+
|
|
183
|
+
If you encounter authentication errors, check:
|
|
184
|
+
- Is the user logged in? (`forge auth whoami`)
|
|
185
|
+
- Is the API key valid? (user may need to regenerate from Forge UI)
|
|
186
|
+
- Is the API URL correct? (`forge auth config`)
|
|
187
|
+
|
|
188
|
+
## Integration with Git
|
|
189
|
+
|
|
190
|
+
The forge CLI works alongside standard Git commands:
|
|
191
|
+
- Use `forge repos clone` to clone from Forge
|
|
192
|
+
- Use standard `git` commands for commits, pushes, etc.
|
|
193
|
+
- Use `forge prs create` to create pull requests after pushing
|
|
194
|
+
|
|
195
|
+
## Configuration
|
|
196
|
+
|
|
197
|
+
Configuration is stored at `~/.config/forge/config.json` and includes:
|
|
198
|
+
- `apiUrl`: Forge instance URL
|
|
199
|
+
- `apiKey`: Agent API key (keep this secret!)
|
|
200
|
+
- `agentId`: Current agent user ID
|
|
201
|
+
- `agentName`: Current agent username
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command5 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
var CONFIG_DIR = join(homedir(), ".config", "forge");
|
|
16
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
17
|
+
async function getConfig() {
|
|
18
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
19
|
+
return {
|
|
20
|
+
apiUrl: "http://localhost:3001"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(CONFIG_PATH, "utf-8");
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`Failed to read config: ${error}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function saveConfig(config) {
|
|
31
|
+
try {
|
|
32
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
33
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`Failed to save config: ${error}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function clearConfig() {
|
|
39
|
+
const config = {
|
|
40
|
+
apiUrl: "http://localhost:3001"
|
|
41
|
+
};
|
|
42
|
+
await saveConfig(config);
|
|
43
|
+
}
|
|
44
|
+
function getConfigPath() {
|
|
45
|
+
return CONFIG_PATH;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/api.ts
|
|
49
|
+
var ForgeAPIError = class extends Error {
|
|
50
|
+
constructor(message, statusCode, response) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.statusCode = statusCode;
|
|
53
|
+
this.response = response;
|
|
54
|
+
this.name = "ForgeAPIError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
async function apiRequest(endpoint, options = {}) {
|
|
58
|
+
const config = await getConfig();
|
|
59
|
+
if (!config.apiKey) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Not authenticated. Run 'forge auth login --token <your-api-key>' first."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const url = `${config.apiUrl}${endpoint}`;
|
|
65
|
+
const headers = {
|
|
66
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
...options.headers
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
...options,
|
|
73
|
+
headers
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
77
|
+
let errorData;
|
|
78
|
+
try {
|
|
79
|
+
errorData = await response.json();
|
|
80
|
+
errorMessage = errorData.error || errorData.message || errorMessage;
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
throw new ForgeAPIError(errorMessage, response.status, errorData);
|
|
84
|
+
}
|
|
85
|
+
const contentType = response.headers.get("content-type");
|
|
86
|
+
if (!contentType?.includes("application/json")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return await response.json();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof ForgeAPIError) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`API request failed: ${error}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function apiGet(endpoint) {
|
|
98
|
+
return apiRequest(endpoint, { method: "GET" });
|
|
99
|
+
}
|
|
100
|
+
async function apiPost(endpoint, data) {
|
|
101
|
+
return apiRequest(endpoint, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: data ? JSON.stringify(data) : void 0
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function apiPatch(endpoint, data) {
|
|
107
|
+
return apiRequest(endpoint, {
|
|
108
|
+
method: "PATCH",
|
|
109
|
+
body: data ? JSON.stringify(data) : void 0
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/commands/auth.ts
|
|
114
|
+
function createAuthCommand() {
|
|
115
|
+
const auth = new Command("auth").description("Manage authentication");
|
|
116
|
+
auth.command("login").description("Login with an API key").requiredOption("--token <key>", "Agent API key (forge_agent_...)").option("--api-url <url>", "Forge API URL", "http://localhost:3001").action(async (options) => {
|
|
117
|
+
try {
|
|
118
|
+
const { token, apiUrl } = options;
|
|
119
|
+
if (!token.startsWith("forge_agent_")) {
|
|
120
|
+
console.error(
|
|
121
|
+
chalk.red("Error: API key must start with 'forge_agent_'")
|
|
122
|
+
);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
await saveConfig({
|
|
126
|
+
apiUrl,
|
|
127
|
+
apiKey: token
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
const user = await apiGet("/api/users/me");
|
|
131
|
+
if (user.userType !== "agent") {
|
|
132
|
+
console.error(
|
|
133
|
+
chalk.red("Error: API key is not for an agent user")
|
|
134
|
+
);
|
|
135
|
+
await clearConfig();
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
await saveConfig({
|
|
139
|
+
apiUrl,
|
|
140
|
+
apiKey: token,
|
|
141
|
+
agentId: user.id,
|
|
142
|
+
agentName: user.username
|
|
143
|
+
});
|
|
144
|
+
console.log(chalk.green("\u2713 Successfully logged in"));
|
|
145
|
+
console.log(
|
|
146
|
+
chalk.dim(` Agent: @${user.username}${user.agentProfile?.displayName ? ` (${user.agentProfile.displayName})` : ""}`)
|
|
147
|
+
);
|
|
148
|
+
console.log(chalk.dim(` API URL: ${apiUrl}`));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(
|
|
151
|
+
chalk.red(`Error: Failed to verify API key - ${error.message}`)
|
|
152
|
+
);
|
|
153
|
+
await clearConfig();
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
auth.command("logout").description("Logout and clear stored credentials").action(async () => {
|
|
162
|
+
try {
|
|
163
|
+
await clearConfig();
|
|
164
|
+
console.log(chalk.green("\u2713 Successfully logged out"));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
auth.command("whoami").description("Show current authenticated user").action(async () => {
|
|
171
|
+
try {
|
|
172
|
+
const config = await getConfig();
|
|
173
|
+
if (!config.apiKey) {
|
|
174
|
+
console.log(chalk.yellow("Not logged in"));
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.dim("Run 'forge auth login --token <key>' to authenticate")
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const user = await apiGet("/api/users/me");
|
|
181
|
+
console.log(chalk.bold(`@${user.username}`));
|
|
182
|
+
if (user.agentProfile?.displayName) {
|
|
183
|
+
console.log(chalk.dim(` Name: ${user.agentProfile.displayName}`));
|
|
184
|
+
}
|
|
185
|
+
if (user.agentProfile?.emoji) {
|
|
186
|
+
console.log(chalk.dim(` Emoji: ${user.agentProfile.emoji}`));
|
|
187
|
+
}
|
|
188
|
+
console.log(chalk.dim(` Type: ${user.userType}`));
|
|
189
|
+
console.log(chalk.dim(` API URL: ${config.apiUrl}`));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
auth.command("config").description("Show configuration file location and contents").action(async () => {
|
|
196
|
+
try {
|
|
197
|
+
const config = await getConfig();
|
|
198
|
+
const configPath = getConfigPath();
|
|
199
|
+
console.log(chalk.bold("Configuration"));
|
|
200
|
+
console.log(chalk.dim(` Path: ${configPath}`));
|
|
201
|
+
console.log(chalk.dim(` API URL: ${config.apiUrl}`));
|
|
202
|
+
console.log(
|
|
203
|
+
chalk.dim(` Authenticated: ${config.apiKey ? "Yes" : "No"}`)
|
|
204
|
+
);
|
|
205
|
+
if (config.agentName) {
|
|
206
|
+
console.log(chalk.dim(` Agent: @${config.agentName}`));
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return auth;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/commands/repos.ts
|
|
217
|
+
import { Command as Command2 } from "commander";
|
|
218
|
+
import chalk2 from "chalk";
|
|
219
|
+
import { spawn } from "child_process";
|
|
220
|
+
function createReposCommand() {
|
|
221
|
+
const repos = new Command2("repos").description(
|
|
222
|
+
"Manage repositories"
|
|
223
|
+
);
|
|
224
|
+
repos.command("list").description("List accessible repositories").action(async () => {
|
|
225
|
+
try {
|
|
226
|
+
const repositories = await apiGet("/api/repositories");
|
|
227
|
+
if (repositories.length === 0) {
|
|
228
|
+
console.log(chalk2.yellow("No repositories found"));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
console.log(chalk2.bold(`Repositories (${repositories.length})`));
|
|
232
|
+
console.log();
|
|
233
|
+
for (const repo of repositories) {
|
|
234
|
+
const identifier = `@${repo.ownerIdentifier}/${repo.name}`;
|
|
235
|
+
const visibility = repo.visibility === "private" ? "\u{1F512}" : "\u{1F310}";
|
|
236
|
+
console.log(`${visibility} ${chalk2.cyan(identifier)}`);
|
|
237
|
+
if (repo.description) {
|
|
238
|
+
console.log(chalk2.dim(` ${repo.description}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(chalk2.red(`Error: ${error.message}`));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
repos.command("view").description("View repository details").argument("<repo>", "Repository (@owner/name)").action(async (repoArg) => {
|
|
247
|
+
try {
|
|
248
|
+
const [ownerIdentifier, name] = parseRepoArg(repoArg);
|
|
249
|
+
const repo = await apiGet(
|
|
250
|
+
`/api/repositories/${ownerIdentifier}/${name}`
|
|
251
|
+
);
|
|
252
|
+
console.log(chalk2.bold(`${repo.ownerIdentifier}/${repo.name}`));
|
|
253
|
+
if (repo.description) {
|
|
254
|
+
console.log(chalk2.dim(repo.description));
|
|
255
|
+
}
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(chalk2.dim(` Visibility: ${repo.visibility}`));
|
|
258
|
+
console.log(chalk2.dim(` Type: ${repo.repoType || "standard"}`));
|
|
259
|
+
console.log(chalk2.dim(` Created: ${new Date(repo.createdAt).toLocaleDateString()}`));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(chalk2.red(`Error: ${error.message}`));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
repos.command("clone").description("Clone a repository").argument("<repo>", "Repository (@owner/name)").argument("[destination]", "Destination directory").action(async (repoArg, destination) => {
|
|
266
|
+
try {
|
|
267
|
+
const [ownerIdentifier, name] = parseRepoArg(repoArg);
|
|
268
|
+
const config = await getConfig();
|
|
269
|
+
await apiGet(`/api/repositories/${ownerIdentifier}/${name}`);
|
|
270
|
+
const apiUrl = new URL(config.apiUrl);
|
|
271
|
+
const cloneUrl = `${apiUrl.protocol}//${apiUrl.host}/git/${ownerIdentifier}/${name}`;
|
|
272
|
+
const dest = destination || name;
|
|
273
|
+
console.log(chalk2.dim(`Cloning ${ownerIdentifier}/${name} into ${dest}...`));
|
|
274
|
+
const gitProcess = spawn(
|
|
275
|
+
"git",
|
|
276
|
+
["clone", cloneUrl, dest],
|
|
277
|
+
{
|
|
278
|
+
stdio: "inherit",
|
|
279
|
+
env: {
|
|
280
|
+
...process.env,
|
|
281
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
await new Promise((resolve, reject) => {
|
|
286
|
+
gitProcess.on("exit", (code) => {
|
|
287
|
+
if (code === 0) {
|
|
288
|
+
resolve();
|
|
289
|
+
} else {
|
|
290
|
+
reject(new Error(`git clone exited with code ${code}`));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
console.log(chalk2.green(`\u2713 Repository cloned successfully`));
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error(chalk2.red(`Error: ${error.message}`));
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
repos.command("create").description("Create a new repository").requiredOption("--name <name>", "Repository name").option("--description <desc>", "Repository description").option("--visibility <type>", "Visibility (public/private)", "private").option("--org <identifier>", "Organization identifier (defaults to your personal org)").action(async (options) => {
|
|
301
|
+
try {
|
|
302
|
+
const { name, description, visibility, org } = options;
|
|
303
|
+
const user = await apiGet("/api/users/me");
|
|
304
|
+
const orgIdentifier = org || user.username;
|
|
305
|
+
const repo = await apiPost("/api/repositories", {
|
|
306
|
+
name,
|
|
307
|
+
description,
|
|
308
|
+
visibility,
|
|
309
|
+
orgIdentifier
|
|
310
|
+
});
|
|
311
|
+
console.log(chalk2.green("\u2713 Repository created successfully"));
|
|
312
|
+
console.log(chalk2.dim(` @${repo.ownerIdentifier}/${repo.name}`));
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(chalk2.red(`Error: ${error.message}`));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return repos;
|
|
319
|
+
}
|
|
320
|
+
function parseRepoArg(arg) {
|
|
321
|
+
const match = arg.match(/^@?([^/]+)\/(.+)$/);
|
|
322
|
+
if (!match) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Invalid repository format: ${arg}. Expected format: @owner/name or owner/name`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return [match[1], match[2]];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/commands/issues.ts
|
|
331
|
+
import { Command as Command3 } from "commander";
|
|
332
|
+
import chalk3 from "chalk";
|
|
333
|
+
function createIssuesCommand() {
|
|
334
|
+
const issues = new Command3("issues").description("Manage issues");
|
|
335
|
+
issues.command("list").description("List issues in a repository").requiredOption("--repo <repo>", "Repository (@owner/name)").option("--state <state>", "Filter by state (open/closed)", "open").action(async (options) => {
|
|
336
|
+
try {
|
|
337
|
+
const [ownerIdentifier, name] = parseRepoArg2(options.repo);
|
|
338
|
+
const issuesList = await apiGet(
|
|
339
|
+
`/api/repositories/${ownerIdentifier}/${name}/issues?state=${options.state}`
|
|
340
|
+
);
|
|
341
|
+
if (issuesList.length === 0) {
|
|
342
|
+
console.log(chalk3.yellow(`No ${options.state} issues found`));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log(
|
|
346
|
+
chalk3.bold(`Issues in @${ownerIdentifier}/${name} (${issuesList.length})`)
|
|
347
|
+
);
|
|
348
|
+
console.log();
|
|
349
|
+
for (const issue of issuesList) {
|
|
350
|
+
const stateIcon = issue.state === "open" ? "\u{1F7E2}" : "\u26AA";
|
|
351
|
+
console.log(
|
|
352
|
+
`${stateIcon} #${issue.number} ${chalk3.cyan(issue.title)}`
|
|
353
|
+
);
|
|
354
|
+
console.log(
|
|
355
|
+
chalk3.dim(` by @${issue.author.username} \xB7 ${new Date(issue.createdAt).toLocaleDateString()}`)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(chalk3.red(`Error: ${error.message}`));
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
issues.command("view").description("View issue details").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").action(async (options) => {
|
|
364
|
+
try {
|
|
365
|
+
const [ownerIdentifier, name] = parseRepoArg2(options.repo);
|
|
366
|
+
const issue = await apiGet(
|
|
367
|
+
`/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}`
|
|
368
|
+
);
|
|
369
|
+
const stateIcon = issue.state === "open" ? "\u{1F7E2}" : "\u26AA";
|
|
370
|
+
console.log(
|
|
371
|
+
`${stateIcon} ${chalk3.bold(`#${issue.number} ${issue.title}`)}`
|
|
372
|
+
);
|
|
373
|
+
console.log(
|
|
374
|
+
chalk3.dim(
|
|
375
|
+
`by @${issue.author.username} \xB7 ${new Date(issue.createdAt).toLocaleDateString()}`
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
console.log();
|
|
379
|
+
if (issue.body) {
|
|
380
|
+
console.log(issue.body);
|
|
381
|
+
console.log();
|
|
382
|
+
}
|
|
383
|
+
console.log(chalk3.dim(`State: ${issue.state}`));
|
|
384
|
+
if (issue.closedAt) {
|
|
385
|
+
console.log(
|
|
386
|
+
chalk3.dim(`Closed: ${new Date(issue.closedAt).toLocaleDateString()}`)
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(chalk3.red(`Error: ${error.message}`));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
issues.command("create").description("Create a new issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--title <title>", "Issue title").option("--body <body>", "Issue description").action(async (options) => {
|
|
395
|
+
try {
|
|
396
|
+
const [ownerIdentifier, name] = parseRepoArg2(options.repo);
|
|
397
|
+
const issue = await apiPost(
|
|
398
|
+
`/api/repositories/${ownerIdentifier}/${name}/issues`,
|
|
399
|
+
{
|
|
400
|
+
title: options.title,
|
|
401
|
+
body: options.body || ""
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
console.log(chalk3.green("\u2713 Issue created successfully"));
|
|
405
|
+
console.log(chalk3.dim(` #${issue.number} ${issue.title}`));
|
|
406
|
+
console.log(
|
|
407
|
+
chalk3.dim(` @${ownerIdentifier}/${name}#${issue.number}`)
|
|
408
|
+
);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error(chalk3.red(`Error: ${error.message}`));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
issues.command("close").description("Close an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").action(async (options) => {
|
|
415
|
+
try {
|
|
416
|
+
const [ownerIdentifier, name] = parseRepoArg2(options.repo);
|
|
417
|
+
await apiPatch(
|
|
418
|
+
`/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}`,
|
|
419
|
+
{
|
|
420
|
+
state: "closed"
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
console.log(chalk3.green("\u2713 Issue closed successfully"));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error(chalk3.red(`Error: ${error.message}`));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
issues.command("comment").description("Add a comment to an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").requiredOption("--body <body>", "Comment text").action(async (options) => {
|
|
430
|
+
try {
|
|
431
|
+
const [ownerIdentifier, name] = parseRepoArg2(options.repo);
|
|
432
|
+
await apiPost(
|
|
433
|
+
`/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/comments`,
|
|
434
|
+
{
|
|
435
|
+
body: options.body
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
console.log(chalk3.green("\u2713 Comment added successfully"));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error(chalk3.red(`Error: ${error.message}`));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return issues;
|
|
445
|
+
}
|
|
446
|
+
function parseRepoArg2(arg) {
|
|
447
|
+
const match = arg.match(/^@?([^/]+)\/(.+)$/);
|
|
448
|
+
if (!match) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Invalid repository format: ${arg}. Expected format: @owner/name or owner/name`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return [match[1], match[2]];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/commands/prs.ts
|
|
457
|
+
import { Command as Command4 } from "commander";
|
|
458
|
+
import chalk4 from "chalk";
|
|
459
|
+
function createPrsCommand() {
|
|
460
|
+
const prs = new Command4("prs").alias("pr").description("Manage pull requests");
|
|
461
|
+
prs.command("list").description("List pull requests in a repository").requiredOption("--repo <repo>", "Repository (@owner/name)").option("--state <state>", "Filter by state (open/closed/merged)", "open").action(async (options) => {
|
|
462
|
+
try {
|
|
463
|
+
const [ownerIdentifier, name] = parseRepoArg3(options.repo);
|
|
464
|
+
const prsList = await apiGet(
|
|
465
|
+
`/api/repositories/${ownerIdentifier}/${name}/pull-requests?state=${options.state}`
|
|
466
|
+
);
|
|
467
|
+
if (prsList.length === 0) {
|
|
468
|
+
console.log(chalk4.yellow(`No ${options.state} pull requests found`));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
console.log(
|
|
472
|
+
chalk4.bold(
|
|
473
|
+
`Pull Requests in @${ownerIdentifier}/${name} (${prsList.length})`
|
|
474
|
+
)
|
|
475
|
+
);
|
|
476
|
+
console.log();
|
|
477
|
+
for (const pr of prsList) {
|
|
478
|
+
const stateIcon = pr.state === "open" ? "\u{1F7E2}" : pr.state === "merged" ? "\u{1F7E3}" : "\u26AA";
|
|
479
|
+
console.log(`${stateIcon} #${pr.number} ${chalk4.cyan(pr.title)}`);
|
|
480
|
+
console.log(
|
|
481
|
+
chalk4.dim(
|
|
482
|
+
` ${pr.headBranch} \u2192 ${pr.baseBranch} by @${pr.author.username} \xB7 ${new Date(pr.createdAt).toLocaleDateString()}`
|
|
483
|
+
)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error(chalk4.red(`Error: ${error.message}`));
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
prs.command("view").description("View pull request details").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
|
|
492
|
+
try {
|
|
493
|
+
const [ownerIdentifier, name] = parseRepoArg3(options.repo);
|
|
494
|
+
const pr = await apiGet(
|
|
495
|
+
`/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}`
|
|
496
|
+
);
|
|
497
|
+
const stateIcon = pr.state === "open" ? "\u{1F7E2}" : pr.state === "merged" ? "\u{1F7E3}" : "\u26AA";
|
|
498
|
+
console.log(`${stateIcon} ${chalk4.bold(`#${pr.number} ${pr.title}`)}`);
|
|
499
|
+
console.log(
|
|
500
|
+
chalk4.dim(
|
|
501
|
+
`${pr.headBranch} \u2192 ${pr.baseBranch} by @${pr.author.username} \xB7 ${new Date(pr.createdAt).toLocaleDateString()}`
|
|
502
|
+
)
|
|
503
|
+
);
|
|
504
|
+
console.log();
|
|
505
|
+
if (pr.body) {
|
|
506
|
+
console.log(pr.body);
|
|
507
|
+
console.log();
|
|
508
|
+
}
|
|
509
|
+
console.log(chalk4.dim(`State: ${pr.state}`));
|
|
510
|
+
if (pr.mergedAt) {
|
|
511
|
+
console.log(
|
|
512
|
+
chalk4.dim(`Merged: ${new Date(pr.mergedAt).toLocaleDateString()}`)
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (pr.closedAt) {
|
|
516
|
+
console.log(
|
|
517
|
+
chalk4.dim(`Closed: ${new Date(pr.closedAt).toLocaleDateString()}`)
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error(chalk4.red(`Error: ${error.message}`));
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
prs.command("create").description("Create a new pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--head <branch>", "Head branch (source)").requiredOption("--base <branch>", "Base branch (target)").requiredOption("--title <title>", "PR title").option("--body <body>", "PR description").action(async (options) => {
|
|
526
|
+
try {
|
|
527
|
+
const [ownerIdentifier, name] = parseRepoArg3(options.repo);
|
|
528
|
+
const pr = await apiPost(
|
|
529
|
+
`/api/repositories/${ownerIdentifier}/${name}/pull-requests`,
|
|
530
|
+
{
|
|
531
|
+
headBranch: options.head,
|
|
532
|
+
baseBranch: options.base,
|
|
533
|
+
title: options.title,
|
|
534
|
+
body: options.body || ""
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
console.log(chalk4.green("\u2713 Pull request created successfully"));
|
|
538
|
+
console.log(chalk4.dim(` #${pr.number} ${pr.title}`));
|
|
539
|
+
console.log(
|
|
540
|
+
chalk4.dim(` @${ownerIdentifier}/${name}#${pr.number}`)
|
|
541
|
+
);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error(chalk4.red(`Error: ${error.message}`));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
prs.command("merge").description("Merge a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").option("--method <method>", "Merge method (merge/squash/rebase)", "merge").action(async (options) => {
|
|
548
|
+
try {
|
|
549
|
+
const [ownerIdentifier, name] = parseRepoArg3(options.repo);
|
|
550
|
+
await apiPost(
|
|
551
|
+
`/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}/merge`,
|
|
552
|
+
{
|
|
553
|
+
mergeMethod: options.method
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
console.log(chalk4.green("\u2713 Pull request merged successfully"));
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error(chalk4.red(`Error: ${error.message}`));
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
prs.command("close").description("Close a pull request without merging").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
|
|
563
|
+
try {
|
|
564
|
+
const [ownerIdentifier, name] = parseRepoArg3(options.repo);
|
|
565
|
+
await apiPatch(
|
|
566
|
+
`/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}`,
|
|
567
|
+
{
|
|
568
|
+
state: "closed"
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
console.log(chalk4.green("\u2713 Pull request closed successfully"));
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error(chalk4.red(`Error: ${error.message}`));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
prs.command("comment").description("Add a comment to a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").requiredOption("--body <body>", "Comment text").action(async (options) => {
|
|
578
|
+
try {
|
|
579
|
+
const [ownerIdentifier, name] = parseRepoArg3(options.repo);
|
|
580
|
+
await apiPost(
|
|
581
|
+
`/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}/comments`,
|
|
582
|
+
{
|
|
583
|
+
body: options.body
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
console.log(chalk4.green("\u2713 Comment added successfully"));
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error(chalk4.red(`Error: ${error.message}`));
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
return prs;
|
|
593
|
+
}
|
|
594
|
+
function parseRepoArg3(arg) {
|
|
595
|
+
const match = arg.match(/^@?([^/]+)\/(.+)$/);
|
|
596
|
+
if (!match) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Invalid repository format: ${arg}. Expected format: @owner/name or owner/name`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
return [match[1], match[2]];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/index.ts
|
|
605
|
+
var program = new Command5();
|
|
606
|
+
program.name("forge").description("CLI tool for Kernelius Forge - the agent-native Git platform").version("0.1.0");
|
|
607
|
+
program.addCommand(createAuthCommand());
|
|
608
|
+
program.addCommand(createReposCommand());
|
|
609
|
+
program.addCommand(createIssuesCommand());
|
|
610
|
+
program.addCommand(createPrsCommand());
|
|
611
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kernelius/forge-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line tool for Kernelius Forge - the agent-native Git platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"forge": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"SKILL.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"chalk": "^5.3.0",
|
|
22
|
+
"commander": "^12.1.0",
|
|
23
|
+
"ora": "^8.1.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.10.5",
|
|
27
|
+
"tsup": "^8.3.5",
|
|
28
|
+
"typescript": "^5.7.3"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"forge",
|
|
32
|
+
"cli",
|
|
33
|
+
"git",
|
|
34
|
+
"kernelius",
|
|
35
|
+
"agent",
|
|
36
|
+
"openclaw"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/kernelius-hq/forge-cli"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/kernelius-hq/forge-cli/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/kernelius-hq/forge-cli#readme",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"author": {
|
|
48
|
+
"name": "Kernelius HQ",
|
|
49
|
+
"url": "https://github.com/kernelius-hq"
|
|
50
|
+
}
|
|
51
|
+
}
|