@bobbyg603/mog 1.5.3 → 1.6.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 +47 -0
- package/package.json +1 -1
- package/src/config.test.ts +147 -0
- package/src/config.ts +92 -0
- package/src/github.ts +11 -2
- package/src/index.ts +125 -37
- package/src/sandbox.ts +20 -0
- package/src/version.test.ts +30 -0
package/README.md
CHANGED
|
@@ -74,6 +74,33 @@ mog 123 --include .env --include serviceAccountKey.json
|
|
|
74
74
|
mog list
|
|
75
75
|
mog list --verbose
|
|
76
76
|
mog owner/repo list --verbose
|
|
77
|
+
|
|
78
|
+
# Check version
|
|
79
|
+
mog --version
|
|
80
|
+
mog -v
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Git identity
|
|
84
|
+
|
|
85
|
+
`mog` automatically configures the git identity inside the sandbox so commits are attributed correctly. Identity is resolved via a 3-tier priority chain:
|
|
86
|
+
|
|
87
|
+
1. **Per-repo mog config** (`~/.mog/repos/<owner>/<repo>/config.json`)
|
|
88
|
+
2. **Host git config** (auto-detected at runtime from your local `git config`)
|
|
89
|
+
3. **Global mog config** (`~/.mog/config.json`)
|
|
90
|
+
|
|
91
|
+
Most users need zero configuration — `mog` reads your host git identity automatically. Use `mog config` to override when needed:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# View current per-repo config (auto-detected from git remote)
|
|
95
|
+
mog config
|
|
96
|
+
|
|
97
|
+
# Set per-repo git identity
|
|
98
|
+
mog config user.name "Your Name"
|
|
99
|
+
mog config user.email "you@example.com"
|
|
100
|
+
|
|
101
|
+
# Set global fallback identity
|
|
102
|
+
mog config --global user.name "Your Name"
|
|
103
|
+
mog config --global user.email "you@example.com"
|
|
77
104
|
```
|
|
78
105
|
|
|
79
106
|
### Re-mogging
|
|
@@ -122,12 +149,32 @@ mog 123 --fresh
|
|
|
122
149
|
|
|
123
150
|
## Configuration
|
|
124
151
|
|
|
152
|
+
### Environment variables
|
|
153
|
+
|
|
125
154
|
| Environment Variable | Default | Description |
|
|
126
155
|
|---|---|---|
|
|
127
156
|
| `MOG_REPOS_DIR` | `~/mog-repos` | Where repos are cloned and worktrees created (also the sandbox workspace) |
|
|
128
157
|
| `MOG_MAX_ITERATIONS` | `30` | Max build loop iterations per issue |
|
|
129
158
|
| `MOG_MAX_CONTINUATIONS` | — | Legacy alias for `MOG_MAX_ITERATIONS` |
|
|
130
159
|
|
|
160
|
+
### Config files
|
|
161
|
+
|
|
162
|
+
`mog config` manages git identity settings stored in `~/.mog/`:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
~/.mog/
|
|
166
|
+
config.json ← global config
|
|
167
|
+
repos/
|
|
168
|
+
owner/repo/config.json ← per-repo config
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Config Key | Description |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `user.name` | Git author name for commits inside the sandbox |
|
|
174
|
+
| `user.email` | Git author email for commits inside the sandbox |
|
|
175
|
+
|
|
176
|
+
See [Git identity](#git-identity) for details on how these are resolved.
|
|
177
|
+
|
|
131
178
|
## Worktree management
|
|
132
179
|
|
|
133
180
|
`mog` uses bare clones and git worktrees so you can run multiple issues concurrently without conflicts:
|
package/package.json
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { loadConfig, saveConfig, loadRepoConfig, saveRepoConfig, getGitIdentity } from "./config";
|
|
6
|
+
|
|
7
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mog-config-test-"));
|
|
8
|
+
const originalHome = process.env.HOME;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
process.env.HOME = tmpDir;
|
|
12
|
+
const mogDir = path.join(tmpDir, ".mog");
|
|
13
|
+
if (fs.existsSync(mogDir)) {
|
|
14
|
+
fs.rmSync(mogDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
process.env.HOME = originalHome;
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("loadConfig", () => {
|
|
24
|
+
test("returns empty object when no config file exists", () => {
|
|
25
|
+
expect(loadConfig()).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns parsed config from file", () => {
|
|
29
|
+
const mogDir = path.join(tmpDir, ".mog");
|
|
30
|
+
fs.mkdirSync(mogDir, { recursive: true });
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.join(mogDir, "config.json"),
|
|
33
|
+
JSON.stringify({ "user.name": "Alice", "user.email": "alice@example.com" })
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(loadConfig()).toEqual({
|
|
37
|
+
"user.name": "Alice",
|
|
38
|
+
"user.email": "alice@example.com",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("returns empty object for invalid JSON", () => {
|
|
43
|
+
const mogDir = path.join(tmpDir, ".mog");
|
|
44
|
+
fs.mkdirSync(mogDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(path.join(mogDir, "config.json"), "not json");
|
|
46
|
+
|
|
47
|
+
expect(loadConfig()).toEqual({});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("saveConfig", () => {
|
|
52
|
+
test("creates .mog directory and writes config", () => {
|
|
53
|
+
saveConfig({ "user.name": "Bob", "user.email": "bob@example.com" });
|
|
54
|
+
|
|
55
|
+
const configPath = path.join(tmpDir, ".mog", "config.json");
|
|
56
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
57
|
+
|
|
58
|
+
const content = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
59
|
+
expect(content).toEqual({ "user.name": "Bob", "user.email": "bob@example.com" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("overwrites existing config", () => {
|
|
63
|
+
saveConfig({ "user.name": "First" });
|
|
64
|
+
saveConfig({ "user.name": "Second", "user.email": "second@example.com" });
|
|
65
|
+
|
|
66
|
+
const configPath = path.join(tmpDir, ".mog", "config.json");
|
|
67
|
+
const content = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
68
|
+
expect(content).toEqual({ "user.name": "Second", "user.email": "second@example.com" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("saves empty config", () => {
|
|
72
|
+
saveConfig({});
|
|
73
|
+
|
|
74
|
+
const configPath = path.join(tmpDir, ".mog", "config.json");
|
|
75
|
+
const content = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
76
|
+
expect(content).toEqual({});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("loadRepoConfig", () => {
|
|
81
|
+
test("returns empty object when no repo config file exists", () => {
|
|
82
|
+
expect(loadRepoConfig("owner/repo")).toEqual({});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("returns parsed repo config from file", () => {
|
|
86
|
+
saveRepoConfig("owner/repo", { "user.name": "Alice", "user.email": "alice@work.com" });
|
|
87
|
+
expect(loadRepoConfig("owner/repo")).toEqual({
|
|
88
|
+
"user.name": "Alice",
|
|
89
|
+
"user.email": "alice@work.com",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("keeps repo configs separate", () => {
|
|
94
|
+
saveRepoConfig("owner/repo-a", { "user.name": "Alice", "user.email": "alice@work.com" });
|
|
95
|
+
saveRepoConfig("owner/repo-b", { "user.name": "Bob", "user.email": "bob@personal.com" });
|
|
96
|
+
|
|
97
|
+
expect(loadRepoConfig("owner/repo-a")).toEqual({ "user.name": "Alice", "user.email": "alice@work.com" });
|
|
98
|
+
expect(loadRepoConfig("owner/repo-b")).toEqual({ "user.name": "Bob", "user.email": "bob@personal.com" });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("getGitIdentity", () => {
|
|
103
|
+
test("falls back to global config when no repo specified", () => {
|
|
104
|
+
saveConfig({ "user.name": "Global", "user.email": "global@example.com" });
|
|
105
|
+
// Note: this test also picks up host git config (tier 2), which runs before global.
|
|
106
|
+
// Since we can't easily mock host git, we just verify it returns something non-null.
|
|
107
|
+
const identity = getGitIdentity();
|
|
108
|
+
expect(identity).not.toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("per-repo config takes priority over global config", () => {
|
|
112
|
+
saveConfig({ "user.name": "Global", "user.email": "global@example.com" });
|
|
113
|
+
saveRepoConfig("owner/repo", { "user.name": "RepoUser", "user.email": "repo@work.com" });
|
|
114
|
+
|
|
115
|
+
expect(getGitIdentity("owner/repo")).toEqual({
|
|
116
|
+
name: "RepoUser",
|
|
117
|
+
email: "repo@work.com",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("falls back through chain when per-repo config is incomplete", () => {
|
|
122
|
+
saveRepoConfig("owner/repo", { "user.name": "RepoUser" }); // missing email
|
|
123
|
+
saveConfig({ "user.name": "Global", "user.email": "global@example.com" });
|
|
124
|
+
|
|
125
|
+
// Should skip per-repo (incomplete), pick up host git or global
|
|
126
|
+
const identity = getGitIdentity("owner/repo");
|
|
127
|
+
expect(identity).not.toBeNull();
|
|
128
|
+
// Should NOT be the incomplete repo config
|
|
129
|
+
expect(identity!.email).not.toBe("");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns null when no config exists anywhere and host git is empty", () => {
|
|
133
|
+
// With a fake HOME, host git config may still return the real system config.
|
|
134
|
+
// This test just verifies it doesn't crash with no mog configs at all.
|
|
135
|
+
const identity = getGitIdentity("nonexistent/repo");
|
|
136
|
+
// Can't assert null since host git may be configured, but it shouldn't throw
|
|
137
|
+
expect(identity === null || (identity.name && identity.email)).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("returns null for global config with empty strings", () => {
|
|
141
|
+
saveConfig({ "user.name": "", "user.email": "" });
|
|
142
|
+
// With empty mog config and potentially no host git, may return null
|
|
143
|
+
const identity = getGitIdentity();
|
|
144
|
+
// If host git is configured it'll return that, otherwise null — both are valid
|
|
145
|
+
expect(identity === null || (identity.name && identity.email)).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export interface MogConfig {
|
|
5
|
+
"user.name"?: string;
|
|
6
|
+
"user.email"?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface GitIdentity {
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getConfigPath(): string {
|
|
15
|
+
return path.join(process.env.HOME || "~", ".mog", "config.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getRepoConfigPath(repo: string): string {
|
|
19
|
+
return path.join(process.env.HOME || "~", ".mog", "repos", repo, "config.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadConfig(): MogConfig {
|
|
23
|
+
const configPath = getConfigPath();
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
26
|
+
return JSON.parse(raw) as MogConfig;
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveConfig(config: MogConfig): void {
|
|
33
|
+
const configPath = getConfigPath();
|
|
34
|
+
const dir = path.dirname(configPath);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadRepoConfig(repo: string): MogConfig {
|
|
40
|
+
const configPath = getRepoConfigPath(repo);
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
43
|
+
return JSON.parse(raw) as MogConfig;
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function saveRepoConfig(repo: string, config: MogConfig): void {
|
|
50
|
+
const configPath = getRepoConfigPath(repo);
|
|
51
|
+
const dir = path.dirname(configPath);
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function detectHostGitIdentity(): GitIdentity | null {
|
|
57
|
+
const nameResult = Bun.spawnSync(["git", "config", "user.name"]);
|
|
58
|
+
const emailResult = Bun.spawnSync(["git", "config", "user.email"]);
|
|
59
|
+
|
|
60
|
+
const name = nameResult.exitCode === 0 ? nameResult.stdout.toString().trim() : "";
|
|
61
|
+
const email = emailResult.exitCode === 0 ? emailResult.stdout.toString().trim() : "";
|
|
62
|
+
|
|
63
|
+
if (name && email) {
|
|
64
|
+
return { name, email };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function identityFromConfig(config: MogConfig): GitIdentity | null {
|
|
71
|
+
const name = config["user.name"];
|
|
72
|
+
const email = config["user.email"];
|
|
73
|
+
if (name && email) {
|
|
74
|
+
return { name, email };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getGitIdentity(repo?: string): GitIdentity | null {
|
|
80
|
+
// 1. Per-repo mog config (explicit override)
|
|
81
|
+
if (repo) {
|
|
82
|
+
const repoIdentity = identityFromConfig(loadRepoConfig(repo));
|
|
83
|
+
if (repoIdentity) return repoIdentity;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Host git config (auto-detected from cwd)
|
|
87
|
+
const hostIdentity = detectHostGitIdentity();
|
|
88
|
+
if (hostIdentity) return hostIdentity;
|
|
89
|
+
|
|
90
|
+
// 3. Global mog config (fallback)
|
|
91
|
+
return identityFromConfig(loadConfig());
|
|
92
|
+
}
|
package/src/github.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getGitIdentity } from "./config";
|
|
1
2
|
import { log } from "./log";
|
|
2
3
|
|
|
3
4
|
export interface Issue {
|
|
@@ -150,6 +151,14 @@ export function getConventionalPrefix(issue: Issue): string {
|
|
|
150
151
|
return labels.includes("enhancement") || labels.includes("feature") ? "feat" : "fix";
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
function gitWithIdentity(repo: string, ...args: string[]): string[] {
|
|
155
|
+
const identity = getGitIdentity(repo);
|
|
156
|
+
if (identity) {
|
|
157
|
+
return ["git", `-c`, `user.name=${identity.name}`, `-c`, `user.email=${identity.email}`, ...args];
|
|
158
|
+
}
|
|
159
|
+
return ["git", ...args];
|
|
160
|
+
}
|
|
161
|
+
|
|
153
162
|
export function pushAndCreatePR(
|
|
154
163
|
repo: string,
|
|
155
164
|
worktreeDir: string,
|
|
@@ -183,7 +192,7 @@ export function pushAndCreatePR(
|
|
|
183
192
|
if (addResult.exitCode !== 0) {
|
|
184
193
|
log.die("Failed to stage changes.");
|
|
185
194
|
}
|
|
186
|
-
const commitResult = Bun.spawnSync(
|
|
195
|
+
const commitResult = Bun.spawnSync(gitWithIdentity(repo, "commit", "-m", `${prefix}: address issue #${issueNum} - ${cleanIssueTitle(issue.title)}`), { cwd: worktreeDir });
|
|
187
196
|
if (commitResult.exitCode !== 0) {
|
|
188
197
|
log.warn("Commit failed — changes may already be committed.");
|
|
189
198
|
}
|
|
@@ -198,7 +207,7 @@ export function pushAndCreatePR(
|
|
|
198
207
|
const squash = Bun.spawnSync(["git", "reset", "--soft", mergeBase], { cwd: worktreeDir });
|
|
199
208
|
if (squash.exitCode === 0) {
|
|
200
209
|
const msg = `${prefix}: ${cleanIssueTitle(issue.title).toLowerCase()} (#${issueNum})`;
|
|
201
|
-
Bun.spawnSync(
|
|
210
|
+
Bun.spawnSync(gitWithIdentity(repo, "commit", "-m", msg), { cwd: worktreeDir });
|
|
202
211
|
log.ok("Commits squashed.");
|
|
203
212
|
} else {
|
|
204
213
|
log.warn("Failed to squash — pushing individual commits instead.");
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,9 @@ import fs from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { fetchIssue, listIssues, fetchPRFeedback, closePR, cleanIssueTitle, pushAndCreatePR } from "./github";
|
|
6
6
|
import { detectRepo, ensureRepo, createWorktree } from "./worktree";
|
|
7
|
-
import { runClaude } from "./sandbox";
|
|
7
|
+
import { runClaude, applySandboxGitConfig } from "./sandbox";
|
|
8
|
+
import { loadConfig, saveConfig, loadRepoConfig, saveRepoConfig } from "./config";
|
|
9
|
+
import type { MogConfig } from "./config";
|
|
8
10
|
import type { PRFeedback } from "./github";
|
|
9
11
|
import { log } from "./log";
|
|
10
12
|
|
|
@@ -59,9 +61,38 @@ async function init() {
|
|
|
59
61
|
log.ok("mog is ready. Run: mog <issue_number> (from a git repo) or mog <owner/repo> <issue_number>");
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
function printUsage(): void {
|
|
65
|
+
console.log("Usage:");
|
|
66
|
+
console.log(" mog init — one-time setup (create sandbox & login)");
|
|
67
|
+
console.log(" mog <issue_num> — auto-detect repo from git remote");
|
|
68
|
+
console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
|
|
69
|
+
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
70
|
+
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
71
|
+
console.log();
|
|
72
|
+
console.log("Options:");
|
|
73
|
+
console.log(" --include <file> — copy a file into the worktree (repeatable)");
|
|
74
|
+
console.log(" --fresh — ignore existing PR, start a brand new one");
|
|
75
|
+
console.log(" --version, -v — print the current version");
|
|
76
|
+
console.log();
|
|
77
|
+
console.log("Example:");
|
|
78
|
+
console.log(" mog init");
|
|
79
|
+
console.log(" mog 123");
|
|
80
|
+
console.log(" mog 123 --include .env");
|
|
81
|
+
console.log(" mog workingdevshero/automate-it 123");
|
|
82
|
+
console.log(" mog list");
|
|
83
|
+
console.log(" mog list --verbose");
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
async function main() {
|
|
63
87
|
const args = process.argv.slice(2);
|
|
64
88
|
|
|
89
|
+
// Handle --version / -v before anything else (no dependencies required)
|
|
90
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
91
|
+
const pkg = await Bun.file(path.join(import.meta.dir, "..", "package.json")).json();
|
|
92
|
+
console.log(pkg.version);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
// Validate dependencies
|
|
66
97
|
for (const cmd of ["gh", "git", "docker"]) {
|
|
67
98
|
const which = Bun.spawnSync(["which", cmd]);
|
|
@@ -86,6 +117,65 @@ async function main() {
|
|
|
86
117
|
return;
|
|
87
118
|
}
|
|
88
119
|
|
|
120
|
+
// mog config [--global] [key] [value]
|
|
121
|
+
if (args[0] === "config") {
|
|
122
|
+
const configArgs = args.slice(1);
|
|
123
|
+
const isGlobal = configArgs.includes("--global");
|
|
124
|
+
const filtered = configArgs.filter(a => a !== "--global");
|
|
125
|
+
const key = filtered[0];
|
|
126
|
+
const value = filtered[1];
|
|
127
|
+
const validKeys: (keyof MogConfig)[] = ["user.name", "user.email"];
|
|
128
|
+
|
|
129
|
+
// Determine which config to use: --global or per-repo (auto-detected)
|
|
130
|
+
let repo: string | null = null;
|
|
131
|
+
if (!isGlobal) {
|
|
132
|
+
repo = detectRepo();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const load = () => repo ? loadRepoConfig(repo) : loadConfig();
|
|
136
|
+
const save = (c: MogConfig) => repo ? saveRepoConfig(repo, c) : saveConfig(c);
|
|
137
|
+
const scope = repo ? `repo (${repo})` : "global";
|
|
138
|
+
|
|
139
|
+
if (!key) {
|
|
140
|
+
// Show all config values
|
|
141
|
+
const config = load();
|
|
142
|
+
if (Object.keys(config).length === 0) {
|
|
143
|
+
log.info(`No ${scope} config values set.`);
|
|
144
|
+
} else {
|
|
145
|
+
log.info(`${scope} config:`);
|
|
146
|
+
for (const [k, v] of Object.entries(config)) {
|
|
147
|
+
console.log(`${k}=${v}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!validKeys.includes(key as keyof MogConfig)) {
|
|
154
|
+
log.die(`Unknown config key: '${key}'. Valid keys: ${validKeys.join(", ")}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const configKey = key as keyof MogConfig;
|
|
158
|
+
|
|
159
|
+
if (value === undefined) {
|
|
160
|
+
// Read a single value
|
|
161
|
+
const config = load();
|
|
162
|
+
const val = config[configKey];
|
|
163
|
+
if (val !== undefined) {
|
|
164
|
+
console.log(val);
|
|
165
|
+
} else {
|
|
166
|
+
log.die(`Config key '${key}' is not set (${scope}).`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Set a value
|
|
172
|
+
const config = load();
|
|
173
|
+
config[configKey] = value;
|
|
174
|
+
save(config);
|
|
175
|
+
log.ok(`${key}=${value} (${scope})`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
89
179
|
// mog list [--verbose] or mog <owner/repo> list [--verbose]
|
|
90
180
|
if (args[0] === "list" || args[1] === "list") {
|
|
91
181
|
let repo: string;
|
|
@@ -106,24 +196,7 @@ async function main() {
|
|
|
106
196
|
}
|
|
107
197
|
|
|
108
198
|
if (args.length < 1) {
|
|
109
|
-
|
|
110
|
-
console.log(" mog init — one-time setup (create sandbox & login)");
|
|
111
|
-
console.log(" mog <issue_num> — auto-detect repo from git remote");
|
|
112
|
-
console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
|
|
113
|
-
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
114
|
-
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
115
|
-
console.log();
|
|
116
|
-
console.log("Options:");
|
|
117
|
-
console.log(" --include <file> — copy a file into the worktree (repeatable)");
|
|
118
|
-
console.log(" --fresh — ignore existing PR, start a brand new one");
|
|
119
|
-
console.log();
|
|
120
|
-
console.log("Example:");
|
|
121
|
-
console.log(" mog init");
|
|
122
|
-
console.log(" mog 123");
|
|
123
|
-
console.log(" mog 123 --include .env");
|
|
124
|
-
console.log(" mog workingdevshero/automate-it 123");
|
|
125
|
-
console.log(" mog list");
|
|
126
|
-
console.log(" mog list --verbose");
|
|
199
|
+
printUsage();
|
|
127
200
|
return;
|
|
128
201
|
}
|
|
129
202
|
|
|
@@ -165,24 +238,7 @@ async function main() {
|
|
|
165
238
|
log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
|
|
166
239
|
}
|
|
167
240
|
} else {
|
|
168
|
-
|
|
169
|
-
console.log(" mog init — one-time setup (create sandbox & login)");
|
|
170
|
-
console.log(" mog <issue_num> — auto-detect repo from git remote");
|
|
171
|
-
console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
|
|
172
|
-
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
173
|
-
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
174
|
-
console.log();
|
|
175
|
-
console.log("Options:");
|
|
176
|
-
console.log(" --include <file> — copy a file into the worktree (repeatable)");
|
|
177
|
-
console.log(" --fresh — ignore existing PR, start a brand new one");
|
|
178
|
-
console.log();
|
|
179
|
-
console.log("Example:");
|
|
180
|
-
console.log(" mog init");
|
|
181
|
-
console.log(" mog 123");
|
|
182
|
-
console.log(" mog 123 --include .env");
|
|
183
|
-
console.log(" mog workingdevshero/automate-it 123");
|
|
184
|
-
console.log(" mog list");
|
|
185
|
-
console.log(" mog list --verbose");
|
|
241
|
+
printUsage();
|
|
186
242
|
return;
|
|
187
243
|
}
|
|
188
244
|
|
|
@@ -251,6 +307,9 @@ async function main() {
|
|
|
251
307
|
const reviewPrompt = buildReviewPrompt(repo, issueNum, issue, defaultBranch);
|
|
252
308
|
const summaryPrompt = buildSummaryPrompt(repo, issueNum, issue, defaultBranch);
|
|
253
309
|
|
|
310
|
+
// Apply git identity inside sandbox before running Claude
|
|
311
|
+
applySandboxGitConfig(SANDBOX_NAME, repo);
|
|
312
|
+
|
|
254
313
|
// Run Claude in sandbox
|
|
255
314
|
log.info("Launching Claude Code in sandbox...");
|
|
256
315
|
log.info(`Branch: ${branchName}`);
|
|
@@ -274,6 +333,35 @@ async function main() {
|
|
|
274
333
|
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary, existingPR);
|
|
275
334
|
}
|
|
276
335
|
|
|
336
|
+
function printUsage(): void {
|
|
337
|
+
console.log("Usage:");
|
|
338
|
+
console.log(" mog init — one-time setup (create sandbox & login)");
|
|
339
|
+
console.log(" mog config [key] [value] — get/set per-repo config (auto-detected)");
|
|
340
|
+
console.log(" mog config --global [key] [value] — get/set global config");
|
|
341
|
+
console.log(" mog <issue_num> — auto-detect repo from git remote");
|
|
342
|
+
console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
|
|
343
|
+
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
344
|
+
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
345
|
+
console.log();
|
|
346
|
+
console.log("Options:");
|
|
347
|
+
console.log(" --include <file> — copy a file into the worktree (repeatable)");
|
|
348
|
+
console.log(" --fresh — ignore existing PR, start a brand new one");
|
|
349
|
+
console.log();
|
|
350
|
+
console.log("Config keys: user.name, user.email");
|
|
351
|
+
console.log(" Git identity is auto-detected from your repo's git config.");
|
|
352
|
+
console.log(" Use 'mog config' to override per-repo, or --global for all repos.");
|
|
353
|
+
console.log();
|
|
354
|
+
console.log("Example:");
|
|
355
|
+
console.log(" mog init");
|
|
356
|
+
console.log(" mog config user.name \"Your Name\"");
|
|
357
|
+
console.log(" mog config --global user.email \"you@example.com\"");
|
|
358
|
+
console.log(" mog 123");
|
|
359
|
+
console.log(" mog 123 --include .env");
|
|
360
|
+
console.log(" mog workingdevshero/automate-it 123");
|
|
361
|
+
console.log(" mog list");
|
|
362
|
+
console.log(" mog list --verbose");
|
|
363
|
+
}
|
|
364
|
+
|
|
277
365
|
function getReposDir(): string {
|
|
278
366
|
return process.env.MOG_REPOS_DIR || `${process.env.HOME}/mog-repos`;
|
|
279
367
|
}
|
package/src/sandbox.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { log } from "./log";
|
|
3
|
+
import { getGitIdentity } from "./config";
|
|
3
4
|
|
|
4
5
|
interface StreamEvent {
|
|
5
6
|
type: string;
|
|
@@ -24,6 +25,25 @@ const MAX_STALLS = 2;
|
|
|
24
25
|
const PLAN_FILENAME = "IMPLEMENTATION_PLAN.md";
|
|
25
26
|
const SUMMARY_FILENAME = "SUMMARY.md";
|
|
26
27
|
|
|
28
|
+
export function applySandboxGitConfig(sandboxName: string, repo?: string): void {
|
|
29
|
+
const identity = getGitIdentity(repo);
|
|
30
|
+
if (!identity) return;
|
|
31
|
+
|
|
32
|
+
Bun.spawnSync([
|
|
33
|
+
"docker", "sandbox", "exec",
|
|
34
|
+
sandboxName,
|
|
35
|
+
"git", "config", "--global", "user.name", identity.name,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
Bun.spawnSync([
|
|
39
|
+
"docker", "sandbox", "exec",
|
|
40
|
+
sandboxName,
|
|
41
|
+
"git", "config", "--global", "user.email", identity.email,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
log.ok(`Sandbox git identity: ${identity.name} <${identity.email}>`);
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
export function readPlanFile(worktreeDir: string): string | null {
|
|
28
48
|
const planPath = `${worktreeDir}/${PLAN_FILENAME}`;
|
|
29
49
|
try {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const entrypoint = path.join(import.meta.dir, "index.ts");
|
|
5
|
+
const bunPath = Bun.which("bun") ?? process.execPath;
|
|
6
|
+
const packageJson = require("../package.json");
|
|
7
|
+
|
|
8
|
+
describe("--version flag", () => {
|
|
9
|
+
test("--version prints the version from package.json", async () => {
|
|
10
|
+
const proc = Bun.spawn([bunPath, "run", entrypoint, "--version"], {
|
|
11
|
+
stdout: "pipe",
|
|
12
|
+
stderr: "pipe",
|
|
13
|
+
});
|
|
14
|
+
const stdout = await new Response(proc.stdout).text();
|
|
15
|
+
await proc.exited;
|
|
16
|
+
expect(proc.exitCode).toBe(0);
|
|
17
|
+
expect(stdout.trim()).toBe(packageJson.version);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("-v prints the version from package.json", async () => {
|
|
21
|
+
const proc = Bun.spawn([bunPath, "run", entrypoint, "-v"], {
|
|
22
|
+
stdout: "pipe",
|
|
23
|
+
stderr: "pipe",
|
|
24
|
+
});
|
|
25
|
+
const stdout = await new Response(proc.stdout).text();
|
|
26
|
+
await proc.exited;
|
|
27
|
+
expect(proc.exitCode).toBe(0);
|
|
28
|
+
expect(stdout.trim()).toBe(packageJson.version);
|
|
29
|
+
});
|
|
30
|
+
});
|