c64bridge 1.0.0 → 1.0.2

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/dist/c64Client.js CHANGED
@@ -18,6 +18,17 @@ import { createLoggingHttpClient } from "./loggingHttpClient.js";
18
18
  import { withDiagnosticSpan, writeDiagnosticEvent } from "./diagnostics.js";
19
19
  import { C64U_NTSC_AUDIO_SAMPLE_RATE, C64U_PAL_AUDIO_SAMPLE_RATE, collectCompleteVideoFrames, parseAudioPacket, parseVideoPacket, } from "./streamCapture.js";
20
20
  import { buildVicBitmapRegisters, resolveVicBitmapMemoryLayout, } from "./vicBitmap.js";
21
+ /** Raised when the KERNAL keyboard buffer did not drain before another chunk
22
+ * could safely be queued. Callers must report this as partial delivery. */
23
+ export class KeyboardQueueDrainTimeoutError extends Error {
24
+ constructor(delivered, requested, timeoutMs) {
25
+ super(`Keyboard queue did not drain after ${timeoutMs}ms; delivered ${delivered} of ${requested} byte(s). Resume the machine and retry the remaining input.`);
26
+ this.name = "KeyboardQueueDrainTimeoutError";
27
+ this.delivered = delivered;
28
+ this.requested = requested;
29
+ this.timeoutMs = timeoutMs;
30
+ }
31
+ }
21
32
  function menuMatrixIncludes(matrix, expected) {
22
33
  // Firmware supplies the character matrix followed by its colour matrix. The
23
34
  // documented wire format is raw bytes, so only inspect the first 40×25
@@ -27,6 +38,38 @@ function menuMatrixIncludes(matrix, expected) {
27
38
  return [screenCodesToAscii(characters), petsciiToAscii(characters)]
28
39
  .some((text) => text.replace(/\s+/g, " ").toUpperCase().includes(normalizedExpected));
29
40
  }
41
+ function selectedMenuRow(matrix) {
42
+ // The firmware contract exposes a 40x25 character matrix followed by its
43
+ // colour matrix. A selected menu row is rendered with one uniform colour
44
+ // distinct from the normal rows. Reject malformed/ambiguous matrices.
45
+ if (matrix.length < 2000)
46
+ return null;
47
+ const colours = matrix.subarray(1000, 2000);
48
+ const rows = [];
49
+ for (let row = 0; row < 25; row += 1) {
50
+ const cells = colours.subarray(row * 40, row * 40 + 40);
51
+ if (cells.length === 40 && cells.every((value) => value === cells[0]))
52
+ rows.push(row);
53
+ }
54
+ if (rows.length === 0)
55
+ return null;
56
+ const normal = new Map();
57
+ for (const row of rows)
58
+ normal.set(colours[row * 40], (normal.get(colours[row * 40]) ?? 0) + 1);
59
+ const [colour, count] = [...normal.entries()].sort((a, b) => b[1] - a[1])[0] ?? [];
60
+ if (colour === undefined || count === undefined)
61
+ return null;
62
+ const selected = rows.filter((row) => colours[row * 40] !== colour);
63
+ return selected.length === 1 ? selected[0] : null;
64
+ }
65
+ function selectedMenuRowIncludes(matrix, expected) {
66
+ const row = selectedMenuRow(matrix);
67
+ if (row === null)
68
+ return false;
69
+ const text = [screenCodesToAscii(matrix.subarray(row * 40, row * 40 + 40)), petsciiToAscii(matrix.subarray(row * 40, row * 40 + 40))]
70
+ .map((value) => value.replace(/\s+/g, " ").toUpperCase());
71
+ return text.some((value) => value.includes(expected.replace(/\s+/g, " ").toUpperCase()));
72
+ }
30
73
  function isMissingEndpoint(error) {
31
74
  return axios.isAxiosError(error) && error.response?.status === 404;
32
75
  }
@@ -101,6 +144,15 @@ export class C64Client {
101
144
  async getBackendType() {
102
145
  return this.getActiveBackendType();
103
146
  }
147
+ /**
148
+ * Snapshot the facade currently active for this invocation. A multi-step
149
+ * flow (pause/read/write/resume, or repeated polling) must resolve the
150
+ * facade once and reuse that same instance for every step, so a concurrent
151
+ * `switchBackend` cannot retarget an operation already underway.
152
+ */
153
+ async pinFacade() {
154
+ return this.facadePromise;
155
+ }
104
156
  async getActiveBackendType() {
105
157
  await this.initPromise;
106
158
  return this.activeType;
@@ -124,6 +176,13 @@ export class C64Client {
124
176
  .then(() => "available")
125
177
  .catch((error) => isMissingEndpoint(error) ? "unavailable" : "unknown");
126
178
  this.machineInputCapabilityProbes.set(facade.type, probe);
179
+ // A transport error is not a capability result. Keep confirmed states,
180
+ // but make a later resource read retry rather than poisoning the cache.
181
+ void probe.then((result) => {
182
+ if (result === "unknown" && this.machineInputCapabilityProbes.get(facade.type) === probe) {
183
+ this.machineInputCapabilityProbes.delete(facade.type);
184
+ }
185
+ });
127
186
  }
128
187
  return { machineInput: await probe, machineMenuScreen: "unknown" };
129
188
  }
@@ -399,9 +458,37 @@ export class C64Client {
399
458
  };
400
459
  }
401
460
  }
