@zenera/rag 1.1.6 → 1.1.8
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 +28 -2
- package/dist/command.js +41 -727
- package/dist/common/embedder.d.ts +3 -0
- package/dist/common/embedder.js +64 -0
- package/dist/common/locate.d.ts +18 -0
- package/dist/{schema → common}/locate.js +42 -28
- package/dist/common/manifest.d.ts +50 -0
- package/dist/common/manifest.js +62 -0
- package/dist/common/progress.d.ts +57 -0
- package/dist/common/progress.js +155 -0
- package/dist/common/prose.d.ts +13 -0
- package/dist/common/prose.js +56 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -4
- package/dist/schema/build.js +6 -2
- package/dist/schema/command.d.ts +3 -0
- package/dist/schema/command.js +819 -0
- package/dist/schema/files.d.ts +5 -27
- package/dist/schema/files.js +9 -29
- package/dist/schema/lookup.d.ts +1 -1
- package/dist/schema/lookup.js +1 -1
- package/dist/{present.d.ts → schema/present.d.ts} +5 -5
- package/dist/{present.js → schema/present.js} +2 -2
- package/dist/{query.d.ts → schema/query.d.ts} +1 -1
- package/dist/schema/readme.d.ts +6 -0
- package/dist/schema/readme.js +122 -0
- package/dist/{repl.d.ts → schema/repl.d.ts} +1 -1
- package/dist/schema/search.js +2 -1
- package/dist/schema/tools.d.ts +1 -1
- package/dist/schema/tools.js +97 -6
- package/dist/schema/trace.d.ts +52 -0
- package/dist/schema/trace.js +144 -0
- package/package.json +3 -3
- package/dist/schema/locate.d.ts +0 -20
- package/dist/schema/progress.d.ts +0 -26
- package/dist/schema/progress.js +0 -316
- /package/dist/{schema → common}/match.d.ts +0 -0
- /package/dist/{schema → common}/match.js +0 -0
- /package/dist/{query.js → schema/query.js} +0 -0
- /package/dist/{repl.js → schema/repl.js} +0 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import { bold, CliError, cyan, dim, EXIT, isInteractive, json, note, parse, table, usageError, write, } from '@zenera/cli/lib';
|
|
2
|
+
import { relative, resolve } from 'node:path';
|
|
3
|
+
import { resolveEmbedder } from "../common/embedder.js";
|
|
4
|
+
import { locateIndex, outputDir } from "../common/locate.js";
|
|
5
|
+
import { assertSameEmbedding } from "../common/manifest.js";
|
|
6
|
+
import { isGlob, loose, matcher, PatternError, wildcard } from "../common/match.js";
|
|
7
|
+
import { buildIndex } from "./build.js";
|
|
8
|
+
import { openIndex, readManifest, readSource, SCHEMA_INDEX } from "./files.js";
|
|
9
|
+
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
10
|
+
import { isFormat, present } from "./present.js";
|
|
11
|
+
import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
12
|
+
import { sourceTag } from "./render.js";
|
|
13
|
+
import { repl } from "./repl.js";
|
|
14
|
+
import { SchemaIndex } from "./search.js";
|
|
15
|
+
import { select, stitch } from "./subgraph.js";
|
|
16
|
+
import { chainOf, DEFAULT_TRACE_HOPS, traceNodes } from "./trace.js";
|
|
17
|
+
const { defaultDir: DEFAULT_DIR, envName: DIR_ENV } = SCHEMA_INDEX;
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// zen rag schema — an api description, as something to search
|
|
20
|
+
//
|
|
21
|
+
// The subject word is gone by the time this runs: the frame in `../command.ts`
|
|
22
|
+
// strips it and hands over the rest, so everything below is about openapi and
|
|
23
|
+
// nothing below knows there is a second subject.
|
|
24
|
+
//
|
|
25
|
+
// `search` has two modes and neither is the afterthought. Interactively it is
|
|
26
|
+
// a loop with prompts; non-interactively it is a tool, and that is the mode
|
|
27
|
+
// that has to be exactly specified: every field settable from a flag, the whole
|
|
28
|
+
// query settable as one JSON object, a stable `--json` shape, no terminal
|
|
29
|
+
// required, and exit 0 when nothing matched — an empty answer is an answer, and
|
|
30
|
+
// a caller that has to tell "no results" from "the index is missing" by parsing
|
|
31
|
+
// stderr will get it wrong.
|
|
32
|
+
//
|
|
33
|
+
// `list`, `grep` and `show` are the other half, and they are deliberately not
|
|
34
|
+
// searches. A ranking can only ever hand back the top of a list, so it cannot
|
|
35
|
+
// answer "is there a field called `password` anywhere" — the honest answer to
|
|
36
|
+
// that question is every match or none, and these three give it without asking
|
|
37
|
+
// a model or a credential for permission.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const USAGE = 'zen rag schema <index|search|list|grep|trace|show|stats> [spec...]';
|
|
40
|
+
const INDEX_USAGE = 'zen rag schema index --embedding <ref> [--out <dir>] <spec...>';
|
|
41
|
+
const SEARCH_USAGE = 'zen rag schema search [--dir <dir>] [query...]';
|
|
42
|
+
const LIST_USAGE = 'zen rag schema list <methods|types|properties> [--dir <dir>]';
|
|
43
|
+
const GREP_USAGE = 'zen rag schema grep <pattern> [--dir <dir>]';
|
|
44
|
+
const TRACE_USAGE = 'zen rag schema trace <pattern|id...> [--dir <dir>]';
|
|
45
|
+
const SHOW_USAGE = 'zen rag schema show [id...] [--method <name>] [--type <name>]';
|
|
46
|
+
export const command = {
|
|
47
|
+
summary: 'Search an openapi/swagger document as a graph.',
|
|
48
|
+
usage: USAGE,
|
|
49
|
+
details: [
|
|
50
|
+
'Commands',
|
|
51
|
+
...table([
|
|
52
|
+
[' index <spec...>', dim('Read the documents and write a searchable index.')],
|
|
53
|
+
[' search', dim('Ask it something. --interactive for a prompt.')],
|
|
54
|
+
[' list <what>', dim('Every method, type or property matching a pattern.')],
|
|
55
|
+
[' grep <pattern>', dim('Every literal match, ranked by nothing.')],
|
|
56
|
+
[' trace <pattern>', dim('Up from a field or schema to the calls that carry it.')],
|
|
57
|
+
[' show [id...]', dim('Print named nodes, with no search in between.')],
|
|
58
|
+
[' stats', dim('What is in an index, and what built it.')],
|
|
59
|
+
]),
|
|
60
|
+
'',
|
|
61
|
+
'Index',
|
|
62
|
+
...table([
|
|
63
|
+
[
|
|
64
|
+
' --embedding <ref>',
|
|
65
|
+
dim('Which embedder makes the vectors. Omit it to be shown the choices.'),
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
' -o, --out <dir>',
|
|
69
|
+
dim(`Where the index goes. Default ${DEFAULT_DIR}, or ${DIR_ENV}.`),
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
' --batch <n>',
|
|
73
|
+
dim('Texts per embedding request, and how often progress prints. Default 96.'),
|
|
74
|
+
],
|
|
75
|
+
[' --no-sources', dim('Do not keep a copy of each document in the index.')],
|
|
76
|
+
]),
|
|
77
|
+
'',
|
|
78
|
+
'Search terms (repeatable)',
|
|
79
|
+
...table([
|
|
80
|
+
[' <text>', dim('A bare phrase, the same as --all.')],
|
|
81
|
+
[' --all <q>', dim('Against everything, unfiltered.')],
|
|
82
|
+
[' --method <q>', dim('Operations.')],
|
|
83
|
+
[' --type <q>', dim('Schemas, on the side --direction names.')],
|
|
84
|
+
[' --input-type <q>', dim('Schemas a call accepts.')],
|
|
85
|
+
[' --output-type <q>', dim('Schemas a call returns.')],
|
|
86
|
+
[' --property <q>', dim('Fields and parameters, per --direction.')],
|
|
87
|
+
[' --input-property <q>', dim('Fields and parameters a call accepts.')],
|
|
88
|
+
[' --output-property <q>', dim('Fields a call returns.')],
|
|
89
|
+
[' --query <json|->', dim('A whole query object; - reads stdin.')],
|
|
90
|
+
]),
|
|
91
|
+
'',
|
|
92
|
+
'Search filters and shape',
|
|
93
|
+
...table([
|
|
94
|
+
[' -d, --dir <dir>', dim(`Which index. Found from here if unset; see ${DIR_ENV}.`)],
|
|
95
|
+
[' --embedding <ref>', dim('Must be the one the index was built with.')],
|
|
96
|
+
[' --direction <d>', dim('input | output | any. Default any.')],
|
|
97
|
+
[' --method-type <t>', dim('read_only | read_write | any. Default any.')],
|
|
98
|
+
[' --exclude-id <id>', dim('Drop a node. Repeatable, as are the three below.')],
|
|
99
|
+
[' --exclude-method <name>', dim('Drop an operation by name.')],
|
|
100
|
+
[' --exclude-type <name>', dim('Drop a schema by name.')],
|
|
101
|
+
[' --exclude-property <name>', dim('Drop a field by name.')],
|
|
102
|
+
[' --limit <n>', dim('Seeds kept per term. Default 5.')],
|
|
103
|
+
[' --max-hops <n>', dim('How far apart two hits may be. Default 3.')],
|
|
104
|
+
[' --max-nodes <n>', dim('Nodes per result. Default 200.')],
|
|
105
|
+
[' --format <f>', dim('text | mermaid | mermaid-flowchart | ts | openapi.')],
|
|
106
|
+
[' --show-source', dim('Name the document each operation and schema came from.')],
|
|
107
|
+
[' --no-docs', dim('Leave the descriptions out.')],
|
|
108
|
+
[' --interactive', dim('Prompt, search, refine. Needs a terminal.')],
|
|
109
|
+
[' --quiet', dim('No narration.')],
|
|
110
|
+
]),
|
|
111
|
+
'',
|
|
112
|
+
'Exact listing — no embedder, no credential',
|
|
113
|
+
...table([
|
|
114
|
+
[' list methods', dim('Operations. Filter with --path and --name.')],
|
|
115
|
+
[' list types', dim('Schemas. Filter with --name.')],
|
|
116
|
+
[' list properties', dim('Fields and parameters. Filter with --name and --path.')],
|
|
117
|
+
[' grep <pattern>', dim('Substring over every node; --regex for a regex.')],
|
|
118
|
+
[' --regex', dim('Read every pattern as a regex, list and grep alike.')],
|
|
119
|
+
[' --case-sensitive', dim('Match the capitals too.')],
|
|
120
|
+
[' --kind <k>', dim('grep: method | type | property. Repeatable.')],
|
|
121
|
+
[' --name <p>', dim('grep too: only nodes whose name matches. Repeatable.')],
|
|
122
|
+
[' --path <p>', dim('grep too: only what sits on a matching route.')],
|
|
123
|
+
[' --ids-only', dim('grep: bare ids, to pipe into show.')],
|
|
124
|
+
[' --source <name>', dim('Only this document, as `stats` names it.')],
|
|
125
|
+
[' --show-source', dim('Print which document each row came from.')],
|
|
126
|
+
[' --limit <n>', dim('Keep at most n; the count still reports them all.')],
|
|
127
|
+
]),
|
|
128
|
+
'',
|
|
129
|
+
dim(' A pattern with * or ? is a glob over the whole name; otherwise it is'),
|
|
130
|
+
dim(' a substring, so --name password finds ResetPasswordPayload. With'),
|
|
131
|
+
dim(' --regex it is a regex either way, so --path "^/(users|teams)/" works.'),
|
|
132
|
+
'',
|
|
133
|
+
'Trace — from a field to the operations that carry it',
|
|
134
|
+
...table([
|
|
135
|
+
[' <pattern|id...>', dim('What to trace up from. A name, a glob, or a node id.')],
|
|
136
|
+
[
|
|
137
|
+
' --kind <k>',
|
|
138
|
+
dim('method | type | property. Repeatable. Schemas and fields by default.'),
|
|
139
|
+
],
|
|
140
|
+
[' --direction <d>', dim('Keep only the calls that accept it, or return it.')],
|
|
141
|
+
[' --max-hops <n>', dim(`How far up to walk. Default ${DEFAULT_TRACE_HOPS}.`)],
|
|
142
|
+
[' --limit <n>', dim('Trace at most n matching nodes.')],
|
|
143
|
+
[' --routes <n>', dim('Operations printed per node; the count still has them all.')],
|
|
144
|
+
[' --ids-only', dim('Bare operation ids, one per line, for piping into show.')],
|
|
145
|
+
[' --regex', dim('Read the pattern as a regex. --case-sensitive too.')],
|
|
146
|
+
[' --source <name>', dim('Only nodes from this document.')],
|
|
147
|
+
]),
|
|
148
|
+
'',
|
|
149
|
+
'Show',
|
|
150
|
+
...table([
|
|
151
|
+
[' <id...>', dim('Node ids, e.g. Type:User or Property:User.email.')],
|
|
152
|
+
[' --method <name>', dim('An operation by name. * to take more. Repeatable.')],
|
|
153
|
+
[' --type <name>', dim('A schema by name. * to take more. Repeatable.')],
|
|
154
|
+
[' --source <name>', dim('A whole document, as it was indexed.')],
|
|
155
|
+
[' --show-source', dim('Name the document each node came from.')],
|
|
156
|
+
[' --exact', dim('Only what was named, without the neighbours.')],
|
|
157
|
+
]),
|
|
158
|
+
'',
|
|
159
|
+
dim(`Without --dir, the index is the nearest one at or above the working`),
|
|
160
|
+
dim(`directory; ${cyan(DIR_ENV)} names it outright.`),
|
|
161
|
+
'',
|
|
162
|
+
dim(`Credentials come from the ${cyan('zen')} keyring — try ${cyan('zen key ls')}.`),
|
|
163
|
+
],
|
|
164
|
+
async run(ctx) {
|
|
165
|
+
const [name, ...tail] = ctx.args;
|
|
166
|
+
switch (name) {
|
|
167
|
+
case 'index':
|
|
168
|
+
return await index(tail, ctx);
|
|
169
|
+
case 'search':
|
|
170
|
+
return await search(tail, ctx);
|
|
171
|
+
case 'list':
|
|
172
|
+
return await list(tail, ctx);
|
|
173
|
+
case 'grep':
|
|
174
|
+
return await grep(tail, ctx);
|
|
175
|
+
case 'trace':
|
|
176
|
+
return await trace(tail, ctx);
|
|
177
|
+
case 'show':
|
|
178
|
+
return await show(tail, ctx);
|
|
179
|
+
case 'stats':
|
|
180
|
+
return await stats(tail, ctx);
|
|
181
|
+
default:
|
|
182
|
+
throw usageError(name ? `unknown command "${name}"` : 'no command given', USAGE);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
async function index(args, ctx) {
|
|
187
|
+
const { values, positionals } = parse(args, {
|
|
188
|
+
out: { type: 'string', short: 'o' },
|
|
189
|
+
embedding: { type: 'string' },
|
|
190
|
+
batch: { type: 'string' },
|
|
191
|
+
'no-sources': { type: 'boolean' },
|
|
192
|
+
quiet: { type: 'boolean' },
|
|
193
|
+
}, INDEX_USAGE);
|
|
194
|
+
if (positionals.length === 0) {
|
|
195
|
+
throw usageError('no document given', INDEX_USAGE);
|
|
196
|
+
}
|
|
197
|
+
const out = outputDir(ctx.cwd, values.out, SCHEMA_INDEX);
|
|
198
|
+
const loud = !values.quiet && !ctx.json;
|
|
199
|
+
const chosen = await resolveEmbedder(values.embedding);
|
|
200
|
+
const started = Date.now();
|
|
201
|
+
const { manifest } = await buildIndex({
|
|
202
|
+
files: positionals.map((file) => resolve(ctx.cwd, file)),
|
|
203
|
+
out,
|
|
204
|
+
embedder: chosen,
|
|
205
|
+
embeddingRef: values.embedding,
|
|
206
|
+
indexer: 'zenera-rag',
|
|
207
|
+
batch: values.batch ? count(values.batch, '--batch') : undefined,
|
|
208
|
+
sources: !values['no-sources'],
|
|
209
|
+
onRead: loud
|
|
210
|
+
? (summary) => {
|
|
211
|
+
printSources(summary.sources);
|
|
212
|
+
// The first batch can take a while and says nothing while it
|
|
213
|
+
// does; this is the line that makes that a wait, not a hang.
|
|
214
|
+
note(dim(` embedding ${summary.counts.entities} entities with ${chosen.id} …`));
|
|
215
|
+
}
|
|
216
|
+
: undefined,
|
|
217
|
+
onProgress: loud
|
|
218
|
+
? (done, total) => note(dim(` embedded ${done}/${total} · ${Math.round((done / total) * 100)}% · ${elapsed(started)}`))
|
|
219
|
+
: undefined,
|
|
220
|
+
});
|
|
221
|
+
if (ctx.json) {
|
|
222
|
+
json({ out, manifest });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// stdout is the path and nothing else, so `DIR=$(zen rag schema index …)`
|
|
226
|
+
// works; what it means goes to stderr, where the narration lives.
|
|
227
|
+
note();
|
|
228
|
+
write(out);
|
|
229
|
+
note(` wrote ${bold(String(manifest.counts.entities))} entities to ${bold(out)}, ` +
|
|
230
|
+
`embedded with ${manifest.embedding.ref} (${manifest.embedding.dimensions}d)`);
|
|
231
|
+
const where = out === resolve(ctx.cwd, DEFAULT_DIR) ? '' : ` --dir ${relative(ctx.cwd, out) || out}`;
|
|
232
|
+
note(dim(` search it: ${cyan(`zen rag schema search${where} --all "what you are after"`)}`));
|
|
233
|
+
}
|
|
234
|
+
const HEADERS = ['PATHS', 'OPERATIONS', 'SCHEMAS', 'FIELDS'];
|
|
235
|
+
function elapsed(since) {
|
|
236
|
+
const seconds = Math.round((Date.now() - since) / 1000);
|
|
237
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
238
|
+
}
|
|
239
|
+
function printSources(sources) {
|
|
240
|
+
const rows = sources.map((s) => ({
|
|
241
|
+
name: s.file,
|
|
242
|
+
dialect: s.dialect,
|
|
243
|
+
cells: [s.paths, s.methods, s.types, s.properties],
|
|
244
|
+
}));
|
|
245
|
+
if (rows.length > 1) {
|
|
246
|
+
rows.push({
|
|
247
|
+
name: 'total',
|
|
248
|
+
dialect: '',
|
|
249
|
+
cells: HEADERS.map((_, i) => rows.reduce((n, r) => n + (r.cells[i] ?? 0), 0)),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Numbers are padded before they are styled: a colour code has no width,
|
|
253
|
+
// and `table` cannot know that.
|
|
254
|
+
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r.cells[i]).length)));
|
|
255
|
+
note();
|
|
256
|
+
notes(table([
|
|
257
|
+
[bold('SPEC'), bold('DIALECT'), ...HEADERS.map((h, i) => bold(h.padStart(widths[i])))],
|
|
258
|
+
...rows.map((r) => [
|
|
259
|
+
r.name === 'total' ? dim(r.name) : r.name,
|
|
260
|
+
dim(r.dialect),
|
|
261
|
+
...r.cells.map((c, i) => String(c).padStart(widths[i])),
|
|
262
|
+
]),
|
|
263
|
+
]).map((line) => ` ${line}`));
|
|
264
|
+
note();
|
|
265
|
+
}
|
|
266
|
+
const MANY = { type: 'string', multiple: true };
|
|
267
|
+
const SEARCH_OPTIONS = {
|
|
268
|
+
dir: { type: 'string', short: 'd' },
|
|
269
|
+
embedding: { type: 'string' },
|
|
270
|
+
all: MANY,
|
|
271
|
+
method: MANY,
|
|
272
|
+
type: MANY,
|
|
273
|
+
'input-type': MANY,
|
|
274
|
+
'output-type': MANY,
|
|
275
|
+
property: MANY,
|
|
276
|
+
'input-property': MANY,
|
|
277
|
+
'output-property': MANY,
|
|
278
|
+
query: { type: 'string' },
|
|
279
|
+
direction: { type: 'string' },
|
|
280
|
+
'method-type': { type: 'string' },
|
|
281
|
+
'exclude-id': MANY,
|
|
282
|
+
'exclude-method': MANY,
|
|
283
|
+
'exclude-type': MANY,
|
|
284
|
+
'exclude-property': MANY,
|
|
285
|
+
limit: { type: 'string' },
|
|
286
|
+
'max-hops': { type: 'string' },
|
|
287
|
+
'max-nodes': { type: 'string' },
|
|
288
|
+
format: { type: 'string' },
|
|
289
|
+
'no-docs': { type: 'boolean' },
|
|
290
|
+
'only-hits': { type: 'boolean' },
|
|
291
|
+
'show-source': { type: 'boolean' },
|
|
292
|
+
interactive: { type: 'boolean' },
|
|
293
|
+
quiet: { type: 'boolean' },
|
|
294
|
+
};
|
|
295
|
+
async function search(args, ctx) {
|
|
296
|
+
const { values, positionals } = parse(args, SEARCH_OPTIONS, SEARCH_USAGE);
|
|
297
|
+
const dir = indexDir(ctx, values.dir);
|
|
298
|
+
const format = formatOf(values.format);
|
|
299
|
+
const options = {
|
|
300
|
+
docs: !values['no-docs'],
|
|
301
|
+
onlyHits: values['only-hits'],
|
|
302
|
+
source: values['show-source'],
|
|
303
|
+
};
|
|
304
|
+
const query = { ...(await fromStdin(values.query)), ...fromFlags(values, positionals) };
|
|
305
|
+
// Everything that can be wrong about the invocation is settled before a
|
|
306
|
+
// credential is asked for, so a typo is a usage error and not a login.
|
|
307
|
+
if (values.interactive && !isInteractive()) {
|
|
308
|
+
throw usageError('--interactive needs a terminal', SEARCH_USAGE);
|
|
309
|
+
}
|
|
310
|
+
if (!values.interactive && isEmpty(query)) {
|
|
311
|
+
throw usageError('no query given', SEARCH_USAGE);
|
|
312
|
+
}
|
|
313
|
+
const manifest = await readManifest(dir);
|
|
314
|
+
const ref = values.embedding ?? manifest.embedding.ref;
|
|
315
|
+
assertSameEmbedding(manifest, ref);
|
|
316
|
+
const index = await SchemaIndex.open(dir, await resolveEmbedder(ref));
|
|
317
|
+
try {
|
|
318
|
+
if (values.interactive) {
|
|
319
|
+
await repl(index, query, { format, ...options });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const result = await index.search(query);
|
|
323
|
+
if (ctx.json) {
|
|
324
|
+
json({
|
|
325
|
+
seeds: result.seeds,
|
|
326
|
+
empty: result.empty,
|
|
327
|
+
subgraphs: result.subgraphs,
|
|
328
|
+
rendered: await present(index, result.subgraphs, format, options),
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const text = await present(index, result.subgraphs, format, options);
|
|
333
|
+
if (text) {
|
|
334
|
+
write(text);
|
|
335
|
+
}
|
|
336
|
+
if (!values.quiet) {
|
|
337
|
+
note(dim(` ${result.seeds.length} seed(s) · ${result.subgraphs.length} result(s)${result.empty.length > 0 ? ` · nothing for: ${result.empty.join(', ')}` : ''}`));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
index.close();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** Flags win over `--query`: the more specific spelling is the later thought. */
|
|
345
|
+
function fromFlags(values, positionals = []) {
|
|
346
|
+
const query = {};
|
|
347
|
+
const put = (key, value) => {
|
|
348
|
+
if (value !== undefined && (!Array.isArray(value) || value.length > 0)) {
|
|
349
|
+
query[key] = value;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
// A bare phrase is the unfiltered search; there is nothing else it could mean.
|
|
353
|
+
put('all', [...(values.all ?? []), ...positionals]);
|
|
354
|
+
put('methods', values.method);
|
|
355
|
+
put('types', values.type);
|
|
356
|
+
put('input_types', values['input-type']);
|
|
357
|
+
put('output_types', values['output-type']);
|
|
358
|
+
put('properties', values.property);
|
|
359
|
+
put('input_properties', values['input-property']);
|
|
360
|
+
put('output_properties', values['output-property']);
|
|
361
|
+
put('exclude_ids', values['exclude-id']);
|
|
362
|
+
put('exclude_methods', values['exclude-method']);
|
|
363
|
+
put('exclude_types', values['exclude-type']);
|
|
364
|
+
put('exclude_properties', values['exclude-property']);
|
|
365
|
+
put('direction', values.direction);
|
|
366
|
+
put('method_type', values['method-type']);
|
|
367
|
+
put('limit', values.limit && count(values.limit, '--limit'));
|
|
368
|
+
put('max_hops', values['max-hops'] && count(values['max-hops'], '--max-hops'));
|
|
369
|
+
put('max_nodes', values['max-nodes'] && count(values['max-nodes'], '--max-nodes'));
|
|
370
|
+
return check(query);
|
|
371
|
+
}
|
|
372
|
+
async function fromStdin(source) {
|
|
373
|
+
if (source === undefined) {
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
const text = source === '-' ? await readStdin() : source;
|
|
377
|
+
let parsed;
|
|
378
|
+
try {
|
|
379
|
+
parsed = JSON.parse(text);
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
throw usageError(`--query is not JSON: ${err.message}`, SEARCH_USAGE);
|
|
383
|
+
}
|
|
384
|
+
return check(parsed);
|
|
385
|
+
}
|
|
386
|
+
async function readStdin() {
|
|
387
|
+
const chunks = [];
|
|
388
|
+
for await (const chunk of process.stdin) {
|
|
389
|
+
chunks.push(chunk);
|
|
390
|
+
}
|
|
391
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
392
|
+
}
|
|
393
|
+
function check(value) {
|
|
394
|
+
try {
|
|
395
|
+
return parseQuery(value);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
if (err instanceof QueryError) {
|
|
399
|
+
throw usageError(err.message, SEARCH_USAGE);
|
|
400
|
+
}
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// list, grep
|
|
406
|
+
//
|
|
407
|
+
// The deterministic half. Neither takes an embedder, because neither ranks
|
|
408
|
+
// anything: `list` filters on the attributes a node already has and `grep`
|
|
409
|
+
// reads the same materialized string the index was built from. What comes back
|
|
410
|
+
// is every match, and where a limit cut the list the count still reports the
|
|
411
|
+
// total — being shown three of three hundred is only useful if you are told
|
|
412
|
+
// which of the two happened.
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
const SUBJECTS = {
|
|
415
|
+
methods: 'method',
|
|
416
|
+
types: 'type',
|
|
417
|
+
properties: 'property',
|
|
418
|
+
};
|
|
419
|
+
async function list(args, ctx) {
|
|
420
|
+
const { values, positionals } = parse(args, {
|
|
421
|
+
dir: { type: 'string', short: 'd' },
|
|
422
|
+
name: MANY,
|
|
423
|
+
path: MANY,
|
|
424
|
+
source: { type: 'string' },
|
|
425
|
+
'method-type': { type: 'string' },
|
|
426
|
+
direction: { type: 'string' },
|
|
427
|
+
regex: { type: 'boolean' },
|
|
428
|
+
'case-sensitive': { type: 'boolean' },
|
|
429
|
+
'show-source': { type: 'boolean' },
|
|
430
|
+
limit: { type: 'string' },
|
|
431
|
+
quiet: { type: 'boolean' },
|
|
432
|
+
}, LIST_USAGE);
|
|
433
|
+
const subject = positionals[0];
|
|
434
|
+
const kind = subject ? SUBJECTS[subject] : undefined;
|
|
435
|
+
if (!kind) {
|
|
436
|
+
throw usageError(subject ? `cannot list "${subject}"` : 'nothing named to list', `expected one of ${Object.keys(SUBJECTS).join(', ')}`);
|
|
437
|
+
}
|
|
438
|
+
if (positionals.length > 1) {
|
|
439
|
+
throw usageError('one subject at a time', LIST_USAGE);
|
|
440
|
+
}
|
|
441
|
+
const how = { regex: values.regex, caseSensitive: values['case-sensitive'] };
|
|
442
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
443
|
+
const found = listNodes(index.graph, {
|
|
444
|
+
kind,
|
|
445
|
+
name: patterns(values.name, '--name', how),
|
|
446
|
+
path: patterns(values.path, '--path', how),
|
|
447
|
+
source: values.source,
|
|
448
|
+
methodType: oneOf(values['method-type'], ['read_only', 'read_write'], '--method-type'),
|
|
449
|
+
direction: oneOf(values.direction, ['input', 'output'], '--direction'),
|
|
450
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
451
|
+
});
|
|
452
|
+
if (ctx.json) {
|
|
453
|
+
json({ found: found.found, truncated: found.truncated, rows: found.rows });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const lines = rowLines(index.graph, kind, found.rows, values['show-source']);
|
|
457
|
+
if (lines.length > 0) {
|
|
458
|
+
write(lines.join('\n'));
|
|
459
|
+
}
|
|
460
|
+
if (!values.quiet) {
|
|
461
|
+
note(dim(` ${found.found} ${subject}${shown(found.found, found.rows.length)}`));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function rowLines(graph, kind, rows, showSource = false) {
|
|
465
|
+
const from = (r) => (showSource ? [dim(sourceTag(r.source))] : []);
|
|
466
|
+
if (kind === 'method') {
|
|
467
|
+
return table(rows.map((r) => [`${r.httpMethod} ${r.path}`, r.name, ...from(r), doc(r.doc)]));
|
|
468
|
+
}
|
|
469
|
+
if (kind === 'type') {
|
|
470
|
+
return table(rows.map((r) => [
|
|
471
|
+
r.name,
|
|
472
|
+
dim(fields(propertyCount(graph, r.id))),
|
|
473
|
+
dim(r.direction === 'none' ? '' : `(${r.direction})`),
|
|
474
|
+
...from(r),
|
|
475
|
+
doc(r.doc),
|
|
476
|
+
]));
|
|
477
|
+
}
|
|
478
|
+
return table(rows.map((r) => [
|
|
479
|
+
`${r.parent ? `${r.parent}.` : ''}${r.name}${r.required ? '' : '?'}`,
|
|
480
|
+
`: ${r.signature || 'unknown'}`,
|
|
481
|
+
...from(r),
|
|
482
|
+
doc(r.doc),
|
|
483
|
+
]));
|
|
484
|
+
}
|
|
485
|
+
async function grep(args, ctx) {
|
|
486
|
+
const { values, positionals } = parse(args, {
|
|
487
|
+
dir: { type: 'string', short: 'd' },
|
|
488
|
+
regex: { type: 'boolean' },
|
|
489
|
+
'case-sensitive': { type: 'boolean' },
|
|
490
|
+
kind: MANY,
|
|
491
|
+
name: MANY,
|
|
492
|
+
path: MANY,
|
|
493
|
+
source: { type: 'string' },
|
|
494
|
+
'show-source': { type: 'boolean' },
|
|
495
|
+
limit: { type: 'string' },
|
|
496
|
+
'ids-only': { type: 'boolean' },
|
|
497
|
+
quiet: { type: 'boolean' },
|
|
498
|
+
}, GREP_USAGE);
|
|
499
|
+
if (positionals.length === 0) {
|
|
500
|
+
throw usageError('no pattern given', GREP_USAGE);
|
|
501
|
+
}
|
|
502
|
+
if (positionals.length > 1) {
|
|
503
|
+
throw usageError('one pattern at a time — quote it if it has spaces', GREP_USAGE);
|
|
504
|
+
}
|
|
505
|
+
const kinds = (values.kind ?? []).map((k) => oneOf(k, ['method', 'type', 'property'], '--kind'));
|
|
506
|
+
// The pattern is read as the flags say; the constraints are always names,
|
|
507
|
+
// so they stay globs-or-substrings even under --regex on the pattern.
|
|
508
|
+
const how = { caseSensitive: values['case-sensitive'] };
|
|
509
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
510
|
+
const result = pattern(() => grepNodes(index.graph, matcher(positionals[0], {
|
|
511
|
+
regex: values.regex,
|
|
512
|
+
caseSensitive: values['case-sensitive'],
|
|
513
|
+
}), {
|
|
514
|
+
kinds,
|
|
515
|
+
source: values.source,
|
|
516
|
+
name: patterns(values.name, '--name', how),
|
|
517
|
+
path: patterns(values.path, '--path', how),
|
|
518
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
519
|
+
}));
|
|
520
|
+
if (ctx.json) {
|
|
521
|
+
json({
|
|
522
|
+
found: result.found,
|
|
523
|
+
truncated: result.truncated,
|
|
524
|
+
matches: result.matches.map((m) => ({ id: m.id, ...m.attributes, text: m.text })),
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (result.matches.length > 0) {
|
|
529
|
+
const lines = values['ids-only']
|
|
530
|
+
? result.matches.map((m) => m.id)
|
|
531
|
+
: table(result.matches.map((m) => [
|
|
532
|
+
m.id,
|
|
533
|
+
...(values['show-source'] ? [dim(sourceTag(m.attributes.source))] : []),
|
|
534
|
+
dim(clip(m.text, 140)),
|
|
535
|
+
]));
|
|
536
|
+
write(lines.join('\n'));
|
|
537
|
+
}
|
|
538
|
+
if (!values.quiet && !values['ids-only']) {
|
|
539
|
+
note(dim(` ${result.found} match(es)${shown(result.found, result.matches.length)}`));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function trace(args, ctx) {
|
|
543
|
+
const { values, positionals } = parse(args, {
|
|
544
|
+
dir: { type: 'string', short: 'd' },
|
|
545
|
+
kind: MANY,
|
|
546
|
+
direction: { type: 'string' },
|
|
547
|
+
'max-hops': { type: 'string' },
|
|
548
|
+
limit: { type: 'string' },
|
|
549
|
+
routes: { type: 'string' },
|
|
550
|
+
'ids-only': { type: 'boolean' },
|
|
551
|
+
regex: { type: 'boolean' },
|
|
552
|
+
'case-sensitive': { type: 'boolean' },
|
|
553
|
+
source: { type: 'string' },
|
|
554
|
+
'show-source': { type: 'boolean' },
|
|
555
|
+
quiet: { type: 'boolean' },
|
|
556
|
+
}, TRACE_USAGE);
|
|
557
|
+
if (positionals.length === 0) {
|
|
558
|
+
throw usageError('nothing named to trace', TRACE_USAGE);
|
|
559
|
+
}
|
|
560
|
+
const side = oneOf(values.direction, ['input', 'output'], '--direction');
|
|
561
|
+
const kinds = (values.kind ?? []).map((k) => oneOf(k, ['method', 'type', 'property'], '--kind'));
|
|
562
|
+
// A node id is a thing, not a pattern: someone pasting `Type:User` back
|
|
563
|
+
// from an earlier answer means that node and no other.
|
|
564
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
565
|
+
const ids = positionals.filter((p) => index.graph.hasNode(p));
|
|
566
|
+
const words = positionals.filter((p) => !ids.includes(p));
|
|
567
|
+
const how = { regex: values.regex, caseSensitive: values['case-sensitive'] };
|
|
568
|
+
const result = traceNodes(index.graph, {
|
|
569
|
+
ids,
|
|
570
|
+
kinds: kinds.length > 0 ? kinds : undefined,
|
|
571
|
+
name: patterns(words, '<pattern>', how),
|
|
572
|
+
source: values.source,
|
|
573
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
574
|
+
maxRoutes: values.routes ? count(values.routes, '--routes') : undefined,
|
|
575
|
+
maxHops: values['max-hops'] ? count(values['max-hops'], '--max-hops') : undefined,
|
|
576
|
+
});
|
|
577
|
+
const traces = result.traces.map((t) => ({
|
|
578
|
+
...t,
|
|
579
|
+
routes: t.routes.filter((r) => !side || r.direction === side || r.direction === 'both'),
|
|
580
|
+
}));
|
|
581
|
+
if (ctx.json) {
|
|
582
|
+
json({
|
|
583
|
+
found: result.found,
|
|
584
|
+
truncated: result.truncated,
|
|
585
|
+
traces: traces.map((t) => ({
|
|
586
|
+
id: t.id,
|
|
587
|
+
name: t.attributes.name,
|
|
588
|
+
found: t.found,
|
|
589
|
+
truncated: t.truncated,
|
|
590
|
+
routes: t.routes.map((r) => ({
|
|
591
|
+
id: r.id,
|
|
592
|
+
name: r.attributes.name,
|
|
593
|
+
method: r.attributes.httpMethod,
|
|
594
|
+
path: r.attributes.path,
|
|
595
|
+
direction: r.direction,
|
|
596
|
+
hops: r.hops,
|
|
597
|
+
via: r.via,
|
|
598
|
+
chain: chainOf(index.graph, t.id, r),
|
|
599
|
+
})),
|
|
600
|
+
})),
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (values['ids-only']) {
|
|
605
|
+
const operations = [...new Set(traces.flatMap((t) => t.routes.map((r) => r.id)))];
|
|
606
|
+
if (operations.length > 0) {
|
|
607
|
+
write(operations.join('\n'));
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const lines = [];
|
|
612
|
+
for (const t of traces) {
|
|
613
|
+
lines.push(t.id);
|
|
614
|
+
if (t.routes.length === 0) {
|
|
615
|
+
// Reachable by nothing is an answer, and a useful one: a schema no
|
|
616
|
+
// call carries is dead weight in the document, or a wrong guess.
|
|
617
|
+
lines.push(dim(' no operation reaches it'));
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
lines.push(...table(t.routes.map((r) => [
|
|
621
|
+
` ${r.attributes.httpMethod} ${r.attributes.path}`,
|
|
622
|
+
r.attributes.name,
|
|
623
|
+
dim(r.direction),
|
|
624
|
+
...(values['show-source'] ? [dim(sourceTag(r.attributes.source))] : []),
|
|
625
|
+
dim(chainOf(index.graph, t.id, r)),
|
|
626
|
+
])));
|
|
627
|
+
}
|
|
628
|
+
if (lines.length > 0) {
|
|
629
|
+
write(lines.join('\n'));
|
|
630
|
+
}
|
|
631
|
+
if (!values.quiet) {
|
|
632
|
+
const routes = traces.reduce((n, t) => n + t.routes.length, 0);
|
|
633
|
+
note(dim(` ${result.found} node(s)${shown(result.found, traces.length)} · ${routes} operation(s)`));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
const shown = (found, kept) => (kept < found ? `, showing ${kept}` : '');
|
|
638
|
+
/**
|
|
639
|
+
* Where the index is, said out loud when nobody named it. Finding one and not
|
|
640
|
+
* saying which would make every answer here unattributable.
|
|
641
|
+
*/
|
|
642
|
+
function indexDir(ctx, flag) {
|
|
643
|
+
const { dir, from } = locateIndex(ctx.cwd, flag, SCHEMA_INDEX);
|
|
644
|
+
if (from === 'found' && !ctx.json) {
|
|
645
|
+
note(dim(` using ${relative(ctx.cwd, dir) || dir}`));
|
|
646
|
+
}
|
|
647
|
+
return dir;
|
|
648
|
+
}
|
|
649
|
+
function patterns(values, flag, options = {}) {
|
|
650
|
+
if (!values || values.length === 0) {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
return values.map((p) => pattern(() => loose(p, options), flag));
|
|
654
|
+
}
|
|
655
|
+
/** A bad pattern is a bad invocation, not a failure of the index. */
|
|
656
|
+
function pattern(run, flag) {
|
|
657
|
+
try {
|
|
658
|
+
return run();
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
if (err instanceof PatternError) {
|
|
662
|
+
throw usageError(`${flag ? `${flag}: ` : ''}${err.message}`, USAGE);
|
|
663
|
+
}
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function oneOf(value, allowed, flag) {
|
|
668
|
+
if (value === undefined || value === 'any') {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
if (!allowed.includes(value)) {
|
|
672
|
+
throw usageError(`${flag} cannot be "${value}"`, `expected ${allowed.join(' or ')}`);
|
|
673
|
+
}
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
const doc = (text) => (text ? dim(`— ${clip(text.replace(/\s+/g, ' '), 90)}`) : '');
|
|
677
|
+
const clip = (text, max) => text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
678
|
+
/**
|
|
679
|
+
* No embedder and no store: naming a node is a graph lookup, and asking for a
|
|
680
|
+
* credential to print something already on disk would be theatre.
|
|
681
|
+
*
|
|
682
|
+
* Ids are the precise way in, and `--method`/`--type` are the way in for
|
|
683
|
+
* someone who has a name rather than an id — which, with `--format openapi
|
|
684
|
+
* --exact`, is how a resolved slice of the document is got out.
|
|
685
|
+
*/
|
|
686
|
+
async function show(args, ctx) {
|
|
687
|
+
const { values, positionals } = parse(args, {
|
|
688
|
+
dir: { type: 'string', short: 'd' },
|
|
689
|
+
method: MANY,
|
|
690
|
+
type: MANY,
|
|
691
|
+
source: { type: 'string' },
|
|
692
|
+
exact: { type: 'boolean' },
|
|
693
|
+
format: { type: 'string' },
|
|
694
|
+
'max-nodes': { type: 'string' },
|
|
695
|
+
'no-docs': { type: 'boolean' },
|
|
696
|
+
'show-source': { type: 'boolean' },
|
|
697
|
+
quiet: { type: 'boolean' },
|
|
698
|
+
}, SHOW_USAGE);
|
|
699
|
+
const format = formatOf(values.format);
|
|
700
|
+
const dir = indexDir(ctx, values.dir);
|
|
701
|
+
// A whole document, verbatim: the copy kept at index time is the resolved
|
|
702
|
+
// original, and anything rebuilt from the graph would be a paraphrase.
|
|
703
|
+
if (values.source && format === 'openapi' && positionals.length === 0 && !named(values)) {
|
|
704
|
+
const document = await readSource(dir, values.source);
|
|
705
|
+
if (document) {
|
|
706
|
+
write(document);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (!values.quiet) {
|
|
710
|
+
note(dim(' this index kept no copy of the documents — rebuilding it from the graph'));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const index = await openIndex(dir);
|
|
714
|
+
const ids = resolveIds(index.graph, positionals, values);
|
|
715
|
+
const subgraphs = values.exact
|
|
716
|
+
? [select(index.graph, ids)]
|
|
717
|
+
: stitch(index.graph, ids.map((id) => ({ id, term: id, field: 'show', score: 1 })), {
|
|
718
|
+
maxNodes: values['max-nodes']
|
|
719
|
+
? count(values['max-nodes'], '--max-nodes')
|
|
720
|
+
: undefined,
|
|
721
|
+
});
|
|
722
|
+
const text = await present(index, subgraphs, format, {
|
|
723
|
+
docs: !values['no-docs'],
|
|
724
|
+
source: values['show-source'],
|
|
725
|
+
});
|
|
726
|
+
if (ctx.json) {
|
|
727
|
+
json({ ids, subgraphs, rendered: text });
|
|
728
|
+
}
|
|
729
|
+
else if (text) {
|
|
730
|
+
write(text);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const named = (values) => Boolean(values.method?.length || values.type?.length);
|
|
734
|
+
/** Ids as given, plus whatever the name selectors resolve to. */
|
|
735
|
+
function resolveIds(graph, ids, values) {
|
|
736
|
+
if (ids.length === 0 && !named(values) && !values.source) {
|
|
737
|
+
throw usageError('no node named', SHOW_USAGE);
|
|
738
|
+
}
|
|
739
|
+
const missing = ids.filter((id) => !graph.hasNode(id));
|
|
740
|
+
if (missing.length > 0) {
|
|
741
|
+
throw new CliError(`no such node: ${missing.join(', ')}`, EXIT.failed, 'ids look like `Type:User` or `Property:User.email`');
|
|
742
|
+
}
|
|
743
|
+
const out = new Set(ids);
|
|
744
|
+
for (const kind of ['method', 'type']) {
|
|
745
|
+
for (const wanted of values[kind] ?? []) {
|
|
746
|
+
// Selecting, not searching: a bare name means that name. A star is
|
|
747
|
+
// the way to ask for more than one.
|
|
748
|
+
const match = isGlob(wanted)
|
|
749
|
+
? pattern(() => wildcard(wanted), `--${kind}`)
|
|
750
|
+
: (name) => name === wanted;
|
|
751
|
+
const rows = listNodes(graph, { kind, name: [match], source: values.source });
|
|
752
|
+
// A selector that matched nothing is a wrong answer, not an empty
|
|
753
|
+
// one: the caller named something they believe is there.
|
|
754
|
+
if (rows.found === 0) {
|
|
755
|
+
throw new CliError(`no ${kind} called ${wanted}`, EXIT.failed, `try: zen rag schema list ${kind}s --name "${wanted}"`);
|
|
756
|
+
}
|
|
757
|
+
for (const row of rows.rows) {
|
|
758
|
+
out.add(row.id);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// `--source` on its own means the whole document.
|
|
763
|
+
if (out.size === 0 && values.source) {
|
|
764
|
+
for (const kind of ['method', 'type']) {
|
|
765
|
+
for (const row of listNodes(graph, { kind, source: values.source }).rows) {
|
|
766
|
+
out.add(row.id);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (out.size === 0) {
|
|
770
|
+
throw new CliError(`nothing in this index came from ${values.source}`, EXIT.failed);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return [...out];
|
|
774
|
+
}
|
|
775
|
+
async function stats(args, ctx) {
|
|
776
|
+
const { values } = parse(args, { dir: { type: 'string', short: 'd' } }, 'zen rag schema stats [--dir <dir>]');
|
|
777
|
+
const dir = indexDir(ctx, values.dir);
|
|
778
|
+
const manifest = await readManifest(dir);
|
|
779
|
+
if (ctx.json) {
|
|
780
|
+
json(manifest);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
note(bold(dir));
|
|
784
|
+
notes(table([
|
|
785
|
+
[' built', manifest.createdAt],
|
|
786
|
+
[' by', manifest.indexer],
|
|
787
|
+
[' embedder', `${manifest.embedding.ref} (${manifest.embedding.dimensions}d)`],
|
|
788
|
+
[
|
|
789
|
+
' indexes',
|
|
790
|
+
`fts ${yes(manifest.indexes.fts)} · vector ${yes(manifest.indexes.vector)}`,
|
|
791
|
+
],
|
|
792
|
+
]));
|
|
793
|
+
printSources(manifest.sources);
|
|
794
|
+
notes(table([[' entities', String(manifest.counts.entities)]]).map(dim));
|
|
795
|
+
}
|
|
796
|
+
function notes(lines) {
|
|
797
|
+
for (const line of lines) {
|
|
798
|
+
note(line);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const yes = (value) => (value ? 'yes' : 'no');
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
function formatOf(value) {
|
|
804
|
+
if (value === undefined) {
|
|
805
|
+
return 'text';
|
|
806
|
+
}
|
|
807
|
+
if (!isFormat(value)) {
|
|
808
|
+
throw usageError(`unknown format "${value}"`, 'expected text, mermaid, mermaid-flowchart, ts or openapi');
|
|
809
|
+
}
|
|
810
|
+
return value;
|
|
811
|
+
}
|
|
812
|
+
function count(value, flag) {
|
|
813
|
+
const number = Number(value);
|
|
814
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
815
|
+
throw usageError(`${flag} must be a whole number of at least 1`, USAGE);
|
|
816
|
+
}
|
|
817
|
+
return number;
|
|
818
|
+
}
|
|
819
|
+
//# sourceMappingURL=command.js.map
|