@kb-labs/shared 1.1.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/.cursorrules +32 -0
- package/.github/workflows/ci.yml +13 -0
- package/.github/workflows/deploy.yml +28 -0
- package/.github/workflows/docker-build.yml +25 -0
- package/.github/workflows/drift-check.yml +10 -0
- package/.github/workflows/profiles-validate.yml +16 -0
- package/.github/workflows/release.yml +8 -0
- package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
- package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
- package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
- package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
- package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
- package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
- package/.kb/devkit/agents/release-manager/context.globs +7 -0
- package/.kb/devkit/agents/release-manager/prompt.md +27 -0
- package/.kb/devkit/agents/release-manager/runbook.md +17 -0
- package/.kb/devkit/agents/test-generator/context.globs +7 -0
- package/.kb/devkit/agents/test-generator/prompt.md +27 -0
- package/.kb/devkit/agents/test-generator/runbook.md +18 -0
- package/.vscode/settings.json +23 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +117 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
- package/docs/DOCUMENTATION.md +70 -0
- package/docs/adr/0000-template.md +52 -0
- package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
- package/docs/adr/0002-plugins-and-extensibility.md +44 -0
- package/docs/adr/0003-package-and-module-boundaries.md +35 -0
- package/docs/adr/0004-versioning-and-release-policy.md +36 -0
- package/docs/adr/0005-reactive-loader-pattern.md +179 -0
- package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
- package/eslint.config.js +27 -0
- package/kb-labs.config.json +5 -0
- package/package.json +88 -0
- package/package.json.bin +25 -0
- package/package.json.lib +30 -0
- package/packages/shared-cli-ui/CHANGELOG.md +20 -0
- package/packages/shared-cli-ui/README.md +342 -0
- package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
- package/packages/shared-cli-ui/eslint.config.js +27 -0
- package/packages/shared-cli-ui/package.json +72 -0
- package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
- package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
- package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
- package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
- package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
- package/packages/shared-cli-ui/src/colors.ts +142 -0
- package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
- package/packages/shared-cli-ui/src/command-output.ts +153 -0
- package/packages/shared-cli-ui/src/command-result.ts +267 -0
- package/packages/shared-cli-ui/src/command-runner.ts +310 -0
- package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
- package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
- package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
- package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
- package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
- package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
- package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
- package/packages/shared-cli-ui/src/debug/index.ts +56 -0
- package/packages/shared-cli-ui/src/debug/types.ts +57 -0
- package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
- package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
- package/packages/shared-cli-ui/src/format.ts +412 -0
- package/packages/shared-cli-ui/src/index.ts +34 -0
- package/packages/shared-cli-ui/src/loader.ts +196 -0
- package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
- package/packages/shared-cli-ui/src/modern-format.ts +271 -0
- package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
- package/packages/shared-cli-ui/src/table.ts +134 -0
- package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
- package/packages/shared-cli-ui/src/utils/context.ts +12 -0
- package/packages/shared-cli-ui/src/utils/env.ts +164 -0
- package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
- package/packages/shared-cli-ui/src/utils/path.ts +8 -0
- package/packages/shared-cli-ui/tsconfig.build.json +15 -0
- package/packages/shared-cli-ui/tsconfig.json +9 -0
- package/packages/shared-cli-ui/tsup.config.ts +11 -0
- package/packages/shared-cli-ui/vitest.config.ts +15 -0
- package/packages/shared-command-kit/CHANGELOG.md +20 -0
- package/packages/shared-command-kit/LICENSE +22 -0
- package/packages/shared-command-kit/README.md +1030 -0
- package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
- package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
- package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
- package/packages/shared-command-kit/eslint.config.js +27 -0
- package/packages/shared-command-kit/eslint.config.ts +14 -0
- package/packages/shared-command-kit/package.json +76 -0
- package/packages/shared-command-kit/prettierrc.json +5 -0
- package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
- package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
- package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
- package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
- package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
- package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
- package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
- package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
- package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
- package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
- package/packages/shared-command-kit/src/analytics/index.ts +6 -0
- package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
- package/packages/shared-command-kit/src/define-action.ts +100 -0
- package/packages/shared-command-kit/src/define-command.ts +113 -0
- package/packages/shared-command-kit/src/define-route.ts +113 -0
- package/packages/shared-command-kit/src/define-system-command.ts +362 -0
- package/packages/shared-command-kit/src/define-webhook.ts +115 -0
- package/packages/shared-command-kit/src/define-websocket.ts +308 -0
- package/packages/shared-command-kit/src/errors/factory.ts +282 -0
- package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
- package/packages/shared-command-kit/src/errors/format.ts +92 -0
- package/packages/shared-command-kit/src/errors/index.ts +9 -0
- package/packages/shared-command-kit/src/errors/types.ts +32 -0
- package/packages/shared-command-kit/src/flags/define.ts +92 -0
- package/packages/shared-command-kit/src/flags/index.ts +9 -0
- package/packages/shared-command-kit/src/flags/types.ts +153 -0
- package/packages/shared-command-kit/src/flags/validate.ts +358 -0
- package/packages/shared-command-kit/src/helpers/context.ts +8 -0
- package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
- package/packages/shared-command-kit/src/helpers/index.ts +42 -0
- package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
- package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
- package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
- package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
- package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
- package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
- package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
- package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
- package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
- package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
- package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
- package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
- package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
- package/packages/shared-command-kit/src/index.ts +410 -0
- package/packages/shared-command-kit/src/jobs.ts +132 -0
- package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
- package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
- package/packages/shared-command-kit/src/manifest.ts +127 -0
- package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
- package/packages/shared-command-kit/src/rest/index.ts +11 -0
- package/packages/shared-command-kit/src/studio/index.ts +12 -0
- package/packages/shared-command-kit/src/validation/index.ts +6 -0
- package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
- package/packages/shared-command-kit/src/ws-types.ts +106 -0
- package/packages/shared-command-kit/tsconfig.build.json +15 -0
- package/packages/shared-command-kit/tsconfig.json +9 -0
- package/packages/shared-command-kit/tsup.config.ts +30 -0
- package/packages/shared-command-kit/vitest.config.ts +4 -0
- package/packages/shared-http/package.json +67 -0
- package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
- package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
- package/packages/shared-http/src/http-observability-collector.ts +363 -0
- package/packages/shared-http/src/index.ts +36 -0
- package/packages/shared-http/src/log-correlation.ts +89 -0
- package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
- package/packages/shared-http/src/register-openapi.ts +108 -0
- package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
- package/packages/shared-http/src/schemas.ts +29 -0
- package/packages/shared-http/src/service-observability.ts +63 -0
- package/packages/shared-http/tsconfig.build.json +15 -0
- package/packages/shared-http/tsconfig.json +9 -0
- package/packages/shared-http/tsup.config.ts +23 -0
- package/packages/shared-http/vitest.config.ts +13 -0
- package/packages/shared-perm-presets/CHANGELOG.md +20 -0
- package/packages/shared-perm-presets/README.md +78 -0
- package/packages/shared-perm-presets/eslint.config.js +27 -0
- package/packages/shared-perm-presets/package.json +45 -0
- package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
- package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
- package/packages/shared-perm-presets/src/combine.ts +278 -0
- package/packages/shared-perm-presets/src/index.ts +18 -0
- package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
- package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
- package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
- package/packages/shared-perm-presets/src/presets/index.ts +8 -0
- package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
- package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
- package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
- package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
- package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
- package/packages/shared-perm-presets/src/types.ts +192 -0
- package/packages/shared-perm-presets/tsconfig.build.json +15 -0
- package/packages/shared-perm-presets/tsconfig.json +9 -0
- package/packages/shared-perm-presets/tsup.config.ts +8 -0
- package/packages/shared-perm-presets/vitest.config.ts +9 -0
- package/packages/shared-testing/CHANGELOG.md +20 -0
- package/packages/shared-testing/README.md +430 -0
- package/packages/shared-testing/package.json +51 -0
- package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
- package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
- package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
- package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
- package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
- package/packages/shared-testing/src/create-test-context.ts +550 -0
- package/packages/shared-testing/src/index.ts +77 -0
- package/packages/shared-testing/src/mock-cache.ts +179 -0
- package/packages/shared-testing/src/mock-llm.ts +319 -0
- package/packages/shared-testing/src/mock-logger.ts +97 -0
- package/packages/shared-testing/src/mock-storage.ts +108 -0
- package/packages/shared-testing/src/setup-platform.ts +101 -0
- package/packages/shared-testing/src/test-command.ts +288 -0
- package/packages/shared-testing/tsconfig.build.json +15 -0
- package/packages/shared-testing/tsconfig.json +9 -0
- package/packages/shared-testing/tsup.config.ts +20 -0
- package/packages/shared-testing/vitest.config.ts +3 -0
- package/packages/shared-tool-kit/CHANGELOG.md +20 -0
- package/packages/shared-tool-kit/package.json +47 -0
- package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
- package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
- package/packages/shared-tool-kit/src/factory.ts +126 -0
- package/packages/shared-tool-kit/src/index.ts +32 -0
- package/packages/shared-tool-kit/src/testing/index.ts +84 -0
- package/packages/shared-tool-kit/tsconfig.build.json +15 -0
- package/packages/shared-tool-kit/tsconfig.json +9 -0
- package/packages/shared-tool-kit/tsup.config.ts +21 -0
- package/pnpm-workspace.yaml +11070 -0
- package/prettierrc.json +1 -0
- package/scripts/devkit-sync.mjs +37 -0
- package/scripts/hooks/post-push +9 -0
- package/scripts/hooks/pre-commit +9 -0
- package/scripts/hooks/pre-push +9 -0
- package/tsconfig.base.json +9 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +9 -0
- package/tsconfig.paths.json +50 -0
- package/tsconfig.tools.json +18 -0
- package/tsup.config.bin.ts +34 -0
- package/tsup.config.cli.ts +41 -0
- package/tsup.config.dual.ts +46 -0
- package/tsup.config.ts +36 -0
- package/tsup.external.json +104 -0
- package/vitest.config.ts +48 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI manifest parsing utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CommandManifest {
|
|
6
|
+
manifestVersion: string;
|
|
7
|
+
id: string;
|
|
8
|
+
aliases?: string[];
|
|
9
|
+
group: string;
|
|
10
|
+
describe: string;
|
|
11
|
+
longDescription?: string;
|
|
12
|
+
requires?: string[];
|
|
13
|
+
flags?: FlagDefinition[];
|
|
14
|
+
examples?: string[];
|
|
15
|
+
loader: () => Promise<{ run: any }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FlagDefinition {
|
|
19
|
+
name: string;
|
|
20
|
+
type: 'string' | 'boolean' | 'number' | 'array';
|
|
21
|
+
alias?: string;
|
|
22
|
+
default?: any;
|
|
23
|
+
description?: string;
|
|
24
|
+
choices?: string[];
|
|
25
|
+
required?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract command IDs from a manifest
|
|
30
|
+
*/
|
|
31
|
+
export function extractCommandIds(manifest: CommandManifest[]): string[] {
|
|
32
|
+
return manifest.map(cmd => cmd.id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract command groups from a manifest
|
|
37
|
+
*/
|
|
38
|
+
export function extractCommandGroups(manifest: CommandManifest[]): string[] {
|
|
39
|
+
const groups = new Set<string>();
|
|
40
|
+
manifest.forEach(cmd => groups.add(cmd.group));
|
|
41
|
+
return Array.from(groups);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find commands by group
|
|
46
|
+
*/
|
|
47
|
+
export function findCommandsByGroup(manifest: CommandManifest[], group: string): CommandManifest[] {
|
|
48
|
+
return manifest.filter(cmd => cmd.group === group);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find command by ID
|
|
53
|
+
*/
|
|
54
|
+
export function findCommandById(manifest: CommandManifest[], id: string): CommandManifest | undefined {
|
|
55
|
+
return manifest.find(cmd => cmd.id === id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get command info for suggestions
|
|
60
|
+
*/
|
|
61
|
+
export function getCommandInfo(manifest: CommandManifest[], commandId: string): {
|
|
62
|
+
id: string;
|
|
63
|
+
group: string;
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
available: boolean;
|
|
67
|
+
} | null {
|
|
68
|
+
const cmd = findCommandById(manifest, commandId);
|
|
69
|
+
if (!cmd) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [group, name] = commandId.split(':');
|
|
74
|
+
return {
|
|
75
|
+
id: commandId,
|
|
76
|
+
group: group || 'unknown',
|
|
77
|
+
name: name || commandId,
|
|
78
|
+
description: cmd.describe,
|
|
79
|
+
available: true
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate suggestions for a specific group
|
|
85
|
+
*/
|
|
86
|
+
export function generateGroupSuggestions(
|
|
87
|
+
manifest: CommandManifest[],
|
|
88
|
+
group: string,
|
|
89
|
+
_warningCodes: Set<string>,
|
|
90
|
+
_context: any
|
|
91
|
+
): Array<{
|
|
92
|
+
id: string;
|
|
93
|
+
command: string;
|
|
94
|
+
args: string[];
|
|
95
|
+
description: string;
|
|
96
|
+
impact: 'safe' | 'disruptive';
|
|
97
|
+
when: string;
|
|
98
|
+
available: boolean;
|
|
99
|
+
}> {
|
|
100
|
+
const groupCommands = findCommandsByGroup(manifest, group);
|
|
101
|
+
const suggestions: Array<{
|
|
102
|
+
id: string;
|
|
103
|
+
command: string;
|
|
104
|
+
args: string[];
|
|
105
|
+
description: string;
|
|
106
|
+
impact: 'safe' | 'disruptive';
|
|
107
|
+
when: string;
|
|
108
|
+
available: boolean;
|
|
109
|
+
}> = [];
|
|
110
|
+
|
|
111
|
+
// Generate common suggestions for each group
|
|
112
|
+
for (const cmd of groupCommands) {
|
|
113
|
+
// Basic command suggestion
|
|
114
|
+
suggestions.push({
|
|
115
|
+
id: `${cmd.group.toUpperCase()}_${cmd.id.split(':')[1]?.toUpperCase() || 'UNKNOWN'}`,
|
|
116
|
+
command: `kb ${cmd.group} ${cmd.id.split(':')[1] || ''}`,
|
|
117
|
+
args: [],
|
|
118
|
+
description: cmd.describe,
|
|
119
|
+
impact: 'safe',
|
|
120
|
+
when: 'GENERAL',
|
|
121
|
+
available: true
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Add specific suggestions based on command type
|
|
125
|
+
if (cmd.id.includes('init')) {
|
|
126
|
+
suggestions.push({
|
|
127
|
+
id: `${cmd.group.toUpperCase()}_INIT_FORCE`,
|
|
128
|
+
command: `kb ${cmd.group} ${cmd.id.split(':')[1] || ''}`,
|
|
129
|
+
args: ['--force'],
|
|
130
|
+
description: `${cmd.describe} (force)`,
|
|
131
|
+
impact: 'disruptive',
|
|
132
|
+
when: 'INIT_FAILED',
|
|
133
|
+
available: true
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (cmd.id.includes('clean') || cmd.id.includes('reset')) {
|
|
138
|
+
suggestions.push({
|
|
139
|
+
id: `${cmd.group.toUpperCase()}_CLEAN_HARD`,
|
|
140
|
+
command: `kb ${cmd.group} ${cmd.id.split(':')[1] || ''}`,
|
|
141
|
+
args: ['--hard'],
|
|
142
|
+
description: `${cmd.describe} (hard)`,
|
|
143
|
+
impact: 'disruptive',
|
|
144
|
+
when: 'CLEAN_NEEDED',
|
|
145
|
+
available: true
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return suggestions;
|
|
151
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern CLI formatting utilities with side border design
|
|
3
|
+
* Provides minimalist, modern UI components for CLI output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { safeColors, safeSymbols } from './colors';
|
|
7
|
+
import { stripAnsi, bulletList as baseBulletList } from './format';
|
|
8
|
+
import { formatTiming as baseFormatTiming } from './command-output';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Side border box - modern minimalist design
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```
|
|
15
|
+
* ┌── Command Name
|
|
16
|
+
* │
|
|
17
|
+
* │ Section Header
|
|
18
|
+
* │ Key: value
|
|
19
|
+
* │
|
|
20
|
+
* └── ✓ Success / 12ms
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface SideBorderBoxOptions {
|
|
24
|
+
title: string;
|
|
25
|
+
sections: SectionContent[];
|
|
26
|
+
footer?: string;
|
|
27
|
+
status?: 'success' | 'error' | 'warning' | 'info';
|
|
28
|
+
timing?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SectionContent {
|
|
32
|
+
header?: string;
|
|
33
|
+
items: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a side-bordered box with modern design
|
|
38
|
+
*/
|
|
39
|
+
export function sideBorderBox(options: SideBorderBoxOptions): string {
|
|
40
|
+
const { title, sections, footer, status, timing } = options;
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
|
|
43
|
+
// Top border with title (using top-left corner)
|
|
44
|
+
const titleLine = `${safeSymbols.topLeft}${safeSymbols.separator.repeat(2)} ${safeColors.primary(safeColors.bold(title))}`;
|
|
45
|
+
lines.push(titleLine);
|
|
46
|
+
lines.push(safeSymbols.border);
|
|
47
|
+
|
|
48
|
+
// Sections
|
|
49
|
+
for (let i = 0; i < sections.length; i++) {
|
|
50
|
+
const section = sections[i];
|
|
51
|
+
if (!section) {continue;}
|
|
52
|
+
|
|
53
|
+
// Section header (optional)
|
|
54
|
+
if (section.header) {
|
|
55
|
+
lines.push(`${safeSymbols.border} ${safeColors.bold(section.header)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Section items
|
|
59
|
+
for (const item of section.items) {
|
|
60
|
+
lines.push(`${safeSymbols.border} ${item}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add spacing between sections (but not after the last one)
|
|
64
|
+
if (i < sections.length - 1) {
|
|
65
|
+
lines.push(safeSymbols.border);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bottom border with status/timing
|
|
70
|
+
if (footer || status || timing !== undefined) {
|
|
71
|
+
lines.push(safeSymbols.border);
|
|
72
|
+
const footerParts: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (footer) {
|
|
75
|
+
footerParts.push(footer);
|
|
76
|
+
} else if (status) {
|
|
77
|
+
const statusSymbol = getStatusSymbol(status);
|
|
78
|
+
const statusText = getStatusText(status);
|
|
79
|
+
const statusColor = getStatusColor(status);
|
|
80
|
+
footerParts.push(statusColor(`${statusSymbol} ${statusText}`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (timing !== undefined) {
|
|
84
|
+
footerParts.push(formatTiming(timing));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const footerLine = `${safeSymbols.bottomLeft}${safeSymbols.separator.repeat(2)} ${footerParts.join(' / ')}`;
|
|
88
|
+
lines.push(footerLine);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format a section header
|
|
96
|
+
*/
|
|
97
|
+
export function sectionHeader(text: string): string {
|
|
98
|
+
return safeColors.bold(text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format metrics list (key: value pairs with aligned values)
|
|
103
|
+
*/
|
|
104
|
+
export function metricsList(metrics: Record<string, string | number>): string[] {
|
|
105
|
+
const entries = Object.entries(metrics);
|
|
106
|
+
if (entries.length === 0) {return [];}
|
|
107
|
+
|
|
108
|
+
// Find max key length for alignment
|
|
109
|
+
const maxKeyLength = Math.max(
|
|
110
|
+
...entries.map(([key]) => stripAnsi(key).length)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return entries.map(([key, value]) => {
|
|
114
|
+
const keyLength = stripAnsi(key).length;
|
|
115
|
+
const padding = ' '.repeat(maxKeyLength - keyLength + 2);
|
|
116
|
+
const formattedKey = safeColors.bold(key);
|
|
117
|
+
const formattedValue = safeColors.muted(String(value));
|
|
118
|
+
return `${formattedKey}:${padding}${formattedValue}`;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format a bullet list (re-exported from base utilities)
|
|
124
|
+
*/
|
|
125
|
+
export const bulletList = baseBulletList;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format timing (re-exported from command-output)
|
|
129
|
+
*/
|
|
130
|
+
export const formatTiming = baseFormatTiming;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format status line for footer
|
|
134
|
+
*/
|
|
135
|
+
export function statusLine(
|
|
136
|
+
status: 'success' | 'error' | 'warning' | 'info',
|
|
137
|
+
timing?: number
|
|
138
|
+
): string {
|
|
139
|
+
const symbol = getStatusSymbol(status);
|
|
140
|
+
const text = getStatusText(status);
|
|
141
|
+
const color = getStatusColor(status);
|
|
142
|
+
|
|
143
|
+
const parts = [color(`${symbol} ${text}`)];
|
|
144
|
+
|
|
145
|
+
if (timing !== undefined) {
|
|
146
|
+
parts.push(formatTiming(timing));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return parts.join(' / ');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Helper functions
|
|
153
|
+
|
|
154
|
+
function getStatusSymbol(status: 'success' | 'error' | 'warning' | 'info'): string {
|
|
155
|
+
switch (status) {
|
|
156
|
+
case 'success':
|
|
157
|
+
return safeSymbols.success;
|
|
158
|
+
case 'error':
|
|
159
|
+
return safeSymbols.error;
|
|
160
|
+
case 'warning':
|
|
161
|
+
return safeSymbols.warning;
|
|
162
|
+
case 'info':
|
|
163
|
+
return safeSymbols.info;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getStatusText(status: 'success' | 'error' | 'warning' | 'info'): string {
|
|
168
|
+
switch (status) {
|
|
169
|
+
case 'success':
|
|
170
|
+
return 'Success';
|
|
171
|
+
case 'error':
|
|
172
|
+
return 'Failed';
|
|
173
|
+
case 'warning':
|
|
174
|
+
return 'Warning';
|
|
175
|
+
case 'info':
|
|
176
|
+
return 'Info';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getStatusColor(status: 'success' | 'error' | 'warning' | 'info'): (text: string) => string {
|
|
181
|
+
switch (status) {
|
|
182
|
+
case 'success':
|
|
183
|
+
return safeColors.success;
|
|
184
|
+
case 'error':
|
|
185
|
+
return safeColors.error;
|
|
186
|
+
case 'warning':
|
|
187
|
+
return safeColors.warning;
|
|
188
|
+
case 'info':
|
|
189
|
+
return safeColors.info;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format command help in modern side-border style
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* const help = formatCommandHelp({
|
|
199
|
+
* title: 'kb version',
|
|
200
|
+
* description: 'Show CLI version',
|
|
201
|
+
* longDescription: 'Displays the current version...',
|
|
202
|
+
* examples: ['kb version', 'kb version --json'],
|
|
203
|
+
* flags: [{name: 'json', description: 'Output in JSON'}]
|
|
204
|
+
* });
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export function formatCommandHelp(options: {
|
|
208
|
+
title: string;
|
|
209
|
+
description?: string;
|
|
210
|
+
longDescription?: string;
|
|
211
|
+
examples?: string[];
|
|
212
|
+
flags?: Array<{ name: string; alias?: string; description?: string; required?: boolean }>;
|
|
213
|
+
aliases?: string[];
|
|
214
|
+
}): string {
|
|
215
|
+
const { title, description, longDescription, examples, flags, aliases } = options;
|
|
216
|
+
const sections: SectionContent[] = [];
|
|
217
|
+
|
|
218
|
+
// Description section
|
|
219
|
+
if (description) {
|
|
220
|
+
sections.push({
|
|
221
|
+
header: 'Description',
|
|
222
|
+
items: [description],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Long description
|
|
227
|
+
if (longDescription) {
|
|
228
|
+
sections.push({
|
|
229
|
+
header: 'Details',
|
|
230
|
+
items: [longDescription],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Aliases
|
|
235
|
+
if (aliases && aliases.length > 0) {
|
|
236
|
+
sections.push({
|
|
237
|
+
header: 'Aliases',
|
|
238
|
+
items: aliases.map(a => safeColors.muted(a)),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Flags
|
|
243
|
+
if (flags && flags.length > 0) {
|
|
244
|
+
const flagItems = flags.map(flag => {
|
|
245
|
+
const label = flag.alias
|
|
246
|
+
? `--${flag.name}, -${flag.alias}`
|
|
247
|
+
: `--${flag.name}`;
|
|
248
|
+
const required = flag.required ? safeColors.warning(' (required)') : '';
|
|
249
|
+
const desc = flag.description ? safeColors.muted(` — ${flag.description}`) : '';
|
|
250
|
+
return `${safeColors.bold(label)}${required}${desc}`;
|
|
251
|
+
});
|
|
252
|
+
sections.push({
|
|
253
|
+
header: 'Flags',
|
|
254
|
+
items: flagItems,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Examples
|
|
259
|
+
if (examples && examples.length > 0) {
|
|
260
|
+
sections.push({
|
|
261
|
+
header: 'Examples',
|
|
262
|
+
items: examples.map(ex => safeColors.muted(` ${ex}`)),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return sideBorderBox({
|
|
267
|
+
title,
|
|
268
|
+
sections,
|
|
269
|
+
status: 'info',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-CLI suggestions system
|
|
3
|
+
* Supports multiple CLI packages with their own manifests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createCommandRegistry,
|
|
8
|
+
generateDevlinkSuggestions,
|
|
9
|
+
type CommandSuggestion,
|
|
10
|
+
type CommandRegistry
|
|
11
|
+
} from './command-suggestions';
|
|
12
|
+
import {
|
|
13
|
+
extractCommandIds,
|
|
14
|
+
generateGroupSuggestions,
|
|
15
|
+
type CommandManifest
|
|
16
|
+
} from './manifest-parser';
|
|
17
|
+
|
|
18
|
+
export interface MultiCLIContext {
|
|
19
|
+
warningCodes: Set<string>;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CLIPackage {
|
|
24
|
+
name: string;
|
|
25
|
+
group: string;
|
|
26
|
+
commands: CommandManifest[];
|
|
27
|
+
priority: number; // Higher priority = more important suggestions
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Multi-CLI suggestions manager
|
|
32
|
+
*/
|
|
33
|
+
export class MultiCLISuggestions {
|
|
34
|
+
private packages: Map<string, CLIPackage> = new Map();
|
|
35
|
+
private globalRegistry: CommandRegistry | null = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register a CLI package
|
|
39
|
+
*/
|
|
40
|
+
registerPackage(pkg: CLIPackage): void {
|
|
41
|
+
this.packages.set(pkg.name, pkg);
|
|
42
|
+
this.globalRegistry = null; // Invalidate cache
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get or create global command registry
|
|
47
|
+
*/
|
|
48
|
+
private getGlobalRegistry(): CommandRegistry {
|
|
49
|
+
if (this.globalRegistry) {
|
|
50
|
+
return this.globalRegistry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const allCommands: string[] = [];
|
|
54
|
+
for (const pkg of this.packages.values()) {
|
|
55
|
+
allCommands.push(...extractCommandIds(pkg.commands));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.globalRegistry = createCommandRegistry(allCommands);
|
|
59
|
+
return this.globalRegistry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate suggestions for a specific group
|
|
64
|
+
*/
|
|
65
|
+
generateGroupSuggestions(
|
|
66
|
+
group: string,
|
|
67
|
+
context: MultiCLIContext
|
|
68
|
+
): CommandSuggestion[] {
|
|
69
|
+
const suggestions: CommandSuggestion[] = [];
|
|
70
|
+
const registry = this.getGlobalRegistry();
|
|
71
|
+
|
|
72
|
+
// Find packages for this group
|
|
73
|
+
const groupPackages = Array.from(this.packages.values())
|
|
74
|
+
.filter(pkg => pkg.group === group)
|
|
75
|
+
.sort((a, b) => b.priority - a.priority);
|
|
76
|
+
|
|
77
|
+
for (const pkg of groupPackages) {
|
|
78
|
+
const groupSuggestions = generateGroupSuggestions(
|
|
79
|
+
pkg.commands,
|
|
80
|
+
group,
|
|
81
|
+
context.warningCodes,
|
|
82
|
+
context
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Convert to CommandSuggestion format and validate
|
|
86
|
+
for (const suggestion of groupSuggestions) {
|
|
87
|
+
if (registry.commands.has(suggestion.command.replace('kb ', '').replace(' ', ':'))) {
|
|
88
|
+
suggestions.push({
|
|
89
|
+
id: suggestion.id,
|
|
90
|
+
command: suggestion.command,
|
|
91
|
+
args: suggestion.args,
|
|
92
|
+
description: suggestion.description,
|
|
93
|
+
impact: suggestion.impact,
|
|
94
|
+
when: suggestion.when,
|
|
95
|
+
available: true
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return suggestions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate all suggestions across all packages
|
|
106
|
+
*/
|
|
107
|
+
generateAllSuggestions(context: MultiCLIContext): CommandSuggestion[] {
|
|
108
|
+
const suggestions: CommandSuggestion[] = [];
|
|
109
|
+
const registry = this.getGlobalRegistry();
|
|
110
|
+
|
|
111
|
+
// Special handling for devlink (highest priority)
|
|
112
|
+
if (this.packages.has('devlink')) {
|
|
113
|
+
const devlinkSuggestions = generateDevlinkSuggestions(
|
|
114
|
+
context.warningCodes,
|
|
115
|
+
{ undo: context.undo },
|
|
116
|
+
registry
|
|
117
|
+
);
|
|
118
|
+
suggestions.push(...devlinkSuggestions);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Generate suggestions for other groups
|
|
122
|
+
const groups = new Set<string>();
|
|
123
|
+
for (const pkg of this.packages.values()) {
|
|
124
|
+
groups.add(pkg.group);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const group of groups) {
|
|
128
|
+
if (group !== 'devlink') { // Skip devlink as it's handled above
|
|
129
|
+
const groupSuggestions = this.generateGroupSuggestions(group, context);
|
|
130
|
+
suggestions.push(...groupSuggestions);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return suggestions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get available commands for a group
|
|
139
|
+
*/
|
|
140
|
+
getAvailableCommands(group: string): string[] {
|
|
141
|
+
const groupPackages = Array.from(this.packages.values())
|
|
142
|
+
.filter(pkg => pkg.group === group);
|
|
143
|
+
|
|
144
|
+
const commands: string[] = [];
|
|
145
|
+
for (const pkg of groupPackages) {
|
|
146
|
+
commands.push(...extractCommandIds(pkg.commands));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return commands;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get all registered packages
|
|
154
|
+
*/
|
|
155
|
+
getPackages(): CLIPackage[] {
|
|
156
|
+
return Array.from(this.packages.values());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table formatting utilities for CLI output
|
|
3
|
+
* Handles proper column alignment accounting for emoji/unicode width
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate visual width of a string (accounting for emoji/wide chars and ANSI codes)
|
|
8
|
+
* Simplified version - emoji count as 2 chars, ANSI codes are stripped
|
|
9
|
+
*/
|
|
10
|
+
function visualWidth(str: string): number {
|
|
11
|
+
// Remove ANSI escape codes first
|
|
12
|
+
const ansiRegex = /\x1B\[[0-9;]*[a-zA-Z]/g;
|
|
13
|
+
const cleanStr = str.replace(ansiRegex, '');
|
|
14
|
+
|
|
15
|
+
// Count emoji and wide characters as 2
|
|
16
|
+
const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]|[\u{2190}-\u{21FF}]|[\u{2300}-\u{23FF}]|[\u{24C2}-\u{1F251}]|[\u{2B50}-\u{2B55}]|[\u{3030}-\u{303D}]|[\u{3297}-\u{3299}]|[\u{1F000}-\u{1F02F}]|[\u{1F0A0}-\u{1F0FF}]|[\u{1F100}-\u{1F1FF}]|[\u{1F200}-\u{1F2FF}]|[\u{1F300}-\u{1F5FF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F700}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]|[\u{23F0}-\u{23FF}]|[\u{25A0}-\u{25FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE00}-\u{FE0F}]|[\u{1F1E6}-\u{1F1FF}]|[\u{1F3FB}-\u{1F3FF}]|[\u{1F9B0}-\u{1F9B3}]|[\u{20E3}]|[\u{FE0F}]/gu;
|
|
17
|
+
const emojiMatches = cleanStr.match(emojiRegex);
|
|
18
|
+
const emojiCount = emojiMatches ? emojiMatches.length : 0;
|
|
19
|
+
|
|
20
|
+
// Basic character count minus emoji count (since emoji are already counted)
|
|
21
|
+
// Then add emoji count * 2 for visual width
|
|
22
|
+
return cleanStr.length - emojiCount + emojiCount * 2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pad string to visual width (accounting for emoji)
|
|
27
|
+
*/
|
|
28
|
+
function padVisual(str: string, width: number, padChar: string = ' '): string {
|
|
29
|
+
const vWidth = visualWidth(str);
|
|
30
|
+
const padding = Math.max(0, width - vWidth);
|
|
31
|
+
return str + padChar.repeat(padding);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TableColumn {
|
|
35
|
+
header: string;
|
|
36
|
+
width?: number; // Auto-calculated if not provided
|
|
37
|
+
align?: 'left' | 'right' | 'center';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TableOptions {
|
|
41
|
+
header?: boolean;
|
|
42
|
+
separator?: string; // Character for separator line (e.g., '─')
|
|
43
|
+
padding?: number; // Padding between columns
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format data as a table with proper column alignment
|
|
48
|
+
*/
|
|
49
|
+
export function formatTable(
|
|
50
|
+
columns: TableColumn[],
|
|
51
|
+
rows: string[][],
|
|
52
|
+
options: TableOptions = {}
|
|
53
|
+
): string[] {
|
|
54
|
+
const { header = true, separator = '─', padding = 1 } = options;
|
|
55
|
+
|
|
56
|
+
// Calculate column widths
|
|
57
|
+
const widths: number[] = columns.map((col, idx) => {
|
|
58
|
+
if (col.width !== undefined) {
|
|
59
|
+
return col.width;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Calculate max width from header and all rows
|
|
63
|
+
let maxWidth = visualWidth(col.header);
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
if (row[idx]) {
|
|
66
|
+
maxWidth = Math.max(maxWidth, visualWidth(String(row[idx])));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return maxWidth;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Header
|
|
75
|
+
if (header) {
|
|
76
|
+
const headerCells = columns.map((col, idx) => {
|
|
77
|
+
const cell = col.header;
|
|
78
|
+
const width = widths[idx]!;
|
|
79
|
+
return padVisual(cell, width);
|
|
80
|
+
});
|
|
81
|
+
lines.push(headerCells.join(' '.repeat(padding)));
|
|
82
|
+
|
|
83
|
+
// Separator
|
|
84
|
+
if (separator) {
|
|
85
|
+
const separatorLine = widths.map(w => separator.repeat(w)).join(' '.repeat(padding));
|
|
86
|
+
lines.push(separatorLine);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Rows
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const cells = columns.map((col, idx) => {
|
|
93
|
+
const cell = row[idx] !== undefined ? String(row[idx]) : '';
|
|
94
|
+
const align = col.align || 'left';
|
|
95
|
+
const width = widths[idx]!;
|
|
96
|
+
|
|
97
|
+
if (align === 'right') {
|
|
98
|
+
const vWidth = visualWidth(cell);
|
|
99
|
+
const padding = Math.max(0, width - vWidth);
|
|
100
|
+
return ' '.repeat(padding) + cell;
|
|
101
|
+
} else if (align === 'center') {
|
|
102
|
+
const vWidth = visualWidth(cell);
|
|
103
|
+
const padding = Math.max(0, width - vWidth);
|
|
104
|
+
const leftPad = Math.floor(padding / 2);
|
|
105
|
+
const rightPad = padding - leftPad;
|
|
106
|
+
return ' '.repeat(leftPad) + cell + ' '.repeat(rightPad);
|
|
107
|
+
} else {
|
|
108
|
+
return padVisual(cell, width);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
lines.push(cells.join(' '.repeat(padding)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format simple key-value pairs as a table
|
|
119
|
+
*/
|
|
120
|
+
export function formatKeyValueTable(
|
|
121
|
+
data: Record<string, string | number>,
|
|
122
|
+
options: { keyWidth?: number; valueWidth?: number } = {}
|
|
123
|
+
): string[] {
|
|
124
|
+
const keys = Object.keys(data);
|
|
125
|
+
if (keys.length === 0) {return [];}
|
|
126
|
+
|
|
127
|
+
const keyWidth = options.keyWidth || Math.max(...keys.map(k => visualWidth(k)), 0);
|
|
128
|
+
const valueWidth = options.valueWidth || Math.max(...Object.values(data).map(v => visualWidth(String(v))), 0);
|
|
129
|
+
|
|
130
|
+
return keys.map(key => {
|
|
131
|
+
const value = String(data[key]);
|
|
132
|
+
return `${padVisual(key, keyWidth)} ${padVisual(value, valueWidth)}`;
|
|
133
|
+
});
|
|
134
|
+
}
|