@ekanos/cli 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +81 -2
  2. package/dist/bin.js +14 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/dev.d.ts +4 -0
  5. package/dist/commands/dev.js +6 -20
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/init.d.ts +61 -0
  8. package/dist/commands/init.js +145 -18
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/publish.d.ts +13 -0
  11. package/dist/commands/publish.js +38 -15
  12. package/dist/commands/publish.js.map +1 -1
  13. package/dist/commands/status.js +9 -2
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/upgrade.d.ts +47 -0
  16. package/dist/commands/upgrade.js +546 -0
  17. package/dist/commands/upgrade.js.map +1 -0
  18. package/dist/commands/validate.d.ts +5 -0
  19. package/dist/commands/validate.js +20 -6
  20. package/dist/commands/validate.js.map +1 -1
  21. package/dist/context.d.ts +9 -0
  22. package/dist/context.js +9 -0
  23. package/dist/context.js.map +1 -1
  24. package/dist/copy-tree.d.ts +48 -0
  25. package/dist/copy-tree.js +55 -0
  26. package/dist/copy-tree.js.map +1 -0
  27. package/dist/delegate.d.ts +28 -0
  28. package/dist/delegate.js +136 -0
  29. package/dist/delegate.js.map +1 -0
  30. package/dist/harness-scaffold.d.ts +21 -6
  31. package/dist/harness-scaffold.js +8 -5
  32. package/dist/harness-scaffold.js.map +1 -1
  33. package/dist/index.js +40 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/package-manager.d.ts +10 -0
  36. package/dist/package-manager.js +32 -0
  37. package/dist/package-manager.js.map +1 -1
  38. package/dist/parse-argv.d.ts +7 -1
  39. package/dist/parse-argv.js +10 -2
  40. package/dist/parse-argv.js.map +1 -1
  41. package/dist/project-checks.d.ts +9 -0
  42. package/dist/project-checks.js +29 -0
  43. package/dist/project-checks.js.map +1 -1
  44. package/dist/project.d.ts +23 -0
  45. package/dist/project.js +66 -1
  46. package/dist/project.js.map +1 -1
  47. package/dist/publish-api.d.ts +1 -1
  48. package/dist/publish-api.js +2 -2
  49. package/dist/publish-api.js.map +1 -1
  50. package/dist/safe-fs.d.ts +1 -1
  51. package/dist/safe-fs.js.map +1 -1
  52. package/dist/schema-skew.d.ts +77 -0
  53. package/dist/schema-skew.js +163 -0
  54. package/dist/schema-skew.js.map +1 -0
  55. package/dist/sources-api.d.ts +1 -1
  56. package/dist/sources-api.js.map +1 -1
  57. package/dist/toolchain-api.d.ts +54 -0
  58. package/dist/toolchain-api.js +58 -0
  59. package/dist/toolchain-api.js.map +1 -0
  60. package/dist/toolchain-resolve.d.ts +26 -0
  61. package/dist/toolchain-resolve.js +100 -0
  62. package/dist/toolchain-resolve.js.map +1 -0
  63. package/dist/toolchain.d.ts +21 -0
  64. package/dist/toolchain.js +16 -0
  65. package/dist/toolchain.js.map +1 -0
  66. package/dist/update-notice.d.ts +32 -0
  67. package/dist/update-notice.js +180 -0
  68. package/dist/update-notice.js.map +1 -0
  69. package/dist/validate-findings.d.ts +12 -0
  70. package/dist/validate-findings.js +14 -1
  71. package/dist/validate-findings.js.map +1 -1
  72. package/package.json +1 -1
  73. package/templates/AGENTS.md.tmpl +81 -0
  74. package/templates/CLAUDE.md.tmpl +2 -1
  75. package/templates/claude-skill.md.tmpl +20 -2
  76. package/templates/docs/api-notes.md.tmpl +21 -0
  77. package/templates/mcp.json.tmpl +8 -0
  78. package/templates/skills/build-integration/SKILL.md +178 -0
  79. package/templates/skills/build-integration/references/api-docs-research.md +93 -0
  80. package/templates/skills/build-integration/references/api-wiring.md +94 -0
  81. package/templates/skills/build-integration/references/widget-design.md +132 -0
  82. package/templates/skills/polish-widgets/SKILL.md +139 -0
  83. package/templates/skills/polish-widgets/references/patterns.md +269 -0
