@ekanos/cli 0.1.1 → 0.1.2

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 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 # works in an empty dir, or merges into an existing package.json
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`, and a `package.json` with real published deps. Runs in an empty directory or on top of an existing project. |
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 --host https://your-fusion-host
262
- ekanos logout --host https://your-fusion-host
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
@@ -11,15 +11,33 @@ export interface AuthEnvironment {
11
11
  host: string;
12
12
  }
13
13
  /**
14
- * Resolve the Fusion host from `--host`, else `EKANOS_HOST`.
14
+ * Resolve the Fusion host, in this order:
15
15
  *
16
- * Deliberately no default. A baked-in default would be wrong for every partner
17
- * running against their own sandbox, and — worse — a CLI that silently picks a
18
- * host is a CLI that can send a developer's credentials somewhere they did not
19
- * intend.
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>): string;
22
- export declare function resolveAuthEnvironment(ctx: CliContext, hostFlag: string | undefined, env: Record<string, string | undefined>): AuthEnvironment;
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;
@@ -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 from `--host`, else `EKANOS_HOST`.
7
+ * Resolve the Fusion host, in this order:
6
8
  *
7
- * Deliberately no default. A baked-in default would be wrong for every partner
8
- * running against their own sandbox, and — worse — a CLI that silently picks a
9
- * host is a CLI that can send a developer's credentials somewhere they did not
10
- * intend.
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 (!raw || raw.trim().length === 0) {
15
- throw usageError('No Fusion host given.', 'Pass "--host https://<your-fusion-host>" or set EKANOS_HOST.');
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
- return normalizeHost(raw.trim());
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
- export function resolveAuthEnvironment(ctx, hostFlag, env) {
20
- const host = resolveHost(hostFlag, env);
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) {
@@ -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":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EACL,eAAe,EAEf,aAAa,EACb,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,aAAa,EACb,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAatB;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,WAAW,CACzB,GAAe,EACf,IAAwB,EACxB,GAAuC,EACvC,UAKI,EAAE;;IAEN,MAAM,GAAG,GAAG,IAAI,aAAJ,IAAI,cAAJ,IAAI,GAAI,GAAG,CAAC,WAAW,CAAC;IAEpC,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAErD,IAAI,QAAQ,EAAE,CAAC;YACb,oEAAoE;YACpE,kEAAkE;YAClE,8DAA8D;YAC9D,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;YAErC,oEAAoE;YACpE,mEAAmE;YACnE,gEAAgE;YAChE,qEAAqE;YACrE,kDAAkD;YAClD,GAAG,CAAC,GAAG,CACL,iBAAiB,IAAI,oCAAoC;gBACvD,0CAA0C,CAC7C,CAAC;YAEF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAA,MAAA,OAAO,CAAC,KAAK,0CAAE,KAAK,EAAE,mCAAI,EAAE,CAAC;IAE5C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;QAExB,GAAG,CAAC,GAAG,CACL,iBAAiB,IAAI,yCAAyC;YAC5D,wCAAwC,CAC3C,CAAC;QAEF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,UAAU,CACd,uBAAuB,EACvB,MAAM,CAAC,MAAM,GAAG,CAAC;QACf,CAAC,CAAC,wBAAwB,MAAM,CAAC,MAAM,WAAW,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;YACrE,wDAAwD;QAC5D,CAAC,CAAC,+DAA+D;YAC7D,iEAAiE;YACjE,cAAc,CACrB,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,UAAkB;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAExD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GACR,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAC3C,CAAC,CAAE,MAA6B,CAAC,IAAI;QACrC,CAAC,CAAC,SAAS,CAAC;IAEhB,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QACvD,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE;QACb,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAOD,MAAM,UAAU,sBAAsB,CACpC,GAAe,EACf,QAA4B,EAC5B,GAAuC,EACvC,UAA8B,EAAE;IAEhC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE;QAC3C,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK;KACN,CAAC,CAAC;IAEH,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAE/B,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,QAAQ,CACtB,GAAe,EACf,GAAuC;IAEvC,OAAO,IAAI,eAAe,CAAC;QACzB,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC;QAC5B,oEAAoE;QACpE,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC;KACrC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,GAAe,EAAE,IAAY;IACxD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAE1B,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO;IAErC,MAAM,QAAQ,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAEhE,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QAC3E,OAAO;IACT,CAAC;IAED,GAAG,CAAC,IAAI,CACN,WAAW,IAAI,sDAAsD;QACnE,gEAAgE,CACnE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,KAAsB,EACtB,IAAY;IAEZ,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEjC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,iBAAiB,CACrB,oBAAoB,IAAI,GAAG,EAC3B,4BAA4B,IAAI,IAAI,CACrC,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAoB,EACpB,OAAsB;IAEtB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAEpE,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAErE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,iBAAiB,CACrB,mBAAmB,GAAG,CAAC,IAAI,wBAAwB,EACnD,6BAA6B,GAAG,CAAC,IAAI,+BAA+B,GAAG,CAAC,IAAI,IAAI,CACjF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAoB,EACpB,OAAsB;IAEtB,MAAM,cAAc,GAAG,CAAC,OAAe,EAAE,EAAE;QACzC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,kCACnB,OAAO,KACV,iBAAiB,EAAE,OAAO,EAC1B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IACnC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAC1C,GAAG,CAAC,IAAI,EACR,OAAO,CAAC,iBAAiB,EACzB,EAAE,cAAc,EAAE,cAAc,EAAE,CACnC,CAAC;IAEF,MAAM,IAAI,GAAkB;QAC1B,iBAAiB,EAAE,SAAS,CAAC,iBAAiB;QAC9C,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,SAAS,EAAE,SAAS,CAAC,SAAS;QAC9B,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,KAAK,EAAE,SAAS,CAAC,KAAK;QACtB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IAEF,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAEhC,OAAO,IAAI,CAAC;AACd,CAAC","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"]}
@@ -14,7 +14,14 @@ export interface SkippedFile {
14
14
  }
15
15
  export interface InitArgs {
16
16
  slug?: string;
17
- dir: string;
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;
@@ -24,6 +24,14 @@ const FILES = [
24
24
  template: 'src/integration.test.ts.tmpl',
25
25
  dest: 'src/integration.test.ts',
26
26
  },
27
+ // The agent-context files. The SDK's day-1 users are coding agents, so the
28
+ // scaffold ships the authoring contract alongside the code: AGENTS.md is
29
+ // the contract, CLAUDE.md points at it, and the Claude Code skill teaches
30
+ // the iterate-validate-publish loop. Version-matched to the CLI because
31
+ // they live in its templates.
32
+ { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },
33
+ { template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },
34
+ { template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },
27
35
  ];
28
36
  /**
29
37
  * `init` — instantiate a project from the bundled template: ekanos.json, a
@@ -43,7 +51,12 @@ export function runInit(ctx, args) {
43
51
  throw usageError(`Invalid slug "${slug}".`, 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +
44
52
  'e.g. "acme-crm".');
45
53
  }
46
- const targetDir = path.resolve(args.dir);
54
+ // Default to `./<slug>`, not the cwd: scaffolding six files into whatever
55
+ // directory the partner happened to be sitting in is how a real project got
56
+ // polluted. `--dir .` remains the explicit way to scaffold in place.
57
+ const targetDir = args.dir !== undefined
58
+ ? path.resolve(args.cwd, args.dir)
59
+ : path.resolve(args.cwd, slug);
47
60
  const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };
48
61
  const created = [];
49
62
  const skipped = [];
@@ -93,6 +106,7 @@ export function runInit(ctx, args) {
93
106
  };
94
107
  const summary = summarize({
95
108
  slug,
109
+ dir: targetDir,
96
110
  created,
97
111
  skipped,
98
112
  manifestAction: manifest.kind,
@@ -171,8 +185,8 @@ function summarize(params) {
171
185
  parts.push(`${params.noteCount} thing(s) need your attention`);
172
186
  }
173
187
  if (parts.length === 0) {
174
- return `init: nothing to do — "${params.slug}" is already scaffolded.`;
188
+ return `init: nothing to do — "${params.slug}" is already scaffolded in ${params.dir}.`;
175
189
  }
176
- return `init: scaffolded "${params.slug}" (${parts.join(', ')}).`;
190
+ return `init: scaffolded "${params.slug}" into ${params.dir} (${parts.join(', ')}).`;
177
191
  }
178
192
  //# 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 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 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"]}
@@ -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
  /**
@@ -16,6 +16,13 @@ import { ERROR_CODES } from '../exit-codes.js';
16
16
  */
17
17
  export async function runLogin(ctx, args) {
18
18
  var _a, _b, _c;
19
+ // No `projectDir`: login is the one verb that CREATES credentials, so its
20
+ // host must come from something the developer chose (--host, EKANOS_HOST)
21
+ // or already trusts (the sole stored login) — never from a committed
22
+ // ekanos.json, which an attacker can set in any cloned repo to point the
23
+ // device flow at their own tenant. Every other verb may read the project
24
+ // host because their bearer tokens are looked up per-host from the store:
25
+ // an unfamiliar host simply has no session to present.
19
26
  const env = resolveAuthEnvironment(ctx, args.host, args.env);
20
27
  if (!args.force) {
21
28
  const existing = env.store.read(env.host);
@@ -1 +1 @@
1
- {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StoredSession } from '../auth/credential-store';\nimport {\n pollForDeviceTokens,\n startDeviceAuthorization,\n} from '../auth/device-flow';\nimport { createFusionSession, fetchCliAuthConfig } from '../auth/fusion-api';\nimport { resolveAuthEnvironment, resolveIdentity } from '../auth/session';\nimport type { CliContext } from '../context';\nimport { CliError } from '../errors';\nimport { ERROR_CODES } from '../exit-codes';\nimport type { ExitCode } from '../exit-codes';\n\nexport interface LoginArgs {\n host?: string;\n /** Start a new browser login even if a working session is already stored. */\n force: boolean;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `login` — authenticate this machine against a Fusion deployment via the\n * Auth0 device authorization flow.\n *\n * Idempotent: with a stored session that still authenticates, it reports that\n * and exits 0 without opening a browser. `--force` skips the check.\n *\n * The user code and verification URL go to STDERR. They have to be visible\n * even when stdout is a pipe into an agent, and stdout in this CLI carries\n * exactly one JSON object.\n */\nexport async function runLogin(\n ctx: CliContext,\n args: LoginArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env);\n\n if (!args.force) {\n const existing = env.store.read(env.host);\n\n if (existing) {\n const reusable = await tryReuse(env, existing);\n\n if (reusable) {\n return ctx.succeed(\n {\n host: env.host,\n status: 'already-authenticated',\n user: reusable.user,\n expiresAt: reusable.expiresAt,\n credentialsPath: env.store.filePath,\n },\n `login: already authenticated to ${env.host} as ${\n reusable.user.email ?? reusable.user.id\n }.`,\n );\n }\n\n ctx.log('The stored session has expired. Starting a new login…');\n }\n }\n\n const config = await fetchCliAuthConfig(env.host);\n const authorization = await startDeviceAuthorization(config);\n\n ctx.log('');\n ctx.log(\n ` Open: ${authorization.verificationUriComplete ?? authorization.verificationUri}`,\n );\n ctx.log(` Code: ${authorization.userCode}`);\n\n if (authorization.verificationUriComplete) {\n ctx.log(\n ` (without the pre-filled code: ${authorization.verificationUri})`,\n );\n }\n\n ctx.log('');\n ctx.log('Waiting for approval…');\n\n const tokens = await pollForDeviceTokens(config, authorization);\n const session = await createFusionSession(env.host, tokens.accessToken);\n\n const stored: StoredSession = {\n auth0RefreshToken: tokens.refreshToken,\n accessToken: session.accessToken,\n expiresAt: session.expiresAt,\n userId: session.userId,\n email: session.email,\n updatedAt: new Date().toISOString(),\n };\n\n env.store.write(env.host, stored);\n\n return ctx.succeed(\n {\n host: env.host,\n status: 'authenticated',\n user: { id: session.userId, email: session.email },\n expiresAt: session.expiresAt,\n credentialsPath: env.store.filePath,\n },\n `login: authenticated to ${env.host} as ${session.email ?? session.userId}.`,\n );\n}\n\n/**\n * Probe a stored session. Returns null when it cannot be revived, so `login`\n * falls through to a fresh device flow rather than failing — \"log in again\"\n * is the whole point of the command being run.\n *\n * Only auth failures are absorbed. A network or configuration error still\n * propagates: silently starting a browser login because the host was\n * unreachable would be the wrong recovery, and would hide the real fault.\n */\nasync function tryReuse(\n env: ReturnType<typeof resolveAuthEnvironment>,\n existing: StoredSession,\n): Promise<{\n user: { id: string; email: string | null };\n expiresAt: string;\n} | null> {\n try {\n const { identity, session } = await resolveIdentity(env, existing);\n\n return {\n user: { id: identity.id, email: identity.email },\n expiresAt: session.expiresAt,\n };\n } catch (error) {\n if (isAuthFailure(error)) {\n return null;\n }\n\n throw error;\n }\n}\n\nfunction isAuthFailure(error: unknown): boolean {\n return error instanceof CliError && error.code === ERROR_CODES.AUTH_REQUIRED;\n}\n"]}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StoredSession } from '../auth/credential-store';\nimport {\n pollForDeviceTokens,\n startDeviceAuthorization,\n} from '../auth/device-flow';\nimport { createFusionSession, fetchCliAuthConfig } from '../auth/fusion-api';\nimport { resolveAuthEnvironment, resolveIdentity } from '../auth/session';\nimport type { CliContext } from '../context';\nimport { CliError } from '../errors';\nimport { ERROR_CODES } from '../exit-codes';\nimport type { ExitCode } from '../exit-codes';\n\nexport interface LoginArgs {\n host?: string;\n /** Start a new browser login even if a working session is already stored. */\n force: boolean;\n /** Where a project's ekanos.json may sit — read-only here, never written. */\n cwd: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `login` — authenticate this machine against a Fusion deployment via the\n * Auth0 device authorization flow.\n *\n * Idempotent: with a stored session that still authenticates, it reports that\n * and exits 0 without opening a browser. `--force` skips the check.\n *\n * The user code and verification URL go to STDERR. They have to be visible\n * even when stdout is a pipe into an agent, and stdout in this CLI carries\n * exactly one JSON object.\n */\nexport async function runLogin(\n ctx: CliContext,\n args: LoginArgs,\n): Promise<ExitCode> {\n // No `projectDir`: login is the one verb that CREATES credentials, so its\n // host must come from something the developer chose (--host, EKANOS_HOST)\n // or already trusts (the sole stored login) — never from a committed\n // ekanos.json, which an attacker can set in any cloned repo to point the\n // device flow at their own tenant. Every other verb may read the project\n // host because their bearer tokens are looked up per-host from the store:\n // an unfamiliar host simply has no session to present.\n const env = resolveAuthEnvironment(ctx, args.host, args.env);\n\n if (!args.force) {\n const existing = env.store.read(env.host);\n\n if (existing) {\n const reusable = await tryReuse(env, existing);\n\n if (reusable) {\n return ctx.succeed(\n {\n host: env.host,\n status: 'already-authenticated',\n user: reusable.user,\n expiresAt: reusable.expiresAt,\n credentialsPath: env.store.filePath,\n },\n `login: already authenticated to ${env.host} as ${\n reusable.user.email ?? reusable.user.id\n }.`,\n );\n }\n\n ctx.log('The stored session has expired. Starting a new login…');\n }\n }\n\n const config = await fetchCliAuthConfig(env.host);\n const authorization = await startDeviceAuthorization(config);\n\n ctx.log('');\n ctx.log(\n ` Open: ${authorization.verificationUriComplete ?? authorization.verificationUri}`,\n );\n ctx.log(` Code: ${authorization.userCode}`);\n\n if (authorization.verificationUriComplete) {\n ctx.log(\n ` (without the pre-filled code: ${authorization.verificationUri})`,\n );\n }\n\n ctx.log('');\n ctx.log('Waiting for approval…');\n\n const tokens = await pollForDeviceTokens(config, authorization);\n const session = await createFusionSession(env.host, tokens.accessToken);\n\n const stored: StoredSession = {\n auth0RefreshToken: tokens.refreshToken,\n accessToken: session.accessToken,\n expiresAt: session.expiresAt,\n userId: session.userId,\n email: session.email,\n updatedAt: new Date().toISOString(),\n };\n\n env.store.write(env.host, stored);\n\n return ctx.succeed(\n {\n host: env.host,\n status: 'authenticated',\n user: { id: session.userId, email: session.email },\n expiresAt: session.expiresAt,\n credentialsPath: env.store.filePath,\n },\n `login: authenticated to ${env.host} as ${session.email ?? session.userId}.`,\n );\n}\n\n/**\n * Probe a stored session. Returns null when it cannot be revived, so `login`\n * falls through to a fresh device flow rather than failing — \"log in again\"\n * is the whole point of the command being run.\n *\n * Only auth failures are absorbed. A network or configuration error still\n * propagates: silently starting a browser login because the host was\n * unreachable would be the wrong recovery, and would hide the real fault.\n */\nasync function tryReuse(\n env: ReturnType<typeof resolveAuthEnvironment>,\n existing: StoredSession,\n): Promise<{\n user: { id: string; email: string | null };\n expiresAt: string;\n} | null> {\n try {\n const { identity, session } = await resolveIdentity(env, existing);\n\n return {\n user: { id: identity.id, email: identity.email },\n expiresAt: session.expiresAt,\n };\n } catch (error) {\n if (isAuthFailure(error)) {\n return null;\n }\n\n throw error;\n }\n}\n\nfunction isAuthFailure(error: unknown): boolean {\n return error instanceof CliError && error.code === ERROR_CODES.AUTH_REQUIRED;\n}\n"]}
@@ -4,6 +4,8 @@ export interface LogoutArgs {
4
4
  host?: string;
5
5
  /** Sign out of every host in the credentials file. */
6
6
  all: boolean;
7
+ /** Where a project's ekanos.json may sit, for its optional `host` field. */
8
+ cwd: string;
7
9
  env: Record<string, string | undefined>;
8
10
  }
9
11
  /**
@@ -16,7 +16,9 @@ export async function runLogout(ctx, args) {
16
16
  if (args.all) {
17
17
  return runLogoutAll(ctx, args);
18
18
  }
19
- const env = resolveAuthEnvironment(ctx, args.host, args.env);
19
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
20
+ projectDir: args.cwd,
21
+ });
20
22
  const session = env.store.read(env.host);
21
23
  if (!session) {
22
24
  return ctx.succeed({ host: env.host, cleared: false, revoked: false }, `logout: no stored session for ${env.host}.`);
@@ -1 +1 @@
1
- {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"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","sourcesContent":["import { revokeRefreshToken } from '../auth/device-flow';\nimport { fetchCliAuthConfig } from '../auth/fusion-api';\nimport { newStore, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\n\nexport interface LogoutArgs {\n host?: string;\n /** Sign out of every host in the credentials file. */\n all: boolean;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `logout` — revoke the stored refresh token at Auth0, then delete it locally.\n *\n * Idempotent: logging out twice exits 0. `cleared: false` in the envelope says\n * there was nothing to remove; it is information, not a failure.\n *\n * Revocation is attempted first but never blocks the local delete. A logout\n * that only forgets the token would leave a rotating credential live for weeks;\n * a logout that REFUSES because the network is down would leave the token on\n * disk, which is worse. So: try to revoke, always delete, report both.\n */\nexport async function runLogout(\n ctx: CliContext,\n args: LogoutArgs,\n): Promise<ExitCode> {\n if (args.all) {\n return runLogoutAll(ctx, args);\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env);\n const session = env.store.read(env.host);\n\n if (!session) {\n return ctx.succeed(\n { host: env.host, cleared: false, revoked: false },\n `logout: no stored session for ${env.host}.`,\n );\n }\n\n const revoked = await tryRevoke(ctx, env.host, session.auth0RefreshToken);\n\n env.store.clear(env.host);\n\n if (!revoked) {\n ctx.warn(\n `ekanos: the local credentials were removed, but the refresh token ` +\n `could not be revoked at Auth0. It stays valid until it expires — ` +\n `revoke the session in Auth0 if this machine is compromised.`,\n );\n }\n\n return ctx.succeed(\n { host: env.host, cleared: true, revoked },\n `logout: signed out of ${env.host}${revoked ? ' and revoked the session' : ''}.`,\n );\n}\n\n/**\n * Never throws. Every failure mode here — an unreachable host, a deployment\n * without the CLI routes, an Auth0 outage — must still end in the local token\n * being deleted.\n */\nasync function tryRevoke(\n ctx: CliContext,\n host: string,\n refreshToken: string,\n): Promise<boolean> {\n try {\n const config = await fetchCliAuthConfig(host);\n\n return await revokeRefreshToken(config, refreshToken);\n } catch (error) {\n // Diagnostic only — the message never carries the token, and the outcome\n // is reported in the envelope's `revoked` field either way.\n ctx.warn(\n `ekanos: could not reach ${host} to revoke the session ` +\n `(${error instanceof Error ? error.message : String(error)}).`,\n );\n\n return false;\n }\n}\n\n/**\n * `logout --all` — the \"I am leaving this machine\" verb. Every host is revoked\n * independently and the local file is emptied regardless, for the same reason\n * the single-host path does not let a failed revocation block the delete.\n */\nasync function runLogoutAll(\n ctx: CliContext,\n args: LogoutArgs,\n): Promise<ExitCode> {\n const store = newStore(ctx, args.env);\n const hosts = store.hosts();\n\n if (hosts.length === 0) {\n return ctx.succeed(\n { hosts: [], cleared: 0 },\n 'logout: no stored sessions.',\n );\n }\n\n const results: { host: string; revoked: boolean }[] = [];\n\n for (const host of hosts) {\n const session = store.read(host);\n\n results.push({\n host,\n revoked: session\n ? await tryRevoke(ctx, host, session.auth0RefreshToken)\n : false,\n });\n }\n\n const cleared = store.clearAll();\n\n return ctx.succeed(\n { hosts: results, cleared },\n `logout: signed out of ${cleared} host${cleared === 1 ? '' : 's'}.`,\n );\n}\n"]}
1
+ {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"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","sourcesContent":["import { revokeRefreshToken } from '../auth/device-flow';\nimport { fetchCliAuthConfig } from '../auth/fusion-api';\nimport { newStore, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\n\nexport interface LogoutArgs {\n host?: string;\n /** Sign out of every host in the credentials file. */\n all: boolean;\n /** Where a project's ekanos.json may sit, for its optional `host` field. */\n cwd: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `logout` — revoke the stored refresh token at Auth0, then delete it locally.\n *\n * Idempotent: logging out twice exits 0. `cleared: false` in the envelope says\n * there was nothing to remove; it is information, not a failure.\n *\n * Revocation is attempted first but never blocks the local delete. A logout\n * that only forgets the token would leave a rotating credential live for weeks;\n * a logout that REFUSES because the network is down would leave the token on\n * disk, which is worse. So: try to revoke, always delete, report both.\n */\nexport async function runLogout(\n ctx: CliContext,\n args: LogoutArgs,\n): Promise<ExitCode> {\n if (args.all) {\n return runLogoutAll(ctx, args);\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.cwd,\n });\n const session = env.store.read(env.host);\n\n if (!session) {\n return ctx.succeed(\n { host: env.host, cleared: false, revoked: false },\n `logout: no stored session for ${env.host}.`,\n );\n }\n\n const revoked = await tryRevoke(ctx, env.host, session.auth0RefreshToken);\n\n env.store.clear(env.host);\n\n if (!revoked) {\n ctx.warn(\n `ekanos: the local credentials were removed, but the refresh token ` +\n `could not be revoked at Auth0. It stays valid until it expires — ` +\n `revoke the session in Auth0 if this machine is compromised.`,\n );\n }\n\n return ctx.succeed(\n { host: env.host, cleared: true, revoked },\n `logout: signed out of ${env.host}${revoked ? ' and revoked the session' : ''}.`,\n );\n}\n\n/**\n * Never throws. Every failure mode here — an unreachable host, a deployment\n * without the CLI routes, an Auth0 outage — must still end in the local token\n * being deleted.\n */\nasync function tryRevoke(\n ctx: CliContext,\n host: string,\n refreshToken: string,\n): Promise<boolean> {\n try {\n const config = await fetchCliAuthConfig(host);\n\n return await revokeRefreshToken(config, refreshToken);\n } catch (error) {\n // Diagnostic only — the message never carries the token, and the outcome\n // is reported in the envelope's `revoked` field either way.\n ctx.warn(\n `ekanos: could not reach ${host} to revoke the session ` +\n `(${error instanceof Error ? error.message : String(error)}).`,\n );\n\n return false;\n }\n}\n\n/**\n * `logout --all` — the \"I am leaving this machine\" verb. Every host is revoked\n * independently and the local file is emptied regardless, for the same reason\n * the single-host path does not let a failed revocation block the delete.\n */\nasync function runLogoutAll(\n ctx: CliContext,\n args: LogoutArgs,\n): Promise<ExitCode> {\n const store = newStore(ctx, args.env);\n const hosts = store.hosts();\n\n if (hosts.length === 0) {\n return ctx.succeed(\n { hosts: [], cleared: 0 },\n 'logout: no stored sessions.',\n );\n }\n\n const results: { host: string; revoked: boolean }[] = [];\n\n for (const host of hosts) {\n const session = store.read(host);\n\n results.push({\n host,\n revoked: session\n ? await tryRevoke(ctx, host, session.auth0RefreshToken)\n : false,\n });\n }\n\n const cleared = store.clearAll();\n\n return ctx.succeed(\n { hosts: results, cleared },\n `logout: signed out of ${cleared} host${cleared === 1 ? '' : 's'}.`,\n );\n}\n"]}