@eyeauras/insight-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cli-package-build.json +1 -0
- package/LICENSE +21 -0
- package/README.md +54 -2
- package/SKILL.md +55 -0
- package/dist/src/administration-results.d.ts +1 -0
- package/dist/src/administration-results.js +84 -0
- package/dist/src/administration.d.ts +14 -0
- package/dist/src/administration.js +120 -0
- package/dist/src/archive-contract.d.ts +6 -0
- package/dist/src/archive-contract.js +108 -0
- package/dist/src/archive.d.ts +12 -0
- package/dist/src/archive.js +116 -0
- package/dist/src/arguments.d.ts +14 -0
- package/dist/src/arguments.js +62 -0
- package/dist/src/artifacts.d.ts +17 -0
- package/dist/src/artifacts.js +115 -0
- package/dist/src/bin.d.ts +2 -0
- package/dist/src/bin.js +4 -0
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +387 -0
- package/dist/src/comment-effects.d.ts +3 -0
- package/dist/src/comment-effects.js +55 -0
- package/dist/src/contexts.d.ts +115 -0
- package/dist/src/contexts.js +1068 -0
- package/dist/src/coordinator.d.ts +41 -0
- package/dist/src/coordinator.js +160 -0
- package/dist/src/cross-results.d.ts +14 -0
- package/dist/src/cross-results.js +71 -0
- package/dist/src/cross.d.ts +3 -0
- package/dist/src/cross.js +43 -0
- package/dist/src/data-effects.d.ts +3 -0
- package/dist/src/data-effects.js +103 -0
- package/dist/src/debugger-results.d.ts +4 -0
- package/dist/src/debugger-results.js +151 -0
- package/dist/src/debugger-supervisor.d.ts +1 -0
- package/dist/src/debugger-supervisor.js +136 -0
- package/dist/src/debugger.d.ts +21 -0
- package/dist/src/debugger.js +267 -0
- package/dist/src/documentation-effects.d.ts +5 -0
- package/dist/src/documentation-effects.js +56 -0
- package/dist/src/extensions.d.ts +64 -0
- package/dist/src/extensions.js +289 -0
- package/dist/src/function-effects.d.ts +4 -0
- package/dist/src/function-effects.js +112 -0
- package/dist/src/ghidra.d.ts +50 -0
- package/dist/src/ghidra.js +468 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/initial-model.d.ts +6 -0
- package/dist/src/initial-model.js +85 -0
- package/dist/src/json.d.ts +2 -0
- package/dist/src/json.js +8 -0
- package/dist/src/lifecycle.d.ts +49 -0
- package/dist/src/lifecycle.js +148 -0
- package/dist/src/memory-inputs.d.ts +8 -0
- package/dist/src/memory-inputs.js +56 -0
- package/dist/src/merge.d.ts +29 -0
- package/dist/src/merge.js +66 -0
- package/dist/src/model.d.ts +74 -0
- package/dist/src/model.js +67 -0
- package/dist/src/mutation-effects.d.ts +4 -0
- package/dist/src/mutation-effects.js +321 -0
- package/dist/src/mutation-inputs.d.ts +2 -0
- package/dist/src/mutation-inputs.js +71 -0
- package/dist/src/mutations.d.ts +4 -0
- package/dist/src/mutations.js +53 -0
- package/dist/src/native-inputs.d.ts +28 -0
- package/dist/src/native-inputs.js +171 -0
- package/dist/src/native-queries.d.ts +2 -0
- package/dist/src/native-queries.js +29 -0
- package/dist/src/projects.d.ts +31 -0
- package/dist/src/projects.js +301 -0
- package/dist/src/prototype-cli.d.ts +8 -0
- package/dist/src/prototype-cli.js +155 -0
- package/dist/src/queries.d.ts +5 -0
- package/dist/src/queries.js +84 -0
- package/dist/src/query-policy.d.ts +6 -0
- package/dist/src/query-policy.js +72 -0
- package/dist/src/query-results.d.ts +2 -0
- package/dist/src/query-results.js +224 -0
- package/dist/src/research.d.ts +184 -0
- package/dist/src/research.js +808 -0
- package/dist/src/saved-materials.d.ts +12 -0
- package/dist/src/saved-materials.js +196 -0
- package/dist/src/scripts.d.ts +19 -0
- package/dist/src/scripts.js +79 -0
- package/dist/src/sdk-contracts.d.ts +13 -0
- package/dist/src/sdk-contracts.js +37 -0
- package/dist/src/sdk-files.d.ts +29 -0
- package/dist/src/sdk-files.js +249 -0
- package/dist/src/sdk.d.ts +4 -0
- package/dist/src/sdk.js +125 -0
- package/dist/src/server.d.ts +44 -0
- package/dist/src/server.js +455 -0
- package/dist/src/store.d.ts +33 -0
- package/dist/src/store.js +178 -0
- package/dist/src/sync-policy.d.ts +22 -0
- package/dist/src/sync-policy.js +123 -0
- package/dist/src/transport.d.ts +23 -0
- package/dist/src/transport.js +135 -0
- package/dist/src/ui-results.d.ts +2 -0
- package/dist/src/ui-results.js +80 -0
- package/dist/src/ui.d.ts +17 -0
- package/dist/src/ui.js +88 -0
- package/dist/src/utility.d.ts +14 -0
- package/dist/src/utility.js +43 -0
- package/dist/src/version-tracking.d.ts +57 -0
- package/dist/src/version-tracking.js +200 -0
- package/dist/src/workspace.d.ts +199 -0
- package/dist/src/workspace.js +564 -0
- package/package.json +45 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +33 -0
- package/references/archive.md +32 -0
- package/references/artifacts.md +47 -0
- package/references/debugger.md +40 -0
- package/references/extensions-and-sync.md +124 -0
- package/references/mutations.json +1424 -0
- package/references/native/InsightApplyDocumentation.java +48 -0
- package/references/native/InsightArchivePayloads.java +36 -0
- package/references/native/InsightBlockTransfer.java +57 -0
- package/references/native/InsightEmulation.java +93 -0
- package/references/native/InsightExtensions.java +36 -0
- package/references/native/InsightFunctionDatabases.java +98 -0
- package/references/native/InsightGuiMain.java +32 -0
- package/references/native/InsightHeadlessMain.java +10 -0
- package/references/native/InsightInitialImport.java +42 -0
- package/references/native/InsightInitialJournal.java +48 -0
- package/references/native/InsightInitialPublication.java +142 -0
- package/references/native/InsightKeytool.java +16 -0
- package/references/native/InsightNativeReceipt.java +31 -0
- package/references/native/InsightPlugins.java +34 -0
- package/references/native/InsightProjectFiles.java +92 -0
- package/references/native/InsightPublicationRecovery.java +63 -0
- package/references/native/InsightPythonWheels.java +31 -0
- package/references/native/InsightResearchQueries.java +188 -0
- package/references/native/InsightSdkFiles.java +142 -0
- package/references/native/InsightSdkScript.java +31 -0
- package/references/native/InsightSdkTools.java +338 -0
- package/references/native/InsightSdkTypes.java +132 -0
- package/references/native/InsightServerAdministration.java +82 -0
- package/references/native/InsightServerInit.java +62 -0
- package/references/native/InsightServerMain.java +43 -0
- package/references/native/InsightServerProbe.java +29 -0
- package/references/native/InsightSnapshotFields.java +68 -0
- package/references/native/InsightSnapshotInspect.java +189 -0
- package/references/native/InsightSnapshotPublisher.java +123 -0
- package/references/native/InsightStartDebugger.java +28 -0
- package/references/native/InsightStopDebugger.java +20 -0
- package/references/native/InsightTraceInspect.java +24 -0
- package/references/native/InsightTraceState.java +130 -0
- package/references/native/InsightTraceTools.java +114 -0
- package/references/native/InsightVersionTracking.java +91 -0
- package/references/native-workflows.md +112 -0
- package/references/queries.json +1761 -0
- package/references/query-semantics.md +96 -0
- package/references/recovery.md +53 -0
- package/references/research.md +101 -0
- package/references/setup.md +47 -0
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-LICENSE +202 -0
- package/runtime/GhidraMCP-NOTICE +10 -0
- package/runtime/provenance.json +11 -0
- package/skills/insight-re/SKILL.md +108 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":1,"artifact":"c3714e4dd4db303fdccc352327ce7c7f5dff074726238298a6953be70aca1db3"}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 insight-cli contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# insight-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI/API for reverse engineering with durable sessions, isolated Ghidra contexts,
|
|
4
|
+
explicit merge/refresh and ordinary saved files. Agents choose the investigation.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
Use Node.js 24 or newer:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install --global @eyeauras/insight-cli
|
|
12
|
+
insight-cli --help
|
|
13
|
+
insight-cli skill insight-re
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The executable is `insight-cli`. Its global npm executable directory must be on PATH.
|
|
17
|
+
No source checkout, TypeScript build or separate Factory installation is required.
|
|
18
|
+
The pinned Factory IPC runtime is a normal npm dependency fetched from an immutable
|
|
19
|
+
GitHub tarball URL; installation therefore needs npm registry and GitHub access.
|
|
20
|
+
Ghidra 12.1.2 and Java 21 are external dependencies for Ghidra commands.
|
|
21
|
+
Help, skill discovery and saving ordinary files do not need Ghidra or Java.
|
|
22
|
+
|
|
23
|
+
## Start
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
insight-cli profile create local --java /path/to/java --ghidra-home /path/to/ghidra
|
|
27
|
+
insight-cli --profile local permissions grant Update
|
|
28
|
+
insight-cli --profile local ghidra open ./client.exe --session new --json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Retain the returned `session` and `target` and supply both on research calls:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
insight-cli --profile local ghidra strings search --session <session> --target <target> --contains config.json --json
|
|
35
|
+
insight-cli --profile local ghidra functions decompile --session <session> --target <target> --address <entry> --json
|
|
36
|
+
insight-cli --profile local ghidra merge --session <session> --target <target> --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Another agent uses `--session new` for its own context on the same input. It receives
|
|
40
|
+
published changes with `ghidra refresh`; local work and conflicts remain explicit.
|
|
41
|
+
Managed local startup needs no separately configured Ghidra Server.
|
|
42
|
+
|
|
43
|
+
Save a completed project file or folder when a retained checkpoint is useful:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
insight-cli --profile local artifacts save ./analysis --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The response returns an ordinary saved path and digest. Follow existing project RE
|
|
50
|
+
rules, formats and evidence locations; the packaged RE skill fills unspecified gaps.
|
|
51
|
+
Installation does not register skills or change agent settings. Use `insight-cli skill`
|
|
52
|
+
and `insight-cli skill insight-re` to locate packaged instructions and references.
|
|
53
|
+
|
|
54
|
+
Full documentation and verification: [GitHub](https://github.com/iXab3r/insight-cli).
|
|
55
|
+
License: MIT. The bundled GhidraMCP bridge has its own Apache-2.0 license and notice.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: insight-cli
|
|
3
|
+
description: Research several executable inputs through durable independent sessions and isolated Ghidra contexts using the matching insight-cli facade. Agents supply the investigation and interpret results.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# insight-cli 0.1.0
|
|
7
|
+
|
|
8
|
+
This skill and its executable helper are one npm package. Run `insight-cli skill` to locate
|
|
9
|
+
these installed resources. Use generated `--help` for syntax; read the references as needed.
|
|
10
|
+
Follow applicable project RE rules for tools, source of truth, evidence layout and format;
|
|
11
|
+
this package's walkthroughs fill gaps rather than replacing those conventions.
|
|
12
|
+
|
|
13
|
+
For coordinating a reverse-engineering investigation with ordinary files, external tools
|
|
14
|
+
and multiple agents, read the packaged [Insight RE skill](skills/insight-re/SKILL.md),
|
|
15
|
+
also located by `insight-cli skill insight-re`. When a retained checkpoint is useful, save
|
|
16
|
+
a completed project file or folder with
|
|
17
|
+
`insight-cli artifacts save <path>`; [save semantics](references/artifacts.md) describe the
|
|
18
|
+
ordinary saved copy and its receipt. Neither operation requires a Ghidra installation.
|
|
19
|
+
|
|
20
|
+
1. Select the configured runtime profile. Ordinary work uses a managed local server; an external
|
|
21
|
+
server is optional. See [setup](references/setup.md). Factory permissions authorize changes.
|
|
22
|
+
2. Open the input with `ghidra open <input> --session new`. Retain the returned session and target.
|
|
23
|
+
Another agent chooses `new` independently; one session can open several inputs.
|
|
24
|
+
3. Pass both `--session` and `--target` on research calls. Follow strings to xrefs and their
|
|
25
|
+
function addresses, then inspect/decompile. See [research](references/research.md).
|
|
26
|
+
4. Rename functions or set/clear comments through ordinary commands. An `applied` result means
|
|
27
|
+
verified saved private work. Other sessions keep their own views; local edits remain unpublished in the default manual mode.
|
|
28
|
+
5. By default, publish explicitly with `ghidra merge`; receive safe shared changes with `ghidra refresh`.
|
|
29
|
+
Resolve overlapping names/comments against an explicitly observed shared value. A conflict
|
|
30
|
+
prevents the whole selected merge and retains local work.
|
|
31
|
+
6. Continue with `ghidra open --session <id> --target <id>`. For uncertain outcomes inspect
|
|
32
|
+
`sessions status <id> --target <id>` and [recovery](references/recovery.md). Supplying a stable
|
|
33
|
+
optional `--request-id` lets a repeated changing intent retrieve its outcome without rerunning.
|
|
34
|
+
|
|
35
|
+
The facade exposes the pinned static research/editing catalog, merge/refresh, project/server/GUI
|
|
36
|
+
controls, debugger, archive exchange and supported recovery. Preserve private work
|
|
37
|
+
and journals while an outcome is unresolved. The CLI supplies the investigation tools; agents
|
|
38
|
+
choose research goals and interpret findings.
|
|
39
|
+
Installing this package does not register this skill in an agent's configuration.
|
|
40
|
+
|
|
41
|
+
For project files, program export/import and configured-server administration, read
|
|
42
|
+
[administration](references/administration.md). These operations preserve separate research views;
|
|
43
|
+
their external effects do not participate in program merge.
|
|
44
|
+
|
|
45
|
+
For owned GUI execution, CPU state, breakpoints and retained traces, read
|
|
46
|
+
[debugger](references/debugger.md). Execution side effects remain separate from program publication.
|
|
47
|
+
|
|
48
|
+
For explicit external documentation exchange and retained submission outcomes, read
|
|
49
|
+
[archive](references/archive.md).
|
|
50
|
+
|
|
51
|
+
For selected native SDK extensions (VT, DB differences, byte/assembly edits, formats, GDT,
|
|
52
|
+
FID/BSim, namespaces and typed IR), read [native workflows](references/native-workflows.md).
|
|
53
|
+
|
|
54
|
+
For managed SDK extension modules, actual language/analyzer/plugin discovery and opt-in
|
|
55
|
+
automatic synchronization, read [extensions and sync](references/extensions-and-sync.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function administrationResult(operation: string, p: Record<string, unknown>, raw: unknown): Record<string, unknown>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { object, fail } from './model.js';
|
|
2
|
+
const integer = (value, min = 0) => { if (!Number.isSafeInteger(value) || Number(value) < min)
|
|
3
|
+
fail('partial', 'Administration returned an invalid integer.'); };
|
|
4
|
+
const names = (value) => { if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || !item))
|
|
5
|
+
fail('partial', 'Administration name inventory is absent.'); return value; };
|
|
6
|
+
const array = (value) => { if (!Array.isArray(value))
|
|
7
|
+
fail('partial', 'Administration item inventory is absent.'); return value.map(object); };
|
|
8
|
+
const file = (value) => { const row = object(value); for (const field of ['name', 'path', 'type'])
|
|
9
|
+
if (typeof row[field] !== 'string')
|
|
10
|
+
fail('partial', 'Repository file identity is incomplete.'); integer(row.version, 1); return row; };
|
|
11
|
+
export function administrationResult(operation, p, raw) {
|
|
12
|
+
const value = object(raw);
|
|
13
|
+
if (JSON.stringify(value).length > 1048576)
|
|
14
|
+
fail('limit', 'Administration result exceeds one MiB.');
|
|
15
|
+
if (['status', 'connect', 'disconnect'].includes(operation)) {
|
|
16
|
+
if (typeof value.connected !== 'boolean' || !['local', 'external'].includes(String(value.mode)))
|
|
17
|
+
fail('partial', 'Server status is incomplete.');
|
|
18
|
+
if (value.connected && typeof value.repository !== 'string')
|
|
19
|
+
fail('partial', 'Connected server omitted its repository.');
|
|
20
|
+
}
|
|
21
|
+
else if (operation === 'repositories') {
|
|
22
|
+
const values = names(value.repositories);
|
|
23
|
+
integer(value.count);
|
|
24
|
+
if (value.count !== values.length)
|
|
25
|
+
fail('partial', 'Repository count is inconsistent.');
|
|
26
|
+
}
|
|
27
|
+
else if (operation === 'create_repository') {
|
|
28
|
+
if (value.state !== 'created' || value.repository !== p.repository)
|
|
29
|
+
fail('partial', 'Created repository identity changed.');
|
|
30
|
+
}
|
|
31
|
+
else if (operation === 'users') {
|
|
32
|
+
const users = array(value.users);
|
|
33
|
+
integer(value.count);
|
|
34
|
+
if (users.length !== value.count || users.some(user => typeof user.name !== 'string'))
|
|
35
|
+
fail('partial', 'Server user count is inconsistent.');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (value.repository !== p.repository)
|
|
39
|
+
fail('binding', 'Administration returned another repository.');
|
|
40
|
+
if (operation === 'files') {
|
|
41
|
+
names(value.folders);
|
|
42
|
+
array(value.files).forEach(file);
|
|
43
|
+
if (value.path !== p.path)
|
|
44
|
+
fail('binding', 'Repository folder changed.');
|
|
45
|
+
}
|
|
46
|
+
else if (operation === 'file') {
|
|
47
|
+
const row = file(value.file);
|
|
48
|
+
if (row.path !== p.path)
|
|
49
|
+
fail('binding', 'Repository file path changed.');
|
|
50
|
+
}
|
|
51
|
+
else if (operation === 'permissions') {
|
|
52
|
+
array(value.users).forEach(user => { if (typeof user.name !== 'string')
|
|
53
|
+
fail('partial', 'ACL username is absent.'); integer(user.access); if (Number(user.access) > 2)
|
|
54
|
+
fail('partial', 'Invalid ACL level.'); });
|
|
55
|
+
if (typeof value.anonymous !== 'boolean')
|
|
56
|
+
fail('partial', 'ACL anonymous flag is absent.');
|
|
57
|
+
}
|
|
58
|
+
else if (operation === 'set_permissions') {
|
|
59
|
+
const users = object(value.users);
|
|
60
|
+
if (value.state !== 'applied' || users[String(p.user)] !== p.access || typeof value.anonymous !== 'boolean')
|
|
61
|
+
fail('partial', 'Permission update lacks exact readback.');
|
|
62
|
+
}
|
|
63
|
+
else if (operation === 'history') {
|
|
64
|
+
if (value.path !== p.path)
|
|
65
|
+
fail('binding', 'History selected another file.');
|
|
66
|
+
array(value.versions).forEach(version => { integer(version.version, 1); if (typeof version.user !== 'string' || typeof version.comment !== 'string')
|
|
67
|
+
fail('partial', 'Version history is incomplete.'); });
|
|
68
|
+
}
|
|
69
|
+
else if (operation === 'checkouts') {
|
|
70
|
+
if (value.path !== p.path)
|
|
71
|
+
fail('binding', 'Checkout selection changed.');
|
|
72
|
+
array(value.checkouts).forEach(item => { if (typeof item.id !== 'string' || !/^\d+$/.test(item.id) || typeof item.path !== 'string' || typeof item.user !== 'string')
|
|
73
|
+
fail('partial', 'Checkout identity is incomplete.'); integer(item.version); });
|
|
74
|
+
}
|
|
75
|
+
else if (operation === 'terminate_checkout' || operation === 'terminate_all_checkouts') {
|
|
76
|
+
if (value.path !== p.path)
|
|
77
|
+
fail('binding', 'Termination selected another path.');
|
|
78
|
+
integer(value.terminated);
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
fail('schema', 'Administration operation lacks a result contract.');
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import type { ResearchBackend } from './research.js';
|
|
3
|
+
import { Workspace } from './workspace.js';
|
|
4
|
+
export declare function repositoryPath(value: unknown): string;
|
|
5
|
+
export declare class Administration {
|
|
6
|
+
readonly workspace: Workspace;
|
|
7
|
+
private readonly backend;
|
|
8
|
+
private readonly tasks;
|
|
9
|
+
constructor(workspace: Workspace, backend: () => Promise<ResearchBackend>, tasks: Map<string, Promise<unknown>>);
|
|
10
|
+
private ordered;
|
|
11
|
+
read(operation: string, parameters: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
12
|
+
change(operation: string, parameters: Record<string, unknown>, callerId?: string): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
export declare function administrationCommands(service: (context: CommandContext) => Administration): CommandDefinition[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { fail, id, text } from './model.js';
|
|
3
|
+
import { option, parsed, key } from './arguments.js';
|
|
4
|
+
import { administrationResult } from './administration-results.js';
|
|
5
|
+
const repository = (value) => { const name = text(value, 128); if (name !== name.trim() || /[\\/\0]/.test(name) || name.startsWith('.'))
|
|
6
|
+
fail('invalid', 'Repository name must be a single bounded name.'); return name; };
|
|
7
|
+
export function repositoryPath(value) {
|
|
8
|
+
const path = text(value, 4096);
|
|
9
|
+
if (!path.startsWith('/') || path.includes('\\') || path.split('/').some(part => part === '.' || part === '..') || path.includes('//'))
|
|
10
|
+
fail('invalid', 'Use an absolute repository path without traversal.');
|
|
11
|
+
return path.length > 1 ? path.replace(/\/$/, '') : path;
|
|
12
|
+
}
|
|
13
|
+
export class Administration {
|
|
14
|
+
workspace;
|
|
15
|
+
backend;
|
|
16
|
+
tasks;
|
|
17
|
+
constructor(workspace, backend, tasks) {
|
|
18
|
+
this.workspace = workspace;
|
|
19
|
+
this.backend = backend;
|
|
20
|
+
this.tasks = tasks;
|
|
21
|
+
}
|
|
22
|
+
async ordered(action) {
|
|
23
|
+
const key = 'administration', previous = this.tasks.get(key), task = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
|
|
24
|
+
this.tasks.set(key, task);
|
|
25
|
+
try {
|
|
26
|
+
return await task;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (this.tasks.get(key) === task)
|
|
30
|
+
this.tasks.delete(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async read(operation, parameters) {
|
|
34
|
+
return this.ordered(async () => {
|
|
35
|
+
const backend = await this.backend();
|
|
36
|
+
if (!backend.administration)
|
|
37
|
+
fail('configuration', 'Selected backend has no repository administration adapter.');
|
|
38
|
+
const value = administrationResult(operation, parameters, await backend.administration(operation, parameters));
|
|
39
|
+
return operation === 'status' ? { ...value, unresolvedRequests: this.workspace.unresolvedServer().map(request => ({ operation: request.operation, state: request.state, ...(request.callerId ? { requestId: request.callerId } : {}) })) } : value;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async change(operation, parameters, callerId) {
|
|
43
|
+
const reserved = this.workspace.reserveAdministration('server.' + operation, parameters, callerId);
|
|
44
|
+
return this.ordered(async () => {
|
|
45
|
+
const request = this.workspace.administration(reserved.request.id);
|
|
46
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
47
|
+
if (request.state === 'applied')
|
|
48
|
+
return request.result;
|
|
49
|
+
fail('unknown', 'Administration outcome is retained and is not re-executed.', request.result);
|
|
50
|
+
}
|
|
51
|
+
const backend = await this.backend();
|
|
52
|
+
if (!backend.administration)
|
|
53
|
+
fail('configuration', 'Selected backend has no repository administration adapter.');
|
|
54
|
+
this.workspace.administrationState(request.id, 'running');
|
|
55
|
+
try {
|
|
56
|
+
const value = administrationResult(operation, parameters, await backend.administration(operation, parameters));
|
|
57
|
+
this.workspace.administrationState(request.id, 'applied', value);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.workspace.administrationState(request.id, 'unknown', { state: 'unknown', operation });
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const repo = { name: 'repository', type: 'string', required: true }, path = { name: 'path', type: 'string', required: false, default: '/' };
|
|
68
|
+
const leaves = [
|
|
69
|
+
{ names: ['status'], operation: 'status', gate: Permission.ReadOnly, params: [] },
|
|
70
|
+
{ names: ['connect'], operation: 'connect', gate: Permission.Update, params: [] },
|
|
71
|
+
{ names: ['disconnect'], operation: 'disconnect', gate: Permission.Update, params: [] },
|
|
72
|
+
{ names: ['repositories', 'list'], operation: 'repositories', gate: Permission.ReadOnly, params: [] },
|
|
73
|
+
{ names: ['repositories', 'create'], operation: 'create_repository', gate: Permission.Update, params: [repo] },
|
|
74
|
+
{ names: ['repositories', 'files'], operation: 'files', gate: Permission.ReadOnly, params: [repo, path] },
|
|
75
|
+
{ names: ['repositories', 'file'], operation: 'file', gate: Permission.ReadOnly, params: [repo, { ...path, required: true, default: undefined }] },
|
|
76
|
+
{ names: ['history'], operation: 'history', gate: Permission.ReadOnly, params: [repo, { ...path, required: true, default: undefined }] },
|
|
77
|
+
{ names: ['checkouts'], operation: 'checkouts', gate: Permission.ReadOnly, params: [repo, path] },
|
|
78
|
+
{ names: ['admin', 'users'], operation: 'users', gate: Permission.ReadOnly, params: [] },
|
|
79
|
+
{ names: ['admin', 'permissions'], operation: 'permissions', gate: Permission.ReadOnly, params: [repo] },
|
|
80
|
+
{ names: ['admin', 'set-permissions'], operation: 'set_permissions', gate: 'Destructive', params: [repo, { name: 'user', type: 'string', required: true }, { name: 'access', type: 'string', required: true }] },
|
|
81
|
+
{ names: ['admin', 'terminate-checkout'], operation: 'terminate_checkout', gate: 'Destructive', params: [repo, { ...path, required: true, default: undefined }, { name: 'checkout_id', type: 'string', required: false }] },
|
|
82
|
+
{ names: ['admin', 'terminate-all-checkouts'], operation: 'terminate_all_checkouts', gate: 'Destructive', params: [repo, path] },
|
|
83
|
+
];
|
|
84
|
+
export function administrationCommands(service) {
|
|
85
|
+
const children = [];
|
|
86
|
+
for (const leaf of leaves) {
|
|
87
|
+
const changing = leaf.gate !== Permission.ReadOnly;
|
|
88
|
+
const definition = command(leaf.names.at(-1), 'Operate on the configured server and explicit repository', ({ options: o }, c) => {
|
|
89
|
+
const parameters = {};
|
|
90
|
+
for (const p of leaf.params) {
|
|
91
|
+
const value = o[key(p.name)];
|
|
92
|
+
if (value !== undefined)
|
|
93
|
+
parameters[p.name] = parsed(p, value);
|
|
94
|
+
}
|
|
95
|
+
if (parameters.repository !== undefined)
|
|
96
|
+
parameters.repository = repository(parameters.repository);
|
|
97
|
+
if (parameters.path !== undefined)
|
|
98
|
+
parameters.path = repositoryPath(parameters.path);
|
|
99
|
+
if (parameters.checkout_id !== undefined && !/^\d+$/.test(String(parameters.checkout_id)))
|
|
100
|
+
fail('invalid', 'Checkout ID must be an exact decimal integer.');
|
|
101
|
+
if (parameters.access !== undefined) {
|
|
102
|
+
const level = String(parameters.access).toLowerCase().replaceAll('-', '_'), access = { read_only: 0, readonly: 0, read: 0, write: 1, admin: 2, '0': 0, '1': 1, '2': 2 }[level];
|
|
103
|
+
if (access === undefined)
|
|
104
|
+
fail('invalid', 'Access must be read_only, write or admin.');
|
|
105
|
+
parameters.access = access;
|
|
106
|
+
}
|
|
107
|
+
return changing ? service(c).change(leaf.operation, parameters, o.requestId === undefined ? undefined : id(o.requestId)) : service(c).read(leaf.operation, parameters);
|
|
108
|
+
}, { permission: leaf.gate, options: [...leaf.params.map(option), ...(changing ? [{ flags: '--request-id <id>', description: 'Retrieve this administration outcome after a lost response' }] : [])] });
|
|
109
|
+
if (leaf.names.length === 1)
|
|
110
|
+
children.push(definition);
|
|
111
|
+
else {
|
|
112
|
+
const group = children.find(item => item.name === leaf.names[0]);
|
|
113
|
+
if (group)
|
|
114
|
+
group.children.push(definition);
|
|
115
|
+
else
|
|
116
|
+
children.push(command(leaf.names[0], `${leaf.names[0]} administration`, [definition]));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [command('server', 'Configured server management', children)];
|
|
120
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function archiveOrigin(value: unknown): string;
|
|
2
|
+
export declare function archivePayload(raw: unknown, binary: string, version: string, run: string): Record<string, unknown>;
|
|
3
|
+
export declare function archivePost(fetch: typeof globalThis.fetch, origin: string, payload: Record<string, unknown>, token?: string, signal?: AbortSignal): Promise<{
|
|
4
|
+
created: boolean;
|
|
5
|
+
conflicts_enqueued: number;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { object, text, fail } from './model.js';
|
|
2
|
+
import { address } from './research.js';
|
|
3
|
+
import { parseDomainJson } from './json.js';
|
|
4
|
+
export function archiveOrigin(value) {
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
url = new URL(text(value, 2048));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
fail('configuration', 'Configure an explicit archive URL.');
|
|
11
|
+
}
|
|
12
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))
|
|
13
|
+
fail('configuration', 'Archive URL must use HTTPS or local HTTP, without credentials, query or fragment.');
|
|
14
|
+
return url.href.replace(/\/$/, '');
|
|
15
|
+
}
|
|
16
|
+
const integer = (value) => { if (!Number.isSafeInteger(value) || Number(value) < 0)
|
|
17
|
+
fail('partial', 'Archive count is invalid.'); return Number(value); };
|
|
18
|
+
export function archivePayload(raw, binary, version, run) {
|
|
19
|
+
const p = object(raw);
|
|
20
|
+
if (p.binary_name !== binary || p.version !== version || p.source_run_id !== run || p.source_tool !== 'ghidra_mcp_ingest')
|
|
21
|
+
fail('binding', 'Archive payload selected another input or request.');
|
|
22
|
+
address(p.address);
|
|
23
|
+
text(p.name);
|
|
24
|
+
text(p.prototype, 65536);
|
|
25
|
+
if (typeof p.no_return !== 'boolean' || typeof p.is_thunk !== 'boolean')
|
|
26
|
+
fail('partial', 'Archive function metadata is incomplete.');
|
|
27
|
+
for (const key of ['function_size_bytes', 'instruction_count', 'basic_block_count'])
|
|
28
|
+
if (p[key] !== undefined)
|
|
29
|
+
integer(p[key]);
|
|
30
|
+
if (p.opcode_hash !== undefined && (typeof p.opcode_hash !== 'string' || !/^[a-f0-9]{64}$/.test(p.opcode_hash)))
|
|
31
|
+
fail('partial', 'Archive opcode hash is malformed.');
|
|
32
|
+
for (const key of ['plate_comment', 'calling_convention'])
|
|
33
|
+
if (p[key] !== undefined)
|
|
34
|
+
text(p[key], 65536);
|
|
35
|
+
if (p.function_tags !== undefined) {
|
|
36
|
+
if (!Array.isArray(p.function_tags) || p.function_tags.length > 10000)
|
|
37
|
+
fail('partial', 'Archive tags are malformed.');
|
|
38
|
+
for (const tag of p.function_tags)
|
|
39
|
+
text(tag);
|
|
40
|
+
}
|
|
41
|
+
for (const key of ['locals', 'instruction_comments'])
|
|
42
|
+
if (p[key] !== undefined) {
|
|
43
|
+
if (!Array.isArray(p[key]) || p[key].length > 10000)
|
|
44
|
+
fail('partial', 'Archive documentation is malformed.');
|
|
45
|
+
for (const rawRow of p[key]) {
|
|
46
|
+
const row = object(rawRow);
|
|
47
|
+
if (key === 'locals') {
|
|
48
|
+
text(row.name);
|
|
49
|
+
text(row.data_type);
|
|
50
|
+
text(row.storage);
|
|
51
|
+
text(row.source_type);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (!Number.isSafeInteger(row.address_offset) || !['EOL', 'PRE', 'POST', 'REPEATABLE'].includes(String(row.type)))
|
|
55
|
+
fail('partial', 'Archive instruction comment is malformed.');
|
|
56
|
+
text(row.text, 65536);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (Buffer.byteLength(JSON.stringify(p)) > 262144)
|
|
61
|
+
fail('limit', 'Archive function payload exceeds 256 KiB.');
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
export async function archivePost(fetch, origin, payload, token, signal) {
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await fetch(archiveOrigin(origin) + '/v1/doc_archive/upsert', { method: 'POST', redirect: 'error', signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000),
|
|
68
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}) }, body: JSON.stringify(payload) });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
fail('unknown', 'Archive response was lost; the request is retained and is not repeated.');
|
|
72
|
+
}
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
await response.body?.cancel();
|
|
75
|
+
fail('unknown', 'Archive rejected the request; its external outcome is retained.');
|
|
76
|
+
}
|
|
77
|
+
const reader = response.body?.getReader();
|
|
78
|
+
if (!reader)
|
|
79
|
+
fail('partial', 'Archive response is empty.');
|
|
80
|
+
const parts = [];
|
|
81
|
+
let size = 0;
|
|
82
|
+
try {
|
|
83
|
+
for (;;) {
|
|
84
|
+
const { value, done } = await reader.read();
|
|
85
|
+
if (done)
|
|
86
|
+
break;
|
|
87
|
+
size += value.byteLength;
|
|
88
|
+
if (size > 65536)
|
|
89
|
+
fail('limit', 'Archive response exceeds 64 KiB.');
|
|
90
|
+
parts.push(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await reader.cancel().catch(() => { });
|
|
95
|
+
}
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = parseDomainJson(Buffer.concat(parts).toString('utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
fail('partial', 'Archive response is not JSON.');
|
|
102
|
+
}
|
|
103
|
+
const result = object(raw);
|
|
104
|
+
if (typeof result.created !== 'boolean')
|
|
105
|
+
fail('partial', 'Archive response omits its created flag.');
|
|
106
|
+
const conflicts = integer(result.conflicts_enqueued);
|
|
107
|
+
return { created: result.created, conflicts_enqueued: conflicts };
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Workspace, type Scope } from './workspace.js';
|
|
3
|
+
import { Research, type ResearchBackend } from './research.js';
|
|
4
|
+
export declare class Archive {
|
|
5
|
+
private readonly workspace;
|
|
6
|
+
private readonly research;
|
|
7
|
+
private readonly backend;
|
|
8
|
+
private readonly c;
|
|
9
|
+
constructor(workspace: Workspace, research: Research, backend: () => Promise<ResearchBackend>, c: CommandContext);
|
|
10
|
+
ingest(selected: Scope, operation: 'function' | 'program', p: Record<string, unknown>, callerId?: string): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare function archiveCommands(service: (c: CommandContext) => Archive): CommandDefinition[];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { scope, address, number } from './research.js';
|
|
3
|
+
import { id, text, object, fail, digest } from './model.js';
|
|
4
|
+
import { selectedOptions } from './arguments.js';
|
|
5
|
+
import { archiveOrigin, archivePayload, archivePost } from './archive-contract.js';
|
|
6
|
+
export class Archive {
|
|
7
|
+
workspace;
|
|
8
|
+
research;
|
|
9
|
+
backend;
|
|
10
|
+
c;
|
|
11
|
+
constructor(workspace, research, backend, c) {
|
|
12
|
+
this.workspace = workspace;
|
|
13
|
+
this.research = research;
|
|
14
|
+
this.backend = backend;
|
|
15
|
+
this.c = c;
|
|
16
|
+
}
|
|
17
|
+
async ingest(selected, operation, p, callerId) {
|
|
18
|
+
const origin = p.dry_run ? undefined : archiveOrigin(this.c.profile.values.archiveUrl);
|
|
19
|
+
const reserved = this.workspace.reserveAdministration('ghidra.archive.' + operation, { ...selected, ...p, ...(origin ? { origin } : {}) }, callerId);
|
|
20
|
+
return this.research.ordered(selected, async () => {
|
|
21
|
+
const request = this.workspace.administration(reserved.request.id);
|
|
22
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
23
|
+
if (request.state === 'applied')
|
|
24
|
+
return request.result;
|
|
25
|
+
fail('unknown', 'Archive outcome is retained and no external requests are repeated.', request.result);
|
|
26
|
+
}
|
|
27
|
+
const backend = await this.backend();
|
|
28
|
+
await backend.open(selected);
|
|
29
|
+
if (!backend.archivePayloads)
|
|
30
|
+
fail('configuration', 'Selected backend lacks archive payload support.');
|
|
31
|
+
const snapshot = await backend.saved(selected), binary = this.workspace.target(selected.target).name, run = 'insight-cli-' + request.id;
|
|
32
|
+
const token = origin ? await this.c.secrets.get('archiveToken') : undefined;
|
|
33
|
+
const result = { ...selected, state: 'running', binary_name: binary, version: String(p.version), source_run_id: run, dry_run: Boolean(p.dry_run),
|
|
34
|
+
total_functions: 0, created: 0, updated: 0, conflicts_enqueued: 0, skipped_default_named: 0, errors: 0, truncated: false, first_errors: [] };
|
|
35
|
+
this.workspace.administrationState(request.id, 'running', { ...result, sourceSnapshot: snapshot });
|
|
36
|
+
let offset = 0, more = true;
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
try {
|
|
39
|
+
const maximum = operation === 'function' ? 1 : Number(p.limit) || 100000;
|
|
40
|
+
while (more && offset < maximum) {
|
|
41
|
+
const page = object(await backend.archivePayloads(selected, { ...(operation === 'function' ? { function: p.function } : {}), binary_name: binary, version: p.version, run_id: run,
|
|
42
|
+
skip_default_named: operation === 'program' && p.skip_default_named === true, offset, limit: Math.min(10, maximum - offset) }));
|
|
43
|
+
if (!Array.isArray(page.payloads) || !Number.isSafeInteger(page.visited) || Number(page.visited) < 0 || Number(page.visited) > Math.min(10, maximum - offset) || !Number.isSafeInteger(page.skipped) || Number(page.skipped) < 0 || page.payloads.length + Number(page.skipped) !== page.visited || typeof page.more !== 'boolean' || page.more && page.visited === 0)
|
|
44
|
+
fail('partial', 'Archive payload inventory has inconsistent counts.');
|
|
45
|
+
const payloads = page.payloads.map(raw => archivePayload(raw, binary, String(p.version), run));
|
|
46
|
+
for (const payload of payloads) {
|
|
47
|
+
const at = address(payload.address);
|
|
48
|
+
if (seen.has(at))
|
|
49
|
+
fail('partial', 'Archive inventory repeated a function.');
|
|
50
|
+
seen.add(at);
|
|
51
|
+
}
|
|
52
|
+
if (operation === 'function' && (payloads.length !== 1 || address(payloads[0].address) !== address(p.function)))
|
|
53
|
+
fail('binding', 'Archive selected another function.');
|
|
54
|
+
result.skipped_default_named += Number(page.skipped);
|
|
55
|
+
result.total_functions += Number(page.skipped);
|
|
56
|
+
for (const payload of payloads) {
|
|
57
|
+
result.total_functions++;
|
|
58
|
+
if (p.dry_run) {
|
|
59
|
+
result.created++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Commit the exact outbound identity before a request can leave the process.
|
|
63
|
+
this.workspace.administrationState(request.id, 'unknown', { ...result, sourceSnapshot: snapshot, pending: { address: payload.address, payloadSha256: digest(payload) } });
|
|
64
|
+
try {
|
|
65
|
+
const received = await archivePost(this.c.fetch, origin, payload, token, this.c.signal);
|
|
66
|
+
if (received.created)
|
|
67
|
+
result.created++;
|
|
68
|
+
else
|
|
69
|
+
result.updated++;
|
|
70
|
+
result.conflicts_enqueued += received.conflicts_enqueued;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
result.errors++;
|
|
74
|
+
if (result.first_errors.length < 10)
|
|
75
|
+
result.first_errors.push({ address: String(payload.address), state: 'unknown' });
|
|
76
|
+
}
|
|
77
|
+
this.workspace.administrationState(request.id, 'unknown', { ...result, sourceSnapshot: snapshot });
|
|
78
|
+
if (this.c.signal.aborted)
|
|
79
|
+
fail('unknown', 'Archive submission was cancelled; completed and uncertain outcomes are retained.');
|
|
80
|
+
}
|
|
81
|
+
offset += Number(page.visited);
|
|
82
|
+
more = page.more;
|
|
83
|
+
}
|
|
84
|
+
if (more && Number(p.limit) === 0)
|
|
85
|
+
fail('limit', 'Archive selection exceeds 100000 functions; use an explicit bounded selection.');
|
|
86
|
+
result.truncated = more;
|
|
87
|
+
result.state = result.errors ? 'partial' : p.dry_run ? 'preview' : 'applied';
|
|
88
|
+
this.workspace.administrationState(request.id, result.errors ? 'unknown' : 'applied', result);
|
|
89
|
+
if (result.errors)
|
|
90
|
+
fail('partial', 'Some archive outcomes are unknown; retained counts are not retried.', result);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const current = this.workspace.administration(request.id);
|
|
95
|
+
if (current.state !== 'applied')
|
|
96
|
+
this.workspace.administrationState(request.id, 'unknown', { ...result, state: 'unknown', sourceSnapshot: snapshot });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function archiveCommands(service) {
|
|
103
|
+
return [command('archive', 'Send selected private documentation to the configured archive', ['function', 'program'].map(operation => command('ingest-' + operation, 'Prepare documentation and retain every external request outcome', ({ options: o }, c) => {
|
|
104
|
+
const p = { version: text(o.versionLabel, 512), dry_run: Boolean(o.dryRun) };
|
|
105
|
+
if (operation === 'function')
|
|
106
|
+
p.function = address(o.address);
|
|
107
|
+
else {
|
|
108
|
+
p.limit = number(o.limit, 100000);
|
|
109
|
+
p.skip_default_named = !o.includeDefaultNamed;
|
|
110
|
+
}
|
|
111
|
+
return service(c).ingest(scope(o), operation, p, o.requestId === undefined ? undefined : id(o.requestId));
|
|
112
|
+
}, { permission: Permission.Update, options: [...selectedOptions,
|
|
113
|
+
...(operation === 'function' ? [{ flags: '--address <address>', description: 'Selected function entry point', required: true }] : [{ flags: '--limit <count>', description: 'Maximum visited functions (0 means all, at most 100000)', defaultValue: 0 }, { flags: '--include-default-named', description: 'Include auto-named functions' }]),
|
|
114
|
+
{ flags: '--version-label <name>', description: 'Archive version label', defaultValue: 'unknown' }, { flags: '--dry-run', description: 'Validate payloads without sending documentation' },
|
|
115
|
+
{ flags: '--request-id <id>', description: 'Retrieve retained counts without repeating archive requests' }] })))];
|
|
116
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OptionDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
export interface Parameter {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
default?: string;
|
|
7
|
+
semantic?: string;
|
|
8
|
+
allowEmpty?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const key: (name: string) => string;
|
|
11
|
+
export declare const flag: (name: string) => string;
|
|
12
|
+
export declare function parsed(p: Parameter, value: unknown): unknown;
|
|
13
|
+
export declare const selectedOptions: OptionDefinition[];
|
|
14
|
+
export declare function option(p: Parameter): OptionDefinition;
|