@kaluchi/jdtbridge 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,8 +45,10 @@ jdt source <FQMN> # (alias: src) source cod
45
45
 
46
46
  ```bash
47
47
  jdt build [--project <name>] [--clean] # (alias: b) build project
48
- jdt test <FQMN> [--timeout N] # run JUnit test class or method
49
- jdt test --project <name> [--package <pkg>] # run tests in project/package
48
+ jdt test run <FQN> [-f] [-q] # launch tests (non-blocking)
49
+ jdt test run --project <name> [-f] # run tests in project
50
+ jdt test status <session> [-f] [--all] [--ignored] # show test progress/results
51
+ jdt test sessions # list test sessions
50
52
  ```
51
53
 
52
54
  All commands auto-refresh from disk. `build` is the only command that triggers explicit builds.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaluchi/jdtbridge",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for Eclipse JDT Bridge — semantic Java analysis via Eclipse JDT SearchEngine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,83 @@
1
+ // Configure Claude Code for JDT Bridge projects.
2
+
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { execSync } from "node:child_process";
6
+
7
+ const JDT_HOOK_COMMAND = `node -e "d=JSON.parse(require('fs').readFileSync(0,'utf8'));d.tool_input?.command?.startsWith('jdt ')&&process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'allow'}}))"`;
8
+
9
+ /**
10
+ * Find the project root (git root or cwd).
11
+ * @returns {string}
12
+ */
13
+ export function findProjectRoot() {
14
+ try {
15
+ return execSync("git rev-parse --show-toplevel", {
16
+ encoding: "utf8",
17
+ stdio: ["pipe", "pipe", "pipe"],
18
+ }).trim();
19
+ } catch {
20
+ return process.cwd();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Merge JDT Bridge settings into a Claude Code settings object.
26
+ * Idempotent — safe to call multiple times.
27
+ * @param {object} settings existing settings (mutated in place)
28
+ * @returns {object} the mutated settings
29
+ */
30
+ export function mergeJdtSettings(settings) {
31
+ // Permissions
32
+ if (!settings.permissions) settings.permissions = {};
33
+ const allow = settings.permissions.allow || [];
34
+ if (!allow.includes("Bash(jdt *)")) allow.push("Bash(jdt *)");
35
+ settings.permissions.allow = allow;
36
+
37
+ // Hook
38
+ if (!settings.hooks) settings.hooks = {};
39
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
40
+ const existing = settings.hooks.PreToolUse.find(
41
+ (h) =>
42
+ h.matcher === "Bash" &&
43
+ h.hooks?.some((hk) => hk.command === JDT_HOOK_COMMAND),
44
+ );
45
+ if (!existing) {
46
+ settings.hooks.PreToolUse.push({
47
+ matcher: "Bash",
48
+ hooks: [{ type: "command", command: JDT_HOOK_COMMAND }],
49
+ });
50
+ }
51
+
52
+ return settings;
53
+ }
54
+
55
+ /**
56
+ * Write Claude Code settings for JDT Bridge to a project.
57
+ * Creates .claude/settings.json if it doesn't exist,
58
+ * merges into existing if it does.
59
+ * @param {string} [root] project root (default: git root or cwd)
60
+ * @returns {{ file: string, settings: object }}
61
+ */
62
+ export function installClaudeSettings(root) {
63
+ if (!root) root = findProjectRoot();
64
+
65
+ const dir = join(root, ".claude");
66
+ const file = join(dir, "settings.json");
67
+
68
+ let settings = {};
69
+ if (existsSync(file)) {
70
+ try {
71
+ settings = JSON.parse(readFileSync(file, "utf8"));
72
+ } catch {
73
+ // corrupt — overwrite
74
+ }
75
+ }
76
+
77
+ mergeJdtSettings(settings);
78
+
79
+ mkdirSync(dir, { recursive: true });
80
+ writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
81
+
82
+ return { file, settings };
83
+ }
package/src/cli.mjs CHANGED
@@ -11,7 +11,9 @@ import { implementors, help as implementorsHelp } from "./commands/implementors.
11
11
  import { typeInfo, help as typeInfoHelp } from "./commands/type-info.mjs";
12
12
  import { source, help as sourceHelp } from "./commands/source.mjs";
13
13
  import { build, help as buildHelp } from "./commands/build.mjs";
14
- import { test, help as testHelp } from "./commands/test.mjs";
14
+ import { testRun, help as testRunHelp } from "./commands/test-run.mjs";
15
+ import { testStatus, help as testStatusHelp } from "./commands/test-status.mjs";
16
+ import { testSessions, help as testSessionsHelp } from "./commands/test-sessions.mjs";
15
17
  import { errors, help as errorsHelp } from "./commands/errors.mjs";
16
18
  import {
17
19
  organizeImports,
@@ -93,6 +95,36 @@ async function launchDispatch(args) {
93
95
  await cmd.fn(rest);
94
96
  }
95
97
 
98
+ const testSubcommands = {
99
+ run: { fn: testRun, help: testRunHelp },
100
+ status: { fn: testStatus, help: testStatusHelp },
101
+ sessions: { fn: testSessions, help: testSessionsHelp },
102
+ };
103
+
104
+ const testHelp = `Run and monitor JUnit tests via Eclipse's built-in runner.
105
+
106
+ Subcommands:
107
+ jdt test run <FQN> [-f] [-q] launch tests (non-blocking)
108
+ jdt test status <session> [-f] [--all] show test progress/results
109
+ jdt test sessions list test sessions
110
+
111
+ Use "jdt help test <subcommand>" for details.`;
112
+
113
+ async function testDispatch(args) {
114
+ const [sub, ...rest] = args;
115
+ if (!sub || sub === "--help") {
116
+ console.log(testHelp);
117
+ return;
118
+ }
119
+ const cmd = testSubcommands[sub];
120
+ if (!cmd) {
121
+ console.error(`Unknown test subcommand: ${sub}`);
122
+ console.log(testHelp);
123
+ process.exit(1);
124
+ }
125
+ await cmd.fn(rest);
126
+ }
127
+
96
128
  const commands = {
97
129
  projects: { fn: projects, help: projectsHelp },
98
130
  "project-info": { fn: projectInfo, help: projectInfoHelp },
@@ -104,7 +136,7 @@ const commands = {
104
136
  "type-info": { fn: typeInfo, help: typeInfoHelp },
105
137
  source: { fn: source, help: sourceHelp },
106
138
  build: { fn: build, help: buildHelp },
107
- test: { fn: test, help: testHelp },
139
+ test: { fn: testDispatch, help: testHelp },
108
140
  errors: { fn: errors, help: errorsHelp },
109
141
  "organize-imports": { fn: organizeImports, help: organizeImportsHelp },
110
142
  format: { fn: format, help: formatHelp },
@@ -162,12 +194,13 @@ Search & navigation:
162
194
  hierarchy${fmtAliases("hierarchy")} <FQN> full hierarchy (supers + interfaces + subtypes)
163
195
  implementors${fmtAliases("implementors")} <FQMN> implementations of interface method
164
196
  type-info${fmtAliases("type-info")} <FQN> class overview (fields, methods, line numbers)
165
- source${fmtAliases("source")} <FQMN> type or method source code (project and libraries)
197
+ source${fmtAliases("source")} <FQMN> source + resolved references (navigation)
166
198
 
167
199
  Testing & building:
168
200
  build${fmtAliases("build")} [--project <name>] [--clean] build project (incremental or clean)
169
- test <FQMN> run JUnit test class or method
170
- test --project <name> [--package <pkg>] run tests in project/package
201
+ test run <FQN> [-f] [-q] launch tests (non-blocking)
202
+ test status <session> [-f] [--all] show test progress/results
203
+ test sessions list test sessions
171
204
 
172
205
  Diagnostics:
173
206
  errors${fmtAliases("errors")} [--file <path>] [--project <name>] compilation errors
@@ -215,6 +248,8 @@ export async function run(argv) {
215
248
  const resolved = topic ? resolve(topic) : null;
216
249
  if (resolved === "launch" && rest[1] && launchSubcommands[rest[1]]) {
217
250
  console.log(launchSubcommands[rest[1]].help);
251
+ } else if (resolved === "test" && rest[1] && testSubcommands[rest[1]]) {
252
+ console.log(testSubcommands[rest[1]].help);
218
253
  } else if (resolved) {
219
254
  console.log(commands[resolved].help);
220
255
  } else if (topic) {
package/src/client.mjs CHANGED
@@ -196,6 +196,55 @@ export function getStream(path, dest) {
196
196
  });
197
197
  }
198
198
 
199
+ /**
200
+ * HTTP GET streaming with line-by-line callback. Designed for
201
+ * JSONL (newline-delimited JSON) endpoints. Calls onLine for
202
+ * each complete line received.
203
+ * Resolves when the server closes the connection.
204
+ * @param {string} path
205
+ * @param {(line: string) => void} onLine
206
+ * @returns {Promise<void>}
207
+ */
208
+ export function getStreamLines(path, onLine) {
209
+ const inst = connect();
210
+ return new Promise((resolve, reject) => {
211
+ const req = request(
212
+ {
213
+ hostname: "127.0.0.1",
214
+ port: inst.port,
215
+ path,
216
+ method: "GET",
217
+ timeout: 0,
218
+ headers: authHeaders(),
219
+ },
220
+ (res) => {
221
+ if (res.statusCode !== 200) {
222
+ let data = "";
223
+ res.on("data", (chunk) => (data += chunk));
224
+ res.on("end", () => reject(new Error(data.trim())));
225
+ return;
226
+ }
227
+ let buffer = "";
228
+ res.on("data", (chunk) => {
229
+ buffer += chunk;
230
+ const lines = buffer.split("\n");
231
+ buffer = lines.pop();
232
+ for (const line of lines) {
233
+ if (line.trim()) onLine(line);
234
+ }
235
+ });
236
+ res.on("end", () => {
237
+ if (buffer.trim()) onLine(buffer);
238
+ resolve();
239
+ });
240
+ res.on("error", reject);
241
+ },
242
+ );
243
+ req.on("error", reject);
244
+ req.end();
245
+ });
246
+ }
247
+
199
248
  /**
200
249
  * Check if error is a connection refused error.
201
250
  * @param {Error} e
@@ -50,6 +50,6 @@ export const openHelp = `Open a type or method in the Eclipse editor.
50
50
  Usage: jdt open <FQN>[#method[(param types)]]
51
51
 
52
52
  Examples:
53
- jdt open app.m8.dao.StaffDaoImpl
54
- jdt open app.m8.dao.StaffDaoImpl#getStaff
55
- jdt open "app.m8.dao.StaffDaoImpl#save(Order)"`;
53
+ jdt open com.example.dao.UserDaoImpl
54
+ jdt open com.example.dao.UserDaoImpl#getStaff
55
+ jdt open "com.example.dao.UserDaoImpl#save(Order)"`;
@@ -45,4 +45,4 @@ export const help = `Show full type hierarchy: superclasses, interfaces, and sub
45
45
 
46
46
  Usage: jdt hierarchy <FQN>
47
47
 
48
- Example: jdt hierarchy app.m8.web.client.AGMEntryPoint`;
48
+ Example: jdt hierarchy com.example.client.AppEntryPoint`;
@@ -35,4 +35,4 @@ export const help = `Find implementations of an interface method across all impl
35
35
  Usage: jdt implementors <FQN>#<method>[(param types)]
36
36
 
37
37
  Examples:
38
- jdt implementors app.m8.web.shared.core.HasId#getId`;
38
+ jdt implementors com.example.core.HasId#getId`;
@@ -109,11 +109,11 @@ Usage: jdt rename <FQN>[#method[(param types)]] <newName>
109
109
  jdt rename <FQN> <newName> [--field <old>]
110
110
 
111
111
  Examples:
112
- jdt rename app.m8.dto.Foo Bar
113
- jdt rename app.m8.dto.Foo#getFoo getBar`;
112
+ jdt rename com.example.dto.Foo Bar
113
+ jdt rename com.example.dto.Foo#getFoo getBar`;
114
114
 
115
115
  export const moveHelp = `Move a type to another package (updates all references).
116
116
 
117
117
  Usage: jdt move <FQN> <target.package>
118
118
 
119
- Example: jdt move app.m8.dto.Foo app.m8.dto.shared`;
119
+ Example: jdt move com.example.dto.Foo com.example.dto.shared`;
@@ -48,7 +48,7 @@ Flags:
48
48
  --field <name> find references to a field
49
49
 
50
50
  Examples:
51
- jdt references app.m8.dto.web.core.IdOrgRoot
52
- jdt references app.m8.dao.StaffDaoImpl#getStaff
53
- jdt references "app.m8.dao.StaffDaoImpl#save(Order)"
54
- jdt references app.m8.dao.StaffDaoImpl --field staffCache`;
51
+ jdt references com.example.dto.BaseEntity
52
+ jdt references com.example.dao.UserDaoImpl#getStaff
53
+ jdt references "com.example.dao.UserDaoImpl#save(Order)"
54
+ jdt references com.example.dao.UserDaoImpl --field staffCache`;
@@ -357,11 +357,26 @@ export async function setup(args) {
357
357
  await runCheck(config);
358
358
  } else if (args.includes("--remove")) {
359
359
  await runRemove(config);
360
+ } else if (args.includes("--claude")) {
361
+ await setupClaude();
360
362
  } else {
361
363
  await runInstall(config, flags);
362
364
  }
363
365
  }
364
366
 
367
+ async function setupClaude() {
368
+ const { installClaudeSettings } = await import("../claude-setup.mjs");
369
+ const { file } = installClaudeSettings();
370
+
371
+ console.log(`${green("✓")} Claude Code settings written to ${file}`);
372
+ console.log();
373
+ console.log(" Installed:");
374
+ console.log(" - Bash(jdt *) permission rule");
375
+ console.log(" - PreToolUse hook for # workaround (issue #34061)");
376
+ console.log();
377
+ console.log(" Restart Claude Code to apply.");
378
+ }
379
+
365
380
  export const help = `Set up the Eclipse JDT Bridge plugin.
366
381
 
367
382
  Usage: jdt setup [options]
@@ -370,6 +385,7 @@ Modes:
370
385
  (default) build plugin from source, install into Eclipse
371
386
  --check show status of all components (diagnostic only)
372
387
  --remove uninstall the plugin from Eclipse
388
+ --claude configure Claude Code for this project (permissions + hooks)
373
389
 
374
390
  Options:
375
391
  --eclipse <path> Eclipse installation directory
@@ -1,46 +1,272 @@
1
- import { getRaw } from "../client.mjs";
2
- import { extractPositional, parseFlags, parseFqmn } from "../args.mjs";
3
- import { stripProject } from "../paths.mjs";
1
+ import { get } from "../client.mjs";
2
+ import { extractPositional, parseFqmn } from "../args.mjs";
4
3
 
5
4
  export async function source(args) {
6
- const pos = extractPositional(args);
7
- const flags = parseFlags(args);
8
- const parsed = parseFqmn(pos[0]);
9
- const fqn = parsed.className;
10
- if (!fqn) {
11
- console.error("Usage: source <FQN>[#method[(param types)]]");
5
+ const jsonFlag = args.includes("--json");
6
+ const pos = extractPositional(args).filter((a) => a !== "--json");
7
+ if (pos.length === 0) {
8
+ console.error("Usage: source <FQMN> [<FQMN> ...] [--json]");
12
9
  process.exit(1);
13
10
  }
14
- const method = parsed.method || pos[1];
15
- let url = `/source?class=${encodeURIComponent(fqn)}`;
16
- if (method) url += `&method=${encodeURIComponent(method)}`;
11
+
12
+ const results = await Promise.all(pos.map((arg) => fetchOne(arg)));
13
+
14
+ if (jsonFlag) {
15
+ console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
16
+ return;
17
+ }
18
+
19
+ const blocks = [];
20
+ for (const r of results) {
21
+ if (r.error) {
22
+ console.error(r.error);
23
+ } else if (Array.isArray(r)) {
24
+ blocks.push(...r.map(formatMarkdown));
25
+ } else {
26
+ blocks.push(formatMarkdown(r));
27
+ }
28
+ }
29
+ if (blocks.length === 0) process.exit(1);
30
+ console.log(blocks.join("\n\n---\n\n"));
31
+ }
32
+
33
+ async function fetchOne(fqmn) {
34
+ const parsed = parseFqmn(fqmn);
35
+ if (!parsed.className) return { error: `Invalid FQMN: ${fqmn}` };
36
+ let url = `/source?class=${encodeURIComponent(parsed.className)}`;
37
+ if (parsed.method) url += `&method=${encodeURIComponent(parsed.method)}`;
17
38
  if (parsed.paramTypes) {
18
39
  url += `&paramTypes=${encodeURIComponent(parsed.paramTypes.join(","))}`;
19
40
  }
20
- const result = await getRaw(url, 30_000);
21
- const file = result.headers["x-file"] || "?";
22
- const startLine = result.headers["x-start-line"] || "?";
23
- const endLine = result.headers["x-end-line"] || "?";
41
+ return get(url, 30_000);
42
+ }
43
+
44
+ // ---- Badge helpers ----
45
+
46
+ const KIND_BADGE = {
47
+ method: "[M]",
48
+ field: "[F]",
49
+ constant: "[K]",
50
+ type: "[C]",
51
+ };
52
+
53
+ const TYPE_KIND_BADGE = {
54
+ class: "[C]",
55
+ interface: "[I]",
56
+ enum: "[E]",
57
+ annotation: "[A]",
58
+ };
59
+
60
+ function badge(ref) {
61
+ if (ref.kind === "type") return TYPE_KIND_BADGE[ref.typeKind] || "[C]";
62
+ return KIND_BADGE[ref.kind] || "[?]";
63
+ }
64
+
65
+ function returnTypeBadge(ref) {
66
+ if (ref.returnTypeKind) return TYPE_KIND_BADGE[ref.returnTypeKind] || "";
67
+ return "";
68
+ }
69
+
70
+ // ---- Ref formatting ----
71
+
72
+ function formatMemberRef(ref) {
73
+ let line = `${badge(ref)} \`${ref.fqmn}\``;
74
+
75
+ // Return / field type
76
+ if (ref.type) {
77
+ const rtBadge = returnTypeBadge(ref);
78
+ const typeName = ref.returnTypeFqn || ref.type;
79
+ if (ref.isTypeVariable && ref.typeBound) {
80
+ line += ` → \`${ref.typeBound}\` (bound)`;
81
+ } else if (rtBadge) {
82
+ line += ` → ${rtBadge} \`${typeName}\``;
83
+ } else {
84
+ line += ` → \`${typeName}\``;
85
+ }
86
+ }
87
+
88
+ // Annotations
89
+ const annotations = [];
90
+ if (ref.static) annotations.push("static");
91
+ if (ref.inherited) annotations.push("inherited");
92
+ if (annotations.length > 0) line += ` (${annotations.join(", ")})`;
93
+
94
+ // Line number (only for incoming)
95
+ if (ref.direction === "incoming" && ref.line) line += ` :${ref.line}`;
96
+
97
+ // Javadoc inline after —
98
+ if (ref.doc) line += ` — ${ref.doc}`;
99
+
100
+ return line;
101
+ }
102
+
103
+ function formatTypeHeader(ref) {
104
+ let line = `${badge(ref)} \`${ref.fqmn}\``;
105
+ if (ref.doc) line += ` — ${ref.doc}`;
106
+ return line;
107
+ }
108
+
109
+ // ---- Grouping ----
110
+
111
+ function groupByDeclaringType(refs) {
112
+ const groups = [];
113
+ const groupMap = {};
114
+ for (const ref of refs) {
115
+ const typeFqn = ref.fqmn.split("#")[0];
116
+ if (!groupMap[typeFqn]) {
117
+ groupMap[typeFqn] = { typeRef: null, members: [] };
118
+ groups.push({ typeFqn, group: groupMap[typeFqn] });
119
+ }
120
+ if (!ref.fqmn.includes("#")) {
121
+ groupMap[typeFqn].typeRef = ref;
122
+ } else {
123
+ groupMap[typeFqn].members.push(ref);
124
+ }
125
+ }
126
+ return groups;
127
+ }
128
+
129
+ function formatRefGroup({ typeFqn, group }, implIndex) {
130
+ const lines = [];
131
+ if (group.typeRef) {
132
+ lines.push(formatTypeHeader(group.typeRef));
133
+ } else if (group.members.length > 0) {
134
+ const tkBadge = TYPE_KIND_BADGE[group.members[0].typeKind] || "[C]";
135
+ lines.push(`${tkBadge} \`${typeFqn}\``);
136
+ }
137
+ for (const ref of group.members) {
138
+ lines.push(formatMemberRef(ref));
139
+ // Show implementations right after the interface method
140
+ const impls = implIndex[ref.fqmn];
141
+ if (impls) {
142
+ for (const impl of impls) {
143
+ lines.push(` → ${badge(impl)} \`${impl.fqmn}\``);
144
+ }
145
+ }
146
+ }
147
+ return lines.join("\n");
148
+ }
24
149
 
25
- if (startLine === "-1") {
26
- // Multiple overloads: body has :startLine-endLine prefixes per block
27
- console.log(
28
- result.body.replace(
29
- /^:(\d+-\d+)/gm,
30
- `${stripProject(file)}:$1`,
31
- ),
32
- );
33
- } else {
34
- console.log(`${stripProject(file)}:${startLine}-${endLine}`);
35
- console.log(result.body);
150
+ /** Build index: interfaceFqmn → [impl refs] */
151
+ function buildImplIndex(refs) {
152
+ const index = {};
153
+ for (const ref of refs) {
154
+ if (ref.implementationOf) {
155
+ if (!index[ref.implementationOf]) index[ref.implementationOf] = [];
156
+ index[ref.implementationOf].push(ref);
157
+ }
36
158
  }
159
+ return index;
37
160
  }
38
161
 
39
- export const help = `Print source code of a type or method.
162
+ // ---- Hierarchy (type-level) ----
163
+
164
+ function formatHierarchy(lines, result) {
165
+ const supers = result.supertypes || [];
166
+ const subs = result.subtypes || [];
167
+ if (supers.length > 0 || subs.length > 0) {
168
+ lines.push("");
169
+ lines.push("#### Hierarchy:");
170
+ for (const s of supers) {
171
+ const b = TYPE_KIND_BADGE[s.kind] || "[C]";
172
+ lines.push(`↑ ${b} \`${s.fqn}\``);
173
+ }
174
+ for (const s of subs) {
175
+ const b = TYPE_KIND_BADGE[s.kind] || "[C]";
176
+ lines.push(`↓ ${b} \`${s.fqn}\``);
177
+ }
178
+ }
179
+ if (result.enclosingType) {
180
+ lines.push("");
181
+ lines.push("#### Enclosing Type:");
182
+ const et = result.enclosingType;
183
+ const fqn = typeof et === "string" ? et : et.fqn;
184
+ const kind = typeof et === "string" ? "class" : (et.kind || "class");
185
+ lines.push(`${TYPE_KIND_BADGE[kind] || "[C]"} \`${fqn}\``);
186
+ }
187
+ }
188
+
189
+ // ---- Markdown output ----
190
+
191
+ function formatMarkdown(result) {
192
+ const lines = [];
193
+
194
+ // Header
195
+ const headerBadge = result.fqmn.includes("#") ? "[M]" : "[C]";
196
+ lines.push(`#### ${headerBadge} ${result.fqmn}`);
197
+ if (result.overrideTarget) {
198
+ const ot = result.overrideTarget;
199
+ // Support both object {fqmn, kind} and legacy string format
200
+ const fqmn = typeof ot === "string"
201
+ ? (ot.includes(" ") ? ot.split(" ", 2)[1] : ot)
202
+ : ot.fqmn;
203
+ lines.push(`overrides [M] \`${fqmn}\``);
204
+ }
205
+ lines.push(`\`${result.file}:${result.startLine}-${result.endLine}\``);
206
+ lines.push("");
207
+
208
+ // Source
209
+ lines.push("```java");
210
+ lines.push((result.source || "").trimEnd());
211
+ lines.push("```");
212
+
213
+ // Type-level: hierarchy instead of refs
214
+ if (result.supertypes || result.subtypes) {
215
+ formatHierarchy(lines, result);
216
+ return lines.join("\n");
217
+ }
218
+
219
+ if (!result.refs || result.refs.length === 0) {
220
+ return lines.join("\n");
221
+ }
222
+
223
+ // Split by direction
224
+ const outgoing = result.refs.filter((r) => r.direction !== "incoming");
225
+ const incoming = result.refs.filter((r) => r.direction === "incoming");
226
+
227
+ if (outgoing.length > 0) {
228
+ const implIndex = buildImplIndex(outgoing);
229
+ const mainRefs = outgoing.filter((r) => !r.implementationOf);
230
+ lines.push("");
231
+ lines.push("#### Outgoing Calls:");
232
+ const groups = groupByDeclaringType(mainRefs);
233
+ for (const g of groups) {
234
+ lines.push(formatRefGroup(g, implIndex));
235
+ }
236
+ }
237
+
238
+ if (incoming.length > 0) {
239
+ const implIndex = buildImplIndex(incoming);
240
+ const mainRefs = incoming.filter((r) => !r.implementationOf);
241
+ lines.push("");
242
+ lines.push("#### Incoming Calls:");
243
+ const groups = groupByDeclaringType(mainRefs);
244
+ for (const g of groups) {
245
+ lines.push(formatRefGroup(g, implIndex));
246
+ }
247
+ }
248
+
249
+ return lines.join("\n");
250
+ }
251
+
252
+ export const help = `Print source code of a type or method with resolved references.
253
+
254
+ Returns markdown with source in a code block and references split into:
255
+ - Outgoing Calls — what this method/type calls or references
256
+ - Incoming Calls — who calls this method/type (when available)
257
+
258
+ References are grouped by declaring type. Each has a badge
259
+ ([M] method, [C] class, [I] interface, [E] enum, [K] constant,
260
+ [F] field, [A] annotation) and metadata (static, inherited,
261
+ return type with kind).
262
+
263
+ Usage: jdt source <FQMN> [<FQMN> ...] [--json]
40
264
 
41
- Usage: jdt source <FQN>[#method[(param types)]]
265
+ Flags:
266
+ --json output raw JSON from the server (for debugging)
42
267
 
43
268
  Examples:
44
- jdt source app.m8.dao.StaffDaoImpl
45
- jdt source app.m8.dao.StaffDaoImpl#getStaff
46
- jdt source "app.m8.dao.StaffDaoImpl#save(Order)"`;
269
+ jdt source com.example.dao.UserDaoImpl
270
+ jdt source com.example.dao.UserDaoImpl#getStaff
271
+ jdt source "com.example.dao.UserDaoImpl#save(Order)"
272
+ jdt source com.example.Foo#bar --json`;
@@ -30,4 +30,4 @@ export const help = `Find all direct and indirect subtypes/implementors of a typ
30
30
 
31
31
  Usage: jdt subtypes <FQN>
32
32
 
33
- Example: jdt subtypes app.m8.web.shared.core.HasId`;
33
+ Example: jdt subtypes com.example.core.HasId`;
@@ -0,0 +1,100 @@
1
+ import { get } from "../client.mjs";
2
+ import { extractPositional, parseFlags, parseFqmn } from "../args.mjs";
3
+ import {
4
+ formatTestRunHeader,
5
+ testRunGuide,
6
+ followTestStream,
7
+ } from "../format/test-status.mjs";
8
+
9
+ /**
10
+ * Launch tests non-blocking. Analogous to `jdt launch run`.
11
+ * Without -f: prints header + onboarding guide.
12
+ * With -f: prints header + streams test progress until done.
13
+ */
14
+ export async function testRun(args) {
15
+ // Filter out single-char flags (-f, -q) before extracting positionals
16
+ const filtered = args.filter((a) => a !== "-f" && a !== "-q");
17
+ const pos = extractPositional(filtered);
18
+ const flags = parseFlags(args);
19
+
20
+ let url = "/test/run?";
21
+ const parsed = parseFqmn(pos[0]);
22
+ const fqn = parsed.className;
23
+
24
+ if (fqn) {
25
+ url += `class=${encodeURIComponent(fqn)}`;
26
+ const method = parsed.method;
27
+ if (method) url += `&method=${encodeURIComponent(method)}`;
28
+ } else if (flags.project) {
29
+ url += `project=${encodeURIComponent(flags.project)}`;
30
+ if (flags.package)
31
+ url += `&package=${encodeURIComponent(flags.package)}`;
32
+ } else {
33
+ console.error(
34
+ "Usage: test run <FQN>[#method] | test run --project <name> [--package <pkg>]",
35
+ );
36
+ process.exit(1);
37
+ }
38
+
39
+ if (args.includes("--no-refresh")) url += "&no-refresh";
40
+
41
+ const result = await get(url, 30_000);
42
+ if (result.error) {
43
+ console.error(result.error);
44
+ process.exit(1);
45
+ }
46
+
47
+ // Wait briefly for session to register total count
48
+ const session = result.session;
49
+ await sleep(500);
50
+ try {
51
+ const status = await get(
52
+ `/test/status?session=${encodeURIComponent(session)}`,
53
+ 5_000,
54
+ );
55
+ if (status && !status.error && status.total > 0) {
56
+ result.total = status.total;
57
+ if (status.label) result.label = status.label;
58
+ }
59
+ } catch {
60
+ // ignore — total just won't be shown
61
+ }
62
+
63
+ console.log(formatTestRunHeader(result));
64
+
65
+ const follow = args.includes("-f") || args.includes("--follow");
66
+ if (follow) {
67
+ console.log();
68
+ const exitCode = await followTestStream(session, args);
69
+ process.exit(exitCode);
70
+ }
71
+
72
+ const quiet = args.includes("-q") || args.includes("--quiet");
73
+ if (!quiet) {
74
+ console.log(testRunGuide(session));
75
+ }
76
+ }
77
+
78
+ function sleep(ms) {
79
+ return new Promise((r) => setTimeout(r, ms));
80
+ }
81
+
82
+ export const help = `Launch tests non-blocking with real-time progress.
83
+
84
+ Usage: jdt test run <FQN>[#method] [-f] [-q]
85
+ jdt test run --project <name> [--package <pkg>] [-f] [-q]
86
+
87
+ Without -f, launches and prints a guide with available commands.
88
+ With -f, launches and streams test progress until completion.
89
+
90
+ Flags:
91
+ -f, --follow stream test status (only failures by default)
92
+ -q, --quiet suppress onboarding guide
93
+ --all include passed tests in output (with -f)
94
+ --ignored show only ignored tests (with -f)
95
+
96
+ Examples:
97
+ jdt test run com.example.MyTest run + show guide
98
+ jdt test run com.example.MyTest -f run + stream failures
99
+ jdt test run com.example.MyTest -f --all run + stream all tests
100
+ jdt test run --project my-project -f run project tests + stream`;
@@ -0,0 +1,46 @@
1
+ import { get } from "../client.mjs";
2
+ import { green, red, yellow, dim } from "../color.mjs";
3
+
4
+ /**
5
+ * List active and completed test sessions.
6
+ */
7
+ export async function testSessions() {
8
+ const results = await get("/test/sessions");
9
+ if (results.error) {
10
+ console.error(results.error);
11
+ process.exit(1);
12
+ }
13
+ if (results.length === 0) {
14
+ console.log("(no test sessions)");
15
+ return;
16
+ }
17
+ for (const s of results) {
18
+ const label = s.label || s.session;
19
+ const counts = formatCounts(s);
20
+ const time =
21
+ Number.isFinite(s.time) && s.time > 0
22
+ ? `${s.time.toFixed(1)}s`
23
+ : "";
24
+ const state = s.state === "running"
25
+ ? `running (${s.completed}/${s.total})`
26
+ : s.state;
27
+ console.log(
28
+ `${s.session} ${label} ${s.total} tests ${counts} ${time} ${state}`,
29
+ );
30
+ }
31
+ }
32
+
33
+ function formatCounts(s) {
34
+ const parts = [];
35
+ if (s.passed > 0) parts.push(green(`${s.passed} passed`));
36
+ if (s.failed > 0) parts.push(red(`${s.failed} failed`));
37
+ if (s.errors > 0) parts.push(red(`${s.errors} errors`));
38
+ if (s.ignored > 0) parts.push(yellow(`${s.ignored} ign`));
39
+ return parts.join(", ");
40
+ }
41
+
42
+ export const help = `List active and completed test sessions.
43
+
44
+ Usage: jdt test sessions
45
+
46
+ Output: session ID, label, test counts, state — one session per line.`;
@@ -0,0 +1,59 @@
1
+ import { get } from "../client.mjs";
2
+ import { extractPositional } from "../args.mjs";
3
+ import {
4
+ formatTestStatus,
5
+ followTestStream,
6
+ } from "../format/test-status.mjs";
7
+
8
+ /**
9
+ * Show test session status (snapshot or stream).
10
+ * Analogous to `jdt launch logs`.
11
+ */
12
+ export async function testStatus(args) {
13
+ const pos = extractPositional(args);
14
+ const session = pos[0];
15
+
16
+ if (!session) {
17
+ console.error("Usage: test status <session> [-f] [--all] [--ignored]");
18
+ process.exit(1);
19
+ }
20
+
21
+ const follow = args.includes("-f") || args.includes("--follow");
22
+ if (follow) {
23
+ const exitCode = await followTestStream(session, args);
24
+ process.exit(exitCode);
25
+ }
26
+
27
+ // Snapshot mode
28
+ let filter = "failures";
29
+ if (args.includes("--all")) filter = "all";
30
+ else if (args.includes("--ignored")) filter = "ignored";
31
+
32
+ let url = `/test/status?session=${encodeURIComponent(session)}`;
33
+ if (filter) url += `&filter=${filter}`;
34
+
35
+ const result = await get(url, 30_000);
36
+ if (result.error) {
37
+ console.error(result.error);
38
+ process.exit(1);
39
+ }
40
+
41
+ formatTestStatus(result);
42
+ }
43
+
44
+ export const help = `Show test session status (snapshot or live stream).
45
+
46
+ Usage: jdt test status <session> [-f] [--all] [--ignored]
47
+
48
+ Without -f, returns a snapshot of the current state.
49
+ With -f, streams test events until the session completes.
50
+
51
+ Flags:
52
+ -f, --follow stream events live until completion
53
+ --all show all tests (default: failures only)
54
+ --ignored show only ignored/skipped tests
55
+
56
+ Examples:
57
+ jdt test status jdtbridge-test-1234567890
58
+ jdt test status jdtbridge-test-1234567890 -f
59
+ jdt test status jdtbridge-test-1234567890 --ignored`;
@@ -44,4 +44,4 @@ export const help = `Show class overview: fields, methods, modifiers, and line n
44
44
 
45
45
  Usage: jdt type-info <FQN>
46
46
 
47
- Example: jdt type-info app.m8.dto.web.core.IdOrgRoot`;
47
+ Example: jdt type-info com.example.dto.BaseEntity`;
@@ -20,7 +20,7 @@ export function formatTestResults(result) {
20
20
  for (const f of result.failures) {
21
21
  const status =
22
22
  f.status === "FAILURE" ? red(f.status) : bold(red(f.status));
23
- console.log(`${status} ${f.class}.${f.method}`);
23
+ console.log(`${status} [M] \`${f.class}#${f.method}\``);
24
24
  if (f.trace) {
25
25
  const traceLines = f.trace.split("\n").slice(0, 10);
26
26
  for (const line of traceLines) console.log(` ${line}`);
@@ -0,0 +1,221 @@
1
+ // Test status formatter.
2
+ // Formats test session snapshots and streaming JSONL events.
3
+ // Uses FQMN with # separator and [M] badges for navigation.
4
+
5
+ import { red, green, yellow, bold, dim } from "../color.mjs";
6
+
7
+ /**
8
+ * Format a test status snapshot (JSON from /test/status).
9
+ * Shows summary line + failure/ignored details based on filter.
10
+ */
11
+ export function formatTestStatus(result) {
12
+ const state = result.state === "running"
13
+ ? " (running)"
14
+ : result.state === "stopped"
15
+ ? " (stopped)"
16
+ : "";
17
+
18
+ const label = result.label || result.session;
19
+ const progress = result.state === "running"
20
+ ? `${result.completed}/${result.total}`
21
+ : `${result.total}/${result.total}`;
22
+
23
+ console.log(
24
+ `#### ${label} — ${progress}${state}, ${formatCounts(result)}`,
25
+ );
26
+
27
+ const entries = result.entries || result.failures || [];
28
+ if (entries.length > 0) {
29
+ console.log();
30
+ for (const f of entries) {
31
+ formatEntry(f);
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Format a single JSONL event line from the stream.
38
+ * Returns true if something was printed.
39
+ */
40
+ export function formatTestEvent(jsonLine) {
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(jsonLine);
44
+ } catch {
45
+ return false;
46
+ }
47
+
48
+ if (parsed.event === "started") {
49
+ // Header already printed by test-run, skip in stream
50
+ return false;
51
+ }
52
+
53
+ if (parsed.event === "finished") {
54
+ console.log();
55
+ console.log(formatCounts(parsed));
56
+ return true;
57
+ }
58
+
59
+ if (parsed.event === "case") {
60
+ const status = parsed.status;
61
+ const fqmn = parsed.fqmn;
62
+ const time = formatTime(parsed.time);
63
+
64
+ if (status === "PASS") {
65
+ console.log(`${green("PASS")} [M] \`${fqmn}\` ${dim(time)}`);
66
+ } else if (status === "FAIL") {
67
+ console.log(`${red("FAIL")} [M] \`${fqmn}\` ${dim(time)}`);
68
+ printTrace(parsed);
69
+ } else if (status === "ERROR") {
70
+ console.log(
71
+ `${bold(red("ERROR"))} [M] \`${fqmn}\` ${dim(time)}`,
72
+ );
73
+ printTrace(parsed);
74
+ } else if (status === "IGNORED") {
75
+ console.log(
76
+ `${yellow("IGNORED")} [M] \`${fqmn}\``,
77
+ );
78
+ }
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Format a single entry from status snapshot (PASS, FAIL, ERROR, IGNORED).
87
+ */
88
+ function formatEntry(f) {
89
+ const status = f.status;
90
+ const fqmn = f.fqmn;
91
+ const time = formatTime(f.time);
92
+
93
+ if (status === "PASS") {
94
+ console.log(`${green("PASS")} [M] \`${fqmn}\` ${dim(time)}`);
95
+ } else if (status === "FAIL") {
96
+ console.log(`${red("FAIL")} [M] \`${fqmn}\` ${dim(time)}`);
97
+ printTrace(f);
98
+ } else if (status === "ERROR") {
99
+ console.log(
100
+ `${bold(red("ERROR"))} [M] \`${fqmn}\` ${dim(time)}`,
101
+ );
102
+ printTrace(f);
103
+ } else if (status === "IGNORED") {
104
+ console.log(`${yellow("IGNORED")} [M] \`${fqmn}\``);
105
+ }
106
+ }
107
+
108
+ function printTrace(parsed) {
109
+ if (parsed.expected != null && parsed.actual != null) {
110
+ console.log(` Expected: ${parsed.expected}`);
111
+ console.log(` Actual: ${parsed.actual}`);
112
+ }
113
+ if (parsed.trace) {
114
+ const lines = parsed.trace.split("\n").slice(0, 10);
115
+ for (const line of lines) console.log(` ${line}`);
116
+ if (parsed.trace.split("\n").length > 10) console.log(" ...");
117
+ }
118
+ }
119
+
120
+ function formatCounts(r) {
121
+ const parts = [`${r.total} tests`];
122
+ if (r.passed > 0) parts.push(green(`${r.passed} passed`));
123
+ if (r.failed > 0) parts.push(red(`${r.failed} failed`));
124
+ if (r.errors > 0) parts.push(red(`${r.errors} errors`));
125
+ if (r.ignored > 0) parts.push(yellow(`${r.ignored} ignored`));
126
+ const time = Number.isFinite(r.time) ? r.time : 0;
127
+ parts.push(`${time.toFixed(1)}s`);
128
+ return parts.join(", ");
129
+ }
130
+
131
+ function formatTime(t) {
132
+ if (t == null || !Number.isFinite(t)) return "";
133
+ return `(${t.toFixed(3)}s)`;
134
+ }
135
+
136
+ /**
137
+ * Format the header printed at test run start.
138
+ * Markdown-friendly: heading + backtick-wrapped values.
139
+ */
140
+ export function formatTestRunHeader(result) {
141
+ const label = result.label || result.session;
142
+ const total = result.total ? ` (${result.total} tests)` : "";
143
+ const parts = [`#### Test: ${label}${total}`];
144
+ parts.push(`Launch: \`${result.session}\``);
145
+ if (result.project) parts.push(`Project: \`${result.project}\``);
146
+ if (result.runner) parts.push(`Runner: ${result.runner}`);
147
+ return parts.join("\n");
148
+ }
149
+
150
+ /**
151
+ * Onboarding guide printed after non-blocking launch.
152
+ * Markdown-friendly: bold sections, backtick commands.
153
+ */
154
+ export function testRunGuide(session) {
155
+ return `
156
+ **Status** — snapshot or live stream:
157
+ \`jdt test status ${session}\` failures only (default)
158
+ \`jdt test status ${session} -f\` stream live until done
159
+ \`jdt test status ${session} --all\` all tests including passed
160
+ \`jdt test status ${session} --ignored\` only skipped/disabled tests
161
+
162
+ **Console** — raw stdout/stderr of the test JVM:
163
+ \`jdt launch logs ${session}\`
164
+
165
+ **Manage:**
166
+ \`jdt test sessions\` list active/completed sessions
167
+ \`jdt launch stop ${session}\` abort
168
+ \`jdt launch clear ${session}\` remove
169
+
170
+ **Navigate** — FQMNs from status output are copy-pasteable:
171
+ \`jdt source <FQMN>\` view test source
172
+ \`jdt test run <FQMN> -f\` re-run single test
173
+
174
+ Add \`-q\` to suppress this guide.`;
175
+ }
176
+
177
+ /**
178
+ * Stream test status via JSONL and format output.
179
+ * Shared by test-run -f and test-status -f.
180
+ * Returns exit code: 0 if all pass, 1 if any failures.
181
+ */
182
+ export async function followTestStream(session, args) {
183
+ const { getStreamLines } = await import("../client.mjs");
184
+
185
+ let filter = "failures";
186
+ if (args.includes("--all")) filter = "all";
187
+ else if (args.includes("--ignored")) filter = "ignored";
188
+
189
+ const url = `/test/status/stream?session=${encodeURIComponent(session)}&filter=${filter}`;
190
+
191
+ let detached = false;
192
+ const onSigint = () => {
193
+ detached = true;
194
+ process.stdout.write("\n");
195
+ process.exit(0);
196
+ };
197
+ process.on("SIGINT", onSigint);
198
+
199
+ let hasFailed = false;
200
+ try {
201
+ await getStreamLines(url, (line) => {
202
+ formatTestEvent(line);
203
+ try {
204
+ const ev = JSON.parse(line);
205
+ if (ev.event === "finished"
206
+ && (ev.failed > 0 || ev.errors > 0)) {
207
+ hasFailed = true;
208
+ }
209
+ } catch { /* ignore parse errors */ }
210
+ });
211
+ } catch (e) {
212
+ if (!detached) {
213
+ console.error(e.message);
214
+ return 1;
215
+ }
216
+ return 0;
217
+ } finally {
218
+ process.removeListener("SIGINT", onSigint);
219
+ }
220
+ return hasFailed ? 1 : 0;
221
+ }
@@ -1,48 +0,0 @@
1
- import { get } from "../client.mjs";
2
- import { extractPositional, parseFlags, parseFqmn } from "../args.mjs";
3
- import { formatTestResults } from "../format/test-results.mjs";
4
-
5
- export async function test(args) {
6
- const pos = extractPositional(args);
7
- const flags = parseFlags(args);
8
- let url = "/test?";
9
- const parsed = parseFqmn(pos[0]);
10
- const fqn = parsed.className;
11
- if (fqn) {
12
- url += `class=${encodeURIComponent(fqn)}`;
13
- const method = parsed.method || pos[1];
14
- if (method) url += `&method=${encodeURIComponent(method)}`;
15
- } else if (flags.project) {
16
- url += `project=${encodeURIComponent(flags.project)}`;
17
- if (flags.package)
18
- url += `&package=${encodeURIComponent(flags.package)}`;
19
- } else {
20
- console.error(
21
- "Usage: test <FQN> [method] | test --project <name> [--package <pkg>]",
22
- );
23
- process.exit(1);
24
- }
25
- if (flags.timeout) url += `&timeout=${flags.timeout}`;
26
- if (args.includes("--no-refresh")) url += "&no-refresh";
27
- const result = await get(url, 300_000);
28
- if (result.error) {
29
- console.error(result.error);
30
- process.exit(1);
31
- }
32
- formatTestResults(result);
33
- }
34
-
35
- export const help = `Run JUnit tests via Eclipse's built-in test runner.
36
-
37
- Usage: jdt test <FQN>[#method]
38
- jdt test --project <name> [--package <pkg>]
39
-
40
- Flags:
41
- --project <name> run all tests in a project
42
- --package <pkg> narrow to a specific package (with --project)
43
- --timeout <sec> test run timeout in seconds (default: 120)
44
-
45
- Examples:
46
- jdt test app.m8ws.utils.ObjectMapperTest
47
- jdt test app.m8ws.utils.ObjectMapperTest#testSerialize
48
- jdt test --project m8-server`;