agentxchain 0.8.6 → 0.8.8

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
@@ -16,12 +16,21 @@ npx agentxchain init
16
16
 
17
17
  ## Quick start
18
18
 
19
+ ### Happy path: net-new project
20
+
19
21
  ```bash
20
- # 1. Create a project (interactive template selection)
21
- agentxchain init
22
+ npx agentxchain init
23
+ cd my-agentxchain-project # default with init -y, or your chosen folder name
24
+ agentxchain kickoff
25
+ ```
26
+
27
+ ### Happy path: existing project
22
28
 
23
- # 2. Run guided PM-first kickoff wizard
24
- cd my-project/
29
+ Run these commands from inside your existing project folder:
30
+
31
+ ```bash
32
+ agentxchain doctor
33
+ agentxchain generate
25
34
  agentxchain kickoff
26
35
  ```
27
36
 
@@ -43,9 +52,11 @@ Agents are now required to maintain `TALK.md` as the human-readable handoff log
43
52
  | `doctor` | Validate local setup (tools, trigger flow, accessibility checks) |
44
53
  | `claim` | Human takes control (agents stop claiming) |
45
54
  | `release` | Hand lock back to agents |
46
- | `stop` | Terminate running Claude Code agent sessions |
47
- | `watch` | Optional: TTL safety net + status logging |
55
+ | `stop` | Stop watch daemon, end Claude Code sessions; Cursor/VS Code chats close manually |
56
+ | `branch` | Show/set Cursor branch override for launches |
57
+ | `watch` | Referee loop: validates turns, writes next trigger, and force-releases stale locks |
48
58
  | `config` | View/edit config, add/remove agents, change rules |
59
+ | `rebind` | Rebuild Cursor workspace/prompt bindings for agents |
49
60
  | `update` | Self-update CLI from npm |
50
61
 
51
62
  ### Full command list
@@ -56,7 +67,9 @@ agentxchain status
56
67
  agentxchain start
57
68
  agentxchain kickoff
58
69
  agentxchain stop
70
+ agentxchain branch
59
71
  agentxchain config
72
+ agentxchain rebind
60
73
  agentxchain generate
61
74
  agentxchain watch
62
75
  agentxchain supervise
@@ -94,40 +107,28 @@ agentxchain watch --daemon # run watch in background
94
107
  agentxchain supervise --autonudge # run watch + AppleScript nudge loop
95
108
  agentxchain supervise --autonudge --send # auto-press Enter after paste
96
109
  agentxchain supervise --interval 2 # set auto-nudge poll interval
110
+ agentxchain rebind # regenerate agent prompt/workspace bindings
111
+ agentxchain rebind --open # regenerate and reopen all Cursor agent windows
112
+ agentxchain rebind --agent pm # regenerate one agent binding only
97
113
  agentxchain claim --agent pm # guarded claim as agent turn owner
98
114
  agentxchain release --agent pm # guarded release as agent turn owner
99
115
  agentxchain release --force # force-release non-human holder lock
116
+ agentxchain config --set "rules.strict_next_owner true" # TALK-only next owner (no cyclic fallback)
100
117
  ```
101
118
 
102
119
  ## macOS auto-nudge (AppleScript)
103
120
 
104
- If you want the next agent chat to be nudged automatically when turn changes, use the built-in AppleScript helper.
105
-
106
- 1) Keep watcher running in your project:
121
+ **Recommended:** `agentxchain supervise --autonudge` (starts `watch` + auto-nudge together). Requires macOS, `jq`, and Accessibility for Terminal + Cursor.
107
122
 
108
123
  ```bash
109
- agentxchain watch
110
- # or use the combined command:
111
124
  agentxchain supervise --autonudge
125
+ agentxchain supervise --autonudge --send # paste + Enter
112
126
  ```
113
127
 
114
- 2) In another terminal (from `cli/`), start auto-nudge:
115
-
116
- ```bash
117
- bash scripts/run-autonudge.sh --project "/absolute/path/to/your-project"
118
- ```
119
-
120
- By default this is **paste-only** (safe mode): it opens chat and pastes the nudge message, but does not press Enter.
121
-
122
- 3) Enable auto-send once confirmed:
123
-
124
- ```bash
125
- bash scripts/run-autonudge.sh --project "/absolute/path/to/your-project" --send
126
- ```
127
-
128
- Stop it anytime:
128
+ **Advanced (debugging):** from a checkout of `cli/`, run the script alone while `watch` is already running:
129
129
 
130
130
  ```bash
