@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,310 @@
1
+ import { box, keyValue, safeKeyValue, hasAnsi } from './format';
2
+ import { formatTiming } from './command-output';
3
+ import { safeColors, safeSymbols } from './colors';
4
+ import {
5
+ displayArtifacts,
6
+ type ArtifactInfo,
7
+ type ArtifactDisplayOptions,
8
+ } from './artifacts-display';
9
+ import { TimingTracker } from './timing-tracker';
10
+
11
+ // Optional analytics types - using interface to avoid type resolution errors
12
+ // when @kb-labs/analytics-sdk-node is not installed
13
+ interface AnalyticsRunScope {
14
+ (options: { runId?: string; actor?: unknown; ctx?: Record<string, unknown> }, fn: (emit: AnalyticsEmit) => Promise<unknown>): Promise<unknown>;
15
+ }
16
+
17
+ interface AnalyticsEmit {
18
+ (event: Partial<{ type: string; payload: Record<string, unknown> }>): Promise<unknown>;
19
+ }
20
+
21
+ export interface CommandPresenter {
22
+ info(message: string): void;
23
+ warn?(message: string): void;
24
+ error(message: string): void;
25
+ write(payload: string): void;
26
+ json(payload: unknown): void;
27
+ }
28
+
29
+ export interface CommandContext {
30
+ cwd: string;
31
+ presenter: CommandPresenter;
32
+ }
33
+
34
+ export interface CommandExecutionResult {
35
+ summary: Record<string, string | number>;
36
+ artifacts?: ArtifactInfo[];
37
+ artifactsOptions?: ArtifactDisplayOptions;
38
+ timing?: number | Record<string, number>;
39
+ diagnostics?: string[];
40
+ warnings?: string[];
41
+ errors?: string[];
42
+ data?: Record<string, unknown>;
43
+ }
44
+
45
+ export interface AnalyticsConfig {
46
+ actor: string;
47
+ started: string;
48
+ finished: string;
49
+ getPayload?: (flags: Record<string, unknown>, result?: CommandExecutionResult) => Record<string, unknown>;
50
+ }
51
+
52
+ export interface CommandRunnerOptions {
53
+ title: string;
54
+ analytics?: AnalyticsConfig;
55
+ execute: (ctx: CommandContext, flags: Record<string, unknown>, tracker: TimingTracker) => Promise<CommandExecutionResult>;
56
+ }
57
+
58
+ export function createCommandRunner(options: CommandRunnerOptions) {
59
+ return async (ctx: CommandContext, argv: string[], flags: Record<string, unknown>): Promise<number> => {
60
+ const tracker = new TimingTracker();
61
+ const jsonMode = flags.json === true;
62
+ const quietMode = flags.quiet === true;
63
+ const start = Date.now();
64
+
65
+ let runScope: AnalyticsRunScope | null = null;
66
+ if (options.analytics !== undefined) {
67
+ try {
68
+ // Dynamic import with type assertion to avoid TypeScript errors
69
+ // @kb-labs/analytics-sdk-node is an optional dependency
70
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
71
+ // @ts-ignore - optional dependency, types may not be available
72
+ const analyticsModule = await import('@kb-labs/analytics-sdk-node') as {
73
+ runScope?: AnalyticsRunScope;
74
+ };
75
+ if (analyticsModule.runScope) {
76
+ runScope = analyticsModule.runScope;
77
+ }
78
+ } catch {
79
+ // Analytics SDK not available, continue without it
80
+ runScope = null;
81
+ }
82
+ }
83
+
84
+ const execute = async (): Promise<number> => {
85
+ tracker.checkpoint('start');
86
+ try {
87
+ const result = await options.execute(ctx, flags, tracker);
88
+ tracker.checkpoint('complete');
89
+ formatSuccessOutput(ctx, options.title, result, tracker.total(), {
90
+ jsonMode,
91
+ quietMode,
92
+ });
93
+ return 0;
94
+ } catch (error) {
95
+ tracker.checkpoint('error');
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ formatErrorOutput(ctx, message, tracker.total(), {
98
+ jsonMode,
99
+ quietMode,
100
+ });
101
+ return 1;
102
+ }
103
+ };
104
+
105
+ if (!runScope || !options.analytics) {
106
+ return execute();
107
+ }
108
+
109
+ const totalStart = Date.now();
110
+ const result = await runScope(
111
+ {
112
+ actor: options.analytics.actor as never,
113
+ ctx: { workspace: ctx.cwd },
114
+ },
115
+ async (emit: AnalyticsEmit) => {
116
+ await emit({
117
+ type: options.analytics!.started,
118
+ payload: options.analytics!.getPayload?.(flags) ?? {},
119
+ });
120
+
121
+ let exitCode = 0;
122
+ try {
123
+ exitCode = await execute();
124
+ await emit({
125
+ type: options.analytics!.finished,
126
+ payload: {
127
+ ...(options.analytics!.getPayload?.(flags) ?? {}),
128
+ durationMs: Date.now() - totalStart,
129
+ result: exitCode === 0 ? 'success' : 'error',
130
+ },
131
+ });
132
+ } catch (error) {
133
+ await emit({
134
+ type: options.analytics!.finished,
135
+ payload: {
136
+ ...(options.analytics!.getPayload?.(flags) ?? {}),
137
+ durationMs: Date.now() - totalStart,
138
+ result: 'error',
139
+ error: error instanceof Error ? error.message : String(error),
140
+ },
141
+ });
142
+ throw error;
143
+ }
144
+
145
+ return exitCode;
146
+ },
147
+ );
148
+
149
+ return result as number;
150
+ };
151
+ }
152
+
153
+ interface OutputFlags {
154
+ jsonMode: boolean;
155
+ quietMode: boolean;
156
+ }
157
+
158
+ function formatSuccessOutput(
159
+ ctx: CommandContext,
160
+ title: string,
161
+ result: CommandExecutionResult,
162
+ durationMs: number,
163
+ flags: OutputFlags,
164
+ ): void {
165
+ if (flags.jsonMode) {
166
+ const payload: Record<string, unknown> = {
167
+ ok: true,
168
+ summary: result.summary,
169
+ timing: result.timing ?? durationMs,
170
+ diagnostics: result.diagnostics,
171
+ warnings: result.warnings,
172
+ errors: result.errors,
173
+ };
174
+ if (result.artifacts) {
175
+ payload.artifacts = result.artifacts;
176
+ }
177
+ if (result.data) {
178
+ Object.assign(payload, result.data);
179
+ }
180
+ ctx.presenter.json(payload);
181
+ return;
182
+ }
183
+
184
+ if (flags.quietMode) {
185
+ return;
186
+ }
187
+
188
+ const statusEntry = extractStatusEntry(result.summary);
189
+ const summaryForDisplay: Record<string, string | number> = statusEntry
190
+ ? { ...result.summary }
191
+ : result.summary;
192
+
193
+ if (statusEntry) {
194
+ delete summaryForDisplay[statusEntry.key];
195
+ }
196
+
197
+ const lines: string[] = [];
198
+ const summaryLines = keyValue(summaryForDisplay);
199
+ if (summaryLines.length > 0) {
200
+ lines.push(...summaryLines);
201
+ }
202
+
203
+ if (result.artifacts && result.artifacts.length > 0) {
204
+ if (lines.length > 0) {
205
+ lines.push('');
206
+ }
207
+ lines.push(...displayArtifacts(result.artifacts, result.artifactsOptions));
208
+ }
209
+
210
+ const timing = result.timing ?? durationMs;
211
+ if (typeof timing !== 'number') {
212
+ const entries = Object.entries(timing);
213
+ if (entries.length > 0) {
214
+ if (lines.length > 0) {
215
+ lines.push('');
216
+ }
217
+ lines.push(safeColors.bold('Timing'));
218
+ for (const [label, value] of entries) {
219
+ lines.push(...safeKeyValue({ [label]: formatTiming(value) }, { indent: 2, pad: false }));
220
+ }
221
+ }
222
+ }
223
+
224
+ if (result.diagnostics && result.diagnostics.length > 0) {
225
+ if (lines.length > 0) {
226
+ lines.push('');
227
+ }
228
+ lines.push(safeColors.bold('Diagnostics'));
229
+ result.diagnostics.forEach((item) => lines.push(safeColors.muted(`- ${item}`)));
230
+ }
231
+
232
+ if (result.warnings && result.warnings.length > 0) {
233
+ if (lines.length > 0) {
234
+ lines.push('');
235
+ }
236
+ lines.push(safeColors.bold('Warnings'));
237
+ result.warnings.forEach((item) => lines.push(safeColors.muted(`- ${item}`)));
238
+ }
239
+
240
+ if (result.errors && result.errors.length > 0) {
241
+ if (lines.length > 0) {
242
+ lines.push('');
243
+ }
244
+ lines.push(safeColors.bold('Errors'));
245
+ result.errors.forEach((item) => lines.push(safeColors.muted(`- ${item}`)));
246
+ }
247
+
248
+ if (lines.length > 0) {
249
+ lines.push('');
250
+ }
251
+ lines.push(formatStatusLine(statusEntry?.value, durationMs));
252
+
253
+ ctx.presenter.write(box(title, lines));
254
+ }
255
+
256
+ function formatErrorOutput(
257
+ ctx: CommandContext,
258
+ errorMessage: string,
259
+ durationMs: number,
260
+ flags: OutputFlags,
261
+ ): void {
262
+ if (flags.jsonMode) {
263
+ ctx.presenter.json({
264
+ ok: false,
265
+ error: errorMessage,
266
+ timing: durationMs,
267
+ });
268
+ return;
269
+ }
270
+
271
+ if (flags.quietMode) {
272
+ return;
273
+ }
274
+
275
+ ctx.presenter.error(errorMessage);
276
+ }
277
+
278
+ interface StatusEntry {
279
+ key: string;
280
+ value: string;
281
+ }
282
+
283
+ function extractStatusEntry(summary: Record<string, string | number>): StatusEntry | null {
284
+ for (const [key, value] of Object.entries(summary)) {
285
+ if (typeof value === 'string' && key.toLowerCase().includes('status')) {
286
+ return { key, value };
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+
292
+ function formatStatusLine(statusValue: string | undefined, durationMs: number): string {
293
+ const rawStatus = statusValue ?? 'Done';
294
+ const normalized = rawStatus.toLowerCase();
295
+
296
+ let symbol = safeSymbols.success;
297
+ let colorize = safeColors.success;
298
+
299
+ if (normalized.includes('error') || normalized.includes('fail')) {
300
+ symbol = safeSymbols.error;
301
+ colorize = safeColors.error;
302
+ } else if (normalized.includes('partial') || normalized.includes('warn')) {
303
+ symbol = safeSymbols.warning;
304
+ colorize = safeColors.warning;
305
+ }
306
+
307
+ const statusText = hasAnsi(rawStatus) ? rawStatus : colorize(rawStatus);
308
+ return `${symbol} ${statusText} · ${safeColors.muted(formatTiming(durationMs))}`;
309
+ }
310
+
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Command suggestions and validation utilities
3
+ */
4
+
5
+ export interface CommandSuggestion {
6
+ id: string;
7
+ command: string;
8
+ args: string[];
9
+ description: string;
10
+ impact: 'safe' | 'disruptive';
11
+ when: string;
12
+ available?: boolean;
13
+ }
14
+
15
+ export interface CommandRegistry {
16
+ commands: Set<string>;
17
+ groups: Map<string, Set<string>>;
18
+ }
19
+
20
+ /**
21
+ * Create a command registry from available commands
22
+ */
23
+ export function createCommandRegistry(commands: string[]): CommandRegistry {
24
+ const commandSet = new Set(commands);
25
+ const groups = new Map<string, Set<string>>();
26
+
27
+ // Group commands by prefix (e.g., "devlink:apply" -> group "devlink")
28
+ for (const cmd of commands) {
29
+ const [group, ...rest] = cmd.split(':');
30
+ if (group && rest.length > 0) {
31
+ if (!groups.has(group)) {
32
+ groups.set(group, new Set());
33
+ }
34
+ groups.get(group)!.add(rest.join(':'));
35
+ }
36
+ }
37
+
38
+ return {
39
+ commands: commandSet,
40
+ groups
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Check if a command is available in the registry
46
+ */
47
+ export function isCommandAvailable(
48
+ command: string,
49
+ registry: CommandRegistry
50
+ ): boolean {
51
+ // Handle full command strings like "kb devlink apply"
52
+ if (command.startsWith('kb ')) {
53
+ const parts = command.split(' ');
54
+ if (parts.length >= 3 && parts[1] && parts[2]) {
55
+ const group = parts[1];
56
+ const subcommand = parts[2];
57
+ const fullCommand = `${group}:${subcommand}`;
58
+ return registry.commands.has(fullCommand);
59
+ }
60
+ }
61
+
62
+ // Handle direct command names like "devlink:apply"
63
+ return registry.commands.has(command);
64
+ }
65
+
66
+ /**
67
+ * Validate suggestions against available commands
68
+ */
69
+ export function validateSuggestions(
70
+ suggestions: CommandSuggestion[],
71
+ registry: CommandRegistry
72
+ ): CommandSuggestion[] {
73
+ return suggestions.map(suggestion => ({
74
+ ...suggestion,
75
+ available: isCommandAvailable(suggestion.command, registry)
76
+ })).filter(suggestion => suggestion.available);
77
+ }
78
+
79
+ /**
80
+ * Generate common devlink suggestions
81
+ */
82
+ export function generateDevlinkSuggestions(
83
+ warningCodes: Set<string>,
84
+ context: { undo?: { available: boolean } },
85
+ registry: CommandRegistry
86
+ ): CommandSuggestion[] {
87
+ const suggestions: CommandSuggestion[] = [];
88
+
89
+ // LOCK_MISMATCH -> apply
90
+ if (warningCodes.has('LOCK_MISMATCH')) {
91
+ suggestions.push({
92
+ id: 'SYNC_LOCK',
93
+ command: 'kb devlink apply',
94
+ args: ['--yes'],
95
+ description: 'Apply changes to sync manifests',
96
+ impact: 'safe',
97
+ when: 'LOCK_MISMATCH'
98
+ });
99
+ }
100
+
101
+ // BACKUP_MISSING -> freeze
102
+ if (warningCodes.has('BACKUP_MISSING')) {
103
+ suggestions.push({
104
+ id: 'CREATE_BACKUP',
105
+ command: 'kb devlink freeze',
106
+ args: ['--replace'],
107
+ description: 'Create a fresh backup',
108
+ impact: 'safe',
109
+ when: 'BACKUP_MISSING'
110
+ });
111
+ }
112
+
113
+ // STALE_LOCK -> freeze
114
+ if (warningCodes.has('STALE_LOCK')) {
115
+ suggestions.push({
116
+ id: 'REFRESH_LOCK',
117
+ command: 'kb devlink freeze',
118
+ args: ['--pin=caret'],
119
+ description: 'Refresh lock file',
120
+ impact: 'safe',
121
+ when: 'STALE_LOCK'
122
+ });
123
+ }
124
+
125
+ // STALE_YALC_ARTIFACTS -> clean
126
+ if (warningCodes.has('STALE_YALC_ARTIFACTS')) {
127
+ suggestions.push({
128
+ id: 'CLEAN_YALC',
129
+ command: 'kb devlink clean',
130
+ args: [],
131
+ description: 'Remove yalc artifacts',
132
+ impact: 'safe',
133
+ when: 'STALE_YALC_ARTIFACTS'
134
+ });
135
+ }
136
+
137
+ // PROTOCOL_CONFLICTS -> clean --hard
138
+ if (warningCodes.has('PROTOCOL_CONFLICTS')) {
139
+ suggestions.push({
140
+ id: 'FIX_PROTOCOLS',
141
+ command: 'kb devlink clean',
142
+ args: ['--hard'],
143
+ description: 'Reset all protocols and reapply',
144
+ impact: 'disruptive',
145
+ when: 'PROTOCOL_CONFLICTS'
146
+ });
147
+ }
148
+
149
+ // Undo suggestion
150
+ if (context.undo?.available) {
151
+ suggestions.push({
152
+ id: 'UNDO_LAST',
153
+ command: 'kb devlink undo',
154
+ args: [],
155
+ description: 'Revert last operation',
156
+ impact: 'safe',
157
+ when: 'BACKUP_AVAILABLE'
158
+ });
159
+ }
160
+
161
+ // Validate and return only available suggestions
162
+ return validateSuggestions(suggestions, registry);
163
+ }
164
+
165
+
166
+ /**
167
+ * Generate quick actions for common scenarios
168
+ */
169
+ export function generateQuickActions(
170
+ hasWarnings: boolean,
171
+ registry: CommandRegistry,
172
+ group: string = 'devlink'
173
+ ): CommandSuggestion[] {
174
+ if (!hasWarnings) {return [];}
175
+
176
+ const quickActions: CommandSuggestion[] = [
177
+ {
178
+ id: 'QUICK_CLEAN',
179
+ command: `kb ${group} clean`,
180
+ args: [],
181
+ description: 'Clean artifacts',
182
+ impact: 'safe',
183
+ when: 'QUICK_ACTIONS'
184
+ },
185
+ {
186
+ id: 'QUICK_PLAN',
187
+ command: `kb ${group} plan`,
188
+ args: [],
189
+ description: 'Create plan',
190
+ impact: 'safe',
191
+ when: 'QUICK_ACTIONS'
192
+ },
193
+ {
194
+ id: 'QUICK_APPLY',
195
+ command: `kb ${group} apply`,
196
+ args: [],
197
+ description: 'Apply changes',
198
+ impact: 'safe',
199
+ when: 'QUICK_ACTIONS'
200
+ }
201
+ ];
202
+
203
+ return validateSuggestions(quickActions, registry);
204
+ }
@@ -0,0 +1,141 @@
1
+ import { box } from '../../format';
2
+ import { formatDebugEntryAI, formatDebugEntriesAI, shouldUseAIFormat } from '../formatters/ai';
3
+ import {
4
+ formatDebugEntryHuman,
5
+ formatDebugEntriesHuman,
6
+ type HumanFormatterOptions,
7
+ } from '../formatters/human';
8
+ import { formatTimelineWithSummary } from '../formatters/timeline';
9
+ import type { DebugDetailLevel, DebugEntry, DebugFormat } from '../types';
10
+
11
+ export interface DebugOutputOptions extends HumanFormatterOptions {
12
+ format?: DebugFormat;
13
+ detailLevel?: DebugDetailLevel;
14
+ useTimeline?: boolean;
15
+ }
16
+
17
+ export function formatDebugOutput(entry: DebugEntry, options: DebugOutputOptions = {}): string {
18
+ if (shouldUseAIFormat(options.format)) {
19
+ return formatDebugEntryAI(entry);
20
+ }
21
+
22
+ return formatDebugEntryHuman(entry, options);
23
+ }
24
+
25
+ export function formatDebugOutputs(entries: DebugEntry[], options: DebugOutputOptions = {}): string {
26
+ if (shouldUseAIFormat(options.format)) {
27
+ return formatDebugEntriesAI(entries);
28
+ }
29
+
30
+ if (options.useTimeline) {
31
+ return formatTimelineWithSummary(entries);
32
+ }
33
+
34
+ return formatDebugEntriesHuman(entries, options);
35
+ }
36
+
37
+ export class DebugSection {
38
+ private readonly entries: DebugEntry[] = [];
39
+ private readonly title: string;
40
+ private readonly options: DebugOutputOptions;
41
+
42
+ constructor(title: string, options: DebugOutputOptions = {}) {
43
+ this.title = title;
44
+ this.options = options;
45
+ }
46
+
47
+ add(entry: DebugEntry): void {
48
+ this.entries.push(entry);
49
+ }
50
+
51
+ addAll(entries: DebugEntry[]): void {
52
+ this.entries.push(...entries);
53
+ }
54
+
55
+ clear(): void {
56
+ this.entries.length = 0;
57
+ }
58
+
59
+ format(): string {
60
+ if (this.entries.length === 0) {
61
+ return '';
62
+ }
63
+
64
+ const content = formatDebugOutputs(this.entries, this.options);
65
+ if (!content) {
66
+ return '';
67
+ }
68
+
69
+ if (shouldUseAIFormat(this.options.format)) {
70
+ return content;
71
+ }
72
+
73
+ return box(this.title, content.split('\n'));
74
+ }
75
+
76
+ get count(): number {
77
+ return this.entries.length;
78
+ }
79
+ }
80
+
81
+ export class DebugOutput {
82
+ private readonly sections = new Map<string, DebugSection>();
83
+ private readonly globalEntries: DebugEntry[] = [];
84
+ private readonly options: DebugOutputOptions;
85
+
86
+ constructor(options: DebugOutputOptions = {}) {
87
+ this.options = options;
88
+ }
89
+
90
+ add(entry: DebugEntry): void {
91
+ this.globalEntries.push(entry);
92
+ }
93
+
94
+ addToSection(sectionName: string, entry: DebugEntry): void {
95
+ if (!this.sections.has(sectionName)) {
96
+ this.sections.set(sectionName, new DebugSection(sectionName, this.options));
97
+ }
98
+ this.sections.get(sectionName)!.add(entry);
99
+ }
100
+
101
+ getSection(sectionName: string): DebugSection {
102
+ if (!this.sections.has(sectionName)) {
103
+ this.sections.set(sectionName, new DebugSection(sectionName, this.options));
104
+ }
105
+ return this.sections.get(sectionName)!;
106
+ }
107
+
108
+ format(): string {
109
+ const parts: string[] = [];
110
+
111
+ if (this.globalEntries.length > 0) {
112
+ const formatted = formatDebugOutputs(this.globalEntries, this.options);
113
+ if (formatted) {
114
+ parts.push(formatted);
115
+ }
116
+ }
117
+
118
+ for (const [, section] of this.sections) {
119
+ const formatted = section.format();
120
+ if (formatted) {
121
+ parts.push(formatted);
122
+ }
123
+ }
124
+
125
+ return parts.join('\n\n');
126
+ }
127
+
128
+ clear(): void {
129
+ this.globalEntries.length = 0;
130
+ this.sections.clear();
131
+ }
132
+
133
+ get totalCount(): number {
134
+ let count = this.globalEntries.length;
135
+ for (const section of this.sections.values()) {
136
+ count += section.count;
137
+ }
138
+ return count;
139
+ }
140
+ }
141
+