@ekanos/cli 0.3.2 → 0.3.3

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 (97) hide show
  1. package/README.md +13 -13
  2. package/dist/auth/credential-store.d.ts +1 -1
  3. package/dist/auth/credential-store.js +3 -2
  4. package/dist/auth/credential-store.js.map +1 -1
  5. package/dist/auth/device-flow.d.ts +1 -1
  6. package/dist/auth/device-flow.js +11 -10
  7. package/dist/auth/device-flow.js.map +1 -1
  8. package/dist/auth/{fusion-api.d.ts → ekanos-api.d.ts} +9 -9
  9. package/dist/auth/{fusion-api.js → ekanos-api.js} +28 -28
  10. package/dist/auth/ekanos-api.js.map +1 -0
  11. package/dist/auth/session.d.ts +5 -5
  12. package/dist/auth/session.js +8 -7
  13. package/dist/auth/session.js.map +1 -1
  14. package/dist/bin.js.map +1 -1
  15. package/dist/commands/dev.d.ts +2 -2
  16. package/dist/commands/dev.js.map +1 -1
  17. package/dist/commands/init.js +4 -0
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/login.d.ts +1 -1
  20. package/dist/commands/login.js +3 -3
  21. package/dist/commands/login.js.map +1 -1
  22. package/dist/commands/logout.js +1 -1
  23. package/dist/commands/logout.js.map +1 -1
  24. package/dist/commands/publish.d.ts +2 -2
  25. package/dist/commands/publish.js +3 -3
  26. package/dist/commands/publish.js.map +1 -1
  27. package/dist/commands/sources.js.map +1 -1
  28. package/dist/commands/status.js +5 -7
  29. package/dist/commands/status.js.map +1 -1
  30. package/dist/commands/test.js.map +1 -1
  31. package/dist/commands/upgrade.js +1 -1
  32. package/dist/commands/upgrade.js.map +1 -1
  33. package/dist/commands/use.js +1 -1
  34. package/dist/commands/use.js.map +1 -1
  35. package/dist/commands/validate.js.map +1 -1
  36. package/dist/commands/whoami.d.ts +1 -1
  37. package/dist/commands/whoami.js +1 -1
  38. package/dist/commands/whoami.js.map +1 -1
  39. package/dist/compatibility.js.map +1 -1
  40. package/dist/context.js.map +1 -1
  41. package/dist/delegate.js.map +1 -1
  42. package/dist/did-you-mean.js +26 -15
  43. package/dist/did-you-mean.js.map +1 -1
  44. package/dist/exit-codes.d.ts +10 -10
  45. package/dist/findings-renderer.js +6 -4
  46. package/dist/findings-renderer.js.map +1 -1
  47. package/dist/generated/fa-free-names.d.ts +1 -1
  48. package/dist/generated/fa-free-names.js +1 -1
  49. package/dist/generated/fa-free-names.js.map +1 -1
  50. package/dist/harness-scaffold.d.ts +1 -1
  51. package/dist/harness-scaffold.js +4 -2
  52. package/dist/harness-scaffold.js.map +1 -1
  53. package/dist/icon-findings.d.ts +1 -1
  54. package/dist/icon-findings.js +2 -2
  55. package/dist/icon-findings.js.map +1 -1
  56. package/dist/index.js +2 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/merge-manifest.js.map +1 -1
  59. package/dist/pack.js +1 -1
  60. package/dist/pack.js.map +1 -1
  61. package/dist/project-checks.js.map +1 -1
  62. package/dist/project.d.ts +7 -84
  63. package/dist/project.js +3 -3
  64. package/dist/project.js.map +1 -1
  65. package/dist/publish-api.d.ts +5 -5
  66. package/dist/publish-api.js +27 -33
  67. package/dist/publish-api.js.map +1 -1
  68. package/dist/safe-fs.js.map +1 -1
  69. package/dist/schema-skew.js.map +1 -1
  70. package/dist/source-locator.js.map +1 -1
  71. package/dist/sources-api.d.ts +3 -3
  72. package/dist/sources-api.js +11 -14
  73. package/dist/sources-api.js.map +1 -1
  74. package/dist/templates.js.map +1 -1
  75. package/dist/test-report.js.map +1 -1
  76. package/dist/toolchain-api.d.ts +4 -4
  77. package/dist/toolchain-api.js +1 -1
  78. package/dist/toolchain-api.js.map +1 -1
  79. package/dist/toolchain-resolve.js.map +1 -1
  80. package/dist/toolchain.d.ts +5 -5
  81. package/dist/ui.js +2 -8
  82. package/dist/ui.js.map +1 -1
  83. package/dist/update-notice.d.ts +1 -1
  84. package/dist/update-notice.js.map +1 -1
  85. package/dist/validate-findings.js.map +1 -1
  86. package/dist/wizard.js.map +1 -1
  87. package/package.json +15 -20
  88. package/templates/AGENTS.md.tmpl +13 -8
  89. package/templates/CLAUDE.md.tmpl +1 -1
  90. package/templates/claude-skill.md.tmpl +2 -2
  91. package/templates/harness/next.config.mjs.tmpl +1 -1
  92. package/templates/package.json.tmpl +6 -5
  93. package/templates/pnpm-workspace.yaml.tmpl +8 -0
  94. package/templates/skills/build-integration/SKILL.md +3 -3
  95. package/templates/skills/build-integration/references/api-wiring.md +2 -2
  96. package/templates/skills/build-integration/references/widget-design.md +1 -1
  97. package/dist/auth/fusion-api.js.map +0 -1
