@haposoft/cafekit 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -28
- package/bin/install.js +125 -1
- package/package.json +5 -3
- package/src/claude/hooks/agent.cjs +203 -0
- package/src/claude/hooks/lib/color.cjs +95 -0
- package/src/claude/hooks/lib/config.cjs +831 -0
- package/src/claude/hooks/lib/context.cjs +616 -0
- package/src/claude/hooks/lib/counter.cjs +103 -0
- package/src/claude/hooks/lib/detect.cjs +474 -0
- package/src/claude/hooks/lib/git.cjs +143 -0
- package/src/claude/hooks/lib/parser.cjs +182 -0
- package/src/claude/hooks/session.cjs +360 -0
- package/src/claude/hooks/usage.cjs +179 -0
- package/src/claude/migration-manifest.json +27 -2
- package/src/claude/settings/status.settings.json +54 -0
- package/src/claude/status.cjs +539 -0
- package/src/common/skills/code/SKILL.md +55 -0
- package/src/common/skills/code/references/execution-loop.md +21 -0
- package/src/common/skills/impact-analysis/references/change-detection.md +16 -16
- package/src/common/skills/impact-analysis/references/dependency-scouting.md +8 -8
- package/src/common/skills/impact-analysis/references/edge-case-identification.md +11 -11
- package/src/common/skills/impact-analysis/references/industry-techniques.md +36 -36
- package/src/common/skills/impact-analysis/references/practical-techniques-guide.md +16 -16
- package/src/common/skills/impact-analysis/references/project-detection.md +1 -1
- package/src/common/skills/impact-analysis/references/report-template.md +2 -2
- package/src/common/skills/impact-analysis/scripts/README.md +3 -3
- package/src/common/skills/review/SKILL.md +46 -0
- package/src/common/skills/review/references/review-focus.md +28 -0
- package/src/common/skills/spec-design/SKILL.md +66 -0
- package/src/common/skills/spec-design/references/design-discovery.md +46 -0
- package/src/common/skills/spec-init/SKILL.md +61 -0
- package/src/common/skills/spec-requirements/SKILL.md +59 -0
- package/src/common/skills/spec-requirements/references/requirements-workflow.md +36 -0
- package/src/common/skills/spec-tasks/SKILL.md +60 -0
- package/src/common/skills/spec-tasks/references/task-sizing.md +36 -0
- package/src/common/skills/test/SKILL.md +40 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Config Counter - Count CLAUDE.md, rules, MCPs, hooks across user and project scopes
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
function getMcpServerNames(filePath) {
|
|
11
|
+
if (!fs.existsSync(filePath)) return new Set();
|
|
12
|
+
try {
|
|
13
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
14
|
+
const config = JSON.parse(content);
|
|
15
|
+
if (config.mcpServers && typeof config.mcpServers === 'object') {
|
|
16
|
+
return new Set(Object.keys(config.mcpServers));
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Silent fail
|
|
20
|
+
}
|
|
21
|
+
return new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function countMcpServersInFile(filePath, excludeFrom) {
|
|
25
|
+
const servers = getMcpServerNames(filePath);
|
|
26
|
+
if (excludeFrom) {
|
|
27
|
+
const exclude = getMcpServerNames(excludeFrom);
|
|
28
|
+
for (const name of exclude) {
|
|
29
|
+
servers.delete(name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return servers.size;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function countHooksInFile(filePath) {
|
|
36
|
+
if (!fs.existsSync(filePath)) return 0;
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
const config = JSON.parse(content);
|
|
40
|
+
if (config.hooks && typeof config.hooks === 'object') {
|
|
41
|
+
return Object.keys(config.hooks).length;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Silent fail
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function countRulesInDir(rulesDir, depth = 0) {
|
|
50
|
+
// Depth limit prevents symlink loops and excessive recursion
|
|
51
|
+
if (depth > 5 || !fs.existsSync(rulesDir)) return 0;
|
|
52
|
+
let count = 0;
|
|
53
|
+
try {
|
|
54
|
+
const entries = fs.readdirSync(rulesDir, { withFileTypes: true });
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
// Skip symlinks to prevent loops
|
|
57
|
+
if (entry.isSymbolicLink()) continue;
|
|
58
|
+
const fullPath = path.join(rulesDir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
count += countRulesInDir(fullPath, depth + 1);
|
|
61
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Silent fail
|
|
67
|
+
}
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function countConfigs(cwd) {
|
|
72
|
+
let claudeMdCount = 0, rulesCount = 0, mcpCount = 0, hooksCount = 0;
|
|
73
|
+
const homeDir = os.homedir();
|
|
74
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
75
|
+
|
|
76
|
+
// User scope
|
|
77
|
+
if (fs.existsSync(path.join(claudeDir, 'CLAUDE.md'))) claudeMdCount++;
|
|
78
|
+
rulesCount += countRulesInDir(path.join(claudeDir, 'rules'));
|
|
79
|
+
const userSettings = path.join(claudeDir, 'settings.json');
|
|
80
|
+
mcpCount += countMcpServersInFile(userSettings);
|
|
81
|
+
hooksCount += countHooksInFile(userSettings);
|
|
82
|
+
mcpCount += countMcpServersInFile(path.join(homeDir, '.claude.json'), userSettings);
|
|
83
|
+
|
|
84
|
+
// Project scope
|
|
85
|
+
if (cwd) {
|
|
86
|
+
if (fs.existsSync(path.join(cwd, 'CLAUDE.md'))) claudeMdCount++;
|
|
87
|
+
if (fs.existsSync(path.join(cwd, 'CLAUDE.local.md'))) claudeMdCount++;
|
|
88
|
+
if (fs.existsSync(path.join(cwd, '.claude', 'CLAUDE.md'))) claudeMdCount++;
|
|
89
|
+
if (fs.existsSync(path.join(cwd, '.claude', 'CLAUDE.local.md'))) claudeMdCount++;
|
|
90
|
+
rulesCount += countRulesInDir(path.join(cwd, '.claude', 'rules'));
|
|
91
|
+
mcpCount += countMcpServersInFile(path.join(cwd, '.mcp.json'));
|
|
92
|
+
const projectSettings = path.join(cwd, '.claude', 'settings.json');
|
|
93
|
+
mcpCount += countMcpServersInFile(projectSettings);
|
|
94
|
+
hooksCount += countHooksInFile(projectSettings);
|
|
95
|
+
const localSettings = path.join(cwd, '.claude', 'settings.local.json');
|
|
96
|
+
mcpCount += countMcpServersInFile(localSettings);
|
|
97
|
+
hooksCount += countHooksInFile(localSettings);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { claudeMdCount, rulesCount, mcpCount, hooksCount };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { countConfigs, getMcpServerNames, countMcpServersInFile, countHooksInFile, countRulesInDir };
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* detect.cjs - Project and environment detection logic
|
|
4
|
+
*
|
|
5
|
+
* Extracted from session.cjs for reuse in both Claude hooks and OpenCode plugins.
|
|
6
|
+
* Detects project type, package manager, framework, and runtime versions.
|
|
7
|
+
*
|
|
8
|
+
* @module detect
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const { execSync, execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// SAFE EXECUTION HELPERS
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safely execute shell command with optional timeout
|
|
22
|
+
* @param {string} cmd - Command to execute
|
|
23
|
+
* @param {number} [timeoutMs=5000] - Timeout in milliseconds
|
|
24
|
+
* @returns {string|null} Output or null on error
|
|
25
|
+
*/
|
|
26
|
+
function execSafe(cmd, timeoutMs = 5000) {
|
|
27
|
+
try {
|
|
28
|
+
return execSync(cmd, {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
timeout: timeoutMs,
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
32
|
+
}).trim();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Safely execute a binary with arguments (no shell interpolation)
|
|
40
|
+
* @param {string} binary - Path to the executable
|
|
41
|
+
* @param {string[]} args - Arguments array
|
|
42
|
+
* @param {number} [timeoutMs=2000] - Timeout in milliseconds
|
|
43
|
+
* @returns {string|null} Output or null on error
|
|
44
|
+
*/
|
|
45
|
+
function execFileSafe(binary, args, timeoutMs = 2000) {
|
|
46
|
+
try {
|
|
47
|
+
return execFileSync(binary, args, {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
timeout: timeoutMs,
|
|
50
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
51
|
+
}).trim();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// PYTHON DETECTION
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate that a path is a file and doesn't contain shell metacharacters
|
|
63
|
+
* @param {string} p - Path to validate
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function isValidPythonPath(p) {
|
|
67
|
+
if (!p || typeof p !== 'string') return false;
|
|
68
|
+
if (/[;&|`$(){}[\]<>!#*?]/.test(p)) return false;
|
|
69
|
+
try {
|
|
70
|
+
const stat = fs.statSync(p);
|
|
71
|
+
return stat.isFile();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build platform-specific Python paths for fast filesystem check
|
|
79
|
+
* @returns {string[]} Array of potential Python paths
|
|
80
|
+
*/
|
|
81
|
+
function getPythonPaths() {
|
|
82
|
+
const paths = [];
|
|
83
|
+
|
|
84
|
+
if (process.env.PYTHON_PATH) {
|
|
85
|
+
paths.push(process.env.PYTHON_PATH);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (process.platform === 'win32') {
|
|
89
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
90
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
91
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
92
|
+
|
|
93
|
+
if (localAppData) {
|
|
94
|
+
paths.push(path.join(localAppData, 'Microsoft', 'WindowsApps', 'python.exe'));
|
|
95
|
+
paths.push(path.join(localAppData, 'Microsoft', 'WindowsApps', 'python3.exe'));
|
|
96
|
+
for (const ver of ['313', '312', '311', '310', '39']) {
|
|
97
|
+
paths.push(path.join(localAppData, 'Programs', 'Python', `Python${ver}`, 'python.exe'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const ver of ['313', '312', '311', '310', '39']) {
|
|
102
|
+
paths.push(path.join(programFiles, `Python${ver}`, 'python.exe'));
|
|
103
|
+
paths.push(path.join(programFilesX86, `Python${ver}`, 'python.exe'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
paths.push('C:\\Python313\\python.exe');
|
|
107
|
+
paths.push('C:\\Python312\\python.exe');
|
|
108
|
+
paths.push('C:\\Python311\\python.exe');
|
|
109
|
+
paths.push('C:\\Python310\\python.exe');
|
|
110
|
+
paths.push('C:\\Python39\\python.exe');
|
|
111
|
+
} else {
|
|
112
|
+
paths.push('/usr/bin/python3');
|
|
113
|
+
paths.push('/usr/local/bin/python3');
|
|
114
|
+
paths.push('/opt/homebrew/bin/python3');
|
|
115
|
+
paths.push('/opt/homebrew/bin/python');
|
|
116
|
+
paths.push('/usr/bin/python');
|
|
117
|
+
paths.push('/usr/local/bin/python');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return paths;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Find Python binary using fast `which` lookup first, then filesystem check
|
|
125
|
+
* @returns {string|null} Python binary path or null
|
|
126
|
+
*/
|
|
127
|
+
function findPythonBinary() {
|
|
128
|
+
// Fast path: try `which` command first (10ms vs 2000ms per path)
|
|
129
|
+
if (process.platform !== 'win32') {
|
|
130
|
+
const whichPython3 = execSafe('which python3', 500);
|
|
131
|
+
if (whichPython3 && isValidPythonPath(whichPython3)) return whichPython3;
|
|
132
|
+
|
|
133
|
+
const whichPython = execSafe('which python', 500);
|
|
134
|
+
if (whichPython && isValidPythonPath(whichPython)) return whichPython;
|
|
135
|
+
} else {
|
|
136
|
+
// Windows: try `where` command
|
|
137
|
+
const wherePython = execSafe('where python', 500);
|
|
138
|
+
if (wherePython) {
|
|
139
|
+
const firstPath = wherePython.split('\n')[0].trim();
|
|
140
|
+
if (isValidPythonPath(firstPath)) return firstPath;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback: check known paths
|
|
145
|
+
const paths = getPythonPaths();
|
|
146
|
+
for (const p of paths) {
|
|
147
|
+
if (isValidPythonPath(p)) return p;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get Python version with optimized detection
|
|
154
|
+
* @returns {string|null} Python version string or null
|
|
155
|
+
*/
|
|
156
|
+
function getPythonVersion() {
|
|
157
|
+
const pythonPath = findPythonBinary();
|
|
158
|
+
if (pythonPath) {
|
|
159
|
+
const result = execFileSafe(pythonPath, ['--version']);
|
|
160
|
+
if (result) return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const commands = ['python3', 'python'];
|
|
164
|
+
for (const cmd of commands) {
|
|
165
|
+
const result = execFileSafe(cmd, ['--version']);
|
|
166
|
+
if (result) return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
173
|
+
// GIT DETECTION
|
|
174
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if current directory is inside a git repository (fast check)
|
|
178
|
+
* Uses filesystem traversal instead of git command to avoid command failures
|
|
179
|
+
* @param {string} [startDir] - Directory to check from (defaults to cwd)
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
182
|
+
function isGitRepo(startDir) {
|
|
183
|
+
let dir;
|
|
184
|
+
try {
|
|
185
|
+
dir = startDir || process.cwd();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
// CWD deleted or inaccessible
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const root = path.parse(dir).root;
|
|
191
|
+
|
|
192
|
+
while (dir !== root) {
|
|
193
|
+
if (fs.existsSync(path.join(dir, '.git'))) return true;
|
|
194
|
+
dir = path.dirname(dir);
|
|
195
|
+
}
|
|
196
|
+
return fs.existsSync(path.join(root, '.git'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get git remote URL
|
|
201
|
+
* @returns {string|null}
|
|
202
|
+
*/
|
|
203
|
+
function getGitRemoteUrl() {
|
|
204
|
+
if (!isGitRepo()) return null;
|
|
205
|
+
return execSafe('git config --get remote.origin.url');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get current git branch
|
|
210
|
+
* @returns {string|null}
|
|
211
|
+
*/
|
|
212
|
+
function getGitBranch() {
|
|
213
|
+
if (!isGitRepo()) return null;
|
|
214
|
+
return execSafe('git branch --show-current');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get git repository root
|
|
219
|
+
* @returns {string|null}
|
|
220
|
+
*/
|
|
221
|
+
function getGitRoot() {
|
|
222
|
+
if (!isGitRepo()) return null;
|
|
223
|
+
return execSafe('git rev-parse --show-toplevel');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
// PROJECT DETECTION
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Detect project type based on workspace indicators
|
|
232
|
+
* @param {string} [configOverride] - Manual override from config
|
|
233
|
+
* @returns {'monorepo' | 'library' | 'single-repo'}
|
|
234
|
+
*/
|
|
235
|
+
function detectProjectType(configOverride) {
|
|
236
|
+
if (configOverride && configOverride !== 'auto') return configOverride;
|
|
237
|
+
|
|
238
|
+
if (fs.existsSync('pnpm-workspace.yaml')) return 'monorepo';
|
|
239
|
+
if (fs.existsSync('lerna.json')) return 'monorepo';
|
|
240
|
+
|
|
241
|
+
if (fs.existsSync('package.json')) {
|
|
242
|
+
try {
|
|
243
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
244
|
+
if (pkg.workspaces) return 'monorepo';
|
|
245
|
+
if (pkg.main || pkg.exports) return 'library';
|
|
246
|
+
} catch (e) { /* ignore */ }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return 'single-repo';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Detect package manager from lock files
|
|
254
|
+
* @param {string} [configOverride] - Manual override from config
|
|
255
|
+
* @returns {'npm' | 'pnpm' | 'yarn' | 'bun' | null}
|
|
256
|
+
*/
|
|
257
|
+
function detectPackageManager(configOverride) {
|
|
258
|
+
if (configOverride && configOverride !== 'auto') return configOverride;
|
|
259
|
+
|
|
260
|
+
if (fs.existsSync('bun.lockb')) return 'bun';
|
|
261
|
+
if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
262
|
+
if (fs.existsSync('yarn.lock')) return 'yarn';
|
|
263
|
+
if (fs.existsSync('package-lock.json')) return 'npm';
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Detect framework from package.json dependencies
|
|
270
|
+
* @param {string} [configOverride] - Manual override from config
|
|
271
|
+
* @returns {string|null}
|
|
272
|
+
*/
|
|
273
|
+
function detectFramework(configOverride) {
|
|
274
|
+
if (configOverride && configOverride !== 'auto') return configOverride;
|
|
275
|
+
if (!fs.existsSync('package.json')) return null;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
279
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
280
|
+
|
|
281
|
+
if (deps['next']) return 'next';
|
|
282
|
+
if (deps['nuxt']) return 'nuxt';
|
|
283
|
+
if (deps['astro']) return 'astro';
|
|
284
|
+
if (deps['@remix-run/node'] || deps['@remix-run/react']) return 'remix';
|
|
285
|
+
if (deps['svelte'] || deps['@sveltejs/kit']) return 'svelte';
|
|
286
|
+
if (deps['vue']) return 'vue';
|
|
287
|
+
if (deps['react']) return 'react';
|
|
288
|
+
if (deps['express']) return 'express';
|
|
289
|
+
if (deps['fastify']) return 'fastify';
|
|
290
|
+
if (deps['hono']) return 'hono';
|
|
291
|
+
if (deps['elysia']) return 'elysia';
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
300
|
+
// CODING LEVEL
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get coding level style name mapping
|
|
305
|
+
* @param {number} level - Coding level (0-5)
|
|
306
|
+
* @returns {string} Style name
|
|
307
|
+
*/
|
|
308
|
+
function getCodingLevelStyleName(level) {
|
|
309
|
+
const styleMap = {
|
|
310
|
+
0: 'coding-level-0-eli5',
|
|
311
|
+
1: 'coding-level-1-junior',
|
|
312
|
+
2: 'coding-level-2-mid',
|
|
313
|
+
3: 'coding-level-3-senior',
|
|
314
|
+
4: 'coding-level-4-lead',
|
|
315
|
+
5: 'coding-level-5-god'
|
|
316
|
+
};
|
|
317
|
+
return styleMap[level] || 'coding-level-5-god';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get coding level guidelines by reading from output-styles .md files
|
|
322
|
+
* @param {number} level - Coding level (-1 to 5)
|
|
323
|
+
* @param {string} [configDir] - Config directory path
|
|
324
|
+
* @returns {string|null} Guidelines text or null if disabled
|
|
325
|
+
*/
|
|
326
|
+
function getCodingLevelGuidelines(level, configDir) {
|
|
327
|
+
if (level === -1 || level === null || level === undefined) return null;
|
|
328
|
+
|
|
329
|
+
const styleName = getCodingLevelStyleName(level);
|
|
330
|
+
const basePath = configDir || path.join(process.cwd(), '.claude');
|
|
331
|
+
const stylePath = path.join(basePath, 'output-styles', `${styleName}.md`);
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
if (!fs.existsSync(stylePath)) return null;
|
|
335
|
+
const content = fs.readFileSync(stylePath, 'utf8');
|
|
336
|
+
const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n*/, '').trim();
|
|
337
|
+
return withoutFrontmatter;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
344
|
+
// CONTEXT OUTPUT
|
|
345
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Build context summary for output (compact, single line)
|
|
349
|
+
* @param {Object} config - Loaded config
|
|
350
|
+
* @param {Object} detections - Project detections
|
|
351
|
+
* @param {{ path: string|null, resolvedBy: string|null }} resolved - Plan resolution
|
|
352
|
+
* @param {string|null} gitRoot - Git repository root
|
|
353
|
+
* @returns {string}
|
|
354
|
+
*/
|
|
355
|
+
function buildContextOutput(config, detections, resolved, gitRoot) {
|
|
356
|
+
const lines = [`Project: ${detections.type || 'unknown'}`];
|
|
357
|
+
if (detections.pm) lines.push(`PM: ${detections.pm}`);
|
|
358
|
+
lines.push(`Plan naming: ${config.plan.namingFormat}`);
|
|
359
|
+
|
|
360
|
+
if (gitRoot && gitRoot !== process.cwd()) {
|
|
361
|
+
lines.push(`Root: ${gitRoot}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (resolved.path) {
|
|
365
|
+
if (resolved.resolvedBy === 'session') {
|
|
366
|
+
lines.push(`Plan: ${resolved.path}`);
|
|
367
|
+
} else {
|
|
368
|
+
lines.push(`Suggested: ${resolved.path}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return lines.join(' | ');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
376
|
+
// MAIN ENTRY POINT
|
|
377
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Detect all project information
|
|
381
|
+
*
|
|
382
|
+
* @param {Object} [options]
|
|
383
|
+
* @param {Object} [options.configOverrides] - Override auto-detection
|
|
384
|
+
* @returns {{
|
|
385
|
+
* type: 'monorepo' | 'library' | 'single-repo',
|
|
386
|
+
* packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | null,
|
|
387
|
+
* framework: string | null,
|
|
388
|
+
* pythonVersion: string | null,
|
|
389
|
+
* nodeVersion: string,
|
|
390
|
+
* gitBranch: string | null,
|
|
391
|
+
* gitRoot: string | null,
|
|
392
|
+
* gitUrl: string | null,
|
|
393
|
+
* osPlatform: string,
|
|
394
|
+
* user: string,
|
|
395
|
+
* locale: string,
|
|
396
|
+
* timezone: string
|
|
397
|
+
* }}
|
|
398
|
+
*/
|
|
399
|
+
function detectProject(options = {}) {
|
|
400
|
+
const { configOverrides = {} } = options;
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
type: detectProjectType(configOverrides.type),
|
|
404
|
+
packageManager: detectPackageManager(configOverrides.packageManager),
|
|
405
|
+
framework: detectFramework(configOverrides.framework),
|
|
406
|
+
pythonVersion: getPythonVersion(),
|
|
407
|
+
nodeVersion: process.version,
|
|
408
|
+
gitBranch: getGitBranch(),
|
|
409
|
+
gitRoot: getGitRoot(),
|
|
410
|
+
gitUrl: getGitRemoteUrl(),
|
|
411
|
+
osPlatform: process.platform,
|
|
412
|
+
user: process.env.USERNAME || process.env.USER || process.env.LOGNAME || os.userInfo().username,
|
|
413
|
+
locale: process.env.LANG || '',
|
|
414
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Build static environment info object
|
|
420
|
+
* @param {string} [configDir] - Config directory path
|
|
421
|
+
* @returns {Object} Static environment info
|
|
422
|
+
*/
|
|
423
|
+
function buildStaticEnv(configDir) {
|
|
424
|
+
return {
|
|
425
|
+
nodeVersion: process.version,
|
|
426
|
+
pythonVersion: getPythonVersion(),
|
|
427
|
+
osPlatform: process.platform,
|
|
428
|
+
gitUrl: getGitRemoteUrl(),
|
|
429
|
+
gitBranch: getGitBranch(),
|
|
430
|
+
gitRoot: getGitRoot(),
|
|
431
|
+
user: process.env.USERNAME || process.env.USER || process.env.LOGNAME || os.userInfo().username,
|
|
432
|
+
locale: process.env.LANG || '',
|
|
433
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
434
|
+
configDir: configDir || path.join(process.cwd(), '.claude')
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
439
|
+
// EXPORTS
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
441
|
+
|
|
442
|
+
module.exports = {
|
|
443
|
+
// Main entry points
|
|
444
|
+
detectProject,
|
|
445
|
+
buildStaticEnv,
|
|
446
|
+
|
|
447
|
+
// Detection functions
|
|
448
|
+
detectProjectType,
|
|
449
|
+
detectPackageManager,
|
|
450
|
+
detectFramework,
|
|
451
|
+
|
|
452
|
+
// Python detection
|
|
453
|
+
getPythonVersion,
|
|
454
|
+
findPythonBinary,
|
|
455
|
+
getPythonPaths,
|
|
456
|
+
isValidPythonPath,
|
|
457
|
+
|
|
458
|
+
// Git detection
|
|
459
|
+
isGitRepo,
|
|
460
|
+
getGitRemoteUrl,
|
|
461
|
+
getGitBranch,
|
|
462
|
+
getGitRoot,
|
|
463
|
+
|
|
464
|
+
// Coding level
|
|
465
|
+
getCodingLevelStyleName,
|
|
466
|
+
getCodingLevelGuidelines,
|
|
467
|
+
|
|
468
|
+
// Output
|
|
469
|
+
buildContextOutput,
|
|
470
|
+
|
|
471
|
+
// Helpers
|
|
472
|
+
execSafe,
|
|
473
|
+
execFileSafe
|
|
474
|
+
};
|