@kaluchi/jdtbridge 1.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.
@@ -0,0 +1,383 @@
1
+ // Setup command — install/update/remove the Eclipse JDT Bridge plugin.
2
+
3
+ import { existsSync } from "node:fs";
4
+ import { join, resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { execSync } from "node:child_process";
7
+ import { createInterface } from "node:readline";
8
+ import { readConfig, writeConfig } from "../home.mjs";
9
+ import { discoverInstances } from "../discovery.mjs";
10
+ import { green, red, bold, dim } from "../color.mjs";
11
+ import { parseFlags } from "../args.mjs";
12
+ import {
13
+ eclipseExe,
14
+ isEclipseRunning,
15
+ findEclipsePath,
16
+ getEclipseVersion,
17
+ detectProfile,
18
+ getInstalledVersion,
19
+ stopEclipse,
20
+ startEclipse,
21
+ getEclipseJavaHome,
22
+ generateTargetPlatform,
23
+ isEclipseInstall,
24
+ waitForBridge,
25
+ p2Install,
26
+ p2Uninstall,
27
+ } from "../eclipse.mjs";
28
+
29
+ const BUNDLE_ID = "io.github.kaluchi.jdtbridge";
30
+ const FEATURE_IU = `${BUNDLE_ID}.feature.feature.group`;
31
+ const __dirname = dirname(fileURLToPath(import.meta.url));
32
+
33
+ // ---- UI helpers ----
34
+
35
+ function ask(question, defaultVal = "") {
36
+ if (!process.stdin.isTTY) return Promise.resolve(defaultVal);
37
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
38
+ return new Promise((res) => {
39
+ rl.question(question, (answer) => {
40
+ rl.close();
41
+ res(answer.trim() || defaultVal);
42
+ });
43
+ });
44
+ }
45
+
46
+ function ok(msg) { console.log(` ${green("\u2713")} ${msg}`); }
47
+ function fail(msg) { console.log(` ${red("\u2717")} ${msg}`); }
48
+ function info(msg) { console.log(` ${dim("\u00b7")} ${msg}`); }
49
+
50
+ // ---- prerequisite checks ----
51
+
52
+ function checkNode() {
53
+ const major = parseInt(process.versions.node);
54
+ return { ok: major >= 20, label: `Node.js ${process.versions.node}` };
55
+ }
56
+
57
+ function checkJava() {
58
+ try {
59
+ const out = execSync("java -version 2>&1", { encoding: "utf8" });
60
+ const m = out.match(/version "([^"]+)"/);
61
+ return { ok: true, label: `Java ${m ? m[1] : "?"}` };
62
+ } catch {
63
+ return { ok: false, label: "Java \u2014 not found" };
64
+ }
65
+ }
66
+
67
+ function checkMaven() {
68
+ try {
69
+ const out = execSync("mvn --version", {
70
+ encoding: "utf8",
71
+ stdio: ["pipe", "pipe", "pipe"],
72
+ });
73
+ const m = out.match(/Apache Maven (\S+)/);
74
+ return { ok: true, label: `Maven ${m ? m[1] : "?"}` };
75
+ } catch {
76
+ return { ok: false, label: "Maven \u2014 not found" };
77
+ }
78
+ }
79
+
80
+ function showPrereqs() {
81
+ console.log(bold("Prerequisites"));
82
+ const checks = [checkNode(), checkJava(), checkMaven()];
83
+ for (const c of checks) (c.ok ? ok : fail)(c.label);
84
+ return checks.every((c) => c.ok);
85
+ }
86
+
87
+ // ---- plugin source ----
88
+
89
+ function findRepoRoot() {
90
+ // cli/src/commands/setup.mjs -> 3 levels up = repo root
91
+ const candidate = resolve(__dirname, "..", "..", "..");
92
+ if (
93
+ existsSync(join(candidate, "pom.xml")) &&
94
+ existsSync(join(candidate, "plugin"))
95
+ ) {
96
+ return candidate;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function getBuiltRepoPath(repoRoot) {
102
+ const dir = join(repoRoot, "site", "target", "repository");
103
+ return existsSync(dir) ? dir : null;
104
+ }
105
+
106
+ // ---- ensure Eclipse is stopped, with confirmation ----
107
+
108
+ async function ensureStopped() {
109
+ if (!isEclipseRunning()) return true;
110
+ const answer = await ask(" Eclipse is running. Stop it? [Y/n] ", "y");
111
+ if (answer.toLowerCase() === "n") {
112
+ console.log(" Eclipse must be stopped. Aborting.");
113
+ return false;
114
+ }
115
+ info("Stopping Eclipse...");
116
+ if (!stopEclipse()) {
117
+ console.error(" Failed to stop Eclipse.");
118
+ process.exit(1);
119
+ }
120
+ info("Eclipse stopped.");
121
+ return true;
122
+ }
123
+
124
+ // ---- modes ----
125
+
126
+ async function runCheck(config) {
127
+ console.log();
128
+ showPrereqs();
129
+
130
+ console.log();
131
+ console.log(bold("Eclipse"));
132
+ const eclipsePath = findEclipsePath(config);
133
+ if (eclipsePath) {
134
+ const ver = getEclipseVersion(eclipsePath);
135
+ ok(
136
+ `${eclipsePath}${ver ? ` (${ver})` : ""}${config.eclipse ? dim(" \u2014 config") : ""}`,
137
+ );
138
+ const profile = detectProfile(eclipsePath);
139
+ (profile ? ok : fail)(`Profile: ${profile || "not found"}`);
140
+ const running = isEclipseRunning();
141
+ (running ? ok : info)(running ? "Running" : "Not running");
142
+ } else {
143
+ fail("Eclipse not found");
144
+ }
145
+
146
+ console.log();
147
+ console.log(bold("Bridge"));
148
+ const instances = discoverInstances();
149
+ if (instances.length > 0) {
150
+ for (const inst of instances) {
151
+ ok(`port ${inst.port}, PID ${inst.pid}, workspace ${inst.workspace}`);
152
+ if (inst.version) ok(`Plugin: ${inst.version}${inst.location ? dim(` — ${inst.location}`) : ""}`);
153
+ }
154
+ } else {
155
+ fail("No live instances");
156
+ }
157
+
158
+ console.log();
159
+ console.log(bold("Plugin source"));
160
+ const repoRoot = findRepoRoot();
161
+ if (repoRoot) {
162
+ ok(`Repo: ${repoRoot}`);
163
+ const built = getBuiltRepoPath(repoRoot);
164
+ (built ? ok : info)(built ? "p2 site: built" : "Not built yet");
165
+ } else {
166
+ info("Repo not found (CLI not in cloned repo)");
167
+ }
168
+ console.log();
169
+ }
170
+
171
+ async function runInstall(config, flags) {
172
+ console.log();
173
+ if (!showPrereqs()) {
174
+ console.error("\nMissing prerequisites.");
175
+ process.exit(1);
176
+ }
177
+
178
+ // Eclipse
179
+ console.log();
180
+ console.log(bold("Eclipse"));
181
+ let eclipsePath = findEclipsePath(config);
182
+ if (!eclipsePath) {
183
+ eclipsePath = await ask(" Eclipse not found. Path: ");
184
+ if (!eclipsePath || !isEclipseInstall(eclipsePath)) {
185
+ console.error(" Not a valid Eclipse installation.");
186
+ process.exit(1);
187
+ }
188
+ }
189
+ const ver = getEclipseVersion(eclipsePath);
190
+ ok(`${eclipsePath}${ver ? ` (${ver})` : ""}`);
191
+ if (config.eclipse !== eclipsePath) {
192
+ writeConfig({ eclipse: eclipsePath });
193
+ info("Saved to config");
194
+ }
195
+
196
+ const profile = detectProfile(eclipsePath);
197
+ if (!profile) {
198
+ console.error(" Cannot detect p2 profile.");
199
+ process.exit(1);
200
+ }
201
+ ok(`Profile: ${profile}`);
202
+
203
+ const installedVersion = getInstalledVersion(eclipsePath, BUNDLE_ID);
204
+ if (installedVersion) {
205
+ info(`Currently installed: ${installedVersion}`);
206
+ }
207
+
208
+ // Build
209
+ console.log();
210
+ console.log(bold("Building plugin..."));
211
+ let repoRoot = findRepoRoot();
212
+ if (!repoRoot) {
213
+ repoRoot = config.pluginSource;
214
+ if (!repoRoot || !existsSync(repoRoot)) {
215
+ repoRoot = await ask(" Plugin source repo path: ");
216
+ }
217
+ }
218
+ if (!repoRoot || !existsSync(join(repoRoot, "pom.xml"))) {
219
+ console.error(" Invalid plugin source path.");
220
+ process.exit(1);
221
+ }
222
+ info(`Source: ${repoRoot}`);
223
+
224
+ if (!flags["skip-build"]) {
225
+ generateTargetPlatform(repoRoot, eclipsePath);
226
+ info(`Target platform: ${eclipsePath}`);
227
+ const javaHome = getEclipseJavaHome(eclipsePath);
228
+ const mvnEnv = javaHome
229
+ ? { ...process.env, JAVA_HOME: javaHome }
230
+ : process.env;
231
+ if (javaHome) info(`JAVA_HOME: ${javaHome}`);
232
+ const mvnCmd = flags.clean ? "mvn clean verify" : "mvn verify";
233
+ try {
234
+ execSync(mvnCmd, { cwd: repoRoot, stdio: "inherit", timeout: 300_000, env: mvnEnv });
235
+ } catch {
236
+ console.error("\n Build failed.");
237
+ process.exit(1);
238
+ }
239
+ } else {
240
+ info("Build skipped (--skip-build)");
241
+ }
242
+
243
+ const repoDir = getBuiltRepoPath(repoRoot);
244
+ if (!repoDir) {
245
+ console.error(" p2 site not found in site/target/repository/.");
246
+ process.exit(1);
247
+ }
248
+ ok("Plugin built");
249
+
250
+ // Capture workspace BEFORE stopping Eclipse (instances are filtered by
251
+ // isPidAlive, so after stop they disappear from discoverInstances).
252
+ const wasRunning = isEclipseRunning();
253
+ let workspace = null;
254
+ if (wasRunning) {
255
+ const instances = discoverInstances();
256
+ if (instances.length > 0) workspace = instances[0].workspace;
257
+ }
258
+
259
+ // Install
260
+ console.log();
261
+ console.log(bold("Installing plugin..."));
262
+ if (!(await ensureStopped())) return;
263
+
264
+ try {
265
+ if (installedVersion) {
266
+ info("Removing old version...");
267
+ try {
268
+ p2Uninstall(eclipsePath, profile, FEATURE_IU);
269
+ } catch {
270
+ /* may fail on dirty profile -- proceed anyway */
271
+ }
272
+ }
273
+ info("Installing via p2 director...");
274
+ p2Install(eclipsePath, profile, repoDir, FEATURE_IU);
275
+ ok("Installed");
276
+ } catch (e) {
277
+ console.error(`\n ${e.message}`);
278
+ process.exit(1);
279
+ }
280
+
281
+ const newVersion = getInstalledVersion(eclipsePath, BUNDLE_ID);
282
+ if (newVersion) ok(`Version: ${newVersion}`);
283
+
284
+ // Restart
285
+ console.log();
286
+ const launcherPath = join(eclipsePath, eclipseExe("eclipse"));
287
+ if (existsSync(launcherPath)) {
288
+ if (wasRunning) {
289
+ const pid = startEclipse(eclipsePath, workspace);
290
+ info(`Eclipse started (PID ${pid})`);
291
+ info("Waiting for bridge...");
292
+ try {
293
+ const { port, projects } = await waitForBridge(discoverInstances, pid);
294
+ ok(`Bridge ready on port ${port} (${projects.length} projects)`);
295
+ } catch {
296
+ fail("Bridge did not start (Eclipse may still be loading)");
297
+ }
298
+ } else {
299
+ info("Start Eclipse to activate the plugin.");
300
+ }
301
+ } else {
302
+ info("Plugin installed. Run your Eclipse product to complete setup and activate the bridge.");
303
+ }
304
+
305
+ console.log();
306
+ ok(bold("Setup complete"));
307
+ console.log();
308
+ }
309
+
310
+ async function runRemove(config) {
311
+ console.log();
312
+ console.log(bold("Removing plugin..."));
313
+
314
+ const eclipsePath = findEclipsePath(config);
315
+ if (!eclipsePath) {
316
+ console.error(" Eclipse not found. Run jdt setup --check first.");
317
+ process.exit(1);
318
+ }
319
+
320
+ const profile = detectProfile(eclipsePath);
321
+ if (!profile) {
322
+ console.error(" Cannot detect p2 profile.");
323
+ process.exit(1);
324
+ }
325
+
326
+ const installed = getInstalledVersion(eclipsePath, BUNDLE_ID);
327
+ if (!installed) {
328
+ info("Plugin is not installed.");
329
+ return;
330
+ }
331
+ info(`Installed: ${installed}`);
332
+
333
+ if (!(await ensureStopped())) return;
334
+
335
+ try {
336
+ p2Uninstall(eclipsePath, profile, FEATURE_IU);
337
+ ok("Plugin removed");
338
+ } catch (e) {
339
+ console.error(` ${e.message}`);
340
+ process.exit(1);
341
+ }
342
+ console.log();
343
+ }
344
+
345
+ // ---- entry point ----
346
+
347
+ export async function setup(args) {
348
+ const flags = parseFlags(args);
349
+ const config = readConfig();
350
+ if (flags.eclipse) config.eclipse = flags.eclipse;
351
+
352
+ if (args.includes("--check")) {
353
+ await runCheck(config);
354
+ } else if (args.includes("--remove")) {
355
+ await runRemove(config);
356
+ } else {
357
+ await runInstall(config, flags);
358
+ }
359
+ }
360
+
361
+ export const help = `Set up the Eclipse JDT Bridge plugin.
362
+
363
+ Usage: jdt setup [options]
364
+
365
+ Modes:
366
+ (default) build plugin from source, install into Eclipse
367
+ --check show status of all components (diagnostic only)
368
+ --remove uninstall the plugin from Eclipse
369
+
370
+ Options:
371
+ --eclipse <path> Eclipse installation directory
372
+ --skip-build install last build without rebuilding
373
+ --clean clean build (mvn clean verify)
374
+
375
+ Eclipse must be stopped for install/update/remove operations.
376
+ If Eclipse is running, you will be prompted to stop it.
377
+
378
+ Examples:
379
+ jdt setup
380
+ jdt setup --check
381
+ jdt setup --eclipse D:/eclipse
382
+ jdt setup --skip-build
383
+ jdt setup --remove`;
@@ -0,0 +1,43 @@
1
+ import { getRaw } from "../client.mjs";
2
+ import { extractPositional, parseFlags } from "../args.mjs";
3
+ import { stripProject } from "../paths.mjs";
4
+
5
+ export async function source(args) {
6
+ const pos = extractPositional(args);
7
+ const flags = parseFlags(args);
8
+ const [fqn, method] = pos;
9
+ if (!fqn) {
10
+ console.error("Usage: source <FQN> [method] [--arity n]");
11
+ process.exit(1);
12
+ }
13
+ let url = `/source?class=${encodeURIComponent(fqn)}`;
14
+ if (method) url += `&method=${encodeURIComponent(method)}`;
15
+ if (flags.arity !== undefined && flags.arity !== true)
16
+ url += `&arity=${flags.arity}`;
17
+ const result = await getRaw(url, 30_000);
18
+ const file = result.headers["x-file"] || "?";
19
+ const startLine = result.headers["x-start-line"] || "?";
20
+ const endLine = result.headers["x-end-line"] || "?";
21
+
22
+ if (startLine === "-1") {
23
+ // Multiple overloads: body has :startLine-endLine prefixes per block
24
+ console.log(
25
+ result.body.replace(
26
+ /^:(\d+-\d+)/gm,
27
+ `${stripProject(file)}:$1`,
28
+ ),
29
+ );
30
+ } else {
31
+ console.log(`${stripProject(file)}:${startLine}-${endLine}`);
32
+ console.log(result.body);
33
+ }
34
+ }
35
+
36
+ export const help = `Print source code of a type or method.
37
+
38
+ Usage: jdt source <FQN> [method] [--arity <n>]
39
+
40
+ Examples:
41
+ jdt source app.m8.dao.StaffDaoImpl
42
+ jdt source app.m8.dao.StaffDaoImpl getStaff
43
+ jdt source app.m8.dao.StaffDaoImpl save --arity 2`;
@@ -0,0 +1,33 @@
1
+ import { get } from "../client.mjs";
2
+ import { extractPositional } from "../args.mjs";
3
+ import { stripProject } from "../paths.mjs";
4
+
5
+ export async function subtypes(args) {
6
+ const pos = extractPositional(args);
7
+ const fqn = pos[0];
8
+ if (!fqn) {
9
+ console.error("Usage: subtypes <FQN>");
10
+ process.exit(1);
11
+ }
12
+ const results = await get(
13
+ `/subtypes?class=${encodeURIComponent(fqn)}`,
14
+ 30_000,
15
+ );
16
+ if (results.error) {
17
+ console.error(results.error);
18
+ process.exit(1);
19
+ }
20
+ if (results.length === 0) {
21
+ console.log("(no subtypes)");
22
+ return;
23
+ }
24
+ for (const r of results) {
25
+ console.log(`${r.fqn} ${stripProject(r.file)}`);
26
+ }
27
+ }
28
+
29
+ export const help = `Find all direct and indirect subtypes/implementors of a type.
30
+
31
+ Usage: jdt subtypes <FQN>
32
+
33
+ Example: jdt subtypes app.m8.web.shared.core.HasId`;
@@ -0,0 +1,47 @@
1
+ import { get } from "../client.mjs";
2
+ import { extractPositional, parseFlags } 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 fqn = pos[0];
10
+ if (fqn) {
11
+ url += `class=${encodeURIComponent(fqn)}`;
12
+ const method = pos[1];
13
+ if (method) url += `&method=${encodeURIComponent(method)}`;
14
+ } else if (flags.project) {
15
+ url += `project=${encodeURIComponent(flags.project)}`;
16
+ if (flags.package)
17
+ url += `&package=${encodeURIComponent(flags.package)}`;
18
+ } else {
19
+ console.error(
20
+ "Usage: test <FQN> [method] | test --project <name> [--package <pkg>]",
21
+ );
22
+ process.exit(1);
23
+ }
24
+ if (flags.timeout) url += `&timeout=${flags.timeout}`;
25
+ if (args.includes("--no-refresh")) url += "&no-refresh";
26
+ const result = await get(url, 300_000);
27
+ if (result.error) {
28
+ console.error(result.error);
29
+ process.exit(1);
30
+ }
31
+ formatTestResults(result);
32
+ }
33
+
34
+ export const help = `Run JUnit tests via Eclipse's built-in test runner.
35
+
36
+ Usage: jdt test <FQN> [method]
37
+ jdt test --project <name> [--package <pkg>]
38
+
39
+ Flags:
40
+ --project <name> run all tests in a project
41
+ --package <pkg> narrow to a specific package (with --project)
42
+ --timeout <sec> test run timeout in seconds (default: 120)
43
+
44
+ Examples:
45
+ jdt test app.m8ws.utils.ObjectMapperTest
46
+ jdt test app.m8ws.utils.ObjectMapperTest testSerialize
47
+ jdt test --project m8-server`;
@@ -0,0 +1,47 @@
1
+ import { get } from "../client.mjs";
2
+ import { extractPositional } from "../args.mjs";
3
+ import { stripProject } from "../paths.mjs";
4
+ import { dim } from "../color.mjs";
5
+
6
+ export async function typeInfo(args) {
7
+ const pos = extractPositional(args);
8
+ const fqn = pos[0];
9
+ if (!fqn) {
10
+ console.error("Usage: type-info <FQN>");
11
+ process.exit(1);
12
+ }
13
+ const result = await get(
14
+ `/type-info?class=${encodeURIComponent(fqn)}`,
15
+ 30_000,
16
+ );
17
+ if (result.error) {
18
+ console.error(result.error);
19
+ process.exit(1);
20
+ }
21
+
22
+ const filePath = stripProject(result.file);
23
+ console.log(`${result.kind} ${result.fqn} ${dim(`(${filePath})`)}`);
24
+ if (result.superclass) console.log(` extends ${result.superclass}`);
25
+ for (const iface of result.interfaces) {
26
+ console.log(` implements ${iface}`);
27
+ }
28
+ if (result.fields.length > 0) {
29
+ console.log();
30
+ for (const f of result.fields) {
31
+ const mods = f.modifiers ? f.modifiers + " " : "";
32
+ console.log(` ${mods}${f.type} ${f.name} ${dim(`:${f.line}`)}`);
33
+ }
34
+ }
35
+ if (result.methods.length > 0) {
36
+ console.log();
37
+ for (const m of result.methods) {
38
+ console.log(` ${m.signature} ${dim(`:${m.line}`)}`);
39
+ }
40
+ }
41
+ }
42
+
43
+ export const help = `Show class overview: fields, methods, modifiers, and line numbers.
44
+
45
+ Usage: jdt type-info <FQN>
46
+
47
+ Example: jdt type-info app.m8.dto.web.core.IdOrgRoot`;
@@ -0,0 +1,82 @@
1
+ // Eclipse instance discovery — find running instances via bridge files and process list.
2
+
3
+ import { readdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { instancesDir } from "./home.mjs";
6
+
7
+ /**
8
+ * @typedef {Object} Instance
9
+ * @property {number} port
10
+ * @property {string} token
11
+ * @property {number} pid
12
+ * @property {string} workspace
13
+ * @property {string} [version] - plugin version (added in 1.1)
14
+ * @property {string} [location] - bundle location URI
15
+ * @property {string} file - path to the instance file
16
+ */
17
+
18
+ /**
19
+ * Read all instance files from JDTBRIDGE_HOME/instances/.
20
+ * Filters out stale instances (PID not alive).
21
+ * @returns {Instance[]}
22
+ */
23
+ export function discoverInstances() {
24
+ const dir = instancesDir();
25
+ let files;
26
+ try {
27
+ files = readdirSync(dir).filter((f) => f.endsWith(".json"));
28
+ } catch {
29
+ return [];
30
+ }
31
+
32
+ const instances = [];
33
+ for (const file of files) {
34
+ const filePath = join(dir, file);
35
+ try {
36
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
37
+ if (!data.port || !data.pid) continue;
38
+ if (!isPidAlive(data.pid)) continue;
39
+ instances.push({ ...data, file: filePath });
40
+ } catch {
41
+ // corrupt file — skip
42
+ }
43
+ }
44
+ return instances;
45
+ }
46
+
47
+ /**
48
+ * Find a single instance. If multiple are running, prefer one matching
49
+ * the given workspace hint (cwd or explicit). If none match, return the
50
+ * first one found (or null).
51
+ * @param {string} [workspaceHint] - substring to match against workspace path
52
+ * @returns {Instance|null}
53
+ */
54
+ export function findInstance(workspaceHint) {
55
+ const instances = discoverInstances();
56
+ if (instances.length === 0) return null;
57
+ if (instances.length === 1) return instances[0];
58
+
59
+ if (workspaceHint) {
60
+ const normalized = workspaceHint.replace(/\\/g, "/").toLowerCase();
61
+ const match = instances.find((i) =>
62
+ i.workspace.replace(/\\/g, "/").toLowerCase().includes(normalized),
63
+ );
64
+ if (match) return match;
65
+ }
66
+
67
+ // Default to first
68
+ return instances[0];
69
+ }
70
+
71
+ /**
72
+ * Check if a PID is still alive.
73
+ * Sends signal 0 which checks existence without killing.
74
+ */
75
+ export function isPidAlive(pid) {
76
+ try {
77
+ process.kill(pid, 0);
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }