@ekanos/cli 0.15.0 → 0.17.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 (287) hide show
  1. package/README.md +103 -8
  2. package/dist/auth/ekanos-api.d.ts +23 -0
  3. package/dist/auth/ekanos-api.js +33 -6
  4. package/dist/auth/ekanos-api.js.map +1 -1
  5. package/dist/commands/add.d.ts +2 -0
  6. package/dist/commands/add.js +118 -0
  7. package/dist/commands/add.js.map +1 -0
  8. package/dist/commands/agents.js +3 -2
  9. package/dist/commands/agents.js.map +1 -1
  10. package/dist/commands/apps.d.ts +11 -3
  11. package/dist/commands/apps.js +86 -28
  12. package/dist/commands/apps.js.map +1 -1
  13. package/dist/commands/audiences.d.ts +31 -0
  14. package/dist/commands/audiences.js +239 -0
  15. package/dist/commands/audiences.js.map +1 -0
  16. package/dist/commands/build.d.ts +2 -0
  17. package/dist/commands/build.js +41 -0
  18. package/dist/commands/build.js.map +1 -0
  19. package/dist/commands/catalog.d.ts +4 -2
  20. package/dist/commands/catalog.js +166 -35
  21. package/dist/commands/catalog.js.map +1 -1
  22. package/dist/commands/check.d.ts +2 -0
  23. package/dist/commands/check.js +43 -0
  24. package/dist/commands/check.js.map +1 -0
  25. package/dist/commands/codegen.d.ts +2 -0
  26. package/dist/commands/codegen.js +49 -0
  27. package/dist/commands/codegen.js.map +1 -0
  28. package/dist/commands/contract.d.ts +52 -0
  29. package/dist/commands/contract.js +300 -0
  30. package/dist/commands/contract.js.map +1 -0
  31. package/dist/commands/dev.d.ts +7 -0
  32. package/dist/commands/dev.js +186 -12
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/index-experience.d.ts +31 -0
  35. package/dist/commands/index-experience.js +21 -0
  36. package/dist/commands/index-experience.js.map +1 -0
  37. package/dist/commands/init.d.ts +7 -0
  38. package/dist/commands/init.js +12 -0
  39. package/dist/commands/init.js.map +1 -1
  40. package/dist/commands/login.d.ts +2 -0
  41. package/dist/commands/login.js +17 -0
  42. package/dist/commands/login.js.map +1 -1
  43. package/dist/commands/new.d.ts +2 -0
  44. package/dist/commands/new.js +39 -0
  45. package/dist/commands/new.js.map +1 -0
  46. package/dist/commands/publish.d.ts +59 -0
  47. package/dist/commands/publish.js +118 -65
  48. package/dist/commands/publish.js.map +1 -1
  49. package/dist/commands/runs.d.ts +7 -3
  50. package/dist/commands/runs.js +21 -5
  51. package/dist/commands/runs.js.map +1 -1
  52. package/dist/commands/scope-notice.d.ts +13 -0
  53. package/dist/commands/scope-notice.js +12 -4
  54. package/dist/commands/scope-notice.js.map +1 -1
  55. package/dist/commands/sources-roles.d.ts +5 -0
  56. package/dist/commands/sources-roles.js +235 -0
  57. package/dist/commands/sources-roles.js.map +1 -0
  58. package/dist/commands/sources.d.ts +5 -0
  59. package/dist/commands/sources.js +8 -1
  60. package/dist/commands/sources.js.map +1 -1
  61. package/dist/commands/sync.d.ts +2 -0
  62. package/dist/commands/sync.js +150 -0
  63. package/dist/commands/sync.js.map +1 -0
  64. package/dist/commands/team-agent-readiness.d.ts +18 -2
  65. package/dist/commands/team-agent-readiness.js +39 -39
  66. package/dist/commands/team-agent-readiness.js.map +1 -1
  67. package/dist/commands/team-agents-agents.d.ts +5 -2
  68. package/dist/commands/team-agents-agents.js +152 -15
  69. package/dist/commands/team-agents-agents.js.map +1 -1
  70. package/dist/commands/team-agents-define.d.ts +31 -0
  71. package/dist/commands/team-agents-define.js +176 -0
  72. package/dist/commands/team-agents-define.js.map +1 -0
  73. package/dist/commands/team-agents-mcp.js +112 -10
  74. package/dist/commands/team-agents-mcp.js.map +1 -1
  75. package/dist/commands/team-agents-triggers.d.ts +1 -1
  76. package/dist/commands/team-agents-triggers.js +137 -5
  77. package/dist/commands/team-agents-triggers.js.map +1 -1
  78. package/dist/commands/team-agents.d.ts +4 -1
  79. package/dist/commands/team-agents.js +17 -4
  80. package/dist/commands/team-agents.js.map +1 -1
  81. package/dist/commands/team-llm-key.d.ts +14 -0
  82. package/dist/commands/team-llm-key.js +122 -0
  83. package/dist/commands/team-llm-key.js.map +1 -0
  84. package/dist/commands/teams-audience.d.ts +12 -0
  85. package/dist/commands/teams-audience.js +144 -0
  86. package/dist/commands/teams-audience.js.map +1 -0
  87. package/dist/commands/teams-connect.js +123 -31
  88. package/dist/commands/teams-connect.js.map +1 -1
  89. package/dist/commands/teams-lifecycle.d.ts +90 -0
  90. package/dist/commands/teams-lifecycle.js +457 -0
  91. package/dist/commands/teams-lifecycle.js.map +1 -0
  92. package/dist/commands/teams-onboard.d.ts +14 -0
  93. package/dist/commands/teams-onboard.js +62 -13
  94. package/dist/commands/teams-onboard.js.map +1 -1
  95. package/dist/commands/teams-overview.js +6 -2
  96. package/dist/commands/teams-overview.js.map +1 -1
  97. package/dist/commands/teams-scope.js +4 -0
  98. package/dist/commands/teams-scope.js.map +1 -1
  99. package/dist/commands/teams.d.ts +25 -5
  100. package/dist/commands/teams.js +106 -39
  101. package/dist/commands/teams.js.map +1 -1
  102. package/dist/commands/workflows.d.ts +37 -0
  103. package/dist/commands/workflows.js +589 -0
  104. package/dist/commands/workflows.js.map +1 -0
  105. package/dist/compatibility.d.ts +10 -1
  106. package/dist/compatibility.js +10 -1
  107. package/dist/compatibility.js.map +1 -1
  108. package/dist/connect-fields.d.ts +22 -0
  109. package/dist/connect-fields.js +115 -0
  110. package/dist/connect-fields.js.map +1 -0
  111. package/dist/context.d.ts +12 -0
  112. package/dist/context.js +32 -9
  113. package/dist/context.js.map +1 -1
  114. package/dist/dev-experience.d.ts +125 -0
  115. package/dist/dev-experience.js +128 -0
  116. package/dist/dev-experience.js.map +1 -0
  117. package/dist/experience/boundary.d.ts +12 -0
  118. package/dist/experience/boundary.js +260 -0
  119. package/dist/experience/boundary.js.map +1 -0
  120. package/dist/experience/build.d.ts +16 -0
  121. package/dist/experience/build.js +90 -0
  122. package/dist/experience/build.js.map +1 -0
  123. package/dist/experience/checks.d.ts +29 -0
  124. package/dist/experience/checks.js +133 -0
  125. package/dist/experience/checks.js.map +1 -0
  126. package/dist/experience/contract-sources.d.ts +36 -0
  127. package/dist/experience/contract-sources.js +111 -0
  128. package/dist/experience/contract-sources.js.map +1 -0
  129. package/dist/experience/definition-checks.d.ts +26 -0
  130. package/dist/experience/definition-checks.js +201 -0
  131. package/dist/experience/definition-checks.js.map +1 -0
  132. package/dist/experience/definition-shape.d.ts +60 -0
  133. package/dist/experience/definition-shape.js +2 -0
  134. package/dist/experience/definition-shape.js.map +1 -0
  135. package/dist/experience/init.d.ts +10 -0
  136. package/dist/experience/init.js +64 -0
  137. package/dist/experience/init.js.map +1 -0
  138. package/dist/experience/integrations-codegen.d.ts +17 -0
  139. package/dist/experience/integrations-codegen.js +51 -0
  140. package/dist/experience/integrations-codegen.js.map +1 -0
  141. package/dist/experience/json-schema-to-ts.d.ts +1 -0
  142. package/dist/experience/json-schema-to-ts.js +90 -0
  143. package/dist/experience/json-schema-to-ts.js.map +1 -0
  144. package/dist/experience/load-experience.d.ts +23 -0
  145. package/dist/experience/load-experience.js +115 -0
  146. package/dist/experience/load-experience.js.map +1 -0
  147. package/dist/experience/overrides.d.ts +8 -0
  148. package/dist/experience/overrides.js +21 -0
  149. package/dist/experience/overrides.js.map +1 -0
  150. package/dist/experience/pin-integration.d.ts +8 -0
  151. package/dist/experience/pin-integration.js +17 -0
  152. package/dist/experience/pin-integration.js.map +1 -0
  153. package/dist/experience/pin-ops.d.ts +12 -0
  154. package/dist/experience/pin-ops.js +36 -0
  155. package/dist/experience/pin-ops.js.map +1 -0
  156. package/dist/experience/pins.d.ts +35 -0
  157. package/dist/experience/pins.js +91 -0
  158. package/dist/experience/pins.js.map +1 -0
  159. package/dist/experience/project.d.ts +52 -0
  160. package/dist/experience/project.js +149 -0
  161. package/dist/experience/project.js.map +1 -0
  162. package/dist/experience/publish.d.ts +51 -0
  163. package/dist/experience/publish.js +144 -0
  164. package/dist/experience/publish.js.map +1 -0
  165. package/dist/experience/regenerate.d.ts +10 -0
  166. package/dist/experience/regenerate.js +33 -0
  167. package/dist/experience/regenerate.js.map +1 -0
  168. package/dist/experience/report.d.ts +9 -0
  169. package/dist/experience/report.js +23 -0
  170. package/dist/experience/report.js.map +1 -0
  171. package/dist/harness-scaffold.d.ts +52 -18
  172. package/dist/harness-scaffold.js +189 -9
  173. package/dist/harness-scaffold.js.map +1 -1
  174. package/dist/index.d.ts +13 -0
  175. package/dist/index.js +494 -127
  176. package/dist/index.js.map +1 -1
  177. package/dist/invocation.d.ts +31 -0
  178. package/dist/invocation.js +58 -0
  179. package/dist/invocation.js.map +1 -0
  180. package/dist/project-checks.d.ts +12 -0
  181. package/dist/project-checks.js +46 -0
  182. package/dist/project-checks.js.map +1 -1
  183. package/dist/project.d.ts +14 -0
  184. package/dist/project.js +10 -0
  185. package/dist/project.js.map +1 -1
  186. package/dist/repo/publish.d.ts +42 -0
  187. package/dist/repo/publish.js +225 -0
  188. package/dist/repo/publish.js.map +1 -0
  189. package/dist/repo/scaffold.d.ts +30 -0
  190. package/dist/repo/scaffold.js +165 -0
  191. package/dist/repo/scaffold.js.map +1 -0
  192. package/dist/runs-api.js +8 -3
  193. package/dist/runs-api.js.map +1 -1
  194. package/dist/safe-fs.d.ts +18 -0
  195. package/dist/safe-fs.js +57 -0
  196. package/dist/safe-fs.js.map +1 -1
  197. package/dist/secret-form.d.ts +48 -0
  198. package/dist/secret-form.js +212 -0
  199. package/dist/secret-form.js.map +1 -0
  200. package/dist/secret-input.d.ts +96 -0
  201. package/dist/secret-input.js +299 -0
  202. package/dist/secret-input.js.map +1 -0
  203. package/dist/team-connect-api.d.ts +78 -3
  204. package/dist/team-connect-api.js +17 -2
  205. package/dist/team-connect-api.js.map +1 -1
  206. package/dist/team-lifecycle-api.d.ts +435 -0
  207. package/dist/team-lifecycle-api.js +301 -0
  208. package/dist/team-lifecycle-api.js.map +1 -0
  209. package/dist/teams-api.d.ts +49 -1
  210. package/dist/teams-api.js +149 -27
  211. package/dist/teams-api.js.map +1 -1
  212. package/dist/unknown-command.d.ts +18 -0
  213. package/dist/unknown-command.js +50 -0
  214. package/dist/unknown-command.js.map +1 -0
  215. package/dist/validate-findings.js +2 -1
  216. package/dist/validate-findings.js.map +1 -1
  217. package/dist/workflows-api.d.ts +69 -0
  218. package/dist/workflows-api.js +182 -0
  219. package/dist/workflows-api.js.map +1 -0
  220. package/package.json +3 -3
  221. package/templates/AGENTS.md.tmpl +6 -4
  222. package/templates/claude-skill.md.tmpl +4 -1
  223. package/templates/ekanos.json.tmpl +1 -0
  224. package/templates/experience/AGENTS.md.tmpl +22 -0
  225. package/templates/experience/ekanos.json.tmpl +7 -0
  226. package/templates/experience/ekanos.lock.json.tmpl +4 -0
  227. package/templates/experience/eslint.config.mjs.tmpl +25 -0
  228. package/templates/experience/gitignore.tmpl +17 -0
  229. package/templates/experience/package.json.tmpl +41 -0
  230. package/templates/experience/src/.ekanos/integrations.d.ts.tmpl +3 -0
  231. package/templates/experience/src/experience.ts.tmpl +30 -0
  232. package/templates/experience/src/widgets/hello-widget.tsx.tmpl +40 -0
  233. package/templates/experience/tsconfig.json.tmpl +20 -0
  234. package/templates/harness/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +9 -0
  235. package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -1
  236. package/templates/harness/app/[slug]/page.tsx.tmpl +1 -1
  237. package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -1
  238. package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -1
  239. package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -1
  240. package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -1
  241. package/templates/harness/app/experience/layout.tsx.tmpl +17 -0
  242. package/templates/harness/app/experience/page.tsx.tmpl +1 -0
  243. package/templates/harness/app/experience/pages/[id]/page.tsx.tmpl +1 -0
  244. package/templates/harness/app/experience/review-queue/page.tsx.tmpl +1 -0
  245. package/templates/harness/app/experience/runs/page.tsx.tmpl +1 -0
  246. package/templates/harness/app/experience/widgets/[id]/page.tsx.tmpl +1 -0
  247. package/templates/harness/app/experience/widgets/page.tsx.tmpl +1 -0
  248. package/templates/harness/app/experience/workflows/[id]/page.tsx.tmpl +1 -0
  249. package/templates/harness/app/experience/workflows/page.tsx.tmpl +1 -0
  250. package/templates/harness/app/integrations/[slug]/connect/page.tsx.tmpl +1 -0
  251. package/templates/harness/app/integrations/[slug]/events/page.tsx.tmpl +1 -0
  252. package/templates/harness/app/integrations/[slug]/health/page.tsx.tmpl +1 -0
  253. package/templates/harness/app/integrations/[slug]/operations/[kind]/[name]/page.tsx.tmpl +1 -0
  254. package/templates/harness/app/integrations/[slug]/operations/page.tsx.tmpl +1 -0
  255. package/templates/harness/app/integrations/[slug]/page.tsx.tmpl +1 -0
  256. package/templates/harness/app/integrations/[slug]/preview/activation/page.tsx.tmpl +1 -0
  257. package/templates/harness/app/integrations/[slug]/preview/layout.tsx.tmpl +1 -0
  258. package/templates/harness/app/integrations/[slug]/preview/page.tsx.tmpl +1 -0
  259. package/templates/harness/app/integrations/[slug]/preview/tile/page.tsx.tmpl +1 -0
  260. package/templates/harness/app/integrations/[slug]/preview/triggers/page.tsx.tmpl +1 -0
  261. package/templates/harness/app/integrations/[slug]/preview/widgets/[widgetId]/page.tsx.tmpl +1 -0
  262. package/templates/harness/app/integrations/[slug]/preview/widgets/page.tsx.tmpl +1 -0
  263. package/templates/harness/app/integrations/[slug]/runs/page.tsx.tmpl +1 -0
  264. package/templates/harness/app/integrations/layout.tsx.tmpl +1 -0
  265. package/templates/harness/app/integrations/page.tsx.tmpl +1 -0
  266. package/templates/harness/app/page.experience.tsx.tmpl +5 -0
  267. package/templates/harness/repo/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +11 -0
  268. package/templates/harness/repo/app/(repo)/connections/page.tsx.tmpl +1 -0
  269. package/templates/harness/repo/app/(repo)/layout.tsx.tmpl +1 -0
  270. package/templates/harness/repo/app/(repo)/page.tsx.tmpl +1 -0
  271. package/templates/harness/repo/app/experiences/[name]/connect/page.tsx.tmpl +1 -0
  272. package/templates/harness/repo/app/experiences/[name]/layout.tsx.tmpl +9 -0
  273. package/templates/harness/repo/app/experiences/page.tsx.tmpl +5 -0
  274. package/templates/harness/repo/app/harness-shell.tsx.tmpl +9 -0
  275. package/templates/harness/repo/app/integrations/[slug]/layout.tsx.tmpl +1 -0
  276. package/templates/harness/repo/app/integrations/page.tsx.tmpl +5 -0
  277. package/templates/harness/repo/app/repo-experiences.ts.tmpl +5 -0
  278. package/templates/harness/styles/globals.css.tmpl +15 -1
  279. package/templates/package.json.tmpl +4 -4
  280. package/templates/skills/build-agent/SKILL.md +43 -10
  281. package/templates/skills/build-agent/references/agent-json.md +4 -1
  282. package/templates/skills/build-agent/references/app-setup.md +14 -7
  283. package/templates/skills/build-agent/references/rollout.md +26 -2
  284. package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
  285. package/templates/skills/build-integration/references/widget-design.md +1 -1
  286. package/templates/skills/ekanos-platform/SKILL.md +66 -14
  287. package/templates/skills/polish-widgets/references/patterns.md +3 -3
