@ekanos/cli 0.1.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/auth/credential-store.d.ts +96 -0
  4. package/dist/auth/credential-store.js +274 -0
  5. package/dist/auth/credential-store.js.map +1 -0
  6. package/dist/auth/device-flow.d.ts +57 -0
  7. package/dist/auth/device-flow.js +201 -0
  8. package/dist/auth/device-flow.js.map +1 -0
  9. package/dist/auth/fusion-api.d.ts +48 -0
  10. package/dist/auth/fusion-api.js +310 -0
  11. package/dist/auth/fusion-api.js.map +1 -0
  12. package/dist/auth/session.d.ts +57 -0
  13. package/dist/auth/session.js +114 -0
  14. package/dist/auth/session.js.map +1 -0
  15. package/dist/bin.d.ts +2 -0
  16. package/dist/bin.js +20 -0
  17. package/dist/bin.js.map +1 -0
  18. package/dist/commands/dev.d.ts +36 -0
  19. package/dist/commands/dev.js +207 -0
  20. package/dist/commands/dev.js.map +1 -0
  21. package/dist/commands/init.d.ts +30 -0
  22. package/dist/commands/init.js +178 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/login.d.ts +20 -0
  25. package/dist/commands/login.js +92 -0
  26. package/dist/commands/login.js.map +1 -0
  27. package/dist/commands/logout.d.ts +20 -0
  28. package/dist/commands/logout.js +75 -0
  29. package/dist/commands/logout.js.map +1 -0
  30. package/dist/commands/test.d.ts +16 -0
  31. package/dist/commands/test.js +54 -0
  32. package/dist/commands/test.js.map +1 -0
  33. package/dist/commands/validate.d.ts +14 -0
  34. package/dist/commands/validate.js +98 -0
  35. package/dist/commands/validate.js.map +1 -0
  36. package/dist/commands/whoami.d.ts +18 -0
  37. package/dist/commands/whoami.js +29 -0
  38. package/dist/commands/whoami.js.map +1 -0
  39. package/dist/compatibility.d.ts +81 -0
  40. package/dist/compatibility.js +135 -0
  41. package/dist/compatibility.js.map +1 -0
  42. package/dist/context.d.ts +81 -0
  43. package/dist/context.js +125 -0
  44. package/dist/context.js.map +1 -0
  45. package/dist/errors.d.ts +41 -0
  46. package/dist/errors.js +111 -0
  47. package/dist/errors.js.map +1 -0
  48. package/dist/exit-codes.d.ts +52 -0
  49. package/dist/exit-codes.js +51 -0
  50. package/dist/exit-codes.js.map +1 -0
  51. package/dist/harness-scaffold.d.ts +212 -0
  52. package/dist/harness-scaffold.js +434 -0
  53. package/dist/harness-scaffold.js.map +1 -0
  54. package/dist/index.d.ts +17 -0
  55. package/dist/index.js +262 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/load-definition.d.ts +20 -0
  58. package/dist/load-definition.js +126 -0
  59. package/dist/load-definition.js.map +1 -0
  60. package/dist/merge-manifest.d.ts +44 -0
  61. package/dist/merge-manifest.js +124 -0
  62. package/dist/merge-manifest.js.map +1 -0
  63. package/dist/package-manager.d.ts +25 -0
  64. package/dist/package-manager.js +57 -0
  65. package/dist/package-manager.js.map +1 -0
  66. package/dist/parse-argv.d.ts +27 -0
  67. package/dist/parse-argv.js +74 -0
  68. package/dist/parse-argv.js.map +1 -0
  69. package/dist/project-checks.d.ts +18 -0
  70. package/dist/project-checks.js +77 -0
  71. package/dist/project-checks.js.map +1 -0
  72. package/dist/project.d.ts +140 -0
  73. package/dist/project.js +228 -0
  74. package/dist/project.js.map +1 -0
  75. package/dist/safe-fs.d.ts +55 -0
  76. package/dist/safe-fs.js +209 -0
  77. package/dist/safe-fs.js.map +1 -0
  78. package/dist/templates.d.ts +23 -0
  79. package/dist/templates.js +49 -0
  80. package/dist/templates.js.map +1 -0
  81. package/package.json +62 -0
  82. package/templates/ekanos.json.tmpl +4 -0
  83. package/templates/gitignore.tmpl +12 -0
  84. package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -0
  85. package/templates/harness/app/[slug]/layout.tsx.tmpl +1 -0
  86. package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -0
  87. package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -0
  88. package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -0
  89. package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -0
  90. package/templates/harness/app/harness-shell.tsx.tmpl +9 -0
  91. package/templates/harness/app/layout.tsx.tmpl +4 -0
  92. package/templates/harness/app/page.tsx.tmpl +7 -0
  93. package/templates/harness/next.config.mjs.tmpl +46 -0
  94. package/templates/harness/package.json.tmpl +11 -0
  95. package/templates/harness/postcss.config.mjs.tmpl +6 -0
  96. package/templates/harness/styles/globals.css.tmpl +33 -0
  97. package/templates/harness/tsconfig.json.tmpl +31 -0
  98. package/templates/harness/types/team-accounts.d.ts.tmpl +16 -0
  99. package/templates/harness.config.entry.ts.tmpl +65 -0
  100. package/templates/harness.config.ts.tmpl +58 -0
  101. package/templates/package.json.tmpl +28 -0
  102. package/templates/src/integration.test.ts.tmpl +24 -0
  103. package/templates/src/integration.ts.tmpl +83 -0
  104. package/templates/tsconfig.json.tmpl +19 -0
  105. package/templates/vitest.config.ts.tmpl +32 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Merging the scaffold's package.json into one a partner already has.
