@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
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport {\n acceptedKeysAt,\n extractUnrecognizedKeys,\n suggestKey,\n} from '../did-you-mean';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\nimport { loadProject } from '../project';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport {\n ENCLOSING_NOTE,\n extractPathFromMessage,\n isRootPath,\n locateDotPath,\n} from '../source-locator';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n env?: Record<string, string | undefined>;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n *\n * The findings pass itself lives in `validate-findings.ts`, shared with\n * `publish` so the publish gate cannot drift from what validate checks.\n *\n * Runs the schema-skew gate FIRST, before anything else — see\n * `schema-skew.ts` for why a newer project schema than the CLI bundles makes\n * every finding below it unreliable.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings = await collectAllFindings(loaded);\n enrichSchemaFindings(findings, loaded.projectDir);\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (errorCount > 0) {\n // Render the shared findings block to stderr BEFORE the envelope\n // settles, human mode only — JSON mode already carries the exact same\n // findings in `data.findings` and must stay byte-identical. The closing\n // order is findings block -> short summary (via `ctx.fail`'s\n // `findingsSummary` option) -> next action: `log` doesn't care that the\n // context already settled, so it's safe to call after `fail`.\n if (!ctx.jsonMode) {\n renderFindings(ctx, findings, { cwd: loaded.projectDir });\n }\n\n const exitCode = ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n !ctx.jsonMode\n ? { findingsSummary: { command: 'ekanos validate', errorCount } }\n : undefined,\n );\n\n if (!ctx.jsonMode) {\n ctx.log(ctx.ui.nextAction('ekanos validate', 'after fixing, re-check'));\n }\n\n return exitCode;\n }\n\n const successLine = ctx.jsonMode\n ? `validate: OK — no findings for ${slugs}.`\n : ctx.ui.ok(`${ctx.ui.glyphs.ok} validate: no findings for ${slugs}.`);\n\n return ctx.succeed(data, successLine);\n}\n\n/**\n * Best-effort dot-path → `file:line` + \"did you mean?\" enrichment for\n * `definition.schema` findings — the ones `collectDefinitionFindings` builds\n * directly from a zod issue (see its doc comment in\n * `@ekanos/integration-schema`). Mutates `line` (already part of `Finding`'s\n * shape, just usually absent) and, for an unrecognized key with a close\n * match, `hint`; for a resolved-but-missing key it appends a plain-text (no\n * ANSI — `findings-renderer.ts` styles it at render time) note to `message`.\n * These are additive changes to `data.findings` in `--json` mode too — the\n * envelope's existing fields never change shape or disappear across a minor\n * release, so a partner's own JSON parsing keeps working; only new optional\n * fields are ever added.\n *\n * Every other finding, and every locator miss, passes through unchanged:\n * `source-locator.ts`'s whole design is to never guess, so a miss here is\n * silent, not an error.\n */\nfunction enrichSchemaFindings(findings: Finding[], projectDir: string): void {\n const sourceCache = new Map<string, string | null>();\n\n const readSource = (file: string): string | null => {\n const cached = sourceCache.get(file);\n if (cached !== undefined) return cached;\n\n let text: string | null;\n try {\n text = fs.readFileSync(path.resolve(projectDir, file), 'utf8');\n } catch {\n // Best-effort — an unreadable file (deleted, permissions, a race with\n // a concurrent edit) just means no enrichment, never a validate\n // failure.\n text = null;\n }\n sourceCache.set(file, text);\n return text;\n };\n\n for (const finding of findings) {\n if (finding.check !== 'definition.schema') continue;\n if (finding.line !== undefined || finding.file === undefined) continue;\n\n const parsed = extractPathFromMessage(finding.message);\n if (!parsed) continue;\n\n const source = readSource(finding.file);\n if (source === null) continue;\n\n const unrecognizedKeys = extractUnrecognizedKeys(parsed.rest);\n if (unrecognizedKeys) {\n applyUnrecognizedKeyEnrichment(\n finding,\n source,\n parsed.path,\n unrecognizedKeys,\n );\n continue;\n }\n\n applyLocation(finding, locateDotPath(source, parsed.path));\n }\n}\n\n/** Sets `line`/`message` from a locate result — shared by the plain schema-path case and the unrecognized-key case below. A `'none'` result changes nothing. */\nfunction applyLocation(\n finding: Finding,\n result: ReturnType<typeof locateDotPath>,\n): void {\n if (result.confidence === 'none') return;\n\n finding.line = result.line;\n if (result.confidence === 'enclosing') {\n finding.message = `${finding.message} ${ENCLOSING_NOTE}`;\n }\n}\n\nfunction applyUnrecognizedKeyEnrichment(\n finding: Finding,\n source: string,\n parentPath: string,\n badKeys: string[],\n): void {\n const candidates = acceptedKeysAt(parentPath);\n const suggestions = badKeys\n .map((bad) => {\n const suggestion = suggestKey(bad, candidates);\n return suggestion ? { bad, suggestion } : null;\n })\n .filter(\n (entry): entry is { bad: string; suggestion: string } => entry !== null,\n );\n\n const [onlySuggestion] = suggestions;\n if (suggestions.length === 1 && badKeys.length === 1 && onlySuggestion) {\n finding.hint = `did you mean \"${onlySuggestion.suggestion}\"?`;\n } else if (suggestions.length > 0) {\n finding.hint =\n suggestions.map((s) => `\"${s.bad}\" → \"${s.suggestion}\"`).join(', ') + '?';\n }\n\n const [firstBadKey] = badKeys;\n if (firstBadKey === undefined) return;\n\n const fullPath = isRootPath(parentPath)\n ? firstBadKey\n : `${parentPath}.${firstBadKey}`;\n applyLocation(finding, locateDotPath(source, fullPath));\n}\n"]}
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport {\n acceptedKeysAt,\n extractUnrecognizedKeys,\n suggestKey,\n} from '../did-you-mean';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\nimport { loadProject } from '../project';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport {\n ENCLOSING_NOTE,\n extractPathFromMessage,\n isRootPath,\n locateDotPath,\n} from '../source-locator';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n env?: Record<string, string | undefined>;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n *\n * The findings pass itself lives in `validate-findings.ts`, shared with\n * `publish` so the publish gate cannot drift from what validate checks.\n *\n * Runs the schema-skew gate FIRST, before anything else — see\n * `schema-skew.ts` for why a newer project schema than the CLI bundles makes\n * every finding below it unreliable.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings = await collectAllFindings(loaded);\n enrichSchemaFindings(findings, loaded.projectDir);\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (errorCount > 0) {\n // Render the shared findings block to stderr BEFORE the envelope\n // settles, human mode only — JSON mode already carries the exact same\n // findings in `data.findings` and must stay byte-identical. The closing\n // order is findings block -> short summary (via `ctx.fail`'s\n // `findingsSummary` option) -> next action: `log` doesn't care that the\n // context already settled, so it's safe to call after `fail`.\n if (!ctx.jsonMode) {\n renderFindings(ctx, findings, { cwd: loaded.projectDir });\n }\n\n const exitCode = ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n !ctx.jsonMode\n ? { findingsSummary: { command: 'ekanos validate', errorCount } }\n : undefined,\n );\n\n if (!ctx.jsonMode) {\n ctx.log(ctx.ui.nextAction('ekanos validate', 'after fixing, re-check'));\n }\n\n return exitCode;\n }\n\n const successLine = ctx.jsonMode\n ? `validate: OK — no findings for ${slugs}.`\n : ctx.ui.ok(`${ctx.ui.glyphs.ok} validate: no findings for ${slugs}.`);\n\n return ctx.succeed(data, successLine);\n}\n\n/**\n * Best-effort dot-path → `file:line` + \"did you mean?\" enrichment for\n * `definition.schema` findings — the ones `collectDefinitionFindings` builds\n * directly from a zod issue (see its doc comment in\n * `@ekanos/integration-schema`). Mutates `line` (already part of `Finding`'s\n * shape, just usually absent) and, for an unrecognized key with a close\n * match, `hint`; for a resolved-but-missing key it appends a plain-text (no\n * ANSI — `findings-renderer.ts` styles it at render time) note to `message`.\n * These are additive changes to `data.findings` in `--json` mode too — the\n * envelope's existing fields never change shape or disappear across a minor\n * release, so a partner's own JSON parsing keeps working; only new optional\n * fields are ever added.\n *\n * Every other finding, and every locator miss, passes through unchanged:\n * `source-locator.ts`'s whole design is to never guess, so a miss here is\n * silent, not an error.\n */\nfunction enrichSchemaFindings(findings: Finding[], projectDir: string): void {\n const sourceCache = new Map<string, string | null>();\n\n const readSource = (file: string): string | null => {\n const cached = sourceCache.get(file);\n if (cached !== undefined) return cached;\n\n let text: string | null;\n try {\n text = fs.readFileSync(path.resolve(projectDir, file), 'utf8');\n } catch {\n // Best-effort — an unreadable file (deleted, permissions, a race with\n // a concurrent edit) just means no enrichment, never a validate\n // failure.\n text = null;\n }\n sourceCache.set(file, text);\n return text;\n };\n\n for (const finding of findings) {\n if (finding.check !== 'definition.schema') continue;\n if (finding.line !== undefined || finding.file === undefined) continue;\n\n const parsed = extractPathFromMessage(finding.message);\n if (!parsed) continue;\n\n const source = readSource(finding.file);\n if (source === null) continue;\n\n const unrecognizedKeys = extractUnrecognizedKeys(parsed.rest);\n if (unrecognizedKeys) {\n applyUnrecognizedKeyEnrichment(\n finding,\n source,\n parsed.path,\n unrecognizedKeys,\n );\n continue;\n }\n\n applyLocation(finding, locateDotPath(source, parsed.path));\n }\n}\n\n/** Sets `line`/`message` from a locate result — shared by the plain schema-path case and the unrecognized-key case below. A `'none'` result changes nothing. */\nfunction applyLocation(\n finding: Finding,\n result: ReturnType<typeof locateDotPath>,\n): void {\n if (result.confidence === 'none') return;\n\n finding.line = result.line;\n if (result.confidence === 'enclosing') {\n finding.message = `${finding.message} ${ENCLOSING_NOTE}`;\n }\n}\n\nfunction applyUnrecognizedKeyEnrichment(\n finding: Finding,\n source: string,\n parentPath: string,\n badKeys: string[],\n): void {\n const candidates = acceptedKeysAt(parentPath);\n const suggestions = badKeys\n .map((bad) => {\n const suggestion = suggestKey(bad, candidates);\n return suggestion ? { bad, suggestion } : null;\n })\n .filter(\n (entry): entry is { bad: string; suggestion: string } => entry !== null,\n );\n\n const [onlySuggestion] = suggestions;\n if (suggestions.length === 1 && badKeys.length === 1 && onlySuggestion) {\n finding.hint = `did you mean \"${onlySuggestion.suggestion}\"?`;\n } else if (suggestions.length > 0) {\n finding.hint =\n suggestions.map((s) => `\"${s.bad}\" → \"${s.suggestion}\"`).join(', ') + '?';\n }\n\n const [firstBadKey] = badKeys;\n if (firstBadKey === undefined) return;\n\n const fullPath = isRootPath(parentPath)\n ? firstBadKey\n : `${parentPath}.${firstBadKey}`;\n applyLocation(finding, locateDotPath(source, fullPath));\n}\n"]}
@@ -7,7 +7,7 @@ export interface WhoamiArgs {
7
7
  env: Record<string, string | undefined>;
8
8
  }
9
9
  /**
10
- * `whoami` — prove the stored session still authenticates against Fusion.
10
+ * `whoami` — prove the stored session still authenticates against Ekanos.
11
11
  *
12
12
  * The identity is resolved server-side by `/api/auth/cli-whoami`, which reads
13
13
  * the account through RLS as the caller. Printing the locally cached email
@@ -1,7 +1,7 @@
1
1
  import { requireSession, resolveAuthEnvironment, resolveIdentity, } from '../auth/session.js';
2
2
  import { fetchSeatsWithOneRefresh } from '../seats.js';
3
3
  /**
4
- * `whoami` — prove the stored session still authenticates against Fusion.
4
+ * `whoami` — prove the stored session still authenticates against Ekanos.
5
5
  *
6
6
  * The identity is resolved server-side by `/api/auth/cli-whoami`, which reads
7
7
  * the account through RLS as the caller. Printing the locally cached email
@@ -1 +1 @@
1
- {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AASpD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAe,EACf,IAAgB;;IAEhB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE;QAC3D,UAAU,EAAE,IAAI,CAAC,GAAG;KACrB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEjE,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAEpD,OAAO,GAAG,CAAC,OAAO,CAChB;QACE,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE;YACJ,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;SACpB;QACD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;QACnC,KAAK;KACN,EACD,WAAW,MAAA,QAAQ,CAAC,KAAK,mCAAI,QAAQ,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,GAAG;QACxD,CAAC,KAAK;YACJ,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAChB,CAAC,CAAC,WAAW,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAChF,CAAC,CAAC,mBAAmB;YACvB,CAAC,CAAC,mCAAmC,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,GAAmD,EACnD,OAAuD;IAEvD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAE5D,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAExC,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import {\n requireSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { fetchSeatsWithOneRefresh } from '../seats';\n\nexport interface WhoamiArgs {\n host?: string;\n /** Where a project's ekanos.json may sit, for its optional `host` field. */\n cwd: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `whoami` — prove the stored session still authenticates against Fusion.\n *\n * The identity is resolved server-side by `/api/auth/cli-whoami`, which reads\n * the account through RLS as the caller. Printing the locally cached email\n * would be a cheaper lie: it would keep saying \"logged in\" after the account\n * was deleted or the token revoked.\n *\n * Exit 4 (auth_required) when there is no session or it cannot be renewed —\n * the code a script should branch on to decide whether to run `ekanos login`.\n *\n * `seats` is a best-effort add-on from the same source-visibility endpoint\n * `ekanos sources` uses, compacted to slug+roles. Its failure — network, MFA,\n * a deployment that predates the route — must never turn a working `whoami`\n * into a failing one, so any error here degrades to `seats: null` rather than\n * propagating.\n */\nexport async function runWhoami(\n ctx: CliContext,\n args: WhoamiArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.cwd,\n });\n const stored = requireSession(env.store, env.host);\n\n const { identity, session } = await resolveIdentity(env, stored);\n\n const seats = await fetchSeatsQuietly(env, session);\n\n return ctx.succeed(\n {\n host: env.host,\n user: {\n id: identity.id,\n email: identity.email,\n name: identity.name,\n },\n expiresAt: session.expiresAt,\n credentialsPath: env.store.filePath,\n seats,\n },\n `whoami: ${identity.email ?? identity.id} on ${env.host}.` +\n (seats\n ? seats.length > 0\n ? ` Seats: ${seats.map((s) => `${s.slug} [${s.roles.join(', ')}]`).join(', ')}.`\n : ' No source seats.'\n : ' (source seat lookup unavailable)'),\n );\n}\n\nasync function fetchSeatsQuietly(\n env: Parameters<typeof fetchSeatsWithOneRefresh>[0],\n session: Parameters<typeof fetchSeatsWithOneRefresh>[1],\n): Promise<{ slug: string; roles: string[] }[] | null> {\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status !== 'ok') return null;\n\n return result.sources.map((s) => ({ slug: s.slug, roles: s.roles }));\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AASpD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAe,EACf,IAAgB;;IAEhB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE;QAC3D,UAAU,EAAE,IAAI,CAAC,GAAG;KACrB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEjE,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAEpD,OAAO,GAAG,CAAC,OAAO,CAChB;QACE,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE;YACJ,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;SACpB;QACD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;QACnC,KAAK;KACN,EACD,WAAW,MAAA,QAAQ,CAAC,KAAK,mCAAI,QAAQ,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,GAAG;QACxD,CAAC,KAAK;YACJ,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAChB,CAAC,CAAC,WAAW,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAChF,CAAC,CAAC,mBAAmB;YACvB,CAAC,CAAC,mCAAmC,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,GAAmD,EACnD,OAAuD;IAEvD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAE5D,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAExC,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;eAAO,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import {\n requireSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { fetchSeatsWithOneRefresh } from '../seats';\n\nexport interface WhoamiArgs {\n host?: string;\n /** Where a project's ekanos.json may sit, for its optional `host` field. */\n cwd: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `whoami` — prove the stored session still authenticates against Ekanos.\n *\n * The identity is resolved server-side by `/api/auth/cli-whoami`, which reads\n * the account through RLS as the caller. Printing the locally cached email\n * would be a cheaper lie: it would keep saying \"logged in\" after the account\n * was deleted or the token revoked.\n *\n * Exit 4 (auth_required) when there is no session or it cannot be renewed —\n * the code a script should branch on to decide whether to run `ekanos login`.\n *\n * `seats` is a best-effort add-on from the same source-visibility endpoint\n * `ekanos sources` uses, compacted to slug+roles. Its failure — network, MFA,\n * a deployment that predates the route — must never turn a working `whoami`\n * into a failing one, so any error here degrades to `seats: null` rather than\n * propagating.\n */\nexport async function runWhoami(\n ctx: CliContext,\n args: WhoamiArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.cwd,\n });\n const stored = requireSession(env.store, env.host);\n\n const { identity, session } = await resolveIdentity(env, stored);\n\n const seats = await fetchSeatsQuietly(env, session);\n\n return ctx.succeed(\n {\n host: env.host,\n user: {\n id: identity.id,\n email: identity.email,\n name: identity.name,\n },\n expiresAt: session.expiresAt,\n credentialsPath: env.store.filePath,\n seats,\n },\n `whoami: ${identity.email ?? identity.id} on ${env.host}.` +\n (seats\n ? seats.length > 0\n ? ` Seats: ${seats.map((s) => `${s.slug} [${s.roles.join(', ')}]`).join(', ')}.`\n : ' No source seats.'\n : ' (source seat lookup unavailable)'),\n );\n}\n\nasync function fetchSeatsQuietly(\n env: Parameters<typeof fetchSeatsWithOneRefresh>[0],\n session: Parameters<typeof fetchSeatsWithOneRefresh>[1],\n): Promise<{ slug: string; roles: string[] }[] | null> {\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status !== 'ok') return null;\n\n return result.sources.map((s) => ({ slug: s.slug, roles: s.roles }));\n } catch {\n return null;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { readTemplateSource, templatesDir } from './templates';\n\n/**\n * The shell templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the interesting failure is the quiet one:\n *\n * `pnpm up @ekanos/harness` → 0.2, which moves an export.\n * `ekanos dev` regenerates from the OLD CLI's templates and stamps \"0.2\"\n * into the marker. Every later run now considers the shell current. It is\n * not: it was generated against an obsolete contract, and what the partner\n * sees is a module-resolution error rather than \"these are incompatible\".\n *\n * That directly attacks the promise the package boundary exists to keep — ship\n * a harness fix as a version bump a partner picks up with `pnpm up`. So the\n * CLI declares which `@ekanos/harness` versions ITS templates are written\n * against, checks the installed one before generating, and names the\n * incompatibility instead of letting a resolution error stand in for it.\n */\n\n/**\n * The shell contract this CLI's templates GENERATE.\n *\n * An integer, not a semver range. A range can only INFER compatibility from\n * version numbers — it cannot tell a harness bugfix from a change to the shell\n * the harness requires, so it either blocks upgrades that were fine or waves\n * through ones that were not. The integer is declared by both sides and is\n * authoritative.\n *\n * **Bump this only when the generated shell's coupling to the package\n * changes**: an export renamed or moved between `/app` and `/routes`, the\n * props of `RootLayout` or `IndexPage`, the server/client split, the alias\n * specifier, which side owns `@import 'tailwindcss'`, or the set of files the\n * shell must contain. Editing a comment in a template does not count — that is\n * what the template fingerprint in the marker is for.\n */\nexport const SHELL_CONTRACT = 2;\n\n/**\n * Contract history — what each bump changed, so a mismatch message can be\n * read against it:\n *\n * 1 the original ten-file shell: pages re-exporting `/routes`, the client\n * shell + index page injecting the registry into `/app`.\n * 2 adds `app/api/integrations/[slug]/[...path]/route.ts`, injecting the\n * registry into the new `@ekanos/harness/api` server entry. A harness\n * without that export cannot back the route; a shell without the route\n * leaves that harness's live-mode OAuth loop unreachable.\n */\n\n/**\n * The contract an installed `@ekanos/harness` declares it needs, from\n * `{ \"ekanos\": { \"shellContract\": N } }` in its package.json.\n *\n * A package that declares nothing is read as contract 1 — the contract that\n * shipped first — so the harness released before this existed keeps working.\n * That default is deliberately silent, which is why `@ekanos/harness` asserts\n * the field's presence in its own pack test rather than relying on it.\n */\nexport function declaredShellContract(harnessManifest: unknown): number {\n if (typeof harnessManifest !== 'object' || harnessManifest === null) return 1;\n const ekanos = (harnessManifest as { ekanos?: unknown }).ekanos;\n if (typeof ekanos !== 'object' || ekanos === null) return 1;\n const declared = (ekanos as { shellContract?: unknown }).shellContract;\n return Number.isInteger(declared) ? (declared as number) : 1;\n}\n\n/** `[major, minor, patch]`, with anything non-numeric treated as 0. */\nfunction parseVersion(version: string): [number, number, number] {\n const core = version.split('+')[0]!.split('-')[0]!;\n const parts = core.split('.').map((part) => Number.parseInt(part, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n for (let i = 0; i < 3; i++) {\n if (left[i]! !== right[i]!) return left[i]! < right[i]! ? -1 : 1;\n }\n return 0;\n}\n\n/** This CLI's own version, read from its package.json at runtime. */\nexport function cliVersion(): string {\n const manifestPath = path.join(templatesDir(), '..', 'package.json');\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n return typeof version === 'string' ? version : '0.0.0';\n}\n\n/**\n * Fail unless this CLI's templates generate the shell the installed harness\n * expects.\n *\n * The templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the failure that matters is the quiet one:\n * `pnpm up @ekanos/harness` moves an export, `ekanos dev` regenerates from the\n * OLD CLI's templates, and the partner sees a module-resolution error rather\n * than anything naming the incompatibility.\n *\n * Byte-comparing generated files cannot catch it: the old templates' bytes are\n * exactly what the old CLI intends, so nothing regenerates differently and the\n * shell stays wired to something that moved. Only a declared contract can.\n *\n * Runs before anything is written, so an incompatible pair produces no shell\n * at all — a broken shell that builds is worse than no shell.\n */\nexport function assertHarnessCompatible(params: {\n harnessVersion: string;\n harnessManifest: unknown;\n installHint: (packageSpec: string) => string;\n}): void {\n const required = declaredShellContract(params.harnessManifest);\n if (required === SHELL_CONTRACT) return;\n\n if (required > SHELL_CONTRACT) {\n throw preconditionError(\n `@ekanos/cli ${cliVersion()} is too old for @ekanos/harness ` +\n `${params.harnessVersion}: that harness needs shell contract ` +\n `${required}, and this CLI generates ${SHELL_CONTRACT}.`,\n `Upgrade the CLI: ${params.installHint('@ekanos/cli@latest')}. Then ` +\n 're-run \"ekanos dev\" and the shell is regenerated for you.',\n );\n }\n\n throw preconditionError(\n `@ekanos/harness ${params.harnessVersion} is too old for @ekanos/cli ` +\n `${cliVersion()}: this CLI generates shell contract ${SHELL_CONTRACT}, ` +\n `and that harness needs ${required}.`,\n `Upgrade it: ${params.installHint('@ekanos/harness@latest')}.`,\n );\n}\n\n/**\n * A fingerprint of the template SOURCES, independent of the slug they are\n * rendered with.\n *\n * The harness version alone cannot certify a shell: the templates can change\n * while it stays put (a CLI patch release), and it can change while they stay\n * put. Hashing the sources means any edit to what we generate invalidates\n * every shell generated before it, including one produced by a CLI version\n * that never shipped.\n */\nexport function templatesFingerprint(templateNames: readonly string[]): string {\n const hash = createHash('sha256');\n for (const name of [...templateNames].sort()) {\n hash.update(name);\n hash.update('\\0');\n hash.update(readTemplateSource(name));\n hash.update('\\0');\n }\n return hash.digest('hex').slice(0, 16);\n}\n"]}
1
+ {"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { readTemplateSource, templatesDir } from './templates';\n\n/**\n * The shell templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the interesting failure is the quiet one:\n *\n * `pnpm up @ekanos/harness` → 0.2, which moves an export.\n * `ekanos dev` regenerates from the OLD CLI's templates and stamps \"0.2\"\n * into the marker. Every later run now considers the shell current. It is\n * not: it was generated against an obsolete contract, and what the partner\n * sees is a module-resolution error rather than \"these are incompatible\".\n *\n * That directly attacks the promise the package boundary exists to keep — ship\n * a harness fix as a version bump a partner picks up with `pnpm up`. So the\n * CLI declares which `@ekanos/harness` versions ITS templates are written\n * against, checks the installed one before generating, and names the\n * incompatibility instead of letting a resolution error stand in for it.\n */\n\n/**\n * The shell contract this CLI's templates GENERATE.\n *\n * An integer, not a semver range. A range can only INFER compatibility from\n * version numbers — it cannot tell a harness bugfix from a change to the shell\n * the harness requires, so it either blocks upgrades that were fine or waves\n * through ones that were not. The integer is declared by both sides and is\n * authoritative.\n *\n * **Bump this only when the generated shell's coupling to the package\n * changes**: an export renamed or moved between `/app` and `/routes`, the\n * props of `RootLayout` or `IndexPage`, the server/client split, the alias\n * specifier, which side owns `@import 'tailwindcss'`, or the set of files the\n * shell must contain. Editing a comment in a template does not count — that is\n * what the template fingerprint in the marker is for.\n */\nexport const SHELL_CONTRACT = 2;\n\n/**\n * Contract history — what each bump changed, so a mismatch message can be\n * read against it:\n *\n * 1 the original ten-file shell: pages re-exporting `/routes`, the client\n * shell + index page injecting the registry into `/app`.\n * 2 adds `app/api/integrations/[slug]/[...path]/route.ts`, injecting the\n * registry into the new `@ekanos/harness/api` server entry. A harness\n * without that export cannot back the route; a shell without the route\n * leaves that harness's live-mode OAuth loop unreachable.\n */\n\n/**\n * The contract an installed `@ekanos/harness` declares it needs, from\n * `{ \"ekanos\": { \"shellContract\": N } }` in its package.json.\n *\n * A package that declares nothing is read as contract 1 — the contract that\n * shipped first — so the harness released before this existed keeps working.\n * That default is deliberately silent, which is why `@ekanos/harness` asserts\n * the field's presence in its own pack test rather than relying on it.\n */\nexport function declaredShellContract(harnessManifest: unknown): number {\n if (typeof harnessManifest !== 'object' || harnessManifest === null) return 1;\n const ekanos = (harnessManifest as { ekanos?: unknown }).ekanos;\n if (typeof ekanos !== 'object' || ekanos === null) return 1;\n const declared = (ekanos as { shellContract?: unknown }).shellContract;\n return Number.isInteger(declared) ? (declared as number) : 1;\n}\n\n/** `[major, minor, patch]`, with anything non-numeric treated as 0. */\nfunction parseVersion(version: string): [number, number, number] {\n const core = version.split('+')[0]!.split('-')[0]!;\n const parts = core.split('.').map((part) => Number.parseInt(part, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n for (let i = 0; i < 3; i++) {\n if (left[i]! !== right[i]!) return left[i]! < right[i]! ? -1 : 1;\n }\n return 0;\n}\n\n/** This CLI's own version, read from its package.json at runtime. */\nexport function cliVersion(): string {\n const manifestPath = path.join(templatesDir(), '..', 'package.json');\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n return typeof version === 'string' ? version : '0.0.0';\n}\n\n/**\n * Fail unless this CLI's templates generate the shell the installed harness\n * expects.\n *\n * The templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the failure that matters is the quiet one:\n * `pnpm up @ekanos/harness` moves an export, `ekanos dev` regenerates from the\n * OLD CLI's templates, and the partner sees a module-resolution error rather\n * than anything naming the incompatibility.\n *\n * Byte-comparing generated files cannot catch it: the old templates' bytes are\n * exactly what the old CLI intends, so nothing regenerates differently and the\n * shell stays wired to something that moved. Only a declared contract can.\n *\n * Runs before anything is written, so an incompatible pair produces no shell\n * at all — a broken shell that builds is worse than no shell.\n */\nexport function assertHarnessCompatible(params: {\n harnessVersion: string;\n harnessManifest: unknown;\n installHint: (packageSpec: string) => string;\n}): void {\n const required = declaredShellContract(params.harnessManifest);\n if (required === SHELL_CONTRACT) return;\n\n if (required > SHELL_CONTRACT) {\n throw preconditionError(\n `@ekanos/cli ${cliVersion()} is too old for @ekanos/harness ` +\n `${params.harnessVersion}: that harness needs shell contract ` +\n `${required}, and this CLI generates ${SHELL_CONTRACT}.`,\n `Upgrade the CLI: ${params.installHint('@ekanos/cli@latest')}. Then ` +\n 're-run \"ekanos dev\" and the shell is regenerated for you.',\n );\n }\n\n throw preconditionError(\n `@ekanos/harness ${params.harnessVersion} is too old for @ekanos/cli ` +\n `${cliVersion()}: this CLI generates shell contract ${SHELL_CONTRACT}, ` +\n `and that harness needs ${required}.`,\n `Upgrade it: ${params.installHint('@ekanos/harness@latest')}.`,\n );\n}\n\n/**\n * A fingerprint of the template SOURCES, independent of the slug they are\n * rendered with.\n *\n * The harness version alone cannot certify a shell: the templates can change\n * while it stays put (a CLI patch release), and it can change while they stay\n * put. Hashing the sources means any edit to what we generate invalidates\n * every shell generated before it, including one produced by a CLI version\n * that never shipped.\n */\nexport function templatesFingerprint(templateNames: readonly string[]): string {\n const hash = createHash('sha256');\n for (const name of [...templateNames].sort()) {\n hash.update(name);\n hash.update('\\0');\n hash.update(readTemplateSource(name));\n hash.update('\\0');\n }\n return hash.digest('hex').slice(0, 16);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliError } from './errors';\nimport { EXIT_CODES, type ExitCode } from './exit-codes';\nimport { type Ui, createUi } from './ui';\n\n/**\n * A single writable sink. Real streams satisfy this; tests pass a capturing\n * buffer so the stdout-purity invariant can be asserted byte-for-byte.\n */\nexport interface WriteStream {\n write(chunk: string): void;\n isTTY?: boolean;\n}\n\nexport interface ContextInput {\n /**\n * The user's EXPLICIT preference, already parsed: `true` for `--json`,\n * `false` for `--no-json`, `undefined` when neither was passed. The\n * undefined case is what allows the mode to be inferred instead.\n */\n jsonFlag?: boolean;\n stdout?: WriteStream;\n stderr?: WriteStream;\n /** Overrides process.env for CLAUDECODE detection (tests). */\n env?: Record<string, string | undefined>;\n}\n\n/** The success envelope. */\ninterface OkEnvelope {\n ok: true;\n data: unknown;\n}\n\n/** The error envelope. `data` is optional — validate carries its findings here. */\ninterface ErrEnvelope {\n ok: false;\n data?: unknown;\n error: { code: string; message: string; hint: string };\n}\n\n/** See `fail`'s doc comment — the human-mode epilogue seam for a command that already rendered a findings block. */\ninterface FindingsSummary {\n /** The command name to name in the summary line, e.g. \"ekanos validate\". */\n command: string;\n errorCount: number;\n}\n\nexport interface FailOptions {\n findingsSummary?: FindingsSummary;\n}\n\n/**\n * `data` is deliberately `unknown` above — each verb owns its own shape — but\n * one convention spans several of them: `validate`, `dev`, `publish` and\n * `status` all add an OPTIONAL `toolchain` field to `data` (never to the\n * envelope's top level) when the proactive update check in `update-notice.ts`\n * finds this CLI is behind. See that file's `ToolchainNoticeData` for the\n * shape, and `README.md`'s \"Keeping the toolchain current\" section for the\n * full contract. It is absent, not `null`, when there is nothing to report.\n */\n\n/**\n * The agent-native output envelope, resolved ONCE at startup and threaded\n * through every command.\n *\n * The load-bearing invariant: in JSON mode, stdout receives EXACTLY ONE JSON\n * object for the whole process — nothing else, ever. Every progress line, warn,\n * and scrap of human prose goes to stderr. This is the single most common way\n * an agent's tool call breaks (a stray log line on stdout makes the JSON\n * unparseable), so the context enforces it structurally: `succeed`/`fail` are\n * the only writers of stdout, and a `settled` latch makes a second call throw\n * rather than emit a second object.\n */\nexport class CliContext {\n readonly jsonMode: boolean;\n /**\n * The environment the context resolved against — exposed so decoration\n * (the gradient banner) can honour `NO_COLOR` / `EKANOS_NO_BANNER` without\n * every call site threading `process.env` through by hand.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n /**\n * The style layer (`ui.ts`), resolved once against stderr — the stream\n * every human-mode render target uses — so every command shares one color\n * decision instead of each re-deriving it. See `ui.ts` for the tokens and\n * components this carries.\n */\n readonly ui: Ui;\n private readonly stdout: WriteStream;\n private readonly stderr: WriteStream;\n private settled = false;\n\n constructor(input: ContextInput = {}) {\n const stdout = input.stdout ?? defaultStream(process.stdout);\n const stderr = input.stderr ?? defaultStream(process.stderr);\n const env = input.env ?? process.env;\n\n this.stdout = stdout;\n this.stderr = stderr;\n this.env = env;\n this.ui = createUi({ isTTY: stderr.isTTY === true, env });\n\n // Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the\n // envelope on even at a terminal, `--no-json` forces the human form even\n // when piped. Only when the user expressed no preference is the mode\n // inferred — from a non-TTY stdout (piped into another program or an agent)\n // or the CLAUDECODE marker.\n const nonTty = stdout.isTTY !== true;\n this.jsonMode = input.jsonFlag ?? (nonTty || env.CLAUDECODE === '1');\n }\n\n /** Whether stderr is a real terminal — decoration (the banner) gates on this. */\n get stderrIsTTY(): boolean {\n return this.stderr.isTTY === true;\n }\n\n /**\n * Whether stdout is a real terminal — `wizard.ts`'s `canWizard` gates on\n * this (clack renders to stdout, unlike the rest of this CLI's human\n * output). `ctx.jsonMode` being false does not by itself guarantee this:\n * `--no-json` forces human mode even against a piped stdout.\n */\n get stdoutIsTTY(): boolean {\n return this.stdout.isTTY === true;\n }\n\n /** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */\n log(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /** A warning — stderr, no ANSI. */\n warn(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /**\n * Emit the `claude-code-hint` protocol line to STDERR. Used on --help and on\n * an unknown command so an agent reading stderr is told what to do next\n * without the line ever touching stdout.\n */\n emitClaudeCodeHint(payload: Record<string, unknown>): void {\n this.stderr.write(`claude-code-hint: ${JSON.stringify(payload)}\\n`);\n }\n\n /**\n * Terminal success. Writes the single stdout object (JSON mode) or a concise\n * human line (human mode). Returns the OK exit code so the caller can end the\n * process with it. Idempotent-by-latch: a second settle throws.\n */\n succeed(data: unknown, humanSummary?: string): ExitCode {\n this.settle();\n if (this.jsonMode) {\n const envelope: OkEnvelope = { ok: true, data };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stdout.write(`${humanSummary ?? 'OK'}\\n`);\n }\n return EXIT_CODES.OK;\n }\n\n /**\n * Terminal failure. Writes the single stdout error object (JSON mode) or the\n * message + hint to stderr (human mode). Returns the error's exit code.\n *\n * `error.hint` is guaranteed non-empty by CliError's constructor, so the\n * envelope's \"every error carries a hint\" invariant holds for every path\n * that reaches here.\n *\n * `options.findingsSummary` is the seam for a command that has ALREADY\n * rendered the shared findings block to stderr (human mode) before calling\n * this — `validate`'s and `publish`'s gate-refusal paths. Passing it swaps\n * the default two-line `Error: … / Hint: …` epilogue (whose hint always\n * points at the JSON-only `data.findings`) for one short summary line,\n * `✖ <command> failed: <n> error(s)`, so a partner never reads the same\n * finding jargon twice. JSON mode is completely unaffected either way —\n * `error.hint` always carries the original, JSON-only-appropriate text in\n * the envelope.\n */\n fail(error: CliError, data?: unknown, options?: FailOptions): ExitCode {\n this.failWith({\n code: error.code,\n message: error.message,\n hint: error.hint,\n exitCode: error.exitCode,\n data,\n findingsSummary: options?.findingsSummary,\n });\n return error.exitCode;\n }\n\n /**\n * The low-level failure emitter. Used directly by `test`, whose exit code is\n * a TRANSPARENT passthrough of the delegated runner's exit code (which is not\n * one of the CLI's own frozen numeric conditions) and whose error `code` is\n * an out-of-taxonomy string. Enforces the same non-empty-hint invariant as\n * CliError so no failure path can emit an empty hint.\n */\n failWith(params: {\n code: string;\n message: string;\n hint: string;\n exitCode: number;\n data?: unknown;\n findingsSummary?: FindingsSummary;\n }): number {\n this.settle();\n if (!params.hint || params.hint.trim().length === 0) {\n throw new Error(\n `failWith for \"${params.message}\" was given an empty hint. Every CLI ` +\n 'error must carry an imperative remediation.',\n );\n }\n if (this.jsonMode) {\n const envelope: ErrEnvelope = {\n ok: false,\n ...(params.data !== undefined ? { data: params.data } : {}),\n error: {\n code: params.code,\n message: params.message,\n hint: params.hint,\n },\n };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else if (params.findingsSummary) {\n const { command, errorCount } = params.findingsSummary;\n this.stderr.write(\n `${this.ui.err(\n `${this.ui.glyphs.err} ${command} failed: ${errorCount} error${\n errorCount === 1 ? '' : 's'\n }`,\n )}\\n`,\n );\n } else {\n this.stderr.write(`Error: ${params.message}\\n`);\n this.stderr.write(`Hint: ${params.hint}\\n`);\n }\n return params.exitCode;\n }\n\n private settle(): void {\n if (this.settled) {\n throw new Error(\n 'CliContext settled twice — a command tried to emit a second ' +\n 'terminal result. In JSON mode this would corrupt stdout with two ' +\n 'objects. This is a bug in the command.',\n );\n }\n this.settled = true;\n }\n}\n\nfunction defaultStream(stream: NodeJS.WriteStream): WriteStream {\n return {\n write: (chunk: string) => {\n stream.write(chunk);\n },\n isTTY: stream.isTTY,\n };\n}\n"]}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliError } from './errors';\nimport { EXIT_CODES, type ExitCode } from './exit-codes';\nimport { type Ui, createUi } from './ui';\n\n/**\n * A single writable sink. Real streams satisfy this; tests pass a capturing\n * buffer so the stdout-purity invariant can be asserted byte-for-byte.\n */\nexport interface WriteStream {\n write(chunk: string): void;\n isTTY?: boolean;\n}\n\nexport interface ContextInput {\n /**\n * The user's EXPLICIT preference, already parsed: `true` for `--json`,\n * `false` for `--no-json`, `undefined` when neither was passed. The\n * undefined case is what allows the mode to be inferred instead.\n */\n jsonFlag?: boolean;\n stdout?: WriteStream;\n stderr?: WriteStream;\n /** Overrides process.env for CLAUDECODE detection (tests). */\n env?: Record<string, string | undefined>;\n}\n\n/** The success envelope. */\ninterface OkEnvelope {\n ok: true;\n data: unknown;\n}\n\n/** The error envelope. `data` is optional — validate carries its findings here. */\ninterface ErrEnvelope {\n ok: false;\n data?: unknown;\n error: { code: string; message: string; hint: string };\n}\n\n/** See `fail`'s doc comment — the human-mode epilogue seam for a command that already rendered a findings block. */\ninterface FindingsSummary {\n /** The command name to name in the summary line, e.g. \"ekanos validate\". */\n command: string;\n errorCount: number;\n}\n\nexport interface FailOptions {\n findingsSummary?: FindingsSummary;\n}\n\n/**\n * `data` is deliberately `unknown` above — each verb owns its own shape — but\n * one convention spans several of them: `validate`, `dev`, `publish` and\n * `status` all add an OPTIONAL `toolchain` field to `data` (never to the\n * envelope's top level) when the proactive update check in `update-notice.ts`\n * finds this CLI is behind. See that file's `ToolchainNoticeData` for the\n * shape, and `README.md`'s \"Keeping the toolchain current\" section for the\n * full contract. It is absent, not `null`, when there is nothing to report.\n */\n\n/**\n * The agent-native output envelope, resolved ONCE at startup and threaded\n * through every command.\n *\n * The load-bearing invariant: in JSON mode, stdout receives EXACTLY ONE JSON\n * object for the whole process — nothing else, ever. Every progress line, warn,\n * and scrap of human prose goes to stderr. This is the single most common way\n * an agent's tool call breaks (a stray log line on stdout makes the JSON\n * unparseable), so the context enforces it structurally: `succeed`/`fail` are\n * the only writers of stdout, and a `settled` latch makes a second call throw\n * rather than emit a second object.\n */\nexport class CliContext {\n readonly jsonMode: boolean;\n /**\n * The environment the context resolved against — exposed so decoration\n * (the gradient banner) can honour `NO_COLOR` / `EKANOS_NO_BANNER` without\n * every call site threading `process.env` through by hand.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n /**\n * The style layer (`ui.ts`), resolved once against stderr — the stream\n * every human-mode render target uses — so every command shares one color\n * decision instead of each re-deriving it. See `ui.ts` for the tokens and\n * components this carries.\n */\n readonly ui: Ui;\n private readonly stdout: WriteStream;\n private readonly stderr: WriteStream;\n private settled = false;\n\n constructor(input: ContextInput = {}) {\n const stdout = input.stdout ?? defaultStream(process.stdout);\n const stderr = input.stderr ?? defaultStream(process.stderr);\n const env = input.env ?? process.env;\n\n this.stdout = stdout;\n this.stderr = stderr;\n this.env = env;\n this.ui = createUi({ isTTY: stderr.isTTY === true, env });\n\n // Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the\n // envelope on even at a terminal, `--no-json` forces the human form even\n // when piped. Only when the user expressed no preference is the mode\n // inferred — from a non-TTY stdout (piped into another program or an agent)\n // or the CLAUDECODE marker.\n const nonTty = stdout.isTTY !== true;\n this.jsonMode = input.jsonFlag ?? (nonTty || env.CLAUDECODE === '1');\n }\n\n /** Whether stderr is a real terminal — decoration (the banner) gates on this. */\n get stderrIsTTY(): boolean {\n return this.stderr.isTTY === true;\n }\n\n /**\n * Whether stdout is a real terminal — `wizard.ts`'s `canWizard` gates on\n * this (clack renders to stdout, unlike the rest of this CLI's human\n * output). `ctx.jsonMode` being false does not by itself guarantee this:\n * `--no-json` forces human mode even against a piped stdout.\n */\n get stdoutIsTTY(): boolean {\n return this.stdout.isTTY === true;\n }\n\n /** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */\n log(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /** A warning — stderr, no ANSI. */\n warn(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /**\n * Emit the `claude-code-hint` protocol line to STDERR. Used on --help and on\n * an unknown command so an agent reading stderr is told what to do next\n * without the line ever touching stdout.\n */\n emitClaudeCodeHint(payload: Record<string, unknown>): void {\n this.stderr.write(`claude-code-hint: ${JSON.stringify(payload)}\\n`);\n }\n\n /**\n * Terminal success. Writes the single stdout object (JSON mode) or a concise\n * human line (human mode). Returns the OK exit code so the caller can end the\n * process with it. Idempotent-by-latch: a second settle throws.\n */\n succeed(data: unknown, humanSummary?: string): ExitCode {\n this.settle();\n if (this.jsonMode) {\n const envelope: OkEnvelope = { ok: true, data };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stdout.write(`${humanSummary ?? 'OK'}\\n`);\n }\n return EXIT_CODES.OK;\n }\n\n /**\n * Terminal failure. Writes the single stdout error object (JSON mode) or the\n * message + hint to stderr (human mode). Returns the error's exit code.\n *\n * `error.hint` is guaranteed non-empty by CliError's constructor, so the\n * envelope's \"every error carries a hint\" invariant holds for every path\n * that reaches here.\n *\n * `options.findingsSummary` is the seam for a command that has ALREADY\n * rendered the shared findings block to stderr (human mode) before calling\n * this — `validate`'s and `publish`'s gate-refusal paths. Passing it swaps\n * the default two-line `Error: … / Hint: …` epilogue (whose hint always\n * points at the JSON-only `data.findings`) for one short summary line,\n * `✖ <command> failed: <n> error(s)`, so a partner never reads the same\n * finding jargon twice. JSON mode is completely unaffected either way —\n * `error.hint` always carries the original, JSON-only-appropriate text in\n * the envelope.\n */\n fail(error: CliError, data?: unknown, options?: FailOptions): ExitCode {\n this.failWith({\n code: error.code,\n message: error.message,\n hint: error.hint,\n exitCode: error.exitCode,\n data,\n findingsSummary: options?.findingsSummary,\n });\n return error.exitCode;\n }\n\n /**\n * The low-level failure emitter. Used directly by `test`, whose exit code is\n * a TRANSPARENT passthrough of the delegated runner's exit code (which is not\n * one of the CLI's own frozen numeric conditions) and whose error `code` is\n * an out-of-taxonomy string. Enforces the same non-empty-hint invariant as\n * CliError so no failure path can emit an empty hint.\n */\n failWith(params: {\n code: string;\n message: string;\n hint: string;\n exitCode: number;\n data?: unknown;\n findingsSummary?: FindingsSummary;\n }): number {\n this.settle();\n if (!params.hint || params.hint.trim().length === 0) {\n throw new Error(\n `failWith for \"${params.message}\" was given an empty hint. Every CLI ` +\n 'error must carry an imperative remediation.',\n );\n }\n if (this.jsonMode) {\n const envelope: ErrEnvelope = {\n ok: false,\n ...(params.data !== undefined ? { data: params.data } : {}),\n error: {\n code: params.code,\n message: params.message,\n hint: params.hint,\n },\n };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else if (params.findingsSummary) {\n const { command, errorCount } = params.findingsSummary;\n this.stderr.write(\n `${this.ui.err(\n `${this.ui.glyphs.err} ${command} failed: ${errorCount} error${\n errorCount === 1 ? '' : 's'\n }`,\n )}\\n`,\n );\n } else {\n this.stderr.write(`Error: ${params.message}\\n`);\n this.stderr.write(`Hint: ${params.hint}\\n`);\n }\n return params.exitCode;\n }\n\n private settle(): void {\n if (this.settled) {\n throw new Error(\n 'CliContext settled twice — a command tried to emit a second ' +\n 'terminal result. In JSON mode this would corrupt stdout with two ' +\n 'objects. This is a bug in the command.',\n );\n }\n this.settled = true;\n }\n}\n\nfunction defaultStream(stream: NodeJS.WriteStream): WriteStream {\n return {\n write: (chunk: string) => {\n stream.write(chunk);\n },\n isTTY: stream.isTTY,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../src/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { cliVersion } from './compatibility';\nimport { readInstalledPackageManifest } from './harness-scaffold';\n\n/**\n * Wrapper-style delegation to the project-local `@ekanos/cli` — the\n * Gradle-wrapper / Yarn / `npx` pattern, applied to this CLI.\n *\n * A global install bundles ONE `@ekanos/integration-schema` version but\n * routinely serves N projects on different `@ekanos/sdk` versions — it\n * cannot be correct for all of them simultaneously (this is exactly the\n * failure mode `schema-skew.ts` detects). Delegation dissolves the tension:\n * `ekanos` stays on PATH for convenience, but the four verbs that actually\n * read or generate project files always run through the project's OWN\n * pinned copy when one is installed and differs from the invoked binary.\n *\n * Machine-level verbs (`init`, `login`, `logout`, `whoami`, `sources`,\n * `use`, `status`, `upgrade`) are deliberately NEVER delegated: you never\n * want an old local copy handling credentials, and `upgrade` must always be\n * the newest logic doing the upgrading — delegating it could have an old CLI\n * \"upgrade\" a project using its own out-of-date resolution.\n */\n\nconst DELEGATABLE_VERBS = new Set(['validate', 'publish', 'dev', 'test']);\n\nexport interface DelegationPlan {\n shouldDelegate: boolean;\n ownVersion: string;\n localVersion?: string;\n /** Absolute path to the local CLI's bin entry, when `shouldDelegate`. */\n localBinPath?: string;\n}\n\n/** The first bare (non-flag) token before a standalone `--` — mirrors `splitCommand` in `index.ts`. */\nfunction firstCommand(argv: readonly string[]): string | null {\n for (const token of argv) {\n if (token === '--') break;\n if (!token.startsWith('-')) return token;\n }\n return null;\n}\n\n/** `--dir <path>` / `--dir=<path>`, before a standalone `--`. */\nfunction scanDirFlag(argv: readonly string[]): string | undefined {\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i]!;\n if (token === '--') break;\n if (token === '--dir') return argv[i + 1];\n if (token.startsWith('--dir=')) return token.slice('--dir='.length);\n }\n return undefined;\n}\n\n/**\n * Decide whether this invocation should delegate. Pure and side-effect-free\n * (beyond the one file read of the local `@ekanos/cli` manifest, mirroring\n * `readInstalledHarness`/`dev`'s own project-package resolution rather than\n * writing a second one) so it is directly testable without spawning anything.\n */\nexport function planDelegation(\n argv: readonly string[],\n env: Record<string, string | undefined>,\n cwd: string,\n): DelegationPlan {\n const ownVersion = cliVersion();\n\n // The recursion guard, and the explicit escape hatch, both win outright.\n if (env.EKANOS_DELEGATED === '1' || env.EKANOS_NO_DELEGATE === '1') {\n return { shouldDelegate: false, ownVersion };\n }\n\n const command = firstCommand(argv);\n if (!command || !DELEGATABLE_VERBS.has(command)) {\n return { shouldDelegate: false, ownVersion };\n }\n\n const projectDir = path.resolve(cwd, scanDirFlag(argv) ?? '.');\n // Best-effort, like every other resolution this function does: a corrupt\n // local manifest must fall through to \"don't delegate\", never throw. This\n // runs in `bin.ts` BEFORE `run()`'s try/catch exists, so an uncaught throw\n // here would bypass the JSON-envelope error machinery entirely and print a\n // raw stderr string instead — a real regression in the \"exactly one\n // terminal emission\" contract the rest of this package guarantees.\n let local: ReturnType<typeof readInstalledPackageManifest> = null;\n try {\n local = readInstalledPackageManifest(projectDir, '@ekanos/cli');\n } catch {\n local = null;\n }\n\n if (!local || local.version === ownVersion) {\n return { shouldDelegate: false, ownVersion, localVersion: local?.version };\n }\n\n return {\n shouldDelegate: true,\n ownVersion,\n localVersion: local.version,\n localBinPath: path.join(\n projectDir,\n 'node_modules',\n '@ekanos',\n 'cli',\n 'dist',\n 'bin.js',\n ),\n };\n}\n\n/**\n * Same JSON-mode inference `CliContext` uses (`context.ts`) — duplicated\n * deliberately rather than shared, because this runs BEFORE a `CliContext`\n * exists (delegation is decided ahead of the whole `run()` machinery). Keep\n * this in sync with `CliContext`'s constructor if that heuristic ever\n * changes: an explicit `--json`/`--no-json` wins, otherwise a non-TTY stdout\n * or `CLAUDECODE=1` means JSON mode.\n */\nfunction looksLikeJsonMode(\n argv: readonly string[],\n env: Record<string, string | undefined>,\n stdoutIsTTY: boolean,\n): boolean {\n for (const token of argv) {\n if (token === '--') break;\n if (token === '--json') return true;\n if (token === '--no-json') return false;\n }\n return !stdoutIsTTY || env.CLAUDECODE === '1';\n}\n\n/**\n * The one-line stderr announcement — human mode only, and NEVER on stdout\n * (a delegated `validate --json` must still emit exactly one JSON object on\n * stdout, from the CHILD, with nothing of ours ahead of it).\n */\nexport function announceDelegation(\n plan: DelegationPlan,\n argv: readonly string[],\n env: Record<string, string | undefined>,\n stdoutIsTTY: boolean,\n): void {\n if (!plan.shouldDelegate) return;\n if (looksLikeJsonMode(argv, env, stdoutIsTTY)) return;\n\n process.stderr.write(\n `ekanos: using this project's pinned CLI ${plan.localVersion} (global: ` +\n `${plan.ownVersion}). Set EKANOS_NO_DELEGATE=1 to bypass.\\n`,\n );\n}\n\n/**\n * Spawn the local CLI's own bin entry with the SAME node executable,\n * `stdio: 'inherit'` so the JSON envelope and banner gating are\n * byte-identical to a direct run, and propagate its exit code exactly.\n * `spawnFn` is injectable so tests never actually fork a process.\n */\nexport function delegate(\n plan: DelegationPlan,\n argv: readonly string[],\n env: NodeJS.ProcessEnv,\n spawnFn: typeof spawn = spawn,\n): Promise<number> {\n return new Promise((resolve) => {\n const child = spawnFn(process.execPath, [plan.localBinPath!, ...argv], {\n stdio: 'inherit',\n env: { ...env, EKANOS_DELEGATED: '1' },\n });\n child.on('error', () => resolve(1));\n child.on('exit', (code) => resolve(code ?? 1));\n });\n}\n"]}
1
+ {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../src/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { cliVersion } from './compatibility';\nimport { readInstalledPackageManifest } from './harness-scaffold';\n\n/**\n * Wrapper-style delegation to the project-local `@ekanos/cli` — the\n * Gradle-wrapper / Yarn / `npx` pattern, applied to this CLI.\n *\n * A global install bundles ONE `@ekanos/integration-schema` version but\n * routinely serves N projects on different `@ekanos/sdk` versions — it\n * cannot be correct for all of them simultaneously (this is exactly the\n * failure mode `schema-skew.ts` detects). Delegation dissolves the tension:\n * `ekanos` stays on PATH for convenience, but the four verbs that actually\n * read or generate project files always run through the project's OWN\n * pinned copy when one is installed and differs from the invoked binary.\n *\n * Machine-level verbs (`init`, `login`, `logout`, `whoami`, `sources`,\n * `use`, `status`, `upgrade`) are deliberately NEVER delegated: you never\n * want an old local copy handling credentials, and `upgrade` must always be\n * the newest logic doing the upgrading — delegating it could have an old CLI\n * \"upgrade\" a project using its own out-of-date resolution.\n */\n\nconst DELEGATABLE_VERBS = new Set(['validate', 'publish', 'dev', 'test']);\n\nexport interface DelegationPlan {\n shouldDelegate: boolean;\n ownVersion: string;\n localVersion?: string;\n /** Absolute path to the local CLI's bin entry, when `shouldDelegate`. */\n localBinPath?: string;\n}\n\n/** The first bare (non-flag) token before a standalone `--` — mirrors `splitCommand` in `index.ts`. */\nfunction firstCommand(argv: readonly string[]): string | null {\n for (const token of argv) {\n if (token === '--') break;\n if (!token.startsWith('-')) return token;\n }\n return null;\n}\n\n/** `--dir <path>` / `--dir=<path>`, before a standalone `--`. */\nfunction scanDirFlag(argv: readonly string[]): string | undefined {\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i]!;\n if (token === '--') break;\n if (token === '--dir') return argv[i + 1];\n if (token.startsWith('--dir=')) return token.slice('--dir='.length);\n }\n return undefined;\n}\n\n/**\n * Decide whether this invocation should delegate. Pure and side-effect-free\n * (beyond the one file read of the local `@ekanos/cli` manifest, mirroring\n * `readInstalledHarness`/`dev`'s own project-package resolution rather than\n * writing a second one) so it is directly testable without spawning anything.\n */\nexport function planDelegation(\n argv: readonly string[],\n env: Record<string, string | undefined>,\n cwd: string,\n): DelegationPlan {\n const ownVersion = cliVersion();\n\n // The recursion guard, and the explicit escape hatch, both win outright.\n if (env.EKANOS_DELEGATED === '1' || env.EKANOS_NO_DELEGATE === '1') {\n return { shouldDelegate: false, ownVersion };\n }\n\n const command = firstCommand(argv);\n if (!command || !DELEGATABLE_VERBS.has(command)) {\n return { shouldDelegate: false, ownVersion };\n }\n\n const projectDir = path.resolve(cwd, scanDirFlag(argv) ?? '.');\n // Best-effort, like every other resolution this function does: a corrupt\n // local manifest must fall through to \"don't delegate\", never throw. This\n // runs in `bin.ts` BEFORE `run()`'s try/catch exists, so an uncaught throw\n // here would bypass the JSON-envelope error machinery entirely and print a\n // raw stderr string instead — a real regression in the \"exactly one\n // terminal emission\" contract the rest of this package guarantees.\n let local: ReturnType<typeof readInstalledPackageManifest> = null;\n try {\n local = readInstalledPackageManifest(projectDir, '@ekanos/cli');\n } catch {\n local = null;\n }\n\n if (!local || local.version === ownVersion) {\n return { shouldDelegate: false, ownVersion, localVersion: local?.version };\n }\n\n return {\n shouldDelegate: true,\n ownVersion,\n localVersion: local.version,\n localBinPath: path.join(\n projectDir,\n 'node_modules',\n '@ekanos',\n 'cli',\n 'dist',\n 'bin.js',\n ),\n };\n}\n\n/**\n * Same JSON-mode inference `CliContext` uses (`context.ts`) — duplicated\n * deliberately rather than shared, because this runs BEFORE a `CliContext`\n * exists (delegation is decided ahead of the whole `run()` machinery). Keep\n * this in sync with `CliContext`'s constructor if that heuristic ever\n * changes: an explicit `--json`/`--no-json` wins, otherwise a non-TTY stdout\n * or `CLAUDECODE=1` means JSON mode.\n */\nfunction looksLikeJsonMode(\n argv: readonly string[],\n env: Record<string, string | undefined>,\n stdoutIsTTY: boolean,\n): boolean {\n for (const token of argv) {\n if (token === '--') break;\n if (token === '--json') return true;\n if (token === '--no-json') return false;\n }\n return !stdoutIsTTY || env.CLAUDECODE === '1';\n}\n\n/**\n * The one-line stderr announcement — human mode only, and NEVER on stdout\n * (a delegated `validate --json` must still emit exactly one JSON object on\n * stdout, from the CHILD, with nothing of ours ahead of it).\n */\nexport function announceDelegation(\n plan: DelegationPlan,\n argv: readonly string[],\n env: Record<string, string | undefined>,\n stdoutIsTTY: boolean,\n): void {\n if (!plan.shouldDelegate) return;\n if (looksLikeJsonMode(argv, env, stdoutIsTTY)) return;\n\n process.stderr.write(\n `ekanos: using this project's pinned CLI ${plan.localVersion} (global: ` +\n `${plan.ownVersion}). Set EKANOS_NO_DELEGATE=1 to bypass.\\n`,\n );\n}\n\n/**\n * Spawn the local CLI's own bin entry with the SAME node executable,\n * `stdio: 'inherit'` so the JSON envelope and banner gating are\n * byte-identical to a direct run, and propagate its exit code exactly.\n * `spawnFn` is injectable so tests never actually fork a process.\n */\nexport function delegate(\n plan: DelegationPlan,\n argv: readonly string[],\n env: NodeJS.ProcessEnv,\n spawnFn: typeof spawn = spawn,\n): Promise<number> {\n return new Promise((resolve) => {\n const child = spawnFn(process.execPath, [plan.localBinPath!, ...argv], {\n stdio: 'inherit',\n env: { ...env, EKANOS_DELEGATED: '1' },\n });\n child.on('error', () => resolve(1));\n child.on('exit', (code) => resolve(code ?? 1));\n });\n}\n"]}
@@ -79,22 +79,33 @@ function asZodLike(value) {
79
79
  }
80
80
  return null;
81
81
  }
82
- /** Peels `ZodEffects` (`.superRefine`/`.refine`), `ZodOptional`, `ZodDefault`, and `ZodNullable` down to the schema they wrap. */
82
+ /** A node's kind in zod 4's lower-case vocabulary, whichever major built it. */
83
+ function kindOf(schema) {
84
+ const { typeName, type } = schema._def;
85
+ return typeName ? typeName.replace(/^Zod/, '').toLowerCase() : (type !== null && type !== void 0 ? type : '');
86
+ }
87
+ const INNER_TYPE_WRAPPERS = new Set([
88
+ 'optional',
89
+ 'default',
90
+ 'nullable',
91
+ 'prefault',
92
+ ]);
93
+ /** Peels refinements/transforms (zod 3 `ZodEffects`, zod 4 `pipe`), `optional`, `default`, and `nullable` down to the schema they wrap. */
83
94
  function unwrap(schema) {
84
95
  let current = schema;
85
96
  for (;;) {
86
- if (current._def.typeName === 'ZodEffects' && current._def.schema) {
87
- current = current._def.schema;
88
- continue;
89
- }
90
- if ((current._def.typeName === 'ZodOptional' ||
91
- current._def.typeName === 'ZodDefault' ||
92
- current._def.typeName === 'ZodNullable') &&
93
- current._def.innerType) {
94
- current = current._def.innerType;
95
- continue;
97
+ const kind = kindOf(current);
98
+ const next = kind === 'effects'
99
+ ? current._def.schema
100
+ : kind === 'pipe'
101
+ ? current._def.in
102
+ : INNER_TYPE_WRAPPERS.has(kind)
103
+ ? current._def.innerType
104
+ : undefined;
105
+ if (!next) {
106
+ return current;
96
107
  }
97
- return current;
108
+ current = next;
98
109
  }
99
110
  }
100
111
  /**
@@ -107,14 +118,14 @@ function unwrap(schema) {
107
118
  function resolveObjectAt(root, segments) {
108
119
  let current = unwrap(root);
109
120
  for (const segment of segments) {
110
- if (current._def.typeName === 'ZodObject' && current.shape) {
121
+ if (kindOf(current) === 'object' && current.shape) {
111
122
  const next = current.shape[segment];
112
123
  if (!next)
113
124
  return null;
114
125
  current = unwrap(next);
115
126
  continue;
116
127
  }
117
- if (current._def.typeName === 'ZodArray' &&
128
+ if (kindOf(current) === 'array' &&
118
129
  current.element &&
119
130
  /^\d+$/.test(segment)) {
120
131
  current = unwrap(current.element);
@@ -122,7 +133,7 @@ function resolveObjectAt(root, segments) {
122
133
  }
123
134
  return null;
124
135
  }
125
- return current._def.typeName === 'ZodObject' ? current : null;
136
+ return kindOf(current) === 'object' ? current : null;
126
137
  }
127
138
  /**
128
139
  * Fallback for the two spots a partner is most likely to typo a key — the
@@ -1 +1 @@
1
- {"version":3,"file":"did-you-mean.js","sourceRoot":"","sources":["../src/did-you-mean.ts"],"names":[],"mappings":"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","sourcesContent":["import { IntegrationDefinitionSchema } from '@ekanos/integration-schema';\n\nimport { isRootPath, splitDotPath } from './source-locator';\n\n/**\n * \"Did you mean?\" for a zod `unrecognized_keys` finding — the typo case\n * (`entrypiont` instead of `entrypoint`). Two pieces: a hand-rolled\n * Levenshtein distance (no new dependency — this package deliberately keeps\n * its runtime dependency set to picocolors + @clack/prompts only) and a\n * structural walk of the REAL `IntegrationDefinitionSchema` to find the\n * sibling keys a given path actually accepts.\n */\n\n/**\n * Classic single-rolling-row DP, O(a.length * b.length). Fine at CLI scale —\n * this compares key names, never paragraphs.\n */\nexport function levenshteinDistance(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n let previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);\n\n for (let i = 1; i <= a.length; i += 1) {\n const currentRow = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n currentRow.push(\n Math.min(\n (previousRow[j] ?? Infinity) + 1, // deletion\n (currentRow[j - 1] ?? Infinity) + 1, // insertion\n (previousRow[j - 1] ?? Infinity) + cost, // substitution\n ),\n );\n }\n previousRow = currentRow;\n }\n\n return previousRow[b.length] ?? Math.max(a.length, b.length);\n}\n\nconst MAX_SUGGESTION_DISTANCE = 2;\n\n/**\n * The single closest candidate within `MAX_SUGGESTION_DISTANCE`, or `null`\n * when nothing is close enough. Never a wrong-confident guess for a key that\n * isn't really a typo of anything this schema accepts.\n */\nexport function suggestKey(\n key: string,\n candidates: readonly string[],\n): string | null {\n let best: string | null = null;\n let bestDistance = MAX_SUGGESTION_DISTANCE + 1;\n\n for (const candidate of candidates) {\n const distance = levenshteinDistance(key, candidate);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = candidate;\n }\n }\n\n return bestDistance <= MAX_SUGGESTION_DISTANCE ? best : null;\n}\n\n/**\n * Parses the literal, comma-separated, single-quoted key list zod's\n * `unrecognized_keys` issue produces (`Unrecognized key(s) in object: 'a',\n * 'b'`) — this exact wording is what `collectDefinitionFindings` bakes into\n * `Finding.message` verbatim (see its doc comment), so this is a text match\n * against a stable, committed third-party format, not a guess at prose.\n * `null` for a message tail that isn't this issue.\n */\nexport function extractUnrecognizedKeys(rest: string): string[] | null {\n const match = /^Unrecognized key\\(s\\) in object: (.+)$/.exec(rest);\n if (!match) return null;\n\n const list = match[1];\n if (!list) return null;\n\n const keys = [...list.matchAll(/'([^']*)'/g)].map((m) => m[1] ?? '');\n return keys.length > 0 ? keys : null;\n}\n\n/**\n * A structural, minimal view of a zod schema instance's internals — `_def`,\n * `.shape` (a `ZodObject`), `.element` (a `ZodArray`) — used ONLY to walk\n * `IntegrationDefinitionSchema` for candidate keys. Deliberately NOT `import\n * { ... } from 'zod'`: this package has no direct `zod` dependency (only\n * `@ekanos/integration-schema` does), the constraint on this feature is no\n * new runtime dependency, and these three properties are the only ones this\n * module reads — stable across zod's v3 line.\n */\ninterface ZodLike {\n _def: {\n typeName: string;\n schema?: ZodLike;\n innerType?: ZodLike;\n };\n shape?: Record<string, ZodLike>;\n element?: ZodLike;\n}\n\nfunction asZodLike(value: unknown): ZodLike | null {\n if (\n typeof value === 'object' &&\n value !== null &&\n '_def' in value &&\n typeof (value as { _def: unknown })._def === 'object' &&\n (value as { _def: unknown })._def !== null\n ) {\n return value as ZodLike;\n }\n return null;\n}\n\n/** Peels `ZodEffects` (`.superRefine`/`.refine`), `ZodOptional`, `ZodDefault`, and `ZodNullable` down to the schema they wrap. */\nfunction unwrap(schema: ZodLike): ZodLike {\n let current = schema;\n\n for (;;) {\n if (current._def.typeName === 'ZodEffects' && current._def.schema) {\n current = current._def.schema;\n continue;\n }\n if (\n (current._def.typeName === 'ZodOptional' ||\n current._def.typeName === 'ZodDefault' ||\n current._def.typeName === 'ZodNullable') &&\n current._def.innerType\n ) {\n current = current._def.innerType;\n continue;\n }\n return current;\n }\n}\n\n/**\n * Walks `root` through `segments` (`.shape` for an object key, `.element`\n * for a numeric array index, unwrapping wrappers at every step), returning\n * the `ZodObject` found at the end — or `null` the moment it hits anything\n * it can't structurally walk (a union, a record, a segment the shape\n * doesn't have). The caller falls back to a curated list in that case.\n */\nfunction resolveObjectAt(\n root: ZodLike,\n segments: readonly string[],\n): ZodLike | null {\n let current = unwrap(root);\n\n for (const segment of segments) {\n if (current._def.typeName === 'ZodObject' && current.shape) {\n const next = current.shape[segment];\n if (!next) return null;\n current = unwrap(next);\n continue;\n }\n if (\n current._def.typeName === 'ZodArray' &&\n current.element &&\n /^\\d+$/.test(segment)\n ) {\n current = unwrap(current.element);\n continue;\n }\n return null;\n }\n\n return current._def.typeName === 'ZodObject' ? current : null;\n}\n\n/**\n * Fallback for the two spots a partner is most likely to typo a key — the\n * top-level declaration and a widget entry — used only when the structural\n * walk above can't resolve the parent path. Keep in sync with\n * `IntegrationDefinitionSchema`'s top-level shape and `widgetSchema` in\n * `@ekanos/integration-schema`'s `integration-definition.ts`; the structural\n * walk is tried first and should cover both of these in practice; this is\n * the safety net for whenever it doesn't.\n */\nconst CURATED_TOP_LEVEL_KEYS = [\n 'slug',\n 'name',\n 'description',\n 'version',\n 'capabilities',\n 'permissions',\n 'components',\n 'workspaceTargets',\n 'tools',\n 'storage',\n 'egress',\n 'webhooks',\n 'schedules',\n 'oauth',\n 'onActivate',\n 'proposes',\n];\n\nconst CURATED_WIDGET_KEYS = [\n 'id',\n 'name',\n 'component',\n 'widgetState',\n 'gridSize',\n 'gridPosition',\n 'layouts',\n 'category',\n 'isCollapsible',\n 'isPinnable',\n 'aiFooterEnabled',\n];\n\nfunction curatedKeysAt(parentDotPath: string): readonly string[] | null {\n if (isRootPath(parentDotPath)) return CURATED_TOP_LEVEL_KEYS;\n if (/^components\\.widgets\\.\\d+$/.test(parentDotPath)) {\n return CURATED_WIDGET_KEYS;\n }\n return null;\n}\n\n/**\n * The keys `IntegrationDefinitionSchema` actually accepts at\n * `parentDotPath` — the zod issue's own `path` for an `unrecognized_keys`\n * finding, so this always names the OBJECT the extra key was found on, never\n * the extra key itself. Structural introspection first, the curated list\n * second, `[]` (no suggestion offered — never a wrong-confident one) last.\n */\nexport function acceptedKeysAt(parentDotPath: string): readonly string[] {\n const root = asZodLike(IntegrationDefinitionSchema);\n const resolved = root\n ? resolveObjectAt(root, splitDotPath(parentDotPath))\n : null;\n\n if (resolved?.shape) return Object.keys(resolved.shape);\n\n return curatedKeysAt(parentDotPath) ?? [];\n}\n"]}
1
+ {"version":3,"file":"did-you-mean.js","sourceRoot":"","sources":["../src/did-you-mean.ts"],"names":[],"mappings":"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","sourcesContent":["import { IntegrationDefinitionSchema } from '@ekanos/integration-schema';\n\nimport { isRootPath, splitDotPath } from './source-locator';\n\n/**\n * \"Did you mean?\" for a zod `unrecognized_keys` finding — the typo case\n * (`entrypiont` instead of `entrypoint`). Two pieces: a hand-rolled\n * Levenshtein distance (no new dependency — this package deliberately keeps\n * its runtime dependency set to picocolors + @clack/prompts only) and a\n * structural walk of the REAL `IntegrationDefinitionSchema` to find the\n * sibling keys a given path actually accepts.\n */\n\n/**\n * Classic single-rolling-row DP, O(a.length * b.length). Fine at CLI scale —\n * this compares key names, never paragraphs.\n */\nexport function levenshteinDistance(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n let previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);\n\n for (let i = 1; i <= a.length; i += 1) {\n const currentRow = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n currentRow.push(\n Math.min(\n (previousRow[j] ?? Infinity) + 1, // deletion\n (currentRow[j - 1] ?? Infinity) + 1, // insertion\n (previousRow[j - 1] ?? Infinity) + cost, // substitution\n ),\n );\n }\n previousRow = currentRow;\n }\n\n return previousRow[b.length] ?? Math.max(a.length, b.length);\n}\n\nconst MAX_SUGGESTION_DISTANCE = 2;\n\n/**\n * The single closest candidate within `MAX_SUGGESTION_DISTANCE`, or `null`\n * when nothing is close enough. Never a wrong-confident guess for a key that\n * isn't really a typo of anything this schema accepts.\n */\nexport function suggestKey(\n key: string,\n candidates: readonly string[],\n): string | null {\n let best: string | null = null;\n let bestDistance = MAX_SUGGESTION_DISTANCE + 1;\n\n for (const candidate of candidates) {\n const distance = levenshteinDistance(key, candidate);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = candidate;\n }\n }\n\n return bestDistance <= MAX_SUGGESTION_DISTANCE ? best : null;\n}\n\n/**\n * Parses the literal, comma-separated, single-quoted key list zod's\n * `unrecognized_keys` issue produces (`Unrecognized key(s) in object: 'a',\n * 'b'`) — this exact wording is what `collectDefinitionFindings` bakes into\n * `Finding.message` verbatim (see its doc comment), so this is a text match\n * against a stable, committed third-party format, not a guess at prose.\n * `null` for a message tail that isn't this issue.\n */\nexport function extractUnrecognizedKeys(rest: string): string[] | null {\n const match = /^Unrecognized key\\(s\\) in object: (.+)$/.exec(rest);\n if (!match) return null;\n\n const list = match[1];\n if (!list) return null;\n\n const keys = [...list.matchAll(/'([^']*)'/g)].map((m) => m[1] ?? '');\n return keys.length > 0 ? keys : null;\n}\n\n/**\n * A structural, minimal view of a zod schema instance's internals — `_def`,\n * `.shape` (a `ZodObject`), `.element` (a `ZodArray`) — used ONLY to walk\n * `IntegrationDefinitionSchema` for candidate keys. Deliberately NOT `import\n * { ... } from 'zod'`: this package has no direct `zod` dependency (only\n * `@ekanos/integration-schema` does), the constraint on this feature is no\n * new runtime dependency, and these few properties are the only ones this\n * module reads. The partner's install decides the zod major (the peer range\n * is `^3.25.76 || ^4.0.0`), so both shapes are read: zod 3 names a node's\n * kind `_def.typeName` (`'ZodObject'`), zod 4 `_def.type` (`'object'`).\n */\ninterface ZodLike {\n _def: {\n typeName?: string;\n type?: string;\n schema?: ZodLike;\n innerType?: ZodLike;\n in?: ZodLike;\n };\n shape?: Record<string, ZodLike>;\n element?: ZodLike;\n}\n\nfunction asZodLike(value: unknown): ZodLike | null {\n if (\n typeof value === 'object' &&\n value !== null &&\n '_def' in value &&\n typeof (value as { _def: unknown })._def === 'object' &&\n (value as { _def: unknown })._def !== null\n ) {\n return value as ZodLike;\n }\n return null;\n}\n\n/** A node's kind in zod 4's lower-case vocabulary, whichever major built it. */\nfunction kindOf(schema: ZodLike): string {\n const { typeName, type } = schema._def;\n\n return typeName ? typeName.replace(/^Zod/, '').toLowerCase() : (type ?? '');\n}\n\nconst INNER_TYPE_WRAPPERS = new Set([\n 'optional',\n 'default',\n 'nullable',\n 'prefault',\n]);\n\n/** Peels refinements/transforms (zod 3 `ZodEffects`, zod 4 `pipe`), `optional`, `default`, and `nullable` down to the schema they wrap. */\nfunction unwrap(schema: ZodLike): ZodLike {\n let current = schema;\n\n for (;;) {\n const kind = kindOf(current);\n const next =\n kind === 'effects'\n ? current._def.schema\n : kind === 'pipe'\n ? current._def.in\n : INNER_TYPE_WRAPPERS.has(kind)\n ? current._def.innerType\n : undefined;\n\n if (!next) {\n return current;\n }\n\n current = next;\n }\n}\n\n/**\n * Walks `root` through `segments` (`.shape` for an object key, `.element`\n * for a numeric array index, unwrapping wrappers at every step), returning\n * the `ZodObject` found at the end — or `null` the moment it hits anything\n * it can't structurally walk (a union, a record, a segment the shape\n * doesn't have). The caller falls back to a curated list in that case.\n */\nfunction resolveObjectAt(\n root: ZodLike,\n segments: readonly string[],\n): ZodLike | null {\n let current = unwrap(root);\n\n for (const segment of segments) {\n if (kindOf(current) === 'object' && current.shape) {\n const next = current.shape[segment];\n if (!next) return null;\n current = unwrap(next);\n continue;\n }\n if (\n kindOf(current) === 'array' &&\n current.element &&\n /^\\d+$/.test(segment)\n ) {\n current = unwrap(current.element);\n continue;\n }\n return null;\n }\n\n return kindOf(current) === 'object' ? current : null;\n}\n\n/**\n * Fallback for the two spots a partner is most likely to typo a key — the\n * top-level declaration and a widget entry — used only when the structural\n * walk above can't resolve the parent path. Keep in sync with\n * `IntegrationDefinitionSchema`'s top-level shape and `widgetSchema` in\n * `@ekanos/integration-schema`'s `integration-definition.ts`; the structural\n * walk is tried first and should cover both of these in practice; this is\n * the safety net for whenever it doesn't.\n */\nconst CURATED_TOP_LEVEL_KEYS = [\n 'slug',\n 'name',\n 'description',\n 'version',\n 'capabilities',\n 'permissions',\n 'components',\n 'workspaceTargets',\n 'tools',\n 'storage',\n 'egress',\n 'webhooks',\n 'schedules',\n 'oauth',\n 'onActivate',\n 'proposes',\n];\n\nconst CURATED_WIDGET_KEYS = [\n 'id',\n 'name',\n 'component',\n 'widgetState',\n 'gridSize',\n 'gridPosition',\n 'layouts',\n 'category',\n 'isCollapsible',\n 'isPinnable',\n 'aiFooterEnabled',\n];\n\nfunction curatedKeysAt(parentDotPath: string): readonly string[] | null {\n if (isRootPath(parentDotPath)) return CURATED_TOP_LEVEL_KEYS;\n if (/^components\\.widgets\\.\\d+$/.test(parentDotPath)) {\n return CURATED_WIDGET_KEYS;\n }\n return null;\n}\n\n/**\n * The keys `IntegrationDefinitionSchema` actually accepts at\n * `parentDotPath` — the zod issue's own `path` for an `unrecognized_keys`\n * finding, so this always names the OBJECT the extra key was found on, never\n * the extra key itself. Structural introspection first, the curated list\n * second, `[]` (no suggestion offered — never a wrong-confident one) last.\n */\nexport function acceptedKeysAt(parentDotPath: string): readonly string[] {\n const root = asZodLike(IntegrationDefinitionSchema);\n const resolved = root\n ? resolveObjectAt(root, splitDotPath(parentDotPath))\n : null;\n\n if (resolved?.shape) return Object.keys(resolved.shape);\n\n return curatedKeysAt(parentDotPath) ?? [];\n}\n"]}
@@ -38,15 +38,15 @@ export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
38
38
  * integer. One code per numeric exit that this taxonomy can emit.
39
39
  */
40
40
  export declare const ERROR_CODES: {
41
- readonly INTERNAL: "internal";
42
- readonly USAGE: "usage";
43
- readonly VALIDATION: "validation";
44
- readonly AUTH_REQUIRED: "auth_required";
45
- readonly FORBIDDEN: "forbidden";
46
- readonly NOT_FOUND: "not_found";
47
- readonly INVALID_STATE: "invalid_state";
48
- readonly NETWORK: "network";
49
- readonly PRECONDITION_FAILED: "precondition_failed";
50
- readonly GATE_FAILED: "gate_failed";
41
+ readonly INTERNAL: 'internal';
42
+ readonly USAGE: 'usage';
43
+ readonly VALIDATION: 'validation';
44
+ readonly AUTH_REQUIRED: 'auth_required';
45
+ readonly FORBIDDEN: 'forbidden';
46
+ readonly NOT_FOUND: 'not_found';
47
+ readonly INVALID_STATE: 'invalid_state';
48
+ readonly NETWORK: 'network';
49
+ readonly PRECONDITION_FAILED: 'precondition_failed';
50
+ readonly GATE_FAILED: 'gate_failed';
51
51
  };
52
52
  export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
@@ -134,7 +134,8 @@ function renderDetailLines(ui, detail) {
134
134
  });
135
135
  }
136
136
  function buildGroupHeader(ui, group, opts) {
137
- var _a, _b;
137
+ var _a;
138
+ var _b;
138
139
  const errorCount = group.findings.filter((f) => f.severity === 'error').length;
139
140
  const warnCount = group.findings.filter((f) => f.severity === 'warn').length;
140
141
  const infoCount = group.findings.filter((f) => f.severity === 'info').length;
@@ -231,9 +232,10 @@ export function formatFindings(ui, findings, options = {}) {
231
232
  group.findings.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
232
233
  }
233
234
  groups.sort((a, b) => {
234
- var _a, _b, _c, _d;
235
- const worstA = SEVERITY_RANK[(_b = (_a = a.findings[0]) === null || _a === void 0 ? void 0 : _a.severity) !== null && _b !== void 0 ? _b : 'info'];
236
- const worstB = SEVERITY_RANK[(_d = (_c = b.findings[0]) === null || _c === void 0 ? void 0 : _c.severity) !== null && _d !== void 0 ? _d : 'info'];
235
+ var _a, _b;
236
+ var _c, _d;
237
+ const worstA = SEVERITY_RANK[(_c = (_a = a.findings[0]) === null || _a === void 0 ? void 0 : _a.severity) !== null && _c !== void 0 ? _c : 'info'];
238
+ const worstB = SEVERITY_RANK[(_d = (_b = b.findings[0]) === null || _b === void 0 ? void 0 : _b.severity) !== null && _d !== void 0 ? _d : 'info'];
237
239
  return worstA - worstB || a.order - b.order;
238
240
  });
239
241
  const blocks = groups.map((group) => {