@ekanos/cli 0.1.10 → 0.2.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 (55) hide show
  1. package/dist/banner.d.ts +5 -1
  2. package/dist/banner.js +7 -2
  3. package/dist/banner.js.map +1 -1
  4. package/dist/commands/init.d.ts +13 -1
  5. package/dist/commands/init.js +70 -13
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/login.d.ts +13 -0
  8. package/dist/commands/login.js +41 -10
  9. package/dist/commands/login.js.map +1 -1
  10. package/dist/commands/publish.js +55 -3
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/commands/status.d.ts +68 -2
  13. package/dist/commands/status.js +312 -43
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/test.d.ts +27 -5
  16. package/dist/commands/test.js +85 -9
  17. package/dist/commands/test.js.map +1 -1
  18. package/dist/commands/validate.js +115 -3
  19. package/dist/commands/validate.js.map +1 -1
  20. package/dist/context.d.ts +37 -1
  21. package/dist/context.js +27 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/did-you-mean.d.ts +36 -0
  24. package/dist/did-you-mean.js +192 -0
  25. package/dist/did-you-mean.js.map +1 -0
  26. package/dist/findings-renderer.d.ts +59 -0
  27. package/dist/findings-renderer.js +271 -0
  28. package/dist/findings-renderer.js.map +1 -0
  29. package/dist/index.d.ts +11 -4
  30. package/dist/index.js +23 -5
  31. package/dist/index.js.map +1 -1
  32. package/dist/project-checks.d.ts +30 -0
  33. package/dist/project-checks.js +162 -0
  34. package/dist/project-checks.js.map +1 -1
  35. package/dist/publish-api.d.ts +13 -0
  36. package/dist/publish-api.js.map +1 -1
  37. package/dist/source-locator.d.ts +67 -0
  38. package/dist/source-locator.js +284 -0
  39. package/dist/source-locator.js.map +1 -0
  40. package/dist/test-report.d.ts +32 -0
  41. package/dist/test-report.js +255 -0
  42. package/dist/test-report.js.map +1 -0
  43. package/dist/ui.d.ts +75 -0
  44. package/dist/ui.js +132 -0
  45. package/dist/ui.js.map +1 -0
  46. package/dist/validate-findings.d.ts +22 -0
  47. package/dist/validate-findings.js +57 -1
  48. package/dist/validate-findings.js.map +1 -1
  49. package/dist/wizard.d.ts +80 -0
  50. package/dist/wizard.js +74 -0
  51. package/dist/wizard.js.map +1 -0
  52. package/package.json +4 -2
  53. package/templates/package.json.tmpl +3 -1
  54. package/templates/scripts/setup-hooks.mjs.tmpl +45 -0
  55. package/templates/tsconfig.json.tmpl +2 -0