461
+ /**
462
+ * Assemble, load, and execute a pure machine-code program. Loading a PRG
463
+ * with LOAD+RUN (the generic path used by {@link runPrg}) is only correct
464
+ * for tokenized BASIC text: RUN misinterprets raw 6510 opcodes as BASIC
465
+ * tokens, and setting BASIC's TXTTAB/VARTAB/ARYTAB/STREND to the code
466
+ * range corrupts the workspace. Instead this loads the assembled bytes
467
+ * without running them, then types `SYS <entry>` to transfer control
468
+ * directly to the first assembled instruction, whatever its address.
469
+ */
402
470
  async uploadAndRunAsm(program) {
403
- const prg = assemblyToPrg(program);
404
- return this.runPrg(prg);
471
+ try {
472
+ const prg = assemblyToPrg(program);
473
+ if (prg.length < 2) {
474
+ return { success: false, details: { message: "Assembled program produced no output" } };
475
+ }
476
+ const entryAddress = prg.readUInt16LE(0);
477
+ const facade = await this.facadePromise;
478
+ const loadResult = await facade.loadPrg(prg);
479
+ if (!loadResult.success) {
480
+ return { success: false, details: loadResult.details };
481
+ }
482
+ const sysCommand = Buffer.from(`SYS${entryAddress}\r`, "ascii");
483
+ const injectResult = await this.injectKeyboardQueue(sysCommand, { pinnedFacade: facade });
484
+ return {
485
+ success: true,
486
+ details: { load: loadResult.details, entryAddress, delivered: injectResult.delivered },
487
+ };
488
+ }
489
+ catch (error) {
490
+ return { success: false, details: this.normaliseError(error) };
491
+ }
405
492
  }
