@ekanos/cli 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +18 -4
  2. package/dist/commands/add.js +21 -10
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/contract.js +12 -2
  5. package/dist/commands/contract.js.map +1 -1
  6. package/dist/commands/dev.js +33 -2
  7. package/dist/commands/dev.js.map +1 -1
  8. package/dist/commands/publish.d.ts +6 -0
  9. package/dist/commands/publish.js +80 -4
  10. package/dist/commands/publish.js.map +1 -1
  11. package/dist/commands/status.d.ts +44 -0
  12. package/dist/commands/status.js +79 -18
  13. package/dist/commands/status.js.map +1 -1
  14. package/dist/commands/upgrade.js +287 -144
  15. package/dist/commands/upgrade.js.map +1 -1
  16. package/dist/docs-links.js +5 -1
  17. package/dist/docs-links.js.map +1 -1
  18. package/dist/errors.d.ts +2 -0
  19. package/dist/errors.js +9 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/exit-codes.d.ts +3 -0
  22. package/dist/exit-codes.js +3 -0
  23. package/dist/exit-codes.js.map +1 -1
  24. package/dist/experience/checks.js +2 -1
  25. package/dist/experience/checks.js.map +1 -1
  26. package/dist/experience/definition-checks.d.ts +7 -0
  27. package/dist/experience/definition-checks.js +36 -4
  28. package/dist/experience/definition-checks.js.map +1 -1
  29. package/dist/experience/init.js +10 -0
  30. package/dist/experience/init.js.map +1 -1
  31. package/dist/experience/load-experience.js +33 -2
  32. package/dist/experience/load-experience.js.map +1 -1
  33. package/dist/experience/pins.d.ts +10 -0
  34. package/dist/experience/pins.js +19 -1
  35. package/dist/experience/pins.js.map +1 -1
  36. package/dist/generated/phosphor-names.d.ts +3 -0
  37. package/dist/generated/phosphor-names.js +1541 -0
  38. package/dist/generated/phosphor-names.js.map +1 -0
  39. package/dist/harness-scaffold.d.ts +13 -0
  40. package/dist/harness-scaffold.js +19 -3
  41. package/dist/harness-scaffold.js.map +1 -1
  42. package/dist/icon-findings.d.ts +12 -17
  43. package/dist/icon-findings.js +49 -74
  44. package/dist/icon-findings.js.map +1 -1
  45. package/dist/index.js +28 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/lockfile-instances.d.ts +60 -0
  48. package/dist/lockfile-instances.js +177 -0
  49. package/dist/lockfile-instances.js.map +1 -0
  50. package/dist/pack-hygiene.d.ts +8 -0
  51. package/dist/pack-hygiene.js +57 -0
  52. package/dist/pack-hygiene.js.map +1 -0
  53. package/dist/package-manager.d.ts +34 -6
  54. package/dist/package-manager.js +130 -20
  55. package/dist/package-manager.js.map +1 -1
  56. package/dist/project.js +11 -0
  57. package/dist/project.js.map +1 -1
  58. package/dist/repo/scaffold.js +4 -0
  59. package/dist/repo/scaffold.js.map +1 -1
  60. package/dist/validate-findings.js +10 -1
  61. package/dist/validate-findings.js.map +1 -1
  62. package/dist/workspace-root.d.ts +16 -0
  63. package/dist/workspace-root.js +50 -0
  64. package/dist/workspace-root.js.map +1 -0
  65. package/package.json +4 -4
  66. package/templates/AGENTS.md.tmpl +11 -15
  67. package/templates/experience/package.json.tmpl +8 -6
  68. package/templates/experience/src/widgets/hello-widget.test.tsx.tmpl +49 -0
  69. package/templates/experience/vitest.config.ts.tmpl +21 -0
  70. package/templates/harness/next.config.mjs.tmpl +7 -4
  71. package/templates/package.json.tmpl +5 -5
  72. package/templates/skills/build-integration/references/widget-design.md +2 -3
  73. package/templates/skills/polish-widgets/references/patterns.md +5 -5
  74. package/dist/generated/fa-free-names.d.ts +0 -9
  75. package/dist/generated/fa-free-names.js +0 -2599
  76. package/dist/generated/fa-free-names.js.map +0 -1
