fix(cliproxy): surface Control Panel URL and login key in status/start

This commit is contained in:
Tam Nhu Tran committed 2026-06-22 16:34:36 -04:00
1 parent 8a0031e514
commit cee9822008
3 files changed
+115 -1

No files matched your search

@@ -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<typeof spyOn>;
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');
});
});
+4 -1
View File
@@ -89,7 +89,10 @@ export async function showHelp(): Promise<void> {
[
['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'],
],
@@ -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<void> {
await initUI();
@@ -30,6 +47,9 @@ export async function handleStart(verbose = false): Promise<void> {
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<void> {
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<void> {
}
console.log('');
console.log(dim('To stop: ccs cliproxy stop'));
printControlPanelAccess(port);
} else {
console.log(` Status: ${color('Not running', 'warning')}`);
console.log('');