@henols/c64-re-tools 2.0.0-rc.8 → 2.0.0-rc.9

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/README.md CHANGED
@@ -27,6 +27,8 @@ Start the Host Runtime and leave it running:
27
27
  npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host
28
28
  ```
29
29
 
30
+ Add `--log-console` to print each request and each c1541 or petcat run, or `--log-dir <directory>` to write them to a file. Both are off by default.
31
+
30
32
  Install into your project and start the agent:
31
33
 
32
34
  ```
@@ -64,7 +66,7 @@ The Feature installs ACME, dxa, Java 21 and Ghidra, puts the toolkit into the pr
64
66
  | `C64RT_GHIDRA` | skills, CLI | The Ghidra installation directory, when `analyzeHeadless` is not on `PATH`. |
65
67
  | `C64RT_HOST` | MCP, skills, CLI | `host:port` of the Host Runtime when it is not on 127.0.0.1, `host.docker.internal` or `host.containers.internal` at 6464. |
66
68
  | `C64RT_VIDEO` | MCP | `pal` (default) or `ntsc`, fixed for the life of the MCP process. |
67
- | `C64RT_TRACE` | host, MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debug-install.md](docs/debug-install.md). Nothing of the trace reaches the agent. |
69
+ | `C64RT_TRACE` | MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debug-install.md](docs/debug-install.md). Nothing of the trace reaches the agent. The host uses `--log-dir` instead. |
68
70
 
69
71
  Ghidra runs with its own settings directory for each request. The toolkit
70
72
  brings its own NMOS 6510 language (all 256 opcodes); your Ghidra installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.8",
3
+ "version": "2.0.0-rc.9",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
package/src/host/main.ts CHANGED
@@ -15,7 +15,7 @@ import { PACKAGE } from "../package.ts";
15
15
  const HELP = `c64-re-tools-host
16
16
 
17
17
  Usage:
18
- npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host [--port <port>]
18
+ npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host [--port <port>] [--log-console] [--log-dir <directory>]
19
19
  c64-re-tools-host --help
20
20
 
21
21
  Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
@@ -27,8 +27,15 @@ connected c64-re-tools-mcp process, and runs the tools that come with VICE
27
27
  emulator and tool it started.
28
28
 
29
29
  Options:
30
- --port <port> Listen on this port instead (0 picks a free port).
31
- --help Show this help.`;
30
+ --port <port> Listen on this port instead (0 picks a free port).
31
+ --log-console Also print each request with its result, and each run
32
+ of c1541 or petcat, to the console.
33
+ --log-dir <directory> Write the same events as JSON lines to a file in this
34
+ directory, with a copy of each VICE log.
35
+ --help Show this help.
36
+
37
+ Without --log-console and --log-dir the host logs only its start, its stop,
38
+ connections and failures. C64RT_TRACE does not apply to the host.`;
32
39
 
33
40
  function log(line: string): void {
34
41
  process.stderr.write(`c64-re-tools-host: ${line}\n`);
@@ -36,9 +43,12 @@ function log(line: string): void {
36
43
  }
37
44
 
38
45
  async function main(): Promise<number> {
39
- let values: { help?: boolean; port?: string };
46
+ let values: { help?: boolean; port?: string; "log-console"?: boolean; "log-dir"?: string };
40
47
  try {
41
- ({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" } }, strict: true }));
48
+ ({ values } = parseArgs({
49
+ options: { help: { type: "boolean" }, port: { type: "string" }, "log-console": { type: "boolean" }, "log-dir": { type: "string" } },
50
+ strict: true,
51
+ }));
42
52
  } catch (error) {
43
53
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP}\n`);
44
54
  return 2;
@@ -47,7 +57,18 @@ async function main(): Promise<number> {
47
57
  process.stdout.write(`${HELP}\n`);
48
58
  return 0;
49
59
  }
50
- startTrace("host", { warn: log });
60
+ const logDir = values["log-dir"];
61
+ if (logDir === "") {
62
+ process.stderr.write("--log-dir needs a directory.\n");
63
+ return 2;
64
+ }
65
+ startTrace("host", {
66
+ target: {
67
+ ...(logDir === undefined ? {} : { dir: { path: logDir, given: `--log-dir ${logDir}` } }),
68
+ ...(values["log-console"] ? { echo: (line: string) => process.stderr.write(`c64-re-tools-host: ${line}\n`) } : {}),
69
+ },
70
+ warn: log,
71
+ });
51
72
 
52
73
  const port = values.port === undefined ? DEFAULT_HOST_PORT : parsePort(values.port);