@@ -0,0 +1,115 @@
1
+ import { build } from 'esbuild';
2
+ import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { preconditionError } from '../errors.js';
7
+ const stubServerOnly = {
8
+ name: 'ekanos-stub-server-only',
9
+ setup(b) {
10
+ b.onResolve({ filter: /^server-only$/ }, () => ({
11
+ path: 'server-only',
12
+ namespace: 'ekanos-server-only-stub',
13
+ }));
14
+ b.onLoad({ filter: /.*/, namespace: 'ekanos-server-only-stub' }, () => ({
15
+ contents: '',
16
+ loader: 'js',
17
+ }));
18
+ },
19
+ };
20
+ /**
21
+ * Evaluate the experience entry in Node to read its definition, including the
22
+ * workflow templates' `build()` output. Components are loaded but never
23
+ * rendered, so a widget needs no browser to be inspected.
24
+ */
25
+ export async function loadExperience(project, options = {}) {
26
+ var _a;
27
+ const entry = path.join(project.dir, project.entry);
28
+ const nodeModules = path.join(project.dir, 'node_modules');
29
+ if (!fs.existsSync(nodeModules)) {
30
+ throw preconditionError(`No node_modules in ${project.dir}: the packages the experience imports are not installed.`, 'Install dependencies first, then re-run.');
31
+ }
32
+ const cacheDir = path.join(nodeModules, '.cache', 'ekanos-cli');
33
+ fs.mkdirSync(cacheDir, { recursive: true });
34
+ const outfile = path.join(cacheDir, `experience-${crypto.randomBytes(6).toString('hex')}.mjs`);
35
+ const inlineSdk = (_a = options.inlineSdk) !== null && _a !== void 0 ? _a : process.env.EKANOS_INLINE_SDK;
36
+ const external = inlineSdk
37
+ ? ['react', 'react-dom', '@tanstack/react-query']
38
+ : ['@ekanos/*', 'react', 'react-dom', '@tanstack/react-query', 'zod'];
39
+ const alias = inlineSdk
40
+ ? Object.fromEntries([
41
+ ['experience', 'index.ts'],
42
+ ['experience/client', 'client.tsx'],
43
+ ['experience/server', 'server.ts'],
44
+ ['experience/runtime', 'runtime.ts'],
45
+ ].map(([sub, file]) => [
46
+ `@ekanos/sdk/${sub}`,
47
+ path.join(inlineSdk, 'src', 'experience', file),
48
+ ]))
49
+ : undefined;
50
+ try {
51
+ try {
52
+ await build({
53
+ stdin: {
54
+ contents: `import definition from ${JSON.stringify(entry)};\n` +
55
+ "export { serializeWorkflowTemplate, slotsUsedBy } from '@ekanos/sdk/experience';\n" +
56
+ 'export default definition;\n',
57
+ resolveDir: project.dir,
58
+ sourcefile: 'ekanos-experience-loader.ts',
59
+ loader: 'ts',
60
+ },
61
+ outfile,
62
+ alias,
63
+ platform: 'node',
64
+ format: 'esm',
65
+ bundle: true,
66
+ jsx: 'automatic',
67
+ external,
68
+ plugins: [stubServerOnly],
69
+ loader: { '.css': 'empty', '.svg': 'dataurl', '.png': 'dataurl' },
70
+ logLevel: 'silent',
71
+ absWorkingDir: project.dir,
72
+ });
73
+ }
74
+ catch (error) {
75
+ return {
76
+ ok: false,
77
+ message: `Could not bundle ${project.entry}: ${error instanceof Error ? error.message : String(error)}`,
78
+ };
79
+ }
80
+ let mod;
81
+ try {
82
+ mod = (await import(pathToFileURL(outfile).href));
83
+ }
84
+ catch (error) {
85
+ return {
86
+ ok: false,
87
+ message: `${project.entry} threw while loading: ${error instanceof Error ? error.message : String(error)}`,
88
+ };
89
+ }
90
+ const def = mod.default;
91
+ if (!def || typeof def !== 'object' || def.kind !== 'experience') {
92
+ return {
93
+ ok: false,
94
+ message: `${project.entry} must "export default defineExperience({...})".`,
95
+ };
96
+ }
97
+ if (typeof mod.serializeWorkflowTemplate !== 'function' ||
98
+ typeof mod.slotsUsedBy !== 'function') {
99
+ return {
100
+ ok: false,
101
+ message: 'The installed @ekanos/sdk has no experience API. Upgrade it (ekanos upgrade).',
102
+ };
103
+ }
104
+ return {
105
+ ok: true,
106
+ definition: def,
107
+ serializeWorkflowTemplate: mod.serializeWorkflowTemplate,
108
+ slotsUsedBy: mod.slotsUsedBy,
109
+ };
110
+ }
111
+ finally {
112
+ fs.rmSync(outfile, { force: true });
113
+ }
114
+ }
115
+ //# sourceMappingURL=load-experience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-experience.js","sourceRoot":"","sources":["../../src/experience/load-experience.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Plugin, build } from 'esbuild';\nimport * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { preconditionError } from '../errors';\nimport type {\n LoadedExperienceDefinition,\n LoadedExperienceModule,\n} from './definition-shape';\nimport type { ExperienceProject } from './project';\n\nconst stubServerOnly: Plugin = {\n name: 'ekanos-stub-server-only',\n setup(b) {\n b.onResolve({ filter: /^server-only$/ }, () => ({\n path: 'server-only',\n namespace: 'ekanos-server-only-stub',\n }));\n b.onLoad({ filter: /.*/, namespace: 'ekanos-server-only-stub' }, () => ({\n contents: '',\n loader: 'js',\n }));\n },\n};\n\nexport interface LoadExperienceOptions {\n /**\n * Root of an `@ekanos/sdk` checkout to bundle instead of importing\n * `@ekanos/sdk` from node_modules. Needed only when the SDK's exports point\n * at TypeScript source (the monorepo's own tests); a published SDK ships\n * built output. Also settable as EKANOS_INLINE_SDK.\n */\n inlineSdk?: string;\n}\n\nexport type LoadedExperience =\n | ({ ok: true } & LoadedExperienceModule)\n | { ok: false; message: string };\n\n/**\n * Evaluate the experience entry in Node to read its definition, including the\n * workflow templates' `build()` output. Components are loaded but never\n * rendered, so a widget needs no browser to be inspected.\n */\nexport async function loadExperience(\n project: ExperienceProject,\n options: LoadExperienceOptions = {},\n): Promise<LoadedExperience> {\n const entry = path.join(project.dir, project.entry);\n const nodeModules = path.join(project.dir, 'node_modules');\n if (!fs.existsSync(nodeModules)) {\n throw preconditionError(\n `No node_modules in ${project.dir}: the packages the experience imports are not installed.`,\n 'Install dependencies first, then re-run.',\n );\n }\n\n const cacheDir = path.join(nodeModules, '.cache', 'ekanos-cli');\n fs.mkdirSync(cacheDir, { recursive: true });\n const outfile = path.join(\n cacheDir,\n `experience-${crypto.randomBytes(6).toString('hex')}.mjs`,\n );\n\n const inlineSdk = options.inlineSdk ?? process.env.EKANOS_INLINE_SDK;\n const external = inlineSdk\n ? ['react', 'react-dom', '@tanstack/react-query']\n : ['@ekanos/*', 'react', 'react-dom', '@tanstack/react-query', 'zod'];\n const alias = inlineSdk\n ? Object.fromEntries(\n [\n ['experience', 'index.ts'],\n ['experience/client', 'client.tsx'],\n ['experience/server', 'server.ts'],\n ['experience/runtime', 'runtime.ts'],\n ].map(([sub, file]) => [\n `@ekanos/sdk/${sub}`,\n path.join(inlineSdk, 'src', 'experience', file as string),\n ]),\n )\n : undefined;\n\n try {\n try {\n await build({\n stdin: {\n contents:\n `import definition from ${JSON.stringify(entry)};\\n` +\n \"export { serializeWorkflowTemplate, slotsUsedBy } from '@ekanos/sdk/experience';\\n\" +\n 'export default definition;\\n',\n resolveDir: project.dir,\n sourcefile: 'ekanos-experience-loader.ts',\n loader: 'ts',\n },\n outfile,\n alias,\n platform: 'node',\n format: 'esm',\n bundle: true,\n jsx: 'automatic',\n external,\n plugins: [stubServerOnly],\n loader: { '.css': 'empty', '.svg': 'dataurl', '.png': 'dataurl' },\n logLevel: 'silent',\n absWorkingDir: project.dir,\n });\n } catch (error) {\n return {\n ok: false,\n message: `Could not bundle ${project.entry}: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n\n let mod: Partial<LoadedExperienceModule> & { default?: unknown };\n try {\n mod = (await import(pathToFileURL(outfile).href)) as typeof mod;\n } catch (error) {\n return {\n ok: false,\n message: `${project.entry} threw while loading: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n\n const def = mod.default as { kind?: unknown } | undefined;\n if (!def || typeof def !== 'object' || def.kind !== 'experience') {\n return {\n ok: false,\n message: `${project.entry} must \"export default defineExperience({...})\".`,\n };\n }\n\n if (\n typeof mod.serializeWorkflowTemplate !== 'function' ||\n typeof mod.slotsUsedBy !== 'function'\n ) {\n return {\n ok: false,\n message:\n 'The installed @ekanos/sdk has no experience API. Upgrade it (ekanos upgrade).',\n };\n }\n\n return {\n ok: true,\n definition: def as LoadedExperienceDefinition,\n serializeWorkflowTemplate: mod.serializeWorkflowTemplate,\n slotsUsedBy: mod.slotsUsedBy,\n };\n } finally {\n fs.rmSync(outfile, { force: true });\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { type ExperienceProject } from './project.js';
2
+ /**
3
+ * What counts as an active override (`ekanos.local.json`, `EKANOS_INTEGRATION_OVERRIDE_<INTEGRATION>`,
4
+ * an unreadable file) is decided once, by the harness's `activeOverrides()`;
5
+ * `activeOverridesFor` in `dev-experience.ts` loads it from the project.
6
+ */
7
+ /** True when git tracks ekanos.local.json (it must stay per-developer). */
8
+ export declare function isOverridesFileTracked(project: ExperienceProject): boolean;
@@ -0,0 +1,21 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { LOCAL_OVERRIDES_FILE } from './project.js';
3
+ /**
4
+ * What counts as an active override (`ekanos.local.json`, `EKANOS_INTEGRATION_OVERRIDE_<INTEGRATION>`,
5
+ * an unreadable file) is decided once, by the harness's `activeOverrides()`;
6
+ * `activeOverridesFor` in `dev-experience.ts` loads it from the project.
7
+ */
8
+ /** True when git tracks ekanos.local.json (it must stay per-developer). */
9
+ export function isOverridesFileTracked(project) {
10
+ try {
11
+ execFileSync('git', ['ls-files', '--error-unmatch', LOCAL_OVERRIDES_FILE], {
12
+ cwd: project.dir,
13
+ stdio: 'ignore',
14
+ });
15
+ return true;
16
+ }
17
+ catch (_a) {
18
+ return false;
19
+ }
20
+ }
21
+ //# sourceMappingURL=overrides.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overrides.js","sourceRoot":"","sources":["../../src/experience/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAA0B,MAAM,WAAW,CAAC;AAEzE;;;;GAIG;AAEH,2EAA2E;AAC3E,MAAM,UAAU,sBAAsB,CAAC,OAA0B;IAC/D,IAAI,CAAC;QACH,YAAY,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,iBAAiB,EAAE,oBAAoB,CAAC,EAAE;YACzE,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC;IACd,CAAC;eAAO,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC","sourcesContent":["import { execFileSync } from 'node:child_process';\n\nimport { LOCAL_OVERRIDES_FILE, type ExperienceProject } from './project';\n\n/**\n * What counts as an active override (`ekanos.local.json`, `EKANOS_INTEGRATION_OVERRIDE_<INTEGRATION>`,\n * an unreadable file) is decided once, by the harness's `activeOverrides()`;\n * `activeOverridesFor` in `dev-experience.ts` loads it from the project.\n */\n\n/** True when git tracks ekanos.local.json (it must stay per-developer). */\nexport function isOverridesFileTracked(project: ExperienceProject): boolean {\n try {\n execFileSync('git', ['ls-files', '--error-unmatch', LOCAL_OVERRIDES_FILE], {\n cwd: project.dir,\n stdio: 'ignore',\n });\n\n return true;\n } catch {\n return false;\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import type { Contract, IntegrationEntry } from '@ekanos/integration-schema';
2
+ import type { ExperienceProject } from './project.js';
3
+ /** Write the contract snapshot and lock entry for one integration. Returns what changed. */
4
+ export declare function pinIntegration(project: ExperienceProject, use: IntegrationEntry, contract: Contract): {
5
+ snapshotChanged: boolean;
6
+ lockChanged: boolean;
7
+ };
8
+ export declare const relativeTo: (project: ExperienceProject, file: string) => string;
@@ -0,0 +1,17 @@
1
+ import * as path from 'node:path';
2
+ import { snapshotFile } from './contract-sources.js';
3
+ import { formatJson, readLockfile, writeIfChanged, writeLockfile, } from './project.js';
4
+ /** Write the contract snapshot and lock entry for one integration. Returns what changed. */
5
+ export function pinIntegration(project, use, contract) {
6
+ const snapshotChanged = writeIfChanged(snapshotFile(project, use.name, use.version), formatJson(contract));
7
+ const lock = readLockfile(project);
8
+ lock.integrations[use.name] = {
9
+ version: use.version,
10
+ contractVersion: contract.contractVersion,
11
+ digest: contract.digest,
12
+ };
13
+ const lockChanged = writeLockfile(project, lock);
14
+ return { snapshotChanged, lockChanged };
15
+ }
16
+ export const relativeTo = (project, file) => path.relative(project.dir, file).split(path.sep).join('/');
17
+ //# sourceMappingURL=pin-integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pin-integration.js","sourceRoot":"","sources":["../../src/experience/pin-integration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,GACd,MAAM,WAAW,CAAC;AAGnB,4FAA4F;AAC5F,MAAM,UAAU,cAAc,CAC5B,OAA0B,EAC1B,GAAqB,EACrB,QAAkB;IAElB,MAAM,eAAe,GAAG,cAAc,CACpC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,EAC5C,UAAU,CAAC,QAAQ,CAAC,CACrB,CAAC;IAEF,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG;QAC5B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,eAAe,EAAE,QAAQ,CAAC,eAAe;QACzC,MAAM,EAAE,QAAQ,CAAC,MAAM;KACxB,CAAC;IACF,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAEjD,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAA0B,EAAE,IAAY,EAAE,EAAE,CACrE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC","sourcesContent":["import type { Contract, IntegrationEntry } from '@ekanos/integration-schema';\nimport * as path from 'node:path';\n\nimport { snapshotFile } from './contract-sources';\nimport {\n formatJson,\n readLockfile,\n writeIfChanged,\n writeLockfile,\n} from './project';\nimport type { ExperienceProject } from './project';\n\n/** Write the contract snapshot and lock entry for one integration. Returns what changed. */\nexport function pinIntegration(\n project: ExperienceProject,\n use: IntegrationEntry,\n contract: Contract,\n): { snapshotChanged: boolean; lockChanged: boolean } {\n const snapshotChanged = writeIfChanged(\n snapshotFile(project, use.name, use.version),\n formatJson(contract),\n );\n\n const lock = readLockfile(project);\n lock.integrations[use.name] = {\n version: use.version,\n contractVersion: contract.contractVersion,\n digest: contract.digest,\n };\n const lockChanged = writeLockfile(project, lock);\n\n return { snapshotChanged, lockChanged };\n}\n\nexport const relativeTo = (project: ExperienceProject, file: string) =>\n path.relative(project.dir, file).split(path.sep).join('/');\n"]}
@@ -0,0 +1,12 @@
1
+ import type { Contract, IntegrationEntry } from '@ekanos/integration-schema';
2
+ import { operationId } from '@ekanos/integration-schema';
3
+ /**
4
+ * Turn `--ops` tokens into canonical operation ids of `contract`. A token is a
5
+ * canonical id, `kind/name`, or a bare `name` that exactly one operation has.
6
+ */
7
+ export declare function resolveOpTokens(contract: Contract, tokens: readonly string[]): string[];
8
+ export declare function readOperationIds(contract: Contract): string[];
9
+ /** Stable order: contract order, so reruns never reshuffle ekanos.json. */
10
+ export declare function inContractOrder(contract: Contract, ids: Iterable<string>): string[];
11
+ export { operationId };
12
+ export type { IntegrationEntry };
@@ -0,0 +1,36 @@
1
+ import { operationId } from '@ekanos/integration-schema';
2
+ import { usageError } from '../errors.js';
3
+ /**
4
+ * Turn `--ops` tokens into canonical operation ids of `contract`. A token is a
5
+ * canonical id, `kind/name`, or a bare `name` that exactly one operation has.
6
+ */
7
+ export function resolveOpTokens(contract, tokens) {
8
+ const ids = [];
9
+ for (const token of tokens) {
10
+ const exact = contract.operations.find((op) => op.id === token || `${op.kind}/${op.name}` === token);
11
+ const byName = contract.operations.filter((op) => op.name === token);
12
+ const found = exact !== null && exact !== void 0 ? exact : (byName.length === 1 ? byName[0] : undefined);
13
+ if (!found) {
14
+ const known = contract.operations
15
+ .map((op) => `${op.kind}/${op.name}`)
16
+ .join(', ');
17
+ throw usageError(byName.length > 1
18
+ ? `"${token}" is ambiguous in ${contract.integration}@${contract.compatLine}; write tool/${token} or action/${token}.`
19
+ : `${contract.integration}@${contract.compatLine} has no operation "${token}".`, `Operations: ${known}.`);
20
+ }
21
+ ids.push(found.id);
22
+ }
23
+ return ids;
24
+ }
25
+ export function readOperationIds(contract) {
26
+ return contract.operations
27
+ .filter((op) => op.effect === 'read')
28
+ .map((op) => op.id);
29
+ }
30
+ /** Stable order: contract order, so reruns never reshuffle ekanos.json. */
31
+ export function inContractOrder(contract, ids) {
32
+ const wanted = new Set(ids);
33
+ return contract.operations.map((op) => op.id).filter((id) => wanted.has(id));
34
+ }
35
+ export { operationId };
36
+ //# sourceMappingURL=pin-ops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pin-ops.js","sourceRoot":"","sources":["../../src/experience/pin-ops.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAkB,EAClB,MAAyB;IAEzB,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CACpC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,CAC7D,CAAC;QACF,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC;QACrE,MAAM,KAAK,GAAG,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACrE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU;iBAC9B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;iBACpC,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,MAAM,UAAU,CACd,MAAM,CAAC,MAAM,GAAG,CAAC;gBACf,CAAC,CAAC,IAAI,KAAK,qBAAqB,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,UAAU,gBAAgB,KAAK,cAAc,KAAK,GAAG;gBACtH,CAAC,CAAC,GAAG,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,UAAU,sBAAsB,KAAK,IAAI,EACjF,eAAe,KAAK,GAAG,CACxB,CAAC;QACJ,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,OAAO,QAAQ,CAAC,UAAU;SACvB,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,KAAK,MAAM,CAAC;SACpC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AACxB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAC7B,QAAkB,EAClB,GAAqB;IAErB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAE5B,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,CAAC","sourcesContent":["import type { Contract, IntegrationEntry } from '@ekanos/integration-schema';\nimport { operationId } from '@ekanos/integration-schema';\n\nimport { usageError } from '../errors';\n\n/**\n * Turn `--ops` tokens into canonical operation ids of `contract`. A token is a\n * canonical id, `kind/name`, or a bare `name` that exactly one operation has.\n */\nexport function resolveOpTokens(\n contract: Contract,\n tokens: readonly string[],\n): string[] {\n const ids: string[] = [];\n for (const token of tokens) {\n const exact = contract.operations.find(\n (op) => op.id === token || `${op.kind}/${op.name}` === token,\n );\n const byName = contract.operations.filter((op) => op.name === token);\n const found = exact ?? (byName.length === 1 ? byName[0] : undefined);\n if (!found) {\n const known = contract.operations\n .map((op) => `${op.kind}/${op.name}`)\n .join(', ');\n throw usageError(\n byName.length > 1\n ? `\"${token}\" is ambiguous in ${contract.integration}@${contract.compatLine}; write tool/${token} or action/${token}.`\n : `${contract.integration}@${contract.compatLine} has no operation \"${token}\".`,\n `Operations: ${known}.`,\n );\n }\n ids.push(found.id);\n }\n\n return ids;\n}\n\nexport function readOperationIds(contract: Contract): string[] {\n return contract.operations\n .filter((op) => op.effect === 'read')\n .map((op) => op.id);\n}\n\n/** Stable order: contract order, so reruns never reshuffle ekanos.json. */\nexport function inContractOrder(\n contract: Contract,\n ids: Iterable<string>,\n): string[] {\n const wanted = new Set(ids);\n\n return contract.operations.map((op) => op.id).filter((id) => wanted.has(id));\n}\n\nexport { operationId };\nexport type { IntegrationEntry };\n"]}
@@ -0,0 +1,35 @@
1
+ import { type Contract, type Finding, type Lockfile, type IntegrationEntry } from '@ekanos/integration-schema';
2
+ import type { ExperienceProject } from './project.js';
3
+ export declare const INTEGRATION_PINS_CHECK = "checks/integration-pins";
4
+ export declare const DEPRECATED_USES = "\"uses\" in ekanos.json is deprecated: an experience declares its dependencies as \"integrations\" (name = the integration, version = the compat line). \"ekanos sync\" rewrites the file.";
5
+ /** What a `uses` -> `integrations` upgrade changed in code terms, or null when nothing was renamed. */
6
+ export declare function renamesNotice(renames: Record<string, string>): string | null;
7
+ export interface PinnedIntegration {
8
+ use: IntegrationEntry;
9
+ contract: Contract;
10
+ }
11
+ /**
12
+ * Resolves an integration's contract through the harness registry chain (catalog,
13
+ * published-package cache, contracts directory), as `createPinsResolver`
14
+ * returns it. `null` falls back to the committed snapshot.
15
+ */
16
+ export type PinsResolver = (integration: {
17
+ name: string;
18
+ version: string;
19
+ }) => {
20
+ contract: Contract;
21
+ lines: string[];
22
+ } | null;
23
+ export declare function loadPinnedContract(project: ExperienceProject, use: IntegrationEntry): Contract | null;
24
+ /**
25
+ * Every `integrations` entry must resolve, against the pinned contract snapshot, to
26
+ * the EXACT compat line and contain each declared operation, with the digest
27
+ * the lockfile recorded. Fails with the first-principles message naming the
28
+ * integration, so "pinning a line missing an op" is a one-line fix.
29
+ */
30
+ export declare function checkIntegrationPins(project: ExperienceProject, lock: Lockfile, resolve?: PinsResolver): {
31
+ findings: Finding[];
32
+ pinned: PinnedIntegration[];
33
+ };
34
+ /** The effect of `integration` + `kind/name` from the pinned contracts, for workflow serialization. */
35
+ export declare function effectLookup(pinned: readonly PinnedIntegration[]): (integration: string, op: string) => "read" | "write" | undefined;
@@ -0,0 +1,91 @@
1
+ import { operationKey, verifyContract, } from '@ekanos/integration-schema';
2
+ import * as fs from 'node:fs';
3
+ import { snapshotFile } from './contract-sources.js';
4
+ export const INTEGRATION_PINS_CHECK = 'checks/integration-pins';
5
+ export const DEPRECATED_USES = '"uses" in ekanos.json is deprecated: an experience declares its dependencies as "integrations" (name = the integration, version = the compat line). "ekanos sync" rewrites the file.';
6
+ /** What a `uses` -> `integrations` upgrade changed in code terms, or null when nothing was renamed. */
7
+ export function renamesNotice(renames) {
8
+ const entries = Object.entries(renames);
9
+ if (entries.length === 0)
10
+ return null;
11
+ return `An integration is now addressed by its own name; update your code: ${entries
12
+ .map(([alias, integration]) => `'${alias}' -> '${integration}'`)
13
+ .join(', ')}.`;
14
+ }
15
+ export function loadPinnedContract(project, use) {
16
+ const file = snapshotFile(project, use.name, use.version);
17
+ if (!fs.existsSync(file))
18
+ return null;
19
+ try {
20
+ return verifyContract(JSON.parse(fs.readFileSync(file, 'utf8'))).contract;
21
+ }
22
+ catch (_a) {
23
+ return null;
24
+ }
25
+ }
26
+ const finding = (message, hint, file) => (Object.assign({ check: INTEGRATION_PINS_CHECK, severity: 'error', message,
27
+ hint }, (file ? { file } : {})));
28
+ /**
29
+ * Every `integrations` entry must resolve, against the pinned contract snapshot, to
30
+ * the EXACT compat line and contain each declared operation, with the digest
31
+ * the lockfile recorded. Fails with the first-principles message naming the
32
+ * integration, so "pinning a line missing an op" is a one-line fix.
33
+ */
34
+ export function checkIntegrationPins(project, lock, resolve) {
35
+ var _a;
36
+ var _b, _c;
37
+ const findings = [];
38
+ const pinned = [];
39
+ if (project.deprecatedUses) {
40
+ findings.push({
41
+ check: INTEGRATION_PINS_CHECK,
42
+ severity: 'warn',
43
+ message: DEPRECATED_USES,
44
+ hint: 'Run "ekanos sync".',
45
+ file: 'ekanos.json',
46
+ });
47
+ }
48
+ if (project.integrations.length === 0) {
49
+ findings.push(finding('ekanos.json declares no integrations.', 'Add one with "ekanos add <integration>@<version> --from-dir <contracts folder>".', 'ekanos.json'));
50
+ }
51
+ for (const use of project.integrations) {
52
+ const contract = (_b = (_a = resolve === null || resolve === void 0 ? void 0 : resolve({ name: use.name, version: use.version })) === null || _a === void 0 ? void 0 : _a.contract) !== null && _b !== void 0 ? _b : loadPinnedContract(project, use);
53
+ if (!contract) {
54
+ findings.push(finding(`integration "${use.name}": no pinned contract for ${use.name}@${use.version}.`, `Run "ekanos add ${use.name}@${use.version} --from-dir <contracts folder>" or "ekanos sync".`, 'ekanos.json'));
55
+ continue;
56
+ }
57
+ if (contract.compatLine !== use.version) {
58
+ findings.push(finding(`integration "${use.name}" pins ${use.name}@${use.version} but the pinned contract is version ${contract.compatLine}.`, 'Re-pin with "ekanos sync".', 'ekanos.json'));
59
+ continue;
60
+ }
61
+ const byId = new Map(contract.operations.map((op) => [op.id, op]));
62
+ for (const id of use.operations) {
63
+ const op = byId.get(id);
64
+ if (!op) {
65
+ const key = (_c = operationKey(id)) !== null && _c !== void 0 ? _c : id;
66
+ findings.push(finding(`integration "${use.name}": ${use.name}@${use.version} does not provide ${key}.`, `Remove ${key} from the integration's operations, or pin a version that has it (known versions come from "ekanos add").`, 'ekanos.json'));
67
+ }
68
+ }
69
+ const locked = lock.integrations[use.name];
70
+ if (!locked) {
71
+ findings.push(finding(`integration "${use.name}" is not in ekanos.lock.json.`, 'Run "ekanos sync" to record the pin.', 'ekanos.lock.json'));
72
+ }
73
+ else if (locked.digest !== contract.digest ||
74
+ locked.version !== use.version) {
75
+ findings.push(finding(`integration "${use.name}": the pinned contract (${contract.digest.slice(0, 15)}…) does not match ekanos.lock.json (${locked.digest.slice(0, 15)}…).`, 'Run "ekanos sync" to accept the new contract, or restore the old snapshot.', 'ekanos.lock.json'));
76
+ }
77
+ pinned.push({ use, contract });
78
+ }
79
+ return { findings, pinned };
80
+ }
81
+ /** The effect of `integration` + `kind/name` from the pinned contracts, for workflow serialization. */
82
+ export function effectLookup(pinned) {
83
+ const effects = new Map();
84
+ for (const { use, contract } of pinned) {
85
+ for (const op of contract.operations) {
86
+ effects.set(`${use.name} ${op.kind}/${op.name}`, op.effect);
87
+ }
88
+ }
89
+ return (integration, op) => effects.get(`${integration} ${op}`);
90
+ }
91
+ //# sourceMappingURL=pins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pins.js","sourceRoot":"","sources":["../../src/experience/pins.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Contract,\n type Finding,\n type Lockfile,\n type IntegrationEntry,\n operationKey,\n verifyContract,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\n\nimport { snapshotFile } from './contract-sources';\nimport type { ExperienceProject } from './project';\n\nexport const INTEGRATION_PINS_CHECK = 'checks/integration-pins';\n\nexport const DEPRECATED_USES =\n '\"uses\" in ekanos.json is deprecated: an experience declares its dependencies as \"integrations\" (name = the integration, version = the compat line). \"ekanos sync\" rewrites the file.';\n\n/** What a `uses` -> `integrations` upgrade changed in code terms, or null when nothing was renamed. */\nexport function renamesNotice(renames: Record<string, string>): string | null {\n const entries = Object.entries(renames);\n if (entries.length === 0) return null;\n\n return `An integration is now addressed by its own name; update your code: ${entries\n .map(([alias, integration]) => `'${alias}' -> '${integration}'`)\n .join(', ')}.`;\n}\n\nexport interface PinnedIntegration {\n use: IntegrationEntry;\n contract: Contract;\n}\n\n/**\n * Resolves an integration's contract through the harness registry chain (catalog,\n * published-package cache, contracts directory), as `createPinsResolver`\n * returns it. `null` falls back to the committed snapshot.\n */\nexport type PinsResolver = (integration: {\n name: string;\n version: string;\n}) => { contract: Contract; lines: string[] } | null;\n\nexport function loadPinnedContract(\n project: ExperienceProject,\n use: IntegrationEntry,\n): Contract | null {\n const file = snapshotFile(project, use.name, use.version);\n if (!fs.existsSync(file)) return null;\n try {\n return verifyContract(JSON.parse(fs.readFileSync(file, 'utf8'))).contract;\n } catch {\n return null;\n }\n}\n\nconst finding = (message: string, hint: string, file?: string): Finding => ({\n check: INTEGRATION_PINS_CHECK,\n severity: 'error',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\n/**\n * Every `integrations` entry must resolve, against the pinned contract snapshot, to\n * the EXACT compat line and contain each declared operation, with the digest\n * the lockfile recorded. Fails with the first-principles message naming the\n * integration, so \"pinning a line missing an op\" is a one-line fix.\n */\nexport function checkIntegrationPins(\n project: ExperienceProject,\n lock: Lockfile,\n resolve?: PinsResolver,\n): { findings: Finding[]; pinned: PinnedIntegration[] } {\n const findings: Finding[] = [];\n const pinned: PinnedIntegration[] = [];\n\n if (project.deprecatedUses) {\n findings.push({\n check: INTEGRATION_PINS_CHECK,\n severity: 'warn',\n message: DEPRECATED_USES,\n hint: 'Run \"ekanos sync\".',\n file: 'ekanos.json',\n });\n }\n\n if (project.integrations.length === 0) {\n findings.push(\n finding(\n 'ekanos.json declares no integrations.',\n 'Add one with \"ekanos add <integration>@<version> --from-dir <contracts folder>\".',\n 'ekanos.json',\n ),\n );\n }\n\n for (const use of project.integrations) {\n const contract =\n resolve?.({ name: use.name, version: use.version })?.contract ??\n loadPinnedContract(project, use);\n if (!contract) {\n findings.push(\n finding(\n `integration \"${use.name}\": no pinned contract for ${use.name}@${use.version}.`,\n `Run \"ekanos add ${use.name}@${use.version} --from-dir <contracts folder>\" or \"ekanos sync\".`,\n 'ekanos.json',\n ),\n );\n continue;\n }\n if (contract.compatLine !== use.version) {\n findings.push(\n finding(\n `integration \"${use.name}\" pins ${use.name}@${use.version} but the pinned contract is version ${contract.compatLine}.`,\n 'Re-pin with \"ekanos sync\".',\n 'ekanos.json',\n ),\n );\n continue;\n }\n\n const byId = new Map(contract.operations.map((op) => [op.id, op]));\n for (const id of use.operations) {\n const op = byId.get(id);\n if (!op) {\n const key = operationKey(id) ?? id;\n findings.push(\n finding(\n `integration \"${use.name}\": ${use.name}@${use.version} does not provide ${key}.`,\n `Remove ${key} from the integration's operations, or pin a version that has it (known versions come from \"ekanos add\").`,\n 'ekanos.json',\n ),\n );\n }\n }\n\n const locked = lock.integrations[use.name];\n if (!locked) {\n findings.push(\n finding(\n `integration \"${use.name}\" is not in ekanos.lock.json.`,\n 'Run \"ekanos sync\" to record the pin.',\n 'ekanos.lock.json',\n ),\n );\n } else if (\n locked.digest !== contract.digest ||\n locked.version !== use.version\n ) {\n findings.push(\n finding(\n `integration \"${use.name}\": the pinned contract (${contract.digest.slice(0, 15)}…) does not match ekanos.lock.json (${locked.digest.slice(0, 15)}…).`,\n 'Run \"ekanos sync\" to accept the new contract, or restore the old snapshot.',\n 'ekanos.lock.json',\n ),\n );\n }\n\n pinned.push({ use, contract });\n }\n\n return { findings, pinned };\n}\n\n/** The effect of `integration` + `kind/name` from the pinned contracts, for workflow serialization. */\nexport function effectLookup(pinned: readonly PinnedIntegration[]) {\n const effects = new Map<string, 'read' | 'write'>();\n for (const { use, contract } of pinned) {\n for (const op of contract.operations) {\n effects.set(`${use.name} ${op.kind}/${op.name}`, op.effect);\n }\n }\n\n return (integration: string, op: string) =>\n effects.get(`${integration} ${op}`);\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import { type EkanosJson, type IntegrationEntry, type Lockfile } from '@ekanos/integration-schema';
2
+ export declare const EKANOS_JSON = "ekanos.json";
3
+ export declare const LOCK_FILE = "ekanos.lock.json";
4
+ export declare const LOCAL_OVERRIDES_FILE = "ekanos.local.json";
5
+ export declare const DEFAULT_CONTRACTS_DIR = "contracts";
6
+ export declare const INTEGRATIONS_DTS = "src/.ekanos/integrations.d.ts";
7
+ /** The previous name of the generated file, kept as a re-export for one version. */
8
+ export declare const LEGACY_SLOTS_DTS = "src/.ekanos/slots.d.ts";
9
+ export interface ExperienceProject {
10
+ /** Absolute project directory. */
11
+ dir: string;
12
+ ekanos: EkanosJson;
13
+ slug: string;
14
+ entry: string;
15
+ /** The integrations it depends on; operations are canonical ids. */
16
+ integrations: IntegrationEntry[];
17
+ /** True when ekanos.json still uses the deprecated `uses` key. */
18
+ deprecatedUses: boolean;
19
+ /** Old `uses` alias -> integration, where they differ (for the sync notice). */
20
+ renames: Record<string, string>;
21
+ /** Absolute directory holding the pinned contract snapshots. */
22
+ contractsDir: string;
23
+ /** The raw ekanos.json, key order preserved, for rewrites. */
24
+ raw: Record<string, unknown>;
25
+ }
26
+ /** `integration` when ekanos.json is absent or says so; used to route commands. */
27
+ export declare function readProjectKind(dir: string): 'integration' | 'experience';
28
+ /**
29
+ * Read and validate an experience's ekanos.json with the shared schema.
30
+ *
31
+ * A fresh scaffold has no integrations yet (you add them with `ekanos add`),
32
+ * which the shared schema rejects ("declares at least one integration"). That
33
+ * one rule is suspended here so `add` can bootstrap a project; `check` and
34
+ * `build` report the empty state themselves.
35
+ *
36
+ * The deprecated `uses` shape is read too: its entries come back as
37
+ * `integrations` (alias dropped) with `deprecatedUses` set and `renames`
38
+ * mapping each old alias to its integration, so `ekanos sync` can rewrite the
39
+ * file and say what code to update.
40
+ */
41
+ export declare function readExperienceProject(dir: string): ExperienceProject;
42
+ /** 2-space JSON with a trailing newline: the format every written file uses. */
43
+ export declare function formatJson(value: unknown): string;
44
+ /** Write only when the bytes differ. Returns whether the file changed. */
45
+ export declare function writeIfChanged(file: string, content: string): boolean;
46
+ export declare function readLockfile(project: ExperienceProject): Lockfile;
47
+ export declare function writeLockfile(project: ExperienceProject, lock: Lockfile): boolean;
48
+ /**
49
+ * Rewrite `integrations` in ekanos.json (short operation form), leaving every
50
+ * other key where it was and dropping the deprecated `uses`.
51
+ */
52
+ export declare function writeIntegrations(project: ExperienceProject, integrations: IntegrationEntry[]): boolean;
@@ -0,0 +1,149 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { EkanosJsonSchema, LockfileSchema, canonicalOperationId, experienceIntegrations, shortOperation, } from '@ekanos/integration-schema';
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { notFoundError, validationError } from '../errors.js';
16
+ export const EKANOS_JSON = 'ekanos.json';
17
+ export const LOCK_FILE = 'ekanos.lock.json';
18
+ export const LOCAL_OVERRIDES_FILE = 'ekanos.local.json';
19
+ export const DEFAULT_CONTRACTS_DIR = 'contracts';
20
+ export const INTEGRATIONS_DTS = 'src/.ekanos/integrations.d.ts';
21
+ /** The previous name of the generated file, kept as a re-export for one version. */
22
+ export const LEGACY_SLOTS_DTS = 'src/.ekanos/slots.d.ts';
23
+ function readJson(file) {
24
+ let text;
25
+ try {
26
+ text = fs.readFileSync(file, 'utf8');
27
+ }
28
+ catch (_a) {
29
+ throw notFoundError(`No ${path.basename(file)} at ${file}.`, 'Run this from an Ekanos project directory, or pass --dir <path>.');
30
+ }
31
+ try {
32
+ return JSON.parse(text);
33
+ }
34
+ catch (error) {
35
+ throw validationError(`${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, 'Fix the syntax error and re-run.');
36
+ }
37
+ }
38
+ /** `integration` when ekanos.json is absent or says so; used to route commands. */
39
+ export function readProjectKind(dir) {
40
+ try {
41
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, EKANOS_JSON), 'utf8'));
42
+ return raw.kind === 'experience' ? 'experience' : 'integration';
43
+ }
44
+ catch (_a) {
45
+ return 'integration';
46
+ }
47
+ }
48
+ /**
49
+ * Read and validate an experience's ekanos.json with the shared schema.
50
+ *
51
+ * A fresh scaffold has no integrations yet (you add them with `ekanos add`),
52
+ * which the shared schema rejects ("declares at least one integration"). That
53
+ * one rule is suspended here so `add` can bootstrap a project; `check` and
54
+ * `build` report the empty state themselves.
55
+ *
56
+ * The deprecated `uses` shape is read too: its entries come back as
57
+ * `integrations` (alias dropped) with `deprecatedUses` set and `renames`
58
+ * mapping each old alias to its integration, so `ekanos sync` can rewrite the
59
+ * file and say what code to update.
60
+ */
61
+ export function readExperienceProject(dir) {
62
+ var _a;
63
+ const absolute = path.resolve(dir);
64
+ const raw = readJson(path.join(absolute, EKANOS_JSON));
65
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
66
+ throw validationError(`${EKANOS_JSON} must be a JSON object.`, 'Fix it.');
67
+ }
68
+ const record = raw;
69
+ if (record.kind !== 'experience') {
70
+ throw validationError(`${EKANOS_JSON} is not an experience ("kind": "experience").`, 'Run experience commands in an experience project; scaffold one with "ekanos init --kind experience --slug <slug>".');
71
+ }
72
+ const declaredNone = !(Array.isArray(record.integrations) && record.integrations.length > 0) &&
73
+ !(Array.isArray(record.uses) && record.uses.length > 0);
74
+ const probe = declaredNone
75
+ ? Object.assign(Object.assign({}, record), { integrations: [PLACEHOLDER_INTEGRATION], uses: undefined }) : record;
76
+ const parsed = EkanosJsonSchema.safeParse(probe);
77
+ if (!parsed.success) {
78
+ throw validationError(`${EKANOS_JSON}: ${parsed.error.issues
79
+ .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
80
+ .join('; ')}`, 'Fix ekanos.json; the experience fields are slug, entry, integrations, contractsDir, allowImports, ignore.');
81
+ }
82
+ const ekanos = parsed.data;
83
+ const declared = declaredNone
84
+ ? { entries: [], deprecatedUses: false, renames: {} }
85
+ : experienceIntegrations(ekanos);
86
+ return {
87
+ dir: absolute,
88
+ ekanos,
89
+ slug: ekanos.slug,
90
+ entry: ekanos.entry,
91
+ integrations: declared.entries.map(withCanonicalOperations),
92
+ deprecatedUses: declared.deprecatedUses,
93
+ renames: declared.renames,
94
+ contractsDir: path.resolve(absolute, (_a = ekanos.contractsDir) !== null && _a !== void 0 ? _a : DEFAULT_CONTRACTS_DIR),
95
+ raw: record,
96
+ };
97
+ }
98
+ /** Operations as canonical `<integration>@<version>/<kind>/<name>` ids, for lookups against a contract. */
99
+ function withCanonicalOperations(entry) {
100
+ return Object.assign(Object.assign({}, entry), { operations: entry.operations.map((id) => canonicalOperationId(entry, id)) });
101
+ }
102
+ /** 2-space JSON with a trailing newline: the format every written file uses. */
103
+ export function formatJson(value) {
104
+ return `${JSON.stringify(value, null, 2)}\n`;
105
+ }
106
+ /** Write only when the bytes differ. Returns whether the file changed. */
107
+ export function writeIfChanged(file, content) {
108
+ try {
109
+ if (fs.readFileSync(file, 'utf8') === content)
110
+ return false;
111
+ }
112
+ catch (_a) {
113
+ // missing: write below
114
+ }
115
+ fs.mkdirSync(path.dirname(file), { recursive: true });
116
+ fs.writeFileSync(file, content);
117
+ return true;
118
+ }
119
+ export function readLockfile(project) {
120
+ const file = path.join(project.dir, LOCK_FILE);
121
+ if (!fs.existsSync(file))
122
+ return { lockfileVersion: 1, integrations: {} };
123
+ const parsed = LockfileSchema.safeParse(readJson(file));
124
+ if (!parsed.success) {
125
+ throw validationError(`${LOCK_FILE}: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`, 'Regenerate it with "ekanos sync".');
126
+ }
127
+ return parsed.data;
128
+ }
129
+ export function writeLockfile(project, lock) {
130
+ const integrations = Object.fromEntries(Object.entries(lock.integrations).sort(([a], [b]) => (a < b ? -1 : 1)));
131
+ return writeIfChanged(path.join(project.dir, LOCK_FILE), formatJson({ lockfileVersion: 1, integrations }));
132
+ }
133
+ const PLACEHOLDER_INTEGRATION = {
134
+ name: 'pending',
135
+ version: '0',
136
+ operations: ['tool/pending'],
137
+ };
138
+ /**
139
+ * Rewrite `integrations` in ekanos.json (short operation form), leaving every
140
+ * other key where it was and dropping the deprecated `uses`.
141
+ */
142
+ export function writeIntegrations(project, integrations) {
143
+ const _a = project.raw, { uses: _uses } = _a, rest = __rest(_a, ["uses"]);
144
+ const next = Object.assign(Object.assign({}, rest), { integrations: integrations.map((entry) => (Object.assign(Object.assign({ name: entry.name, version: entry.version, operations: entry.operations.map(shortOperation) }, (entry.required === 'required' ? {} : { required: entry.required })), (entry.modes.length === 1 && entry.modes[0] === 'team'
145
+ ? {}
146
+ : { modes: entry.modes })))) });
147
+ return writeIfChanged(path.join(project.dir, EKANOS_JSON), formatJson(next));
148
+ }
149
+ //# sourceMappingURL=project.js.map