@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.
Files changed (232) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/.vscode/settings.json +23 -0
  21. package/CHANGELOG.md +33 -0
  22. package/CONTRIBUTING.md +117 -0
  23. package/LICENSE +21 -0
  24. package/README.md +306 -0
  25. package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
  26. package/docs/DOCUMENTATION.md +70 -0
  27. package/docs/adr/0000-template.md +52 -0
  28. package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
  29. package/docs/adr/0002-plugins-and-extensibility.md +44 -0
  30. package/docs/adr/0003-package-and-module-boundaries.md +35 -0
  31. package/docs/adr/0004-versioning-and-release-policy.md +36 -0
  32. package/docs/adr/0005-reactive-loader-pattern.md +179 -0
  33. package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
  34. package/eslint.config.js +27 -0
  35. package/kb-labs.config.json +5 -0
  36. package/package.json +88 -0
  37. package/package.json.bin +25 -0
  38. package/package.json.lib +30 -0
  39. package/packages/shared-cli-ui/CHANGELOG.md +20 -0
  40. package/packages/shared-cli-ui/README.md +342 -0
  41. package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
  42. package/packages/shared-cli-ui/eslint.config.js +27 -0
  43. package/packages/shared-cli-ui/package.json +72 -0
  44. package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
  45. package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
  46. package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
  47. package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
  48. package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
  49. package/packages/shared-cli-ui/src/colors.ts +142 -0
  50. package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
  51. package/packages/shared-cli-ui/src/command-output.ts +153 -0
  52. package/packages/shared-cli-ui/src/command-result.ts +267 -0
  53. package/packages/shared-cli-ui/src/command-runner.ts +310 -0
  54. package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
  55. package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
  56. package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
  57. package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
  58. package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
  59. package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
  60. package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
  61. package/packages/shared-cli-ui/src/debug/index.ts +56 -0
  62. package/packages/shared-cli-ui/src/debug/types.ts +57 -0
  63. package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
  64. package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
  65. package/packages/shared-cli-ui/src/format.ts +412 -0
  66. package/packages/shared-cli-ui/src/index.ts +34 -0
  67. package/packages/shared-cli-ui/src/loader.ts +196 -0
  68. package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
  69. package/packages/shared-cli-ui/src/modern-format.ts +271 -0
  70. package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
  71. package/packages/shared-cli-ui/src/table.ts +134 -0
  72. package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
  73. package/packages/shared-cli-ui/src/utils/context.ts +12 -0
  74. package/packages/shared-cli-ui/src/utils/env.ts +164 -0
  75. package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
  76. package/packages/shared-cli-ui/src/utils/path.ts +8 -0
  77. package/packages/shared-cli-ui/tsconfig.build.json +15 -0
  78. package/packages/shared-cli-ui/tsconfig.json +9 -0
  79. package/packages/shared-cli-ui/tsup.config.ts +11 -0
  80. package/packages/shared-cli-ui/vitest.config.ts +15 -0
  81. package/packages/shared-command-kit/CHANGELOG.md +20 -0
  82. package/packages/shared-command-kit/LICENSE +22 -0
  83. package/packages/shared-command-kit/README.md +1030 -0
  84. package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
  85. package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
  86. package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
  87. package/packages/shared-command-kit/eslint.config.js +27 -0
  88. package/packages/shared-command-kit/eslint.config.ts +14 -0
  89. package/packages/shared-command-kit/package.json +76 -0
  90. package/packages/shared-command-kit/prettierrc.json +5 -0
  91. package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
  92. package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
  93. package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
  94. package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
  95. package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
  96. package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
  97. package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
  98. package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
  99. package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
  100. package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
  101. package/packages/shared-command-kit/src/analytics/index.ts +6 -0
  102. package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
  103. package/packages/shared-command-kit/src/define-action.ts +100 -0
  104. package/packages/shared-command-kit/src/define-command.ts +113 -0
  105. package/packages/shared-command-kit/src/define-route.ts +113 -0
  106. package/packages/shared-command-kit/src/define-system-command.ts +362 -0
  107. package/packages/shared-command-kit/src/define-webhook.ts +115 -0
  108. package/packages/shared-command-kit/src/define-websocket.ts +308 -0
  109. package/packages/shared-command-kit/src/errors/factory.ts +282 -0
  110. package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
  111. package/packages/shared-command-kit/src/errors/format.ts +92 -0
  112. package/packages/shared-command-kit/src/errors/index.ts +9 -0
  113. package/packages/shared-command-kit/src/errors/types.ts +32 -0
  114. package/packages/shared-command-kit/src/flags/define.ts +92 -0
  115. package/packages/shared-command-kit/src/flags/index.ts +9 -0
  116. package/packages/shared-command-kit/src/flags/types.ts +153 -0
  117. package/packages/shared-command-kit/src/flags/validate.ts +358 -0
  118. package/packages/shared-command-kit/src/helpers/context.ts +8 -0
  119. package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
  120. package/packages/shared-command-kit/src/helpers/index.ts +42 -0
  121. package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
  122. package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
  123. package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
  124. package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
  125. package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
  126. package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
  127. package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
  128. package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
  129. package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
  130. package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
  131. package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
  132. package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
  133. package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
  134. package/packages/shared-command-kit/src/index.ts +410 -0
  135. package/packages/shared-command-kit/src/jobs.ts +132 -0
  136. package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
  137. package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
  138. package/packages/shared-command-kit/src/manifest.ts +127 -0
  139. package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
  140. package/packages/shared-command-kit/src/rest/index.ts +11 -0
  141. package/packages/shared-command-kit/src/studio/index.ts +12 -0
  142. package/packages/shared-command-kit/src/validation/index.ts +6 -0
  143. package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
  144. package/packages/shared-command-kit/src/ws-types.ts +106 -0
  145. package/packages/shared-command-kit/tsconfig.build.json +15 -0
  146. package/packages/shared-command-kit/tsconfig.json +9 -0
  147. package/packages/shared-command-kit/tsup.config.ts +30 -0
  148. package/packages/shared-command-kit/vitest.config.ts +4 -0
  149. package/packages/shared-http/package.json +67 -0
  150. package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
  151. package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
  152. package/packages/shared-http/src/http-observability-collector.ts +363 -0
  153. package/packages/shared-http/src/index.ts +36 -0
  154. package/packages/shared-http/src/log-correlation.ts +89 -0
  155. package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
  156. package/packages/shared-http/src/register-openapi.ts +108 -0
  157. package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
  158. package/packages/shared-http/src/schemas.ts +29 -0
  159. package/packages/shared-http/src/service-observability.ts +63 -0
  160. package/packages/shared-http/tsconfig.build.json +15 -0
  161. package/packages/shared-http/tsconfig.json +9 -0
  162. package/packages/shared-http/tsup.config.ts +23 -0
  163. package/packages/shared-http/vitest.config.ts +13 -0
  164. package/packages/shared-perm-presets/CHANGELOG.md +20 -0
  165. package/packages/shared-perm-presets/README.md +78 -0
  166. package/packages/shared-perm-presets/eslint.config.js +27 -0
  167. package/packages/shared-perm-presets/package.json +45 -0
  168. package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
  169. package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
  170. package/packages/shared-perm-presets/src/combine.ts +278 -0
  171. package/packages/shared-perm-presets/src/index.ts +18 -0
  172. package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
  173. package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
  174. package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
  175. package/packages/shared-perm-presets/src/presets/index.ts +8 -0
  176. package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
  177. package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
  178. package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
  179. package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
  180. package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
  181. package/packages/shared-perm-presets/src/types.ts +192 -0
  182. package/packages/shared-perm-presets/tsconfig.build.json +15 -0
  183. package/packages/shared-perm-presets/tsconfig.json +9 -0
  184. package/packages/shared-perm-presets/tsup.config.ts +8 -0
  185. package/packages/shared-perm-presets/vitest.config.ts +9 -0
  186. package/packages/shared-testing/CHANGELOG.md +20 -0
  187. package/packages/shared-testing/README.md +430 -0
  188. package/packages/shared-testing/package.json +51 -0
  189. package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
  190. package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
  191. package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
  192. package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
  193. package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
  194. package/packages/shared-testing/src/create-test-context.ts +550 -0
  195. package/packages/shared-testing/src/index.ts +77 -0
  196. package/packages/shared-testing/src/mock-cache.ts +179 -0
  197. package/packages/shared-testing/src/mock-llm.ts +319 -0
  198. package/packages/shared-testing/src/mock-logger.ts +97 -0
  199. package/packages/shared-testing/src/mock-storage.ts +108 -0
  200. package/packages/shared-testing/src/setup-platform.ts +101 -0
  201. package/packages/shared-testing/src/test-command.ts +288 -0
  202. package/packages/shared-testing/tsconfig.build.json +15 -0
  203. package/packages/shared-testing/tsconfig.json +9 -0
  204. package/packages/shared-testing/tsup.config.ts +20 -0
  205. package/packages/shared-testing/vitest.config.ts +3 -0
  206. package/packages/shared-tool-kit/CHANGELOG.md +20 -0
  207. package/packages/shared-tool-kit/package.json +47 -0
  208. package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
  209. package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
  210. package/packages/shared-tool-kit/src/factory.ts +126 -0
  211. package/packages/shared-tool-kit/src/index.ts +32 -0
  212. package/packages/shared-tool-kit/src/testing/index.ts +84 -0
  213. package/packages/shared-tool-kit/tsconfig.build.json +15 -0
  214. package/packages/shared-tool-kit/tsconfig.json +9 -0
  215. package/packages/shared-tool-kit/tsup.config.ts +21 -0
  216. package/pnpm-workspace.yaml +11070 -0
  217. package/prettierrc.json +1 -0
  218. package/scripts/devkit-sync.mjs +37 -0
  219. package/scripts/hooks/post-push +9 -0
  220. package/scripts/hooks/pre-commit +9 -0
  221. package/scripts/hooks/pre-push +9 -0
  222. package/tsconfig.base.json +9 -0
  223. package/tsconfig.build.json +15 -0
  224. package/tsconfig.json +9 -0
  225. package/tsconfig.paths.json +50 -0
  226. package/tsconfig.tools.json +18 -0
  227. package/tsup.config.bin.ts +34 -0
  228. package/tsup.config.cli.ts +41 -0
  229. package/tsup.config.dual.ts +46 -0
  230. package/tsup.config.ts +36 -0
  231. package/tsup.external.json +104 -0
  232. 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
+ }