@ekanos/cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/commands/init.d.ts +36 -0
- package/dist/commands/init.js +126 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.js +108 -7
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/copy-tree.d.ts +48 -0
- package/dist/copy-tree.js +55 -0
- package/dist/copy-tree.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parse-argv.d.ts +7 -1
- package/dist/parse-argv.js +10 -2
- package/dist/parse-argv.js.map +1 -1
- package/dist/project-checks.d.ts +9 -0
- package/dist/project-checks.js +29 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +23 -0
- package/dist/project.js +66 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +1 -1
- package/dist/publish-api.js +2 -2
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.d.ts +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/sources-api.d.ts +1 -1
- package/dist/sources-api.js.map +1 -1
- package/dist/validate-findings.js +2 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +57 -0
- package/templates/claude-skill.md.tmpl +7 -0
- package/templates/docs/api-notes.md.tmpl +21 -0
- package/templates/mcp.json.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +178 -0
- package/templates/skills/build-integration/references/api-docs-research.md +93 -0
- package/templates/skills/build-integration/references/api-wiring.md +94 -0
- package/templates/skills/build-integration/references/widget-design.md +132 -0
- package/templates/skills/polish-widgets/SKILL.md +139 -0
- package/templates/skills/polish-widgets/references/patterns.md +269 -0
|
@@ -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;
|
|
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.
|
|
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",
|
package/templates/AGENTS.md.tmpl
CHANGED
|
@@ -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 —
|
|
@@ -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).
|
|
@@ -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,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-integration
|
|
3
|
+
description: Interview-driven workflow for building and publishing a Fusion integration with the Ekanos SDK. Use when building a new integration from scratch ("build an integration for X", "connect a service like Stripe or Slack", a fresh ekanos scaffold with no substance yet), when adding or changing API wiring ("add an endpoint", "wire this to the API", "add a webhook/tool/schedule"), when researching the target service's API ("how does this API paginate", "what auth does it use", "where are the docs"), or when publishing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a Fusion Integration
|
|
7
|
+
|
|
8
|
+
Guide the user from intent to a published integration. The user supplies
|
|
9
|
+
judgment (what service, what it should show and do); the CLI supplies
|
|
10
|
+
determinism (scaffold, validate, test, publish). Never hand-roll what a verb
|
|
11
|
+
does; never guess at platform APIs — the contract is in `AGENTS.md` at the
|
|
12
|
+
project root. Read it before writing any integration code. The integration's
|
|
13
|
+
identity is the `slug` in `ekanos.json`; the definition is one
|
|
14
|
+
`defineIntegration()` in `src/integration.ts`.
|
|
15
|
+
|
|
16
|
+
Every `ekanos` verb supports `--json` and exits nonzero on failure. Validate
|
|
17
|
+
findings carry a `hint` that is a remediation instruction: follow it.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
1. **Interview** — establish the plan and the docs, get approval
|
|
22
|
+
2. **Research** — ground the target API in its real docs (see `references/api-docs-research.md`)
|
|
23
|
+
3. **Declare** — write the `defineIntegration` skeleton, validate clean
|
|
24
|
+
4. **Widgets** — design-first, all four states (see `references/widget-design.md`)
|
|
25
|
+
5. **Wire** — activation → secrets → `ctx.fetch`/storage → tools/webhooks/schedules (see `references/api-wiring.md`)
|
|
26
|
+
6. **Verify** — validate + test + human look at the harness
|
|
27
|
+
7. **Publish** — checklist, explicit human approval, `ekanos publish`
|
|
28
|
+
|
|
29
|
+
Work one phase at a time. After each phase, state in one line what was done
|
|
30
|
+
and what is next.
|
|
31
|
+
|
|
32
|
+
## Phase 1: Interview
|
|
33
|
+
|
|
34
|
+
Ask in order, ONE message per step, skipping anything the user already said.
|
|
35
|
+
Use AskUserQuestion for the multiple-choice steps when the tool is available;
|
|
36
|
+
otherwise ask in plain prose with lettered options.
|
|
37
|
+
|
|
38
|
+
1. **The service** (free text): which target service or API? Whether they
|
|
39
|
+
already have an account / API key / sandbox.
|
|
40
|
+
2. **The docs** (free text): where does the target API's documentation live?
|
|
41
|
+
Prefer, in this order:
|
|
42
|
+
- (a) an **OpenAPI/Swagger spec URL** — the gold standard: exact paths,
|
|
43
|
+
params, and auth schemes with no prose ambiguity;
|
|
44
|
+
- (b) an official docs URL;
|
|
45
|
+
- (c) a local file the developer drops into `docs/`.
|
|
46
|
+
|
|
47
|
+
If `ekanos.json` already has a `docs` array (set via `ekanos init
|
|
48
|
+
--docs`), confirm those entries instead of re-asking. Whatever the
|
|
49
|
+
developer supplies, WRITE it into `ekanos.json`'s `docs` array (http(s)
|
|
50
|
+
URLs or repo-relative paths) so it is versioned with the project.
|
|
51
|
+
|
|
52
|
+
3. **The outcomes** (free text): "What should a team see on their dashboard,
|
|
53
|
+
and what should the integration be able to do?" Push for 1–3 concrete
|
|
54
|
+
outcomes ("show open invoices", "alert on new disputes", "let the
|
|
55
|
+
assistant create a customer"). Each SEE-outcome becomes a widget; each
|
|
56
|
+
DO-outcome becomes a tool, webhook, or schedule.
|
|
57
|
+
4. **Auth** (choice): API key / Bearer / OAuth 2.0 / Basic / not sure
|
|
58
|
+
(research will settle it). This decides the activation-form fields — and
|
|
59
|
+
every string field name on that form IS a secret name (see AGENTS.md).
|
|
60
|
+
5. **Freshness** (choice): live fetch on view / cached with a refresh
|
|
61
|
+
schedule / pushed by webhooks. This decides storage keys and schedules.
|
|
62
|
+
|
|
63
|
+
Then present **the plan** as a short table — widgets (name, what it shows),
|
|
64
|
+
activation fields (exact names), egress origins, storage keys (with data-type
|
|
65
|
+
prefixes), tools/webhooks/schedules — and get an explicit yes before writing
|
|
66
|
+
code. The plan is the contract for the rest of the session; note any change
|
|
67
|
+
the user makes to it later.
|
|
68
|
+
|
|
69
|
+
## Phase 2: Research
|
|
70
|
+
|
|
71
|
+
Read `references/api-docs-research.md` before this phase — it carries the
|
|
72
|
+
mechanics: how to read a spec without ingesting it whole, what to extract
|
|
73
|
+
from prose, when Context7 helps, and the notes-file skeleton.
|
|
74
|
+
|
|
75
|
+
Resolve documentation down this ladder — stop at the first rung that answers
|
|
76
|
+
the question at hand:
|
|
77
|
+
|
|
78
|
+
1. **Local files** listed in `ekanos.json`'s `docs` array — read them.
|
|
79
|
+
2. **OpenAPI/Swagger spec URL** — fetch it; prefer it over prose docs.
|
|
80
|
+
3. **Official docs URL** — fetch and extract.
|
|
81
|
+
4. **Context7 MCP** (available via this project's `.mcp.json`) — best for
|
|
82
|
+
questions about the service's official client libraries and SDKs, not
|
|
83
|
+
for raw REST surfaces.
|
|
84
|
+
5. **Web search** — last resort, and SAY you are falling back to search.
|
|
85
|
+
|
|
86
|
+
**Never invent an API surface from memory alone.** If nothing on the ladder
|
|
87
|
+
resolves, stop and ask the developer for a file or URL rather than
|
|
88
|
+
proceeding.
|
|
89
|
+
|
|
90
|
+
Write findings into `docs/api-notes.md` (the scaffold ships a stub): auth
|
|
91
|
+
model, base URL(s), the endpoints each planned outcome needs (method, path,
|
|
92
|
+
key params), pagination scheme, rate limits, error shapes, webhook/event
|
|
93
|
+
surfaces if planned. One reason it is a file and not a comment: it is
|
|
94
|
+
versioned, reviewable, and survives docs-site rot — later sessions read it
|
|
95
|
+
instead of re-researching. Correct the plan if research contradicts it and
|
|
96
|
+
tell the user what changed.
|
|
97
|
+
|
|
98
|
+
## Phase 3: Declare
|
|
99
|
+
|
|
100
|
+
Write the `defineIntegration` skeleton per AGENTS.md: metadata, `egress`
|
|
101
|
+
(only the origins from the plan), `storage` schemas (declared keys only,
|
|
102
|
+
data-type prefixes from the closed vocabulary), empty-but-typed widgets/tools
|
|
103
|
+
lists. Then:
|
|
104
|
+
|
|
105
|
+
npx ekanos validate
|
|
106
|
+
|
|
107
|
+
Zero findings before moving on. Re-run after EVERY change to the declaration
|
|
108
|
+
for the rest of the session — it is fast and its findings are instructions.
|
|
109
|
+
|
|
110
|
+
## Phase 4: Widgets — design first
|
|
111
|
+
|
|
112
|
+
Read `references/widget-design.md` before the first widget. Build each widget
|
|
113
|
+
with realistic mock data first, make it distinctive and polished, implement
|
|
114
|
+
all four states (loading / empty / error / success), THEN wire real data.
|
|
115
|
+
Check rendering in the harness:
|
|
116
|
+
|
|
117
|
+
npx ekanos dev
|
|
118
|
+
|
|
119
|
+
and ask the user to look at http://localhost:3100 at each significant visual
|
|
120
|
+
change — do not claim a widget looks good without either a screenshot or the
|
|
121
|
+
user's confirmation. A review-and-fix pass over widgets that already exist
|
|
122
|
+
(flat rows, no hierarchy, overflow, a bare tile) is the `polish-widgets`
|
|
123
|
+
skill — use it here as the final styling pass and again before publish.
|
|
124
|
+
|
|
125
|
+
## Phase 5: Wire
|
|
126
|
+
|
|
127
|
+
Read `references/api-wiring.md`. Work from `docs/api-notes.md`, not
|
|
128
|
+
conversational memory — every endpoint, header, and param in client code
|
|
129
|
+
must trace to a line in the notes. When the live API disagrees with the
|
|
130
|
+
notes, update `docs/api-notes.md` in the same commit as the code fix.
|
|
131
|
+
|
|
132
|
+
Order: activation form (field names = secret names, keep them in exported
|
|
133
|
+
constants) → server handlers using `ctx.fetch` / `ctx.storage` /
|
|
134
|
+
`ctx.secrets` only → tools → webhooks/schedules. Write tests alongside each
|
|
135
|
+
handler with `createMockContext` / `invokeWebhook` / `invokeSchedule` from
|
|
136
|
+
`@ekanos/sdk/testing`, and run:
|
|
137
|
+
|
|
138
|
+
npx ekanos test
|
|
139
|
+
|
|
140
|
+
## Phase 6: Verify
|
|
141
|
+
|
|
142
|
+
All of these, in order, all green:
|
|
143
|
+
|
|
144
|
+
1. `npx ekanos validate` — zero findings
|
|
145
|
+
2. `npx ekanos test` — all passing, including at least one test per handler
|
|
146
|
+
and one failure-path test per external call
|
|
147
|
+
3. Harness check — every widget renders all four states (mock the states if
|
|
148
|
+
needed); user has seen and approved the visuals
|
|
149
|
+
4. Grep your own diff for leaks: no credential in `ctx.storage`, `configData`,
|
|
150
|
+
logs, or tool results; no global `fetch`/`process.env`/`node:fs`
|
|
151
|
+
|
|
152
|
+
## Phase 7: Publish — gated
|
|
153
|
+
|
|
154
|
+
Publishing sends the project to the Fusion host and is versioned immutably.
|
|
155
|
+
NEVER run it on your own judgment. Show the user:
|
|
156
|
+
|
|
157
|
+
- the verify results (1–4 above)
|
|
158
|
+
- `package.json` version (bump it if this slug+version was already published)
|
|
159
|
+
- the target host and source (from ekanos.json, or ask)
|
|
160
|
+
|
|
161
|
+
and ask plainly: "Ready to publish?" Only on an explicit yes:
|
|
162
|
+
|
|
163
|
+
npx ekanos publish
|
|
164
|
+
|
|
165
|
+
If it exits 4 (auth required), the user must run `npx ekanos login` — the
|
|
166
|
+
device flow needs their browser; wait for them. On success, report the
|
|
167
|
+
submission id and state, and mention `npx ekanos status` shows it from now
|
|
168
|
+
on. On `invalid_state` (exit 7), the version exists — bump `package.json`
|
|
169
|
+
and re-confirm before retrying.
|
|
170
|
+
|
|
171
|
+
## Failure etiquette
|
|
172
|
+
|
|
173
|
+
- A validate finding, test failure, or nonzero verb exit is never something
|
|
174
|
+
to narrate past — fix it or surface it.
|
|
175
|
+
- If the same error survives two fix attempts, stop and show the user the
|
|
176
|
+
exact output.
|
|
177
|
+
- Never edit files under `.ekanos/` (generated) or weaken a test to get to
|
|
178
|
+
green.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Researching the target API
|
|
2
|
+
|
|
3
|
+
Read this at Phase 2 (Research), before fetching anything. The ladder and the
|
|
4
|
+
hard rule live in SKILL.md; this file is the mechanics — how to work each
|
|
5
|
+
rung and what to leave behind in `docs/api-notes.md`.
|
|
6
|
+
|
|
7
|
+
## Reading an OpenAPI/Swagger spec efficiently
|
|
8
|
+
|
|
9
|
+
Do not ingest the whole spec — production specs run to megabytes and most of
|
|
10
|
+
it is endpoints the plan will never touch. Read in this order:
|
|
11
|
+
|
|
12
|
+
1. `servers` (or Swagger 2.0 `host` + `basePath`) — the real base URL(s),
|
|
13
|
+
including environment variants (sandbox vs production).
|
|
14
|
+
2. `components.securitySchemes` (2.0: `securityDefinitions`) plus top-level
|
|
15
|
+
`security` — the auth model and the EXACT header/query shape. This settles
|
|
16
|
+
the interview's "not sure" auth answer.
|
|
17
|
+
3. Only the paths the plan needs: search the spec for the resource nouns from
|
|
18
|
+
the plan's outcomes, then read those operations fully — method, path
|
|
19
|
+
template, required params, request/response schemas, error responses.
|
|
20
|
+
4. Pagination: look at the list operations' params (`cursor`, `page`,
|
|
21
|
+
`limit`, `starting_after`) and response envelopes (`next`, `has_more`,
|
|
22
|
+
`Link` header) — pagination is per-operation, never assume one scheme.
|
|
23
|
+
|
|
24
|
+
If the spec is JSON, `jq` the sections above rather than reading linearly.
|
|
25
|
+
Note the spec's own `info.version` and the URL it was fetched from in the
|
|
26
|
+
notes — that is the line future sessions use to detect drift.
|
|
27
|
+
|
|
28
|
+
## Extracting from prose docs
|
|
29
|
+
|
|
30
|
+
Capture: base URL, auth header shape (copy a verbatim example request),
|
|
31
|
+
endpoint tables (method, path, required params), pagination scheme, documented
|
|
32
|
+
rate limits and the 429 behavior, error response body shape, webhook event
|
|
33
|
+
names and signature scheme. Copy exact strings — paths, header names, event
|
|
34
|
+
names — never paraphrase them.
|
|
35
|
+
|
|
36
|
+
Skip: marketing pages, quickstart tutorials, SDK installation guides,
|
|
37
|
+
changelog prose. Watch for versioned base URLs (`/v1/` vs `/v2/`) — docs
|
|
38
|
+
sites routinely show both, and mixing them is a classic silent 404.
|
|
39
|
+
|
|
40
|
+
## Using Context7
|
|
41
|
+
|
|
42
|
+
Context7 (via this project's `.mcp.json`) serves indexed documentation for
|
|
43
|
+
libraries and SDKs. It is good for: the target service's OFFICIAL client
|
|
44
|
+
library — method signatures, initialization, idiomatic usage — and for
|
|
45
|
+
questions about packages the integration depends on. It is NOT an authority
|
|
46
|
+
on raw REST surfaces: when Context7's account of an endpoint disagrees with
|
|
47
|
+
the OpenAPI spec or official docs, the spec wins. If the integration calls
|
|
48
|
+
the REST API directly (the common case — `ctx.fetch` means no vendor SDK),
|
|
49
|
+
Context7 is a supplement, not a source.
|
|
50
|
+
|
|
51
|
+
## The `docs/api-notes.md` structure
|
|
52
|
+
|
|
53
|
+
The scaffold ships a stub; grow it into this shape (drop sections that do not
|
|
54
|
+
apply, keep the order):
|
|
55
|
+
|
|
56
|
+
# API notes: <service>
|
|
57
|
+
Sources: <spec/docs URLs + fetch date>
|
|
58
|
+
|
|
59
|
+
## Auth
|
|
60
|
+
<scheme; exact header shape, e.g. `Authorization: Bearer <key>`>
|
|
61
|
+
|
|
62
|
+
## Base URLs
|
|
63
|
+
<production; sandbox if any>
|
|
64
|
+
|
|
65
|
+
## Endpoints
|
|
66
|
+
| Outcome | Method | Path | Key params |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| open invoices widget | GET | /v1/invoices | status, limit, cursor |
|
|
69
|
+
|
|
70
|
+
## Pagination
|
|
71
|
+
<scheme, per-operation exceptions>
|
|
72
|
+
|
|
73
|
+
## Rate limits
|
|
74
|
+
<documented limits; 429 behavior, Retry-After>
|
|
75
|
+
|
|
76
|
+
## Errors
|
|
77
|
+
<error body shape; which statuses are retryable>
|
|
78
|
+
|
|
79
|
+
## Webhooks
|
|
80
|
+
<events used, signature header + scheme, retry behavior>
|
|
81
|
+
|
|
82
|
+
Every endpoint the Wire phase touches must have a row here first. Keep it a
|
|
83
|
+
snapshot, not a mirror — only what THIS integration uses.
|
|
84
|
+
|
|
85
|
+
## When the live API contradicts the notes
|
|
86
|
+
|
|
87
|
+
A 404 on a noted path, an auth header that bounces, a response missing a
|
|
88
|
+
noted field: believe the live API. Update `docs/api-notes.md` in the SAME
|
|
89
|
+
commit as the code fix — a note that disagrees with shipped code is worse
|
|
90
|
+
than no note — and record what was observed and when, so the correction is
|
|
91
|
+
not "fixed back" from the stale docs site later. If the contradiction changes
|
|
92
|
+
the plan (an endpoint gone, auth scheme different), surface it to the user
|
|
93
|
+
before wiring around it.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Wiring an integration to its target API
|
|
2
|
+
|
|
3
|
+
Read this at Phase 5 (Wire). The binding contract (ctx surface, storage
|
|
4
|
+
vocabulary, secret naming) is `AGENTS.md`; the target API's contract is
|
|
5
|
+
`docs/api-notes.md` — write client code from the notes, not from memory, and
|
|
6
|
+
when the live API contradicts them, fix the notes in the same commit as the
|
|
7
|
+
code. This file is the order of work and the traps.
|
|
8
|
+
|
|
9
|
+
## Order of work
|
|
10
|
+
|
|
11
|
+
1. **Constants first** — `src/config.ts` exporting every name that crosses a
|
|
12
|
+
boundary: secret names (= activation field names), storage keys, webhook
|
|
13
|
+
ids, egress origins. Both the form and every handler import from here;
|
|
14
|
+
a renamed form field silently orphans its secret otherwise.
|
|
15
|
+
2. **Activation form** — zod schema + react-hook-form inside
|
|
16
|
+
`BaseActivationForm` (see AGENTS.md). Every string field the user submits
|
|
17
|
+
becomes `ctx.secrets.get('<field name>')`. Validate the credential shape
|
|
18
|
+
in the schema (prefix, length) so a paste error fails at the form, not in
|
|
19
|
+
a handler.
|
|
20
|
+
3. **A thin API client** — one module owning base URL, auth header assembly,
|
|
21
|
+
response parsing, and error mapping, taking `ctx` (or just
|
|
22
|
+
`{ fetch, secrets }`) as an argument. Handlers never call `ctx.fetch`
|
|
23
|
+
directly for endpoints the client covers.
|
|
24
|
+
4. **Handlers** — tools, then webhooks, then schedules, each with tests as
|
|
25
|
+
they are written (`createMockContext`, `invokeWebhook`, `invokeSchedule`
|
|
26
|
+
from `@ekanos/sdk/testing`).
|
|
27
|
+
5. **Widget data** — widgets cannot call the target API (no client-side
|
|
28
|
+
`ctx`). The pattern: a schedule (or webhook) fetches and writes a
|
|
29
|
+
`clientReadable` storage key; the widget reads it back with
|
|
30
|
+
`fetchIntegrationStorage()` from `@ekanos/sdk/hooks`. Cache freshness in
|
|
31
|
+
the stored value (`fetchedAt`) and show it in the UI when staleness
|
|
32
|
+
matters.
|
|
33
|
+
|
|
34
|
+
## Traps, each one earned
|
|
35
|
+
|
|
36
|
+
- **Egress is an allowlist, checked synchronously.** Every origin the client
|
|
37
|
+
touches must be in `egress` — including the OAuth token host and any host a
|
|
38
|
+
redirect lands on. A missing origin is a thrown error, not a warning.
|
|
39
|
+
- **Secrets never leave the secrets surface.** Not into `ctx.storage`, not
|
|
40
|
+
into logs, not into a tool result, not into a widget's stored value. Token
|
|
41
|
+
rotation: `ctx.secrets.set()` (account tier only).
|
|
42
|
+
- **`ctx.storage.user` throws for machine actors** (workflow runs). A tool
|
|
43
|
+
that touches user storage must branch on `ctx.actor.kind` or stick to
|
|
44
|
+
account storage.
|
|
45
|
+
- **Webhook signatures are declared, not hand-verified** — `signature:
|
|
46
|
+
{ header, secretName }` in the declaration; the transport verifies before
|
|
47
|
+
the handler runs. Return `{ status: 'ignored' }` for valid-but-irrelevant
|
|
48
|
+
deliveries so the sender stops retrying; throw only for retryable faults.
|
|
49
|
+
- **Handlers run under redelivery and "Run now".** Make writes idempotent
|
|
50
|
+
(stable keys, upsert semantics); `invocation.trigger` distinguishes cron
|
|
51
|
+
from manual.
|
|
52
|
+
- **Schedules are 5-field numeric cron** — no names, no `@daily`.
|
|
53
|
+
- **Rate limits**: respect `Retry-After` on 429 in the client; a schedule
|
|
54
|
+
that hammers a free-tier API takes every tenant down with it.
|
|
55
|
+
- **Storage schema evolution**: a stored value that no longer parses throws
|
|
56
|
+
on read — version the key or widen with a union; never edit a schema to a
|
|
57
|
+
shape old rows can't satisfy.
|
|
58
|
+
- **Cache rows must know what they were built from.** A cache keyed by
|
|
59
|
+
nothing and invalidated only by time serves the PREVIOUS configuration
|
|
60
|
+
after the user changes activation data (old city, old account, old list)
|
|
61
|
+
until the next schedule tick — confidently wrong answers. Embed a config
|
|
62
|
+
fingerprint in the stored value (the activation fields the fetch depended
|
|
63
|
+
on) and treat a mismatch on read as a cache miss. Tools reading the cache
|
|
64
|
+
must check the fingerprint, not just row freshness. Three details that are
|
|
65
|
+
each easy to get wrong:
|
|
66
|
+
- Include the fields that change the VALUES, not just the ones that select
|
|
67
|
+
the data — units, currency, locale, timezone. They leave the identity
|
|
68
|
+
byte-identical while making every number wrong.
|
|
69
|
+
- Stabilize noisy members (round coordinates, normalize case) so the
|
|
70
|
+
fingerprint doesn't churn under inputs the answer doesn't depend on.
|
|
71
|
+
- Declare the fingerprint field `.optional()` and treat ABSENT as a miss.
|
|
72
|
+
A required field makes every pre-existing row throw on read (see the
|
|
73
|
+
schema-evolution trap above) — and for a `clientReadable` key that throw
|
|
74
|
+
reaches the widgets, turning a cache improvement into a dashboard
|
|
75
|
+
outage. Optional-plus-absent-is-stale degrades to a refetch instead.
|
|
76
|
+
|
|
77
|
+
## Test floor per handler
|
|
78
|
+
|
|
79
|
+
- happy path against `createMockContext` with seeded secrets + programmed
|
|
80
|
+
fetch responses
|
|
81
|
+
- one failure path (API 500/timeout) proving the handler returns/throws the
|
|
82
|
+
designed way rather than leaking the raw error
|
|
83
|
+
- for webhooks: one `ignored` case; for schedules: one idempotency case
|
|
84
|
+
(same invocation twice, no duplicate writes)
|
|
85
|
+
|
|
86
|
+
## Verifying against the real API — live mode
|
|
87
|
+
|
|
88
|
+
The harness mocks Fusion, never the vendor. Do not tell the developer that
|
|
89
|
+
local work is mock-only: opt the integration into live mode (a `live` block
|
|
90
|
+
on its `harness.config.ts` entry — see AGENTS.md "Live mode") and the
|
|
91
|
+
toolbar's fixtures ⇄ live switch runs real widget queries against the real
|
|
92
|
+
API, gated only by the declared `egress`. Use fixtures for the Fusion-side
|
|
93
|
+
state widgets need first, and for webhook/schedule handlers (the Triggers
|
|
94
|
+
surface runs those against the mock context by design).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Widget design for Ekanos integrations
|
|
2
|
+
|
|
3
|
+
Read this before building the first widget. The mechanics (declaration shape,
|
|
4
|
+
`Widget.*` API, `WidgetContext`, preview) are in `AGENTS.md` and the
|
|
5
|
+
`@ekanos/sdk` README §`components.widgets` — this file is the design judgment.
|
|
6
|
+
|
|
7
|
+
## Philosophy: form over function
|
|
8
|
+
|
|
9
|
+
Build the widget beautiful with realistic MOCK data first; wire real data
|
|
10
|
+
last. A widget that renders nothing interesting with perfect plumbing is a
|
|
11
|
+
failure; the reverse is a demo. Sequence per widget:
|
|
12
|
+
|
|
13
|
+
1. Distinctive UI with hardcoded, realistic mock data
|
|
14
|
+
2. All four states (loading / empty / error / success)
|
|
15
|
+
3. Real data via the wiring reference
|
|
16
|
+
4. A final styling pass against the checklist below
|
|
17
|
+
|
|
18
|
+
"Distinctive" means: if the mock data were swapped for another service's, the
|
|
19
|
+
widget should NOT still look right. Lead with the one number, chart, or list
|
|
20
|
+
the user said matters (the interview's SEE-outcome); trim everything that
|
|
21
|
+
does not serve it.
|
|
22
|
+
|
|
23
|
+
## The key question — decide it before writing JSX
|
|
24
|
+
|
|
25
|
+
Every widget answers 1–2 questions, and the answer must be the most prominent
|
|
26
|
+
element on it — size, weight, position. Everything else is supporting context
|
|
27
|
+
and recedes (`text-muted-foreground`, smaller). Name the question from the
|
|
28
|
+
interview's SEE-outcome before writing the first class; a widget you cannot
|
|
29
|
+
name the question for is a widget you cannot design.
|
|
30
|
+
|
|
31
|
+
| Widget type | Key question | Most prominent element |
|
|
32
|
+
| ------------------- | ---------------------------------- | --------------------------------------------------------------------- |
|
|
33
|
+
| KPI / stat | "How are we doing?" | The hero number: `text-3xl font-semibold tracking-tight tabular-nums` |
|
|
34
|
+
| List / feed | "What needs my attention?" | Item name `text-base font-medium` + its value, right-aligned |
|
|
35
|
+
| Chart | "Which way is it going?" | The chart + current value; axes and legends muted |
|
|
36
|
+
| Action / connection | "What can I do / who's connected?" | One clear CTA or the identity + status |
|
|
37
|
+
|
|
38
|
+
The default failure is uniform weight: everything `text-sm`, values dressed
|
|
39
|
+
like labels. Squint at the render — if nothing reads first, there is no
|
|
40
|
+
hierarchy, and no amount of padding will save it.
|
|
41
|
+
|
|
42
|
+
## Layout recipes (the shapes that work)
|
|
43
|
+
|
|
44
|
+
**KPI / stat card** — one hero, a delta, supporting stats demoted:
|
|
45
|
+
|
|
46
|
+
- Center block: label `text-sm text-muted-foreground`, then the hero value
|
|
47
|
+
`text-3xl font-semibold tracking-tight tabular-nums`, then a delta line
|
|
48
|
+
`text-sm font-medium` in `text-success-strong` / `text-destructive`.
|
|
49
|
+
- Supporting stats as a `border-t` footer strip: per-stat
|
|
50
|
+
`text-lg font-semibold` over a `text-xs text-muted-foreground` label,
|
|
51
|
+
columns separated by `bg-border h-8 w-px`.
|
|
52
|
+
- One `text-3xl` element per widget — two heroes is zero heroes.
|
|
53
|
+
|
|
54
|
+
**List / feed** — rows with a first read:
|
|
55
|
+
|
|
56
|
+
- Container `divide-y`; each row `flex items-center gap-3 px-4 py-3`.
|
|
57
|
+
- Name: `min-w-0 flex-1` wrapping `truncate text-base font-medium`, with an
|
|
58
|
+
optional `text-sm text-muted-foreground` second line.
|
|
59
|
+
- Value: `shrink-0 text-right text-base font-semibold tabular-nums`.
|
|
60
|
+
- 5–7 rows visible; the rest behind inner scroll or a "View all" ghost button.
|
|
61
|
+
- One status treatment per row: a `Badge` variant (`success` / `warning` /
|
|
62
|
+
`info` / `destructive`) or an `h-2.5 w-2.5` semantic dot — never both.
|
|
63
|
+
|
|
64
|
+
**Chart** — the picture is the point:
|
|
65
|
+
|
|
66
|
+
- Chart fills the body; current value as a `text-2xl font-semibold` readout
|
|
67
|
+
above or beside it, axes/gridlines/legend all muted.
|
|
68
|
+
- Series colors from chart tokens (`--chart-1`…), never hand-picked palette.
|
|
69
|
+
|
|
70
|
+
## The skeleton
|
|
71
|
+
|
|
72
|
+
Every widget: `'use client'`, `Widget.DataState` wrapping `Widget.Card` →
|
|
73
|
+
`Widget.Header` + `Widget.Loading` + `Widget.Error` + `Widget.Active`. Gate
|
|
74
|
+
fetching on `ctx?.state.state === 'active'` from `WidgetContext`. Copy the
|
|
75
|
+
canonical example from the SDK README rather than improvising the tree.
|
|
76
|
+
|
|
77
|
+
## Four states, no exceptions
|
|
78
|
+
|
|
79
|
+
- **Loading**: `Widget.Loading` (or `skeleton` blocks shaped like the real
|
|
80
|
+
content — never a bare spinner in a big empty card).
|
|
81
|
+
- **Error**: `Widget.Error`, plus a one-line human cause when known. Never
|
|
82
|
+
render an exception message verbatim.
|
|
83
|
+
- **Empty**: a real designed state — icon (h-8 w-8, muted), one sentence
|
|
84
|
+
saying what will appear here and what action produces it. Empty ≠ error.
|
|
85
|
+
- **Success**: the design.
|
|
86
|
+
|
|
87
|
+
While iterating, expose a temporary way to flip states (a dev-only prop or a
|
|
88
|
+
mock switch) so the harness can show all four; remove it before publish.
|
|
89
|
+
|
|
90
|
+
## Fusion design rules (the harness applies the host theme)
|
|
91
|
+
|
|
92
|
+
- **Semantic tokens only**: `bg-card`, `text-foreground`, `text-muted-foreground`,
|
|
93
|
+
`border-border`, `bg-primary`, `text-destructive`. NEVER raw palette
|
|
94
|
+
classes (`bg-white`, `text-gray-500`, `bg-amber-400`) or hex/oklch
|
|
95
|
+
literals — they break in dark mode and off-brand tenants.
|
|
96
|
+
- **Accent discipline**: the primary (coral-orange) on ≤10% of the widget —
|
|
97
|
+
one CTA, one highlighted value, one active state. Semantic status colors
|
|
98
|
+
(success/warning/destructive) are not accents.
|
|
99
|
+
- **Depth via shadow, not borders** where a surface needs lift.
|
|
100
|
+
- **Sizing floors**: body text `text-sm`+; `text-xs` only for tertiary
|
|
101
|
+
metadata (timestamps, badge counts). Icons in lists `h-5 w-5`+. Card
|
|
102
|
+
padding `p-5`/`p-6`. Row padding: `px-4 py-3` (the recipes above use
|
|
103
|
+
this; hard floor `py-2 px-3` — never below). Section gaps `space-y-5`+.
|
|
104
|
+
- **Numbers**: hero/KPI values large and `tracking-tight` with
|
|
105
|
+
`tabular-nums` when digits align.
|
|
106
|
+
- **Overflow**: the widget must hold its grid size — inner scroll
|
|
107
|
+
(`overflow-y-auto` on the list, never the card) and `truncate` on
|
|
108
|
+
user-supplied strings. Test the sm layout, not just lg.
|
|
109
|
+
- **Components**: build from `@ekanos/ui/*` (the module list is in
|
|
110
|
+
AGENTS.md); `cn()` from `@ekanos/ui/utils` for conditional classes; icons
|
|
111
|
+
through `@ekanos/ui/icon` (Font Awesome names — they render empty outside
|
|
112
|
+
a host that loads FA; that is expected in bare tests).
|
|
113
|
+
|
|
114
|
+
## Layout sizing
|
|
115
|
+
|
|
116
|
+
Pick `layouts` from the content, not habit: KPI/stat cards 3–4 wide × 6–8
|
|
117
|
+
high; lists and feeds 4–6 × 10–14; charts 6–8 × 10–12 (grid units, lg
|
|
118
|
+
breakpoint). Give md/sm the same content priority order — cut columns, not
|
|
119
|
+
meaning.
|
|
120
|
+
|
|
121
|
+
## Checklist before calling a widget done
|
|
122
|
+
|
|
123
|
+
- [ ] Four states render in the harness; user has seen them
|
|
124
|
+
- [ ] No raw color literals; accent ≤10%; dark mode looks intentional
|
|
125
|
+
- [ ] Text floors respected; nothing truncates that matters at sm
|
|
126
|
+
- [ ] Mock data replaced by real data (or the mock is clearly temporary and
|
|
127
|
+
the wiring phase is next)
|
|
128
|
+
- [ ] Widget id is slug-prefixed and globally unique
|
|
129
|
+
|
|
130
|
+
A styling review of existing widgets — worst-first findings, before→after
|
|
131
|
+
fixes, the tile — is the `polish-widgets` skill; use it rather than
|
|
132
|
+
re-deriving a review pass from this file.
|