@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,158 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { matchesPathFilters } from '../core/receipt.js';
|
|
3
|
+
import { query } from '../store/query.js';
|
|
4
|
+
import { graphAfterExclusion, GraphBuilder, projectGraph } from './graph.js';
|
|
5
|
+
import { nodeFor } from './types.js';
|
|
6
|
+
export async function references(context, record, filters) {
|
|
7
|
+
const rows = storeRows(context, 'MATCH (owner:Declaration)-[reference:REFERENCES]->(target:Declaration) WHERE target.key = $key RETURN owner.file AS file, owner.kind AS kind, owner.qname AS qname, reference.line AS line, reference.col AS col, reference.role AS role', { key: recordKey(context, record) });
|
|
8
|
+
return rows.flatMap((row) => {
|
|
9
|
+
const file = text(row, 'file');
|
|
10
|
+
if (!matchesPathFilters(file, filters))
|
|
11
|
+
return [];
|
|
12
|
+
const kind = text(row, 'kind');
|
|
13
|
+
const qualifiedName = kind === 'module' ? path.basename(file) : text(row, 'qname');
|
|
14
|
+
return [{ path: file, line: number(row, 'line'), col: number(row, 'col'), kind, qualifiedName, role: text(row, 'role') }];
|
|
15
|
+
}).sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.col - b.col);
|
|
16
|
+
}
|
|
17
|
+
export async function implementations(context, record) {
|
|
18
|
+
const key = recordKey(context, record);
|
|
19
|
+
const rows = [
|
|
20
|
+
...storeRows(context, 'MATCH (impl:Declaration)-[:IMPLEMENTS]->(target:Declaration) WHERE target.key = $key RETURN DISTINCT impl.key AS key, impl.file AS file, impl.line AS declarationLine, impl.col AS declarationCol, impl.name AS name, impl.qname AS qname, impl.kind AS kind', { key }),
|
|
21
|
+
...storeRows(context, 'MATCH (impl:Declaration)-[:EXTENDS]->(target:Declaration) WHERE target.key = $key RETURN DISTINCT impl.key AS key, impl.file AS file, impl.line AS declarationLine, impl.col AS declarationCol, impl.name AS name, impl.qname AS qname, impl.kind AS kind', { key }),
|
|
22
|
+
];
|
|
23
|
+
return resolveRows(context, rows);
|
|
24
|
+
}
|
|
25
|
+
export async function traverse(context, seeds, direction, depth, filters) {
|
|
26
|
+
const builder = new GraphBuilder();
|
|
27
|
+
const keys = new Map();
|
|
28
|
+
for (const seed of seeds) {
|
|
29
|
+
builder.addNode(nodeFor(seed, 0));
|
|
30
|
+
keys.set(seed.address, recordKey(context, seed));
|
|
31
|
+
}
|
|
32
|
+
const roots = direction === 'callers' ? await incomingRoots(context, seeds, keys) : seeds;
|
|
33
|
+
for (const root of roots)
|
|
34
|
+
builder.addNode(nodeFor(root, 0));
|
|
35
|
+
const seen = new Set(roots.map((root) => root.address));
|
|
36
|
+
let frontier = [...roots];
|
|
37
|
+
for (let level = 1; level <= depth && frontier.length; level++) {
|
|
38
|
+
const next = [];
|
|
39
|
+
for (const current of frontier) {
|
|
40
|
+
const found = direction === 'calls' ? await outgoing(context, current, keys) : await incoming(context, current, keys);
|
|
41
|
+
for (const unresolved of found.unresolved)
|
|
42
|
+
builder.addUnresolved(unresolved);
|
|
43
|
+
for (const edge of found.edges) {
|
|
44
|
+
builder.addNode(nodeFor(edge.record, level));
|
|
45
|
+
builder.addEdge(edge.from, edge.to, edge.kind, edge.siteId);
|
|
46
|
+
if (!seen.has(edge.record.address)) {
|
|
47
|
+
seen.add(edge.record.address);
|
|
48
|
+
next.push(edge.record);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
frontier = next;
|
|
53
|
+
}
|
|
54
|
+
return projectGraph(graphAfterExclusion(builder.build(), filters.exclude, direction), direction, filters.in);
|
|
55
|
+
}
|
|
56
|
+
async function outgoing(context, current, keys) {
|
|
57
|
+
const key = keyFor(context, current, keys);
|
|
58
|
+
const declarationRows = storeRows(context, 'MATCH (source:Declaration)-[call:CALLS]->(target:Declaration) WHERE source.key = $key RETURN target.key AS key, target.file AS file, target.line AS declarationLine, target.col AS declarationCol, target.name AS name, target.qname AS qname, target.kind AS kind, call.line AS line, call.col AS col, call.dispatch AS dispatch', { key });
|
|
59
|
+
const externalRows = storeRows(context, 'MATCH (source:Declaration)-[call:CALLS]->(target:External) WHERE source.key = $key RETURN target.name AS name, call.line AS line, call.col AS col', { key });
|
|
60
|
+
const edges = [];
|
|
61
|
+
const unresolved = externalRows.map((row) => unresolvedSite(current, row, text(row, 'name'), 'external', false));
|
|
62
|
+
for (const row of declarationRows) {
|
|
63
|
+
const siteId = site(current, row);
|
|
64
|
+
const target = recordFromStore(context, row);
|
|
65
|
+
const targetKey = text(row, 'key');
|
|
66
|
+
keys.set(target.address, targetKey);
|
|
67
|
+
if (!boolean(row, 'dispatch')) {
|
|
68
|
+
edges.push({ from: current.address, to: target.address, kind: 'call', siteId, record: target });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const implementations = await implementationRows(context, targetKey);
|
|
72
|
+
if (!implementations.length) {
|
|
73
|
+
unresolved.push(unresolvedSite(current, row, target.name, 'dispatch', true));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const implementation of implementations) {
|
|
77
|
+
keys.set(implementation.address, keyFor(context, implementation, keys));
|
|
78
|
+
edges.push({ from: current.address, to: implementation.address, kind: 'impl', siteId, record: implementation });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { edges, unresolved };
|
|
82
|
+
}
|
|
83
|
+
async function incomingRoots(context, seeds, keys) {
|
|
84
|
+
const roots = new Map(seeds.map((seed) => [seed.address, seed]));
|
|
85
|
+
for (const seed of seeds)
|
|
86
|
+
for (const implementation of await implementationRows(context, keyFor(context, seed, keys)))
|
|
87
|
+
roots.set(implementation.address, implementation);
|
|
88
|
+
return [...roots.values()];
|
|
89
|
+
}
|
|
90
|
+
async function incoming(context, current, keys) {
|
|
91
|
+
const key = keyFor(context, current, keys);
|
|
92
|
+
const edges = [];
|
|
93
|
+
for (const row of storeRows(context, 'MATCH (caller:Declaration)-[call:CALLS]->(target:Declaration) WHERE target.key = $key AND call.dispatch = false RETURN caller.key AS key, caller.file AS file, caller.line AS declarationLine, caller.col AS declarationCol, caller.name AS name, caller.qname AS qname, caller.kind AS kind, call.line AS line, call.col AS col', { key })) {
|
|
94
|
+
const caller = recordFromStore(context, row);
|
|
95
|
+
keys.set(caller.address, text(row, 'key'));
|
|
96
|
+
edges.push({ from: caller.address, to: current.address, kind: 'call', siteId: site(caller, row), record: caller });
|
|
97
|
+
}
|
|
98
|
+
for (const contractKey of storeRows(context, 'MATCH (concrete:Declaration)-[:IMPLEMENTS]->(target:Declaration) WHERE concrete.key = $key RETURN DISTINCT target.key AS key', { key }).map((row) => text(row, 'key'))) {
|
|
99
|
+
for (const row of storeRows(context, 'MATCH (caller:Declaration)-[call:CALLS]->(target:Declaration) WHERE target.key = $key AND call.dispatch = true RETURN caller.file AS file, caller.line AS declarationLine, caller.col AS declarationCol, call.line AS line, call.col AS col', { key: contractKey })) {
|
|
100
|
+
const caller = await context.resolver.resolve(declarationAddress(row));
|
|
101
|
+
keys.set(caller.address, keyFor(context, caller, keys));
|
|
102
|
+
edges.push({ from: caller.address, to: current.address, kind: 'impl', siteId: site(caller, row), record: caller });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { edges, unresolved: [] };
|
|
106
|
+
}
|
|
107
|
+
async function implementationRows(context, targetKey) {
|
|
108
|
+
return resolveRows(context, storeRows(context, 'MATCH (impl:Declaration)-[:IMPLEMENTS]->(target:Declaration) WHERE target.key = $key RETURN DISTINCT impl.key AS key, impl.file AS file, impl.line AS declarationLine, impl.col AS declarationCol, impl.name AS name, impl.qname AS qname, impl.kind AS kind', { key: targetKey }));
|
|
109
|
+
}
|
|
110
|
+
async function resolveRows(context, rows) {
|
|
111
|
+
return [...new Map(rows.map((row) => {
|
|
112
|
+
const record = recordFromStore(context, row);
|
|
113
|
+
return [record.address, record];
|
|
114
|
+
})).values()];
|
|
115
|
+
}
|
|
116
|
+
function recordFromStore(context, row) {
|
|
117
|
+
const location = { path: text(row, 'file'), line: number(row, 'declarationLine'), col: number(row, 'declarationCol') };
|
|
118
|
+
const isModule = location.line === 0 && location.col === 0;
|
|
119
|
+
const name = isModule ? path.basename(location.path) : text(row, 'name');
|
|
120
|
+
return {
|
|
121
|
+
address: declarationAddress(row),
|
|
122
|
+
location,
|
|
123
|
+
file: `${context.root}/${location.path}`,
|
|
124
|
+
kind: text(row, 'kind'),
|
|
125
|
+
name,
|
|
126
|
+
qualifiedName: isModule ? name : text(row, 'qname'),
|
|
127
|
+
nameSpan: { start: 0, end: name.length },
|
|
128
|
+
span: { start: 0, end: 0 },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function storeRows(context, statement, parameters) {
|
|
132
|
+
if (!context.store)
|
|
133
|
+
throw new Error('The TypeScript index is not available yet.');
|
|
134
|
+
return query(context.store, statement, parameters);
|
|
135
|
+
}
|
|
136
|
+
function recordKey(context, record) {
|
|
137
|
+
const { path: file, line, col } = record.location;
|
|
138
|
+
const rows = storeRows(context, 'MATCH (declaration:Declaration) WHERE declaration.file = $file AND declaration.line = $line AND declaration.col = $col RETURN declaration.key AS key', { file, line, col });
|
|
139
|
+
const key = rows[0] && text(rows[0], 'key');
|
|
140
|
+
if (!key)
|
|
141
|
+
throw new Error(`${record.address} is absent from the TypeScript index.`);
|
|
142
|
+
return key;
|
|
143
|
+
}
|
|
144
|
+
function keyFor(context, record, keys) {
|
|
145
|
+
const key = keys.get(record.address) ?? recordKey(context, record);
|
|
146
|
+
keys.set(record.address, key);
|
|
147
|
+
return key;
|
|
148
|
+
}
|
|
149
|
+
function declarationAddress(row) { return `${text(row, 'file')}:${number(row, 'declarationLine')}:${number(row, 'declarationCol')}`; }
|
|
150
|
+
function site(record, row) { return `${record.location.path}:${number(row, 'line')}:${number(row, 'col')}`; }
|
|
151
|
+
function unresolvedSite(record, row, name, reason, dispatch) {
|
|
152
|
+
const line = number(row, 'line');
|
|
153
|
+
const col = number(row, 'col');
|
|
154
|
+
return { from: record.address, siteId: `${record.location.path}:${line}:${col}`, name, reason, path: record.location.path, start: { line, col }, text: name, dispatch };
|
|
155
|
+
}
|
|
156
|
+
function text(row, key) { return String(row[key] ?? ''); }
|
|
157
|
+
function number(row, key) { return Number(row[key]); }
|
|
158
|
+
function boolean(row, key) { return row[key] === true; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DeclarationRecord } from '../core/declarations.js';
|
|
2
|
+
export type Direction = 'calls' | 'callers';
|
|
3
|
+
export type EdgeKind = 'call' | 'impl';
|
|
4
|
+
export type UnresolvedReason = 'dispatch' | 'dynamic' | 'external';
|
|
5
|
+
export interface GraphNode {
|
|
6
|
+
id: string;
|
|
7
|
+
path: string;
|
|
8
|
+
start: {
|
|
9
|
+
line: number;
|
|
10
|
+
col: number;
|
|
11
|
+
};
|
|
12
|
+
end: {
|
|
13
|
+
line: number;
|
|
14
|
+
col: number;
|
|
15
|
+
};
|
|
16
|
+
kind: string;
|
|
17
|
+
name: string;
|
|
18
|
+
qualifiedName: string;
|
|
19
|
+
depth: number;
|
|
20
|
+
connecting?: true;
|
|
21
|
+
}
|
|
22
|
+
export interface GraphEdge {
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
kind: EdgeKind;
|
|
26
|
+
siteIds: readonly string[];
|
|
27
|
+
callSites: number;
|
|
28
|
+
}
|
|
29
|
+
export interface UnresolvedSite {
|
|
30
|
+
from: string;
|
|
31
|
+
siteId: string;
|
|
32
|
+
name: string;
|
|
33
|
+
reason: UnresolvedReason;
|
|
34
|
+
path: string;
|
|
35
|
+
start: {
|
|
36
|
+
line: number;
|
|
37
|
+
col: number;
|
|
38
|
+
};
|
|
39
|
+
text: string;
|
|
40
|
+
dispatch: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface Graph {
|
|
43
|
+
nodes: GraphNode[];
|
|
44
|
+
edges: GraphEdge[];
|
|
45
|
+
unresolved: UnresolvedSite[];
|
|
46
|
+
}
|
|
47
|
+
export interface RelationRow extends GraphNode {
|
|
48
|
+
callSites: number;
|
|
49
|
+
edgeKinds: readonly EdgeKind[];
|
|
50
|
+
}
|
|
51
|
+
export interface TraversalResult {
|
|
52
|
+
graph: Graph;
|
|
53
|
+
rows: RelationRow[];
|
|
54
|
+
excluded: number;
|
|
55
|
+
unresolved: number;
|
|
56
|
+
dispatch: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function nodeFor(record: DeclarationRecord, depth: number): GraphNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function nodeFor(record, depth) {
|
|
2
|
+
return {
|
|
3
|
+
id: record.address,
|
|
4
|
+
path: record.location.path,
|
|
5
|
+
start: { line: record.location.line, col: record.location.col },
|
|
6
|
+
end: { line: record.location.line, col: record.location.col },
|
|
7
|
+
kind: record.kind,
|
|
8
|
+
name: record.name,
|
|
9
|
+
qualifiedName: record.qualifiedName,
|
|
10
|
+
depth,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Completion, Invocation } from '../commands/types.js';
|
|
2
|
+
export declare function invokeResident(invocation: Invocation): Promise<Completion>;
|
|
3
|
+
export declare function serverStatus(rootOverride?: string): Promise<Completion>;
|
|
4
|
+
export declare function serverStop(rootOverride?: string): Promise<Completion>;
|
|
5
|
+
export declare function serverReload(rootOverride?: string): Promise<Completion>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, existsSync, openSync } from 'node:fs';
|
|
3
|
+
import { connect } from 'node:net';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { discoverRoot, ensureStateDirectory, pathsForRoot } from './lifecycle.js';
|
|
6
|
+
import { NdjsonReader, request, writeFrame } from './wire.js';
|
|
7
|
+
class SessionDied extends Error {
|
|
8
|
+
}
|
|
9
|
+
class Stopping extends Error {
|
|
10
|
+
}
|
|
11
|
+
class DeadlineExpired extends Error {
|
|
12
|
+
phase;
|
|
13
|
+
command;
|
|
14
|
+
constructor(phase, command) {
|
|
15
|
+
super(`${phase ?? 'admission'} deadline exceeded`);
|
|
16
|
+
this.phase = phase;
|
|
17
|
+
this.command = command;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function invokeResident(invocation) {
|
|
21
|
+
const root = discoverRoot(invocation.root);
|
|
22
|
+
return send(root, 'invoke', invocation, true);
|
|
23
|
+
}
|
|
24
|
+
export async function serverStatus(rootOverride) {
|
|
25
|
+
const root = discoverRoot(rootOverride);
|
|
26
|
+
const paths = pathsForRoot(root);
|
|
27
|
+
if (!existsSync(paths.port))
|
|
28
|
+
return { data: { element: 'server', attributes: { root, state: 'stopped' } }, count: 0 };
|
|
29
|
+
return send(root, 'status', undefined, false);
|
|
30
|
+
}
|
|
31
|
+
export async function serverStop(rootOverride) {
|
|
32
|
+
const root = discoverRoot(rootOverride);
|
|
33
|
+
const paths = pathsForRoot(root);
|
|
34
|
+
if (!existsSync(paths.port))
|
|
35
|
+
return { data: { element: 'server', attributes: { root, state: 'stopped' } }, count: 0 };
|
|
36
|
+
return send(root, 'stop', undefined, false);
|
|
37
|
+
}
|
|
38
|
+
export async function serverReload(rootOverride) {
|
|
39
|
+
const root = discoverRoot(rootOverride);
|
|
40
|
+
await serverStop(root);
|
|
41
|
+
const completion = await ensure(root, true, true);
|
|
42
|
+
return withStartup(completion, completion.startupStartedAt);
|
|
43
|
+
}
|
|
44
|
+
async function send(root, operation, invocation, ensureServer) {
|
|
45
|
+
const paths = pathsForRoot(root);
|
|
46
|
+
if (ensureServer) {
|
|
47
|
+
const ensured = await ensure(root);
|
|
48
|
+
if ('category' in ensured)
|
|
49
|
+
return ensured;
|
|
50
|
+
try {
|
|
51
|
+
return withStartup(await exchange(paths, request(root, operation, invocation)), ensured.startupStartedAt);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof DeadlineExpired)
|
|
55
|
+
return deadlineFailure(paths, error);
|
|
56
|
+
if (error instanceof Stopping)
|
|
57
|
+
return stoppingFailure(paths);
|
|
58
|
+
if (!(error instanceof SessionDied))
|
|
59
|
+
return unavailable(paths, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
try {
|
|
64
|
+
return await exchange(paths, request(root, operation, invocation));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof DeadlineExpired)
|
|
68
|
+
return deadlineFailure(paths, error);
|
|
69
|
+
if (error instanceof Stopping)
|
|
70
|
+
return stoppingFailure(paths);
|
|
71
|
+
return unavailable(paths, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const ensured = await ensure(root, true);
|
|
75
|
+
if ('category' in ensured)
|
|
76
|
+
return ensured;
|
|
77
|
+
try {
|
|
78
|
+
return withStartup(await exchange(paths, request(root, operation, invocation)), ensured.startupStartedAt);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof DeadlineExpired)
|
|
82
|
+
return deadlineFailure(paths, error);
|
|
83
|
+
if (error instanceof Stopping)
|
|
84
|
+
return stoppingFailure(paths);
|
|
85
|
+
if (error instanceof SessionDied)
|
|
86
|
+
return { category: 'tool', code: 'server-died', message: 'The resident server died again before completing the request.', logPath: paths.log, next: 'Read the server log and repair the workspace before retrying.' };
|
|
87
|
+
return unavailable(paths, error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function ensure(root, force = false, rebuild = false) {
|
|
91
|
+
const paths = pathsForRoot(root);
|
|
92
|
+
if (!force && existsSync(paths.port)) {
|
|
93
|
+
try {
|
|
94
|
+
return await exchange(paths, request(root, 'status'));
|
|
95
|
+
}
|
|
96
|
+
catch { /* a lock holder will clean stale endpoint */ }
|
|
97
|
+
}
|
|
98
|
+
const startedAt = performance.now();
|
|
99
|
+
ensureStateDirectory(paths);
|
|
100
|
+
const log = openSync(paths.log, 'a', 0o600);
|
|
101
|
+
try {
|
|
102
|
+
spawn(process.execPath, [fileURLToPath(new URL('../../dist/server/main.js', import.meta.url)), '--root', root, ...(rebuild ? ['--rebuild'] : [])], { detached: true, stdio: ['ignore', log, log] }).unref();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
closeSync(log);
|
|
106
|
+
}
|
|
107
|
+
const deadline = startedAt + 30_000;
|
|
108
|
+
while (performance.now() < deadline) {
|
|
109
|
+
if (existsSync(paths.port)) {
|
|
110
|
+
try {
|
|
111
|
+
return { ...await exchange(paths, request(root, 'status')), startupStartedAt: startedAt };
|
|
112
|
+
}
|
|
113
|
+
catch { /* wait for the winning candidate */ }
|
|
114
|
+
}
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
116
|
+
}
|
|
117
|
+
return unavailable(paths, 'No resident became ready within 30 seconds.');
|
|
118
|
+
}
|
|
119
|
+
function exchange(paths, payload) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const socket = connect(paths.port);
|
|
122
|
+
const reader = new NdjsonReader();
|
|
123
|
+
let settled = false;
|
|
124
|
+
let stopping = false;
|
|
125
|
+
let deadlinePhase;
|
|
126
|
+
let command;
|
|
127
|
+
const finish = (completion) => { if (!settled) {
|
|
128
|
+
settled = true;
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
socket.end();
|
|
131
|
+
resolve(completion);
|
|
132
|
+
} };
|
|
133
|
+
const fail = (error) => { if (!settled) {
|
|
134
|
+
settled = true;
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
socket.destroy();
|
|
137
|
+
reject(error);
|
|
138
|
+
} };
|
|
139
|
+
let timeout = arm(30_000);
|
|
140
|
+
const rearm = (remainingMs) => {
|
|
141
|
+
if (remainingMs === undefined)
|
|
142
|
+
return;
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
timeout = arm(remainingMs);
|
|
145
|
+
};
|
|
146
|
+
function arm(delay) { return setTimeout(() => fail(new DeadlineExpired(deadlinePhase, command)), delay); }
|
|
147
|
+
const phase = (frame) => {
|
|
148
|
+
switch (frame.phase) {
|
|
149
|
+
case 'queued':
|
|
150
|
+
deadlinePhase = 'queued';
|
|
151
|
+
break;
|
|
152
|
+
case 'project-load-start':
|
|
153
|
+
case 'project-load-end':
|
|
154
|
+
case 'index-build-start':
|
|
155
|
+
case 'index-build-end':
|
|
156
|
+
deadlinePhase = 'project-load';
|
|
157
|
+
break;
|
|
158
|
+
case 'running':
|
|
159
|
+
deadlinePhase = 'running';
|
|
160
|
+
break;
|
|
161
|
+
case 'command-start':
|
|
162
|
+
command = frame.command;
|
|
163
|
+
break;
|
|
164
|
+
case 'stopping':
|
|
165
|
+
stopping = true;
|
|
166
|
+
break;
|
|
167
|
+
case 'accepted': break;
|
|
168
|
+
}
|
|
169
|
+
rearm(frame.remainingMs);
|
|
170
|
+
};
|
|
171
|
+
socket.on('connect', () => writeFrame(socket, payload));
|
|
172
|
+
socket.on('data', (chunk) => {
|
|
173
|
+
for (const value of reader.push(chunk)) {
|
|
174
|
+
const frame = value;
|
|
175
|
+
if (frame.requestId !== payload.requestId)
|
|
176
|
+
continue;
|
|
177
|
+
if (frame.type === 'phase') {
|
|
178
|
+
phase(frame);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (frame.type !== 'response')
|
|
182
|
+
continue;
|
|
183
|
+
const response = frame;
|
|
184
|
+
if (response.completion.answer)
|
|
185
|
+
finish(response.completion.answer);
|
|
186
|
+
else if (response.completion.error)
|
|
187
|
+
finish(response.completion.error);
|
|
188
|
+
else
|
|
189
|
+
fail(new Error('invalid terminal response'));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
socket.on('error', (error) => fail(stopping ? new Stopping(error.message) : new SessionDied(error.message)));
|
|
193
|
+
socket.on('close', () => { if (!settled)
|
|
194
|
+
fail(stopping ? new Stopping('resident stopped before responding') : new SessionDied('resident closed before responding')); });
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function withStartup(completion, startupStartedAt) {
|
|
198
|
+
if (startupStartedAt === undefined || 'category' in completion)
|
|
199
|
+
return completion;
|
|
200
|
+
const { startupStartedAt: _, ...answer } = completion;
|
|
201
|
+
return { ...answer, startupMs: Math.round(performance.now() - startupStartedAt) };
|
|
202
|
+
}
|
|
203
|
+
function deadlineFailure(paths, deadline) {
|
|
204
|
+
if (deadline.phase === 'project-load')
|
|
205
|
+
return { category: 'tool', code: 'project-load-timeout', message: 'TypeScript did not load the workspace project before its deadline.', logPath: paths.log, next: 'Repair the project, then retry.' };
|
|
206
|
+
if (deadline.phase === 'running')
|
|
207
|
+
return { category: 'tool', code: 'request-timeout', message: 'The semantic request exceeded its deadline.', logPath: paths.log, received: deadline.command ?? 'no semantic operation started', next: 'Retry after repairing the workspace or narrowing the command.' };
|
|
208
|
+
return { category: 'tool', code: 'server-unavailable', message: 'The resident did not begin this request before its admission deadline.', logPath: paths.log, next: 'Retry after the current request completes.' };
|
|
209
|
+
}
|
|
210
|
+
function stoppingFailure(paths) {
|
|
211
|
+
return { category: 'tool', code: 'server-unavailable', message: 'The resident stopped before completing the request.', logPath: paths.log, next: 'Wait for stop to complete before invoking tsym.' };
|
|
212
|
+
}
|
|
213
|
+
function unavailable(paths, error) {
|
|
214
|
+
return { category: 'tool', code: 'server-unavailable', message: 'The resident server could not be reached.', logPath: paths.log, received: error instanceof Error ? error.message : String(error), expected: 'a ready resident for this workspace', next: 'Run `tsym server status`; do not delete the lock file.' };
|
|
215
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DeclarationResolver } from '../core/declarations.js';
|
|
2
|
+
import { SourceReader } from '../core/source.js';
|
|
3
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
4
|
+
import { ProjectRegistry } from './projects.js';
|
|
5
|
+
/** What a store-reading leaf reports about the index it answered from (design §4.5). */
|
|
6
|
+
export interface IndexState {
|
|
7
|
+
indexedAt: string;
|
|
8
|
+
state: 'current' | 'indexing' | 'stale';
|
|
9
|
+
built: import('../index/build.js').BuiltIndex;
|
|
10
|
+
}
|
|
11
|
+
/** The per-resident semantic API used by leaf handlers inside the FIFO invocation queue. */
|
|
12
|
+
export interface SemanticContext {
|
|
13
|
+
root: string;
|
|
14
|
+
program: Ts7Host;
|
|
15
|
+
projects: ProjectRegistry;
|
|
16
|
+
resolver: DeclarationResolver;
|
|
17
|
+
source: SourceReader;
|
|
18
|
+
store?: import('../store/store.js').Store;
|
|
19
|
+
index?: IndexState;
|
|
20
|
+
}
|
|
21
|
+
export declare function createSemanticContext(root: string): SemanticContext;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DeclarationResolver } from '../core/declarations.js';
|
|
2
|
+
import { SourceReader } from '../core/source.js';
|
|
3
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
4
|
+
import { ProjectRegistry } from './projects.js';
|
|
5
|
+
export function createSemanticContext(root) {
|
|
6
|
+
const program = new Ts7Host(root);
|
|
7
|
+
const projects = new ProjectRegistry(root, program);
|
|
8
|
+
const source = new SourceReader(projects);
|
|
9
|
+
let context;
|
|
10
|
+
context = { root, program, projects, resolver: new DeclarationResolver(root, source, () => context.store), source };
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ServerPaths {
|
|
2
|
+
root: string;
|
|
3
|
+
state: string;
|
|
4
|
+
lock: string;
|
|
5
|
+
port: string;
|
|
6
|
+
pid: string;
|
|
7
|
+
log: string;
|
|
8
|
+
store: string;
|
|
9
|
+
results: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PublishedState {
|
|
12
|
+
root: string;
|
|
13
|
+
pid: number;
|
|
14
|
+
bootedAt: string;
|
|
15
|
+
port: string;
|
|
16
|
+
log: string;
|
|
17
|
+
idleDeadline: string;
|
|
18
|
+
state: 'ready' | 'stopping';
|
|
19
|
+
index?: 'current' | 'stale';
|
|
20
|
+
declarations?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function discoverRoot(input: string | undefined, cwd?: string): string;
|
|
23
|
+
export declare function pathsForRoot(root: string): ServerPaths;
|
|
24
|
+
export declare function ensureStateDirectory(paths: ServerPaths): void;
|
|
25
|
+
export declare function readState(paths: ServerPaths): PublishedState | null;
|
|
26
|
+
export declare function publishState(paths: ServerPaths, state: PublishedState): void;
|
|
27
|
+
export declare function removeEndpoint(paths: ServerPaths): void;
|
|
28
|
+
export declare function idleSeconds(): number;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export function discoverRoot(input, cwd = process.cwd()) {
|
|
6
|
+
let current = path.resolve(input ?? cwd);
|
|
7
|
+
for (const marker of ['tsconfig.json', 'package.json', '.git']) {
|
|
8
|
+
let candidate = current;
|
|
9
|
+
for (;;) {
|
|
10
|
+
if (existsSync(path.join(candidate, marker)))
|
|
11
|
+
return realpathSync(candidate);
|
|
12
|
+
const parent = path.dirname(candidate);
|
|
13
|
+
if (parent === candidate)
|
|
14
|
+
break;
|
|
15
|
+
candidate = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return realpathSync(current);
|
|
19
|
+
}
|
|
20
|
+
export function pathsForRoot(root) {
|
|
21
|
+
const stateBase = process.env.XDG_CACHE_HOME ?? path.join(process.env.HOME ?? '~', '.cache');
|
|
22
|
+
const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
23
|
+
const state = path.join(stateBase, 'tsym', id);
|
|
24
|
+
return { root, state, lock: path.join(state, 'lock'), port: path.join(state, 'port'), pid: path.join(state, 'pid'), log: path.join(state, 'log'), store: path.join(state, 'store'), results: path.join(state, 'results') };
|
|
25
|
+
}
|
|
26
|
+
export function ensureStateDirectory(paths) {
|
|
27
|
+
mkdirSync(paths.state, { recursive: true, mode: 0o700 });
|
|
28
|
+
chmodSync(paths.state, 0o700);
|
|
29
|
+
for (const directory of [paths.store, paths.results]) {
|
|
30
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
31
|
+
chmodSync(directory, 0o700);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function readState(paths) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(paths.pid, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function publishState(paths, state) {
|
|
43
|
+
writeFileSync(paths.pid, JSON.stringify(state) + '\n', { mode: 0o600 });
|
|
44
|
+
chmodSync(paths.pid, 0o600);
|
|
45
|
+
}
|
|
46
|
+
export function removeEndpoint(paths) {
|
|
47
|
+
for (const entry of [paths.port, paths.pid]) {
|
|
48
|
+
try {
|
|
49
|
+
rmSync(entry);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== 'ENOENT')
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function idleSeconds() {
|
|
58
|
+
const configured = process.env.TSYM_IDLE_SECONDS;
|
|
59
|
+
if (configured !== undefined && /^\d+$/.test(configured))
|
|
60
|
+
return Number(configured);
|
|
61
|
+
return 14_400;
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|