diff --git a/src/commands/cliproxy/__tests__/proxy-lifecycle-control-panel.test.ts b/src/commands/cliproxy/__tests__/proxy-lifecycle-control-panel.test.ts new file mode 100644 index 00000000..48aa735b --- /dev/null +++ b/src/commands/cliproxy/__tests__/proxy-lifecycle-control-panel.test.ts @@ -0,0 +1,89 @@ +/** + * Tests for printControlPanelAccess: the Control Panel (API Management Center) + * URL + login-key surface shown by `ccs cliproxy status` / `start`. + * + * Why this matters: + * - The Control Panel login screen only asks for a "Management Key" with no + * hint. Users who don't know the default (`ccs`) cannot get in. This surface + * is the fix, so it must print the panel URL on the active port and the + * EFFECTIVE key (default `ccs`, or the user's custom management_secret). + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; + +import { printControlPanelAccess } from '../proxy-lifecycle-subcommand'; +import { invalidateConfigCache, mutateConfig } from '../../../config/config-loader-facade'; + +function createTestHome(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-control-panel-test-')); + const ccsDir = path.join(dir, '.ccs'); + fs.mkdirSync(path.join(ccsDir, 'cliproxy', 'auth'), { recursive: true }); + fs.writeFileSync(path.join(ccsDir, 'config.yaml'), 'version: 1\n', 'utf8'); + return dir; +} + +describe('printControlPanelAccess', () => { + let tempHome: string; + let originalCcsHome: string | undefined; + let logSpy: ReturnType; + let lines: string[]; + + beforeEach(() => { + tempHome = createTestHome(); + originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + invalidateConfigCache(); + lines = []; + logSpy = spyOn(console, 'log').mockImplementation((msg?: unknown) => { + lines.push(String(msg ?? '')); + }); + }); + + afterEach(() => { + logSpy.mockRestore(); + if (originalCcsHome === undefined) { + delete process.env.CCS_HOME; + } else { + process.env.CCS_HOME = originalCcsHome; + } + invalidateConfigCache(); + try { + fs.rmSync(tempHome, { recursive: true, force: true }); + } catch { + /* best-effort cleanup */ + } + }); + + it('prints the panel URL on the given port and the default key (ccs)', () => { + printControlPanelAccess(8317); + const out = lines.join('\n'); + expect(out).toContain('http://127.0.0.1:8317/management.html'); + expect(out).toContain('Panel login key:'); + expect(out).toContain('ccs'); + }); + + it('uses the active port in the URL', () => { + printControlPanelAccess(9000); + expect(lines.join('\n')).toContain('http://127.0.0.1:9000/management.html'); + }); + + it('prints a custom management_secret when configured', () => { + mutateConfig((config) => { + if (!config.cliproxy) { + config.cliproxy = {}; + } + if (!config.cliproxy.auth) { + config.cliproxy.auth = {}; + } + config.cliproxy.auth.management_secret = 'my-custom-key'; + }); + invalidateConfigCache(); + + printControlPanelAccess(8317); + const out = lines.join('\n'); + expect(out).toContain('Panel login key:'); + expect(out).toContain('my-custom-key'); + }); +}); diff --git a/src/commands/cliproxy/help-subcommand.ts b/src/commands/cliproxy/help-subcommand.ts index 4049a616..9fb96480 100644 --- a/src/commands/cliproxy/help-subcommand.ts +++ b/src/commands/cliproxy/help-subcommand.ts @@ -89,7 +89,10 @@ export async function showHelp(): Promise { [ ['start', 'Start CLIProxy instance in background'], ['restart', 'Restart CLIProxy instance'], - ['status [--verbose]', 'Show running CLIProxy status (--verbose adds uptime)'], + [ + 'status [--verbose]', + 'Show CLIProxy status + Control Panel URL and login key (--verbose adds uptime)', + ], ['stop', 'Stop running CLIProxy instance'], ['doctor | diag', 'Quota diagnostics and shared project detection'], ], diff --git a/src/commands/cliproxy/proxy-lifecycle-subcommand.ts b/src/commands/cliproxy/proxy-lifecycle-subcommand.ts index 7c4e4462..6794260d 100644 --- a/src/commands/cliproxy/proxy-lifecycle-subcommand.ts +++ b/src/commands/cliproxy/proxy-lifecycle-subcommand.ts @@ -12,6 +12,23 @@ import { initUI, header, color, dim, ok, warn, info } from '../../utils/ui'; import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services'; import { detectRunningProxy } from '../../cliproxy/proxy/proxy-detector'; import { resolveLifecyclePort } from '../../cliproxy/config/port-manager'; +import { getEffectiveManagementSecret } from '../../cliproxy/auth/auth-token-manager'; + +/** + * Print how to reach the local CLIProxy Control Panel (a.k.a. API Management + * Center) and the key needed to log in. + * + * The panel is served by CLIProxy at `/management.html` on the proxy port and + * its login is gated by the management secret (default `ccs`). The login screen + * only asks for a "Management Key" with no hint, so users frequently cannot get + * in. Surfacing the URL + resolved key here removes that guesswork. + */ +export function printControlPanelAccess(port: number): void { + const secret = getEffectiveManagementSecret(); + console.log(''); + console.log(` Control Panel: http://127.0.0.1:${port}/management.html`); + console.log(` Panel login key: ${secret}`); +} export async function handleStart(verbose = false): Promise { await initUI(); @@ -30,6 +47,9 @@ export async function handleStart(verbose = false): Promise { console.log(ok(`CLIProxy started on port ${result.port}`)); } console.log(dim('To stop: ccs cliproxy stop')); + if (result.port) { + printControlPanelAccess(result.port); + } } else { console.log(warn(result.error || 'Failed to start CLIProxy')); } @@ -93,6 +113,7 @@ export async function handleProxyStatus(verbose = false): Promise { console.log(''); console.log(dim('To stop: ccs cliproxy stop')); + printControlPanelAccess(status.port ?? port); } else { // Fallback: detect untracked/orphaned proxy process (e.g. detached session without lock file). const detected = await detectRunningProxy(port); @@ -109,6 +130,7 @@ export async function handleProxyStatus(verbose = false): Promise { } console.log(''); console.log(dim('To stop: ccs cliproxy stop')); + printControlPanelAccess(port); } else { console.log(` Status: ${color('Not running', 'warning')}`); console.log('');