@ekanos/cli 0.3.2 → 0.3.4
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.
- package/README.md +13 -13
- package/dist/auth/credential-store.d.ts +1 -1
- package/dist/auth/credential-store.js +3 -2
- package/dist/auth/credential-store.js.map +1 -1
- package/dist/auth/device-flow.d.ts +1 -1
- package/dist/auth/device-flow.js +11 -10
- package/dist/auth/device-flow.js.map +1 -1
- package/dist/auth/{fusion-api.d.ts → ekanos-api.d.ts} +9 -9
- package/dist/auth/{fusion-api.js → ekanos-api.js} +28 -28
- package/dist/auth/ekanos-api.js.map +1 -0
- package/dist/auth/session.d.ts +5 -5
- package/dist/auth/session.js +21 -11
- package/dist/auth/session.js.map +1 -1
- package/dist/banner.d.ts +1 -1
- package/dist/banner.js +4 -13
- package/dist/banner.js.map +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -2
- package/dist/commands/dev.js +11 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.js +72 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +28 -17
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +31 -5
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.d.ts +2 -2
- package/dist/commands/publish.js +28 -15
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/sources.js +26 -16
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +56 -45
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.js +15 -5
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/upgrade.js +86 -54
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +11 -3
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/validate.js +21 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/whoami.d.ts +1 -1
- package/dist/commands/whoami.js +17 -7
- package/dist/commands/whoami.js.map +1 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/context.js +10 -3
- package/dist/context.js.map +1 -1
- package/dist/delegate.js.map +1 -1
- package/dist/did-you-mean.js +26 -15
- package/dist/did-you-mean.js.map +1 -1
- package/dist/exit-codes.d.ts +10 -10
- package/dist/findings-renderer.js +6 -4
- package/dist/findings-renderer.js.map +1 -1
- package/dist/generated/fa-free-names.d.ts +1 -1
- package/dist/generated/fa-free-names.js +1 -1
- package/dist/generated/fa-free-names.js.map +1 -1
- package/dist/harness-scaffold.d.ts +1 -1
- package/dist/harness-scaffold.js +4 -2
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/icon-findings.d.ts +1 -1
- package/dist/icon-findings.js +2 -2
- package/dist/icon-findings.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/merge-manifest.js.map +1 -1
- package/dist/pack.js +1 -1
- package/dist/pack.js.map +1 -1
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +7 -84
- package/dist/project.js +3 -3
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +5 -5
- package/dist/publish-api.js +27 -33
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/schema-skew.js.map +1 -1
- package/dist/source-locator.js.map +1 -1
- package/dist/sources-api.d.ts +3 -3
- package/dist/sources-api.js +11 -14
- package/dist/sources-api.js.map +1 -1
- package/dist/templates.js.map +1 -1
- package/dist/test-report.js.map +1 -1
- package/dist/toolchain-api.d.ts +4 -4
- package/dist/toolchain-api.js +1 -1
- package/dist/toolchain-api.js.map +1 -1
- package/dist/toolchain-resolve.js.map +1 -1
- package/dist/toolchain.d.ts +5 -5
- package/dist/ui.d.ts +36 -0
- package/dist/ui.js +79 -10
- package/dist/ui.js.map +1 -1
- package/dist/update-notice.d.ts +1 -1
- package/dist/update-notice.js.map +1 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +15 -20
- package/templates/AGENTS.md.tmpl +13 -8
- package/templates/CLAUDE.md.tmpl +1 -1
- package/templates/claude-skill.md.tmpl +2 -2
- package/templates/harness/next.config.mjs.tmpl +1 -1
- package/templates/package.json.tmpl +6 -5
- package/templates/pnpm-workspace.yaml.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +3 -3
- package/templates/skills/build-integration/references/api-wiring.md +2 -2
- package/templates/skills/build-integration/references/widget-design.md +1 -1
- package/dist/auth/fusion-api.js.map +0 -1
package/dist/safe-fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside the project root ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n '\"ekanos dev\".',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `\"ekanos dev\" only writes real files inside your project. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so the harness shell ` +\n 'cannot be written there.',\n `Move or delete ${current}, then re-run \"ekanos dev\".`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
|
|
1
|
+
{"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside the project root ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n '\"ekanos dev\".',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `\"ekanos dev\" only writes real files inside your project. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so the harness shell ` +\n 'cannot be written there.',\n `Move or delete ${current}, then re-run \"ekanos dev\".`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
|
package/dist/schema-skew.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-skew.js","sourceRoot":"","sources":["../src/schema-skew.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { compareVersions } from './compatibility';\nimport type { CliContext } from './context';\nimport { preconditionError } from './errors';\nimport { readInstalledPackageManifest } from './harness-scaffold';\nimport { templatesDir } from './templates';\n\n/**\n * The schema-skew gate: `validate` and `publish` parse a partner's\n * declaration with the REAL zod schemas from `@ekanos/integration-schema`,\n * pinned to the EXACT version this build of `@ekanos/cli` was built against.\n * A partner project can end up with a NEWER schema installed than the one\n * this CLI validates with — `@ekanos/sdk` (or the project directly) got\n * upgraded, the CLI did not — and that state is not merely \"a little behind\".\n * It is actively WRONG: a field the newer schema added parses here as an\n * `unrecognized_keys` zod issue, and the generic \"host-assigned fields are\n * never partner-authorable\" hint tells the partner to delete code that is\n * completely correct. That happened to a real partner. Failing loudly here\n * beats answering falsely, so this gate runs BEFORE validate or publish do\n * any of their own work — an incompatible pair produces no findings at all,\n * rather than confidently wrong ones.\n *\n * The opposite skew — the project's schema is OLDER than the CLI's — is not\n * dangerous in the same way (the CLI's validation is still correct; the\n * partner just cannot use fields the CLI would recognize yet), so it warns\n * instead of blocking.\n */\n\nexport const INTEGRATION_SCHEMA_PACKAGE = '@ekanos/integration-schema';\nconst SDK_PACKAGE = '@ekanos/sdk';\n\nconst SEMVER_LIKE = /^\\d+\\.\\d+\\.\\d+/;\n\nfunction looksLikeSemver(value: unknown): value is string {\n return typeof value === 'string' && SEMVER_LIKE.test(value);\n}\n\nfunction readJsonFile(filePath: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * The version of `@ekanos/integration-schema` THIS CLI validates with.\n *\n * Two tiers, tried in order:\n *\n * 1. The exact version declared in this CLI's own `package.json`\n * dependencies. In a published build this is a real pinned semver — `pnpm\n * publish` rewrites the monorepo's `workspace:*` protocol to the exact\n * version of the linked package at publish time, which is exactly the\n * \"declare it explicitly\" contract `compatibility.ts` already commits to\n * for the shell contract.\n * 2. Failing that (this repo's own dev checkout, where the dependency is\n * still the literal `workspace:*` string), fall back to whatever\n * `@ekanos/integration-schema` this CLI itself resolves on disk — the\n * package a workspace install actually links next to the CLI.\n *\n * Returns null — never throws — when neither resolves, which degrades the\n * gate to a no-op exactly like \"the project has no node_modules\" does. A\n * partner's validate/publish must never fail because THIS lookup couldn't\n * find an answer.\n */\nexport function bundledSchemaVersion(): string | null {\n const cliRoot = path.join(templatesDir(), '..');\n\n const ownManifest = readJsonFile(path.join(cliRoot, 'package.json'));\n const declared =\n typeof ownManifest === 'object' && ownManifest !== null\n ? ((ownManifest as { dependencies?: Record<string, unknown> })\n .dependencies ?? {})[INTEGRATION_SCHEMA_PACKAGE]\n : undefined;\n if (looksLikeSemver(declared)) return declared;\n\n const nested = readJsonFile(\n path.join(\n cliRoot,\n 'node_modules',\n '@ekanos',\n 'integration-schema',\n 'package.json',\n ),\n );\n const nestedVersion =\n typeof nested === 'object' && nested !== null\n ? (nested as { version?: unknown }).version\n : undefined;\n\n return looksLikeSemver(nestedVersion) ? nestedVersion : null;\n}\n\nexport interface SchemaSkewResult {\n status: 'ok' | 'project-newer' | 'project-older' | 'unresolvable';\n cliSchemaVersion: string | null;\n projectSchemaVersion: string | null;\n}\n\n/**\n * What the PROJECT resolves for `@ekanos/integration-schema`, compared\n * against what this CLI bundles.\n *\n * The project rarely depends on the schema package directly — it is a\n * transitive dependency of `@ekanos/sdk` — so two candidates are considered,\n * per the task: a direct top-level resolution (works when a package manager\n * hoists it, or when a partner added it explicitly) AND the version\n * `@ekanos/sdk` itself declares a dependency on (its package.json is always a\n * direct project dependency, so it always resolves at the top level even when\n * the schema package's own copy does not). The greater of whichever resolves\n * is what \"the project's installed schema\" means here — either could be the\n * copy Node actually loads, and closing the gate on the newer of the two is\n * the safe direction: it only ever asks a partner to upgrade a CLI that is\n * genuinely behind.\n */\nexport function detectSchemaSkew(projectDir: string): SchemaSkewResult {\n const cliSchemaVersion = bundledSchemaVersion();\n if (!cliSchemaVersion) {\n return {\n status: 'unresolvable',\n cliSchemaVersion: null,\n projectSchemaVersion: null,\n };\n }\n\n const direct = readInstalledPackageManifest(\n projectDir,\n INTEGRATION_SCHEMA_PACKAGE,\n );\n const sdk = readInstalledPackageManifest(projectDir, SDK_PACKAGE);\n const sdkDeclaredSchema =\n sdk && typeof sdk.manifest === 'object' && sdk.manifest !== null\n ? ((sdk.manifest as { dependencies?: Record<string, unknown> })\n .dependencies ?? {})[INTEGRATION_SCHEMA_PACKAGE]\n : undefined;\n\n const candidates = [direct?.version, sdkDeclaredSchema].filter(\n looksLikeSemver,\n );\n\n if (candidates.length === 0) {\n return {\n status: 'unresolvable',\n cliSchemaVersion,\n projectSchemaVersion: null,\n };\n }\n\n const projectSchemaVersion = candidates.reduce((max, candidate) =>\n compareVersions(candidate, max) > 0 ? candidate : max,\n );\n\n const cmp = compareVersions(projectSchemaVersion, cliSchemaVersion);\n\n return {\n status: cmp > 0 ? 'project-newer' : cmp < 0 ? 'project-older' : 'ok',\n cliSchemaVersion,\n projectSchemaVersion,\n };\n}\n\n/**\n * The gate itself. Call before `validate`/`publish` do anything else.\n *\n * Throws PRECONDITION_FAILED (never a validation finding — the definition\n * cannot even be trusted to parse correctly in this state) when the project's\n * schema is newer. Warns on stderr and returns normally for every other\n * status, including the two that mean \"nothing to say\" (`ok`, `unresolvable`)\n * so callers can uniformly branch on the returned result to decide whether to\n * annotate their own JSON envelope.\n */\nexport function assertSchemaNotSkewed(\n ctx: CliContext,\n projectDir: string,\n): SchemaSkewResult {\n const result = detectSchemaSkew(projectDir);\n\n if (result.status === 'project-newer') {\n throw preconditionError(\n `The project's ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +\n `is newer than ${result.cliSchemaVersion}, the version this ` +\n `@ekanos/cli build validates with. Findings from \"ekanos validate\" ` +\n 'are UNRELIABLE in this state: a field the newer schema added ' +\n 'parses here as an unrecognized key, and the usual hint for that ' +\n 'tells you to delete code that is actually correct.',\n // NEVER interpolate the schema version into a \"@ekanos/cli@<version>\"\n // install spec — the two version completely independently (this build\n // is cli 0.1.6 validating against schema 0.1.4). \"ekanos upgrade\"\n // resolves the actual coherent set; the manual fallback can only ever\n // be \"latest\", never a computed number.\n 'Upgrade the CLI: run \"ekanos upgrade\" (it resolves a coherent target ' +\n 'version set), or pin it explicitly by hand with ' +\n '\"npm i -D @ekanos/cli@latest\". Then re-run \"ekanos validate\" or ' +\n '\"ekanos publish\".',\n );\n }\n\n if (result.status === 'project-older') {\n ctx.warn(\n `ekanos: the installed ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +\n `is older than ${result.cliSchemaVersion}, the version this CLI ` +\n 'validates with — some fields this CLI recognizes may not be usable ' +\n 'yet. Run \"ekanos upgrade\" to bring the toolchain in sync.',\n );\n }\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"schema-skew.js","sourceRoot":"","sources":["../src/schema-skew.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { compareVersions } from './compatibility';\nimport type { CliContext } from './context';\nimport { preconditionError } from './errors';\nimport { readInstalledPackageManifest } from './harness-scaffold';\nimport { templatesDir } from './templates';\n\n/**\n * The schema-skew gate: `validate` and `publish` parse a partner's\n * declaration with the REAL zod schemas from `@ekanos/integration-schema`,\n * pinned to the EXACT version this build of `@ekanos/cli` was built against.\n * A partner project can end up with a NEWER schema installed than the one\n * this CLI validates with — `@ekanos/sdk` (or the project directly) got\n * upgraded, the CLI did not — and that state is not merely \"a little behind\".\n * It is actively WRONG: a field the newer schema added parses here as an\n * `unrecognized_keys` zod issue, and the generic \"host-assigned fields are\n * never partner-authorable\" hint tells the partner to delete code that is\n * completely correct. That happened to a real partner. Failing loudly here\n * beats answering falsely, so this gate runs BEFORE validate or publish do\n * any of their own work — an incompatible pair produces no findings at all,\n * rather than confidently wrong ones.\n *\n * The opposite skew — the project's schema is OLDER than the CLI's — is not\n * dangerous in the same way (the CLI's validation is still correct; the\n * partner just cannot use fields the CLI would recognize yet), so it warns\n * instead of blocking.\n */\n\nexport const INTEGRATION_SCHEMA_PACKAGE = '@ekanos/integration-schema';\nconst SDK_PACKAGE = '@ekanos/sdk';\n\nconst SEMVER_LIKE = /^\\d+\\.\\d+\\.\\d+/;\n\nfunction looksLikeSemver(value: unknown): value is string {\n return typeof value === 'string' && SEMVER_LIKE.test(value);\n}\n\nfunction readJsonFile(filePath: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * The version of `@ekanos/integration-schema` THIS CLI validates with.\n *\n * Two tiers, tried in order:\n *\n * 1. The exact version declared in this CLI's own `package.json`\n * dependencies. In a published build this is a real pinned semver — `pnpm\n * publish` rewrites the monorepo's `workspace:*` protocol to the exact\n * version of the linked package at publish time, which is exactly the\n * \"declare it explicitly\" contract `compatibility.ts` already commits to\n * for the shell contract.\n * 2. Failing that (this repo's own dev checkout, where the dependency is\n * still the literal `workspace:*` string), fall back to whatever\n * `@ekanos/integration-schema` this CLI itself resolves on disk — the\n * package a workspace install actually links next to the CLI.\n *\n * Returns null — never throws — when neither resolves, which degrades the\n * gate to a no-op exactly like \"the project has no node_modules\" does. A\n * partner's validate/publish must never fail because THIS lookup couldn't\n * find an answer.\n */\nexport function bundledSchemaVersion(): string | null {\n const cliRoot = path.join(templatesDir(), '..');\n\n const ownManifest = readJsonFile(path.join(cliRoot, 'package.json'));\n const declared =\n typeof ownManifest === 'object' && ownManifest !== null\n ? ((ownManifest as { dependencies?: Record<string, unknown> })\n .dependencies ?? {})[INTEGRATION_SCHEMA_PACKAGE]\n : undefined;\n if (looksLikeSemver(declared)) return declared;\n\n const nested = readJsonFile(\n path.join(\n cliRoot,\n 'node_modules',\n '@ekanos',\n 'integration-schema',\n 'package.json',\n ),\n );\n const nestedVersion =\n typeof nested === 'object' && nested !== null\n ? (nested as { version?: unknown }).version\n : undefined;\n\n return looksLikeSemver(nestedVersion) ? nestedVersion : null;\n}\n\nexport interface SchemaSkewResult {\n status: 'ok' | 'project-newer' | 'project-older' | 'unresolvable';\n cliSchemaVersion: string | null;\n projectSchemaVersion: string | null;\n}\n\n/**\n * What the PROJECT resolves for `@ekanos/integration-schema`, compared\n * against what this CLI bundles.\n *\n * The project rarely depends on the schema package directly — it is a\n * transitive dependency of `@ekanos/sdk` — so two candidates are considered,\n * per the task: a direct top-level resolution (works when a package manager\n * hoists it, or when a partner added it explicitly) AND the version\n * `@ekanos/sdk` itself declares a dependency on (its package.json is always a\n * direct project dependency, so it always resolves at the top level even when\n * the schema package's own copy does not). The greater of whichever resolves\n * is what \"the project's installed schema\" means here — either could be the\n * copy Node actually loads, and closing the gate on the newer of the two is\n * the safe direction: it only ever asks a partner to upgrade a CLI that is\n * genuinely behind.\n */\nexport function detectSchemaSkew(projectDir: string): SchemaSkewResult {\n const cliSchemaVersion = bundledSchemaVersion();\n if (!cliSchemaVersion) {\n return {\n status: 'unresolvable',\n cliSchemaVersion: null,\n projectSchemaVersion: null,\n };\n }\n\n const direct = readInstalledPackageManifest(\n projectDir,\n INTEGRATION_SCHEMA_PACKAGE,\n );\n const sdk = readInstalledPackageManifest(projectDir, SDK_PACKAGE);\n const sdkDeclaredSchema =\n sdk && typeof sdk.manifest === 'object' && sdk.manifest !== null\n ? ((sdk.manifest as { dependencies?: Record<string, unknown> })\n .dependencies ?? {})[INTEGRATION_SCHEMA_PACKAGE]\n : undefined;\n\n const candidates = [direct?.version, sdkDeclaredSchema].filter(\n looksLikeSemver,\n );\n\n if (candidates.length === 0) {\n return {\n status: 'unresolvable',\n cliSchemaVersion,\n projectSchemaVersion: null,\n };\n }\n\n const projectSchemaVersion = candidates.reduce((max, candidate) =>\n compareVersions(candidate, max) > 0 ? candidate : max,\n );\n\n const cmp = compareVersions(projectSchemaVersion, cliSchemaVersion);\n\n return {\n status: cmp > 0 ? 'project-newer' : cmp < 0 ? 'project-older' : 'ok',\n cliSchemaVersion,\n projectSchemaVersion,\n };\n}\n\n/**\n * The gate itself. Call before `validate`/`publish` do anything else.\n *\n * Throws PRECONDITION_FAILED (never a validation finding — the definition\n * cannot even be trusted to parse correctly in this state) when the project's\n * schema is newer. Warns on stderr and returns normally for every other\n * status, including the two that mean \"nothing to say\" (`ok`, `unresolvable`)\n * so callers can uniformly branch on the returned result to decide whether to\n * annotate their own JSON envelope.\n */\nexport function assertSchemaNotSkewed(\n ctx: CliContext,\n projectDir: string,\n): SchemaSkewResult {\n const result = detectSchemaSkew(projectDir);\n\n if (result.status === 'project-newer') {\n throw preconditionError(\n `The project's ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +\n `is newer than ${result.cliSchemaVersion}, the version this ` +\n `@ekanos/cli build validates with. Findings from \"ekanos validate\" ` +\n 'are UNRELIABLE in this state: a field the newer schema added ' +\n 'parses here as an unrecognized key, and the usual hint for that ' +\n 'tells you to delete code that is actually correct.',\n // NEVER interpolate the schema version into a \"@ekanos/cli@<version>\"\n // install spec — the two version completely independently (this build\n // is cli 0.1.6 validating against schema 0.1.4). \"ekanos upgrade\"\n // resolves the actual coherent set; the manual fallback can only ever\n // be \"latest\", never a computed number.\n 'Upgrade the CLI: run \"ekanos upgrade\" (it resolves a coherent target ' +\n 'version set), or pin it explicitly by hand with ' +\n '\"npm i -D @ekanos/cli@latest\". Then re-run \"ekanos validate\" or ' +\n '\"ekanos publish\".',\n );\n }\n\n if (result.status === 'project-older') {\n ctx.warn(\n `ekanos: the installed ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +\n `is older than ${result.cliSchemaVersion}, the version this CLI ` +\n 'validates with — some fields this CLI recognizes may not be usable ' +\n 'yet. Run \"ekanos upgrade\" to bring the toolchain in sync.',\n );\n }\n\n return result;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-locator.js","sourceRoot":"","sources":["../src/source-locator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Best-effort zod dot-path → source line resolution for `validate`'s\n * schema findings.\n *\n * `collectDefinitionFindings` (in `@ekanos/integration-schema`) bakes a zod\n * PATH into `Finding.message` (`\"components.widgets.0.id: Required\"`), never\n * a source offset — zod reports a `path`, not a byte offset, and fabricating\n * a line number would be a lie (see the `Finding` doc comment there). This\n * module recovers a plausible line by scanning the partner's declaration\n * source text: no AST, no new dependency — TypeScript's compiler API is a\n * devDep in this workspace, not a runtime one, and the published CLI never\n * ships it, so it is never imported here. Just a brace-tracking scan of the\n * object/array literal passed to `defineIntegration(...)`.\n *\n * The rule that matters more than any mechanic below: a WRONG line number is\n * worse than none. Anything this module can't resolve with confidence\n * returns `{ confidence: 'none' }` — never a guess.\n */\n\nexport type LocateConfidence = 'exact' | 'enclosing' | 'none';\n\nexport type LocateResult =\n | { confidence: 'exact'; line: number }\n | { confidence: 'enclosing'; line: number }\n | { confidence: 'none' };\n\n/**\n * Appended, PLAIN TEXT (no ANSI — this module never touches color; see\n * `ui.ts`'s doc comment on why nothing outside it may), to a finding's\n * message when only the enclosing object/array was found, not the missing\n * key itself. `findings-renderer.ts` recognizes this literal suffix and dims\n * it at render time.\n */\nexport const ENCLOSING_NOTE =\n '(showing the enclosing object — add the missing key here)';\n\n/** The literal path `collectDefinitionFindings` emits for a zod issue whose `path` array is empty. */\nexport function isRootPath(dotPath: string): boolean {\n return dotPath === '(root)';\n}\n\n/**\n * `\"components.widgets[0].id\"` and `\"components.widgets.0.id\"` both split to\n * `['components', 'widgets', '0', 'id']` — `collectDefinitionFindings` only\n * ever emits the dot form (a plain `issue.path.join('.')`), but this accepts\n * either, matching the pattern `findings-renderer.ts`'s dot-path detector\n * already tolerates.\n */\nexport function splitDotPath(dotPath: string): string[] {\n if (isRootPath(dotPath)) return [];\n return dotPath.match(/[^.[\\]]+/g) ?? [];\n}\n\n/**\n * A finding message is `\"<dot-path>: <zod message>\"` (or `(root): …` for an\n * empty path) — see the `Finding` doc comment in `@ekanos/integration-schema`.\n * Splits the two apart; `null` for a message with no such prefix (every\n * finding NOT built from a zod issue, e.g. `definition.plain-data`).\n */\nexport function extractPathFromMessage(\n message: string,\n): { path: string; rest: string } | null {\n const match =\n /^((?:\\(root\\))|[A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+|\\[\\d+\\])*): ([^]+)$/.exec(\n message,\n );\n if (!match) return null;\n\n const [, path, rest] = match;\n if (path === undefined || rest === undefined) return null;\n\n return { path, rest };\n}\n\ninterface Scope {\n /** Index of the opening delimiter (`{` or `[`) in both `source` and the masked text. */\n start: number;\n /** Index of the matching closing delimiter. */\n end: number;\n kind: '{' | '[';\n}\n\n/**\n * Replaces the CONTENTS of every string/template literal and comment with a\n * neutral filler, preserving length, newlines, and every character outside\n * those regions verbatim — so a later brace-depth scan never miscounts a\n * `{`/`}`/`,` that only appears inside a string, while every index computed\n * against the result still points at the same character in the original\n * `source`. Delimiter characters (quotes, `/*`, `//`) are kept as-is; only\n * their insides are masked.\n */\nfunction mask(source: string): string {\n const out = source.split('');\n const n = source.length;\n let i = 0;\n\n while (i < n) {\n const ch = source[i];\n const next = source[i + 1];\n\n if (ch === '/' && next === '/') {\n // Mask the `//` marker itself too, not just what follows it — a bare\n // `/` isn't whitespace, so `skipMaskedWhitespace` needs the WHOLE\n // comment (marker included) to read as blank in order to skip past a\n // comment that precedes a key.\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && source[i] !== '\\n') {\n out[i] = ' ';\n i += 1;\n }\n continue;\n }\n\n if (ch === '/' && next === '*') {\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {\n if (source[i] !== '\\n') out[i] = ' ';\n i += 1;\n }\n if (i < n) out[i] = ' ';\n if (i + 1 < n) out[i + 1] = ' ';\n i += 2;\n continue;\n }\n\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n const quote = ch;\n i += 1;\n while (i < n && source[i] !== quote) {\n if (source[i] === '\\\\') {\n if (source[i + 1] !== '\\n' && i + 1 < n) out[i + 1] = 'x';\n i += 2;\n continue;\n }\n if (source[i] !== '\\n') out[i] = 'x';\n i += 1;\n }\n i += 1;\n continue;\n }\n\n i += 1;\n }\n\n return out.join('');\n}\n\n/** Index of the delimiter matching `masked[openIndex]`, or `-1` for unbalanced/malformed input. Operates on masked text so a bracket inside a string never throws off the count. */\nfunction findMatchingClose(masked: string, openIndex: number): number {\n const stack: string[] = [];\n\n for (let i = openIndex; i < masked.length; i += 1) {\n const ch = masked[i];\n if (ch === '{') stack.push('}');\n else if (ch === '[') stack.push(']');\n else if (ch === '(') stack.push(')');\n else if (ch === '}' || ch === ']' || ch === ')') {\n if (stack.pop() !== ch) return -1;\n if (stack.length === 0) return i;\n }\n }\n\n return -1;\n}\n\n/** Top-level (depth-0-relative-to-the-scope) member spans between a scope's delimiters, split at depth-0 commas — one span per object property or array element, in source order. */\nfunction splitTopLevelMembers(\n masked: string,\n scope: Scope,\n): Array<{ start: number; end: number }> {\n const members: Array<{ start: number; end: number }> = [];\n let depth = 0;\n let memberStart = scope.start + 1;\n\n for (let i = scope.start + 1; i < scope.end; i += 1) {\n const ch = masked[i];\n if (ch === '{' || ch === '[' || ch === '(') depth += 1;\n else if (ch === '}' || ch === ']' || ch === ')') depth -= 1;\n else if (ch === ',' && depth === 0) {\n members.push({ start: memberStart, end: i });\n memberStart = i + 1;\n }\n }\n members.push({ start: memberStart, end: scope.end });\n\n return members.filter((m) => masked.slice(m.start, m.end).trim() !== '');\n}\n\nfunction scopeAt(masked: string, index: number): Scope | null {\n const ch = masked[index];\n if (ch !== '{' && ch !== '[') return null;\n\n const close = findMatchingClose(masked, index);\n if (close === -1) return null;\n\n return { start: index, end: close, kind: ch };\n}\n\nfunction findRootScope(masked: string): Scope | null {\n const match = /defineIntegration\\s*\\(/.exec(masked);\n if (!match) return null;\n\n let i = match.index + match[0].length;\n while (i < masked.length && /\\s/.test(masked[i] ?? '')) i += 1;\n\n return scopeAt(masked, i);\n}\n\n/**\n * A quoted (`'id'`/`\"id\"`) or bare (`id`) object key immediately followed by\n * `:`, anchored at the very start of the string it's matched against — the\n * caller is responsible for having already skipped past any leading\n * whitespace AND leading comment (both of which read as whitespace in the\n * MASKED text, see `skipMaskedWhitespace`). Matched against the RAW source\n * slice, never the masked one — masking a quoted key's own text would\n * destroy the very name being compared.\n */\nconst KEY_PATTERN =\n /^(?:(['\"])((?:\\\\.|(?!\\1)[^\\\\])*)\\1|([A-Za-z_$][\\w$]*))\\s*:/;\n\n/** First index at or after `start` (and before `end`) whose MASKED character is not whitespace — a masked comment reads as whitespace here too, so this skips leading comments along with real whitespace/newlines. */\nfunction skipMaskedWhitespace(\n masked: string,\n start: number,\n end: number,\n): number {\n let i = start;\n while (i < end && /\\s/.test(masked[i] ?? '')) i += 1;\n return i;\n}\n\ntype DescendResult =\n 'not-found' | 'mismatch' | { targetStart: number; valueScope: Scope | null };\n\nfunction descend(\n source: string,\n masked: string,\n scope: Scope,\n segment: string,\n): DescendResult {\n const members = splitTopLevelMembers(masked, scope);\n\n if (scope.kind === '[') {\n if (!/^\\d+$/.test(segment)) return 'mismatch';\n\n const member = members[Number(segment)];\n if (!member) return 'not-found';\n\n const targetStart = skipMaskedWhitespace(masked, member.start, member.end);\n return { targetStart, valueScope: scopeAt(masked, targetStart) };\n }\n\n for (const member of members) {\n const contentStart = skipMaskedWhitespace(masked, member.start, member.end);\n const raw = source.slice(contentStart, member.end);\n const match = KEY_PATTERN.exec(raw);\n if (!match) continue;\n\n const keyName = match[2] ?? match[3];\n if (keyName !== segment) continue;\n\n const valueStart = skipMaskedWhitespace(\n masked,\n contentStart + match[0].length,\n member.end,\n );\n\n return {\n targetStart: contentStart,\n valueScope: scopeAt(masked, valueStart),\n };\n }\n\n return 'not-found';\n}\n\nfunction lineAt(source: string, index: number): number {\n let line = 1;\n for (let i = 0; i < index && i < source.length; i += 1) {\n if (source[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/**\n * Locates `dotPath` (a zod path string, e.g. `\"components.widgets.0.id\"` or\n * `\"(root)\"`) within `source`, the declaration file's full text. `source`\n * MUST contain an inline `defineIntegration({ … })` call — a definition\n * built from a variable, spread, or factory function can't be resolved\n * statically and always comes back `none`. A one-line (minified/no-newline)\n * source also always comes back `none`: a codeframe there can't isolate\n * anything the reader couldn't already see, so it isn't worth the risk of a\n * technically-correct-but-useless \"line 1\" frame.\n */\nexport function locateDotPath(source: string, dotPath: string): LocateResult {\n // Strip trailing newline(s) before checking: a one-line declaration saved\n // with a normal trailing EOF newline must still count as \"one line,\" not\n // \"multi-line\" by virtue of that single trailing `\\n`.\n if (!source.replace(/\\n+$/, '').includes('\\n')) return { confidence: 'none' };\n\n const masked = mask(source);\n const root = findRootScope(masked);\n if (!root) return { confidence: 'none' };\n\n const segments = splitDotPath(dotPath);\n if (segments.length === 0) {\n return { confidence: 'enclosing', line: lineAt(source, root.start) };\n }\n\n let scope: Scope = root;\n\n for (let i = 0; i < segments.length; i += 1) {\n const segment = segments[i];\n if (segment === undefined) break;\n\n const result = descend(source, masked, scope, segment);\n\n if (result === 'mismatch') return { confidence: 'none' };\n if (result === 'not-found') {\n return { confidence: 'enclosing', line: lineAt(source, scope.start) };\n }\n\n const isLast = i === segments.length - 1;\n if (isLast) {\n return { confidence: 'exact', line: lineAt(source, result.targetStart) };\n }\n\n if (result.valueScope === null) return { confidence: 'none' };\n scope = result.valueScope;\n }\n\n return { confidence: 'none' };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"source-locator.js","sourceRoot":"","sources":["../src/source-locator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Best-effort zod dot-path → source line resolution for `validate`'s\n * schema findings.\n *\n * `collectDefinitionFindings` (in `@ekanos/integration-schema`) bakes a zod\n * PATH into `Finding.message` (`\"components.widgets.0.id: Required\"`), never\n * a source offset — zod reports a `path`, not a byte offset, and fabricating\n * a line number would be a lie (see the `Finding` doc comment there). This\n * module recovers a plausible line by scanning the partner's declaration\n * source text: no AST, no new dependency — TypeScript's compiler API is a\n * devDep in this workspace, not a runtime one, and the published CLI never\n * ships it, so it is never imported here. Just a brace-tracking scan of the\n * object/array literal passed to `defineIntegration(...)`.\n *\n * The rule that matters more than any mechanic below: a WRONG line number is\n * worse than none. Anything this module can't resolve with confidence\n * returns `{ confidence: 'none' }` — never a guess.\n */\n\nexport type LocateConfidence = 'exact' | 'enclosing' | 'none';\n\nexport type LocateResult =\n | { confidence: 'exact'; line: number }\n | { confidence: 'enclosing'; line: number }\n | { confidence: 'none' };\n\n/**\n * Appended, PLAIN TEXT (no ANSI — this module never touches color; see\n * `ui.ts`'s doc comment on why nothing outside it may), to a finding's\n * message when only the enclosing object/array was found, not the missing\n * key itself. `findings-renderer.ts` recognizes this literal suffix and dims\n * it at render time.\n */\nexport const ENCLOSING_NOTE =\n '(showing the enclosing object — add the missing key here)';\n\n/** The literal path `collectDefinitionFindings` emits for a zod issue whose `path` array is empty. */\nexport function isRootPath(dotPath: string): boolean {\n return dotPath === '(root)';\n}\n\n/**\n * `\"components.widgets[0].id\"` and `\"components.widgets.0.id\"` both split to\n * `['components', 'widgets', '0', 'id']` — `collectDefinitionFindings` only\n * ever emits the dot form (a plain `issue.path.join('.')`), but this accepts\n * either, matching the pattern `findings-renderer.ts`'s dot-path detector\n * already tolerates.\n */\nexport function splitDotPath(dotPath: string): string[] {\n if (isRootPath(dotPath)) return [];\n return dotPath.match(/[^.[\\]]+/g) ?? [];\n}\n\n/**\n * A finding message is `\"<dot-path>: <zod message>\"` (or `(root): …` for an\n * empty path) — see the `Finding` doc comment in `@ekanos/integration-schema`.\n * Splits the two apart; `null` for a message with no such prefix (every\n * finding NOT built from a zod issue, e.g. `definition.plain-data`).\n */\nexport function extractPathFromMessage(\n message: string,\n): { path: string; rest: string } | null {\n const match =\n /^((?:\\(root\\))|[A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+|\\[\\d+\\])*): ([^]+)$/.exec(\n message,\n );\n if (!match) return null;\n\n const [, path, rest] = match;\n if (path === undefined || rest === undefined) return null;\n\n return { path, rest };\n}\n\ninterface Scope {\n /** Index of the opening delimiter (`{` or `[`) in both `source` and the masked text. */\n start: number;\n /** Index of the matching closing delimiter. */\n end: number;\n kind: '{' | '[';\n}\n\n/**\n * Replaces the CONTENTS of every string/template literal and comment with a\n * neutral filler, preserving length, newlines, and every character outside\n * those regions verbatim — so a later brace-depth scan never miscounts a\n * `{`/`}`/`,` that only appears inside a string, while every index computed\n * against the result still points at the same character in the original\n * `source`. Delimiter characters (quotes, `/*`, `//`) are kept as-is; only\n * their insides are masked.\n */\nfunction mask(source: string): string {\n const out = source.split('');\n const n = source.length;\n let i = 0;\n\n while (i < n) {\n const ch = source[i];\n const next = source[i + 1];\n\n if (ch === '/' && next === '/') {\n // Mask the `//` marker itself too, not just what follows it — a bare\n // `/` isn't whitespace, so `skipMaskedWhitespace` needs the WHOLE\n // comment (marker included) to read as blank in order to skip past a\n // comment that precedes a key.\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && source[i] !== '\\n') {\n out[i] = ' ';\n i += 1;\n }\n continue;\n }\n\n if (ch === '/' && next === '*') {\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {\n if (source[i] !== '\\n') out[i] = ' ';\n i += 1;\n }\n if (i < n) out[i] = ' ';\n if (i + 1 < n) out[i + 1] = ' ';\n i += 2;\n continue;\n }\n\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n const quote = ch;\n i += 1;\n while (i < n && source[i] !== quote) {\n if (source[i] === '\\\\') {\n if (source[i + 1] !== '\\n' && i + 1 < n) out[i + 1] = 'x';\n i += 2;\n continue;\n }\n if (source[i] !== '\\n') out[i] = 'x';\n i += 1;\n }\n i += 1;\n continue;\n }\n\n i += 1;\n }\n\n return out.join('');\n}\n\n/** Index of the delimiter matching `masked[openIndex]`, or `-1` for unbalanced/malformed input. Operates on masked text so a bracket inside a string never throws off the count. */\nfunction findMatchingClose(masked: string, openIndex: number): number {\n const stack: string[] = [];\n\n for (let i = openIndex; i < masked.length; i += 1) {\n const ch = masked[i];\n if (ch === '{') stack.push('}');\n else if (ch === '[') stack.push(']');\n else if (ch === '(') stack.push(')');\n else if (ch === '}' || ch === ']' || ch === ')') {\n if (stack.pop() !== ch) return -1;\n if (stack.length === 0) return i;\n }\n }\n\n return -1;\n}\n\n/** Top-level (depth-0-relative-to-the-scope) member spans between a scope's delimiters, split at depth-0 commas — one span per object property or array element, in source order. */\nfunction splitTopLevelMembers(\n masked: string,\n scope: Scope,\n): Array<{ start: number; end: number }> {\n const members: Array<{ start: number; end: number }> = [];\n let depth = 0;\n let memberStart = scope.start + 1;\n\n for (let i = scope.start + 1; i < scope.end; i += 1) {\n const ch = masked[i];\n if (ch === '{' || ch === '[' || ch === '(') depth += 1;\n else if (ch === '}' || ch === ']' || ch === ')') depth -= 1;\n else if (ch === ',' && depth === 0) {\n members.push({ start: memberStart, end: i });\n memberStart = i + 1;\n }\n }\n members.push({ start: memberStart, end: scope.end });\n\n return members.filter((m) => masked.slice(m.start, m.end).trim() !== '');\n}\n\nfunction scopeAt(masked: string, index: number): Scope | null {\n const ch = masked[index];\n if (ch !== '{' && ch !== '[') return null;\n\n const close = findMatchingClose(masked, index);\n if (close === -1) return null;\n\n return { start: index, end: close, kind: ch };\n}\n\nfunction findRootScope(masked: string): Scope | null {\n const match = /defineIntegration\\s*\\(/.exec(masked);\n if (!match) return null;\n\n let i = match.index + match[0].length;\n while (i < masked.length && /\\s/.test(masked[i] ?? '')) i += 1;\n\n return scopeAt(masked, i);\n}\n\n/**\n * A quoted (`'id'`/`\"id\"`) or bare (`id`) object key immediately followed by\n * `:`, anchored at the very start of the string it's matched against — the\n * caller is responsible for having already skipped past any leading\n * whitespace AND leading comment (both of which read as whitespace in the\n * MASKED text, see `skipMaskedWhitespace`). Matched against the RAW source\n * slice, never the masked one — masking a quoted key's own text would\n * destroy the very name being compared.\n */\nconst KEY_PATTERN =\n /^(?:(['\"])((?:\\\\.|(?!\\1)[^\\\\])*)\\1|([A-Za-z_$][\\w$]*))\\s*:/;\n\n/** First index at or after `start` (and before `end`) whose MASKED character is not whitespace — a masked comment reads as whitespace here too, so this skips leading comments along with real whitespace/newlines. */\nfunction skipMaskedWhitespace(\n masked: string,\n start: number,\n end: number,\n): number {\n let i = start;\n while (i < end && /\\s/.test(masked[i] ?? '')) i += 1;\n return i;\n}\n\ntype DescendResult =\n | 'not-found'\n | 'mismatch'\n | { targetStart: number; valueScope: Scope | null };\n\nfunction descend(\n source: string,\n masked: string,\n scope: Scope,\n segment: string,\n): DescendResult {\n const members = splitTopLevelMembers(masked, scope);\n\n if (scope.kind === '[') {\n if (!/^\\d+$/.test(segment)) return 'mismatch';\n\n const member = members[Number(segment)];\n if (!member) return 'not-found';\n\n const targetStart = skipMaskedWhitespace(masked, member.start, member.end);\n return { targetStart, valueScope: scopeAt(masked, targetStart) };\n }\n\n for (const member of members) {\n const contentStart = skipMaskedWhitespace(masked, member.start, member.end);\n const raw = source.slice(contentStart, member.end);\n const match = KEY_PATTERN.exec(raw);\n if (!match) continue;\n\n const keyName = match[2] ?? match[3];\n if (keyName !== segment) continue;\n\n const valueStart = skipMaskedWhitespace(\n masked,\n contentStart + match[0].length,\n member.end,\n );\n\n return {\n targetStart: contentStart,\n valueScope: scopeAt(masked, valueStart),\n };\n }\n\n return 'not-found';\n}\n\nfunction lineAt(source: string, index: number): number {\n let line = 1;\n for (let i = 0; i < index && i < source.length; i += 1) {\n if (source[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/**\n * Locates `dotPath` (a zod path string, e.g. `\"components.widgets.0.id\"` or\n * `\"(root)\"`) within `source`, the declaration file's full text. `source`\n * MUST contain an inline `defineIntegration({ … })` call — a definition\n * built from a variable, spread, or factory function can't be resolved\n * statically and always comes back `none`. A one-line (minified/no-newline)\n * source also always comes back `none`: a codeframe there can't isolate\n * anything the reader couldn't already see, so it isn't worth the risk of a\n * technically-correct-but-useless \"line 1\" frame.\n */\nexport function locateDotPath(source: string, dotPath: string): LocateResult {\n // Strip trailing newline(s) before checking: a one-line declaration saved\n // with a normal trailing EOF newline must still count as \"one line,\" not\n // \"multi-line\" by virtue of that single trailing `\\n`.\n if (!source.replace(/\\n+$/, '').includes('\\n')) return { confidence: 'none' };\n\n const masked = mask(source);\n const root = findRootScope(masked);\n if (!root) return { confidence: 'none' };\n\n const segments = splitDotPath(dotPath);\n if (segments.length === 0) {\n return { confidence: 'enclosing', line: lineAt(source, root.start) };\n }\n\n let scope: Scope = root;\n\n for (let i = 0; i < segments.length; i += 1) {\n const segment = segments[i];\n if (segment === undefined) break;\n\n const result = descend(source, masked, scope, segment);\n\n if (result === 'mismatch') return { confidence: 'none' };\n if (result === 'not-found') {\n return { confidence: 'enclosing', line: lineAt(source, scope.start) };\n }\n\n const isLast = i === segments.length - 1;\n if (isLast) {\n return { confidence: 'exact', line: lineAt(source, result.targetStart) };\n }\n\n if (result.valueScope === null) return { confidence: 'none' };\n scope = result.valueScope;\n }\n\n return { confidence: 'none' };\n}\n"]}
|
package/dist/sources-api.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The CLI's client for the
|
|
2
|
+
* The CLI's client for the Ekanos source-seat visibility route.
|
|
3
3
|
*
|
|
4
4
|
* ---------------------------------------------------------------------------
|
|
5
|
-
* WIRE CONTRACT — the seam between this package and the
|
|
5
|
+
* WIRE CONTRACT — the seam between this package and the Ekanos web app.
|
|
6
6
|
*
|
|
7
7
|
* The server side is the host's `/api/partner/sources` route. As with the
|
|
8
8
|
* auth routes and `publish-api.ts`, the two halves are coupled only by
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* refresh once and retry
|
|
17
17
|
* → 403 { error, code: "MFA_REQUIRED" }
|
|
18
18
|
* → 3xx treated as 401 (an
|
|
19
|
-
* auth-gated
|
|
19
|
+
* auth-gated Ekanos page
|
|
20
20
|
* answers 307)
|
|
21
21
|
* ---------------------------------------------------------------------------
|
|
22
22
|
*/
|
package/dist/sources-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { request } from './auth/
|
|
1
|
+
import { request } from './auth/ekanos-api.js';
|
|
2
2
|
import { authRequiredError, networkError } from './errors.js';
|
|
3
3
|
/**
|
|
4
4
|
* List the sources the caller holds a seat on (any role). Authorization is
|
|
@@ -23,26 +23,23 @@ export async function fetchSourceSeats(host, accessToken) {
|
|
|
23
23
|
}
|
|
24
24
|
if (!response.ok) {
|
|
25
25
|
throw networkError(`${host}/api/partner/sources responded ${response.status}.`, response.status >= 500
|
|
26
|
-
? `The
|
|
26
|
+
? `The Ekanos deployment returned a server error. Retry shortly; if ` +
|
|
27
27
|
`it persists, report it with the status code.`
|
|
28
|
-
: `Confirm the host is
|
|
28
|
+
: `Confirm the host is an Ekanos deployment running a build that ` +
|
|
29
29
|
`includes the partner sources route.`);
|
|
30
30
|
}
|
|
31
31
|
const data = ((_b = body.data) !== null && _b !== void 0 ? _b : {});
|
|
32
32
|
const rows = Array.isArray(data.sources) ? data.sources : [];
|
|
33
33
|
const sources = rows
|
|
34
34
|
.filter((row) => typeof row === 'object' && row !== null)
|
|
35
|
-
.map((row) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
: [],
|
|
44
|
-
});
|
|
45
|
-
});
|
|
35
|
+
.map((row) => { var _a, _b; return ({
|
|
36
|
+
id: (_a = str(row.id)) !== null && _a !== void 0 ? _a : '',
|
|
37
|
+
slug: (_b = str(row.slug)) !== null && _b !== void 0 ? _b : '',
|
|
38
|
+
name: str(row.name),
|
|
39
|
+
roles: Array.isArray(row.roles)
|
|
40
|
+
? row.roles.filter((role) => typeof role === 'string')
|
|
41
|
+
: [],
|
|
42
|
+
}); });
|
|
46
43
|
return { status: 'ok', sources };
|
|
47
44
|
}
|
|
48
45
|
/** Guard for the "still unauthorized after one refresh" terminal case. */
|
package/dist/sources-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAqC3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAY,EACZ,WAAmB;;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAE9C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/ekanos-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Ekanos source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/partner/sources` route. As with the\n * auth routes and `publish-api.ts`, the two halves are coupled only by\n * HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"developer\" | \"source_admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Ekanos page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the source seat lookup.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
package/dist/templates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,cAAc,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAc,EACd,IAAsC;IAEtC,IAAI,QAAQ,GAAG,MAAM,CAAC;IACtB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtD,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,yBAAyB,QAAQ,CAAC,CAAC,CAAC,mCAAmC;YACrE,kEAAkE,CACrE,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,YAAoB;IACrD,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;AAC1E,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,YAAY,CAC1B,YAAoB,EACpB,IAAsC;IAEtC,OAAO,cAAc,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,CAAC;AAChE,CAAC;AAED,+BAA+B;AAC/B,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI;SACR,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SACpE,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * The bundled `templates/` directory, resolved relative to the compiled module\n * so it works from `dist/` and from `src/` alike. `package.json#files` ships\n * `templates` next to `dist`, so the `../..` hop is the same either way.\n */\nexport function templatesDir(): string {\n return fileURLToPath(new URL('../templates', import.meta.url));\n}\n\n/**\n * The one substitution mechanism the CLI has: `__TOKEN__` placeholders,\n * replaced literally. Deliberately not a template engine — a scaffolded file\n * must be readable as-is in the repo, and every placeholder must be greppable.\n * An unknown `__TOKEN__` left in a template is a bug, so this throws rather\n * than shipping a placeholder into a partner's project.\n */\nexport function renderTemplate(\n source: string,\n vars: Readonly<Record<string, string>>,\n): string {\n let rendered = source;\n for (const [name, value] of Object.entries(vars)) {\n rendered = rendered.split(`__${name}__`).join(value);\n }\n\n const leftover = /__[A-Z][A-Z0-9_]*__/.exec(rendered);\n if (leftover) {\n throw new Error(\n `Template placeholder \"${leftover[0]}\" was not substituted. This is a ` +\n 'bug in @ekanos/cli — the template and its variable set disagree.',\n );\n }\n\n return rendered;\n}\n\n/**\n * The raw, unrendered source of a bundled template. Used for the compatibility\n * fingerprint, which must not vary with the slug it would be rendered for.\n */\nexport function readTemplateSource(relativePath: string): string {\n return fs.readFileSync(path.join(templatesDir(), relativePath), 'utf8');\n}\n\n/** Read a bundled template and render it in one step. */\nexport function readTemplate(\n relativePath: string,\n vars: Readonly<Record<string, string>>,\n): string {\n return renderTemplate(readTemplateSource(relativePath), vars);\n}\n\n/** \"acme-crm\" → \"Acme Crm\". */\nexport function toDisplayName(slug: string): string {\n return slug\n .split('-')\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join(' ');\n}\n"]}
|
package/dist/test-report.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-report.js","sourceRoot":"","sources":["../src/test-report.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport type { RenderableFinding } from './findings-renderer';\n\n/**\n * Parses vitest's `--reporter=json` output into the curated-failures\n * appendix `ekanos test` renders locally, giving a partner the same\n * per-test intelligence the server-side gate already has.\n *\n * This is a DELIBERATE, small duplication of the parsing the host's own\n * gate uses to grade a submission's test run — not an import. `@ekanos/\n * checks` is never published and never a dependency of a partner's project\n * or of this CLI; keep the two in sync by eye if vitest's JSON reporter\n * shape ever changes.\n *\n * Every entry point here is defensive end to end: a missing report file,\n * malformed JSON, or an unrecognized shape all resolve to `null` rather than\n * throwing. A partner's toolchain misbehaving must never crash `ekanos\n * test` — it can only mean the curated appendix is unavailable, with the\n * command falling back to exactly today's raw-passthrough behavior.\n */\n\n/** Cap on assertion-diff lines shown per finding — see `extractDiff`. */\nconst MAX_DIFF_LINES = 10;\n\nexport interface TestSummary {\n passed: number;\n failed: number;\n files: number;\n}\n\nexport interface ParsedTestReport {\n /** One finding per failing test, grouped (via `check`) per test FILE. */\n findings: RenderableFinding[];\n /** Relative paths of files with at least one failure, first-seen order. */\n failingFiles: string[];\n summary: TestSummary | null;\n}\n\ninterface ParsedFailure {\n /** Absolute path, exactly as vitest reported it. */\n file: string;\n suite: string;\n title: string;\n message: string;\n line?: number;\n detail?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Whether the project's OWN `test` script names vitest — the one signal\n * used to decide whether to inject the JSON reporter at all. Deliberately\n * conservative: a jest project, a custom node runner, or a shell wrapper is\n * left completely alone, with no reporter flags appended and no report\n * parsed — `ekanos test` behaves exactly as it did before this feature.\n */\nexport function projectTestScriptUsesVitest(projectDir: string): boolean {\n const manifestPath = path.join(projectDir, 'package.json');\n\n try {\n const manifest: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n if (!isRecord(manifest) || !isRecord(manifest.scripts)) return false;\n\n const script = manifest.scripts.test;\n return typeof script === 'string' && script.includes('vitest');\n } catch {\n return false;\n }\n}\n\n/** A fresh, unique path for the JSON report — one per invocation. The caller owns cleanup. */\nexport function newReportPath(): string {\n return path.join(\n os.tmpdir(),\n `ekanos-test-vitest-${crypto.randomUUID()}.json`,\n );\n}\n\n/** The extra argv that makes vitest ALSO emit a JSON report alongside its normal terminal output. */\nexport function reporterArgs(outputFilePath: string): string[] {\n return [\n '--reporter=default',\n '--reporter=json',\n `--outputFile=${outputFilePath}`,\n ];\n}\n\n/** The first non-empty line of a vitest failure message/stack — the\n * assertion or thrown error itself, not the frames beneath it. */\nfunction firstMeaningfulLine(text: string): string {\n const line = text\n .split('\\n')\n .map((entry) => entry.trim())\n .find((entry) => entry.length > 0);\n return line ?? 'Test failed with no error message.';\n}\n\n/**\n * Vitest's failure stacks always include the absolute test file path at\n * least once — at the failure site itself — with the frames beneath it\n * being the runner's own internals. The FIRST occurrence is the line that\n * matters.\n */\nfunction lineForFile(stack: string, absoluteFile: string): number | undefined {\n const escaped = absoluteFile.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const match = new RegExp(`${escaped}:(\\\\d+):\\\\d+`).exec(stack);\n return match ? Number(match[1]) : undefined;\n}\n\n/**\n * Up to `MAX_DIFF_LINES` lines of an expected/received diff block from one\n * vitest failure message, or `null` when the message doesn't carry one.\n *\n * NOTE (found via manual end-to-end verification, not a hypothetical):\n * vitest's OWN `toBe`/`toEqual`-family matchers never put this in the JSON\n * reporter's `failureMessages` — the pretty \"- Expected / + Received\" block\n * is a terminal-only rendering vitest's default reporter computes live from\n * the error's `expected`/`actual`, and is never serialized into the report\n * (checked vitest 3.2.7 and 4.1.10; `message`/`stack` is all that ships).\n * This function still earns its place: a thrown `AssertionError` whose\n * `.message` already embeds diff-formatted text — a custom matcher, a\n * different assertion library layered on top of vitest, a hand-built\n * `expect.extend` — DOES carry it verbatim in `failureMessages`, and this is\n * what picks it up. It is intentionally a no-op for the common case rather\n * than reconstructing a diff ourselves (a real diff needs the actual/expected\n * values, which the JSON reporter doesn't expose, and computing one would\n * mean a new dependency, and this package deliberately keeps its runtime\n * dependency set to picocolors + @clack/prompts only).\n *\n * The diff sits between the failure's own title line and the stack frames\n * (`at ...`) beneath it; within that span, only unified-diff-style `-`/`+`\n * lines are kept — the jest-diff format such a message would use.\n */\nfunction extractDiff(failureMessage: string): string | null {\n const lines = failureMessage\n .split('\\n')\n .map((line) => line.replace(/\\r$/, ''));\n const stackStart = lines.findIndex((line) => /^\\s*at\\s/.test(line));\n const body = stackStart === -1 ? lines.slice(1) : lines.slice(1, stackStart);\n\n const diffLines = body.filter(\n (line) =>\n /^\\s*[-+]/.test(line) &&\n !line.startsWith('---') &&\n !line.startsWith('+++'),\n );\n if (diffLines.length === 0) return null;\n\n const capped = diffLines.slice(0, MAX_DIFF_LINES);\n const overflow = diffLines.length - capped.length;\n if (overflow > 0) {\n capped.push(`… and ${overflow} more diff line${overflow === 1 ? '' : 's'}`);\n }\n return capped.join('\\n');\n}\n\n/** `path.relative` against the project root, falling back to the raw\n * absolute path if the two disagree on realpath-vs-given-path (a symlinked\n * temp dir resolving differently between this process and the child)\n * rather than emitting a `../../../`-laden mess. */\nfunction archiveRelative(projectDir: string, absoluteFile: string): string {\n const direct = path.relative(projectDir, absoluteFile);\n if (!direct.startsWith('..')) return direct;\n try {\n const realProjectDir = fs.realpathSync(projectDir);\n const viaRealpath = path.relative(realProjectDir, absoluteFile);\n if (!viaRealpath.startsWith('..')) return viaRealpath;\n } catch {\n // realpathSync failing (e.g. the directory was already torn down) just\n // means we fall through to the direct (if imperfect) relative path.\n }\n return direct;\n}\n\n/**\n * Turns a parsed `--reporter=json` report into one `ParsedFailure` per\n * failed test, across every test file in the run. Returns `null` — not\n * `[]` — for anything that isn't recognizably a vitest JSON report at all,\n * so the caller can tell \"a real report with zero structured failures\"\n * apart from \"not a report\" (the latter falls back to the raw runner\n * output; the former is a genuinely clean, if unexpected, run).\n */\nfunction extractFailures(report: unknown): ParsedFailure[] | null {\n if (!isRecord(report) || !Array.isArray(report.testResults)) return null;\n\n const failures: ParsedFailure[] = [];\n for (const fileResult of report.testResults as unknown[]) {\n if (!isRecord(fileResult)) continue;\n const file = fileResult.name;\n const assertionResults = fileResult.assertionResults;\n if (typeof file !== 'string' || !Array.isArray(assertionResults)) continue;\n\n for (const assertion of assertionResults as unknown[]) {\n if (!isRecord(assertion) || assertion.status !== 'failed') continue;\n\n const ancestorTitles = Array.isArray(assertion.ancestorTitles)\n ? assertion.ancestorTitles.filter(\n (entry): entry is string => typeof entry === 'string',\n )\n : [];\n const title =\n typeof assertion.title === 'string'\n ? assertion.title\n : '(unnamed test)';\n const failureMessages = Array.isArray(assertion.failureMessages)\n ? assertion.failureMessages.filter(\n (entry): entry is string => typeof entry === 'string',\n )\n : [];\n const firstError = failureMessages[0] ?? '';\n\n const failure: ParsedFailure = {\n file,\n suite: ancestorTitles.join(' > '),\n title,\n message: firstMeaningfulLine(firstError),\n };\n const line = lineForFile(firstError, file);\n if (line !== undefined) failure.line = line;\n const detail = extractDiff(firstError);\n if (detail !== null) failure.detail = detail;\n\n failures.push(failure);\n }\n }\n return failures;\n}\n\n/** `null` unless the report carries vitest's own aggregate pass/fail counts. */\nfunction extractSummary(report: unknown): TestSummary | null {\n if (!isRecord(report) || !Array.isArray(report.testResults)) return null;\n\n const passed = report.numPassedTests;\n const failed = report.numFailedTests;\n if (typeof passed !== 'number' || typeof failed !== 'number') return null;\n\n return { passed, failed, files: report.testResults.length };\n}\n\n/**\n * Reads and parses `reportPath` end to end. `null` on ANY failure (missing\n * file — vitest crashed before writing it; malformed JSON; a shape this\n * function doesn't recognize) so the caller falls back to the raw runner\n * output instead of the curated appendix, and never to a crash.\n */\nexport function parseTestReport(\n reportPath: string,\n projectDir: string,\n): ParsedTestReport | null {\n let raw: string;\n try {\n raw = fs.readFileSync(reportPath, 'utf8');\n } catch {\n return null;\n }\n\n let report: unknown;\n try {\n report = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const failures = extractFailures(report);\n if (failures === null) return null;\n\n const findings: RenderableFinding[] = [];\n const failingFiles: string[] = [];\n const seenFiles = new Set<string>();\n\n for (const failure of failures) {\n const relFile = archiveRelative(projectDir, failure.file);\n if (!seenFiles.has(relFile)) {\n seenFiles.add(relFile);\n failingFiles.push(relFile);\n }\n\n const label = failure.suite\n ? `${failure.suite} > ${failure.title}`\n : failure.title;\n const finding: RenderableFinding = {\n // Grouping by test FILE (not by suite/title) is deliberate — the\n // shared renderer groups by `check`, so this makes the group header\n // show the file once with every failing test beneath it.\n check: `tests/${relFile}`,\n severity: 'error',\n file: relFile,\n message: `${label}: ${failure.message}`,\n hint: 'Fix the failing assertion, then re-run \"ekanos test\".',\n };\n if (failure.line !== undefined) finding.line = failure.line;\n if (failure.detail !== undefined) finding.detail = failure.detail;\n findings.push(finding);\n }\n\n return { findings, failingFiles, summary: extractSummary(report) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"test-report.js","sourceRoot":"","sources":["../src/test-report.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport type { RenderableFinding } from './findings-renderer';\n\n/**\n * Parses vitest's `--reporter=json` output into the curated-failures\n * appendix `ekanos test` renders locally, giving a partner the same\n * per-test intelligence the server-side gate already has.\n *\n * This is a DELIBERATE, small duplication of the parsing the host's own\n * gate uses to grade a submission's test run — not an import. `@ekanos/\n * checks` is never published and never a dependency of a partner's project\n * or of this CLI; keep the two in sync by eye if vitest's JSON reporter\n * shape ever changes.\n *\n * Every entry point here is defensive end to end: a missing report file,\n * malformed JSON, or an unrecognized shape all resolve to `null` rather than\n * throwing. A partner's toolchain misbehaving must never crash `ekanos\n * test` — it can only mean the curated appendix is unavailable, with the\n * command falling back to exactly today's raw-passthrough behavior.\n */\n\n/** Cap on assertion-diff lines shown per finding — see `extractDiff`. */\nconst MAX_DIFF_LINES = 10;\n\nexport interface TestSummary {\n passed: number;\n failed: number;\n files: number;\n}\n\nexport interface ParsedTestReport {\n /** One finding per failing test, grouped (via `check`) per test FILE. */\n findings: RenderableFinding[];\n /** Relative paths of files with at least one failure, first-seen order. */\n failingFiles: string[];\n summary: TestSummary | null;\n}\n\ninterface ParsedFailure {\n /** Absolute path, exactly as vitest reported it. */\n file: string;\n suite: string;\n title: string;\n message: string;\n line?: number;\n detail?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Whether the project's OWN `test` script names vitest — the one signal\n * used to decide whether to inject the JSON reporter at all. Deliberately\n * conservative: a jest project, a custom node runner, or a shell wrapper is\n * left completely alone, with no reporter flags appended and no report\n * parsed — `ekanos test` behaves exactly as it did before this feature.\n */\nexport function projectTestScriptUsesVitest(projectDir: string): boolean {\n const manifestPath = path.join(projectDir, 'package.json');\n\n try {\n const manifest: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n if (!isRecord(manifest) || !isRecord(manifest.scripts)) return false;\n\n const script = manifest.scripts.test;\n return typeof script === 'string' && script.includes('vitest');\n } catch {\n return false;\n }\n}\n\n/** A fresh, unique path for the JSON report — one per invocation. The caller owns cleanup. */\nexport function newReportPath(): string {\n return path.join(\n os.tmpdir(),\n `ekanos-test-vitest-${crypto.randomUUID()}.json`,\n );\n}\n\n/** The extra argv that makes vitest ALSO emit a JSON report alongside its normal terminal output. */\nexport function reporterArgs(outputFilePath: string): string[] {\n return [\n '--reporter=default',\n '--reporter=json',\n `--outputFile=${outputFilePath}`,\n ];\n}\n\n/** The first non-empty line of a vitest failure message/stack — the\n * assertion or thrown error itself, not the frames beneath it. */\nfunction firstMeaningfulLine(text: string): string {\n const line = text\n .split('\\n')\n .map((entry) => entry.trim())\n .find((entry) => entry.length > 0);\n return line ?? 'Test failed with no error message.';\n}\n\n/**\n * Vitest's failure stacks always include the absolute test file path at\n * least once — at the failure site itself — with the frames beneath it\n * being the runner's own internals. The FIRST occurrence is the line that\n * matters.\n */\nfunction lineForFile(stack: string, absoluteFile: string): number | undefined {\n const escaped = absoluteFile.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const match = new RegExp(`${escaped}:(\\\\d+):\\\\d+`).exec(stack);\n return match ? Number(match[1]) : undefined;\n}\n\n/**\n * Up to `MAX_DIFF_LINES` lines of an expected/received diff block from one\n * vitest failure message, or `null` when the message doesn't carry one.\n *\n * NOTE (found via manual end-to-end verification, not a hypothetical):\n * vitest's OWN `toBe`/`toEqual`-family matchers never put this in the JSON\n * reporter's `failureMessages` — the pretty \"- Expected / + Received\" block\n * is a terminal-only rendering vitest's default reporter computes live from\n * the error's `expected`/`actual`, and is never serialized into the report\n * (checked vitest 3.2.7 and 4.1.10; `message`/`stack` is all that ships).\n * This function still earns its place: a thrown `AssertionError` whose\n * `.message` already embeds diff-formatted text — a custom matcher, a\n * different assertion library layered on top of vitest, a hand-built\n * `expect.extend` — DOES carry it verbatim in `failureMessages`, and this is\n * what picks it up. It is intentionally a no-op for the common case rather\n * than reconstructing a diff ourselves (a real diff needs the actual/expected\n * values, which the JSON reporter doesn't expose, and computing one would\n * mean a new dependency, and this package deliberately keeps its runtime\n * dependency set to picocolors + @clack/prompts only).\n *\n * The diff sits between the failure's own title line and the stack frames\n * (`at ...`) beneath it; within that span, only unified-diff-style `-`/`+`\n * lines are kept — the jest-diff format such a message would use.\n */\nfunction extractDiff(failureMessage: string): string | null {\n const lines = failureMessage\n .split('\\n')\n .map((line) => line.replace(/\\r$/, ''));\n const stackStart = lines.findIndex((line) => /^\\s*at\\s/.test(line));\n const body = stackStart === -1 ? lines.slice(1) : lines.slice(1, stackStart);\n\n const diffLines = body.filter(\n (line) =>\n /^\\s*[-+]/.test(line) &&\n !line.startsWith('---') &&\n !line.startsWith('+++'),\n );\n if (diffLines.length === 0) return null;\n\n const capped = diffLines.slice(0, MAX_DIFF_LINES);\n const overflow = diffLines.length - capped.length;\n if (overflow > 0) {\n capped.push(`… and ${overflow} more diff line${overflow === 1 ? '' : 's'}`);\n }\n return capped.join('\\n');\n}\n\n/** `path.relative` against the project root, falling back to the raw\n * absolute path if the two disagree on realpath-vs-given-path (a symlinked\n * temp dir resolving differently between this process and the child)\n * rather than emitting a `../../../`-laden mess. */\nfunction archiveRelative(projectDir: string, absoluteFile: string): string {\n const direct = path.relative(projectDir, absoluteFile);\n if (!direct.startsWith('..')) return direct;\n try {\n const realProjectDir = fs.realpathSync(projectDir);\n const viaRealpath = path.relative(realProjectDir, absoluteFile);\n if (!viaRealpath.startsWith('..')) return viaRealpath;\n } catch {\n // realpathSync failing (e.g. the directory was already torn down) just\n // means we fall through to the direct (if imperfect) relative path.\n }\n return direct;\n}\n\n/**\n * Turns a parsed `--reporter=json` report into one `ParsedFailure` per\n * failed test, across every test file in the run. Returns `null` — not\n * `[]` — for anything that isn't recognizably a vitest JSON report at all,\n * so the caller can tell \"a real report with zero structured failures\"\n * apart from \"not a report\" (the latter falls back to the raw runner\n * output; the former is a genuinely clean, if unexpected, run).\n */\nfunction extractFailures(report: unknown): ParsedFailure[] | null {\n if (!isRecord(report) || !Array.isArray(report.testResults)) return null;\n\n const failures: ParsedFailure[] = [];\n for (const fileResult of report.testResults as unknown[]) {\n if (!isRecord(fileResult)) continue;\n const file = fileResult.name;\n const assertionResults = fileResult.assertionResults;\n if (typeof file !== 'string' || !Array.isArray(assertionResults)) continue;\n\n for (const assertion of assertionResults as unknown[]) {\n if (!isRecord(assertion) || assertion.status !== 'failed') continue;\n\n const ancestorTitles = Array.isArray(assertion.ancestorTitles)\n ? assertion.ancestorTitles.filter(\n (entry): entry is string => typeof entry === 'string',\n )\n : [];\n const title =\n typeof assertion.title === 'string'\n ? assertion.title\n : '(unnamed test)';\n const failureMessages = Array.isArray(assertion.failureMessages)\n ? assertion.failureMessages.filter(\n (entry): entry is string => typeof entry === 'string',\n )\n : [];\n const firstError = failureMessages[0] ?? '';\n\n const failure: ParsedFailure = {\n file,\n suite: ancestorTitles.join(' > '),\n title,\n message: firstMeaningfulLine(firstError),\n };\n const line = lineForFile(firstError, file);\n if (line !== undefined) failure.line = line;\n const detail = extractDiff(firstError);\n if (detail !== null) failure.detail = detail;\n\n failures.push(failure);\n }\n }\n return failures;\n}\n\n/** `null` unless the report carries vitest's own aggregate pass/fail counts. */\nfunction extractSummary(report: unknown): TestSummary | null {\n if (!isRecord(report) || !Array.isArray(report.testResults)) return null;\n\n const passed = report.numPassedTests;\n const failed = report.numFailedTests;\n if (typeof passed !== 'number' || typeof failed !== 'number') return null;\n\n return { passed, failed, files: report.testResults.length };\n}\n\n/**\n * Reads and parses `reportPath` end to end. `null` on ANY failure (missing\n * file — vitest crashed before writing it; malformed JSON; a shape this\n * function doesn't recognize) so the caller falls back to the raw runner\n * output instead of the curated appendix, and never to a crash.\n */\nexport function parseTestReport(\n reportPath: string,\n projectDir: string,\n): ParsedTestReport | null {\n let raw: string;\n try {\n raw = fs.readFileSync(reportPath, 'utf8');\n } catch {\n return null;\n }\n\n let report: unknown;\n try {\n report = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const failures = extractFailures(report);\n if (failures === null) return null;\n\n const findings: RenderableFinding[] = [];\n const failingFiles: string[] = [];\n const seenFiles = new Set<string>();\n\n for (const failure of failures) {\n const relFile = archiveRelative(projectDir, failure.file);\n if (!seenFiles.has(relFile)) {\n seenFiles.add(relFile);\n failingFiles.push(relFile);\n }\n\n const label = failure.suite\n ? `${failure.suite} > ${failure.title}`\n : failure.title;\n const finding: RenderableFinding = {\n // Grouping by test FILE (not by suite/title) is deliberate — the\n // shared renderer groups by `check`, so this makes the group header\n // show the file once with every failing test beneath it.\n check: `tests/${relFile}`,\n severity: 'error',\n file: relFile,\n message: `${label}: ${failure.message}`,\n hint: 'Fix the failing assertion, then re-run \"ekanos test\".',\n };\n if (failure.line !== undefined) finding.line = failure.line;\n if (failure.detail !== undefined) finding.detail = failure.detail;\n findings.push(finding);\n }\n\n return { findings, failingFiles, summary: extractSummary(report) };\n}\n"]}
|
package/dist/toolchain-api.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type ToolchainNote, type ToolchainVersions } from './toolchain.js';
|
|
2
2
|
/**
|
|
3
|
-
* The CLI's client for the
|
|
3
|
+
* The CLI's client for the Ekanos toolchain-advisory route.
|
|
4
4
|
*
|
|
5
5
|
* ---------------------------------------------------------------------------
|
|
6
|
-
* WIRE CONTRACT — the seam between this package and the
|
|
6
|
+
* WIRE CONTRACT — the seam between this package and the Ekanos web app.
|
|
7
7
|
*
|
|
8
8
|
* The server side is the host's `/api/partner/toolchain` route and ships on
|
|
9
9
|
* a DIFFERENT BRANCH from this file, same as every other route in
|
|
10
|
-
* `auth/
|
|
10
|
+
* `auth/ekanos-api.ts` and `publish-api.ts`. Nothing here imports host code;
|
|
11
11
|
* the two halves are coupled only by HTTP, and the tests stub `fetch`,
|
|
12
12
|
* never the route.
|
|
13
13
|
*
|
|
@@ -27,7 +27,7 @@ import { type ToolchainNote, type ToolchainVersions } from './toolchain.js';
|
|
|
27
27
|
* → any other status, or a body that fails to parse or match this shape
|
|
28
28
|
*
|
|
29
29
|
* This endpoint is treated as ADVISORY, not authoritative in the way the auth
|
|
30
|
-
* and submission routes are: a 404 (an older
|
|
30
|
+
* and submission routes are: a 404 (an older Ekanos deployment that predates
|
|
31
31
|
* this route) or a 501 (deliberately unimplemented) are both entirely
|
|
32
32
|
* ordinary, and a 4xx/5xx/network failure/malformed body all mean exactly the
|
|
33
33
|
* same thing to every caller — "the endpoint is unavailable, fall back to the
|
package/dist/toolchain-api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolchain-api.js","sourceRoot":"","sources":["../src/toolchain-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EACL,kBAAkB,GAGnB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"toolchain-api.js","sourceRoot":"","sources":["../src/toolchain-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/ekanos-api';\nimport {\n TOOLCHAIN_PACKAGES,\n type ToolchainNote,\n type ToolchainVersions,\n} from './toolchain';\n\n/**\n * The CLI's client for the Ekanos toolchain-advisory route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/partner/toolchain` route and ships on\n * a DIFFERENT BRANCH from this file, same as every other route in\n * `auth/ekanos-api.ts` and `publish-api.ts`. Nothing here imports host code;\n * the two halves are coupled only by HTTP, and the tests stub `fetch`,\n * never the route.\n *\n * GET /api/partner/toolchain [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: {\n * current: { cli, sdk, ui, integrationSchema, harness }, // the\n * // coherent\n * // set the\n * // host\n * // recommends\n * minimum: { cli, sdk, ui, integrationSchema, harness }, // below\n * // this the\n * // host\n * // refuses\n * notes?: [{ level: \"warn\" | \"info\", message: string }],\n * } }\n * → any other status, or a body that fails to parse or match this shape\n *\n * This endpoint is treated as ADVISORY, not authoritative in the way the auth\n * and submission routes are: a 404 (an older Ekanos deployment that predates\n * this route) or a 501 (deliberately unimplemented) are both entirely\n * ordinary, and a 4xx/5xx/network failure/malformed body all mean exactly the\n * same thing to every caller — \"the endpoint is unavailable, fall back to the\n * npm registry\" — so this module never throws. There is no not-configured\n * distinction to make here the way `cli-config` has: a caller with no way to\n * reach this route at all (no host, no session) never calls it in the first\n * place; see `toolchain-resolve.ts`.\n *\n * Unknown extra fields in the response are tolerated (only the fields above\n * are read); a missing `notes` defaults to an empty array.\n * ---------------------------------------------------------------------------\n */\n\nexport interface ToolchainReport {\n current: ToolchainVersions;\n minimum: ToolchainVersions;\n notes: ToolchainNote[];\n}\n\nexport type FetchToolchainResult =\n | { status: 'ok'; report: ToolchainReport }\n | { status: 'unavailable' };\n\nexport async function fetchToolchainReport(\n host: string,\n accessToken: string,\n): Promise<FetchToolchainResult> {\n try {\n const response = await request(`${host}/api/partner/toolchain`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (!response.ok) return { status: 'unavailable' };\n\n const body: unknown = await response.json();\n const report = parseReport(body);\n\n return report ? { status: 'ok', report } : { status: 'unavailable' };\n } catch {\n // Network failure, timeout, or a body that is not even JSON — all read\n // the same: the endpoint is unavailable this time, fall back to npm.\n return { status: 'unavailable' };\n }\n}\n\nfunction parseReport(body: unknown): ToolchainReport | null {\n if (typeof body !== 'object' || body === null) return null;\n const data = (body as { data?: unknown }).data;\n if (typeof data !== 'object' || data === null) return null;\n\n const current = parseVersions((data as { current?: unknown }).current);\n const minimum = parseVersions((data as { minimum?: unknown }).minimum);\n if (!current || !minimum) return null;\n\n const rawNotes = (data as { notes?: unknown }).notes;\n const notes = Array.isArray(rawNotes) ? rawNotes.filter(isNote) : [];\n\n return { current, minimum, notes };\n}\n\nfunction parseVersions(value: unknown): ToolchainVersions | null {\n if (typeof value !== 'object' || value === null) return null;\n const record = value as Record<string, unknown>;\n const result: Partial<ToolchainVersions> = {};\n\n for (const key of Object.keys(\n TOOLCHAIN_PACKAGES,\n ) as (keyof ToolchainVersions)[]) {\n const raw = record[key];\n if (typeof raw !== 'string' || raw.length === 0) return null;\n result[key] = raw;\n }\n\n return result as ToolchainVersions;\n}\n\nfunction isNote(value: unknown): value is ToolchainNote {\n if (typeof value !== 'object' || value === null) return false;\n const note = value as { level?: unknown; message?: unknown };\n return (\n (note.level === 'warn' || note.level === 'info') &&\n typeof note.message === 'string'\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolchain-resolve.js","sourceRoot":"","sources":["../src/toolchain-resolve.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"toolchain-resolve.js","sourceRoot":"","sources":["../src/toolchain-resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n TOOLCHAIN_PACKAGES,\n type ToolchainKey,\n type ToolchainNote,\n type ToolchainVersions,\n} from './toolchain';\nimport { fetchToolchainReport } from './toolchain-api';\n\n/**\n * The single place that answers \"what should this toolchain be at?\" — shared\n * by `ekanos upgrade` (which installs the answer) and the proactive update\n * notice (which only reports it). Both need the exact same coherent target,\n * so there is exactly one resolution path, tried in this order:\n *\n * 1. `GET /api/partner/toolchain` on the resolved host, when both a host and\n * a session are available. Whatever the host returns is trusted as\n * already-coherent — it is the operator's own recommendation.\n * 2. The npm registry's `dist-tags.latest` for all five packages, with an\n * explicit COHERENCE check before ever calling it a \"target\": the\n * `@ekanos/sdk` / `@ekanos/ui` / `@ekanos/integration-schema` trio must\n * resolve to one identical version, and the `@ekanos/sdk` release at that\n * version must declare the exact same `@ekanos/integration-schema`\n * dependency. A multi-package publish is not atomic — this repo hit that\n * exact window mid-publish — so assembling \"latest of each, independently\"\n * can produce a set that literally cannot install together. Refusing is\n * the only safe response to a set caught in that state; the failure is\n * obvious and self-resolves the moment the publish finishes.\n */\n\nconst REGISTRY_BASE = 'https://registry.npmjs.org';\n\n/** npm registry package URLs encode the scope's `/` but not its `@`. */\nfunction registryPackageUrl(pkg: string, version?: string): string {\n const encoded = encodeURIComponent(pkg).replace('%40', '@');\n return version\n ? `${REGISTRY_BASE}/${encoded}/${encodeURIComponent(version)}`\n : `${REGISTRY_BASE}/${encoded}`;\n}\n\nexport type ToolchainResolution =\n | {\n status: 'ok';\n source: 'host';\n target: ToolchainVersions;\n minimum: ToolchainVersions;\n notes: ToolchainNote[];\n }\n | { status: 'ok'; source: 'npm'; target: ToolchainVersions }\n | { status: 'incoherent'; detail: string }\n | { status: 'unavailable' };\n\nexport interface ResolveToolchainParams {\n host?: string;\n session?: { accessToken: string } | null;\n /** Per-request timeout for the underlying fetches. */\n timeoutMs?: number;\n}\n\nexport async function resolveToolchainTarget(\n params: ResolveToolchainParams,\n): Promise<ToolchainResolution> {\n if (params.host && params.session) {\n const hostResult = await fetchToolchainReport(\n params.host,\n params.session.accessToken,\n );\n\n if (hostResult.status === 'ok') {\n return {\n status: 'ok',\n source: 'host',\n target: hostResult.report.current,\n minimum: hostResult.report.minimum,\n notes: hostResult.report.notes,\n };\n }\n\n // 404 / 501 / network failure / malformed body: fall through to npm.\n }\n\n return resolveFromNpmRegistry(params.timeoutMs);\n}\n\nasync function resolveFromNpmRegistry(\n timeoutMs?: number,\n): Promise<ToolchainResolution> {\n const entries = Object.entries(TOOLCHAIN_PACKAGES) as [\n ToolchainKey,\n string,\n ][];\n\n const latests = await Promise.all(\n entries.map(async ([key, pkg]) => {\n const body = await fetchRegistryJson(registryPackageUrl(pkg), timeoutMs);\n const latest = (body as { 'dist-tags'?: { latest?: unknown } } | null)?.[\n 'dist-tags'\n ]?.latest;\n return [key, typeof latest === 'string' ? latest : null] as const;\n }),\n );\n\n const versions = Object.fromEntries(latests) as Record<\n ToolchainKey,\n string | null\n >;\n\n if (Object.values(versions).some((version) => version === null)) {\n return { status: 'unavailable' };\n }\n\n const target = versions as ToolchainVersions;\n\n const trio = [target.sdk, target.ui, target.integrationSchema];\n if (new Set(trio).size !== 1) {\n return {\n status: 'incoherent',\n detail:\n `the npm registry reports mismatched versions for the sdk/ui/schema ` +\n `trio: @ekanos/sdk@${target.sdk}, @ekanos/ui@${target.ui}, ` +\n `@ekanos/integration-schema@${target.integrationSchema}. This is ` +\n `usually a multi-package publish still in flight — retry shortly.`,\n };\n }\n\n const sdkManifest = await fetchRegistryJson(\n registryPackageUrl(TOOLCHAIN_PACKAGES.sdk, target.sdk),\n timeoutMs,\n );\n const sdkPinnedSchema = (\n sdkManifest as { dependencies?: Record<string, unknown> } | null\n )?.dependencies?.['@ekanos/integration-schema'];\n\n if (\n typeof sdkPinnedSchema !== 'string' ||\n sdkPinnedSchema !== target.integrationSchema\n ) {\n return {\n status: 'incoherent',\n detail:\n `@ekanos/sdk@${target.sdk} on npm depends on ` +\n `@ekanos/integration-schema@${String(sdkPinnedSchema)}, which does ` +\n `not match the published @ekanos/integration-schema@` +\n `${target.integrationSchema}. This is usually a multi-package ` +\n `publish still in flight — retry shortly.`,\n };\n }\n\n return { status: 'ok', source: 'npm', target };\n}\n\nasync function fetchRegistryJson(\n url: string,\n timeoutMs?: number,\n): Promise<unknown | null> {\n try {\n const response = await fetch(url, {\n headers: { accept: 'application/json' },\n signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n });\n if (!response.ok) return null;\n return await response.json();\n } catch {\n return null;\n }\n}\n"]}
|
package/dist/toolchain.d.ts
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* features cannot drift apart on what "the toolchain" even means.
|
|
8
8
|
*/
|
|
9
9
|
export declare const TOOLCHAIN_PACKAGES: {
|
|
10
|
-
readonly cli:
|
|
11
|
-
readonly sdk:
|
|
12
|
-
readonly ui:
|
|
13
|
-
readonly integrationSchema:
|
|
14
|
-
readonly harness:
|
|
10
|
+
readonly cli: '@ekanos/cli';
|
|
11
|
+
readonly sdk: '@ekanos/sdk';
|
|
12
|
+
readonly ui: '@ekanos/ui';
|
|
13
|
+
readonly integrationSchema: '@ekanos/integration-schema';
|
|
14
|
+
readonly harness: '@ekanos/harness';
|
|
15
15
|
};
|
|
16
16
|
export type ToolchainKey = keyof typeof TOOLCHAIN_PACKAGES;
|
|
17
17
|
export type ToolchainVersions = Record<ToolchainKey, string>;
|