@dexto/image-local 1.16.5 → 1.17.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/dist/local-workspace-handle-provider.cjs +57 -16
- package/dist/local-workspace-handle-provider.d.ts.map +1 -1
- package/dist/local-workspace-handle-provider.js +59 -18
- package/dist/local-workspace-handle-provider.test.cjs +107 -0
- package/dist/local-workspace-handle-provider.test.d.ts +2 -0
- package/dist/local-workspace-handle-provider.test.d.ts.map +1 -0
- package/dist/local-workspace-handle-provider.test.js +84 -0
- package/dist/run-command.cjs +108 -0
- package/dist/run-command.d.ts +17 -0
- package/dist/run-command.d.ts.map +1 -0
- package/dist/run-command.js +84 -0
- package/dist/workspace-bash-environment.test.cjs +121 -0
- package/dist/workspace-bash-environment.test.d.ts +2 -0
- package/dist/workspace-bash-environment.test.d.ts.map +1 -0
- package/dist/workspace-bash-environment.test.js +98 -0
- package/package.json +12 -12
|
@@ -31,13 +31,11 @@ __export(local_workspace_handle_provider_exports, {
|
|
|
31
31
|
LocalWorkspaceHandleProvider: () => LocalWorkspaceHandleProvider
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(local_workspace_handle_provider_exports);
|
|
34
|
-
var import_node_child_process = require("node:child_process");
|
|
35
34
|
var import_promises = require("node:fs/promises");
|
|
36
35
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
37
|
-
var import_node_util = require("node:util");
|
|
38
36
|
var import_glob = require("glob");
|
|
39
37
|
var import_workspace = require("@dexto/core/workspace");
|
|
40
|
-
|
|
38
|
+
var import_run_command = require("./run-command.js");
|
|
41
39
|
class LocalWorkspaceHandleProvider {
|
|
42
40
|
async open(input) {
|
|
43
41
|
const root = import_node_path.default.resolve(input.context.path);
|
|
@@ -61,9 +59,21 @@ class LocalWorkspaceFiles {
|
|
|
61
59
|
constructor(root) {
|
|
62
60
|
this.root = root;
|
|
63
61
|
}
|
|
62
|
+
realRoot;
|
|
64
63
|
readText = async (filePath) => {
|
|
65
64
|
try {
|
|
66
|
-
return await (0, import_promises.readFile)(this.resolveInsideRoot(filePath), "utf-8");
|
|
65
|
+
return await (0, import_promises.readFile)(await this.resolveInsideRoot(filePath), "utf-8");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === "ENOENT") {
|
|
68
|
+
throw import_workspace.WorkspaceError.fileNotFound(filePath);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/** Size in bytes, from the same contained real path `readText` would read. */
|
|
74
|
+
readFileSize = async (filePath) => {
|
|
75
|
+
try {
|
|
76
|
+
return (await (0, import_promises.stat)(await this.resolveInsideRoot(filePath))).size;
|
|
67
77
|
} catch (error) {
|
|
68
78
|
if (error.code === "ENOENT") {
|
|
69
79
|
throw import_workspace.WorkspaceError.fileNotFound(filePath);
|
|
@@ -77,7 +87,7 @@ class LocalWorkspaceFiles {
|
|
|
77
87
|
glob = async (pattern) => {
|
|
78
88
|
this.assertRelativePattern(pattern);
|
|
79
89
|
const files = await (0, import_glob.glob)(pattern, {
|
|
80
|
-
cwd: this.
|
|
90
|
+
cwd: await this.getRealRoot(),
|
|
81
91
|
absolute: false,
|
|
82
92
|
nodir: true,
|
|
83
93
|
follow: false,
|
|
@@ -86,7 +96,7 @@ class LocalWorkspaceFiles {
|
|
|
86
96
|
return files.sort();
|
|
87
97
|
};
|
|
88
98
|
writeFile = async (filePath, content) => {
|
|
89
|
-
const resolvedPath = this.resolveInsideRoot(filePath);
|
|
99
|
+
const resolvedPath = await this.resolveInsideRoot(filePath);
|
|
90
100
|
await (0, import_promises.mkdir)(import_node_path.default.dirname(resolvedPath), { recursive: true });
|
|
91
101
|
try {
|
|
92
102
|
await (0, import_promises.writeFile)(resolvedPath, content, "utf-8");
|
|
@@ -98,16 +108,27 @@ class LocalWorkspaceFiles {
|
|
|
98
108
|
}
|
|
99
109
|
};
|
|
100
110
|
listFiles = async (directoryPath = ".") => {
|
|
101
|
-
const resolvedPath = this.resolveInsideRoot(directoryPath);
|
|
102
|
-
const relativePath = import_node_path.default.relative(this.
|
|
111
|
+
const resolvedPath = await this.resolveInsideRoot(directoryPath);
|
|
112
|
+
const relativePath = import_node_path.default.relative(await this.getRealRoot(), resolvedPath);
|
|
103
113
|
const pattern = relativePath ? `${relativePath.split(import_node_path.default.sep).join("/")}/**/*` : "**/*";
|
|
104
114
|
return this.glob(pattern);
|
|
105
115
|
};
|
|
106
|
-
|
|
116
|
+
/** The workspace root's real path, resolved once. The root itself may sit behind a symlink. */
|
|
117
|
+
async getRealRoot() {
|
|
118
|
+
this.realRoot ??= await (0, import_promises.realpath)(this.root);
|
|
119
|
+
return this.realRoot;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve a path to its real location and require it to be inside the real workspace root,
|
|
123
|
+
* so a symlink in the workspace cannot lead out of it.
|
|
124
|
+
*/
|
|
125
|
+
async resolveInsideRoot(filePath) {
|
|
107
126
|
const resolvedPath = import_node_path.default.isAbsolute(filePath) ? import_node_path.default.resolve(filePath) : import_node_path.default.resolve(this.root, filePath);
|
|
108
|
-
const
|
|
127
|
+
const realRoot = await this.getRealRoot();
|
|
128
|
+
const realPath = await realpathOfPossiblyMissing(resolvedPath, filePath);
|
|
129
|
+
const relativePath = import_node_path.default.relative(realRoot, realPath);
|
|
109
130
|
if (relativePath === "" || !relativePath.startsWith("..") && !import_node_path.default.isAbsolute(relativePath)) {
|
|
110
|
-
return
|
|
131
|
+
return realPath;
|
|
111
132
|
}
|
|
112
133
|
throw import_workspace.WorkspaceError.pathOutsideWorkspace(filePath);
|
|
113
134
|
}
|
|
@@ -123,11 +144,12 @@ class LocalWorkspaceProcesses {
|
|
|
123
144
|
}
|
|
124
145
|
exec = async (input) => {
|
|
125
146
|
const cwd = input.cwd === void 0 ? this.root : resolveInsideRoot(this.root, input.cwd);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
return (0, import_run_command.runCommand)(input.command, {
|
|
148
|
+
cwd,
|
|
149
|
+
env: (0, import_workspace.buildShellEnvironment)(process.env, input.env),
|
|
150
|
+
...input.timeout === void 0 ? {} : { timeout: input.timeout },
|
|
151
|
+
signal: input.signal
|
|
152
|
+
});
|
|
131
153
|
};
|
|
132
154
|
}
|
|
133
155
|
function resolveCapabilities(input) {
|
|
@@ -145,6 +167,25 @@ function resolveInsideRoot(root, filePath) {
|
|
|
145
167
|
}
|
|
146
168
|
throw import_workspace.WorkspaceError.pathOutsideWorkspace(filePath);
|
|
147
169
|
}
|
|
170
|
+
async function realpathOfPossiblyMissing(target, requestedPath) {
|
|
171
|
+
try {
|
|
172
|
+
return await (0, import_promises.realpath)(target);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const code = error.code;
|
|
175
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const stats = await (0, import_promises.lstat)(target).catch(() => void 0);
|
|
180
|
+
if (stats?.isSymbolicLink()) {
|
|
181
|
+
throw import_workspace.WorkspaceError.pathOutsideWorkspace(requestedPath);
|
|
182
|
+
}
|
|
183
|
+
const parent = import_node_path.default.dirname(target);
|
|
184
|
+
if (parent === target) {
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
return import_node_path.default.join(await realpathOfPossiblyMissing(parent, requestedPath), import_node_path.default.basename(target));
|
|
188
|
+
}
|
|
148
189
|
// Annotate the CommonJS export names for ESM import in node:
|
|
149
190
|
0 && (module.exports = {
|
|
150
191
|
LocalWorkspaceHandleProvider
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-workspace-handle-provider.d.ts","sourceRoot":"","sources":["../src/local-workspace-handle-provider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-workspace-handle-provider.d.ts","sourceRoot":"","sources":["../src/local-workspace-handle-provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,kBAAkB,EAElB,gBAAgB,EAChB,eAAe,EACf,uBAAuB,EAE1B,MAAM,uBAAuB,CAAC;AAG/B,qBAAa,4BAA6B,YAAW,uBAAuB;IAClE,IAAI,CAAC,KAAK,EAAE;QACd,OAAO,EAAE,gBAAgB,CAAC;QAC1B,KAAK,CAAC,EAAE,kBAAkB,CAAC;KAC9B,GAAG,OAAO,CAAC,eAAe,CAAC;CAoB/B"}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { lstat, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
3
|
import { glob } from "glob";
|
|
6
|
-
import { WorkspaceError } from "@dexto/core/workspace";
|
|
7
|
-
|
|
4
|
+
import { WorkspaceError, buildShellEnvironment } from "@dexto/core/workspace";
|
|
5
|
+
import { runCommand } from "./run-command.js";
|
|
8
6
|
class LocalWorkspaceHandleProvider {
|
|
9
7
|
async open(input) {
|
|
10
8
|
const root = path.resolve(input.context.path);
|
|
@@ -28,9 +26,21 @@ class LocalWorkspaceFiles {
|
|
|
28
26
|
constructor(root) {
|
|
29
27
|
this.root = root;
|
|
30
28
|
}
|
|
29
|
+
realRoot;
|
|
31
30
|
readText = async (filePath) => {
|
|
32
31
|
try {
|
|
33
|
-
return await readFile(this.resolveInsideRoot(filePath), "utf-8");
|
|
32
|
+
return await readFile(await this.resolveInsideRoot(filePath), "utf-8");
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.code === "ENOENT") {
|
|
35
|
+
throw WorkspaceError.fileNotFound(filePath);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** Size in bytes, from the same contained real path `readText` would read. */
|
|
41
|
+
readFileSize = async (filePath) => {
|
|
42
|
+
try {
|
|
43
|
+
return (await stat(await this.resolveInsideRoot(filePath))).size;
|
|
34
44
|
} catch (error) {
|
|
35
45
|
if (error.code === "ENOENT") {
|
|
36
46
|
throw WorkspaceError.fileNotFound(filePath);
|
|
@@ -44,7 +54,7 @@ class LocalWorkspaceFiles {
|
|
|
44
54
|
glob = async (pattern) => {
|
|
45
55
|
this.assertRelativePattern(pattern);
|
|
46
56
|
const files = await glob(pattern, {
|
|
47
|
-
cwd: this.
|
|
57
|
+
cwd: await this.getRealRoot(),
|
|
48
58
|
absolute: false,
|
|
49
59
|
nodir: true,
|
|
50
60
|
follow: false,
|
|
@@ -53,7 +63,7 @@ class LocalWorkspaceFiles {
|
|
|
53
63
|
return files.sort();
|
|
54
64
|
};
|
|
55
65
|
writeFile = async (filePath, content) => {
|
|
56
|
-
const resolvedPath = this.resolveInsideRoot(filePath);
|
|
66
|
+
const resolvedPath = await this.resolveInsideRoot(filePath);
|
|
57
67
|
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
58
68
|
try {
|
|
59
69
|
await writeFile(resolvedPath, content, "utf-8");
|
|
@@ -65,16 +75,27 @@ class LocalWorkspaceFiles {
|
|
|
65
75
|
}
|
|
66
76
|
};
|
|
67
77
|
listFiles = async (directoryPath = ".") => {
|
|
68
|
-
const resolvedPath = this.resolveInsideRoot(directoryPath);
|
|
69
|
-
const relativePath = path.relative(this.
|
|
78
|
+
const resolvedPath = await this.resolveInsideRoot(directoryPath);
|
|
79
|
+
const relativePath = path.relative(await this.getRealRoot(), resolvedPath);
|
|
70
80
|
const pattern = relativePath ? `${relativePath.split(path.sep).join("/")}/**/*` : "**/*";
|
|
71
81
|
return this.glob(pattern);
|
|
72
82
|
};
|
|
73
|
-
|
|
83
|
+
/** The workspace root's real path, resolved once. The root itself may sit behind a symlink. */
|
|
84
|
+
async getRealRoot() {
|
|
85
|
+
this.realRoot ??= await realpath(this.root);
|
|
86
|
+
return this.realRoot;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a path to its real location and require it to be inside the real workspace root,
|
|
90
|
+
* so a symlink in the workspace cannot lead out of it.
|
|
91
|
+
*/
|
|
92
|
+
async resolveInsideRoot(filePath) {
|
|
74
93
|
const resolvedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(this.root, filePath);
|
|
75
|
-
const
|
|
94
|
+
const realRoot = await this.getRealRoot();
|
|
95
|
+
const realPath = await realpathOfPossiblyMissing(resolvedPath, filePath);
|
|
96
|
+
const relativePath = path.relative(realRoot, realPath);
|
|
76
97
|
if (relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
77
|
-
return
|
|
98
|
+
return realPath;
|
|
78
99
|
}
|
|
79
100
|
throw WorkspaceError.pathOutsideWorkspace(filePath);
|
|
80
101
|
}
|
|
@@ -90,11 +111,12 @@ class LocalWorkspaceProcesses {
|
|
|
90
111
|
}
|
|
91
112
|
exec = async (input) => {
|
|
92
113
|
const cwd = input.cwd === void 0 ? this.root : resolveInsideRoot(this.root, input.cwd);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
return runCommand(input.command, {
|
|
115
|
+
cwd,
|
|
116
|
+
env: buildShellEnvironment(process.env, input.env),
|
|
117
|
+
...input.timeout === void 0 ? {} : { timeout: input.timeout },
|
|
118
|
+
signal: input.signal
|
|
119
|
+
});
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
function resolveCapabilities(input) {
|
|
@@ -112,6 +134,25 @@ function resolveInsideRoot(root, filePath) {
|
|
|
112
134
|
}
|
|
113
135
|
throw WorkspaceError.pathOutsideWorkspace(filePath);
|
|
114
136
|
}
|
|
137
|
+
async function realpathOfPossiblyMissing(target, requestedPath) {
|
|
138
|
+
try {
|
|
139
|
+
return await realpath(target);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const code = error.code;
|
|
142
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const stats = await lstat(target).catch(() => void 0);
|
|
147
|
+
if (stats?.isSymbolicLink()) {
|
|
148
|
+
throw WorkspaceError.pathOutsideWorkspace(requestedPath);
|
|
149
|
+
}
|
|
150
|
+
const parent = path.dirname(target);
|
|
151
|
+
if (parent === target) {
|
|
152
|
+
return target;
|
|
153
|
+
}
|
|
154
|
+
return path.join(await realpathOfPossiblyMissing(parent, requestedPath), path.basename(target));
|
|
155
|
+
}
|
|
115
156
|
export {
|
|
116
157
|
LocalWorkspaceHandleProvider
|
|
117
158
|
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var import_promises = require("node:fs/promises");
|
|
25
|
+
var import_node_os = require("node:os");
|
|
26
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
27
|
+
var import_vitest = require("vitest");
|
|
28
|
+
var import_local_workspace_handle_provider = require("./local-workspace-handle-provider.js");
|
|
29
|
+
(0, import_vitest.describe)("LocalWorkspaceHandleProvider files", () => {
|
|
30
|
+
let base;
|
|
31
|
+
let root;
|
|
32
|
+
let outside;
|
|
33
|
+
(0, import_vitest.beforeEach)(async () => {
|
|
34
|
+
base = await (0, import_promises.mkdtemp)(import_node_path.default.join((0, import_node_os.tmpdir)(), "workspace-handle-"));
|
|
35
|
+
root = import_node_path.default.join(base, "root");
|
|
36
|
+
outside = import_node_path.default.join(base, "outside");
|
|
37
|
+
await (0, import_promises.mkdir)(root);
|
|
38
|
+
await (0, import_promises.mkdir)(outside);
|
|
39
|
+
await (0, import_promises.writeFile)(import_node_path.default.join(outside, "private.txt"), "private");
|
|
40
|
+
});
|
|
41
|
+
(0, import_vitest.afterEach)(async () => {
|
|
42
|
+
await (0, import_promises.rm)(base, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
async function openFiles() {
|
|
45
|
+
const context = { path: root };
|
|
46
|
+
const handle = await new import_local_workspace_handle_provider.LocalWorkspaceHandleProvider().open({ context });
|
|
47
|
+
return handle.files;
|
|
48
|
+
}
|
|
49
|
+
(0, import_vitest.it)("reads and writes regular files inside the workspace", async () => {
|
|
50
|
+
const files = await openFiles();
|
|
51
|
+
await files.writeFile("sub/a.txt", "hello");
|
|
52
|
+
(0, import_vitest.expect)(await files.readText("sub/a.txt")).toBe("hello");
|
|
53
|
+
});
|
|
54
|
+
(0, import_vitest.it)("refuses to read through a symlink that leaves the workspace", async () => {
|
|
55
|
+
await (0, import_promises.symlink)(import_node_path.default.join(outside, "private.txt"), import_node_path.default.join(root, "link.txt"));
|
|
56
|
+
const files = await openFiles();
|
|
57
|
+
await (0, import_vitest.expect)(files.readText("link.txt")).rejects.toThrow();
|
|
58
|
+
});
|
|
59
|
+
(0, import_vitest.it)("refuses to write through a symlink that leaves the workspace", async () => {
|
|
60
|
+
await (0, import_promises.symlink)(import_node_path.default.join(outside, "private.txt"), import_node_path.default.join(root, "link.txt"));
|
|
61
|
+
const files = await openFiles();
|
|
62
|
+
await (0, import_vitest.expect)(files.writeFile("link.txt", "changed")).rejects.toThrow();
|
|
63
|
+
(0, import_vitest.expect)(await (0, import_promises.readFile)(import_node_path.default.join(outside, "private.txt"), "utf-8")).toBe("private");
|
|
64
|
+
});
|
|
65
|
+
(0, import_vitest.it)("refuses to create files through a symlinked directory that leaves the workspace", async () => {
|
|
66
|
+
await (0, import_promises.symlink)(outside, import_node_path.default.join(root, "linked-dir"));
|
|
67
|
+
const files = await openFiles();
|
|
68
|
+
await (0, import_vitest.expect)(files.writeFile("linked-dir/new.txt", "x")).rejects.toThrow();
|
|
69
|
+
await (0, import_vitest.expect)((0, import_promises.readFile)(import_node_path.default.join(outside, "new.txt"))).rejects.toThrow();
|
|
70
|
+
});
|
|
71
|
+
(0, import_vitest.it)("allows a symlink that stays inside the workspace", async () => {
|
|
72
|
+
await (0, import_promises.writeFile)(import_node_path.default.join(root, "real.txt"), "inside");
|
|
73
|
+
await (0, import_promises.symlink)(import_node_path.default.join(root, "real.txt"), import_node_path.default.join(root, "link.txt"));
|
|
74
|
+
const files = await openFiles();
|
|
75
|
+
(0, import_vitest.expect)(await files.readText("link.txt")).toBe("inside");
|
|
76
|
+
});
|
|
77
|
+
(0, import_vitest.describe)("when the workspace root is reached through a symlink", () => {
|
|
78
|
+
async function openThroughLink() {
|
|
79
|
+
const linkRoot = import_node_path.default.join(base, "link-root");
|
|
80
|
+
await (0, import_promises.symlink)(root, linkRoot);
|
|
81
|
+
const context = { path: linkRoot };
|
|
82
|
+
const handle = await new import_local_workspace_handle_provider.LocalWorkspaceHandleProvider().open({ context });
|
|
83
|
+
return { files: handle.files, linkRoot };
|
|
84
|
+
}
|
|
85
|
+
(0, import_vitest.it)("reads, writes, lists and globs files", async () => {
|
|
86
|
+
const { files } = await openThroughLink();
|
|
87
|
+
await files.writeFile("sub/a.txt", "hello");
|
|
88
|
+
(0, import_vitest.expect)(await files.readText("sub/a.txt")).toBe("hello");
|
|
89
|
+
(0, import_vitest.expect)(await files.listFiles(".")).toEqual(["sub/a.txt"]);
|
|
90
|
+
(0, import_vitest.expect)(await files.listFiles("sub")).toEqual(["sub/a.txt"]);
|
|
91
|
+
(0, import_vitest.expect)(await files.glob("**/*.txt")).toEqual(["sub/a.txt"]);
|
|
92
|
+
});
|
|
93
|
+
(0, import_vitest.it)("accepts an absolute path under the linked or the real root", async () => {
|
|
94
|
+
const { files, linkRoot } = await openThroughLink();
|
|
95
|
+
await files.writeFile(import_node_path.default.join(linkRoot, "a.txt"), "one");
|
|
96
|
+
(0, import_vitest.expect)(await files.readText(import_node_path.default.join(root, "a.txt"))).toBe("one");
|
|
97
|
+
});
|
|
98
|
+
(0, import_vitest.it)("still refuses links that leave the workspace", async () => {
|
|
99
|
+
await (0, import_promises.symlink)(import_node_path.default.join(outside, "private.txt"), import_node_path.default.join(root, "link.txt"));
|
|
100
|
+
await (0, import_promises.symlink)(outside, import_node_path.default.join(root, "linked-dir"));
|
|
101
|
+
const { files } = await openThroughLink();
|
|
102
|
+
await (0, import_vitest.expect)(files.readText("link.txt")).rejects.toThrow();
|
|
103
|
+
await (0, import_vitest.expect)(files.writeFile("linked-dir/new.txt", "x")).rejects.toThrow();
|
|
104
|
+
await (0, import_vitest.expect)(files.listFiles("linked-dir")).rejects.toThrow();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-workspace-handle-provider.test.d.ts","sourceRoot":"","sources":["../src/local-workspace-handle-provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { LocalWorkspaceHandleProvider } from "./local-workspace-handle-provider.js";
|
|
6
|
+
describe("LocalWorkspaceHandleProvider files", () => {
|
|
7
|
+
let base;
|
|
8
|
+
let root;
|
|
9
|
+
let outside;
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
base = await mkdtemp(path.join(tmpdir(), "workspace-handle-"));
|
|
12
|
+
root = path.join(base, "root");
|
|
13
|
+
outside = path.join(base, "outside");
|
|
14
|
+
await mkdir(root);
|
|
15
|
+
await mkdir(outside);
|
|
16
|
+
await writeFile(path.join(outside, "private.txt"), "private");
|
|
17
|
+
});
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await rm(base, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
async function openFiles() {
|
|
22
|
+
const context = { path: root };
|
|
23
|
+
const handle = await new LocalWorkspaceHandleProvider().open({ context });
|
|
24
|
+
return handle.files;
|
|
25
|
+
}
|
|
26
|
+
it("reads and writes regular files inside the workspace", async () => {
|
|
27
|
+
const files = await openFiles();
|
|
28
|
+
await files.writeFile("sub/a.txt", "hello");
|
|
29
|
+
expect(await files.readText("sub/a.txt")).toBe("hello");
|
|
30
|
+
});
|
|
31
|
+
it("refuses to read through a symlink that leaves the workspace", async () => {
|
|
32
|
+
await symlink(path.join(outside, "private.txt"), path.join(root, "link.txt"));
|
|
33
|
+
const files = await openFiles();
|
|
34
|
+
await expect(files.readText("link.txt")).rejects.toThrow();
|
|
35
|
+
});
|
|
36
|
+
it("refuses to write through a symlink that leaves the workspace", async () => {
|
|
37
|
+
await symlink(path.join(outside, "private.txt"), path.join(root, "link.txt"));
|
|
38
|
+
const files = await openFiles();
|
|
39
|
+
await expect(files.writeFile("link.txt", "changed")).rejects.toThrow();
|
|
40
|
+
expect(await readFile(path.join(outside, "private.txt"), "utf-8")).toBe("private");
|
|
41
|
+
});
|
|
42
|
+
it("refuses to create files through a symlinked directory that leaves the workspace", async () => {
|
|
43
|
+
await symlink(outside, path.join(root, "linked-dir"));
|
|
44
|
+
const files = await openFiles();
|
|
45
|
+
await expect(files.writeFile("linked-dir/new.txt", "x")).rejects.toThrow();
|
|
46
|
+
await expect(readFile(path.join(outside, "new.txt"))).rejects.toThrow();
|
|
47
|
+
});
|
|
48
|
+
it("allows a symlink that stays inside the workspace", async () => {
|
|
49
|
+
await writeFile(path.join(root, "real.txt"), "inside");
|
|
50
|
+
await symlink(path.join(root, "real.txt"), path.join(root, "link.txt"));
|
|
51
|
+
const files = await openFiles();
|
|
52
|
+
expect(await files.readText("link.txt")).toBe("inside");
|
|
53
|
+
});
|
|
54
|
+
describe("when the workspace root is reached through a symlink", () => {
|
|
55
|
+
async function openThroughLink() {
|
|
56
|
+
const linkRoot = path.join(base, "link-root");
|
|
57
|
+
await symlink(root, linkRoot);
|
|
58
|
+
const context = { path: linkRoot };
|
|
59
|
+
const handle = await new LocalWorkspaceHandleProvider().open({ context });
|
|
60
|
+
return { files: handle.files, linkRoot };
|
|
61
|
+
}
|
|
62
|
+
it("reads, writes, lists and globs files", async () => {
|
|
63
|
+
const { files } = await openThroughLink();
|
|
64
|
+
await files.writeFile("sub/a.txt", "hello");
|
|
65
|
+
expect(await files.readText("sub/a.txt")).toBe("hello");
|
|
66
|
+
expect(await files.listFiles(".")).toEqual(["sub/a.txt"]);
|
|
67
|
+
expect(await files.listFiles("sub")).toEqual(["sub/a.txt"]);
|
|
68
|
+
expect(await files.glob("**/*.txt")).toEqual(["sub/a.txt"]);
|
|
69
|
+
});
|
|
70
|
+
it("accepts an absolute path under the linked or the real root", async () => {
|
|
71
|
+
const { files, linkRoot } = await openThroughLink();
|
|
72
|
+
await files.writeFile(path.join(linkRoot, "a.txt"), "one");
|
|
73
|
+
expect(await files.readText(path.join(root, "a.txt"))).toBe("one");
|
|
74
|
+
});
|
|
75
|
+
it("still refuses links that leave the workspace", async () => {
|
|
76
|
+
await symlink(path.join(outside, "private.txt"), path.join(root, "link.txt"));
|
|
77
|
+
await symlink(outside, path.join(root, "linked-dir"));
|
|
78
|
+
const { files } = await openThroughLink();
|
|
79
|
+
await expect(files.readText("link.txt")).rejects.toThrow();
|
|
80
|
+
await expect(files.writeFile("linked-dir/new.txt", "x")).rejects.toThrow();
|
|
81
|
+
await expect(files.listFiles("linked-dir")).rejects.toThrow();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var run_command_exports = {};
|
|
20
|
+
__export(run_command_exports, {
|
|
21
|
+
runCommand: () => runCommand
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(run_command_exports);
|
|
24
|
+
var import_node_child_process = require("node:child_process");
|
|
25
|
+
const MAX_OUTPUT_CHARS = 10 * 1024 * 1024;
|
|
26
|
+
const KILL_GRACE_MS = 1e3;
|
|
27
|
+
const ABORTED_EXIT_CODE = 130;
|
|
28
|
+
function runCommand(command, options) {
|
|
29
|
+
const { cwd, env, timeout, signal } = options;
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (signal?.aborted) {
|
|
32
|
+
resolve({ exitCode: ABORTED_EXIT_CODE, stdout: "", stderr: "(Command was cancelled)" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const child = (0, import_node_child_process.spawn)(command, {
|
|
36
|
+
cwd,
|
|
37
|
+
env,
|
|
38
|
+
shell: true,
|
|
39
|
+
detached: process.platform !== "win32"
|
|
40
|
+
});
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
let timedOut = false;
|
|
44
|
+
let aborted = false;
|
|
45
|
+
let killTimer;
|
|
46
|
+
const stop = () => {
|
|
47
|
+
signalProcessGroup(child, "SIGTERM");
|
|
48
|
+
killTimer ??= setTimeout(() => signalProcessGroup(child, "SIGKILL"), KILL_GRACE_MS);
|
|
49
|
+
};
|
|
50
|
+
const timeoutTimer = timeout === void 0 ? void 0 : setTimeout(() => {
|
|
51
|
+
timedOut = true;
|
|
52
|
+
stop();
|
|
53
|
+
}, timeout);
|
|
54
|
+
const onAbort = () => {
|
|
55
|
+
aborted = true;
|
|
56
|
+
stop();
|
|
57
|
+
};
|
|
58
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
const cleanUp = () => {
|
|
60
|
+
clearTimeout(timeoutTimer);
|
|
61
|
+
clearTimeout(killTimer);
|
|
62
|
+
signal?.removeEventListener("abort", onAbort);
|
|
63
|
+
};
|
|
64
|
+
child.stdout?.on("data", (chunk) => {
|
|
65
|
+
stdout = appendCapped(stdout, chunk.toString());
|
|
66
|
+
});
|
|
67
|
+
child.stderr?.on("data", (chunk) => {
|
|
68
|
+
stderr = appendCapped(stderr, chunk.toString());
|
|
69
|
+
});
|
|
70
|
+
child.once("error", (error) => {
|
|
71
|
+
cleanUp();
|
|
72
|
+
reject(error);
|
|
73
|
+
});
|
|
74
|
+
child.once("close", (code) => {
|
|
75
|
+
if (timedOut || aborted) signalProcessGroup(child, "SIGKILL");
|
|
76
|
+
cleanUp();
|
|
77
|
+
if (aborted) {
|
|
78
|
+
resolve({
|
|
79
|
+
exitCode: ABORTED_EXIT_CODE,
|
|
80
|
+
stdout: `${stdout}
|
|
81
|
+
|
|
82
|
+
(Command was cancelled)`,
|
|
83
|
+
stderr
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
resolve({ exitCode: code ?? 1, stdout, stderr, ...timedOut && { timedOut } });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function appendCapped(current, chunk) {
|
|
92
|
+
return current.length >= MAX_OUTPUT_CHARS ? current : (current + chunk).slice(0, MAX_OUTPUT_CHARS);
|
|
93
|
+
}
|
|
94
|
+
function signalProcessGroup(child, signal) {
|
|
95
|
+
if (child.pid === void 0) return;
|
|
96
|
+
if (process.platform === "win32") {
|
|
97
|
+
(0, import_node_child_process.spawn)("taskkill", ["/pid", String(child.pid), "/f", "/t"], { stdio: "ignore" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
process.kill(-child.pid, signal);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
106
|
+
0 && (module.exports = {
|
|
107
|
+
runCommand
|
|
108
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RunCommandResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
timedOut?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Run a shell command in its own process group. A timeout or abort stops the whole group, so
|
|
9
|
+
* commands started by the shell stop too. Exits, including non-zero ones, are returned.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runCommand(command: string, options: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
env: Record<string, string>;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
signal?: AbortSignal | undefined;
|
|
16
|
+
}): Promise<RunCommandResult>;
|
|
17
|
+
//# sourceMappingURL=run-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../src/run-command.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IACL,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CACpC,GACF,OAAO,CAAC,gBAAgB,CAAC,CAqE3B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const MAX_OUTPUT_CHARS = 10 * 1024 * 1024;
|
|
3
|
+
const KILL_GRACE_MS = 1e3;
|
|
4
|
+
const ABORTED_EXIT_CODE = 130;
|
|
5
|
+
function runCommand(command, options) {
|
|
6
|
+
const { cwd, env, timeout, signal } = options;
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
if (signal?.aborted) {
|
|
9
|
+
resolve({ exitCode: ABORTED_EXIT_CODE, stdout: "", stderr: "(Command was cancelled)" });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const child = spawn(command, {
|
|
13
|
+
cwd,
|
|
14
|
+
env,
|
|
15
|
+
shell: true,
|
|
16
|
+
detached: process.platform !== "win32"
|
|
17
|
+
});
|
|
18
|
+
let stdout = "";
|
|
19
|
+
let stderr = "";
|
|
20
|
+
let timedOut = false;
|
|
21
|
+
let aborted = false;
|
|
22
|
+
let killTimer;
|
|
23
|
+
const stop = () => {
|
|
24
|
+
signalProcessGroup(child, "SIGTERM");
|
|
25
|
+
killTimer ??= setTimeout(() => signalProcessGroup(child, "SIGKILL"), KILL_GRACE_MS);
|
|
26
|
+
};
|
|
27
|
+
const timeoutTimer = timeout === void 0 ? void 0 : setTimeout(() => {
|
|
28
|
+
timedOut = true;
|
|
29
|
+
stop();
|
|
30
|
+
}, timeout);
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
aborted = true;
|
|
33
|
+
stop();
|
|
34
|
+
};
|
|
35
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
36
|
+
const cleanUp = () => {
|
|
37
|
+
clearTimeout(timeoutTimer);
|
|
38
|
+
clearTimeout(killTimer);
|
|
39
|
+
signal?.removeEventListener("abort", onAbort);
|
|
40
|
+
};
|
|
41
|
+
child.stdout?.on("data", (chunk) => {
|
|
42
|
+
stdout = appendCapped(stdout, chunk.toString());
|
|
43
|
+
});
|
|
44
|
+
child.stderr?.on("data", (chunk) => {
|
|
45
|
+
stderr = appendCapped(stderr, chunk.toString());
|
|
46
|
+
});
|
|
47
|
+
child.once("error", (error) => {
|
|
48
|
+
cleanUp();
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
child.once("close", (code) => {
|
|
52
|
+
if (timedOut || aborted) signalProcessGroup(child, "SIGKILL");
|
|
53
|
+
cleanUp();
|
|
54
|
+
if (aborted) {
|
|
55
|
+
resolve({
|
|
56
|
+
exitCode: ABORTED_EXIT_CODE,
|
|
57
|
+
stdout: `${stdout}
|
|
58
|
+
|
|
59
|
+
(Command was cancelled)`,
|
|
60
|
+
stderr
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve({ exitCode: code ?? 1, stdout, stderr, ...timedOut && { timedOut } });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function appendCapped(current, chunk) {
|
|
69
|
+
return current.length >= MAX_OUTPUT_CHARS ? current : (current + chunk).slice(0, MAX_OUTPUT_CHARS);
|
|
70
|
+
}
|
|
71
|
+
function signalProcessGroup(child, signal) {
|
|
72
|
+
if (child.pid === void 0) return;
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
spawn("taskkill", ["/pid", String(child.pid), "/f", "/t"], { stdio: "ignore" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
process.kill(-child.pid, signal);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
runCommand
|
|
84
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var import_promises = require("node:fs/promises");
|
|
25
|
+
var import_node_os = require("node:os");
|
|
26
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
27
|
+
var import_vitest = require("vitest");
|
|
28
|
+
var import_workspace = require("@dexto/core/workspace");
|
|
29
|
+
var import_tools_process = require("@dexto/tools-process");
|
|
30
|
+
var import_local_workspace_handle_provider = require("./local-workspace-handle-provider.js");
|
|
31
|
+
function createLogger() {
|
|
32
|
+
const logger = {
|
|
33
|
+
debug: () => {
|
|
34
|
+
},
|
|
35
|
+
info: () => {
|
|
36
|
+
},
|
|
37
|
+
warn: () => {
|
|
38
|
+
},
|
|
39
|
+
error: () => {
|
|
40
|
+
},
|
|
41
|
+
createChild: () => createLogger()
|
|
42
|
+
};
|
|
43
|
+
return logger;
|
|
44
|
+
}
|
|
45
|
+
function createWorkspaceStore() {
|
|
46
|
+
const workspaces = /* @__PURE__ */ new Map();
|
|
47
|
+
let currentId;
|
|
48
|
+
return {
|
|
49
|
+
saveWorkspace: async ({ workspace }) => {
|
|
50
|
+
workspaces.set(workspace.id, workspace);
|
|
51
|
+
},
|
|
52
|
+
getWorkspace: async ({ id }) => workspaces.get(id),
|
|
53
|
+
findWorkspaceByPath: async ({ path: workspacePath }) => [...workspaces.values()].find((workspace) => workspace.path === workspacePath),
|
|
54
|
+
listWorkspaces: async () => [...workspaces.values()],
|
|
55
|
+
setCurrentWorkspace: async ({ id }) => {
|
|
56
|
+
currentId = id;
|
|
57
|
+
},
|
|
58
|
+
getCurrentWorkspaceId: async () => currentId,
|
|
59
|
+
clearCurrentWorkspace: async () => {
|
|
60
|
+
currentId = void 0;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
(0, import_vitest.describe)("bash_exec in a workspace", () => {
|
|
65
|
+
let directory;
|
|
66
|
+
(0, import_vitest.beforeEach)(async () => {
|
|
67
|
+
directory = await (0, import_promises.mkdtemp)(import_node_path.default.join((0, import_node_os.tmpdir)(), "workspace-bash-"));
|
|
68
|
+
});
|
|
69
|
+
(0, import_vitest.afterEach)(async () => {
|
|
70
|
+
import_vitest.vi.unstubAllEnvs();
|
|
71
|
+
await (0, import_promises.rm)(directory, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
async function runForeground(command, config = {}) {
|
|
74
|
+
const workspaceManager = new import_workspace.WorkspaceManager(
|
|
75
|
+
createWorkspaceStore(),
|
|
76
|
+
{ emit: () => true },
|
|
77
|
+
createLogger(),
|
|
78
|
+
new import_local_workspace_handle_provider.LocalWorkspaceHandleProvider()
|
|
79
|
+
);
|
|
80
|
+
await workspaceManager.setWorkspace({ path: directory });
|
|
81
|
+
const bashExec = import_tools_process.processToolsFactory.create(
|
|
82
|
+
import_tools_process.processToolsFactory.configSchema.parse({
|
|
83
|
+
type: "process-tools",
|
|
84
|
+
securityLevel: "permissive",
|
|
85
|
+
...config
|
|
86
|
+
})
|
|
87
|
+
).find((tool) => tool.id === "bash_exec");
|
|
88
|
+
if (bashExec === void 0) {
|
|
89
|
+
throw new Error("Expected bash_exec tool to be registered.");
|
|
90
|
+
}
|
|
91
|
+
const context = {
|
|
92
|
+
logger: createLogger(),
|
|
93
|
+
services: { workspaceManager }
|
|
94
|
+
};
|
|
95
|
+
return await bashExec.execute(
|
|
96
|
+
{ command, run_in_background: false, timeout: 2e4 },
|
|
97
|
+
context
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const printVariables = 'echo "${DEXTO_TEST_HOST_MARKER:-absent}|${DEXTO_TEST_CONFIGURED:-absent}|${DEXTO_TEST_FORWARDED:-absent}"';
|
|
101
|
+
(0, import_vitest.it)("does not pass host variables to the command, but passes configured ones", async () => {
|
|
102
|
+
import_vitest.vi.stubEnv("DEXTO_TEST_HOST_MARKER", "leaked-host-value");
|
|
103
|
+
import_vitest.vi.stubEnv("DEXTO_TEST_FORWARDED", "forwarded-host-value");
|
|
104
|
+
const result = await runForeground(printVariables, {
|
|
105
|
+
environment: { DEXTO_TEST_CONFIGURED: "configured-value" }
|
|
106
|
+
});
|
|
107
|
+
(0, import_vitest.expect)(result).toMatchObject({ stdout: "absent|configured-value|absent\n" });
|
|
108
|
+
});
|
|
109
|
+
(0, import_vitest.it)("forwards host variables the user names in passEnvironment", async () => {
|
|
110
|
+
import_vitest.vi.stubEnv("DEXTO_TEST_HOST_MARKER", "leaked-host-value");
|
|
111
|
+
import_vitest.vi.stubEnv("DEXTO_TEST_FORWARDED", "forwarded-host-value");
|
|
112
|
+
const result = await runForeground(printVariables, {
|
|
113
|
+
passEnvironment: ["DEXTO_TEST_FORWARDED"]
|
|
114
|
+
});
|
|
115
|
+
(0, import_vitest.expect)(result).toMatchObject({ stdout: "absent|absent|forwarded-host-value\n" });
|
|
116
|
+
});
|
|
117
|
+
(0, import_vitest.it)("still finds commands on PATH", async () => {
|
|
118
|
+
const result = await runForeground("echo ready");
|
|
119
|
+
(0, import_vitest.expect)(result).toMatchObject({ stdout: "ready\n" });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-bash-environment.test.d.ts","sourceRoot":"","sources":["../src/workspace-bash-environment.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { WorkspaceManager } from "@dexto/core/workspace";
|
|
6
|
+
import { processToolsFactory } from "@dexto/tools-process";
|
|
7
|
+
import { LocalWorkspaceHandleProvider } from "./local-workspace-handle-provider.js";
|
|
8
|
+
function createLogger() {
|
|
9
|
+
const logger = {
|
|
10
|
+
debug: () => {
|
|
11
|
+
},
|
|
12
|
+
info: () => {
|
|
13
|
+
},
|
|
14
|
+
warn: () => {
|
|
15
|
+
},
|
|
16
|
+
error: () => {
|
|
17
|
+
},
|
|
18
|
+
createChild: () => createLogger()
|
|
19
|
+
};
|
|
20
|
+
return logger;
|
|
21
|
+
}
|
|
22
|
+
function createWorkspaceStore() {
|
|
23
|
+
const workspaces = /* @__PURE__ */ new Map();
|
|
24
|
+
let currentId;
|
|
25
|
+
return {
|
|
26
|
+
saveWorkspace: async ({ workspace }) => {
|
|
27
|
+
workspaces.set(workspace.id, workspace);
|
|
28
|
+
},
|
|
29
|
+
getWorkspace: async ({ id }) => workspaces.get(id),
|
|
30
|
+
findWorkspaceByPath: async ({ path: workspacePath }) => [...workspaces.values()].find((workspace) => workspace.path === workspacePath),
|
|
31
|
+
listWorkspaces: async () => [...workspaces.values()],
|
|
32
|
+
setCurrentWorkspace: async ({ id }) => {
|
|
33
|
+
currentId = id;
|
|
34
|
+
},
|
|
35
|
+
getCurrentWorkspaceId: async () => currentId,
|
|
36
|
+
clearCurrentWorkspace: async () => {
|
|
37
|
+
currentId = void 0;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
describe("bash_exec in a workspace", () => {
|
|
42
|
+
let directory;
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
directory = await mkdtemp(path.join(tmpdir(), "workspace-bash-"));
|
|
45
|
+
});
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
vi.unstubAllEnvs();
|
|
48
|
+
await rm(directory, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
async function runForeground(command, config = {}) {
|
|
51
|
+
const workspaceManager = new WorkspaceManager(
|
|
52
|
+
createWorkspaceStore(),
|
|
53
|
+
{ emit: () => true },
|
|
54
|
+
createLogger(),
|
|
55
|
+
new LocalWorkspaceHandleProvider()
|
|
56
|
+
);
|
|
57
|
+
await workspaceManager.setWorkspace({ path: directory });
|
|
58
|
+
const bashExec = processToolsFactory.create(
|
|
59
|
+
processToolsFactory.configSchema.parse({
|
|
60
|
+
type: "process-tools",
|
|
61
|
+
securityLevel: "permissive",
|
|
62
|
+
...config
|
|
63
|
+
})
|
|
64
|
+
).find((tool) => tool.id === "bash_exec");
|
|
65
|
+
if (bashExec === void 0) {
|
|
66
|
+
throw new Error("Expected bash_exec tool to be registered.");
|
|
67
|
+
}
|
|
68
|
+
const context = {
|
|
69
|
+
logger: createLogger(),
|
|
70
|
+
services: { workspaceManager }
|
|
71
|
+
};
|
|
72
|
+
return await bashExec.execute(
|
|
73
|
+
{ command, run_in_background: false, timeout: 2e4 },
|
|
74
|
+
context
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const printVariables = 'echo "${DEXTO_TEST_HOST_MARKER:-absent}|${DEXTO_TEST_CONFIGURED:-absent}|${DEXTO_TEST_FORWARDED:-absent}"';
|
|
78
|
+
it("does not pass host variables to the command, but passes configured ones", async () => {
|
|
79
|
+
vi.stubEnv("DEXTO_TEST_HOST_MARKER", "leaked-host-value");
|
|
80
|
+
vi.stubEnv("DEXTO_TEST_FORWARDED", "forwarded-host-value");
|
|
81
|
+
const result = await runForeground(printVariables, {
|
|
82
|
+
environment: { DEXTO_TEST_CONFIGURED: "configured-value" }
|
|
83
|
+
});
|
|
84
|
+
expect(result).toMatchObject({ stdout: "absent|configured-value|absent\n" });
|
|
85
|
+
});
|
|
86
|
+
it("forwards host variables the user names in passEnvironment", async () => {
|
|
87
|
+
vi.stubEnv("DEXTO_TEST_HOST_MARKER", "leaked-host-value");
|
|
88
|
+
vi.stubEnv("DEXTO_TEST_FORWARDED", "forwarded-host-value");
|
|
89
|
+
const result = await runForeground(printVariables, {
|
|
90
|
+
passEnvironment: ["DEXTO_TEST_FORWARDED"]
|
|
91
|
+
});
|
|
92
|
+
expect(result).toMatchObject({ stdout: "absent|absent|forwarded-host-value\n" });
|
|
93
|
+
});
|
|
94
|
+
it("still finds commands on PATH", async () => {
|
|
95
|
+
const result = await runForeground("echo ready");
|
|
96
|
+
expect(result).toMatchObject({ stdout: "ready\n" });
|
|
97
|
+
});
|
|
98
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexto/image-local",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Local development base image for Dexto agents with filesystem and process tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"glob": "^14.0.3",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@dexto/agent-config": "1.
|
|
26
|
-
"@dexto/agent-management": "1.
|
|
27
|
-
"@dexto/core": "1.
|
|
28
|
-
"@dexto/storage": "1.
|
|
29
|
-
"@dexto/tools-builtins": "1.
|
|
30
|
-
"@dexto/tools-filesystem": "1.
|
|
31
|
-
"@dexto/tools-lifecycle": "1.
|
|
32
|
-
"@dexto/tools-plan": "1.
|
|
33
|
-
"@dexto/tools-process": "1.
|
|
34
|
-
"@dexto/tools-scheduler": "1.
|
|
35
|
-
"@dexto/tools-todo": "1.
|
|
25
|
+
"@dexto/agent-config": "1.17.0",
|
|
26
|
+
"@dexto/agent-management": "1.17.0",
|
|
27
|
+
"@dexto/core": "1.17.0",
|
|
28
|
+
"@dexto/storage": "1.17.0",
|
|
29
|
+
"@dexto/tools-builtins": "1.17.0",
|
|
30
|
+
"@dexto/tools-filesystem": "1.17.0",
|
|
31
|
+
"@dexto/tools-lifecycle": "1.17.0",
|
|
32
|
+
"@dexto/tools-plan": "1.17.0",
|
|
33
|
+
"@dexto/tools-process": "1.17.0",
|
|
34
|
+
"@dexto/tools-scheduler": "1.17.0",
|
|
35
|
+
"@dexto/tools-todo": "1.17.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"tsup": "^8.0.0",
|