@maxgfr/codeindex 2.15.0 → 2.16.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 CHANGED
@@ -17,7 +17,7 @@ engine as a single file instead of taking an npm dependency.
17
17
  and count caps (`capped` flag, never silent truncation), symlink-cycle guard.
18
18
  - **Scan** every file into a `FileRecord`: classification, language, symbols,
19
19
  imports, headings, hashes — with an incremental cache fastpath.
20
- - **Extract symbols** via tree-sitter (10 languages, when the wasm sidecar is
20
+ - **Extract symbols** via tree-sitter (13 languages, when the wasm sidecar is
21
21
  present) or per-language regex rules (15 languages, always available).
22
22
  - **Resolve imports** across languages: tsconfig paths, package `exports`,
23
23
  go.mod, Cargo, Java packages, PSR-4, C# namespaces.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxgfr/codeindex",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Self-contained, deterministic repo-indexing engine: walk + language detection + symbol/import extraction (tree-sitter AST with regex fallback) + import resolution + typed cross-file link-graph + analytics. Ships as a single zero-dependency engine.mjs that consumer tools vendor.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -1,4 +1,4 @@
1
- declare const ENGINE_VERSION = "2.15.0";
1
+ declare const ENGINE_VERSION = "2.16.0";
2
2
  declare const SCHEMA_VERSION = 4;
3
3
  declare const EXTRACTOR_VERSION = 10;
4
4
  type FileKind = "code" | "doc" | "config" | "asset" | "other";
@@ -255,7 +255,7 @@ declare function extractAst(rel: string, ext: string, content: string, opts?: {
255
255
  maxCalls?: number;
256
256
  }): AstResult | undefined;
257
257
 
258
- declare const DEFAULT_GRAMMARS_URL = "https://github.com/maxgfr/codeindex/releases/download/v2.15.0/grammars-2.15.0.tar.gz";
258
+ declare const DEFAULT_GRAMMARS_URL = "https://github.com/maxgfr/codeindex/releases/download/v2.16.0/grammars-2.16.0.tar.gz";
259
259
  interface GrammarsPullTarget {
260
260
  url: string;
261
261
  sha256Url?: string;
@@ -265,6 +265,38 @@ declare function fetchGrammarsTarball(url: string, expectedSha256?: string): Pro
265
265
  declare function fetchExpectedSha256(url: string): Promise<string>;
266
266
  declare function extractTarInto(rawTar: Uint8Array, destDir: string): string[];
267
267
  declare function extractGrammarsTarball(bytes: Uint8Array, destDir: string): string[];
268
+ interface GrammarsPullResult {
269
+ ok: boolean;
270
+ status: "up-to-date" | "pulled" | "failed";
271
+ cacheDir: string;
272
+ /** One human-readable line; the CLI writes it to stderr, a library caller may log or drop it. */
273
+ message: string;
274
+ }
275
+ declare function pullGrammars(cacheDir: string, opts?: {
276
+ onNote?: (msg: string) => void;
277
+ }): Promise<GrammarsPullResult>;
278
+
279
+ interface WarmGrammarsResult {
280
+ /** Tier AFTER the warm-up (a successful pull moves "none" → "cache"). */
281
+ tier: GrammarsTierName;
282
+ /** True when at least one requested grammar is loaded ⇒ the AST tier is live. */
283
+ ready: boolean;
284
+ /** True when this call populated the shared cache over the network. */
285
+ pulled: boolean;
286
+ /** Everything written to `onNote`, in order — so a caller can persist the trail in its run artifacts. */
287
+ notes: string[];
288
+ }
289
+ interface WarmGrammarsOptions {
290
+ /** Grammars to load. Default: every shipped grammar. Narrow it with `grammarKeysForExts` when the repo's languages are known. */
291
+ keys?: Iterable<string>;
292
+ /** Fetch the wasms into the shared cache when nothing is resolvable. Default true; `CODEINDEX_NO_GRAMMARS_PULL=1` forces false. */
293
+ pull?: boolean;
294
+ /** Prefix for the diagnostics ("ultrasec: …"). Default "codeindex". */
295
+ label?: string;
296
+ /** Where diagnostics go. Default: process.stderr. Pass a sink to keep stdout/stderr clean. */
297
+ onNote?: (msg: string) => void;
298
+ }
299
+ declare function warmGrammars(opts?: WarmGrammarsOptions): Promise<WarmGrammarsResult>;
268
300
 
269
301
  type Resolution = {
270
302
  kind: "resolved";
@@ -747,4 +779,4 @@ declare function rrf<T>(lists: T[][], keyOf: (item: T) => string, k?: number): M
747
779
 
748
780
  declare function runCli(argv: string[]): Promise<void>;
749
781
 
750
- export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_GRAMMARS_URL, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, EMBED_VERSION, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type EmbedEndpointOptions, type EmbedPullTarget, type EmbeddingIndex, type EmbeddingRecord, type EmbeddingUnit, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type GrammarsPullTarget, type GrammarsTier, type GrammarsTierName, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type McpServerOptions, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawCallerIndex, type RawCallerSite, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type SemanticSearchOptions, type SemanticSearchResult, type ShResult, type StaticEmbedModel, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildArtifactsFromScan, buildCallerIndex, buildEmbeddingIndex, buildEndpointIndex, buildGraph, buildIndexArtifacts, buildModules, buildRawCallerIndex, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, deserializeEmbeddings, detectCommunities, detectWorkspaces, diffFiles, diffHunks, embedEndpointUrl, embedViaEndpoint, embeddingUnits, enclosingSymbol, encode, encodeQueryViaEndpoint, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractGrammarsTarball, extractMarkdown, extractSymbols, extractTarInto, fetchExpectedSha256, fetchGrammarsTarball, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarKeysForExts, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveGrammarsDir, resolveGrammarsPullTarget, resolveGrammarsTier, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, sharedGrammarsCacheDir, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, wordpiece, writeMemory };
782
+ export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_GRAMMARS_URL, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, EMBED_VERSION, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type EmbedEndpointOptions, type EmbedPullTarget, type EmbeddingIndex, type EmbeddingRecord, type EmbeddingUnit, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type GrammarsPullResult, type GrammarsPullTarget, type GrammarsTier, type GrammarsTierName, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type McpServerOptions, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawCallerIndex, type RawCallerSite, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type SemanticSearchOptions, type SemanticSearchResult, type ShResult, type StaticEmbedModel, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WarmGrammarsOptions, type WarmGrammarsResult, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildArtifactsFromScan, buildCallerIndex, buildEmbeddingIndex, buildEndpointIndex, buildGraph, buildIndexArtifacts, buildModules, buildRawCallerIndex, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, deserializeEmbeddings, detectCommunities, detectWorkspaces, diffFiles, diffHunks, embedEndpointUrl, embedViaEndpoint, embeddingUnits, enclosingSymbol, encode, encodeQueryViaEndpoint, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractGrammarsTarball, extractMarkdown, extractSymbols, extractTarInto, fetchExpectedSha256, fetchGrammarsTarball, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarKeysForExts, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, pullGrammars, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveGrammarsDir, resolveGrammarsPullTarget, resolveGrammarsTier, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, sharedGrammarsCacheDir, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, warmGrammars, wordpiece, writeMemory };
@@ -14,7 +14,7 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
14
14
  var init_types = __esm({
15
15
  "src/types.ts"() {
16
16
  "use strict";
17
- ENGINE_VERSION = "2.15.0";
17
+ ENGINE_VERSION = "2.16.0";
18
18
  SCHEMA_VERSION = 4;
19
19
  EXTRACTOR_VERSION = 10;
20
20
  }
@@ -8259,7 +8259,7 @@ var init_query = __esm({
8259
8259
  });
8260
8260
 
8261
8261
  // src/edit.ts
8262
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8262
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
8263
8263
  import { join as join9 } from "path";
8264
8264
  function resolveUniqueSymbol(scan2, namePath, file) {
8265
8265
  let matches = findSymbol(scan2, namePath);
@@ -8273,7 +8273,7 @@ function resolveUniqueSymbol(scan2, namePath, file) {
8273
8273
  throw new Error(`"${namePath}" is ambiguous (${matches.length} matches: ${list}) \u2014 qualify with \`file\` or a Parent/name path`);
8274
8274
  }
8275
8275
  function readLines(abs) {
8276
- return readFileSync3(abs, "utf8").split("\n");
8276
+ return readFileSync4(abs, "utf8").split("\n");
8277
8277
  }
8278
8278
  function replaceSymbolBody(scan2, namePath, body2, file) {
8279
8279
  const sym = resolveUniqueSymbol(scan2, namePath, file);
@@ -8317,7 +8317,7 @@ var init_edit = __esm({
8317
8317
  });
8318
8318
 
8319
8319
  // src/memory.ts
8320
- import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
8320
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
8321
8321
  import { dirname as dirname3, join as join10 } from "path";
8322
8322
  function sanitize(name2) {
8323
8323
  const clean = name2.replace(/^mem:/, "").replace(/\.md$/, "");
@@ -8342,7 +8342,7 @@ function writeMemory(repo, name2, content) {
8342
8342
  }
8343
8343
  function readMemory(repo, name2) {
8344
8344
  try {
8345
- return readFileSync4(memoryPath(repo, name2), "utf8");
8345
+ return readFileSync5(memoryPath(repo, name2), "utf8");
8346
8346
  } catch {
8347
8347
  return void 0;
8348
8348
  }
@@ -8354,7 +8354,7 @@ function deleteMemory(repo, name2) {
8354
8354
  } catch {
8355
8355
  return false;
8356
8356
  }
8357
- rmSync(path);
8357
+ rmSync2(path);
8358
8358
  return true;
8359
8359
  }
8360
8360
  function listMemories(repo) {
@@ -8384,7 +8384,7 @@ var init_memory = __esm({
8384
8384
  });
8385
8385
 
8386
8386
  // src/workspaces.ts
8387
- import { existsSync as existsSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
8387
+ import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
8388
8388
  import { join as join11 } from "path";
8389
8389
  function readJson(path, label, warnings) {
8390
8390
  const raw = readText(path);
@@ -8437,7 +8437,7 @@ function wsGlobToRegExp(pat) {
8437
8437
  }
8438
8438
  function probeNodePkg(root, dir, kind, warnings) {
8439
8439
  const path = join11(root, dir, "package.json");
8440
- if (!existsSync2(path)) return void 0;
8440
+ if (!existsSync3(path)) return void 0;
8441
8441
  const manifest = `${dir}/package.json`;
8442
8442
  const pkg = readJson(path, manifest, warnings);
8443
8443
  const out2 = {
@@ -8451,7 +8451,7 @@ function probeNodePkg(root, dir, kind, warnings) {
8451
8451
  }
8452
8452
  function probeCargo(root, dir) {
8453
8453
  const path = join11(root, dir, "Cargo.toml");
8454
- if (!existsSync2(path)) return void 0;
8454
+ if (!existsSync3(path)) return void 0;
8455
8455
  const body2 = tomlSectionBody(readText(path), "package");
8456
8456
  const out2 = {
8457
8457
  name: tomlString(body2, "name") ?? dir,
@@ -8465,18 +8465,18 @@ function probeCargo(root, dir) {
8465
8465
  }
8466
8466
  function probeGoMod(root, dir) {
8467
8467
  const path = join11(root, dir, "go.mod");
8468
- if (!existsSync2(path)) return void 0;
8468
+ if (!existsSync3(path)) return void 0;
8469
8469
  const name2 = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
8470
8470
  return { name: name2, dir, kind: "go", manifest: `${dir}/go.mod` };
8471
8471
  }
8472
8472
  function probeMaven(root, dir) {
8473
8473
  const path = join11(root, dir, "pom.xml");
8474
- if (!existsSync2(path)) return void 0;
8474
+ if (!existsSync3(path)) return void 0;
8475
8475
  return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
8476
8476
  }
8477
8477
  function probePyproject(root, dir) {
8478
8478
  const path = join11(root, dir, "pyproject.toml");
8479
- if (!existsSync2(path)) return void 0;
8479
+ if (!existsSync3(path)) return void 0;
8480
8480
  const toml = readText(path);
8481
8481
  const project = tomlSectionBody(toml, "project");
8482
8482
  const poetry = tomlSectionBody(toml, "tool.poetry");
@@ -8492,7 +8492,7 @@ function probePyproject(root, dir) {
8492
8492
  }
8493
8493
  function probeComposer(root, dir, warnings) {
8494
8494
  const path = join11(root, dir, "composer.json");
8495
- if (!existsSync2(path)) return void 0;
8495
+ if (!existsSync3(path)) return void 0;
8496
8496
  const manifest = `${dir}/composer.json`;
8497
8497
  const pkg = readJson(path, manifest, warnings);
8498
8498
  const out2 = {
@@ -8506,7 +8506,7 @@ function probeComposer(root, dir, warnings) {
8506
8506
  }
8507
8507
  function probeNxProject(root, dir, warnings) {
8508
8508
  const path = join11(root, dir, "project.json");
8509
- if (!existsSync2(path)) return void 0;
8509
+ if (!existsSync3(path)) return void 0;
8510
8510
  const manifest = `${dir}/project.json`;
8511
8511
  const proj = readJson(path, manifest, warnings);
8512
8512
  return {
@@ -8518,7 +8518,7 @@ function probeNxProject(root, dir, warnings) {
8518
8518
  }
8519
8519
  function probeGradle(root, dir) {
8520
8520
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8521
- if (existsSync2(join11(root, dir, f))) {
8521
+ if (existsSync3(join11(root, dir, f))) {
8522
8522
  return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
8523
8523
  }
8524
8524
  }
@@ -9565,7 +9565,7 @@ var init_grep = __esm({
9565
9565
 
9566
9566
  // src/embed/model.ts
9567
9567
  import { createHash as createHash3 } from "crypto";
9568
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9568
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
9569
9569
  import { join as join13 } from "path";
9570
9570
  function resolveEmbedModelDir(repo) {
9571
9571
  const env = process.env.CODEINDEX_EMBED_DIR;
@@ -9574,7 +9574,7 @@ function resolveEmbedModelDir(repo) {
9574
9574
  if (repo) candidates.push(join13(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9575
9575
  candidates.push(join13(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9576
9576
  for (const c2 of candidates) {
9577
- if (existsSync3(join13(c2, "model.json"))) return c2;
9577
+ if (existsSync4(join13(c2, "model.json"))) return c2;
9578
9578
  }
9579
9579
  return void 0;
9580
9580
  }
@@ -9608,8 +9608,8 @@ function parseEmbedModel(raw, source) {
9608
9608
  function loadEmbedModel(dir) {
9609
9609
  if (!dir) return void 0;
9610
9610
  const path = join13(dir, "model.json");
9611
- if (!existsSync3(path)) return void 0;
9612
- const raw = JSON.parse(readFileSync5(path, "utf8"));
9611
+ if (!existsSync4(path)) return void 0;
9612
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
9613
9613
  return parseEmbedModel(raw, path);
9614
9614
  }
9615
9615
  function resolveEmbedPullUrl() {
@@ -10303,7 +10303,7 @@ __export(mcp_exports, {
10303
10303
  toCacheMap: () => toCacheMap,
10304
10304
  warmGrammarsForRepo: () => warmGrammarsForRepo
10305
10305
  });
10306
- import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
10306
+ import { readFileSync as readFileSync7, statSync as statSync4 } from "fs";
10307
10307
  import { join as join14 } from "path";
10308
10308
  import { createInterface } from "readline";
10309
10309
  function str(v) {
@@ -10360,7 +10360,7 @@ function toCacheMap(scan2) {
10360
10360
  function readPersistedIndex(repo) {
10361
10361
  let parsed;
10362
10362
  try {
10363
- parsed = JSON.parse(readFileSync6(join14(repo, ".codeindex", "cache.json"), "utf8"));
10363
+ parsed = JSON.parse(readFileSync7(join14(repo, ".codeindex", "cache.json"), "utf8"));
10364
10364
  } catch {
10365
10365
  return void 0;
10366
10366
  }
@@ -10384,8 +10384,8 @@ function preloadArtifacts(repo, scan2, meta) {
10384
10384
  let graphBytes;
10385
10385
  let symbolsBytes;
10386
10386
  try {
10387
- graphBytes = readFileSync6(join14(dir, "graph.json"));
10388
- symbolsBytes = readFileSync6(join14(dir, "symbols.json"));
10387
+ graphBytes = readFileSync7(join14(dir, "graph.json"));
10388
+ symbolsBytes = readFileSync7(join14(dir, "symbols.json"));
10389
10389
  } catch {
10390
10390
  return void 0;
10391
10391
  }
@@ -11130,8 +11130,8 @@ init_loader();
11130
11130
  // src/ast/grammars-pull.ts
11131
11131
  init_types();
11132
11132
  import { createHash as createHash2 } from "crypto";
11133
- import { mkdirSync, writeFileSync } from "fs";
11134
- import { dirname as dirname2, resolve, sep as sep2 } from "path";
11133
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
11134
+ import { dirname as dirname2, join as join3, resolve, sep as sep2 } from "path";
11135
11135
  import { gunzipSync } from "zlib";
11136
11136
  var DEFAULT_GRAMMARS_URL = `https://github.com/maxgfr/codeindex/releases/download/v${ENGINE_VERSION}/grammars-${ENGINE_VERSION}.tar.gz`;
11137
11137
  function resolveGrammarsPullTarget() {
@@ -11223,6 +11223,109 @@ function extractGrammarsTarball(bytes, destDir) {
11223
11223
  const raw = b.length >= 2 && b[0] === 31 && b[1] === 139 ? gunzipSync(b) : b;
11224
11224
  return extractTarInto(raw, destDir);
11225
11225
  }
11226
+ async function pullGrammars(cacheDir, opts = {}) {
11227
+ const note = opts.onNote ?? (() => {
11228
+ });
11229
+ const target = resolveGrammarsPullTarget();
11230
+ let expected;
11231
+ if (target.sha256Url) {
11232
+ try {
11233
+ expected = await fetchExpectedSha256(target.sha256Url);
11234
+ } catch (e) {
11235
+ note(`codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) \u2014 proceeding unverified
11236
+ `);
11237
+ }
11238
+ }
11239
+ const runtime = join3(cacheDir, "web-tree-sitter.wasm");
11240
+ const markerPath = join3(dirname2(cacheDir), `${ENGINE_VERSION}.sha256`);
11241
+ if (existsSync2(runtime) && expected && existsSync2(markerPath)) {
11242
+ let marker = "";
11243
+ try {
11244
+ marker = readFileSync3(markerPath, "utf8").trim();
11245
+ } catch {
11246
+ }
11247
+ if (marker === expected) {
11248
+ return { ok: true, status: "up-to-date", cacheDir, message: `codeindex: grammars already present at ${cacheDir} (up to date)
11249
+ ` };
11250
+ }
11251
+ }
11252
+ note(`codeindex: fetching grammars from ${target.url} \u2192 ${cacheDir}
11253
+ `);
11254
+ let bytes;
11255
+ try {
11256
+ bytes = await fetchGrammarsTarball(target.url, expected);
11257
+ } catch (e) {
11258
+ return {
11259
+ ok: false,
11260
+ status: "failed",
11261
+ cacheDir,
11262
+ message: `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
11263
+ `
11264
+ };
11265
+ }
11266
+ let tmp;
11267
+ try {
11268
+ mkdirSync(dirname2(cacheDir), { recursive: true });
11269
+ tmp = mkdtempSync(join3(dirname2(cacheDir), ".grammars-tmp-"));
11270
+ extractGrammarsTarball(bytes, tmp);
11271
+ if (!existsSync2(join3(tmp, "web-tree-sitter.wasm"))) {
11272
+ throw new Error("archive is missing web-tree-sitter.wasm");
11273
+ }
11274
+ if (existsSync2(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
11275
+ renameSync(tmp, cacheDir);
11276
+ tmp = void 0;
11277
+ if (expected) writeFileSync(markerPath, expected + "\n");
11278
+ } catch (e) {
11279
+ if (tmp) {
11280
+ try {
11281
+ rmSync(tmp, { recursive: true, force: true });
11282
+ } catch {
11283
+ }
11284
+ }
11285
+ return {
11286
+ ok: false,
11287
+ status: "failed",
11288
+ cacheDir,
11289
+ message: `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
11290
+ `
11291
+ };
11292
+ }
11293
+ return { ok: true, status: "pulled", cacheDir, message: `codeindex: grammars extracted \u2192 ${cacheDir}
11294
+ ` };
11295
+ }
11296
+
11297
+ // src/ast/warm.ts
11298
+ init_loader();
11299
+ async function warmGrammars(opts = {}) {
11300
+ const label = opts.label ?? "codeindex";
11301
+ const notes = [];
11302
+ const note = (msg) => {
11303
+ notes.push(msg);
11304
+ if (opts.onNote) opts.onNote(msg);
11305
+ else process.stderr.write(msg);
11306
+ };
11307
+ const noPull = process.env.CODEINDEX_NO_GRAMMARS_PULL;
11308
+ const mayPull = (opts.pull ?? true) && !(noPull && noPull.trim() && noPull !== "0");
11309
+ const keys = [...opts.keys ?? allGrammarKeys()];
11310
+ let pulled = false;
11311
+ if (resolveGrammarsTier().tier === "none" && mayPull) {
11312
+ note(`${label}: tree-sitter grammars not found locally \u2014 pulling them into the shared cache (once per machine)\u2026
11313
+ `);
11314
+ const res = await pullGrammars(sharedGrammarsCacheDir(), { onNote: note });
11315
+ note(res.message);
11316
+ pulled = res.ok && res.status === "pulled";
11317
+ }
11318
+ await ensureGrammars(keys);
11319
+ const tier = resolveGrammarsTier().tier;
11320
+ const ready = keys.some((k) => grammarReady(k));
11321
+ if (!ready) {
11322
+ note(
11323
+ `${label}: no tree-sitter grammars available (offline?) \u2014 extracting with the regex tier, so symbols and call sites are less precise. Run \`codeindex grammars pull\` once online to enable AST precision.
11324
+ `
11325
+ );
11326
+ }
11327
+ return { tier, ready, pulled, notes };
11328
+ }
11226
11329
 
11227
11330
  // src/engine.ts
11228
11331
  init_resolve();
@@ -11506,8 +11609,8 @@ init_util();
11506
11609
  init_types();
11507
11610
  init_types();
11508
11611
  init_loader();
11509
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
11510
- import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
11612
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
11613
+ import { join as join15, resolve as resolve2 } from "path";
11511
11614
  init_pipeline();
11512
11615
  init_hash();
11513
11616
  init_graph_json();
@@ -11687,7 +11790,7 @@ async function runCli(argv) {
11687
11790
  return;
11688
11791
  }
11689
11792
  const flags2 = parseFlags(rest);
11690
- if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11793
+ if (!existsSync5(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11691
11794
  const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
11692
11795
  let precomputedWalk;
11693
11796
  if (scans && !flags2.noAst) {
@@ -11707,7 +11810,7 @@ async function runCli(argv) {
11707
11810
  let cache;
11708
11811
  let meta = {};
11709
11812
  try {
11710
- const parsed = JSON.parse(readFileSync7(cachePath, "utf8"));
11813
+ const parsed = JSON.parse(readFileSync8(cachePath, "utf8"));
11711
11814
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
11712
11815
  cache = new Map(Object.entries(parsed.files));
11713
11816
  meta = {
@@ -11728,7 +11831,7 @@ async function runCli(argv) {
11728
11831
  const embedPath = join15(outDir, "embeddings.bin");
11729
11832
  const artifactSha = (path) => {
11730
11833
  try {
11731
- return sha1(readFileSync7(path));
11834
+ return sha1(readFileSync8(path));
11732
11835
  } catch {
11733
11836
  return void 0;
11734
11837
  }
@@ -11948,7 +12051,7 @@ async function runCli(argv) {
11948
12051
  const cacheDir = sharedGrammarsCacheDir();
11949
12052
  if (sub === "status") {
11950
12053
  const info2 = resolveGrammarsTier();
11951
- const runtimePresent = info2.dir ? existsSync4(join15(info2.dir, "web-tree-sitter.wasm")) : false;
12054
+ const runtimePresent = info2.dir ? existsSync5(join15(info2.dir, "web-tree-sitter.wasm")) : false;
11952
12055
  const target = resolveGrammarsPullTarget();
11953
12056
  const status = {
11954
12057
  engineVersion: ENGINE_VERSION,
@@ -11961,77 +12064,15 @@ async function runCli(argv) {
11961
12064
  };
11962
12065
  emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
11963
12066
  } else if (sub === "pull") {
11964
- const target = resolveGrammarsPullTarget();
11965
- let expected;
11966
- if (target.sha256Url) {
11967
- try {
11968
- expected = await fetchExpectedSha256(target.sha256Url);
11969
- } catch (e) {
11970
- process.stderr.write(
11971
- `codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) \u2014 proceeding unverified
11972
- `
11973
- );
11974
- }
11975
- }
11976
- const runtime = join15(cacheDir, "web-tree-sitter.wasm");
11977
- const markerPath = join15(dirname4(cacheDir), `${ENGINE_VERSION}.sha256`);
11978
- if (existsSync4(runtime) && expected && existsSync4(markerPath)) {
11979
- let marker = "";
11980
- try {
11981
- marker = readFileSync7(markerPath, "utf8").trim();
11982
- } catch {
11983
- }
11984
- if (marker === expected) {
11985
- process.stderr.write(`codeindex: grammars already present at ${cacheDir} (up to date)
11986
- `);
11987
- return;
11988
- }
11989
- }
11990
- process.stderr.write(`codeindex: fetching grammars from ${target.url} \u2192 ${cacheDir}
11991
- `);
11992
- let bytes;
11993
- try {
11994
- bytes = await fetchGrammarsTarball(target.url, expected);
11995
- } catch (e) {
11996
- process.stderr.write(`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
11997
- `);
11998
- process.exitCode = 1;
11999
- return;
12000
- }
12001
- let tmp;
12002
- try {
12003
- mkdirSync3(dirname4(cacheDir), { recursive: true });
12004
- tmp = mkdtempSync(join15(dirname4(cacheDir), ".grammars-tmp-"));
12005
- extractGrammarsTarball(bytes, tmp);
12006
- if (!existsSync4(join15(tmp, "web-tree-sitter.wasm"))) {
12007
- throw new Error("archive is missing web-tree-sitter.wasm");
12008
- }
12009
- if (existsSync4(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
12010
- renameSync(tmp, cacheDir);
12011
- tmp = void 0;
12012
- if (expected) writeFileSync4(markerPath, expected + "\n");
12013
- } catch (e) {
12014
- if (tmp) {
12015
- try {
12016
- rmSync2(tmp, { recursive: true, force: true });
12017
- } catch {
12018
- }
12019
- }
12020
- process.stderr.write(
12021
- `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
12022
- `
12023
- );
12024
- process.exitCode = 1;
12025
- return;
12026
- }
12027
- process.stderr.write(`codeindex: grammars extracted \u2192 ${cacheDir}
12028
- `);
12067
+ const res = await pullGrammars(cacheDir, { onNote: (m) => process.stderr.write(m) });
12068
+ process.stderr.write(res.message);
12069
+ if (!res.ok) process.exitCode = 1;
12029
12070
  } else {
12030
12071
  throw new Error("grammars needs a subcommand: status | pull");
12031
12072
  }
12032
12073
  } else if (cmd === "rules") {
12033
12074
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
12034
- const rules = parseRules(JSON.parse(readFileSync7(flags2.config, "utf8")));
12075
+ const rules = parseRules(JSON.parse(readFileSync8(flags2.config, "utf8")));
12035
12076
  const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
12036
12077
  const violations = checkRules(graph, rules);
12037
12078
  const errors = violations.filter((v) => v.severity === "error").length;
@@ -12182,6 +12223,7 @@ export {
12182
12223
  parseGitignore,
12183
12224
  parseRules,
12184
12225
  probeEndpoint,
12226
+ pullGrammars,
12185
12227
  quantize,
12186
12228
  rankHotspots,
12187
12229
  rankedKeywords,
@@ -12228,6 +12270,7 @@ export {
12228
12270
  untestedModules,
12229
12271
  untrackedFiles,
12230
12272
  walk,
12273
+ warmGrammars,
12231
12274
  wordpiece,
12232
12275
  writeMemory
12233
12276
  };
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
4
  import { gunzipSync } from "node:zlib";
5
5
  import { ENGINE_VERSION } from "../types.js";
@@ -176,3 +176,97 @@ export function extractGrammarsTarball(bytes: Uint8Array, destDir: string): stri
176
176
  const raw = b.length >= 2 && b[0] === 0x1f && b[1] === 0x8b ? gunzipSync(b) : b;
177
177
  return extractTarInto(raw, destDir);
178
178
  }
179
+
180
+ // What a pull did. `status` distinguishes the three terminal states so a caller
181
+ // can react without parsing prose: nothing to do, wasms installed, or failed
182
+ // (and on failure NOTHING was written — the cache is exactly as it was).
183
+ export interface GrammarsPullResult {
184
+ ok: boolean;
185
+ status: "up-to-date" | "pulled" | "failed";
186
+ cacheDir: string;
187
+ /** One human-readable line; the CLI writes it to stderr, a library caller may log or drop it. */
188
+ message: string;
189
+ }
190
+
191
+ // The whole `grammars pull` mechanic — resolve target, fetch the sha256 sidecar,
192
+ // skip when the cache already holds that exact digest, download, then install
193
+ // ATOMICALLY (extract into a tmp sibling, verify the runtime wasm landed, swap
194
+ // by rename). Extracted from the CLI so `warmGrammars` and `codeindex grammars
195
+ // pull` share ONE implementation: the CLI maps the result onto stderr +
196
+ // process.exitCode, the warm path folds it into its own report.
197
+ //
198
+ // NEVER throws: every failure becomes `{ ok: false, status: "failed" }` with a
199
+ // message, because both callers must survive an offline machine — the engine
200
+ // simply stays on the regex tier. `onNote` receives progress lines (checksum
201
+ // degradation, download start) that are not the terminal message.
202
+ export async function pullGrammars(
203
+ cacheDir: string,
204
+ opts: { onNote?: (msg: string) => void } = {},
205
+ ): Promise<GrammarsPullResult> {
206
+ const note = opts.onNote ?? (() => {});
207
+ const target = resolveGrammarsPullTarget();
208
+ let expected: string | undefined;
209
+ if (target.sha256Url) {
210
+ try {
211
+ expected = await fetchExpectedSha256(target.sha256Url);
212
+ } catch (e) {
213
+ note(`codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) — proceeding unverified\n`);
214
+ }
215
+ }
216
+ // Idempotent: the marker sibling records the digest of the tarball that
217
+ // populated cacheDir. Runtime wasm present AND marker === freshly-fetched
218
+ // digest ⇒ already up to date, skip the ~22 MB download.
219
+ const runtime = join(cacheDir, "web-tree-sitter.wasm");
220
+ const markerPath = join(dirname(cacheDir), `${ENGINE_VERSION}.sha256`);
221
+ if (existsSync(runtime) && expected && existsSync(markerPath)) {
222
+ let marker = "";
223
+ try {
224
+ marker = readFileSync(markerPath, "utf8").trim();
225
+ } catch {
226
+ // unreadable marker → fall through and re-pull
227
+ }
228
+ if (marker === expected) {
229
+ return { ok: true, status: "up-to-date", cacheDir, message: `codeindex: grammars already present at ${cacheDir} (up to date)\n` };
230
+ }
231
+ }
232
+ note(`codeindex: fetching grammars from ${target.url} → ${cacheDir}\n`);
233
+ let bytes: Uint8Array;
234
+ try {
235
+ bytes = await fetchGrammarsTarball(target.url, expected);
236
+ } catch (e) {
237
+ return {
238
+ ok: false,
239
+ status: "failed",
240
+ cacheDir,
241
+ message: `codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`,
242
+ };
243
+ }
244
+ let tmp: string | undefined;
245
+ try {
246
+ mkdirSync(dirname(cacheDir), { recursive: true });
247
+ tmp = mkdtempSync(join(dirname(cacheDir), ".grammars-tmp-"));
248
+ extractGrammarsTarball(bytes, tmp);
249
+ if (!existsSync(join(tmp, "web-tree-sitter.wasm"))) {
250
+ throw new Error("archive is missing web-tree-sitter.wasm");
251
+ }
252
+ if (existsSync(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
253
+ renameSync(tmp, cacheDir);
254
+ tmp = undefined;
255
+ if (expected) writeFileSync(markerPath, expected + "\n");
256
+ } catch (e) {
257
+ if (tmp) {
258
+ try {
259
+ rmSync(tmp, { recursive: true, force: true });
260
+ } catch {
261
+ // best-effort cleanup
262
+ }
263
+ }
264
+ return {
265
+ ok: false,
266
+ status: "failed",
267
+ cacheDir,
268
+ message: `codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`,
269
+ };
270
+ }
271
+ return { ok: true, status: "pulled", cacheDir, message: `codeindex: grammars extracted → ${cacheDir}\n` };
272
+ }
@@ -0,0 +1,74 @@
1
+ import { allGrammarKeys, ensureGrammars, grammarReady, resolveGrammarsTier, sharedGrammarsCacheDir } from "./loader.js";
2
+ import type { GrammarsTierName } from "./loader.js";
3
+ import { pullGrammars } from "./grammars-pull.js";
4
+
5
+ // The one-call AST warm-up every consumer needs, and which every consumer but
6
+ // ultraindex was missing.
7
+ //
8
+ // WHY THIS EXISTS. Extraction is AST-preferred with a regex fallback
9
+ // (src/extract/code.ts), but `extractAst` returns undefined unless
10
+ // `grammarReady(key)` — and that is only true after an `await ensureGrammars()`.
11
+ // `scanRepo` is deliberately synchronous, so it CANNOT warm anything itself.
12
+ // A consumer that never awaits the warm-up therefore runs on the regex tier
13
+ // forever, on every machine, no matter what the grammar cache holds — silently,
14
+ // since the fallback is by design invisible. That is a capability quietly left
15
+ // on the table, not a crash, which is exactly why it survived unnoticed.
16
+ //
17
+ // So: call this ONCE at your CLI entry, before the synchronous pipeline. It is
18
+ // idempotent, offline-safe, and never throws.
19
+ export interface WarmGrammarsResult {
20
+ /** Tier AFTER the warm-up (a successful pull moves "none" → "cache"). */
21
+ tier: GrammarsTierName;
22
+ /** True when at least one requested grammar is loaded ⇒ the AST tier is live. */
23
+ ready: boolean;
24
+ /** True when this call populated the shared cache over the network. */
25
+ pulled: boolean;
26
+ /** Everything written to `onNote`, in order — so a caller can persist the trail in its run artifacts. */
27
+ notes: string[];
28
+ }
29
+
30
+ export interface WarmGrammarsOptions {
31
+ /** Grammars to load. Default: every shipped grammar. Narrow it with `grammarKeysForExts` when the repo's languages are known. */
32
+ keys?: Iterable<string>;
33
+ /** Fetch the wasms into the shared cache when nothing is resolvable. Default true; `CODEINDEX_NO_GRAMMARS_PULL=1` forces false. */
34
+ pull?: boolean;
35
+ /** Prefix for the diagnostics ("ultrasec: …"). Default "codeindex". */
36
+ label?: string;
37
+ /** Where diagnostics go. Default: process.stderr. Pass a sink to keep stdout/stderr clean. */
38
+ onNote?: (msg: string) => void;
39
+ }
40
+
41
+ export async function warmGrammars(opts: WarmGrammarsOptions = {}): Promise<WarmGrammarsResult> {
42
+ const label = opts.label ?? "codeindex";
43
+ const notes: string[] = [];
44
+ const note = (msg: string): void => {
45
+ notes.push(msg);
46
+ if (opts.onNote) opts.onNote(msg);
47
+ else process.stderr.write(msg);
48
+ };
49
+ const noPull = process.env.CODEINDEX_NO_GRAMMARS_PULL;
50
+ const mayPull = (opts.pull ?? true) && !(noPull && noPull.trim() && noPull !== "0");
51
+ const keys = [...(opts.keys ?? allGrammarKeys())];
52
+
53
+ let pulled = false;
54
+ if (resolveGrammarsTier().tier === "none" && mayPull) {
55
+ note(`${label}: tree-sitter grammars not found locally — pulling them into the shared cache (once per machine)…\n`);
56
+ const res = await pullGrammars(sharedGrammarsCacheDir(), { onNote: note });
57
+ note(res.message);
58
+ pulled = res.ok && res.status === "pulled";
59
+ }
60
+
61
+ await ensureGrammars(keys);
62
+
63
+ const tier = resolveGrammarsTier().tier;
64
+ const ready = keys.some((k) => grammarReady(k));
65
+ if (!ready) {
66
+ // Never silent: a degraded run must say so, and stay a SUCCESSFUL run —
67
+ // the regex tier still produces a complete, searchable result.
68
+ note(
69
+ `${label}: no tree-sitter grammars available (offline?) — extracting with the regex tier, so symbols and call sites are less precise. ` +
70
+ "Run `codeindex grammars pull` once online to enable AST precision.\n",
71
+ );
72
+ }
73
+ return { tier, ready, pulled, notes };
74
+ }
package/src/engine-cli.ts CHANGED
@@ -3,12 +3,7 @@ import { dirname, join, resolve } from "node:path";
3
3
  import { SCHEMA_VERSION, EXTRACTOR_VERSION, type FileRecord } from "./types.js";
4
4
  import { ENGINE_VERSION } from "./types.js";
5
5
  import { ensureGrammars, grammarKeysForExts, resolveGrammarsTier, sharedGrammarsCacheDir } from "./ast/loader.js";
6
- import {
7
- resolveGrammarsPullTarget,
8
- fetchGrammarsTarball,
9
- fetchExpectedSha256,
10
- extractGrammarsTarball,
11
- } from "./ast/grammars-pull.js";
6
+ import { resolveGrammarsPullTarget, pullGrammars } from "./ast/grammars-pull.js";
12
7
  import { buildIndexArtifacts, buildArtifactsFromScan, type BuildIndexOptions } from "./pipeline.js";
13
8
  import { sha1 } from "./hash.js";
14
9
  import { renderGraphJson } from "./render/graph-json.js";
@@ -593,81 +588,14 @@ export async function runCli(argv: string[]): Promise<void> {
593
588
  };
594
589
  emit(JSON.stringify(status, null, 2) + "\n", flags.out);
595
590
  } else if (sub === "pull") {
596
- // Default: the official per-release asset + its `.sha256` sidecar. A
597
- // user-set CODEINDEX_GRAMMARS_URL overrides both (mirror/custom, no
598
- // verification). Fetch the expected digest from the sidecar first; a
599
- // missing sidecar degrades to an unverified pull with a note (never fatal).
600
- const target = resolveGrammarsPullTarget();
601
- let expected: string | undefined;
602
- if (target.sha256Url) {
603
- try {
604
- expected = await fetchExpectedSha256(target.sha256Url);
605
- } catch (e) {
606
- process.stderr.write(
607
- `codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) — proceeding unverified\n`,
608
- );
609
- }
610
- }
611
- // Idempotent: the marker sibling records the digest of the tarball that
612
- // populated cacheDir. If the runtime wasm is present AND the marker matches
613
- // the freshly-fetched digest, the cache is already up to date — skip the
614
- // ~22 MB download entirely. (Keeps cacheDir itself byte-identical to the
615
- // tarball; the marker lives next to it, never inside it.)
616
- const runtime = join(cacheDir, "web-tree-sitter.wasm");
617
- const markerPath = join(dirname(cacheDir), `${ENGINE_VERSION}.sha256`);
618
- if (existsSync(runtime) && expected && existsSync(markerPath)) {
619
- let marker = "";
620
- try {
621
- marker = readFileSync(markerPath, "utf8").trim();
622
- } catch {
623
- // unreadable marker → fall through and re-pull
624
- }
625
- if (marker === expected) {
626
- process.stderr.write(`codeindex: grammars already present at ${cacheDir} (up to date)\n`);
627
- return;
628
- }
629
- }
630
- process.stderr.write(`codeindex: fetching grammars from ${target.url} → ${cacheDir}\n`);
631
- let bytes: Uint8Array;
632
- try {
633
- // Follows redirects (GitHub → CDN) and verifies sha256 when known.
634
- bytes = await fetchGrammarsTarball(target.url, expected);
635
- } catch (e) {
636
- process.stderr.write(`codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`);
637
- process.exitCode = 1;
638
- return;
639
- }
640
- // Atomic install: extract into a tmp dir SIBLING to cacheDir (same
641
- // filesystem → rename is atomic), sanity-check the runtime wasm landed,
642
- // then swap it into place. A failure at any step discards the tmp dir and
643
- // leaves any existing cache untouched — a half-populated cache never shows.
644
- let tmp: string | undefined;
645
- try {
646
- mkdirSync(dirname(cacheDir), { recursive: true });
647
- tmp = mkdtempSync(join(dirname(cacheDir), ".grammars-tmp-"));
648
- extractGrammarsTarball(bytes, tmp);
649
- if (!existsSync(join(tmp, "web-tree-sitter.wasm"))) {
650
- throw new Error("archive is missing web-tree-sitter.wasm");
651
- }
652
- if (existsSync(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
653
- renameSync(tmp, cacheDir);
654
- tmp = undefined;
655
- if (expected) writeFileSync(markerPath, expected + "\n");
656
- } catch (e) {
657
- if (tmp) {
658
- try {
659
- rmSync(tmp, { recursive: true, force: true });
660
- } catch {
661
- // best-effort cleanup
662
- }
663
- }
664
- process.stderr.write(
665
- `codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`,
666
- );
667
- process.exitCode = 1;
668
- return;
669
- }
670
- process.stderr.write(`codeindex: grammars extracted → ${cacheDir}\n`);
591
+ // The mechanic itself (sidecar checksum, idempotent skip, atomic install)
592
+ // lives in pullGrammars shared verbatim with warmGrammars, so the CLI
593
+ // and the library warm-up can never drift apart. Here we only map its
594
+ // result onto the CLI contract: progress notes and the terminal message to
595
+ // stderr, non-zero exit on failure (which wrote nothing).
596
+ const res = await pullGrammars(cacheDir, { onNote: (m) => process.stderr.write(m) });
597
+ process.stderr.write(res.message);
598
+ if (!res.ok) process.exitCode = 1;
671
599
  } else {
672
600
  throw new Error("grammars needs a subcommand: status | pull");
673
601
  }
package/src/engine.ts CHANGED
@@ -60,8 +60,14 @@ export {
60
60
  fetchExpectedSha256,
61
61
  extractTarInto,
62
62
  extractGrammarsTarball,
63
+ pullGrammars,
63
64
  } from "./ast/grammars-pull.js";
64
- export type { GrammarsPullTarget } from "./ast/grammars-pull.js";
65
+ export type { GrammarsPullTarget, GrammarsPullResult } from "./ast/grammars-pull.js";
66
+ // The one-call AST warm-up for consumers. `scanRepo` is synchronous and cannot
67
+ // warm the grammars itself, so a consumer that never awaits this stays on the
68
+ // regex tier forever — silently. Call it once at the CLI entry.
69
+ export { warmGrammars } from "./ast/warm.js";
70
+ export type { WarmGrammarsResult, WarmGrammarsOptions } from "./ast/warm.js";
65
71
 
66
72
  // Resolution + modules + graph tier.
67
73
  export { buildResolveContext, resolveImport, resolveDocLink } from "./resolve.js";
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Single source of truth for the engine version the bundle reports. Kept in
2
2
  // lockstep with package.json by the release pipeline. Do not edit by hand
3
3
  // outside a release.
4
- export const ENGINE_VERSION = "2.15.0";
4
+ export const ENGINE_VERSION = "2.16.0";
5
5
 
6
6
  // Bumped whenever the on-disk artifact shape changes, so a consumer can reject
7
7
  // an index written by an incompatible engine instead of misreading it. The