@ekanos/cli 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/dist/agents-api.d.ts +11 -2
- package/dist/agents-api.js +39 -3
- package/dist/agents-api.js.map +1 -1
- package/dist/apps-api.d.ts +3 -2
- package/dist/apps-api.js +3 -7
- package/dist/apps-api.js.map +1 -1
- package/dist/auth/ekanos-api.d.ts +16 -2
- package/dist/auth/ekanos-api.js +101 -15
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/auth/host-trust.d.ts +46 -0
- package/dist/auth/host-trust.js +79 -0
- package/dist/auth/host-trust.js.map +1 -0
- package/dist/auth/session.d.ts +10 -1
- package/dist/auth/session.js +28 -28
- package/dist/auth/session.js.map +1 -1
- package/dist/commands/agents-edit.d.ts +6 -2
- package/dist/commands/agents-edit.js +57 -26
- package/dist/commands/agents-edit.js.map +1 -1
- package/dist/commands/agents.d.ts +13 -2
- package/dist/commands/agents.js +148 -36
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +15 -4
- package/dist/commands/apps.js +133 -51
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/catalog.js +7 -6
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/knowledge.d.ts +56 -0
- package/dist/commands/knowledge.js +456 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +30 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +9 -1
- package/dist/commands/logout.js +56 -7
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +3 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/reviewers.d.ts +2 -0
- package/dist/commands/reviewers.js +7 -3
- package/dist/commands/reviewers.js.map +1 -1
- package/dist/commands/routes.d.ts +2 -0
- package/dist/commands/routes.js +42 -9
- package/dist/commands/routes.js.map +1 -1
- package/dist/commands/runs.d.ts +13 -2
- package/dist/commands/runs.js +96 -15
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +21 -0
- package/dist/commands/scope-notice.js +35 -0
- package/dist/commands/scope-notice.js.map +1 -0
- package/dist/commands/skills.d.ts +7 -1
- package/dist/commands/skills.js +80 -24
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/sources.d.ts +4 -0
- package/dist/commands/sources.js +233 -5
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.js +5 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/team-agent-readiness.d.ts +59 -0
- package/dist/commands/team-agent-readiness.js +341 -0
- package/dist/commands/team-agent-readiness.js.map +1 -0
- package/dist/commands/team-agents-agents.d.ts +24 -3
- package/dist/commands/team-agents-agents.js +336 -35
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-mcp.js +32 -3
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.js +230 -54
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +3 -1
- package/dist/commands/team-agents.js +27 -7
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/teams-channel-install.d.ts +24 -0
- package/dist/commands/teams-channel-install.js +94 -0
- package/dist/commands/teams-channel-install.js.map +1 -0
- package/dist/commands/teams-connect.d.ts +66 -0
- package/dist/commands/teams-connect.js +397 -0
- package/dist/commands/teams-connect.js.map +1 -0
- package/dist/commands/teams-events.d.ts +12 -0
- package/dist/commands/teams-events.js +138 -0
- package/dist/commands/teams-events.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +16 -5
- package/dist/commands/teams-onboard.js +61 -36
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.d.ts +11 -0
- package/dist/commands/teams-overview.js +160 -0
- package/dist/commands/teams-overview.js.map +1 -0
- package/dist/commands/teams-scope.d.ts +10 -0
- package/dist/commands/teams-scope.js +109 -0
- package/dist/commands/teams-scope.js.map +1 -0
- package/dist/commands/teams.d.ts +64 -7
- package/dist/commands/teams.js +254 -60
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/upgrade.js +12 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +2 -2
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connections-api.js +1 -1
- package/dist/connections-api.js.map +1 -1
- package/dist/delegate.js +11 -4
- package/dist/delegate.js.map +1 -1
- package/dist/harness-scaffold.js +12 -0
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +724 -152
- package/dist/index.js.map +1 -1
- package/dist/knowledge-api.d.ts +175 -0
- package/dist/knowledge-api.js +169 -0
- package/dist/knowledge-api.js.map +1 -0
- package/dist/open-url.d.ts +16 -0
- package/dist/open-url.js +73 -0
- package/dist/open-url.js.map +1 -0
- package/dist/parse-argv.d.ts +2 -0
- package/dist/parse-argv.js.map +1 -1
- package/dist/publish-api.d.ts +2 -1
- package/dist/publish-api.js +4 -3
- package/dist/publish-api.js.map +1 -1
- package/dist/pull-write.d.ts +27 -0
- package/dist/pull-write.js +60 -0
- package/dist/pull-write.js.map +1 -0
- package/dist/routes-api.d.ts +3 -0
- package/dist/routes-api.js +9 -4
- package/dist/routes-api.js.map +1 -1
- package/dist/runs-api.d.ts +7 -1
- package/dist/runs-api.js +21 -6
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +2 -0
- package/dist/safe-fs.js +12 -6
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.js +69 -0
- package/dist/secret-file.js.map +1 -0
- package/dist/skills-api.d.ts +10 -2
- package/dist/skills-api.js +37 -2
- package/dist/skills-api.js.map +1 -1
- package/dist/source-products-api.d.ts +63 -0
- package/dist/source-products-api.js +123 -0
- package/dist/source-products-api.js.map +1 -0
- package/dist/source-target.js +2 -1
- package/dist/source-target.js.map +1 -1
- package/dist/sources-api.d.ts +2 -1
- package/dist/sources-api.js +3 -2
- package/dist/sources-api.js.map +1 -1
- package/dist/team-connect-api.d.ts +286 -0
- package/dist/team-connect-api.js +107 -0
- package/dist/team-connect-api.js.map +1 -0
- package/dist/teams-api.d.ts +42 -1
- package/dist/teams-api.js +98 -20
- package/dist/teams-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +9 -0
- package/dist/toolchain-api.js +17 -2
- package/dist/toolchain-api.js.map +1 -1
- package/dist/toolchain-resolve.d.ts +3 -0
- package/dist/toolchain-resolve.js +15 -1
- package/dist/toolchain-resolve.js.map +1 -1
- package/dist/toolchain.d.ts +6 -0
- package/dist/toolchain.js +19 -0
- package/dist/toolchain.js.map +1 -1
- package/dist/update-notice.js +2 -1
- package/dist/update-notice.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +6 -1
- package/templates/claude-skill.md.tmpl +5 -0
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +115 -16
- package/templates/skills/build-agent/references/agent-json.md +26 -8
- package/templates/skills/build-agent/references/app-setup.md +2 -1
- package/templates/skills/build-agent/references/rollout.md +27 -4
- package/templates/skills/build-agent/references/set-up-an-integration.md +74 -15
- package/templates/skills/build-agent/references/skills.md +20 -1
- package/templates/skills/build-agent/references/triggers.md +84 -20
- package/templates/skills/build-integration/references/widget-design.md +3 -3
- package/templates/skills/ekanos-platform/SKILL.md +70 -17
- package/templates/skills/polish-widgets/references/patterns.md +2 -2
package/dist/commands/login.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { pollForDeviceTokens, startDeviceAuthorization, } from '../auth/device-flow.js';
|
|
2
2
|
import { createEkanosSession, fetchCliAuthConfig } from '../auth/ekanos-api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { isKnownHost } from '../auth/host-trust.js';
|
|
4
|
+
import { resolveAuthEnvironment, resolveIdentity, } from '../auth/session.js';
|
|
4
5
|
import { printBanner } from '../banner.js';
|
|
5
6
|
import { claudeDocsRow, offerDocsToClaude } from '../claude-docs.js';
|
|
6
|
-
import { CliError } from '../errors.js';
|
|
7
|
+
import { CliError, invalidStateError, validationError } from '../errors.js';
|
|
7
8
|
import { ERROR_CODES } from '../exit-codes.js';
|
|
8
9
|
import { shortHost } from '../ui.js';
|
|
9
10
|
import { canWizard, wizard } from '../wizard.js';
|
|
@@ -69,6 +70,7 @@ export async function runLogin(ctx, args) {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
const config = await fetchCliAuthConfig(env.host);
|
|
73
|
+
await confirmNewHost(ctx, args, env, config.auth0Domain);
|
|
72
74
|
const authorization = await startDeviceAuthorization(config);
|
|
73
75
|
const openUrl = (_b = authorization.verificationUriComplete) !== null && _b !== void 0 ? _b : authorization.verificationUri;
|
|
74
76
|
if (useWizard) {
|
|
@@ -133,6 +135,32 @@ export async function runLogin(ctx, args) {
|
|
|
133
135
|
],
|
|
134
136
|
}));
|
|
135
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* A login sends the Auth0 access token, and later the refresh token, to the
|
|
140
|
+
* host. A hostile host can return Ekanos's real Auth0 domain and client id,
|
|
141
|
+
* so the browser step looks genuine and proves nothing about the host. So a
|
|
142
|
+
* host that is neither stored nor a known Ekanos host is shown, with the
|
|
143
|
+
* Auth0 domain it named, and asked about; with nobody to ask (JSON mode, an
|
|
144
|
+
* agent, no TTY) it needs `--trust-host`.
|
|
145
|
+
*/
|
|
146
|
+
async function confirmNewHost(ctx, args, env, auth0Domain) {
|
|
147
|
+
var _a, _b;
|
|
148
|
+
if (args.trustHost || isKnownHost(env.store, env.host))
|
|
149
|
+
return;
|
|
150
|
+
const canPrompt = !ctx.jsonMode && ((_a = args.stdinIsTTY) !== null && _a !== void 0 ? _a : process.stdin.isTTY === true);
|
|
151
|
+
if (!canPrompt) {
|
|
152
|
+
throw invalidStateError(`${env.host} is not a host you have logged in to before, and it ` +
|
|
153
|
+
`signs in through ${auth0Domain}. A login sends your credentials there.`, `Check with the person you work for that ${env.host} is their Ekanos ` +
|
|
154
|
+
`deployment. If it is, re-run: ekanos login --host ${env.host} --trust-host`);
|
|
155
|
+
}
|
|
156
|
+
ctx.log(` Host: ${env.host}`);
|
|
157
|
+
ctx.log(` Sign-in: ${auth0Domain}`);
|
|
158
|
+
const sure = await ((_b = args.confirmHost) !== null && _b !== void 0 ? _b : promptYesNo)(`You have never logged in to ${env.host}. Send your credentials there? [y/N]`);
|
|
159
|
+
if (!sure) {
|
|
160
|
+
throw validationError('Nothing was changed.', `Check the host is the Ekanos deployment you meant, then run ` +
|
|
161
|
+
`"ekanos login --host <url>" again.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
136
164
|
/**
|
|
137
165
|
* Probe a stored session. Returns null when it cannot be revived, so `login`
|
|
138
166
|
* falls through to a fresh device flow rather than failing — "log in again"
|
|
@@ -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 { createEkanosSession, fetchCliAuthConfig } from '../auth/ekanos-api';\nimport { resolveAuthEnvironment, resolveIdentity } from '../auth/session';\nimport { printBanner } from '../banner';\nimport { claudeDocsRow, offerDocsToClaude } from '../claude-docs';\nimport type { CliContext } from '../context';\nimport { CliError } from '../errors';\nimport { ERROR_CODES } from '../exit-codes';\nimport type { ExitCode } from '../exit-codes';\nimport { shortHost } from '../ui';\nimport { canWizard, wizard } from '../wizard';\nimport { promptYesNo } from './publish';\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 * Test-only override for the clack wizard gate (`canWizard` in\n * `wizard.ts`) — never set from real CLI wiring. See `RunOptions` in\n * `index.ts`.\n */\n stdinIsTTY?: boolean;\n /** `--claude` adds the Ekanos docs to Claude Code without asking; `--no-claude` skips the offer. */\n claude?: boolean;\n /** Test seam for the Claude Code offer's question. */\n confirm?: (question: string) => Promise<boolean>;\n}\n\n/**\n * `login` — authenticate this machine against an Ekanos 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 *\n * In human mode at a real TTY (`canWizard`), this runs behind a clack\n * `intro`/`outro` flow with a spinner over the approval wait — see\n * `wizard.ts`. The device code and URL themselves stay plain, copy-pasteable\n * text (a clack `note`, not a prompt) either way: a partner needs to select\n * and open them, not interact with them through clack. JSON mode and human\n * non-TTY mode are byte-for-byte what they were before this existed.\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 const useWizard = canWizard(ctx, { stdinIsTTY: args.stdinIsTTY });\n if (useWizard) wizard.intro('ekanos login');\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 printBanner(ctx);\n\n const email = reusable.user.email ?? reusable.user.id;\n if (useWizard) {\n wizard.outro(\n `Already signed in as ${email} on ${shortHost(env.host)}.`,\n );\n }\n\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 useWizard\n ? ''\n : ctx.ui.result({\n title: 'Already signed in',\n rows: [\n ['Account', email],\n ['Host', shortHost(env.host)],\n ],\n }),\n );\n }\n\n ctx.log(\n ctx.ui.dim('The stored session has expired. Starting a new login…'),\n );\n }\n }\n\n const config = await fetchCliAuthConfig(env.host);\n const authorization = await startDeviceAuthorization(config);\n const openUrl =\n authorization.verificationUriComplete ?? authorization.verificationUri;\n\n if (useWizard) {\n const noteLines = [`Open: ${openUrl}`, `Code: ${authorization.userCode}`];\n if (authorization.verificationUriComplete) {\n noteLines.push(\n `(without the pre-filled code: ${authorization.verificationUri})`,\n );\n }\n wizard.note(noteLines.join('\\n'), 'Device login');\n } else {\n ctx.log('');\n ctx.log(` Open: ${openUrl}`);\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\n const spinner = useWizard ? wizard.spinner() : null;\n spinner?.start('Waiting for approval…');\n\n const tokens = await pollForDeviceTokens(config, authorization);\n const session = await createEkanosSession(env.host, tokens.accessToken);\n\n spinner?.stop(`✓ authenticated as ${session.email ?? session.userId}`);\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 // A first sign-in is when a developer is setting up: offer the docs to\n // Claude Code once, so it can look Ekanos up instead of guessing.\n const claudeDocs = await offerDocsToClaude({\n choice: args.claude,\n canPrompt:\n !ctx.jsonMode && (args.stdinIsTTY ?? process.stdin.isTTY === true),\n confirm: args.confirm ?? promptYesNo,\n env: args.env,\n });\n const claudeRow = claudeDocsRow(claudeDocs);\n\n printBanner(ctx);\n\n if (useWizard) {\n wizard.outro(\n `Signed in as ${session.email ?? session.userId} on ${shortHost(env.host)}.`,\n );\n }\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 claudeDocs,\n },\n useWizard\n ? ''\n : ctx.ui.result({\n title: 'Signed in',\n rows: [\n ['Account', session.email ?? session.userId],\n ['Host', shortHost(env.host)],\n ...(claudeRow ? [claudeRow] : []),\n ],\n }),\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 { createEkanosSession, fetchCliAuthConfig } from '../auth/ekanos-api';\nimport { isKnownHost } from '../auth/host-trust';\nimport {\n type AuthEnvironment,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport { printBanner } from '../banner';\nimport { claudeDocsRow, offerDocsToClaude } from '../claude-docs';\nimport type { CliContext } from '../context';\nimport { CliError, invalidStateError, validationError } from '../errors';\nimport { ERROR_CODES } from '../exit-codes';\nimport type { ExitCode } from '../exit-codes';\nimport { shortHost } from '../ui';\nimport { canWizard, wizard } from '../wizard';\nimport { promptYesNo } from './publish';\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 * Test-only override for the clack wizard gate (`canWizard` in\n * `wizard.ts`) — never set from real CLI wiring. See `RunOptions` in\n * `index.ts`.\n */\n stdinIsTTY?: boolean;\n /** `--claude` adds the Ekanos docs to Claude Code without asking; `--no-claude` skips the offer. */\n claude?: boolean;\n /** Test seam for the Claude Code offer's question. */\n confirm?: (question: string) => Promise<boolean>;\n /** `--trust-host`: sign in to a host you have never used without the question. */\n trustHost?: boolean;\n /** Test seam for the new-host question. */\n confirmHost?: (question: string) => Promise<boolean>;\n}\n\n/**\n * `login` — authenticate this machine against an Ekanos 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 *\n * In human mode at a real TTY (`canWizard`), this runs behind a clack\n * `intro`/`outro` flow with a spinner over the approval wait — see\n * `wizard.ts`. The device code and URL themselves stay plain, copy-pasteable\n * text (a clack `note`, not a prompt) either way: a partner needs to select\n * and open them, not interact with them through clack. JSON mode and human\n * non-TTY mode are byte-for-byte what they were before this existed.\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 const useWizard = canWizard(ctx, { stdinIsTTY: args.stdinIsTTY });\n if (useWizard) wizard.intro('ekanos login');\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 printBanner(ctx);\n\n const email = reusable.user.email ?? reusable.user.id;\n if (useWizard) {\n wizard.outro(\n `Already signed in as ${email} on ${shortHost(env.host)}.`,\n );\n }\n\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 useWizard\n ? ''\n : ctx.ui.result({\n title: 'Already signed in',\n rows: [\n ['Account', email],\n ['Host', shortHost(env.host)],\n ],\n }),\n );\n }\n\n ctx.log(\n ctx.ui.dim('The stored session has expired. Starting a new login…'),\n );\n }\n }\n\n const config = await fetchCliAuthConfig(env.host);\n await confirmNewHost(ctx, args, env, config.auth0Domain);\n const authorization = await startDeviceAuthorization(config);\n const openUrl =\n authorization.verificationUriComplete ?? authorization.verificationUri;\n\n if (useWizard) {\n const noteLines = [`Open: ${openUrl}`, `Code: ${authorization.userCode}`];\n if (authorization.verificationUriComplete) {\n noteLines.push(\n `(without the pre-filled code: ${authorization.verificationUri})`,\n );\n }\n wizard.note(noteLines.join('\\n'), 'Device login');\n } else {\n ctx.log('');\n ctx.log(` Open: ${openUrl}`);\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\n const spinner = useWizard ? wizard.spinner() : null;\n spinner?.start('Waiting for approval…');\n\n const tokens = await pollForDeviceTokens(config, authorization);\n const session = await createEkanosSession(env.host, tokens.accessToken);\n\n spinner?.stop(`✓ authenticated as ${session.email ?? session.userId}`);\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 // A first sign-in is when a developer is setting up: offer the docs to\n // Claude Code once, so it can look Ekanos up instead of guessing.\n const claudeDocs = await offerDocsToClaude({\n choice: args.claude,\n canPrompt:\n !ctx.jsonMode && (args.stdinIsTTY ?? process.stdin.isTTY === true),\n confirm: args.confirm ?? promptYesNo,\n env: args.env,\n });\n const claudeRow = claudeDocsRow(claudeDocs);\n\n printBanner(ctx);\n\n if (useWizard) {\n wizard.outro(\n `Signed in as ${session.email ?? session.userId} on ${shortHost(env.host)}.`,\n );\n }\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 claudeDocs,\n },\n useWizard\n ? ''\n : ctx.ui.result({\n title: 'Signed in',\n rows: [\n ['Account', session.email ?? session.userId],\n ['Host', shortHost(env.host)],\n ...(claudeRow ? [claudeRow] : []),\n ],\n }),\n );\n}\n\n/**\n * A login sends the Auth0 access token, and later the refresh token, to the\n * host. A hostile host can return Ekanos's real Auth0 domain and client id,\n * so the browser step looks genuine and proves nothing about the host. So a\n * host that is neither stored nor a known Ekanos host is shown, with the\n * Auth0 domain it named, and asked about; with nobody to ask (JSON mode, an\n * agent, no TTY) it needs `--trust-host`.\n */\nasync function confirmNewHost(\n ctx: CliContext,\n args: LoginArgs,\n env: AuthEnvironment,\n auth0Domain: string,\n): Promise<void> {\n if (args.trustHost || isKnownHost(env.store, env.host)) return;\n\n const canPrompt =\n !ctx.jsonMode && (args.stdinIsTTY ?? process.stdin.isTTY === true);\n\n if (!canPrompt) {\n throw invalidStateError(\n `${env.host} is not a host you have logged in to before, and it ` +\n `signs in through ${auth0Domain}. A login sends your credentials there.`,\n `Check with the person you work for that ${env.host} is their Ekanos ` +\n `deployment. If it is, re-run: ekanos login --host ${env.host} --trust-host`,\n );\n }\n\n ctx.log(` Host: ${env.host}`);\n ctx.log(` Sign-in: ${auth0Domain}`);\n\n const sure = await (args.confirmHost ?? promptYesNo)(\n `You have never logged in to ${env.host}. Send your credentials there? [y/N]`,\n );\n\n if (!sure) {\n throw validationError(\n 'Nothing was changed.',\n `Check the host is the Ekanos deployment you meant, then run ` +\n `\"ekanos login --host <url>\" again.`,\n );\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"]}
|
|
@@ -9,7 +9,15 @@ export interface LogoutArgs {
|
|
|
9
9
|
env: Record<string, string | undefined>;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* `logout` —
|
|
12
|
+
* `logout` — end the Ekanos session on the server, revoke the stored refresh
|
|
13
|
+
* token at Auth0, then delete both locally.
|
|
14
|
+
*
|
|
15
|
+
* Two revocations, because they stop different things. The Ekanos one
|
|
16
|
+
* (`/api/auth/cli-logout`, `sessionRevoked` in the envelope) marks the
|
|
17
|
+
* server-side session revoked, so no copy of these credentials can be
|
|
18
|
+
* refreshed again — even one already copied off this machine. The Auth0 one
|
|
19
|
+
* (`revoked`) kills the refresh token itself. Neither invalidates the last
|
|
20
|
+
* minted access token before its expiry (an hour at most).
|
|
13
21
|
*
|
|
14
22
|
* Idempotent: logging out twice exits 0. `cleared: false` in the envelope says
|
|
15
23
|
* there was nothing to remove; it is information, not a failure.
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { revokeRefreshToken } from '../auth/device-flow.js';
|
|
2
|
-
import { fetchCliAuthConfig } from '../auth/ekanos-api.js';
|
|
2
|
+
import { fetchCliAuthConfig, revokeEkanosSession } from '../auth/ekanos-api.js';
|
|
3
|
+
import { isPlaintextRemoteHost } from '../auth/host-trust.js';
|
|
3
4
|
import { newStore, resolveAuthEnvironment } from '../auth/session.js';
|
|
4
5
|
import { pluralize, shortHost } from '../ui.js';
|
|
5
6
|
/**
|
|
6
|
-
* `logout` —
|
|
7
|
+
* `logout` — end the Ekanos session on the server, revoke the stored refresh
|
|
8
|
+
* token at Auth0, then delete both locally.
|
|
9
|
+
*
|
|
10
|
+
* Two revocations, because they stop different things. The Ekanos one
|
|
11
|
+
* (`/api/auth/cli-logout`, `sessionRevoked` in the envelope) marks the
|
|
12
|
+
* server-side session revoked, so no copy of these credentials can be
|
|
13
|
+
* refreshed again — even one already copied off this machine. The Auth0 one
|
|
14
|
+
* (`revoked`) kills the refresh token itself. Neither invalidates the last
|
|
15
|
+
* minted access token before its expiry (an hour at most).
|
|
7
16
|
*
|
|
8
17
|
* Idempotent: logging out twice exits 0. `cleared: false` in the envelope says
|
|
9
18
|
* there was nothing to remove; it is information, not a failure.
|
|
@@ -19,6 +28,7 @@ export async function runLogout(ctx, args) {
|
|
|
19
28
|
}
|
|
20
29
|
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
21
30
|
projectDir: args.cwd,
|
|
31
|
+
allowPlaintext: true,
|
|
22
32
|
});
|
|
23
33
|
const session = env.store.read(env.host);
|
|
24
34
|
if (!session) {
|
|
@@ -29,22 +39,58 @@ export async function runLogout(ctx, args) {
|
|
|
29
39
|
notes: ['Nothing was stored for this host, so nothing was removed.'],
|
|
30
40
|
}));
|
|
31
41
|
}
|
|
42
|
+
const sessionRevoked = await tryEndSession(ctx, env.host, session.accessToken);
|
|
32
43
|
const revoked = await tryRevoke(ctx, env.host, session.auth0RefreshToken);
|
|
33
44
|
env.store.clear(env.host);
|
|
45
|
+
if (!sessionRevoked) {
|
|
46
|
+
ctx.warn(`ekanos: the Ekanos session could not be ended on ${env.host}; it ` +
|
|
47
|
+
`stays refreshable until it expires if these credentials were copied.`);
|
|
48
|
+
}
|
|
34
49
|
if (!revoked) {
|
|
35
50
|
ctx.warn(`ekanos: the local credentials were removed, but the refresh token ` +
|
|
36
51
|
`could not be revoked at Auth0. It stays valid until it expires — ` +
|
|
37
52
|
`revoke the session in Auth0 if this machine is compromised.`);
|
|
38
53
|
}
|
|
39
|
-
return ctx.succeed({ host: env.host, cleared: true, revoked }, ctx.ui.result({
|
|
54
|
+
return ctx.succeed({ host: env.host, cleared: true, revoked, sessionRevoked }, ctx.ui.result({
|
|
40
55
|
title: 'Signed out',
|
|
41
|
-
tone: revoked ? 'ok' : 'warn',
|
|
56
|
+
tone: revoked && sessionRevoked ? 'ok' : 'warn',
|
|
42
57
|
rows: [
|
|
43
58
|
['Host', shortHost(env.host)],
|
|
44
|
-
['Session', revoked
|
|
59
|
+
['Session', describeRevocation(revoked, sessionRevoked)],
|
|
45
60
|
],
|
|
46
61
|
}));
|
|
47
62
|
}
|
|
63
|
+
function describeRevocation(revoked, sessionRevoked) {
|
|
64
|
+
if (revoked && sessionRevoked)
|
|
65
|
+
return 'ended on the server and at Auth0';
|
|
66
|
+
if (sessionRevoked)
|
|
67
|
+
return 'ended on the server; Auth0 revoke failed';
|
|
68
|
+
if (revoked)
|
|
69
|
+
return 'revoked at Auth0';
|
|
70
|
+
return 'removed locally only';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* End the server-side Ekanos session. Never throws, for the same reason as
|
|
74
|
+
* `tryRevoke`: nothing may stand between `logout` and the local delete.
|
|
75
|
+
*
|
|
76
|
+
* Skipped for a legacy remote `http://` host: ending the session means
|
|
77
|
+
* sending the access token, and the CLI never puts a credential on a
|
|
78
|
+
* plaintext wire. `logout` still clears such a host; it just cannot end the
|
|
79
|
+
* server session, and says so.
|
|
80
|
+
*/
|
|
81
|
+
async function tryEndSession(ctx, host, accessToken) {
|
|
82
|
+
if (isPlaintextRemoteHost(host)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return await revokeEkanosSession(host, accessToken);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
ctx.warn(`ekanos: could not reach ${host} to end the session ` +
|
|
90
|
+
`(${error instanceof Error ? error.message : String(error)}).`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
48
94
|
/**
|
|
49
95
|
* Never throws. Every failure mode here — an unreachable host, a deployment
|
|
50
96
|
* without the CLI routes, an Auth0 outage — must still end in the local token
|
|
@@ -83,6 +129,9 @@ async function runLogoutAll(ctx, args) {
|
|
|
83
129
|
const session = store.read(host);
|
|
84
130
|
results.push({
|
|
85
131
|
host,
|
|
132
|
+
sessionRevoked: session
|
|
133
|
+
? await tryEndSession(ctx, host, session.accessToken)
|
|
134
|
+
: false,
|
|
86
135
|
revoked: session
|
|
87
136
|
? await tryRevoke(ctx, host, session.auth0RefreshToken)
|
|
88
137
|
: false,
|
|
@@ -91,11 +140,11 @@ async function runLogoutAll(ctx, args) {
|
|
|
91
140
|
const cleared = store.clearAll();
|
|
92
141
|
return ctx.succeed({ hosts: results, cleared }, ctx.ui.result({
|
|
93
142
|
title: `Signed out of ${pluralize(cleared, 'host')}`,
|
|
94
|
-
tone: results.every((r) => r.revoked) ? 'ok' : 'warn',
|
|
143
|
+
tone: results.every((r) => r.revoked && r.sessionRevoked) ? 'ok' : 'warn',
|
|
95
144
|
rows: [
|
|
96
145
|
[
|
|
97
146
|
'Hosts',
|
|
98
|
-
results.map((r) => `${shortHost(r.host)} · ${r.revoked
|
|
147
|
+
results.map((r) => `${shortHost(r.host)} · ${describeRevocation(r.revoked, r.sessionRevoked)}`),
|
|
99
148
|
],
|
|
100
149
|
],
|
|
101
150
|
}));
|
|
@@ -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/ekanos-api';\nimport { newStore, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { pluralize, shortHost } from '../ui';\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 ctx.ui.result({\n title: 'Not signed in',\n tone: 'warn',\n rows: [['Host', shortHost(env.host)]],\n notes: ['Nothing was stored for this host, so nothing was removed.'],\n }),\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 ctx.ui.result({\n title: 'Signed out',\n tone: revoked ? 'ok' : 'warn',\n rows: [\n ['Host', shortHost(env.host)],\n ['Session', revoked ? 'revoked at Auth0' : 'removed locally only'],\n ],\n }),\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 ctx.ui.result({\n title: 'Not signed in',\n tone: 'warn',\n notes: ['No stored sessions on this machine.'],\n }),\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 ctx.ui.result({\n title: `Signed out of ${pluralize(cleared, 'host')}`,\n tone: results.every((r) => r.revoked) ? 'ok' : 'warn',\n rows: [\n [\n 'Hosts',\n results.map(\n (r) =>\n `${shortHost(r.host)} · ${r.revoked ? 'revoked' : 'removed locally only'}`,\n ),\n ],\n ],\n }),\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, revokeEkanosSession } from '../auth/ekanos-api';\nimport { isPlaintextRemoteHost } from '../auth/host-trust';\nimport { newStore, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { pluralize, shortHost } from '../ui';\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` — end the Ekanos session on the server, revoke the stored refresh\n * token at Auth0, then delete both locally.\n *\n * Two revocations, because they stop different things. The Ekanos one\n * (`/api/auth/cli-logout`, `sessionRevoked` in the envelope) marks the\n * server-side session revoked, so no copy of these credentials can be\n * refreshed again — even one already copied off this machine. The Auth0 one\n * (`revoked`) kills the refresh token itself. Neither invalidates the last\n * minted access token before its expiry (an hour at most).\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 allowPlaintext: true,\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 ctx.ui.result({\n title: 'Not signed in',\n tone: 'warn',\n rows: [['Host', shortHost(env.host)]],\n notes: ['Nothing was stored for this host, so nothing was removed.'],\n }),\n );\n }\n\n const sessionRevoked = await tryEndSession(\n ctx,\n env.host,\n session.accessToken,\n );\n const revoked = await tryRevoke(ctx, env.host, session.auth0RefreshToken);\n\n env.store.clear(env.host);\n\n if (!sessionRevoked) {\n ctx.warn(\n `ekanos: the Ekanos session could not be ended on ${env.host}; it ` +\n `stays refreshable until it expires if these credentials were copied.`,\n );\n }\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, sessionRevoked },\n ctx.ui.result({\n title: 'Signed out',\n tone: revoked && sessionRevoked ? 'ok' : 'warn',\n rows: [\n ['Host', shortHost(env.host)],\n ['Session', describeRevocation(revoked, sessionRevoked)],\n ],\n }),\n );\n}\n\nfunction describeRevocation(revoked: boolean, sessionRevoked: boolean): string {\n if (revoked && sessionRevoked) return 'ended on the server and at Auth0';\n if (sessionRevoked) return 'ended on the server; Auth0 revoke failed';\n if (revoked) return 'revoked at Auth0';\n\n return 'removed locally only';\n}\n\n/**\n * End the server-side Ekanos session. Never throws, for the same reason as\n * `tryRevoke`: nothing may stand between `logout` and the local delete.\n *\n * Skipped for a legacy remote `http://` host: ending the session means\n * sending the access token, and the CLI never puts a credential on a\n * plaintext wire. `logout` still clears such a host; it just cannot end the\n * server session, and says so.\n */\nasync function tryEndSession(\n ctx: CliContext,\n host: string,\n accessToken: string,\n): Promise<boolean> {\n if (isPlaintextRemoteHost(host)) {\n return false;\n }\n\n try {\n return await revokeEkanosSession(host, accessToken);\n } catch (error) {\n ctx.warn(\n `ekanos: could not reach ${host} to end the session ` +\n `(${error instanceof Error ? error.message : String(error)}).`,\n );\n\n return false;\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 ctx.ui.result({\n title: 'Not signed in',\n tone: 'warn',\n notes: ['No stored sessions on this machine.'],\n }),\n );\n }\n\n const results: { host: string; revoked: boolean; sessionRevoked: boolean }[] =\n [];\n\n for (const host of hosts) {\n const session = store.read(host);\n\n results.push({\n host,\n sessionRevoked: session\n ? await tryEndSession(ctx, host, session.accessToken)\n : false,\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 ctx.ui.result({\n title: `Signed out of ${pluralize(cleared, 'host')}`,\n tone: results.every((r) => r.revoked && r.sessionRevoked) ? 'ok' : 'warn',\n rows: [\n [\n 'Hosts',\n results.map(\n (r) =>\n `${shortHost(r.host)} · ${describeRevocation(r.revoked, r.sessionRevoked)}`,\n ),\n ],\n ],\n }),\n );\n}\n"]}
|
package/dist/commands/publish.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function runPublish(ctx, args) {
|
|
|
55
55
|
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
56
56
|
projectDir: args.dir,
|
|
57
57
|
});
|
|
58
|
-
const session = requireSession(env
|
|
58
|
+
const session = requireSession(env);
|
|
59
59
|
const loaded = loadProject(args.dir);
|
|
60
60
|
// Fail fast, before anything else: a project whose schema is newer than
|
|
61
61
|
// this CLI bundles cannot be trusted to validate correctly, so publishing
|
|
@@ -207,7 +207,7 @@ async function verifyPublishTarget(ctx, env, session, args) {
|
|
|
207
207
|
var _a, _b;
|
|
208
208
|
const result = await fetchSeatsWithOneRefresh(env, session);
|
|
209
209
|
if (result.status === 'unauthorized') {
|
|
210
|
-
stillUnauthorizedSourceSeats(env
|
|
210
|
+
stillUnauthorizedSourceSeats(env);
|
|
211
211
|
}
|
|
212
212
|
const seat = result.sources.find((s) => s.slug === args.source);
|
|
213
213
|
if (!seat) {
|
|
@@ -305,7 +305,7 @@ async function submitWithOneRefresh(env, session, payload) {
|
|
|
305
305
|
const second = await submitArchive(env.host, refreshed.accessToken, payload);
|
|
306
306
|
if (second.status === 'ok')
|
|
307
307
|
return second.receipt;
|
|
308
|
-
return stillUnauthorized(env
|
|
308
|
+
return stillUnauthorized(env);
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
311
|
* Resolve the source slug (flag → ekanos.json) and whether it was given
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport {\n forbiddenError,\n gateFailedError,\n preconditionError,\n validationError,\n} from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\nimport { packProject } from '../pack';\nimport { type LoadedProject, loadProject, serializeProject } from '../project';\nimport {\n MAX_MANIFEST_BYTES,\n stillUnauthorized,\n submitArchive,\n} from '../publish-api';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { shortHost } from '../ui';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\n/**\n * Checks that are WARN in plain `ekanos validate` but must refuse a\n * `publish` outright: a project that will fail the gate is not allowed to\n * upload and find that out from the gate instead of from itself.\n * `project.host-compiler-rules` (see `project-checks.ts`) is WARN-severity\n * everywhere else on purpose — it reads a partner's local tsconfig, not the\n * gate's own verdict, so it must not block plain validate on a false\n * positive — but at the publish gate, \"this WILL fail the gate\" is exactly\n * the thing a submission preflight exists to refuse. A list, not a second\n * check, so the underlying rule and its message/hint stay defined in exactly\n * one place.\n */\nconst PUBLISH_ERROR_ESCALATIONS: readonly string[] = [\n 'project.host-compiler-rules',\n];\n\n/**\n * Escalate the checks in `PUBLISH_ERROR_ESCALATIONS` from `warn` to `error`\n * for THIS publish run only — `validate`'s own output (and the underlying\n * finding objects elsewhere) is never touched.\n */\nfunction escalateForPublish(findings: Finding[]): Finding[] {\n return findings.map((finding) =>\n finding.severity === 'warn' &&\n PUBLISH_ERROR_ESCALATIONS.includes(finding.check)\n ? { ...finding, severity: 'error' }\n : finding,\n );\n}\n\nexport interface PublishArgs {\n host?: string;\n /** Ekanos source slug. Falls back to ekanos.json's \"source\" field. */\n source?: string;\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /**\n * Skips the interactive confirm when the source resolved IMPLICITLY (from\n * ekanos.json) and the caller holds more than one developer seat. Has no\n * effect when `--source` was passed this invocation (explicit is already\n * consent) or when the caller holds exactly one seat (nothing to\n * disambiguate).\n */\n yes: boolean;\n env: Record<string, string | undefined>;\n /**\n * Whether stdin is a real interactive terminal — the SECOND condition (with\n * human/non-JSON mode) required before publish will ever prompt. Defaults\n * to `process.stdin.isTTY === true`. Injectable so tests can exercise the\n * confirm branch without a real TTY.\n */\n stdinIsTTY?: boolean;\n /**\n * Injectable y/N prompt, for tests. Defaults to a real readline prompt over\n * stdin/stderr. Never invoked unless BOTH human mode and a real stdin TTY\n * are in play — see `canPrompt` in `verifyPublishTarget`.\n */\n confirm?: (question: string) => Promise<boolean>;\n}\n\n/** Where the resolved source slug came from — governs the confirmation rules below. */\ninterface SourceResolution {\n source: string;\n /** True iff `--source` was passed THIS invocation. */\n explicit: boolean;\n}\n\n/**\n * `publish` — pack the project and submit it to an Ekanos deployment.\n *\n * The submission gate is the SAME findings pass `validate` runs (one\n * implementation, `validate-findings.ts`): any error-severity finding refuses\n * the publish with GATE_FAILED (exit 10) and the findings in `data`, so an\n * agent can branch on the code and fix-then-retry without re-running validate.\n *\n * Authorization is server-side — the dev seat on the target source — and the\n * stored session is refreshed once on a 401, mirroring `whoami`. On the first\n * successful publish the chosen source slug is persisted into `ekanos.json`\n * so later publishes need no `--source`.\n */\nexport async function runPublish(\n ctx: CliContext,\n args: PublishArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const session = requireSession(env.store, env.host);\n\n const loaded = loadProject(args.dir);\n\n // Fail fast, before anything else: a project whose schema is newer than\n // this CLI bundles cannot be trusted to validate correctly, so publishing\n // it — the one verb whose findings gate is meant to be authoritative —\n // must refuse before packing or submitting anything. See `schema-skew.ts`.\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({\n env: args.env,\n host: env.host,\n session,\n });\n announceUpdate(ctx, notice);\n\n // The host would reject a build from a CLI it has declared unsupported\n // anyway (see the wire contract in `toolchain-api.ts`) — refuse here with a\n // hint naming the fix, rather than let the submission round-trip fail with\n // whatever generic error the host happens to answer with.\n if (notice?.status === 'unsupported') {\n throw preconditionError(\n `@ekanos/cli ${notice.current.cli} is below ${env.host}'s minimum ` +\n `supported version (needs at least ${notice.target.cli}).`,\n 'Run \"ekanos upgrade\" before publishing — the host will refuse this ' +\n 'submission from an unsupported CLI regardless.',\n );\n }\n\n const resolution = await resolveSourceSlug(env, session, args.source, loaded);\n const version = readProjectVersion(loaded.projectDir);\n const slug = loaded.primary.slug;\n\n ctx.log(`Validating ${slug} before publishing…`);\n\n const findings = escalateForPublish(await collectAllFindings(loaded));\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n\n if (errorCount > 0) {\n // Same rule as `validate`: render the shared findings block to stderr\n // BEFORE the envelope settles, human mode only. JSON mode carries the\n // identical findings in `data.findings` and must stay byte-identical.\n // Closing order is findings block -> short summary (via `ctx.fail`'s\n // `findingsSummary` option) -> next action.\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.err(\n `${ctx.ui.glyphs.err} Refusing to publish: ${errorCount} finding` +\n `${errorCount === 1 ? '' : 's'}.`,\n ),\n );\n renderFindings(ctx, findings, { cwd: loaded.projectDir });\n }\n\n const exitCode = ctx.fail(\n gateFailedError(\n `Refusing to publish: ${errorCount} validation finding` +\n `${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings (they are ' +\n 'exactly what \"ekanos validate\" reports), then re-run ' +\n '\"ekanos publish\".',\n ),\n { slug, version, findings },\n !ctx.jsonMode\n ? { findingsSummary: { command: 'ekanos publish', errorCount } }\n : undefined,\n );\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.nextAction(\n 'ekanos validate',\n 'fix the error-severity findings above, then re-run \"ekanos publish\"',\n ),\n );\n }\n\n return exitCode;\n }\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} validated ${slug} — no error-severity findings.`,\n ),\n );\n }\n\n // The 100%-certainty mechanism: confirm the resolved source is one the\n // caller actually holds a seat on — and, when it was resolved IMPLICITLY\n // from ekanos.json with more than one seat in play, that a human (or an\n // explicit --yes) actually intends this target — BEFORE anything is packed\n // or sent over the wire.\n const verified = await verifyPublishTarget(ctx, env, resolution.session, {\n source: resolution.source,\n explicit: resolution.explicit,\n slug,\n version,\n yes: args.yes,\n stdinIsTTY: args.stdinIsTTY ?? process.stdin.isTTY === true,\n confirm: args.confirm ?? promptYesNo,\n });\n\n const source = verified.sourceSeat.slug;\n\n ctx.log(`Packing ${slug}@${version}…`);\n\n const packed = packProject(loaded.projectDir);\n const manifest = buildManifest(loaded, packed.files);\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} packed ${slug}@${version} (${packed.files.length} files).`,\n ),\n );\n }\n\n ctx.log(`Submitting to ${env.host} (source \"${source}\")…`);\n\n const receipt = await submitWithOneRefresh(env, verified.session, {\n source,\n slug,\n version,\n manifest,\n archive: packed.archive,\n });\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} submitted ${receipt.slug}@${receipt.version} ` +\n `(state \"${receipt.state}\").`,\n ),\n );\n }\n\n const persisted = persistProjectFields(loaded, source, env.host);\n\n const persistedNote = [\n ...(persisted.source ? [`\"source\": \"${source}\"`] : []),\n ...(persisted.host ? [`\"host\": \"${env.host}\"`] : []),\n ];\n\n return ctx.succeed(\n {\n host: env.host,\n source: {\n id: verified.sourceSeat.id,\n slug: verified.sourceSeat.slug,\n name: verified.sourceSeat.name,\n },\n slug: receipt.slug,\n version: receipt.version,\n submissionId: receipt.submissionId,\n state: receipt.state,\n archiveSha256: receipt.archiveSha256,\n files: packed.files,\n sourcePersisted: persisted.source,\n hostPersisted: persisted.host,\n hostOverride: persisted.hostOverride,\n ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\n },\n ctx.ui.result({\n title: 'Submitted',\n rows: [\n ['Integration', `${receipt.slug}@${receipt.version}`],\n ['State', receipt.state],\n ['Source', source],\n ['Host', shortHost(env.host)],\n ...(persistedNote.length > 0\n ? [\n [\n 'Saved',\n `${persistedNote.join(' and ')} to ekanos.json`,\n ] as const,\n ]\n : []),\n ],\n notes: persisted.hostOverride\n ? [\n `This publish went to ${env.host}, but ekanos.json keeps \"host\": ` +\n `\"${persisted.hostOverride}\" — a one-off override never replaces ` +\n 'the saved host; edit ekanos.json to change it.',\n ]\n : [],\n }),\n );\n}\n\n/**\n * The publish-target verification gate.\n *\n * FORBIDDEN (exit 5) whenever the resolved source is not among the caller's\n * seats — regardless of whether it came from `--source` or ekanos.json.\n * Beyond that, the confirmation rules branch on how the source was resolved:\n *\n * - `--source` THIS invocation: explicit is consent. Proceed.\n * - ekanos.json, exactly one seat held (which must be the match, or the\n * FORBIDDEN check above would have already fired): proceed, but say so —\n * there is nothing to disambiguate, but the target should never be silent.\n * - ekanos.json, multiple seats held: the ambiguous case this mechanism\n * exists for. Only when BOTH human (non-JSON) mode AND stdin is a real\n * TTY does it print the target and require an explicit y/N confirm\n * (`--yes` skips it) — see `canPrompt` below for why stdin matters, not\n * just stdout. Every other combination (JSON mode, or human mode with a\n * closed/non-interactive stdin) NEVER prompts — it proceeds only with\n * `--yes`, otherwise VALIDATION (exit 3) with the seat list, naming\n * `--source`, `--yes` and `ekanos use`.\n */\nasync function verifyPublishTarget(\n ctx: CliContext,\n env: AuthEnvironment,\n session: StoredSession,\n args: SourceResolution & {\n slug: string;\n version: string;\n yes: boolean;\n /** Real interactivity signal for stdin — see `canPrompt` below. */\n stdinIsTTY: boolean;\n confirm: (question: string) => Promise<boolean>;\n },\n): Promise<{ sourceSeat: SourceSeat; session: StoredSession }> {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'unauthorized') {\n stillUnauthorizedSourceSeats(env.host);\n }\n\n const seat = result.sources.find((s) => s.slug === args.source);\n\n if (!seat) {\n throw forbiddenError(\n `This account does not hold a seat on source \"${args.source}\" on ${env.host}.`,\n seatsHint(result.sources, env.host),\n );\n }\n\n if (args.explicit) {\n return { sourceSeat: seat, session: result.session };\n }\n\n if (result.sources.length === 1) {\n ctx.log(\n `Publishing to ${seat.name ?? seat.slug} (${seat.slug}) — your only ` +\n `developer seat.`,\n );\n return { sourceSeat: seat, session: result.session };\n }\n\n // Implicit source, multiple seats: the ambiguous case.\n //\n // `ctx.jsonMode` alone is NOT enough to decide whether an interactive\n // confirm is safe: `--no-json` forces human mode even when stdout is\n // piped, and neither `--no-json` nor a bare human run says anything about\n // stdin. Prompting `readline`'s `.question()` against a closed/non-TTY\n // stdin (a cron job, a wrapper script piping stdout but not attached to a\n // terminal) never resolves — the process hangs instead of exiting with a\n // clean code. So an actual prompt requires BOTH: human mode AND a real\n // stdin TTY. Anything else — JSON mode, or human mode with non-interactive\n // stdin — falls through to the same non-prompting `--yes`-or-refuse rule.\n const canPrompt = !ctx.jsonMode && args.stdinIsTTY;\n\n if (canPrompt) {\n ctx.log(\n `Publishing ${args.slug}@${args.version} to SOURCE ${seat.name ?? seat.slug} ` +\n `(${seat.slug}) on ${env.host}`,\n );\n\n if (!args.yes) {\n const confirmed = await args.confirm('Proceed? [y/N]');\n\n if (!confirmed) {\n throw validationError(\n `Publish to \"${seat.slug}\" was not confirmed.`,\n multiSeatHint(result.sources),\n );\n }\n }\n\n return { sourceSeat: seat, session: result.session };\n }\n\n if (args.yes) {\n return { sourceSeat: seat, session: result.session };\n }\n\n throw validationError(\n `Ambiguous publish target: this account holds ${result.sources.length} ` +\n `developer seats and the source was resolved from ekanos.json, not ` +\n `an explicit flag.`,\n multiSeatHint(result.sources),\n );\n}\n\nfunction seatsHint(sources: SourceSeat[], host: string): string {\n if (sources.length === 0) {\n return (\n `You hold no seats on any source on ${host}. Ask an Ekanos super-admin ` +\n `to grant your account the developer seat on the intended source.`\n );\n }\n\n return (\n `You hold a seat on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `naming one of these, or run \"ekanos use <slug>\" to set the project's ` +\n `default.`\n );\n}\n\nfunction multiSeatHint(sources: SourceSeat[]): string {\n return (\n `You hold seats on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `to name the target explicitly, add \"--yes\" to confirm the target ` +\n `already in ekanos.json non-interactively, or run \"ekanos use <slug>\" ` +\n `to change the project's default.`\n );\n}\n\nfunction describeSeats(sources: SourceSeat[]): string {\n return sources.map((s) => `${s.slug} (${s.roles.join(', ')})`).join(', ');\n}\n\n/**\n * The real, non-test y/N prompt. Reads a line from stdin and writes the\n * question to stderr, so it never touches stdout — the one stream the JSON\n * envelope invariant depends on. Only reachable when `verifyPublishTarget`'s\n * `canPrompt` is true — human mode AND a real stdin TTY — so this never\n * blocks on a closed or non-interactive stdin.\n */\n/**\n * Exported so `upgrade` can reuse the exact same y/N prompt rather than\n * writing a second one — see `canPrompt` above for the stdin-TTY gating this\n * is only ever invoked behind.\n */\nexport async function promptYesNo(question: string): Promise<boolean> {\n const readline = await import('node:readline/promises');\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n\n try {\n const answer = await rl.question(`${question} `);\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\n }\n}\n\nasync function submitWithOneRefresh(\n env: ReturnType<typeof resolveAuthEnvironment>,\n session: StoredSession,\n payload: Parameters<typeof submitArchive>[2],\n) {\n const first = await submitArchive(env.host, session.accessToken, payload);\n\n if (first.status === 'ok') return first.receipt;\n\n // The access token no longer authenticates — an ordinary event, not an\n // error. One refresh, one retry; a second 401 is authoritative.\n const refreshed = await refreshStoredSession(env, session);\n const second = await submitArchive(env.host, refreshed.accessToken, payload);\n\n if (second.status === 'ok') return second.receipt;\n\n return stillUnauthorized(env.host);\n}\n\n/**\n * Resolve the source slug (flag → ekanos.json) and whether it was given\n * EXPLICITLY this invocation — the fact `verifyPublishTarget` uses to decide\n * whether a confirmation is even in play. `flag !== undefined` (rather than\n * \"non-empty after trim\") is deliberate: an explicit `--source` that is\n * effectively empty is still a caller who typed the flag, not one who\n * fell through to ekanos.json.\n */\nasync function resolveSourceSlug(\n env: AuthEnvironment,\n session: StoredSession,\n flag: string | undefined,\n loaded: LoadedProject,\n): Promise<SourceResolution & { session: StoredSession }> {\n const explicit = flag !== undefined;\n const raw = flag ?? loaded.project.source;\n\n if (!raw || raw.trim().length === 0) {\n return noSourceError(env, session);\n }\n\n return { source: raw.trim(), explicit, session };\n}\n\n/**\n * The no-source validation error, enriched with the caller's seat list when\n * it can be fetched — best-effort, since a session is guaranteed to exist\n * here (publish already called `requireSession`) but the fetch itself can\n * still fail (network, MFA, a down deployment). A failure to enrich must\n * never mask or replace the primary \"no source given\" error.\n */\nasync function noSourceError(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<never> {\n let seatsNote = '';\n\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'ok') {\n seatsNote =\n result.sources.length > 0\n ? ` You hold a seat on: ${describeSeats(result.sources)}.`\n : ' You hold no seats on any source on this host yet.';\n }\n } catch {\n // Best-effort — the primary error below stands on its own.\n }\n\n throw validationError(\n 'No Ekanos source given for the submission.',\n 'Pass \"--source <slug>\" (your operator names the source), or set ' +\n '\"source\": \"<slug>\" in ekanos.json. A successful publish saves it ' +\n `there for you.${seatsNote}`,\n );\n}\n\n/**\n * Persist the chosen source AND host into ekanos.json after a SUCCESSFUL\n * publish, so the next publish — and every other host-taking verb — needs no\n * flag. Returns what was written. A write failure is reported but never fails\n * the command — the submission already landed.\n *\n * `publish` is the only verb that writes ekanos.json: it is the one verb that\n * proves the (host, source) pair actually works, and it always runs inside a\n * project. `login` deliberately never writes — it is routinely run outside\n * one, and the credential store's sole-host fallback covers it.\n *\n * `host` persists on FIRST WRITE ONLY. It decides where every future verb\n * sends the bearer token and the source archive, so a one-off `--host`\n * override (a staging test, say) must not silently become the project's\n * sticky default; when the override differs from the stored host, the stored\n * value is kept and the difference is called out in the result instead.\n * Changing it is an explicit edit of ekanos.json. `source` keeps the simpler\n * always-update semantics — it is scoped BY the host, and updating it is the\n * documented way to repoint a project.\n */\nexport function persistProjectFields(\n loaded: LoadedProject,\n source: string,\n host: string,\n): { source: boolean; host: boolean; hostOverride: string | null } {\n const wants = {\n source: loaded.project.source !== source,\n host: loaded.project.host === undefined,\n };\n\n const hostOverride =\n loaded.project.host !== undefined && loaded.project.host !== host\n ? loaded.project.host\n : null;\n\n if (!wants.source && !wants.host) {\n return { source: false, host: false, hostOverride };\n }\n\n try {\n fs.writeFileSync(\n loaded.configPath,\n serializeProject({\n ...loaded.project,\n source,\n host: wants.host ? host : loaded.project.host,\n }),\n );\n\n return { ...wants, hostOverride };\n } catch {\n // Diagnostics-only: the publish succeeded; a read-only ekanos.json just\n // means the next publish needs the flags again.\n return { source: false, host: false, hostOverride };\n }\n}\n\n/**\n * How many packed paths the manifest embeds before switching to a truncated\n * list plus a `filesTruncated` total. The manifest is metadata — the archive\n * itself is the payload — and the server caps it at MAX_MANIFEST_BYTES, so a\n * many-small-files project must not be able to blow the cap with its file\n * list alone.\n */\nconst MANIFEST_FILE_LIST_CAP = 200;\n\n/**\n * The submission manifest: the parsed ekanos.json, the integration entries,\n * and (a bounded prefix of) the packed file list. Refuses to build one over\n * the server's byte cap — a clear VALIDATION error here beats the server's\n * generic 400 after a 4 MiB upload.\n */\nfunction buildManifest(loaded: LoadedProject, files: string[]): string {\n const manifest = JSON.stringify({\n project: loaded.project,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n files: files.slice(0, MANIFEST_FILE_LIST_CAP),\n ...(files.length > MANIFEST_FILE_LIST_CAP\n ? { filesTruncated: files.length }\n : {}),\n });\n\n const bytes = Buffer.byteLength(manifest, 'utf8');\n\n if (bytes > MAX_MANIFEST_BYTES) {\n throw validationError(\n `The submission manifest is ${bytes} bytes, over the ` +\n `${MAX_MANIFEST_BYTES}-byte server cap.`,\n 'The manifest embeds your ekanos.json — trim oversized fields there ' +\n '(sourceGlobs is the usual culprit), then re-run \"ekanos publish\".',\n );\n }\n\n return manifest;\n}\n\nconst SEMVER_PATTERN =\n /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/;\n\n/**\n * The submitted version is the project's own `package.json#version` — one\n * source of truth, the same field npm would publish.\n */\nexport function readProjectVersion(projectDir: string): string {\n const manifestPath = path.join(projectDir, 'package.json');\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `Could not read ${manifestPath}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Ensure the project has a valid package.json — its \"version\" field is ` +\n `what gets submitted.`,\n );\n }\n\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n\n if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {\n throw validationError(\n `package.json \"version\" is ${\n typeof version === 'string' ? `\"${version}\"` : 'missing'\n }, which is not a semver version.`,\n `Set \"version\" in ${manifestPath} to a semver string like \"0.1.0\", ` +\n `then re-run \"ekanos publish\".`,\n );\n }\n\n return version;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport {\n forbiddenError,\n gateFailedError,\n preconditionError,\n validationError,\n} from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\nimport { packProject } from '../pack';\nimport { type LoadedProject, loadProject, serializeProject } from '../project';\nimport {\n MAX_MANIFEST_BYTES,\n stillUnauthorized,\n submitArchive,\n} from '../publish-api';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { shortHost } from '../ui';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\n/**\n * Checks that are WARN in plain `ekanos validate` but must refuse a\n * `publish` outright: a project that will fail the gate is not allowed to\n * upload and find that out from the gate instead of from itself.\n * `project.host-compiler-rules` (see `project-checks.ts`) is WARN-severity\n * everywhere else on purpose — it reads a partner's local tsconfig, not the\n * gate's own verdict, so it must not block plain validate on a false\n * positive — but at the publish gate, \"this WILL fail the gate\" is exactly\n * the thing a submission preflight exists to refuse. A list, not a second\n * check, so the underlying rule and its message/hint stay defined in exactly\n * one place.\n */\nconst PUBLISH_ERROR_ESCALATIONS: readonly string[] = [\n 'project.host-compiler-rules',\n];\n\n/**\n * Escalate the checks in `PUBLISH_ERROR_ESCALATIONS` from `warn` to `error`\n * for THIS publish run only — `validate`'s own output (and the underlying\n * finding objects elsewhere) is never touched.\n */\nfunction escalateForPublish(findings: Finding[]): Finding[] {\n return findings.map((finding) =>\n finding.severity === 'warn' &&\n PUBLISH_ERROR_ESCALATIONS.includes(finding.check)\n ? { ...finding, severity: 'error' }\n : finding,\n );\n}\n\nexport interface PublishArgs {\n host?: string;\n /** Ekanos source slug. Falls back to ekanos.json's \"source\" field. */\n source?: string;\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /**\n * Skips the interactive confirm when the source resolved IMPLICITLY (from\n * ekanos.json) and the caller holds more than one developer seat. Has no\n * effect when `--source` was passed this invocation (explicit is already\n * consent) or when the caller holds exactly one seat (nothing to\n * disambiguate).\n */\n yes: boolean;\n env: Record<string, string | undefined>;\n /**\n * Whether stdin is a real interactive terminal — the SECOND condition (with\n * human/non-JSON mode) required before publish will ever prompt. Defaults\n * to `process.stdin.isTTY === true`. Injectable so tests can exercise the\n * confirm branch without a real TTY.\n */\n stdinIsTTY?: boolean;\n /**\n * Injectable y/N prompt, for tests. Defaults to a real readline prompt over\n * stdin/stderr. Never invoked unless BOTH human mode and a real stdin TTY\n * are in play — see `canPrompt` in `verifyPublishTarget`.\n */\n confirm?: (question: string) => Promise<boolean>;\n}\n\n/** Where the resolved source slug came from — governs the confirmation rules below. */\ninterface SourceResolution {\n source: string;\n /** True iff `--source` was passed THIS invocation. */\n explicit: boolean;\n}\n\n/**\n * `publish` — pack the project and submit it to an Ekanos deployment.\n *\n * The submission gate is the SAME findings pass `validate` runs (one\n * implementation, `validate-findings.ts`): any error-severity finding refuses\n * the publish with GATE_FAILED (exit 10) and the findings in `data`, so an\n * agent can branch on the code and fix-then-retry without re-running validate.\n *\n * Authorization is server-side — the dev seat on the target source — and the\n * stored session is refreshed once on a 401, mirroring `whoami`. On the first\n * successful publish the chosen source slug is persisted into `ekanos.json`\n * so later publishes need no `--source`.\n */\nexport async function runPublish(\n ctx: CliContext,\n args: PublishArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const session = requireSession(env);\n\n const loaded = loadProject(args.dir);\n\n // Fail fast, before anything else: a project whose schema is newer than\n // this CLI bundles cannot be trusted to validate correctly, so publishing\n // it — the one verb whose findings gate is meant to be authoritative —\n // must refuse before packing or submitting anything. See `schema-skew.ts`.\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({\n env: args.env,\n host: env.host,\n session,\n });\n announceUpdate(ctx, notice);\n\n // The host would reject a build from a CLI it has declared unsupported\n // anyway (see the wire contract in `toolchain-api.ts`) — refuse here with a\n // hint naming the fix, rather than let the submission round-trip fail with\n // whatever generic error the host happens to answer with.\n if (notice?.status === 'unsupported') {\n throw preconditionError(\n `@ekanos/cli ${notice.current.cli} is below ${env.host}'s minimum ` +\n `supported version (needs at least ${notice.target.cli}).`,\n 'Run \"ekanos upgrade\" before publishing — the host will refuse this ' +\n 'submission from an unsupported CLI regardless.',\n );\n }\n\n const resolution = await resolveSourceSlug(env, session, args.source, loaded);\n const version = readProjectVersion(loaded.projectDir);\n const slug = loaded.primary.slug;\n\n ctx.log(`Validating ${slug} before publishing…`);\n\n const findings = escalateForPublish(await collectAllFindings(loaded));\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n\n if (errorCount > 0) {\n // Same rule as `validate`: render the shared findings block to stderr\n // BEFORE the envelope settles, human mode only. JSON mode carries the\n // identical findings in `data.findings` and must stay byte-identical.\n // Closing order is findings block -> short summary (via `ctx.fail`'s\n // `findingsSummary` option) -> next action.\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.err(\n `${ctx.ui.glyphs.err} Refusing to publish: ${errorCount} finding` +\n `${errorCount === 1 ? '' : 's'}.`,\n ),\n );\n renderFindings(ctx, findings, { cwd: loaded.projectDir });\n }\n\n const exitCode = ctx.fail(\n gateFailedError(\n `Refusing to publish: ${errorCount} validation finding` +\n `${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings (they are ' +\n 'exactly what \"ekanos validate\" reports), then re-run ' +\n '\"ekanos publish\".',\n ),\n { slug, version, findings },\n !ctx.jsonMode\n ? { findingsSummary: { command: 'ekanos publish', errorCount } }\n : undefined,\n );\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.nextAction(\n 'ekanos validate',\n 'fix the error-severity findings above, then re-run \"ekanos publish\"',\n ),\n );\n }\n\n return exitCode;\n }\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} validated ${slug} — no error-severity findings.`,\n ),\n );\n }\n\n // The 100%-certainty mechanism: confirm the resolved source is one the\n // caller actually holds a seat on — and, when it was resolved IMPLICITLY\n // from ekanos.json with more than one seat in play, that a human (or an\n // explicit --yes) actually intends this target — BEFORE anything is packed\n // or sent over the wire.\n const verified = await verifyPublishTarget(ctx, env, resolution.session, {\n source: resolution.source,\n explicit: resolution.explicit,\n slug,\n version,\n yes: args.yes,\n stdinIsTTY: args.stdinIsTTY ?? process.stdin.isTTY === true,\n confirm: args.confirm ?? promptYesNo,\n });\n\n const source = verified.sourceSeat.slug;\n\n ctx.log(`Packing ${slug}@${version}…`);\n\n const packed = packProject(loaded.projectDir);\n const manifest = buildManifest(loaded, packed.files);\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} packed ${slug}@${version} (${packed.files.length} files).`,\n ),\n );\n }\n\n ctx.log(`Submitting to ${env.host} (source \"${source}\")…`);\n\n const receipt = await submitWithOneRefresh(env, verified.session, {\n source,\n slug,\n version,\n manifest,\n archive: packed.archive,\n });\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} submitted ${receipt.slug}@${receipt.version} ` +\n `(state \"${receipt.state}\").`,\n ),\n );\n }\n\n const persisted = persistProjectFields(loaded, source, env.host);\n\n const persistedNote = [\n ...(persisted.source ? [`\"source\": \"${source}\"`] : []),\n ...(persisted.host ? [`\"host\": \"${env.host}\"`] : []),\n ];\n\n return ctx.succeed(\n {\n host: env.host,\n source: {\n id: verified.sourceSeat.id,\n slug: verified.sourceSeat.slug,\n name: verified.sourceSeat.name,\n },\n slug: receipt.slug,\n version: receipt.version,\n submissionId: receipt.submissionId,\n state: receipt.state,\n archiveSha256: receipt.archiveSha256,\n files: packed.files,\n sourcePersisted: persisted.source,\n hostPersisted: persisted.host,\n hostOverride: persisted.hostOverride,\n ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\n },\n ctx.ui.result({\n title: 'Submitted',\n rows: [\n ['Integration', `${receipt.slug}@${receipt.version}`],\n ['State', receipt.state],\n ['Source', source],\n ['Host', shortHost(env.host)],\n ...(persistedNote.length > 0\n ? [\n [\n 'Saved',\n `${persistedNote.join(' and ')} to ekanos.json`,\n ] as const,\n ]\n : []),\n ],\n notes: persisted.hostOverride\n ? [\n `This publish went to ${env.host}, but ekanos.json keeps \"host\": ` +\n `\"${persisted.hostOverride}\" — a one-off override never replaces ` +\n 'the saved host; edit ekanos.json to change it.',\n ]\n : [],\n }),\n );\n}\n\n/**\n * The publish-target verification gate.\n *\n * FORBIDDEN (exit 5) whenever the resolved source is not among the caller's\n * seats — regardless of whether it came from `--source` or ekanos.json.\n * Beyond that, the confirmation rules branch on how the source was resolved:\n *\n * - `--source` THIS invocation: explicit is consent. Proceed.\n * - ekanos.json, exactly one seat held (which must be the match, or the\n * FORBIDDEN check above would have already fired): proceed, but say so —\n * there is nothing to disambiguate, but the target should never be silent.\n * - ekanos.json, multiple seats held: the ambiguous case this mechanism\n * exists for. Only when BOTH human (non-JSON) mode AND stdin is a real\n * TTY does it print the target and require an explicit y/N confirm\n * (`--yes` skips it) — see `canPrompt` below for why stdin matters, not\n * just stdout. Every other combination (JSON mode, or human mode with a\n * closed/non-interactive stdin) NEVER prompts — it proceeds only with\n * `--yes`, otherwise VALIDATION (exit 3) with the seat list, naming\n * `--source`, `--yes` and `ekanos use`.\n */\nasync function verifyPublishTarget(\n ctx: CliContext,\n env: AuthEnvironment,\n session: StoredSession,\n args: SourceResolution & {\n slug: string;\n version: string;\n yes: boolean;\n /** Real interactivity signal for stdin — see `canPrompt` below. */\n stdinIsTTY: boolean;\n confirm: (question: string) => Promise<boolean>;\n },\n): Promise<{ sourceSeat: SourceSeat; session: StoredSession }> {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'unauthorized') {\n stillUnauthorizedSourceSeats(env);\n }\n\n const seat = result.sources.find((s) => s.slug === args.source);\n\n if (!seat) {\n throw forbiddenError(\n `This account does not hold a seat on source \"${args.source}\" on ${env.host}.`,\n seatsHint(result.sources, env.host),\n );\n }\n\n if (args.explicit) {\n return { sourceSeat: seat, session: result.session };\n }\n\n if (result.sources.length === 1) {\n ctx.log(\n `Publishing to ${seat.name ?? seat.slug} (${seat.slug}) — your only ` +\n `developer seat.`,\n );\n return { sourceSeat: seat, session: result.session };\n }\n\n // Implicit source, multiple seats: the ambiguous case.\n //\n // `ctx.jsonMode` alone is NOT enough to decide whether an interactive\n // confirm is safe: `--no-json` forces human mode even when stdout is\n // piped, and neither `--no-json` nor a bare human run says anything about\n // stdin. Prompting `readline`'s `.question()` against a closed/non-TTY\n // stdin (a cron job, a wrapper script piping stdout but not attached to a\n // terminal) never resolves — the process hangs instead of exiting with a\n // clean code. So an actual prompt requires BOTH: human mode AND a real\n // stdin TTY. Anything else — JSON mode, or human mode with non-interactive\n // stdin — falls through to the same non-prompting `--yes`-or-refuse rule.\n const canPrompt = !ctx.jsonMode && args.stdinIsTTY;\n\n if (canPrompt) {\n ctx.log(\n `Publishing ${args.slug}@${args.version} to SOURCE ${seat.name ?? seat.slug} ` +\n `(${seat.slug}) on ${env.host}`,\n );\n\n if (!args.yes) {\n const confirmed = await args.confirm('Proceed? [y/N]');\n\n if (!confirmed) {\n throw validationError(\n `Publish to \"${seat.slug}\" was not confirmed.`,\n multiSeatHint(result.sources),\n );\n }\n }\n\n return { sourceSeat: seat, session: result.session };\n }\n\n if (args.yes) {\n return { sourceSeat: seat, session: result.session };\n }\n\n throw validationError(\n `Ambiguous publish target: this account holds ${result.sources.length} ` +\n `developer seats and the source was resolved from ekanos.json, not ` +\n `an explicit flag.`,\n multiSeatHint(result.sources),\n );\n}\n\nfunction seatsHint(sources: SourceSeat[], host: string): string {\n if (sources.length === 0) {\n return (\n `You hold no seats on any source on ${host}. Ask an Ekanos super-admin ` +\n `to grant your account the developer seat on the intended source.`\n );\n }\n\n return (\n `You hold a seat on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `naming one of these, or run \"ekanos use <slug>\" to set the project's ` +\n `default.`\n );\n}\n\nfunction multiSeatHint(sources: SourceSeat[]): string {\n return (\n `You hold seats on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `to name the target explicitly, add \"--yes\" to confirm the target ` +\n `already in ekanos.json non-interactively, or run \"ekanos use <slug>\" ` +\n `to change the project's default.`\n );\n}\n\nfunction describeSeats(sources: SourceSeat[]): string {\n return sources.map((s) => `${s.slug} (${s.roles.join(', ')})`).join(', ');\n}\n\n/**\n * The real, non-test y/N prompt. Reads a line from stdin and writes the\n * question to stderr, so it never touches stdout — the one stream the JSON\n * envelope invariant depends on. Only reachable when `verifyPublishTarget`'s\n * `canPrompt` is true — human mode AND a real stdin TTY — so this never\n * blocks on a closed or non-interactive stdin.\n */\n/**\n * Exported so `upgrade` can reuse the exact same y/N prompt rather than\n * writing a second one — see `canPrompt` above for the stdin-TTY gating this\n * is only ever invoked behind.\n */\nexport async function promptYesNo(question: string): Promise<boolean> {\n const readline = await import('node:readline/promises');\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n\n try {\n const answer = await rl.question(`${question} `);\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\n }\n}\n\nasync function submitWithOneRefresh(\n env: ReturnType<typeof resolveAuthEnvironment>,\n session: StoredSession,\n payload: Parameters<typeof submitArchive>[2],\n) {\n const first = await submitArchive(env.host, session.accessToken, payload);\n\n if (first.status === 'ok') return first.receipt;\n\n // The access token no longer authenticates — an ordinary event, not an\n // error. One refresh, one retry; a second 401 is authoritative.\n const refreshed = await refreshStoredSession(env, session);\n const second = await submitArchive(env.host, refreshed.accessToken, payload);\n\n if (second.status === 'ok') return second.receipt;\n\n return stillUnauthorized(env);\n}\n\n/**\n * Resolve the source slug (flag → ekanos.json) and whether it was given\n * EXPLICITLY this invocation — the fact `verifyPublishTarget` uses to decide\n * whether a confirmation is even in play. `flag !== undefined` (rather than\n * \"non-empty after trim\") is deliberate: an explicit `--source` that is\n * effectively empty is still a caller who typed the flag, not one who\n * fell through to ekanos.json.\n */\nasync function resolveSourceSlug(\n env: AuthEnvironment,\n session: StoredSession,\n flag: string | undefined,\n loaded: LoadedProject,\n): Promise<SourceResolution & { session: StoredSession }> {\n const explicit = flag !== undefined;\n const raw = flag ?? loaded.project.source;\n\n if (!raw || raw.trim().length === 0) {\n return noSourceError(env, session);\n }\n\n return { source: raw.trim(), explicit, session };\n}\n\n/**\n * The no-source validation error, enriched with the caller's seat list when\n * it can be fetched — best-effort, since a session is guaranteed to exist\n * here (publish already called `requireSession`) but the fetch itself can\n * still fail (network, MFA, a down deployment). A failure to enrich must\n * never mask or replace the primary \"no source given\" error.\n */\nasync function noSourceError(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<never> {\n let seatsNote = '';\n\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'ok') {\n seatsNote =\n result.sources.length > 0\n ? ` You hold a seat on: ${describeSeats(result.sources)}.`\n : ' You hold no seats on any source on this host yet.';\n }\n } catch {\n // Best-effort — the primary error below stands on its own.\n }\n\n throw validationError(\n 'No Ekanos source given for the submission.',\n 'Pass \"--source <slug>\" (your operator names the source), or set ' +\n '\"source\": \"<slug>\" in ekanos.json. A successful publish saves it ' +\n `there for you.${seatsNote}`,\n );\n}\n\n/**\n * Persist the chosen source AND host into ekanos.json after a SUCCESSFUL\n * publish, so the next publish — and every other host-taking verb — needs no\n * flag. Returns what was written. A write failure is reported but never fails\n * the command — the submission already landed.\n *\n * `publish` is the only verb that writes ekanos.json: it is the one verb that\n * proves the (host, source) pair actually works, and it always runs inside a\n * project. `login` deliberately never writes — it is routinely run outside\n * one, and the credential store's sole-host fallback covers it.\n *\n * `host` persists on FIRST WRITE ONLY. It decides where every future verb\n * sends the bearer token and the source archive, so a one-off `--host`\n * override (a staging test, say) must not silently become the project's\n * sticky default; when the override differs from the stored host, the stored\n * value is kept and the difference is called out in the result instead.\n * Changing it is an explicit edit of ekanos.json. `source` keeps the simpler\n * always-update semantics — it is scoped BY the host, and updating it is the\n * documented way to repoint a project.\n */\nexport function persistProjectFields(\n loaded: LoadedProject,\n source: string,\n host: string,\n): { source: boolean; host: boolean; hostOverride: string | null } {\n const wants = {\n source: loaded.project.source !== source,\n host: loaded.project.host === undefined,\n };\n\n const hostOverride =\n loaded.project.host !== undefined && loaded.project.host !== host\n ? loaded.project.host\n : null;\n\n if (!wants.source && !wants.host) {\n return { source: false, host: false, hostOverride };\n }\n\n try {\n fs.writeFileSync(\n loaded.configPath,\n serializeProject({\n ...loaded.project,\n source,\n host: wants.host ? host : loaded.project.host,\n }),\n );\n\n return { ...wants, hostOverride };\n } catch {\n // Diagnostics-only: the publish succeeded; a read-only ekanos.json just\n // means the next publish needs the flags again.\n return { source: false, host: false, hostOverride };\n }\n}\n\n/**\n * How many packed paths the manifest embeds before switching to a truncated\n * list plus a `filesTruncated` total. The manifest is metadata — the archive\n * itself is the payload — and the server caps it at MAX_MANIFEST_BYTES, so a\n * many-small-files project must not be able to blow the cap with its file\n * list alone.\n */\nconst MANIFEST_FILE_LIST_CAP = 200;\n\n/**\n * The submission manifest: the parsed ekanos.json, the integration entries,\n * and (a bounded prefix of) the packed file list. Refuses to build one over\n * the server's byte cap — a clear VALIDATION error here beats the server's\n * generic 400 after a 4 MiB upload.\n */\nfunction buildManifest(loaded: LoadedProject, files: string[]): string {\n const manifest = JSON.stringify({\n project: loaded.project,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n files: files.slice(0, MANIFEST_FILE_LIST_CAP),\n ...(files.length > MANIFEST_FILE_LIST_CAP\n ? { filesTruncated: files.length }\n : {}),\n });\n\n const bytes = Buffer.byteLength(manifest, 'utf8');\n\n if (bytes > MAX_MANIFEST_BYTES) {\n throw validationError(\n `The submission manifest is ${bytes} bytes, over the ` +\n `${MAX_MANIFEST_BYTES}-byte server cap.`,\n 'The manifest embeds your ekanos.json — trim oversized fields there ' +\n '(sourceGlobs is the usual culprit), then re-run \"ekanos publish\".',\n );\n }\n\n return manifest;\n}\n\nconst SEMVER_PATTERN =\n /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/;\n\n/**\n * The submitted version is the project's own `package.json#version` — one\n * source of truth, the same field npm would publish.\n */\nexport function readProjectVersion(projectDir: string): string {\n const manifestPath = path.join(projectDir, 'package.json');\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `Could not read ${manifestPath}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Ensure the project has a valid package.json — its \"version\" field is ` +\n `what gets submitted.`,\n );\n }\n\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n\n if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {\n throw validationError(\n `package.json \"version\" is ${\n typeof version === 'string' ? `\"${version}\"` : 'missing'\n }, which is not a semver version.`,\n `Set \"version\" in ${manifestPath} to a semver string like \"0.1.0\", ` +\n `then re-run \"ekanos publish\".`,\n );\n }\n\n return version;\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { loginHint } from '../auth/host-trust.js';
|
|
1
2
|
import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
|
|
2
|
-
import { authRequiredError, usageError } from '../errors.js';
|
|
3
|
+
import { authRequiredError, invalidStateError, usageError } from '../errors.js';
|
|
3
4
|
import { addReviewer, listReviewers, removeReviewer, } from '../reviewers-api.js';
|
|
4
5
|
import { resolveSourceTarget } from '../source-target.js';
|
|
5
6
|
export const REVIEWERS_SUBCOMMANDS = ['list', 'add', 'remove'];
|
|
@@ -30,10 +31,13 @@ export async function runReviewers(ctx, args) {
|
|
|
30
31
|
if (subcommand === 'remove' && args.reach) {
|
|
31
32
|
throw usageError('reviewers remove takes no --reach.', `Run "ekanos reviewers remove ${email}" (every reach) or add "--team <slug>" for one team's.`);
|
|
32
33
|
}
|
|
34
|
+
if (subcommand === 'remove' && !args.yes) {
|
|
35
|
+
throw invalidStateError(`Removing ${email} as a reviewer stops them deciding reply drafts${team ? ` from ${team}'s agents` : ' at every reach'}.`, `Re-run with --yes: ekanos reviewers remove ${email}${team ? ` --team ${team}` : ''} --yes`);
|
|
36
|
+
}
|
|
33
37
|
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
34
38
|
projectDir: args.dir,
|
|
35
39
|
});
|
|
36
|
-
const resolved = await resolveSourceTarget(env, requireSession(env
|
|
40
|
+
const resolved = await resolveSourceTarget(env, requireSession(env), args);
|
|
37
41
|
const client = new ReviewersClient(env, resolved.session);
|
|
38
42
|
const source = resolved.source;
|
|
39
43
|
if (subcommand === 'list') {
|
|
@@ -117,7 +121,7 @@ class ReviewersClient {
|
|
|
117
121
|
}
|
|
118
122
|
if (result.status === 'ok')
|
|
119
123
|
return result.data;
|
|
120
|
-
throw authRequiredError(`The session for ${this.env.host} could not authorize the reviewers call.`,
|
|
124
|
+
throw authRequiredError(`The session for ${this.env.host} could not authorize the reviewers call.`, loginHint(this.env));
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
function parseSubcommand(value) {
|