cc4pm 1.8.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/.claude-plugin/README.md +17 -0
- package/.claude-plugin/plugin.json +25 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/README.zh-CN.md +134 -0
- package/contexts/dev.md +20 -0
- package/contexts/research.md +26 -0
- package/contexts/review.md +22 -0
- package/examples/CLAUDE.md +100 -0
- package/examples/statusline.json +19 -0
- package/examples/user-CLAUDE.md +109 -0
- package/install.sh +17 -0
- package/manifests/install-components.json +173 -0
- package/manifests/install-modules.json +335 -0
- package/manifests/install-profiles.json +75 -0
- package/package.json +117 -0
- package/schemas/ecc-install-config.schema.json +58 -0
- package/schemas/hooks.schema.json +197 -0
- package/schemas/install-components.schema.json +56 -0
- package/schemas/install-modules.schema.json +105 -0
- package/schemas/install-profiles.schema.json +45 -0
- package/schemas/install-state.schema.json +210 -0
- package/schemas/package-manager.schema.json +23 -0
- package/schemas/plugin.schema.json +58 -0
- package/scripts/ci/catalog.js +83 -0
- package/scripts/ci/validate-agents.js +81 -0
- package/scripts/ci/validate-commands.js +135 -0
- package/scripts/ci/validate-hooks.js +239 -0
- package/scripts/ci/validate-install-manifests.js +211 -0
- package/scripts/ci/validate-no-personal-paths.js +63 -0
- package/scripts/ci/validate-rules.js +81 -0
- package/scripts/ci/validate-skills.js +54 -0
- package/scripts/claw.js +468 -0
- package/scripts/doctor.js +110 -0
- package/scripts/ecc.js +194 -0
- package/scripts/hooks/auto-tmux-dev.js +88 -0
- package/scripts/hooks/check-console-log.js +71 -0
- package/scripts/hooks/check-hook-enabled.js +12 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +100 -0
- package/scripts/hooks/insaits-security-monitor.py +269 -0
- package/scripts/hooks/insaits-security-wrapper.js +88 -0
- package/scripts/hooks/post-bash-build-complete.js +27 -0
- package/scripts/hooks/post-bash-pr-created.js +36 -0
- package/scripts/hooks/post-edit-console-warn.js +54 -0
- package/scripts/hooks/post-edit-format.js +109 -0
- package/scripts/hooks/post-edit-typecheck.js +96 -0
- package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
- package/scripts/hooks/pre-compact.js +48 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +168 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +120 -0
- package/scripts/hooks/session-end-marker.js +15 -0
- package/scripts/hooks/session-end.js +299 -0
- package/scripts/hooks/session-start.js +97 -0
- package/scripts/hooks/suggest-compact.js +80 -0
- package/scripts/install-apply.js +137 -0
- package/scripts/install-plan.js +254 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install/apply.js +23 -0
- package/scripts/lib/install/config.js +82 -0
- package/scripts/lib/install/request.js +113 -0
- package/scripts/lib/install/runtime.js +42 -0
- package/scripts/lib/install-executor.js +605 -0
- package/scripts/lib/install-lifecycle.js +763 -0
- package/scripts/lib/install-manifests.js +305 -0
- package/scripts/lib/install-state.js +120 -0
- package/scripts/lib/install-targets/antigravity-project.js +9 -0
- package/scripts/lib/install-targets/claude-home.js +10 -0
- package/scripts/lib/install-targets/codex-home.js +10 -0
- package/scripts/lib/install-targets/cursor-project.js +10 -0
- package/scripts/lib/install-targets/helpers.js +89 -0
- package/scripts/lib/install-targets/opencode-home.js +10 -0
- package/scripts/lib/install-targets/registry.js +64 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.d.ts +119 -0
- package/scripts/lib/package-manager.js +431 -0
- package/scripts/lib/project-detect.js +428 -0
- package/scripts/lib/resolve-formatter.js +185 -0
- package/scripts/lib/session-adapters/canonical-session.js +138 -0
- package/scripts/lib/session-adapters/claude-history.js +149 -0
- package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
- package/scripts/lib/session-adapters/registry.js +111 -0
- package/scripts/lib/session-aliases.d.ts +136 -0
- package/scripts/lib/session-aliases.js +481 -0
- package/scripts/lib/session-manager.d.ts +131 -0
- package/scripts/lib/session-manager.js +464 -0
- package/scripts/lib/shell-split.js +86 -0
- package/scripts/lib/skill-improvement/amendify.js +89 -0
- package/scripts/lib/skill-improvement/evaluate.js +59 -0
- package/scripts/lib/skill-improvement/health.js +118 -0
- package/scripts/lib/skill-improvement/observations.js +108 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
- package/scripts/lib/utils.d.ts +183 -0
- package/scripts/lib/utils.js +543 -0
- package/scripts/list-installed.js +90 -0
- package/scripts/orchestrate-codex-worker.sh +92 -0
- package/scripts/orchestrate-worktrees.js +108 -0
- package/scripts/orchestration-status.js +62 -0
- package/scripts/repair.js +97 -0
- package/scripts/session-inspect.js +150 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/skill-create-output.js +244 -0
- package/scripts/uninstall.js +96 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project type and framework detection
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform (Windows, macOS, Linux) project type detection
|
|
5
|
+
* by inspecting files in the working directory.
|
|
6
|
+
*
|
|
7
|
+
* Resolves: https://github.com/istarwyh/cc4pm/issues/293
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Language detection rules.
|
|
15
|
+
* Each rule checks for marker files or glob patterns in the project root.
|
|
16
|
+
*/
|
|
17
|
+
const LANGUAGE_RULES = [
|
|
18
|
+
{
|
|
19
|
+
type: 'python',
|
|
20
|
+
markers: ['requirements.txt', 'pyproject.toml', 'setup.py', 'setup.cfg', 'Pipfile', 'poetry.lock'],
|
|
21
|
+
extensions: ['.py']
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'typescript',
|
|
25
|
+
markers: ['tsconfig.json', 'tsconfig.build.json'],
|
|
26
|
+
extensions: ['.ts', '.tsx']
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: 'javascript',
|
|
30
|
+
markers: ['package.json', 'jsconfig.json'],
|
|
31
|
+
extensions: ['.js', '.jsx', '.mjs']
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'golang',
|
|
35
|
+
markers: ['go.mod', 'go.sum'],
|
|
36
|
+
extensions: ['.go']
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'rust',
|
|
40
|
+
markers: ['Cargo.toml', 'Cargo.lock'],
|
|
41
|
+
extensions: ['.rs']
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'ruby',
|
|
45
|
+
markers: ['Gemfile', 'Gemfile.lock', 'Rakefile'],
|
|
46
|
+
extensions: ['.rb']
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'java',
|
|
50
|
+
markers: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
51
|
+
extensions: ['.java']
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'csharp',
|
|
55
|
+
markers: [],
|
|
56
|
+
extensions: ['.cs', '.csproj', '.sln']
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'swift',
|
|
60
|
+
markers: ['Package.swift'],
|
|
61
|
+
extensions: ['.swift']
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'kotlin',
|
|
65
|
+
markers: [],
|
|
66
|
+
extensions: ['.kt', '.kts']
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'elixir',
|
|
70
|
+
markers: ['mix.exs'],
|
|
71
|
+
extensions: ['.ex', '.exs']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'php',
|
|
75
|
+
markers: ['composer.json', 'composer.lock'],
|
|
76
|
+
extensions: ['.php']
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Framework detection rules.
|
|
82
|
+
* Checked after language detection for more specific identification.
|
|
83
|
+
*/
|
|
84
|
+
const FRAMEWORK_RULES = [
|
|
85
|
+
// Python frameworks
|
|
86
|
+
{ framework: 'django', language: 'python', markers: ['manage.py'], packageKeys: ['django'] },
|
|
87
|
+
{ framework: 'fastapi', language: 'python', markers: [], packageKeys: ['fastapi'] },
|
|
88
|
+
{ framework: 'flask', language: 'python', markers: [], packageKeys: ['flask'] },
|
|
89
|
+
|
|
90
|
+
// JavaScript/TypeScript frameworks
|
|
91
|
+
{ framework: 'nextjs', language: 'typescript', markers: ['next.config.js', 'next.config.mjs', 'next.config.ts'], packageKeys: ['next'] },
|
|
92
|
+
{ framework: 'react', language: 'typescript', markers: [], packageKeys: ['react'] },
|
|
93
|
+
{ framework: 'vue', language: 'typescript', markers: ['vue.config.js'], packageKeys: ['vue'] },
|
|
94
|
+
{ framework: 'angular', language: 'typescript', markers: ['angular.json'], packageKeys: ['@angular/core'] },
|
|
95
|
+
{ framework: 'svelte', language: 'typescript', markers: ['svelte.config.js'], packageKeys: ['svelte'] },
|
|
96
|
+
{ framework: 'express', language: 'javascript', markers: [], packageKeys: ['express'] },
|
|
97
|
+
{ framework: 'nestjs', language: 'typescript', markers: ['nest-cli.json'], packageKeys: ['@nestjs/core'] },
|
|
98
|
+
{ framework: 'remix', language: 'typescript', markers: [], packageKeys: ['@remix-run/node', '@remix-run/react'] },
|
|
99
|
+
{ framework: 'astro', language: 'typescript', markers: ['astro.config.mjs', 'astro.config.ts'], packageKeys: ['astro'] },
|
|
100
|
+
{ framework: 'nuxt', language: 'typescript', markers: ['nuxt.config.js', 'nuxt.config.ts'], packageKeys: ['nuxt'] },
|
|
101
|
+
{ framework: 'electron', language: 'typescript', markers: [], packageKeys: ['electron'] },
|
|
102
|
+
|
|
103
|
+
// Ruby frameworks
|
|
104
|
+
{ framework: 'rails', language: 'ruby', markers: ['config/routes.rb', 'bin/rails'], packageKeys: [] },
|
|
105
|
+
|
|
106
|
+
// Go frameworks
|
|
107
|
+
{ framework: 'gin', language: 'golang', markers: [], packageKeys: ['github.com/gin-gonic/gin'] },
|
|
108
|
+
{ framework: 'echo', language: 'golang', markers: [], packageKeys: ['github.com/labstack/echo'] },
|
|
109
|
+
|
|
110
|
+
// Rust frameworks
|
|
111
|
+
{ framework: 'actix', language: 'rust', markers: [], packageKeys: ['actix-web'] },
|
|
112
|
+
{ framework: 'axum', language: 'rust', markers: [], packageKeys: ['axum'] },
|
|
113
|
+
|
|
114
|
+
// Java frameworks
|
|
115
|
+
{ framework: 'spring', language: 'java', markers: [], packageKeys: ['spring-boot', 'org.springframework'] },
|
|
116
|
+
|
|
117
|
+
// PHP frameworks
|
|
118
|
+
{ framework: 'laravel', language: 'php', markers: ['artisan'], packageKeys: ['laravel/framework'] },
|
|
119
|
+
{ framework: 'symfony', language: 'php', markers: ['symfony.lock'], packageKeys: ['symfony/framework-bundle'] },
|
|
120
|
+
|
|
121
|
+
// Elixir frameworks
|
|
122
|
+
{ framework: 'phoenix', language: 'elixir', markers: [], packageKeys: ['phoenix'] }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a file exists relative to the project directory
|
|
127
|
+
* @param {string} projectDir - Project root directory
|
|
128
|
+
* @param {string} filePath - Relative file path
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
function fileExists(projectDir, filePath) {
|
|
132
|
+
try {
|
|
133
|
+
return fs.existsSync(path.join(projectDir, filePath));
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if any file with given extension exists in the project root (non-recursive, top-level only)
|
|
141
|
+
* @param {string} projectDir - Project root directory
|
|
142
|
+
* @param {string[]} extensions - File extensions to check
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
function hasFileWithExtension(projectDir, extensions) {
|
|
146
|
+
try {
|
|
147
|
+
const entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
148
|
+
return entries.some(entry => {
|
|
149
|
+
if (!entry.isFile()) return false;
|
|
150
|
+
const ext = path.extname(entry.name);
|
|
151
|
+
return extensions.includes(ext);
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read and parse package.json dependencies
|
|
160
|
+
* @param {string} projectDir - Project root directory
|
|
161
|
+
* @returns {string[]} Array of dependency names
|
|
162
|
+
*/
|
|
163
|
+
function getPackageJsonDeps(projectDir) {
|
|
164
|
+
try {
|
|
165
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
166
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
167
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
168
|
+
return [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})];
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Read requirements.txt or pyproject.toml for Python package names
|
|
176
|
+
* @param {string} projectDir - Project root directory
|
|
177
|
+
* @returns {string[]} Array of dependency names (lowercase)
|
|
178
|
+
*/
|
|
179
|
+
function getPythonDeps(projectDir) {
|
|
180
|
+
const deps = [];
|
|
181
|
+
|
|
182
|
+
// requirements.txt
|
|
183
|
+
try {
|
|
184
|
+
const reqPath = path.join(projectDir, 'requirements.txt');
|
|
185
|
+
if (fs.existsSync(reqPath)) {
|
|
186
|
+
const content = fs.readFileSync(reqPath, 'utf8');
|
|
187
|
+
content.split('\n').forEach(line => {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('-')) {
|
|
190
|
+
const name = trimmed
|
|
191
|
+
.split(/[>=<![;]/)[0]
|
|
192
|
+
.trim()
|
|
193
|
+
.toLowerCase();
|
|
194
|
+
if (name) deps.push(name);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
/* ignore */
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// pyproject.toml — simple extraction of dependency names
|
|
203
|
+
try {
|
|
204
|
+
const tomlPath = path.join(projectDir, 'pyproject.toml');
|
|
205
|
+
if (fs.existsSync(tomlPath)) {
|
|
206
|
+
const content = fs.readFileSync(tomlPath, 'utf8');
|
|
207
|
+
const depMatches = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
|
|
208
|
+
if (depMatches) {
|
|
209
|
+
const block = depMatches[1];
|
|
210
|
+
block.match(/"([^"]+)"/g)?.forEach(m => {
|
|
211
|
+
const name = m
|
|
212
|
+
.replace(/"/g, '')
|
|
213
|
+
.split(/[>=<![;]/)[0]
|
|
214
|
+
.trim()
|
|
215
|
+
.toLowerCase();
|
|
216
|
+
if (name) deps.push(name);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
/* ignore */
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return deps;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Read go.mod for Go module dependencies
|
|
229
|
+
* @param {string} projectDir - Project root directory
|
|
230
|
+
* @returns {string[]} Array of module paths
|
|
231
|
+
*/
|
|
232
|
+
function getGoDeps(projectDir) {
|
|
233
|
+
try {
|
|
234
|
+
const modPath = path.join(projectDir, 'go.mod');
|
|
235
|
+
if (!fs.existsSync(modPath)) return [];
|
|
236
|
+
const content = fs.readFileSync(modPath, 'utf8');
|
|
237
|
+
const deps = [];
|
|
238
|
+
const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
|
|
239
|
+
if (requireBlock) {
|
|
240
|
+
requireBlock[1].split('\n').forEach(line => {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (trimmed && !trimmed.startsWith('//')) {
|
|
243
|
+
const parts = trimmed.split(/\s+/);
|
|
244
|
+
if (parts[0]) deps.push(parts[0]);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return deps;
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Read Cargo.toml for Rust crate dependencies
|
|
256
|
+
* @param {string} projectDir - Project root directory
|
|
257
|
+
* @returns {string[]} Array of crate names
|
|
258
|
+
*/
|
|
259
|
+
function getRustDeps(projectDir) {
|
|
260
|
+
try {
|
|
261
|
+
const cargoPath = path.join(projectDir, 'Cargo.toml');
|
|
262
|
+
if (!fs.existsSync(cargoPath)) return [];
|
|
263
|
+
const content = fs.readFileSync(cargoPath, 'utf8');
|
|
264
|
+
const deps = [];
|
|
265
|
+
// Match [dependencies] and [dev-dependencies] sections
|
|
266
|
+
const sections = content.match(/\[(dev-)?dependencies\]([\s\S]*?)(?=\n\[|$)/g);
|
|
267
|
+
if (sections) {
|
|
268
|
+
sections.forEach(section => {
|
|
269
|
+
section.split('\n').forEach(line => {
|
|
270
|
+
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
|
271
|
+
if (match && !line.startsWith('[')) {
|
|
272
|
+
deps.push(match[1]);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return deps;
|
|
278
|
+
} catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read composer.json for PHP package dependencies
|
|
285
|
+
* @param {string} projectDir - Project root directory
|
|
286
|
+
* @returns {string[]} Array of package names
|
|
287
|
+
*/
|
|
288
|
+
function getComposerDeps(projectDir) {
|
|
289
|
+
try {
|
|
290
|
+
const composerPath = path.join(projectDir, 'composer.json');
|
|
291
|
+
if (!fs.existsSync(composerPath)) return [];
|
|
292
|
+
const composer = JSON.parse(fs.readFileSync(composerPath, 'utf8'));
|
|
293
|
+
return [...Object.keys(composer.require || {}), ...Object.keys(composer['require-dev'] || {})];
|
|
294
|
+
} catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read mix.exs for Elixir dependencies (simple pattern match)
|
|
301
|
+
* @param {string} projectDir - Project root directory
|
|
302
|
+
* @returns {string[]} Array of dependency atom names
|
|
303
|
+
*/
|
|
304
|
+
function getElixirDeps(projectDir) {
|
|
305
|
+
try {
|
|
306
|
+
const mixPath = path.join(projectDir, 'mix.exs');
|
|
307
|
+
if (!fs.existsSync(mixPath)) return [];
|
|
308
|
+
const content = fs.readFileSync(mixPath, 'utf8');
|
|
309
|
+
const deps = [];
|
|
310
|
+
const matches = content.match(/\{:(\w+)/g);
|
|
311
|
+
if (matches) {
|
|
312
|
+
matches.forEach(m => deps.push(m.replace('{:', '')));
|
|
313
|
+
}
|
|
314
|
+
return deps;
|
|
315
|
+
} catch {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Detect project languages and frameworks
|
|
322
|
+
* @param {string} [projectDir] - Project directory (defaults to cwd)
|
|
323
|
+
* @returns {{ languages: string[], frameworks: string[], primary: string, projectDir: string }}
|
|
324
|
+
*/
|
|
325
|
+
function detectProjectType(projectDir) {
|
|
326
|
+
projectDir = projectDir || process.cwd();
|
|
327
|
+
const languages = [];
|
|
328
|
+
const frameworks = [];
|
|
329
|
+
|
|
330
|
+
// Step 1: Detect languages
|
|
331
|
+
for (const rule of LANGUAGE_RULES) {
|
|
332
|
+
const hasMarker = rule.markers.some(m => fileExists(projectDir, m));
|
|
333
|
+
const hasExt = rule.extensions.length > 0 && hasFileWithExtension(projectDir, rule.extensions);
|
|
334
|
+
|
|
335
|
+
if (hasMarker || hasExt) {
|
|
336
|
+
languages.push(rule.type);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Deduplicate: if both typescript and javascript detected, keep typescript
|
|
341
|
+
if (languages.includes('typescript') && languages.includes('javascript')) {
|
|
342
|
+
const idx = languages.indexOf('javascript');
|
|
343
|
+
if (idx !== -1) languages.splice(idx, 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Step 2: Detect frameworks based on markers and dependencies
|
|
347
|
+
const npmDeps = getPackageJsonDeps(projectDir);
|
|
348
|
+
const pyDeps = getPythonDeps(projectDir);
|
|
349
|
+
const goDeps = getGoDeps(projectDir);
|
|
350
|
+
const rustDeps = getRustDeps(projectDir);
|
|
351
|
+
const composerDeps = getComposerDeps(projectDir);
|
|
352
|
+
const elixirDeps = getElixirDeps(projectDir);
|
|
353
|
+
|
|
354
|
+
for (const rule of FRAMEWORK_RULES) {
|
|
355
|
+
// Check marker files
|
|
356
|
+
const hasMarker = rule.markers.some(m => fileExists(projectDir, m));
|
|
357
|
+
|
|
358
|
+
// Check package dependencies
|
|
359
|
+
let hasDep = false;
|
|
360
|
+
if (rule.packageKeys.length > 0) {
|
|
361
|
+
let depList = [];
|
|
362
|
+
switch (rule.language) {
|
|
363
|
+
case 'python':
|
|
364
|
+
depList = pyDeps;
|
|
365
|
+
break;
|
|
366
|
+
case 'typescript':
|
|
367
|
+
case 'javascript':
|
|
368
|
+
depList = npmDeps;
|
|
369
|
+
break;
|
|
370
|
+
case 'golang':
|
|
371
|
+
depList = goDeps;
|
|
372
|
+
break;
|
|
373
|
+
case 'rust':
|
|
374
|
+
depList = rustDeps;
|
|
375
|
+
break;
|
|
376
|
+
case 'php':
|
|
377
|
+
depList = composerDeps;
|
|
378
|
+
break;
|
|
379
|
+
case 'elixir':
|
|
380
|
+
depList = elixirDeps;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
hasDep = rule.packageKeys.some(key => depList.some(dep => dep.toLowerCase().includes(key.toLowerCase())));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (hasMarker || hasDep) {
|
|
387
|
+
frameworks.push(rule.framework);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Step 3: Determine primary type
|
|
392
|
+
let primary = 'unknown';
|
|
393
|
+
if (frameworks.length > 0) {
|
|
394
|
+
primary = frameworks[0];
|
|
395
|
+
} else if (languages.length > 0) {
|
|
396
|
+
primary = languages[0];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Determine if fullstack (both frontend and backend languages)
|
|
400
|
+
const frontendSignals = ['react', 'vue', 'angular', 'svelte', 'nextjs', 'nuxt', 'astro', 'remix'];
|
|
401
|
+
const backendSignals = ['django', 'fastapi', 'flask', 'express', 'nestjs', 'rails', 'spring', 'laravel', 'phoenix', 'gin', 'echo', 'actix', 'axum'];
|
|
402
|
+
const hasFrontend = frameworks.some(f => frontendSignals.includes(f));
|
|
403
|
+
const hasBackend = frameworks.some(f => backendSignals.includes(f));
|
|
404
|
+
|
|
405
|
+
if (hasFrontend && hasBackend) {
|
|
406
|
+
primary = 'fullstack';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
languages,
|
|
411
|
+
frameworks,
|
|
412
|
+
primary,
|
|
413
|
+
projectDir
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
detectProjectType,
|
|
419
|
+
LANGUAGE_RULES,
|
|
420
|
+
FRAMEWORK_RULES,
|
|
421
|
+
// Exported for testing
|
|
422
|
+
getPackageJsonDeps,
|
|
423
|
+
getPythonDeps,
|
|
424
|
+
getGoDeps,
|
|
425
|
+
getRustDeps,
|
|
426
|
+
getComposerDeps,
|
|
427
|
+
getElixirDeps
|
|
428
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatter resolution utilities with caching.
|
|
3
|
+
*
|
|
4
|
+
* Extracts project-root discovery, formatter detection, and binary
|
|
5
|
+
* resolution into a single module so that post-edit-format.js and
|
|
6
|
+
* quality-gate.js avoid duplicating work and filesystem lookups.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// ── Caches (per-process, cleared on next hook invocation) ───────────
|
|
15
|
+
const projectRootCache = new Map();
|
|
16
|
+
const formatterCache = new Map();
|
|
17
|
+
const binCache = new Map();
|
|
18
|
+
|
|
19
|
+
// ── Config file lists (single source of truth) ─────────────────────
|
|
20
|
+
|
|
21
|
+
const BIOME_CONFIGS = ['biome.json', 'biome.jsonc'];
|
|
22
|
+
|
|
23
|
+
const PRETTIER_CONFIGS = [
|
|
24
|
+
'.prettierrc',
|
|
25
|
+
'.prettierrc.json',
|
|
26
|
+
'.prettierrc.js',
|
|
27
|
+
'.prettierrc.cjs',
|
|
28
|
+
'.prettierrc.mjs',
|
|
29
|
+
'.prettierrc.yml',
|
|
30
|
+
'.prettierrc.yaml',
|
|
31
|
+
'.prettierrc.toml',
|
|
32
|
+
'prettier.config.js',
|
|
33
|
+
'prettier.config.cjs',
|
|
34
|
+
'prettier.config.mjs'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const PROJECT_ROOT_MARKERS = ['package.json', ...BIOME_CONFIGS, ...PRETTIER_CONFIGS];
|
|
38
|
+
|
|
39
|
+
// ── Windows .cmd shim mapping ───────────────────────────────────────
|
|
40
|
+
const WIN_CMD_SHIMS = { npx: 'npx.cmd', pnpm: 'pnpm.cmd', yarn: 'yarn.cmd', bunx: 'bunx.cmd' };
|
|
41
|
+
|
|
42
|
+
// ── Formatter → package name mapping ────────────────────────────────
|
|
43
|
+
const FORMATTER_PACKAGES = {
|
|
44
|
+
biome: { binName: 'biome', pkgName: '@biomejs/biome' },
|
|
45
|
+
prettier: { binName: 'prettier', pkgName: 'prettier' }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Public helpers ──────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Walk up from `startDir` until a directory containing a known project
|
|
52
|
+
* root marker (package.json or formatter config) is found.
|
|
53
|
+
* Returns `startDir` as fallback when no marker exists above it.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} startDir - Absolute directory path to start from
|
|
56
|
+
* @returns {string} Absolute path to the project root
|
|
57
|
+
*/
|
|
58
|
+
function findProjectRoot(startDir) {
|
|
59
|
+
if (projectRootCache.has(startDir)) return projectRootCache.get(startDir);
|
|
60
|
+
|
|
61
|
+
let dir = startDir;
|
|
62
|
+
while (dir !== path.dirname(dir)) {
|
|
63
|
+
for (const marker of PROJECT_ROOT_MARKERS) {
|
|
64
|
+
if (fs.existsSync(path.join(dir, marker))) {
|
|
65
|
+
projectRootCache.set(startDir, dir);
|
|
66
|
+
return dir;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
dir = path.dirname(dir);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
projectRootCache.set(startDir, startDir);
|
|
73
|
+
return startDir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detect the formatter configured in the project.
|
|
78
|
+
* Biome takes priority over Prettier.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
81
|
+
* @returns {'biome' | 'prettier' | null}
|
|
82
|
+
*/
|
|
83
|
+
function detectFormatter(projectRoot) {
|
|
84
|
+
if (formatterCache.has(projectRoot)) return formatterCache.get(projectRoot);
|
|
85
|
+
|
|
86
|
+
for (const cfg of BIOME_CONFIGS) {
|
|
87
|
+
if (fs.existsSync(path.join(projectRoot, cfg))) {
|
|
88
|
+
formatterCache.set(projectRoot, 'biome');
|
|
89
|
+
return 'biome';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check package.json "prettier" key before config files
|
|
94
|
+
try {
|
|
95
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
96
|
+
if (fs.existsSync(pkgPath)) {
|
|
97
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
98
|
+
if ('prettier' in pkg) {
|
|
99
|
+
formatterCache.set(projectRoot, 'prettier');
|
|
100
|
+
return 'prettier';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Malformed package.json — continue to file-based detection
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const cfg of PRETTIER_CONFIGS) {
|
|
108
|
+
if (fs.existsSync(path.join(projectRoot, cfg))) {
|
|
109
|
+
formatterCache.set(projectRoot, 'prettier');
|
|
110
|
+
return 'prettier';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
formatterCache.set(projectRoot, null);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the runner binary and prefix args for the configured package
|
|
120
|
+
* manager (respects CLAUDE_PACKAGE_MANAGER env and project config).
|
|
121
|
+
*
|
|
122
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
123
|
+
* @returns {{ bin: string, prefix: string[] }}
|
|
124
|
+
*/
|
|
125
|
+
function getRunnerFromPackageManager(projectRoot) {
|
|
126
|
+
const isWin = process.platform === 'win32';
|
|
127
|
+
const { getPackageManager } = require('./package-manager');
|
|
128
|
+
const pm = getPackageManager({ projectDir: projectRoot });
|
|
129
|
+
const execCmd = pm?.config?.execCmd || 'npx';
|
|
130
|
+
const [rawBin = 'npx', ...prefix] = execCmd.split(/\s+/).filter(Boolean);
|
|
131
|
+
const bin = isWin ? WIN_CMD_SHIMS[rawBin] || rawBin : rawBin;
|
|
132
|
+
return { bin, prefix };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the formatter binary, preferring the local node_modules/.bin
|
|
137
|
+
* installation over the package manager exec command to avoid
|
|
138
|
+
* package-resolution overhead.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
141
|
+
* @param {'biome' | 'prettier'} formatter - Detected formatter name
|
|
142
|
+
* @returns {{ bin: string, prefix: string[] } | null}
|
|
143
|
+
* `bin` – executable path (absolute local path or runner binary)
|
|
144
|
+
* `prefix` – extra args to prepend (e.g. ['@biomejs/biome'] when using npx)
|
|
145
|
+
*/
|
|
146
|
+
function resolveFormatterBin(projectRoot, formatter) {
|
|
147
|
+
const cacheKey = `${projectRoot}:${formatter}`;
|
|
148
|
+
if (binCache.has(cacheKey)) return binCache.get(cacheKey);
|
|
149
|
+
|
|
150
|
+
const pkg = FORMATTER_PACKAGES[formatter];
|
|
151
|
+
if (!pkg) {
|
|
152
|
+
binCache.set(cacheKey, null);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const isWin = process.platform === 'win32';
|
|
157
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', isWin ? `${pkg.binName}.cmd` : pkg.binName);
|
|
158
|
+
|
|
159
|
+
if (fs.existsSync(localBin)) {
|
|
160
|
+
const result = { bin: localBin, prefix: [] };
|
|
161
|
+
binCache.set(cacheKey, result);
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const runner = getRunnerFromPackageManager(projectRoot);
|
|
166
|
+
const result = { bin: runner.bin, prefix: [...runner.prefix, pkg.pkgName] };
|
|
167
|
+
binCache.set(cacheKey, result);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear all caches. Useful for testing.
|
|
173
|
+
*/
|
|
174
|
+
function clearCaches() {
|
|
175
|
+
projectRootCache.clear();
|
|
176
|
+
formatterCache.clear();
|
|
177
|
+
binCache.clear();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
findProjectRoot,
|
|
182
|
+
detectFormatter,
|
|
183
|
+
resolveFormatterBin,
|
|
184
|
+
clearCaches
|
|
185
|
+
};
|