@canonical/pragma-cli 0.18.0 → 0.21.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/LICENSE +166 -0
- package/dist/pragma +0 -0
- package/package.json +31 -18
- package/src/constants.ts +1 -1
- package/src/domains/block/blockConfig.ts +47 -3
- package/src/domains/block/commands/list.test.ts +1 -1
- package/src/domains/block/commands/list.ts +27 -0
- package/src/domains/block/commands/lookup.ts +26 -52
- package/src/domains/block/formatters/index.ts +1 -1
- package/src/domains/block/formatters/list.test.ts +1 -1
- package/src/domains/block/formatters/list.ts +28 -42
- package/src/domains/block/formatters/lookup.test.ts +3 -2
- package/src/domains/block/formatters/lookup.ts +91 -296
- package/src/domains/block/formatters/types.ts +1 -1
- package/src/domains/block/operations/list.test.ts +1 -1
- package/src/domains/block/operations/list.ts +1 -1
- package/src/domains/block/operations/lookup.test.ts +29 -8
- package/src/domains/block/operations/lookup.ts +94 -16
- package/src/domains/block/orchestration/blockEmptyError.ts +1 -1
- package/src/domains/block/orchestration/buildBlockFilters.ts +1 -1
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -2
- package/src/domains/block/orchestration/resolveBlockList.ts +1 -1
- package/src/domains/block/orchestration/resolveBlockLookup.ts +8 -2
- package/src/domains/block/types.ts +1 -1
- package/src/domains/config/commands/channel.ts +6 -4
- package/src/domains/config/commands/tier.ts +6 -4
- package/src/domains/config/mcp/specs.ts +18 -6
- package/src/domains/config/operations/validateTier.ts +1 -1
- package/src/domains/config/tasks/index.ts +3 -0
- package/src/domains/config/tasks/setChannel.ts +30 -0
- package/src/domains/config/tasks/setTier.ts +33 -0
- package/src/domains/create/commands/package.test.ts +2 -2
- package/src/domains/doctor/mcp/index.ts +1 -1
- package/src/domains/doctor/mcp/specs.ts +2 -49
- package/src/domains/graph/commands/inspect.test.ts +1 -1
- package/src/domains/graph/formatters/inspect.ts +12 -7
- package/src/domains/graph/operations/inspectUri.ts +4 -1
- package/src/domains/info/commands/upgrade.ts +6 -3
- package/src/domains/info/index.ts +3 -1
- package/src/domains/info/mcp/index.ts +2 -0
- package/src/domains/info/mcp/specs.ts +65 -0
- package/src/domains/llm/data/commandReference.ts +4 -4
- package/src/domains/llm/data/decisionTrees.ts +14 -12
- package/src/domains/llm/operations/collectContext.ts +1 -1
- package/src/domains/modifier/commands/list.ts +9 -0
- package/src/domains/modifier/commands/lookup.ts +22 -42
- package/src/domains/modifier/formatters/index.ts +4 -1
- package/src/domains/modifier/formatters/list.test.ts +1 -1
- package/src/domains/modifier/formatters/list.ts +13 -26
- package/src/domains/modifier/formatters/lookup.test.ts +1 -1
- package/src/domains/modifier/formatters/lookup.ts +41 -29
- package/src/domains/modifier/index.ts +1 -0
- package/src/domains/modifier/modifierConfig.ts +36 -0
- package/src/domains/modifier/operations/list.ts +1 -1
- package/src/domains/modifier/operations/lookup.ts +4 -3
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +1 -1
- package/src/domains/modifier/types.ts +1 -1
- package/src/domains/ontology/commands/list.test.ts +1 -1
- package/src/domains/ontology/commands/list.ts +18 -0
- package/src/domains/ontology/commands/show.test.ts +1 -1
- package/src/domains/ontology/formatters/list.ts +5 -3
- package/src/domains/ontology/formatters/show.ts +38 -13
- package/src/domains/ontology/helpers/queryClasses.ts +1 -1
- package/src/domains/ontology/helpers/queryProperties.ts +1 -1
- package/src/domains/ontology/operations/listOntologies.ts +1 -1
- package/src/domains/ontology/operations/showOntology.ts +1 -1
- package/src/domains/setup/helpers/completionScripts.test.ts +3 -3
- package/src/domains/setup/helpers/completionScripts.ts +4 -4
- package/src/domains/shared/ToolSpec.ts +1 -1
- package/src/domains/shared/context.ts +3 -2
- package/src/domains/shared/contracts.ts +40 -1
- package/src/domains/shared/filters/buildFilterConfig.ts +1 -2
- package/src/domains/shared/filters/buildFilters.ts +1 -1
- package/src/domains/shared/filters/describeFilters.ts +1 -1
- package/src/domains/shared/formatters.ts +47 -0
- package/src/domains/shared/renderers.ts +410 -0
- package/src/domains/shared/runtime.ts +7 -22
- package/src/domains/shared/types/block.ts +107 -0
- package/src/domains/shared/types/common.ts +33 -0
- package/src/domains/shared/types/graph.ts +17 -0
- package/src/domains/shared/types/index.ts +49 -0
- package/src/domains/shared/types/modifier.ts +22 -0
- package/src/domains/shared/types/ontology.ts +50 -0
- package/src/domains/shared/types/runtime.ts +21 -0
- package/src/domains/shared/types/standard.ts +64 -0
- package/src/domains/shared/types/tier.ts +15 -0
- package/src/domains/shared/types/token.ts +30 -0
- package/src/domains/standard/commands/list.ts +9 -0
- package/src/domains/standard/commands/lookup.ts +22 -50
- package/src/domains/standard/formatters/categories.test.ts +1 -1
- package/src/domains/standard/formatters/categories.ts +1 -1
- package/src/domains/standard/formatters/index.ts +4 -1
- package/src/domains/standard/formatters/list.test.ts +4 -1
- package/src/domains/standard/formatters/list.ts +112 -84
- package/src/domains/standard/formatters/lookup.test.ts +1 -1
- package/src/domains/standard/formatters/lookup.ts +85 -70
- package/src/domains/standard/formatters/types.ts +1 -1
- package/src/domains/standard/index.ts +1 -0
- package/src/domains/standard/operations/categories.ts +1 -1
- package/src/domains/standard/operations/list.ts +1 -1
- package/src/domains/standard/operations/lookup.ts +1 -1
- package/src/domains/standard/orchestration/buildStandardFilters.ts +1 -1
- package/src/domains/standard/orchestration/resolveStandardList.ts +4 -1
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +1 -1
- package/src/domains/standard/orchestration/standardEmptyError.ts +1 -1
- package/src/domains/standard/standardConfig.ts +42 -0
- package/src/domains/standard/types.ts +1 -1
- package/src/domains/tier/commands/list.ts +16 -0
- package/src/domains/tier/formatters/list.test.ts +1 -1
- package/src/domains/tier/formatters/list.ts +11 -6
- package/src/domains/tier/operations/list.ts +1 -1
- package/src/domains/token/commands/list.ts +9 -0
- package/src/domains/token/commands/lookup.ts +22 -48
- package/src/domains/token/formatters/index.ts +4 -1
- package/src/domains/token/formatters/list.test.ts +1 -1
- package/src/domains/token/formatters/list.ts +15 -28
- package/src/domains/token/formatters/lookup.test.ts +2 -2
- package/src/domains/token/formatters/lookup.ts +80 -45
- package/src/domains/token/index.ts +1 -0
- package/src/domains/token/operations/list.ts +1 -1
- package/src/domains/token/operations/lookup.ts +1 -1
- package/src/domains/token/orchestration/resolveTokenLookup.ts +1 -1
- package/src/domains/token/tokenConfig.ts +38 -0
- package/src/domains/token/types.ts +1 -1
- package/src/index.ts +1 -1
- package/src/mcp/runMcpServer.test.ts +2 -2
- package/src/mcp/tools/index.ts +2 -0
- package/src/mcp/tools/registerTools.test.ts +8 -3
- package/src/package-manager/types.ts +1 -3
- package/src/pipeline/createProgram.ts +20 -1
- package/src/pipeline/index.ts +4 -1
- package/src/pipeline/parseGlobalFlags.test.ts +92 -0
- package/src/pipeline/parseGlobalFlags.ts +25 -0
- package/src/pipeline/runCli.orchestration.test.ts +1 -0
- package/src/pipeline/runCli.ts +11 -4
- package/src/pipeline/runInteractiveCommand.ts +198 -0
- package/src/testing/cli.ts +2 -2
- package/src/testing/fixtures.ts +2 -2
- package/src/testing/integration/boot.test.ts +2 -2
- package/src/testing/integration/cli-user-stories.test.ts +198 -0
- package/src/testing/integration/edge-cases.test.ts +263 -0
- package/src/testing/integration/parity.test.ts +377 -27
- package/src/testing/store.ts +1 -1
- package/src/tui/components/BulletList/BulletList.tsx +46 -0
- package/src/tui/components/BulletList/index.ts +2 -0
- package/src/tui/components/BulletList/types.ts +6 -0
- package/src/tui/components/Card/Card.tsx +73 -0
- package/src/tui/components/Card/index.ts +1 -0
- package/src/tui/components/Card/types.ts +12 -0
- package/src/tui/components/CodeBlock/CodeBlock.tsx +37 -0
- package/src/tui/components/CodeBlock/index.ts +2 -0
- package/src/tui/components/CodeBlock/types.ts +6 -0
- package/src/tui/components/DataRow/DataRow.tsx +80 -0
- package/src/tui/components/DataRow/index.ts +2 -0
- package/src/tui/components/DataRow/types.ts +14 -0
- package/src/tui/components/FieldRow/FieldRow.tsx +24 -0
- package/src/tui/components/FieldRow/index.ts +2 -0
- package/src/tui/components/FieldRow/types.ts +10 -0
- package/src/tui/components/HeaderRow/HeaderRow.tsx +32 -0
- package/src/tui/components/HeaderRow/index.ts +2 -0
- package/src/tui/components/HeaderRow/types.ts +8 -0
- package/src/tui/components/MarkdownText/MarkdownText.tsx +19 -0
- package/src/tui/components/MarkdownText/index.ts +2 -0
- package/src/tui/components/MarkdownText/types.ts +4 -0
- package/src/tui/components/MiniTable/MiniTable.tsx +82 -0
- package/src/tui/components/MiniTable/index.ts +2 -0
- package/src/tui/components/MiniTable/types.ts +6 -0
- package/src/tui/components/SectionRenderer/SectionRenderer.tsx +91 -0
- package/src/tui/components/SectionRenderer/index.ts +2 -0
- package/src/tui/components/SectionRenderer/types.ts +15 -0
- package/src/tui/components/TreeView/TreeView.tsx +84 -0
- package/src/tui/components/TreeView/index.ts +2 -0
- package/src/tui/components/TreeView/types.ts +6 -0
- package/src/tui/components/index.ts +19 -0
- package/src/tui/constants.ts +39 -0
- package/src/tui/createListView.ts +23 -0
- package/src/tui/createLookupView.ts +26 -0
- package/src/tui/helpers/computeWidths.ts +53 -0
- package/src/tui/helpers/fitColumns.ts +33 -0
- package/src/tui/helpers/formatMarkdown.ts +19 -0
- package/src/tui/helpers/getTerminalSize.ts +16 -0
- package/src/tui/helpers/index.ts +6 -0
- package/src/tui/helpers/syntaxColor.ts +67 -0
- package/src/tui/helpers/truncateText.ts +12 -0
- package/src/tui/hooks/index.ts +3 -0
- package/src/tui/hooks/types.ts +7 -0
- package/src/tui/hooks/useAutoExit.ts +17 -0
- package/src/tui/hooks/useTerminalSize.ts +19 -0
- package/src/tui/index.ts +15 -0
- package/src/tui/renderInk.tsx +18 -0
- package/src/tui/types.ts +12 -0
- package/src/tui/views/ListView/ListView.tsx +104 -0
- package/src/tui/views/ListView/index.ts +2 -0
- package/src/tui/views/ListView/types.ts +14 -0
- package/src/tui/views/LookupView/LookupView.tsx +244 -0
- package/src/tui/views/LookupView/index.ts +2 -0
- package/src/tui/views/LookupView/types.ts +16 -0
- package/src/tui/views/index.ts +4 -0
- package/src/domains/shared/types.test-d.ts +0 -127
- package/src/domains/shared/types.ts +0 -348
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ColumnDef } from "../../domains/shared/contracts.js";
|
|
2
|
+
import { COL_GAP, MIN_COL_WIDTH } from "../constants.js";
|
|
3
|
+
|
|
4
|
+
const FIRST_COL_MIN_RATIO = 0.3;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compute proportional column widths based on content and terminal width.
|
|
8
|
+
*
|
|
9
|
+
* Measures the maximum content width for each column across all items,
|
|
10
|
+
* then distributes the available terminal width proportionally. The first
|
|
11
|
+
* column (typically Name) receives at least 30% of the available width
|
|
12
|
+
* to ensure readability.
|
|
13
|
+
*
|
|
14
|
+
* @param columns - Visible column definitions.
|
|
15
|
+
* @param items - Data items to measure content widths against.
|
|
16
|
+
* @param terminalWidth - Available width in characters.
|
|
17
|
+
* @returns Array of computed widths (one per column).
|
|
18
|
+
*/
|
|
19
|
+
export default function computeWidths<T>(
|
|
20
|
+
columns: readonly ColumnDef<T>[],
|
|
21
|
+
items: readonly T[],
|
|
22
|
+
terminalWidth: number,
|
|
23
|
+
): readonly number[] {
|
|
24
|
+
if (columns.length === 0) return [];
|
|
25
|
+
|
|
26
|
+
const totalGaps = (columns.length - 1) * COL_GAP;
|
|
27
|
+
const available = terminalWidth - totalGaps - 1;
|
|
28
|
+
|
|
29
|
+
const naturalWidths = columns.map((col) => {
|
|
30
|
+
const labelWidth = col.label.length;
|
|
31
|
+
const maxContent = items.reduce((max, item) => {
|
|
32
|
+
const raw = item[col.key];
|
|
33
|
+
const formatted = col.format ? col.format(raw) : String(raw ?? "");
|
|
34
|
+
return Math.max(max, formatted.length);
|
|
35
|
+
}, 0);
|
|
36
|
+
return Math.max(labelWidth, maxContent);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const totalNatural = naturalWidths.reduce((sum, w) => sum + w, 0);
|
|
40
|
+
|
|
41
|
+
if (totalNatural <= available) {
|
|
42
|
+
return naturalWidths;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const firstColMin = Math.floor(available * FIRST_COL_MIN_RATIO);
|
|
46
|
+
return naturalWidths.map((natural, i) => {
|
|
47
|
+
const ratio =
|
|
48
|
+
totalNatural > 0 ? natural / totalNatural : 1 / columns.length;
|
|
49
|
+
const proportional = Math.floor(available * ratio);
|
|
50
|
+
if (i === 0) return Math.max(proportional, firstColMin);
|
|
51
|
+
return Math.max(proportional, MIN_COL_WIDTH);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ColumnDef } from "../../domains/shared/contracts.js";
|
|
2
|
+
import { COL_GAP, MIN_COL_WIDTH } from "../constants.js";
|
|
3
|
+
|
|
4
|
+
const MIN_VISIBLE = 3;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine which columns are visible given the available terminal width.
|
|
8
|
+
*
|
|
9
|
+
* Columns are ordered by priority (first = highest). When the terminal
|
|
10
|
+
* is too narrow to fit all columns with minimum widths, columns are
|
|
11
|
+
* dropped right-to-left (lowest priority first). At least three columns
|
|
12
|
+
* are always retained, and the function never returns an empty array
|
|
13
|
+
* if the input is non-empty.
|
|
14
|
+
*
|
|
15
|
+
* @param columns - Ordered column definitions (priority order).
|
|
16
|
+
* @param terminalWidth - Available width in characters.
|
|
17
|
+
* @returns The subset of columns that fit within the terminal width.
|
|
18
|
+
*/
|
|
19
|
+
export default function fitColumns<T>(
|
|
20
|
+
columns: readonly ColumnDef<T>[],
|
|
21
|
+
terminalWidth: number,
|
|
22
|
+
): readonly ColumnDef<T>[] {
|
|
23
|
+
const visible = [...columns];
|
|
24
|
+
|
|
25
|
+
while (visible.length > MIN_VISIBLE) {
|
|
26
|
+
const requiredWidth =
|
|
27
|
+
visible.length * MIN_COL_WIDTH + (visible.length - 1) * COL_GAP;
|
|
28
|
+
if (requiredWidth <= terminalWidth) break;
|
|
29
|
+
visible.pop();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return visible;
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert minimal Markdown to ANSI-styled text.
|
|
5
|
+
*
|
|
6
|
+
* Handles only the subset of Markdown commonly found in ontology
|
|
7
|
+
* descriptions: bold, inline code, and bullet lists. No heading
|
|
8
|
+
* parsing, no link rendering, no block-level structures beyond
|
|
9
|
+
* paragraph separation.
|
|
10
|
+
*
|
|
11
|
+
* @param markdown - Raw Markdown text.
|
|
12
|
+
* @returns ANSI-styled string suitable for terminal output.
|
|
13
|
+
*/
|
|
14
|
+
export default function formatMarkdown(markdown: string): string {
|
|
15
|
+
return markdown
|
|
16
|
+
.replace(/\*\*(.+?)\*\*/g, (_, text: string) => chalk.bold(text))
|
|
17
|
+
.replace(/`(.+?)`/g, (_, text: string) => chalk.dim(text))
|
|
18
|
+
.replace(/^- /gm, "• ");
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the current terminal dimensions.
|
|
3
|
+
*
|
|
4
|
+
* Reads `process.stdout.columns` and `process.stdout.rows` directly.
|
|
5
|
+
* Falls back to 80x24 when dimensions are unavailable (e.g., piped
|
|
6
|
+
* output or non-TTY environments).
|
|
7
|
+
*/
|
|
8
|
+
export default function getTerminalSize(): {
|
|
9
|
+
columns: number;
|
|
10
|
+
rows: number;
|
|
11
|
+
} {
|
|
12
|
+
return {
|
|
13
|
+
columns: process.stdout.columns ?? 80,
|
|
14
|
+
rows: process.stdout.rows ?? 24,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as computeWidths } from "./computeWidths.js";
|
|
2
|
+
export { default as fitColumns } from "./fitColumns.js";
|
|
3
|
+
export { default as formatMarkdown } from "./formatMarkdown.js";
|
|
4
|
+
export { default as getTerminalSize } from "./getTerminalSize.js";
|
|
5
|
+
export { default as syntaxColor } from "./syntaxColor.js";
|
|
6
|
+
export { default as truncateText } from "./truncateText.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply lightweight syntax coloring to a code string.
|
|
5
|
+
*
|
|
6
|
+
* Performs keyword-level colorization for YAML and Turtle (TTL) content.
|
|
7
|
+
* This is not full syntax highlighting — just enough to make structure
|
|
8
|
+
* scannable in the terminal.
|
|
9
|
+
*
|
|
10
|
+
* - YAML keys: bold
|
|
11
|
+
* - YAML list markers (`-`): dim
|
|
12
|
+
* - TTL prefixes (`@prefix`): cyan
|
|
13
|
+
* - TTL predicates (prefixed names like `ds:name`): blue
|
|
14
|
+
* - TTL type declarations (`a ds:Component`): magenta
|
|
15
|
+
*
|
|
16
|
+
* @param code - The raw code string.
|
|
17
|
+
* @param language - Language identifier ("yaml", "ttl", or "text").
|
|
18
|
+
* @returns The string with ANSI color codes applied.
|
|
19
|
+
*/
|
|
20
|
+
export default function syntaxColor(code: string, language: string): string {
|
|
21
|
+
switch (language) {
|
|
22
|
+
case "yaml":
|
|
23
|
+
return colorizeYaml(code);
|
|
24
|
+
case "ttl":
|
|
25
|
+
return colorizeTurtle(code);
|
|
26
|
+
default:
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function colorizeYaml(code: string): string {
|
|
32
|
+
return code
|
|
33
|
+
.split("\n")
|
|
34
|
+
.map((line) => {
|
|
35
|
+
const listMatch = line.match(/^(\s*)(- )(.*)/);
|
|
36
|
+
if (listMatch) {
|
|
37
|
+
return `${listMatch[1]}${chalk.dim(listMatch[2])}${listMatch[3]}`;
|
|
38
|
+
}
|
|
39
|
+
const kvMatch = line.match(/^(\s*)([^:]+):(.*)/);
|
|
40
|
+
if (kvMatch) {
|
|
41
|
+
return `${kvMatch[1]}${chalk.bold(kvMatch[2])}:${kvMatch[3]}`;
|
|
42
|
+
}
|
|
43
|
+
return line;
|
|
44
|
+
})
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function colorizeTurtle(code: string): string {
|
|
49
|
+
return code
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map((line) => {
|
|
52
|
+
if (line.trimStart().startsWith("@prefix")) {
|
|
53
|
+
return chalk.cyan(line);
|
|
54
|
+
}
|
|
55
|
+
const typeMatch = line.match(/(\s+a\s+)(\S+)/);
|
|
56
|
+
if (typeMatch) {
|
|
57
|
+
return line.replace(
|
|
58
|
+
typeMatch[0],
|
|
59
|
+
`${typeMatch[1]}${chalk.magenta(typeMatch[2])}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return line.replace(/\b([a-z]+:[a-zA-Z.]+)/g, (match) =>
|
|
63
|
+
chalk.blue(match),
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncate a string to fit within a maximum width, appending an ellipsis
|
|
3
|
+
* when the string exceeds the limit.
|
|
4
|
+
*
|
|
5
|
+
* Returns the original string unchanged if it fits. Otherwise, trims to
|
|
6
|
+
* `maxWidth - 1` characters and appends `…`.
|
|
7
|
+
*/
|
|
8
|
+
export default function truncateText(text: string, maxWidth: number): string {
|
|
9
|
+
if (text.length <= maxWidth) return text;
|
|
10
|
+
if (maxWidth <= 1) return "…";
|
|
11
|
+
return `${text.slice(0, maxWidth - 1)}…`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Return type for the useTerminalSize hook. */
|
|
2
|
+
export interface UseTerminalSizeResult {
|
|
3
|
+
/** Terminal width in columns. Falls back to 80 if not detectable. */
|
|
4
|
+
readonly columns: number;
|
|
5
|
+
/** Terminal height in rows. Falls back to 24 if not detectable. */
|
|
6
|
+
readonly rows: number;
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useApp } from "ink";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Exit the Ink application after the initial render.
|
|
6
|
+
*
|
|
7
|
+
* All pragma TUI views are static — they render a single frame and
|
|
8
|
+
* exit. This hook calls `useApp().exit()` in a `useEffect`, which
|
|
9
|
+
* runs after the first paint. Ink then flushes the frame to stdout
|
|
10
|
+
* and resolves the `waitUntilExit()` promise in the renderInk bridge.
|
|
11
|
+
*/
|
|
12
|
+
export default function useAutoExit(): void {
|
|
13
|
+
const { exit } = useApp();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
exit();
|
|
16
|
+
}, [exit]);
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UseTerminalSizeResult } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return the current terminal dimensions.
|
|
5
|
+
*
|
|
6
|
+
* Reads `process.stdout.columns` and `process.stdout.rows` directly.
|
|
7
|
+
* Falls back to 80x24 when dimensions are unavailable (e.g., piped
|
|
8
|
+
* output, or Ink's renderToString mode where stdout is not attached).
|
|
9
|
+
*
|
|
10
|
+
* For renderToString usage, Ink passes the column width via its own
|
|
11
|
+
* options — this hook provides the value for components that need
|
|
12
|
+
* to compute layout independently.
|
|
13
|
+
*/
|
|
14
|
+
export default function useTerminalSize(): UseTerminalSizeResult {
|
|
15
|
+
return {
|
|
16
|
+
columns: process.stdout.columns ?? 80,
|
|
17
|
+
rows: process.stdout.rows ?? 24,
|
|
18
|
+
};
|
|
19
|
+
}
|
package/src/tui/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI rendering domain for the pragma CLI.
|
|
3
|
+
*
|
|
4
|
+
* Provides Ink-based React components for rich terminal output of list
|
|
5
|
+
* and lookup commands. Activated when stdout is an interactive TTY and
|
|
6
|
+
* no machine-readable format flag is set.
|
|
7
|
+
*
|
|
8
|
+
* @module tui
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { default as createListView } from "./createListView.js";
|
|
12
|
+
export { default as createLookupView } from "./createLookupView.js";
|
|
13
|
+
export { default as renderInk } from "./renderInk.js";
|
|
14
|
+
export type { ListViewProps, LookupViewProps } from "./views/index.js";
|
|
15
|
+
export { ListView, LookupView } from "./views/index.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write TUI-rendered output to stdout.
|
|
3
|
+
*
|
|
4
|
+
* The ink renderers in pragma produce chalk-styled strings directly
|
|
5
|
+
* (not React elements), so this function simply writes the string
|
|
6
|
+
* to stdout. The dynamic import boundary is preserved so that domain
|
|
7
|
+
* command files can reference the `#tui` barrel without pulling in
|
|
8
|
+
* React/Ink at parse time for non-TTY invocations.
|
|
9
|
+
*
|
|
10
|
+
* @param output - A chalk-styled string produced by a TUI view.
|
|
11
|
+
*
|
|
12
|
+
* @note Impure — writes to process.stdout.
|
|
13
|
+
*/
|
|
14
|
+
export default async function renderInk(output: unknown): Promise<void> {
|
|
15
|
+
if (typeof output === "string" && output.length > 0) {
|
|
16
|
+
process.stdout.write(`${output}\n`);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/tui/types.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A color pair for domain-aware TUI rendering.
|
|
3
|
+
*
|
|
4
|
+
* Maps chalk color method names — `classBg` for background colors
|
|
5
|
+
* on class-level labels, `instanceFg` for text colors on instance values.
|
|
6
|
+
*/
|
|
7
|
+
export interface DomainColorPair {
|
|
8
|
+
/** Chalk background color name for class-level labels. */
|
|
9
|
+
readonly classBg: string;
|
|
10
|
+
/** Chalk text color name for instance values. */
|
|
11
|
+
readonly instanceFg: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import compactUri from "../../../domains/shared/compactUri.js";
|
|
3
|
+
import { PREFIX_MAP } from "../../../domains/shared/prefixes.js";
|
|
4
|
+
import { COL_GAP, DOMAIN_COLORS } from "../../constants.js";
|
|
5
|
+
import {
|
|
6
|
+
computeWidths,
|
|
7
|
+
fitColumns,
|
|
8
|
+
getTerminalSize,
|
|
9
|
+
truncateText,
|
|
10
|
+
} from "../../helpers/index.js";
|
|
11
|
+
import type { ListViewProps } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A responsive table view for list commands.
|
|
15
|
+
*
|
|
16
|
+
* Renders all items of a domain (block, token, modifier, etc.) as a
|
|
17
|
+
* tabular display with column headers, proportional widths, and
|
|
18
|
+
* domain-aware coloring. Columns drop right-to-left when the terminal
|
|
19
|
+
* is too narrow. Output is built as a chalk-styled string inside a
|
|
20
|
+
* single `<Text>` element to avoid Ink's flex-column spacing.
|
|
21
|
+
*/
|
|
22
|
+
export default function ListView<T>({
|
|
23
|
+
heading,
|
|
24
|
+
domain,
|
|
25
|
+
items,
|
|
26
|
+
columns,
|
|
27
|
+
prefixes,
|
|
28
|
+
}: ListViewProps<T>) {
|
|
29
|
+
const { columns: termWidth } = getTerminalSize();
|
|
30
|
+
const prefixMap = prefixes ?? PREFIX_MAP;
|
|
31
|
+
const colors = DOMAIN_COLORS[domain];
|
|
32
|
+
const colorFn = resolveChalkColor(colors?.instanceFg);
|
|
33
|
+
|
|
34
|
+
if (items.length === 0) {
|
|
35
|
+
return `${chalk.bold(colorFn(`${heading} (0)`))}\n${chalk.dim("No results.")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const visibleColumns = fitColumns(columns, termWidth);
|
|
39
|
+
const widths = computeWidths(visibleColumns, items, termWidth);
|
|
40
|
+
const gap = " ".repeat(COL_GAP);
|
|
41
|
+
|
|
42
|
+
const headingLine = chalk.bold(colorFn(`${heading} (${items.length})`));
|
|
43
|
+
|
|
44
|
+
const headerLine = chalk.bold(
|
|
45
|
+
visibleColumns
|
|
46
|
+
.map((col, i) => {
|
|
47
|
+
const w = widths[i] ?? col.label.length;
|
|
48
|
+
return truncateText(col.label, w).padEnd(w);
|
|
49
|
+
})
|
|
50
|
+
.join(gap),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const totalWidth =
|
|
54
|
+
widths.reduce((sum, w) => sum + w, 0) + (widths.length - 1) * COL_GAP;
|
|
55
|
+
const separator = chalk.dim("─".repeat(totalWidth));
|
|
56
|
+
|
|
57
|
+
const dataLines = items.map((item) =>
|
|
58
|
+
visibleColumns
|
|
59
|
+
.map((col, i) => {
|
|
60
|
+
const w = widths[i] ?? 20;
|
|
61
|
+
const raw = (item as Record<string, unknown>)[col.key];
|
|
62
|
+
const formatted = formatCellValue(raw, col, prefixMap);
|
|
63
|
+
const padded = truncateText(formatted, w).padEnd(w);
|
|
64
|
+
|
|
65
|
+
if (col.key === "uri") return chalk.dim(padded);
|
|
66
|
+
if (col.key === "name") return colorFn(padded);
|
|
67
|
+
return padded;
|
|
68
|
+
})
|
|
69
|
+
.join(gap),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return [headingLine, headerLine, separator, ...dataLines].join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatCellValue(
|
|
76
|
+
raw: unknown,
|
|
77
|
+
col: { format?: (value: unknown) => string },
|
|
78
|
+
prefixes: Readonly<Record<string, string>>,
|
|
79
|
+
): string {
|
|
80
|
+
if (col.format) return col.format(raw);
|
|
81
|
+
if (raw === null || raw === undefined) return "";
|
|
82
|
+
if (typeof raw === "string" && looksLikeUri(raw))
|
|
83
|
+
return compactUri(raw, prefixes);
|
|
84
|
+
if (Array.isArray(raw)) return raw.map(String).join(", ");
|
|
85
|
+
return String(raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function looksLikeUri(value: string): boolean {
|
|
89
|
+
return (
|
|
90
|
+
value.startsWith("http://") ||
|
|
91
|
+
value.startsWith("https://") ||
|
|
92
|
+
/^[a-z][a-z0-9+.-]*:/i.test(value)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveChalkColor(
|
|
97
|
+
colorName: string | undefined,
|
|
98
|
+
): (text: string) => string {
|
|
99
|
+
if (!colorName) return (text) => text;
|
|
100
|
+
const fn = chalk[colorName as keyof typeof chalk];
|
|
101
|
+
return typeof fn === "function"
|
|
102
|
+
? (fn as (text: string) => string)
|
|
103
|
+
: (text) => text;
|
|
104
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ColumnDef } from "../../../domains/shared/contracts.js";
|
|
2
|
+
|
|
3
|
+
export interface ListViewProps<T> {
|
|
4
|
+
/** Heading text displayed above the table (e.g., "Blocks"). */
|
|
5
|
+
readonly heading: string;
|
|
6
|
+
/** Domain name — indexes into DOMAIN_COLORS for heading and row coloring. */
|
|
7
|
+
readonly domain: string;
|
|
8
|
+
/** Data items to render as table rows. */
|
|
9
|
+
readonly items: readonly T[];
|
|
10
|
+
/** Column definitions controlling which fields appear and how they format. */
|
|
11
|
+
readonly columns: readonly ColumnDef<T>[];
|
|
12
|
+
/** URI prefix map for compacting IRIs. */
|
|
13
|
+
readonly prefixes?: Readonly<Record<string, string>>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import compactUri from "../../../domains/shared/compactUri.js";
|
|
3
|
+
import { PREFIX_MAP } from "../../../domains/shared/prefixes.js";
|
|
4
|
+
import { buildCard } from "../../components/Card/index.js";
|
|
5
|
+
import { formatMarkdown, syntaxColor } from "../../helpers/index.js";
|
|
6
|
+
import type { LookupViewProps } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A stacked card view for lookup commands.
|
|
10
|
+
*
|
|
11
|
+
* Renders each lookup result as a bordered card with field rows
|
|
12
|
+
* and sections. Multiple results show a `[N of M]` badge in each
|
|
13
|
+
* card header. Errors are appended after all cards. Output is built
|
|
14
|
+
* as a chalk-styled string inside a single `<Text>` element.
|
|
15
|
+
*/
|
|
16
|
+
export default function LookupView<T>({
|
|
17
|
+
results,
|
|
18
|
+
errors,
|
|
19
|
+
options,
|
|
20
|
+
domain,
|
|
21
|
+
}: LookupViewProps<T>) {
|
|
22
|
+
const prefixes = options.prefixes ?? PREFIX_MAP;
|
|
23
|
+
const total = results.length;
|
|
24
|
+
const labelWidth = computeLabelWidth(options.fields);
|
|
25
|
+
const termWidth = process.stdout.columns ?? 80;
|
|
26
|
+
|
|
27
|
+
const cards = results.map((entity, i) => {
|
|
28
|
+
const bodyLines: string[] = [];
|
|
29
|
+
|
|
30
|
+
// 2 (left pad) + labelWidth + 2 (gap) = value column offset
|
|
31
|
+
const valueOffset = 2 + labelWidth + 2;
|
|
32
|
+
const valueIndent = " ".repeat(valueOffset);
|
|
33
|
+
const innerWidth = Math.max(termWidth - 4, 20);
|
|
34
|
+
const maxValueWidth = innerWidth - valueOffset;
|
|
35
|
+
|
|
36
|
+
for (const field of options.fields) {
|
|
37
|
+
const value = field.value(entity);
|
|
38
|
+
if (isEmpty(value)) continue;
|
|
39
|
+
const raw = formatInlineValue(value, prefixes);
|
|
40
|
+
const formatted = typeof value === "string" ? formatMarkdown(raw) : raw;
|
|
41
|
+
const label = chalk.bold(field.label.padEnd(labelWidth));
|
|
42
|
+
const wrapped = wrapText(formatted, maxValueWidth);
|
|
43
|
+
bodyLines.push(` ${label} ${wrapped[0]}`);
|
|
44
|
+
for (let l = 1; l < wrapped.length; l++) {
|
|
45
|
+
bodyLines.push(`${valueIndent}${wrapped[l]}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const section of options.sections) {
|
|
50
|
+
const value = entity[section.key];
|
|
51
|
+
if (!section.showWhenEmpty && isEmpty(value)) continue;
|
|
52
|
+
|
|
53
|
+
const sectionBody = formatSectionValue(value, section.kind, prefixes);
|
|
54
|
+
if (!sectionBody) continue;
|
|
55
|
+
|
|
56
|
+
bodyLines.push("");
|
|
57
|
+
bodyLines.push(` ${chalk.bold(section.heading)}`);
|
|
58
|
+
for (const line of sectionBody.split("\n")) {
|
|
59
|
+
bodyLines.push(` ${line}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const badge = total > 1 ? `[${i + 1} of ${total}]` : undefined;
|
|
64
|
+
return buildCard(
|
|
65
|
+
options.title(entity),
|
|
66
|
+
domain,
|
|
67
|
+
bodyLines,
|
|
68
|
+
termWidth,
|
|
69
|
+
badge,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const errorLines =
|
|
74
|
+
errors.length > 0
|
|
75
|
+
? [
|
|
76
|
+
"",
|
|
77
|
+
chalk.bold.red("Errors:"),
|
|
78
|
+
...errors.map((err) => chalk.red(` • ${err.query}: ${err.message}`)),
|
|
79
|
+
]
|
|
80
|
+
: [];
|
|
81
|
+
|
|
82
|
+
return [...cards, ...errorLines].join("\n\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function computeLabelWidth<T>(
|
|
86
|
+
fields: readonly { label: string; value: (entity: T) => unknown }[],
|
|
87
|
+
): number {
|
|
88
|
+
return fields.reduce((max, f) => Math.max(max, f.label.length), 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatInlineValue(
|
|
92
|
+
value: unknown,
|
|
93
|
+
prefixes: Readonly<Record<string, string>>,
|
|
94
|
+
): string {
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.map((entry) => formatInlineValue(entry, prefixes)).join(", ");
|
|
97
|
+
}
|
|
98
|
+
if (value && typeof value === "object") {
|
|
99
|
+
return Object.entries(value as Record<string, unknown>)
|
|
100
|
+
.filter(([, v]) => !isEmpty(v))
|
|
101
|
+
.map(([k, v]) => `${k}=${formatInlineValue(v, prefixes)}`)
|
|
102
|
+
.join("; ");
|
|
103
|
+
}
|
|
104
|
+
return formatScalar(value, prefixes);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatScalar(
|
|
108
|
+
value: unknown,
|
|
109
|
+
prefixes: Readonly<Record<string, string>>,
|
|
110
|
+
): string {
|
|
111
|
+
if (value === null || value === undefined) return "";
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
if (looksLikeUri(value)) return compactUri(value, prefixes);
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatSectionValue(
|
|
120
|
+
value: unknown,
|
|
121
|
+
kind: string,
|
|
122
|
+
prefixes: Readonly<Record<string, string>>,
|
|
123
|
+
): string | null {
|
|
124
|
+
switch (kind) {
|
|
125
|
+
case "field":
|
|
126
|
+
return typeof value === "string" && value.trim().length > 0
|
|
127
|
+
? formatMarkdown(value)
|
|
128
|
+
: null;
|
|
129
|
+
case "code":
|
|
130
|
+
return typeof value === "string" && value.trim().length > 0
|
|
131
|
+
? syntaxColor(value, "yaml")
|
|
132
|
+
: null;
|
|
133
|
+
case "list":
|
|
134
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
135
|
+
return value
|
|
136
|
+
.map((item) => `• ${formatScalar(item, prefixes)}`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
case "table":
|
|
139
|
+
case "nested-table":
|
|
140
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
141
|
+
return value
|
|
142
|
+
.map((entry) => {
|
|
143
|
+
if (typeof entry === "object" && entry !== null) {
|
|
144
|
+
return Object.entries(entry as Record<string, unknown>)
|
|
145
|
+
.filter(([, v]) => !isEmpty(v))
|
|
146
|
+
.map(([k, v]) => `${k}: ${formatScalar(v, prefixes)}`)
|
|
147
|
+
.join(" ");
|
|
148
|
+
}
|
|
149
|
+
return formatScalar(entry, prefixes);
|
|
150
|
+
})
|
|
151
|
+
.join("\n");
|
|
152
|
+
case "tree":
|
|
153
|
+
return formatTree(value, prefixes);
|
|
154
|
+
default:
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTree(
|
|
160
|
+
value: unknown,
|
|
161
|
+
prefixes: Readonly<Record<string, string>>,
|
|
162
|
+
depth = 0,
|
|
163
|
+
): string | null {
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
const lines = value
|
|
166
|
+
.map((entry) => formatTree(entry, prefixes, depth))
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!value || typeof value !== "object") {
|
|
172
|
+
return typeof value === "string" && value.length > 0
|
|
173
|
+
? `${" ".repeat(depth)}├─ ${formatScalar(value, prefixes)}`
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const node = value as {
|
|
178
|
+
root?: unknown;
|
|
179
|
+
name?: string;
|
|
180
|
+
uri?: string;
|
|
181
|
+
children?: unknown;
|
|
182
|
+
};
|
|
183
|
+
if (node.root) return formatTree(node.root, prefixes, depth);
|
|
184
|
+
|
|
185
|
+
const label =
|
|
186
|
+
node.name && node.name.length > 0
|
|
187
|
+
? node.name
|
|
188
|
+
: node.uri
|
|
189
|
+
? compactUri(node.uri, prefixes)
|
|
190
|
+
: "item";
|
|
191
|
+
|
|
192
|
+
const prefix = depth > 0 ? "├─ " : "";
|
|
193
|
+
const lines = [`${" ".repeat(depth)}${prefix}${label}`];
|
|
194
|
+
if (Array.isArray(node.children)) {
|
|
195
|
+
const childResult = formatTree(node.children, prefixes, depth + 1);
|
|
196
|
+
if (childResult) lines.push(childResult);
|
|
197
|
+
}
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isEmpty(value: unknown): boolean {
|
|
202
|
+
if (value === null || value === undefined) return true;
|
|
203
|
+
if (typeof value === "string") return value.trim().length === 0;
|
|
204
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function looksLikeUri(value: string): boolean {
|
|
209
|
+
return (
|
|
210
|
+
value.startsWith("http://") ||
|
|
211
|
+
value.startsWith("https://") ||
|
|
212
|
+
/^[a-z][a-z0-9+.-]*:/i.test(value)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Wrap text at word boundaries to fit within maxWidth.
|
|
218
|
+
* Splits on existing newlines first, then wraps each segment.
|
|
219
|
+
*/
|
|
220
|
+
function wrapText(text: string, maxWidth: number): string[] {
|
|
221
|
+
const segments = text.split("\n");
|
|
222
|
+
return segments.flatMap((segment) => wrapSegment(segment, maxWidth));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function wrapSegment(text: string, maxWidth: number): string[] {
|
|
226
|
+
if (text.length <= maxWidth) return [text];
|
|
227
|
+
|
|
228
|
+
const words = text.split(/\s+/);
|
|
229
|
+
const lines: string[] = [];
|
|
230
|
+
let current = "";
|
|
231
|
+
|
|
232
|
+
for (const word of words) {
|
|
233
|
+
const candidate = current.length === 0 ? word : `${current} ${word}`;
|
|
234
|
+
if (candidate.length > maxWidth && current.length > 0) {
|
|
235
|
+
lines.push(current);
|
|
236
|
+
current = word;
|
|
237
|
+
} else {
|
|
238
|
+
current = candidate;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (current.length > 0) lines.push(current);
|
|
242
|
+
|
|
243
|
+
return lines;
|
|
244
|
+
}
|