@ekanos/cli 0.1.4 → 0.1.6

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 (72) hide show
  1. package/README.md +132 -6
  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 +25 -0
  8. package/dist/commands/init.js +19 -8
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/publish.d.ts +59 -1
  11. package/dist/commands/publish.js +209 -25
  12. package/dist/commands/publish.js.map +1 -1
  13. package/dist/commands/sources.d.ts +17 -0
  14. package/dist/commands/sources.js +75 -0
  15. package/dist/commands/sources.js.map +1 -0
  16. package/dist/commands/status.js +9 -2
  17. package/dist/commands/status.js.map +1 -1
  18. package/dist/commands/upgrade.d.ts +47 -0
  19. package/dist/commands/upgrade.js +445 -0
  20. package/dist/commands/upgrade.js.map +1 -0
  21. package/dist/commands/use.d.ts +21 -0
  22. package/dist/commands/use.js +62 -0
  23. package/dist/commands/use.js.map +1 -0
  24. package/dist/commands/validate.d.ts +5 -0
  25. package/dist/commands/validate.js +20 -6
  26. package/dist/commands/validate.js.map +1 -1
  27. package/dist/commands/whoami.d.ts +6 -0
  28. package/dist/commands/whoami.js +26 -1
  29. package/dist/commands/whoami.js.map +1 -1
  30. package/dist/context.d.ts +9 -0
  31. package/dist/context.js +9 -0
  32. package/dist/context.js.map +1 -1
  33. package/dist/delegate.d.ts +28 -0
  34. package/dist/delegate.js +136 -0
  35. package/dist/delegate.js.map +1 -0
  36. package/dist/harness-scaffold.d.ts +21 -6
  37. package/dist/harness-scaffold.js +8 -5
  38. package/dist/harness-scaffold.js.map +1 -1
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.js +81 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/package-manager.d.ts +10 -0
  43. package/dist/package-manager.js +32 -0
  44. package/dist/package-manager.js.map +1 -1
  45. package/dist/schema-skew.d.ts +77 -0
  46. package/dist/schema-skew.js +163 -0
  47. package/dist/schema-skew.js.map +1 -0
  48. package/dist/seats.d.ts +21 -0
  49. package/dist/seats.js +15 -0
  50. package/dist/seats.js.map +1 -0
  51. package/dist/sources-api.d.ts +44 -0
  52. package/dist/sources-api.js +69 -0
  53. package/dist/sources-api.js.map +1 -0
  54. package/dist/toolchain-api.d.ts +54 -0
  55. package/dist/toolchain-api.js +58 -0
  56. package/dist/toolchain-api.js.map +1 -0
  57. package/dist/toolchain-resolve.d.ts +26 -0
  58. package/dist/toolchain-resolve.js +100 -0
  59. package/dist/toolchain-resolve.js.map +1 -0
  60. package/dist/toolchain.d.ts +21 -0
  61. package/dist/toolchain.js +16 -0
  62. package/dist/toolchain.js.map +1 -0
  63. package/dist/update-notice.d.ts +32 -0
  64. package/dist/update-notice.js +180 -0
  65. package/dist/update-notice.js.map +1 -0
  66. package/dist/validate-findings.d.ts +12 -0
  67. package/dist/validate-findings.js +12 -0
  68. package/dist/validate-findings.js.map +1 -1
  69. package/package.json +1 -1
  70. package/templates/AGENTS.md.tmpl +81 -18
  71. package/templates/CLAUDE.md.tmpl +2 -1
  72. package/templates/claude-skill.md.tmpl +45 -13
