oh-my-harness 0.2.1 → 0.3.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.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/dist/cli/commands/init.js +10 -1
  3. package/dist/cli/tool-checker.d.ts +2 -2
  4. package/dist/cli/tool-checker.js +51 -12
  5. package/dist/cli/tui/init-flow.d.ts +2 -0
  6. package/dist/cli/tui/init-flow.js +39 -1
  7. package/dist/core/harness-schema.d.ts +4 -4
  8. package/dist/detector/detectors/cpp.d.ts +2 -0
  9. package/dist/detector/detectors/cpp.js +44 -0
  10. package/dist/detector/detectors/dart.d.ts +2 -0
  11. package/dist/detector/detectors/dart.js +44 -0
  12. package/dist/detector/detectors/dotnet.d.ts +2 -0
  13. package/dist/detector/detectors/dotnet.js +43 -0
  14. package/dist/detector/detectors/elixir.d.ts +2 -0
  15. package/dist/detector/detectors/elixir.js +22 -0
  16. package/dist/detector/detectors/go.d.ts +2 -0
  17. package/dist/detector/detectors/go.js +22 -0
  18. package/dist/detector/detectors/index.d.ts +2 -0
  19. package/dist/detector/detectors/index.js +30 -0
  20. package/dist/detector/detectors/java.d.ts +2 -0
  21. package/dist/detector/detectors/java.js +59 -0
  22. package/dist/detector/detectors/node.d.ts +2 -0
  23. package/dist/detector/detectors/node.js +130 -0
  24. package/dist/detector/detectors/php.d.ts +2 -0
  25. package/dist/detector/detectors/php.js +63 -0
  26. package/dist/detector/detectors/python.d.ts +2 -0
  27. package/dist/detector/detectors/python.js +142 -0
  28. package/dist/detector/detectors/ruby.d.ts +2 -0
  29. package/dist/detector/detectors/ruby.js +67 -0
  30. package/dist/detector/detectors/rust.d.ts +2 -0
  31. package/dist/detector/detectors/rust.js +23 -0
  32. package/dist/detector/detectors/scala.d.ts +2 -0
  33. package/dist/detector/detectors/scala.js +22 -0
  34. package/dist/detector/detectors/swift.d.ts +2 -0
  35. package/dist/detector/detectors/swift.js +55 -0
  36. package/dist/detector/detectors/zig.d.ts +2 -0
  37. package/dist/detector/detectors/zig.js +21 -0
  38. package/dist/detector/project-detector.d.ts +3 -0
  39. package/dist/detector/project-detector.js +33 -0
  40. package/dist/detector/types.d.ts +17 -0
  41. package/dist/detector/types.js +26 -0
  42. package/dist/nl/parse-intent.d.ts +2 -1
  43. package/dist/nl/parse-intent.js +2 -2
  44. package/dist/nl/prompt-templates.d.ts +2 -1
  45. package/dist/nl/prompt-templates.js +32 -6
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -241,7 +241,7 @@ rules:
241
241
  priority: 10
242
242
 
243
243
  enforcement:
244
- preCommit: ["test", "lint", "build"]
244
+ preCommit: ["pnpm test", "npx eslint .", "npx tsc --noEmit"]
245
245
  blockedPaths: [".next/", "node_modules/"]
246
246
  blockedCommands: ["rm -rf", "sudo"]
247
247
  postSave:
@@ -290,6 +290,10 @@ oh-my-harness/
290
290
  │ │ ├── hooks.ts # Executable hook scripts
291
291
  │ │ ├── settings.ts # .claude/settings.json
292
292
  │ │ └── gitignore.ts # .gitignore updater
293
+ │ ├── detector/
294
+ │ │ ├── project-detector.ts # Deterministic project detection
295
+ │ │ ├── types.ts # ProjectFacts, Detector interface
296
+ │ │ └── detectors/ # 14 language detectors
293
297
  │ ├── nl/
