wowdump 0.2.0 → 0.3.1

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +56 -118
  3. package/dist/agent.js +5 -8
  4. package/dist/cli.js +497 -0
  5. package/dist/discovery.js +1 -12
  6. package/dist/dry-run.js +0 -2
  7. package/dist/focused-session.js +61 -1329
  8. package/dist/frida-runtime.js +51 -73
  9. package/dist/frida-worker.js +100 -0
  10. package/dist/ghidra.js +769 -0
  11. package/dist/main.js +66 -0
  12. package/dist/processes.js +2 -5
  13. package/dist/reader-broker.js +460 -0
  14. package/dist/reader-client.js +1 -0
  15. package/dist/reader-main.js +67 -0
  16. package/dist/session.js +17 -120
  17. package/dist/toolchain.js +594 -0
  18. package/dist/windows-launcher.js +207 -0
  19. package/dist/windows-reader.js +102 -0
  20. package/dist/wow-analysis.js +211 -236
  21. package/package.json +18 -35
  22. package/skills/wowdump/SKILL.md +15 -0
  23. package/skills/wowdump/commands.md +44 -0
  24. package/dist/analysis-path.js +0 -38
  25. package/dist/analysis-process-log.js +0 -146
  26. package/dist/broker-client.js +0 -411
  27. package/dist/broker-codec.js +0 -148
  28. package/dist/broker-core.js +0 -1045
  29. package/dist/broker-gateway.js +0 -447
  30. package/dist/broker-ledger.js +0 -196
  31. package/dist/broker-main.js +0 -291
  32. package/dist/broker-protocol.js +0 -119
  33. package/dist/broker-runtime.js +0 -1283
  34. package/dist/broker-server.js +0 -466
  35. package/dist/build-bundle-loader.js +0 -183
  36. package/dist/build-bundle.js +0 -11
  37. package/dist/focus-errors.js +0 -63
  38. package/dist/focus-service.js +0 -1855
  39. package/dist/mcp-main.js +0 -51
  40. package/dist/mcp.js +0 -924
  41. package/dist/process-log-lock.js +0 -181
  42. package/dist/runtime-config.js +0 -399
  43. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  44. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  45. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  46. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  47. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
