@henols/vice-mcp 0.1.4
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 +66 -0
- package/build.ts +268 -0
- package/container-guard.mts +226 -0
- package/containerpath.ts +296 -0
- package/hostpath.ts +318 -0
- package/incident-record.ts +442 -0
- package/install-resources.ts +532 -0
- package/package.json +71 -0
- package/refresh-manifest.ts +103 -0
- package/repo-root.ts +198 -0
- package/resources/broker-control.mjs +343 -0
- package/resources/broker-epoch.mjs +126 -0
- package/resources/broker-kill.mjs +491 -0
- package/resources/broker-launch.mjs +659 -0
- package/resources/broker-state.mjs +173 -0
- package/resources/container-guard.mjs +211 -0
- package/resources/vice-broker.mjs +855 -0
- package/resources/vice-launcher.sh +169 -0
- package/tools-manifest.json +1231 -0
- package/vice-broker-client.ts +899 -0
- package/vice-probe.ts +278 -0
- package/vice-proxy.ts +3093 -0
- package/vice-sync.ts +336 -0
- package/vice.ts +772 -0
package/vice-sync.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The project's checkpoint-synchronisation primitives -- the third module in
|
|
3
|
+
// this skill, alongside tools/vice.mjs (the resilient transport seam) and
|
|
4
|
+
// vice-probe.mjs (the deliberately-fragile liveness probe). Those two must
|
|
5
|
+
// never be merged, for the reasons vice-probe.mjs's own header already
|
|
6
|
+
// records; synchronisation is a third such job -- different concern, own
|
|
7
|
+
// module, structurally isolated. Land here cold and know not to fold this
|
|
8
|
+
// into either of them: the seam retries transport failures until a call
|
|
9
|
+
// eventually succeeds, the probe answers one fast yes/no question, and this
|
|
10
|
+
// module decides WHERE in emulated time the machine stops and therefore what
|
|
11
|
+
// bytes become a captured artifact -- a different, load-bearing job from
|
|
12
|
+
// either.
|
|
13
|
+
//
|
|
14
|
+
// Three measurements shaped every function below. Re-verify them against
|
|
15
|
+
// .planning/STATE.md rather than trusting this comment if they ever need
|
|
16
|
+
// re-checking:
|
|
17
|
+
// - `vice_execution_run` is the call the host server dies on -- six
|
|
18
|
+
// outages in one session, the last three all on that call. The resume
|
|
19
|
+
// count is therefore the risk every wait here minimises.
|
|
20
|
+
// - `vice_ping` is the one call measured NON-pausing -- 986,693 cycles/s
|
|
21
|
+
// while ping-polling versus 991,569 fully quiet. Every other
|
|
22
|
+
// state-reading vice_* call pauses the emulator and does not resume it.
|
|
23
|
+
// - The machine is usually ALREADY paused when a checkpoint is armed
|
|
24
|
+
// (every checkpoint stop leaves it paused, and every state read pauses
|
|
25
|
+
// it), which is why every wait keys on the checkpoint's own hit count
|
|
26
|
+
// rather than on whether execution is paused.
|
|
27
|
+
//
|
|
28
|
+
// Three invariants a maintainer must not break:
|
|
29
|
+
// 1. Exactly one resume (`vice_execution_run`) per wait.
|
|
30
|
+
// 2. Never poll on whether execution is paused -- poll on the
|
|
31
|
+
// checkpoint's own `hit_count`.
|
|
32
|
+
// 3. Never delete a checkpoint VICE marked `temporary`.
|
|
33
|
+
//
|
|
34
|
+
// These three invariants are also precisely why 01.6.1-06 cannot cover
|
|
35
|
+
// readCheckpoint()/waitCheckpointHit()/runToCheckpoint()/reset()/
|
|
36
|
+
// screenshot() with a unit test: each is only meaningful against a real
|
|
37
|
+
// emulator's timing (a stub server answering fast and deterministically
|
|
38
|
+
// would prove nothing about a resume count or a hit_count race), and
|
|
39
|
+
// mcp__vice__* is this project's only permitted route to that emulator --
|
|
40
|
+
// a test process cannot open its own connection (CLAUDE.md's hard rule).
|
|
41
|
+
// vice-sync.test.ts records this as five named `todo` entries rather than
|
|
42
|
+
// faking it with a stub. Everything else below (addrNum, hex4, the two
|
|
43
|
+
// timing constants, the armedCheckpoints tracker) is pure or near-pure and
|
|
44
|
+
// IS covered for real.
|
|
45
|
+
//
|
|
46
|
+
// `tryHostPaths` (used by screenshot() below) comes from the sibling
|
|
47
|
+
// `devcontainer-host-path` skill -- and that is not a new dependency this
|
|
48
|
+
// module introduces. This module tree's own resource-deployment path already
|
|
49
|
+
// pulls it in: `vice.mjs` statically imports `repo-root.ts`, which statically
|
|
50
|
+
// imports `install-resources.ts`, which imports
|
|
51
|
+
// `../../skills/devcontainer-host-path/scripts/hostpath.mjs` -- a mandatory
|
|
52
|
+
// edge on every entry into this tree. screenshot() is simply its second
|
|
53
|
+
// consumer. A future maintainer should neither believe this module introduced
|
|
54
|
+
// that edge nor "fix" it by hand-rolling a second path translator.
|
|
55
|
+
//
|
|
56
|
+
// This module's OWN `repo-root.ts` import (below) is new as of 01.6.1-02
|
|
57
|
+
// (RESEARCH §3.4 Option B): hostpath.ts no longer resolves the workspace
|
|
58
|
+
// root itself, so every caller of tryHostPaths()/hostPath() now threads it
|
|
59
|
+
// through explicitly. vice-sync.mjs is NOT a member of the repo-root cycle
|
|
60
|
+
// (repo-root.ts -> install-resources.ts -> hostpath.ts -> repo-root.ts)
|
|
61
|
+
// -- it only calls repoRoot() lazily, inside screenshot(), well after every
|
|
62
|
+
// cycle member has already finished evaluating -- but this import is exactly
|
|
63
|
+
// the kind of fresh route load-order.test.ts's module-scope call-site guard
|
|
64
|
+
// (Part 3, added in 01.6.1-02) exists to police: a future top-level,
|
|
65
|
+
// unguarded `repoRoot()` call added here would rebuild the TDZ hazard by a
|
|
66
|
+
// new path even though the three-module cycle itself is gone.
|
|
67
|
+
import { mkdirSync } from "node:fs";
|
|
68
|
+
import { dirname } from "node:path";
|
|
69
|
+
|
|
70
|
+
import { call } from "./vice.ts";
|
|
71
|
+
import { tryHostPaths } from "./hostpath.ts";
|
|
72
|
+
import { repoRoot } from "./repo-root.ts";
|
|
73
|
+
|
|
74
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalise an address to a number, accepting either a number or a string in
|
|
78
|
+
* "$08B1" / "08B1" / "0x08B1" form. Addresses cross a JSON boundary (the
|
|
79
|
+
* registry stores them as "$08B1" strings for human readability) and a raw
|
|
80
|
+
* hex4() over a string silently produces "$$08B1", which VICE rejects with
|
|
81
|
+
* "invalid hex address" -- so every address entering a vice_* call goes
|
|
82
|
+
* through here first.
|
|
83
|
+
*/
|
|
84
|
+
export function addrNum(a: number | string): number {
|
|
85
|
+
if (typeof a === "number") return a;
|
|
86
|
+
if (typeof a === "string") {
|
|
87
|
+
const s = a.trim().replace(/^\$/, "").replace(/^0x/i, "");
|
|
88
|
+
const n = parseInt(s, 16);
|
|
89
|
+
if (Number.isFinite(n)) return n;
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`addrNum: cannot interpret ${JSON.stringify(a)} as an address`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const hex4 = (n: number | string): string => `$${addrNum(n).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
95
|
+
|
|
96
|
+
// Each poll cycle is: read state (which PAUSES the machine), resume, then let
|
|
97
|
+
// it run for one window. The window is not idle waiting -- it is the only
|
|
98
|
+
// interval in which the emulated CPU actually advances, so a short window
|
|
99
|
+
// starves the machine and the trigger appears to "never fire". A KERNAL cold
|
|
100
|
+
// boot plus a turbo-loader disk load needs tens of emulated seconds.
|
|
101
|
+
//
|
|
102
|
+
// Progressively longer run windows, in ms. Rationale, and it is not just about
|
|
103
|
+
// speed: a `stop:true` checkpoint halts the machine exactly at the trigger
|
|
104
|
+
// whether we notice 2 seconds later or 30, so POLLING FREQUENCY HAS NO EFFECT
|
|
105
|
+
// ON WHERE THE MACHINE STOPS. Polling rarely is therefore strictly better --
|
|
106
|
+
// identical determinism, an order of magnitude fewer monitor enter/exit
|
|
107
|
+
// transitions. That matters because the host server has dropped its connection
|
|
108
|
+
// five times in one session, always during a monitor transition
|
|
109
|
+
// (`vice_execution_run` or checkpoint work), so transition count is the one
|
|
110
|
+
// risk factor we control. This schedule spans ~150s of emulated running in 8
|
|
111
|
+
// round-trips instead of ~60.
|
|
112
|
+
//
|
|
113
|
+
// Typed as a readonly array (01.6.1-06): a later mutation (push/splice/index
|
|
114
|
+
// assignment) is now a compile error rather than a silent runtime change to
|
|
115
|
+
// a timing contract other code reasons about.
|
|
116
|
+
export const POLL_WINDOWS_MS: readonly number[] = [3000, 6000, 12000, 20000, 25000, 28000, 28000, 28000];
|
|
117
|
+
// How often to ask `vice_ping` whether the machine has stopped yet. Ping is
|
|
118
|
+
// free (it does not pause the machine), so this only costs a round-trip.
|
|
119
|
+
export const PING_INTERVAL_MS: number = 1000;
|
|
120
|
+
|
|
121
|
+
// NOTE: a `waitPaused()` helper used to live here, polling vice_ping until
|
|
122
|
+
// execution reported "paused". It is deliberately DELETED, not kept "just in
|
|
123
|
+
// case". It was wrong in a way that produced a silently-wrong capture point:
|
|
124
|
+
// the machine is normally ALREADY paused when we arm a checkpoint (every
|
|
125
|
+
// checkpoint stop leaves it paused, and every state read pauses it), so the
|
|
126
|
+
// poll returned instantly without any transition having occurred, and the
|
|
127
|
+
// caller then read hit_count 0 and either refused or captured from the wrong
|
|
128
|
+
// place. Wait on the checkpoint's own hit_count instead -- see
|
|
129
|
+
// waitCheckpointHit below. Do not reintroduce a paused-poll.
|
|
130
|
+
|
|
131
|
+
/** The shape of one entry from vice_checkpoint_list/vice_checkpoint_add --
|
|
132
|
+
* loosely typed (`start` accepts either form addrNum() itself accepts,
|
|
133
|
+
* unknown fields pass through) since this module only ever reads
|
|
134
|
+
* checkpoint_num/start/hit_count/temporary off it, never asserts a closed
|
|
135
|
+
* shape. */
|
|
136
|
+
export interface Checkpoint {
|
|
137
|
+
checkpoint_num: number;
|
|
138
|
+
start?: number | string;
|
|
139
|
+
hit_count?: number;
|
|
140
|
+
temporary?: boolean;
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Checkpoints this harness itself armed for its own reasons (a boot gate, the
|
|
145
|
+
// dump trigger), tracked here so assertSameMachine()'s checkpoint-fallback
|
|
146
|
+
// probe (D-3) has something to check when no supervisor epoch file exists --
|
|
147
|
+
// the ONLY identity signal available in that case. This costs no NEW
|
|
148
|
+
// checkpoints: arming a sentinel checkpoint purely for identity-probing was
|
|
149
|
+
// rejected because checkpoint work is itself one of the two leading crash
|
|
150
|
+
// suspects recorded in STATE.md's HAZARD CANDIDATE entry. Added on
|
|
151
|
+
// vice_checkpoint_add success, removed on successful vice_checkpoint_delete.
|
|
152
|
+
//
|
|
153
|
+
// A SINGLETON object, not a bare Set and not a factory: there is one machine
|
|
154
|
+
// per process, and both this module's runToCheckpoint() and
|
|
155
|
+
// tools/recover.mjs's capture() -- which deliberately hand-rolls its own
|
|
156
|
+
// arm/wait/delete so it can interleave the identity check and the held-key
|
|
157
|
+
// release between the wait and the delete -- must register ids in the same
|
|
158
|
+
// place. One source of truth, both callers writing through one door.
|
|
159
|
+
const armedCheckpointIds = new Set<number>();
|
|
160
|
+
|
|
161
|
+
export interface ArmedCheckpointTracker {
|
|
162
|
+
track(id: number): void;
|
|
163
|
+
untrack(id: number): void;
|
|
164
|
+
ids(): number[];
|
|
165
|
+
clear(): void;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const armedCheckpoints: ArmedCheckpointTracker = {
|
|
169
|
+
track(id: number): void {
|
|
170
|
+
armedCheckpointIds.add(id);
|
|
171
|
+
},
|
|
172
|
+
untrack(id: number): void {
|
|
173
|
+
armedCheckpointIds.delete(id);
|
|
174
|
+
},
|
|
175
|
+
ids(): number[] {
|
|
176
|
+
return [...armedCheckpointIds];
|
|
177
|
+
},
|
|
178
|
+
clear(): void {
|
|
179
|
+
armedCheckpointIds.clear();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Arm an exec checkpoint at `addr`, resume, and wait for the machine to stop
|
|
185
|
+
* ON THAT CHECKPOINT -- verified via its own hit_count, not inferred from the
|
|
186
|
+
* mere fact that execution paused. Returns the checkpoint id so the caller can
|
|
187
|
+
* delete it; leaving stale checkpoints armed would contaminate the next stage.
|
|
188
|
+
*
|
|
189
|
+
* This is the project's one synchronisation primitive. Every wait in this file
|
|
190
|
+
* is a checkpoint hit, never an elapsed duration -- a duration cannot be
|
|
191
|
+
* re-armed, and success criterion 1's byte-identical claim depends on the stop
|
|
192
|
+
* point being re-armable.
|
|
193
|
+
*
|
|
194
|
+
* NOT unit-tested (01.6.1-06): needs a real emulator's vice_checkpoint_list
|
|
195
|
+
* to exercise meaningfully; see vice-sync.test.ts's named todo entry.
|
|
196
|
+
*/
|
|
197
|
+
export async function readCheckpoint(cpId: number | null, addr: number): Promise<Checkpoint | undefined> {
|
|
198
|
+
const { checkpoints } = (await call("vice_checkpoint_list", {})) as { checkpoints: Checkpoint[] };
|
|
199
|
+
return (
|
|
200
|
+
checkpoints.find((c) => c.checkpoint_num === cpId) ||
|
|
201
|
+
checkpoints.find((c) => c.start !== undefined && addrNum(c.start) === addr)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Wait for a checkpoint using exactly ONE resume.
|
|
207
|
+
*
|
|
208
|
+
* `vice_execution_run` is the call this host server dies on -- six outages in
|
|
209
|
+
* one session, the last three all on that call -- so the resume count is the
|
|
210
|
+
* risk we minimise. The lever is a measurement from the speed trials:
|
|
211
|
+
* `vice_ping` does NOT pause the machine (ping-polling sustained 986,693
|
|
212
|
+
* cycles/s against 991,569 for a completely quiet machine), whereas
|
|
213
|
+
* `vice_checkpoint_list` does. So we can watch progress with ping, for free,
|
|
214
|
+
* and resume only once instead of once per window -- an ~8x cut in the
|
|
215
|
+
* offending call.
|
|
216
|
+
*
|
|
217
|
+
* Order matters and is the fix for an earlier bug: check hit_count BEFORE
|
|
218
|
+
* resuming (the machine is often already stopped on the checkpoint, and blindly
|
|
219
|
+
* resuming would run straight past the dump point), then resume, then wait for
|
|
220
|
+
* `paused`, then CONFIRM via hit_count that the stop was actually this
|
|
221
|
+
* checkpoint rather than something else.
|
|
222
|
+
*
|
|
223
|
+
* NOT unit-tested (01.6.1-06): the exactly-one-resume and poll-on-hit_count
|
|
224
|
+
* invariants only mean something against a real emulator's timing; see
|
|
225
|
+
* vice-sync.test.ts's named todo entry.
|
|
226
|
+
*/
|
|
227
|
+
export async function waitCheckpointHit(cpId: number | null, addr: number, label: string): Promise<Checkpoint> {
|
|
228
|
+
// Already fired? Then we are standing on the trigger -- never resume past it.
|
|
229
|
+
const pre = await readCheckpoint(cpId, addr);
|
|
230
|
+
if (pre && (pre.hit_count ?? 0) >= 1) return pre;
|
|
231
|
+
|
|
232
|
+
await call("vice_execution_run", {}); // the single resume
|
|
233
|
+
const budgetMs = POLL_WINDOWS_MS.reduce((a, b) => a + b, 0);
|
|
234
|
+
const deadline = Date.now() + budgetMs;
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
await sleep(PING_INTERVAL_MS);
|
|
237
|
+
const p = (await call("vice_ping", {})) as { execution?: string };
|
|
238
|
+
if (p.execution !== "paused") continue;
|
|
239
|
+
const cp = await readCheckpoint(cpId, addr);
|
|
240
|
+
if (cp && (cp.hit_count ?? 0) >= 1) return cp;
|
|
241
|
+
// Paused for some other reason: resume and keep waiting. Rare, and we
|
|
242
|
+
// deliberately do not treat a bare pause as the trigger.
|
|
243
|
+
await call("vice_execution_run", {});
|
|
244
|
+
}
|
|
245
|
+
// Deadline passed -- one last read before giving up, in case the checkpoint
|
|
246
|
+
// fired between the final ping and now.
|
|
247
|
+
const last = await readCheckpoint(cpId, addr);
|
|
248
|
+
if (last && (last.hit_count ?? 0) >= 1) return last;
|
|
249
|
+
|
|
250
|
+
throw new Error(
|
|
251
|
+
`waitCheckpointHit(${label} ${hex4(addr)}): checkpoint never fired within ${budgetMs / 1000}s. ` +
|
|
252
|
+
`vice_run_until's cycles argument is documented as "not yet implemented" so there is no ` +
|
|
253
|
+
`server-side timeout backing this. Recovery is a HOST-SIDE restart, which this container ` +
|
|
254
|
+
`cannot perform -- run tools/vice-launcher.sh on the HOST; its on-demand broker launches a ` +
|
|
255
|
+
`boot-fresh instance, supervises it, and respawns a crashed one with backoff, logging the ` +
|
|
256
|
+
`crash for the still-open root-cause investigation (see .planning/STATE.md).`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** NOT unit-tested (01.6.1-06): composes readCheckpoint()/waitCheckpointHit()
|
|
261
|
+
* against a real emulator; see vice-sync.test.ts's named todo entry. */
|
|
262
|
+
export async function runToCheckpoint(addr: number, label: string): Promise<{ id: number | null; hitCount?: number }> {
|
|
263
|
+
const added = (await call("vice_checkpoint_add", { start: hex4(addr), exec: true, stop: true })) as {
|
|
264
|
+
checkpoint_num?: number;
|
|
265
|
+
checkpoint?: { checkpoint_num?: number };
|
|
266
|
+
};
|
|
267
|
+
const id = added.checkpoint_num ?? added.checkpoint?.checkpoint_num ?? null;
|
|
268
|
+
if (id != null) armedCheckpoints.track(id);
|
|
269
|
+
// No resume here: waitCheckpointHit owns the single resume, so that the
|
|
270
|
+
// vice_execution_run count stays at exactly one per wait.
|
|
271
|
+
const cp = await waitCheckpointHit(id, addr, label);
|
|
272
|
+
if (id != null) {
|
|
273
|
+
await call("vice_checkpoint_delete", { checkpoint_num: id });
|
|
274
|
+
armedCheckpoints.untrack(id);
|
|
275
|
+
}
|
|
276
|
+
return { id, hitCount: cp.hit_count };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The clean-slate ritual, and a step of `recover` -- not an optional
|
|
281
|
+
* courtesy. No bulk-clear checkpoint tool exists, so each returned id is
|
|
282
|
+
* enumerated and deleted individually.
|
|
283
|
+
*
|
|
284
|
+
* NOT unit-tested (01.6.1-06): the never-delete-a-temporary-checkpoint
|
|
285
|
+
* invariant needs a real emulator's own `temporary` checkpoint flag; see
|
|
286
|
+
* vice-sync.test.ts's named todo entry.
|
|
287
|
+
*/
|
|
288
|
+
export async function reset(): Promise<void> {
|
|
289
|
+
// Any checkpoint id tracked from a PRIOR run in this same process (e.g.
|
|
290
|
+
// reproduce()'s second recover() call) is no longer valid once we're about
|
|
291
|
+
// to delete every checkpoint the server knows about -- clear it here so a
|
|
292
|
+
// later assertSameMachine() probe never gets tripped up by a stale id.
|
|
293
|
+
armedCheckpoints.clear();
|
|
294
|
+
const { checkpoints } = (await call("vice_checkpoint_list", {})) as { checkpoints: Checkpoint[] };
|
|
295
|
+
for (const cp of checkpoints) {
|
|
296
|
+
// Never delete a checkpoint VICE marked `temporary`: those are created and
|
|
297
|
+
// auto-reaped by vice_run_until, so by the time we enumerate them the id
|
|
298
|
+
// may already be gone, and deleting a stale id is one of the two leading
|
|
299
|
+
// suspects for the host-server crashes recorded in STATE.md. Leave them to
|
|
300
|
+
// the hard reset, which clears them anyway.
|
|
301
|
+
if (cp.temporary) continue;
|
|
302
|
+
try {
|
|
303
|
+
await call("vice_checkpoint_delete", { checkpoint_num: cp.checkpoint_num });
|
|
304
|
+
} catch (e) {
|
|
305
|
+
console.error(`warn: checkpoint_delete ${cp.checkpoint_num} failed (continuing): ${(e as Error).message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const unit of [8, 9, 10, 11]) {
|
|
309
|
+
try {
|
|
310
|
+
await call("vice_disk_detach", { unit });
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.error(`warn: disk_detach unit ${unit} failed (continuing): ${(e as Error).message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
await call("vice_machine_reset", { mode: "hard", run_after: false });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* VICE writes screenshots itself, on the HOST -- so the path handed to
|
|
320
|
+
* vice_display_screenshot must be a host path, exactly like the one handed to
|
|
321
|
+
* vice_disk_attach. Passing the container path silently fails with
|
|
322
|
+
* "Failed to save screenshot".
|
|
323
|
+
*
|
|
324
|
+
* NOT unit-tested (01.6.1-06): needs a real emulator to prove the host-path
|
|
325
|
+
* translation actually lands a screenshot; see vice-sync.test.ts's named
|
|
326
|
+
* todo entry.
|
|
327
|
+
*/
|
|
328
|
+
export async function screenshot(containerPath: string): Promise<string> {
|
|
329
|
+
mkdirSync(dirname(containerPath), { recursive: true });
|
|
330
|
+
const { hostPath } = await tryHostPaths(
|
|
331
|
+
containerPath,
|
|
332
|
+
(p: string) => call("vice_display_screenshot", { path: p }),
|
|
333
|
+
{ workspaceRoot: repoRoot() }
|
|
334
|
+
);
|
|
335
|
+
return hostPath;
|
|
336
|
+
}
|