3
+ *
4
+ * The ordinary way to start a project is `npm init -y`, and doing that before
5
+ * `ekanos init` used to cost the partner every dependency, every script, and
6
+ * leave `"type": "commonjs"` under ESM/TypeScript sources — reported only as
7
+ * `skipped package.json (already exists)`. A skip that expensive has to either
8
+ * do the work or say precisely what it did not do.
9
+ *
10
+ * The rule here is ADDITIVE ONLY: a value the partner already set is never
11
+ * changed, because their pinned version or their own `test` script is a
12
+ * decision, not an omission. Anything we would have set but cannot becomes a
13
+ * note carrying its own remediation.
14
+ */
15
+ const RECORD_FIELDS = ['dependencies', 'devDependencies', 'scripts'];
16
+ /**
17
+ * Values `npm init -y` writes that are placeholders, not decisions.
18
+ *
19
+ * Treating npm's stub `test` script as a real choice left the ordinary
20
+ * `npm init -y` → `ekanos init` path with a `test` script that exits 1, so
21
+ * `ekanos test` — the second command a partner runs — failed on a fresh
22
+ * scaffold. This is the one narrow exception to additive-only, and it is safe
23
+ * because the string is npm's own fixed literal, not something a partner types.
24
+ */
25
+ const NPM_PLACEHOLDERS = {
26
+ 'scripts.test': ['echo "Error: no test specified" && exit 1'],
27
+ };
28
+ /**
29
+ * `npm init -y` writes `"type": "commonjs"` EXPLICITLY (npm 11.6.2), so
30
+ * "only set it when absent" never fires for the ordinary way a partner starts
31
+ * a project — it is the mainline, not an edge case.
32
+ *
33
+ * It is npm boilerplate by exactly the same argument as the stub `test` script
34
+ * above, and treating one as boilerplate and the other as a considered choice
35
+ * was inconsistent. The decisive point: the files this scaffolder writes are
36
+ * ESM TypeScript and cannot load under `commonjs`, so leaving it would mean
37
+ * writing files that do not run. A scaffolder that does that has not
38
+ * scaffolded anything.
39
+ *
40
+ * So it is overwritten, loudly — with a note saying what changed and what to
41
+ * do if the project really does have CommonJS sources.
42
+ */
43
+ const REQUIRED_TYPE = 'module';
44
+ function isPlaceholder(field, name, value) {
45
+ var _a;
46
+ return ((_a = NPM_PLACEHOLDERS[`${field}.${name}`]) !== null && _a !== void 0 ? _a : []).includes(value);
47
+ }
48
+ function asRecord(value) {
49
+ if (typeof value !== 'object' || value === null)
50
+ return {};
51
+ const out = {};
52
+ for (const [key, entry] of Object.entries(value)) {
53
+ if (typeof entry === 'string')
54
+ out[key] = entry;
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Merge `template` into `existing`, adding only what is missing.
60
+ *
61
+ * `type` is the one field that cannot be resolved additively: the scaffold's
62
+ * sources are ESM TypeScript, so they need `"type": "module"`, but flipping an
63
+ * explicit `"commonjs"` could break code the partner already has. Absent, we
64
+ * set it; set to anything else, we leave it and say what will break.
65
+ */
66
+ export function mergeManifest(existing, template) {
67
+ const manifest = Object.assign({}, existing);
68
+ const added = [];
69
+ const kept = [];
70
+ const notes = [];
71
+ for (const field of RECORD_FIELDS) {
72
+ const wanted = asRecord(template[field]);
73
+ if (Object.keys(wanted).length === 0)
74
+ continue;
75
+ const current = asRecord(existing[field]);
76
+ const merged = Object.assign({}, current);
77
+ let replacedPlaceholder = false;
78
+ for (const [name, value] of Object.entries(wanted)) {
79
+ const existingValue = current[name];
80
+ if (existingValue !== undefined) {
81
+ if (!isPlaceholder(field, name, existingValue)) {
82
+ kept.push(`${field}.${name}`);
83
+ continue;
84
+ }
85
+ replacedPlaceholder = true;
86
+ }
87
+ merged[name] = value;
88
+ added.push(`${field}.${name}`);
89
+ }
90
+ if (replacedPlaceholder ||
91
+ Object.keys(merged).length > Object.keys(current).length) {
92
+ manifest[field] = sortKeys(merged);
93
+ }
94
+ }
95
+ const existingType = existing.type;
96
+ if (existingType === REQUIRED_TYPE) {
97
+ kept.push('type');
98
+ }
99
+ else {
100
+ manifest.type = REQUIRED_TYPE;
101
+ added.push('type');
102
+ if (existingType !== undefined) {
103
+ notes.push({
104
+ field: 'type',
105
+ message: `Changed "type" from ${JSON.stringify(existingType)} to "module" in ` +
106
+ 'package.json. The scaffolded sources are ES modules and Node ' +
107
+ 'cannot load them otherwise, so leaving it would have written files ' +
108
+ 'that do not run.',
109
+ hint: 'If this project has CommonJS sources of its own, revert that line ' +
110
+ 'and put your integration in its own package instead — the two ' +
111
+ 'module systems cannot share one package.json.',
112
+ });
113
+ }
114
+ }
115
+ return { manifest, added, kept, notes, changed: added.length > 0 };
116
+ }
117
+ function sortKeys(record) {
118
+ return Object.fromEntries(Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
119
+ }
120
+ /** Serialize a manifest the way npm does — 2-space indent, trailing newline. */
121
+ export function serializeManifest(manifest) {
122
+ return `${JSON.stringify(manifest, null, 2)}\n`;
123
+ }
124
+ //# sourceMappingURL=merge-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-manifest.js","sourceRoot":"","sources":["../src/merge-manifest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Merging the scaffold's package.json into one a partner already has.\n *\n * The ordinary way to start a project is `npm init -y`, and doing that before\n * `ekanos init` used to cost the partner every dependency, every script, and\n * leave `\"type\": \"commonjs\"` under ESM/TypeScript sources — reported only as\n * `skipped package.json (already exists)`. A skip that expensive has to either\n * do the work or say precisely what it did not do.\n *\n * The rule here is ADDITIVE ONLY: a value the partner already set is never\n * changed, because their pinned version or their own `test` script is a\n * decision, not an omission. Anything we would have set but cannot becomes a\n * note carrying its own remediation.\n */\n\nexport interface Manifest {\n [key: string]: unknown;\n}\n\nexport interface ManifestNote {\n field: string;\n message: string;\n hint: string;\n}\n\nexport interface MergeResult {\n manifest: Manifest;\n /** Dotted paths this merge added, e.g. `dependencies.@ekanos/sdk`. */\n added: string[];\n /** Fields left alone because the partner had already set them. */\n kept: string[];\n /** Things a partner must decide or do themselves. */\n notes: ManifestNote[];\n /** False when nothing changed, so the caller can skip the write entirely. */\n changed: boolean;\n}\n\nconst RECORD_FIELDS = ['dependencies', 'devDependencies', 'scripts'] as const;\n\n/**\n * Values `npm init -y` writes that are placeholders, not decisions.\n *\n * Treating npm's stub `test` script as a real choice left the ordinary\n * `npm init -y` → `ekanos init` path with a `test` script that exits 1, so\n * `ekanos test` — the second command a partner runs — failed on a fresh\n * scaffold. This is the one narrow exception to additive-only, and it is safe\n * because the string is npm's own fixed literal, not something a partner types.\n */\nconst NPM_PLACEHOLDERS: Record<string, readonly string[]> = {\n 'scripts.test': ['echo \"Error: no test specified\" && exit 1'],\n};\n\n/**\n * `npm init -y` writes `\"type\": \"commonjs\"` EXPLICITLY (npm 11.6.2), so\n * \"only set it when absent\" never fires for the ordinary way a partner starts\n * a project — it is the mainline, not an edge case.\n *\n * It is npm boilerplate by exactly the same argument as the stub `test` script\n * above, and treating one as boilerplate and the other as a considered choice\n * was inconsistent. The decisive point: the files this scaffolder writes are\n * ESM TypeScript and cannot load under `commonjs`, so leaving it would mean\n * writing files that do not run. A scaffolder that does that has not\n * scaffolded anything.\n *\n * So it is overwritten, loudly — with a note saying what changed and what to\n * do if the project really does have CommonJS sources.\n */\nconst REQUIRED_TYPE = 'module';\n\nfunction isPlaceholder(field: string, name: string, value: string): boolean {\n return (NPM_PLACEHOLDERS[`${field}.${name}`] ?? []).includes(value);\n}\n\nfunction asRecord(value: unknown): Record<string, string> {\n if (typeof value !== 'object' || value === null) return {};\n const out: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') out[key] = entry;\n }\n return out;\n}\n\n/**\n * Merge `template` into `existing`, adding only what is missing.\n *\n * `type` is the one field that cannot be resolved additively: the scaffold's\n * sources are ESM TypeScript, so they need `\"type\": \"module\"`, but flipping an\n * explicit `\"commonjs\"` could break code the partner already has. Absent, we\n * set it; set to anything else, we leave it and say what will break.\n */\nexport function mergeManifest(\n existing: Manifest,\n template: Manifest,\n): MergeResult {\n const manifest: Manifest = { ...existing };\n const added: string[] = [];\n const kept: string[] = [];\n const notes: ManifestNote[] = [];\n\n for (const field of RECORD_FIELDS) {\n const wanted = asRecord(template[field]);\n if (Object.keys(wanted).length === 0) continue;\n\n const current = asRecord(existing[field]);\n const merged = { ...current };\n let replacedPlaceholder = false;\n for (const [name, value] of Object.entries(wanted)) {\n const existingValue = current[name];\n if (existingValue !== undefined) {\n if (!isPlaceholder(field, name, existingValue)) {\n kept.push(`${field}.${name}`);\n continue;\n }\n replacedPlaceholder = true;\n }\n merged[name] = value;\n added.push(`${field}.${name}`);\n }\n\n if (\n replacedPlaceholder ||\n Object.keys(merged).length > Object.keys(current).length\n ) {\n manifest[field] = sortKeys(merged);\n }\n }\n\n const existingType = existing.type;\n if (existingType === REQUIRED_TYPE) {\n kept.push('type');\n } else {\n manifest.type = REQUIRED_TYPE;\n added.push('type');\n\n if (existingType !== undefined) {\n notes.push({\n field: 'type',\n message:\n `Changed \"type\" from ${JSON.stringify(existingType)} to \"module\" in ` +\n 'package.json. The scaffolded sources are ES modules and Node ' +\n 'cannot load them otherwise, so leaving it would have written files ' +\n 'that do not run.',\n hint:\n 'If this project has CommonJS sources of its own, revert that line ' +\n 'and put your integration in its own package instead — the two ' +\n 'module systems cannot share one package.json.',\n });\n }\n }\n\n return { manifest, added, kept, notes, changed: added.length > 0 };\n}\n\nfunction sortKeys(record: Record<string, string>): Record<string, string> {\n return Object.fromEntries(\n Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n\n/** Serialize a manifest the way npm does — 2-space indent, trailing newline. */\nexport function serializeManifest(manifest: Manifest): string {\n return `${JSON.stringify(manifest, null, 2)}\\n`;\n}\n"]}
@@ -0,0 +1,25 @@
1
+ export type PackageManagerName = 'pnpm' | 'yarn' | 'bun' | 'npm';
2
+ export interface PackageManager {
3
+ name: PackageManagerName;
4
+ bin: string;
5
+ }
6
+ /**
7
+ * Detect the package manager from the lockfile in the project root, falling
8
+ * back to npm. Only the lockfile is authoritative — we do not parse
9
+ * `packageManager` fields or probe $PATH.
10
+ */
11
+ export declare function detectPackageManager(projectDir: string): PackageManager;
12
+ /**
13
+ * The argv for running a project-local binary through a package manager.
14
+ *
15
+ * The harness shell has no `node_modules` of its own — `next` lives in the
16
+ * partner's project root and resolves upward — so we cannot spawn the bin
17
+ * directly by path without guessing a layout (pnpm's is not npm's). Every
18
+ * package manager already knows how to find it; each just spells it
19
+ * differently.
20
+ */
21
+ export declare function execArgs(pm: PackageManager, bin: string, args: readonly string[]): string[];
22
+ /** The same invocation as a display string, for logs and the JSON envelope. */
23
+ export declare function formatCommand(bin: string, args: readonly string[]): string;
24
+ /** The install command that adds `packages` as dev dependencies. */
25
+ export declare function installCommand(pm: PackageManager, packages: readonly string[]): string;
@@ -0,0 +1,57 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ /**
4
+ * Detect the package manager from the lockfile in the project root, falling
5
+ * back to npm. Only the lockfile is authoritative — we do not parse
6
+ * `packageManager` fields or probe $PATH.
7
+ */
8
+ export function detectPackageManager(projectDir) {
9
+ const has = (file) => fs.existsSync(path.join(projectDir, file));
10
+ if (has('pnpm-lock.yaml'))
11
+ return { name: 'pnpm', bin: 'pnpm' };
12
+ if (has('yarn.lock'))
13
+ return { name: 'yarn', bin: 'yarn' };
14
+ if (has('bun.lockb') || has('bun.lock'))
15
+ return { name: 'bun', bin: 'bun' };
16
+ return { name: 'npm', bin: 'npm' };
17
+ }
18
+ /**
19
+ * The argv for running a project-local binary through a package manager.
20
+ *
21
+ * The harness shell has no `node_modules` of its own — `next` lives in the
22
+ * partner's project root and resolves upward — so we cannot spawn the bin
23
+ * directly by path without guessing a layout (pnpm's is not npm's). Every
24
+ * package manager already knows how to find it; each just spells it
25
+ * differently.
26
+ */
27
+ export function execArgs(pm, bin, args) {
28
+ switch (pm.name) {
29
+ case 'pnpm':
30
+ return ['exec', bin, ...args];
31
+ case 'yarn':
32
+ return [bin, ...args];
33
+ case 'bun':
34
+ return ['x', bin, ...args];
35
+ case 'npm':
36
+ return ['exec', '--', bin, ...args];
37
+ }
38
+ }
39
+ /** The same invocation as a display string, for logs and the JSON envelope. */
40
+ export function formatCommand(bin, args) {
41
+ return [bin, ...args].join(' ');
42
+ }
43
+ /** The install command that adds `packages` as dev dependencies. */
44
+ export function installCommand(pm, packages) {
45
+ const spec = packages.join(' ');
46
+ switch (pm.name) {
47
+ case 'pnpm':
48
+ return `pnpm add -D ${spec}`;
49
+ case 'yarn':
50
+ return `yarn add -D ${spec}`;
51
+ case 'bun':
52
+ return `bun add -d ${spec}`;
53
+ case 'npm':
54
+ return `npm install -D ${spec}`;
55
+ }
56
+ }
57
+ //# sourceMappingURL=package-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-manager.js","sourceRoot":"","sources":["../src/package-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AASlC;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;IACzE,IAAI,GAAG,CAAC,gBAAgB,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAChE,IAAI,GAAG,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAC3D,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAC5E,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CACtB,EAAkB,EAClB,GAAW,EACX,IAAuB;IAEvB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,MAAM;YACT,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAChC,KAAK,MAAM;YACT,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACxB,KAAK,KAAK;YACR,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC7B,KAAK,KAAK;YACR,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACxC,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,IAAuB;IAChE,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,cAAc,CAC5B,EAAkB,EAClB,QAA2B;IAE3B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,MAAM;YACT,OAAO,eAAe,IAAI,EAAE,CAAC;QAC/B,KAAK,MAAM;YACT,OAAO,eAAe,IAAI,EAAE,CAAC;QAC/B,KAAK,KAAK;YACR,OAAO,cAAc,IAAI,EAAE,CAAC;QAC9B,KAAK,KAAK;YACR,OAAO,kBAAkB,IAAI,EAAE,CAAC;IACpC,CAAC;AACH,CAAC","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nexport type PackageManagerName = 'pnpm' | 'yarn' | 'bun' | 'npm';\n\nexport interface PackageManager {\n name: PackageManagerName;\n bin: string;\n}\n\n/**\n * Detect the package manager from the lockfile in the project root, falling\n * back to npm. Only the lockfile is authoritative — we do not parse\n * `packageManager` fields or probe $PATH.\n */\nexport function detectPackageManager(projectDir: string): PackageManager {\n const has = (file: string) => fs.existsSync(path.join(projectDir, file));\n if (has('pnpm-lock.yaml')) return { name: 'pnpm', bin: 'pnpm' };\n if (has('yarn.lock')) return { name: 'yarn', bin: 'yarn' };\n if (has('bun.lockb') || has('bun.lock')) return { name: 'bun', bin: 'bun' };\n return { name: 'npm', bin: 'npm' };\n}\n\n/**\n * The argv for running a project-local binary through a package manager.\n *\n * The harness shell has no `node_modules` of its own — `next` lives in the\n * partner's project root and resolves upward — so we cannot spawn the bin\n * directly by path without guessing a layout (pnpm's is not npm's). Every\n * package manager already knows how to find it; each just spells it\n * differently.\n */\nexport function execArgs(\n pm: PackageManager,\n bin: string,\n args: readonly string[],\n): string[] {\n switch (pm.name) {\n case 'pnpm':\n return ['exec', bin, ...args];\n case 'yarn':\n return [bin, ...args];\n case 'bun':\n return ['x', bin, ...args];\n case 'npm':\n return ['exec', '--', bin, ...args];\n }\n}\n\n/** The same invocation as a display string, for logs and the JSON envelope. */\nexport function formatCommand(bin: string, args: readonly string[]): string {\n return [bin, ...args].join(' ');\n}\n\n/** The install command that adds `packages` as dev dependencies. */\nexport function installCommand(\n pm: PackageManager,\n packages: readonly string[],\n): string {\n const spec = packages.join(' ');\n switch (pm.name) {\n case 'pnpm':\n return `pnpm add -D ${spec}`;\n case 'yarn':\n return `yarn add -D ${spec}`;\n case 'bun':\n return `bun add -d ${spec}`;\n case 'npm':\n return `npm install -D ${spec}`;\n }\n}\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A hand-rolled argv parser — deliberately NOT commander/yargs/citty. The
3
+ * agent-native envelope needs total control over how an unknown flag is
4
+ * reported (exit 2 through the JSON envelope with a hint, never a library's own
5
+ * `console.error` + `process.exit`), and it must never abbreviate, never
6
+ * auto-print help, and never guess. Every off-ramp routes through `usageError`.
7
+ *
8
+ * Supports: `--flag` (boolean), `--key=value`, `--key value`, `--no-<bool>`
9
+ * negation, positionals, and a `--` passthrough where everything after the
10
+ * first bare `--` is captured verbatim (that argv belongs to `test`'s child).
11
+ */
12
+ export type FlagType = 'boolean' | 'string';
13
+ export interface FlagSpec {
14
+ type: FlagType;
15
+ /** Optional default applied when the flag is absent. */
16
+ default?: string | boolean;
17
+ }
18
+ export type FlagSpecs = Record<string, FlagSpec>;
19
+ export interface ParsedArgs {
20
+ positionals: string[];
21
+ flags: Record<string, string | boolean>;
22
+ /** Raw argv after the first standalone `--`, unparsed. */
23
+ passthrough: string[];
24
+ /** True when a standalone `--` was present (even with nothing after it). */
25
+ hasPassthrough: boolean;
26
+ }
27
+ export declare function parseArgv(argv: readonly string[], specs: FlagSpecs): ParsedArgs;
@@ -0,0 +1,74 @@
1
+ import { usageError } from './errors.js';
2
+ export function parseArgv(argv, specs) {
3
+ var _a;
4
+ const positionals = [];
5
+ const flags = {};
6
+ const passthrough = [];
7
+ let hasPassthrough = false;
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const token = argv[i];
10
+ // Everything after the first standalone `--` is passthrough, verbatim.
11
+ if (token === '--') {
12
+ hasPassthrough = true;
13
+ passthrough.push(...argv.slice(i + 1));
14
+ break;
15
+ }
16
+ // A bare positional (does not start with `-`), or a lone `-`.
17
+ if (!token.startsWith('--')) {
18
+ if (token.startsWith('-') && token !== '-') {
19
+ throw usageError(`Unknown flag "${token}". Short flags are not supported.`, `Use the long form (e.g. "--json"). Run "ekanos --help" for the ` +
20
+ `full flag list.`);
21
+ }
22
+ positionals.push(token);
23
+ continue;
24
+ }
25
+ // A long flag. Split an inline `--key=value`.
26
+ const body = token.slice(2);
27
+ const eq = body.indexOf('=');
28
+ const hasInlineValue = eq !== -1;
29
+ let name = hasInlineValue ? body.slice(0, eq) : body;
30
+ const inlineValue = hasInlineValue ? body.slice(eq + 1) : undefined;
31
+ // `--no-<bool>` negation.
32
+ let negated = false;
33
+ if (!hasInlineValue && name.startsWith('no-')) {
34
+ const candidate = name.slice(3);
35
+ if (((_a = specs[candidate]) === null || _a === void 0 ? void 0 : _a.type) === 'boolean') {
36
+ negated = true;
37
+ name = candidate;
38
+ }
39
+ }
40
+ const spec = specs[name];
41
+ if (!spec) {
42
+ throw usageError(`Unknown flag "--${hasInlineValue ? name : body}".`, `Remove it or check the spelling. Run "ekanos --help" (or ` +
43
+ `"ekanos <command> --help") for the accepted flags.`);
44
+ }
45
+ if (spec.type === 'boolean') {
46
+ if (hasInlineValue) {
47
+ throw usageError(`Flag "--${name}" is a boolean and does not take a value.`, `Pass "--${name}" to enable it or "--no-${name}" to disable it — ` +
48
+ `not "--${name}=${inlineValue !== null && inlineValue !== void 0 ? inlineValue : ''}".`);
49
+ }
50
+ flags[name] = !negated;
51
+ continue;
52
+ }
53
+ // String flag: value is inline (`--key=value`) or the next token
54
+ // (`--key value`).
55
+ if (hasInlineValue) {
56
+ flags[name] = inlineValue;
57
+ continue;
58
+ }
59
+ const next = argv[i + 1];
60
+ if (next === undefined || next === '--') {
61
+ throw usageError(`Flag "--${name}" expects a value.`, `Provide one as "--${name} <value>" or "--${name}=<value>".`);
62
+ }
63
+ flags[name] = next;
64
+ i++;
65
+ }
66
+ // Apply declared defaults for flags the user did not pass.
67
+ for (const [name, spec] of Object.entries(specs)) {
68
+ if (spec.default !== undefined && !(name in flags)) {
69
+ flags[name] = spec.default;
70
+ }
71
+ }
72
+ return { positionals, flags, passthrough, hasPassthrough };
73
+ }
74
+ //# sourceMappingURL=parse-argv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-argv.js","sourceRoot":"","sources":["../src/parse-argv.ts"],"names":[],"mappings":"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","sourcesContent":["import { usageError } from './errors';\n\n/**\n * A hand-rolled argv parser — deliberately NOT commander/yargs/citty. The\n * agent-native envelope needs total control over how an unknown flag is\n * reported (exit 2 through the JSON envelope with a hint, never a library's own\n * `console.error` + `process.exit`), and it must never abbreviate, never\n * auto-print help, and never guess. Every off-ramp routes through `usageError`.\n *\n * Supports: `--flag` (boolean), `--key=value`, `--key value`, `--no-<bool>`\n * negation, positionals, and a `--` passthrough where everything after the\n * first bare `--` is captured verbatim (that argv belongs to `test`'s child).\n */\n\nexport type FlagType = 'boolean' | 'string';\n\nexport interface FlagSpec {\n type: FlagType;\n /** Optional default applied when the flag is absent. */\n default?: string | boolean;\n}\n\nexport type FlagSpecs = Record<string, FlagSpec>;\n\nexport interface ParsedArgs {\n positionals: string[];\n flags: Record<string, string | boolean>;\n /** Raw argv after the first standalone `--`, unparsed. */\n passthrough: string[];\n /** True when a standalone `--` was present (even with nothing after it). */\n hasPassthrough: boolean;\n}\n\nexport function parseArgv(\n argv: readonly string[],\n specs: FlagSpecs,\n): ParsedArgs {\n const positionals: string[] = [];\n const flags: Record<string, string | boolean> = {};\n const passthrough: string[] = [];\n let hasPassthrough = false;\n\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i]!;\n\n // Everything after the first standalone `--` is passthrough, verbatim.\n if (token === '--') {\n hasPassthrough = true;\n passthrough.push(...argv.slice(i + 1));\n break;\n }\n\n // A bare positional (does not start with `-`), or a lone `-`.\n if (!token.startsWith('--')) {\n if (token.startsWith('-') && token !== '-') {\n throw usageError(\n `Unknown flag \"${token}\". Short flags are not supported.`,\n `Use the long form (e.g. \"--json\"). Run \"ekanos --help\" for the ` +\n `full flag list.`,\n );\n }\n positionals.push(token);\n continue;\n }\n\n // A long flag. Split an inline `--key=value`.\n const body = token.slice(2);\n const eq = body.indexOf('=');\n const hasInlineValue = eq !== -1;\n let name = hasInlineValue ? body.slice(0, eq) : body;\n const inlineValue = hasInlineValue ? body.slice(eq + 1) : undefined;\n\n // `--no-<bool>` negation.\n let negated = false;\n if (!hasInlineValue && name.startsWith('no-')) {\n const candidate = name.slice(3);\n if (specs[candidate]?.type === 'boolean') {\n negated = true;\n name = candidate;\n }\n }\n\n const spec = specs[name];\n if (!spec) {\n throw usageError(\n `Unknown flag \"--${hasInlineValue ? name : body}\".`,\n `Remove it or check the spelling. Run \"ekanos --help\" (or ` +\n `\"ekanos <command> --help\") for the accepted flags.`,\n );\n }\n\n if (spec.type === 'boolean') {\n if (hasInlineValue) {\n throw usageError(\n `Flag \"--${name}\" is a boolean and does not take a value.`,\n `Pass \"--${name}\" to enable it or \"--no-${name}\" to disable it — ` +\n `not \"--${name}=${inlineValue ?? ''}\".`,\n );\n }\n flags[name] = !negated;\n continue;\n }\n\n // String flag: value is inline (`--key=value`) or the next token\n // (`--key value`).\n if (hasInlineValue) {\n flags[name] = inlineValue!;\n continue;\n }\n\n const next = argv[i + 1];\n if (next === undefined || next === '--') {\n throw usageError(\n `Flag \"--${name}\" expects a value.`,\n `Provide one as \"--${name} <value>\" or \"--${name}=<value>\".`,\n );\n }\n flags[name] = next;\n i++;\n }\n\n // Apply declared defaults for flags the user did not pass.\n for (const [name, spec] of Object.entries(specs)) {\n if (spec.default !== undefined && !(name in flags)) {\n flags[name] = spec.default;\n }\n }\n\n return { positionals, flags, passthrough, hasPassthrough };\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import type { Finding } from '@ekanos/integration-schema';
2
+ /**
3
+ * Project-level integrity checks that read files in the project root (next to
4
+ * ekanos.json), NOT the integration definition. These are deliberately kept
5
+ * out of the schema package's definition collectors: the definition is valid or
6
+ * not on its own; whether the partner's TEST runner is configured to load the
7
+ * published SDK is a property of THIS project.
8
+ *
9
+ * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The
10
+ * published SDK dist imports extensionless `next/link`, so without
11
+ * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the
12
+ * test file silently imports nothing, and vitest reports green over zero tests.
13
+ *
14
+ * The check is conditional: if the project declares NO vitest/vite config at
15
+ * all, there is nothing to assert (the `test` verb would surface a missing
16
+ * runner on its own). If a config IS present, it MUST inline the SDK.
17
+ */
18
+ export declare function collectProjectFindings(projectDir: string): Finding[];
@@ -0,0 +1,77 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ const VITEST_CONFIG_NAMES = [
4
+ 'vitest.config.ts',
5
+ 'vitest.config.mts',
6
+ 'vitest.config.cts',
7
+ 'vitest.config.js',
8
+ 'vitest.config.mjs',
9
+ 'vitest.config.cjs',
10
+ 'vite.config.ts',
11
+ 'vite.config.mts',
12
+ 'vite.config.js',
13
+ 'vite.config.mjs',
14
+ ];
15
+ /** The exact block the hint quotes — the one config that makes the SDK load. */
16
+ const REQUIRED_INLINE_BLOCK = [
17
+ 'test: {',
18
+ ' server: {',
19
+ ' deps: {',
20
+ " inline: ['@ekanos/sdk'],",
21
+ ' },',
22
+ ' },',
23
+ '}',
24
+ ].join('\n');
25
+ /**
26
+ * Project-level integrity checks that read files in the project root (next to
27
+ * ekanos.json), NOT the integration definition. These are deliberately kept
28
+ * out of the schema package's definition collectors: the definition is valid or
29
+ * not on its own; whether the partner's TEST runner is configured to load the
30
+ * published SDK is a property of THIS project.
31
+ *
32
+ * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The
33
+ * published SDK dist imports extensionless `next/link`, so without
34
+ * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the
35
+ * test file silently imports nothing, and vitest reports green over zero tests.
36
+ *
37
+ * The check is conditional: if the project declares NO vitest/vite config at
38
+ * all, there is nothing to assert (the `test` verb would surface a missing
39
+ * runner on its own). If a config IS present, it MUST inline the SDK.
40
+ */
41
+ export function collectProjectFindings(projectDir) {
42
+ const configFile = VITEST_CONFIG_NAMES.map((name) => path.join(projectDir, name)).find((candidate) => fs.existsSync(candidate));
43
+ if (!configFile)
44
+ return [];
45
+ const source = fs.readFileSync(configFile, 'utf8');
46
+ if (vitestInlinesSdk(source))
47
+ return [];
48
+ return [
49
+ {
50
+ check: 'project.vitest-inline-sdk',
51
+ severity: 'error',
52
+ file: configFile,
53
+ message: `${path.basename(configFile)} does not inline @ekanos/sdk. The ` +
54
+ `published SDK dist imports extensionless "next/link", so without ` +
55
+ `test.server.deps.inline including "@ekanos/sdk", vitest cannot load ` +
56
+ `the SDK — the test file imports nothing and vitest reports green ` +
57
+ `over zero tests.`,
58
+ hint: `Add this to your vitest config's defineConfig so the SDK is ` +
59
+ `transformed for the test runner:\n\n${REQUIRED_INLINE_BLOCK}`,
60
+ },
61
+ ];
62
+ }
63
+ /**
64
+ * True when the config source contains an `inline: [ … ]` array that lists
65
+ * `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module
66
+ * would run arbitrary code; a lint-style scan does not.
67
+ */
68
+ function vitestInlinesSdk(source) {
69
+ var _a;
70
+ const inlineArrayPattern = /inline\s*:\s*\[([\s\S]*?)\]/g;
71
+ for (let match = inlineArrayPattern.exec(source); match !== null; match = inlineArrayPattern.exec(source)) {
72
+ if (/['"]@ekanos\/sdk['"]/.test((_a = match[1]) !== null && _a !== void 0 ? _a : ''))
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+ //# sourceMappingURL=project-checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-checks.js","sourceRoot":"","sources":["../src/project-checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,mBAAmB,GAAG;IAC1B,kBAAkB;IAClB,mBAAmB;IACnB,mBAAmB;IACnB,kBAAkB;IAClB,mBAAmB;IACnB,mBAAmB;IACnB,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,iBAAiB;CAClB,CAAC;AAEF,gFAAgF;AAChF,MAAM,qBAAqB,GAAG;IAC5B,SAAS;IACT,aAAa;IACb,aAAa;IACb,gCAAgC;IAChC,QAAQ;IACR,MAAM;IACN,GAAG;CACJ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,MAAM,UAAU,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAClD,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAC5B,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAE3B,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnD,IAAI,gBAAgB,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,OAAO;QACL;YACE,KAAK,EAAE,2BAA2B;YAClC,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,UAAU;YAChB,OAAO,EACL,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,oCAAoC;gBAChE,mEAAmE;gBACnE,sEAAsE;gBACtE,mEAAmE;gBACnE,kBAAkB;YACpB,IAAI,EACF,8DAA8D;gBAC9D,uCAAuC,qBAAqB,EAAE;SACjE;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,MAAc;;IACtC,MAAM,kBAAkB,GAAG,8BAA8B,CAAC;IAC1D,KACE,IAAI,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAC3C,KAAK,KAAK,IAAI,EACd,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EACvC,CAAC;QACD,IAAI,sBAAsB,CAAC,IAAI,CAAC,MAAA,KAAK,CAAC,CAAC,CAAC,mCAAI,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;IAC/D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nconst VITEST_CONFIG_NAMES = [\n 'vitest.config.ts',\n 'vitest.config.mts',\n 'vitest.config.cts',\n 'vitest.config.js',\n 'vitest.config.mjs',\n 'vitest.config.cjs',\n 'vite.config.ts',\n 'vite.config.mts',\n 'vite.config.js',\n 'vite.config.mjs',\n];\n\n/** The exact block the hint quotes — the one config that makes the SDK load. */\nconst REQUIRED_INLINE_BLOCK = [\n 'test: {',\n ' server: {',\n ' deps: {',\n \" inline: ['@ekanos/sdk'],\",\n ' },',\n ' },',\n '}',\n].join('\\n');\n\n/**\n * Project-level integrity checks that read files in the project root (next to\n * ekanos.json), NOT the integration definition. These are deliberately kept\n * out of the schema package's definition collectors: the definition is valid or\n * not on its own; whether the partner's TEST runner is configured to load the\n * published SDK is a property of THIS project.\n *\n * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The\n * published SDK dist imports extensionless `next/link`, so without\n * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the\n * test file silently imports nothing, and vitest reports green over zero tests.\n *\n * The check is conditional: if the project declares NO vitest/vite config at\n * all, there is nothing to assert (the `test` verb would surface a missing\n * runner on its own). If a config IS present, it MUST inline the SDK.\n */\nexport function collectProjectFindings(projectDir: string): Finding[] {\n const configFile = VITEST_CONFIG_NAMES.map((name) =>\n path.join(projectDir, name),\n ).find((candidate) => fs.existsSync(candidate));\n\n if (!configFile) return [];\n\n const source = fs.readFileSync(configFile, 'utf8');\n if (vitestInlinesSdk(source)) return [];\n\n return [\n {\n check: 'project.vitest-inline-sdk',\n severity: 'error',\n file: configFile,\n message:\n `${path.basename(configFile)} does not inline @ekanos/sdk. The ` +\n `published SDK dist imports extensionless \"next/link\", so without ` +\n `test.server.deps.inline including \"@ekanos/sdk\", vitest cannot load ` +\n `the SDK — the test file imports nothing and vitest reports green ` +\n `over zero tests.`,\n hint:\n `Add this to your vitest config's defineConfig so the SDK is ` +\n `transformed for the test runner:\\n\\n${REQUIRED_INLINE_BLOCK}`,\n },\n ];\n}\n\n/**\n * True when the config source contains an `inline: [ … ]` array that lists\n * `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module\n * would run arbitrary code; a lint-style scan does not.\n */\nfunction vitestInlinesSdk(source: string): boolean {\n const inlineArrayPattern = /inline\\s*:\\s*\\[([\\s\\S]*?)\\]/g;\n for (\n let match = inlineArrayPattern.exec(source);\n match !== null;\n match = inlineArrayPattern.exec(source)\n ) {\n if (/['\"]@ekanos\\/sdk['\"]/.test(match[1] ?? '')) return true;\n }\n return false;\n}\n"]}
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod';
2
+ /** One integration: the pair that addresses a definition module. */
3
+ declare const IntegrationEntrySchema: z.ZodObject<{
4
+ slug: z.ZodString;
5
+ entry: z.ZodString;
6
+ }, "strict", z.ZodTypeAny, {
7
+ slug: string;
8
+ entry: string;
9
+ }, {
10
+ slug: string;
11
+ entry: string;
12
+ }>;
13
+ export type IntegrationEntry = z.infer<typeof IntegrationEntrySchema>;
14
+ /**
15
+ * `ekanos.json` — the project contract. Written by `init`, read by `validate`,
16
+ * `dev` and `test`.
17
+ *
18
+ * Two shapes, because a project can hold more than one integration and the
19
+ * single-integration form is what `init` writes and what most projects keep:
20
+ *
21
+ * ```json
22
+ * { "slug": "acme-crm", "entry": "src/integration.ts" }
23
+ *
24
+ * { "integrations": [
25
+ * { "slug": "acme-crm", "entry": "src/crm.ts" },
26
+ * { "slug": "acme-billing", "entry": "src/billing.ts" }
27
+ * ] }
28
+ * ```
29
+ *
30
+ * A second integration used to be invisible to the CLI entirely — the harness
31
+ * registry is an array and `collectCollisionFindings` exists precisely to
32
+ * cross-check several definitions against each other, but `validate` could
33
+ * only ever see one. Both forms normalise to the same list, so nothing
34
+ * downstream has to know which was written.
35
+ */
36
+ export declare const EkanosProjectSchema: z.ZodEffects<z.ZodObject<{
37
+ $schema: z.ZodOptional<z.ZodString>;
38
+ slug: z.ZodOptional<z.ZodString>;
39
+ entry: z.ZodOptional<z.ZodString>;
40
+ integrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
+ slug: z.ZodString;
42
+ entry: z.ZodString;
43
+ }, "strict", z.ZodTypeAny, {
44
+ slug: string;
45
+ entry: string;
46
+ }, {
47
+ slug: string;
48
+ entry: string;
49
+ }>, "many">>;
50
+ sourceGlobs: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, "many">>;
51
+ }, "strict", z.ZodTypeAny, {
52
+ slug?: string | undefined;
53
+ entry?: string | undefined;
54
+ $schema?: string | undefined;
55
+ integrations?: {
56
+ slug: string;
57
+ entry: string;
58
+ }[] | undefined;
59
+ sourceGlobs?: string[] | undefined;
60
+ }, {
61
+ slug?: string | undefined;
62
+ entry?: string | undefined;
63
+ $schema?: string | undefined;
64
+ integrations?: {
65
+ slug: string;
66
+ entry: string;
67
+ }[] | undefined;
68
+ sourceGlobs?: string[] | undefined;
69
+ }>, {
70
+ slug?: string | undefined;
71
+ entry?: string | undefined;
72
+ $schema?: string | undefined;
73
+ integrations?: {
74
+ slug: string;
75
+ entry: string;
76
+ }[] | undefined;
77
+ sourceGlobs?: string[] | undefined;
78
+ }, {
79
+ slug?: string | undefined;
80
+ entry?: string | undefined;
81
+ $schema?: string | undefined;
82
+ integrations?: {
83
+ slug: string;
84
+ entry: string;
85
+ }[] | undefined;
86
+ sourceGlobs?: string[] | undefined;
87
+ }>;
88
+ export type EkanosProject = z.infer<typeof EkanosProjectSchema>;
89
+ /** Both `ekanos.json` shapes, flattened to the one the CLI works in. */
90
+ export declare function projectIntegrations(project: EkanosProject): IntegrationEntry[];
91
+ export declare const EKANOS_CONFIG_FILENAME = "ekanos.json";
92
+ /** One integration from `ekanos.json`, with its entry resolved on disk. */
93
+ export interface LoadedIntegration {
94
+ slug: string;
95
+ entry: string;
96
+ /** Absolute path to the resolved entry module. */
97
+ entryPath: string;
98
+ }
99
+ export interface LoadedProject {
100
+ project: EkanosProject;
101
+ /** Absolute path to the directory containing ekanos.json (the project root). */
102
+ projectDir: string;
103
+ /** Absolute path to ekanos.json itself. */
104
+ configPath: string;
105
+ /** Every integration the project declares, in declaration order. */
106
+ integrations: LoadedIntegration[];
107
+ /**
108
+ * The first integration. It names the generated harness shell and is what a
109
+ * single-integration project means by "the" integration; nothing else
110
+ * privileges it.
111
+ */
112
+ primary: LoadedIntegration;
113
+ }
114
+ /**
115
+ * Read and validate ekanos.json from a directory. Missing file → not-found
116
+ * (exit 6); malformed JSON or a schema violation → precondition failed
117
+ * (exit 9), because a project the CLI cannot even read is a precondition for
118
+ * every verb, not a definition-level validation finding.
119
+ */
120
+ export declare function loadProject(dir: string): LoadedProject;
121
+ /**
122
+ * Fail unless the project is an ES module package.
123
+ *
124
+ * `init` sets `"type": "module"` and says so, but a warning can be scrolled
125
+ * past, and a partner who misses it gets a confusing module-resolution error
126
+ * from deep inside Node or Next rather than a sentence naming the problem.
127
+ * This is the backstop: one precondition, in the loader every verb after
128
+ * `init` goes through, so the failure is loud and says what to do.
129
+ *
130
+ * `init` deliberately does NOT come through here — it is the thing that fixes
131
+ * this state.
132
+ *
133
+ * A project with no package.json at all is left alone: `dev` reports missing
134
+ * dependencies with its own better message, and `validate`/`test` have their
135
+ * own preconditions. We only assert on a manifest that exists and disagrees.
136
+ */
137
+ export declare function assertEsmProject(projectDir: string): void;
138
+ /** Serialize an ekanos.json body with a trailing newline. */
139
+ export declare function serializeProject(project: EkanosProject): string;
140
+ export {};