claude-coder 1.9.0 → 1.9.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.
Files changed (74) hide show
  1. package/README.md +214 -214
  2. package/bin/cli.js +155 -155
  3. package/package.json +55 -55
  4. package/recipes/_shared/roles/developer.md +11 -11
  5. package/recipes/_shared/roles/product.md +12 -12
  6. package/recipes/_shared/roles/tester.md +12 -12
  7. package/recipes/_shared/test/report-format.md +86 -86
  8. package/recipes/backend/base.md +27 -27
  9. package/recipes/backend/components/auth.md +18 -18
  10. package/recipes/backend/components/crud-api.md +18 -18
  11. package/recipes/backend/components/file-service.md +15 -15
  12. package/recipes/backend/manifest.json +20 -20
  13. package/recipes/backend/test/api-test.md +25 -25
  14. package/recipes/console/base.md +37 -37
  15. package/recipes/console/components/modal-form.md +20 -20
  16. package/recipes/console/components/pagination.md +17 -17
  17. package/recipes/console/components/search.md +17 -17
  18. package/recipes/console/components/table-list.md +18 -18
  19. package/recipes/console/components/tabs.md +14 -14
  20. package/recipes/console/components/tree.md +15 -15
  21. package/recipes/console/components/upload.md +15 -15
  22. package/recipes/console/manifest.json +24 -24
  23. package/recipes/console/test/crud-e2e.md +47 -47
  24. package/recipes/h5/base.md +26 -26
  25. package/recipes/h5/components/animation.md +11 -11
  26. package/recipes/h5/components/countdown.md +11 -11
  27. package/recipes/h5/components/share.md +11 -11
  28. package/recipes/h5/components/swiper.md +11 -11
  29. package/recipes/h5/manifest.json +21 -21
  30. package/recipes/h5/test/h5-e2e.md +20 -20
  31. package/src/commands/auth.js +362 -362
  32. package/src/commands/setup-modules/helpers.js +100 -100
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +115 -115
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +47 -47
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +245 -245
  40. package/src/common/config.js +125 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +260 -260
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +50 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +213 -213
  48. package/src/core/coding.js +33 -33
  49. package/src/core/go.js +264 -264
  50. package/src/core/hooks.js +500 -500
  51. package/src/core/init.js +166 -165
  52. package/src/core/plan.js +188 -187
  53. package/src/core/prompts.js +247 -247
  54. package/src/core/repair.js +36 -36
  55. package/src/core/runner.js +458 -458
  56. package/src/core/scan.js +93 -93
  57. package/src/core/session.js +271 -271
  58. package/src/core/simplify.js +74 -74
  59. package/src/core/state.js +105 -105
  60. package/src/index.js +76 -76
  61. package/templates/bash-process.md +12 -12
  62. package/templates/codingSystem.md +65 -65
  63. package/templates/codingUser.md +17 -17
  64. package/templates/coreProtocol.md +29 -29
  65. package/templates/goSystem.md +130 -130
  66. package/templates/guidance.json +72 -72
  67. package/templates/planSystem.md +78 -78
  68. package/templates/planUser.md +8 -8
  69. package/templates/requirements.example.md +57 -57
  70. package/templates/scanSystem.md +120 -120
  71. package/templates/scanUser.md +10 -10
  72. package/templates/test_rule.md +194 -194
  73. package/templates/web-testing.md +17 -17
  74. package/types/index.d.ts +217 -217
