gitspace 0.2.0-rc.19 → 0.2.0-rc.20
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/.gitspace/events.json +11 -0
- package/.gitspace/processes.json +23 -0
- package/package.json +5 -5
- package/scripts/sample-events.ts +263 -0
- package/src/app/session/__tests__/useAttachController.test.ts +27 -0
- package/src/app/session/useAttachController.ts +9 -1
- package/src/app/session/useProcessActions.ts +201 -0
- package/src/app.tui.tsx +311 -4
- package/src/app.web.tsx +277 -3
- package/src/commands/__tests__/events.test.ts +201 -0
- package/src/commands/__tests__/process.test.ts +251 -0
- package/src/commands/__tests__/serve-process-hosting.test.ts +63 -0
- package/src/commands/events.ts +157 -0
- package/src/commands/host.ts +106 -8
- package/src/commands/process.ts +104 -0
- package/src/commands/serve.ts +395 -5
- package/src/components/Events.tsx +137 -0
- package/src/components/Events.tui.tsx +129 -0
- package/src/components/Events.web.tsx +386 -0
- package/src/components/RemoteMachineScreen.tui.tsx +195 -4
- package/src/components/SessionTerminal.tui.tsx +21 -6
- package/src/components/SessionTerminal.web.tsx +16 -2
- package/src/components/SpacesBrowser.tsx +225 -15
- package/src/components/SpacesBrowser.tui.tsx +97 -2
- package/src/components/SpacesBrowser.web.tsx +106 -12
- package/src/components/__tests__/SpacesBrowser.test.ts +541 -0
- package/src/components/__tests__/SpacesBrowser.tui.test.tsx +249 -0
- package/src/components/index.ts +2 -0
- package/src/core/config.ts +14 -1
- package/src/hooks/useLocalSession.tui.ts +34 -0
- package/src/index.ts +128 -0
- package/src/lib/events/__tests__/collector-filter.test.ts +105 -0
- package/src/lib/events/__tests__/store-query.test.ts +103 -0
- package/src/lib/events/collector.ts +494 -0
- package/src/lib/events/filters.ts +26 -0
- package/src/lib/events/index.ts +11 -0
- package/src/lib/events/indexer.ts +14 -0
- package/src/lib/events/paths.ts +69 -0
- package/src/lib/events/reader.ts +212 -0
- package/src/lib/events/store.ts +141 -0
- package/src/lib/processes/__tests__/config.test.ts +83 -0
- package/src/lib/processes/__tests__/names.test.ts +125 -0
- package/src/lib/processes/__tests__/schema.test.ts +208 -0
- package/src/lib/processes/__tests__/watchdog.test.ts +210 -0
- package/src/lib/processes/autostart.ts +16 -0
- package/src/lib/processes/config.ts +187 -0
- package/src/lib/processes/control.ts +53 -0
- package/src/lib/processes/editor.ts +32 -0
- package/src/lib/processes/events-config.ts +37 -0
- package/src/lib/processes/index.ts +14 -0
- package/src/lib/processes/instances.ts +20 -0
- package/src/lib/processes/manager.ts +131 -0
- package/src/lib/processes/names.ts +71 -0
- package/src/lib/processes/registry.ts +26 -0
- package/src/lib/processes/runner.ts +211 -0
- package/src/lib/processes/scheduler.ts +17 -0
- package/src/lib/processes/schema.ts +74 -0
- package/src/lib/processes/session-list.ts +15 -0
- package/src/lib/processes/state.ts +82 -0
- package/src/lib/processes/watchdog.test.ts +79 -0
- package/src/lib/processes/watchdog.ts +106 -0
- package/src/lib/remote-session/__tests__/protocol.test.ts +291 -0
- package/src/lib/remote-session/index.ts +1 -1
- package/src/lib/remote-session/protocol.ts +75 -4
- package/src/lib/remote-session/session-handler.ts +409 -72
- package/src/lib/tmux-lite/cli.ts +15 -2
- package/src/lib/tmux-lite/process-run.integration.test.ts +266 -0
- package/src/lib/tmux-lite/protocol.ts +11 -2
- package/src/lib/tmux-lite/server.ts +27 -5
- package/src/serve/client-session-manager.ts +16 -3
- package/src/serve/types.ts +5 -0
- package/src/session/__tests__/local-session-backend.test.ts +34 -1
- package/src/session/__tests__/remote-session-backend.test.ts +104 -0
- package/src/session/backend.ts +12 -0
- package/src/session/backends/local-session-backend.ts +262 -67
- package/src/session/backends/remote-session-backend.ts +154 -1
- package/src/session/events.ts +7 -3
- package/src/session/reducer.ts +34 -0
- package/src/session/types.ts +16 -0
- package/src/session/useBundleRefreshAttachFlow.ts +8 -0
- package/src/session/useRemoteSessionClient.ts +44 -0
- package/src/session/useSessionEngine.ts +64 -0
- package/src/tui/local-terminal-sync.ts +1 -1
- package/src/types/config.ts +88 -0
- package/src/types/events.ts +91 -0
- package/src/types/processes.ts +45 -0
- package/src/utils/hostnames.ts +43 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"processes": [
|
|
3
|
+
{
|
|
4
|
+
"name": "sample-events",
|
|
5
|
+
"command": "bun",
|
|
6
|
+
"args": ["scripts/sample-events.ts"],
|
|
7
|
+
"autostart": false,
|
|
8
|
+
"events": {
|
|
9
|
+
"keepRawOutput": false,
|
|
10
|
+
"correlationField": "requestId",
|
|
11
|
+
"aggregateMode": "stream",
|
|
12
|
+
"updateIntervalMs": 200,
|
|
13
|
+
"maxTimeline": 50
|
|
14
|
+
},
|
|
15
|
+
"restart": {
|
|
16
|
+
"policy": "always",
|
|
17
|
+
"maxAttempts": 10,
|
|
18
|
+
"backoffMs": 2000,
|
|
19
|
+
"maxBackoffMs": 15000
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitspace",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.20",
|
|
4
4
|
"description": "CLI for managing GitHub workspaces with git worktrees and secure remote terminal access",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gssh": "./bin/gssh"
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"relay": "bun src/relay/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@gitspace/darwin-arm64": "0.2.0-rc.
|
|
21
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
22
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
23
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
20
|
+
"@gitspace/darwin-arm64": "0.2.0-rc.20",
|
|
21
|
+
"@gitspace/darwin-x64": "0.2.0-rc.20",
|
|
22
|
+
"@gitspace/linux-x64": "0.2.0-rc.20",
|
|
23
|
+
"@gitspace/linux-arm64": "0.2.0-rc.20"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"cli",
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
type EventLevel = "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
type SampleEvent = {
|
|
4
|
+
event: string;
|
|
5
|
+
eventId: string;
|
|
6
|
+
level: EventLevel;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
message: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
traceId?: string;
|
|
11
|
+
spanId?: string;
|
|
12
|
+
parentSpanId?: string;
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type TaskDefinition = {
|
|
17
|
+
name: string;
|
|
18
|
+
startMessage: string;
|
|
19
|
+
finishMessage: string;
|
|
20
|
+
errorMessage: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ScheduledEvent = {
|
|
24
|
+
offsetMs: number;
|
|
25
|
+
event: string;
|
|
26
|
+
message: string;
|
|
27
|
+
level?: EventLevel;
|
|
28
|
+
spanId: string;
|
|
29
|
+
parentSpanId?: string;
|
|
30
|
+
durationMs?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const TASKS: TaskDefinition[] = [
|
|
34
|
+
{
|
|
35
|
+
name: "auth.check",
|
|
36
|
+
startMessage: "Validating token",
|
|
37
|
+
finishMessage: "Token validated",
|
|
38
|
+
errorMessage: "Token validation failed",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "db.query",
|
|
42
|
+
startMessage: "Querying primary database",
|
|
43
|
+
finishMessage: "Query complete",
|
|
44
|
+
errorMessage: "Database timeout",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "cache.lookup",
|
|
48
|
+
startMessage: "Cache lookup",
|
|
49
|
+
finishMessage: "Cache response",
|
|
50
|
+
errorMessage: "Cache lookup failed",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "worker.dispatch",
|
|
54
|
+
startMessage: "Dispatching queue worker",
|
|
55
|
+
finishMessage: "Worker acknowledged",
|
|
56
|
+
errorMessage: "Worker dispatch stalled",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "billing.capture",
|
|
60
|
+
startMessage: "Capturing payment",
|
|
61
|
+
finishMessage: "Payment captured",
|
|
62
|
+
errorMessage: "Payment capture failed",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "inventory.reserve",
|
|
66
|
+
startMessage: "Reserving inventory",
|
|
67
|
+
finishMessage: "Inventory reserved",
|
|
68
|
+
errorMessage: "Inventory reservation failed",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "feature.flag",
|
|
72
|
+
startMessage: "Evaluating feature flags",
|
|
73
|
+
finishMessage: "Flags applied",
|
|
74
|
+
errorMessage: "Flag evaluation failed",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "webhook.emit",
|
|
78
|
+
startMessage: "Emitting webhook",
|
|
79
|
+
finishMessage: "Webhook delivered",
|
|
80
|
+
errorMessage: "Webhook delivery failed",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const REQUEST_DURATION_RANGE_MS = [6000, 18000] as const;
|
|
85
|
+
const REQUEST_INTERVAL_RANGE_MS = [2000, 4000] as const;
|
|
86
|
+
const TASK_COUNT_RANGE = [4, 8] as const;
|
|
87
|
+
const FAILURE_RATE = 0.22;
|
|
88
|
+
|
|
89
|
+
function randomId(prefix: string): string {
|
|
90
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function randomBetween(min: number, max: number): number {
|
|
94
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function shuffle<T>(items: T[]): T[] {
|
|
98
|
+
const copy = [...items];
|
|
99
|
+
for (let i = copy.length - 1; i > 0; i -= 1) {
|
|
100
|
+
const j = randomBetween(0, i);
|
|
101
|
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
102
|
+
}
|
|
103
|
+
return copy;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function pickOne<T>(items: T[]): T {
|
|
107
|
+
return items[randomBetween(0, items.length - 1)];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function emitEvent(payload: SampleEvent): void {
|
|
111
|
+
console.log(`@event ${JSON.stringify(payload)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildEvent(params: {
|
|
115
|
+
requestId: string;
|
|
116
|
+
traceId: string;
|
|
117
|
+
spanId: string;
|
|
118
|
+
parentSpanId?: string;
|
|
119
|
+
event: string;
|
|
120
|
+
message: string;
|
|
121
|
+
level?: EventLevel;
|
|
122
|
+
durationMs?: number;
|
|
123
|
+
}): SampleEvent {
|
|
124
|
+
const now = new Date();
|
|
125
|
+
return {
|
|
126
|
+
event: params.event,
|
|
127
|
+
eventId: randomId("evt"),
|
|
128
|
+
level: params.level ?? "info",
|
|
129
|
+
timestamp: now.toISOString(),
|
|
130
|
+
message: params.message,
|
|
131
|
+
requestId: params.requestId,
|
|
132
|
+
traceId: params.traceId,
|
|
133
|
+
spanId: params.spanId,
|
|
134
|
+
parentSpanId: params.parentSpanId,
|
|
135
|
+
durationMs: params.durationMs,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function scheduleEvents(params: {
|
|
140
|
+
requestId: string;
|
|
141
|
+
traceId: string;
|
|
142
|
+
events: ScheduledEvent[];
|
|
143
|
+
}): void {
|
|
144
|
+
const { requestId, traceId, events } = params;
|
|
145
|
+
const sorted = [...events].sort((a, b) => a.offsetMs - b.offsetMs);
|
|
146
|
+
for (const event of sorted) {
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
emitEvent(buildEvent({
|
|
149
|
+
requestId,
|
|
150
|
+
traceId,
|
|
151
|
+
spanId: event.spanId,
|
|
152
|
+
parentSpanId: event.parentSpanId,
|
|
153
|
+
event: event.event,
|
|
154
|
+
message: event.message,
|
|
155
|
+
level: event.level,
|
|
156
|
+
durationMs: event.durationMs,
|
|
157
|
+
}));
|
|
158
|
+
}, event.offsetMs);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function emitRequestSequence(): void {
|
|
163
|
+
const requestId = randomId("req");
|
|
164
|
+
const traceId = randomId("trace");
|
|
165
|
+
const rootSpanId = randomId("span");
|
|
166
|
+
const totalDuration = randomBetween(...REQUEST_DURATION_RANGE_MS);
|
|
167
|
+
const willFail = Math.random() < FAILURE_RATE;
|
|
168
|
+
const failureOffset = willFail
|
|
169
|
+
? randomBetween(Math.floor(totalDuration * 0.4), Math.floor(totalDuration * 0.85))
|
|
170
|
+
: totalDuration;
|
|
171
|
+
const finalOffset = willFail
|
|
172
|
+
? failureOffset + randomBetween(120, 480)
|
|
173
|
+
: totalDuration + randomBetween(200, 600);
|
|
174
|
+
|
|
175
|
+
const scheduled: ScheduledEvent[] = [
|
|
176
|
+
{
|
|
177
|
+
offsetMs: 0,
|
|
178
|
+
event: "request.start",
|
|
179
|
+
message: "Request started",
|
|
180
|
+
spanId: rootSpanId,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const taskCount = randomBetween(...TASK_COUNT_RANGE);
|
|
185
|
+
const selectedTasks = shuffle(TASKS).slice(0, taskCount);
|
|
186
|
+
const taskWindowEnd = Math.max(600, failureOffset - 600);
|
|
187
|
+
|
|
188
|
+
for (const task of selectedTasks) {
|
|
189
|
+
const spanId = randomId("span");
|
|
190
|
+
const startOffset = randomBetween(200, taskWindowEnd);
|
|
191
|
+
if (willFail && startOffset > failureOffset - 200) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const duration = randomBetween(400, 3200);
|
|
196
|
+
const finishOffset = startOffset + duration;
|
|
197
|
+
|
|
198
|
+
scheduled.push({
|
|
199
|
+
offsetMs: startOffset,
|
|
200
|
+
event: `${task.name}.start`,
|
|
201
|
+
message: task.startMessage,
|
|
202
|
+
spanId,
|
|
203
|
+
parentSpanId: rootSpanId,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!willFail || finishOffset < failureOffset - 200) {
|
|
207
|
+
scheduled.push({
|
|
208
|
+
offsetMs: finishOffset,
|
|
209
|
+
event: `${task.name}.finish`,
|
|
210
|
+
message: task.finishMessage,
|
|
211
|
+
spanId,
|
|
212
|
+
parentSpanId: rootSpanId,
|
|
213
|
+
durationMs: duration,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (willFail) {
|
|
219
|
+
const failureTask = pickOne(selectedTasks);
|
|
220
|
+
const failureSpanId = randomId("span");
|
|
221
|
+
const failureDetailOffset = Math.max(200, failureOffset - randomBetween(150, 400));
|
|
222
|
+
|
|
223
|
+
scheduled.push({
|
|
224
|
+
offsetMs: failureDetailOffset,
|
|
225
|
+
event: `${failureTask.name}.error`,
|
|
226
|
+
message: failureTask.errorMessage,
|
|
227
|
+
spanId: failureSpanId,
|
|
228
|
+
parentSpanId: rootSpanId,
|
|
229
|
+
level: "error",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
scheduled.push({
|
|
234
|
+
offsetMs: finalOffset,
|
|
235
|
+
event: willFail ? "request.failed" : "request.complete",
|
|
236
|
+
message: willFail ? "Request failed" : "Request completed",
|
|
237
|
+
spanId: rootSpanId,
|
|
238
|
+
level: willFail ? "error" : "info",
|
|
239
|
+
durationMs: finalOffset,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
scheduleEvents({ requestId, traceId, events: scheduled });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
emitEvent(buildEvent({
|
|
246
|
+
requestId: "bootstrap",
|
|
247
|
+
traceId: randomId("trace"),
|
|
248
|
+
spanId: randomId("span"),
|
|
249
|
+
event: "sample.start",
|
|
250
|
+
message: "Sample event emitter started",
|
|
251
|
+
level: "info",
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
function scheduleNextSequence(): void {
|
|
255
|
+
const delay = randomBetween(...REQUEST_INTERVAL_RANGE_MS);
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
emitRequestSequence();
|
|
258
|
+
scheduleNextSequence();
|
|
259
|
+
}, delay);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
scheduleNextSequence();
|
|
263
|
+
emitRequestSequence();
|
|
@@ -124,6 +124,33 @@ describe('useAttachController', () => {
|
|
|
124
124
|
)
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
+
it('passes through viewOnly on existing session attaches', async () => {
|
|
128
|
+
const attachSessionWithBundleRefresh = mock(async () => true)
|
|
129
|
+
|
|
130
|
+
const { result } = renderHook(() =>
|
|
131
|
+
useAttachController({
|
|
132
|
+
flow: {
|
|
133
|
+
showInput: () => {},
|
|
134
|
+
showMessage: () => {},
|
|
135
|
+
close: () => {},
|
|
136
|
+
},
|
|
137
|
+
attachSessionWithBundleRefresh,
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
await result.current.attachFromSelection({ sessionId: 'session-view', viewOnly: true })
|
|
142
|
+
|
|
143
|
+
expect(attachSessionWithBundleRefresh).toHaveBeenCalledWith(
|
|
144
|
+
{
|
|
145
|
+
sessionId: 'session-view',
|
|
146
|
+
viewOnly: true,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
projectName: null,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
127
154
|
it('runs lifecycle callbacks for cancelled and failed attach attempts', async () => {
|
|
128
155
|
const onAttachCancelled = mock(() => {})
|
|
129
156
|
const onAttachError = mock(() => {})
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
export interface AttachSelectionParams {
|
|
9
9
|
sessionId?: string
|
|
10
10
|
workspaceId?: string
|
|
11
|
+
viewOnly?: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export type AttachTarget = 'session' | 'workspace'
|
|
@@ -182,7 +183,14 @@ export function useAttachController(options: UseAttachControllerOptions): UseAtt
|
|
|
182
183
|
return
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
const attachParams: BundleRefreshAttachParams = {
|
|
187
|
+
sessionId: selection.sessionId,
|
|
188
|
+
}
|
|
189
|
+
if (selection.viewOnly !== undefined) {
|
|
190
|
+
attachParams.viewOnly = selection.viewOnly
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await attach(attachParams)
|
|
186
194
|
return
|
|
187
195
|
}
|
|
188
196
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ProcessSessionInfo {
|
|
4
|
+
id: string
|
|
5
|
+
workspaceId: string
|
|
6
|
+
processName?: string
|
|
7
|
+
processInstance?: number
|
|
8
|
+
createdAt: number
|
|
9
|
+
exitCode?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ProcessActionParams {
|
|
13
|
+
workspaceId: string
|
|
14
|
+
processName: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProcessStartAttachParams extends ProcessActionParams {
|
|
18
|
+
instance?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PendingProcessAttachTarget {
|
|
22
|
+
workspaceId: string
|
|
23
|
+
processName: string
|
|
24
|
+
instance: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseProcessActionsOptions {
|
|
28
|
+
sessions: ProcessSessionInfo[]
|
|
29
|
+
startProcess: (workspaceId: string, processName: string, instance?: number) => Promise<void>
|
|
30
|
+
stopProcess: (workspaceId: string, processName: string) => Promise<void>
|
|
31
|
+
attachSession: (params: { sessionId?: string; workspaceId?: string; viewOnly?: boolean }) => Promise<void>
|
|
32
|
+
onStartProcessError?: (error: unknown) => void
|
|
33
|
+
onStopProcessError?: (error: unknown) => void
|
|
34
|
+
onStartProcessAttachError?: (error: unknown) => void
|
|
35
|
+
onAttachError?: (error: unknown) => void
|
|
36
|
+
onAttachTimeout?: (target: PendingProcessAttachTarget) => void
|
|
37
|
+
onStartProcessFinally?: () => void | Promise<void>
|
|
38
|
+
onStopProcessFinally?: () => void | Promise<void>
|
|
39
|
+
onStartProcessAttachFinally?: () => void | Promise<void>
|
|
40
|
+
pendingAttachCancelSignal?: unknown
|
|
41
|
+
attachTimeoutMs?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface UseProcessActionsResult {
|
|
45
|
+
handleStartProcess: (params: ProcessActionParams) => void
|
|
46
|
+
handleStopProcess: (params: ProcessActionParams) => void
|
|
47
|
+
handleStartProcessAttach: (params: ProcessStartAttachParams) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matchesTarget(
|
|
51
|
+
current: PendingProcessAttachTarget | null,
|
|
52
|
+
target: PendingProcessAttachTarget
|
|
53
|
+
): boolean {
|
|
54
|
+
return Boolean(
|
|
55
|
+
current &&
|
|
56
|
+
current.workspaceId === target.workspaceId &&
|
|
57
|
+
current.processName === target.processName &&
|
|
58
|
+
current.instance === target.instance
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useProcessActions(options: UseProcessActionsOptions): UseProcessActionsResult {
|
|
63
|
+
const {
|
|
64
|
+
sessions,
|
|
65
|
+
startProcess,
|
|
66
|
+
stopProcess,
|
|
67
|
+
attachSession,
|
|
68
|
+
onStartProcessError,
|
|
69
|
+
onStopProcessError,
|
|
70
|
+
onStartProcessAttachError,
|
|
71
|
+
onAttachError,
|
|
72
|
+
onAttachTimeout,
|
|
73
|
+
onStartProcessFinally,
|
|
74
|
+
onStopProcessFinally,
|
|
75
|
+
onStartProcessAttachFinally,
|
|
76
|
+
pendingAttachCancelSignal,
|
|
77
|
+
attachTimeoutMs = 8000,
|
|
78
|
+
} = options
|
|
79
|
+
|
|
80
|
+
const [pendingProcessAttach, setPendingProcessAttach] =
|
|
81
|
+
useState<PendingProcessAttachTarget | null>(null)
|
|
82
|
+
const pendingProcessAttachRef = useRef<PendingProcessAttachTarget | null>(null)
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
pendingProcessAttachRef.current = pendingProcessAttach
|
|
86
|
+
}, [pendingProcessAttach])
|
|
87
|
+
|
|
88
|
+
const runFinally = useCallback((fn?: () => void | Promise<void>) => {
|
|
89
|
+
if (!fn) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
void Promise.resolve(fn()).catch(() => {})
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
const handleStartProcess = useCallback((params: ProcessActionParams) => {
|
|
96
|
+
void Promise.resolve(startProcess(params.workspaceId, params.processName))
|
|
97
|
+
.catch((error) => {
|
|
98
|
+
onStartProcessError?.(error)
|
|
99
|
+
})
|
|
100
|
+
.finally(() => {
|
|
101
|
+
runFinally(onStartProcessFinally)
|
|
102
|
+
})
|
|
103
|
+
}, [onStartProcessError, onStartProcessFinally, runFinally, startProcess])
|
|
104
|
+
|
|
105
|
+
const handleStopProcess = useCallback((params: ProcessActionParams) => {
|
|
106
|
+
void Promise.resolve(stopProcess(params.workspaceId, params.processName))
|
|
107
|
+
.catch((error) => {
|
|
108
|
+
onStopProcessError?.(error)
|
|
109
|
+
})
|
|
110
|
+
.finally(() => {
|
|
111
|
+
runFinally(onStopProcessFinally)
|
|
112
|
+
})
|
|
113
|
+
}, [onStopProcessError, onStopProcessFinally, runFinally, stopProcess])
|
|
114
|
+
|
|
115
|
+
const handleStartProcessAttach = useCallback((params: ProcessStartAttachParams) => {
|
|
116
|
+
const target: PendingProcessAttachTarget = {
|
|
117
|
+
workspaceId: params.workspaceId,
|
|
118
|
+
processName: params.processName,
|
|
119
|
+
instance: params.instance ?? 1,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setPendingProcessAttach(target)
|
|
123
|
+
void Promise.resolve(startProcess(target.workspaceId, target.processName, target.instance))
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
setPendingProcessAttach((current) =>
|
|
126
|
+
matchesTarget(current, target) ? null : current
|
|
127
|
+
)
|
|
128
|
+
const errorHandler = onStartProcessAttachError ?? onStartProcessError
|
|
129
|
+
errorHandler?.(error)
|
|
130
|
+
})
|
|
131
|
+
.finally(() => {
|
|
132
|
+
runFinally(onStartProcessAttachFinally)
|
|
133
|
+
})
|
|
134
|
+
}, [
|
|
135
|
+
onStartProcessAttachError,
|
|
136
|
+
onStartProcessAttachFinally,
|
|
137
|
+
onStartProcessError,
|
|
138
|
+
runFinally,
|
|
139
|
+
startProcess,
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!pendingProcessAttach) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const session = sessions
|
|
148
|
+
.filter((item) =>
|
|
149
|
+
item.workspaceId === pendingProcessAttach.workspaceId &&
|
|
150
|
+
item.processName === pendingProcessAttach.processName &&
|
|
151
|
+
(item.processInstance ?? 1) === pendingProcessAttach.instance &&
|
|
152
|
+
item.exitCode === undefined
|
|
153
|
+
)
|
|
154
|
+
.sort((a, b) => b.createdAt - a.createdAt)[0]
|
|
155
|
+
|
|
156
|
+
if (!session) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const target = pendingProcessAttach
|
|
161
|
+
setPendingProcessAttach((current) =>
|
|
162
|
+
matchesTarget(current, target) ? null : current
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
void attachSession({ sessionId: session.id, viewOnly: true }).catch((error) => {
|
|
166
|
+
onAttachError?.(error)
|
|
167
|
+
})
|
|
168
|
+
}, [attachSession, onAttachError, pendingProcessAttach, sessions])
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!pendingProcessAttach) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const target = pendingProcessAttach
|
|
176
|
+
const timeout = setTimeout(() => {
|
|
177
|
+
const current = pendingProcessAttachRef.current
|
|
178
|
+
if (!matchesTarget(current, target)) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setPendingProcessAttach(null)
|
|
183
|
+
onAttachTimeout?.(target)
|
|
184
|
+
}, attachTimeoutMs)
|
|
185
|
+
|
|
186
|
+
return () => clearTimeout(timeout)
|
|
187
|
+
}, [attachTimeoutMs, onAttachTimeout, pendingProcessAttach])
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!pendingProcessAttach || !pendingAttachCancelSignal) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
setPendingProcessAttach(null)
|
|
194
|
+
}, [pendingAttachCancelSignal, pendingProcessAttach])
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
handleStartProcess,
|
|
198
|
+
handleStopProcess,
|
|
199
|
+
handleStartProcessAttach,
|
|
200
|
+
}
|
|
201
|
+
}
|