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.
Files changed (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. 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
+ }