@juanibiapina/pi-gob 0.2.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/daemon-client.d.ts +44 -0
- package/dist/daemon-client.d.ts.map +1 -0
- package/dist/daemon-client.js +218 -0
- package/dist/daemon-client.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/widget.d.ts +18 -0
- package/dist/widget.d.ts.map +1 -0
- package/dist/widget.js +75 -0
- package/dist/widget.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juan Ibiapina
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @juanibiapina/pi-gob
|
|
2
|
+
|
|
3
|
+
A [pi](https://github.com/badlogic/pi-mono) extension for managing [gob](https://github.com/juanibiapina/gob) background jobs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Live job widget** — Running jobs displayed below the editor, updated in real time via daemon connection
|
|
8
|
+
- **Progress bars** — Jobs with historical run data show a progress bar based on average duration
|
|
9
|
+
- **`/gob` command** — Interactive list of all jobs with actions (logs, stop, start, restart, remove)
|
|
10
|
+
- **Daemon protocol** — Connects directly to the gob daemon Unix socket for instant updates, with CLI fallback
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pi install npm:@juanibiapina/pi-gob
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Widget
|
|
21
|
+
|
|
22
|
+
The widget appears automatically below the editor when there are running gob jobs in the current working directory. It disappears when all jobs stop.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
● npm run dev │ ● make build ████░░░ 57%
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Jobs without historical stats show just the command name
|
|
29
|
+
- Jobs with previous runs show a progress bar based on average duration
|
|
30
|
+
- The widget updates in real time via the gob daemon event stream
|
|
31
|
+
|
|
32
|
+
### `/gob` Command
|
|
33
|
+
|
|
34
|
+
Use `/gob` to open an interactive job list. Navigate with arrow keys, press Enter to see available actions.
|
|
35
|
+
|
|
36
|
+
| Action | Available When | Description |
|
|
37
|
+
|--------|---------------|-------------|
|
|
38
|
+
| logs | Always | View last 50 lines of output |
|
|
39
|
+
| stop | Running | Stop the job |
|
|
40
|
+
| start | Stopped | Start the job again |
|
|
41
|
+
| restart | Always | Stop and start the job |
|
|
42
|
+
| remove | Stopped | Remove the job |
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
The extension connects to the gob daemon's Unix socket (`$XDG_RUNTIME_DIR/gob/daemon.sock`) and subscribes to job events. If the daemon isn't running, the extension retries every 5 seconds and falls back to the `gob` CLI for the `/gob` command.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon client for connecting to gob daemon via Unix socket.
|
|
3
|
+
* Uses the newline-delimited JSON protocol defined in gob's daemon package.
|
|
4
|
+
*/
|
|
5
|
+
import type { Event, JobResponse, Response } from "./types.js";
|
|
6
|
+
export declare class DaemonClient {
|
|
7
|
+
private socketPath;
|
|
8
|
+
private subscriptionConn;
|
|
9
|
+
private _connected;
|
|
10
|
+
constructor();
|
|
11
|
+
get connected(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Probe the daemon socket and verify it's reachable.
|
|
14
|
+
* Does NOT auto-start the daemon.
|
|
15
|
+
* Returns true if daemon is running, false otherwise.
|
|
16
|
+
*/
|
|
17
|
+
connect(): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Send a request to the daemon over an ephemeral connection.
|
|
20
|
+
*/
|
|
21
|
+
sendRequest(type: string, payload?: Record<string, unknown>): Promise<Response>;
|
|
22
|
+
/**
|
|
23
|
+
* List jobs, optionally filtered by workdir.
|
|
24
|
+
*/
|
|
25
|
+
list(workdir?: string): Promise<JobResponse[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to daemon events. Returns an unsubscribe function.
|
|
28
|
+
* The subscription uses a persistent connection that streams events.
|
|
29
|
+
*
|
|
30
|
+
* @param workdir - Filter events to jobs in this workdir
|
|
31
|
+
* @param onEvent - Callback for each event
|
|
32
|
+
* @param onError - Callback when the subscription disconnects
|
|
33
|
+
*/
|
|
34
|
+
subscribe(workdir: string | undefined, onEvent: (event: Event) => void, onError: (err: Error) => void): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Close the subscription connection.
|
|
37
|
+
*/
|
|
38
|
+
private closeSubscription;
|
|
39
|
+
/**
|
|
40
|
+
* Disconnect from the daemon, closing the subscription.
|
|
41
|
+
*/
|
|
42
|
+
disconnect(): void;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=daemon-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-client.d.ts","sourceRoot":"","sources":["../src/daemon-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,YAAY,CAAC;AAyExE,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,UAAU,CAAS;;IAM3B,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBjC;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQrF;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAgBpD;;;;;;;OAOG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAqElH;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,UAAU,IAAI,IAAI;CAIlB"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon client for connecting to gob daemon via Unix socket.
|
|
3
|
+
* Uses the newline-delimited JSON protocol defined in gob's daemon package.
|
|
4
|
+
*/
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
/**
|
|
9
|
+
* Get the gob daemon socket path.
|
|
10
|
+
* Uses $XDG_RUNTIME_DIR/gob/daemon.sock if set.
|
|
11
|
+
* Falls back to ~/Library/Application Support/gob/daemon.sock on macOS
|
|
12
|
+
* (matching adrg/xdg Go library behavior).
|
|
13
|
+
* Falls back to /run/user/<uid>/gob/daemon.sock on Linux.
|
|
14
|
+
*/
|
|
15
|
+
function getSocketPath() {
|
|
16
|
+
const xdgRuntime = process.env.XDG_RUNTIME_DIR;
|
|
17
|
+
if (xdgRuntime) {
|
|
18
|
+
return path.join(xdgRuntime, "gob", "daemon.sock");
|
|
19
|
+
}
|
|
20
|
+
// macOS fallback — adrg/xdg uses ~/Library/Application Support as RuntimeDir
|
|
21
|
+
if (process.platform === "darwin") {
|
|
22
|
+
return path.join(os.homedir(), "Library", "Application Support", "gob", "daemon.sock");
|
|
23
|
+
}
|
|
24
|
+
// Linux fallback if XDG_RUNTIME_DIR not set
|
|
25
|
+
return path.join("/run/user", String(process.getuid?.()), "gob", "daemon.sock");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Send a single request over a new ephemeral connection and return the response.
|
|
29
|
+
* The daemon closes the connection after each response (except subscribe).
|
|
30
|
+
*/
|
|
31
|
+
async function sendEphemeralRequest(socketPath, req) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const conn = net.createConnection(socketPath);
|
|
34
|
+
let buffer = "";
|
|
35
|
+
conn.on("connect", () => {
|
|
36
|
+
conn.write(`${JSON.stringify(req)}\n`);
|
|
37
|
+
});
|
|
38
|
+
conn.on("data", (data) => {
|
|
39
|
+
buffer += data.toString();
|
|
40
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
41
|
+
if (newlineIdx !== -1) {
|
|
42
|
+
const line = buffer.slice(0, newlineIdx);
|
|
43
|
+
try {
|
|
44
|
+
const resp = JSON.parse(line);
|
|
45
|
+
conn.destroy();
|
|
46
|
+
resolve(resp);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
conn.destroy();
|
|
50
|
+
reject(new Error(`Failed to parse response: ${err}`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
conn.on("error", (err) => {
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
conn.on("close", () => {
|
|
58
|
+
if (buffer.trim()) {
|
|
59
|
+
try {
|
|
60
|
+
resolve(JSON.parse(buffer.trim()));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
reject(new Error("Connection closed before complete response"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Timeout after 5 seconds
|
|
68
|
+
conn.setTimeout(5000, () => {
|
|
69
|
+
conn.destroy();
|
|
70
|
+
reject(new Error("Connection timed out"));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export class DaemonClient {
|
|
75
|
+
socketPath;
|
|
76
|
+
subscriptionConn = null;
|
|
77
|
+
_connected = false;
|
|
78
|
+
constructor() {
|
|
79
|
+
this.socketPath = getSocketPath();
|
|
80
|
+
}
|
|
81
|
+
get connected() {
|
|
82
|
+
return this._connected;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Probe the daemon socket and verify it's reachable.
|
|
86
|
+
* Does NOT auto-start the daemon.
|
|
87
|
+
* Returns true if daemon is running, false otherwise.
|
|
88
|
+
*/
|
|
89
|
+
async connect() {
|
|
90
|
+
try {
|
|
91
|
+
const resp = await sendEphemeralRequest(this.socketPath, {
|
|
92
|
+
type: "version",
|
|
93
|
+
payload: { client_version: "pi-extension" },
|
|
94
|
+
});
|
|
95
|
+
if (resp.success) {
|
|
96
|
+
this._connected = true;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Send a request to the daemon over an ephemeral connection.
|
|
107
|
+
*/
|
|
108
|
+
async sendRequest(type, payload) {
|
|
109
|
+
const req = { type: type };
|
|
110
|
+
if (payload) {
|
|
111
|
+
req.payload = payload;
|
|
112
|
+
}
|
|
113
|
+
return sendEphemeralRequest(this.socketPath, req);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* List jobs, optionally filtered by workdir.
|
|
117
|
+
*/
|
|
118
|
+
async list(workdir) {
|
|
119
|
+
const payload = {};
|
|
120
|
+
if (workdir) {
|
|
121
|
+
payload.workdir = workdir;
|
|
122
|
+
}
|
|
123
|
+
const resp = await this.sendRequest("list", payload);
|
|
124
|
+
if (!resp.success) {
|
|
125
|
+
throw new Error(`list failed: ${resp.error}`);
|
|
126
|
+
}
|
|
127
|
+
const jobs = resp.data?.jobs;
|
|
128
|
+
if (!jobs || !Array.isArray(jobs)) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
return jobs;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Subscribe to daemon events. Returns an unsubscribe function.
|
|
135
|
+
* The subscription uses a persistent connection that streams events.
|
|
136
|
+
*
|
|
137
|
+
* @param workdir - Filter events to jobs in this workdir
|
|
138
|
+
* @param onEvent - Callback for each event
|
|
139
|
+
* @param onError - Callback when the subscription disconnects
|
|
140
|
+
*/
|
|
141
|
+
subscribe(workdir, onEvent, onError) {
|
|
142
|
+
// Close existing subscription if any
|
|
143
|
+
this.closeSubscription();
|
|
144
|
+
const conn = net.createConnection(this.socketPath);
|
|
145
|
+
this.subscriptionConn = conn;
|
|
146
|
+
let buffer = "";
|
|
147
|
+
let gotInitialResponse = false;
|
|
148
|
+
conn.on("connect", () => {
|
|
149
|
+
const req = { type: "subscribe" };
|
|
150
|
+
if (workdir) {
|
|
151
|
+
req.payload = { workdir };
|
|
152
|
+
}
|
|
153
|
+
conn.write(`${JSON.stringify(req)}\n`);
|
|
154
|
+
});
|
|
155
|
+
conn.on("data", (data) => {
|
|
156
|
+
buffer += data.toString();
|
|
157
|
+
// Process all complete lines
|
|
158
|
+
for (let newlineIdx = buffer.indexOf("\n"); newlineIdx !== -1; newlineIdx = buffer.indexOf("\n")) {
|
|
159
|
+
const line = buffer.slice(0, newlineIdx);
|
|
160
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
161
|
+
if (!line.trim())
|
|
162
|
+
continue;
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(line);
|
|
165
|
+
if (!gotInitialResponse) {
|
|
166
|
+
// First message is the subscribe response
|
|
167
|
+
gotInitialResponse = true;
|
|
168
|
+
if (!parsed.success) {
|
|
169
|
+
conn.destroy();
|
|
170
|
+
onError(new Error(`Subscribe failed: ${parsed.error}`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Subsequent messages are events
|
|
176
|
+
onEvent(parsed);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Skip malformed lines
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
conn.on("error", (err) => {
|
|
184
|
+
this.subscriptionConn = null;
|
|
185
|
+
onError(err);
|
|
186
|
+
});
|
|
187
|
+
conn.on("close", () => {
|
|
188
|
+
const wasSubscription = this.subscriptionConn === conn;
|
|
189
|
+
if (wasSubscription) {
|
|
190
|
+
this.subscriptionConn = null;
|
|
191
|
+
onError(new Error("Subscription connection closed"));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// Return unsubscribe function
|
|
195
|
+
return () => {
|
|
196
|
+
if (this.subscriptionConn === conn) {
|
|
197
|
+
this.closeSubscription();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Close the subscription connection.
|
|
203
|
+
*/
|
|
204
|
+
closeSubscription() {
|
|
205
|
+
if (this.subscriptionConn) {
|
|
206
|
+
this.subscriptionConn.destroy();
|
|
207
|
+
this.subscriptionConn = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Disconnect from the daemon, closing the subscription.
|
|
212
|
+
*/
|
|
213
|
+
disconnect() {
|
|
214
|
+
this.closeSubscription();
|
|
215
|
+
this._connected = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=daemon-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-client.js","sourceRoot":"","sources":["../src/daemon-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Daemon client for connecting to gob daemon via Unix socket.\n * Uses the newline-delimited JSON protocol defined in gob's daemon package.\n */\n\nimport * as net from \"node:net\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Event, JobResponse, Request, Response } from \"./types.js\";\n\n/**\n * Get the gob daemon socket path.\n * Uses $XDG_RUNTIME_DIR/gob/daemon.sock if set.\n * Falls back to ~/Library/Application Support/gob/daemon.sock on macOS\n * (matching adrg/xdg Go library behavior).\n * Falls back to /run/user/<uid>/gob/daemon.sock on Linux.\n */\nfunction getSocketPath(): string {\n\tconst xdgRuntime = process.env.XDG_RUNTIME_DIR;\n\tif (xdgRuntime) {\n\t\treturn path.join(xdgRuntime, \"gob\", \"daemon.sock\");\n\t}\n\t// macOS fallback — adrg/xdg uses ~/Library/Application Support as RuntimeDir\n\tif (process.platform === \"darwin\") {\n\t\treturn path.join(os.homedir(), \"Library\", \"Application Support\", \"gob\", \"daemon.sock\");\n\t}\n\t// Linux fallback if XDG_RUNTIME_DIR not set\n\treturn path.join(\"/run/user\", String(process.getuid?.()), \"gob\", \"daemon.sock\");\n}\n\n/**\n * Send a single request over a new ephemeral connection and return the response.\n * The daemon closes the connection after each response (except subscribe).\n */\nasync function sendEphemeralRequest(socketPath: string, req: Request): Promise<Response> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst conn = net.createConnection(socketPath);\n\t\tlet buffer = \"\";\n\n\t\tconn.on(\"connect\", () => {\n\t\t\tconn.write(`${JSON.stringify(req)}\\n`);\n\t\t});\n\n\t\tconn.on(\"data\", (data) => {\n\t\t\tbuffer += data.toString();\n\t\t\tconst newlineIdx = buffer.indexOf(\"\\n\");\n\t\t\tif (newlineIdx !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIdx);\n\t\t\t\ttry {\n\t\t\t\t\tconst resp = JSON.parse(line) as Response;\n\t\t\t\t\tconn.destroy();\n\t\t\t\t\tresolve(resp);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconn.destroy();\n\t\t\t\t\treject(new Error(`Failed to parse response: ${err}`));\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tconn.on(\"error\", (err) => {\n\t\t\treject(err);\n\t\t});\n\n\t\tconn.on(\"close\", () => {\n\t\t\tif (buffer.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\tresolve(JSON.parse(buffer.trim()) as Response);\n\t\t\t\t} catch {\n\t\t\t\t\treject(new Error(\"Connection closed before complete response\"));\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// Timeout after 5 seconds\n\t\tconn.setTimeout(5000, () => {\n\t\t\tconn.destroy();\n\t\t\treject(new Error(\"Connection timed out\"));\n\t\t});\n\t});\n}\n\nexport class DaemonClient {\n\tprivate socketPath: string;\n\tprivate subscriptionConn: net.Socket | null = null;\n\tprivate _connected = false;\n\n\tconstructor() {\n\t\tthis.socketPath = getSocketPath();\n\t}\n\n\tget connected(): boolean {\n\t\treturn this._connected;\n\t}\n\n\t/**\n\t * Probe the daemon socket and verify it's reachable.\n\t * Does NOT auto-start the daemon.\n\t * Returns true if daemon is running, false otherwise.\n\t */\n\tasync connect(): Promise<boolean> {\n\t\ttry {\n\t\t\tconst resp = await sendEphemeralRequest(this.socketPath, {\n\t\t\t\ttype: \"version\",\n\t\t\t\tpayload: { client_version: \"pi-extension\" },\n\t\t\t});\n\t\t\tif (resp.success) {\n\t\t\t\tthis._connected = true;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Send a request to the daemon over an ephemeral connection.\n\t */\n\tasync sendRequest(type: string, payload?: Record<string, unknown>): Promise<Response> {\n\t\tconst req: Request = { type: type as Request[\"type\"] };\n\t\tif (payload) {\n\t\t\treq.payload = payload;\n\t\t}\n\t\treturn sendEphemeralRequest(this.socketPath, req);\n\t}\n\n\t/**\n\t * List jobs, optionally filtered by workdir.\n\t */\n\tasync list(workdir?: string): Promise<JobResponse[]> {\n\t\tconst payload: Record<string, unknown> = {};\n\t\tif (workdir) {\n\t\t\tpayload.workdir = workdir;\n\t\t}\n\t\tconst resp = await this.sendRequest(\"list\", payload);\n\t\tif (!resp.success) {\n\t\t\tthrow new Error(`list failed: ${resp.error}`);\n\t\t}\n\t\tconst jobs = resp.data?.jobs;\n\t\tif (!jobs || !Array.isArray(jobs)) {\n\t\t\treturn [];\n\t\t}\n\t\treturn jobs as JobResponse[];\n\t}\n\n\t/**\n\t * Subscribe to daemon events. Returns an unsubscribe function.\n\t * The subscription uses a persistent connection that streams events.\n\t *\n\t * @param workdir - Filter events to jobs in this workdir\n\t * @param onEvent - Callback for each event\n\t * @param onError - Callback when the subscription disconnects\n\t */\n\tsubscribe(workdir: string | undefined, onEvent: (event: Event) => void, onError: (err: Error) => void): () => void {\n\t\t// Close existing subscription if any\n\t\tthis.closeSubscription();\n\n\t\tconst conn = net.createConnection(this.socketPath);\n\t\tthis.subscriptionConn = conn;\n\t\tlet buffer = \"\";\n\t\tlet gotInitialResponse = false;\n\n\t\tconn.on(\"connect\", () => {\n\t\t\tconst req: Request = { type: \"subscribe\" };\n\t\t\tif (workdir) {\n\t\t\t\treq.payload = { workdir };\n\t\t\t}\n\t\t\tconn.write(`${JSON.stringify(req)}\\n`);\n\t\t});\n\n\t\tconn.on(\"data\", (data) => {\n\t\t\tbuffer += data.toString();\n\t\t\t// Process all complete lines\n\t\t\tfor (let newlineIdx = buffer.indexOf(\"\\n\"); newlineIdx !== -1; newlineIdx = buffer.indexOf(\"\\n\")) {\n\t\t\t\tconst line = buffer.slice(0, newlineIdx);\n\t\t\t\tbuffer = buffer.slice(newlineIdx + 1);\n\n\t\t\t\tif (!line.trim()) continue;\n\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(line);\n\n\t\t\t\t\tif (!gotInitialResponse) {\n\t\t\t\t\t\t// First message is the subscribe response\n\t\t\t\t\t\tgotInitialResponse = true;\n\t\t\t\t\t\tif (!parsed.success) {\n\t\t\t\t\t\t\tconn.destroy();\n\t\t\t\t\t\t\tonError(new Error(`Subscribe failed: ${parsed.error}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Subsequent messages are events\n\t\t\t\t\tonEvent(parsed as Event);\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip malformed lines\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tconn.on(\"error\", (err) => {\n\t\t\tthis.subscriptionConn = null;\n\t\t\tonError(err);\n\t\t});\n\n\t\tconn.on(\"close\", () => {\n\t\t\tconst wasSubscription = this.subscriptionConn === conn;\n\t\t\tif (wasSubscription) {\n\t\t\t\tthis.subscriptionConn = null;\n\t\t\t\tonError(new Error(\"Subscription connection closed\"));\n\t\t\t}\n\t\t});\n\n\t\t// Return unsubscribe function\n\t\treturn () => {\n\t\t\tif (this.subscriptionConn === conn) {\n\t\t\t\tthis.closeSubscription();\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Close the subscription connection.\n\t */\n\tprivate closeSubscription(): void {\n\t\tif (this.subscriptionConn) {\n\t\t\tthis.subscriptionConn.destroy();\n\t\t\tthis.subscriptionConn = null;\n\t\t}\n\t}\n\n\t/**\n\t * Disconnect from the daemon, closing the subscription.\n\t */\n\tdisconnect(): void {\n\t\tthis.closeSubscription();\n\t\tthis._connected = false;\n\t}\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gob Extension
|
|
3
|
+
*
|
|
4
|
+
* Manages gob background jobs from within pi.
|
|
5
|
+
* Connects to the gob daemon via Unix socket for real-time job monitoring.
|
|
6
|
+
* Shows a widget below the editor with running jobs.
|
|
7
|
+
* Use /gob to view and interact with running and stopped jobs.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
export default function (pi: ExtensionAPI): void;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAWpF,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,QAuaxC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gob Extension
|
|
3
|
+
*
|
|
4
|
+
* Manages gob background jobs from within pi.
|
|
5
|
+
* Connects to the gob daemon via Unix socket for real-time job monitoring.
|
|
6
|
+
* Shows a widget below the editor with running jobs.
|
|
7
|
+
* Use /gob to view and interact with running and stopped jobs.
|
|
8
|
+
*/
|
|
9
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { Container, Key, matchesKey, SelectList, Text } from "@mariozechner/pi-tui";
|
|
11
|
+
import { DaemonClient } from "./daemon-client.js";
|
|
12
|
+
import { renderJobWidget } from "./widget.js";
|
|
13
|
+
const WIDGET_KEY = "gob-jobs";
|
|
14
|
+
const RECONNECT_DELAY_MS = 5000;
|
|
15
|
+
const TICK_INTERVAL_MS = 1000;
|
|
16
|
+
export default function (pi) {
|
|
17
|
+
const client = new DaemonClient();
|
|
18
|
+
// Map of running jobs by ID (only running jobs for the current workdir)
|
|
19
|
+
const runningJobs = new Map();
|
|
20
|
+
// Session CWD
|
|
21
|
+
let sessionCwd;
|
|
22
|
+
// Cleanup handles
|
|
23
|
+
let unsubscribe;
|
|
24
|
+
let reconnectTimer;
|
|
25
|
+
let tickInterval;
|
|
26
|
+
let sessionCtx;
|
|
27
|
+
/**
|
|
28
|
+
* Update the widget with current running jobs.
|
|
29
|
+
*/
|
|
30
|
+
function updateWidget() {
|
|
31
|
+
if (!sessionCtx?.hasUI)
|
|
32
|
+
return;
|
|
33
|
+
const jobs = Array.from(runningJobs.values());
|
|
34
|
+
if (jobs.length === 0) {
|
|
35
|
+
sessionCtx.ui.setWidget(WIDGET_KEY, undefined);
|
|
36
|
+
stopTick();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
sessionCtx.ui.setWidget(WIDGET_KEY, (_tui, theme) => {
|
|
40
|
+
return {
|
|
41
|
+
render: (width) => renderJobWidget(jobs, theme, width),
|
|
42
|
+
invalidate: () => { },
|
|
43
|
+
};
|
|
44
|
+
}, { placement: "belowEditor" });
|
|
45
|
+
startTick();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Start the 1-second tick interval for updating elapsed times.
|
|
49
|
+
*/
|
|
50
|
+
function startTick() {
|
|
51
|
+
if (tickInterval)
|
|
52
|
+
return;
|
|
53
|
+
tickInterval = setInterval(() => {
|
|
54
|
+
if (runningJobs.size > 0) {
|
|
55
|
+
updateWidget();
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
stopTick();
|
|
59
|
+
}
|
|
60
|
+
}, TICK_INTERVAL_MS);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Stop the tick interval.
|
|
64
|
+
*/
|
|
65
|
+
function stopTick() {
|
|
66
|
+
if (tickInterval) {
|
|
67
|
+
clearInterval(tickInterval);
|
|
68
|
+
tickInterval = undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handle a daemon event by updating the running jobs map.
|
|
73
|
+
*/
|
|
74
|
+
function handleEvent(event) {
|
|
75
|
+
const job = event.job;
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case "job_added":
|
|
78
|
+
case "job_started":
|
|
79
|
+
case "run_started":
|
|
80
|
+
case "job_updated":
|
|
81
|
+
case "ports_updated":
|
|
82
|
+
if (job.status === "running") {
|
|
83
|
+
runningJobs.set(job.id, job);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
runningJobs.delete(job.id);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "job_stopped":
|
|
90
|
+
case "run_stopped":
|
|
91
|
+
runningJobs.delete(job.id);
|
|
92
|
+
break;
|
|
93
|
+
case "job_removed":
|
|
94
|
+
case "run_removed":
|
|
95
|
+
runningJobs.delete(job.id);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
updateWidget();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Connect to daemon, fetch initial state, and subscribe to events.
|
|
102
|
+
*/
|
|
103
|
+
async function connectAndSubscribe() {
|
|
104
|
+
// Clear any pending reconnect
|
|
105
|
+
if (reconnectTimer) {
|
|
106
|
+
clearTimeout(reconnectTimer);
|
|
107
|
+
reconnectTimer = undefined;
|
|
108
|
+
}
|
|
109
|
+
const ok = await client.connect();
|
|
110
|
+
if (!ok) {
|
|
111
|
+
// Daemon not running, schedule reconnect
|
|
112
|
+
scheduleReconnect();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
// Fetch initial list of jobs
|
|
117
|
+
const jobs = await client.list(sessionCwd);
|
|
118
|
+
runningJobs.clear();
|
|
119
|
+
for (const job of jobs) {
|
|
120
|
+
if (job.status === "running") {
|
|
121
|
+
runningJobs.set(job.id, job);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
updateWidget();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Failed to list, schedule reconnect
|
|
128
|
+
scheduleReconnect();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Subscribe to events
|
|
132
|
+
unsubscribe = client.subscribe(sessionCwd, (event) => handleEvent(event), (_err) => {
|
|
133
|
+
// Subscription disconnected, try to reconnect
|
|
134
|
+
unsubscribe = undefined;
|
|
135
|
+
runningJobs.clear();
|
|
136
|
+
updateWidget();
|
|
137
|
+
scheduleReconnect();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Schedule a reconnection attempt.
|
|
142
|
+
*/
|
|
143
|
+
function scheduleReconnect() {
|
|
144
|
+
if (reconnectTimer)
|
|
145
|
+
return;
|
|
146
|
+
reconnectTimer = setTimeout(() => {
|
|
147
|
+
reconnectTimer = undefined;
|
|
148
|
+
connectAndSubscribe();
|
|
149
|
+
}, RECONNECT_DELAY_MS);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Cleanup all connections and timers.
|
|
153
|
+
*/
|
|
154
|
+
function cleanup() {
|
|
155
|
+
if (unsubscribe) {
|
|
156
|
+
unsubscribe();
|
|
157
|
+
unsubscribe = undefined;
|
|
158
|
+
}
|
|
159
|
+
if (reconnectTimer) {
|
|
160
|
+
clearTimeout(reconnectTimer);
|
|
161
|
+
reconnectTimer = undefined;
|
|
162
|
+
}
|
|
163
|
+
stopTick();
|
|
164
|
+
client.disconnect();
|
|
165
|
+
runningJobs.clear();
|
|
166
|
+
}
|
|
167
|
+
// ── Lifecycle ──────────────────────────────────────────
|
|
168
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
169
|
+
sessionCtx = ctx;
|
|
170
|
+
sessionCwd = ctx.cwd;
|
|
171
|
+
await connectAndSubscribe();
|
|
172
|
+
});
|
|
173
|
+
pi.on("session_shutdown", async () => {
|
|
174
|
+
cleanup();
|
|
175
|
+
if (sessionCtx?.hasUI) {
|
|
176
|
+
sessionCtx.ui.setWidget(WIDGET_KEY, undefined);
|
|
177
|
+
}
|
|
178
|
+
sessionCtx = undefined;
|
|
179
|
+
});
|
|
180
|
+
// ── Job list UI (existing functionality, now with protocol support) ──
|
|
181
|
+
async function fetchJobs() {
|
|
182
|
+
// Try daemon protocol first
|
|
183
|
+
if (client.connected) {
|
|
184
|
+
try {
|
|
185
|
+
return await client.list(sessionCwd);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Fall through to CLI
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// CLI fallback
|
|
192
|
+
const result = await pi.exec("gob", ["list"]);
|
|
193
|
+
if (result.code !== 0) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
return parseGobList(result.stdout);
|
|
197
|
+
}
|
|
198
|
+
function parseGobList(output) {
|
|
199
|
+
const jobs = [];
|
|
200
|
+
const lines = output.split("\n").filter((l) => l.trim().length > 0);
|
|
201
|
+
let i = 0;
|
|
202
|
+
while (i < lines.length) {
|
|
203
|
+
const line = lines[i];
|
|
204
|
+
// Format: <job_id>: [<pid>] <status> (<exit_code>): <command>
|
|
205
|
+
const match = line.match(/^(\S+): \[(\S+)\] (\S+)(?: \((\d+)\))?: (.+)$/);
|
|
206
|
+
if (match) {
|
|
207
|
+
const description = i + 1 < lines.length && lines[i + 1].startsWith(" ") ? lines[i + 1].trim() : undefined;
|
|
208
|
+
if (description)
|
|
209
|
+
i++;
|
|
210
|
+
jobs.push({
|
|
211
|
+
id: match[1],
|
|
212
|
+
pid: Number.parseInt(match[2], 10) || 0,
|
|
213
|
+
status: match[3],
|
|
214
|
+
command: match[5].split(" "),
|
|
215
|
+
workdir: "",
|
|
216
|
+
description,
|
|
217
|
+
created_at: "",
|
|
218
|
+
started_at: "",
|
|
219
|
+
stdout_path: "",
|
|
220
|
+
stderr_path: "",
|
|
221
|
+
exit_code: match[4] ? Number.parseInt(match[4], 10) : undefined,
|
|
222
|
+
run_count: 0,
|
|
223
|
+
success_count: 0,
|
|
224
|
+
failure_count: 0,
|
|
225
|
+
success_rate: 0,
|
|
226
|
+
avg_duration_ms: 0,
|
|
227
|
+
failure_avg_duration_ms: 0,
|
|
228
|
+
min_duration_ms: 0,
|
|
229
|
+
max_duration_ms: 0,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
return jobs;
|
|
235
|
+
}
|
|
236
|
+
async function executeJobAction(action, jobId) {
|
|
237
|
+
// Try daemon protocol first
|
|
238
|
+
if (client.connected) {
|
|
239
|
+
try {
|
|
240
|
+
const resp = await client.sendRequest(action, { job_id: jobId });
|
|
241
|
+
if (resp.success) {
|
|
242
|
+
const verb = action === "stop"
|
|
243
|
+
? "Stopped"
|
|
244
|
+
: action === "start"
|
|
245
|
+
? "Started"
|
|
246
|
+
: action === "restart"
|
|
247
|
+
? "Restarted"
|
|
248
|
+
: action === "remove"
|
|
249
|
+
? "Removed"
|
|
250
|
+
: action;
|
|
251
|
+
return { message: `${verb} ${jobId}`, success: true };
|
|
252
|
+
}
|
|
253
|
+
return { message: `Error: ${resp.error}`, success: false };
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Fall through to CLI
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// CLI fallback
|
|
260
|
+
const result = await pi.exec("gob", [action, jobId]);
|
|
261
|
+
if (result.code === 0) {
|
|
262
|
+
const verb = action === "stop"
|
|
263
|
+
? "Stopped"
|
|
264
|
+
: action === "start"
|
|
265
|
+
? "Started"
|
|
266
|
+
: action === "restart"
|
|
267
|
+
? "Restarted"
|
|
268
|
+
: action === "remove"
|
|
269
|
+
? "Removed"
|
|
270
|
+
: action;
|
|
271
|
+
return { message: `${verb} ${jobId}`, success: true };
|
|
272
|
+
}
|
|
273
|
+
return { message: `Error: ${result.stderr}`, success: false };
|
|
274
|
+
}
|
|
275
|
+
const showJobList = async (ctx) => {
|
|
276
|
+
if (!ctx.hasUI) {
|
|
277
|
+
ctx.ui.notify("No UI available", "error");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const jobs = await fetchJobs();
|
|
281
|
+
if (jobs.length === 0) {
|
|
282
|
+
ctx.ui.notify("No gob jobs found", "info");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
286
|
+
const container = new Container();
|
|
287
|
+
// Top border
|
|
288
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
289
|
+
// Title
|
|
290
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Gob Jobs")), 0, 0));
|
|
291
|
+
// Build select items
|
|
292
|
+
const items = jobs.map((job) => {
|
|
293
|
+
const statusColor = job.status === "running" ? "success" : "muted";
|
|
294
|
+
const statusIcon = job.status === "running" ? "●" : "○";
|
|
295
|
+
const exitInfo = job.status === "stopped" && job.exit_code != null ? theme.fg("dim", ` (${job.exit_code})`) : "";
|
|
296
|
+
const status = theme.fg(statusColor, statusIcon);
|
|
297
|
+
const cmdStr = job.command.join(" ");
|
|
298
|
+
const label = `${status} ${theme.fg("dim", job.id)} ${cmdStr}${exitInfo}`;
|
|
299
|
+
const description = job.description ? theme.fg("muted", job.description) : undefined;
|
|
300
|
+
return {
|
|
301
|
+
value: job.id,
|
|
302
|
+
label,
|
|
303
|
+
description,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
const visibleRows = Math.min(jobs.length, 15);
|
|
307
|
+
let currentIndex = 0;
|
|
308
|
+
const selectList = new SelectList(items, visibleRows, {
|
|
309
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
310
|
+
selectedText: (t) => t,
|
|
311
|
+
description: (t) => t,
|
|
312
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
313
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
314
|
+
});
|
|
315
|
+
selectList.onSelect = async (item) => {
|
|
316
|
+
done();
|
|
317
|
+
const job = jobs.find((j) => j.id === item.value);
|
|
318
|
+
if (job) {
|
|
319
|
+
await showJobActions(ctx, job);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
selectList.onCancel = () => done();
|
|
323
|
+
selectList.onSelectionChange = (item) => {
|
|
324
|
+
currentIndex = items.indexOf(item);
|
|
325
|
+
};
|
|
326
|
+
container.addChild(selectList);
|
|
327
|
+
// Help text
|
|
328
|
+
container.addChild(new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter actions • esc close"), 0, 0));
|
|
329
|
+
// Bottom border
|
|
330
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
331
|
+
return {
|
|
332
|
+
render: (w) => container.render(w),
|
|
333
|
+
invalidate: () => container.invalidate(),
|
|
334
|
+
handleInput: (data) => {
|
|
335
|
+
if (matchesKey(data, Key.left)) {
|
|
336
|
+
currentIndex = Math.max(0, currentIndex - visibleRows);
|
|
337
|
+
selectList.setSelectedIndex(currentIndex);
|
|
338
|
+
}
|
|
339
|
+
else if (matchesKey(data, Key.right)) {
|
|
340
|
+
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
|
341
|
+
selectList.setSelectedIndex(currentIndex);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
selectList.handleInput(data);
|
|
345
|
+
}
|
|
346
|
+
tui.requestRender();
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
const showJobActions = async (ctx, job) => {
|
|
352
|
+
const actions = [];
|
|
353
|
+
if (job.status === "running") {
|
|
354
|
+
actions.push("logs", "stop", "restart");
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
actions.push("logs", "start", "restart", "remove");
|
|
358
|
+
}
|
|
359
|
+
const cmdStr = job.command.join(" ");
|
|
360
|
+
const action = await ctx.ui.select(`${job.id}: ${cmdStr}`, actions);
|
|
361
|
+
if (!action)
|
|
362
|
+
return;
|
|
363
|
+
switch (action) {
|
|
364
|
+
case "logs": {
|
|
365
|
+
const result = await pi.exec("gob", ["logs", "--tail", "50", job.id], { timeout: 5000 });
|
|
366
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
367
|
+
ctx.ui.notify(`Logs for ${job.id}:\n${result.stdout.trim()}`, "info");
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
ctx.ui.notify(`No logs for ${job.id}`, "info");
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "stop":
|
|
375
|
+
case "start":
|
|
376
|
+
case "restart":
|
|
377
|
+
case "remove": {
|
|
378
|
+
const result = await executeJobAction(action, job.id);
|
|
379
|
+
ctx.ui.notify(result.message, result.success ? "info" : "error");
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
// Register /gob command
|
|
385
|
+
pi.registerCommand("gob", {
|
|
386
|
+
description: "View and manage gob background jobs",
|
|
387
|
+
handler: async (_args, ctx) => showJobList(ctx),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Gob Extension\n *\n * Manages gob background jobs from within pi.\n * Connects to the gob daemon via Unix socket for real-time job monitoring.\n * Shows a widget below the editor with running jobs.\n * Use /gob to view and interact with running and stopped jobs.\n */\n\nimport type { ExtensionAPI, ExtensionContext } from \"@mariozechner/pi-coding-agent\";\nimport { DynamicBorder } from \"@mariozechner/pi-coding-agent\";\nimport { Container, Key, matchesKey, type SelectItem, SelectList, Text } from \"@mariozechner/pi-tui\";\nimport { DaemonClient } from \"./daemon-client.js\";\nimport type { Event, JobResponse } from \"./types.js\";\nimport { renderJobWidget } from \"./widget.js\";\n\nconst WIDGET_KEY = \"gob-jobs\";\nconst RECONNECT_DELAY_MS = 5000;\nconst TICK_INTERVAL_MS = 1000;\n\nexport default function (pi: ExtensionAPI) {\n\tconst client = new DaemonClient();\n\n\t// Map of running jobs by ID (only running jobs for the current workdir)\n\tconst runningJobs = new Map<string, JobResponse>();\n\n\t// Session CWD\n\tlet sessionCwd: string | undefined;\n\n\t// Cleanup handles\n\tlet unsubscribe: (() => void) | undefined;\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n\tlet tickInterval: ReturnType<typeof setInterval> | undefined;\n\tlet sessionCtx: ExtensionContext | undefined;\n\n\t/**\n\t * Update the widget with current running jobs.\n\t */\n\tfunction updateWidget(): void {\n\t\tif (!sessionCtx?.hasUI) return;\n\n\t\tconst jobs = Array.from(runningJobs.values());\n\n\t\tif (jobs.length === 0) {\n\t\t\tsessionCtx.ui.setWidget(WIDGET_KEY, undefined);\n\t\t\tstopTick();\n\t\t\treturn;\n\t\t}\n\n\t\tsessionCtx.ui.setWidget(\n\t\t\tWIDGET_KEY,\n\t\t\t(_tui, theme) => {\n\t\t\t\treturn {\n\t\t\t\t\trender: (width: number) => renderJobWidget(jobs, theme, width),\n\t\t\t\t\tinvalidate: () => {},\n\t\t\t\t};\n\t\t\t},\n\t\t\t{ placement: \"belowEditor\" },\n\t\t);\n\n\t\tstartTick();\n\t}\n\n\t/**\n\t * Start the 1-second tick interval for updating elapsed times.\n\t */\n\tfunction startTick(): void {\n\t\tif (tickInterval) return;\n\t\ttickInterval = setInterval(() => {\n\t\t\tif (runningJobs.size > 0) {\n\t\t\t\tupdateWidget();\n\t\t\t} else {\n\t\t\t\tstopTick();\n\t\t\t}\n\t\t}, TICK_INTERVAL_MS);\n\t}\n\n\t/**\n\t * Stop the tick interval.\n\t */\n\tfunction stopTick(): void {\n\t\tif (tickInterval) {\n\t\t\tclearInterval(tickInterval);\n\t\t\ttickInterval = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle a daemon event by updating the running jobs map.\n\t */\n\tfunction handleEvent(event: Event): void {\n\t\tconst job = event.job;\n\n\t\tswitch (event.type) {\n\t\t\tcase \"job_added\":\n\t\t\tcase \"job_started\":\n\t\t\tcase \"run_started\":\n\t\t\tcase \"job_updated\":\n\t\t\tcase \"ports_updated\":\n\t\t\t\tif (job.status === \"running\") {\n\t\t\t\t\trunningJobs.set(job.id, job);\n\t\t\t\t} else {\n\t\t\t\t\trunningJobs.delete(job.id);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"job_stopped\":\n\t\t\tcase \"run_stopped\":\n\t\t\t\trunningJobs.delete(job.id);\n\t\t\t\tbreak;\n\n\t\t\tcase \"job_removed\":\n\t\t\tcase \"run_removed\":\n\t\t\t\trunningJobs.delete(job.id);\n\t\t\t\tbreak;\n\t\t}\n\n\t\tupdateWidget();\n\t}\n\n\t/**\n\t * Connect to daemon, fetch initial state, and subscribe to events.\n\t */\n\tasync function connectAndSubscribe(): Promise<void> {\n\t\t// Clear any pending reconnect\n\t\tif (reconnectTimer) {\n\t\t\tclearTimeout(reconnectTimer);\n\t\t\treconnectTimer = undefined;\n\t\t}\n\n\t\tconst ok = await client.connect();\n\t\tif (!ok) {\n\t\t\t// Daemon not running, schedule reconnect\n\t\t\tscheduleReconnect();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// Fetch initial list of jobs\n\t\t\tconst jobs = await client.list(sessionCwd);\n\t\t\trunningJobs.clear();\n\t\t\tfor (const job of jobs) {\n\t\t\t\tif (job.status === \"running\") {\n\t\t\t\t\trunningJobs.set(job.id, job);\n\t\t\t\t}\n\t\t\t}\n\t\t\tupdateWidget();\n\t\t} catch {\n\t\t\t// Failed to list, schedule reconnect\n\t\t\tscheduleReconnect();\n\t\t\treturn;\n\t\t}\n\n\t\t// Subscribe to events\n\t\tunsubscribe = client.subscribe(\n\t\t\tsessionCwd,\n\t\t\t(event) => handleEvent(event),\n\t\t\t(_err) => {\n\t\t\t\t// Subscription disconnected, try to reconnect\n\t\t\t\tunsubscribe = undefined;\n\t\t\t\trunningJobs.clear();\n\t\t\t\tupdateWidget();\n\t\t\t\tscheduleReconnect();\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Schedule a reconnection attempt.\n\t */\n\tfunction scheduleReconnect(): void {\n\t\tif (reconnectTimer) return;\n\t\treconnectTimer = setTimeout(() => {\n\t\t\treconnectTimer = undefined;\n\t\t\tconnectAndSubscribe();\n\t\t}, RECONNECT_DELAY_MS);\n\t}\n\n\t/**\n\t * Cleanup all connections and timers.\n\t */\n\tfunction cleanup(): void {\n\t\tif (unsubscribe) {\n\t\t\tunsubscribe();\n\t\t\tunsubscribe = undefined;\n\t\t}\n\t\tif (reconnectTimer) {\n\t\t\tclearTimeout(reconnectTimer);\n\t\t\treconnectTimer = undefined;\n\t\t}\n\t\tstopTick();\n\t\tclient.disconnect();\n\t\trunningJobs.clear();\n\t}\n\n\t// ── Lifecycle ──────────────────────────────────────────\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsessionCtx = ctx;\n\t\tsessionCwd = ctx.cwd;\n\t\tawait connectAndSubscribe();\n\t});\n\n\tpi.on(\"session_shutdown\", async () => {\n\t\tcleanup();\n\t\tif (sessionCtx?.hasUI) {\n\t\t\tsessionCtx.ui.setWidget(WIDGET_KEY, undefined);\n\t\t}\n\t\tsessionCtx = undefined;\n\t});\n\n\t// ── Job list UI (existing functionality, now with protocol support) ──\n\n\tasync function fetchJobs(): Promise<JobResponse[]> {\n\t\t// Try daemon protocol first\n\t\tif (client.connected) {\n\t\t\ttry {\n\t\t\t\treturn await client.list(sessionCwd);\n\t\t\t} catch {\n\t\t\t\t// Fall through to CLI\n\t\t\t}\n\t\t}\n\n\t\t// CLI fallback\n\t\tconst result = await pi.exec(\"gob\", [\"list\"]);\n\t\tif (result.code !== 0) {\n\t\t\treturn [];\n\t\t}\n\t\treturn parseGobList(result.stdout);\n\t}\n\n\tfunction parseGobList(output: string): JobResponse[] {\n\t\tconst jobs: JobResponse[] = [];\n\t\tconst lines = output.split(\"\\n\").filter((l) => l.trim().length > 0);\n\n\t\tlet i = 0;\n\t\twhile (i < lines.length) {\n\t\t\tconst line = lines[i];\n\t\t\t// Format: <job_id>: [<pid>] <status> (<exit_code>): <command>\n\t\t\tconst match = line.match(/^(\\S+): \\[(\\S+)\\] (\\S+)(?: \\((\\d+)\\))?: (.+)$/);\n\t\t\tif (match) {\n\t\t\t\tconst description =\n\t\t\t\t\ti + 1 < lines.length && lines[i + 1].startsWith(\" \") ? lines[i + 1].trim() : undefined;\n\n\t\t\t\tif (description) i++;\n\n\t\t\t\tjobs.push({\n\t\t\t\t\tid: match[1],\n\t\t\t\t\tpid: Number.parseInt(match[2], 10) || 0,\n\t\t\t\t\tstatus: match[3],\n\t\t\t\t\tcommand: match[5].split(\" \"),\n\t\t\t\t\tworkdir: \"\",\n\t\t\t\t\tdescription,\n\t\t\t\t\tcreated_at: \"\",\n\t\t\t\t\tstarted_at: \"\",\n\t\t\t\t\tstdout_path: \"\",\n\t\t\t\t\tstderr_path: \"\",\n\t\t\t\t\texit_code: match[4] ? Number.parseInt(match[4], 10) : undefined,\n\t\t\t\t\trun_count: 0,\n\t\t\t\t\tsuccess_count: 0,\n\t\t\t\t\tfailure_count: 0,\n\t\t\t\t\tsuccess_rate: 0,\n\t\t\t\t\tavg_duration_ms: 0,\n\t\t\t\t\tfailure_avg_duration_ms: 0,\n\t\t\t\t\tmin_duration_ms: 0,\n\t\t\t\t\tmax_duration_ms: 0,\n\t\t\t\t});\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\n\t\treturn jobs;\n\t}\n\n\tasync function executeJobAction(action: string, jobId: string): Promise<{ message: string; success: boolean }> {\n\t\t// Try daemon protocol first\n\t\tif (client.connected) {\n\t\t\ttry {\n\t\t\t\tconst resp = await client.sendRequest(action, { job_id: jobId });\n\t\t\t\tif (resp.success) {\n\t\t\t\t\tconst verb =\n\t\t\t\t\t\taction === \"stop\"\n\t\t\t\t\t\t\t? \"Stopped\"\n\t\t\t\t\t\t\t: action === \"start\"\n\t\t\t\t\t\t\t\t? \"Started\"\n\t\t\t\t\t\t\t\t: action === \"restart\"\n\t\t\t\t\t\t\t\t\t? \"Restarted\"\n\t\t\t\t\t\t\t\t\t: action === \"remove\"\n\t\t\t\t\t\t\t\t\t\t? \"Removed\"\n\t\t\t\t\t\t\t\t\t\t: action;\n\t\t\t\t\treturn { message: `${verb} ${jobId}`, success: true };\n\t\t\t\t}\n\t\t\t\treturn { message: `Error: ${resp.error}`, success: false };\n\t\t\t} catch {\n\t\t\t\t// Fall through to CLI\n\t\t\t}\n\t\t}\n\n\t\t// CLI fallback\n\t\tconst result = await pi.exec(\"gob\", [action, jobId]);\n\t\tif (result.code === 0) {\n\t\t\tconst verb =\n\t\t\t\taction === \"stop\"\n\t\t\t\t\t? \"Stopped\"\n\t\t\t\t\t: action === \"start\"\n\t\t\t\t\t\t? \"Started\"\n\t\t\t\t\t\t: action === \"restart\"\n\t\t\t\t\t\t\t? \"Restarted\"\n\t\t\t\t\t\t\t: action === \"remove\"\n\t\t\t\t\t\t\t\t? \"Removed\"\n\t\t\t\t\t\t\t\t: action;\n\t\t\treturn { message: `${verb} ${jobId}`, success: true };\n\t\t}\n\t\treturn { message: `Error: ${result.stderr}`, success: false };\n\t}\n\n\tconst showJobList = async (ctx: ExtensionContext) => {\n\t\tif (!ctx.hasUI) {\n\t\t\tctx.ui.notify(\"No UI available\", \"error\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst jobs = await fetchJobs();\n\n\t\tif (jobs.length === 0) {\n\t\t\tctx.ui.notify(\"No gob jobs found\", \"info\");\n\t\t\treturn;\n\t\t}\n\n\t\tawait ctx.ui.custom<void>((tui, theme, _kb, done) => {\n\t\t\tconst container = new Container();\n\n\t\t\t// Top border\n\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t// Title\n\t\t\tcontainer.addChild(new Text(theme.fg(\"accent\", theme.bold(\" Gob Jobs\")), 0, 0));\n\n\t\t\t// Build select items\n\t\t\tconst items: SelectItem[] = jobs.map((job) => {\n\t\t\t\tconst statusColor = job.status === \"running\" ? \"success\" : \"muted\";\n\t\t\t\tconst statusIcon = job.status === \"running\" ? \"●\" : \"○\";\n\t\t\t\tconst exitInfo =\n\t\t\t\t\tjob.status === \"stopped\" && job.exit_code != null ? theme.fg(\"dim\", ` (${job.exit_code})`) : \"\";\n\t\t\t\tconst status = theme.fg(statusColor, statusIcon);\n\t\t\t\tconst cmdStr = job.command.join(\" \");\n\t\t\t\tconst label = `${status} ${theme.fg(\"dim\", job.id)} ${cmdStr}${exitInfo}`;\n\t\t\t\tconst description = job.description ? theme.fg(\"muted\", job.description) : undefined;\n\t\t\t\treturn {\n\t\t\t\t\tvalue: job.id,\n\t\t\t\t\tlabel,\n\t\t\t\t\tdescription,\n\t\t\t\t};\n\t\t\t});\n\n\t\t\tconst visibleRows = Math.min(jobs.length, 15);\n\t\t\tlet currentIndex = 0;\n\n\t\t\tconst selectList = new SelectList(items, visibleRows, {\n\t\t\t\tselectedPrefix: (t) => theme.fg(\"accent\", t),\n\t\t\t\tselectedText: (t) => t,\n\t\t\t\tdescription: (t) => t,\n\t\t\t\tscrollInfo: (t) => theme.fg(\"dim\", t),\n\t\t\t\tnoMatch: (t) => theme.fg(\"warning\", t),\n\t\t\t});\n\n\t\t\tselectList.onSelect = async (item) => {\n\t\t\t\tdone();\n\t\t\t\tconst job = jobs.find((j) => j.id === item.value);\n\t\t\t\tif (job) {\n\t\t\t\t\tawait showJobActions(ctx, job);\n\t\t\t\t}\n\t\t\t};\n\t\t\tselectList.onCancel = () => done();\n\t\t\tselectList.onSelectionChange = (item) => {\n\t\t\t\tcurrentIndex = items.indexOf(item);\n\t\t\t};\n\t\t\tcontainer.addChild(selectList);\n\n\t\t\t// Help text\n\t\t\tcontainer.addChild(new Text(theme.fg(\"dim\", \" ↑↓ navigate • ←→ page • enter actions • esc close\"), 0, 0));\n\n\t\t\t// Bottom border\n\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\treturn {\n\t\t\t\trender: (w) => container.render(w),\n\t\t\t\tinvalidate: () => container.invalidate(),\n\t\t\t\thandleInput: (data) => {\n\t\t\t\t\tif (matchesKey(data, Key.left)) {\n\t\t\t\t\t\tcurrentIndex = Math.max(0, currentIndex - visibleRows);\n\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t} else if (matchesKey(data, Key.right)) {\n\t\t\t\t\t\tcurrentIndex = Math.min(items.length - 1, currentIndex + visibleRows);\n\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tselectList.handleInput(data);\n\t\t\t\t\t}\n\t\t\t\t\ttui.requestRender();\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t};\n\n\tconst showJobActions = async (ctx: ExtensionContext, job: JobResponse) => {\n\t\tconst actions: string[] = [];\n\n\t\tif (job.status === \"running\") {\n\t\t\tactions.push(\"logs\", \"stop\", \"restart\");\n\t\t} else {\n\t\t\tactions.push(\"logs\", \"start\", \"restart\", \"remove\");\n\t\t}\n\n\t\tconst cmdStr = job.command.join(\" \");\n\t\tconst action = await ctx.ui.select(`${job.id}: ${cmdStr}`, actions);\n\t\tif (!action) return;\n\n\t\tswitch (action) {\n\t\t\tcase \"logs\": {\n\t\t\t\tconst result = await pi.exec(\"gob\", [\"logs\", \"--tail\", \"50\", job.id], { timeout: 5000 });\n\t\t\t\tif (result.code === 0 && result.stdout.trim()) {\n\t\t\t\t\tctx.ui.notify(`Logs for ${job.id}:\\n${result.stdout.trim()}`, \"info\");\n\t\t\t\t} else {\n\t\t\t\t\tctx.ui.notify(`No logs for ${job.id}`, \"info\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stop\":\n\t\t\tcase \"start\":\n\t\t\tcase \"restart\":\n\t\t\tcase \"remove\": {\n\t\t\t\tconst result = await executeJobAction(action, job.id);\n\t\t\t\tctx.ui.notify(result.message, result.success ? \"info\" : \"error\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t// Register /gob command\n\tpi.registerCommand(\"gob\", {\n\t\tdescription: \"View and manage gob background jobs\",\n\t\thandler: async (_args, ctx) => showJobList(ctx),\n\t});\n}\n"]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript types mirroring the gob daemon protocol.
|
|
3
|
+
* See gob/internal/daemon/protocol.go for the canonical definitions.
|
|
4
|
+
*/
|
|
5
|
+
export type RequestType = "ping" | "shutdown" | "list" | "add" | "create" | "stop" | "start" | "restart" | "remove" | "stop_all" | "signal" | "get_job" | "runs" | "stats" | "subscribe" | "version" | "ports" | "remove_run";
|
|
6
|
+
export type EventType = "job_added" | "job_started" | "job_stopped" | "job_removed" | "job_updated" | "run_started" | "run_stopped" | "run_removed" | "ports_updated";
|
|
7
|
+
export interface Request {
|
|
8
|
+
type: RequestType;
|
|
9
|
+
payload?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface Response {
|
|
12
|
+
success: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
data?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface PortInfo {
|
|
17
|
+
port: number;
|
|
18
|
+
protocol: string;
|
|
19
|
+
pid: number;
|
|
20
|
+
address: string;
|
|
21
|
+
}
|
|
22
|
+
export interface JobResponse {
|
|
23
|
+
id: string;
|
|
24
|
+
pid: number;
|
|
25
|
+
status: string;
|
|
26
|
+
command: string[];
|
|
27
|
+
workdir: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
blocked?: boolean;
|
|
30
|
+
created_at: string;
|
|
31
|
+
started_at: string;
|
|
32
|
+
stopped_at?: string;
|
|
33
|
+
stdout_path: string;
|
|
34
|
+
stderr_path: string;
|
|
35
|
+
exit_code?: number | null;
|
|
36
|
+
ports?: PortInfo[];
|
|
37
|
+
run_count: number;
|
|
38
|
+
success_count: number;
|
|
39
|
+
failure_count: number;
|
|
40
|
+
success_rate: number;
|
|
41
|
+
avg_duration_ms: number;
|
|
42
|
+
failure_avg_duration_ms: number;
|
|
43
|
+
min_duration_ms: number;
|
|
44
|
+
max_duration_ms: number;
|
|
45
|
+
}
|
|
46
|
+
export interface RunResponse {
|
|
47
|
+
id: string;
|
|
48
|
+
job_id: string;
|
|
49
|
+
pid: number;
|
|
50
|
+
status: string;
|
|
51
|
+
exit_code?: number | null;
|
|
52
|
+
stdout_path: string;
|
|
53
|
+
stderr_path: string;
|
|
54
|
+
started_at: string;
|
|
55
|
+
stopped_at?: string;
|
|
56
|
+
duration_ms: number;
|
|
57
|
+
}
|
|
58
|
+
export interface Event {
|
|
59
|
+
type: EventType;
|
|
60
|
+
job_id: string;
|
|
61
|
+
job: JobResponse;
|
|
62
|
+
run?: RunResponse;
|
|
63
|
+
ports?: PortInfo[];
|
|
64
|
+
job_count: number;
|
|
65
|
+
running_job_count: number;
|
|
66
|
+
}
|
|
67
|
+
export interface JobPorts {
|
|
68
|
+
job_id: string;
|
|
69
|
+
pid: number;
|
|
70
|
+
ports: PortInfo[];
|
|
71
|
+
status?: string;
|
|
72
|
+
message?: string;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * TypeScript types mirroring the gob daemon protocol.\n * See gob/internal/daemon/protocol.go for the canonical definitions.\n */\n\n// Request types\nexport type RequestType =\n\t| \"ping\"\n\t| \"shutdown\"\n\t| \"list\"\n\t| \"add\"\n\t| \"create\"\n\t| \"stop\"\n\t| \"start\"\n\t| \"restart\"\n\t| \"remove\"\n\t| \"stop_all\"\n\t| \"signal\"\n\t| \"get_job\"\n\t| \"runs\"\n\t| \"stats\"\n\t| \"subscribe\"\n\t| \"version\"\n\t| \"ports\"\n\t| \"remove_run\";\n\n// Event types\nexport type EventType =\n\t| \"job_added\"\n\t| \"job_started\"\n\t| \"job_stopped\"\n\t| \"job_removed\"\n\t| \"job_updated\"\n\t| \"run_started\"\n\t| \"run_stopped\"\n\t| \"run_removed\"\n\t| \"ports_updated\";\n\n// Protocol messages\nexport interface Request {\n\ttype: RequestType;\n\tpayload?: Record<string, unknown>;\n}\n\nexport interface Response {\n\tsuccess: boolean;\n\terror?: string;\n\tdata?: Record<string, unknown>;\n}\n\n// Port information\nexport interface PortInfo {\n\tport: number;\n\tprotocol: string;\n\tpid: number;\n\taddress: string;\n}\n\n// Job response from daemon\nexport interface JobResponse {\n\tid: string;\n\tpid: number;\n\tstatus: string;\n\tcommand: string[];\n\tworkdir: string;\n\tdescription?: string;\n\tblocked?: boolean;\n\tcreated_at: string;\n\tstarted_at: string;\n\tstopped_at?: string;\n\tstdout_path: string;\n\tstderr_path: string;\n\texit_code?: number | null;\n\tports?: PortInfo[];\n\n\t// Statistics\n\trun_count: number;\n\tsuccess_count: number;\n\tfailure_count: number;\n\tsuccess_rate: number;\n\tavg_duration_ms: number;\n\tfailure_avg_duration_ms: number;\n\tmin_duration_ms: number;\n\tmax_duration_ms: number;\n}\n\n// Run response from daemon\nexport interface RunResponse {\n\tid: string;\n\tjob_id: string;\n\tpid: number;\n\tstatus: string;\n\texit_code?: number | null;\n\tstdout_path: string;\n\tstderr_path: string;\n\tstarted_at: string;\n\tstopped_at?: string;\n\tduration_ms: number;\n}\n\n// Event from subscription stream\nexport interface Event {\n\ttype: EventType;\n\tjob_id: string;\n\tjob: JobResponse;\n\trun?: RunResponse;\n\tports?: PortInfo[];\n\tjob_count: number;\n\trunning_job_count: number;\n}\n\n// Job ports response\nexport interface JobPorts {\n\tjob_id: string;\n\tpid: number;\n\tports: PortInfo[];\n\tstatus?: string;\n\tmessage?: string;\n}\n"]}
|
package/dist/widget.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget rendering for gob running jobs.
|
|
3
|
+
* Renders a single horizontal line showing running jobs.
|
|
4
|
+
*
|
|
5
|
+
* Layout: ● server 0:12:34 │ ● build ████░░░ 57% │ ● lint 0:00:05
|
|
6
|
+
*/
|
|
7
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { JobResponse } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Render the running jobs widget as a single horizontal line.
|
|
11
|
+
* Returns an empty array if no running jobs.
|
|
12
|
+
*
|
|
13
|
+
* @param jobs - Running jobs to display
|
|
14
|
+
* @param theme - Theme for styling
|
|
15
|
+
* @param width - Terminal width for truncation
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderJobWidget(jobs: JobResponse[], theme: Theme, width: number): string[];
|
|
18
|
+
//# sourceMappingURL=widget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.d.ts","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA8D9C;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAU1F"}
|
package/dist/widget.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget rendering for gob running jobs.
|
|
3
|
+
* Renders a single horizontal line showing running jobs.
|
|
4
|
+
*
|
|
5
|
+
* Layout: ● server 0:12:34 │ ● build ████░░░ 57% │ ● lint 0:00:05
|
|
6
|
+
*/
|
|
7
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
8
|
+
/**
|
|
9
|
+
* Format elapsed time as H:MM:SS or M:SS.
|
|
10
|
+
*/
|
|
11
|
+
function formatElapsed(startedAt) {
|
|
12
|
+
const start = new Date(startedAt).getTime();
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const elapsed = Math.max(0, Math.floor((now - start) / 1000));
|
|
15
|
+
const hours = Math.floor(elapsed / 3600);
|
|
16
|
+
const minutes = Math.floor((elapsed % 3600) / 60);
|
|
17
|
+
const seconds = elapsed % 60;
|
|
18
|
+
if (hours > 0) {
|
|
19
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
20
|
+
}
|
|
21
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render a progress bar for a job with known average duration.
|
|
25
|
+
* Returns something like "████░░░░ 57%" or "████████ 2:15" (overtime).
|
|
26
|
+
*/
|
|
27
|
+
function renderProgress(startedAt, avgDurationMs, theme) {
|
|
28
|
+
const start = new Date(startedAt).getTime();
|
|
29
|
+
const elapsedMs = Date.now() - start;
|
|
30
|
+
const ratio = Math.min(elapsedMs / avgDurationMs, 1);
|
|
31
|
+
const barWidth = 8;
|
|
32
|
+
const filled = Math.round(ratio * barWidth);
|
|
33
|
+
const empty = barWidth - filled;
|
|
34
|
+
const isOvertime = elapsedMs > avgDurationMs;
|
|
35
|
+
if (isOvertime) {
|
|
36
|
+
// Full bar in warning color + overtime elapsed
|
|
37
|
+
const bar = theme.fg("warning", "█".repeat(barWidth));
|
|
38
|
+
const overtime = formatElapsed(startedAt);
|
|
39
|
+
return `${bar} ${theme.fg("warning", overtime)}`;
|
|
40
|
+
}
|
|
41
|
+
const percent = Math.round(ratio * 100);
|
|
42
|
+
const bar = theme.fg("success", "█".repeat(filled)) + theme.fg("dim", "░".repeat(empty));
|
|
43
|
+
return `${bar} ${theme.fg("dim", `${percent}%`)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Render a single job entry for the widget.
|
|
47
|
+
* Returns: "● {id} {elapsed_or_progress}"
|
|
48
|
+
*/
|
|
49
|
+
function renderJob(job, theme) {
|
|
50
|
+
const dot = theme.fg("success", "●");
|
|
51
|
+
const cmd = theme.fg("text", job.command.join(" "));
|
|
52
|
+
if (job.avg_duration_ms > 0) {
|
|
53
|
+
const info = renderProgress(job.started_at, job.avg_duration_ms, theme);
|
|
54
|
+
return `${dot} ${cmd} ${info}`;
|
|
55
|
+
}
|
|
56
|
+
return `${dot} ${cmd}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Render the running jobs widget as a single horizontal line.
|
|
60
|
+
* Returns an empty array if no running jobs.
|
|
61
|
+
*
|
|
62
|
+
* @param jobs - Running jobs to display
|
|
63
|
+
* @param theme - Theme for styling
|
|
64
|
+
* @param width - Terminal width for truncation
|
|
65
|
+
*/
|
|
66
|
+
export function renderJobWidget(jobs, theme, width) {
|
|
67
|
+
if (jobs.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const separator = theme.fg("dim", " │ ");
|
|
71
|
+
const parts = jobs.map((job) => renderJob(job, theme));
|
|
72
|
+
const line = parts.join(separator);
|
|
73
|
+
return [truncateToWidth(line, width)];
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.js","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Widget rendering for gob running jobs.\n * Renders a single horizontal line showing running jobs.\n *\n * Layout: ● server 0:12:34 │ ● build ████░░░ 57% │ ● lint 0:00:05\n */\n\nimport type { Theme } from \"@mariozechner/pi-coding-agent\";\nimport { truncateToWidth } from \"@mariozechner/pi-tui\";\nimport type { JobResponse } from \"./types.js\";\n\n/**\n * Format elapsed time as H:MM:SS or M:SS.\n */\nfunction formatElapsed(startedAt: string): string {\n\tconst start = new Date(startedAt).getTime();\n\tconst now = Date.now();\n\tconst elapsed = Math.max(0, Math.floor((now - start) / 1000));\n\n\tconst hours = Math.floor(elapsed / 3600);\n\tconst minutes = Math.floor((elapsed % 3600) / 60);\n\tconst seconds = elapsed % 60;\n\n\tif (hours > 0) {\n\t\treturn `${hours}:${String(minutes).padStart(2, \"0\")}:${String(seconds).padStart(2, \"0\")}`;\n\t}\n\treturn `${minutes}:${String(seconds).padStart(2, \"0\")}`;\n}\n\n/**\n * Render a progress bar for a job with known average duration.\n * Returns something like \"████░░░░ 57%\" or \"████████ 2:15\" (overtime).\n */\nfunction renderProgress(startedAt: string, avgDurationMs: number, theme: Theme): string {\n\tconst start = new Date(startedAt).getTime();\n\tconst elapsedMs = Date.now() - start;\n\tconst ratio = Math.min(elapsedMs / avgDurationMs, 1);\n\tconst barWidth = 8;\n\tconst filled = Math.round(ratio * barWidth);\n\tconst empty = barWidth - filled;\n\n\tconst isOvertime = elapsedMs > avgDurationMs;\n\n\tif (isOvertime) {\n\t\t// Full bar in warning color + overtime elapsed\n\t\tconst bar = theme.fg(\"warning\", \"█\".repeat(barWidth));\n\t\tconst overtime = formatElapsed(startedAt);\n\t\treturn `${bar} ${theme.fg(\"warning\", overtime)}`;\n\t}\n\n\tconst percent = Math.round(ratio * 100);\n\tconst bar = theme.fg(\"success\", \"█\".repeat(filled)) + theme.fg(\"dim\", \"░\".repeat(empty));\n\treturn `${bar} ${theme.fg(\"dim\", `${percent}%`)}`;\n}\n\n/**\n * Render a single job entry for the widget.\n * Returns: \"● {id} {elapsed_or_progress}\"\n */\nfunction renderJob(job: JobResponse, theme: Theme): string {\n\tconst dot = theme.fg(\"success\", \"●\");\n\tconst cmd = theme.fg(\"text\", job.command.join(\" \"));\n\n\tif (job.avg_duration_ms > 0) {\n\t\tconst info = renderProgress(job.started_at, job.avg_duration_ms, theme);\n\t\treturn `${dot} ${cmd} ${info}`;\n\t}\n\n\treturn `${dot} ${cmd}`;\n}\n\n/**\n * Render the running jobs widget as a single horizontal line.\n * Returns an empty array if no running jobs.\n *\n * @param jobs - Running jobs to display\n * @param theme - Theme for styling\n * @param width - Terminal width for truncation\n */\nexport function renderJobWidget(jobs: JobResponse[], theme: Theme, width: number): string[] {\n\tif (jobs.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst separator = theme.fg(\"dim\", \" │ \");\n\tconst parts = jobs.map((job) => renderJob(job, theme));\n\tconst line = parts.join(separator);\n\n\treturn [truncateToWidth(line, width)];\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juanibiapina/pi-gob",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pi extension for managing gob background jobs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"clean": "rm -rf dist",
|
|
10
|
+
"build": "tsc -p tsconfig.build.json",
|
|
11
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
12
|
+
"check": "biome check --write --error-on-warnings . && tsc --noEmit",
|
|
13
|
+
"prepublishOnly": "npm run clean && npm run build && npm run check"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/**/*",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi",
|
|
21
|
+
"pi-package",
|
|
22
|
+
"extension",
|
|
23
|
+
"gob",
|
|
24
|
+
"background-jobs"
|
|
25
|
+
],
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": ["./dist/index.js"]
|
|
28
|
+
},
|
|
29
|
+
"author": "Juan Ibiapina",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+ssh://git@github.com/juanibiapina/pi-gob.git"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
41
|
+
"@mariozechner/pi-tui": "*"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "2.3.5",
|
|
45
|
+
"@mariozechner/pi-coding-agent": "^0.51.1",
|
|
46
|
+
"@types/node": "^22.10.5",
|
|
47
|
+
"typescript": "^5.9.2"
|
|
48
|
+
}
|
|
49
|
+
}
|