@cydm/happy-elves 0.1.0-beta.46 → 0.1.0-beta.48

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.
@@ -1,4 +1,8 @@
1
1
  import type { MachineCommand } from "../../../../packages/shared/dist/index.js";
2
2
  import type { DaemonConfig, DirectoryListResult } from "../types.js";
3
+ export declare function availableWindowsDriveRoots(options?: {
4
+ platform?: NodeJS.Platform;
5
+ access?: (target: string) => Promise<unknown>;
6
+ }): Promise<string[]>;
3
7
  export declare function buildDirectoryList(config: DaemonConfig, requestedPath?: string): Promise<DirectoryListResult>;
4
8
  export declare function handleListDirectory(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
@@ -1,10 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
4
- import { assertPathInsideWorkspace, pathInsideRoot, workspaceRealRoots } from "./workspace-paths.js";
5
+ import { isUncPath, workspaceRealRoots } from "./workspace-paths.js";
5
6
  const directoryEntryLimit = 500;
6
7
  const directoryStatConcurrency = 16;
7
- async function findRepoRoot(startPath, workspaceRoots) {
8
+ const windowsDriveLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
9
+ async function findRepoRoot(startPath) {
8
10
  let current = startPath;
9
11
  while (true) {
10
12
  try {
@@ -15,16 +17,68 @@ async function findRepoRoot(startPath, workspaceRoots) {
15
17
  const parent = path.dirname(current);
16
18
  if (parent === current)
17
19
  return null;
18
- if (!workspaceRoots.some((root) => pathInsideRoot(root, parent)))
19
- return null;
20
20
  current = parent;
21
21
  }
22
22
  }
23
23
  }
24
+ async function directoryRoots(workspaceRoots, currentPath) {
25
+ const roots = [];
26
+ const seen = new Set();
27
+ async function add(candidate, label) {
28
+ if (!candidate || isUncPath(candidate))
29
+ return;
30
+ try {
31
+ const real = await fs.realpath(path.resolve(candidate));
32
+ if (seen.has(real))
33
+ return;
34
+ seen.add(real);
35
+ roots.push({ name: (label ?? path.basename(real)) || real, path: real });
36
+ }
37
+ catch {
38
+ // Optional navigation shortcuts should not break directory listing.
39
+ }
40
+ }
41
+ for (const root of workspaceRoots)
42
+ await add(root);
43
+ await add(os.homedir(), "home");
44
+ const currentRoot = path.parse(currentPath).root;
45
+ await add(currentRoot, filesystemRootName(currentRoot));
46
+ const homeRoot = path.parse(os.homedir()).root;
47
+ if (homeRoot !== currentRoot)
48
+ await add(homeRoot, filesystemRootName(homeRoot));
49
+ for (const driveRoot of await availableWindowsDriveRoots())
50
+ await add(driveRoot, filesystemRootName(driveRoot));
51
+ return roots;
52
+ }
53
+ export async function availableWindowsDriveRoots(options = {}) {
54
+ const platform = options.platform ?? process.platform;
55
+ if (platform !== "win32")
56
+ return [];
57
+ const access = options.access ?? ((target) => fs.access(target));
58
+ const roots = await Promise.all([...windowsDriveLetters].map(async (letter) => {
59
+ const root = `${letter}:\\`;
60
+ try {
61
+ await access(root);
62
+ return root;
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }));
68
+ return roots.filter((root) => Boolean(root));
69
+ }
70
+ function filesystemRootName(root) {
71
+ const match = /^([A-Za-z]:)[\\/]*$/u.exec(root);
72
+ if (match)
73
+ return match[1];
74
+ return root;
75
+ }
24
76
  export async function buildDirectoryList(config, requestedPath) {
77
+ if (requestedPath && isUncPath(requestedPath)) {
78
+ throw Object.assign(new Error("UNC paths are not supported for directory browsing."), { code: "DIRECTORY_UNSUPPORTED_PATH" });
79
+ }
25
80
  const resolvedPath = path.resolve(requestedPath || process.cwd());
26
81
  const realPath = await fs.realpath(resolvedPath);
27
- await assertPathInsideWorkspace(realPath);
28
82
  const workspaceRoots = await workspaceRealRoots();
29
83
  const entries = [];
30
84
  let truncated = false;
@@ -66,13 +120,12 @@ export async function buildDirectoryList(config, requestedPath) {
66
120
  return a.name.localeCompare(b.name);
67
121
  });
68
122
  const parentPath = path.dirname(realPath);
69
- const parentInsideWorkspace = parentPath !== realPath && workspaceRoots.some((root) => pathInsideRoot(root, parentPath));
70
123
  return {
71
124
  machineId: config.machineId,
72
125
  path: realPath,
73
- parentPath: parentInsideWorkspace ? parentPath : null,
74
- repoRoot: await findRepoRoot(realPath, workspaceRoots),
75
- roots: workspaceRoots.map((root) => ({ name: path.basename(root) || root, path: root })),
126
+ parentPath: parentPath !== realPath ? parentPath : null,
127
+ repoRoot: await findRepoRoot(realPath),
128
+ roots: await directoryRoots(workspaceRoots, realPath),
76
129
  entries: sortedEntries,
77
130
  truncated,
78
131
  entryLimit: directoryEntryLimit,
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { appendAudit } from "../audit.js";
4
4
  import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
5
- import { assertRootInsideWorkspace, isUncPath, pathInsideRoot } from "./workspace-paths.js";
5
+ import { isUncPath, pathInsideRoot } from "./workspace-paths.js";
6
6
  const textLimitBytes = 256 * 1024;
7
7
  const textHardLimitBytes = 1024 * 1024;
8
8
  const imageLimitBytes = 16 * 1024 * 1024;
@@ -60,7 +60,6 @@ export async function buildFilePreview(config, rootPath, requestedPath, mode = "
60
60
  ? requestedPath
61
61
  : path.resolve(rootAbsolutePath, requestedPath);
62
62
  const rootRealPath = await fs.realpath(rootAbsolutePath);
63
- await assertRootInsideWorkspace(rootRealPath);
64
63
  const targetRealPath = await fs.realpath(targetPath);
65
64
  assertInsideRoot(rootRealPath, targetRealPath);
66
65
  const stat = await fs.stat(targetRealPath);
@@ -1,5 +1,3 @@
1
1
  export declare function workspaceRealRoots(): Promise<string[]>;
2
- export declare function assertPathInsideWorkspace(realPath: string): Promise<void>;
3
- export declare function assertRootInsideWorkspace(rootRealPath: string): Promise<void>;
4
2
  export declare function pathInsideRoot(rootPath: string, targetPath: string): boolean;
5
3
  export declare function isUncPath(input: string): boolean;
@@ -16,23 +16,11 @@ export async function workspaceRealRoots() {
16
16
  roots.push(real);
17
17
  }
18
18
  catch {
19
- // A stale session cwd should not widen or break the daemon workspace boundary.
19
+ // A stale session cwd should not break directory root shortcuts.
20
20
  }
21
21
  }
22
22
  return roots;
23
23
  }
24
- export async function assertPathInsideWorkspace(realPath) {
25
- const roots = await workspaceRealRoots();
26
- if (roots.some((root) => pathInsideRoot(root, realPath)))
27
- return;
28
- throw Object.assign(new Error("Path must stay inside a known workspace."), { code: "WORKSPACE_PATH_FORBIDDEN" });
29
- }
30
- export async function assertRootInsideWorkspace(rootRealPath) {
31
- const roots = await workspaceRealRoots();
32
- if (roots.some((root) => pathInsideRoot(root, rootRealPath)))
33
- return;
34
- throw Object.assign(new Error("Root path must stay inside a known workspace."), { code: "WORKSPACE_ROOT_FORBIDDEN" });
35
- }
36
24
  export function pathInsideRoot(rootPath, targetPath) {
37
25
  const relative = path.relative(rootPath, targetPath);
38
26
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.46",
3
+ "version": "0.1.0-beta.48",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.46",
3
+ "version": "0.1.0-beta.48",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@cydm/happy-elves",
9
- "version": "0.1.0-beta.46",
9
+ "version": "0.1.0-beta.48",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@fastify/cors": "11.2.0",
@@ -799,9 +799,9 @@
799
799
  }
800
800
  },
801
801
  "node_modules/bare-os": {
802
- "version": "3.9.1",
803
- "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
804
- "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
802
+ "version": "3.9.3",
803
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz",
804
+ "integrity": "sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==",
805
805
  "license": "Apache-2.0",
806
806
  "engines": {
807
807
  "bare": ">=1.14.0"
@@ -1021,9 +1021,9 @@
1021
1021
  }
1022
1022
  },
1023
1023
  "node_modules/fast-uri": {
1024
- "version": "3.1.2",
1025
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
1026
- "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
1024
+ "version": "3.1.3",
1025
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
1026
+ "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
1027
1027
  "funding": [
1028
1028
  {
1029
1029
  "type": "github",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.46",
3
+ "version": "0.1.0-beta.48",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {