@pasko70/pibo 1.8.2 → 1.9.2
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/dist/apps/chat/data/chat-data-mappers.js +8 -0
- package/dist/apps/chat/data/project-service.js +23 -0
- package/dist/apps/chat/static-assets.js +6 -5
- package/dist/apps/chat-ui/assets/{dist--lraqdDn.js → dist-2KdPXbMT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dgz3iWay.js → dist-5GM30SQK.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CMFjl7MX.js → dist-9lsp1UpA.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-6pcjNbPQ.js → dist-BWbWIOcD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-COlFTw2x.js → dist-BYKMZlI0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-IlQbTNzw.js → dist-C-5u2QIS.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-bLd-NaVv.js → dist-Cge8JklW.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CoCvyd4f.js → dist-CjYtD7ZT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CZSEM5So.js → dist-DFhhiR8M.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-YdEosvsr.js → dist-Etxmpyxg.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CajLLpaw.js → dist-Idz5kzy8.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BqD_bm7z.js +173 -0
- package/dist/apps/chat-ui/assets/index-C0x9nEcf.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Cst9OUkC.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli.js +17 -0
- package/dist/compute/resource-health.js +12 -11
- package/dist/gateway/server.js +14 -1
- package/dist/gateway/web.js +2 -1
- package/dist/plugins/context-files.js +4 -2
- package/dist/ralph/store.js +1 -1
- package/dist/resources/cli.js +135 -0
- package/dist/resources/lifecycle.js +353 -0
- package/dist/resources/reaper-state.js +94 -0
- package/dist/resources/reaper.js +145 -0
- package/dist/session-ui/activeTurn.js +126 -0
- package/dist/session-ui/delegation.js +89 -0
- package/dist/session-ui/index.js +1 -0
- package/dist/session-ui/terminalRows.js +61 -0
- package/dist/shared/trace-engine.js +3 -2
- package/dist/shared/trace-event-projection.js +124 -14
- package/dist/shared/trace-transcript.js +73 -5
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-DbRZGRDd.css +0 -1
- package/dist/apps/chat-ui/assets/index-HPWlrJwv.js +0 -173
- package/dist/apps/chat-vscode-web/assets/index-Dge6XEYB.js +0 -41
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#101d22" />
|
|
7
7
|
<title>Pibo</title>
|
|
8
|
-
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-Cst9OUkC.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-B5QK07zO.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/dist/cli.js
CHANGED
|
@@ -115,6 +115,11 @@ export async function runPiboCli(argv = process.argv) {
|
|
|
115
115
|
await runComputeCli([argv[0] ?? "node", "pibo compute", ...argv.slice(3)]);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
+
if (argv[2] === "resources") {
|
|
119
|
+
const { runResourcesCli } = await import("./resources/cli.js");
|
|
120
|
+
await runResourcesCli([argv[0] ?? "node", "pibo resources", ...argv.slice(3)]);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
118
123
|
if (argv[2] === "setup") {
|
|
119
124
|
const { runSetupCli } = await import("./setup/cli.js");
|
|
120
125
|
await runSetupCli([argv[0] ?? "node", "pibo setup", ...argv.slice(3)]);
|
|
@@ -217,6 +222,17 @@ export async function runPiboCli(argv = process.argv) {
|
|
|
217
222
|
const { runComputeCli } = await import("./compute/cli.js");
|
|
218
223
|
await runComputeCli([argv[0] ?? "node", "pibo compute", ...args]);
|
|
219
224
|
});
|
|
225
|
+
program
|
|
226
|
+
.command("resources")
|
|
227
|
+
.description("Inspect and safely reap managed compute and browser resources")
|
|
228
|
+
.helpOption(false)
|
|
229
|
+
.allowUnknownOption(true)
|
|
230
|
+
.allowExcessArguments(true)
|
|
231
|
+
.argument("[args...]")
|
|
232
|
+
.action(async (args) => {
|
|
233
|
+
const { runResourcesCli } = await import("./resources/cli.js");
|
|
234
|
+
await runResourcesCli([argv[0] ?? "node", "pibo resources", ...args]);
|
|
235
|
+
});
|
|
220
236
|
program
|
|
221
237
|
.command("setup")
|
|
222
238
|
.description("Plan user-host installs and developer-host upgrades")
|
|
@@ -439,6 +455,7 @@ Commands:
|
|
|
439
455
|
debug Inspect local Pibo data
|
|
440
456
|
data Inspect and maintain Pibo data stores
|
|
441
457
|
compute Manage Pibo Docker compute workers
|
|
458
|
+
resources Inspect and safely reap managed compute and browser resources
|
|
442
459
|
setup Plan user-host installs and developer-host upgrades
|
|
443
460
|
skills Manage Pibo user skills
|
|
444
461
|
cron Manage scheduled Pibo jobs
|
|
@@ -4,6 +4,7 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { readResourceReaperTimerStatus } from "../resources/reaper-state.js";
|
|
7
8
|
import { createEmptyBrowserPoolState, normalizeBrowserPoolState } from "../tools/browser-pool.js";
|
|
8
9
|
import { getComputeDiskDiagnostics, listWorkers } from "./docker.js";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -13,7 +14,9 @@ export function parseProcessList(output) {
|
|
|
13
14
|
for (const line of output.split("\n")) {
|
|
14
15
|
if (!line.trim())
|
|
15
16
|
continue;
|
|
16
|
-
const
|
|
17
|
+
const withElapsed = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
|
|
18
|
+
const legacy = withElapsed ? undefined : line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
|
|
19
|
+
const match = withElapsed ?? legacy;
|
|
17
20
|
if (!match)
|
|
18
21
|
continue;
|
|
19
22
|
const pid = Number(match[1]);
|
|
@@ -21,10 +24,11 @@ export function parseProcessList(output) {
|
|
|
21
24
|
const pgid = Number(match[3]);
|
|
22
25
|
if (!Number.isInteger(pid) || !Number.isInteger(ppid) || !Number.isInteger(pgid))
|
|
23
26
|
continue;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
27
|
+
const elapsedSeconds = withElapsed ? Number(match[4]) : undefined;
|
|
28
|
+
const commandName = withElapsed ? match[5] ?? "" : match[4] ?? "";
|
|
29
|
+
const args = withElapsed ? match[6] ?? "" : match[5] ?? "";
|
|
26
30
|
const isChromium = isChromiumCommand(commandName, args);
|
|
27
|
-
processes.push({ pid, ppid, pgid, commandName, args, isChromium, isMainProcess: isChromium && !/\s--type=/.test(` ${args}`) });
|
|
31
|
+
processes.push({ pid, ppid, pgid, elapsedSeconds, commandName, args, isChromium, isMainProcess: isChromium && !/\s--type=/.test(` ${args}`) });
|
|
28
32
|
}
|
|
29
33
|
return processes;
|
|
30
34
|
}
|
|
@@ -160,6 +164,7 @@ function describeUnassignedBrowserProcess(process, workers) {
|
|
|
160
164
|
workerName: worker?.name,
|
|
161
165
|
userDataDir,
|
|
162
166
|
cdpPort,
|
|
167
|
+
elapsedSeconds: process.elapsedSeconds,
|
|
163
168
|
argsPreview: sanitizeArgsPreview(process.args),
|
|
164
169
|
nextCommands,
|
|
165
170
|
};
|
|
@@ -242,7 +247,7 @@ async function collectWorkers() {
|
|
|
242
247
|
}
|
|
243
248
|
async function collectProcesses() {
|
|
244
249
|
try {
|
|
245
|
-
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,pgid=,comm=,args="], { maxBuffer: 10 * 1024 * 1024 });
|
|
250
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,pgid=,etimes=,comm=,args="], { maxBuffer: 10 * 1024 * 1024 });
|
|
246
251
|
return { processes: parseProcessList(stdout) };
|
|
247
252
|
}
|
|
248
253
|
catch (error) {
|
|
@@ -290,13 +295,9 @@ async function findStateJsonFiles(rootDir) {
|
|
|
290
295
|
}
|
|
291
296
|
function detectReaperTimerStatus() {
|
|
292
297
|
if (process.env.PIBO_RESOURCE_REAPER_TIMER_STATUS === "configured") {
|
|
293
|
-
return { status: "configured", details: "Resource reaper timer is marked configured by PIBO_RESOURCE_REAPER_TIMER_STATUS.", nextCommands: ["pibo
|
|
298
|
+
return { status: "configured", details: "Resource reaper timer is marked configured by PIBO_RESOURCE_REAPER_TIMER_STATUS.", nextCommands: ["pibo resources status --json"] };
|
|
294
299
|
}
|
|
295
|
-
return
|
|
296
|
-
status: "missing",
|
|
297
|
-
details: "No automatic resource reaper timer status was found; keep using read-only diagnostics and dry-run cleanup until timers are explicitly enabled.",
|
|
298
|
-
nextCommands: ["pibo compute health --json", "pibo tools browser-use pool reap --json", "pibo compute reap --dry-run --json"],
|
|
299
|
-
};
|
|
300
|
+
return readResourceReaperTimerStatus();
|
|
300
301
|
}
|
|
301
302
|
function browserProcessMatchesPool(process, state) {
|
|
302
303
|
if (state.pid && process.pid === state.pid)
|
package/dist/gateway/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createServer } from "node:net";
|
|
|
2
2
|
import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
|
|
3
3
|
import { PiboSessionRouter } from "../core/session-router.js";
|
|
4
4
|
import { loadPiboModelDefaults, selectRequestedModelProfile } from "../core/model-defaults.js";
|
|
5
|
+
import { ResourceReaperService } from "../resources/reaper.js";
|
|
5
6
|
import { DEFAULT_GATEWAY_HOST, DEFAULT_GATEWAY_PORT, encodeFrame, errorResponse, isGatewayRequestFrame, isGatewaySubscribeFrame, } from "./protocol.js";
|
|
6
7
|
import { releaseFallbackGatewayPid, releaseGatewayPid, writeFallbackGatewayPid, writeGatewayPid } from "./pidfile.js";
|
|
7
8
|
const DEFAULT_MAX_BACKPRESSURE_FRAMES = 1_000;
|
|
@@ -131,6 +132,7 @@ export class PiboGatewayServer {
|
|
|
131
132
|
closedSlowClients = 0;
|
|
132
133
|
server;
|
|
133
134
|
unsubscribe;
|
|
135
|
+
resourceReaper;
|
|
134
136
|
constructor(options = {}) {
|
|
135
137
|
this.options = options;
|
|
136
138
|
this.pluginRegistry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
|
|
@@ -159,8 +161,14 @@ export class PiboGatewayServer {
|
|
|
159
161
|
if (this.options.startChannels !== false) {
|
|
160
162
|
await this.startChannels();
|
|
161
163
|
}
|
|
164
|
+
if (this.options.resourceReaper) {
|
|
165
|
+
this.resourceReaper = new ResourceReaperService(this.options.resourceReaper);
|
|
166
|
+
await this.resourceReaper.start();
|
|
167
|
+
}
|
|
162
168
|
}
|
|
163
169
|
async stop() {
|
|
170
|
+
await this.resourceReaper?.stop();
|
|
171
|
+
this.resourceReaper = undefined;
|
|
164
172
|
await this.stopChannels();
|
|
165
173
|
await this.pluginRegistry.getAuthService()?.stop?.();
|
|
166
174
|
this.unsubscribe?.();
|
|
@@ -341,6 +349,11 @@ export class PiboGatewayServer {
|
|
|
341
349
|
return this.sessionStore;
|
|
342
350
|
}
|
|
343
351
|
}
|
|
352
|
+
export function resolveGatewayResourceReaperOptions(options) {
|
|
353
|
+
if (options.resourceReaper === false || process.env.PIBO_RESOURCE_REAPER_DISABLED === "1")
|
|
354
|
+
return false;
|
|
355
|
+
return options.resourceReaper ?? {};
|
|
356
|
+
}
|
|
344
357
|
export async function runGatewayServer(options = {}) {
|
|
345
358
|
const fallbackMode = process.env.PIBO_FALLBACK_MODE === "1";
|
|
346
359
|
try {
|
|
@@ -357,7 +370,7 @@ export async function runGatewayServer(options = {}) {
|
|
|
357
370
|
const releasePid = fallbackMode ? releaseFallbackGatewayPid : releaseGatewayPid;
|
|
358
371
|
let server;
|
|
359
372
|
try {
|
|
360
|
-
server = new PiboGatewayServer(options);
|
|
373
|
+
server = new PiboGatewayServer({ ...options, resourceReaper: resolveGatewayResourceReaperOptions(options) });
|
|
361
374
|
await server.start();
|
|
362
375
|
}
|
|
363
376
|
catch (error) {
|
package/dist/gateway/web.js
CHANGED
|
@@ -12,7 +12,7 @@ import { PiboPluginRegistry } from "../plugins/registry.js";
|
|
|
12
12
|
import { createPiboWebHostPlugin } from "../plugins/web.js";
|
|
13
13
|
import { DEFAULT_WEB_CHANNEL_HOST, DEFAULT_WEB_CHANNEL_PORT } from "../web/channel.js";
|
|
14
14
|
import { loadPiboConfig } from "../config/config.js";
|
|
15
|
-
import { PiboGatewayServer } from "./server.js";
|
|
15
|
+
import { PiboGatewayServer, resolveGatewayResourceReaperOptions } from "./server.js";
|
|
16
16
|
import { releaseFallbackGatewayPid, releaseGatewayPid, writeFallbackGatewayPid, writeGatewayPid } from "./pidfile.js";
|
|
17
17
|
const PUBLIC_WEB_CHANNEL_HOST = "0.0.0.0";
|
|
18
18
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -196,6 +196,7 @@ export async function runWebGatewayServer(options = {}) {
|
|
|
196
196
|
server = new PiboGatewayServer({
|
|
197
197
|
...resolvedOptions,
|
|
198
198
|
pluginRegistry,
|
|
199
|
+
resourceReaper: resolveGatewayResourceReaperOptions(resolvedOptions),
|
|
199
200
|
});
|
|
200
201
|
await server.start();
|
|
201
202
|
}
|
|
@@ -362,12 +362,13 @@ class ContextFileService {
|
|
|
362
362
|
const markdown = normalizeMarkdown(body.markdown ?? "");
|
|
363
363
|
const scope = normalizeScope(body.scope);
|
|
364
364
|
const agentProfileName = normalizeAgentProfileName(body.agentProfileName, scope === "agent");
|
|
365
|
+
const key = uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key)));
|
|
365
366
|
const targetDir = this.resolveManagedDir(scope, agentProfileName);
|
|
366
367
|
const absolutePath = uniquePath(targetDir, managedFileName(label));
|
|
367
368
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
368
369
|
await writeFile(absolutePath, markdown, "utf8");
|
|
369
370
|
const record = this.store.createFile({
|
|
370
|
-
key
|
|
371
|
+
key,
|
|
371
372
|
label,
|
|
372
373
|
managedPath: absolutePath,
|
|
373
374
|
scope,
|
|
@@ -398,12 +399,13 @@ class ContextFileService {
|
|
|
398
399
|
const label = normalizeOptionalLabel(body.label) ?? sourceFile.label ?? sourceKey;
|
|
399
400
|
const scope = normalizeScope(body.scope, "global");
|
|
400
401
|
const agentProfileName = normalizeAgentProfileName(body.agentProfileName, scope === "agent");
|
|
402
|
+
const key = uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key)));
|
|
401
403
|
const targetDir = this.resolveManagedDir(scope, agentProfileName);
|
|
402
404
|
const absolutePath = uniquePath(targetDir, managedFileName(label));
|
|
403
405
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
404
406
|
await writeFile(absolutePath, sourceDescriptor.content ?? "", "utf8");
|
|
405
407
|
const record = this.store.createFile({
|
|
406
|
-
key
|
|
408
|
+
key,
|
|
407
409
|
label,
|
|
408
410
|
managedPath: absolutePath,
|
|
409
411
|
scope,
|
package/dist/ralph/store.js
CHANGED
|
@@ -340,7 +340,7 @@ export class PiboRalphStore {
|
|
|
340
340
|
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
341
341
|
const shouldDisable = reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
|
|
342
342
|
const state = { ...job.state, runningAt: undefined, completedIterations, lastRunAt: timestamp, lastRunId: input.runId, lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok', lastError: input.error, lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId, consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation };
|
|
343
|
-
this.db.prepare('UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = ?, reason = ?, error = ?, completed_at = ?, updated_at = ? WHERE id = ?').run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, timestamp, timestamp, input.runId);
|
|
343
|
+
this.db.prepare('UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, completed_at = ?, updated_at = ? WHERE id = ?').run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, timestamp, timestamp, input.runId);
|
|
344
344
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(shouldDisable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
|
|
345
345
|
}
|
|
346
346
|
appendRunFact(input) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getComputeResourceHealth } from "../compute/resource-health.js";
|
|
3
|
+
import { renderComputeResourceHealthText } from "../compute/cli.js";
|
|
4
|
+
import { applyResourceReapPlan, getActiveResourceLeases, planResourceReap, } from "./lifecycle.js";
|
|
5
|
+
function printJson(value) {
|
|
6
|
+
console.log(JSON.stringify(value, null, 2));
|
|
7
|
+
}
|
|
8
|
+
function parseNonNegativeNumber(value) {
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
11
|
+
throw new Error("Value must be a non-negative number");
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
function parsePidList(value) {
|
|
15
|
+
const pids = value.split(",").map((item) => Number.parseInt(item.trim(), 10));
|
|
16
|
+
if (pids.some((pid) => !Number.isInteger(pid) || pid <= 0))
|
|
17
|
+
throw new Error("PIDs must be positive integers separated by commas");
|
|
18
|
+
return [...new Set(pids)];
|
|
19
|
+
}
|
|
20
|
+
export function renderResourceLeasesText(leases) {
|
|
21
|
+
if (leases.length === 0)
|
|
22
|
+
return "No active managed browser-pool leases.\nNext: pibo resources status";
|
|
23
|
+
const lines = ["LEASE_ID\tHOLDER\tWORKER/POOL\tEXPIRY\tSTATE"];
|
|
24
|
+
for (const lease of leases) {
|
|
25
|
+
lines.push(`${lease.leaseId}\t${lease.holder ?? "-"}\t${lease.workerId}/${lease.poolId}\t${lease.expiresAt ?? "-"}\t${lease.state}`);
|
|
26
|
+
}
|
|
27
|
+
lines.push("Next: pibo resources status");
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
export function renderResourceReapText(value) {
|
|
31
|
+
const applied = "applied" in value;
|
|
32
|
+
const plan = applied ? value.plan : value;
|
|
33
|
+
const lines = [
|
|
34
|
+
`Resource reap ${applied ? "apply" : "dry-run"}: ${plan.browserPools.selected} browser pool(s), ${plan.unmanagedBrowsers.selected} unmanaged browser process group(s), ${plan.staleFiles.selected} stale pid/port file(s), ${plan.compute.summary.selected} compute worker(s) selected`,
|
|
35
|
+
"BROWSER_ACTION\tWORKER/POOL OR PID/PGID\tREASON",
|
|
36
|
+
];
|
|
37
|
+
for (const item of plan.browserPools.items)
|
|
38
|
+
lines.push(`${item.action}\t${item.workerId}/${item.poolId}\t${item.reason}`);
|
|
39
|
+
for (const item of plan.unmanagedBrowsers.items)
|
|
40
|
+
lines.push(`${item.action}\t${item.pid}/${item.processGroupId}\t${item.reason}`);
|
|
41
|
+
if (applied) {
|
|
42
|
+
lines.push(`Reaped browser pools: ${value.browserResults.filter((result) => result.reaped).length}`);
|
|
43
|
+
lines.push(`Terminated unmanaged browser process groups: ${value.terminatedUnmanagedBrowsers.join(", ") || "none"}`);
|
|
44
|
+
lines.push(`Removed stale pid/port files: ${value.removedStaleFiles.length}`);
|
|
45
|
+
lines.push(`Removed compute workers: ${value.removedComputeWorkers.join(", ") || "none"}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const args = [
|
|
49
|
+
"pibo resources reap --apply",
|
|
50
|
+
`--max-age-minutes ${plan.options.maxAgeMinutes}`,
|
|
51
|
+
`--idle-timeout-minutes ${plan.options.idleTimeoutMinutes}`,
|
|
52
|
+
`--unmanaged-browser-grace-minutes ${plan.options.unmanagedBrowserGraceMinutes}`,
|
|
53
|
+
];
|
|
54
|
+
if (plan.options.includeDev)
|
|
55
|
+
args.push("--include-dev");
|
|
56
|
+
if (plan.options.browserPoolRoot)
|
|
57
|
+
args.push(`--browser-pool-root ${plan.options.browserPoolRoot}`);
|
|
58
|
+
if (plan.options.browserUseHome)
|
|
59
|
+
args.push(`--browser-use-home ${plan.options.browserUseHome}`);
|
|
60
|
+
lines.push(`Dry-run only. Apply after review with: ${args.join(" ")}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push("Worktrees are always preserved.");
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
export function serializeResourceStatus(health) {
|
|
66
|
+
return {
|
|
67
|
+
...health,
|
|
68
|
+
nextCommands: [...new Set([
|
|
69
|
+
"pibo resources status --json",
|
|
70
|
+
"pibo resources leases --json",
|
|
71
|
+
"pibo resources reap --dry-run --json",
|
|
72
|
+
...health.nextCommands,
|
|
73
|
+
])],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function runResourcesCli(argv) {
|
|
77
|
+
const program = new Command();
|
|
78
|
+
program
|
|
79
|
+
.name("pibo resources")
|
|
80
|
+
.description("Inspect and safely reap managed compute and browser resources")
|
|
81
|
+
.helpOption("-h, --help", "Show help")
|
|
82
|
+
.showHelpAfterError()
|
|
83
|
+
.helpCommand("help [command]", "Show help for command")
|
|
84
|
+
.addHelpText("after", "\nNext:\n pibo resources status\n pibo resources leases\n pibo resources reap --help\n");
|
|
85
|
+
program
|
|
86
|
+
.command("status")
|
|
87
|
+
.alias("doctor")
|
|
88
|
+
.description("Show read-only aggregate compute and browser health")
|
|
89
|
+
.option("--browser-pool-root <path>", "Browser pool root directory to scan")
|
|
90
|
+
.option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
|
|
91
|
+
.option("--json", "Print machine-readable resource health")
|
|
92
|
+
.action(async (options) => {
|
|
93
|
+
const health = serializeResourceStatus(await getComputeResourceHealth(options));
|
|
94
|
+
if (options.json)
|
|
95
|
+
printJson(health);
|
|
96
|
+
else
|
|
97
|
+
console.log(renderComputeResourceHealthText(health).replace(/^Compute resource health:/, "Resource status:"));
|
|
98
|
+
});
|
|
99
|
+
program
|
|
100
|
+
.command("leases")
|
|
101
|
+
.description("List active managed browser-pool leases")
|
|
102
|
+
.option("--browser-pool-root <path>", "Browser pool root directory to scan")
|
|
103
|
+
.option("--json", "Print machine-readable leases")
|
|
104
|
+
.action(async (options) => {
|
|
105
|
+
const leases = await getActiveResourceLeases(options.browserPoolRoot);
|
|
106
|
+
if (options.json)
|
|
107
|
+
printJson({ leases });
|
|
108
|
+
else
|
|
109
|
+
console.log(renderResourceLeasesText(leases));
|
|
110
|
+
});
|
|
111
|
+
program
|
|
112
|
+
.command("reap")
|
|
113
|
+
.description("Preview or apply aggregate browser and compute cleanup")
|
|
114
|
+
.option("--dry-run", "Preview cleanup without changing resources (default)")
|
|
115
|
+
.option("--apply", "Apply cleanup after rechecking current resource safety")
|
|
116
|
+
.option("--include-dev", "Also select eligible dev compute workers")
|
|
117
|
+
.option("--max-age-minutes <n>", "Select compute workers older than this many minutes", parseNonNegativeNumber, 60)
|
|
118
|
+
.option("--idle-timeout-minutes <n>", "Select browser pools idle for this many minutes", parseNonNegativeNumber, 10)
|
|
119
|
+
.option("--unmanaged-browser-grace-minutes <n>", "Select unmanaged Chromium older than this many minutes", parseNonNegativeNumber, 10)
|
|
120
|
+
.option("--exempt-browser-pids <list>", "Comma-separated browser PIDs or process groups to preserve", parsePidList)
|
|
121
|
+
.option("--browser-pool-root <path>", "Browser pool root directory to scan")
|
|
122
|
+
.option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
|
|
123
|
+
.option("--json", "Print machine-readable cleanup plan or result")
|
|
124
|
+
.action(async (options) => {
|
|
125
|
+
if (options.apply && options.dryRun)
|
|
126
|
+
throw new Error("Use either --apply or --dry-run, not both");
|
|
127
|
+
const plan = await planResourceReap(options);
|
|
128
|
+
const result = options.apply ? await applyResourceReapPlan(plan) : plan;
|
|
129
|
+
if (options.json)
|
|
130
|
+
printJson(options.apply ? result : { applied: false, dryRun: true, plan: result });
|
|
131
|
+
else
|
|
132
|
+
console.log(renderResourceReapText(result));
|
|
133
|
+
});
|
|
134
|
+
await program.parseAsync(argv);
|
|
135
|
+
}
|