@ekanos/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/auth/credential-store.d.ts +96 -0
- package/dist/auth/credential-store.js +274 -0
- package/dist/auth/credential-store.js.map +1 -0
- package/dist/auth/device-flow.d.ts +57 -0
- package/dist/auth/device-flow.js +201 -0
- package/dist/auth/device-flow.js.map +1 -0
- package/dist/auth/fusion-api.d.ts +48 -0
- package/dist/auth/fusion-api.js +310 -0
- package/dist/auth/fusion-api.js.map +1 -0
- package/dist/auth/session.d.ts +57 -0
- package/dist/auth/session.js +114 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +20 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/dev.d.ts +36 -0
- package/dist/commands/dev.js +207 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +30 -0
- package/dist/commands/init.js +178 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +20 -0
- package/dist/commands/login.js +92 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +20 -0
- package/dist/commands/logout.js +75 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/test.d.ts +16 -0
- package/dist/commands/test.js +54 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/validate.d.ts +14 -0
- package/dist/commands/validate.js +98 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/whoami.d.ts +18 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/compatibility.d.ts +81 -0
- package/dist/compatibility.js +135 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/context.d.ts +81 -0
- package/dist/context.js +125 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +111 -0
- package/dist/errors.js.map +1 -0
- package/dist/exit-codes.d.ts +52 -0
- package/dist/exit-codes.js +51 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/harness-scaffold.d.ts +212 -0
- package/dist/harness-scaffold.js +434 -0
- package/dist/harness-scaffold.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +262 -0
- package/dist/index.js.map +1 -0
- package/dist/load-definition.d.ts +20 -0
- package/dist/load-definition.js +126 -0
- package/dist/load-definition.js.map +1 -0
- package/dist/merge-manifest.d.ts +44 -0
- package/dist/merge-manifest.js +124 -0
- package/dist/merge-manifest.js.map +1 -0
- package/dist/package-manager.d.ts +25 -0
- package/dist/package-manager.js +57 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/parse-argv.d.ts +27 -0
- package/dist/parse-argv.js +74 -0
- package/dist/parse-argv.js.map +1 -0
- package/dist/project-checks.d.ts +18 -0
- package/dist/project-checks.js +77 -0
- package/dist/project-checks.js.map +1 -0
- package/dist/project.d.ts +140 -0
- package/dist/project.js +228 -0
- package/dist/project.js.map +1 -0
- package/dist/safe-fs.d.ts +55 -0
- package/dist/safe-fs.js +209 -0
- package/dist/safe-fs.js.map +1 -0
- package/dist/templates.d.ts +23 -0
- package/dist/templates.js +49 -0
- package/dist/templates.js.map +1 -0
- package/package.json +62 -0
- package/templates/ekanos.json.tmpl +4 -0
- package/templates/gitignore.tmpl +12 -0
- package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -0
- package/templates/harness/app/[slug]/layout.tsx.tmpl +1 -0
- package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -0
- package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -0
- package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -0
- package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/harness-shell.tsx.tmpl +9 -0
- package/templates/harness/app/layout.tsx.tmpl +4 -0
- package/templates/harness/app/page.tsx.tmpl +7 -0
- package/templates/harness/next.config.mjs.tmpl +46 -0
- package/templates/harness/package.json.tmpl +11 -0
- package/templates/harness/postcss.config.mjs.tmpl +6 -0
- package/templates/harness/styles/globals.css.tmpl +33 -0
- package/templates/harness/tsconfig.json.tmpl +31 -0
- package/templates/harness/types/team-accounts.d.ts.tmpl +16 -0
- package/templates/harness.config.entry.ts.tmpl +65 -0
- package/templates/harness.config.ts.tmpl +58 -0
- package/templates/package.json.tmpl +28 -0
- package/templates/src/integration.test.ts.tmpl +24 -0
- package/templates/src/integration.ts.tmpl +83 -0
- package/templates/tsconfig.json.tmpl +19 -0
- package/templates/vitest.config.ts.tmpl +32 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekanos/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"license": "MIT",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"email": "npm@govastly.com"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"ekanos",
|
|
12
|
+
"fusion",
|
|
13
|
+
"integration",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"ekanos": "./dist/bin.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"templates",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@ekanos/integration-schema": "0.1.2",
|
|
36
|
+
"esbuild": "0.28.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"zod": "^3.25.76"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@kit/eslint-config": "0.2.0",
|
|
43
|
+
"@kit/prettier-config": "0.1.0",
|
|
44
|
+
"@kit/tsconfig": "0.1.0",
|
|
45
|
+
"@types/node": "25.0.1",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "4.1.10",
|
|
48
|
+
"zod": "3.25.76"
|
|
49
|
+
},
|
|
50
|
+
"prettier": "@kit/prettier-config",
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs",
|
|
53
|
+
"clean": "git clean -xdf .turbo node_modules dist",
|
|
54
|
+
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
|
55
|
+
"lint": "eslint .",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run --config vitest.config.ts"
|
|
58
|
+
},
|
|
59
|
+
"main": "./dist/index.js",
|
|
60
|
+
"module": "./dist/index.js",
|
|
61
|
+
"types": "./dist/index.d.ts"
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActivationPage as default } from '@ekanos/harness/routes';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IntegrationLayout as default } from '@ekanos/harness/routes';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TilePage as default } from '@ekanos/harness/routes';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TriggersPage as default } from '@ekanos/harness/routes';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SingleWidgetPage as default } from '@ekanos/harness/routes';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WidgetsPage as default } from '@ekanos/harness/routes';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { RootLayout } from '@ekanos/harness/app';
|
|
4
|
+
|
|
5
|
+
import { harnessIntegrations } from '../../../harness.config';
|
|
6
|
+
|
|
7
|
+
export default function HarnessShell(props: { children: React.ReactNode }) {
|
|
8
|
+
return <RootLayout integrations={harnessIntegrations} {...props} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Generated by `ekanos dev`. Owned by @ekanos/cli — every file under
|
|
2
|
+
// .ekanos/ is regenerated on each run, so edits here are lost. Change your
|
|
3
|
+
// integration or harness.config.ts instead.
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shell lives two directories below your project root and has no
|
|
8
|
+
* node_modules of its own; `next`, `react` and `@ekanos/harness` all resolve
|
|
9
|
+
* upward into yours. Pointing Next at that root stops it inferring a different
|
|
10
|
+
* one and keeps file watching over your integration source.
|
|
11
|
+
*/
|
|
12
|
+
const projectRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
13
|
+
|
|
14
|
+
/** @type {import('next').NextConfig} */
|
|
15
|
+
const config = {
|
|
16
|
+
reactStrictMode: true,
|
|
17
|
+
// Everything else @ekanos ships is pre-bundled dist that Next can consume
|
|
18
|
+
// as-is. `@ekanos/harness` is the exception: its dist is compiled per file
|
|
19
|
+
// rather than bundled, and `RootLayout` reaches `next/font/google` — a
|
|
20
|
+
// build-time macro that only runs on code Next itself compiles. Drop this
|
|
21
|
+
// and the harness renders in the system font stack, with no error.
|
|
22
|
+
transpilePackages: ['@ekanos/harness'],
|
|
23
|
+
turbopack: {
|
|
24
|
+
root: projectRoot,
|
|
25
|
+
resolveAlias: {
|
|
26
|
+
// A widget written for the real Fusion host imports
|
|
27
|
+
// `useTeamAccountWorkspace()` to read `account.slug`. That module
|
|
28
|
+
// reaches Supabase, which the harness deliberately does not have, so
|
|
29
|
+
// this points the specifier at the shape-identical mock @ekanos/harness
|
|
30
|
+
// ships. It is what lets your widget source run here unmodified.
|
|
31
|
+
//
|
|
32
|
+
// The target is a bare specifier rather than a path, so it resolves
|
|
33
|
+
// through the package's own exports map and holds however your package
|
|
34
|
+
// manager laid out node_modules.
|
|
35
|
+
//
|
|
36
|
+
// Its type side is types/team-accounts.d.ts, generated next to this
|
|
37
|
+
// file. Change one without the other and the harness runs but stops
|
|
38
|
+
// type-checking — which is why both are generated and neither is
|
|
39
|
+
// yours to edit.
|
|
40
|
+
'@kit/team-accounts/hooks/use-team-account-workspace':
|
|
41
|
+
'@ekanos/harness/mocks/team-account-workspace',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default config;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "Generated by `ekanos dev`. Owned by @ekanos/cli — edits are overwritten. Declares no dependencies on purpose: next, react and @ekanos/harness all resolve upward into your project's node_modules, so this runs your Next and your React rather than installing a second copy of each.",
|
|
3
|
+
"name": "__SLUG__-harness",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "next dev --port 3100",
|
|
9
|
+
"build": "next build"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Generated by `ekanos dev`. Owned by @ekanos/cli — edits are overwritten.
|
|
3
|
+
*
|
|
4
|
+
* One import for the harness itself, then the `@source` globs covering code
|
|
5
|
+
* YOU own. Everything the harness chrome renders — its own components,
|
|
6
|
+
* `@ekanos/ui`, `@ekanos/sdk` — is already accounted for inside
|
|
7
|
+
* `@ekanos/harness/styles.css`, so nothing about the harness needs a glob here.
|
|
8
|
+
*/
|
|
9
|
+
@import '@ekanos/harness/styles.css';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Your integration source, and the registry that lists it. A missing @source
|
|
13
|
+
* path is a no-op and Tailwind never scans node_modules unless told to, so a
|
|
14
|
+
* glob that matches nothing costs you a completely unstyled widget with no
|
|
15
|
+
* build error at all.
|
|
16
|
+
*
|
|
17
|
+
* This file is regenerated on every `ekanos dev`, so do not add lines here —
|
|
18
|
+
* they would be erased. If your integration lives somewhere these globs miss,
|
|
19
|
+
* add it to "sourceGlobs" in ekanos.json and it appears at the bottom of this
|
|
20
|
+
* file.
|
|
21
|
+
*/
|
|
22
|
+
@source '../../../harness.config.ts';
|
|
23
|
+
@source '../../../{src,app,lib,components,integrations,packages}/**/*.{ts,tsx}';
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Tailwind never scans node_modules unless told to, so this points it at the
|
|
27
|
+
* harness's own compiled output. `@ekanos/harness/styles.css` already covers
|
|
28
|
+
* itself, making this a deliberate duplicate: a repeated @source costs
|
|
29
|
+
* nothing, and a missing one costs you a page with no styling and no error to
|
|
30
|
+
* explain it.
|
|
31
|
+
*/
|
|
32
|
+
@source '../../../node_modules/@ekanos/harness/dist/**/*.js';
|
|
33
|
+
__SOURCE_GLOBS__
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "Generated by `ekanos dev`. Edits here are overwritten on the next run — change your integration or harness.config.ts instead. Imports are spelled relative rather than through a `~/*` alias because this directory sits two levels below your project root, so one alias would have to mean two different roots. The team-accounts mock is typed by types/team-accounts.d.ts rather than a `paths` entry, so it resolves through the package export map. There is no `baseUrl`: TypeScript 7 removed it, and it is not needed here.",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"incremental": true,
|
|
17
|
+
"plugins": [{ "name": "next" }],
|
|
18
|
+
"tsBuildInfoFile": ".next/cache/tsbuildinfo.json"
|
|
19
|
+
},
|
|
20
|
+
"//include": "next build rewrites this file in place if it disagrees, which would fight regeneration on the next `ekanos dev`. These are the values Next asks for, pre-applied: `jsx: react-jsx` for the automatic runtime, and both generated type roots.",
|
|
21
|
+
"include": [
|
|
22
|
+
"next-env.d.ts",
|
|
23
|
+
"types/**/*.d.ts",
|
|
24
|
+
".next/types/**/*.ts",
|
|
25
|
+
".next/dev/types/**/*.ts",
|
|
26
|
+
"**/*.ts",
|
|
27
|
+
"**/*.tsx",
|
|
28
|
+
"../../harness.config.ts"
|
|
29
|
+
],
|
|
30
|
+
"exclude": ["node_modules", ".next"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Generated by `ekanos dev`. Owned by @ekanos/cli — edits are overwritten.
|
|
3
|
+
*
|
|
4
|
+
* Type side of the `next.config.mjs` resolveAlias. Widgets written for the
|
|
5
|
+
* real host import `@kit/team-accounts/hooks/use-team-account-workspace`,
|
|
6
|
+
* which does not exist here; the harness ships a shape-identical mock.
|
|
7
|
+
*
|
|
8
|
+
* This goes through the package's PUBLIC export map rather than a literal
|
|
9
|
+
* path into its `dist/`, so a harness release is free to move the file as
|
|
10
|
+
* long as it keeps the exports entry — which is what an exports map is for.
|
|
11
|
+
* A `paths` entry cannot do that: TypeScript resolves a `paths` target as a
|
|
12
|
+
* file path, not as a module specifier, so it would have to name `dist/`.
|
|
13
|
+
*/
|
|
14
|
+
declare module '@kit/team-accounts/hooks/use-team-account-workspace' {
|
|
15
|
+
export * from '@ekanos/harness/mocks/team-account-workspace';
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
slug: __IDENT__.slug,
|
|
3
|
+
name: __IDENT__.name,
|
|
4
|
+
description: __IDENT__.description,
|
|
5
|
+
|
|
6
|
+
// Powers the Triggers surface from your declared webhooks and schedules.
|
|
7
|
+
definition: asHarnessDefinition(__IDENT__),
|
|
8
|
+
|
|
9
|
+
// Recorded HTTP exchanges, per variant. This is how you give the harness
|
|
10
|
+
// something to answer with: it issues no requests of its own, and an
|
|
11
|
+
// unrecorded URL is refused by name rather than silently missing.
|
|
12
|
+
//
|
|
13
|
+
// Declared here rather than on a widget because a URL is not owned by one
|
|
14
|
+
// — the same request may answer two widgets and a webhook handler.
|
|
15
|
+
//
|
|
16
|
+
// Uncomment and point it at a route of yours:
|
|
17
|
+
//
|
|
18
|
+
// fixtures: {
|
|
19
|
+
// default: [
|
|
20
|
+
// {
|
|
21
|
+
// request: 'GET /api/integrations/__SLUG__/things',
|
|
22
|
+
// response: { things: [{ id: 1, name: 'First' }] },
|
|
23
|
+
// },
|
|
24
|
+
// ],
|
|
25
|
+
// // `error` is not a special code path — it is ordinary fixtures with
|
|
26
|
+
// // error statuses, so you exercise the real failure branch.
|
|
27
|
+
// error: [{ request: 'GET /api/integrations/__SLUG__/things', status: 503 }],
|
|
28
|
+
// },
|
|
29
|
+
//
|
|
30
|
+
// That is a HOST route, which is what most widgets actually call: the
|
|
31
|
+
// vendor credential lives server-side, so the browser talks to your route
|
|
32
|
+
// and your route talks to the vendor. Path-only under `/api` resolves
|
|
33
|
+
// against the harness's own origin, so it needs no configuration.
|
|
34
|
+
//
|
|
35
|
+
// Recording a vendor call directly? Use the absolute form —
|
|
36
|
+
// `'GET https://api.example.com/v1/things'`. The path-only shorthand
|
|
37
|
+
// outside `/api` resolves against the single origin in
|
|
38
|
+
// `defineIntegration({ egress })`, and this starter declares none.
|
|
39
|
+
// Every widget your definition declares. Deriving the list is just a
|
|
40
|
+
// starting point — replace this with explicit entries importing your
|
|
41
|
+
// widget components directly and the cast below goes away:
|
|
42
|
+
// `defineIntegration` erases a component to a structural
|
|
43
|
+
// `ComponentReference` so the schema package can stay free of React, and
|
|
44
|
+
// only the author knows the real prop type.
|
|
45
|
+
//
|
|
46
|
+
// A widget can also carry `seeds` — query-key values written straight into
|
|
47
|
+
// the react-query cache, so it paints with no request at all. That is the
|
|
48
|
+
// fast path, not the default: it works only if your data layer is
|
|
49
|
+
// react-query, and it addresses data by query key rather than by what the
|
|
50
|
+
// vendor actually returns. Prefer `fixtures` above; reach for `seeds` when
|
|
51
|
+
// you want one specific widget to skip the request entirely.
|
|
52
|
+
widgets: (__IDENT__.components?.widgets ?? []).map(
|
|
53
|
+
(widget): HarnessWidget => ({
|
|
54
|
+
id: widget.id,
|
|
55
|
+
title: widget.name,
|
|
56
|
+
component: widget.component as HarnessWidget['component'],
|
|
57
|
+
width: 'half',
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
// Uncomment as you build them out:
|
|
62
|
+
// tile: YourMarketplaceTile,
|
|
63
|
+
// activationForm: YourActivationForm,
|
|
64
|
+
// triggerMocks: { secrets: { account: { apiKey: 'test-key' } } },
|
|
65
|
+
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* THE HARNESS REGISTRY — the one harness file you own.
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `ekanos dev` writes this once and then never touches it again: it is yours,
|
|
7
|
+
* it is git-tracked, and the generated shell under `.ekanos/` reads it. That
|
|
8
|
+
* shell IS regenerated on every run, and `@ekanos/harness` is an ordinary
|
|
9
|
+
* dependency, so a harness bugfix reaches you through `pnpm up` with no edit
|
|
10
|
+
* to any file you own.
|
|
11
|
+
*
|
|
12
|
+
* Each entry appears at `/<slug>` with five surfaces wired up: widgets in the
|
|
13
|
+
* real dashboard grid, a single-widget page, the marketplace tile, the
|
|
14
|
+
* activation form, and Triggers (declared webhooks/schedules, executed
|
|
15
|
+
* locally against a mock context).
|
|
16
|
+
*
|
|
17
|
+
* The `fixtures` are the part that matters. The harness issues no requests of
|
|
18
|
+
* its own, so a surface stays empty until a recorded exchange answers it.
|
|
19
|
+
* Write them as HTTP — the shape your vendor actually returns — and one
|
|
20
|
+
* recording feeds every widget and handler that calls the same URL.
|
|
21
|
+
*
|
|
22
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
* ONE CONSTRAINT THIS FILE IMPOSES ON YOUR CODE
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* This module is imported from BOTH a server component (the harness index
|
|
27
|
+
* page) and a client component (the shell that provides the registry), so
|
|
28
|
+
* everything reachable from here must be importable in both graphs.
|
|
29
|
+
*
|
|
30
|
+
* In practice that means: do not reach into a `'use client'` module for
|
|
31
|
+
* fixture query keys. It compiles, then fails at request time with
|
|
32
|
+
*
|
|
33
|
+
* Attempted to call <yourKeyFn>() from the server but <yourKeyFn> is on
|
|
34
|
+
* the client.
|
|
35
|
+
*
|
|
36
|
+
* The fix is to move the query-key builders into a plain module with no
|
|
37
|
+
* `'use client'` directive and have your hooks import THAT. Your hooks stay
|
|
38
|
+
* client-only; only the key builders move. This is worth doing regardless —
|
|
39
|
+
* shared keys that both sides import are how you stop widget `seeds` drifting
|
|
40
|
+
* away from the hooks they are meant to seed.
|
|
41
|
+
*/
|
|
42
|
+
import { defineHarnessConfig } from '@ekanos/harness/config';
|
|
43
|
+
import {
|
|
44
|
+
asHarnessDefinition,
|
|
45
|
+
type HarnessWidget,
|
|
46
|
+
} from '@ekanos/harness/registry';
|
|
47
|
+
|
|
48
|
+
__INTEGRATION_IMPORTS__
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `defineHarnessConfig` is an identity function at runtime, but it is not
|
|
52
|
+
* decoration: it checks the things TypeScript cannot — that every slug and
|
|
53
|
+
* widget id survives being a URL segment, and that none of them collide. Those
|
|
54
|
+
* are the mistakes that produce a blank page or the WRONG page rather than an
|
|
55
|
+
* error, and this is the one place every entry passes through.
|
|
56
|
+
*/
|
|
57
|
+
export const harnessIntegrations = defineHarnessConfig([
|
|
58
|
+
__INTEGRATION_ENTRIES__]);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__SLUG__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A Fusion integration built with @ekanos/sdk.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@ekanos/sdk": "^0.1.0",
|
|
13
|
+
"@ekanos/ui": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@hookform/resolvers": "^5.2.2",
|
|
17
|
+
"@tanstack/react-query": "^5.101.4",
|
|
18
|
+
"@types/react": "^19.2.0",
|
|
19
|
+
"@types/react-dom": "^19.2.0",
|
|
20
|
+
"next": "^16.0.0",
|
|
21
|
+
"react": "^19.2.8",
|
|
22
|
+
"react-dom": "^19.2.8",
|
|
23
|
+
"react-hook-form": "^7.68.0",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vitest": "4.1.10",
|
|
26
|
+
"zod": "^3.25.76"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createMockContext } from '@ekanos/sdk/testing';
|
|
4
|
+
|
|
5
|
+
import { integration } from './integration';
|
|
6
|
+
|
|
7
|
+
describe('__DISPLAY_NAME__ integration', () => {
|
|
8
|
+
it('declares its slug and at least one widget', () => {
|
|
9
|
+
expect(integration.slug).toBe('__SLUG__');
|
|
10
|
+
expect(integration.components?.widgets?.length ?? 0).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('runs get_greeting against a mock context', async () => {
|
|
14
|
+
// `createMockContext` requires its options object, and `integration` is
|
|
15
|
+
// required within it — the mock derives the slug, storage schemas and
|
|
16
|
+
// egress allowlist from your declaration rather than guessing.
|
|
17
|
+
const ctx = createMockContext({ integration: { slug: '__SLUG__' } });
|
|
18
|
+
const tool = integration.tools?.find((t) => t.name === 'get_greeting');
|
|
19
|
+
expect(tool).toBeDefined();
|
|
20
|
+
|
|
21
|
+
const result = await tool!.run(ctx as never, { name: 'Ekanos' });
|
|
22
|
+
expect(result).toEqual({ greeting: 'Hello, Ekanos!' });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your integration, declared in one object.
|
|
3
|
+
*
|
|
4
|
+
* `defineIntegration()` validates this at import time and hands back a
|
|
5
|
+
* deep-frozen copy. The host re-validates the same object against the same
|
|
6
|
+
* schema when it registers the package, so a definition `ekanos validate`
|
|
7
|
+
* accepts is a definition the platform will accept.
|
|
8
|
+
*
|
|
9
|
+
* The export MUST be named `integration`.
|
|
10
|
+
*/
|
|
11
|
+
import { createElement } from 'react';
|
|
12
|
+
|
|
13
|
+
import { defineIntegration } from '@ekanos/sdk/integration';
|
|
14
|
+
|
|
15
|
+
// A minimal dashboard widget. This starter uses React.createElement so it can
|
|
16
|
+
// live in a .ts file; move it to its own .tsx file and reach for the Widget.*
|
|
17
|
+
// compound API from '@ekanos/sdk/components' when you build something real.
|
|
18
|
+
function HelloWidget() {
|
|
19
|
+
return createElement(
|
|
20
|
+
'div',
|
|
21
|
+
{ style: { padding: 16 } },
|
|
22
|
+
'Hello from __DISPLAY_NAME__',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const integration = defineIntegration({
|
|
27
|
+
slug: '__SLUG__',
|
|
28
|
+
name: '__DISPLAY_NAME__',
|
|
29
|
+
description:
|
|
30
|
+
'A starter __DISPLAY_NAME__ integration scaffolded by @ekanos/cli. Replace this with a real description of what your integration does.',
|
|
31
|
+
version: '0.1.0',
|
|
32
|
+
|
|
33
|
+
capabilities: [
|
|
34
|
+
{
|
|
35
|
+
label: 'A first capability',
|
|
36
|
+
description: 'Describe one thing this integration does for the user.',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
permissions: [
|
|
41
|
+
{
|
|
42
|
+
label: 'Read your data',
|
|
43
|
+
detail: 'Explain exactly what this integration reads, and why.',
|
|
44
|
+
type: 'read',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
components: {
|
|
49
|
+
widgets: [
|
|
50
|
+
{
|
|
51
|
+
// Widget ids are GLOBAL — the dashboard resolves widget_config rows by
|
|
52
|
+
// them — so keep the integration slug as the prefix.
|
|
53
|
+
id: '__SLUG__-hello',
|
|
54
|
+
name: 'Hello',
|
|
55
|
+
component: HelloWidget,
|
|
56
|
+
widgetState: 'active',
|
|
57
|
+
isCollapsible: true,
|
|
58
|
+
layouts: {
|
|
59
|
+
lg: { x: 0, y: 0, w: 4, h: 6 },
|
|
60
|
+
md: { x: 0, y: 0, w: 4, h: 6 },
|
|
61
|
+
sm: { x: 0, y: 0, w: 4, h: 6 },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
tools: [
|
|
68
|
+
{
|
|
69
|
+
name: 'get_greeting',
|
|
70
|
+
description: 'Return a friendly greeting from __DISPLAY_NAME__.',
|
|
71
|
+
parameters: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: { name: { type: 'string' } },
|
|
74
|
+
required: [],
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
run: async (_ctx, args) => {
|
|
78
|
+
const who = typeof args.name === 'string' ? args.name : 'there';
|
|
79
|
+
return { greeting: `Hello, ${who}!` };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src", "harness.config.ts", "vitest.config.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist", ".ekanos"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
// Some @ekanos/sdk client entrypoints transitively import `server-only`,
|
|
8
|
+
// whose default export throws outside a React Server Component. Point it
|
|
9
|
+
// at a no-op stub for the Node test process. (Create the stub at
|
|
10
|
+
// src/__tests__/server-only.stub.ts when you import an SDK client
|
|
11
|
+
// surface from a tested module.)
|
|
12
|
+
'server-only': fileURLToPath(
|
|
13
|
+
new URL('./src/__tests__/server-only.stub.ts', import.meta.url),
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
test: {
|
|
18
|
+
globals: true,
|
|
19
|
+
environment: 'node',
|
|
20
|
+
include: ['src/**/*.test.ts'],
|
|
21
|
+
// REQUIRED — do not remove. The published @ekanos/sdk dist imports the
|
|
22
|
+
// extensionless "next/link"; without inlining the SDK here, vitest cannot
|
|
23
|
+
// transform and load it, your test files import nothing, and vitest reports
|
|
24
|
+
// GREEN over zero tests. `ekanos validate` fails the build if this block
|
|
25
|
+
// is missing.
|
|
26
|
+
server: {
|
|
27
|
+
deps: {
|
|
28
|
+
inline: ['@ekanos/sdk'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|