dsh-wsl-workspace 0.7.1 → 0.7.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/README.md +20 -0
- package/README.zh.md +5 -0
- package/TESTING.md +3 -1
- package/lib/client.js +4 -4
- package/lib/client.js.map +1 -1
- package/lib/index.js +111 -61
- package/lib/index.js.map +1 -1
- package/package.json +4 -2
- package/src/client/locales.ts +4 -4
- package/src/host/wsl-skills.ts +141 -62
package/lib/index.js
CHANGED
|
@@ -434,9 +434,15 @@ const MAX_LINK_RESOLUTIONS = 32;
|
|
|
434
434
|
/** How many parent levels above the session cwd are searched for a `.git` project marker. */
|
|
435
435
|
const MAX_ANCESTOR_WALK = 64;
|
|
436
436
|
/**
|
|
437
|
-
* How
|
|
437
|
+
* How many directories one discovery layer may probe at once.
|
|
438
|
+
*
|
|
439
|
+
* A `readdir` over the `\\wsl.localhost\…` share measured 15.6 ms against 1.6 ms
|
|
440
|
+
* for a `stat`, and a budget-sized walk is 4096 directories: probing one
|
|
441
|
+
* directory at a time costs 20-30 s, which is what a turn in a large workspace
|
|
442
|
+
* used to wait for. The layer stays bounded so the share is never flooded; node's
|
|
443
|
+
* own filesystem thread pool is what ultimately caps the real parallelism.
|
|
438
444
|
*/
|
|
439
|
-
const
|
|
445
|
+
const WALK_CONCURRENCY = 16;
|
|
440
446
|
/** Maximum cached lookups (one entry per distinct scan root across sessions). */
|
|
441
447
|
const CACHE_MAX_ENTRIES = 32;
|
|
442
448
|
/**
|
|
@@ -516,7 +522,56 @@ async function nearestGitAncestor(distro, linuxDir, io) {
|
|
|
516
522
|
return void 0;
|
|
517
523
|
}
|
|
518
524
|
/**
|
|
525
|
+
* Everything the walk learns from one directory, in the order the round trips
|
|
526
|
+
* are worth paying for: the listing first — it also says which skill markers can
|
|
527
|
+
* exist there at all — then the markers it showed.
|
|
528
|
+
* @param distro - the WSL distribution name.
|
|
529
|
+
* @param dir - the directory's Linux path.
|
|
530
|
+
* @param depth - its depth below the scan root.
|
|
531
|
+
* @param io - filesystem face.
|
|
532
|
+
* @returns the directory's roots, child directories and unresolved links.
|
|
533
|
+
*/
|
|
534
|
+
async function probeDirectory(distro, dir, depth, io) {
|
|
535
|
+
let entries;
|
|
536
|
+
if (depth < MAX_SCAN_DEPTH) try {
|
|
537
|
+
entries = await io.readdir(joinUnc(distro, dir), { withFileTypes: true });
|
|
538
|
+
} catch {}
|
|
539
|
+
const listed = entries ?? [];
|
|
540
|
+
const allMarkers = PROJECT_SKILL_MARKERS.map(([marker]) => marker);
|
|
541
|
+
const markers = entries === void 0 ? allMarkers : allMarkers.filter((marker) => listed.some((entry) => entry.name === marker));
|
|
542
|
+
const roots = await skillRootsOfDirectory(distro, dir, io, markers);
|
|
543
|
+
const children = [];
|
|
544
|
+
const links = [];
|
|
545
|
+
for (const entry of listed) {
|
|
546
|
+
if (PRUNED_DIRECTORY_NAMES.has(entry.name)) continue;
|
|
547
|
+
if (entry.name.startsWith(".") && entry.name !== ".dsh" && entry.name !== ".agents") continue;
|
|
548
|
+
if (entry.name === ".dsh" || entry.name === ".agents") continue;
|
|
549
|
+
const childPath = posix.join(dir, entry.name);
|
|
550
|
+
if (entry.isDirectory()) {
|
|
551
|
+
children.push([childPath, depth + 1]);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!entry.isSymbolicLink()) continue;
|
|
555
|
+
try {
|
|
556
|
+
const target = await io.stat(joinUnc(distro, childPath));
|
|
557
|
+
if (target.isDirectory()) children.push([childPath, depth + 1]);
|
|
558
|
+
continue;
|
|
559
|
+
} catch {}
|
|
560
|
+
links.push([childPath, depth + 1]);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
roots,
|
|
564
|
+
children,
|
|
565
|
+
links
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
519
569
|
* Scan a WSL workspace root for nested skill directories.
|
|
570
|
+
*
|
|
571
|
+
* A layer is probed concurrently but published in frontier order, so the
|
|
572
|
+
* catalog never depends on which probe happened to finish first, and the budget
|
|
573
|
+
* is claimed before any probe starts: concurrency must not change what is
|
|
574
|
+
* visited, only how long it takes.
|
|
520
575
|
* @param distro - the WSL distribution name.
|
|
521
576
|
* @param linuxRoot - the workspace's absolute Linux path.
|
|
522
577
|
* @param io - filesystem face.
|
|
@@ -528,41 +583,32 @@ async function discoverSkillRoots(distro, linuxRoot, io) {
|
|
|
528
583
|
let linksResolved = 0;
|
|
529
584
|
let frontier = [[linuxRoot, 0]];
|
|
530
585
|
while (frontier.length > 0 && roots.length < MAX_SKILL_ROOTS) {
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
if (visited.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
let
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
for (const entry of entries) {
|
|
549
|
-
if (PRUNED_DIRECTORY_NAMES.has(entry.name)) continue;
|
|
550
|
-
if (entry.name.startsWith(".") && entry.name !== ".dsh" && entry.name !== ".agents") continue;
|
|
551
|
-
if (entry.name === ".dsh" || entry.name === ".agents") continue;
|
|
552
|
-
const childPath = posix.join(dir, entry.name);
|
|
553
|
-
if (entry.isDirectory()) {
|
|
554
|
-
next.push([childPath, depth + 1]);
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
if (!entry.isSymbolicLink()) continue;
|
|
558
|
-
try {
|
|
559
|
-
const target = await io.stat(joinUnc(distro, childPath));
|
|
560
|
-
if (target.isDirectory()) next.push([childPath, depth + 1]);
|
|
561
|
-
continue;
|
|
562
|
-
} catch {}
|
|
563
|
-
links.push([childPath, depth + 1]);
|
|
586
|
+
const layer = [];
|
|
587
|
+
for (const item of frontier) {
|
|
588
|
+
if (visited.size >= MAX_VISITED_DIRECTORIES) break;
|
|
589
|
+
if (visited.has(item[0])) continue;
|
|
590
|
+
visited.add(item[0]);
|
|
591
|
+
layer.push(item);
|
|
592
|
+
}
|
|
593
|
+
if (layer.length === 0) return roots;
|
|
594
|
+
const probes = new Array(layer.length);
|
|
595
|
+
let next = 0;
|
|
596
|
+
await Promise.all(Array.from({ length: Math.min(WALK_CONCURRENCY, layer.length) }, async () => {
|
|
597
|
+
for (let index = next; index < layer.length; index = next) {
|
|
598
|
+
next += 1;
|
|
599
|
+
const [dir, depth] = layer[index];
|
|
600
|
+
probes[index] = await probeDirectory(distro, dir, depth, io);
|
|
564
601
|
}
|
|
602
|
+
}));
|
|
603
|
+
const nextLayer = [];
|
|
604
|
+
const links = [];
|
|
605
|
+
for (const probe of probes) {
|
|
606
|
+
if (probe === void 0) continue;
|
|
607
|
+
if (roots.length < MAX_SKILL_ROOTS) roots.push(...probe.roots.slice(0, MAX_SKILL_ROOTS - roots.length));
|
|
608
|
+
nextLayer.push(...probe.children);
|
|
609
|
+
links.push(...probe.links);
|
|
565
610
|
}
|
|
611
|
+
if (visited.size >= MAX_VISITED_DIRECTORIES) return roots;
|
|
566
612
|
if (links.length > 0 && io.resolveLinks !== void 0 && linksResolved < MAX_LINK_RESOLUTIONS) {
|
|
567
613
|
const batch = links.slice(0, MAX_LINK_RESOLUTIONS - linksResolved);
|
|
568
614
|
linksResolved += batch.length;
|
|
@@ -572,33 +618,39 @@ async function discoverSkillRoots(distro, linuxRoot, io) {
|
|
|
572
618
|
if (real === void 0) continue;
|
|
573
619
|
try {
|
|
574
620
|
const info = await io.stat(real);
|
|
575
|
-
if (info.isDirectory())
|
|
621
|
+
if (info.isDirectory()) nextLayer.push([uncToLinux(real), batch[index][1]]);
|
|
576
622
|
} catch {}
|
|
577
623
|
}
|
|
578
624
|
}
|
|
579
|
-
frontier =
|
|
625
|
+
frontier = nextLayer;
|
|
580
626
|
}
|
|
581
627
|
return roots;
|
|
582
628
|
}
|
|
629
|
+
/** The two project skill markers, with the source and rank each publishes. */
|
|
630
|
+
const PROJECT_SKILL_MARKERS = [[
|
|
631
|
+
".dsh",
|
|
632
|
+
"project-dsh",
|
|
633
|
+
PROJECT_DSH_RANK
|
|
634
|
+
], [
|
|
635
|
+
".agents",
|
|
636
|
+
"project-agents",
|
|
637
|
+
PROJECT_AGENTS_RANK
|
|
638
|
+
]];
|
|
583
639
|
/**
|
|
584
640
|
* Publish the skill roots of one scanned directory (its `.dsh/skills` and
|
|
585
641
|
* `.agents/skills`, each with the host's project ranks).
|
|
586
642
|
* @param distro - the WSL distribution name.
|
|
587
643
|
* @param linuxDir - the scanned directory's Linux path.
|
|
588
644
|
* @param io - filesystem face.
|
|
645
|
+
* @param markers - the markers worth probing. The walk narrows this to the ones
|
|
646
|
+
* its listing actually showed, because each probe is a round trip and a
|
|
647
|
+
* directory without a `.dsh` entry cannot hold `.dsh/skills`.
|
|
589
648
|
* @returns the directory's skill roots that exist.
|
|
590
649
|
*/
|
|
591
|
-
async function skillRootsOfDirectory(distro, linuxDir, io) {
|
|
650
|
+
async function skillRootsOfDirectory(distro, linuxDir, io, markers = PROJECT_SKILL_MARKERS.map(([marker]) => marker)) {
|
|
592
651
|
const result = [];
|
|
593
|
-
for (const [marker, source, rank] of
|
|
594
|
-
|
|
595
|
-
"project-dsh",
|
|
596
|
-
PROJECT_DSH_RANK
|
|
597
|
-
], [
|
|
598
|
-
".agents",
|
|
599
|
-
"project-agents",
|
|
600
|
-
PROJECT_AGENTS_RANK
|
|
601
|
-
]]) {
|
|
652
|
+
for (const [marker, source, rank] of PROJECT_SKILL_MARKERS) {
|
|
653
|
+
if (!markers.includes(marker)) continue;
|
|
602
654
|
const path = joinUnc(distro, posix.join(linuxDir, marker, "skills"));
|
|
603
655
|
try {
|
|
604
656
|
const info = await io.stat(path);
|
|
@@ -790,9 +842,10 @@ function frontmatterBoolean(fields, key, dflt = false) {
|
|
|
790
842
|
* The WSL workspace skill provider. Registered on the host's `ctx.skills`
|
|
791
843
|
* registry; serves only lookups whose cwd is a WSL UNC workspace path.
|
|
792
844
|
*
|
|
793
|
-
* Completed `list()` lookups are cached per scan root
|
|
794
|
-
*
|
|
795
|
-
*
|
|
845
|
+
* Completed `list()` lookups are cached per scan root and served as-is: a lookup
|
|
846
|
+
* is on the session's request path, and re-walking the tree there is what made
|
|
847
|
+
* every turn in a large workspace wait for a 4096-directory scan of the 9P
|
|
848
|
+
* share. `get()` always re-reads the skill file so body edits are picked up
|
|
796
849
|
* immediately.
|
|
797
850
|
*
|
|
798
851
|
* A scan root that has been served is re-checked every REFRESH_POLL_MS: the
|
|
@@ -801,23 +854,23 @@ function frontmatterBoolean(fields, key, dflt = false) {
|
|
|
801
854
|
* changed directory listing clears the cache and calls `control.invalidate()`,
|
|
802
855
|
* which bumps the registry's revision; the catalog middleware re-collects on
|
|
803
856
|
* the session's next request, so a skill added mid-session reaches the model
|
|
804
|
-
* without starting a new session
|
|
857
|
+
* without starting a new session, and the cache is only ever refilled by a
|
|
858
|
+
* lookup the detector has already invalidated.
|
|
805
859
|
*/
|
|
806
860
|
var WslSkillsProvider = class {
|
|
807
861
|
name = "wsl-workspace";
|
|
808
862
|
control;
|
|
809
863
|
io;
|
|
810
|
-
now;
|
|
811
864
|
refreshMs;
|
|
812
865
|
/** Cheap polls between two full discovery walks (at least one). */
|
|
813
866
|
walkEveryPolls;
|
|
867
|
+
/** The published catalog per scan root, served until its detector drops it. */
|
|
814
868
|
cache = /* @__PURE__ */ new Map();
|
|
815
869
|
/** One change detector per served scan root, keyed like {@link cache}. */
|
|
816
870
|
detectors = /* @__PURE__ */ new Map();
|
|
817
|
-
constructor(control, io = nodeSkillIo,
|
|
871
|
+
constructor(control, io = nodeSkillIo, refreshMs = REFRESH_POLL_MS, discoveryMs = DISCOVERY_POLL_MS) {
|
|
818
872
|
this.control = control;
|
|
819
873
|
this.io = io;
|
|
820
|
-
this.now = now;
|
|
821
874
|
this.refreshMs = refreshMs;
|
|
822
875
|
this.walkEveryPolls = Math.max(1, Math.round(discoveryMs / refreshMs));
|
|
823
876
|
}
|
|
@@ -836,10 +889,10 @@ var WslSkillsProvider = class {
|
|
|
836
889
|
const scanRoot = await nearestGitAncestor(unc.distro, unc.linuxPath, this.io) ?? unc.linuxPath;
|
|
837
890
|
const cacheKey = `${unc.distro}\u0000${scanRoot}`;
|
|
838
891
|
const cached = this.cache.get(cacheKey);
|
|
839
|
-
if (cached !== void 0
|
|
892
|
+
if (cached !== void 0) {
|
|
840
893
|
this.cache.delete(cacheKey);
|
|
841
894
|
this.cache.set(cacheKey, cached);
|
|
842
|
-
return [...cached
|
|
895
|
+
return [...cached];
|
|
843
896
|
}
|
|
844
897
|
const roots = await discoverSkillRoots(unc.distro, scanRoot, this.io);
|
|
845
898
|
const candidates = [];
|
|
@@ -871,17 +924,14 @@ var WslSkillsProvider = class {
|
|
|
871
924
|
});
|
|
872
925
|
}
|
|
873
926
|
}
|
|
874
|
-
this.cache.set(cacheKey,
|
|
875
|
-
expiresAt: this.now() + CACHE_TTL_MS,
|
|
876
|
-
candidates
|
|
877
|
-
});
|
|
927
|
+
this.cache.set(cacheKey, candidates);
|
|
878
928
|
while (this.cache.size > CACHE_MAX_ENTRIES) {
|
|
879
929
|
const oldest = this.cache.keys().next().value;
|
|
880
930
|
if (oldest === void 0) break;
|
|
881
931
|
this.cache.delete(oldest);
|
|
882
932
|
}
|
|
883
933
|
this.watch(cacheKey, unc.distro, scanRoot, signature.join(""), roots);
|
|
884
|
-
return candidates;
|
|
934
|
+
return [...candidates];
|
|
885
935
|
}
|
|
886
936
|
/**
|
|
887
937
|
* Keep one scan root's catalog honest for as long as this provider is
|