@aliou/pi-processes 0.5.0 → 0.6.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/README.md +83 -13
- package/package.json +2 -2
- package/src/commands/clear/command.ts +14 -0
- package/src/commands/clear/index.ts +1 -0
- package/src/commands/completions.ts +38 -0
- package/src/commands/dock/command.ts +29 -0
- package/src/commands/dock/index.ts +1 -0
- package/src/commands/index.ts +26 -327
- package/src/commands/kill/command.ts +69 -0
- package/src/commands/kill/index.ts +1 -0
- package/src/commands/logs/command.ts +47 -0
- package/src/commands/logs/index.ts +1 -0
- package/src/commands/pick-process.ts +34 -0
- package/src/commands/pin/command.ts +33 -0
- package/src/commands/pin/index.ts +1 -0
- package/src/commands/processes/command.ts +39 -0
- package/src/commands/processes/index.ts +1 -0
- package/src/commands/settings/apply-setting-change.ts +72 -0
- package/src/commands/settings/build-sections.ts +160 -0
- package/src/commands/settings/command.ts +20 -0
- package/src/commands/settings/index.ts +1 -0
- package/src/components/log-dock-component.ts +238 -0
- package/src/components/log-file-viewer.ts +317 -0
- package/src/components/log-overlay-component.ts +555 -0
- package/src/components/processes-component.ts +0 -1
- package/src/config.ts +28 -1
- package/src/constants/index.ts +1 -0
- package/src/constants/types.ts +7 -1
- package/src/hooks/index.ts +5 -3
- package/src/hooks/process-end.ts +3 -30
- package/src/hooks/widget/index.ts +2 -0
- package/src/hooks/widget/setup.ts +168 -0
- package/src/hooks/{widget.ts → widget/status-widget.ts} +7 -74
- package/src/hooks/widget/types.ts +21 -0
- package/src/index.ts +8 -3
- package/src/manager.ts +61 -9
- package/src/tools/actions/index.ts +5 -0
- package/src/tools/actions/write.ts +87 -0
- package/src/tools/index.ts +82 -14
- package/src/utils/command-executor.ts +1 -1
- package/src/utils/keybindings.ts +76 -0
- package/src/commands/settings-command.ts +0 -179
- package/src/components/log-stream-component.ts +0 -149
- package/src/test/test-exit-crash.sh +0 -19
- package/src/test/test-exit-failure.sh +0 -17
- package/src/test/test-exit-success.sh +0 -16
- package/src/test/test-output.sh +0 -28
package/src/hooks/process-end.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
2
|
import { MESSAGE_TYPE_PROCESS_UPDATE, type ProcessInfo } from "../constants";
|
|
6
3
|
import type { ProcessManager } from "../manager";
|
|
7
4
|
import { formatRuntime } from "../utils";
|
|
@@ -17,21 +14,6 @@ interface ProcessUpdateDetails {
|
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
|
|
20
|
-
let latestContext: ExtensionContext | null = null;
|
|
21
|
-
|
|
22
|
-
// Capture context from session events
|
|
23
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
24
|
-
latestContext = ctx;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
pi.on("turn_start", async (_event, ctx) => {
|
|
28
|
-
latestContext = ctx;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
pi.on("turn_end", async (_event, ctx) => {
|
|
32
|
-
latestContext = ctx;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
17
|
manager.onEvent((event) => {
|
|
36
18
|
if (event.type !== "process_ended") return;
|
|
37
19
|
|
|
@@ -47,27 +29,18 @@ export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
47
29
|
|
|
48
30
|
const runtime = formatRuntime(info.startTime, info.endTime);
|
|
49
31
|
|
|
50
|
-
// Build
|
|
32
|
+
// Build message
|
|
51
33
|
let message: string;
|
|
52
|
-
let level: "info" | "error" | "warning";
|
|
53
34
|
|
|
54
35
|
if (info.status === "killed") {
|
|
55
36
|
message = `Process '${info.name}' was terminated (${runtime})`;
|
|
56
|
-
level = "warning";
|
|
57
37
|
} else if (info.success) {
|
|
58
38
|
message = `Process '${info.name}' completed successfully (${runtime})`;
|
|
59
|
-
level = "info";
|
|
60
39
|
} else {
|
|
61
40
|
message = `Process '${info.name}' crashed with exit code ${info.exitCode ?? "?"} (${runtime})`;
|
|
62
|
-
level = "error";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Always notify user via UI
|
|
66
|
-
if (latestContext?.hasUI) {
|
|
67
|
-
latestContext.ui.notify(message, level);
|
|
68
41
|
}
|
|
69
42
|
|
|
70
|
-
//
|
|
43
|
+
// Send the message to the conversation - displayed via custom renderer in UI
|
|
71
44
|
// Only trigger an agent turn when the notification preferences say so.
|
|
72
45
|
const details: ProcessUpdateDetails = {
|
|
73
46
|
processId: info.id,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { LogDockComponent } from "../../components/log-dock-component";
|
|
6
|
+
import { configLoader, type ResolvedProcessesConfig } from "../../config";
|
|
7
|
+
import { LIVE_STATUSES } from "../../constants";
|
|
8
|
+
import type { ProcessManager } from "../../manager";
|
|
9
|
+
import { renderStatusWidget } from "./status-widget";
|
|
10
|
+
import {
|
|
11
|
+
type DockActions,
|
|
12
|
+
type DockState,
|
|
13
|
+
LOG_DOCK_WIDGET_ID,
|
|
14
|
+
STATUS_WIDGET_ID,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
export function setupProcessWidget(
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
manager: ProcessManager,
|
|
20
|
+
config: ResolvedProcessesConfig,
|
|
21
|
+
) {
|
|
22
|
+
let latestContext: ExtensionContext | null = null;
|
|
23
|
+
let logDockComponent: LogDockComponent | null = null;
|
|
24
|
+
let logDockComponentTui: { requestRender(): void } | null = null;
|
|
25
|
+
|
|
26
|
+
const dockState: DockState = {
|
|
27
|
+
visibility: "hidden",
|
|
28
|
+
followEnabled: config.follow.enabledByDefault,
|
|
29
|
+
focusedProcessId: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function updateWidget() {
|
|
33
|
+
if (!latestContext?.hasUI) return;
|
|
34
|
+
|
|
35
|
+
if (!configLoader.getConfig().widget.showStatusWidget) {
|
|
36
|
+
latestContext.ui.setWidget(STATUS_WIDGET_ID, undefined);
|
|
37
|
+
} else {
|
|
38
|
+
const processes = manager.list();
|
|
39
|
+
const maxWidth = process.stdout.columns || 120;
|
|
40
|
+
const lines = renderStatusWidget(
|
|
41
|
+
processes,
|
|
42
|
+
latestContext.ui.theme,
|
|
43
|
+
maxWidth,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (lines.length === 0) {
|
|
47
|
+
latestContext.ui.setWidget(STATUS_WIDGET_ID, undefined);
|
|
48
|
+
} else {
|
|
49
|
+
latestContext.ui.setWidget(STATUS_WIDGET_ID, lines, {
|
|
50
|
+
placement: "belowEditor",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (dockState.visibility === "hidden") {
|
|
56
|
+
latestContext.ui.setWidget(LOG_DOCK_WIDGET_ID, undefined);
|
|
57
|
+
if (logDockComponent) {
|
|
58
|
+
logDockComponent.dispose();
|
|
59
|
+
logDockComponent = null;
|
|
60
|
+
logDockComponentTui = null;
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mode = dockState.visibility as "collapsed" | "open";
|
|
66
|
+
const height = mode === "collapsed" ? 3 : config.widget.dockHeight;
|
|
67
|
+
|
|
68
|
+
if (logDockComponent && logDockComponentTui) {
|
|
69
|
+
logDockComponent.update({
|
|
70
|
+
mode,
|
|
71
|
+
focusedProcessId: dockState.focusedProcessId,
|
|
72
|
+
dockHeight: height,
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
const ctx = latestContext;
|
|
76
|
+
ctx.ui.setWidget(
|
|
77
|
+
LOG_DOCK_WIDGET_ID,
|
|
78
|
+
(tui: { requestRender(): void }, theme: typeof ctx.ui.theme) => {
|
|
79
|
+
logDockComponent = new LogDockComponent({
|
|
80
|
+
manager,
|
|
81
|
+
tui,
|
|
82
|
+
theme,
|
|
83
|
+
mode,
|
|
84
|
+
focusedProcessId: dockState.focusedProcessId,
|
|
85
|
+
dockHeight: height,
|
|
86
|
+
});
|
|
87
|
+
logDockComponentTui = tui;
|
|
88
|
+
return logDockComponent;
|
|
89
|
+
},
|
|
90
|
+
{ placement: "aboveEditor" },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const dockActions: DockActions = {
|
|
96
|
+
getFocusedProcessId: () => dockState.focusedProcessId,
|
|
97
|
+
isFollowEnabled: () => dockState.followEnabled,
|
|
98
|
+
setFocus(id) {
|
|
99
|
+
dockState.focusedProcessId = id;
|
|
100
|
+
if (id && dockState.visibility === "hidden")
|
|
101
|
+
dockState.visibility = "open";
|
|
102
|
+
updateWidget();
|
|
103
|
+
},
|
|
104
|
+
expand() {
|
|
105
|
+
dockState.visibility = "open";
|
|
106
|
+
updateWidget();
|
|
107
|
+
},
|
|
108
|
+
collapse() {
|
|
109
|
+
dockState.visibility = "collapsed";
|
|
110
|
+
updateWidget();
|
|
111
|
+
},
|
|
112
|
+
hide() {
|
|
113
|
+
dockState.visibility = "hidden";
|
|
114
|
+
updateWidget();
|
|
115
|
+
},
|
|
116
|
+
toggle() {
|
|
117
|
+
if (dockState.visibility === "hidden") dockState.visibility = "collapsed";
|
|
118
|
+
else if (dockState.visibility === "collapsed")
|
|
119
|
+
dockState.visibility = "open";
|
|
120
|
+
else dockState.visibility = "collapsed";
|
|
121
|
+
updateWidget();
|
|
122
|
+
},
|
|
123
|
+
toggleFollow() {
|
|
124
|
+
dockState.followEnabled = !dockState.followEnabled;
|
|
125
|
+
updateWidget();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
manager.onEvent((event) => {
|
|
130
|
+
if (event.type === "process_started") {
|
|
131
|
+
if (dockState.followEnabled && dockState.visibility === "hidden") {
|
|
132
|
+
dockState.visibility = "collapsed";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (event.type === "process_ended") {
|
|
137
|
+
if (dockState.focusedProcessId === event.info.id) {
|
|
138
|
+
dockState.focusedProcessId = null;
|
|
139
|
+
}
|
|
140
|
+
const running = manager.list().filter((p) => LIVE_STATUSES.has(p.status));
|
|
141
|
+
if (
|
|
142
|
+
running.length === 0 &&
|
|
143
|
+
config.follow.autoHideOnFinish &&
|
|
144
|
+
dockState.followEnabled
|
|
145
|
+
) {
|
|
146
|
+
dockState.visibility = "hidden";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
updateWidget();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
154
|
+
latestContext = ctx;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
158
|
+
if (logDockComponent) {
|
|
159
|
+
logDockComponent.dispose();
|
|
160
|
+
logDockComponent = null;
|
|
161
|
+
logDockComponentTui = null;
|
|
162
|
+
}
|
|
163
|
+
latestContext = ctx;
|
|
164
|
+
updateWidget();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return { update: updateWidget, dockActions };
|
|
168
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
2
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
6
|
-
import {
|
|
7
|
-
import type { ProcessInfo } from "../constants";
|
|
8
|
-
import type { ProcessManager } from "../manager";
|
|
9
|
-
|
|
10
|
-
const WIDGET_ID = "processes-status";
|
|
3
|
+
import type { ProcessInfo } from "../../constants";
|
|
11
4
|
|
|
12
5
|
function formatProcessStatus(
|
|
13
6
|
proc: ProcessInfo,
|
|
@@ -35,14 +28,12 @@ function formatProcessStatus(
|
|
|
35
28
|
}
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
function
|
|
31
|
+
export function renderStatusWidget(
|
|
39
32
|
processes: ProcessInfo[],
|
|
40
33
|
theme: ExtensionContext["ui"]["theme"],
|
|
41
34
|
maxWidth?: number,
|
|
42
35
|
): string[] {
|
|
43
|
-
if (processes.length === 0)
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
36
|
+
if (processes.length === 0) return [];
|
|
46
37
|
|
|
47
38
|
const aliveish = processes.filter(
|
|
48
39
|
(p) =>
|
|
@@ -76,13 +67,9 @@ function renderWidget(
|
|
|
76
67
|
const formatted = formatProcessStatus(proc, theme);
|
|
77
68
|
const formattedLen = visibleWidth(formatted);
|
|
78
69
|
const remaining = allProcs.length - includedCount - 1;
|
|
79
|
-
|
|
80
|
-
// Check if adding this part would exceed the width
|
|
81
70
|
const needed =
|
|
82
71
|
includedCount > 0 ? separatorLen + formattedLen : formattedLen;
|
|
83
72
|
|
|
84
|
-
// If there are more processes after this one, reserve space for the
|
|
85
|
-
// overflow suffix (separator + "+N more") so the final line fits.
|
|
86
73
|
let reservedForSuffix = 0;
|
|
87
74
|
if (remaining > 0) {
|
|
88
75
|
const suffixText = `+${remaining} more`;
|
|
@@ -93,11 +80,8 @@ function renderWidget(
|
|
|
93
80
|
currentLen + needed + reservedForSuffix > effectiveMax &&
|
|
94
81
|
includedCount > 0
|
|
95
82
|
) {
|
|
96
|
-
// Show how many are hidden
|
|
97
83
|
const hiddenCount = allProcs.length - includedCount;
|
|
98
|
-
if (hiddenCount > 0) {
|
|
99
|
-
parts.push(theme.fg("dim", `+${hiddenCount} more`));
|
|
100
|
-
}
|
|
84
|
+
if (hiddenCount > 0) parts.push(theme.fg("dim", `+${hiddenCount} more`));
|
|
101
85
|
break;
|
|
102
86
|
}
|
|
103
87
|
|
|
@@ -106,18 +90,11 @@ function renderWidget(
|
|
|
106
90
|
includedCount++;
|
|
107
91
|
}
|
|
108
92
|
|
|
109
|
-
// Edge case: the very first element was too wide and the loop's overflow
|
|
110
|
-
// check was skipped (it only fires when includedCount > 0). The suffix
|
|
111
|
-
// reservation already shrinks the budget, but a single process that fills
|
|
112
|
-
// the whole line still slips through. Show it truncated rather than nothing.
|
|
113
93
|
if (includedCount === 0 && allProcs.length > 0) {
|
|
114
|
-
|
|
115
|
-
parts.push(formatted);
|
|
94
|
+
parts.push(formatProcessStatus(allProcs[0], theme));
|
|
116
95
|
}
|
|
117
96
|
|
|
118
|
-
if (parts.length === 0)
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
97
|
+
if (parts.length === 0) return [];
|
|
121
98
|
|
|
122
99
|
const line = prefix + parts.join(separator);
|
|
123
100
|
return [
|
|
@@ -126,47 +103,3 @@ function renderWidget(
|
|
|
126
103
|
: line,
|
|
127
104
|
];
|
|
128
105
|
}
|
|
129
|
-
|
|
130
|
-
export function setupProcessWidget(pi: ExtensionAPI, manager: ProcessManager) {
|
|
131
|
-
let latestContext: ExtensionContext | null = null;
|
|
132
|
-
|
|
133
|
-
function updateWidget() {
|
|
134
|
-
if (!latestContext?.hasUI) return;
|
|
135
|
-
|
|
136
|
-
if (!configLoader.getConfig().widget.showStatusWidget) {
|
|
137
|
-
latestContext.ui.setWidget(WIDGET_ID, undefined);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const processes = manager.list();
|
|
142
|
-
const maxWidth = process.stdout.columns || 120;
|
|
143
|
-
const lines = renderWidget(processes, latestContext.ui.theme, maxWidth);
|
|
144
|
-
|
|
145
|
-
if (lines.length === 0) {
|
|
146
|
-
latestContext.ui.setWidget(WIDGET_ID, undefined);
|
|
147
|
-
} else {
|
|
148
|
-
latestContext.ui.setWidget(WIDGET_ID, lines, {
|
|
149
|
-
placement: "belowEditor",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
155
|
-
// Startup defer: capture context only. First render happens on process
|
|
156
|
-
// manager events or explicit settings updates.
|
|
157
|
-
latestContext = ctx;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
161
|
-
latestContext = ctx;
|
|
162
|
-
updateWidget();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
manager.onEvent(() => {
|
|
166
|
-
updateWidget();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
update: updateWidget,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type DockVisibility = "hidden" | "collapsed" | "open";
|
|
2
|
+
|
|
3
|
+
export interface DockState {
|
|
4
|
+
visibility: DockVisibility;
|
|
5
|
+
followEnabled: boolean;
|
|
6
|
+
focusedProcessId: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DockActions {
|
|
10
|
+
getFocusedProcessId(): string | null;
|
|
11
|
+
isFollowEnabled(): boolean;
|
|
12
|
+
setFocus(id: string | null): void;
|
|
13
|
+
expand(): void;
|
|
14
|
+
collapse(): void;
|
|
15
|
+
hide(): void;
|
|
16
|
+
toggle(): void;
|
|
17
|
+
toggleFollow(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const STATUS_WIDGET_ID = "processes-status";
|
|
21
|
+
export const LOG_DOCK_WIDGET_ID = "processes-dock";
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { setupProcessesCommands } from "./commands";
|
|
3
|
-
import { registerProcessesSettings } from "./commands/settings
|
|
3
|
+
import { registerProcessesSettings } from "./commands/settings";
|
|
4
4
|
import { configLoader } from "./config";
|
|
5
5
|
import { setupProcessesHooks } from "./hooks";
|
|
6
6
|
import { ProcessManager } from "./manager";
|
|
@@ -21,8 +21,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const config = configLoader.getConfig();
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
const { update: updateWidget, dockActions } = setupProcessesHooks(
|
|
26
|
+
pi,
|
|
27
|
+
manager,
|
|
28
|
+
config,
|
|
29
|
+
);
|
|
30
|
+
setupProcessesCommands(pi, manager, dockActions);
|
|
26
31
|
setupProcessesTools(pi, manager);
|
|
27
32
|
registerProcessesSettings(pi, () => {
|
|
28
33
|
updateWidget();
|
package/src/manager.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import type { Writable } from "node:stream";
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
type KillResult,
|
|
@@ -17,12 +18,15 @@ import {
|
|
|
17
18
|
type ProcessInfo,
|
|
18
19
|
type ProcessStatus,
|
|
19
20
|
type StartOptions,
|
|
21
|
+
type WriteResult,
|
|
20
22
|
} from "./constants";
|
|
21
23
|
import { isProcessGroupAlive, killProcessGroup } from "./utils";
|
|
22
24
|
import { spawnCommand } from "./utils/command-executor";
|
|
23
25
|
|
|
24
26
|
interface ManagedProcess extends ProcessInfo {
|
|
25
27
|
process: ChildProcess;
|
|
28
|
+
stdin: Writable | null;
|
|
29
|
+
stdinClosed: boolean;
|
|
26
30
|
lastSignalSent: NodeJS.Signals | null;
|
|
27
31
|
combinedFile: string;
|
|
28
32
|
}
|
|
@@ -57,15 +61,8 @@ export class ProcessManager {
|
|
|
57
61
|
|
|
58
62
|
private transition(managed: ManagedProcess, next: ProcessStatus): void {
|
|
59
63
|
if (managed.status === next) return;
|
|
60
|
-
const prev = managed.status;
|
|
61
64
|
managed.status = next;
|
|
62
65
|
|
|
63
|
-
this.emit({
|
|
64
|
-
type: "process_status_changed",
|
|
65
|
-
info: this.toProcessInfo(managed),
|
|
66
|
-
prev,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
66
|
if (next === "exited" || next === "killed") {
|
|
70
67
|
this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
|
|
71
68
|
}
|
|
@@ -159,6 +156,8 @@ export class ProcessManager {
|
|
|
159
156
|
alertOnFailure: options?.alertOnFailure ?? true,
|
|
160
157
|
alertOnKill: options?.alertOnKill ?? false,
|
|
161
158
|
process: child,
|
|
159
|
+
stdin: child.stdin,
|
|
160
|
+
stdinClosed: false,
|
|
162
161
|
lastSignalSent: null,
|
|
163
162
|
};
|
|
164
163
|
|
|
@@ -319,12 +318,15 @@ export class ProcessManager {
|
|
|
319
318
|
}
|
|
320
319
|
}
|
|
321
320
|
|
|
322
|
-
getLogFiles(
|
|
321
|
+
getLogFiles(
|
|
322
|
+
id: string,
|
|
323
|
+
): { stdoutFile: string; stderrFile: string; combinedFile: string } | null {
|
|
323
324
|
const managed = this.processes.get(id);
|
|
324
325
|
if (!managed) return null;
|
|
325
326
|
return {
|
|
326
327
|
stdoutFile: managed.stdoutFile,
|
|
327
328
|
stderrFile: managed.stderrFile,
|
|
329
|
+
combinedFile: managed.combinedFile,
|
|
328
330
|
};
|
|
329
331
|
}
|
|
330
332
|
|
|
@@ -407,6 +409,50 @@ export class ProcessManager {
|
|
|
407
409
|
return { ok: true, info: this.toProcessInfo(managed) };
|
|
408
410
|
}
|
|
409
411
|
|
|
412
|
+
writeToStdin(
|
|
413
|
+
id: string,
|
|
414
|
+
data: string,
|
|
415
|
+
opts?: { end?: boolean },
|
|
416
|
+
): WriteResult {
|
|
417
|
+
const managed = this.processes.get(id);
|
|
418
|
+
if (!managed) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
reason: "not_found",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!LIVE_STATUSES.has(managed.status)) {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
reason: "process_exited",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (managed.stdinClosed || !managed.stdin) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: "stdin_closed",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
managed.stdin.write(data);
|
|
441
|
+
|
|
442
|
+
if (opts?.end) {
|
|
443
|
+
managed.stdin.end();
|
|
444
|
+
managed.stdinClosed = true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { ok: true };
|
|
448
|
+
} catch {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
reason: "write_error",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
410
456
|
clearFinished(): number {
|
|
411
457
|
let cleared = 0;
|
|
412
458
|
for (const [id, managed] of this.processes) {
|
|
@@ -519,4 +565,10 @@ export class ProcessManager {
|
|
|
519
565
|
}
|
|
520
566
|
}
|
|
521
567
|
|
|
522
|
-
export type {
|
|
568
|
+
export type {
|
|
569
|
+
ProcessInfo,
|
|
570
|
+
ProcessStatus,
|
|
571
|
+
ManagerEvent,
|
|
572
|
+
KillResult,
|
|
573
|
+
WriteResult,
|
|
574
|
+
};
|
|
@@ -7,12 +7,15 @@ import { executeList } from "./list";
|
|
|
7
7
|
import { executeLogs } from "./logs";
|
|
8
8
|
import { executeOutput } from "./output";
|
|
9
9
|
import { executeStart } from "./start";
|
|
10
|
+
import { executeWrite } from "./write";
|
|
10
11
|
|
|
11
12
|
interface ActionParams {
|
|
12
13
|
action: string;
|
|
13
14
|
command?: string;
|
|
14
15
|
name?: string;
|
|
15
16
|
id?: string;
|
|
17
|
+
input?: string;
|
|
18
|
+
end?: boolean;
|
|
16
19
|
alertOnSuccess?: boolean;
|
|
17
20
|
alertOnFailure?: boolean;
|
|
18
21
|
alertOnKill?: boolean;
|
|
@@ -36,6 +39,8 @@ export async function executeAction(
|
|
|
36
39
|
return executeKill(params, manager);
|
|
37
40
|
case "clear":
|
|
38
41
|
return executeClear(manager);
|
|
42
|
+
case "write":
|
|
43
|
+
return executeWrite(params, manager);
|
|
39
44
|
default:
|
|
40
45
|
return {
|
|
41
46
|
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ExecuteResult } from "../../constants";
|
|
2
|
+
import type { ProcessManager } from "../../manager";
|
|
3
|
+
|
|
4
|
+
interface WriteParams {
|
|
5
|
+
id?: string;
|
|
6
|
+
input?: string;
|
|
7
|
+
end?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function executeWrite(
|
|
11
|
+
params: WriteParams,
|
|
12
|
+
manager: ProcessManager,
|
|
13
|
+
): ExecuteResult {
|
|
14
|
+
const { id, input, end } = params;
|
|
15
|
+
|
|
16
|
+
if (!id) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: "Missing required parameter: id" }],
|
|
19
|
+
details: {
|
|
20
|
+
action: "write",
|
|
21
|
+
success: false,
|
|
22
|
+
message: "Missing required parameter: id",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (input === undefined) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: "Missing required parameter: input" }],
|
|
30
|
+
details: {
|
|
31
|
+
action: "write",
|
|
32
|
+
success: false,
|
|
33
|
+
message: "Missing required parameter: input",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const process = manager.find(id);
|
|
39
|
+
if (!process) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: `Process not found: ${id}` }],
|
|
42
|
+
details: {
|
|
43
|
+
action: "write",
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Process not found: ${id}`,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = manager.writeToStdin(process.id, input, { end });
|
|
51
|
+
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
const messages: Record<string, string> = {
|
|
54
|
+
not_found: `Process not found: ${process.id}`,
|
|
55
|
+
process_exited: `Process has already exited: ${process.id}`,
|
|
56
|
+
stdin_closed: `Stdin already closed for process: ${process.id}`,
|
|
57
|
+
write_error: `Failed to write to stdin for process: ${process.id}`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const message =
|
|
61
|
+
messages[result.reason] || `Unknown error: ${result.reason}`;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: message }],
|
|
65
|
+
details: {
|
|
66
|
+
action: "write",
|
|
67
|
+
success: false,
|
|
68
|
+
message,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const suffix = end ? " (stdin closed)" : "";
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Wrote ${input.length} bytes to ${process.id}${suffix}`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
details: {
|
|
82
|
+
action: "write",
|
|
83
|
+
success: true,
|
|
84
|
+
message: `Wrote ${input.length} bytes to process stdin${suffix}`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|