@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,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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolchain.js","sourceRoot":"","sources":["../src/toolchain.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,aAAa;IAClB,EAAE,EAAE,YAAY;IAChB,iBAAiB,EAAE,4BAA4B;IAC/C,OAAO,EAAE,iBAAiB;CAClB,CAAC","sourcesContent":["/**\n * The five packages that make up \"the toolchain\" — the CLI plus the four\n * packages a partner's project itself installs. `ekanos upgrade` and the\n * proactive update notice both reason about this exact set, from two\n * different angles: upgrade moves a PROJECT to a coherent target, the notice\n * tells a partner the CLI itself is behind. One shared vocabulary so the two\n * features cannot drift apart on what \"the toolchain\" even means.\n */\nexport const TOOLCHAIN_PACKAGES = {\n cli: '@ekanos/cli',\n sdk: '@ekanos/sdk',\n ui: '@ekanos/ui',\n integrationSchema: '@ekanos/integration-schema',\n harness: '@ekanos/harness',\n} as const;\n\nexport type ToolchainKey = keyof typeof TOOLCHAIN_PACKAGES;\n\nexport type ToolchainVersions = Record<ToolchainKey, string>;\n\nexport interface ToolchainNote {\n level: 'warn' | 'info';\n message: string;\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import type { CliContext } from './context.js';
2
+ import type { ToolchainVersions } from './toolchain.js';
3
+ export interface ToolchainNoticeData {
4
+ status: 'behind' | 'unsupported';
5
+ current: {
6
+ cli: string;
7
+ };
8
+ target: ToolchainVersions;
9
+ upgradeCommand: 'ekanos upgrade';
10
+ }
11
+ export interface CheckForUpdateParams {
12
+ env: Record<string, string | undefined>;
13
+ /** An already-resolved Fusion host/session, when the caller has one handy. */
14
+ host?: string;
15
+ session?: {
16
+ accessToken: string;
17
+ } | null;
18
+ }
19
+ /**
20
+ * Resolve whether this CLI is behind, respecting the 24h cache and the hard
21
+ * `EKANOS_NO_UPDATE_CHECK` opt-out. Returns null whenever there is nothing to
22
+ * say — current, unresolvable, or any failure along the way.
23
+ */
24
+ export declare function checkForUpdate(params: CheckForUpdateParams): Promise<ToolchainNoticeData | null>;
25
+ /**
26
+ * Surface a notice, if any. Human mode gets one stderr line; every mode gets
27
+ * the `claude-code-hint` stderr line so an agent parsing stderr for that
28
+ * marker sees it regardless of `--json`. NEVER writes to stdout — the JSON
29
+ * envelope's caller is responsible for embedding `notice` under a `toolchain`
30
+ * key in its own `data`, per the envelope docs in `context.ts`.
31
+ */
32
+ export declare function announceUpdate(ctx: CliContext, notice: ToolchainNoticeData | null): void;
@@ -0,0 +1,180 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { CredentialStore, resolveEkanosHome } from './auth/credential-store.js';
4
+ import { cliVersion, compareVersions } from './compatibility.js';
5
+ import { resolveToolchainTarget } from './toolchain-resolve.js';
6
+ /**
7
+ * The proactive, NEVER-mutating "your CLI is behind" notice. Runs on
8
+ * `validate`, `dev`, `publish` and `status` — the verbs a partner (or an
9
+ * agent acting for one) runs constantly — so staleness is discovered on the
10
+ * next ordinary command instead of only when something breaks.
11
+ *
12
+ * The one rule that governs every line of this file: this check must be
13
+ * INVISIBLE on failure. A partner offline, a flaky registry, a host that
14
+ * predates this endpoint — none of that may add meaningful latency, change a
15
+ * verb's exit code, or so much as touch the JSON envelope's `ok` field.
16
+ * Every failure path below resolves to `null` ("say nothing"), never a throw.
17
+ */
18
+ const CACHE_FILENAME = 'update-check.json';
19
+ const CACHE_DIR_MODE = 0o700;
20
+ const CACHE_FILE_MODE = 0o600;
21
+ const TTL_MS = 24 * 60 * 60 * 1000;
22
+ /** The hard ceiling on this check's total network time, whatever else happens. */
23
+ const DEADLINE_MS = 1500;
24
+ function cachePath(env) {
25
+ return path.join(resolveEkanosHome(env), '.ekanos', CACHE_FILENAME);
26
+ }
27
+ function readCache(env) {
28
+ try {
29
+ const raw = fs.readFileSync(cachePath(env), 'utf8');
30
+ const parsed = JSON.parse(raw);
31
+ return isCacheFile(parsed) ? parsed : null;
32
+ }
33
+ catch (_a) {
34
+ // Missing, unreadable, or corrupt — all read as "no cache", never an
35
+ // error. This check is a courtesy, not a source of truth.
36
+ return null;
37
+ }
38
+ }
39
+ function writeCache(env, file) {
40
+ const target = cachePath(env);
41
+ try {
42
+ fs.mkdirSync(path.dirname(target), {
43
+ recursive: true,
44
+ mode: CACHE_DIR_MODE,
45
+ });
46
+ const tmp = `${target}.${process.pid}.tmp`;
47
+ fs.writeFileSync(tmp, JSON.stringify(file), { mode: CACHE_FILE_MODE });
48
+ fs.renameSync(tmp, target);
49
+ }
50
+ catch (_a) {
51
+ // Best-effort. The next run simply re-checks — this is a cache, not a
52
+ // durable record.
53
+ }
54
+ }
55
+ function isCacheFile(value) {
56
+ if (typeof value !== 'object' || value === null)
57
+ return false;
58
+ const candidate = value;
59
+ return (typeof candidate.checkedAt === 'number' &&
60
+ typeof candidate.target === 'object' &&
61
+ candidate.target !== null &&
62
+ (candidate.source === 'host' || candidate.source === 'npm'));
63
+ }
64
+ /**
65
+ * The one host/session this check will ever try, resolved WITHOUT any of the
66
+ * throwing machinery in `auth/session.ts` — `resolveHost` is built to demand
67
+ * an answer (a usage error naming the ambiguity) because a real command needs
68
+ * one; this check needs an answer only if one is obvious, and silence
69
+ * otherwise. Exactly one stored host is "obvious"; zero or several are not,
70
+ * and this never prompts or guesses to resolve that ambiguity — it just
71
+ * falls back to the npm registry, same as no session at all.
72
+ */
73
+ function bestEffortHostSession(env) {
74
+ try {
75
+ const store = new CredentialStore({ home: resolveEkanosHome(env) });
76
+ const hosts = store.hosts();
77
+ if (hosts.length !== 1)
78
+ return null;
79
+ const session = store.read(hosts[0]);
80
+ return session ? { host: hosts[0], session } : null;
81
+ }
82
+ catch (_a) {
83
+ return null;
84
+ }
85
+ }
86
+ function withDeadline(promise, ms) {
87
+ return new Promise((resolve) => {
88
+ let settled = false;
89
+ const timer = setTimeout(() => {
90
+ if (!settled) {
91
+ settled = true;
92
+ resolve(null);
93
+ }
94
+ }, ms);
95
+ // A rejection is a failure like any other here — resolve to null rather
96
+ // than letting it become an unhandled rejection.
97
+ promise.then((value) => {
98
+ if (!settled) {
99
+ settled = true;
100
+ clearTimeout(timer);
101
+ resolve(value);
102
+ }
103
+ }, () => {
104
+ if (!settled) {
105
+ settled = true;
106
+ clearTimeout(timer);
107
+ resolve(null);
108
+ }
109
+ });
110
+ });
111
+ }
112
+ /**
113
+ * Resolve whether this CLI is behind, respecting the 24h cache and the hard
114
+ * `EKANOS_NO_UPDATE_CHECK` opt-out. Returns null whenever there is nothing to
115
+ * say — current, unresolvable, or any failure along the way.
116
+ */
117
+ export async function checkForUpdate(params) {
118
+ if (params.env.EKANOS_NO_UPDATE_CHECK)
119
+ return null;
120
+ const cached = readCache(params.env);
121
+ const now = Date.now();
122
+ let target;
123
+ let minimum;
124
+ if (cached && now - cached.checkedAt < TTL_MS) {
125
+ target = cached.target;
126
+ minimum = cached.minimum;
127
+ }
128
+ else {
129
+ const auth = params.host && params.session
130
+ ? { host: params.host, session: params.session }
131
+ : bestEffortHostSession(params.env);
132
+ const resolution = await withDeadline(resolveToolchainTarget({
133
+ host: auth === null || auth === void 0 ? void 0 : auth.host,
134
+ session: auth === null || auth === void 0 ? void 0 : auth.session,
135
+ timeoutMs: DEADLINE_MS,
136
+ }), DEADLINE_MS);
137
+ if (!resolution || resolution.status !== 'ok')
138
+ return null;
139
+ target = resolution.target;
140
+ minimum = resolution.source === 'host' ? resolution.minimum : undefined;
141
+ writeCache(params.env, {
142
+ checkedAt: now,
143
+ target,
144
+ minimum,
145
+ source: resolution.source,
146
+ });
147
+ }
148
+ const current = cliVersion();
149
+ if (compareVersions(current, target.cli) >= 0)
150
+ return null;
151
+ const status = minimum && compareVersions(current, minimum.cli) < 0
152
+ ? 'unsupported'
153
+ : 'behind';
154
+ return {
155
+ status,
156
+ current: { cli: current },
157
+ target,
158
+ upgradeCommand: 'ekanos upgrade',
159
+ };
160
+ }
161
+ /**
162
+ * Surface a notice, if any. Human mode gets one stderr line; every mode gets
163
+ * the `claude-code-hint` stderr line so an agent parsing stderr for that
164
+ * marker sees it regardless of `--json`. NEVER writes to stdout — the JSON
165
+ * envelope's caller is responsible for embedding `notice` under a `toolchain`
166
+ * key in its own `data`, per the envelope docs in `context.ts`.
167
+ */
168
+ export function announceUpdate(ctx, notice) {
169
+ if (!notice)
170
+ return;
171
+ if (!ctx.jsonMode) {
172
+ ctx.warn(notice.status === 'unsupported'
173
+ ? `ekanos: @ekanos/cli ${notice.current.cli} is below the version ` +
174
+ `this host requires (${notice.target.cli}). Run "ekanos upgrade".`
175
+ : `ekanos: a newer @ekanos/cli is available (${notice.current.cli} ` +
176
+ `→ ${notice.target.cli}). Run "ekanos upgrade".`);
177
+ }
178
+ ctx.emitClaudeCodeHint({ tool: 'ekanos', toolchain: notice });
179
+ }
180
+ //# sourceMappingURL=update-notice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-notice.js","sourceRoot":"","sources":["../src/update-notice.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { CredentialStore, resolveEkanosHome } from './auth/credential-store';\nimport { cliVersion, compareVersions } from './compatibility';\nimport type { CliContext } from './context';\nimport type { ToolchainVersions } from './toolchain';\nimport { resolveToolchainTarget } from './toolchain-resolve';\n\n/**\n * The proactive, NEVER-mutating \"your CLI is behind\" notice. Runs on\n * `validate`, `dev`, `publish` and `status` — the verbs a partner (or an\n * agent acting for one) runs constantly — so staleness is discovered on the\n * next ordinary command instead of only when something breaks.\n *\n * The one rule that governs every line of this file: this check must be\n * INVISIBLE on failure. A partner offline, a flaky registry, a host that\n * predates this endpoint — none of that may add meaningful latency, change a\n * verb's exit code, or so much as touch the JSON envelope's `ok` field.\n * Every failure path below resolves to `null` (\"say nothing\"), never a throw.\n */\n\nconst CACHE_FILENAME = 'update-check.json';\nconst CACHE_DIR_MODE = 0o700;\nconst CACHE_FILE_MODE = 0o600;\nconst TTL_MS = 24 * 60 * 60 * 1000;\n/** The hard ceiling on this check's total network time, whatever else happens. */\nconst DEADLINE_MS = 1500;\n\nexport interface ToolchainNoticeData {\n status: 'behind' | 'unsupported';\n current: { cli: string };\n target: ToolchainVersions;\n upgradeCommand: 'ekanos upgrade';\n}\n\ninterface CacheFile {\n checkedAt: number;\n target: ToolchainVersions;\n minimum?: ToolchainVersions;\n source: 'host' | 'npm';\n}\n\nfunction cachePath(env: Record<string, string | undefined>): string {\n return path.join(resolveEkanosHome(env), '.ekanos', CACHE_FILENAME);\n}\n\nfunction readCache(env: Record<string, string | undefined>): CacheFile | null {\n try {\n const raw = fs.readFileSync(cachePath(env), 'utf8');\n const parsed: unknown = JSON.parse(raw);\n return isCacheFile(parsed) ? parsed : null;\n } catch {\n // Missing, unreadable, or corrupt — all read as \"no cache\", never an\n // error. This check is a courtesy, not a source of truth.\n return null;\n }\n}\n\nfunction writeCache(\n env: Record<string, string | undefined>,\n file: CacheFile,\n): void {\n const target = cachePath(env);\n try {\n fs.mkdirSync(path.dirname(target), {\n recursive: true,\n mode: CACHE_DIR_MODE,\n });\n const tmp = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(file), { mode: CACHE_FILE_MODE });\n fs.renameSync(tmp, target);\n } catch {\n // Best-effort. The next run simply re-checks — this is a cache, not a\n // durable record.\n }\n}\n\nfunction isCacheFile(value: unknown): value is CacheFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<CacheFile>;\n return (\n typeof candidate.checkedAt === 'number' &&\n typeof candidate.target === 'object' &&\n candidate.target !== null &&\n (candidate.source === 'host' || candidate.source === 'npm')\n );\n}\n\n/**\n * The one host/session this check will ever try, resolved WITHOUT any of the\n * throwing machinery in `auth/session.ts` — `resolveHost` is built to demand\n * an answer (a usage error naming the ambiguity) because a real command needs\n * one; this check needs an answer only if one is obvious, and silence\n * otherwise. Exactly one stored host is \"obvious\"; zero or several are not,\n * and this never prompts or guesses to resolve that ambiguity — it just\n * falls back to the npm registry, same as no session at all.\n */\nfunction bestEffortHostSession(\n env: Record<string, string | undefined>,\n): { host: string; session: { accessToken: string } } | null {\n try {\n const store = new CredentialStore({ home: resolveEkanosHome(env) });\n const hosts = store.hosts();\n if (hosts.length !== 1) return null;\n const session = store.read(hosts[0]!);\n return session ? { host: hosts[0]!, session } : null;\n } catch {\n return null;\n }\n}\n\nfunction withDeadline<T>(promise: Promise<T>, ms: number): Promise<T | null> {\n return new Promise((resolve) => {\n let settled = false;\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n resolve(null);\n }\n }, ms);\n // A rejection is a failure like any other here — resolve to null rather\n // than letting it become an unhandled rejection.\n promise.then(\n (value) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(value);\n }\n },\n () => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(null);\n }\n },\n );\n });\n}\n\nexport interface CheckForUpdateParams {\n env: Record<string, string | undefined>;\n /** An already-resolved Fusion host/session, when the caller has one handy. */\n host?: string;\n session?: { accessToken: string } | null;\n}\n\n/**\n * Resolve whether this CLI is behind, respecting the 24h cache and the hard\n * `EKANOS_NO_UPDATE_CHECK` opt-out. Returns null whenever there is nothing to\n * say — current, unresolvable, or any failure along the way.\n */\nexport async function checkForUpdate(\n params: CheckForUpdateParams,\n): Promise<ToolchainNoticeData | null> {\n if (params.env.EKANOS_NO_UPDATE_CHECK) return null;\n\n const cached = readCache(params.env);\n const now = Date.now();\n\n let target: ToolchainVersions;\n let minimum: ToolchainVersions | undefined;\n\n if (cached && now - cached.checkedAt < TTL_MS) {\n target = cached.target;\n minimum = cached.minimum;\n } else {\n const auth =\n params.host && params.session\n ? { host: params.host, session: params.session }\n : bestEffortHostSession(params.env);\n\n const resolution = await withDeadline(\n resolveToolchainTarget({\n host: auth?.host,\n session: auth?.session,\n timeoutMs: DEADLINE_MS,\n }),\n DEADLINE_MS,\n );\n\n if (!resolution || resolution.status !== 'ok') return null;\n\n target = resolution.target;\n minimum = resolution.source === 'host' ? resolution.minimum : undefined;\n\n writeCache(params.env, {\n checkedAt: now,\n target,\n minimum,\n source: resolution.source,\n });\n }\n\n const current = cliVersion();\n if (compareVersions(current, target.cli) >= 0) return null;\n\n const status =\n minimum && compareVersions(current, minimum.cli) < 0\n ? 'unsupported'\n : 'behind';\n\n return {\n status,\n current: { cli: current },\n target,\n upgradeCommand: 'ekanos upgrade',\n };\n}\n\n/**\n * Surface a notice, if any. Human mode gets one stderr line; every mode gets\n * the `claude-code-hint` stderr line so an agent parsing stderr for that\n * marker sees it regardless of `--json`. NEVER writes to stdout — the JSON\n * envelope's caller is responsible for embedding `notice` under a `toolchain`\n * key in its own `data`, per the envelope docs in `context.ts`.\n */\nexport function announceUpdate(\n ctx: CliContext,\n notice: ToolchainNoticeData | null,\n): void {\n if (!notice) return;\n\n if (!ctx.jsonMode) {\n ctx.warn(\n notice.status === 'unsupported'\n ? `ekanos: @ekanos/cli ${notice.current.cli} is below the version ` +\n `this host requires (${notice.target.cli}). Run \"ekanos upgrade\".`\n : `ekanos: a newer @ekanos/cli is available (${notice.current.cli} ` +\n `→ ${notice.target.cli}). Run \"ekanos upgrade\".`,\n );\n }\n\n ctx.emitClaudeCodeHint({ tool: 'ekanos', toolchain: notice });\n}\n"]}
@@ -6,5 +6,17 @@ import type { LoadedProject } from './project.js';
6
6
  * implementation so the publish gate can never drift from what `validate`
7
7
  * checks — a project that validates clean is, by construction, a project that
8
8
  * publish will accept locally.
9
+ *
10
+ * NOTE on schema skew: an earlier version of this function rewrote the
11
+ * unrecognized-key hint when the project's `@ekanos/integration-schema` was
12
+ * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —
13
+ * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`
14
+ * or `publish` ever calls this function in that state, so a project-newer
15
+ * finding can never reach here to be rewritten, and the precondition error
16
+ * already names both versions and points at the fix. It was removed rather
17
+ * than kept "just in case": if a future caller ever makes validate proceed
18
+ * under skew instead of failing fast, THAT is when a rewrite here becomes
19
+ * necessary again, and it should be reintroduced with a real caller driving
20
+ * it, not tests that fake their own reachability.
9
21
  */
10
22
  export declare function collectAllFindings(loaded: LoadedProject): Promise<Finding[]>;
@@ -8,6 +8,18 @@ import { collectProjectFindings } from './project-checks.js';
8
8
  * implementation so the publish gate can never drift from what `validate`
9
9
  * checks — a project that validates clean is, by construction, a project that
10
10
  * publish will accept locally.
11
+ *
12
+ * NOTE on schema skew: an earlier version of this function rewrote the
13
+ * unrecognized-key hint when the project's `@ekanos/integration-schema` was
14
+ * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —
15
+ * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`
16
+ * or `publish` ever calls this function in that state, so a project-newer
17
+ * finding can never reach here to be rewritten, and the precondition error
18
+ * already names both versions and points at the fix. It was removed rather
19
+ * than kept "just in case": if a future caller ever makes validate proceed
20
+ * under skew instead of failing fast, THAT is when a rewrite here becomes
21
+ * necessary again, and it should be reintroduced with a real caller driving
22
+ * it, not tests that fake their own reachability.
11
23
  */
12
24
  export async function collectAllFindings(loaded) {
13
25
  const findings = [];
@@ -1 +1 @@
1
- {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAqB;IAErB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,WAAW,GAA+B,EAAE,CAAC;IAEnD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,cAAc,CACjC,WAAW,CAAC,SAAS,EACrB,MAAM,CAAC,UAAU,CAClB,CAAC;QAEF,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,CACX,GAAG,yBAAyB,CAAC,MAAM,CAAC,UAAU,EAAE;gBAC9C,IAAI,EAAE,WAAW,CAAC,SAAS;aAC5B,CAAC,CACH,CAAC;YACF,QAAQ,CAAC,IAAI,CACX,GAAG,4BAA4B,CAC7B,WAAW,EACX,MAAM,CAAC,UAAgC,CACxC,CACF,CAAC;YACF,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAsC,CAAC,CAAC;YAChE,SAAS;QACX,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,yEAAyE;YACzE,iEAAiE;YACjE,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK,EAAE,iBAAiB;gBACxB,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,WAAW,CAAC,SAAS;gBAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EACF,oEAAoE;oBACpE,uBAAuB;aAC1B,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,kEAAkE;QAClE,MAAM,iBAAiB,CACrB,kDAAkD,WAAW,CAAC,IAAI,KAAK;YACrE,MAAM,CAAC,OAAO,EAChB,oEAAoE;YAClE,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAE5D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,4BAA4B,CACnC,WAA+D,EAC/D,UAA8B;IAE9B,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC;IACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,WAAW,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAE7E,OAAO;QACL;YACE,KAAK,EAAE,wBAAwB;YAC/B,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,WAAW,CAAC,SAAS;YAC3B,OAAO,EACL,8BAA8B,WAAW,CAAC,IAAI,wBAAwB;gBACtE,wBAAwB,QAAQ,4BAA4B;gBAC5D,sEAAsE;gBACtE,qCAAqC;YACvC,IAAI,EACF,sDAAsD,QAAQ,OAAO;gBACrE,+BAA+B,WAAW,CAAC,IAAI,OAAO;gBACtD,sBAAsB;SACzB;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\n\nimport { preconditionError } from './errors';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport { collectProjectFindings } from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n"]}
1
+ {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAqB;IAErB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,WAAW,GAA+B,EAAE,CAAC;IAEnD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,cAAc,CACjC,WAAW,CAAC,SAAS,EACrB,MAAM,CAAC,UAAU,CAClB,CAAC;QAEF,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,CACX,GAAG,yBAAyB,CAAC,MAAM,CAAC,UAAU,EAAE;gBAC9C,IAAI,EAAE,WAAW,CAAC,SAAS;aAC5B,CAAC,CACH,CAAC;YACF,QAAQ,CAAC,IAAI,CACX,GAAG,4BAA4B,CAC7B,WAAW,EACX,MAAM,CAAC,UAAgC,CACxC,CACF,CAAC;YACF,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAsC,CAAC,CAAC;YAChE,SAAS;QACX,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,yEAAyE;YACzE,iEAAiE;YACjE,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK,EAAE,iBAAiB;gBACxB,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,WAAW,CAAC,SAAS;gBAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EACF,oEAAoE;oBACpE,uBAAuB;aAC1B,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,kEAAkE;QAClE,MAAM,iBAAiB,CACrB,kDAAkD,WAAW,CAAC,IAAI,KAAK;YACrE,MAAM,CAAC,OAAO,EAChB,oEAAoE;YAClE,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAE5D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,4BAA4B,CACnC,WAA+D,EAC/D,UAA8B;IAE9B,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC;IACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,WAAW,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAE7E,OAAO;QACL;YACE,KAAK,EAAE,wBAAwB;YAC/B,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,WAAW,CAAC,SAAS;YAC3B,OAAO,EACL,8BAA8B,WAAW,CAAC,IAAI,wBAAwB;gBACtE,wBAAwB,QAAQ,4BAA4B;gBAC5D,sEAAsE;gBACtE,qCAAqC;YACvC,IAAI,EACF,sDAAsD,QAAQ,OAAO;gBACrE,+BAA+B,WAAW,CAAC,IAAI,OAAO;gBACtD,sBAAsB;SACzB;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\n\nimport { preconditionError } from './errors';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport { collectProjectFindings } from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "The Ekanos partner toolchain CLI: scaffold, validate, and test a Fusion integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
6
6
  "license": "MIT",
@@ -156,10 +156,20 @@ activationData is updated — **seed the cache in `onActivate`; the schedule
156
156
  keeps it fresh.** Without it, a cache-backed widget is empty until the first
157
157
  schedule tick. v1 errors are non-fatal: a throw is logged as a warning and the
158
158
  activation stays connected, so this is for cache seeding and eager
159
- validation, never a connect gate. Seed BEST-EFFORT: catch transient upstream
160
- failures (timeouts, 5xx), log, and return a throw surfaces a scary warning
161
- on a brand-new connection that the schedule will heal within one tick anyway.
162
- Reserve the throw for misconfiguration the user can actually fix.
159
+ validation, never a connect gate. Seed BEST-EFFORT swallow what you can
160
+ name, log loudly what you can't: catch the upstream error classes you can
161
+ reason about (timeouts, 5xx, provider errors) and warn; log anything
162
+ unrecognized as an error and return, so a genuine bug stays visible in logs
163
+ without telling a user their brand-new connection failed. Reserve the throw
164
+ for misconfiguration the user can actually fix. Treat onActivate as an
165
+ OPTIMIZATION, never a correctness dependency: the schedule remains the
166
+ freshness guarantee, and a config fingerprint keeps the stale window safe
167
+ whether or not the hook fires.
168
+
169
+ Troubleshooting `ekanos dev`: adding/removing/re-adding handler modules can
170
+ leave the harness's Turbopack cache resolving a deleted path ("Module not
171
+ found" for a file that exists). Clear it with `rm -rf .ekanos/harness/.next`
172
+ and restart.
163
173
 
164
174
  A cache invalidated only by age still serves the *previous* activation's data
165
175
  for a while even with `onActivate` wired up. Close that gap with a
@@ -190,8 +200,17 @@ system. Available modules (import individually, e.g. `@ekanos/ui/button`):
190
200
  `switch`, `textarea`, `tooltip`, `trans`, `utils` (the `cn()` helper),
191
201
  `ai-prompt-input`, plus the stylesheets `styles.css` / `tokens.css` /
192
202
  `theme.css` / `base.css`. Icons are Font Awesome glyph names via
193
- `@ekanos/ui/icon`; outside a host that loads Font Awesome they render as
194
- nothingexpected, not a bug.
203
+ `@ekanos/ui/icon`. This generated shell loads Font Awesome **Free**
204
+ (`@fortawesome/fontawesome-free`)a much smaller set than the Pro kit Fusion
205
+ itself runs — so a name that's Pro-only (or just mistyped) renders a
206
+ circle-question disc instead of vanishing, and dev builds `console.warn` once
207
+ per name to catch it before a screenshot review does. `fa-light` / `fa-duotone`
208
+ / `fa-thin` are rewritten to `fa-solid`, the one weight Free ships, so author
209
+ `fa-light` anyway — it degrades harmlessly today, and the intended weight
210
+ survives if this host ever loads Pro instead. Verify a glyph name against the
211
+ Free set (`pnpm --filter @ekanos/ui check:fa-free-names`, or
212
+ https://fontawesome.com/search?o=r&s=solid,regular,brands) before relying on
213
+ it looking like anything in particular.
195
214
 
196
215
  ## Testing
197
216
 
@@ -223,6 +242,14 @@ with transport semantics (payload validated first, signature skip logged) and
223
242
  derive the context from the definition. Reuse one context across invocations
224
243
  via `options.context` to accumulate state.
225
244
 
245
+ A global install (`npm i -g @ekanos/cli`) is fine and convenient: inside a
246
+ project, `validate`/`dev`/`publish`/`test` automatically delegate to THIS
247
+ project's own pinned `@ekanos/cli` devDependency whenever it differs from the
248
+ invoked binary (the same pattern as the Gradle wrapper or `npx`) — one global
249
+ binary on PATH, but the version that actually reads your definition is always
250
+ the one this project pins. That is also why the scaffold's local
251
+ `@ekanos/cli` devDependency stays mandatory even with a global install.
252
+
226
253
  ## The iteration loop (`npx ekanos <verb>`)
227
254
 
228
255
  Every verb speaks a JSON envelope on stdout with `--json` (auto-on when piped):
@@ -237,20 +264,56 @@ carries an imperative `hint`; treat hints as remediation instructions.
237
264
  | `ekanos test` | Run this project's test script through its package manager |
238
265
  | `ekanos login --host <url>` | Device-flow login to a Fusion deployment (stores the host for later verbs) |
239
266
  | `ekanos status` | Login state for the resolved host + this project's submissions |
240
- | `ekanos publish` | Validate, pack (whitelist: ekanos.json, package.json, README.md, src/), and submit. Refuses on any error-severity finding |
241
-
242
- Host resolution for `logout`/`whoami`/`status`/`publish`: `--host` flag
243
- `EKANOS_HOST` the `host` field in ekanos.json the sole stored login. The
244
- first successful `publish` saves `host` and `source` into ekanos.json.
245
- `login` is the exception — it never reads ekanos.json for a host (`--host` →
246
- `EKANOS_HOST` → the sole stored login only), since it is the verb that
247
- creates credentials and a committed file must not be able to redirect it.
267
+ | `ekanos sources` | List every Fusion source you hold a developer/admin seat on, marking this project's current target |
268
+ | `ekanos use <source-slug>` | Set this project's publish target — refuses (`forbidden`) a slug you hold no seat on |
269
+ | `ekanos publish` | Validate, pack (whitelist: ekanos.json, package.json, README.md, src/), verify the target, and submit. Refuses on any error-severity finding |
270
+ | `ekanos upgrade` | Move this project's toolchain to a coherent target version, with verify-and-rollback |
271
+
272
+ ### Keeping the toolchain current
273
+
274
+ `validate`/`dev`/`publish` all bundle an EXACT `@ekanos/integration-schema`
275
+ version, and a project can end up NEWER than that (a `pnpm up` on
276
+ `@ekanos/sdk` alone is enough). In that state `validate`'s findings are
277
+ unreliable — a field the newer schema added parses here as an unrecognized
278
+ key, and it is easy to "fix" that by deleting perfectly correct code. If a
279
+ finding smells like that (an unrecognized key on a field you are confident is
280
+ right), run `ekanos upgrade --check` first: it is READ-ONLY and always safe —
281
+ it only reports current vs. target versions and never touches the project. A
282
+ bare `ekanos upgrade` (no flags) MUTATES the project: it installs the target
283
+ versions, verifies with `ekanos validate` and this project's own test script,
284
+ and rolls everything back automatically if either fails. Never run a bare
285
+ `ekanos upgrade` as a way to "try and see" — use `--check` for that.
286
+
287
+ Host resolution for `logout`/`whoami`/`status`/`sources`/`use`/`publish`:
288
+ `--host` flag → `EKANOS_HOST` → the `host` field in ekanos.json → the sole
289
+ stored login. The first successful `publish` saves `host` and `source` into
290
+ ekanos.json. `login` is the exception — it never reads ekanos.json for a host
291
+ (`--host` → `EKANOS_HOST` → the sole stored login only), since it is the verb
292
+ that creates credentials and a committed file must not be able to redirect it.
293
+
294
+ ### Publish targeting — do not skip this if you hold more than one seat
295
+
296
+ `publish` verifies its target BEFORE packing or submitting anything: it fetches
297
+ your seat list and refuses (`forbidden`, exit 5) a source you hold no seat on,
298
+ naming your actual seats in the hint. If the target came from ekanos.json
299
+ (not `--source` this run) and you hold more than one seat, a scripted/agent
300
+ run (JSON mode, which is what running under an agent means) is refused with
301
+ `validation` (exit 3) and the seat list **unless `--yes` is passed** — publish
302
+ never guesses and never prompts in this mode.
303
+
304
+ **As an agent: never pass `--yes` to paper over that ambiguity.** Run
305
+ `ekanos sources` first and pass `--source <slug>` explicitly, or run
306
+ `ekanos use <slug>` once to set the project's target deliberately. `--yes` is
307
+ for a human who already confirmed the target, or a pipeline pinned to one
308
+ source — not a way to silence the check when you are unsure.
248
309
 
249
310
  Exit codes (frozen contract — branch on these): `0` ok, `1` internal, `2`
250
- usage, `3` validation, `4` auth required (run `ekanos login`), `5` forbidden,
251
- `6` not found, `7` invalid state (e.g. version already submitted bump
252
- `package.json#version`), `8` network (retry, do NOT re-login), `9`
253
- precondition failed, `10` publish gate failed (fix `data.findings`).
311
+ usage, `3` validation (also: ambiguous publish target pass `--source` or
312
+ `--yes`), `4` auth required (run `ekanos login`), `5` forbidden (also: no seat
313
+ on the target source — check `ekanos sources`), `6` not found, `7` invalid
314
+ state (e.g. version already submitted bump `package.json#version`), `8`
315
+ network (retry, do NOT re-login), `9` precondition failed, `10` publish gate
316
+ failed (fix `data.findings`).
254
317
 
255
318
  The loop: `ekanos dev` → edit → `npx tsc --noEmit` → `ekanos validate` →
256
319
  `ekanos test` → `ekanos publish`.
@@ -3,4 +3,5 @@
3
3
  This project is an Ekanos integration for the Fusion platform — read
4
4
  [AGENTS.md](./AGENTS.md) for the full authoring contract before changing code.
5
5
  The toolchain is `npx ekanos <verb>` (init, validate, dev, test, login,
6
- status, publish); every verb supports `--json` and a stable exit-code taxonomy.
6
+ status, publish, upgrade); every verb supports `--json` and a stable
7
+ exit-code taxonomy.