@@ -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"]}
@@ -0,0 +1,21 @@
1
+ import type { StoredSession } from './auth/credential-store.js';
2
+ import { type AuthEnvironment } from './auth/session.js';
3
+ import { type SourceSeat } from './sources-api.js';
4
+ /**
5
+ * `sources`, `use`, `publish`'s target verification, and `whoami`'s seats
6
+ * degradation all need the same thing: the caller's source-seat list, with
7
+ * the one-refresh-one-retry recovery every other verb in this CLI applies to
8
+ * a stale access token (see `resolveIdentity` in `auth/session.ts`).
9
+ * Centralized here so all four call sites share one refresh policy instead of
10
+ * four subtly different copies.
11
+ */
12
+ export type SeatsResult = {
13
+ status: 'ok';
14
+ sources: SourceSeat[];
15
+ session: StoredSession;
16
+ }
17
+ /** Unauthorized even after one refresh. */
18
+ | {
19
+ status: 'unauthorized';
20
+ };
21
+ export declare function fetchSeatsWithOneRefresh(env: AuthEnvironment, session: StoredSession): Promise<SeatsResult>;
package/dist/seats.js ADDED
@@ -0,0 +1,15 @@
1
+ import { refreshStoredSession } from './auth/session.js';
2
+ import { fetchSourceSeats } from './sources-api.js';
3
+ export async function fetchSeatsWithOneRefresh(env, session) {
4
+ const first = await fetchSourceSeats(env.host, session.accessToken);
5
+ if (first.status === 'ok') {
6
+ return { status: 'ok', sources: first.sources, session };
7
+ }
8
+ const refreshed = await refreshStoredSession(env, session);
9
+ const second = await fetchSourceSeats(env.host, refreshed.accessToken);
10
+ if (second.status === 'ok') {
11
+ return { status: 'ok', sources: second.sources, session: refreshed };
12
+ }
13
+ return { status: 'unauthorized' };
14
+ }
15
+ //# sourceMappingURL=seats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seats.js","sourceRoot":"","sources":["../src/seats.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAmB,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAelE,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,GAAoB,EACpB,OAAsB;IAEtB,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAEpE,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;IAC3D,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAEvE,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC3B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IACvE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;AACpC,CAAC","sourcesContent":["import type { StoredSession } from './auth/credential-store';\nimport { type AuthEnvironment, refreshStoredSession } from './auth/session';\nimport { type SourceSeat, fetchSourceSeats } from './sources-api';\n\n/**\n * `sources`, `use`, `publish`'s target verification, and `whoami`'s seats\n * degradation all need the same thing: the caller's source-seat list, with\n * the one-refresh-one-retry recovery every other verb in this CLI applies to\n * a stale access token (see `resolveIdentity` in `auth/session.ts`).\n * Centralized here so all four call sites share one refresh policy instead of\n * four subtly different copies.\n */\nexport type SeatsResult =\n | { status: 'ok'; sources: SourceSeat[]; session: StoredSession }\n /** Unauthorized even after one refresh. */\n | { status: 'unauthorized' };\n\nexport async function fetchSeatsWithOneRefresh(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<SeatsResult> {\n const first = await fetchSourceSeats(env.host, session.accessToken);\n\n if (first.status === 'ok') {\n return { status: 'ok', sources: first.sources, session };\n }\n\n const refreshed = await refreshStoredSession(env, session);\n const second = await fetchSourceSeats(env.host, refreshed.accessToken);\n\n if (second.status === 'ok') {\n return { status: 'ok', sources: second.sources, session: refreshed };\n }\n\n return { status: 'unauthorized' };\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The CLI's client for the Fusion source-seat visibility route.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * WIRE CONTRACT — the seam between this package and the Fusion web app.
6
+ *
7
+ * The server side lives in `apps/web/app/api/partner/sources/route.ts`. As
8
+ * with the auth routes and `publish-api.ts`, the two halves are coupled only
9
+ * by HTTP; tests stub `fetch`, never the route.
10
+ *
11
+ * GET /api/partner/sources [Authorization: Bearer <access_token>]
12
+ * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }
13
+ * roles is a non-empty array of "dev" | "admin", sorted; sources
14
+ * is sorted by slug. Empty array means no seats anywhere.
15
+ * → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
16
+ * refresh once and retry
17
+ * → 403 { error, code: "MFA_REQUIRED" }
18
+ * → 3xx treated as 401 (an
19
+ * auth-gated Fusion page
20
+ * answers 307)
21
+ * ---------------------------------------------------------------------------
22
+ */
23
+ export interface SourceSeat {
24
+ id: string;
25
+ slug: string;
26
+ name: string | null;
27
+ roles: string[];
28
+ }
29
+ export type FetchSourceSeatsResult = {
30
+ status: 'ok';
31
+ sources: SourceSeat[];
32
+ }
33
+ /** The bearer token did not authenticate — refresh and retry, once. */
34
+ | {
35
+ status: 'unauthorized';
36
+ };
37
+ /**
38
+ * List the sources the caller holds a seat on (any role). Authorization is
39
+ * entirely server-side (RLS on `source_memberships`): there is no client-side
40
+ * filter to get wrong here.
41
+ */
42
+ export declare function fetchSourceSeats(host: string, accessToken: string): Promise<FetchSourceSeatsResult>;
43
+ /** Guard for the "still unauthorized after one refresh" terminal case. */
44
+ export declare function stillUnauthorizedSourceSeats(host: string): never;
@@ -0,0 +1,69 @@
1
+ import { request } from './auth/fusion-api.js';
2
+ import { authRequiredError, networkError } from './errors.js';
3
+ /**
4
+ * List the sources the caller holds a seat on (any role). Authorization is
5
+ * entirely server-side (RLS on `source_memberships`): there is no client-side
6
+ * filter to get wrong here.
7
+ */
8
+ export async function fetchSourceSeats(host, accessToken) {
9
+ var _a, _b;
10
+ const response = await request(`${host}/api/partner/sources`, {
11
+ method: 'GET',
12
+ headers: {
13
+ accept: 'application/json',
14
+ authorization: `Bearer ${accessToken}`,
15
+ },
16
+ });
17
+ if (response.status === 401 || isRedirect(response.status)) {
18
+ return { status: 'unauthorized' };
19
+ }
20
+ const body = await readOptionalJson(response);
21
+ if (response.status === 403) {
22
+ throw authRequiredError((_a = str(body.error)) !== null && _a !== void 0 ? _a : `${host} requires multi-factor authentication for this account.`, `Complete MFA in the browser, then run "ekanos login" again.`);
23
+ }
24
+ if (!response.ok) {
25
+ throw networkError(`${host}/api/partner/sources responded ${response.status}.`, response.status >= 500
26
+ ? `The Fusion deployment returned a server error. Retry shortly; if ` +
27
+ `it persists, report it with the status code.`
28
+ : `Confirm the host is a Fusion deployment running a build that ` +
29
+ `includes the partner sources route.`);
30
+ }
31
+ const data = ((_b = body.data) !== null && _b !== void 0 ? _b : {});
32
+ const rows = Array.isArray(data.sources) ? data.sources : [];
33
+ const sources = rows
34
+ .filter((row) => typeof row === 'object' && row !== null)
35
+ .map((row) => {
36
+ var _a, _b;
37
+ return ({
38
+ id: (_a = str(row.id)) !== null && _a !== void 0 ? _a : '',
39
+ slug: (_b = str(row.slug)) !== null && _b !== void 0 ? _b : '',
40
+ name: str(row.name),
41
+ roles: Array.isArray(row.roles)
42
+ ? row.roles.filter((role) => typeof role === 'string')
43
+ : [],
44
+ });
45
+ });
46
+ return { status: 'ok', sources };
47
+ }
48
+ /** Guard for the "still unauthorized after one refresh" terminal case. */
49
+ export function stillUnauthorizedSourceSeats(host) {
50
+ throw authRequiredError(`The session for ${host} could not authorize the source seat lookup.`, `Run "ekanos login --host ${host}" and retry.`);
51
+ }
52
+ async function readOptionalJson(response) {
53
+ try {
54
+ const parsed = await response.json();
55
+ return typeof parsed === 'object' && parsed !== null
56
+ ? parsed
57
+ : {};
58
+ }
59
+ catch (_a) {
60
+ return {};
61
+ }
62
+ }
63
+ function isRedirect(status) {
64
+ return status >= 300 && status < 400;
65
+ }
66
+ function str(value) {
67
+ return typeof value === 'string' && value.length > 0 ? value : null;
68
+ }
69
+ //# sourceMappingURL=sources-api.js.map
@@ -0,0 +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"]}
@@ -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>;