@ekanos/cli 0.2.3 → 0.3.1
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 +111 -51
- package/dist/commands/dev.js +24 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/compatibility.d.ts +12 -1
- package/dist/compatibility.js +12 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/harness-scaffold.d.ts +6 -1
- package/dist/harness-scaffold.js +10 -1
- package/dist/harness-scaffold.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +134 -17
- package/templates/claude-skill.md.tmpl +6 -1
- package/templates/harness/app/api/integrations/[slug]/[...path]/route.ts.tmpl +9 -0
- package/templates/package.json.tmpl +2 -2
- package/templates/skills/build-integration/SKILL.md +17 -4
- package/templates/skills/build-integration/references/api-wiring.md +17 -1
- package/templates/skills/build-integration/references/oauth-setup.md +76 -0
- package/templates/skills/build-integration/references/widget-design.md +12 -2
- package/templates/src/integration.ts.tmpl +16 -0
package/README.md
CHANGED
|
@@ -13,20 +13,20 @@ npx @ekanos/cli dev
|
|
|
13
13
|
|
|
14
14
|
## Verbs
|
|
15
15
|
|
|
16
|
-
| Verb
|
|
17
|
-
|
|
18
|
-
| `init`
|
|
19
|
-
| `validate` | Load the definition via esbuild and validate it with the **real** `@ekanos/integration-schema` zod schemas; add project checks (the required vitest SDK-inline block).
|
|
20
|
-
| `dev`
|
|
21
|
-
| `test`
|
|
22
|
-
| `login`
|
|
23
|
-
| `logout`
|
|
24
|
-
| `whoami`
|
|
25
|
-
| `status`
|
|
26
|
-
| `sources`
|
|
27
|
-
| `use`
|
|
28
|
-
| `publish`
|
|
29
|
-
| `upgrade`
|
|
16
|
+
| Verb | What it does |
|
|
17
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `init` | Scaffold `ekanos.json`, a working `defineIntegration`, a test, a `tsconfig.json`, a vitest config, `.gitignore`, a `package.json` with real published deps, and the agent-context files (`AGENTS.md`, `CLAUDE.md`, a Claude Code skill). Defaults to a new `./<slug>` directory; `--dir .` scaffolds in place, on top of an existing project if there is one. |
|
|
19
|
+
| `validate` | Load the definition via esbuild and validate it with the **real** `@ekanos/integration-schema` zod schemas; add project checks (the required vitest SDK-inline block). |
|
|
20
|
+
| `dev` | Scaffold `.ekanos/harness/` and run it with **your** Next, so your widgets, tile, activation form and triggers render in real Fusion chrome. |
|
|
21
|
+
| `test` | Delegate to the project's own test script via its package manager. |
|
|
22
|
+
| `login` | Authenticate this machine against a Fusion deployment (Auth0 device flow). |
|
|
23
|
+
| `logout` | Revoke the session at Auth0 and delete the local credentials. |
|
|
24
|
+
| `whoami` | Print the identity the stored session authenticates as, plus a compact `seats` list (slug + roles) from the same endpoint `sources` uses — degrades to `seats: null` rather than failing if that lookup is unavailable. |
|
|
25
|
+
| `status` | The read verb: login state for the resolved host, this project's `ekanos.json`, and its submissions on the host (`GET /api/partner/submissions`). Never fails just because you are logged out — it reports `authenticated: false` instead. |
|
|
26
|
+
| `sources` | List every Fusion source you hold a developer or source-admin seat on (`GET /api/partner/sources`), with a marker on this project's current publish target. |
|
|
27
|
+
| `use` | `ekanos use <source-slug>` — set this project's publish target to a source you hold a seat on. Refuses (`forbidden`, with your actual seats in the hint) for a slug you don't hold a seat on, so a typo can't silently repoint a project. |
|
|
28
|
+
| `publish` | Validate, pack and submit the integration to a Fusion host — refusing (`forbidden`) a target you hold no seat on, and confirming an ambiguous multi-seat target before submitting. See [Publish target verification](#publish-target-verification). |
|
|
29
|
+
| `upgrade` | Move this toolchain to a coherent target version, with verify-and-rollback. `--check` reports current vs. target and changes nothing; a bare `upgrade` installs, verifies (`validate` + the test script), and rolls back automatically on any failure. See [Keeping the toolchain current](#keeping-the-toolchain-current). |
|
|
30
30
|
|
|
31
31
|
### Installing: global is fine
|
|
32
32
|
|
|
@@ -45,12 +45,12 @@ binary you invoked. Set `EKANOS_NO_DELEGATE=1` to opt out for one invocation.
|
|
|
45
45
|
|
|
46
46
|
## Global flags
|
|
47
47
|
|
|
48
|
-
| Flag
|
|
49
|
-
|
|
50
|
-
| `--json`
|
|
51
|
-
| `--no-json` | Force the human-readable form on, even when piped.
|
|
52
|
-
| `--version` | Print the CLI version and exit 0. Works with or without a verb.
|
|
53
|
-
| `--help`
|
|
48
|
+
| Flag | What it does |
|
|
49
|
+
| ----------- | ---------------------------------------------------------------- |
|
|
50
|
+
| `--json` | Force the JSON envelope on, even at a terminal. |
|
|
51
|
+
| `--no-json` | Force the human-readable form on, even when piped. |
|
|
52
|
+
| `--version` | Print the CLI version and exit 0. Works with or without a verb. |
|
|
53
|
+
| `--help` | Print usage. Also available per-verb (`ekanos validate --help`). |
|
|
54
54
|
|
|
55
55
|
Short flags are not supported — the parser rejects them rather than guessing.
|
|
56
56
|
|
|
@@ -63,7 +63,7 @@ in either direction. In JSON mode **stdout carries exactly one JSON object** —
|
|
|
63
63
|
stderr. Errors always carry an imperative `hint`.
|
|
64
64
|
|
|
65
65
|
The CLI never prompts, with **one deliberate exception**: `publish` asks for a
|
|
66
|
-
y/N confirm at a real terminal when the publish target is
|
|
66
|
+
y/N confirm at a real terminal when the publish target is _ambiguous_ — an
|
|
67
67
|
implicit (ekanos.json) source and more than one developer seat. See
|
|
68
68
|
[Publish target verification](#publish-target-verification). Every other verb,
|
|
69
69
|
and `publish` itself in JSON/non-TTY mode or with an explicit `--source`, never
|
|
@@ -94,10 +94,12 @@ then cross-checks them against each other (colliding slugs, widget ids, tool
|
|
|
94
94
|
names), which it cannot do when it can only see one:
|
|
95
95
|
|
|
96
96
|
```json
|
|
97
|
-
{
|
|
98
|
-
|
|
97
|
+
{
|
|
98
|
+
"integrations": [
|
|
99
|
+
{ "slug": "acme-crm", "entry": "src/crm.ts" },
|
|
99
100
|
{ "slug": "acme-billing", "entry": "src/billing.ts" }
|
|
100
|
-
]
|
|
101
|
+
]
|
|
102
|
+
}
|
|
101
103
|
```
|
|
102
104
|
|
|
103
105
|
Declare one form or the other, never both. `validate` also checks that each
|
|
@@ -107,15 +109,15 @@ than a warning.
|
|
|
107
109
|
|
|
108
110
|
### Every field
|
|
109
111
|
|
|
110
|
-
| Field
|
|
111
|
-
|
|
112
|
-
| `slug`
|
|
113
|
-
| `entry`
|
|
114
|
-
| `integrations[]` | yes, in the list form
|
|
115
|
-
| `sourceGlobs`
|
|
116
|
-
| `source`
|
|
117
|
-
| `host`
|
|
118
|
-
| `$schema`
|
|
112
|
+
| Field | Required | Where | What |
|
|
113
|
+
| ---------------- | ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `slug` | yes, in the single form | top level | kebab-case; must equal the definition's `slug` |
|
|
115
|
+
| `entry` | yes, in the single form | top level | path to the module exporting your `defineIntegration(...)` result |
|
|
116
|
+
| `integrations[]` | yes, in the list form | top level | `{ slug, entry }` per integration, same rules, slugs unique |
|
|
117
|
+
| `sourceGlobs` | no | **top level only** | extra Tailwind `@source` globs; project-relative, no `..` |
|
|
118
|
+
| `source` | no | top level | the Fusion source slug this project publishes to; written by the first successful `publish` |
|
|
119
|
+
| `host` | no | top level | the Fusion host (http(s) origin) this project publishes to; written by the first successful `publish`, read by every host-taking verb |
|
|
120
|
+
| `$schema` | no | top level | ignored, for editor tooling |
|
|
119
121
|
|
|
120
122
|
The file is **strict**: an unrecognised key is an error naming the key, not a
|
|
121
123
|
silent no-op. `sourceGlobs` is deliberately not per-integration — Tailwind scans
|
|
@@ -138,6 +140,9 @@ imply an isolation that does not exist.
|
|
|
138
140
|
app/[slug]/tile/page.tsx ├─ one-line re-exports of @ekanos/harness/routes
|
|
139
141
|
app/[slug]/activation/page.tsx
|
|
140
142
|
app/[slug]/triggers/page.tsx ─┘
|
|
143
|
+
app/api/integrations/[slug]/[...path]/route.ts
|
|
144
|
+
server — injects your registry into @ekanos/harness/api
|
|
145
|
+
(live-mode OAuth loop, storage bridge, trigger route)
|
|
141
146
|
next.config.mjs transpiles @ekanos/harness, aliases the workspace mock
|
|
142
147
|
postcss.config.mjs @tailwindcss/postcss
|
|
143
148
|
tsconfig.json no baseUrl, no paths
|
|
@@ -174,14 +179,63 @@ default wildcard bind hands all of that to anyone who can reach the port.
|
|
|
174
179
|
warning saying what is now reachable, and the JSON envelope carries
|
|
175
180
|
`server.exposed: true` so a script can refuse to run that way.
|
|
176
181
|
|
|
182
|
+
The shell's API routes (below) are stricter than the pages: they hold your
|
|
183
|
+
local OAuth client secret and the tokens `onTokens` stored, and they run your
|
|
184
|
+
handlers on request, so they **refuse any request whose `Host` is not
|
|
185
|
+
loopback** unless `EKANOS_HARNESS_EXPOSED=1`. `dev --host <non-loopback>` sets
|
|
186
|
+
that for the server it starts (and prefixes it on the command it prints with
|
|
187
|
+
`--no-start`), after the warning. Starting Next by hand with a wildcard bind
|
|
188
|
+
gets the refusal, not the exposure — which is the point.
|
|
189
|
+
|
|
190
|
+
### Local OAuth — the real loop, on localhost
|
|
191
|
+
|
|
192
|
+
An integration that declares `oauth` in `defineIntegration()` can run the whole
|
|
193
|
+
authorize → callback → code-exchange → `onTokens` loop against **your own
|
|
194
|
+
developer app**, in live mode, with no Fusion host involved:
|
|
195
|
+
|
|
196
|
+
1. Switch the toolbar's **Data** dropdown to **Live API** (the entry needs a
|
|
197
|
+
`live` block). An integration whose widgets never call the vendor from the
|
|
198
|
+
browser — the OAuth shape — can declare `live: { default: 'live' }` and the
|
|
199
|
+
harness starts there; a choice made at the toolbar still wins.
|
|
200
|
+
2. Open the Activation surface. The **Local OAuth** panel starts with a
|
|
201
|
+
checklist for the part only a human can do in the provider's developer
|
|
202
|
+
console (create the app, add the redirect URI
|
|
203
|
+
`http://localhost:3100/api/integrations/<slug>/oauth/callback`, enable the
|
|
204
|
+
declared scopes, copy the client id and secret) — derived from your
|
|
205
|
+
declaration, with a console link and provider-specific notes if your harness
|
|
206
|
+
entry sets `oauth.setup` — and then a form with one field per secret name
|
|
207
|
+
your definition declares
|
|
208
|
+
(`oauth.credentials.*SecretName`, every signed webhook's `secretName`).
|
|
209
|
+
Paste the values and **Save credentials**. They are written server-side to
|
|
210
|
+
**`.ekanos/local-secrets.json`** — gitignored, never packed, read on every
|
|
211
|
+
request — and never sent back to the browser: a saved key shows as `filled`
|
|
212
|
+
beside an empty field, and a blank field keeps what is saved. (Editing the
|
|
213
|
+
file by hand works too.)
|
|
214
|
+
3. Click **Connect**. The shell's route builds the authorize URL (PKCE when
|
|
215
|
+
declared), the provider sends you back, the route exchanges the code through
|
|
216
|
+
`ctx.fetch` — so an undeclared token-endpoint origin fails with
|
|
217
|
+
`EgressDeniedError` exactly as on the host — and calls your `onTokens` on a
|
|
218
|
+
**server-held context**. `oauth/status` then reports connected, the SDK's
|
|
219
|
+
`OAuthActivationForm` auto-activates, and `onActivate` runs on that same
|
|
220
|
+
context.
|
|
221
|
+
4. Your widgets read what `onTokens`, `onActivate` and your schedules wrote
|
|
222
|
+
through the ordinary storage route, unchanged.
|
|
223
|
+
|
|
224
|
+
The connection lives in the dev server's memory: restarting `next dev` forgets
|
|
225
|
+
it and you Connect again. Editing a value in the secrets file rebuilds the
|
|
226
|
+
context and forgets the connection too — a different client id invalidates the
|
|
227
|
+
tokens anyway. In **fixtures** mode none of this runs: the status route is
|
|
228
|
+
answered from `oauth.connected` in your harness entry (connected by default),
|
|
229
|
+
so widgets render without a round trip and nothing leaves the browser.
|
|
230
|
+
|
|
177
231
|
### Who owns what
|
|
178
232
|
|
|
179
|
-
| Path
|
|
180
|
-
|
|
181
|
-
| `harness.config.ts` (project root) | **you**
|
|
182
|
-
| your integration source
|
|
183
|
-
| `.ekanos/harness/**`
|
|
184
|
-
| `@ekanos/harness`
|
|
233
|
+
| Path | Owner | You edit it? |
|
|
234
|
+
| ---------------------------------- | --------- | ----------------------------------------------------------------------- |
|
|
235
|
+
| `harness.config.ts` (project root) | **you** | **yes — the only one.** Written once, git-tracked, never touched again. |
|
|
236
|
+
| your integration source | **you** | yes |
|
|
237
|
+
| `.ekanos/harness/**` | generated | no — gitignored, rewritten on every run |
|
|
238
|
+
| `@ekanos/harness` | us | no — it is an ordinary dependency |
|
|
185
239
|
|
|
186
240
|
That split is the point: a harness bugfix ships as an `@ekanos/harness` version
|
|
187
241
|
bump you pick up with `pnpm up`, with zero edits to any file you own. Every
|
|
@@ -219,8 +273,11 @@ integration lives somewhere else, add globs to `ekanos.json` — do NOT edit the
|
|
|
219
273
|
stylesheet, which is regenerated on every run:
|
|
220
274
|
|
|
221
275
|
```json
|
|
222
|
-
{
|
|
223
|
-
"
|
|
276
|
+
{
|
|
277
|
+
"slug": "acme-crm",
|
|
278
|
+
"entry": "src/integration.ts",
|
|
279
|
+
"sourceGlobs": ["vendor/acme/**/*.{ts,tsx}"]
|
|
280
|
+
}
|
|
224
281
|
```
|
|
225
282
|
|
|
226
283
|
A glob Tailwind is not pointed at costs you a silently unstyled integration
|
|
@@ -244,6 +301,10 @@ upgrade.
|
|
|
244
301
|
and a hash of the templates, so a change to any of them regenerates the shell
|
|
245
302
|
and `dev` says which one moved.
|
|
246
303
|
|
|
304
|
+
Contract history: **1** was the original ten-file shell; **2** (CLI 0.3 /
|
|
305
|
+
harness 0.2) added `app/api/integrations/[slug]/[...path]/route.ts`, the shell's
|
|
306
|
+
one server route, injecting the registry into `@ekanos/harness/api`.
|
|
307
|
+
|
|
247
308
|
**For maintainers:** propagation is byte-driven — `dev` rewrites a generated
|
|
248
309
|
file whenever its content differs, marker or not. So a change to the SHELL
|
|
249
310
|
ships in an `@ekanos/cli` release, never a harness one: bumping
|
|
@@ -325,7 +386,7 @@ version and your own `test` script are decisions, not omissions.
|
|
|
325
386
|
Deliberate exceptions:
|
|
326
387
|
|
|
327
388
|
- `npm init -y`'s stub `test` script (`echo "Error: no test specified" && exit
|
|
328
|
-
|
|
389
|
+
1`) is replaced. It is npm's placeholder, not a choice, and keeping it left
|
|
329
390
|
`ekanos test` failing on a fresh scaffold.
|
|
330
391
|
- `"type"` is set to `module` even if you declare `"commonjs"`, with a loud
|
|
331
392
|
note saying it changed. `npm init -y` writes `"commonjs"` explicitly, so that
|
|
@@ -365,7 +426,7 @@ host in this order:
|
|
|
365
426
|
|
|
366
427
|
`login` is the exception: it resolves from `--host`, else `EKANOS_HOST`, else
|
|
367
428
|
the sole stored host — **never** from `ekanos.json`. Login is the one verb
|
|
368
|
-
that
|
|
429
|
+
that _creates_ credentials, and `ekanos.json` is a committed file; if login
|
|
369
430
|
honored it, anyone could point your device flow at their own tenant just by
|
|
370
431
|
editing a file in a repo you clone. Every other verb is safe reading it,
|
|
371
432
|
because their bearer tokens are looked up per-host from the credential store —
|
|
@@ -464,13 +525,13 @@ never silently written.
|
|
|
464
525
|
`ekanos.json`), then — **before packing or submitting anything** — fetches your
|
|
465
526
|
seat list and applies these rules:
|
|
466
527
|
|
|
467
|
-
| Source came from
|
|
468
|
-
|
|
469
|
-
| any
|
|
470
|
-
| `--source` flag (this run) | any
|
|
471
|
-
| `ekanos.json` (implicit)
|
|
472
|
-
| `ekanos.json` (implicit)
|
|
473
|
-
| `ekanos.json` (implicit)
|
|
528
|
+
| Source came from | Seats held | Behavior |
|
|
529
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
530
|
+
| any | not among your seats | `forbidden` (exit 5): refuses before packing/submitting; the hint lists your actual seats |
|
|
531
|
+
| `--source` flag (this run) | any | proceeds immediately — an explicit flag is consent |
|
|
532
|
+
| `ekanos.json` (implicit) | exactly one | proceeds, printing "Publishing to `<name>` (`<slug>`) — your only developer seat" |
|
|
533
|
+
| `ekanos.json` (implicit) | more than one, human mode **and** a real terminal on stdin | prints the target and asks for an explicit `y/N` confirm; `--yes` skips it |
|
|
534
|
+
| `ekanos.json` (implicit) | more than one, anything else (JSON mode, `--json`, piped, an agent, **or** human mode with non-interactive stdin — a cron job, a wrapper script) | **never prompts** — proceeds only with `--yes`; otherwise `validation` (exit 3) with the seat list and a hint naming `--source`, `--yes`, and `ekanos use` |
|
|
474
535
|
|
|
475
536
|
A successful publish's envelope carries `source: { id, slug, name }` — the
|
|
476
537
|
verified target, not just the slug string — so a script or an agent can log
|
|
@@ -480,4 +541,3 @@ exactly which source a submission landed on.
|
|
|
480
541
|
Either pass `--source <slug>` explicitly, or run `ekanos sources` first and
|
|
481
542
|
choose deliberately. `--yes` exists for a human's already-confirmed intent and
|
|
482
543
|
for CI pipelines pinned to one source — not as a way to silence the check.
|
|
483
|
-
|
package/dist/commands/dev.js
CHANGED
|
@@ -21,6 +21,16 @@ const DEFAULT_PORT = 3100;
|
|
|
21
21
|
const DEFAULT_HOST = '127.0.0.1';
|
|
22
22
|
/** Addresses that are only reachable from this machine. */
|
|
23
23
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
24
|
+
/**
|
|
25
|
+
* Set for the dev server when `--host` binds beyond loopback. The shell's
|
|
26
|
+
* API routes (`@ekanos/harness/api`) refuse non-loopback `Host` headers
|
|
27
|
+
* unless this is `1`: they hold the partner's local OAuth credentials and
|
|
28
|
+
* run handlers on request, so exposing them has to be a decision — this one,
|
|
29
|
+
* made after the warning below — rather than a side effect of the bind.
|
|
30
|
+
*/
|
|
31
|
+
const EXPOSED_ENV = 'EKANOS_HARNESS_EXPOSED';
|
|
32
|
+
/** Where the harness generates, and reads, local dev credentials. */
|
|
33
|
+
const LOCAL_SECRETS_PATH = path.join('.ekanos', 'local-secrets.json');
|
|
24
34
|
/**
|
|
25
35
|
* `dev` — scaffold the local harness shell into the partner's project, then
|
|
26
36
|
* run it with their own Next.
|
|
@@ -96,17 +106,26 @@ export async function runDev(ctx, args) {
|
|
|
96
106
|
ctx.log(` updated ${HARNESS_DIR}/${file}`);
|
|
97
107
|
if (gitignoreUpdated)
|
|
98
108
|
ctx.log(' updated .gitignore (.ekanos/)');
|
|
109
|
+
ctx.log(` live-mode OAuth: the harness generates ${LOCAL_SECRETS_PATH} on first ` +
|
|
110
|
+
'use — fill it in and follow "Local OAuth" on the Activation surface.');
|
|
99
111
|
// `--hostname` is passed ALWAYS, never left to Next's default, so the URL
|
|
100
112
|
// we print is the URL that is actually served.
|
|
101
113
|
const serverArgs = ['dev', '--port', String(port), '--hostname', host];
|
|
102
|
-
const
|
|
114
|
+
const serverEnv = exposed
|
|
115
|
+
? { [EXPOSED_ENV]: '1' }
|
|
116
|
+
: {};
|
|
117
|
+
const command = (exposed ? `${EXPOSED_ENV}=1 ` : '') +
|
|
118
|
+
formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));
|
|
103
119
|
const url = `http://${displayHost(host)}:${port}`;
|
|
104
120
|
const scaffold = Object.assign({ slug: loaded.primary.slug, integrations: loaded.integrations.map((i) => i.slug), projectDir: loaded.projectDir, harnessDir: shell.harnessDir, harnessVersion, previousVersion: shell.previousVersion, previousMarker: shell.previousMarker, stale: shell.stale, staleReasons: shell.staleReasons, sourceGlobs: (_b = loaded.project.sourceGlobs) !== null && _b !== void 0 ? _b : [], created: shell.created, updated: shell.updated, unchanged: shell.unchanged, configBootstrapped: config.bootstrapped, configPath: config.configPath, gitignoreUpdated }, (notice ? { toolchain: notice } : {}));
|
|
105
121
|
if (exposed) {
|
|
106
122
|
ctx.warn(`WARNING: --host ${host} exposes the harness beyond this machine. It ` +
|
|
107
123
|
'has no authentication, and it renders your fixtures and — in live ' +
|
|
108
|
-
'mode — real responses from your vendor API.
|
|
109
|
-
|
|
124
|
+
'mode — real responses from your vendor API. Its API routes hold the ' +
|
|
125
|
+
`client secret from ${LOCAL_SECRETS_PATH}, the tokens onTokens stored, ` +
|
|
126
|
+
'and run your handlers on request; they refuse non-loopback callers ' +
|
|
127
|
+
`unless ${EXPOSED_ENV}=1, which this sets. Anyone who can reach ` +
|
|
128
|
+
`${url} can read and run all of it. Omit --host to bind ${DEFAULT_HOST}.`);
|
|
110
129
|
}
|
|
111
130
|
// A JSON caller is an agent or a pipe. Handing it a dev server that never
|
|
112
131
|
// exits would hang its tool call forever and it would never see the
|
|
@@ -122,6 +141,7 @@ export async function runDev(ctx, args) {
|
|
|
122
141
|
bin: pm.bin,
|
|
123
142
|
argv: execArgs(pm, 'next', serverArgs),
|
|
124
143
|
cwd: shell.harnessDir,
|
|
144
|
+
env: serverEnv,
|
|
125
145
|
});
|
|
126
146
|
const data = Object.assign(Object.assign({}, scaffold), { server: { started: true, command, url, port, host, exposed, exitCode } });
|
|
127
147
|
if (exitCode === 0) {
|
|
@@ -143,6 +163,7 @@ function spawnServer(params) {
|
|
|
143
163
|
var _a, _b;
|
|
144
164
|
const child = spawn(params.bin, params.argv, {
|
|
145
165
|
cwd: params.cwd,
|
|
166
|
+
env: Object.assign(Object.assign({}, process.env), params.env),
|
|
146
167
|
// stdin inherited so Next's own key handling works; stdout/stderr piped
|
|
147
168
|
// so we can route them and keep OUR stdout reserved for the envelope.
|
|
148
169
|
stdio: ['inherit', 'pipe', 'pipe'],
|
package/dist/commands/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Fusion HOST the update notice checks against —\n * this is the local bind address for `next dev`, never a Fusion origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const command = formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Anyone who can reach ' +\n `${url} can read all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n `dev: harness ready in ${shell.harnessDir}. Start it with: ${command}`,\n );\n }\n\n ctx.log(`Starting the harness on ${url} (${command})…`);\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(data, `dev: harness stopped cleanly.`);\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Fusion HOST the update notice checks against —\n * this is the local bind address for `next dev`, never a Fusion origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * Set for the dev server when `--host` binds beyond loopback. The shell's\n * API routes (`@ekanos/harness/api`) refuse non-loopback `Host` headers\n * unless this is `1`: they hold the partner's local OAuth credentials and\n * run handlers on request, so exposing them has to be a decision — this one,\n * made after the warning below — rather than a side effect of the bind.\n */\nconst EXPOSED_ENV = 'EKANOS_HARNESS_EXPOSED';\n\n/** Where the harness generates, and reads, local dev credentials. */\nconst LOCAL_SECRETS_PATH = path.join('.ekanos', 'local-secrets.json');\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n ctx.log(\n ` live-mode OAuth: the harness generates ${LOCAL_SECRETS_PATH} on first ` +\n 'use — fill it in and follow \"Local OAuth\" on the Activation surface.',\n );\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const serverEnv: Record<string, string> = exposed\n ? { [EXPOSED_ENV]: '1' }\n : {};\n const command =\n (exposed ? `${EXPOSED_ENV}=1 ` : '') +\n formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Its API routes hold the ' +\n `client secret from ${LOCAL_SECRETS_PATH}, the tokens onTokens stored, ` +\n 'and run your handlers on request; they refuse non-loopback callers ' +\n `unless ${EXPOSED_ENV}=1, which this sets. Anyone who can reach ` +\n `${url} can read and run all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n `dev: harness ready in ${shell.harnessDir}. Start it with: ${command}`,\n );\n }\n\n ctx.log(`Starting the harness on ${url} (${command})…`);\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n env: serverEnv,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(data, `dev: harness stopped cleanly.`);\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n env: Record<string, string>;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n env: { ...process.env, ...params.env },\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
|
package/dist/compatibility.d.ts
CHANGED
|
@@ -31,7 +31,18 @@
|
|
|
31
31
|
* shell must contain. Editing a comment in a template does not count — that is
|
|
32
32
|
* what the template fingerprint in the marker is for.
|
|
33
33
|
*/
|
|
34
|
-
export declare const SHELL_CONTRACT =
|
|
34
|
+
export declare const SHELL_CONTRACT = 2;
|
|
35
|
+
/**
|
|
36
|
+
* Contract history — what each bump changed, so a mismatch message can be
|
|
37
|
+
* read against it:
|
|
38
|
+
*
|
|
39
|
+
* 1 the original ten-file shell: pages re-exporting `/routes`, the client
|
|
40
|
+
* shell + index page injecting the registry into `/app`.
|
|
41
|
+
* 2 adds `app/api/integrations/[slug]/[...path]/route.ts`, injecting the
|
|
42
|
+
* registry into the new `@ekanos/harness/api` server entry. A harness
|
|
43
|
+
* without that export cannot back the route; a shell without the route
|
|
44
|
+
* leaves that harness's live-mode OAuth loop unreachable.
|
|
45
|
+
*/
|
|
35
46
|
/**
|
|
36
47
|
* The contract an installed `@ekanos/harness` declares it needs, from
|
|
37
48
|
* `{ "ekanos": { "shellContract": N } }` in its package.json.
|
package/dist/compatibility.js
CHANGED
|
@@ -36,7 +36,18 @@ import { readTemplateSource, templatesDir } from './templates.js';
|
|
|
36
36
|
* shell must contain. Editing a comment in a template does not count — that is
|
|
37
37
|
* what the template fingerprint in the marker is for.
|
|
38
38
|
*/
|
|
39
|
-
export const SHELL_CONTRACT =
|
|
39
|
+
export const SHELL_CONTRACT = 2;
|
|
40
|
+
/**
|
|
41
|
+
* Contract history — what each bump changed, so a mismatch message can be
|
|
42
|
+
* read against it:
|
|
43
|
+
*
|
|
44
|
+
* 1 the original ten-file shell: pages re-exporting `/routes`, the client
|
|
45
|
+
* shell + index page injecting the registry into `/app`.
|
|
46
|
+
* 2 adds `app/api/integrations/[slug]/[...path]/route.ts`, injecting the
|
|
47
|
+
* registry into the new `@ekanos/harness/api` server entry. A harness
|
|
48
|
+
* without that export cannot back the route; a shell without the route
|
|
49
|
+
* leaves that harness's live-mode OAuth loop unreachable.
|
|
50
|
+
*/
|
|
40
51
|
/**
|
|
41
52
|
* The contract an installed `@ekanos/harness` declares it needs, from
|
|
42
53
|
* `{ "ekanos": { "shellContract": N } }` in its package.json.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { readTemplateSource, templatesDir } from './templates';\n\n/**\n * The shell templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the interesting failure is the quiet one:\n *\n * `pnpm up @ekanos/harness` → 0.2, which moves an export.\n * `ekanos dev` regenerates from the OLD CLI's templates and stamps \"0.2\"\n * into the marker. Every later run now considers the shell current. It is\n * not: it was generated against an obsolete contract, and what the partner\n * sees is a module-resolution error rather than \"these are incompatible\".\n *\n * That directly attacks the promise the package boundary exists to keep — ship\n * a harness fix as a version bump a partner picks up with `pnpm up`. So the\n * CLI declares which `@ekanos/harness` versions ITS templates are written\n * against, checks the installed one before generating, and names the\n * incompatibility instead of letting a resolution error stand in for it.\n */\n\n/**\n * The shell contract this CLI's templates GENERATE.\n *\n * An integer, not a semver range. A range can only INFER compatibility from\n * version numbers — it cannot tell a harness bugfix from a change to the shell\n * the harness requires, so it either blocks upgrades that were fine or waves\n * through ones that were not. The integer is declared by both sides and is\n * authoritative.\n *\n * **Bump this only when the generated shell's coupling to the package\n * changes**: an export renamed or moved between `/app` and `/routes`, the\n * props of `RootLayout` or `IndexPage`, the server/client split, the alias\n * specifier, which side owns `@import 'tailwindcss'`, or the set of files the\n * shell must contain. Editing a comment in a template does not count — that is\n * what the template fingerprint in the marker is for.\n */\nexport const SHELL_CONTRACT = 1;\n\n/**\n * The contract an installed `@ekanos/harness` declares it needs, from\n * `{ \"ekanos\": { \"shellContract\": N } }` in its package.json.\n *\n * A package that declares nothing is read as contract 1 — the contract that\n * shipped first — so the harness released before this existed keeps working.\n * That default is deliberately silent, which is why `@ekanos/harness` asserts\n * the field's presence in its own pack test rather than relying on it.\n */\nexport function declaredShellContract(harnessManifest: unknown): number {\n if (typeof harnessManifest !== 'object' || harnessManifest === null) return 1;\n const ekanos = (harnessManifest as { ekanos?: unknown }).ekanos;\n if (typeof ekanos !== 'object' || ekanos === null) return 1;\n const declared = (ekanos as { shellContract?: unknown }).shellContract;\n return Number.isInteger(declared) ? (declared as number) : 1;\n}\n\n/** `[major, minor, patch]`, with anything non-numeric treated as 0. */\nfunction parseVersion(version: string): [number, number, number] {\n const core = version.split('+')[0]!.split('-')[0]!;\n const parts = core.split('.').map((part) => Number.parseInt(part, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n for (let i = 0; i < 3; i++) {\n if (left[i]! !== right[i]!) return left[i]! < right[i]! ? -1 : 1;\n }\n return 0;\n}\n\n/** This CLI's own version, read from its package.json at runtime. */\nexport function cliVersion(): string {\n const manifestPath = path.join(templatesDir(), '..', 'package.json');\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n return typeof version === 'string' ? version : '0.0.0';\n}\n\n/**\n * Fail unless this CLI's templates generate the shell the installed harness\n * expects.\n *\n * The templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the failure that matters is the quiet one:\n * `pnpm up @ekanos/harness` moves an export, `ekanos dev` regenerates from the\n * OLD CLI's templates, and the partner sees a module-resolution error rather\n * than anything naming the incompatibility.\n *\n * Byte-comparing generated files cannot catch it: the old templates' bytes are\n * exactly what the old CLI intends, so nothing regenerates differently and the\n * shell stays wired to something that moved. Only a declared contract can.\n *\n * Runs before anything is written, so an incompatible pair produces no shell\n * at all — a broken shell that builds is worse than no shell.\n */\nexport function assertHarnessCompatible(params: {\n harnessVersion: string;\n harnessManifest: unknown;\n installHint: (packageSpec: string) => string;\n}): void {\n const required = declaredShellContract(params.harnessManifest);\n if (required === SHELL_CONTRACT) return;\n\n if (required > SHELL_CONTRACT) {\n throw preconditionError(\n `@ekanos/cli ${cliVersion()} is too old for @ekanos/harness ` +\n `${params.harnessVersion}: that harness needs shell contract ` +\n `${required}, and this CLI generates ${SHELL_CONTRACT}.`,\n `Upgrade the CLI: ${params.installHint('@ekanos/cli@latest')}. Then ` +\n 're-run \"ekanos dev\" and the shell is regenerated for you.',\n );\n }\n\n throw preconditionError(\n `@ekanos/harness ${params.harnessVersion} is too old for @ekanos/cli ` +\n `${cliVersion()}: this CLI generates shell contract ${SHELL_CONTRACT}, ` +\n `and that harness needs ${required}.`,\n `Upgrade it: ${params.installHint('@ekanos/harness@latest')}.`,\n );\n}\n\n/**\n * A fingerprint of the template SOURCES, independent of the slug they are\n * rendered with.\n *\n * The harness version alone cannot certify a shell: the templates can change\n * while it stays put (a CLI patch release), and it can change while they stay\n * put. Hashing the sources means any edit to what we generate invalidates\n * every shell generated before it, including one produced by a CLI version\n * that never shipped.\n */\nexport function templatesFingerprint(templateNames: readonly string[]): string {\n const hash = createHash('sha256');\n for (const name of [...templateNames].sort()) {\n hash.update(name);\n hash.update('\\0');\n hash.update(readTemplateSource(name));\n hash.update('\\0');\n }\n return hash.digest('hex').slice(0, 16);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { readTemplateSource, templatesDir } from './templates';\n\n/**\n * The shell templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the interesting failure is the quiet one:\n *\n * `pnpm up @ekanos/harness` → 0.2, which moves an export.\n * `ekanos dev` regenerates from the OLD CLI's templates and stamps \"0.2\"\n * into the marker. Every later run now considers the shell current. It is\n * not: it was generated against an obsolete contract, and what the partner\n * sees is a module-resolution error rather than \"these are incompatible\".\n *\n * That directly attacks the promise the package boundary exists to keep — ship\n * a harness fix as a version bump a partner picks up with `pnpm up`. So the\n * CLI declares which `@ekanos/harness` versions ITS templates are written\n * against, checks the installed one before generating, and names the\n * incompatibility instead of letting a resolution error stand in for it.\n */\n\n/**\n * The shell contract this CLI's templates GENERATE.\n *\n * An integer, not a semver range. A range can only INFER compatibility from\n * version numbers — it cannot tell a harness bugfix from a change to the shell\n * the harness requires, so it either blocks upgrades that were fine or waves\n * through ones that were not. The integer is declared by both sides and is\n * authoritative.\n *\n * **Bump this only when the generated shell's coupling to the package\n * changes**: an export renamed or moved between `/app` and `/routes`, the\n * props of `RootLayout` or `IndexPage`, the server/client split, the alias\n * specifier, which side owns `@import 'tailwindcss'`, or the set of files the\n * shell must contain. Editing a comment in a template does not count — that is\n * what the template fingerprint in the marker is for.\n */\nexport const SHELL_CONTRACT = 2;\n\n/**\n * Contract history — what each bump changed, so a mismatch message can be\n * read against it:\n *\n * 1 the original ten-file shell: pages re-exporting `/routes`, the client\n * shell + index page injecting the registry into `/app`.\n * 2 adds `app/api/integrations/[slug]/[...path]/route.ts`, injecting the\n * registry into the new `@ekanos/harness/api` server entry. A harness\n * without that export cannot back the route; a shell without the route\n * leaves that harness's live-mode OAuth loop unreachable.\n */\n\n/**\n * The contract an installed `@ekanos/harness` declares it needs, from\n * `{ \"ekanos\": { \"shellContract\": N } }` in its package.json.\n *\n * A package that declares nothing is read as contract 1 — the contract that\n * shipped first — so the harness released before this existed keeps working.\n * That default is deliberately silent, which is why `@ekanos/harness` asserts\n * the field's presence in its own pack test rather than relying on it.\n */\nexport function declaredShellContract(harnessManifest: unknown): number {\n if (typeof harnessManifest !== 'object' || harnessManifest === null) return 1;\n const ekanos = (harnessManifest as { ekanos?: unknown }).ekanos;\n if (typeof ekanos !== 'object' || ekanos === null) return 1;\n const declared = (ekanos as { shellContract?: unknown }).shellContract;\n return Number.isInteger(declared) ? (declared as number) : 1;\n}\n\n/** `[major, minor, patch]`, with anything non-numeric treated as 0. */\nfunction parseVersion(version: string): [number, number, number] {\n const core = version.split('+')[0]!.split('-')[0]!;\n const parts = core.split('.').map((part) => Number.parseInt(part, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n for (let i = 0; i < 3; i++) {\n if (left[i]! !== right[i]!) return left[i]! < right[i]! ? -1 : 1;\n }\n return 0;\n}\n\n/** This CLI's own version, read from its package.json at runtime. */\nexport function cliVersion(): string {\n const manifestPath = path.join(templatesDir(), '..', 'package.json');\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n return typeof version === 'string' ? version : '0.0.0';\n}\n\n/**\n * Fail unless this CLI's templates generate the shell the installed harness\n * expects.\n *\n * The templates ship with `@ekanos/cli`; the code they import ships with\n * `@ekanos/harness`. The two version independently, so a partner can upgrade\n * one without the other — and the failure that matters is the quiet one:\n * `pnpm up @ekanos/harness` moves an export, `ekanos dev` regenerates from the\n * OLD CLI's templates, and the partner sees a module-resolution error rather\n * than anything naming the incompatibility.\n *\n * Byte-comparing generated files cannot catch it: the old templates' bytes are\n * exactly what the old CLI intends, so nothing regenerates differently and the\n * shell stays wired to something that moved. Only a declared contract can.\n *\n * Runs before anything is written, so an incompatible pair produces no shell\n * at all — a broken shell that builds is worse than no shell.\n */\nexport function assertHarnessCompatible(params: {\n harnessVersion: string;\n harnessManifest: unknown;\n installHint: (packageSpec: string) => string;\n}): void {\n const required = declaredShellContract(params.harnessManifest);\n if (required === SHELL_CONTRACT) return;\n\n if (required > SHELL_CONTRACT) {\n throw preconditionError(\n `@ekanos/cli ${cliVersion()} is too old for @ekanos/harness ` +\n `${params.harnessVersion}: that harness needs shell contract ` +\n `${required}, and this CLI generates ${SHELL_CONTRACT}.`,\n `Upgrade the CLI: ${params.installHint('@ekanos/cli@latest')}. Then ` +\n 're-run \"ekanos dev\" and the shell is regenerated for you.',\n );\n }\n\n throw preconditionError(\n `@ekanos/harness ${params.harnessVersion} is too old for @ekanos/cli ` +\n `${cliVersion()}: this CLI generates shell contract ${SHELL_CONTRACT}, ` +\n `and that harness needs ${required}.`,\n `Upgrade it: ${params.installHint('@ekanos/harness@latest')}.`,\n );\n}\n\n/**\n * A fingerprint of the template SOURCES, independent of the slug they are\n * rendered with.\n *\n * The harness version alone cannot certify a shell: the templates can change\n * while it stays put (a CLI patch release), and it can change while they stay\n * put. Hashing the sources means any edit to what we generate invalidates\n * every shell generated before it, including one produced by a CLI version\n * that never shipped.\n */\nexport function templatesFingerprint(templateNames: readonly string[]): string {\n const hash = createHash('sha256');\n for (const name of [...templateNames].sort()) {\n hash.update(name);\n hash.update('\\0');\n hash.update(readTemplateSource(name));\n hash.update('\\0');\n }\n return hash.digest('hex').slice(0, 16);\n}\n"]}
|
|
@@ -15,11 +15,16 @@ export declare const VERSION_MARKER = ".ekanos-version";
|
|
|
15
15
|
export declare const HARNESS_CONFIG_FILENAME = "harness.config.ts";
|
|
16
16
|
/**
|
|
17
17
|
* Every file in the generated shell: bundled template → path inside
|
|
18
|
-
* `.ekanos/harness/`. The
|
|
18
|
+
* `.ekanos/harness/`. The ten route files are mechanically derivable from the
|
|
19
19
|
* export map alone — which is the test of whether the package boundary is in
|
|
20
20
|
* the right place. If a shell file ever needs a judgment call, the boundary
|
|
21
21
|
* has moved, not the template.
|
|
22
22
|
*
|
|
23
|
+
* `app/api/integrations/[slug]/[...path]/route.ts` is the shell's one SERVER
|
|
24
|
+
* route: it injects the registry into `@ekanos/harness/api`, which serves the
|
|
25
|
+
* live-mode OAuth loop, storage bridge and trigger route over a server-held
|
|
26
|
+
* context. It is the third registry injection point, beside the two pages.
|
|
27
|
+
*
|
|
23
28
|
* `app/harness-shell.tsx` exists because a `HarnessIntegration` holds React
|
|
24
29
|
* component references and MCP `run` functions. Next cannot serialize those
|
|
25
30
|
* across a server→client boundary, so the `'use client'` mark has to sit ABOVE
|
package/dist/harness-scaffold.js
CHANGED
|
@@ -21,11 +21,16 @@ export const VERSION_MARKER = '.ekanos-version';
|
|
|
21
21
|
export const HARNESS_CONFIG_FILENAME = 'harness.config.ts';
|
|
22
22
|
/**
|
|
23
23
|
* Every file in the generated shell: bundled template → path inside
|
|
24
|
-
* `.ekanos/harness/`. The
|
|
24
|
+
* `.ekanos/harness/`. The ten route files are mechanically derivable from the
|
|
25
25
|
* export map alone — which is the test of whether the package boundary is in
|
|
26
26
|
* the right place. If a shell file ever needs a judgment call, the boundary
|
|
27
27
|
* has moved, not the template.
|
|
28
28
|
*
|
|
29
|
+
* `app/api/integrations/[slug]/[...path]/route.ts` is the shell's one SERVER
|
|
30
|
+
* route: it injects the registry into `@ekanos/harness/api`, which serves the
|
|
31
|
+
* live-mode OAuth loop, storage bridge and trigger route over a server-held
|
|
32
|
+
* context. It is the third registry injection point, beside the two pages.
|
|
33
|
+
*
|
|
29
34
|
* `app/harness-shell.tsx` exists because a `HarnessIntegration` holds React
|
|
30
35
|
* component references and MCP `run` functions. Next cannot serialize those
|
|
31
36
|
* across a server→client boundary, so the `'use client'` mark has to sit ABOVE
|
|
@@ -64,6 +69,10 @@ export const SHELL_FILES = [
|
|
|
64
69
|
template: 'harness/app/[slug]/triggers/page.tsx.tmpl',
|
|
65
70
|
dest: 'app/[slug]/triggers/page.tsx',
|
|
66
71
|
},
|
|
72
|
+
{
|
|
73
|
+
template: 'harness/app/api/integrations/[slug]/[...path]/route.ts.tmpl',
|
|
74
|
+
dest: 'app/api/integrations/[slug]/[...path]/route.ts',
|
|
75
|
+
},
|
|
67
76
|
{ template: 'harness/next.config.mjs.tmpl', dest: 'next.config.mjs' },
|
|
68
77
|
{ template: 'harness/postcss.config.mjs.tmpl', dest: 'postcss.config.mjs' },
|
|
69
78
|
{ template: 'harness/tsconfig.json.tmpl', dest: 'tsconfig.json' },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-scaffold.js","sourceRoot":"","sources":["../src/harness-scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,gEAAgE;AAChE,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEjD,qEAAqE;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AAE3D,yEAAyE;AACzE,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEhD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,WAAW,GAAkD;IACxE,EAAE,QAAQ,EAAE,6BAA6B,EAAE,IAAI,EAAE,gBAAgB,EAAE;IACnE;QACE,QAAQ,EAAE,oCAAoC;QAC9C,IAAI,EAAE,uBAAuB;KAC9B;IACD,EAAE,QAAQ,EAAE,2BAA2B,EAAE,IAAI,EAAE,cAAc,EAAE;IAC/D;QACE,QAAQ,EAAE,oCAAoC;QAC9C,IAAI,EAAE,uBAAuB;KAC9B;IACD;QACE,QAAQ,EAAE,0CAA0C;QACpD,IAAI,EAAE,6BAA6B;KACpC;IACD;QACE,QAAQ,EAAE,qDAAqD;QAC/D,IAAI,EAAE,wCAAwC;KAC/C;IACD;QACE,QAAQ,EAAE,uCAAuC;QACjD,IAAI,EAAE,0BAA0B;KACjC;IACD;QACE,QAAQ,EAAE,6CAA6C;QACvD,IAAI,EAAE,gCAAgC;KACvC;IACD;QACE,QAAQ,EAAE,2CAA2C;QACrD,IAAI,EAAE,8BAA8B;KACrC;IACD,EAAE,QAAQ,EAAE,8BAA8B,EAAE,IAAI,EAAE,iBAAiB,EAAE;IACrE,EAAE,QAAQ,EAAE,iCAAiC,EAAE,IAAI,EAAE,oBAAoB,EAAE;IAC3E,EAAE,QAAQ,EAAE,4BAA4B,EAAE,IAAI,EAAE,eAAe,EAAE;IACjE,EAAE,QAAQ,EAAE,iCAAiC,EAAE,IAAI,EAAE,oBAAoB,EAAE;IAC3E,EAAE,QAAQ,EAAE,2BAA2B,EAAE,IAAI,EAAE,cAAc,EAAE;IAC/D;QACE,QAAQ,EAAE,uCAAuC;QACjD,IAAI,EAAE,0BAA0B;KACjC;CACF,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,eAAe;IACf,MAAM;IACN,OAAO;IACP,WAAW;IACX,aAAa;IACb,sBAAsB;CACd,CAAC;AAgCX,MAAM,UAAU,4BAA4B,CAC1C,UAAkB,EAClB,WAAmB;IAEnB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAC5B,UAAU,EACV,cAAc,EACd,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,EACzB,cAAc,CACf,CAAC;IACF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,MAAM,OAAO,GACX,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAC3C,CAAC,CAAE,MAAgC,CAAC,OAAO;YAC3C,CAAC,CAAC,SAAS,CAAC;QAChB,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACrE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,iBAAiB,CACrB,GAAG,WAAW,kDACZ,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,EACF,6BAA6B,UAAU,OAAO,WAAW,YAAY,CACtE,CAAC;IACJ,CAAC;AACH,CAAC;AAKD,MAAM,UAAU,oBAAoB,CAClC,UAAkB;IAElB,OAAO,4BAA4B,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;AACnE,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,2BAA2B,CAAC,UAAkB;;IAC5D,OAAO,MAAA,MAAA,oBAAoB,CAAC,UAAU,CAAC,0CAAE,OAAO,mCAAI,IAAI,CAAC;AAC3D,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,OAAO,qBAAqB,CAAC,MAAM,CACjC,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,EAAE,CAAC,UAAU,CACZ,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,CAAC,CAC5D,CACJ,CAAC;AACJ,CAAC;AAqCD,MAAM,UAAU,oBAAoB,CAAC,UAAkB;;IACrD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACrD,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAEhE,MAAM,GAAG,GAAG,CAAC,MAAA,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,mCAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAEjD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAChF,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,6EAA6E;IAC7E,wEAAwE;IACxE,kEAAkE;IAClE,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,6DAA6D;IAC7D,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;YAC1B,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;YAC9C,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAiC,CAAC;QACjD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC7C,OAAO;YACL,OAAO;YACP,GAAG,EAAE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;YACvD,SAAS,EAAE,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;SAC1E,CAAC;IACJ,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,MAAM,KAAK,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,UAAkB;;IACjD,OAAO,MAAA,MAAA,eAAe,CAAC,UAAU,CAAC,0CAAE,OAAO,mCAAI,IAAI,CAAC;AACtD,CAAC;AAED,SAAS,eAAe,CAAC,MAAmB;IAC1C,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,QAAgB,EAChB,QAAgB,EAChB,IAAkB,EAClB,KAAa,EACb,KAAc;IAEd,wEAAwE;IACxE,yEAAyE;IACzE,0EAA0E;IAC1E,uBAAuB;IACvB,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC;IAElC,IACE,CAAC,KAAK;QACN,IAAI,KAAK,MAAM;QACf,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,QAAQ,EAC7C,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO;IACT,CAAC;IAED,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC5C,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACrD,CAAC;AA2BD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB;;IACnD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAC3D,oBAAoB,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAE7C,MAAM,WAAW,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;IACrD,MAAM,cAAc,GAAG,WAAW,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7E,MAAM,eAAe,GAAG,MAAA,cAAc,aAAd,cAAc,uBAAd,cAAc,CAAE,OAAO,mCAAI,IAAI,CAAC;IACxD,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,IAAI,CAAC,cAAc;QAC5B,GAAG,EAAE,UAAU,EAAE;QACjB,SAAS,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;KACpE,CAAC;IAEF,uEAAuE;IACvE,4EAA4E;IAC5E,8DAA8D;IAC9D,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,IAAI,WAAW,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACnC,YAAY,CAAC,IAAI,CACf,GAAG,cAAc,sDAAsD;YACrE,WAAW,CACd,CAAC;IACJ,CAAC;IACD,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,IAAI,cAAc,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9C,YAAY,CAAC,IAAI,CACf,mBAAmB,cAAc,CAAC,OAAO,MAAM,MAAM,CAAC,OAAO,EAAE,CAChE,CAAC;QACJ,CAAC;QACD,IAAI,cAAc,CAAC,GAAG,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC;YACtC,YAAY,CAAC,IAAI,CACf,eAAe,MAAA,cAAc,CAAC,GAAG,mCAAI,SAAS,MAAM,MAAM,CAAC,GAAG,EAAE,CACjE,CAAC;QACJ,CAAC;QACD,IAAI,cAAc,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;YAClD,YAAY,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IAEtC,MAAM,KAAK,GAAiB,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACxE,MAAM,IAAI,GAAG;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,YAAY,EAAE,iBAAiB,CAAC,MAAA,IAAI,CAAC,WAAW,mCAAI,EAAE,CAAC;KACxD,CAAC;IAEF,KAAK,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,CAAC;QAC7C,cAAc,CACZ,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,EAC3B,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,EAC5B,KAAK,EACL,IAAI,EACJ,IAAI,CAAC,KAAK,CACX,CAAC;IACJ,CAAC;IAED,cAAc,CACZ,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,EACrC,eAAe,CAAC,MAAM,CAAC,EACvB,KAAK,EACL,cAAc,EACd,IAAI,CAAC,KAAK,CACX,CAAC;IAEF,uCACK,KAAK,KACR,UAAU;QACV,eAAe;QACf,cAAc;QACd,KAAK;QACL,YAAY,IACZ;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,KAAwB;IACjD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,gBAAgB,GAAG,IAAI,IAAI,CAAC,CAAC;IAC3E,OAAO,+CAA+C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AAC7E,CAAC;AAED,kEAAkE;AAClE,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAErC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAkB,EAClB,KAAc;IAEd,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IAEtC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,iBAAiB,CACrB,GAAG,UAAU,2DAA2D,EACxE,UAAU,UAAU,qDAAqD;YACvE,qBAAqB,CACxB,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QAC/C,MAAM,iBAAiB,CACrB,GAAG,UAAU,iCAAiC,EAC9C,kBAAkB,UAAU,6BAA6B,CAC1D,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEpC,MAAM,KAAK,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAE/C,yEAAyE;IACzE,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC;IACrD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IAEnD,IAAI,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,MAAM,iBAAiB,CACrB,GAAG,UAAU,kCAAkC,cAAc,WAAW;QACtE,0CAA0C,EAC5C,UAAU,UAAU,6CAA6C;QAC/D,mEAAmE;QACnE,gCAAgC,CACnC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACxE,OAAO,UAAU,CAAC,OAAO,CAAC,oCAAoC,EAAE,EAAE,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,SAAS,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAE9C,4CAA4C;IAC5C,IAAI,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/C,MAAM,iBAAiB,CACrB,uEAAuE;YACrE,wBAAwB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,EACtD,oEAAoE;YAClE,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,SAAS,EAAE,CAAC,CAAC;AAC1C,CAAC;AAQD;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAkB,EAClB,YAAwD;IAExD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,uBAAuB,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IAEtC,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,iBAAiB,CACrB,GAAG,UAAU,2DAA2D,EACxE,WAAW,UAAU,8CAA8C;YACjE,uCAAuC,CAC1C,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC7C,CAAC;IAED,iBAAiB,CAAC,UAAU,EAAE,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC;IAE7E,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,mBAAmB,CAC1B,YAAwD;IAExD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,iBAAiB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CACV,2BAA2B,KAAK,WAAW,kBAAkB,CAC3D,WAAW,CAAC,KAAK,CAClB,GAAG,CACL,CAAC;QACF,OAAO,CAAC,IAAI,CACV,YAAY,CAAC,8BAA8B,EAAE;YAC3C,KAAK,EAAE,KAAK;YACZ,IAAI,EAAE,WAAW,CAAC,IAAI;SACvB,CAAC,CACH,CAAC;IACJ,CAAC;IAED,OAAO,YAAY,CAAC,wBAAwB,EAAE;QAC5C,mBAAmB,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;QACvC,mBAAmB,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;KACtC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAC7D,IAAI,CAAC,WAAW,EAAE,CACnB,CAAC;IACF,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AACpD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;;IACrD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,2EAA2E,eAAe,IAAI,CAAC;IAE7G,MAAM,IAAI,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAEzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,iBAAiB,CACrB,GAAG,aAAa,uDAAuD,EACvE,QAAQ,eAAe,iDAAiD;YACtE,GAAG,aAAa,iDAAiD,CACpE,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,iBAAiB,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,MAAA,gBAAgB,CAAC,UAAU,EAAE,aAAa,CAAC,mCAAI,EAAE,CAAC;IACnE,IAAI,eAAe,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAE5C,MAAM,SAAS,GACb,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,kBAAkB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,SAAS,KAAK,KAAK,EAAE,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC;IAC9E,OAAO,MAAM;SACV,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { cliVersion, templatesFingerprint } from './compatibility';\nimport { invalidStateError, preconditionError } from './errors';\nimport {\n appendFileNoFollow,\n classifyPath,\n readFileNoFollow,\n writeFileNoFollow,\n} from './safe-fs';\nimport { readTemplate } from './templates';\n\n/** The dependency that owns every line of harness behaviour. */\nexport const HARNESS_PACKAGE = '@ekanos/harness';\n\n/** Where the generated shell lives, relative to the project root. */\nexport const HARNESS_DIR = path.join('.ekanos', 'harness');\n\n/** The single `.gitignore` line that covers the whole generated tree. */\nexport const GITIGNORE_ENTRY = '.ekanos/';\n\n/**\n * The staleness marker, inside the generated tree so deleting the tree also\n * deletes the marker. Holds the `@ekanos/harness` version that generated the\n * shell — a version bump means the shell may be wired against an older export\n * surface, so it is regenerated.\n */\nexport const VERSION_MARKER = '.ekanos-version';\n\n/** The partner-owned registry, at the PROJECT ROOT. Never regenerated. */\nexport const HARNESS_CONFIG_FILENAME = 'harness.config.ts';\n\n/**\n * Every file in the generated shell: bundled template → path inside\n * `.ekanos/harness/`. The nine route files are mechanically derivable from the\n * export map alone — which is the test of whether the package boundary is in\n * the right place. If a shell file ever needs a judgment call, the boundary\n * has moved, not the template.\n *\n * `app/harness-shell.tsx` exists because a `HarnessIntegration` holds React\n * component references and MCP `run` functions. Next cannot serialize those\n * across a server→client boundary, so the `'use client'` mark has to sit ABOVE\n * the point where the registry is injected — hence a client shell of its own\n * rather than a client `RootLayout` called from a server layout. Keeping\n * `app/page.tsx` a server component is what lets `/` prerender static.\n */\nexport const SHELL_FILES: readonly { template: string; dest: string }[] = [\n { template: 'harness/app/layout.tsx.tmpl', dest: 'app/layout.tsx' },\n {\n template: 'harness/app/harness-shell.tsx.tmpl',\n dest: 'app/harness-shell.tsx',\n },\n { template: 'harness/app/page.tsx.tmpl', dest: 'app/page.tsx' },\n {\n template: 'harness/app/[slug]/layout.tsx.tmpl',\n dest: 'app/[slug]/layout.tsx',\n },\n {\n template: 'harness/app/[slug]/widgets/page.tsx.tmpl',\n dest: 'app/[slug]/widgets/page.tsx',\n },\n {\n template: 'harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl',\n dest: 'app/[slug]/widgets/[widgetId]/page.tsx',\n },\n {\n template: 'harness/app/[slug]/tile/page.tsx.tmpl',\n dest: 'app/[slug]/tile/page.tsx',\n },\n {\n template: 'harness/app/[slug]/activation/page.tsx.tmpl',\n dest: 'app/[slug]/activation/page.tsx',\n },\n {\n template: 'harness/app/[slug]/triggers/page.tsx.tmpl',\n dest: 'app/[slug]/triggers/page.tsx',\n },\n { template: 'harness/next.config.mjs.tmpl', dest: 'next.config.mjs' },\n { template: 'harness/postcss.config.mjs.tmpl', dest: 'postcss.config.mjs' },\n { template: 'harness/tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'harness/styles/globals.css.tmpl', dest: 'styles/globals.css' },\n { template: 'harness/package.json.tmpl', dest: 'package.json' },\n {\n template: 'harness/types/team-accounts.d.ts.tmpl',\n dest: 'types/team-accounts.d.ts',\n },\n];\n\n/**\n * Packages the shell needs to resolve upward out of the partner's own\n * `node_modules`. The shell declares no dependencies of its own by design, so\n * a missing one shows up as an opaque Next resolution error at dev-server\n * start; checking here turns that into one precondition with one fix.\n */\nexport const REQUIRED_DEPENDENCIES = [\n HARNESS_PACKAGE,\n 'next',\n 'react',\n 'react-dom',\n 'tailwindcss',\n '@tailwindcss/postcss',\n] as const;\n\nexport interface WrittenFiles {\n created: string[];\n updated: string[];\n unchanged: string[];\n}\n\n/**\n * Read an installed package's `package.json` out of a project's TOP-LEVEL\n * `node_modules`. Resolved by direct path join rather than `require.resolve`\n * on purpose, for two independent reasons:\n *\n * - The CLI runs from its own install and must read the version of the copy\n * in THE CALLER'S project, not one hoisted next to itself.\n * - Several of the packages this reads (`@ekanos/integration-schema`\n * included) declare an `exports` map with no `./package.json` subpath, so\n * `require.resolve('<pkg>/package.json')` throws\n * `ERR_PACKAGE_PATH_NOT_EXPORTED` under Node's ESM resolver. A plain file\n * read is not subject to the export map at all.\n *\n * Shared by every \"what does this project have installed?\" check in the\n * CLI — `readInstalledHarness` below, the schema-skew gate, and `upgrade`'s\n * current-toolchain report — so there is exactly one implementation of \"read\n * a package's on-disk version\", not a subtly different one per call site.\n */\nexport interface InstalledPackage {\n version: string;\n /** The raw package.json, for callers that need more than the version. */\n manifest: unknown;\n}\n\nexport function readInstalledPackageManifest(\n projectDir: string,\n packageName: string,\n): InstalledPackage | null {\n const manifestPath = path.join(\n projectDir,\n 'node_modules',\n ...packageName.split('/'),\n 'package.json',\n );\n if (!fs.existsSync(manifestPath)) return null;\n\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n if (typeof version !== 'string' || version.length === 0) return null;\n return { version, manifest: parsed };\n } catch (error) {\n throw preconditionError(\n `${packageName}'s package.json in node_modules is unreadable: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Reinstall dependencies in ${projectDir} so ${packageName} resolves.`,\n );\n }\n}\n\n/** Read the installed `@ekanos/harness` version out of the partner's project. */\nexport type InstalledHarness = InstalledPackage;\n\nexport function readInstalledHarness(\n projectDir: string,\n): InstalledHarness | null {\n return readInstalledPackageManifest(projectDir, HARNESS_PACKAGE);\n}\n\n/** Just the installed version, for callers that need nothing else. */\nexport function readInstalledHarnessVersion(projectDir: string): string | null {\n return readInstalledHarness(projectDir)?.version ?? null;\n}\n\n/** Which of `REQUIRED_DEPENDENCIES` are not installed in the project root. */\nexport function missingDependencies(projectDir: string): string[] {\n return REQUIRED_DEPENDENCIES.filter(\n (name) =>\n !fs.existsSync(\n path.join(projectDir, 'node_modules', name, 'package.json'),\n ),\n );\n}\n\n/**\n * What generated an existing shell.\n *\n * All three fields matter, and recording only `harness` was a real hole: the\n * templates ship with the CLI, so a shell can be stale because the CLI changed\n * while the harness did not. `templates` is the authoritative one — it is a\n * hash of the template sources, so it moves whenever what we generate moves,\n * including for a CLI build that never shipped a version bump.\n */\nexport interface ShellMarker {\n harness: string;\n /** null for a legacy bare-version marker written before this was recorded. */\n cli: string | null;\n templates: string | null;\n}\n\n/**\n * Three outcomes, not two, because \"no marker\" and \"unreadable marker\" call\n * for opposite responses.\n *\n * A marker's PRESENCE is itself evidence the tree is ours — a squatter does\n * not write `.ekanos-version` — and the likeliest cause of an unreadable one\n * is our own interrupted write. Everything under `.ekanos/harness/` is\n * CLI-owned, gitignored and reproducible from the templates, so regenerating\n * destroys nothing: there is no value at risk to justify charging a partner a\n * confusing `--force` to recover from our own bad write.\n *\n * An ABSENT marker over a non-empty tree is the opposite: nothing says the\n * files are ours, so we refuse. That is the L1 rule and it stays.\n */\nexport type MarkerState =\n | { kind: 'missing' }\n | { kind: 'corrupt' }\n | { kind: 'ok'; marker: ShellMarker };\n\nexport function readShellMarkerState(harnessDir: string): MarkerState {\n const marker = path.join(harnessDir, VERSION_MARKER);\n if (classifyPath(marker) !== 'file') return { kind: 'missing' };\n\n const raw = (readFileNoFollow(harnessDir, marker) ?? '').trim();\n if (raw.length === 0) return { kind: 'corrupt' };\n\n const parsed = parseMarker(raw);\n return parsed === null ? { kind: 'corrupt' } : { kind: 'ok', marker: parsed };\n}\n\nfunction parseMarker(raw: string): ShellMarker | null {\n // v1 markers were a bare version string. Read them rather than crashing, and\n // report the unknown fields as null so the shell counts as stale and is\n // regenerated once — which is the correct outcome for an upgrade.\n //\n // It must actually LOOK like a version: without this, any garbage that does\n // not happen to start with `{` — a truncated write, a stray word — reads as\n // a legacy marker named after the garbage, and the corrupt-marker path never\n // fires. That is how a truncated `.ekanos-version` was silently reported as\n // \"@ekanos/harness truncat → 0.1.0\" instead of \"unreadable\".\n if (!raw.startsWith('{')) {\n return /^\\d+\\.\\d+/.test(raw)\n ? { harness: raw, cli: null, templates: null }\n : null;\n }\n\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null) return null;\n const record = parsed as Record<string, unknown>;\n const harness = record.harness;\n if (typeof harness !== 'string') return null;\n return {\n harness,\n cli: typeof record.cli === 'string' ? record.cli : null,\n templates: typeof record.templates === 'string' ? record.templates : null,\n };\n } catch {\n return null;\n }\n}\n\n/** The parsed marker, or null when it is absent OR unreadable. */\nexport function readShellMarker(harnessDir: string): ShellMarker | null {\n const state = readShellMarkerState(harnessDir);\n return state.kind === 'ok' ? state.marker : null;\n}\n\n/** The `@ekanos/harness` version an existing shell was generated against. */\nexport function readShellVersion(harnessDir: string): string | null {\n return readShellMarker(harnessDir)?.harness ?? null;\n}\n\nfunction serializeMarker(marker: ShellMarker): string {\n return `${JSON.stringify(marker, null, 2)}\\n`;\n}\n\n/**\n * Write a generated file, but ONLY when its bytes would change.\n *\n * This is what makes a re-run cheap and a watcher quiet: an unchanged file is\n * not rewritten, so its mtime does not move and Next's dev server does not\n * rebuild. It also means a hand-edit inside the CLI-owned tree is silently\n * corrected on the next run, which is the intended contract for `.ekanos/`.\n */\nexport function writeIfChanged(\n root: string,\n destPath: string,\n contents: string,\n into: WrittenFiles,\n label: string,\n force: boolean,\n): void {\n // `classifyPath` uses lstat, so a symlink here is reported as a symlink\n // rather than as whatever it points at — including a DANGLING one, which\n // `existsSync` would call absent and a plain write would then materialize\n // outside the project.\n const kind = classifyPath(destPath);\n const exists = kind !== 'missing';\n\n if (\n !force &&\n kind === 'file' &&\n readFileNoFollow(root, destPath) === contents\n ) {\n into.unchanged.push(label);\n return;\n }\n\n writeFileNoFollow(root, destPath, contents);\n (exists ? into.updated : into.created).push(label);\n}\n\nexport interface ScaffoldShellArgs {\n projectDir: string;\n slug: string;\n harnessVersion: string;\n /** Rewrite every file even when its bytes already match. */\n force: boolean;\n /** Extra Tailwind @source globs from ekanos.json, already validated. */\n sourceGlobs?: readonly string[];\n}\n\nexport interface ScaffoldShellResult extends WrittenFiles {\n harnessDir: string;\n /** The version the shell carried before this run — null on a first run. */\n previousVersion: string | null;\n /** What generated the previous shell, or null on a first run. */\n previousMarker: ShellMarker | null;\n /** True when anything that generated the shell has since changed. */\n stale: boolean;\n /**\n * Which of harness / cli / templates moved. Empty on a first run; this is\n * what lets `dev` say WHY it regenerated rather than just that it did.\n */\n staleReasons: string[];\n}\n\n/**\n * Generate `.ekanos/harness/**` for `harnessVersion`. Every file here is\n * CLI-owned and gitignored, so regeneration needs no permission and asks no\n * question — the partner owns `harness.config.ts` and their integration\n * source, and nothing else.\n */\nexport function scaffoldShell(args: ScaffoldShellArgs): ScaffoldShellResult {\n const harnessDir = path.join(args.projectDir, HARNESS_DIR);\n assertShellDirIsOurs(harnessDir, args.force);\n\n const markerState = readShellMarkerState(harnessDir);\n const previousMarker = markerState.kind === 'ok' ? markerState.marker : null;\n const previousVersion = previousMarker?.harness ?? null;\n const marker: ShellMarker = {\n harness: args.harnessVersion,\n cli: cliVersion(),\n templates: templatesFingerprint(SHELL_FILES.map((f) => f.template)),\n };\n\n // A shell is current only if EVERYTHING that produced it is unchanged.\n // Checking the harness version alone certified shells generated by an older\n // CLI, against templates that had since moved, as up to date.\n const staleReasons: string[] = [];\n if (markerState.kind === 'corrupt') {\n staleReasons.push(\n `${VERSION_MARKER} was unreadable, so the existing shell could not be ` +\n 'certified',\n );\n }\n if (previousMarker !== null) {\n if (previousMarker.harness !== marker.harness) {\n staleReasons.push(\n `@ekanos/harness ${previousMarker.harness} → ${marker.harness}`,\n );\n }\n if (previousMarker.cli !== marker.cli) {\n staleReasons.push(\n `@ekanos/cli ${previousMarker.cli ?? 'unknown'} → ${marker.cli}`,\n );\n }\n if (previousMarker.templates !== marker.templates) {\n staleReasons.push('the shell templates changed');\n }\n }\n const stale = staleReasons.length > 0;\n\n const files: WrittenFiles = { created: [], updated: [], unchanged: [] };\n const vars = {\n SLUG: args.slug,\n SOURCE_GLOBS: renderSourceGlobs(args.sourceGlobs ?? []),\n };\n\n for (const { template, dest } of SHELL_FILES) {\n writeIfChanged(\n args.projectDir,\n path.join(harnessDir, dest),\n readTemplate(template, vars),\n files,\n dest,\n args.force,\n );\n }\n\n writeIfChanged(\n args.projectDir,\n path.join(harnessDir, VERSION_MARKER),\n serializeMarker(marker),\n files,\n VERSION_MARKER,\n args.force,\n );\n\n return {\n ...files,\n harnessDir,\n previousVersion,\n previousMarker,\n stale,\n staleReasons,\n };\n}\n\n/**\n * Turn validated `sourceGlobs` into the `@source` lines the generated\n * stylesheet carries, each relative to the shell's `styles/` directory.\n *\n * Tailwind never scans what it is not pointed at, and a glob that matches\n * nothing fails silently — so an integration living outside the default\n * layout renders completely unstyled with no error anywhere. That is what\n * `sourceGlobs` is for, and until now it was parsed and thrown away.\n */\nfunction renderSourceGlobs(globs: readonly string[]): string {\n if (globs.length === 0) return '';\n const lines = globs.map((glob) => `@source '${SHELL_TO_PROJECT}${glob}';`);\n return `\\n/* From \"sourceGlobs\" in ekanos.json. */\\n${lines.join('\\n')}\\n`;\n}\n\n/** From `.ekanos/harness/styles/` back up to the project root. */\nconst SHELL_TO_PROJECT = '../../../';\n\n/**\n * Establish that `.ekanos/harness` is a tree WE generated before writing a\n * single byte into it, and return the version marker it carried.\n *\n * Path location alone is not ownership. A directory that exists but carries no\n * `.ekanos-version` was put there by something else — a partner, another tool,\n * a symlink — and clobbering it because it happens to sit at the path we like\n * is the same class of mistake as following a symlink. An empty directory is\n * fine (that is just `mkdir -p` having run), and `--force` is the explicit\n * override.\n *\n * Runs BEFORE `bootstrapHarnessConfig` in `dev`, so a shell that cannot be\n * written leaves the project completely untouched rather than half-scaffolded.\n */\nexport function assertShellDirIsOurs(\n harnessDir: string,\n force: boolean,\n): string | null {\n const kind = classifyPath(harnessDir);\n\n if (kind === 'symlink') {\n throw invalidStateError(\n `${harnessDir} is a symlink, so \"ekanos dev\" will not write through it.`,\n `Delete ${harnessDir} and re-run \"ekanos dev\" — the shell is generated, ` +\n 'so nothing is lost.',\n );\n }\n\n if (kind !== 'missing' && kind !== 'directory') {\n throw invalidStateError(\n `${harnessDir} exists and is not a directory.`,\n `Move or delete ${harnessDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n if (kind === 'missing') return null;\n\n const state = readShellMarkerState(harnessDir);\n\n // A marker we wrote, however mangled, still identifies the tree as ours.\n if (state.kind === 'ok') return state.marker.harness;\n if (state.kind === 'corrupt' || force) return null;\n\n if (fs.readdirSync(harnessDir).length === 0) return null;\n\n throw invalidStateError(\n `${harnessDir} already contains files but no ${VERSION_MARKER} marker, ` +\n 'so it was not generated by \"ekanos dev\".',\n `Delete ${harnessDir} if you do not need it, or pass --force to ` +\n 'overwrite it. Everything \"ekanos dev\" generates is reproducible; ' +\n 'anything else in there is not.',\n );\n}\n\n/**\n * The module specifier `harness.config.ts` uses to import the partner's\n * integration: the `entry` from ekanos.json, made relative and extension-free.\n * \"src/integration.ts\" → \"src/integration\".\n */\nexport function entryImportSpecifier(entry: string): string {\n const normalized = entry.split(path.sep).join('/').replace(/^\\.\\//, '');\n return normalized.replace(/\\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/, '');\n}\n\n/**\n * The same specifier as a JavaScript string LITERAL, ready to drop into a\n * generated `import` statement.\n *\n * `entry` is attacker-influenced data from `ekanos.json`, and a POSIX filename\n * may legally contain a quote or a newline. Interpolating it raw would let a\n * crafted-but-valid path close the string and append statements to the module\n * we generate. `JSON.stringify` produces a correctly escaped literal, which is\n * the actual fix; the explicit rejection below is a second line that also\n * gives the partner a comprehensible error instead of a bizarre filename\n * silently appearing, escaped, in their config.\n *\n * This does not cross a trust boundary on its own — a partner's own code runs\n * unsandboxed anyway — but \"the templates cannot be injected into\" is a claim\n * we make, and it should be true.\n */\nexport function entryImportLiteral(entry: string): string {\n const specifier = entryImportSpecifier(entry);\n\n // eslint-disable-next-line no-control-regex\n if (/[\"'`\\\\\\n\\r\\u0000-\\u001f]/.test(specifier)) {\n throw preconditionError(\n `The \"entry\" in ekanos.json contains characters that cannot appear in ` +\n `an import specifier: ${JSON.stringify(specifier)}.`,\n 'Rename the file so its path has no quotes, backslashes or control ' +\n 'characters, then update \"entry\" in ekanos.json to match.',\n );\n }\n\n return JSON.stringify(`./${specifier}`);\n}\n\nexport interface BootstrapConfigResult {\n /** True only when this run created the file. */\n bootstrapped: boolean;\n configPath: string;\n}\n\n/**\n * Write a starter `harness.config.ts` at the project root — ONCE.\n *\n * An existing file is left strictly alone, byte for byte, and no flag changes\n * that: it is the one harness file the partner owns, it is git-tracked, and\n * regenerating it would throw away their fixtures. `--force` regenerates the\n * shell, never this.\n */\nexport function bootstrapHarnessConfig(\n projectDir: string,\n integrations: readonly { slug: string; entry: string }[],\n): BootstrapConfigResult {\n const configPath = path.join(projectDir, HARNESS_CONFIG_FILENAME);\n const kind = classifyPath(configPath);\n\n // A symlink here — including a dangling one, which `existsSync` reports as\n // absent — would have us write through to its target. Since this file is the\n // partner's to own, the only safe answer is to touch nothing and say why.\n if (kind === 'symlink') {\n throw invalidStateError(\n `${configPath} is a symlink, so \"ekanos dev\" will not write through it.`,\n `Replace ${configPath} with a real file (or delete it to have one ` +\n 'generated), then re-run \"ekanos dev\".',\n );\n }\n\n if (kind !== 'missing') {\n return { bootstrapped: false, configPath };\n }\n\n writeFileNoFollow(projectDir, configPath, renderHarnessConfig(integrations));\n\n return { bootstrapped: true, configPath };\n}\n\n/**\n * Render the starter registry for EVERY integration the project declares.\n *\n * The harness registry is an array and `ekanos.json` can now hold a list, so a\n * project's second integration reaches the harness the same way its first\n * does. Each entry imports the definition under a distinct local identifier\n * derived from its slug, because two `integration` imports in one module do\n * not compile.\n */\nfunction renderHarnessConfig(\n integrations: readonly { slug: string; entry: string }[],\n): string {\n const imports: string[] = [];\n const entries: string[] = [];\n\n for (const integration of integrations) {\n const ident = identifierForSlug(integration.slug);\n imports.push(\n `import { integration as ${ident} } from ${entryImportLiteral(\n integration.entry,\n )};`,\n );\n entries.push(\n readTemplate('harness.config.entry.ts.tmpl', {\n IDENT: ident,\n SLUG: integration.slug,\n }),\n );\n }\n\n return readTemplate('harness.config.ts.tmpl', {\n INTEGRATION_IMPORTS: imports.join('\\n'),\n INTEGRATION_ENTRIES: entries.join(''),\n });\n}\n\n/**\n * \"acme-crm\" → \"acmeCrm\". A slug is kebab-case by schema, so this only has to\n * handle hyphens, and a leading digit is impossible.\n */\nexport function identifierForSlug(slug: string): string {\n const camel = slug.replace(/-([a-z0-9])/g, (_, char: string) =>\n char.toUpperCase(),\n );\n return /^[0-9]/.test(camel) ? `i${camel}` : camel;\n}\n\n/**\n * Add `.ekanos/` to the project `.gitignore`, exactly once.\n *\n * Matching is line-wise over the spellings git treats as the same rule\n * (`.ekanos`, `.ekanos/`, `/.ekanos/`), so re-running never appends a\n * duplicate — the thing that makes a scaffold verb safe to run in a loop.\n * A negation (`!.ekanos/`) is deliberately NOT treated as a match: the\n * partner meant to un-ignore it, and we do not fight that silently.\n */\nexport function ensureGitignoreEntry(projectDir: string): boolean {\n const gitignorePath = path.join(projectDir, '.gitignore');\n const block = `# Ekanos dev harness — generated by \"ekanos dev\", never edited by hand\\n${GITIGNORE_ENTRY}\\n`;\n\n const kind = classifyPath(gitignorePath);\n\n if (kind === 'symlink') {\n throw invalidStateError(\n `${gitignorePath} is a symlink, so \"ekanos dev\" will not append to it.`,\n `Add \"${GITIGNORE_ENTRY}\" to your real .gitignore yourself, or replace ` +\n `${gitignorePath} with a regular file, then re-run \"ekanos dev\".`,\n );\n }\n\n if (kind === 'missing') {\n writeFileNoFollow(projectDir, gitignorePath, block);\n return true;\n }\n\n const existing = readFileNoFollow(projectDir, gitignorePath) ?? '';\n if (gitignoreCovers(existing)) return false;\n\n const separator =\n existing.length === 0 || existing.endsWith('\\n') ? '' : '\\n';\n appendFileNoFollow(projectDir, gitignorePath, `${separator}\\n${block}`);\n return true;\n}\n\nfunction gitignoreCovers(source: string): boolean {\n const equivalents = new Set(['.ekanos', '.ekanos/', '/.ekanos', '/.ekanos/']);\n return source\n .split('\\n')\n .map((line) => line.trim())\n .some((line) => equivalents.has(line));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"harness-scaffold.js","sourceRoot":"","sources":["../src/harness-scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { cliVersion, templatesFingerprint } from './compatibility';\nimport { invalidStateError, preconditionError } from './errors';\nimport {\n appendFileNoFollow,\n classifyPath,\n readFileNoFollow,\n writeFileNoFollow,\n} from './safe-fs';\nimport { readTemplate } from './templates';\n\n/** The dependency that owns every line of harness behaviour. */\nexport const HARNESS_PACKAGE = '@ekanos/harness';\n\n/** Where the generated shell lives, relative to the project root. */\nexport const HARNESS_DIR = path.join('.ekanos', 'harness');\n\n/** The single `.gitignore` line that covers the whole generated tree. */\nexport const GITIGNORE_ENTRY = '.ekanos/';\n\n/**\n * The staleness marker, inside the generated tree so deleting the tree also\n * deletes the marker. Holds the `@ekanos/harness` version that generated the\n * shell — a version bump means the shell may be wired against an older export\n * surface, so it is regenerated.\n */\nexport const VERSION_MARKER = '.ekanos-version';\n\n/** The partner-owned registry, at the PROJECT ROOT. Never regenerated. */\nexport const HARNESS_CONFIG_FILENAME = 'harness.config.ts';\n\n/**\n * Every file in the generated shell: bundled template → path inside\n * `.ekanos/harness/`. The ten route files are mechanically derivable from the\n * export map alone — which is the test of whether the package boundary is in\n * the right place. If a shell file ever needs a judgment call, the boundary\n * has moved, not the template.\n *\n * `app/api/integrations/[slug]/[...path]/route.ts` is the shell's one SERVER\n * route: it injects the registry into `@ekanos/harness/api`, which serves the\n * live-mode OAuth loop, storage bridge and trigger route over a server-held\n * context. It is the third registry injection point, beside the two pages.\n *\n * `app/harness-shell.tsx` exists because a `HarnessIntegration` holds React\n * component references and MCP `run` functions. Next cannot serialize those\n * across a server→client boundary, so the `'use client'` mark has to sit ABOVE\n * the point where the registry is injected — hence a client shell of its own\n * rather than a client `RootLayout` called from a server layout. Keeping\n * `app/page.tsx` a server component is what lets `/` prerender static.\n */\nexport const SHELL_FILES: readonly { template: string; dest: string }[] = [\n { template: 'harness/app/layout.tsx.tmpl', dest: 'app/layout.tsx' },\n {\n template: 'harness/app/harness-shell.tsx.tmpl',\n dest: 'app/harness-shell.tsx',\n },\n { template: 'harness/app/page.tsx.tmpl', dest: 'app/page.tsx' },\n {\n template: 'harness/app/[slug]/layout.tsx.tmpl',\n dest: 'app/[slug]/layout.tsx',\n },\n {\n template: 'harness/app/[slug]/widgets/page.tsx.tmpl',\n dest: 'app/[slug]/widgets/page.tsx',\n },\n {\n template: 'harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl',\n dest: 'app/[slug]/widgets/[widgetId]/page.tsx',\n },\n {\n template: 'harness/app/[slug]/tile/page.tsx.tmpl',\n dest: 'app/[slug]/tile/page.tsx',\n },\n {\n template: 'harness/app/[slug]/activation/page.tsx.tmpl',\n dest: 'app/[slug]/activation/page.tsx',\n },\n {\n template: 'harness/app/[slug]/triggers/page.tsx.tmpl',\n dest: 'app/[slug]/triggers/page.tsx',\n },\n {\n template: 'harness/app/api/integrations/[slug]/[...path]/route.ts.tmpl',\n dest: 'app/api/integrations/[slug]/[...path]/route.ts',\n },\n { template: 'harness/next.config.mjs.tmpl', dest: 'next.config.mjs' },\n { template: 'harness/postcss.config.mjs.tmpl', dest: 'postcss.config.mjs' },\n { template: 'harness/tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'harness/styles/globals.css.tmpl', dest: 'styles/globals.css' },\n { template: 'harness/package.json.tmpl', dest: 'package.json' },\n {\n template: 'harness/types/team-accounts.d.ts.tmpl',\n dest: 'types/team-accounts.d.ts',\n },\n];\n\n/**\n * Packages the shell needs to resolve upward out of the partner's own\n * `node_modules`. The shell declares no dependencies of its own by design, so\n * a missing one shows up as an opaque Next resolution error at dev-server\n * start; checking here turns that into one precondition with one fix.\n */\nexport const REQUIRED_DEPENDENCIES = [\n HARNESS_PACKAGE,\n 'next',\n 'react',\n 'react-dom',\n 'tailwindcss',\n '@tailwindcss/postcss',\n] as const;\n\nexport interface WrittenFiles {\n created: string[];\n updated: string[];\n unchanged: string[];\n}\n\n/**\n * Read an installed package's `package.json` out of a project's TOP-LEVEL\n * `node_modules`. Resolved by direct path join rather than `require.resolve`\n * on purpose, for two independent reasons:\n *\n * - The CLI runs from its own install and must read the version of the copy\n * in THE CALLER'S project, not one hoisted next to itself.\n * - Several of the packages this reads (`@ekanos/integration-schema`\n * included) declare an `exports` map with no `./package.json` subpath, so\n * `require.resolve('<pkg>/package.json')` throws\n * `ERR_PACKAGE_PATH_NOT_EXPORTED` under Node's ESM resolver. A plain file\n * read is not subject to the export map at all.\n *\n * Shared by every \"what does this project have installed?\" check in the\n * CLI — `readInstalledHarness` below, the schema-skew gate, and `upgrade`'s\n * current-toolchain report — so there is exactly one implementation of \"read\n * a package's on-disk version\", not a subtly different one per call site.\n */\nexport interface InstalledPackage {\n version: string;\n /** The raw package.json, for callers that need more than the version. */\n manifest: unknown;\n}\n\nexport function readInstalledPackageManifest(\n projectDir: string,\n packageName: string,\n): InstalledPackage | null {\n const manifestPath = path.join(\n projectDir,\n 'node_modules',\n ...packageName.split('/'),\n 'package.json',\n );\n if (!fs.existsSync(manifestPath)) return null;\n\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n if (typeof version !== 'string' || version.length === 0) return null;\n return { version, manifest: parsed };\n } catch (error) {\n throw preconditionError(\n `${packageName}'s package.json in node_modules is unreadable: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Reinstall dependencies in ${projectDir} so ${packageName} resolves.`,\n );\n }\n}\n\n/** Read the installed `@ekanos/harness` version out of the partner's project. */\nexport type InstalledHarness = InstalledPackage;\n\nexport function readInstalledHarness(\n projectDir: string,\n): InstalledHarness | null {\n return readInstalledPackageManifest(projectDir, HARNESS_PACKAGE);\n}\n\n/** Just the installed version, for callers that need nothing else. */\nexport function readInstalledHarnessVersion(projectDir: string): string | null {\n return readInstalledHarness(projectDir)?.version ?? null;\n}\n\n/** Which of `REQUIRED_DEPENDENCIES` are not installed in the project root. */\nexport function missingDependencies(projectDir: string): string[] {\n return REQUIRED_DEPENDENCIES.filter(\n (name) =>\n !fs.existsSync(\n path.join(projectDir, 'node_modules', name, 'package.json'),\n ),\n );\n}\n\n/**\n * What generated an existing shell.\n *\n * All three fields matter, and recording only `harness` was a real hole: the\n * templates ship with the CLI, so a shell can be stale because the CLI changed\n * while the harness did not. `templates` is the authoritative one — it is a\n * hash of the template sources, so it moves whenever what we generate moves,\n * including for a CLI build that never shipped a version bump.\n */\nexport interface ShellMarker {\n harness: string;\n /** null for a legacy bare-version marker written before this was recorded. */\n cli: string | null;\n templates: string | null;\n}\n\n/**\n * Three outcomes, not two, because \"no marker\" and \"unreadable marker\" call\n * for opposite responses.\n *\n * A marker's PRESENCE is itself evidence the tree is ours — a squatter does\n * not write `.ekanos-version` — and the likeliest cause of an unreadable one\n * is our own interrupted write. Everything under `.ekanos/harness/` is\n * CLI-owned, gitignored and reproducible from the templates, so regenerating\n * destroys nothing: there is no value at risk to justify charging a partner a\n * confusing `--force` to recover from our own bad write.\n *\n * An ABSENT marker over a non-empty tree is the opposite: nothing says the\n * files are ours, so we refuse. That is the L1 rule and it stays.\n */\nexport type MarkerState =\n | { kind: 'missing' }\n | { kind: 'corrupt' }\n | { kind: 'ok'; marker: ShellMarker };\n\nexport function readShellMarkerState(harnessDir: string): MarkerState {\n const marker = path.join(harnessDir, VERSION_MARKER);\n if (classifyPath(marker) !== 'file') return { kind: 'missing' };\n\n const raw = (readFileNoFollow(harnessDir, marker) ?? '').trim();\n if (raw.length === 0) return { kind: 'corrupt' };\n\n const parsed = parseMarker(raw);\n return parsed === null ? { kind: 'corrupt' } : { kind: 'ok', marker: parsed };\n}\n\nfunction parseMarker(raw: string): ShellMarker | null {\n // v1 markers were a bare version string. Read them rather than crashing, and\n // report the unknown fields as null so the shell counts as stale and is\n // regenerated once — which is the correct outcome for an upgrade.\n //\n // It must actually LOOK like a version: without this, any garbage that does\n // not happen to start with `{` — a truncated write, a stray word — reads as\n // a legacy marker named after the garbage, and the corrupt-marker path never\n // fires. That is how a truncated `.ekanos-version` was silently reported as\n // \"@ekanos/harness truncat → 0.1.0\" instead of \"unreadable\".\n if (!raw.startsWith('{')) {\n return /^\\d+\\.\\d+/.test(raw)\n ? { harness: raw, cli: null, templates: null }\n : null;\n }\n\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null) return null;\n const record = parsed as Record<string, unknown>;\n const harness = record.harness;\n if (typeof harness !== 'string') return null;\n return {\n harness,\n cli: typeof record.cli === 'string' ? record.cli : null,\n templates: typeof record.templates === 'string' ? record.templates : null,\n };\n } catch {\n return null;\n }\n}\n\n/** The parsed marker, or null when it is absent OR unreadable. */\nexport function readShellMarker(harnessDir: string): ShellMarker | null {\n const state = readShellMarkerState(harnessDir);\n return state.kind === 'ok' ? state.marker : null;\n}\n\n/** The `@ekanos/harness` version an existing shell was generated against. */\nexport function readShellVersion(harnessDir: string): string | null {\n return readShellMarker(harnessDir)?.harness ?? null;\n}\n\nfunction serializeMarker(marker: ShellMarker): string {\n return `${JSON.stringify(marker, null, 2)}\\n`;\n}\n\n/**\n * Write a generated file, but ONLY when its bytes would change.\n *\n * This is what makes a re-run cheap and a watcher quiet: an unchanged file is\n * not rewritten, so its mtime does not move and Next's dev server does not\n * rebuild. It also means a hand-edit inside the CLI-owned tree is silently\n * corrected on the next run, which is the intended contract for `.ekanos/`.\n */\nexport function writeIfChanged(\n root: string,\n destPath: string,\n contents: string,\n into: WrittenFiles,\n label: string,\n force: boolean,\n): void {\n // `classifyPath` uses lstat, so a symlink here is reported as a symlink\n // rather than as whatever it points at — including a DANGLING one, which\n // `existsSync` would call absent and a plain write would then materialize\n // outside the project.\n const kind = classifyPath(destPath);\n const exists = kind !== 'missing';\n\n if (\n !force &&\n kind === 'file' &&\n readFileNoFollow(root, destPath) === contents\n ) {\n into.unchanged.push(label);\n return;\n }\n\n writeFileNoFollow(root, destPath, contents);\n (exists ? into.updated : into.created).push(label);\n}\n\nexport interface ScaffoldShellArgs {\n projectDir: string;\n slug: string;\n harnessVersion: string;\n /** Rewrite every file even when its bytes already match. */\n force: boolean;\n /** Extra Tailwind @source globs from ekanos.json, already validated. */\n sourceGlobs?: readonly string[];\n}\n\nexport interface ScaffoldShellResult extends WrittenFiles {\n harnessDir: string;\n /** The version the shell carried before this run — null on a first run. */\n previousVersion: string | null;\n /** What generated the previous shell, or null on a first run. */\n previousMarker: ShellMarker | null;\n /** True when anything that generated the shell has since changed. */\n stale: boolean;\n /**\n * Which of harness / cli / templates moved. Empty on a first run; this is\n * what lets `dev` say WHY it regenerated rather than just that it did.\n */\n staleReasons: string[];\n}\n\n/**\n * Generate `.ekanos/harness/**` for `harnessVersion`. Every file here is\n * CLI-owned and gitignored, so regeneration needs no permission and asks no\n * question — the partner owns `harness.config.ts` and their integration\n * source, and nothing else.\n */\nexport function scaffoldShell(args: ScaffoldShellArgs): ScaffoldShellResult {\n const harnessDir = path.join(args.projectDir, HARNESS_DIR);\n assertShellDirIsOurs(harnessDir, args.force);\n\n const markerState = readShellMarkerState(harnessDir);\n const previousMarker = markerState.kind === 'ok' ? markerState.marker : null;\n const previousVersion = previousMarker?.harness ?? null;\n const marker: ShellMarker = {\n harness: args.harnessVersion,\n cli: cliVersion(),\n templates: templatesFingerprint(SHELL_FILES.map((f) => f.template)),\n };\n\n // A shell is current only if EVERYTHING that produced it is unchanged.\n // Checking the harness version alone certified shells generated by an older\n // CLI, against templates that had since moved, as up to date.\n const staleReasons: string[] = [];\n if (markerState.kind === 'corrupt') {\n staleReasons.push(\n `${VERSION_MARKER} was unreadable, so the existing shell could not be ` +\n 'certified',\n );\n }\n if (previousMarker !== null) {\n if (previousMarker.harness !== marker.harness) {\n staleReasons.push(\n `@ekanos/harness ${previousMarker.harness} → ${marker.harness}`,\n );\n }\n if (previousMarker.cli !== marker.cli) {\n staleReasons.push(\n `@ekanos/cli ${previousMarker.cli ?? 'unknown'} → ${marker.cli}`,\n );\n }\n if (previousMarker.templates !== marker.templates) {\n staleReasons.push('the shell templates changed');\n }\n }\n const stale = staleReasons.length > 0;\n\n const files: WrittenFiles = { created: [], updated: [], unchanged: [] };\n const vars = {\n SLUG: args.slug,\n SOURCE_GLOBS: renderSourceGlobs(args.sourceGlobs ?? []),\n };\n\n for (const { template, dest } of SHELL_FILES) {\n writeIfChanged(\n args.projectDir,\n path.join(harnessDir, dest),\n readTemplate(template, vars),\n files,\n dest,\n args.force,\n );\n }\n\n writeIfChanged(\n args.projectDir,\n path.join(harnessDir, VERSION_MARKER),\n serializeMarker(marker),\n files,\n VERSION_MARKER,\n args.force,\n );\n\n return {\n ...files,\n harnessDir,\n previousVersion,\n previousMarker,\n stale,\n staleReasons,\n };\n}\n\n/**\n * Turn validated `sourceGlobs` into the `@source` lines the generated\n * stylesheet carries, each relative to the shell's `styles/` directory.\n *\n * Tailwind never scans what it is not pointed at, and a glob that matches\n * nothing fails silently — so an integration living outside the default\n * layout renders completely unstyled with no error anywhere. That is what\n * `sourceGlobs` is for, and until now it was parsed and thrown away.\n */\nfunction renderSourceGlobs(globs: readonly string[]): string {\n if (globs.length === 0) return '';\n const lines = globs.map((glob) => `@source '${SHELL_TO_PROJECT}${glob}';`);\n return `\\n/* From \"sourceGlobs\" in ekanos.json. */\\n${lines.join('\\n')}\\n`;\n}\n\n/** From `.ekanos/harness/styles/` back up to the project root. */\nconst SHELL_TO_PROJECT = '../../../';\n\n/**\n * Establish that `.ekanos/harness` is a tree WE generated before writing a\n * single byte into it, and return the version marker it carried.\n *\n * Path location alone is not ownership. A directory that exists but carries no\n * `.ekanos-version` was put there by something else — a partner, another tool,\n * a symlink — and clobbering it because it happens to sit at the path we like\n * is the same class of mistake as following a symlink. An empty directory is\n * fine (that is just `mkdir -p` having run), and `--force` is the explicit\n * override.\n *\n * Runs BEFORE `bootstrapHarnessConfig` in `dev`, so a shell that cannot be\n * written leaves the project completely untouched rather than half-scaffolded.\n */\nexport function assertShellDirIsOurs(\n harnessDir: string,\n force: boolean,\n): string | null {\n const kind = classifyPath(harnessDir);\n\n if (kind === 'symlink') {\n throw invalidStateError(\n `${harnessDir} is a symlink, so \"ekanos dev\" will not write through it.`,\n `Delete ${harnessDir} and re-run \"ekanos dev\" — the shell is generated, ` +\n 'so nothing is lost.',\n );\n }\n\n if (kind !== 'missing' && kind !== 'directory') {\n throw invalidStateError(\n `${harnessDir} exists and is not a directory.`,\n `Move or delete ${harnessDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n if (kind === 'missing') return null;\n\n const state = readShellMarkerState(harnessDir);\n\n // A marker we wrote, however mangled, still identifies the tree as ours.\n if (state.kind === 'ok') return state.marker.harness;\n if (state.kind === 'corrupt' || force) return null;\n\n if (fs.readdirSync(harnessDir).length === 0) return null;\n\n throw invalidStateError(\n `${harnessDir} already contains files but no ${VERSION_MARKER} marker, ` +\n 'so it was not generated by \"ekanos dev\".',\n `Delete ${harnessDir} if you do not need it, or pass --force to ` +\n 'overwrite it. Everything \"ekanos dev\" generates is reproducible; ' +\n 'anything else in there is not.',\n );\n}\n\n/**\n * The module specifier `harness.config.ts` uses to import the partner's\n * integration: the `entry` from ekanos.json, made relative and extension-free.\n * \"src/integration.ts\" → \"src/integration\".\n */\nexport function entryImportSpecifier(entry: string): string {\n const normalized = entry.split(path.sep).join('/').replace(/^\\.\\//, '');\n return normalized.replace(/\\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/, '');\n}\n\n/**\n * The same specifier as a JavaScript string LITERAL, ready to drop into a\n * generated `import` statement.\n *\n * `entry` is attacker-influenced data from `ekanos.json`, and a POSIX filename\n * may legally contain a quote or a newline. Interpolating it raw would let a\n * crafted-but-valid path close the string and append statements to the module\n * we generate. `JSON.stringify` produces a correctly escaped literal, which is\n * the actual fix; the explicit rejection below is a second line that also\n * gives the partner a comprehensible error instead of a bizarre filename\n * silently appearing, escaped, in their config.\n *\n * This does not cross a trust boundary on its own — a partner's own code runs\n * unsandboxed anyway — but \"the templates cannot be injected into\" is a claim\n * we make, and it should be true.\n */\nexport function entryImportLiteral(entry: string): string {\n const specifier = entryImportSpecifier(entry);\n\n // eslint-disable-next-line no-control-regex\n if (/[\"'`\\\\\\n\\r\\u0000-\\u001f]/.test(specifier)) {\n throw preconditionError(\n `The \"entry\" in ekanos.json contains characters that cannot appear in ` +\n `an import specifier: ${JSON.stringify(specifier)}.`,\n 'Rename the file so its path has no quotes, backslashes or control ' +\n 'characters, then update \"entry\" in ekanos.json to match.',\n );\n }\n\n return JSON.stringify(`./${specifier}`);\n}\n\nexport interface BootstrapConfigResult {\n /** True only when this run created the file. */\n bootstrapped: boolean;\n configPath: string;\n}\n\n/**\n * Write a starter `harness.config.ts` at the project root — ONCE.\n *\n * An existing file is left strictly alone, byte for byte, and no flag changes\n * that: it is the one harness file the partner owns, it is git-tracked, and\n * regenerating it would throw away their fixtures. `--force` regenerates the\n * shell, never this.\n */\nexport function bootstrapHarnessConfig(\n projectDir: string,\n integrations: readonly { slug: string; entry: string }[],\n): BootstrapConfigResult {\n const configPath = path.join(projectDir, HARNESS_CONFIG_FILENAME);\n const kind = classifyPath(configPath);\n\n // A symlink here — including a dangling one, which `existsSync` reports as\n // absent — would have us write through to its target. Since this file is the\n // partner's to own, the only safe answer is to touch nothing and say why.\n if (kind === 'symlink') {\n throw invalidStateError(\n `${configPath} is a symlink, so \"ekanos dev\" will not write through it.`,\n `Replace ${configPath} with a real file (or delete it to have one ` +\n 'generated), then re-run \"ekanos dev\".',\n );\n }\n\n if (kind !== 'missing') {\n return { bootstrapped: false, configPath };\n }\n\n writeFileNoFollow(projectDir, configPath, renderHarnessConfig(integrations));\n\n return { bootstrapped: true, configPath };\n}\n\n/**\n * Render the starter registry for EVERY integration the project declares.\n *\n * The harness registry is an array and `ekanos.json` can now hold a list, so a\n * project's second integration reaches the harness the same way its first\n * does. Each entry imports the definition under a distinct local identifier\n * derived from its slug, because two `integration` imports in one module do\n * not compile.\n */\nfunction renderHarnessConfig(\n integrations: readonly { slug: string; entry: string }[],\n): string {\n const imports: string[] = [];\n const entries: string[] = [];\n\n for (const integration of integrations) {\n const ident = identifierForSlug(integration.slug);\n imports.push(\n `import { integration as ${ident} } from ${entryImportLiteral(\n integration.entry,\n )};`,\n );\n entries.push(\n readTemplate('harness.config.entry.ts.tmpl', {\n IDENT: ident,\n SLUG: integration.slug,\n }),\n );\n }\n\n return readTemplate('harness.config.ts.tmpl', {\n INTEGRATION_IMPORTS: imports.join('\\n'),\n INTEGRATION_ENTRIES: entries.join(''),\n });\n}\n\n/**\n * \"acme-crm\" → \"acmeCrm\". A slug is kebab-case by schema, so this only has to\n * handle hyphens, and a leading digit is impossible.\n */\nexport function identifierForSlug(slug: string): string {\n const camel = slug.replace(/-([a-z0-9])/g, (_, char: string) =>\n char.toUpperCase(),\n );\n return /^[0-9]/.test(camel) ? `i${camel}` : camel;\n}\n\n/**\n * Add `.ekanos/` to the project `.gitignore`, exactly once.\n *\n * Matching is line-wise over the spellings git treats as the same rule\n * (`.ekanos`, `.ekanos/`, `/.ekanos/`), so re-running never appends a\n * duplicate — the thing that makes a scaffold verb safe to run in a loop.\n * A negation (`!.ekanos/`) is deliberately NOT treated as a match: the\n * partner meant to un-ignore it, and we do not fight that silently.\n */\nexport function ensureGitignoreEntry(projectDir: string): boolean {\n const gitignorePath = path.join(projectDir, '.gitignore');\n const block = `# Ekanos dev harness — generated by \"ekanos dev\", never edited by hand\\n${GITIGNORE_ENTRY}\\n`;\n\n const kind = classifyPath(gitignorePath);\n\n if (kind === 'symlink') {\n throw invalidStateError(\n `${gitignorePath} is a symlink, so \"ekanos dev\" will not append to it.`,\n `Add \"${GITIGNORE_ENTRY}\" to your real .gitignore yourself, or replace ` +\n `${gitignorePath} with a regular file, then re-run \"ekanos dev\".`,\n );\n }\n\n if (kind === 'missing') {\n writeFileNoFollow(projectDir, gitignorePath, block);\n return true;\n }\n\n const existing = readFileNoFollow(projectDir, gitignorePath) ?? '';\n if (gitignoreCovers(existing)) return false;\n\n const separator =\n existing.length === 0 || existing.endsWith('\\n') ? '' : '\\n';\n appendFileNoFollow(projectDir, gitignorePath, `${separator}\\n${block}`);\n return true;\n}\n\nfunction gitignoreCovers(source: string): boolean {\n const equivalents = new Set(['.ekanos', '.ekanos/', '/.ekanos', '/.ekanos/']);\n return source\n .split('\\n')\n .map((line) => line.trim())\n .some((line) => equivalents.has(line));\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "1.7.0",
|
|
36
|
-
"@ekanos/integration-schema": "0.
|
|
36
|
+
"@ekanos/integration-schema": "0.3.0",
|
|
37
37
|
"esbuild": "0.28.1",
|
|
38
38
|
"picocolors": "1.1.1"
|
|
39
39
|
},
|
package/templates/AGENTS.md.tmpl
CHANGED
|
@@ -27,6 +27,7 @@ export const integration = defineIntegration<MyStorage>({
|
|
|
27
27
|
permissions: [ /* disclosure of what you read/write */ ],
|
|
28
28
|
components: { widgets: [ /* … */ ] },
|
|
29
29
|
tools: [ /* assistant-callable functions */ ],
|
|
30
|
+
actions: { /* deterministic, widget-invoked server handlers */ },
|
|
30
31
|
webhooks: [ /* inbound deliveries */ ],
|
|
31
32
|
schedules: [ /* cron runs */ ],
|
|
32
33
|
onActivate: async (ctx) => { /* seed caches, sanity-check credentials */ },
|
|
@@ -172,6 +173,76 @@ failure). Schedule results: `{ status: 'completed' }` / `{ status: 'skipped' }`
|
|
|
172
173
|
(`invocation.trigger` distinguishes them). Cron is 5-field numeric only — no
|
|
173
174
|
names, no `@daily`, no seconds.
|
|
174
175
|
|
|
176
|
+
### Actions
|
|
177
|
+
|
|
178
|
+
`actions` is a record — keyed by kebab-case action name (`'create-record'`,
|
|
179
|
+
not `tools`' `snake_case`) — of deterministic, widget-invoked server
|
|
180
|
+
handlers. It is a **parallel surface to `tools`, not a flag on it**: a
|
|
181
|
+
widget's Save button already has fully-typed, user-entered values, so it
|
|
182
|
+
calls your handler directly instead of routing a natural-language prompt
|
|
183
|
+
through the assistant and hoping the model picks the right tool with the
|
|
184
|
+
right arguments.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
actions: {
|
|
188
|
+
'create-record': {
|
|
189
|
+
description: 'Create a record in the base.',
|
|
190
|
+
input: CreateRecordInput, // a real zod schema
|
|
191
|
+
output: CreateRecordOutput, // optional — validates what handler resolves to
|
|
192
|
+
handler: async (ctx, input) => {
|
|
193
|
+
// Same ctx every tool/webhook/schedule handler gets: ctx.fetch,
|
|
194
|
+
// ctx.secrets, ctx.storage — runs server-side, never in the browser.
|
|
195
|
+
return createRecordViaApi(ctx, input);
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`input` (and the optional `output`) are real `z.ZodTypeAny` instances, not
|
|
202
|
+
the JSON-schema-shaped `parameters` a tool declares for LLM argument
|
|
203
|
+
synthesis — an action is only ever parsed in-process, never serialized to a
|
|
204
|
+
model. Export the inferred types alongside the declaration
|
|
205
|
+
(`export type CreateRecordInput = z.infer<typeof CreateRecordInput>`) so the
|
|
206
|
+
widget importing them gets real autocomplete, per the SDK typing note below.
|
|
207
|
+
|
|
208
|
+
**Invoke it from a widget with `useIntegrationAction()`** (`@ekanos/sdk/hooks`)
|
|
209
|
+
— never `useAskAssistant()` for this. `useAskAssistant()` sends a
|
|
210
|
+
conversational question to the assistant dock (`Widget.AskFooter`'s "ask
|
|
211
|
+
anything about this widget"); it is not a write path and does not call your
|
|
212
|
+
handler deterministically. A form's Save button calls the handler you
|
|
213
|
+
declared, with the exact values the user typed:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
|
|
217
|
+
'__SLUG__',
|
|
218
|
+
'create-record',
|
|
219
|
+
{ accountId },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
<Button onClick={() => createRecord.mutate(formValues)} disabled={createRecord.isPending}>
|
|
223
|
+
Save
|
|
224
|
+
</Button>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A rejected mutation's `error` is always an `IntegrationActionError`
|
|
228
|
+
(`status`, `code`, and — for a 400 — your own declared zod `issues`), never a
|
|
229
|
+
raw fetch exception. The route answers a fixed set of `code`s your widget can
|
|
230
|
+
branch on without string-matching: `action_not_found`, `invalid_input`,
|
|
231
|
+
`rate_limited`, `handler_error` (plus the client-assigned `network_error` /
|
|
232
|
+
`invalid_response` for a request that never reached a valid envelope).
|
|
233
|
+
|
|
234
|
+
A handler that wants its OWN message shown to the end user verbatim (e.g.
|
|
235
|
+
"That record already exists") throws `PublicActionError`
|
|
236
|
+
(`@ekanos/sdk/integration`) — the one escape hatch from the route's default
|
|
237
|
+
opaque `handler_error` message. Every other throw collapses to a generic
|
|
238
|
+
"Action failed." on the wire, though the real error is always logged
|
|
239
|
+
server-side.
|
|
240
|
+
|
|
241
|
+
Reuse one handler as both an action and a tool without double-declaring
|
|
242
|
+
logic — export the handler once and reference it from both declarations
|
|
243
|
+
(`run: (ctx, args) => createRecord.handler(ctx, CreateRecordInput.parse(args))`)
|
|
244
|
+
if you also want the assistant able to call it.
|
|
245
|
+
|
|
175
246
|
### `onActivate`
|
|
176
247
|
|
|
177
248
|
Runs once after an activation is first persisted, and again after
|
|
@@ -222,6 +293,12 @@ harness and in production. A widget reads declared `clientReadable`
|
|
|
222
293
|
account-scope storage back with `fetchIntegrationStorage()` from
|
|
223
294
|
`@ekanos/sdk/hooks`.
|
|
224
295
|
|
|
296
|
+
For a widget that WRITES — a form's Save button, not a read — reach for
|
|
297
|
+
`useIntegrationAction()` (see "Actions" above), not `useAskAssistant()`.
|
|
298
|
+
`useAskAssistant()` sends a conversational question to the assistant dock;
|
|
299
|
+
it is not a deterministic write path and gives no guarantee the model calls
|
|
300
|
+
your handler with the values the user actually typed.
|
|
301
|
+
|
|
225
302
|
Build widget UI from `@ekanos/ui` — the published slice of the host design
|
|
226
303
|
system. Available modules (import individually, e.g. `@ekanos/ui/button`):
|
|
227
304
|
`alert`, `badge`, `button`, `card`, `dialog`, `dropdown-menu`, `form`, `icon`,
|
|
@@ -267,26 +344,58 @@ live: {
|
|
|
267
344
|
```
|
|
268
345
|
|
|
269
346
|
Egress is read off the definition automatically — do not repeat it here.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
347
|
+
Add `default: 'live'` to that block ONLY when your widgets never call the
|
|
348
|
+
vendor from the browser (every vendor call is server-side behind `ctx.fetch`,
|
|
349
|
+
the OAuth shape): the harness then opens in Live API, and nothing fires at the
|
|
350
|
+
vendor until the user acts. A choice made at the toolbar always wins.
|
|
351
|
+
|
|
352
|
+
There is still no Fusion database or auth in either mode. What differs is
|
|
353
|
+
who answers `/api/integrations/<slug>/…`:
|
|
354
|
+
|
|
355
|
+
- **Fixtures**: your fixtures do, in the browser. Storage, secrets and the
|
|
356
|
+
OAuth status (`oauth.connected` in the harness entry, connected by default)
|
|
357
|
+
are all mocked; `ctx.fetch` in handlers answers from fixtures and never
|
|
358
|
+
touches the network.
|
|
359
|
+
- **Live**: the generated shell's own API routes do (`@ekanos/harness/api`),
|
|
360
|
+
over ONE server-held context per integration. That context's `ctx.fetch`
|
|
361
|
+
reaches the real vendor under your declared `egress`; its secrets come from
|
|
362
|
+
`.ekanos/local-secrets.json` (written by the **Local OAuth** panel's form on
|
|
363
|
+
the Activation surface — one field per declared secret name; never commit
|
|
364
|
+
it) plus whatever `onTokens` stored; its storage is
|
|
365
|
+
whatever your handlers wrote. The Triggers and Activation surfaces run
|
|
366
|
+
webhooks, schedules and `onActivate` on that context, and the **OAuth loop
|
|
367
|
+
runs for real**. The Activation surface's **Local OAuth** panel guides the
|
|
368
|
+
human part — a checklist for the provider's developer console (register
|
|
369
|
+
`http://localhost:3100/api/integrations/<slug>/oauth/callback`, enable the
|
|
370
|
+
declared scopes, copy the keys) derived from the declaration plus the
|
|
371
|
+
entry's `oauth.setup` (console link, provider-specific notes — WRITE these
|
|
372
|
+
for every OAuth integration), then a form that saves the keys server-side.
|
|
373
|
+
Then Connect, and `onTokens` runs on the server. The connection is
|
|
374
|
+
memory-only — a dev-server restart means one more Connect. **Never take a
|
|
375
|
+
user's client id, secret or token through chat or write one into a file;
|
|
376
|
+
the form is the only path.**
|
|
377
|
+
|
|
378
|
+
Boundaries to know: live-mode WIDGET requests come from the browser, so a
|
|
379
|
+
vendor that blocks browser origins (CORS) or needs a server-held secret must be
|
|
380
|
+
exercised through handlers and read back through storage — which is exactly
|
|
381
|
+
what the live context is for.
|
|
279
382
|
|
|
280
383
|
### Network model
|
|
281
384
|
|
|
282
385
|
Two different fetches, two different guarantees:
|
|
283
386
|
|
|
284
|
-
- **`ctx.fetch` (handlers — `onActivate`/webhooks/schedules/tools).**
|
|
285
|
-
enforcement, in the harness AND in production: your `egress`
|
|
286
|
-
redirect re-vetting, IP pinning. Locally
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
387
|
+
- **`ctx.fetch` (handlers — `onActivate`/webhooks/schedules/tools/actions).**
|
|
388
|
+
Real enforcement, in the harness AND in production: your `egress`
|
|
389
|
+
allowlist, redirect re-vetting, IP pinning. Locally it depends on the
|
|
390
|
+
toolbar: in fixtures mode handlers run in the browser against the mock
|
|
391
|
+
context and `ctx.fetch` answers from your fixtures, never the network; in
|
|
392
|
+
live mode they run on the dev server's live context and `ctx.fetch` reaches
|
|
393
|
+
the real vendor, egress-checked first — which is how the OAuth code
|
|
394
|
+
exchange and a post-connect sync work locally at all. An `actions[name]`
|
|
395
|
+
handler is executed exactly the same way as a tool or webhook handler — a
|
|
396
|
+
widget's `useIntegrationAction()` call never reaches the vendor directly;
|
|
397
|
+
it always crosses through your server-side handler and `ctx.fetch` first,
|
|
398
|
+
in the harness and in production alike.
|
|
290
399
|
- **`useIntegrationFetch()` (widgets, browser-side).** In the harness, live
|
|
291
400
|
mode routes it through the same egress check as a convenience, enforced
|
|
292
401
|
centrally rather than something you configure per widget. **That check
|
|
@@ -298,8 +407,16 @@ Two different fetches, two different guarantees:
|
|
|
298
407
|
means to rule out.
|
|
299
408
|
|
|
300
409
|
In short: widgets get real network in Live API mode, gated by convenience in
|
|
301
|
-
the harness and by nothing in production; handlers get fixtures
|
|
302
|
-
the real, enforced thing
|
|
410
|
+
the harness and by nothing in production; handlers get fixtures in fixtures
|
|
411
|
+
mode and the real, enforced thing in live mode — locally on the dev server,
|
|
412
|
+
then on the host after publish. `actions` handlers follow the handler rule:
|
|
413
|
+
`useIntegrationAction()` in the harness runs your handler against fixtures
|
|
414
|
+
by default, same as `invokeWebhook`/`invokeSchedule` — live execution is an
|
|
415
|
+
explicit, per-session opt-in from the dev toolbar (a confirmation the first
|
|
416
|
+
time a call would touch the real vendor that session, not a per-call
|
|
417
|
+
prompt), never the default. In production there is only one mode: the
|
|
418
|
+
handler always runs server-side with the real `IntegrationContext` and real
|
|
419
|
+
`ctx.fetch` egress enforcement.
|
|
303
420
|
|
|
304
421
|
## Testing
|
|
305
422
|
|
|
@@ -19,7 +19,12 @@ in the same commit when the live API disagrees.
|
|
|
19
19
|
|
|
20
20
|
## The loop
|
|
21
21
|
|
|
22
|
-
1. `npx ekanos dev` — local harness (only when you need to see UI).
|
|
22
|
+
1. `npx ekanos dev` — local harness (only when you need to see UI). For an
|
|
23
|
+
OAuth integration it is also where the USER connects: the Activation
|
|
24
|
+
surface's Local OAuth panel walks them through their provider console and
|
|
25
|
+
takes their client id/secret in a form. Point them there; never ask for a
|
|
26
|
+
secret in chat or write one into a file (`build-integration` →
|
|
27
|
+
`references/oauth-setup.md`).
|
|
23
28
|
2. Edit code. Never use global `fetch`/`process.env`/`fs` in integration
|
|
24
29
|
code — everything goes through `ctx`.
|
|
25
30
|
3. `npx tsc --noEmit` — types clean.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHarnessApiHandlers } from '@ekanos/harness/api';
|
|
2
|
+
|
|
3
|
+
import { harnessIntegrations } from '../../../../../../../harness.config';
|
|
4
|
+
|
|
5
|
+
const handlers = createHarnessApiHandlers(harnessIntegrations);
|
|
6
|
+
|
|
7
|
+
export const GET = handlers.GET;
|
|
8
|
+
export const POST = handlers.POST;
|
|
9
|
+
export const PUT = handlers.PUT;
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"@ekanos/ui": "^0.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@ekanos/cli": "^0.
|
|
19
|
-
"@ekanos/harness": "^0.
|
|
18
|
+
"@ekanos/cli": "^0.3.0",
|
|
19
|
+
"@ekanos/harness": "^0.2.0",
|
|
20
20
|
"@hookform/resolvers": "^5.2.2",
|
|
21
21
|
"@tailwindcss/postcss": "^4.3.3",
|
|
22
22
|
"@tanstack/react-query": "^5.101.4",
|
|
@@ -52,11 +52,16 @@ otherwise ask in plain prose with lettered options.
|
|
|
52
52
|
3. **The outcomes** (free text): "What should a team see on their dashboard,
|
|
53
53
|
and what should the integration be able to do?" Push for 1–3 concrete
|
|
54
54
|
outcomes ("show open invoices", "alert on new disputes", "let the
|
|
55
|
-
assistant create a customer"
|
|
56
|
-
|
|
55
|
+
assistant create a customer", "let someone create a record from a
|
|
56
|
+
dashboard form"). Each SEE-outcome becomes a widget; each DO-outcome
|
|
57
|
+
becomes a tool (assistant-invoked), a webhook/schedule (event-driven), or
|
|
58
|
+
an action (a widget's own deterministic write — a form's Save button).
|
|
57
59
|
4. **Auth** (choice): API key / Bearer / OAuth 2.0 / Basic / not sure
|
|
58
60
|
(research will settle it). This decides the activation-form fields — and
|
|
59
61
|
every string field name on that form IS a secret name (see AGENTS.md).
|
|
62
|
+
For OAuth 2.0, also ask whether they have a developer/sandbox app already;
|
|
63
|
+
during research, find the console page where one is created — you will
|
|
64
|
+
put it in `harness.config.ts` (see `references/oauth-setup.md`).
|
|
60
65
|
5. **Freshness** (choice): live fetch on view / cached with a refresh
|
|
61
66
|
schedule / pushed by webhooks. This decides storage keys and schedules.
|
|
62
67
|
|
|
@@ -131,7 +136,13 @@ notes, update `docs/api-notes.md` in the same commit as the code fix.
|
|
|
131
136
|
|
|
132
137
|
Order: activation form (field names = secret names, keep them in exported
|
|
133
138
|
constants) → server handlers using `ctx.fetch` / `ctx.storage` /
|
|
134
|
-
`ctx.secrets` only → tools → webhooks/schedules
|
|
139
|
+
`ctx.secrets` only → tools → webhooks/schedules → actions (deterministic
|
|
140
|
+
widget writes — a form's Save button, invoked with `useIntegrationAction()`,
|
|
141
|
+
never routed through the assistant). For an OAuth integration,
|
|
142
|
+
also read `references/oauth-setup.md`: it tells you what to put on the
|
|
143
|
+
harness entry so the harness walks the user through their provider's console,
|
|
144
|
+
and how to hand the user off to the Local OAuth panel — the user's secrets
|
|
145
|
+
never pass through you. Write tests alongside each
|
|
135
146
|
handler with `createMockContext` / `invokeWebhook` / `invokeSchedule` from
|
|
136
147
|
`@ekanos/sdk/testing`, and run:
|
|
137
148
|
|
|
@@ -145,7 +156,9 @@ All of these, in order, all green:
|
|
|
145
156
|
2. `npx ekanos test` — all passing, including at least one test per handler
|
|
146
157
|
and one failure-path test per external call
|
|
147
158
|
3. Harness check — every widget renders all four states (mock the states if
|
|
148
|
-
needed); user has seen and approved the visuals
|
|
159
|
+
needed); user has seen and approved the visuals. For OAuth: the user has
|
|
160
|
+
completed the Local OAuth panel (checklist → Save credentials → Connect)
|
|
161
|
+
and the widgets render THEIR sandbox data in Live API
|
|
149
162
|
4. Grep your own diff for leaks: no credential in `ctx.storage`, `configData`,
|
|
150
163
|
logs, or tool results; no global `fetch`/`process.env`/`node:fs`
|
|
151
164
|
|
|
@@ -30,11 +30,23 @@ code. This file is the order of work and the traps.
|
|
|
30
30
|
`fetchIntegrationStorage()` from `@ekanos/sdk/hooks`. Cache freshness in
|
|
31
31
|
the stored value (`fetchedAt`) and show it in the UI when staleness
|
|
32
32
|
matters.
|
|
33
|
+
6. **Widget writes** — the read path's mirror. A form's Save button declares
|
|
34
|
+
an `actions[name]` handler (zod `input`, optional `output`, `handler(ctx,
|
|
35
|
+
input)`) and calls it with `useIntegrationAction()` from
|
|
36
|
+
`@ekanos/sdk/hooks` — deterministic, with the values the user actually
|
|
37
|
+
typed, never a natural-language hand-off to the assistant. A handler that
|
|
38
|
+
wants its own message shown to the user verbatim throws
|
|
39
|
+
`PublicActionError` from `@ekanos/sdk/integration`; every other throw
|
|
40
|
+
collapses to a generic error on the wire (still logged in full
|
|
41
|
+
server-side). See "Actions" in AGENTS.md.
|
|
33
42
|
|
|
34
43
|
## Traps, each one earned
|
|
35
44
|
|
|
36
45
|
- **Egress is an allowlist, checked synchronously.** Every origin the client
|
|
37
46
|
touches must be in `egress` — including the OAuth token host and any host a
|
|
47
|
+
For OAuth integrations, `oauth-setup.md` in this folder covers the human
|
|
48
|
+
setup and the harness's Local OAuth panel — the user's client id and secret
|
|
49
|
+
go into that form, never into chat or a file you write.
|
|
38
50
|
redirect lands on. A missing origin is a thrown error, not a warning.
|
|
39
51
|
- **Secrets never leave the secrets surface.** Not into `ctx.storage`, not
|
|
40
52
|
into logs, not into a tool result, not into a widget's stored value. Token
|
|
@@ -91,4 +103,8 @@ on its `harness.config.ts` entry — see AGENTS.md "Live mode") and the
|
|
|
91
103
|
toolbar's fixtures ⇄ live switch runs real widget queries against the real
|
|
92
104
|
API, gated only by the declared `egress`. Use fixtures for the Fusion-side
|
|
93
105
|
state widgets need first, and for webhook/schedule handlers (the Triggers
|
|
94
|
-
surface runs those against the mock context by design).
|
|
106
|
+
surface runs those against the mock context by design). `actions[name]`
|
|
107
|
+
handlers follow the same rule: `useIntegrationAction()` calls run against
|
|
108
|
+
fixtures by default, and only reach the real vendor after an explicit,
|
|
109
|
+
per-session live-mode confirmation from the toolbar — never on the first
|
|
110
|
+
call by accident.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# OAuth integrations — set the human up, never handle their secrets
|
|
2
|
+
|
|
3
|
+
An OAuth integration has a step no code can do: a person signs in to the
|
|
4
|
+
provider's developer console, creates an app, registers a redirect URI,
|
|
5
|
+
enables scopes, and copies a client id and secret out. Your job is to make
|
|
6
|
+
that step obvious and short, and to keep the secrets out of the
|
|
7
|
+
conversation and out of every file you write.
|
|
8
|
+
|
|
9
|
+
## What you write (in `harness.config.ts`, on the integration's entry)
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
oauth: {
|
|
13
|
+
// Fixtures mode answers the SDK's /oauth/status route from this; `empty`
|
|
14
|
+
// usually wants the Connect prompt rather than empty widgets.
|
|
15
|
+
connected: { empty: false },
|
|
16
|
+
// The human checklist the harness cannot derive from the declaration:
|
|
17
|
+
// where the console is, and the provider's own names for each key.
|
|
18
|
+
setup: {
|
|
19
|
+
consoleUrl: 'https://<provider developer console>/apps',
|
|
20
|
+
consoleLabel: '<Provider> → Apps and Keys',
|
|
21
|
+
notes: [
|
|
22
|
+
'<Provider's name for the client id> is the client id (<clientIdSecretName>).',
|
|
23
|
+
'<Where the secret is minted, and whether it is shown once> (<clientSecretSecretName>).',
|
|
24
|
+
// One line per thing a first-time user would otherwise get wrong.
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
// When every vendor call runs server-side behind ctx.fetch (the OAuth shape)
|
|
29
|
+
// and widgets only talk to the harness's own routes, open in Live API:
|
|
30
|
+
live: { default: 'live' },
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Find `consoleUrl` and the notes in the provider's docs during Phase 2
|
|
34
|
+
(research) — the "create an app" / "register an application" page. Write
|
|
35
|
+
what the CONSOLE calls things, not what the declaration calls them; the
|
|
36
|
+
harness already prints the declared secret names beside each field. The
|
|
37
|
+
harness derives the rest — provider host, redirect URI, scopes, PKCE — from
|
|
38
|
+
`defineIntegration({ oauth })`, so do not repeat those in `notes`.
|
|
39
|
+
|
|
40
|
+
## What the harness then shows (so you can point the user at it)
|
|
41
|
+
|
|
42
|
+
The Activation surface (`npx ekanos dev` → `http://localhost:3100/<slug>/activation`)
|
|
43
|
+
opens with a **Local OAuth** panel:
|
|
44
|
+
|
|
45
|
+
1. the human checklist — console link, the redirect URI to register (Copy
|
|
46
|
+
button), the scopes to enable, PKCE, which key goes where, your notes;
|
|
47
|
+
2. a form with one password field per declared secret name and a **Save
|
|
48
|
+
credentials** button, written server-side to a gitignored file and never
|
|
49
|
+
echoed back;
|
|
50
|
+
3. the connect dialog, whose **Connect** runs the real authorize → callback →
|
|
51
|
+
`onTokens` loop against the provider's developer sandbox.
|
|
52
|
+
|
|
53
|
+
## How to guide the user through it
|
|
54
|
+
|
|
55
|
+
- After the declaration validates and the harness entry has `oauth.setup`,
|
|
56
|
+
tell the user in ONE message: open the Activation surface, follow the
|
|
57
|
+
checklist in the panel, paste the two keys into the form, click Connect.
|
|
58
|
+
Name the console and the redirect URI in the message so they can start
|
|
59
|
+
before the harness loads.
|
|
60
|
+
- **Never ask for a client id, client secret, token, or signing key in
|
|
61
|
+
chat, and never write one into a file** — not `harness.config.ts`, not
|
|
62
|
+
`triggerMocks.secrets` (those are FAKE seeds and are bundled into the
|
|
63
|
+
browser build), not `.env`, not `.ekanos/local-secrets.json` by hand. The
|
|
64
|
+
form is the only path; if the user pastes a secret into the conversation,
|
|
65
|
+
tell them to rotate it in the console and use the form.
|
|
66
|
+
- The connection is memory-only: after a dev-server restart, "not
|
|
67
|
+
connected" is expected — say so instead of debugging.
|
|
68
|
+
- `<slug>_oauth_error=<code>` on the return URL is the transport's
|
|
69
|
+
vocabulary: `not_configured` = a key is still missing (check the panel's
|
|
70
|
+
badges), `denied` = the user cancelled at the provider, `exchange_failed` =
|
|
71
|
+
the provider rejected the exchange (wrong redirect URI or secret, or the
|
|
72
|
+
token host missing from `egress`), `save_failed` = your `onTokens` threw
|
|
73
|
+
(read the dev-server log). Fix the cause; do not retry blindly.
|
|
74
|
+
- Widgets read the connection through `useOAuthWidgetGate` / the storage
|
|
75
|
+
route. If they show "Connect" after a successful loop, check `onActivate`
|
|
76
|
+
or the schedule wrote the `clientReadable` keys the widgets read.
|
|
@@ -35,6 +35,14 @@ name the question for is a widget you cannot design.
|
|
|
35
35
|
| Chart | "Which way is it going?" | The chart + current value; axes and legends muted |
|
|
36
36
|
| Action / connection | "What can I do / who's connected?" | One clear CTA or the identity + status |
|
|
37
37
|
|
|
38
|
+
A widget whose CTA WRITES something (a form, a "Create record" button) calls
|
|
39
|
+
a declared `actions[name]` handler via `useIntegrationAction()` — a
|
|
40
|
+
deterministic RPC with the values the user actually typed, not a
|
|
41
|
+
conversational hand-off. Reach for `useAskAssistant()` only for an actual
|
|
42
|
+
"ask about this" affordance (`Widget.AskFooter`); it is not a write path and
|
|
43
|
+
gives no guarantee the assistant calls anything with the right arguments.
|
|
44
|
+
See "Actions" in `AGENTS.md`.
|
|
45
|
+
|
|
38
46
|
The default failure is uniform weight: everything `text-sm`, values dressed
|
|
39
47
|
like labels. Squint at the render — if nothing reads first, there is no
|
|
40
48
|
hierarchy, and no amount of padding will save it.
|
|
@@ -74,8 +82,10 @@ Every widget: `'use client'`, `Widget.DataState` wrapping `Widget.Card` →
|
|
|
74
82
|
fetching on `ctx?.state.state === 'active'` from `WidgetContext`, and fetch
|
|
75
83
|
with `useIntegrationFetch()` from `@ekanos/sdk/hooks` — never global
|
|
76
84
|
`fetch` — reaching for `useActivationData()` (same module) if the widget
|
|
77
|
-
needs a client-visible credential.
|
|
78
|
-
|
|
85
|
+
needs a client-visible credential. For a write (a form submit), call
|
|
86
|
+
`useIntegrationAction()` (same module) instead — it posts to your declared
|
|
87
|
+
`actions[name]` handler, not the vendor directly. Copy the canonical example
|
|
88
|
+
from the SDK README rather than improvising the tree.
|
|
79
89
|
|
|
80
90
|
`Widget.Active` itself renders flush (no padding) so full-bleed charts and
|
|
81
91
|
tables can run edge-to-edge. For ordinary content — text, rows, a KPI
|
|
@@ -80,4 +80,20 @@ export const integration = defineIntegration({
|
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
82
|
],
|
|
83
|
+
|
|
84
|
+
// `actions` is the deterministic counterpart to `tools`: a widget's own
|
|
85
|
+
// Save button calls a declared handler directly (via `useIntegrationAction()`
|
|
86
|
+
// from `@ekanos/sdk/hooks`), with the exact values the user typed — never a
|
|
87
|
+
// natural-language hand-off to the assistant. Uncomment and adapt once a
|
|
88
|
+
// widget needs to write something; see "Actions" in AGENTS.md.
|
|
89
|
+
//
|
|
90
|
+
// actions: {
|
|
91
|
+
// 'create-record': {
|
|
92
|
+
// description: 'Create a record in the base.',
|
|
93
|
+
// input: z.object({ name: z.string().min(1) }),
|
|
94
|
+
// handler: async (_ctx, input) => {
|
|
95
|
+
// return { id: 'rec_123', name: input.name };
|
|
96
|
+
// },
|
|
97
|
+
// },
|
|
98
|
+
// },
|
|
83
99
|
});
|