@leynier/ccst 0.5.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +47 -85
- package/package.json +4 -2
- package/readme.md +112 -0
- package/src/commands/ccs/install.ts +136 -0
- package/src/commands/ccs/setup.ts +39 -0
- package/src/commands/ccs/start.ts +93 -12
- package/src/commands/ccs/status.ts +44 -25
- package/src/commands/ccs/stop.ts +21 -2
- package/src/commands/import-profiles/ccs.ts +27 -11
- package/src/index.ts +33 -1
- package/src/scripts/watcher.ts +88 -0
- package/src/utils/daemon.ts +83 -2
- package/src/utils/interactive.ts +3 -0
- package/src/utils/watcher-daemon.ts +191 -0
package/AGENTS.md
CHANGED
|
@@ -1,105 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
# CLAUDE.md
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- Use `bun test` instead of `jest` or `vitest`
|
|
5
|
-
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
6
|
-
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
7
|
-
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
8
|
-
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
9
|
-
- Bun automatically loads .env, so don't use dotenv.
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
10
4
|
|
|
11
|
-
##
|
|
5
|
+
## Build & Development Commands
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
17
|
-
- `WebSocket` is built-in. Don't use `ws`.
|
|
18
|
-
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
19
|
-
- Bun.$`ls` instead of execa.
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
bun install
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
# Format code
|
|
12
|
+
bun run format
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
# Lint code (with auto-fix)
|
|
15
|
+
bun run lint
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
# Run both format and lint
|
|
18
|
+
bun run validate
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
# Run tests
|
|
21
|
+
bun test
|
|
22
|
+
|
|
23
|
+
# Run a single test file
|
|
24
|
+
bun test src/core/context-manager.test.ts
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
36
|
-
|
|
37
|
-
Server:
|
|
38
|
-
|
|
39
|
-
```ts#index.ts
|
|
40
|
-
import index from "./index.html"
|
|
41
|
-
|
|
42
|
-
Bun.serve({
|
|
43
|
-
routes: {
|
|
44
|
-
"/": index,
|
|
45
|
-
"/api/users/:id": {
|
|
46
|
-
GET: (req) => {
|
|
47
|
-
return new Response(JSON.stringify({ id: req.params.id }));
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
// optional websocket support
|
|
52
|
-
websocket: {
|
|
53
|
-
open: (ws) => {
|
|
54
|
-
ws.send("Hello, world!");
|
|
55
|
-
},
|
|
56
|
-
message: (ws, message) => {
|
|
57
|
-
ws.send(message);
|
|
58
|
-
},
|
|
59
|
-
close: (ws) => {
|
|
60
|
-
// handle close
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
development: {
|
|
64
|
-
hmr: true,
|
|
65
|
-
console: true,
|
|
66
|
-
}
|
|
67
|
-
})
|
|
26
|
+
# Run CLI locally during development
|
|
27
|
+
bun src/index.ts [args]
|
|
68
28
|
```
|
|
69
29
|
|
|
70
|
-
|
|
30
|
+
## Architecture Overview
|
|
71
31
|
|
|
72
|
-
|
|
73
|
-
<html>
|
|
74
|
-
<body>
|
|
75
|
-
<h1>Hello, world!</h1>
|
|
76
|
-
<script type="module" src="./frontend.tsx"></script>
|
|
77
|
-
</body>
|
|
78
|
-
</html>
|
|
79
|
-
```
|
|
32
|
+
**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.
|
|
80
33
|
|
|
81
|
-
|
|
34
|
+
### Core Components
|
|
82
35
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
36
|
+
- **`src/index.ts`** - Main CLI entry point using Commander.js. Defines all commands and routes to handlers.
|
|
37
|
+
- **`src/core/context-manager.ts`** - Central class for all context operations (CRUD, switching, merging). All operations flow through this class.
|
|
38
|
+
- **`src/core/merge-manager.ts`** - Handles permission merging with history tracking and smart deduplication.
|
|
39
|
+
- **`src/utils/daemon.ts`** - Cross-platform daemon process management (Windows/Unix). Critical for CCS daemon commands.
|
|
40
|
+
- **`src/utils/paths.ts`** - Resolves paths for all three settings levels (user/project/local).
|
|
86
41
|
|
|
87
|
-
|
|
88
|
-
import './index.css';
|
|
42
|
+
### Settings Levels
|
|
89
43
|
|
|
90
|
-
|
|
44
|
+
The tool operates at three hierarchical levels:
|
|
91
45
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
46
|
+
- **User:** `~/.claude/settings.json` and `~/.claude/settings/`
|
|
47
|
+
- **Project:** `./.claude/settings.json` and `./.claude/settings/`
|
|
48
|
+
- **Local:** `./.claude/settings.local.json` and `./.claude/settings/`
|
|
95
49
|
|
|
96
|
-
|
|
97
|
-
```
|
|
50
|
+
### Command Structure
|
|
98
51
|
|
|
99
|
-
|
|
52
|
+
Commands in `src/commands/` are organized by function:
|
|
100
53
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
54
|
+
- `ccs/` - CCS daemon management (start, stop, status, logs, setup, install)
|
|
55
|
+
- `config/` - Configuration backup/restore (dump, load)
|
|
56
|
+
- `import-profiles/` - Profile importers (ccs, configs)
|
|
57
|
+
|
|
58
|
+
## Development Guidelines
|
|
59
|
+
|
|
60
|
+
- **Use Bun, not Node.js** - All file operations use `Bun.file()`, `Bun.write()`, `Bun.remove()`. See `AGENTS.md` for Bun API patterns.
|
|
61
|
+
- **Use `Bun.$` for shell commands** - Not execa or child_process.
|
|
62
|
+
- **Cross-platform handling** - `src/utils/daemon.ts` has separate code paths for Windows (taskkill, netstat) and Unix (lsof, kill signals). Test both when modifying.
|
|
63
|
+
- **Formatting/Linting** - Uses Biome. Run `bun run validate` before committing.
|
|
64
|
+
|
|
65
|
+
## Commit Message Pattern
|
|
104
66
|
|
|
105
|
-
|
|
67
|
+
Follow existing commit prefixes: `feat:`, `fix:`, `chore:`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leynier/ccst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Claude Code Switch Tools for managing contexts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"commander": "^12.1.0",
|
|
35
|
+
"get-port": "^7.1.0",
|
|
35
36
|
"jszip": "^3.10.1",
|
|
36
37
|
"picocolors": "^1.1.0"
|
|
37
38
|
},
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"files": [
|
|
46
47
|
"src",
|
|
47
48
|
"index.ts",
|
|
49
|
+
"readme.md",
|
|
48
50
|
"README.md",
|
|
49
51
|
"LICENSE",
|
|
50
52
|
"AGENTS.md",
|
|
@@ -56,4 +58,4 @@
|
|
|
56
58
|
"publishConfig": {
|
|
57
59
|
"access": "public"
|
|
58
60
|
}
|
|
59
|
-
}
|
|
61
|
+
}
|
package/readme.md
CHANGED
|
@@ -185,6 +185,85 @@ ccst import configs
|
|
|
185
185
|
ccst import configs -d /path/to/configs
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
## CCS Daemon Management
|
|
189
|
+
|
|
190
|
+
ccst can manage the CCS (Claude Code Server) daemon for background processing:
|
|
191
|
+
|
|
192
|
+
### Installation & Setup
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Install CCS CLI tool (interactive package manager selection)
|
|
196
|
+
ccst ccs install
|
|
197
|
+
|
|
198
|
+
# Run initial setup
|
|
199
|
+
ccst ccs setup
|
|
200
|
+
|
|
201
|
+
# Force setup even if already configured
|
|
202
|
+
ccst ccs setup -f
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Starting & Stopping
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Start daemon
|
|
209
|
+
ccst ccs start
|
|
210
|
+
|
|
211
|
+
# Start with specific dashboard port
|
|
212
|
+
ccst ccs start -p 3001
|
|
213
|
+
|
|
214
|
+
# Force restart if already running
|
|
215
|
+
ccst ccs start -f
|
|
216
|
+
|
|
217
|
+
# Keep existing logs (append instead of truncate)
|
|
218
|
+
ccst ccs start --keep-logs
|
|
219
|
+
|
|
220
|
+
# Stop daemon
|
|
221
|
+
ccst ccs stop
|
|
222
|
+
|
|
223
|
+
# Force kill daemon
|
|
224
|
+
ccst ccs stop -f
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Monitoring
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Check daemon status
|
|
231
|
+
ccst ccs status
|
|
232
|
+
|
|
233
|
+
# View logs (last 50 lines by default)
|
|
234
|
+
ccst ccs logs
|
|
235
|
+
|
|
236
|
+
# View more lines
|
|
237
|
+
ccst ccs logs -n 100
|
|
238
|
+
|
|
239
|
+
# Follow logs in real-time
|
|
240
|
+
ccst ccs logs -f
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Configuration Backup
|
|
244
|
+
|
|
245
|
+
Backup and restore all CCS configuration:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Export configuration to ZIP file
|
|
249
|
+
ccst config dump
|
|
250
|
+
|
|
251
|
+
# Export to custom path
|
|
252
|
+
ccst config dump my-backup.zip
|
|
253
|
+
|
|
254
|
+
# Import configuration from ZIP
|
|
255
|
+
ccst config load
|
|
256
|
+
|
|
257
|
+
# Import from custom path
|
|
258
|
+
ccst config load my-backup.zip
|
|
259
|
+
|
|
260
|
+
# Replace all existing files during import
|
|
261
|
+
ccst config load -r
|
|
262
|
+
|
|
263
|
+
# Skip confirmation prompt
|
|
264
|
+
ccst config load -y
|
|
265
|
+
```
|
|
266
|
+
|
|
188
267
|
## File Structure
|
|
189
268
|
|
|
190
269
|
Contexts are stored as individual JSON files at different levels:
|
|
@@ -213,6 +292,15 @@ Project level (`./.claude/`):
|
|
|
213
292
|
└── .cctx-state.local.json # Local state
|
|
214
293
|
```
|
|
215
294
|
|
|
295
|
+
CCS daemon files (`~/.ccs/`):
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
~/.ccs/
|
|
299
|
+
├── .daemon.pid # Daemon process ID
|
|
300
|
+
├── .daemon.log # Daemon log file
|
|
301
|
+
└── .daemon.ports # Dashboard port tracking
|
|
302
|
+
```
|
|
303
|
+
|
|
216
304
|
## Interactive Mode
|
|
217
305
|
|
|
218
306
|
When no arguments are provided, ccst can enter interactive mode:
|
|
@@ -311,10 +399,34 @@ ccst -c
|
|
|
311
399
|
- `ccst --in-project` - Project-level contexts (`./.claude/settings.json`)
|
|
312
400
|
- `ccst --local` - Local project contexts (`./.claude/settings.local.json`)
|
|
313
401
|
|
|
402
|
+
### CCS Daemon Commands
|
|
403
|
+
|
|
404
|
+
- `ccst ccs install` - Install CCS CLI tool (interactive package manager selection)
|
|
405
|
+
- `ccst ccs setup` - Run CCS initial setup
|
|
406
|
+
- `ccst ccs setup -f` - Force setup even if already configured
|
|
407
|
+
- `ccst ccs start` - Start CCS daemon
|
|
408
|
+
- `ccst ccs start -f` - Force restart if already running
|
|
409
|
+
- `ccst ccs start -p <port>` - Start with specific dashboard port
|
|
410
|
+
- `ccst ccs start --keep-logs` - Keep existing logs (append instead of truncate)
|
|
411
|
+
- `ccst ccs stop` - Stop CCS daemon
|
|
412
|
+
- `ccst ccs stop -f` - Force kill daemon (SIGKILL)
|
|
413
|
+
- `ccst ccs status` - Show daemon status, PID, and log info
|
|
414
|
+
- `ccst ccs logs` - View daemon logs (last 50 lines)
|
|
415
|
+
- `ccst ccs logs -n <lines>` - View specified number of lines
|
|
416
|
+
- `ccst ccs logs -f` - Follow log output in real-time
|
|
417
|
+
|
|
418
|
+
### Configuration Commands
|
|
419
|
+
|
|
420
|
+
- `ccst config dump [output]` - Export CCS config to ZIP (default: ccs-config.zip)
|
|
421
|
+
- `ccst config load [input]` - Import CCS config from ZIP (default: ccs-config.zip)
|
|
422
|
+
- `ccst config load -r` - Replace all existing files during import
|
|
423
|
+
- `ccst config load -y` - Skip confirmation prompt
|
|
424
|
+
|
|
314
425
|
### Other Options
|
|
315
426
|
|
|
316
427
|
- `ccst --completions <shell>` - Generate shell completions
|
|
317
428
|
- `ccst --help` - Show help information
|
|
429
|
+
- `ccst --version` - Show version
|
|
318
430
|
|
|
319
431
|
## Compatibility Note
|
|
320
432
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { promptInput } from "../../utils/interactive.js";
|
|
4
|
+
|
|
5
|
+
type PackageManager = {
|
|
6
|
+
name: string;
|
|
7
|
+
displayCommand: string;
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const packageManagers: PackageManager[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "bun",
|
|
15
|
+
displayCommand: "bun add -g @kaitranntt/ccs",
|
|
16
|
+
command: "bun",
|
|
17
|
+
args: ["add", "-g", "@kaitranntt/ccs"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "npm",
|
|
21
|
+
displayCommand: "npm install -g @kaitranntt/ccs",
|
|
22
|
+
command: "npm",
|
|
23
|
+
args: ["install", "-g", "@kaitranntt/ccs"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "pnpm",
|
|
27
|
+
displayCommand: "pnpm add -g @kaitranntt/ccs",
|
|
28
|
+
command: "pnpm",
|
|
29
|
+
args: ["add", "-g", "@kaitranntt/ccs"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "yarn",
|
|
33
|
+
displayCommand: "yarn global add @kaitranntt/ccs",
|
|
34
|
+
command: "yarn",
|
|
35
|
+
args: ["global", "add", "@kaitranntt/ccs"],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const selectPackageManager = async (): Promise<PackageManager | undefined> => {
|
|
40
|
+
const lines = packageManagers.map(
|
|
41
|
+
(pm, index) => `${index + 1}. ${pm.name} (${pm.displayCommand})`,
|
|
42
|
+
);
|
|
43
|
+
console.log("Select package manager to install @kaitranntt/ccs:");
|
|
44
|
+
console.log(lines.join("\n"));
|
|
45
|
+
|
|
46
|
+
const input = await promptInput("Select option (1-4)");
|
|
47
|
+
const index = Number.parseInt(input || "", 10);
|
|
48
|
+
|
|
49
|
+
if (!Number.isFinite(index) || index < 1 || index > packageManagers.length) {
|
|
50
|
+
console.log(pc.red("Invalid selection"));
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return packageManagers[index - 1];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const verifyInstallation = async (): Promise<string | null> => {
|
|
58
|
+
const result = spawnSync("ccs", ["--version"], {
|
|
59
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const version = result.stdout?.trim();
|
|
68
|
+
return version && version.length > 0 ? version : null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const promptForSetup = async (): Promise<boolean> => {
|
|
72
|
+
const response = await promptInput("Run ccs setup now? (y/n)");
|
|
73
|
+
return response?.toLowerCase() === "y";
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const ccsInstallCommand = async (): Promise<void> => {
|
|
77
|
+
// Step 1: Select package manager
|
|
78
|
+
const selectedPm = await selectPackageManager();
|
|
79
|
+
if (!selectedPm) {
|
|
80
|
+
console.log(pc.dim("Installation cancelled"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 2: Execute installation
|
|
85
|
+
console.log(
|
|
86
|
+
pc.dim(
|
|
87
|
+
`Installing @kaitranntt/ccs using ${selectedPm.name}... (this may take a moment)`,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
const installResult = spawnSync(selectedPm.command, selectedPm.args, {
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (installResult.status !== 0) {
|
|
95
|
+
console.log(
|
|
96
|
+
pc.red(
|
|
97
|
+
`Error: Installation failed with exit code ${installResult.status}`,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Step 3: Verify installation
|
|
104
|
+
console.log(pc.dim("Verifying installation..."));
|
|
105
|
+
const version = await verifyInstallation();
|
|
106
|
+
|
|
107
|
+
if (!version) {
|
|
108
|
+
console.log(
|
|
109
|
+
pc.yellow("Warning: ccs installed but could not verify installation"),
|
|
110
|
+
);
|
|
111
|
+
console.log(pc.dim("You may need to restart your terminal"));
|
|
112
|
+
console.log(pc.dim("Try running 'which ccs' or 'ccs --version' manually"));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(pc.green(`ccs installed successfully (${version})`));
|
|
117
|
+
|
|
118
|
+
// Step 4: Ask if user wants to run setup
|
|
119
|
+
const shouldRunSetup = await promptForSetup();
|
|
120
|
+
if (shouldRunSetup) {
|
|
121
|
+
console.log(pc.dim("Running ccs setup..."));
|
|
122
|
+
const setupResult = spawnSync("ccs", ["setup"], { stdio: "inherit" });
|
|
123
|
+
|
|
124
|
+
if (setupResult.status === 0) {
|
|
125
|
+
console.log(pc.green("Setup completed successfully"));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(
|
|
128
|
+
pc.yellow(
|
|
129
|
+
`Setup exited with code ${setupResult.status} (this may not be an error)`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
console.log(pc.dim("You can run 'ccst ccs setup' later to configure ccs"));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
export type SetupOptions = {
|
|
5
|
+
force?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const ccsSetupCommand = async (
|
|
9
|
+
options?: SetupOptions,
|
|
10
|
+
): Promise<void> => {
|
|
11
|
+
// Check if ccs is installed
|
|
12
|
+
const which = spawnSync("which", ["ccs"], { stdio: "ignore" });
|
|
13
|
+
if (which.status !== 0) {
|
|
14
|
+
console.log(pc.red("Error: ccs command not found"));
|
|
15
|
+
console.log(pc.dim("Run 'ccst ccs install' to install it"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Build arguments
|
|
20
|
+
const args = ["setup"];
|
|
21
|
+
if (options?.force) {
|
|
22
|
+
args.push("--force");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Execute ccs setup with real-time output
|
|
26
|
+
const result = spawnSync("ccs", args, {
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Check exit code
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
console.log(
|
|
33
|
+
pc.red(`Error: ccs setup failed with exit code ${result.status}`),
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(pc.green("Setup completed successfully"));
|
|
39
|
+
};
|
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { openSync } from "node:fs";
|
|
2
|
+
import { openSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import getPort from "get-port";
|
|
3
6
|
import pc from "picocolors";
|
|
4
7
|
import {
|
|
5
8
|
ensureDaemonDir,
|
|
6
9
|
getCcsExecutable,
|
|
10
|
+
getCliproxyPort,
|
|
7
11
|
getLogPath,
|
|
8
12
|
getProcessByPort,
|
|
9
13
|
getRunningDaemonPid,
|
|
10
14
|
isProcessRunning,
|
|
11
15
|
killProcessTree,
|
|
16
|
+
truncateFile,
|
|
12
17
|
writePid,
|
|
18
|
+
writePorts,
|
|
13
19
|
} from "../../utils/daemon.js";
|
|
20
|
+
import {
|
|
21
|
+
getRunningWatcherPid,
|
|
22
|
+
startWatcher,
|
|
23
|
+
stopWatcher,
|
|
24
|
+
} from "../../utils/watcher-daemon.js";
|
|
14
25
|
|
|
15
26
|
export type StartOptions = {
|
|
16
27
|
force?: boolean;
|
|
28
|
+
keepLogs?: boolean;
|
|
29
|
+
port?: number;
|
|
30
|
+
noWatch?: boolean;
|
|
31
|
+
timeout?: number;
|
|
17
32
|
};
|
|
18
33
|
|
|
19
34
|
export const ccsStartCommand = async (
|
|
@@ -32,6 +47,11 @@ export const ccsStartCommand = async (
|
|
|
32
47
|
if (existingPid !== null && options?.force) {
|
|
33
48
|
console.log(pc.dim(`Stopping existing daemon (PID: ${existingPid})...`));
|
|
34
49
|
await killProcessTree(existingPid, true);
|
|
50
|
+
// Also stop watcher if running
|
|
51
|
+
const watcherPid = await getRunningWatcherPid();
|
|
52
|
+
if (watcherPid !== null) {
|
|
53
|
+
await stopWatcher(true);
|
|
54
|
+
}
|
|
35
55
|
// Wait for process to terminate
|
|
36
56
|
const maxWait = 3000;
|
|
37
57
|
const startTime = Date.now();
|
|
@@ -44,28 +64,78 @@ export const ccsStartCommand = async (
|
|
|
44
64
|
}
|
|
45
65
|
ensureDaemonDir();
|
|
46
66
|
const logPath = getLogPath();
|
|
67
|
+
if (!options?.keepLogs) {
|
|
68
|
+
try {
|
|
69
|
+
await truncateFile(logPath);
|
|
70
|
+
} catch {
|
|
71
|
+
console.warn(
|
|
72
|
+
pc.yellow(
|
|
73
|
+
`Warning: could not truncate log; continuing (logs will be appended): ${logPath}`,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Detect ports
|
|
79
|
+
const cliproxyPort = await getCliproxyPort();
|
|
80
|
+
const dashboardPort =
|
|
81
|
+
options?.port ??
|
|
82
|
+
(await getPort({
|
|
83
|
+
port: [3000, 3001, 3002, 8000, 8080],
|
|
84
|
+
}));
|
|
85
|
+
// Save ports for stop command
|
|
86
|
+
await writePorts({ dashboard: dashboardPort, cliproxy: cliproxyPort });
|
|
87
|
+
console.log(
|
|
88
|
+
pc.dim(
|
|
89
|
+
`Using dashboard port: ${dashboardPort}, CLIProxy port: ${cliproxyPort}`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
47
92
|
const ccsPath = getCcsExecutable();
|
|
48
93
|
let pid: number | undefined;
|
|
49
94
|
if (process.platform === "win32") {
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
95
|
+
// VBScript is the ONLY reliable way to run a process completely hidden on Windows
|
|
96
|
+
// WScript.Shell.Run with 0 = hidden window, False = don't wait
|
|
97
|
+
// Redirect output to log file using cmd /c with shell redirection
|
|
98
|
+
const escapedLogPath = logPath.replace(/\\/g, "\\\\");
|
|
99
|
+
const vbsContent = `CreateObject("WScript.Shell").Run "cmd /c ${ccsPath} config --port ${dashboardPort} >> ${escapedLogPath} 2>&1", 0, False`;
|
|
100
|
+
const vbsPath = join(tmpdir(), `ccs-start-${Date.now()}.vbs`);
|
|
101
|
+
await Bun.write(vbsPath, vbsContent);
|
|
102
|
+
|
|
103
|
+
// Run the vbs file (wscript itself doesn't show a window)
|
|
104
|
+
const proc = spawn("wscript", [vbsPath], {
|
|
105
|
+
detached: true,
|
|
53
106
|
stdio: "ignore",
|
|
54
107
|
windowsHide: true,
|
|
55
|
-
detached: true,
|
|
56
108
|
});
|
|
57
109
|
proc.unref();
|
|
58
110
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
111
|
+
// Clean up the vbs file after a short delay
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(vbsPath);
|
|
115
|
+
} catch {}
|
|
116
|
+
}, 1000);
|
|
117
|
+
|
|
118
|
+
// Poll for the port to become available
|
|
119
|
+
// ccs config takes ~6s to start (5s CLIProxy timeout + dashboard startup)
|
|
120
|
+
console.log(
|
|
121
|
+
pc.dim("Starting CCS config daemon (this may take a few seconds)..."),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const maxWaitMs = options?.timeout ?? 30000;
|
|
125
|
+
const pollIntervalMs = 500;
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
let foundPid: number | null = null;
|
|
128
|
+
|
|
129
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
130
|
+
foundPid = await getProcessByPort(dashboardPort);
|
|
131
|
+
if (foundPid !== null) break;
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
133
|
+
}
|
|
62
134
|
|
|
63
|
-
// Find the process by port 3000 (dashboard port)
|
|
64
|
-
const foundPid = await getProcessByPort(3000);
|
|
65
135
|
if (foundPid === null) {
|
|
66
136
|
console.log(pc.red("Failed to start CCS config daemon"));
|
|
67
137
|
console.log(
|
|
68
|
-
pc.dim("Check if ccs is installed: npm install -g @
|
|
138
|
+
pc.dim("Check if ccs is installed: npm install -g @kaitranntt/ccs"),
|
|
69
139
|
);
|
|
70
140
|
return;
|
|
71
141
|
}
|
|
@@ -73,7 +143,7 @@ export const ccsStartCommand = async (
|
|
|
73
143
|
} else {
|
|
74
144
|
// On Unix, use regular spawn with detached mode
|
|
75
145
|
const logFd = openSync(logPath, "a");
|
|
76
|
-
const child = spawn(ccsPath, ["config"], {
|
|
146
|
+
const child = spawn(ccsPath, ["config", "--port", String(dashboardPort)], {
|
|
77
147
|
detached: true,
|
|
78
148
|
stdio: ["ignore", logFd, logFd],
|
|
79
149
|
});
|
|
@@ -87,6 +157,17 @@ export const ccsStartCommand = async (
|
|
|
87
157
|
await writePid(pid);
|
|
88
158
|
console.log(pc.green(`CCS config daemon started (PID: ${pid})`));
|
|
89
159
|
console.log(pc.dim(`Logs: ${logPath}`));
|
|
160
|
+
|
|
161
|
+
// Start file watcher unless --no-watch is specified
|
|
162
|
+
if (!options?.noWatch) {
|
|
163
|
+
const watcherPid = await startWatcher();
|
|
164
|
+
if (watcherPid) {
|
|
165
|
+
console.log(pc.dim(`File watcher started (PID: ${watcherPid})`));
|
|
166
|
+
} else {
|
|
167
|
+
console.log(pc.yellow("Warning: Failed to start file watcher"));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
90
171
|
console.log(pc.dim("Run 'ccst ccs status' to check status"));
|
|
91
172
|
console.log(pc.dim("Run 'ccst ccs logs' to view logs"));
|
|
92
173
|
};
|
|
@@ -5,37 +5,56 @@ import {
|
|
|
5
5
|
getPidPath,
|
|
6
6
|
getRunningDaemonPid,
|
|
7
7
|
} from "../../utils/daemon.js";
|
|
8
|
+
import {
|
|
9
|
+
getRunningWatcherPid,
|
|
10
|
+
getWatcherLogPath,
|
|
11
|
+
} from "../../utils/watcher-daemon.js";
|
|
8
12
|
|
|
9
13
|
export const ccsStatusCommand = async (): Promise<void> => {
|
|
14
|
+
// Show daemon status
|
|
10
15
|
const pid = await getRunningDaemonPid();
|
|
11
16
|
if (pid === null) {
|
|
12
17
|
console.log(pc.yellow("CCS config daemon is not running"));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
18
|
+
} else {
|
|
19
|
+
console.log(pc.green(`CCS config daemon is running (PID: ${pid})`));
|
|
20
|
+
const pidPath = getPidPath();
|
|
21
|
+
const logPath = getLogPath();
|
|
22
|
+
console.log(pc.dim(`PID file: ${pidPath}`));
|
|
23
|
+
if (existsSync(logPath)) {
|
|
24
|
+
const stats = statSync(logPath);
|
|
25
|
+
const sizeKb = (stats.size / 1024).toFixed(2);
|
|
26
|
+
console.log(pc.dim(`Log file: ${logPath} (${sizeKb} KB)`));
|
|
27
|
+
}
|
|
28
|
+
// Try to get process uptime (Unix only)
|
|
29
|
+
if (process.platform !== "win32") {
|
|
30
|
+
try {
|
|
31
|
+
const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "etime="], {
|
|
32
|
+
stdout: "pipe",
|
|
33
|
+
stderr: "ignore",
|
|
34
|
+
});
|
|
35
|
+
const output = await new Response(proc.stdout).text();
|
|
36
|
+
const uptime = output.trim();
|
|
37
|
+
if (uptime) {
|
|
38
|
+
console.log(pc.dim(`Uptime: ${uptime}`));
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ps command not available or failed
|
|
36
42
|
}
|
|
37
|
-
} catch {
|
|
38
|
-
// ps command not available or failed
|
|
39
43
|
}
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
// Show watcher status
|
|
47
|
+
console.log();
|
|
48
|
+
const watcherPid = await getRunningWatcherPid();
|
|
49
|
+
if (watcherPid !== null) {
|
|
50
|
+
console.log(pc.green(`File watcher is running (PID: ${watcherPid})`));
|
|
51
|
+
const watcherLogPath = getWatcherLogPath();
|
|
52
|
+
if (existsSync(watcherLogPath)) {
|
|
53
|
+
const stats = statSync(watcherLogPath);
|
|
54
|
+
const sizeKb = (stats.size / 1024).toFixed(2);
|
|
55
|
+
console.log(pc.dim(`Watcher log: ${watcherLogPath} (${sizeKb} KB)`));
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(pc.yellow("File watcher is not running"));
|
|
59
|
+
}
|
|
41
60
|
};
|
package/src/commands/ccs/stop.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
getPortsToKill,
|
|
4
4
|
getRunningDaemonPid,
|
|
5
5
|
isProcessRunning,
|
|
6
6
|
killProcessByPort,
|
|
7
7
|
killProcessTree,
|
|
8
8
|
removePid,
|
|
9
|
+
removePorts,
|
|
9
10
|
} from "../../utils/daemon.js";
|
|
11
|
+
import {
|
|
12
|
+
getRunningWatcherPid,
|
|
13
|
+
stopWatcher,
|
|
14
|
+
} from "../../utils/watcher-daemon.js";
|
|
10
15
|
|
|
11
16
|
export type StopOptions = {
|
|
12
17
|
force?: boolean;
|
|
@@ -32,13 +37,27 @@ export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
|
|
|
32
37
|
stopped = true;
|
|
33
38
|
}
|
|
34
39
|
// Phase 2: Kill processes by port (especially important on Windows)
|
|
35
|
-
|
|
40
|
+
// Use saved ports or fallback to defaults
|
|
41
|
+
const ports = await getPortsToKill();
|
|
42
|
+
for (const port of ports) {
|
|
36
43
|
const killed = await killProcessByPort(port, options?.force ?? true);
|
|
37
44
|
if (killed) {
|
|
38
45
|
console.log(pc.dim(`Cleaned up process on port ${port}`));
|
|
39
46
|
stopped = true;
|
|
40
47
|
}
|
|
41
48
|
}
|
|
49
|
+
// Clean up ports file
|
|
50
|
+
removePorts();
|
|
51
|
+
|
|
52
|
+
// Stop file watcher
|
|
53
|
+
const watcherPid = await getRunningWatcherPid();
|
|
54
|
+
if (watcherPid !== null) {
|
|
55
|
+
const watcherStopped = await stopWatcher(options?.force);
|
|
56
|
+
if (watcherStopped) {
|
|
57
|
+
console.log(pc.dim(`File watcher stopped (PID: ${watcherPid})`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
if (!stopped) {
|
|
43
62
|
console.log(pc.yellow("CCS config daemon is not running"));
|
|
44
63
|
}
|
|
@@ -7,7 +7,7 @@ import { deepMerge } from "../../utils/deep-merge.js";
|
|
|
7
7
|
import { readJson, readJsonIfExists } from "../../utils/json.js";
|
|
8
8
|
|
|
9
9
|
const defaultConfigsDir = (): string => path.join(homedir(), ".ccst");
|
|
10
|
-
const ccsDir = (): string => path.join(homedir(), ".ccs");
|
|
10
|
+
export const ccsDir = (): string => path.join(homedir(), ".ccs");
|
|
11
11
|
|
|
12
12
|
const ensureDefaultConfig = async (
|
|
13
13
|
manager: ContextManager,
|
|
@@ -49,9 +49,10 @@ const importProfile = async (
|
|
|
49
49
|
profileName: string,
|
|
50
50
|
merged: Record<string, unknown>,
|
|
51
51
|
): Promise<void> => {
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
52
|
+
// Write directly to the context file, overwriting if exists
|
|
53
|
+
const contextPath = path.join(manager.contextsDir, `${profileName}.json`);
|
|
54
|
+
const content = `${JSON.stringify(merged, null, 2)}\n`;
|
|
55
|
+
await Bun.write(contextPath, content);
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
const createDefaultProfileIfNeeded = async (
|
|
@@ -65,15 +66,19 @@ const createDefaultProfileIfNeeded = async (
|
|
|
65
66
|
await importProfile(manager, "default", defaultProfile);
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
export
|
|
69
|
+
export type PerformCcsImportResult = {
|
|
70
|
+
importedCount: number;
|
|
71
|
+
profileNames: string[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const performCcsImport = async (
|
|
69
75
|
manager: ContextManager,
|
|
70
76
|
configsDir?: string,
|
|
71
|
-
): Promise<
|
|
77
|
+
): Promise<PerformCcsImportResult> => {
|
|
72
78
|
const ccsPath = ccsDir();
|
|
73
79
|
if (!existsSync(ccsPath)) {
|
|
74
80
|
throw new Error(`CCS directory not found: ${ccsPath}`);
|
|
75
81
|
}
|
|
76
|
-
console.log(`📥 Importing profiles from CCS settings...`);
|
|
77
82
|
const dir = configsDir ?? defaultConfigsDir();
|
|
78
83
|
const { created } = await ensureDefaultConfig(manager, dir);
|
|
79
84
|
const defaultConfig = await loadDefaultConfig(dir);
|
|
@@ -91,23 +96,34 @@ export const importFromCcs = async (
|
|
|
91
96
|
} catch {
|
|
92
97
|
entries = [];
|
|
93
98
|
}
|
|
94
|
-
|
|
99
|
+
const profileNames: string[] = [];
|
|
95
100
|
for (const fileName of entries) {
|
|
96
101
|
const settingsPath = path.join(ccsPath, fileName);
|
|
97
102
|
const profileName = fileName.replace(/\.settings\.json$/u, "");
|
|
98
|
-
console.log(` 📦 Importing '${colors.cyan(profileName)}'...`);
|
|
99
103
|
const settings = await readJson<Record<string, unknown>>(settingsPath);
|
|
100
104
|
const merged = deepMerge(defaultConfig, settings);
|
|
101
105
|
if (currentContext && currentContext === profileName) {
|
|
102
106
|
await manager.unsetContext();
|
|
103
107
|
}
|
|
104
108
|
await importProfile(manager, profileName, merged);
|
|
105
|
-
|
|
109
|
+
profileNames.push(profileName);
|
|
106
110
|
}
|
|
107
111
|
if (currentContext) {
|
|
108
112
|
await manager.switchContext(currentContext);
|
|
109
113
|
}
|
|
114
|
+
return { importedCount: profileNames.length, profileNames };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const importFromCcs = async (
|
|
118
|
+
manager: ContextManager,
|
|
119
|
+
configsDir?: string,
|
|
120
|
+
): Promise<void> => {
|
|
121
|
+
console.log(`📥 Importing profiles from CCS settings...`);
|
|
122
|
+
const result = await performCcsImport(manager, configsDir);
|
|
123
|
+
for (const profileName of result.profileNames) {
|
|
124
|
+
console.log(` 📦 Imported '${colors.cyan(profileName)}'`);
|
|
125
|
+
}
|
|
110
126
|
console.log(
|
|
111
|
-
`✅ Imported ${colors.bold(colors.green(String(importedCount)))} profiles from CCS`,
|
|
127
|
+
`✅ Imported ${colors.bold(colors.green(String(result.importedCount)))} profiles from CCS`,
|
|
112
128
|
);
|
|
113
129
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import pkg from "../package.json";
|
|
4
|
+
import { ccsInstallCommand } from "./commands/ccs/install.js";
|
|
4
5
|
import { ccsLogsCommand } from "./commands/ccs/logs.js";
|
|
6
|
+
import { ccsSetupCommand } from "./commands/ccs/setup.js";
|
|
5
7
|
import { ccsStartCommand } from "./commands/ccs/start.js";
|
|
6
8
|
import { ccsStatusCommand } from "./commands/ccs/status.js";
|
|
7
9
|
import { ccsStopCommand } from "./commands/ccs/stop.js";
|
|
@@ -188,8 +190,25 @@ const main = async (): Promise<void> => {
|
|
|
188
190
|
.command("start")
|
|
189
191
|
.description("Start CCS config as background daemon")
|
|
190
192
|
.option("-f, --force", "Force restart if already running")
|
|
193
|
+
.option("--keep-logs", "Keep existing log file (append)")
|
|
194
|
+
.option(
|
|
195
|
+
"-p, --port <number>",
|
|
196
|
+
"Dashboard port (auto-detect if not specified)",
|
|
197
|
+
)
|
|
198
|
+
.option("-W, --no-watch", "Skip file watcher")
|
|
199
|
+
.option(
|
|
200
|
+
"-t, --timeout <seconds>",
|
|
201
|
+
"Timeout in seconds for daemon startup (Windows only)",
|
|
202
|
+
"30",
|
|
203
|
+
)
|
|
191
204
|
.action(async (options) => {
|
|
192
|
-
await ccsStartCommand(
|
|
205
|
+
await ccsStartCommand({
|
|
206
|
+
force: options.force,
|
|
207
|
+
keepLogs: options.keepLogs,
|
|
208
|
+
port: options.port ? Number.parseInt(options.port, 10) : undefined,
|
|
209
|
+
noWatch: options.watch === false,
|
|
210
|
+
timeout: Number.parseInt(options.timeout, 10) * 1000,
|
|
211
|
+
});
|
|
193
212
|
});
|
|
194
213
|
ccsCommandGroup
|
|
195
214
|
.command("stop")
|
|
@@ -215,6 +234,19 @@ const main = async (): Promise<void> => {
|
|
|
215
234
|
lines: parseInt(options.lines, 10),
|
|
216
235
|
});
|
|
217
236
|
});
|
|
237
|
+
ccsCommandGroup
|
|
238
|
+
.command("setup")
|
|
239
|
+
.description("Run CCS initial setup")
|
|
240
|
+
.option("-f, --force", "Force setup even if already configured")
|
|
241
|
+
.action(async (options) => {
|
|
242
|
+
await ccsSetupCommand(options);
|
|
243
|
+
});
|
|
244
|
+
ccsCommandGroup
|
|
245
|
+
.command("install")
|
|
246
|
+
.description("Install CCS CLI tool")
|
|
247
|
+
.action(async () => {
|
|
248
|
+
await ccsInstallCommand();
|
|
249
|
+
});
|
|
218
250
|
try {
|
|
219
251
|
await program.parseAsync(process.argv);
|
|
220
252
|
} catch {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, watch } from "node:fs";
|
|
3
|
+
import { ccsDir, performCcsImport } from "../commands/import-profiles/ccs.js";
|
|
4
|
+
import { ContextManager } from "../core/context-manager.js";
|
|
5
|
+
import { getPaths } from "../utils/paths.js";
|
|
6
|
+
|
|
7
|
+
const DEBOUNCE_MS = 1000;
|
|
8
|
+
|
|
9
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
|
+
let isImporting = false;
|
|
11
|
+
|
|
12
|
+
const log = (message: string): void => {
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
console.log(`[${timestamp}] ${message}`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const runImport = async (): Promise<void> => {
|
|
18
|
+
if (isImporting) {
|
|
19
|
+
log("Import already in progress, skipping");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
isImporting = true;
|
|
23
|
+
try {
|
|
24
|
+
const manager = new ContextManager(getPaths("user"));
|
|
25
|
+
const result = await performCcsImport(manager);
|
|
26
|
+
if (result.importedCount > 0) {
|
|
27
|
+
log(
|
|
28
|
+
`Imported ${result.importedCount} profiles: ${result.profileNames.join(", ")}`,
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
log("No profiles to import");
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
log(`Import error: ${error}`);
|
|
35
|
+
} finally {
|
|
36
|
+
isImporting = false;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleFileChange = (eventType: string, filename: string | null): void => {
|
|
41
|
+
if (!filename || !filename.endsWith(".settings.json")) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
log(`Detected ${eventType} on ${filename}`);
|
|
45
|
+
|
|
46
|
+
// Debounce
|
|
47
|
+
if (debounceTimer) {
|
|
48
|
+
clearTimeout(debounceTimer);
|
|
49
|
+
}
|
|
50
|
+
debounceTimer = setTimeout(runImport, DEBOUNCE_MS);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const main = async (): Promise<void> => {
|
|
54
|
+
const ccsPath = ccsDir();
|
|
55
|
+
|
|
56
|
+
log(`CCS Settings Watcher - PID: ${process.pid}`);
|
|
57
|
+
log(`Watching directory: ${ccsPath}`);
|
|
58
|
+
|
|
59
|
+
if (!existsSync(ccsPath)) {
|
|
60
|
+
log(`ERROR: CCS directory not found: ${ccsPath}`);
|
|
61
|
+
log("Please run 'ccs setup' first to initialize CCS");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Run initial import
|
|
66
|
+
log("Running initial import...");
|
|
67
|
+
await runImport();
|
|
68
|
+
|
|
69
|
+
// Start watching
|
|
70
|
+
const watcher = watch(ccsPath, { persistent: true }, handleFileChange);
|
|
71
|
+
|
|
72
|
+
// Graceful shutdown
|
|
73
|
+
const cleanup = (): void => {
|
|
74
|
+
log("Shutting down watcher");
|
|
75
|
+
if (debounceTimer) {
|
|
76
|
+
clearTimeout(debounceTimer);
|
|
77
|
+
}
|
|
78
|
+
watcher.close();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.on("SIGINT", cleanup);
|
|
83
|
+
process.on("SIGTERM", cleanup);
|
|
84
|
+
|
|
85
|
+
log("Watcher started successfully");
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await main();
|
package/src/utils/daemon.ts
CHANGED
|
@@ -11,6 +11,11 @@ export const getPidPath = (): string => join(getDaemonDir(), "ccs-config.pid");
|
|
|
11
11
|
// Log file path
|
|
12
12
|
export const getLogPath = (): string => join(getDaemonDir(), "ccs-config.log");
|
|
13
13
|
|
|
14
|
+
// Truncate a file (or create it empty if missing)
|
|
15
|
+
export const truncateFile = async (filePath: string): Promise<void> => {
|
|
16
|
+
await Bun.write(filePath, "");
|
|
17
|
+
};
|
|
18
|
+
|
|
14
19
|
// Ensure daemon directory exists
|
|
15
20
|
export const ensureDaemonDir = (): void => {
|
|
16
21
|
const dir = getDaemonDir();
|
|
@@ -82,8 +87,84 @@ export const getCcsExecutable = (): string => {
|
|
|
82
87
|
return "ccs";
|
|
83
88
|
};
|
|
84
89
|
|
|
85
|
-
//
|
|
86
|
-
export const
|
|
90
|
+
// Default CCS daemon ports (fallback)
|
|
91
|
+
export const DEFAULT_DASHBOARD_PORT = 3000;
|
|
92
|
+
export const DEFAULT_CLIPROXY_PORT = 8317;
|
|
93
|
+
|
|
94
|
+
// Ports file path
|
|
95
|
+
export const getPortsPath = (): string => join(getDaemonDir(), "ports.json");
|
|
96
|
+
|
|
97
|
+
// Type for daemon ports
|
|
98
|
+
export type DaemonPorts = {
|
|
99
|
+
dashboard: number;
|
|
100
|
+
cliproxy: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Read saved ports
|
|
104
|
+
export const readPorts = async (): Promise<DaemonPorts | null> => {
|
|
105
|
+
const portsPath = getPortsPath();
|
|
106
|
+
if (!existsSync(portsPath)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const content = await Bun.file(portsPath).text();
|
|
111
|
+
const ports = JSON.parse(content) as DaemonPorts;
|
|
112
|
+
if (
|
|
113
|
+
typeof ports.dashboard === "number" &&
|
|
114
|
+
typeof ports.cliproxy === "number"
|
|
115
|
+
) {
|
|
116
|
+
return ports;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Write ports to file
|
|
125
|
+
export const writePorts = async (ports: DaemonPorts): Promise<void> => {
|
|
126
|
+
ensureDaemonDir();
|
|
127
|
+
await Bun.write(getPortsPath(), JSON.stringify(ports, null, 2));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Remove ports file
|
|
131
|
+
export const removePorts = (): void => {
|
|
132
|
+
const portsPath = getPortsPath();
|
|
133
|
+
if (existsSync(portsPath)) {
|
|
134
|
+
unlinkSync(portsPath);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Read CLIProxy port from config.yaml
|
|
139
|
+
export const getCliproxyPort = async (): Promise<number> => {
|
|
140
|
+
const configPath = join(homedir(), ".ccs", "cliproxy", "config.yaml");
|
|
141
|
+
if (!existsSync(configPath)) {
|
|
142
|
+
return DEFAULT_CLIPROXY_PORT;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const content = await Bun.file(configPath).text();
|
|
146
|
+
// Simple YAML parsing for "port: XXXX"
|
|
147
|
+
const match = content.match(/^port:\s*(\d+)/m);
|
|
148
|
+
if (match?.[1]) {
|
|
149
|
+
const port = Number.parseInt(match[1], 10);
|
|
150
|
+
if (Number.isFinite(port) && port > 0) {
|
|
151
|
+
return port;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return DEFAULT_CLIPROXY_PORT;
|
|
155
|
+
} catch {
|
|
156
|
+
return DEFAULT_CLIPROXY_PORT;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Get ports to kill (from saved file or defaults)
|
|
161
|
+
export const getPortsToKill = async (): Promise<number[]> => {
|
|
162
|
+
const saved = await readPorts();
|
|
163
|
+
if (saved) {
|
|
164
|
+
return [saved.dashboard, saved.cliproxy];
|
|
165
|
+
}
|
|
166
|
+
return [DEFAULT_DASHBOARD_PORT, DEFAULT_CLIPROXY_PORT];
|
|
167
|
+
};
|
|
87
168
|
|
|
88
169
|
// Kill process tree (on Windows, kills all child processes)
|
|
89
170
|
export const killProcessTree = async (
|
package/src/utils/interactive.ts
CHANGED
|
@@ -71,6 +71,9 @@ const readStdinLine = async (): Promise<string> => {
|
|
|
71
71
|
const cleanup = () => {
|
|
72
72
|
process.stdin.off("data", onData);
|
|
73
73
|
process.stdin.off("end", onEnd);
|
|
74
|
+
if (process.stdin.isTTY) {
|
|
75
|
+
process.stdin.pause();
|
|
76
|
+
}
|
|
74
77
|
};
|
|
75
78
|
if (process.stdin.isTTY) {
|
|
76
79
|
process.stdin.resume();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync, openSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ensureDaemonDir,
|
|
5
|
+
getDaemonDir,
|
|
6
|
+
isProcessRunning,
|
|
7
|
+
killProcessTree,
|
|
8
|
+
truncateFile,
|
|
9
|
+
} from "./daemon.js";
|
|
10
|
+
|
|
11
|
+
// Watcher PID file path
|
|
12
|
+
export const getWatcherPidPath = (): string =>
|
|
13
|
+
join(getDaemonDir(), "watcher.pid");
|
|
14
|
+
|
|
15
|
+
// Watcher log file path
|
|
16
|
+
export const getWatcherLogPath = (): string =>
|
|
17
|
+
join(getDaemonDir(), "watcher.log");
|
|
18
|
+
|
|
19
|
+
// Read watcher PID from file
|
|
20
|
+
export const readWatcherPid = async (): Promise<number | null> => {
|
|
21
|
+
const pidPath = getWatcherPidPath();
|
|
22
|
+
if (!existsSync(pidPath)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const content = await Bun.file(pidPath).text();
|
|
27
|
+
const pid = parseInt(content.trim(), 10);
|
|
28
|
+
return Number.isFinite(pid) ? pid : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Write watcher PID to file
|
|
35
|
+
export const writeWatcherPid = async (pid: number): Promise<void> => {
|
|
36
|
+
ensureDaemonDir();
|
|
37
|
+
await Bun.write(getWatcherPidPath(), String(pid));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Remove watcher PID file
|
|
41
|
+
export const removeWatcherPid = (): void => {
|
|
42
|
+
const pidPath = getWatcherPidPath();
|
|
43
|
+
if (existsSync(pidPath)) {
|
|
44
|
+
unlinkSync(pidPath);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Get running watcher PID (validates process is actually running)
|
|
49
|
+
export const getRunningWatcherPid = async (): Promise<number | null> => {
|
|
50
|
+
const pid = await readWatcherPid();
|
|
51
|
+
if (pid === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (!isProcessRunning(pid)) {
|
|
55
|
+
// Stale PID file - clean it up
|
|
56
|
+
removeWatcherPid();
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return pid;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Get watcher script path
|
|
63
|
+
const getWatcherScriptPath = (): string => {
|
|
64
|
+
// The script is in src/scripts/watcher.ts relative to the package
|
|
65
|
+
// When running as installed package, we need to find it
|
|
66
|
+
const scriptPath = join(import.meta.dir, "..", "scripts", "watcher.ts");
|
|
67
|
+
return scriptPath;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Start watcher process
|
|
71
|
+
export const startWatcher = async (): Promise<number | null> => {
|
|
72
|
+
const existingPid = await getRunningWatcherPid();
|
|
73
|
+
if (existingPid !== null) {
|
|
74
|
+
return existingPid;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ensureDaemonDir();
|
|
78
|
+
const logPath = getWatcherLogPath();
|
|
79
|
+
const scriptPath = getWatcherScriptPath();
|
|
80
|
+
|
|
81
|
+
// Truncate log file
|
|
82
|
+
await truncateFile(logPath);
|
|
83
|
+
|
|
84
|
+
if (process.platform === "win32") {
|
|
85
|
+
return startWatcherWindows(scriptPath, logPath);
|
|
86
|
+
}
|
|
87
|
+
return startWatcherUnix(scriptPath, logPath);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Start watcher on Windows using VBScript to hide console
|
|
91
|
+
const startWatcherWindows = async (
|
|
92
|
+
scriptPath: string,
|
|
93
|
+
logPath: string,
|
|
94
|
+
): Promise<number | null> => {
|
|
95
|
+
const vbsPath = join(getDaemonDir(), "start-watcher.vbs");
|
|
96
|
+
const vbsContent = `
|
|
97
|
+
Set WshShell = CreateObject("WScript.Shell")
|
|
98
|
+
WshShell.Run "cmd /c bun run ""${scriptPath}"" >> ""${logPath}"" 2>&1", 0, False
|
|
99
|
+
`.trim();
|
|
100
|
+
|
|
101
|
+
await Bun.write(vbsPath, vbsContent);
|
|
102
|
+
|
|
103
|
+
const proc = Bun.spawn(["wscript", "//Nologo", vbsPath], {
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
106
|
+
});
|
|
107
|
+
proc.unref();
|
|
108
|
+
|
|
109
|
+
// Clean up VBS file after short delay
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
try {
|
|
112
|
+
unlinkSync(vbsPath);
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore cleanup errors
|
|
115
|
+
}
|
|
116
|
+
}, 1000);
|
|
117
|
+
|
|
118
|
+
// Poll for log file to have content (indicating process started)
|
|
119
|
+
const maxWait = 5000;
|
|
120
|
+
const interval = 200;
|
|
121
|
+
let waited = 0;
|
|
122
|
+
|
|
123
|
+
while (waited < maxWait) {
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
125
|
+
waited += interval;
|
|
126
|
+
|
|
127
|
+
// Check if log file has content
|
|
128
|
+
if (existsSync(logPath)) {
|
|
129
|
+
const file = Bun.file(logPath);
|
|
130
|
+
const size = file.size;
|
|
131
|
+
if (size > 0) {
|
|
132
|
+
// Try to find the PID by reading the log
|
|
133
|
+
const content = await file.text();
|
|
134
|
+
const match = content.match(/PID:\s*(\d+)/);
|
|
135
|
+
if (match?.[1]) {
|
|
136
|
+
const pid = Number.parseInt(match[1], 10);
|
|
137
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
138
|
+
await writeWatcherPid(pid);
|
|
139
|
+
return pid;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Start watcher on Unix
|
|
150
|
+
const startWatcherUnix = async (
|
|
151
|
+
scriptPath: string,
|
|
152
|
+
logPath: string,
|
|
153
|
+
): Promise<number | null> => {
|
|
154
|
+
const logFd = openSync(logPath, "a");
|
|
155
|
+
|
|
156
|
+
const child = Bun.spawn(["bun", "run", scriptPath], {
|
|
157
|
+
detached: true,
|
|
158
|
+
stdio: ["ignore", logFd, logFd],
|
|
159
|
+
});
|
|
160
|
+
child.unref();
|
|
161
|
+
|
|
162
|
+
const pid = child.pid;
|
|
163
|
+
if (pid) {
|
|
164
|
+
await writeWatcherPid(pid);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return pid;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Stop watcher process
|
|
171
|
+
export const stopWatcher = async (force?: boolean): Promise<boolean> => {
|
|
172
|
+
const pid = await readWatcherPid();
|
|
173
|
+
if (pid === null) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const killed = await killProcessTree(pid, force);
|
|
178
|
+
|
|
179
|
+
// Wait for process to terminate
|
|
180
|
+
const maxWait = force ? 1000 : 5000;
|
|
181
|
+
const interval = 100;
|
|
182
|
+
let waited = 0;
|
|
183
|
+
|
|
184
|
+
while (waited < maxWait && isProcessRunning(pid)) {
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
186
|
+
waited += interval;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
removeWatcherPid();
|
|
190
|
+
return killed || !isProcessRunning(pid);
|
|
191
|
+
};
|