diff --git a/README.md b/README.md
index b3125fb0..b4861825 100644
--- a/README.md
+++ b/README.md
@@ -229,6 +229,15 @@ ccs cliproxy doctor # Check quota status for all agy accounts
**Auto-Failover**: When an Antigravity account runs out of quota, CCS automatically switches to another account with remaining capacity. Shared GCP project accounts are excluded (pooled quota).
+### CLIProxy Lifecycle
+
+```bash
+ccs cliproxy start # Start CLIProxy background service
+ccs cliproxy status # Check running status
+ccs cliproxy restart # Restart CLIProxy service
+ccs cliproxy stop # Stop running CLIProxy service
+```
+
## Configuration
diff --git a/src/cliproxy/services/index.ts b/src/cliproxy/services/index.ts
index b8fa43de..b67ed7a3 100644
--- a/src/cliproxy/services/index.ts
+++ b/src/cliproxy/services/index.ts
@@ -22,10 +22,12 @@ export {
export {
getProxyStatus,
stopProxy,
+ startProxy,
isProxyRunning,
getActiveSessionCount,
type ProxyStatusResult,
type StopProxyResult,
+ type StartProxyResult,
} from './proxy-lifecycle-service';
// Binary management
diff --git a/src/cliproxy/services/proxy-lifecycle-service.ts b/src/cliproxy/services/proxy-lifecycle-service.ts
index 7ea4c124..ecd888d9 100644
--- a/src/cliproxy/services/proxy-lifecycle-service.ts
+++ b/src/cliproxy/services/proxy-lifecycle-service.ts
@@ -2,13 +2,15 @@
* CLIProxy Proxy Lifecycle Service
*
* Handles start/stop/status operations for CLIProxy instances.
- * Delegates to session-tracker for actual process management.
+ * Delegates to session-tracker and service-manager for actual process management.
*/
import {
stopProxy as stopProxySession,
getProxyStatus as getProxyStatusSession,
} from '../session-tracker';
+import { ensureCliproxyService } from '../service-manager';
+import { CLIPROXY_DEFAULT_PORT } from '../config-generator';
/** Proxy status result */
export interface ProxyStatusResult {
@@ -27,6 +29,15 @@ export interface StopProxyResult {
error?: string;
}
+/** Start proxy result */
+export interface StartProxyResult {
+ started: boolean;
+ alreadyRunning: boolean;
+ port: number;
+ configRegenerated?: boolean;
+ error?: string;
+}
+
/**
* Get current proxy status
*/
@@ -41,6 +52,16 @@ export async function stopProxy(): Promise {
return stopProxySession();
}
+/**
+ * Start CLIProxy service (or reuse existing running instance)
+ */
+export async function startProxy(
+ port: number = CLIPROXY_DEFAULT_PORT,
+ verbose: boolean = false
+): Promise {
+ return ensureCliproxyService(port, verbose);
+}
+
/**
* Check if proxy is currently running
*/
diff --git a/src/commands/cliproxy/help-subcommand.ts b/src/commands/cliproxy/help-subcommand.ts
index fe136927..7971c701 100644
--- a/src/commands/cliproxy/help-subcommand.ts
+++ b/src/commands/cliproxy/help-subcommand.ts
@@ -61,6 +61,8 @@ export async function showHelp(): Promise {
[
'Proxy Lifecycle:',
[
+ ['start', 'Start CLIProxy instance in background'],
+ ['restart', 'Restart CLIProxy instance'],
['status', 'Show running CLIProxy status'],
['stop', 'Stop running CLIProxy instance'],
['doctor', 'Quota diagnostics and shared project detection'],
diff --git a/src/commands/cliproxy/index.ts b/src/commands/cliproxy/index.ts
index bdefb355..308f9838 100644
--- a/src/commands/cliproxy/index.ts
+++ b/src/commands/cliproxy/index.ts
@@ -20,7 +20,12 @@ import {
handleResumeAccount,
} from './quota-subcommand';
import { handleCreate, handleRemove, handleEdit } from './variant-subcommand';
-import { handleProxyStatus, handleStop } from './proxy-lifecycle-subcommand';
+import {
+ handleProxyStatus,
+ handleStart,
+ handleStop,
+ handleRestart,
+} from './proxy-lifecycle-subcommand';
import { showStatus, handleInstallVersion, handleInstallLatest } from './install-subcommand';
import { showHelp } from './help-subcommand';
import {
@@ -198,11 +203,21 @@ export async function handleCliproxyCommand(args: string[]): Promise {
}
// Proxy lifecycle commands
+ if (command === 'start') {
+ await handleStart(verbose);
+ return;
+ }
+
if (command === 'stop') {
await handleStop();
return;
}
+ if (command === 'restart') {
+ await handleRestart(verbose);
+ return;
+ }
+
if (command === 'status') {
await handleProxyStatus();
return;
diff --git a/src/commands/cliproxy/proxy-lifecycle-subcommand.ts b/src/commands/cliproxy/proxy-lifecycle-subcommand.ts
index d59319c2..c2983ecb 100644
--- a/src/commands/cliproxy/proxy-lifecycle-subcommand.ts
+++ b/src/commands/cliproxy/proxy-lifecycle-subcommand.ts
@@ -2,15 +2,68 @@
* CLIProxy Lifecycle Management
*
* Handles:
+ * - ccs cliproxy start
+ * - ccs cliproxy restart
* - ccs cliproxy status
* - ccs cliproxy stop
*/
import { initUI, header, color, dim, ok, warn, info } from '../../utils/ui';
-import { getProxyStatus, stopProxy } from '../../cliproxy/services';
+import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services';
import { detectRunningProxy } from '../../cliproxy/proxy-detector';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config-generator';
+export async function handleStart(verbose = false): Promise {
+ await initUI();
+ console.log(header('Start CLIProxy'));
+ console.log('');
+
+ const result = await startProxy(CLIPROXY_DEFAULT_PORT, verbose);
+ if (result.started) {
+ if (result.alreadyRunning) {
+ console.log(info(`CLIProxy already running on port ${result.port}`));
+ if (result.configRegenerated) {
+ console.log(warn('Config updated - restart CLIProxy to apply changes'));
+ }
+ } else {
+ console.log(ok(`CLIProxy started on port ${result.port}`));
+ }
+ console.log(dim('To stop: ccs cliproxy stop'));
+ } else {
+ console.log(warn(result.error || 'Failed to start CLIProxy'));
+ }
+ console.log('');
+}
+
+export async function handleRestart(verbose = false): Promise {
+ await initUI();
+ console.log(header('Restart CLIProxy'));
+ console.log('');
+
+ const stopResult = await stopProxy();
+ if (stopResult.stopped) {
+ console.log(ok(`CLIProxy stopped (PID ${stopResult.pid})`));
+ } else if (stopResult.error === 'No active CLIProxy session found') {
+ console.log(info('No active CLIProxy session found, starting a new instance'));
+ } else {
+ console.log(warn(stopResult.error || 'Failed to stop existing CLIProxy'));
+ console.log(info('Attempting to start a fresh instance...'));
+ }
+
+ const startResult = await startProxy(CLIPROXY_DEFAULT_PORT, verbose);
+ if (startResult.started) {
+ if (startResult.alreadyRunning) {
+ console.log(info(`CLIProxy already running on port ${startResult.port}`));
+ } else {
+ console.log(ok(`CLIProxy started on port ${startResult.port}`));
+ }
+ } else {
+ console.log(warn(startResult.error || 'Failed to restart CLIProxy'));
+ }
+
+ console.log('');
+}
+
export async function handleProxyStatus(): Promise {
await initUI();
console.log(header('CLIProxy Status'));
diff --git a/tests/unit/commands/cliproxy-command.test.js b/tests/unit/commands/cliproxy-command.test.js
index 2f7c2a97..e6fc3a0b 100644
--- a/tests/unit/commands/cliproxy-command.test.js
+++ b/tests/unit/commands/cliproxy-command.test.js
@@ -491,6 +491,18 @@ describe('CLIProxy Command - Proxy Lifecycle', () => {
});
describe('Command Routing', () => {
+ it('routes "start" subcommand correctly', () => {
+ const args = ['cliproxy', 'start'];
+ const subcommand = args[1];
+ assert.strictEqual(subcommand, 'start');
+ });
+
+ it('routes "restart" subcommand correctly', () => {
+ const args = ['cliproxy', 'restart'];
+ const subcommand = args[1];
+ assert.strictEqual(subcommand, 'restart');
+ });
+
it('routes "stop" subcommand correctly', () => {
const args = ['cliproxy', 'stop'];
const subcommand = args[1];
@@ -504,7 +516,7 @@ describe('CLIProxy Command - Proxy Lifecycle', () => {
});
it('handles unknown subcommand', () => {
- const validSubcommands = ['stop', 'status', 'create', 'list', 'remove'];
+ const validSubcommands = ['start', 'restart', 'stop', 'status', 'create', 'list', 'remove'];
const unknownCommand = 'invalid';
assert.strictEqual(validSubcommands.includes(unknownCommand), false);
});