@pasko70/pibo 1.8.1 → 1.9.1
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-CoCvyd4f.js → dist-8oSZX4UV.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-6pcjNbPQ.js → dist-B1Fsqkt8.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-COlFTw2x.js → dist-BYeVKSuc.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CajLLpaw.js → dist-BZS06o95.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CMFjl7MX.js → dist-BZwDovWR.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-YdEosvsr.js → dist-BsxshSq9.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-IlQbTNzw.js → dist-CWGTu0U4.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CZSEM5So.js → dist-DE9BRlB6.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist--lraqdDn.js → dist-NWq0Jh7F.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-bLd-NaVv.js → dist-j4CZc7Hs.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dgz3iWay.js → dist-z23GNStM.js} +1 -1
- package/dist/apps/chat-ui/assets/index-C0x9nEcf.css +1 -0
- package/dist/apps/chat-ui/assets/index-Di8T05_5.js +173 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-ujYozhmx.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/core/provider-telemetry.js +40 -2
- package/dist/core/runtime-telemetry.js +84 -46
- package/dist/core/session-router.js +52 -17
- package/dist/data/telemetry-writer.js +114 -0
- package/dist/data/telemetry.js +14 -0
- 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/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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 25;
|
|
2
|
+
const DEFAULT_MAX_PENDING_OPERATIONS = 1_024;
|
|
3
|
+
/**
|
|
4
|
+
* Gateway-scoped, ordered telemetry writer.
|
|
5
|
+
*
|
|
6
|
+
* Normal writes are deferred briefly so telemetry from multiple routed sessions
|
|
7
|
+
* shares one SQLite transaction. The queue never drops lifecycle events: when
|
|
8
|
+
* the hard bound is reached, it drains immediately in the caller instead.
|
|
9
|
+
*/
|
|
10
|
+
export class AsyncTelemetryWriter {
|
|
11
|
+
store;
|
|
12
|
+
options;
|
|
13
|
+
flushIntervalMs;
|
|
14
|
+
maxPendingOperations;
|
|
15
|
+
pending = [];
|
|
16
|
+
flushTimer;
|
|
17
|
+
flushing = false;
|
|
18
|
+
closed = false;
|
|
19
|
+
constructor(store, options = {}) {
|
|
20
|
+
this.store = store;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.flushIntervalMs = nonNegativeFinite(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
|
|
23
|
+
this.maxPendingOperations = positiveInteger(options.maxPendingOperations, DEFAULT_MAX_PENDING_OPERATIONS);
|
|
24
|
+
}
|
|
25
|
+
enqueue(write, onError) {
|
|
26
|
+
if (this.closed) {
|
|
27
|
+
this.reportError(new Error("Telemetry writer is closed."), onError);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
this.pending.push({ write, onError });
|
|
31
|
+
if (this.pending.length >= this.maxPendingOperations) {
|
|
32
|
+
this.flushNow();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.scheduleFlush();
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
async flush() {
|
|
40
|
+
this.flushNow();
|
|
41
|
+
}
|
|
42
|
+
async dispose() {
|
|
43
|
+
if (this.closed)
|
|
44
|
+
return;
|
|
45
|
+
this.flushNow();
|
|
46
|
+
this.closed = true;
|
|
47
|
+
}
|
|
48
|
+
scheduleFlush() {
|
|
49
|
+
if (this.flushTimer)
|
|
50
|
+
return;
|
|
51
|
+
this.flushTimer = setTimeout(() => {
|
|
52
|
+
this.flushTimer = undefined;
|
|
53
|
+
this.flushNow();
|
|
54
|
+
}, this.flushIntervalMs);
|
|
55
|
+
this.flushTimer.unref();
|
|
56
|
+
}
|
|
57
|
+
flushNow() {
|
|
58
|
+
if (this.flushing)
|
|
59
|
+
return;
|
|
60
|
+
if (this.flushTimer)
|
|
61
|
+
clearTimeout(this.flushTimer);
|
|
62
|
+
this.flushTimer = undefined;
|
|
63
|
+
this.flushing = true;
|
|
64
|
+
try {
|
|
65
|
+
while (this.pending.length > 0) {
|
|
66
|
+
const batch = this.pending;
|
|
67
|
+
this.pending = [];
|
|
68
|
+
try {
|
|
69
|
+
this.store.transaction(() => {
|
|
70
|
+
for (const operation of batch) {
|
|
71
|
+
try {
|
|
72
|
+
operation.write();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.reportError(error, operation.onError);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
for (const operation of batch)
|
|
82
|
+
this.reportError(error, operation.onError);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.flushing = false;
|
|
88
|
+
if (!this.closed && this.pending.length > 0)
|
|
89
|
+
this.scheduleFlush();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
reportError(error, operationHandler) {
|
|
93
|
+
try {
|
|
94
|
+
operationHandler?.(error);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Telemetry error reporting must not affect runtime work.
|
|
98
|
+
}
|
|
99
|
+
if (operationHandler === this.options.onError)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
this.options.onError?.(error);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Telemetry error reporting must not affect runtime work.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function nonNegativeFinite(value, fallback) {
|
|
110
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
111
|
+
}
|
|
112
|
+
function positiveInteger(value, fallback) {
|
|
113
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
114
|
+
}
|
package/dist/data/telemetry.js
CHANGED
|
@@ -8,6 +8,20 @@ export class TelemetryStore {
|
|
|
8
8
|
constructor(db) {
|
|
9
9
|
this.db = db;
|
|
10
10
|
}
|
|
11
|
+
transaction(action) {
|
|
12
|
+
if (this.db.isTransaction)
|
|
13
|
+
return action();
|
|
14
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
15
|
+
try {
|
|
16
|
+
const result = action();
|
|
17
|
+
this.db.exec("COMMIT");
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
this.db.exec("ROLLBACK");
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
11
25
|
listSessions(input = {}) {
|
|
12
26
|
return listTelemetrySessions(this.db, input);
|
|
13
27
|
}
|
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
|
+
}
|