@leynier/ccst 0.7.0 → 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 +3 -2
- package/readme.md +112 -0
- package/src/commands/ccs/start.ts +24 -1
- package/src/commands/ccs/status.ts +44 -25
- package/src/commands/ccs/stop.ts +14 -0
- package/src/commands/import-profiles/ccs.ts +27 -11
- package/src/index.ts +8 -0
- package/src/scripts/watcher.ts +88 -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",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"files": [
|
|
47
47
|
"src",
|
|
48
48
|
"index.ts",
|
|
49
|
+
"readme.md",
|
|
49
50
|
"README.md",
|
|
50
51
|
"LICENSE",
|
|
51
52
|
"AGENTS.md",
|
|
@@ -57,4 +58,4 @@
|
|
|
57
58
|
"publishConfig": {
|
|
58
59
|
"access": "public"
|
|
59
60
|
}
|
|
60
|
-
}
|
|
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
|
|
|
@@ -17,11 +17,18 @@ import {
|
|
|
17
17
|
writePid,
|
|
18
18
|
writePorts,
|
|
19
19
|
} from "../../utils/daemon.js";
|
|
20
|
+
import {
|
|
21
|
+
getRunningWatcherPid,
|
|
22
|
+
startWatcher,
|
|
23
|
+
stopWatcher,
|
|
24
|
+
} from "../../utils/watcher-daemon.js";
|
|
20
25
|
|
|
21
26
|
export type StartOptions = {
|
|
22
27
|
force?: boolean;
|
|
23
28
|
keepLogs?: boolean;
|
|
24
29
|
port?: number;
|
|
30
|
+
noWatch?: boolean;
|
|
31
|
+
timeout?: number;
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
export const ccsStartCommand = async (
|
|
@@ -40,6 +47,11 @@ export const ccsStartCommand = async (
|
|
|
40
47
|
if (existingPid !== null && options?.force) {
|
|
41
48
|
console.log(pc.dim(`Stopping existing daemon (PID: ${existingPid})...`));
|
|
42
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
|
+
}
|
|
43
55
|
// Wait for process to terminate
|
|
44
56
|
const maxWait = 3000;
|
|
45
57
|
const startTime = Date.now();
|
|
@@ -109,7 +121,7 @@ export const ccsStartCommand = async (
|
|
|
109
121
|
pc.dim("Starting CCS config daemon (this may take a few seconds)..."),
|
|
110
122
|
);
|
|
111
123
|
|
|
112
|
-
const maxWaitMs =
|
|
124
|
+
const maxWaitMs = options?.timeout ?? 30000;
|
|
113
125
|
const pollIntervalMs = 500;
|
|
114
126
|
const startTime = Date.now();
|
|
115
127
|
let foundPid: number | null = null;
|
|
@@ -145,6 +157,17 @@ export const ccsStartCommand = async (
|
|
|
145
157
|
await writePid(pid);
|
|
146
158
|
console.log(pc.green(`CCS config daemon started (PID: ${pid})`));
|
|
147
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
|
+
|
|
148
171
|
console.log(pc.dim("Run 'ccst ccs status' to check status"));
|
|
149
172
|
console.log(pc.dim("Run 'ccst ccs logs' to view logs"));
|
|
150
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
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
removePid,
|
|
9
9
|
removePorts,
|
|
10
10
|
} from "../../utils/daemon.js";
|
|
11
|
+
import {
|
|
12
|
+
getRunningWatcherPid,
|
|
13
|
+
stopWatcher,
|
|
14
|
+
} from "../../utils/watcher-daemon.js";
|
|
11
15
|
|
|
12
16
|
export type StopOptions = {
|
|
13
17
|
force?: boolean;
|
|
@@ -44,6 +48,16 @@ export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
|
|
|
44
48
|
}
|
|
45
49
|
// Clean up ports file
|
|
46
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
|
+
|
|
47
61
|
if (!stopped) {
|
|
48
62
|
console.log(pc.yellow("CCS config daemon is not running"));
|
|
49
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
|
@@ -195,11 +195,19 @@ const main = async (): Promise<void> => {
|
|
|
195
195
|
"-p, --port <number>",
|
|
196
196
|
"Dashboard port (auto-detect if not specified)",
|
|
197
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
|
+
)
|
|
198
204
|
.action(async (options) => {
|
|
199
205
|
await ccsStartCommand({
|
|
200
206
|
force: options.force,
|
|
201
207
|
keepLogs: options.keepLogs,
|
|
202
208
|
port: options.port ? Number.parseInt(options.port, 10) : undefined,
|
|
209
|
+
noWatch: options.watch === false,
|
|
210
|
+
timeout: Number.parseInt(options.timeout, 10) * 1000,
|
|
203
211
|
});
|
|
204
212
|
});
|
|
205
213
|
ccsCommandGroup
|
|
@@ -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();
|
|
@@ -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
|
+
};
|