@henols/vice-mcp 0.2.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +6 -4
- package/anno-cli.ts +1 -1
- package/anno-store.ts +1 -1
- package/backend-detect.mts +143 -19
- package/build.ts +42 -1
- package/evid-ingest.ts +7 -3
- package/host-tool-client.ts +7 -3
- package/package.json +3 -1
- package/prerequisites.json +416 -0
- package/prg-image.ts +1 -1
- package/repo-root.ts +19 -8
- package/resources/backend-detect.mjs +90 -18
- package/resources/broker-launch.mjs +11 -2
- package/resources/ghidra-project.mjs +2 -2
- package/resources/host-tool.mjs +306 -82
- package/resources/prerequisites.json +416 -0
- package/resources/tool-location.mjs +750 -0
- package/resources/vice-broker.mjs +85 -12
- package/stock-checkpoints.ts +2 -2
- package/stock-connect.ts +12 -10
- package/stock-execution.ts +4 -2
- package/stock-memory-search.ts +3 -2
- package/stock-protocol.ts +60 -28
- package/stock-registers.ts +3 -1
- package/text-protocol.ts +6 -3
- package/textmon-profile.ts +8 -3
- package/version.ts +34 -246
- package/vice-proxy.ts +23 -23
package/resources/host-tool.mjs
CHANGED
|
@@ -117,6 +117,33 @@ import { resolveGhidraProject, buildAnalyzeHeadlessArgv, hasDotPrefixedSegment,
|
|
|
117
117
|
// once per broker lifetime -- never re-probed per c1541.* call within the
|
|
118
118
|
// SAME process, per findSiblingBinary()'s own memo below.
|
|
119
119
|
import { resolvedBackend } from "./backend-detect.mjs";
|
|
120
|
+
// This module's THIRD sibling import, and its own use of the `.mjs`-extension
|
|
121
|
+
// rule for a host-bound sibling -- the same form the `backend-detect.mjs`
|
|
122
|
+
// import immediately above already uses, a plain static VALUE import, never
|
|
123
|
+
// the lazy `createRequire()` dual-path load `backend-detect.mts`'s own
|
|
124
|
+
// `toolLocationSeam()` uses. That dance exists ONLY for a module that ships
|
|
125
|
+
// two ways -- unbuilt, imported directly by a real no-build-step entry point,
|
|
126
|
+
// AND compiled -- and `host-tool.mts` does not: every real consumer (this
|
|
127
|
+
// project's own broker, the direct host-spawn route, and every test file
|
|
128
|
+
// that reaches `runHostTool()`) imports the COMPILED `resources/host-tool.mjs`
|
|
129
|
+
// artifact only, exactly like the `ghidra-project.mjs` import above already
|
|
130
|
+
// does; the unbuilt `.mts` source is never loaded as a live ESM module by
|
|
131
|
+
// anything in this tree (confirmed: no import specifier anywhere in this
|
|
132
|
+
// repository names `"./host-tool.mts"`). A static `"./tool-location.mjs"`
|
|
133
|
+
// specifier is therefore safe here the same way it is safe for
|
|
134
|
+
// `ghidra-project.mjs`: it only ever resolves once both compiled siblings sit
|
|
135
|
+
// together in `resources/`, which `build.ts`'s `HOST_BOUND_ARTIFACTS` already
|
|
136
|
+
// guarantees for both. `resolveTool` is `LOC-01`'s seam call (a `tools.json`
|
|
137
|
+
// entry for `acme`/`acme-lib`/`ghidra` reaching this module's spawn);
|
|
138
|
+
// `remedyTextsFor` is `DECL-03`'s remedy reader (plan 60-02) -- every refusal
|
|
139
|
+
// this module returns for `acme`, `acme-lib`, `ghidra` or `dxa` composes its
|
|
140
|
+
// closing sentence from this function's return value, never a re-authored
|
|
141
|
+
// literal. `resolveOnPath` is plan 60-04's addition (`LOC-04`, PD-08):
|
|
142
|
+
// `findSiblingBinary()`'s own inline `$PATH` loop now calls this exported
|
|
143
|
+
// walk instead of carrying a second copy of the same algorithm -- the
|
|
144
|
+
// SECOND of Phase 59 `D-02`'s three coexisting `$PATH`-walk copies to
|
|
145
|
+
// collapse (plan 60-01 collapsed the first, inside `backend-detect.mts`).
|
|
146
|
+
import { resolveTool, remedyTextsFor, resolveOnPath } from "./tool-location.mjs";
|
|
120
147
|
// This module's own directory, used ONLY to compute the vendored dxa
|
|
121
148
|
// binary's fixed path. Never an environment-variable override: dxa is
|
|
122
149
|
// vendored AND built by this project (unlike ACME_BIN/GHIDRA_HOME, which
|
|
@@ -891,22 +918,81 @@ export function resolveWorkspacePath(repoRoot, relative) {
|
|
|
891
918
|
}
|
|
892
919
|
return { ok: true, path: walkedCandidate.path };
|
|
893
920
|
}
|
|
921
|
+
/** Returns `locate` verbatim when supplied, or -- when absent -- a locator
|
|
922
|
+
* derived from `process.cwd()` (PD-06). The production route
|
|
923
|
+
* (`runHostTool()`) always supplies a real locator built from its own
|
|
924
|
+
* already-resolved `repoRootAbs`; this fallback exists only so the argv-shape
|
|
925
|
+
* test call sites across this file's own test suite -- which exercise argv
|
|
926
|
+
* construction alone -- keep compiling and keep returning the same argv they
|
|
927
|
+
* return today, with no per-call-site edit required. Documented as a last
|
|
928
|
+
* resort, not a guess: a working directory with no `.c64-re-tools/tools.json`
|
|
929
|
+
* makes the file layer a silent no-op for that call, so this fallback can
|
|
930
|
+
* only WIDEN resolution (adding the environment and `$PATH`/fixed-prefix
|
|
931
|
+
* layers a bare literal never had) where a real project root already happens
|
|
932
|
+
* to be the working directory -- mirrors `backend-detect.mts`'s own
|
|
933
|
+
* `ResolvedBackendDeps` derivation (PD-03) for the identical reason. */
|
|
934
|
+
function locatorFrom(locate) {
|
|
935
|
+
if (locate)
|
|
936
|
+
return locate;
|
|
937
|
+
const projectRoot = process.cwd();
|
|
938
|
+
return { toolsDir: join(projectRoot, ".c64-re-tools"), projectRoot };
|
|
939
|
+
}
|
|
940
|
+
/** Appends a declared tool's remedy prose (`remedyTextsFor()`, DECL-03) to a
|
|
941
|
+
* refusal `base` sentence, joined with `"; "` -- appending NOTHING at all
|
|
942
|
+
* when the declaration carries no remedy for the running platform, so a
|
|
943
|
+
* refusal never ends in a dangling separator with nothing after it. `here`
|
|
944
|
+
* threads `HostToolLocator.here` through so a test pointing resolution at a
|
|
945
|
+
* scratch declaration (see that field's own doc comment) reads the remedy
|
|
946
|
+
* text from the SAME scratch declaration, never the real committed one. */
|
|
947
|
+
function withRemedy(base, id, here) {
|
|
948
|
+
const remedies = remedyTextsFor(id, here !== undefined ? { here } : {});
|
|
949
|
+
return remedies.length > 0 ? `${base} -- ${remedies.join("; ")}` : base;
|
|
950
|
+
}
|
|
894
951
|
/** Deterministic: the same typed request and the same resolved paths yield
|
|
895
952
|
* a byte-identical argv array on two successive calls -- no randomness, no
|
|
896
953
|
* timestamp, no environment-dependent ordering. `log` is OPTIONAL and used
|
|
897
954
|
* ONLY by the c1541.dir branch to report a PATH-fallback binary resolution
|
|
898
955
|
* -- every pre-existing branch ignores it, exactly as they already ignore
|
|
899
|
-
* any parameter they do not need.
|
|
900
|
-
|
|
956
|
+
* any parameter they do not need. `locate` is OPTIONAL (PD-06): the one
|
|
957
|
+
* production caller, `runHostTool()`, always supplies it; every other call
|
|
958
|
+
* site -- this file's own argv-shape tests foremost -- may omit it and falls
|
|
959
|
+
* back to `locatorFrom()`'s own `process.cwd()`-derived pair. */
|
|
960
|
+
export function buildHostToolArgv(request, resolved, log, locate) {
|
|
961
|
+
const loc = locatorFrom(locate);
|
|
901
962
|
if (request.tool === "acme.build") {
|
|
902
963
|
const { args } = request;
|
|
903
964
|
const { sourcePath, outDirPath, includePaths } = resolved;
|
|
904
965
|
const stem = join(outDirPath, basename(sourcePath).replace(/\.(a|asm|s)$/i, ""));
|
|
905
966
|
const prg = `${stem}.prg`;
|
|
906
967
|
// Overridable local variable named for what it holds -- never `binPath`/
|
|
907
|
-
// `viceBin`/`VICE_BIN`/`x64sc
|
|
908
|
-
//
|
|
909
|
-
|
|
968
|
+
// `viceBin`/`VICE_BIN`/`x64sc`. Kept deliberately, by CONVENTION, since
|
|
969
|
+
// the test that once scanned for those four tokens (its own file,
|
|
970
|
+
// its own EMULATOR_BIN_SHAPE classifier) was deleted in commit
|
|
971
|
+
// `276c15c9` and is not revived here -- no guard enforces this naming
|
|
972
|
+
// rule today. What IS mechanically enforced, over the four tool-location
|
|
973
|
+
// environment-variable names this seam reads, is the closed consumer set
|
|
974
|
+
// `tool-location-consumers.test.ts` (plan 60-05) asserts.
|
|
975
|
+
// Resolved through the seam (LOC-01) rather than a direct read of the
|
|
976
|
+
// declared ACME environment variable: env -> tools.json -> $PATH, in
|
|
977
|
+
// that order, the one precedence this tree now states once. A malformed
|
|
978
|
+
// tools.json entry is a REFUSAL (quoted verbatim, no remedy appended --
|
|
979
|
+
// the declaration's remedy is prose for "ACME is missing", not for "your
|
|
980
|
+
// tools.json entry is wrong"); a well-formed-but-nonexistent path anywhere
|
|
981
|
+
// along the chain is "not found", which DOES carry the declared remedy.
|
|
982
|
+
// This existence check is NEW behaviour (T-60-08): today there is none at
|
|
983
|
+
// all, and a missing ACME surfaces only as a raw operating-system spawn
|
|
984
|
+
// error inside spawnErrorMessage.
|
|
985
|
+
const acmeResolved = resolveTool("acme", { toolsDir: loc.toolsDir, projectRoot: loc.projectRoot, here: loc.here });
|
|
986
|
+
if (acmeResolved.refusal) {
|
|
987
|
+
return { ok: false, message: `host_tool "acme.build" refuses: ${acmeResolved.refusal}` };
|
|
988
|
+
}
|
|
989
|
+
if (acmeResolved.path === null) {
|
|
990
|
+
return {
|
|
991
|
+
ok: false,
|
|
992
|
+
message: withRemedy(`host_tool "acme.build" refuses: the ACME binary was not found (tried: ${acmeResolved.tried.join(", ")})`, "acme", loc.here),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
const acmePath = acmeResolved.path;
|
|
910
996
|
// Fixed flags first, in the SAME order src/skills/acme-build/scripts/
|
|
911
997
|
// acme.mjs's build() uses today, then one -D per define and one -I pair
|
|
912
998
|
// per include in caller-given order, then --setpc if given, then the
|
|
@@ -950,23 +1036,34 @@ export function buildHostToolArgv(request, resolved, log) {
|
|
|
950
1036
|
}
|
|
951
1037
|
if (request.tool === "ghidra.analyze") {
|
|
952
1038
|
const { importPath, projectLocation, projectName, preScriptPath, postScriptPath, scriptPathResolved, entrypointsPathResolved, exportPathResolved, dataRangesPathResolved, } = resolved;
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
1039
|
+
// Resolved through the seam (LOC-01) -- env (GHIDRA_HOME) -> tools.json
|
|
1040
|
+
// -> no $PATH probe at all (a `directory`-kind id gets no probe layer,
|
|
1041
|
+
// D-15) -- never a guessed install location and never this repository's
|
|
1042
|
+
// own local probe directory (T-34-16). Ghidra has no fixed-prefix
|
|
1043
|
+
// fallback by design (unlike ACME's library, PD-07 below), so this
|
|
1044
|
+
// preserves today's env-only-then-refuse behaviour exactly while adding
|
|
1045
|
+
// the file layer between the environment and the refusal.
|
|
1046
|
+
const ghidraResolved = resolveTool("ghidra", { toolsDir: loc.toolsDir, projectRoot: loc.projectRoot, here: loc.here });
|
|
1047
|
+
if (ghidraResolved.path === null) {
|
|
957
1048
|
return {
|
|
958
1049
|
ok: false,
|
|
959
|
-
message: `host_tool "ghidra.analyze"
|
|
1050
|
+
message: withRemedy(`host_tool "ghidra.analyze" refuses: no Ghidra installation directory is known${ghidraResolved.refusal ? `: ${ghidraResolved.refusal}` : ` (tried: ${ghidraResolved.tried.join(", ")})`}`, "ghidra", loc.here),
|
|
960
1051
|
};
|
|
961
1052
|
}
|
|
1053
|
+
const ghidraHome = ghidraResolved.path;
|
|
962
1054
|
// Overridable local variable named for what it holds -- never `binPath`/
|
|
963
|
-
// `viceBin`/`VICE_BIN`/`x64sc
|
|
964
|
-
//
|
|
1055
|
+
// `viceBin`/`VICE_BIN`/`x64sc`. Kept deliberately, by CONVENTION, since
|
|
1056
|
+
// the test that once scanned for those four tokens (its own file,
|
|
1057
|
+
// its own EMULATOR_BIN_SHAPE classifier) was deleted in commit
|
|
1058
|
+
// `276c15c9` and is not revived here -- no guard enforces this naming
|
|
1059
|
+
// rule today. What IS mechanically enforced, over the four tool-location
|
|
1060
|
+
// environment-variable names this seam reads, is the closed consumer set
|
|
1061
|
+
// `tool-location-consumers.test.ts` (plan 60-05) asserts.
|
|
965
1062
|
const ghidraPath = join(ghidraHome, "support", "analyzeHeadless");
|
|
966
1063
|
if (!existsSync(ghidraPath)) {
|
|
967
1064
|
return {
|
|
968
1065
|
ok: false,
|
|
969
|
-
message: `host_tool "ghidra.analyze" refuses:
|
|
1066
|
+
message: withRemedy(`host_tool "ghidra.analyze" refuses: the resolved Ghidra installation directory (${ghidraHome}) does not contain "support/analyzeHeadless"`, "ghidra", loc.here),
|
|
970
1067
|
};
|
|
971
1068
|
}
|
|
972
1069
|
// The checked, NON-MATERIALISING language preflight -- refuses by
|
|
@@ -1054,15 +1151,22 @@ export function buildHostToolArgv(request, resolved, log) {
|
|
|
1054
1151
|
if (request.tool === "dxa.disassemble") {
|
|
1055
1152
|
const { args } = request;
|
|
1056
1153
|
const { imagePath, outDirPath, entrypointsPath, datablocksPath, labelsPath } = resolved;
|
|
1057
|
-
// A-01: fixed, computed path -- never an env-var override
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1154
|
+
// A-01: fixed, computed path -- never an env-var override, never
|
|
1155
|
+
// tools.json, never the seam at all (Phase 59 D-16, LOC-05: dxa is
|
|
1156
|
+
// vendored and built by this project, so an override could only ever
|
|
1157
|
+
// select a binary this project did not build and did not pin -- see the
|
|
1158
|
+
// HERE and findDxaBinary() comments above). Refuses BY NAME when the
|
|
1159
|
+
// vendored binary does not exist at EITHER candidate location; the
|
|
1160
|
+
// remedy sentence is the declaration's own (DECL-03, `remedyTextsFor()`),
|
|
1161
|
+
// never a literal re-authored here -- it happens to match the previous
|
|
1162
|
+
// hardcoded build-script sentence's text by coincidence of two
|
|
1163
|
+
// independently-authored strings, and after this change there is one
|
|
1164
|
+
// source.
|
|
1061
1165
|
const dxaFound = findDxaBinary(HERE);
|
|
1062
1166
|
if (dxaFound.path === null) {
|
|
1063
1167
|
return {
|
|
1064
1168
|
ok: false,
|
|
1065
|
-
message: `host_tool "dxa.disassemble" refuses: the vendored dxa binary does not exist (tried: ${dxaFound.tried.join(", ")})
|
|
1169
|
+
message: withRemedy(`host_tool "dxa.disassemble" refuses: the vendored dxa binary does not exist (tried: ${dxaFound.tried.join(", ")})`, "dxa", loc.here),
|
|
1066
1170
|
};
|
|
1067
1171
|
}
|
|
1068
1172
|
const dxaPath = dxaFound.path;
|
|
@@ -1095,25 +1199,27 @@ export function buildHostToolArgv(request, resolved, log) {
|
|
|
1095
1199
|
if (request.tool === "ghidra.installExtension") {
|
|
1096
1200
|
const { moduleName } = resolved;
|
|
1097
1201
|
// Independently re-derived rather than threaded through `resolved` --
|
|
1098
|
-
// mirrors ghidra.analyze's own branch above, which
|
|
1099
|
-
// itself instead of accepting it as a resolved field.
|
|
1100
|
-
// checks were already performed (and, for the copy,
|
|
1101
|
-
// by runHostTool()'s own resolution branch before this
|
|
1102
|
-
// ever called; re-checking here is defense in depth, the
|
|
1103
|
-
// buildAnalyzeHeadlessArgv()'s own independent dot-segment
|
|
1104
|
-
// takes for a caller that bypassed the resolution branch
|
|
1105
|
-
|
|
1106
|
-
|
|
1202
|
+
// mirrors ghidra.analyze's own branch above, which resolves GHIDRA_HOME
|
|
1203
|
+
// through the seam itself instead of accepting it as a resolved field.
|
|
1204
|
+
// Both existence checks were already performed (and, for the copy,
|
|
1205
|
+
// already acted on) by runHostTool()'s own resolution branch before this
|
|
1206
|
+
// function was ever called; re-checking here is defense in depth, the
|
|
1207
|
+
// same posture buildAnalyzeHeadlessArgv()'s own independent dot-segment
|
|
1208
|
+
// re-check takes for a caller that bypassed the resolution branch
|
|
1209
|
+
// entirely.
|
|
1210
|
+
const ghidraResolved = resolveTool("ghidra", { toolsDir: loc.toolsDir, projectRoot: loc.projectRoot, here: loc.here });
|
|
1211
|
+
if (ghidraResolved.path === null) {
|
|
1107
1212
|
return {
|
|
1108
1213
|
ok: false,
|
|
1109
|
-
message: `host_tool "ghidra.installExtension"
|
|
1214
|
+
message: withRemedy(`host_tool "ghidra.installExtension" refuses: no Ghidra installation directory is known${ghidraResolved.refusal ? `: ${ghidraResolved.refusal}` : ` (tried: ${ghidraResolved.tried.join(", ")})`}`, "ghidra", loc.here),
|
|
1110
1215
|
};
|
|
1111
1216
|
}
|
|
1217
|
+
const ghidraHome = ghidraResolved.path;
|
|
1112
1218
|
const sleighPath = join(ghidraHome, "support", "sleigh");
|
|
1113
1219
|
if (!existsSync(sleighPath)) {
|
|
1114
1220
|
return {
|
|
1115
1221
|
ok: false,
|
|
1116
|
-
message: `host_tool "ghidra.installExtension" refuses:
|
|
1222
|
+
message: withRemedy(`host_tool "ghidra.installExtension" refuses: the resolved Ghidra installation directory (${ghidraHome}) does not contain "support/sleigh"`, "ghidra", loc.here),
|
|
1117
1223
|
};
|
|
1118
1224
|
}
|
|
1119
1225
|
const installLanguagesDir = join(ghidraHome, "Ghidra", "Extensions", moduleName, "data", "languages");
|
|
@@ -1129,20 +1235,35 @@ export function buildHostToolArgv(request, resolved, log) {
|
|
|
1129
1235
|
request.tool === "c1541.chain" ||
|
|
1130
1236
|
request.tool === "c1541.read") {
|
|
1131
1237
|
const { imagePath, outDirPath } = resolved;
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
1238
|
+
// A tools.json entry for "c1541" (plan 60-04, LOC-04) is honoured FIRST
|
|
1239
|
+
// (PD-08) -- c1541 carries no environment variable of its own (Phase 59
|
|
1240
|
+
// D-06), so this is its only override route. Absent one, resolved as a
|
|
1241
|
+
// SIBLING of whichever x64sc backend-detect.mts already resolved --
|
|
1242
|
+
// never a bare-name spawn, which a host carrying both a stock and a
|
|
1243
|
+
// fork build would silently answer with whichever build's directory
|
|
1244
|
+
// happens to sort first on $PATH (MEASURED live on this project's own
|
|
1245
|
+
// dev host, see the import comment above).
|
|
1137
1246
|
// `resolvedBackend()` with no `supervisorDir` never touches the on-disk
|
|
1138
1247
|
// cache; it still memoises in-process, which is what keeps a
|
|
1139
1248
|
// long-running broker's SECOND call here free -- see the import
|
|
1140
1249
|
// comment's own memoisation posture.
|
|
1141
|
-
const c1541Found = findSiblingBinary("c1541", resolvedBackend().binPath, log);
|
|
1250
|
+
const c1541Found = findSiblingBinary("c1541", resolvedBackend().binPath, log, loc);
|
|
1251
|
+
// WR-01 (plan 60-07): a refusal from the seam's own file layer (a
|
|
1252
|
+
// malformed tools.json entry -- wrong kind, missing executable bit, path
|
|
1253
|
+
// absent on disk) is quoted verbatim with NO remedy appended, mirroring
|
|
1254
|
+
// buildHostToolArgv()'s own ACME branch above -- the declaration's
|
|
1255
|
+
// remedy is prose for "c1541 is missing", not for "your tools.json entry
|
|
1256
|
+
// is wrong", and appending it here would send a user with a working
|
|
1257
|
+
// c1541 install to the wrong fix. Only the true not-found case (no
|
|
1258
|
+
// refusal, path still null) keeps today's generic sentence and its
|
|
1259
|
+
// withRemedy() call.
|
|
1260
|
+
if (c1541Found.refusal) {
|
|
1261
|
+
return { ok: false, message: `host_tool "${request.tool}" refuses: ${c1541Found.refusal}` };
|
|
1262
|
+
}
|
|
1142
1263
|
if (c1541Found.path === null) {
|
|
1143
1264
|
return {
|
|
1144
1265
|
ok: false,
|
|
1145
|
-
message: `host_tool "${request.tool}" refuses: "c1541" does not exist (tried: ${c1541Found.tried.join(", ")})`,
|
|
1266
|
+
message: withRemedy(`host_tool "${request.tool}" refuses: "c1541" does not exist (tried: ${c1541Found.tried.join(", ")})`, "c1541", loc.here),
|
|
1146
1267
|
};
|
|
1147
1268
|
}
|
|
1148
1269
|
const c1541Path = c1541Found.path;
|
|
@@ -1204,14 +1325,22 @@ export function buildHostToolArgv(request, resolved, log) {
|
|
|
1204
1325
|
}
|
|
1205
1326
|
if (request.tool === "petcat.decode") {
|
|
1206
1327
|
const { imagePath, outDirPath } = resolved;
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1210
|
-
|
|
1328
|
+
// Same precedence as c1541.* above (plan 60-04, LOC-04, PD-08): a
|
|
1329
|
+
// tools.json entry for "petcat" wins first, then a SIBLING of whichever
|
|
1330
|
+
// x64sc backend-detect.mts already resolved, never a bare-name spawn,
|
|
1331
|
+
// with a logged $PATH-fallback warning -- the same mechanism c1541.*
|
|
1332
|
+
// already use above.
|
|
1333
|
+
const petcatFound = findSiblingBinary("petcat", resolvedBackend().binPath, log, loc);
|
|
1334
|
+
// WR-01 (plan 60-07): same refusal-first branch as the c1541 case above
|
|
1335
|
+
// -- a seam refusal is quoted verbatim with no remedy appended; only the
|
|
1336
|
+
// true not-found case carries the remedy.
|
|
1337
|
+
if (petcatFound.refusal) {
|
|
1338
|
+
return { ok: false, message: `host_tool "petcat.decode" refuses: ${petcatFound.refusal}` };
|
|
1339
|
+
}
|
|
1211
1340
|
if (petcatFound.path === null) {
|
|
1212
1341
|
return {
|
|
1213
1342
|
ok: false,
|
|
1214
|
-
message: `host_tool "petcat.decode" refuses: "petcat" does not exist (tried: ${petcatFound.tried.join(", ")})`,
|
|
1343
|
+
message: withRemedy(`host_tool "petcat.decode" refuses: "petcat" does not exist (tried: ${petcatFound.tried.join(", ")})`, "petcat", loc.here),
|
|
1215
1344
|
};
|
|
1216
1345
|
}
|
|
1217
1346
|
const petcatPath = petcatFound.path;
|
|
@@ -1680,11 +1809,21 @@ function spawnHostTool(toolPath, argv, timeoutMs, env, cwd) {
|
|
|
1680
1809
|
// ---------------------------------------------------------------------------
|
|
1681
1810
|
// The ACME library probe. Moved server-side from acme.mjs's own
|
|
1682
1811
|
// findAcmeLib(): the project owner's rule is that a container has no PATH
|
|
1683
|
-
// to a host binary, and these
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1812
|
+
// to a host binary, and these candidates are HOST paths -- so probing them
|
|
1813
|
+
// belongs on the host side of the seam, not in the container-side skill
|
|
1814
|
+
// script.
|
|
1815
|
+
//
|
|
1816
|
+
// PD-07 (Phase 60, correcting an earlier claim in 60-PATTERNS.md that this
|
|
1817
|
+
// function is "a drop-in" for the seam): WIDENED, not replaced. The seam's
|
|
1818
|
+
// probe layer is a $PATH walk for executable-kind ids ONLY -- a
|
|
1819
|
+
// directory-kind id (acme-lib's own declared `kind`) gets no probe layer at
|
|
1820
|
+
// all -- so the seam now answers the environment (`ACME`) and `tools.json`
|
|
1821
|
+
// layers ahead of this function's own fixed, well-known-install-location
|
|
1822
|
+
// list, and that list stays as the probe layer beneath both: same order,
|
|
1823
|
+
// same marker file, same "first candidate whose marker exists wins" rule it
|
|
1824
|
+
// has always had. Only the environment-variable candidate is gone from the
|
|
1825
|
+
// list below -- the seam's own env layer already covers it, reading the
|
|
1826
|
+
// SAME `ACME` name from the declaration rather than a literal here.
|
|
1688
1827
|
// ---------------------------------------------------------------------------
|
|
1689
1828
|
/** The marker file used to validate a candidate ACME library directory --
|
|
1690
1829
|
* the layout fact `acme.mjs`'s own troubleshooting hint names. */
|
|
@@ -1712,10 +1851,21 @@ export function findDxaBinary(here) {
|
|
|
1712
1851
|
}
|
|
1713
1852
|
return { path: null, tried };
|
|
1714
1853
|
}
|
|
1715
|
-
function findAcmeLib() {
|
|
1854
|
+
function findAcmeLib(locate) {
|
|
1855
|
+
const loc = locatorFrom(locate);
|
|
1716
1856
|
const tried = [];
|
|
1857
|
+
// The seam layer FIRST (PD-07): env (`ACME`) -> tools.json. A malformed
|
|
1858
|
+
// tools.json entry for `acme-lib` is a REFUSAL the seam already detected;
|
|
1859
|
+
// this function has no `refusal`-shaped return of its own (its callers
|
|
1860
|
+
// never had one), so a refusal is folded into "not found" here rather than
|
|
1861
|
+
// surfaced separately -- the caller's own conditional stderr hint (below,
|
|
1862
|
+
// in runHostTool()) already treats a null `path` uniformly regardless of
|
|
1863
|
+
// why.
|
|
1864
|
+
const seamResolved = resolveTool("acme-lib", { toolsDir: loc.toolsDir, projectRoot: loc.projectRoot, here: loc.here });
|
|
1865
|
+
tried.push(...seamResolved.tried);
|
|
1866
|
+
if (seamResolved.path !== null)
|
|
1867
|
+
return { path: seamResolved.path, tried };
|
|
1717
1868
|
const candidates = [
|
|
1718
|
-
process.env.ACME,
|
|
1719
1869
|
"/usr/local/share/acme",
|
|
1720
1870
|
"/usr/share/acme",
|
|
1721
1871
|
"/usr/lib/acme",
|
|
@@ -1734,11 +1884,20 @@ function findAcmeLib() {
|
|
|
1734
1884
|
// only when it does not exist, and return every candidate tried so a
|
|
1735
1885
|
// refusal can name them all. Unlike findDxaBinary() (a FIXED,
|
|
1736
1886
|
// project-vendored path) and findAcmeLib() (a FIXED list of well-known
|
|
1737
|
-
// host install locations), this probe's
|
|
1887
|
+
// host install locations), this probe's SECOND candidate is COMPUTED per
|
|
1738
1888
|
// call, from whichever x64sc backend-detect.mts already resolved -- see
|
|
1739
1889
|
// the resolvedBackend() import comment above for why that call is cheap
|
|
1740
1890
|
// here. No version probe: deliberately withdrawn by the project owner --
|
|
1741
1891
|
// this stays a name-and-location probe only and must not gain one back.
|
|
1892
|
+
//
|
|
1893
|
+
// Plan 60-04 (LOC-04, PD-08) adds a FIRST candidate ahead of the sibling:
|
|
1894
|
+
// c1541 and petcat carry no environment-variable location of their own
|
|
1895
|
+
// (Phase 59 D-06), so `tools.json` is their only override route, and
|
|
1896
|
+
// ROADMAP criterion 4 requires it sit AHEAD of the sibling probe -- a
|
|
1897
|
+
// sibling that happens to exist must never silently outrank a path the
|
|
1898
|
+
// user explicitly wrote in the file. The resulting order: tools.json (via
|
|
1899
|
+
// the seam), then the sibling-of-x64sc candidate (unchanged), then $PATH
|
|
1900
|
+
// (now the seam's own exported walk, not a second hand-written copy).
|
|
1742
1901
|
// ---------------------------------------------------------------------------
|
|
1743
1902
|
/** Memoised per binary name for the process lifetime -- mirrors
|
|
1744
1903
|
* backend-detect.mts's own stated posture of resolving once per process,
|
|
@@ -1750,40 +1909,93 @@ function findAcmeLib() {
|
|
|
1750
1909
|
* -- this probe's OWN per-binary-name memo exists because its first
|
|
1751
1910
|
* candidate is computed from a resolvedBackend() call that is itself
|
|
1752
1911
|
* memoised, so re-deriving it per call would just re-walk $PATH for no new
|
|
1753
|
-
* information).
|
|
1912
|
+
* information).
|
|
1913
|
+
*
|
|
1914
|
+
* **`WR-02` (plan 60-07, code review): this memo's caching of a `tools.json`
|
|
1915
|
+
* answer is inconsistent with the seam's own no-memo rationale.**
|
|
1916
|
+
* `tool-location.mts`'s own module header states plainly that the seam
|
|
1917
|
+
* "deliberately holds no memo" and that caching a resolved location would
|
|
1918
|
+
* make a stale answer structurally possible. This memo does exactly that for
|
|
1919
|
+
* the one layer the whole phase exists to make user-editable: an edited
|
|
1920
|
+
* `tools.json` entry for `c1541`/`petcat` has NO EFFECT until this process
|
|
1921
|
+
* restarts, while the identical edit for `acme`/`acme-lib`/`ghidra` (neither
|
|
1922
|
+
* of which is memoised this way) takes effect on the very next call. The
|
|
1923
|
+
* roadmap's own cross-cutting constraint for this phase requires this memo
|
|
1924
|
+
* keep its current semantics -- widening what it can resolve (this plan's
|
|
1925
|
+
* own `refusal` field, below) must not change WHEN it caches -- so this
|
|
1926
|
+
* tension is recorded here, not resolved: no reset hatch is added, and no
|
|
1927
|
+
* guard enforces the tension away. The trigger for revisiting it is named in
|
|
1928
|
+
* `60-01-PLAN.md`'s own assumption-delta note: when the sibling-probe
|
|
1929
|
+
* mechanism this memo exists for moves inside the seam itself, the memo
|
|
1930
|
+
* moves with it or is dropped with it -- that is the right moment to
|
|
1931
|
+
* reconcile this function's caching posture with the seam's stated
|
|
1932
|
+
* no-memo rationale, not before. */
|
|
1754
1933
|
const siblingBinaryMemo = new Map();
|
|
1755
|
-
|
|
1934
|
+
/** `locate` threads the caller's `HostToolLocator` into the seam call below
|
|
1935
|
+
* (`buildHostToolArgv()`'s own `loc`, itself built from `locatorFrom()`) --
|
|
1936
|
+
* absent when the caller omits it, exactly like `buildHostToolArgv()`'s own
|
|
1937
|
+
* `locate` parameter, and falling back to the SAME `locatorFrom()` helper.
|
|
1938
|
+
*
|
|
1939
|
+
* The seam call is deliberately NOT trusted for its own internal `$PATH`
|
|
1940
|
+
* layer (layer 3 inside `resolveTool()`, which fires for any
|
|
1941
|
+
* executable-kind id -- both `c1541` and `petcat` are -- once neither the
|
|
1942
|
+
* environment nor `tools.json` answered). Only a `layer === "file"` answer
|
|
1943
|
+
* is accepted as this function's file-layer result: accepting a
|
|
1944
|
+
* `"probe"`-layer answer here too would let the seam's OWN `$PATH` walk
|
|
1945
|
+
* silently win over the sibling candidate below, before this function ever
|
|
1946
|
+
* got a chance to try it -- inverting PD-08's precedence (tools.json, THEN
|
|
1947
|
+
* sibling, THEN `$PATH`) and resolving to a `$PATH` match with NO shadowing
|
|
1948
|
+
* warning, the one thing this function exists to prevent. A `refusal` (a
|
|
1949
|
+
* named `tools.json` entry that failed its own checks) is always terminal
|
|
1950
|
+
* regardless of layer, matching the seam's own file-layer contract
|
|
1951
|
+
* (`tool-location.mts`, D-09) -- it never falls through to the sibling
|
|
1952
|
+
* candidate. */
|
|
1953
|
+
function findSiblingBinary(binaryName, resolvedX64scPath, log, locate) {
|
|
1756
1954
|
const memoised = siblingBinaryMemo.get(binaryName);
|
|
1757
1955
|
if (memoised)
|
|
1758
1956
|
return memoised;
|
|
1759
1957
|
const tried = [];
|
|
1760
|
-
//
|
|
1958
|
+
// Layer 1 (PD-08): the tools.json seam, ahead of the sibling candidate.
|
|
1959
|
+
const loc = locatorFrom(locate);
|
|
1960
|
+
const seamResolved = resolveTool(binaryName, { toolsDir: loc.toolsDir, projectRoot: loc.projectRoot, here: loc.here });
|
|
1961
|
+
tried.push(...seamResolved.tried);
|
|
1962
|
+
if (seamResolved.refusal) {
|
|
1963
|
+
// WR-01 (plan 60-07): the seam's own reason is carried verbatim rather
|
|
1964
|
+
// than discarded -- this is the one field the two call sites below now
|
|
1965
|
+
// read instead of composing their own generic "does not exist" sentence.
|
|
1966
|
+
const result = { path: null, tried, refusal: seamResolved.refusal };
|
|
1967
|
+
siblingBinaryMemo.set(binaryName, result);
|
|
1968
|
+
return result;
|
|
1969
|
+
}
|
|
1970
|
+
if (seamResolved.path !== null && seamResolved.layer === "file") {
|
|
1971
|
+
const result = { path: seamResolved.path, tried, refusal: null };
|
|
1972
|
+
siblingBinaryMemo.set(binaryName, result);
|
|
1973
|
+
return result;
|
|
1974
|
+
}
|
|
1975
|
+
// Layer 2: the SAME directory the resolved x64sc itself lives in.
|
|
1761
1976
|
const siblingCandidate = join(dirname(resolvedX64scPath), binaryName);
|
|
1762
1977
|
tried.push(siblingCandidate);
|
|
1763
1978
|
if (existsSync(siblingCandidate)) {
|
|
1764
|
-
const result = { path: siblingCandidate, tried };
|
|
1979
|
+
const result = { path: siblingCandidate, tried, refusal: null };
|
|
1765
1980
|
siblingBinaryMemo.set(binaryName, result);
|
|
1766
1981
|
return result;
|
|
1767
1982
|
}
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
// the
|
|
1771
|
-
//
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
siblingBinaryMemo.set(binaryName, result);
|
|
1783
|
-
return result;
|
|
1784
|
-
}
|
|
1983
|
+
// Layer 3: the seam's own exported $PATH walk (resolveOnPath(), the
|
|
1984
|
+
// SECOND of Phase 59 D-02's three coexisting copies to collapse -- plan
|
|
1985
|
+
// 60-01 collapsed the first, inside backend-detect.mts). Mirrors
|
|
1986
|
+
// defaultResolveBinPath()'s own algorithm exactly; the warning stays HERE
|
|
1987
|
+
// rather than inside the seam, because only this function knows the
|
|
1988
|
+
// resolved x64sc a $PATH match is being compared against.
|
|
1989
|
+
const probe = resolveOnPath(binaryName, process.env);
|
|
1990
|
+
tried.push(...probe.tried);
|
|
1991
|
+
if (probe.path) {
|
|
1992
|
+
log?.(`host_tool: "${binaryName}" was not found alongside the resolved x64sc (${resolvedX64scPath}); ` +
|
|
1993
|
+
`falling back to a $PATH match at ${probe.path} -- this may be a DIFFERENT VICE build than the emulator`);
|
|
1994
|
+
const result = { path: probe.path, tried, refusal: null };
|
|
1995
|
+
siblingBinaryMemo.set(binaryName, result);
|
|
1996
|
+
return result;
|
|
1785
1997
|
}
|
|
1786
|
-
const result = { path: null, tried };
|
|
1998
|
+
const result = { path: null, tried, refusal: null };
|
|
1787
1999
|
siblingBinaryMemo.set(binaryName, result);
|
|
1788
2000
|
return result;
|
|
1789
2001
|
}
|
|
@@ -1815,6 +2027,13 @@ export async function runHostTool(raw, deps) {
|
|
|
1815
2027
|
if (request.tool === "oracle.run")
|
|
1816
2028
|
return runOracleRun(request.args, deps);
|
|
1817
2029
|
const repoRootAbs = resolvePath(deps.repoRoot);
|
|
2030
|
+
// Built ONCE, from the already-computed repoRootAbs (PD-06), and passed as
|
|
2031
|
+
// the fourth argument to every buildHostToolArgv() call site below plus
|
|
2032
|
+
// findAcmeLib() and ghidra.installExtension's own pre-materialisation
|
|
2033
|
+
// resolution -- one locator, not a per-branch re-derivation. `here` is
|
|
2034
|
+
// `deps.here`, always undefined in production (HostToolDeps.here's own
|
|
2035
|
+
// doc comment).
|
|
2036
|
+
const hostToolLocator = { toolsDir: join(repoRootAbs, ".c64-re-tools"), projectRoot: repoRootAbs, here: deps.here };
|
|
1818
2037
|
let built;
|
|
1819
2038
|
let acmeLib = null;
|
|
1820
2039
|
// resolveGhidraProject() (below, in the ghidra.analyze branch) RESERVES
|
|
@@ -1853,8 +2072,8 @@ export async function runHostTool(raw, deps) {
|
|
|
1853
2072
|
return { ok: false, message: includeResolved.message };
|
|
1854
2073
|
includePaths.push(includeResolved.path);
|
|
1855
2074
|
}
|
|
1856
|
-
built = buildHostToolArgv(request, { sourcePath: sourceResolved.path, outDirPath, includePaths });
|
|
1857
|
-
acmeLib = findAcmeLib();
|
|
2075
|
+
built = buildHostToolArgv(request, { sourcePath: sourceResolved.path, outDirPath, includePaths }, undefined, hostToolLocator);
|
|
2076
|
+
acmeLib = findAcmeLib(hostToolLocator);
|
|
1858
2077
|
}
|
|
1859
2078
|
else if (request.tool === "ghidra.analyze") {
|
|
1860
2079
|
// Converted from a previous `if (acme.build) … else (ghidra.analyze)`
|
|
@@ -1938,7 +2157,7 @@ export async function runHostTool(raw, deps) {
|
|
|
1938
2157
|
entrypointsPathResolved,
|
|
1939
2158
|
exportPathResolved,
|
|
1940
2159
|
dataRangesPathResolved,
|
|
1941
|
-
});
|
|
2160
|
+
}, undefined, hostToolLocator);
|
|
1942
2161
|
}
|
|
1943
2162
|
else if (request.tool === "dxa.disassemble") {
|
|
1944
2163
|
// (35-01, item 7). `image` and each present optional path resolved
|
|
@@ -1986,7 +2205,7 @@ export async function runHostTool(raw, deps) {
|
|
|
1986
2205
|
entrypointsPath,
|
|
1987
2206
|
datablocksPath,
|
|
1988
2207
|
labelsPath,
|
|
1989
|
-
});
|
|
2208
|
+
}, undefined, hostToolLocator);
|
|
1990
2209
|
}
|
|
1991
2210
|
else if (request.tool === "ghidra.installExtension") {
|
|
1992
2211
|
// `sourceDir`
|
|
@@ -2018,18 +2237,23 @@ export async function runHostTool(raw, deps) {
|
|
|
2018
2237
|
`installation; got ${JSON.stringify(request.args.sourceDir)}, which resolves to ${sourceDirResolved.path}`,
|
|
2019
2238
|
};
|
|
2020
2239
|
}
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2240
|
+
const ghidraResolvedPre = resolveTool("ghidra", {
|
|
2241
|
+
toolsDir: hostToolLocator.toolsDir,
|
|
2242
|
+
projectRoot: hostToolLocator.projectRoot,
|
|
2243
|
+
here: hostToolLocator.here,
|
|
2244
|
+
});
|
|
2245
|
+
if (ghidraResolvedPre.path === null) {
|
|
2023
2246
|
return {
|
|
2024
2247
|
ok: false,
|
|
2025
|
-
message: `host_tool "ghidra.installExtension"
|
|
2248
|
+
message: withRemedy(`host_tool "ghidra.installExtension" refuses: no Ghidra installation directory is known${ghidraResolvedPre.refusal ? `: ${ghidraResolvedPre.refusal}` : ` (tried: ${ghidraResolvedPre.tried.join(", ")})`}`, "ghidra", hostToolLocator.here),
|
|
2026
2249
|
};
|
|
2027
2250
|
}
|
|
2251
|
+
const ghidraHome = ghidraResolvedPre.path;
|
|
2028
2252
|
const sleighPath = join(ghidraHome, "support", "sleigh");
|
|
2029
2253
|
if (!existsSync(sleighPath)) {
|
|
2030
2254
|
return {
|
|
2031
2255
|
ok: false,
|
|
2032
|
-
message: `host_tool "ghidra.installExtension" refuses:
|
|
2256
|
+
message: withRemedy(`host_tool "ghidra.installExtension" refuses: the resolved Ghidra installation directory (${ghidraHome}) does not contain "support/sleigh"`, "ghidra", hostToolLocator.here),
|
|
2033
2257
|
};
|
|
2034
2258
|
}
|
|
2035
2259
|
const stockLanguagesDir = join(ghidraHome, "Ghidra", "Processors", "6502", "data", "languages");
|
|
@@ -2055,7 +2279,7 @@ export async function runHostTool(raw, deps) {
|
|
|
2055
2279
|
message: `host_tool "ghidra.installExtension" failed to materialise the extension at ${installDir}: ${e instanceof Error ? e.message : String(e)}`,
|
|
2056
2280
|
};
|
|
2057
2281
|
}
|
|
2058
|
-
built = buildHostToolArgv(request, { sourceDirPath: sourceDirResolved.path, moduleName: request.args.moduleName });
|
|
2282
|
+
built = buildHostToolArgv(request, { sourceDirPath: sourceDirResolved.path, moduleName: request.args.moduleName }, undefined, hostToolLocator);
|
|
2059
2283
|
}
|
|
2060
2284
|
else {
|
|
2061
2285
|
// request.tool is one of the five c1541.* ids or petcat.decode -- every
|
|
@@ -2080,7 +2304,7 @@ export async function runHostTool(raw, deps) {
|
|
|
2080
2304
|
else {
|
|
2081
2305
|
outDirPath = dirname(imageResolved.path);
|
|
2082
2306
|
}
|
|
2083
|
-
built = buildHostToolArgv(request, { imagePath: imageResolved.path, outDirPath }, deps.log);
|
|
2307
|
+
built = buildHostToolArgv(request, { imagePath: imageResolved.path, outDirPath }, deps.log, hostToolLocator);
|
|
2084
2308
|
}
|
|
2085
2309
|
if (!built.ok) {
|
|
2086
2310
|
// buildHostToolArgv()'s own GHIDRA_HOME/launcher/language preflight
|
|
@@ -2456,7 +2680,7 @@ async function runOracleRun(args, deps) {
|
|
|
2456
2680
|
}
|
|
2457
2681
|
// ---------------------------------------------------------------------------
|
|
2458
2682
|
// CLI entry point (guarded on being the process entry point, the
|
|
2459
|
-
//
|
|
2683
|
+
// entry-point idiom the retired tarball checker also used). Needed for the
|
|
2460
2684
|
// host-local route (no broker in the loop). `node resources/host-tool.mjs
|
|
2461
2685
|
// run --repo-root <path> --request <json>` prints the response as one JSON
|
|
2462
2686
|
// line on stdout and exits non-zero on a refusal.
|