@ekanos/cli 0.16.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 (190) 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/project-checks.d.ts +12 -0
  117. package/dist/project-checks.js +46 -0
  118. package/dist/project-checks.js.map +1 -1
  119. package/dist/project.d.ts +14 -0
  120. package/dist/project.js +10 -0
  121. package/dist/project.js.map +1 -1
  122. package/dist/repo/publish.d.ts +42 -0
  123. package/dist/repo/publish.js +225 -0
  124. package/dist/repo/publish.js.map +1 -0
  125. package/dist/repo/scaffold.d.ts +30 -0
  126. package/dist/repo/scaffold.js +165 -0
  127. package/dist/repo/scaffold.js.map +1 -0
  128. package/dist/teams-api.d.ts +37 -1
  129. package/dist/teams-api.js +62 -1
  130. package/dist/teams-api.js.map +1 -1
  131. package/dist/validate-findings.js +2 -1
  132. package/dist/validate-findings.js.map +1 -1
  133. package/package.json +2 -2
  134. package/templates/ekanos.json.tmpl +1 -0
  135. package/templates/experience/AGENTS.md.tmpl +22 -0
  136. package/templates/experience/ekanos.json.tmpl +7 -0
  137. package/templates/experience/ekanos.lock.json.tmpl +4 -0
  138. package/templates/experience/eslint.config.mjs.tmpl +25 -0
  139. package/templates/experience/gitignore.tmpl +17 -0
  140. package/templates/experience/package.json.tmpl +41 -0
  141. package/templates/experience/src/.ekanos/integrations.d.ts.tmpl +3 -0
  142. package/templates/experience/src/experience.ts.tmpl +30 -0
  143. package/templates/experience/src/widgets/hello-widget.tsx.tmpl +40 -0
  144. package/templates/experience/tsconfig.json.tmpl +20 -0
  145. package/templates/harness/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +9 -0
  146. package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -1
  147. package/templates/harness/app/[slug]/page.tsx.tmpl +1 -1
  148. package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -1
  149. package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -1
  150. package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -1
  151. package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -1
  152. package/templates/harness/app/experience/layout.tsx.tmpl +17 -0
  153. package/templates/harness/app/experience/page.tsx.tmpl +1 -0
  154. package/templates/harness/app/experience/pages/[id]/page.tsx.tmpl +1 -0
  155. package/templates/harness/app/experience/review-queue/page.tsx.tmpl +1 -0
  156. package/templates/harness/app/experience/runs/page.tsx.tmpl +1 -0
  157. package/templates/harness/app/experience/widgets/[id]/page.tsx.tmpl +1 -0
  158. package/templates/harness/app/experience/widgets/page.tsx.tmpl +1 -0
  159. package/templates/harness/app/experience/workflows/[id]/page.tsx.tmpl +1 -0
  160. package/templates/harness/app/experience/workflows/page.tsx.tmpl +1 -0
  161. package/templates/harness/app/integrations/[slug]/connect/page.tsx.tmpl +1 -0
  162. package/templates/harness/app/integrations/[slug]/events/page.tsx.tmpl +1 -0
  163. package/templates/harness/app/integrations/[slug]/health/page.tsx.tmpl +1 -0
  164. package/templates/harness/app/integrations/[slug]/operations/[kind]/[name]/page.tsx.tmpl +1 -0
  165. package/templates/harness/app/integrations/[slug]/operations/page.tsx.tmpl +1 -0
  166. package/templates/harness/app/integrations/[slug]/page.tsx.tmpl +1 -0
  167. package/templates/harness/app/integrations/[slug]/preview/activation/page.tsx.tmpl +1 -0
  168. package/templates/harness/app/integrations/[slug]/preview/layout.tsx.tmpl +1 -0
  169. package/templates/harness/app/integrations/[slug]/preview/page.tsx.tmpl +1 -0
  170. package/templates/harness/app/integrations/[slug]/preview/tile/page.tsx.tmpl +1 -0
  171. package/templates/harness/app/integrations/[slug]/preview/triggers/page.tsx.tmpl +1 -0
  172. package/templates/harness/app/integrations/[slug]/preview/widgets/[widgetId]/page.tsx.tmpl +1 -0
  173. package/templates/harness/app/integrations/[slug]/preview/widgets/page.tsx.tmpl +1 -0
  174. package/templates/harness/app/integrations/[slug]/runs/page.tsx.tmpl +1 -0
  175. package/templates/harness/app/integrations/layout.tsx.tmpl +1 -0
  176. package/templates/harness/app/integrations/page.tsx.tmpl +1 -0
  177. package/templates/harness/app/page.experience.tsx.tmpl +5 -0
  178. package/templates/harness/repo/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +11 -0
  179. package/templates/harness/repo/app/(repo)/connections/page.tsx.tmpl +1 -0
  180. package/templates/harness/repo/app/(repo)/layout.tsx.tmpl +1 -0
  181. package/templates/harness/repo/app/(repo)/page.tsx.tmpl +1 -0
  182. package/templates/harness/repo/app/experiences/[name]/connect/page.tsx.tmpl +1 -0
  183. package/templates/harness/repo/app/experiences/[name]/layout.tsx.tmpl +9 -0
  184. package/templates/harness/repo/app/experiences/page.tsx.tmpl +5 -0
  185. package/templates/harness/repo/app/harness-shell.tsx.tmpl +9 -0
  186. package/templates/harness/repo/app/integrations/[slug]/layout.tsx.tmpl +1 -0
  187. package/templates/harness/repo/app/integrations/page.tsx.tmpl +5 -0
  188. package/templates/harness/repo/app/repo-experiences.ts.tmpl +5 -0
  189. package/templates/harness/styles/globals.css.tmpl +15 -1
  190. package/templates/package.json.tmpl +4 -4
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/experience/project.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type EkanosJson,\n EkanosJsonSchema,\n type IntegrationEntry,\n type Lockfile,\n LockfileSchema,\n canonicalOperationId,\n experienceIntegrations,\n shortOperation,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { notFoundError, validationError } from '../errors';\n\nexport const EKANOS_JSON = 'ekanos.json';\nexport const LOCK_FILE = 'ekanos.lock.json';\nexport const LOCAL_OVERRIDES_FILE = 'ekanos.local.json';\nexport const DEFAULT_CONTRACTS_DIR = 'contracts';\nexport const INTEGRATIONS_DTS = 'src/.ekanos/integrations.d.ts';\n/** The previous name of the generated file, kept as a re-export for one version. */\nexport const LEGACY_SLOTS_DTS = 'src/.ekanos/slots.d.ts';\n\nexport interface ExperienceProject {\n /** Absolute project directory. */\n dir: string;\n ekanos: EkanosJson;\n slug: string;\n entry: string;\n /** The integrations it depends on; operations are canonical ids. */\n integrations: IntegrationEntry[];\n /** True when ekanos.json still uses the deprecated `uses` key. */\n deprecatedUses: boolean;\n /** Old `uses` alias -> integration, where they differ (for the sync notice). */\n renames: Record<string, string>;\n /** Absolute directory holding the pinned contract snapshots. */\n contractsDir: string;\n /** The raw ekanos.json, key order preserved, for rewrites. */\n raw: Record<string, unknown>;\n}\n\nfunction readJson(file: string): unknown {\n let text: string;\n try {\n text = fs.readFileSync(file, 'utf8');\n } catch {\n throw notFoundError(\n `No ${path.basename(file)} at ${file}.`,\n 'Run this from an Ekanos project directory, or pass --dir <path>.',\n );\n }\n try {\n return JSON.parse(text);\n } catch (error) {\n throw validationError(\n `${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the syntax error and re-run.',\n );\n }\n}\n\n/** `integration` when ekanos.json is absent or says so; used to route commands. */\nexport function readProjectKind(dir: string): 'integration' | 'experience' {\n try {\n const raw = JSON.parse(\n fs.readFileSync(path.join(dir, EKANOS_JSON), 'utf8'),\n ) as { kind?: unknown };\n\n return raw.kind === 'experience' ? 'experience' : 'integration';\n } catch {\n return 'integration';\n }\n}\n\n/**\n * Read and validate an experience's ekanos.json with the shared schema.\n *\n * A fresh scaffold has no integrations yet (you add them with `ekanos add`),\n * which the shared schema rejects (\"declares at least one integration\"). That\n * one rule is suspended here so `add` can bootstrap a project; `check` and\n * `build` report the empty state themselves.\n *\n * The deprecated `uses` shape is read too: its entries come back as\n * `integrations` (alias dropped) with `deprecatedUses` set and `renames`\n * mapping each old alias to its integration, so `ekanos sync` can rewrite the\n * file and say what code to update.\n */\nexport function readExperienceProject(dir: string): ExperienceProject {\n const absolute = path.resolve(dir);\n const raw = readJson(path.join(absolute, EKANOS_JSON));\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw validationError(`${EKANOS_JSON} must be a JSON object.`, 'Fix it.');\n }\n const record = raw as Record<string, unknown>;\n\n if (record.kind !== 'experience') {\n throw validationError(\n `${EKANOS_JSON} is not an experience (\"kind\": \"experience\").`,\n 'Run experience commands in an experience project; scaffold one with \"ekanos init --kind experience --slug <slug>\".',\n );\n }\n\n const declaredNone =\n !(Array.isArray(record.integrations) && record.integrations.length > 0) &&\n !(Array.isArray(record.uses) && record.uses.length > 0);\n const probe = declaredNone\n ? { ...record, integrations: [PLACEHOLDER_INTEGRATION], uses: undefined }\n : record;\n const parsed = EkanosJsonSchema.safeParse(probe);\n if (!parsed.success) {\n throw validationError(\n `${EKANOS_JSON}: ${parsed.error.issues\n .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('; ')}`,\n 'Fix ekanos.json; the experience fields are slug, entry, integrations, contractsDir, allowImports, ignore.',\n );\n }\n\n const ekanos = parsed.data;\n const declared = declaredNone\n ? { entries: [], deprecatedUses: false, renames: {} }\n : experienceIntegrations(ekanos);\n\n return {\n dir: absolute,\n ekanos,\n slug: ekanos.slug as string,\n entry: ekanos.entry as string,\n integrations: declared.entries.map(withCanonicalOperations),\n deprecatedUses: declared.deprecatedUses,\n renames: declared.renames,\n contractsDir: path.resolve(\n absolute,\n ekanos.contractsDir ?? DEFAULT_CONTRACTS_DIR,\n ),\n raw: record,\n };\n}\n\n/** Operations as canonical `<integration>@<version>/<kind>/<name>` ids, for lookups against a contract. */\nfunction withCanonicalOperations(entry: IntegrationEntry): IntegrationEntry {\n return {\n ...entry,\n operations: entry.operations.map((id) => canonicalOperationId(entry, id)),\n };\n}\n\n/** 2-space JSON with a trailing newline: the format every written file uses. */\nexport function formatJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Write only when the bytes differ. Returns whether the file changed. */\nexport function writeIfChanged(file: string, content: string): boolean {\n try {\n if (fs.readFileSync(file, 'utf8') === content) return false;\n } catch {\n // missing: write below\n }\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\nexport function readLockfile(project: ExperienceProject): Lockfile {\n const file = path.join(project.dir, LOCK_FILE);\n if (!fs.existsSync(file)) return { lockfileVersion: 1, integrations: {} };\n const parsed = LockfileSchema.safeParse(readJson(file));\n if (!parsed.success) {\n throw validationError(\n `${LOCK_FILE}: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'Regenerate it with \"ekanos sync\".',\n );\n }\n\n return parsed.data;\n}\n\nexport function writeLockfile(\n project: ExperienceProject,\n lock: Lockfile,\n): boolean {\n const integrations = Object.fromEntries(\n Object.entries(lock.integrations).sort(([a], [b]) => (a < b ? -1 : 1)),\n );\n\n return writeIfChanged(\n path.join(project.dir, LOCK_FILE),\n formatJson({ lockfileVersion: 1, integrations }),\n );\n}\n\nconst PLACEHOLDER_INTEGRATION = {\n name: 'pending',\n version: '0',\n operations: ['tool/pending'],\n} as const;\n\n/**\n * Rewrite `integrations` in ekanos.json (short operation form), leaving every\n * other key where it was and dropping the deprecated `uses`.\n */\nexport function writeIntegrations(\n project: ExperienceProject,\n integrations: IntegrationEntry[],\n): boolean {\n const { uses: _uses, ...rest } = project.raw;\n const next = {\n ...rest,\n integrations: integrations.map((entry) => ({\n name: entry.name,\n version: entry.version,\n operations: entry.operations.map(shortOperation),\n ...(entry.required === 'required' ? {} : { required: entry.required }),\n ...(entry.modes.length === 1 && entry.modes[0] === 'team'\n ? {}\n : { modes: entry.modes }),\n })),\n };\n\n return writeIfChanged(path.join(project.dir, EKANOS_JSON), formatJson(next));\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import type { CliContext } from '../context.js';
2
+ import type { ExitCode } from '../exit-codes.js';
3
+ import { type ExperienceCheckResult } from './checks.js';
4
+ import { type ExperienceProject } from './project.js';
5
+ export interface ExperiencePublishArgs {
6
+ dir: string;
7
+ dryRun: boolean;
8
+ env: Record<string, string | undefined>;
9
+ }
10
+ export interface ExperienceGate {
11
+ project: ExperienceProject;
12
+ result: ExperienceCheckResult;
13
+ }
14
+ /**
15
+ * The refusals and the gate every experience publish passes, before anything
16
+ * is built: active overrides, a tracked overrides file, `ignore` entries, then
17
+ * the experience checks. Throws a CliError for the first three; the findings
18
+ * come back for the caller to render (one envelope for one package, a table
19
+ * for a repo).
20
+ */
21
+ export declare function gateExperience(args: ExperiencePublishArgs): Promise<ExperienceGate>;
22
+ export interface PackedExperience {
23
+ slug: string;
24
+ version: string;
25
+ digest: string;
26
+ filename: string | undefined;
27
+ files: string[];
28
+ integrity: string | undefined;
29
+ /** Where the tarball was kept, when `artifactDir` was given. */
30
+ tarball: string | undefined;
31
+ }
32
+ /**
33
+ * Build the experience and pack it with `npm pack`, checking the tarball holds
34
+ * nothing but its own build output. `artifactDir` keeps the tarball (a repo
35
+ * dry run puts it under `.ekanos/publish/`); otherwise it is discarded.
36
+ */
37
+ export declare function packExperience(gate: ExperienceGate, artifactDir?: string): Promise<PackedExperience>;
38
+ /**
39
+ * `ekanos publish` for an experience. Defined, but gated:
40
+ *
41
+ * 1. Active integration overrides refuse it (exit 3, naming each integration and path): what
42
+ * you ran is not what a host would resolve. An integration of the same repo is not an override.
43
+ * 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.
44
+ * 3. Without --dry-run it stops: the host cannot admit an experience yet.
45
+ * 4. With --dry-run it builds and packs, and checks the tarball holds no
46
+ * integration code.
47
+ *
48
+ * Runs before any login: a dry run needs no session.
49
+ */
50
+ export declare function runExperiencePublish(ctx: CliContext, args: ExperiencePublishArgs): Promise<ExitCode>;
51
+ export declare const EXPERIENCE_NOT_SUPPORTED = "Publishing an experience is not supported by the host yet.";
@@ -0,0 +1,144 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { activeOverridesFor, harnessPinsResolver } from '../dev-experience.js';
6
+ import { gateFailedError, preconditionError, validationError } from '../errors.js';
7
+ import { emitExperience } from './build.js';
8
+ import { runExperienceChecks } from './checks.js';
9
+ import { isOverridesFileTracked } from './overrides.js';
10
+ import { LOCAL_OVERRIDES_FILE, readExperienceProject, } from './project.js';
11
+ import { failOnErrors } from './report.js';
12
+ /** What a packed experience may contain: its own build output and pins, nothing else. */
13
+ const ALLOWED_PACK_PATHS = [
14
+ /^package\.json$/,
15
+ /^ekanos\.json$/,
16
+ /^manifest\.json$/,
17
+ /^ekanos\.lock\.json$/,
18
+ /^contracts\/[^/]+\.contract\.json$/,
19
+ /^contracts\/\.gitkeep$/,
20
+ /^dist\/[^/]+$/,
21
+ /^README\.md$/,
22
+ /^LICENSE$/,
23
+ ];
24
+ /**
25
+ * The refusals and the gate every experience publish passes, before anything
26
+ * is built: active overrides, a tracked overrides file, `ignore` entries, then
27
+ * the experience checks. Throws a CliError for the first three; the findings
28
+ * come back for the caller to render (one envelope for one package, a table
29
+ * for a repo).
30
+ */
31
+ export async function gateExperience(args) {
32
+ var _a;
33
+ const project = readExperienceProject(args.dir);
34
+ const overrides = await activeOverridesFor(project.dir, args.env);
35
+ if (overrides.length > 0) {
36
+ throw validationError(`Cannot publish with ${overrides.length} local integration override${overrides.length === 1 ? '' : 's'} active: ${overrides
37
+ .map((o) => `${o.integration} -> ${o.path}`)
38
+ .join(', ')}.`, `Delete ${LOCAL_OVERRIDES_FILE} (and unset EKANOS_INTEGRATION_OVERRIDE_*) so every integration resolves to its published version, then re-run.`);
39
+ }
40
+ if (isOverridesFileTracked(project)) {
41
+ throw validationError(`${LOCAL_OVERRIDES_FILE} is tracked by git.`, `It is per-developer: "git rm --cached ${LOCAL_OVERRIDES_FILE}" and add it to .gitignore.`);
42
+ }
43
+ const ignored = (_a = project.ekanos.ignore) !== null && _a !== void 0 ? _a : [];
44
+ if (ignored.length > 0) {
45
+ throw validationError(`ekanos.json "ignore" excludes ${ignored.join(', ')} from the boundary scan, so it cannot be published.`, 'Remove the "ignore" entries (move the archive out of the project) and re-run.');
46
+ }
47
+ const result = await runExperienceChecks(project, {
48
+ typecheck: false,
49
+ loadDefinition: true,
50
+ inlineSdk: args.env.EKANOS_INLINE_SDK,
51
+ pinsResolver: await harnessPinsResolver(project.dir, args.env),
52
+ });
53
+ return { project, result };
54
+ }
55
+ /**
56
+ * Build the experience and pack it with `npm pack`, checking the tarball holds
57
+ * nothing but its own build output. `artifactDir` keeps the tarball (a repo
58
+ * dry run puts it under `.ekanos/publish/`); otherwise it is discarded.
59
+ */
60
+ export async function packExperience(gate, artifactDir) {
61
+ var _a;
62
+ const { project, result } = gate;
63
+ const output = await emitExperience(project, result);
64
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-pack-'));
65
+ let packed;
66
+ let tarball;
67
+ try {
68
+ const stdout = execFileSync('npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', scratch], { cwd: project.dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
69
+ packed = JSON.parse(stdout);
70
+ const first = packed[0];
71
+ if (artifactDir && first) {
72
+ fs.mkdirSync(artifactDir, { recursive: true });
73
+ tarball = path.join(artifactDir, path.basename(first.filename));
74
+ fs.copyFileSync(path.join(scratch, first.filename), tarball);
75
+ }
76
+ }
77
+ catch (error) {
78
+ throw preconditionError(`npm pack failed: ${error instanceof Error ? error.message : String(error)}`, 'Make sure npm is installed and package.json "files" lists dist, manifest.json, ekanos.json, ekanos.lock.json and contracts.');
79
+ }
80
+ finally {
81
+ fs.rmSync(scratch, { recursive: true, force: true });
82
+ }
83
+ const entry = packed[0];
84
+ const files = ((_a = entry === null || entry === void 0 ? void 0 : entry.files) !== null && _a !== void 0 ? _a : []).map((f) => f.path);
85
+ const stray = files.filter((f) => !ALLOWED_PACK_PATHS.some((re) => re.test(f)));
86
+ if (stray.length > 0) {
87
+ throw validationError(`The package would ship files an experience must not contain: ${stray.slice(0, 5).join(', ')}.`, 'Restrict package.json "files" to dist, manifest.json, ekanos.json, ekanos.lock.json and contracts.');
88
+ }
89
+ for (const required of [
90
+ 'manifest.json',
91
+ 'dist/experience.js',
92
+ 'ekanos.lock.json',
93
+ ]) {
94
+ if (!files.includes(required)) {
95
+ throw validationError(`The package would be missing ${required}.`, 'Add it to package.json "files".');
96
+ }
97
+ }
98
+ return {
99
+ slug: project.slug,
100
+ version: output.manifest.version,
101
+ digest: output.manifest.digest,
102
+ filename: entry === null || entry === void 0 ? void 0 : entry.filename,
103
+ files,
104
+ integrity: entry === null || entry === void 0 ? void 0 : entry.integrity,
105
+ tarball,
106
+ };
107
+ }
108
+ /**
109
+ * `ekanos publish` for an experience. Defined, but gated:
110
+ *
111
+ * 1. Active integration overrides refuse it (exit 3, naming each integration and path): what
112
+ * you ran is not what a host would resolve. An integration of the same repo is not an override.
113
+ * 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.
114
+ * 3. Without --dry-run it stops: the host cannot admit an experience yet.
115
+ * 4. With --dry-run it builds and packs, and checks the tarball holds no
116
+ * integration code.
117
+ *
118
+ * Runs before any login: a dry run needs no session.
119
+ */
120
+ export async function runExperiencePublish(ctx, args) {
121
+ const gate = await gateExperience(args);
122
+ const { project, result } = gate;
123
+ const failed = failOnErrors(ctx, 'publish', result.findings, { slug: project.slug }, project.dir);
124
+ if (failed !== null)
125
+ return failed;
126
+ if (!args.dryRun) {
127
+ throw gateFailedError(EXPERIENCE_NOT_SUPPORTED, 'Run "ekanos publish --dry-run" to build and pack it; the host has no admission path for experiences until it ships one.');
128
+ }
129
+ const packed = await packExperience(gate);
130
+ return ctx.succeed({
131
+ slug: packed.slug,
132
+ version: packed.version,
133
+ digest: packed.digest,
134
+ dryRun: true,
135
+ package: {
136
+ filename: packed.filename,
137
+ files: packed.files,
138
+ integrity: packed.integrity,
139
+ },
140
+ published: false,
141
+ }, `Dry run: packed ${packed.slug}@${packed.version} (${packed.files.length} files, ${packed.digest.slice(0, 18)}…). Nothing was published.`);
142
+ }
143
+ export const EXPERIENCE_NOT_SUPPORTED = 'Publishing an experience is not supported by the host yet.';
144
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/experience/publish.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { activeOverridesFor, harnessPinsResolver } from '../dev-experience';\nimport { gateFailedError, preconditionError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { emitExperience } from './build';\nimport { type ExperienceCheckResult, runExperienceChecks } from './checks';\nimport { isOverridesFileTracked } from './overrides';\nimport {\n type ExperienceProject,\n LOCAL_OVERRIDES_FILE,\n readExperienceProject,\n} from './project';\nimport { failOnErrors } from './report';\n\nexport interface ExperiencePublishArgs {\n dir: string;\n dryRun: boolean;\n env: Record<string, string | undefined>;\n}\n\n/** What a packed experience may contain: its own build output and pins, nothing else. */\nconst ALLOWED_PACK_PATHS = [\n /^package\\.json$/,\n /^ekanos\\.json$/,\n /^manifest\\.json$/,\n /^ekanos\\.lock\\.json$/,\n /^contracts\\/[^/]+\\.contract\\.json$/,\n /^contracts\\/\\.gitkeep$/,\n /^dist\\/[^/]+$/,\n /^README\\.md$/,\n /^LICENSE$/,\n];\n\nexport interface ExperienceGate {\n project: ExperienceProject;\n result: ExperienceCheckResult;\n}\n\n/**\n * The refusals and the gate every experience publish passes, before anything\n * is built: active overrides, a tracked overrides file, `ignore` entries, then\n * the experience checks. Throws a CliError for the first three; the findings\n * come back for the caller to render (one envelope for one package, a table\n * for a repo).\n */\nexport async function gateExperience(\n args: ExperiencePublishArgs,\n): Promise<ExperienceGate> {\n const project = readExperienceProject(args.dir);\n\n const overrides = await activeOverridesFor(project.dir, args.env);\n if (overrides.length > 0) {\n throw validationError(\n `Cannot publish with ${overrides.length} local integration override${overrides.length === 1 ? '' : 's'} active: ${overrides\n .map((o) => `${o.integration} -> ${o.path}`)\n .join(', ')}.`,\n `Delete ${LOCAL_OVERRIDES_FILE} (and unset EKANOS_INTEGRATION_OVERRIDE_*) so every integration resolves to its published version, then re-run.`,\n );\n }\n\n if (isOverridesFileTracked(project)) {\n throw validationError(\n `${LOCAL_OVERRIDES_FILE} is tracked by git.`,\n `It is per-developer: \"git rm --cached ${LOCAL_OVERRIDES_FILE}\" and add it to .gitignore.`,\n );\n }\n\n const ignored = project.ekanos.ignore ?? [];\n if (ignored.length > 0) {\n throw validationError(\n `ekanos.json \"ignore\" excludes ${ignored.join(', ')} from the boundary scan, so it cannot be published.`,\n 'Remove the \"ignore\" entries (move the archive out of the project) and re-run.',\n );\n }\n\n const result = await runExperienceChecks(project, {\n typecheck: false,\n loadDefinition: true,\n inlineSdk: args.env.EKANOS_INLINE_SDK,\n pinsResolver: await harnessPinsResolver(project.dir, args.env),\n });\n\n return { project, result };\n}\n\nexport interface PackedExperience {\n slug: string;\n version: string;\n digest: string;\n filename: string | undefined;\n files: string[];\n integrity: string | undefined;\n /** Where the tarball was kept, when `artifactDir` was given. */\n tarball: string | undefined;\n}\n\n/**\n * Build the experience and pack it with `npm pack`, checking the tarball holds\n * nothing but its own build output. `artifactDir` keeps the tarball (a repo\n * dry run puts it under `.ekanos/publish/`); otherwise it is discarded.\n */\nexport async function packExperience(\n gate: ExperienceGate,\n artifactDir?: string,\n): Promise<PackedExperience> {\n const { project, result } = gate;\n const output = await emitExperience(project, result);\n\n const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-pack-'));\n let packed: Array<{\n filename: string;\n files: Array<{ path: string; size: number }>;\n integrity?: string;\n }>;\n let tarball: string | undefined;\n try {\n const stdout = execFileSync(\n 'npm',\n ['pack', '--json', '--ignore-scripts', '--pack-destination', scratch],\n { cwd: project.dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },\n );\n packed = JSON.parse(stdout) as typeof packed;\n const first = packed[0];\n if (artifactDir && first) {\n fs.mkdirSync(artifactDir, { recursive: true });\n tarball = path.join(artifactDir, path.basename(first.filename));\n fs.copyFileSync(path.join(scratch, first.filename), tarball);\n }\n } catch (error) {\n throw preconditionError(\n `npm pack failed: ${error instanceof Error ? error.message : String(error)}`,\n 'Make sure npm is installed and package.json \"files\" lists dist, manifest.json, ekanos.json, ekanos.lock.json and contracts.',\n );\n } finally {\n fs.rmSync(scratch, { recursive: true, force: true });\n }\n\n const entry = packed[0];\n const files = (entry?.files ?? []).map((f) => f.path);\n const stray = files.filter(\n (f) => !ALLOWED_PACK_PATHS.some((re) => re.test(f)),\n );\n if (stray.length > 0) {\n throw validationError(\n `The package would ship files an experience must not contain: ${stray.slice(0, 5).join(', ')}.`,\n 'Restrict package.json \"files\" to dist, manifest.json, ekanos.json, ekanos.lock.json and contracts.',\n );\n }\n for (const required of [\n 'manifest.json',\n 'dist/experience.js',\n 'ekanos.lock.json',\n ]) {\n if (!files.includes(required)) {\n throw validationError(\n `The package would be missing ${required}.`,\n 'Add it to package.json \"files\".',\n );\n }\n }\n\n return {\n slug: project.slug,\n version: output.manifest.version,\n digest: output.manifest.digest,\n filename: entry?.filename,\n files,\n integrity: entry?.integrity,\n tarball,\n };\n}\n\n/**\n * `ekanos publish` for an experience. Defined, but gated:\n *\n * 1. Active integration overrides refuse it (exit 3, naming each integration and path): what\n * you ran is not what a host would resolve. An integration of the same repo is not an override.\n * 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.\n * 3. Without --dry-run it stops: the host cannot admit an experience yet.\n * 4. With --dry-run it builds and packs, and checks the tarball holds no\n * integration code.\n *\n * Runs before any login: a dry run needs no session.\n */\nexport async function runExperiencePublish(\n ctx: CliContext,\n args: ExperiencePublishArgs,\n): Promise<ExitCode> {\n const gate = await gateExperience(args);\n const { project, result } = gate;\n const failed = failOnErrors(\n ctx,\n 'publish',\n result.findings,\n { slug: project.slug },\n project.dir,\n );\n if (failed !== null) return failed;\n\n if (!args.dryRun) {\n throw gateFailedError(\n EXPERIENCE_NOT_SUPPORTED,\n 'Run \"ekanos publish --dry-run\" to build and pack it; the host has no admission path for experiences until it ships one.',\n );\n }\n\n const packed = await packExperience(gate);\n\n return ctx.succeed(\n {\n slug: packed.slug,\n version: packed.version,\n digest: packed.digest,\n dryRun: true,\n package: {\n filename: packed.filename,\n files: packed.files,\n integrity: packed.integrity,\n },\n published: false,\n },\n `Dry run: packed ${packed.slug}@${packed.version} (${packed.files.length} files, ${packed.digest.slice(0, 18)}…). Nothing was published.`,\n );\n}\n\nexport const EXPERIENCE_NOT_SUPPORTED =\n 'Publishing an experience is not supported by the host yet.';\n"]}
@@ -0,0 +1,10 @@
1
+ import { type PinnedIntegration } from './pins.js';
2
+ import { type ExperienceProject } from './project.js';
3
+ /** Load every pinned snapshot. Integrations without one are returned as `missing`. */
4
+ export declare function loadPins(project: ExperienceProject): {
5
+ pinned: PinnedIntegration[];
6
+ missing: string[];
7
+ };
8
+ /** Write `src/.ekanos/integrations.d.ts`; returns whether it changed. Idempotent. */
9
+ export declare function regenerateIntegrations(project: ExperienceProject, pinned: readonly PinnedIntegration[]): boolean;
10
+ export declare const LEGACY_SLOTS_DTS_CONTENT: string;
@@ -0,0 +1,33 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { expectedIntegrationsDts } from './checks.js';
4
+ import { loadPinnedContract } from './pins.js';
5
+ import { INTEGRATIONS_DTS, LEGACY_SLOTS_DTS, writeIfChanged, } from './project.js';
6
+ /** Load every pinned snapshot. Integrations without one are returned as `missing`. */
7
+ export function loadPins(project) {
8
+ const pinned = [];
9
+ const missing = [];
10
+ for (const use of project.integrations) {
11
+ const contract = loadPinnedContract(project, use);
12
+ if (contract)
13
+ pinned.push({ use, contract });
14
+ else
15
+ missing.push(use.name);
16
+ }
17
+ return { pinned, missing };
18
+ }
19
+ /** Write `src/.ekanos/integrations.d.ts`; returns whether it changed. Idempotent. */
20
+ export function regenerateIntegrations(project, pinned) {
21
+ const changed = writeIfChanged(path.join(project.dir, INTEGRATIONS_DTS), expectedIntegrationsDts(pinned));
22
+ // The file used to be `slots.d.ts`. A project that still has it (and lists
23
+ // it in its tsconfig) keeps compiling: it becomes a re-export for one version.
24
+ const legacy = path.join(project.dir, LEGACY_SLOTS_DTS);
25
+ const legacyChanged = fs.existsSync(legacy)
26
+ ? writeIfChanged(legacy, LEGACY_SLOTS_DTS_CONTENT)
27
+ : false;
28
+ return changed || legacyChanged;
29
+ }
30
+ export const LEGACY_SLOTS_DTS_CONTENT = '// DEPRECATED: this file is now src/.ekanos/integrations.d.ts. It re-exports it so an\n' +
31
+ '// existing tsconfig keeps working for this version; delete it and list integrations.d.ts instead.\n' +
32
+ "import './integrations';\n\nexport {};\n";
33
+ //# sourceMappingURL=regenerate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regenerate.js","sourceRoot":"","sources":["../../src/experience/regenerate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAA0B,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AACpE,OAAO,EAEL,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,OAA0B;IAIjD,MAAM,MAAM,GAAwB,EAAE,CAAC;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACvC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAClD,IAAI,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;;YACxC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAC7B,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,sBAAsB,CACpC,OAA0B,EAC1B,MAAoC;IAEpC,MAAM,OAAO,GAAG,cAAc,CAC5B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,EACxC,uBAAuB,CAAC,MAAM,CAAC,CAChC,CAAC;IAEF,2EAA2E;IAC3E,+EAA+E;IAC/E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IACxD,MAAM,aAAa,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QACzC,CAAC,CAAC,cAAc,CAAC,MAAM,EAAE,wBAAwB,CAAC;QAClD,CAAC,CAAC,KAAK,CAAC;IAEV,OAAO,OAAO,IAAI,aAAa,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,MAAM,wBAAwB,GACnC,yFAAyF;IACzF,sGAAsG;IACtG,0CAA0C,CAAC","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { expectedIntegrationsDts } from './checks';\nimport { type PinnedIntegration, loadPinnedContract } from './pins';\nimport {\n type ExperienceProject,\n INTEGRATIONS_DTS,\n LEGACY_SLOTS_DTS,\n writeIfChanged,\n} from './project';\n\n/** Load every pinned snapshot. Integrations without one are returned as `missing`. */\nexport function loadPins(project: ExperienceProject): {\n pinned: PinnedIntegration[];\n missing: string[];\n} {\n const pinned: PinnedIntegration[] = [];\n const missing: string[] = [];\n for (const use of project.integrations) {\n const contract = loadPinnedContract(project, use);\n if (contract) pinned.push({ use, contract });\n else missing.push(use.name);\n }\n\n return { pinned, missing };\n}\n\n/** Write `src/.ekanos/integrations.d.ts`; returns whether it changed. Idempotent. */\nexport function regenerateIntegrations(\n project: ExperienceProject,\n pinned: readonly PinnedIntegration[],\n): boolean {\n const changed = writeIfChanged(\n path.join(project.dir, INTEGRATIONS_DTS),\n expectedIntegrationsDts(pinned),\n );\n\n // The file used to be `slots.d.ts`. A project that still has it (and lists\n // it in its tsconfig) keeps compiling: it becomes a re-export for one version.\n const legacy = path.join(project.dir, LEGACY_SLOTS_DTS);\n const legacyChanged = fs.existsSync(legacy)\n ? writeIfChanged(legacy, LEGACY_SLOTS_DTS_CONTENT)\n : false;\n\n return changed || legacyChanged;\n}\n\nexport const LEGACY_SLOTS_DTS_CONTENT =\n '// DEPRECATED: this file is now src/.ekanos/integrations.d.ts. It re-exports it so an\\n' +\n '// existing tsconfig keeps working for this version; delete it and list integrations.d.ts instead.\\n' +\n \"import './integrations';\\n\\nexport {};\\n\";\n"]}
@@ -0,0 +1,9 @@
1
+ import type { Finding } from '@ekanos/integration-schema';
2
+ import type { CliContext } from '../context.js';
3
+ import type { ExitCode } from '../exit-codes.js';
4
+ /**
5
+ * Settle a command that produced findings: exit 3 with the findings in the
6
+ * envelope when any is an error, otherwise return null so the caller reports
7
+ * its own success (warnings ride along in `data.findings`).
8
+ */
9
+ export declare function failOnErrors(ctx: CliContext, command: string, findings: readonly Finding[], data: Record<string, unknown>, cwd: string): ExitCode | null;
@@ -0,0 +1,23 @@
1
+ import { validationError } from '../errors.js';
2
+ import { renderFindings } from '../findings-renderer.js';
3
+ /**
4
+ * Settle a command that produced findings: exit 3 with the findings in the
5
+ * envelope when any is an error, otherwise return null so the caller reports
6
+ * its own success (warnings ride along in `data.findings`).
7
+ */
8
+ export function failOnErrors(ctx, command, findings, data, cwd) {
9
+ const errors = findings.filter((f) => f.severity === 'error');
10
+ if (errors.length === 0)
11
+ return null;
12
+ if (!ctx.jsonMode)
13
+ renderFindings(ctx, [...findings], { cwd });
14
+ return ctx.fail(validationError(`${errors.length} finding${errors.length === 1 ? '' : 's'} to fix.`, `Resolve the error findings in data.findings, then re-run "ekanos ${command}".`), Object.assign(Object.assign({}, data), { findings }), !ctx.jsonMode
15
+ ? {
16
+ findingsSummary: {
17
+ command: `ekanos ${command}`,
18
+ errorCount: errors.length,
19
+ },
20
+ }
21
+ : undefined);
22
+ }
23
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/experience/report.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAe,EACf,OAAe,EACf,QAA4B,EAC5B,IAA6B,EAC7B,GAAW;IAEX,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC9D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,IAAI,CAAC,GAAG,CAAC,QAAQ;QAAE,cAAc,CAAC,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAE/D,OAAO,GAAG,CAAC,IAAI,CACb,eAAe,CACb,GAAG,MAAM,CAAC,MAAM,WAAW,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU,EACnE,oEAAoE,OAAO,IAAI,CAChF,kCACI,IAAI,KAAE,QAAQ,KACnB,CAAC,GAAG,CAAC,QAAQ;QACX,CAAC,CAAC;YACE,eAAe,EAAE;gBACf,OAAO,EAAE,UAAU,OAAO,EAAE;gBAC5B,UAAU,EAAE,MAAM,CAAC,MAAM;aAC1B;SACF;QACH,CAAC,CAAC,SAAS,CACd,CAAC;AACJ,CAAC","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\n\nimport type { CliContext } from '../context';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\n\n/**\n * Settle a command that produced findings: exit 3 with the findings in the\n * envelope when any is an error, otherwise return null so the caller reports\n * its own success (warnings ride along in `data.findings`).\n */\nexport function failOnErrors(\n ctx: CliContext,\n command: string,\n findings: readonly Finding[],\n data: Record<string, unknown>,\n cwd: string,\n): ExitCode | null {\n const errors = findings.filter((f) => f.severity === 'error');\n if (errors.length === 0) return null;\n\n if (!ctx.jsonMode) renderFindings(ctx, [...findings], { cwd });\n\n return ctx.fail(\n validationError(\n `${errors.length} finding${errors.length === 1 ? '' : 's'} to fix.`,\n `Resolve the error findings in data.findings, then re-run \"ekanos ${command}\".`,\n ),\n { ...data, findings },\n !ctx.jsonMode\n ? {\n findingsSummary: {\n command: `ekanos ${command}`,\n errorCount: errors.length,\n },\n }\n : undefined,\n );\n}\n"]}