mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
Issue #1161. Sweeps 127 files to import from src/config/config-loader-facade.ts instead of unified-config-loader or utils/config-manager directly. WRITE callers (32 files): replaced raw saveUnifiedConfig / mutateUnifiedConfig / updateUnifiedConfig calls with the facade's cache-coherent wrappers saveConfig / mutateConfig / updateConfig. This fixes a latent stale-cache window where direct writes through the underlying loader bypassed the facade's memoization. READ callers (95 files): mechanical import-path migration only — function names unchanged because the facade re-exports them. No behavior change. Also updated: - tests/unit/utils/browser/browser-setup.test.ts (DI interface rename) - src/management/checks/image-analysis-check.ts (dynamic import rename) - src/web-server/health-service.ts (dynamic require rename) - src/ccs.ts (path prefix fix from sweep script) After sweep: zero raw write callers remain outside src/config/. Direct imports of config-manager remain only for symbols not in the facade (getConfigPath, getCcsDirSource, etc). Behavior unchanged; full suite passes 1824/1824. Out of scope: switching loadOrCreateUnifiedConfig() callers to getCachedConfig() — needs per-callsite cache-safety analysis. Tracked as follow-up. Refs #1161
261 lines
8.0 KiB
TypeScript
261 lines
8.0 KiB
TypeScript
/**
|
|
* Config Command Handler
|
|
*
|
|
* Launches web-based configuration dashboard.
|
|
* Ensures CLIProxy service is running for dashboard features.
|
|
* Usage: ccs config [--port PORT] [--host HOST] [--dev]
|
|
*/
|
|
|
|
import getPort from 'get-port';
|
|
import open from 'open';
|
|
import { startServer } from '../web-server';
|
|
import { setupGracefulShutdown } from '../web-server/shutdown';
|
|
import { ensureCliproxyService } from '../cliproxy/service-manager';
|
|
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/config-generator';
|
|
|
|
import { initUI, header, ok, info, warn, fail } from '../utils/ui';
|
|
import { resolveNamedCommand, type NamedCommandRoute } from './named-command-router';
|
|
import {
|
|
isLoopbackHost,
|
|
isWildcardHost,
|
|
normalizeDashboardHost,
|
|
resolveDashboardUrls,
|
|
} from './config-dashboard-host';
|
|
import { parseConfigCommandArgs, showConfigCommandHelp } from './config-command-options';
|
|
import { createLogger } from '../services/logging';
|
|
import { getDashboardAuthConfig } from '../config/config-loader-facade';
|
|
|
|
const logger = createLogger('command:config');
|
|
|
|
const CONFIG_SUBCOMMAND_ROUTES: readonly NamedCommandRoute[] = [
|
|
{
|
|
name: 'channels',
|
|
handle: async (args) => {
|
|
const { handleConfigChannelsCommand } = await import('./config-channels-command');
|
|
await handleConfigChannelsCommand(args);
|
|
},
|
|
},
|
|
{
|
|
name: 'auth',
|
|
handle: async (args) => {
|
|
const { handleConfigAuthCommand } = await import('./config-auth');
|
|
await handleConfigAuthCommand(args);
|
|
},
|
|
},
|
|
{
|
|
name: 'image-analysis',
|
|
handle: async (args) => {
|
|
const { handleConfigImageAnalysisCommand } = await import('./config-image-analysis-command');
|
|
await handleConfigImageAnalysisCommand(args);
|
|
},
|
|
},
|
|
{
|
|
name: 'thinking',
|
|
handle: async (args) => {
|
|
const { handleConfigThinkingCommand } = await import('./config-thinking-command');
|
|
await handleConfigThinkingCommand(args);
|
|
},
|
|
},
|
|
];
|
|
|
|
interface ConfigCommandDependencies {
|
|
getPort: typeof getPort;
|
|
openBrowser: typeof open;
|
|
startServer: typeof startServer;
|
|
setupGracefulShutdown: typeof setupGracefulShutdown;
|
|
ensureCliproxyService: typeof ensureCliproxyService;
|
|
getDashboardAuthConfig: typeof getDashboardAuthConfig;
|
|
initUI: typeof initUI;
|
|
header: typeof header;
|
|
ok: typeof ok;
|
|
info: typeof info;
|
|
warn: typeof warn;
|
|
fail: typeof fail;
|
|
resolveNamedCommand: typeof resolveNamedCommand;
|
|
configSubcommandRoutes: readonly NamedCommandRoute[];
|
|
}
|
|
|
|
const defaultConfigCommandDependencies: ConfigCommandDependencies = {
|
|
getPort,
|
|
openBrowser: open,
|
|
startServer,
|
|
setupGracefulShutdown,
|
|
ensureCliproxyService,
|
|
getDashboardAuthConfig,
|
|
initUI,
|
|
header,
|
|
ok,
|
|
info,
|
|
warn,
|
|
fail,
|
|
resolveNamedCommand,
|
|
configSubcommandRoutes: CONFIG_SUBCOMMAND_ROUTES,
|
|
};
|
|
|
|
/**
|
|
* Handle config command
|
|
*/
|
|
export async function handleConfigCommand(
|
|
args: string[],
|
|
deps: ConfigCommandDependencies = defaultConfigCommandDependencies
|
|
): Promise<void> {
|
|
if (args.length === 1 && args[0] === 'help') {
|
|
await deps.initUI();
|
|
showConfigCommandHelp();
|
|
process.exit(0);
|
|
}
|
|
|
|
const subcommand = args[0]?.startsWith('-')
|
|
? undefined
|
|
: deps.resolveNamedCommand(args[0], deps.configSubcommandRoutes);
|
|
if (subcommand) {
|
|
await subcommand.handle(args.slice(1));
|
|
return;
|
|
}
|
|
|
|
await deps.initUI();
|
|
|
|
const parsed = parseConfigCommandArgs(args);
|
|
if (parsed.help) {
|
|
showConfigCommandHelp();
|
|
process.exit(0);
|
|
}
|
|
if (parsed.error) {
|
|
console.error(deps.fail(parsed.error));
|
|
process.exit(1);
|
|
}
|
|
|
|
const options = parsed.options;
|
|
const verbose = options.dev;
|
|
logger.info('dashboard.launch_requested', 'Config dashboard launch requested', {
|
|
dev: Boolean(options.dev),
|
|
host: options.host || null,
|
|
port: options.port || null,
|
|
});
|
|
|
|
console.log(deps.header('CCS Config Dashboard'));
|
|
console.log('');
|
|
|
|
// Ensure CLIProxy service is running for dashboard features
|
|
console.log(deps.info('Starting CLIProxy service...'));
|
|
const cliproxyResult = await deps.ensureCliproxyService(CLIPROXY_DEFAULT_PORT, verbose);
|
|
logger.info('cliproxy.ensure_result', 'Config command checked CLIProxy availability', {
|
|
started: cliproxyResult.started,
|
|
alreadyRunning: cliproxyResult.alreadyRunning,
|
|
configRegenerated: cliproxyResult.configRegenerated,
|
|
port: cliproxyResult.port || null,
|
|
error: cliproxyResult.error || null,
|
|
});
|
|
|
|
if (cliproxyResult.started) {
|
|
if (cliproxyResult.alreadyRunning) {
|
|
console.log(deps.ok(`CLIProxy already running on port ${cliproxyResult.port}`));
|
|
if (cliproxyResult.configRegenerated) {
|
|
console.log(deps.warn('Config updated - restart CLIProxy to apply changes'));
|
|
}
|
|
} else {
|
|
console.log(deps.ok(`CLIProxy started on port ${cliproxyResult.port}`));
|
|
}
|
|
} else {
|
|
console.log(deps.warn(`CLIProxy not available: ${cliproxyResult.error}`));
|
|
console.log(deps.info('Dashboard will work but Control Panel/Stats may be limited'));
|
|
}
|
|
console.log('');
|
|
|
|
console.log(deps.info('Starting dashboard server...'));
|
|
|
|
// Find available port
|
|
const port =
|
|
options.port ??
|
|
(await deps.getPort({
|
|
port: [3000, 3001, 3002, 8000, 8080],
|
|
}));
|
|
|
|
try {
|
|
// Start server
|
|
const serverOptions: Parameters<typeof startServer>[0] = {
|
|
port,
|
|
dev: options.dev,
|
|
};
|
|
if (options.hostProvided && options.host) {
|
|
serverOptions.host = normalizeDashboardHost(options.host);
|
|
}
|
|
|
|
const { server, wss, cleanup } = await deps.startServer(serverOptions);
|
|
|
|
// Setup graceful shutdown
|
|
deps.setupGracefulShutdown(server, wss, cleanup);
|
|
|
|
const urls = resolveDashboardUrls(resolveServerBindHost(server) ?? options.host, port);
|
|
const shouldWarnAboutExposure = urls.bindHost ? !isLoopbackHost(urls.bindHost) : false;
|
|
|
|
if (options.dev) {
|
|
console.log(deps.ok(`Dev Server: ${urls.browserUrl}`));
|
|
console.log('');
|
|
console.log(deps.info('HMR enabled - UI changes will hot-reload'));
|
|
} else {
|
|
console.log(deps.ok(`Dashboard: ${urls.browserUrl}`));
|
|
}
|
|
|
|
if (shouldWarnAboutExposure && urls.bindHost) {
|
|
console.log(deps.info(`Bind host: ${urls.bindHost}`));
|
|
if (urls.networkUrls?.length === 1) {
|
|
console.log(deps.info(`Network URL: ${urls.networkUrls[0]}`));
|
|
} else if (urls.networkUrls && urls.networkUrls.length > 1) {
|
|
console.log(deps.info('Network URLs:'));
|
|
for (const networkUrl of urls.networkUrls) {
|
|
console.log(deps.info(` ${networkUrl}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldWarnAboutExposure && urls.bindHost) {
|
|
const authConfig = deps.getDashboardAuthConfig();
|
|
console.log(
|
|
deps.warn('Dashboard may be reachable from other devices that can connect to this machine.')
|
|
);
|
|
if (!authConfig.enabled) {
|
|
console.log(deps.info('Protect it before sharing: ccs config auth setup'));
|
|
}
|
|
if (isWildcardHost(urls.bindHost) && !urls.networkUrls?.length) {
|
|
console.log(deps.info('Use your machine IP or hostname from the other device.'));
|
|
}
|
|
}
|
|
console.log('');
|
|
|
|
// Open browser
|
|
try {
|
|
await deps.openBrowser(urls.browserUrl, { wait: false });
|
|
logger.info('dashboard.browser_opened', 'Config dashboard browser launch attempted', {
|
|
browserUrl: urls.browserUrl,
|
|
});
|
|
console.log(deps.info('Browser opened automatically'));
|
|
} catch {
|
|
logger.warn('dashboard.browser_open_failed', 'Automatic browser launch failed', {
|
|
browserUrl: urls.browserUrl,
|
|
});
|
|
console.log(deps.info(`Open manually: ${urls.browserUrl}`));
|
|
}
|
|
|
|
console.log('');
|
|
console.log(deps.info('Press Ctrl+C to stop'));
|
|
} catch (error) {
|
|
logger.error('dashboard.launch_failed', 'Config dashboard failed to launch', {
|
|
message: (error as Error).message,
|
|
});
|
|
console.error(deps.fail(`Failed to start server: ${(error as Error).message}`));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function resolveServerBindHost(server: {
|
|
address(): string | { address: string } | null;
|
|
}): string | undefined {
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
return undefined;
|
|
}
|
|
|
|
return address.address;
|
|
}
|