presubmit 0.1.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 +202 -0
- package/README.md +51 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
- package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
- package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
- package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
- package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
- package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
- package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
- package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
- package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
- package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/apps/web/dist/index.html +6 -0
- package/apps/web/dist/service-worker.js +11 -0
- package/dist/apps/server/src/agent-builder.js +204 -0
- package/dist/apps/server/src/attach.js +163 -0
- package/dist/apps/server/src/attachments.js +86 -0
- package/dist/apps/server/src/browser-access.js +131 -0
- package/dist/apps/server/src/check-provenance.js +95 -0
- package/dist/apps/server/src/check-supervisor.js +112 -0
- package/dist/apps/server/src/checks.js +402 -0
- package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
- package/dist/apps/server/src/child-sessions.js +191 -0
- package/dist/apps/server/src/cli.js +162 -0
- package/dist/apps/server/src/conversation-search.js +12 -0
- package/dist/apps/server/src/coordinator-messages.js +72 -0
- package/dist/apps/server/src/dev-auth.js +54 -0
- package/dist/apps/server/src/dev-server.js +201 -0
- package/dist/apps/server/src/dev-worker.js +48 -0
- package/dist/apps/server/src/diagnostics.js +105 -0
- package/dist/apps/server/src/fast-mode.js +8 -0
- package/dist/apps/server/src/folder-picker.js +54 -0
- package/dist/apps/server/src/git-blobs.js +62 -0
- package/dist/apps/server/src/git-handoff.js +127 -0
- package/dist/apps/server/src/history-sources.js +59 -0
- package/dist/apps/server/src/hub.js +159 -0
- package/dist/apps/server/src/internal-transport.js +38 -0
- package/dist/apps/server/src/local-embeddings.js +70 -0
- package/dist/apps/server/src/ollama-setup.js +56 -0
- package/dist/apps/server/src/planning.js +30 -0
- package/dist/apps/server/src/project-agents.js +276 -0
- package/dist/apps/server/src/project-memory.js +94 -0
- package/dist/apps/server/src/projects.js +106 -0
- package/dist/apps/server/src/queued-launch.js +13 -0
- package/dist/apps/server/src/reply-search.js +172 -0
- package/dist/apps/server/src/review-context.js +60 -0
- package/dist/apps/server/src/review-refresh.js +14 -0
- package/dist/apps/server/src/reviewers.js +75 -0
- package/dist/apps/server/src/search-settings.js +22 -0
- package/dist/apps/server/src/semantic-history.js +186 -0
- package/dist/apps/server/src/server.js +4338 -0
- package/dist/apps/server/src/session-changes.js +181 -0
- package/dist/apps/server/src/session-router-settings.js +198 -0
- package/dist/apps/server/src/session-workspaces.js +135 -0
- package/dist/apps/server/src/sessions.js +579 -0
- package/dist/apps/server/src/shared-auth.js +70 -0
- package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
- package/dist/apps/server/src/store.js +203 -0
- package/dist/apps/server/src/tailscale-process.js +115 -0
- package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
- package/dist/apps/server/src/tailscale-serve.js +276 -0
- package/dist/apps/server/src/task-capacity.js +92 -0
- package/dist/apps/server/src/task-graph.js +190 -0
- package/dist/apps/server/src/task-logs.js +74 -0
- package/dist/apps/server/src/task-numbers.js +25 -0
- package/dist/apps/server/src/task-orchestrator.js +1088 -0
- package/dist/apps/server/src/terminal.js +182 -0
- package/dist/apps/server/src/terminals.js +80 -0
- package/dist/apps/server/src/tool-safety.js +2 -0
- package/dist/apps/server/src/workspace-lock.js +140 -0
- package/dist/apps/server/src/workspace-watcher.js +65 -0
- package/dist/apps/server/src/workspace.js +517 -0
- package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
- package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
- package/dist/packages/agent-pi/src/ask-user.js +86 -0
- package/dist/packages/agent-pi/src/auth.js +510 -0
- package/dist/packages/agent-pi/src/check-tools.js +72 -0
- package/dist/packages/agent-pi/src/command-policy.js +246 -0
- package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
- package/dist/packages/agent-pi/src/fast-mode.js +67 -0
- package/dist/packages/agent-pi/src/file-tools.js +163 -0
- package/dist/packages/agent-pi/src/history-tool.js +40 -0
- package/dist/packages/agent-pi/src/history.js +34 -0
- package/dist/packages/agent-pi/src/index.js +177 -0
- package/dist/packages/agent-pi/src/ipc.js +314 -0
- package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
- package/dist/packages/agent-pi/src/jev-tools.js +71 -0
- package/dist/packages/agent-pi/src/local-model.js +277 -0
- package/dist/packages/agent-pi/src/memory-tools.js +119 -0
- package/dist/packages/agent-pi/src/model.js +25 -0
- package/dist/packages/agent-pi/src/planning.js +206 -0
- package/dist/packages/agent-pi/src/resource-loader.js +23 -0
- package/dist/packages/agent-pi/src/review-tools.js +158 -0
- package/dist/packages/agent-pi/src/review-transition.js +73 -0
- package/dist/packages/agent-pi/src/review.js +40 -0
- package/dist/packages/agent-pi/src/routing.js +27 -0
- package/dist/packages/agent-pi/src/session-setup.js +118 -0
- package/dist/packages/agent-pi/src/shell-context.js +51 -0
- package/dist/packages/agent-pi/src/shell-policy.js +281 -0
- package/dist/packages/agent-pi/src/shell-safety.js +205 -0
- package/dist/packages/agent-pi/src/shell.js +121 -0
- package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
- package/dist/packages/agent-pi/src/spike.js +41 -0
- package/dist/packages/agent-pi/src/task-tools.js +53 -0
- package/dist/packages/agent-pi/src/types.js +1 -0
- package/dist/packages/agent-pi/src/usage.js +30 -0
- package/dist/packages/agent-pi/src/worker.js +602 -0
- package/dist/packages/protocol/src/activity-summary.js +77 -0
- package/dist/packages/protocol/src/agents.js +301 -0
- package/dist/packages/protocol/src/browser-access.js +1 -0
- package/dist/packages/protocol/src/checks.js +40 -0
- package/dist/packages/protocol/src/child-sessions.js +28 -0
- package/dist/packages/protocol/src/command-policy.js +3 -0
- package/dist/packages/protocol/src/coordinator.js +135 -0
- package/dist/packages/protocol/src/delegation.js +40 -0
- package/dist/packages/protocol/src/git-diff.js +71 -0
- package/dist/packages/protocol/src/git-handoff.js +1 -0
- package/dist/packages/protocol/src/index.js +1 -0
- package/dist/packages/protocol/src/memory.js +46 -0
- package/dist/packages/protocol/src/metrics.js +70 -0
- package/dist/packages/protocol/src/planning.js +138 -0
- package/dist/packages/protocol/src/retrieval.js +36 -0
- package/dist/packages/protocol/src/reviewers.js +17 -0
- package/dist/packages/protocol/src/search.js +16 -0
- package/dist/packages/protocol/src/session-changes.js +29 -0
- package/dist/packages/protocol/src/session-router.js +123 -0
- package/dist/packages/protocol/src/shell-evaluator.js +43 -0
- package/dist/packages/protocol/src/task-graph.js +1 -0
- package/dist/packages/protocol/src/token-usage.js +17 -0
- package/dist/packages/protocol/src/user-questions.js +106 -0
- package/package.json +71 -0
- package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
- package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
- package/vendor/lancedb/dist/arrow.d.ts +296 -0
- package/vendor/lancedb/dist/arrow.js +1145 -0
- package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
- package/vendor/lancedb/dist/arrow_type.js +29 -0
- package/vendor/lancedb/dist/connection.d.ts +508 -0
- package/vendor/lancedb/dist/connection.js +298 -0
- package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
- package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
- package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
- package/vendor/lancedb/dist/embedding/index.js +108 -0
- package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
- package/vendor/lancedb/dist/embedding/openai.js +81 -0
- package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
- package/vendor/lancedb/dist/embedding/registry.js +221 -0
- package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
- package/vendor/lancedb/dist/embedding/transformers.js +110 -0
- package/vendor/lancedb/dist/header.d.ts +162 -0
- package/vendor/lancedb/dist/header.js +217 -0
- package/vendor/lancedb/dist/index.d.ts +246 -0
- package/vendor/lancedb/dist/index.js +176 -0
- package/vendor/lancedb/dist/indices.d.ts +721 -0
- package/vendor/lancedb/dist/indices.js +166 -0
- package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
- package/vendor/lancedb/dist/materialized_view.js +112 -0
- package/vendor/lancedb/dist/merge.d.ts +104 -0
- package/vendor/lancedb/dist/merge.js +120 -0
- package/vendor/lancedb/dist/native.d.ts +1193 -0
- package/vendor/lancedb/dist/native.js +688 -0
- package/vendor/lancedb/dist/oauth.d.ts +66 -0
- package/vendor/lancedb/dist/oauth.js +15 -0
- package/vendor/lancedb/dist/otel.d.ts +26 -0
- package/vendor/lancedb/dist/otel.js +114 -0
- package/vendor/lancedb/dist/permutation.d.ts +143 -0
- package/vendor/lancedb/dist/permutation.js +184 -0
- package/vendor/lancedb/dist/query.d.ts +663 -0
- package/vendor/lancedb/dist/query.js +966 -0
- package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
- package/vendor/lancedb/dist/rerankers/index.js +19 -0
- package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
- package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
- package/vendor/lancedb/dist/sanitize.d.ts +32 -0
- package/vendor/lancedb/dist/sanitize.js +544 -0
- package/vendor/lancedb/dist/scannable.d.ts +92 -0
- package/vendor/lancedb/dist/scannable.js +200 -0
- package/vendor/lancedb/dist/schema.d.ts +16 -0
- package/vendor/lancedb/dist/schema.js +387 -0
- package/vendor/lancedb/dist/table.d.ts +1069 -0
- package/vendor/lancedb/dist/table.js +498 -0
- package/vendor/lancedb/dist/util.d.ts +14 -0
- package/vendor/lancedb/dist/util.js +77 -0
- package/vendor/lancedb/license_header.txt +2 -0
- package/vendor/lancedb/package.json +56 -0
- package/vendor/transformers/LICENSE +202 -0
- package/vendor/transformers/dist/transformers.cjs +30844 -0
- package/vendor/transformers/dist/transformers.mjs +31404 -0
- package/vendor/transformers/package.json +55 -0
- package/vendor/transformers/types/backends/onnx.d.ts +30 -0
- package/vendor/transformers/types/configs.d.ts +85 -0
- package/vendor/transformers/types/env.d.ts +110 -0
- package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
- package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
- package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
- package/vendor/transformers/types/generation/parameters.d.ts +47 -0
- package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
- package/vendor/transformers/types/generation/streamers.d.ts +81 -0
- package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
- package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
- package/vendor/transformers/types/models.d.ts +3684 -0
- package/vendor/transformers/types/ops/registry.d.ts +11 -0
- package/vendor/transformers/types/pipelines.d.ts +2402 -0
- package/vendor/transformers/types/processors.d.ts +924 -0
- package/vendor/transformers/types/tokenizers.d.ts +990 -0
- package/vendor/transformers/types/transformers.d.ts +13 -0
- package/vendor/transformers/types/utils/audio.d.ts +130 -0
- package/vendor/transformers/types/utils/constants.d.ts +2 -0
- package/vendor/transformers/types/utils/core.d.ts +98 -0
- package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
- package/vendor/transformers/types/utils/devices.d.ts +18 -0
- package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
- package/vendor/transformers/types/utils/generic.d.ts +11 -0
- package/vendor/transformers/types/utils/hub.d.ts +152 -0
- package/vendor/transformers/types/utils/image.d.ts +119 -0
- package/vendor/transformers/types/utils/maths.d.ts +280 -0
- package/vendor/transformers/types/utils/tensor.d.ts +418 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import headless from '@xterm/headless';
|
|
3
|
+
import serializer from '@xterm/addon-serialize';
|
|
4
|
+
import { WebSocket } from 'ws';
|
|
5
|
+
/** One server-owned PTY per terminal. Browser sockets are observers, not process owners. */
|
|
6
|
+
export class SharedTerminal {
|
|
7
|
+
cwd;
|
|
8
|
+
shell;
|
|
9
|
+
onChange;
|
|
10
|
+
process;
|
|
11
|
+
screen = new headless.Terminal({
|
|
12
|
+
cols: 100,
|
|
13
|
+
rows: 30,
|
|
14
|
+
scrollback: 2000,
|
|
15
|
+
allowProposedApi: true,
|
|
16
|
+
});
|
|
17
|
+
serializer = new serializer.SerializeAddon();
|
|
18
|
+
peers = new Set();
|
|
19
|
+
sizes = new Map();
|
|
20
|
+
status = 'idle';
|
|
21
|
+
exitCode;
|
|
22
|
+
disposed = false;
|
|
23
|
+
pending = 0;
|
|
24
|
+
constructor(cwd, shell = process.env.SHELL || '/bin/sh', onChange = () => { }) {
|
|
25
|
+
this.cwd = cwd;
|
|
26
|
+
this.shell = shell;
|
|
27
|
+
this.onChange = onChange;
|
|
28
|
+
this.screen.loadAddon(this.serializer);
|
|
29
|
+
}
|
|
30
|
+
get lifecycle() {
|
|
31
|
+
return this.status;
|
|
32
|
+
}
|
|
33
|
+
get running() {
|
|
34
|
+
return !!this.process;
|
|
35
|
+
}
|
|
36
|
+
send(peer, value) {
|
|
37
|
+
if (peer.readyState !== WebSocket.OPEN)
|
|
38
|
+
return;
|
|
39
|
+
if (peer.bufferedAmount > 4 * 1024 * 1024) {
|
|
40
|
+
peer.close(1013, 'Reconnect to catch up');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
peer.send(JSON.stringify(value));
|
|
44
|
+
}
|
|
45
|
+
broadcast(value) {
|
|
46
|
+
for (const peer of this.peers)
|
|
47
|
+
this.send(peer, value);
|
|
48
|
+
}
|
|
49
|
+
state() {
|
|
50
|
+
return { type: 'status', status: this.status, exitCode: this.exitCode, peers: this.peers.size };
|
|
51
|
+
}
|
|
52
|
+
attach(peer) {
|
|
53
|
+
// Flush pending parser work before serializing, then subscribe without a replay/live gap.
|
|
54
|
+
this.screen.write('', () => {
|
|
55
|
+
if (this.disposed || peer.readyState !== WebSocket.OPEN)
|
|
56
|
+
return;
|
|
57
|
+
this.send(peer, {
|
|
58
|
+
type: 'snapshot',
|
|
59
|
+
data: this.serializer.serialize(),
|
|
60
|
+
cols: this.screen.cols,
|
|
61
|
+
rows: this.screen.rows,
|
|
62
|
+
});
|
|
63
|
+
this.peers.add(peer);
|
|
64
|
+
this.broadcast(this.state());
|
|
65
|
+
});
|
|
66
|
+
peer.on('message', (bytes) => {
|
|
67
|
+
if (!this.peers.has(peer))
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
if (bytes.toString().length > 64 * 1024)
|
|
71
|
+
throw new Error('Terminal message too large');
|
|
72
|
+
const message = JSON.parse(bytes.toString());
|
|
73
|
+
if (message.type === 'start')
|
|
74
|
+
this.start();
|
|
75
|
+
else if (message.type === 'input' &&
|
|
76
|
+
typeof message.data === 'string' &&
|
|
77
|
+
message.data.length <= 32768)
|
|
78
|
+
this.process?.write(message.data);
|
|
79
|
+
else if (message.type === 'resize' &&
|
|
80
|
+
Number.isInteger(message.cols) &&
|
|
81
|
+
Number.isInteger(message.rows) &&
|
|
82
|
+
message.cols >= 20 &&
|
|
83
|
+
message.cols <= 300 &&
|
|
84
|
+
message.rows >= 5 &&
|
|
85
|
+
message.rows <= 100) {
|
|
86
|
+
this.sizes.set(peer, { cols: message.cols, rows: message.rows });
|
|
87
|
+
this.fitVisiblePeers();
|
|
88
|
+
}
|
|
89
|
+
else if (message.type === 'hidden') {
|
|
90
|
+
this.sizes.delete(peer);
|
|
91
|
+
this.fitVisiblePeers();
|
|
92
|
+
}
|
|
93
|
+
else if (message.type === 'stop')
|
|
94
|
+
this.process?.kill();
|
|
95
|
+
else
|
|
96
|
+
throw new Error('Invalid terminal message');
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.send(peer, {
|
|
100
|
+
type: 'error',
|
|
101
|
+
message: error instanceof Error ? error.message : 'Terminal error',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
peer.on('close', () => {
|
|
106
|
+
this.peers.delete(peer);
|
|
107
|
+
this.sizes.delete(peer);
|
|
108
|
+
this.fitVisiblePeers();
|
|
109
|
+
this.broadcast(this.state());
|
|
110
|
+
});
|
|
111
|
+
peer.on('error', () => this.peers.delete(peer));
|
|
112
|
+
}
|
|
113
|
+
fitVisiblePeers() {
|
|
114
|
+
this.screen.write('', () => {
|
|
115
|
+
if (this.disposed || !this.sizes.size)
|
|
116
|
+
return;
|
|
117
|
+
const sizes = [...this.sizes.values()];
|
|
118
|
+
const cols = Math.min(...sizes.map((size) => size.cols));
|
|
119
|
+
const rows = Math.min(...sizes.map((size) => size.rows));
|
|
120
|
+
if (cols === this.screen.cols && rows === this.screen.rows)
|
|
121
|
+
return;
|
|
122
|
+
this.screen.resize(cols, rows);
|
|
123
|
+
this.process?.resize(cols, rows);
|
|
124
|
+
this.broadcast({ type: 'resize', cols, rows });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
start() {
|
|
128
|
+
if (this.process || this.disposed)
|
|
129
|
+
return;
|
|
130
|
+
this.screen.reset();
|
|
131
|
+
const child = pty.spawn(this.shell, ['-i'], {
|
|
132
|
+
cwd: this.cwd,
|
|
133
|
+
name: 'xterm-256color',
|
|
134
|
+
cols: this.screen.cols,
|
|
135
|
+
rows: this.screen.rows,
|
|
136
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
137
|
+
});
|
|
138
|
+
this.process = child;
|
|
139
|
+
this.status = 'running';
|
|
140
|
+
this.onChange();
|
|
141
|
+
this.exitCode = undefined;
|
|
142
|
+
this.broadcast({ type: 'snapshot', data: '', cols: this.screen.cols, rows: this.screen.rows });
|
|
143
|
+
this.broadcast(this.state());
|
|
144
|
+
child.onData((data) => {
|
|
145
|
+
if (this.disposed)
|
|
146
|
+
return;
|
|
147
|
+
this.pending += data.length;
|
|
148
|
+
if (this.pending > 256 * 1024)
|
|
149
|
+
child.pause();
|
|
150
|
+
this.screen.write(data, () => {
|
|
151
|
+
if (this.disposed)
|
|
152
|
+
return;
|
|
153
|
+
this.pending -= data.length;
|
|
154
|
+
this.broadcast({ type: 'output', data });
|
|
155
|
+
if (this.pending < 64 * 1024 && this.process === child)
|
|
156
|
+
child.resume();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
child.onExit(({ exitCode }) => {
|
|
160
|
+
if (this.disposed)
|
|
161
|
+
return;
|
|
162
|
+
this.process = undefined;
|
|
163
|
+
this.status = 'exited';
|
|
164
|
+
this.onChange();
|
|
165
|
+
this.exitCode = exitCode;
|
|
166
|
+
this.screen.write('', () => {
|
|
167
|
+
if (!this.disposed)
|
|
168
|
+
this.broadcast(this.state());
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
dispose(code = 1001, reason = 'Server stopping') {
|
|
173
|
+
this.disposed = true;
|
|
174
|
+
this.process?.kill();
|
|
175
|
+
this.process = undefined;
|
|
176
|
+
for (const peer of this.peers)
|
|
177
|
+
peer.close(code, reason);
|
|
178
|
+
this.peers.clear();
|
|
179
|
+
this.sizes.clear();
|
|
180
|
+
this.screen.dispose();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { SharedTerminal } from './terminal.js';
|
|
3
|
+
export class SessionTerminals {
|
|
4
|
+
cwd;
|
|
5
|
+
shell;
|
|
6
|
+
items = new Map();
|
|
7
|
+
peers = new Set();
|
|
8
|
+
nextTitle = 1;
|
|
9
|
+
constructor(cwd, shell = process.env.SHELL || '/bin/sh') {
|
|
10
|
+
this.cwd = cwd;
|
|
11
|
+
this.shell = shell;
|
|
12
|
+
this.create('main');
|
|
13
|
+
}
|
|
14
|
+
get running() {
|
|
15
|
+
return [...this.items.values()].some((item) => item.terminal.running);
|
|
16
|
+
}
|
|
17
|
+
list() {
|
|
18
|
+
return [...this.items.values()].map(({ id, title, terminal }) => ({
|
|
19
|
+
id,
|
|
20
|
+
title,
|
|
21
|
+
status: terminal.lifecycle,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
create(id) {
|
|
25
|
+
if (id !== 'main' && !/^[a-f0-9-]{36}$/i.test(id))
|
|
26
|
+
throw new Error('Invalid terminal ID');
|
|
27
|
+
if (this.items.has(id))
|
|
28
|
+
return this.list().find((item) => item.id === id);
|
|
29
|
+
if (this.items.size >= 16)
|
|
30
|
+
throw new Error('This session has 16 terminals. Close an existing terminal first.');
|
|
31
|
+
const title = `Terminal ${this.nextTitle++}`;
|
|
32
|
+
const terminal = new SharedTerminal(this.cwd, this.shell, () => this.broadcast());
|
|
33
|
+
this.items.set(id, { id, title, terminal });
|
|
34
|
+
this.broadcast();
|
|
35
|
+
return { id, title, status: terminal.lifecycle };
|
|
36
|
+
}
|
|
37
|
+
close(id) {
|
|
38
|
+
const item = this.items.get(id);
|
|
39
|
+
if (!item)
|
|
40
|
+
return;
|
|
41
|
+
this.items.delete(id);
|
|
42
|
+
item.terminal.dispose(1008, 'Terminal closed');
|
|
43
|
+
this.broadcast();
|
|
44
|
+
}
|
|
45
|
+
attach(id, peer) {
|
|
46
|
+
const item = this.items.get(id);
|
|
47
|
+
if (!item) {
|
|
48
|
+
peer.close(1008, 'Terminal unavailable');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
item.terminal.attach(peer);
|
|
52
|
+
}
|
|
53
|
+
watch(peer) {
|
|
54
|
+
this.peers.add(peer);
|
|
55
|
+
this.send(peer);
|
|
56
|
+
peer.on('close', () => this.peers.delete(peer));
|
|
57
|
+
peer.on('error', () => this.peers.delete(peer));
|
|
58
|
+
}
|
|
59
|
+
send(peer) {
|
|
60
|
+
if (peer.readyState !== WebSocket.OPEN)
|
|
61
|
+
return;
|
|
62
|
+
if (peer.bufferedAmount > 1024 * 1024) {
|
|
63
|
+
peer.close(1013, 'Reconnect');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
peer.send(JSON.stringify({ type: 'terminals', terminals: this.list() }));
|
|
67
|
+
}
|
|
68
|
+
broadcast() {
|
|
69
|
+
for (const peer of this.peers)
|
|
70
|
+
this.send(peer);
|
|
71
|
+
}
|
|
72
|
+
dispose() {
|
|
73
|
+
for (const peer of this.peers)
|
|
74
|
+
peer.close(1001, 'Server stopping');
|
|
75
|
+
this.peers.clear();
|
|
76
|
+
for (const item of this.items.values())
|
|
77
|
+
item.terminal.dispose();
|
|
78
|
+
this.items.clear();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createConnection, createServer } from 'node:net';
|
|
3
|
+
import { chmod, lstat, mkdir, rmdir, unlink } from 'node:fs/promises';
|
|
4
|
+
function socketPath(canonicalCwd) {
|
|
5
|
+
const hash = createHash('sha256').update(canonicalCwd).digest('hex').slice(0, 20);
|
|
6
|
+
return `/tmp/presubmit-${process.getuid?.() ?? 'user'}-${hash}.sock`;
|
|
7
|
+
}
|
|
8
|
+
function probe(path) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const socket = createConnection(path);
|
|
11
|
+
let content = '';
|
|
12
|
+
socket.setTimeout(1500, () => socket.destroy(new Error('Existing workspace server did not respond.')));
|
|
13
|
+
socket.on('data', (chunk) => {
|
|
14
|
+
content += String(chunk);
|
|
15
|
+
if (content.length > 4096)
|
|
16
|
+
socket.destroy(new Error('Invalid workspace lock response.'));
|
|
17
|
+
});
|
|
18
|
+
socket.on('error', reject);
|
|
19
|
+
socket.on('end', () => {
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(content);
|
|
22
|
+
resolve({
|
|
23
|
+
pid: typeof value.pid === 'number' ? value.pid : undefined,
|
|
24
|
+
url: typeof value.url === 'string' ? value.url : undefined,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
resolve({});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function listen(server, path) {
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
const onError = (error) => {
|
|
36
|
+
server.off('listening', onListen);
|
|
37
|
+
reject(error);
|
|
38
|
+
};
|
|
39
|
+
const onListen = () => {
|
|
40
|
+
server.off('error', onError);
|
|
41
|
+
resolve();
|
|
42
|
+
};
|
|
43
|
+
server.once('error', onError);
|
|
44
|
+
server.once('listening', onListen);
|
|
45
|
+
server.listen(path);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export class WorkspaceAlreadyRunningError extends Error {
|
|
49
|
+
pid;
|
|
50
|
+
url;
|
|
51
|
+
constructor(pid, url) {
|
|
52
|
+
super(`Presubmit already owns this workspace${pid ? ` (PID ${pid})` : ''}.${url ? ` Open ${url}` : ' Wait for that server to finish starting, or stop it before starting another.'}`);
|
|
53
|
+
this.pid = pid;
|
|
54
|
+
this.url = url;
|
|
55
|
+
this.name = 'WorkspaceAlreadyRunningError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function existing(value) {
|
|
59
|
+
return new WorkspaceAlreadyRunningError(value.pid, value.url);
|
|
60
|
+
}
|
|
61
|
+
/** A workspace-scoped Unix socket lock is independent of the selected data directory. */
|
|
62
|
+
export async function acquireWorkspaceLock(canonicalCwd) {
|
|
63
|
+
if (process.platform === 'win32')
|
|
64
|
+
throw new Error('Workspace locking currently supports macOS and Linux.');
|
|
65
|
+
const path = socketPath(canonicalCwd);
|
|
66
|
+
let url;
|
|
67
|
+
const server = createServer((socket) => {
|
|
68
|
+
socket.on('error', () => { });
|
|
69
|
+
socket.end(JSON.stringify({ pid: process.pid, url }) + '\n');
|
|
70
|
+
socket.setTimeout(1500, () => socket.destroy());
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await listen(server, path);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== 'EADDRINUSE')
|
|
77
|
+
throw new Error(`Cannot acquire workspace lock at ${path}: ${error.message}`);
|
|
78
|
+
try {
|
|
79
|
+
throw existing(await probe(path));
|
|
80
|
+
}
|
|
81
|
+
catch (probeError) {
|
|
82
|
+
if (probeError.code === 'ENOTSOCK')
|
|
83
|
+
throw new Error(`Refusing to remove non-socket workspace lock: ${path}`);
|
|
84
|
+
if (probeError.code !== 'ECONNREFUSED')
|
|
85
|
+
throw probeError;
|
|
86
|
+
}
|
|
87
|
+
// Serialize stale recovery. Without this, two contenders could unlink a newly bound socket.
|
|
88
|
+
const recovery = `${path}.recovery`;
|
|
89
|
+
try {
|
|
90
|
+
await mkdir(recovery, { mode: 0o700 });
|
|
91
|
+
}
|
|
92
|
+
catch (recoveryError) {
|
|
93
|
+
if (recoveryError.code === 'EEXIST')
|
|
94
|
+
throw new Error(`Another process is recovering the workspace lock. Retry shortly. If it crashed, remove the empty recovery directory ${recovery}.`);
|
|
95
|
+
throw recoveryError;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
// A prior contender may have bound since our first failed probe.
|
|
99
|
+
try {
|
|
100
|
+
throw existing(await probe(path));
|
|
101
|
+
}
|
|
102
|
+
catch (probeError) {
|
|
103
|
+
if (!['ECONNREFUSED', 'ENOENT'].includes(probeError.code ?? ''))
|
|
104
|
+
throw probeError;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const info = await lstat(path);
|
|
108
|
+
if (!info.isSocket())
|
|
109
|
+
throw new Error(`Refusing to remove non-socket workspace lock: ${path}`);
|
|
110
|
+
await unlink(path);
|
|
111
|
+
}
|
|
112
|
+
catch (statError) {
|
|
113
|
+
if (statError.code !== 'ENOENT')
|
|
114
|
+
throw statError;
|
|
115
|
+
}
|
|
116
|
+
// Rebind while holding recovery ownership; other contenders can only probe/retry.
|
|
117
|
+
await listen(server, path);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await rmdir(recovery);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await chmod(path, 0o600);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
let released;
|
|
131
|
+
return {
|
|
132
|
+
setUrl(value) {
|
|
133
|
+
url = value;
|
|
134
|
+
},
|
|
135
|
+
release() {
|
|
136
|
+
released ??= new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
137
|
+
return released;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { watch } from 'chokidar';
|
|
3
|
+
import { diagnosticsEnabled, performanceMetrics, diagnosticGauges } from './diagnostics.js';
|
|
4
|
+
export function createWorkspaceWatchers(create = watch, debounceMs = 500) {
|
|
5
|
+
const entries = new Map();
|
|
6
|
+
const updateGauges = () => {
|
|
7
|
+
diagnosticGauges.set('workspace.watchers', entries.size);
|
|
8
|
+
diagnosticGauges.set('workspace.subscribers', [...entries.values()].reduce((sum, entry) => sum + entry.subscribers.size, 0));
|
|
9
|
+
};
|
|
10
|
+
return (cwd, subscriber) => {
|
|
11
|
+
const key = realpathSync(cwd);
|
|
12
|
+
let entry = entries.get(key);
|
|
13
|
+
if (!entry) {
|
|
14
|
+
const watcher = create(key, {
|
|
15
|
+
ignoreInitial: true,
|
|
16
|
+
ignored: (path) => path
|
|
17
|
+
.split(/[\\/]/)
|
|
18
|
+
.some((segment) => ['node_modules', '.git', 'dist', '.cache'].includes(segment)),
|
|
19
|
+
});
|
|
20
|
+
entry = { watcher, subscribers: new Set() };
|
|
21
|
+
entries.set(key, entry);
|
|
22
|
+
const shared = entry;
|
|
23
|
+
watcher.on('all', () => {
|
|
24
|
+
if (diagnosticsEnabled)
|
|
25
|
+
performanceMetrics.count('workspace.fileEvents');
|
|
26
|
+
// Coalesce a sustained stream as well as a short burst. Do not allocate
|
|
27
|
+
// a fresh timer for every event or postpone refresh indefinitely.
|
|
28
|
+
if (shared.timer)
|
|
29
|
+
return;
|
|
30
|
+
shared.timer = setTimeout(() => {
|
|
31
|
+
shared.timer = undefined;
|
|
32
|
+
if (diagnosticsEnabled)
|
|
33
|
+
performanceMetrics.count('workspace.changeBatches');
|
|
34
|
+
for (const listener of shared.subscribers)
|
|
35
|
+
listener.changed();
|
|
36
|
+
}, debounceMs);
|
|
37
|
+
});
|
|
38
|
+
watcher.on('error', (error) => {
|
|
39
|
+
for (const listener of shared.subscribers)
|
|
40
|
+
listener.error(error);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
entry.subscribers.add(subscriber);
|
|
44
|
+
updateGauges();
|
|
45
|
+
const shared = entry;
|
|
46
|
+
let closed;
|
|
47
|
+
return {
|
|
48
|
+
close() {
|
|
49
|
+
if (closed)
|
|
50
|
+
return closed;
|
|
51
|
+
shared.subscribers.delete(subscriber);
|
|
52
|
+
if (!shared.subscribers.size) {
|
|
53
|
+
entries.delete(key);
|
|
54
|
+
clearTimeout(shared.timer);
|
|
55
|
+
closed = shared.watcher.close();
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
closed = Promise.resolve();
|
|
59
|
+
updateGauges();
|
|
60
|
+
return closed;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export const watchWorkspace = createWorkspaceWatchers();
|