patchdrill 0.1.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/.patchdrill.yml +33 -0
- package/CHANGELOG.md +150 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/SECURITY.md +28 -0
- package/action.yml +338 -0
- package/dist/baseline.d.ts +9 -0
- package/dist/baseline.js +38 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +662 -0
- package/dist/cli.js.map +1 -0
- package/dist/codeowners.d.ts +14 -0
- package/dist/codeowners.js +104 -0
- package/dist/codeowners.js.map +1 -0
- package/dist/command-plan.d.ts +3 -0
- package/dist/command-plan.js +26 -0
- package/dist/command-plan.js.map +1 -0
- package/dist/demo.d.ts +5 -0
- package/dist/demo.js +525 -0
- package/dist/demo.js.map +1 -0
- package/dist/dependency.d.ts +4 -0
- package/dist/dependency.js +1424 -0
- package/dist/dependency.js.map +1 -0
- package/dist/doctor.d.ts +26 -0
- package/dist/doctor.js +183 -0
- package/dist/doctor.js.map +1 -0
- package/dist/evidence.d.ts +64 -0
- package/dist/evidence.js +352 -0
- package/dist/evidence.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.js +349 -0
- package/dist/git.js.map +1 -0
- package/dist/i18n-catalog.d.ts +8 -0
- package/dist/i18n-catalog.js +446 -0
- package/dist/i18n-catalog.js.map +1 -0
- package/dist/i18n.d.ts +20 -0
- package/dist/i18n.js +67 -0
- package/dist/i18n.js.map +1 -0
- package/dist/init.d.ts +13 -0
- package/dist/init.js +312 -0
- package/dist/init.js.map +1 -0
- package/dist/markdown-links.d.ts +18 -0
- package/dist/markdown-links.js +180 -0
- package/dist/markdown-links.js.map +1 -0
- package/dist/package-scripts.d.ts +3 -0
- package/dist/package-scripts.js +55 -0
- package/dist/package-scripts.js.map +1 -0
- package/dist/planner.d.ts +8 -0
- package/dist/planner.js +2351 -0
- package/dist/planner.js.map +1 -0
- package/dist/policy.d.ts +12 -0
- package/dist/policy.js +255 -0
- package/dist/policy.js.map +1 -0
- package/dist/project.d.ts +2 -0
- package/dist/project.js +1085 -0
- package/dist/project.js.map +1 -0
- package/dist/release-readiness.d.ts +25 -0
- package/dist/release-readiness.js +426 -0
- package/dist/release-readiness.js.map +1 -0
- package/dist/report-annotations.d.ts +3 -0
- package/dist/report-annotations.js +28 -0
- package/dist/report-annotations.js.map +1 -0
- package/dist/report-contract.d.ts +2 -0
- package/dist/report-contract.js +82 -0
- package/dist/report-contract.js.map +1 -0
- package/dist/report-html.d.ts +7 -0
- package/dist/report-html.js +706 -0
- package/dist/report-html.js.map +1 -0
- package/dist/report-sarif.d.ts +2 -0
- package/dist/report-sarif.js +90 -0
- package/dist/report-sarif.js.map +1 -0
- package/dist/report.d.ts +14 -0
- package/dist/report.js +310 -0
- package/dist/report.js.map +1 -0
- package/dist/risk.d.ts +19 -0
- package/dist/risk.js +1226 -0
- package/dist/risk.js.map +1 -0
- package/dist/runner.d.ts +8 -0
- package/dist/runner.js +113 -0
- package/dist/runner.js.map +1 -0
- package/dist/scan.d.ts +2 -0
- package/dist/scan.js +195 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.js +30 -0
- package/dist/schema.js.map +1 -0
- package/dist/stack-coverage.d.ts +8 -0
- package/dist/stack-coverage.js +94 -0
- package/dist/stack-coverage.js.map +1 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verification.d.ts +11 -0
- package/dist/verification.js +108 -0
- package/dist/verification.js.map +1 -0
- package/docs/ANNOTATIONS.md +34 -0
- package/docs/ARCHITECTURE.md +79 -0
- package/docs/BASELINES.md +32 -0
- package/docs/CASE_STUDIES.md +106 -0
- package/docs/CODEOWNERS.md +23 -0
- package/docs/DASHBOARD.md +87 -0
- package/docs/EVIDENCE.md +55 -0
- package/docs/LAUNCH_PLAYBOOK.md +103 -0
- package/docs/MONOREPOS.md +74 -0
- package/docs/POLICY.md +98 -0
- package/docs/PROOF_PACKS.md +57 -0
- package/docs/PR_COMMENTS.md +56 -0
- package/docs/RELEASE.md +35 -0
- package/docs/ROADMAP.md +152 -0
- package/docs/RULE_CATALOG.md +90 -0
- package/docs/SARIF.md +74 -0
- package/docs/SCHEMAS.md +49 -0
- package/docs/SECURITY_POSTURE.md +32 -0
- package/docs/STACK_COVERAGE.md +20 -0
- package/docs/assets/patchdrill-demo.svg +21 -0
- package/docs/media/patchdrill-dashboard.png +0 -0
- package/docs/media/patchdrill-demo.gif +0 -0
- package/examples/case-studies/README.md +20 -0
- package/examples/demo/README.md +21 -0
- package/examples/demo/patchdrill-demo-summary.md +35 -0
- package/examples/demo/patchdrill-demo.html +623 -0
- package/examples/demo/patchdrill-demo.json +355 -0
- package/examples/demo/patchdrill-demo.md +120 -0
- package/examples/demo/patchdrill-demo.sarif +195 -0
- package/examples/report.md +128 -0
- package/examples/risky-agent-pr/README.md +15 -0
- package/examples/risky-agent-pr/patchdrill-demo-summary.md +41 -0
- package/examples/risky-agent-pr/patchdrill-demo.html +681 -0
- package/examples/risky-agent-pr/patchdrill-demo.json +483 -0
- package/examples/risky-agent-pr/patchdrill-demo.md +140 -0
- package/examples/risky-agent-pr/patchdrill-demo.sarif +398 -0
- package/fixtures/stacks/README.md +4 -0
- package/fixtures/stacks/android-gradle/fixture.json +33 -0
- package/fixtures/stacks/aspnet-core-service/fixture.json +36 -0
- package/fixtures/stacks/bazel-workspace/fixture.json +30 -0
- package/fixtures/stacks/buck2-workspace/fixture.json +30 -0
- package/fixtures/stacks/cargo-workspace/fixture.json +48 -0
- package/fixtures/stacks/django-app/fixture.json +25 -0
- package/fixtures/stacks/docker-compose/fixture.json +17 -0
- package/fixtures/stacks/dockerfile-service/fixture.json +17 -0
- package/fixtures/stacks/dotnet-service/fixture.json +36 -0
- package/fixtures/stacks/dotnet-solution-filter/fixture.json +62 -0
- package/fixtures/stacks/fastapi-app/fixture.json +29 -0
- package/fixtures/stacks/go-workspace/fixture.json +48 -0
- package/fixtures/stacks/java-gradle/fixture.json +29 -0
- package/fixtures/stacks/java-maven/fixture.json +32 -0
- package/fixtures/stacks/kubernetes-helm/fixture.json +25 -0
- package/fixtures/stacks/kubernetes-kustomize/fixture.json +21 -0
- package/fixtures/stacks/nested-go-workspace/fixture.json +51 -0
- package/fixtures/stacks/nextjs-app/fixture.json +34 -0
- package/fixtures/stacks/node-turbo-workspace/fixture.json +39 -0
- package/fixtures/stacks/pants-python/fixture.json +33 -0
- package/fixtures/stacks/php-composer/fixture.json +31 -0
- package/fixtures/stacks/python-service/fixture.json +21 -0
- package/fixtures/stacks/rails-app/fixture.json +25 -0
- package/fixtures/stacks/spring-boot-gradle/fixture.json +29 -0
- package/fixtures/stacks/spring-boot-maven/fixture.json +43 -0
- package/fixtures/stacks/swift-package/fixture.json +21 -0
- package/fixtures/stacks/terraform-module/fixture.json +17 -0
- package/fixtures/stacks/uv-python-service/fixture.json +47 -0
- package/fixtures/stacks/xcode-app/fixture.json +72 -0
- package/package.json +80 -0
- package/schemas/patchdrill-doctor.schema.json +171 -0
- package/schemas/patchdrill-evidence.schema.json +239 -0
- package/schemas/patchdrill-policy.schema.json +170 -0
- package/schemas/patchdrill-release-check.schema.json +78 -0
- package/schemas/patchdrill-report.schema.json +647 -0
package/dist/project.js
ADDED
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
const PYTHON_MANIFEST_NAMES = ["pyproject.toml", "uv.lock", "requirements.txt", "setup.py", "setup.cfg", "manage.py"];
|
|
5
|
+
const NESTED_PROJECT_MAX_DEPTH = 5;
|
|
6
|
+
export function discoverProjectSignals(root) {
|
|
7
|
+
const signals = [];
|
|
8
|
+
const add = (signal) => signals.push(signal);
|
|
9
|
+
if (exists(root, "package.json")) {
|
|
10
|
+
const packageJson = readPackageJson(root);
|
|
11
|
+
const taskRunner = detectNodeTaskRunner(root, packageJson);
|
|
12
|
+
add({
|
|
13
|
+
ecosystem: "node",
|
|
14
|
+
manifestPath: "package.json",
|
|
15
|
+
packageManager: detectNodePackageManager(root),
|
|
16
|
+
...(taskRunner ? { taskRunner } : {}),
|
|
17
|
+
scripts: packageJson.scripts ?? {},
|
|
18
|
+
workspacePackages: discoverNodeWorkspacePackages(root)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const pythonManifestPath = firstExisting(root, PYTHON_MANIFEST_NAMES);
|
|
22
|
+
if (pythonManifestPath) {
|
|
23
|
+
const framework = detectPythonFramework(root);
|
|
24
|
+
const entrypoint = detectPythonEntrypoint(root, framework);
|
|
25
|
+
add({
|
|
26
|
+
ecosystem: "python",
|
|
27
|
+
manifestPath: pythonManifestPath,
|
|
28
|
+
...(framework ? { framework } : {}),
|
|
29
|
+
...(entrypoint ? { entrypoint } : {})
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const signal of discoverNestedPythonSignals(root))
|
|
33
|
+
add(signal);
|
|
34
|
+
if (exists(root, "Cargo.toml")) {
|
|
35
|
+
add({
|
|
36
|
+
ecosystem: "rust",
|
|
37
|
+
manifestPath: "Cargo.toml",
|
|
38
|
+
workspacePackages: discoverCargoWorkspacePackages(root, "Cargo.toml")
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
for (const signal of discoverNestedRustSignals(root))
|
|
42
|
+
add(signal);
|
|
43
|
+
const goManifestPath = firstExisting(root, ["go.work", "go.mod"]);
|
|
44
|
+
if (goManifestPath) {
|
|
45
|
+
add({
|
|
46
|
+
ecosystem: "go",
|
|
47
|
+
manifestPath: goManifestPath,
|
|
48
|
+
workspacePackages: discoverGoWorkspacePackages(root, goManifestPath)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const signal of discoverNestedGoSignals(root))
|
|
52
|
+
add(signal);
|
|
53
|
+
const androidManifestPath = findAndroidManifestPath(root);
|
|
54
|
+
if (androidManifestPath) {
|
|
55
|
+
add({
|
|
56
|
+
ecosystem: "android",
|
|
57
|
+
manifestPath: androidManifestPath
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const javaManifestPath = firstExisting(root, ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"]);
|
|
61
|
+
if (javaManifestPath && !androidManifestPath) {
|
|
62
|
+
const framework = detectJavaFramework(root);
|
|
63
|
+
add({
|
|
64
|
+
ecosystem: "java",
|
|
65
|
+
manifestPath: javaManifestPath,
|
|
66
|
+
...(framework ? { framework } : {})
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (exists(root, "Gemfile")) {
|
|
70
|
+
const framework = detectRubyFramework(root);
|
|
71
|
+
add({
|
|
72
|
+
ecosystem: "ruby",
|
|
73
|
+
manifestPath: "Gemfile",
|
|
74
|
+
...(framework ? { framework } : {})
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (exists(root, "composer.json")) {
|
|
78
|
+
const framework = detectPhpFramework(root);
|
|
79
|
+
const scripts = readComposerScripts(root);
|
|
80
|
+
add({
|
|
81
|
+
ecosystem: "php",
|
|
82
|
+
manifestPath: "composer.json",
|
|
83
|
+
...(framework ? { framework } : {}),
|
|
84
|
+
...(Object.keys(scripts).length > 0 ? { scripts } : {})
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const dotnetManifestPath = findDotnetManifestPath(root);
|
|
88
|
+
if (dotnetManifestPath) {
|
|
89
|
+
const framework = detectDotnetFramework(root);
|
|
90
|
+
add({
|
|
91
|
+
ecosystem: "dotnet",
|
|
92
|
+
manifestPath: dotnetManifestPath,
|
|
93
|
+
...(framework ? { framework } : {})
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (exists(root, "Package.swift"))
|
|
97
|
+
add({ ecosystem: "swift", manifestPath: "Package.swift" });
|
|
98
|
+
const xcodeManifestPath = findXcodeManifestPath(root);
|
|
99
|
+
if (xcodeManifestPath)
|
|
100
|
+
add({ ecosystem: "xcode", manifestPath: xcodeManifestPath });
|
|
101
|
+
const dockerManifestPath = findDockerManifestPath(root);
|
|
102
|
+
if (dockerManifestPath) {
|
|
103
|
+
add({
|
|
104
|
+
ecosystem: "docker",
|
|
105
|
+
manifestPath: dockerManifestPath
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (exists(root, "pants.toml"))
|
|
109
|
+
add({ ecosystem: "pants", manifestPath: "pants.toml" });
|
|
110
|
+
const bazelManifestPath = findBazelManifestPath(root);
|
|
111
|
+
if (bazelManifestPath)
|
|
112
|
+
add({ ecosystem: "bazel", manifestPath: bazelManifestPath });
|
|
113
|
+
const buckManifestPath = findBuckManifestPath(root);
|
|
114
|
+
if (buckManifestPath)
|
|
115
|
+
add({ ecosystem: "buck", manifestPath: buckManifestPath });
|
|
116
|
+
if (hasTerraform(root))
|
|
117
|
+
add({ ecosystem: "terraform", manifestPath: "*.tf" });
|
|
118
|
+
const kubernetesManifestPath = findKubernetesManifestPath(root);
|
|
119
|
+
if (kubernetesManifestPath)
|
|
120
|
+
add({ ecosystem: "kubernetes", manifestPath: kubernetesManifestPath });
|
|
121
|
+
if (exists(root, ".github/workflows"))
|
|
122
|
+
add({ ecosystem: "github-actions", manifestPath: ".github/workflows" });
|
|
123
|
+
return signals;
|
|
124
|
+
}
|
|
125
|
+
function exists(root, relativePath) {
|
|
126
|
+
return existsSync(join(root, relativePath));
|
|
127
|
+
}
|
|
128
|
+
function firstExisting(root, candidates) {
|
|
129
|
+
return candidates.find((candidate) => exists(root, candidate));
|
|
130
|
+
}
|
|
131
|
+
function detectNodePackageManager(root) {
|
|
132
|
+
if (exists(root, "pnpm-lock.yaml"))
|
|
133
|
+
return "pnpm";
|
|
134
|
+
if (exists(root, "yarn.lock"))
|
|
135
|
+
return "yarn";
|
|
136
|
+
if (exists(root, "bun.lockb") || exists(root, "bun.lock"))
|
|
137
|
+
return "bun";
|
|
138
|
+
return "npm";
|
|
139
|
+
}
|
|
140
|
+
function readPackageJson(root) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function detectPythonFramework(root) {
|
|
150
|
+
if (exists(root, "manage.py"))
|
|
151
|
+
return "django";
|
|
152
|
+
if (pythonDependencyDeclared(root, "django"))
|
|
153
|
+
return "django";
|
|
154
|
+
if (pythonDependencyDeclared(root, "fastapi"))
|
|
155
|
+
return "fastapi";
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function detectPythonEntrypoint(root, framework) {
|
|
159
|
+
if (framework === "fastapi")
|
|
160
|
+
return findFastApiEntrypoint(root);
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
function findFastApiEntrypoint(root) {
|
|
164
|
+
for (const path of ["main.py", "app/main.py", "src/main.py", "src/app/main.py"]) {
|
|
165
|
+
const entrypoint = parseFastApiEntrypoint(root, path);
|
|
166
|
+
if (entrypoint)
|
|
167
|
+
return entrypoint;
|
|
168
|
+
}
|
|
169
|
+
const nestedPath = findFileWithExtensionMentioning(root, ".py", ["FastAPI("], 4);
|
|
170
|
+
return nestedPath ? parseFastApiEntrypoint(root, nestedPath) : undefined;
|
|
171
|
+
}
|
|
172
|
+
function parseFastApiEntrypoint(root, path) {
|
|
173
|
+
try {
|
|
174
|
+
const content = readFileSync(join(root, path), "utf8");
|
|
175
|
+
const match = /(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*FastAPI\s*\(/.exec(content);
|
|
176
|
+
if (!match?.[1])
|
|
177
|
+
return undefined;
|
|
178
|
+
const moduleName = pythonModuleName(path);
|
|
179
|
+
if (!moduleName)
|
|
180
|
+
return undefined;
|
|
181
|
+
return `${moduleName}:${match[1]}`;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function pythonModuleName(path) {
|
|
188
|
+
const withoutSourceRoot = path.startsWith("src/") ? path.slice("src/".length) : path;
|
|
189
|
+
const moduleName = withoutSourceRoot.replace(/\.py$/, "").replaceAll("/", ".");
|
|
190
|
+
return moduleName.split(".").every(isPythonIdentifier) ? moduleName : undefined;
|
|
191
|
+
}
|
|
192
|
+
function isPythonIdentifier(value) {
|
|
193
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
194
|
+
}
|
|
195
|
+
function pythonDependencyDeclared(root, packageName) {
|
|
196
|
+
return ["pyproject.toml", "requirements.txt", "setup.py", "setup.cfg"].some((path) => pythonManifestMentionsPackage(root, path, packageName));
|
|
197
|
+
}
|
|
198
|
+
function pythonManifestMentionsPackage(root, path, packageName) {
|
|
199
|
+
try {
|
|
200
|
+
const content = readFileSync(join(root, path), "utf8");
|
|
201
|
+
const normalizedName = packageName.toLowerCase().replaceAll("_", "-");
|
|
202
|
+
const searchable = content
|
|
203
|
+
.split(/\r?\n/)
|
|
204
|
+
.map((line) => line.replace(/#.*/, "").trim().toLowerCase())
|
|
205
|
+
.join("\n")
|
|
206
|
+
.replaceAll("_", "-");
|
|
207
|
+
const escapedName = escapeRegExp(normalizedName);
|
|
208
|
+
const dependencyPattern = new RegExp(`(^|[^a-z0-9.-])${escapedName}(?:\\[[^\\]]+\\])?\\s*($|[<>=!~;,\\]"'])`, "i");
|
|
209
|
+
return dependencyPattern.test(searchable);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function detectJavaFramework(root) {
|
|
216
|
+
if (javaManifestMentions(root, ["org.springframework.boot", "spring-boot-starter"]))
|
|
217
|
+
return "spring-boot";
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
function detectRubyFramework(root) {
|
|
221
|
+
if (exists(root, "config/application.rb"))
|
|
222
|
+
return "rails";
|
|
223
|
+
if (rubyManifestMentions(root, ["gem \"rails\"", "gem 'rails'", " rails ("]))
|
|
224
|
+
return "rails";
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
function detectPhpFramework(root) {
|
|
228
|
+
if (exists(root, "artisan"))
|
|
229
|
+
return "laravel";
|
|
230
|
+
if (composerManifestMentions(root, ["laravel/framework"]))
|
|
231
|
+
return "laravel";
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
function detectDotnetFramework(root) {
|
|
235
|
+
if (findFileWithExtensionMentioning(root, ".csproj", ["Microsoft.NET.Sdk.Web", "Microsoft.AspNetCore"], 4))
|
|
236
|
+
return "aspnet-core";
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
function rubyManifestMentions(root, needles) {
|
|
240
|
+
return ["Gemfile", "Gemfile.lock"].some((path) => {
|
|
241
|
+
try {
|
|
242
|
+
const content = readFileSync(join(root, path), "utf8").toLowerCase();
|
|
243
|
+
return needles.some((needle) => content.includes(needle.toLowerCase()));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function composerManifestMentions(root, needles) {
|
|
251
|
+
try {
|
|
252
|
+
const content = readFileSync(join(root, "composer.json"), "utf8").toLowerCase();
|
|
253
|
+
return needles.some((needle) => content.includes(needle.toLowerCase()));
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function readComposerScripts(root) {
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(readFileSync(join(root, "composer.json"), "utf8"));
|
|
262
|
+
if (!isRecord(parsed.scripts))
|
|
263
|
+
return {};
|
|
264
|
+
const scripts = {};
|
|
265
|
+
for (const [name, value] of Object.entries(parsed.scripts)) {
|
|
266
|
+
if (typeof value === "string")
|
|
267
|
+
scripts[name] = value;
|
|
268
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
|
269
|
+
scripts[name] = value.join(" && ");
|
|
270
|
+
}
|
|
271
|
+
return scripts;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function findDotnetManifestPath(root) {
|
|
278
|
+
return firstExisting(root, ["global.json"]) ?? findFileWithExtension(root, ".slnf", 2) ?? findFileWithExtension(root, ".sln", 2) ?? findFileWithExtension(root, ".csproj", 3);
|
|
279
|
+
}
|
|
280
|
+
function findAndroidManifestPath(root) {
|
|
281
|
+
const directBuildFile = ["settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts", "app/build.gradle", "app/build.gradle.kts"].find((path) => androidManifestMentions(root, path));
|
|
282
|
+
if (directBuildFile)
|
|
283
|
+
return directBuildFile;
|
|
284
|
+
const nestedBuildFile = findFileMentioning(root, ["build.gradle", "build.gradle.kts"], ["com.android.application", "com.android.library"], 3);
|
|
285
|
+
if (nestedBuildFile)
|
|
286
|
+
return nestedBuildFile;
|
|
287
|
+
if (hasFileNamed(root, "AndroidManifest.xml", 5))
|
|
288
|
+
return "AndroidManifest.xml";
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
function findXcodeManifestPath(root) {
|
|
292
|
+
return findDirectoryWithExtension(root, ".xcworkspace", 3) ?? findDirectoryWithExtension(root, ".xcodeproj", 3);
|
|
293
|
+
}
|
|
294
|
+
function findDockerManifestPath(root) {
|
|
295
|
+
const candidates = ["Dockerfile", "compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
296
|
+
const direct = firstExisting(root, candidates);
|
|
297
|
+
if (direct)
|
|
298
|
+
return direct;
|
|
299
|
+
for (const candidate of candidates) {
|
|
300
|
+
const match = findFilesNamed(root, [candidate], 4)[0];
|
|
301
|
+
if (match)
|
|
302
|
+
return match;
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
function androidManifestMentions(root, path) {
|
|
307
|
+
try {
|
|
308
|
+
const content = readFileSync(join(root, path), "utf8").toLowerCase();
|
|
309
|
+
return content.includes("com.android.application") || content.includes("com.android.library") || content.includes("com.android.tools.build:gradle");
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function javaManifestMentions(root, needles) {
|
|
316
|
+
return ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "gradle/libs.versions.toml"].some((path) => {
|
|
317
|
+
try {
|
|
318
|
+
const content = readFileSync(join(root, path), "utf8").toLowerCase();
|
|
319
|
+
return needles.some((needle) => content.includes(needle.toLowerCase()));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function detectNodeTaskRunner(root, manifest) {
|
|
327
|
+
if (exists(root, "turbo.json"))
|
|
328
|
+
return "turbo";
|
|
329
|
+
if (exists(root, "nx.json"))
|
|
330
|
+
return "nx";
|
|
331
|
+
if (hasPackageDependency(manifest, "turbo"))
|
|
332
|
+
return "turbo";
|
|
333
|
+
if (hasPackageDependency(manifest, "nx"))
|
|
334
|
+
return "nx";
|
|
335
|
+
if (scriptsMention(manifest.scripts, "turbo"))
|
|
336
|
+
return "turbo";
|
|
337
|
+
if (scriptsMention(manifest.scripts, "nx"))
|
|
338
|
+
return "nx";
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
function hasPackageDependency(manifest, packageName) {
|
|
342
|
+
return [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies, manifest.optionalDependencies].some((section) => Boolean(section?.[packageName]));
|
|
343
|
+
}
|
|
344
|
+
function scriptsMention(scripts, commandName) {
|
|
345
|
+
if (!scripts)
|
|
346
|
+
return false;
|
|
347
|
+
const pattern = new RegExp(`(^|[\\s&|;(])${escapeRegExp(commandName)}(\\s|$)`);
|
|
348
|
+
return Object.values(scripts).some((script) => pattern.test(script));
|
|
349
|
+
}
|
|
350
|
+
function discoverNodeWorkspacePackages(root) {
|
|
351
|
+
const patterns = readWorkspacePatterns(root);
|
|
352
|
+
if (patterns.length === 0)
|
|
353
|
+
return [];
|
|
354
|
+
const packages = new Map();
|
|
355
|
+
for (const pattern of patterns) {
|
|
356
|
+
for (const packagePath of expandWorkspacePattern(root, pattern)) {
|
|
357
|
+
const manifest = readPackageJson(join(root, packagePath));
|
|
358
|
+
if (!manifest.name)
|
|
359
|
+
continue;
|
|
360
|
+
const projectMetadata = readProjectMetadata(join(root, packagePath));
|
|
361
|
+
const workspacePackage = {
|
|
362
|
+
name: manifest.name,
|
|
363
|
+
...(projectMetadata.name && projectMetadata.name !== manifest.name ? { projectName: projectMetadata.name } : {}),
|
|
364
|
+
path: packagePath,
|
|
365
|
+
scripts: manifest.scripts ?? {},
|
|
366
|
+
...(projectMetadata.targets.length > 0 ? { targets: projectMetadata.targets } : {})
|
|
367
|
+
};
|
|
368
|
+
const dependencies = readPackageDependencyNames(manifest);
|
|
369
|
+
if (dependencies.length > 0)
|
|
370
|
+
workspacePackage.dependencies = dependencies;
|
|
371
|
+
packages.set(packagePath, workspacePackage);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const workspaceNames = new Set([...packages.values()].map((workspacePackage) => workspacePackage.name));
|
|
375
|
+
return [...packages.values()]
|
|
376
|
+
.map((workspacePackage) => {
|
|
377
|
+
const dependencies = workspacePackage.dependencies?.filter((dependency) => workspaceNames.has(dependency)) ?? [];
|
|
378
|
+
if (dependencies.length === 0) {
|
|
379
|
+
const { dependencies: _dependencies, ...withoutDependencies } = workspacePackage;
|
|
380
|
+
return withoutDependencies;
|
|
381
|
+
}
|
|
382
|
+
return { ...workspacePackage, dependencies };
|
|
383
|
+
})
|
|
384
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
385
|
+
}
|
|
386
|
+
function readProjectMetadata(packageRoot) {
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, "project.json"), "utf8"));
|
|
389
|
+
const targets = isRecord(parsed.targets) ? Object.keys(parsed.targets).sort() : [];
|
|
390
|
+
return {
|
|
391
|
+
...(typeof parsed.name === "string" ? { name: parsed.name } : {}),
|
|
392
|
+
targets
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return { targets: [] };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function discoverNestedPythonSignals(root) {
|
|
400
|
+
const selected = new Map();
|
|
401
|
+
for (const manifestPath of findFilesNamed(root, PYTHON_MANIFEST_NAMES, NESTED_PROJECT_MAX_DEPTH)) {
|
|
402
|
+
const projectRoot = parentPath(manifestPath) || ".";
|
|
403
|
+
if (projectRoot === ".")
|
|
404
|
+
continue;
|
|
405
|
+
const existing = selected.get(projectRoot);
|
|
406
|
+
if (!existing || manifestPriority(manifestPath, PYTHON_MANIFEST_NAMES) < manifestPriority(existing, PYTHON_MANIFEST_NAMES)) {
|
|
407
|
+
selected.set(projectRoot, manifestPath);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return [...selected.entries()]
|
|
411
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
412
|
+
.map(([projectRoot, manifestPath]) => {
|
|
413
|
+
const packageRoot = join(root, projectRoot);
|
|
414
|
+
const framework = detectPythonFramework(packageRoot);
|
|
415
|
+
const entrypoint = detectPythonEntrypoint(packageRoot, framework);
|
|
416
|
+
return {
|
|
417
|
+
ecosystem: "python",
|
|
418
|
+
manifestPath,
|
|
419
|
+
...(framework ? { framework } : {}),
|
|
420
|
+
...(entrypoint ? { entrypoint } : {})
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function discoverNestedRustSignals(root) {
|
|
425
|
+
const rootWorkspacePackageRoots = exists(root, "Cargo.toml")
|
|
426
|
+
? discoverCargoWorkspacePackages(root, "Cargo.toml").map((workspacePackage) => workspacePackage.path)
|
|
427
|
+
: [];
|
|
428
|
+
const manifests = findFilesNamed(root, ["Cargo.toml"], NESTED_PROJECT_MAX_DEPTH).filter((manifestPath) => {
|
|
429
|
+
const projectRoot = parentPath(manifestPath);
|
|
430
|
+
if (projectRoot === "")
|
|
431
|
+
return false;
|
|
432
|
+
return !rootWorkspacePackageRoots.some((packageRoot) => projectRoot === packageRoot || projectRoot.startsWith(`${packageRoot}/`));
|
|
433
|
+
});
|
|
434
|
+
const workspaceManifests = manifests.filter((manifestPath) => readCargoWorkspaceMembers(readText(root, manifestPath)).length > 0);
|
|
435
|
+
const workspaceRoots = workspaceManifests.map((manifestPath) => parentPath(manifestPath)).sort((a, b) => a.localeCompare(b));
|
|
436
|
+
const selected = new Set();
|
|
437
|
+
for (const manifestPath of workspaceManifests)
|
|
438
|
+
selected.add(manifestPath);
|
|
439
|
+
for (const manifestPath of manifests) {
|
|
440
|
+
if (selected.has(manifestPath))
|
|
441
|
+
continue;
|
|
442
|
+
const projectRoot = parentPath(manifestPath);
|
|
443
|
+
if (workspaceRoots.some((workspaceRoot) => projectRoot.startsWith(`${workspaceRoot}/`)))
|
|
444
|
+
continue;
|
|
445
|
+
selected.add(manifestPath);
|
|
446
|
+
}
|
|
447
|
+
return [...selected]
|
|
448
|
+
.sort((a, b) => a.localeCompare(b))
|
|
449
|
+
.map((manifestPath) => ({
|
|
450
|
+
ecosystem: "rust",
|
|
451
|
+
manifestPath,
|
|
452
|
+
workspacePackages: discoverCargoWorkspacePackages(root, manifestPath)
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
455
|
+
function discoverNestedGoSignals(root) {
|
|
456
|
+
const rootWorkspaceModuleRoots = exists(root, "go.work")
|
|
457
|
+
? discoverGoWorkspacePackages(root, "go.work").map((workspacePackage) => workspacePackage.path)
|
|
458
|
+
: [];
|
|
459
|
+
const manifests = findFilesNamed(root, ["go.work", "go.mod"], NESTED_PROJECT_MAX_DEPTH).filter((manifestPath) => {
|
|
460
|
+
const projectRoot = parentPath(manifestPath);
|
|
461
|
+
if (projectRoot === "")
|
|
462
|
+
return false;
|
|
463
|
+
return !rootWorkspaceModuleRoots.some((moduleRoot) => projectRoot === moduleRoot || projectRoot.startsWith(`${moduleRoot}/`));
|
|
464
|
+
});
|
|
465
|
+
const workspaceManifests = manifests.filter((manifestPath) => fileName(manifestPath) === "go.work");
|
|
466
|
+
const workspaceRoots = workspaceManifests.map((manifestPath) => parentPath(manifestPath)).sort((a, b) => a.localeCompare(b));
|
|
467
|
+
const selected = new Set();
|
|
468
|
+
for (const manifestPath of workspaceManifests)
|
|
469
|
+
selected.add(manifestPath);
|
|
470
|
+
for (const manifestPath of manifests) {
|
|
471
|
+
if (selected.has(manifestPath) || fileName(manifestPath) !== "go.mod")
|
|
472
|
+
continue;
|
|
473
|
+
const projectRoot = parentPath(manifestPath);
|
|
474
|
+
// A go.work with `use .` places the root module at the workspace root itself,
|
|
475
|
+
// so exclude an equal projectRoot too — otherwise it is double-listed as both a
|
|
476
|
+
// workspace member and a standalone go signal.
|
|
477
|
+
if (workspaceRoots.some((workspaceRoot) => projectRoot === workspaceRoot || projectRoot.startsWith(`${workspaceRoot}/`)))
|
|
478
|
+
continue;
|
|
479
|
+
selected.add(manifestPath);
|
|
480
|
+
}
|
|
481
|
+
return [...selected]
|
|
482
|
+
.sort((a, b) => a.localeCompare(b))
|
|
483
|
+
.map((manifestPath) => ({
|
|
484
|
+
ecosystem: "go",
|
|
485
|
+
manifestPath,
|
|
486
|
+
workspacePackages: fileName(manifestPath) === "go.work" ? discoverGoWorkspacePackages(root, manifestPath) : []
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
function discoverCargoWorkspacePackages(root, manifestPath) {
|
|
490
|
+
let rootManifest;
|
|
491
|
+
try {
|
|
492
|
+
rootManifest = readFileSync(join(root, manifestPath), "utf8");
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
const members = readCargoWorkspaceMembers(rootManifest);
|
|
498
|
+
if (members.length === 0)
|
|
499
|
+
return [];
|
|
500
|
+
const packages = new Map();
|
|
501
|
+
const workspaceRoot = parentPath(manifestPath) || ".";
|
|
502
|
+
const absoluteWorkspaceRoot = workspaceRoot === "." ? root : join(root, workspaceRoot);
|
|
503
|
+
for (const pattern of members) {
|
|
504
|
+
for (const packagePath of expandWorkspacePattern(absoluteWorkspaceRoot, pattern, "Cargo.toml")) {
|
|
505
|
+
const repoPackagePath = joinRepoPath(workspaceRoot, packagePath);
|
|
506
|
+
const manifest = readCargoManifest(join(root, repoPackagePath));
|
|
507
|
+
if (!manifest.name)
|
|
508
|
+
continue;
|
|
509
|
+
const workspacePackage = {
|
|
510
|
+
name: manifest.name,
|
|
511
|
+
path: repoPackagePath,
|
|
512
|
+
scripts: {}
|
|
513
|
+
};
|
|
514
|
+
if (manifest.dependencies.length > 0)
|
|
515
|
+
workspacePackage.dependencies = manifest.dependencies;
|
|
516
|
+
packages.set(repoPackagePath, workspacePackage);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const workspaceNames = new Set([...packages.values()].map((workspacePackage) => workspacePackage.name));
|
|
520
|
+
return [...packages.values()]
|
|
521
|
+
.map((workspacePackage) => {
|
|
522
|
+
const dependencies = workspacePackage.dependencies?.filter((dependency) => workspaceNames.has(dependency)) ?? [];
|
|
523
|
+
if (dependencies.length === 0) {
|
|
524
|
+
const { dependencies: _dependencies, ...withoutDependencies } = workspacePackage;
|
|
525
|
+
return withoutDependencies;
|
|
526
|
+
}
|
|
527
|
+
return { ...workspacePackage, dependencies };
|
|
528
|
+
})
|
|
529
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
530
|
+
}
|
|
531
|
+
function readCargoWorkspaceMembers(manifest) {
|
|
532
|
+
const workspaceSection = readTomlSection(manifest, "workspace");
|
|
533
|
+
if (!workspaceSection)
|
|
534
|
+
return [];
|
|
535
|
+
const match = /^members\s*=\s*\[([\s\S]*?)\]/m.exec(workspaceSection);
|
|
536
|
+
if (!match?.[1])
|
|
537
|
+
return [];
|
|
538
|
+
return readTomlStringArray(match[1]).filter((member) => !member.startsWith("!"));
|
|
539
|
+
}
|
|
540
|
+
function readCargoManifest(packageRoot) {
|
|
541
|
+
try {
|
|
542
|
+
const manifest = readFileSync(join(packageRoot, "Cargo.toml"), "utf8");
|
|
543
|
+
return {
|
|
544
|
+
...readCargoPackageName(manifest),
|
|
545
|
+
dependencies: readCargoDependencyNames(manifest)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return { dependencies: [] };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function readCargoPackageName(manifest) {
|
|
553
|
+
const packageSection = readTomlSection(manifest, "package");
|
|
554
|
+
const match = packageSection ? /^name\s*=\s*["']([^"']+)["']/m.exec(packageSection) : undefined;
|
|
555
|
+
return match?.[1] ? { name: match[1] } : {};
|
|
556
|
+
}
|
|
557
|
+
function readCargoDependencyNames(manifest) {
|
|
558
|
+
const dependencies = new Set();
|
|
559
|
+
let inDependencySection = false;
|
|
560
|
+
for (const rawLine of manifest.split(/\r?\n/)) {
|
|
561
|
+
const line = rawLine.trim();
|
|
562
|
+
if (!line || line.startsWith("#"))
|
|
563
|
+
continue;
|
|
564
|
+
const section = /^\[([^\]]+)\]$/.exec(line);
|
|
565
|
+
if (section?.[1]) {
|
|
566
|
+
inDependencySection = section[1] === "dependencies" || section[1] === "dev-dependencies" || section[1] === "build-dependencies" || section[1].endsWith(".dependencies");
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!inDependencySection)
|
|
570
|
+
continue;
|
|
571
|
+
const match = /^["']?([A-Za-z0-9_.-]+)["']?\s*=/.exec(line);
|
|
572
|
+
if (match?.[1])
|
|
573
|
+
dependencies.add(match[1]);
|
|
574
|
+
}
|
|
575
|
+
return [...dependencies].sort();
|
|
576
|
+
}
|
|
577
|
+
function readTomlSection(manifest, sectionName) {
|
|
578
|
+
const lines = manifest.split(/\r?\n/);
|
|
579
|
+
const sectionLines = [];
|
|
580
|
+
let inSection = false;
|
|
581
|
+
for (const line of lines) {
|
|
582
|
+
const section = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
|
583
|
+
if (section?.[1]) {
|
|
584
|
+
if (inSection)
|
|
585
|
+
break;
|
|
586
|
+
inSection = section[1] === sectionName;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (inSection)
|
|
590
|
+
sectionLines.push(line);
|
|
591
|
+
}
|
|
592
|
+
return sectionLines.length > 0 ? sectionLines.join("\n") : undefined;
|
|
593
|
+
}
|
|
594
|
+
function readTomlStringArray(value) {
|
|
595
|
+
const strings = [];
|
|
596
|
+
const pattern = /"([^"]+)"|'([^']+)'/g;
|
|
597
|
+
let match;
|
|
598
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
599
|
+
const item = match[1] ?? match[2];
|
|
600
|
+
if (item)
|
|
601
|
+
strings.push(item);
|
|
602
|
+
}
|
|
603
|
+
return strings;
|
|
604
|
+
}
|
|
605
|
+
function discoverGoWorkspacePackages(root, manifestPath) {
|
|
606
|
+
let workspace;
|
|
607
|
+
try {
|
|
608
|
+
workspace = readFileSync(join(root, manifestPath), "utf8");
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
const modules = readGoWorkspaceModules(workspace);
|
|
614
|
+
if (modules.length === 0)
|
|
615
|
+
return [];
|
|
616
|
+
const packages = new Map();
|
|
617
|
+
const workspaceRoot = parentPath(manifestPath) || ".";
|
|
618
|
+
const absoluteWorkspaceRoot = workspaceRoot === "." ? root : join(root, workspaceRoot);
|
|
619
|
+
for (const modulePath of modules) {
|
|
620
|
+
const repoModulePath = joinRepoPath(workspaceRoot, modulePath);
|
|
621
|
+
const manifest = readGoModuleManifest(join(absoluteWorkspaceRoot, modulePath));
|
|
622
|
+
if (!manifest.name)
|
|
623
|
+
continue;
|
|
624
|
+
const workspacePackage = {
|
|
625
|
+
name: manifest.name,
|
|
626
|
+
path: repoModulePath,
|
|
627
|
+
scripts: {}
|
|
628
|
+
};
|
|
629
|
+
if (manifest.dependencies.length > 0)
|
|
630
|
+
workspacePackage.dependencies = manifest.dependencies;
|
|
631
|
+
packages.set(repoModulePath, workspacePackage);
|
|
632
|
+
}
|
|
633
|
+
const workspaceNames = new Set([...packages.values()].map((workspacePackage) => workspacePackage.name));
|
|
634
|
+
return [...packages.values()]
|
|
635
|
+
.map((workspacePackage) => {
|
|
636
|
+
const dependencies = workspacePackage.dependencies?.filter((dependency) => workspaceNames.has(dependency)) ?? [];
|
|
637
|
+
if (dependencies.length === 0) {
|
|
638
|
+
const { dependencies: _dependencies, ...withoutDependencies } = workspacePackage;
|
|
639
|
+
return withoutDependencies;
|
|
640
|
+
}
|
|
641
|
+
return { ...workspacePackage, dependencies };
|
|
642
|
+
})
|
|
643
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
644
|
+
}
|
|
645
|
+
function readGoWorkspaceModules(workspace) {
|
|
646
|
+
const modules = [];
|
|
647
|
+
let inUseBlock = false;
|
|
648
|
+
for (const rawLine of workspace.split(/\r?\n/)) {
|
|
649
|
+
const line = stripGoComment(rawLine).trim();
|
|
650
|
+
if (!line)
|
|
651
|
+
continue;
|
|
652
|
+
if (/^use\s*\($/.test(line)) {
|
|
653
|
+
inUseBlock = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (inUseBlock && line === ")") {
|
|
657
|
+
inUseBlock = false;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (inUseBlock) {
|
|
661
|
+
const modulePath = normalizeGoWorkspacePath(line);
|
|
662
|
+
if (modulePath)
|
|
663
|
+
modules.push(modulePath);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (line.startsWith("use ")) {
|
|
667
|
+
const modulePath = normalizeGoWorkspacePath(line.slice(4).trim());
|
|
668
|
+
if (modulePath)
|
|
669
|
+
modules.push(modulePath);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return [...new Set(modules)].sort();
|
|
673
|
+
}
|
|
674
|
+
function readGoModuleManifest(moduleRoot) {
|
|
675
|
+
try {
|
|
676
|
+
const manifest = readFileSync(join(moduleRoot, "go.mod"), "utf8");
|
|
677
|
+
return {
|
|
678
|
+
...readGoModuleName(manifest),
|
|
679
|
+
dependencies: readGoRequireNames(manifest)
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
return { dependencies: [] };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function readGoModuleName(manifest) {
|
|
687
|
+
const match = /^module\s+(\S+)/m.exec(manifest);
|
|
688
|
+
return match?.[1] ? { name: match[1] } : {};
|
|
689
|
+
}
|
|
690
|
+
function readGoRequireNames(manifest) {
|
|
691
|
+
const dependencies = new Set();
|
|
692
|
+
let inRequireBlock = false;
|
|
693
|
+
for (const rawLine of manifest.split(/\r?\n/)) {
|
|
694
|
+
const line = stripGoComment(rawLine).trim();
|
|
695
|
+
if (!line)
|
|
696
|
+
continue;
|
|
697
|
+
if (/^require\s*\($/.test(line)) {
|
|
698
|
+
inRequireBlock = true;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (inRequireBlock && line === ")") {
|
|
702
|
+
inRequireBlock = false;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const requireLine = inRequireBlock ? line : line.startsWith("require ") ? line.slice(8).trim() : "";
|
|
706
|
+
if (!requireLine)
|
|
707
|
+
continue;
|
|
708
|
+
const dependency = requireLine.split(/\s+/, 1)[0];
|
|
709
|
+
if (dependency)
|
|
710
|
+
dependencies.add(dependency);
|
|
711
|
+
}
|
|
712
|
+
return [...dependencies].sort();
|
|
713
|
+
}
|
|
714
|
+
function normalizeGoWorkspacePath(value) {
|
|
715
|
+
const normalized = value.replace(/^["']|["']$/g, "").replace(/^\.\//, "").replace(/\/$/, "");
|
|
716
|
+
if (!normalized)
|
|
717
|
+
return undefined;
|
|
718
|
+
return normalized === "." ? "." : normalized;
|
|
719
|
+
}
|
|
720
|
+
function stripGoComment(value) {
|
|
721
|
+
return value.replace(/\s*\/\/.*$/, "");
|
|
722
|
+
}
|
|
723
|
+
function readText(root, path) {
|
|
724
|
+
try {
|
|
725
|
+
return readFileSync(join(root, path), "utf8");
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return "";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function manifestPriority(path, names) {
|
|
732
|
+
const priority = names.indexOf(fileName(path));
|
|
733
|
+
return priority >= 0 ? priority : names.length;
|
|
734
|
+
}
|
|
735
|
+
function fileName(path) {
|
|
736
|
+
const slash = path.lastIndexOf("/");
|
|
737
|
+
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
738
|
+
}
|
|
739
|
+
function parentPath(path) {
|
|
740
|
+
const slash = path.lastIndexOf("/");
|
|
741
|
+
return slash >= 0 ? path.slice(0, slash) : "";
|
|
742
|
+
}
|
|
743
|
+
function joinRepoPath(base, child) {
|
|
744
|
+
if (base === "." || !base)
|
|
745
|
+
return child;
|
|
746
|
+
if (child === ".")
|
|
747
|
+
return base;
|
|
748
|
+
return `${base}/${child}`;
|
|
749
|
+
}
|
|
750
|
+
function readPackageDependencyNames(manifest) {
|
|
751
|
+
const dependencies = new Set();
|
|
752
|
+
for (const section of [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies, manifest.optionalDependencies]) {
|
|
753
|
+
if (!section)
|
|
754
|
+
continue;
|
|
755
|
+
for (const name of Object.keys(section))
|
|
756
|
+
dependencies.add(name);
|
|
757
|
+
}
|
|
758
|
+
return [...dependencies].sort();
|
|
759
|
+
}
|
|
760
|
+
function readWorkspacePatterns(root) {
|
|
761
|
+
const packageJson = readPackageJson(root);
|
|
762
|
+
const patterns = new Set();
|
|
763
|
+
const workspaces = packageJson.workspaces;
|
|
764
|
+
if (Array.isArray(workspaces)) {
|
|
765
|
+
for (const pattern of workspaces)
|
|
766
|
+
if (typeof pattern === "string")
|
|
767
|
+
patterns.add(pattern);
|
|
768
|
+
}
|
|
769
|
+
else if (isRecord(workspaces) && Array.isArray(workspaces.packages)) {
|
|
770
|
+
for (const pattern of workspaces.packages)
|
|
771
|
+
if (typeof pattern === "string")
|
|
772
|
+
patterns.add(pattern);
|
|
773
|
+
}
|
|
774
|
+
if (exists(root, "pnpm-workspace.yaml")) {
|
|
775
|
+
try {
|
|
776
|
+
const parsed = parseYaml(readFileSync(join(root, "pnpm-workspace.yaml"), "utf8"));
|
|
777
|
+
if (Array.isArray(parsed.packages)) {
|
|
778
|
+
for (const pattern of parsed.packages)
|
|
779
|
+
if (typeof pattern === "string")
|
|
780
|
+
patterns.add(pattern);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
// Ignore malformed workspace metadata; normal project detection still works.
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return [...patterns].filter((pattern) => !pattern.startsWith("!"));
|
|
788
|
+
}
|
|
789
|
+
function expandWorkspacePattern(root, pattern, manifestName = "package.json") {
|
|
790
|
+
const normalized = pattern.replace(/^\.\//, "").replace(/\/$/, "");
|
|
791
|
+
if (!normalized.includes("*"))
|
|
792
|
+
return exists(root, join(normalized, manifestName)) ? [normalized] : [];
|
|
793
|
+
const segments = normalized.split("/");
|
|
794
|
+
const baseSegments = [];
|
|
795
|
+
for (const segment of segments) {
|
|
796
|
+
if (segment.includes("*"))
|
|
797
|
+
break;
|
|
798
|
+
baseSegments.push(segment);
|
|
799
|
+
}
|
|
800
|
+
const base = baseSegments.join("/") || ".";
|
|
801
|
+
const hasGlobstar = normalized.includes("**");
|
|
802
|
+
const maxDepth = hasGlobstar ? 10 : Math.max(1, segments.length - baseSegments.length);
|
|
803
|
+
const matcher = workspacePatternToRegExp(normalized);
|
|
804
|
+
// Walk candidate manifest directories, then keep only those whose full relative
|
|
805
|
+
// path matches the glob — honoring segments after "*" and excluding the base
|
|
806
|
+
// directory itself, which a single "*" must not match.
|
|
807
|
+
return walkForManifest(join(root, base), root, manifestName, maxDepth)
|
|
808
|
+
.filter((candidate) => candidate !== "." && matcher.test(candidate))
|
|
809
|
+
.sort((a, b) => a.localeCompare(b));
|
|
810
|
+
}
|
|
811
|
+
function workspacePatternToRegExp(pattern) {
|
|
812
|
+
let source = "";
|
|
813
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
814
|
+
const char = pattern[index];
|
|
815
|
+
const next = pattern[index + 1];
|
|
816
|
+
if (char === "*" && next === "*") {
|
|
817
|
+
source += ".*";
|
|
818
|
+
index += 1;
|
|
819
|
+
}
|
|
820
|
+
else if (char === "*") {
|
|
821
|
+
source += "[^/]+";
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
source += escapeRegExp(char ?? "");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return new RegExp(`^${source}$`);
|
|
828
|
+
}
|
|
829
|
+
function walkForManifest(directory, root, manifestName, maxDepth, depth = 0) {
|
|
830
|
+
if (depth > maxDepth)
|
|
831
|
+
return [];
|
|
832
|
+
try {
|
|
833
|
+
const entries = readDirentsSorted(directory);
|
|
834
|
+
const results = [];
|
|
835
|
+
if (entries.some((entry) => entry.isFile() && entry.name === manifestName)) {
|
|
836
|
+
results.push(relativePath(root, directory));
|
|
837
|
+
}
|
|
838
|
+
for (const entry of entries) {
|
|
839
|
+
if (!entry.isDirectory() || shouldSkipDirectory(entry.name))
|
|
840
|
+
continue;
|
|
841
|
+
for (const match of walkForManifest(join(directory, entry.name), root, manifestName, maxDepth, depth + 1)) {
|
|
842
|
+
results.push(match);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return results;
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function shouldSkipDirectory(name) {
|
|
852
|
+
// Skip dependency stores, build output, and tool caches: they are never the
|
|
853
|
+
// user's project source, and walking them (e.g. a huge .pnpm-store) wastes work.
|
|
854
|
+
return [
|
|
855
|
+
"node_modules",
|
|
856
|
+
".git",
|
|
857
|
+
".patchdrill",
|
|
858
|
+
".next",
|
|
859
|
+
".turbo",
|
|
860
|
+
".nx",
|
|
861
|
+
"dist",
|
|
862
|
+
"coverage",
|
|
863
|
+
"build",
|
|
864
|
+
"tmp",
|
|
865
|
+
".pnpm-store",
|
|
866
|
+
".yarn",
|
|
867
|
+
".venv",
|
|
868
|
+
"venv",
|
|
869
|
+
"__pycache__",
|
|
870
|
+
".uv-cache",
|
|
871
|
+
".ruff_cache",
|
|
872
|
+
".mypy_cache",
|
|
873
|
+
".pytest_cache"
|
|
874
|
+
].includes(name);
|
|
875
|
+
}
|
|
876
|
+
function relativePath(root, path) {
|
|
877
|
+
return path.slice(root.length).replace(/^\//, "") || ".";
|
|
878
|
+
}
|
|
879
|
+
function isRecord(value) {
|
|
880
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
881
|
+
}
|
|
882
|
+
function escapeRegExp(value) {
|
|
883
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
884
|
+
}
|
|
885
|
+
// readdirSync order is filesystem-dependent. Single-result walkers must iterate
|
|
886
|
+
// deterministically (files before directories, then by name) so the same repo
|
|
887
|
+
// yields the same chosen path on every machine — the byte-identical promise.
|
|
888
|
+
function readDirentsSorted(directory) {
|
|
889
|
+
return readdirSync(directory, { withFileTypes: true, encoding: "utf8" }).sort((a, b) => {
|
|
890
|
+
const aFile = a.isFile();
|
|
891
|
+
const bFile = b.isFile();
|
|
892
|
+
if (aFile !== bFile)
|
|
893
|
+
return aFile ? -1 : 1;
|
|
894
|
+
return a.name.localeCompare(b.name);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function findFileWithExtension(root, extension, maxDepth) {
|
|
898
|
+
return walkForExtensionPath(root, root, extension, maxDepth, 0);
|
|
899
|
+
}
|
|
900
|
+
function findDirectoryWithExtension(root, extension, maxDepth) {
|
|
901
|
+
return walkForDirectoryExtensionPath(root, root, extension, maxDepth, 0);
|
|
902
|
+
}
|
|
903
|
+
function findFilesNamed(root, fileNames, maxDepth) {
|
|
904
|
+
return walkForFileNames(root, root, new Set(fileNames), maxDepth, 0).sort((a, b) => a.localeCompare(b));
|
|
905
|
+
}
|
|
906
|
+
function walkForFileNames(root, directory, fileNames, maxDepth, depth) {
|
|
907
|
+
if (depth > maxDepth)
|
|
908
|
+
return [];
|
|
909
|
+
try {
|
|
910
|
+
const entries = readDirentsSorted(directory);
|
|
911
|
+
const results = [];
|
|
912
|
+
for (const entry of entries) {
|
|
913
|
+
if (shouldSkipDirectory(entry.name))
|
|
914
|
+
continue;
|
|
915
|
+
const path = join(directory, entry.name);
|
|
916
|
+
if (entry.isFile() && fileNames.has(entry.name))
|
|
917
|
+
results.push(relativePath(root, path));
|
|
918
|
+
if (entry.isDirectory()) {
|
|
919
|
+
for (const match of walkForFileNames(root, path, fileNames, maxDepth, depth + 1))
|
|
920
|
+
results.push(match);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return results;
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
return [];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function walkForExtensionPath(root, directory, extension, maxDepth, depth) {
|
|
930
|
+
if (depth > maxDepth)
|
|
931
|
+
return undefined;
|
|
932
|
+
try {
|
|
933
|
+
const entries = readDirentsSorted(directory);
|
|
934
|
+
for (const entry of entries) {
|
|
935
|
+
if (shouldSkipDirectory(entry.name))
|
|
936
|
+
continue;
|
|
937
|
+
const path = join(directory, entry.name);
|
|
938
|
+
if (entry.isFile() && entry.name.endsWith(extension))
|
|
939
|
+
return relativePath(root, path);
|
|
940
|
+
if (entry.isDirectory()) {
|
|
941
|
+
const match = walkForExtensionPath(root, path, extension, maxDepth, depth + 1);
|
|
942
|
+
if (match)
|
|
943
|
+
return match;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
function walkForDirectoryExtensionPath(root, directory, extension, maxDepth, depth) {
|
|
953
|
+
if (depth > maxDepth)
|
|
954
|
+
return undefined;
|
|
955
|
+
try {
|
|
956
|
+
const entries = readDirentsSorted(directory);
|
|
957
|
+
for (const entry of entries) {
|
|
958
|
+
if (!entry.isDirectory() || shouldSkipDirectory(entry.name))
|
|
959
|
+
continue;
|
|
960
|
+
const path = join(directory, entry.name);
|
|
961
|
+
if (entry.name.endsWith(extension))
|
|
962
|
+
return relativePath(root, path);
|
|
963
|
+
const match = walkForDirectoryExtensionPath(root, path, extension, maxDepth, depth + 1);
|
|
964
|
+
if (match)
|
|
965
|
+
return match;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
function hasTerraform(root) {
|
|
974
|
+
return ["main.tf", "variables.tf", "providers.tf", "terraform.tfvars"].some((file) => exists(root, file));
|
|
975
|
+
}
|
|
976
|
+
function findKubernetesManifestPath(root) {
|
|
977
|
+
const direct = firstExisting(root, ["Chart.yaml", "kustomization.yaml", "kustomization.yml", "k8s", "kubernetes", "manifests", "charts"]);
|
|
978
|
+
if (direct)
|
|
979
|
+
return direct;
|
|
980
|
+
if (hasFileNamed(root, "Chart.yaml", 3))
|
|
981
|
+
return "Chart.yaml";
|
|
982
|
+
if (hasFileNamed(root, "kustomization.yaml", 3) || hasFileNamed(root, "kustomization.yml", 3))
|
|
983
|
+
return "kustomization.yaml";
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
function findBazelManifestPath(root) {
|
|
987
|
+
const direct = firstExisting(root, ["MODULE.bazel", "WORKSPACE.bazel", "WORKSPACE", ".bazelrc"]);
|
|
988
|
+
if (direct)
|
|
989
|
+
return direct;
|
|
990
|
+
if (exists(root, "pants.toml"))
|
|
991
|
+
return undefined;
|
|
992
|
+
if (hasFileNamed(root, "BUILD.bazel", 3) || hasFileNamed(root, "BUILD", 3))
|
|
993
|
+
return "BUILD.bazel";
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
function findBuckManifestPath(root) {
|
|
997
|
+
const direct = firstExisting(root, [".buckconfig", "BUCK", "BUCK.v2"]);
|
|
998
|
+
if (direct)
|
|
999
|
+
return direct;
|
|
1000
|
+
if (hasFileNamed(root, "BUCK", 3) || hasFileNamed(root, "BUCK.v2", 3))
|
|
1001
|
+
return "BUCK";
|
|
1002
|
+
return undefined;
|
|
1003
|
+
}
|
|
1004
|
+
function hasFileNamed(root, fileName, maxDepth) {
|
|
1005
|
+
return walkForFileName(root, fileName, maxDepth, 0);
|
|
1006
|
+
}
|
|
1007
|
+
function findFileMentioning(root, fileNames, needles, maxDepth) {
|
|
1008
|
+
return walkForFileMentioning(root, root, new Set(fileNames), needles.map((needle) => needle.toLowerCase()), maxDepth, 0);
|
|
1009
|
+
}
|
|
1010
|
+
function findFileWithExtensionMentioning(root, extension, needles, maxDepth) {
|
|
1011
|
+
return walkForExtensionMentioning(root, root, extension, needles.map((needle) => needle.toLowerCase()), maxDepth, 0);
|
|
1012
|
+
}
|
|
1013
|
+
function walkForFileMentioning(directory, root, fileNames, needles, maxDepth, depth) {
|
|
1014
|
+
if (depth > maxDepth)
|
|
1015
|
+
return undefined;
|
|
1016
|
+
try {
|
|
1017
|
+
const entries = readDirentsSorted(directory);
|
|
1018
|
+
for (const entry of entries) {
|
|
1019
|
+
if (shouldSkipDirectory(entry.name))
|
|
1020
|
+
continue;
|
|
1021
|
+
const path = join(directory, entry.name);
|
|
1022
|
+
if (entry.isFile() && fileNames.has(entry.name)) {
|
|
1023
|
+
const content = readFileSync(path, "utf8").toLowerCase();
|
|
1024
|
+
if (needles.some((needle) => content.includes(needle)))
|
|
1025
|
+
return relativePath(root, path);
|
|
1026
|
+
}
|
|
1027
|
+
if (entry.isDirectory()) {
|
|
1028
|
+
const match = walkForFileMentioning(path, root, fileNames, needles, maxDepth, depth + 1);
|
|
1029
|
+
if (match)
|
|
1030
|
+
return match;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
function walkForExtensionMentioning(root, directory, extension, needles, maxDepth, depth) {
|
|
1040
|
+
if (depth > maxDepth)
|
|
1041
|
+
return undefined;
|
|
1042
|
+
try {
|
|
1043
|
+
const entries = readDirentsSorted(directory);
|
|
1044
|
+
for (const entry of entries) {
|
|
1045
|
+
if (shouldSkipDirectory(entry.name))
|
|
1046
|
+
continue;
|
|
1047
|
+
const path = join(directory, entry.name);
|
|
1048
|
+
if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
1049
|
+
const content = readFileSync(path, "utf8").toLowerCase();
|
|
1050
|
+
if (needles.some((needle) => content.includes(needle.toLowerCase())))
|
|
1051
|
+
return relativePath(root, path);
|
|
1052
|
+
}
|
|
1053
|
+
if (entry.isDirectory()) {
|
|
1054
|
+
const match = walkForExtensionMentioning(root, path, extension, needles, maxDepth, depth + 1);
|
|
1055
|
+
if (match)
|
|
1056
|
+
return match;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
return undefined;
|
|
1064
|
+
}
|
|
1065
|
+
function walkForFileName(directory, fileName, maxDepth, depth) {
|
|
1066
|
+
if (depth > maxDepth)
|
|
1067
|
+
return false;
|
|
1068
|
+
try {
|
|
1069
|
+
const entries = readDirentsSorted(directory);
|
|
1070
|
+
for (const entry of entries) {
|
|
1071
|
+
if (shouldSkipDirectory(entry.name))
|
|
1072
|
+
continue;
|
|
1073
|
+
const path = join(directory, entry.name);
|
|
1074
|
+
if (entry.isFile() && entry.name === fileName)
|
|
1075
|
+
return true;
|
|
1076
|
+
if (entry.isDirectory() && walkForFileName(path, fileName, maxDepth, depth + 1))
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
catch {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
//# sourceMappingURL=project.js.map
|