@praneeth_54/agentdoctor 0.1.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +348 -0
  4. package/dist/agents/claude/adapter.d.ts +1 -0
  5. package/dist/agents/claude/adapter.js +1 -0
  6. package/dist/agents/claude/detector.d.ts +16 -0
  7. package/dist/agents/claude/detector.js +225 -0
  8. package/dist/agents/codex/adapter.d.ts +1 -0
  9. package/dist/agents/codex/adapter.js +1 -0
  10. package/dist/agents/codex/detector.d.ts +17 -0
  11. package/dist/agents/codex/detector.js +151 -0
  12. package/dist/agents/cursor/adapter.d.ts +1 -0
  13. package/dist/agents/cursor/adapter.js +1 -0
  14. package/dist/agents/cursor/detector.d.ts +17 -0
  15. package/dist/agents/cursor/detector.js +198 -0
  16. package/dist/agents/detect-agents.d.ts +17 -0
  17. package/dist/agents/detect-agents.js +23 -0
  18. package/dist/agents/inspect.d.ts +36 -0
  19. package/dist/agents/inspect.js +165 -0
  20. package/dist/agents/registry.d.ts +7 -0
  21. package/dist/agents/registry.js +11 -0
  22. package/dist/agents/types.d.ts +52 -0
  23. package/dist/agents/types.js +15 -0
  24. package/dist/cli/commands/doctor.d.ts +5 -0
  25. package/dist/cli/commands/doctor.js +22 -0
  26. package/dist/cli/commands/explain.d.ts +5 -0
  27. package/dist/cli/commands/explain.js +51 -0
  28. package/dist/cli/commands/fix.d.ts +8 -0
  29. package/dist/cli/commands/fix.js +19 -0
  30. package/dist/cli/commands/scan.d.ts +10 -0
  31. package/dist/cli/commands/scan.js +59 -0
  32. package/dist/cli/index.d.ts +2 -0
  33. package/dist/cli/index.js +3 -0
  34. package/dist/cli/program.d.ts +3 -0
  35. package/dist/cli/program.js +124 -0
  36. package/dist/constants.d.ts +5 -0
  37. package/dist/constants.js +38 -0
  38. package/dist/core/mcp/parse.d.ts +7 -0
  39. package/dist/core/mcp/parse.js +303 -0
  40. package/dist/core/mcp/types.d.ts +33 -0
  41. package/dist/core/mcp/types.js +1 -0
  42. package/dist/core/rules/build-context.d.ts +10 -0
  43. package/dist/core/rules/build-context.js +31 -0
  44. package/dist/core/rules/context/generated-directory.d.ts +2 -0
  45. package/dist/core/rules/context/generated-directory.js +89 -0
  46. package/dist/core/rules/context/large-instruction-file.d.ts +2 -0
  47. package/dist/core/rules/context/large-instruction-file.js +56 -0
  48. package/dist/core/rules/context/large-log-file.d.ts +2 -0
  49. package/dist/core/rules/context/large-log-file.js +45 -0
  50. package/dist/core/rules/dedupe.d.ts +13 -0
  51. package/dist/core/rules/dedupe.js +53 -0
  52. package/dist/core/rules/ignore.d.ts +23 -0
  53. package/dist/core/rules/ignore.js +97 -0
  54. package/dist/core/rules/instructions/duplicate-content.d.ts +2 -0
  55. package/dist/core/rules/instructions/duplicate-content.js +99 -0
  56. package/dist/core/rules/instructions/empty-instructions.d.ts +2 -0
  57. package/dist/core/rules/instructions/empty-instructions.js +53 -0
  58. package/dist/core/rules/instructions/missing-path-reference.d.ts +2 -0
  59. package/dist/core/rules/instructions/missing-path-reference.js +131 -0
  60. package/dist/core/rules/mcp/mcp-rules.d.ts +3 -0
  61. package/dist/core/rules/mcp/mcp-rules.js +83 -0
  62. package/dist/core/rules/registry.d.ts +6 -0
  63. package/dist/core/rules/registry.js +31 -0
  64. package/dist/core/rules/run-rules.d.ts +16 -0
  65. package/dist/core/rules/run-rules.js +36 -0
  66. package/dist/core/rules/security/claude-bypass-permissions.d.ts +7 -0
  67. package/dist/core/rules/security/claude-bypass-permissions.js +52 -0
  68. package/dist/core/rules/security/env-file-exposure.d.ts +2 -0
  69. package/dist/core/rules/security/env-file-exposure.js +87 -0
  70. package/dist/core/rules/security/mcp-broad-filesystem.d.ts +2 -0
  71. package/dist/core/rules/security/mcp-broad-filesystem.js +42 -0
  72. package/dist/core/rules/security/private-key-file.d.ts +2 -0
  73. package/dist/core/rules/security/private-key-file.js +60 -0
  74. package/dist/core/rules/text-cache.d.ts +19 -0
  75. package/dist/core/rules/text-cache.js +112 -0
  76. package/dist/core/rules/thresholds.d.ts +13 -0
  77. package/dist/core/rules/thresholds.js +13 -0
  78. package/dist/core/rules/types.d.ts +57 -0
  79. package/dist/core/rules/types.js +23 -0
  80. package/dist/core/scanner/scan.d.ts +7 -0
  81. package/dist/core/scanner/scan.js +70 -0
  82. package/dist/core/scoring/placeholder.d.ts +7 -0
  83. package/dist/core/scoring/placeholder.js +29 -0
  84. package/dist/detectors/framework.d.ts +17 -0
  85. package/dist/detectors/framework.js +148 -0
  86. package/dist/detectors/language.d.ts +14 -0
  87. package/dist/detectors/language.js +122 -0
  88. package/dist/detectors/monorepo.d.ts +13 -0
  89. package/dist/detectors/monorepo.js +44 -0
  90. package/dist/detectors/package-manager.d.ts +14 -0
  91. package/dist/detectors/package-manager.js +78 -0
  92. package/dist/detectors/project.d.ts +10 -0
  93. package/dist/detectors/project.js +60 -0
  94. package/dist/discovery/files.d.ts +15 -0
  95. package/dist/discovery/files.js +118 -0
  96. package/dist/index.d.ts +8 -0
  97. package/dist/index.js +7 -0
  98. package/dist/reporters/json/report.d.ts +5 -0
  99. package/dist/reporters/json/report.js +84 -0
  100. package/dist/reporters/terminal/report.d.ts +8 -0
  101. package/dist/reporters/terminal/report.js +147 -0
  102. package/dist/security/redaction.d.ts +9 -0
  103. package/dist/security/redaction.js +26 -0
  104. package/dist/types/index.d.ts +137 -0
  105. package/dist/types/index.js +6 -0
  106. package/dist/utils/colors.d.ts +14 -0
  107. package/dist/utils/colors.js +26 -0
  108. package/dist/utils/fs.d.ts +17 -0
  109. package/dist/utils/fs.js +63 -0
  110. package/dist/utils/path.d.ts +17 -0
  111. package/dist/utils/path.js +37 -0
  112. package/package.json +80 -0
