@ekanos/cli 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -4
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +118 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/audiences.d.ts +31 -0
- package/dist/commands/audiences.js +239 -0
- package/dist/commands/audiences.js.map +1 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +41 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +43 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/codegen.d.ts +2 -0
- package/dist/commands/codegen.js +49 -0
- package/dist/commands/codegen.js.map +1 -0
- package/dist/commands/contract.d.ts +52 -0
- package/dist/commands/contract.js +300 -0
- package/dist/commands/contract.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.js +186 -12
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/index-experience.d.ts +31 -0
- package/dist/commands/index-experience.js +21 -0
- package/dist/commands/index-experience.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +12 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +39 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/publish.d.ts +59 -0
- package/dist/commands/publish.js +118 -65
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +6 -0
- package/dist/commands/scope-notice.js +15 -0
- package/dist/commands/scope-notice.js.map +1 -1
- package/dist/commands/sources.js +2 -0
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +150 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/teams-audience.d.ts +12 -0
- package/dist/commands/teams-audience.js +144 -0
- package/dist/commands/teams-audience.js.map +1 -0
- package/dist/commands/teams-scope.js +4 -0
- package/dist/commands/teams-scope.js.map +1 -1
- package/dist/commands/teams.d.ts +1 -1
- package/dist/commands/teams.js +6 -1
- package/dist/commands/teams.js.map +1 -1
- package/dist/compatibility.d.ts +10 -1
- package/dist/compatibility.js +10 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/dev-experience.d.ts +125 -0
- package/dist/dev-experience.js +128 -0
- package/dist/dev-experience.js.map +1 -0
- package/dist/experience/boundary.d.ts +12 -0
- package/dist/experience/boundary.js +260 -0
- package/dist/experience/boundary.js.map +1 -0
- package/dist/experience/build.d.ts +16 -0
- package/dist/experience/build.js +90 -0
- package/dist/experience/build.js.map +1 -0
- package/dist/experience/checks.d.ts +29 -0
- package/dist/experience/checks.js +133 -0
- package/dist/experience/checks.js.map +1 -0
- package/dist/experience/contract-sources.d.ts +36 -0
- package/dist/experience/contract-sources.js +111 -0
- package/dist/experience/contract-sources.js.map +1 -0
- package/dist/experience/definition-checks.d.ts +26 -0
- package/dist/experience/definition-checks.js +201 -0
- package/dist/experience/definition-checks.js.map +1 -0
- package/dist/experience/definition-shape.d.ts +60 -0
- package/dist/experience/definition-shape.js +2 -0
- package/dist/experience/definition-shape.js.map +1 -0
- package/dist/experience/init.d.ts +10 -0
- package/dist/experience/init.js +64 -0
- package/dist/experience/init.js.map +1 -0
- package/dist/experience/integrations-codegen.d.ts +17 -0
- package/dist/experience/integrations-codegen.js +51 -0
- package/dist/experience/integrations-codegen.js.map +1 -0
- package/dist/experience/json-schema-to-ts.d.ts +1 -0
- package/dist/experience/json-schema-to-ts.js +90 -0
- package/dist/experience/json-schema-to-ts.js.map +1 -0
- package/dist/experience/load-experience.d.ts +23 -0
- package/dist/experience/load-experience.js +115 -0
- package/dist/experience/load-experience.js.map +1 -0
- package/dist/experience/overrides.d.ts +8 -0
- package/dist/experience/overrides.js +21 -0
- package/dist/experience/overrides.js.map +1 -0
- package/dist/experience/pin-integration.d.ts +8 -0
- package/dist/experience/pin-integration.js +17 -0
- package/dist/experience/pin-integration.js.map +1 -0
- package/dist/experience/pin-ops.d.ts +12 -0
- package/dist/experience/pin-ops.js +36 -0
- package/dist/experience/pin-ops.js.map +1 -0
- package/dist/experience/pins.d.ts +35 -0
- package/dist/experience/pins.js +91 -0
- package/dist/experience/pins.js.map +1 -0
- package/dist/experience/project.d.ts +52 -0
- package/dist/experience/project.js +149 -0
- package/dist/experience/project.js.map +1 -0
- package/dist/experience/publish.d.ts +51 -0
- package/dist/experience/publish.js +144 -0
- package/dist/experience/publish.js.map +1 -0
- package/dist/experience/regenerate.d.ts +10 -0
- package/dist/experience/regenerate.js +33 -0
- package/dist/experience/regenerate.js.map +1 -0
- package/dist/experience/report.d.ts +9 -0
- package/dist/experience/report.js +23 -0
- package/dist/experience/report.js.map +1 -0
- package/dist/harness-scaffold.d.ts +52 -18
- package/dist/harness-scaffold.js +189 -9
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.js +139 -7
- package/dist/index.js.map +1 -1
- package/dist/pack.js +5 -0
- package/dist/pack.js.map +1 -1
- package/dist/project-checks.d.ts +19 -0
- package/dist/project-checks.js +93 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +14 -0
- package/dist/project.js +10 -0
- package/dist/project.js.map +1 -1
- package/dist/repo/publish.d.ts +42 -0
- package/dist/repo/publish.js +225 -0
- package/dist/repo/publish.js.map +1 -0
- package/dist/repo/scaffold.d.ts +30 -0
- package/dist/repo/scaffold.js +165 -0
- package/dist/repo/scaffold.js.map +1 -0
- package/dist/teams-api.d.ts +37 -1
- package/dist/teams-api.js +62 -1
- package/dist/teams-api.js.map +1 -1
- package/dist/validate-findings.js +3 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +2 -2
- package/templates/ekanos.json.tmpl +1 -0
- package/templates/experience/AGENTS.md.tmpl +22 -0
- package/templates/experience/ekanos.json.tmpl +7 -0
- package/templates/experience/ekanos.lock.json.tmpl +4 -0
- package/templates/experience/eslint.config.mjs.tmpl +25 -0
- package/templates/experience/gitignore.tmpl +17 -0
- package/templates/experience/package.json.tmpl +41 -0
- package/templates/experience/src/.ekanos/integrations.d.ts.tmpl +3 -0
- package/templates/experience/src/experience.ts.tmpl +30 -0
- package/templates/experience/src/widgets/hello-widget.tsx.tmpl +40 -0
- package/templates/experience/tsconfig.json.tmpl +20 -0
- package/templates/harness/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +9 -0
- package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -1
- package/templates/harness/app/experience/layout.tsx.tmpl +17 -0
- package/templates/harness/app/experience/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/pages/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/review-queue/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/runs/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/widgets/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/workflows/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/workflows/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/connect/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/events/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/health/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/operations/[kind]/[name]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/operations/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/activation/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/layout.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/tile/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/triggers/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/widgets/[widgetId]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/runs/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/layout.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/page.tsx.tmpl +1 -0
- package/templates/harness/app/page.experience.tsx.tmpl +5 -0
- package/templates/harness/repo/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +11 -0
- package/templates/harness/repo/app/(repo)/connections/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/(repo)/layout.tsx.tmpl +1 -0
- package/templates/harness/repo/app/(repo)/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/experiences/[name]/connect/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/experiences/[name]/layout.tsx.tmpl +9 -0
- package/templates/harness/repo/app/experiences/page.tsx.tmpl +5 -0
- package/templates/harness/repo/app/harness-shell.tsx.tmpl +9 -0
- package/templates/harness/repo/app/integrations/[slug]/layout.tsx.tmpl +1 -0
- package/templates/harness/repo/app/integrations/page.tsx.tmpl +5 -0
- package/templates/harness/repo/app/repo-experiences.ts.tmpl +5 -0
- package/templates/harness/styles/globals.css.tmpl +15 -1
- package/templates/package.json.tmpl +5 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Contract, compatLine } from '@ekanos/integration-schema';
|
|
2
|
+
import type { ExperienceProject } from './project.js';
|
|
3
|
+
/** Where an integration's contract came from, for messages and `sync` bookkeeping. */
|
|
4
|
+
export interface ResolvedContract {
|
|
5
|
+
contract: Contract;
|
|
6
|
+
origin: string;
|
|
7
|
+
kind: 'dir' | 'path' | 'snapshot';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A pluggable contract source. The dir/path/snapshot sources below are the
|
|
11
|
+
* offline defaults; the harness resolver (catalog, published packages) plugs
|
|
12
|
+
* in here as another source, so `add`/`sync` never learn where contracts live.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContractSource {
|
|
15
|
+
name: string;
|
|
16
|
+
resolve(integration: string, line: string): Promise<ResolvedContract | null> | ResolvedContract | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SourceOptions {
|
|
19
|
+
/** A directory of `*.contract.json` (the monorepo contracts folder or a registry mirror). */
|
|
20
|
+
fromDir?: string;
|
|
21
|
+
/** An integration project directory; its `contract.json` is read. */
|
|
22
|
+
fromPath?: string;
|
|
23
|
+
env?: Record<string, string | undefined>;
|
|
24
|
+
}
|
|
25
|
+
/** Every `*.contract.json` of `dir` for `integration`, highest contract version within `line` first. */
|
|
26
|
+
export declare function contractsInDir(dir: string, integration: string, line: string): Contract[];
|
|
27
|
+
export declare function dirSource(dir: string): ContractSource;
|
|
28
|
+
export declare function pathSource(projectPath: string): ContractSource;
|
|
29
|
+
/** The committed pin: `<contractsDir>/<integration>@<version>.contract.json`. */
|
|
30
|
+
export declare function snapshotFile(project: ExperienceProject, integration: string, line: string): string;
|
|
31
|
+
export declare function snapshotSource(project: ExperienceProject): ContractSource;
|
|
32
|
+
/** Sources in resolution order: explicit flags, env, then (optionally) the snapshot. */
|
|
33
|
+
export declare function defaultSources(project: ExperienceProject, options: SourceOptions, extra?: readonly ContractSource[], includeSnapshot?: boolean): ContractSource[];
|
|
34
|
+
export declare function resolveContract(sources: readonly ContractSource[], integration: string, line: string): Promise<ResolvedContract>;
|
|
35
|
+
/** Line for a package version, re-exported so callers need no schema import. */
|
|
36
|
+
export { compatLine };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { compareSemver, compatLine, verifyContract, } from '@ekanos/integration-schema';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { notFoundError, validationError } from '../errors.js';
|
|
5
|
+
function readContractFile(file) {
|
|
6
|
+
let json;
|
|
7
|
+
try {
|
|
8
|
+
json = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch (_a) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const verified = verifyContract(json);
|
|
14
|
+
if (!verified.contract || verified.problems.length > 0) {
|
|
15
|
+
const first = verified.problems[0];
|
|
16
|
+
throw validationError(`${file} is not a valid contract: ${first ? `${first.rule}: ${first.message}` : 'unreadable'}`, 'Regenerate it with "ekanos contract" in the integration, or point at a valid contracts directory.');
|
|
17
|
+
}
|
|
18
|
+
return verified.contract;
|
|
19
|
+
}
|
|
20
|
+
/** Every `*.contract.json` of `dir` for `integration`, highest contract version within `line` first. */
|
|
21
|
+
export function contractsInDir(dir, integration, line) {
|
|
22
|
+
let names;
|
|
23
|
+
try {
|
|
24
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.contract.json'));
|
|
25
|
+
}
|
|
26
|
+
catch (_a) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const found = [];
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
if (!name.startsWith(`${integration}@`) &&
|
|
32
|
+
!name.startsWith(`${integration}.`)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const contract = readContractFile(path.join(dir, name));
|
|
36
|
+
if (contract &&
|
|
37
|
+
contract.integration === integration &&
|
|
38
|
+
contract.compatLine === line) {
|
|
39
|
+
found.push(contract);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return found.sort((a, b) => compareSemver(b.contractVersion, a.contractVersion));
|
|
43
|
+
}
|
|
44
|
+
export function dirSource(dir) {
|
|
45
|
+
return {
|
|
46
|
+
name: `dir ${dir}`,
|
|
47
|
+
resolve(integration, line) {
|
|
48
|
+
const [best] = contractsInDir(dir, integration, line);
|
|
49
|
+
return best ? { contract: best, origin: dir, kind: 'dir' } : null;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function pathSource(projectPath) {
|
|
54
|
+
return {
|
|
55
|
+
name: `path ${projectPath}`,
|
|
56
|
+
resolve(integration, line) {
|
|
57
|
+
const file = path.join(path.resolve(projectPath), 'contract.json');
|
|
58
|
+
if (!fs.existsSync(file)) {
|
|
59
|
+
throw notFoundError(`${file} does not exist.`, 'Run "ekanos contract" in that integration to derive its contract.json.');
|
|
60
|
+
}
|
|
61
|
+
const contract = readContractFile(file);
|
|
62
|
+
if (!contract ||
|
|
63
|
+
contract.integration !== integration ||
|
|
64
|
+
contract.compatLine !== line) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return { contract, origin: file, kind: 'path' };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** The committed pin: `<contractsDir>/<integration>@<version>.contract.json`. */
|
|
72
|
+
export function snapshotFile(project, integration, line) {
|
|
73
|
+
return path.join(project.contractsDir, `${integration}@${line}.contract.json`);
|
|
74
|
+
}
|
|
75
|
+
export function snapshotSource(project) {
|
|
76
|
+
return {
|
|
77
|
+
name: 'committed snapshot',
|
|
78
|
+
resolve(integration, line) {
|
|
79
|
+
const file = snapshotFile(project, integration, line);
|
|
80
|
+
const contract = fs.existsSync(file) ? readContractFile(file) : null;
|
|
81
|
+
return contract ? { contract, origin: file, kind: 'snapshot' } : null;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Sources in resolution order: explicit flags, env, then (optionally) the snapshot. */
|
|
86
|
+
export function defaultSources(project, options, extra = [], includeSnapshot = true) {
|
|
87
|
+
var _a;
|
|
88
|
+
const sources = [];
|
|
89
|
+
if (options.fromPath)
|
|
90
|
+
sources.push(pathSource(options.fromPath));
|
|
91
|
+
if (options.fromDir)
|
|
92
|
+
sources.push(dirSource(path.resolve(options.fromDir)));
|
|
93
|
+
const envDir = (_a = options.env) === null || _a === void 0 ? void 0 : _a.EKANOS_CONTRACTS_DIR;
|
|
94
|
+
if (!options.fromDir && envDir)
|
|
95
|
+
sources.push(dirSource(path.resolve(envDir)));
|
|
96
|
+
sources.push(...extra);
|
|
97
|
+
if (includeSnapshot)
|
|
98
|
+
sources.push(snapshotSource(project));
|
|
99
|
+
return sources;
|
|
100
|
+
}
|
|
101
|
+
export async function resolveContract(sources, integration, line) {
|
|
102
|
+
for (const source of sources) {
|
|
103
|
+
const hit = await source.resolve(integration, line);
|
|
104
|
+
if (hit)
|
|
105
|
+
return hit;
|
|
106
|
+
}
|
|
107
|
+
throw notFoundError(`No contract for ${integration}@${line} (looked in: ${sources.map((s) => s.name).join(', ') || 'nothing'}).`, 'Pass --from-dir <contracts folder> or --from-path <integration project>, or set EKANOS_CONTRACTS_DIR.');
|
|
108
|
+
}
|
|
109
|
+
/** Line for a package version, re-exported so callers need no schema import. */
|
|
110
|
+
export { compatLine };
|
|
111
|
+
//# sourceMappingURL=contract-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-sources.js","sourceRoot":"","sources":["../../src/experience/contract-sources.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Contract,\n compareSemver,\n compatLine,\n verifyContract,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { notFoundError, validationError } from '../errors';\nimport type { ExperienceProject } from './project';\n\n/** Where an integration's contract came from, for messages and `sync` bookkeeping. */\nexport interface ResolvedContract {\n contract: Contract;\n origin: string;\n kind: 'dir' | 'path' | 'snapshot';\n}\n\n/**\n * A pluggable contract source. The dir/path/snapshot sources below are the\n * offline defaults; the harness resolver (catalog, published packages) plugs\n * in here as another source, so `add`/`sync` never learn where contracts live.\n */\nexport interface ContractSource {\n name: string;\n resolve(\n integration: string,\n line: string,\n ): Promise<ResolvedContract | null> | ResolvedContract | null;\n}\n\nexport interface SourceOptions {\n /** A directory of `*.contract.json` (the monorepo contracts folder or a registry mirror). */\n fromDir?: string;\n /** An integration project directory; its `contract.json` is read. */\n fromPath?: string;\n env?: Record<string, string | undefined>;\n}\n\nfunction readContractFile(file: string): Contract | null {\n let json: unknown;\n try {\n json = JSON.parse(fs.readFileSync(file, 'utf8'));\n } catch {\n return null;\n }\n const verified = verifyContract(json);\n if (!verified.contract || verified.problems.length > 0) {\n const first = verified.problems[0];\n throw validationError(\n `${file} is not a valid contract: ${first ? `${first.rule}: ${first.message}` : 'unreadable'}`,\n 'Regenerate it with \"ekanos contract\" in the integration, or point at a valid contracts directory.',\n );\n }\n\n return verified.contract;\n}\n\n/** Every `*.contract.json` of `dir` for `integration`, highest contract version within `line` first. */\nexport function contractsInDir(\n dir: string,\n integration: string,\n line: string,\n): Contract[] {\n let names: string[];\n try {\n names = fs.readdirSync(dir).filter((n) => n.endsWith('.contract.json'));\n } catch {\n return [];\n }\n const found: Contract[] = [];\n for (const name of names) {\n if (\n !name.startsWith(`${integration}@`) &&\n !name.startsWith(`${integration}.`)\n ) {\n continue;\n }\n const contract = readContractFile(path.join(dir, name));\n if (\n contract &&\n contract.integration === integration &&\n contract.compatLine === line\n ) {\n found.push(contract);\n }\n }\n\n return found.sort((a, b) =>\n compareSemver(b.contractVersion, a.contractVersion),\n );\n}\n\nexport function dirSource(dir: string): ContractSource {\n return {\n name: `dir ${dir}`,\n resolve(integration, line) {\n const [best] = contractsInDir(dir, integration, line);\n\n return best ? { contract: best, origin: dir, kind: 'dir' } : null;\n },\n };\n}\n\nexport function pathSource(projectPath: string): ContractSource {\n return {\n name: `path ${projectPath}`,\n resolve(integration, line) {\n const file = path.join(path.resolve(projectPath), 'contract.json');\n if (!fs.existsSync(file)) {\n throw notFoundError(\n `${file} does not exist.`,\n 'Run \"ekanos contract\" in that integration to derive its contract.json.',\n );\n }\n const contract = readContractFile(file);\n if (\n !contract ||\n contract.integration !== integration ||\n contract.compatLine !== line\n ) {\n return null;\n }\n\n return { contract, origin: file, kind: 'path' };\n },\n };\n}\n\n/** The committed pin: `<contractsDir>/<integration>@<version>.contract.json`. */\nexport function snapshotFile(\n project: ExperienceProject,\n integration: string,\n line: string,\n): string {\n return path.join(\n project.contractsDir,\n `${integration}@${line}.contract.json`,\n );\n}\n\nexport function snapshotSource(project: ExperienceProject): ContractSource {\n return {\n name: 'committed snapshot',\n resolve(integration, line) {\n const file = snapshotFile(project, integration, line);\n const contract = fs.existsSync(file) ? readContractFile(file) : null;\n\n return contract ? { contract, origin: file, kind: 'snapshot' } : null;\n },\n };\n}\n\n/** Sources in resolution order: explicit flags, env, then (optionally) the snapshot. */\nexport function defaultSources(\n project: ExperienceProject,\n options: SourceOptions,\n extra: readonly ContractSource[] = [],\n includeSnapshot = true,\n): ContractSource[] {\n const sources: ContractSource[] = [];\n if (options.fromPath) sources.push(pathSource(options.fromPath));\n if (options.fromDir) sources.push(dirSource(path.resolve(options.fromDir)));\n const envDir = options.env?.EKANOS_CONTRACTS_DIR;\n if (!options.fromDir && envDir) sources.push(dirSource(path.resolve(envDir)));\n sources.push(...extra);\n if (includeSnapshot) sources.push(snapshotSource(project));\n\n return sources;\n}\n\nexport async function resolveContract(\n sources: readonly ContractSource[],\n integration: string,\n line: string,\n): Promise<ResolvedContract> {\n for (const source of sources) {\n const hit = await source.resolve(integration, line);\n if (hit) return hit;\n }\n\n throw notFoundError(\n `No contract for ${integration}@${line} (looked in: ${sources.map((s) => s.name).join(', ') || 'nothing'}).`,\n 'Pass --from-dir <contracts folder> or --from-path <integration project>, or set EKANOS_CONTRACTS_DIR.',\n );\n}\n\n/** Line for a package version, re-exported so callers need no schema import. */\nexport { compatLine };\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ExperienceManifest, type Finding, type WorkflowTemplate } from '@ekanos/integration-schema';
|
|
2
|
+
import type { LoadedExperienceDefinition, LoadedExperienceModule } from './definition-shape.js';
|
|
3
|
+
import { type PinnedIntegration } from './pins.js';
|
|
4
|
+
import type { ExperienceProject } from './project.js';
|
|
5
|
+
export declare const DEFINITION_CHECK = "checks/experience-definition";
|
|
6
|
+
export interface DefinitionAnalysis {
|
|
7
|
+
findings: Finding[];
|
|
8
|
+
workflows: WorkflowTemplate[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Cross-check the loaded definition against `ekanos.json` and the pinned
|
|
12
|
+
* contracts: every integration a widget, page, review section or workflow step names
|
|
13
|
+
* must be declared, every operation must be in that integration's allowlist, the
|
|
14
|
+
* review queue reads only, and each template serializes to the engine's DSL.
|
|
15
|
+
*/
|
|
16
|
+
export declare function analyzeDefinition(project: ExperienceProject, loaded: LoadedExperienceModule, pinned: readonly PinnedIntegration[]): DefinitionAnalysis;
|
|
17
|
+
/** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */
|
|
18
|
+
export declare function checkFixtures(project: ExperienceProject, pinned: readonly PinnedIntegration[]): Finding[];
|
|
19
|
+
export declare function buildManifest(input: {
|
|
20
|
+
project: ExperienceProject;
|
|
21
|
+
definition: LoadedExperienceDefinition;
|
|
22
|
+
pinned: readonly PinnedIntegration[];
|
|
23
|
+
workflows: readonly WorkflowTemplate[];
|
|
24
|
+
version: string;
|
|
25
|
+
sdk: string;
|
|
26
|
+
}): ExperienceManifest;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { WorkflowTemplateSchema, canonicalJson, sha256Hex, validateAgainstSchema, workflowTemplateProblems, } from '@ekanos/integration-schema';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { declaredOperationKeys } from './integrations-codegen.js';
|
|
5
|
+
import { effectLookup } from './pins.js';
|
|
6
|
+
export const DEFINITION_CHECK = 'checks/experience-definition';
|
|
7
|
+
const err = (message, hint, file) => (Object.assign({ check: DEFINITION_CHECK, severity: 'error', message,
|
|
8
|
+
hint }, (file ? { file } : {})));
|
|
9
|
+
const warn = (message, hint, file) => (Object.assign({ check: DEFINITION_CHECK, severity: 'warn', message,
|
|
10
|
+
hint }, (file ? { file } : {})));
|
|
11
|
+
/**
|
|
12
|
+
* Cross-check the loaded definition against `ekanos.json` and the pinned
|
|
13
|
+
* contracts: every integration a widget, page, review section or workflow step names
|
|
14
|
+
* must be declared, every operation must be in that integration's allowlist, the
|
|
15
|
+
* review queue reads only, and each template serializes to the engine's DSL.
|
|
16
|
+
*/
|
|
17
|
+
export function analyzeDefinition(project, loaded, pinned) {
|
|
18
|
+
var _a, _b, _c, _d, _e;
|
|
19
|
+
var _f, _g;
|
|
20
|
+
const { definition } = loaded;
|
|
21
|
+
const findings = [];
|
|
22
|
+
const workflows = [];
|
|
23
|
+
const declaredIntegrations = new Map(project.integrations.map((u) => [u.name, u]));
|
|
24
|
+
const declared = new Map(project.integrations.map((u) => [
|
|
25
|
+
u.name,
|
|
26
|
+
new Set(declaredOperationKeys(u)),
|
|
27
|
+
]));
|
|
28
|
+
const contractOps = new Map(pinned.map(({ use, contract }) => [
|
|
29
|
+
use.name,
|
|
30
|
+
new Map(contract.operations.map((op) => [`${op.kind}/${op.name}`, op])),
|
|
31
|
+
]));
|
|
32
|
+
if (definition.slug !== project.slug) {
|
|
33
|
+
findings.push(err(`the definition's slug "${definition.slug}" differs from ekanos.json "${project.slug}".`, 'Make them equal.', project.entry));
|
|
34
|
+
}
|
|
35
|
+
const checkIntegration = (integration, owner) => {
|
|
36
|
+
if (!declaredIntegrations.has(integration)) {
|
|
37
|
+
findings.push(err(`${owner} uses integration "${integration}", which ekanos.json "integrations" does not declare.`, `Add it with "ekanos add ${integration}@<version>".`, project.entry));
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
for (const widget of definition.widgets) {
|
|
43
|
+
for (const integration of widget.integrations)
|
|
44
|
+
checkIntegration(integration, `widget "${widget.id}"`);
|
|
45
|
+
}
|
|
46
|
+
for (const page of definition.pages) {
|
|
47
|
+
for (const integration of page.integrations)
|
|
48
|
+
checkIntegration(integration, `page "${page.id}"`);
|
|
49
|
+
}
|
|
50
|
+
for (const section of (_f = (_a = definition.reviewQueue) === null || _a === void 0 ? void 0 : _a.sections) !== null && _f !== void 0 ? _f : []) {
|
|
51
|
+
if (!checkIntegration(section.integration, `review section "${section.id}"`))
|
|
52
|
+
continue;
|
|
53
|
+
if (!((_b = declared.get(section.integration)) === null || _b === void 0 ? void 0 : _b.has(section.read))) {
|
|
54
|
+
findings.push(err(`review section "${section.id}" reads ${section.read}, which integration "${section.integration}" does not declare.`, 'Add the operation to the integration in ekanos.json, or read another one.', project.entry));
|
|
55
|
+
}
|
|
56
|
+
else if (((_d = (_c = contractOps.get(section.integration)) === null || _c === void 0 ? void 0 : _c.get(section.read)) === null || _d === void 0 ? void 0 : _d.effect) !== 'read') {
|
|
57
|
+
findings.push(err(`review section "${section.id}" reads ${section.read}, which is not a read operation.`, 'The review queue is reads-only; writes belong behind a workflow approval.', project.entry));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const effectOf = effectLookup(pinned);
|
|
61
|
+
for (const template of definition.workflows) {
|
|
62
|
+
const where = `workflow "${template.id}"`;
|
|
63
|
+
let steps;
|
|
64
|
+
try {
|
|
65
|
+
steps = template.build();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
findings.push(err(`${where}: build() threw: ${error instanceof Error ? error.message : String(error)}`, 'Fix the template.', project.entry));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let stepsOk = true;
|
|
72
|
+
for (const step of steps) {
|
|
73
|
+
if (step.type !== 'tool' || !step.integration || !step.op)
|
|
74
|
+
continue;
|
|
75
|
+
if (!checkIntegration(step.integration, `${where} step "${step.id}"`)) {
|
|
76
|
+
stepsOk = false;
|
|
77
|
+
}
|
|
78
|
+
else if (!((_e = declared.get(step.integration)) === null || _e === void 0 ? void 0 : _e.has(step.op))) {
|
|
79
|
+
stepsOk = false;
|
|
80
|
+
findings.push(err(`${where} step "${step.id}" calls ${step.integration} ${step.op}, which the integration does not declare.`, 'Add the operation to the integration\'s "operations" in ekanos.json (ekanos add --ops).', project.entry));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!stepsOk)
|
|
84
|
+
continue;
|
|
85
|
+
let dsl;
|
|
86
|
+
try {
|
|
87
|
+
dsl = loaded.serializeWorkflowTemplate(template, { effectOf });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
findings.push(err(`${where}: ${error instanceof Error ? error.message : String(error)}`, 'Fix the template.', project.entry));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const parsed = WorkflowTemplateSchema.safeParse(dsl);
|
|
94
|
+
if (!parsed.success) {
|
|
95
|
+
findings.push(err(`${where} does not serialize to a valid workflow: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`, 'Fix the step definitions.', project.entry));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const problem of workflowTemplateProblems(parsed.data)) {
|
|
99
|
+
findings.push(err(`${where}: ${problem}.`, 'Fix the template.', project.entry));
|
|
100
|
+
}
|
|
101
|
+
const gated = new Set();
|
|
102
|
+
const preds = new Map();
|
|
103
|
+
for (const e of parsed.data.edges)
|
|
104
|
+
preds.set(e.to, [...((_g = preds.get(e.to)) !== null && _g !== void 0 ? _g : []), e.from]);
|
|
105
|
+
const hasGate = (id, seen = new Set()) => {
|
|
106
|
+
var _a;
|
|
107
|
+
if (seen.has(id))
|
|
108
|
+
return false;
|
|
109
|
+
seen.add(id);
|
|
110
|
+
return ((_a = preds.get(id)) !== null && _a !== void 0 ? _a : []).some((p) => { var _a; return ((_a = parsed.data.steps.find((s) => s.id === p)) === null || _a === void 0 ? void 0 : _a.type) === 'approval_gate' ||
|
|
111
|
+
hasGate(p, seen); });
|
|
112
|
+
};
|
|
113
|
+
for (const s of parsed.data.steps) {
|
|
114
|
+
if (s.type === 'fusion_tool' && s.effect === 'write' && !hasGate(s.id)) {
|
|
115
|
+
gated.add(s.id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const id of gated) {
|
|
119
|
+
findings.push(warn(`${where}: write step "${id}" has no approval gate upstream.`, 'Put step.approval(...) before a write so a person confirms it.', project.entry));
|
|
120
|
+
}
|
|
121
|
+
workflows.push(parsed.data);
|
|
122
|
+
}
|
|
123
|
+
return { findings, workflows };
|
|
124
|
+
}
|
|
125
|
+
// ─── fixtures ───────────────────────────────────────────────────────────────
|
|
126
|
+
const FIXTURE_NAME = /^(tool|action)-(.+)\.(default|empty|error)\.json$/;
|
|
127
|
+
/** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */
|
|
128
|
+
export function checkFixtures(project, pinned) {
|
|
129
|
+
const findings = [];
|
|
130
|
+
const root = path.join(project.dir, 'fixtures');
|
|
131
|
+
if (!fs.existsSync(root))
|
|
132
|
+
return findings;
|
|
133
|
+
for (const { use, contract } of pinned) {
|
|
134
|
+
const dir = path.join(root, use.name);
|
|
135
|
+
if (!fs.existsSync(dir))
|
|
136
|
+
continue;
|
|
137
|
+
for (const name of fs.readdirSync(dir)) {
|
|
138
|
+
const rel = `fixtures/${use.name}/${name}`;
|
|
139
|
+
const match = FIXTURE_NAME.exec(name);
|
|
140
|
+
if (!match) {
|
|
141
|
+
findings.push(warn(`${rel} is not <kind>-<name>.<default|empty|error>.json.`, 'Rename it so the harness can find it.', rel));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const [, kind, op, variant] = match;
|
|
145
|
+
if (variant === 'error')
|
|
146
|
+
continue;
|
|
147
|
+
const operation = contract.operations.find((o) => o.kind === kind && o.name === op);
|
|
148
|
+
if (!operation) {
|
|
149
|
+
findings.push(err(`${rel}: ${use.name}@${use.version} has no ${kind}/${op}.`, 'Delete the fixture or fix its name.', rel));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (operation.opaque.output)
|
|
153
|
+
continue;
|
|
154
|
+
let value;
|
|
155
|
+
try {
|
|
156
|
+
value = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
157
|
+
}
|
|
158
|
+
catch (_a) {
|
|
159
|
+
findings.push(err(`${rel} is not valid JSON.`, 'Fix the syntax.', rel));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const problems = validateAgainstSchema(operation.output, value);
|
|
163
|
+
if (problems.length > 0) {
|
|
164
|
+
findings.push(err(`${rel} violates the pinned output schema: ${problems[0]}`, 'A fixture must satisfy the contract, or the harness would show data the integration can never return.', rel));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return findings;
|
|
169
|
+
}
|
|
170
|
+
// ─── manifest ───────────────────────────────────────────────────────────────
|
|
171
|
+
export function buildManifest(input) {
|
|
172
|
+
const { project, definition, pinned } = input;
|
|
173
|
+
const body = Object.assign(Object.assign(Object.assign({ schemaVersion: 1, slug: project.slug, form: 'composed', version: input.version, sdk: input.sdk, integrations: project.integrations, contractRefs: pinned.map(({ use, contract }) => ({
|
|
174
|
+
integration: use.name,
|
|
175
|
+
version: use.version,
|
|
176
|
+
contractVersion: contract.contractVersion,
|
|
177
|
+
digest: contract.digest,
|
|
178
|
+
})) }, (definition.sidebar ? { sidebar: definition.sidebar } : {})), { widgets: definition.widgets.map((w) => (Object.assign(Object.assign(Object.assign({ id: w.id, title: w.title, integrations: [...w.integrations] }, (w.width ? { width: w.width } : {})), (w.isCollapsible === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { isCollapsible: w.isCollapsible })), (w.aiFooterEnabled === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: { aiFooterEnabled: w.aiFooterEnabled })))), pages: definition.pages.map((p) => ({
|
|
183
|
+
id: p.id,
|
|
184
|
+
path: p.path,
|
|
185
|
+
title: p.title,
|
|
186
|
+
integrations: [...p.integrations],
|
|
187
|
+
})), workflowTemplates: [...input.workflows] }), (definition.reviewQueue
|
|
188
|
+
? {
|
|
189
|
+
reviewQueue: {
|
|
190
|
+
readsOnly: true,
|
|
191
|
+
sections: definition.reviewQueue.sections.map((s) => ({
|
|
192
|
+
id: s.id,
|
|
193
|
+
integration: s.integration,
|
|
194
|
+
read: s.read,
|
|
195
|
+
})),
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {}));
|
|
199
|
+
return Object.assign(Object.assign({}, body), { digest: `sha256:${sha256Hex(canonicalJson(body))}` });
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=definition-checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definition-checks.js","sourceRoot":"","sources":["../../src/experience/definition-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ExperienceManifest,\n type Finding,\n type WorkflowTemplate,\n WorkflowTemplateSchema,\n canonicalJson,\n sha256Hex,\n validateAgainstSchema,\n workflowTemplateProblems,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type {\n LoadedExperienceDefinition,\n LoadedExperienceModule,\n} from './definition-shape';\nimport { declaredOperationKeys } from './integrations-codegen';\nimport { type PinnedIntegration, effectLookup } from './pins';\nimport type { ExperienceProject } from './project';\n\nexport const DEFINITION_CHECK = 'checks/experience-definition';\n\nconst err = (message: string, hint: string, file?: string): Finding => ({\n check: DEFINITION_CHECK,\n severity: 'error',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\nconst warn = (message: string, hint: string, file?: string): Finding => ({\n check: DEFINITION_CHECK,\n severity: 'warn',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\nexport interface DefinitionAnalysis {\n findings: Finding[];\n workflows: WorkflowTemplate[];\n}\n\n/**\n * Cross-check the loaded definition against `ekanos.json` and the pinned\n * contracts: every integration a widget, page, review section or workflow step names\n * must be declared, every operation must be in that integration's allowlist, the\n * review queue reads only, and each template serializes to the engine's DSL.\n */\nexport function analyzeDefinition(\n project: ExperienceProject,\n loaded: LoadedExperienceModule,\n pinned: readonly PinnedIntegration[],\n): DefinitionAnalysis {\n const { definition } = loaded;\n const findings: Finding[] = [];\n const workflows: WorkflowTemplate[] = [];\n const declaredIntegrations = new Map(\n project.integrations.map((u) => [u.name, u]),\n );\n const declared = new Map(\n project.integrations.map((u) => [\n u.name,\n new Set(declaredOperationKeys(u)),\n ]),\n );\n const contractOps = new Map(\n pinned.map(({ use, contract }) => [\n use.name,\n new Map(contract.operations.map((op) => [`${op.kind}/${op.name}`, op])),\n ]),\n );\n\n if (definition.slug !== project.slug) {\n findings.push(\n err(\n `the definition's slug \"${definition.slug}\" differs from ekanos.json \"${project.slug}\".`,\n 'Make them equal.',\n project.entry,\n ),\n );\n }\n\n const checkIntegration = (integration: string, owner: string) => {\n if (!declaredIntegrations.has(integration)) {\n findings.push(\n err(\n `${owner} uses integration \"${integration}\", which ekanos.json \"integrations\" does not declare.`,\n `Add it with \"ekanos add ${integration}@<version>\".`,\n project.entry,\n ),\n );\n\n return false;\n }\n\n return true;\n };\n\n for (const widget of definition.widgets) {\n for (const integration of widget.integrations)\n checkIntegration(integration, `widget \"${widget.id}\"`);\n }\n for (const page of definition.pages) {\n for (const integration of page.integrations)\n checkIntegration(integration, `page \"${page.id}\"`);\n }\n\n for (const section of definition.reviewQueue?.sections ?? []) {\n if (\n !checkIntegration(section.integration, `review section \"${section.id}\"`)\n )\n continue;\n if (!declared.get(section.integration)?.has(section.read)) {\n findings.push(\n err(\n `review section \"${section.id}\" reads ${section.read}, which integration \"${section.integration}\" does not declare.`,\n 'Add the operation to the integration in ekanos.json, or read another one.',\n project.entry,\n ),\n );\n } else if (\n contractOps.get(section.integration)?.get(section.read)?.effect !== 'read'\n ) {\n findings.push(\n err(\n `review section \"${section.id}\" reads ${section.read}, which is not a read operation.`,\n 'The review queue is reads-only; writes belong behind a workflow approval.',\n project.entry,\n ),\n );\n }\n }\n\n const effectOf = effectLookup(pinned);\n for (const template of definition.workflows) {\n const where = `workflow \"${template.id}\"`;\n let steps: ReturnType<typeof template.build>;\n try {\n steps = template.build();\n } catch (error) {\n findings.push(\n err(\n `${where}: build() threw: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the template.',\n project.entry,\n ),\n );\n continue;\n }\n\n let stepsOk = true;\n for (const step of steps) {\n if (step.type !== 'tool' || !step.integration || !step.op) continue;\n if (!checkIntegration(step.integration, `${where} step \"${step.id}\"`)) {\n stepsOk = false;\n } else if (!declared.get(step.integration)?.has(step.op)) {\n stepsOk = false;\n findings.push(\n err(\n `${where} step \"${step.id}\" calls ${step.integration} ${step.op}, which the integration does not declare.`,\n 'Add the operation to the integration\\'s \"operations\" in ekanos.json (ekanos add --ops).',\n project.entry,\n ),\n );\n }\n }\n if (!stepsOk) continue;\n\n let dsl: unknown;\n try {\n dsl = loaded.serializeWorkflowTemplate(template, { effectOf });\n } catch (error) {\n findings.push(\n err(\n `${where}: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the template.',\n project.entry,\n ),\n );\n continue;\n }\n const parsed = WorkflowTemplateSchema.safeParse(dsl);\n if (!parsed.success) {\n findings.push(\n err(\n `${where} does not serialize to a valid workflow: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'Fix the step definitions.',\n project.entry,\n ),\n );\n continue;\n }\n for (const problem of workflowTemplateProblems(parsed.data)) {\n findings.push(\n err(`${where}: ${problem}.`, 'Fix the template.', project.entry),\n );\n }\n\n const gated = new Set<string>();\n const preds = new Map<string, string[]>();\n for (const e of parsed.data.edges)\n preds.set(e.to, [...(preds.get(e.to) ?? []), e.from]);\n const hasGate = (id: string, seen = new Set<string>()): boolean => {\n if (seen.has(id)) return false;\n seen.add(id);\n\n return (preds.get(id) ?? []).some(\n (p) =>\n parsed.data.steps.find((s) => s.id === p)?.type === 'approval_gate' ||\n hasGate(p, seen),\n );\n };\n for (const s of parsed.data.steps) {\n if (s.type === 'fusion_tool' && s.effect === 'write' && !hasGate(s.id)) {\n gated.add(s.id);\n }\n }\n for (const id of gated) {\n findings.push(\n warn(\n `${where}: write step \"${id}\" has no approval gate upstream.`,\n 'Put step.approval(...) before a write so a person confirms it.',\n project.entry,\n ),\n );\n }\n\n workflows.push(parsed.data);\n }\n\n return { findings, workflows };\n}\n\n// ─── fixtures ───────────────────────────────────────────────────────────────\n\nconst FIXTURE_NAME = /^(tool|action)-(.+)\\.(default|empty|error)\\.json$/;\n\n/** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */\nexport function checkFixtures(\n project: ExperienceProject,\n pinned: readonly PinnedIntegration[],\n): Finding[] {\n const findings: Finding[] = [];\n const root = path.join(project.dir, 'fixtures');\n if (!fs.existsSync(root)) return findings;\n\n for (const { use, contract } of pinned) {\n const dir = path.join(root, use.name);\n if (!fs.existsSync(dir)) continue;\n for (const name of fs.readdirSync(dir)) {\n const rel = `fixtures/${use.name}/${name}`;\n const match = FIXTURE_NAME.exec(name);\n if (!match) {\n findings.push(\n warn(\n `${rel} is not <kind>-<name>.<default|empty|error>.json.`,\n 'Rename it so the harness can find it.',\n rel,\n ),\n );\n continue;\n }\n const [, kind, op, variant] = match;\n if (variant === 'error') continue;\n const operation = contract.operations.find(\n (o) => o.kind === kind && o.name === op,\n );\n if (!operation) {\n findings.push(\n err(\n `${rel}: ${use.name}@${use.version} has no ${kind}/${op}.`,\n 'Delete the fixture or fix its name.',\n rel,\n ),\n );\n continue;\n }\n if (operation.opaque.output) continue;\n let value: unknown;\n try {\n value = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));\n } catch {\n findings.push(err(`${rel} is not valid JSON.`, 'Fix the syntax.', rel));\n continue;\n }\n const problems = validateAgainstSchema(operation.output, value);\n if (problems.length > 0) {\n findings.push(\n err(\n `${rel} violates the pinned output schema: ${problems[0]}`,\n 'A fixture must satisfy the contract, or the harness would show data the integration can never return.',\n rel,\n ),\n );\n }\n }\n }\n\n return findings;\n}\n\n// ─── manifest ───────────────────────────────────────────────────────────────\n\nexport function buildManifest(input: {\n project: ExperienceProject;\n definition: LoadedExperienceDefinition;\n pinned: readonly PinnedIntegration[];\n workflows: readonly WorkflowTemplate[];\n version: string;\n sdk: string;\n}): ExperienceManifest {\n const { project, definition, pinned } = input;\n const body = {\n schemaVersion: 1 as const,\n slug: project.slug,\n form: 'composed' as const,\n version: input.version,\n sdk: input.sdk,\n integrations: project.integrations,\n contractRefs: pinned.map(({ use, contract }) => ({\n integration: use.name,\n version: use.version,\n contractVersion: contract.contractVersion,\n digest: contract.digest,\n })),\n ...(definition.sidebar ? { sidebar: definition.sidebar } : {}),\n widgets: definition.widgets.map((w) => ({\n id: w.id,\n title: w.title,\n integrations: [...w.integrations],\n ...(w.width ? { width: w.width } : {}),\n ...(w.isCollapsible === undefined\n ? {}\n : { isCollapsible: w.isCollapsible }),\n ...(w.aiFooterEnabled === undefined\n ? {}\n : { aiFooterEnabled: w.aiFooterEnabled }),\n })),\n pages: definition.pages.map((p) => ({\n id: p.id,\n path: p.path,\n title: p.title,\n integrations: [...p.integrations],\n })),\n workflowTemplates: [...input.workflows],\n ...(definition.reviewQueue\n ? {\n reviewQueue: {\n readsOnly: true as const,\n sections: definition.reviewQueue.sections.map((s) => ({\n id: s.id,\n integration: s.integration,\n read: s.read,\n })),\n },\n }\n : {}),\n };\n\n return {\n ...body,\n digest: `sha256:${sha256Hex(canonicalJson(body))}`,\n };\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural slice of an `ExperienceDefinition` the CLI reads. The CLI does
|
|
3
|
+
* not depend on `@ekanos/sdk`: the definition is produced by whichever SDK the
|
|
4
|
+
* project installed, and anything the CLI needs computed (workflow
|
|
5
|
+
* serialization) is obtained from that same SDK at load time.
|
|
6
|
+
*/
|
|
7
|
+
export interface LoadedWorkflowTemplate {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
build(): ReadonlyArray<{
|
|
11
|
+
type: string;
|
|
12
|
+
id: string;
|
|
13
|
+
integration?: string;
|
|
14
|
+
op?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface LoadedExperienceDefinition {
|
|
18
|
+
kind: 'experience';
|
|
19
|
+
slug: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
sidebar?: {
|
|
23
|
+
section: string;
|
|
24
|
+
icon: string;
|
|
25
|
+
label: string;
|
|
26
|
+
};
|
|
27
|
+
widgets: ReadonlyArray<{
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
integrations: readonly string[];
|
|
31
|
+
width?: string;
|
|
32
|
+
isCollapsible?: boolean;
|
|
33
|
+
isPinnable?: boolean;
|
|
34
|
+
aiFooterEnabled?: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
pages: ReadonlyArray<{
|
|
37
|
+
id: string;
|
|
38
|
+
path: string;
|
|
39
|
+
title: string;
|
|
40
|
+
integrations: readonly string[];
|
|
41
|
+
}>;
|
|
42
|
+
workflows: readonly LoadedWorkflowTemplate[];
|
|
43
|
+
reviewQueue?: {
|
|
44
|
+
readsOnly: true;
|
|
45
|
+
sections: ReadonlyArray<{
|
|
46
|
+
id: string;
|
|
47
|
+
integration: string;
|
|
48
|
+
read: string;
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface SerializedWorkflowOptions {
|
|
53
|
+
effectOf: (integration: string, op: string) => 'read' | 'write' | undefined;
|
|
54
|
+
}
|
|
55
|
+
export interface LoadedExperienceModule {
|
|
56
|
+
definition: LoadedExperienceDefinition;
|
|
57
|
+
/** The project's own SDK serializer, so the output matches the SDK that authored it. */
|
|
58
|
+
serializeWorkflowTemplate(template: LoadedWorkflowTemplate, options: SerializedWorkflowOptions): unknown;
|
|
59
|
+
slotsUsedBy(definition: LoadedExperienceDefinition): string[];
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definition-shape.js","sourceRoot":"","sources":["../../src/experience/definition-shape.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The structural slice of an `ExperienceDefinition` the CLI reads. The CLI does\n * not depend on `@ekanos/sdk`: the definition is produced by whichever SDK the\n * project installed, and anything the CLI needs computed (workflow\n * serialization) is obtained from that same SDK at load time.\n */\nexport interface LoadedWorkflowTemplate {\n id: string;\n name: string;\n build(): ReadonlyArray<{\n type: string;\n id: string;\n integration?: string;\n op?: string;\n }>;\n}\n\nexport interface LoadedExperienceDefinition {\n kind: 'experience';\n slug: string;\n name: string;\n description?: string;\n sidebar?: { section: string; icon: string; label: string };\n widgets: ReadonlyArray<{\n id: string;\n title: string;\n integrations: readonly string[];\n width?: string;\n isCollapsible?: boolean;\n isPinnable?: boolean;\n aiFooterEnabled?: boolean;\n }>;\n pages: ReadonlyArray<{\n id: string;\n path: string;\n title: string;\n integrations: readonly string[];\n }>;\n workflows: readonly LoadedWorkflowTemplate[];\n reviewQueue?: {\n readsOnly: true;\n sections: ReadonlyArray<{ id: string; integration: string; read: string }>;\n };\n}\n\nexport interface SerializedWorkflowOptions {\n effectOf: (integration: string, op: string) => 'read' | 'write' | undefined;\n}\n\nexport interface LoadedExperienceModule {\n definition: LoadedExperienceDefinition;\n /** The project's own SDK serializer, so the output matches the SDK that authored it. */\n serializeWorkflowTemplate(\n template: LoadedWorkflowTemplate,\n options: SerializedWorkflowOptions,\n ): unknown;\n slotsUsedBy(definition: LoadedExperienceDefinition): string[];\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CliContext } from '../context.js';
|
|
2
|
+
import type { ExitCode } from '../exit-codes.js';
|
|
3
|
+
export interface InitExperienceArgs {
|
|
4
|
+
slug?: string;
|
|
5
|
+
dir?: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
force: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** `ekanos init --kind experience`: scaffold a project with no integrations yet. */
|
|
10
|
+
export declare function runInitExperience(ctx: CliContext, args: InitExperienceArgs): ExitCode;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { invalidStateError, usageError } from '../errors.js';
|
|
4
|
+
import { classifyPath, writeFileNoFollow } from '../safe-fs.js';
|
|
5
|
+
import { readTemplate, toDisplayName } from '../templates.js';
|
|
6
|
+
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
/** template (under templates/experience/) -> destination. */
|
|
8
|
+
const FILES = [
|
|
9
|
+
{ template: 'ekanos.json.tmpl', dest: 'ekanos.json' },
|
|
10
|
+
{ template: 'package.json.tmpl', dest: 'package.json' },
|
|
11
|
+
{ template: 'tsconfig.json.tmpl', dest: 'tsconfig.json' },
|
|
12
|
+
{ template: 'gitignore.tmpl', dest: '.gitignore' },
|
|
13
|
+
{ template: 'eslint.config.mjs.tmpl', dest: 'eslint.config.mjs' },
|
|
14
|
+
{ template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },
|
|
15
|
+
{ template: 'ekanos.lock.json.tmpl', dest: 'ekanos.lock.json' },
|
|
16
|
+
{ template: 'src/experience.ts.tmpl', dest: 'src/experience.ts' },
|
|
17
|
+
{
|
|
18
|
+
template: 'src/widgets/hello-widget.tsx.tmpl',
|
|
19
|
+
dest: 'src/widgets/hello-widget.tsx',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
template: 'src/.ekanos/integrations.d.ts.tmpl',
|
|
23
|
+
dest: 'src/.ekanos/integrations.d.ts',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
/** `ekanos init --kind experience`: scaffold a project with no integrations yet. */
|
|
27
|
+
export function runInitExperience(ctx, args) {
|
|
28
|
+
var _a;
|
|
29
|
+
const slug = args.slug;
|
|
30
|
+
if (!slug || !SLUG_PATTERN.test(slug)) {
|
|
31
|
+
throw usageError(slug ? `Invalid slug "${slug}".` : 'init requires a slug.', 'Pass kebab-case: "ekanos init --kind experience --slug content-engine".');
|
|
32
|
+
}
|
|
33
|
+
const targetDir = path.resolve(args.cwd, (_a = args.dir) !== null && _a !== void 0 ? _a : slug);
|
|
34
|
+
if (fs.existsSync(targetDir) && fs.lstatSync(targetDir).isSymbolicLink()) {
|
|
35
|
+
throw invalidStateError(`The target directory ${targetDir} is a symlink.`, 'Scaffold into the real directory.');
|
|
36
|
+
}
|
|
37
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
38
|
+
const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };
|
|
39
|
+
const created = [];
|
|
40
|
+
const skipped = [];
|
|
41
|
+
for (const { template, dest } of FILES) {
|
|
42
|
+
const destPath = path.join(targetDir, dest);
|
|
43
|
+
const kind = classifyPath(destPath);
|
|
44
|
+
if (kind !== 'missing' && kind !== 'symlink' && !args.force) {
|
|
45
|
+
skipped.push(dest);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
writeFileNoFollow(targetDir, destPath, readTemplate(`experience/${template}`, vars));
|
|
49
|
+
created.push(dest);
|
|
50
|
+
}
|
|
51
|
+
for (const keep of ['contracts/.gitkeep', 'fixtures/.gitkeep']) {
|
|
52
|
+
const destPath = path.join(targetDir, keep);
|
|
53
|
+
if (classifyPath(destPath) === 'missing') {
|
|
54
|
+
writeFileNoFollow(targetDir, destPath, '');
|
|
55
|
+
created.push(keep);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const file of created)
|
|
59
|
+
ctx.log(` created ${file}`);
|
|
60
|
+
for (const file of skipped)
|
|
61
|
+
ctx.log(` skipped ${file} (already exists; --force overwrites)`);
|
|
62
|
+
return ctx.succeed({ kind: 'experience', slug, dir: targetDir, created, skipped }, `Scaffolded experience "${slug}". Next: cd ${path.relative(args.cwd, targetDir) || '.'} && install, then "ekanos add <integration>@<version> --from-dir <contracts folder>".`);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/experience/init.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { invalidStateError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { classifyPath, writeFileNoFollow } from '../safe-fs';\nimport { readTemplate, toDisplayName } from '../templates';\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/** template (under templates/experience/) -> destination. */\nconst FILES: ReadonlyArray<{ template: string; dest: string }> = [\n { template: 'ekanos.json.tmpl', dest: 'ekanos.json' },\n { template: 'package.json.tmpl', dest: 'package.json' },\n { template: 'tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'gitignore.tmpl', dest: '.gitignore' },\n { template: 'eslint.config.mjs.tmpl', dest: 'eslint.config.mjs' },\n { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },\n { template: 'ekanos.lock.json.tmpl', dest: 'ekanos.lock.json' },\n { template: 'src/experience.ts.tmpl', dest: 'src/experience.ts' },\n {\n template: 'src/widgets/hello-widget.tsx.tmpl',\n dest: 'src/widgets/hello-widget.tsx',\n },\n {\n template: 'src/.ekanos/integrations.d.ts.tmpl',\n dest: 'src/.ekanos/integrations.d.ts',\n },\n];\n\nexport interface InitExperienceArgs {\n slug?: string;\n dir?: string;\n cwd: string;\n force: boolean;\n}\n\n/** `ekanos init --kind experience`: scaffold a project with no integrations yet. */\nexport function runInitExperience(\n ctx: CliContext,\n args: InitExperienceArgs,\n): ExitCode {\n const slug = args.slug;\n if (!slug || !SLUG_PATTERN.test(slug)) {\n throw usageError(\n slug ? `Invalid slug \"${slug}\".` : 'init requires a slug.',\n 'Pass kebab-case: \"ekanos init --kind experience --slug content-engine\".',\n );\n }\n const targetDir = path.resolve(args.cwd, args.dir ?? slug);\n if (fs.existsSync(targetDir) && fs.lstatSync(targetDir).isSymbolicLink()) {\n throw invalidStateError(\n `The target directory ${targetDir} is a symlink.`,\n 'Scaffold into the real directory.',\n );\n }\n fs.mkdirSync(targetDir, { recursive: true });\n\n const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };\n const created: string[] = [];\n const skipped: string[] = [];\n for (const { template, dest } of FILES) {\n const destPath = path.join(targetDir, dest);\n const kind = classifyPath(destPath);\n if (kind !== 'missing' && kind !== 'symlink' && !args.force) {\n skipped.push(dest);\n continue;\n }\n writeFileNoFollow(\n targetDir,\n destPath,\n readTemplate(`experience/${template}`, vars),\n );\n created.push(dest);\n }\n for (const keep of ['contracts/.gitkeep', 'fixtures/.gitkeep']) {\n const destPath = path.join(targetDir, keep);\n if (classifyPath(destPath) === 'missing') {\n writeFileNoFollow(targetDir, destPath, '');\n created.push(keep);\n }\n }\n\n for (const file of created) ctx.log(` created ${file}`);\n for (const file of skipped)\n ctx.log(` skipped ${file} (already exists; --force overwrites)`);\n\n return ctx.succeed(\n { kind: 'experience', slug, dir: targetDir, created, skipped },\n `Scaffolded experience \"${slug}\". Next: cd ${path.relative(args.cwd, targetDir) || '.'} && install, then \"ekanos add <integration>@<version> --from-dir <contracts folder>\".`,\n );\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Contract } from '@ekanos/integration-schema';
|
|
2
|
+
import type { ExperienceProject } from './project.js';
|
|
3
|
+
export declare const GENERATED_BANNER: string;
|
|
4
|
+
export interface IntegrationContract {
|
|
5
|
+
integration: string;
|
|
6
|
+
contract: Contract;
|
|
7
|
+
/** The `kind/name` keys the experience declared for this integration in `integrations[].operations`. */
|
|
8
|
+
operations: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `declare module` file that gives `useOperation` / `operation()` /
|
|
12
|
+
* `step.tool` their types. Only operations the experience declared appear, so
|
|
13
|
+
* calling any other is a compile error as well as a runner refusal.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateIntegrationsDts(integrations: readonly IntegrationContract[]): string;
|
|
16
|
+
/** `kind/name` keys of an integration's declared operations (canonical ids stripped). */
|
|
17
|
+
export declare function declaredOperationKeys(integration: ExperienceProject['integrations'][number]): string[];
|