package/dist/project.js CHANGED
@@ -268,6 +268,17 @@ export function loadProject(dir) {
268
268
  catch (error) {
269
269
  throw preconditionError(`${EKANOS_CONFIG_FILENAME} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, `Fix the JSON syntax in ${configPath}.`);
270
270
  }
271
+ // A repo root's manifest is a different shape (`kind: "repo"` plus package
272
+ // globs). Name that, rather than reporting "kind" as invalid in a file
273
+ // `ekanos init --kind repo` wrote.
274
+ if (typeof raw === 'object' &&
275
+ raw !== null &&
276
+ raw.kind === 'repo') {
277
+ throw preconditionError(`${projectDir} is an Ekanos repo root (ekanos.json "kind": "repo"), ` +
278
+ 'and this command works on one integration package.', 'Run it inside a package (cd integrations/<slug>), or use a command ' +
279
+ 'that serves the whole repo: "ekanos dev", "ekanos upgrade", ' +
280
+ '"ekanos publish --all".');
281
+ }
271
282
  const result = EkanosProjectSchema.safeParse(raw);
272
283
  if (!result.success) {
273
284
  const first = result.error.issues[0];
@@ -1 +1 @@
1
- {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { AvailabilitySchema } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\nimport { notFoundError, preconditionError } from './errors';\n\nconst SlugSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {\n message:\n 'slug must be kebab-case ([a-z0-9] segments separated by single hyphens), e.g. \"acme-crm\".',\n});\n\nconst EntrySchema = z\n .string()\n .min(1, { message: 'entry must be a path to the integration module.' });\n\n/**\n * Extra Tailwind content globs, for an integration that does not live under\n * the default layout. They are interpolated into the generated\n * `styles/globals.css` as `@source` lines, so they are validated as\n * DATA-IN-CSS: project-relative, no escaping the project, and none of the\n * characters that could close the string or the surrounding comment.\n *\n * Being strict here is deliberate. A glob that matches nothing costs a partner\n * a silently unstyled integration; a glob that is rejected costs them one\n * clear error.\n */\nconst SOURCE_GLOB_PATTERN = /^[A-Za-z0-9_\\-./*{},[\\]!]+$/;\n\nconst SourceGlobSchema = z\n .string()\n .min(1, { message: 'a sourceGlobs entry must not be empty.' })\n .max(200, { message: 'a sourceGlobs entry must be under 200 characters.' })\n .regex(SOURCE_GLOB_PATTERN, {\n message:\n 'a sourceGlobs entry may contain only letters, digits and _-./*{},[]! ' +\n '— it is written verbatim into the generated stylesheet.',\n })\n .refine((glob) => !glob.startsWith('/'), {\n message: 'a sourceGlobs entry must be relative to the project root.',\n })\n .refine((glob) => !glob.split('/').includes('..'), {\n message: 'a sourceGlobs entry must not escape the project root with \"..\".',\n });\n\n/** Anything with a scheme prefix — `https:`, `file:`, a Windows drive `C:`. */\nconst URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;\n\n/** True when a `docs` entry is a URL (never touched by local checks). */\nexport function isDocUrl(entry: string): boolean {\n return /^https?:\\/\\//i.test(entry);\n}\n\n/**\n * Why a `docs` entry is invalid, or null when it is fine. One rule, shared by\n * the schema below and `init --docs`, so \"valid on the command line\" and\n * \"valid in ekanos.json\" cannot drift apart: an entry is either an http(s)\n * URL or a path relative to the project root. Absolute paths are rejected\n * because they are meaningless on anyone else's machine, and non-http(s)\n * schemes because nothing here will ever dereference them.\n */\nexport function docEntryProblem(entry: string): string | null {\n if (entry.trim() === '') return 'a docs entry must not be empty.';\n\n if (URL_SCHEME_PATTERN.test(entry)) {\n if (!isDocUrl(entry)) {\n return (\n `\"${entry}\" is neither an http(s) URL nor a relative path — a docs ` +\n 'entry is a URL to API documentation or a path relative to the ' +\n 'project root.'\n );\n }\n try {\n new URL(entry);\n return null;\n } catch {\n return `\"${entry}\" is not a valid http(s) URL.`;\n }\n }\n\n if (path.isAbsolute(entry)) {\n return (\n `\"${entry}\" is an absolute path — a docs entry that is a file must be ` +\n 'relative to the project root, so it means the same thing on every ' +\n 'machine that clones the project.'\n );\n }\n\n // Same rule as SourceGlobSchema, for the same reason: a `..` segment lets a\n // relative entry escape the project root, and `validate` resolves these\n // paths for the missing-file check.\n if (entry.split(/[\\\\/]/).includes('..')) {\n return (\n `\"${entry}\" escapes the project root with \"..\" — a docs entry that is ` +\n 'a file must stay inside the project.'\n );\n }\n\n return null;\n}\n\n/**\n * One `docs` entry: where the target API's documentation lives. An http(s)\n * URL (an OpenAPI spec, the official docs site) or a project-relative path\n * (a vendored spec, `docs/api-notes.md`). Local paths are advisory-checked by\n * `validate` (`docs/missing-file`, a warning); URLs are never fetched.\n */\nconst DocEntrySchema = z.string().superRefine((entry, ctx) => {\n const problem = docEntryProblem(entry);\n if (problem) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });\n }\n});\n\n/** One integration: the pair that addresses a definition module. */\nconst IntegrationEntrySchema = z\n .object({ slug: SlugSchema, entry: EntrySchema })\n .strict();\n\nexport type IntegrationEntry = z.infer<typeof IntegrationEntrySchema>;\n\n/**\n * `ekanos.json` — the project contract. Written by `init`, read by `validate`,\n * `dev` and `test`.\n *\n * Two shapes, because a project can hold more than one integration and the\n * single-integration form is what `init` writes and what most projects keep:\n *\n * ```json\n * { \"slug\": \"acme-crm\", \"entry\": \"src/integration.ts\" }\n *\n * { \"integrations\": [\n * { \"slug\": \"acme-crm\", \"entry\": \"src/crm.ts\" },\n * { \"slug\": \"acme-billing\", \"entry\": \"src/billing.ts\" }\n * ] }\n * ```\n *\n * A second integration used to be invisible to the CLI entirely — the harness\n * registry is an array and `collectCollisionFindings` exists precisely to\n * cross-check several definitions against each other, but `validate` could\n * only ever see one. Both forms normalise to the same list, so nothing\n * downstream has to know which was written.\n */\n/**\n * The Ekanos source this project publishes to, by slug. Ekanos source slugs\n * are opaque server-side identifiers, so the only local claims are \"one\n * token, sanely sized\" — the server resolves and authorizes it.\n */\nconst SourceSlugSchema = z\n .string()\n .min(1, { message: 'source must not be empty.' })\n .max(255, { message: 'source must be at most 255 characters.' })\n .regex(/^\\S+$/, { message: 'source must not contain whitespace.' });\n\n/**\n * The Ekanos host this project publishes to. Origin-shaped: the same\n * normalization the credential store applies (`normalizeHost`) accepts it, so\n * \"valid here\" and \"usable as a session key\" cannot drift apart.\n */\nconst HostSchema = z\n .string()\n .min(1, { message: 'host must not be empty.' })\n .max(255, { message: 'host must be at most 255 characters.' })\n .refine(\n (value) => {\n try {\n const url = new URL(value);\n return url.protocol === 'https:' || url.protocol === 'http:';\n } catch {\n return false;\n }\n },\n {\n message:\n 'host must be a full http(s) origin, e.g. \"https://app.example.com\".',\n },\n );\n\nexport const EkanosProjectSchema = z\n .object({\n $schema: z.string().optional(),\n /** `integration` (default) or `experience`; see `EkanosJsonSchema`. */\n kind: z.enum(['integration', 'experience']).optional(),\n /** The authored contract version of an integration (`ekanos contract`). */\n contract: z\n .object({ version: z.string().regex(/^\\d+\\.\\d+\\.\\d+/) })\n .strict()\n .optional(),\n slug: SlugSchema.optional(),\n entry: EntrySchema.optional(),\n integrations: z.array(IntegrationEntrySchema).optional(),\n /** Who may use the published package: every Source, or only the named ones. */\n availability: AvailabilitySchema.optional(),\n /**\n * Written by the first successful `ekanos publish --source <slug>` so\n * later publishes need no flag. Read only by `publish`.\n */\n source: SourceSlugSchema.optional(),\n /**\n * The Ekanos host this project publishes to — an http(s) origin, e.g.\n * \"https://app.example.com\". Written by the first successful `ekanos\n * publish` (alongside `source`), read by every host-taking verb as the\n * fallback after `--host` and `EKANOS_HOST`.\n */\n host: HostSchema.optional(),\n /**\n * Where the target API's documentation lives — http(s) URLs and\n * project-relative paths, in the order an agent should try them. Written\n * by `init --docs`, read by agents (see AGENTS.md's \"API documentation\")\n * and advisory-checked by `validate`. Optional and allowed empty: a\n * project that predates the field stays valid.\n */\n docs: z.array(DocEntrySchema).optional(),\n // There is deliberately no `harness` field. One existed, accepted by the\n // schema and read by nothing — the same accept-and-ignore that made\n // `sourceGlobs` cost a partner a silently unstyled integration. `.strict()`\n // now rejects it by name, which is the honest answer until something\n // actually consumes it.\n sourceGlobs: z.array(SourceGlobSchema).optional(),\n })\n .strict()\n .superRefine((value, ctx) => {\n const hasSingle = value.slug !== undefined || value.entry !== undefined;\n const hasList = value.integrations !== undefined;\n\n if (hasSingle && hasList) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n 'use either the single form ({ slug, entry }) or the list form ' +\n '({ integrations: [...] }), not both — two places to declare the ' +\n 'same thing is how they drift apart.',\n });\n return;\n }\n\n if (hasList) {\n if (value.integrations!.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations'],\n message: 'integrations must list at least one integration.',\n });\n }\n const seen = new Set<string>();\n value.integrations!.forEach((entry, index) => {\n if (seen.has(entry.slug)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations', index, 'slug'],\n message:\n `duplicate slug \"${entry.slug}\" — every integration in a ` +\n 'project needs its own, since the slug addresses it everywhere.',\n });\n }\n seen.add(entry.slug);\n });\n return;\n }\n\n if (value.slug === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['slug'],\n message: 'slug is required (or use \"integrations\": [...]).',\n });\n }\n if (value.entry === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['entry'],\n message: 'entry is required (or use \"integrations\": [...]).',\n });\n }\n });\n\nexport type EkanosProject = z.infer<typeof EkanosProjectSchema>;\n\n/** Both `ekanos.json` shapes, flattened to the one the CLI works in. */\nexport function projectIntegrations(\n project: EkanosProject,\n): IntegrationEntry[] {\n if (project.integrations !== undefined) return [...project.integrations];\n return [{ slug: project.slug!, entry: project.entry! }];\n}\n\nexport const EKANOS_CONFIG_FILENAME = 'ekanos.json';\n\n/** One integration from `ekanos.json`, with its entry resolved on disk. */\nexport interface LoadedIntegration {\n slug: string;\n entry: string;\n /** Absolute path to the resolved entry module. */\n entryPath: string;\n}\n\nexport interface LoadedProject {\n project: EkanosProject;\n /** Absolute path to the directory containing ekanos.json (the project root). */\n projectDir: string;\n /** Absolute path to ekanos.json itself. */\n configPath: string;\n /** Every integration the project declares, in declaration order. */\n integrations: LoadedIntegration[];\n /**\n * The first integration. It names the generated harness shell and is what a\n * single-integration project means by \"the\" integration; nothing else\n * privileges it.\n */\n primary: LoadedIntegration;\n}\n\n/**\n * Read and validate ekanos.json from a directory. Missing file → not-found\n * (exit 6); malformed JSON or a schema violation → precondition failed\n * (exit 9), because a project the CLI cannot even read is a precondition for\n * every verb, not a definition-level validation finding.\n */\nexport function loadProject(dir: string): LoadedProject {\n const projectDir = path.resolve(dir);\n const configPath = path.join(projectDir, EKANOS_CONFIG_FILENAME);\n\n if (!fs.existsSync(configPath)) {\n throw notFoundError(\n `No ${EKANOS_CONFIG_FILENAME} found in ${projectDir}.`,\n `Run \"ekanos init\" here first, or change into the project directory ` +\n `that contains ${EKANOS_CONFIG_FILENAME}.`,\n );\n }\n\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Fix the JSON syntax in ${configPath}.`,\n );\n }\n\n const result = EkanosProjectSchema.safeParse(raw);\n if (!result.success) {\n const first = result.error.issues[0];\n const where =\n first && first.path.length > 0 ? first.path.join('.') : '(root)';\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is invalid at \"${where}\": ${\n first?.message ?? 'schema violation'\n }`,\n `Correct ${configPath} so it matches the ekanos.json schema ` +\n `({ slug, entry } or { integrations: [...] }, plus optional ` +\n `source, host, docs and sourceGlobs).`,\n );\n }\n\n const project = result.data;\n\n const integrations = projectIntegrations(project).map((declared) => {\n const entryPath = path.resolve(projectDir, declared.entry);\n if (!fs.existsSync(entryPath)) {\n throw notFoundError(\n `entry \"${declared.entry}\" for \"${declared.slug}\" resolves to ` +\n `${entryPath}, which does not exist.`,\n `Point it at your integration module (the file that exports the ` +\n `defineIntegration(...) result) in ${configPath}.`,\n );\n }\n return { slug: declared.slug, entry: declared.entry, entryPath };\n });\n\n assertEsmProject(projectDir);\n\n return {\n project,\n projectDir,\n configPath,\n integrations,\n primary: integrations[0]!,\n };\n}\n\n/**\n * Fail unless the project is an ES module package.\n *\n * `init` sets `\"type\": \"module\"` and says so, but a warning can be scrolled\n * past, and a partner who misses it gets a confusing module-resolution error\n * from deep inside Node or Next rather than a sentence naming the problem.\n * This is the backstop: one precondition, in the loader every verb after\n * `init` goes through, so the failure is loud and says what to do.\n *\n * `init` deliberately does NOT come through here — it is the thing that fixes\n * this state.\n *\n * A project with no package.json at all is left alone: `dev` reports missing\n * dependencies with its own better message, and `validate`/`test` have their\n * own preconditions. We only assert on a manifest that exists and disagrees.\n */\nexport function assertEsmProject(projectDir: string): void {\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return;\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed JSON is npm's problem to report, not ours to guess at; the\n // partner will hit it on their next install with a better message.\n return;\n }\n\n const declared =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { type?: unknown }).type\n : undefined;\n\n if (declared === 'module') return;\n\n throw preconditionError(\n declared === undefined\n ? `${manifestPath} does not declare \"type\": \"module\", so Node treats ` +\n 'your integration as CommonJS and cannot load it.'\n : `${manifestPath} declares \"type\": ${JSON.stringify(declared)}, but the ` +\n 'integration sources are ES modules and Node cannot load them.',\n `Set \"type\": \"module\" in ${manifestPath}. If this project has CommonJS ` +\n 'sources of its own, put your integration in its own package instead — ' +\n 'the two module systems cannot share one package.json. Re-running ' +\n '\"ekanos init\" also fixes this.',\n );\n}\n\n/** Serialize an ekanos.json body with a trailing newline. */\nexport function serializeProject(project: EkanosProject): string {\n return `${JSON.stringify(project, null, 2)}\\n`;\n}\n"]}
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { AvailabilitySchema } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\nimport { notFoundError, preconditionError } from './errors';\n\nconst SlugSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {\n message:\n 'slug must be kebab-case ([a-z0-9] segments separated by single hyphens), e.g. \"acme-crm\".',\n});\n\nconst EntrySchema = z\n .string()\n .min(1, { message: 'entry must be a path to the integration module.' });\n\n/**\n * Extra Tailwind content globs, for an integration that does not live under\n * the default layout. They are interpolated into the generated\n * `styles/globals.css` as `@source` lines, so they are validated as\n * DATA-IN-CSS: project-relative, no escaping the project, and none of the\n * characters that could close the string or the surrounding comment.\n *\n * Being strict here is deliberate. A glob that matches nothing costs a partner\n * a silently unstyled integration; a glob that is rejected costs them one\n * clear error.\n */\nconst SOURCE_GLOB_PATTERN = /^[A-Za-z0-9_\\-./*{},[\\]!]+$/;\n\nconst SourceGlobSchema = z\n .string()\n .min(1, { message: 'a sourceGlobs entry must not be empty.' })\n .max(200, { message: 'a sourceGlobs entry must be under 200 characters.' })\n .regex(SOURCE_GLOB_PATTERN, {\n message:\n 'a sourceGlobs entry may contain only letters, digits and _-./*{},[]! ' +\n '— it is written verbatim into the generated stylesheet.',\n })\n .refine((glob) => !glob.startsWith('/'), {\n message: 'a sourceGlobs entry must be relative to the project root.',\n })\n .refine((glob) => !glob.split('/').includes('..'), {\n message: 'a sourceGlobs entry must not escape the project root with \"..\".',\n });\n\n/** Anything with a scheme prefix — `https:`, `file:`, a Windows drive `C:`. */\nconst URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;\n\n/** True when a `docs` entry is a URL (never touched by local checks). */\nexport function isDocUrl(entry: string): boolean {\n return /^https?:\\/\\//i.test(entry);\n}\n\n/**\n * Why a `docs` entry is invalid, or null when it is fine. One rule, shared by\n * the schema below and `init --docs`, so \"valid on the command line\" and\n * \"valid in ekanos.json\" cannot drift apart: an entry is either an http(s)\n * URL or a path relative to the project root. Absolute paths are rejected\n * because they are meaningless on anyone else's machine, and non-http(s)\n * schemes because nothing here will ever dereference them.\n */\nexport function docEntryProblem(entry: string): string | null {\n if (entry.trim() === '') return 'a docs entry must not be empty.';\n\n if (URL_SCHEME_PATTERN.test(entry)) {\n if (!isDocUrl(entry)) {\n return (\n `\"${entry}\" is neither an http(s) URL nor a relative path — a docs ` +\n 'entry is a URL to API documentation or a path relative to the ' +\n 'project root.'\n );\n }\n try {\n new URL(entry);\n return null;\n } catch {\n return `\"${entry}\" is not a valid http(s) URL.`;\n }\n }\n\n if (path.isAbsolute(entry)) {\n return (\n `\"${entry}\" is an absolute path — a docs entry that is a file must be ` +\n 'relative to the project root, so it means the same thing on every ' +\n 'machine that clones the project.'\n );\n }\n\n // Same rule as SourceGlobSchema, for the same reason: a `..` segment lets a\n // relative entry escape the project root, and `validate` resolves these\n // paths for the missing-file check.\n if (entry.split(/[\\\\/]/).includes('..')) {\n return (\n `\"${entry}\" escapes the project root with \"..\" — a docs entry that is ` +\n 'a file must stay inside the project.'\n );\n }\n\n return null;\n}\n\n/**\n * One `docs` entry: where the target API's documentation lives. An http(s)\n * URL (an OpenAPI spec, the official docs site) or a project-relative path\n * (a vendored spec, `docs/api-notes.md`). Local paths are advisory-checked by\n * `validate` (`docs/missing-file`, a warning); URLs are never fetched.\n */\nconst DocEntrySchema = z.string().superRefine((entry, ctx) => {\n const problem = docEntryProblem(entry);\n if (problem) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });\n }\n});\n\n/** One integration: the pair that addresses a definition module. */\nconst IntegrationEntrySchema = z\n .object({ slug: SlugSchema, entry: EntrySchema })\n .strict();\n\nexport type IntegrationEntry = z.infer<typeof IntegrationEntrySchema>;\n\n/**\n * `ekanos.json` — the project contract. Written by `init`, read by `validate`,\n * `dev` and `test`.\n *\n * Two shapes, because a project can hold more than one integration and the\n * single-integration form is what `init` writes and what most projects keep:\n *\n * ```json\n * { \"slug\": \"acme-crm\", \"entry\": \"src/integration.ts\" }\n *\n * { \"integrations\": [\n * { \"slug\": \"acme-crm\", \"entry\": \"src/crm.ts\" },\n * { \"slug\": \"acme-billing\", \"entry\": \"src/billing.ts\" }\n * ] }\n * ```\n *\n * A second integration used to be invisible to the CLI entirely — the harness\n * registry is an array and `collectCollisionFindings` exists precisely to\n * cross-check several definitions against each other, but `validate` could\n * only ever see one. Both forms normalise to the same list, so nothing\n * downstream has to know which was written.\n */\n/**\n * The Ekanos source this project publishes to, by slug. Ekanos source slugs\n * are opaque server-side identifiers, so the only local claims are \"one\n * token, sanely sized\" — the server resolves and authorizes it.\n */\nconst SourceSlugSchema = z\n .string()\n .min(1, { message: 'source must not be empty.' })\n .max(255, { message: 'source must be at most 255 characters.' })\n .regex(/^\\S+$/, { message: 'source must not contain whitespace.' });\n\n/**\n * The Ekanos host this project publishes to. Origin-shaped: the same\n * normalization the credential store applies (`normalizeHost`) accepts it, so\n * \"valid here\" and \"usable as a session key\" cannot drift apart.\n */\nconst HostSchema = z\n .string()\n .min(1, { message: 'host must not be empty.' })\n .max(255, { message: 'host must be at most 255 characters.' })\n .refine(\n (value) => {\n try {\n const url = new URL(value);\n return url.protocol === 'https:' || url.protocol === 'http:';\n } catch {\n return false;\n }\n },\n {\n message:\n 'host must be a full http(s) origin, e.g. \"https://app.example.com\".',\n },\n );\n\nexport const EkanosProjectSchema = z\n .object({\n $schema: z.string().optional(),\n /** `integration` (default) or `experience`; see `EkanosJsonSchema`. */\n kind: z.enum(['integration', 'experience']).optional(),\n /** The authored contract version of an integration (`ekanos contract`). */\n contract: z\n .object({ version: z.string().regex(/^\\d+\\.\\d+\\.\\d+/) })\n .strict()\n .optional(),\n slug: SlugSchema.optional(),\n entry: EntrySchema.optional(),\n integrations: z.array(IntegrationEntrySchema).optional(),\n /** Who may use the published package: every Source, or only the named ones. */\n availability: AvailabilitySchema.optional(),\n /**\n * Written by the first successful `ekanos publish --source <slug>` so\n * later publishes need no flag. Read only by `publish`.\n */\n source: SourceSlugSchema.optional(),\n /**\n * The Ekanos host this project publishes to — an http(s) origin, e.g.\n * \"https://app.example.com\". Written by the first successful `ekanos\n * publish` (alongside `source`), read by every host-taking verb as the\n * fallback after `--host` and `EKANOS_HOST`.\n */\n host: HostSchema.optional(),\n /**\n * Where the target API's documentation lives — http(s) URLs and\n * project-relative paths, in the order an agent should try them. Written\n * by `init --docs`, read by agents (see AGENTS.md's \"API documentation\")\n * and advisory-checked by `validate`. Optional and allowed empty: a\n * project that predates the field stays valid.\n */\n docs: z.array(DocEntrySchema).optional(),\n // There is deliberately no `harness` field. One existed, accepted by the\n // schema and read by nothing — the same accept-and-ignore that made\n // `sourceGlobs` cost a partner a silently unstyled integration. `.strict()`\n // now rejects it by name, which is the honest answer until something\n // actually consumes it.\n sourceGlobs: z.array(SourceGlobSchema).optional(),\n })\n .strict()\n .superRefine((value, ctx) => {\n const hasSingle = value.slug !== undefined || value.entry !== undefined;\n const hasList = value.integrations !== undefined;\n\n if (hasSingle && hasList) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n 'use either the single form ({ slug, entry }) or the list form ' +\n '({ integrations: [...] }), not both — two places to declare the ' +\n 'same thing is how they drift apart.',\n });\n return;\n }\n\n if (hasList) {\n if (value.integrations!.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations'],\n message: 'integrations must list at least one integration.',\n });\n }\n const seen = new Set<string>();\n value.integrations!.forEach((entry, index) => {\n if (seen.has(entry.slug)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['integrations', index, 'slug'],\n message:\n `duplicate slug \"${entry.slug}\" — every integration in a ` +\n 'project needs its own, since the slug addresses it everywhere.',\n });\n }\n seen.add(entry.slug);\n });\n return;\n }\n\n if (value.slug === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['slug'],\n message: 'slug is required (or use \"integrations\": [...]).',\n });\n }\n if (value.entry === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['entry'],\n message: 'entry is required (or use \"integrations\": [...]).',\n });\n }\n });\n\nexport type EkanosProject = z.infer<typeof EkanosProjectSchema>;\n\n/** Both `ekanos.json` shapes, flattened to the one the CLI works in. */\nexport function projectIntegrations(\n project: EkanosProject,\n): IntegrationEntry[] {\n if (project.integrations !== undefined) return [...project.integrations];\n return [{ slug: project.slug!, entry: project.entry! }];\n}\n\nexport const EKANOS_CONFIG_FILENAME = 'ekanos.json';\n\n/** One integration from `ekanos.json`, with its entry resolved on disk. */\nexport interface LoadedIntegration {\n slug: string;\n entry: string;\n /** Absolute path to the resolved entry module. */\n entryPath: string;\n}\n\nexport interface LoadedProject {\n project: EkanosProject;\n /** Absolute path to the directory containing ekanos.json (the project root). */\n projectDir: string;\n /** Absolute path to ekanos.json itself. */\n configPath: string;\n /** Every integration the project declares, in declaration order. */\n integrations: LoadedIntegration[];\n /**\n * The first integration. It names the generated harness shell and is what a\n * single-integration project means by \"the\" integration; nothing else\n * privileges it.\n */\n primary: LoadedIntegration;\n}\n\n/**\n * Read and validate ekanos.json from a directory. Missing file → not-found\n * (exit 6); malformed JSON or a schema violation → precondition failed\n * (exit 9), because a project the CLI cannot even read is a precondition for\n * every verb, not a definition-level validation finding.\n */\nexport function loadProject(dir: string): LoadedProject {\n const projectDir = path.resolve(dir);\n const configPath = path.join(projectDir, EKANOS_CONFIG_FILENAME);\n\n if (!fs.existsSync(configPath)) {\n throw notFoundError(\n `No ${EKANOS_CONFIG_FILENAME} found in ${projectDir}.`,\n `Run \"ekanos init\" here first, or change into the project directory ` +\n `that contains ${EKANOS_CONFIG_FILENAME}.`,\n );\n }\n\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Fix the JSON syntax in ${configPath}.`,\n );\n }\n\n // A repo root's manifest is a different shape (`kind: \"repo\"` plus package\n // globs). Name that, rather than reporting \"kind\" as invalid in a file\n // `ekanos init --kind repo` wrote.\n if (\n typeof raw === 'object' &&\n raw !== null &&\n (raw as { kind?: unknown }).kind === 'repo'\n ) {\n throw preconditionError(\n `${projectDir} is an Ekanos repo root (ekanos.json \"kind\": \"repo\"), ` +\n 'and this command works on one integration package.',\n 'Run it inside a package (cd integrations/<slug>), or use a command ' +\n 'that serves the whole repo: \"ekanos dev\", \"ekanos upgrade\", ' +\n '\"ekanos publish --all\".',\n );\n }\n\n const result = EkanosProjectSchema.safeParse(raw);\n if (!result.success) {\n const first = result.error.issues[0];\n const where =\n first && first.path.length > 0 ? first.path.join('.') : '(root)';\n throw preconditionError(\n `${EKANOS_CONFIG_FILENAME} is invalid at \"${where}\": ${\n first?.message ?? 'schema violation'\n }`,\n `Correct ${configPath} so it matches the ekanos.json schema ` +\n `({ slug, entry } or { integrations: [...] }, plus optional ` +\n `source, host, docs and sourceGlobs).`,\n );\n }\n\n const project = result.data;\n\n const integrations = projectIntegrations(project).map((declared) => {\n const entryPath = path.resolve(projectDir, declared.entry);\n if (!fs.existsSync(entryPath)) {\n throw notFoundError(\n `entry \"${declared.entry}\" for \"${declared.slug}\" resolves to ` +\n `${entryPath}, which does not exist.`,\n `Point it at your integration module (the file that exports the ` +\n `defineIntegration(...) result) in ${configPath}.`,\n );\n }\n return { slug: declared.slug, entry: declared.entry, entryPath };\n });\n\n assertEsmProject(projectDir);\n\n return {\n project,\n projectDir,\n configPath,\n integrations,\n primary: integrations[0]!,\n };\n}\n\n/**\n * Fail unless the project is an ES module package.\n *\n * `init` sets `\"type\": \"module\"` and says so, but a warning can be scrolled\n * past, and a partner who misses it gets a confusing module-resolution error\n * from deep inside Node or Next rather than a sentence naming the problem.\n * This is the backstop: one precondition, in the loader every verb after\n * `init` goes through, so the failure is loud and says what to do.\n *\n * `init` deliberately does NOT come through here — it is the thing that fixes\n * this state.\n *\n * A project with no package.json at all is left alone: `dev` reports missing\n * dependencies with its own better message, and `validate`/`test` have their\n * own preconditions. We only assert on a manifest that exists and disagrees.\n */\nexport function assertEsmProject(projectDir: string): void {\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return;\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed JSON is npm's problem to report, not ours to guess at; the\n // partner will hit it on their next install with a better message.\n return;\n }\n\n const declared =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { type?: unknown }).type\n : undefined;\n\n if (declared === 'module') return;\n\n throw preconditionError(\n declared === undefined\n ? `${manifestPath} does not declare \"type\": \"module\", so Node treats ` +\n 'your integration as CommonJS and cannot load it.'\n : `${manifestPath} declares \"type\": ${JSON.stringify(declared)}, but the ` +\n 'integration sources are ES modules and Node cannot load them.',\n `Set \"type\": \"module\" in ${manifestPath}. If this project has CommonJS ` +\n 'sources of its own, put your integration in its own package instead — ' +\n 'the two module systems cannot share one package.json. Re-running ' +\n '\"ekanos init\" also fixes this.',\n );\n}\n\n/** Serialize an ekanos.json body with a trailing newline. */\nexport function serializeProject(project: EkanosProject): string {\n return `${JSON.stringify(project, null, 2)}\\n`;\n}\n"]}
@@ -32,6 +32,10 @@ const HARNESS_DEV_DEPS = [
32
32
  'react-dom',
33
33
  'tailwindcss',
34
34
  'typescript',
35
+ // The harness and the SDK peer zod. Declaring it here, at the template's
36
+ // range, keeps the root and every package on one zod — and so on one copy
37
+ // of @ekanos/sdk under pnpm.
38
+ 'zod',
35
39
  ];
36
40
  const HARNESS_DEPS = ['@ekanos/sdk', '@ekanos/ui'];
37
41
  /** Next type-checks its own config and offers to install this when it is missing. */
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/repo/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n discoverRepo,\n findRepoRoot,\n matchesPackageGlob,\n} from '@ekanos/integration-schema/repo';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport {\n invalidStateError,\n preconditionError,\n usageError,\n validationError,\n} from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { classifyPath, writeFileNoFollow } from '../safe-fs';\nimport { readTemplateSource } from '../templates';\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport const REPO_PACKAGE_GLOBS = ['integrations/*', 'experiences/*'] as const;\n\n/** Where `ekanos new <kind>` puts a package, relative to the repo root. */\nexport const REPO_PACKAGE_DIR = {\n integration: 'integrations',\n experience: 'experiences',\n} as const;\n\nfunction formatJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/**\n * What the repo-root harness shell resolves upward into the root's\n * node_modules: Next, React and the @ekanos packages. The pins come from the\n * integration template, so there is one source of truth.\n */\nconst HARNESS_DEV_DEPS = [\n '@ekanos/cli',\n '@ekanos/harness',\n '@tailwindcss/postcss',\n '@tanstack/react-query',\n '@types/react',\n '@types/react-dom',\n 'next',\n 'next-intl',\n 'react',\n 'react-dom',\n 'tailwindcss',\n 'typescript',\n] as const;\n\nconst HARNESS_DEPS = ['@ekanos/sdk', '@ekanos/ui'] as const;\n\n/** Next type-checks its own config and offers to install this when it is missing. */\nconst NODE_TYPES_PIN = '^22.10.0';\n\nfunction templatePins(\n section: 'dependencies' | 'devDependencies',\n names: readonly string[],\n): Record<string, string> {\n const template = JSON.parse(\n readTemplateSource('package.json.tmpl'),\n ) as Record<'dependencies' | 'devDependencies', Record<string, string>>;\n\n return Object.fromEntries(\n names.flatMap((name) => {\n const range = template[section][name];\n return range ? [[name, range] as const] : [];\n }),\n );\n}\n\nfunction harnessDevDependencies(): Record<string, string> {\n return Object.fromEntries(\n Object.entries({\n ...templatePins('devDependencies', HARNESS_DEV_DEPS),\n '@types/node': NODE_TYPES_PIN,\n }).sort(([left], [right]) => left.localeCompare(right)),\n );\n}\n\nconst REPO_GITIGNORE = `# Dependencies\nnode_modules/\n\n# Build output\ndist/\n*.tsbuildinfo\n\n# Ekanos dev harness and publish artifacts (generated)\n.ekanos/\n\n# Per-developer integration overrides and credentials\nekanos.local.json\n\n# Logs\n*.log\n`;\n\nfunction repoPnpmWorkspace(): string {\n return `${readTemplateSource('pnpm-workspace.yaml.tmpl').trimEnd()}\n\npackages:\n${REPO_PACKAGE_GLOBS.map((glob) => ` - '${glob}'`).join('\\n')}\n`;\n}\n\nexport interface InitRepoArgs {\n slug?: string;\n dir?: string;\n cwd: string;\n force: boolean;\n}\n\n/** `ekanos init --kind repo`: a root that holds many integrations and experiences. */\nexport function runInitRepo(ctx: CliContext, args: InitRepoArgs): ExitCode {\n const slug = args.slug;\n if (slug !== undefined && !SLUG_PATTERN.test(slug)) {\n throw usageError(\n `Invalid slug \"${slug}\".`,\n 'Use kebab-case, e.g. \"ekanos init --kind repo --slug acme-platform\".',\n );\n }\n if (!slug && args.dir === undefined) {\n throw usageError(\n 'init --kind repo needs a name or a directory.',\n 'Pass \"--slug <name>\" (creates ./<name>) or \"--dir .\" to scaffold in place.',\n );\n }\n\n const targetDir = path.resolve(args.cwd, args.dir ?? (slug as string));\n if (fs.existsSync(targetDir) && fs.lstatSync(targetDir).isSymbolicLink()) {\n throw invalidStateError(\n `The target directory ${targetDir} is a symlink.`,\n 'Scaffold into the real directory.',\n );\n }\n if (findRepoRoot(targetDir) !== null && !args.force) {\n throw invalidStateError(\n `${targetDir} is already inside an Ekanos repo.`,\n 'Add packages with \"ekanos new integration <slug>\" or \"ekanos new experience <slug>\".',\n );\n }\n fs.mkdirSync(targetDir, { recursive: true });\n\n const name = slug ?? path.basename(targetDir);\n const files: Array<{ dest: string; content: string }> = [\n {\n dest: 'ekanos.json',\n content: formatJson({\n kind: 'repo',\n packages: [...REPO_PACKAGE_GLOBS],\n }),\n },\n {\n dest: 'package.json',\n content: formatJson({\n name: SLUG_PATTERN.test(name) ? name : 'ekanos-repo',\n version: '0.0.0',\n private: true,\n type: 'module',\n description: 'Ekanos integrations and experiences in one repo.',\n scripts: { dev: 'ekanos dev --start' },\n dependencies: templatePins('dependencies', HARNESS_DEPS),\n devDependencies: harnessDevDependencies(),\n }),\n },\n { dest: 'pnpm-workspace.yaml', content: repoPnpmWorkspace() },\n { dest: '.gitignore', content: REPO_GITIGNORE },\n { dest: 'integrations/.gitkeep', content: '' },\n { dest: 'experiences/.gitkeep', content: '' },\n ];\n\n const created: string[] = [];\n const skipped: string[] = [];\n for (const { dest, content } of files) {\n const destPath = path.join(targetDir, dest);\n const kind = classifyPath(destPath);\n if (kind !== 'missing' && kind !== 'symlink' && !args.force) {\n skipped.push(dest);\n continue;\n }\n writeFileNoFollow(targetDir, destPath, content);\n created.push(dest);\n }\n\n for (const file of created) ctx.log(` created ${file}`);\n for (const file of skipped) {\n ctx.log(` skipped ${file} (already exists; --force overwrites)`);\n }\n\n const rel = path.relative(args.cwd, targetDir) || '.';\n\n return ctx.succeed(\n { kind: 'repo', name, dir: targetDir, created, skipped },\n `Scaffolded repo \"${name}\". Next: ${rel === '.' ? '' : `cd ${rel} && `}ekanos new integration <slug> and ekanos new experience <slug>.`,\n );\n}\n\nexport interface NewPackageArgs {\n kind: string | undefined;\n slug: string | undefined;\n cwd: string;\n force: boolean;\n /** Runs the per-kind scaffold; injected by the command to avoid an import cycle with init. */\n scaffold: (args: {\n kind: 'integration' | 'experience';\n slug: string;\n dir: string;\n }) => Promise<ExitCode>;\n}\n\n/** `ekanos new integration|experience <slug>`: scaffold a package into the right folder of the repo. */\nexport async function runNewPackage(\n ctx: CliContext,\n args: NewPackageArgs,\n): Promise<ExitCode> {\n const kind = args.kind;\n if (kind !== 'integration' && kind !== 'experience') {\n throw usageError(\n kind ? `Unknown package kind \"${kind}\".` : 'new needs a package kind.',\n 'Use \"ekanos new integration <slug>\" or \"ekanos new experience <slug>\".',\n );\n }\n const slug = args.slug;\n if (!slug || !SLUG_PATTERN.test(slug)) {\n throw usageError(\n slug ? `Invalid slug \"${slug}\".` : 'new needs a slug.',\n `Pass kebab-case, e.g. \"ekanos new ${kind} acme-${kind === 'integration' ? 'crm' : 'queue'}\".`,\n );\n }\n\n const root = findRepoRoot(args.cwd);\n if (!root) {\n throw preconditionError(\n 'This directory is not inside an Ekanos repo.',\n 'Run \"ekanos init --kind repo --slug <name>\" first, or run this from inside one.',\n );\n }\n\n const repo = discoverRepo(root);\n const clash = repo.packages.find((pkg) => pkg.name === slug);\n if (clash) {\n throw validationError(\n `\"${slug}\" is already a ${clash.kind} in this repo (${clash.relDir}).`,\n 'Pick a different slug; a package is addressed by its slug.',\n );\n }\n\n const dir = path.join(root, REPO_PACKAGE_DIR[kind], slug);\n const globs = repo.manifest.packages;\n const rel = path.relative(root, dir).split(path.sep).join('/');\n const covered = globs.some((glob) => matchesPackageGlob(rel, glob));\n if (!covered) {\n throw validationError(\n `${rel} is not matched by the repo's \"packages\" globs (${globs.join(', ')}).`,\n 'Add a glob that covers it to the root ekanos.json, then re-run.',\n );\n }\n\n return args.scaffold({ kind, slug, dir });\n}\n"]}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/repo/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n discoverRepo,\n findRepoRoot,\n matchesPackageGlob,\n} from '@ekanos/integration-schema/repo';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport {\n invalidStateError,\n preconditionError,\n usageError,\n validationError,\n} from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { classifyPath, writeFileNoFollow } from '../safe-fs';\nimport { readTemplateSource } from '../templates';\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport const REPO_PACKAGE_GLOBS = ['integrations/*', 'experiences/*'] as const;\n\n/** Where `ekanos new <kind>` puts a package, relative to the repo root. */\nexport const REPO_PACKAGE_DIR = {\n integration: 'integrations',\n experience: 'experiences',\n} as const;\n\nfunction formatJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/**\n * What the repo-root harness shell resolves upward into the root's\n * node_modules: Next, React and the @ekanos packages. The pins come from the\n * integration template, so there is one source of truth.\n */\nconst HARNESS_DEV_DEPS = [\n '@ekanos/cli',\n '@ekanos/harness',\n '@tailwindcss/postcss',\n '@tanstack/react-query',\n '@types/react',\n '@types/react-dom',\n 'next',\n 'next-intl',\n 'react',\n 'react-dom',\n 'tailwindcss',\n 'typescript',\n // The harness and the SDK peer zod. Declaring it here, at the template's\n // range, keeps the root and every package on one zod — and so on one copy\n // of @ekanos/sdk under pnpm.\n 'zod',\n] as const;\n\nconst HARNESS_DEPS = ['@ekanos/sdk', '@ekanos/ui'] as const;\n\n/** Next type-checks its own config and offers to install this when it is missing. */\nconst NODE_TYPES_PIN = '^22.10.0';\n\nfunction templatePins(\n section: 'dependencies' | 'devDependencies',\n names: readonly string[],\n): Record<string, string> {\n const template = JSON.parse(\n readTemplateSource('package.json.tmpl'),\n ) as Record<'dependencies' | 'devDependencies', Record<string, string>>;\n\n return Object.fromEntries(\n names.flatMap((name) => {\n const range = template[section][name];\n return range ? [[name, range] as const] : [];\n }),\n );\n}\n\nfunction harnessDevDependencies(): Record<string, string> {\n return Object.fromEntries(\n Object.entries({\n ...templatePins('devDependencies', HARNESS_DEV_DEPS),\n '@types/node': NODE_TYPES_PIN,\n }).sort(([left], [right]) => left.localeCompare(right)),\n );\n}\n\nconst REPO_GITIGNORE = `# Dependencies\nnode_modules/\n\n# Build output\ndist/\n*.tsbuildinfo\n\n# Ekanos dev harness and publish artifacts (generated)\n.ekanos/\n\n# Per-developer integration overrides and credentials\nekanos.local.json\n\n# Logs\n*.log\n`;\n\nfunction repoPnpmWorkspace(): string {\n return `${readTemplateSource('pnpm-workspace.yaml.tmpl').trimEnd()}\n\npackages:\n${REPO_PACKAGE_GLOBS.map((glob) => ` - '${glob}'`).join('\\n')}\n`;\n}\n\nexport interface InitRepoArgs {\n slug?: string;\n dir?: string;\n cwd: string;\n force: boolean;\n}\n\n/** `ekanos init --kind repo`: a root that holds many integrations and experiences. */\nexport function runInitRepo(ctx: CliContext, args: InitRepoArgs): ExitCode {\n const slug = args.slug;\n if (slug !== undefined && !SLUG_PATTERN.test(slug)) {\n throw usageError(\n `Invalid slug \"${slug}\".`,\n 'Use kebab-case, e.g. \"ekanos init --kind repo --slug acme-platform\".',\n );\n }\n if (!slug && args.dir === undefined) {\n throw usageError(\n 'init --kind repo needs a name or a directory.',\n 'Pass \"--slug <name>\" (creates ./<name>) or \"--dir .\" to scaffold in place.',\n );\n }\n\n const targetDir = path.resolve(args.cwd, args.dir ?? (slug as string));\n if (fs.existsSync(targetDir) && fs.lstatSync(targetDir).isSymbolicLink()) {\n throw invalidStateError(\n `The target directory ${targetDir} is a symlink.`,\n 'Scaffold into the real directory.',\n );\n }\n if (findRepoRoot(targetDir) !== null && !args.force) {\n throw invalidStateError(\n `${targetDir} is already inside an Ekanos repo.`,\n 'Add packages with \"ekanos new integration <slug>\" or \"ekanos new experience <slug>\".',\n );\n }\n fs.mkdirSync(targetDir, { recursive: true });\n\n const name = slug ?? path.basename(targetDir);\n const files: Array<{ dest: string; content: string }> = [\n {\n dest: 'ekanos.json',\n content: formatJson({\n kind: 'repo',\n packages: [...REPO_PACKAGE_GLOBS],\n }),\n },\n {\n dest: 'package.json',\n content: formatJson({\n name: SLUG_PATTERN.test(name) ? name : 'ekanos-repo',\n version: '0.0.0',\n private: true,\n type: 'module',\n description: 'Ekanos integrations and experiences in one repo.',\n scripts: { dev: 'ekanos dev --start' },\n dependencies: templatePins('dependencies', HARNESS_DEPS),\n devDependencies: harnessDevDependencies(),\n }),\n },\n { dest: 'pnpm-workspace.yaml', content: repoPnpmWorkspace() },\n { dest: '.gitignore', content: REPO_GITIGNORE },\n { dest: 'integrations/.gitkeep', content: '' },\n { dest: 'experiences/.gitkeep', content: '' },\n ];\n\n const created: string[] = [];\n const skipped: string[] = [];\n for (const { dest, content } of files) {\n const destPath = path.join(targetDir, dest);\n const kind = classifyPath(destPath);\n if (kind !== 'missing' && kind !== 'symlink' && !args.force) {\n skipped.push(dest);\n continue;\n }\n writeFileNoFollow(targetDir, destPath, content);\n created.push(dest);\n }\n\n for (const file of created) ctx.log(` created ${file}`);\n for (const file of skipped) {\n ctx.log(` skipped ${file} (already exists; --force overwrites)`);\n }\n\n const rel = path.relative(args.cwd, targetDir) || '.';\n\n return ctx.succeed(\n { kind: 'repo', name, dir: targetDir, created, skipped },\n `Scaffolded repo \"${name}\". Next: ${rel === '.' ? '' : `cd ${rel} && `}ekanos new integration <slug> and ekanos new experience <slug>.`,\n );\n}\n\nexport interface NewPackageArgs {\n kind: string | undefined;\n slug: string | undefined;\n cwd: string;\n force: boolean;\n /** Runs the per-kind scaffold; injected by the command to avoid an import cycle with init. */\n scaffold: (args: {\n kind: 'integration' | 'experience';\n slug: string;\n dir: string;\n }) => Promise<ExitCode>;\n}\n\n/** `ekanos new integration|experience <slug>`: scaffold a package into the right folder of the repo. */\nexport async function runNewPackage(\n ctx: CliContext,\n args: NewPackageArgs,\n): Promise<ExitCode> {\n const kind = args.kind;\n if (kind !== 'integration' && kind !== 'experience') {\n throw usageError(\n kind ? `Unknown package kind \"${kind}\".` : 'new needs a package kind.',\n 'Use \"ekanos new integration <slug>\" or \"ekanos new experience <slug>\".',\n );\n }\n const slug = args.slug;\n if (!slug || !SLUG_PATTERN.test(slug)) {\n throw usageError(\n slug ? `Invalid slug \"${slug}\".` : 'new needs a slug.',\n `Pass kebab-case, e.g. \"ekanos new ${kind} acme-${kind === 'integration' ? 'crm' : 'queue'}\".`,\n );\n }\n\n const root = findRepoRoot(args.cwd);\n if (!root) {\n throw preconditionError(\n 'This directory is not inside an Ekanos repo.',\n 'Run \"ekanos init --kind repo --slug <name>\" first, or run this from inside one.',\n );\n }\n\n const repo = discoverRepo(root);\n const clash = repo.packages.find((pkg) => pkg.name === slug);\n if (clash) {\n throw validationError(\n `\"${slug}\" is already a ${clash.kind} in this repo (${clash.relDir}).`,\n 'Pick a different slug; a package is addressed by its slug.',\n );\n }\n\n const dir = path.join(root, REPO_PACKAGE_DIR[kind], slug);\n const globs = repo.manifest.packages;\n const rel = path.relative(root, dir).split(path.sep).join('/');\n const covered = globs.some((glob) => matchesPackageGlob(rel, glob));\n if (!covered) {\n throw validationError(\n `${rel} is not matched by the repo's \"packages\" globs (${globs.join(', ')}).`,\n 'Add a glob that covers it to the root ekanos.json, then re-run.',\n );\n }\n\n return args.scaffold({ kind, slug, dir });\n}\n"]}
@@ -1,10 +1,12 @@
1
- import { collectCollisionFindings, collectDefinitionFindings, } from '@ekanos/integration-schema';
1
+ import { collectCollisionFindings, collectCredentialLevelFindings, collectDefinitionFindings, collectScopeLevelFindings, } from '@ekanos/integration-schema';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { collectDesignFindings } from './design-findings.js';
5
5
  import { preconditionError } from './errors.js';
