@leynier/ccst
Advanced tools
| #!/usr/bin/env bun | ||
| import { existsSync, watch } from "node:fs"; | ||
| import { ccsDir, performCcsImport } from "../commands/import-profiles/ccs.js"; | ||
| import { ContextManager } from "../core/context-manager.js"; | ||
| import { getPaths } from "../utils/paths.js"; | ||
| const DEBOUNCE_MS = 1000; | ||
| let debounceTimer: ReturnType<typeof setTimeout> | null = null; | ||
| let isImporting = false; | ||
| const log = (message: string): void => { | ||
| const timestamp = new Date().toISOString(); | ||
| console.log(`[${timestamp}] ${message}`); | ||
| }; | ||
| const runImport = async (): Promise<void> => { | ||
| if (isImporting) { | ||
| log("Import already in progress, skipping"); | ||
| return; | ||
| } | ||
| isImporting = true; | ||
| try { | ||
| const manager = new ContextManager(getPaths("user")); | ||
| const result = await performCcsImport(manager); | ||
| if (result.importedCount > 0) { | ||
| log( | ||
| `Imported ${result.importedCount} profiles: ${result.profileNames.join(", ")}`, | ||
| ); | ||
| } else { | ||
| log("No profiles to import"); | ||
| } | ||
| } catch (error) { | ||
| log(`Import error: ${error}`); | ||
| } finally { | ||
| isImporting = false; | ||
| } | ||
| }; | ||
| const handleFileChange = (eventType: string, filename: string | null): void => { | ||
| if (!filename || !filename.endsWith(".settings.json")) { | ||
| return; | ||
| } | ||
| log(`Detected ${eventType} on ${filename}`); | ||
| // Debounce | ||
| if (debounceTimer) { | ||
| clearTimeout(debounceTimer); | ||
| } | ||
| debounceTimer = setTimeout(runImport, DEBOUNCE_MS); | ||
| }; | ||
| const main = async (): Promise<void> => { | ||
| const ccsPath = ccsDir(); | ||
| log(`CCS Settings Watcher - PID: ${process.pid}`); | ||
| log(`Watching directory: ${ccsPath}`); | ||
| if (!existsSync(ccsPath)) { | ||
| log(`ERROR: CCS directory not found: ${ccsPath}`); | ||
| log("Please run 'ccs setup' first to initialize CCS"); | ||
| process.exit(1); | ||
| } | ||
| // Run initial import | ||
| log("Running initial import..."); | ||
| await runImport(); | ||
| // Start watching | ||
| const watcher = watch(ccsPath, { persistent: true }, handleFileChange); | ||
| // Graceful shutdown | ||
| const cleanup = (): void => { | ||
| log("Shutting down watcher"); | ||
| if (debounceTimer) { | ||
| clearTimeout(debounceTimer); | ||
| } | ||
| watcher.close(); | ||
| process.exit(0); | ||
| }; | ||
| process.on("SIGINT", cleanup); | ||
| process.on("SIGTERM", cleanup); | ||
| log("Watcher started successfully"); | ||
| }; | ||
| await main(); |
| import { existsSync, openSync, unlinkSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { | ||
| ensureDaemonDir, | ||
| getDaemonDir, | ||
| isProcessRunning, | ||
| killProcessTree, | ||
| truncateFile, | ||
| } from "./daemon.js"; | ||
| // Watcher PID file path | ||
| export const getWatcherPidPath = (): string => | ||
| join(getDaemonDir(), "watcher.pid"); | ||
| // Watcher log file path | ||
| export const getWatcherLogPath = (): string => | ||
| join(getDaemonDir(), "watcher.log"); | ||
| // Read watcher PID from file | ||
| export const readWatcherPid = async (): Promise<number | null> => { | ||
| const pidPath = getWatcherPidPath(); | ||
| 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 watcher PID to file | ||
| export const writeWatcherPid = async (pid: number): Promise<void> => { | ||
| ensureDaemonDir(); | ||
| await Bun.write(getWatcherPidPath(), String(pid)); | ||
| }; | ||
| // Remove watcher PID file | ||
| export const removeWatcherPid = (): void => { | ||
| const pidPath = getWatcherPidPath(); | ||
| if (existsSync(pidPath)) { | ||
| unlinkSync(pidPath); | ||
| } | ||
| }; | ||
| // Get running watcher PID (validates process is actually running) | ||
| export const getRunningWatcherPid = async (): Promise<number | null> => { | ||
| const pid = await readWatcherPid(); | ||
| if (pid === null) { | ||
| return null; | ||
| } | ||
| if (!isProcessRunning(pid)) { | ||
| // Stale PID file - clean it up | ||
| removeWatcherPid(); | ||
| return null; | ||
| } | ||
| return pid; | ||
| }; | ||
| // Get watcher script path | ||
| const getWatcherScriptPath = (): string => { | ||
| // The script is in src/scripts/watcher.ts relative to the package | ||
| // When running as installed package, we need to find it | ||
| const scriptPath = join(import.meta.dir, "..", "scripts", "watcher.ts"); | ||
| return scriptPath; | ||
| }; | ||
| // Start watcher process | ||
| export const startWatcher = async (): Promise<number | null> => { | ||
| const existingPid = await getRunningWatcherPid(); | ||
| if (existingPid !== null) { | ||
| return existingPid; | ||
| } | ||
| ensureDaemonDir(); | ||
| const logPath = getWatcherLogPath(); | ||
| const scriptPath = getWatcherScriptPath(); | ||
| // Truncate log file | ||
| await truncateFile(logPath); | ||
| if (process.platform === "win32") { | ||
| return startWatcherWindows(scriptPath, logPath); | ||
| } | ||
| return startWatcherUnix(scriptPath, logPath); | ||
| }; | ||
| // Start watcher on Windows using VBScript to hide console | ||
| const startWatcherWindows = async ( | ||
| scriptPath: string, | ||
| logPath: string, | ||
| ): Promise<number | null> => { | ||
| const vbsPath = join(getDaemonDir(), "start-watcher.vbs"); | ||
| const vbsContent = ` | ||
| Set WshShell = CreateObject("WScript.Shell") | ||
| WshShell.Run "cmd /c bun run ""${scriptPath}"" >> ""${logPath}"" 2>&1", 0, False | ||
| `.trim(); | ||
| await Bun.write(vbsPath, vbsContent); | ||
| const proc = Bun.spawn(["wscript", "//Nologo", vbsPath], { | ||
| detached: true, | ||
| stdio: ["ignore", "ignore", "ignore"], | ||
| }); | ||
| proc.unref(); | ||
| // Clean up VBS file after short delay | ||
| setTimeout(() => { | ||
| try { | ||
| unlinkSync(vbsPath); | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| }, 1000); | ||
| // Poll for log file to have content (indicating process started) | ||
| const maxWait = 5000; | ||
| const interval = 200; | ||
| let waited = 0; | ||
| while (waited < maxWait) { | ||
| await new Promise((resolve) => setTimeout(resolve, interval)); | ||
| waited += interval; | ||
| // Check if log file has content | ||
| if (existsSync(logPath)) { | ||
| const file = Bun.file(logPath); | ||
| const size = file.size; | ||
| if (size > 0) { | ||
| // Try to find the PID by reading the log | ||
| const content = await file.text(); | ||
| const match = content.match(/PID:\s*(\d+)/); | ||
| if (match?.[1]) { | ||
| const pid = Number.parseInt(match[1], 10); | ||
| if (Number.isFinite(pid) && pid > 0) { | ||
| await writeWatcherPid(pid); | ||
| return pid; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| // Start watcher on Unix | ||
| const startWatcherUnix = async ( | ||
| scriptPath: string, | ||
| logPath: string, | ||
| ): Promise<number | null> => { | ||
| const logFd = openSync(logPath, "a"); | ||
| const child = Bun.spawn(["bun", "run", scriptPath], { | ||
| detached: true, | ||
| stdio: ["ignore", logFd, logFd], | ||
| }); | ||
| child.unref(); | ||
| const pid = child.pid; | ||
| if (pid) { | ||
| await writeWatcherPid(pid); | ||
| } | ||
| return pid; | ||
| }; | ||
| // Stop watcher process | ||
| export const stopWatcher = async (force?: boolean): Promise<boolean> => { | ||
| const pid = await readWatcherPid(); | ||
| if (pid === null) { | ||
| return false; | ||
| } | ||
| const killed = await killProcessTree(pid, force); | ||
| // Wait for process to terminate | ||
| const maxWait = force ? 1000 : 5000; | ||
| const interval = 100; | ||
| let waited = 0; | ||
| while (waited < maxWait && isProcessRunning(pid)) { | ||
| await new Promise((resolve) => setTimeout(resolve, interval)); | ||
| waited += interval; | ||
| } | ||
| removeWatcherPid(); | ||
| return killed || !isProcessRunning(pid); | ||
| }; |
+44
-82
@@ -1,105 +0,67 @@ | ||
| Default to using Bun instead of Node.js. | ||
| # CLAUDE.md | ||
| - Use `bun <file>` instead of `node <file>` or `ts-node <file>` | ||
| - Use `bun test` instead of `jest` or `vitest` | ||
| - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild` | ||
| - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` | ||
| - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>` | ||
| - Use `bunx <package> <command>` instead of `npx <package> <command>` | ||
| - Bun automatically loads .env, so don't use dotenv. | ||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
| ## APIs | ||
| ## Build & Development Commands | ||
| - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. | ||
| - `bun:sqlite` for SQLite. Don't use `better-sqlite3`. | ||
| - `Bun.redis` for Redis. Don't use `ioredis`. | ||
| - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. | ||
| - `WebSocket` is built-in. Don't use `ws`. | ||
| - Prefer `Bun.file` over `node:fs`'s readFile/writeFile | ||
| - Bun.$`ls` instead of execa. | ||
| ```bash | ||
| # Install dependencies | ||
| bun install | ||
| ## Testing | ||
| # Format code | ||
| bun run format | ||
| Use `bun test` to run tests. | ||
| # Lint code (with auto-fix) | ||
| bun run lint | ||
| ```ts#index.test.ts | ||
| import { test, expect } from "bun:test"; | ||
| # Run both format and lint | ||
| bun run validate | ||
| test("hello world", () => { | ||
| expect(1).toBe(1); | ||
| }); | ||
| ``` | ||
| # Run tests | ||
| bun test | ||
| ## Frontend | ||
| # Run a single test file | ||
| bun test src/core/context-manager.test.ts | ||
| Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind. | ||
| # Run CLI locally during development | ||
| bun src/index.ts [args] | ||
| ``` | ||
| Server: | ||
| ## Architecture Overview | ||
| ```ts#index.ts | ||
| import index from "./index.html" | ||
| **ccst** (Claude Code Switch Tools) is a CLI tool that manages Claude Code IDE contexts and configurations. It allows users to switch between different permission sets, environments, and settings at user, project, and local levels. | ||
| Bun.serve({ | ||
| routes: { | ||
| "/": index, | ||
| "/api/users/:id": { | ||
| GET: (req) => { | ||
| return new Response(JSON.stringify({ id: req.params.id })); | ||
| }, | ||
| }, | ||
| }, | ||
| // optional websocket support | ||
| websocket: { | ||
| open: (ws) => { | ||
| ws.send("Hello, world!"); | ||
| }, | ||
| message: (ws, message) => { | ||
| ws.send(message); | ||
| }, | ||
| close: (ws) => { | ||
| // handle close | ||
| } | ||
| }, | ||
| development: { | ||
| hmr: true, | ||
| console: true, | ||
| } | ||
| }) | ||
| ``` | ||
| ### Core Components | ||
| HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle. | ||
| - **`src/index.ts`** - Main CLI entry point using Commander.js. Defines all commands and routes to handlers. | ||
| - **`src/core/context-manager.ts`** - Central class for all context operations (CRUD, switching, merging). All operations flow through this class. | ||
| - **`src/core/merge-manager.ts`** - Handles permission merging with history tracking and smart deduplication. | ||
| - **`src/utils/daemon.ts`** - Cross-platform daemon process management (Windows/Unix). Critical for CCS daemon commands. | ||
| - **`src/utils/paths.ts`** - Resolves paths for all three settings levels (user/project/local). | ||
| ```html#index.html | ||
| <html> | ||
| <body> | ||
| <h1>Hello, world!</h1> | ||
| <script type="module" src="./frontend.tsx"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ### Settings Levels | ||
| With the following `frontend.tsx`: | ||
| The tool operates at three hierarchical levels: | ||
| ```tsx#frontend.tsx | ||
| import React from "react"; | ||
| import { createRoot } from "react-dom/client"; | ||
| - **User:** `~/.claude/settings.json` and `~/.claude/settings/` | ||
| - **Project:** `./.claude/settings.json` and `./.claude/settings/` | ||
| - **Local:** `./.claude/settings.local.json` and `./.claude/settings/` | ||
| // import .css files directly and it works | ||
| import './index.css'; | ||
| ### Command Structure | ||
| const root = createRoot(document.body); | ||
| Commands in `src/commands/` are organized by function: | ||
| export default function Frontend() { | ||
| return <h1>Hello, world!</h1>; | ||
| } | ||
| - `ccs/` - CCS daemon management (start, stop, status, logs, setup, install) | ||
| - `config/` - Configuration backup/restore (dump, load) | ||
| - `import-profiles/` - Profile importers (ccs, configs) | ||
| root.render(<Frontend />); | ||
| ``` | ||
| ## Development Guidelines | ||
| Then, run index.ts | ||
| - **Use Bun, not Node.js** - All file operations use `Bun.file()`, `Bun.write()`, `Bun.remove()`. See `AGENTS.md` for Bun API patterns. | ||
| - **Use `Bun.$` for shell commands** - Not execa or child_process. | ||
| - **Cross-platform handling** - `src/utils/daemon.ts` has separate code paths for Windows (taskkill, netstat) and Unix (lsof, kill signals). Test both when modifying. | ||
| - **Formatting/Linting** - Uses Biome. Run `bun run validate` before committing. | ||
| ```sh | ||
| bun --hot ./index.ts | ||
| ``` | ||
| ## Commit Message Pattern | ||
| For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. | ||
| Follow existing commit prefixes: `feat:`, `fix:`, `chore:` |
+3
-2
| { | ||
| "name": "@leynier/ccst", | ||
| "version": "0.7.0", | ||
| "version": "1.0.0", | ||
| "description": "Claude Code Switch Tools for managing contexts", | ||
@@ -49,2 +49,3 @@ "keywords": [ | ||
| "index.ts", | ||
| "readme.md", | ||
| "README.md", | ||
@@ -61,2 +62,2 @@ "LICENSE", | ||
| } | ||
| } | ||
| } |
+112
-0
@@ -188,2 +188,81 @@ # ccst - Claude Code Switch Tools | ||
| ## CCS Daemon Management | ||
| ccst can manage the CCS (Claude Code Server) daemon for background processing: | ||
| ### Installation & Setup | ||
| ```bash | ||
| # Install CCS CLI tool (interactive package manager selection) | ||
| ccst ccs install | ||
| # Run initial setup | ||
| ccst ccs setup | ||
| # Force setup even if already configured | ||
| ccst ccs setup -f | ||
| ``` | ||
| ### Starting & Stopping | ||
| ```bash | ||
| # Start daemon | ||
| ccst ccs start | ||
| # Start with specific dashboard port | ||
| ccst ccs start -p 3001 | ||
| # Force restart if already running | ||
| ccst ccs start -f | ||
| # Keep existing logs (append instead of truncate) | ||
| ccst ccs start --keep-logs | ||
| # Stop daemon | ||
| ccst ccs stop | ||
| # Force kill daemon | ||
| ccst ccs stop -f | ||
| ``` | ||
| ### Monitoring | ||
| ```bash | ||
| # Check daemon status | ||
| ccst ccs status | ||
| # View logs (last 50 lines by default) | ||
| ccst ccs logs | ||
| # View more lines | ||
| ccst ccs logs -n 100 | ||
| # Follow logs in real-time | ||
| ccst ccs logs -f | ||
| ``` | ||
| ## Configuration Backup | ||
| Backup and restore all CCS configuration: | ||
| ```bash | ||
| # Export configuration to ZIP file | ||
| ccst config dump | ||
| # Export to custom path | ||
| ccst config dump my-backup.zip | ||
| # Import configuration from ZIP | ||
| ccst config load | ||
| # Import from custom path | ||
| ccst config load my-backup.zip | ||
| # Replace all existing files during import | ||
| ccst config load -r | ||
| # Skip confirmation prompt | ||
| ccst config load -y | ||
| ``` | ||
| ## File Structure | ||
@@ -217,2 +296,11 @@ | ||
| CCS daemon files (`~/.ccs/`): | ||
| ```text | ||
| ~/.ccs/ | ||
| ├── .daemon.pid # Daemon process ID | ||
| ├── .daemon.log # Daemon log file | ||
| └── .daemon.ports # Dashboard port tracking | ||
| ``` | ||
| ## Interactive Mode | ||
@@ -316,2 +404,25 @@ | ||
| ### CCS Daemon Commands | ||
| - `ccst ccs install` - Install CCS CLI tool (interactive package manager selection) | ||
| - `ccst ccs setup` - Run CCS initial setup | ||
| - `ccst ccs setup -f` - Force setup even if already configured | ||
| - `ccst ccs start` - Start CCS daemon | ||
| - `ccst ccs start -f` - Force restart if already running | ||
| - `ccst ccs start -p <port>` - Start with specific dashboard port | ||
| - `ccst ccs start --keep-logs` - Keep existing logs (append instead of truncate) | ||
| - `ccst ccs stop` - Stop CCS daemon | ||
| - `ccst ccs stop -f` - Force kill daemon (SIGKILL) | ||
| - `ccst ccs status` - Show daemon status, PID, and log info | ||
| - `ccst ccs logs` - View daemon logs (last 50 lines) | ||
| - `ccst ccs logs -n <lines>` - View specified number of lines | ||
| - `ccst ccs logs -f` - Follow log output in real-time | ||
| ### Configuration Commands | ||
| - `ccst config dump [output]` - Export CCS config to ZIP (default: ccs-config.zip) | ||
| - `ccst config load [input]` - Import CCS config from ZIP (default: ccs-config.zip) | ||
| - `ccst config load -r` - Replace all existing files during import | ||
| - `ccst config load -y` - Skip confirmation prompt | ||
| ### Other Options | ||
@@ -321,2 +432,3 @@ | ||
| - `ccst --help` - Show help information | ||
| - `ccst --version` - Show version | ||
@@ -323,0 +435,0 @@ ## Compatibility Note |
@@ -20,2 +20,7 @@ import { spawn } from "node:child_process"; | ||
| } from "../../utils/daemon.js"; | ||
| import { | ||
| getRunningWatcherPid, | ||
| startWatcher, | ||
| stopWatcher, | ||
| } from "../../utils/watcher-daemon.js"; | ||
@@ -26,2 +31,4 @@ export type StartOptions = { | ||
| port?: number; | ||
| noWatch?: boolean; | ||
| timeout?: number; | ||
| }; | ||
@@ -45,2 +52,7 @@ | ||
| await killProcessTree(existingPid, true); | ||
| // Also stop watcher if running | ||
| const watcherPid = await getRunningWatcherPid(); | ||
| if (watcherPid !== null) { | ||
| await stopWatcher(true); | ||
| } | ||
| // Wait for process to terminate | ||
@@ -115,3 +127,3 @@ const maxWait = 3000; | ||
| const maxWaitMs = 15000; // 15 seconds max | ||
| const maxWaitMs = options?.timeout ?? 30000; | ||
| const pollIntervalMs = 500; | ||
@@ -152,4 +164,15 @@ const startTime = Date.now(); | ||
| console.log(pc.dim(`Logs: ${logPath}`)); | ||
| // Start file watcher unless --no-watch is specified | ||
| if (!options?.noWatch) { | ||
| const watcherPid = await startWatcher(); | ||
| if (watcherPid) { | ||
| console.log(pc.dim(`File watcher started (PID: ${watcherPid})`)); | ||
| } else { | ||
| console.log(pc.yellow("Warning: Failed to start file watcher")); | ||
| } | ||
| } | ||
| console.log(pc.dim("Run 'ccst ccs status' to check status")); | ||
| console.log(pc.dim("Run 'ccst ccs logs' to view logs")); | ||
| }; |
@@ -8,35 +8,54 @@ import { existsSync, statSync } from "node:fs"; | ||
| } from "../../utils/daemon.js"; | ||
| import { | ||
| getRunningWatcherPid, | ||
| getWatcherLogPath, | ||
| } from "../../utils/watcher-daemon.js"; | ||
| export const ccsStatusCommand = async (): Promise<void> => { | ||
| // Show daemon status | ||
| 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}`)); | ||
| } else { | ||
| console.log(pc.green(`CCS config daemon is running (PID: ${pid})`)); | ||
| 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 | ||
| } | ||
| } catch { | ||
| // ps command not available or failed | ||
| } | ||
| } | ||
| // Show watcher status | ||
| console.log(); | ||
| const watcherPid = await getRunningWatcherPid(); | ||
| if (watcherPid !== null) { | ||
| console.log(pc.green(`File watcher is running (PID: ${watcherPid})`)); | ||
| const watcherLogPath = getWatcherLogPath(); | ||
| if (existsSync(watcherLogPath)) { | ||
| const stats = statSync(watcherLogPath); | ||
| const sizeKb = (stats.size / 1024).toFixed(2); | ||
| console.log(pc.dim(`Watcher log: ${watcherLogPath} (${sizeKb} KB)`)); | ||
| } | ||
| } else { | ||
| console.log(pc.yellow("File watcher is not running")); | ||
| } | ||
| }; |
@@ -11,2 +11,6 @@ import pc from "picocolors"; | ||
| } from "../../utils/daemon.js"; | ||
| import { | ||
| getRunningWatcherPid, | ||
| stopWatcher, | ||
| } from "../../utils/watcher-daemon.js"; | ||
@@ -48,2 +52,12 @@ export type StopOptions = { | ||
| removePorts(); | ||
| // Stop file watcher | ||
| const watcherPid = await getRunningWatcherPid(); | ||
| if (watcherPid !== null) { | ||
| const watcherStopped = await stopWatcher(options?.force); | ||
| if (watcherStopped) { | ||
| console.log(pc.dim(`File watcher stopped (PID: ${watcherPid})`)); | ||
| } | ||
| } | ||
| if (!stopped) { | ||
@@ -50,0 +64,0 @@ console.log(pc.yellow("CCS config daemon is not running")); |
@@ -10,3 +10,3 @@ import { existsSync, mkdirSync, readdirSync } from "node:fs"; | ||
| const defaultConfigsDir = (): string => path.join(homedir(), ".ccst"); | ||
| const ccsDir = (): string => path.join(homedir(), ".ccs"); | ||
| export const ccsDir = (): string => path.join(homedir(), ".ccs"); | ||
@@ -53,5 +53,6 @@ const ensureDefaultConfig = async ( | ||
| ): Promise<void> => { | ||
| await manager.deleteContext(profileName).catch(() => undefined); | ||
| const input = `${JSON.stringify(merged, null, 2)}\n`; | ||
| await manager.importContextFromString(profileName, input); | ||
| // Write directly to the context file, overwriting if exists | ||
| const contextPath = path.join(manager.contextsDir, `${profileName}.json`); | ||
| const content = `${JSON.stringify(merged, null, 2)}\n`; | ||
| await Bun.write(contextPath, content); | ||
| }; | ||
@@ -70,6 +71,11 @@ | ||
| export const importFromCcs = async ( | ||
| export type PerformCcsImportResult = { | ||
| importedCount: number; | ||
| profileNames: string[]; | ||
| }; | ||
| export const performCcsImport = async ( | ||
| manager: ContextManager, | ||
| configsDir?: string, | ||
| ): Promise<void> => { | ||
| ): Promise<PerformCcsImportResult> => { | ||
| const ccsPath = ccsDir(); | ||
@@ -79,3 +85,2 @@ if (!existsSync(ccsPath)) { | ||
| } | ||
| console.log(`📥 Importing profiles from CCS settings...`); | ||
| const dir = configsDir ?? defaultConfigsDir(); | ||
@@ -98,7 +103,6 @@ const { created } = await ensureDefaultConfig(manager, dir); | ||
| } | ||
| let importedCount = 0; | ||
| const profileNames: string[] = []; | ||
| for (const fileName of entries) { | ||
| const settingsPath = path.join(ccsPath, fileName); | ||
| const profileName = fileName.replace(/\.settings\.json$/u, ""); | ||
| console.log(` 📦 Importing '${colors.cyan(profileName)}'...`); | ||
| const settings = await readJson<Record<string, unknown>>(settingsPath); | ||
@@ -110,3 +114,3 @@ const merged = deepMerge(defaultConfig, settings); | ||
| await importProfile(manager, profileName, merged); | ||
| importedCount++; | ||
| profileNames.push(profileName); | ||
| } | ||
@@ -116,5 +120,17 @@ if (currentContext) { | ||
| } | ||
| return { importedCount: profileNames.length, profileNames }; | ||
| }; | ||
| export const importFromCcs = async ( | ||
| manager: ContextManager, | ||
| configsDir?: string, | ||
| ): Promise<void> => { | ||
| console.log(`📥 Importing profiles from CCS settings...`); | ||
| const result = await performCcsImport(manager, configsDir); | ||
| for (const profileName of result.profileNames) { | ||
| console.log(` 📦 Imported '${colors.cyan(profileName)}'`); | ||
| } | ||
| console.log( | ||
| `✅ Imported ${colors.bold(colors.green(String(importedCount)))} profiles from CCS`, | ||
| `✅ Imported ${colors.bold(colors.green(String(result.importedCount)))} profiles from CCS`, | ||
| ); | ||
| }; |
+8
-0
@@ -198,2 +198,8 @@ #!/usr/bin/env bun | ||
| ) | ||
| .option("-W, --no-watch", "Skip file watcher") | ||
| .option( | ||
| "-t, --timeout <seconds>", | ||
| "Timeout in seconds for daemon startup (Windows only)", | ||
| "30", | ||
| ) | ||
| .action(async (options) => { | ||
@@ -204,2 +210,4 @@ await ccsStartCommand({ | ||
| port: options.port ? Number.parseInt(options.port, 10) : undefined, | ||
| noWatch: options.watch === false, | ||
| timeout: Number.parseInt(options.timeout, 10) * 1000, | ||
| }); | ||
@@ -206,0 +214,0 @@ }); |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
105812
12.48%48
4.35%2963
11.64%0
-100%434
34.78%