@ekanos/cli 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +81 -2
  2. package/dist/bin.js +14 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/dev.d.ts +4 -0
  5. package/dist/commands/dev.js +6 -20
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/init.d.ts +61 -0
  8. package/dist/commands/init.js +145 -18
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/publish.d.ts +13 -0
  11. package/dist/commands/publish.js +38 -15
  12. package/dist/commands/publish.js.map +1 -1
  13. package/dist/commands/status.js +9 -2
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/upgrade.d.ts +47 -0
  16. package/dist/commands/upgrade.js +546 -0
  17. package/dist/commands/upgrade.js.map +1 -0
  18. package/dist/commands/validate.d.ts +5 -0
  19. package/dist/commands/validate.js +20 -6
  20. package/dist/commands/validate.js.map +1 -1
  21. package/dist/context.d.ts +9 -0
  22. package/dist/context.js +9 -0
  23. package/dist/context.js.map +1 -1
  24. package/dist/copy-tree.d.ts +48 -0
  25. package/dist/copy-tree.js +55 -0
  26. package/dist/copy-tree.js.map +1 -0
  27. package/dist/delegate.d.ts +28 -0
  28. package/dist/delegate.js +136 -0
  29. package/dist/delegate.js.map +1 -0
  30. package/dist/harness-scaffold.d.ts +21 -6
  31. package/dist/harness-scaffold.js +8 -5
  32. package/dist/harness-scaffold.js.map +1 -1
  33. package/dist/index.js +40 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/package-manager.d.ts +10 -0
  36. package/dist/package-manager.js +32 -0
  37. package/dist/package-manager.js.map +1 -1
  38. package/dist/parse-argv.d.ts +7 -1
  39. package/dist/parse-argv.js +10 -2
  40. package/dist/parse-argv.js.map +1 -1
  41. package/dist/project-checks.d.ts +9 -0
  42. package/dist/project-checks.js +29 -0
  43. package/dist/project-checks.js.map +1 -1
  44. package/dist/project.d.ts +23 -0
  45. package/dist/project.js +66 -1
  46. package/dist/project.js.map +1 -1
  47. package/dist/publish-api.d.ts +1 -1
  48. package/dist/publish-api.js +2 -2
  49. package/dist/publish-api.js.map +1 -1
  50. package/dist/safe-fs.d.ts +1 -1
  51. package/dist/safe-fs.js.map +1 -1
  52. package/dist/schema-skew.d.ts +77 -0
  53. package/dist/schema-skew.js +163 -0
  54. package/dist/schema-skew.js.map +1 -0
  55. package/dist/sources-api.d.ts +1 -1
  56. package/dist/sources-api.js.map +1 -1
  57. package/dist/toolchain-api.d.ts +54 -0
  58. package/dist/toolchain-api.js +58 -0
  59. package/dist/toolchain-api.js.map +1 -0
  60. package/dist/toolchain-resolve.d.ts +26 -0
  61. package/dist/toolchain-resolve.js +100 -0
  62. package/dist/toolchain-resolve.js.map +1 -0
  63. package/dist/toolchain.d.ts +21 -0
  64. package/dist/toolchain.js +16 -0
  65. package/dist/toolchain.js.map +1 -0
  66. package/dist/update-notice.d.ts +32 -0
  67. package/dist/update-notice.js +180 -0
  68. package/dist/update-notice.js.map +1 -0
  69. package/dist/validate-findings.d.ts +12 -0
  70. package/dist/validate-findings.js +14 -1
  71. package/dist/validate-findings.js.map +1 -1
  72. package/package.json +1 -1
  73. package/templates/AGENTS.md.tmpl +81 -0
  74. package/templates/CLAUDE.md.tmpl +2 -1
  75. package/templates/claude-skill.md.tmpl +20 -2
  76. package/templates/docs/api-notes.md.tmpl +21 -0
  77. package/templates/mcp.json.tmpl +8 -0
  78. package/templates/skills/build-integration/SKILL.md +178 -0
  79. package/templates/skills/build-integration/references/api-docs-research.md +93 -0
  80. package/templates/skills/build-integration/references/api-wiring.md +94 -0
  81. package/templates/skills/build-integration/references/widget-design.md +132 -0
  82. package/templates/skills/polish-widgets/SKILL.md +139 -0
  83. package/templates/skills/polish-widgets/references/patterns.md +269 -0
