leduo-patrol 2.0.0 → 2.0.1
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 +5 -2
- package/dist/server/__tests__/claude-cli-session.test.js +24 -0
- package/dist/server/__tests__/server-helpers.test.js +17 -1
- package/dist/server/__tests__/shell-session.test.js +35 -0
- package/dist/server/claude-cli-session.js +56 -12
- package/dist/server/index.js +2 -1
- package/dist/server/server-helpers.js +22 -0
- package/dist/server/session-manager.js +18 -0
- package/dist/server/shell-session.js +59 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
- Node.js 22+
|
|
98
98
|
- 已能正常运行 Claude Code
|
|
99
99
|
- 服务器环境里已配置 `ANTHROPIC_API_KEY`
|
|
100
|
+
- 若 `claude` 不在默认 `PATH` 中,请设置 `LEDUO_PATROL_CLAUDE_BIN=/absolute/path/to/claude`
|
|
100
101
|
|
|
101
102
|
## 启动
|
|
102
103
|
|
|
@@ -148,15 +149,17 @@ LEDUO_PATROL_BIND_MODE=server
|
|
|
148
149
|
LEDUO_PATROL_APP_NAME=乐多汪汪队
|
|
149
150
|
LEDUO_PATROL_WORKSPACE_PATH=/absolute/workspace/path
|
|
150
151
|
LEDUO_PATROL_ALLOWED_ROOTS=/absolute/workspace/path,/another/allowed/root
|
|
152
|
+
LEDUO_PATROL_CLAUDE_BIN=/absolute/path/to/claude
|
|
153
|
+
LEDUO_PATROL_SHELL=/absolute/path/to/zsh
|
|
151
154
|
ANTHROPIC_API_KEY=sk-...
|
|
152
155
|
LEDUO_PATROL_ACCESS_KEY=your-fixed-key
|
|
153
|
-
LEDUO_PATROL_AGENT_BIN=/absolute/path/to/claude-code-acp
|
|
154
156
|
LEDUO_ENABLE_SHELL=true
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
如果设置了 `LEDUO_PATROL_ALLOWED_ROOTS`,网页中只能连接这些根目录之下的路径;未设置时默认只允许启动命令所在目录。
|
|
158
160
|
如果未设置 `LEDUO_PATROL_WORKSPACE_PATH`,默认工作目录为启动命令所在目录(`process.cwd()`),并在启动日志中提示如何通过环境变量修改。
|
|
159
161
|
如果未设置 `LEDUO_PATROL_ALLOWED_ROOTS`,默认允许根目录同样为启动命令所在目录,并会在启动日志中提示可配置项。
|
|
162
|
+
如果发布安装后的内嵌终端无法启动,可通过 `LEDUO_PATROL_SHELL` 显式指定 shell 路径;例如 macOS 上常见的 `/bin/zsh`。
|
|
160
163
|
|
|
161
164
|
## 状态持久化
|
|
162
165
|
|
|
@@ -187,7 +190,7 @@ LEDUO_ENABLE_SHELL=true
|
|
|
187
190
|
|
|
188
191
|
```bash
|
|
189
192
|
LEDUO_PATROL_ACCESS_KEY=your-fixed-key
|
|
190
|
-
|
|
193
|
+
LEDUO_PATROL_CLAUDE_BIN=/absolute/path/to/claude
|
|
191
194
|
```
|
|
192
195
|
|
|
193
196
|
## 已知限制
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { claudeCliSessionTestables } from "../claude-cli-session.js";
|
|
7
|
+
test("claudeCliSessionTestables.findExecutableOnPath resolves commands from PATH entries", () => {
|
|
8
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "leduo-patrol-claude-bin-"));
|
|
9
|
+
const binaryName = process.platform === "win32" ? "claude.cmd" : "claude";
|
|
10
|
+
const binaryPath = path.join(tempDir, binaryName);
|
|
11
|
+
writeFileSync(binaryPath, process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n");
|
|
12
|
+
const resolved = claudeCliSessionTestables.findExecutableOnPath("claude", tempDir);
|
|
13
|
+
assert.equal(resolved, binaryPath);
|
|
14
|
+
});
|
|
15
|
+
test("claudeCliSessionTestables.resolveClaudeBin accepts explicit executable paths", () => {
|
|
16
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "leduo-patrol-claude-explicit-"));
|
|
17
|
+
const binaryPath = path.join(tempDir, "claude");
|
|
18
|
+
writeFileSync(binaryPath, "#!/bin/sh\n");
|
|
19
|
+
const resolved = claudeCliSessionTestables.resolveClaudeBin(binaryPath, { PATH: "" });
|
|
20
|
+
assert.equal(resolved, binaryPath);
|
|
21
|
+
});
|
|
22
|
+
test("claudeCliSessionTestables.resolveClaudeBin throws actionable error when claude is missing", () => {
|
|
23
|
+
assert.throws(() => claudeCliSessionTestables.resolveClaudeBin(undefined, { PATH: "" }), /LEDUO_PATROL_CLAUDE_BIN/);
|
|
24
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { buildSpawnFailureMessage, ensureDirectoryExistsSync, formatError, resolveAllowedPath, } from "../server-helpers.js";
|
|
5
7
|
test("server helpers formatError handles Error and primitives", () => {
|
|
6
8
|
assert.equal(formatError(new Error("boom")), "boom");
|
|
7
9
|
assert.equal(formatError("plain"), '"plain"');
|
|
@@ -16,3 +18,17 @@ test("server helpers resolveAllowedPath rejects outside roots", () => {
|
|
|
16
18
|
const root = path.resolve("/tmp/repo");
|
|
17
19
|
assert.throws(() => resolveAllowedPath("/etc", [root]), /outside allowed roots/);
|
|
18
20
|
});
|
|
21
|
+
test("server helpers ensureDirectoryExistsSync accepts existing directories", () => {
|
|
22
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "leduo-patrol-dir-"));
|
|
23
|
+
assert.equal(ensureDirectoryExistsSync(tempDir, "Workspace"), tempDir);
|
|
24
|
+
});
|
|
25
|
+
test("server helpers ensureDirectoryExistsSync rejects files and missing paths", () => {
|
|
26
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "leduo-patrol-file-"));
|
|
27
|
+
const filePath = path.join(tempDir, "file.txt");
|
|
28
|
+
writeFileSync(filePath, "hello");
|
|
29
|
+
assert.throws(() => ensureDirectoryExistsSync(filePath, "Workspace"), /is not a directory/);
|
|
30
|
+
assert.throws(() => ensureDirectoryExistsSync(path.join(tempDir, "missing"), "Workspace"), /does not exist/);
|
|
31
|
+
});
|
|
32
|
+
test("server helpers buildSpawnFailureMessage includes command cwd and hint", () => {
|
|
33
|
+
assert.equal(buildSpawnFailureMessage("shell", "/bin/zsh", "/repo", new Error("posix_spawnp failed"), "Try another shell."), 'Failed to start shell "/bin/zsh" in "/repo": posix_spawnp failed. Try another shell.');
|
|
34
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { shellSessionTestables } from "../shell-session.js";
|
|
4
|
+
test("shellSessionTestables.resolveShellLaunch prefers configured shell over defaults", () => {
|
|
5
|
+
const resolved = shellSessionTestables.resolveShellLaunch({
|
|
6
|
+
LEDUO_PATROL_SHELL: "/custom/bin/zsh",
|
|
7
|
+
SHELL: "/bin/bash",
|
|
8
|
+
}, (candidate) => candidate === "/custom/bin/zsh" || candidate === "/bin/bash");
|
|
9
|
+
assert.deepEqual(resolved, {
|
|
10
|
+
command: "/custom/bin/zsh",
|
|
11
|
+
args: ["-l"],
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
test("shellSessionTestables.resolveShellLaunch falls back to /bin/sh when bash and zsh are unavailable", () => {
|
|
15
|
+
const resolved = shellSessionTestables.resolveShellLaunch({
|
|
16
|
+
SHELL: "/missing/shell",
|
|
17
|
+
}, (candidate) => candidate === "/bin/sh");
|
|
18
|
+
assert.deepEqual(resolved, {
|
|
19
|
+
command: "/bin/sh",
|
|
20
|
+
args: [],
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
test("shellSessionTestables.resolveShellLaunch accepts plain configured shell names as a last resort", () => {
|
|
24
|
+
const resolved = shellSessionTestables.resolveShellLaunch({
|
|
25
|
+
LEDUO_PATROL_SHELL: "fish",
|
|
26
|
+
SHELL: "",
|
|
27
|
+
}, () => false);
|
|
28
|
+
assert.deepEqual(resolved, {
|
|
29
|
+
command: "fish",
|
|
30
|
+
args: ["-l"],
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
test("shellSessionTestables.resolveShellLaunch throws actionable error when no shell is available", () => {
|
|
34
|
+
assert.throws(() => shellSessionTestables.resolveShellLaunch({ SHELL: "" }, () => false), /LEDUO_PATROL_SHELL/);
|
|
35
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node-pty";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { buildSpawnFailureMessage, ensureDirectoryExistsSync } from "./server-helpers.js";
|
|
3
6
|
/**
|
|
4
7
|
* A PTY-backed session that runs the native Claude Code CLI.
|
|
5
8
|
*
|
|
@@ -15,24 +18,30 @@ export class ClaudeCliSession extends EventEmitter {
|
|
|
15
18
|
constructor(opts) {
|
|
16
19
|
super();
|
|
17
20
|
this.sessionId = opts.sessionId;
|
|
18
|
-
|
|
21
|
+
ensureDirectoryExistsSync(opts.workspacePath, "Session workspace");
|
|
22
|
+
const bin = resolveClaudeBin(opts.claudeBin);
|
|
19
23
|
const args = opts.resume
|
|
20
24
|
? ["--resume", opts.sessionId]
|
|
21
25
|
: ["--session-id", opts.sessionId];
|
|
22
26
|
if (opts.allowSkipPermissions) {
|
|
23
27
|
args.push("--allow-dangerously-skip-permissions");
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
try {
|
|
30
|
+
this.pty = spawn(bin, args, {
|
|
31
|
+
name: "xterm-256color",
|
|
32
|
+
cols: opts.cols ?? 80,
|
|
33
|
+
rows: opts.rows ?? 24,
|
|
34
|
+
cwd: opts.workspacePath,
|
|
35
|
+
env: {
|
|
36
|
+
...process.env,
|
|
37
|
+
TERM: "xterm-256color",
|
|
38
|
+
COLORTERM: "truecolor",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(buildSpawnFailureMessage("Claude CLI", bin, opts.workspacePath, error, "Check that the command is installed correctly, or override it with LEDUO_PATROL_CLAUDE_BIN."));
|
|
44
|
+
}
|
|
36
45
|
this.pty.onData((data) => {
|
|
37
46
|
this.emit("output", data);
|
|
38
47
|
});
|
|
@@ -61,3 +70,38 @@ export class ClaudeCliSession extends EventEmitter {
|
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
}
|
|
73
|
+
function findExecutableOnPath(command, envPath = process.env.PATH) {
|
|
74
|
+
if (!envPath)
|
|
75
|
+
return null;
|
|
76
|
+
const extensions = process.platform === "win32"
|
|
77
|
+
? (process.env.PATHEXT?.split(";").filter(Boolean) ?? [".exe", ".cmd", ".bat"])
|
|
78
|
+
: [""];
|
|
79
|
+
for (const entry of envPath.split(path.delimiter).filter(Boolean)) {
|
|
80
|
+
for (const extension of extensions) {
|
|
81
|
+
const candidate = path.join(entry, `${command}${extension}`);
|
|
82
|
+
if (existsSync(candidate)) {
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function resolveClaudeBin(configuredBin, env = process.env) {
|
|
90
|
+
const candidate = configuredBin?.trim() || "claude";
|
|
91
|
+
const hasExplicitPath = candidate.includes("/") || candidate.includes("\\");
|
|
92
|
+
if (hasExplicitPath) {
|
|
93
|
+
if (existsSync(candidate)) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Claude CLI not found at "${candidate}". Set LEDUO_PATROL_CLAUDE_BIN to a valid Claude executable path.`);
|
|
97
|
+
}
|
|
98
|
+
const resolved = findExecutableOnPath(candidate, env.PATH);
|
|
99
|
+
if (resolved) {
|
|
100
|
+
return resolved;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Claude CLI "${candidate}" was not found in PATH. Install Claude Code first, or set LEDUO_PATROL_CLAUDE_BIN=/absolute/path/to/claude.`);
|
|
103
|
+
}
|
|
104
|
+
export const claudeCliSessionTestables = {
|
|
105
|
+
findExecutableOnPath,
|
|
106
|
+
resolveClaudeBin,
|
|
107
|
+
};
|
package/dist/server/index.js
CHANGED
|
@@ -27,7 +27,8 @@ const vscodeRemoteUri = process.env.LEDUO_PATROL_VSCODE_URI ??
|
|
|
27
27
|
(sshHost ? `vscode://vscode-remote/ssh-remote+${encodeURIComponent(sshHost)}${sshPath}` : "");
|
|
28
28
|
const requestedPort = Number(process.env.PORT ?? 3001);
|
|
29
29
|
const devWebPort = Number(process.env.LEDUO_PATROL_WEB_PORT ?? 5173);
|
|
30
|
-
const
|
|
30
|
+
const npmLifecycleEvent = process.env.npm_lifecycle_event ?? "";
|
|
31
|
+
const isDevServer = npmLifecycleEvent === "dev:server" || npmLifecycleEvent === "dev:server:local";
|
|
31
32
|
const bindMode = await resolveBindMode();
|
|
32
33
|
const listenHost = bindMode === "local" ? "127.0.0.1" : "0.0.0.0";
|
|
33
34
|
const launchHost = bindMode === "local" ? "127.0.0.1" : pickPreferredLanIp();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
export function formatError(error) {
|
|
3
4
|
if (error instanceof Error) {
|
|
@@ -21,3 +22,24 @@ export function resolveAllowedPath(requestedPath, allowedRoots) {
|
|
|
21
22
|
}
|
|
22
23
|
return resolvedPath;
|
|
23
24
|
}
|
|
25
|
+
export function ensureDirectoryExistsSync(requestedPath, label) {
|
|
26
|
+
try {
|
|
27
|
+
const stats = statSync(requestedPath);
|
|
28
|
+
if (!stats.isDirectory()) {
|
|
29
|
+
throw new Error(`${label} is not a directory: ${requestedPath}`);
|
|
30
|
+
}
|
|
31
|
+
return requestedPath;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof Error && error.message.includes("is not a directory")) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`${label} does not exist or is not accessible: ${requestedPath}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function buildSpawnFailureMessage(commandLabel, command, cwd, error, hint) {
|
|
41
|
+
const quotedCommand = JSON.stringify(command);
|
|
42
|
+
const quotedCwd = JSON.stringify(cwd);
|
|
43
|
+
const message = `Failed to start ${commandLabel} ${quotedCommand} in ${quotedCwd}: ${formatError(error)}`;
|
|
44
|
+
return hint ? `${message}. ${hint}` : message;
|
|
45
|
+
}
|
|
@@ -56,7 +56,13 @@ export class SessionManager {
|
|
|
56
56
|
}
|
|
57
57
|
async initialize() {
|
|
58
58
|
const persistedState = await this.readPersistedState();
|
|
59
|
+
let skippedPersistedSessions = false;
|
|
59
60
|
for (const persisted of persistedState.sessions) {
|
|
61
|
+
if (!(await this.isRestorableWorkspace(persisted.workspacePath))) {
|
|
62
|
+
skippedPersistedSessions = true;
|
|
63
|
+
console.warn(`[SessionManager] Skipping persisted session with unavailable workspace: ${persisted.workspacePath}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
60
66
|
const snapshot = {
|
|
61
67
|
clientSessionId: persisted.clientSessionId,
|
|
62
68
|
title: persisted.title,
|
|
@@ -75,6 +81,9 @@ export class SessionManager {
|
|
|
75
81
|
this.sessionIdIndex.set(snapshot.sessionId, snapshot.clientSessionId);
|
|
76
82
|
this.activityMonitor.watch(snapshot.sessionId, snapshot.workspacePath);
|
|
77
83
|
}
|
|
84
|
+
if (skippedPersistedSessions) {
|
|
85
|
+
await this.writePersistedState().catch(() => undefined);
|
|
86
|
+
}
|
|
78
87
|
for (const entry of this.sessions.values()) {
|
|
79
88
|
this.startCliSession(entry, true).catch((error) => {
|
|
80
89
|
this.handleManagerError(entry.snapshot.clientSessionId, error);
|
|
@@ -279,6 +288,15 @@ export class SessionManager {
|
|
|
279
288
|
await access(resolvedWorkspacePath);
|
|
280
289
|
return resolvedWorkspacePath;
|
|
281
290
|
}
|
|
291
|
+
async isRestorableWorkspace(workspacePath) {
|
|
292
|
+
try {
|
|
293
|
+
const workspaceStats = await stat(workspacePath);
|
|
294
|
+
return workspaceStats.isDirectory();
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
282
300
|
// ---------------------------------------------------------------------------
|
|
283
301
|
// /clear detection → session ID discovery
|
|
284
302
|
// ---------------------------------------------------------------------------
|
|
@@ -1,45 +1,80 @@
|
|
|
1
1
|
import { spawn } from "node-pty";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { buildSpawnFailureMessage, ensureDirectoryExistsSync } from "./server-helpers.js";
|
|
6
|
+
function buildShellLoginArgs(command) {
|
|
7
|
+
const shellName = path.basename(command).toLowerCase();
|
|
8
|
+
if (["bash", "zsh", "ksh", "fish"].includes(shellName)) {
|
|
9
|
+
return ["-l"];
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
function resolveShellLaunch(env = process.env, shellExists = existsSync) {
|
|
14
|
+
const configuredShell = env.LEDUO_PATROL_SHELL?.trim() ?? "";
|
|
15
|
+
const loginShell = env.SHELL?.trim() ?? "";
|
|
16
|
+
const absoluteCandidates = [
|
|
17
|
+
configuredShell,
|
|
18
|
+
loginShell,
|
|
19
|
+
"/bin/bash",
|
|
20
|
+
"/bin/zsh",
|
|
21
|
+
"/bin/sh",
|
|
22
|
+
"/usr/bin/bash",
|
|
23
|
+
"/usr/bin/zsh",
|
|
24
|
+
"/usr/local/bin/bash",
|
|
25
|
+
"/usr/local/bin/zsh",
|
|
26
|
+
"/opt/homebrew/bin/bash",
|
|
27
|
+
"/opt/homebrew/bin/zsh",
|
|
28
|
+
].filter((candidate, index, all) => candidate && all.indexOf(candidate) === index);
|
|
29
|
+
for (const candidate of absoluteCandidates) {
|
|
30
|
+
if (shellExists(candidate)) {
|
|
31
|
+
return {
|
|
32
|
+
command: candidate,
|
|
33
|
+
args: buildShellLoginArgs(candidate),
|
|
34
|
+
};
|
|
13
35
|
}
|
|
14
36
|
}
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
if (configuredShell) {
|
|
38
|
+
return {
|
|
39
|
+
command: configuredShell,
|
|
40
|
+
args: buildShellLoginArgs(configuredShell),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`No supported shell was found. Set LEDUO_PATROL_SHELL to an absolute shell path if your system does not provide /bin/bash, /bin/zsh, or /bin/sh.`);
|
|
17
44
|
}
|
|
18
45
|
/**
|
|
19
46
|
* An interactive shell session backed by a PTY.
|
|
20
47
|
*
|
|
21
|
-
* Spawns
|
|
22
|
-
*
|
|
23
|
-
* pyenv, nvm, brew, cargo, aliases, etc. are
|
|
48
|
+
* Spawns the best-available interactive shell for the host environment.
|
|
49
|
+
* We prefer a login shell when possible so that user toolchains such as
|
|
50
|
+
* pyenv, nvm, brew, cargo, aliases, etc. are available in published installs too.
|
|
24
51
|
*/
|
|
25
52
|
export class ShellSession extends EventEmitter {
|
|
26
53
|
pty;
|
|
27
54
|
_alive = true;
|
|
28
55
|
constructor(workspacePath, cols = 80, rows = 24) {
|
|
29
56
|
super();
|
|
57
|
+
ensureDirectoryExistsSync(workspacePath, "Shell workspace");
|
|
58
|
+
const shellLaunch = resolveShellLaunch();
|
|
30
59
|
const env = {
|
|
31
60
|
...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] != null)),
|
|
32
61
|
TERM: "xterm-256color",
|
|
33
62
|
COLORTERM: "truecolor",
|
|
34
63
|
PWD: workspacePath,
|
|
64
|
+
SHELL: shellLaunch.command,
|
|
35
65
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
try {
|
|
67
|
+
this.pty = spawn(shellLaunch.command, shellLaunch.args, {
|
|
68
|
+
name: "xterm-256color",
|
|
69
|
+
cols,
|
|
70
|
+
rows,
|
|
71
|
+
cwd: workspacePath,
|
|
72
|
+
env,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(buildSpawnFailureMessage("shell", shellLaunch.command, workspacePath, error, "Set LEDUO_PATROL_SHELL to a valid shell path if this environment uses a non-standard shell location."));
|
|
77
|
+
}
|
|
43
78
|
this.pty.onData((data) => {
|
|
44
79
|
this.emit("output", data);
|
|
45
80
|
});
|
|
@@ -68,3 +103,7 @@ export class ShellSession extends EventEmitter {
|
|
|
68
103
|
}
|
|
69
104
|
}
|
|
70
105
|
}
|
|
106
|
+
export const shellSessionTestables = {
|
|
107
|
+
buildShellLoginArgs,
|
|
108
|
+
resolveShellLaunch,
|
|
109
|
+
};
|