406
493
  async runPrg(prg) {
407
494
  try {
@@ -517,13 +604,13 @@ export class C64Client {
517
604
  return { success: false, details: this.normaliseError(error) };
518
605
  }
519
606
  }
520
- async readMemory(addressInput, lengthInput) {
607
+ async readMemory(addressInput, lengthInput, pinnedFacade) {
521
608
  try {
522
609
  const resolved = resolveAddressSymbol(addressInput);
523
610
  const address = resolved ?? this.parseNumeric(addressInput);
524
611
  const length = this.parseNumeric(lengthInput);
525
612
  validateMemoryRange(address, length);
526
- const rawBytes = await this.readMemoryRaw(address, length);
613
+ const rawBytes = await this.readMemoryRaw(address, length, pinnedFacade);
527
614
  const bytes = rawBytes.slice(0, length);
528
615
  return {
529
616
  success: true,
@@ -541,7 +628,7 @@ export class C64Client {
541
628
  };
542
629
  }
543
630
  }
544
- async writeMemory(addressInput, bytesInput) {
631
+ async writeMemory(addressInput, bytesInput, pinnedFacade) {
545
632
  try {
546
633
  const resolved = resolveAddressSymbol(addressInput);
547
634
  const address = resolved ?? this.parseNumeric(addressInput);
@@ -550,7 +637,7 @@ export class C64Client {
550
637
  // Prefer PUT with hex data for up to 128 bytes; fall back to POST binary for larger writes
551
638
  const addrStr = this.formatAddress(address);
552
639
  try {
553
- const facade = await this.facadePromise;
640
+ const facade = pinnedFacade ?? await this.facadePromise;
554
641
  await facade.writeMemory(address, dataBuffer);
555
642
  return {
556
643
  success: true,
@@ -594,10 +681,10 @@ export class C64Client {
594
681
  }
595
682
  }
596
683
  // --- SID/Music helpers ---
597
- async sidSetVolume(volume) {
684
+ async sidSetVolume(volume, pinnedFacade) {
598
685
  const clamped = Math.max(0, Math.min(15, Math.floor(volume)));
599
686
  const byte = Buffer.from([clamped]);
600
- return this.writeMemory("$D418", this.bytesToHex(byte));
687
+ return this.writeMemory("$D418", this.bytesToHex(byte), pinnedFacade);
601
688
  }
602
689
  async sidReset(hard = false) {
603
690
  try {
@@ -620,7 +707,7 @@ export class C64Client {
620
707
  return { success: false, details: this.normaliseError(error) };
621
708
  }
622
709
  }
623
- async sidNoteOn(options) {
710
+ async sidNoteOn(options, pinnedFacade) {
624
711
  const voice = options.voice ?? 1;
625
712
  if (voice < 1 || voice > 3) {
626
713
  return { success: false, details: { message: "Voice must be 1..3" } };
@@ -653,7 +740,7 @@ export class C64Client {
653
740
  const base = 0xd400 + (voice - 1) * 7;
654
741
  const bytes = Buffer.from([freqLo, freqHi, pwLo, pwHi, ctrl, ad, sr]);
655
742
  try {
656
- const facade = await this.facadePromise;
743
+ const facade = pinnedFacade ?? await this.facadePromise;
657
744
  await facade.writeMemory(base, bytes);
658
745
  return { success: true };
659
746
  }
@@ -661,13 +748,13 @@ export class C64Client {
661
748
  return { success: false, details: this.normaliseError(error) };
662
749
  }
663
750
  }
664
- async sidNoteOff(voice) {
751
+ async sidNoteOff(voice, pinnedFacade) {
665
752
  if (voice < 1 || voice > 3) {
666
753
  return { success: false, details: { message: "Voice must be 1..3" } };
667
754
  }
668
755
  const ctrlAddr = 0xd400 + (voice - 1) * 7 + 4;
669
756
  try {
670
- const facade = await this.facadePromise;
757
+ const facade = pinnedFacade ?? await this.facadePromise;
671
758
  await facade.writeMemory(ctrlAddr, Buffer.from([0x00]));
672
759
  return { success: true };
673
760
  }
@@ -687,26 +774,26 @@ export class C64Client {
687
774
  const facade = await this.facadePromise;
688
775
  return facade.info();
689
776
  }
690
- async pause() {
777
+ async pause(pinnedFacade) {
691
778
  try {
692
- if (await this.shouldUseC64uMockBypass()) {
779
+ if (!pinnedFacade && await this.shouldUseC64uMockBypass()) {
693
780
  const res = await this.api.v1.machinePauseUpdate(":pause");
694
781
  return { success: true, details: res.data };
695
782
  }
696
- const facade = await this.facadePromise;
783
+ const facade = pinnedFacade ?? await this.facadePromise;
697
784
  return await facade.pause();
698
785
  }
699
786
  catch (error) {
700
787
  return { success: false, details: this.normaliseError(error) };
701
788
  }
702
789
  }
703
- async resume() {
790
+ async resume(pinnedFacade) {
704
791
  try {
705
- if (await this.shouldUseC64uMockBypass()) {
792
+ if (!pinnedFacade && await this.shouldUseC64uMockBypass()) {
706
793
  const res = await this.api.v1.machineResumeUpdate(":resume");
707
794
  return { success: true, details: res.data };
708
795
  }
709
- const facade = await this.facadePromise;
796
+ const facade = pinnedFacade ?? await this.facadePromise;
710
797
  return await facade.resume();
711
798
  }
712
799
  catch (error) {
@@ -728,8 +815,12 @@ export class C64Client {
728
815
  * VICE is restarted when it is managed by c64bridge.
729
816
  */
730
817
  async powerCycle() {
818
+ let menuOpened = false;
819
+ let cleanup;
820
+ let pinnedFacade;
731
821
  try {
732
822
  const facade = await this.facadePromise;
823
+ pinnedFacade = facade;
733
824
  if (facade.type === "u2") {
734
825
  const result = await facade.reboot();
735
826
  return { ...result, details: { strategy: "reboot", result: result.details } };
@@ -753,6 +844,7 @@ export class C64Client {
753
844
  if (!menuResult.success) {
754
845
  throw new Error("Unable to open the Ultimate menu for power cycling");
755
846
  }
847
+ menuOpened = true;
756
848
  const initial = await this.readVerifiedMenuScreen(facade, "open Ultimate menu");
757
849
  await this.requirePowerCycleInput(facade, ["f1"]);
758
850
  await this.readVerifiedMenuScreen(facade, "open Tool Menu", "TOOL MENU", initial);
@@ -762,10 +854,21 @@ export class C64Client {
762
854
  await this.requirePowerCycleInput(facade, ["cursor_up_down"]);
763
855
  current = await this.readVerifiedMenuScreen(facade, `move to Power Cycle (${index + 1}/4)`, "POWER CYCLE", current);
764
856
  }
857
+ if (!selectedMenuRowIncludes(current, "POWER CYCLE")) {
858
+ throw new Error("Unable to verify that POWER CYCLE is the selected Tool Menu item; final RETURN was not sent.");
859
+ }
765
860
  await this.requirePowerCycleInput(facade, ["return"]);
766
861
  return { success: true, details: { strategy: "tool_menu", verifiedMenu: "Tool Menu", selectedItem: "Power Cycle" } };
767
862
  }
768
863
  catch (error) {
864
+ if (menuOpened) {
865
+ try {
866
+ cleanup = await pinnedFacade?.menuButton() ?? { success: false, details: "No facade available for menu cleanup" };
867
+ }
868
+ catch (cleanupError) {
869
+ cleanup = { success: false, details: this.normaliseError(cleanupError) };
870
+ }
871
+ }
769
872
  if (isMissingEndpoint(error)) {
770
873
  return {
771
874
  success: false,
@@ -776,7 +879,7 @@ export class C64Client {
776
879
  },
777
880
  };
778
881
  }
779
- return { success: false, details: this.normaliseError(error) };
882
+ return { success: false, details: { error: this.normaliseError(error), cleanup: cleanup ?? { success: false, details: "menu cleanup was not attempted" } } };
780
883
  }
781
884
  }
782
885
  async requirePowerCycleInput(facade, inputs) {
@@ -1588,10 +1691,10 @@ export class C64Client {
1588
1691
  * raw binary bytes or JSON with a base64 payload.
1589
1692
  * Public to allow advanced polling and monitoring use cases.
1590
1693
  */
1591
- async readMemoryRaw(address, length) {
1694
+ async readMemoryRaw(address, length, pinnedFacade) {
1592
1695
  validateMemoryRange(address, length);
1593
1696
  try {
1594
- const facade = await this.facadePromise;
1697
+ const facade = pinnedFacade ?? await this.facadePromise;
1595
1698
  return await facade.readMemory(address, length);
1596
1699
  }
1597
1700
  catch (facadeError) {
@@ -1620,11 +1723,11 @@ export class C64Client {
1620
1723
  * Public so tools that need to drive cross-platform machine state (e.g.
1621
1724
  * keyboard-buffer injection) can avoid backend-specific paths.
1622
1725
  */
1623
- async writeMemoryRaw(address, bytes) {
1726
+ async writeMemoryRaw(address, bytes, pinnedFacade) {
1624
1727
  if (!Number.isInteger(address) || address < 0 || address > 0xffff) {
1625
1728
  throw new Error("Address must be within 0x0000 - 0xFFFF");
1626
1729
  }
1627
- const facade = await this.facadePromise;
1730
+ const facade = pinnedFacade ?? await this.facadePromise;
1628
1731
  const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
1629
1732
  validateMemoryRange(address, buf.length);
1630
1733
  await facade.writeMemory(address, buf);
@@ -1648,32 +1751,39 @@ export class C64Client {
1648
1751
  ? bytes
1649
1752
  : Buffer.from(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
1650
1753
  if (data.length === 0) {
1651
- return;
1754
+ return { delivered: 0 };
1652
1755
  }
1653
1756
  const chunkSize = Math.max(1, Math.min(10, options?.chunkSize ?? 10));
1654
1757
  const pollMs = Math.max(1, options?.drainPollMs ?? 8);
1655
1758
  const totalTimeoutMs = Math.max(50, options?.drainTimeoutMs ?? 2000);
1656
- const facade = await this.facadePromise;
1759
+ const facade = options?.pinnedFacade ?? await this.facadePromise;
1657
1760
  const KEYD = 0x0277; // KERNAL keyboard buffer base
1658
1761
  const NDX = 0x00C6; // pending byte count
1762
+ let delivered = 0;
1659
1763
  for (let offset = 0; offset < data.length; offset += chunkSize) {
1660
1764
  const slice = data.subarray(offset, Math.min(data.length, offset + chunkSize));
1661
1765
  // Write the bytes into the queue first so KERNAL never reads a stale
1662
1766
  // length+old-byte combination.
1663
1767
  await facade.writeMemory(KEYD, Buffer.from(slice));
1664
1768
  await facade.writeMemory(NDX, Buffer.from([slice.length]));
1665
- // Drain: wait for KERNAL to consume the queue. If the machine is paused
1666
- // or otherwise not running, give up after the timeout and continue —
1667
- // higher layers can decide whether that is a failure for their context.
1769
+ // Drain before another write. Overwriting a non-empty KERNAL queue loses
1770
+ // user input, so a timeout is an explicit partial-delivery failure.
1668
1771
  const drainStart = Date.now();
1772
+ let drained = false;
1669
1773
  while (Date.now() - drainStart < totalTimeoutMs) {
1670
1774
  const ndx = await facade.readMemory(NDX, 1);
1671
1775
  if ((ndx[0] ?? 0) === 0) {
1776
+ drained = true;
1672
1777
  break;
1673
1778
  }
1674
1779
  await new Promise((resolve) => setTimeout(resolve, pollMs));
1675
1780
  }
1781
+ delivered += slice.length;
1782
+ if (!drained) {
1783
+ throw new KeyboardQueueDrainTimeoutError(delivered, data.length, totalTimeoutMs);
1784
+ }
1676
1785
  }
1786
+ return { delivered };
1677
1787
  }
1678
1788
  async viceExitMonitor() {
1679
1789
  await this.withViceMonitor((client) => client.exitMonitor());
@@ -1685,6 +1795,9 @@ export class C64Client {
1685
1795
  const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
1686
1796
  await this.withViceMonitor((client) => client.memSet(address, buf));
1687
1797
  }
1798
+ async viceJoyportSet(port, activeLowMask) {
1799
+ await this.withViceMonitor((client) => client.joyportSet(port, activeLowMask));
1800
+ }
1688
1801
  async viceMemGet(address, length) {
1689
1802
  const end = (address + length - 1) & 0xffff;
1690
1803
  return this.withViceMonitor((client) => client.memGet(address, end));
@@ -1883,7 +1996,7 @@ export class C64Client {
1883
1996
  }
1884
1997
  hzToSidFrequency(hz, system = "PAL") {
1885
1998
  const phi2 = system === "PAL" ? 985248 : 1022727;
1886
- const value = Math.round((hz * 65536) / phi2);
1999
+ const value = Math.round((hz * 16777216) / phi2);
1887
2000
  // Clamp to 16-bit
1888
2001
  return Math.max(0, Math.min(0xffff, value));
1889
2002
  }
@@ -2064,7 +2177,7 @@ function buildSingleSpriteProgram(opts) {
2064
2177
  const source = lines.join("\n");
2065
2178
  return assemblyToPrg(source, { fileName: "sprite_gen.asm", loadAddress: 0x0801 });
2066
2179
  }
2067
- function buildPetsciiScreenBasic(opts) {
2180
+ export function buildPetsciiScreenBasic(opts) {
2068
2181
  const border = toByte(opts.borderColor ?? 6); // default blue-ish
2069
2182
  const bg = toByte(opts.backgroundColor ?? 0); // default black
2070
2183
  // Clear screen, set colours, print text starting at 1,1
@@ -2073,7 +2186,7 @@ function buildPetsciiScreenBasic(opts) {
2073
2186
  const program = [
2074
2187
  `10 POKE 53280,${border}:POKE 53281,${bg}:PRINT CHR$(147)`,
2075
2188
  `20 PRINT "${sanitized}"`,
2076
- `30 GETA$:IFA$<>""THENEND:REM wait for key then end`,
2189
+ `30 GETA$:IFA$=""THEN30:END:REM wait for key then end`,
2077
2190
  ].join("\n");
2078
2191
  return program;
2079
2192
  }
@@ -2099,11 +2212,11 @@ export function buildPrinterBasicProgram(opts) {
2099
2212
  ln += 10;
2100
2213
  continue;
2101
2214
  }
2102
- const chunks = chunkString(logical, 60);
2215
+ const chunks = chunkString(logical, 45);
2103
2216
  for (let i = 0; i < chunks.length; i += 1) {
2104
- const chunk = escapeBasicQuotes(chunks[i]);
2217
+ const chunk = basicStringExpression(chunks[i]);
2105
2218
  const tail = i < chunks.length - 1 ? ";" : ""; // avoid CR between chunks within the same logical line
2106
- lines.push(`${ln} PRINT#1,"${chunk}"${tail}`);
2219
+ lines.push(`${ln} PRINT#1,${chunk}${tail}`);
2107
2220
  ln += 10;
2108
2221
  }
2109
2222
  }
@@ -2127,9 +2240,10 @@ function chunkString(input, maxLen) {
2127
2240
  }
2128
2241
  return parts;
2129
2242
  }
2130
- function escapeBasicQuotes(input) {
2131
- // In Commodore BASIC, embed a double quote by doubling it
2132
- return input.replace(/"/g, '""');
2243
+ function basicStringExpression(input) {
2244
+ // BASIC V2 has no doubled-quote escaping. Splice literal quote characters
2245
+ // through CHR$(34), including leading/trailing/consecutive quote cases.
2246
+ return input.split('"').map((part) => `"${part}"`).join(";CHR$(34);");
2133
2247
  }
2134
2248
  export function buildCommodoreBitmapBasicProgram(opts) {
2135
2249
  const repeats = Math.max(1, Math.floor(opts.repeats ?? 1));
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
+ import os from "node:os";
3
4
  import { fileURLToPath } from "url";
4
5
  const DEFAULT_HOST = "c64u";
5
6
  const DEFAULT_PORT = 80;
@@ -8,16 +9,16 @@ const DEFAULT_CONFIG = {
8
9
  baseUrl: buildBaseUrl(DEFAULT_HOST, DEFAULT_PORT),
9
10
  c64_port: DEFAULT_PORT,
10
11
  vicePrewarm: false,
12
+ backendConfig: {},
11
13
  };
12
- let cachedConfig = null;
13
14
  export function loadConfig() {
14
- if (cachedConfig) {
15
- return cachedConfig;
16
- }
17
15
  const configPath = process.env.C64BRIDGE_CONFIG;
18
- const repoConfigPath = join(dirname(fileURLToPath(import.meta.url)), "..", ".c64bridge.json");
19
- const homeConfigPath = process.env.HOME ? `${process.env.HOME}/.c64bridge.json` : null;
20
- const candidatePaths = [configPath, repoConfigPath, homeConfigPath];
16
+ const cwdConfigPath = join(process.cwd(), ".c64bridge.json");
17
+ const homeConfigPath = join(os.homedir(), ".c64bridge.json");
18
+ // Legacy package-adjacent config is deliberately last: it is useful for
19
+ // source checkouts but must never override a project or user config.
20
+ const legacyPackageConfigPath = join(dirname(fileURLToPath(import.meta.url)), "..", ".c64bridge.json");
21
+ const candidatePaths = [configPath, cwdConfigPath, homeConfigPath, legacyPackageConfigPath];
21
22
  let rawConfig;
22
23
  for (const candidatePath of candidatePaths) {
23
24
  if (!candidatePath) {
@@ -29,7 +30,9 @@ export function loadConfig() {
29
30
  }
30
31
  catch (error) {
31
32
  if (error.code !== "ENOENT") {
32
- throw error;
33
+ // Deliberately omit file contents/credentials. Configuration mistakes
34
+ // should not prevent the MCP server from exposing a recoverable path.
35
+ console.error(`[c64bridge] ignoring configuration '${candidatePath}': ${error instanceof Error ? error.message : String(error)}`);
33
36
  }
34
37
  }
35
38
  }
@@ -80,8 +83,12 @@ export function loadConfig() {
80
83
  c64_port: port,
81
84
  networkPassword: firstDefined(configuredString(useU2 ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(hardware?.networkPassword), configuredString(rawConfig?.networkPassword)),
82
85
  vicePrewarm: firstDefined(configuredBoolean(process.env.VICE_PREWARM), configuredBoolean(vice?.prewarm), configuredBoolean(rawConfig?.vicePrewarm)) ?? false,
86
+ backendConfig: {
87
+ ...(rawConfig?.c64u && typeof rawConfig.c64u === "object" && !Array.isArray(rawConfig.c64u) ? { c64u: rawConfig.c64u } : {}),
88
+ ...(rawConfig?.u2 && typeof rawConfig.u2 === "object" && !Array.isArray(rawConfig.u2) ? { u2: rawConfig.u2 } : {}),
89
+ ...(rawConfig?.vice && typeof rawConfig.vice === "object" && !Array.isArray(rawConfig.vice) ? { vice: rawConfig.vice } : {}),
90
+ },
83
91
  };
84
- cachedConfig = config;
85
92
  return config;
86
93
  }
87
94
  function configuredString(value) {
@@ -166,5 +173,6 @@ function formatHost(host) {
166
173
  return host;
167
174
  }
168
175
  export function __resetConfigCacheForTests() {
169
- cachedConfig = null;
176
+ // Kept as a no-op compatibility hook: config is intentionally reloaded for
177
+ // each construction so startup and facade creation cannot disagree.
170
178
  }