@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,43 @@
|
|
|
1
|
+
import { harnessPinsResolver } from '../dev-experience.js';
|
|
2
|
+
import { runExperienceChecks } from '../experience/checks.js';
|
|
3
|
+
import { readExperienceProject } from '../experience/project.js';
|
|
4
|
+
import { failOnErrors } from '../experience/report.js';
|
|
5
|
+
export const checkCommand = {
|
|
6
|
+
name: 'check',
|
|
7
|
+
usage: [
|
|
8
|
+
' check Run the experience gate: integration pins, generated types, integration-',
|
|
9
|
+
' import boundary (source + bundle), fixtures, definition, typecheck',
|
|
10
|
+
],
|
|
11
|
+
specs: {
|
|
12
|
+
dir: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Project directory (default: the current one)',
|
|
15
|
+
},
|
|
16
|
+
typecheck: {
|
|
17
|
+
type: 'boolean',
|
|
18
|
+
default: true,
|
|
19
|
+
description: 'Run tsc over the project (--no-typecheck skips)',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
async run(ctx, args) {
|
|
23
|
+
const project = readExperienceProject(args.dir);
|
|
24
|
+
const result = await runExperienceChecks(project, {
|
|
25
|
+
typecheck: args.flags.typecheck !== false,
|
|
26
|
+
loadDefinition: true,
|
|
27
|
+
inlineSdk: args.env.EKANOS_INLINE_SDK,
|
|
28
|
+
pinsResolver: await harnessPinsResolver(project.dir, args.env),
|
|
29
|
+
});
|
|
30
|
+
const failed = failOnErrors(ctx, 'check', result.findings, {
|
|
31
|
+
slug: project.slug,
|
|
32
|
+
integrations: project.integrations.map((u) => u.name),
|
|
33
|
+
}, project.dir);
|
|
34
|
+
if (failed !== null)
|
|
35
|
+
return failed;
|
|
36
|
+
return ctx.succeed({
|
|
37
|
+
slug: project.slug,
|
|
38
|
+
integrations: project.integrations.map((u) => u.name),
|
|
39
|
+
findings: result.findings,
|
|
40
|
+
}, `check: OK - ${project.slug} (${project.integrations.length} integration${project.integrations.length === 1 ? '' : 's'})${result.findings.length ? `, ${result.findings.length} warning${result.findings.length === 1 ? '' : 's'}` : ''}.`);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,MAAM,CAAC,MAAM,YAAY,GAAsB;IAC7C,IAAI,EAAE,OAAO;IACb,KAAK,EAAE;QACL,uFAAuF;QACvF,iFAAiF;KAClF;IACD,KAAK,EAAE;QACL,GAAG,EAAE;YACH,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,8CAA8C;SAC5D;QACD,SAAS,EAAE;YACT,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,iDAAiD;SAC/D;KACF;IACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI;QACjB,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE;YAChD,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,KAAK,KAAK;YACzC,cAAc,EAAE,IAAI;YACpB,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB;YACrC,YAAY,EAAE,MAAM,mBAAmB,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;SAC/D,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,YAAY,CACzB,GAAG,EACH,OAAO,EACP,MAAM,CAAC,QAAQ,EACf;YACE,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SACtD,EACD,OAAO,CAAC,GAAG,CACZ,CAAC;QACF,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC;QAEnC,OAAO,GAAG,CAAC,OAAO,CAChB;YACE,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YACrD,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,EACD,eAAe,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,YAAY,CAAC,MAAM,eAAe,OAAO,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,WAAW,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAC3O,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { harnessPinsResolver } from '../dev-experience';\nimport { runExperienceChecks } from '../experience/checks';\nimport { readExperienceProject } from '../experience/project';\nimport { failOnErrors } from '../experience/report';\nimport type { ExperienceCommand } from './index-experience';\n\nexport const checkCommand: ExperienceCommand = {\n name: 'check',\n usage: [\n ' check Run the experience gate: integration pins, generated types, integration-',\n ' import boundary (source + bundle), fixtures, definition, typecheck',\n ],\n specs: {\n dir: {\n type: 'string',\n description: 'Project directory (default: the current one)',\n },\n typecheck: {\n type: 'boolean',\n default: true,\n description: 'Run tsc over the project (--no-typecheck skips)',\n },\n },\n async run(ctx, args) {\n const project = readExperienceProject(args.dir);\n const result = await runExperienceChecks(project, {\n typecheck: args.flags.typecheck !== false,\n loadDefinition: true,\n inlineSdk: args.env.EKANOS_INLINE_SDK,\n pinsResolver: await harnessPinsResolver(project.dir, args.env),\n });\n\n const failed = failOnErrors(\n ctx,\n 'check',\n result.findings,\n {\n slug: project.slug,\n integrations: project.integrations.map((u) => u.name),\n },\n project.dir,\n );\n if (failed !== null) return failed;\n\n return ctx.succeed(\n {\n slug: project.slug,\n integrations: project.integrations.map((u) => u.name),\n findings: result.findings,\n },\n `check: OK - ${project.slug} (${project.integrations.length} integration${project.integrations.length === 1 ? '' : 's'})${result.findings.length ? `, ${result.findings.length} warning${result.findings.length === 1 ? '' : 's'}` : ''}.`,\n );\n },\n};\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { preconditionError, validationError } from '../errors.js';
|
|
2
|
+
import { expectedIntegrationsDts } from '../experience/checks.js';
|
|
3
|
+
import { readExperienceProject } from '../experience/project.js';
|
|
4
|
+
import { loadPins, regenerateIntegrations } from '../experience/regenerate.js';
|
|
5
|
+
export const codegenCommand = {
|
|
6
|
+
name: 'codegen',
|
|
7
|
+
usage: [
|
|
8
|
+
' codegen Regenerate src/.ekanos/integrations.d.ts from the pinned contracts',
|
|
9
|
+
' (offline, idempotent; --check fails when it is stale)',
|
|
10
|
+
],
|
|
11
|
+
specs: {
|
|
12
|
+
dir: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Project directory (default: the current one)',
|
|
15
|
+
},
|
|
16
|
+
check: {
|
|
17
|
+
type: 'boolean',
|
|
18
|
+
description: 'Fail when the committed file differs; write nothing',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
async run(ctx, args) {
|
|
22
|
+
const project = readExperienceProject(args.dir);
|
|
23
|
+
const { pinned, missing } = loadPins(project);
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw preconditionError(`No pinned contract for integration${missing.length === 1 ? '' : 's'}: ${missing.join(', ')}.`, 'Run "ekanos add <integration>@<version> --from-dir <contracts folder>" or "ekanos sync" first.');
|
|
26
|
+
}
|
|
27
|
+
if (args.flags.check === true) {
|
|
28
|
+
const fs = await import('node:fs');
|
|
29
|
+
const path = await import('node:path');
|
|
30
|
+
const file = path.join(project.dir, 'src/.ekanos/integrations.d.ts');
|
|
31
|
+
const current = fs.existsSync(file)
|
|
32
|
+
? fs.readFileSync(file, 'utf8')
|
|
33
|
+
: null;
|
|
34
|
+
if (current !== expectedIntegrationsDts(pinned)) {
|
|
35
|
+
throw validationError('src/.ekanos/integrations.d.ts is stale.', 'Run "ekanos codegen" and commit the result.');
|
|
36
|
+
}
|
|
37
|
+
return ctx.succeed({ changed: false, integrations: pinned.length }, 'integrations.d.ts is up to date.');
|
|
38
|
+
}
|
|
39
|
+
const changed = regenerateIntegrations(project, pinned);
|
|
40
|
+
return ctx.succeed({
|
|
41
|
+
changed,
|
|
42
|
+
integrations: pinned.length,
|
|
43
|
+
file: 'src/.ekanos/integrations.d.ts',
|
|
44
|
+
}, changed
|
|
45
|
+
? `Wrote src/.ekanos/integrations.d.ts (${pinned.length} integration${pinned.length === 1 ? '' : 's'}).`
|
|
46
|
+
: 'integrations.d.ts is already up to date.');
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=codegen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen.js","sourceRoot":"","sources":["../../src/commands/codegen.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAG5E,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,IAAI,EAAE,SAAS;IACf,KAAK,EAAE;QACL,iFAAiF;QACjF,oEAAoE;KACrE;IACD,KAAK,EAAE;QACL,GAAG,EAAE;YACH,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,8CAA8C;SAC5D;QACD,KAAK,EAAE;YACL,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,qDAAqD;SACnE;KACF;IACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI;QACjB,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,iBAAiB,CACrB,qCAAqC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAC9F,gGAAgG,CACjG,CAAC;QACJ,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YAC9B,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;YACnC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,+BAA+B,CAAC,CAAC;YACrE,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;gBACjC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;gBAC/B,CAAC,CAAC,IAAI,CAAC;YACT,IAAI,OAAO,KAAK,uBAAuB,CAAC,MAAM,CAAC,EAAE,CAAC;gBAChD,MAAM,eAAe,CACnB,yCAAyC,EACzC,6CAA6C,CAC9C,CAAC;YACJ,CAAC;YAED,OAAO,GAAG,CAAC,OAAO,CAChB,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,EAC/C,kCAAkC,CACnC,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,sBAAsB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAExD,OAAO,GAAG,CAAC,OAAO,CAChB;YACE,OAAO;YACP,YAAY,EAAE,MAAM,CAAC,MAAM;YAC3B,IAAI,EAAE,+BAA+B;SACtC,EACD,OAAO;YACL,CAAC,CAAC,wCAAwC,MAAM,CAAC,MAAM,eAAe,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI;YACxG,CAAC,CAAC,0CAA0C,CAC/C,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { preconditionError, validationError } from '../errors';\nimport { expectedIntegrationsDts } from '../experience/checks';\nimport { readExperienceProject } from '../experience/project';\nimport { loadPins, regenerateIntegrations } from '../experience/regenerate';\nimport type { ExperienceCommand } from './index-experience';\n\nexport const codegenCommand: ExperienceCommand = {\n name: 'codegen',\n usage: [\n ' codegen Regenerate src/.ekanos/integrations.d.ts from the pinned contracts',\n ' (offline, idempotent; --check fails when it is stale)',\n ],\n specs: {\n dir: {\n type: 'string',\n description: 'Project directory (default: the current one)',\n },\n check: {\n type: 'boolean',\n description: 'Fail when the committed file differs; write nothing',\n },\n },\n async run(ctx, args) {\n const project = readExperienceProject(args.dir);\n const { pinned, missing } = loadPins(project);\n if (missing.length > 0) {\n throw preconditionError(\n `No pinned contract for integration${missing.length === 1 ? '' : 's'}: ${missing.join(', ')}.`,\n 'Run \"ekanos add <integration>@<version> --from-dir <contracts folder>\" or \"ekanos sync\" first.',\n );\n }\n\n if (args.flags.check === true) {\n const fs = await import('node:fs');\n const path = await import('node:path');\n const file = path.join(project.dir, 'src/.ekanos/integrations.d.ts');\n const current = fs.existsSync(file)\n ? fs.readFileSync(file, 'utf8')\n : null;\n if (current !== expectedIntegrationsDts(pinned)) {\n throw validationError(\n 'src/.ekanos/integrations.d.ts is stale.',\n 'Run \"ekanos codegen\" and commit the result.',\n );\n }\n\n return ctx.succeed(\n { changed: false, integrations: pinned.length },\n 'integrations.d.ts is up to date.',\n );\n }\n\n const changed = regenerateIntegrations(project, pinned);\n\n return ctx.succeed(\n {\n changed,\n integrations: pinned.length,\n file: 'src/.ekanos/integrations.d.ts',\n },\n changed\n ? `Wrote src/.ekanos/integrations.d.ts (${pinned.length} integration${pinned.length === 1 ? '' : 's'}).`\n : 'integrations.d.ts is already up to date.',\n );\n },\n};\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Contract } from '@ekanos/integration-schema';
|
|
2
|
+
import type { CliContext } from '../context.js';
|
|
3
|
+
import { type HarnessResolver } from '../dev-experience.js';
|
|
4
|
+
import type { ExitCode } from '../exit-codes.js';
|
|
5
|
+
import type { ExperienceCommand } from './index-experience.js';
|
|
6
|
+
/** The slice of `@ekanos/sdk/contract` this command uses. */
|
|
7
|
+
export interface ContractModule {
|
|
8
|
+
analyzeContract(definition: unknown, options: {
|
|
9
|
+
packageVersion: string;
|
|
10
|
+
contractVersion: string;
|
|
11
|
+
previous?: Contract | null;
|
|
12
|
+
examples?: Readonly<Record<string, unknown>>;
|
|
13
|
+
}): {
|
|
14
|
+
contract: Contract;
|
|
15
|
+
diagnostics: Array<{
|
|
16
|
+
rule: string;
|
|
17
|
+
severity: 'error' | 'warn' | 'info';
|
|
18
|
+
message: string;
|
|
19
|
+
operation?: string;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
synthesizeExample(schema: unknown): unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface ContractArgs {
|
|
25
|
+
dir: string;
|
|
26
|
+
check: boolean;
|
|
27
|
+
record: boolean;
|
|
28
|
+
bump: boolean;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* `--record --op tool/x[,action/y]`: record REAL examples for these
|
|
32
|
+
* operations from a run through the harness runner, instead of writing
|
|
33
|
+
* placeholders.
|
|
34
|
+
*/
|
|
35
|
+
ops?: string[];
|
|
36
|
+
/** Which run mode produces the example (default `fixtures`). */
|
|
37
|
+
from?: 'fixtures' | 'live';
|
|
38
|
+
/** JSON input for the recorded run (single `--op` only); default: synthesized from the input schema. */
|
|
39
|
+
input?: string;
|
|
40
|
+
/** Test seam; the default loads the project's installed `@ekanos/harness/resolver`. */
|
|
41
|
+
loadHarness?: (projectDir: string) => Promise<HarnessResolver>;
|
|
42
|
+
/** Test seam; the default loads `@ekanos/sdk/contract` from the project. */
|
|
43
|
+
loadModule?: (projectDir: string) => Promise<ContractModule>;
|
|
44
|
+
}
|
|
45
|
+
/** The smallest contractVersion that satisfies the transition rules. */
|
|
46
|
+
export declare function nextContractVersion(previous: Contract, next: Contract): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* `ekanos contract`: derive `contract.json` from the integration definition.
|
|
49
|
+
* See the command's usage text for the three modes.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runContract(ctx: CliContext, args: ContractArgs): Promise<ExitCode>;
|
|
52
|
+
export declare const contractCommand: ExperienceCommand;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { assessContractTransition, checkContractFreshness, diffContract, validateAgainstSchema, verifyContract, } from '@ekanos/integration-schema';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { loadHarnessResolver } from '../dev-experience.js';
|
|
7
|
+
import { preconditionError, usageError, validationError } from '../errors.js';
|
|
8
|
+
import { loadDefinition } from '../load-definition.js';
|
|
9
|
+
import { loadProject } from '../project.js';
|
|
10
|
+
async function loadProjectModule(projectDir) {
|
|
11
|
+
try {
|
|
12
|
+
const require = createRequire(path.join(projectDir, 'package.json'));
|
|
13
|
+
return (await import(pathToFileURL(require.resolve('@ekanos/sdk/contract')).href));
|
|
14
|
+
}
|
|
15
|
+
catch (_a) {
|
|
16
|
+
throw preconditionError('This project\'s @ekanos/sdk does not provide "@ekanos/sdk/contract".', 'Upgrade @ekanos/sdk to a version that ships contract emission, then re-run.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function readJson(file) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch (_a) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readExamples(dir) {
|
|
28
|
+
const out = {};
|
|
29
|
+
if (!fs.existsSync(dir))
|
|
30
|
+
return out;
|
|
31
|
+
for (const name of fs.readdirSync(dir)) {
|
|
32
|
+
const match = /^(tool|action)-(.+)\.json$/.exec(name);
|
|
33
|
+
const value = match ? readJson(path.join(dir, name)) : null;
|
|
34
|
+
if (match && value !== null)
|
|
35
|
+
out[`${match[1]}/${match[2]}`] = value;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** The smallest contractVersion that satisfies the transition rules. */
|
|
40
|
+
export function nextContractVersion(previous, next) {
|
|
41
|
+
const diff = diffContract(previous, next);
|
|
42
|
+
if (diff.breaking.length === 0 && diff.compatible.length === 0) {
|
|
43
|
+
return canonicalSurfaceEqual(previous, next)
|
|
44
|
+
? null
|
|
45
|
+
: bump(previous.contractVersion, 'patch');
|
|
46
|
+
}
|
|
47
|
+
const zero = previous.compatLine.startsWith('0.');
|
|
48
|
+
if (diff.breaking.length > 0)
|
|
49
|
+
return bump(previous.contractVersion, zero ? 'minor' : 'major');
|
|
50
|
+
return bump(previous.contractVersion, zero ? 'patch' : 'minor');
|
|
51
|
+
}
|
|
52
|
+
function canonicalSurfaceEqual(a, b) {
|
|
53
|
+
return checkContractFreshness(b, a).length === 0;
|
|
54
|
+
}
|
|
55
|
+
function bump(version, part) {
|
|
56
|
+
const [major = 0, minor = 0, patch = 0] = version.split(/[.+-]/).map(Number);
|
|
57
|
+
if (part === 'major')
|
|
58
|
+
return `${major + 1}.0.0`;
|
|
59
|
+
if (part === 'minor')
|
|
60
|
+
return `${major}.${minor + 1}.0`;
|
|
61
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
62
|
+
}
|
|
63
|
+
function describe(problem) {
|
|
64
|
+
return `${problem.operation ? `${problem.operation}: ` : ''}${problem.message}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `ekanos contract`: derive `contract.json` from the integration definition.
|
|
68
|
+
* See the command's usage text for the three modes.
|
|
69
|
+
*/
|
|
70
|
+
export async function runContract(ctx, args) {
|
|
71
|
+
var _a;
|
|
72
|
+
var _b, _c, _d, _e, _f;
|
|
73
|
+
if (args.check && (args.record || args.bump)) {
|
|
74
|
+
throw usageError('--check cannot be combined with --record or --bump.', 'Run them separately: "ekanos contract --record", then "ekanos contract --check".');
|
|
75
|
+
}
|
|
76
|
+
const loaded = loadProject(args.dir);
|
|
77
|
+
if (loaded.project.kind === 'experience') {
|
|
78
|
+
throw preconditionError('An experience has no contract of its own.', 'Run "ekanos contract" in the integration project.');
|
|
79
|
+
}
|
|
80
|
+
const configPath = loaded.configPath;
|
|
81
|
+
const declared = (_a = loaded.project.contract) === null || _a === void 0 ? void 0 : _a.version;
|
|
82
|
+
if (!declared) {
|
|
83
|
+
throw preconditionError('ekanos.json declares no contract version.', 'Add "contract": { "version": "0.1.0" } to ekanos.json; the compat line follows it.');
|
|
84
|
+
}
|
|
85
|
+
const pkg = readJson(path.join(loaded.projectDir, 'package.json'));
|
|
86
|
+
if (!(pkg === null || pkg === void 0 ? void 0 : pkg.version)) {
|
|
87
|
+
throw preconditionError('package.json has no readable "version".', 'Add a semver version to package.json.');
|
|
88
|
+
}
|
|
89
|
+
const sdk = await ((_b = args.loadModule) !== null && _b !== void 0 ? _b : loadProjectModule)(loaded.projectDir);
|
|
90
|
+
const def = await loadDefinition(loaded.primary.entryPath, loaded.projectDir);
|
|
91
|
+
if (!def.ok) {
|
|
92
|
+
throw validationError(`The integration definition did not load: ${def.message}`, 'Run "ekanos validate" first and fix what it reports.');
|
|
93
|
+
}
|
|
94
|
+
const contractFile = path.join(loaded.projectDir, 'contract.json');
|
|
95
|
+
const examplesDir = path.join(loaded.projectDir, 'contracts', 'examples');
|
|
96
|
+
const previous = fs.existsSync(contractFile)
|
|
97
|
+
? verifyContract(readJson(contractFile)).contract
|
|
98
|
+
: null;
|
|
99
|
+
let contractVersion = declared;
|
|
100
|
+
const analyze = () => sdk.analyzeContract(def.definition, {
|
|
101
|
+
packageVersion: pkg.version,
|
|
102
|
+
contractVersion,
|
|
103
|
+
previous,
|
|
104
|
+
examples: readExamples(examplesDir),
|
|
105
|
+
});
|
|
106
|
+
let result = analyze();
|
|
107
|
+
const wanted = (_c = args.ops) !== null && _c !== void 0 ? _c : [];
|
|
108
|
+
if (wanted.length > 0 && !args.record) {
|
|
109
|
+
throw usageError('--op only applies with --record.', 'Run "ekanos contract --record --op tool/<name>".');
|
|
110
|
+
}
|
|
111
|
+
if (args.input !== undefined && wanted.length !== 1) {
|
|
112
|
+
throw usageError('--input needs exactly one --op.', 'Record one operation at a time with its own --input.');
|
|
113
|
+
}
|
|
114
|
+
if (args.record && wanted.length > 0) {
|
|
115
|
+
let input;
|
|
116
|
+
if (args.input !== undefined) {
|
|
117
|
+
try {
|
|
118
|
+
input = JSON.parse(args.input);
|
|
119
|
+
}
|
|
120
|
+
catch (_g) {
|
|
121
|
+
throw usageError('--input is not valid JSON.', 'Pass the operation input as a JSON object, e.g. --input \'{"limit":5}\'.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const mode = (_d = args.from) !== null && _d !== void 0 ? _d : 'fixtures';
|
|
125
|
+
const harness = await ((_e = args.loadHarness) !== null && _e !== void 0 ? _e : loadHarnessResolver)(loaded.projectDir);
|
|
126
|
+
if (typeof harness.runOperationOnce !== 'function') {
|
|
127
|
+
throw preconditionError('The installed @ekanos/harness cannot run an operation for recording.', 'Upgrade @ekanos/harness and @ekanos/cli together, then re-run.');
|
|
128
|
+
}
|
|
129
|
+
const wrote = [];
|
|
130
|
+
for (const key of wanted) {
|
|
131
|
+
const op = result.contract.operations.find((o) => o.id.endsWith(`/${key}`));
|
|
132
|
+
if (!op) {
|
|
133
|
+
throw validationError(`The contract has no operation "${key}".`, `Known operations: ${result.contract.operations.map((o) => `${o.kind}/${o.name}`).join(', ') || 'none'}.`);
|
|
134
|
+
}
|
|
135
|
+
if (op.effect === 'write') {
|
|
136
|
+
// A write is only ever recorded from a scenario fixture: reading a
|
|
137
|
+
// file performs nothing. Running it live would perform it for real.
|
|
138
|
+
const fixture = writeFixturePath(loaded.primary.slug, key);
|
|
139
|
+
if (mode === 'live') {
|
|
140
|
+
throw validationError(`${key} is a write; --from live would perform it for real, and a recording never does.`, `Add ${fixture} (a real-shaped response), then run "ekanos contract --record --op ${key} --from fixtures".`);
|
|
141
|
+
}
|
|
142
|
+
if (!fs.existsSync(path.join(loaded.projectDir, fixture))) {
|
|
143
|
+
throw validationError(`${key} is a write with no scenario fixture to record from.`, `Add ${fixture} holding a real-shaped response, then re-run "ekanos contract --record --op ${key} --from fixtures". Nothing is called.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (op.opaque.output) {
|
|
147
|
+
throw validationError(`${key} has an opaque output schema, so a recording cannot be validated.`, 'Declare an output schema first.');
|
|
148
|
+
}
|
|
149
|
+
const run = await harness.runOperationOnce({
|
|
150
|
+
projectRoot: loaded.projectDir,
|
|
151
|
+
op: key,
|
|
152
|
+
input: input !== null && input !== void 0 ? input : sdk.synthesizeExample(op.input),
|
|
153
|
+
mode,
|
|
154
|
+
env: args.env,
|
|
155
|
+
deriveContract: () => result.contract,
|
|
156
|
+
});
|
|
157
|
+
if (!run.ok) {
|
|
158
|
+
throw validationError(`Recording ${key} (${mode}) failed: ${run.reason}${run.detail ? ` - ${run.detail}` : ` - ${run.message}`}`, mode === 'live'
|
|
159
|
+
? 'Check the credentials (ekanos dev -> Connect) and the input, then re-run.'
|
|
160
|
+
: `Fixtures mode reads fixtures/<slug>/${key.replace('/', '-')}.default.json (or an existing outputExample). Author that file, or record from a real run with "--from live".`);
|
|
161
|
+
}
|
|
162
|
+
const problems = validateAgainstSchema(op.output, run.data);
|
|
163
|
+
if (problems.length > 0) {
|
|
164
|
+
throw validationError(`${key} returned data that does not satisfy its output schema: ${problems[0]}`, 'Fix the output schema or the handler; nothing was written.');
|
|
165
|
+
}
|
|
166
|
+
const file = path.join(examplesDir, `${key.replace('/', '-')}.json`);
|
|
167
|
+
fs.mkdirSync(examplesDir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(file, `${JSON.stringify(run.data, null, 2)}\n`);
|
|
169
|
+
wrote.push(path.relative(loaded.projectDir, file));
|
|
170
|
+
}
|
|
171
|
+
ctx.log(`Recorded ${wrote.length} example(s) from a ${mode} run: ${wrote.join(', ')}. Review them before committing${mode === 'live' ? ' (live data may contain real records)' : ''}.`);
|
|
172
|
+
result = analyze();
|
|
173
|
+
}
|
|
174
|
+
else if (args.record) {
|
|
175
|
+
const wrote = [];
|
|
176
|
+
const advice = [];
|
|
177
|
+
for (const diag of result.diagnostics) {
|
|
178
|
+
if (diag.rule !== 'contract/example-missing' || !diag.operation)
|
|
179
|
+
continue;
|
|
180
|
+
const op = result.contract.operations.find((o) => o.id.endsWith(`/${diag.operation}`));
|
|
181
|
+
if (!op || op.opaque.output)
|
|
182
|
+
continue;
|
|
183
|
+
const file = path.join(examplesDir, `${diag.operation.replace('/', '-')}.json`);
|
|
184
|
+
fs.mkdirSync(examplesDir, { recursive: true });
|
|
185
|
+
fs.writeFileSync(file, `${JSON.stringify(sdk.synthesizeExample(op.output), null, 2)}\n`);
|
|
186
|
+
wrote.push(path.relative(loaded.projectDir, file));
|
|
187
|
+
advice.push(op.effect === 'write'
|
|
188
|
+
? `${diag.operation}: add ${writeFixturePath(loaded.primary.slug, diag.operation)} with a real-shaped response, then "ekanos contract --record --op ${diag.operation} --from fixtures" (writes are never recorded live)`
|
|
189
|
+
: `${diag.operation}: add ${writeFixturePath(loaded.primary.slug, diag.operation)}, or "ekanos contract --record --op ${diag.operation} --from live"`);
|
|
190
|
+
}
|
|
191
|
+
if (wrote.length > 0) {
|
|
192
|
+
ctx.log(`Wrote ${wrote.length} placeholder example(s) synthesized from the output schema (not recorded from a run): ${wrote.join(', ')}. Fixtures mode will serve these placeholders until you replace them:\n ${advice.join('\n ')}`);
|
|
193
|
+
result = analyze();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (args.bump && previous) {
|
|
197
|
+
const wanted = nextContractVersion(previous, result.contract);
|
|
198
|
+
if (wanted && wanted !== contractVersion) {
|
|
199
|
+
contractVersion = wanted;
|
|
200
|
+
const raw = (_f = readJson(configPath)) !== null && _f !== void 0 ? _f : {};
|
|
201
|
+
raw.contract = Object.assign(Object.assign({}, raw.contract), { version: wanted });
|
|
202
|
+
fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`);
|
|
203
|
+
ctx.log(`contract.version ${declared} -> ${wanted} in ekanos.json`);
|
|
204
|
+
result = analyze();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const diag of result.diagnostics) {
|
|
208
|
+
ctx.log(`${diag.severity === 'error' ? 'error' : diag.severity === 'warn' ? 'warn ' : 'info '} ${diag.operation ? `${diag.operation}: ` : ''}${diag.message} (${diag.rule})`);
|
|
209
|
+
}
|
|
210
|
+
const errors = result.diagnostics.filter((d) => d.severity === 'error');
|
|
211
|
+
if (errors.length > 0) {
|
|
212
|
+
throw validationError(`${errors.length} contract error(s): ${errors
|
|
213
|
+
.slice(0, 3)
|
|
214
|
+
.map((e) => { var _a; return `${(_a = e.operation) !== null && _a !== void 0 ? _a : ''} ${e.message}`.trim(); })
|
|
215
|
+
.join('; ')}`, 'Fix them (declare outputs and examples, or run "ekanos contract --record"), then re-run.');
|
|
216
|
+
}
|
|
217
|
+
const transition = previous
|
|
218
|
+
? assessContractTransition(previous, result.contract).problems
|
|
219
|
+
: [];
|
|
220
|
+
if (args.check) {
|
|
221
|
+
const problems = checkContractFreshness(result.contract, previous, {
|
|
222
|
+
strictPackageVersion: true,
|
|
223
|
+
});
|
|
224
|
+
if (problems.length > 0) {
|
|
225
|
+
throw validationError(`contract.json is out of date: ${problems.map(describe).join(' | ')}`, 'Run "ekanos contract" (add --bump for the version) and commit contract.json.');
|
|
226
|
+
}
|
|
227
|
+
return ctx.succeed({
|
|
228
|
+
ok: true,
|
|
229
|
+
digest: result.contract.digest,
|
|
230
|
+
operations: result.contract.operations.length,
|
|
231
|
+
diagnostics: result.diagnostics,
|
|
232
|
+
}, `contract.json is current (${result.contract.operations.length} operations).`);
|
|
233
|
+
}
|
|
234
|
+
if (transition.length > 0) {
|
|
235
|
+
throw validationError(`This change is not allowed on the current contract version: ${transition.map(describe).join(' | ')}`, 'Run "ekanos contract --bump", or set contract.version in ekanos.json by hand (a breaking change needs a new compat line).');
|
|
236
|
+
}
|
|
237
|
+
fs.writeFileSync(contractFile, `${JSON.stringify(result.contract, null, 2)}\n`);
|
|
238
|
+
return ctx.succeed({
|
|
239
|
+
written: 'contract.json',
|
|
240
|
+
digest: result.contract.digest,
|
|
241
|
+
operations: result.contract.operations.length,
|
|
242
|
+
diagnostics: result.diagnostics,
|
|
243
|
+
}, `Wrote contract.json (${result.contract.operations.length} operations, ${result.contract.compatLine} line, contract ${result.contract.contractVersion}).`);
|
|
244
|
+
}
|
|
245
|
+
export const contractCommand = {
|
|
246
|
+
name: 'contract',
|
|
247
|
+
usage: [
|
|
248
|
+
' contract Derive ./contract.json from the integration definition;',
|
|
249
|
+
' --check fails on a stale file (CI, prepack), --record writes',
|
|
250
|
+
' missing output examples, --bump bumps the contract version',
|
|
251
|
+
],
|
|
252
|
+
specs: {
|
|
253
|
+
dir: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: 'Project directory (default: the current one)',
|
|
256
|
+
},
|
|
257
|
+
check: {
|
|
258
|
+
type: 'boolean',
|
|
259
|
+
description: 'Derive in memory and fail if contract.json differs',
|
|
260
|
+
},
|
|
261
|
+
record: {
|
|
262
|
+
type: 'boolean',
|
|
263
|
+
description: 'Write placeholder examples (contracts/examples) for operations that have none; with --op, record real ones from a run',
|
|
264
|
+
},
|
|
265
|
+
op: {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description: 'With --record: operation(s) to record from a real run, comma separated (tool/list_posts)',
|
|
268
|
+
},
|
|
269
|
+
from: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'With --op: run mode to record from, fixtures (default) or live',
|
|
272
|
+
},
|
|
273
|
+
input: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'With --op: JSON input for the recorded run',
|
|
276
|
+
},
|
|
277
|
+
bump: {
|
|
278
|
+
type: 'boolean',
|
|
279
|
+
description: 'Rewrite ekanos.json contract.version to match the surface change',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
run(ctx, args) {
|
|
283
|
+
const from = args.flags.from;
|
|
284
|
+
if (from !== undefined && from !== 'live' && from !== 'fixtures') {
|
|
285
|
+
throw usageError(`--from must be "fixtures" or "live", not "${String(from)}".`, 'Run "ekanos contract --record --op tool/<name> --from live".');
|
|
286
|
+
}
|
|
287
|
+
return runContract(ctx, Object.assign(Object.assign(Object.assign({ dir: args.dir, check: args.flags.check === true, record: args.flags.record === true, bump: args.flags.bump === true, env: args.env }, (typeof args.flags.op === 'string' && args.flags.op !== ''
|
|
288
|
+
? { ops: args.flags.op.split(',').map((o) => o.trim()) }
|
|
289
|
+
: {})), (args.flags.from === 'live' || args.flags.from === 'fixtures'
|
|
290
|
+
? { from: args.flags.from }
|
|
291
|
+
: {})), (typeof args.flags.input === 'string'
|
|
292
|
+
? { input: args.flags.input }
|
|
293
|
+
: {})));
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
/** Where a scenario fixture for an operation lives, project-relative. */
|
|
297
|
+
function writeFixturePath(slug, opKey) {
|
|
298
|
+
return path.join('fixtures', slug, `${opKey.replace('/', '-')}.default.json`);
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/commands/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,wBAAwB,EACxB,sBAAsB,EACtB,YAAY,EACZ,qBAAqB,EACrB,cAAc,GACf,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EAAwB,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE3E,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA+CzC,KAAK,UAAU,iBAAiB,CAAC,UAAkB;IACjD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;QACrE,OAAO,CAAC,MAAM,MAAM,CAClB,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAC5D,CAAmB,CAAC;IACvB,CAAC;eAAO,CAAC;QACP,MAAM,iBAAiB,CACrB,sEAAsE,EACtE,6EAA6E,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAI,IAAY;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAM,CAAC;IACxD,CAAC;eAAO,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAU,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACrE,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI;YAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;IACtE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,mBAAmB,CACjC,QAAkB,EAClB,IAAc;IAEd,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/D,OAAO,qBAAqB,CAAC,QAAQ,EAAE,IAAI,CAAC;YAC1C,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QAC1B,OAAO,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAClE,OAAO,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,qBAAqB,CAAC,CAAW,EAAE,CAAW;IACrD,OAAO,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,IAAI,CAAC,OAAe,EAAE,IAAiC;IAC9D,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7E,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,GAAG,KAAK,GAAG,CAAC,MAAM,CAAC;IAChD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC;IACvD,OAAO,GAAG,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,QAAQ,CAAC,OAAwB;IACxC,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAe,EACf,IAAkB;;;IAElB,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,UAAU,CACd,qDAAqD,EACrD,kFAAkF,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACzC,MAAM,iBAAiB,CACrB,2CAA2C,EAC3C,mDAAmD,CACpD,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IACrC,MAAM,QAAQ,GAAG,MAAA,MAAM,CAAC,OAAO,CAAC,QAAQ,0CAAE,OAAO,CAAC;IAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,iBAAiB,CACrB,2CAA2C,EAC3C,oFAAoF,CACrF,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,QAAQ,CAClB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,cAAc,CAAC,CAC7C,CAAC;IACF,IAAI,EAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,OAAO,CAAA,EAAE,CAAC;QAClB,MAAM,iBAAiB,CACrB,yCAAyC,EACzC,uCAAuC,CACxC,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,OAAC,IAAI,CAAC,UAAU,mCAAI,iBAAiB,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC9E,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,eAAe,CACnB,4CAA4C,GAAG,CAAC,OAAO,EAAE,EACzD,sDAAsD,CACvD,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IACnE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;QAC1C,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAU,YAAY,CAAC,CAAC,CAAC,QAAQ;QAC1D,CAAC,CAAC,IAAI,CAAC;IAET,IAAI,eAAe,GAAG,QAAQ,CAAC;IAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CACnB,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,UAAU,EAAE;QAClC,cAAc,EAAE,GAAG,CAAC,OAAiB;QACrC,eAAe;QACf,QAAQ;QACR,QAAQ,EAAE,YAAY,CAAC,WAAW,CAAC;KACpC,CAAC,CAAC;IAEL,IAAI,MAAM,GAAG,OAAO,EAAE,CAAC;IAEvB,MAAM,MAAM,SAAG,IAAI,CAAC,GAAG,mCAAI,EAAE,CAAC;IAC9B,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACtC,MAAM,UAAU,CACd,kCAAkC,EAClC,kDAAkD,CACnD,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,UAAU,CACd,iCAAiC,EACjC,sDAAsD,CACvD,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,IAAI,KAAc,CAAC;QACnB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjC,CAAC;uBAAO,CAAC;gBACP,MAAM,UAAU,CACd,4BAA4B,EAC5B,0EAA0E,CAC3E,CAAC;YACJ,CAAC;QACH,CAAC;QACD,MAAM,IAAI,SAAG,IAAI,CAAC,IAAI,mCAAI,UAAU,CAAC;QACrC,MAAM,OAAO,GAAG,MAAM,OAAC,IAAI,CAAC,WAAW,mCAAI,mBAAmB,CAAC,CAC7D,MAAM,CAAC,UAAU,CAClB,CAAC;QACF,IAAI,OAAO,OAAO,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;YACnD,MAAM,iBAAiB,CACrB,sEAAsE,EACtE,gEAAgE,CACjE,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAC/C,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,GAAG,EAAE,CAAC,CACzB,CAAC;YACF,IAAI,CAAC,EAAE,EAAE,CAAC;gBACR,MAAM,eAAe,CACnB,kCAAkC,GAAG,IAAI,EACzC,qBAAqB,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAC1G,CAAC;YACJ,CAAC;YACD,IAAI,EAAE,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;gBAC1B,mEAAmE;gBACnE,oEAAoE;gBACpE,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC3D,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,MAAM,eAAe,CACnB,GAAG,GAAG,iFAAiF,EACvF,OAAO,OAAO,sEAAsE,GAAG,oBAAoB,CAC5G,CAAC;gBACJ,CAAC;gBACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;oBAC1D,MAAM,eAAe,CACnB,GAAG,GAAG,sDAAsD,EAC5D,OAAO,OAAO,+EAA+E,GAAG,uCAAuC,CACxI,CAAC;gBACJ,CAAC;YACH,CAAC;YACD,IAAI,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACrB,MAAM,eAAe,CACnB,GAAG,GAAG,mEAAmE,EACzE,iCAAiC,CAClC,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC;gBACzC,WAAW,EAAE,MAAM,CAAC,UAAU;gBAC9B,EAAE,EAAE,GAAG;gBACP,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,CAAC;gBAC/C,IAAI;gBACJ,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,cAAc,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ;aACtC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,eAAe,CACnB,aAAa,GAAG,KAAK,IAAI,aAAa,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,EAAE,EAC1G,IAAI,KAAK,MAAM;oBACb,CAAC,CAAC,2EAA2E;oBAC7E,CAAC,CAAC,uCAAuC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,+GAA+G,CAChL,CAAC;YACJ,CAAC;YACD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,MAAM,eAAe,CACnB,GAAG,GAAG,2DAA2D,QAAQ,CAAC,CAAC,CAAC,EAAE,EAC9E,4DAA4D,CAC7D,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACrE,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACjE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;QACrD,CAAC;QACD,GAAG,CAAC,GAAG,CACL,YAAY,KAAK,CAAC,MAAM,sBAAsB,IAAI,SAAS,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kCAAkC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAAC,CAAC,EAAE,GAAG,CAC/K,CAAC;QACF,MAAM,GAAG,OAAO,EAAE,CAAC;IACrB,CAAC;SAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,IAAI,KAAK,0BAA0B,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,SAAS;YAC1E,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAC/C,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,CACpC,CAAC;YACF,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,MAAM;gBAAE,SAAS;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CACpB,WAAW,EACX,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAC3C,CAAC;YACF,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,EAAE,CAAC,aAAa,CACd,IAAI,EACJ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CACjE,CAAC;YACF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;YACnD,MAAM,CAAC,IAAI,CACT,EAAE,CAAC,MAAM,KAAK,OAAO;gBACnB,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,SAAS,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,qEAAqE,IAAI,CAAC,SAAS,oDAAoD;gBACxN,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,SAAS,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,uCAAuC,IAAI,CAAC,SAAS,eAAe,CACxJ,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,GAAG,CAAC,GAAG,CACL,SAAS,KAAK,CAAC,MAAM,yFAAyF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,4EAA4E,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAChO,CAAC;YACF,MAAM,GAAG,OAAO,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9D,IAAI,MAAM,IAAI,MAAM,KAAK,eAAe,EAAE,CAAC;YACzC,eAAe,GAAG,MAAM,CAAC;YACzB,MAAM,GAAG,SAAG,QAAQ,CAA0B,UAAU,CAAC,mCAAI,EAAE,CAAC;YAChE,GAAG,CAAC,QAAQ,mCACN,GAAG,CAAC,QAA+B,KACvC,OAAO,EAAE,MAAM,GAChB,CAAC;YACF,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YAClE,GAAG,CAAC,GAAG,CAAC,oBAAoB,QAAQ,OAAO,MAAM,iBAAiB,CAAC,CAAC;YACpE,MAAM,GAAG,OAAO,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACtC,GAAG,CAAC,GAAG,CACL,GAAG,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,GAAG,CACrK,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IACxE,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,eAAe,CACnB,GAAG,MAAM,CAAC,MAAM,uBAAuB,MAAM;aAC1C,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,kBAAC,GAAG,MAAA,CAAC,CAAC,SAAS,mCAAI,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,GAAA,CAAC;aACtD,IAAI,CAAC,IAAI,CAAC,EAAE,EACf,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,QAAQ;QACzB,CAAC,CAAC,wBAAwB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ;QAC9D,CAAC,CAAC,EAAE,CAAC;IAEP,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE;YACjE,oBAAoB,EAAE,IAAI;SAC3B,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,eAAe,CACnB,iCAAiC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EACrE,8EAA8E,CAC/E,CAAC;QACJ,CAAC;QACD,OAAO,GAAG,CAAC,OAAO,CAChB;YACE,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM;YAC9B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM;YAC7C,WAAW,EAAE,MAAM,CAAC,WAAW;SAChC,EACD,6BAA6B,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,eAAe,CAC9E,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,eAAe,CACnB,+DAA+D,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EACrG,2HAA2H,CAC5H,CAAC;IACJ,CAAC;IACD,EAAE,CAAC,aAAa,CACd,YAAY,EACZ,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAChD,CAAC;IACF,OAAO,GAAG,CAAC,OAAO,CAChB;QACE,OAAO,EAAE,eAAe;QACxB,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM;QAC9B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM;QAC7C,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,EACD,wBAAwB,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,gBAAgB,MAAM,CAAC,QAAQ,CAAC,UAAU,mBAAmB,MAAM,CAAC,QAAQ,CAAC,eAAe,IAAI,CAC1J,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAsB;IAChD,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE;QACL,sEAAsE;QACtE,2EAA2E;QAC3E,yEAAyE;KAC1E;IACD,KAAK,EAAE;QACL,GAAG,EAAE;YACH,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,8CAA8C;SAC5D;QACD,KAAK,EAAE;YACL,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,oDAAoD;SAClE;QACD,MAAM,EAAE;YACN,IAAI,EAAE,SAAS;YACf,WAAW,EACT,uHAAuH;SAC1H;QACD,EAAE,EAAE;YACF,IAAI,EAAE,QAAQ;YACd,WAAW,EACT,0FAA0F;SAC7F;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,QAAQ;YACd,WAAW,EACT,gEAAgE;SACnE;QACD,KAAK,EAAE;YACL,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,4CAA4C;SAC1D;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,SAAS;YACf,WAAW,EACT,kEAAkE;SACrE;KACF;IACD,GAAG,CAAC,GAAG,EAAE,IAAI;QACX,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QAC7B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACjE,MAAM,UAAU,CACd,6CAA6C,MAAM,CAAC,IAAI,CAAC,IAAI,EAC7D,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QAED,OAAO,WAAW,CAAC,GAAG,8CACpB,GAAG,EAAE,IAAI,CAAC,GAAG,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,EAChC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,IAAI,EAClC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,EAC9B,GAAG,EAAE,IAAI,CAAC,GAAG,IACV,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE;YAC3D,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE;YACxD,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU;YAC9D,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;YAC3B,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ;YACtC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;YAC7B,CAAC,CAAC,EAAE,CAAC,EACP,CAAC;IACL,CAAC;CACF,CAAC;AAEF,yEAAyE;AACzE,SAAS,gBAAgB,CAAC,IAAY,EAAE,KAAa;IACnD,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC;AAChF,CAAC","sourcesContent":["import {\n type Contract,\n type ContractProblem,\n assessContractTransition,\n checkContractFreshness,\n diffContract,\n validateAgainstSchema,\n verifyContract,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport * as path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport type { CliContext } from '../context';\nimport { type HarnessResolver, loadHarnessResolver } from '../dev-experience';\nimport { preconditionError, usageError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { loadDefinition } from '../load-definition';\nimport { loadProject } from '../project';\nimport type { ExperienceCommand } from './index-experience';\n\n/** The slice of `@ekanos/sdk/contract` this command uses. */\nexport interface ContractModule {\n analyzeContract(\n definition: unknown,\n options: {\n packageVersion: string;\n contractVersion: string;\n previous?: Contract | null;\n examples?: Readonly<Record<string, unknown>>;\n },\n ): {\n contract: Contract;\n diagnostics: Array<{\n rule: string;\n severity: 'error' | 'warn' | 'info';\n message: string;\n operation?: string;\n }>;\n };\n synthesizeExample(schema: unknown): unknown;\n}\n\nexport interface ContractArgs {\n dir: string;\n check: boolean;\n record: boolean;\n bump: boolean;\n env?: Record<string, string | undefined>;\n /**\n * `--record --op tool/x[,action/y]`: record REAL examples for these\n * operations from a run through the harness runner, instead of writing\n * placeholders.\n */\n ops?: string[];\n /** Which run mode produces the example (default `fixtures`). */\n from?: 'fixtures' | 'live';\n /** JSON input for the recorded run (single `--op` only); default: synthesized from the input schema. */\n input?: string;\n /** Test seam; the default loads the project's installed `@ekanos/harness/resolver`. */\n loadHarness?: (projectDir: string) => Promise<HarnessResolver>;\n /** Test seam; the default loads `@ekanos/sdk/contract` from the project. */\n loadModule?: (projectDir: string) => Promise<ContractModule>;\n}\n\nasync function loadProjectModule(projectDir: string): Promise<ContractModule> {\n try {\n const require = createRequire(path.join(projectDir, 'package.json'));\n return (await import(\n pathToFileURL(require.resolve('@ekanos/sdk/contract')).href\n )) as ContractModule;\n } catch {\n throw preconditionError(\n 'This project\\'s @ekanos/sdk does not provide \"@ekanos/sdk/contract\".',\n 'Upgrade @ekanos/sdk to a version that ships contract emission, then re-run.',\n );\n }\n}\n\nfunction readJson<T>(file: string): T | null {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8')) as T;\n } catch {\n return null;\n }\n}\n\nfunction readExamples(dir: string): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (!fs.existsSync(dir)) return out;\n for (const name of fs.readdirSync(dir)) {\n const match = /^(tool|action)-(.+)\\.json$/.exec(name);\n const value = match ? readJson<unknown>(path.join(dir, name)) : null;\n if (match && value !== null) out[`${match[1]}/${match[2]}`] = value;\n }\n return out;\n}\n\n/** The smallest contractVersion that satisfies the transition rules. */\nexport function nextContractVersion(\n previous: Contract,\n next: Contract,\n): string | null {\n const diff = diffContract(previous, next);\n if (diff.breaking.length === 0 && diff.compatible.length === 0) {\n return canonicalSurfaceEqual(previous, next)\n ? null\n : bump(previous.contractVersion, 'patch');\n }\n const zero = previous.compatLine.startsWith('0.');\n if (diff.breaking.length > 0)\n return bump(previous.contractVersion, zero ? 'minor' : 'major');\n return bump(previous.contractVersion, zero ? 'patch' : 'minor');\n}\n\nfunction canonicalSurfaceEqual(a: Contract, b: Contract): boolean {\n return checkContractFreshness(b, a).length === 0;\n}\n\nfunction bump(version: string, part: 'major' | 'minor' | 'patch'): string {\n const [major = 0, minor = 0, patch = 0] = version.split(/[.+-]/).map(Number);\n if (part === 'major') return `${major + 1}.0.0`;\n if (part === 'minor') return `${major}.${minor + 1}.0`;\n return `${major}.${minor}.${patch + 1}`;\n}\n\nfunction describe(problem: ContractProblem): string {\n return `${problem.operation ? `${problem.operation}: ` : ''}${problem.message}`;\n}\n\n/**\n * `ekanos contract`: derive `contract.json` from the integration definition.\n * See the command's usage text for the three modes.\n */\nexport async function runContract(\n ctx: CliContext,\n args: ContractArgs,\n): Promise<ExitCode> {\n if (args.check && (args.record || args.bump)) {\n throw usageError(\n '--check cannot be combined with --record or --bump.',\n 'Run them separately: \"ekanos contract --record\", then \"ekanos contract --check\".',\n );\n }\n const loaded = loadProject(args.dir);\n if (loaded.project.kind === 'experience') {\n throw preconditionError(\n 'An experience has no contract of its own.',\n 'Run \"ekanos contract\" in the integration project.',\n );\n }\n const configPath = loaded.configPath;\n const declared = loaded.project.contract?.version;\n if (!declared) {\n throw preconditionError(\n 'ekanos.json declares no contract version.',\n 'Add \"contract\": { \"version\": \"0.1.0\" } to ekanos.json; the compat line follows it.',\n );\n }\n const pkg = readJson<{ version?: string }>(\n path.join(loaded.projectDir, 'package.json'),\n );\n if (!pkg?.version) {\n throw preconditionError(\n 'package.json has no readable \"version\".',\n 'Add a semver version to package.json.',\n );\n }\n\n const sdk = await (args.loadModule ?? loadProjectModule)(loaded.projectDir);\n const def = await loadDefinition(loaded.primary.entryPath, loaded.projectDir);\n if (!def.ok) {\n throw validationError(\n `The integration definition did not load: ${def.message}`,\n 'Run \"ekanos validate\" first and fix what it reports.',\n );\n }\n\n const contractFile = path.join(loaded.projectDir, 'contract.json');\n const examplesDir = path.join(loaded.projectDir, 'contracts', 'examples');\n const previous = fs.existsSync(contractFile)\n ? verifyContract(readJson<unknown>(contractFile)).contract\n : null;\n\n let contractVersion = declared;\n const analyze = () =>\n sdk.analyzeContract(def.definition, {\n packageVersion: pkg.version as string,\n contractVersion,\n previous,\n examples: readExamples(examplesDir),\n });\n\n let result = analyze();\n\n const wanted = args.ops ?? [];\n if (wanted.length > 0 && !args.record) {\n throw usageError(\n '--op only applies with --record.',\n 'Run \"ekanos contract --record --op tool/<name>\".',\n );\n }\n if (args.input !== undefined && wanted.length !== 1) {\n throw usageError(\n '--input needs exactly one --op.',\n 'Record one operation at a time with its own --input.',\n );\n }\n\n if (args.record && wanted.length > 0) {\n let input: unknown;\n if (args.input !== undefined) {\n try {\n input = JSON.parse(args.input);\n } catch {\n throw usageError(\n '--input is not valid JSON.',\n 'Pass the operation input as a JSON object, e.g. --input \\'{\"limit\":5}\\'.',\n );\n }\n }\n const mode = args.from ?? 'fixtures';\n const harness = await (args.loadHarness ?? loadHarnessResolver)(\n loaded.projectDir,\n );\n if (typeof harness.runOperationOnce !== 'function') {\n throw preconditionError(\n 'The installed @ekanos/harness cannot run an operation for recording.',\n 'Upgrade @ekanos/harness and @ekanos/cli together, then re-run.',\n );\n }\n const wrote: string[] = [];\n for (const key of wanted) {\n const op = result.contract.operations.find((o) =>\n o.id.endsWith(`/${key}`),\n );\n if (!op) {\n throw validationError(\n `The contract has no operation \"${key}\".`,\n `Known operations: ${result.contract.operations.map((o) => `${o.kind}/${o.name}`).join(', ') || 'none'}.`,\n );\n }\n if (op.effect === 'write') {\n // A write is only ever recorded from a scenario fixture: reading a\n // file performs nothing. Running it live would perform it for real.\n const fixture = writeFixturePath(loaded.primary.slug, key);\n if (mode === 'live') {\n throw validationError(\n `${key} is a write; --from live would perform it for real, and a recording never does.`,\n `Add ${fixture} (a real-shaped response), then run \"ekanos contract --record --op ${key} --from fixtures\".`,\n );\n }\n if (!fs.existsSync(path.join(loaded.projectDir, fixture))) {\n throw validationError(\n `${key} is a write with no scenario fixture to record from.`,\n `Add ${fixture} holding a real-shaped response, then re-run \"ekanos contract --record --op ${key} --from fixtures\". Nothing is called.`,\n );\n }\n }\n if (op.opaque.output) {\n throw validationError(\n `${key} has an opaque output schema, so a recording cannot be validated.`,\n 'Declare an output schema first.',\n );\n }\n const run = await harness.runOperationOnce({\n projectRoot: loaded.projectDir,\n op: key,\n input: input ?? sdk.synthesizeExample(op.input),\n mode,\n env: args.env,\n deriveContract: () => result.contract,\n });\n if (!run.ok) {\n throw validationError(\n `Recording ${key} (${mode}) failed: ${run.reason}${run.detail ? ` - ${run.detail}` : ` - ${run.message}`}`,\n mode === 'live'\n ? 'Check the credentials (ekanos dev -> Connect) and the input, then re-run.'\n : `Fixtures mode reads fixtures/<slug>/${key.replace('/', '-')}.default.json (or an existing outputExample). Author that file, or record from a real run with \"--from live\".`,\n );\n }\n const problems = validateAgainstSchema(op.output, run.data);\n if (problems.length > 0) {\n throw validationError(\n `${key} returned data that does not satisfy its output schema: ${problems[0]}`,\n 'Fix the output schema or the handler; nothing was written.',\n );\n }\n const file = path.join(examplesDir, `${key.replace('/', '-')}.json`);\n fs.mkdirSync(examplesDir, { recursive: true });\n fs.writeFileSync(file, `${JSON.stringify(run.data, null, 2)}\\n`);\n wrote.push(path.relative(loaded.projectDir, file));\n }\n ctx.log(\n `Recorded ${wrote.length} example(s) from a ${mode} run: ${wrote.join(', ')}. Review them before committing${mode === 'live' ? ' (live data may contain real records)' : ''}.`,\n );\n result = analyze();\n } else if (args.record) {\n const wrote: string[] = [];\n const advice: string[] = [];\n for (const diag of result.diagnostics) {\n if (diag.rule !== 'contract/example-missing' || !diag.operation) continue;\n const op = result.contract.operations.find((o) =>\n o.id.endsWith(`/${diag.operation}`),\n );\n if (!op || op.opaque.output) continue;\n const file = path.join(\n examplesDir,\n `${diag.operation.replace('/', '-')}.json`,\n );\n fs.mkdirSync(examplesDir, { recursive: true });\n fs.writeFileSync(\n file,\n `${JSON.stringify(sdk.synthesizeExample(op.output), null, 2)}\\n`,\n );\n wrote.push(path.relative(loaded.projectDir, file));\n advice.push(\n op.effect === 'write'\n ? `${diag.operation}: add ${writeFixturePath(loaded.primary.slug, diag.operation)} with a real-shaped response, then \"ekanos contract --record --op ${diag.operation} --from fixtures\" (writes are never recorded live)`\n : `${diag.operation}: add ${writeFixturePath(loaded.primary.slug, diag.operation)}, or \"ekanos contract --record --op ${diag.operation} --from live\"`,\n );\n }\n if (wrote.length > 0) {\n ctx.log(\n `Wrote ${wrote.length} placeholder example(s) synthesized from the output schema (not recorded from a run): ${wrote.join(', ')}. Fixtures mode will serve these placeholders until you replace them:\\n ${advice.join('\\n ')}`,\n );\n result = analyze();\n }\n }\n\n if (args.bump && previous) {\n const wanted = nextContractVersion(previous, result.contract);\n if (wanted && wanted !== contractVersion) {\n contractVersion = wanted;\n const raw = readJson<Record<string, unknown>>(configPath) ?? {};\n raw.contract = {\n ...(raw.contract as object | undefined),\n version: wanted,\n };\n fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\\n`);\n ctx.log(`contract.version ${declared} -> ${wanted} in ekanos.json`);\n result = analyze();\n }\n }\n\n for (const diag of result.diagnostics) {\n ctx.log(\n `${diag.severity === 'error' ? 'error' : diag.severity === 'warn' ? 'warn ' : 'info '} ${diag.operation ? `${diag.operation}: ` : ''}${diag.message} (${diag.rule})`,\n );\n }\n const errors = result.diagnostics.filter((d) => d.severity === 'error');\n if (errors.length > 0) {\n throw validationError(\n `${errors.length} contract error(s): ${errors\n .slice(0, 3)\n .map((e) => `${e.operation ?? ''} ${e.message}`.trim())\n .join('; ')}`,\n 'Fix them (declare outputs and examples, or run \"ekanos contract --record\"), then re-run.',\n );\n }\n\n const transition = previous\n ? assessContractTransition(previous, result.contract).problems\n : [];\n\n if (args.check) {\n const problems = checkContractFreshness(result.contract, previous, {\n strictPackageVersion: true,\n });\n if (problems.length > 0) {\n throw validationError(\n `contract.json is out of date: ${problems.map(describe).join(' | ')}`,\n 'Run \"ekanos contract\" (add --bump for the version) and commit contract.json.',\n );\n }\n return ctx.succeed(\n {\n ok: true,\n digest: result.contract.digest,\n operations: result.contract.operations.length,\n diagnostics: result.diagnostics,\n },\n `contract.json is current (${result.contract.operations.length} operations).`,\n );\n }\n\n if (transition.length > 0) {\n throw validationError(\n `This change is not allowed on the current contract version: ${transition.map(describe).join(' | ')}`,\n 'Run \"ekanos contract --bump\", or set contract.version in ekanos.json by hand (a breaking change needs a new compat line).',\n );\n }\n fs.writeFileSync(\n contractFile,\n `${JSON.stringify(result.contract, null, 2)}\\n`,\n );\n return ctx.succeed(\n {\n written: 'contract.json',\n digest: result.contract.digest,\n operations: result.contract.operations.length,\n diagnostics: result.diagnostics,\n },\n `Wrote contract.json (${result.contract.operations.length} operations, ${result.contract.compatLine} line, contract ${result.contract.contractVersion}).`,\n );\n}\n\nexport const contractCommand: ExperienceCommand = {\n name: 'contract',\n usage: [\n ' contract Derive ./contract.json from the integration definition;',\n ' --check fails on a stale file (CI, prepack), --record writes',\n ' missing output examples, --bump bumps the contract version',\n ],\n specs: {\n dir: {\n type: 'string',\n description: 'Project directory (default: the current one)',\n },\n check: {\n type: 'boolean',\n description: 'Derive in memory and fail if contract.json differs',\n },\n record: {\n type: 'boolean',\n description:\n 'Write placeholder examples (contracts/examples) for operations that have none; with --op, record real ones from a run',\n },\n op: {\n type: 'string',\n description:\n 'With --record: operation(s) to record from a real run, comma separated (tool/list_posts)',\n },\n from: {\n type: 'string',\n description:\n 'With --op: run mode to record from, fixtures (default) or live',\n },\n input: {\n type: 'string',\n description: 'With --op: JSON input for the recorded run',\n },\n bump: {\n type: 'boolean',\n description:\n 'Rewrite ekanos.json contract.version to match the surface change',\n },\n },\n run(ctx, args) {\n const from = args.flags.from;\n if (from !== undefined && from !== 'live' && from !== 'fixtures') {\n throw usageError(\n `--from must be \"fixtures\" or \"live\", not \"${String(from)}\".`,\n 'Run \"ekanos contract --record --op tool/<name> --from live\".',\n );\n }\n\n return runContract(ctx, {\n dir: args.dir,\n check: args.flags.check === true,\n record: args.flags.record === true,\n bump: args.flags.bump === true,\n env: args.env,\n ...(typeof args.flags.op === 'string' && args.flags.op !== ''\n ? { ops: args.flags.op.split(',').map((o) => o.trim()) }\n : {}),\n ...(args.flags.from === 'live' || args.flags.from === 'fixtures'\n ? { from: args.flags.from }\n : {}),\n ...(typeof args.flags.input === 'string'\n ? { input: args.flags.input }\n : {}),\n });\n },\n};\n\n/** Where a scenario fixture for an operation lives, project-relative. */\nfunction writeFixturePath(slug: string, opKey: string): string {\n return path.join('fixtures', slug, `${opKey.replace('/', '-')}.default.json`);\n}\n"]}
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type SourceCatalog } from '@ekanos/integration-schema';
|
|
1
2
|
import type { CliContext } from '../context.js';
|
|
3
|
+
import { type HarnessResolver } from '../dev-experience.js';
|
|
2
4
|
export interface DevArgs {
|
|
3
5
|
/** Project directory (contains ekanos.json). Defaults to cwd. */
|
|
4
6
|
dir: string;
|
|
@@ -21,6 +23,11 @@ export interface DevArgs {
|
|
|
21
23
|
*/
|
|
22
24
|
start?: boolean;
|
|
23
25
|
env?: Record<string, string | undefined>;
|
|
26
|
+
/** Experience projects: an offline directory of contracts (`--contracts-dir`). */
|
|
27
|
+
contractsDir?: string;
|
|
28
|
+
/** Test seams. */
|
|
29
|
+
loadResolver?: (projectDir: string) => Promise<HarnessResolver>;
|
|
30
|
+
fetchCatalog?: () => Promise<SourceCatalog | null>;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
33
|
* `dev` — scaffold the local harness shell into the partner's project, then
|