@aliou/pi-processes 0.4.2 → 0.4.4
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/commands/settings-command.ts +29 -0
- package/config.ts +8 -0
- package/hooks/widget.ts +2 -1
- package/index.ts +3 -1
- package/manager.ts +11 -8
- package/package.json +2 -2
- package/utils/command-executor.test.ts +47 -0
- package/utils/command-executor.ts +56 -0
|
@@ -71,6 +71,28 @@ export function registerProcessesSettings(
|
|
|
71
71
|
},
|
|
72
72
|
],
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
label: "Execution",
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
id: "execution.shellPath",
|
|
79
|
+
label: "Shell path",
|
|
80
|
+
description:
|
|
81
|
+
"Absolute shell path override used to execute commands",
|
|
82
|
+
currentValue:
|
|
83
|
+
tabConfig?.execution?.shellPath ??
|
|
84
|
+
resolved.execution.shellPath ??
|
|
85
|
+
"auto",
|
|
86
|
+
values: [
|
|
87
|
+
"auto",
|
|
88
|
+
"/run/current-system/sw/bin/bash",
|
|
89
|
+
"/bin/bash",
|
|
90
|
+
"/usr/bin/bash",
|
|
91
|
+
"/usr/local/bin/bash",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
74
96
|
{
|
|
75
97
|
label: "Widget",
|
|
76
98
|
items: [
|
|
@@ -97,6 +119,13 @@ export function registerProcessesSettings(
|
|
|
97
119
|
updated.widget.showStatusWidget = newValue === "on";
|
|
98
120
|
return updated;
|
|
99
121
|
}
|
|
122
|
+
if (id === "execution.shellPath") {
|
|
123
|
+
if (!updated.execution) updated.execution = {};
|
|
124
|
+
updated.execution.shellPath =
|
|
125
|
+
newValue === "auto" ? undefined : newValue;
|
|
126
|
+
return updated;
|
|
127
|
+
}
|
|
128
|
+
|
|
100
129
|
// Numeric fields.
|
|
101
130
|
const num = Number.parseInt(newValue, 10);
|
|
102
131
|
if (Number.isNaN(num)) return null;
|
package/config.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface ProcessesConfig {
|
|
|
20
20
|
/** Hard cap on output lines returned to the agent. */
|
|
21
21
|
maxOutputLines?: number;
|
|
22
22
|
};
|
|
23
|
+
execution?: {
|
|
24
|
+
/** Absolute shell path override. Leave unset to auto-resolve. */
|
|
25
|
+
shellPath?: string;
|
|
26
|
+
};
|
|
23
27
|
widget?: {
|
|
24
28
|
/** Show the status widget below the editor. */
|
|
25
29
|
showStatusWidget?: boolean;
|
|
@@ -35,6 +39,9 @@ export interface ResolvedProcessesConfig {
|
|
|
35
39
|
defaultTailLines: number;
|
|
36
40
|
maxOutputLines: number;
|
|
37
41
|
};
|
|
42
|
+
execution: {
|
|
43
|
+
shellPath?: string;
|
|
44
|
+
};
|
|
38
45
|
widget: {
|
|
39
46
|
showStatusWidget: boolean;
|
|
40
47
|
};
|
|
@@ -49,6 +56,7 @@ const DEFAULT_CONFIG: ResolvedProcessesConfig = {
|
|
|
49
56
|
defaultTailLines: 100,
|
|
50
57
|
maxOutputLines: 200,
|
|
51
58
|
},
|
|
59
|
+
execution: {},
|
|
52
60
|
widget: {
|
|
53
61
|
showStatusWidget: true,
|
|
54
62
|
},
|
package/hooks/widget.ts
CHANGED
|
@@ -126,8 +126,9 @@ export function setupProcessWidget(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
pi.on("session_start", async (_event, ctx) => {
|
|
129
|
+
// Startup defer: capture context only. First render happens on process
|
|
130
|
+
// manager events or explicit settings updates.
|
|
129
131
|
latestContext = ctx;
|
|
130
|
-
updateWidget();
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
pi.on("session_switch", async (_event, ctx) => {
|
package/index.ts
CHANGED
|
@@ -16,7 +16,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
await configLoader.load();
|
|
19
|
-
const manager = new ProcessManager(
|
|
19
|
+
const manager = new ProcessManager({
|
|
20
|
+
getConfiguredShellPath: () => configLoader.getConfig().execution.shellPath,
|
|
21
|
+
});
|
|
20
22
|
|
|
21
23
|
const { update: updateWidget } = setupProcessesHooks(pi, manager);
|
|
22
24
|
setupProcessesCommands(pi, manager);
|
package/manager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type StartOptions,
|
|
20
20
|
} from "./constants";
|
|
21
21
|
import { isProcessGroupAlive, killProcessGroup } from "./utils";
|
|
22
|
+
import { spawnCommand } from "./utils/command-executor";
|
|
22
23
|
|
|
23
24
|
interface ManagedProcess extends ProcessInfo {
|
|
24
25
|
process: ChildProcess;
|
|
@@ -26,16 +27,23 @@ interface ManagedProcess extends ProcessInfo {
|
|
|
26
27
|
combinedFile: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
interface ProcessManagerOptions {
|
|
31
|
+
getConfiguredShellPath?: () => string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export class ProcessManager {
|
|
30
35
|
private processes: Map<string, ManagedProcess> = new Map();
|
|
31
36
|
private counter = 0;
|
|
32
37
|
private logDir: string;
|
|
33
38
|
private events = new EventEmitter();
|
|
34
39
|
private watcher: ReturnType<typeof setInterval> | null = null;
|
|
40
|
+
private getConfiguredShellPath: () => string | undefined;
|
|
35
41
|
|
|
36
|
-
constructor() {
|
|
42
|
+
constructor(options?: ProcessManagerOptions) {
|
|
37
43
|
this.logDir = join(tmpdir(), `pi-processes-${Date.now()}`);
|
|
38
44
|
mkdirSync(this.logDir, { recursive: true });
|
|
45
|
+
this.getConfiguredShellPath =
|
|
46
|
+
options?.getConfiguredShellPath ?? (() => undefined);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
onEvent(listener: (event: ManagerEvent) => void): () => void {
|
|
@@ -129,12 +137,7 @@ export class ProcessManager {
|
|
|
129
137
|
appendFileSync(stderrFile, "");
|
|
130
138
|
appendFileSync(combinedFile, "");
|
|
131
139
|
|
|
132
|
-
const child =
|
|
133
|
-
cwd,
|
|
134
|
-
env: process.env,
|
|
135
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
-
detached: true,
|
|
137
|
-
});
|
|
140
|
+
const child = spawnCommand(command, cwd, this.getConfiguredShellPath());
|
|
138
141
|
|
|
139
142
|
child.unref();
|
|
140
143
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-processes",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@sinclair/typebox": "^0.34.41",
|
|
37
|
-
"@aliou/pi-utils-settings": "0.
|
|
37
|
+
"@aliou/pi-utils-settings": "0.4.0",
|
|
38
38
|
"@aliou/pi-utils-ui": "^0.1.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { resolveShellExecutable } from "./command-executor";
|
|
4
|
+
|
|
5
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import("node:fs")>();
|
|
7
|
+
return { ...actual, existsSync: vi.fn() };
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const existsSyncMock = vi.mocked(existsSync);
|
|
11
|
+
|
|
12
|
+
describe("resolveShellExecutable", () => {
|
|
13
|
+
it("prefers shell configured in settings when it is an existing absolute path", () => {
|
|
14
|
+
existsSyncMock.mockImplementation(
|
|
15
|
+
(path) => path === "/nix/store/abc-bash-5.3/bin/bash",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const resolved = resolveShellExecutable({
|
|
19
|
+
configuredShell: "/nix/store/abc-bash-5.3/bin/bash",
|
|
20
|
+
knownPaths: ["/bin/bash", "/usr/bin/bash"],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(resolved).toBe("/nix/store/abc-bash-5.3/bin/bash");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("falls back to first existing known shell path", () => {
|
|
27
|
+
existsSyncMock.mockImplementation((path) => path === "/usr/bin/bash");
|
|
28
|
+
|
|
29
|
+
const resolved = resolveShellExecutable({
|
|
30
|
+
configuredShell: undefined,
|
|
31
|
+
knownPaths: ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(resolved).toBe("/usr/bin/bash");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("throws when no configured/known shell path exists", () => {
|
|
38
|
+
existsSyncMock.mockReturnValue(false);
|
|
39
|
+
|
|
40
|
+
expect(() =>
|
|
41
|
+
resolveShellExecutable({
|
|
42
|
+
configuredShell: undefined,
|
|
43
|
+
knownPaths: ["/bin/bash", "/usr/bin/bash"],
|
|
44
|
+
}),
|
|
45
|
+
).toThrow(/shell/i);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
|
|
5
|
+
interface ResolveShellExecutableOptions {
|
|
6
|
+
configuredShell?: string;
|
|
7
|
+
knownPaths: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_KNOWN_SHELL_PATHS = [
|
|
11
|
+
"/run/current-system/sw/bin/bash",
|
|
12
|
+
"/bin/bash",
|
|
13
|
+
"/usr/bin/bash",
|
|
14
|
+
"/usr/local/bin/bash",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function isExistingAbsolutePath(shell: string | undefined): shell is string {
|
|
18
|
+
return typeof shell === "string" && isAbsolute(shell) && existsSync(shell);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveShellExecutable({
|
|
22
|
+
configuredShell,
|
|
23
|
+
knownPaths,
|
|
24
|
+
}: ResolveShellExecutableOptions): string {
|
|
25
|
+
if (isExistingAbsolutePath(configuredShell)) {
|
|
26
|
+
return configuredShell;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const path of knownPaths) {
|
|
30
|
+
if (isExistingAbsolutePath(path)) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Unable to resolve shell executable. Checked configured shell and known shell paths.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function spawnCommand(
|
|
41
|
+
command: string,
|
|
42
|
+
cwd: string,
|
|
43
|
+
configuredShell?: string,
|
|
44
|
+
): ChildProcess {
|
|
45
|
+
const shellExecutable = resolveShellExecutable({
|
|
46
|
+
configuredShell,
|
|
47
|
+
knownPaths: DEFAULT_KNOWN_SHELL_PATHS,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return spawn(shellExecutable, ["-lc", command], {
|
|
51
|
+
cwd,
|
|
52
|
+
env: process.env,
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
detached: true,
|
|
55
|
+
});
|
|
56
|
+
}
|