131
+ bash scripts/run-autonudge.sh --project "/absolute/path/to/your-project" [--send]
131
132
  bash scripts/stop-autonudge.sh
132
133
  ```
133
134
 
@@ -146,22 +147,21 @@ Notes:
146
147
  2. Each window gets a unique prompt copied to clipboard
147
148
  3. Kickoff validates PM signoff and launches remaining agents
148
149
  4. Agent prompts are single-turn: claim → work → validate → release → stop
149
- 5. Agents know their rotation order from `agentxchain.json` and only claim when the previous agent released
150
+ 5. Agents use the latest `Next owner:` in `TALK.md` to pick who goes next (fallback: config order)
150
151
  6. Human can `claim` to pause and `release` to resume anytime
151
152
 
152
153
  ### VS Code mode
153
154
 
154
155
  1. `agentxchain init` generates `.github/agents/*.agent.md` (VS Code custom agents) and `.github/hooks/` (lifecycle hooks)
155
- 2. VS Code auto-discovers agents in the Chat dropdown
156
+ 2. VS Code discovers custom agents in Chat when using **GitHub Copilot** agents (see Microsoft docs)
156
157
  3. The `Stop` hook acts as referee — hands off to next agent automatically
157
158
 
158
- ### Turn rotation
159
+ ### Turn ownership
159
160
 
160
- Agents follow a round-robin order defined in `agentxchain.json`:
161
- - PM waits for: lock free + last released by `human`, `null`, or last agent in rotation
162
- - Dev waits for: lock free + last released by `pm`
163
- - QA waits for: lock free + last released by `dev`
164
- - And so on...
161
+ Agent turns are handoff-driven:
162
+ - Each turn appends a `Next owner:` in `TALK.md` with a valid agent id
163
+ - `watch`/`supervise` dispatches the next trigger from that handoff
164
+ - `claim --agent <id>` enforces that expected owner (with guarded fallback)
165
165
 
166
166
  ## Key features
167
167
 
@@ -176,14 +176,12 @@ Agents follow a round-robin order defined in `agentxchain.json`:
176
176
 
177
177
  ## VS Code extension (optional)
178
178
 
179
- For a richer UI in VS Code:
179
+ The VSIX is not committed to the repo. Build/package from `cli/vscode-extension/` (see that folder’s README or `vsce package`), then:
180
180
 
181
181
  ```bash
182
- code --install-extension cli/vscode-extension/agentxchain-0.1.0.vsix
182
+ code --install-extension /path/to/agentxchain-*.vsix
183
183
  ```
184
184
 
185
- Adds: status bar (lock holder, turn, phase), sidebar dashboard, command palette integration.
186
-
187
185
  ## Publish updates (maintainers)
188
186
 
189
187
  ```bash
@@ -1,9 +1,52 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync } from 'fs';
4
- import { join, dirname } from 'path';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { join, dirname, parse as pathParse, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { Command } from 'commander';
7
+
8
+ // Load .env from AgentXchain project root when available, then cwd as fallback.
9
+ (function loadDotenv() {
10
+ const cwd = process.cwd();
11
+ const projectRoot = findNearestProjectRoot(cwd);
12
+ const envPaths = [];
13
+
14
+ if (projectRoot) {
15
+ envPaths.push(join(projectRoot, '.env'));
16
+ }
17
+ if (!projectRoot || projectRoot !== cwd) {
18
+ envPaths.push(join(cwd, '.env'));
19
+ }
20
+
21
+ for (const envPath of envPaths) {
22
+ if (!existsSync(envPath)) continue;
23
+ try {
24
+ const content = readFileSync(envPath, 'utf8');
25
+ for (const line of content.split(/\r?\n/)) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith('#')) continue;
28
+ const eqIdx = trimmed.indexOf('=');
29
+ if (eqIdx === -1) continue;
30
+ const key = trimmed.slice(0, eqIdx).trim();
31
+ let val = trimmed.slice(eqIdx + 1).trim();
32
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
33
+ val = val.slice(1, -1);
34
+ }
35
+ if (!process.env[key]) process.env[key] = val;
36
+ }
37
+ } catch {}
38
+ }
39
+ })();
40
+
41
+ function findNearestProjectRoot(startDir) {
42
+ let dir = resolve(startDir);
43
+ const { root: fsRoot } = pathParse(dir);
44
+ while (true) {
45
+ if (existsSync(join(dir, 'agentxchain.json'))) return dir;
46
+ if (dir === fsRoot) return null;
47
+ dir = join(dir, '..');
48
+ }
49
+ }
7
50
  import { initCommand } from '../src/commands/init.js';
8
51
  import { statusCommand } from '../src/commands/status.js';
9
52
  import { startCommand } from '../src/commands/start.js';
@@ -17,6 +60,8 @@ import { doctorCommand } from '../src/commands/doctor.js';
17
60
  import { superviseCommand } from '../src/commands/supervise.js';
18
61
  import { validateCommand } from '../src/commands/validate.js';
19
62
  import { kickoffCommand } from '../src/commands/kickoff.js';
63
+ import { rebindCommand } from '../src/commands/rebind.js';
64
+ import { branchCommand } from '../src/commands/branch.js';
20
65
 
21
66
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
67
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -60,7 +105,7 @@ program
60
105
 
61
106
  program
62
107
  .command('stop')
63
- .description('Stop all running agent sessions')
108
+ .description('Stop watch daemon and Claude Code sessions; close Cursor/VS Code chats manually')
64
109
  .action(stopCommand);
65
110
 
66
111
  program
@@ -72,6 +117,13 @@ program
72
117
  .option('-j, --json', 'Output config as JSON')
73
118
  .action(configCommand);
74
119
 
120
+ program
121
+ .command('branch [name]')
122
+ .description('Show or set the Cursor branch used for launches')
123
+ .option('--use-current', 'Set override to the current local git branch')
124
+ .option('--unset', 'Remove override and follow the active git branch automatically')
125
+ .action(branchCommand);
126
+
75
127
  program
76
128
  .command('generate')
77
129
  .description('Regenerate VS Code agent files (.agent.md, hooks) from agentxchain.json')
@@ -91,6 +143,13 @@ program
91
143
  .option('--interval <seconds>', 'Auto-nudge poll interval in seconds', '3')
92
144
  .action(superviseCommand);
93
145
 
146
+ program
147
+ .command('rebind')
148
+ .description('Rebuild Cursor prompt/workspace bindings for agents')
149
+ .option('--agent <id>', 'Rebind a single agent only')
150
+ .option('--open', 'Reopen Cursor windows after rebinding')
151
+ .action(rebindCommand);
152
+
94
153
  program
95
154
  .command('claim')
96
155
  .description('Claim the lock as a human (take control)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "dev": "node bin/agentxchain.js",
17
+ "test": "node --test test/*.test.js",
17
18
  "build:macos": "bun build bin/agentxchain.js --compile --target=bun-darwin-arm64 --outfile=dist/agentxchain-macos-arm64",
18
19
  "build:linux": "bun build bin/agentxchain.js --compile --target=bun-linux-x64 --outfile=dist/agentxchain-linux-x64",
19
20
  "publish:npm": "bash scripts/publish-npm.sh"
@@ -43,6 +44,6 @@
43
44
  "ora": "^8.0.0"
44
45
  },
45
46
  "engines": {
46
- "node": ">=18"
47
+ "node": ">=18.17.0 || >=20.5.0"
47
48
  }
48
49
  }
@@ -1,6 +1,7 @@
1
1
  property projectRoot : ""
2
2
  property pollSeconds : 3
3
3
  property autoSend : false
4
+ property lastFailedDispatch : ""
4
5
 
5
6
  on run argv
6
7
  if (count of argv) < 1 then
@@ -41,8 +42,10 @@ on run argv
41
42
  set lastKey to do shell script "test -f " & quoted form of statePath & " && cat " & quoted form of statePath & " || echo ''"
42
43
 
43
44
  if dispatchKey is not lastKey then
44
- my nudgeAgent(agentId, turnNum)
45
- do shell script "printf %s " & quoted form of dispatchKey & " > " & quoted form of statePath
45
+ set nudgedOk to my nudgeAgent(agentId, turnNum, dispatchKey)
46
+ if nudgedOk then
47
+ do shell script "printf %s " & quoted form of dispatchKey & " > " & quoted form of statePath
48
+ end if
46
49
  end if
47
50
  end if
48
51
  end if
@@ -52,35 +55,67 @@ on run argv
52
55
  end repeat
53
56
  end run
54
57
 
55
- on nudgeAgent(agentId, turnNum)
58
+ on nudgeAgent(agentId, turnNum, dispatchKey)
56
59
  set nudgeText to "Hey " & agentId & ", it is your turn now (turn " & turnNum & "). Read lock.json, claim the lock, check state.md + history.jsonl + planning docs, do your work, and release lock."
57
60
  set the clipboard to nudgeText
58
61
 
62
+ tell application "System Events"
63
+ if not (exists process "Cursor") then
64
+ if lastFailedDispatch is not dispatchKey then
65
+ do shell script "osascript -e " & quoted form of ("display notification \"Cursor is not running.\" with title \"AgentXchain\"")
66
+ set lastFailedDispatch to dispatchKey
67
+ end if
68
+ return false
69
+ end if
70
+ end tell
71
+
59
72
  tell application "Cursor" to activate
60
- delay 0.5
73
+ delay 0.6
74
+
75
+ -- Verify Cursor is actually frontmost before sending keystrokes
76
+ tell application "System Events"
77
+ set frontApp to name of first application process whose frontmost is true
78
+ if frontApp is not "Cursor" then
79
+ if lastFailedDispatch is not dispatchKey then
80
+ do shell script "osascript -e " & quoted form of ("display notification \"Cursor lost focus, skipping nudge for " & agentId & ".\" with title \"AgentXchain\"")
81
+ set lastFailedDispatch to dispatchKey
82
+ end if
83
+ return false
84
+ end if
85
+ end tell
86
+
61
87
  set focusedOk to my focusAgentWindow(agentId)
62
88
  if focusedOk is false then
63
- do shell script "osascript -e " & quoted form of ("display notification \"Could not identify a unique window for " & agentId & ".\" with title \"AgentXchain\"")
64
- return
89
+ if lastFailedDispatch is not dispatchKey then
90
+ do shell script "osascript -e " & quoted form of ("display notification \"Could not identify a unique window for " & agentId & ".\" with title \"AgentXchain\"")
91
+ set lastFailedDispatch to dispatchKey
92
+ end if
93
+ return false
65
94
  end if
66
- delay 0.2
95
+ delay 0.3
67
96
 
97
+ -- Re-verify focus before keystrokes
68
98
  tell application "System Events"
69
- if not (exists process "Cursor") then return
99
+ set frontApp to name of first application process whose frontmost is true
100
+ if frontApp is not "Cursor" then
101
+ return false
102
+ end if
70
103
 
71
104
  tell process "Cursor"
72
105
  set frontmost to true
73
106
  keystroke "l" using {command down}
74
- delay 0.2
107
+ delay 0.3
75
108
  keystroke "v" using {command down}
76
109
  if autoSend then
77
- delay 0.15
110
+ delay 0.2
78
111
  key code 36
79
112
  end if
80
113
  end tell
81
114
  end tell
82
115
 
116
+ set lastFailedDispatch to ""
83
117
  do shell script "osascript -e " & quoted form of ("display notification \"Nudged " & agentId & " for turn " & turnNum & "\" with title \"AgentXchain\"")
118
+ return true
84
119
  end nudgeAgent
85
120
 
86
121
  on focusAgentWindow(agentId)
@@ -117,6 +152,10 @@ end focusAgentWindow
117
152
 
118
153
  on isStrongWindowMatch(windowName, agentId)
119
154
  set tokenA to ".agentxchain-workspaces/" & agentId
155
+ set tokenB to ".agentxchain-workspaces\\" & agentId
156
+ set tokenC to agentId & ".code-workspace"
120
157
  if windowName contains tokenA then return true
158
+ if windowName contains tokenB then return true
159
+ if windowName contains tokenC then return true
121
160
  return false
122
161
  end isStrongWindowMatch
@@ -104,7 +104,7 @@ echo "Mode: $( [[ "${AUTO_SEND}" == "true" ]] && echo "auto-send" || echo "
104
104
  echo "Interval: ${INTERVAL_SECONDS}s"
105
105
  echo ""
106
106
  echo "Requirements:"
107
- echo "- Keep 'agentxchain watch' running in another terminal."
107
+ echo "- Watch must be running (e.g. 'agentxchain supervise --autonudge' starts it in the same supervisor, or run 'agentxchain watch' in another terminal)."
108
108
  echo "- Grant Accessibility permission to Terminal and Cursor."
109
109
  echo ""
110
110
 
@@ -1,8 +1,9 @@
1
1
  import { spawn } from 'child_process';
2
2
  import chalk from 'chalk';
3
3
  import { generateSeedPrompt } from '../lib/seed-prompt.js';
4
- import { writeFileSync } from 'fs';
5
4
  import { join } from 'path';
5
+ import { safeWriteJson } from '../lib/safe-write.js';
6
+ import { filterAgents } from '../lib/filter-agents.js';
6
7
 
7
8
  export async function launchClaudeCodeAgents(config, root, opts) {
8
9
  const agents = filterAgents(config, opts.agent);
@@ -28,22 +29,14 @@ export async function launchClaudeCodeAgents(config, root, opts) {
28
29
  }
29
30
 
30
31
  if (launched.length > 0) {
31
- const sessionFile = JSON.stringify({ launched, started_at: new Date().toISOString(), ide: 'claude-code' }, null, 2);
32
- writeFileSync(join(root, '.agentxchain-session.json'), sessionFile + '\n');
32
+ safeWriteJson(join(root, '.agentxchain-session.json'), {
33
+ launched,
34
+ started_at: new Date().toISOString(),
35
+ ide: 'claude-code'
36
+ });
33
37
  console.log('');
34
38
  console.log(chalk.dim(` Session saved to .agentxchain-session.json`));
35
39
  }
36
40
 
37
41
  return launched;
38
42
  }
39
-
40
- function filterAgents(config, specificId) {
41
- if (specificId) {
42
- if (!config.agents[specificId]) {
43
- console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
44
- process.exit(1);
45
- }
46
- return { [specificId]: config.agents[specificId] };
47
- }
48
- return config.agents;
49
- }
@@ -1,9 +1,10 @@
1
1
  import { execSync } from 'child_process';
2
- import { writeFileSync, mkdirSync, existsSync, symlinkSync, lstatSync, unlinkSync } from 'fs';
2
+ import { writeFileSync, mkdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import inquirer from 'inquirer';
6
6
  import { generatePollingPrompt } from '../lib/seed-prompt-polling.js';
7
+ import { filterAgents } from '../lib/filter-agents.js';
7
8
 
8
9
  export async function launchCursorLocal(config, root, opts) {
9
10
  const agents = filterAgents(config, opts.agent);
@@ -29,7 +30,7 @@ export async function launchCursorLocal(config, root, opts) {
29
30
  writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
30
31
  }
31
32
 
32
- // Create per-agent symlinked workspace folders so Cursor opens separate windows
33
+ // Create per-agent workspace files so each Cursor window has a unique identity
33
34
  const workspacesDir = join(root, '.agentxchain-workspaces');
34
35
  mkdirSync(workspacesDir, { recursive: true });
35
36
 
@@ -39,28 +40,33 @@ export async function launchCursorLocal(config, root, opts) {
39
40
  ? generateKickoffPrompt(id, agent, config, root)
40
41
  : generatePollingPrompt(id, agent, config, root);
41
42
 
42
- // Create symlink: .agentxchain-workspaces/<id> -> project root
43
- const agentWorkspace = join(workspacesDir, id);
44
- try {
45
- if (existsSync(agentWorkspace)) {
46
- const stat = lstatSync(agentWorkspace);
47
- if (stat.isSymbolicLink()) unlinkSync(agentWorkspace);
48
- }
49
- if (!existsSync(agentWorkspace)) {
50
- symlinkSync(root, agentWorkspace, 'dir');
51
- }
52
- } catch {}
43
+ // Create workspace file: .agentxchain-workspaces/<id>.code-workspace
44
+ const agentWorkspace = join(workspacesDir, `${id}.code-workspace`);
45
+ const workspaceJson = {
46
+ folders: [{ path: root }],
47
+ settings: { 'agentxchain.agentId': id }
48
+ };
49
+ writeFileSync(agentWorkspace, JSON.stringify(workspaceJson, null, 2) + '\n');
53
50
 
54
51
  console.log(chalk.cyan(` ─── Agent ${i + 1}/${total}: ${chalk.bold(id)} — ${agent.name} ───`));
55
52
  console.log('');
56
53
 
57
- copyToClipboard(prompt);
58
- console.log(chalk.green(` ✓ Prompt copied to clipboard.`));
54
+ const copied = copyToClipboard(prompt);
55
+ if (copied) {
56
+ console.log(chalk.green(' ✓ Prompt copied to clipboard.'));
57
+ } else {
58
+ console.log(chalk.yellow(' ! Clipboard copy failed. Use the saved prompt file manually.'));
59
+ }
59
60
  console.log(chalk.dim(` Saved to: .agentxchain-prompts/${id}.prompt.md`));
60
61
 
61
62
  // Open a separate Cursor window using the symlinked path
62
- openCursorWindow(agentWorkspace);
63
- console.log(chalk.dim(` Cursor window opened for ${id}.`));
63
+ const opened = openCursorWindow(agentWorkspace);
64
+ if (opened) {
65
+ console.log(chalk.dim(` Cursor window opened for ${id}.`));
66
+ } else {
67
+ console.log(chalk.yellow(` Could not open Cursor window automatically for ${id}.`));
68
+ console.log(chalk.dim(` Open manually: cursor --new-window "${agentWorkspace}"`));
69
+ }
64
70
 
65
71
  console.log('');
66
72
  console.log(` ${chalk.bold('In the new Cursor window:')}`);
@@ -112,17 +118,6 @@ export async function launchCursorLocal(config, root, opts) {
112
118
  console.log('');
113
119
  }
114
120
 
115
- function filterAgents(config, specificId) {
116
- if (specificId) {
117
- if (!config.agents[specificId]) {
118
- console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
119
- process.exit(1);
120
- }
121
- return { [specificId]: config.agents[specificId] };
122
- }
123
- return config.agents;
124
- }
125
-
126
121
  function copyToClipboard(text) {
127
122
  try {
128
123
  if (process.platform === 'darwin') {
@@ -141,10 +136,12 @@ function openCursorWindow(folderPath) {
141
136
  try {
142
137
  if (process.platform === 'darwin') {
143
138
  execSync(`open -na "Cursor" --args "${folderPath}"`, { stdio: 'ignore' });
144
- return;
139
+ return true;
145
140
  }
146
141
  execSync(`cursor --new-window "${folderPath}"`, { stdio: 'ignore' });
142
+ return true;
147
143
  } catch {}
144
+ return false;
148
145
  }
149
146
 
150
147
  function isPmLike(agentId, agentDef) {
@@ -1,7 +1,7 @@
1
- import { writeFileSync } from 'fs';
2
1
  import { join } from 'path';
3
2
  import chalk from 'chalk';
4
3
  import { loadConfig, CONFIG_FILE } from '../lib/config.js';
4
+ import { safeWriteJson } from '../lib/safe-write.js';
5
5
  import { getCurrentBranch } from '../lib/repo.js';
6
6
 
7
7
  export async function branchCommand(name, opts) {
@@ -94,5 +94,5 @@ function setBranchOverride(config, configPath, branch) {
94
94
  }
95
95
 
96
96
  function saveConfig(configPath, config) {
97
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
97
+ safeWriteJson(configPath, config);
98
98
  }