package/dist/cli.js ADDED
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, realpathSync } from "node:fs";
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { basename, extname, isAbsolute, join, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { Command } from "commander";
8
+ import { WindowsBrokerManager } from "./windows-launcher.js";
9
+ import { bootstrapToolchain, resolveToolchain, resolveWowdumpHome } from "./toolchain.js";
10
+ import { runGhidraStaticAnalysis as runCanonicalGhidraStaticAnalysis, buildGhidraHeadlessCommand } from "./ghidra.js";
11
+ async function runStaticAnalysis(options) {
12
+ // ghidra.ts owns the canonical evidence/profile schema. Keep the small
13
+ // toolchain resolver as the CLI-facing bootstrap layer, then translate the
14
+ // command options into the canonical runner's vocabulary.
15
+ const ghidraValue = options.ghidra;
16
+ const ghidraResolved = ghidraValue ? resolve(ghidraValue) : undefined;
17
+ const ghidraIsFile = ghidraResolved ? existsSync(ghidraResolved) && !existsSync(join(ghidraResolved, "support")) : false;
18
+ const canonicalOptions = {
19
+ exe: options.executable,
20
+ buildKey: options.buildKey,
21
+ output: options.evidenceFile ?? join(options.outputDirectory ?? resolveWowdumpHome(options.env ?? process.env), "profiles", `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
22
+ projectDir: options.projectDirectory,
23
+ projectName: options.projectName,
24
+ ghidraHome: ghidraResolved && !ghidraIsFile ? ghidraResolved : undefined,
25
+ analyzeHeadless: ghidraResolved && ghidraIsFile ? ghidraResolved : undefined,
26
+ javaPath: options.java,
27
+ scriptPath: options.scriptDirectory,
28
+ scriptName: options.exporterScript,
29
+ timeoutMs: (options.timeoutSeconds ?? 1800) * 1000,
30
+ env: options.env,
31
+ home: options.home,
32
+ commandRunner: options.runner
33
+ ? async (file, args, commandOptions) => {
34
+ const value = await options.runner({ file, args: [...args], cwd: commandOptions.cwd, env: commandOptions.env });
35
+ return { code: value.exitCode, stdout: value.stdout, stderr: value.stderr };
36
+ }
37
+ : undefined
38
+ };
39
+ if (options.dryRun) {
40
+ const resolved = resolveToolchain(options);
41
+ if (!resolved.ghidra || !resolved.java) {
42
+ throw new CliError("TOOLCHAIN_INCOMPLETE", `Ghidra toolchain is incomplete: missing ${resolved.missing.join(", ")}`, resolved);
43
+ }
44
+ const projectDir = options.projectDirectory ?? join(resolved.home, "cache", "ghidra", options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-"));
45
+ const command = buildGhidraHeadlessCommand({ analyzeHeadless: resolved.ghidra.executable, javaPath: resolved.java.executable }, {
46
+ exe: options.executable,
47
+ buildKey: options.buildKey,
48
+ projectDir,
49
+ projectName: options.projectName,
50
+ scriptPath: options.scriptDirectory,
51
+ scriptName: options.exporterScript,
52
+ output: options.evidenceFile
53
+ });
54
+ return {
55
+ ok: true,
56
+ command: "analyze.static",
57
+ dryRun: true,
58
+ buildKey: options.buildKey,
59
+ executable: resolve(options.executable),
60
+ outputFile: options.evidenceFile ?? join(projectDir, `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
61
+ commandLine: { file: command.executable, args: command.args, cwd: command.cwd },
62
+ toolchain: resolved,
63
+ profile: { schema: "wowdump.profile.v1", buildKey: options.buildKey, confidence: "candidate" }
64
+ };
65
+ }
66
+ const result = await runCanonicalGhidraStaticAnalysis(canonicalOptions);
67
+ return {
68
+ ok: true,
69
+ command: "analyze.static",
70
+ dryRun: false,
71
+ buildKey: result.buildKey,
72
+ executable: result.executable.path,
73
+ executableSha256: result.executable.sha256,
74
+ outputFile: options.evidenceFile ?? join(options.outputDirectory ?? resolveWowdumpHome(options.env ?? process.env), "profiles", `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
75
+ commandLine: { file: result.analysis.command[0] ?? "", args: result.analysis.command.slice(1), cwd: options.projectDirectory ?? resolveWowdumpHome(options.env ?? process.env) },
76
+ toolchain: resolveToolchain(options),
77
+ profile: result.profile,
78
+ evidence: result
79
+ };
80
+ }
81
+ class CliError extends Error {
82
+ code;
83
+ details;
84
+ constructor(code, message, details) {
85
+ super(message);
86
+ this.name = "CliError";
87
+ this.code = code;
88
+ this.details = details;
89
+ }
90
+ }
91
+ function positiveInteger(value, field) {
92
+ const result = Number(value);
93
+ if (!Number.isSafeInteger(result) || result <= 0)
94
+ throw new CliError("ARGUMENT_INVALID", `${field} must be a positive integer`);
95
+ return result;
96
+ }
97
+ function nonNegativeInteger(value, field) {
98
+ const result = Number(value);
99
+ if (!Number.isSafeInteger(result) || result < 0)
100
+ throw new CliError("ARGUMENT_INVALID", `${field} must be a non-negative integer`);
101
+ return result;
102
+ }
103
+ function jsonRecord(value, field) {
104
+ let decoded;
105
+ try {
106
+ decoded = JSON.parse(value);
107
+ }
108
+ catch {
109
+ throw new CliError("ARGUMENT_INVALID", `${field} must be valid JSON`);
110
+ }
111
+ if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
112
+ throw new CliError("ARGUMENT_INVALID", `${field} must be a JSON object`);
113
+ }
114
+ return decoded;
115
+ }
116
+ function writeJson(io, value) {
117
+ io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
118
+ }
119
+ async function defaultSidecar(file, args, input) {
120
+ return new Promise(resolveResult => {
121
+ const child = spawn(file, args, { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
122
+ let stdout = "";
123
+ let stderr = "";
124
+ child.stdout.setEncoding("utf8");
125
+ child.stderr.setEncoding("utf8");
126
+ child.stdout.on("data", value => { stdout += value; });
127
+ child.stderr.on("data", value => { stderr += value; });
128
+ child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
129
+ child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
130
+ if (input !== undefined)
131
+ child.stdin.end(input);
132
+ else
133
+ child.stdin.end();
134
+ });
135
+ }
136
+ function brokerCommandLine(env, cwd) {
137
+ const configured = env.WOWDUMP_READER_COMMAND?.trim();
138
+ if (configured)
139
+ return { file: configured, args: [] };
140
+ const candidate = resolve(cwd, "dist", "reader-main.js");
141
+ return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
142
+ }
143
+ async function defaultBroker(invocation, env, cwd, sidecar) {
144
+ const command = brokerCommandLine(env, cwd);
145
+ if (!command) {
146
+ throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: resolve(cwd, "dist", "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
147
+ }
148
+ const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
149
+ if (result.exitCode !== 0)
150
+ throw new CliError("BROKER_FAILED", result.stderr.trim() || `reader broker exited with ${result.exitCode}`);
151
+ const line = result.stdout.split(/\r?\n/).find(value => value.trim());
152
+ if (!line)
153
+ throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned no JSON response");
154
+ try {
155
+ return JSON.parse(line);
156
+ }
157
+ catch {
158
+ throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
159
+ }
160
+ }
161
+ const windowsBrokerManagers = new Map();
162
+ function persistentWindowsBroker(home, cwd) {
163
+ const key = resolve(home).toLowerCase();
164
+ let manager = windowsBrokerManagers.get(key);
165
+ if (!manager) {
166
+ manager = new WindowsBrokerManager({ home, readerEntry: resolve(cwd, "dist", "reader-main.js") });
167
+ windowsBrokerManagers.set(key, manager);
168
+ }
169
+ return manager;
170
+ }
171
+ async function listProfileFiles(directory) {
172
+ try {
173
+ const entries = await readdir(directory, { withFileTypes: true });
174
+ return entries
175
+ .filter(entry => entry.isFile() && extname(entry.name).toLowerCase() === ".json")
176
+ .map(entry => join(directory, entry.name))
177
+ .sort((left, right) => left.localeCompare(right));
178
+ }
179
+ catch (error) {
180
+ if (error.code === "ENOENT")
181
+ return [];
182
+ throw error;
183
+ }
184
+ }
185
+ async function readProfileSummary(file) {
186
+ const profile = jsonRecord(await readFile(file, "utf8"), file);
187
+ return {
188
+ id: typeof profile.id === "string" ? profile.id : basename(file, extname(file)),
189
+ file,
190
+ buildKey: profile.buildKey ?? null,
191
+ confidence: profile.confidence ?? "unverified",
192
+ executableSha256: profile.executableSha256
193
+ ?? (profile.executable && typeof profile.executable === "object" && !Array.isArray(profile.executable)
194
+ ? profile.executable.sha256 ?? null
195
+ : null)
196
+ };
197
+ }
198
+ function profileFile(profileRoot, idOrFile) {
199
+ if (isAbsolute(idOrFile))
200
+ return resolve(idOrFile);
201
+ const safeId = idOrFile.replace(/\.json$/i, "");
202
+ if (!/^[A-Za-z0-9_.@-]+$/.test(safeId))
203
+ throw new CliError("ARGUMENT_INVALID", "profile ID contains unsupported characters");
204
+ return join(profileRoot, `${safeId}.json`);
205
+ }
206
+ function selected(source, keys) {
207
+ return Object.fromEntries(keys.filter(key => source[key] !== undefined).map(key => [key, source[key]]));
208
+ }
209
+ function requireRuntimeConfirmation(options) {
210
+ if (options.confirm === true)
211
+ return;
212
+ throw new CliError("CONFIRMATION_REQUIRED", "runtime export requires --confirm", {
213
+ pid: options.pid,
214
+ buildKey: options.build,
215
+ operation: "runtime-export",
216
+ kind: options.kind,
217
+ maxHooks: options.maxHooks,
218
+ durationMs: options.durationMs,
219
+ maxEvents: options.maxEvents,
220
+ cleanupDeadlineMs: options.cleanupDeadlineMs
221
+ });
222
+ }
223
+ function nested(record, ...path) {
224
+ let value = record;
225
+ for (const key of path) {
226
+ if (!value || typeof value !== "object" || Array.isArray(value))
227
+ return undefined;
228
+ value = value[key];
229
+ }
230
+ return value;
231
+ }
232
+ function compareEvidence(staticProfile, runtimeExport) {
233
+ const definitions = [
234
+ { name: "buildKey", left: staticProfile.buildKey, right: runtimeExport.buildKey },
235
+ {
236
+ name: "executableSha256",
237
+ left: staticProfile.executableSha256 ?? nested(staticProfile, "executable", "sha256"),
238
+ right: runtimeExport.executableSha256 ?? nested(runtimeExport, "executable", "sha256")
239
+ },
240
+ { name: "moduleBase", left: staticProfile.moduleBase ?? nested(staticProfile, "module", "base"), right: runtimeExport.moduleBase ?? nested(runtimeExport, "module", "base") },
241
+ { name: "sectionBounds", left: staticProfile.sectionBounds ?? staticProfile.sections, right: runtimeExport.sectionBounds ?? runtimeExport.sections },
242
+ { name: "entryBytes", left: staticProfile.entryBytes, right: runtimeExport.entryBytes },
243
+ { name: "matchCount", left: staticProfile.matchCount, right: runtimeExport.matchCount }
244
+ ];
245
+ return definitions.map(item => ({
246
+ name: item.name,
247
+ static: item.left ?? null,
248
+ runtime: item.right ?? null,
249
+ status: item.left === undefined || item.right === undefined
250
+ ? "missing"
251
+ : JSON.stringify(item.left) === JSON.stringify(item.right) ? "match" : "mismatch"
252
+ }));
253
+ }
254
+ export function createWowdumpCli(dependencies = {}) {
255
+ const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
256
+ const env = dependencies.env ?? process.env;
257
+ const cwd = resolve(dependencies.cwd ?? process.cwd());
258
+ const home = resolveWowdumpHome(env);
259
+ const broker = dependencies.broker ?? (process.platform === "win32" && !dependencies.sidecar
260
+ ? (invocation => persistentWindowsBroker(home, cwd).request(invocation))
261
+ : (invocation => defaultBroker(invocation, env, cwd, dependencies.sidecar)));
262
+ const staticAnalysis = dependencies.staticAnalysis ?? runStaticAnalysis;
263
+ const sidecar = dependencies.sidecar ?? defaultSidecar;
264
+ const program = new Command()
265
+ .name("wowdump")
266
+ .description("Build-aware WoW native memory analysis CLI")
267
+ .version("0.3.0")
268
+ .showHelpAfterError()
269
+ .configureOutput({ writeOut: value => io.stdout.write(value), writeErr: value => io.stderr.write(value) });
270
+ program.command("target")
271
+ .description("Show target, reader broker, profile, monitor, and Frida state")
272
+ .option("--pid <pid>", "target process ID")
273
+ .option("--build <buildKey>", "expected build key")
274
+ .action(async (options) => {
275
+ const payload = {
276
+ ...(options.pid ? { pid: positiveInteger(options.pid, "pid") } : {}),
277
+ ...(options.build ? { buildKey: options.build } : {})
278
+ };
279
+ const status = await broker({ command: "status", payload });
280
+ writeJson(io, { ok: true, command: "target", home, broker: status, toolchain: resolveToolchain({ home, env }) });
281
+ });
282
+ program.command("profiles")
283
+ .description("List profiles or describe one profile")
284
+ .argument("[id-or-file]", "profile ID or absolute JSON file")
285
+ .option("--directory <path>", "profile directory", join(home, "profiles"))
286
+ .action(async (idOrFile, options) => {
287
+ const directory = resolve(options.directory);
288
+ if (idOrFile) {
289
+ const file = profileFile(directory, idOrFile);
290
+ const profile = jsonRecord(await readFile(file, "utf8"), file);
291
+ writeJson(io, { ok: true, command: "profiles.describe", file, profile });
292
+ return;
293
+ }
294
+ const files = await listProfileFiles(directory);
295
+ writeJson(io, { ok: true, command: "profiles.list", directory, profiles: await Promise.all(files.map(readProfileSummary)) });
296
+ });
297
+ const memory = program.command("memory").description("Read or monitor profile-defined native memory");
298
+ memory.command("read")
299
+ .description("Perform one bounded read")
300
+ .requiredOption("--pid <pid>", "target process ID")
301
+ .option("--build <buildKey>", "expected build key")
302
+ .option("--profile <id>", "profile ID")
303
+ .option("--field <name...>", "field names")
304
+ .option("--address <hex>", "explicit address as hexadecimal text")
305
+ .option("--size <bytes>", "bounded byte count")
306
+ .option("--request <json>", "complete read request as JSON")
307
+ .action(async (options) => {
308
+ const payload = options.request ? jsonRecord(options.request, "request") : {
309
+ pid: positiveInteger(options.pid, "pid"),
310
+ ...(options.build ? { buildKey: options.build } : {}),
311
+ ...(options.profile ? { profileId: options.profile } : {}),
312
+ ...(options.field ? { fields: options.field } : {}),
313
+ ...(options.address ? { address: options.address } : {}),
314
+ ...(options.size ? { size: positiveInteger(options.size, "size") } : {})
315
+ };
316
+ writeJson(io, await broker({ command: "read", payload }));
317
+ });
318
+ const watch = memory.command("watch").description("Manage broker-owned memory monitors");
319
+ watch.command("start")
320
+ .description("Start a monitor")
321
+ .requiredOption("--pid <pid>", "target process ID")
322
+ .option("--build <buildKey>", "expected build key")
323
+ .option("--profile <id>", "profile ID")
324
+ .option("--field <name...>", "field names")
325
+ .option("--address <hex>", "explicit address as hexadecimal text")
326
+ .option("--size <bytes>", "bounded byte count")
327
+ .option("--interval <ms>", "sample interval", "250")
328
+ .option("--max-samples <count>", "sample limit", "1000")
329
+ .option("--all-samples", "emit unchanged samples")
330
+ .action(async (options) => writeJson(io, await broker({ command: "watch.start", payload: {
331
+ pid: positiveInteger(options.pid, "pid"),
332
+ ...(options.build ? { buildKey: options.build } : {}),
333
+ ...(options.profile ? { profileId: options.profile } : {}),
334
+ ...(options.field ? { fields: options.field } : {}),
335
+ ...(options.address ? { address: options.address } : {}),
336
+ ...(options.size ? { size: positiveInteger(options.size, "size") } : {}),
337
+ intervalMs: positiveInteger(options.interval, "interval"),
338
+ maxSamples: positiveInteger(options.maxSamples, "max-samples"),
339
+ changeOnly: options.allSamples !== true
340
+ } })));
341
+ watch.command("poll")
342
+ .description("Poll monitor events")
343
+ .requiredOption("--id <watchId>", "monitor ID")
344
+ .option("--after <sequence>", "sequence cursor", "0")
345
+ .action(async (options) => writeJson(io, await broker({ command: "watch.poll", payload: {
346
+ watchId: options.id,
347
+ afterSequence: nonNegativeInteger(options.after, "after")
348
+ } })));
349
+ watch.command("stop")
350
+ .description("Stop a monitor and release its resources")
351
+ .requiredOption("--id <watchId>", "monitor ID")
352
+ .action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
353
+ const analyze = program.command("analyze").description("Run separate static, runtime-export, and verification stages");
354
+ analyze.command("static")
355
+ .description("Analyze a disk executable with Ghidra Headless")
356
+ .requiredOption("--exe <path>", "path to Wow.exe")
357
+ .requiredOption("--build <buildKey>", "build key")
358
+ .option("--build-key <buildKey>", "alias for --build")
359
+ .option("--java <path>", "Java executable or JDK root")
360
+ .option("--ghidra <path>", "analyzeHeadless executable or Ghidra root")
361
+ .option("--output <directory>", "profile output directory")
362
+ .option("--project-directory <directory>", "Ghidra project directory")
363
+ .option("--project-name <name>", "Ghidra project name")
364
+ .option("--script-directory <directory>", "Ghidra script directory")
365
+ .option("--exporter-script <name>", "Ghidra post-script name")
366
+ .option("--evidence <file>", "Ghidra exporter JSON output")
367
+ .option("--timeout <seconds>", "analysis timeout", "1800")
368
+ .option("--dry-run", "print the exact command without starting Ghidra")
369
+ .action(async (options) => writeJson(io, await staticAnalysis({
370
+ executable: options.exe,
371
+ buildKey: options.build ?? options.buildKey,
372
+ home,
373
+ env,
374
+ java: options.java,
375
+ ghidra: options.ghidra,
376
+ outputDirectory: options.output,
377
+ projectDirectory: options.projectDirectory,
378
+ projectName: options.projectName,
379
+ scriptDirectory: options.scriptDirectory,
380
+ exporterScript: options.exporterScript,
381
+ evidenceFile: options.evidence,
382
+ timeoutSeconds: positiveInteger(options.timeout, "timeout"),
383
+ dryRun: options.dryRun === true
384
+ })));
385
+ analyze.command("runtime-export")
386
+ .description("Export runtime evidence through the isolated Frida sidecar")
387
+ .requiredOption("--pid <pid>", "target process ID")
388
+ .requiredOption("--build <buildKey>", "build key")
389
+ .option("--build-key <buildKey>", "alias for --build")
390
+ .option("--kind <kind>", "text or providers", "providers")
391
+ .option("--worker <path>", "Frida worker entry")
392
+ .option("--static-profile <path>", "static profile containing candidate RVAs")
393
+ .option("--max-hooks <count>", "maximum hooks", "1")
394
+ .option("--duration-ms <ms>", "maximum duration", "5000")
395
+ .option("--max-events <count>", "maximum events", "100")
396
+ .option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
397
+ .option("--confirm", "confirm the exact bounded Frida operation")
398
+ .action(async (options) => {
399
+ const normalized = {
400
+ pid: positiveInteger(options.pid, "pid"),
401
+ build: options.build ?? options.buildKey,
402
+ kind: options.kind,
403
+ maxHooks: positiveInteger(options.maxHooks, "max-hooks"),
404
+ durationMs: positiveInteger(options.durationMs, "duration-ms"),
405
+ maxEvents: positiveInteger(options.maxEvents, "max-events"),
406
+ cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
407
+ confirm: options.confirm === true
408
+ };
409
+ if (!["text", "providers"].includes(normalized.kind))
410
+ throw new CliError("ARGUMENT_INVALID", "kind must be text or providers");
411
+ requireRuntimeConfirmation(normalized);
412
+ const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(cwd, "dist", "frida-worker.js"));
413
+ if (!existsSync(worker))
414
+ throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
415
+ const request = {
416
+ command: "runtime-export",
417
+ ...selected(normalized, ["pid", "build", "kind", "maxHooks", "durationMs", "maxEvents", "cleanupDeadlineMs"]),
418
+ ...(options.staticProfile ? { staticProfile: resolve(options.staticProfile) } : {})
419
+ };
420
+ const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
421
+ if (result.exitCode !== 0)
422
+ throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
423
+ const line = result.stdout.split(/\r?\n/).find(value => value.trim());
424
+ writeJson(io, line ? JSON.parse(line) : { ok: true, command: "analyze.runtime-export", stdout: result.stdout });
425
+ });
426
+ analyze.command("verify")
427
+ .description("Compare static and runtime evidence without implicit promotion")
428
+ .requiredOption("--static-profile <path>", "static profile JSON")
429
+ .requiredOption("--runtime-export <path>", "runtime export JSON")
430
+ .option("--output <path>", "optional verification report path")
431
+ .action(async (options) => {
432
+ const staticFile = resolve(options.staticProfile);
433
+ const runtimeFile = resolve(options.runtimeExport);
434
+ const staticProfile = jsonRecord(await readFile(staticFile, "utf8"), staticFile);
435
+ const runtimeExport = jsonRecord(await readFile(runtimeFile, "utf8"), runtimeFile);
436
+ const checks = compareEvidence(staticProfile, runtimeExport);
437
+ const report = {
438
+ ok: checks.every(check => check.status !== "mismatch"),
439
+ command: "analyze.verify",
440
+ staticProfile: staticFile,
441
+ runtimeExport: runtimeFile,
442
+ checks,
443
+ confidence: staticProfile.confidence ?? "unverified",
444
+ promoted: false
445
+ };
446
+ if (options.output) {
447
+ const { writeFile } = await import("node:fs/promises");
448
+ await writeFile(resolve(options.output), `${JSON.stringify(report, null, 2)}\n`, "utf8");
449
+ }
450
+ writeJson(io, report);
451
+ });
452
+ program.command("init")
453
+ .description("Initialize WOWDUMP_HOME without overwriting existing files")
454
+ .option("--skip-toolchain-download", "only detect existing Java/Ghidra installations")
455
+ .action(async (options) => {
456
+ const installMissing = options.skipToolchainDownload !== true && env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
457
+ const result = await bootstrapToolchain({ home, env, installMissing });
458
+ writeJson(io, {
459
+ ok: result.toolchain.ready,
460
+ command: "init",
461
+ ...result,
462
+ toolchain: result.toolchain
463
+ });
464
+ });
465
+ return program;
466
+ }
467
+ export async function runWowdumpCli(argv = process.argv, dependencies = {}) {
468
+ const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
469
+ try {
470
+ await createWowdumpCli(dependencies).parseAsync([...argv]);
471
+ return 0;
472
+ }
473
+ catch (error) {
474
+ const failure = error;
475
+ io.stderr.write(`${JSON.stringify({
476
+ ok: false,
477
+ error: {
478
+ code: typeof failure.code === "string" ? failure.code : "CLI_FAILED",
479
+ message: error instanceof Error ? error.message : String(error),
480
+ ...(failure.details !== undefined ? { details: failure.details } : {})
481
+ }
482
+ })}\n`);
483
+ return 1;
484
+ }
485
+ }
486
+ let invokedFile = "";
487
+ if (process.argv[1]) {
488
+ const invokedPath = resolve(process.argv[1]);
489
+ try {
490
+ invokedFile = pathToFileURL(realpathSync(invokedPath)).href;
491
+ }
492
+ catch {
493
+ invokedFile = pathToFileURL(invokedPath).href;
494
+ }
495
+ }
496
+ if (invokedFile === import.meta.url)
497
+ process.exitCode = await runWowdumpCli();
package/dist/discovery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
- import { basename, join, resolve } from "node:path";
2
+ import { basename, join } from "node:path";
3
3
  function parseBuildInfo(file) {
4
4
  if (!existsSync(file))
5
5
  return [];
@@ -46,14 +46,3 @@ export function discoverInstalls(gameRoot) {
46
46
  return [{ flavor, root, executable, build }];
47
47
  }).sort((a, b) => a.flavor.localeCompare(b.flavor));
48
48
  }
49
- export function discoverInstallsAcrossRoots(gameRoots, discover = discoverInstalls) {
50
- const installs = new Map();
51
- for (const gameRoot of gameRoots) {
52
- for (const install of discover(gameRoot)) {
53
- const key = resolve(install.executable).toLowerCase();
54
- if (!installs.has(key))
55
- installs.set(key, install);
56
- }
57
- }
58
- return [...installs.values()].sort((a, b) => a.flavor.localeCompare(b.flavor) || a.executable.localeCompare(b.executable));
59
- }
package/dist/dry-run.js CHANGED
@@ -29,8 +29,6 @@ export function createDryRunReport(installs, processes, registry) {
29
29
  processes: processes
30
30
  .map(process => ({
31
31
  pid: process.pid,
32
- executable: process.executable,
33
- ...(process.startTime ? { startTime: process.startTime } : {}),
34
32
  ...installStatus(process.install, registry)
35
33
  }))
36
34
  .sort((a, b) => a.pid - b.pid)