@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,182 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { readFile, writeFile, readdir, mkdir, open, rename, rm, realpath, stat } from 'node:fs/promises';
3
+ import { basename, delimiter, dirname, isAbsolute, join, relative, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { fail, stable, text } from './model.js';
8
+ import { managedExtensions, extensionJvmArguments } from './extensions.js';
9
+ import { runtimeValues, runtimeSelection } from './runtime-selection.js';
10
+ export const pinnedMcpJar = '7084e2fda16dc779dd05398e48821ff3f9ace4d32757ea143b257b39689e5fbd';
11
+ export const packagedMcpJar = fileURLToPath(new URL('../../runtime/GhidraMCP-7.0.0.jar', import.meta.url));
12
+ /** Ghidra has its own cache/settings overrides in addition to Java's home/tmp properties. */
13
+ export const nativeJvmDirectories = (root) => ['-Duser.home=' + root, '-Djava.io.tmpdir=' + root,
14
+ '-Dapplication.cachedir=' + join(root, 'cache'), '-Dapplication.settingsdir=' + join(root, 'settings'), '-Dapplication.tempdir=' + root];
15
+ const run = promisify(execFile);
16
+ export async function moduleJars(home) {
17
+ const jars = [];
18
+ async function collect(folder) {
19
+ for (const entry of (await readdir(folder, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
20
+ const path = join(folder, entry.name);
21
+ if (entry.isDirectory() && entry.name !== 'data')
22
+ await collect(path);
23
+ else if (entry.isFile() && entry.name.endsWith('.jar') && basename(folder) === 'lib')
24
+ jars.push(path);
25
+ }
26
+ }
27
+ await collect(join(home, 'Ghidra'));
28
+ return jars;
29
+ }
30
+ async function sha(path) {
31
+ const handle = await open(path, 'r');
32
+ try {
33
+ if (!(await handle.stat()).isFile())
34
+ fail('configuration', 'Native runtime input must be a regular file.');
35
+ const hash = createHash('sha256');
36
+ for await (const chunk of handle.createReadStream({ autoClose: false }))
37
+ hash.update(chunk);
38
+ return hash.digest('hex');
39
+ }
40
+ finally {
41
+ await handle.close();
42
+ }
43
+ }
44
+ /** Installed native code/data, excluding documentation and isolated generated caches. */
45
+ export async function nativeRuntimeFiles(ghidraHome, java) {
46
+ const files = [], jdkHome = dirname(dirname(java)), suffix = process.platform === 'win32' ? '.exe' : '';
47
+ const named = (prefix, base, path) => ({ name: prefix + '/' + relative(base, path).split(sep).join('/'), path });
48
+ for (const path of [java, ...['javac', 'keytool'].map(name => join(dirname(java), name + suffix))])
49
+ if (!(await stat(path)).isFile())
50
+ fail('configuration', 'The configured JDK tool is not a regular file.');
51
+ async function walk(path, base, prefix, selected, ancestors = new Set()) {
52
+ const real = await realpath(path);
53
+ if (ancestors.has(real))
54
+ fail('configuration', 'Native runtime directories contain a cycle.');
55
+ const parents = new Set(ancestors);
56
+ parents.add(real);
57
+ for (const entry of (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
58
+ if (['__pycache__', '.DS_Store'].includes(entry.name) || prefix === 'jdk' && entry.name === 'src.zip')
59
+ continue;
60
+ const item = join(path, entry.name), kind = entry.isSymbolicLink() ? await stat(item) : entry;
61
+ if (kind.isDirectory())
62
+ await walk(item, base, prefix, selected || ['data', 'os', 'pypkg', 'ghidra_scripts'].includes(entry.name), parents);
63
+ else if (kind.isFile() && (selected || entry.name === 'Module.manifest' || entry.name === 'application.properties' || entry.name.endsWith('.jar') && basename(path) === 'lib'))
64
+ files.push(named(prefix, base, item));
65
+ }
66
+ }
67
+ await walk(join(ghidraHome, 'Ghidra'), ghidraHome, 'ghidra', false);
68
+ for (const directory of ['bin', 'lib', 'conf'])
69
+ await walk(join(jdkHome, directory), jdkHome, 'jdk', true);
70
+ files.push(named('jdk', jdkHome, join(jdkHome, 'release')));
71
+ if (!(await stat(join(jdkHome, 'lib', 'modules'))).isFile())
72
+ fail('configuration', 'The configured JDK omitted its runtime modules.');
73
+ return files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
74
+ }
75
+ export async function nativeRuntimeFingerprint(ghidraVersion, files) {
76
+ const hashes = [];
77
+ for (const file of files)
78
+ hashes.push({ name: file.name, sha256: await sha(file.path) });
79
+ return createHash('sha256').update(stable({ schema: 2, ghidraVersion, hashes })).digest('hex');
80
+ }
81
+ export async function nativeInputs(values, directory) {
82
+ if (directory)
83
+ values = runtimeValues(dirname(directory), values);
84
+ if (!values.java || !values.ghidraHome)
85
+ fail('configuration', 'Ghidra is not installed. Run insight-cli ghidra install, or supply external runtime paths.');
86
+ const requested = [text(values.java, 4096), text(values.ghidraHome, 4096), text(values.mcpJar ?? packagedMcpJar, 4096)];
87
+ if (!requested.every(isAbsolute))
88
+ fail('configuration', 'Configure absolute Java, Ghidra home and GhidraMCP jar paths.');
89
+ const [java, ghidraHome, mcpJar] = await Promise.all(requested.map(path => realpath(path)));
90
+ const jarHash = await sha(mcpJar);
91
+ if (jarHash !== pinnedMcpJar || values.mcpJarSha256 !== undefined && String(values.mcpJarSha256).toLowerCase() !== jarHash)
92
+ fail('binding', 'GhidraMCP artifact differs from the pinned, verified runtime.');
93
+ const properties = await readFile(join(ghidraHome, 'Ghidra', 'application.properties'), 'utf8');
94
+ const ghidraVersion = /^application.version=(.+)$/m.exec(properties)?.[1]?.trim();
95
+ if (ghidraVersion !== '12.1.2')
96
+ fail('configuration', 'This build requires the verified Ghidra 12.1.2 runtime.');
97
+ let jdk;
98
+ try {
99
+ jdk = await run(java, ['--version'], { timeout: 10000, maxBuffer: 16384 });
100
+ }
101
+ catch {
102
+ fail('configuration', 'Configured Java executable could not be verified.');
103
+ }
104
+ if (!/^(?:openjdk|java) 21(?:[.\s-]|$)/m.test(jdk.stdout + '\n' + jdk.stderr))
105
+ fail('configuration', 'A Java 21 JDK is required.');
106
+ for (const program of ['javac', 'keytool'])
107
+ if (!(await stat(join(dirname(java), program + (process.platform === 'win32' ? '.exe' : '')))).isFile())
108
+ fail('configuration', 'Java must come from a JDK with javac and keytool.');
109
+ const modules = await managedExtensions(directory);
110
+ const extraJars = modules.flatMap(m => m.files.filter(f => /^lib\/[^/]+\.jar$/.test(f.name)).map(f => join(m.root, f.name)));
111
+ const baseJars = [mcpJar, ...await moduleJars(ghidraHome)], jars = [...baseJars, ...extraJars];
112
+ const debug = join(ghidraHome, 'Ghidra', 'Debug'), debuggerWheels = [
113
+ join(debug, 'Debugger-rmi-trace/pypkg/dist/protobuf-6.31.0-py3-none-any.whl'),
114
+ join(debug, 'Debugger-rmi-trace/pypkg/dist/ghidratrace-12.1-py3-none-any.whl'),
115
+ join(debug, 'Debugger-agent-lldb/pypkg/dist/ghidralldb-12.1-py3-none-any.whl')
116
+ ];
117
+ const files = await nativeRuntimeFiles(ghidraHome, java);
118
+ const baseFingerprint = await nativeRuntimeFingerprint(ghidraVersion, [...files, { name: 'bridge/GhidraMCP-7.0.0.jar', path: mcpJar }]);
119
+ if (directory) {
120
+ const selected = runtimeSelection(dirname(directory));
121
+ if (selected && selected.fingerprint !== baseFingerprint)
122
+ fail('binding', 'Selected runtime inputs differ from their installation receipt; reinstall or restore the verified files.');
123
+ }
124
+ for (const module of modules)
125
+ for (const file of module.files)
126
+ files.push({ name: 'extension/' + module.name + '/' + file.name, path: join(module.root, file.name) });
127
+ // Logical names bind locations without embedding the author's installation path.
128
+ files.push({ name: 'bridge/GhidraMCP-7.0.0.jar', path: mcpJar });
129
+ const fingerprint = modules.length ? await nativeRuntimeFingerprint(ghidraVersion, files) : baseFingerprint;
130
+ return { java, ghidraHome, mcpJar, jars, classpath: jars.join(delimiter), ghidraVersion, fingerprint, debuggerWheels, extensionRoots: modules.map(m => m.root), serverClasspath: baseJars.join(delimiter) };
131
+ }
132
+ /** Compile packaged helper sources against the selected installation, never an author checkout. */
133
+ export async function nativeHelpers(directory, inputs) {
134
+ const sources = fileURLToPath(new URL('../../references/native/', import.meta.url));
135
+ const names = [...(await readdir(sources)).filter(name => name.endsWith('.java')), '../InsightRepositoryProbe.java'].sort();
136
+ if (!names.length)
137
+ fail('configuration', 'Installed package omitted its native helper sources.');
138
+ const hashes = [];
139
+ for (const name of names)
140
+ hashes.push(await sha(join(sources, name)));
141
+ const key = createHash('sha256').update(stable([inputs.fingerprint, names, hashes])).digest('hex');
142
+ const parent = join(directory, 'native-helpers');
143
+ await mkdir(parent, { recursive: true, mode: 0o700 });
144
+ const classes = join(parent, key);
145
+ try {
146
+ if (await readFile(join(classes, 'complete'), 'utf8') === key)
147
+ return classes;
148
+ }
149
+ catch (error) {
150
+ if (error.code !== 'ENOENT')
151
+ throw error;
152
+ }
153
+ const temporary = join(parent, 'compiling-' + randomUUID());
154
+ await mkdir(temporary, { mode: 0o700 });
155
+ try {
156
+ await run(join(dirname(inputs.java), 'javac' + (process.platform === 'win32' ? '.exe' : '')), ['-proc:none', '-cp', inputs.classpath, '-d', temporary, ...names.map(name => join(sources, name))], { timeout: 60000, maxBuffer: 262144 });
157
+ const complete = await open(join(temporary, 'complete'), 'wx', 0o600);
158
+ try {
159
+ await complete.writeFile(key);
160
+ await complete.sync();
161
+ }
162
+ finally {
163
+ await complete.close();
164
+ }
165
+ try {
166
+ await rename(temporary, classes);
167
+ }
168
+ catch (error) {
169
+ if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? '') || await readFile(join(classes, 'complete'), 'utf8') !== key)
170
+ throw error;
171
+ }
172
+ return classes;
173
+ }
174
+ catch (error) {
175
+ await writeFile(join(parent, 'compilation-' + key + '.log'), String(error.stderr ?? 'Native compilation failed'), { mode: 0o600 });
176
+ fail('configuration', 'Native helper compilation failed against the configured runtime; diagnostics remain in owned profile storage.');
177
+ }
178
+ finally {
179
+ await rm(temporary, { recursive: true, force: true });
180
+ }
181
+ }
182
+ export const nativeExtensionArguments = (inputs) => extensionJvmArguments(inputs.extensionRoots ?? []);
@@ -0,0 +1,2 @@
1
+ import type { HttpBridge } from './transport.js';
2
+ export declare function nativeQuery(http: HttpBridge, program: string, operation: string, parameters: Record<string, unknown>, write?: boolean): Promise<unknown>;
@@ -0,0 +1,29 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { object, fail } from './model.js';
4
+ export async function nativeQuery(http, program, operation, parameters, write = false) {
5
+ const base = operation.startsWith('/sdk/') ? 'InsightSdkScript' : operation.startsWith('/emulate_') ? 'InsightEmulation' : 'InsightResearchQueries';
6
+ const name = base + '_' + randomUUID().replaceAll('-', '');
7
+ const source = await readFile(new URL('../../references/native/' + base + '.java', import.meta.url), 'utf8');
8
+ const response = object(await http.call('/run_script_inline', { program, code: source.replace('public class ' + base + ' ', 'public class ' + name + ' '),
9
+ args: Buffer.from(JSON.stringify({ operation, parameters, ...(write ? { write: true } : {}) }), 'utf8').toString('base64') }));
10
+ if (response.success !== true || typeof response.console_output !== 'string')
11
+ fail('partial', 'Native research helper did not complete.');
12
+ const matches = [...response.console_output.matchAll(/^INSIGHT_QUERY:(.+)$/gm)];
13
+ if (matches.length !== 1)
14
+ fail('partial', 'Native research helper omitted its unique result.');
15
+ let result;
16
+ try {
17
+ result = object(JSON.parse(matches[0][1]));
18
+ }
19
+ catch {
20
+ fail('partial', 'Native research helper returned malformed JSON.');
21
+ }
22
+ if (result.error) {
23
+ const error = object(result.error), code = String(error.code);
24
+ fail(['invalid', 'unsupported', 'limit', ...(operation.startsWith('/sdk/') ? ['rejected'] : [])].includes(code) ? code : 'partial', typeof error.message === 'string' ? error.message.slice(0, 500) : 'Native research failed.');
25
+ }
26
+ if (!Object.hasOwn(result, 'value'))
27
+ fail('partial', 'Native research helper omitted its value.');
28
+ return result.value;
29
+ }
@@ -0,0 +1,31 @@
1
+ import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
2
+ import { Workspace } from './workspace.js';
3
+ import { Research, type ResearchBackend } from './research.js';
4
+ export declare class Projects {
5
+ private readonly workspace;
6
+ private readonly backend;
7
+ private readonly research;
8
+ private readonly tasks;
9
+ constructor(workspace: Workspace, backend: () => Promise<ResearchBackend>, research: Research, tasks: Map<string, Promise<unknown>>);
10
+ private ordered;
11
+ private container;
12
+ private owned;
13
+ private result;
14
+ private native;
15
+ list(): Promise<{
16
+ items: {
17
+ project: string;
18
+ }[];
19
+ count: number;
20
+ }>;
21
+ read(op: string, name: string, p: Record<string, unknown>): Promise<Record<string, unknown>>;
22
+ private recovered;
23
+ change(op: string, name: string, p: Record<string, unknown>, callerId?: string): Promise<unknown>;
24
+ private newOutput;
25
+ private exportArtifact;
26
+ export(selected: {
27
+ session: string;
28
+ target: string;
29
+ }, output: string, callerId?: string): Promise<unknown>;
30
+ }
31
+ export declare function projectCommands(service: (c: CommandContext) => Projects): CommandDefinition[];
@@ -0,0 +1,301 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { constants } from 'node:fs';
3
+ import { mkdir, lstat, realpath, readFile, open, rename, readdir, copyFile, chmod } from 'node:fs/promises';
4
+ import { join, resolve, dirname } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { scope, number } from './research.js';
7
+ import { id, text, object, fail, stable } from './model.js';
8
+ import { repositoryPath } from './administration.js';
9
+ import { selectedOptions } from './arguments.js';
10
+ const projectName = (value) => { const name = id(value); if (name.startsWith('.') || name.length > 80)
11
+ fail('invalid', 'Project name requires 1–80 non-hidden ID characters.'); return name; };
12
+ async function persist(path, value) {
13
+ const temporary = path + '.new-' + randomUUID(), h = await open(temporary, 'wx', 0o600);
14
+ try {
15
+ await h.writeFile(stable(value));
16
+ await h.sync();
17
+ }
18
+ finally {
19
+ await h.close();
20
+ }
21
+ await rename(temporary, path);
22
+ if (process.platform !== 'win32') {
23
+ const dir = await open(dirname(path), 'r');
24
+ try {
25
+ await dir.sync();
26
+ }
27
+ finally {
28
+ await dir.close();
29
+ }
30
+ }
31
+ }
32
+ export class Projects {
33
+ workspace;
34
+ backend;
35
+ research;
36
+ tasks;
37
+ constructor(workspace, backend, research, tasks) {
38
+ this.workspace = workspace;
39
+ this.backend = backend;
40
+ this.research = research;
41
+ this.tasks = tasks;
42
+ }
43
+ async ordered(action) {
44
+ const key = 'projects', previous = this.tasks.get(key), task = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
45
+ this.tasks.set(key, task);
46
+ try {
47
+ return await task;
48
+ }
49
+ finally {
50
+ if (this.tasks.get(key) === task)
51
+ this.tasks.delete(key);
52
+ }
53
+ }
54
+ async container() {
55
+ const path = join(this.workspace.directory, 'ghidra-projects');
56
+ await mkdir(path, { recursive: true, mode: 0o700 });
57
+ if ((await lstat(path)).isSymbolicLink() || await realpath(path) !== resolve(path))
58
+ fail('binding', 'Project container moved outside the selected workspace.');
59
+ return path;
60
+ }
61
+ async owned(name, create = false) {
62
+ const root = join(await this.container(), projectName(name));
63
+ if (create) {
64
+ await mkdir(root, { mode: 0o700 });
65
+ await persist(join(root, 'owner.json'), { schema: 1, project: name, profile: this.workspace.profile });
66
+ }
67
+ if ((await lstat(root)).isSymbolicLink() || await realpath(root) !== resolve(root))
68
+ fail('binding', 'Project root moved outside owned storage.');
69
+ const owner = object(JSON.parse(await readFile(join(root, 'owner.json'), 'utf8')));
70
+ if (owner.schema !== 1 || owner.project !== name || owner.profile !== this.workspace.profile)
71
+ fail('binding', 'Project ownership receipt differs.');
72
+ return root;
73
+ }
74
+ result(op, name, p, raw) {
75
+ const value = object(raw);
76
+ if (value.project !== name)
77
+ fail('binding', 'Native operation returned another project.');
78
+ const states = { create: 'ready', restore: 'ready', open: 'ready', close: 'closed', info: 'ready', create_folder: 'created', move_file: 'moved', move_folder: 'moved', delete_file: 'deleted', delete: 'deleted', import: 'imported', archive: 'archived' };
79
+ if (op === 'files') {
80
+ if (value.path !== p.path || value.offset !== p.offset || value.limit !== p.limit || !Number.isSafeInteger(value.total) || Number(value.total) < 0 || !Array.isArray(value.items) || value.items.length > Number(p.limit))
81
+ fail('partial', 'Project listing omitted its selected page.');
82
+ for (const raw of value.items) {
83
+ const row = object(raw);
84
+ for (const field of ['name', 'path', 'type'])
85
+ if (typeof row[field] !== 'string' || !row[field])
86
+ fail('partial', 'Project file identity is incomplete.');
87
+ repositoryPath(row.path);
88
+ }
89
+ return { ...value, truncated: Number(p.offset) + value.items.length < Number(value.total) };
90
+ }
91
+ if (value.state !== states[op])
92
+ fail('partial', 'Project operation did not establish its expected state.');
93
+ if (['create_folder', 'delete_file'].includes(op) && value.path !== p.path)
94
+ fail('binding', 'Project operation selected another path.');
95
+ if (['move_file', 'move_folder'].includes(op)) {
96
+ const expected = (p.destination === '/' ? '' : p.destination) + '/' + String(p.path).split('/').at(-1);
97
+ if (value.path !== expected)
98
+ fail('binding', 'Moved item destination differs.');
99
+ }
100
+ if (op === 'import' && object(value.file).path !== p.path)
101
+ fail('binding', 'Imported file destination differs.');
102
+ if (op === 'archive' && (!Number.isSafeInteger(value.size) || Number(value.size) < 1))
103
+ fail('partial', 'Archive output is empty.');
104
+ return value;
105
+ }
106
+ async native(op, name, p, create = false) {
107
+ const root = await this.owned(name, create), backend = await this.backend();
108
+ if (!backend.project)
109
+ fail('configuration', 'Selected backend lacks project file operations.');
110
+ return this.result(op, name, p, await backend.project(op, { request_id: 'read_' + randomUUID(), ...p, root, project: name, profile: this.workspace.profile }));
111
+ }
112
+ async list() {
113
+ return this.ordered(async () => {
114
+ const items = [];
115
+ for (const name of (await readdir(await this.container())).sort()) {
116
+ const root = await this.owned(name);
117
+ try {
118
+ if ((await lstat(join(root, 'project.gpr'))).isFile())
119
+ items.push({ project: name });
120
+ }
121
+ catch (e) {
122
+ if (e.code !== 'ENOENT')
123
+ throw e;
124
+ }
125
+ }
126
+ return { items, count: items.length };
127
+ });
128
+ }
129
+ async read(op, name, p) {
130
+ return this.ordered(async () => {
131
+ const value = await this.native(op, name, p);
132
+ return op === 'info' ? { ...value, unresolvedRequests: this.workspace.unresolvedProject(name).map(request => ({ operation: request.operation, state: request.state, ...(request.callerId ? { requestId: request.callerId } : {}) })) } : value;
133
+ });
134
+ }
135
+ async recovered(request, op, name, p) {
136
+ const original = object(object(request.intent).parameters), expected = { ...original, request_id: request.id };
137
+ if (op === 'import' || op === 'restore')
138
+ expected.input = await this.workspace.artifacts.verify(original.input);
139
+ if (op === 'archive')
140
+ expected.output = join(this.workspace.directory, 'project-archives', request.id, 'project.gar');
141
+ const backend = await this.backend(), root = await this.owned(name), outcome = await backend.projectOutcome?.(request.id, { ...expected, project: name, root, profile: this.workspace.profile, operation: op });
142
+ if (!outcome)
143
+ return;
144
+ let value = this.result(op, name, p, outcome);
145
+ if (op === 'archive') {
146
+ const output = join(this.workspace.directory, 'project-archives', request.id, 'project.gar'), artifact = await this.workspace.artifacts.capture(output, 'input');
147
+ await this.exportArtifact(artifact, text(p.output, 4096), true);
148
+ value = { ...value, output: p.output, sha256: artifact.sha256 };
149
+ }
150
+ this.workspace.administrationState(request.id, 'applied', value);
151
+ return value;
152
+ }
153
+ async change(op, name, p, callerId) {
154
+ name = projectName(name);
155
+ let input;
156
+ if (op === 'import' || op === 'restore')
157
+ input = await this.workspace.artifacts.capture(text(p.input, 4096), 'input');
158
+ const intent = { project: name, ...p, ...(input ? { input } : {}) }, reserved = this.workspace.reserveAdministration('projects.' + op, intent, callerId);
159
+ return this.ordered(async () => {
160
+ const request = this.workspace.administration(reserved.request.id);
161
+ if (reserved.replay && request.state !== 'accepted') {
162
+ if (request.state === 'applied')
163
+ return request.result;
164
+ const value = await this.recovered(request, op, name, p);
165
+ if (value)
166
+ return value;
167
+ fail('unknown', 'Project outcome is retained; the operation is not repeated.', request.result);
168
+ }
169
+ for (const earlier of this.workspace.unresolvedProject(name))
170
+ if (earlier.id !== request.id && earlier.operation.startsWith('projects.')) {
171
+ const params = object(object(earlier.intent).parameters);
172
+ await this.recovered(earlier, earlier.operation.slice('projects.'.length), name, params);
173
+ }
174
+ if (this.workspace.unresolvedProject(name).some(item => item.id !== request.id))
175
+ fail('blocked', 'An earlier project operation is unresolved; preserve its project before continuing.');
176
+ const creating = op === 'create' || op === 'restore';
177
+ if (!creating)
178
+ await this.owned(name);
179
+ const nativeParameters = { ...p, request_id: request.id, ...(input ? { input: await this.workspace.artifacts.verify(input) } : {}) };
180
+ let output;
181
+ if (op === 'archive') {
182
+ await this.newOutput(text(p.output, 4096));
183
+ const stage = join(this.workspace.directory, 'project-archives', request.id);
184
+ await mkdir(stage, { recursive: true, mode: 0o700 });
185
+ output = join(stage, 'project.gar');
186
+ nativeParameters.output = output;
187
+ }
188
+ this.workspace.administrationState(request.id, 'running');
189
+ try {
190
+ const value = await this.native(op, name, nativeParameters, creating);
191
+ if (output) {
192
+ const artifact = await this.workspace.artifacts.capture(output, 'input');
193
+ await this.exportArtifact(artifact, text(p.output, 4096));
194
+ const result = { ...value, output: p.output, sha256: artifact.sha256 };
195
+ this.workspace.administrationState(request.id, 'applied', result);
196
+ return result;
197
+ }
198
+ this.workspace.administrationState(request.id, 'applied', value);
199
+ return value;
200
+ }
201
+ catch (error) {
202
+ this.workspace.administrationState(request.id, 'unknown', { project: name, operation: op, state: 'unknown' });
203
+ throw error;
204
+ }
205
+ });
206
+ }
207
+ async newOutput(output) { try {
208
+ await lstat(output);
209
+ }
210
+ catch (error) {
211
+ if (error.code === 'ENOENT')
212
+ return;
213
+ throw error;
214
+ } fail('exists', 'Output already exists; choose a new file.'); }
215
+ async exportArtifact(artifact, output, recover = false) {
216
+ const source = await this.workspace.artifacts.verify(artifact);
217
+ try {
218
+ await copyFile(source, output, constants.COPYFILE_EXCL);
219
+ }
220
+ catch (error) {
221
+ if (!recover || error.code !== 'EEXIST')
222
+ throw error;
223
+ }
224
+ const readback = await this.workspace.artifacts.capture(output, artifact.kind);
225
+ if (readback.id !== artifact.id)
226
+ fail('partial', 'Exported bytes differ from the retained snapshot.');
227
+ await chmod(output, 0o600);
228
+ const handle = await open(output, 'r+');
229
+ try {
230
+ await handle.sync();
231
+ }
232
+ finally {
233
+ await handle.close();
234
+ }
235
+ }
236
+ async export(selected, output, callerId) {
237
+ const reserved = this.workspace.reserveRequest(selected, 'ghidra.program.export', { output }, callerId);
238
+ return this.research.ordered(selected, async () => {
239
+ const request = this.workspace.request(reserved.request.id);
240
+ if (reserved.replay && request.state !== 'accepted') {
241
+ if (request.state === 'applied')
242
+ return request.result;
243
+ const execution = object(request.execution), snapshot = execution.snapshot;
244
+ await this.workspace.artifacts.verify(snapshot);
245
+ const observed = await this.workspace.artifacts.capture(output, 'snapshot'), current = this.workspace.context(selected);
246
+ if (observed.id !== snapshot.id || current.generation !== execution.generation)
247
+ fail('unknown', 'Export output cannot establish the retained outcome.');
248
+ const result = { ...selected, state: 'exported', output, sha256: snapshot.sha256, size: snapshot.size };
249
+ this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot }, result);
250
+ return result;
251
+ }
252
+ await this.newOutput(output);
253
+ const backend = await this.backend();
254
+ await backend.open(selected);
255
+ const snapshot = await backend.saved(selected), context = this.workspace.context(selected);
256
+ this.workspace.startRequest(request.id, { kind: 'export', generation: context.generation, snapshot, output });
257
+ try {
258
+ await this.exportArtifact(snapshot, output);
259
+ const result = { ...selected, state: 'exported', output, sha256: snapshot.sha256, size: snapshot.size };
260
+ this.workspace.completeSavedRequest(selected, request.id, context.generation, { workingSnapshot: snapshot }, result);
261
+ return result;
262
+ }
263
+ catch (error) {
264
+ this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown', savedSnapshot: snapshot });
265
+ throw error;
266
+ }
267
+ });
268
+ }
269
+ }
270
+ const projectOption = { flags: '--project <name>', description: 'Select a managed project in this profile', required: true }, requestOption = { flags: '--request-id <id>', description: 'Retrieve this outcome without repeating it' };
271
+ export function projectCommands(service) {
272
+ const leaves = [command('list', 'List owned projects', (_, c) => service(c).list(), { permission: Permission.ReadOnly })];
273
+ for (const [name, op, permission] of [['create', 'create', Permission.Update], ['open', 'open', Permission.Update], ['close', 'close', Permission.Update], ['delete', 'delete', 'Destructive'], ['info', 'info', Permission.ReadOnly],
274
+ ['files', 'files', Permission.ReadOnly], ['create-folder', 'create_folder', Permission.Update], ['move-file', 'move_file', Permission.Update], ['move-folder', 'move_folder', Permission.Update], ['delete-file', 'delete_file', 'Destructive'], ['archive', 'archive', Permission.Update], ['restore', 'restore', Permission.Update]]) {
275
+ const path = ['files', 'create_folder', 'move_file', 'move_folder', 'delete_file'].includes(op), moving = op.startsWith('move_'), changing = permission !== Permission.ReadOnly;
276
+ const options = [projectOption, ...(path ? [{ flags: '--path <path>', description: 'Absolute path inside the project', ...(op === 'files' ? { defaultValue: '/' } : { required: true }) }] : []),
277
+ ...(moving ? [{ flags: '--destination <folder>', description: 'Existing destination folder', required: true }] : []),
278
+ ...(op === 'files' ? [{ flags: '--limit <count>', description: 'Page size 1–100', defaultValue: 50 }, { flags: '--offset <count>', description: 'Page offset', defaultValue: 0 }] : []),
279
+ ...(op === 'archive' ? [{ flags: '--output <file>', description: 'Create a new Ghidra project archive', required: true }] : []),
280
+ ...(op === 'restore' ? [{ flags: '--input <file>', description: 'Restore into a new owned project', required: true }] : []), ...(changing ? [requestOption] : [])];
281
+ leaves.push(command(name, 'Manage project files without changing research contexts', ({ options: o }, c) => {
282
+ const p = {};
283
+ if (path)
284
+ p.path = repositoryPath(o.path);
285
+ if (moving)
286
+ p.destination = repositoryPath(o.destination);
287
+ if (op === 'files') {
288
+ p.limit = number(o.limit, 100, 1);
289
+ p.offset = number(o.offset, 2147483647);
290
+ }
291
+ for (const key of ['input', 'output'])
292
+ if (o[key] !== undefined)
293
+ p[key] = resolve(c.cwd, text(o[key], 4096));
294
+ return changing ? service(c).change(op, projectName(o.project), p, o.requestId === undefined ? undefined : id(o.requestId)) : service(c).read(op, projectName(o.project), p);
295
+ }, { permission, options }));
296
+ }
297
+ return [command('projects', 'Manage isolated Ghidra projects in the selected profile', leaves), command('program', 'Program files', [
298
+ command('export', 'Export the selected private program to a new GZF file', ({ options: o }, c) => service(c).export(scope(o), resolve(c.cwd, text(o.output, 4096)), o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [...selectedOptions, { flags: '--output <file>', description: 'New output file; existing files are preserved', required: true }, requestOption] }),
299
+ command('import', 'Import a GZF into a managed project', ({ options: o }, c) => service(c).change('import', projectName(o.project), { input: resolve(c.cwd, text(o.input, 4096)), path: repositoryPath(o.path) }, o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [projectOption, { flags: '--input <file>', description: 'Saved Ghidra program file', required: true }, { flags: '--path <path>', description: 'New file path inside the project', required: true }, requestOption] }),
300
+ ])];
301
+ }
@@ -0,0 +1,8 @@
1
+ import { type CliDefinition, type CliRuntime, type CommandContext } from '@eyeauras/cli-factory';
2
+ import { type Publisher } from './coordinator.js';
3
+ export interface Options {
4
+ runtime?: CliRuntime;
5
+ publisher?: (context: CommandContext) => Publisher;
6
+ }
7
+ export declare function createDefinition(options?: Options): CliDefinition;
8
+ export declare const createInsightCli: (options?: Options) => import("@eyeauras/cli-factory").CliApplication;