@ekanos/cli 0.16.0 → 0.17.1

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 (192) hide show
  1. package/README.md +62 -4
  2. package/dist/commands/add.d.ts +2 -0
  3. package/dist/commands/add.js +118 -0
  4. package/dist/commands/add.js.map +1 -0
  5. package/dist/commands/audiences.d.ts +31 -0
  6. package/dist/commands/audiences.js +239 -0
  7. package/dist/commands/audiences.js.map +1 -0
  8. package/dist/commands/build.d.ts +2 -0
  9. package/dist/commands/build.js +41 -0
  10. package/dist/commands/build.js.map +1 -0
  11. package/dist/commands/check.d.ts +2 -0
  12. package/dist/commands/check.js +43 -0
  13. package/dist/commands/check.js.map +1 -0
  14. package/dist/commands/codegen.d.ts +2 -0
  15. package/dist/commands/codegen.js +49 -0
  16. package/dist/commands/codegen.js.map +1 -0
  17. package/dist/commands/contract.d.ts +52 -0
  18. package/dist/commands/contract.js +300 -0
  19. package/dist/commands/contract.js.map +1 -0
  20. package/dist/commands/dev.d.ts +7 -0
  21. package/dist/commands/dev.js +186 -12
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/index-experience.d.ts +31 -0
  24. package/dist/commands/index-experience.js +21 -0
  25. package/dist/commands/index-experience.js.map +1 -0
  26. package/dist/commands/init.d.ts +7 -0
  27. package/dist/commands/init.js +12 -0
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/new.d.ts +2 -0
  30. package/dist/commands/new.js +39 -0
  31. package/dist/commands/new.js.map +1 -0
  32. package/dist/commands/publish.d.ts +59 -0
  33. package/dist/commands/publish.js +118 -65
  34. package/dist/commands/publish.js.map +1 -1
  35. package/dist/commands/scope-notice.d.ts +6 -0
  36. package/dist/commands/scope-notice.js +15 -0
  37. package/dist/commands/scope-notice.js.map +1 -1
  38. package/dist/commands/sources.js +2 -0
  39. package/dist/commands/sources.js.map +1 -1
  40. package/dist/commands/sync.d.ts +2 -0
  41. package/dist/commands/sync.js +150 -0
  42. package/dist/commands/sync.js.map +1 -0
  43. package/dist/commands/teams-audience.d.ts +12 -0
  44. package/dist/commands/teams-audience.js +144 -0
  45. package/dist/commands/teams-audience.js.map +1 -0
  46. package/dist/commands/teams-scope.js +4 -0
  47. package/dist/commands/teams-scope.js.map +1 -1
  48. package/dist/commands/teams.d.ts +1 -1
  49. package/dist/commands/teams.js +6 -1
  50. package/dist/commands/teams.js.map +1 -1
  51. package/dist/compatibility.d.ts +10 -1
  52. package/dist/compatibility.js +10 -1
  53. package/dist/compatibility.js.map +1 -1
  54. package/dist/dev-experience.d.ts +125 -0
  55. package/dist/dev-experience.js +128 -0
  56. package/dist/dev-experience.js.map +1 -0
  57. package/dist/experience/boundary.d.ts +12 -0
  58. package/dist/experience/boundary.js +260 -0
  59. package/dist/experience/boundary.js.map +1 -0
  60. package/dist/experience/build.d.ts +16 -0
  61. package/dist/experience/build.js +90 -0
  62. package/dist/experience/build.js.map +1 -0
  63. package/dist/experience/checks.d.ts +29 -0
  64. package/dist/experience/checks.js +133 -0
  65. package/dist/experience/checks.js.map +1 -0
  66. package/dist/experience/contract-sources.d.ts +36 -0
  67. package/dist/experience/contract-sources.js +111 -0
  68. package/dist/experience/contract-sources.js.map +1 -0
  69. package/dist/experience/definition-checks.d.ts +26 -0
  70. package/dist/experience/definition-checks.js +201 -0
  71. package/dist/experience/definition-checks.js.map +1 -0
  72. package/dist/experience/definition-shape.d.ts +60 -0
  73. package/dist/experience/definition-shape.js +2 -0
  74. package/dist/experience/definition-shape.js.map +1 -0
  75. package/dist/experience/init.d.ts +10 -0
  76. package/dist/experience/init.js +64 -0
  77. package/dist/experience/init.js.map +1 -0
  78. package/dist/experience/integrations-codegen.d.ts +17 -0
  79. package/dist/experience/integrations-codegen.js +51 -0
  80. package/dist/experience/integrations-codegen.js.map +1 -0
  81. package/dist/experience/json-schema-to-ts.d.ts +1 -0
  82. package/dist/experience/json-schema-to-ts.js +90 -0
  83. package/dist/experience/json-schema-to-ts.js.map +1 -0
  84. package/dist/experience/load-experience.d.ts +23 -0
  85. package/dist/experience/load-experience.js +115 -0
  86. package/dist/experience/load-experience.js.map +1 -0
  87. package/dist/experience/overrides.d.ts +8 -0
  88. package/dist/experience/overrides.js +21 -0
  89. package/dist/experience/overrides.js.map +1 -0
  90. package/dist/experience/pin-integration.d.ts +8 -0
  91. package/dist/experience/pin-integration.js +17 -0
  92. package/dist/experience/pin-integration.js.map +1 -0
  93. package/dist/experience/pin-ops.d.ts +12 -0
  94. package/dist/experience/pin-ops.js +36 -0
  95. package/dist/experience/pin-ops.js.map +1 -0
  96. package/dist/experience/pins.d.ts +35 -0
  97. package/dist/experience/pins.js +91 -0
  98. package/dist/experience/pins.js.map +1 -0
  99. package/dist/experience/project.d.ts +52 -0
  100. package/dist/experience/project.js +149 -0
  101. package/dist/experience/project.js.map +1 -0
  102. package/dist/experience/publish.d.ts +51 -0
  103. package/dist/experience/publish.js +144 -0
  104. package/dist/experience/publish.js.map +1 -0
  105. package/dist/experience/regenerate.d.ts +10 -0
  106. package/dist/experience/regenerate.js +33 -0
  107. package/dist/experience/regenerate.js.map +1 -0
  108. package/dist/experience/report.d.ts +9 -0
  109. package/dist/experience/report.js +23 -0
  110. package/dist/experience/report.js.map +1 -0
  111. package/dist/harness-scaffold.d.ts +52 -18
  112. package/dist/harness-scaffold.js +189 -9
  113. package/dist/harness-scaffold.js.map +1 -1
  114. package/dist/index.js +139 -7
  115. package/dist/index.js.map +1 -1
  116. package/dist/pack.js +5 -0
  117. package/dist/pack.js.map +1 -1
  118. package/dist/project-checks.d.ts +19 -0
  119. package/dist/project-checks.js +93 -0
  120. package/dist/project-checks.js.map +1 -1
  121. package/dist/project.d.ts +14 -0
  122. package/dist/project.js +10 -0
  123. package/dist/project.js.map +1 -1
  124. package/dist/repo/publish.d.ts +42 -0
  125. package/dist/repo/publish.js +225 -0
  126. package/dist/repo/publish.js.map +1 -0
  127. package/dist/repo/scaffold.d.ts +30 -0
  128. package/dist/repo/scaffold.js +165 -0
  129. package/dist/repo/scaffold.js.map +1 -0
  130. package/dist/teams-api.d.ts +37 -1
  131. package/dist/teams-api.js +62 -1
  132. package/dist/teams-api.js.map +1 -1
  133. package/dist/validate-findings.js +3 -1
  134. package/dist/validate-findings.js.map +1 -1
  135. package/package.json +2 -2
  136. package/templates/ekanos.json.tmpl +1 -0
  137. package/templates/experience/AGENTS.md.tmpl +22 -0
  138. package/templates/experience/ekanos.json.tmpl +7 -0
  139. package/templates/experience/ekanos.lock.json.tmpl +4 -0
  140. package/templates/experience/eslint.config.mjs.tmpl +25 -0
  141. package/templates/experience/gitignore.tmpl +17 -0
  142. package/templates/experience/package.json.tmpl +41 -0
  143. package/templates/experience/src/.ekanos/integrations.d.ts.tmpl +3 -0
  144. package/templates/experience/src/experience.ts.tmpl +30 -0
  145. package/templates/experience/src/widgets/hello-widget.tsx.tmpl +40 -0
  146. package/templates/experience/tsconfig.json.tmpl +20 -0
  147. package/templates/harness/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +9 -0
  148. package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -1
  149. package/templates/harness/app/[slug]/page.tsx.tmpl +1 -1
  150. package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -1
  151. package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -1
  152. package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -1
  153. package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -1
  154. package/templates/harness/app/experience/layout.tsx.tmpl +17 -0
  155. package/templates/harness/app/experience/page.tsx.tmpl +1 -0
  156. package/templates/harness/app/experience/pages/[id]/page.tsx.tmpl +1 -0
  157. package/templates/harness/app/experience/review-queue/page.tsx.tmpl +1 -0
  158. package/templates/harness/app/experience/runs/page.tsx.tmpl +1 -0
  159. package/templates/harness/app/experience/widgets/[id]/page.tsx.tmpl +1 -0
  160. package/templates/harness/app/experience/widgets/page.tsx.tmpl +1 -0
  161. package/templates/harness/app/experience/workflows/[id]/page.tsx.tmpl +1 -0
  162. package/templates/harness/app/experience/workflows/page.tsx.tmpl +1 -0
  163. package/templates/harness/app/integrations/[slug]/connect/page.tsx.tmpl +1 -0
  164. package/templates/harness/app/integrations/[slug]/events/page.tsx.tmpl +1 -0
  165. package/templates/harness/app/integrations/[slug]/health/page.tsx.tmpl +1 -0
  166. package/templates/harness/app/integrations/[slug]/operations/[kind]/[name]/page.tsx.tmpl +1 -0
  167. package/templates/harness/app/integrations/[slug]/operations/page.tsx.tmpl +1 -0
  168. package/templates/harness/app/integrations/[slug]/page.tsx.tmpl +1 -0
  169. package/templates/harness/app/integrations/[slug]/preview/activation/page.tsx.tmpl +1 -0
  170. package/templates/harness/app/integrations/[slug]/preview/layout.tsx.tmpl +1 -0
  171. package/templates/harness/app/integrations/[slug]/preview/page.tsx.tmpl +1 -0
  172. package/templates/harness/app/integrations/[slug]/preview/tile/page.tsx.tmpl +1 -0
  173. package/templates/harness/app/integrations/[slug]/preview/triggers/page.tsx.tmpl +1 -0
  174. package/templates/harness/app/integrations/[slug]/preview/widgets/[widgetId]/page.tsx.tmpl +1 -0
  175. package/templates/harness/app/integrations/[slug]/preview/widgets/page.tsx.tmpl +1 -0
  176. package/templates/harness/app/integrations/[slug]/runs/page.tsx.tmpl +1 -0
  177. package/templates/harness/app/integrations/layout.tsx.tmpl +1 -0
  178. package/templates/harness/app/integrations/page.tsx.tmpl +1 -0
  179. package/templates/harness/app/page.experience.tsx.tmpl +5 -0
  180. package/templates/harness/repo/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +11 -0
  181. package/templates/harness/repo/app/(repo)/connections/page.tsx.tmpl +1 -0
  182. package/templates/harness/repo/app/(repo)/layout.tsx.tmpl +1 -0
  183. package/templates/harness/repo/app/(repo)/page.tsx.tmpl +1 -0
  184. package/templates/harness/repo/app/experiences/[name]/connect/page.tsx.tmpl +1 -0
  185. package/templates/harness/repo/app/experiences/[name]/layout.tsx.tmpl +9 -0
  186. package/templates/harness/repo/app/experiences/page.tsx.tmpl +5 -0
  187. package/templates/harness/repo/app/harness-shell.tsx.tmpl +9 -0
  188. package/templates/harness/repo/app/integrations/[slug]/layout.tsx.tmpl +1 -0
  189. package/templates/harness/repo/app/integrations/page.tsx.tmpl +5 -0
  190. package/templates/harness/repo/app/repo-experiences.ts.tmpl +5 -0
  191. package/templates/harness/styles/globals.css.tmpl +15 -1
  192. package/templates/package.json.tmpl +5 -4
