dsh-plugin-wsl-env 0.10.0 → 0.12.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/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +117 -137
- package/cordis.patch.yml +8 -17
- package/docs/ARCHITECTURE.md +23 -24
- package/docs/CONFIGURATION.md +7 -6
- package/docs/LIMITATIONS.md +31 -0
- package/docs/README.zh.md +115 -136
- package/docs/ROADMAP.md +79 -0
- package/lib/agent-shared.js +33 -5
- package/lib/agent.d.ts +103 -0
- package/lib/agent.js +144 -8
- package/lib/index.js +3 -1
- package/lib/paths.d.ts +15 -0
- package/lib/paths.js +28 -0
- package/package.json +4 -12
- package/lib/workspace-files-route.d.ts +0 -136
- package/lib/workspace-files-route.js +0 -225
- package/lib/workspace-files-wsl.d.ts +0 -257
- package/lib/workspace-files-wsl.js +0 -418
package/lib/agent.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ export interface ExecResult {
|
|
|
59
59
|
export interface PendingRequest {
|
|
60
60
|
/** The request id, as the agent echoes it. */
|
|
61
61
|
id: string;
|
|
62
|
+
/** Which metric bucket this request belongs to. */
|
|
63
|
+
kind: "fs" | "exec";
|
|
64
|
+
/** When the frame was written, for the queue measurement. */
|
|
65
|
+
sentAt: number;
|
|
66
|
+
/** When the agent ACKed it, for the execution measurement. */
|
|
67
|
+
ackedAt?: number;
|
|
68
|
+
/** Whether the metrics recorded this request's settle. */
|
|
69
|
+
measured: boolean;
|
|
62
70
|
/** The frame lines (kept so a rebuild can re-send). */
|
|
63
71
|
lines: string[];
|
|
64
72
|
/** Settles a RES. */
|
|
@@ -80,6 +88,51 @@ export interface PendingRequest {
|
|
|
80
88
|
/** The caller was already settled; a late answer only cleans up. */
|
|
81
89
|
settled: boolean;
|
|
82
90
|
}
|
|
91
|
+
/** Per-kind request metrics. */
|
|
92
|
+
export interface RequestMetrics {
|
|
93
|
+
/** Requests that settled (successful, failed or cancelled). */
|
|
94
|
+
requests: number;
|
|
95
|
+
/** Total queue wait (sent -> ACK), ms. */
|
|
96
|
+
queueMs: number;
|
|
97
|
+
/** Largest single queue wait, ms. */
|
|
98
|
+
queueMaxMs: number;
|
|
99
|
+
/** Total execution time (ACK -> settle), ms. */
|
|
100
|
+
execMs: number;
|
|
101
|
+
/** Largest single execution time, ms. */
|
|
102
|
+
execMaxMs: number;
|
|
103
|
+
/** Settled with an error (not cancellation). */
|
|
104
|
+
failed: number;
|
|
105
|
+
/** Cancelled by the caller. */
|
|
106
|
+
cancelled: number;
|
|
107
|
+
/** Rejected before tracking: queue full, already aborted, or send failed. */
|
|
108
|
+
rejected: number;
|
|
109
|
+
}
|
|
110
|
+
/** The agent's cumulative request metrics. */
|
|
111
|
+
export interface AgentMetrics {
|
|
112
|
+
/** FS frames. */
|
|
113
|
+
fs: RequestMetrics;
|
|
114
|
+
/** EXEC frames (commands, ping). */
|
|
115
|
+
exec: RequestMetrics;
|
|
116
|
+
/** Peak number of pending requests. */
|
|
117
|
+
inFlightMax: number;
|
|
118
|
+
}
|
|
119
|
+
/** The entry an async preparation may select before the first spawn. */
|
|
120
|
+
export interface WslAgentPrepared {
|
|
121
|
+
/** The agent entrypoint (the shell script or the native cache path). */
|
|
122
|
+
scriptPath: string;
|
|
123
|
+
/** The HELLO digest to verify. */
|
|
124
|
+
expectedDigest?: string;
|
|
125
|
+
/** Whether USR1 can reach the agent (false for the native KILL-frame path). */
|
|
126
|
+
signalable?: boolean;
|
|
127
|
+
/** The process factory for this entry. */
|
|
128
|
+
spawnTransport?: (options: {
|
|
129
|
+
wslPath: string;
|
|
130
|
+
distro: string;
|
|
131
|
+
scriptPath: string;
|
|
132
|
+
argvPrefix: string[];
|
|
133
|
+
leaseMs: number;
|
|
134
|
+
}) => ChildProcess;
|
|
135
|
+
}
|
|
83
136
|
/**
|
|
84
137
|
* The Windows environment the transport starts from, pinned.
|
|
85
138
|
*
|
|
@@ -180,6 +233,10 @@ export declare class WslAgent {
|
|
|
180
233
|
idleTimer: NodeJS.Timeout | null;
|
|
181
234
|
/** The last failure, for `unavailableReason` and log messages. */
|
|
182
235
|
lastError: string;
|
|
236
|
+
/** Cumulative request metrics; see {@link metrics}. */
|
|
237
|
+
requestMetrics: AgentMetrics;
|
|
238
|
+
/** The optional async preparation hook. */
|
|
239
|
+
prepare: (() => Promise<WslAgentPrepared | undefined>) | undefined;
|
|
183
240
|
/**
|
|
184
241
|
* @param config - the agent configuration.
|
|
185
242
|
*/
|
|
@@ -248,6 +305,8 @@ export declare class WslAgent {
|
|
|
248
305
|
* refused instead of growing the request map without bound.
|
|
249
306
|
*/
|
|
250
307
|
maxPending?: number;
|
|
308
|
+
/** Optional async preparation selecting the entry before the first spawn. */
|
|
309
|
+
prepare?: () => Promise<WslAgentPrepared | undefined>;
|
|
251
310
|
});
|
|
252
311
|
/**
|
|
253
312
|
* Whether the agent is permanently out — the executor's fallback cue.
|
|
@@ -264,6 +323,18 @@ export declare class WslAgent {
|
|
|
264
323
|
* @returns resolved once the state is `ready`.
|
|
265
324
|
*/
|
|
266
325
|
start(): Promise<void>;
|
|
326
|
+
/**
|
|
327
|
+
* Apply an async preparation entry to this agent before the first spawn.
|
|
328
|
+
*
|
|
329
|
+
* @param entry - the selected entry.
|
|
330
|
+
*/
|
|
331
|
+
private applyPrepared;
|
|
332
|
+
/**
|
|
333
|
+
* Spawn the process and run the handshake.
|
|
334
|
+
*
|
|
335
|
+
* @returns resolved once the state is `ready`.
|
|
336
|
+
*/
|
|
337
|
+
private beginStart;
|
|
267
338
|
/**
|
|
268
339
|
* One stdout line from the agent. The first must be a matching HELLO.
|
|
269
340
|
*
|
|
@@ -384,6 +455,38 @@ export declare class WslAgent {
|
|
|
384
455
|
*
|
|
385
456
|
* @param signal - the signal to deliver (`SIGUSR1` is the cancel trap).
|
|
386
457
|
*/
|
|
458
|
+
/**
|
|
459
|
+
* The cumulative request metrics: queue wait (sent -> ACK) and execution
|
|
460
|
+
* time (ACK -> settle) per request kind, plus the peak in-flight count.
|
|
461
|
+
*
|
|
462
|
+
* @returns a snapshot the caller can keep.
|
|
463
|
+
*/
|
|
464
|
+
metrics(): AgentMetrics;
|
|
465
|
+
/** Reset the cumulative metrics (diagnostics/probes). */
|
|
466
|
+
resetMetrics(): void;
|
|
467
|
+
/**
|
|
468
|
+
* The metric bucket for a request kind.
|
|
469
|
+
* @param kind - fs or exec.
|
|
470
|
+
* @returns the bucket.
|
|
471
|
+
*/
|
|
472
|
+
private kindMetrics;
|
|
473
|
+
/**
|
|
474
|
+
* Record a request that never entered the pending map.
|
|
475
|
+
* @param kind - the bucket.
|
|
476
|
+
*/
|
|
477
|
+
private noteRejected;
|
|
478
|
+
/**
|
|
479
|
+
* Record the dequeue ACK: the request's queue wait.
|
|
480
|
+
* @param pending - the request.
|
|
481
|
+
*/
|
|
482
|
+
private recordAck;
|
|
483
|
+
/**
|
|
484
|
+
* Record a settle: the requests count, the execution time (or the queue wait
|
|
485
|
+
* for a request that never ACKed), and the outcome.
|
|
486
|
+
* @param pending - the request.
|
|
487
|
+
* @param error - the failure, when any.
|
|
488
|
+
*/
|
|
489
|
+
private recordSettle;
|
|
387
490
|
private signalAgent;
|
|
388
491
|
/**
|
|
389
492
|
* Shut the agent down: SHUTDOWN first, then kill after a short grace. The
|
package/lib/agent.js
CHANGED
|
@@ -40,6 +40,20 @@
|
|
|
40
40
|
import { spawn } from "node:child_process";
|
|
41
41
|
import { AgentUnavailableError, CwdError } from "./agent-errors.js";
|
|
42
42
|
import { AGENT_NAME, MAX_FRAME_BYTES, PROTOCOL_VERSION, encodeExecFrame, encodeFsFrame, encodeKill, encodeSetEnv, parseAgentLine } from "./agent-protocol.js";
|
|
43
|
+
/**
|
|
44
|
+
* A zeroed per-kind metrics record.
|
|
45
|
+
* @returns the record.
|
|
46
|
+
*/
|
|
47
|
+
function emptyRequestMetrics() {
|
|
48
|
+
return { requests: 0, queueMs: 0, queueMaxMs: 0, execMs: 0, execMaxMs: 0, failed: 0, cancelled: 0, rejected: 0 };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A zeroed metrics snapshot.
|
|
52
|
+
* @returns the snapshot.
|
|
53
|
+
*/
|
|
54
|
+
function emptyAgentMetrics() {
|
|
55
|
+
return { fs: emptyRequestMetrics(), exec: emptyRequestMetrics(), inFlightMax: 0 };
|
|
56
|
+
}
|
|
43
57
|
/** How long a rebuilt agent has to re-answer the handshake before giving up. */
|
|
44
58
|
const HANDSHAKE_TIMEOUT_MS = 15000;
|
|
45
59
|
/** Extra time the host watchdog waits past the request's own in-distro timeout. */
|
|
@@ -160,6 +174,8 @@ export class WslAgent {
|
|
|
160
174
|
this.rebuildUsed = false;
|
|
161
175
|
this.idleTimer = null;
|
|
162
176
|
this.lastError = "";
|
|
177
|
+
this.requestMetrics = emptyAgentMetrics();
|
|
178
|
+
this.prepare = config.prepare;
|
|
163
179
|
}
|
|
164
180
|
/**
|
|
165
181
|
* Whether the agent is permanently out — the executor's fallback cue.
|
|
@@ -187,9 +203,42 @@ export class WslAgent {
|
|
|
187
203
|
return Promise.resolve();
|
|
188
204
|
if (this.startPromise)
|
|
189
205
|
return this.startPromise;
|
|
206
|
+
const prepared = this.prepare?.();
|
|
207
|
+
if (prepared === undefined) {
|
|
208
|
+
const started = this.beginStart();
|
|
209
|
+
this.startPromise = started;
|
|
210
|
+
return started;
|
|
211
|
+
}
|
|
212
|
+
const started = prepared.then((entry) => {
|
|
213
|
+
if (entry !== undefined)
|
|
214
|
+
this.applyPrepared(entry);
|
|
215
|
+
return this.beginStart();
|
|
216
|
+
});
|
|
217
|
+
this.startPromise = started;
|
|
218
|
+
return started;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Apply an async preparation entry to this agent before the first spawn.
|
|
222
|
+
*
|
|
223
|
+
* @param entry - the selected entry.
|
|
224
|
+
*/
|
|
225
|
+
applyPrepared(entry) {
|
|
226
|
+
this.scriptPath = entry.scriptPath;
|
|
227
|
+
this.expectedDigest = entry.expectedDigest;
|
|
228
|
+
if (entry.signalable !== undefined)
|
|
229
|
+
this.signalable = entry.signalable;
|
|
230
|
+
if (entry.spawnTransport !== undefined)
|
|
231
|
+
this.configuredSpawn = entry.spawnTransport;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Spawn the process and run the handshake.
|
|
235
|
+
*
|
|
236
|
+
* @returns resolved once the state is `ready`.
|
|
237
|
+
*/
|
|
238
|
+
beginStart() {
|
|
190
239
|
this.state = "starting";
|
|
191
240
|
this.agentPid = undefined;
|
|
192
|
-
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
193
242
|
// stdio is all-pipe (the spawn options above), so every standard stream
|
|
194
243
|
// exists; the broad ChildProcess type models them as nullable.
|
|
195
244
|
const child = (this.process = (this.configuredSpawn ?? spawnDefault)({
|
|
@@ -286,7 +335,6 @@ export class WslAgent {
|
|
|
286
335
|
this.lastError = String(chunk).trim().slice(0, 2000) || this.lastError;
|
|
287
336
|
});
|
|
288
337
|
});
|
|
289
|
-
return this.startPromise;
|
|
290
338
|
}
|
|
291
339
|
/**
|
|
292
340
|
* One stdout line from the agent. The first must be a matching HELLO.
|
|
@@ -333,6 +381,7 @@ export class WslAgent {
|
|
|
333
381
|
const pending = this.pending.get(message.id);
|
|
334
382
|
if (pending) {
|
|
335
383
|
pending.started = true;
|
|
384
|
+
this.recordAck(pending);
|
|
336
385
|
if (pending.cancelled)
|
|
337
386
|
this.signalAgent("SIGUSR1");
|
|
338
387
|
pending.armWatchdog?.();
|
|
@@ -487,16 +536,26 @@ export class WslAgent {
|
|
|
487
536
|
*
|
|
488
537
|
* @param request - the request to register.
|
|
489
538
|
*/
|
|
490
|
-
track({ id, lines, resolve, reject, timeoutMs, signal }) {
|
|
539
|
+
track({ id, kind = "exec", lines, resolve, reject, timeoutMs, signal }) {
|
|
491
540
|
if (this.pending.size >= this.maxPending) {
|
|
541
|
+
this.noteRejected(kind);
|
|
492
542
|
reject(new Error(`the resident agent has ${this.pending.size} requests in flight (max ${this.maxPending}); retry when the queue drains`));
|
|
493
543
|
return;
|
|
494
544
|
}
|
|
495
545
|
const pending = {
|
|
496
546
|
id,
|
|
547
|
+
kind,
|
|
548
|
+
sentAt: performance.now(),
|
|
549
|
+
measured: false,
|
|
497
550
|
lines,
|
|
498
|
-
resolve
|
|
499
|
-
|
|
551
|
+
resolve: (result) => {
|
|
552
|
+
this.recordSettle(pending, undefined);
|
|
553
|
+
resolve(result);
|
|
554
|
+
},
|
|
555
|
+
reject: (error) => {
|
|
556
|
+
this.recordSettle(pending, error);
|
|
557
|
+
reject(error);
|
|
558
|
+
},
|
|
500
559
|
watchdog: null,
|
|
501
560
|
signal: signal ?? null,
|
|
502
561
|
onAbort: null,
|
|
@@ -525,9 +584,11 @@ export class WslAgent {
|
|
|
525
584
|
: null,
|
|
526
585
|
};
|
|
527
586
|
this.pending.set(id, pending);
|
|
587
|
+
this.requestMetrics.inFlightMax = Math.max(this.requestMetrics.inFlightMax, this.pending.size);
|
|
528
588
|
if (signal) {
|
|
529
589
|
if (signal.aborted) {
|
|
530
590
|
this.pending.delete(id);
|
|
591
|
+
this.noteRejected(kind);
|
|
531
592
|
reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
532
593
|
return;
|
|
533
594
|
}
|
|
@@ -557,6 +618,7 @@ export class WslAgent {
|
|
|
557
618
|
if (!this.writeLines(lines)) {
|
|
558
619
|
this.clearPending(pending);
|
|
559
620
|
this.pending.delete(pending.id);
|
|
621
|
+
this.noteRejected(kind);
|
|
560
622
|
reject(new AgentUnavailableError("the agent exited before the request could be sent; it never ran"));
|
|
561
623
|
return;
|
|
562
624
|
}
|
|
@@ -580,7 +642,7 @@ export class WslAgent {
|
|
|
580
642
|
...encodeExecFrame({ id, cwd, argv, timeoutMs, maxOutputBytes }),
|
|
581
643
|
];
|
|
582
644
|
return new Promise((resolve, reject) => {
|
|
583
|
-
this.track({ id, lines, resolve, reject, timeoutMs, signal });
|
|
645
|
+
this.track({ id, kind: "exec", lines, resolve, reject, timeoutMs, signal });
|
|
584
646
|
});
|
|
585
647
|
}
|
|
586
648
|
/**
|
|
@@ -602,7 +664,7 @@ export class WslAgent {
|
|
|
602
664
|
const id = `r${this.nextRequestId++}`;
|
|
603
665
|
const lines = encodeFsFrame({ id, op, args, timeoutMs });
|
|
604
666
|
return new Promise((resolve, reject) => {
|
|
605
|
-
this.track({ id, lines, resolve, reject, timeoutMs, signal });
|
|
667
|
+
this.track({ id, kind: "fs", lines, resolve, reject, timeoutMs, signal });
|
|
606
668
|
});
|
|
607
669
|
}
|
|
608
670
|
/**
|
|
@@ -620,7 +682,7 @@ export class WslAgent {
|
|
|
620
682
|
return new Promise((resolve, reject) => {
|
|
621
683
|
const id = `r${this.nextRequestId++}`;
|
|
622
684
|
const lines = encodeExecFrame({ id, cwd: "/", argv: ["true"], timeoutMs: 0 });
|
|
623
|
-
this.track({ id, lines, resolve: () => resolve(), reject, timeoutMs: HANDSHAKE_TIMEOUT_MS, signal });
|
|
685
|
+
this.track({ id, kind: "exec", lines, resolve: () => resolve(), reject, timeoutMs: HANDSHAKE_TIMEOUT_MS, signal });
|
|
624
686
|
});
|
|
625
687
|
}
|
|
626
688
|
/**
|
|
@@ -637,6 +699,80 @@ export class WslAgent {
|
|
|
637
699
|
*
|
|
638
700
|
* @param signal - the signal to deliver (`SIGUSR1` is the cancel trap).
|
|
639
701
|
*/
|
|
702
|
+
/**
|
|
703
|
+
* The cumulative request metrics: queue wait (sent -> ACK) and execution
|
|
704
|
+
* time (ACK -> settle) per request kind, plus the peak in-flight count.
|
|
705
|
+
*
|
|
706
|
+
* @returns a snapshot the caller can keep.
|
|
707
|
+
*/
|
|
708
|
+
metrics() {
|
|
709
|
+
return {
|
|
710
|
+
fs: { ...this.requestMetrics.fs },
|
|
711
|
+
exec: { ...this.requestMetrics.exec },
|
|
712
|
+
inFlightMax: this.requestMetrics.inFlightMax,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/** Reset the cumulative metrics (diagnostics/probes). */
|
|
716
|
+
resetMetrics() {
|
|
717
|
+
this.requestMetrics = emptyAgentMetrics();
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* The metric bucket for a request kind.
|
|
721
|
+
* @param kind - fs or exec.
|
|
722
|
+
* @returns the bucket.
|
|
723
|
+
*/
|
|
724
|
+
kindMetrics(kind) {
|
|
725
|
+
return kind === "fs" ? this.requestMetrics.fs : this.requestMetrics.exec;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Record a request that never entered the pending map.
|
|
729
|
+
* @param kind - the bucket.
|
|
730
|
+
*/
|
|
731
|
+
noteRejected(kind) {
|
|
732
|
+
this.kindMetrics(kind).rejected += 1;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Record the dequeue ACK: the request's queue wait.
|
|
736
|
+
* @param pending - the request.
|
|
737
|
+
*/
|
|
738
|
+
recordAck(pending) {
|
|
739
|
+
if (pending.ackedAt !== undefined)
|
|
740
|
+
return;
|
|
741
|
+
const now = performance.now();
|
|
742
|
+
pending.ackedAt = now;
|
|
743
|
+
const stats = this.kindMetrics(pending.kind);
|
|
744
|
+
const wait = now - pending.sentAt;
|
|
745
|
+
stats.queueMs += wait;
|
|
746
|
+
stats.queueMaxMs = Math.max(stats.queueMaxMs, wait);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Record a settle: the requests count, the execution time (or the queue wait
|
|
750
|
+
* for a request that never ACKed), and the outcome.
|
|
751
|
+
* @param pending - the request.
|
|
752
|
+
* @param error - the failure, when any.
|
|
753
|
+
*/
|
|
754
|
+
recordSettle(pending, error) {
|
|
755
|
+
if (pending.measured)
|
|
756
|
+
return;
|
|
757
|
+
pending.measured = true;
|
|
758
|
+
const now = performance.now();
|
|
759
|
+
const stats = this.kindMetrics(pending.kind);
|
|
760
|
+
stats.requests += 1;
|
|
761
|
+
if (pending.ackedAt !== undefined) {
|
|
762
|
+
const exec = now - pending.ackedAt;
|
|
763
|
+
stats.execMs += exec;
|
|
764
|
+
stats.execMaxMs = Math.max(stats.execMaxMs, exec);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
const wait = now - pending.sentAt;
|
|
768
|
+
stats.queueMs += wait;
|
|
769
|
+
stats.queueMaxMs = Math.max(stats.queueMaxMs, wait);
|
|
770
|
+
}
|
|
771
|
+
if (pending.cancelled)
|
|
772
|
+
stats.cancelled += 1;
|
|
773
|
+
else if (error !== undefined)
|
|
774
|
+
stats.failed += 1;
|
|
775
|
+
}
|
|
640
776
|
signalAgent(signal) {
|
|
641
777
|
const pid = this.agentPid;
|
|
642
778
|
if (pid === undefined || !this.signalable)
|
package/lib/index.js
CHANGED
|
@@ -246,7 +246,9 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
246
246
|
this.resolvedShell = config.shell;
|
|
247
247
|
this.resolvedHome = undefined;
|
|
248
248
|
this.mode = config.sandbox ? ctx.sandboxPolicy.defaultMode : undefined;
|
|
249
|
-
this.sandbox = config.sandbox
|
|
249
|
+
this.sandbox = config.sandbox
|
|
250
|
+
? new WslSandbox({ wslPath: config.wslPath, maskWindowsDrive: config.maskWindowsDrive })
|
|
251
|
+
: undefined;
|
|
250
252
|
this.processFacts = new Map();
|
|
251
253
|
this.sessionRuntimes = sessionRuntimes;
|
|
252
254
|
this.sessionRuntimeEntries = new Map();
|
package/lib/paths.d.ts
CHANGED
|
@@ -138,6 +138,21 @@ export declare function windowsToLinuxMount(winPath: string): string | undefined
|
|
|
138
138
|
* @returns an absolute Linux path, or the input unchanged when it cannot be mapped.
|
|
139
139
|
*/
|
|
140
140
|
export declare function toLinuxPath(value: string, options?: WorldPathOptions): string;
|
|
141
|
+
/**
|
|
142
|
+
* Join a requested path onto a workspace root in the distro's Linux
|
|
143
|
+
* coordinate system: UNC absolutes map to their Linux paths, POSIX absolutes
|
|
144
|
+
* win over the root, and relative paths join onto it.
|
|
145
|
+
*
|
|
146
|
+
* This is the path half of the shipped `workspace-files` service's `ctx.fs`
|
|
147
|
+
* calls: the routed backend receives the same path/`cwd` pairs a host
|
|
148
|
+
* workspace would, so the stock service can serve a distro workspace without a
|
|
149
|
+
* second implementation.
|
|
150
|
+
*
|
|
151
|
+
* @param linuxRoot - the workspace root in Linux form.
|
|
152
|
+
* @param path - the requested path, in any coordinate system.
|
|
153
|
+
* @returns the absolute Linux path.
|
|
154
|
+
*/
|
|
155
|
+
export declare function linuxJoin(linuxRoot: string, path: string): string;
|
|
141
156
|
/**
|
|
142
157
|
* Resolve a caller-supplied path into the UNC coordinate system the host's fs
|
|
143
158
|
* calls use. Linux paths from the model, UNC paths from the harness, and
|
package/lib/paths.js
CHANGED
|
@@ -238,6 +238,34 @@ export function toLinuxPath(value, options) {
|
|
|
238
238
|
const baseLinux = toLinuxPath(base, { distro: options?.distro });
|
|
239
239
|
return `${baseLinux.replace(/\/+$/, "")}/${text.replace(/^\.\//, "")}`;
|
|
240
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Join a requested path onto a workspace root in the distro's Linux
|
|
243
|
+
* coordinate system: UNC absolutes map to their Linux paths, POSIX absolutes
|
|
244
|
+
* win over the root, and relative paths join onto it.
|
|
245
|
+
*
|
|
246
|
+
* This is the path half of the shipped `workspace-files` service's `ctx.fs`
|
|
247
|
+
* calls: the routed backend receives the same path/`cwd` pairs a host
|
|
248
|
+
* workspace would, so the stock service can serve a distro workspace without a
|
|
249
|
+
* second implementation.
|
|
250
|
+
*
|
|
251
|
+
* @param linuxRoot - the workspace root in Linux form.
|
|
252
|
+
* @param path - the requested path, in any coordinate system.
|
|
253
|
+
* @returns the absolute Linux path.
|
|
254
|
+
*/
|
|
255
|
+
export function linuxJoin(linuxRoot, path) {
|
|
256
|
+
// The UNC check runs on the RAW spelling: the backslash-to-slash rewrite
|
|
257
|
+
// below would leave `//wsl.localhost/...`, which the identity regex cannot
|
|
258
|
+
// see.
|
|
259
|
+
const parsed = uncToPosix(path);
|
|
260
|
+
if (parsed !== undefined)
|
|
261
|
+
return parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
262
|
+
const text = String(path).replaceAll("\\", "/");
|
|
263
|
+
if (text.startsWith("/"))
|
|
264
|
+
return text.length > 1 ? text.replace(/\/+$/, "") : "/";
|
|
265
|
+
const base = linuxRoot === "/" ? "" : linuxRoot;
|
|
266
|
+
const joined = text.length === 0 ? base : `${base}/${text}`.replace(/\/\.(?=\/|$)/gu, "");
|
|
267
|
+
return joined.length === 0 ? "/" : joined;
|
|
268
|
+
}
|
|
241
269
|
/**
|
|
242
270
|
* Resolve a caller-supplied path into the UNC coordinate system the host's fs
|
|
243
271
|
* calls use. Linux paths from the model, UNC paths from the harness, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "\u8ba9 DeepSeek Harness \u4ee5 WSL \u5b50\u7cfb\u7edf\u4f5c\u4e3a\u4f1a\u8bdd\u7684\u6267\u884c\u73af\u5883\uff1ashell\u3001\u6587\u4ef6\u8bfb\u5199\u4e0e\u547d\u4ee4\u6267\u884c\u5747\u5728\u5b50\u7cfb\u7edf\u5185\u5b8c\u6210\uff0c\u76ee\u5f55\u9009\u62e9\u5668\u4e0e GUI \u7ec8\u7aef\u540c\u6837\u5b9a\u4f4d\u5230\u5b50\u7cfb\u7edf\u8def\u5f84\uff0c\u5e76\u901a\u8fc7\u5b50\u7cfb\u7edf\u5185\u7684 bubblewrap \u5bf9\u547d\u4ee4\u8fdb\u884c\u6c99\u7bb1\u9694\u79bb",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -75,10 +75,6 @@
|
|
|
75
75
|
},
|
|
76
76
|
"./subprocess/locale/en.json": "./locale/subprocess/en.json",
|
|
77
77
|
"./subprocess/locale/zh.json": "./locale/subprocess/zh.json",
|
|
78
|
-
"./workspace-files-wsl": {
|
|
79
|
-
"types": "./lib/workspace-files-wsl.d.ts",
|
|
80
|
-
"default": "./lib/workspace-files-wsl.js"
|
|
81
|
-
},
|
|
82
78
|
"./wsl": {
|
|
83
79
|
"types": "./lib/wsl.d.ts",
|
|
84
80
|
"default": "./lib/wsl.js"
|
|
@@ -106,6 +102,7 @@
|
|
|
106
102
|
"docs/UPSTREAM-FSIO-EXPORT.md",
|
|
107
103
|
"docs/UPSTREAM-TERMINAL-TITLE.md",
|
|
108
104
|
"docs/ARCHITECTURE.md",
|
|
105
|
+
"docs/ROADMAP.md",
|
|
109
106
|
"CHANGELOG.md",
|
|
110
107
|
"CONTRIBUTING.md",
|
|
111
108
|
"SECURITY.md",
|
|
@@ -119,9 +116,9 @@
|
|
|
119
116
|
"lint:types": "tsc --noEmit",
|
|
120
117
|
"build": "node scripts/build.mjs",
|
|
121
118
|
"lint:build": "node test/build-freshness.mjs",
|
|
122
|
-
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/
|
|
119
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs",
|
|
123
120
|
"test:syntax": "node test/syntax.mjs",
|
|
124
|
-
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/
|
|
121
|
+
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs",
|
|
125
122
|
"lint:style": "node test/style.mjs",
|
|
126
123
|
"prepublishOnly": "pnpm test",
|
|
127
124
|
"probe": "bash test/probe/run.sh",
|
|
@@ -165,7 +162,6 @@
|
|
|
165
162
|
"@deepseek-ai/cordis": "4.0.5",
|
|
166
163
|
"@deepseek-ai/cordis-plugin-include": "*",
|
|
167
164
|
"@deepseek-ai/cordis-plugin-loader": "*",
|
|
168
|
-
"@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
|
|
169
165
|
"@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
|
|
170
166
|
"@deepseek-ai/dsh-fs": "0.2.1-rc.2",
|
|
171
167
|
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
|
|
@@ -183,7 +179,6 @@
|
|
|
183
179
|
},
|
|
184
180
|
"peerDependencies": {
|
|
185
181
|
"@deepseek-ai/cordis": "~4.0.5",
|
|
186
|
-
"@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
|
|
187
182
|
"@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
|
|
188
183
|
"@deepseek-ai/dsh-fs": "0.2.1-rc.2",
|
|
189
184
|
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
|
|
@@ -198,9 +193,6 @@
|
|
|
198
193
|
"@deepseek-ai/cordis": {
|
|
199
194
|
"optional": true
|
|
200
195
|
},
|
|
201
|
-
"@deepseek-ai/dsh-api-workspace-files": {
|
|
202
|
-
"optional": true
|
|
203
|
-
},
|
|
204
196
|
"@deepseek-ai/dsh-bash-local": {
|
|
205
197
|
"optional": true
|
|
206
198
|
},
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure builders and parsers for the distro-routed `workspace-files` service —
|
|
3
|
-
* the GUI file tree and previews for a `\\wsl.localhost\<distro>` workspace,
|
|
4
|
-
* served from inside the distro instead of Windows-side 9P walks.
|
|
5
|
-
*
|
|
6
|
-
* Peer-free by construction (no DSH imports): the service subclass imports
|
|
7
|
-
* this alongside the shipped `dsh-api-workspace-files` base class, and the
|
|
8
|
-
* unit tests import it standalone.
|
|
9
|
-
*
|
|
10
|
-
* This is a TypeScript source built to `lib/workspace-files-route.js`; edit
|
|
11
|
-
* THIS file and run `pnpm run build` — the artifact under `lib/` is generated,
|
|
12
|
-
* and `pnpm test` fails when it drifts.
|
|
13
|
-
*
|
|
14
|
-
* @module dsh-plugin-wsl/workspace-files-route
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Translate one path-shaped value into distro coordinates: UNC identities map
|
|
18
|
-
* onto their Linux paths, drive-letter paths onto their `/mnt/<drive>` mounts,
|
|
19
|
-
* everything else passes through verbatim.
|
|
20
|
-
*
|
|
21
|
-
* @param value - the value to translate.
|
|
22
|
-
* @returns the distro-side spelling.
|
|
23
|
-
*/
|
|
24
|
-
export declare function distroPath(value: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Whether a workspace root belongs to a distro (and therefore routes
|
|
27
|
-
* distro-side).
|
|
28
|
-
*
|
|
29
|
-
* @param workspaceRoot - the scope's workspace root, any coordinate system.
|
|
30
|
-
* @returns true for a `\\wsl.localhost\<distro>` UNC root.
|
|
31
|
-
*/
|
|
32
|
-
export declare function isDistroWorkspace(workspaceRoot: string): boolean;
|
|
33
|
-
/**
|
|
34
|
-
* The Linux working path for one request: UNC absolutes map onto their Linux
|
|
35
|
-
* paths, POSIX absolutes and relative paths join onto the root.
|
|
36
|
-
*
|
|
37
|
-
* @param linuxRoot - the workspace root in Linux form.
|
|
38
|
-
* @param path - the requested path, any coordinate system.
|
|
39
|
-
* @returns the absolute Linux path.
|
|
40
|
-
*/
|
|
41
|
-
export declare function linuxJoin(linuxRoot: string, path: string): string;
|
|
42
|
-
/**
|
|
43
|
-
* The record invocation for one path: line 1 is the no-follow kind (the
|
|
44
|
-
* upstream lstat gate), line 2 the follow-stat fields (the upstream final
|
|
45
|
-
* stat), tab-joined. The path is resolved and contained first — the GUI's
|
|
46
|
-
* workspace is a boundary for every read, not just for listings.
|
|
47
|
-
*
|
|
48
|
-
* @param file - absolute Linux path.
|
|
49
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
50
|
-
* @returns the agent argv.
|
|
51
|
-
*/
|
|
52
|
-
export declare function statRecordArgv(file: string, root: string): string[];
|
|
53
|
-
/**
|
|
54
|
-
* Parse one stat record (kind + follow fields, tab-joined).
|
|
55
|
-
*
|
|
56
|
-
* @param line - the record line.
|
|
57
|
-
* @returns the upstream kind spelling, the byte size, and the synthesized
|
|
58
|
-
* opaque version.
|
|
59
|
-
*/
|
|
60
|
-
export declare function parseStatRecord(line: string | Buffer): {
|
|
61
|
-
kind: string;
|
|
62
|
-
type: string;
|
|
63
|
-
size: number;
|
|
64
|
-
version: string;
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Map a `stat` kind onto the wire's kind token.
|
|
68
|
-
*
|
|
69
|
-
* @param kind - the `stat -c %F` spelling.
|
|
70
|
-
* @returns the wire kind token.
|
|
71
|
-
*/
|
|
72
|
-
export declare function wireKind(kind: string): string;
|
|
73
|
-
/**
|
|
74
|
-
* The read invocation for one line-paged text read: the stat record rides
|
|
75
|
-
* stderr (the caller builds the stat half of the response), the page rides
|
|
76
|
-
* stdout — one line beyond the page, so EOF is decidable from the yield.
|
|
77
|
-
* `$1` is the file, `$2` the 1-based first line, `$3` the line count.
|
|
78
|
-
*
|
|
79
|
-
* @param file - absolute Linux path.
|
|
80
|
-
* @param offset - first line to return (1-based).
|
|
81
|
-
* @param limit - maximum lines to return.
|
|
82
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
83
|
-
* @returns the agent argv.
|
|
84
|
-
*/
|
|
85
|
-
export declare function pageArgv(file: string, offset: number, limit: number, root: string): string[];
|
|
86
|
-
/**
|
|
87
|
-
* The byte-window invocation: the stat record rides stderr, the window rides
|
|
88
|
-
* stdout. `$1` is the file, `$2` the byte offset, `$3` the byte count.
|
|
89
|
-
*
|
|
90
|
-
* @param file - absolute Linux path.
|
|
91
|
-
* @param offset - byte offset (0-based).
|
|
92
|
-
* @param length - byte count.
|
|
93
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
94
|
-
* @returns the agent argv.
|
|
95
|
-
*/
|
|
96
|
-
export declare function bytesArgv(file: string, offset: number, length: number, root: string): string[];
|
|
97
|
-
/**
|
|
98
|
-
* Parse the stat record a read or byte-window relayed on stderr.
|
|
99
|
-
*
|
|
100
|
-
* @param stderr - the relay's raw stderr.
|
|
101
|
-
* @returns the parsed stat record, or undefined when stderr carries none.
|
|
102
|
-
*/
|
|
103
|
-
export declare function parseStatRecordFromStderr(stderr: string | Buffer): {
|
|
104
|
-
kind: string;
|
|
105
|
-
type: string;
|
|
106
|
-
size: number;
|
|
107
|
-
version: string;
|
|
108
|
-
} | undefined;
|
|
109
|
-
/**
|
|
110
|
-
* The listing invocation for one directory: resolve the final component
|
|
111
|
-
* through symlinks, refuse paths outside the root and non-directories, then
|
|
112
|
-
* list one more entry than the cap so truncation is decidable from the yield.
|
|
113
|
-
* `-L` matches the upstream child semantics (a symlink to a directory lists
|
|
114
|
-
* as a directory, a broken link drops out); `-p` marks directories with the
|
|
115
|
-
* trailing `/` the parser keys on.
|
|
116
|
-
*
|
|
117
|
-
* @param linuxRoot - the workspace root in Linux form (containment boundary).
|
|
118
|
-
* @param linuxDir - the directory to list.
|
|
119
|
-
* @param maxEntries - the caller's entry cap.
|
|
120
|
-
* @returns the agent argv.
|
|
121
|
-
*/
|
|
122
|
-
export declare function listDistroArgv(linuxRoot: string, linuxDir: string, maxEntries: number): string[];
|
|
123
|
-
/**
|
|
124
|
-
* Parse one capped `ls -1ALp` listing into wire entries.
|
|
125
|
-
*
|
|
126
|
-
* @param stdout - the listing's raw stdout.
|
|
127
|
-
* @param maxEntries - the entry cap the caller configured.
|
|
128
|
-
* @returns at most `maxEntries` entries and whether the level had more.
|
|
129
|
-
*/
|
|
130
|
-
export declare function parseDirListing(stdout: string | Buffer, maxEntries: number): {
|
|
131
|
-
entries: Array<{
|
|
132
|
-
name: string;
|
|
133
|
-
type: "file" | "directory";
|
|
134
|
-
}>;
|
|
135
|
-
truncated: boolean;
|
|
136
|
-
};
|