@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.
- package/README.md +81 -2
- package/dist/bin.js +14 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/dev.d.ts +4 -0
- package/dist/commands/dev.js +6 -20
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts +61 -0
- package/dist/commands/init.js +145 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/publish.d.ts +13 -0
- package/dist/commands/publish.js +38 -15
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.js +9 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts +47 -0
- package/dist/commands/upgrade.js +546 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/validate.d.ts +5 -0
- package/dist/commands/validate.js +20 -6
- package/dist/commands/validate.js.map +1 -1
- package/dist/context.d.ts +9 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -1
- package/dist/copy-tree.d.ts +48 -0
- package/dist/copy-tree.js +55 -0
- package/dist/copy-tree.js.map +1 -0
- package/dist/delegate.d.ts +28 -0
- package/dist/delegate.js +136 -0
- package/dist/delegate.js.map +1 -0
- package/dist/harness-scaffold.d.ts +21 -6
- package/dist/harness-scaffold.js +8 -5
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.js +40 -3
- package/dist/index.js.map +1 -1
- package/dist/package-manager.d.ts +10 -0
- package/dist/package-manager.js +32 -0
- package/dist/package-manager.js.map +1 -1
- package/dist/parse-argv.d.ts +7 -1
- package/dist/parse-argv.js +10 -2
- package/dist/parse-argv.js.map +1 -1
- package/dist/project-checks.d.ts +9 -0
- package/dist/project-checks.js +29 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +23 -0
- package/dist/project.js +66 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +1 -1
- package/dist/publish-api.js +2 -2
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.d.ts +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/schema-skew.d.ts +77 -0
- package/dist/schema-skew.js +163 -0
- package/dist/schema-skew.js.map +1 -0
- package/dist/sources-api.d.ts +1 -1
- package/dist/sources-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +54 -0
- package/dist/toolchain-api.js +58 -0
- package/dist/toolchain-api.js.map +1 -0
- package/dist/toolchain-resolve.d.ts +26 -0
- package/dist/toolchain-resolve.js +100 -0
- package/dist/toolchain-resolve.js.map +1 -0
- package/dist/toolchain.d.ts +21 -0
- package/dist/toolchain.js +16 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/update-notice.d.ts +32 -0
- package/dist/update-notice.js +180 -0
- package/dist/update-notice.js.map +1 -0
- package/dist/validate-findings.d.ts +12 -0
- package/dist/validate-findings.js +14 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +81 -0
- package/templates/CLAUDE.md.tmpl +2 -1
- package/templates/claude-skill.md.tmpl +20 -2
- package/templates/docs/api-notes.md.tmpl +21 -0
- package/templates/mcp.json.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +178 -0
- package/templates/skills/build-integration/references/api-docs-research.md +93 -0
- package/templates/skills/build-integration/references/api-wiring.md +94 -0
- package/templates/skills/build-integration/references/widget-design.md +132 -0
- package/templates/skills/polish-widgets/SKILL.md +139 -0
- 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":"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","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"]}
|
package/dist/sources-api.d.ts
CHANGED
|
@@ -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 "
|
|
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
|
package/dist/sources-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 \"
|
|
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
|