genesis-compiler 1.6.0 → 1.7.1

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
@@ -898,6 +898,10 @@ import {
898
898
  installCodex,
899
899
  listEngineeringProfiles,
900
900
  listStackPieces,
901
+ listParsers,
902
+ installParsers,
903
+ verifyParsers,
904
+ parserEnvironment,
901
905
  migrate,
902
906
  projectSessionContext,
903
907
  projectTurnContext,
@@ -911,6 +915,9 @@ import {
911
915
 
912
916
  `initialize()` installs the project files, Genesis workflow skills, selected
913
917
  Stack skills, and local Codex hooks.
918
+ Extra language parsers install on first use, outside the project's dependencies.
919
+ The CLI can also prepare and verify parsers for an offline host; see
920
+ [language parser management](docs/parsers.md).
914
921
  `migrate()` updates recognized older Genesis project-file formats and returns
915
922
  the resulting structural check for hosts such as Vibe64.
916
923
  `adoptProject()` also returns the initial `adopt` prompt for an existing
@@ -0,0 +1,46 @@
1
+ # Language parsers
2
+
3
+ Genesis installs extra language parsers automatically the first time it indexes
4
+ matching source. JavaScript, TypeScript and TSX use its built-in core parser.
5
+ No project dependency or lockfile is changed.
6
+
7
+ ```sh
8
+ genesis parsers list
9
+ genesis parsers install python cpp
10
+ genesis parsers install --all
11
+ genesis parsers verify --all
12
+ ```
13
+
14
+ These commands work outside a Git repository and support `--json`. Downloads
15
+ and verification progress go to stderr. The language names come from `parsers
16
+ list`; C and C++ are separate parsers (`c` and `cpp`), and shell uses `bash`.
17
+
18
+ Use `--directory /absolute/path` or `GENESIS_PARSER_ROOT` to choose a cache.
19
+ Otherwise Genesis uses `$XDG_CACHE_HOME/genesis/parsers`, falling back to
20
+ `~/.cache/genesis/parsers`. Installations are pinned by language, package version,
21
+ OS and CPU. npm must be available for installation. Genesis uses the public npm
22
+ registry, disables package lifecycle scripts, verifies each native parser, and
23
+ retains only its current-platform binary and loader. Prebuilt native support is
24
+ required; Genesis does not compile a missing parser on the user's machine.
25
+
26
+ For a prepared host or release artifact:
27
+
28
+ ```sh
29
+ genesis parsers install --all --directory /absolute/release/parsers
30
+ genesis parsers verify --all --directory /absolute/release/parsers
31
+ export GENESIS_PARSER_ROOT=/absolute/release/parsers
32
+ export GENESIS_PARSER_AUTO_INSTALL=0
33
+ ```
34
+
35
+ The cache can be relocated on the same OS/CPU. Verify it again at its destination
36
+ before activation. Missing or broken parsers fail verification; indexing reports
37
+ an actionable diagnostic. A damaged installed version is preserved for inspection:
38
+ remove only that reported version directory deliberately, then rerun install.
39
+ Successful installations are reusable offline and require no runtime writes.
40
+
41
+ The library exports `listParsers`, `installParsers`, `verifyParsers`, and
42
+ `parserEnvironment`. The first three accept `directory`; install/verify accept
43
+ `languages` or `all` and an abort `signal`. Install accepts `onProgress`.
44
+ `parserEnvironment({ directory, autoInstall, environment })` returns the two
45
+ normalized environment values for a host and its commands. Preparing or verifying
46
+ all parsers uses short-lived children; native indexing runs in a finite child and releases parser memory on exit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with project agent guidance.",
6
6
  "repository": {
@@ -43,25 +43,18 @@
43
43
  "plugins/opencode",
44
44
  "profiles",
45
45
  "skills",
46
- "src"
46
+ "src",
47
+ "docs/parsers.md"
47
48
  ],
48
49
  "scripts": {
49
50
  "test": "node --test test/*.test.js"
50
51
  },
51
52
  "dependencies": {
52
- "@ast-grep/lang-bash": "^0.0.8",
53
- "@ast-grep/lang-c": "^0.0.6",
54
- "@ast-grep/lang-cpp": "^0.0.6",
55
- "@ast-grep/lang-csharp": "^0.0.6",
56
- "@ast-grep/lang-go": "^0.0.6",
57
- "@ast-grep/lang-java": "^0.0.7",
58
- "@ast-grep/lang-kotlin": "^0.0.7",
59
- "@ast-grep/lang-php": "^0.0.7",
60
- "@ast-grep/lang-python": "^0.0.6",
61
- "@ast-grep/lang-ruby": "^0.0.7",
62
- "@ast-grep/lang-rust": "^0.0.7",
63
53
  "@ast-grep/napi": "^0.45.1",
64
54
  "mdast-util-from-markdown": "^2.0.3",
65
55
  "yaml": "^2.9.0"
56
+ },
57
+ "devDependencies": {
58
+ "@ast-grep/lang-python": "0.0.6"
66
59
  }
67
60
  }
@@ -4,3 +4,5 @@
4
4
  - Prefer direct control flow, existing ownership boundaries, and established project or technology seams.
5
5
  - Do not overengineer or overcomplicate the solution. Do not add speculative abstractions, layers, dependencies, infrastructure, compatibility paths, distributed patterns, cryptography, enterprise patterns, or other advanced machinery unless an explicit requirement or a concrete constraint makes it necessary.
6
6
  - If good software requires material complexity beyond these rules, stop before writing it. Explain the requirement, why the direct design is insufficient, and the smallest added complexity proposed, then ask the user to approve or clarify the tradeoff.
7
+ - During implementation, run the relevant checks and batch related cases when they share setup. Follow the project's verification policy for broad or final checks; do not repeat them merely because an item finishes or a session resumes. Preserve required coverage and isolation.
8
+ - Keep complete test output in local artifacts and preserve exit status. Read concise counts, timings and actionable failure excerpts first, expanding evidence only when needed. Do not load full passing reports or repeatedly poll unchanged logs into agent context. Retain startup and cleanup failures; retry only after a change or new evidence justifies it.
@@ -144,8 +144,14 @@ Neither is authority or proof; both may be regenerated with the Genesis
144
144
 
145
145
  ## Verify and report
146
146
 
147
- After the selected technology's workspace substrate exists, use the Genesis
148
- `verify` operation for the Stack's declared final checks. An unconfigured result
147
+ During implementation, select the checks relevant to the change. Run the
148
+ Stack's declared final checks through Genesis `verify` when required by the
149
+ project's verification policy or the user's request, after the selected
150
+ technology's workspace substrate exists. Finishing an item or resuming a
151
+ session does not itself require a broad verification run. Keep full output in
152
+ local artifacts, preserve exit status, and read summaries and relevant failure
153
+ excerpts before expanding logs. Do not weaken assertions or omit required
154
+ coverage to save time or tokens. An unconfigured result
149
155
  means the declared workspace or checks do not exist yet; it is not a failing
150
156
  check. Before reporting completion, compare the requested observable behavior,
151
157
  required inputs and resources, declared project operations, and focused evidence
package/src/cli.js CHANGED
@@ -21,6 +21,9 @@ import {
21
21
  listEngineeringProfiles,
22
22
  listStackPieces,
23
23
  listTemplates,
24
+ listParsers,
25
+ installParsers,
26
+ verifyParsers,
24
27
  migrate,
25
28
  setCollaboration,
26
29
  setEngineeringProfile,
@@ -45,6 +48,7 @@ import {
45
48
  import { projectSessionContext, projectTurnContext } from './index/session-context.js';
46
49
  import { installedFirstPartyStackPackages } from './index/stack-catalog.js';
47
50
  import { readStack } from './index/stack.js';
51
+ import { withParserCancellation } from './index/code-indexers/parsers.js';
48
52
 
49
53
  const USAGE = `Usage:
50
54
  genesis init
@@ -62,6 +66,9 @@ const USAGE = `Usage:
62
66
  genesis templates apply <catalogue:technology/variant>
63
67
  genesis context <path...>
64
68
  genesis index [function-or-path...]
69
+ genesis parsers list
70
+ genesis parsers install <language...> | --all
71
+ genesis parsers verify <language...> | --all
65
72
  genesis migrate
66
73
  genesis inspect subsystems
67
74
  genesis inspect environment
@@ -78,6 +85,8 @@ Options:
78
85
  --stack-package <name> Add an installed external Stack package (repeatable)
79
86
  --template-source <namespace=repository[#branch]> Add a template catalogue (repeatable)
80
87
  --task <task> Select the prompt task (default: work)
88
+ --directory <path> Parser cache directory (parsers commands only)
89
+ --all Select every parser (parsers install/verify only)
81
90
  --json Emit one machine-readable result
82
91
  -h, --help Show this help
83
92
 
@@ -86,7 +95,7 @@ prompt to the agent you already use. Review all edits through the ordinary Git
86
95
  diff, then run genesis verify for the Stack's concrete checks.
87
96
  `;
88
97
 
89
- const COMMANDS = new Set(['adopt', 'check', 'codex', 'collaboration', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'skills', 'stack', 'templates', 'verify']);
98
+ const COMMANDS = new Set(['adopt', 'check', 'codex', 'collaboration', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'parsers', 'prompt', 'skills', 'stack', 'templates', 'verify']);
90
99
 
91
100
  function parseCommand(argv) {
92
101
  if (argv.length === 0 || argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
@@ -102,6 +111,8 @@ function parseCommand(argv) {
102
111
  strict: true,
103
112
  options: {
104
113
  json: { type: 'boolean', default: false },
114
+ all: { type: 'boolean', default: false },
115
+ directory: { type: 'string' },
105
116
  'project-root': { type: 'string' },
106
117
  'stack-package': { type: 'string', multiple: true, default: [] },
107
118
  'template-source': { type: 'string', multiple: true, default: [] },
@@ -113,6 +124,8 @@ function parseCommand(argv) {
113
124
  }
114
125
  const options = {
115
126
  json: parsed.values.json,
127
+ all: parsed.values.all,
128
+ directory: parsed.values.directory,
116
129
  projectRoot: parsed.values['project-root'],
117
130
  stackPackages: parsed.values['stack-package'],
118
131
  templateSources: parsed.values['template-source'].map((source) => {
@@ -125,13 +138,23 @@ function parseCommand(argv) {
125
138
  task: parsed.values.task,
126
139
  };
127
140
  const operands = parsed.positionals;
141
+ if (command !== 'parsers' && (options.all || options.directory !== undefined)) {
142
+ fail('CLI_OPTION_NOT_APPLICABLE', 'Options --all and --directory apply only to parsers commands.');
143
+ }
128
144
  if (options.templateSources.length && command !== 'templates') {
129
145
  fail('CLI_OPTION_NOT_APPLICABLE', `Option --template-source is not applicable to ${command}.`);
130
146
  }
131
147
  if (options.task !== undefined && command !== 'prompt') {
132
148
  fail('CLI_OPTION_NOT_APPLICABLE', `Option --task is not applicable to ${command}.`);
133
149
  }
134
- if (command === 'templates') {
150
+ if (command === 'parsers') {
151
+ const [action, ...languages] = operands;
152
+ if (!['list', 'install', 'verify'].includes(action)) fail('CLI_PARSER_ACTION_REQUIRED', 'Use parsers list, install, or verify.');
153
+ if (action === 'list' && (languages.length || options.all)) fail('CLI_EXTRA_ARGUMENT', 'parsers list accepts no languages or --all.');
154
+ if (action !== 'list' && (options.all ? languages.length : !languages.length)) {
155
+ fail('PARSER_SELECTION_INVALID', 'Choose parser names or --all.');
156
+ }
157
+ } else if (command === 'templates') {
135
158
  if (!(operands[0] === 'list' && operands.length === 1) && !(operands[0] === 'apply' && operands.length === 2)) {
136
159
  fail('CLI_TEMPLATE_ACTION_REQUIRED', 'Use templates list or templates apply <catalogue:technology/variant>.');
137
160
  }
@@ -290,6 +313,12 @@ function writeInspection(result) {
290
313
  }
291
314
 
292
315
  function writeResult(command, result) {
316
+ if (command === 'parsers') {
317
+ line(process.stdout, `Parser directory: ${result.directory}`);
318
+ if (result.parsers) for (const parser of result.parsers) line(process.stdout, `${parser.language}: ${parser.status}${parser.message ? ` — ${parser.message}` : ''}`);
319
+ else line(process.stdout, `Parsers verified: ${result.languages.join(', ')}`);
320
+ return;
321
+ }
293
322
  if (command === 'inspect' && result.inspection === 'skills') {
294
323
  line(process.stdout, `Agent Skills: ${result.status}`);
295
324
  for (const skill of result.skills) line(process.stdout, ` - ${skill.name}: ${skill.status}`);
@@ -429,6 +458,13 @@ async function cliStackPackages(projectRoot, supplied) {
429
458
  }
430
459
 
431
460
  async function execute({ command, operands, options }, { signal } = {}) {
461
+ if (command === 'parsers') {
462
+ const parserOptions = {
463
+ languages: operands.slice(1), all: options.all, directory: options.directory, signal,
464
+ onProgress: (message) => line(process.stderr, message),
465
+ };
466
+ return { list: listParsers, install: installParsers, verify: verifyParsers }[operands[0]](parserOptions);
467
+ }
432
468
  const projectRoot = options.projectRoot || process.cwd();
433
469
  const stackPackages = await cliStackPackages(projectRoot, options.stackPackages || []);
434
470
  if (command === 'init') return initialize({ projectRoot, stackPackages });
@@ -582,7 +618,7 @@ async function optionalProjectFormat(projectRoot) {
582
618
  }
583
619
 
584
620
  function requiresCurrentProjectFormat(command, operands) {
585
- if (['check', 'codex', 'hook', 'migrate'].includes(command)) return false;
621
+ if (['check', 'codex', 'hook', 'migrate', 'parsers'].includes(command)) return false;
586
622
  if (command === 'engineering') {
587
623
  return operands[0] === 'set' || (operands[0] === 'show' && operands.length === 1);
588
624
  }
@@ -611,7 +647,7 @@ export async function runCli(argv = process.argv.slice(2)) {
611
647
  return 0;
612
648
  }
613
649
  options = parsed.options;
614
- const projectFormat = await optionalProjectFormat(options.projectRoot || process.cwd());
650
+ const projectFormat = parsed.command === 'parsers' ? null : await optionalProjectFormat(options.projectRoot || process.cwd());
615
651
  const formatDiagnostic = projectFormatDiagnostic(projectFormat);
616
652
  if (formatDiagnostic && !options.json) {
617
653
  line(process.stderr, `WARNING: ${formatDiagnostic.message}`);
@@ -619,13 +655,10 @@ export async function runCli(argv = process.argv.slice(2)) {
619
655
  if (formatDiagnostic && requiresCurrentProjectFormat(parsed.command, parsed.operands)) {
620
656
  fail(formatDiagnostic.code, formatDiagnostic.message, formatDiagnostic.details);
621
657
  }
622
- const finiteCommand = parsed.command === 'verify';
623
- if (finiteCommand) {
624
- process.once('SIGINT', abort);
625
- process.once('SIGTERM', abort);
626
- }
658
+ process.once('SIGINT', abort);
659
+ process.once('SIGTERM', abort);
627
660
  const result = withProjectFormatWarning(
628
- await execute(parsed, { signal: controller.signal }),
661
+ await withParserCancellation(controller.signal, () => execute(parsed, { signal: controller.signal })),
629
662
  projectFormat,
630
663
  formatDiagnostic,
631
664
  );
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- import { astGrepCodeIndexers } from './code-indexers/ast-grep.js';
4
+ import { extractCodeIndexes } from './code-indexers/ast-grep.js';
5
5
  import { asDiagnostic } from './errors.js';
6
6
  import { buildCityPresentation } from './city-presentation.js';
7
7
  import { cityPathExcluded } from './stack-city-presentation.js';
@@ -28,8 +28,6 @@ export const GENESIS_DERIVED_ARTIFACTS = Object.freeze([
28
28
  }),
29
29
  ]);
30
30
 
31
- const INDEXERS = astGrepCodeIndexers;
32
-
33
31
  function directoryId(value) {
34
32
  return `directory:${value || '.'}`;
35
33
  }
@@ -262,22 +260,7 @@ export async function buildProjectIndex({
262
260
  ))
263
261
  .map(([filePath, state]) => ({ path: filePath, hash: state.hash, mode: state.mode }));
264
262
  const indexers = [...new Set(stack.components.flatMap((component) => component.indexers || []))].sort();
265
- const contributions = [];
266
- const diagnostics = [];
267
- for (const id of indexers) {
268
- const indexer = INDEXERS.get(id);
269
- if (!indexer) {
270
- diagnostics.push({ code: 'CODE_INDEXER_UNAVAILABLE', message: `No installed code indexer exists for ${id}.` });
271
- continue;
272
- }
273
- try {
274
- const contribution = await indexer.extract({ files, projectRoot: root });
275
- contributions.push({ ...contribution, extractor: id });
276
- diagnostics.push(...(contribution.diagnostics || []).map((diagnostic) => ({ ...diagnostic, extractor: id })));
277
- } catch (error) {
278
- diagnostics.push({ ...asDiagnostic(error), code: 'CODE_INDEXER_FAILED', extractor: id });
279
- }
280
- }
263
+ const { contributions, diagnostics } = await extractCodeIndexes({ indexers, files, projectRoot: root });
281
264
  const machine = machineCity({
282
265
  cityRegions: stack.cityRegions,
283
266
  components: stack.components.map(({ id }) => id),
@@ -1,18 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- import bashLanguage from '@ast-grep/lang-bash';
5
- import cLanguage from '@ast-grep/lang-c';
6
- import cppLanguage from '@ast-grep/lang-cpp';
7
- import csharpLanguage from '@ast-grep/lang-csharp';
8
- import goLanguage from '@ast-grep/lang-go';
9
- import javaLanguage from '@ast-grep/lang-java';
10
- import kotlinLanguage from '@ast-grep/lang-kotlin';
11
- import phpLanguage from '@ast-grep/lang-php';
12
- import pythonLanguage from '@ast-grep/lang-python';
13
- import rubyLanguage from '@ast-grep/lang-ruby';
14
- import rustLanguage from '@ast-grep/lang-rust';
15
- import { parseAsync, registerDynamicLanguage } from '@ast-grep/napi';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { asDiagnostic } from '../errors.js';
6
+ import { runProcess } from '../process.js';
7
+ import { parserAbortSignal, prepareCodeParsers } from './parsers.js';
16
8
 
17
9
  const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
18
10
  const OPENING_PARAMETER_DELIMITERS = '([{<';
@@ -22,20 +14,6 @@ const EXCLUDED_DIRECTORIES = new Set([
22
14
  'node_modules', 'storage', 'target', 'vendor',
23
15
  ]);
24
16
 
25
- registerDynamicLanguage({
26
- bash: bashLanguage,
27
- c: cLanguage,
28
- cpp: cppLanguage,
29
- csharp: csharpLanguage,
30
- go: goLanguage,
31
- java: javaLanguage,
32
- kotlin: kotlinLanguage,
33
- php: phpLanguage,
34
- python: pythonLanguage,
35
- ruby: rubyLanguage,
36
- rust: rustLanguage,
37
- });
38
-
39
17
  function javascriptParser(filePath) {
40
18
  if (/\.tsx$/u.test(filePath)) return 'Tsx';
41
19
  if (/\.ts$/u.test(filePath)) return 'TypeScript';
@@ -350,7 +328,7 @@ function createIndexer(id) {
350
328
  const definition = DEFINITIONS[id];
351
329
  return {
352
330
  id,
353
- async extract({ files, projectRoot }) {
331
+ async extract({ files, projectRoot, parseAsync, parserErrors = {} }) {
354
332
  const indexedFiles = [];
355
333
  const diagnostics = [];
356
334
  for (const file of files.filter((candidate) => isCandidate(candidate, definition))) {
@@ -377,6 +355,10 @@ function createIndexer(id) {
377
355
  const functions = [];
378
356
  for (const unit of scriptUnits(source, file.path, definition)) {
379
357
  try {
358
+ if (parserErrors[unit.parser]) {
359
+ diagnostics.push({ ...parserErrors[unit.parser], path: file.path });
360
+ continue;
361
+ }
380
362
  const parsed = await parseAsync(unit.parser, unit.source);
381
363
  const root = parsed.root();
382
364
  functions.push(...functionsInTree(root, definition, {
@@ -386,8 +368,9 @@ function createIndexer(id) {
386
368
  }));
387
369
  diagnostics.push(...syntaxDiagnostics(root, file.path, unit.lineOffset));
388
370
  } catch (error) {
371
+ if (error.code === 'PROCESS_ABORTED') throw error;
389
372
  diagnostics.push({
390
- code: 'CODE_INDEX_PARSE_FAILED',
373
+ code: error.code?.startsWith('PARSER_') ? error.code : 'CODE_INDEX_PARSE_FAILED',
391
374
  message: error.message,
392
375
  path: file.path,
393
376
  });
@@ -412,3 +395,32 @@ function createIndexer(id) {
412
395
  export const astGrepCodeIndexers = new Map(
413
396
  Object.keys(DEFINITIONS).map((id) => [id, createIndexer(id)]),
414
397
  );
398
+
399
+ // ast-grep permits one registration per process. A finite indexing child loads
400
+ // this request's languages once and releases every native parser when it exits.
401
+ export async function extractCodeIndexes({ indexers, files, projectRoot }) {
402
+ const selected = indexers.filter(id => astGrepCodeIndexers.has(id));
403
+ const diagnostics = indexers.filter(id => !astGrepCodeIndexers.has(id)).map(id => ({
404
+ code: 'CODE_INDEXER_UNAVAILABLE', message: `No installed code indexer exists for ${id}.`,
405
+ }));
406
+ const languages = selected.flatMap(id => files.filter(file => isCandidate(file, DEFINITIONS[id]))
407
+ .map(file => typeof DEFINITIONS[id].parser === 'function'
408
+ ? DEFINITIONS[id].parser(file.path) : DEFINITIONS[id].parser));
409
+ if (languages.length === 0) return { contributions: [], diagnostics };
410
+ const { registrations, errors } = await prepareCodeParsers(languages);
411
+ let contributions;
412
+ try {
413
+ const result = await runProcess(process.execPath, [fileURLToPath(new URL('./worker.js', import.meta.url))], {
414
+ input: JSON.stringify({ indexers: selected, files, projectRoot, registrations, errors }),
415
+ signal: parserAbortSignal(), timeoutMs: 180_000, code: 'CODE_INDEXER_FAILED',
416
+ });
417
+ contributions = JSON.parse(result.stdout.toString('utf8'));
418
+ } catch (error) {
419
+ if (error.code === 'PROCESS_ABORTED') throw error;
420
+ return { contributions: [], diagnostics: [...diagnostics, {
421
+ ...asDiagnostic(error), code: 'CODE_INDEXER_FAILED',
422
+ }] };
423
+ }
424
+ return { contributions, diagnostics: [...diagnostics, ...contributions.flatMap(value =>
425
+ value.diagnostics.map(diagnostic => ({ ...diagnostic, extractor: value.extractor }))) ] };
426
+ }
@@ -0,0 +1,231 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { access, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import { GenesisError, fail } from '../errors.js';
8
+ import { runProcess } from '../process.js';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const PACKAGES = Object.freeze({
12
+ bash: ['0.0.8', 'function example() { echo hello; }'],
13
+ c: ['0.0.6', 'int example(void) { return 1; }'],
14
+ cpp: ['0.0.6', 'int example() { return 1; }'],
15
+ csharp: ['0.0.6', 'class Example { public int Run() { return 1; } }'],
16
+ go: ['0.0.6', 'package example\nfunc Example() int { return 1 }'],
17
+ java: ['0.0.7', 'class Example { public int run() { return 1; } }'],
18
+ kotlin: ['0.0.7', 'fun example(): Int { return 1 }'],
19
+ php: ['0.0.7', '<?php function example() { return 1; }'],
20
+ python: ['0.0.6', 'def example():\n return 1\n'],
21
+ ruby: ['0.0.7', 'def example\n 1\nend'],
22
+ rust: ['0.0.7', 'fn example() -> i32 { 1 }'],
23
+ });
24
+ const installations = new Map();
25
+ const cancellation = new AsyncLocalStorage();
26
+
27
+ export function parserAbortSignal() {
28
+ return cancellation.getStore();
29
+ }
30
+
31
+ export function withParserCancellation(signal, operation) {
32
+ return cancellation.run(signal, operation);
33
+ }
34
+
35
+ function parserDefinition(language) {
36
+ if (language === 'javascript') return { language, package: '@ast-grep/napi', version: require('@ast-grep/napi/package.json').version, sample: 'function example() { return 1; }' };
37
+ if (!Object.hasOwn(PACKAGES, language)) {
38
+ fail('PARSER_UNKNOWN', `Unknown parser: ${language}. Run genesis parsers list.`);
39
+ }
40
+ const [version, sample] = PACKAGES[language];
41
+ return { language, package: `@ast-grep/lang-${language}`, version, sample };
42
+ }
43
+
44
+ function parserRoot(directory, environment = process.env) {
45
+ const root = directory || environment.GENESIS_PARSER_ROOT
46
+ || path.join(environment.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'genesis', 'parsers');
47
+ if (!path.isAbsolute(root)) fail('PARSER_ROOT_INVALID', 'The parser directory must be an absolute path.');
48
+ return root;
49
+ }
50
+
51
+ export function parserEnvironment({ directory, autoInstall, environment = process.env } = {}) {
52
+ const automatic = autoInstall === undefined ? environment.GENESIS_PARSER_AUTO_INSTALL ?? '1' : autoInstall ? '1' : '0';
53
+ if (!['0', '1'].includes(automatic)) fail('PARSER_POLICY_INVALID', 'GENESIS_PARSER_AUTO_INSTALL must be 0 or 1.');
54
+ return { GENESIS_PARSER_ROOT: parserRoot(directory, environment), GENESIS_PARSER_AUTO_INSTALL: automatic };
55
+ }
56
+
57
+ function parserLocation(definition, root) {
58
+ return path.join(root, definition.language, definition.version, `${process.platform}-${process.arch}`);
59
+ }
60
+
61
+ async function installedParser(definition, directory) {
62
+ if (definition.language === 'javascript') return { entry: '', packageRoot: null };
63
+ try {
64
+ await access(directory);
65
+ } catch (error) {
66
+ if (error.code === 'ENOENT') return null;
67
+ throw error;
68
+ }
69
+ const packageRoot = path.join(directory, 'node_modules', definition.package);
70
+ try {
71
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
72
+ if (manifest.name !== definition.package || manifest.version !== definition.version) {
73
+ throw new Error(`Expected ${definition.package}@${definition.version}.`);
74
+ }
75
+ const entry = path.join(packageRoot, 'index.js');
76
+ await access(entry);
77
+ return { entry, packageRoot };
78
+ } catch (error) {
79
+ throw new GenesisError('PARSER_INSTALL_INVALID', `Parser installation is invalid at ${directory}: ${error.message}`, { directory });
80
+ }
81
+ }
82
+
83
+ // Validate in a child so its native library is closed before moving the install,
84
+ // and preparing all languages never loads them into the hosting process.
85
+ async function verifyParser(definition, installation, signal) {
86
+ const source = `
87
+ const { createRequire } = await import('node:module');
88
+ const require = createRequire(import.meta.url);
89
+ const [core, entry, language, sample] = process.argv.slice(1);
90
+ const { registerDynamicLanguage, parseAsync } = require(core);
91
+ const definition = entry ? require(entry) : null;
92
+ if (definition) registerDynamicLanguage({ [language]: definition });
93
+ const root = (await parseAsync(definition ? language : 'JavaScript', sample)).root();
94
+ if (root.findAll({ rule: { kind: 'ERROR' } }).length) throw new Error('Parser smoke check failed.');
95
+ process.stdout.write(definition?.libraryPath || '');
96
+ `;
97
+ const result = await runProcess(process.execPath, [
98
+ '--input-type=module', '-e', source,
99
+ require.resolve('@ast-grep/napi'), installation.entry, definition.language, definition.sample,
100
+ ], { signal, timeoutMs: 30_000, code: 'PARSER_VERIFY_FAILED' });
101
+ return result.stdout.toString('utf8');
102
+ }
103
+
104
+ async function installParser(definition, root, { signal, onProgress, environment = process.env }) {
105
+ const destination = parserLocation(definition, root);
106
+ const existing = await installedParser(definition, destination);
107
+ if (existing) return existing;
108
+ onProgress?.(`Preparing ${definition.language} code indexing (${definition.package}@${definition.version})…`);
109
+ await mkdir(path.dirname(destination), { recursive: true });
110
+ const staging = await mkdtemp(`${destination}.install-`);
111
+ try {
112
+ await writeFile(path.join(staging, 'package.json'), JSON.stringify({ private: true }));
113
+ // Windows cannot spawn npm.cmd without a shell; invoke npm's JavaScript
114
+ // entry with Node so cache paths are never interpreted as shell code.
115
+ const npmCli = process.platform === 'win32'
116
+ ? environment.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js')
117
+ : null;
118
+ await runProcess(npmCli ? process.execPath : 'npm', [
119
+ ...(npmCli ? [npmCli] : []),
120
+ 'install', '--prefix', staging, '--ignore-scripts', '--no-audit', '--no-fund',
121
+ '--omit=dev', '--omit=optional', '--package-lock=false', '--save-exact',
122
+ '--registry=https://registry.npmjs.org', `${definition.package}@${definition.version}`,
123
+ ], { cwd: staging, env: environment, signal, timeoutMs: 180_000, code: 'PARSER_INSTALL_FAILED' });
124
+ const installation = await installedParser(definition, staging);
125
+ const libraryPath = await verifyParser(definition, installation, signal);
126
+ const prebuilds = path.join(installation.packageRoot, 'prebuilds');
127
+ const selected = path.dirname(libraryPath);
128
+ if (path.dirname(selected) !== prebuilds) {
129
+ fail('PARSER_PLATFORM_UNSUPPORTED', `No packaged parser binary is available for ${process.platform}-${process.arch}.`);
130
+ }
131
+ // The prepared cache is for this host. Keep upstream metadata and loader,
132
+ // but omit build sources and binaries for other operating systems.
133
+ await rm(path.join(installation.packageRoot, 'src'), { recursive: true, force: true });
134
+ for (const entry of await readdir(prebuilds)) {
135
+ if (path.join(prebuilds, entry) !== selected) {
136
+ await rm(path.join(prebuilds, entry), { recursive: true, force: true });
137
+ }
138
+ }
139
+ await verifyParser(definition, installation, signal);
140
+ try {
141
+ await rename(staging, destination);
142
+ } catch (error) {
143
+ // Concurrent processes may prepare the same immutable version. Only a
144
+ // complete, independently verified directory can win the atomic rename.
145
+ if (!['EEXIST', 'ENOTEMPTY'].includes(error.code)) throw error;
146
+ await installedParser(definition, destination);
147
+ }
148
+ return await installedParser(definition, destination);
149
+ } finally {
150
+ await rm(staging, { recursive: true, force: true });
151
+ }
152
+ }
153
+
154
+ function ensureInstalled(definition, root, options) {
155
+ const key = parserLocation(definition, root);
156
+ if (!installations.has(key)) {
157
+ const pending = installParser(definition, root, options).finally(() => installations.delete(key));
158
+ installations.set(key, pending);
159
+ }
160
+ return installations.get(key);
161
+ }
162
+
163
+ function selectedParsers(languages, all) {
164
+ if (all && languages.length) fail('PARSER_SELECTION_INVALID', 'Choose parser names or --all, not both.');
165
+ if (!all && !languages.length) fail('PARSER_SELECTION_REQUIRED', 'Name a parser or use --all.');
166
+ return [...new Set(all ? ['javascript', ...Object.keys(PACKAGES)] : languages)].map(parserDefinition);
167
+ }
168
+
169
+ export async function listParsers({ directory, environment = process.env } = {}) {
170
+ const root = parserRoot(directory, environment);
171
+ const parsers = [{ language: 'javascript', status: 'built-in', package: '@ast-grep/napi' }];
172
+ for (const language of Object.keys(PACKAGES)) {
173
+ const definition = parserDefinition(language);
174
+ try {
175
+ const installed = await installedParser(definition, parserLocation(definition, root));
176
+ parsers.push({ language, package: definition.package, version: definition.version, status: installed ? 'installed' : 'missing' });
177
+ } catch (error) {
178
+ parsers.push({ language, package: definition.package, version: definition.version, status: 'invalid', message: error.message });
179
+ }
180
+ }
181
+ return { status: 'ready', directory: root, parsers };
182
+ }
183
+
184
+ export async function installParsers({ languages = [], all = false, directory, environment = process.env, signal, onProgress } = {}) {
185
+ const definitions = selectedParsers(languages, all);
186
+ const root = parserRoot(directory, environment);
187
+ for (const definition of definitions) {
188
+ const installation = await ensureInstalled(definition, root, { signal, onProgress, environment });
189
+ await verifyParser(definition, installation, signal);
190
+ }
191
+ return { status: 'ready', directory: root, languages: definitions.map(({ language }) => language) };
192
+ }
193
+
194
+ export async function verifyParsers({ languages = [], all = false, directory, environment = process.env, signal } = {}) {
195
+ const definitions = selectedParsers(languages, all);
196
+ const root = parserRoot(directory, environment);
197
+ for (const definition of definitions) {
198
+ const installation = await installedParser(definition, parserLocation(definition, root));
199
+ if (!installation) fail('PARSER_NOT_INSTALLED', `${definition.language} is not installed in ${root}. Run genesis parsers install ${definition.language}.`);
200
+ await verifyParser(definition, installation, signal);
201
+ }
202
+ return { status: 'ready', directory: root, languages: definitions.map(({ language }) => language) };
203
+ }
204
+
205
+ export async function prepareCodeParsers(languages, { environment = process.env, signal = cancellation.getStore() } = {}) {
206
+ const policy = parserEnvironment({ environment });
207
+ const root = policy.GENESIS_PARSER_ROOT;
208
+ const registrations = {};
209
+ const errors = {};
210
+ for (const language of new Set(languages)) {
211
+ if (['JavaScript', 'TypeScript', 'Tsx'].includes(language)) continue;
212
+ try {
213
+ const definition = parserDefinition(language);
214
+ let installation = await installedParser(definition, parserLocation(definition, root));
215
+ if (!installation) {
216
+ if (policy.GENESIS_PARSER_AUTO_INSTALL === '0') {
217
+ fail('PARSER_NOT_INSTALLED', `${language} is not installed in ${root}; automatic parser installation is disabled.`);
218
+ }
219
+ installation = await ensureInstalled(definition, root, {
220
+ signal, environment, onProgress: (message) => process.stderr.write(`[genesis] ${message}\n`),
221
+ });
222
+ }
223
+ // Read the upstream loader's data without loading the native library.
224
+ registrations[language] = { ...require(installation.entry) };
225
+ } catch (error) {
226
+ if (error.code === 'PROCESS_ABORTED') throw error;
227
+ errors[language] = { code: error.code || 'PARSER_LOAD_FAILED', message: error.message };
228
+ }
229
+ }
230
+ return { registrations, errors };
231
+ }
@@ -0,0 +1,20 @@
1
+ import { parseAsync, registerDynamicLanguage } from '@ast-grep/napi';
2
+ import { astGrepCodeIndexers } from './ast-grep.js';
3
+
4
+ try {
5
+ const chunks = [];
6
+ for await (const chunk of process.stdin) chunks.push(chunk);
7
+ const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
8
+ if (Object.keys(input.registrations).length) registerDynamicLanguage(input.registrations);
9
+ const contributions = [];
10
+ for (const id of input.indexers) {
11
+ const result = await astGrepCodeIndexers.get(id).extract({
12
+ files: input.files, projectRoot: input.projectRoot, parseAsync, parserErrors: input.errors,
13
+ });
14
+ contributions.push({ ...result, extractor: id });
15
+ }
16
+ process.stdout.write(JSON.stringify(contributions));
17
+ } catch (error) {
18
+ process.stderr.write(`${error.stack || error}\n`);
19
+ process.exitCode = 1;
20
+ }
@@ -38,6 +38,7 @@ function signalProcessTree(child, signal) {
38
38
 
39
39
  function executeFile(command, args, {
40
40
  maxBuffer,
41
+ input,
41
42
  signal,
42
43
  terminationGraceMs = DEFAULT_TERMINATION_GRACE_MS,
43
44
  timeoutMs,
@@ -130,7 +131,10 @@ function executeFile(command, args, {
130
131
  )), timeoutMs);
131
132
  timeoutTimer.unref?.();
132
133
  }
133
- child.stdin?.end();
134
+ child.stdin?.on('error', (error) => {
135
+ if (error.code !== 'EPIPE') spawnError = error;
136
+ });
137
+ child.stdin?.end(input);
134
138
  });
135
139
  }
136
140
 
@@ -156,6 +160,7 @@ export async function runProcess(command, args, {
156
160
  env = process.env,
157
161
  maxBytes = 32 * 1024 * 1024,
158
162
  code = 'PROCESS_EXEC_FAILED',
163
+ input,
159
164
  signal,
160
165
  terminationGraceMs,
161
166
  timeoutMs,
@@ -168,6 +173,7 @@ export async function runProcess(command, args, {
168
173
  encoding: 'buffer',
169
174
  maxBuffer: maxBytes,
170
175
  shell: false,
176
+ input,
171
177
  signal,
172
178
  terminationGraceMs,
173
179
  timeoutMs,
@@ -93,13 +93,12 @@ function moduleContents(source, programPath) {
93
93
  const sources = sourceLines.map((line) => line.match(SOURCE_LINE)?.[1]);
94
94
  const nextHeading = headings.find(({ index }) => index > contractHeading[0].index);
95
95
  const contractLines = lines
96
- .slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length)
97
- .filter((line) => line.trim());
96
+ .slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length);
98
97
  if (
99
98
  sources.length === 0
100
99
  || sources.some((sourcePath) => sourcePath === undefined)
101
100
  || new Set(sources).size !== sources.length
102
- || contractLines.length === 0
101
+ || !contractLines.some((line) => line.trim())
103
102
  ) {
104
103
  throw new GenesisError(
105
104
  'PROGRAM_INVALID',
@@ -33,7 +33,10 @@ export async function inspectSubsystems({ projectRoot = process.cwd() } = {}) {
33
33
  const empty = lines.join('\n').trim() === '- Nothing.';
34
34
  if (!empty && !lines.some((line) => line.trim())) invalid('An empty map must say - Nothing.');
35
35
  if (!empty) for (const line of lines) {
36
- if (!line.trim()) continue;
36
+ if (!line.trim()) {
37
+ if (current && !section) current.description += '\n';
38
+ continue;
39
+ }
37
40
  const heading = line.match(/^## `([a-z0-9]+(?:-[a-z0-9]+)*)` (\S.*)$/u);
38
41
  if (heading) {
39
42
  finish();
package/src/index.js CHANGED
@@ -31,6 +31,7 @@ import { projectSessionContext, projectTurnContext } from './index/session-conte
31
31
  import { withTrustedGitRepository } from './index/process.js';
32
32
 
33
33
  export { inspectProject };
34
+ export { listParsers, installParsers, verifyParsers, parserEnvironment } from './index/code-indexers/parsers.js';
34
35
  export { listTemplates } from './index/template-catalog.js';
35
36
  export { applyTemplate } from './index/template-project.js';
36
37
  import {