53
74
  if (port === undefined) {
package/src/trace.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  // An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
2
- // in the environment, each process appends its events as JSON lines to a file
3
- // of its own in that directory; unset, every call does nothing. The trace is
4
- // for people: nothing of it reaches the agent, and no result depends on it.
2
+ // in the environment (the host: --log-dir), each process appends its events as
3
+ // JSON lines to a file of its own in that directory. The host can also echo
4
+ // each event to the console (--log-console). Off, every call does nothing. The
5
+ // trace is for people: nothing of it reaches the agent, and no result depends on it.
5
6
 
6
7
  import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
7
8
  import { join } from "node:path";
@@ -66,14 +67,16 @@ export class Trace {
66
67
  readonly kind: TraceKind;
67
68
  /** The directory of the trace, or undefined when the trace is off. */
68
69
  readonly dir: string | undefined;
69
- /** The file this trace appends to, or undefined when the trace is off. */
70
+ /** The file this trace appends to, or undefined when the trace writes no file. */
70
71
  readonly file: string | undefined;
72
+ readonly #echo: ((line: string) => void) | undefined;
71
73
  #broken = false;
72
74
 
73
- private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
75
+ private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined, echo?: (line: string) => void) {
74
76
  this.kind = kind;
75
77
  this.dir = dir;
76
78
  this.file = file;
79
+ this.#echo = echo;
77
80
  }
78
81
 
79
82
  /** A trace that records nothing. */
@@ -81,31 +84,46 @@ export class Trace {
81
84
  return new Trace(kind, undefined, undefined);
82
85
  }
83
86
 
87
+ /** A trace that writes no file and gives each event to `echo` as one line. */
88
+ static echoing(kind: TraceKind, echo: (line: string) => void): Trace {
89
+ return new Trace(kind, undefined, undefined, echo);
90
+ }
91
+
84
92
  /**
85
93
  * A trace into `dir`, created when it is missing. The file is
86
94
  * `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
87
95
  * Throws when the directory cannot be used.
88
96
  */
89
- static open(kind: TraceKind, dir: string, now = new Date()): Trace {
97
+ static open(kind: TraceKind, dir: string, now = new Date(), echo?: (line: string) => void): Trace {
90
98
  mkdirSync(dir, { recursive: true });
91
- const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
99
+ const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`), echo);
92
100
  trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
93
101
  return trace;
94
102
  }
95
103
 
96
104
  get on(): boolean {
105
+ return this.#writes || this.#echo !== undefined;
106
+ }
107
+
108
+ get #writes(): boolean {
97
109
  return this.file !== undefined && !this.#broken;
98
110
  }
99
111
 
100
- /** Appends one event. A write that fails turns the trace off; nothing throws. */
112
+ /**
113
+ * Records one event: appends it to the file, and gives it to the echo as
114
+ * "<event> <fields as JSON>". The echo skips `log` events, which the program
115
+ * prints itself. A write that fails stops the file; nothing throws.
116
+ */
101
117
  event(name: string, fields: TraceFields = {}): void {
102
118
  if (!this.on) return;
103
- const line: Record<string, unknown> = { t: isoCet(), kind: this.kind, event: name };
119
+ const summary: Record<string, unknown> = {};
104
120
  for (const [key, value] of Object.entries(fields)) {
105
- if (!RESERVED.has(key) && value !== undefined) line[key] = summarize(value);
121
+ if (!RESERVED.has(key) && value !== undefined) summary[key] = summarize(value);
106
122
  }
123
+ if (this.#echo !== undefined && name !== "log") this.#echo(`${name} ${JSON.stringify(summary)}`);
124
+ if (!this.#writes) return;
107
125
  try {
108
- appendFileSync(this.file!, `${JSON.stringify(line)}\n`);
126
+ appendFileSync(this.file!, `${JSON.stringify({ t: isoCet(), kind: this.kind, event: name, ...summary })}\n`);
109
127
  } catch {
110
128
  this.#broken = true;
111
129
  }
@@ -121,7 +139,7 @@ export class Trace {
121
139
  * trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
122
140
  */
123
141
  keep(source: string, name: string): void {
124
- if (!this.on) return;
142
+ if (!this.#writes) return;
125
143
  const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
126
144
  try {
127
145
  copyFileSync(source, target);
@@ -135,23 +153,37 @@ export class Trace {
135
153
  // Off until a program calls startTrace; the kind is a placeholder until then.
136
154
  let current = Trace.off("script");
137
155
 
156
+ /** Where the trace of a process goes: a directory for its file, and an echo for each event. */
157
+ export interface TraceTarget {
158
+ /** The directory, and how the user gave it (`--log-dir <path>`, for example) for a warning. */
159
+ dir?: { path: string; given: string };
160
+ echo?: (line: string) => void;
161
+ }
162
+
163
+ /** The target that C64RT_TRACE in `env` gives: its directory, or nothing. */
164
+ export function traceTargetFromEnv(env: NodeJS.ProcessEnv = process.env): TraceTarget {
165
+ const path = env[TRACE_VARIABLE];
166
+ return path === undefined || path === "" ? {} : { dir: { path, given: `${TRACE_VARIABLE}=${path}` } };
167
+ }
168
+
138
169
  /**
139
- * Switches the trace of this process on when `env` (the environment by
140
- * default) has C64RT_TRACE, and off otherwise. A directory that cannot be used
141
- * leaves the trace off and reports that through `warn`. Called once, at the
142
- * start of a program.
170
+ * Switches the trace of this process on for `target` (by default what
171
+ * C64RT_TRACE in `env` gives), and off when the target is empty. A directory
172
+ * that cannot be used writes no file and reports that through `warn`; an echo
173
+ * stays on. Called once, at the start of a program.
143
174
  */
144
- export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
145
- const dir = (options.env ?? process.env)[TRACE_VARIABLE];
146
- if (dir === undefined || dir === "") {
147
- current = Trace.off(kind);
175
+ export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; target?: TraceTarget; warn?: (line: string) => void } = {}): Trace {
176
+ const { dir, echo } = options.target ?? traceTargetFromEnv(options.env);
177
+ const noFile = () => (echo === undefined ? Trace.off(kind) : Trace.echoing(kind, echo));
178
+ if (dir === undefined) {
179
+ current = noFile();
148
180
  return current;
149
181
  }
150
182
  try {
151
- current = Trace.open(kind, dir);
183
+ current = Trace.open(kind, dir.path, new Date(), echo);
152
184
  } catch (error) {
153
- current = Trace.off(kind);
154
- options.warn?.(`${TRACE_VARIABLE}=${dir} cannot be used (${error instanceof Error ? error.message : String(error)}); the trace is off.`);
185
+ current = noFile();
186
+ options.warn?.(`${dir.given} cannot be used (${error instanceof Error ? error.message : String(error)}); no trace file is written.`);
155
187
  }
156
188
  return current;
157
189
  }