@ekanos/cli 0.1.9 → 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 (68) hide show
  1. package/dist/auth/fusion-api.js +2 -2
  2. package/dist/auth/fusion-api.js.map +1 -1
  3. package/dist/banner.d.ts +5 -1
  4. package/dist/banner.js +7 -2
  5. package/dist/banner.js.map +1 -1
  6. package/dist/commands/init.d.ts +13 -1
  7. package/dist/commands/init.js +70 -13
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/login.d.ts +13 -0
  10. package/dist/commands/login.js +41 -10
  11. package/dist/commands/login.js.map +1 -1
  12. package/dist/commands/publish.js +55 -3
  13. package/dist/commands/publish.js.map +1 -1
  14. package/dist/commands/status.d.ts +68 -2
  15. package/dist/commands/status.js +312 -43
  16. package/dist/commands/status.js.map +1 -1
  17. package/dist/commands/test.d.ts +27 -5
  18. package/dist/commands/test.js +85 -9
  19. package/dist/commands/test.js.map +1 -1
  20. package/dist/commands/validate.js +115 -3
  21. package/dist/commands/validate.js.map +1 -1
  22. package/dist/context.d.ts +37 -1
  23. package/dist/context.js +27 -1
  24. package/dist/context.js.map +1 -1
  25. package/dist/did-you-mean.d.ts +36 -0
  26. package/dist/did-you-mean.js +192 -0
  27. package/dist/did-you-mean.js.map +1 -0
  28. package/dist/findings-renderer.d.ts +59 -0
  29. package/dist/findings-renderer.js +271 -0
  30. package/dist/findings-renderer.js.map +1 -0
  31. package/dist/generated/fa-free-names.js +2 -2
  32. package/dist/generated/fa-free-names.js.map +1 -1
  33. package/dist/icon-findings.js +5 -5
  34. package/dist/icon-findings.js.map +1 -1
  35. package/dist/index.d.ts +11 -4
  36. package/dist/index.js +23 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/pack.js +43 -0
  39. package/dist/pack.js.map +1 -1
  40. package/dist/project-checks.d.ts +37 -0
  41. package/dist/project-checks.js +169 -1
  42. package/dist/project-checks.js.map +1 -1
  43. package/dist/publish-api.d.ts +24 -11
  44. package/dist/publish-api.js +7 -8
  45. package/dist/publish-api.js.map +1 -1
  46. package/dist/source-locator.d.ts +67 -0
  47. package/dist/source-locator.js +284 -0
  48. package/dist/source-locator.js.map +1 -0
  49. package/dist/sources-api.d.ts +3 -3
  50. package/dist/sources-api.js.map +1 -1
  51. package/dist/test-report.d.ts +32 -0
  52. package/dist/test-report.js +255 -0
  53. package/dist/test-report.js.map +1 -0
  54. package/dist/toolchain-api.d.ts +5 -5
  55. package/dist/toolchain-api.js.map +1 -1
  56. package/dist/ui.d.ts +75 -0
  57. package/dist/ui.js +132 -0
  58. package/dist/ui.js.map +1 -0
  59. package/dist/validate-findings.d.ts +22 -0
  60. package/dist/validate-findings.js +57 -1
  61. package/dist/validate-findings.js.map +1 -1
  62. package/dist/wizard.d.ts +80 -0
  63. package/dist/wizard.js +74 -0
  64. package/dist/wizard.js.map +1 -0
  65. package/package.json +5 -2
  66. package/templates/package.json.tmpl +3 -1
  67. package/templates/scripts/setup-hooks.mjs.tmpl +45 -0
  68. package/templates/tsconfig.json.tmpl +2 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-locator.js","sourceRoot":"","sources":["../src/source-locator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Best-effort zod dot-path → source line resolution for `validate`'s\n * schema findings.\n *\n * `collectDefinitionFindings` (in `@ekanos/integration-schema`) bakes a zod\n * PATH into `Finding.message` (`\"components.widgets.0.id: Required\"`), never\n * a source offset — zod reports a `path`, not a byte offset, and fabricating\n * a line number would be a lie (see the `Finding` doc comment there). This\n * module recovers a plausible line by scanning the partner's declaration\n * source text: no AST, no new dependency — TypeScript's compiler API is a\n * devDep in this workspace, not a runtime one, and the published CLI never\n * ships it, so it is never imported here. Just a brace-tracking scan of the\n * object/array literal passed to `defineIntegration(...)`.\n *\n * The rule that matters more than any mechanic below: a WRONG line number is\n * worse than none. Anything this module can't resolve with confidence\n * returns `{ confidence: 'none' }` — never a guess.\n */\n\nexport type LocateConfidence = 'exact' | 'enclosing' | 'none';\n\nexport type LocateResult =\n | { confidence: 'exact'; line: number }\n | { confidence: 'enclosing'; line: number }\n | { confidence: 'none' };\n\n/**\n * Appended, PLAIN TEXT (no ANSI — this module never touches color; see\n * `ui.ts`'s doc comment on why nothing outside it may), to a finding's\n * message when only the enclosing object/array was found, not the missing\n * key itself. `findings-renderer.ts` recognizes this literal suffix and dims\n * it at render time.\n */\nexport const ENCLOSING_NOTE =\n '(showing the enclosing object — add the missing key here)';\n\n/** The literal path `collectDefinitionFindings` emits for a zod issue whose `path` array is empty. */\nexport function isRootPath(dotPath: string): boolean {\n return dotPath === '(root)';\n}\n\n/**\n * `\"components.widgets[0].id\"` and `\"components.widgets.0.id\"` both split to\n * `['components', 'widgets', '0', 'id']` — `collectDefinitionFindings` only\n * ever emits the dot form (a plain `issue.path.join('.')`), but this accepts\n * either, matching the pattern `findings-renderer.ts`'s dot-path detector\n * already tolerates.\n */\nexport function splitDotPath(dotPath: string): string[] {\n if (isRootPath(dotPath)) return [];\n return dotPath.match(/[^.[\\]]+/g) ?? [];\n}\n\n/**\n * A finding message is `\"<dot-path>: <zod message>\"` (or `(root): …` for an\n * empty path) — see the `Finding` doc comment in `@ekanos/integration-schema`.\n * Splits the two apart; `null` for a message with no such prefix (every\n * finding NOT built from a zod issue, e.g. `definition.plain-data`).\n */\nexport function extractPathFromMessage(\n message: string,\n): { path: string; rest: string } | null {\n const match =\n /^((?:\\(root\\))|[A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+|\\[\\d+\\])*): ([^]+)$/.exec(\n message,\n );\n if (!match) return null;\n\n const [, path, rest] = match;\n if (path === undefined || rest === undefined) return null;\n\n return { path, rest };\n}\n\ninterface Scope {\n /** Index of the opening delimiter (`{` or `[`) in both `source` and the masked text. */\n start: number;\n /** Index of the matching closing delimiter. */\n end: number;\n kind: '{' | '[';\n}\n\n/**\n * Replaces the CONTENTS of every string/template literal and comment with a\n * neutral filler, preserving length, newlines, and every character outside\n * those regions verbatim — so a later brace-depth scan never miscounts a\n * `{`/`}`/`,` that only appears inside a string, while every index computed\n * against the result still points at the same character in the original\n * `source`. Delimiter characters (quotes, `/*`, `//`) are kept as-is; only\n * their insides are masked.\n */\nfunction mask(source: string): string {\n const out = source.split('');\n const n = source.length;\n let i = 0;\n\n while (i < n) {\n const ch = source[i];\n const next = source[i + 1];\n\n if (ch === '/' && next === '/') {\n // Mask the `//` marker itself too, not just what follows it — a bare\n // `/` isn't whitespace, so `skipMaskedWhitespace` needs the WHOLE\n // comment (marker included) to read as blank in order to skip past a\n // comment that precedes a key.\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && source[i] !== '\\n') {\n out[i] = ' ';\n i += 1;\n }\n continue;\n }\n\n if (ch === '/' && next === '*') {\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {\n if (source[i] !== '\\n') out[i] = ' ';\n i += 1;\n }\n if (i < n) out[i] = ' ';\n if (i + 1 < n) out[i + 1] = ' ';\n i += 2;\n continue;\n }\n\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n const quote = ch;\n i += 1;\n while (i < n && source[i] !== quote) {\n if (source[i] === '\\\\') {\n if (source[i + 1] !== '\\n' && i + 1 < n) out[i + 1] = 'x';\n i += 2;\n continue;\n }\n if (source[i] !== '\\n') out[i] = 'x';\n i += 1;\n }\n i += 1;\n continue;\n }\n\n i += 1;\n }\n\n return out.join('');\n}\n\n/** Index of the delimiter matching `masked[openIndex]`, or `-1` for unbalanced/malformed input. Operates on masked text so a bracket inside a string never throws off the count. */\nfunction findMatchingClose(masked: string, openIndex: number): number {\n const stack: string[] = [];\n\n for (let i = openIndex; i < masked.length; i += 1) {\n const ch = masked[i];\n if (ch === '{') stack.push('}');\n else if (ch === '[') stack.push(']');\n else if (ch === '(') stack.push(')');\n else if (ch === '}' || ch === ']' || ch === ')') {\n if (stack.pop() !== ch) return -1;\n if (stack.length === 0) return i;\n }\n }\n\n return -1;\n}\n\n/** Top-level (depth-0-relative-to-the-scope) member spans between a scope's delimiters, split at depth-0 commas — one span per object property or array element, in source order. */\nfunction splitTopLevelMembers(\n masked: string,\n scope: Scope,\n): Array<{ start: number; end: number }> {\n const members: Array<{ start: number; end: number }> = [];\n let depth = 0;\n let memberStart = scope.start + 1;\n\n for (let i = scope.start + 1; i < scope.end; i += 1) {\n const ch = masked[i];\n if (ch === '{' || ch === '[' || ch === '(') depth += 1;\n else if (ch === '}' || ch === ']' || ch === ')') depth -= 1;\n else if (ch === ',' && depth === 0) {\n members.push({ start: memberStart, end: i });\n memberStart = i + 1;\n }\n }\n members.push({ start: memberStart, end: scope.end });\n\n return members.filter((m) => masked.slice(m.start, m.end).trim() !== '');\n}\n\nfunction scopeAt(masked: string, index: number): Scope | null {\n const ch = masked[index];\n if (ch !== '{' && ch !== '[') return null;\n\n const close = findMatchingClose(masked, index);\n if (close === -1) return null;\n\n return { start: index, end: close, kind: ch };\n}\n\nfunction findRootScope(masked: string): Scope | null {\n const match = /defineIntegration\\s*\\(/.exec(masked);\n if (!match) return null;\n\n let i = match.index + match[0].length;\n while (i < masked.length && /\\s/.test(masked[i] ?? '')) i += 1;\n\n return scopeAt(masked, i);\n}\n\n/**\n * A quoted (`'id'`/`\"id\"`) or bare (`id`) object key immediately followed by\n * `:`, anchored at the very start of the string it's matched against — the\n * caller is responsible for having already skipped past any leading\n * whitespace AND leading comment (both of which read as whitespace in the\n * MASKED text, see `skipMaskedWhitespace`). Matched against the RAW source\n * slice, never the masked one — masking a quoted key's own text would\n * destroy the very name being compared.\n */\nconst KEY_PATTERN =\n /^(?:(['\"])((?:\\\\.|(?!\\1)[^\\\\])*)\\1|([A-Za-z_$][\\w$]*))\\s*:/;\n\n/** First index at or after `start` (and before `end`) whose MASKED character is not whitespace — a masked comment reads as whitespace here too, so this skips leading comments along with real whitespace/newlines. */\nfunction skipMaskedWhitespace(\n masked: string,\n start: number,\n end: number,\n): number {\n let i = start;\n while (i < end && /\\s/.test(masked[i] ?? '')) i += 1;\n return i;\n}\n\ntype DescendResult =\n 'not-found' | 'mismatch' | { targetStart: number; valueScope: Scope | null };\n\nfunction descend(\n source: string,\n masked: string,\n scope: Scope,\n segment: string,\n): DescendResult {\n const members = splitTopLevelMembers(masked, scope);\n\n if (scope.kind === '[') {\n if (!/^\\d+$/.test(segment)) return 'mismatch';\n\n const member = members[Number(segment)];\n if (!member) return 'not-found';\n\n const targetStart = skipMaskedWhitespace(masked, member.start, member.end);\n return { targetStart, valueScope: scopeAt(masked, targetStart) };\n }\n\n for (const member of members) {\n const contentStart = skipMaskedWhitespace(masked, member.start, member.end);\n const raw = source.slice(contentStart, member.end);\n const match = KEY_PATTERN.exec(raw);\n if (!match) continue;\n\n const keyName = match[2] ?? match[3];\n if (keyName !== segment) continue;\n\n const valueStart = skipMaskedWhitespace(\n masked,\n contentStart + match[0].length,\n member.end,\n );\n\n return {\n targetStart: contentStart,\n valueScope: scopeAt(masked, valueStart),\n };\n }\n\n return 'not-found';\n}\n\nfunction lineAt(source: string, index: number): number {\n let line = 1;\n for (let i = 0; i < index && i < source.length; i += 1) {\n if (source[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/**\n * Locates `dotPath` (a zod path string, e.g. `\"components.widgets.0.id\"` or\n * `\"(root)\"`) within `source`, the declaration file's full text. `source`\n * MUST contain an inline `defineIntegration({ … })` call — a definition\n * built from a variable, spread, or factory function can't be resolved\n * statically and always comes back `none`. A one-line (minified/no-newline)\n * source also always comes back `none`: a codeframe there can't isolate\n * anything the reader couldn't already see, so it isn't worth the risk of a\n * technically-correct-but-useless \"line 1\" frame.\n */\nexport function locateDotPath(source: string, dotPath: string): LocateResult {\n // Strip trailing newline(s) before checking: a one-line declaration saved\n // with a normal trailing EOF newline must still count as \"one line,\" not\n // \"multi-line\" by virtue of that single trailing `\\n`.\n if (!source.replace(/\\n+$/, '').includes('\\n')) return { confidence: 'none' };\n\n const masked = mask(source);\n const root = findRootScope(masked);\n if (!root) return { confidence: 'none' };\n\n const segments = splitDotPath(dotPath);\n if (segments.length === 0) {\n return { confidence: 'enclosing', line: lineAt(source, root.start) };\n }\n\n let scope: Scope = root;\n\n for (let i = 0; i < segments.length; i += 1) {\n const segment = segments[i];\n if (segment === undefined) break;\n\n const result = descend(source, masked, scope, segment);\n\n if (result === 'mismatch') return { confidence: 'none' };\n if (result === 'not-found') {\n return { confidence: 'enclosing', line: lineAt(source, scope.start) };\n }\n\n const isLast = i === segments.length - 1;\n if (isLast) {\n return { confidence: 'exact', line: lineAt(source, result.targetStart) };\n }\n\n if (result.valueScope === null) return { confidence: 'none' };\n scope = result.valueScope;\n }\n\n return { confidence: 'none' };\n}\n"]}
@@ -4,9 +4,9 @@
4
4
  * ---------------------------------------------------------------------------
5
5
  * WIRE CONTRACT — the seam between this package and the Fusion web app.
6
6
  *
7
- * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As
8
- * with the auth routes and `publish-api.ts`, the two halves are coupled only
9
- * by HTTP; tests stub `fetch`, never the route.
7
+ * The server side is the host's `/api/partner/sources` route. As with the
8
+ * auth routes and `publish-api.ts`, the two halves are coupled only by
9
+ * HTTP; tests stub `fetch`, never the route.
10
10
  *
11
11
  * GET /api/partner/sources [Authorization: Bearer <access_token>]
12
12
  * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }
@@ -1 +1 @@
1
- {"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Fusion source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As\n * with the auth routes and `publish-api.ts`, the two halves are coupled only\n * by HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"developer\" | \"source_admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the source seat lookup.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
1
+ {"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Fusion source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side is the host's `/api/partner/sources` route. As with the\n * auth routes and `publish-api.ts`, the two halves are coupled only by\n * HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"developer\" | \"source_admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the source seat lookup.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import type { RenderableFinding } from './findings-renderer.js';
2
+ export interface TestSummary {
3
+ passed: number;
4
+ failed: number;
5
+ files: number;
6
+ }
7
+ export interface ParsedTestReport {
8
+ /** One finding per failing test, grouped (via `check`) per test FILE. */
9
+ findings: RenderableFinding[];
10
+ /** Relative paths of files with at least one failure, first-seen order. */
11
+ failingFiles: string[];
12
+ summary: TestSummary | null;
13
+ }
14
+ /**
15
+ * Whether the project's OWN `test` script names vitest — the one signal
16
+ * used to decide whether to inject the JSON reporter at all. Deliberately
17
+ * conservative: a jest project, a custom node runner, or a shell wrapper is
18
+ * left completely alone, with no reporter flags appended and no report
19
+ * parsed — `ekanos test` behaves exactly as it did before this feature.
20
+ */
21
+ export declare function projectTestScriptUsesVitest(projectDir: string): boolean;
22
+ /** A fresh, unique path for the JSON report — one per invocation. The caller owns cleanup. */
23
+ export declare function newReportPath(): string;
24
+ /** The extra argv that makes vitest ALSO emit a JSON report alongside its normal terminal output. */
25
+ export declare function reporterArgs(outputFilePath: string): string[];
26
+ /**
27
+ * Reads and parses `reportPath` end to end. `null` on ANY failure (missing
28
+ * file — vitest crashed before writing it; malformed JSON; a shape this
29
+ * function doesn't recognize) so the caller falls back to the raw runner
30
+ * output instead of the curated appendix, and never to a crash.
31
+ */
32
+ export declare function parseTestReport(reportPath: string, projectDir: string): ParsedTestReport | null;
@@ -0,0 +1,255 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ /**
6
+ * Parses vitest's `--reporter=json` output into the curated-failures
7
+ * appendix `ekanos test` renders locally, giving a partner the same
8
+ * per-test intelligence the server-side gate already has.
9
+ *
10
+ * This is a DELIBERATE, small duplication of the parsing the host's own
11
+ * gate uses to grade a submission's test run — not an import. `@ekanos/
12
+ * checks` is never published and never a dependency of a partner's project
13
+ * or of this CLI; keep the two in sync by eye if vitest's JSON reporter
14
+ * shape ever changes.
15
+ *
16
+ * Every entry point here is defensive end to end: a missing report file,
17
+ * malformed JSON, or an unrecognized shape all resolve to `null` rather than
18
+ * throwing. A partner's toolchain misbehaving must never crash `ekanos
19
+ * test` — it can only mean the curated appendix is unavailable, with the
20
+ * command falling back to exactly today's raw-passthrough behavior.
21
+ */
22
+ /** Cap on assertion-diff lines shown per finding — see `extractDiff`. */
23
+ const MAX_DIFF_LINES = 10;
24
+ function isRecord(value) {
25
+ return typeof value === 'object' && value !== null;
26
+ }
27
+ /**
28
+ * Whether the project's OWN `test` script names vitest — the one signal
29
+ * used to decide whether to inject the JSON reporter at all. Deliberately
30
+ * conservative: a jest project, a custom node runner, or a shell wrapper is
31
+ * left completely alone, with no reporter flags appended and no report
32
+ * parsed — `ekanos test` behaves exactly as it did before this feature.
33
+ */
34
+ export function projectTestScriptUsesVitest(projectDir) {
35
+ const manifestPath = path.join(projectDir, 'package.json');
36
+ try {
37
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
38
+ if (!isRecord(manifest) || !isRecord(manifest.scripts))
39
+ return false;
40
+ const script = manifest.scripts.test;
41
+ return typeof script === 'string' && script.includes('vitest');
42
+ }
43
+ catch (_a) {
44
+ return false;
45
+ }
46
+ }
47
+ /** A fresh, unique path for the JSON report — one per invocation. The caller owns cleanup. */
48
+ export function newReportPath() {
49
+ return path.join(os.tmpdir(), `ekanos-test-vitest-${crypto.randomUUID()}.json`);
50
+ }
51
+ /** The extra argv that makes vitest ALSO emit a JSON report alongside its normal terminal output. */
52
+ export function reporterArgs(outputFilePath) {
53
+ return [
54
+ '--reporter=default',
55
+ '--reporter=json',
56
+ `--outputFile=${outputFilePath}`,
57
+ ];
58
+ }
59
+ /** The first non-empty line of a vitest failure message/stack — the
60
+ * assertion or thrown error itself, not the frames beneath it. */
61
+ function firstMeaningfulLine(text) {
62
+ const line = text
63
+ .split('\n')
64
+ .map((entry) => entry.trim())
65
+ .find((entry) => entry.length > 0);
66
+ return line !== null && line !== void 0 ? line : 'Test failed with no error message.';
67
+ }
68
+ /**
69
+ * Vitest's failure stacks always include the absolute test file path at
70
+ * least once — at the failure site itself — with the frames beneath it
71
+ * being the runner's own internals. The FIRST occurrence is the line that
72
+ * matters.
73
+ */
74
+ function lineForFile(stack, absoluteFile) {
75
+ const escaped = absoluteFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
76
+ const match = new RegExp(`${escaped}:(\\d+):\\d+`).exec(stack);
77
+ return match ? Number(match[1]) : undefined;
78
+ }
79
+ /**
80
+ * Up to `MAX_DIFF_LINES` lines of an expected/received diff block from one
81
+ * vitest failure message, or `null` when the message doesn't carry one.
82
+ *
83
+ * NOTE (found via manual end-to-end verification, not a hypothetical):
84
+ * vitest's OWN `toBe`/`toEqual`-family matchers never put this in the JSON
85
+ * reporter's `failureMessages` — the pretty "- Expected / + Received" block
86
+ * is a terminal-only rendering vitest's default reporter computes live from
87
+ * the error's `expected`/`actual`, and is never serialized into the report
88
+ * (checked vitest 3.2.7 and 4.1.10; `message`/`stack` is all that ships).
89
+ * This function still earns its place: a thrown `AssertionError` whose
90
+ * `.message` already embeds diff-formatted text — a custom matcher, a
91
+ * different assertion library layered on top of vitest, a hand-built
92
+ * `expect.extend` — DOES carry it verbatim in `failureMessages`, and this is
93
+ * what picks it up. It is intentionally a no-op for the common case rather
94
+ * than reconstructing a diff ourselves (a real diff needs the actual/expected
95
+ * values, which the JSON reporter doesn't expose, and computing one would
96
+ * mean a new dependency, and this package deliberately keeps its runtime
97
+ * dependency set to picocolors + @clack/prompts only).
98
+ *
99
+ * The diff sits between the failure's own title line and the stack frames
100
+ * (`at ...`) beneath it; within that span, only unified-diff-style `-`/`+`
101
+ * lines are kept — the jest-diff format such a message would use.
102
+ */
103
+ function extractDiff(failureMessage) {
104
+ const lines = failureMessage
105
+ .split('\n')
106
+ .map((line) => line.replace(/\r$/, ''));
107
+ const stackStart = lines.findIndex((line) => /^\s*at\s/.test(line));
108
+ const body = stackStart === -1 ? lines.slice(1) : lines.slice(1, stackStart);
109
+ const diffLines = body.filter((line) => /^\s*[-+]/.test(line) &&
110
+ !line.startsWith('---') &&
111
+ !line.startsWith('+++'));
112
+ if (diffLines.length === 0)
113
+ return null;
114
+ const capped = diffLines.slice(0, MAX_DIFF_LINES);
115
+ const overflow = diffLines.length - capped.length;
116
+ if (overflow > 0) {
117
+ capped.push(`… and ${overflow} more diff line${overflow === 1 ? '' : 's'}`);
118
+ }
119
+ return capped.join('\n');
120
+ }
121
+ /** `path.relative` against the project root, falling back to the raw
122
+ * absolute path if the two disagree on realpath-vs-given-path (a symlinked
123
+ * temp dir resolving differently between this process and the child)
124
+ * rather than emitting a `../../../`-laden mess. */
125
+ function archiveRelative(projectDir, absoluteFile) {
126
+ const direct = path.relative(projectDir, absoluteFile);
127
+ if (!direct.startsWith('..'))
128
+ return direct;
129
+ try {
130
+ const realProjectDir = fs.realpathSync(projectDir);
131
+ const viaRealpath = path.relative(realProjectDir, absoluteFile);
132
+ if (!viaRealpath.startsWith('..'))
133
+ return viaRealpath;
134
+ }
135
+ catch (_a) {
136
+ // realpathSync failing (e.g. the directory was already torn down) just
137
+ // means we fall through to the direct (if imperfect) relative path.
138
+ }
139
+ return direct;
140
+ }
141
+ /**
142
+ * Turns a parsed `--reporter=json` report into one `ParsedFailure` per
143
+ * failed test, across every test file in the run. Returns `null` — not
144
+ * `[]` — for anything that isn't recognizably a vitest JSON report at all,
145
+ * so the caller can tell "a real report with zero structured failures"
146
+ * apart from "not a report" (the latter falls back to the raw runner
147
+ * output; the former is a genuinely clean, if unexpected, run).
148
+ */
149
+ function extractFailures(report) {
150
+ var _a;
151
+ if (!isRecord(report) || !Array.isArray(report.testResults))
152
+ return null;
153
+ const failures = [];
154
+ for (const fileResult of report.testResults) {
155
+ if (!isRecord(fileResult))
156
+ continue;
157
+ const file = fileResult.name;
158
+ const assertionResults = fileResult.assertionResults;
159
+ if (typeof file !== 'string' || !Array.isArray(assertionResults))
160
+ continue;
161
+ for (const assertion of assertionResults) {
162
+ if (!isRecord(assertion) || assertion.status !== 'failed')
163
+ continue;
164
+ const ancestorTitles = Array.isArray(assertion.ancestorTitles)
165
+ ? assertion.ancestorTitles.filter((entry) => typeof entry === 'string')
166
+ : [];
167
+ const title = typeof assertion.title === 'string'
168
+ ? assertion.title
169
+ : '(unnamed test)';
170
+ const failureMessages = Array.isArray(assertion.failureMessages)
171
+ ? assertion.failureMessages.filter((entry) => typeof entry === 'string')
172
+ : [];
173
+ const firstError = (_a = failureMessages[0]) !== null && _a !== void 0 ? _a : '';
174
+ const failure = {
175
+ file,
176
+ suite: ancestorTitles.join(' > '),
177
+ title,
178
+ message: firstMeaningfulLine(firstError),
179
+ };
180
+ const line = lineForFile(firstError, file);
181
+ if (line !== undefined)
182
+ failure.line = line;
183
+ const detail = extractDiff(firstError);
184
+ if (detail !== null)
185
+ failure.detail = detail;
186
+ failures.push(failure);
187
+ }
188
+ }
189
+ return failures;
190
+ }
191
+ /** `null` unless the report carries vitest's own aggregate pass/fail counts. */
192
+ function extractSummary(report) {
193
+ if (!isRecord(report) || !Array.isArray(report.testResults))
194
+ return null;
195
+ const passed = report.numPassedTests;
196
+ const failed = report.numFailedTests;
197
+ if (typeof passed !== 'number' || typeof failed !== 'number')
198
+ return null;
199
+ return { passed, failed, files: report.testResults.length };
200
+ }
201
+ /**
202
+ * Reads and parses `reportPath` end to end. `null` on ANY failure (missing
203
+ * file — vitest crashed before writing it; malformed JSON; a shape this
204
+ * function doesn't recognize) so the caller falls back to the raw runner
205
+ * output instead of the curated appendix, and never to a crash.
206
+ */
207
+ export function parseTestReport(reportPath, projectDir) {
208
+ let raw;
209
+ try {
210
+ raw = fs.readFileSync(reportPath, 'utf8');
211
+ }
212
+ catch (_a) {
213
+ return null;
214
+ }
215
+ let report;
216
+ try {
217
+ report = JSON.parse(raw);
218
+ }
219
+ catch (_b) {
220
+ return null;
221
+ }
222
+ const failures = extractFailures(report);
223
+ if (failures === null)
224
+ return null;
225
+ const findings = [];
226
+ const failingFiles = [];
227
+ const seenFiles = new Set();
228
+ for (const failure of failures) {
229
+ const relFile = archiveRelative(projectDir, failure.file);
230
+ if (!seenFiles.has(relFile)) {
231
+ seenFiles.add(relFile);
232
+ failingFiles.push(relFile);
233
+ }
234
+ const label = failure.suite
235
+ ? `${failure.suite} > ${failure.title}`
236
+ : failure.title;
237
+ const finding = {
238
+ // Grouping by test FILE (not by suite/title) is deliberate — the
239
+ // shared renderer groups by `check`, so this makes the group header
240
+ // show the file once with every failing test beneath it.
241
+ check: `tests/${relFile}`,
242
+ severity: 'error',
243
+ file: relFile,
244
+ message: `${label}: ${failure.message}`,
245
+ hint: 'Fix the failing assertion, then re-run "ekanos test".',
246
+ };
247
+ if (failure.line !== undefined)
248
+ finding.line = failure.line;
249
+ if (failure.detail !== undefined)
250
+ finding.detail = failure.detail;
251
+ findings.push(finding);
252
+ }
253
+ return { findings, failingFiles, summary: extractSummary(report) };
254
+ }
255
+ //# sourceMappingURL=test-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-report.js","sourceRoot":"","sources":["../src/test-report.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport type { RenderableFinding } from './findings-renderer';\n\n/**\n * Parses vitest's `--reporter=json` output into the curated-failures\n * appendix `ekanos test` renders locally, giving a partner the same\n * per-test intelligence the server-side gate already has.\n *\n * This is a DELIBERATE, small duplication of the parsing the host's own\n * gate uses to grade a submission's test run — not an import. `@ekanos/\n * checks` is never published and never a dependency of a partner's project\n * or of this CLI; keep the two in sync by eye if vitest's JSON reporter\n * shape ever changes.\n *\n * Every entry point here is defensive end to end: a missing report file,\n * malformed JSON, or an unrecognized shape all resolve to `null` rather than\n * throwing. A partner's toolchain misbehaving must never crash `ekanos\n * test` — it can only mean the curated appendix is unavailable, with the\n * command falling back to exactly today's raw-passthrough behavior.\n */\n\n/** Cap on assertion-diff lines shown per finding — see `extractDiff`. */\nconst MAX_DIFF_LINES = 10;\n\nexport interface TestSummary {\n passed: number;\n failed: number;\n files: number;\n}\n\nexport interface ParsedTestReport {\n /** One finding per failing test, grouped (via `check`) per test FILE. */\n findings: RenderableFinding[];\n /** Relative paths of files with at least one failure, first-seen order. */\n failingFiles: string[];\n summary: TestSummary | null;\n}\n\ninterface ParsedFailure {\n /** Absolute path, exactly as vitest reported it. */\n file: string;\n suite: string;\n title: string;\n message: string;\n line?: number;\n detail?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Whether the project's OWN `test` script names vitest — the one signal\n * used to decide whether to inject the JSON reporter at all. Deliberately\n * conservative: a jest project, a custom node runner, or a shell wrapper is\n * left completely alone, with no reporter flags appended and no report\n * parsed — `ekanos test` behaves exactly as it did before this feature.\n */\nexport function projectTestScriptUsesVitest(projectDir: string): boolean {\n const manifestPath = path.join(projectDir, 'package.json');\n\n try {\n const manifest: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n if (!isRecord(manifest) || !isRecord(manifest.scripts)) return false;\n\n const script = manifest.scripts.test;\n return typeof script === 'string' && script.includes('vitest');\n } catch {\n return false;\n }\n}\n\n/** A fresh, unique path for the JSON report — one per invocation. The caller owns cleanup. */\nexport function newReportPath(): string {\n return path.join(\n os.tmpdir(),\n `ekanos-test-vitest-${crypto.randomUUID()}.json`,\n );\n}\n\n/** The extra argv that makes vitest ALSO emit a JSON report alongside its normal terminal output. */\nexport function reporterArgs(outputFilePath: string): string[] {\n return [\n '--reporter=default',\n '--reporter=json',\n `--outputFile=${outputFilePath}`,\n ];\n}\n\n/** The first non-empty line of a vitest failure message/stack — the\n * assertion or thrown error itself, not the frames beneath it. */\nfunction firstMeaningfulLine(text: string): string {\n const line = text\n .split('\\n')\n .map((entry) => entry.trim())\n .find((entry) => entry.length > 0);\n return line ?? 'Test failed with no error message.';\n}\n\n/**\n * Vitest's failure stacks always include the absolute test file path at\n * least once — at the failure site itself — with the frames beneath it\n * being the runner's own internals. The FIRST occurrence is the line that\n * matters.\n */\nfunction lineForFile(stack: string, absoluteFile: string): number | undefined {\n const escaped = absoluteFile.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const match = new RegExp(`${escaped}:(\\\\d+):\\\\d+`).exec(stack);\n return match ? Number(match[1]) : undefined;\n}\n\n/**\n * Up to `MAX_DIFF_LINES` lines of an expected/received diff block from one\n * vitest failure message, or `null` when the message doesn't carry one.\n *\n * NOTE (found via manual end-to-end verification, not a hypothetical):\n * vitest's OWN `toBe`/`toEqual`-family matchers never put this in the JSON\n * reporter's `failureMessages` — the pretty \"- Expected / + Received\" block\n * is a terminal-only rendering vitest's default reporter computes live from\n * the error's `expected`/`actual`, and is never serialized into the report\n * (checked vitest 3.2.7 and 4.1.10; `message`/`stack` is all that ships).\n * This function still earns its place: a thrown `AssertionError` whose\n * `.message` already embeds diff-formatted text — a custom matcher, a\n * different assertion library layered on top of vitest, a hand-built\n * `expect.extend` — DOES carry it verbatim in `failureMessages`, and this is\n * what picks it up. It is intentionally a no-op for the common case rather\n * than reconstructing a diff ourselves (a real diff needs the actual/expected\n * values, which the JSON reporter doesn't expose, and computing one would\n * mean a new dependency, and this package deliberately keeps its runtime\n * dependency set to picocolors + @clack/prompts only).\n *\n * The diff sits between the failure's own title line and the stack frames\n * (`at ...`) beneath it; within that span, only unified-diff-style `-`/`+`\n * lines are kept — the jest-diff format such a message would use.\n */\nfunction extractDiff(failureMessage: string): string | null {\n const lines = failureMessage\n .split('\\n')\n .map((line) => line.replace(/\\r$/, ''));\n const stackStart = lines.findIndex((line) => /^\\s*at\\s/.test(line));\n const body = stackStart === -1 ? lines.slice(1) : lines.slice(1, stackStart);\n\n const diffLines = body.filter(\n (line) =>\n /^\\s*[-+]/.test(line) &&\n !line.startsWith('---') &&\n !line.startsWith('+++'),\n );\n if (diffLines.length === 0) return null;\n\n const capped = diffLines.slice(0, MAX_DIFF_LINES);\n const overflow = diffLines.length - capped.length;\n if (overflow > 0) {\n capped.push(`… and ${overflow} more diff line${overflow === 1 ? '' : 's'}`);\n }\n return capped.join('\\n');\n}\n\n/** `path.relative` against the project root, falling back to the raw\n * absolute path if the two disagree on realpath-vs-given-path (a symlinked\n * temp dir resolving differently between this process and the child)\n * rather than emitting a `../../../`-laden mess. */\nfunction archiveRelative(projectDir: string, absoluteFile: string): string {\n const direct = path.relative(projectDir, absoluteFile);\n if (!direct.startsWith('..')) return direct;\n try {\n const realProjectDir = fs.realpathSync(projectDir);\n const viaRealpath = path.relative(realProjectDir, absoluteFile);\n if (!viaRealpath.startsWith('..')) return viaRealpath;\n } catch {\n // realpathSync failing (e.g. the directory was already torn down) just\n // means we fall through to the direct (if imperfect) relative path.\n }\n return direct;\n}\n\n/**\n * Turns a parsed `--reporter=json` report into one `ParsedFailure` per\n * failed test, across every test file in the run. Returns `null` — not\n * `[]` — for anything that isn't recognizably a vitest JSON report at all,\n * so the caller can tell \"a real report with zero structured failures\"\n * apart from \"not a report\" (the latter falls back to the raw runner\n * output; the former is a genuinely clean, if unexpected, run).\n */\nfunction extractFailures(report: unknown): ParsedFailure[] | null {\n if (!isRecord(report) || !Array.isArray(report.testResults)) return null;\n\n const failures: ParsedFailure[] = [];\n for (const fileResult of report.testResults as unknown[]) {\n if (!isRecord(fileResult)) continue;\n const file = fileResult.name;\n const assertionResults = fileResult.assertionResults;\n if (typeof file !== 'string' || !Array.isArray(assertionResults)) continue;\n\n for (const assertion of assertionResults as unknown[]) {\n if (!isRecord(assertion) || assertion.status !== 'failed') continue;\n\n const ancestorTitles = Array.isArray(assertion.ancestorTitles)\n ? assertion.ancestorTitles.filter(\n (entry): entry is string => typeof entry === 'string',\n )\n : [];\n const title =\n typeof assertion.title === 'string'\n ? assertion.title\n : '(unnamed test)';\n const failureMessages = Array.isArray(assertion.failureMessages)\n ? assertion.failureMessages.filter(\n (entry): entry is string => typeof entry === 'string',\n )\n : [];\n const firstError = failureMessages[0] ?? '';\n\n const failure: ParsedFailure = {\n file,\n suite: ancestorTitles.join(' > '),\n title,\n message: firstMeaningfulLine(firstError),\n };\n const line = lineForFile(firstError, file);\n if (line !== undefined) failure.line = line;\n const detail = extractDiff(firstError);\n if (detail !== null) failure.detail = detail;\n\n failures.push(failure);\n }\n }\n return failures;\n}\n\n/** `null` unless the report carries vitest's own aggregate pass/fail counts. */\nfunction extractSummary(report: unknown): TestSummary | null {\n if (!isRecord(report) || !Array.isArray(report.testResults)) return null;\n\n const passed = report.numPassedTests;\n const failed = report.numFailedTests;\n if (typeof passed !== 'number' || typeof failed !== 'number') return null;\n\n return { passed, failed, files: report.testResults.length };\n}\n\n/**\n * Reads and parses `reportPath` end to end. `null` on ANY failure (missing\n * file — vitest crashed before writing it; malformed JSON; a shape this\n * function doesn't recognize) so the caller falls back to the raw runner\n * output instead of the curated appendix, and never to a crash.\n */\nexport function parseTestReport(\n reportPath: string,\n projectDir: string,\n): ParsedTestReport | null {\n let raw: string;\n try {\n raw = fs.readFileSync(reportPath, 'utf8');\n } catch {\n return null;\n }\n\n let report: unknown;\n try {\n report = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const failures = extractFailures(report);\n if (failures === null) return null;\n\n const findings: RenderableFinding[] = [];\n const failingFiles: string[] = [];\n const seenFiles = new Set<string>();\n\n for (const failure of failures) {\n const relFile = archiveRelative(projectDir, failure.file);\n if (!seenFiles.has(relFile)) {\n seenFiles.add(relFile);\n failingFiles.push(relFile);\n }\n\n const label = failure.suite\n ? `${failure.suite} > ${failure.title}`\n : failure.title;\n const finding: RenderableFinding = {\n // Grouping by test FILE (not by suite/title) is deliberate — the\n // shared renderer groups by `check`, so this makes the group header\n // show the file once with every failing test beneath it.\n check: `tests/${relFile}`,\n severity: 'error',\n file: relFile,\n message: `${label}: ${failure.message}`,\n hint: 'Fix the failing assertion, then re-run \"ekanos test\".',\n };\n if (failure.line !== undefined) finding.line = failure.line;\n if (failure.detail !== undefined) finding.detail = failure.detail;\n findings.push(finding);\n }\n\n return { findings, failingFiles, summary: extractSummary(report) };\n}\n"]}
@@ -5,11 +5,11 @@ import { type ToolchainNote, type ToolchainVersions } from './toolchain.js';
5
5
  * ---------------------------------------------------------------------------
6
6
  * WIRE CONTRACT — the seam between this package and the Fusion web app.
7
7
  *
8
- * The server side lives in `apps/web/app/api/partner/toolchain/route.ts` and
9
- * ships on a DIFFERENT BRANCH from this file, same as every other route in
10
- * `auth/fusion-api.ts` and `publish-api.ts`. Nothing here imports from
11
- * `apps/web`; the two halves are coupled only by HTTP, and the tests stub
12
- * `fetch`, never the route.
8
+ * The server side is the host's `/api/partner/toolchain` route and ships on
9
+ * a DIFFERENT BRANCH from this file, same as every other route in
10
+ * `auth/fusion-api.ts` and `publish-api.ts`. Nothing here imports host code;
11
+ * the two halves are coupled only by HTTP, and the tests stub `fetch`,
12
+ * never the route.
13
13
  *
14
14
  * GET /api/partner/toolchain [Authorization: Bearer <access_token>]
15
15
  * → 200 { ok: true, data: {
@@ -1 +1 @@
1
- {"version":3,"file":"toolchain-api.js","sourceRoot":"","sources":["../src/toolchain-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n TOOLCHAIN_PACKAGES,\n type ToolchainNote,\n type ToolchainVersions,\n} from './toolchain';\n\n/**\n * The CLI's client for the Fusion toolchain-advisory route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/toolchain/route.ts` and\n * ships on a DIFFERENT BRANCH from this file, same as every other route in\n * `auth/fusion-api.ts` and `publish-api.ts`. Nothing here imports from\n * `apps/web`; the two halves are coupled only by HTTP, and the tests stub\n * `fetch`, never the route.\n *\n * GET /api/partner/toolchain [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: {\n * current: { cli, sdk, ui, integrationSchema, harness }, // the\n * // coherent\n * // set the\n * // host\n * // recommends\n * minimum: { cli, sdk, ui, integrationSchema, harness }, // below\n * // this the\n * // host\n * // refuses\n * notes?: [{ level: \"warn\" | \"info\", message: string }],\n * } }\n * → any other status, or a body that fails to parse or match this shape\n *\n * This endpoint is treated as ADVISORY, not authoritative in the way the auth\n * and submission routes are: a 404 (an older Fusion deployment that predates\n * this route) or a 501 (deliberately unimplemented) are both entirely\n * ordinary, and a 4xx/5xx/network failure/malformed body all mean exactly the\n * same thing to every caller — \"the endpoint is unavailable, fall back to the\n * npm registry\" — so this module never throws. There is no not-configured\n * distinction to make here the way `cli-config` has: a caller with no way to\n * reach this route at all (no host, no session) never calls it in the first\n * place; see `toolchain-resolve.ts`.\n *\n * Unknown extra fields in the response are tolerated (only the fields above\n * are read); a missing `notes` defaults to an empty array.\n * ---------------------------------------------------------------------------\n */\n\nexport interface ToolchainReport {\n current: ToolchainVersions;\n minimum: ToolchainVersions;\n notes: ToolchainNote[];\n}\n\nexport type FetchToolchainResult =\n { status: 'ok'; report: ToolchainReport } | { status: 'unavailable' };\n\nexport async function fetchToolchainReport(\n host: string,\n accessToken: string,\n): Promise<FetchToolchainResult> {\n try {\n const response = await request(`${host}/api/partner/toolchain`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (!response.ok) return { status: 'unavailable' };\n\n const body: unknown = await response.json();\n const report = parseReport(body);\n\n return report ? { status: 'ok', report } : { status: 'unavailable' };\n } catch {\n // Network failure, timeout, or a body that is not even JSON — all read\n // the same: the endpoint is unavailable this time, fall back to npm.\n return { status: 'unavailable' };\n }\n}\n\nfunction parseReport(body: unknown): ToolchainReport | null {\n if (typeof body !== 'object' || body === null) return null;\n const data = (body as { data?: unknown }).data;\n if (typeof data !== 'object' || data === null) return null;\n\n const current = parseVersions((data as { current?: unknown }).current);\n const minimum = parseVersions((data as { minimum?: unknown }).minimum);\n if (!current || !minimum) return null;\n\n const rawNotes = (data as { notes?: unknown }).notes;\n const notes = Array.isArray(rawNotes) ? rawNotes.filter(isNote) : [];\n\n return { current, minimum, notes };\n}\n\nfunction parseVersions(value: unknown): ToolchainVersions | null {\n if (typeof value !== 'object' || value === null) return null;\n const record = value as Record<string, unknown>;\n const result: Partial<ToolchainVersions> = {};\n\n for (const key of Object.keys(\n TOOLCHAIN_PACKAGES,\n ) as (keyof ToolchainVersions)[]) {\n const raw = record[key];\n if (typeof raw !== 'string' || raw.length === 0) return null;\n result[key] = raw;\n }\n\n return result as ToolchainVersions;\n}\n\nfunction isNote(value: unknown): value is ToolchainNote {\n if (typeof value !== 'object' || value === null) return false;\n const note = value as { level?: unknown; message?: unknown };\n return (\n (note.level === 'warn' || note.level === 'info') &&\n typeof note.message === 'string'\n );\n}\n"]}
1
+ {"version":3,"file":"toolchain-api.js","sourceRoot":"","sources":["../src/toolchain-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n TOOLCHAIN_PACKAGES,\n type ToolchainNote,\n type ToolchainVersions,\n} from './toolchain';\n\n/**\n * The CLI's client for the Fusion toolchain-advisory route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side is the host's `/api/partner/toolchain` route and ships on\n * a DIFFERENT BRANCH from this file, same as every other route in\n * `auth/fusion-api.ts` and `publish-api.ts`. Nothing here imports host code;\n * the two halves are coupled only by HTTP, and the tests stub `fetch`,\n * never the route.\n *\n * GET /api/partner/toolchain [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: {\n * current: { cli, sdk, ui, integrationSchema, harness }, // the\n * // coherent\n * // set the\n * // host\n * // recommends\n * minimum: { cli, sdk, ui, integrationSchema, harness }, // below\n * // this the\n * // host\n * // refuses\n * notes?: [{ level: \"warn\" | \"info\", message: string }],\n * } }\n * → any other status, or a body that fails to parse or match this shape\n *\n * This endpoint is treated as ADVISORY, not authoritative in the way the auth\n * and submission routes are: a 404 (an older Fusion deployment that predates\n * this route) or a 501 (deliberately unimplemented) are both entirely\n * ordinary, and a 4xx/5xx/network failure/malformed body all mean exactly the\n * same thing to every caller — \"the endpoint is unavailable, fall back to the\n * npm registry\" — so this module never throws. There is no not-configured\n * distinction to make here the way `cli-config` has: a caller with no way to\n * reach this route at all (no host, no session) never calls it in the first\n * place; see `toolchain-resolve.ts`.\n *\n * Unknown extra fields in the response are tolerated (only the fields above\n * are read); a missing `notes` defaults to an empty array.\n * ---------------------------------------------------------------------------\n */\n\nexport interface ToolchainReport {\n current: ToolchainVersions;\n minimum: ToolchainVersions;\n notes: ToolchainNote[];\n}\n\nexport type FetchToolchainResult =\n { status: 'ok'; report: ToolchainReport } | { status: 'unavailable' };\n\nexport async function fetchToolchainReport(\n host: string,\n accessToken: string,\n): Promise<FetchToolchainResult> {\n try {\n const response = await request(`${host}/api/partner/toolchain`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (!response.ok) return { status: 'unavailable' };\n\n const body: unknown = await response.json();\n const report = parseReport(body);\n\n return report ? { status: 'ok', report } : { status: 'unavailable' };\n } catch {\n // Network failure, timeout, or a body that is not even JSON — all read\n // the same: the endpoint is unavailable this time, fall back to npm.\n return { status: 'unavailable' };\n }\n}\n\nfunction parseReport(body: unknown): ToolchainReport | null {\n if (typeof body !== 'object' || body === null) return null;\n const data = (body as { data?: unknown }).data;\n if (typeof data !== 'object' || data === null) return null;\n\n const current = parseVersions((data as { current?: unknown }).current);\n const minimum = parseVersions((data as { minimum?: unknown }).minimum);\n if (!current || !minimum) return null;\n\n const rawNotes = (data as { notes?: unknown }).notes;\n const notes = Array.isArray(rawNotes) ? rawNotes.filter(isNote) : [];\n\n return { current, minimum, notes };\n}\n\nfunction parseVersions(value: unknown): ToolchainVersions | null {\n if (typeof value !== 'object' || value === null) return null;\n const record = value as Record<string, unknown>;\n const result: Partial<ToolchainVersions> = {};\n\n for (const key of Object.keys(\n TOOLCHAIN_PACKAGES,\n ) as (keyof ToolchainVersions)[]) {\n const raw = record[key];\n if (typeof raw !== 'string' || raw.length === 0) return null;\n result[key] = raw;\n }\n\n return result as ToolchainVersions;\n}\n\nfunction isNote(value: unknown): value is ToolchainNote {\n if (typeof value !== 'object' || value === null) return false;\n const note = value as { level?: unknown; message?: unknown };\n return (\n (note.level === 'warn' || note.level === 'info') &&\n typeof note.message === 'string'\n );\n}\n"]}
package/dist/ui.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The CLI's sole style layer. Every color, glyph, and human-output component
3
+ * lives here — nothing else in this package may `import 'picocolors'` or
4
+ * write a raw ANSI escape (the gradient banner in `banner.ts` is the one
5
+ * documented exception: it paints a true-color gradient picocolors cannot
6
+ * express, but even it asks THIS module's `colorsEnabled` for the on/off
7
+ * decision, so there is exactly one gate).
8
+ *
9
+ * Everything here renders plain strings. Printing is still `CliContext`'s
10
+ * job (`ctx.log`) — this module never touches a stream directly, so the
11
+ * stdout-purity invariant in `context.ts` cannot be bypassed by a stray
12
+ * color call.
13
+ */
14
+ /** What `colorsEnabled` needs to know to decide — never the real stream. */
15
+ export interface ColorProbe {
16
+ /** Whether the destination (stderr, always — see `context.ts`) is a TTY. */
17
+ isTTY: boolean;
18
+ env: Readonly<Record<string, string | undefined>>;
19
+ }
20
+ /**
21
+ * The one color-on/off decision for the whole CLI. True only when the
22
+ * stream is a real terminal AND `NO_COLOR` is unset AND `TERM` is not
23
+ * `"dumb"` — EXCEPT `FORCE_COLOR`, which overrides everything else on when
24
+ * set to anything other than `"0"` (matching the de facto convention: Node,
25
+ * chalk, and picocolors itself all treat a defined-and-nonzero `FORCE_COLOR`
26
+ * as "yes, even if you don't think you can").
27
+ */
28
+ export declare function colorsEnabled(probe: ColorProbe): boolean;
29
+ export interface Glyphs {
30
+ ok: string;
31
+ err: string;
32
+ warn: string;
33
+ info: string;
34
+ }
35
+ /** One row of `ui.keyValue`. */
36
+ export type KeyValueRow = readonly [key: string, value: string];
37
+ export interface CodeframeOptions {
38
+ /** Not rendered directly — callers already print `file:line` above/around this. Kept for future use and self-documentation at call sites. */
39
+ file?: string;
40
+ /** 1-indexed line to highlight. */
41
+ line: number;
42
+ /** The full source text of `file`. */
43
+ source: string;
44
+ /** Lines of context above/below the target line. Defaults to 2. */
45
+ contextLines?: number;
46
+ }
47
+ export type BadgeKind = 'dim' | 'accent' | 'warn' | 'err';
48
+ export interface Ui {
49
+ readonly colors: boolean;
50
+ readonly glyphs: Glyphs;
51
+ err(text: string): string;
52
+ warn(text: string): string;
53
+ ok(text: string): string;
54
+ accent(text: string): string;
55
+ dim(text: string): string;
56
+ bold(text: string): string;
57
+ /** A dim, uppercased section header. */
58
+ section(title: string): string;
59
+ /** Aligned `key value` rows, 2-space gutter, keys dimmed. */
60
+ keyValue(rows: readonly KeyValueRow[]): string;
61
+ /** A header-having, left/right-aligned table with no box-drawing. */
62
+ table(headers: readonly string[], rows: readonly (readonly (string | number)[])[]): string;
63
+ /** A `→ ` prefixed, accent-colored hint line. */
64
+ hint(text: string): string;
65
+ /**
66
+ * THE closing line of any failure path: a blank line, then a dim
67
+ * `→ <why>: ` (or bare `→ `) followed by the bold remediation command.
68
+ */
69
+ nextAction(cmd: string, why?: string): string;
70
+ /** A Prisma/Vite-style source snippet around one line. */
71
+ codeframe(opts: CodeframeOptions): string;
72
+ /** A bracketed, dim-by-default tag: `[gate · 2h ago]`. */
73
+ badge(text: string, kind?: BadgeKind): string;
74
+ }
75
+ export declare function createUi(probe: ColorProbe): Ui;