From 04dfe7d64a93ceb74ecf48bf421bb2526e1afe78 Mon Sep 17 00:00:00 2001 From: hbrooks Date: Tue, 28 Jul 2026 13:05:25 -0400 Subject: [PATCH] install: open the dashboard sign-in page from the CLI Onboarding starts on the web, since the GitHub App install lives there. Give callers one command that gets them to the right page instead of a URL they have to be told. The link is built from the active host's app base, so a beta or self-hosted CLI does not send its user to prod. --- README.md | 1 + skills/ellipsis/SKILL.md | 7 ++++--- src/cli.tsx | 2 ++ src/commands/install.ts | 19 +++++++++++++++++++ src/lib/help.ts | 2 +- src/lib/urls.ts | 7 +++++++ test/urls.test.ts | 14 +++++++++++++- 7 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 src/commands/install.ts diff --git a/README.md b/README.md index b12233d..8103efa 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ skills: ## Usage ```sh +agent install # open the dashboard sign-in page, where you install Ellipsis agent login # device-code auth against the active host agent logout # remove stored credentials (--all for every host) agent me # show the current credential's identity diff --git a/skills/ellipsis/SKILL.md b/skills/ellipsis/SKILL.md index 9d8f1b3..95eba16 100644 --- a/skills/ellipsis/SKILL.md +++ b/skills/ellipsis/SKILL.md @@ -45,14 +45,15 @@ sandbox image actually builds, then deploy. The steps below are that flow. ```sh brew install ellipsis-dev/cli/agent + agent install # opens the dashboard sign-in page, where you install Ellipsis agent login # device-code flow tied to your GitHub identity agent ping # confirms the CLI can authenticate and reach the API ``` Sessions clone real repositories, so the GitHub App must be installed on - the account (app.ellipsis.dev walks through it). In CI or another headless - environment, export an API key as `ELLIPSIS_API_TOKEN` instead of logging - in. + the account. `agent install` opens the page that walks through it. In CI or + another headless environment, export an API key as `ELLIPSIS_API_TOKEN` + instead of logging in. 2. See a session run before configuring anything: diff --git a/src/cli.tsx b/src/cli.tsx index 43d89f6..5669567 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,4 +1,5 @@ import { Command } from 'commander' +import { registerInstall } from './commands/install' import { registerLogin } from './commands/login' import { registerHost } from './commands/host' import { registerMe } from './commands/me' @@ -33,6 +34,7 @@ program // rendering (sorted, alias-free, grouped at the top level). configureCliHelp(program) +registerInstall(program) registerLogin(program) registerHost(program) registerMe(program) diff --git a/src/commands/install.ts b/src/commands/install.ts new file mode 100644 index 0000000..92bb0a1 --- /dev/null +++ b/src/commands/install.ts @@ -0,0 +1,19 @@ +import type { Command } from 'commander' +import { resolveAppBase } from '../lib/config' +import { openBrowser } from '../lib/auth' +import { appLoginUrl } from '../lib/urls' + +export function registerInstall(program: Command): void { + program + .command('install') + .description('Open the dashboard sign-in page, where you install Ellipsis') + .option('--no-browser', 'print the URL without opening a browser (for headless or SSH)') + .action((opts: { browser?: boolean }) => { + const url = appLoginUrl(resolveAppBase()) + console.log('To install Ellipsis, open this URL and sign in:') + console.log(` ${url}`) + if (opts.browser !== false) { + openBrowser(url) + } + }) +} diff --git a/src/lib/help.ts b/src/lib/help.ts index a3ecdae..6ade446 100644 --- a/src/lib/help.ts +++ b/src/lib/help.ts @@ -23,7 +23,7 @@ const TOP_LEVEL_GROUPS: ReadonlyArray<{ title: string; commands: readonly string { title: 'Platform', commands: ['variable', 'asset', 'hook'] }, { title: 'Integrations', commands: ['integration', 'github', 'slack', 'linear', 'sentry'] }, { title: 'Spend', commands: ['budget', 'usage', 'analytics'] }, - { title: 'Account', commands: ['login', 'logout', 'me', 'host', 'ping'] }, + { title: 'Account', commands: ['install', 'login', 'logout', 'me', 'host', 'ping'] }, ] export function configureCliHelp(program: Command): void { diff --git a/src/lib/urls.ts b/src/lib/urls.ts index d885360..3aed24c 100644 --- a/src/lib/urls.ts +++ b/src/lib/urls.ts @@ -25,6 +25,13 @@ export function cliAuthUrl(appBase: string, userCode: string): string { return `${appBase}/cli-auth?code=${encodeURIComponent(userCode)}` } +// The dashboard sign-in page, where a new customer installs the Ellipsis +// GitHub App. Built from the active host's app base for the same reason as +// cliAuthUrl: a beta or self-hosted CLI must not send its user to prod. +export function appLoginUrl(appBase: string): string { + return `${appBase}/login` +} + // Wrap `label` in an OSC 8 hyperlink so it opens `url` on click in terminals // that support it (iTerm2, VS Code, WezTerm, kitty, GNOME); degrades to plain // text elsewhere. Only emitted to a TTY — piped output stays free of escapes. diff --git a/test/urls.test.ts b/test/urls.test.ts index 2ff1b1a..5215207 100644 --- a/test/urls.test.ts +++ b/test/urls.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cliAuthUrl, configUrl, hyperlink, sessionUrl } from '../src/lib/urls' +import { appLoginUrl, cliAuthUrl, configUrl, hyperlink, sessionUrl } from '../src/lib/urls' describe('sessionUrl', () => { it('builds the account page link with the session query param', () => { @@ -37,6 +37,18 @@ describe('cliAuthUrl', () => { }) }) +describe('appLoginUrl', () => { + it('builds the dashboard sign-in page url', () => { + expect(appLoginUrl('https://app.ellipsis.dev')).toBe('https://app.ellipsis.dev/login') + }) + + it('tracks the app base host, so a beta base yields a beta sign-in url', () => { + expect(appLoginUrl('https://beta-app.ellipsis.dev')).toBe( + 'https://beta-app.ellipsis.dev/login', + ) + }) +}) + describe('hyperlink', () => { const ESC = String.fromCharCode(27)