@ekanos/cli 0.1.1 → 0.1.3
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 +35 -4
- package/dist/auth/session.d.ts +25 -7
- package/dist/auth/session.js +88 -15
- package/dist/auth/session.js.map +1 -1
- package/dist/banner.d.ts +21 -0
- package/dist/banner.js +89 -0
- package/dist/banner.js.map +1 -0
- package/dist/commands/init.d.ts +8 -1
- package/dist/commands/init.js +19 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +16 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +3 -1
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +85 -27
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +28 -0
- package/dist/commands/status.js +146 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +3 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.js +5 -0
- package/dist/context.js.map +1 -1
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +11 -0
- package/dist/project.js +28 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +45 -0
- package/dist/publish-api.js +113 -0
- package/dist/publish-api.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +247 -0
- package/templates/CLAUDE.md.tmpl +6 -0
- package/templates/claude-skill.md.tmpl +53 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ against the published `@ekanos` packages — from any Node project, with no
|
|
|
5
5
|
bundler and no dependency on `@ekanos/sdk` at runtime.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @ekanos/cli init --slug my-integration --yes #
|
|
8
|
+
npx @ekanos/cli init --slug my-integration --yes # scaffolds into ./my-integration (or --dir . to scaffold in place)
|
|
9
9
|
npx @ekanos/cli validate
|
|
10
10
|
npx @ekanos/cli test
|
|
11
11
|
npx @ekanos/cli dev
|
|
@@ -15,13 +15,15 @@ npx @ekanos/cli dev
|
|
|
15
15
|
|
|
16
16
|
| Verb | What it does |
|
|
17
17
|
|---|---|
|
|
18
|
-
| `init` | Scaffold `ekanos.json`, a working `defineIntegration`, a test, a `tsconfig.json`, a vitest config, `.gitignore`,
|
|
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
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
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
21
|
| `test` | Delegate to the project's own test script via its package manager. |
|
|
22
22
|
| `login` | Authenticate this machine against a Fusion deployment (Auth0 device flow). |
|
|
23
23
|
| `logout` | Revoke the session at Auth0 and delete the local credentials. |
|
|
24
24
|
| `whoami` | Print the identity the stored session authenticates as. |
|
|
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
|
+
| `publish` | Validate, pack and submit the integration to a Fusion host. |
|
|
25
27
|
|
|
26
28
|
## Global flags
|
|
27
29
|
|
|
@@ -81,6 +83,8 @@ than a warning.
|
|
|
81
83
|
| `entry` | yes, in the single form | top level | path to the module exporting your `defineIntegration(...)` result |
|
|
82
84
|
| `integrations[]` | yes, in the list form | top level | `{ slug, entry }` per integration, same rules, slugs unique |
|
|
83
85
|
| `sourceGlobs` | no | **top level only** | extra Tailwind `@source` globs; project-relative, no `..` |
|
|
86
|
+
| `source` | no | top level | the Fusion source slug this project publishes to; written by the first successful `publish` |
|
|
87
|
+
| `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 |
|
|
84
88
|
| `$schema` | no | top level | ignored, for editor tooling |
|
|
85
89
|
|
|
86
90
|
The file is **strict**: an unrecognised key is an error naming the key, not a
|
|
@@ -258,10 +262,37 @@ carries the same for manifest fields it could not resolve.
|
|
|
258
262
|
|
|
259
263
|
```bash
|
|
260
264
|
ekanos login --host https://your-fusion-host # or set EKANOS_HOST
|
|
261
|
-
ekanos whoami
|
|
262
|
-
ekanos logout
|
|
265
|
+
ekanos whoami # host resolves — see below
|
|
266
|
+
ekanos logout
|
|
263
267
|
```
|
|
264
268
|
|
|
269
|
+
### Host resolution
|
|
270
|
+
|
|
271
|
+
`logout`, `whoami`, `status` and `publish` resolve the Fusion host in this
|
|
272
|
+
order:
|
|
273
|
+
|
|
274
|
+
1. the `--host` flag
|
|
275
|
+
2. the `EKANOS_HOST` environment variable
|
|
276
|
+
3. the `host` field in the project's `ekanos.json`
|
|
277
|
+
4. the credential store, when it holds **exactly one** host (which one was
|
|
278
|
+
chosen is logged to stderr)
|
|
279
|
+
|
|
280
|
+
`login` is the exception: it resolves from `--host`, else `EKANOS_HOST`, else
|
|
281
|
+
the sole stored host — **never** from `ekanos.json`. Login is the one verb
|
|
282
|
+
that *creates* credentials, and `ekanos.json` is a committed file; if login
|
|
283
|
+
honored it, anyone could point your device flow at their own tenant just by
|
|
284
|
+
editing a file in a repo you clone. Every other verb is safe reading it,
|
|
285
|
+
because their bearer tokens are looked up per-host from the credential store —
|
|
286
|
+
an unfamiliar host in `ekanos.json` simply has no session to present.
|
|
287
|
+
|
|
288
|
+
So the common single-deployment flow is: `ekanos login --host <url>` once, then
|
|
289
|
+
every later verb with no flag at all. The first successful `ekanos publish`
|
|
290
|
+
also persists `host` (and `source`) into `ekanos.json`, so the project itself
|
|
291
|
+
remembers its deployment from then on. With **two or more** stored hosts and no
|
|
292
|
+
other answer, the CLI refuses with a usage error listing them — it never picks
|
|
293
|
+
one silently. `login` never writes `ekanos.json`; it is routinely run outside a
|
|
294
|
+
project.
|
|
295
|
+
|
|
265
296
|
`login` uses the **OAuth 2.0 device authorization flow**: it prints a short code
|
|
266
297
|
and a URL to **stderr**, and you approve the request in a browser on any device.
|
|
267
298
|
That is deliberate — a loopback/PKCE login needs a browser on the same machine
|
package/dist/auth/session.d.ts
CHANGED
|
@@ -11,15 +11,33 @@ export interface AuthEnvironment {
|
|
|
11
11
|
host: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Resolve the Fusion host
|
|
14
|
+
* Resolve the Fusion host, in this order:
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* host
|
|
19
|
-
*
|
|
16
|
+
* 1. the `--host` flag
|
|
17
|
+
* 2. the `EKANOS_HOST` environment variable
|
|
18
|
+
* 3. the `host` field of the project's `ekanos.json` (written by the first
|
|
19
|
+
* successful `ekanos publish`, or by hand)
|
|
20
|
+
* 4. the credential store, when it holds EXACTLY one host — a partner logged
|
|
21
|
+
* in to a single deployment should not have to repeat its URL on every
|
|
22
|
+
* verb, and one stored host is unambiguous
|
|
23
|
+
*
|
|
24
|
+
* Deliberately no default beyond those. A baked-in default would be wrong for
|
|
25
|
+
* every partner running against their own sandbox, and — worse — a CLI that
|
|
26
|
+
* silently picks a host is a CLI that can send a developer's credentials
|
|
27
|
+
* somewhere they did not intend. Two or more stored hosts is exactly that
|
|
28
|
+
* ambiguity, so it stays a usage error that LISTS them rather than a guess.
|
|
20
29
|
*/
|
|
21
|
-
export declare function resolveHost(flag: string | undefined, env: Record<string, string | undefined
|
|
22
|
-
|
|
30
|
+
export declare function resolveHost(ctx: CliContext, flag: string | undefined, env: Record<string, string | undefined>, options?: {
|
|
31
|
+
/** Directory whose ekanos.json may carry a `host` field. */
|
|
32
|
+
projectDir?: string;
|
|
33
|
+
/** The credential store, for the sole-stored-host fallback. */
|
|
34
|
+
store?: CredentialStore;
|
|
35
|
+
}): string;
|
|
36
|
+
export interface ResolveAuthOptions {
|
|
37
|
+
/** Directory whose ekanos.json may carry a `host` field. */
|
|
38
|
+
projectDir?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveAuthEnvironment(ctx: CliContext, hostFlag: string | undefined, env: Record<string, string | undefined>, options?: ResolveAuthOptions): AuthEnvironment;
|
|
23
41
|
/** A store with no host attached — for `logout --all`. */
|
|
24
42
|
export declare function newStore(ctx: CliContext, env: Record<string, string | undefined>): CredentialStore;
|
|
25
43
|
export declare function requireSession(store: CredentialStore, host: string): StoredSession;
|
package/dist/auth/session.js
CHANGED
|
@@ -1,28 +1,101 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
1
3
|
import { authRequiredError, usageError } from '../errors.js';
|
|
2
4
|
import { CredentialStore, normalizeHost, resolveEkanosHome, } from './credential-store.js';
|
|
3
5
|
import { fetchIdentity, refreshFusionSession, } from './fusion-api.js';
|
|
4
6
|
/**
|
|
5
|
-
* Resolve the Fusion host
|
|
7
|
+
* Resolve the Fusion host, in this order:
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* host
|
|
10
|
-
*
|
|
9
|
+
* 1. the `--host` flag
|
|
10
|
+
* 2. the `EKANOS_HOST` environment variable
|
|
11
|
+
* 3. the `host` field of the project's `ekanos.json` (written by the first
|
|
12
|
+
* successful `ekanos publish`, or by hand)
|
|
13
|
+
* 4. the credential store, when it holds EXACTLY one host — a partner logged
|
|
14
|
+
* in to a single deployment should not have to repeat its URL on every
|
|
15
|
+
* verb, and one stored host is unambiguous
|
|
16
|
+
*
|
|
17
|
+
* Deliberately no default beyond those. A baked-in default would be wrong for
|
|
18
|
+
* every partner running against their own sandbox, and — worse — a CLI that
|
|
19
|
+
* silently picks a host is a CLI that can send a developer's credentials
|
|
20
|
+
* somewhere they did not intend. Two or more stored hosts is exactly that
|
|
21
|
+
* ambiguity, so it stays a usage error that LISTS them rather than a guess.
|
|
11
22
|
*/
|
|
12
|
-
export function resolveHost(flag, env) {
|
|
23
|
+
export function resolveHost(ctx, flag, env, options = {}) {
|
|
24
|
+
var _a, _b;
|
|
13
25
|
const raw = flag !== null && flag !== void 0 ? flag : env.EKANOS_HOST;
|
|
14
|
-
if (
|
|
15
|
-
|
|
26
|
+
if (raw && raw.trim().length > 0) {
|
|
27
|
+
return normalizeHost(raw.trim());
|
|
28
|
+
}
|
|
29
|
+
if (options.projectDir) {
|
|
30
|
+
const declared = readProjectHost(options.projectDir);
|
|
31
|
+
if (declared) {
|
|
32
|
+
// normalizeHost throws a precondition error for a malformed value —
|
|
33
|
+
// an EXPLICIT host in ekanos.json that is wrong deserves an error
|
|
34
|
+
// naming it, never a silent fall-through to a different host.
|
|
35
|
+
const host = normalizeHost(declared);
|
|
36
|
+
// Announce the choice, like the sole-stored-host branch below. This
|
|
37
|
+
// source deserves it MOST: ekanos.json is a committed file, so its
|
|
38
|
+
// host can change under a developer via an ordinary merge or an
|
|
39
|
+
// agent-applied patch — and this is where login sends the OAuth flow
|
|
40
|
+
// and publish sends the bearer token and archive.
|
|
41
|
+
ctx.log(`ekanos: using ${host} — the host saved in ekanos.json. ` +
|
|
42
|
+
`Pass --host to override for one command.`);
|
|
43
|
+
return host;
|
|
44
|
+
}
|
|
16
45
|
}
|
|
17
|
-
|
|
46
|
+
const stored = (_b = (_a = options.store) === null || _a === void 0 ? void 0 : _a.hosts()) !== null && _b !== void 0 ? _b : [];
|
|
47
|
+
if (stored.length === 1) {
|
|
48
|
+
const host = stored[0];
|
|
49
|
+
ctx.log(`ekanos: using ${host} — the only host you are logged in to. ` +
|
|
50
|
+
`Pass --host to target a different one.`);
|
|
51
|
+
return host;
|
|
52
|
+
}
|
|
53
|
+
throw usageError('No Fusion host given.', stored.length > 1
|
|
54
|
+
? `You are logged in to ${stored.length} hosts (${stored.join(', ')}) — ` +
|
|
55
|
+
`pass "--host <url>" to choose one, or set EKANOS_HOST.`
|
|
56
|
+
: 'Pass "--host https://<your-fusion-host>" or set EKANOS_HOST. ' +
|
|
57
|
+
'"ekanos login --host <url>" stores the host, so later commands ' +
|
|
58
|
+
'can omit it.');
|
|
18
59
|
}
|
|
19
|
-
|
|
20
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Best-effort read of the `host` field from a directory's ekanos.json.
|
|
62
|
+
*
|
|
63
|
+
* Lenient on purpose: host resolution runs for verbs like `login` that are
|
|
64
|
+
* routinely run OUTSIDE a project, so a missing, unparseable, or host-less
|
|
65
|
+
* ekanos.json is simply "no answer here", never an error — the verbs that
|
|
66
|
+
* require a valid project (`publish`, `validate`) load it strictly themselves.
|
|
67
|
+
*/
|
|
68
|
+
function readProjectHost(projectDir) {
|
|
69
|
+
const configPath = path.join(projectDir, 'ekanos.json');
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
catch (_a) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
catch (_b) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const host = typeof parsed === 'object' && parsed !== null
|
|
85
|
+
? parsed.host
|
|
86
|
+
: undefined;
|
|
87
|
+
return typeof host === 'string' && host.trim().length > 0
|
|
88
|
+
? host.trim()
|
|
89
|
+
: null;
|
|
90
|
+
}
|
|
91
|
+
export function resolveAuthEnvironment(ctx, hostFlag, env, options = {}) {
|
|
92
|
+
const store = newStore(ctx, env);
|
|
93
|
+
const host = resolveHost(ctx, hostFlag, env, {
|
|
94
|
+
projectDir: options.projectDir,
|
|
95
|
+
store,
|
|
96
|
+
});
|
|
21
97
|
warnOnPlaintextHost(ctx, host);
|
|
22
|
-
return {
|
|
23
|
-
host,
|
|
24
|
-
store: newStore(ctx, env),
|
|
25
|
-
};
|
|
98
|
+
return { host, store };
|
|
26
99
|
}
|
|
27
100
|
/** A store with no host attached — for `logout --all`. */
|
|
28
101
|
export function newStore(ctx, env) {
|
package/dist/auth/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/auth/session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CliContext } from '../context';\nimport { authRequiredError, usageError } from '../errors';\nimport {\n CredentialStore,\n type StoredSession,\n normalizeHost,\n resolveEkanosHome,\n} from './credential-store';\nimport {\n type FusionIdentity,\n fetchIdentity,\n refreshFusionSession,\n} from './fusion-api';\n\n/**\n * Session plumbing shared by `login`, `logout` and `whoami`: where the Fusion\n * host comes from, where credentials live, and how a stale access token is\n * silently replaced.\n */\n\nexport interface AuthEnvironment {\n store: CredentialStore;\n host: string;\n}\n\n/**\n * Resolve the Fusion host from `--host`, else `EKANOS_HOST`.\n *\n * Deliberately no default. A baked-in default would be wrong for every partner\n * running against their own sandbox, and — worse — a CLI that silently picks a\n * host is a CLI that can send a developer's credentials somewhere they did not\n * intend.\n */\nexport function resolveHost(\n flag: string | undefined,\n env: Record<string, string | undefined>,\n): string {\n const raw = flag ?? env.EKANOS_HOST;\n\n if (!raw || raw.trim().length === 0) {\n throw usageError(\n 'No Fusion host given.',\n 'Pass \"--host https://<your-fusion-host>\" or set EKANOS_HOST.',\n );\n }\n\n return normalizeHost(raw.trim());\n}\n\nexport function resolveAuthEnvironment(\n ctx: CliContext,\n hostFlag: string | undefined,\n env: Record<string, string | undefined>,\n): AuthEnvironment {\n const host = resolveHost(hostFlag, env);\n\n warnOnPlaintextHost(ctx, host);\n\n return {\n host,\n store: newStore(ctx, env),\n };\n}\n\n/** A store with no host attached — for `logout --all`. */\nexport function newStore(\n ctx: CliContext,\n env: Record<string, string | undefined>,\n): CredentialStore {\n return new CredentialStore({\n home: resolveEkanosHome(env),\n // Permission warnings are diagnostics, not results: stderr, always.\n warn: (message) => ctx.warn(message),\n });\n}\n\n/**\n * `http://` is allowed because local development needs it, but sending a\n * bearer token to a plaintext REMOTE host puts it on the wire in the clear.\n * Warn rather than refuse: refusing would break `--host http://localhost:3000`\n * for anyone whose loopback resolves to something other than the names below.\n */\nfunction warnOnPlaintextHost(ctx: CliContext, host: string): void {\n const url = new URL(host);\n\n if (url.protocol !== 'http:') return;\n\n const loopback = ['localhost', '127.0.0.1', '[::1]', '0.0.0.0'];\n\n if (loopback.includes(url.hostname) || url.hostname.endsWith('.localhost')) {\n return;\n }\n\n ctx.warn(\n `ekanos: ${host} is plaintext HTTP. Your credentials will cross the ` +\n `network unencrypted. Use https:// unless this is a local host.`,\n );\n}\n\nexport function requireSession(\n store: CredentialStore,\n host: string,\n): StoredSession {\n const session = store.read(host);\n\n if (!session) {\n throw authRequiredError(\n `Not logged in to ${host}.`,\n `Run \"ekanos login --host ${host}\".`,\n );\n }\n\n return session;\n}\n\n/**\n * Resolve the caller's identity, refreshing the access token once if the\n * stored one no longer authenticates.\n *\n * Validity is decided by the SERVER, not by comparing `expiresAt` to the local\n * clock. A container with a skewed clock — the exact environment this flow\n * exists to support — would otherwise either refuse a perfectly good token or\n * keep presenting a dead one. A 401 is the authoritative answer; one refresh\n * and one retry is the whole recovery.\n */\nexport async function resolveIdentity(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<{ identity: FusionIdentity; session: StoredSession }> {\n const identity = await fetchIdentity(env.host, session.accessToken);\n\n if (identity) {\n return { identity, session };\n }\n\n const refreshed = await refreshStoredSession(env, session);\n const retried = await fetchIdentity(env.host, refreshed.accessToken);\n\n if (!retried) {\n throw authRequiredError(\n `The session for ${env.host} could not be renewed.`,\n `Run \"ekanos logout --host ${env.host}\" then \"ekanos login --host ${env.host}\".`,\n );\n }\n\n return { identity: retried, session: refreshed };\n}\n\n/**\n * Refresh and persist.\n *\n * The rotated refresh token is written the MOMENT the server reports it, not\n * when the refresh finishes. Auth0 rotates as soon as the server's exchange\n * succeeds, so from that instant the presented token is dead and the\n * replacement is the only copy in existence — and `cli-refresh` can still fail\n * afterwards (a `/userinfo` blip, a missing account, an unexpected throw) and\n * return that replacement on an ERROR response. Saving only on success would\n * discard it and lock the partner out for good over a transient upstream\n * hiccup: one process, no concurrency needed.\n *\n * So `onRotatedToken` writes it immediately and `refreshFusionSession` invokes\n * that before it throws. The write keeps the OLD access token, which is\n * correct: that token is merely expired, while the refresh token is current\n * and is what the next attempt needs.\n */\nexport async function refreshStoredSession(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<StoredSession> {\n const persistRotated = (rotated: string) => {\n env.store.write(env.host, {\n ...session,\n auth0RefreshToken: rotated,\n updatedAt: new Date().toISOString(),\n });\n };\n\n const refreshed = await refreshFusionSession(\n env.host,\n session.auth0RefreshToken,\n { onRotatedToken: persistRotated },\n );\n\n const next: StoredSession = {\n auth0RefreshToken: refreshed.auth0RefreshToken,\n accessToken: refreshed.accessToken,\n expiresAt: refreshed.expiresAt,\n userId: refreshed.userId,\n email: refreshed.email,\n updatedAt: new Date().toISOString(),\n };\n\n env.store.write(env.host, next);\n\n return next;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/auth/session.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { authRequiredError, usageError } from '../errors';\nimport {\n CredentialStore,\n type StoredSession,\n normalizeHost,\n resolveEkanosHome,\n} from './credential-store';\nimport {\n type FusionIdentity,\n fetchIdentity,\n refreshFusionSession,\n} from './fusion-api';\n\n/**\n * Session plumbing shared by `login`, `logout` and `whoami`: where the Fusion\n * host comes from, where credentials live, and how a stale access token is\n * silently replaced.\n */\n\nexport interface AuthEnvironment {\n store: CredentialStore;\n host: string;\n}\n\n/**\n * Resolve the Fusion host, in this order:\n *\n * 1. the `--host` flag\n * 2. the `EKANOS_HOST` environment variable\n * 3. the `host` field of the project's `ekanos.json` (written by the first\n * successful `ekanos publish`, or by hand)\n * 4. the credential store, when it holds EXACTLY one host — a partner logged\n * in to a single deployment should not have to repeat its URL on every\n * verb, and one stored host is unambiguous\n *\n * Deliberately no default beyond those. A baked-in default would be wrong for\n * every partner running against their own sandbox, and — worse — a CLI that\n * silently picks a host is a CLI that can send a developer's credentials\n * somewhere they did not intend. Two or more stored hosts is exactly that\n * ambiguity, so it stays a usage error that LISTS them rather than a guess.\n */\nexport function resolveHost(\n ctx: CliContext,\n flag: string | undefined,\n env: Record<string, string | undefined>,\n options: {\n /** Directory whose ekanos.json may carry a `host` field. */\n projectDir?: string;\n /** The credential store, for the sole-stored-host fallback. */\n store?: CredentialStore;\n } = {},\n): string {\n const raw = flag ?? env.EKANOS_HOST;\n\n if (raw && raw.trim().length > 0) {\n return normalizeHost(raw.trim());\n }\n\n if (options.projectDir) {\n const declared = readProjectHost(options.projectDir);\n\n if (declared) {\n // normalizeHost throws a precondition error for a malformed value —\n // an EXPLICIT host in ekanos.json that is wrong deserves an error\n // naming it, never a silent fall-through to a different host.\n const host = normalizeHost(declared);\n\n // Announce the choice, like the sole-stored-host branch below. This\n // source deserves it MOST: ekanos.json is a committed file, so its\n // host can change under a developer via an ordinary merge or an\n // agent-applied patch — and this is where login sends the OAuth flow\n // and publish sends the bearer token and archive.\n ctx.log(\n `ekanos: using ${host} — the host saved in ekanos.json. ` +\n `Pass --host to override for one command.`,\n );\n\n return host;\n }\n }\n\n const stored = options.store?.hosts() ?? [];\n\n if (stored.length === 1) {\n const host = stored[0]!;\n\n ctx.log(\n `ekanos: using ${host} — the only host you are logged in to. ` +\n `Pass --host to target a different one.`,\n );\n\n return host;\n }\n\n throw usageError(\n 'No Fusion host given.',\n stored.length > 1\n ? `You are logged in to ${stored.length} hosts (${stored.join(', ')}) — ` +\n `pass \"--host <url>\" to choose one, or set EKANOS_HOST.`\n : 'Pass \"--host https://<your-fusion-host>\" or set EKANOS_HOST. ' +\n '\"ekanos login --host <url>\" stores the host, so later commands ' +\n 'can omit it.',\n );\n}\n\n/**\n * Best-effort read of the `host` field from a directory's ekanos.json.\n *\n * Lenient on purpose: host resolution runs for verbs like `login` that are\n * routinely run OUTSIDE a project, so a missing, unparseable, or host-less\n * ekanos.json is simply \"no answer here\", never an error — the verbs that\n * require a valid project (`publish`, `validate`) load it strictly themselves.\n */\nfunction readProjectHost(projectDir: string): string | null {\n const configPath = path.join(projectDir, 'ekanos.json');\n\n let raw: string;\n try {\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const host =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { host?: unknown }).host\n : undefined;\n\n return typeof host === 'string' && host.trim().length > 0\n ? host.trim()\n : null;\n}\n\nexport interface ResolveAuthOptions {\n /** Directory whose ekanos.json may carry a `host` field. */\n projectDir?: string;\n}\n\nexport function resolveAuthEnvironment(\n ctx: CliContext,\n hostFlag: string | undefined,\n env: Record<string, string | undefined>,\n options: ResolveAuthOptions = {},\n): AuthEnvironment {\n const store = newStore(ctx, env);\n const host = resolveHost(ctx, hostFlag, env, {\n projectDir: options.projectDir,\n store,\n });\n\n warnOnPlaintextHost(ctx, host);\n\n return { host, store };\n}\n\n/** A store with no host attached — for `logout --all`. */\nexport function newStore(\n ctx: CliContext,\n env: Record<string, string | undefined>,\n): CredentialStore {\n return new CredentialStore({\n home: resolveEkanosHome(env),\n // Permission warnings are diagnostics, not results: stderr, always.\n warn: (message) => ctx.warn(message),\n });\n}\n\n/**\n * `http://` is allowed because local development needs it, but sending a\n * bearer token to a plaintext REMOTE host puts it on the wire in the clear.\n * Warn rather than refuse: refusing would break `--host http://localhost:3000`\n * for anyone whose loopback resolves to something other than the names below.\n */\nfunction warnOnPlaintextHost(ctx: CliContext, host: string): void {\n const url = new URL(host);\n\n if (url.protocol !== 'http:') return;\n\n const loopback = ['localhost', '127.0.0.1', '[::1]', '0.0.0.0'];\n\n if (loopback.includes(url.hostname) || url.hostname.endsWith('.localhost')) {\n return;\n }\n\n ctx.warn(\n `ekanos: ${host} is plaintext HTTP. Your credentials will cross the ` +\n `network unencrypted. Use https:// unless this is a local host.`,\n );\n}\n\nexport function requireSession(\n store: CredentialStore,\n host: string,\n): StoredSession {\n const session = store.read(host);\n\n if (!session) {\n throw authRequiredError(\n `Not logged in to ${host}.`,\n `Run \"ekanos login --host ${host}\".`,\n );\n }\n\n return session;\n}\n\n/**\n * Resolve the caller's identity, refreshing the access token once if the\n * stored one no longer authenticates.\n *\n * Validity is decided by the SERVER, not by comparing `expiresAt` to the local\n * clock. A container with a skewed clock — the exact environment this flow\n * exists to support — would otherwise either refuse a perfectly good token or\n * keep presenting a dead one. A 401 is the authoritative answer; one refresh\n * and one retry is the whole recovery.\n */\nexport async function resolveIdentity(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<{ identity: FusionIdentity; session: StoredSession }> {\n const identity = await fetchIdentity(env.host, session.accessToken);\n\n if (identity) {\n return { identity, session };\n }\n\n const refreshed = await refreshStoredSession(env, session);\n const retried = await fetchIdentity(env.host, refreshed.accessToken);\n\n if (!retried) {\n throw authRequiredError(\n `The session for ${env.host} could not be renewed.`,\n `Run \"ekanos logout --host ${env.host}\" then \"ekanos login --host ${env.host}\".`,\n );\n }\n\n return { identity: retried, session: refreshed };\n}\n\n/**\n * Refresh and persist.\n *\n * The rotated refresh token is written the MOMENT the server reports it, not\n * when the refresh finishes. Auth0 rotates as soon as the server's exchange\n * succeeds, so from that instant the presented token is dead and the\n * replacement is the only copy in existence — and `cli-refresh` can still fail\n * afterwards (a `/userinfo` blip, a missing account, an unexpected throw) and\n * return that replacement on an ERROR response. Saving only on success would\n * discard it and lock the partner out for good over a transient upstream\n * hiccup: one process, no concurrency needed.\n *\n * So `onRotatedToken` writes it immediately and `refreshFusionSession` invokes\n * that before it throws. The write keeps the OLD access token, which is\n * correct: that token is merely expired, while the refresh token is current\n * and is what the next attempt needs.\n */\nexport async function refreshStoredSession(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<StoredSession> {\n const persistRotated = (rotated: string) => {\n env.store.write(env.host, {\n ...session,\n auth0RefreshToken: rotated,\n updatedAt: new Date().toISOString(),\n });\n };\n\n const refreshed = await refreshFusionSession(\n env.host,\n session.auth0RefreshToken,\n { onRotatedToken: persistRotated },\n );\n\n const next: StoredSession = {\n auth0RefreshToken: refreshed.auth0RefreshToken,\n accessToken: refreshed.accessToken,\n expiresAt: refreshed.expiresAt,\n userId: refreshed.userId,\n email: refreshed.email,\n updatedAt: new Date().toISOString(),\n };\n\n env.store.write(env.host, next);\n\n return next;\n}\n"]}
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CliContext } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Paint ASCII art lines with the gradient, one color per column, left to
|
|
4
|
+
* right. `color: false` renders the raw art with zero escape codes — the
|
|
5
|
+
* `NO_COLOR` and non-TTY paths. Pure and exported so the gradient math is
|
|
6
|
+
* unit-testable independent of the printing gate in `printBanner`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function paintBanner(lines: readonly string[], color: boolean): string;
|
|
9
|
+
/**
|
|
10
|
+
* Print the gradient banner to stderr, gated on ALL of:
|
|
11
|
+
*
|
|
12
|
+
* - human mode (never in JSON mode — an agent must never see this)
|
|
13
|
+
* - stderr being a TTY (never when piped, redirected, or captured)
|
|
14
|
+
* - `EKANOS_NO_BANNER` unset (any value at all suppresses it entirely)
|
|
15
|
+
*
|
|
16
|
+
* `NO_COLOR` does not suppress the banner — it renders the same art with zero
|
|
17
|
+
* escape codes, per the standard (https://no-color.org).
|
|
18
|
+
*
|
|
19
|
+
* Call sites stay a single line: all gating lives here, not at the call site.
|
|
20
|
+
*/
|
|
21
|
+
export declare function printBanner(ctx: CliContext, kind: 'full' | 'mini'): void;
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** The website's brand gradient: teal → mauve → red, left to right. */
|
|
2
|
+
const GRADIENT_STOPS = [
|
|
3
|
+
[0x70, 0xa5, 0xab],
|
|
4
|
+
[0x9f, 0x70, 0x77],
|
|
5
|
+
[0xed, 0x1c, 0x24],
|
|
6
|
+
];
|
|
7
|
+
/** Option C ("colossal") — the full banner, shown on first init/login. */
|
|
8
|
+
const FULL_ART = [
|
|
9
|
+
' .d88b. 888 888 8888b. 88888b. .d88b. .d8888b ',
|
|
10
|
+
'd8P Y8b 888 .88P "88b 888 "88b d88""88b 88K ',
|
|
11
|
+
'88888888 888888K .d888888 888 888 888 888 "Y8888b. ',
|
|
12
|
+
'Y8b. 888 "88b 888 888 888 888 Y88..88P X88 ',
|
|
13
|
+
' "Y8888 888 888 "Y888888 888 888 "Y88P" 88888P\' ',
|
|
14
|
+
];
|
|
15
|
+
/** Option D ("pagga") — the mini-mark, shown on a repeat login. */
|
|
16
|
+
const MINI_ART = [
|
|
17
|
+
'░█▀▀░█░█░█▀█░█▀█░█▀█░█▀▀',
|
|
18
|
+
'░█▀▀░█▀▄░█▀█░█░█░█░█░▀▀█',
|
|
19
|
+
'░▀▀▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀',
|
|
20
|
+
];
|
|
21
|
+
const TAGLINE = 'The SDK with a skill-full DX';
|
|
22
|
+
function lerp(a, b, t) {
|
|
23
|
+
return Math.round(a + (b - a) * t);
|
|
24
|
+
}
|
|
25
|
+
/** The gradient color at position `t` (0..1) along the three-stop ramp. */
|
|
26
|
+
function colorAt(t) {
|
|
27
|
+
const [start, mid, end] = GRADIENT_STOPS;
|
|
28
|
+
const [from, to, localT] = t < 0.5 ? [start, mid, t * 2] : [mid, end, (t - 0.5) * 2];
|
|
29
|
+
return [
|
|
30
|
+
lerp(from[0], to[0], localT),
|
|
31
|
+
lerp(from[1], to[1], localT),
|
|
32
|
+
lerp(from[2], to[2], localT),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Paint ASCII art lines with the gradient, one color per column, left to
|
|
37
|
+
* right. `color: false` renders the raw art with zero escape codes — the
|
|
38
|
+
* `NO_COLOR` and non-TTY paths. Pure and exported so the gradient math is
|
|
39
|
+
* unit-testable independent of the printing gate in `printBanner`.
|
|
40
|
+
*/
|
|
41
|
+
export function paintBanner(lines, color) {
|
|
42
|
+
const width = Math.max(...lines.map((line) => line.length));
|
|
43
|
+
return lines
|
|
44
|
+
.map((line) => {
|
|
45
|
+
const padded = line.padEnd(width);
|
|
46
|
+
if (!color)
|
|
47
|
+
return padded;
|
|
48
|
+
const painted = [...padded]
|
|
49
|
+
.map((ch, x) => {
|
|
50
|
+
if (ch === ' ')
|
|
51
|
+
return ch;
|
|
52
|
+
const [r, g, b] = colorAt(width <= 1 ? 0 : x / (width - 1));
|
|
53
|
+
return `\x1b[38;2;${r};${g};${b}m${ch}`;
|
|
54
|
+
})
|
|
55
|
+
.join('');
|
|
56
|
+
return `${painted}\x1b[0m`;
|
|
57
|
+
})
|
|
58
|
+
.join('\n');
|
|
59
|
+
}
|
|
60
|
+
function dim(text, color) {
|
|
61
|
+
return color ? `\x1b[2m${text}\x1b[0m` : text;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Print the gradient banner to stderr, gated on ALL of:
|
|
65
|
+
*
|
|
66
|
+
* - human mode (never in JSON mode — an agent must never see this)
|
|
67
|
+
* - stderr being a TTY (never when piped, redirected, or captured)
|
|
68
|
+
* - `EKANOS_NO_BANNER` unset (any value at all suppresses it entirely)
|
|
69
|
+
*
|
|
70
|
+
* `NO_COLOR` does not suppress the banner — it renders the same art with zero
|
|
71
|
+
* escape codes, per the standard (https://no-color.org).
|
|
72
|
+
*
|
|
73
|
+
* Call sites stay a single line: all gating lives here, not at the call site.
|
|
74
|
+
*/
|
|
75
|
+
export function printBanner(ctx, kind) {
|
|
76
|
+
if (ctx.jsonMode)
|
|
77
|
+
return;
|
|
78
|
+
if (!ctx.stderrIsTTY)
|
|
79
|
+
return;
|
|
80
|
+
if (ctx.env.EKANOS_NO_BANNER !== undefined)
|
|
81
|
+
return;
|
|
82
|
+
const color = ctx.env.NO_COLOR === undefined;
|
|
83
|
+
const art = kind === 'full' ? FULL_ART : MINI_ART;
|
|
84
|
+
ctx.log(paintBanner(art, color));
|
|
85
|
+
if (kind === 'full') {
|
|
86
|
+
ctx.log(dim(TAGLINE, color));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CliContext } from './context';\n\n/**\n * The gradient ASCII banner shown on a first-time `ekanos init` / `login`.\n *\n * Every gate here matters more than the art: this is decoration on stderr,\n * and it must never leak into an agent's view of the CLI. In particular it\n * must NEVER print in JSON mode (stdout carries exactly one JSON object for\n * the whole process, and this is stderr noise an agent has no reason to\n * parse) and it must never print when stderr is not a TTY (piped into a log\n * file, captured by a harness, redirected in CI).\n */\n\ntype RGB = readonly [number, number, number];\n\n/** The website's brand gradient: teal → mauve → red, left to right. */\nconst GRADIENT_STOPS: readonly [RGB, RGB, RGB] = [\n [0x70, 0xa5, 0xab],\n [0x9f, 0x70, 0x77],\n [0xed, 0x1c, 0x24],\n];\n\n/** Option C (\"colossal\") — the full banner, shown on first init/login. */\nconst FULL_ART: readonly string[] = [\n ' .d88b. 888 888 8888b. 88888b. .d88b. .d8888b ',\n 'd8P Y8b 888 .88P \"88b 888 \"88b d88\"\"88b 88K ',\n '88888888 888888K .d888888 888 888 888 888 \"Y8888b. ',\n 'Y8b. 888 \"88b 888 888 888 888 Y88..88P X88 ',\n ' \"Y8888 888 888 \"Y888888 888 888 \"Y88P\" 88888P\\' ',\n];\n\n/** Option D (\"pagga\") — the mini-mark, shown on a repeat login. */\nconst MINI_ART: readonly string[] = [\n '░█▀▀░█░█░█▀█░█▀█░█▀█░█▀▀',\n '░█▀▀░█▀▄░█▀█░█░█░█░█░▀▀█',\n '░▀▀▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀',\n];\n\nconst TAGLINE = 'The SDK with a skill-full DX';\n\nfunction lerp(a: number, b: number, t: number): number {\n return Math.round(a + (b - a) * t);\n}\n\n/** The gradient color at position `t` (0..1) along the three-stop ramp. */\nfunction colorAt(t: number): RGB {\n const [start, mid, end] = GRADIENT_STOPS;\n const [from, to, localT]: [RGB, RGB, number] =\n t < 0.5 ? [start, mid, t * 2] : [mid, end, (t - 0.5) * 2];\n\n return [\n lerp(from[0], to[0], localT),\n lerp(from[1], to[1], localT),\n lerp(from[2], to[2], localT),\n ];\n}\n\n/**\n * Paint ASCII art lines with the gradient, one color per column, left to\n * right. `color: false` renders the raw art with zero escape codes — the\n * `NO_COLOR` and non-TTY paths. Pure and exported so the gradient math is\n * unit-testable independent of the printing gate in `printBanner`.\n */\nexport function paintBanner(lines: readonly string[], color: boolean): string {\n const width = Math.max(...lines.map((line) => line.length));\n\n return lines\n .map((line) => {\n const padded = line.padEnd(width);\n\n if (!color) return padded;\n\n const painted = [...padded]\n .map((ch, x) => {\n if (ch === ' ') return ch;\n const [r, g, b] = colorAt(width <= 1 ? 0 : x / (width - 1));\n return `\\x1b[38;2;${r};${g};${b}m${ch}`;\n })\n .join('');\n\n return `${painted}\\x1b[0m`;\n })\n .join('\\n');\n}\n\nfunction dim(text: string, color: boolean): string {\n return color ? `\\x1b[2m${text}\\x1b[0m` : text;\n}\n\n/**\n * Print the gradient banner to stderr, gated on ALL of:\n *\n * - human mode (never in JSON mode — an agent must never see this)\n * - stderr being a TTY (never when piped, redirected, or captured)\n * - `EKANOS_NO_BANNER` unset (any value at all suppresses it entirely)\n *\n * `NO_COLOR` does not suppress the banner — it renders the same art with zero\n * escape codes, per the standard (https://no-color.org).\n *\n * Call sites stay a single line: all gating lives here, not at the call site.\n */\nexport function printBanner(ctx: CliContext, kind: 'full' | 'mini'): void {\n if (ctx.jsonMode) return;\n if (!ctx.stderrIsTTY) return;\n if (ctx.env.EKANOS_NO_BANNER !== undefined) return;\n\n const color = ctx.env.NO_COLOR === undefined;\n const art = kind === 'full' ? FULL_ART : MINI_ART;\n\n ctx.log(paintBanner(art, color));\n\n if (kind === 'full') {\n ctx.log(dim(TAGLINE, color));\n }\n}\n"]}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -14,7 +14,14 @@ export interface SkippedFile {
|
|
|
14
14
|
}
|
|
15
15
|
export interface InitArgs {
|
|
16
16
|
slug?: string;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Explicit target directory. When omitted, init scaffolds into `./<slug>`
|
|
19
|
+
* under `cwd` — never into `cwd` itself. A real partner ran `init` in a
|
|
20
|
+
* populated directory and got six files sprayed into it; `--dir .` is the
|
|
21
|
+
* explicit opt-in for that.
|
|
22
|
+
*/
|
|
23
|
+
dir?: string;
|
|
24
|
+
cwd: string;
|
|
18
25
|
force: boolean;
|
|
19
26
|
/** Accepted for non-interactive ergonomics; init never prompts regardless. */
|
|
20
27
|
yes: boolean;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { printBanner } from '../banner.js';
|
|
3
4
|
import { usageError } from '../errors.js';
|
|
4
5
|
import { mergeManifest, serializeManifest, } from '../merge-manifest.js';
|
|
5
6
|
import { readTemplate, toDisplayName } from '../templates.js';
|
|
@@ -24,6 +25,14 @@ const FILES = [
|
|
|
24
25
|
template: 'src/integration.test.ts.tmpl',
|
|
25
26
|
dest: 'src/integration.test.ts',
|
|
26
27
|
},
|
|
28
|
+
// The agent-context files. The SDK's day-1 users are coding agents, so the
|
|
29
|
+
// scaffold ships the authoring contract alongside the code: AGENTS.md is
|
|
30
|
+
// the contract, CLAUDE.md points at it, and the Claude Code skill teaches
|
|
31
|
+
// the iterate-validate-publish loop. Version-matched to the CLI because
|
|
32
|
+
// they live in its templates.
|
|
33
|
+
{ template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },
|
|
34
|
+
{ template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },
|
|
35
|
+
{ template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },
|
|
27
36
|
];
|
|
28
37
|
/**
|
|
29
38
|
* `init` — instantiate a project from the bundled template: ekanos.json, a
|
|
@@ -43,7 +52,12 @@ export function runInit(ctx, args) {
|
|
|
43
52
|
throw usageError(`Invalid slug "${slug}".`, 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +
|
|
44
53
|
'e.g. "acme-crm".');
|
|
45
54
|
}
|
|
46
|
-
|
|
55
|
+
// Default to `./<slug>`, not the cwd: scaffolding six files into whatever
|
|
56
|
+
// directory the partner happened to be sitting in is how a real project got
|
|
57
|
+
// polluted. `--dir .` remains the explicit way to scaffold in place.
|
|
58
|
+
const targetDir = args.dir !== undefined
|
|
59
|
+
? path.resolve(args.cwd, args.dir)
|
|
60
|
+
: path.resolve(args.cwd, slug);
|
|
47
61
|
const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };
|
|
48
62
|
const created = [];
|
|
49
63
|
const skipped = [];
|
|
@@ -93,12 +107,14 @@ export function runInit(ctx, args) {
|
|
|
93
107
|
};
|
|
94
108
|
const summary = summarize({
|
|
95
109
|
slug,
|
|
110
|
+
dir: targetDir,
|
|
96
111
|
created,
|
|
97
112
|
skipped,
|
|
98
113
|
manifestAction: manifest.kind,
|
|
99
114
|
manifestAdded: manifest.added.length,
|
|
100
115
|
noteCount: manifest.notes.length,
|
|
101
116
|
});
|
|
117
|
+
printBanner(ctx, 'full');
|
|
102
118
|
return ctx.succeed(data, summary);
|
|
103
119
|
}
|
|
104
120
|
/**
|
|
@@ -171,8 +187,8 @@ function summarize(params) {
|
|
|
171
187
|
parts.push(`${params.noteCount} thing(s) need your attention`);
|
|
172
188
|
}
|
|
173
189
|
if (parts.length === 0) {
|
|
174
|
-
return `init: nothing to do — "${params.slug}" is already scaffolded.`;
|
|
190
|
+
return `init: nothing to do — "${params.slug}" is already scaffolded in ${params.dir}.`;
|
|
175
191
|
}
|
|
176
|
-
return `init: scaffolded "${params.slug}" (${parts.join(', ')}).`;
|
|
192
|
+
return `init: scaffolded "${params.slug}" into ${params.dir} (${parts.join(', ')}).`;
|
|
177
193
|
}
|
|
178
194
|
//# sourceMappingURL=init.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport {\n type Manifest,\n type ManifestNote,\n mergeManifest,\n serializeManifest,\n} from '../merge-manifest';\nimport { readTemplate, toDisplayName } from '../templates';\n\n/**\n * A file init did not write, and what the partner should do about it.\n *\n * Every ERROR path in this CLI carries an imperative hint; a skip did not, and\n * that asymmetry was the bug — `skipped package.json (already exists)` hid the\n * loss of eleven dependencies and the project's module type.\n */\nexport interface SkippedFile {\n file: string;\n reason: string;\n hint: string;\n}\n\nexport interface InitArgs {\n slug?: string;\n dir: string;\n force: boolean;\n /** Accepted for non-interactive ergonomics; init never prompts regardless. */\n yes: boolean;\n}\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/**\n * The files init writes: template basename → destination path.\n *\n * `package.json` is handled separately (see `writeManifest`) because it is the\n * one file a partner plausibly already has — `npm init -y` before `ekanos\n * init` is the ordinary way to start — and skipping it silently costs them\n * every dependency and script.\n */\nconst FILES: { template: string; dest: string }[] = [\n { template: 'ekanos.json.tmpl', dest: 'ekanos.json' },\n { template: 'gitignore.tmpl', dest: '.gitignore' },\n // Without this, the `typecheck` script below prints tsc's help and exits 1 —\n // and, worse, no type error in the scaffold ever surfaces.\n { template: 'tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'vitest.config.ts.tmpl', dest: 'vitest.config.ts' },\n { template: 'src/integration.ts.tmpl', dest: 'src/integration.ts' },\n {\n template: 'src/integration.test.ts.tmpl',\n dest: 'src/integration.test.ts',\n },\n];\n\n/**\n * `init` — instantiate a project from the bundled template: ekanos.json, a\n * working minimal defineIntegration, a test, a vitest config (with the\n * required SDK inline block), a .gitignore, and a package.json with real\n * published deps. Idempotent: an existing file is left untouched (reported as\n * skipped) unless --force is passed. Never prompts — a missing --slug fails\n * with a usage error naming the flag.\n */\nexport function runInit(ctx: CliContext, args: InitArgs): ExitCode {\n const slug = args.slug;\n if (!slug) {\n throw usageError(\n 'init requires a slug.',\n 'Pass it explicitly: \"ekanos init --slug <your-integration> --yes\". ' +\n 'The CLI never prompts, so the slug must be on the command line.',\n );\n }\n if (!SLUG_PATTERN.test(slug)) {\n throw usageError(\n `Invalid slug \"${slug}\".`,\n 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +\n 'e.g. \"acme-crm\".',\n );\n }\n\n const targetDir = path.resolve(args.dir);\n const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };\n\n const created: string[] = [];\n const skipped: SkippedFile[] = [];\n\n for (const { template, dest } of FILES) {\n const destPath = path.join(targetDir, dest);\n if (fs.existsSync(destPath) && !args.force) {\n skipped.push({\n file: dest,\n reason: 'it already exists',\n hint:\n `Compare it against the scaffold and merge anything you are ` +\n `missing, or re-run with --force to overwrite ${dest}.`,\n });\n continue;\n }\n\n fs.mkdirSync(path.dirname(destPath), { recursive: true });\n fs.writeFileSync(destPath, readTemplate(template, vars));\n created.push(dest);\n }\n\n const manifest = writeManifest(targetDir, vars, args.force);\n if (manifest.kind === 'created') created.push('package.json');\n\n for (const file of created) ctx.log(` created ${file}`);\n if (manifest.kind === 'merged') {\n ctx.log(\n ` merged package.json (added ${manifest.added.length} field${\n manifest.added.length === 1 ? '' : 's'\n })`,\n );\n }\n if (manifest.kind === 'unchanged') {\n ctx.log(' package.json already had everything the scaffold adds');\n }\n for (const skip of skipped) {\n ctx.log(` skipped ${skip.file} — ${skip.reason}. ${skip.hint}`);\n }\n for (const note of manifest.notes) {\n ctx.warn(` ${note.message} ${note.hint}`);\n }\n\n const data = {\n slug,\n dir: targetDir,\n created,\n skipped,\n manifest: {\n action: manifest.kind,\n added: manifest.added,\n kept: manifest.kept,\n notes: manifest.notes,\n },\n };\n\n const summary = summarize({\n slug,\n created,\n skipped,\n manifestAction: manifest.kind,\n manifestAdded: manifest.added.length,\n noteCount: manifest.notes.length,\n });\n\n return ctx.succeed(data, summary);\n}\n\ntype ManifestOutcome = {\n kind: 'created' | 'merged' | 'unchanged';\n added: string[];\n kept: string[];\n notes: ManifestNote[];\n};\n\n/**\n * Write or MERGE package.json.\n *\n * A partner who ran `npm init -y` first had a manifest with none of the\n * scaffold's dependencies and `\"type\": \"commonjs\"` under ESM sources, and the\n * old behaviour reported that as one word: \"skipped\". Merging is additive — a\n * version or script they already set is theirs and is never touched — so the\n * ordinary `npm init -y` path now lands somewhere that installs and runs.\n */\nfunction writeManifest(\n targetDir: string,\n vars: Readonly<Record<string, string>>,\n force: boolean,\n): ManifestOutcome {\n const destPath = path.join(targetDir, 'package.json');\n const rendered = readTemplate('package.json.tmpl', vars);\n\n if (!fs.existsSync(destPath) || force) {\n fs.mkdirSync(path.dirname(destPath), { recursive: true });\n fs.writeFileSync(destPath, rendered);\n return { kind: 'created', added: [], kept: [], notes: [] };\n }\n\n const template = JSON.parse(rendered) as Manifest;\n\n let existing: Manifest;\n try {\n existing = JSON.parse(fs.readFileSync(destPath, 'utf8')) as Manifest;\n } catch (error) {\n // Unparseable: do not guess, and do not clobber. Name the fix.\n return {\n kind: 'unchanged',\n added: [],\n kept: [],\n notes: [\n {\n field: '(root)',\n message: `package.json is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n hint:\n 'Fix the JSON syntax, then re-run \"ekanos init\" to have the ' +\n \"scaffold's dependencies and scripts merged in.\",\n },\n ],\n };\n }\n\n const result = mergeManifest(existing, template);\n if (!result.changed) {\n return {\n kind: 'unchanged',\n added: [],\n kept: result.kept,\n notes: result.notes,\n };\n }\n\n fs.writeFileSync(destPath, serializeManifest(result.manifest));\n return {\n kind: 'merged',\n added: result.added,\n kept: result.kept,\n notes: result.notes,\n };\n}\n\nfunction summarize(params: {\n slug: string;\n created: string[];\n skipped: SkippedFile[];\n manifestAction: ManifestOutcome['kind'];\n manifestAdded: number;\n noteCount: number;\n}): string {\n const parts: string[] = [];\n if (params.created.length > 0) {\n parts.push(\n `${params.created.length} file${params.created.length === 1 ? '' : 's'} written`,\n );\n }\n if (params.manifestAction === 'merged') {\n parts.push(`${params.manifestAdded} package.json field(s) merged`);\n }\n if (params.skipped.length > 0) {\n parts.push(`${params.skipped.length} skipped`);\n }\n if (params.noteCount > 0) {\n parts.push(`${params.noteCount} thing(s) need your attention`);\n }\n\n if (parts.length === 0) {\n return `init: nothing to do — \"${params.slug}\" is already scaffolded.`;\n }\n return `init: scaffolded \"${params.slug}\" (${parts.join(', ')}).`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { printBanner } from '../banner';\nimport type { CliContext } from '../context';\nimport { usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport {\n type Manifest,\n type ManifestNote,\n mergeManifest,\n serializeManifest,\n} from '../merge-manifest';\nimport { readTemplate, toDisplayName } from '../templates';\n\n/**\n * A file init did not write, and what the partner should do about it.\n *\n * Every ERROR path in this CLI carries an imperative hint; a skip did not, and\n * that asymmetry was the bug — `skipped package.json (already exists)` hid the\n * loss of eleven dependencies and the project's module type.\n */\nexport interface SkippedFile {\n file: string;\n reason: string;\n hint: string;\n}\n\nexport interface InitArgs {\n slug?: string;\n /**\n * Explicit target directory. When omitted, init scaffolds into `./<slug>`\n * under `cwd` — never into `cwd` itself. A real partner ran `init` in a\n * populated directory and got six files sprayed into it; `--dir .` is the\n * explicit opt-in for that.\n */\n dir?: string;\n cwd: string;\n force: boolean;\n /** Accepted for non-interactive ergonomics; init never prompts regardless. */\n yes: boolean;\n}\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/**\n * The files init writes: template basename → destination path.\n *\n * `package.json` is handled separately (see `writeManifest`) because it is the\n * one file a partner plausibly already has — `npm init -y` before `ekanos\n * init` is the ordinary way to start — and skipping it silently costs them\n * every dependency and script.\n */\nconst FILES: { template: string; dest: string }[] = [\n { template: 'ekanos.json.tmpl', dest: 'ekanos.json' },\n { template: 'gitignore.tmpl', dest: '.gitignore' },\n // Without this, the `typecheck` script below prints tsc's help and exits 1 —\n // and, worse, no type error in the scaffold ever surfaces.\n { template: 'tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'vitest.config.ts.tmpl', dest: 'vitest.config.ts' },\n { template: 'src/integration.ts.tmpl', dest: 'src/integration.ts' },\n {\n template: 'src/integration.test.ts.tmpl',\n dest: 'src/integration.test.ts',\n },\n // The agent-context files. The SDK's day-1 users are coding agents, so the\n // scaffold ships the authoring contract alongside the code: AGENTS.md is\n // the contract, CLAUDE.md points at it, and the Claude Code skill teaches\n // the iterate-validate-publish loop. Version-matched to the CLI because\n // they live in its templates.\n { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },\n { template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },\n { template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },\n];\n\n/**\n * `init` — instantiate a project from the bundled template: ekanos.json, a\n * working minimal defineIntegration, a test, a vitest config (with the\n * required SDK inline block), a .gitignore, and a package.json with real\n * published deps. Idempotent: an existing file is left untouched (reported as\n * skipped) unless --force is passed. Never prompts — a missing --slug fails\n * with a usage error naming the flag.\n */\nexport function runInit(ctx: CliContext, args: InitArgs): ExitCode {\n const slug = args.slug;\n if (!slug) {\n throw usageError(\n 'init requires a slug.',\n 'Pass it explicitly: \"ekanos init --slug <your-integration> --yes\". ' +\n 'The CLI never prompts, so the slug must be on the command line.',\n );\n }\n if (!SLUG_PATTERN.test(slug)) {\n throw usageError(\n `Invalid slug \"${slug}\".`,\n 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +\n 'e.g. \"acme-crm\".',\n );\n }\n\n // Default to `./<slug>`, not the cwd: scaffolding six files into whatever\n // directory the partner happened to be sitting in is how a real project got\n // polluted. `--dir .` remains the explicit way to scaffold in place.\n const targetDir =\n args.dir !== undefined\n ? path.resolve(args.cwd, args.dir)\n : path.resolve(args.cwd, slug);\n const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };\n\n const created: string[] = [];\n const skipped: SkippedFile[] = [];\n\n for (const { template, dest } of FILES) {\n const destPath = path.join(targetDir, dest);\n if (fs.existsSync(destPath) && !args.force) {\n skipped.push({\n file: dest,\n reason: 'it already exists',\n hint:\n `Compare it against the scaffold and merge anything you are ` +\n `missing, or re-run with --force to overwrite ${dest}.`,\n });\n continue;\n }\n\n fs.mkdirSync(path.dirname(destPath), { recursive: true });\n fs.writeFileSync(destPath, readTemplate(template, vars));\n created.push(dest);\n }\n\n const manifest = writeManifest(targetDir, vars, args.force);\n if (manifest.kind === 'created') created.push('package.json');\n\n for (const file of created) ctx.log(` created ${file}`);\n if (manifest.kind === 'merged') {\n ctx.log(\n ` merged package.json (added ${manifest.added.length} field${\n manifest.added.length === 1 ? '' : 's'\n })`,\n );\n }\n if (manifest.kind === 'unchanged') {\n ctx.log(' package.json already had everything the scaffold adds');\n }\n for (const skip of skipped) {\n ctx.log(` skipped ${skip.file} — ${skip.reason}. ${skip.hint}`);\n }\n for (const note of manifest.notes) {\n ctx.warn(` ${note.message} ${note.hint}`);\n }\n\n const data = {\n slug,\n dir: targetDir,\n created,\n skipped,\n manifest: {\n action: manifest.kind,\n added: manifest.added,\n kept: manifest.kept,\n notes: manifest.notes,\n },\n };\n\n const summary = summarize({\n slug,\n dir: targetDir,\n created,\n skipped,\n manifestAction: manifest.kind,\n manifestAdded: manifest.added.length,\n noteCount: manifest.notes.length,\n });\n\n printBanner(ctx, 'full');\n\n return ctx.succeed(data, summary);\n}\n\ntype ManifestOutcome = {\n kind: 'created' | 'merged' | 'unchanged';\n added: string[];\n kept: string[];\n notes: ManifestNote[];\n};\n\n/**\n * Write or MERGE package.json.\n *\n * A partner who ran `npm init -y` first had a manifest with none of the\n * scaffold's dependencies and `\"type\": \"commonjs\"` under ESM sources, and the\n * old behaviour reported that as one word: \"skipped\". Merging is additive — a\n * version or script they already set is theirs and is never touched — so the\n * ordinary `npm init -y` path now lands somewhere that installs and runs.\n */\nfunction writeManifest(\n targetDir: string,\n vars: Readonly<Record<string, string>>,\n force: boolean,\n): ManifestOutcome {\n const destPath = path.join(targetDir, 'package.json');\n const rendered = readTemplate('package.json.tmpl', vars);\n\n if (!fs.existsSync(destPath) || force) {\n fs.mkdirSync(path.dirname(destPath), { recursive: true });\n fs.writeFileSync(destPath, rendered);\n return { kind: 'created', added: [], kept: [], notes: [] };\n }\n\n const template = JSON.parse(rendered) as Manifest;\n\n let existing: Manifest;\n try {\n existing = JSON.parse(fs.readFileSync(destPath, 'utf8')) as Manifest;\n } catch (error) {\n // Unparseable: do not guess, and do not clobber. Name the fix.\n return {\n kind: 'unchanged',\n added: [],\n kept: [],\n notes: [\n {\n field: '(root)',\n message: `package.json is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n hint:\n 'Fix the JSON syntax, then re-run \"ekanos init\" to have the ' +\n \"scaffold's dependencies and scripts merged in.\",\n },\n ],\n };\n }\n\n const result = mergeManifest(existing, template);\n if (!result.changed) {\n return {\n kind: 'unchanged',\n added: [],\n kept: result.kept,\n notes: result.notes,\n };\n }\n\n fs.writeFileSync(destPath, serializeManifest(result.manifest));\n return {\n kind: 'merged',\n added: result.added,\n kept: result.kept,\n notes: result.notes,\n };\n}\n\nfunction summarize(params: {\n slug: string;\n dir: string;\n created: string[];\n skipped: SkippedFile[];\n manifestAction: ManifestOutcome['kind'];\n manifestAdded: number;\n noteCount: number;\n}): string {\n const parts: string[] = [];\n if (params.created.length > 0) {\n parts.push(\n `${params.created.length} file${params.created.length === 1 ? '' : 's'} written`,\n );\n }\n if (params.manifestAction === 'merged') {\n parts.push(`${params.manifestAdded} package.json field(s) merged`);\n }\n if (params.skipped.length > 0) {\n parts.push(`${params.skipped.length} skipped`);\n }\n if (params.noteCount > 0) {\n parts.push(`${params.noteCount} thing(s) need your attention`);\n }\n\n if (parts.length === 0) {\n return `init: nothing to do — \"${params.slug}\" is already scaffolded in ${params.dir}.`;\n }\n return `init: scaffolded \"${params.slug}\" into ${params.dir} (${parts.join(', ')}).`;\n}\n"]}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface LoginArgs {
|
|
|
4
4
|
host?: string;
|
|
5
5
|
/** Start a new browser login even if a working session is already stored. */
|
|
6
6
|
force: boolean;
|
|
7
|
+
/** Where a project's ekanos.json may sit — read-only here, never written. */
|
|
8
|
+
cwd: string;
|
|
7
9
|
env: Record<string, string | undefined>;
|
|
8
10
|
}
|
|
9
11
|
/**
|
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pollForDeviceTokens, startDeviceAuthorization, } from '../auth/device-flow.js';
|
|
2
2
|
import { createFusionSession, fetchCliAuthConfig } from '../auth/fusion-api.js';
|
|
3
3
|
import { resolveAuthEnvironment, resolveIdentity } from '../auth/session.js';
|
|
4
|
+
import { printBanner } from '../banner.js';
|
|
4
5
|
import { CliError } from '../errors.js';
|
|
5
6
|
import { ERROR_CODES } from '../exit-codes.js';
|
|
6
7
|
/**
|
|
@@ -16,12 +17,26 @@ import { ERROR_CODES } from '../exit-codes.js';
|
|
|
16
17
|
*/
|
|
17
18
|
export async function runLogin(ctx, args) {
|
|
18
19
|
var _a, _b, _c;
|
|
20
|
+
// No `projectDir`: login is the one verb that CREATES credentials, so its
|
|
21
|
+
// host must come from something the developer chose (--host, EKANOS_HOST)
|
|
22
|
+
// or already trusts (the sole stored login) — never from a committed
|
|
23
|
+
// ekanos.json, which an attacker can set in any cloned repo to point the
|
|
24
|
+
// device flow at their own tenant. Every other verb may read the project
|
|
25
|
+
// host because their bearer tokens are looked up per-host from the store:
|
|
26
|
+
// an unfamiliar host simply has no session to present.
|
|
19
27
|
const env = resolveAuthEnvironment(ctx, args.host, args.env);
|
|
28
|
+
// Whether this host had a session stored BEFORE this command touched
|
|
29
|
+
// anything — the sole signal for full banner (first login per host) vs
|
|
30
|
+
// mini-mark (repeat), independent of --force or which branch below
|
|
31
|
+
// actually produces the success.
|
|
32
|
+
const hadSession = env.store.read(env.host) !== null;
|
|
33
|
+
const bannerKind = hadSession ? 'mini' : 'full';
|
|
20
34
|
if (!args.force) {
|
|
21
35
|
const existing = env.store.read(env.host);
|
|
22
36
|
if (existing) {
|
|
23
37
|
const reusable = await tryReuse(env, existing);
|
|
24
38
|
if (reusable) {
|
|
39
|
+
printBanner(ctx, bannerKind);
|
|
25
40
|
return ctx.succeed({
|
|
26
41
|
host: env.host,
|
|
27
42
|
status: 'already-authenticated',
|
|
@@ -54,6 +69,7 @@ export async function runLogin(ctx, args) {
|
|
|
54
69
|
updatedAt: new Date().toISOString(),
|
|
55
70
|
};
|
|
56
71
|
env.store.write(env.host, stored);
|
|
72
|
+
printBanner(ctx, bannerKind);
|
|
57
73
|
return ctx.succeed({
|
|
58
74
|
host: env.host,
|
|
59
75
|
status: 'authenticated',
|