🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@leynier/ccst

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@leynier/ccst - npm Package Compare versions

Comparing version
0.5.3
to
0.7.0
+136
src/commands/ccs/install.ts
import { spawnSync } from "node:child_process";
import pc from "picocolors";
import { promptInput } from "../../utils/interactive.js";
type PackageManager = {
name: string;
displayCommand: string;
command: string;
args: string[];
};
const packageManagers: PackageManager[] = [
{
name: "bun",
displayCommand: "bun add -g @kaitranntt/ccs",
command: "bun",
args: ["add", "-g", "@kaitranntt/ccs"],
},
{
name: "npm",
displayCommand: "npm install -g @kaitranntt/ccs",
command: "npm",
args: ["install", "-g", "@kaitranntt/ccs"],
},
{
name: "pnpm",
displayCommand: "pnpm add -g @kaitranntt/ccs",
command: "pnpm",
args: ["add", "-g", "@kaitranntt/ccs"],
},
{
name: "yarn",
displayCommand: "yarn global add @kaitranntt/ccs",
command: "yarn",
args: ["global", "add", "@kaitranntt/ccs"],
},
];
const selectPackageManager = async (): Promise<PackageManager | undefined> => {
const lines = packageManagers.map(
(pm, index) => `${index + 1}. ${pm.name} (${pm.displayCommand})`,
);
console.log("Select package manager to install @kaitranntt/ccs:");
console.log(lines.join("\n"));
const input = await promptInput("Select option (1-4)");
const index = Number.parseInt(input || "", 10);
if (!Number.isFinite(index) || index < 1 || index > packageManagers.length) {
console.log(pc.red("Invalid selection"));
return undefined;
}
return packageManagers[index - 1];
};
const verifyInstallation = async (): Promise<string | null> => {
const result = spawnSync("ccs", ["--version"], {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
});
if (result.status !== 0) {
return null;
}
const version = result.stdout?.trim();
return version && version.length > 0 ? version : null;
};
const promptForSetup = async (): Promise<boolean> => {
const response = await promptInput("Run ccs setup now? (y/n)");
return response?.toLowerCase() === "y";
};
export const ccsInstallCommand = async (): Promise<void> => {
// Step 1: Select package manager
const selectedPm = await selectPackageManager();
if (!selectedPm) {
console.log(pc.dim("Installation cancelled"));
return;
}
// Step 2: Execute installation
console.log(
pc.dim(
`Installing @kaitranntt/ccs using ${selectedPm.name}... (this may take a moment)`,
),
);
const installResult = spawnSync(selectedPm.command, selectedPm.args, {
stdio: "inherit",
});
if (installResult.status !== 0) {
console.log(
pc.red(
`Error: Installation failed with exit code ${installResult.status}`,
),
);
return;
}
// Step 3: Verify installation
console.log(pc.dim("Verifying installation..."));
const version = await verifyInstallation();
if (!version) {
console.log(
pc.yellow("Warning: ccs installed but could not verify installation"),
);
console.log(pc.dim("You may need to restart your terminal"));
console.log(pc.dim("Try running 'which ccs' or 'ccs --version' manually"));
return;
}
console.log(pc.green(`ccs installed successfully (${version})`));
// Step 4: Ask if user wants to run setup
const shouldRunSetup = await promptForSetup();
if (shouldRunSetup) {
console.log(pc.dim("Running ccs setup..."));
const setupResult = spawnSync("ccs", ["setup"], { stdio: "inherit" });
if (setupResult.status === 0) {
console.log(pc.green("Setup completed successfully"));
} else {
console.log(
pc.yellow(
`Setup exited with code ${setupResult.status} (this may not be an error)`,
),
);
}
} else {
console.log(pc.dim("You can run 'ccst ccs setup' later to configure ccs"));
}
};
import { spawnSync } from "node:child_process";
import pc from "picocolors";
export type SetupOptions = {
force?: boolean;
};
export const ccsSetupCommand = async (
options?: SetupOptions,
): Promise<void> => {
// Check if ccs is installed
const which = spawnSync("which", ["ccs"], { stdio: "ignore" });
if (which.status !== 0) {
console.log(pc.red("Error: ccs command not found"));
console.log(pc.dim("Run 'ccst ccs install' to install it"));
return;
}
// Build arguments
const args = ["setup"];
if (options?.force) {
args.push("--force");
}
// Execute ccs setup with real-time output
const result = spawnSync("ccs", args, {
stdio: "inherit",
});
// Check exit code
if (result.status !== 0) {
console.log(
pc.red(`Error: ccs setup failed with exit code ${result.status}`),
);
return;
}
console.log(pc.green("Setup completed successfully"));
};
+2
-1
{
"name": "@leynier/ccst",
"version": "0.5.3",
"version": "0.7.0",
"description": "Claude Code Switch Tools for managing contexts",

@@ -35,2 +35,3 @@ "keywords": [

"commander": "^12.1.0",
"get-port": "^7.1.0",
"jszip": "^3.10.1",

@@ -37,0 +38,0 @@ "picocolors": "^1.1.0"

import { spawn } from "node:child_process";
import { openSync } from "node:fs";
import { openSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import getPort from "get-port";
import pc from "picocolors";

@@ -7,2 +10,3 @@ import {

getCcsExecutable,
getCliproxyPort,
getLogPath,

@@ -13,3 +17,5 @@ getProcessByPort,

killProcessTree,
truncateFile,
writePid,
writePorts,
} from "../../utils/daemon.js";

@@ -19,2 +25,4 @@

force?: boolean;
keepLogs?: boolean;
port?: number;
};

@@ -50,24 +58,74 @@

const logPath = getLogPath();
if (!options?.keepLogs) {
try {
await truncateFile(logPath);
} catch {
console.warn(
pc.yellow(
`Warning: could not truncate log; continuing (logs will be appended): ${logPath}`,
),
);
}
}
// Detect ports
const cliproxyPort = await getCliproxyPort();
const dashboardPort =
options?.port ??
(await getPort({
port: [3000, 3001, 3002, 8000, 8080],
}));
// Save ports for stop command
await writePorts({ dashboard: dashboardPort, cliproxy: cliproxyPort });
console.log(
pc.dim(
`Using dashboard port: ${dashboardPort}, CLIProxy port: ${cliproxyPort}`,
),
);
const ccsPath = getCcsExecutable();
let pid: number | undefined;
if (process.platform === "win32") {
// On Windows, use cmd /c start /B to launch without creating a new window
// This works with npm-installed .cmd wrappers that create their own console
const proc = spawn("cmd", ["/c", `start /B "" "${ccsPath}" config`], {
// VBScript is the ONLY reliable way to run a process completely hidden on Windows
// WScript.Shell.Run with 0 = hidden window, False = don't wait
// Redirect output to log file using cmd /c with shell redirection
const escapedLogPath = logPath.replace(/\\/g, "\\\\");
const vbsContent = `CreateObject("WScript.Shell").Run "cmd /c ${ccsPath} config --port ${dashboardPort} >> ${escapedLogPath} 2>&1", 0, False`;
const vbsPath = join(tmpdir(), `ccs-start-${Date.now()}.vbs`);
await Bun.write(vbsPath, vbsContent);
// Run the vbs file (wscript itself doesn't show a window)
const proc = spawn("wscript", [vbsPath], {
detached: true,
stdio: "ignore",
windowsHide: true,
detached: true,
});
proc.unref();
// Wait for the process to start, then find it by port
console.log(pc.dim("Starting CCS config daemon..."));
await new Promise((resolve) => setTimeout(resolve, 2000));
// Clean up the vbs file after a short delay
setTimeout(() => {
try {
unlinkSync(vbsPath);
} catch {}
}, 1000);
// Find the process by port 3000 (dashboard port)
const foundPid = await getProcessByPort(3000);
// Poll for the port to become available
// ccs config takes ~6s to start (5s CLIProxy timeout + dashboard startup)
console.log(
pc.dim("Starting CCS config daemon (this may take a few seconds)..."),
);
const maxWaitMs = 15000; // 15 seconds max
const pollIntervalMs = 500;
const startTime = Date.now();
let foundPid: number | null = null;
while (Date.now() - startTime < maxWaitMs) {
foundPid = await getProcessByPort(dashboardPort);
if (foundPid !== null) break;
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
if (foundPid === null) {
console.log(pc.red("Failed to start CCS config daemon"));
console.log(
pc.dim("Check if ccs is installed: npm install -g @anthropic/ccs"),
pc.dim("Check if ccs is installed: npm install -g @kaitranntt/ccs"),
);

@@ -80,3 +138,3 @@ return;

const logFd = openSync(logPath, "a");
const child = spawn(ccsPath, ["config"], {
const child = spawn(ccsPath, ["config", "--port", String(dashboardPort)], {
detached: true,

@@ -83,0 +141,0 @@ stdio: ["ignore", logFd, logFd],

import pc from "picocolors";
import {
CCS_PORTS,
getPortsToKill,
getRunningDaemonPid,

@@ -9,2 +9,3 @@ isProcessRunning,

removePid,
removePorts,
} from "../../utils/daemon.js";

@@ -36,3 +37,5 @@

// Phase 2: Kill processes by port (especially important on Windows)
for (const port of CCS_PORTS) {
// Use saved ports or fallback to defaults
const ports = await getPortsToKill();
for (const port of ports) {
const killed = await killProcessByPort(port, options?.force ?? true);

@@ -44,2 +47,4 @@ if (killed) {

}
// Clean up ports file
removePorts();
if (!stopped) {

@@ -46,0 +51,0 @@ console.log(pc.yellow("CCS config daemon is not running"));

#!/usr/bin/env bun
import { Command } from "commander";
import pkg from "../package.json";
import { ccsInstallCommand } from "./commands/ccs/install.js";
import { ccsLogsCommand } from "./commands/ccs/logs.js";
import { ccsSetupCommand } from "./commands/ccs/setup.js";
import { ccsStartCommand } from "./commands/ccs/start.js";

@@ -191,4 +193,13 @@ import { ccsStatusCommand } from "./commands/ccs/status.js";

.option("-f, --force", "Force restart if already running")
.option("--keep-logs", "Keep existing log file (append)")
.option(
"-p, --port <number>",
"Dashboard port (auto-detect if not specified)",
)
.action(async (options) => {
await ccsStartCommand(options);
await ccsStartCommand({
force: options.force,
keepLogs: options.keepLogs,
port: options.port ? Number.parseInt(options.port, 10) : undefined,
});
});

@@ -219,2 +230,15 @@ ccsCommandGroup

});
ccsCommandGroup
.command("setup")
.description("Run CCS initial setup")
.option("-f, --force", "Force setup even if already configured")
.action(async (options) => {
await ccsSetupCommand(options);
});
ccsCommandGroup
.command("install")
.description("Install CCS CLI tool")
.action(async () => {
await ccsInstallCommand();
});
try {

@@ -221,0 +245,0 @@ await program.parseAsync(process.argv);

@@ -14,2 +14,7 @@ import { existsSync, mkdirSync, unlinkSync } from "node:fs";

// Truncate a file (or create it empty if missing)
export const truncateFile = async (filePath: string): Promise<void> => {
await Bun.write(filePath, "");
};
// Ensure daemon directory exists

@@ -86,5 +91,81 @@ export const ensureDaemonDir = (): void => {

// Known CCS daemon ports
export const CCS_PORTS = [3000, 8317];
// Default CCS daemon ports (fallback)
export const DEFAULT_DASHBOARD_PORT = 3000;
export const DEFAULT_CLIPROXY_PORT = 8317;
// Ports file path
export const getPortsPath = (): string => join(getDaemonDir(), "ports.json");
// Type for daemon ports
export type DaemonPorts = {
dashboard: number;
cliproxy: number;
};
// Read saved ports
export const readPorts = async (): Promise<DaemonPorts | null> => {
const portsPath = getPortsPath();
if (!existsSync(portsPath)) {
return null;
}
try {
const content = await Bun.file(portsPath).text();
const ports = JSON.parse(content) as DaemonPorts;
if (
typeof ports.dashboard === "number" &&
typeof ports.cliproxy === "number"
) {
return ports;
}
return null;
} catch {
return null;
}
};
// Write ports to file
export const writePorts = async (ports: DaemonPorts): Promise<void> => {
ensureDaemonDir();
await Bun.write(getPortsPath(), JSON.stringify(ports, null, 2));
};
// Remove ports file
export const removePorts = (): void => {
const portsPath = getPortsPath();
if (existsSync(portsPath)) {
unlinkSync(portsPath);
}
};
// Read CLIProxy port from config.yaml
export const getCliproxyPort = async (): Promise<number> => {
const configPath = join(homedir(), ".ccs", "cliproxy", "config.yaml");
if (!existsSync(configPath)) {
return DEFAULT_CLIPROXY_PORT;
}
try {
const content = await Bun.file(configPath).text();
// Simple YAML parsing for "port: XXXX"
const match = content.match(/^port:\s*(\d+)/m);
if (match?.[1]) {
const port = Number.parseInt(match[1], 10);
if (Number.isFinite(port) && port > 0) {
return port;
}
}
return DEFAULT_CLIPROXY_PORT;
} catch {
return DEFAULT_CLIPROXY_PORT;
}
};
// Get ports to kill (from saved file or defaults)
export const getPortsToKill = async (): Promise<number[]> => {
const saved = await readPorts();
if (saved) {
return [saved.dashboard, saved.cliproxy];
}
return [DEFAULT_DASHBOARD_PORT, DEFAULT_CLIPROXY_PORT];
};
// Kill process tree (on Windows, kills all child processes)

@@ -91,0 +172,0 @@ export const killProcessTree = async (

@@ -74,2 +74,5 @@ import { spawnSync } from "node:child_process";

process.stdin.off("end", onEnd);
if (process.stdin.isTTY) {
process.stdin.pause();
}
};

@@ -76,0 +79,0 @@ if (process.stdin.isTTY) {