@@ -0,0 +1,192 @@
1
+ import { IntegrationDefinitionSchema } from '@ekanos/integration-schema';
2
+ import { isRootPath, splitDotPath } from './source-locator.js';
3
+ /**
4
+ * "Did you mean?" for a zod `unrecognized_keys` finding — the typo case
5
+ * (`entrypiont` instead of `entrypoint`). Two pieces: a hand-rolled
6
+ * Levenshtein distance (no new dependency — this package deliberately keeps
7
+ * its runtime dependency set to picocolors + @clack/prompts only) and a
8
+ * structural walk of the REAL `IntegrationDefinitionSchema` to find the
9
+ * sibling keys a given path actually accepts.
10
+ */
11
+ /**
12
+ * Classic single-rolling-row DP, O(a.length * b.length). Fine at CLI scale —
13
+ * this compares key names, never paragraphs.
14
+ */
15
+ export function levenshteinDistance(a, b) {
16
+ var _a, _b, _c, _d;
17
+ if (a === b)
18
+ return 0;
19
+ if (a.length === 0)
20
+ return b.length;
21
+ if (b.length === 0)
22
+ return a.length;
23
+ let previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);
24
+ for (let i = 1; i <= a.length; i += 1) {
25
+ const currentRow = [i];
26
+ for (let j = 1; j <= b.length; j += 1) {
27
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
28
+ currentRow.push(Math.min(((_a = previousRow[j]) !== null && _a !== void 0 ? _a : Infinity) + 1, // deletion
29
+ ((_b = currentRow[j - 1]) !== null && _b !== void 0 ? _b : Infinity) + 1, // insertion
30
+ ((_c = previousRow[j - 1]) !== null && _c !== void 0 ? _c : Infinity) + cost));
31
+ }
32
+ previousRow = currentRow;
33
+ }
34
+ return (_d = previousRow[b.length]) !== null && _d !== void 0 ? _d : Math.max(a.length, b.length);
35
+ }
36
+ const MAX_SUGGESTION_DISTANCE = 2;
37
+ /**
38
+ * The single closest candidate within `MAX_SUGGESTION_DISTANCE`, or `null`
39
+ * when nothing is close enough. Never a wrong-confident guess for a key that
40
+ * isn't really a typo of anything this schema accepts.
41
+ */
42
+ export function suggestKey(key, candidates) {
43
+ let best = null;
44
+ let bestDistance = MAX_SUGGESTION_DISTANCE + 1;
45
+ for (const candidate of candidates) {
46
+ const distance = levenshteinDistance(key, candidate);
47
+ if (distance < bestDistance) {
48
+ bestDistance = distance;
49
+ best = candidate;
50
+ }
51
+ }
52
+ return bestDistance <= MAX_SUGGESTION_DISTANCE ? best : null;
53
+ }
54
+ /**
55
+ * Parses the literal, comma-separated, single-quoted key list zod's
56
+ * `unrecognized_keys` issue produces (`Unrecognized key(s) in object: 'a',
57
+ * 'b'`) — this exact wording is what `collectDefinitionFindings` bakes into
58
+ * `Finding.message` verbatim (see its doc comment), so this is a text match
59
+ * against a stable, committed third-party format, not a guess at prose.
60
+ * `null` for a message tail that isn't this issue.
61
+ */
62
+ export function extractUnrecognizedKeys(rest) {
63
+ const match = /^Unrecognized key\(s\) in object: (.+)$/.exec(rest);
64
+ if (!match)
65
+ return null;
66
+ const list = match[1];
67
+ if (!list)
68
+ return null;
69
+ const keys = [...list.matchAll(/'([^']*)'/g)].map((m) => { var _a; return (_a = m[1]) !== null && _a !== void 0 ? _a : ''; });
70
+ return keys.length > 0 ? keys : null;
71
+ }
72
+ function asZodLike(value) {
73
+ if (typeof value === 'object' &&
74
+ value !== null &&
75
+ '_def' in value &&
76
+ typeof value._def === 'object' &&
77
+ value._def !== null) {
78
+ return value;
79
+ }
80
+ return null;
81
+ }
82
+ /** Peels `ZodEffects` (`.superRefine`/`.refine`), `ZodOptional`, `ZodDefault`, and `ZodNullable` down to the schema they wrap. */
83
+ function unwrap(schema) {
84
+ let current = schema;
85
+ for (;;) {
86
+ if (current._def.typeName === 'ZodEffects' && current._def.schema) {
87
+ current = current._def.schema;
88
+ continue;
89
+ }
90
+ if ((current._def.typeName === 'ZodOptional' ||
91
+ current._def.typeName === 'ZodDefault' ||
92
+ current._def.typeName === 'ZodNullable') &&
93
+ current._def.innerType) {
94
+ current = current._def.innerType;
95
+ continue;
96
+ }
97
+ return current;
98
+ }
99
+ }
100
+ /**
101
+ * Walks `root` through `segments` (`.shape` for an object key, `.element`
102
+ * for a numeric array index, unwrapping wrappers at every step), returning
103
+ * the `ZodObject` found at the end — or `null` the moment it hits anything
104
+ * it can't structurally walk (a union, a record, a segment the shape
105
+ * doesn't have). The caller falls back to a curated list in that case.
106
+ */
107
+ function resolveObjectAt(root, segments) {
108
+ let current = unwrap(root);
109
+ for (const segment of segments) {
110
+ if (current._def.typeName === 'ZodObject' && current.shape) {
111
+ const next = current.shape[segment];
112
+ if (!next)
113
+ return null;
114
+ current = unwrap(next);
115
+ continue;
116
+ }
117
+ if (current._def.typeName === 'ZodArray' &&
118
+ current.element &&
119
+ /^\d+$/.test(segment)) {
120
+ current = unwrap(current.element);
121
+ continue;
122
+ }
123
+ return null;
124
+ }
125
+ return current._def.typeName === 'ZodObject' ? current : null;
126
+ }
127
+ /**
128
+ * Fallback for the two spots a partner is most likely to typo a key — the
129
+ * top-level declaration and a widget entry — used only when the structural
130
+ * walk above can't resolve the parent path. Keep in sync with
131
+ * `IntegrationDefinitionSchema`'s top-level shape and `widgetSchema` in
132
+ * `@ekanos/integration-schema`'s `integration-definition.ts`; the structural
133
+ * walk is tried first and should cover both of these in practice; this is
134
+ * the safety net for whenever it doesn't.
135
+ */
136
+ const CURATED_TOP_LEVEL_KEYS = [
137
+ 'slug',
138
+ 'name',
139
+ 'description',
140
+ 'version',
141
+ 'capabilities',
142
+ 'permissions',
143
+ 'components',
144
+ 'workspaceTargets',
145
+ 'tools',
146
+ 'storage',
147
+ 'egress',
148
+ 'webhooks',
149
+ 'schedules',
150
+ 'oauth',
151
+ 'onActivate',
152
+ 'proposes',
153
+ ];
154
+ const CURATED_WIDGET_KEYS = [
155
+ 'id',
156
+ 'name',
157
+ 'component',
158
+ 'widgetState',
159
+ 'gridSize',
160
+ 'gridPosition',
161
+ 'layouts',
162
+ 'category',
163
+ 'isCollapsible',
164
+ 'isPinnable',
165
+ 'aiFooterEnabled',
166
+ ];
167
+ function curatedKeysAt(parentDotPath) {
168
+ if (isRootPath(parentDotPath))
169
+ return CURATED_TOP_LEVEL_KEYS;
170
+ if (/^components\.widgets\.\d+$/.test(parentDotPath)) {
171
+ return CURATED_WIDGET_KEYS;
172
+ }
173
+ return null;
174
+ }
175
+ /**
176
+ * The keys `IntegrationDefinitionSchema` actually accepts at
177
+ * `parentDotPath` — the zod issue's own `path` for an `unrecognized_keys`
178
+ * finding, so this always names the OBJECT the extra key was found on, never
179
+ * the extra key itself. Structural introspection first, the curated list
180
+ * second, `[]` (no suggestion offered — never a wrong-confident one) last.
181
+ */
182
+ export function acceptedKeysAt(parentDotPath) {
183
+ var _a;
184
+ const root = asZodLike(IntegrationDefinitionSchema);
185
+ const resolved = root
186
+ ? resolveObjectAt(root, splitDotPath(parentDotPath))
187
+ : null;
188
+ if (resolved === null || resolved === void 0 ? void 0 : resolved.shape)
189
+ return Object.keys(resolved.shape);
190
+ return (_a = curatedKeysAt(parentDotPath)) !== null && _a !== void 0 ? _a : [];
191
+ }
192
+ //# sourceMappingURL=did-you-mean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"did-you-mean.js","sourceRoot":"","sources":["../src/did-you-mean.ts"],"names":[],"mappings":"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","sourcesContent":["import { IntegrationDefinitionSchema } from '@ekanos/integration-schema';\n\nimport { isRootPath, splitDotPath } from './source-locator';\n\n/**\n * \"Did you mean?\" for a zod `unrecognized_keys` finding — the typo case\n * (`entrypiont` instead of `entrypoint`). Two pieces: a hand-rolled\n * Levenshtein distance (no new dependency — this package deliberately keeps\n * its runtime dependency set to picocolors + @clack/prompts only) and a\n * structural walk of the REAL `IntegrationDefinitionSchema` to find the\n * sibling keys a given path actually accepts.\n */\n\n/**\n * Classic single-rolling-row DP, O(a.length * b.length). Fine at CLI scale —\n * this compares key names, never paragraphs.\n */\nexport function levenshteinDistance(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n let previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);\n\n for (let i = 1; i <= a.length; i += 1) {\n const currentRow = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n currentRow.push(\n Math.min(\n (previousRow[j] ?? Infinity) + 1, // deletion\n (currentRow[j - 1] ?? Infinity) + 1, // insertion\n (previousRow[j - 1] ?? Infinity) + cost, // substitution\n ),\n );\n }\n previousRow = currentRow;\n }\n\n return previousRow[b.length] ?? Math.max(a.length, b.length);\n}\n\nconst MAX_SUGGESTION_DISTANCE = 2;\n\n/**\n * The single closest candidate within `MAX_SUGGESTION_DISTANCE`, or `null`\n * when nothing is close enough. Never a wrong-confident guess for a key that\n * isn't really a typo of anything this schema accepts.\n */\nexport function suggestKey(\n key: string,\n candidates: readonly string[],\n): string | null {\n let best: string | null = null;\n let bestDistance = MAX_SUGGESTION_DISTANCE + 1;\n\n for (const candidate of candidates) {\n const distance = levenshteinDistance(key, candidate);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = candidate;\n }\n }\n\n return bestDistance <= MAX_SUGGESTION_DISTANCE ? best : null;\n}\n\n/**\n * Parses the literal, comma-separated, single-quoted key list zod's\n * `unrecognized_keys` issue produces (`Unrecognized key(s) in object: 'a',\n * 'b'`) — this exact wording is what `collectDefinitionFindings` bakes into\n * `Finding.message` verbatim (see its doc comment), so this is a text match\n * against a stable, committed third-party format, not a guess at prose.\n * `null` for a message tail that isn't this issue.\n */\nexport function extractUnrecognizedKeys(rest: string): string[] | null {\n const match = /^Unrecognized key\\(s\\) in object: (.+)$/.exec(rest);\n if (!match) return null;\n\n const list = match[1];\n if (!list) return null;\n\n const keys = [...list.matchAll(/'([^']*)'/g)].map((m) => m[1] ?? '');\n return keys.length > 0 ? keys : null;\n}\n\n/**\n * A structural, minimal view of a zod schema instance's internals — `_def`,\n * `.shape` (a `ZodObject`), `.element` (a `ZodArray`) — used ONLY to walk\n * `IntegrationDefinitionSchema` for candidate keys. Deliberately NOT `import\n * { ... } from 'zod'`: this package has no direct `zod` dependency (only\n * `@ekanos/integration-schema` does), the constraint on this feature is no\n * new runtime dependency, and these three properties are the only ones this\n * module reads — stable across zod's v3 line.\n */\ninterface ZodLike {\n _def: {\n typeName: string;\n schema?: ZodLike;\n innerType?: ZodLike;\n };\n shape?: Record<string, ZodLike>;\n element?: ZodLike;\n}\n\nfunction asZodLike(value: unknown): ZodLike | null {\n if (\n typeof value === 'object' &&\n value !== null &&\n '_def' in value &&\n typeof (value as { _def: unknown })._def === 'object' &&\n (value as { _def: unknown })._def !== null\n ) {\n return value as ZodLike;\n }\n return null;\n}\n\n/** Peels `ZodEffects` (`.superRefine`/`.refine`), `ZodOptional`, `ZodDefault`, and `ZodNullable` down to the schema they wrap. */\nfunction unwrap(schema: ZodLike): ZodLike {\n let current = schema;\n\n for (;;) {\n if (current._def.typeName === 'ZodEffects' && current._def.schema) {\n current = current._def.schema;\n continue;\n }\n if (\n (current._def.typeName === 'ZodOptional' ||\n current._def.typeName === 'ZodDefault' ||\n current._def.typeName === 'ZodNullable') &&\n current._def.innerType\n ) {\n current = current._def.innerType;\n continue;\n }\n return current;\n }\n}\n\n/**\n * Walks `root` through `segments` (`.shape` for an object key, `.element`\n * for a numeric array index, unwrapping wrappers at every step), returning\n * the `ZodObject` found at the end — or `null` the moment it hits anything\n * it can't structurally walk (a union, a record, a segment the shape\n * doesn't have). The caller falls back to a curated list in that case.\n */\nfunction resolveObjectAt(\n root: ZodLike,\n segments: readonly string[],\n): ZodLike | null {\n let current = unwrap(root);\n\n for (const segment of segments) {\n if (current._def.typeName === 'ZodObject' && current.shape) {\n const next = current.shape[segment];\n if (!next) return null;\n current = unwrap(next);\n continue;\n }\n if (\n current._def.typeName === 'ZodArray' &&\n current.element &&\n /^\\d+$/.test(segment)\n ) {\n current = unwrap(current.element);\n continue;\n }\n return null;\n }\n\n return current._def.typeName === 'ZodObject' ? current : null;\n}\n\n/**\n * Fallback for the two spots a partner is most likely to typo a key — the\n * top-level declaration and a widget entry — used only when the structural\n * walk above can't resolve the parent path. Keep in sync with\n * `IntegrationDefinitionSchema`'s top-level shape and `widgetSchema` in\n * `@ekanos/integration-schema`'s `integration-definition.ts`; the structural\n * walk is tried first and should cover both of these in practice; this is\n * the safety net for whenever it doesn't.\n */\nconst CURATED_TOP_LEVEL_KEYS = [\n 'slug',\n 'name',\n 'description',\n 'version',\n 'capabilities',\n 'permissions',\n 'components',\n 'workspaceTargets',\n 'tools',\n 'storage',\n 'egress',\n 'webhooks',\n 'schedules',\n 'oauth',\n 'onActivate',\n 'proposes',\n];\n\nconst CURATED_WIDGET_KEYS = [\n 'id',\n 'name',\n 'component',\n 'widgetState',\n 'gridSize',\n 'gridPosition',\n 'layouts',\n 'category',\n 'isCollapsible',\n 'isPinnable',\n 'aiFooterEnabled',\n];\n\nfunction curatedKeysAt(parentDotPath: string): readonly string[] | null {\n if (isRootPath(parentDotPath)) return CURATED_TOP_LEVEL_KEYS;\n if (/^components\\.widgets\\.\\d+$/.test(parentDotPath)) {\n return CURATED_WIDGET_KEYS;\n }\n return null;\n}\n\n/**\n * The keys `IntegrationDefinitionSchema` actually accepts at\n * `parentDotPath` — the zod issue's own `path` for an `unrecognized_keys`\n * finding, so this always names the OBJECT the extra key was found on, never\n * the extra key itself. Structural introspection first, the curated list\n * second, `[]` (no suggestion offered — never a wrong-confident one) last.\n */\nexport function acceptedKeysAt(parentDotPath: string): readonly string[] {\n const root = asZodLike(IntegrationDefinitionSchema);\n const resolved = root\n ? resolveObjectAt(root, splitDotPath(parentDotPath))\n : null;\n\n if (resolved?.shape) return Object.keys(resolved.shape);\n\n return curatedKeysAt(parentDotPath) ?? [];\n}\n"]}
@@ -0,0 +1,59 @@
1
+ import type { CliContext } from './context.js';
2
+ import type { Ui } from './ui.js';
3
+ /**
4
+ * The shared findings block: one rendering used by `validate` (a failed run),
5
+ * `publish` (a gate refusal), and `status --findings`, so a partner sees the
6
+ * exact same shape wherever a finding shows up. This is the ONLY findings
7
+ * renderer in the package — nothing else may hand-format a `Finding`/
8
+ * `GateFinding` array for a human.
9
+ *
10
+ * Deliberately structural rather than importing `Finding` or `GateFinding`
11
+ * directly as the parameter type: both already satisfy this shape (their
12
+ * extra/missing fields are all optional), and a structural type keeps this
13
+ * module from caring which of the two ships that array.
14
+ */
15
+ export interface RenderableFinding {
16
+ check: string;
17
+ severity: 'error' | 'warn' | 'info';
18
+ file?: string;
19
+ line?: number;
20
+ message: string;
21
+ hint: string;
22
+ /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire — see `GateFinding`. */
23
+ origin?: string;
24
+ createdAt?: string;
25
+ resolvedAt?: string | null;
26
+ /**
27
+ * Optional multi-line supplement rendered directly under the finding's
28
+ * message — currently only `ekanos test`'s curated failures appendix sets
29
+ * this (an assertion diff). Additive: neither `Finding` nor `GateFinding`
30
+ * carries it, so every pre-existing caller renders byte-identically to
31
+ * before. Lines are shown as-is except a leading `-`/`+` (unified-diff
32
+ * style), which is colored red/green respectively.
33
+ */
34
+ detail?: string;
35
+ }
36
+ export interface RenderFindingsOptions {
37
+ /** Findings shown per group before an overflow line takes over. Default 10. */
38
+ maxPerGroup?: number;
39
+ /** Base for resolving a finding's `file` before reading it for a codeframe. Default `process.cwd()`. */
40
+ cwd?: string;
41
+ /** Injectable for `relativeTime` snapshot tests. Default `new Date()`. */
42
+ now?: Date;
43
+ }
44
+ /**
45
+ * `relativeTime('2026-09-01T10:00:00Z')` → `"2h ago"`. Deliberately coarse
46
+ * (rounds to the nearest unit, one unit at a time) — this is a findings-list
47
+ * badge, not an audit-log timestamp, and a partner scanning for "is this
48
+ * recent" doesn't need sub-minute precision.
49
+ */
50
+ export declare function relativeTime(iso: string, now?: Date): string;
51
+ /**
52
+ * Pure formatter — no I/O beyond an optional codeframe file read, no
53
+ * printing. Kept separate from `renderFindings` so tests can assert exact
54
+ * strings against a `Ui` built with a known color/glyph decision, without
55
+ * constructing a full `CliContext`.
56
+ */
57
+ export declare function formatFindings(ui: Ui, findings: readonly RenderableFinding[], options?: RenderFindingsOptions): string;
58
+ /** Render the shared findings block to `ctx`'s human-mode output (stderr). A no-op for an empty findings array. */
59
+ export declare function renderFindings(ctx: CliContext, findings: readonly RenderableFinding[], options?: RenderFindingsOptions): void;
@@ -0,0 +1,271 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { ENCLOSING_NOTE } from './source-locator.js';
4
+ // Compile-time proof the two real shapes this renders are both assignable.
5
+ const _typeCheck = (f) => f;
6
+ void _typeCheck;
7
+ const DEFAULT_MAX_PER_GROUP = 10;
8
+ const SEVERITY_RANK = {
9
+ error: 0,
10
+ warn: 1,
11
+ info: 2,
12
+ };
13
+ /**
14
+ * `relativeTime('2026-09-01T10:00:00Z')` → `"2h ago"`. Deliberately coarse
15
+ * (rounds to the nearest unit, one unit at a time) — this is a findings-list
16
+ * badge, not an audit-log timestamp, and a partner scanning for "is this
17
+ * recent" doesn't need sub-minute precision.
18
+ */
19
+ export function relativeTime(iso, now = new Date()) {
20
+ const then = new Date(iso).getTime();
21
+ if (Number.isNaN(then))
22
+ return iso;
23
+ const diffSec = Math.max(0, Math.round((now.getTime() - then) / 1000));
24
+ if (diffSec < 5)
25
+ return 'just now';
26
+ if (diffSec < 60)
27
+ return `${diffSec}s ago`;
28
+ const diffMin = Math.round(diffSec / 60);
29
+ if (diffMin < 60)
30
+ return `${diffMin}m ago`;
31
+ const diffHour = Math.round(diffMin / 60);
32
+ if (diffHour < 24)
33
+ return `${diffHour}h ago`;
34
+ const diffDay = Math.round(diffHour / 24);
35
+ if (diffDay < 30)
36
+ return `${diffDay}d ago`;
37
+ const diffMonth = Math.round(diffDay / 30);
38
+ if (diffMonth < 12)
39
+ return `${diffMonth}mo ago`;
40
+ const diffYear = Math.round(diffMonth / 12);
41
+ return `${diffYear}y ago`;
42
+ }
43
+ /**
44
+ * A message beginning with a zod-style dot-path (`components.widgets[2].id: …`,
45
+ * the shape `collectDefinitionFindings` produces — see the `Finding` doc
46
+ * comment in `@ekanos/integration-schema`). Captures the path and the rest
47
+ * separately so the renderer can dim the path instead of shouting the whole
48
+ * sentence in one uniform color.
49
+ */
50
+ const DOT_PATH_MESSAGE = /^([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+|\[\d+\])*): ([^]+)$/;
51
+ /**
52
+ * Render one finding's message: a leading dot-path identical to the
53
+ * finding's own `file` is pure duplication (the path would just repeat what
54
+ * `file:line` already told the reader) and is dropped; any other dot-path
55
+ * prefix is kept but dimmed, so the sentence itself reads at full contrast.
56
+ * A message with no dot-path prefix passes through untouched.
57
+ *
58
+ * A trailing `source-locator.ts` `ENCLOSING_NOTE` (plain text in the data —
59
+ * see that module's doc comment on why it never carries color itself) is
60
+ * peeled off before the dot-path handling above and dimmed on its own, then
61
+ * reattached — so it stays visually secondary to the message itself in
62
+ * exactly the case where the frame it accompanies points at the enclosing
63
+ * object rather than the missing key.
64
+ */
65
+ function renderMessage(ui, finding) {
66
+ const enclosingSuffix = ` ${ENCLOSING_NOTE}`;
67
+ const hasEnclosingNote = finding.message.endsWith(enclosingSuffix);
68
+ const message = hasEnclosingNote
69
+ ? finding.message.slice(0, -enclosingSuffix.length)
70
+ : finding.message;
71
+ const match = DOT_PATH_MESSAGE.exec(message);
72
+ const rendered = (() => {
73
+ if (!match)
74
+ return message;
75
+ const [, dotPath, rest] = match;
76
+ if (dotPath === finding.file)
77
+ return rest !== null && rest !== void 0 ? rest : message;
78
+ return `${ui.dim(dotPath !== null && dotPath !== void 0 ? dotPath : '')}: ${rest !== null && rest !== void 0 ? rest : ''}`;
79
+ })();
80
+ return hasEnclosingNote ? `${rendered} ${ui.dim(ENCLOSING_NOTE)}` : rendered;
81
+ }
82
+ function severityColor(ui, severity) {
83
+ if (severity === 'error')
84
+ return ui.err;
85
+ if (severity === 'warn')
86
+ return ui.warn;
87
+ return ui.dim;
88
+ }
89
+ function severityGlyph(ui, severity) {
90
+ if (severity === 'warn')
91
+ return ui.glyphs.warn;
92
+ if (severity === 'info')
93
+ return ui.glyphs.info;
94
+ return ui.glyphs.err;
95
+ }
96
+ function ellipsis(ui) {
97
+ return ui.glyphs.ok === '✓' ? '…' : '...';
98
+ }
99
+ /** Attempts a codeframe for `finding`; `null` when it isn't renderable (no line, or the file can't be read locally). */
100
+ function tryCodeframe(ui, finding, cwd) {
101
+ if (finding.file === undefined || finding.line === undefined)
102
+ return null;
103
+ const resolved = path.resolve(cwd, finding.file);
104
+ if (!fs.existsSync(resolved))
105
+ return null;
106
+ try {
107
+ const source = fs.readFileSync(resolved, 'utf8');
108
+ return ui.codeframe({ file: finding.file, line: finding.line, source });
109
+ }
110
+ catch (_a) {
111
+ // Best-effort decoration — an unreadable file (permissions, a race with
112
+ // a concurrent edit) just means no codeframe, never a renderer failure.
113
+ return null;
114
+ }
115
+ }
116
+ function originBadgeKind(_origin) {
117
+ return 'dim';
118
+ }
119
+ /**
120
+ * Renders a finding's `detail` block, one line per source line: a
121
+ * unified-diff `-` prefix is red, a `+` prefix is green, everything else
122
+ * (context lines, "Expected"/"Received" headers, an overflow note) is dim.
123
+ * `---`/`+++` file-header lines never appear here — `test-report.ts` never
124
+ * emits them — but are guarded against anyway since this is the one place
125
+ * that would mis-color them.
126
+ */
127
+ function renderDetailLines(ui, detail) {
128
+ return detail.split('\n').map((line) => {
129
+ if (line.startsWith('-') && !line.startsWith('---'))
130
+ return ui.err(line);
131
+ if (line.startsWith('+') && !line.startsWith('+++'))
132
+ return ui.ok(line);
133
+ return ui.dim(line);
134
+ });
135
+ }
136
+ function buildGroupHeader(ui, group, opts) {
137
+ var _a, _b;
138
+ const errorCount = group.findings.filter((f) => f.severity === 'error').length;
139
+ const warnCount = group.findings.filter((f) => f.severity === 'warn').length;
140
+ const infoCount = group.findings.filter((f) => f.severity === 'info').length;
141
+ const worst = (_b = (_a = group.findings[0]) === null || _a === void 0 ? void 0 : _a.severity) !== null && _b !== void 0 ? _b : 'info';
142
+ const color = severityColor(ui, worst);
143
+ const glyph = severityGlyph(ui, worst);
144
+ const parts = [];
145
+ if (errorCount > 0)
146
+ parts.push(`${errorCount} error${errorCount === 1 ? '' : 's'}`);
147
+ if (warnCount > 0)
148
+ parts.push(`${warnCount} warning${warnCount === 1 ? '' : 's'}`);
149
+ if (infoCount > 0)
150
+ parts.push(`${infoCount} info`);
151
+ // The representative for the group's origin badge is the first (i.e.
152
+ // worst-severity, then earliest) finding that carries one — a check
153
+ // almost always shares one origin, and this avoids a badge per line.
154
+ const withOrigin = group.findings.find((f) => f.origin !== undefined);
155
+ const badge = (withOrigin === null || withOrigin === void 0 ? void 0 : withOrigin.origin) === undefined
156
+ ? ''
157
+ : ` ${ui.badge(withOrigin.createdAt
158
+ ? `${withOrigin.origin} · ${relativeTime(withOrigin.createdAt, opts.now)}`
159
+ : withOrigin.origin, originBadgeKind(withOrigin.origin))}`;
160
+ return `${color(`${glyph} ${group.check}`)} · ${parts.join(', ')}${badge}`;
161
+ }
162
+ function buildGroupBody(ui, group, opts) {
163
+ const uniqueHints = [...new Set(group.findings.map((f) => f.hint))];
164
+ const [firstHint] = uniqueHints;
165
+ const dedupedHint = uniqueHints.length === 1 && firstHint !== undefined ? firstHint : null;
166
+ const visible = group.findings.slice(0, opts.maxPerGroup);
167
+ const overflow = group.findings.length - visible.length;
168
+ const lines = [];
169
+ let codeframeRendered = false;
170
+ for (const finding of visible) {
171
+ const location = finding.file !== undefined
172
+ ? ui.accent(finding.line !== undefined
173
+ ? `${finding.file}:${finding.line}`
174
+ : finding.file)
175
+ : null;
176
+ const message = renderMessage(ui, finding);
177
+ lines.push(` ${ui.dim('│')}${location ? ` ${location}` : ''} ${message}`);
178
+ if (finding.detail !== undefined) {
179
+ lines.push(...renderDetailLines(ui, finding.detail).map((line) => ` ${line}`));
180
+ }
181
+ if (dedupedHint === null) {
182
+ lines.push(` ${ui.hint(finding.hint)}`);
183
+ }
184
+ if (!codeframeRendered) {
185
+ const frame = tryCodeframe(ui, finding, opts.cwd);
186
+ if (frame !== null) {
187
+ codeframeRendered = true;
188
+ lines.push(frame
189
+ .split('\n')
190
+ .map((line) => ` ${line}`)
191
+ .join('\n'));
192
+ }
193
+ }
194
+ }
195
+ if (overflow > 0) {
196
+ lines.push(` ${ui.dim(`${ellipsis(ui)} and ${overflow} more in this group`)}`);
197
+ }
198
+ if (dedupedHint !== null) {
199
+ lines.push(` ${ui.hint(dedupedHint)}`);
200
+ }
201
+ return lines;
202
+ }
203
+ /**
204
+ * Pure formatter — no I/O beyond an optional codeframe file read, no
205
+ * printing. Kept separate from `renderFindings` so tests can assert exact
206
+ * strings against a `Ui` built with a known color/glyph decision, without
207
+ * constructing a full `CliContext`.
208
+ */
209
+ export function formatFindings(ui, findings, options = {}) {
210
+ var _a, _b, _c;
211
+ if (findings.length === 0)
212
+ return '';
213
+ const maxPerGroup = (_a = options.maxPerGroup) !== null && _a !== void 0 ? _a : DEFAULT_MAX_PER_GROUP;
214
+ const cwd = (_b = options.cwd) !== null && _b !== void 0 ? _b : process.cwd();
215
+ const now = (_c = options.now) !== null && _c !== void 0 ? _c : new Date();
216
+ const groupsByCheck = new Map();
217
+ findings.forEach((finding, index) => {
218
+ const existing = groupsByCheck.get(finding.check);
219
+ if (existing) {
220
+ existing.findings.push(finding);
221
+ return;
222
+ }
223
+ groupsByCheck.set(finding.check, {
224
+ check: finding.check,
225
+ findings: [finding],
226
+ order: index,
227
+ });
228
+ });
229
+ const groups = [...groupsByCheck.values()];
230
+ for (const group of groups) {
231
+ group.findings.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
232
+ }
233
+ groups.sort((a, b) => {
234
+ var _a, _b, _c, _d;
235
+ const worstA = SEVERITY_RANK[(_b = (_a = a.findings[0]) === null || _a === void 0 ? void 0 : _a.severity) !== null && _b !== void 0 ? _b : 'info'];
236
+ const worstB = SEVERITY_RANK[(_d = (_c = b.findings[0]) === null || _c === void 0 ? void 0 : _c.severity) !== null && _d !== void 0 ? _d : 'info'];
237
+ return worstA - worstB || a.order - b.order;
238
+ });
239
+ const blocks = groups.map((group) => {
240
+ const header = buildGroupHeader(ui, group, { now });
241
+ const body = buildGroupBody(ui, group, { maxPerGroup, cwd });
242
+ return [header, ...body].join('\n');
243
+ });
244
+ const totalErrors = findings.filter((f) => f.severity === 'error').length;
245
+ const totalWarnings = findings.filter((f) => f.severity === 'warn').length;
246
+ const totalInfo = findings.filter((f) => f.severity === 'info').length;
247
+ const summaryParts = [];
248
+ if (totalErrors > 0)
249
+ summaryParts.push(`${totalErrors} error${totalErrors === 1 ? '' : 's'}`);
250
+ if (totalWarnings > 0)
251
+ summaryParts.push(`${totalWarnings} warning${totalWarnings === 1 ? '' : 's'}`);
252
+ if (totalInfo > 0 && totalErrors === 0 && totalWarnings === 0) {
253
+ summaryParts.push(`${totalInfo} info`);
254
+ }
255
+ const summaryColor = totalErrors > 0 ? ui.err : totalWarnings > 0 ? ui.warn : ui.dim;
256
+ const summaryGlyph = totalErrors > 0
257
+ ? ui.glyphs.err
258
+ : totalWarnings > 0
259
+ ? ui.glyphs.warn
260
+ : ui.glyphs.info;
261
+ const checksCount = groups.length;
262
+ const summary = summaryColor(`${summaryGlyph} ${summaryParts.join(', ')} across ${checksCount} check${checksCount === 1 ? '' : 's'}`);
263
+ return [...blocks, summary].join('\n\n');
264
+ }
265
+ /** Render the shared findings block to `ctx`'s human-mode output (stderr). A no-op for an empty findings array. */
266
+ export function renderFindings(ctx, findings, options = {}) {
267
+ const block = formatFindings(ctx.ui, findings, options);
268
+ if (block !== '')
269
+ ctx.log(block);
270
+ }
271
+ //# sourceMappingURL=findings-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings-renderer.js","sourceRoot":"","sources":["../src/findings-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from './context';\nimport type { GateFinding } from './publish-api';\nimport { ENCLOSING_NOTE } from './source-locator';\nimport type { BadgeKind, Ui } from './ui';\n\n/**\n * The shared findings block: one rendering used by `validate` (a failed run),\n * `publish` (a gate refusal), and `status --findings`, so a partner sees the\n * exact same shape wherever a finding shows up. This is the ONLY findings\n * renderer in the package — nothing else may hand-format a `Finding`/\n * `GateFinding` array for a human.\n *\n * Deliberately structural rather than importing `Finding` or `GateFinding`\n * directly as the parameter type: both already satisfy this shape (their\n * extra/missing fields are all optional), and a structural type keeps this\n * module from caring which of the two ships that array.\n */\nexport interface RenderableFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire — see `GateFinding`. */\n origin?: string;\n createdAt?: string;\n resolvedAt?: string | null;\n /**\n * Optional multi-line supplement rendered directly under the finding's\n * message — currently only `ekanos test`'s curated failures appendix sets\n * this (an assertion diff). Additive: neither `Finding` nor `GateFinding`\n * carries it, so every pre-existing caller renders byte-identically to\n * before. Lines are shown as-is except a leading `-`/`+` (unified-diff\n * style), which is colored red/green respectively.\n */\n detail?: string;\n}\n\n// Compile-time proof the two real shapes this renders are both assignable.\nconst _typeCheck: (f: Finding | GateFinding) => RenderableFinding = (f) => f;\nvoid _typeCheck;\n\nexport interface RenderFindingsOptions {\n /** Findings shown per group before an overflow line takes over. Default 10. */\n maxPerGroup?: number;\n /** Base for resolving a finding's `file` before reading it for a codeframe. Default `process.cwd()`. */\n cwd?: string;\n /** Injectable for `relativeTime` snapshot tests. Default `new Date()`. */\n now?: Date;\n}\n\nconst DEFAULT_MAX_PER_GROUP = 10;\n\nconst SEVERITY_RANK: Record<RenderableFinding['severity'], number> = {\n error: 0,\n warn: 1,\n info: 2,\n};\n\ninterface Group {\n check: string;\n findings: RenderableFinding[];\n /** Insertion order, for a stable tie-break when sorting groups. */\n order: number;\n}\n\n/**\n * `relativeTime('2026-09-01T10:00:00Z')` → `\"2h ago\"`. Deliberately coarse\n * (rounds to the nearest unit, one unit at a time) — this is a findings-list\n * badge, not an audit-log timestamp, and a partner scanning for \"is this\n * recent\" doesn't need sub-minute precision.\n */\nexport function relativeTime(iso: string, now: Date = new Date()): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return iso;\n\n const diffSec = Math.max(0, Math.round((now.getTime() - then) / 1000));\n\n if (diffSec < 5) return 'just now';\n if (diffSec < 60) return `${diffSec}s ago`;\n\n const diffMin = Math.round(diffSec / 60);\n if (diffMin < 60) return `${diffMin}m ago`;\n\n const diffHour = Math.round(diffMin / 60);\n if (diffHour < 24) return `${diffHour}h ago`;\n\n const diffDay = Math.round(diffHour / 24);\n if (diffDay < 30) return `${diffDay}d ago`;\n\n const diffMonth = Math.round(diffDay / 30);\n if (diffMonth < 12) return `${diffMonth}mo ago`;\n\n const diffYear = Math.round(diffMonth / 12);\n return `${diffYear}y ago`;\n}\n\n/**\n * A message beginning with a zod-style dot-path (`components.widgets[2].id: …`,\n * the shape `collectDefinitionFindings` produces — see the `Finding` doc\n * comment in `@ekanos/integration-schema`). Captures the path and the rest\n * separately so the renderer can dim the path instead of shouting the whole\n * sentence in one uniform color.\n */\nconst DOT_PATH_MESSAGE =\n /^([A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+|\\[\\d+\\])*): ([^]+)$/;\n\n/**\n * Render one finding's message: a leading dot-path identical to the\n * finding's own `file` is pure duplication (the path would just repeat what\n * `file:line` already told the reader) and is dropped; any other dot-path\n * prefix is kept but dimmed, so the sentence itself reads at full contrast.\n * A message with no dot-path prefix passes through untouched.\n *\n * A trailing `source-locator.ts` `ENCLOSING_NOTE` (plain text in the data —\n * see that module's doc comment on why it never carries color itself) is\n * peeled off before the dot-path handling above and dimmed on its own, then\n * reattached — so it stays visually secondary to the message itself in\n * exactly the case where the frame it accompanies points at the enclosing\n * object rather than the missing key.\n */\nfunction renderMessage(ui: Ui, finding: RenderableFinding): string {\n const enclosingSuffix = ` ${ENCLOSING_NOTE}`;\n const hasEnclosingNote = finding.message.endsWith(enclosingSuffix);\n const message = hasEnclosingNote\n ? finding.message.slice(0, -enclosingSuffix.length)\n : finding.message;\n\n const match = DOT_PATH_MESSAGE.exec(message);\n const rendered = (() => {\n if (!match) return message;\n\n const [, dotPath, rest] = match;\n if (dotPath === finding.file) return rest ?? message;\n\n return `${ui.dim(dotPath ?? '')}: ${rest ?? ''}`;\n })();\n\n return hasEnclosingNote ? `${rendered} ${ui.dim(ENCLOSING_NOTE)}` : rendered;\n}\n\nfunction severityColor(\n ui: Ui,\n severity: RenderableFinding['severity'],\n): (text: string) => string {\n if (severity === 'error') return ui.err;\n if (severity === 'warn') return ui.warn;\n return ui.dim;\n}\n\nfunction severityGlyph(\n ui: Ui,\n severity: RenderableFinding['severity'],\n): string {\n if (severity === 'warn') return ui.glyphs.warn;\n if (severity === 'info') return ui.glyphs.info;\n return ui.glyphs.err;\n}\n\nfunction ellipsis(ui: Ui): string {\n return ui.glyphs.ok === '✓' ? '…' : '...';\n}\n\n/** Attempts a codeframe for `finding`; `null` when it isn't renderable (no line, or the file can't be read locally). */\nfunction tryCodeframe(\n ui: Ui,\n finding: RenderableFinding,\n cwd: string,\n): string | null {\n if (finding.file === undefined || finding.line === undefined) return null;\n\n const resolved = path.resolve(cwd, finding.file);\n if (!fs.existsSync(resolved)) return null;\n\n try {\n const source = fs.readFileSync(resolved, 'utf8');\n return ui.codeframe({ file: finding.file, line: finding.line, source });\n } catch {\n // Best-effort decoration — an unreadable file (permissions, a race with\n // a concurrent edit) just means no codeframe, never a renderer failure.\n return null;\n }\n}\n\nfunction originBadgeKind(_origin: string): BadgeKind {\n return 'dim';\n}\n\n/**\n * Renders a finding's `detail` block, one line per source line: a\n * unified-diff `-` prefix is red, a `+` prefix is green, everything else\n * (context lines, \"Expected\"/\"Received\" headers, an overflow note) is dim.\n * `---`/`+++` file-header lines never appear here — `test-report.ts` never\n * emits them — but are guarded against anyway since this is the one place\n * that would mis-color them.\n */\nfunction renderDetailLines(ui: Ui, detail: string): string[] {\n return detail.split('\\n').map((line) => {\n if (line.startsWith('-') && !line.startsWith('---')) return ui.err(line);\n if (line.startsWith('+') && !line.startsWith('+++')) return ui.ok(line);\n return ui.dim(line);\n });\n}\n\nfunction buildGroupHeader(\n ui: Ui,\n group: Group,\n opts: Required<Pick<RenderFindingsOptions, 'now'>>,\n): string {\n const errorCount = group.findings.filter(\n (f) => f.severity === 'error',\n ).length;\n const warnCount = group.findings.filter((f) => f.severity === 'warn').length;\n const infoCount = group.findings.filter((f) => f.severity === 'info').length;\n\n const worst = group.findings[0]?.severity ?? 'info';\n const color = severityColor(ui, worst);\n const glyph = severityGlyph(ui, worst);\n\n const parts: string[] = [];\n if (errorCount > 0)\n parts.push(`${errorCount} error${errorCount === 1 ? '' : 's'}`);\n if (warnCount > 0)\n parts.push(`${warnCount} warning${warnCount === 1 ? '' : 's'}`);\n if (infoCount > 0) parts.push(`${infoCount} info`);\n\n // The representative for the group's origin badge is the first (i.e.\n // worst-severity, then earliest) finding that carries one — a check\n // almost always shares one origin, and this avoids a badge per line.\n const withOrigin = group.findings.find((f) => f.origin !== undefined);\n const badge =\n withOrigin?.origin === undefined\n ? ''\n : ` ${ui.badge(\n withOrigin.createdAt\n ? `${withOrigin.origin} · ${relativeTime(withOrigin.createdAt, opts.now)}`\n : withOrigin.origin,\n originBadgeKind(withOrigin.origin),\n )}`;\n\n return `${color(`${glyph} ${group.check}`)} · ${parts.join(', ')}${badge}`;\n}\n\nfunction buildGroupBody(\n ui: Ui,\n group: Group,\n opts: Required<Pick<RenderFindingsOptions, 'maxPerGroup' | 'cwd'>>,\n): string[] {\n const uniqueHints = [...new Set(group.findings.map((f) => f.hint))];\n const [firstHint] = uniqueHints;\n const dedupedHint =\n uniqueHints.length === 1 && firstHint !== undefined ? firstHint : null;\n\n const visible = group.findings.slice(0, opts.maxPerGroup);\n const overflow = group.findings.length - visible.length;\n\n const lines: string[] = [];\n let codeframeRendered = false;\n\n for (const finding of visible) {\n const location =\n finding.file !== undefined\n ? ui.accent(\n finding.line !== undefined\n ? `${finding.file}:${finding.line}`\n : finding.file,\n )\n : null;\n const message = renderMessage(ui, finding);\n\n lines.push(` ${ui.dim('│')}${location ? ` ${location}` : ''} ${message}`);\n\n if (finding.detail !== undefined) {\n lines.push(\n ...renderDetailLines(ui, finding.detail).map((line) => ` ${line}`),\n );\n }\n\n if (dedupedHint === null) {\n lines.push(` ${ui.hint(finding.hint)}`);\n }\n\n if (!codeframeRendered) {\n const frame = tryCodeframe(ui, finding, opts.cwd);\n if (frame !== null) {\n codeframeRendered = true;\n lines.push(\n frame\n .split('\\n')\n .map((line) => ` ${line}`)\n .join('\\n'),\n );\n }\n }\n }\n\n if (overflow > 0) {\n lines.push(\n ` ${ui.dim(`${ellipsis(ui)} and ${overflow} more in this group`)}`,\n );\n }\n\n if (dedupedHint !== null) {\n lines.push(` ${ui.hint(dedupedHint)}`);\n }\n\n return lines;\n}\n\n/**\n * Pure formatter — no I/O beyond an optional codeframe file read, no\n * printing. Kept separate from `renderFindings` so tests can assert exact\n * strings against a `Ui` built with a known color/glyph decision, without\n * constructing a full `CliContext`.\n */\nexport function formatFindings(\n ui: Ui,\n findings: readonly RenderableFinding[],\n options: RenderFindingsOptions = {},\n): string {\n if (findings.length === 0) return '';\n\n const maxPerGroup = options.maxPerGroup ?? DEFAULT_MAX_PER_GROUP;\n const cwd = options.cwd ?? process.cwd();\n const now = options.now ?? new Date();\n\n const groupsByCheck = new Map<string, Group>();\n findings.forEach((finding, index) => {\n const existing = groupsByCheck.get(finding.check);\n if (existing) {\n existing.findings.push(finding);\n return;\n }\n groupsByCheck.set(finding.check, {\n check: finding.check,\n findings: [finding],\n order: index,\n });\n });\n\n const groups = [...groupsByCheck.values()];\n for (const group of groups) {\n group.findings.sort(\n (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity],\n );\n }\n groups.sort((a, b) => {\n const worstA = SEVERITY_RANK[a.findings[0]?.severity ?? 'info'];\n const worstB = SEVERITY_RANK[b.findings[0]?.severity ?? 'info'];\n return worstA - worstB || a.order - b.order;\n });\n\n const blocks = groups.map((group) => {\n const header = buildGroupHeader(ui, group, { now });\n const body = buildGroupBody(ui, group, { maxPerGroup, cwd });\n return [header, ...body].join('\\n');\n });\n\n const totalErrors = findings.filter((f) => f.severity === 'error').length;\n const totalWarnings = findings.filter((f) => f.severity === 'warn').length;\n const totalInfo = findings.filter((f) => f.severity === 'info').length;\n\n const summaryParts: string[] = [];\n if (totalErrors > 0)\n summaryParts.push(`${totalErrors} error${totalErrors === 1 ? '' : 's'}`);\n if (totalWarnings > 0)\n summaryParts.push(\n `${totalWarnings} warning${totalWarnings === 1 ? '' : 's'}`,\n );\n if (totalInfo > 0 && totalErrors === 0 && totalWarnings === 0) {\n summaryParts.push(`${totalInfo} info`);\n }\n\n const summaryColor =\n totalErrors > 0 ? ui.err : totalWarnings > 0 ? ui.warn : ui.dim;\n const summaryGlyph =\n totalErrors > 0\n ? ui.glyphs.err\n : totalWarnings > 0\n ? ui.glyphs.warn\n : ui.glyphs.info;\n const checksCount = groups.length;\n\n const summary = summaryColor(\n `${summaryGlyph} ${summaryParts.join(', ')} across ${checksCount} check${\n checksCount === 1 ? '' : 's'\n }`,\n );\n\n return [...blocks, summary].join('\\n\\n');\n}\n\n/** Render the shared findings block to `ctx`'s human-mode output (stderr). A no-op for an empty findings array. */\nexport function renderFindings(\n ctx: CliContext,\n findings: readonly RenderableFinding[],\n options: RenderFindingsOptions = {},\n): void {\n const block = formatFindings(ctx.ui, findings, options);\n if (block !== '') ctx.log(block);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -8,10 +8,11 @@ export interface RunOptions {
8
8
  env?: Record<string, string | undefined>;
