@crouton-kit/tsym 0.1.0 → 0.2.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/README.md +1 -1
- package/dist/commands/handlers/find.js +11 -10
- package/dist/commands/handlers/inspect.js +64 -20
- package/dist/commands/handlers/outline.js +25 -14
- package/dist/commands/handlers/show.js +6 -5
- package/dist/commands/handlers/type.js +3 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/tree.js +2 -2
- package/dist/commands/types.d.ts +6 -0
- package/dist/context-exposure.d.ts +9 -0
- package/dist/context-exposure.js +55 -0
- package/dist/core/members.d.ts +9 -0
- package/dist/core/members.js +12 -0
- package/dist/index/build.d.ts +2 -1
- package/dist/index/build.js +3 -3
- package/dist/index/refresh.d.ts +2 -1
- package/dist/index/refresh.js +2 -2
- package/dist/output.js +10 -3
- package/dist/relations/store-relations.d.ts +14 -0
- package/dist/relations/store-relations.js +24 -0
- package/dist/server/client.js +7 -2
- package/dist/server/main.js +129 -53
- package/dist/server/wire.d.ts +4 -1
- package/dist/store/load.js +2 -2
- package/dist/store/meta.d.ts +13 -9
- package/dist/store/meta.js +30 -18
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +1 -1
- package/dist/store/seed.d.ts +13 -0
- package/dist/store/seed.js +185 -0
- package/dist/store/store.d.ts +2 -0
- package/dist/store/store.js +11 -2
- package/package.json +2 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, readdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
4
|
+
import { connect } from 'node:net';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pathsForRoot } from '../server/lifecycle.js';
|
|
7
|
+
import { NdjsonReader, WIRE_VERSION, writeFrame } from '../server/wire.js';
|
|
8
|
+
import { readMetadata, sameFormat, writeMetadata } from './meta.js';
|
|
9
|
+
/** Finds compatible sibling stores and installs one only after a complete copy validates. */
|
|
10
|
+
export async function seedFromSibling(root, paths, expected, identity) {
|
|
11
|
+
const started = performance.now();
|
|
12
|
+
// A process that died mid-copy on a previous start leaves this behind; it is never valid to keep across a start.
|
|
13
|
+
rmSync(seedPath(paths), { recursive: true, force: true });
|
|
14
|
+
for (const candidate of siblings(root, paths, expected, identity)) {
|
|
15
|
+
try {
|
|
16
|
+
const copied = copyWhileLocked(candidate, paths) || await copyFromOwner(candidate, paths);
|
|
17
|
+
if (!copied)
|
|
18
|
+
continue;
|
|
19
|
+
const copiedMetadata = readMetadata(seedPath(paths));
|
|
20
|
+
if (copiedMetadata === null || copiedMetadata.root !== candidate.root || !sameFormat(copiedMetadata, identity))
|
|
21
|
+
throw new Error('copied metadata did not match the selected sibling');
|
|
22
|
+
writeMetadata(seedPath(paths), { ...copiedMetadata, root });
|
|
23
|
+
rmSync(paths.store, { recursive: true, force: true });
|
|
24
|
+
renameSync(seedPath(paths), paths.store);
|
|
25
|
+
return { root: candidate.root, bytes: directoryBytes(paths.store), durationMs: performance.now() - started };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
rmSync(seedPath(paths), { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Copies a resident's store after the owner checkpointed it. The owner excludes its process-local marker. */
|
|
34
|
+
export function copyStore(source, destination) {
|
|
35
|
+
if (existsSync(destination))
|
|
36
|
+
throw new Error(`Seed destination already exists: ${destination}.`);
|
|
37
|
+
cpSync(source, destination, { recursive: true, filter: (entry) => path.basename(entry) !== '.tsym-owner' });
|
|
38
|
+
return directoryBytes(destination);
|
|
39
|
+
}
|
|
40
|
+
/** Returns the wire refusal reason, keeping the owner’s destination safety contract testable. */
|
|
41
|
+
export function refuseSeed(dest, cacheBase, bootstrapped, busy) {
|
|
42
|
+
if (!bootstrapped)
|
|
43
|
+
return 'The resident has not bootstrapped a store.';
|
|
44
|
+
if (busy)
|
|
45
|
+
return 'The resident is writing its store.';
|
|
46
|
+
if (typeof dest !== 'string' || !inside(cacheBase, dest) || existsSync(dest))
|
|
47
|
+
return 'The seed destination must be a new directory inside the tsym cache.';
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function siblings(root, paths, expected, identity) {
|
|
51
|
+
const candidates = [];
|
|
52
|
+
const base = path.dirname(paths.state);
|
|
53
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
54
|
+
if (!entry.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
const store = path.join(base, entry.name, 'store');
|
|
57
|
+
let metadata;
|
|
58
|
+
try {
|
|
59
|
+
metadata = readMetadata(store);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (metadata === null || metadata.root === root || !sameFormat(metadata, identity))
|
|
65
|
+
continue;
|
|
66
|
+
if (fileDifference(metadata.files, expected.files) !== 0)
|
|
67
|
+
continue;
|
|
68
|
+
candidates.push({ root: metadata.root, store, metadata });
|
|
69
|
+
}
|
|
70
|
+
return candidates;
|
|
71
|
+
}
|
|
72
|
+
function copyWhileLocked(candidate, paths) {
|
|
73
|
+
let lock;
|
|
74
|
+
try {
|
|
75
|
+
lock = new DatabaseSync(pathsForRoot(candidate.root).lock);
|
|
76
|
+
lock.exec('BEGIN EXCLUSIVE');
|
|
77
|
+
copyStore(candidate.store, seedPath(paths));
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isBusy(error))
|
|
82
|
+
throw error;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
try {
|
|
87
|
+
lock?.close();
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function copyFromOwner(candidate, paths) {
|
|
93
|
+
const destination = seedPath(paths);
|
|
94
|
+
const sourcePaths = pathsForRoot(candidate.root);
|
|
95
|
+
try {
|
|
96
|
+
const response = await requestSeed(sourcePaths.port, candidate.root, destination);
|
|
97
|
+
return response.root === candidate.root && response.store === destination;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function requestSeed(port, root, dest) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const socket = connect(port);
|
|
106
|
+
const reader = new NdjsonReader();
|
|
107
|
+
const requestId = randomUUID();
|
|
108
|
+
const timer = setTimeout(() => fail(new Error('seed request timed out')), 30_000);
|
|
109
|
+
let settled = false;
|
|
110
|
+
const finish = (value) => { if (!settled) {
|
|
111
|
+
settled = true;
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
socket.end();
|
|
114
|
+
resolve(value);
|
|
115
|
+
} };
|
|
116
|
+
const fail = (error) => { if (!settled) {
|
|
117
|
+
settled = true;
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
socket.destroy();
|
|
120
|
+
reject(error);
|
|
121
|
+
} };
|
|
122
|
+
socket.on('connect', () => writeFrame(socket, { version: WIRE_VERSION, requestId, root, operation: 'seed', seed: { dest } }));
|
|
123
|
+
socket.on('data', (chunk) => {
|
|
124
|
+
for (const value of reader.push(chunk)) {
|
|
125
|
+
const frame = value;
|
|
126
|
+
if (frame.type !== 'response' || frame.requestId !== requestId)
|
|
127
|
+
continue;
|
|
128
|
+
const answer = frame.completion.answer;
|
|
129
|
+
const data = answer?.data;
|
|
130
|
+
const node = Array.isArray(data) ? data[0] : data;
|
|
131
|
+
if (node?.element === 'seed' && typeof node.attributes?.root === 'string' && typeof node.attributes.store === 'string')
|
|
132
|
+
finish({ root: node.attributes.root, store: node.attributes.store });
|
|
133
|
+
else
|
|
134
|
+
fail(new Error('seed request was refused'));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
socket.on('error', fail);
|
|
138
|
+
socket.on('close', () => { if (!settled)
|
|
139
|
+
fail(new Error('seed owner closed without responding')); });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function seedPath(paths) { return path.join(paths.state, 'store.seed'); }
|
|
143
|
+
function fileDifference(left, right) {
|
|
144
|
+
const files = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
145
|
+
let differing = 0;
|
|
146
|
+
for (const file of files)
|
|
147
|
+
if (left[file] !== right[file])
|
|
148
|
+
differing++;
|
|
149
|
+
return differing;
|
|
150
|
+
}
|
|
151
|
+
function directoryBytes(directory) {
|
|
152
|
+
let bytes = 0;
|
|
153
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
154
|
+
const file = path.join(directory, entry.name);
|
|
155
|
+
if (entry.isDirectory())
|
|
156
|
+
bytes += directoryBytes(file);
|
|
157
|
+
else if (entry.isFile())
|
|
158
|
+
bytes += lstatSync(file).size;
|
|
159
|
+
}
|
|
160
|
+
return bytes;
|
|
161
|
+
}
|
|
162
|
+
function isBusy(error) { return error.code === 'ERR_SQLITE_ERROR' && error.errcode === 5 || String(error).includes('database is locked'); }
|
|
163
|
+
function inside(parent, child) {
|
|
164
|
+
try {
|
|
165
|
+
const physicalParent = realpathSync(parent);
|
|
166
|
+
const physicalChild = physicalPath(child);
|
|
167
|
+
const relative = path.relative(physicalParent, physicalChild);
|
|
168
|
+
return relative !== '' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function physicalPath(target) {
|
|
175
|
+
const missing = [];
|
|
176
|
+
let existing = path.resolve(target);
|
|
177
|
+
while (!existsSync(existing)) {
|
|
178
|
+
const parent = path.dirname(existing);
|
|
179
|
+
if (parent === existing)
|
|
180
|
+
throw new Error(`No existing ancestor for ${target}.`);
|
|
181
|
+
missing.unshift(path.basename(existing));
|
|
182
|
+
existing = parent;
|
|
183
|
+
}
|
|
184
|
+
return path.join(realpathSync(existing), ...missing);
|
|
185
|
+
}
|
package/dist/store/store.d.ts
CHANGED
|
@@ -24,10 +24,12 @@ export declare class Store {
|
|
|
24
24
|
private boundedConnection;
|
|
25
25
|
private closed;
|
|
26
26
|
private readonly boundedQueryTimeoutMs;
|
|
27
|
+
private writing;
|
|
27
28
|
constructor(storePath: string, options?: StoreOptions);
|
|
28
29
|
execute(statement: string, parameters?: Record<string, LbugValue>): StoreRow[];
|
|
29
30
|
/** Executes a read query on a separate connection whose 30-second engine timeout never affects writes. */
|
|
30
31
|
executeBounded(statement: string, parameters?: Record<string, LbugValue>): StoreRow[];
|
|
32
|
+
get busy(): boolean;
|
|
31
33
|
transaction<T>(work: () => T): T;
|
|
32
34
|
checkpoint(): void;
|
|
33
35
|
close(): void;
|
package/dist/store/store.js
CHANGED
|
@@ -38,6 +38,7 @@ export class Store {
|
|
|
38
38
|
boundedConnection;
|
|
39
39
|
closed = false;
|
|
40
40
|
boundedQueryTimeoutMs;
|
|
41
|
+
writing = false;
|
|
41
42
|
constructor(storePath, options = {}) {
|
|
42
43
|
this.path = path.resolve(storePath);
|
|
43
44
|
this.boundedQueryTimeoutMs = options.boundedQueryTimeoutMs ?? DEFAULT_BOUNDED_QUERY_TIMEOUT_MS;
|
|
@@ -92,9 +93,13 @@ export class Store {
|
|
|
92
93
|
throw new StoreReadOnlyError();
|
|
93
94
|
return collect(connection.executeSync(prepared, parameters));
|
|
94
95
|
}
|
|
96
|
+
get busy() { return this.writing; }
|
|
95
97
|
transaction(work) {
|
|
96
|
-
this.
|
|
98
|
+
if (this.writing)
|
|
99
|
+
throw new Error('Store transaction is already in progress.');
|
|
100
|
+
this.writing = true;
|
|
97
101
|
try {
|
|
102
|
+
this.execute('BEGIN TRANSACTION');
|
|
98
103
|
const value = work();
|
|
99
104
|
this.execute('COMMIT');
|
|
100
105
|
return value;
|
|
@@ -106,6 +111,9 @@ export class Store {
|
|
|
106
111
|
catch { }
|
|
107
112
|
throw error;
|
|
108
113
|
}
|
|
114
|
+
finally {
|
|
115
|
+
this.writing = false;
|
|
116
|
+
}
|
|
109
117
|
}
|
|
110
118
|
checkpoint() {
|
|
111
119
|
this.execute('CHECKPOINT');
|
|
@@ -171,7 +179,8 @@ function collect(result) {
|
|
|
171
179
|
const rows = [];
|
|
172
180
|
for (const item of results) {
|
|
173
181
|
try {
|
|
174
|
-
|
|
182
|
+
for (const row of item.getAllSync())
|
|
183
|
+
rows.push(row);
|
|
175
184
|
}
|
|
176
185
|
finally {
|
|
177
186
|
item.close();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/tsym",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Symbol-level reading of a TypeScript codebase for agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
|
-
"tsym": "
|
|
8
|
+
"tsym": "bin/tsym.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|