@getpaseo/server 0.1.99 → 0.1.101
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/server/executable-resolution/windows.js +3 -0
- package/dist/server/server/agent/agent-manager.d.ts +10 -0
- package/dist/server/server/agent/agent-manager.js +65 -27
- package/dist/server/server/agent/agent-sdk-types.d.ts +8 -0
- package/dist/server/server/agent/mcp-server.d.ts +2 -45
- package/dist/server/server/agent/mcp-server.js +45 -1985
- package/dist/server/server/agent/prompt-attachments.js +6 -2
- package/dist/server/server/agent/provider-registry.js +1 -0
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +4 -0
- package/dist/server/server/agent/provider-snapshot-manager.js +58 -13
- package/dist/server/server/agent/providers/acp-agent.d.ts +39 -2
- package/dist/server/server/agent/providers/acp-agent.js +281 -20
- package/dist/server/server/agent/providers/claude/agent.js +96 -62
- package/dist/server/server/agent/providers/codex-app-server-agent.js +6 -57
- package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +2 -1
- package/dist/server/server/agent/providers/copilot-acp-agent.js +10 -0
- package/dist/server/server/agent/providers/diagnostic-utils.d.ts +1 -0
- package/dist/server/server/agent/providers/diagnostic-utils.js +1 -1
- package/dist/server/server/agent/providers/generic-acp-agent.d.ts +3 -0
- package/dist/server/server/agent/providers/generic-acp-agent.js +41 -23
- package/dist/server/server/agent/providers/mock-load-test-agent.js +4 -2
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts +14 -11
- package/dist/server/server/agent/providers/opencode/server-manager.js +149 -91
- package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +6 -5
- package/dist/server/server/agent/providers/opencode/test-server-manager.js +13 -3
- package/dist/server/server/agent/providers/opencode/test-utils/{test-opencode-runtime.d.ts → test-opencode-harness.d.ts} +11 -11
- package/dist/server/server/agent/providers/opencode/test-utils/{test-opencode-runtime.js → test-opencode-harness.js} +23 -10
- package/dist/server/server/agent/providers/opencode-agent.d.ts +9 -3
- package/dist/server/server/agent/providers/opencode-agent.js +26 -38
- package/dist/server/server/agent/providers/pi/agent.d.ts +4 -2
- package/dist/server/server/agent/providers/pi/agent.js +8 -3
- package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +3 -0
- package/dist/server/server/agent/providers/pi/cli-runtime.js +6 -3
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +2 -1
- package/dist/server/server/agent/providers/provider-image-output.d.ts +5 -0
- package/dist/server/server/agent/providers/provider-image-output.js +55 -0
- package/dist/server/server/agent/tools/paseo-tools.d.ts +48 -0
- package/dist/server/server/agent/tools/paseo-tools.js +2121 -0
- package/dist/server/server/agent/tools/types.d.ts +36 -0
- package/dist/server/server/agent/tools/types.js +2 -0
- package/dist/server/server/bootstrap.js +71 -62
- package/dist/server/server/persisted-config.d.ts +5 -0
- package/dist/server/server/persisted-config.js +10 -2
- package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +59 -0
- package/dist/server/server/session/agent-updates/agent-updates-service.js +220 -0
- package/dist/server/server/session/checkout/checkout-session.d.ts +13 -15
- package/dist/server/server/session/checkout/checkout-session.js +18 -16
- package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
- package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
- package/dist/server/server/session/daemon/daemon-session.d.ts +14 -0
- package/dist/server/server/session/daemon/daemon-session.js +38 -0
- package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
- package/dist/server/server/session/daemon/diagnostics.js +421 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
- package/dist/server/server/session.d.ts +7 -51
- package/dist/server/server/session.js +113 -938
- package/dist/server/server/speech/providers/openai/config.d.ts +1 -2
- package/dist/server/server/speech/providers/openai/config.js +13 -9
- package/dist/server/server/speech/providers/openai/runtime.js +2 -16
- package/dist/server/server/speech/providers/openai/stt.d.ts +1 -0
- package/dist/server/server/speech/providers/openai/stt.js +4 -2
- package/dist/server/server/speech/providers/openai/tts.d.ts +1 -0
- package/dist/server/server/speech/providers/openai/tts.js +1 -0
- package/dist/server/server/websocket/runtime-metrics.d.ts +20 -0
- package/dist/server/server/websocket-server.d.ts +1 -2
- package/dist/server/server/websocket-server.js +26 -21
- package/dist/server/server/worktree-bootstrap.d.ts +1 -1
- package/dist/server/server/worktree-branch-name-generator.js +3 -1
- package/dist/server/utils/checkout-git.js +51 -26
- package/dist/src/executable-resolution/windows.js +3 -0
- package/dist/src/server/persisted-config.js +10 -2
- package/package.json +5 -5
- package/dist/server/server/agent/providers/opencode/runtime.d.ts +0 -28
- package/dist/server/server/agent/providers/opencode/runtime.js +0 -5
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts +0 -42
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.js +0 -168
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { open, statfs } from "node:fs/promises";
|
|
2
|
+
import { cpus, freemem, loadavg, platform, release, totalmem, type } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execCommand } from "../../../utils/spawn.js";
|
|
5
|
+
const TOOL_TIMEOUT_MS = 3000;
|
|
6
|
+
const TOOL_OUTPUT_LIMIT = 512;
|
|
7
|
+
const LOG_TAIL_LINES = 80;
|
|
8
|
+
const LOG_TAIL_MAX_BYTES = 64 * 1024;
|
|
9
|
+
export async function collectDaemonDiagnostics(options) {
|
|
10
|
+
const sections = [
|
|
11
|
+
formatSection("Paseo diagnostics", [
|
|
12
|
+
{ label: "Collected at", value: new Date().toISOString() },
|
|
13
|
+
{ label: "Server ID", value: options.serverId ?? "unknown" },
|
|
14
|
+
{ label: "Daemon version", value: options.daemonVersion ?? "unknown" },
|
|
15
|
+
]),
|
|
16
|
+
];
|
|
17
|
+
sections.push(await safeSection("Daemon process", () => collectProcessEntries(options), options.logger));
|
|
18
|
+
sections.push(await safeSection("Runtime config", () => collectRuntimeConfigEntries(options), options.logger));
|
|
19
|
+
sections.push(await safeSection("System", collectSystemEntries, options.logger));
|
|
20
|
+
sections.push(await safeSection("Disk", () => collectDiskEntries(options), options.logger));
|
|
21
|
+
sections.push(await safeSection("Agents", () => collectAgentEntries(options), options.logger));
|
|
22
|
+
sections.push(await safeSection("Workspaces", () => collectWorkspaceEntries(options), options.logger));
|
|
23
|
+
sections.push(await safeSection("Providers", () => collectProviderEntries(options), options.logger));
|
|
24
|
+
sections.push(await safeSection("WebSocket runtime metrics", () => collectWebSocketRuntimeEntries(options), options.logger));
|
|
25
|
+
sections.push(await safeSection("Tools", collectToolEntries, options.logger));
|
|
26
|
+
sections.push(await safeLogTailSection(options));
|
|
27
|
+
return redactDiagnostic(sections.filter(Boolean).join("\n\n"), options);
|
|
28
|
+
}
|
|
29
|
+
async function safeSection(title, collect, logger) {
|
|
30
|
+
try {
|
|
31
|
+
return formatSection(title, await collect());
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger.debug({ err: error, title }, "diagnostic section failed");
|
|
35
|
+
return formatSection(title, [{ label: "Error", value: toErrorMessage(error) }]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function formatSection(title, entries) {
|
|
39
|
+
return [title, ...entries.map((entry) => ` ${entry.label}: ${entry.value}`)].join("\n");
|
|
40
|
+
}
|
|
41
|
+
function collectProcessEntries(options) {
|
|
42
|
+
const memory = process.memoryUsage();
|
|
43
|
+
return [
|
|
44
|
+
{ label: "PID", value: String(process.pid) },
|
|
45
|
+
{ label: "Node", value: process.version },
|
|
46
|
+
{ label: "Node path", value: process.execPath },
|
|
47
|
+
{ label: "PATH", value: getEnvValue("PATH", "Path") ?? "unset" },
|
|
48
|
+
{ label: "Shell", value: formatDaemonShell() },
|
|
49
|
+
{ label: "Uptime", value: formatDurationMs(process.uptime() * 1000) },
|
|
50
|
+
{ label: "Paseo home", value: options.paseoHome },
|
|
51
|
+
{ label: "RSS", value: formatBytes(memory.rss) },
|
|
52
|
+
{
|
|
53
|
+
label: "Heap used",
|
|
54
|
+
value: `${formatBytes(memory.heapUsed)} / ${formatBytes(memory.heapTotal)}`,
|
|
55
|
+
},
|
|
56
|
+
{ label: "External", value: formatBytes(memory.external) },
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function collectRuntimeConfigEntries(options) {
|
|
60
|
+
const relay = options.daemonRuntimeConfig?.relay ?? null;
|
|
61
|
+
return [
|
|
62
|
+
{ label: "Listen", value: formatListenKind(options.daemonRuntimeConfig?.listen ?? null) },
|
|
63
|
+
{ label: "Relay enabled", value: relay ? String(relay.enabled) : "false" },
|
|
64
|
+
{ label: "Relay endpoint configured", value: relay?.endpoint ? "true" : "false" },
|
|
65
|
+
{ label: "Relay public endpoint configured", value: relay?.publicEndpoint ? "true" : "false" },
|
|
66
|
+
{ label: "Relay TLS", value: relay ? String(relay.useTls) : "n/a" },
|
|
67
|
+
{ label: "Relay public TLS", value: relay ? String(relay.publicUseTls) : "n/a" },
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function collectSystemEntries() {
|
|
71
|
+
const loads = loadavg();
|
|
72
|
+
return [
|
|
73
|
+
{ label: "OS", value: `${type()} ${release()}` },
|
|
74
|
+
{ label: "Platform", value: `${platform()} ${process.arch}` },
|
|
75
|
+
{ label: "CPU cores", value: String(cpus().length) },
|
|
76
|
+
{ label: "Load avg", value: loads.map((value) => value.toFixed(2)).join(", ") },
|
|
77
|
+
{ label: "Memory free", value: `${formatBytes(freemem())} / ${formatBytes(totalmem())}` },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
async function collectDiskEntries(options) {
|
|
81
|
+
const stats = await statfs(options.paseoHome);
|
|
82
|
+
const freeBytes = stats.bavail * stats.bsize;
|
|
83
|
+
const totalBytes = stats.blocks * stats.bsize;
|
|
84
|
+
return [
|
|
85
|
+
{ label: "Path", value: options.paseoHome },
|
|
86
|
+
{ label: "Free", value: `${formatBytes(freeBytes)} / ${formatBytes(totalBytes)}` },
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
function collectAgentEntries(options) {
|
|
90
|
+
const agents = options.listAgents();
|
|
91
|
+
return [
|
|
92
|
+
{ label: "Total", value: String(agents.length) },
|
|
93
|
+
{ label: "By provider", value: formatCountMap(countBy(agents, (agent) => agent.provider)) },
|
|
94
|
+
{ label: "By lifecycle", value: formatCountMap(countBy(agents, (agent) => agent.lifecycle)) },
|
|
95
|
+
{
|
|
96
|
+
label: "Pending permissions",
|
|
97
|
+
value: String(agents.reduce((total, agent) => total + (agent.pendingPermissions?.size ?? 0), 0)),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
async function collectWorkspaceEntries(options) {
|
|
102
|
+
const [projects, workspaces] = await Promise.all([
|
|
103
|
+
options.listProjects(),
|
|
104
|
+
options.listWorkspaces(),
|
|
105
|
+
]);
|
|
106
|
+
const activeProjects = projects.filter((project) => !project.archivedAt);
|
|
107
|
+
const activeWorkspaces = workspaces.filter((workspace) => !workspace.archivedAt);
|
|
108
|
+
return [
|
|
109
|
+
{ label: "Projects", value: `${activeProjects.length} active / ${projects.length} total` },
|
|
110
|
+
{
|
|
111
|
+
label: "Workspaces",
|
|
112
|
+
value: `${activeWorkspaces.length} active / ${workspaces.length} total`,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: "Workspaces by kind",
|
|
116
|
+
value: formatCountMap(countBy(activeWorkspaces, (workspace) => workspace.kind)),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
async function collectProviderEntries(options) {
|
|
121
|
+
const providers = await options.listProviderAvailability();
|
|
122
|
+
return [
|
|
123
|
+
{ label: "Total", value: String(providers.length) },
|
|
124
|
+
{
|
|
125
|
+
label: "Available",
|
|
126
|
+
value: String(providers.filter((provider) => provider.available).length),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: "Unavailable",
|
|
130
|
+
value: providers
|
|
131
|
+
.filter((provider) => !provider.available)
|
|
132
|
+
.map((provider) => provider.error ? `${provider.provider} (${provider.error})` : provider.provider)
|
|
133
|
+
.join(", ") || "none",
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
async function collectToolEntries() {
|
|
138
|
+
const [git, gh] = await Promise.all([
|
|
139
|
+
checkTool("git", ["--version"]),
|
|
140
|
+
checkTool("gh", ["--version"]),
|
|
141
|
+
]);
|
|
142
|
+
return [
|
|
143
|
+
{ label: "git", value: git },
|
|
144
|
+
{ label: "gh", value: gh },
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
function collectWebSocketRuntimeEntries(options) {
|
|
148
|
+
const snapshot = options.getWebSocketRuntimeMetrics();
|
|
149
|
+
if (!snapshot) {
|
|
150
|
+
return [{ label: "Status", value: "no runtime metrics window has been flushed yet" }];
|
|
151
|
+
}
|
|
152
|
+
const runtime = snapshot.runtime;
|
|
153
|
+
const agents = snapshot.agents;
|
|
154
|
+
return [
|
|
155
|
+
{ label: "Collected at", value: snapshot.collectedAt },
|
|
156
|
+
{ label: "Window", value: formatDurationMs(snapshot.windowMs) },
|
|
157
|
+
{ label: "Final", value: String(snapshot.final) },
|
|
158
|
+
{
|
|
159
|
+
label: "Sessions",
|
|
160
|
+
value: [
|
|
161
|
+
`active=${snapshot.sessions.activeConnections}`,
|
|
162
|
+
`externalKeys=${snapshot.sessions.externalSessionKeys}`,
|
|
163
|
+
`reconnectGrace=${snapshot.sessions.reconnectGraceSessions}`,
|
|
164
|
+
].join(", "),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: "Sockets",
|
|
168
|
+
value: [
|
|
169
|
+
`active=${snapshot.sockets.activeSockets}`,
|
|
170
|
+
`pending=${snapshot.sockets.pendingConnections}`,
|
|
171
|
+
].join(", "),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
label: "Runtime requests",
|
|
175
|
+
value: [
|
|
176
|
+
`inflight=${formatNumberMetric(runtime.inflightRequests)}`,
|
|
177
|
+
`peakInflight=${formatNumberMetric(runtime.peakInflightRequests)}`,
|
|
178
|
+
].join(", "),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
label: "Terminal subscriptions",
|
|
182
|
+
value: [
|
|
183
|
+
`terminals=${formatNumberMetric(runtime.terminalSubscriptionCount)}`,
|
|
184
|
+
`directories=${formatNumberMetric(runtime.terminalDirectorySubscriptionCount)}`,
|
|
185
|
+
].join(", "),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
label: "Checkout diff",
|
|
189
|
+
value: [
|
|
190
|
+
`targets=${formatNumberMetric(runtime.checkoutDiffTargetCount)}`,
|
|
191
|
+
`subscriptions=${formatNumberMetric(runtime.checkoutDiffSubscriptionCount)}`,
|
|
192
|
+
`watchers=${formatNumberMetric(runtime.checkoutDiffWatcherCount)}`,
|
|
193
|
+
`fallbackRefreshTargets=${formatNumberMetric(runtime.checkoutDiffFallbackRefreshTargetCount)}`,
|
|
194
|
+
].join(", "),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: "Buffered amount",
|
|
198
|
+
value: `p95=${formatBytes(snapshot.bufferedAmount.p95)}, max=${formatBytes(snapshot.bufferedAmount.max)}`,
|
|
199
|
+
},
|
|
200
|
+
{ label: "Event loop delay", value: formatEventLoopDelay(snapshot.eventLoopDelay) },
|
|
201
|
+
{ label: "Latency", value: formatLatencyStats(snapshot.latency) },
|
|
202
|
+
{ label: "Inbound messages", value: formatTopCounts(snapshot.inboundMessageTypesTop) },
|
|
203
|
+
{
|
|
204
|
+
label: "Inbound session requests",
|
|
205
|
+
value: formatTopCounts(snapshot.inboundSessionRequestTypesTop),
|
|
206
|
+
},
|
|
207
|
+
{ label: "Outbound messages", value: formatTopCounts(snapshot.outboundMessageTypesTop) },
|
|
208
|
+
{
|
|
209
|
+
label: "Outbound session messages",
|
|
210
|
+
value: formatTopCounts(snapshot.outboundSessionMessageTypesTop),
|
|
211
|
+
},
|
|
212
|
+
{ label: "Agent streams", value: formatTopCounts(snapshot.outboundAgentStreamTypesTop) },
|
|
213
|
+
{ label: "Agent stream agents", value: formatTopCounts(snapshot.outboundAgentStreamAgentsTop) },
|
|
214
|
+
{ label: "Binary frames", value: formatTopCounts(snapshot.outboundBinaryFrameTypesTop) },
|
|
215
|
+
{ label: "Counters", value: formatNonZeroNumberRecord(snapshot.counters) },
|
|
216
|
+
{
|
|
217
|
+
label: "Agent metrics",
|
|
218
|
+
value: [
|
|
219
|
+
`total=${formatNumberMetric(agents.total)}`,
|
|
220
|
+
`activeForegroundTurns=${formatNumberMetric(agents.withActiveForegroundTurn)}`,
|
|
221
|
+
].join(", "),
|
|
222
|
+
},
|
|
223
|
+
{ label: "Agent lifecycle", value: formatNumberRecord(agents.byLifecycle) },
|
|
224
|
+
{
|
|
225
|
+
label: "Agent timelines",
|
|
226
|
+
value: [
|
|
227
|
+
`items=${formatNumberMetric(agents.timelineStats?.totalItems)}`,
|
|
228
|
+
`maxPerAgent=${formatNumberMetric(agents.timelineStats?.maxItemsPerAgent)}`,
|
|
229
|
+
].join(", "),
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
function formatTopCounts(counts) {
|
|
234
|
+
if (counts.length === 0)
|
|
235
|
+
return "none";
|
|
236
|
+
return counts.map(([key, count]) => `${key}=${count}`).join(", ");
|
|
237
|
+
}
|
|
238
|
+
function formatLatencyStats(stats) {
|
|
239
|
+
if (stats.length === 0)
|
|
240
|
+
return "none";
|
|
241
|
+
return stats
|
|
242
|
+
.map((stat) => `${stat.type} count=${stat.count} p50=${formatMilliseconds(stat.p50Ms)} max=${formatMilliseconds(stat.maxMs)} total=${formatMilliseconds(stat.totalMs)}`)
|
|
243
|
+
.join("; ");
|
|
244
|
+
}
|
|
245
|
+
function formatEventLoopDelay(stats) {
|
|
246
|
+
if (!stats)
|
|
247
|
+
return "unavailable";
|
|
248
|
+
return `p50=${formatMilliseconds(stats.p50Ms)}, p99=${formatMilliseconds(stats.p99Ms)}, max=${formatMilliseconds(stats.maxMs)}`;
|
|
249
|
+
}
|
|
250
|
+
function formatNumberRecord(record) {
|
|
251
|
+
if (!record)
|
|
252
|
+
return "unknown";
|
|
253
|
+
const entries = Object.entries(record).filter(([, value]) => Number.isFinite(value));
|
|
254
|
+
if (entries.length === 0)
|
|
255
|
+
return "none";
|
|
256
|
+
return entries
|
|
257
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
258
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
259
|
+
.join(", ");
|
|
260
|
+
}
|
|
261
|
+
function formatNonZeroNumberRecord(record) {
|
|
262
|
+
const entries = Object.entries(record).filter(([, value]) => Number.isFinite(value) && value !== 0);
|
|
263
|
+
if (entries.length === 0)
|
|
264
|
+
return "none";
|
|
265
|
+
return entries
|
|
266
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
267
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
268
|
+
.join(", ");
|
|
269
|
+
}
|
|
270
|
+
function formatNumberMetric(value) {
|
|
271
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
272
|
+
return "unknown";
|
|
273
|
+
return String(value);
|
|
274
|
+
}
|
|
275
|
+
function formatMilliseconds(ms) {
|
|
276
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
277
|
+
return "unknown";
|
|
278
|
+
return `${Math.round(ms)}ms`;
|
|
279
|
+
}
|
|
280
|
+
async function checkTool(command, args) {
|
|
281
|
+
try {
|
|
282
|
+
const result = await execCommand(command, args, {
|
|
283
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
284
|
+
maxBuffer: TOOL_OUTPUT_LIMIT * 2,
|
|
285
|
+
});
|
|
286
|
+
const output = truncateForDiagnostic((result.stdout || result.stderr).trim(), TOOL_OUTPUT_LIMIT);
|
|
287
|
+
return output || "ok";
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return `error: ${truncateForDiagnostic(toErrorMessage(error), TOOL_OUTPUT_LIMIT)}`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function safeLogTailSection(options) {
|
|
294
|
+
const logPath = path.join(options.paseoHome, "daemon.log");
|
|
295
|
+
try {
|
|
296
|
+
const tail = await tailFile(logPath, LOG_TAIL_LINES, LOG_TAIL_MAX_BYTES);
|
|
297
|
+
return ["Daemon log tail", ` Path: ${logPath}`, tail ? tail : " No log lines found"].join("\n");
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
options.logger.debug({ err: error, logPath }, "diagnostic log tail failed");
|
|
301
|
+
return ["Daemon log tail", ` Path: ${logPath}`, ` Error: ${toErrorMessage(error)}`].join("\n");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function tailFile(filePath, lines, maxBytes) {
|
|
305
|
+
const handle = await open(filePath, "r");
|
|
306
|
+
try {
|
|
307
|
+
const stats = await handle.stat();
|
|
308
|
+
const length = Math.min(stats.size, maxBytes);
|
|
309
|
+
const buffer = Buffer.alloc(length);
|
|
310
|
+
await handle.read(buffer, 0, length, stats.size - length);
|
|
311
|
+
return buffer
|
|
312
|
+
.toString("utf8")
|
|
313
|
+
.split("\n")
|
|
314
|
+
.filter(Boolean)
|
|
315
|
+
.slice(-lines)
|
|
316
|
+
.map((line) => ` ${line}`)
|
|
317
|
+
.join("\n");
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
await handle.close();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function formatListenKind(listen) {
|
|
324
|
+
if (!listen)
|
|
325
|
+
return "not configured";
|
|
326
|
+
if (listen.startsWith("unix://") || listen.startsWith("/"))
|
|
327
|
+
return "local socket";
|
|
328
|
+
if (listen.startsWith("pipe://") || listen.startsWith("\\\\.\\pipe\\"))
|
|
329
|
+
return "local pipe";
|
|
330
|
+
return "direct TCP";
|
|
331
|
+
}
|
|
332
|
+
function countBy(items, getKey) {
|
|
333
|
+
const counts = new Map();
|
|
334
|
+
for (const item of items) {
|
|
335
|
+
const key = getKey(item) || "unknown";
|
|
336
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
337
|
+
}
|
|
338
|
+
return counts;
|
|
339
|
+
}
|
|
340
|
+
function formatCountMap(counts) {
|
|
341
|
+
if (counts.size === 0)
|
|
342
|
+
return "none";
|
|
343
|
+
return [...counts.entries()]
|
|
344
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
345
|
+
.map(([key, count]) => `${key}=${count}`)
|
|
346
|
+
.join(", ");
|
|
347
|
+
}
|
|
348
|
+
function formatDurationMs(ms) {
|
|
349
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
350
|
+
const hours = Math.floor(seconds / 3600);
|
|
351
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
352
|
+
const remainingSeconds = seconds % 60;
|
|
353
|
+
if (hours > 0)
|
|
354
|
+
return `${hours}h ${minutes}m ${remainingSeconds}s`;
|
|
355
|
+
if (minutes > 0)
|
|
356
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
357
|
+
return `${remainingSeconds}s`;
|
|
358
|
+
}
|
|
359
|
+
function formatBytes(bytes) {
|
|
360
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
361
|
+
return "unknown";
|
|
362
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
363
|
+
let value = bytes;
|
|
364
|
+
let unitIndex = 0;
|
|
365
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
366
|
+
value /= 1024;
|
|
367
|
+
unitIndex += 1;
|
|
368
|
+
}
|
|
369
|
+
return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
370
|
+
}
|
|
371
|
+
function truncateForDiagnostic(value, maxLength) {
|
|
372
|
+
const trimmed = value.trim();
|
|
373
|
+
if (trimmed.length <= maxLength)
|
|
374
|
+
return trimmed;
|
|
375
|
+
return `${trimmed.slice(0, maxLength)}...(truncated)`;
|
|
376
|
+
}
|
|
377
|
+
function formatDaemonShell() {
|
|
378
|
+
const shell = getEnvValue("SHELL");
|
|
379
|
+
if (shell)
|
|
380
|
+
return `SHELL=${shell}`;
|
|
381
|
+
const comspec = getEnvValue("ComSpec", "COMSPEC");
|
|
382
|
+
if (comspec)
|
|
383
|
+
return `ComSpec=${comspec}`;
|
|
384
|
+
return "unset";
|
|
385
|
+
}
|
|
386
|
+
function getEnvValue(...names) {
|
|
387
|
+
for (const name of names) {
|
|
388
|
+
const value = process.env[name];
|
|
389
|
+
if (value)
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
const lowerNames = new Set(names.map((name) => name.toLowerCase()));
|
|
393
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
394
|
+
if (value && lowerNames.has(key.toLowerCase()))
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function toErrorMessage(error) {
|
|
400
|
+
if (error instanceof Error)
|
|
401
|
+
return error.message;
|
|
402
|
+
if (typeof error === "string")
|
|
403
|
+
return error;
|
|
404
|
+
return String(error);
|
|
405
|
+
}
|
|
406
|
+
export function redactDiagnostic(value, options) {
|
|
407
|
+
let redacted = value;
|
|
408
|
+
const sensitiveValues = [
|
|
409
|
+
options?.daemonRuntimeConfig?.listen,
|
|
410
|
+
options?.daemonRuntimeConfig?.relay?.endpoint,
|
|
411
|
+
options?.daemonRuntimeConfig?.relay?.publicEndpoint,
|
|
412
|
+
].filter((item) => Boolean(item));
|
|
413
|
+
for (const sensitive of sensitiveValues) {
|
|
414
|
+
redacted = redacted.split(sensitive).join("[redacted]");
|
|
415
|
+
}
|
|
416
|
+
return redacted
|
|
417
|
+
.replace(/paseo:\/\/\S+/gi, "paseo://[redacted]")
|
|
418
|
+
.replace(/([?&](?:password|token|secret|key|publicKey|daemonPublicKeyB64)=)[^&\s"']+/gi, "$1[redacted]")
|
|
419
|
+
.replace(/((?:password|token|secret|authorization|api[_-]?key|daemonPublicKeyB64|relayKey)\s*[:=]\s*)("[^"]+"|'[^']+'|[^\s,}]+)/gi, "$1[redacted]");
|
|
420
|
+
}
|
|
421
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type pino from "pino";
|
|
2
|
+
import type { GitHubService } from "../../../services/github-service.js";
|
|
3
|
+
import { type CheckoutExistingBranchResult, type GitMutationRefreshReason } from "../../../utils/checkout-git.js";
|
|
4
|
+
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
|
5
|
+
/**
|
|
6
|
+
* The git branch / working-tree mutation primitives a client session performs on a
|
|
7
|
+
* workspace: switch to an existing branch, create a branch from a base, and force a
|
|
8
|
+
* snapshot refresh (plus optional GitHub cache invalidation) after any mutation.
|
|
9
|
+
*
|
|
10
|
+
* CheckoutSession (the branch/commit/merge commands), the worktree session-config
|
|
11
|
+
* builder, and the auto-naming + worktree-creation paths all funnel their git
|
|
12
|
+
* mutations through this one module, so the validate-ref → clean-tree → execute →
|
|
13
|
+
* refresh sequence lives in a single place instead of being smeared across the
|
|
14
|
+
* session as loose callbacks.
|
|
15
|
+
*/
|
|
16
|
+
export interface GitMutationService {
|
|
17
|
+
checkoutExistingBranch(cwd: string, branch: string): Promise<CheckoutExistingBranchResult>;
|
|
18
|
+
createBranchFromBase(params: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
baseBranch: string;
|
|
21
|
+
newBranchName: string;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
notifyGitMutation(cwd: string, reason: GitMutationRefreshReason, options?: {
|
|
24
|
+
invalidateGithub?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
type GitMutationGitSource = Pick<WorkspaceGitService, "validateBranchRef" | "getSnapshot" | "hasLocalBranch">;
|
|
28
|
+
export declare function createGitMutationService(deps: {
|
|
29
|
+
workspaceGitService: GitMutationGitSource;
|
|
30
|
+
github: Pick<GitHubService, "invalidate">;
|
|
31
|
+
logger: pino.Logger;
|
|
32
|
+
}): GitMutationService;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=git-mutation-service.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getErrorMessage } from "@getpaseo/protocol/error-utils";
|
|
2
|
+
import { checkoutResolvedBranch, } from "../../../utils/checkout-git.js";
|
|
3
|
+
import { execCommand } from "../../../utils/spawn.js";
|
|
4
|
+
import { assertSafeGitRef as assertWorktreeSafeGitRef } from "../../worktree-session.js";
|
|
5
|
+
export function createGitMutationService(deps) {
|
|
6
|
+
const { workspaceGitService, github, logger } = deps;
|
|
7
|
+
function assertSafeGitRef(ref, label) {
|
|
8
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
|
|
9
|
+
throw new Error(`Invalid ${label}: ${ref}`);
|
|
10
|
+
}
|
|
11
|
+
assertWorktreeSafeGitRef(ref, label);
|
|
12
|
+
}
|
|
13
|
+
async function isWorkingTreeDirty(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
const snapshot = await workspaceGitService.getSnapshot(cwd);
|
|
16
|
+
return snapshot.git.isDirty === true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`Unable to inspect git status for ${cwd}: ${getErrorMessage(error)}`, {
|
|
20
|
+
cause: error,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function ensureCleanWorkingTree(cwd) {
|
|
25
|
+
const dirty = await isWorkingTreeDirty(cwd);
|
|
26
|
+
if (dirty) {
|
|
27
|
+
throw new Error("Working directory has uncommitted changes. Commit or stash before switching branches.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function notifyGitMutation(cwd, reason, options) {
|
|
31
|
+
if (options?.invalidateGithub) {
|
|
32
|
+
github.invalidate({ cwd });
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await workspaceGitService.getSnapshot(cwd, { force: true, reason });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.warn({ err: error, cwd, reason }, "Failed to force-refresh workspace git snapshot after mutation");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
async checkoutExistingBranch(cwd, branch) {
|
|
43
|
+
assertSafeGitRef(branch, "branch");
|
|
44
|
+
const resolution = await workspaceGitService.validateBranchRef(cwd, branch);
|
|
45
|
+
if (resolution.kind === "not-found") {
|
|
46
|
+
throw new Error(`Branch not found: ${branch}`);
|
|
47
|
+
}
|
|
48
|
+
await ensureCleanWorkingTree(cwd);
|
|
49
|
+
const result = await checkoutResolvedBranch({ cwd, resolution });
|
|
50
|
+
await notifyGitMutation(cwd, "switch-branch", { invalidateGithub: true });
|
|
51
|
+
return result;
|
|
52
|
+
},
|
|
53
|
+
async createBranchFromBase({ cwd, baseBranch, newBranchName }) {
|
|
54
|
+
assertSafeGitRef(baseBranch, "base branch");
|
|
55
|
+
assertSafeGitRef(newBranchName, "new branch");
|
|
56
|
+
const baseResolution = await workspaceGitService.validateBranchRef(cwd, baseBranch);
|
|
57
|
+
if (baseResolution.kind === "not-found") {
|
|
58
|
+
throw new Error(`Base branch not found: ${baseBranch}`);
|
|
59
|
+
}
|
|
60
|
+
const exists = await workspaceGitService.hasLocalBranch(cwd, newBranchName);
|
|
61
|
+
if (exists) {
|
|
62
|
+
throw new Error(`Branch already exists: ${newBranchName}`);
|
|
63
|
+
}
|
|
64
|
+
await ensureCleanWorkingTree(cwd);
|
|
65
|
+
await execCommand("git", ["checkout", "-b", newBranchName, baseBranch], { cwd });
|
|
66
|
+
await notifyGitMutation(cwd, "create-branch");
|
|
67
|
+
},
|
|
68
|
+
notifyGitMutation,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=git-mutation-service.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type pino from "pino";
|
|
2
|
+
import type { WorkspaceDescriptorPayload } from "../../messages.js";
|
|
3
|
+
import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "../../workspace-git-service.js";
|
|
4
|
+
import type { PersistedWorkspaceRecord } from "../../workspace-registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Observes a workspace's git state on disk (via WorkspaceGitService) and drives the
|
|
7
|
+
* live update fan-out: branch-change notifications, workspace-card refreshes, and
|
|
8
|
+
* checkout status updates. It owns the per-cwd watch targets and the WorkspaceGitService
|
|
9
|
+
* subscription handles, so the registration / dedupe / teardown lifecycle lives in one
|
|
10
|
+
* module instead of being smeared across the client session.
|
|
11
|
+
*
|
|
12
|
+
* Branch changes reach `onBranchChanged` from two paths that share `lastBranchName`: the
|
|
13
|
+
* on-disk snapshot listener (handleBranchSnapshot) and the workspace-emit loop
|
|
14
|
+
* (recordDescriptorState). Both stay inside this module so the shared state is coherent.
|
|
15
|
+
*/
|
|
16
|
+
export interface WorkspaceGitObserverService {
|
|
17
|
+
syncObservers(workspaces: Iterable<WorkspaceDescriptorPayload>): void;
|
|
18
|
+
syncObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void>;
|
|
19
|
+
warmGitData(workspace: PersistedWorkspaceRecord): Promise<void>;
|
|
20
|
+
shouldSkipUpdate(workspaceId: string, workspace: WorkspaceDescriptorPayload | null): boolean;
|
|
21
|
+
recordDescriptorState(workspaceId: string, workspace: WorkspaceDescriptorPayload | null): void;
|
|
22
|
+
handleBranchSnapshot(cwd: string, branchName: string | null): void;
|
|
23
|
+
removeForWorkspaceId(workspaceId: string): void;
|
|
24
|
+
removeForCwd(cwd: string): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createWorkspaceGitObserverService(deps: {
|
|
28
|
+
workspaceGitService: Pick<WorkspaceGitService, "registerWorkspace">;
|
|
29
|
+
describeWorkspaceRecordWithGitData: (workspace: PersistedWorkspaceRecord) => Promise<WorkspaceDescriptorPayload>;
|
|
30
|
+
emitWorkspaceUpdateForCwd: (cwd: string) => Promise<void>;
|
|
31
|
+
emitWorkspaceUpdateForWorkspaceId: (workspaceId: string) => Promise<void>;
|
|
32
|
+
emitStatusUpdate: (cwd: string, snapshot: WorkspaceGitRuntimeSnapshot) => void;
|
|
33
|
+
onBranchChanged?: (workspaceId: string, oldBranch: string | null, newBranch: string | null) => void;
|
|
34
|
+
logger: pino.Logger;
|
|
35
|
+
}): WorkspaceGitObserverService;
|
|
36
|
+
//# sourceMappingURL=workspace-git-observer-service.d.ts.map
|