ronds_ai 0.1.10 → 0.1.12
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 +22 -1
- package/bin/ronds_ai.js +59 -2
- package/lib/check_record.js +232 -106
- package/lib/code_record.js +74 -21
- package/lib/doctor.js +7 -10
- package/lib/git.js +22 -0
- package/lib/hooks_deploy.js +72 -1
- package/lib/skills_prompt.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,9 +49,29 @@ npx ronds_ai@latest check record
|
|
|
49
49
|
- 当前 Node 版本
|
|
50
50
|
- Node 是否满足 `>=16`
|
|
51
51
|
- 当前目录下读取到的 `git user.email`
|
|
52
|
-
- 当前 `
|
|
52
|
+
- 当前 `MCP_TRACKER_WORKER_ID` 的来源和取值
|
|
53
53
|
- `~/.profile` 路径
|
|
54
54
|
|
|
55
|
+
输出 JSON 后,命令还会继续在终端里显示当前 `MCP_TRACKER_WORKER_ID`,并询问是否要修改。
|
|
56
|
+
如果选择修改:
|
|
57
|
+
|
|
58
|
+
- Windows:写入用户级持久环境变量,新的终端可直接读取
|
|
59
|
+
- Linux:写入 `~/.profile`,新的 shell 可读取
|
|
60
|
+
|
|
61
|
+
如果不想进入交互流程,可加上:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ronds_ai check record --no-prompt
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
或:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx ronds_ai@latest check record --no-prompt
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
交互式修改后,命令会再次显示更新后的 `MCP_TRACKER_WORKER_ID`。
|
|
74
|
+
|
|
55
75
|
输出是一个 JSON,例如:
|
|
56
76
|
|
|
57
77
|
```json
|
|
@@ -201,6 +221,7 @@ npx ronds_ai@latest hooks deploy
|
|
|
201
221
|
- 在 `.cursor/hooks.json` 中确保存在 `npx ronds_ai@latest record cursor`
|
|
202
222
|
- 在 `.claude/settings.json` 中确保存在 `npx ronds_ai@latest record claude`
|
|
203
223
|
- 在 `.codex/hooks.json` 中确保存在 `npx ronds_ai@latest record codex`(`UserPromptSubmit` + `Stop` 两个 hook)
|
|
224
|
+
- 在 `.codex/config.toml` 中确保存在 `[features] codex_hooks = true`,启用 Codex hooks
|
|
204
225
|
- 清理旧版 hook 脚本文件
|
|
205
226
|
- 如果存在 `.claude/settings.local.json`,会移除其中由本工具管理的旧 hook,避免重复触发
|
|
206
227
|
- 所有工具的失败事件统一写入 `~/.ronds_ai/failed-events`
|
package/bin/ronds_ai.js
CHANGED
|
@@ -1,9 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { runCodeRecord, saveCliError } = require('../lib/code_record');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
ENVIRONMENT_VARIABLE_NAME,
|
|
6
|
+
getRefreshNotice,
|
|
7
|
+
getPersistentWorkerHint,
|
|
8
|
+
isPersistentWorkerUpdateSupported,
|
|
9
|
+
runCheckRecord,
|
|
10
|
+
savePersistentWorkerId,
|
|
11
|
+
} = require('../lib/check_record');
|
|
5
12
|
const { runDoctor } = require('../lib/doctor');
|
|
6
13
|
const { deployHooks } = require('../lib/hooks_deploy');
|
|
14
|
+
const { promptForText, promptYesNo } = require('../lib/skills_prompt');
|
|
15
|
+
|
|
16
|
+
function writeWorkerSummary(result) {
|
|
17
|
+
process.stderr.write(`${ENVIRONMENT_VARIABLE_NAME}: ${result.workerId.value || '(空)'}\n`);
|
|
18
|
+
process.stderr.write(`来源: ${result.workerId.source || '未设置'}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function maybeUpdateWorkerId() {
|
|
22
|
+
const initialResult = runCheckRecord(process.cwd());
|
|
23
|
+
writeWorkerSummary(initialResult);
|
|
24
|
+
process.stderr.write(`${getPersistentWorkerHint()}\n`);
|
|
25
|
+
|
|
26
|
+
if (!isPersistentWorkerUpdateSupported()) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const shouldModify = await promptYesNo(`是否要修改 ${ENVIRONMENT_VARIABLE_NAME}?`);
|
|
31
|
+
if (!shouldModify) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nextValue = await promptForText(`请输入新的 ${ENVIRONMENT_VARIABLE_NAME}:`, initialResult.workerId.value);
|
|
36
|
+
if (!nextValue) {
|
|
37
|
+
process.stderr.write(`输入为空,已跳过更新 ${ENVIRONMENT_VARIABLE_NAME}。\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const saveResult = savePersistentWorkerId(nextValue);
|
|
42
|
+
process.stderr.write(`${ENVIRONMENT_VARIABLE_NAME} 已保存到 ${saveResult.location}。\n`);
|
|
43
|
+
|
|
44
|
+
const refreshNotice = getRefreshNotice();
|
|
45
|
+
if (refreshNotice) {
|
|
46
|
+
process.stderr.write(`${refreshNotice}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeWorkerSummary(runCheckRecord(process.cwd()));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldPromptWorkerUpdate(args) {
|
|
53
|
+
return !args.includes('--no-prompt');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stripCheckFlags(args) {
|
|
57
|
+
return args.filter((value) => value !== '--no-prompt');
|
|
58
|
+
}
|
|
7
59
|
|
|
8
60
|
const SUPPORTED_SOURCES = new Set(['claude', 'cursor', 'codex']);
|
|
9
61
|
|
|
@@ -47,7 +99,8 @@ async function runHooksCommand(args) {
|
|
|
47
99
|
}
|
|
48
100
|
|
|
49
101
|
async function runCheckCommand(args) {
|
|
50
|
-
const
|
|
102
|
+
const filteredArgs = stripCheckFlags(args);
|
|
103
|
+
const [target] = filteredArgs;
|
|
51
104
|
|
|
52
105
|
if (target !== 'record') {
|
|
53
106
|
throw new Error('Unsupported check command');
|
|
@@ -56,6 +109,10 @@ async function runCheckCommand(args) {
|
|
|
56
109
|
const result = runCheckRecord(process.cwd());
|
|
57
110
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
58
111
|
|
|
112
|
+
if (shouldPromptWorkerUpdate(args)) {
|
|
113
|
+
await maybeUpdateWorkerId();
|
|
114
|
+
}
|
|
115
|
+
|
|
59
116
|
if (!result.ok) {
|
|
60
117
|
process.exitCode = 1;
|
|
61
118
|
}
|
package/lib/check_record.js
CHANGED
|
@@ -1,106 +1,232 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { execFileSync } = require('child_process');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { runGit } = require('./git');
|
|
6
|
+
|
|
7
|
+
const ENVIRONMENT_VARIABLE_NAME = process.env.ENVIRONMENT_VARIABLE_NAME || 'MCP_TRACKER_WORKER_ID';
|
|
8
|
+
const MINIMUM_NODE_MAJOR = 16;
|
|
9
|
+
const SUPPORTED_PERSISTENT_PLATFORMS = new Set(['win32', 'linux']);
|
|
10
|
+
|
|
11
|
+
function readGitUserEmail(baseDir) {
|
|
12
|
+
return runGit(baseDir, ['config', 'user.email'], false)
|
|
13
|
+
.replace(/^["'“”]+/, '')
|
|
14
|
+
.replace(/["'“”]+$/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getProfileFilePath() {
|
|
18
|
+
return path.join(os.homedir(), '.profile');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stripShellQuotes(value) {
|
|
22
|
+
return String(value || '')
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/^['"]/, '')
|
|
25
|
+
.replace(/['"]$/, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readWorkerIdFromProfile() {
|
|
29
|
+
const profileFile = getProfileFilePath();
|
|
30
|
+
if (!fs.existsSync(profileFile)) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lines = fs.readFileSync(profileFile, 'utf8').split('\n');
|
|
35
|
+
const prefix = `export ${ENVIRONMENT_VARIABLE_NAME}=`;
|
|
36
|
+
|
|
37
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
38
|
+
const line = lines[index].trim();
|
|
39
|
+
if (!line.startsWith(prefix)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return stripShellQuotes(line.slice(prefix.length));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readWorkerIdFromWindowsUserEnv() {
|
|
50
|
+
try {
|
|
51
|
+
return execFileSync('powershell', ['-NoProfile', '-Command', `[Environment]::GetEnvironmentVariable('${ENVIRONMENT_VARIABLE_NAME}', 'User')`], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
54
|
+
}).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCurrentWorkerId() {
|
|
61
|
+
const envValue = String(process.env[ENVIRONMENT_VARIABLE_NAME] || '').trim();
|
|
62
|
+
if (envValue) {
|
|
63
|
+
return {
|
|
64
|
+
name: ENVIRONMENT_VARIABLE_NAME,
|
|
65
|
+
value: envValue,
|
|
66
|
+
source: 'env',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (process.platform === 'win32') {
|
|
71
|
+
const userEnvValue = readWorkerIdFromWindowsUserEnv();
|
|
72
|
+
if (userEnvValue) {
|
|
73
|
+
return {
|
|
74
|
+
name: ENVIRONMENT_VARIABLE_NAME,
|
|
75
|
+
value: userEnvValue,
|
|
76
|
+
source: 'user_env',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const profileValue = readWorkerIdFromProfile();
|
|
82
|
+
if (profileValue) {
|
|
83
|
+
return {
|
|
84
|
+
name: ENVIRONMENT_VARIABLE_NAME,
|
|
85
|
+
value: profileValue,
|
|
86
|
+
source: 'profile',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name: ENVIRONMENT_VARIABLE_NAME,
|
|
92
|
+
value: '',
|
|
93
|
+
source: '',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function escapeProfileValue(value) {
|
|
98
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function upsertProfileWorkerId(value) {
|
|
102
|
+
const profileFile = getProfileFilePath();
|
|
103
|
+
const nextLine = `export ${ENVIRONMENT_VARIABLE_NAME}="${escapeProfileValue(value)}"`;
|
|
104
|
+
const currentContent = fs.existsSync(profileFile)
|
|
105
|
+
? fs.readFileSync(profileFile, 'utf8')
|
|
106
|
+
: '';
|
|
107
|
+
const lines = currentContent ? currentContent.split('\n') : [];
|
|
108
|
+
const prefix = `export ${ENVIRONMENT_VARIABLE_NAME}=`;
|
|
109
|
+
let updated = false;
|
|
110
|
+
|
|
111
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
112
|
+
if (!lines[index].trim().startsWith(prefix)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lines[index] = nextLine;
|
|
117
|
+
updated = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nextLines = updated ? lines : lines.concat(nextLine);
|
|
122
|
+
const nextContent = `${nextLines.join('\n').replace(/\n*$/, '\n')}`;
|
|
123
|
+
if (nextContent !== currentContent) {
|
|
124
|
+
fs.writeFileSync(profileFile, nextContent, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
location: profileFile,
|
|
129
|
+
source: 'profile',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setWindowsUserWorkerId(value) {
|
|
134
|
+
execFileSync('setx', [ENVIRONMENT_VARIABLE_NAME, String(value)], {
|
|
135
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
location: 'Windows user environment',
|
|
140
|
+
source: 'user_env',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setPersistentWorkerId(value) {
|
|
145
|
+
const normalizedValue = String(value || '').trim();
|
|
146
|
+
if (!normalizedValue) {
|
|
147
|
+
throw new Error(`${ENVIRONMENT_VARIABLE_NAME} cannot be empty`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!SUPPORTED_PERSISTENT_PLATFORMS.has(process.platform)) {
|
|
151
|
+
throw new Error(`Persistent ${ENVIRONMENT_VARIABLE_NAME} updates are not supported on ${process.platform}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (process.platform === 'win32') {
|
|
155
|
+
return setWindowsUserWorkerId(normalizedValue);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return upsertProfileWorkerId(normalizedValue);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function savePersistentWorkerId(value) {
|
|
162
|
+
const normalizedValue = String(value || '').trim();
|
|
163
|
+
const result = setPersistentWorkerId(normalizedValue);
|
|
164
|
+
process.env[ENVIRONMENT_VARIABLE_NAME] = normalizedValue;
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isPersistentWorkerUpdateSupported() {
|
|
169
|
+
return SUPPORTED_PERSISTENT_PLATFORMS.has(process.platform);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getPersistentWorkerHint() {
|
|
173
|
+
if (process.platform === 'win32') {
|
|
174
|
+
return '会写入 Windows 用户级环境变量,新开的终端可以读取到。';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (process.platform === 'linux') {
|
|
178
|
+
return `会写入 ${getProfileFilePath()},新开的 shell 可以读取到。`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `当前平台暂不支持持久化修改 ${ENVIRONMENT_VARIABLE_NAME}。`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getRefreshNotice() {
|
|
185
|
+
if (process.platform === 'win32') {
|
|
186
|
+
return '请打开新的终端,让 shell 继承更新后的用户环境变量。';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (process.platform === 'linux') {
|
|
190
|
+
return `请打开新的 shell,或执行 source ${getProfileFilePath()} 以在当前 shell 中加载新变量。`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseNodeMajor(version) {
|
|
197
|
+
const normalized = String(version || '').trim();
|
|
198
|
+
const match = normalized.match(/^v?(\d+)/);
|
|
199
|
+
return match ? Number(match[1]) : 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function runCheckRecord(targetDir = process.cwd()) {
|
|
203
|
+
const baseDir = path.resolve(targetDir);
|
|
204
|
+
const nodeVersion = process.version;
|
|
205
|
+
const nodeMajor = parseNodeMajor(nodeVersion);
|
|
206
|
+
const gitUserEmail = readGitUserEmail(baseDir);
|
|
207
|
+
const worker = getCurrentWorkerId();
|
|
208
|
+
const nodeSatisfied = nodeMajor >= MINIMUM_NODE_MAJOR;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
ok: nodeSatisfied,
|
|
212
|
+
targetDir: baseDir,
|
|
213
|
+
node: {
|
|
214
|
+
version: nodeVersion,
|
|
215
|
+
requirement: `>=${MINIMUM_NODE_MAJOR}`,
|
|
216
|
+
satisfied: nodeSatisfied,
|
|
217
|
+
},
|
|
218
|
+
gitUserEmail,
|
|
219
|
+
workerId: worker,
|
|
220
|
+
profileFile: getProfileFilePath(),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
ENVIRONMENT_VARIABLE_NAME,
|
|
226
|
+
getCurrentWorkerId,
|
|
227
|
+
getPersistentWorkerHint,
|
|
228
|
+
getRefreshNotice,
|
|
229
|
+
isPersistentWorkerUpdateSupported,
|
|
230
|
+
runCheckRecord,
|
|
231
|
+
savePersistentWorkerId,
|
|
232
|
+
};
|
package/lib/code_record.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const { execFileSync } = require('child_process');
|
|
7
|
+
const { runGit } = require('./git');
|
|
7
8
|
const { createHash, randomUUID } = require('crypto');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 10000;
|
|
@@ -14,6 +15,7 @@ const CLAUDE_SUPPORTED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
|
14
15
|
const CURSOR_SUPPORTED_HOOK_EVENT = 'afterFileEdit';
|
|
15
16
|
const CODEX_SUPPORTED_HOOK_EVENTS = new Set(['UserPromptSubmit', 'Stop']);
|
|
16
17
|
const CODEX_SNAPSHOT_DIR = path.join(os.homedir(), '.ronds_ai', 'codex-snapshots');
|
|
18
|
+
const CODEX_SNAPSHOT_TTL_SEC = 86400;
|
|
17
19
|
const GIT_STATUS_ARGS = ['status', '--porcelain=v1', '--untracked-files=all'];
|
|
18
20
|
const CODEX_STOP_OUTPUT = {
|
|
19
21
|
continue: false,
|
|
@@ -135,23 +137,6 @@ function findGitRepoRoot(startDir) {
|
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
function runGit(repoRoot, args, required = true) {
|
|
139
|
-
try {
|
|
140
|
-
return execFileSync('git', args, {
|
|
141
|
-
cwd: repoRoot,
|
|
142
|
-
encoding: 'utf8',
|
|
143
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
144
|
-
}).trim();
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (!required) {
|
|
147
|
-
return '';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const stderr = error && error.stderr ? String(error.stderr).trim() : String(error);
|
|
151
|
-
throw new Error(`git ${args.join(' ')} failed: ${stderr}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
140
|
function extractRepoNameFromRemoteUrl(remoteUrl) {
|
|
156
141
|
const normalizedUrl = String(remoteUrl || '').trim().replace(/[\\/]+$/, '');
|
|
157
142
|
if (!normalizedUrl) {
|
|
@@ -509,9 +494,9 @@ function readFileState(filePath) {
|
|
|
509
494
|
}
|
|
510
495
|
}
|
|
511
496
|
|
|
512
|
-
function
|
|
497
|
+
function readGitRefFileState(repoRoot, ref, relativePath) {
|
|
513
498
|
try {
|
|
514
|
-
const buffer = execFileSync('git', ['show',
|
|
499
|
+
const buffer = execFileSync('git', ['show', `${ref}:${relativePath}`], {
|
|
515
500
|
cwd: repoRoot,
|
|
516
501
|
encoding: null,
|
|
517
502
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -539,6 +524,27 @@ function readHeadFileState(repoRoot, relativePath) {
|
|
|
539
524
|
}
|
|
540
525
|
}
|
|
541
526
|
|
|
527
|
+
function readHeadFileState(repoRoot, relativePath) {
|
|
528
|
+
return readGitRefFileState(repoRoot, 'HEAD', relativePath);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function readCurrentHead(repoRoot) {
|
|
532
|
+
return runGit(repoRoot, ['rev-parse', 'HEAD'], false);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function parseGitDiffNameOnly(repoRoot, fromRef, toRef) {
|
|
536
|
+
const normalizedFromRef = String(fromRef || '').trim();
|
|
537
|
+
const normalizedToRef = String(toRef || '').trim();
|
|
538
|
+
if (!normalizedFromRef || !normalizedToRef || normalizedFromRef === normalizedToRef) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const output = runGit(repoRoot, ['diff', '--name-only', `${normalizedFromRef}..${normalizedToRef}`], false);
|
|
543
|
+
return output.split('\n')
|
|
544
|
+
.map((line) => line.trim().replace(/\\/g, '/'))
|
|
545
|
+
.filter(Boolean);
|
|
546
|
+
}
|
|
547
|
+
|
|
542
548
|
function statesEqual(beforeState, afterState) {
|
|
543
549
|
return beforeState.exists === afterState.exists
|
|
544
550
|
&& beforeState.isBinary === afterState.isBinary
|
|
@@ -569,6 +575,41 @@ function buildCodexChange(beforeState, afterState) {
|
|
|
569
575
|
};
|
|
570
576
|
}
|
|
571
577
|
|
|
578
|
+
function cleanupStaleSnapshots() {
|
|
579
|
+
let entries;
|
|
580
|
+
try {
|
|
581
|
+
if (!fs.existsSync(CODEX_SNAPSHOT_DIR)) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
entries = fs.readdirSync(CODEX_SNAPSHOT_DIR);
|
|
585
|
+
} catch {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const nowSec = Date.now() / 1000;
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
if (!entry.endsWith('.json')) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const filePath = path.join(CODEX_SNAPSHOT_DIR, entry);
|
|
596
|
+
try {
|
|
597
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
598
|
+
const snapshot = JSON.parse(raw);
|
|
599
|
+
const createdAt = String(snapshot.created_at || '');
|
|
600
|
+
const createdSec = new Date(createdAt).getTime() / 1000;
|
|
601
|
+
if (isNaN(createdSec)) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (nowSec - createdSec > CODEX_SNAPSHOT_TTL_SEC) {
|
|
605
|
+
fs.rmSync(filePath, { force: true });
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
572
613
|
function handleCodexUserPromptSubmit(payload) {
|
|
573
614
|
const cwd = payload.cwd || process.cwd();
|
|
574
615
|
const repoRoot = findGitRepoRoot(cwd);
|
|
@@ -577,6 +618,7 @@ function handleCodexUserPromptSubmit(payload) {
|
|
|
577
618
|
return [];
|
|
578
619
|
}
|
|
579
620
|
|
|
621
|
+
const head = readCurrentHead(repoRoot);
|
|
580
622
|
const baseline = {};
|
|
581
623
|
for (const entry of parseGitStatus(repoRoot)) {
|
|
582
624
|
const absolutePath = path.join(repoRoot, entry.path);
|
|
@@ -585,12 +627,14 @@ function handleCodexUserPromptSubmit(payload) {
|
|
|
585
627
|
: readFileState(absolutePath);
|
|
586
628
|
}
|
|
587
629
|
|
|
630
|
+
cleanupStaleSnapshots();
|
|
588
631
|
fs.mkdirSync(CODEX_SNAPSHOT_DIR, { recursive: true });
|
|
589
632
|
fs.writeFileSync(snapshotPath, JSON.stringify({
|
|
590
633
|
version: 1,
|
|
591
634
|
session_id: payload.session_id || payload.sessionId || '',
|
|
592
635
|
turn_id: payload.turn_id || payload.turnId || '',
|
|
593
636
|
repo_root: repoRoot,
|
|
637
|
+
head,
|
|
594
638
|
created_at: new Date().toISOString(),
|
|
595
639
|
baseline,
|
|
596
640
|
}), 'utf8');
|
|
@@ -620,9 +664,15 @@ function buildCodexStopEvents(payload, source) {
|
|
|
620
664
|
: {};
|
|
621
665
|
const currentEntries = parseGitStatus(repoRoot);
|
|
622
666
|
const currentPaths = new Set(currentEntries.map((entry) => entry.path));
|
|
667
|
+
const baseRef = snapshot && typeof snapshot.head === 'string' && snapshot.head.trim()
|
|
668
|
+
? snapshot.head.trim()
|
|
669
|
+
: 'HEAD';
|
|
670
|
+
const currentHead = readCurrentHead(repoRoot) || 'HEAD';
|
|
671
|
+
const committedPaths = new Set(parseGitDiffNameOnly(repoRoot, baseRef, currentHead));
|
|
623
672
|
const allPaths = new Set([
|
|
624
673
|
...Object.keys(baseline),
|
|
625
674
|
...currentPaths,
|
|
675
|
+
...committedPaths,
|
|
626
676
|
]);
|
|
627
677
|
const workerId = resolveWorkerId(repoRoot);
|
|
628
678
|
const git = resolveGitMetadata(repoRoot);
|
|
@@ -630,8 +680,11 @@ function buildCodexStopEvents(payload, source) {
|
|
|
630
680
|
|
|
631
681
|
for (const repoRelativePath of allPaths) {
|
|
632
682
|
const absolutePath = path.join(repoRoot, repoRelativePath);
|
|
633
|
-
const beforeState = baseline[repoRelativePath] ||
|
|
634
|
-
|
|
683
|
+
const beforeState = baseline[repoRelativePath] || readGitRefFileState(repoRoot, baseRef, repoRelativePath);
|
|
684
|
+
let afterState = readFileState(absolutePath);
|
|
685
|
+
if (!afterState.exists && committedPaths.has(repoRelativePath)) {
|
|
686
|
+
afterState = readGitRefFileState(repoRoot, currentHead, repoRelativePath);
|
|
687
|
+
}
|
|
635
688
|
|
|
636
689
|
if (statesEqual(beforeState, afterState)) {
|
|
637
690
|
continue;
|
package/lib/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const {
|
|
4
|
+
const { runGit } = require('./git');
|
|
5
5
|
|
|
6
6
|
const SUPPORTED_TOOLS = new Set(['claude', 'codex', 'cursor']);
|
|
7
7
|
|
|
@@ -74,15 +74,11 @@ function getConfigChecks(tool, baseDir) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function readGitUserEmail(baseDir) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}).trim();
|
|
83
|
-
} catch {
|
|
84
|
-
return '';
|
|
85
|
-
}
|
|
77
|
+
return runGit(baseDir, ['config', 'user.email'], false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readGitRemoteUrl(baseDir) {
|
|
81
|
+
return runGit(baseDir, ['remote', 'get-url', 'origin'], false);
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
function runDoctor(tool, targetDir = process.cwd()) {
|
|
@@ -101,6 +97,7 @@ function runDoctor(tool, targetDir = process.cwd()) {
|
|
|
101
97
|
tool: normalizedTool,
|
|
102
98
|
targetDir: baseDir,
|
|
103
99
|
gitUserEmail: readGitUserEmail(baseDir),
|
|
100
|
+
gitRemoteUrl: readGitRemoteUrl(baseDir),
|
|
104
101
|
configChecks,
|
|
105
102
|
recentLogs: readRecentLogs(normalizedTool),
|
|
106
103
|
};
|
package/lib/git.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function runGit(repoRoot, args, required = true) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync('git', args, {
|
|
6
|
+
cwd: repoRoot,
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
}).trim();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (!required) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const stderr = error && error.stderr ? String(error.stderr).trim() : String(error);
|
|
16
|
+
throw new Error(`git ${args.join(' ')} failed: ${stderr}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
runGit,
|
|
22
|
+
};
|
package/lib/hooks_deploy.js
CHANGED
|
@@ -72,6 +72,19 @@ function writeJsonFile(filePath, data) {
|
|
|
72
72
|
return true;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function writeTextFileIfChanged(filePath, nextContent) {
|
|
76
|
+
if (fs.existsSync(filePath)) {
|
|
77
|
+
const currentContent = fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
if (currentContent === nextContent) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ensureDir(path.dirname(filePath));
|
|
84
|
+
fs.writeFileSync(filePath, nextContent, 'utf8');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
function removeFileIfExists(filePath, removedFiles) {
|
|
76
89
|
if (!fs.existsSync(filePath)) {
|
|
77
90
|
return;
|
|
@@ -217,6 +230,57 @@ function cleanupCodexManagedHooks(hooks) {
|
|
|
217
230
|
return removeCommandEntriesByMatcher(withoutManagedCommands, CODEX_OLD_COMMAND_MATCHERS);
|
|
218
231
|
}
|
|
219
232
|
|
|
233
|
+
function ensureCodexHooksFeatureFlag(toml) {
|
|
234
|
+
const newline = toml.includes('\r\n') ? '\r\n' : '\n';
|
|
235
|
+
const desiredLine = `codex_hooks = true${newline}`;
|
|
236
|
+
const desiredSection = `[features]${newline}${desiredLine}`;
|
|
237
|
+
|
|
238
|
+
if (!toml.trim()) {
|
|
239
|
+
return desiredSection;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const featuresHeaderPattern = /^[ \t]*\[features\][ \t]*(?:#.*)?$/m;
|
|
243
|
+
const featuresHeaderMatch = featuresHeaderPattern.exec(toml);
|
|
244
|
+
if (!featuresHeaderMatch) {
|
|
245
|
+
const trimmed = toml.replace(/[ \t\r\n]*$/, '');
|
|
246
|
+
return `${trimmed}${newline}${newline}${desiredSection}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const headerStart = featuresHeaderMatch.index;
|
|
250
|
+
const headerEnd = headerStart + featuresHeaderMatch[0].length;
|
|
251
|
+
const sectionAfterHeaderStart = toml.indexOf('\n', headerEnd) === -1
|
|
252
|
+
? toml.length
|
|
253
|
+
: toml.indexOf('\n', headerEnd) + 1;
|
|
254
|
+
const afterHeader = toml.slice(sectionAfterHeaderStart);
|
|
255
|
+
const nextHeaderMatch = /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?$/m.exec(afterHeader);
|
|
256
|
+
const sectionEnd = nextHeaderMatch
|
|
257
|
+
? sectionAfterHeaderStart + nextHeaderMatch.index
|
|
258
|
+
: toml.length;
|
|
259
|
+
const section = toml.slice(headerStart, sectionEnd);
|
|
260
|
+
const flagPattern = /^([ \t]*codex_hooks[ \t]*=[ \t]*)(true|false)([ \t]*(?:#.*)?$)/m;
|
|
261
|
+
const flagMatch = flagPattern.exec(section);
|
|
262
|
+
|
|
263
|
+
if (flagMatch) {
|
|
264
|
+
if (flagMatch[2] === 'true') {
|
|
265
|
+
return toml;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const nextSection = section.replace(flagPattern, '$1true$3');
|
|
269
|
+
return `${toml.slice(0, headerStart)}${nextSection}${toml.slice(sectionEnd)}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return `${toml.slice(0, sectionAfterHeaderStart)}${desiredLine}${toml.slice(sectionAfterHeaderStart)}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ensureCodexConfigToml(configPath) {
|
|
276
|
+
const exists = fs.existsSync(configPath);
|
|
277
|
+
const currentContent = exists ? fs.readFileSync(configPath, 'utf8') : '';
|
|
278
|
+
const nextContent = ensureCodexHooksFeatureFlag(currentContent);
|
|
279
|
+
const changed = writeTextFileIfChanged(configPath, nextContent);
|
|
280
|
+
|
|
281
|
+
return { exists, changed };
|
|
282
|
+
}
|
|
283
|
+
|
|
220
284
|
function ensureCodexHook(config) {
|
|
221
285
|
const next = isPlainObject(config) ? { ...config } : {};
|
|
222
286
|
const hooks = isPlainObject(next.hooks) ? { ...next.hooks } : {};
|
|
@@ -289,13 +353,20 @@ function deployHooks(targetDir = process.cwd()) {
|
|
|
289
353
|
(claudeSettingsResult.exists ? updatedFiles : createdFiles).push(claudeSettingsPath);
|
|
290
354
|
}
|
|
291
355
|
|
|
292
|
-
const
|
|
356
|
+
const codexDir = path.join(baseDir, '.codex');
|
|
357
|
+
const codexHooksPath = path.join(codexDir, 'hooks.json');
|
|
293
358
|
const codexHooksResult = readJsonFile(codexHooksPath, {});
|
|
294
359
|
const nextCodexHooks = ensureCodexHook(codexHooksResult.data);
|
|
295
360
|
if (writeJsonFile(codexHooksPath, nextCodexHooks)) {
|
|
296
361
|
(codexHooksResult.exists ? updatedFiles : createdFiles).push(codexHooksPath);
|
|
297
362
|
}
|
|
298
363
|
|
|
364
|
+
const codexConfigPath = path.join(codexDir, 'config.toml');
|
|
365
|
+
const codexConfigResult = ensureCodexConfigToml(codexConfigPath);
|
|
366
|
+
if (codexConfigResult.changed) {
|
|
367
|
+
(codexConfigResult.exists ? updatedFiles : createdFiles).push(codexConfigPath);
|
|
368
|
+
}
|
|
369
|
+
|
|
299
370
|
const localCandidates = [
|
|
300
371
|
path.join(baseDir, '.claude', 'settings.local.json'),
|
|
301
372
|
path.join(baseDir, '.claude', 'setting.local.json'),
|
package/lib/skills_prompt.js
CHANGED
|
@@ -81,11 +81,50 @@ async function promptForScope() {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
async function promptYesNo(message) {
|
|
85
|
+
const rl = readline.createInterface({
|
|
86
|
+
input: process.stdin,
|
|
87
|
+
output: process.stdout,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
process.stderr.write(`${message} [y/N]\n`);
|
|
92
|
+
const answer = String(await new Promise((resolve) => {
|
|
93
|
+
rl.question('> ', resolve);
|
|
94
|
+
})).trim().toLowerCase();
|
|
95
|
+
return answer === 'y' || answer === 'yes';
|
|
96
|
+
} finally {
|
|
97
|
+
rl.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function promptForText(message, defaultValue = '') {
|
|
102
|
+
const rl = readline.createInterface({
|
|
103
|
+
input: process.stdin,
|
|
104
|
+
output: process.stdout,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
process.stderr.write(`${message}\n`);
|
|
109
|
+
if (defaultValue) {
|
|
110
|
+
process.stderr.write(`Current value: ${defaultValue}\n`);
|
|
111
|
+
}
|
|
112
|
+
const answer = await new Promise((resolve) => {
|
|
113
|
+
rl.question('> ', resolve);
|
|
114
|
+
});
|
|
115
|
+
return String(answer).trim();
|
|
116
|
+
} finally {
|
|
117
|
+
rl.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
84
121
|
module.exports = {
|
|
85
122
|
SCOPE_CHOICES,
|
|
86
123
|
TOOL_CHOICES,
|
|
87
124
|
normalizeChoiceInput,
|
|
88
125
|
normalizeCsvInput,
|
|
89
126
|
promptForScope,
|
|
127
|
+
promptForText,
|
|
90
128
|
promptForTools,
|
|
129
|
+
promptYesNo,
|
|
91
130
|
};
|