@@ -0,0 +1,148 @@
1
+ function hasDep(deps, name) {
2
+ return Boolean(deps && name in deps);
3
+ }
4
+ function hasFile(paths, ...names) {
5
+ return names.some((name) => paths.has(name.toLowerCase()));
6
+ }
7
+ /**
8
+ * Detect frameworks from config markers and dependency names.
9
+ * Does not fail when nothing matches — returns unknown.
10
+ */
11
+ export function detectFrameworks(input) {
12
+ const pathSet = new Set(input.relativePaths.map((p) => p.toLowerCase()));
13
+ const basenames = new Set(input.relativePaths.map((p) => {
14
+ const parts = p.split("/");
15
+ return (parts[parts.length - 1] ?? p).toLowerCase();
16
+ }));
17
+ const deps = {
18
+ ...(input.packageJsonDependencies ?? {}),
19
+ };
20
+ const composer = input.composerJsonRequire ?? {};
21
+ const detected = [];
22
+ const isNext = hasFile(basenames, "next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs") ||
23
+ hasDep(deps, "next");
24
+ if (isNext) {
25
+ detected.push("nextjs");
26
+ }
27
+ const isNuxt = hasFile(basenames, "nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs") ||
28
+ hasDep(deps, "nuxt");
29
+ if (isNuxt) {
30
+ detected.push("nuxt");
31
+ }
32
+ const isNest = hasDep(deps, "@nestjs/core") ||
33
+ hasFile(basenames, "nest-cli.json") ||
34
+ pathSet.has("nest-cli.json");
35
+ if (isNest) {
36
+ detected.push("nestjs");
37
+ }
38
+ const isExpress = hasDep(deps, "express") && !isNest && !isNext;
39
+ if (isExpress) {
40
+ detected.push("express");
41
+ }
42
+ const isVue = hasDep(deps, "vue") ||
43
+ ([...pathSet].some((p) => p.endsWith(".vue")) &&
44
+ (hasFile(basenames, "vite.config.ts", "vite.config.js", "vue.config.js") ||
45
+ hasDep(deps, "vite")));
46
+ if (isVue && !isNuxt) {
47
+ detected.push("vue");
48
+ }
49
+ const isSvelte = hasDep(deps, "svelte") ||
50
+ hasDep(deps, "@sveltejs/kit") ||
51
+ hasFile(basenames, "svelte.config.js", "svelte.config.ts");
52
+ if (isSvelte) {
53
+ detected.push("svelte");
54
+ }
55
+ const isReact = (hasDep(deps, "react") || hasDep(deps, "react-dom")) && !isNext && !detected.includes("react");
56
+ if (isReact) {
57
+ detected.push("react");
58
+ }
59
+ if (!isNext &&
60
+ !isNuxt &&
61
+ !isNest &&
62
+ !isExpress &&
63
+ !isVue &&
64
+ !isSvelte &&
65
+ !isReact &&
66
+ (hasFile(basenames, "package.json") || hasDep(deps, "express") === false) &&
67
+ hasFile(basenames, "package.json")) {
68
+ // Generic Node.js project with package.json but no web framework
69
+ if (!detected.includes("nodejs")) {
70
+ detected.push("nodejs");
71
+ }
72
+ }
73
+ if (hasFile(basenames, "pubspec.yaml") || input.pubspecName !== undefined) {
74
+ // Flutter: pubspec presence implies Flutter/Dart project context for detection
75
+ if (hasFile(pathSet, "pubspec.yaml") || basenames.has("pubspec.yaml")) {
76
+ detected.push("flutter");
77
+ }
78
+ }
79
+ if (hasDep(composer, "laravel/framework") || hasFile(basenames, "artisan")) {
80
+ detected.push("laravel");
81
+ }
82
+ if (hasFile(basenames, "manage.py") ||
83
+ [...pathSet].some((p) => p.endsWith("/settings.py") || p === "settings.py")) {
84
+ detected.push("django");
85
+ }
86
+ // FastAPI: path heuristics only (requirements/pyproject often lack a stable marker)
87
+ if ([...pathSet].some((p) => p.includes("fastapi") || p.endsWith("requirements-fastapi.txt") || p === "app/main.py")) {
88
+ detected.push("fastapi");
89
+ }
90
+ // Deduplicate while preserving order
91
+ const unique = [...new Set(detected)];
92
+ // Priority for primary framework
93
+ const priority = [
94
+ "nextjs",
95
+ "nuxt",
96
+ "nestjs",
97
+ "laravel",
98
+ "django",
99
+ "fastapi",
100
+ "flutter",
101
+ "svelte",
102
+ "vue",
103
+ "react",
104
+ "express",
105
+ "nodejs",
106
+ ];
107
+ let primaryFramework = "unknown";
108
+ for (const candidate of priority) {
109
+ if (unique.includes(candidate)) {
110
+ primaryFramework = candidate;
111
+ break;
112
+ }
113
+ }
114
+ if (unique.length === 0) {
115
+ return { frameworks: ["unknown"], primaryFramework: "unknown" };
116
+ }
117
+ return { frameworks: unique, primaryFramework };
118
+ }
119
+ export function formatFramework(framework) {
120
+ switch (framework) {
121
+ case "nextjs":
122
+ return "Next.js";
123
+ case "react":
124
+ return "React";
125
+ case "vue":
126
+ return "Vue";
127
+ case "nuxt":
128
+ return "Nuxt";
129
+ case "svelte":
130
+ return "Svelte";
131
+ case "nodejs":
132
+ return "Node.js";
133
+ case "express":
134
+ return "Express";
135
+ case "nestjs":
136
+ return "NestJS";
137
+ case "flutter":
138
+ return "Flutter";
139
+ case "laravel":
140
+ return "Laravel";
141
+ case "django":
142
+ return "Django";
143
+ case "fastapi":
144
+ return "FastAPI";
145
+ default:
146
+ return "Unknown";
147
+ }
148
+ }
@@ -0,0 +1,14 @@
1
+ import type { LanguageId } from "../types/index.js";
2
+ export interface LanguageDetectionInput {
3
+ relativePaths: string[];
4
+ }
5
+ export interface LanguageDetectionResult {
6
+ languages: LanguageId[];
7
+ primaryLanguage: LanguageId;
8
+ }
9
+ /**
10
+ * Detect languages from known marker files and source extensions.
11
+ * TypeScript is preferred over JavaScript when both appear.
12
+ */
13
+ export declare function detectLanguages(input: LanguageDetectionInput): LanguageDetectionResult;
14
+ export declare function formatLanguage(language: LanguageId): string;
@@ -0,0 +1,122 @@
1
+ const LANGUAGE_MARKERS = [
2
+ {
3
+ language: "typescript",
4
+ files: ["tsconfig.json", "tsconfig.base.json"],
5
+ extensions: [".ts", ".tsx", ".mts", ".cts"],
6
+ },
7
+ {
8
+ language: "javascript",
9
+ files: ["jsconfig.json", "package.json"],
10
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
11
+ },
12
+ {
13
+ language: "python",
14
+ files: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
15
+ extensions: [".py"],
16
+ },
17
+ {
18
+ language: "dart",
19
+ files: ["pubspec.yaml"],
20
+ extensions: [".dart"],
21
+ },
22
+ {
23
+ language: "php",
24
+ files: ["composer.json"],
25
+ extensions: [".php"],
26
+ },
27
+ {
28
+ language: "go",
29
+ files: ["go.mod", "go.work"],
30
+ extensions: [".go"],
31
+ },
32
+ {
33
+ language: "rust",
34
+ files: ["Cargo.toml"],
35
+ extensions: [".rs"],
36
+ },
37
+ {
38
+ language: "java",
39
+ files: ["pom.xml", "build.gradle", "build.gradle.kts"],
40
+ extensions: [".java"],
41
+ },
42
+ {
43
+ language: "kotlin",
44
+ files: ["build.gradle.kts"],
45
+ extensions: [".kt", ".kts"],
46
+ },
47
+ ];
48
+ function hasExtension(filePath, extensions) {
49
+ const lower = filePath.toLowerCase();
50
+ return extensions.some((ext) => lower.endsWith(ext));
51
+ }
52
+ /**
53
+ * Detect languages from known marker files and source extensions.
54
+ * TypeScript is preferred over JavaScript when both appear.
55
+ */
56
+ export function detectLanguages(input) {
57
+ const pathSet = new Set(input.relativePaths.map((p) => p.toLowerCase()));
58
+ const basenameSet = new Set(input.relativePaths.map((p) => {
59
+ const parts = p.split("/");
60
+ return (parts[parts.length - 1] ?? p).toLowerCase();
61
+ }));
62
+ const scores = new Map();
63
+ for (const marker of LANGUAGE_MARKERS) {
64
+ let score = 0;
65
+ for (const file of marker.files) {
66
+ if (basenameSet.has(file.toLowerCase()) || pathSet.has(file.toLowerCase())) {
67
+ score += 10;
68
+ }
69
+ }
70
+ for (const relative of input.relativePaths) {
71
+ if (hasExtension(relative, marker.extensions)) {
72
+ score += 1;
73
+ }
74
+ }
75
+ if (score > 0) {
76
+ scores.set(marker.language, score);
77
+ }
78
+ }
79
+ // Prefer TypeScript over JavaScript when both present with TS markers.
80
+ if (scores.has("typescript") && scores.has("javascript")) {
81
+ const jsScore = scores.get("javascript") ?? 0;
82
+ // package.json alone shouldn't claim JS over TS
83
+ if ((scores.get("typescript") ?? 0) >= 10) {
84
+ scores.set("javascript", Math.min(jsScore, 5));
85
+ }
86
+ }
87
+ const ordered = [...scores.entries()].sort((a, b) => b[1] - a[1]);
88
+ const languages = ordered.map(([lang]) => lang);
89
+ let primaryLanguage = languages[0] ?? "unknown";
90
+ // Explicit preference: TS > JS when both detected with meaningful scores
91
+ if (languages.includes("typescript") && languages.includes("javascript")) {
92
+ primaryLanguage = "typescript";
93
+ }
94
+ return {
95
+ languages: languages.length > 0 ? languages : ["unknown"],
96
+ primaryLanguage,
97
+ };
98
+ }
99
+ export function formatLanguage(language) {
100
+ switch (language) {
101
+ case "typescript":
102
+ return "TypeScript";
103
+ case "javascript":
104
+ return "JavaScript";
105
+ case "python":
106
+ return "Python";
107
+ case "dart":
108
+ return "Dart";
109
+ case "php":
110
+ return "PHP";
111
+ case "go":
112
+ return "Go";
113
+ case "rust":
114
+ return "Rust";
115
+ case "java":
116
+ return "Java";
117
+ case "kotlin":
118
+ return "Kotlin";
119
+ default:
120
+ return "Unknown";
121
+ }
122
+ }
@@ -0,0 +1,13 @@
1
+ import type { MonorepoToolId } from "../types/index.js";
2
+ export interface MonorepoDetectionInput {
3
+ relativePaths: string[];
4
+ packageJsonWorkspaces?: unknown;
5
+ }
6
+ export interface MonorepoDetectionResult {
7
+ monorepo: MonorepoToolId;
8
+ }
9
+ /**
10
+ * Detect common monorepo tooling from config markers.
11
+ */
12
+ export declare function detectMonorepo(input: MonorepoDetectionInput): MonorepoDetectionResult;
13
+ export declare function formatMonorepo(id: MonorepoToolId): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Detect common monorepo tooling from config markers.
3
+ */
4
+ export function detectMonorepo(input) {
5
+ const basenames = new Set(input.relativePaths.map((p) => {
6
+ const parts = p.split("/");
7
+ return (parts[parts.length - 1] ?? p).toLowerCase();
8
+ }));
9
+ if (basenames.has("turbo.json") || basenames.has("turbo.jsonc")) {
10
+ return { monorepo: "turborepo" };
11
+ }
12
+ if (basenames.has("nx.json")) {
13
+ return { monorepo: "nx" };
14
+ }
15
+ if (basenames.has("pnpm-workspace.yaml")) {
16
+ return { monorepo: "pnpm-workspaces" };
17
+ }
18
+ if (input.packageJsonWorkspaces !== undefined && input.packageJsonWorkspaces !== null) {
19
+ const ws = input.packageJsonWorkspaces;
20
+ const hasWorkspaces = Array.isArray(ws) ||
21
+ (typeof ws === "object" &&
22
+ ws !== null &&
23
+ "packages" in ws &&
24
+ Array.isArray(ws.packages));
25
+ if (hasWorkspaces) {
26
+ return { monorepo: "npm-workspaces" };
27
+ }
28
+ }
29
+ return { monorepo: "none" };
30
+ }
31
+ export function formatMonorepo(id) {
32
+ switch (id) {
33
+ case "npm-workspaces":
34
+ return "npm workspaces";
35
+ case "pnpm-workspaces":
36
+ return "pnpm workspaces";
37
+ case "turborepo":
38
+ return "Turborepo";
39
+ case "nx":
40
+ return "Nx";
41
+ default:
42
+ return "None";
43
+ }
44
+ }
@@ -0,0 +1,14 @@
1
+ import type { PackageManagerId } from "../types/index.js";
2
+ export interface PackageManagerDetectionInput {
3
+ relativePaths: string[];
4
+ }
5
+ export interface PackageManagerDetectionResult {
6
+ packageManagers: PackageManagerId[];
7
+ primaryPackageManager: PackageManagerId;
8
+ }
9
+ /**
10
+ * Detect package managers from lockfiles and manifests.
11
+ * Lockfiles take precedence over generic manifests for primary selection.
12
+ */
13
+ export declare function detectPackageManagers(input: PackageManagerDetectionInput): PackageManagerDetectionResult;
14
+ export declare function formatPackageManager(id: PackageManagerId): string;
@@ -0,0 +1,78 @@
1
+ const MARKERS = [
2
+ { id: "pnpm", files: ["pnpm-lock.yaml", "pnpm-workspace.yaml"] },
3
+ { id: "yarn", files: ["yarn.lock", ".yarnrc.yml"] },
4
+ { id: "bun", files: ["bun.lockb", "bun.lock"] },
5
+ { id: "npm", files: ["package-lock.json", "npm-shrinkwrap.json"] },
6
+ { id: "composer", files: ["composer.lock", "composer.json"] },
7
+ { id: "poetry", files: ["poetry.lock", "pyproject.toml"] },
8
+ { id: "pip", files: ["requirements.txt", "Pipfile", "Pipfile.lock"] },
9
+ { id: "cargo", files: ["Cargo.lock", "Cargo.toml"] },
10
+ { id: "gradle", files: ["gradlew", "build.gradle", "build.gradle.kts", "settings.gradle"] },
11
+ ];
12
+ /**
13
+ * Detect package managers from lockfiles and manifests.
14
+ * Lockfiles take precedence over generic manifests for primary selection.
15
+ */
16
+ export function detectPackageManagers(input) {
17
+ const basenames = new Set(input.relativePaths.map((p) => {
18
+ const parts = p.split("/");
19
+ return (parts[parts.length - 1] ?? p).toLowerCase();
20
+ }));
21
+ const found = [];
22
+ for (const marker of MARKERS) {
23
+ if (marker.files.some((f) => basenames.has(f.toLowerCase()))) {
24
+ found.push(marker.id);
25
+ }
26
+ }
27
+ if (basenames.has("package.json") &&
28
+ !found.some((id) => ["npm", "pnpm", "yarn", "bun"].includes(id))) {
29
+ found.push("npm");
30
+ }
31
+ const unique = [...new Set(found)];
32
+ const lockPriority = [
33
+ "pnpm",
34
+ "yarn",
35
+ "bun",
36
+ "npm",
37
+ "poetry",
38
+ "composer",
39
+ "cargo",
40
+ "gradle",
41
+ "pip",
42
+ ];
43
+ let primary = "unknown";
44
+ for (const id of lockPriority) {
45
+ if (unique.includes(id)) {
46
+ primary = id;
47
+ break;
48
+ }
49
+ }
50
+ return {
51
+ packageManagers: unique.length > 0 ? unique : ["unknown"],
52
+ primaryPackageManager: primary,
53
+ };
54
+ }
55
+ export function formatPackageManager(id) {
56
+ switch (id) {
57
+ case "npm":
58
+ return "npm";
59
+ case "pnpm":
60
+ return "pnpm";
61
+ case "yarn":
62
+ return "Yarn";
63
+ case "bun":
64
+ return "Bun";
65
+ case "composer":
66
+ return "Composer";
67
+ case "pip":
68
+ return "pip";
69
+ case "poetry":
70
+ return "Poetry";
71
+ case "cargo":
72
+ return "Cargo";
73
+ case "gradle":
74
+ return "Gradle";
75
+ default:
76
+ return "Unknown";
77
+ }
78
+ }
@@ -0,0 +1,10 @@
1
+ import type { DiscoveryResult, RepositoryInfo } from "../types/index.js";
2
+ export interface ProjectDetectionResult {
3
+ repository: RepositoryInfo;
4
+ discovery: DiscoveryResult;
5
+ diagnostics: string[];
6
+ }
7
+ /**
8
+ * Discover files and detect repository characteristics.
9
+ */
10
+ export declare function detectProject(cwd: string, maxFileSizeBytes?: number): Promise<ProjectDetectionResult>;
@@ -0,0 +1,60 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
3
+ import { discoverFiles } from "../discovery/files.js";
4
+ import { detectFrameworks } from "./framework.js";
5
+ import { detectLanguages } from "./language.js";
6
+ import { detectMonorepo } from "./monorepo.js";
7
+ import { detectPackageManagers } from "./package-manager.js";
8
+ import { isDirectory, readJsonFile } from "../utils/fs.js";
9
+ import { resolveRepoRoot } from "../utils/path.js";
10
+ /**
11
+ * Discover files and detect repository characteristics.
12
+ */
13
+ export async function detectProject(cwd, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
14
+ const root = resolveRepoRoot(cwd);
15
+ const diagnostics = [];
16
+ if (!(await isDirectory(root))) {
17
+ throw new Error(`Not a directory: ${root}`);
18
+ }
19
+ const discovery = await discoverFiles({ root, maxFileSizeBytes });
20
+ const relativePaths = discovery.files.map((f) => f.relativePath);
21
+ for (const err of discovery.permissionErrors) {
22
+ diagnostics.push(`Permission/read issue: ${err}`);
23
+ }
24
+ let packageJson;
25
+ const packageJsonPath = path.join(root, "package.json");
26
+ const packageResult = await readJsonFile(packageJsonPath, maxFileSizeBytes);
27
+ if (packageResult.ok) {
28
+ packageJson = packageResult.data;
29
+ }
30
+ else if (relativePaths.some((p) => p === "package.json" || p.endsWith("/package.json"))) {
31
+ diagnostics.push(`Malformed package.json: ${packageResult.error}`);
32
+ }
33
+ const deps = {
34
+ ...(packageJson?.dependencies ?? {}),
35
+ ...(packageJson?.devDependencies ?? {}),
36
+ ...(packageJson?.peerDependencies ?? {}),
37
+ };
38
+ const languages = detectLanguages({ relativePaths });
39
+ const frameworks = detectFrameworks({
40
+ relativePaths,
41
+ packageJsonDependencies: deps,
42
+ });
43
+ const packageManagers = detectPackageManagers({ relativePaths });
44
+ const monorepo = detectMonorepo({
45
+ relativePaths,
46
+ packageJsonWorkspaces: packageJson?.workspaces,
47
+ });
48
+ const repository = {
49
+ root,
50
+ languages: languages.languages,
51
+ primaryLanguage: languages.primaryLanguage,
52
+ frameworks: frameworks.frameworks,
53
+ primaryFramework: frameworks.primaryFramework,
54
+ packageManagers: packageManagers.packageManagers,
55
+ primaryPackageManager: packageManagers.primaryPackageManager,
56
+ monorepo: monorepo.monorepo,
57
+ filesScanned: discovery.files.length,
58
+ };
59
+ return { repository, discovery, diagnostics };
60
+ }
@@ -0,0 +1,15 @@
1
+ import type { DiscoveryResult } from "../types/index.js";
2
+ export interface DiscoverFilesOptions {
3
+ root: string;
4
+ maxFileSizeBytes?: number;
5
+ ignoreDirectories?: Set<string>;
6
+ }
7
+ /**
8
+ * Walk the repository without blindly reading every file.
9
+ * Skips common generated/dependency directories and records permission errors.
10
+ */
11
+ export declare function discoverFiles(options: DiscoverFilesOptions): Promise<DiscoveryResult>;
12
+ /**
13
+ * Targeted existence checks for known config files (O(1) per path).
14
+ */
15
+ export declare function findKnownFiles(root: string, candidates: readonly string[]): Promise<string[]>;
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_IGNORE_DIRECTORIES, DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
4
+ import { isPathInsideRoot, toPosixRelative } from "../utils/path.js";
5
+ /**
6
+ * Walk the repository without blindly reading every file.
7
+ * Skips common generated/dependency directories and records permission errors.
8
+ */
9
+ export async function discoverFiles(options) {
10
+ const started = performance.now();
11
+ const root = path.resolve(options.root);
12
+ const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
13
+ const ignoreDirectories = options.ignoreDirectories ?? DEFAULT_IGNORE_DIRECTORIES;
14
+ const files = [];
15
+ const directoriesSkipped = [];
16
+ const permissionErrors = [];
17
+ let filesSkippedOversized = 0;
18
+ const queue = [root];
19
+ while (queue.length > 0) {
20
+ const current = queue.pop();
21
+ if (!current) {
22
+ continue;
23
+ }
24
+ if (!isPathInsideRoot(root, current)) {
25
+ continue;
26
+ }
27
+ let entries;
28
+ try {
29
+ entries = await fs.readdir(current, { withFileTypes: true });
30
+ }
31
+ catch (error) {
32
+ const message = error instanceof Error ? error.message : "permission denied";
33
+ permissionErrors.push(`${toPosixRelative(root, current) || "."}: ${message}`);
34
+ continue;
35
+ }
36
+ for (const entry of entries) {
37
+ const absolutePath = path.join(current, entry.name);
38
+ const relativePath = toPosixRelative(root, absolutePath);
39
+ if (!isPathInsideRoot(root, absolutePath)) {
40
+ continue;
41
+ }
42
+ let isDirectory = entry.isDirectory();
43
+ const isSymlink = entry.isSymbolicLink();
44
+ let isFile = entry.isFile();
45
+ // Resolve symlink type carefully without escaping the root.
46
+ if (isSymlink) {
47
+ try {
48
+ const real = await fs.realpath(absolutePath);
49
+ if (!isPathInsideRoot(root, real)) {
50
+ continue;
51
+ }
52
+ const realStat = await fs.stat(real);
53
+ isDirectory = realStat.isDirectory();
54
+ isFile = realStat.isFile();
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ }
60
+ if (isDirectory) {
61
+ if (ignoreDirectories.has(entry.name)) {
62
+ directoriesSkipped.push(relativePath);
63
+ continue;
64
+ }
65
+ queue.push(absolutePath);
66
+ continue;
67
+ }
68
+ if (!isFile) {
69
+ continue;
70
+ }
71
+ try {
72
+ const stat = await fs.lstat(absolutePath);
73
+ if (stat.size > maxFileSizeBytes) {
74
+ filesSkippedOversized += 1;
75
+ continue;
76
+ }
77
+ files.push({
78
+ absolutePath,
79
+ relativePath,
80
+ sizeBytes: Number(stat.size),
81
+ isSymlink,
82
+ });
83
+ }
84
+ catch (error) {
85
+ const message = error instanceof Error ? error.message : "stat failed";
86
+ permissionErrors.push(`${relativePath}: ${message}`);
87
+ }
88
+ }
89
+ }
90
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
91
+ return {
92
+ files,
93
+ directoriesSkipped,
94
+ filesSkippedOversized,
95
+ permissionErrors,
96
+ elapsedMs: Math.round(performance.now() - started),
97
+ };
98
+ }
99
+ /**
100
+ * Targeted existence checks for known config files (O(1) per path).
101
+ */
102
+ export async function findKnownFiles(root, candidates) {
103
+ const found = [];
104
+ await Promise.all(candidates.map(async (relative) => {
105
+ const absolute = path.join(root, relative);
106
+ if (!isPathInsideRoot(root, absolute)) {
107
+ return;
108
+ }
109
+ try {
110
+ await fs.access(absolute);
111
+ found.push(relative.split(path.sep).join("/"));
112
+ }
113
+ catch {
114
+ // missing is fine
115
+ }
116
+ }));
117
+ return found.sort((a, b) => a.localeCompare(b));
118
+ }
@@ -0,0 +1,8 @@
1
+ export { scan } from "./core/scanner/scan.js";
2
+ export { PACKAGE_VERSION } from "./constants.js";
3
+ export type { ScanOptions, ScanResult, RepositoryInfo, Finding, FindingsSummary, Scores, AgentPresence, AgentDetectionResult, CliOptions, ExitCode, } from "./types/index.js";
4
+ export { EXIT_CODES } from "./types/index.js";
5
+ export { agentRegistry } from "./agents/registry.js";
6
+ export { detectAgents } from "./agents/detect-agents.js";
7
+ export { ruleRegistry, getRuleById } from "./core/rules/registry.js";
8
+ export { runRules } from "./core/rules/run-rules.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { scan } from "./core/scanner/scan.js";
2
+ export { PACKAGE_VERSION } from "./constants.js";
3
+ export { EXIT_CODES } from "./types/index.js";
4
+ export { agentRegistry } from "./agents/registry.js";
5
+ export { detectAgents } from "./agents/detect-agents.js";
6
+ export { ruleRegistry, getRuleById } from "./core/rules/registry.js";
7
+ export { runRules } from "./core/rules/run-rules.js";
@@ -0,0 +1,5 @@
1
+ import type { ScanResult } from "../../types/index.js";
2
+ /**
3
+ * Stable machine-readable scan output. No decorative terminal formatting.
4
+ */
5
+ export declare function renderJsonReport(result: ScanResult): string;