9
9
  cwd?: string;
10
10
  /**
11
- * Test-only overrides for `publish`'s interactive-confirm gate — see
12
- * `PublishArgs` in `commands/publish.ts`. Never set these from real CLI
13
- * wiring; they exist so a test can exercise the human y/N confirm branch
14
- * without a real terminal.
11
+ * Test-only override for `publish`'s interactive-confirm gate and `init`
12
+ * / `login`'s clack wizard gate (`canWizard` in `wizard.ts`) — see
13
+ * `PublishArgs` in `commands/publish.ts`. Never set this from real CLI
14
+ * wiring; it exists so a test can exercise the human y/N confirm branch or
15
+ * the wizard flow without a real terminal.
15
16
  */
16
17
  stdinIsTTY?: boolean;
17
18
  confirm?: (question: string) => Promise<boolean>;
@@ -21,6 +22,12 @@ export interface RunOptions {
21
22
  * so a test can exercise the poll loop without waiting 30s per iteration.
22
23
  */
23
24
  sleep?: (ms: number) => Promise<void>;
25
+ /**
26
+ * Test-only override for `status`'s AGE column ("now") — see `StatusArgs`
27
+ * in `commands/status.ts`. Never set this from real CLI wiring; it exists
28
+ * so a test can assert an exact `relativeTime` string deterministically.
29
+ */
30
+ now?: Date;
24
31
  }
25
32
  /**
26
33
  * The single entry point. Resolves the agent-native envelope once, dispatches