@crouton-kit/tsym 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 +21 -0
- package/README.md +51 -0
- package/bin/tsym.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +89 -0
- package/dist/commands/handlers/check.d.ts +3 -0
- package/dist/commands/handlers/check.js +94 -0
- package/dist/commands/handlers/find.d.ts +3 -0
- package/dist/commands/handlers/find.js +56 -0
- package/dist/commands/handlers/inspect.d.ts +3 -0
- package/dist/commands/handlers/inspect.js +81 -0
- package/dist/commands/handlers/outline.d.ts +3 -0
- package/dist/commands/handlers/outline.js +101 -0
- package/dist/commands/handlers/query.d.ts +3 -0
- package/dist/commands/handlers/query.js +196 -0
- package/dist/commands/handlers/rename.d.ts +3 -0
- package/dist/commands/handlers/rename.js +53 -0
- package/dist/commands/handlers/search.d.ts +5 -0
- package/dist/commands/handlers/search.js +32 -0
- package/dist/commands/handlers/server-reload.d.ts +3 -0
- package/dist/commands/handlers/server-reload.js +7 -0
- package/dist/commands/handlers/server-status.d.ts +3 -0
- package/dist/commands/handlers/server-status.js +7 -0
- package/dist/commands/handlers/server-stop.d.ts +3 -0
- package/dist/commands/handlers/server-stop.js +7 -0
- package/dist/commands/handlers/show.d.ts +3 -0
- package/dist/commands/handlers/show.js +63 -0
- package/dist/commands/handlers/type.d.ts +3 -0
- package/dist/commands/handlers/type.js +45 -0
- package/dist/commands/handlers.d.ts +12 -0
- package/dist/commands/handlers.js +44 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +27 -0
- package/dist/commands/rename-apply.d.ts +37 -0
- package/dist/commands/rename-apply.js +115 -0
- package/dist/commands/tree.d.ts +9 -0
- package/dist/commands/tree.js +277 -0
- package/dist/commands/types.d.ts +80 -0
- package/dist/commands/types.js +1 -0
- package/dist/core/declarations.d.ts +48 -0
- package/dist/core/declarations.js +153 -0
- package/dist/core/receipt.d.ts +7 -0
- package/dist/core/receipt.js +9 -0
- package/dist/core/search.d.ts +29 -0
- package/dist/core/search.js +47 -0
- package/dist/core/source.d.ts +32 -0
- package/dist/core/source.js +96 -0
- package/dist/core/workspace.d.ts +28 -0
- package/dist/core/workspace.js +137 -0
- package/dist/index/build.d.ts +22 -0
- package/dist/index/build.js +79 -0
- package/dist/index/classify.d.ts +20 -0
- package/dist/index/classify.js +60 -0
- package/dist/index/commits.d.ts +15 -0
- package/dist/index/commits.js +201 -0
- package/dist/index/descriptor.d.ts +19 -0
- package/dist/index/descriptor.js +295 -0
- package/dist/index/graph.d.ts +82 -0
- package/dist/index/graph.js +1 -0
- package/dist/index/heritage.d.ts +15 -0
- package/dist/index/heritage.js +122 -0
- package/dist/index/refresh.d.ts +28 -0
- package/dist/index/refresh.js +110 -0
- package/dist/index/scope.d.ts +4 -0
- package/dist/index/scope.js +21 -0
- package/dist/index/state.d.ts +22 -0
- package/dist/index/state.js +63 -0
- package/dist/index/walk.d.ts +23 -0
- package/dist/index/walk.js +550 -0
- package/dist/index/watch.d.ts +18 -0
- package/dist/index/watch.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.js +54 -0
- package/dist/read/read.d.ts +32 -0
- package/dist/read/read.js +89 -0
- package/dist/relations/graph.d.ts +23 -0
- package/dist/relations/graph.js +115 -0
- package/dist/relations/mermaid.d.ts +3 -0
- package/dist/relations/mermaid.js +37 -0
- package/dist/relations/store-relations.d.ts +20 -0
- package/dist/relations/store-relations.js +158 -0
- package/dist/relations/types.d.ts +58 -0
- package/dist/relations/types.js +12 -0
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +215 -0
- package/dist/server/context.d.ts +21 -0
- package/dist/server/context.js +12 -0
- package/dist/server/lifecycle.d.ts +28 -0
- package/dist/server/lifecycle.js +62 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +541 -0
- package/dist/server/projects.d.ts +21 -0
- package/dist/server/projects.js +28 -0
- package/dist/server/wire.d.ts +39 -0
- package/dist/server/wire.js +19 -0
- package/dist/store/load.d.ts +67 -0
- package/dist/store/load.js +193 -0
- package/dist/store/meta.d.ts +28 -0
- package/dist/store/meta.js +137 -0
- package/dist/store/query.d.ts +5 -0
- package/dist/store/query.js +4 -0
- package/dist/store/schema.d.ts +4 -0
- package/dist/store/schema.js +19 -0
- package/dist/store/store.d.ts +36 -0
- package/dist/store/store.js +240 -0
- package/dist/ts7/api.d.ts +35 -0
- package/dist/ts7/api.js +164 -0
- package/dist/ts7/ast.d.ts +1 -0
- package/dist/ts7/ast.js +1 -0
- package/dist/ts7/checker.d.ts +55 -0
- package/dist/ts7/checker.js +383 -0
- package/dist/ts7/projects.d.ts +27 -0
- package/dist/ts7/projects.js +94 -0
- package/package.json +47 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { appendFileSync, chmodSync, existsSync, openSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { discoverRoot, ensureStateDirectory, idleSeconds, pathsForRoot, publishState, removeEndpoint } from './lifecycle.js';
|
|
6
|
+
import { NdjsonReader, WIRE_VERSION, writeFrame } from './wire.js';
|
|
7
|
+
import { createSemanticContext } from './context.js';
|
|
8
|
+
import { bundledTypeScriptVersion, Ts7ServerDiedError, TypeScriptBinaryError } from '../ts7/api.js';
|
|
9
|
+
import { buildIndex, reuseIndex } from '../index/build.js';
|
|
10
|
+
import { ingestCommits } from '../index/commits.js';
|
|
11
|
+
import { refreshIndex } from '../index/refresh.js';
|
|
12
|
+
import { IndexStateController } from '../index/state.js';
|
|
13
|
+
import { watchIndex } from '../index/watch.js';
|
|
14
|
+
import { canReuse, hasSourceDrift, metadataFor, readMetadata, writeMetadata } from '../store/meta.js';
|
|
15
|
+
import { EngineBinaryError, Store } from '../store/store.js';
|
|
16
|
+
import { executeSemantic } from '../commands/handlers.js';
|
|
17
|
+
import { findNode } from '../commands/tree.js';
|
|
18
|
+
class DeadlineExceeded extends Error {
|
|
19
|
+
phase;
|
|
20
|
+
constructor(phase) {
|
|
21
|
+
super(`${phase} deadline exceeded`);
|
|
22
|
+
this.phase = phase;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function main() {
|
|
26
|
+
const root = discoverRoot(arg('--root'));
|
|
27
|
+
const paths = pathsForRoot(root);
|
|
28
|
+
ensureStateDirectory(paths);
|
|
29
|
+
const logFd = openSync(paths.log, 'a', 0o600);
|
|
30
|
+
let database;
|
|
31
|
+
try {
|
|
32
|
+
database = new DatabaseSync(paths.lock);
|
|
33
|
+
database.exec('BEGIN EXCLUSIVE');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (isBusy(error))
|
|
37
|
+
process.exit(0);
|
|
38
|
+
append(paths, `lock failure: ${safe(error)}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const forceRebuild = process.argv.includes('--rebuild');
|
|
42
|
+
let semantic;
|
|
43
|
+
let store;
|
|
44
|
+
let index;
|
|
45
|
+
let watcher;
|
|
46
|
+
const bootedAt = new Date();
|
|
47
|
+
const bootedAtMonotonic = performance.now();
|
|
48
|
+
let bootstrapped = false;
|
|
49
|
+
let loading = false;
|
|
50
|
+
let stopping = false;
|
|
51
|
+
let retired = false;
|
|
52
|
+
let active;
|
|
53
|
+
let queue = [];
|
|
54
|
+
let currentCommand;
|
|
55
|
+
let loadDeadlineAt;
|
|
56
|
+
let activeDeadlineAt;
|
|
57
|
+
let activeTimer;
|
|
58
|
+
let idleTimer;
|
|
59
|
+
let listenerClosing = false;
|
|
60
|
+
const sockets = new Set();
|
|
61
|
+
const invokeSockets = new Map();
|
|
62
|
+
const stopRequests = [];
|
|
63
|
+
const closeProgram = () => {
|
|
64
|
+
try {
|
|
65
|
+
semantic?.program.close();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
append(paths, `TypeScript 7 shutdown failure: ${safe(error)}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const publishedState = (snapshot = index?.current()) => ({
|
|
72
|
+
root,
|
|
73
|
+
pid: process.pid,
|
|
74
|
+
bootedAt: bootedAt.toISOString(),
|
|
75
|
+
port: paths.port,
|
|
76
|
+
log: paths.log,
|
|
77
|
+
idleDeadline: new Date(Date.now() + idleSeconds() * 1000).toISOString(),
|
|
78
|
+
state: stopping ? 'stopping' : 'ready',
|
|
79
|
+
index: snapshot === undefined ? undefined : snapshot.state === 'current' ? 'current' : 'stale',
|
|
80
|
+
declarations: snapshot?.built.receipt.declarations,
|
|
81
|
+
});
|
|
82
|
+
const server = createServer((socket) => {
|
|
83
|
+
sockets.add(socket);
|
|
84
|
+
const reader = new NdjsonReader();
|
|
85
|
+
socket.on('data', (chunk) => {
|
|
86
|
+
for (const value of reader.push(chunk))
|
|
87
|
+
void receive(value, socket);
|
|
88
|
+
});
|
|
89
|
+
socket.on('close', () => {
|
|
90
|
+
sockets.delete(socket);
|
|
91
|
+
invokeSockets.delete(socket);
|
|
92
|
+
const queued = queue.find((item) => item.socket === socket);
|
|
93
|
+
if (queued)
|
|
94
|
+
removeQueued(queued);
|
|
95
|
+
});
|
|
96
|
+
socket.on('error', () => { });
|
|
97
|
+
});
|
|
98
|
+
const touch = () => {
|
|
99
|
+
if (idleTimer)
|
|
100
|
+
clearTimeout(idleTimer);
|
|
101
|
+
idleTimer = setTimeout(() => {
|
|
102
|
+
if (!active && queue.length === 0)
|
|
103
|
+
retire();
|
|
104
|
+
}, idleSeconds() * 1000);
|
|
105
|
+
idleTimer.unref();
|
|
106
|
+
};
|
|
107
|
+
const remaining = (deadlineAt) => Math.max(0, Math.ceil((deadlineAt ?? performance.now()) - performance.now()));
|
|
108
|
+
const sendPhase = (socket, requestId, phase, remainingMs, command) => {
|
|
109
|
+
if (!socket.destroyed)
|
|
110
|
+
send(socket, { type: 'phase', requestId, phase, remainingMs, command });
|
|
111
|
+
};
|
|
112
|
+
const toolError = (code, message, next) => ({ category: 'tool', code, message, logPath: paths.log, next });
|
|
113
|
+
const respond = (socket, requestId, completion) => {
|
|
114
|
+
if (!socket.destroyed)
|
|
115
|
+
send(socket, { type: 'response', requestId, completion: 'category' in completion ? { error: completion } : { answer: completion } });
|
|
116
|
+
};
|
|
117
|
+
const finishSocket = (socket) => { if (!socket.destroyed)
|
|
118
|
+
socket.end(); };
|
|
119
|
+
const removeQueued = (queued) => {
|
|
120
|
+
if (queued.queueTimer)
|
|
121
|
+
clearTimeout(queued.queueTimer);
|
|
122
|
+
queue = queue.filter((item) => item !== queued);
|
|
123
|
+
};
|
|
124
|
+
const pauseQueueDeadline = (queued) => {
|
|
125
|
+
if (queued.queueTimer)
|
|
126
|
+
clearTimeout(queued.queueTimer);
|
|
127
|
+
queued.queueTimer = undefined;
|
|
128
|
+
if (queued.queueStartedAt !== undefined)
|
|
129
|
+
queued.remainingQueueMs = Math.max(0, queued.remainingQueueMs - (performance.now() - queued.queueStartedAt));
|
|
130
|
+
queued.queueStartedAt = undefined;
|
|
131
|
+
};
|
|
132
|
+
const queueTimeout = (queued) => {
|
|
133
|
+
if (retired || loading || !queue.includes(queued))
|
|
134
|
+
return;
|
|
135
|
+
removeQueued(queued);
|
|
136
|
+
respond(queued.socket, queued.request.requestId, toolError('server-unavailable', 'The resident did not begin this request within its admission deadline.', 'Retry after the current request completes.'));
|
|
137
|
+
finishSocket(queued.socket);
|
|
138
|
+
void processQueue();
|
|
139
|
+
};
|
|
140
|
+
const armQueueDeadline = (queued) => {
|
|
141
|
+
if (retired || loading || !queue.includes(queued))
|
|
142
|
+
return;
|
|
143
|
+
queued.queueStartedAt = performance.now();
|
|
144
|
+
queued.queueTimer = setTimeout(() => queueTimeout(queued), queued.remainingQueueMs);
|
|
145
|
+
queued.queueTimer.unref();
|
|
146
|
+
sendPhase(queued.socket, queued.request.requestId, 'queued', queued.remainingQueueMs);
|
|
147
|
+
};
|
|
148
|
+
const failQueued = (queued, completion) => {
|
|
149
|
+
removeQueued(queued);
|
|
150
|
+
respond(queued.socket, queued.request.requestId, completion);
|
|
151
|
+
finishSocket(queued.socket);
|
|
152
|
+
};
|
|
153
|
+
const exitAfterCleanup = () => {
|
|
154
|
+
if (listenerClosing)
|
|
155
|
+
return;
|
|
156
|
+
listenerClosing = true;
|
|
157
|
+
server.close(() => process.exit(0));
|
|
158
|
+
};
|
|
159
|
+
const finishRetirement = async (preserve) => {
|
|
160
|
+
watcher?.close();
|
|
161
|
+
closeProgram();
|
|
162
|
+
store?.close();
|
|
163
|
+
removeEndpoint(paths);
|
|
164
|
+
try {
|
|
165
|
+
database.close();
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
for (const socket of sockets)
|
|
169
|
+
if (!preserve.has(socket))
|
|
170
|
+
socket.destroy();
|
|
171
|
+
exitAfterCleanup();
|
|
172
|
+
};
|
|
173
|
+
const retire = (preserve = new Set()) => {
|
|
174
|
+
if (retired)
|
|
175
|
+
return;
|
|
176
|
+
retired = true;
|
|
177
|
+
if (idleTimer)
|
|
178
|
+
clearTimeout(idleTimer);
|
|
179
|
+
if (activeTimer)
|
|
180
|
+
clearTimeout(activeTimer);
|
|
181
|
+
for (const queued of queue)
|
|
182
|
+
if (queued.queueTimer)
|
|
183
|
+
clearTimeout(queued.queueTimer);
|
|
184
|
+
queue = [];
|
|
185
|
+
void finishRetirement(preserve);
|
|
186
|
+
};
|
|
187
|
+
const completeStop = () => {
|
|
188
|
+
if (retired || !stopping || active)
|
|
189
|
+
return;
|
|
190
|
+
retired = true;
|
|
191
|
+
void finishStop();
|
|
192
|
+
};
|
|
193
|
+
const finishStop = async () => {
|
|
194
|
+
if (idleTimer)
|
|
195
|
+
clearTimeout(idleTimer);
|
|
196
|
+
watcher?.close();
|
|
197
|
+
closeProgram();
|
|
198
|
+
store?.close();
|
|
199
|
+
removeEndpoint(paths);
|
|
200
|
+
try {
|
|
201
|
+
database.close();
|
|
202
|
+
}
|
|
203
|
+
catch { }
|
|
204
|
+
for (const stop of stopRequests) {
|
|
205
|
+
respond(stop.socket, stop.requestId, { data: { element: 'server', attributes: { root, state: 'stopping' } }, count: 1 });
|
|
206
|
+
finishSocket(stop.socket);
|
|
207
|
+
}
|
|
208
|
+
for (const socket of sockets)
|
|
209
|
+
if (!stopRequests.some((stop) => stop.socket === socket))
|
|
210
|
+
socket.destroy();
|
|
211
|
+
exitAfterCleanup();
|
|
212
|
+
};
|
|
213
|
+
const projectLoadTimeout = () => {
|
|
214
|
+
if (retired)
|
|
215
|
+
return;
|
|
216
|
+
const completion = toolError('project-load-timeout', 'TypeScript did not load the workspace project before its deadline.', 'Repair the project, then retry.');
|
|
217
|
+
if (active) {
|
|
218
|
+
respond(active.socket, active.request.requestId, completion);
|
|
219
|
+
finishSocket(active.socket);
|
|
220
|
+
}
|
|
221
|
+
const preserving = new Set([...queue.map((queued) => queued.socket), ...(active ? [active.socket] : [])]);
|
|
222
|
+
for (const queued of queue)
|
|
223
|
+
failQueued(queued, completion);
|
|
224
|
+
retire(preserving);
|
|
225
|
+
};
|
|
226
|
+
const requestTimeout = () => {
|
|
227
|
+
if (retired || !active)
|
|
228
|
+
return;
|
|
229
|
+
const detail = currentCommand ?? 'no semantic operation started';
|
|
230
|
+
respond(active.socket, active.request.requestId, { ...toolError('request-timeout', 'The semantic request exceeded its deadline.', 'Retry after repairing the workspace or narrowing the command.'), received: detail });
|
|
231
|
+
finishSocket(active.socket);
|
|
232
|
+
retire(new Set([active.socket]));
|
|
233
|
+
};
|
|
234
|
+
const armActiveDeadline = () => {
|
|
235
|
+
activeDeadlineAt = performance.now() + 120_000;
|
|
236
|
+
activeTimer = setTimeout(requestTimeout, 120_000);
|
|
237
|
+
activeTimer.unref();
|
|
238
|
+
if (active)
|
|
239
|
+
sendPhase(active.socket, active.request.requestId, 'running', 120_000);
|
|
240
|
+
};
|
|
241
|
+
const loadProjects = async () => {
|
|
242
|
+
loading = true;
|
|
243
|
+
loadDeadlineAt = performance.now() + 60_000;
|
|
244
|
+
if (active)
|
|
245
|
+
sendPhase(active.socket, active.request.requestId, 'project-load-start', 60_000);
|
|
246
|
+
for (const queued of queue) {
|
|
247
|
+
pauseQueueDeadline(queued);
|
|
248
|
+
sendPhase(queued.socket, queued.request.requestId, 'project-load-start', 60_000);
|
|
249
|
+
}
|
|
250
|
+
let timedOut = false;
|
|
251
|
+
let deadlineTimer;
|
|
252
|
+
try {
|
|
253
|
+
await Promise.race([
|
|
254
|
+
(async () => {
|
|
255
|
+
semantic ??= createSemanticContext(root);
|
|
256
|
+
const { program, projects } = semantic;
|
|
257
|
+
await projects.bootstrap();
|
|
258
|
+
store = new Store(paths.store);
|
|
259
|
+
if (active)
|
|
260
|
+
sendPhase(active.socket, active.request.requestId, 'index-build-start', remaining(loadDeadlineAt));
|
|
261
|
+
// The watcher starts before the build, not after it: buildIndex takes tens of seconds on a large workspace,
|
|
262
|
+
// and an edit landing in that window would otherwise never be recorded, publishing a stale index as current.
|
|
263
|
+
// buildIndex is synchronous, so no watch callback can run until index is assigned below.
|
|
264
|
+
watcher = watchIndex(root, projects.configured(), refresh, (error) => append(paths, `index refresh failure: ${safe(error)}`), (flush) => index?.markStale(flush));
|
|
265
|
+
const configured = projects.configured();
|
|
266
|
+
const expected = metadataFor(root, configured, headCommit(root));
|
|
267
|
+
const metadata = readMetadata(store.path);
|
|
268
|
+
let built;
|
|
269
|
+
if (!forceRebuild && canReuse(metadata, expected)) {
|
|
270
|
+
const reused = reuseIndex(store);
|
|
271
|
+
built = hasSourceDrift(metadata, expected)
|
|
272
|
+
? refreshIndex(root, program, configured, store, reused, { fileChanges: { invalidateAll: true } }).built
|
|
273
|
+
: reused;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
built = buildIndex(root, program, configured, store);
|
|
277
|
+
}
|
|
278
|
+
index = new IndexStateController({ indexedAt: indexedAt(root), state: 'current', built }, (snapshot) => {
|
|
279
|
+
semantic.index = snapshot;
|
|
280
|
+
publishState(paths, publishedState(snapshot));
|
|
281
|
+
});
|
|
282
|
+
semantic.store = store;
|
|
283
|
+
if (active)
|
|
284
|
+
sendPhase(active.socket, active.request.requestId, 'index-build-end', remaining(loadDeadlineAt));
|
|
285
|
+
})(),
|
|
286
|
+
new Promise((_, reject) => { deadlineTimer = setTimeout(() => { timedOut = true; reject(new DeadlineExceeded('project-load')); }, 60_000); deadlineTimer.unref(); }),
|
|
287
|
+
]);
|
|
288
|
+
bootstrapped = true;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
if (deadlineTimer)
|
|
292
|
+
clearTimeout(deadlineTimer);
|
|
293
|
+
loading = false;
|
|
294
|
+
loadDeadlineAt = undefined;
|
|
295
|
+
if (!timedOut && !retired) {
|
|
296
|
+
if (active)
|
|
297
|
+
sendPhase(active.socket, active.request.requestId, 'project-load-end', 60_000);
|
|
298
|
+
for (const queued of queue) {
|
|
299
|
+
sendPhase(queued.socket, queued.request.requestId, 'project-load-end', 60_000);
|
|
300
|
+
armQueueDeadline(queued);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const processQueue = async () => {
|
|
306
|
+
if (active || retired)
|
|
307
|
+
return;
|
|
308
|
+
if (stopping) {
|
|
309
|
+
completeStop();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const queued = queue.shift();
|
|
313
|
+
if (!queued) {
|
|
314
|
+
touch();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (queued.queueTimer)
|
|
318
|
+
clearTimeout(queued.queueTimer);
|
|
319
|
+
if (queued.socket.destroyed) {
|
|
320
|
+
void processQueue();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
active = queued;
|
|
324
|
+
currentCommand = undefined;
|
|
325
|
+
sendPhase(queued.socket, queued.request.requestId, 'running', 120_000);
|
|
326
|
+
try {
|
|
327
|
+
if (!bootstrapped)
|
|
328
|
+
await loadProjects();
|
|
329
|
+
if (retired)
|
|
330
|
+
return;
|
|
331
|
+
armActiveDeadline();
|
|
332
|
+
const requestInvocation = queued.request.invocation;
|
|
333
|
+
if (!requestInvocation)
|
|
334
|
+
throw new Error('invoke request omitted its invocation');
|
|
335
|
+
currentCommand = requestInvocation.path.join(' ');
|
|
336
|
+
const declaration = findNode(requestInvocation.path).node;
|
|
337
|
+
if (readsIndex(declaration.handler))
|
|
338
|
+
await index?.wait();
|
|
339
|
+
const completion = await executeSemantic(semantic, { root, cwd: requestInvocation.cwd, json: false, path: requestInvocation.path, declaration, input: { args: requestInvocation.args, flags: requestInvocation.flags, explicit: new Set(requestInvocation.explicit) } });
|
|
340
|
+
if ('category' in completion && completion.category === 'tool' && !completion.logPath)
|
|
341
|
+
completion.logPath = paths.log;
|
|
342
|
+
if (!retired)
|
|
343
|
+
respond(queued.socket, queued.request.requestId, completion);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (error instanceof DeadlineExceeded && error.phase === 'project-load') {
|
|
347
|
+
projectLoadTimeout();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (error instanceof Ts7ServerDiedError) {
|
|
351
|
+
if (!retired)
|
|
352
|
+
respond(queued.socket, queued.request.requestId, toolError('server-died', 'The bundled TypeScript 7 process stopped responding after one recovery attempt.', 'Retry to start a fresh resident.'));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (error instanceof EngineBinaryError || error instanceof TypeScriptBinaryError) {
|
|
356
|
+
if (!retired)
|
|
357
|
+
respond(queued.socket, queued.request.requestId, { category: 'tool', code: error.code, attributes: { platform: error.platform }, message: error.message });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!retired)
|
|
361
|
+
respond(queued.socket, queued.request.requestId, toolError('project-load-failed', 'TypeScript could not load the workspace project.', error instanceof Error ? error.message : 'Read the log and repair the project.'));
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (activeTimer)
|
|
365
|
+
clearTimeout(activeTimer);
|
|
366
|
+
activeTimer = undefined;
|
|
367
|
+
activeDeadlineAt = undefined;
|
|
368
|
+
if (active === queued)
|
|
369
|
+
active = undefined;
|
|
370
|
+
if (!retired) {
|
|
371
|
+
if (stopping)
|
|
372
|
+
completeStop();
|
|
373
|
+
else
|
|
374
|
+
void processQueue();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const refresh = async (batch) => {
|
|
379
|
+
if (!existsSync(root)) {
|
|
380
|
+
append(paths, `workspace root disappeared; stopping resident root=${root}`);
|
|
381
|
+
retire();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!store || !index)
|
|
385
|
+
throw new Error('The index refresh ran before startup completed.');
|
|
386
|
+
await index.rebuild(async () => {
|
|
387
|
+
if (!semantic)
|
|
388
|
+
throw new Error('The index refresh ran before TypeScript initialized.');
|
|
389
|
+
const configured = semantic.projects.configured();
|
|
390
|
+
const refreshed = refreshIndex(root, semantic.program, configured, store, index.current().built, {
|
|
391
|
+
fileChanges: batch.fileChanges,
|
|
392
|
+
full: batch.full,
|
|
393
|
+
validate: process.env.TSYM_VALIDATE_INDEX === '1',
|
|
394
|
+
});
|
|
395
|
+
if (batch.git) {
|
|
396
|
+
const commit = headCommit(root);
|
|
397
|
+
ingestCommits(root, store, [commit]);
|
|
398
|
+
writeMetadata(store.path, metadataFor(root, configured, commit, refreshed.built.receipt.durationMs));
|
|
399
|
+
}
|
|
400
|
+
append(paths, `index refresh stored=${refreshed.stored} changedFiles=${refreshed.changedFiles.length} duration=${Math.round(refreshed.built.receipt.durationMs)}ms`);
|
|
401
|
+
return { indexedAt: indexedAt(root), state: 'current', built: refreshed.built };
|
|
402
|
+
});
|
|
403
|
+
if (batch.full)
|
|
404
|
+
watcher?.refresh();
|
|
405
|
+
};
|
|
406
|
+
const receive = async (value, socket) => {
|
|
407
|
+
const request = value;
|
|
408
|
+
if (request.version !== WIRE_VERSION || request.root !== root || typeof request.requestId !== 'string' || !request.operation) {
|
|
409
|
+
respond(socket, typeof request.requestId === 'string' ? request.requestId : '', toolError('server-unavailable', 'The resident received an invalid wire request.', 'Retry with the installed tsym CLI.'));
|
|
410
|
+
finishSocket(socket);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (retired) {
|
|
414
|
+
respond(socket, request.requestId, toolError('server-unavailable', 'The resident is retiring.', 'Wait for the next resident before retrying.'));
|
|
415
|
+
finishSocket(socket);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (request.operation === 'status' && forceRebuild && !bootstrapped) {
|
|
419
|
+
try {
|
|
420
|
+
await loadProjects();
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
if (error instanceof EngineBinaryError || error instanceof TypeScriptBinaryError)
|
|
424
|
+
respond(socket, request.requestId, { category: 'tool', code: error.code, attributes: { platform: error.platform }, message: error.message });
|
|
425
|
+
else
|
|
426
|
+
respond(socket, request.requestId, toolError('project-load-failed', 'TypeScript could not load the workspace project.', error instanceof Error ? error.message : 'Read the log and repair the project.'));
|
|
427
|
+
finishSocket(socket);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (request.operation === 'status') {
|
|
432
|
+
let memory;
|
|
433
|
+
try {
|
|
434
|
+
memory = residentMemory(semantic?.program.pid);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
respond(socket, request.requestId, toolError('server-unavailable', 'The resident could not measure its process memory.', error instanceof Error ? error.message : 'Read the server log.'));
|
|
438
|
+
finishSocket(socket);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const state = stopping ? 'stopping' : 'ready';
|
|
442
|
+
const snapshot = index?.current();
|
|
443
|
+
const receipt = snapshot?.built.receipt;
|
|
444
|
+
const indexedCommit = readMetadata(paths.store)?.indexedCommit;
|
|
445
|
+
respond(socket, request.requestId, { data: { element: 'server', attributes: { root, state, pid: process.pid, port: paths.port, log: paths.log, idle: `${idleSeconds()}s`, uptime: `${Math.floor(performance.now() - bootedAtMonotonic)}ms`, memory, typescript: semantic ? bundledTypeScriptVersion() : undefined, store: paths.store, indexedCommit, declarations: receipt?.declarations, edges: receipt ? Object.values(receipt.relationships).reduce((total, count) => total + count, 0) : undefined, notInProgram: semantic ? unconfiguredFileCount(root, semantic.projects.records()) : undefined, index: snapshot?.state, indexDuration: receipt ? `${Math.round(receipt.durationMs)}ms` : undefined } }, count: 1 });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (request.operation === 'stop') {
|
|
449
|
+
stopRequests.push({ requestId: request.requestId, socket });
|
|
450
|
+
if (!stopping) {
|
|
451
|
+
stopping = true;
|
|
452
|
+
if (idleTimer)
|
|
453
|
+
clearTimeout(idleTimer);
|
|
454
|
+
publishState(paths, publishedState());
|
|
455
|
+
for (const [invokeSocket, requestId] of invokeSockets) {
|
|
456
|
+
const deadlineAt = active?.socket === invokeSocket ? (loading ? loadDeadlineAt : activeDeadlineAt) : undefined;
|
|
457
|
+
sendPhase(invokeSocket, requestId, 'stopping', deadlineAt === undefined ? undefined : remaining(deadlineAt));
|
|
458
|
+
}
|
|
459
|
+
const completion = toolError('server-unavailable', 'The resident is stopping.', 'Wait for stop to complete before invoking tsym.');
|
|
460
|
+
for (const queued of [...queue])
|
|
461
|
+
failQueued(queued, completion);
|
|
462
|
+
}
|
|
463
|
+
completeStop();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (stopping) {
|
|
467
|
+
respond(socket, request.requestId, toolError('server-unavailable', 'The resident is stopping.', 'Wait for stop to complete before invoking tsym.'));
|
|
468
|
+
finishSocket(socket);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
touch();
|
|
472
|
+
invokeSockets.set(socket, request.requestId);
|
|
473
|
+
const queued = { request: request, socket, remainingQueueMs: 30_000 };
|
|
474
|
+
sendPhase(socket, queued.request.requestId, 'accepted', 30_000);
|
|
475
|
+
queue.push(queued);
|
|
476
|
+
if (loading) {
|
|
477
|
+
sendPhase(socket, queued.request.requestId, 'queued', queued.remainingQueueMs);
|
|
478
|
+
sendPhase(socket, queued.request.requestId, 'project-load-start', remaining(loadDeadlineAt));
|
|
479
|
+
}
|
|
480
|
+
else
|
|
481
|
+
armQueueDeadline(queued);
|
|
482
|
+
void processQueue();
|
|
483
|
+
};
|
|
484
|
+
removeEndpoint(paths);
|
|
485
|
+
server.listen(paths.port, () => {
|
|
486
|
+
chmodSync(paths.port, 0o600);
|
|
487
|
+
publishState(paths, publishedState());
|
|
488
|
+
append(paths, `ready pid=${process.pid} root=${root}`);
|
|
489
|
+
touch();
|
|
490
|
+
});
|
|
491
|
+
server.on('error', (error) => { append(paths, `socket failure: ${safe(error)}`); retire(); });
|
|
492
|
+
process.on('SIGTERM', retire);
|
|
493
|
+
process.on('SIGINT', retire);
|
|
494
|
+
void logFd;
|
|
495
|
+
}
|
|
496
|
+
function send(socket, frame) { writeFrame(socket, frame); }
|
|
497
|
+
function arg(name) { const index = process.argv.indexOf(name); return index < 0 ? undefined : process.argv[index + 1]; }
|
|
498
|
+
function isBusy(error) { return error.code === 'ERR_SQLITE_ERROR' && error.errcode === 5 || String(error).includes('database is locked'); }
|
|
499
|
+
function append(paths, message) { appendFileSync(paths.log, `${new Date().toISOString()} ${message}\n`, { mode: 0o600 }); }
|
|
500
|
+
function safe(error) { return error instanceof Error ? error.message : String(error); }
|
|
501
|
+
function readsIndex(handler) {
|
|
502
|
+
return handler === 'outline' || handler === 'find' || handler === 'show' || handler === 'inspect' || handler === 'query';
|
|
503
|
+
}
|
|
504
|
+
function indexedAt(root) {
|
|
505
|
+
return `${headCommit(root).slice(0, 7)} ${new Date().toISOString()}`;
|
|
506
|
+
}
|
|
507
|
+
function headCommit(root) {
|
|
508
|
+
try {
|
|
509
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return 'unknown';
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function residentMemory(tsgoPid) {
|
|
516
|
+
const pids = tsgoPid === undefined ? [process.pid] : [process.pid, tsgoPid];
|
|
517
|
+
const rss = execFileSync('ps', ['-o', 'rss=', '-p', pids.join(',')], { encoding: 'utf8' }).trim().split(/\s+/).filter(Boolean).map(Number);
|
|
518
|
+
if (rss.length !== pids.length || rss.some((value) => !Number.isFinite(value)))
|
|
519
|
+
throw new Error('ps did not report every resident process');
|
|
520
|
+
return rss.reduce((total, value) => total + value, 0) * 1024;
|
|
521
|
+
}
|
|
522
|
+
function unconfiguredFileCount(root, records) {
|
|
523
|
+
const configured = new Set(records.flatMap((record) => [...record.files]));
|
|
524
|
+
const extensions = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
525
|
+
const skipped = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
|
|
526
|
+
let count = 0;
|
|
527
|
+
const visit = (directory) => {
|
|
528
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
529
|
+
const file = `${directory}/${entry.name}`;
|
|
530
|
+
if (entry.isDirectory()) {
|
|
531
|
+
if (!skipped.has(entry.name))
|
|
532
|
+
visit(file);
|
|
533
|
+
}
|
|
534
|
+
else if (entry.isFile() && extensions.has(file.slice(file.lastIndexOf('.'))) && !configured.has(file))
|
|
535
|
+
count++;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
visit(root);
|
|
539
|
+
return count;
|
|
540
|
+
}
|
|
541
|
+
void main();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
2
|
+
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
3
|
+
export interface ProjectRecord {
|
|
4
|
+
config: string;
|
|
5
|
+
files: ReadonlySet<string>;
|
|
6
|
+
bootstrap: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** Registry of configured TypeScript 7 projects. */
|
|
9
|
+
export declare class ProjectRegistry {
|
|
10
|
+
readonly root: string;
|
|
11
|
+
private readonly projects;
|
|
12
|
+
private bootstrapped;
|
|
13
|
+
constructor(root: string, program?: Ts7Host);
|
|
14
|
+
bootstrap(): Promise<readonly ProjectRecord[]>;
|
|
15
|
+
membership(file: string): Promise<ProjectRecord | null>;
|
|
16
|
+
refreshFile(file: string): void;
|
|
17
|
+
sourceFile(file: string): import("typescript/unstable/ast").SourceFile | undefined;
|
|
18
|
+
anySourceFile(file: string): import("typescript/unstable/ast").SourceFile | undefined;
|
|
19
|
+
records(): readonly ProjectRecord[];
|
|
20
|
+
configured(): ConfiguredProjects;
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
2
|
+
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
3
|
+
/** Registry of configured TypeScript 7 projects. */
|
|
4
|
+
export class ProjectRegistry {
|
|
5
|
+
root;
|
|
6
|
+
projects;
|
|
7
|
+
bootstrapped = false;
|
|
8
|
+
constructor(root, program = new Ts7Host(root)) {
|
|
9
|
+
this.root = root;
|
|
10
|
+
this.projects = new ConfiguredProjects(root, program);
|
|
11
|
+
}
|
|
12
|
+
async bootstrap() {
|
|
13
|
+
if (!this.bootstrapped) {
|
|
14
|
+
this.projects.load();
|
|
15
|
+
this.bootstrapped = true;
|
|
16
|
+
}
|
|
17
|
+
return this.projects.records();
|
|
18
|
+
}
|
|
19
|
+
async membership(file) {
|
|
20
|
+
await this.bootstrap();
|
|
21
|
+
return this.projects.membership(file);
|
|
22
|
+
}
|
|
23
|
+
refreshFile(file) { this.projects.refreshFile(file); }
|
|
24
|
+
sourceFile(file) { return this.projects.sourceFile(file); }
|
|
25
|
+
anySourceFile(file) { return this.projects.anySourceFile(file); }
|
|
26
|
+
records() { return this.projects.records(); }
|
|
27
|
+
configured() { return this.projects; }
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Answer, Invocation, InvocationError, ToolError } from '../commands/types.js';
|
|
2
|
+
export declare const WIRE_VERSION = 1;
|
|
3
|
+
export type Operation = 'invoke' | 'status' | 'stop';
|
|
4
|
+
export interface WireInvocation {
|
|
5
|
+
path: string[];
|
|
6
|
+
cwd: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
flags: Invocation['input']['flags'];
|
|
9
|
+
explicit: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface WireRequest {
|
|
12
|
+
version: typeof WIRE_VERSION;
|
|
13
|
+
requestId: string;
|
|
14
|
+
root: string;
|
|
15
|
+
operation: Operation;
|
|
16
|
+
invocation?: WireInvocation;
|
|
17
|
+
}
|
|
18
|
+
export interface PhaseFrame {
|
|
19
|
+
type: 'phase';
|
|
20
|
+
requestId: string;
|
|
21
|
+
phase: 'accepted' | 'queued' | 'running' | 'project-load-start' | 'project-load-end' | 'index-build-start' | 'index-build-end' | 'command-start' | 'stopping';
|
|
22
|
+
remainingMs?: number;
|
|
23
|
+
command?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ResponseFrame {
|
|
26
|
+
type: 'response';
|
|
27
|
+
requestId: string;
|
|
28
|
+
completion: {
|
|
29
|
+
answer?: Answer;
|
|
30
|
+
error?: InvocationError | ToolError;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export type WireFrame = PhaseFrame | ResponseFrame;
|
|
34
|
+
export declare function request(root: string, operation: Operation, invocation?: Invocation): WireRequest;
|
|
35
|
+
export declare function writeFrame(stream: NodeJS.WritableStream, frame: WireRequest | WireFrame): void;
|
|
36
|
+
export declare class NdjsonReader {
|
|
37
|
+
private buffered;
|
|
38
|
+
push(chunk: Buffer | string): unknown[];
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export const WIRE_VERSION = 1;
|
|
3
|
+
export function request(root, operation, invocation) {
|
|
4
|
+
return { version: WIRE_VERSION, requestId: randomUUID(), root, operation, invocation: invocation ? { path: invocation.path, cwd: invocation.cwd, args: invocation.input.args, flags: invocation.input.flags, explicit: [...invocation.input.explicit] } : undefined };
|
|
5
|
+
}
|
|
6
|
+
export function writeFrame(stream, frame) { stream.write(JSON.stringify(frame) + '\n'); }
|
|
7
|
+
export class NdjsonReader {
|
|
8
|
+
buffered = '';
|
|
9
|
+
push(chunk) {
|
|
10
|
+
this.buffered += chunk.toString();
|
|
11
|
+
const lines = this.buffered.split('\n');
|
|
12
|
+
this.buffered = lines.pop();
|
|
13
|
+
const values = [];
|
|
14
|
+
for (const line of lines)
|
|
15
|
+
if (line)
|
|
16
|
+
values.push(JSON.parse(line));
|
|
17
|
+
return values;
|
|
18
|
+
}
|
|
19
|
+
}
|