6
6
  import { collectIconFindings } from './icon-findings.js';
7
7
  import { loadDefinition } from './load-definition.js';
8
+ import { collectDuplicateInstanceFindings } from './lockfile-instances.js';
9
+ import { collectPackHygieneFindings } from './pack-hygiene.js';
8
10
  import { collectDocsFindings, collectHostCompilerRulesFindings, collectLintToolingFindings, collectNodeTypesFindings, collectProjectFindings, } from './project-checks.js';
9
11
  /**
10
12
  * The full findings pass shared by `validate` (which reports them) and
@@ -37,6 +39,11 @@ export async function collectAllFindings(loaded) {
37
39
  findings.push(...collectSlugAgreementFindings(integration, result.definition));
38
40
  findings.push(...collectVersionAgreementFindings(integration, result.definition, loaded.projectDir));
39
41
  findings.push(...collectOAuthSetupFindings(integration, result.definition));
42
+ findings.push(...collectScopeLevelFindings(result.definition, {
43
+ file: integration.entryPath,
44
+ }), ...collectCredentialLevelFindings(result.definition, {
45
+ file: integration.entryPath,
46
+ }));
40
47
  definitions.push(result.definition);
41
48
  continue;
42
49
  }
@@ -67,6 +74,8 @@ export async function collectAllFindings(loaded) {
67
74
  findings.push(...collectIconFindings(loaded.projectDir));
68
75
  findings.push(...collectDesignFindings(loaded.projectDir));
69
76
  findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));
77
+ findings.push(...collectPackHygieneFindings(loaded.projectDir));
78
+ findings.push(...collectDuplicateInstanceFindings(loaded.projectDir));
70
79
  return findings;
71
80
  }
72
81
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { collectDesignFindings } from './design-findings';\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport {\n collectDocsFindings,\n collectHostCompilerRulesFindings,\n collectLintToolingFindings,\n collectNodeTypesFindings,\n collectProjectFindings,\n} from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n findings.push(\n ...collectVersionAgreementFindings(\n integration,\n result.definition as { version?: unknown },\n loaded.projectDir,\n ),\n );\n findings.push(\n ...collectOAuthSetupFindings(integration, result.definition),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectLintToolingFindings(loaded.projectDir));\n findings.push(...collectNodeTypesFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n findings.push(...collectDesignFindings(loaded.projectDir));\n findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n\n/**\n * `package.json`'s `version` field and the `version` `defineIntegration()`\n * declares are two more independent sources of truth for the same fact —\n * this project shipped weather@0.1.1 with a package.json bump, but the\n * declaration itself still said 0.1.0. Generated seeds embed the\n * DECLARATION's version in their header, so a stale declaration produces a\n * seed that looks stale even though the package itself moved on.\n *\n * WARN, not error, unlike slug agreement: a mismatched version is a quality\n * signal a partner should fix, not a broken cross-reference that breaks\n * something else at runtime, so it must not block `publish`. And a project\n * with no package.json, or a package.json with no `version` field, has\n * nothing to compare the declaration against — skip rather than warn on\n * absence.\n */\nexport function collectVersionAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { version?: unknown },\n projectDir: string,\n): Finding[] {\n const declared = definition.version;\n if (typeof declared !== 'string') return [];\n\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return [];\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed package.json is npm's problem to report, not a validate\n // finding — nothing here can compare against an unparseable manifest.\n return [];\n }\n\n const packageVersion =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { version?: unknown }).version\n : undefined;\n if (typeof packageVersion !== 'string' || packageVersion === declared) {\n return [];\n }\n\n return [\n {\n check: 'project.version-agreement',\n severity: 'warn',\n file: integration.entryPath,\n message:\n `package.json declares version \"${packageVersion}\", but the ` +\n `defineIntegration() declaration for \"${integration.slug}\" still ` +\n `says \"${declared}\". Generated seed headers embed the ` +\n \"declaration's version, so a stale declaration ships stale-looking \" +\n 'metadata even though the package itself moved on.',\n hint:\n `Update the \"version\" passed to defineIntegration() in ` +\n `${integration.entryPath} to match package.json's \"${packageVersion}\".`,\n },\n ];\n}\n\n/**\n * An OAuth connection with no `setup` steps leaves whoever creates its app —\n * the Source admin (`ekanos apps`) or each team — to work out the provider's\n * console alone. A warning, not an error: the connection works without it.\n */\nexport function collectOAuthSetupFindings(\n integration: { entryPath: string },\n definition: unknown,\n): Finding[] {\n if (typeof definition !== 'object' || definition === null) return [];\n const { oauth, oauthConnections } = definition as {\n oauth?: { setup?: unknown; teamApp?: unknown };\n oauthConnections?: Record<string, { setup?: unknown; teamApp?: unknown }>;\n };\n\n type Connection = { setup?: unknown; teamApp?: unknown };\n const connections: Array<readonly [string, Connection]> = [\n ...(oauth ? [['oauth', oauth] as const] : []),\n ...Object.entries(oauthConnections ?? {}).map(\n ([id, connection]) => [`oauthConnections.${id}`, connection] as const,\n ),\n ];\n\n return connections\n .filter(([, connection]) => connection.setup === undefined)\n .map(([where, connection]) => ({\n check: 'oauth.setup-missing',\n severity: 'warn' as const,\n file: integration.entryPath,\n message:\n `${where} has no setup steps, so ` +\n (connection.teamApp\n ? 'each team creating its app'\n : \"the Source admin creating the Source's app (ekanos apps set)\") +\n \" gets no guidance in the provider's console.\",\n hint:\n `Add \\`setup: { consoleUrl, steps: [...] }\\` to ${where}: where the app is ` +\n 'created, and the steps in order. Write {callbackUrl} and {scopes} where ' +\n 'Ekanos should fill in the exact redirect URI and scopes.',\n }));\n}\n"]}
1
+ {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectCredentialLevelFindings,\n collectDefinitionFindings,\n collectScopeLevelFindings,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { collectDesignFindings } from './design-findings';\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport { collectDuplicateInstanceFindings } from './lockfile-instances';\nimport { collectPackHygieneFindings } from './pack-hygiene';\nimport type { LoadedProject } from './project';\nimport {\n collectDocsFindings,\n collectHostCompilerRulesFindings,\n collectLintToolingFindings,\n collectNodeTypesFindings,\n collectProjectFindings,\n} from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n findings.push(\n ...collectVersionAgreementFindings(\n integration,\n result.definition as { version?: unknown },\n loaded.projectDir,\n ),\n );\n findings.push(\n ...collectOAuthSetupFindings(integration, result.definition),\n );\n findings.push(\n ...collectScopeLevelFindings(result.definition, {\n file: integration.entryPath,\n }),\n ...collectCredentialLevelFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectLintToolingFindings(loaded.projectDir));\n findings.push(...collectNodeTypesFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n findings.push(...collectDesignFindings(loaded.projectDir));\n findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));\n findings.push(...collectPackHygieneFindings(loaded.projectDir));\n findings.push(...collectDuplicateInstanceFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n\n/**\n * `package.json`'s `version` field and the `version` `defineIntegration()`\n * declares are two more independent sources of truth for the same fact —\n * this project shipped weather@0.1.1 with a package.json bump, but the\n * declaration itself still said 0.1.0. Generated seeds embed the\n * DECLARATION's version in their header, so a stale declaration produces a\n * seed that looks stale even though the package itself moved on.\n *\n * WARN, not error, unlike slug agreement: a mismatched version is a quality\n * signal a partner should fix, not a broken cross-reference that breaks\n * something else at runtime, so it must not block `publish`. And a project\n * with no package.json, or a package.json with no `version` field, has\n * nothing to compare the declaration against — skip rather than warn on\n * absence.\n */\nexport function collectVersionAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { version?: unknown },\n projectDir: string,\n): Finding[] {\n const declared = definition.version;\n if (typeof declared !== 'string') return [];\n\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return [];\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed package.json is npm's problem to report, not a validate\n // finding — nothing here can compare against an unparseable manifest.\n return [];\n }\n\n const packageVersion =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { version?: unknown }).version\n : undefined;\n if (typeof packageVersion !== 'string' || packageVersion === declared) {\n return [];\n }\n\n return [\n {\n check: 'project.version-agreement',\n severity: 'warn',\n file: integration.entryPath,\n message:\n `package.json declares version \"${packageVersion}\", but the ` +\n `defineIntegration() declaration for \"${integration.slug}\" still ` +\n `says \"${declared}\". Generated seed headers embed the ` +\n \"declaration's version, so a stale declaration ships stale-looking \" +\n 'metadata even though the package itself moved on.',\n hint:\n `Update the \"version\" passed to defineIntegration() in ` +\n `${integration.entryPath} to match package.json's \"${packageVersion}\".`,\n },\n ];\n}\n\n/**\n * An OAuth connection with no `setup` steps leaves whoever creates its app —\n * the Source admin (`ekanos apps`) or each team — to work out the provider's\n * console alone. A warning, not an error: the connection works without it.\n */\nexport function collectOAuthSetupFindings(\n integration: { entryPath: string },\n definition: unknown,\n): Finding[] {\n if (typeof definition !== 'object' || definition === null) return [];\n const { oauth, oauthConnections } = definition as {\n oauth?: { setup?: unknown; teamApp?: unknown };\n oauthConnections?: Record<string, { setup?: unknown; teamApp?: unknown }>;\n };\n\n type Connection = { setup?: unknown; teamApp?: unknown };\n const connections: Array<readonly [string, Connection]> = [\n ...(oauth ? [['oauth', oauth] as const] : []),\n ...Object.entries(oauthConnections ?? {}).map(\n ([id, connection]) => [`oauthConnections.${id}`, connection] as const,\n ),\n ];\n\n return connections\n .filter(([, connection]) => connection.setup === undefined)\n .map(([where, connection]) => ({\n check: 'oauth.setup-missing',\n severity: 'warn' as const,\n file: integration.entryPath,\n message:\n `${where} has no setup steps, so ` +\n (connection.teamApp\n ? 'each team creating its app'\n : \"the Source admin creating the Source's app (ekanos apps set)\") +\n \" gets no guidance in the provider's console.\",\n hint:\n `Add \\`setup: { consoleUrl, steps: [...] }\\` to ${where}: where the app is ` +\n 'created, and the steps in order. Write {callbackUrl} and {scopes} where ' +\n 'Ekanos should fill in the exact redirect URI and scopes.',\n }));\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Whether `dir` is the root of a package-manager workspace: a
3
+ * `pnpm-workspace.yaml`, an Ekanos repo (`ekanos.json` with `kind: "repo"`),
4
+ * or a `package.json` declaring npm/yarn/bun `workspaces`.
5
+ */
6
+ export declare function isWorkspaceRoot(dir: string): boolean;
7
+ /**
8
+ * The nearest workspace root at or above `projectDir`, or `projectDir` itself
9
+ * when it is not inside one. A standalone project scaffolded by `ekanos init`
10
+ * carries its own `pnpm-workspace.yaml`, so it is its own root.
11
+ *
12
+ * Under a workspace, dependencies are hoisted (or symlinked, under pnpm) into
13
+ * the ROOT's `node_modules`, so this is where Next's Turbopack root has to sit
14
+ * and where the lockfile a package manager writes lives.
15
+ */
16
+ export declare function findWorkspaceRoot(projectDir: string): string;
@@ -0,0 +1,50 @@
1
+ import { isRepoRoot } from '@ekanos/integration-schema/repo';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ /**
5
+ * Whether `dir` is the root of a package-manager workspace: a
6
+ * `pnpm-workspace.yaml`, an Ekanos repo (`ekanos.json` with `kind: "repo"`),
7
+ * or a `package.json` declaring npm/yarn/bun `workspaces`.
8
+ */
9
+ export function isWorkspaceRoot(dir) {
10
+ if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')))
11
+ return true;
12
+ if (isRepoRoot(dir))
13
+ return true;
14
+ const manifestPath = path.join(dir, 'package.json');
15
+ if (!fs.existsSync(manifestPath))
16
+ return false;
17
+ try {
18
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
19
+ return (typeof parsed === 'object' &&
20
+ parsed !== null &&
21
+ 'workspaces' in parsed &&
22
+ parsed.workspaces !== undefined);
23
+ }
24
+ catch (_a) {
25
+ // An unparseable package.json declares nothing; the commands that need
26
+ // it report it themselves.
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * The nearest workspace root at or above `projectDir`, or `projectDir` itself
32
+ * when it is not inside one. A standalone project scaffolded by `ekanos init`
33
+ * carries its own `pnpm-workspace.yaml`, so it is its own root.
34
+ *
35
+ * Under a workspace, dependencies are hoisted (or symlinked, under pnpm) into
36
+ * the ROOT's `node_modules`, so this is where Next's Turbopack root has to sit
37
+ * and where the lockfile a package manager writes lives.
38
+ */
39
+ export function findWorkspaceRoot(projectDir) {
40
+ let current = path.resolve(projectDir);
41
+ for (;;) {
42
+ if (isWorkspaceRoot(current))
43
+ return current;
44
+ const parent = path.dirname(current);
45
+ if (parent === current)
46
+ return path.resolve(projectDir);
47
+ current = parent;
48
+ }
49
+ }
50
+ //# sourceMappingURL=workspace-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-root.js","sourceRoot":"","sources":["../src/workspace-root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACtE,IAAI,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACpD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,OAAO,CACL,OAAO,MAAM,KAAK,QAAQ;YAC1B,MAAM,KAAK,IAAI;YACf,YAAY,IAAI,MAAM;YACrB,MAAmC,CAAC,UAAU,KAAK,SAAS,CAC9D,CAAC;IACJ,CAAC;eAAO,CAAC;QACP,uEAAuE;QACvE,2BAA2B;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvC,SAAS,CAAC;QACR,IAAI,eAAe,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACxD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;AACH,CAAC","sourcesContent":["import { isRepoRoot } from '@ekanos/integration-schema/repo';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Whether `dir` is the root of a package-manager workspace: a\n * `pnpm-workspace.yaml`, an Ekanos repo (`ekanos.json` with `kind: \"repo\"`),\n * or a `package.json` declaring npm/yarn/bun `workspaces`.\n */\nexport function isWorkspaceRoot(dir: string): boolean {\n if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return true;\n if (isRepoRoot(dir)) return true;\n\n const manifestPath = path.join(dir, 'package.json');\n if (!fs.existsSync(manifestPath)) return false;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n return (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'workspaces' in parsed &&\n (parsed as { workspaces?: unknown }).workspaces !== undefined\n );\n } catch {\n // An unparseable package.json declares nothing; the commands that need\n // it report it themselves.\n return false;\n }\n}\n\n/**\n * The nearest workspace root at or above `projectDir`, or `projectDir` itself\n * when it is not inside one. A standalone project scaffolded by `ekanos init`\n * carries its own `pnpm-workspace.yaml`, so it is its own root.\n *\n * Under a workspace, dependencies are hoisted (or symlinked, under pnpm) into\n * the ROOT's `node_modules`, so this is where Next's Turbopack root has to sit\n * and where the lockfile a package manager writes lives.\n */\nexport function findWorkspaceRoot(projectDir: string): string {\n let current = path.resolve(projectDir);\n for (;;) {\n if (isWorkspaceRoot(current)) return current;\n const parent = path.dirname(current);\n if (parent === current) return path.resolve(projectDir);\n current = parent;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/cli",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "private": false,
5
5
  "description": "The Ekanos partner toolchain CLI: scaffold, validate, and test an Ekanos integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@clack/prompts": "1.8.1",
38
- "@ekanos/integration-schema": "0.25.0",
38
+ "@ekanos/integration-schema": "0.26.0",
39
39
  "esbuild": "0.28.2",
40
40
  "picocolors": "1.1.1"
41
41
  },
@@ -51,9 +51,9 @@
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs",
54
- "check:fa-free-names": "node scripts/generate-fa-free-names.mjs",
54
+ "check:icon-names": "node scripts/generate-icon-names.mjs",
55
55
  "clean": "git clean -xdf .turbo node_modules dist",
56
- "generate:fa-free-names": "node scripts/generate-fa-free-names.mjs --write",
56
+ "generate:icon-names": "node scripts/generate-icon-names.mjs --write",
57
57
  "typecheck": "tsc --noEmit",
58
58
  "test": "vitest run --config vitest.config.ts"
59
59
  },
@@ -420,8 +420,9 @@ your handler with the values the user actually typed.
420
420
 
421
421
  Build widget UI from `@ekanos/ui` — the published slice of the host design
422
422
  system. Available modules (import individually, e.g. `@ekanos/ui/button`):
423
- `ai-prompt-input`, `alert`, `badge` (counts, categories and tags), `button`,
424
- `card`, `confirm-dialog`, `data-table` (the house table: sorting, paging,
423
+ `ai-prompt-input`, `alert`, `avatar` (`Avatar`, `AvatarGroup`,
424
+ `PersonAvatar`), `badge` (counts, categories and tags), `button`, `card`,
425
+ `charts` (`Sparkline`, `ProgressRing`, `BarSeries`), `confirm-dialog`, `data-table` (the house table: sorting, paging,
425
426
  row clicks), `dialog`, `dropdown-menu`, `empty-note`, `empty-state`, `form`,
426
427
  `icon`, `if`, `input`, `label`, `page` (page shape: `PageRail`,
427
428
  `PageTitleSection`, `PageToolbar`, `PageSidenavLayout`, `ListMasthead`,
@@ -444,19 +445,14 @@ Source's branding. `ekanos validate` reports each of these as a `design/*`
444
445
  warning. Widget layout and type rules are in
445
446
  `.claude/skills/build-integration/references/widget-design.md`.
446
447
 
447
- Icons are Font Awesome glyph names via `@ekanos/ui/icon`. This generated shell loads Font Awesome **Free**
448
- (`@fortawesome/fontawesome-free`) — a much smaller set than the Pro kit Ekanos
449
- itself runs — so a name that's Pro-only (or just mistyped) renders a
450
- circle-question disc instead of vanishing, and dev builds `console.warn` once
451
- per name to catch it before a screenshot review does. Write icon names
452
- family-less (`fa-gear`, not `fa-light fa-gear`): the host's theme picks the
453
- family, so a Source's icon style reaches your icons too, and
454
- `design/no-default-icon-family` rejects `fa-light` / `fa-regular`. On Free,
455
- families it lacks (`fa-light`, `fa-duotone`, `fa-thin`) still render as
456
- `fa-solid`. Verify a glyph name against the
457
- Free set (`pnpm --filter @ekanos/ui check:fa-free-names`, or
458
- https://fontawesome.com/search?o=r&s=solid,regular,brands) before relying on
459
- it looking like anything in particular.
448
+ Icons are Phosphor names (phosphoricons.com, MIT) via `@ekanos/ui/icon`:
449
+ `<Icon name="house" />`. The fonts ship inside `@ekanos/ui`, so the harness
450
+ draws exactly what Ekanos draws. Write the name without a weight: the host's
451
+ theme picks it, so a Source's icon style reaches your icons too. A filled
452
+ state glyph (a selected star) is `"star:fill"`. A name that is not a Phosphor
453
+ icon draws the `shapes` fallback, and `ekanos validate` warns on it
454
+ (`icons/unknown`) and on a Font Awesome value left over from before the move
455
+ (`icons/font-awesome`).
460
456
 
461
457
  ## Live mode — real API calls, locally
462
458
 
@@ -17,20 +17,21 @@
17
17
  "scripts": {
18
18
  "typecheck": "tsc --noEmit",
19
19
  "lint": "eslint .",
20
+ "test": "vitest run",
20
21
  "check": "npx ekanos check",
21
22
  "build": "npx ekanos build"
22
23
  },
23
24
  "dependencies": {
24
- "@ekanos/sdk": "^0.25.0",
25
- "@ekanos/ui": "^0.25.0",
25
+ "@ekanos/sdk": "^0.26.0",
26
+ "@ekanos/ui": "^0.26.0",
26
27
  "@tanstack/react-query": "^5.101.4",
27
28
  "react": "^19.2.8",
28
29
  "react-dom": "^19.2.8",
29
- "zod": "^3.25.76"
30
+ "zod": "^4.6.7"
30
31
  },
31
32
  "devDependencies": {
32
- "@ekanos/cli": "^0.22.1",
33
- "@ekanos/harness": "^0.25.0",
33
+ "@ekanos/cli": "^0.23.0",
34
+ "@ekanos/harness": "^0.26.0",
34
35
  "@tailwindcss/postcss": "^4.3.3",
35
36
  "@typescript-eslint/parser": "^8.46.2",
36
37
  "@types/react": "^19.2.0",
@@ -39,6 +40,7 @@
39
40
  "next": "^16.0.0",
40
41
  "react-hook-form": "^7.68.0",
41
42
  "tailwindcss": "^4.3.3",
42
- "typescript": "^5.9.3"
43
+ "typescript": "^5.9.3",
44
+ "vitest": "4.1.10"
43
45
  }
44
46
  }
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { renderToStaticMarkup } from 'react-dom/server';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { WidgetPreviewProvider } from '@ekanos/sdk/components';
8
+ import { ExperienceRuntimeProvider } from '@ekanos/sdk/experience/client';
9
+ import type { ExperienceRuntime } from '@ekanos/sdk/experience/runtime';
10
+
11
+ import { HelloWidget } from './hello-widget';
12
+
13
+ // Widget.* needs the host's widget context, which WidgetPreviewProvider
14
+ // stands in for (state="loading" | "error" walks the other branches).
15
+ //
16
+ // A stand-in for the host's runtime. A widget that calls
17
+ // useOperation('<integration>', 'tool/<name>') reaches it through
18
+ // `call`: return { ok: true, data } or { ok: false, reason, message } to
19
+ // test the happy path and a refusal.
20
+ const runtime: ExperienceRuntime = {
21
+ call: async () => ({
22
+ ok: false,
23
+ reason: 'binding_missing',
24
+ message: 'Not connected in a test.',
25
+ }),
26
+ integrations: () => [],
27
+ };
28
+
29
+ function render(node: ReactNode) {
30
+ const client = new QueryClient({
31
+ defaultOptions: { queries: { retry: false } },
32
+ });
33
+
34
+ return renderToStaticMarkup(
35
+ <QueryClientProvider client={client}>
36
+ <ExperienceRuntimeProvider runtime={runtime}>
37
+ <WidgetPreviewProvider widgetId="hello" title="Hello">
38
+ {node}
39
+ </WidgetPreviewProvider>
40
+ </ExperienceRuntimeProvider>
41
+ </QueryClientProvider>,
42
+ );
43
+ }
44
+
45
+ describe('HelloWidget', () => {
46
+ it('renders inside the experience runtime', () => {
47
+ expect(render(<HelloWidget />)).toContain('Integrations');
48
+ });
49
+ });
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ // Widgets are rendered to a string with react-dom/server, so no DOM
7
+ // environment is needed. Add jsdom and a testing library only if you
8
+ // start testing interaction.
9
+ environment: 'node',
10
+ include: ['src/**/*.test.{ts,tsx}'],
11
+ // REQUIRED — do not remove. The published @ekanos/sdk dist imports the
12
+ // extensionless "next/link"; without inlining the SDK here, vitest cannot
13
+ // transform and load it, your test files import nothing, and vitest reports
14
+ // GREEN over zero tests.
15
+ server: {
16
+ deps: {
17
+ inline: ['@ekanos/sdk', '@ekanos/ui'],
18
+ },
19
+ },
20
+ },
21
+ });