@@ -0,0 +1,77 @@
1
+ import type { CliContext } from './context.js';
2
+ /**
3
+ * The schema-skew gate: `validate` and `publish` parse a partner's
4
+ * declaration with the REAL zod schemas from `@ekanos/integration-schema`,
5
+ * pinned to the EXACT version this build of `@ekanos/cli` was built against.
6
+ * A partner project can end up with a NEWER schema installed than the one
7
+ * this CLI validates with — `@ekanos/sdk` (or the project directly) got
8
+ * upgraded, the CLI did not — and that state is not merely "a little behind".
9
+ * It is actively WRONG: a field the newer schema added parses here as an
10
+ * `unrecognized_keys` zod issue, and the generic "host-assigned fields are
11
+ * never partner-authorable" hint tells the partner to delete code that is
12
+ * completely correct. That happened to a real partner. Failing loudly here
13
+ * beats answering falsely, so this gate runs BEFORE validate or publish do
14
+ * any of their own work — an incompatible pair produces no findings at all,
15
+ * rather than confidently wrong ones.
16
+ *
17
+ * The opposite skew — the project's schema is OLDER than the CLI's — is not
18
+ * dangerous in the same way (the CLI's validation is still correct; the
19
+ * partner just cannot use fields the CLI would recognize yet), so it warns
20
+ * instead of blocking.
21
+ */
22
+ export declare const INTEGRATION_SCHEMA_PACKAGE = "@ekanos/integration-schema";
23
+ /**
24
+ * The version of `@ekanos/integration-schema` THIS CLI validates with.
25
+ *
26
+ * Two tiers, tried in order:
27
+ *
28
+ * 1. The exact version declared in this CLI's own `package.json`
29
+ * dependencies. In a published build this is a real pinned semver — `pnpm
30
+ * publish` rewrites the monorepo's `workspace:*` protocol to the exact
31
+ * version of the linked package at publish time, which is exactly the
32
+ * "declare it explicitly" contract `compatibility.ts` already commits to
33
+ * for the shell contract.
34
+ * 2. Failing that (this repo's own dev checkout, where the dependency is
35
+ * still the literal `workspace:*` string), fall back to whatever
36
+ * `@ekanos/integration-schema` this CLI itself resolves on disk — the
37
+ * package a workspace install actually links next to the CLI.
38
+ *
39
+ * Returns null — never throws — when neither resolves, which degrades the
40
+ * gate to a no-op exactly like "the project has no node_modules" does. A
41
+ * partner's validate/publish must never fail because THIS lookup couldn't
42
+ * find an answer.
43
+ */
44
+ export declare function bundledSchemaVersion(): string | null;
45
+ export interface SchemaSkewResult {
46
+ status: 'ok' | 'project-newer' | 'project-older' | 'unresolvable';
47
+ cliSchemaVersion: string | null;
48
+ projectSchemaVersion: string | null;
49
+ }
50
+ /**
51
+ * What the PROJECT resolves for `@ekanos/integration-schema`, compared
52
+ * against what this CLI bundles.
53
+ *
54
+ * The project rarely depends on the schema package directly — it is a
55
+ * transitive dependency of `@ekanos/sdk` — so two candidates are considered,
56
+ * per the task: a direct top-level resolution (works when a package manager
57
+ * hoists it, or when a partner added it explicitly) AND the version
58
+ * `@ekanos/sdk` itself declares a dependency on (its package.json is always a
59
+ * direct project dependency, so it always resolves at the top level even when
60
+ * the schema package's own copy does not). The greater of whichever resolves
61
+ * is what "the project's installed schema" means here — either could be the
62
+ * copy Node actually loads, and closing the gate on the newer of the two is
63
+ * the safe direction: it only ever asks a partner to upgrade a CLI that is
64
+ * genuinely behind.
65
+ */
66
+ export declare function detectSchemaSkew(projectDir: string): SchemaSkewResult;
67
+ /**
68
+ * The gate itself. Call before `validate`/`publish` do anything else.
69
+ *
70
+ * Throws PRECONDITION_FAILED (never a validation finding — the definition
71
+ * cannot even be trusted to parse correctly in this state) when the project's
72
+ * schema is newer. Warns on stderr and returns normally for every other
73
+ * status, including the two that mean "nothing to say" (`ok`, `unresolvable`)
74
+ * so callers can uniformly branch on the returned result to decide whether to
75
+ * annotate their own JSON envelope.
76
+ */
77
+ export declare function assertSchemaNotSkewed(ctx: CliContext, projectDir: string): SchemaSkewResult;
@@ -0,0 +1,163 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { compareVersions } from './compatibility.js';
4
+ import { preconditionError } from './errors.js';
5
+ import { readInstalledPackageManifest } from './harness-scaffold.js';
6
+ import { templatesDir } from './templates.js';
7
+ /**
8
+ * The schema-skew gate: `validate` and `publish` parse a partner's
9
+ * declaration with the REAL zod schemas from `@ekanos/integration-schema`,
10
+ * pinned to the EXACT version this build of `@ekanos/cli` was built against.
11
+ * A partner project can end up with a NEWER schema installed than the one
12
+ * this CLI validates with — `@ekanos/sdk` (or the project directly) got
13
+ * upgraded, the CLI did not — and that state is not merely "a little behind".
14
+ * It is actively WRONG: a field the newer schema added parses here as an
15
+ * `unrecognized_keys` zod issue, and the generic "host-assigned fields are
16
+ * never partner-authorable" hint tells the partner to delete code that is
17
+ * completely correct. That happened to a real partner. Failing loudly here
18
+ * beats answering falsely, so this gate runs BEFORE validate or publish do
19
+ * any of their own work — an incompatible pair produces no findings at all,
20
+ * rather than confidently wrong ones.
21
+ *
22
+ * The opposite skew — the project's schema is OLDER than the CLI's — is not
23
+ * dangerous in the same way (the CLI's validation is still correct; the
24
+ * partner just cannot use fields the CLI would recognize yet), so it warns
25
+ * instead of blocking.
26
+ */
27
+ export const INTEGRATION_SCHEMA_PACKAGE = '@ekanos/integration-schema';
28
+ const SDK_PACKAGE = '@ekanos/sdk';
29
+ const SEMVER_LIKE = /^\d+\.\d+\.\d+/;
30
+ function looksLikeSemver(value) {
31
+ return typeof value === 'string' && SEMVER_LIKE.test(value);
32
+ }
33
+ function readJsonFile(filePath) {
34
+ try {
35
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ }
37
+ catch (_a) {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * The version of `@ekanos/integration-schema` THIS CLI validates with.
43
+ *
44
+ * Two tiers, tried in order:
45
+ *
46
+ * 1. The exact version declared in this CLI's own `package.json`
47
+ * dependencies. In a published build this is a real pinned semver — `pnpm
48
+ * publish` rewrites the monorepo's `workspace:*` protocol to the exact
49
+ * version of the linked package at publish time, which is exactly the
50
+ * "declare it explicitly" contract `compatibility.ts` already commits to
51
+ * for the shell contract.
52
+ * 2. Failing that (this repo's own dev checkout, where the dependency is
53
+ * still the literal `workspace:*` string), fall back to whatever
54
+ * `@ekanos/integration-schema` this CLI itself resolves on disk — the
55
+ * package a workspace install actually links next to the CLI.
56
+ *
57
+ * Returns null — never throws — when neither resolves, which degrades the
58
+ * gate to a no-op exactly like "the project has no node_modules" does. A
59
+ * partner's validate/publish must never fail because THIS lookup couldn't
60
+ * find an answer.
61
+ */
62
+ export function bundledSchemaVersion() {
63
+ var _a;
64
+ const cliRoot = path.join(templatesDir(), '..');
65
+ const ownManifest = readJsonFile(path.join(cliRoot, 'package.json'));
66
+ const declared = typeof ownManifest === 'object' && ownManifest !== null
67
+ ? ((_a = ownManifest
68
+ .dependencies) !== null && _a !== void 0 ? _a : {})[INTEGRATION_SCHEMA_PACKAGE]
69
+ : undefined;
70
+ if (looksLikeSemver(declared))
71
+ return declared;
72
+ const nested = readJsonFile(path.join(cliRoot, 'node_modules', '@ekanos', 'integration-schema', 'package.json'));
73
+ const nestedVersion = typeof nested === 'object' && nested !== null
74
+ ? nested.version
75
+ : undefined;
76
+ return looksLikeSemver(nestedVersion) ? nestedVersion : null;
77
+ }
78
+ /**
79
+ * What the PROJECT resolves for `@ekanos/integration-schema`, compared
80
+ * against what this CLI bundles.
81
+ *
82
+ * The project rarely depends on the schema package directly — it is a
83
+ * transitive dependency of `@ekanos/sdk` — so two candidates are considered,
84
+ * per the task: a direct top-level resolution (works when a package manager
85
+ * hoists it, or when a partner added it explicitly) AND the version
86
+ * `@ekanos/sdk` itself declares a dependency on (its package.json is always a
87
+ * direct project dependency, so it always resolves at the top level even when
88
+ * the schema package's own copy does not). The greater of whichever resolves
89
+ * is what "the project's installed schema" means here — either could be the
90
+ * copy Node actually loads, and closing the gate on the newer of the two is
91
+ * the safe direction: it only ever asks a partner to upgrade a CLI that is
92
+ * genuinely behind.
93
+ */
94
+ export function detectSchemaSkew(projectDir) {
95
+ var _a;
96
+ const cliSchemaVersion = bundledSchemaVersion();
97
+ if (!cliSchemaVersion) {
98
+ return {
99
+ status: 'unresolvable',
100
+ cliSchemaVersion: null,
101
+ projectSchemaVersion: null,
102
+ };
103
+ }
104
+ const direct = readInstalledPackageManifest(projectDir, INTEGRATION_SCHEMA_PACKAGE);
105
+ const sdk = readInstalledPackageManifest(projectDir, SDK_PACKAGE);
106
+ const sdkDeclaredSchema = sdk && typeof sdk.manifest === 'object' && sdk.manifest !== null
107
+ ? ((_a = sdk.manifest
108
+ .dependencies) !== null && _a !== void 0 ? _a : {})[INTEGRATION_SCHEMA_PACKAGE]
109
+ : undefined;
110
+ const candidates = [direct === null || direct === void 0 ? void 0 : direct.version, sdkDeclaredSchema].filter(looksLikeSemver);
111
+ if (candidates.length === 0) {
112
+ return {
113
+ status: 'unresolvable',
114
+ cliSchemaVersion,
115
+ projectSchemaVersion: null,
116
+ };
117
+ }
118
+ const projectSchemaVersion = candidates.reduce((max, candidate) => compareVersions(candidate, max) > 0 ? candidate : max);
119
+ const cmp = compareVersions(projectSchemaVersion, cliSchemaVersion);
120
+ return {
121
+ status: cmp > 0 ? 'project-newer' : cmp < 0 ? 'project-older' : 'ok',
122
+ cliSchemaVersion,
123
+ projectSchemaVersion,
124
+ };
125
+ }
126
+ /**
127
+ * The gate itself. Call before `validate`/`publish` do anything else.
128
+ *
129
+ * Throws PRECONDITION_FAILED (never a validation finding — the definition
130
+ * cannot even be trusted to parse correctly in this state) when the project's
131
+ * schema is newer. Warns on stderr and returns normally for every other
132
+ * status, including the two that mean "nothing to say" (`ok`, `unresolvable`)
133
+ * so callers can uniformly branch on the returned result to decide whether to
134
+ * annotate their own JSON envelope.
135
+ */
136
+ export function assertSchemaNotSkewed(ctx, projectDir) {
137
+ const result = detectSchemaSkew(projectDir);
138
+ if (result.status === 'project-newer') {
139
+ throw preconditionError(`The project's ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +
140
+ `is newer than ${result.cliSchemaVersion}, the version this ` +
141
+ `@ekanos/cli build validates with. Findings from "ekanos validate" ` +
142
+ 'are UNRELIABLE in this state: a field the newer schema added ' +
143
+ 'parses here as an unrecognized key, and the usual hint for that ' +
144
+ 'tells you to delete code that is actually correct.',
145
+ // NEVER interpolate the schema version into a "@ekanos/cli@<version>"
146
+ // install spec — the two version completely independently (this build
147
+ // is cli 0.1.6 validating against schema 0.1.4). "ekanos upgrade"
148
+ // resolves the actual coherent set; the manual fallback can only ever
149
+ // be "latest", never a computed number.
150
+ 'Upgrade the CLI: run "ekanos upgrade" (it resolves a coherent target ' +
151
+ 'version set), or pin it explicitly by hand with ' +
152
+ '"npm i -D @ekanos/cli@latest". Then re-run "ekanos validate" or ' +
153
+ '"ekanos publish".');
154
+ }
155
+ if (result.status === 'project-older') {
156
+ ctx.warn(`ekanos: the installed ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +
157
+ `is older than ${result.cliSchemaVersion}, the version this CLI ` +
158
+ 'validates with — some fields this CLI recognizes may not be usable ' +
159
+ 'yet. Run "ekanos upgrade" to bring the toolchain in sync.');
160
+ }
161
+ return result;
162
+ }
163
+ //# sourceMappingURL=schema-skew.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-skew.js","sourceRoot":"","sources":["../src/schema-skew.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,CAAC,MAAM,0BAA0B,GAAG,4BAA4B,CAAC;AACvE,MAAM,WAAW,GAAG,aAAa,CAAC;AAElC,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACpC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,oBAAoB;;IAClC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,CAAC,CAAC;IAEhD,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;IACrE,MAAM,QAAQ,GACZ,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,KAAK,IAAI;QACrD,CAAC,CAAC,CAAC,MAAC,WAA0D;aACzD,YAAY,mCAAI,EAAE,CAAC,CAAC,0BAA0B,CAAC;QACpD,CAAC,CAAC,SAAS,CAAC;IAChB,IAAI,eAAe,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE/C,MAAM,MAAM,GAAG,YAAY,CACzB,IAAI,CAAC,IAAI,CACP,OAAO,EACP,cAAc,EACd,SAAS,EACT,oBAAoB,EACpB,cAAc,CACf,CACF,CAAC;IACF,MAAM,aAAa,GACjB,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAC3C,CAAC,CAAE,MAAgC,CAAC,OAAO;QAC3C,CAAC,CAAC,SAAS,CAAC;IAEhB,OAAO,eAAe,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAQD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB;;IACjD,MAAM,gBAAgB,GAAG,oBAAoB,EAAE,CAAC;IAChD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,OAAO;YACL,MAAM,EAAE,cAAc;YACtB,gBAAgB,EAAE,IAAI;YACtB,oBAAoB,EAAE,IAAI;SAC3B,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,4BAA4B,CACzC,UAAU,EACV,0BAA0B,CAC3B,CAAC;IACF,MAAM,GAAG,GAAG,4BAA4B,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAClE,MAAM,iBAAiB,GACrB,GAAG,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI;QAC9D,CAAC,CAAC,CAAC,MAAC,GAAG,CAAC,QAAuD;aAC1D,YAAY,mCAAI,EAAE,CAAC,CAAC,0BAA0B,CAAC;QACpD,CAAC,CAAC,SAAS,CAAC;IAEhB,MAAM,UAAU,GAAG,CAAC,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,MAAM,CAC5D,eAAe,CAChB,CAAC;IAEF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,MAAM,EAAE,cAAc;YACtB,gBAAgB;YAChB,oBAAoB,EAAE,IAAI;SAC3B,CAAC;IACJ,CAAC;IAED,MAAM,oBAAoB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,CAChE,eAAe,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CACtD,CAAC;IAEF,MAAM,GAAG,GAAG,eAAe,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAAC;IAEpE,OAAO;QACL,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI;QACpE,gBAAgB;QAChB,oBAAoB;KACrB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAe,EACf,UAAkB;IAElB,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAE5C,IAAI,MAAM,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;QACtC,MAAM,iBAAiB,CACrB,iBAAiB,0BAA0B,IAAI,MAAM,CAAC,oBAAoB,GAAG;YAC3E,iBAAiB,MAAM,CAAC,gBAAgB,qBAAqB;YAC7D,oEAAoE;YACpE,+DAA+D;YAC/D,kEAAkE;YAClE,oDAAoD;QACtD,sEAAsE;QACtE,sEAAsE;QACtE,kEAAkE;QAClE,sEAAsE;QACtE,wCAAwC;QACxC,uEAAuE;YACrE,kDAAkD;YAClD,kEAAkE;YAClE,mBAAmB,CACtB,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;QACtC,GAAG,CAAC,IAAI,CACN,yBAAyB,0BAA0B,IAAI,MAAM,CAAC,oBAAoB,GAAG;YACnF,iBAAiB,MAAM,CAAC,gBAAgB,yBAAyB;YACjE,qEAAqE;YACrE,2DAA2D,CAC9D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { compareVersions } from './compatibility';\nimport type { CliContext } from './context';\nimport { preconditionError } from './errors';\nimport { readInstalledPackageManifest } from './harness-scaffold';\nimport { templatesDir } from './templates';\n\n/**\n * The schema-skew gate: `validate` and `publish` parse a partner's\n * declaration with the REAL zod schemas from `@ekanos/integration-schema`,\n * pinned to the EXACT version this build of `@ekanos/cli` was built against.\n * A partner project can end up with a NEWER schema installed than the one\n * this CLI validates with — `@ekanos/sdk` (or the project directly) got\n * upgraded, the CLI did not — and that state is not merely \"a little behind\".\n * It is actively WRONG: a field the newer schema added parses here as an\n * `unrecognized_keys` zod issue, and the generic \"host-assigned fields are\n * never partner-authorable\" hint tells the partner to delete code that is\n * completely correct. That happened to a real partner. Failing loudly here\n * beats answering falsely, so this gate runs BEFORE validate or publish do\n * any of their own work — an incompatible pair produces no findings at all,\n * rather than confidently wrong ones.\n *\n * The opposite skew — the project's schema is OLDER than the CLI's — is not\n * dangerous in the same way (the CLI's validation is still correct; the\n * partner just cannot use fields the CLI would recognize yet), so it warns\n * instead of blocking.\n */\n\nexport const INTEGRATION_SCHEMA_PACKAGE = '@ekanos/integration-schema';\nconst SDK_PACKAGE = '@ekanos/sdk';\n\nconst SEMVER_LIKE = /^\\d+\\.\\d+\\.\\d+/;\n\nfunction looksLikeSemver(value: unknown): value is string {\n return typeof value === 'string' && SEMVER_LIKE.test(value);\n}\n\nfunction readJsonFile(filePath: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * The version of `@ekanos/integration-schema` THIS CLI validates with.\n *\n * Two tiers, tried in order:\n *\n * 1. The exact version declared in this CLI's own `package.json`\n * dependencies. In a published build this is a real pinned semver — `pnpm\n * publish` rewrites the monorepo's `workspace:*` protocol to the exact\n * version of the linked package at publish time, which is exactly the\n * \"declare it explicitly\" contract `compatibility.ts` already commits to\n * for the shell contract.\n * 2. Failing that (this repo's own dev checkout, where the dependency is\n * still the literal `workspace:*` string), fall back to whatever\n * `@ekanos/integration-schema` this CLI itself resolves on disk — the\n * package a workspace install actually links next to the CLI.\n *\n * Returns null — never throws — when neither resolves, which degrades the\n * gate to a no-op exactly like \"the project has no node_modules\" does. A\n * partner's validate/publish must never fail because THIS lookup couldn't\n * find an answer.\n */\nexport function bundledSchemaVersion(): string | null {\n const cliRoot = path.join(templatesDir(), '..');\n\n const ownManifest = readJsonFile(path.join(cliRoot, 'package.json'));\n const declared =\n typeof ownManifest === 'object' && ownManifest !== null\n ? ((ownManifest as { dependencies?: Record<string, unknown> })\n .dependencies ?? {})[INTEGRATION_SCHEMA_PACKAGE]\n : undefined;\n if (looksLikeSemver(declared)) return declared;\n\n const nested = readJsonFile(\n path.join(\n cliRoot,\n 'node_modules',\n '@ekanos',\n 'integration-schema',\n 'package.json',\n ),\n );\n const nestedVersion =\n typeof nested === 'object' && nested !== null\n ? (nested as { version?: unknown }).version\n : undefined;\n\n return looksLikeSemver(nestedVersion) ? nestedVersion : null;\n}\n\nexport interface SchemaSkewResult {\n status: 'ok' | 'project-newer' | 'project-older' | 'unresolvable';\n cliSchemaVersion: string | null;\n projectSchemaVersion: string | null;\n}\n\n/**\n * What the PROJECT resolves for `@ekanos/integration-schema`, compared\n * against what this CLI bundles.\n *\n * The project rarely depends on the schema package directly — it is a\n * transitive dependency of `@ekanos/sdk` — so two candidates are considered,\n * per the task: a direct top-level resolution (works when a package manager\n * hoists it, or when a partner added it explicitly) AND the version\n * `@ekanos/sdk` itself declares a dependency on (its package.json is always a\n * direct project dependency, so it always resolves at the top level even when\n * the schema package's own copy does not). The greater of whichever resolves\n * is what \"the project's installed schema\" means here — either could be the\n * copy Node actually loads, and closing the gate on the newer of the two is\n * the safe direction: it only ever asks a partner to upgrade a CLI that is\n * genuinely behind.\n */\nexport function detectSchemaSkew(projectDir: string): SchemaSkewResult {\n const cliSchemaVersion = bundledSchemaVersion();\n if (!cliSchemaVersion) {\n return {\n status: 'unresolvable',\n cliSchemaVersion: null,\n projectSchemaVersion: null,\n };\n }\n\n const direct = readInstalledPackageManifest(\n projectDir,\n INTEGRATION_SCHEMA_PACKAGE,\n );\n const sdk = readInstalledPackageManifest(projectDir, SDK_PACKAGE);\n const sdkDeclaredSchema =\n sdk && typeof sdk.manifest === 'object' && sdk.manifest !== null\n ? ((sdk.manifest as { dependencies?: Record<string, unknown> })\n .dependencies ?? {})[INTEGRATION_SCHEMA_PACKAGE]\n : undefined;\n\n const candidates = [direct?.version, sdkDeclaredSchema].filter(\n looksLikeSemver,\n );\n\n if (candidates.length === 0) {\n return {\n status: 'unresolvable',\n cliSchemaVersion,\n projectSchemaVersion: null,\n };\n }\n\n const projectSchemaVersion = candidates.reduce((max, candidate) =>\n compareVersions(candidate, max) > 0 ? candidate : max,\n );\n\n const cmp = compareVersions(projectSchemaVersion, cliSchemaVersion);\n\n return {\n status: cmp > 0 ? 'project-newer' : cmp < 0 ? 'project-older' : 'ok',\n cliSchemaVersion,\n projectSchemaVersion,\n };\n}\n\n/**\n * The gate itself. Call before `validate`/`publish` do anything else.\n *\n * Throws PRECONDITION_FAILED (never a validation finding — the definition\n * cannot even be trusted to parse correctly in this state) when the project's\n * schema is newer. Warns on stderr and returns normally for every other\n * status, including the two that mean \"nothing to say\" (`ok`, `unresolvable`)\n * so callers can uniformly branch on the returned result to decide whether to\n * annotate their own JSON envelope.\n */\nexport function assertSchemaNotSkewed(\n ctx: CliContext,\n projectDir: string,\n): SchemaSkewResult {\n const result = detectSchemaSkew(projectDir);\n\n if (result.status === 'project-newer') {\n throw preconditionError(\n `The project's ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +\n `is newer than ${result.cliSchemaVersion}, the version this ` +\n `@ekanos/cli build validates with. Findings from \"ekanos validate\" ` +\n 'are UNRELIABLE in this state: a field the newer schema added ' +\n 'parses here as an unrecognized key, and the usual hint for that ' +\n 'tells you to delete code that is actually correct.',\n // NEVER interpolate the schema version into a \"@ekanos/cli@<version>\"\n // install spec — the two version completely independently (this build\n // is cli 0.1.6 validating against schema 0.1.4). \"ekanos upgrade\"\n // resolves the actual coherent set; the manual fallback can only ever\n // be \"latest\", never a computed number.\n 'Upgrade the CLI: run \"ekanos upgrade\" (it resolves a coherent target ' +\n 'version set), or pin it explicitly by hand with ' +\n '\"npm i -D @ekanos/cli@latest\". Then re-run \"ekanos validate\" or ' +\n '\"ekanos publish\".',\n );\n }\n\n if (result.status === 'project-older') {\n ctx.warn(\n `ekanos: the installed ${INTEGRATION_SCHEMA_PACKAGE}@${result.projectSchemaVersion} ` +\n `is older than ${result.cliSchemaVersion}, the version this CLI ` +\n 'validates with — some fields this CLI recognizes may not be usable ' +\n 'yet. Run \"ekanos upgrade\" to bring the toolchain in sync.',\n );\n }\n\n return result;\n}\n"]}
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * GET /api/partner/sources [Authorization: Bearer <access_token>]
12
12
  * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }
13
- * roles is a non-empty array of "dev" | "admin", sorted; sources
13
+ * roles is a non-empty array of "developer" | "source_admin", sorted; sources
14
14
  * is sorted by slug. Empty array means no seats anywhere.
15
15
  * → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
16
16
  * refresh once and retry
@@ -1 +1 @@
1
- {"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAqC3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAY,EACZ,WAAmB;;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAE9C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,MAAA,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mCACb,GAAG,IAAI,yDAAyD,EAClE,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,CAChB,GAAG,IAAI,kCAAkC,QAAQ,CAAC,MAAM,GAAG,EAC3D,QAAQ,CAAC,MAAM,IAAI,GAAG;YACpB,CAAC,CAAC,mEAAmE;gBACjE,8CAA8C;YAClD,CAAC,CAAC,+DAA+D;gBAC7D,qCAAqC,CAC5C,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAA,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAC1D,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAE7D,MAAM,OAAO,GAAG,IAAI;SACjB,MAAM,CACL,CAAC,GAAG,EAAkC,EAAE,CACtC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAC1C;SACA,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;;QAAC,OAAA,CAAC;YACb,EAAE,EAAE,MAAA,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,mCAAI,EAAE;YACrB,IAAI,EAAE,MAAA,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,EAAE;YACzB,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;gBAC7B,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC;gBACtE,CAAC,CAAC,EAAE;SACP,CAAC,CAAA;KAAA,CAAC,CAAC;IAEN,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,4BAA4B,CAAC,IAAY;IACvD,MAAM,iBAAiB,CACrB,mBAAmB,IAAI,8CAA8C,EACrE,4BAA4B,IAAI,cAAc,CAC/C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,QAAkB;IAElB,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE9C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAClD,CAAC,CAAE,MAAkC;YACrC,CAAC,CAAC,EAAE,CAAC;IACT,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAc;IAChC,OAAO,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC;AACvC,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC","sourcesContent":["import { request } from './auth/fusion-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Fusion source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As\n * with the auth routes and `publish-api.ts`, the two halves are coupled only\n * by HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"dev\" | \"admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the source seat lookup.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
1
+ {"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Fusion source-seat visibility route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As\n * with the auth routes and `publish-api.ts`, the two halves are coupled only\n * by HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"developer\" | \"source_admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the source seat lookup.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import { type ToolchainNote, type ToolchainVersions } from './toolchain.js';
2
+ /**
3
+ * The CLI's client for the Fusion toolchain-advisory route.
4
+ *
5
+ * ---------------------------------------------------------------------------
6
+ * WIRE CONTRACT — the seam between this package and the Fusion web app.
7
+ *
8
+ * The server side lives in `apps/web/app/api/partner/toolchain/route.ts` and
9
+ * ships on a DIFFERENT BRANCH from this file, same as every other route in
10
+ * `auth/fusion-api.ts` and `publish-api.ts`. Nothing here imports from
11
+ * `apps/web`; the two halves are coupled only by HTTP, and the tests stub
12
+ * `fetch`, never the route.
13
+ *
14
+ * GET /api/partner/toolchain [Authorization: Bearer <access_token>]
15
+ * → 200 { ok: true, data: {
16
+ * current: { cli, sdk, ui, integrationSchema, harness }, // the
17
+ * // coherent
18
+ * // set the
19
+ * // host
20
+ * // recommends
21
+ * minimum: { cli, sdk, ui, integrationSchema, harness }, // below
22
+ * // this the
23
+ * // host
24
+ * // refuses
25
+ * notes?: [{ level: "warn" | "info", message: string }],
26
+ * } }
27
+ * → any other status, or a body that fails to parse or match this shape
28
+ *
29
+ * This endpoint is treated as ADVISORY, not authoritative in the way the auth
30
+ * and submission routes are: a 404 (an older Fusion deployment that predates
31
+ * this route) or a 501 (deliberately unimplemented) are both entirely
32
+ * ordinary, and a 4xx/5xx/network failure/malformed body all mean exactly the
33
+ * same thing to every caller — "the endpoint is unavailable, fall back to the
34
+ * npm registry" — so this module never throws. There is no not-configured
35
+ * distinction to make here the way `cli-config` has: a caller with no way to
36
+ * reach this route at all (no host, no session) never calls it in the first
37
+ * place; see `toolchain-resolve.ts`.
38
+ *
39
+ * Unknown extra fields in the response are tolerated (only the fields above
40
+ * are read); a missing `notes` defaults to an empty array.
41
+ * ---------------------------------------------------------------------------
42
+ */
43
+ export interface ToolchainReport {
44
+ current: ToolchainVersions;
45
+ minimum: ToolchainVersions;
46
+ notes: ToolchainNote[];
47
+ }
48
+ export type FetchToolchainResult = {
49
+ status: 'ok';
50
+ report: ToolchainReport;
51
+ } | {
52
+ status: 'unavailable';
53
+ };
54
+ export declare function fetchToolchainReport(host: string, accessToken: string): Promise<FetchToolchainResult>;
@@ -0,0 +1,58 @@
1
+ import { request } from './auth/fusion-api.js';
2
+ import { TOOLCHAIN_PACKAGES, } from './toolchain.js';
3
+ export async function fetchToolchainReport(host, accessToken) {
4
+ try {
5
+ const response = await request(`${host}/api/partner/toolchain`, {
6
+ method: 'GET',
7
+ headers: {
8
+ accept: 'application/json',
9
+ authorization: `Bearer ${accessToken}`,
10
+ },
11
+ });
12
+ if (!response.ok)
13
+ return { status: 'unavailable' };
14
+ const body = await response.json();
15
+ const report = parseReport(body);
16
+ return report ? { status: 'ok', report } : { status: 'unavailable' };
17
+ }
18
+ catch (_a) {
19
+ // Network failure, timeout, or a body that is not even JSON — all read
20
+ // the same: the endpoint is unavailable this time, fall back to npm.
21
+ return { status: 'unavailable' };
22
+ }
23
+ }
24
+ function parseReport(body) {
25
+ if (typeof body !== 'object' || body === null)
26
+ return null;
27
+ const data = body.data;
28
+ if (typeof data !== 'object' || data === null)
29
+ return null;
30
+ const current = parseVersions(data.current);
31
+ const minimum = parseVersions(data.minimum);
32
+ if (!current || !minimum)
33
+ return null;
34
+ const rawNotes = data.notes;
35
+ const notes = Array.isArray(rawNotes) ? rawNotes.filter(isNote) : [];
36
+ return { current, minimum, notes };
37
+ }
38
+ function parseVersions(value) {
39
+ if (typeof value !== 'object' || value === null)
40
+ return null;
41
+ const record = value;
42
+ const result = {};
43
+ for (const key of Object.keys(TOOLCHAIN_PACKAGES)) {
44
+ const raw = record[key];
45
+ if (typeof raw !== 'string' || raw.length === 0)
46
+ return null;
47
+ result[key] = raw;
48
+ }
49
+ return result;
50
+ }
51
+ function isNote(value) {
52
+ if (typeof value !== 'object' || value === null)
53
+ return false;
54
+ const note = value;
55
+ return ((note.level === 'warn' || note.level === 'info') &&
56
+ typeof note.message === 'string');
57
+ }
58
+ //# sourceMappingURL=toolchain-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolchain-api.js","sourceRoot":"","sources":["../src/toolchain-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n TOOLCHAIN_PACKAGES,\n type ToolchainNote,\n type ToolchainVersions,\n} from './toolchain';\n\n/**\n * The CLI's client for the Fusion toolchain-advisory route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/toolchain/route.ts` and\n * ships on a DIFFERENT BRANCH from this file, same as every other route in\n * `auth/fusion-api.ts` and `publish-api.ts`. Nothing here imports from\n * `apps/web`; the two halves are coupled only by HTTP, and the tests stub\n * `fetch`, never the route.\n *\n * GET /api/partner/toolchain [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: {\n * current: { cli, sdk, ui, integrationSchema, harness }, // the\n * // coherent\n * // set the\n * // host\n * // recommends\n * minimum: { cli, sdk, ui, integrationSchema, harness }, // below\n * // this the\n * // host\n * // refuses\n * notes?: [{ level: \"warn\" | \"info\", message: string }],\n * } }\n * → any other status, or a body that fails to parse or match this shape\n *\n * This endpoint is treated as ADVISORY, not authoritative in the way the auth\n * and submission routes are: a 404 (an older Fusion deployment that predates\n * this route) or a 501 (deliberately unimplemented) are both entirely\n * ordinary, and a 4xx/5xx/network failure/malformed body all mean exactly the\n * same thing to every caller — \"the endpoint is unavailable, fall back to the\n * npm registry\" — so this module never throws. There is no not-configured\n * distinction to make here the way `cli-config` has: a caller with no way to\n * reach this route at all (no host, no session) never calls it in the first\n * place; see `toolchain-resolve.ts`.\n *\n * Unknown extra fields in the response are tolerated (only the fields above\n * are read); a missing `notes` defaults to an empty array.\n * ---------------------------------------------------------------------------\n */\n\nexport interface ToolchainReport {\n current: ToolchainVersions;\n minimum: ToolchainVersions;\n notes: ToolchainNote[];\n}\n\nexport type FetchToolchainResult =\n { status: 'ok'; report: ToolchainReport } | { status: 'unavailable' };\n\nexport async function fetchToolchainReport(\n host: string,\n accessToken: string,\n): Promise<FetchToolchainResult> {\n try {\n const response = await request(`${host}/api/partner/toolchain`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (!response.ok) return { status: 'unavailable' };\n\n const body: unknown = await response.json();\n const report = parseReport(body);\n\n return report ? { status: 'ok', report } : { status: 'unavailable' };\n } catch {\n // Network failure, timeout, or a body that is not even JSON — all read\n // the same: the endpoint is unavailable this time, fall back to npm.\n return { status: 'unavailable' };\n }\n}\n\nfunction parseReport(body: unknown): ToolchainReport | null {\n if (typeof body !== 'object' || body === null) return null;\n const data = (body as { data?: unknown }).data;\n if (typeof data !== 'object' || data === null) return null;\n\n const current = parseVersions((data as { current?: unknown }).current);\n const minimum = parseVersions((data as { minimum?: unknown }).minimum);\n if (!current || !minimum) return null;\n\n const rawNotes = (data as { notes?: unknown }).notes;\n const notes = Array.isArray(rawNotes) ? rawNotes.filter(isNote) : [];\n\n return { current, minimum, notes };\n}\n\nfunction parseVersions(value: unknown): ToolchainVersions | null {\n if (typeof value !== 'object' || value === null) return null;\n const record = value as Record<string, unknown>;\n const result: Partial<ToolchainVersions> = {};\n\n for (const key of Object.keys(\n TOOLCHAIN_PACKAGES,\n ) as (keyof ToolchainVersions)[]) {\n const raw = record[key];\n if (typeof raw !== 'string' || raw.length === 0) return null;\n result[key] = raw;\n }\n\n return result as ToolchainVersions;\n}\n\nfunction isNote(value: unknown): value is ToolchainNote {\n if (typeof value !== 'object' || value === null) return false;\n const note = value as { level?: unknown; message?: unknown };\n return (\n (note.level === 'warn' || note.level === 'info') &&\n typeof note.message === 'string'\n );\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import { type ToolchainNote, type ToolchainVersions } from './toolchain.js';
2
+ export type ToolchainResolution = {
3
+ status: 'ok';
4
+ source: 'host';
5
+ target: ToolchainVersions;
6
+ minimum: ToolchainVersions;
7
+ notes: ToolchainNote[];
8
+ } | {
9
+ status: 'ok';
10
+ source: 'npm';
11
+ target: ToolchainVersions;
12
+ } | {
13
+ status: 'incoherent';
14
+ detail: string;
15
+ } | {
16
+ status: 'unavailable';
17
+ };
18
+ export interface ResolveToolchainParams {
19
+ host?: string;
20
+ session?: {
21
+ accessToken: string;
22
+ } | null;
23
+ /** Per-request timeout for the underlying fetches. */
24
+ timeoutMs?: number;
25
+ }
26
+ export declare function resolveToolchainTarget(params: ResolveToolchainParams): Promise<ToolchainResolution>;
@@ -0,0 +1,100 @@
1
+ import { TOOLCHAIN_PACKAGES, } from './toolchain.js';
2
+ import { fetchToolchainReport } from './toolchain-api.js';
3
+ /**
4
+ * The single place that answers "what should this toolchain be at?" — shared
5
+ * by `ekanos upgrade` (which installs the answer) and the proactive update
6
+ * notice (which only reports it). Both need the exact same coherent target,
7
+ * so there is exactly one resolution path, tried in this order:
8
+ *
9
+ * 1. `GET /api/partner/toolchain` on the resolved host, when both a host and
10
+ * a session are available. Whatever the host returns is trusted as
11
+ * already-coherent — it is the operator's own recommendation.
12
+ * 2. The npm registry's `dist-tags.latest` for all five packages, with an
13
+ * explicit COHERENCE check before ever calling it a "target": the
14
+ * `@ekanos/sdk` / `@ekanos/ui` / `@ekanos/integration-schema` trio must
15
+ * resolve to one identical version, and the `@ekanos/sdk` release at that
16
+ * version must declare the exact same `@ekanos/integration-schema`
17
+ * dependency. A multi-package publish is not atomic — this repo hit that
18
+ * exact window mid-publish — so assembling "latest of each, independently"
19
+ * can produce a set that literally cannot install together. Refusing is
20
+ * the only safe response to a set caught in that state; the failure is
21
+ * obvious and self-resolves the moment the publish finishes.
22
+ */
23
+ const REGISTRY_BASE = 'https://registry.npmjs.org';
24
+ /** npm registry package URLs encode the scope's `/` but not its `@`. */
25
+ function registryPackageUrl(pkg, version) {
26
+ const encoded = encodeURIComponent(pkg).replace('%40', '@');
27
+ return version
28
+ ? `${REGISTRY_BASE}/${encoded}/${encodeURIComponent(version)}`
29
+ : `${REGISTRY_BASE}/${encoded}`;
30
+ }
31
+ export async function resolveToolchainTarget(params) {
32
+ if (params.host && params.session) {
33
+ const hostResult = await fetchToolchainReport(params.host, params.session.accessToken);
34
+ if (hostResult.status === 'ok') {
35
+ return {
36
+ status: 'ok',
37
+ source: 'host',
38
+ target: hostResult.report.current,
39
+ minimum: hostResult.report.minimum,
40
+ notes: hostResult.report.notes,
41
+ };
42
+ }
43
+ // 404 / 501 / network failure / malformed body: fall through to npm.
44
+ }
45
+ return resolveFromNpmRegistry(params.timeoutMs);
46
+ }
47
+ async function resolveFromNpmRegistry(timeoutMs) {
48
+ var _a;
49
+ const entries = Object.entries(TOOLCHAIN_PACKAGES);
50
+ const latests = await Promise.all(entries.map(async ([key, pkg]) => {
51
+ var _a;
52
+ const body = await fetchRegistryJson(registryPackageUrl(pkg), timeoutMs);
53
+ const latest = (_a = body === null || body === void 0 ? void 0 : body['dist-tags']) === null || _a === void 0 ? void 0 : _a.latest;
54
+ return [key, typeof latest === 'string' ? latest : null];
55
+ }));
56
+ const versions = Object.fromEntries(latests);
57
+ if (Object.values(versions).some((version) => version === null)) {
58
+ return { status: 'unavailable' };
59
+ }
60
+ const target = versions;
61
+ const trio = [target.sdk, target.ui, target.integrationSchema];
62
+ if (new Set(trio).size !== 1) {
63
+ return {
64
+ status: 'incoherent',
65
+ detail: `the npm registry reports mismatched versions for the sdk/ui/schema ` +
66
+ `trio: @ekanos/sdk@${target.sdk}, @ekanos/ui@${target.ui}, ` +
67
+ `@ekanos/integration-schema@${target.integrationSchema}. This is ` +
68
+ `usually a multi-package publish still in flight — retry shortly.`,
69
+ };
70
+ }
71
+ const sdkManifest = await fetchRegistryJson(registryPackageUrl(TOOLCHAIN_PACKAGES.sdk, target.sdk), timeoutMs);
72
+ const sdkPinnedSchema = (_a = sdkManifest === null || sdkManifest === void 0 ? void 0 : sdkManifest.dependencies) === null || _a === void 0 ? void 0 : _a['@ekanos/integration-schema'];
73
+ if (typeof sdkPinnedSchema !== 'string' ||
74
+ sdkPinnedSchema !== target.integrationSchema) {
75
+ return {
76
+ status: 'incoherent',
77
+ detail: `@ekanos/sdk@${target.sdk} on npm depends on ` +
78
+ `@ekanos/integration-schema@${String(sdkPinnedSchema)}, which does ` +
79
+ `not match the published @ekanos/integration-schema@` +
80
+ `${target.integrationSchema}. This is usually a multi-package ` +
81
+ `publish still in flight — retry shortly.`,
82
+ };
83
+ }
84
+ return { status: 'ok', source: 'npm', target };
85
+ }
86
+ async function fetchRegistryJson(url, timeoutMs) {
87
+ try {
88
+ const response = await fetch(url, {
89
+ headers: { accept: 'application/json' },
90
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
91
+ });
92
+ if (!response.ok)
93
+ return null;
94
+ return await response.json();
95
+ }
96
+ catch (_a) {
97
+ return null;
98
+ }
99
+ }
100
+ //# sourceMappingURL=toolchain-resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolchain-resolve.js","sourceRoot":"","sources":["../src/toolchain-resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n TOOLCHAIN_PACKAGES,\n type ToolchainKey,\n type ToolchainNote,\n type ToolchainVersions,\n} from './toolchain';\nimport { fetchToolchainReport } from './toolchain-api';\n\n/**\n * The single place that answers \"what should this toolchain be at?\" — shared\n * by `ekanos upgrade` (which installs the answer) and the proactive update\n * notice (which only reports it). Both need the exact same coherent target,\n * so there is exactly one resolution path, tried in this order:\n *\n * 1. `GET /api/partner/toolchain` on the resolved host, when both a host and\n * a session are available. Whatever the host returns is trusted as\n * already-coherent — it is the operator's own recommendation.\n * 2. The npm registry's `dist-tags.latest` for all five packages, with an\n * explicit COHERENCE check before ever calling it a \"target\": the\n * `@ekanos/sdk` / `@ekanos/ui` / `@ekanos/integration-schema` trio must\n * resolve to one identical version, and the `@ekanos/sdk` release at that\n * version must declare the exact same `@ekanos/integration-schema`\n * dependency. A multi-package publish is not atomic — this repo hit that\n * exact window mid-publish — so assembling \"latest of each, independently\"\n * can produce a set that literally cannot install together. Refusing is\n * the only safe response to a set caught in that state; the failure is\n * obvious and self-resolves the moment the publish finishes.\n */\n\nconst REGISTRY_BASE = 'https://registry.npmjs.org';\n\n/** npm registry package URLs encode the scope's `/` but not its `@`. */\nfunction registryPackageUrl(pkg: string, version?: string): string {\n const encoded = encodeURIComponent(pkg).replace('%40', '@');\n return version\n ? `${REGISTRY_BASE}/${encoded}/${encodeURIComponent(version)}`\n : `${REGISTRY_BASE}/${encoded}`;\n}\n\nexport type ToolchainResolution =\n | {\n status: 'ok';\n source: 'host';\n target: ToolchainVersions;\n minimum: ToolchainVersions;\n notes: ToolchainNote[];\n }\n | { status: 'ok'; source: 'npm'; target: ToolchainVersions }\n | { status: 'incoherent'; detail: string }\n | { status: 'unavailable' };\n\nexport interface ResolveToolchainParams {\n host?: string;\n session?: { accessToken: string } | null;\n /** Per-request timeout for the underlying fetches. */\n timeoutMs?: number;\n}\n\nexport async function resolveToolchainTarget(\n params: ResolveToolchainParams,\n): Promise<ToolchainResolution> {\n if (params.host && params.session) {\n const hostResult = await fetchToolchainReport(\n params.host,\n params.session.accessToken,\n );\n\n if (hostResult.status === 'ok') {\n return {\n status: 'ok',\n source: 'host',\n target: hostResult.report.current,\n minimum: hostResult.report.minimum,\n notes: hostResult.report.notes,\n };\n }\n\n // 404 / 501 / network failure / malformed body: fall through to npm.\n }\n\n return resolveFromNpmRegistry(params.timeoutMs);\n}\n\nasync function resolveFromNpmRegistry(\n timeoutMs?: number,\n): Promise<ToolchainResolution> {\n const entries = Object.entries(TOOLCHAIN_PACKAGES) as [\n ToolchainKey,\n string,\n ][];\n\n const latests = await Promise.all(\n entries.map(async ([key, pkg]) => {\n const body = await fetchRegistryJson(registryPackageUrl(pkg), timeoutMs);\n const latest = (body as { 'dist-tags'?: { latest?: unknown } } | null)?.[\n 'dist-tags'\n ]?.latest;\n return [key, typeof latest === 'string' ? latest : null] as const;\n }),\n );\n\n const versions = Object.fromEntries(latests) as Record<\n ToolchainKey,\n string | null\n >;\n\n if (Object.values(versions).some((version) => version === null)) {\n return { status: 'unavailable' };\n }\n\n const target = versions as ToolchainVersions;\n\n const trio = [target.sdk, target.ui, target.integrationSchema];\n if (new Set(trio).size !== 1) {\n return {\n status: 'incoherent',\n detail:\n `the npm registry reports mismatched versions for the sdk/ui/schema ` +\n `trio: @ekanos/sdk@${target.sdk}, @ekanos/ui@${target.ui}, ` +\n `@ekanos/integration-schema@${target.integrationSchema}. This is ` +\n `usually a multi-package publish still in flight — retry shortly.`,\n };\n }\n\n const sdkManifest = await fetchRegistryJson(\n registryPackageUrl(TOOLCHAIN_PACKAGES.sdk, target.sdk),\n timeoutMs,\n );\n const sdkPinnedSchema = (\n sdkManifest as { dependencies?: Record<string, unknown> } | null\n )?.dependencies?.['@ekanos/integration-schema'];\n\n if (\n typeof sdkPinnedSchema !== 'string' ||\n sdkPinnedSchema !== target.integrationSchema\n ) {\n return {\n status: 'incoherent',\n detail:\n `@ekanos/sdk@${target.sdk} on npm depends on ` +\n `@ekanos/integration-schema@${String(sdkPinnedSchema)}, which does ` +\n `not match the published @ekanos/integration-schema@` +\n `${target.integrationSchema}. This is usually a multi-package ` +\n `publish still in flight — retry shortly.`,\n };\n }\n\n return { status: 'ok', source: 'npm', target };\n}\n\nasync function fetchRegistryJson(\n url: string,\n timeoutMs?: number,\n): Promise<unknown | null> {\n try {\n const response = await fetch(url, {\n headers: { accept: 'application/json' },\n signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n });\n if (!response.ok) return null;\n return await response.json();\n } catch {\n return null;\n }\n}\n"]}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The five packages that make up "the toolchain" — the CLI plus the four
3
+ * packages a partner's project itself installs. `ekanos upgrade` and the
4
+ * proactive update notice both reason about this exact set, from two
5
+ * different angles: upgrade moves a PROJECT to a coherent target, the notice
6
+ * tells a partner the CLI itself is behind. One shared vocabulary so the two
7
+ * features cannot drift apart on what "the toolchain" even means.
8
+ */
9
+ export declare const TOOLCHAIN_PACKAGES: {
10
+ readonly cli: "@ekanos/cli";
11
+ readonly sdk: "@ekanos/sdk";
12
+ readonly ui: "@ekanos/ui";
13
+ readonly integrationSchema: "@ekanos/integration-schema";
14
+ readonly harness: "@ekanos/harness";
15
+ };
16
+ export type ToolchainKey = keyof typeof TOOLCHAIN_PACKAGES;
17
+ export type ToolchainVersions = Record<ToolchainKey, string>;
18
+ export interface ToolchainNote {
19
+ level: 'warn' | 'info';
20
+ message: string;
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The five packages that make up "the toolchain" — the CLI plus the four
3
+ * packages a partner's project itself installs. `ekanos upgrade` and the
4
+ * proactive update notice both reason about this exact set, from two
5
+ * different angles: upgrade moves a PROJECT to a coherent target, the notice
6
+ * tells a partner the CLI itself is behind. One shared vocabulary so the two
7
+ * features cannot drift apart on what "the toolchain" even means.
8
+ */
9
+ export const TOOLCHAIN_PACKAGES = {
10
+ cli: '@ekanos/cli',
11
+ sdk: '@ekanos/sdk',
12
+ ui: '@ekanos/ui',
13
+ integrationSchema: '@ekanos/integration-schema',
14
+ harness: '@ekanos/harness',
15
+ };
16
+ //# sourceMappingURL=toolchain.js.map