@danypops/lector 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 +57 -0
- package/package.json +48 -0
- package/src/adapters/directory-size.ts +19 -0
- package/src/adapters/find-source-files.ts +35 -0
- package/src/adapters/git-repo-fetcher.ts +146 -0
- package/src/adapters/in-memory-content-cache.ts +19 -0
- package/src/adapters/in-memory-search-cache.ts +32 -0
- package/src/adapters/in-memory-symbol-graph.ts +80 -0
- package/src/adapters/in-memory-workspace.ts +28 -0
- package/src/adapters/local-filesystem-workspace.ts +96 -0
- package/src/adapters/local-git.ts +73 -0
- package/src/adapters/lsp/discover-seed-file.ts +130 -0
- package/src/adapters/lsp/json-rpc-stream.ts +67 -0
- package/src/adapters/lsp/language-server-process.ts +184 -0
- package/src/adapters/lsp/lsp-symbol-index.ts +470 -0
- package/src/adapters/lsp/process-resource-usage.ts +61 -0
- package/src/adapters/lsp/typescript-project-files.ts +51 -0
- package/src/adapters/read-only-workspace.ts +23 -0
- package/src/adapters/ripgrep-text-search.ts +97 -0
- package/src/adapters/source-manifest.ts +44 -0
- package/src/adapters/sqlite-content-cache.ts +67 -0
- package/src/adapters/sqlite-search-cache.ts +60 -0
- package/src/adapters/sqlite-symbol-graph.ts +154 -0
- package/src/adapters/tiered-search-cache.ts +29 -0
- package/src/adapters/tree-sitter/typescript-tree-sitter-symbol-index.ts +155 -0
- package/src/cli.ts +777 -0
- package/src/client.ts +63 -0
- package/src/constants.ts +16 -0
- package/src/daemon.ts +166 -0
- package/src/domain/assert-safe-git-argument.ts +19 -0
- package/src/domain/assert-safe-path-segment.ts +17 -0
- package/src/domain/assert-safe-repo-reference.ts +20 -0
- package/src/domain/assert-safe-search-query.ts +17 -0
- package/src/domain/bounded-job-executor.ts +219 -0
- package/src/domain/call-hierarchy.ts +23 -0
- package/src/domain/code-range.ts +11 -0
- package/src/domain/content-hash.ts +14 -0
- package/src/domain/diagnostic.ts +12 -0
- package/src/domain/diagnostics.ts +7 -0
- package/src/domain/document-symbol.ts +19 -0
- package/src/domain/document-symbols.ts +7 -0
- package/src/domain/exact-edit.ts +43 -0
- package/src/domain/find-references.ts +7 -0
- package/src/domain/find-workspace-symbols.ts +7 -0
- package/src/domain/git-diff-result.ts +5 -0
- package/src/domain/git-log-entry.ts +9 -0
- package/src/domain/git-status.ts +17 -0
- package/src/domain/go-to-definition.ts +7 -0
- package/src/domain/go-to-implementation.ts +7 -0
- package/src/domain/hover-at.ts +8 -0
- package/src/domain/hover.ts +7 -0
- package/src/domain/incoming-calls.ts +8 -0
- package/src/domain/language-server-descriptor.ts +122 -0
- package/src/domain/outgoing-calls.ts +8 -0
- package/src/domain/populate-symbol-graph.ts +91 -0
- package/src/domain/prepare-call-hierarchy.ts +8 -0
- package/src/domain/race-workspace-query.ts +39 -0
- package/src/domain/raw-read.ts +24 -0
- package/src/domain/reachable-symbols-from.ts +13 -0
- package/src/domain/repo-fetch-result.ts +18 -0
- package/src/domain/repo-reference.ts +7 -0
- package/src/domain/search-cache-key.ts +16 -0
- package/src/domain/search-text.ts +29 -0
- package/src/domain/symbol-edges-from.ts +9 -0
- package/src/domain/symbol-edges-to.ts +9 -0
- package/src/domain/symbol-graph-generation.ts +14 -0
- package/src/domain/symbol-node-id.ts +13 -0
- package/src/domain/text-search-result.ts +15 -0
- package/src/domain/workspace-query-outcome.ts +24 -0
- package/src/domain/workspace-symbol.ts +14 -0
- package/src/index.ts +121 -0
- package/src/ports/code-intelligence-port.ts +38 -0
- package/src/ports/content-cache-port.ts +52 -0
- package/src/ports/git-port.ts +23 -0
- package/src/ports/repo-fetcher-port.ts +11 -0
- package/src/ports/search-cache-port.ts +15 -0
- package/src/ports/symbol-graph-port.ts +35 -0
- package/src/ports/symbol-index-port.ts +11 -0
- package/src/ports/text-search-port.ts +12 -0
- package/src/ports/workspace-port.ts +35 -0
- package/src/service.ts +961 -0
- package/src/version.ts +6 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { InMemoryWorkspace } from "./adapters/in-memory-workspace.ts";
|
|
8
|
+
import { LocalFilesystemWorkspace } from "./adapters/local-filesystem-workspace.ts";
|
|
9
|
+
import { connectLectorClient } from "./client.ts";
|
|
10
|
+
import { LECTOR_PATH_NAMES } from "./constants.ts";
|
|
11
|
+
import { serveMain } from "./daemon.ts";
|
|
12
|
+
import type { JobSnapshot } from "./domain/bounded-job-executor.ts";
|
|
13
|
+
import type { ContentHash } from "./domain/content-hash.ts";
|
|
14
|
+
import type { WorkspacePort } from "./ports/workspace-port.ts";
|
|
15
|
+
import type { WorkspaceId } from "./service.ts";
|
|
16
|
+
|
|
17
|
+
const USAGE = `Usage:
|
|
18
|
+
lector serve [--workspace <id>]... [--workspace-path <id>=<dir>]... [--dynamic-workspaces]
|
|
19
|
+
at least one --workspace, --workspace-path, or --dynamic-workspaces is required
|
|
20
|
+
--workspace <id> ephemeral in-memory workspace (data lost on restart)
|
|
21
|
+
--workspace-path <id>=<dir> real directory <dir>, registered under <id>
|
|
22
|
+
--dynamic-workspaces start with none pre-registered; every workspace is added at
|
|
23
|
+
runtime via "lector workspace register" (workspace.registerPath) --
|
|
24
|
+
the mode a long-lived background daemon (e.g. lector.service) wants,
|
|
25
|
+
since it does not know upfront which project(s) will attach to it
|
|
26
|
+
lector service <install|start|stop|restart|status>
|
|
27
|
+
install: writes a user systemd unit (lector serve --dynamic-workspaces), enables + starts it
|
|
28
|
+
lector workspace register <dir> [--json]
|
|
29
|
+
lector workspace read <workspace-id> <path> [--json]
|
|
30
|
+
lector workspace edit <workspace-id> <path> --content <text> (--expected-hash <hash> | --create) [--json]
|
|
31
|
+
lector workspace symbols <workspace-id> <query> [--seed-file <path>] [--json]
|
|
32
|
+
lector workspace definition <workspace-id> <path> <line> <character> [--json]
|
|
33
|
+
lector workspace implementation <workspace-id> <path> <line> <character> [--json]
|
|
34
|
+
lector workspace references <workspace-id> <path> <line> <character> [--include-declaration] [--json]
|
|
35
|
+
lector workspace hover <workspace-id> <path> <line> <character> [--json]
|
|
36
|
+
lector workspace document-symbols <workspace-id> <path> [--json]
|
|
37
|
+
lector workspace diagnostics <workspace-id> <path> [--json]
|
|
38
|
+
lector workspace call-hierarchy <prepare|incoming|outgoing> <workspace-id> <path> <line> <character> [--json]
|
|
39
|
+
lector workspace populate-symbol-graph <workspace-id> --max-files <n> --max-symbols-per-file <n>
|
|
40
|
+
[--background] [--wait-ms <n>] [--json]
|
|
41
|
+
--background submits a bounded process-lifetime job; --wait-ms waits briefly for a fast result
|
|
42
|
+
lector job status <job-id> [--json]
|
|
43
|
+
lector workspace symbol-graph <reachable-from|edges-from|edges-to> <workspace-id> <path> <line> <character>
|
|
44
|
+
[--max-depth <n>] [--kind <calls|references|contains>] [--json]
|
|
45
|
+
--max-depth is required for reachable-from, ignored for edges-from/edges-to
|
|
46
|
+
lector workspace has-warm-index <workspace-id> [--json]
|
|
47
|
+
never spawns a symbol index -- reports whether one is already warm
|
|
48
|
+
lector workspace cache-status <workspace-id> --max-files <n> --max-symbols-per-file <n> [--json]
|
|
49
|
+
lector workspace git-status <workspace-id> [--json]
|
|
50
|
+
lector workspace git-log <workspace-id> --max-count <n> [--json]
|
|
51
|
+
lector workspace git-diff <workspace-id> [--ref <ref>] --max-bytes <n> [--json]
|
|
52
|
+
lector workspace repo-fetch <owner>/<repo>[@ref] [--host <host>] [--json]
|
|
53
|
+
shallow-clones an external repo into a disk-bounded cache and registers it read-only
|
|
54
|
+
lector workspace search-text <workspace-id> <query> --max-matches <n> --max-bytes <n> [--json]
|
|
55
|
+
lector search symbols <query> [--workspace <id>]... [--timeout-ms <n>] [--json]
|
|
56
|
+
lector search text <query> --max-matches <n> --max-bytes <n> [--workspace <id>]... [--timeout-ms <n>] [--json]
|
|
57
|
+
fans out across the given --workspace id(s); with none given, every currently-registered
|
|
58
|
+
workspace, daemon-wide -- this daemon is a shared service, so that default can include a
|
|
59
|
+
project a different, concurrent Pi session registered. Prefer explicit --workspace when you
|
|
60
|
+
mean "my own current projects".
|
|
61
|
+
a workspace whose language server is still cold-starting is reported as "loading", not
|
|
62
|
+
silently omitted and not blocking every other workspace's real results
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
function fail(message: string): never {
|
|
66
|
+
console.error(message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectFlagValues(args: string[], flag: string): string[] {
|
|
71
|
+
const values: string[] = [];
|
|
72
|
+
for (let index = 0; index < args.length; index++) {
|
|
73
|
+
if (args[index] === flag) {
|
|
74
|
+
const value = args[index + 1];
|
|
75
|
+
if (value === undefined) fail(`${flag} requires a value`);
|
|
76
|
+
values.push(value);
|
|
77
|
+
index++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return values;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function flagValue(args: string[], flag: string): string | undefined {
|
|
84
|
+
return collectFlagValues(args, flag).at(-1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
88
|
+
return args.includes(flag);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseWorkspacePathFlag(raw: string): { id: string; dir: string } {
|
|
92
|
+
const separatorIndex = raw.indexOf("=");
|
|
93
|
+
if (separatorIndex <= 0 || separatorIndex === raw.length - 1) {
|
|
94
|
+
fail(`--workspace-path expects <id>=<dir>, got "${raw}"`);
|
|
95
|
+
}
|
|
96
|
+
return { id: raw.slice(0, separatorIndex), dir: raw.slice(separatorIndex + 1) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function runServe(args: string[]): Promise<void> {
|
|
100
|
+
const memoryIds = collectFlagValues(args, "--workspace");
|
|
101
|
+
const pathEntries = collectFlagValues(args, "--workspace-path").map(parseWorkspacePathFlag);
|
|
102
|
+
const dynamicWorkspaces = hasFlag(args, "--dynamic-workspaces");
|
|
103
|
+
if (memoryIds.length === 0 && pathEntries.length === 0 && !dynamicWorkspaces) {
|
|
104
|
+
fail("lector serve requires at least one --workspace <id>, --workspace-path <id>=<dir>, or --dynamic-workspaces");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const workspaces = new Map<WorkspaceId, WorkspacePort>();
|
|
108
|
+
for (const id of memoryIds) workspaces.set(id, new InMemoryWorkspace());
|
|
109
|
+
for (const { id, dir } of pathEntries) workspaces.set(id, new LocalFilesystemWorkspace(dir));
|
|
110
|
+
|
|
111
|
+
const summary =
|
|
112
|
+
[...memoryIds.map((id) => `${id} (in-memory)`), ...pathEntries.map(({ id, dir }) => `${id} (${dir})`)].join(", ") || "none pre-registered, dynamic-only";
|
|
113
|
+
|
|
114
|
+
serveMain({
|
|
115
|
+
workspaces,
|
|
116
|
+
allowDynamicOnly: dynamicWorkspaces,
|
|
117
|
+
onListen: ({ host, port }) => {
|
|
118
|
+
console.error(`Lector listening on ${host}:${port} (workspaces: ${summary})`);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runWorkspaceRegister(dir: string | undefined, flags: string[]): Promise<void> {
|
|
124
|
+
if (!dir) fail(USAGE);
|
|
125
|
+
const client = await connectLectorClient();
|
|
126
|
+
const result = await client.call("workspace.registerPath", { path: dir });
|
|
127
|
+
console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : `${result.workspaceId} (${result.created ? "created" : "already registered"})`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runWorkspaceSymbols(workspaceId: string | undefined, query: string | undefined, flags: string[]): Promise<void> {
|
|
131
|
+
if (!workspaceId || !query) fail(USAGE);
|
|
132
|
+
const seedFile = flagValue(flags, "--seed-file"); // omit to auto-discover one
|
|
133
|
+
const client = await connectLectorClient();
|
|
134
|
+
const { symbols } = await client.call("workspace.findSymbols", { workspaceId, query, seedFile });
|
|
135
|
+
if (hasFlag(flags, "--json")) {
|
|
136
|
+
console.log(JSON.stringify(symbols));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (symbols.length === 0) {
|
|
140
|
+
console.log(`no symbols matched "${query}"`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const symbol of symbols) {
|
|
144
|
+
console.log(`${symbol.kind} ${symbol.name} -- ${symbol.location.path}:${symbol.location.line}:${symbol.location.character}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parsePosition(line: string | undefined, character: string | undefined): { line: number; character: number } {
|
|
149
|
+
const parsedLine = Number(line);
|
|
150
|
+
const parsedCharacter = Number(character);
|
|
151
|
+
if (!line || !character || !Number.isInteger(parsedLine) || !Number.isInteger(parsedCharacter)) {
|
|
152
|
+
fail(USAGE);
|
|
153
|
+
}
|
|
154
|
+
return { line: parsedLine, character: parsedCharacter };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runWorkspaceDefinition(workspaceId: string | undefined, path: string | undefined, rest: string[]): Promise<void> {
|
|
158
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
159
|
+
const [lineArg, characterArg, ...flags] = rest;
|
|
160
|
+
const { line, character } = parsePosition(lineArg, characterArg);
|
|
161
|
+
const client = await connectLectorClient();
|
|
162
|
+
const { locations } = await client.call("workspace.goToDefinition", { workspaceId, path, line, character });
|
|
163
|
+
if (hasFlag(flags, "--json")) {
|
|
164
|
+
console.log(JSON.stringify(locations));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (locations.length === 0) {
|
|
168
|
+
console.log("no definition found");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const location of locations) console.log(`${location.path}:${location.line}:${location.character}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runWorkspaceImplementation(workspaceId: string | undefined, path: string | undefined, rest: string[]): Promise<void> {
|
|
175
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
176
|
+
const [lineArg, characterArg, ...flags] = rest;
|
|
177
|
+
const { line, character } = parsePosition(lineArg, characterArg);
|
|
178
|
+
const client = await connectLectorClient();
|
|
179
|
+
const { locations } = await client.call("workspace.goToImplementation", { workspaceId, path, line, character });
|
|
180
|
+
if (hasFlag(flags, "--json")) {
|
|
181
|
+
console.log(JSON.stringify(locations));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (locations.length === 0) {
|
|
185
|
+
console.log("no implementation found");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const location of locations) console.log(`${location.path}:${location.line}:${location.character}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function runWorkspaceReferences(workspaceId: string | undefined, path: string | undefined, rest: string[]): Promise<void> {
|
|
192
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
193
|
+
const [lineArg, characterArg, ...flags] = rest;
|
|
194
|
+
const { line, character } = parsePosition(lineArg, characterArg);
|
|
195
|
+
const includeDeclaration = hasFlag(flags, "--include-declaration");
|
|
196
|
+
const client = await connectLectorClient();
|
|
197
|
+
const { locations } = await client.call("workspace.findReferences", { workspaceId, path, line, character, includeDeclaration });
|
|
198
|
+
if (hasFlag(flags, "--json")) {
|
|
199
|
+
console.log(JSON.stringify(locations));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (locations.length === 0) {
|
|
203
|
+
console.log("no references found");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const location of locations) console.log(`${location.path}:${location.line}:${location.character}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runWorkspaceHover(workspaceId: string | undefined, path: string | undefined, rest: string[]): Promise<void> {
|
|
210
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
211
|
+
const [lineArg, characterArg, ...flags] = rest;
|
|
212
|
+
const { line, character } = parsePosition(lineArg, characterArg);
|
|
213
|
+
const client = await connectLectorClient();
|
|
214
|
+
const { hover } = await client.call("workspace.hover", { workspaceId, path, line, character });
|
|
215
|
+
if (hasFlag(flags, "--json")) {
|
|
216
|
+
console.log(JSON.stringify(hover ?? null));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log(hover ? hover.contents : "no hover information available");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runWorkspaceDocumentSymbols(workspaceId: string | undefined, path: string | undefined, flags: string[]): Promise<void> {
|
|
223
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
224
|
+
const client = await connectLectorClient();
|
|
225
|
+
const { symbols } = await client.call("workspace.documentSymbols", { workspaceId, path });
|
|
226
|
+
if (hasFlag(flags, "--json")) {
|
|
227
|
+
console.log(JSON.stringify(symbols));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (symbols.length === 0) {
|
|
231
|
+
console.log("no symbols found");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const printEntry = (entry: (typeof symbols)[number], depth: number): void => {
|
|
235
|
+
console.log(`${" ".repeat(depth)}${entry.kind} ${entry.name} -- ${entry.range.path}:${entry.range.start.line}:${entry.range.start.character}`);
|
|
236
|
+
for (const child of entry.children ?? []) printEntry(child, depth + 1);
|
|
237
|
+
};
|
|
238
|
+
for (const entry of symbols) printEntry(entry, 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function runWorkspaceDiagnostics(workspaceId: string | undefined, path: string | undefined, flags: string[]): Promise<void> {
|
|
242
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
243
|
+
const client = await connectLectorClient();
|
|
244
|
+
const { diagnostics } = await client.call("workspace.diagnostics", { workspaceId, path });
|
|
245
|
+
if (hasFlag(flags, "--json")) {
|
|
246
|
+
console.log(JSON.stringify(diagnostics));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (diagnostics.length === 0) {
|
|
250
|
+
console.log("no diagnostics");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
for (const diagnostic of diagnostics) {
|
|
254
|
+
console.log(
|
|
255
|
+
`${diagnostic.severity} ${diagnostic.range.path}:${diagnostic.range.start.line}:${diagnostic.range.start.character} -- ${diagnostic.message}${diagnostic.source ? ` (${diagnostic.source}${diagnostic.code !== undefined ? ` ${diagnostic.code}` : ""})` : ""}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function formatCallHierarchyEntry(entry: { kind: string; name: string; location: { path: string; line: number; character: number } }): string {
|
|
261
|
+
return `${entry.kind} ${entry.name} -- ${entry.location.path}:${entry.location.line}:${entry.location.character}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function runWorkspaceCallHierarchy(
|
|
265
|
+
subcommand: string | undefined,
|
|
266
|
+
workspaceId: string | undefined,
|
|
267
|
+
path: string | undefined,
|
|
268
|
+
rest: string[],
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
271
|
+
const [lineArg, characterArg, ...flags] = rest;
|
|
272
|
+
const { line, character } = parsePosition(lineArg, characterArg);
|
|
273
|
+
const client = await connectLectorClient();
|
|
274
|
+
|
|
275
|
+
if (subcommand === "prepare") {
|
|
276
|
+
const { items } = await client.call("workspace.prepareCallHierarchy", { workspaceId, path, line, character });
|
|
277
|
+
if (hasFlag(flags, "--json")) {
|
|
278
|
+
console.log(JSON.stringify(items));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (items.length === 0) {
|
|
282
|
+
console.log("no call-hierarchy root at this position");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const item of items) console.log(formatCallHierarchyEntry(item));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (subcommand === "incoming") {
|
|
289
|
+
const { calls } = await client.call("workspace.incomingCalls", { workspaceId, path, line, character });
|
|
290
|
+
if (hasFlag(flags, "--json")) {
|
|
291
|
+
console.log(JSON.stringify(calls));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (calls.length === 0) {
|
|
295
|
+
console.log("no incoming calls found");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
for (const call of calls) console.log(formatCallHierarchyEntry(call.from));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (subcommand === "outgoing") {
|
|
302
|
+
const { calls } = await client.call("workspace.outgoingCalls", { workspaceId, path, line, character });
|
|
303
|
+
if (hasFlag(flags, "--json")) {
|
|
304
|
+
console.log(JSON.stringify(calls));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (calls.length === 0) {
|
|
308
|
+
console.log("no outgoing calls found");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
for (const call of calls) console.log(formatCallHierarchyEntry(call.to));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
fail(USAGE);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatSymbolNode(node: { kind: string; name: string; location: { path: string; line: number; character: number } }): string {
|
|
318
|
+
return `${node.kind} ${node.name} -- ${node.location.path}:${node.location.line}:${node.location.character}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function requiredIntFlag(flags: string[], flag: string): number {
|
|
322
|
+
const raw = flagValue(flags, flag);
|
|
323
|
+
const parsed = Number(raw);
|
|
324
|
+
if (raw === undefined || !Number.isInteger(parsed)) fail(`${flag} <n> is required`);
|
|
325
|
+
return parsed;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatJobSnapshot(job: JobSnapshot<{ filesProcessed: number; symbolsProcessed: number; nodesAdded: number; edgesAdded: number }>): string {
|
|
329
|
+
if (job.status === "queued") return `${job.id}: queued (${job.operation}); poll with: lector job status ${job.id}`;
|
|
330
|
+
if (job.status === "running") return `${job.id}: still running (${job.operation}); poll with: lector job status ${job.id}`;
|
|
331
|
+
if (job.status === "failed") return `${job.id}: failed [${job.error.code}] -- ${job.error.message}`;
|
|
332
|
+
return `${job.id}: succeeded -- ${job.result.filesProcessed} files, ${job.result.symbolsProcessed} symbols, ${job.result.nodesAdded} nodes, ${job.result.edgesAdded} edges`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function runWorkspacePopulateSymbolGraph(workspaceId: string | undefined, flags: string[]): Promise<void> {
|
|
336
|
+
if (!workspaceId) fail(USAGE);
|
|
337
|
+
const maxFiles = requiredIntFlag(flags, "--max-files");
|
|
338
|
+
const maxSymbolsPerFile = requiredIntFlag(flags, "--max-symbols-per-file");
|
|
339
|
+
const client = await connectLectorClient();
|
|
340
|
+
if (hasFlag(flags, "--background")) {
|
|
341
|
+
const waitMsRaw = flagValue(flags, "--wait-ms");
|
|
342
|
+
const waitMs = waitMsRaw === undefined ? 0 : Number(waitMsRaw);
|
|
343
|
+
const { job } = await client.call("job.submit", {
|
|
344
|
+
operation: "workspace.populateSymbolGraph",
|
|
345
|
+
input: { workspaceId, maxFiles, maxSymbolsPerFile },
|
|
346
|
+
waitMs,
|
|
347
|
+
});
|
|
348
|
+
console.log(hasFlag(flags, "--json") ? JSON.stringify(job) : formatJobSnapshot(job));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const result = await client.call("workspace.populateSymbolGraph", { workspaceId, maxFiles, maxSymbolsPerFile });
|
|
352
|
+
console.log(
|
|
353
|
+
hasFlag(flags, "--json")
|
|
354
|
+
? JSON.stringify(result)
|
|
355
|
+
: `${result.filesProcessed} files, ${result.symbolsProcessed} symbols, ${result.nodesAdded} nodes, ${result.edgesAdded} edges`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function runJobStatus(jobId: string | undefined, flags: string[]): Promise<void> {
|
|
360
|
+
if (!jobId) fail(USAGE);
|
|
361
|
+
const client = await connectLectorClient();
|
|
362
|
+
const { job } = await client.call("job.status", { jobId });
|
|
363
|
+
console.log(hasFlag(flags, "--json") ? JSON.stringify(job) : formatJobSnapshot(job));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function runWorkspaceCacheStatus(workspaceId: string | undefined, flags: string[]): Promise<void> {
|
|
367
|
+
if (!workspaceId) fail(USAGE);
|
|
368
|
+
const maxFiles = requiredIntFlag(flags, "--max-files");
|
|
369
|
+
const maxSymbolsPerFile = requiredIntFlag(flags, "--max-symbols-per-file");
|
|
370
|
+
const client = await connectLectorClient();
|
|
371
|
+
const status = await client.call("workspace.cacheStatus", { workspaceId, maxFiles, maxSymbolsPerFile });
|
|
372
|
+
if (hasFlag(flags, "--json")) {
|
|
373
|
+
console.log(JSON.stringify(status));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (status.status === "not-cached") console.log(`not cached -- ${status.reason}`);
|
|
377
|
+
else if (status.status === "caching") console.log(`caching -- job ${status.jobId}`);
|
|
378
|
+
else console.log(`cached -- completed ${new Date(status.generation.completedAt).toISOString()}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function runWorkspaceHasWarmIndex(workspaceId: string | undefined, flags: string[]): Promise<void> {
|
|
382
|
+
if (!workspaceId) fail(USAGE);
|
|
383
|
+
const client = await connectLectorClient();
|
|
384
|
+
const { warm } = await client.call("workspace.hasWarmIndex", { workspaceId });
|
|
385
|
+
console.log(hasFlag(flags, "--json") ? JSON.stringify({ warm }) : warm ? "warm" : "not warm");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function runWorkspaceGitStatus(workspaceId: string | undefined, flags: string[]): Promise<void> {
|
|
389
|
+
if (!workspaceId) fail(USAGE);
|
|
390
|
+
const client = await connectLectorClient();
|
|
391
|
+
const summary = await client.call("workspace.gitStatus", { workspaceId });
|
|
392
|
+
if (hasFlag(flags, "--json")) {
|
|
393
|
+
console.log(JSON.stringify(summary));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const branch = summary.current ?? "(detached)";
|
|
397
|
+
const tracking = summary.tracking ? `, tracking ${summary.tracking} (+${summary.ahead}/-${summary.behind})` : "";
|
|
398
|
+
console.log(`On branch ${branch}${tracking}`);
|
|
399
|
+
if (summary.files.length === 0) {
|
|
400
|
+
console.log("working tree clean");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
for (const file of summary.files) {
|
|
404
|
+
const code = `${file.indexStatus}${file.workingDirStatus}`;
|
|
405
|
+
console.log(file.renamedFrom ? `${code} ${file.renamedFrom} -> ${file.path}` : `${code} ${file.path}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function runWorkspaceGitLog(workspaceId: string | undefined, flags: string[]): Promise<void> {
|
|
410
|
+
if (!workspaceId) fail(USAGE);
|
|
411
|
+
const maxCount = requiredIntFlag(flags, "--max-count");
|
|
412
|
+
const client = await connectLectorClient();
|
|
413
|
+
const { entries } = await client.call("workspace.gitLog", { workspaceId, maxCount });
|
|
414
|
+
if (hasFlag(flags, "--json")) {
|
|
415
|
+
console.log(JSON.stringify(entries));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
for (const entry of entries) console.log(`${entry.sha.slice(0, 8)} ${entry.authoredAt} ${entry.authorName} -- ${entry.message}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function runWorkspaceGitDiff(workspaceId: string | undefined, flags: string[]): Promise<void> {
|
|
422
|
+
if (!workspaceId) fail(USAGE);
|
|
423
|
+
const ref = flagValue(flags, "--ref");
|
|
424
|
+
const maxBytes = requiredIntFlag(flags, "--max-bytes");
|
|
425
|
+
const client = await connectLectorClient();
|
|
426
|
+
const result = await client.call("workspace.gitDiff", { workspaceId, ref, maxBytes });
|
|
427
|
+
if (hasFlag(flags, "--json")) {
|
|
428
|
+
console.log(JSON.stringify(result));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
console.log(result.diff);
|
|
432
|
+
if (result.truncated) console.log("... (truncated)");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Parses "owner/repo[@ref]" into the explicit fields repo.fetch expects; --host overrides the "github.com" default. */
|
|
436
|
+
function parseRepoSpec(spec: string, host: string): { host: string; owner: string; repo: string; ref: string | null } {
|
|
437
|
+
const [ownerRepo, ref] = spec.split("@");
|
|
438
|
+
const [owner, repo] = (ownerRepo ?? "").split("/");
|
|
439
|
+
if (!owner || !repo) fail(`repo spec must be "<owner>/<repo>[@ref]", got "${spec}"`);
|
|
440
|
+
return { host, owner, repo, ref: ref ?? null };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function runWorkspaceRepoFetch(spec: string | undefined, flags: string[]): Promise<void> {
|
|
444
|
+
if (!spec) fail(USAGE);
|
|
445
|
+
const host = flagValue(flags, "--host") ?? "github.com";
|
|
446
|
+
const reference = parseRepoSpec(spec, host);
|
|
447
|
+
const client = await connectLectorClient();
|
|
448
|
+
const result = await client.call("repo.fetch", reference);
|
|
449
|
+
if (hasFlag(flags, "--json")) {
|
|
450
|
+
console.log(JSON.stringify(result));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log(`${result.workspaceId} ${result.fromCache ? "(from cache)" : "(fetched)"} -- ${result.path}`);
|
|
454
|
+
if (result.refFallbackOccurred) console.log(`note: requested ref not found, fell back to the default branch (resolved: ${result.resolvedRef})`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function runWorkspaceSearchText(workspaceId: string | undefined, query: string | undefined, flags: string[]): Promise<void> {
|
|
458
|
+
if (!workspaceId || !query) fail(USAGE);
|
|
459
|
+
const maxMatches = requiredIntFlag(flags, "--max-matches");
|
|
460
|
+
const maxBytes = requiredIntFlag(flags, "--max-bytes");
|
|
461
|
+
const client = await connectLectorClient();
|
|
462
|
+
const result = await client.call("workspace.searchText", { workspaceId, query, maxMatches, maxBytes });
|
|
463
|
+
if (hasFlag(flags, "--json")) {
|
|
464
|
+
console.log(JSON.stringify(result));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (result.matches.length === 0) {
|
|
468
|
+
console.log(`no matches for "${query}"`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
for (const match of result.matches) console.log(`${match.path}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}`);
|
|
472
|
+
if (result.truncated) console.log("... (truncated)");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function runSearchSymbols(query: string | undefined, flags: string[]): Promise<void> {
|
|
476
|
+
if (!query) fail(USAGE);
|
|
477
|
+
const timeoutMs = flagValue(flags, "--timeout-ms");
|
|
478
|
+
const workspaceIds = collectFlagValues(flags, "--workspace");
|
|
479
|
+
const client = await connectLectorClient();
|
|
480
|
+
const { results } = await client.call("search.symbols", {
|
|
481
|
+
query,
|
|
482
|
+
workspaceIds: workspaceIds.length === 0 ? undefined : workspaceIds,
|
|
483
|
+
timeoutMs: timeoutMs === undefined ? undefined : Number(timeoutMs),
|
|
484
|
+
});
|
|
485
|
+
if (hasFlag(flags, "--json")) {
|
|
486
|
+
console.log(JSON.stringify(results));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (results.length === 0) {
|
|
490
|
+
console.log("no workspaces registered with a known root to search");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
for (const outcome of results) {
|
|
494
|
+
if (outcome.status === "loading") {
|
|
495
|
+
console.log(`${outcome.workspaceId}: still loading -- ${outcome.message}`);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (outcome.status === "error") {
|
|
499
|
+
console.log(`${outcome.workspaceId}: error -- ${outcome.message}`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (outcome.result.symbols.length === 0) {
|
|
503
|
+
console.log(`${outcome.workspaceId}: no symbols matched "${query}"`);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
for (const symbol of outcome.result.symbols) {
|
|
507
|
+
console.log(`${outcome.workspaceId}: ${symbol.kind} ${symbol.name} -- ${symbol.location.path}:${symbol.location.line}:${symbol.location.character}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function runSearchText(query: string | undefined, flags: string[]): Promise<void> {
|
|
513
|
+
if (!query) fail(USAGE);
|
|
514
|
+
const maxMatches = requiredIntFlag(flags, "--max-matches");
|
|
515
|
+
const maxBytes = requiredIntFlag(flags, "--max-bytes");
|
|
516
|
+
const timeoutMs = flagValue(flags, "--timeout-ms");
|
|
517
|
+
const workspaceIds = collectFlagValues(flags, "--workspace");
|
|
518
|
+
const client = await connectLectorClient();
|
|
519
|
+
const { results } = await client.call("search.text", {
|
|
520
|
+
query,
|
|
521
|
+
maxMatches,
|
|
522
|
+
maxBytes,
|
|
523
|
+
workspaceIds: workspaceIds.length === 0 ? undefined : workspaceIds,
|
|
524
|
+
timeoutMs: timeoutMs === undefined ? undefined : Number(timeoutMs),
|
|
525
|
+
});
|
|
526
|
+
if (hasFlag(flags, "--json")) {
|
|
527
|
+
console.log(JSON.stringify(results));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (results.length === 0) {
|
|
531
|
+
console.log("no workspaces registered with a known root to search");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
for (const outcome of results) {
|
|
535
|
+
if (outcome.status === "loading") {
|
|
536
|
+
console.log(`${outcome.workspaceId}: still loading -- ${outcome.message}`);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (outcome.status === "error") {
|
|
540
|
+
console.log(`${outcome.workspaceId}: error -- ${outcome.message}`);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (outcome.result.matches.length === 0) {
|
|
544
|
+
console.log(`${outcome.workspaceId}: no matches for "${query}"`);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
for (const match of outcome.result.matches) {
|
|
548
|
+
console.log(`${outcome.workspaceId}: ${match.path}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}`);
|
|
549
|
+
}
|
|
550
|
+
if (outcome.result.truncated) console.log(`${outcome.workspaceId}: ... (truncated)`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function parseSymbolEdgeKind(flags: string[]): "calls" | "references" | "contains" | undefined {
|
|
555
|
+
const raw = flagValue(flags, "--kind");
|
|
556
|
+
if (raw === undefined) return undefined;
|
|
557
|
+
if (raw !== "calls" && raw !== "references" && raw !== "contains") fail(`--kind must be calls, references, or contains; got "${raw}"`);
|
|
558
|
+
return raw;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function runWorkspaceSymbolGraphQuery(
|
|
562
|
+
subcommand: string | undefined,
|
|
563
|
+
workspaceId: string | undefined,
|
|
564
|
+
path: string | undefined,
|
|
565
|
+
rest: string[],
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
568
|
+
const [lineArg, characterArg, ...flags] = rest;
|
|
569
|
+
const { line, character } = parsePosition(lineArg, characterArg);
|
|
570
|
+
const kind = parseSymbolEdgeKind(flags);
|
|
571
|
+
const client = await connectLectorClient();
|
|
572
|
+
|
|
573
|
+
if (subcommand === "reachable-from") {
|
|
574
|
+
const maxDepth = requiredIntFlag(flags, "--max-depth");
|
|
575
|
+
const { symbols } = await client.call("workspace.reachableFrom", { workspaceId, path, line, character, maxDepth, kind });
|
|
576
|
+
if (hasFlag(flags, "--json")) {
|
|
577
|
+
console.log(JSON.stringify(symbols));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (symbols.length === 0) {
|
|
581
|
+
console.log("nothing reachable at this position (has the graph been populated for this workspace?)");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
for (const symbol of symbols) console.log(formatSymbolNode(symbol));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (subcommand === "edges-from" || subcommand === "edges-to") {
|
|
588
|
+
const operation = subcommand === "edges-from" ? "workspace.symbolEdgesFrom" : "workspace.symbolEdgesTo";
|
|
589
|
+
const { symbols } = await client.call(operation, { workspaceId, path, line, character, kind });
|
|
590
|
+
if (hasFlag(flags, "--json")) {
|
|
591
|
+
console.log(JSON.stringify(symbols));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (symbols.length === 0) {
|
|
595
|
+
console.log("no edges found (has the graph been populated for this workspace?)");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
for (const symbol of symbols) console.log(formatSymbolNode(symbol));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
fail(USAGE);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function runWorkspaceRead(workspaceId: string | undefined, path: string | undefined, flags: string[]): Promise<void> {
|
|
605
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
606
|
+
const client = await connectLectorClient();
|
|
607
|
+
const result = await client.call("workspace.rawRead", { workspaceId, path });
|
|
608
|
+
console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : `${result.path} [${result.hash}]\n${result.content}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function runWorkspaceEdit(workspaceId: string | undefined, path: string | undefined, flags: string[]): Promise<void> {
|
|
612
|
+
if (!workspaceId || !path) fail(USAGE);
|
|
613
|
+
const content = flagValue(flags, "--content");
|
|
614
|
+
if (content === undefined) fail("lector workspace edit requires --content <text>");
|
|
615
|
+
|
|
616
|
+
const create = hasFlag(flags, "--create");
|
|
617
|
+
const expectedHashFlag = flagValue(flags, "--expected-hash");
|
|
618
|
+
if (create === (expectedHashFlag !== undefined)) {
|
|
619
|
+
fail("lector workspace edit requires exactly one of --create or --expected-hash <hash>");
|
|
620
|
+
}
|
|
621
|
+
const expectedHash = create ? null : (expectedHashFlag as ContentHash);
|
|
622
|
+
|
|
623
|
+
const client = await connectLectorClient();
|
|
624
|
+
const result = await client.call("workspace.exactEdit", { workspaceId, path, expectedHash, content });
|
|
625
|
+
console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : `${result.path}: ${result.previousHash ?? "(new)"} -> ${result.newHash}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* systemd user-unit lifecycle (`install|start|stop|restart|status`) for a
|
|
630
|
+
* persistent Lector daemon. `install` always runs `serve
|
|
631
|
+
* --dynamic-workspaces`: a long-lived background daemon cannot know
|
|
632
|
+
* upfront which project(s) will attach to it, so it starts with zero
|
|
633
|
+
* pre-registered workspaces and relies entirely on workspace.registerPath
|
|
634
|
+
* at runtime.
|
|
635
|
+
*/
|
|
636
|
+
export interface SystemdUnitOptions {
|
|
637
|
+
bunBin: string;
|
|
638
|
+
cliPath: string;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function renderSystemdUnit(options: SystemdUnitOptions): string {
|
|
642
|
+
return `[Unit]
|
|
643
|
+
Description=Lector filesystem & code-intelligence service
|
|
644
|
+
After=default.target
|
|
645
|
+
|
|
646
|
+
[Service]
|
|
647
|
+
Type=simple
|
|
648
|
+
ExecStart=${options.bunBin} ${options.cliPath} serve --dynamic-workspaces
|
|
649
|
+
Restart=always
|
|
650
|
+
RestartSec=2
|
|
651
|
+
|
|
652
|
+
[Install]
|
|
653
|
+
WantedBy=default.target
|
|
654
|
+
`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function unitPath(): string {
|
|
658
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
|
|
659
|
+
return join(configHome, "systemd", "user", LECTOR_PATH_NAMES.systemdUnitName);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function systemctl(...args: string[]): void {
|
|
663
|
+
execFileSync("systemctl", ["--user", ...args], { stdio: "inherit" });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function installService(): void {
|
|
667
|
+
const path = unitPath();
|
|
668
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
669
|
+
writeFileSync(path, renderSystemdUnit({ bunBin: process.execPath, cliPath: fileURLToPath(import.meta.url) }));
|
|
670
|
+
systemctl("daemon-reload");
|
|
671
|
+
systemctl("enable", LECTOR_PATH_NAMES.systemdUnitName);
|
|
672
|
+
systemctl("restart", LECTOR_PATH_NAMES.systemdUnitName);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function runService(action: string | undefined): void {
|
|
676
|
+
switch (action) {
|
|
677
|
+
case "install":
|
|
678
|
+
installService();
|
|
679
|
+
return;
|
|
680
|
+
case "start":
|
|
681
|
+
systemctl("start", LECTOR_PATH_NAMES.systemdUnitName);
|
|
682
|
+
return;
|
|
683
|
+
case "stop":
|
|
684
|
+
systemctl("stop", LECTOR_PATH_NAMES.systemdUnitName);
|
|
685
|
+
return;
|
|
686
|
+
case "restart":
|
|
687
|
+
systemctl("restart", LECTOR_PATH_NAMES.systemdUnitName);
|
|
688
|
+
return;
|
|
689
|
+
case "status":
|
|
690
|
+
systemctl("status", LECTOR_PATH_NAMES.systemdUnitName);
|
|
691
|
+
return;
|
|
692
|
+
default:
|
|
693
|
+
fail(USAGE);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function main(): Promise<void> {
|
|
698
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
699
|
+
|
|
700
|
+
if (command === "serve") return runServe(rest);
|
|
701
|
+
if (command === "service") return runService(rest[0]);
|
|
702
|
+
|
|
703
|
+
if (command === "search") {
|
|
704
|
+
const [action, query, ...searchFlags] = rest;
|
|
705
|
+
if (action === "symbols") return runSearchSymbols(query, searchFlags);
|
|
706
|
+
if (action === "text") return runSearchText(query, searchFlags);
|
|
707
|
+
fail(USAGE);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (command === "job") {
|
|
711
|
+
const [action, jobId, ...jobFlags] = rest;
|
|
712
|
+
if (action === "status") return runJobStatus(jobId, jobFlags);
|
|
713
|
+
fail(USAGE);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (command === "workspace") {
|
|
717
|
+
const [action, ...actionArgs] = rest;
|
|
718
|
+
if (action === "register") {
|
|
719
|
+
const [dir, ...flags] = actionArgs;
|
|
720
|
+
return runWorkspaceRegister(dir, flags);
|
|
721
|
+
}
|
|
722
|
+
const [workspaceId, path, ...flags] = actionArgs;
|
|
723
|
+
if (action === "read") return runWorkspaceRead(workspaceId, path, flags);
|
|
724
|
+
if (action === "edit") return runWorkspaceEdit(workspaceId, path, flags);
|
|
725
|
+
if (action === "symbols") return runWorkspaceSymbols(workspaceId, path, flags);
|
|
726
|
+
if (action === "search-text") return runWorkspaceSearchText(workspaceId, path, flags);
|
|
727
|
+
if (action === "definition") return runWorkspaceDefinition(workspaceId, path, flags);
|
|
728
|
+
if (action === "implementation") return runWorkspaceImplementation(workspaceId, path, flags);
|
|
729
|
+
if (action === "references") return runWorkspaceReferences(workspaceId, path, flags);
|
|
730
|
+
if (action === "hover") return runWorkspaceHover(workspaceId, path, flags);
|
|
731
|
+
if (action === "document-symbols") return runWorkspaceDocumentSymbols(workspaceId, path, flags);
|
|
732
|
+
if (action === "diagnostics") return runWorkspaceDiagnostics(workspaceId, path, flags);
|
|
733
|
+
if (action === "call-hierarchy") {
|
|
734
|
+
const [subcommand, chWorkspaceId, chPath, ...chRest] = actionArgs;
|
|
735
|
+
return runWorkspaceCallHierarchy(subcommand, chWorkspaceId, chPath, chRest);
|
|
736
|
+
}
|
|
737
|
+
if (action === "populate-symbol-graph") {
|
|
738
|
+
const [psgWorkspaceId, ...psgFlags] = actionArgs;
|
|
739
|
+
return runWorkspacePopulateSymbolGraph(psgWorkspaceId, psgFlags);
|
|
740
|
+
}
|
|
741
|
+
if (action === "symbol-graph") {
|
|
742
|
+
const [subcommand, sgWorkspaceId, sgPath, ...sgRest] = actionArgs;
|
|
743
|
+
return runWorkspaceSymbolGraphQuery(subcommand, sgWorkspaceId, sgPath, sgRest);
|
|
744
|
+
}
|
|
745
|
+
if (action === "has-warm-index") {
|
|
746
|
+
const [hwiWorkspaceId, ...hwiFlags] = actionArgs;
|
|
747
|
+
return runWorkspaceHasWarmIndex(hwiWorkspaceId, hwiFlags);
|
|
748
|
+
}
|
|
749
|
+
if (action === "cache-status") {
|
|
750
|
+
const [cacheWorkspaceId, ...cacheFlags] = actionArgs;
|
|
751
|
+
return runWorkspaceCacheStatus(cacheWorkspaceId, cacheFlags);
|
|
752
|
+
}
|
|
753
|
+
if (action === "git-status" || action === "git-log" || action === "git-diff") {
|
|
754
|
+
// None of these take a <path> positional -- the generic [workspaceId, path, ...flags]
|
|
755
|
+
// destructure above would misparse the first flag as path (the exact bug
|
|
756
|
+
// populate-symbol-graph's own CLI wiring hit).
|
|
757
|
+
const [gitWorkspaceId, ...gitFlags] = actionArgs;
|
|
758
|
+
if (action === "git-status") return runWorkspaceGitStatus(gitWorkspaceId, gitFlags);
|
|
759
|
+
if (action === "git-log") return runWorkspaceGitLog(gitWorkspaceId, gitFlags);
|
|
760
|
+
return runWorkspaceGitDiff(gitWorkspaceId, gitFlags);
|
|
761
|
+
}
|
|
762
|
+
if (action === "repo-fetch") {
|
|
763
|
+
const [spec, ...repoFlags] = actionArgs;
|
|
764
|
+
return runWorkspaceRepoFetch(spec, repoFlags);
|
|
765
|
+
}
|
|
766
|
+
fail(USAGE);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
fail(USAGE);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (import.meta.main) {
|
|
773
|
+
main().catch((error: unknown) => {
|
|
774
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
775
|
+
process.exit(1);
|
|
776
|
+
});
|
|
777
|
+
}
|