@eyeauras/insight-cli 0.0.0-stage → 0.2.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 (174) hide show
  1. package/.cli-package-build.json +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +60 -2
  4. package/SKILL.md +58 -0
  5. package/dist/src/administration-results.d.ts +1 -0
  6. package/dist/src/administration-results.js +84 -0
  7. package/dist/src/administration.d.ts +14 -0
  8. package/dist/src/administration.js +134 -0
  9. package/dist/src/archive-contract.d.ts +6 -0
  10. package/dist/src/archive-contract.js +108 -0
  11. package/dist/src/archive.d.ts +12 -0
  12. package/dist/src/archive.js +116 -0
  13. package/dist/src/arguments.d.ts +14 -0
  14. package/dist/src/arguments.js +62 -0
  15. package/dist/src/artifacts.d.ts +17 -0
  16. package/dist/src/artifacts.js +115 -0
  17. package/dist/src/bin.d.ts +2 -0
  18. package/dist/src/bin.js +4 -0
  19. package/dist/src/cli.d.ts +10 -0
  20. package/dist/src/cli.js +419 -0
  21. package/dist/src/comment-effects.d.ts +3 -0
  22. package/dist/src/comment-effects.js +55 -0
  23. package/dist/src/contexts.d.ts +115 -0
  24. package/dist/src/contexts.js +1078 -0
  25. package/dist/src/coordinator.d.ts +41 -0
  26. package/dist/src/coordinator.js +160 -0
  27. package/dist/src/cross-results.d.ts +14 -0
  28. package/dist/src/cross-results.js +71 -0
  29. package/dist/src/cross.d.ts +3 -0
  30. package/dist/src/cross.js +43 -0
  31. package/dist/src/data-effects.d.ts +3 -0
  32. package/dist/src/data-effects.js +103 -0
  33. package/dist/src/debugger-results.d.ts +4 -0
  34. package/dist/src/debugger-results.js +151 -0
  35. package/dist/src/debugger-supervisor.d.ts +1 -0
  36. package/dist/src/debugger-supervisor.js +136 -0
  37. package/dist/src/debugger.d.ts +21 -0
  38. package/dist/src/debugger.js +267 -0
  39. package/dist/src/documentation-effects.d.ts +5 -0
  40. package/dist/src/documentation-effects.js +56 -0
  41. package/dist/src/extensions.d.ts +64 -0
  42. package/dist/src/extensions.js +289 -0
  43. package/dist/src/function-effects.d.ts +4 -0
  44. package/dist/src/function-effects.js +112 -0
  45. package/dist/src/ghidra.d.ts +50 -0
  46. package/dist/src/ghidra.js +468 -0
  47. package/dist/src/index.d.ts +2 -0
  48. package/dist/src/index.js +1 -0
  49. package/dist/src/initial-model.d.ts +6 -0
  50. package/dist/src/initial-model.js +85 -0
  51. package/dist/src/json.d.ts +2 -0
  52. package/dist/src/json.js +8 -0
  53. package/dist/src/lifecycle.d.ts +49 -0
  54. package/dist/src/lifecycle.js +154 -0
  55. package/dist/src/memory-inputs.d.ts +8 -0
  56. package/dist/src/memory-inputs.js +56 -0
  57. package/dist/src/merge.d.ts +29 -0
  58. package/dist/src/merge.js +66 -0
  59. package/dist/src/model.d.ts +74 -0
  60. package/dist/src/model.js +67 -0
  61. package/dist/src/mutation-effects.d.ts +4 -0
  62. package/dist/src/mutation-effects.js +321 -0
  63. package/dist/src/mutation-inputs.d.ts +2 -0
  64. package/dist/src/mutation-inputs.js +71 -0
  65. package/dist/src/mutations.d.ts +4 -0
  66. package/dist/src/mutations.js +53 -0
  67. package/dist/src/native-inputs.d.ts +28 -0
  68. package/dist/src/native-inputs.js +182 -0
  69. package/dist/src/native-queries.d.ts +2 -0
  70. package/dist/src/native-queries.js +29 -0
  71. package/dist/src/projects.d.ts +31 -0
  72. package/dist/src/projects.js +301 -0
  73. package/dist/src/prototype-cli.d.ts +8 -0
  74. package/dist/src/prototype-cli.js +155 -0
  75. package/dist/src/queries.d.ts +5 -0
  76. package/dist/src/queries.js +84 -0
  77. package/dist/src/query-policy.d.ts +6 -0
  78. package/dist/src/query-policy.js +72 -0
  79. package/dist/src/query-results.d.ts +2 -0
  80. package/dist/src/query-results.js +224 -0
  81. package/dist/src/research.d.ts +184 -0
  82. package/dist/src/research.js +808 -0
  83. package/dist/src/runtime-archives.d.ts +5 -0
  84. package/dist/src/runtime-archives.js +127 -0
  85. package/dist/src/runtime-catalog.d.ts +18 -0
  86. package/dist/src/runtime-catalog.js +27 -0
  87. package/dist/src/runtime-manager.d.ts +101 -0
  88. package/dist/src/runtime-manager.js +444 -0
  89. package/dist/src/runtime-selection.d.ts +13 -0
  90. package/dist/src/runtime-selection.js +25 -0
  91. package/dist/src/saved-materials.d.ts +12 -0
  92. package/dist/src/saved-materials.js +196 -0
  93. package/dist/src/scripts.d.ts +19 -0
  94. package/dist/src/scripts.js +79 -0
  95. package/dist/src/sdk-contracts.d.ts +13 -0
  96. package/dist/src/sdk-contracts.js +37 -0
  97. package/dist/src/sdk-files.d.ts +29 -0
  98. package/dist/src/sdk-files.js +249 -0
  99. package/dist/src/sdk.d.ts +4 -0
  100. package/dist/src/sdk.js +125 -0
  101. package/dist/src/server.d.ts +46 -0
  102. package/dist/src/server.js +460 -0
  103. package/dist/src/store.d.ts +33 -0
  104. package/dist/src/store.js +178 -0
  105. package/dist/src/sync-policy.d.ts +22 -0
  106. package/dist/src/sync-policy.js +123 -0
  107. package/dist/src/transport.d.ts +23 -0
  108. package/dist/src/transport.js +135 -0
  109. package/dist/src/ui-results.d.ts +2 -0
  110. package/dist/src/ui-results.js +80 -0
  111. package/dist/src/ui.d.ts +17 -0
  112. package/dist/src/ui.js +88 -0
  113. package/dist/src/utility.d.ts +14 -0
  114. package/dist/src/utility.js +43 -0
  115. package/dist/src/version-tracking.d.ts +57 -0
  116. package/dist/src/version-tracking.js +200 -0
  117. package/dist/src/workspace.d.ts +199 -0
  118. package/dist/src/workspace.js +564 -0
  119. package/package.json +47 -4
  120. package/references/InsightRepositoryProbe.java +55 -0
  121. package/references/administration.md +34 -0
  122. package/references/archive.md +32 -0
  123. package/references/artifacts.md +47 -0
  124. package/references/debugger.md +40 -0
  125. package/references/extensions-and-sync.md +124 -0
  126. package/references/managed-runtime.md +116 -0
  127. package/references/mutations.json +1424 -0
  128. package/references/native/InsightApplyDocumentation.java +48 -0
  129. package/references/native/InsightArchivePayloads.java +36 -0
  130. package/references/native/InsightBlockTransfer.java +57 -0
  131. package/references/native/InsightEmulation.java +93 -0
  132. package/references/native/InsightExtensions.java +36 -0
  133. package/references/native/InsightFunctionDatabases.java +98 -0
  134. package/references/native/InsightGuiMain.java +32 -0
  135. package/references/native/InsightHeadlessMain.java +10 -0
  136. package/references/native/InsightInitialImport.java +42 -0
  137. package/references/native/InsightInitialJournal.java +48 -0
  138. package/references/native/InsightInitialPublication.java +142 -0
  139. package/references/native/InsightKeytool.java +16 -0
  140. package/references/native/InsightNativeReceipt.java +31 -0
  141. package/references/native/InsightPlugins.java +34 -0
  142. package/references/native/InsightProjectFiles.java +92 -0
  143. package/references/native/InsightPublicationRecovery.java +63 -0
  144. package/references/native/InsightPythonWheels.java +31 -0
  145. package/references/native/InsightResearchQueries.java +188 -0
  146. package/references/native/InsightRuntimeDoctor.java +33 -0
  147. package/references/native/InsightSdkFiles.java +142 -0
  148. package/references/native/InsightSdkScript.java +31 -0
  149. package/references/native/InsightSdkTools.java +338 -0
  150. package/references/native/InsightSdkTypes.java +132 -0
  151. package/references/native/InsightServerAdministration.java +82 -0
  152. package/references/native/InsightServerInit.java +62 -0
  153. package/references/native/InsightServerMain.java +43 -0
  154. package/references/native/InsightServerProbe.java +29 -0
  155. package/references/native/InsightSnapshotFields.java +68 -0
  156. package/references/native/InsightSnapshotInspect.java +189 -0
  157. package/references/native/InsightSnapshotPublisher.java +123 -0
  158. package/references/native/InsightStartDebugger.java +28 -0
  159. package/references/native/InsightStopDebugger.java +20 -0
  160. package/references/native/InsightTraceInspect.java +24 -0
  161. package/references/native/InsightTraceState.java +130 -0
  162. package/references/native/InsightTraceTools.java +114 -0
  163. package/references/native/InsightVersionTracking.java +91 -0
  164. package/references/native-workflows.md +112 -0
  165. package/references/queries.json +1761 -0
  166. package/references/query-semantics.md +96 -0
  167. package/references/recovery.md +53 -0
  168. package/references/research.md +101 -0
  169. package/references/setup.md +49 -0
  170. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  171. package/runtime/GhidraMCP-LICENSE +202 -0
  172. package/runtime/GhidraMCP-NOTICE +10 -0
  173. package/runtime/provenance.json +11 -0
  174. package/skills/insight-re/SKILL.md +108 -0