294
298
  │ │ ├── parse-intent.ts # claude -p integration
295
299
  │ │ └── prompt-templates.ts # LLM prompt construction
@@ -301,7 +305,7 @@ oh-my-harness/
301
305
  │ ├── nextjs/
302
306
  │ ├── fastapi/
303
307
  │ └── nextjs-fastapi/
304
- └── tests/ # 219 tests (unit + integration)
308
+ └── tests/ # 422 tests (unit + integration)
305
309
  ```
306
310
 
307
311
  ---
@@ -354,6 +358,7 @@ No code changes required. The registry auto-discovers it.
354
358
  - [x] `npx oh-my-harness` — zero-install usage
355
359
  - [x] `oh-my-harness sync` — regenerate from harness.yaml
356
360
  - [x] Building block catalog — 10 verified hook templates
361
+ - [x] Project detector — 14 language auto-detection for accurate NL generation
357
362
  - [ ] Cursor (`.cursor/rules/`) emitter
358
363
  - [ ] Codex (`AGENTS.md`) emitter
359
364
  - [ ] GitHub Copilot emitter
@@ -5,6 +5,7 @@ import { PresetRegistry } from "../../core/preset-registry.js";
5
5
  import { mergePresets } from "../../core/config-merger.js";
6
6
  import { generate } from "../../core/generator.js";
7
7
  import { generateHarnessConfig } from "../../nl/parse-intent.js";
8
+ import { detectProject } from "../../detector/project-detector.js";
8
9
  import { harnessToMergedConfigV2 } from "../../core/harness-converter-v2.js";
9
10
  import { createDefaultRegistry } from "../../catalog/registry.js";
10
11
  function getDefaultPresetsDir() {
@@ -107,6 +108,14 @@ async function initWithNL(projectDir, presetsDir, options) {
107
108
  return;
108
109
  }
109
110
  console.log(`Generating harness config for: "${description}"`);
111
+ // Detect project facts for richer prompt context
112
+ let facts;
113
+ try {
114
+ facts = await detectProject(projectDir);
115
+ }
116
+ catch {
117
+ // Non-fatal: continue with no facts
118
+ }
110
119
  // Load catalog blocks so LLM knows available building blocks
111
120
  const registry = await createDefaultRegistry();
112
121
  const catalogBlocks = registry.list().map((b) => ({
@@ -117,7 +126,7 @@ async function initWithNL(projectDir, presetsDir, options) {
117
126
  matcher: b.matcher,
118
127
  params: b.params.map((p) => ({ name: p.name, type: p.type, description: p.description, required: p.required, default: p.default })),
119
128
  }));
120
- const harness = await generateHarnessConfig(description, options.nlRunner, catalogBlocks);
129
+ const harness = await generateHarnessConfig(description, options.nlRunner, catalogBlocks, facts);
121
130
  // Show summary
122
131
  const stackNames = harness.project.stacks.map((s) => `${s.name} (${s.framework})`).join(", ");
123
132
  console.log(`\nStacks: ${stackNames}`);
@@ -6,10 +6,10 @@ export interface ToolCheck {
6
6
  installCmd: string;
7
7
  installed: boolean;
8
8
  }
9
- interface ToolRef {
9
+ export interface ToolRef {
10
10
  name: string;
11
11
  source: string;
12
+ lookupCommand: string;
12
13
  }
13
14
  export declare function extractToolNames(config: HarnessConfig): ToolRef[];
14
15
  export declare function checkReferencedTools(config: HarnessConfig): Promise<ToolCheck[]>;
15
- export {};
@@ -17,27 +17,66 @@ function extractBinary(command) {
17
17
  const trimmed = command.trim();
18
18
  if (!trimmed)
19
19
  return undefined;
20
- return trimmed.split(/\s+/)[0];
20
+ const parts = trimmed.split(/\s+/);
21
+ // Skip gradle wrapper commands (managed by project)
22
+ if (parts[0] === "./gradlew" || parts[0] === "gradlew")
23
+ return undefined;
24
+ // npx [options] <tool> ... → skip option flags, extract first non-option token
25
+ if (parts[0] === "npx") {
26
+ let i = 1;
27
+ while (i < parts.length && parts[i].startsWith("-")) {
28
+ if (parts[i] === "-p" || parts[i] === "--package")
29
+ i += 2;
30
+ else
31
+ i += 1;
32
+ }
33
+ return i < parts.length ? { name: parts[i], lookupCommand: "npx" } : undefined;
34
+ }
35
+ // npm exec <tool> → extract tool; other npm subcommands → skip
36
+ if (parts[0] === "npm") {
37
+ if (parts[1] === "exec" && parts.length > 2)
38
+ return { name: parts[2], lookupCommand: "npm" };
39
+ return undefined;
40
+ }
41
+ // pnpm exec <tool> → wrapper lookup; pnpm dlx <tool> → direct lookup
42
+ if (parts[0] === "pnpm") {
43
+ if (parts[1] === "exec" && parts.length > 2)
44
+ return { name: parts[2], lookupCommand: "pnpm" };
45
+ if (parts[1] === "dlx" && parts.length > 2)
46
+ return { name: parts[2], lookupCommand: "pnpm" };
47
+ return undefined;
48
+ }
49
+ // yarn dlx <tool> → direct lookup
50
+ if (parts[0] === "yarn") {
51
+ if (parts[1] === "dlx" && parts.length > 2)
52
+ return { name: parts[2], lookupCommand: "yarn" };
53
+ return undefined;
54
+ }
55
+ // poetry run <tool> → check poetry existence
56
+ if (parts[0] === "poetry" && parts[1] === "run" && parts.length > 2) {
57
+ return { name: parts[2], lookupCommand: "poetry" };
58
+ }
59
+ return { name: parts[0], lookupCommand: parts[0] };
21
60
  }
22
61
  export function extractToolNames(config) {
23
62
  const seen = new Set();
24
63
  const tools = [];
25
64
  for (const cmd of config.enforcement.preCommit) {
26
- const name = extractBinary(cmd);
27
- if (!name)
65
+ const result = extractBinary(cmd);
66
+ if (!result)
28
67
  continue;
29
- if (!seen.has(name)) {
30
- seen.add(name);
31
- tools.push({ name, source: "pre-commit" });
68
+ if (!seen.has(result.name)) {
69
+ seen.add(result.name);
70
+ tools.push({ name: result.name, lookupCommand: result.lookupCommand, source: "pre-commit" });
32
71
  }
33
72
  }
34
73
  for (const ps of config.enforcement.postSave) {
35
- const name = extractBinary(ps.command);
36
- if (!name)
74
+ const result = extractBinary(ps.command);
75
+ if (!result)
37
76
  continue;
38
- if (!seen.has(name)) {
39
- seen.add(name);
40
- tools.push({ name, source: "post-save hook" });
77
+ if (!seen.has(result.name)) {
78
+ seen.add(result.name);
79
+ tools.push({ name: result.name, lookupCommand: result.lookupCommand, source: "post-save hook" });
41
80
  }
42
81
  }
43
82
  return tools;
@@ -58,7 +97,7 @@ export async function checkReferencedTools(config) {
58
97
  const refs = extractToolNames(config);
59
98
  const results = [];
60
99
  for (const ref of refs) {
61
- const installed = await commandExists(ref.name);
100
+ const installed = await commandExists(ref.lookupCommand);
62
101
  results.push({
63
102
  name: ref.name,
64
103
  command: ref.name,
@@ -1,7 +1,9 @@
1
1
  import type { DepCheck } from "../deps-checker.js";
2
2
  import type { HarnessConfig } from "../../core/harness-schema.js";
3
+ import type { ProjectFacts } from "../../detector/types.js";
3
4
  export declare function formatDepResults(deps: DepCheck[]): string;
4
5
  export declare function formatConfigSummary(config: HarnessConfig): string;
6
+ export declare function formatProjectFacts(facts: ProjectFacts): string;
5
7
  export declare function runInitTUI(options?: {
6
8
  projectDir?: string;
7
9
  presetsDir?: string;
@@ -12,6 +12,7 @@ import { generate } from "../../core/generator.js";
12
12
  import { generateHarnessConfig } from "../../nl/parse-intent.js";
13
13
  import { harnessToMergedConfig } from "../../core/harness-converter.js";
14
14
  import { HarnessConfigSchema } from "../../core/harness-schema.js";
15
+ import { detectProject } from "../../detector/project-detector.js";
15
16
  function getDefaultPresetsDir() {
16
17
  return path.resolve(import.meta.dirname, "../../../presets");
17
18
  }
@@ -79,6 +80,25 @@ export function formatConfigSummary(config) {
79
80
  }
80
81
  return lines.join("\n");
81
82
  }
83
+ export function formatProjectFacts(facts) {
84
+ const lines = [];
85
+ const entries = [
86
+ ["Languages", facts.languages],
87
+ ["Frameworks", facts.frameworks],
88
+ ["Package managers", facts.packageManagers],
89
+ ["Test commands", facts.testCommands],
90
+ ["Lint commands", facts.lintCommands],
91
+ ["Build commands", facts.buildCommands],
92
+ ["Typecheck", facts.typecheckCommands],
93
+ ["Blocked paths", facts.blockedPaths],
94
+ ];
95
+ for (const [label, values] of entries) {
96
+ if (values.length > 0) {
97
+ lines.push(` ${chalk.bold(label)}: ${values.join(", ")}`);
98
+ }
99
+ }
100
+ return lines.length > 0 ? lines.join("\n") : ` ${chalk.dim("No project signals detected")}`;
101
+ }
82
102
  function handleCancel(value) {
83
103
  if (p.isCancel(value)) {
84
104
  p.cancel("Operation cancelled.");
@@ -114,6 +134,24 @@ export async function runInitTUI(options) {
114
134
  if (!claudeInstalled) {
115
135
  p.log.warn("claude CLI not installed. AI-powered mode will not be available.");
116
136
  }
137
+ // Step 2.5: Project Detection
138
+ const detectSpinner = p.spinner();
139
+ detectSpinner.start("Detecting project type...");
140
+ let projectFacts;
141
+ try {
142
+ const facts = await detectProject(projectDir);
143
+ const hasAnyFacts = facts.languages.length > 0 || facts.frameworks.length > 0;
144
+ if (hasAnyFacts) {
145
+ projectFacts = facts;
146
+ }
147
+ detectSpinner.stop("Project detected");
148
+ }
149
+ catch {
150
+ detectSpinner.stop("Project detection skipped");
151
+ }
152
+ if (projectFacts) {
153
+ p.note(formatProjectFacts(projectFacts), "Detected Project");
154
+ }
117
155
  const modeOptions = [];
118
156
  if (claudeInstalled) {
119
157
  modeOptions.push({
@@ -163,7 +201,7 @@ export async function runInitTUI(options) {
163
201
  const genSpinner = p.spinner();
164
202
  genSpinner.start("Generating harness configuration...");
165
203
  try {
166
- harnessConfig = await generateHarnessConfig(description);
204
+ harnessConfig = await generateHarnessConfig(description, undefined, undefined, projectFacts);
167
205
  genSpinner.stop("Configuration generated");
168
206
  }
169
207
  catch (err) {
@@ -80,16 +80,16 @@ export declare const HarnessConfigSchema: z.ZodObject<{
80
80
  pattern: string;
81
81
  }>, "many">>;
82
82
  }, "strip", z.ZodTypeAny, {
83
- preCommit: string[];
84
83
  blockedPaths: string[];
84
+ preCommit: string[];
85
85
  blockedCommands: string[];
86
86
  postSave: {
87
87
  command: string;
88
88
  pattern: string;
89
89
  }[];
90
90
  }, {
91
- preCommit?: string[] | undefined;
92
91
  blockedPaths?: string[] | undefined;
92
+ preCommit?: string[] | undefined;
93
93
  blockedCommands?: string[] | undefined;
94
94
  postSave?: {
95
95
  command: string;
@@ -145,8 +145,8 @@ export declare const HarnessConfigSchema: z.ZodObject<{
145
145
  priority: number;
146
146
  }[];
147
147
  enforcement: {
148
- preCommit: string[];
149
148
  blockedPaths: string[];
149
+ preCommit: string[];
150
150
  blockedCommands: string[];
151
151
  postSave: {
152
152
  command: string;
@@ -173,8 +173,8 @@ export declare const HarnessConfigSchema: z.ZodObject<{
173
173
  priority?: number | undefined;
174
174
  }[];
175
175
  enforcement: {
176
- preCommit?: string[] | undefined;
177
176
  blockedPaths?: string[] | undefined;
177
+ preCommit?: string[] | undefined;
178
178
  blockedCommands?: string[] | undefined;
179
179
  postSave?: {
180
180
  command: string;
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const cppDetector: Detector;
@@ -0,0 +1,44 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ export const cppDetector = {
4
+ name: "cpp",
5
+ detect: async (projectDir) => {
6
+ const cmakeFile = path.join(projectDir, "CMakeLists.txt");
7
+ const makeFile = path.join(projectDir, "Makefile");
8
+ const mesonFile = path.join(projectDir, "meson.build");
9
+ const [hasCMake, hasMakefile, hasMeson] = await Promise.all([
10
+ fs.access(cmakeFile).then(() => true).catch(() => false),
11
+ fs.access(makeFile).then(() => true).catch(() => false),
12
+ fs.access(mesonFile).then(() => true).catch(() => false),
13
+ ]);
14
+ const blockedPaths = ["build/", "cmake-build-*/"];
15
+ if (hasCMake) {
16
+ return {
17
+ languages: ["c", "cpp"],
18
+ buildCommands: ["cmake --build build"],
19
+ testCommands: ["ctest --test-dir build"],
20
+ blockedPaths,
21
+ detectedFiles: ["CMakeLists.txt"],
22
+ };
23
+ }
24
+ if (hasMeson) {
25
+ return {
26
+ languages: ["c", "cpp"],
27
+ buildCommands: ["meson compile -C build"],
28
+ testCommands: ["meson test -C build"],
29
+ blockedPaths,
30
+ detectedFiles: ["meson.build"],
31
+ };
32
+ }
33
+ if (hasMakefile) {
34
+ return {
35
+ languages: ["c"],
36
+ buildCommands: ["make"],
37
+ testCommands: ["make test"],
38
+ blockedPaths,
39
+ detectedFiles: ["Makefile"],
40
+ };
41
+ }
42
+ return {};
43
+ },
44
+ };
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const dartDetector: Detector;
@@ -0,0 +1,44 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ export const dartDetector = {
4
+ name: "dart",
5
+ detect: async (projectDir) => {
6
+ let entries;
7
+ try {
8
+ entries = await fs.readdir(projectDir);
9
+ }
10
+ catch {
11
+ return {};
12
+ }
13
+ if (!entries.includes("pubspec.yaml"))
14
+ return {};
15
+ let pubspecContent = "";
16
+ try {
17
+ pubspecContent = await fs.readFile(path.join(projectDir, "pubspec.yaml"), "utf-8");
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ const isFlutter = pubspecContent.includes("flutter:");
23
+ const languages = ["dart"];
24
+ const frameworks = [];
25
+ const packageManagers = [];
26
+ const testCommands = [];
27
+ const buildCommands = [];
28
+ const lintCommands = ["dart analyze"];
29
+ const blockedPaths = [".dart_tool/", "build/"];
30
+ const detectedFiles = ["pubspec.yaml"];
31
+ if (isFlutter) {
32
+ frameworks.push("flutter");
33
+ packageManagers.push("flutter");
34
+ testCommands.push("flutter test");
35
+ buildCommands.push("flutter build");
36
+ }
37
+ else {
38
+ packageManagers.push("pub");
39
+ testCommands.push("dart test");
40
+ buildCommands.push("dart compile exe");
41
+ }
42
+ return { languages, frameworks, packageManagers, testCommands, buildCommands, lintCommands, blockedPaths, detectedFiles };
43
+ },
44
+ };
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const dotnetDetector: Detector;
@@ -0,0 +1,43 @@
1
+ import * as fs from "fs/promises";
2
+ export const dotnetDetector = {
3
+ name: "dotnet",
4
+ detect: async (projectDir) => {
5
+ let entries;
6
+ try {
7
+ entries = await fs.readdir(projectDir);
8
+ }
9
+ catch {
10
+ return {};
11
+ }
12
+ const sorted = [...entries].sort();
13
+ const csprojFiles = sorted.filter((e) => e.endsWith(".csproj"));
14
+ const fsprojFiles = sorted.filter((e) => e.endsWith(".fsproj"));
15
+ const slnFiles = sorted.filter((e) => e.endsWith(".sln"));
16
+ if (!csprojFiles.length && !fsprojFiles.length && !slnFiles.length) {
17
+ return {};
18
+ }
19
+ const detectedFiles = [];
20
+ const languages = [];
21
+ if (csprojFiles.length > 0) {
22
+ detectedFiles.push(...csprojFiles);
23
+ languages.push("csharp");
24
+ }
25
+ if (fsprojFiles.length > 0) {
26
+ detectedFiles.push(...fsprojFiles);
27
+ languages.push("fsharp");
28
+ }
29
+ if (slnFiles.length > 0) {
30
+ detectedFiles.push(...slnFiles);
31
+ }
32
+ return {
33
+ ...(languages.length > 0 ? { languages } : {}),
34
+ frameworks: ["dotnet"],
35
+ packageManagers: ["nuget"],
36
+ buildCommands: ["dotnet build"],
37
+ testCommands: ["dotnet test"],
38
+ lintCommands: ["dotnet format"],
39
+ blockedPaths: ["bin/", "obj/"],
40
+ detectedFiles,
41
+ };
42
+ },
43
+ };
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const elixirDetector: Detector;
@@ -0,0 +1,22 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ export const elixirDetector = {
4
+ name: "elixir",
5
+ detect: async (projectDir) => {
6
+ try {
7
+ await fs.access(path.join(projectDir, "mix.exs"));
8
+ }
9
+ catch {
10
+ return {};
11
+ }
12
+ return {
13
+ languages: ["elixir"],
14
+ packageManagers: ["mix"],
15
+ testCommands: ["mix test"],
16
+ lintCommands: ["mix credo"],
17
+ buildCommands: ["mix compile"],
18
+ blockedPaths: ["_build/", "deps/"],
19
+ detectedFiles: ["mix.exs"],
20
+ };
21
+ },
22
+ };
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const goDetector: Detector;
@@ -0,0 +1,22 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ export const goDetector = {
4
+ name: "go",
5
+ detect: async (projectDir) => {
6
+ try {
7
+ await fs.access(path.join(projectDir, "go.mod"));
8
+ }
9
+ catch {
10
+ return {};
11
+ }
12
+ return {
13
+ languages: ["go"],
14
+ packageManagers: ["go modules"],
15
+ testCommands: ["go test ./..."],
16
+ lintCommands: ["golangci-lint run"],
17
+ buildCommands: ["go build ./..."],
18
+ blockedPaths: ["vendor/"],
19
+ detectedFiles: ["go.mod"],
20
+ };
21
+ },
22
+ };
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const allDetectors: Detector[];
@@ -0,0 +1,30 @@
1
+ import { nodeDetector } from "./node.js";
2
+ import { pythonDetector } from "./python.js";
3
+ import { swiftDetector } from "./swift.js";
4
+ import { goDetector } from "./go.js";
5
+ import { rustDetector } from "./rust.js";
6
+ import { javaDetector } from "./java.js";
7
+ import { cppDetector } from "./cpp.js";
8
+ import { dotnetDetector } from "./dotnet.js";
9
+ import { phpDetector } from "./php.js";
10
+ import { rubyDetector } from "./ruby.js";
11
+ import { dartDetector } from "./dart.js";
12
+ import { elixirDetector } from "./elixir.js";
13
+ import { scalaDetector } from "./scala.js";
14
+ import { zigDetector } from "./zig.js";
15
+ export const allDetectors = [
16
+ nodeDetector,
17
+ pythonDetector,
18
+ swiftDetector,
19
+ goDetector,
20
+ rustDetector,
21
+ javaDetector,
22
+ cppDetector,
23
+ dotnetDetector,
24
+ phpDetector,
25
+ rubyDetector,
26
+ dartDetector,
27
+ elixirDetector,
28
+ scalaDetector,
29
+ zigDetector,
30
+ ];
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const javaDetector: Detector;
@@ -0,0 +1,59 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ async function fileExists(filePath) {
4
+ try {
5
+ await fs.access(filePath);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export const javaDetector = {
13
+ name: "java",
14
+ detect: async (projectDir) => {
15
+ const pomPath = path.join(projectDir, "pom.xml");
16
+ const gradlePath = path.join(projectDir, "build.gradle");
17
+ const gradleKtsPath = path.join(projectDir, "build.gradle.kts");
18
+ const gradlewPath = path.join(projectDir, "gradlew");
19
+ const [hasPom, hasGradle, hasGradleKts, hasGradlew] = await Promise.all([
20
+ fileExists(pomPath),
21
+ fileExists(gradlePath),
22
+ fileExists(gradleKtsPath),
23
+ fileExists(gradlewPath),
24
+ ]);
25
+ if (hasPom) {
26
+ return {
27
+ languages: ["java"],
28
+ packageManagers: ["maven"],
29
+ testCommands: ["mvn test"],
30
+ buildCommands: ["mvn compile"],
31
+ blockedPaths: ["target/"],
32
+ detectedFiles: ["pom.xml"],
33
+ };
34
+ }
35
+ const gradleCmd = hasGradlew ? "./gradlew" : "gradle";
36
+ const gradlewFiles = hasGradlew ? ["gradlew"] : [];
37
+ if (hasGradleKts) {
38
+ return {
39
+ languages: ["java", "kotlin"],
40
+ packageManagers: ["gradle"],
41
+ testCommands: [`${gradleCmd} test`],
42
+ buildCommands: [`${gradleCmd} build`],
43
+ blockedPaths: ["build/", ".gradle/"],
44
+ detectedFiles: ["build.gradle.kts", ...gradlewFiles],
45
+ };
46
+ }
47
+ if (hasGradle) {
48
+ return {
49
+ languages: ["java"],
50
+ packageManagers: ["gradle"],
51
+ testCommands: [`${gradleCmd} test`],
52
+ buildCommands: [`${gradleCmd} build`],
53
+ blockedPaths: ["build/", ".gradle/"],
54
+ detectedFiles: ["build.gradle", ...gradlewFiles],
55
+ };
56
+ }
57
+ return {};
58
+ },
59
+ };
@@ -0,0 +1,2 @@
1
+ import type { Detector } from "../types.js";
2
+ export declare const nodeDetector: Detector;