@@ -1,88 +1,88 @@
1
- 'use strict';
2
-
3
- const { log, COLOR } = require('./config');
4
- const { assets } = require('./assets');
5
-
6
- function loadTasks() {
7
- return assets.readJson('tasks', null);
8
- }
9
-
10
- function saveTasks(data) {
11
- assets.writeJson('tasks', data);
12
- }
13
-
14
- function getFeatures(data) {
15
- return data?.features || [];
16
- }
17
-
18
- function getStats(data) {
19
- const features = getFeatures(data);
20
- return {
21
- total: features.length,
22
- done: features.filter(f => f.status === 'done').length,
23
- failed: features.filter(f => f.status === 'failed').length,
24
- in_progress: features.filter(f => f.status === 'in_progress').length,
25
- testing: features.filter(f => f.status === 'testing').length,
26
- pending: features.filter(f => f.status === 'pending').length,
27
- };
28
- }
29
-
30
- function printStats() {
31
- const data = loadTasks();
32
- if (!data) return;
33
- const stats = getStats(data);
34
- log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
35
- }
36
-
37
- function showStatus() {
38
- const data = loadTasks();
39
- if (!data) {
40
- log('warn', '未找到 .claude-coder/tasks.json,请先运行 claude-coder run');
41
- return;
42
- }
43
-
44
- const stats = getStats(data);
45
- const features = getFeatures(data);
46
-
47
- console.log(`\n${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
48
- console.log(` ${COLOR.blue}📋 任务状态${COLOR.reset} 项目: ${data.project || '(未命名)'}`);
49
- console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
50
-
51
- const bar = stats.total > 0
52
- ? `[${'█'.repeat(Math.floor(stats.done / stats.total * 30))}${'░'.repeat(30 - Math.floor(stats.done / stats.total * 30))}]`
53
- : '[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]';
54
- console.log(` 进度: ${bar} ${stats.done}/${stats.total}`);
55
-
56
- console.log(`\n ${COLOR.green}✔ done: ${stats.done}${COLOR.reset} ${COLOR.yellow}⏳ pending: ${stats.pending}${COLOR.reset} ${COLOR.red}✘ failed: ${stats.failed}${COLOR.reset}`);
57
-
58
- if (stats.in_progress > 0 || stats.testing > 0) {
59
- console.log(` ▸ in_progress: ${stats.in_progress} ▸ testing: ${stats.testing}`);
60
- }
61
-
62
- const progress = assets.readJson('progress', null);
63
- if (progress) {
64
- const sessions = (progress.sessions || []).filter(s => typeof s.cost === 'number');
65
- if (sessions.length > 0) {
66
- const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
67
- console.log(`\n ${COLOR.blue}💰 累计成本${COLOR.reset}: $${totalCost.toFixed(4)} (${sessions.length} sessions)`);
68
- }
69
- }
70
-
71
- console.log(`\n ${'─'.repeat(45)}`);
72
- for (const f of features) {
73
- const icon = { done: '✔', pending: '○', in_progress: '▸', testing: '⟳', failed: '✘' }[f.status] || '?';
74
- const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow }[f.status] || '';
75
- console.log(` ${color}${icon}${COLOR.reset} [${f.id}] ${f.description} (${f.status})`);
76
- }
77
-
78
- console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}\n`);
79
- }
80
-
81
- module.exports = {
82
- loadTasks,
83
- saveTasks,
84
- getFeatures,
85
- getStats,
86
- printStats,
87
- showStatus,
88
- };
1
+ 'use strict';
2
+
3
+ const { log, COLOR } = require('./config');
4
+ const { assets } = require('./assets');
5
+
6
+ function loadTasks() {
7
+ return assets.readJson('tasks', null);
8
+ }
9
+
10
+ function saveTasks(data) {
11
+ assets.writeJson('tasks', data);
12
+ }
13
+
14
+ function getFeatures(data) {
15
+ return data?.features || [];
16
+ }
17
+
18
+ function getStats(data) {
19
+ const features = getFeatures(data);
20
+ return {
21
+ total: features.length,
22
+ done: features.filter(f => f.status === 'done').length,
23
+ failed: features.filter(f => f.status === 'failed').length,
24
+ in_progress: features.filter(f => f.status === 'in_progress').length,
25
+ testing: features.filter(f => f.status === 'testing').length,
26
+ pending: features.filter(f => f.status === 'pending').length,
27
+ };
28
+ }
29
+
30
+ function printStats() {
31
+ const data = loadTasks();
32
+ if (!data) return;
33
+ const stats = getStats(data);
34
+ log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
35
+ }
36
+
37
+ function showStatus() {
38
+ const data = loadTasks();
39
+ if (!data) {
40
+ log('warn', '未找到 .claude-coder/tasks.json,请先运行 claude-coder run');
41
+ return;
42
+ }
43
+
44
+ const stats = getStats(data);
45
+ const features = getFeatures(data);
46
+
47
+ console.log(`\n${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
48
+ console.log(` ${COLOR.blue}📋 任务状态${COLOR.reset} 项目: ${data.project || '(未命名)'}`);
49
+ console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
50
+
51
+ const bar = stats.total > 0
52
+ ? `[${'█'.repeat(Math.floor(stats.done / stats.total * 30))}${'░'.repeat(30 - Math.floor(stats.done / stats.total * 30))}]`
53
+ : '[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]';
54
+ console.log(` 进度: ${bar} ${stats.done}/${stats.total}`);
55
+
56
+ console.log(`\n ${COLOR.green}✔ done: ${stats.done}${COLOR.reset} ${COLOR.yellow}⏳ pending: ${stats.pending}${COLOR.reset} ${COLOR.red}✘ failed: ${stats.failed}${COLOR.reset}`);
57
+
58
+ if (stats.in_progress > 0 || stats.testing > 0) {
59
+ console.log(` ▸ in_progress: ${stats.in_progress} ▸ testing: ${stats.testing}`);
60
+ }
61
+
62
+ const progress = assets.readJson('progress', null);
63
+ if (progress) {
64
+ const sessions = (progress.sessions || []).filter(s => typeof s.cost === 'number');
65
+ if (sessions.length > 0) {
66
+ const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
67
+ console.log(`\n ${COLOR.blue}💰 累计成本${COLOR.reset}: $${totalCost.toFixed(4)} (${sessions.length} sessions)`);
68
+ }
69
+ }
70
+
71
+ console.log(`\n ${'─'.repeat(45)}`);
72
+ for (const f of features) {
73
+ const icon = { done: '✔', pending: '○', in_progress: '▸', testing: '⟳', failed: '✘' }[f.status] || '?';
74
+ const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow }[f.status] || '';
75
+ console.log(` ${color}${icon}${COLOR.reset} [${f.id}] ${f.description} (${f.status})`);
76
+ }
77
+
78
+ console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}\n`);
79
+ }
80
+
81
+ module.exports = {
82
+ loadTasks,
83
+ saveTasks,
84
+ getFeatures,
85
+ getStats,
86
+ printStats,
87
+ showStatus,
88
+ };
@@ -1,214 +1,214 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const { execSync } = require('child_process');
5
-
6
- // ─────────────────────────────────────────────────────────────
7
- // 字符串工具
8
- // ─────────────────────────────────────────────────────────────
9
-
10
- /**
11
- * 中间截断字符串,保留首尾
12
- * @param {string} str - 原字符串
13
- * @param {number} maxLen - 最大长度
14
- * @returns {string}
15
- */
16
- function truncateMiddle(str, maxLen) {
17
- if (!str || str.length <= maxLen) return str || '';
18
- const startLen = Math.ceil((maxLen - 1) / 2);
19
- const endLen = Math.floor((maxLen - 1) / 2);
20
- return str.slice(0, startLen) + '…' + str.slice(-endLen);
21
- }
22
-
23
- /**
24
- * 路径感知截断:优先保留文件名,截断目录中间
25
- * @param {string} path - 文件路径
26
- * @param {number} maxLen - 最大长度
27
- * @returns {string}
28
- */
29
- function truncatePath(path, maxLen) {
30
- if (!path || path.length <= maxLen) return path || '';
31
-
32
- const lastSlash = path.lastIndexOf('/');
33
- if (lastSlash === -1) {
34
- return truncateMiddle(path, maxLen);
35
- }
36
-
37
- const fileName = path.slice(lastSlash + 1);
38
- const dirPath = path.slice(0, lastSlash);
39
-
40
- // 文件名本身超长,截断文件名
41
- if (fileName.length >= maxLen - 2) {
42
- return truncateMiddle(path, maxLen);
43
- }
44
-
45
- // 保留文件名,截断目录
46
- const availableForDir = maxLen - fileName.length - 2; // -2 for '…/'
47
- if (availableForDir <= 0) {
48
- return '…/' + fileName.slice(0, maxLen - 2);
49
- }
50
-
51
- // 目录两端保留
52
- const dirStart = Math.ceil(availableForDir / 2);
53
- const dirEnd = Math.floor(availableForDir / 2);
54
- const truncatedDir = dirPath.slice(0, dirStart) + '…' + (dirEnd > 0 ? dirPath.slice(-dirEnd) : '');
55
-
56
- return truncatedDir + '/' + fileName;
57
- }
58
-
59
- /**
60
- * 命令字符串截断:保留头部,超长时截断
61
- * @param {string} cmd - 命令字符串
62
- * @param {number} maxLen - 最大长度
63
- * @returns {string}
64
- */
65
- function truncateCommand(cmd, maxLen) {
66
- if (!cmd || cmd.length <= maxLen) return cmd || '';
67
- return cmd.slice(0, maxLen - 1) + '…';
68
- }
69
-
70
- // ─────────────────────────────────────────────────────────────
71
- // Git 工具
72
- // ─────────────────────────────────────────────────────────────
73
-
74
- /**
75
- * 获取当前 git HEAD commit hash
76
- * @param {string} cwd - 工作目录
77
- * @returns {string} commit hash 或 'none'
78
- */
79
- function getGitHead(cwd) {
80
- try {
81
- return execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
82
- } catch {
83
- return 'none';
84
- }
85
- }
86
-
87
- /**
88
- * 检查是否在 git 仓库中
89
- * @param {string} cwd - 工作目录
90
- * @returns {boolean}
91
- */
92
- function isGitRepo(cwd) {
93
- try {
94
- execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- // ─────────────────────────────────────────────────────────────
102
- // .gitignore 工具
103
- // ─────────────────────────────────────────────────────────────
104
-
105
- /**
106
- * 向 .gitignore 追加条目(如果不存在)
107
- * @param {string} projectRoot - 项目根目录
108
- * @param {string} entry - 要添加的条目
109
- * @returns {boolean} 是否有新增
110
- */
111
- function appendGitignore(projectRoot, entry) {
112
- const path = require('path');
113
- const gitignorePath = path.join(projectRoot, '.gitignore');
114
- let content = '';
115
- if (fs.existsSync(gitignorePath)) {
116
- content = fs.readFileSync(gitignorePath, 'utf8');
117
- }
118
- if (content.includes(entry)) return false;
119
-
120
- const suffix = content.endsWith('\n') || content === '' ? '' : '\n';
121
- fs.appendFileSync(gitignorePath, `${suffix}${entry}\n`, 'utf8');
122
- return true;
123
- }
124
-
125
- /**
126
- * 确保 .gitignore 包含 claude-coder 的忽略规则
127
- * 使用通配符忽略整个目录,仅白名单放行需要版本控制的文件
128
- * @param {string} projectRoot - 项目根目录
129
- * @returns {boolean} 是否有新增
130
- */
131
- function ensureGitignore(projectRoot) {
132
- const patterns = [
133
- '.claude-coder/*',
134
- '!.claude-coder/tasks.json',
135
- '!.claude-coder/project_profile.json',
136
- ];
137
- let added = false;
138
- for (const p of patterns) {
139
- if (appendGitignore(projectRoot, p)) added = true;
140
- }
141
- return added;
142
- }
143
-
144
- // ─────────────────────────────────────────────────────────────
145
- // 进程工具
146
- // ─────────────────────────────────────────────────────────────
147
-
148
- function sleep(ms) {
149
- return new Promise(resolve => setTimeout(resolve, ms));
150
- }
151
-
152
- // ─────────────────────────────────────────────────────────────
153
- // 项目服务管理
154
- // ─────────────────────────────────────────────────────────────
155
-
156
- function tryPush(projectRoot) {
157
- const { log } = require('./config');
158
- try {
159
- const remotes = execSync('git remote', { cwd: projectRoot, encoding: 'utf8' }).trim();
160
- if (!remotes) return;
161
- log('info', '正在推送代码...');
162
- execSync('git push', { cwd: projectRoot, stdio: 'inherit' });
163
- log('ok', '推送成功');
164
- } catch {
165
- log('warn', '推送失败 (请检查网络或权限),继续执行...');
166
- }
167
- }
168
-
169
- function killServices(projectRoot) {
170
- const { log } = require('./config');
171
- const { assets } = require('./assets');
172
- const profile = assets.readJson('profile', null);
173
- if (!profile) return;
174
- const ports = (profile.services || []).map(s => s.port).filter(Boolean);
175
- if (ports.length === 0) return;
176
-
177
- for (const port of ports) {
178
- try {
179
- if (process.platform === 'win32') {
180
- const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
181
- const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
182
- for (const pid of pids) { try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
183
- } else {
184
- execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
185
- }
186
- } catch { /* no process on port */ }
187
- }
188
- log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
189
- }
190
-
191
- // ─────────────────────────────────────────────────────────────
192
- // 日志工具
193
- // ─────────────────────────────────────────────────────────────
194
- function localTimestamp() {
195
- const d = new Date();
196
- const hh = String(d.getHours()).padStart(2, '0');
197
- const mm = String(d.getMinutes()).padStart(2, '0');
198
- const ss = String(d.getSeconds()).padStart(2, '0');
199
- return `${hh}:${mm}:${ss}`;
200
- }
201
-
202
- module.exports = {
203
- truncateMiddle,
204
- truncatePath,
205
- truncateCommand,
206
- getGitHead,
207
- isGitRepo,
208
- appendGitignore,
209
- ensureGitignore,
210
- sleep,
211
- tryPush,
212
- killServices,
213
- localTimestamp,
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { execSync } = require('child_process');
5
+
6
+ // ─────────────────────────────────────────────────────────────
7
+ // 字符串工具
8
+ // ─────────────────────────────────────────────────────────────
9
+
10
+ /**
11
+ * 中间截断字符串,保留首尾
12
+ * @param {string} str - 原字符串
13
+ * @param {number} maxLen - 最大长度
14
+ * @returns {string}
15
+ */
16
+ function truncateMiddle(str, maxLen) {
17
+ if (!str || str.length <= maxLen) return str || '';
18
+ const startLen = Math.ceil((maxLen - 1) / 2);
19
+ const endLen = Math.floor((maxLen - 1) / 2);
20
+ return str.slice(0, startLen) + '…' + str.slice(-endLen);
21
+ }
22
+
23
+ /**
24
+ * 路径感知截断:优先保留文件名,截断目录中间
25
+ * @param {string} path - 文件路径
26
+ * @param {number} maxLen - 最大长度
27
+ * @returns {string}
28
+ */
29
+ function truncatePath(p, maxLen) {
30
+ if (!p || p.length <= maxLen) return p || '';
31
+
32
+ const lastSlashFwd = p.lastIndexOf('/');
33
+ const lastSlashBwd = p.lastIndexOf('\\');
34
+ const lastSlash = Math.max(lastSlashFwd, lastSlashBwd);
35
+ if (lastSlash === -1) {
36
+ return truncateMiddle(p, maxLen);
37
+ }
38
+
39
+ const sep = p[lastSlash];
40
+ const fileName = p.slice(lastSlash + 1);
41
+ const dirPath = p.slice(0, lastSlash);
42
+
43
+ if (fileName.length >= maxLen - 2) {
44
+ return truncateMiddle(p, maxLen);
45
+ }
46
+
47
+ const availableForDir = maxLen - fileName.length - 2;
48
+ if (availableForDir <= 0) {
49
+ return '…' + sep + fileName.slice(0, maxLen - 2);
50
+ }
51
+
52
+ const dirStart = Math.ceil(availableForDir / 2);
53
+ const dirEnd = Math.floor(availableForDir / 2);
54
+ const truncatedDir = dirPath.slice(0, dirStart) + '…' + (dirEnd > 0 ? dirPath.slice(-dirEnd) : '');
55
+
56
+ return truncatedDir + sep + fileName;
57
+ }
58
+
59
+ /**
60
+ * 命令字符串截断:保留头部,超长时截断
61
+ * @param {string} cmd - 命令字符串
62
+ * @param {number} maxLen - 最大长度
63
+ * @returns {string}
64
+ */
65
+ function truncateCommand(cmd, maxLen) {
66
+ if (!cmd || cmd.length <= maxLen) return cmd || '';
67
+ return cmd.slice(0, maxLen - 1) + '…';
68
+ }
69
+
70
+ // ─────────────────────────────────────────────────────────────
71
+ // Git 工具
72
+ // ─────────────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * 获取当前 git HEAD commit hash
76
+ * @param {string} cwd - 工作目录
77
+ * @returns {string} commit hash 或 'none'
78
+ */
79
+ function getGitHead(cwd) {
80
+ try {
81
+ return execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
82
+ } catch {
83
+ return 'none';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 检查是否在 git 仓库中
89
+ * @param {string} cwd - 工作目录
90
+ * @returns {boolean}
91
+ */
92
+ function isGitRepo(cwd) {
93
+ try {
94
+ execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────
102
+ // .gitignore 工具
103
+ // ─────────────────────────────────────────────────────────────
104
+
105
+ /**
106
+ * 向 .gitignore 追加条目(如果不存在)
107
+ * @param {string} projectRoot - 项目根目录
108
+ * @param {string} entry - 要添加的条目
109
+ * @returns {boolean} 是否有新增
110
+ */
111
+ function appendGitignore(projectRoot, entry) {
112
+ const path = require('path');
113
+ const gitignorePath = path.join(projectRoot, '.gitignore');
114
+ let content = '';
115
+ if (fs.existsSync(gitignorePath)) {
116
+ content = fs.readFileSync(gitignorePath, 'utf8');
117
+ }
118
+ if (content.includes(entry)) return false;
119
+
120
+ const suffix = content.endsWith('\n') || content === '' ? '' : '\n';
121
+ fs.appendFileSync(gitignorePath, `${suffix}${entry}\n`, 'utf8');
122
+ return true;
123
+ }
124
+
125
+ /**
126
+ * 确保 .gitignore 包含 claude-coder 的忽略规则
127
+ * 使用通配符忽略整个目录,仅白名单放行需要版本控制的文件
128
+ * @param {string} projectRoot - 项目根目录
129
+ * @returns {boolean} 是否有新增
130
+ */
131
+ function ensureGitignore(projectRoot) {
132
+ const patterns = [
133
+ '.claude-coder/*',
134
+ '!.claude-coder/tasks.json',
135
+ '!.claude-coder/project_profile.json',
136
+ ];
137
+ let added = false;
138
+ for (const p of patterns) {
139
+ if (appendGitignore(projectRoot, p)) added = true;
140
+ }
141
+ return added;
142
+ }
143
+
144
+ // ─────────────────────────────────────────────────────────────
145
+ // 进程工具
146
+ // ─────────────────────────────────────────────────────────────
147
+
148
+ function sleep(ms) {
149
+ return new Promise(resolve => setTimeout(resolve, ms));
150
+ }
151
+
152
+ // ─────────────────────────────────────────────────────────────
153
+ // 项目服务管理
154
+ // ─────────────────────────────────────────────────────────────
155
+
156
+ function tryPush(projectRoot) {
157
+ const { log } = require('./config');
158
+ try {
159
+ const remotes = execSync('git remote', { cwd: projectRoot, encoding: 'utf8' }).trim();
160
+ if (!remotes) return;
161
+ log('info', '正在推送代码...');
162
+ execSync('git push', { cwd: projectRoot, stdio: 'inherit' });
163
+ log('ok', '推送成功');
164
+ } catch {
165
+ log('warn', '推送失败 (请检查网络或权限),继续执行...');
166
+ }
167
+ }
168
+
169
+ function killServices(projectRoot) {
170
+ const { log } = require('./config');
171
+ const { assets } = require('./assets');
172
+ const profile = assets.readJson('profile', null);
173
+ if (!profile) return;
174
+ const ports = (profile.services || []).map(s => s.port).filter(Boolean);
175
+ if (ports.length === 0) return;
176
+
177
+ for (const port of ports) {
178
+ try {
179
+ if (process.platform === 'win32') {
180
+ const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
181
+ const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
182
+ for (const pid of pids) { try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
183
+ } else {
184
+ execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
185
+ }
186
+ } catch { /* no process on port */ }
187
+ }
188
+ log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
189
+ }
190
+
191
+ // ─────────────────────────────────────────────────────────────
192
+ // 日志工具
193
+ // ─────────────────────────────────────────────────────────────
194
+ function localTimestamp() {
195
+ const d = new Date();
196
+ const hh = String(d.getHours()).padStart(2, '0');
197
+ const mm = String(d.getMinutes()).padStart(2, '0');
198
+ const ss = String(d.getSeconds()).padStart(2, '0');
199
+ return `${hh}:${mm}:${ss}`;
200
+ }
201
+
202
+ module.exports = {
203
+ truncateMiddle,
204
+ truncatePath,
205
+ truncateCommand,
206
+ getGitHead,
207
+ isGitRepo,
208
+ appendGitignore,
209
+ ensureGitignore,
210
+ sleep,
211
+ tryPush,
212
+ killServices,
213
+ localTimestamp,
214
214
  };