genesis-compiler 1.6.0 → 1.7.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 +7 -0
- package/docs/parsers.md +46 -0
- package/package.json +6 -13
- package/src/cli.js +43 -10
- package/src/index/code-index.js +2 -19
- package/src/index/code-indexers/ast-grep.js +40 -28
- package/src/index/code-indexers/parsers.js +231 -0
- package/src/index/code-indexers/worker.js +20 -0
- package/src/index/process.js +7 -1
- package/src/index/program.js +2 -3
- package/src/index/subsystems.js +4 -1
- package/src/index.js +1 -0
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
|
package/docs/parsers.md
ADDED
|
@@ -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.
|
|
3
|
+
"version": "1.7.0",
|
|
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
|
}
|
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 === '
|
|
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
|
-
|
|
623
|
-
|
|
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
|
);
|
package/src/index/code-index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
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
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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
|
+
}
|
package/src/index/process.js
CHANGED
|
@@ -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?.
|
|
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,
|
package/src/index/program.js
CHANGED
|
@@ -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.
|
|
101
|
+
|| !contractLines.some((line) => line.trim())
|
|
103
102
|
) {
|
|
104
103
|
throw new GenesisError(
|
|
105
104
|
'PROGRAM_INVALID',
|
package/src/index/subsystems.js
CHANGED
|
@@ -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())
|
|
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 {
|