@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "One command to go from GitHub issue to pull request, powered by Claude Code in a Docker sandbox",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -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(["git", "commit", "-m", `${prefix}: address issue #${issueNum} - ${cleanIssueTitle(issue.title)}`], { cwd: worktreeDir });
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(["git", "commit", "-m", msg], { cwd: worktreeDir });
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
- console.log("Usage:");
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
- console.log("Usage:");
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
+ });