@cassiomc1/forgeloop 1.10.2 → 1.11.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/.cursor/rules/project-loop.mdc +2 -2
- package/.forgeloop/forgeloop.gitignore +1 -0
- package/.github/copilot-instructions.md +2 -2
- package/AGENTS.md +1 -0
- package/AGENT_COMPATIBILITY.md +7 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +10 -0
- package/LOOP_SYSTEM_DESIGN.md +12 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +17 -0
- package/README.md +12 -10
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/THREAT_MODEL.md +21 -0
- package/benchmarks/repository-index/README.md +73 -0
- package/benchmarks/repository-index/queries.json +12 -0
- package/benchmarks/repository-index/run-hot-path.mjs +142 -0
- package/benchmarks/repository-index/run-persistent-transport.mjs +169 -0
- package/completions/_forgeloop +7 -1
- package/completions/forgeloop.bash +13 -1
- package/completions/forgeloop.fish +40 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +8 -1
- package/docs/CLI_REFERENCE.md +114 -2
- package/docs/DOCUMENTATION_GUIDE.md +10 -5
- package/docs/GETTING_STARTED.md +17 -0
- package/docs/MCP.md +16 -1
- package/docs/PACKAGE_CONTENTS.md +7 -1
- package/docs/PERSISTENT_SEARCH_TRANSPORT.md +289 -0
- package/docs/RECIPES.md +30 -0
- package/docs/RELEASE_CHECKLIST.md +34 -0
- package/docs/REPOSITORY_INDEX.md +553 -0
- package/docs/TROUBLESHOOTING.md +147 -0
- package/docs/UNIVERSAL_INTEGRATION.md +30 -0
- package/docs/diagrams/README.md +10 -0
- package/package.json +11 -2
- package/scripts/update-tgrep-manifest.mjs +86 -0
- package/scripts/verify-tgrep-manifest.mjs +17 -0
- package/src/cli.js +35 -0
- package/src/commands/doctor.js +76 -1
- package/src/commands/index-rebuild.js +1 -0
- package/src/commands/index-setup.js +1 -0
- package/src/commands/index-start.js +1 -0
- package/src/commands/index-status.js +1 -0
- package/src/commands/index-stop.js +1 -0
- package/src/commands/init.js +39 -1
- package/src/commands/repository-index.js +111 -0
- package/src/commands/search.js +1 -0
- package/src/commands/update.js +32 -4
- package/src/core/cli-command-definitions.js +97 -3
- package/src/core/command-executors.js +35 -2
- package/src/core/command-input.js +23 -0
- package/src/core/error-codes.js +195 -0
- package/src/core/filesystem.js +10 -1
- package/src/core/integration-invocation-policy.js +27 -0
- package/src/core/integration-resources.js +16 -1
- package/src/core/protocol-info.js +23 -0
- package/src/integration.d.ts +90 -0
- package/src/integration.js +21 -0
- package/src/persistent-transport/client.js +293 -0
- package/src/persistent-transport/constants.js +24 -0
- package/src/persistent-transport/errors.js +38 -0
- package/src/persistent-transport/framing.js +61 -0
- package/src/persistent-transport/lifecycle.js +116 -0
- package/src/persistent-transport/ownership.js +184 -0
- package/src/persistent-transport/paths.js +31 -0
- package/src/persistent-transport/protocol.js +95 -0
- package/src/persistent-transport/server.js +256 -0
- package/src/persistent-transport/state.js +49 -0
- package/src/repository-index/args.js +59 -0
- package/src/repository-index/binary-manager.js +413 -0
- package/src/repository-index/constants.js +45 -0
- package/src/repository-index/errors.js +38 -0
- package/src/repository-index/lifecycle.js +17 -0
- package/src/repository-index/lock.js +113 -0
- package/src/repository-index/manifest.js +132 -0
- package/src/repository-index/metrics.js +30 -0
- package/src/repository-index/normalize-json.js +187 -0
- package/src/repository-index/paths.js +39 -0
- package/src/repository-index/platform.js +20 -0
- package/src/repository-index/process.js +140 -0
- package/src/repository-index/readiness.js +62 -0
- package/src/repository-index/search.js +262 -0
- package/src/repository-index/server.js +432 -0
- package/src/repository-index/status.js +397 -0
- package/src/repository-index/tgrep-manifest.json +38 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { getCanonicalRepositorySearchArgs, appendSearchFilters } from "./args.js";
|
|
4
|
+
import { REPOSITORY_INDEX_DEFAULTS, REPOSITORY_INDEX_ENV_BINARY } from "./constants.js";
|
|
5
|
+
import { REPOSITORY_INDEX_ERROR_CODES, repositoryIndexError } from "./errors.js";
|
|
6
|
+
import { getManagedTgrepBinaryPath, getTgrepIndexPath } from "./paths.js";
|
|
7
|
+
import { getRepositoryIndexPlatform } from "./platform.js";
|
|
8
|
+
import { loadTgrepManifest } from "./manifest.js";
|
|
9
|
+
import { createTgrepBinaryHandle, runTgrep } from "./process.js";
|
|
10
|
+
import { normalizeTgrepJson } from "./normalize-json.js";
|
|
11
|
+
import { setupRepositoryIndex } from "./server.js";
|
|
12
|
+
import { getRepositoryIndexStatus } from "./status.js";
|
|
13
|
+
import { buildRepositorySearchMetrics } from "./metrics.js";
|
|
14
|
+
import {
|
|
15
|
+
getRepositoryIndexReadiness,
|
|
16
|
+
invalidateRepositoryIndexReadiness,
|
|
17
|
+
isRepositoryIndexReadinessUsable,
|
|
18
|
+
markRepositoryIndexSearchSucceeded,
|
|
19
|
+
rememberRepositoryIndexReadiness,
|
|
20
|
+
} from "./readiness.js";
|
|
21
|
+
|
|
22
|
+
const MAX_PATTERN_CHARS = 4_096;
|
|
23
|
+
const MAX_FILTERS = 32;
|
|
24
|
+
|
|
25
|
+
function validateSearchRequest({ pattern, globs = [], types = [], context, beforeContext, afterContext, maxCount } = {}) {
|
|
26
|
+
if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > MAX_PATTERN_CHARS) {
|
|
27
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.REQUEST_INVALID, `Search pattern must contain 1-${MAX_PATTERN_CHARS} characters`);
|
|
28
|
+
}
|
|
29
|
+
for (const [label, values] of [["globs", globs], ["types", types]]) {
|
|
30
|
+
if (!Array.isArray(values) || values.length > MAX_FILTERS || values.some((value) => typeof value !== "string" || value.length === 0 || value.length > 256)) {
|
|
31
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.REQUEST_INVALID, `Search ${label} must contain at most ${MAX_FILTERS} bounded strings`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const [label, value] of [["context", context], ["beforeContext", beforeContext], ["afterContext", afterContext], ["maxCount", maxCount]]) {
|
|
35
|
+
if (value !== undefined && value !== null && (!Number.isSafeInteger(value) || value < 0 || (label === "maxCount" && value === 0))) {
|
|
36
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.REQUEST_INVALID, `Search ${label} must be a valid positive or non-negative integer`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeSearchInvocation(repositoryRoot, request) {
|
|
42
|
+
if (repositoryRoot && typeof repositoryRoot === "object" && request && Object.keys(request).length === 0) {
|
|
43
|
+
const normalizedRequest = { ...repositoryRoot };
|
|
44
|
+
return {
|
|
45
|
+
repositoryRoot: normalizedRequest.repoRoot ?? normalizedRequest.repositoryRoot ?? normalizedRequest.projectPath,
|
|
46
|
+
request: normalizedRequest,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { repositoryRoot, request };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function prepareSearch(repositoryRoot, request) {
|
|
53
|
+
const canonicalRoot = await realpath(repositoryRoot);
|
|
54
|
+
const setupImpl = request.setupRepositoryIndexImpl ?? setupRepositoryIndex;
|
|
55
|
+
const cached = getRepositoryIndexReadiness(canonicalRoot, request.platform ?? process.platform);
|
|
56
|
+
if (cached) {
|
|
57
|
+
const expected = await expectedReadiness(canonicalRoot, request);
|
|
58
|
+
if (isRepositoryIndexReadinessUsable(cached, expected, { isAlive: request.processIsAliveImpl })) {
|
|
59
|
+
return {
|
|
60
|
+
status: cached.status,
|
|
61
|
+
canonicalRoot,
|
|
62
|
+
indexPath: cached.indexPath,
|
|
63
|
+
binaryPath: cached.binaryPath,
|
|
64
|
+
cacheHit: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
invalidateRepositoryIndexReadiness(canonicalRoot, request.platform ?? process.platform);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const setup = await setupImpl(repositoryRoot, request);
|
|
71
|
+
const status = setup.status?.repositoryRoot
|
|
72
|
+
? setup.status
|
|
73
|
+
: await (request.getRepositoryIndexStatusImpl ?? getRepositoryIndexStatus)(canonicalRoot, { ...request, includeLocalPaths: true });
|
|
74
|
+
if (status.health !== "READY") {
|
|
75
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.SERVER_UNHEALTHY, "Repository Index is not ready for search", { status });
|
|
76
|
+
}
|
|
77
|
+
const indexPath = status.indexPath;
|
|
78
|
+
const binaryPath = status.binaryPath ?? request.binaryPath;
|
|
79
|
+
if (!indexPath || !binaryPath) {
|
|
80
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.SERVER_UNHEALTHY, "Repository Index readiness did not provide executable paths", { status });
|
|
81
|
+
}
|
|
82
|
+
rememberRepositoryIndexReadiness({ canonicalRoot, status, platform: request.platform });
|
|
83
|
+
return { status, canonicalRoot, indexPath, binaryPath, cacheHit: false };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function expectedReadiness(canonicalRoot, request) {
|
|
87
|
+
const manifest = await loadTgrepManifest(request.packageRoot);
|
|
88
|
+
const selectedPlatform = getRepositoryIndexPlatform({ platform: request.platform, arch: request.arch });
|
|
89
|
+
const override = request.binaryPath
|
|
90
|
+
?? (request.env === undefined ? process.env[REPOSITORY_INDEX_ENV_BINARY] : request.env?.[REPOSITORY_INDEX_ENV_BINARY]);
|
|
91
|
+
const binaryPath = override ?? getManagedTgrepBinaryPath(manifest.version, selectedPlatform.key, {
|
|
92
|
+
homeDirectory: request.homeDirectory,
|
|
93
|
+
windows: selectedPlatform.platform === "win32",
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
canonicalRoot,
|
|
97
|
+
indexPath: getTgrepIndexPath(canonicalRoot),
|
|
98
|
+
binaryPath,
|
|
99
|
+
engineVersion: manifest.version,
|
|
100
|
+
platform: selectedPlatform.platform,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertSearchInvocation(repositoryRoot, request) {
|
|
105
|
+
if (typeof repositoryRoot !== "string" || repositoryRoot.length === 0) {
|
|
106
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.REQUEST_INVALID, "Search repoRoot is required");
|
|
107
|
+
}
|
|
108
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
109
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.REQUEST_INVALID, "Search request must be an object");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildSearchArgs({ indexPath, canonicalRoot, request }) {
|
|
114
|
+
const args = [...getCanonicalRepositorySearchArgs({
|
|
115
|
+
indexPath,
|
|
116
|
+
pattern: request.pattern,
|
|
117
|
+
options: request,
|
|
118
|
+
config: request.config,
|
|
119
|
+
})];
|
|
120
|
+
appendSearchFilters(args, { globs: request.globs, types: request.types });
|
|
121
|
+
args.push(canonicalRoot);
|
|
122
|
+
return args;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runSearch({ binaryPath, status, canonicalRoot, request, args }) {
|
|
126
|
+
const startedAt = Date.now();
|
|
127
|
+
const result = await (request.runTgrepImpl ?? runTgrep)({
|
|
128
|
+
binary: createTgrepBinaryHandle(binaryPath),
|
|
129
|
+
repoRoot: canonicalRoot,
|
|
130
|
+
args,
|
|
131
|
+
env: request.env,
|
|
132
|
+
spawnImpl: request.spawnImpl,
|
|
133
|
+
timeoutMs: request.commandTimeoutMs ?? REPOSITORY_INDEX_DEFAULTS.commandTimeoutMs,
|
|
134
|
+
maxOutputBytes: request.maxProcessOutputBytes ?? REPOSITORY_INDEX_DEFAULTS.maxProcessOutputBytes,
|
|
135
|
+
});
|
|
136
|
+
if (result.timedOut || ![0, 1].includes(result.exitCode)) {
|
|
137
|
+
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.SEARCH_FAILED, `tgrep search failed with exit code ${result.exitCode ?? "unknown"}`, {
|
|
138
|
+
exitCode: result.exitCode,
|
|
139
|
+
stderr: result.stderr.slice(0, 4000),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const normalized = await normalizeTgrepJson(result.stdout, {
|
|
143
|
+
repositoryRoot: canonicalRoot,
|
|
144
|
+
filesWithMatches: request.filesWithMatches === true,
|
|
145
|
+
});
|
|
146
|
+
return { result, normalized, startedAt, status };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isRecoverableSearchFailure(error) {
|
|
150
|
+
if (error?.code === REPOSITORY_INDEX_ERROR_CODES.ENGINE_EXECUTION_FAILED) return true;
|
|
151
|
+
if (error?.code !== REPOSITORY_INDEX_ERROR_CODES.SEARCH_FAILED) return false;
|
|
152
|
+
return /server|index|connect|connection|not running|metadata|broken pipe|no such file|unavailable/i.test(error.stderr ?? "");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectErrorPaths(error) {
|
|
156
|
+
const paths = [];
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (let current = error; current && !seen.has(current); current = current.cause) {
|
|
159
|
+
seen.add(current);
|
|
160
|
+
for (const key of ["path", "binaryPath", "repositoryRoot", "indexPath", "statePath", "archivePath", "manifestPath", "source"]) {
|
|
161
|
+
if (typeof current[key] === "string") paths.push(current[key]);
|
|
162
|
+
}
|
|
163
|
+
const status = current.status;
|
|
164
|
+
if (status && typeof status === "object") {
|
|
165
|
+
for (const value of [status.binaryPath, status.repositoryRoot, status.indexPath, status.statePath, status.index?.rootPath]) {
|
|
166
|
+
if (typeof value === "string") paths.push(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...new Set(paths)].sort((left, right) => right.length - left.length);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sanitizeSearchError(error, request, repositoryRoot) {
|
|
174
|
+
const localPaths = [...new Set([
|
|
175
|
+
repositoryRoot,
|
|
176
|
+
request.binaryPath,
|
|
177
|
+
request.homeDirectory,
|
|
178
|
+
request.assetPath,
|
|
179
|
+
process.cwd(),
|
|
180
|
+
...collectErrorPaths(error),
|
|
181
|
+
].filter((value) => typeof value === "string" && value.length > 0))].sort((left, right) => right.length - left.length);
|
|
182
|
+
const redact = (value) => localPaths.reduce((result, localPath) => result.replaceAll(localPath, "<local-path>"), String(value ?? ""));
|
|
183
|
+
if (localPaths.length === 0) return error;
|
|
184
|
+
const sanitized = repositoryIndexError(error.code ?? REPOSITORY_INDEX_ERROR_CODES.SEARCH_FAILED, redact(error.message));
|
|
185
|
+
for (const key of ["exitCode", "signal", "timedOut", "stderr", "stdout", "expectedVersion", "actualVersion"]) {
|
|
186
|
+
if (error[key] !== undefined) sanitized[key] = typeof error[key] === "string" ? redact(error[key]) : error[key];
|
|
187
|
+
}
|
|
188
|
+
return sanitized;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildSearchResult({ request, status, result, normalized, startedAt }) {
|
|
192
|
+
return {
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
query: {
|
|
195
|
+
pattern: request.pattern,
|
|
196
|
+
globs: [...(request.globs ?? [])],
|
|
197
|
+
types: [...(request.types ?? [])],
|
|
198
|
+
filesWithMatches: request.filesWithMatches === true,
|
|
199
|
+
stats: request.stats === true,
|
|
200
|
+
fixedStrings: request.fixedStrings === true,
|
|
201
|
+
ignoreCase: request.ignoreCase === true,
|
|
202
|
+
smartCase: request.smartCase === true,
|
|
203
|
+
wordRegexp: request.wordRegexp === true,
|
|
204
|
+
context: request.context ?? null,
|
|
205
|
+
beforeContext: request.beforeContext ?? null,
|
|
206
|
+
afterContext: request.afterContext ?? null,
|
|
207
|
+
maxCount: request.maxCount ?? null,
|
|
208
|
+
},
|
|
209
|
+
repositoryIndex: {
|
|
210
|
+
engine: status?.engine ?? "tgrep",
|
|
211
|
+
engineVersion: status?.engineVersion ?? null,
|
|
212
|
+
indexed: true,
|
|
213
|
+
server: status?.server?.running === true,
|
|
214
|
+
},
|
|
215
|
+
matches: normalized.matches,
|
|
216
|
+
contexts: normalized.contexts,
|
|
217
|
+
files: normalized.files,
|
|
218
|
+
stats: normalized.stats,
|
|
219
|
+
metrics: buildRepositorySearchMetrics({
|
|
220
|
+
durationMs: Date.now() - startedAt,
|
|
221
|
+
nativeDurationMs: result.durationMs,
|
|
222
|
+
result: { ...normalized, exitCode: result.exitCode },
|
|
223
|
+
engine: status?.engine ?? "tgrep",
|
|
224
|
+
engineVersion: status?.engineVersion ?? null,
|
|
225
|
+
serverUsed: status?.server?.running === true,
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function searchRepository(repositoryRoot, request = {}) {
|
|
231
|
+
({ repositoryRoot, request } = normalizeSearchInvocation(repositoryRoot, request));
|
|
232
|
+
assertSearchInvocation(repositoryRoot, request);
|
|
233
|
+
validateSearchRequest(request);
|
|
234
|
+
let prepared;
|
|
235
|
+
try {
|
|
236
|
+
prepared = await prepareSearch(repositoryRoot, request);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw sanitizeSearchError(error, request, repositoryRoot);
|
|
239
|
+
}
|
|
240
|
+
let args = buildSearchArgs({ indexPath: prepared.indexPath, canonicalRoot: prepared.canonicalRoot, request });
|
|
241
|
+
try {
|
|
242
|
+
const execution = await runSearch({ binaryPath: prepared.binaryPath, status: prepared.status, canonicalRoot: prepared.canonicalRoot, request, args });
|
|
243
|
+
markRepositoryIndexSearchSucceeded(prepared.canonicalRoot, request.platform ?? process.platform);
|
|
244
|
+
return buildSearchResult({ request, ...execution });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (!isRecoverableSearchFailure(error)) throw sanitizeSearchError(error, request, prepared.canonicalRoot);
|
|
247
|
+
invalidateRepositoryIndexReadiness(prepared.canonicalRoot, request.platform ?? process.platform);
|
|
248
|
+
try {
|
|
249
|
+
prepared = await prepareSearch(prepared.canonicalRoot, request);
|
|
250
|
+
} catch (recoveryError) {
|
|
251
|
+
throw sanitizeSearchError(recoveryError, request, prepared.canonicalRoot);
|
|
252
|
+
}
|
|
253
|
+
args = buildSearchArgs({ indexPath: prepared.indexPath, canonicalRoot: prepared.canonicalRoot, request });
|
|
254
|
+
try {
|
|
255
|
+
const execution = await runSearch({ binaryPath: prepared.binaryPath, status: prepared.status, canonicalRoot: prepared.canonicalRoot, request, args });
|
|
256
|
+
markRepositoryIndexSearchSucceeded(prepared.canonicalRoot, request.platform ?? process.platform);
|
|
257
|
+
return buildSearchResult({ request, ...execution });
|
|
258
|
+
} catch (retryError) {
|
|
259
|
+
throw sanitizeSearchError(retryError, request, prepared.canonicalRoot);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rm, unlink } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { assertSafePath, realpathWithTransientWindowsRetry, writeFileAtomic } from "../core/filesystem.js";
|
|
6
|
+
import { REPOSITORY_INDEX_DEFAULTS } from "./constants.js";
|
|
7
|
+
import { REPOSITORY_INDEX_ERROR_CODES as ERROR_CODES, repositoryIndexError, wrapRepositoryIndexError } from "./errors.js";
|
|
8
|
+
import { ensureManagedTgrep } from "./binary-manager.js";
|
|
9
|
+
import { getCanonicalRepositoryIndexArgs } from "./args.js";
|
|
10
|
+
import { withRepositoryIndexLock } from "./lock.js";
|
|
11
|
+
import { createTgrepBinaryHandle, runTgrep, spawnTgrepServer } from "./process.js";
|
|
12
|
+
import {
|
|
13
|
+
getRepositoryIndexRoot,
|
|
14
|
+
getRepositoryIndexStatePath,
|
|
15
|
+
getTgrepIndexPath,
|
|
16
|
+
} from "./paths.js";
|
|
17
|
+
import { getRepositoryIndexStatus, inspectRepositoryIndexServer, processIsAlive } from "./status.js";
|
|
18
|
+
import { invalidateRepositoryIndexReadiness, rememberRepositoryIndexReadiness } from "./readiness.js";
|
|
19
|
+
|
|
20
|
+
async function readJson(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const info = await lstat(filePath);
|
|
23
|
+
if (info.isSymbolicLink() || !info.isFile()) return null;
|
|
24
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code === "ENOENT") return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function writeJson(filePath, value) {
|
|
32
|
+
await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function removeFileIfPresent(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
await unlink(filePath);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT") throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function serverArgs(indexPath, config) {
|
|
44
|
+
return [
|
|
45
|
+
"serve",
|
|
46
|
+
...getCanonicalRepositoryIndexArgs({ mode: "serve", indexPath, config }),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function indexArgs(indexPath, config, force) {
|
|
51
|
+
return [
|
|
52
|
+
"index",
|
|
53
|
+
...(force ? ["--force"] : []),
|
|
54
|
+
...getCanonicalRepositoryIndexArgs({ mode: "index", indexPath, config }),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function assertIndexPaths(repositoryRoot) {
|
|
59
|
+
await assertSafePath(repositoryRoot, ".forgeloop/repository-index");
|
|
60
|
+
await assertSafePath(repositoryRoot, ".forgeloop/repository-index/tgrep");
|
|
61
|
+
await assertSafePath(repositoryRoot, ".forgeloop/repository-index/engine-state.json");
|
|
62
|
+
await mkdir(getRepositoryIndexRoot(repositoryRoot), { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readIndexMetadata(repositoryRoot) {
|
|
66
|
+
const indexPath = getTgrepIndexPath(repositoryRoot);
|
|
67
|
+
const statePath = getRepositoryIndexStatePath(repositoryRoot);
|
|
68
|
+
return {
|
|
69
|
+
meta: await readJson(path.join(indexPath, "meta.json")),
|
|
70
|
+
state: await readJson(statePath),
|
|
71
|
+
serve: await readJson(path.join(indexPath, "serve.json")),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function comparableRepositoryPath(value, platform = process.platform) {
|
|
76
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
77
|
+
let normalized = pathApi.normalize(value);
|
|
78
|
+
if (platform === "win32") {
|
|
79
|
+
normalized = normalized.replace(/^\\\\\?\\/u, "").replace(/^\\\\\.\\/u, "");
|
|
80
|
+
normalized = normalized.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sameRepositoryPath(left, right, platform = process.platform) {
|
|
86
|
+
return typeof left === "string"
|
|
87
|
+
&& typeof right === "string"
|
|
88
|
+
&& comparableRepositoryPath(left, platform) === comparableRepositoryPath(right, platform);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function indexIsUsable({ meta, state, repositoryRoot, engineVersion, platform = process.platform }) {
|
|
92
|
+
return meta?.complete === true
|
|
93
|
+
&& typeof meta.root_path === "string"
|
|
94
|
+
&& sameRepositoryPath(meta.root_path, repositoryRoot, platform)
|
|
95
|
+
&& state?.schemaVersion === 1
|
|
96
|
+
&& state?.engine === "tgrep"
|
|
97
|
+
&& state?.engineVersion === engineVersion
|
|
98
|
+
&& sameRepositoryPath(state?.repositoryRoot, repositoryRoot, platform)
|
|
99
|
+
&& sameRepositoryPath(state?.indexPath, getTgrepIndexPath(repositoryRoot), platform);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function waitForServerReady({ repositoryRoot, indexPath, child, engine, timeoutMs, processInspector }) {
|
|
103
|
+
const deadline = Date.now() + timeoutMs;
|
|
104
|
+
let lastError = null;
|
|
105
|
+
while (Date.now() < deadline) {
|
|
106
|
+
if (child?.pid && !processIsAlive(child.pid)) {
|
|
107
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_START_FAILED, "tgrep server exited before becoming ready", { pid: child.pid });
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const { serve, state, meta } = await readIndexMetadata(repositoryRoot);
|
|
111
|
+
const ownership = await inspectRepositoryIndexServer({
|
|
112
|
+
state,
|
|
113
|
+
serve,
|
|
114
|
+
repositoryRoot,
|
|
115
|
+
indexPath,
|
|
116
|
+
processInspector,
|
|
117
|
+
});
|
|
118
|
+
if (ownership.running && meta?.complete === true) {
|
|
119
|
+
return { serve, state, meta, ownership };
|
|
120
|
+
}
|
|
121
|
+
lastError = ownership.reason ?? "metadata not ready";
|
|
122
|
+
} catch (error) {
|
|
123
|
+
lastError = error;
|
|
124
|
+
}
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
126
|
+
}
|
|
127
|
+
throw repositoryIndexError(
|
|
128
|
+
ERROR_CODES.SERVER_START_FAILED,
|
|
129
|
+
`tgrep server did not become ready within ${timeoutMs}ms${lastError ? `: ${lastError.message ?? lastError}` : ""}`,
|
|
130
|
+
{ binaryPath: engine.binaryPath, repositoryRoot, indexPath },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function terminateOwnedServer({ repositoryRoot, indexPath, statePath, processInspector = {}, timeoutMs = 15_000 }) {
|
|
135
|
+
const { state, serve } = await readIndexMetadata(repositoryRoot);
|
|
136
|
+
if (state?.startedByForgeLoop === true && !serve) {
|
|
137
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_STOP_FAILED, "ForgeLoop server state has no matching tgrep serve metadata; refusing to guess the process identity");
|
|
138
|
+
}
|
|
139
|
+
const ownership = await inspectRepositoryIndexServer({
|
|
140
|
+
state,
|
|
141
|
+
serve,
|
|
142
|
+
repositoryRoot,
|
|
143
|
+
indexPath,
|
|
144
|
+
processInspector,
|
|
145
|
+
});
|
|
146
|
+
if (ownership.running) {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(ownership.pid);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error.code !== "ESRCH") {
|
|
151
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_STOP_FAILED, `Unable to stop owned tgrep server: ${error.message}`, { cause: error, pid: ownership.pid });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const deadline = Date.now() + timeoutMs;
|
|
155
|
+
while (Date.now() < deadline && processIsAlive(ownership.pid)) {
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
157
|
+
}
|
|
158
|
+
if (processIsAlive(ownership.pid)) {
|
|
159
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_STOP_FAILED, `Owned tgrep server did not stop within ${timeoutMs}ms`, { pid: ownership.pid });
|
|
160
|
+
}
|
|
161
|
+
} else if (serve && !ownership.owned) {
|
|
162
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_STOP_FAILED, "Refusing to remove tgrep server metadata without verifiable ForgeLoop ownership", { reason: ownership.reason });
|
|
163
|
+
}
|
|
164
|
+
if (ownership.owned || !serve) {
|
|
165
|
+
await removeFileIfPresent(path.join(indexPath, "serve.json"));
|
|
166
|
+
const currentState = await readJson(statePath);
|
|
167
|
+
if (currentState?.startedByForgeLoop === true && currentState.repositoryRoot === repositoryRoot) {
|
|
168
|
+
await removeFileIfPresent(statePath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { stopped: ownership.running || ownership.owned, pid: ownership.pid ?? null };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function startUnderLock(repositoryRoot, options, engine) {
|
|
175
|
+
const indexPath = getTgrepIndexPath(repositoryRoot);
|
|
176
|
+
const statePath = getRepositoryIndexStatePath(repositoryRoot);
|
|
177
|
+
const metadata = await readIndexMetadata(repositoryRoot);
|
|
178
|
+
if (!metadata.meta?.complete) {
|
|
179
|
+
throw repositoryIndexError(ERROR_CODES.NOT_INITIALIZED, "Repository Index has not been built; run forgeloop index-setup first");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (metadata.serve || metadata.state) {
|
|
183
|
+
const ownership = await inspectRepositoryIndexServer({
|
|
184
|
+
state: metadata.state,
|
|
185
|
+
serve: metadata.serve,
|
|
186
|
+
repositoryRoot,
|
|
187
|
+
indexPath,
|
|
188
|
+
processInspector: options.processInspector,
|
|
189
|
+
});
|
|
190
|
+
if (ownership.running) {
|
|
191
|
+
return {
|
|
192
|
+
started: false,
|
|
193
|
+
alreadyRunning: true,
|
|
194
|
+
pid: ownership.pid,
|
|
195
|
+
port: ownership.port,
|
|
196
|
+
status: await getRepositoryIndexStatus(repositoryRoot, { ...options, packageRoot: options.packageRoot, binaryPath: engine.overridden ? engine.binaryPath : undefined, skipNativeStatus: true, includeLocalPaths: true }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (metadata.serve && !ownership.owned) {
|
|
200
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_START_FAILED, "Refusing to replace a tgrep server without verifiable ForgeLoop ownership", { reason: ownership.reason });
|
|
201
|
+
}
|
|
202
|
+
if (metadata.state?.startedByForgeLoop === true && !metadata.serve) {
|
|
203
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_START_FAILED, "ForgeLoop server state has no matching tgrep serve metadata; refusing to guess the process identity");
|
|
204
|
+
}
|
|
205
|
+
if (ownership.owned || !metadata.serve) await terminateOwnedServer({ repositoryRoot, indexPath, statePath, processInspector: options.processInspector });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const startedAt = new Date().toISOString();
|
|
209
|
+
const child = spawnTgrepServer({
|
|
210
|
+
binary: createTgrepBinaryHandle(engine.binaryPath),
|
|
211
|
+
repoRoot: repositoryRoot,
|
|
212
|
+
args: serverArgs(indexPath, options.config),
|
|
213
|
+
env: options.env,
|
|
214
|
+
spawnImpl: options.spawnImpl,
|
|
215
|
+
stdio: options.stdio ?? "ignore",
|
|
216
|
+
});
|
|
217
|
+
if (!Number.isInteger(child.pid) || child.pid <= 0) {
|
|
218
|
+
throw repositoryIndexError(ERROR_CODES.SERVER_START_FAILED, "tgrep server did not provide a valid process ID");
|
|
219
|
+
}
|
|
220
|
+
const state = {
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
engine: "tgrep",
|
|
223
|
+
engineVersion: engine.engineVersion,
|
|
224
|
+
binaryPath: engine.binaryPath,
|
|
225
|
+
repositoryRoot,
|
|
226
|
+
indexPath,
|
|
227
|
+
observedPid: child.pid,
|
|
228
|
+
startedByForgeLoop: true,
|
|
229
|
+
watcher: true,
|
|
230
|
+
startedAt,
|
|
231
|
+
stateId: randomUUID(),
|
|
232
|
+
};
|
|
233
|
+
await writeJson(statePath, state);
|
|
234
|
+
try {
|
|
235
|
+
const ready = await waitForServerReady({
|
|
236
|
+
repositoryRoot,
|
|
237
|
+
indexPath,
|
|
238
|
+
child,
|
|
239
|
+
engine,
|
|
240
|
+
timeoutMs: options.startupTimeoutMs ?? REPOSITORY_INDEX_DEFAULTS.startupTimeoutMs,
|
|
241
|
+
processInspector: options.processInspector,
|
|
242
|
+
});
|
|
243
|
+
await writeJson(statePath, {
|
|
244
|
+
...(ready.state ?? state),
|
|
245
|
+
lastReadyAt: new Date().toISOString(),
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
started: true,
|
|
249
|
+
alreadyRunning: false,
|
|
250
|
+
pid: ready.ownership.pid,
|
|
251
|
+
port: ready.ownership.port,
|
|
252
|
+
status: await getRepositoryIndexStatus(repositoryRoot, { ...options, packageRoot: options.packageRoot, binaryPath: engine.overridden ? engine.binaryPath : undefined, skipNativeStatus: true, includeLocalPaths: true }),
|
|
253
|
+
};
|
|
254
|
+
} catch (error) {
|
|
255
|
+
try { process.kill(child.pid); } catch { /* best effort for the exact child only */ }
|
|
256
|
+
await removeFileIfPresent(statePath);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function buildUnderLock(repositoryRoot, options, engine, { force = false } = {}) {
|
|
262
|
+
const indexPath = getTgrepIndexPath(repositoryRoot);
|
|
263
|
+
const result = await runTgrep({
|
|
264
|
+
binary: createTgrepBinaryHandle(engine.binaryPath),
|
|
265
|
+
repoRoot: repositoryRoot,
|
|
266
|
+
args: indexArgs(indexPath, options.config, force),
|
|
267
|
+
env: options.env,
|
|
268
|
+
spawnImpl: options.spawnImpl,
|
|
269
|
+
timeoutMs: options.commandTimeoutMs ?? REPOSITORY_INDEX_DEFAULTS.commandTimeoutMs,
|
|
270
|
+
maxOutputBytes: options.maxProcessOutputBytes ?? REPOSITORY_INDEX_DEFAULTS.maxProcessOutputBytes,
|
|
271
|
+
});
|
|
272
|
+
if (result.timedOut || result.exitCode !== 0) {
|
|
273
|
+
throw repositoryIndexError(ERROR_CODES.REBUILD_FAILED, `tgrep index failed with exit code ${result.exitCode ?? "unknown"}`, {
|
|
274
|
+
exitCode: result.exitCode,
|
|
275
|
+
stderr: result.stderr.slice(0, 4000),
|
|
276
|
+
stdout: result.stdout.slice(0, 4000),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const meta = await readJson(path.join(indexPath, "meta.json"));
|
|
280
|
+
if (!meta?.complete || typeof meta.root_path !== "string" || !sameRepositoryPath(meta.root_path, repositoryRoot, options.platform ?? process.platform)) {
|
|
281
|
+
throw repositoryIndexError(ERROR_CODES.REBUILD_FAILED, "tgrep index completed without valid repository metadata");
|
|
282
|
+
}
|
|
283
|
+
return { result, meta };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function engineOptions(options) {
|
|
287
|
+
return {
|
|
288
|
+
packageRoot: options.packageRoot,
|
|
289
|
+
repoRoot: options.repositoryRoot,
|
|
290
|
+
platform: options.platform,
|
|
291
|
+
arch: options.arch,
|
|
292
|
+
env: options.env,
|
|
293
|
+
binaryPath: options.binaryPath,
|
|
294
|
+
assetPath: options.assetPath,
|
|
295
|
+
homeDirectory: options.homeDirectory,
|
|
296
|
+
fetchImpl: options.fetchImpl,
|
|
297
|
+
spawnImpl: options.spawnImpl,
|
|
298
|
+
execFileImpl: options.execFileImpl,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function prepareEngine(repositoryRoot, options) {
|
|
303
|
+
try {
|
|
304
|
+
return await ensureManagedTgrep(engineOptions({ ...options, repositoryRoot }));
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error.code?.startsWith?.("E_REPOSITORY_INDEX_")) throw error;
|
|
307
|
+
throw wrapRepositoryIndexError(ERROR_CODES.ENGINE_EXECUTION_FAILED, "Unable to prepare managed tgrep", error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function startRepositoryIndexServer(repositoryRoot, options = {}) {
|
|
312
|
+
const canonicalRoot = await realpathWithTransientWindowsRetry(repositoryRoot);
|
|
313
|
+
invalidateRepositoryIndexReadiness(canonicalRoot, options.platform ?? process.platform);
|
|
314
|
+
await assertIndexPaths(canonicalRoot);
|
|
315
|
+
const result = await withRepositoryIndexLock(canonicalRoot, "index-start", async () => {
|
|
316
|
+
const engine = await prepareEngine(canonicalRoot, options);
|
|
317
|
+
return startUnderLock(canonicalRoot, { ...options, repositoryRoot: canonicalRoot }, engine);
|
|
318
|
+
}, options);
|
|
319
|
+
rememberRepositoryIndexReadiness({ canonicalRoot, status: result.status, platform: options.platform });
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function restartRepositoryIndexServer(repositoryRoot, options = {}) {
|
|
324
|
+
await stopRepositoryIndexServer(repositoryRoot, options);
|
|
325
|
+
return startRepositoryIndexServer(repositoryRoot, options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function stopRepositoryIndexServer(repositoryRoot, options = {}) {
|
|
329
|
+
const canonicalRoot = await realpathWithTransientWindowsRetry(repositoryRoot);
|
|
330
|
+
invalidateRepositoryIndexReadiness(canonicalRoot, options.platform ?? process.platform);
|
|
331
|
+
await assertIndexPaths(canonicalRoot);
|
|
332
|
+
return withRepositoryIndexLock(canonicalRoot, "index-stop", async () => {
|
|
333
|
+
const indexPath = getTgrepIndexPath(canonicalRoot);
|
|
334
|
+
return terminateOwnedServer({
|
|
335
|
+
repositoryRoot: canonicalRoot,
|
|
336
|
+
indexPath,
|
|
337
|
+
statePath: getRepositoryIndexStatePath(canonicalRoot),
|
|
338
|
+
processInspector: options.processInspector,
|
|
339
|
+
timeoutMs: options.stopTimeoutMs ?? 15_000,
|
|
340
|
+
});
|
|
341
|
+
}, options);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function rebuildRepositoryIndex(repositoryRoot, options = {}) {
|
|
345
|
+
const canonicalRoot = await realpathWithTransientWindowsRetry(repositoryRoot);
|
|
346
|
+
invalidateRepositoryIndexReadiness(canonicalRoot, options.platform ?? process.platform);
|
|
347
|
+
await assertIndexPaths(canonicalRoot);
|
|
348
|
+
const result = await withRepositoryIndexLock(canonicalRoot, "index-rebuild", async () => {
|
|
349
|
+
const engine = await prepareEngine(canonicalRoot, options);
|
|
350
|
+
const indexPath = getTgrepIndexPath(canonicalRoot);
|
|
351
|
+
const metadata = await readIndexMetadata(canonicalRoot);
|
|
352
|
+
if (metadata.serve || metadata.state) {
|
|
353
|
+
await terminateOwnedServer({
|
|
354
|
+
repositoryRoot: canonicalRoot,
|
|
355
|
+
indexPath,
|
|
356
|
+
statePath: getRepositoryIndexStatePath(canonicalRoot),
|
|
357
|
+
processInspector: options.processInspector,
|
|
358
|
+
timeoutMs: options.stopTimeoutMs ?? 15_000,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
await assertSafePath(canonicalRoot, ".forgeloop/repository-index/tgrep");
|
|
362
|
+
await rm(indexPath, { recursive: true, force: true });
|
|
363
|
+
await mkdir(indexPath, { recursive: true });
|
|
364
|
+
const built = await buildUnderLock(canonicalRoot, { ...options, repositoryRoot: canonicalRoot }, engine, { force: true });
|
|
365
|
+
const statePath = getRepositoryIndexStatePath(canonicalRoot);
|
|
366
|
+
await writeJson(statePath, {
|
|
367
|
+
schemaVersion: 1,
|
|
368
|
+
engine: "tgrep",
|
|
369
|
+
engineVersion: engine.engineVersion,
|
|
370
|
+
binaryPath: engine.binaryPath,
|
|
371
|
+
repositoryRoot: canonicalRoot,
|
|
372
|
+
indexPath,
|
|
373
|
+
indexEngineVersion: engine.engineVersion,
|
|
374
|
+
builtAt: new Date().toISOString(),
|
|
375
|
+
startedByForgeLoop: false,
|
|
376
|
+
});
|
|
377
|
+
const started = await startUnderLock(canonicalRoot, { ...options, repositoryRoot: canonicalRoot }, engine);
|
|
378
|
+
return { ...started, rebuilt: true, indexed: built.meta };
|
|
379
|
+
}, options);
|
|
380
|
+
rememberRepositoryIndexReadiness({ canonicalRoot, status: result.status, platform: options.platform });
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function setupRepositoryIndex(repositoryRoot, options = {}) {
|
|
385
|
+
const canonicalRoot = await realpathWithTransientWindowsRetry(repositoryRoot);
|
|
386
|
+
invalidateRepositoryIndexReadiness(canonicalRoot, options.platform ?? process.platform);
|
|
387
|
+
await assertIndexPaths(canonicalRoot);
|
|
388
|
+
const result = await withRepositoryIndexLock(canonicalRoot, "index-setup", async () => {
|
|
389
|
+
const engine = await prepareEngine(canonicalRoot, options);
|
|
390
|
+
const indexPath = getTgrepIndexPath(canonicalRoot);
|
|
391
|
+
const metadata = await readIndexMetadata(canonicalRoot);
|
|
392
|
+
if (options.force === true || !indexIsUsable({
|
|
393
|
+
...metadata,
|
|
394
|
+
repositoryRoot: canonicalRoot,
|
|
395
|
+
engineVersion: engine.engineVersion,
|
|
396
|
+
platform: options.platform ?? process.platform,
|
|
397
|
+
})) {
|
|
398
|
+
if (metadata.serve || metadata.state) {
|
|
399
|
+
await terminateOwnedServer({
|
|
400
|
+
repositoryRoot: canonicalRoot,
|
|
401
|
+
indexPath,
|
|
402
|
+
statePath: getRepositoryIndexStatePath(canonicalRoot),
|
|
403
|
+
processInspector: options.processInspector,
|
|
404
|
+
timeoutMs: options.stopTimeoutMs ?? 15_000,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (options.force === true) {
|
|
408
|
+
await assertSafePath(canonicalRoot, ".forgeloop/repository-index/tgrep");
|
|
409
|
+
await rm(indexPath, { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
await mkdir(indexPath, { recursive: true });
|
|
412
|
+
const built = await buildUnderLock(canonicalRoot, { ...options, repositoryRoot: canonicalRoot }, engine, { force: true });
|
|
413
|
+
await writeJson(getRepositoryIndexStatePath(canonicalRoot), {
|
|
414
|
+
schemaVersion: 1,
|
|
415
|
+
engine: "tgrep",
|
|
416
|
+
engineVersion: engine.engineVersion,
|
|
417
|
+
binaryPath: engine.binaryPath,
|
|
418
|
+
repositoryRoot: canonicalRoot,
|
|
419
|
+
indexPath,
|
|
420
|
+
indexEngineVersion: engine.engineVersion,
|
|
421
|
+
builtAt: new Date().toISOString(),
|
|
422
|
+
startedByForgeLoop: false,
|
|
423
|
+
});
|
|
424
|
+
const started = await startUnderLock(canonicalRoot, { ...options, repositoryRoot: canonicalRoot }, engine);
|
|
425
|
+
return { ...started, setup: true, rebuilt: true, indexed: built.meta };
|
|
426
|
+
}
|
|
427
|
+
const started = await startUnderLock(canonicalRoot, { ...options, repositoryRoot: canonicalRoot }, engine);
|
|
428
|
+
return { ...started, setup: true, rebuilt: false, indexed: metadata.meta };
|
|
429
|
+
}, options);
|
|
430
|
+
rememberRepositoryIndexReadiness({ canonicalRoot, status: result.status, platform: options.platform });
|
|
431
|
+
return result;
|
|
432
|
+
}
|