@searls/turbocommit 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -22
- package/cli.js +65 -42
- package/lib/agent.js +11 -4
- package/lib/codex.js +34 -0
- package/lib/config.js +52 -0
- package/lib/doctor.js +102 -10
- package/lib/harness.js +99 -0
- package/lib/init.js +18 -12
- package/lib/install.js +123 -5
- package/lib/log.js +5 -3
- package/lib/monitor.js +16 -3
- package/lib/run.js +105 -40
- package/lib/session.js +27 -21
- package/lib/track.js +14 -11
- package/lib/transcript.js +99 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -2,28 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
# turbocommit
|
|
4
4
|
|
|
5
|
-
turbocommit creates a git commit containing everything Claude Code
|
|
6
|
-
on each turn. Think of it like save state in a game emulator: whenever
|
|
7
|
-
fall in a pit, you can safely rewind and try again.
|
|
5
|
+
turbocommit creates a git commit containing everything Claude Code or Codex
|
|
6
|
+
changes on each turn. Think of it like save state in a game emulator: whenever
|
|
7
|
+
you fall in a pit, you can safely rewind and try again.
|
|
8
8
|
|
|
9
9
|
Captures every prompt/response transcript for the associated changes so you
|
|
10
10
|
and your agent never lose the thread on what you were doing after the fact,
|
|
11
11
|
even if you didn't leave behind any comments or docs.
|
|
12
12
|
|
|
13
|
-
Each commit also [links back to the previous commit from the same
|
|
14
|
-
session](#continuity-across-workstreams), so you can run multiple agent
|
|
13
|
+
Each commit also [links back to the previous commit from the same agent
|
|
14
|
+
session](#continuity-across-workstreams), so you can run multiple agent
|
|
15
15
|
sessions concurrently and detangle which agent committed what after the fact.
|
|
16
16
|
|
|
17
17
|
## How it works
|
|
18
18
|
|
|
19
|
-
turbocommit registers
|
|
20
|
-
|
|
19
|
+
turbocommit registers hooks with the harnesses you use:
|
|
20
|
+
|
|
21
|
+
- Claude Code: **PreToolUse**, **SessionStart**, **SessionEnd**, and **Stop**.
|
|
22
|
+
- Codex: **PreToolUse**, **SessionStart**, **PreCompact**, and
|
|
23
|
+
**Stop**.
|
|
21
24
|
|
|
22
25
|
- **PreToolUse** tracks which sessions actually modify files (Write, Edit,
|
|
23
26
|
MultiEdit, NotebookEdit, MCP tools). Read-only sessions (Grep, Read,
|
|
24
27
|
Bash-only) are never committed.
|
|
25
28
|
- **SessionStart / SessionEnd** chain sessions across `/clear` boundaries
|
|
26
29
|
so planning context survives into the eventual commit.
|
|
30
|
+
- **PreCompact** on Codex buffers visible transcript context before compaction.
|
|
27
31
|
- **Stop** fires after every turn. If the session modified files, it
|
|
28
32
|
commits with `git add -A`. If not, it buffers the transcript for pickup
|
|
29
33
|
by a later session that does commit.
|
|
@@ -40,6 +44,22 @@ brew install searlsco/tap/turbocommit
|
|
|
40
44
|
turbocommit install
|
|
41
45
|
```
|
|
42
46
|
|
|
47
|
+
Or install from npm:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g @searls/turbocommit
|
|
51
|
+
turbocommit install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With no flags, `turbocommit install` installs into existing harness config
|
|
55
|
+
directories only: `~/.claude` and/or `~/.codex`. To force one harness and create
|
|
56
|
+
its config directory if needed:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
turbocommit install --harness claude
|
|
60
|
+
turbocommit install --harness codex
|
|
61
|
+
```
|
|
62
|
+
|
|
43
63
|
Then enable it per-project:
|
|
44
64
|
|
|
45
65
|
```bash
|
|
@@ -57,20 +77,24 @@ brew uninstall turbocommit
|
|
|
57
77
|
|
|
58
78
|
## Where turbocommit goes in your hook chain
|
|
59
79
|
|
|
60
|
-
`turbocommit install` adds one hook group per event
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
`turbocommit install` adds one hook group per event for each installed harness.
|
|
81
|
+
The hooks are fire-and-forget: they always exit 0 and never block the agent.
|
|
82
|
+
|
|
83
|
+
If you rearrange your hooks manually, the turbocommit hooks can go anywhere.
|
|
84
|
+
Order doesn't matter since they never block.
|
|
63
85
|
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
Codex users should restart Codex and run `/hooks` after install to review and
|
|
87
|
+
trust the hook commands.
|
|
66
88
|
|
|
67
89
|
## Commands
|
|
68
90
|
|
|
69
91
|
| Command | Description |
|
|
70
92
|
|---------|-------------|
|
|
71
|
-
| `turbocommit install` | Add hooks to
|
|
93
|
+
| `turbocommit install` | Add hooks to existing Claude and/or Codex config dirs |
|
|
94
|
+
| `turbocommit install --harness claude` | Add hooks to `~/.claude/settings.json` |
|
|
95
|
+
| `turbocommit install --harness codex` | Add hooks to `~/.codex/hooks.json` |
|
|
72
96
|
| `turbocommit uninstall` | Remove them |
|
|
73
|
-
| `turbocommit init` | Create `.
|
|
97
|
+
| `turbocommit init` | Create `.turbocommit.json` in current repo |
|
|
74
98
|
| `turbocommit deinit` | Remove it |
|
|
75
99
|
| `turbocommit doctor` | Check hook and config health |
|
|
76
100
|
| `turbocommit monitor` | Tail the event log (start/success/skip/fail) |
|
|
@@ -80,8 +104,8 @@ anywhere — order doesn't matter since they never block.
|
|
|
80
104
|
## Continuity across workstreams
|
|
81
105
|
|
|
82
106
|
Every turbocommit adds a `Continuation of <SHA>` reference linking back to
|
|
83
|
-
the previous commit from the same
|
|
84
|
-
multiple sessions in parallel
|
|
107
|
+
the previous commit from the same agent session. This means you can run
|
|
108
|
+
multiple sessions in parallel, across different tabs and different tasks, all
|
|
85
109
|
committing to a single branch, and still trace each logical workstream
|
|
86
110
|
by following the SHA chain.
|
|
87
111
|
|
|
@@ -119,9 +143,24 @@ is traceable.
|
|
|
119
143
|
|
|
120
144
|
## Configuration
|
|
121
145
|
|
|
122
|
-
`turbocommit init` creates `.
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
`turbocommit init` creates `.turbocommit.json` with `{"enabled": true}`.
|
|
147
|
+
The legacy project path `.claude/turbocommit.json` is still read when the
|
|
148
|
+
neutral file is absent.
|
|
149
|
+
|
|
150
|
+
Global config follows the XDG Unix config standard:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
$XDG_CONFIG_HOME/turbocommit/config.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
When `XDG_CONFIG_HOME` is unset, turbocommit reads:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
~/.config/turbocommit/config.json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The legacy global path `~/.claude/turbocommit.json` is still read when the XDG
|
|
163
|
+
file is absent. Project config is deep-merged on top of global config.
|
|
125
164
|
|
|
126
165
|
Here's every property with its default:
|
|
127
166
|
|
|
@@ -152,7 +191,7 @@ Here's every property with its default:
|
|
|
152
191
|
|
|
153
192
|
// Prompt template sent to the title agent. Use {{transcript}} as the
|
|
154
193
|
// placeholder for the session transcript.
|
|
155
|
-
"prompt": "You have 10 seconds. Write a single-line git commit headline (max 72 chars) from this coding session transcript. Speed over perfection
|
|
194
|
+
"prompt": "You have 10 seconds. Write a single-line git commit headline (max 72 chars) from this coding session transcript. Speed over perfection: a rough title beats no title.\n\nRules:\n- Imperative mood (\"Add\", \"Fix\", \"Update\")\n- Specific about what changed\n- No trailing period\n- No conventional commit prefixes unless clearly a fix/feat\n\nTranscript:\n{{transcript}}\n\nRespond with ONLY the headline, nothing else. Do not deliberate."
|
|
156
195
|
},
|
|
157
196
|
|
|
158
197
|
"body": {
|
|
@@ -165,7 +204,7 @@ Here's every property with its default:
|
|
|
165
204
|
|
|
166
205
|
// Prompt template sent to the body agent. Use {{transcript}} as the
|
|
167
206
|
// placeholder for the session transcript.
|
|
168
|
-
"prompt": "Given this transcript of a coding session, write a concise git commit body.\n\nRules:\n- Summarize what was done and why\n- Be concise
|
|
207
|
+
"prompt": "Given this transcript of a coding session, write a concise git commit body.\n\nRules:\n- Summarize what was done and why\n- Be concise: a few sentences or bullet points\n- Focus on the \"why\" more than the \"what\"\n\nTranscript:\n{{transcript}}\n\nRespond with ONLY the commit body, nothing else.",
|
|
169
208
|
|
|
170
209
|
// Wrap prose lines at this width. Code blocks, tables, headers, and
|
|
171
210
|
// other structured content are preserved verbatim.
|
|
@@ -182,4 +221,4 @@ runtime without changing config.
|
|
|
182
221
|
|
|
183
222
|
- Node.js >= 18
|
|
184
223
|
- Git
|
|
185
|
-
- Claude Code
|
|
224
|
+
- Claude Code or Codex CLI
|
package/cli.js
CHANGED
|
@@ -3,26 +3,27 @@
|
|
|
3
3
|
const { readStdin } = require('./lib/io')
|
|
4
4
|
const { install, uninstall } = require('./lib/install')
|
|
5
5
|
const { init, deinit } = require('./lib/init')
|
|
6
|
-
const { run } = require('./lib/run')
|
|
6
|
+
const { run, runPreCompact } = require('./lib/run')
|
|
7
7
|
const { handleTrack } = require('./lib/track')
|
|
8
8
|
const { handleSessionStart, handleSessionEnd } = require('./lib/session')
|
|
9
9
|
const { doctor } = require('./lib/doctor')
|
|
10
10
|
const { monitor } = require('./lib/monitor')
|
|
11
11
|
const { gitRoot } = require('./lib/git')
|
|
12
|
+
const { parseHarnessArg, normalizeHookInput } = require('./lib/harness')
|
|
12
13
|
|
|
13
14
|
const VERSION = require('./package.json').version
|
|
14
15
|
|
|
15
16
|
const USAGE = `turbocommit v${VERSION}
|
|
16
|
-
Auto-commit after every
|
|
17
|
+
Auto-commit after every AI coding agent turn.
|
|
17
18
|
|
|
18
19
|
Commands:
|
|
19
|
-
install Add turbocommit hooks to
|
|
20
|
-
uninstall Remove turbocommit
|
|
21
|
-
init Create .
|
|
22
|
-
deinit Remove
|
|
20
|
+
install Add turbocommit hooks to installed harnesses
|
|
21
|
+
uninstall Remove turbocommit hooks
|
|
22
|
+
init Create .turbocommit.json in current git repo
|
|
23
|
+
deinit Remove turbocommit project config
|
|
23
24
|
doctor Check hook and config health
|
|
24
25
|
monitor Tail the event log (start/success/fail)
|
|
25
|
-
hook Hook entry points (called by
|
|
26
|
+
hook Hook entry points (called by harness hooks, not manually)
|
|
26
27
|
help Show this help text
|
|
27
28
|
--version, -v Show version
|
|
28
29
|
|
|
@@ -34,23 +35,30 @@ Usage:
|
|
|
34
35
|
`
|
|
35
36
|
|
|
36
37
|
function main (argv) {
|
|
37
|
-
const
|
|
38
|
+
const parsed = parseHarnessArg(argv)
|
|
39
|
+
if (!parsed.ok) {
|
|
40
|
+
console.error(parsed.error)
|
|
41
|
+
console.error('Run "turbocommit help" for usage.')
|
|
42
|
+
process.exitCode = 1
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const cmd = parsed.args[0]
|
|
38
46
|
|
|
39
47
|
switch (cmd) {
|
|
40
48
|
case 'install':
|
|
41
|
-
return cmdInstall()
|
|
49
|
+
return cmdInstall(parsed.harness)
|
|
42
50
|
case 'uninstall':
|
|
43
|
-
return cmdUninstall()
|
|
51
|
+
return cmdUninstall(parsed.harness)
|
|
44
52
|
case 'doctor':
|
|
45
|
-
return cmdDoctor()
|
|
53
|
+
return cmdDoctor(parsed.harness)
|
|
46
54
|
case 'monitor':
|
|
47
|
-
return cmdMonitor()
|
|
55
|
+
return cmdMonitor(parsed.harness)
|
|
48
56
|
case 'init':
|
|
49
57
|
return cmdInit()
|
|
50
58
|
case 'deinit':
|
|
51
59
|
return cmdDeinit()
|
|
52
60
|
case 'hook':
|
|
53
|
-
return cmdHook(
|
|
61
|
+
return cmdHook(parsed.args.slice(1), parsed.harness)
|
|
54
62
|
case 'run':
|
|
55
63
|
return cmdRunDeprecated()
|
|
56
64
|
case '--version':
|
|
@@ -71,33 +79,42 @@ function main (argv) {
|
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
function cmdInstall () {
|
|
75
|
-
const result = install()
|
|
76
|
-
if (result.
|
|
77
|
-
console.log('
|
|
78
|
-
console.log(` Settings: ${result.settingsPath}`)
|
|
82
|
+
function cmdInstall (harness) {
|
|
83
|
+
const result = install({ harness })
|
|
84
|
+
if (result.results.length === 0) {
|
|
85
|
+
console.log('No supported harness config dirs found.')
|
|
79
86
|
return
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
for (const r of result.results) {
|
|
89
|
+
console.log(r.alreadyInstalled ? `turbocommit already installed for ${r.harness}.` : `turbocommit installed for ${r.harness}.`)
|
|
90
|
+
console.log(` ${r.harness === 'codex' ? 'Hooks' : 'Settings'}: ${r.hooksPath || r.settingsPath}`)
|
|
91
|
+
}
|
|
83
92
|
console.log('')
|
|
84
93
|
console.log('════════════════════════════════════════════════════════════════════')
|
|
85
|
-
|
|
94
|
+
for (const r of result.results) {
|
|
95
|
+
if (r.harness === 'codex') {
|
|
96
|
+
console.log('IMPORTANT: Restart Codex and run /hooks to review turbocommit hooks.')
|
|
97
|
+
} else {
|
|
98
|
+
console.log('IMPORTANT: Restart Claude Code for the hooks to take effect.')
|
|
99
|
+
}
|
|
100
|
+
}
|
|
86
101
|
console.log('════════════════════════════════════════════════════════════════════')
|
|
87
102
|
console.log('')
|
|
88
103
|
console.log('Next steps:')
|
|
89
|
-
console.log(' 1. Restart
|
|
104
|
+
console.log(' 1. Restart the installed harness')
|
|
90
105
|
console.log(' 2. Run: turbocommit init in a repo to enable auto-commits')
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
function cmdUninstall () {
|
|
94
|
-
const result = uninstall()
|
|
95
|
-
if (
|
|
96
|
-
console.log('
|
|
108
|
+
function cmdUninstall (harness) {
|
|
109
|
+
const result = uninstall({ harness })
|
|
110
|
+
if (result.results.length === 0) {
|
|
111
|
+
console.log('No supported harness config dirs found.')
|
|
97
112
|
return
|
|
98
113
|
}
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
for (const r of result.results) {
|
|
115
|
+
console.log(r.wasInstalled ? `turbocommit uninstalled for ${r.harness}.` : `turbocommit was not installed for ${r.harness}.`)
|
|
116
|
+
console.log(` ${r.harness === 'codex' ? 'Hooks' : 'Settings'}: ${r.hooksPath || r.settingsPath}`)
|
|
117
|
+
}
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
function cmdInit () {
|
|
@@ -124,16 +141,17 @@ function cmdDeinit () {
|
|
|
124
141
|
return
|
|
125
142
|
}
|
|
126
143
|
if (!result.existed) {
|
|
127
|
-
console.log('turbocommit was not
|
|
144
|
+
console.log('turbocommit project config was not found.')
|
|
145
|
+
console.log('Global config may still enable turbocommit.')
|
|
128
146
|
return
|
|
129
147
|
}
|
|
130
148
|
console.log('turbocommit disabled.')
|
|
131
|
-
console.log(` Removed: ${
|
|
149
|
+
for (const p of result.paths) console.log(` Removed: ${p}`)
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
function cmdDoctor () {
|
|
152
|
+
function cmdDoctor (harness) {
|
|
135
153
|
const STATUS = { ok: ' ok', warn: 'warn', error: ' err', info: 'info' }
|
|
136
|
-
const result = doctor()
|
|
154
|
+
const result = doctor({ harness })
|
|
137
155
|
for (const check of result.checks) {
|
|
138
156
|
console.log(`[${STATUS[check.status] || check.status}] ${check.name}: ${check.message}`)
|
|
139
157
|
}
|
|
@@ -142,34 +160,39 @@ function cmdDoctor () {
|
|
|
142
160
|
}
|
|
143
161
|
}
|
|
144
162
|
|
|
145
|
-
function cmdMonitor () {
|
|
146
|
-
monitor()
|
|
163
|
+
function cmdMonitor (harness) {
|
|
164
|
+
if (!monitor(harness)) process.exitCode = 1
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
function cmdHook (argv) {
|
|
167
|
+
function cmdHook (argv, harness) {
|
|
150
168
|
const event = argv[0]
|
|
151
169
|
try {
|
|
152
170
|
const input = readStdin()
|
|
153
|
-
const
|
|
171
|
+
const hookInput = normalizeHookInput(input, event, harness)
|
|
172
|
+
if (!hookInput) return
|
|
173
|
+
const root = gitRoot(hookInput.cwd || process.cwd())
|
|
154
174
|
switch (event) {
|
|
155
175
|
case 'pre-tool-use':
|
|
156
|
-
handleTrack(
|
|
176
|
+
handleTrack(hookInput, root)
|
|
157
177
|
return
|
|
158
178
|
case 'session-start':
|
|
159
|
-
handleSessionStart(
|
|
179
|
+
handleSessionStart(hookInput, root)
|
|
160
180
|
return
|
|
161
181
|
case 'session-end':
|
|
162
|
-
handleSessionEnd(
|
|
182
|
+
handleSessionEnd(hookInput, root)
|
|
183
|
+
return
|
|
184
|
+
case 'pre-compact':
|
|
185
|
+
runPreCompact(hookInput)
|
|
163
186
|
return
|
|
164
187
|
case 'stop':
|
|
165
|
-
run(
|
|
188
|
+
run(hookInput)
|
|
166
189
|
break
|
|
167
190
|
default:
|
|
168
|
-
// Unknown hook event
|
|
191
|
+
// Unknown hook event: ignore silently (never fail)
|
|
169
192
|
break
|
|
170
193
|
}
|
|
171
194
|
} catch {
|
|
172
|
-
// Never fail
|
|
195
|
+
// Never fail: fire and forget
|
|
173
196
|
}
|
|
174
197
|
}
|
|
175
198
|
|
package/lib/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { tryRun } = require('./io')
|
|
2
2
|
|
|
3
3
|
const DEFAULT_COMMAND = 'claude -p --model haiku'
|
|
4
|
+
const DEFAULT_CODEX_COMMAND = 'codex exec --ephemeral -'
|
|
4
5
|
|
|
5
6
|
const DEFAULT_TITLE_PROMPT = `You have 10 seconds. Write a single-line git commit headline (max 72 chars) from this coding session transcript. Speed over perfection — a rough title beats no title.
|
|
6
7
|
|
|
@@ -49,8 +50,12 @@ function runAgent (root, command, prompt) {
|
|
|
49
50
|
return output || null
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function
|
|
53
|
-
|
|
53
|
+
function defaultCommand (harness) {
|
|
54
|
+
return harness === 'codex' ? DEFAULT_CODEX_COMMAND : DEFAULT_COMMAND
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runTitleAgent (root, titleCfg, transcript, harness) {
|
|
58
|
+
const command = titleCfg.command || defaultCommand(harness)
|
|
54
59
|
const template = titleCfg.prompt || DEFAULT_TITLE_PROMPT
|
|
55
60
|
const prompt = renderPrompt(template, transcript)
|
|
56
61
|
const result = runAgent(root, command, prompt)
|
|
@@ -59,8 +64,8 @@ function runTitleAgent (root, titleCfg, transcript) {
|
|
|
59
64
|
return result.split('\n')[0].slice(0, 72) || null
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
function runBodyAgent (root, bodyCfg, transcript) {
|
|
63
|
-
const command = bodyCfg.command ||
|
|
67
|
+
function runBodyAgent (root, bodyCfg, transcript, harness) {
|
|
68
|
+
const command = bodyCfg.command || defaultCommand(harness)
|
|
64
69
|
const template = bodyCfg.prompt || DEFAULT_BODY_PROMPT
|
|
65
70
|
const prompt = renderPrompt(template, transcript)
|
|
66
71
|
return runAgent(root, command, prompt)
|
|
@@ -68,8 +73,10 @@ function runBodyAgent (root, bodyCfg, transcript) {
|
|
|
68
73
|
|
|
69
74
|
module.exports = {
|
|
70
75
|
DEFAULT_COMMAND,
|
|
76
|
+
DEFAULT_CODEX_COMMAND,
|
|
71
77
|
DEFAULT_TITLE_PROMPT,
|
|
72
78
|
DEFAULT_BODY_PROMPT,
|
|
79
|
+
defaultCommand,
|
|
73
80
|
renderPrompt,
|
|
74
81
|
runAgent,
|
|
75
82
|
runTitleAgent,
|
package/lib/codex.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { codexHome } = require('./harness')
|
|
4
|
+
|
|
5
|
+
function resolveCodexTranscriptPath (hookInput) {
|
|
6
|
+
if (hookInput.transcriptPath) return hookInput.transcriptPath
|
|
7
|
+
if (!hookInput.sessionId) return null
|
|
8
|
+
|
|
9
|
+
const catalog = path.join(codexHome(), 'sessions', 'index', 'catalog.jsonl')
|
|
10
|
+
let lines
|
|
11
|
+
try {
|
|
12
|
+
lines = fs.readFileSync(catalog, 'utf8').split('\n')
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (!line.trim()) continue
|
|
19
|
+
let entry
|
|
20
|
+
try {
|
|
21
|
+
entry = JSON.parse(line)
|
|
22
|
+
} catch {
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
if (entry.session_id === hookInput.sessionId && entry.rollout_path) {
|
|
26
|
+
return path.isAbsolute(entry.rollout_path)
|
|
27
|
+
? entry.rollout_path
|
|
28
|
+
: path.join(codexHome(), entry.rollout_path)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { resolveCodexTranscriptPath }
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { loadJson, mergeConfig } = require('./io')
|
|
4
|
+
|
|
5
|
+
function xdgConfigPath () {
|
|
6
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
7
|
+
return path.join(base, 'turbocommit', 'config.json')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function legacyGlobalConfigPath () {
|
|
11
|
+
return path.join(os.homedir(), '.claude', 'turbocommit.json')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function globalConfigPath () {
|
|
15
|
+
return loadJson(xdgConfigPath()) ? xdgConfigPath() : legacyGlobalConfigPath()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function neutralProjectConfigPath (root) {
|
|
19
|
+
return path.join(root, '.turbocommit.json')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function legacyProjectConfigPath (root) {
|
|
23
|
+
return path.join(root, '.claude', 'turbocommit.json')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function projectConfigPath (root) {
|
|
27
|
+
return loadJson(neutralProjectConfigPath(root)) ? neutralProjectConfigPath(root) : legacyProjectConfigPath(root)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function activeConfig (root) {
|
|
31
|
+
const globalPath = globalConfigPath()
|
|
32
|
+
const projectPath = root ? projectConfigPath(root) : null
|
|
33
|
+
const globalCfg = loadJson(globalPath)
|
|
34
|
+
const projectCfg = projectPath ? loadJson(projectPath) : null
|
|
35
|
+
return {
|
|
36
|
+
globalPath,
|
|
37
|
+
projectPath,
|
|
38
|
+
globalCfg,
|
|
39
|
+
projectCfg,
|
|
40
|
+
config: mergeConfig(globalCfg || {}, projectCfg || {})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
xdgConfigPath,
|
|
46
|
+
legacyGlobalConfigPath,
|
|
47
|
+
globalConfigPath,
|
|
48
|
+
neutralProjectConfigPath,
|
|
49
|
+
legacyProjectConfigPath,
|
|
50
|
+
projectConfigPath,
|
|
51
|
+
activeConfig
|
|
52
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
const os = require('os')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { loadJson, mergeConfig } = require('./io')
|
|
4
|
-
const { hasTurbocommit, isFullyInstalled, getSettingsPath, HOOK_DEFS } = require('./install')
|
|
4
|
+
const { hasTurbocommit, isFullyInstalled, isCodexFullyInstalled, getSettingsPath, getCodexHooksPath, HOOK_DEFS, CODEX_HOOK_DEFS } = require('./install')
|
|
5
5
|
const { gitRoot } = require('./git')
|
|
6
6
|
const { configPath } = require('./init')
|
|
7
|
+
const { activeConfig, xdgConfigPath, legacyGlobalConfigPath, neutralProjectConfigPath, legacyProjectConfigPath } = require('./config')
|
|
8
|
+
const { selectedHarnesses } = require('./harness')
|
|
7
9
|
|
|
8
10
|
function globalConfigPath () {
|
|
9
11
|
return path.join(os.homedir(), '.claude', 'turbocommit.json')
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
function doctor (
|
|
14
|
+
function doctor (settingsPathOrOptions, cwd) {
|
|
15
|
+
if (typeof settingsPathOrOptions === 'string' || settingsPathOrOptions == null) {
|
|
16
|
+
return doctorLegacy(settingsPathOrOptions, cwd)
|
|
17
|
+
}
|
|
18
|
+
const options = settingsPathOrOptions || {}
|
|
19
|
+
cwd = options.cwd || process.cwd()
|
|
20
|
+
const checks = []
|
|
21
|
+
const harnesses = selectedHarnesses(options.harness)
|
|
22
|
+
|
|
23
|
+
for (const harness of harnesses) {
|
|
24
|
+
if (harness === 'codex') {
|
|
25
|
+
checkCodex(checks, options.codexHooksPath || getCodexHooksPath())
|
|
26
|
+
} else {
|
|
27
|
+
checkClaude(checks, options.claudeSettingsPath || getSettingsPath())
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (harnesses.length === 0) {
|
|
31
|
+
checks.push({ name: 'Harnesses', status: 'info', message: 'No supported harness config dirs found' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
checkConfig(checks, cwd)
|
|
35
|
+
|
|
36
|
+
const ok = checks.every(c => c.status !== 'error')
|
|
37
|
+
return { ok, checks }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function doctorLegacy (settingsPath, cwd) {
|
|
13
41
|
settingsPath = settingsPath || getSettingsPath()
|
|
14
42
|
cwd = cwd || process.cwd()
|
|
15
43
|
const checks = []
|
|
16
|
-
|
|
17
|
-
// 1. Global settings exist
|
|
18
44
|
const settings = loadJson(settingsPath)
|
|
19
45
|
if (!settings) {
|
|
20
46
|
checks.push({ name: 'Global settings', status: 'error', message: `Not found: ${settingsPath}` })
|
|
@@ -32,7 +58,7 @@ function doctor (settingsPath, cwd) {
|
|
|
32
58
|
}
|
|
33
59
|
checks.push({ name: 'Hooks installed', status: 'ok', message: `All ${Object.keys(HOOK_DEFS).length} hooks installed` })
|
|
34
60
|
|
|
35
|
-
// 3. Stop group isolation
|
|
61
|
+
// 3. Stop group isolation: turbocommit must be the sole hook in the last group
|
|
36
62
|
const stopGroups = (settings.hooks && settings.hooks.Stop) || []
|
|
37
63
|
const tcGroupIndex = findTurbocommitGroup(stopGroups)
|
|
38
64
|
if (tcGroupIndex >= 0) {
|
|
@@ -41,7 +67,7 @@ function doctor (settingsPath, cwd) {
|
|
|
41
67
|
const isSoleHook = tcGroup.hooks.length === 1
|
|
42
68
|
|
|
43
69
|
if (!isSoleHook) {
|
|
44
|
-
checks.push({ name: 'Group isolation', status: 'warn', message: 'turbocommit shares a Stop group with other hooks
|
|
70
|
+
checks.push({ name: 'Group isolation', status: 'warn', message: 'turbocommit shares a Stop group with other hooks; will commit even if another hook blocks' })
|
|
45
71
|
} else if (!isLastGroup) {
|
|
46
72
|
checks.push({ name: 'Group isolation', status: 'warn', message: 'Another group runs after turbocommit in Stop' })
|
|
47
73
|
} else {
|
|
@@ -61,21 +87,23 @@ function doctor (settingsPath, cwd) {
|
|
|
61
87
|
// 5. Local config exists
|
|
62
88
|
const root = gitRoot(cwd)
|
|
63
89
|
if (!root) {
|
|
64
|
-
checks.push({ name: 'Local config', status: 'info', message: 'Not in a git repo
|
|
90
|
+
checks.push({ name: 'Local config', status: 'info', message: 'Not in a git repo; skipping local checks' })
|
|
65
91
|
} else {
|
|
66
|
-
const
|
|
92
|
+
const neutralPath = configPath(root)
|
|
93
|
+
const legacyPath = legacyProjectConfigPath(root)
|
|
94
|
+
const localPath = loadJson(neutralPath) ? neutralPath : legacyPath
|
|
67
95
|
const localConfig = loadJson(localPath)
|
|
68
96
|
if (!localConfig) {
|
|
69
97
|
if (globalCfg) {
|
|
70
98
|
checks.push({ name: 'Local config', status: 'info', message: 'Not found (using global config)' })
|
|
71
99
|
} else {
|
|
72
|
-
checks.push({ name: 'Local config', status: 'warn', message: `Not found: ${
|
|
100
|
+
checks.push({ name: 'Local config', status: 'warn', message: `Not found: ${neutralPath}. Run: turbocommit init` })
|
|
73
101
|
}
|
|
74
102
|
} else {
|
|
75
103
|
checks.push({ name: 'Local config', status: 'ok', message: localPath })
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
// 6. Enabled
|
|
106
|
+
// 6. Enabled: evaluate merged config
|
|
79
107
|
const merged = mergeConfig(globalCfg || {}, localConfig || {})
|
|
80
108
|
if (merged.enabled !== true) {
|
|
81
109
|
checks.push({ name: 'Enabled', status: 'warn', message: 'enabled is not true in merged config' })
|
|
@@ -88,6 +116,70 @@ function doctor (settingsPath, cwd) {
|
|
|
88
116
|
return { ok, checks }
|
|
89
117
|
}
|
|
90
118
|
|
|
119
|
+
function checkClaude (checks, settingsPath) {
|
|
120
|
+
const settings = loadJson(settingsPath)
|
|
121
|
+
if (!settings) {
|
|
122
|
+
checks.push({ name: 'Claude settings', status: 'error', message: `Not found: ${settingsPath}` })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
checks.push({ name: 'Claude settings', status: 'ok', message: settingsPath })
|
|
126
|
+
|
|
127
|
+
if (!isFullyInstalled(settings)) {
|
|
128
|
+
const missing = Object.keys(HOOK_DEFS).filter(event =>
|
|
129
|
+
!hasTurbocommit((settings.hooks && settings.hooks[event]) || [])
|
|
130
|
+
)
|
|
131
|
+
checks.push({ name: 'Claude hooks', status: 'error', message: `Missing hooks: ${missing.join(', ')}` })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
checks.push({ name: 'Claude hooks', status: 'ok', message: `All ${Object.keys(HOOK_DEFS).length} hooks installed` })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function checkCodex (checks, hooksPath) {
|
|
138
|
+
const hooks = loadJson(hooksPath)
|
|
139
|
+
if (!hooks) {
|
|
140
|
+
checks.push({ name: 'Codex hooks', status: 'error', message: `Not found: ${hooksPath}` })
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (!isCodexFullyInstalled(hooks)) {
|
|
144
|
+
const missing = Object.keys(CODEX_HOOK_DEFS).filter(event =>
|
|
145
|
+
!hasTurbocommit((hooks.hooks && hooks.hooks[event]) || [])
|
|
146
|
+
)
|
|
147
|
+
checks.push({ name: 'Codex hooks', status: 'error', message: `Missing hooks: ${missing.join(', ')}` })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
checks.push({ name: 'Codex hooks', status: 'ok', message: `All ${Object.keys(CODEX_HOOK_DEFS).length} hooks installed` })
|
|
151
|
+
checks.push({ name: 'Codex hook trust', status: 'warn', message: 'Unknown. Run /hooks in Codex to verify trust.' })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function checkConfig (checks, cwd) {
|
|
155
|
+
const root = gitRoot(cwd)
|
|
156
|
+
const xdgPath = xdgConfigPath()
|
|
157
|
+
const legacyGlobal = legacyGlobalConfigPath()
|
|
158
|
+
const globalPath = loadJson(xdgPath) ? xdgPath : legacyGlobal
|
|
159
|
+
const globalCfg = loadJson(globalPath)
|
|
160
|
+
checks.push(globalCfg
|
|
161
|
+
? { name: 'Global config', status: 'ok', message: globalPath }
|
|
162
|
+
: { name: 'Global config', status: 'info', message: 'Not found (optional)' })
|
|
163
|
+
|
|
164
|
+
if (!root) {
|
|
165
|
+
checks.push({ name: 'Project config', status: 'info', message: 'Not in a git repo' })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const neutralPath = neutralProjectConfigPath(root)
|
|
170
|
+
const legacyPath = legacyProjectConfigPath(root)
|
|
171
|
+
const projectPath = loadJson(neutralPath) ? neutralPath : legacyPath
|
|
172
|
+
const projectCfg = loadJson(projectPath)
|
|
173
|
+
checks.push(projectCfg
|
|
174
|
+
? { name: 'Project config', status: 'ok', message: projectPath }
|
|
175
|
+
: { name: 'Project config', status: 'info', message: `Not found: ${configPath(root)}` })
|
|
176
|
+
|
|
177
|
+
const { config } = activeConfig(root)
|
|
178
|
+
checks.push(config.enabled === true
|
|
179
|
+
? { name: 'Enabled', status: 'ok', message: 'Enabled' }
|
|
180
|
+
: { name: 'Enabled', status: 'warn', message: 'enabled is not true in active config' })
|
|
181
|
+
}
|
|
182
|
+
|
|
91
183
|
function findTurbocommitGroup (groups) {
|
|
92
184
|
for (let i = 0; i < groups.length; i++) {
|
|
93
185
|
const hooks = (groups[i] && groups[i].hooks) || []
|