@@ -0,0 +1,444 @@
1
+ import { command, Permission, CliError } from '@eyeauras/cli-factory';
2
+ import { readFile, mkdir, rename, rm, open, realpath, readdir, stat, cp } from 'node:fs/promises';
3
+ import { existsSync, constants } from 'node:fs';
4
+ import { join, dirname, resolve, delimiter, basename } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { execFile } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import { runtimeRoot, runtimeSelection, runtimeValues } from './runtime-selection.js';
9
+ import { runtimeCatalog } from './runtime-catalog.js';
10
+ import { cacheArtifact, extractArchive, fileSha256 } from './runtime-archives.js';
11
+ import { nativeInputs, nativeHelpers, nativeJvmDirectories, pinnedMcpJar } from './native-inputs.js';
12
+ import { GhidraServer, stopLocalServer } from './server.js';
13
+ import { fail, text, digest, object } from './model.js';
14
+ const exec = promisify(execFile), suffix = process.platform === 'win32' ? '.exe' : '';
15
+ export async function runtimeWrite(path, value) {
16
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
17
+ const temporary = path + '.new-' + randomUUID(), handle = await open(temporary, 'wx', 0o600);
18
+ try {
19
+ await handle.writeFile(JSON.stringify(value, null, 2) + '\n');
20
+ await handle.sync();
21
+ }
22
+ finally {
23
+ await handle.close();
24
+ }
25
+ await rename(temporary, path);
26
+ if (process.platform !== 'win32') {
27
+ const parent = await open(dirname(path), 'r');
28
+ try {
29
+ await parent.sync();
30
+ }
31
+ finally {
32
+ await parent.close();
33
+ }
34
+ }
35
+ }
36
+ const live = (pid) => { if (!Number.isSafeInteger(pid) || Number(pid) < 1)
37
+ return false; try {
38
+ process.kill(Number(pid), 0);
39
+ return true;
40
+ }
41
+ catch (error) {
42
+ if (error.code === 'ESRCH')
43
+ return false;
44
+ throw error;
45
+ } };
46
+ /** Passive, conservative inventory: a reused PID blocks deletion; it never grants kill authority. */
47
+ export async function runtimeProcesses(directory) {
48
+ const items = [];
49
+ async function walk(path, depth = 0) {
50
+ if (depth > 6)
51
+ return;
52
+ let entries;
53
+ try {
54
+ entries = await readdir(path, { withFileTypes: true });
55
+ }
56
+ catch (error) {
57
+ if (error.code === 'ENOENT')
58
+ return;
59
+ throw error;
60
+ }
61
+ for (const entry of entries) {
62
+ const at = join(path, entry.name);
63
+ if (entry.isDirectory())
64
+ await walk(at, depth + 1);
65
+ else if (entry.name === 'process.json') {
66
+ const p = object(JSON.parse(await readFile(at, 'utf8')));
67
+ let stopped = false;
68
+ try {
69
+ const s = object(JSON.parse(await readFile(join(path, 'stopped.json'), 'utf8')));
70
+ stopped = s.pid === p.pid && s.tokenSecret === p.tokenSecret;
71
+ }
72
+ catch (error) {
73
+ if (error.code !== 'ENOENT')
74
+ throw error;
75
+ }
76
+ if (!stopped && live(p.pid))
77
+ items.push({ kind: p.kind, pid: p.pid, sessionContext: p.context, target: p.target, mode: p.mode });
78
+ }
79
+ }
80
+ }
81
+ await walk(join(directory, 'workspace', 'ghidra-native'));
82
+ try {
83
+ const s = object(JSON.parse(await readFile(join(directory, 'workspace/server/local.json'), 'utf8')));
84
+ if (live(s.pid))
85
+ items.push({ kind: 'server', pid: s.pid });
86
+ }
87
+ catch (error) {
88
+ if (error.code !== 'ENOENT')
89
+ throw error;
90
+ }
91
+ return items;
92
+ }
93
+ export async function externalGhidra(path) {
94
+ const resolved = await realpath(path), kind = await stat(resolved);
95
+ const parent = kind.isDirectory() ? resolved : dirname(dirname(resolved));
96
+ const candidates = kind.isDirectory() ? [resolved, join(resolved, 'libexec')] : [dirname(resolved), parent, join(parent, 'libexec')];
97
+ for (const home of candidates)
98
+ if (existsSync(join(home, 'Ghidra/application.properties'))) {
99
+ if (!kind.isDirectory() && !['ghidraRun', 'ghidraRun.bat', 'analyzeHeadless', 'analyzeHeadless.bat'].includes(basename(resolved)))
100
+ fail('configuration', 'Use the Ghidra launcher or installation directory, not a target EXE.');
101
+ return home;
102
+ }
103
+ fail('configuration', 'The external path does not identify a complete Ghidra installation. Use its ghidraRun launcher or installation directory.');
104
+ }
105
+ async function findRoot(root, marker, depth = 0) {
106
+ if (existsSync(join(root, marker)))
107
+ return root;
108
+ if (depth >= 4)
109
+ return;
110
+ for (const entry of await readdir(root, { withFileTypes: true }))
111
+ if (entry.isDirectory()) {
112
+ const found = await findRoot(join(root, entry.name), marker, depth + 1);
113
+ if (found)
114
+ return found;
115
+ }
116
+ }
117
+ export class RuntimeManager {
118
+ c;
119
+ directory;
120
+ root;
121
+ constructor(c) {
122
+ this.c = c;
123
+ this.directory = c.appArguments.AppDataDirectory;
124
+ this.root = runtimeRoot(this.directory);
125
+ }
126
+ async status() {
127
+ const selected = runtimeSelection(this.directory), values = selected ? runtimeValuesForStatus(selected, this.c.profile.values) : this.c.profile.values;
128
+ const java = typeof values.java === 'string' ? values.java : undefined, home = typeof values.ghidraHome === 'string' ? values.ghidraHome : undefined;
129
+ const missing = [];
130
+ if (!java || !existsSync(java))
131
+ missing.push('jdk');
132
+ if (!home || !existsSync(join(home, 'Ghidra/application.properties')))
133
+ missing.push('ghidra');
134
+ if (selected?.uninstalledAt)
135
+ missing.push('uninstalled');
136
+ if (selected?.mode === 'managed' && !existsSync(join(this.root, 'installations', selected.id, 'ready.json')))
137
+ missing.push('installation_receipt');
138
+ let version;
139
+ if (home)
140
+ try {
141
+ version = /^application.version=(.+)$/m.exec(await readFile(join(home, 'Ghidra/application.properties'), 'utf8'))?.[1]?.trim();
142
+ }
143
+ catch (error) {
144
+ if (error.code !== 'ENOENT')
145
+ throw error;
146
+ }
147
+ const host = process.platform === 'darwin' ? (process.arch === 'arm64' ? 'mac_arm_64' : 'mac_x86_64') : process.platform === 'win32' ? 'win_x86_64' : 'linux_x86_64';
148
+ const native = home ? join(home, 'Ghidra/Features/Decompiler/os', host, 'decompile' + suffix) : undefined;
149
+ if (home && (!native || !existsSync(native)))
150
+ missing.push('decompiler');
151
+ if (version && version !== '12.1.2')
152
+ missing.push('compatible_ghidra');
153
+ let lastCheck;
154
+ try {
155
+ lastCheck = JSON.parse(await readFile(join(this.root, 'last-check.json'), 'utf8'));
156
+ }
157
+ catch (error) {
158
+ if (error.code !== 'ENOENT')
159
+ throw error;
160
+ }
161
+ const installations = [];
162
+ let stages = [];
163
+ try {
164
+ for (const name of (await readdir(join(this.root, 'installations'))).slice(0, 100)) {
165
+ if (!/^[a-f0-9]{64}$/.test(name))
166
+ continue;
167
+ const path = join(this.root, 'installations', name);
168
+ if (existsSync(join(path, 'ready.json'))) {
169
+ const ready = object(JSON.parse(await readFile(join(path, 'ready.json'), 'utf8')));
170
+ installations.push({ id: name, selected: name === selected?.id, ghidraVersion: ready.ghidraVersion, verifiedAt: ready.verifiedAt });
171
+ }
172
+ }
173
+ }
174
+ catch (error) {
175
+ if (error.code !== 'ENOENT')
176
+ throw error;
177
+ }
178
+ try {
179
+ stages = (await readdir(join(this.root, 'stages'))).slice(0, 100);
180
+ }
181
+ catch (error) {
182
+ if (error.code !== 'ENOENT')
183
+ throw error;
184
+ }
185
+ const processes = await runtimeProcesses(this.directory), server = processes.find(p => p.kind === 'server');
186
+ return { state: missing.length ? 'not_ready' : 'installed', mode: selected?.mode ?? (home || java ? 'external' : 'unconfigured'), recommended: 'managed', ghidraVersion: version, java, ghidraHome: home,
187
+ ...(selected ? { installation: selected.id, fingerprint: selected.fingerprint } : {}), missing, capabilities: { headless: !missing.length, decompiler: !missing.length, gui: !missing.length,
188
+ debugger: process.platform === 'darwin' ? { backend: 'system-lldb', requires: 'Apple developer tools and OS debugger authorization', checkedBy: 'ghidra doctor' } : { backend: 'host-debugger', checkedBy: 'ghidra doctor' } },
189
+ server: { mode: this.c.profile.values.server ? 'external' : 'local', state: this.c.profile.values.server ? 'configured' : server ? 'running' : 'stopped', ...(server ? { pid: server.pid } : {}) }, processes, installations, retainedStages: stages, lastCheck: lastCheck ? { ...lastCheck, appliesToSelection: lastCheck.fingerprint === selected?.fingerprint && !selected?.uninstalledAt } : undefined,
190
+ storage: { runtime: this.root, research: join(this.directory, 'workspace'), materials: join(this.directory, 'materials') } };
191
+ }
192
+ async download(offline = false) {
193
+ const catalog = runtimeCatalog(), cache = join(this.root, 'cache'), items = [];
194
+ for (const artifact of [catalog.ghidra, catalog.java]) {
195
+ const path = await cacheArtifact(artifact, cache, this.c.fetch, this.c.signal, offline);
196
+ items.push({ name: artifact.name, path, sha256: artifact.sha256, license: artifact.license });
197
+ }
198
+ return { state: 'downloaded', ghidraVersion: catalog.ghidraVersion, javaVersion: catalog.javaVersion, items };
199
+ }
200
+ async archive(artifact, provided, checksum, offline) {
201
+ if (provided === undefined)
202
+ return { path: await cacheArtifact(artifact, join(this.root, 'cache'), this.c.fetch, this.c.signal, offline), artifact };
203
+ const path = await realpath(resolve(this.c.cwd, text(provided, 4096))), cache = join(this.root, 'cache');
204
+ await mkdir(cache, { recursive: true, mode: 0o700 });
205
+ const temporary = join(cache, 'local.partial-' + randomUUID());
206
+ const source = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
207
+ try {
208
+ const metadata = await source.stat();
209
+ if (!metadata.isFile() || metadata.size > 2 * 1024 ** 3)
210
+ fail('invalid', 'Local runtime archive must be a regular file no larger than two GiB.');
211
+ const target = await open(temporary, 'wx', 0o600);
212
+ let size = 0;
213
+ try {
214
+ for await (const chunk of source.createReadStream({ autoClose: false })) {
215
+ if ((size += chunk.length) > 2 * 1024 ** 3)
216
+ fail('limit', 'Local runtime archive grew beyond two GiB.');
217
+ await target.writeFile(chunk);
218
+ }
219
+ await target.sync();
220
+ }
221
+ finally {
222
+ await target.close();
223
+ }
224
+ const hash = await fileSha256(temporary);
225
+ if (checksum !== undefined && hash !== text(checksum, 64).toLowerCase())
226
+ fail('binding', 'Local runtime archive differs from its expected SHA256.');
227
+ const format = (await readMagic(temporary)) === '504b' ? 'zip' : 'tgz', owned = join(cache, hash + '.' + format);
228
+ if (existsSync(owned)) {
229
+ if (await fileSha256(owned) !== hash)
230
+ fail('binding', 'Existing local-archive cache entry is corrupt; remove it before retrying.');
231
+ }
232
+ else
233
+ await rename(temporary, owned);
234
+ return { path: owned, artifact: { ...artifact, sha256: hash, format } };
235
+ }
236
+ finally {
237
+ await source.close();
238
+ await rm(temporary, { force: true });
239
+ }
240
+ }
241
+ async cleanupProbes() {
242
+ // A killed owner may leave a disposable validation server alive. Its authenticated
243
+ // receipt, not PID existence, authorizes recovery; no research repository is touched.
244
+ for (const parent of ['stages', 'checks']) {
245
+ let names;
246
+ try {
247
+ names = await readdir(join(this.root, parent));
248
+ }
249
+ catch (error) {
250
+ if (error.code === 'ENOENT')
251
+ continue;
252
+ throw error;
253
+ }
254
+ for (const name of names) {
255
+ if (!/^[a-f0-9-]{36}$/.test(name))
256
+ continue;
257
+ const root = join(this.root, parent, name, ...(parent === 'stages' ? ['validation'] : []));
258
+ if (existsSync(join(root, 'server/local.json'))) {
259
+ if (await realpath(root) !== resolve(root))
260
+ fail('binding', 'Retained runtime probe moved outside its owned storage.');
261
+ await stopLocalServer(root, this.c);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ async install(o) {
267
+ await this.cleanupProbes();
268
+ if (o.executable && o.archive)
269
+ fail('invalid', 'Choose --executable or --archive.');
270
+ if (o.archiveSha256 && !o.archive || o.javaArchiveSha256 && !o.javaArchive)
271
+ fail('invalid', 'Archive checksum requires its archive path.');
272
+ const catalog = runtimeCatalog(), offline = Boolean(o.offline), external = o.executable ? await externalGhidra(resolve(this.c.cwd, text(o.executable, 4096))) : undefined;
273
+ const ghidra = external ? undefined : await this.archive(catalog.ghidra, o.archive, o.archiveSha256, offline);
274
+ const java = o.java ? await realpath(resolve(this.c.cwd, text(o.java, 4096))) : undefined;
275
+ if (java && o.javaArchive)
276
+ fail('invalid', 'Choose --java or --java-archive.');
277
+ const jdk = java ? undefined : await this.archive(catalog.java, o.javaArchive, o.javaArchiveSha256, offline);
278
+ const identity = digest({ schema: 1, platform: process.platform, architecture: process.arch, external, java, ghidra: ghidra?.artifact.sha256, jdk: jdk?.artifact.sha256, bridge: pinnedMcpJar });
279
+ const installation = join(this.root, 'installations', identity), temporary = join(this.root, 'stages', randomUUID());
280
+ await mkdir(temporary, { recursive: true, mode: 0o700 });
281
+ let selected, complete = false;
282
+ try {
283
+ let inputs;
284
+ if (existsSync(join(installation, 'ready.json'))) {
285
+ const receipt = object(JSON.parse(await readFile(join(installation, 'ready.json'), 'utf8')));
286
+ inputs = await nativeInputs({ ...this.c.profile.values, java: java ?? receipt.java, ghidraHome: external ?? join(installation, 'ghidra') });
287
+ if (inputs.fingerprint !== receipt.fingerprint)
288
+ fail('binding', 'Installed runtime differs from its verified installation receipt.');
289
+ }
290
+ else {
291
+ let ghidraHome = external;
292
+ if (ghidra) {
293
+ const extracted = join(temporary, 'ghidra-extract');
294
+ await extractArchive(ghidra.path, extracted, ghidra.artifact.format);
295
+ const found = await findRoot(extracted, 'Ghidra/application.properties');
296
+ if (!found)
297
+ fail('configuration', 'Archive omitted a complete Ghidra installation.');
298
+ await rename(found, join(temporary, 'ghidra'));
299
+ await rm(extracted, { recursive: true, force: true });
300
+ ghidraHome = join(temporary, 'ghidra');
301
+ const host = process.arch === 'arm64' ? 'mac_arm_64' : 'mac_x86_64';
302
+ if (process.platform === 'darwin' && !existsSync(join(ghidraHome, 'Ghidra/Features/Decompiler/os', host, 'decompile'))) {
303
+ const bottle = await cacheArtifact(catalog.ghidra, join(this.root, 'cache'), this.c.fetch, this.c.signal, offline), nativeRoot = join(temporary, 'native-extract');
304
+ await extractArchive(bottle, nativeRoot, 'tgz');
305
+ const nativeHome = join(nativeRoot, catalog.ghidra.root);
306
+ for (const module of ['Decompiler', 'FileFormats'])
307
+ await cp(join(nativeHome, 'Ghidra/Features', module, 'os', host), join(ghidraHome, 'Ghidra/Features', module, 'os', host), { recursive: true, errorOnExist: true });
308
+ await rm(nativeRoot, { recursive: true, force: true });
309
+ }
310
+ }
311
+ let javaPath = java;
312
+ if (jdk) {
313
+ const extracted = join(temporary, 'jdk-extract');
314
+ await extractArchive(jdk.path, extracted, jdk.artifact.format);
315
+ const home = await findRoot(extracted, 'bin/java' + suffix);
316
+ if (!home)
317
+ fail('configuration', 'Java archive omitted a JDK.');
318
+ await rename(home, join(temporary, 'jdk'));
319
+ await rm(extracted, { recursive: true, force: true });
320
+ javaPath = join(temporary, 'jdk/bin/java' + suffix);
321
+ }
322
+ inputs = await nativeInputs({ ...this.c.profile.values, java: javaPath, ghidraHome });
323
+ await this.probe(inputs, join(temporary, 'validation'));
324
+ await rm(join(temporary, 'validation'), { recursive: true, force: true });
325
+ const receipt = { schema: 1, id: identity, ghidraVersion: inputs.ghidraVersion, fingerprint: inputs.fingerprint, java: java ?? join(installation, 'jdk/bin/java' + suffix), ghidraHome: external ?? join(installation, 'ghidra'),
326
+ archives: [...(ghidra ? [{ sha256: ghidra.artifact.sha256, source: o.archive ? 'local' : catalog.ghidra.url }] : []), ...(jdk ? [{ sha256: jdk.artifact.sha256, source: o.javaArchive ? 'local' : catalog.java.url }] : [])], verifiedAt: new Date().toISOString(), checks: ['jdk', 'native-decompiler', 'local-server-reader-publisher-authority'] };
327
+ await runtimeWrite(join(temporary, 'ready.json'), receipt);
328
+ await mkdir(dirname(installation), { recursive: true, mode: 0o700 });
329
+ if (existsSync(installation))
330
+ fail('binding', 'Incomplete installation directory is retained; inspect it before removing it.');
331
+ await rename(temporary, installation);
332
+ if (process.platform !== 'win32') {
333
+ const parent = await open(dirname(installation), 'r');
334
+ try {
335
+ await parent.sync();
336
+ }
337
+ finally {
338
+ await parent.close();
339
+ }
340
+ }
341
+ }
342
+ selected = { schema: 1, mode: external ? 'external' : 'managed', id: identity, ghidraHome: external ?? join(installation, 'ghidra'), java: java ?? join(installation, 'jdk/bin/java' + suffix), fingerprint: inputs.fingerprint, selectedAt: new Date().toISOString() };
343
+ await runtimeWrite(join(this.root, 'selection.json'), selected);
344
+ complete = true;
345
+ return { state: 'installed', ...selected, checks: ['jdk', 'native-decompiler', 'local-server-reader-publisher-authority'] };
346
+ }
347
+ catch (error) {
348
+ await runtimeWrite(join(temporary, 'failure.json'), { state: 'failed', code: error instanceof CliError ? error.code : 'installation', message: error instanceof Error ? error.message : 'Runtime installation failed' });
349
+ if (error instanceof CliError)
350
+ throw new CliError(error.message, { code: error.code, result: { ...(error.result && typeof error.result === 'object' ? error.result : {}), diagnostics: temporary } });
351
+ throw error;
352
+ }
353
+ finally {
354
+ if (complete)
355
+ await rm(temporary, { recursive: true, force: true });
356
+ }
357
+ }
358
+ async uninstall(purgeCache = false) {
359
+ await this.cleanupProbes();
360
+ const selected = runtimeSelection(this.directory);
361
+ if (!selected)
362
+ fail('configuration', 'No managed selection exists; externally configured profile paths are never removed.');
363
+ if (selected.id) {
364
+ if (!/^[a-f0-9]{64}$/.test(selected.id))
365
+ fail('binding', 'Invalid installation identity.');
366
+ await rm(join(this.root, 'installations', selected.id), { recursive: true, force: true });
367
+ }
368
+ if (purgeCache)
369
+ await rm(join(this.root, 'cache'), { recursive: true, force: true });
370
+ await runtimeWrite(join(this.root, 'selection.json'), { ...selected, uninstalledAt: new Date().toISOString() });
371
+ // Retain the selection: an explicit external profile must never become an implicit fallback.
372
+ return { state: 'uninstalled', mode: selected.mode, externalInstallationPreserved: selected.mode === 'external', researchPreserved: true, materialsPreserved: true, cachePreserved: !purgeCache };
373
+ }
374
+ async probe(inputs, root) {
375
+ await mkdir(root, { recursive: true, mode: 0o700 });
376
+ const classes = await nativeHelpers(root, inputs), log = join(root, 'doctor.log');
377
+ let output;
378
+ try {
379
+ output = await exec(inputs.java, ['-Djava.awt.headless=true', '-Dghidra.home=' + inputs.ghidraHome, ...nativeJvmDirectories(root), '-cp', classes + delimiter + inputs.classpath, 'InsightRuntimeDoctor'], { cwd: root, env: { PATH: dirname(inputs.java), HOME: root, TMPDIR: root }, timeout: 90000, maxBuffer: 1024 * 1024, signal: this.c.signal });
380
+ }
381
+ catch (error) {
382
+ await runtimeWrite(log, { failure: String(error.stderr ?? error) });
383
+ fail('configuration', 'Native decompiler probe failed; the diagnostic stage is retained.', { diagnostics: log });
384
+ }
385
+ const value = object(JSON.parse(output.stdout));
386
+ if (value.state !== 'ready' || value.decompiler !== true || value.expectedReturn !== 42)
387
+ fail('partial', 'Native decompiler probe did not verify its expected result.');
388
+ const probeContext = { ...this.c, profile: { ...this.c.profile, values: { ...this.c.profile.values, server: undefined } } };
389
+ const server = new GhidraServer(root, inputs, probeContext);
390
+ try {
391
+ const result = await server.administration('connect', {});
392
+ if (result.connected !== true || result.mode !== 'local')
393
+ fail('partial', 'Owned server probe did not connect.');
394
+ const checked = await server.administration('status', {});
395
+ if (checked.connected !== true)
396
+ fail('partial', 'Owned server identity/authority probe failed.');
397
+ }
398
+ finally {
399
+ await server.stop();
400
+ }
401
+ return value;
402
+ }
403
+ async doctor() {
404
+ await this.cleanupProbes();
405
+ const root = join(this.root, 'checks', randomUUID()), values = runtimeValues(this.directory, this.c.profile.values);
406
+ const inputs = await nativeInputs(values), native = await this.probe(inputs, root);
407
+ let debuggerCheck = { state: 'host_specific' };
408
+ if (process.platform === 'darwin')
409
+ try {
410
+ const checked = await exec('/usr/bin/lldb', ['--no-lldbinit', '--batch', '-o', 'script import lldb,sys; print("INSIGHT_LLDB_PYTHON_OK")'], { env: { PATH: '/usr/bin:/bin', HOME: root, TMPDIR: root }, timeout: 30000, maxBuffer: 65536 });
411
+ debuggerCheck = { state: checked.stdout.includes('INSIGHT_LLDB_PYTHON_OK') ? 'ready' : 'missing', backend: 'system-lldb' };
412
+ }
413
+ catch {
414
+ debuggerCheck = { state: 'missing', backend: 'system-lldb', reason: 'Apple developer tools or debugger authorization is unavailable' };
415
+ }
416
+ const result = { state: 'verified', fingerprint: inputs.fingerprint, ghidraVersion: inputs.ghidraVersion, checkedAt: new Date().toISOString(), headless: native.headless, decompiler: native.decompiler, server: true, debugger: debuggerCheck, diagnostics: root };
417
+ await runtimeWrite(join(this.root, 'last-check.json'), result);
418
+ if (debuggerCheck.state === 'missing')
419
+ fail('partial', 'Static analysis and local server passed; the host debugger is unavailable.', { ...result, state: 'partial' });
420
+ return result;
421
+ }
422
+ }
423
+ function runtimeValuesForStatus(selection, values) { return { ...values, java: selection.java, ghidraHome: selection.ghidraHome }; }
424
+ async function readMagic(path) { const handle = await open(path, 'r'); try {
425
+ const b = Buffer.alloc(2);
426
+ await handle.read(b, 0, 2, 0);
427
+ return b.toString('hex');
428
+ }
429
+ finally {
430
+ await handle.close();
431
+ } }
432
+ export function runtimeCommands(exclusive) {
433
+ const source = [{ flags: '--archive <path>', description: 'Local Ghidra zip/tar.gz; missing dependencies are downloaded' },
434
+ { flags: '--archive-sha256 <sha256>', description: 'Expected SHA256 for the supplied Ghidra archive' },
435
+ { flags: '--java-archive <path>', description: 'Local Java 21 JDK zip/tar.gz' },
436
+ { flags: '--java-archive-sha256 <sha256>', description: 'Expected SHA256 for the supplied JDK archive' },
437
+ { flags: '--java <path>', description: 'Use an external Java 21 JDK executable' },
438
+ { flags: '--executable <path>', description: 'Use an external Ghidra launcher or installation directory; never deleted' },
439
+ { flags: '--offline', description: 'Use local archives/cache only; never access the network' }];
440
+ return [command('download', 'Cache the pinned complete Ghidra/JDK distribution without activation', ({ options: o }, c) => exclusive(c, () => new RuntimeManager(c).download(Boolean(o.offline)), false), { permission: Permission.Update, options: [source.at(-1)] }),
441
+ command('install', 'Prepare, verify and select a complete runtime, including Ghidra Server', ({ options: o }, c) => exclusive(c, () => new RuntimeManager(c).install(o)), { permission: Permission.Update, options: source }),
442
+ command('uninstall', 'Remove selected owned runtime files while preserving research and external installations', ({ options: o }, c) => exclusive(c, () => new RuntimeManager(c).uninstall(Boolean(o.purgeCache))), { permission: 'Destructive', options: [{ flags: '--purge-cache', description: 'Also remove owned downloaded archives' }] }),
443
+ command('doctor', 'Launch a disposable native decompiler and local server probe', (_input, c) => exclusive(c, () => new RuntimeManager(c).doctor(), false), { permission: Permission.Update })];
444
+ }
@@ -0,0 +1,13 @@
1
+ export interface RuntimeSelection {
2
+ schema: 1;
3
+ mode: 'managed' | 'external';
4
+ id?: string;
5
+ ghidraHome: string;
6
+ java: string;
7
+ fingerprint: string;
8
+ selectedAt: string;
9
+ uninstalledAt?: string;
10
+ }
11
+ export declare const runtimeRoot: (directory: string) => string;
12
+ export declare function runtimeSelection(directory: string): RuntimeSelection | undefined;
13
+ export declare function runtimeValues(directory: string, values: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,25 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join, isAbsolute } from 'node:path';
3
+ import { fail, object } from './model.js';
4
+ export const runtimeRoot = (directory) => join(directory, 'runtime');
5
+ export function runtimeSelection(directory) {
6
+ const path = join(runtimeRoot(directory), 'selection.json');
7
+ if (!existsSync(path))
8
+ return;
9
+ const value = object(JSON.parse(readFileSync(path, 'utf8')));
10
+ if (value.schema !== 1 || !['managed', 'external'].includes(String(value.mode)) || typeof value.java !== 'string' || !isAbsolute(value.java) || typeof value.ghidraHome !== 'string' || !isAbsolute(value.ghidraHome) || !/^[a-f0-9]{64}$/.test(String(value.fingerprint)))
11
+ fail('binding', 'Runtime selection receipt is invalid.');
12
+ if (value.mode === 'managed' && (!/^[a-f0-9]{64}$/.test(String(value.id)) || value.ghidraHome !== join(runtimeRoot(directory), 'installations', String(value.id), 'ghidra')))
13
+ fail('binding', 'Managed runtime moved outside its owned installation.');
14
+ return value;
15
+ }
16
+ export function runtimeValues(directory, values) {
17
+ const selected = runtimeSelection(directory);
18
+ if (!selected)
19
+ return values;
20
+ if (selected.uninstalledAt)
21
+ fail('configuration', 'Selected Ghidra runtime was uninstalled. Run insight-cli ghidra install.');
22
+ if (selected.mode === 'managed' && !existsSync(join(runtimeRoot(directory), 'installations', selected.id, 'ready.json')))
23
+ fail('configuration', 'Selected managed Ghidra is not installed. Run insight-cli ghidra install.');
24
+ return { ...values, java: selected.java, ghidraHome: selected.ghidraHome };
25
+ }
@@ -0,0 +1,12 @@
1
+ import { type CommandDefinition } from '@eyeauras/cli-factory';
2
+ export interface SavedMaterials {
3
+ state: 'saved';
4
+ path: string;
5
+ kind: 'file' | 'directory';
6
+ files: number;
7
+ bytes: number;
8
+ sha256: string;
9
+ }
10
+ /** Copy user materials without initializing Ghidra, targets, sessions or Workspace. */
11
+ export declare function saveMaterials(appDataDirectory: string, source: string, signal?: AbortSignal): Promise<SavedMaterials>;
12
+ export declare function artifactsCommands(): CommandDefinition[];