package/dist/ui.js CHANGED
@@ -74,15 +74,9 @@ export function createUi(probe) {
74
74
  const aligns = Array.from({ length: columnCount }, (_, col) => rows.length > 0 && rows.every((row) => { var _a; return isNumeric((_a = row[col]) !== null && _a !== void 0 ? _a : ''); })
75
75
  ? 'right'
76
76
  : 'left');
77
- const widths = Array.from({ length: columnCount }, (_, col) => {
78
- var _a, _b;
79
- return Math.max((_b = (_a = headers[col]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, ...rows.map((row) => { var _a; return String((_a = row[col]) !== null && _a !== void 0 ? _a : '').length; }));
80
- });
77
+ const widths = Array.from({ length: columnCount }, (_, col) => { var _a; var _b; return Math.max((_b = (_a = headers[col]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, ...rows.map((row) => { var _a; return String((_a = row[col]) !== null && _a !== void 0 ? _a : '').length; })); });
81
78
  const headerLine = headers
82
- .map((header, col) => {
83
- var _a, _b;
84
- return dim(padCell(header.toUpperCase(), (_a = widths[col]) !== null && _a !== void 0 ? _a : 0, (_b = aligns[col]) !== null && _b !== void 0 ? _b : 'left'));
85
- })
79
+ .map((header, col) => { var _a, _b; return dim(padCell(header.toUpperCase(), (_a = widths[col]) !== null && _a !== void 0 ? _a : 0, (_b = aligns[col]) !== null && _b !== void 0 ? _b : 'left')); })
86
80
  .join(' ');
87
81
  const bodyLines = rows.map((row) => row
88
82
  .map((cell, col) => { var _a, _b; return padCell(String(cell), (_a = widths[col]) !== null && _a !== void 0 ? _a : 0, (_b = aligns[col]) !== null && _b !== void 0 ? _b : 'left'); })
package/dist/ui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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","sourcesContent":["import pc from 'picocolors';\n\n/**\n * The CLI's sole style layer. Every color, glyph, and human-output component\n * lives here — nothing else in this package may `import 'picocolors'` or\n * write a raw ANSI escape (the gradient banner in `banner.ts` is the one\n * documented exception: it paints a true-color gradient picocolors cannot\n * express, but even it asks THIS module's `colorsEnabled` for the on/off\n * decision, so there is exactly one gate).\n *\n * Everything here renders plain strings. Printing is still `CliContext`'s\n * job (`ctx.log`) — this module never touches a stream directly, so the\n * stdout-purity invariant in `context.ts` cannot be bypassed by a stray\n * color call.\n */\n\n/** What `colorsEnabled` needs to know to decide — never the real stream. */\nexport interface ColorProbe {\n /** Whether the destination (stderr, always — see `context.ts`) is a TTY. */\n isTTY: boolean;\n env: Readonly<Record<string, string | undefined>>;\n}\n\n/**\n * The one color-on/off decision for the whole CLI. True only when the\n * stream is a real terminal AND `NO_COLOR` is unset AND `TERM` is not\n * `\"dumb\"` — EXCEPT `FORCE_COLOR`, which overrides everything else on when\n * set to anything other than `\"0\"` (matching the de facto convention: Node,\n * chalk, and picocolors itself all treat a defined-and-nonzero `FORCE_COLOR`\n * as \"yes, even if you don't think you can\").\n */\nexport function colorsEnabled(probe: ColorProbe): boolean {\n const { env } = probe;\n\n if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0') return true;\n if (env.NO_COLOR !== undefined) return false;\n if (env.TERM === 'dumb') return false;\n\n return probe.isTTY;\n}\n\n/**\n * Whether the destination can render the box-drawing/Unicode glyphs this\n * module prefers. Deliberately simple: a \"dumb\" terminal never can, and a\n * locale that names a non-UTF-8 charset can't be trusted to either. Anything\n * else — including an unset locale, which is the common case for CI/agent\n * environments — is assumed capable rather than guessed at further.\n */\nfunction unicodeSupported(\n env: Readonly<Record<string, string | undefined>>,\n): boolean {\n if (env.TERM === 'dumb') return false;\n\n const locale = env.LANG ?? env.LC_ALL ?? env.LC_CTYPE ?? '';\n if (locale && !/utf-?8/i.test(locale)) return false;\n\n return true;\n}\n\nexport interface Glyphs {\n ok: string;\n err: string;\n warn: string;\n info: string;\n}\n\nconst UNICODE_GLYPHS: Glyphs = { ok: '✓', err: '✖', warn: '▲', info: '●' };\nconst ASCII_GLYPHS: Glyphs = { ok: 'ok', err: 'x', warn: '!', info: '*' };\n\n/** One row of `ui.keyValue`. */\nexport type KeyValueRow = readonly [key: string, value: string];\n\nexport interface CodeframeOptions {\n /** Not rendered directly — callers already print `file:line` above/around this. Kept for future use and self-documentation at call sites. */\n file?: string;\n /** 1-indexed line to highlight. */\n line: number;\n /** The full source text of `file`. */\n source: string;\n /** Lines of context above/below the target line. Defaults to 2. */\n contextLines?: number;\n}\n\nexport type BadgeKind = 'dim' | 'accent' | 'warn' | 'err';\n\nexport interface Ui {\n readonly colors: boolean;\n readonly glyphs: Glyphs;\n\n err(text: string): string;\n warn(text: string): string;\n ok(text: string): string;\n accent(text: string): string;\n dim(text: string): string;\n bold(text: string): string;\n\n /** A dim, uppercased section header. */\n section(title: string): string;\n /** Aligned `key value` rows, 2-space gutter, keys dimmed. */\n keyValue(rows: readonly KeyValueRow[]): string;\n /** A header-having, left/right-aligned table with no box-drawing. */\n table(\n headers: readonly string[],\n rows: readonly (readonly (string | number)[])[],\n ): string;\n /** A `→ ` prefixed, accent-colored hint line. */\n hint(text: string): string;\n /**\n * THE closing line of any failure path: a blank line, then a dim\n * `→ <why>: ` (or bare `→ `) followed by the bold remediation command.\n */\n nextAction(cmd: string, why?: string): string;\n /** A Prisma/Vite-style source snippet around one line. */\n codeframe(opts: CodeframeOptions): string;\n /** A bracketed, dim-by-default tag: `[gate · 2h ago]`. */\n badge(text: string, kind?: BadgeKind): string;\n}\n\n/** Right-pad (left-align) a cell to `width`, ignoring color codes in length math is the CALLER's job — pad before coloring, never after. */\nfunction padCell(text: string, width: number, align: 'left' | 'right'): string {\n return align === 'right' ? text.padStart(width) : text.padEnd(width);\n}\n\nexport function createUi(probe: ColorProbe): Ui {\n const colors = colorsEnabled(probe);\n const c = pc.createColors(colors);\n const glyphs = unicodeSupported(probe.env) ? UNICODE_GLYPHS : ASCII_GLYPHS;\n\n const err = (text: string) => c.red(text);\n const warn = (text: string) => c.yellow(text);\n const ok = (text: string) => c.green(text);\n const accent = (text: string) => c.cyan(text);\n const dim = (text: string) => c.dim(text);\n const bold = (text: string) => c.bold(text);\n\n return {\n colors,\n glyphs,\n err,\n warn,\n ok,\n accent,\n dim,\n bold,\n\n section(title: string): string {\n return dim(title.toUpperCase());\n },\n\n keyValue(rows: readonly KeyValueRow[]): string {\n const keyWidth = Math.max(0, ...rows.map(([key]) => key.length));\n\n return rows\n .map(([key, value]) => `${dim(key.padEnd(keyWidth))} ${value}`)\n .join('\\n');\n },\n\n table(\n headers: readonly string[],\n rows: readonly (readonly (string | number)[])[],\n ): string {\n const columnCount = headers.length;\n const isNumeric = (value: string | number) =>\n typeof value === 'number' ||\n (value.trim() !== '' && !Number.isNaN(Number(value)));\n\n const aligns: Array<'left' | 'right'> = Array.from(\n { length: columnCount },\n (_, col) =>\n rows.length > 0 && rows.every((row) => isNumeric(row[col] ?? ''))\n ? 'right'\n : 'left',\n );\n\n const widths = Array.from({ length: columnCount }, (_, col) =>\n Math.max(\n headers[col]?.length ?? 0,\n ...rows.map((row) => String(row[col] ?? '').length),\n ),\n );\n\n const headerLine = headers\n .map((header, col) =>\n dim(\n padCell(\n header.toUpperCase(),\n widths[col] ?? 0,\n aligns[col] ?? 'left',\n ),\n ),\n )\n .join(' ');\n\n const bodyLines = rows.map((row) =>\n row\n .map((cell, col) =>\n padCell(String(cell), widths[col] ?? 0, aligns[col] ?? 'left'),\n )\n .join(' '),\n );\n\n return [headerLine, ...bodyLines].join('\\n');\n },\n\n hint(text: string): string {\n return accent(`→ ${text}`);\n },\n\n nextAction(cmd: string, why?: string): string {\n const prefix = why ? `→ ${why}: ` : '→ ';\n return `\\n${dim(prefix)}${bold(cmd)}`;\n },\n\n codeframe(opts: CodeframeOptions): string {\n const contextLines = opts.contextLines ?? 2;\n const lines = opts.source.split('\\n');\n const start = Math.max(1, opts.line - contextLines);\n const end = Math.min(lines.length, opts.line + contextLines);\n const gutterWidth = String(end).length;\n\n const rows: string[] = [];\n for (let n = start; n <= end; n += 1) {\n const isTarget = n === opts.line;\n const text = lines[n - 1] ?? '';\n const marker = isTarget ? '>' : ' ';\n const lineNo = String(n).padStart(gutterWidth);\n const rendered = isTarget ? bold(text) : text;\n\n rows.push(\n `${isTarget ? accent(marker) : marker} ${dim(lineNo)} ${dim('│')} ${rendered}`,\n );\n }\n\n return rows.join('\\n');\n },\n\n badge(text: string, kind: BadgeKind = 'dim'): string {\n const wrapped = `[${text}]`;\n\n switch (kind) {\n case 'err':\n return err(wrapped);\n case 'warn':\n return warn(wrapped);\n case 'accent':\n return accent(wrapped);\n default:\n return dim(wrapped);\n }\n },\n };\n}\n"]}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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","sourcesContent":["import pc from 'picocolors';\n\n/**\n * The CLI's sole style layer. Every color, glyph, and human-output component\n * lives here — nothing else in this package may `import 'picocolors'` or\n * write a raw ANSI escape (the gradient banner in `banner.ts` is the one\n * documented exception: it paints a true-color gradient picocolors cannot\n * express, but even it asks THIS module's `colorsEnabled` for the on/off\n * decision, so there is exactly one gate).\n *\n * Everything here renders plain strings. Printing is still `CliContext`'s\n * job (`ctx.log`) — this module never touches a stream directly, so the\n * stdout-purity invariant in `context.ts` cannot be bypassed by a stray\n * color call.\n */\n\n/** What `colorsEnabled` needs to know to decide — never the real stream. */\nexport interface ColorProbe {\n /** Whether the destination (stderr, always — see `context.ts`) is a TTY. */\n isTTY: boolean;\n env: Readonly<Record<string, string | undefined>>;\n}\n\n/**\n * The one color-on/off decision for the whole CLI. True only when the\n * stream is a real terminal AND `NO_COLOR` is unset AND `TERM` is not\n * `\"dumb\"` — EXCEPT `FORCE_COLOR`, which overrides everything else on when\n * set to anything other than `\"0\"` (matching the de facto convention: Node,\n * chalk, and picocolors itself all treat a defined-and-nonzero `FORCE_COLOR`\n * as \"yes, even if you don't think you can\").\n */\nexport function colorsEnabled(probe: ColorProbe): boolean {\n const { env } = probe;\n\n if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0') return true;\n if (env.NO_COLOR !== undefined) return false;\n if (env.TERM === 'dumb') return false;\n\n return probe.isTTY;\n}\n\n/**\n * Whether the destination can render the box-drawing/Unicode glyphs this\n * module prefers. Deliberately simple: a \"dumb\" terminal never can, and a\n * locale that names a non-UTF-8 charset can't be trusted to either. Anything\n * else — including an unset locale, which is the common case for CI/agent\n * environments — is assumed capable rather than guessed at further.\n */\nfunction unicodeSupported(\n env: Readonly<Record<string, string | undefined>>,\n): boolean {\n if (env.TERM === 'dumb') return false;\n\n const locale = env.LANG ?? env.LC_ALL ?? env.LC_CTYPE ?? '';\n if (locale && !/utf-?8/i.test(locale)) return false;\n\n return true;\n}\n\nexport interface Glyphs {\n ok: string;\n err: string;\n warn: string;\n info: string;\n}\n\nconst UNICODE_GLYPHS: Glyphs = { ok: '✓', err: '✖', warn: '▲', info: '●' };\nconst ASCII_GLYPHS: Glyphs = { ok: 'ok', err: 'x', warn: '!', info: '*' };\n\n/** One row of `ui.keyValue`. */\nexport type KeyValueRow = readonly [key: string, value: string];\n\nexport interface CodeframeOptions {\n /** Not rendered directly — callers already print `file:line` above/around this. Kept for future use and self-documentation at call sites. */\n file?: string;\n /** 1-indexed line to highlight. */\n line: number;\n /** The full source text of `file`. */\n source: string;\n /** Lines of context above/below the target line. Defaults to 2. */\n contextLines?: number;\n}\n\nexport type BadgeKind = 'dim' | 'accent' | 'warn' | 'err';\n\nexport interface Ui {\n readonly colors: boolean;\n readonly glyphs: Glyphs;\n\n err(text: string): string;\n warn(text: string): string;\n ok(text: string): string;\n accent(text: string): string;\n dim(text: string): string;\n bold(text: string): string;\n\n /** A dim, uppercased section header. */\n section(title: string): string;\n /** Aligned `key value` rows, 2-space gutter, keys dimmed. */\n keyValue(rows: readonly KeyValueRow[]): string;\n /** A header-having, left/right-aligned table with no box-drawing. */\n table(\n headers: readonly string[],\n rows: readonly (readonly (string | number)[])[],\n ): string;\n /** A `→ ` prefixed, accent-colored hint line. */\n hint(text: string): string;\n /**\n * THE closing line of any failure path: a blank line, then a dim\n * `→ <why>: ` (or bare `→ `) followed by the bold remediation command.\n */\n nextAction(cmd: string, why?: string): string;\n /** A Prisma/Vite-style source snippet around one line. */\n codeframe(opts: CodeframeOptions): string;\n /** A bracketed, dim-by-default tag: `[gate · 2h ago]`. */\n badge(text: string, kind?: BadgeKind): string;\n}\n\n/** Right-pad (left-align) a cell to `width`, ignoring color codes in length math is the CALLER's job — pad before coloring, never after. */\nfunction padCell(text: string, width: number, align: 'left' | 'right'): string {\n return align === 'right' ? text.padStart(width) : text.padEnd(width);\n}\n\nexport function createUi(probe: ColorProbe): Ui {\n const colors = colorsEnabled(probe);\n const c = pc.createColors(colors);\n const glyphs = unicodeSupported(probe.env) ? UNICODE_GLYPHS : ASCII_GLYPHS;\n\n const err = (text: string) => c.red(text);\n const warn = (text: string) => c.yellow(text);\n const ok = (text: string) => c.green(text);\n const accent = (text: string) => c.cyan(text);\n const dim = (text: string) => c.dim(text);\n const bold = (text: string) => c.bold(text);\n\n return {\n colors,\n glyphs,\n err,\n warn,\n ok,\n accent,\n dim,\n bold,\n\n section(title: string): string {\n return dim(title.toUpperCase());\n },\n\n keyValue(rows: readonly KeyValueRow[]): string {\n const keyWidth = Math.max(0, ...rows.map(([key]) => key.length));\n\n return rows\n .map(([key, value]) => `${dim(key.padEnd(keyWidth))} ${value}`)\n .join('\\n');\n },\n\n table(\n headers: readonly string[],\n rows: readonly (readonly (string | number)[])[],\n ): string {\n const columnCount = headers.length;\n const isNumeric = (value: string | number) =>\n typeof value === 'number' ||\n (value.trim() !== '' && !Number.isNaN(Number(value)));\n\n const aligns: Array<'left' | 'right'> = Array.from(\n { length: columnCount },\n (_, col) =>\n rows.length > 0 && rows.every((row) => isNumeric(row[col] ?? ''))\n ? 'right'\n : 'left',\n );\n\n const widths = Array.from({ length: columnCount }, (_, col) =>\n Math.max(\n headers[col]?.length ?? 0,\n ...rows.map((row) => String(row[col] ?? '').length),\n ),\n );\n\n const headerLine = headers\n .map((header, col) =>\n dim(\n padCell(\n header.toUpperCase(),\n widths[col] ?? 0,\n aligns[col] ?? 'left',\n ),\n ),\n )\n .join(' ');\n\n const bodyLines = rows.map((row) =>\n row\n .map((cell, col) =>\n padCell(String(cell), widths[col] ?? 0, aligns[col] ?? 'left'),\n )\n .join(' '),\n );\n\n return [headerLine, ...bodyLines].join('\\n');\n },\n\n hint(text: string): string {\n return accent(`→ ${text}`);\n },\n\n nextAction(cmd: string, why?: string): string {\n const prefix = why ? `→ ${why}: ` : '→ ';\n return `\\n${dim(prefix)}${bold(cmd)}`;\n },\n\n codeframe(opts: CodeframeOptions): string {\n const contextLines = opts.contextLines ?? 2;\n const lines = opts.source.split('\\n');\n const start = Math.max(1, opts.line - contextLines);\n const end = Math.min(lines.length, opts.line + contextLines);\n const gutterWidth = String(end).length;\n\n const rows: string[] = [];\n for (let n = start; n <= end; n += 1) {\n const isTarget = n === opts.line;\n const text = lines[n - 1] ?? '';\n const marker = isTarget ? '>' : ' ';\n const lineNo = String(n).padStart(gutterWidth);\n const rendered = isTarget ? bold(text) : text;\n\n rows.push(\n `${isTarget ? accent(marker) : marker} ${dim(lineNo)} ${dim('│')} ${rendered}`,\n );\n }\n\n return rows.join('\\n');\n },\n\n badge(text: string, kind: BadgeKind = 'dim'): string {\n const wrapped = `[${text}]`;\n\n switch (kind) {\n case 'err':\n return err(wrapped);\n case 'warn':\n return warn(wrapped);\n case 'accent':\n return accent(wrapped);\n default:\n return dim(wrapped);\n }\n },\n };\n}\n"]}
@@ -10,7 +10,7 @@ export interface ToolchainNoticeData {
10
10
  }
11
11
  export interface CheckForUpdateParams {
12
12
  env: Record<string, string | undefined>;
13
- /** An already-resolved Fusion host/session, when the caller has one handy. */
13
+ /** An already-resolved Ekanos host/session, when the caller has one handy. */
14
14
  host?: string;
15
15
  session?: {
16
16
  accessToken: string;
@@ -1 +1 @@
1
- {"version":3,"file":"update-notice.js","sourceRoot":"","sources":["../src/update-notice.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { CredentialStore, resolveEkanosHome } from './auth/credential-store';\nimport { cliVersion, compareVersions } from './compatibility';\nimport type { CliContext } from './context';\nimport type { ToolchainVersions } from './toolchain';\nimport { resolveToolchainTarget } from './toolchain-resolve';\n\n/**\n * The proactive, NEVER-mutating \"your CLI is behind\" notice. Runs on\n * `validate`, `dev`, `publish` and `status` — the verbs a partner (or an\n * agent acting for one) runs constantly — so staleness is discovered on the\n * next ordinary command instead of only when something breaks.\n *\n * The one rule that governs every line of this file: this check must be\n * INVISIBLE on failure. A partner offline, a flaky registry, a host that\n * predates this endpoint — none of that may add meaningful latency, change a\n * verb's exit code, or so much as touch the JSON envelope's `ok` field.\n * Every failure path below resolves to `null` (\"say nothing\"), never a throw.\n */\n\nconst CACHE_FILENAME = 'update-check.json';\nconst CACHE_DIR_MODE = 0o700;\nconst CACHE_FILE_MODE = 0o600;\nconst TTL_MS = 24 * 60 * 60 * 1000;\n/** The hard ceiling on this check's total network time, whatever else happens. */\nconst DEADLINE_MS = 1500;\n\nexport interface ToolchainNoticeData {\n status: 'behind' | 'unsupported';\n current: { cli: string };\n target: ToolchainVersions;\n upgradeCommand: 'ekanos upgrade';\n}\n\ninterface CacheFile {\n checkedAt: number;\n target: ToolchainVersions;\n minimum?: ToolchainVersions;\n source: 'host' | 'npm';\n}\n\nfunction cachePath(env: Record<string, string | undefined>): string {\n return path.join(resolveEkanosHome(env), '.ekanos', CACHE_FILENAME);\n}\n\nfunction readCache(env: Record<string, string | undefined>): CacheFile | null {\n try {\n const raw = fs.readFileSync(cachePath(env), 'utf8');\n const parsed: unknown = JSON.parse(raw);\n return isCacheFile(parsed) ? parsed : null;\n } catch {\n // Missing, unreadable, or corrupt — all read as \"no cache\", never an\n // error. This check is a courtesy, not a source of truth.\n return null;\n }\n}\n\nfunction writeCache(\n env: Record<string, string | undefined>,\n file: CacheFile,\n): void {\n const target = cachePath(env);\n try {\n fs.mkdirSync(path.dirname(target), {\n recursive: true,\n mode: CACHE_DIR_MODE,\n });\n const tmp = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(file), { mode: CACHE_FILE_MODE });\n fs.renameSync(tmp, target);\n } catch {\n // Best-effort. The next run simply re-checks — this is a cache, not a\n // durable record.\n }\n}\n\nfunction isCacheFile(value: unknown): value is CacheFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<CacheFile>;\n return (\n typeof candidate.checkedAt === 'number' &&\n typeof candidate.target === 'object' &&\n candidate.target !== null &&\n (candidate.source === 'host' || candidate.source === 'npm')\n );\n}\n\n/**\n * The one host/session this check will ever try, resolved WITHOUT any of the\n * throwing machinery in `auth/session.ts` — `resolveHost` is built to demand\n * an answer (a usage error naming the ambiguity) because a real command needs\n * one; this check needs an answer only if one is obvious, and silence\n * otherwise. Exactly one stored host is \"obvious\"; zero or several are not,\n * and this never prompts or guesses to resolve that ambiguity — it just\n * falls back to the npm registry, same as no session at all.\n */\nfunction bestEffortHostSession(\n env: Record<string, string | undefined>,\n): { host: string; session: { accessToken: string } } | null {\n try {\n const store = new CredentialStore({ home: resolveEkanosHome(env) });\n const hosts = store.hosts();\n if (hosts.length !== 1) return null;\n const session = store.read(hosts[0]!);\n return session ? { host: hosts[0]!, session } : null;\n } catch {\n return null;\n }\n}\n\nfunction withDeadline<T>(promise: Promise<T>, ms: number): Promise<T | null> {\n return new Promise((resolve) => {\n let settled = false;\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n resolve(null);\n }\n }, ms);\n // A rejection is a failure like any other here — resolve to null rather\n // than letting it become an unhandled rejection.\n promise.then(\n (value) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(value);\n }\n },\n () => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(null);\n }\n },\n );\n });\n}\n\nexport interface CheckForUpdateParams {\n env: Record<string, string | undefined>;\n /** An already-resolved Fusion host/session, when the caller has one handy. */\n host?: string;\n session?: { accessToken: string } | null;\n}\n\n/**\n * Resolve whether this CLI is behind, respecting the 24h cache and the hard\n * `EKANOS_NO_UPDATE_CHECK` opt-out. Returns null whenever there is nothing to\n * say — current, unresolvable, or any failure along the way.\n */\nexport async function checkForUpdate(\n params: CheckForUpdateParams,\n): Promise<ToolchainNoticeData | null> {\n if (params.env.EKANOS_NO_UPDATE_CHECK) return null;\n\n const cached = readCache(params.env);\n const now = Date.now();\n\n let target: ToolchainVersions;\n let minimum: ToolchainVersions | undefined;\n\n if (cached && now - cached.checkedAt < TTL_MS) {\n target = cached.target;\n minimum = cached.minimum;\n } else {\n const auth =\n params.host && params.session\n ? { host: params.host, session: params.session }\n : bestEffortHostSession(params.env);\n\n const resolution = await withDeadline(\n resolveToolchainTarget({\n host: auth?.host,\n session: auth?.session,\n timeoutMs: DEADLINE_MS,\n }),\n DEADLINE_MS,\n );\n\n if (!resolution || resolution.status !== 'ok') return null;\n\n target = resolution.target;\n minimum = resolution.source === 'host' ? resolution.minimum : undefined;\n\n writeCache(params.env, {\n checkedAt: now,\n target,\n minimum,\n source: resolution.source,\n });\n }\n\n const current = cliVersion();\n if (compareVersions(current, target.cli) >= 0) return null;\n\n const status =\n minimum && compareVersions(current, minimum.cli) < 0\n ? 'unsupported'\n : 'behind';\n\n return {\n status,\n current: { cli: current },\n target,\n upgradeCommand: 'ekanos upgrade',\n };\n}\n\n/**\n * Surface a notice, if any. Human mode gets one stderr line; every mode gets\n * the `claude-code-hint` stderr line so an agent parsing stderr for that\n * marker sees it regardless of `--json`. NEVER writes to stdout — the JSON\n * envelope's caller is responsible for embedding `notice` under a `toolchain`\n * key in its own `data`, per the envelope docs in `context.ts`.\n */\nexport function announceUpdate(\n ctx: CliContext,\n notice: ToolchainNoticeData | null,\n): void {\n if (!notice) return;\n\n if (!ctx.jsonMode) {\n ctx.warn(\n notice.status === 'unsupported'\n ? `ekanos: @ekanos/cli ${notice.current.cli} is below the version ` +\n `this host requires (${notice.target.cli}). Run \"ekanos upgrade\".`\n : `ekanos: a newer @ekanos/cli is available (${notice.current.cli} ` +\n `→ ${notice.target.cli}). Run \"ekanos upgrade\".`,\n );\n }\n\n ctx.emitClaudeCodeHint({ tool: 'ekanos', toolchain: notice });\n}\n"]}
1
+ {"version":3,"file":"update-notice.js","sourceRoot":"","sources":["../src/update-notice.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { CredentialStore, resolveEkanosHome } from './auth/credential-store';\nimport { cliVersion, compareVersions } from './compatibility';\nimport type { CliContext } from './context';\nimport type { ToolchainVersions } from './toolchain';\nimport { resolveToolchainTarget } from './toolchain-resolve';\n\n/**\n * The proactive, NEVER-mutating \"your CLI is behind\" notice. Runs on\n * `validate`, `dev`, `publish` and `status` — the verbs a partner (or an\n * agent acting for one) runs constantly — so staleness is discovered on the\n * next ordinary command instead of only when something breaks.\n *\n * The one rule that governs every line of this file: this check must be\n * INVISIBLE on failure. A partner offline, a flaky registry, a host that\n * predates this endpoint — none of that may add meaningful latency, change a\n * verb's exit code, or so much as touch the JSON envelope's `ok` field.\n * Every failure path below resolves to `null` (\"say nothing\"), never a throw.\n */\n\nconst CACHE_FILENAME = 'update-check.json';\nconst CACHE_DIR_MODE = 0o700;\nconst CACHE_FILE_MODE = 0o600;\nconst TTL_MS = 24 * 60 * 60 * 1000;\n/** The hard ceiling on this check's total network time, whatever else happens. */\nconst DEADLINE_MS = 1500;\n\nexport interface ToolchainNoticeData {\n status: 'behind' | 'unsupported';\n current: { cli: string };\n target: ToolchainVersions;\n upgradeCommand: 'ekanos upgrade';\n}\n\ninterface CacheFile {\n checkedAt: number;\n target: ToolchainVersions;\n minimum?: ToolchainVersions;\n source: 'host' | 'npm';\n}\n\nfunction cachePath(env: Record<string, string | undefined>): string {\n return path.join(resolveEkanosHome(env), '.ekanos', CACHE_FILENAME);\n}\n\nfunction readCache(env: Record<string, string | undefined>): CacheFile | null {\n try {\n const raw = fs.readFileSync(cachePath(env), 'utf8');\n const parsed: unknown = JSON.parse(raw);\n return isCacheFile(parsed) ? parsed : null;\n } catch {\n // Missing, unreadable, or corrupt — all read as \"no cache\", never an\n // error. This check is a courtesy, not a source of truth.\n return null;\n }\n}\n\nfunction writeCache(\n env: Record<string, string | undefined>,\n file: CacheFile,\n): void {\n const target = cachePath(env);\n try {\n fs.mkdirSync(path.dirname(target), {\n recursive: true,\n mode: CACHE_DIR_MODE,\n });\n const tmp = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(file), { mode: CACHE_FILE_MODE });\n fs.renameSync(tmp, target);\n } catch {\n // Best-effort. The next run simply re-checks — this is a cache, not a\n // durable record.\n }\n}\n\nfunction isCacheFile(value: unknown): value is CacheFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<CacheFile>;\n return (\n typeof candidate.checkedAt === 'number' &&\n typeof candidate.target === 'object' &&\n candidate.target !== null &&\n (candidate.source === 'host' || candidate.source === 'npm')\n );\n}\n\n/**\n * The one host/session this check will ever try, resolved WITHOUT any of the\n * throwing machinery in `auth/session.ts` — `resolveHost` is built to demand\n * an answer (a usage error naming the ambiguity) because a real command needs\n * one; this check needs an answer only if one is obvious, and silence\n * otherwise. Exactly one stored host is \"obvious\"; zero or several are not,\n * and this never prompts or guesses to resolve that ambiguity — it just\n * falls back to the npm registry, same as no session at all.\n */\nfunction bestEffortHostSession(\n env: Record<string, string | undefined>,\n): { host: string; session: { accessToken: string } } | null {\n try {\n const store = new CredentialStore({ home: resolveEkanosHome(env) });\n const hosts = store.hosts();\n if (hosts.length !== 1) return null;\n const session = store.read(hosts[0]!);\n return session ? { host: hosts[0]!, session } : null;\n } catch {\n return null;\n }\n}\n\nfunction withDeadline<T>(promise: Promise<T>, ms: number): Promise<T | null> {\n return new Promise((resolve) => {\n let settled = false;\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n resolve(null);\n }\n }, ms);\n // A rejection is a failure like any other here — resolve to null rather\n // than letting it become an unhandled rejection.\n promise.then(\n (value) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(value);\n }\n },\n () => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(null);\n }\n },\n );\n });\n}\n\nexport interface CheckForUpdateParams {\n env: Record<string, string | undefined>;\n /** An already-resolved Ekanos host/session, when the caller has one handy. */\n host?: string;\n session?: { accessToken: string } | null;\n}\n\n/**\n * Resolve whether this CLI is behind, respecting the 24h cache and the hard\n * `EKANOS_NO_UPDATE_CHECK` opt-out. Returns null whenever there is nothing to\n * say — current, unresolvable, or any failure along the way.\n */\nexport async function checkForUpdate(\n params: CheckForUpdateParams,\n): Promise<ToolchainNoticeData | null> {\n if (params.env.EKANOS_NO_UPDATE_CHECK) return null;\n\n const cached = readCache(params.env);\n const now = Date.now();\n\n let target: ToolchainVersions;\n let minimum: ToolchainVersions | undefined;\n\n if (cached && now - cached.checkedAt < TTL_MS) {\n target = cached.target;\n minimum = cached.minimum;\n } else {\n const auth =\n params.host && params.session\n ? { host: params.host, session: params.session }\n : bestEffortHostSession(params.env);\n\n const resolution = await withDeadline(\n resolveToolchainTarget({\n host: auth?.host,\n session: auth?.session,\n timeoutMs: DEADLINE_MS,\n }),\n DEADLINE_MS,\n );\n\n if (!resolution || resolution.status !== 'ok') return null;\n\n target = resolution.target;\n minimum = resolution.source === 'host' ? resolution.minimum : undefined;\n\n writeCache(params.env, {\n checkedAt: now,\n target,\n minimum,\n source: resolution.source,\n });\n }\n\n const current = cliVersion();\n if (compareVersions(current, target.cli) >= 0) return null;\n\n const status =\n minimum && compareVersions(current, minimum.cli) < 0\n ? 'unsupported'\n : 'behind';\n\n return {\n status,\n current: { cli: current },\n target,\n upgradeCommand: 'ekanos upgrade',\n };\n}\n\n/**\n * Surface a notice, if any. Human mode gets one stderr line; every mode gets\n * the `claude-code-hint` stderr line so an agent parsing stderr for that\n * marker sees it regardless of `--json`. NEVER writes to stdout — the JSON\n * envelope's caller is responsible for embedding `notice` under a `toolchain`\n * key in its own `data`, per the envelope docs in `context.ts`.\n */\nexport function announceUpdate(\n ctx: CliContext,\n notice: ToolchainNoticeData | null,\n): void {\n if (!notice) return;\n\n if (!ctx.jsonMode) {\n ctx.warn(\n notice.status === 'unsupported'\n ? `ekanos: @ekanos/cli ${notice.current.cli} is below the version ` +\n `this host requires (${notice.target.cli}). Run \"ekanos upgrade\".`\n : `ekanos: a newer @ekanos/cli is available (${notice.current.cli} ` +\n `→ ${notice.target.cli}). Run \"ekanos upgrade\".`,\n );\n }\n\n ctx.emitClaudeCodeHint({ tool: 'ekanos', toolchain: notice });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport {\n collectDocsFindings,\n collectHostCompilerRulesFindings,\n collectProjectFindings,\n} from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n findings.push(\n ...collectVersionAgreementFindings(\n integration,\n result.definition as { version?: unknown },\n loaded.projectDir,\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n\n/**\n * `package.json`'s `version` field and the `version` `defineIntegration()`\n * declares are two more independent sources of truth for the same fact —\n * this project shipped weather@0.1.1 with a package.json bump, but the\n * declaration itself still said 0.1.0. Generated seeds embed the\n * DECLARATION's version in their header, so a stale declaration produces a\n * seed that looks stale even though the package itself moved on.\n *\n * WARN, not error, unlike slug agreement: a mismatched version is a quality\n * signal a partner should fix, not a broken cross-reference that breaks\n * something else at runtime, so it must not block `publish`. And a project\n * with no package.json, or a package.json with no `version` field, has\n * nothing to compare the declaration against — skip rather than warn on\n * absence.\n */\nexport function collectVersionAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { version?: unknown },\n projectDir: string,\n): Finding[] {\n const declared = definition.version;\n if (typeof declared !== 'string') return [];\n\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return [];\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed package.json is npm's problem to report, not a validate\n // finding — nothing here can compare against an unparseable manifest.\n return [];\n }\n\n const packageVersion =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { version?: unknown }).version\n : undefined;\n if (typeof packageVersion !== 'string' || packageVersion === declared) {\n return [];\n }\n\n return [\n {\n check: 'project.version-agreement',\n severity: 'warn',\n file: integration.entryPath,\n message:\n `package.json declares version \"${packageVersion}\", but the ` +\n `defineIntegration() declaration for \"${integration.slug}\" still ` +\n `says \"${declared}\". Generated seed headers embed the ` +\n \"declaration's version, so a stale declaration ships stale-looking \" +\n 'metadata even though the package itself moved on.',\n hint:\n `Update the \"version\" passed to defineIntegration() in ` +\n `${integration.entryPath} to match package.json's \"${packageVersion}\".`,\n },\n ];\n}\n"]}
1
+ {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport {\n collectDocsFindings,\n collectHostCompilerRulesFindings,\n collectProjectFindings,\n} from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n findings.push(\n ...collectVersionAgreementFindings(\n integration,\n result.definition as { version?: unknown },\n loaded.projectDir,\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n\n/**\n * `package.json`'s `version` field and the `version` `defineIntegration()`\n * declares are two more independent sources of truth for the same fact —\n * this project shipped weather@0.1.1 with a package.json bump, but the\n * declaration itself still said 0.1.0. Generated seeds embed the\n * DECLARATION's version in their header, so a stale declaration produces a\n * seed that looks stale even though the package itself moved on.\n *\n * WARN, not error, unlike slug agreement: a mismatched version is a quality\n * signal a partner should fix, not a broken cross-reference that breaks\n * something else at runtime, so it must not block `publish`. And a project\n * with no package.json, or a package.json with no `version` field, has\n * nothing to compare the declaration against — skip rather than warn on\n * absence.\n */\nexport function collectVersionAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { version?: unknown },\n projectDir: string,\n): Finding[] {\n const declared = definition.version;\n if (typeof declared !== 'string') return [];\n\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return [];\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed package.json is npm's problem to report, not a validate\n // finding — nothing here can compare against an unparseable manifest.\n return [];\n }\n\n const packageVersion =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { version?: unknown }).version\n : undefined;\n if (typeof packageVersion !== 'string' || packageVersion === declared) {\n return [];\n }\n\n return [\n {\n check: 'project.version-agreement',\n severity: 'warn',\n file: integration.entryPath,\n message:\n `package.json declares version \"${packageVersion}\", but the ` +\n `defineIntegration() declaration for \"${integration.slug}\" still ` +\n `says \"${declared}\". Generated seed headers embed the ` +\n \"declaration's version, so a stale declaration ships stale-looking \" +\n 'metadata even though the package itself moved on.',\n hint:\n `Update the \"version\" passed to defineIntegration() in ` +\n `${integration.entryPath} to match package.json's \"${packageVersion}\".`,\n },\n ];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../src/wizard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAGxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAyC1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6DAA6D,CAC9D,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW;IAC5B,KAAK,CAAC,KAAK;QACT,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,KAAK,CAAC,OAAO;QACX,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,KAAK;QACjB,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,CAAC,OAAO;QACZ,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,QAAQ,CAAC,KAAK;QACZ,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO;QACL,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;QACrD,OAAO;YACL,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;YACpC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;SACnC,CAAC;IACJ,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CACvB,GAAe,EACf,QAAkC,EAAE;;IAEpC,MAAM,UAAU,GAAG,MAAA,KAAK,CAAC,UAAU,mCAAI,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,WAAW,IAAI,UAAU,CAAC;AACxD,CAAC","sourcesContent":["import * as clack from '@clack/prompts';\n\nimport type { CliContext } from './context';\nimport { EXIT_CODES } from './exit-codes';\n\n/**\n * The clack-powered wizard seam.\n *\n * This is the ONE file in the package that imports `@clack/prompts` — `ui.ts`\n * stays prompt-free (its color/glyph tokens gate a PLAIN string renderer with\n * no interactivity of its own), and this module is the seam any future\n * prompt-driven command shares rather than each wiring clack directly. Thin,\n * named wrappers rather than a re-export of the clack namespace so a test can\n * `vi.mock('@clack/prompts')` and assert call order without its target\n * (`init`, `login`, …) needing to know clack exists underneath.\n *\n * A command reaches for this seam instead of `ui.ts`'s plain renderer only\n * when `canWizard()` below says yes — human mode AND a real stdout TTY AND a\n * real stdin TTY, all three, checked fresh for that command's own streams.\n * Anything less (JSON mode, a piped stream, a non-interactive stdin) falls\n * back to the plain renderer; this seam never partially renders.\n */\n\nexport interface WizardSpinner {\n start(message?: string): void;\n stop(message?: string): void;\n}\n\nexport interface Wizard {\n intro(title: string): void;\n outro(message: string): void;\n /** A block of pre-formatted, copy-pasteable text — clack draws a box around it. */\n note(message: string, title?: string): void;\n cancel(message: string): void;\n /** Passthrough of clack's cancel-symbol check, for a future prompt-based command. */\n isCancel(value: unknown): boolean;\n /**\n * A spinner pre-wired to exit cleanly on ctrl-c — see `exitCancelled` for\n * why this cannot simply throw and let `run()`'s try/catch take it from\n * there the way every other refusal in this CLI does.\n */\n spinner(): WizardSpinner;\n}\n\n/**\n * The ONE non-throwing exit path in this package. Every other refusal is a\n * `CliError` that `run()`'s top-level try/catch turns into `ctx.fail(...)` —\n * clean by construction, because it always has a live call stack to unwind\n * back to. A ctrl-c while a `wizard.spinner()` is running cannot take that\n * path: `@clack/prompts`' spinner registers its OWN `process.on('SIGINT', …)`\n * listener for the lifetime of `start()`..`stop()`, and Node only runs its\n * default exit-on-SIGINT behaviour when NO listener is registered — so\n * without an explicit exit here, ctrl-c would silently stop the spinner's\n * animation and otherwise do nothing, leaving `login`'s device-code poll\n * (which can legitimately run for minutes) spinning in the background with\n * no way back to a prompt.\n *\n * So this mirrors `ctx.fail`'s OUTPUT contract by hand — stderr only, an\n * `Error: …` / `Hint: …` pair, never a stack trace — and exits with the same\n * code a plain usage error would (`EXIT_CODES.USAGE`), matching every other\n * clean-refusal path in the CLI. Exported so tests can assert on it without\n * a real `process.exit` ending the test run.\n */\nexport function exitCancelled(): void {\n process.stderr.write(\n 'Error: Cancelled.\\nHint: Re-run the command to try again.\\n',\n );\n process.exit(EXIT_CODES.USAGE);\n}\n\nexport const wizard: Wizard = {\n intro(title) {\n clack.intro(title);\n },\n outro(message) {\n clack.outro(message);\n },\n note(message, title) {\n clack.note(message, title);\n },\n cancel(message) {\n clack.cancel(message);\n },\n isCancel(value) {\n return clack.isCancel(value);\n },\n spinner() {\n const s = clack.spinner({ onCancel: exitCancelled });\n return {\n start: (message) => s.start(message),\n stop: (message) => s.stop(message),\n };\n },\n};\n\n/**\n * Whether a clack-powered wizard flow may run for this invocation.\n *\n * Mirrors `publish.ts`'s `canPrompt`, with one addition: `ctx.jsonMode` alone\n * is not enough (`--no-json` forces human mode even against a piped stdout,\n * and says nothing about stdin), so this needs BOTH streams confirmed\n * interactive, not just one:\n *\n * - stdout a real TTY: clack renders to stdout (unlike the rest of this\n * CLI's human output, which is stderr-only — see `ui.ts`'s docs), and\n * `ctx.jsonMode` false does not by itself guarantee stdout is a terminal.\n * - stdin a real TTY: clack reads keypresses from it interactively. A\n * closed or non-interactive stdin (a cron job, a wrapper script) can\n * never resolve an interactive prompt, the same reason `publish`'s y/N\n * confirm gates on it.\n *\n * Anything else falls back to the plain, existing human-mode rendering —\n * never a hang.\n */\nexport function canWizard(\n ctx: CliContext,\n probe: { stdinIsTTY?: boolean } = {},\n): boolean {\n const stdinIsTTY = probe.stdinIsTTY ?? process.stdin.isTTY === true;\n return !ctx.jsonMode && ctx.stdoutIsTTY && stdinIsTTY;\n}\n"]}
1
+ {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../src/wizard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAGxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAyC1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6DAA6D,CAC9D,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW;IAC5B,KAAK,CAAC,KAAK;QACT,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,KAAK,CAAC,OAAO;QACX,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,KAAK;QACjB,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,CAAC,OAAO;QACZ,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,QAAQ,CAAC,KAAK;QACZ,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO;QACL,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;QACrD,OAAO;YACL,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;YACpC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;SACnC,CAAC;IACJ,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CACvB,GAAe,EACf,KAAK,GAA6B,EAAE;;IAEpC,MAAM,UAAU,SAAG,KAAK,CAAC,UAAU,mCAAI,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,WAAW,IAAI,UAAU,CAAC;AACxD,CAAC","sourcesContent":["import * as clack from '@clack/prompts';\n\nimport type { CliContext } from './context';\nimport { EXIT_CODES } from './exit-codes';\n\n/**\n * The clack-powered wizard seam.\n *\n * This is the ONE file in the package that imports `@clack/prompts` — `ui.ts`\n * stays prompt-free (its color/glyph tokens gate a PLAIN string renderer with\n * no interactivity of its own), and this module is the seam any future\n * prompt-driven command shares rather than each wiring clack directly. Thin,\n * named wrappers rather than a re-export of the clack namespace so a test can\n * `vi.mock('@clack/prompts')` and assert call order without its target\n * (`init`, `login`, …) needing to know clack exists underneath.\n *\n * A command reaches for this seam instead of `ui.ts`'s plain renderer only\n * when `canWizard()` below says yes — human mode AND a real stdout TTY AND a\n * real stdin TTY, all three, checked fresh for that command's own streams.\n * Anything less (JSON mode, a piped stream, a non-interactive stdin) falls\n * back to the plain renderer; this seam never partially renders.\n */\n\nexport interface WizardSpinner {\n start(message?: string): void;\n stop(message?: string): void;\n}\n\nexport interface Wizard {\n intro(title: string): void;\n outro(message: string): void;\n /** A block of pre-formatted, copy-pasteable text — clack draws a box around it. */\n note(message: string, title?: string): void;\n cancel(message: string): void;\n /** Passthrough of clack's cancel-symbol check, for a future prompt-based command. */\n isCancel(value: unknown): boolean;\n /**\n * A spinner pre-wired to exit cleanly on ctrl-c — see `exitCancelled` for\n * why this cannot simply throw and let `run()`'s try/catch take it from\n * there the way every other refusal in this CLI does.\n */\n spinner(): WizardSpinner;\n}\n\n/**\n * The ONE non-throwing exit path in this package. Every other refusal is a\n * `CliError` that `run()`'s top-level try/catch turns into `ctx.fail(...)` —\n * clean by construction, because it always has a live call stack to unwind\n * back to. A ctrl-c while a `wizard.spinner()` is running cannot take that\n * path: `@clack/prompts`' spinner registers its OWN `process.on('SIGINT', …)`\n * listener for the lifetime of `start()`..`stop()`, and Node only runs its\n * default exit-on-SIGINT behaviour when NO listener is registered — so\n * without an explicit exit here, ctrl-c would silently stop the spinner's\n * animation and otherwise do nothing, leaving `login`'s device-code poll\n * (which can legitimately run for minutes) spinning in the background with\n * no way back to a prompt.\n *\n * So this mirrors `ctx.fail`'s OUTPUT contract by hand — stderr only, an\n * `Error: …` / `Hint: …` pair, never a stack trace — and exits with the same\n * code a plain usage error would (`EXIT_CODES.USAGE`), matching every other\n * clean-refusal path in the CLI. Exported so tests can assert on it without\n * a real `process.exit` ending the test run.\n */\nexport function exitCancelled(): void {\n process.stderr.write(\n 'Error: Cancelled.\\nHint: Re-run the command to try again.\\n',\n );\n process.exit(EXIT_CODES.USAGE);\n}\n\nexport const wizard: Wizard = {\n intro(title) {\n clack.intro(title);\n },\n outro(message) {\n clack.outro(message);\n },\n note(message, title) {\n clack.note(message, title);\n },\n cancel(message) {\n clack.cancel(message);\n },\n isCancel(value) {\n return clack.isCancel(value);\n },\n spinner() {\n const s = clack.spinner({ onCancel: exitCancelled });\n return {\n start: (message) => s.start(message),\n stop: (message) => s.stop(message),\n };\n },\n};\n\n/**\n * Whether a clack-powered wizard flow may run for this invocation.\n *\n * Mirrors `publish.ts`'s `canPrompt`, with one addition: `ctx.jsonMode` alone\n * is not enough (`--no-json` forces human mode even against a piped stdout,\n * and says nothing about stdin), so this needs BOTH streams confirmed\n * interactive, not just one:\n *\n * - stdout a real TTY: clack renders to stdout (unlike the rest of this\n * CLI's human output, which is stderr-only — see `ui.ts`'s docs), and\n * `ctx.jsonMode` false does not by itself guarantee stdout is a terminal.\n * - stdin a real TTY: clack reads keypresses from it interactively. A\n * closed or non-interactive stdin (a cron job, a wrapper script) can\n * never resolve an interactive prompt, the same reason `publish`'s y/N\n * confirm gates on it.\n *\n * Anything else falls back to the plain, existing human-mode rendering —\n * never a hang.\n */\nexport function canWizard(\n ctx: CliContext,\n probe: { stdinIsTTY?: boolean } = {},\n): boolean {\n const stdinIsTTY = probe.stdinIsTTY ?? process.stdin.isTTY === true;\n return !ctx.jsonMode && ctx.stdoutIsTTY && stdinIsTTY;\n}\n"]}
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@ekanos/cli",
3
- "version": "0.3.2",
4
- "type": "module",
5
- "description": "The Ekanos partner toolchain CLI: scaffold, validate, and test a Fusion integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
6
- "license": "MIT",
7
- "bugs": {
8
- "email": "npm@govastly.com"
9
- },
3
+ "version": "0.3.3",
4
+ "description": "The Ekanos partner toolchain CLI: scaffold, validate, and test an Ekanos integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
10
5
  "keywords": [
6
+ "cli",
11
7
  "ekanos",
12
8
  "fusion",
13
- "integration",
14
- "cli"
9
+ "integration"
15
10
  ],
11
+ "bugs": {
12
+ "email": "npm@govastly.com"
13
+ },
14
+ "license": "MIT",
16
15
  "bin": {
17
16
  "ekanos": "./dist/bin.js"
18
17
  },
@@ -22,6 +21,7 @@
22
21
  "README.md",
23
22
  "LICENSE"
24
23
  ],
24
+ "type": "module",
25
25
  "exports": {
26
26
  ".": {
27
27
  "types": "./dist/index.d.ts",
@@ -33,30 +33,25 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "1.7.0",
36
- "@ekanos/integration-schema": "0.3.0",
36
+ "@ekanos/integration-schema": "0.4.0",
37
37
  "esbuild": "0.28.1",
38
38
  "picocolors": "1.1.1"
39
39
  },
40
- "peerDependencies": {
41
- "zod": "^3.25.76"
42
- },
43
40
  "devDependencies": {
44
- "@kit/eslint-config": "0.2.0",
45
- "@kit/prettier-config": "0.1.0",
46
41
  "@kit/tsconfig": "0.1.0",
47
42
  "@types/node": "25.0.1",
48
- "typescript": "^5.9.3",
43
+ "typescript": "^7.0.2",
49
44
  "vitest": "4.1.10",
50
- "zod": "3.25.76"
45
+ "zod": "4.6.5"
46
+ },
47
+ "peerDependencies": {
48
+ "zod": "^3.25.76 || ^4.0.0"
51
49
  },
52
- "prettier": "@kit/prettier-config",
53
50
  "scripts": {
54
51
  "build": "tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs",
55
52
  "check:fa-free-names": "node scripts/generate-fa-free-names.mjs",
56
53
  "clean": "git clean -xdf .turbo node_modules dist",
57
- "format": "prettier --check \"**/*.{ts,tsx}\"",
58
54
  "generate:fa-free-names": "node scripts/generate-fa-free-names.mjs --write",
59
- "lint": "eslint .",
60
55
  "typecheck": "tsc --noEmit",
61
56
  "test": "vitest run --config vitest.config.ts"
62
57
  },
@@ -1,6 +1,6 @@
1
1
  # __DISPLAY_NAME__ — an Ekanos integration
2
2
 
3
- This project is a **Fusion integration** built with `@ekanos/sdk`. This file is
3
+ This project is a **Ekanos integration** built with `@ekanos/sdk`. This file is
4
4
  the authoring contract: everything an agent needs to build, test, and publish
5
5
  it without fetching external documentation. The `@ekanos/sdk` README (in
6
6
  `node_modules/@ekanos/sdk/README.md` after install) is the long-form version;
@@ -164,7 +164,12 @@ your slug at discovery.
164
164
 
165
165
  Declared with kebab-case ids. The transport validates `payloadSchema` before
166
166
  your handler runs and owns signature verification (`signature: { header,
167
- secretName }` or `'none'`) — never verify a signature in the handler.
167
+ secretName, scheme? }` or `'none'`) — never verify a signature in the handler.
168
+ Omit `scheme` for a plain HMAC-SHA256 body digest; declare one for keyed or
169
+ timestamped headers (Stripe/Calendly: `{ format: 'keyed', digestKey: 'v1',
170
+ timestampKey: 't', signedPayload: '{timestamp}.{body}' }`) or SHA-1
171
+ (`{ algorithm: 'sha1' }`). The README's `signature.scheme` table has the
172
+ common providers.
168
173
 
169
174
  Webhook results: `{ status: 'processed' }` (done), `{ status: 'ignored' }`
170
175
  (valid but irrelevant — the sender must not retry), or **throw** (retryable
@@ -307,7 +312,7 @@ system. Available modules (import individually, e.g. `@ekanos/ui/button`):
307
312
  `ai-prompt-input`, plus the stylesheets `styles.css` / `tokens.css` /
308
313
  `theme.css` / `base.css`. Icons are Font Awesome glyph names via
309
314
  `@ekanos/ui/icon`. This generated shell loads Font Awesome **Free**
310
- (`@fortawesome/fontawesome-free`) — a much smaller set than the Pro kit Fusion
315
+ (`@fortawesome/fontawesome-free`) — a much smaller set than the Pro kit Ekanos
311
316
  itself runs — so a name that's Pro-only (or just mistyped) renders a
312
317
  circle-question disc instead of vanishing, and dev builds `console.warn` once
313
318
  per name to catch it before a screenshot review does. `fa-light` / `fa-duotone`
@@ -320,7 +325,7 @@ it looking like anything in particular.
320
325
 
321
326
  ## Live mode — real API calls, locally
322
327
 
323
- The harness (`npx ekanos dev`) mocks FUSION, not your vendor. **You do not
328
+ The harness (`npx ekanos dev`) mocks EKANOS, not your vendor. **You do not
324
329
  need mock data to develop against the real API**, and never claim otherwise:
325
330
  the dev toolbar has a fixtures ⇄ live switch per integration, and in live
326
331
  mode your widgets' real query functions — calling `useIntegrationFetch()` —
@@ -336,7 +341,7 @@ Live mode is opt-in per integration. Add a `live` block to the entry in
336
341
 
337
342
  ```ts
338
343
  live: {
339
- // Fusion-side state widgets read before calling anything — what your
344
+ // Ekanos-side state widgets read before calling anything — what your
340
345
  // host route would have returned. Everything NOT seeded runs its real
341
346
  // query function against the real network.
342
347
  seeds: [{ queryKey: settingsKey, data: MY_SETTINGS }],
@@ -349,7 +354,7 @@ vendor from the browser (every vendor call is server-side behind `ctx.fetch`,
349
354
  the OAuth shape): the harness then opens in Live API, and nothing fires at the
350
355
  vendor until the user acts. A choice made at the toolbar always wins.
351
356
 
352
- There is still no Fusion database or auth in either mode. What differs is
357
+ There is still no Ekanos database or auth in either mode. What differs is
353
358
  who answers `/api/integrations/<slug>/…`:
354
359
 
355
360
  - **Fixtures**: your fixtures do, in the browser. Storage, secrets and the
@@ -468,9 +473,9 @@ carries an imperative `hint`; treat hints as remediation instructions.
468
473
  | `ekanos validate` | Parse ekanos.json + load the definition + run every finding pass. Findings land in `data.findings`, each with severity and a hint |
469
474
  | `ekanos dev` | Scaffold `.ekanos/harness` and run the integration in a local Next dev shell (loopback by default) |
470
475
  | `ekanos test` | Run this project's test script through its package manager |
471
- | `ekanos login --host <url>` | Device-flow login to a Fusion deployment (stores the host for later verbs) |
476
+ | `ekanos login --host <url>` | Device-flow login to an Ekanos deployment (stores the host for later verbs) |
472
477
  | `ekanos status` | Login state for the resolved host + this project's submissions |
473
- | `ekanos sources` | List every Fusion source you hold a developer/admin seat on, marking this project's current target |
478
+ | `ekanos sources` | List every Ekanos source you hold a developer/admin seat on, marking this project's current target |
474
479
  | `ekanos use <source-slug>` | Set this project's publish target — refuses (`forbidden`) a slug you hold no seat on |
475
480
  | `ekanos publish` | Validate, pack (whitelist: ekanos.json, package.json, README.md, src/), verify the target, and submit. Refuses on any error-severity finding |
476
481
  | `ekanos upgrade` | Move this project's toolchain to a coherent target version, with verify-and-rollback |
@@ -1,6 +1,6 @@
1
1
  # __DISPLAY_NAME__
2
2
 
3
- This project is an Ekanos integration for the Fusion platform — read
3
+ This project is an Ekanos integration for the Ekanos platform — read
4
4
  [AGENTS.md](./AGENTS.md) for the full authoring contract before changing code.
5
5
  The toolchain is `npx ekanos <verb>` (init, validate, dev, test, login,
6
6
  status, publish, upgrade); every verb supports `--json` and a stable
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ekanos
3
- description: Build, iterate on, validate, and publish this Fusion integration with the ekanos CLI. Use for any change to the integration definition, widgets, tools, webhooks, schedules, or tests, and for publishing to a Fusion host.
3
+ description: Build, iterate on, validate, and publish this Ekanos integration with the ekanos CLI. Use for any change to the integration definition, widgets, tools, webhooks, schedules, or tests, and for publishing to an Ekanos host.
4
4
  ---
5
5
 
6
6
  # Working on this Ekanos integration
@@ -35,7 +35,7 @@ in the same commit when the live API disagrees.
35
35
  `--source <slug>` explicitly to `publish`. Never pass `--yes` to get past
36
36
  an ambiguous-target error you have not actually resolved — `--yes` is
37
37
  consent to a target you already chose, not a way to silence the check.
38
- 7. `npx ekanos publish --source <slug>` — submits to the Fusion host (refuses
38
+ 7. `npx ekanos publish --source <slug>` — submits to the Ekanos host (refuses
39
39
  while any error-severity finding remains, and refuses `forbidden` if you
40
40
  hold no seat on that source).
41
41
 
@@ -23,7 +23,7 @@ const config = {
23
23
  turbopack: {
24
24
  root: projectRoot,
25
25
  resolveAlias: {
26
- // A widget written for the real Fusion host imports
26
+ // A widget written for the real Ekanos host imports
27
27
  // `useTeamAccountWorkspace()` to read `account.slug`. That module
28
28
  // reaches Supabase, which the harness deliberately does not have, so
29
29
  // this points the specifier at the shape-identical mock @ekanos/harness
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "description": "A Fusion integration built with @ekanos/sdk.",
6
+ "description": "An Ekanos integration built with @ekanos/sdk.",
7
7
  "scripts": {
8
8
  "typecheck": "tsc --noEmit",
9
9
  "test": "vitest run",
@@ -11,18 +11,19 @@
11
11
  "prepare": "node scripts/setup-hooks.mjs"
12
12
  },
13
13
  "dependencies": {
14
- "@ekanos/sdk": "^0.3.0",
15
- "@ekanos/ui": "^0.3.0"
14
+ "@ekanos/sdk": "^0.4.0",
15
+ "@ekanos/ui": "^0.4.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@ekanos/cli": "^0.3.1",
19
- "@ekanos/harness": "^0.3.0",
18
+ "@ekanos/cli": "^0.3.3",
19
+ "@ekanos/harness": "^0.4.0",
20
20
  "@hookform/resolvers": "^5.2.2",
21
21
  "@tailwindcss/postcss": "^4.3.3",
22
22
  "@tanstack/react-query": "^5.101.4",
23
23
  "@types/react": "^19.2.0",
24
24
  "@types/react-dom": "^19.2.0",
25
25
  "next": "^16.0.0",
26
+ "next-intl": "^4.14.7",
26
27
  "react": "^19.2.8",
27
28
  "react-dom": "^19.2.8",
28
29
  "react-hook-form": "^7.68.0",
@@ -0,0 +1,8 @@
1
+ # pnpm 10+ refuses to run dependency build scripts it has not been told to
2
+ # allow, and `vitest` pulls in `esbuild`, whose install script compiles its
3
+ # binary. Without this file the first `pnpm install` writes a placeholder
4
+ # value here instead, and every later `ekanos dev` / `ekanos test` fails
5
+ # with ERR_PNPM_IGNORED_BUILDS until a human edits it. npm and yarn ignore
6
+ # this file.
7
+ allowBuilds:
8
+ esbuild: true
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: build-integration
3
- description: Interview-driven workflow for building and publishing a Fusion integration with the Ekanos SDK. Use when building a new integration from scratch ("build an integration for X", "connect a service like Stripe or Slack", a fresh ekanos scaffold with no substance yet), when adding or changing API wiring ("add an endpoint", "wire this to the API", "add a webhook/tool/schedule"), when researching the target service's API ("how does this API paginate", "what auth does it use", "where are the docs"), or when publishing.
3
+ description: Interview-driven workflow for building and publishing an Ekanos integration with the Ekanos SDK. Use when building a new integration from scratch ("build an integration for X", "connect a service like Stripe or Slack", a fresh ekanos scaffold with no substance yet), when adding or changing API wiring ("add an endpoint", "wire this to the API", "add a webhook/tool/schedule"), when researching the target service's API ("how does this API paginate", "what auth does it use", "where are the docs"), or when publishing.
4
4
  ---
5
5
 
6
- # Build a Fusion Integration
6
+ # Build an Ekanos Integration
7
7
 
8
8
  Guide the user from intent to a published integration. The user supplies
9
9
  judgment (what service, what it should show and do); the CLI supplies
@@ -164,7 +164,7 @@ All of these, in order, all green:
164
164
 
165
165
  ## Phase 7: Publish — gated
166
166
 
167
- Publishing sends the project to the Fusion host and is versioned immutably.
167
+ Publishing sends the project to the Ekanos host and is versioned immutably.
168
168
  NEVER run it on your own judgment. Show the user:
169
169
 
170
170
  - the verify results (1–4 above)
@@ -97,11 +97,11 @@ code. This file is the order of work and the traps.
97
97
 
98
98
  ## Verifying against the real API — live mode
99
99
 
100
- The harness mocks Fusion, never the vendor. Do not tell the developer that
100
+ The harness mocks Ekanos, never the vendor. Do not tell the developer that
101
101
  local work is mock-only: opt the integration into live mode (a `live` block
102
102
  on its `harness.config.ts` entry — see AGENTS.md "Live mode") and the
103
103
  toolbar's fixtures ⇄ live switch runs real widget queries against the real
104
- API, gated only by the declared `egress`. Use fixtures for the Fusion-side
104
+ API, gated only by the declared `egress`. Use fixtures for the Ekanos-side
105
105
  state widgets need first, and for webhook/schedule handlers (the Triggers
106
106
  surface runs those against the mock context by design). `actions[name]`
107
107
  handlers follow the same rule: `useIntegrationAction()` calls run against
@@ -105,7 +105,7 @@ pb-5`) instead of a hand-rolled `px-4`/`px-5` wrapper div.
105
105
  While iterating, expose a temporary way to flip states (a dev-only prop or a
106
106
  mock switch) so the harness can show all four; remove it before publish.
107
107
 
108
- ## Fusion design rules (the harness applies the host theme)
108
+ ## Ekanos design rules (the harness applies the host theme)
109
109
 
110
110
  - **Semantic tokens only**: `bg-card`, `text-foreground`, `text-muted-foreground`,
111
111
  `border-border`, `bg-primary`, `text-destructive`. NEVER raw palette
@@ -1 +0,0 @@
1
- {"version":3,"file":"fusion-api.js","sourceRoot":"","sources":["../../src/auth/fusion-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { authRequiredError, networkError, preconditionError } from '../errors';\n\n/**\n * The CLI's client for the four Fusion auth routes.\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/auth/cli-<verb>` routes, and it ships on\n * a DIFFERENT BRANCH from this file. This block is the single place the shapes\n * are written down on the client side; if you change one side, change the\n * other and update this block. Nothing here imports host code — the two\n * halves are coupled only by HTTP, and the tests stub `fetch`, never the route.\n *\n * GET /api/auth/cli-config [no auth]\n * → 200 { auth0_domain, client_id, scope,\n * device_authorization_endpoint, token_endpoint } all strings\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n *\n * POST /api/auth/cli-session [no auth]\n * ← { auth0_access_token: string }\n * → 200 { access_token, expires_at, user: { id, email } }\n * → 400 { error, code: \"INVALID_REQUEST\" } malformed / wrong shape\n * → 401 { error, code: \"SESSION_EXPIRED\" } Auth0 said no, OR no\n * Fusion account for it\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 500 { error, code: \"INTERNAL_ERROR\" }\n *\n * POST /api/auth/cli-refresh [no auth]\n * ← { auth0_refresh_token: string }\n * → 200 { access_token, expires_at, user: { id, email },\n * auth0_refresh_token } ← ROTATED; the presented one is dead\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"SESSION_EXPIRED\" } expired or revoked\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\",\n * auth0_refresh_token } no Fusion account\n * → 502 { error, code: \"TOKEN_EXCHANGE_FAILED\" } Auth0 returned no\n * rotated token\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 503 { error, code: \"REFRESH_INCOMPLETE\",\n * auth0_refresh_token } rotated, but /userinfo\n * did not answer; RETRYABLE\n * → 500 { error, code: \"INTERNAL_ERROR\",\n * auth0_refresh_token? } present iff rotation\n * completed before the throw\n *\n * !! cli-refresh MAY RETURN `auth0_refresh_token` ON ERROR RESPONSES, and a\n * client MUST persist it before surfacing the error. Once the server's\n * exchange with Auth0 succeeds the presented token is dead and the value\n * in that body is the ONLY copy in existence; discarding it on a failure\n * path destroys the session permanently. Note 503 has TWO codes here —\n * branch on `code`, not on the status alone.\n *\n * All three routes above read the Fusion CLI Auth0 application config, so all\n * three can answer 503 CLI_NOT_CONFIGURED. cli-whoami cannot — it reads no\n * Auth0 config, only the caller's bearer token.\n *\n * GET /api/auth/cli-whoami [Authorization: Bearer <access_token>]\n * → 200 { user: { id, email, name } }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or invalid\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\" }\n * → 3xx treated as 401, see below\n *\n * `access_token` is a Fusion HS256 JWT. Every authenticated Fusion route\n * accepts it as `Authorization: Bearer` — see `resolveRequestSupabaseJwt` in\n * `@kit/supabase`, which both `requireUser` and the RLS server client consume.\n * ---------------------------------------------------------------------------\n *\n * Two rules hold everywhere in this file:\n *\n * - `redirect: 'manual'`. Fusion answers an unauthenticated page request with\n * a 307 to `/auth/sign-in`; Node's fetch follows redirects by default, so\n * without this a stale token would come back as a 200 full of HTML and be\n * read as success. A 3xx here is treated as \"not authenticated\", full stop.\n *\n * - No token is ever interpolated into an error message. The server's error\n * bodies are `{ error, code }` documents that do not echo credentials, and\n * the request bodies that DO carry credentials are never included in a\n * thrown message.\n */\n\nconst REQUEST_TIMEOUT_MS = 15_000;\n\nexport interface CliAuthConfig {\n auth0Domain: string;\n clientId: string;\n scope: string;\n deviceAuthorizationEndpoint: string;\n tokenEndpoint: string;\n}\n\nexport interface FusionSession {\n accessToken: string;\n expiresAt: string;\n userId: string;\n email: string | null;\n}\n\nexport interface FusionIdentity {\n id: string;\n email: string | null;\n name: string | null;\n}\n\n/** Fetch the device-flow parameters for a host. */\nexport async function fetchCliAuthConfig(host: string): Promise<CliAuthConfig> {\n const response = await request(`${host}/api/auth/cli-config`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n });\n\n throwIfNotConfigured(response, host);\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-config', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-config`);\n\n const config = {\n auth0Domain: str(body.auth0_domain),\n clientId: str(body.client_id),\n scope: str(body.scope),\n deviceAuthorizationEndpoint: str(body.device_authorization_endpoint),\n tokenEndpoint: str(body.token_endpoint),\n };\n\n if (Object.values(config).some((value) => value === null)) {\n throw preconditionError(\n `${host} returned an incomplete CLI login configuration.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `/api/auth/cli-config route.`,\n );\n }\n\n // The endpoints come from the server, so they are attacker-controlled if the\n // host is. Pin them to the Auth0 domain the same response declares, so a\n // compromised or misconfigured Fusion cannot redirect the device flow — and\n // the resulting credentials — to an arbitrary origin.\n assertOnDomain(config.deviceAuthorizationEndpoint!, config.auth0Domain!);\n assertOnDomain(config.tokenEndpoint!, config.auth0Domain!);\n\n return config as CliAuthConfig;\n}\n\n/** Exchange a verified Auth0 access token for a Fusion JWT. */\nexport async function createFusionSession(\n host: string,\n auth0AccessToken: string,\n): Promise<FusionSession> {\n const response = await request(`${host}/api/auth/cli-session`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ auth0_access_token: auth0AccessToken }),\n });\n\n throwIfNotConfigured(response, host);\n\n if (response.status === 401) {\n throw authRequiredError(\n `${host} rejected the Auth0 identity.`,\n `Run \"ekanos login\" again. If it keeps failing, confirm your account ` +\n `exists on ${host} — the browser login can succeed for an identity ` +\n `that has no Fusion account.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-session', response.status);\n }\n\n return toSession(\n await readJson(response, `${host}/api/auth/cli-session`),\n host,\n );\n}\n\nexport interface RefreshedSession extends FusionSession {\n /** The ROTATED refresh token. The presented one is already invalid. */\n auth0RefreshToken: string;\n}\n\nexport interface RefreshOptions {\n /**\n * Called the INSTANT a rotated refresh token is seen in the response — on\n * success and on failure alike, before anything is thrown.\n *\n * Persist-on-learn, not persist-on-success. Once the server's exchange with\n * Auth0 succeeds the presented token is dead and the replacement in that\n * body is the only copy in existence, so a client that only saves it on the\n * happy path destroys the partner's credential the first time /userinfo\n * blips. The callback runs before any throw so no error path can skip it.\n */\n onRotatedToken?: (rotated: string) => void;\n}\n\n/** Re-mint a Fusion JWT from the stored Auth0 refresh token. */\nexport async function refreshFusionSession(\n host: string,\n auth0RefreshToken: string,\n options: RefreshOptions = {},\n): Promise<RefreshedSession> {\n const response = await request(`${host}/api/auth/cli-refresh`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ auth0_refresh_token: auth0RefreshToken }),\n });\n\n // Read the body FIRST, whatever the status: the rotated token can arrive on\n // a 503, a 404 or a 500 just as readily as on a 200, and it must be captured\n // before any branch below throws.\n const body = await readOptionalJson(response);\n const rotated = str(body.auth0_refresh_token);\n\n if (rotated) {\n options.onRotatedToken?.(rotated);\n }\n\n throwIfNotConfigured(response, host, body);\n\n if (response.status === 401) {\n throw authRequiredError(\n `The stored session for ${host} is no longer valid.`,\n `Run \"ekanos login\" to sign in again. This is expected after the ` +\n `refresh token expires, after it is revoked in Auth0, or if two ` +\n `machines used the same credentials file.`,\n );\n }\n\n if (response.status === 503 && body.code === 'REFRESH_INCOMPLETE') {\n // NOT an auth failure. The credential was renewed — and, thanks to the\n // callback above, already saved. Auth0's /userinfo is simply not answering.\n throw networkError(\n `${host} renewed the session but could not verify the identity with Auth0.`,\n `Retry in a moment — your credentials are still valid and have been ` +\n `updated. You do NOT need to run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no Fusion account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then retry.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-refresh', response.status);\n }\n\n if (!rotated) {\n throw networkError(\n `${host} refreshed the session without returning a rotated refresh token.`,\n `Run \"ekanos login\" to start a fresh session, and report this — the ` +\n `deployment's Auth0 CLI application may not have rotation enabled.`,\n );\n }\n\n return { ...toSession(body, host), auth0RefreshToken: rotated };\n}\n\n/**\n * Prove the Fusion JWT authenticates. Returns null on 401 so the caller can\n * decide whether to refresh and retry — an expired access token is an ordinary\n * event, not an error.\n */\nexport async function fetchIdentity(\n host: string,\n accessToken: string,\n): Promise<FusionIdentity | null> {\n const response = await request(`${host}/api/auth/cli-whoami`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n // 401, or the 307 that an auth-gated Fusion route answers with. Both mean\n // \"this token did not authenticate\".\n if (response.status === 401 || isRedirect(response.status)) {\n return null;\n }\n\n if (response.status === 403) {\n throw authRequiredError(\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.status === 404) {\n throw authRequiredError(\n `${host} has no account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-whoami', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-whoami`);\n const user = (body.user ?? {}) as Record<string, unknown>;\n const id = str(user.id);\n\n if (!id) {\n throw networkError(\n `${host} returned an identity without a user id.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `/api/auth/cli-whoami route.`,\n );\n }\n\n return { id, email: str(user.email), name: str(user.name) };\n}\n\nfunction toSession(body: Record<string, unknown>, host: string): FusionSession {\n const user = (body.user ?? {}) as Record<string, unknown>;\n\n const accessToken = str(body.access_token);\n const expiresAt = str(body.expires_at);\n const userId = str(user.id);\n\n if (!accessToken || !expiresAt || !userId) {\n throw networkError(\n `${host} returned an incomplete session.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `CLI auth routes, then retry.`,\n );\n }\n\n return { accessToken, expiresAt, userId, email: str(user.email) };\n}\n\nexport async function request(\n url: string,\n init: RequestInit,\n): Promise<Response> {\n try {\n return await fetch(url, {\n ...init,\n redirect: 'manual',\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n\n throw networkError(\n `Could not reach ${new URL(url).origin}: ${reason}`,\n `Check the host is correct and reachable from this machine ` +\n `(proxy, VPN, DNS), then retry.`,\n );\n }\n}\n\n/**\n * Best-effort body read for a response whose status may be an error. Returns\n * an empty object rather than throwing, because the caller needs to inspect\n * the body BEFORE deciding what the status means — and a 500 from a proxy may\n * carry no JSON at all.\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\nasync function readJson(\n response: Response,\n what: string,\n): Promise<Record<string, unknown>> {\n let parsed: unknown;\n\n try {\n parsed = await response.json();\n } catch {\n throw networkError(\n `${what} did not return JSON.`,\n `Confirm the host is a Fusion deployment and not a proxy or captive ` +\n `portal sitting in front of one.`,\n );\n }\n\n if (typeof parsed !== 'object' || parsed === null) {\n throw networkError(\n `${what} returned an unexpected payload.`,\n `Confirm the host is a Fusion deployment, then retry.`,\n );\n }\n\n return parsed as Record<string, unknown>;\n}\n\n/**\n * 503 means the deployment is not provisioned for CLI login — an operator\n * action that has not happened, not a fault in the request.\n *\n * Every route that reads the Fusion CLI Auth0 config can answer this, so every\n * caller has to check for it. Falling through to `unexpectedStatus` would tell\n * a partner \"confirm the host is a Fusion deployment\" when the host is a\n * perfectly good Fusion deployment that is simply missing one env var — and\n * would exit 8 (network, retryable) instead of 9 (precondition, not).\n */\nfunction throwIfNotConfigured(\n response: Response,\n host: string,\n body?: Record<string, unknown>,\n): void {\n if (response.status !== 503) return;\n\n // 503 carries two meanings on these routes. Only CLI_NOT_CONFIGURED is a\n // precondition; REFRESH_INCOMPLETE is transient and handled by its caller.\n if (body && body.code !== 'CLI_NOT_CONFIGURED') return;\n\n throw preconditionError(\n `${host} does not have CLI login configured.`,\n `Ask the Fusion operator to provision the \"Fusion CLI\" Auth0 ` +\n `application and set AUTH0_CLI_CLIENT_ID on that deployment.`,\n );\n}\n\nfunction unexpectedStatus(host: string, route: string, status: number) {\n return networkError(\n `${host}${route} responded ${status}.`,\n status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if it ` +\n `persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes ${route}.`,\n );\n}\n\nfunction assertOnDomain(endpoint: string, domain: string): void {\n let url: URL;\n\n try {\n url = new URL(endpoint);\n } catch {\n throw preconditionError(\n `The login configuration contains a malformed endpoint.`,\n `Report this to the operator of the Fusion deployment.`,\n );\n }\n\n if (\n url.protocol !== 'https:' ||\n url.host.toLowerCase() !== domain.toLowerCase()\n ) {\n throw preconditionError(\n `The login configuration points at an endpoint outside its own Auth0 ` +\n `domain (${url.host}).`,\n `Do not continue. Report this to the operator of the Fusion ` +\n `deployment — it may be misconfigured or compromised.`,\n );\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"]}