diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cfc5ba..d9d0094 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,6 @@ jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - node: [20, 22] - steps: - name: Checkout repository uses: actions/checkout@v4 @@ -23,10 +19,10 @@ jobs: with: version: 9 - - name: Setup Node.js ${{ matrix.node }} + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node }} + node-version: 20 cache: pnpm - name: Install dependencies diff --git a/src/commands/auth/index.ts b/src/commands/auth/index.ts index 380f7a9..865f5f7 100644 --- a/src/commands/auth/index.ts +++ b/src/commands/auth/index.ts @@ -1,4 +1,3 @@ export { loginCommand } from './login.js'; export { logoutCommand } from './logout.js'; export { whoamiCommand } from './whoami.js'; -export { signupCommand } from './signup.js'; diff --git a/src/commands/auth/signup.tsx b/src/commands/auth/signup.tsx deleted file mode 100644 index 20d80f5..0000000 --- a/src/commands/auth/signup.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { Box, Text, useApp } from 'ink'; -import { Select, Spinner, StatusMessage } from '@inkjs/ui'; -import { Command } from 'commander'; -import { App } from '../../components/App.js'; -import { KeyValue } from '../../components/KeyValue.js'; -import { setToken, setApiUrl, setWorkspaceId, setProjectId, getApiUrl } from '../../lib/config.js'; -import { createSdkClient } from '../../lib/sdk-client.js'; -import { fetchWorkspacesWithProjects } from '../../lib/workspaces.js'; -import type { Project, WorkspaceWithProjects } from '../../lib/types.js'; -import { openWithFallback } from '../../lib/browser.js'; -import { - getSignupWithCliAuthUrl, - getWorkspaceCreateUrl, - getProjectCreateUrl, -} from '../../lib/urls.js'; -import { startAuthServer, type AuthServer } from '../../lib/auth-server.js'; -import { renderCommand, jsonError, stdoutWrite, setUsageError } from '../../lib/render.js'; -import { isNonInteractive, isJsonMode } from '../../lib/cli-flags.js'; - -type Step = - | 'browserAuth' - | 'verifying' - | 'verified' - | 'selectWorkspace' - | 'selectProject' - | 'done' - | 'error'; - -interface SignupCommandProps { - token?: string; - skipContext?: boolean; - skipVerify?: boolean; - workspaceId?: string; - projectId?: string; - json?: boolean; -} - -function SignupCommand({ token: tokenFlag, skipContext, skipVerify, workspaceId: wsIdFlag, projectId: projIdFlag, json: jsonProp }: SignupCommandProps): React.ReactElement { - const json = jsonProp || isJsonMode(); - const { exit } = useApp(); - const [step, setStep] = useState(() => { - if (tokenFlag) return 'verifying'; - if (isNonInteractive()) return 'error'; - return 'browserAuth'; - }); - const [token, setTokenValue] = useState(tokenFlag || ''); - const [error, setError] = useState(null); - - // Set error for NI mode without token - useEffect(() => { - if (step === 'error' && !tokenFlag && isNonInteractive() && !error) { - setUsageError(); - setError('Signup requires a browser. In non-interactive mode, use: unlayer login --token '); - } - }, [step, tokenFlag, error]); - const [workspaces, setWorkspaces] = useState([]); - const [selectedWorkspace, setSelectedWorkspace] = useState(null); - const [selectedProject, setSelectedProject] = useState(null); - const [authUrl, setAuthUrl] = useState(''); - const authServerRef = useRef(null); - - // Step: Browser auth — single browser open does signup + auth via returnUrl chain - useEffect(() => { - if (step !== 'browserAuth') return; - - let cancelled = false; - - (async () => { - try { - const server = await startAuthServer(); - if (cancelled) { - server.close(); - return; - } - - authServerRef.current = server; - - // Single browser open: signup page with returnUrl → cli-auth → localhost callback - const url = getSignupWithCliAuthUrl(server.port, server.state); - setAuthUrl(url); - - await openWithFallback(url); - - const result = await server.waitForAuth(); - if (cancelled) return; - - setTokenValue(result.token); - setStep('verifying'); - server.close(); - } catch (err) { - if (cancelled) return; - const msg = err instanceof Error ? err.message : 'Authentication failed'; - setError(msg); - setStep('error'); - } - })(); - - return () => { - cancelled = true; - authServerRef.current?.close(); - }; - }, [step]); - - // Verify token - useEffect(() => { - if (step === 'verifying' && token) { - if (skipVerify) { - setToken(token); - setApiUrl(getApiUrl()); - setStep('done'); - return; - } - const apiUrl = getApiUrl(); - const client = createSdkClient(token, apiUrl); - fetchWorkspacesWithProjects(client) - .then((detailed) => { - setWorkspaces(detailed); - setToken(token); - setApiUrl(apiUrl); - setStep('verified'); - }) - .catch((err) => { - const msg = err instanceof Error ? err.message : 'Verification failed'; - if (msg.includes('401') || msg.includes('Unauthorized')) { - setError('Invalid token. Please check your token and try again.'); - } else { - setError(msg); - } - setStep('error'); - }); - } - }, [step, token]); - - // After verified, decide next step — auto-select when possible - useEffect(() => { - if (step !== 'verified') return; - - if (skipContext) { - setStep('done'); - return; - } - - if (workspaces.length === 0) { - setStep('done'); - return; - } - - // Auto-select workspace: flag > single workspace > interactive - let ws: WorkspaceWithProjects | undefined; - if (wsIdFlag) { - const id = parseInt(wsIdFlag, 10); - ws = workspaces.find((w) => w.id === id); - if (!ws) { - setError(`Workspace ID ${wsIdFlag} not found. Available: ${workspaces.map((w) => `${w.id} (${w.name})`).join(', ')}`); - setStep('error'); - return; - } - } else if (workspaces.length === 1) { - ws = workspaces[0]; - } - - if (ws) { - setSelectedWorkspace(ws); - setWorkspaceId(ws.id!); - setProjectId(null); - - const projects = ws.projects || []; - - // Auto-select project: flag > single project > interactive - if (projIdFlag) { - const id = parseInt(projIdFlag, 10); - const proj = projects.find((p) => p.id === id); - if (!proj) { - setError(`Project ID ${projIdFlag} not found in workspace "${ws.name}". Available: ${projects.map((p) => `${p.id} (${p.name})`).join(', ') || 'none'}`); - setStep('error'); - return; - } - setSelectedProject(proj); - setProjectId(proj.id!); - setStep('done'); - } else if (projects.length === 1) { - setSelectedProject(projects[0]); - setProjectId(projects[0].id!); - setStep('done'); - } else if (projects.length > 1) { - if (isNonInteractive()) { - setUsageError(); - setError(`Non-interactive mode: use --project-id to select a project. Available: ${projects.map((p) => `${p.id} (${p.name})`).join(', ')}`); - setStep('error'); - return; - } - setStep('selectProject'); - } else { - setStep('done'); - } - } else { - if (isNonInteractive()) { - setUsageError(); - setError(`Non-interactive mode: use --workspace-id to select a workspace. Available: ${workspaces.map((w) => `${w.id} (${w.name})`).join(', ')}`); - setStep('error'); - return; - } - setStep('selectWorkspace'); - } - }, [step, skipContext, workspaces, wsIdFlag, projIdFlag]); - - const handleWorkspaceSelect = useCallback((value: string) => { - if (value === '__create__') { - openWithFallback(getWorkspaceCreateUrl()); - return; - } - const wsId = parseInt(value, 10); - const ws = workspaces.find((w) => w.id === wsId); - if (ws) { - setSelectedWorkspace(ws); - setWorkspaceId(wsId); - setProjectId(null); - if (ws.projects && ws.projects.length > 0) { - setStep('selectProject'); - } else { - setStep('done'); - } - } - }, [workspaces]); - - const handleProjectSelect = useCallback((value: string) => { - if (value === '__create__' && selectedWorkspace) { - openWithFallback( - getProjectCreateUrl(selectedWorkspace.id!), - ); - return; - } - const projId = parseInt(value, 10); - const proj = selectedWorkspace?.projects?.find((p) => p.id === projId); - if (proj) { - setSelectedProject(proj); - setProjectId(projId); - setStep('done'); - } - }, [selectedWorkspace]); - - // JSON output on success - useEffect(() => { - if (step === 'done' && json) { - stdoutWrite(JSON.stringify({ - status: 'authenticated', - apiUrl: getApiUrl(), - workspace: selectedWorkspace ? { id: selectedWorkspace.id, name: selectedWorkspace.name } : null, - project: selectedProject ? { id: selectedProject.id, name: selectedProject.name } : null, - }, null, 2)); - } - }, [step, json, selectedWorkspace, selectedProject]); - - // Auto-exit after done - useEffect(() => { - if (step === 'done') { - const timer = setTimeout(() => exit(), 100); - return () => clearTimeout(timer); - } - if (step === 'error' && error) { - jsonError(error); - const timer = setTimeout(() => exit(new Error(error)), 100); - return () => clearTimeout(timer); - } - return; - }, [step, exit, error]); - - return ( - - {/* Step: Browser auth — single open for signup + auth */} - {step === 'browserAuth' && ( - - - Opening browser for signup... - - - {authUrl ? ( - <> - - If the browser didn't open, visit: - - - {authUrl} - - - ) : null} - - )} - - {/* Step: Verifying */} - {step === 'verifying' && ( - - )} - - {/* Step: Select workspace */} - {step === 'selectWorkspace' && !isNonInteractive() && ( - - Credentials verified - - Select a workspace: - - ({ - label: proj.name || 'Unnamed', - value: String(proj.id), - })), - { - label: '+ Create new project (opens browser)', - value: '__create__', - }, - ]} - onChange={handleProjectSelect} - /> - - )} - - {/* Step: Done */} - {step === 'done' && ( - - Credentials verified - {selectedWorkspace && selectedProject && ( - - - Context Updated - - - - )} - - Welcome to Unlayer! - - - )} - - {/* Step: Error */} - {step === 'error' && error && ( - - {error} - - - You can also authenticate manually: unlayer login --manual - - - - )} - - ); -} - -export const signupCommand = new Command('signup') - .description('Create an Unlayer account and connect your CLI') - .option('-t, --token ', 'Personal Access Token (skip signup, just authenticate)') - .option('--workspace-id ', 'Auto-select workspace by ID (agent-friendly)') - .option('--project-id ', 'Auto-select project by ID (agent-friendly)') - .option('--skip-context', 'Skip workspace/project selection') - .option('--skip-verify', 'Skip token verification (for local development)') - .option('--json', 'Output as JSON') - .action((options) => { - const { waitUntilExit } = renderCommand( - , - ); - waitUntilExit().catch((err) => { - if (err) { - process.exit(process.exitCode || 1); - } - }); - }); diff --git a/src/index.ts b/src/index.ts index cf66cb1..3fc7ef4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,6 @@ import { loginCommand, logoutCommand, whoamiCommand, - signupCommand, } from './commands/auth/index.js'; // Resource commands @@ -64,8 +63,7 @@ const formatHelp = () => { // Authentication console.log(` ${c.yellow('Authentication')}`); - console.log(` ${c.white('signup')} ${c.dim('Create an account and connect your CLI')}`); - console.log(` ${c.white('login')} ${c.dim('Authenticate with your Personal Access Token')}`); + console.log(` ${c.white('login')} ${c.dim('Authenticate with your Unlayer account')}`); console.log(` ${c.white('logout')} ${c.dim('Clear stored credentials')}`); console.log(` ${c.white('whoami')} ${c.dim('Show current user info')}`); console.log(); @@ -93,9 +91,8 @@ const formatHelp = () => { // Quick examples console.log(` ${c.yellow('Quick Start')}`); - console.log(` ${c.dim('$')} ${c.cyan('unlayer signup')} ${c.dim('# Create account')}`); - console.log(` ${c.dim('$')} ${c.cyan('unlayer init my-app')} ${c.dim('# Create new project')}`); console.log(` ${c.dim('$')} ${c.cyan('unlayer login')} ${c.dim('# Authenticate')}`); + console.log(` ${c.dim('$')} ${c.cyan('unlayer init my-app')} ${c.dim('# Create new project')}`); console.log(` ${c.dim('$')} ${c.cyan('unlayer template list')} ${c.dim('# List templates')}`); console.log(` ${c.dim('$')} ${c.cyan('unlayer pull')} ${c.dim('# Pull all templates locally')}`); console.log(` ${c.dim('$')} ${c.cyan('unlayer diff ./template.json')} ${c.dim('# Compare local vs remote')}`); @@ -117,7 +114,6 @@ program .addHelpCommand('help [command]', 'Display help for a command'); // Authentication commands (top-level) -program.addCommand(signupCommand); program.addCommand(loginCommand); program.addCommand(logoutCommand); program.addCommand(whoamiCommand); diff --git a/src/lib/__tests__/config.test.ts b/src/lib/__tests__/config.test.ts index a4ecf4e..00751fb 100644 --- a/src/lib/__tests__/config.test.ts +++ b/src/lib/__tests__/config.test.ts @@ -58,28 +58,10 @@ import { getApiUrl, getConsoleUrl, getAccountsUrl, - getEnvironments, - getActiveEnvironment, - setActiveEnvironment, - addEnvironment, - removeEnvironment, - getActiveEnvironmentUrls, } from '../config.js'; // ── Helpers ────────────────────────────────────────────────────────────────── -const DEV_ENV = { - apiUrl: 'https://api.dev.unlayer.com', - consoleUrl: 'https://console.dev.unlayer.com', - accountsUrl: 'https://accounts.dev.unlayer.com', -}; - -const STAGING_ENV = { - apiUrl: 'https://api.staging.unlayer.com', - consoleUrl: 'https://console.staging.unlayer.com', - accountsUrl: 'https://accounts.staging.unlayer.com', -}; - let savedEnv: NodeJS.ProcessEnv; beforeEach(() => { @@ -97,217 +79,49 @@ afterEach(() => { // ───────────────────────────────────────────────────────────────────────────── describe('getApiUrl()', () => { + it('returns default prod URL when nothing is configured', () => { + expect(getApiUrl()).toBe(DEFAULT_API_URL); + }); + it('returns env var when UNLAYER_API_URL is set', () => { process.env.UNLAYER_API_URL = 'https://custom-api.example.com'; expect(getApiUrl()).toBe('https://custom-api.example.com'); }); - it('returns active environment URL when no env var', () => { - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - expect(getApiUrl()).toBe('https://api.dev.unlayer.com'); - }); - - it('returns stored config apiUrl when no env var and no active env', () => { + it('returns stored config apiUrl when no env var', () => { store.set('apiUrl', 'https://stored-api.example.com'); expect(getApiUrl()).toBe('https://stored-api.example.com'); }); - - it('returns default when nothing is configured', () => { - expect(getApiUrl()).toBe(DEFAULT_API_URL); - }); - - it('env var wins over active environment', () => { - process.env.UNLAYER_API_URL = 'https://env-var-api.example.com'; - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - expect(getApiUrl()).toBe('https://env-var-api.example.com'); - }); }); describe('getConsoleUrl()', () => { + it('returns default prod URL when nothing is configured', () => { + expect(getConsoleUrl()).toBe('https://console.unlayer.com'); + }); + it('returns env var when UNLAYER_CONSOLE_URL is set', () => { process.env.UNLAYER_CONSOLE_URL = 'https://custom-console.example.com'; expect(getConsoleUrl()).toBe('https://custom-console.example.com'); }); - it('returns active environment URL when no env var', () => { - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - expect(getConsoleUrl()).toBe('https://console.dev.unlayer.com'); - }); - - it('returns default when nothing is configured', () => { - expect(getConsoleUrl()).toBe('https://console.unlayer.com'); - }); - it('strips trailing slash from env var', () => { process.env.UNLAYER_CONSOLE_URL = 'https://console.example.com///'; expect(getConsoleUrl()).toBe('https://console.example.com'); }); - - it('strips trailing slash from active environment URL', () => { - addEnvironment('dev', { - ...DEV_ENV, - consoleUrl: 'https://console.dev.unlayer.com/', - }); - setActiveEnvironment('dev'); - expect(getConsoleUrl()).toBe('https://console.dev.unlayer.com'); - }); }); describe('getAccountsUrl()', () => { + it('returns default prod URL when nothing is configured', () => { + expect(getAccountsUrl()).toBe('https://accounts.unlayer.com'); + }); + it('returns env var when UNLAYER_ACCOUNTS_URL is set', () => { process.env.UNLAYER_ACCOUNTS_URL = 'https://custom-accounts.example.com'; expect(getAccountsUrl()).toBe('https://custom-accounts.example.com'); }); - it('returns active environment URL when no env var', () => { - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - expect(getAccountsUrl()).toBe('https://accounts.dev.unlayer.com'); - }); - - it('returns default when nothing is configured', () => { - expect(getAccountsUrl()).toBe('https://accounts.unlayer.com'); - }); - it('strips trailing slash from env var', () => { process.env.UNLAYER_ACCOUNTS_URL = 'https://accounts.example.com/'; expect(getAccountsUrl()).toBe('https://accounts.example.com'); }); - - it('strips trailing slash from active environment URL', () => { - addEnvironment('dev', { - ...DEV_ENV, - accountsUrl: 'https://accounts.dev.unlayer.com/', - }); - setActiveEnvironment('dev'); - expect(getAccountsUrl()).toBe('https://accounts.dev.unlayer.com'); - }); -}); - -// ───────────────────────────────────────────────────────────────────────────── -// Environment CRUD -// ───────────────────────────────────────────────────────────────────────────── - -describe('addEnvironment()', () => { - it('stores an environment retrievable via getEnvironments()', () => { - addEnvironment('dev', DEV_ENV); - const envs = getEnvironments(); - expect(envs.dev).toMatchObject(DEV_ENV); - }); - - it('overwrites an existing environment with the same name', () => { - addEnvironment('dev', DEV_ENV); - const updated = { - apiUrl: 'https://api.dev2.unlayer.com', - consoleUrl: 'https://console.dev2.unlayer.com', - accountsUrl: 'https://accounts.dev2.unlayer.com', - }; - addEnvironment('dev', updated); - const envs = getEnvironments(); - expect(envs.dev).toMatchObject(updated); - }); -}); - -describe('removeEnvironment()', () => { - it('returns true and removes an existing environment', () => { - addEnvironment('dev', DEV_ENV); - expect(removeEnvironment('dev')).toBe(true); - expect(getEnvironments().dev).toBeUndefined(); - }); - - it('returns false for a non-existent environment', () => { - expect(removeEnvironment('nope')).toBe(false); - }); - - it('clears active environment when the active one is removed', () => { - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - removeEnvironment('dev'); - expect(getActiveEnvironment()).toBeNull(); - }); -}); - -describe('setActiveEnvironment()', () => { - it('persists the active environment name', () => { - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - expect(getActiveEnvironment()).toBe('dev'); - }); - - it('clears active environment when set to null', () => { - setActiveEnvironment('dev'); - setActiveEnvironment(null); - expect(getActiveEnvironment()).toBeNull(); - }); -}); - -// ───────────────────────────────────────────────────────────────────────────── -// Environment Merge (getEnvironments) -// ───────────────────────────────────────────────────────────────────────────── - -describe('getEnvironments()', () => { - it('returns global environments with source "global"', () => { - addEnvironment('dev', DEV_ENV); - const envs = getEnvironments(); - expect(envs.dev.source).toBe('global'); - }); - - it('returns project environments with source "project"', () => { - mockProjectConfig = { - environments: { staging: STAGING_ENV }, - }; - const envs = getEnvironments(); - expect(envs.staging).toMatchObject({ ...STAGING_ENV, source: 'project' }); - }); - - it('global overrides project when both have the same name', () => { - mockProjectConfig = { - environments: { dev: STAGING_ENV }, - }; - addEnvironment('dev', DEV_ENV); - const envs = getEnvironments(); - expect(envs.dev.apiUrl).toBe(DEV_ENV.apiUrl); - expect(envs.dev.source).toBe('global'); - }); - - it('skips malformed entries missing required fields', () => { - mockProjectConfig = { - environments: { - good: STAGING_ENV, - bad: { apiUrl: 'https://api.bad.com' }, // missing consoleUrl, accountsUrl - }, - }; - const envs = getEnvironments(); - expect(envs.good).toBeDefined(); - expect(envs.bad).toBeUndefined(); - }); - - it('returns empty object when no environments exist', () => { - expect(getEnvironments()).toEqual({}); - }); -}); - -// ───────────────────────────────────────────────────────────────────────────── -// getActiveEnvironmentUrls() -// ───────────────────────────────────────────────────────────────────────────── - -describe('getActiveEnvironmentUrls()', () => { - it('returns EnvironmentUrls when an active environment exists', () => { - addEnvironment('dev', DEV_ENV); - setActiveEnvironment('dev'); - expect(getActiveEnvironmentUrls()).toEqual(DEV_ENV); - }); - - it('returns null when no active environment is set', () => { - addEnvironment('dev', DEV_ENV); - expect(getActiveEnvironmentUrls()).toBeNull(); - }); - - it('returns null when active environment references a deleted env', () => { - store.set('activeEnvironment', 'deleted'); - expect(getActiveEnvironmentUrls()).toBeNull(); - }); }); diff --git a/src/lib/__tests__/project-config.test.ts b/src/lib/__tests__/project-config.test.ts index 55ebfcd..5d6d152 100644 --- a/src/lib/__tests__/project-config.test.ts +++ b/src/lib/__tests__/project-config.test.ts @@ -95,21 +95,6 @@ describe('loadProjectConfig()', () => { expect((result as Record).$schema).toBeUndefined(); }); - it('includes environments field', () => { - const envData = { - environments: { - dev: { - apiUrl: 'https://api.dev.unlayer.com', - consoleUrl: 'https://console.dev.unlayer.com', - accountsUrl: 'https://accounts.dev.unlayer.com', - }, - }, - }; - writeConfig(tmpDir, JSON.stringify(envData)); - const result = loadProjectConfig(tmpDir); - expect(result?.environments?.dev).toEqual(envData.environments.dev); - }); - it('returns null and warns for array JSON', () => { writeConfig(tmpDir, '[1, 2, 3]'); const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); diff --git a/src/lib/auth-server.ts b/src/lib/auth-server.ts index a4a243f..ce569a0 100644 --- a/src/lib/auth-server.ts +++ b/src/lib/auth-server.ts @@ -14,71 +14,338 @@ export interface AuthServer { close: () => void; } -const SUCCESS_HTML = ` - +const SUCCESS_HTML = (email: string) => ` + - Unlayer CLI + + + Unlayer CLI — Authenticated -
-
-

Authenticated!

-

You can close this tab and return to the terminal.

+
+
+ + +
+ + + + +
+ +

Authenticated!

+ ${email ? `` : '
'} +
+

You can close this tab and return to the terminal.

+
+ + Unlayer CLI
`; const ERROR_HTML = (message: string) => ` - + - Unlayer CLI - Error + + + Unlayer CLI — Error -
-
-

Authentication Failed

-

${escapeHtml(message)}

+
+
+ + +
+ + + + + +
+ +

Authentication Failed

+

${escapeHtml(message)}

+
+

Please return to the terminal and try again.

+
+ + Unlayer CLI
`; @@ -154,7 +421,7 @@ export async function startAuthServer(options?: { // Success - send HTML response and resolve res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(SUCCESS_HTML); + res.end(SUCCESS_HTML(escapeHtml(callbackEmail))); clearTimeout(timeoutId); const parsedTokenId = callbackTokenId ? parseInt(callbackTokenId, 10) : null; diff --git a/src/lib/urls.ts b/src/lib/urls.ts index 97c140f..56d81fe 100644 --- a/src/lib/urls.ts +++ b/src/lib/urls.ts @@ -38,12 +38,6 @@ export function getLoginWithCliAuthUrl(port: number, state: string, tokenId?: nu return `${getAccountsUrl()}/?returnUrl=${encodeURIComponent(cliAuthUrl)}`; } -/** Accounts signup page with returnUrl pointing to cli-auth (chains: signup → PAT → localhost) */ -export function getSignupWithCliAuthUrl(port: number, state: string): string { - const cliAuthUrl = getCliAuthUrl(port, state); - return `${getAccountsUrl()}/signup?returnUrl=${encodeURIComponent(cliAuthUrl)}`; -} - /** Console template editor page */ export function getTemplateEditorUrl( projectId: number | string,