@@ -0,0 +1 @@
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[]>;
@@ -1,13 +1,25 @@
1
1
  import { collectCollisionFindings, collectDefinitionFindings, } from '@ekanos/integration-schema';
2
2
  import { preconditionError } from './errors.js';
3
3
  import { loadDefinition } from './load-definition.js';
4
- import { collectProjectFindings } from './project-checks.js';
4
+ import { collectDocsFindings, collectProjectFindings } from './project-checks.js';
5
5
  /**
6
6
  * The full findings pass shared by `validate` (which reports them) and
7
7
  * `publish` (which refuses to submit on any error-severity finding). One
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 = [];
@@ -43,6 +55,7 @@ export async function collectAllFindings(loaded) {
43
55
  // Cross-checks only mean something with more than one definition in hand.
44
56
  findings.push(...collectCollisionFindings(definitions));
45
57
  findings.push(...collectProjectFindings(loaded.projectDir));
58
+ findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));
46
59
  return findings;
47
60
  }
48
61
  /**
@@ -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,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE/E;;;;;;;;;;;;;;;;;;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;IAC5D,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAEzE,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 { collectDocsFindings, 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 findings.push(...collectDocsFindings(loaded.project, 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.5",
3
+ "version": "0.1.7",
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",
@@ -43,6 +43,29 @@ structurally un-settable — the closest you get is the `proposes` block.
43
43
  `ekanos.json` at the project root maps the slug to the entry module. The slug
44
44
  lives in both files; `ekanos validate` reports a finding if they disagree.
45
45
 
46
+ ## API documentation — ground every claim about the target API
47
+
48
+ `ekanos.json` may declare a `docs` array: http(s) URLs and project-relative
49
+ paths pointing at the target API's documentation (an OpenAPI spec, the
50
+ official docs site, a vendored export). `docs/api-notes.md` is this project's
51
+ own structured snapshot of that API — auth model, base URL, key endpoints,
52
+ pagination, rate limits, error shapes, webhooks — written and updated during
53
+ research and kept under version control so a reviewer can see what the
54
+ integration believes. `ekanos validate` warns (`docs/missing-file`, never
55
+ blocking) when a listed local file does not exist.
56
+
57
+ Resolve documentation in this order, and announce which rung you are on: a
58
+ local file from `docs` → an OpenAPI spec URL → the official docs URL → the
59
+ Context7 MCP server (configured in `.mcp.json`) → a web search, announced as
60
+ such.
61
+
62
+ **Never invent an API surface from memory.** If none of those rungs resolves
63
+ to real documentation, stop and ask the developer for a file or URL rather
64
+ than guessing at endpoints, parameters, or error shapes.
65
+
66
+ Codex users (and other agents without `.mcp.json` support) can add Context7 to
67
+ their own MCP configuration to get the same fallback.
68
+
46
69
  ## The capability context (`ctx`) — the ONLY platform surface
47
70
 
48
71
  Every server-side handler (tool `run`, webhook `handler`, schedule `handler`,
@@ -212,6 +235,40 @@ Free set (`pnpm --filter @ekanos/ui check:fa-free-names`, or
212
235
  https://fontawesome.com/search?o=r&s=solid,regular,brands) before relying on
213
236
  it looking like anything in particular.
214
237
 
238
+ ## Live mode — real API calls, locally
239
+
240
+ The harness (`npx ekanos dev`) mocks FUSION, not your vendor. **You do not
241
+ need mock data to develop against the real API**, and never claim otherwise:
242
+ the dev toolbar has a fixtures ⇄ live switch per integration, and in live
243
+ mode your widgets' real query functions run and their requests reach the
244
+ real vendor API. The only gate is the `egress` allowlist from
245
+ `defineIntegration()` — enforced locally by the same matcher production
246
+ uses, so an origin you forgot to declare fails on this machine with
247
+ `EgressDeniedError` instead of in review.
248
+
249
+ Live mode is opt-in per integration. Add a `live` block to the entry in
250
+ `harness.config.ts`:
251
+
252
+ ```ts
253
+ live: {
254
+ // Fusion-side state widgets read before calling anything — what your
255
+ // host route would have returned. Everything NOT seeded runs its real
256
+ // query function against the real network.
257
+ seeds: [{ queryKey: settingsKey, data: MY_SETTINGS }],
258
+ },
259
+ ```
260
+
261
+ Egress is read off the definition automatically — do not repeat it here.
262
+
263
+ What stays mocked either way (the Fusion bits): storage, secrets/vault,
264
+ auth, host `/api/` routes — that is what `seeds` and fixtures are for.
265
+ Boundaries to know: live-mode widget requests come from the BROWSER, so a
266
+ vendor that blocks browser origins (CORS) or needs a server-held secret
267
+ must be exercised through handlers instead; and the Triggers surface runs
268
+ webhook/schedule/onActivate handlers against the mock context (its
269
+ `ctx.fetch` never touches the network) — cover handler network behavior
270
+ with fixtures in tests, then for real on the dev host after publishing.
271
+
215
272
  ## Testing
216
273
 
217
274
  `@ekanos/sdk/testing` builds the same context production uses, in memory —
@@ -242,6 +299,14 @@ with transport semantics (payload validated first, signature skip logged) and
242
299
  derive the context from the definition. Reuse one context across invocations
243
300
  via `options.context` to accumulate state.
244
301
 
302
+ A global install (`npm i -g @ekanos/cli`) is fine and convenient: inside a
303
+ project, `validate`/`dev`/`publish`/`test` automatically delegate to THIS
304
+ project's own pinned `@ekanos/cli` devDependency whenever it differs from the
305
+ invoked binary (the same pattern as the Gradle wrapper or `npx`) — one global
306
+ binary on PATH, but the version that actually reads your definition is always
307
+ the one this project pins. That is also why the scaffold's local
308
+ `@ekanos/cli` devDependency stays mandatory even with a global install.
309
+
245
310
  ## The iteration loop (`npx ekanos <verb>`)
246
311
 
247
312
  Every verb speaks a JSON envelope on stdout with `--json` (auto-on when piped):
@@ -259,6 +324,22 @@ carries an imperative `hint`; treat hints as remediation instructions.
259
324
  | `ekanos sources` | List every Fusion source you hold a developer/admin seat on, marking this project's current target |
260
325
  | `ekanos use <source-slug>` | Set this project's publish target — refuses (`forbidden`) a slug you hold no seat on |
261
326
  | `ekanos publish` | Validate, pack (whitelist: ekanos.json, package.json, README.md, src/), verify the target, and submit. Refuses on any error-severity finding |
327
+ | `ekanos upgrade` | Move this project's toolchain to a coherent target version, with verify-and-rollback |
328
+
329
+ ### Keeping the toolchain current
330
+
331
+ `validate`/`dev`/`publish` all bundle an EXACT `@ekanos/integration-schema`
332
+ version, and a project can end up NEWER than that (a `pnpm up` on
333
+ `@ekanos/sdk` alone is enough). In that state `validate`'s findings are
334
+ unreliable — a field the newer schema added parses here as an unrecognized
335
+ key, and it is easy to "fix" that by deleting perfectly correct code. If a
336
+ finding smells like that (an unrecognized key on a field you are confident is
337
+ right), run `ekanos upgrade --check` first: it is READ-ONLY and always safe —
338
+ it only reports current vs. target versions and never touches the project. A
339
+ bare `ekanos upgrade` (no flags) MUTATES the project: it installs the target
340
+ versions, verifies with `ekanos validate` and this project's own test script,
341
+ and rolls everything back automatically if either fails. Never run a bare
342
+ `ekanos upgrade` as a way to "try and see" — use `--check` for that.
262
343
 
263
344
  Host resolution for `logout`/`whoami`/`status`/`sources`/`use`/`publish`:
264
345
  `--host` flag → `EKANOS_HOST` → the `host` field in ekanos.json → the sole
@@ -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.
@@ -10,6 +10,13 @@ rules, widget and testing patterns — lives in **AGENTS.md** at the project
10
10
  root. Read it before writing integration code. The definition is one
11
11
  `defineIntegration()` in `src/integration.ts`, exported as `integration`.
12
12
 
13
+ For the full authoring workflow — interview, API research, widgets, wiring,
14
+ publish — use the `build-integration` skill. For a styling review of widgets
15
+ that already exist (plain/flat widgets, overflow, a bare marketplace tile),
16
+ use the `polish-widgets` skill. The target API's contract lives
17
+ in `docs/api-notes.md`: write API client code from that file, and update it
18
+ in the same commit when the live API disagrees.
19
+
13
20
  ## The loop
14
21
 
15
22
  1. `npx ekanos dev` — local harness (only when you need to see UI).
@@ -27,6 +34,16 @@ root. Read it before writing integration code. The definition is one
27
34
  while any error-severity finding remains, and refuses `forbidden` if you
28
35
  hold no seat on that source).
29
36
 
37
+ ## Keeping the toolchain current
38
+
39
+ If a `validate` finding smells like version skew (an unrecognized-key error on
40
+ a field you're confident is correct), run `npx ekanos upgrade --check` —
41
+ read-only, always safe, reports current vs. target versions and changes
42
+ nothing. A bare `npx ekanos upgrade` MUTATES the project (installs the target
43
+ toolchain, verifies with validate + the test script, rolls back automatically
44
+ on any failure). Never run the bare form just to "see what happens" — use
45
+ `--check` for that.
46
+
30
47
  ## Reading CLI output
31
48
 
32
49
  Every verb emits ONE JSON envelope on stdout when piped (or with `--json`):
@@ -42,8 +59,9 @@ Exit codes to branch on: `0` ok · `2` usage · `3` validation (also: ambiguous
42
59
  publish target — pass `--source` or `--yes`, never guess) · `4` run
43
60
  `ekanos login` · `5` forbidden (also: no seat on the target source — run
44
61
  `ekanos sources`) · `7` version already submitted (bump `package.json#version`) ·
45
- `8` network (retry — do NOT re-login) · `9` precondition · `10` fix findings,
46
- publish again.
62
+ `8` network (retry — do NOT re-login) · `9` precondition (also: the project's
63
+ `@ekanos/integration-schema` is newer than this CLI bundles — run
64
+ `ekanos upgrade`) · `10` fix findings, publish again.
47
65
 
48
66
  ## Hosts and sessions
49
67
 
@@ -0,0 +1,21 @@
1
+ # __DISPLAY_NAME__ — API notes
2
+
3
+ This file is the structured snapshot of the target API: what this integration
4
+ currently believes about the upstream service. It is written and updated by
5
+ the build-integration skill's Research phase, and it is versioned so a
6
+ reviewer can see exactly what the integration assumes.
7
+
8
+ As the authoring agent, fill in and maintain the sections below from real
9
+ documentation (the `docs` entries in `ekanos.json`, an OpenAPI spec, the
10
+ official docs site) — never from memory. Record:
11
+
12
+ - **Auth model** — scheme, token lifetime, where the credential comes from.
13
+ - **Base URL(s)** — per environment, and what `egress` must allowlist.
14
+ - **Key endpoints** — method, path, purpose, request/response shape.
15
+ - **Pagination** — parameter names, page-size limits, cursor semantics.
16
+ - **Rate limits** — the numeric limits and the backoff the API expects.
17
+ - **Error shapes** — status codes, error body structure, retryable or not.
18
+ - **Webhooks** — events, payloads, signature scheme, retry behaviour.
19
+
20
+ Update this file in the same change as the code that depends on the new
21
+ knowledge. If the API contradicts these notes, the notes are wrong — fix them.
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "context7": {
4
+ "command": "npx",
5
+ "args": ["-y", "@upstash/context7-mcp"]
6
+ }
7
+ }
8
+ }