patram 0.0.2 → 0.2.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/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +270 -0
- package/lib/build-graph.js +156 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/cli-help-metadata.js +552 -0
- package/lib/command-output.js +83 -0
- package/lib/derived-summary.js +278 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +19 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +62 -0
- package/lib/layout-stored-queries.js +361 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +505 -18
- package/lib/load-patram-config.types.ts +40 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +88 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +446 -0
- package/lib/parse-cli-arguments.js +266 -0
- package/lib/parse-cli-arguments.types.ts +69 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +707 -0
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +464 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +368 -0
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +161 -0
- package/lib/render-output-view.js +222 -0
- package/lib/render-plain-output.js +182 -0
- package/lib/render-rich-output.js +240 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +66 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/tagged-fenced-block-error.js +17 -0
- package/lib/tagged-fenced-block-markdown.js +111 -0
- package/lib/tagged-fenced-block-metadata.js +97 -0
- package/lib/tagged-fenced-block-parser.js +292 -0
- package/lib/tagged-fenced-blocks.js +100 -0
- package/lib/tagged-fenced-blocks.types.ts +38 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +28 -12
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- package/lib/patram-config.test.js +0 -147
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
/**
|
|
3
|
+
* @import {
|
|
4
|
+
* CliCommandName,
|
|
5
|
+
* CliHelpTopicName,
|
|
6
|
+
* } from './parse-cli-arguments.types.ts';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* description: string,
|
|
12
|
+
* label: string,
|
|
13
|
+
* }} CliHelpOption
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* allowed_option_names: Set<string>,
|
|
19
|
+
* examples: string[],
|
|
20
|
+
* extra_positionals_message: string,
|
|
21
|
+
* help_topics: CliHelpTopicName[],
|
|
22
|
+
* max_positionals: number,
|
|
23
|
+
* min_positionals: number,
|
|
24
|
+
* missing_argument_examples: string[],
|
|
25
|
+
* missing_argument_label: string | null,
|
|
26
|
+
* missing_usage_lines: string[],
|
|
27
|
+
* option_column_width: number,
|
|
28
|
+
* options: CliHelpOption[],
|
|
29
|
+
* related: CliCommandName[],
|
|
30
|
+
* root_summary: string,
|
|
31
|
+
* summary: string,
|
|
32
|
+
* syntax_lines?: string[],
|
|
33
|
+
* usage_lines: string[],
|
|
34
|
+
* }} CliCommandDefinition
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {{
|
|
39
|
+
* examples: string[],
|
|
40
|
+
* lead: string,
|
|
41
|
+
* operators: CliHelpOption[],
|
|
42
|
+
* relation_terms: CliHelpOption[],
|
|
43
|
+
* terms: string[],
|
|
44
|
+
* usage_lines: string[],
|
|
45
|
+
* }} CliHelpTopicDefinition
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export const COMMAND_NAMES = /** @type {const} */ ([
|
|
49
|
+
'check',
|
|
50
|
+
'query',
|
|
51
|
+
'queries',
|
|
52
|
+
'show',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export const HELP_TOPIC_NAMES = /** @type {const} */ (['query-language']);
|
|
56
|
+
|
|
57
|
+
export const GLOBAL_OPTION_NAMES = new Set([
|
|
58
|
+
'help',
|
|
59
|
+
'plain',
|
|
60
|
+
'json',
|
|
61
|
+
'color',
|
|
62
|
+
'no-color',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const ROOT_HELP_SUMMARY = 'Patram explores docs and how they link to sources.';
|
|
66
|
+
const ROOT_HELP_USAGE_LINES = [
|
|
67
|
+
'patram <command> [options]',
|
|
68
|
+
'patram help [command]',
|
|
69
|
+
];
|
|
70
|
+
const ROOT_HELP_GLOBAL_OPTIONS = [
|
|
71
|
+
'--help',
|
|
72
|
+
'--plain',
|
|
73
|
+
'--json',
|
|
74
|
+
'--color <auto|always|never>',
|
|
75
|
+
'--no-color',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** @type {Record<CliCommandName, CliCommandDefinition>} */
|
|
79
|
+
const COMMAND_DEFINITIONS = {
|
|
80
|
+
check: {
|
|
81
|
+
allowed_option_names: new Set(),
|
|
82
|
+
examples: [
|
|
83
|
+
'patram check',
|
|
84
|
+
'patram check docs',
|
|
85
|
+
'patram check docs/patram.md',
|
|
86
|
+
],
|
|
87
|
+
extra_positionals_message: 'Check accepts at most one path.',
|
|
88
|
+
help_topics: [],
|
|
89
|
+
max_positionals: 1,
|
|
90
|
+
min_positionals: 0,
|
|
91
|
+
missing_argument_examples: [],
|
|
92
|
+
missing_argument_label: null,
|
|
93
|
+
missing_usage_lines: ['patram check [path]'],
|
|
94
|
+
option_column_width: 10,
|
|
95
|
+
options: [
|
|
96
|
+
{
|
|
97
|
+
description: 'Print plain text output',
|
|
98
|
+
label: '--plain',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
description: 'Print JSON output',
|
|
102
|
+
label: '--json',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
related: ['show', 'query'],
|
|
106
|
+
root_summary: 'Validate a project, directory, or file',
|
|
107
|
+
summary:
|
|
108
|
+
'Validate a project, directory, or file and report graph diagnostics.',
|
|
109
|
+
usage_lines: ['patram check [path] [options]'],
|
|
110
|
+
},
|
|
111
|
+
query: {
|
|
112
|
+
allowed_option_names: new Set([
|
|
113
|
+
'explain',
|
|
114
|
+
'limit',
|
|
115
|
+
'lint',
|
|
116
|
+
'offset',
|
|
117
|
+
'where',
|
|
118
|
+
]),
|
|
119
|
+
examples: [
|
|
120
|
+
'patram query active-plans',
|
|
121
|
+
'patram query --where "tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md"',
|
|
122
|
+
'patram query --where "status not in [done, dropped, superseded]"',
|
|
123
|
+
'patram query --where "kind=plan and none(in:tracked_in, kind=decision)"',
|
|
124
|
+
'patram query --where "count(in:decided_by, kind=task) = 0"',
|
|
125
|
+
'patram query ready-tasks --explain',
|
|
126
|
+
'patram query --where "kind=decision and status=accepted and count(in:decided_by, kind=task) = 0" --lint',
|
|
127
|
+
'patram query active-plans --limit 10 --offset 20',
|
|
128
|
+
],
|
|
129
|
+
extra_positionals_message:
|
|
130
|
+
'Query accepts either "--where" or a stored query name.',
|
|
131
|
+
help_topics: ['query-language'],
|
|
132
|
+
max_positionals: 1,
|
|
133
|
+
min_positionals: 0,
|
|
134
|
+
missing_argument_examples: [
|
|
135
|
+
'patram query active-plans',
|
|
136
|
+
'patram query --where "tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md"',
|
|
137
|
+
],
|
|
138
|
+
missing_argument_label: '<name> or --where "<clause>"',
|
|
139
|
+
missing_usage_lines: [
|
|
140
|
+
'patram query <name> [options]',
|
|
141
|
+
'patram query --where "<clause>" [options]',
|
|
142
|
+
],
|
|
143
|
+
option_column_width: 19,
|
|
144
|
+
options: [
|
|
145
|
+
{
|
|
146
|
+
description: 'Run an ad hoc query instead of a stored query',
|
|
147
|
+
label: '--where <clause>',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
description: 'Skip the first N matches',
|
|
151
|
+
label: '--offset <number>',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
description: 'Limit the number of matches',
|
|
155
|
+
label: '--limit <number>',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
description: 'Explain the resolved query without rendering results',
|
|
159
|
+
label: '--explain',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
description: 'Validate syntax and relation references only',
|
|
163
|
+
label: '--lint',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
description: 'Print plain text output',
|
|
167
|
+
label: '--plain',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
description: 'Print JSON output',
|
|
171
|
+
label: '--json',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
related: ['queries', 'show'],
|
|
175
|
+
root_summary: 'Run a stored query or an ad hoc where clause',
|
|
176
|
+
summary:
|
|
177
|
+
'Run a stored query or an ad hoc where clause against graph nodes.',
|
|
178
|
+
syntax_lines: [
|
|
179
|
+
'id=<value> | kind=<value> | path=<value> | status=<value>',
|
|
180
|
+
'id^=<prefix> | path^=<prefix> | title~<text>',
|
|
181
|
+
'<field> in [<value>, ...] | <field> not in [<value>, ...]',
|
|
182
|
+
'<relation>:* | <relation>=<target-id>',
|
|
183
|
+
'any(<traversal>, <term> and <term>)',
|
|
184
|
+
'none(<traversal>, <term> and <term>)',
|
|
185
|
+
'count(<traversal>, <term> and <term>) <comparison> <number>',
|
|
186
|
+
],
|
|
187
|
+
usage_lines: [
|
|
188
|
+
'patram query <name> [options]',
|
|
189
|
+
'patram query --where "<clause>" [options]',
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
queries: {
|
|
193
|
+
allowed_option_names: new Set(),
|
|
194
|
+
examples: ['patram queries'],
|
|
195
|
+
extra_positionals_message: 'Queries does not accept positional arguments.',
|
|
196
|
+
help_topics: [],
|
|
197
|
+
max_positionals: 0,
|
|
198
|
+
min_positionals: 0,
|
|
199
|
+
missing_argument_examples: [],
|
|
200
|
+
missing_argument_label: null,
|
|
201
|
+
missing_usage_lines: ['patram queries'],
|
|
202
|
+
option_column_width: 10,
|
|
203
|
+
options: [
|
|
204
|
+
{
|
|
205
|
+
description: 'Print plain text output',
|
|
206
|
+
label: '--plain',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
description: 'Print JSON output',
|
|
210
|
+
label: '--json',
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
related: ['query'],
|
|
214
|
+
root_summary: 'List stored queries',
|
|
215
|
+
summary: 'List the stored queries defined in the project configuration.',
|
|
216
|
+
usage_lines: ['patram queries [options]'],
|
|
217
|
+
},
|
|
218
|
+
show: {
|
|
219
|
+
allowed_option_names: new Set(),
|
|
220
|
+
examples: ['patram show docs/patram.md', 'patram show lib/patram-cli.js'],
|
|
221
|
+
extra_positionals_message: 'Show accepts exactly one file path.',
|
|
222
|
+
help_topics: [],
|
|
223
|
+
max_positionals: 1,
|
|
224
|
+
min_positionals: 1,
|
|
225
|
+
missing_argument_examples: [
|
|
226
|
+
'patram show docs/patram.md',
|
|
227
|
+
'patram show lib/patram-cli.js',
|
|
228
|
+
],
|
|
229
|
+
missing_argument_label: '<file>',
|
|
230
|
+
missing_usage_lines: ['patram show <file>'],
|
|
231
|
+
option_column_width: 10,
|
|
232
|
+
options: [
|
|
233
|
+
{
|
|
234
|
+
description: 'Print plain text output',
|
|
235
|
+
label: '--plain',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
description: 'Print JSON output',
|
|
239
|
+
label: '--json',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
related: ['query', 'check'],
|
|
243
|
+
root_summary: 'Print a file with resolved links',
|
|
244
|
+
summary:
|
|
245
|
+
'Print one source file with indexed links resolved against the graph.',
|
|
246
|
+
usage_lines: ['patram show <file> [options]'],
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** @type {Record<CliHelpTopicName, CliHelpTopicDefinition>} */
|
|
251
|
+
const HELP_TOPIC_DEFINITIONS = {
|
|
252
|
+
'query-language': {
|
|
253
|
+
examples: [
|
|
254
|
+
'kind=decision and status=accepted',
|
|
255
|
+
'path^=docs/plans/',
|
|
256
|
+
'title~query',
|
|
257
|
+
'tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md',
|
|
258
|
+
'implements_command=command:query',
|
|
259
|
+
'status not in [done, dropped, superseded]',
|
|
260
|
+
'any(in:tracked_in, kind=task and status in [pending, ready, in_progress, blocked])',
|
|
261
|
+
'none(in:tracked_in, kind=decision)',
|
|
262
|
+
'count(in:decided_by, kind=task) = 0',
|
|
263
|
+
'not uses_term=term:graph',
|
|
264
|
+
],
|
|
265
|
+
lead: 'Query language filters graph nodes with field, relation, traversal, and aggregate terms.',
|
|
266
|
+
operators: [
|
|
267
|
+
{
|
|
268
|
+
description: 'Exact field match or exact count comparison',
|
|
269
|
+
label: '=',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
description: 'Prefix match for id and path',
|
|
273
|
+
label: '^=',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
description: 'Contains text for title',
|
|
277
|
+
label: '~',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
description: 'Set membership for supported fields',
|
|
281
|
+
label: 'in',
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
description: 'Set exclusion for supported fields',
|
|
285
|
+
label: 'not in',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
description: 'Negate one term',
|
|
289
|
+
label: 'not',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
description: 'Combine terms',
|
|
293
|
+
label: 'and',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
description: 'Count comparisons',
|
|
297
|
+
label: '!= < > >= <=',
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
relation_terms: [
|
|
301
|
+
{
|
|
302
|
+
description: 'Match nodes with at least one outgoing relation',
|
|
303
|
+
label: '<relation>:*',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
description: 'Match nodes linked to an exact target id',
|
|
307
|
+
label: '<relation>=<target-id>',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
description: 'Traverse one incoming relation hop',
|
|
311
|
+
label: 'in:<relation>',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
description: 'Traverse one outgoing relation hop',
|
|
315
|
+
label: 'out:<relation>',
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
terms: [
|
|
319
|
+
'Exact match: id, kind, path, status',
|
|
320
|
+
'Prefix match: id, path',
|
|
321
|
+
'Contains text: title',
|
|
322
|
+
'Set membership: id, kind, path, status, title',
|
|
323
|
+
],
|
|
324
|
+
usage_lines: [
|
|
325
|
+
'<field>=<value>',
|
|
326
|
+
'id^=<prefix>',
|
|
327
|
+
'path^=<prefix>',
|
|
328
|
+
'title~<text>',
|
|
329
|
+
'<field> in [<value>, ...]',
|
|
330
|
+
'<field> not in [<value>, ...]',
|
|
331
|
+
'<relation>:*',
|
|
332
|
+
'<relation>=<target-id>',
|
|
333
|
+
'any(<traversal>, <term> and <term>)',
|
|
334
|
+
'none(<traversal>, <term> and <term>)',
|
|
335
|
+
'count(<traversal>, <term> and <term>) <comparison> <number>',
|
|
336
|
+
'not <term>',
|
|
337
|
+
'<term> and <term>',
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @returns {{ global_options: string[], summary: string, usage_lines: string[] }}
|
|
344
|
+
*/
|
|
345
|
+
export function getRootHelpDefinition() {
|
|
346
|
+
return {
|
|
347
|
+
global_options: ROOT_HELP_GLOBAL_OPTIONS,
|
|
348
|
+
summary: ROOT_HELP_SUMMARY,
|
|
349
|
+
usage_lines: ROOT_HELP_USAGE_LINES,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {CliCommandName} command_name
|
|
355
|
+
* @returns {CliCommandDefinition}
|
|
356
|
+
*/
|
|
357
|
+
export function getCommandDefinition(command_name) {
|
|
358
|
+
return COMMAND_DEFINITIONS[command_name];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @param {CliHelpTopicName} help_topic_name
|
|
363
|
+
* @returns {CliHelpTopicDefinition}
|
|
364
|
+
*/
|
|
365
|
+
export function getHelpTopicDefinition(help_topic_name) {
|
|
366
|
+
return HELP_TOPIC_DEFINITIONS[help_topic_name];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param {string | undefined} command_name
|
|
371
|
+
* @returns {command_name is CliCommandName}
|
|
372
|
+
*/
|
|
373
|
+
export function isCommandName(command_name) {
|
|
374
|
+
return COMMAND_NAMES.includes(
|
|
375
|
+
/** @type {CliCommandName} */ (command_name ?? ''),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @param {string | undefined} help_topic_name
|
|
381
|
+
* @returns {help_topic_name is CliHelpTopicName}
|
|
382
|
+
*/
|
|
383
|
+
export function isHelpTopicName(help_topic_name) {
|
|
384
|
+
return HELP_TOPIC_NAMES.includes(
|
|
385
|
+
/** @type {CliHelpTopicName} */ (help_topic_name ?? ''),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {string} input_text
|
|
391
|
+
* @returns {CliCommandName | undefined}
|
|
392
|
+
*/
|
|
393
|
+
export function findCommandSuggestion(input_text) {
|
|
394
|
+
return /** @type {CliCommandName | undefined} */ (
|
|
395
|
+
findCloseMatch(input_text, COMMAND_NAMES)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @param {string} input_text
|
|
401
|
+
* @returns {CliCommandName | CliHelpTopicName | undefined}
|
|
402
|
+
*/
|
|
403
|
+
export function findHelpTargetSuggestion(input_text) {
|
|
404
|
+
return /** @type {CliCommandName | CliHelpTopicName | undefined} */ (
|
|
405
|
+
findCloseMatch(input_text, [...COMMAND_NAMES, ...HELP_TOPIC_NAMES])
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {string} input_text
|
|
411
|
+
* @param {CliCommandName | undefined} command_name
|
|
412
|
+
* @returns {string | undefined}
|
|
413
|
+
*/
|
|
414
|
+
export function findOptionSuggestion(input_text, command_name) {
|
|
415
|
+
const candidates = listOptionLabels(command_name);
|
|
416
|
+
|
|
417
|
+
return findCloseMatch(input_text, candidates);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @returns {CliCommandName[]}
|
|
422
|
+
*/
|
|
423
|
+
export function listCommandNames() {
|
|
424
|
+
return [...COMMAND_NAMES];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @returns {CliHelpTopicName[]}
|
|
429
|
+
*/
|
|
430
|
+
export function listHelpTopicNames() {
|
|
431
|
+
return [...HELP_TOPIC_NAMES];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @param {CliCommandName | undefined} command_name
|
|
436
|
+
* @returns {string[]}
|
|
437
|
+
*/
|
|
438
|
+
function listOptionLabels(command_name) {
|
|
439
|
+
const option_labels = new Set(
|
|
440
|
+
[...GLOBAL_OPTION_NAMES].map((option_name) => `--${option_name}`),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (command_name) {
|
|
444
|
+
for (const option_name of getCommandDefinition(command_name)
|
|
445
|
+
.allowed_option_names) {
|
|
446
|
+
option_labels.add(`--${option_name}`);
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
option_labels.add('--limit');
|
|
450
|
+
option_labels.add('--offset');
|
|
451
|
+
option_labels.add('--where');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return [...option_labels];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* @param {string} input_text
|
|
459
|
+
* @param {readonly string[]} candidates
|
|
460
|
+
* @returns {string | undefined}
|
|
461
|
+
*/
|
|
462
|
+
function findCloseMatch(input_text, candidates) {
|
|
463
|
+
let best_candidate;
|
|
464
|
+
let best_score = 0;
|
|
465
|
+
|
|
466
|
+
for (const candidate of candidates) {
|
|
467
|
+
const score = scoreCandidate(input_text, candidate);
|
|
468
|
+
|
|
469
|
+
if (score > best_score) {
|
|
470
|
+
best_candidate = candidate;
|
|
471
|
+
best_score = score;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (best_score < 0.6) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return best_candidate;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @param {string} input_text
|
|
484
|
+
* @param {string} candidate
|
|
485
|
+
* @returns {number}
|
|
486
|
+
*/
|
|
487
|
+
function scoreCandidate(input_text, candidate) {
|
|
488
|
+
const max_length = Math.max(input_text.length, candidate.length);
|
|
489
|
+
|
|
490
|
+
if (max_length === 0) {
|
|
491
|
+
return 1;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
1 - calculateDamerauLevenshteinDistance(input_text, candidate) / max_length
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @param {string} left_text
|
|
501
|
+
* @param {string} right_text
|
|
502
|
+
* @returns {number}
|
|
503
|
+
*/
|
|
504
|
+
function calculateDamerauLevenshteinDistance(left_text, right_text) {
|
|
505
|
+
/** @type {number[][]} */
|
|
506
|
+
const matrix = Array.from({ length: left_text.length + 1 }, () =>
|
|
507
|
+
Array(right_text.length + 1).fill(0),
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
for (let left_index = 0; left_index <= left_text.length; left_index += 1) {
|
|
511
|
+
matrix[left_index][0] = left_index;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (
|
|
515
|
+
let right_index = 0;
|
|
516
|
+
right_index <= right_text.length;
|
|
517
|
+
right_index += 1
|
|
518
|
+
) {
|
|
519
|
+
matrix[0][right_index] = right_index;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (let left_index = 1; left_index <= left_text.length; left_index += 1) {
|
|
523
|
+
for (
|
|
524
|
+
let right_index = 1;
|
|
525
|
+
right_index <= right_text.length;
|
|
526
|
+
right_index += 1
|
|
527
|
+
) {
|
|
528
|
+
const substitution_cost =
|
|
529
|
+
left_text[left_index - 1] === right_text[right_index - 1] ? 0 : 1;
|
|
530
|
+
|
|
531
|
+
matrix[left_index][right_index] = Math.min(
|
|
532
|
+
matrix[left_index - 1][right_index] + 1,
|
|
533
|
+
matrix[left_index][right_index - 1] + 1,
|
|
534
|
+
matrix[left_index - 1][right_index - 1] + substitution_cost,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (
|
|
538
|
+
left_index > 1 &&
|
|
539
|
+
right_index > 1 &&
|
|
540
|
+
left_text[left_index - 1] === right_text[right_index - 2] &&
|
|
541
|
+
left_text[left_index - 2] === right_text[right_index - 1]
|
|
542
|
+
) {
|
|
543
|
+
matrix[left_index][right_index] = Math.min(
|
|
544
|
+
matrix[left_index][right_index],
|
|
545
|
+
matrix[left_index - 2][right_index - 2] + 1,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return matrix[left_text.length][right_text.length];
|
|
552
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
|
|
3
|
+
* @import { OutputView } from './output-view.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
|
|
8
|
+
import { renderOutputView } from './render-output-view.js';
|
|
9
|
+
import { resolveOutputMode } from './resolve-output-mode.js';
|
|
10
|
+
import { writePagedOutput } from './write-paged-output.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TTY and pager output control.
|
|
14
|
+
*
|
|
15
|
+
* Resolves the final output mode and switches between direct stdout writes and
|
|
16
|
+
* interactive pager output.
|
|
17
|
+
*
|
|
18
|
+
* Kind: output
|
|
19
|
+
* Status: active
|
|
20
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
21
|
+
* Decided by: ../docs/decisions/tty-pager-output.md
|
|
22
|
+
* @patram
|
|
23
|
+
* @see {@link ./render-output-view.js}
|
|
24
|
+
* @see {@link ../docs/decisions/tty-pager-output.md}
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{ stdout: { isTTY?: boolean, write(chunk: string): boolean }, write_paged_output?: (output_text: string) => Promise<void> }} io_context
|
|
29
|
+
* @param {ParsedCliArguments} parsed_command
|
|
30
|
+
* @param {OutputView} output_view
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
export async function writeCommandOutput(
|
|
34
|
+
io_context,
|
|
35
|
+
parsed_command,
|
|
36
|
+
output_view,
|
|
37
|
+
) {
|
|
38
|
+
const rendered_output = await renderOutputView(
|
|
39
|
+
output_view,
|
|
40
|
+
resolveOutputMode(parsed_command, {
|
|
41
|
+
is_tty: io_context.stdout.isTTY === true,
|
|
42
|
+
no_color: process.env.NO_COLOR !== undefined,
|
|
43
|
+
term: process.env.TERM,
|
|
44
|
+
}),
|
|
45
|
+
parsed_command,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (shouldPageCommandOutput(parsed_command, io_context.stdout)) {
|
|
49
|
+
await writeInteractiveOutput(io_context, rendered_output);
|
|
50
|
+
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
io_context.stdout.write(rendered_output);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {ParsedCliArguments} parsed_command
|
|
59
|
+
* @param {{ isTTY?: boolean }} output_stream
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
export function shouldPageCommandOutput(parsed_command, output_stream) {
|
|
63
|
+
return (
|
|
64
|
+
output_stream.isTTY === true &&
|
|
65
|
+
(parsed_command.command_name === 'query' ||
|
|
66
|
+
parsed_command.command_name === 'show')
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {{ write_paged_output?: (output_text: string) => Promise<void> }} io_context
|
|
72
|
+
* @param {string} rendered_output
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async function writeInteractiveOutput(io_context, rendered_output) {
|
|
76
|
+
if (io_context.write_paged_output) {
|
|
77
|
+
await io_context.write_paged_output(rendered_output);
|
|
78
|
+
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await writePagedOutput(rendered_output);
|
|
83
|
+
}
|