🚀 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.3.2
to
0.4.0
+74
src/commands/ccs/logs.ts
import { existsSync, unwatchFile, watchFile } from "node:fs";
import pc from "picocolors";
import { getLogPath } from "../../utils/daemon.js";
export type LogsOptions = {
follow?: boolean;
lines?: number;
};
export const ccsLogsCommand = async (options?: LogsOptions): Promise<void> => {
const logPath = getLogPath();
if (!existsSync(logPath)) {
console.log(pc.yellow("No log file found"));
console.log(pc.dim(`Expected at: ${logPath}`));
return;
}
const lines = options?.lines ?? 50;
const follow = options?.follow ?? false;
// On Unix, use native tail for better performance
if (process.platform !== "win32" && follow) {
const tailProcess = Bun.spawn(
["tail", "-f", "-n", String(lines), logPath],
{
stdout: "inherit",
stderr: "inherit",
},
);
// Handle cleanup on Ctrl+C
const cleanup = () => {
tailProcess.kill();
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
await tailProcess.exited;
return;
}
// Read last N lines
const content = await Bun.file(logPath).text();
const allLines = content.split("\n");
const lastLines = allLines.slice(-lines).join("\n");
console.log(lastLines);
if (!follow) {
return;
}
// Follow mode for Windows: watch for changes
console.log(pc.dim("--- Following log file (Ctrl+C to stop) ---"));
let lastSize = (await Bun.file(logPath).stat()).size;
// Handle graceful shutdown
const cleanup = () => {
unwatchFile(logPath);
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
// Watch file for changes
watchFile(logPath, { interval: 500 }, async (curr) => {
if (curr.size > lastSize) {
// Read new content
const file = Bun.file(logPath);
const newContent = await file.slice(lastSize).text();
process.stdout.write(newContent);
lastSize = curr.size;
} else if (curr.size < lastSize) {
// File was truncated/rotated
console.log(pc.dim("--- Log file rotated ---"));
const newContent = await Bun.file(logPath).text();
process.stdout.write(newContent);
lastSize = curr.size;
}
});
// Keep process alive
await new Promise(() => {});
};
import { spawn } from "node:child_process";
import { openSync } from "node:fs";
import pc from "picocolors";
import {
ensureDaemonDir,
getCcsExecutable,
getLogPath,
getRunningDaemonPid,
isProcessRunning,
writePid,
} from "../../utils/daemon.js";
export type StartOptions = {
force?: boolean;
};
export const ccsStartCommand = async (
options?: StartOptions,
): Promise<void> => {
// Check if already running
const existingPid = await getRunningDaemonPid();
if (existingPid !== null && !options?.force) {
console.log(
pc.yellow(`CCS config daemon is already running (PID: ${existingPid})`),
);
console.log(pc.dim("Use --force to restart"));
return;
}
// If force and running, stop first
if (existingPid !== null && options?.force) {
console.log(pc.dim(`Stopping existing daemon (PID: ${existingPid})...`));
try {
process.kill(existingPid, "SIGTERM");
// Wait for process to terminate
const maxWait = 3000;
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
if (!isProcessRunning(existingPid)) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
} catch {
// Process may have already exited
}
}
ensureDaemonDir();
const logPath = getLogPath();
const ccsPath = getCcsExecutable();
// Open log file for writing (append mode)
const logFd = openSync(logPath, "a");
// Spawn detached process
const child = spawn(ccsPath, ["config"], {
detached: true,
stdio: ["ignore", logFd, logFd],
// On Windows, need shell: true for proper detachment
...(process.platform === "win32" ? { shell: true } : {}),
});
if (!child.pid) {
console.log(pc.red("Failed to start CCS config daemon"));
return;
}
await writePid(child.pid);
// Unref to allow parent to exit independently
child.unref();
console.log(pc.green(`CCS config daemon started (PID: ${child.pid})`));
console.log(pc.dim(`Logs: ${logPath}`));
console.log(pc.dim("Run 'ccst ccs status' to check status"));
console.log(pc.dim("Run 'ccst ccs logs' to view logs"));
};
import { existsSync, statSync } from "node:fs";
import pc from "picocolors";
import {
getLogPath,
getPidPath,
getRunningDaemonPid,
} from "../../utils/daemon.js";
export const ccsStatusCommand = async (): Promise<void> => {
const pid = await getRunningDaemonPid();
if (pid === null) {
console.log(pc.yellow("CCS config daemon is not running"));
return;
}
console.log(pc.green(`CCS config daemon is running (PID: ${pid})`));
// Show additional info
const pidPath = getPidPath();
const logPath = getLogPath();
console.log(pc.dim(`PID file: ${pidPath}`));
if (existsSync(logPath)) {
const stats = statSync(logPath);
const sizeKb = (stats.size / 1024).toFixed(2);
console.log(pc.dim(`Log file: ${logPath} (${sizeKb} KB)`));
}
// Try to get process uptime (Unix only)
if (process.platform !== "win32") {
try {
const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "etime="], {
stdout: "pipe",
stderr: "ignore",
});
const output = await new Response(proc.stdout).text();
const uptime = output.trim();
if (uptime) {
console.log(pc.dim(`Uptime: ${uptime}`));
}
} catch {
// ps command not available or failed
}
}
};
import pc from "picocolors";
import {
getRunningDaemonPid,
isProcessRunning,
removePid,
} from "../../utils/daemon.js";
export type StopOptions = {
force?: boolean;
};
export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
const pid = await getRunningDaemonPid();
if (pid === null) {
console.log(pc.yellow("CCS config daemon is not running"));
return;
}
try {
// Send SIGTERM for graceful shutdown, SIGKILL if --force
const signal = options?.force ? "SIGKILL" : "SIGTERM";
process.kill(pid, signal);
// Wait for process to terminate (with timeout)
const maxWait = options?.force ? 1000 : 5000;
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
if (!isProcessRunning(pid)) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
removePid();
console.log(pc.green(`CCS config daemon stopped (PID: ${pid})`));
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ESRCH") {
// Process doesn't exist - clean up stale PID file
removePid();
console.log(pc.yellow("Process not found, cleaned up stale PID file"));
} else if (err.code === "EPERM") {
console.log(
pc.red("Permission denied. Try running with elevated privileges."),
);
} else {
console.log(pc.red(`Failed to stop daemon: ${err.message}`));
}
}
};
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
// Daemon directory inside CCS home
export const getDaemonDir = (): string => join(homedir(), ".ccs", "daemon");
// PID file path
export const getPidPath = (): string => join(getDaemonDir(), "ccs-config.pid");
// Log file path
export const getLogPath = (): string => join(getDaemonDir(), "ccs-config.log");
// Ensure daemon directory exists
export const ensureDaemonDir = (): void => {
const dir = getDaemonDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
};
// Read PID from file
export const readPid = async (): Promise<number | null> => {
const pidPath = getPidPath();
if (!existsSync(pidPath)) {
return null;
}
try {
const content = await Bun.file(pidPath).text();
const pid = parseInt(content.trim(), 10);
return Number.isFinite(pid) ? pid : null;
} catch {
return null;
}
};
// Write PID to file
export const writePid = async (pid: number): Promise<void> => {
ensureDaemonDir();
await Bun.write(getPidPath(), String(pid));
};
// Remove PID file
export const removePid = (): void => {
const pidPath = getPidPath();
if (existsSync(pidPath)) {
unlinkSync(pidPath);
}
};
// Check if process is running using kill(pid, 0)
export const isProcessRunning = (pid: number): boolean => {
try {
process.kill(pid, 0);
return true;
} catch (error) {
// EPERM = permission denied (but process exists)
if ((error as NodeJS.ErrnoException).code === "EPERM") {
return true;
}
return false;
}
};
// Get running daemon PID (validates process is actually running)
export const getRunningDaemonPid = async (): Promise<number | null> => {
const pid = await readPid();
if (pid === null) {
return null;
}
if (!isProcessRunning(pid)) {
// Stale PID file - clean it up
removePid();
return null;
}
return pid;
};
// Locate ccs executable
export const getCcsExecutable = (): string => {
// Use 'ccs' from PATH - it should be globally installed
return "ccs";
};
+1
-1
{
"name": "@leynier/ccst",
"version": "0.3.2",
"version": "0.4.0",
"description": "Claude Code Switch Tools for managing contexts",

@@ -5,0 +5,0 @@ "keywords": [

#!/usr/bin/env bun
import { Command } from "commander";
import pkg from "../package.json";
import { ccsLogsCommand } from "./commands/ccs/logs.js";
import { ccsStartCommand } from "./commands/ccs/start.js";
import { ccsStatusCommand } from "./commands/ccs/status.js";
import { ccsStopCommand } from "./commands/ccs/stop.js";
import { completionsCommand } from "./commands/completions.js";

@@ -180,2 +184,36 @@ import { configDumpCommand } from "./commands/config/dump.js";

});
const ccsCommandGroup = program
.command("ccs")
.description("CCS daemon management");
ccsCommandGroup
.command("start")
.description("Start CCS config as background daemon")
.option("-f, --force", "Force restart if already running")
.action(async (options) => {
await ccsStartCommand(options);
});
ccsCommandGroup
.command("stop")
.description("Stop the CCS config daemon")
.option("-f, --force", "Force kill (SIGKILL)")
.action(async (options) => {
await ccsStopCommand(options);
});
ccsCommandGroup
.command("status")
.description("Check CCS config daemon status")
.action(async () => {
await ccsStatusCommand();
});
ccsCommandGroup
.command("logs")
.description("View CCS config daemon logs")
.option("-f, --follow", "Follow log output")
.option("-n, --lines <number>", "Number of lines", "50")
.action(async (options) => {
await ccsLogsCommand({
follow: options.follow,
lines: parseInt(options.lines, 10),
});
});
try {

@@ -182,0 +220,0 @@ await program.parseAsync(process.argv);