@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,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.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: polish-widgets
3
+ description: Review-and-fix pass over existing widget styling. Use when the user asks to review widget styling, wants widgets or the marketplace tile to look better, says the tile design is bad or the widgets look plain/generic/flat, reports overflow or responsiveness problems, or before publishing as a final styling pass.
4
+ ---
5
+
6
+ # Polish Widgets
7
+
8
+ A widget that renders the right data can still be a bad widget: every row the
9
+ same weight, no element answering the widget's question, a grid of identical
10
+ stat chips. This skill is the fix pass — render each widget in the harness,
11
+ find the worst offenses, fix them, show the user before and after. The
12
+ creation-time design rules live in
13
+ `../build-integration/references/widget-design.md`; this skill assumes they
14
+ exist and enforces them on code already written.
15
+
16
+ ## The key question
17
+
18
+ Every widget exists to answer 1–2 questions. Name the question FIRST. Its
19
+ answer must be the most prominent element — size, weight, position — and
20
+ everything else demotes to supporting context. If you cannot name the
21
+ question, that is the finding: ask the user what the widget is for before
22
+ touching its classes.
23
+
24
+ | Widget type | Key question | Most prominent element |
25
+ | ------------------- | ---------------------------------- | --------------------------------------------------------------------------- |
26
+ | KPI / stat | "How are we doing?" | The hero number: `text-3xl font-semibold tracking-tight tabular-nums` |
27
+ | List / feed | "What needs my attention?" | Item name `text-base font-medium` + its value, right-aligned `tabular-nums` |
28
+ | Chart | "Which way is it going?" | The chart plus the current value; axes and legends muted |
29
+ | Action card | "What can I do here?" | One clear CTA; supporting copy secondary |
30
+ | Status / connection | "Is it working? What's connected?" | Badge or status dot + the identity it describes |
31
+
32
+ The most common failure is uniform weight: everything `text-sm`, values styled
33
+ like their labels. Squint at the rendered widget — if nothing reads first,
34
+ there is no hierarchy, and that fix comes before any padding nit.
35
+
36
+ ## Review procedure
37
+
38
+ 1. **Enumerate** the widgets from `components.widgets` in `src/integration.ts`
39
+ — id, name, component file. Review all of them unless the user scoped it.
40
+ 2. **Render**: `npx ekanos dev`, then the Widgets surface at
41
+ `http://localhost:3100/<slug>/widgets`. Drive the review from the dev
42
+ toolbar: Render state (loading / error / inactive / disabled / active),
43
+ Viewport presets (check sm, not just lg), Collapsed, Unhealthy, and the
44
+ theme toggle — dark mode is part of the review, not an extra.
45
+ 3. **Assess** each widget against the checklist below and write findings per
46
+ widget, worst-first. Missing hierarchy outranks spacing; a broken sm layout
47
+ outranks a badge color.
48
+ 4. **Fix** worst-first, one widget at a time. After each: `npx tsc --noEmit`,
49
+ re-render in the harness.
50
+ 5. **Show the user** before/after for each widget — screenshots or ask them to
51
+ look — and get their read before the next one. Never claim a widget looks
52
+ better without one of the two.
53
+
54
+ ## Checklist
55
+
56
+ **Hierarchy**
57
+
58
+ - The key question's answer is the visually dominant element
59
+ - One prominent element per row; supporting info is `text-muted-foreground`
60
+ - Hero/stat values match the recipe: `text-3xl font-semibold tracking-tight
61
+ tabular-nums` (never below `text-2xl`, and never `text-sm` on a number
62
+ that matters)
63
+ - List row titles `text-base font-medium`; subtitles `text-sm text-muted-foreground`
64
+ - `text-xs` only on tertiary metadata (timestamps, header badge counts)
65
+
66
+ **Tokens & color**
67
+
68
+ - Semantic tokens only — no `bg-white`, `text-gray-500`, hex, raw palette shades
69
+ - Primary accent on ≤10% of the widget; status through `Badge` variants
70
+ (`success` / `warning` / `info` / `destructive`) or a semantic status dot
71
+ - No two colored elements competing in one row
72
+ - Both themes checked with the toolbar toggle
73
+
74
+ **Layout & overflow**
75
+
76
+ - Lists cap at 5–7 visible items; more goes behind inner scroll
77
+ (`min-h-0 flex-1 overflow-y-auto` on the list, never the card) or "View all"
78
+ - `truncate` + `min-w-0` on user-supplied strings; values `shrink-0` and right-aligned
79
+ - The sm viewport holds: cut columns, keep meaning
80
+ - Sizing floors hold: rows `px-4 py-3`+, list icons `h-5 w-5`+, gaps `gap-3`+,
81
+ body text `text-sm`+
82
+
83
+ **States & numbers**
84
+
85
+ - All four states render (the toolbar flips loading/error/inactive; empty needs
86
+ a data mock) and empty is a designed state, not a blank card
87
+ - Digits that align use `tabular-nums`; money is right-aligned with fixed width
88
+
89
+ The canonical fix for the flat-row failure, since it is the one you will apply
90
+ most:
91
+
92
+ ```tsx
93
+ // Before: three spans, one size, no first read
94
+ <div className="flex justify-between text-sm">
95
+ <span>{item.name}</span>
96
+ <span>{item.amount}</span>
97
+ </div>
98
+
99
+ // After: name leads, value anchors, metadata recedes
100
+ <div className="flex items-center gap-3 px-4 py-3">
101
+ <div className="min-w-0 flex-1">
102
+ <p className="truncate text-base font-medium">{item.name}</p>
103
+ <p className="text-muted-foreground text-sm">{item.detail}</p>
104
+ </div>
105
+ <span className="shrink-0 text-base font-semibold tabular-nums">
106
+ {formatCurrency(item.amount)}
107
+ </span>
108
+ </div>
109
+ ```
110
+
111
+ Full before→after gallery — hero KPI, status treatment, sizing floors, inner
112
+ scroll, row actions, designed empty state — in `references/patterns.md`. Read
113
+ it before rewriting any widget body.
114
+
115
+ ## The marketplace tile
116
+
117
+ "The tile looks bad" is usually not a styling bug — it is missing branding.
118
+ The tile is `components.marketplaceTile` in the definition, typically built on
119
+ `BaseMarketplaceTile` from `@ekanos/sdk/components`, and it renders from the
120
+ props you pass: `name`, `summary` (1–2 sentences shown on the card),
121
+ `brandName`, `logoPath`, `categories`, `productColor`, `ctaText`. Omit
122
+ `logoPath` and the card falls back to a single-letter monogram; omit `summary`
123
+ and the body sits blank next to its neighbours. Check the props before
124
+ touching CSS.
125
+
126
+ Review it at `http://localhost:3100/<slug>/tile` — the harness renders it
127
+ inside the real catalogue grid with two inert neighbour cards, which is the
128
+ only honest test: does the title wrap at column width, does the summary clamp
129
+ like the neighbours', is the card the same height? (Register the component as
130
+ `tile:` in `harness.config.ts` if the surface says none is registered.) The
131
+ harness also notes this surface is lightly used by the live marketplace today
132
+ — keep it correct and branded, don't gold-plate it.
133
+
134
+ ## Reporting
135
+
136
+ Finish with a per-widget summary: the key question you assigned it, the
137
+ findings fixed (worst-first), anything deliberately left, and confirmation the
138
+ user saw the result. Then run the standard gate: `npx tsc --noEmit` and
139
+ `npx ekanos validate` clean.