@@ -0,0 +1,51 @@
1
+ import { operationKey } from '@ekanos/integration-schema';
2
+ import { toTs } from './json-schema-to-ts.js';
3
+ export const GENERATED_BANNER = '// GENERATED by `ekanos codegen` from the pinned contracts. Do not edit; commit it.\n' +
4
+ '// Run `ekanos add <integration>@<version>` or `ekanos sync` to change the integrations.\n';
5
+ /**
6
+ * The `declare module` file that gives `useOperation` / `operation()` /
7
+ * `step.tool` their types. Only operations the experience declared appear, so
8
+ * calling any other is a compile error as well as a runner refusal.
9
+ */
10
+ export function generateIntegrationsDts(integrations) {
11
+ const sorted = [...integrations].sort((a, b) => a.integration < b.integration ? -1 : 1);
12
+ const blocks = sorted.map(({ integration, contract, operations }) => {
13
+ const wanted = new Set(operations);
14
+ const ops = contract.operations
15
+ .filter((op) => wanted.has(`${op.kind}/${op.name}`))
16
+ .sort((a, b) => (`${a.kind}/${a.name}` < `${b.kind}/${b.name}` ? -1 : 1))
17
+ .map((op) => {
18
+ const input = toTs(op.input, ' ');
19
+ const output = op.opaque.output
20
+ ? 'unknown'
21
+ : toTs(op.output, ' ');
22
+ return (` ${JSON.stringify(`${op.kind}/${op.name}`)}: {\n` +
23
+ ` effect: ${JSON.stringify(op.effect)};\n` +
24
+ ` input: ${input};\n` +
25
+ ` output: ${output};\n` +
26
+ ' };');
27
+ });
28
+ return (` ${JSON.stringify(integration).replace(/^"|"$/g, (q) => q)}: {\n` +
29
+ ` version: ${JSON.stringify(contract.compatLine)};\n` +
30
+ ' ops: {\n' +
31
+ `${ops.join('\n')}\n` +
32
+ ' };\n' +
33
+ ' };');
34
+ });
35
+ if (blocks.length === 0) {
36
+ return `${GENERATED_BANNER}export {};\n`;
37
+ }
38
+ return (`${GENERATED_BANNER}export {};\n\n` +
39
+ "declare module '@ekanos/sdk/experience' {\n" +
40
+ ' interface IntegrationRegistry {\n' +
41
+ `${blocks.join('\n')}\n` +
42
+ ' }\n' +
43
+ '}\n');
44
+ }
45
+ /** `kind/name` keys of an integration's declared operations (canonical ids stripped). */
46
+ export function declaredOperationKeys(integration) {
47
+ return integration.operations
48
+ .map((id) => operationKey(id))
49
+ .filter((key) => key !== null);
50
+ }
51
+ //# sourceMappingURL=integrations-codegen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrations-codegen.js","sourceRoot":"","sources":["../../src/experience/integrations-codegen.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Contract, operationKey } from '@ekanos/integration-schema';\n\nimport { toTs } from './json-schema-to-ts';\nimport type { ExperienceProject } from './project';\n\nexport const GENERATED_BANNER =\n '// GENERATED by `ekanos codegen` from the pinned contracts. Do not edit; commit it.\\n' +\n '// Run `ekanos add <integration>@<version>` or `ekanos sync` to change the integrations.\\n';\n\nexport interface IntegrationContract {\n integration: string;\n contract: Contract;\n /** The `kind/name` keys the experience declared for this integration in `integrations[].operations`. */\n operations: readonly string[];\n}\n\n/**\n * The `declare module` file that gives `useOperation` / `operation()` /\n * `step.tool` their types. Only operations the experience declared appear, so\n * calling any other is a compile error as well as a runner refusal.\n */\nexport function generateIntegrationsDts(\n integrations: readonly IntegrationContract[],\n): string {\n const sorted = [...integrations].sort((a, b) =>\n a.integration < b.integration ? -1 : 1,\n );\n const blocks = sorted.map(({ integration, contract, operations }) => {\n const wanted = new Set(operations);\n const ops = contract.operations\n .filter((op) => wanted.has(`${op.kind}/${op.name}`))\n .sort((a, b) => (`${a.kind}/${a.name}` < `${b.kind}/${b.name}` ? -1 : 1))\n .map((op) => {\n const input = toTs(op.input, ' ');\n const output = op.opaque.output\n ? 'unknown'\n : toTs(op.output, ' ');\n\n return (\n ` ${JSON.stringify(`${op.kind}/${op.name}`)}: {\\n` +\n ` effect: ${JSON.stringify(op.effect)};\\n` +\n ` input: ${input};\\n` +\n ` output: ${output};\\n` +\n ' };'\n );\n });\n\n return (\n ` ${JSON.stringify(integration).replace(/^\"|\"$/g, (q) => q)}: {\\n` +\n ` version: ${JSON.stringify(contract.compatLine)};\\n` +\n ' ops: {\\n' +\n `${ops.join('\\n')}\\n` +\n ' };\\n' +\n ' };'\n );\n });\n\n if (blocks.length === 0) {\n return `${GENERATED_BANNER}export {};\\n`;\n }\n\n return (\n `${GENERATED_BANNER}export {};\\n\\n` +\n \"declare module '@ekanos/sdk/experience' {\\n\" +\n ' interface IntegrationRegistry {\\n' +\n `${blocks.join('\\n')}\\n` +\n ' }\\n' +\n '}\\n'\n );\n}\n\n/** `kind/name` keys of an integration's declared operations (canonical ids stripped). */\nexport function declaredOperationKeys(\n integration: ExperienceProject['integrations'][number],\n): string[] {\n return integration.operations\n .map((id) => operationKey(id))\n .filter((key): key is NonNullable<typeof key> => key !== null);\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function toTs(schema: unknown, indent?: string): string;
@@ -0,0 +1,90 @@
1
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3
+ function key(name) {
4
+ return IDENT.test(name) ? name : JSON.stringify(name);
5
+ }
6
+ function literal(value) {
7
+ var _a;
8
+ return (_a = JSON.stringify(value)) !== null && _a !== void 0 ? _a : 'unknown';
9
+ }
10
+ function forType(type, schema, indent) {
11
+ switch (type) {
12
+ case 'string':
13
+ return 'string';
14
+ case 'number':
15
+ case 'integer':
16
+ return 'number';
17
+ case 'boolean':
18
+ return 'boolean';
19
+ case 'null':
20
+ return 'null';
21
+ case 'array': {
22
+ const items = isRecord(schema.items)
23
+ ? toTs(schema.items, indent)
24
+ : 'unknown';
25
+ return items.includes(' | ') || items.includes('&')
26
+ ? `Array<${items}>`
27
+ : `${items}[]`;
28
+ }
29
+ case 'object':
30
+ return objectType(schema, indent);
31
+ default:
32
+ return 'unknown';
33
+ }
34
+ }
35
+ function objectType(schema, indent) {
36
+ const properties = isRecord(schema.properties) ? schema.properties : {};
37
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
38
+ const names = Object.keys(properties);
39
+ const extra = schema.additionalProperties;
40
+ if (names.length === 0) {
41
+ if (isRecord(extra))
42
+ return `Record<string, ${toTs(extra, indent)}>`;
43
+ if (extra === true)
44
+ return 'Record<string, unknown>';
45
+ return 'Record<string, never>';
46
+ }
47
+ const inner = `${indent} `;
48
+ const lines = names.map((name) => {
49
+ const value = properties[name];
50
+ const type = isRecord(value) ? toTs(value, inner) : 'unknown';
51
+ return `${inner}${key(name)}${required.has(name) ? '' : '?'}: ${type};`;
52
+ });
53
+ if (isRecord(extra)) {
54
+ lines.push(`${inner}[key: string]: ${toTs(extra, inner)};`);
55
+ }
56
+ else if (extra === true) {
57
+ lines.push(`${inner}[key: string]: unknown;`);
58
+ }
59
+ return `{\n${lines.join('\n')}\n${indent}}`;
60
+ }
61
+ export function toTs(schema, indent = '') {
62
+ var _a;
63
+ if (schema === true || schema === undefined)
64
+ return 'unknown';
65
+ if (schema === false)
66
+ return 'never';
67
+ if (!isRecord(schema))
68
+ return 'unknown';
69
+ if ('const' in schema)
70
+ return literal(schema.const);
71
+ if (Array.isArray(schema.enum)) {
72
+ return schema.enum.map(literal).join(' | ') || 'never';
73
+ }
74
+ const alternatives = (_a = schema.anyOf) !== null && _a !== void 0 ? _a : schema.oneOf;
75
+ if (Array.isArray(alternatives)) {
76
+ const parts = alternatives.map((alt) => toTs(alt, indent));
77
+ return [...new Set(parts)].join(' | ') || 'never';
78
+ }
79
+ if (Array.isArray(schema.type)) {
80
+ const parts = schema.type.map((type) => forType(type, schema, indent));
81
+ return [...new Set(parts)].join(' | ');
82
+ }
83
+ if (typeof schema.type === 'string') {
84
+ return forType(schema.type, schema, indent);
85
+ }
86
+ if (isRecord(schema.properties))
87
+ return objectType(schema, indent);
88
+ return 'unknown';
89
+ }
90
+ //# sourceMappingURL=json-schema-to-ts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-schema-to-ts.js","sourceRoot":"","sources":["../../src/experience/json-schema-to-ts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * JSON Schema (the contract dialect) to a TypeScript type expression.\n *\n * Deliberately small: contracts only carry the subset `validateAgainstSchema`\n * understands (type, properties/required/additionalProperties, items, enum,\n * const, anyOf/oneOf, nullable type arrays). Anything else degrades to\n * `unknown`, never to a wrong type. Output is deterministic (property order is\n * the schema's own), so regenerating is a byte-for-byte no-op.\n */\ntype Schema = Record<string, unknown>;\n\nconst isRecord = (value: unknown): value is Schema =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\nfunction key(name: string): string {\n return IDENT.test(name) ? name : JSON.stringify(name);\n}\n\nfunction literal(value: unknown): string {\n return JSON.stringify(value) ?? 'unknown';\n}\n\nfunction forType(type: string, schema: Schema, indent: string): string {\n switch (type) {\n case 'string':\n return 'string';\n case 'number':\n case 'integer':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'null':\n return 'null';\n case 'array': {\n const items = isRecord(schema.items)\n ? toTs(schema.items, indent)\n : 'unknown';\n\n return items.includes(' | ') || items.includes('&')\n ? `Array<${items}>`\n : `${items}[]`;\n }\n case 'object':\n return objectType(schema, indent);\n default:\n return 'unknown';\n }\n}\n\nfunction objectType(schema: Schema, indent: string): string {\n const properties = isRecord(schema.properties) ? schema.properties : {};\n const required = new Set(\n Array.isArray(schema.required) ? (schema.required as string[]) : [],\n );\n const names = Object.keys(properties);\n const extra = schema.additionalProperties;\n\n if (names.length === 0) {\n if (isRecord(extra)) return `Record<string, ${toTs(extra, indent)}>`;\n if (extra === true) return 'Record<string, unknown>';\n\n return 'Record<string, never>';\n }\n\n const inner = `${indent} `;\n const lines = names.map((name) => {\n const value = properties[name];\n const type = isRecord(value) ? toTs(value, inner) : 'unknown';\n\n return `${inner}${key(name)}${required.has(name) ? '' : '?'}: ${type};`;\n });\n if (isRecord(extra)) {\n lines.push(`${inner}[key: string]: ${toTs(extra, inner)};`);\n } else if (extra === true) {\n lines.push(`${inner}[key: string]: unknown;`);\n }\n\n return `{\\n${lines.join('\\n')}\\n${indent}}`;\n}\n\nexport function toTs(schema: unknown, indent = ''): string {\n if (schema === true || schema === undefined) return 'unknown';\n if (schema === false) return 'never';\n if (!isRecord(schema)) return 'unknown';\n\n if ('const' in schema) return literal(schema.const);\n\n if (Array.isArray(schema.enum)) {\n return schema.enum.map(literal).join(' | ') || 'never';\n }\n\n const alternatives = schema.anyOf ?? schema.oneOf;\n if (Array.isArray(alternatives)) {\n const parts = alternatives.map((alt) => toTs(alt, indent));\n\n return [...new Set(parts)].join(' | ') || 'never';\n }\n\n if (Array.isArray(schema.type)) {\n const parts = (schema.type as string[]).map((type) =>\n forType(type, schema, indent),\n );\n\n return [...new Set(parts)].join(' | ');\n }\n\n if (typeof schema.type === 'string') {\n return forType(schema.type, schema, indent);\n }\n\n if (isRecord(schema.properties)) return objectType(schema, indent);\n\n return 'unknown';\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { LoadedExperienceModule } from './definition-shape.js';
2
+ import type { ExperienceProject } from './project.js';
3
+ export interface LoadExperienceOptions {
4
+ /**
5
+ * Root of an `@ekanos/sdk` checkout to bundle instead of importing
6
+ * `@ekanos/sdk` from node_modules. Needed only when the SDK's exports point
7
+ * at TypeScript source (the monorepo's own tests); a published SDK ships
8
+ * built output. Also settable as EKANOS_INLINE_SDK.
9
+ */
10
+ inlineSdk?: string;
11
+ }
12
+ export type LoadedExperience = ({
13
+ ok: true;
14
+ } & LoadedExperienceModule) | {
15
+ ok: false;
16
+ message: string;
17
+ };
18
+ /**
19
+ * Evaluate the experience entry in Node to read its definition, including the
20
+ * workflow templates' `build()` output. Components are loaded but never
21
+ * rendered, so a widget needs no browser to be inspected.
22
+ */
23
+ export declare function loadExperience(project: ExperienceProject, options?: LoadExperienceOptions): Promise<LoadedExperience>;
@@ -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"]}