@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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting utilities for structured CLI output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { safeColors, safeSymbols } from './colors';
|
|
6
|
+
|
|
7
|
+
const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
8
|
+
|
|
9
|
+
export function stripAnsi(input: string): string {
|
|
10
|
+
return input.replace(ANSI_PATTERN, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hasAnsi(input: string): boolean {
|
|
14
|
+
return /\u001B\[[0-9;]*m/.test(input);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function visibleLength(input: string): number {
|
|
18
|
+
return stripAnsi(input).length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a boxed section with title
|
|
23
|
+
*/
|
|
24
|
+
export function box(title: string, content: string[] = [], maxWidth?: number): string {
|
|
25
|
+
const lines = content.length > 0 ? content : [''];
|
|
26
|
+
const titleWidth = visibleLength(title);
|
|
27
|
+
|
|
28
|
+
// Determine max width: use provided maxWidth, or terminal width (default 80), or content width
|
|
29
|
+
const terminalWidth = typeof process !== 'undefined' && process.stdout?.columns ? process.stdout.columns : 80;
|
|
30
|
+
const effectiveMaxWidth = maxWidth ?? Math.min(terminalWidth - 4, 120); // -4 for borders and padding
|
|
31
|
+
|
|
32
|
+
const bodyWidth = Math.min(
|
|
33
|
+
effectiveMaxWidth,
|
|
34
|
+
Math.max(titleWidth, ...lines.map(line => visibleLength(line)))
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Wrap long lines
|
|
38
|
+
const wrappedLines: string[] = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const lineLength = visibleLength(line);
|
|
41
|
+
if (lineLength <= bodyWidth) {
|
|
42
|
+
wrappedLines.push(line);
|
|
43
|
+
} else {
|
|
44
|
+
// Split long lines at word boundaries when possible
|
|
45
|
+
const words = line.split(/(\s+)/);
|
|
46
|
+
let currentLine = '';
|
|
47
|
+
for (const word of words) {
|
|
48
|
+
const testLine = currentLine + word;
|
|
49
|
+
if (visibleLength(testLine) <= bodyWidth) {
|
|
50
|
+
currentLine = testLine;
|
|
51
|
+
} else {
|
|
52
|
+
if (currentLine) {
|
|
53
|
+
wrappedLines.push(currentLine.trimEnd());
|
|
54
|
+
}
|
|
55
|
+
// If single word is too long, truncate it
|
|
56
|
+
if (visibleLength(word) > bodyWidth) {
|
|
57
|
+
wrappedLines.push(truncate(word, bodyWidth));
|
|
58
|
+
currentLine = '';
|
|
59
|
+
} else {
|
|
60
|
+
currentLine = word;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (currentLine) {
|
|
65
|
+
wrappedLines.push(currentLine.trimEnd());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const topBorder = `┌${'─'.repeat(bodyWidth + 2)}┐`;
|
|
71
|
+
const titleLine = `│ ${safeColors.bold(title)}${' '.repeat(Math.max(0, bodyWidth - titleWidth))} │`;
|
|
72
|
+
const bodyLines = wrappedLines.map((line) => {
|
|
73
|
+
const padding = Math.max(0, bodyWidth - visibleLength(line));
|
|
74
|
+
return `│ ${line}${' '.repeat(padding)} │`;
|
|
75
|
+
});
|
|
76
|
+
const bottomBorder = `└${'─'.repeat(bodyWidth + 2)}┘`;
|
|
77
|
+
|
|
78
|
+
return [topBorder, titleLine, ...bodyLines, bottomBorder].join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add consistent indentation to lines
|
|
83
|
+
*/
|
|
84
|
+
export function indent(lines: string[], level = 1): string[] {
|
|
85
|
+
const prefix = ' '.repeat(level);
|
|
86
|
+
return lines.map(line => `${prefix}${line}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a section with header and content
|
|
91
|
+
*/
|
|
92
|
+
export function section(header: string, content: string[]): string[] {
|
|
93
|
+
return [
|
|
94
|
+
'',
|
|
95
|
+
safeColors.bold(header),
|
|
96
|
+
...indent(content),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Format a table with consistent spacing
|
|
102
|
+
*/
|
|
103
|
+
export function table(rows: (string | number)[][], headers?: string[]): string[] {
|
|
104
|
+
if (rows.length === 0) {return [];}
|
|
105
|
+
|
|
106
|
+
// Calculate column widths
|
|
107
|
+
const allRows = headers ? [headers, ...rows] : rows;
|
|
108
|
+
if (allRows.length === 0) {return [];}
|
|
109
|
+
|
|
110
|
+
const columnWidths = allRows[0]!.map((_, colIndex) =>
|
|
111
|
+
Math.max(...allRows.map(row => String(row[colIndex] || '').length))
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Format rows
|
|
115
|
+
return allRows.map(row => {
|
|
116
|
+
return row.map((cell, colIndex) =>
|
|
117
|
+
String(cell || '').padEnd(columnWidths[colIndex] || 0)
|
|
118
|
+
).join(' ');
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format key-value pairs
|
|
124
|
+
*/
|
|
125
|
+
export interface KeyValueOptions {
|
|
126
|
+
padKeys?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function keyValue(
|
|
130
|
+
pairs: Record<string, string | number>,
|
|
131
|
+
options: KeyValueOptions = {},
|
|
132
|
+
): string[] {
|
|
133
|
+
const { padKeys = true } = options;
|
|
134
|
+
return safeKeyValue(pairs, { pad: padKeys });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format a list with bullets
|
|
139
|
+
*/
|
|
140
|
+
export function bulletList(items: string[]): string[] {
|
|
141
|
+
return items.map(item => `${safeSymbols.bullet} ${item}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface SafeKeyValueOptions {
|
|
145
|
+
indent?: number;
|
|
146
|
+
pad?: boolean;
|
|
147
|
+
valueColor?: (value: string, key: string) => string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function safeKeyValue(
|
|
151
|
+
pairs: Record<string, string | number>,
|
|
152
|
+
options: SafeKeyValueOptions = {},
|
|
153
|
+
): string[] {
|
|
154
|
+
const { indent = 0, pad = true, valueColor } = options;
|
|
155
|
+
const keys = Object.keys(pairs);
|
|
156
|
+
if (keys.length === 0) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const indentStr = ' '.repeat(indent);
|
|
160
|
+
const maxKeyLength = pad
|
|
161
|
+
? Math.max(...keys.map(key => indent + stripAnsi(key).length))
|
|
162
|
+
: 0;
|
|
163
|
+
return keys.map((key) => {
|
|
164
|
+
const rawKey = key;
|
|
165
|
+
const rawValue = String(pairs[key] ?? '');
|
|
166
|
+
const keyLength = indent + stripAnsi(rawKey).length;
|
|
167
|
+
const padding = pad ? Math.max(0, maxKeyLength - keyLength) : 0;
|
|
168
|
+
const formattedKey = `${indentStr}${rawKey}${' '.repeat(padding)}`;
|
|
169
|
+
const computedValue = valueColor ? valueColor(rawValue, key) : rawValue;
|
|
170
|
+
const valueText = hasAnsi(computedValue) ? computedValue : safeColors.muted(computedValue);
|
|
171
|
+
return `${safeColors.bold(formattedKey)}: ${valueText}`;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pad2 = (value: number): string => value.toString().padStart(2, '0');
|
|
176
|
+
|
|
177
|
+
const toDate = (value: string | Date): Date => (value instanceof Date ? new Date(value.getTime()) : new Date(value));
|
|
178
|
+
|
|
179
|
+
const isValidDate = (date: Date): boolean => Number.isFinite(date.getTime());
|
|
180
|
+
|
|
181
|
+
function getOffsetMinutes(date: Date, timeZone?: string): number {
|
|
182
|
+
if (!timeZone) {
|
|
183
|
+
return -date.getTimezoneOffset();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
187
|
+
timeZone,
|
|
188
|
+
hour12: false,
|
|
189
|
+
year: 'numeric',
|
|
190
|
+
month: '2-digit',
|
|
191
|
+
day: '2-digit',
|
|
192
|
+
hour: '2-digit',
|
|
193
|
+
minute: '2-digit',
|
|
194
|
+
second: '2-digit',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const parts = dtf.formatToParts(date);
|
|
198
|
+
const lookup = Object.fromEntries(
|
|
199
|
+
parts
|
|
200
|
+
.filter(part => part.type !== 'literal')
|
|
201
|
+
.map(part => [part.type, Number(part.value)]),
|
|
202
|
+
) as Record<string, number | undefined>;
|
|
203
|
+
|
|
204
|
+
const year = lookup.year ?? date.getUTCFullYear();
|
|
205
|
+
const month = (lookup.month ?? date.getUTCMonth() + 1) - 1;
|
|
206
|
+
const day = lookup.day ?? date.getUTCDate();
|
|
207
|
+
// Intl.DateTimeFormat with hour12:false may return 24 for midnight, normalize to 0
|
|
208
|
+
const hour = (lookup.hour ?? date.getUTCHours()) % 24;
|
|
209
|
+
const minute = lookup.minute ?? date.getUTCMinutes();
|
|
210
|
+
const second = lookup.second ?? date.getUTCSeconds();
|
|
211
|
+
|
|
212
|
+
const asUTC = Date.UTC(year, month, day, hour, minute, second);
|
|
213
|
+
|
|
214
|
+
return Math.round((asUTC - date.getTime()) / 60000);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatOffset(offsetMinutes: number): string {
|
|
218
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
219
|
+
const absMinutes = Math.abs(offsetMinutes);
|
|
220
|
+
const hours = Math.floor(absMinutes / 60);
|
|
221
|
+
const minutes = absMinutes % 60;
|
|
222
|
+
return `${sign}${pad2(hours)}:${pad2(minutes)}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getDateParts(date: Date, timeZone?: string): {
|
|
226
|
+
year: number;
|
|
227
|
+
month: number;
|
|
228
|
+
day: number;
|
|
229
|
+
hour: number;
|
|
230
|
+
minute: number;
|
|
231
|
+
second: number;
|
|
232
|
+
} {
|
|
233
|
+
if (!timeZone) {
|
|
234
|
+
return {
|
|
235
|
+
year: date.getFullYear(),
|
|
236
|
+
month: date.getMonth() + 1,
|
|
237
|
+
day: date.getDate(),
|
|
238
|
+
hour: date.getHours(),
|
|
239
|
+
minute: date.getMinutes(),
|
|
240
|
+
second: date.getSeconds(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
245
|
+
timeZone,
|
|
246
|
+
hour12: false,
|
|
247
|
+
year: 'numeric',
|
|
248
|
+
month: '2-digit',
|
|
249
|
+
day: '2-digit',
|
|
250
|
+
hour: '2-digit',
|
|
251
|
+
minute: '2-digit',
|
|
252
|
+
second: '2-digit',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const parts = dtf.formatToParts(date);
|
|
256
|
+
const lookup = Object.fromEntries(
|
|
257
|
+
parts
|
|
258
|
+
.filter(part => part.type !== 'literal')
|
|
259
|
+
.map(part => [part.type, Number(part.value)]),
|
|
260
|
+
) as Record<string, number | undefined>;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
year: lookup.year ?? date.getUTCFullYear(),
|
|
264
|
+
month: lookup.month ?? date.getUTCMonth() + 1,
|
|
265
|
+
day: lookup.day ?? date.getUTCDate(),
|
|
266
|
+
// Intl.DateTimeFormat with hour12:false may return 24 for midnight, normalize to 0
|
|
267
|
+
hour: (lookup.hour ?? date.getUTCHours()) % 24,
|
|
268
|
+
minute: lookup.minute ?? date.getUTCMinutes(),
|
|
269
|
+
second: lookup.second ?? date.getUTCSeconds(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Apply primary headline styling
|
|
275
|
+
*/
|
|
276
|
+
export function headline(text: string): string {
|
|
277
|
+
return safeColors.primary(safeColors.bold(text));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Accent label style (used for tags/pills)
|
|
282
|
+
*/
|
|
283
|
+
export function accentLabel(text: string): string {
|
|
284
|
+
return safeColors.accent(safeColors.bold(text));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Muted helper
|
|
289
|
+
*/
|
|
290
|
+
export function muted(text: string): string {
|
|
291
|
+
return safeColors.muted(text);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Format file size
|
|
296
|
+
*/
|
|
297
|
+
export function formatSize(bytes: number): string {
|
|
298
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
299
|
+
let size = bytes;
|
|
300
|
+
let unitIndex = 0;
|
|
301
|
+
|
|
302
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
303
|
+
size /= 1024;
|
|
304
|
+
unitIndex++;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Format relative time
|
|
312
|
+
*/
|
|
313
|
+
export function formatRelativeTime(timestamp: string | Date): string {
|
|
314
|
+
const now = new Date();
|
|
315
|
+
const time = toDate(timestamp);
|
|
316
|
+
|
|
317
|
+
if (!isValidDate(time)) {
|
|
318
|
+
return 'Invalid date';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const diffMs = now.getTime() - time.getTime();
|
|
322
|
+
|
|
323
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
324
|
+
const minutes = Math.floor(seconds / 60);
|
|
325
|
+
const hours = Math.floor(minutes / 60);
|
|
326
|
+
const days = Math.floor(hours / 24);
|
|
327
|
+
|
|
328
|
+
if (days > 0) {
|
|
329
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
330
|
+
} else if (hours > 0) {
|
|
331
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
332
|
+
} else if (minutes > 0) {
|
|
333
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
334
|
+
} else {
|
|
335
|
+
return `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface FormatTimestampOptions {
|
|
340
|
+
mode?: 'local' | 'iso';
|
|
341
|
+
timeZone?: string;
|
|
342
|
+
includeSeconds?: boolean;
|
|
343
|
+
includeMilliseconds?: boolean;
|
|
344
|
+
includeOffset?: boolean;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Format timestamps as absolute values (local or ISO) with optional offsets
|
|
349
|
+
*/
|
|
350
|
+
export function formatTimestamp(
|
|
351
|
+
timestamp: string | Date,
|
|
352
|
+
options: FormatTimestampOptions = {},
|
|
353
|
+
): string {
|
|
354
|
+
const {
|
|
355
|
+
mode = 'local',
|
|
356
|
+
timeZone,
|
|
357
|
+
includeSeconds = false,
|
|
358
|
+
includeMilliseconds = true,
|
|
359
|
+
includeOffset = true,
|
|
360
|
+
} = options;
|
|
361
|
+
|
|
362
|
+
const date = toDate(timestamp);
|
|
363
|
+
if (!isValidDate(date)) {
|
|
364
|
+
return 'Invalid date';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const offsetMinutes = includeOffset ? getOffsetMinutes(date, timeZone) : null;
|
|
368
|
+
const offsetSuffix =
|
|
369
|
+
includeOffset && offsetMinutes !== null ? ` (${formatOffset(offsetMinutes)})` : '';
|
|
370
|
+
|
|
371
|
+
if (mode === 'iso') {
|
|
372
|
+
const isoRaw = date.toISOString();
|
|
373
|
+
const iso = includeMilliseconds ? isoRaw : isoRaw.replace(/\.\d{3}Z$/, 'Z');
|
|
374
|
+
return `${iso}${offsetSuffix}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { year, month, day, hour, minute, second } = getDateParts(date, timeZone);
|
|
378
|
+
const base = [
|
|
379
|
+
`${year}-${pad2(month)}-${pad2(day)}`,
|
|
380
|
+
`${pad2(hour)}:${pad2(minute)}${includeSeconds ? `:${pad2(second)}` : ''}`,
|
|
381
|
+
].join(' ');
|
|
382
|
+
|
|
383
|
+
return `${base}${offsetSuffix}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Truncate text with ellipsis
|
|
388
|
+
*/
|
|
389
|
+
export function truncate(text: string, maxLength: number): string {
|
|
390
|
+
if (text.length <= maxLength) {return text;}
|
|
391
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Pad string to specific width
|
|
396
|
+
*/
|
|
397
|
+
export function pad(text: string, width: number, align: 'left' | 'right' | 'center' = 'left'): string {
|
|
398
|
+
if (text.length >= width) {return text;}
|
|
399
|
+
|
|
400
|
+
const padding = width - text.length;
|
|
401
|
+
|
|
402
|
+
switch (align) {
|
|
403
|
+
case 'right':
|
|
404
|
+
return ' '.repeat(padding) + text;
|
|
405
|
+
case 'center':
|
|
406
|
+
const leftPad = Math.floor(padding / 2);
|
|
407
|
+
const rightPad = padding - leftPad;
|
|
408
|
+
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
|
409
|
+
default:
|
|
410
|
+
return text + ' '.repeat(padding);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main exports for @kb-labs/shared-cli-ui
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './colors';
|
|
6
|
+
export * from './loader';
|
|
7
|
+
export * from './format';
|
|
8
|
+
export * from './command-output';
|
|
9
|
+
export * from './timing-tracker';
|
|
10
|
+
export * from './command-suggestions';
|
|
11
|
+
export * from './command-discovery';
|
|
12
|
+
export * from './manifest-parser';
|
|
13
|
+
export * from './multi-cli-suggestions';
|
|
14
|
+
export * from './dynamic-command-discovery';
|
|
15
|
+
export * from './artifacts-display';
|
|
16
|
+
export * from './table';
|
|
17
|
+
export * from './debug';
|
|
18
|
+
export * from './command-runner';
|
|
19
|
+
export * from './utils/flags';
|
|
20
|
+
export * from './utils/env';
|
|
21
|
+
export * from './utils/context';
|
|
22
|
+
export * from './utils/path';
|
|
23
|
+
|
|
24
|
+
// Modern UI Kit (new) - selective exports to avoid conflicts
|
|
25
|
+
export {
|
|
26
|
+
sideBorderBox,
|
|
27
|
+
sectionHeader,
|
|
28
|
+
metricsList,
|
|
29
|
+
statusLine,
|
|
30
|
+
formatCommandHelp,
|
|
31
|
+
type SideBorderBoxOptions,
|
|
32
|
+
type SectionContent,
|
|
33
|
+
} from './modern-format';
|
|
34
|
+
export * from './command-result';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress indicators and loaders for CLI operations
|
|
3
|
+
*
|
|
4
|
+
* Periodic animation implementation (works in child processes as multi-line output).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { safeColors, safeSymbols } from './colors';
|
|
8
|
+
|
|
9
|
+
export interface LoaderOptions {
|
|
10
|
+
/** Text to show while loading */
|
|
11
|
+
text?: string;
|
|
12
|
+
/** Whether to show spinner (true) or progress bar (false) */
|
|
13
|
+
spinner?: boolean;
|
|
14
|
+
/** Total items for progress bar */
|
|
15
|
+
total?: number;
|
|
16
|
+
/** Current item for progress bar */
|
|
17
|
+
current?: number;
|
|
18
|
+
/** Whether JSON mode is enabled (disables all visual feedback) */
|
|
19
|
+
jsonMode?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SPINNER_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
|
+
|
|
24
|
+
// Process-scoped json mode flag. Set by SDK before invoking handler; read by useLoader().
|
|
25
|
+
let _jsonMode = false;
|
|
26
|
+
|
|
27
|
+
export function setJsonMode(enabled: boolean): void {
|
|
28
|
+
_jsonMode = enabled;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isJsonMode(): boolean {
|
|
32
|
+
return _jsonMode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class Loader {
|
|
36
|
+
private isActive = false;
|
|
37
|
+
private options: LoaderOptions;
|
|
38
|
+
private frameIndex = 0;
|
|
39
|
+
private intervalId?: NodeJS.Timeout;
|
|
40
|
+
private currentText: string; // ← Реактивная переменная для текста
|
|
41
|
+
|
|
42
|
+
constructor(options: LoaderOptions = {}) {
|
|
43
|
+
this.options = {
|
|
44
|
+
text: 'Loading...',
|
|
45
|
+
spinner: true,
|
|
46
|
+
jsonMode: false,
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
this.currentText = this.options.text ?? 'Loading...';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
start(): void {
|
|
53
|
+
if (this.options.jsonMode || this.isActive) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.isActive = true;
|
|
57
|
+
|
|
58
|
+
// Start periodic animation updates (every 200ms)
|
|
59
|
+
if (this.options.spinner && !this.options.jsonMode) {
|
|
60
|
+
this.intervalId = setInterval(() => {
|
|
61
|
+
if (!this.isActive) {
|
|
62
|
+
this.clearInterval();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const char = SPINNER_CHARS[this.frameIndex % SPINNER_CHARS.length];
|
|
67
|
+
process.stdout.write(`\r${char} ${this.currentText}`); // ← Читаем реактивный текст
|
|
68
|
+
this.frameIndex++;
|
|
69
|
+
}, 200);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
update(options: Partial<LoaderOptions>): void {
|
|
74
|
+
this.options = { ...this.options, ...options };
|
|
75
|
+
|
|
76
|
+
if (!this.isActive || this.options.jsonMode) {return;}
|
|
77
|
+
|
|
78
|
+
// Update reactive text - setInterval will pick it up on next tick
|
|
79
|
+
if (options.text !== undefined) {
|
|
80
|
+
this.currentText = options.text;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// For manual updates when no interval (progress bar mode)
|
|
84
|
+
if (!this.intervalId) {
|
|
85
|
+
if (this.options.spinner) {
|
|
86
|
+
const text = this.options.text ?? 'Loading...';
|
|
87
|
+
console.log(`${safeSymbols.info} ${text}`);
|
|
88
|
+
} else if (this.options.total !== undefined) {
|
|
89
|
+
this.updateProgress();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stop(): void {
|
|
95
|
+
this.isActive = false;
|
|
96
|
+
this.clearInterval();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
succeed(message?: string): void {
|
|
100
|
+
this.stop();
|
|
101
|
+
if (!this.options.jsonMode && message) {
|
|
102
|
+
process.stdout.write(`\r\x1b[K${safeSymbols.success} ${message}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fail(message?: string): void {
|
|
107
|
+
this.stop();
|
|
108
|
+
if (!this.options.jsonMode && message) {
|
|
109
|
+
process.stdout.write(`\r\x1b[K${safeSymbols.error} ${message}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private clearInterval(): void {
|
|
114
|
+
if (this.intervalId) {
|
|
115
|
+
clearInterval(this.intervalId);
|
|
116
|
+
this.intervalId = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private updateProgress(): void {
|
|
121
|
+
if (this.options.total === undefined || this.options.current === undefined) {return;}
|
|
122
|
+
|
|
123
|
+
const current = this.options.current;
|
|
124
|
+
const total = this.options.total;
|
|
125
|
+
const percentage = Math.round((current / total) * 100);
|
|
126
|
+
const barLength = 20;
|
|
127
|
+
const filledLength = Math.round((current / total) * barLength);
|
|
128
|
+
|
|
129
|
+
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
|
|
130
|
+
const text = this.options.text || 'Progress';
|
|
131
|
+
|
|
132
|
+
process.stdout.write(`\r${safeColors.info('→')} ${text}... ${bar} ${percentage}% (${current}/${total})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a simple spinner
|
|
138
|
+
*/
|
|
139
|
+
export function createSpinner(text: string, jsonMode = false): Loader {
|
|
140
|
+
return new Loader({ text, spinner: true, jsonMode });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a progress bar
|
|
145
|
+
*/
|
|
146
|
+
export function createProgressBar(text: string, total: number, jsonMode = false): Loader {
|
|
147
|
+
return new Loader({ text, spinner: false, total, current: 0, jsonMode });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Simple loading message without spinner
|
|
152
|
+
*/
|
|
153
|
+
export function showLoading(text: string, jsonMode = false): void {
|
|
154
|
+
if (!jsonMode) {
|
|
155
|
+
console.log(`${safeColors.info('→')} ${text}...`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Show completion message
|
|
161
|
+
*/
|
|
162
|
+
export function showSuccess(text: string, jsonMode = false): void {
|
|
163
|
+
if (!jsonMode) {
|
|
164
|
+
console.log(`${safeSymbols.success} ${text}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Show error message
|
|
170
|
+
*/
|
|
171
|
+
export function showError(text: string, jsonMode = false): void {
|
|
172
|
+
if (!jsonMode) {
|
|
173
|
+
console.log(`${safeSymbols.error} ${text}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a loader for progress indication
|
|
179
|
+
*
|
|
180
|
+
* @param text - Text to display while loading
|
|
181
|
+
* @param options - Optional configuration
|
|
182
|
+
* @returns Loader instance
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* import { useLoader } from '@kb-labs/sdk';
|
|
187
|
+
*
|
|
188
|
+
* const loader = useLoader('Processing data...');
|
|
189
|
+
* loader.start();
|
|
190
|
+
* // ... do work ...
|
|
191
|
+
* loader.succeed('Processing complete!');
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function useLoader(text: string, options?: Partial<LoaderOptions>): Loader {
|
|
195
|
+
return new Loader({ text, jsonMode: _jsonMode, ...options });
|
|
196
|
+
}
|