contextswitch 0.1.1
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/README.md +185 -0
- package/dist/archive-3IGWZBSO.js +12 -0
- package/dist/chunk-2LUEUGBV.js +110 -0
- package/dist/chunk-56TY2J6E.js +366 -0
- package/dist/chunk-A7YXSI66.js +163 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/chunk-UKMZ4CUZ.js +116 -0
- package/dist/chunk-XGE4JP55.js +303 -0
- package/dist/cli.js +239 -0
- package/dist/process-WBBCEFGG.js +10 -0
- package/dist/reset-3DSXZKRH.js +60 -0
- package/dist/session-YAMF4YD7.js +9 -0
- package/dist/switch-MWKYEYHE.js +218 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# ContextSwitch
|
|
2
|
+
|
|
3
|
+
Domain-based context management for Claude Code CLI. Switch between different project contexts while preserving session history.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
✅ **Session Persistence** - Resume previous Claude sessions when switching domains
|
|
8
|
+
✅ **Domain Management** - Organize projects with separate configurations
|
|
9
|
+
✅ **MCP Server Support** - Configure Model Context Protocol servers per domain
|
|
10
|
+
✅ **Cross-Platform** - Works on macOS, Linux, and Windows
|
|
11
|
+
✅ **Session Archives** - Save and restore session snapshots
|
|
12
|
+
✅ **Process Management** - Clean switching between Claude instances
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Quick Install
|
|
17
|
+
```bash
|
|
18
|
+
# Clone the repository
|
|
19
|
+
git clone https://github.com/yourusername/contextswitch.git
|
|
20
|
+
cd contextswitch
|
|
21
|
+
|
|
22
|
+
# Run the installation script
|
|
23
|
+
chmod +x install.sh
|
|
24
|
+
./install.sh
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This sets up the `cs` command for easy use. For other installation methods, see [INSTALLATION.md](INSTALLATION.md).
|
|
28
|
+
|
|
29
|
+
### Manual Install
|
|
30
|
+
```bash
|
|
31
|
+
# Install dependencies
|
|
32
|
+
npm install
|
|
33
|
+
|
|
34
|
+
# Build the project
|
|
35
|
+
npm run build:node
|
|
36
|
+
|
|
37
|
+
# Add alias to your shell
|
|
38
|
+
echo "alias cs='node $(pwd)/dist/cli.js'" >> ~/.zshrc
|
|
39
|
+
source ~/.zshrc
|
|
40
|
+
|
|
41
|
+
# Now you can use 'cs' command
|
|
42
|
+
cs --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Initialize ContextSwitch
|
|
49
|
+
node dist/cli.js init
|
|
50
|
+
|
|
51
|
+
# Create a new domain for your project (working directory is REQUIRED)
|
|
52
|
+
node dist/cli.js domain add backend --working-dir ~/projects/api
|
|
53
|
+
|
|
54
|
+
# The working directory is where Claude will operate when you switch to this domain
|
|
55
|
+
node dist/cli.js switch backend
|
|
56
|
+
|
|
57
|
+
# Check status
|
|
58
|
+
node dist/cli.js status
|
|
59
|
+
|
|
60
|
+
# List all domains
|
|
61
|
+
node dist/cli.js list
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
### Core Commands
|
|
67
|
+
|
|
68
|
+
- `cs init` - Initialize ContextSwitch configuration
|
|
69
|
+
- `cs switch <domain>` - Switch to a different domain
|
|
70
|
+
- `cs list` - List all available domains
|
|
71
|
+
- `cs status [domain]` - Show current domain and session status
|
|
72
|
+
|
|
73
|
+
### Domain Management
|
|
74
|
+
|
|
75
|
+
- `cs domain add <name>` - Create a new domain
|
|
76
|
+
- `cs domain remove <name>` - Delete a domain
|
|
77
|
+
- `cs reset <domain>` - Reset domain session (with auto-archive)
|
|
78
|
+
- `cs archive <domain>` - Archive current session
|
|
79
|
+
|
|
80
|
+
### Diagnostics
|
|
81
|
+
|
|
82
|
+
- `cs doctor` - Check system configuration
|
|
83
|
+
- `cs --help` - Show help information
|
|
84
|
+
- `cs --version` - Show version
|
|
85
|
+
|
|
86
|
+
## Domain Configuration
|
|
87
|
+
|
|
88
|
+
Domains are stored as YAML files in the configuration directory. Example domain:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
name: backend
|
|
92
|
+
workingDirectory: ~/projects/api
|
|
93
|
+
|
|
94
|
+
claudeConfig:
|
|
95
|
+
instructions: ./CLAUDE.md
|
|
96
|
+
memory:
|
|
97
|
+
- ./context/api-overview.md
|
|
98
|
+
|
|
99
|
+
mcpServers:
|
|
100
|
+
filesystem:
|
|
101
|
+
command: npx
|
|
102
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
|
103
|
+
env:
|
|
104
|
+
DIRECTORY: "./"
|
|
105
|
+
|
|
106
|
+
env:
|
|
107
|
+
NODE_ENV: development
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Project Structure
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
contextswitch/
|
|
114
|
+
├── src/
|
|
115
|
+
│ ├── cli.ts # Main CLI entry point
|
|
116
|
+
│ ├── commands/ # Command implementations
|
|
117
|
+
│ │ ├── switch.ts # Domain switching logic
|
|
118
|
+
│ │ ├── reset.ts # Session reset
|
|
119
|
+
│ │ └── archive.ts # Session archiving
|
|
120
|
+
│ └── core/ # Core modules
|
|
121
|
+
│ ├── domain.ts # Domain schemas (Zod)
|
|
122
|
+
│ ├── config.ts # Configuration management
|
|
123
|
+
│ ├── session.ts # Session ID generation
|
|
124
|
+
│ ├── process.ts # Process management
|
|
125
|
+
│ └── paths.ts # Cross-platform paths
|
|
126
|
+
├── domains/ # Domain templates
|
|
127
|
+
├── package.json
|
|
128
|
+
├── tsconfig.json
|
|
129
|
+
└── README.md
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Configuration Files
|
|
133
|
+
|
|
134
|
+
- **Domains**: `~/Library/Application Support/contextswitch/domains/*.yml` (macOS)
|
|
135
|
+
- **State**: `~/Library/Application Support/contextswitch/state.json`
|
|
136
|
+
- **Archives**: `~/Library/Application Support/contextswitch/archives/`
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Run TypeScript compiler check
|
|
142
|
+
npx tsc --noEmit
|
|
143
|
+
|
|
144
|
+
# Run in development mode
|
|
145
|
+
npm run dev
|
|
146
|
+
|
|
147
|
+
# Build for production
|
|
148
|
+
npm run build:node
|
|
149
|
+
|
|
150
|
+
# Run tests (when implemented)
|
|
151
|
+
npm test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Technical Details
|
|
155
|
+
|
|
156
|
+
- Built with TypeScript and Commander.js
|
|
157
|
+
- Uses Zod for runtime validation
|
|
158
|
+
- Deterministic session IDs via UUID v5
|
|
159
|
+
- Atomic file operations for data safety
|
|
160
|
+
- Cross-platform process management
|
|
161
|
+
|
|
162
|
+
## Status
|
|
163
|
+
|
|
164
|
+
**Current Version**: 0.1.0 (MVP)
|
|
165
|
+
|
|
166
|
+
### Implemented
|
|
167
|
+
- ✅ Core domain management
|
|
168
|
+
- ✅ Session persistence with `--resume`
|
|
169
|
+
- ✅ Process management (kill/spawn Claude)
|
|
170
|
+
- ✅ State tracking and atomic writes
|
|
171
|
+
- ✅ Cross-platform path resolution
|
|
172
|
+
- ✅ Basic CLI commands
|
|
173
|
+
- ✅ Archive functionality
|
|
174
|
+
|
|
175
|
+
### Not Yet Implemented
|
|
176
|
+
- ⏳ Git sync for team sharing
|
|
177
|
+
- ⏳ Token usage estimation
|
|
178
|
+
- ⏳ Advanced diagnostics
|
|
179
|
+
- ⏳ Binary compilation with Bun
|
|
180
|
+
- ⏳ Automated tests
|
|
181
|
+
- ⏳ Shell integration scripts
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configManager
|
|
3
|
+
} from "./chunk-XGE4JP55.js";
|
|
4
|
+
import {
|
|
5
|
+
init_session,
|
|
6
|
+
session_exports
|
|
7
|
+
} from "./chunk-UKMZ4CUZ.js";
|
|
8
|
+
import {
|
|
9
|
+
paths
|
|
10
|
+
} from "./chunk-A7YXSI66.js";
|
|
11
|
+
import {
|
|
12
|
+
__toCommonJS
|
|
13
|
+
} from "./chunk-PNKVD2UK.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/archive.ts
|
|
16
|
+
import picocolors from "picocolors";
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, readdirSync } from "fs";
|
|
18
|
+
import { join, basename } from "path";
|
|
19
|
+
var pc = picocolors;
|
|
20
|
+
async function archiveCommand(domainName) {
|
|
21
|
+
try {
|
|
22
|
+
if (!configManager.domainExists(domainName)) {
|
|
23
|
+
throw new Error(`Domain '${domainName}' not found`);
|
|
24
|
+
}
|
|
25
|
+
const state = configManager.loadState();
|
|
26
|
+
const session = state.sessions?.[domainName];
|
|
27
|
+
if (!session) {
|
|
28
|
+
console.log(pc.yellow(`Domain '${domainName}' has no active session to archive.`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(pc.cyan(`\u{1F4E6} Archiving session for domain '${domainName}'...`));
|
|
32
|
+
const archiveDir = join(paths.archivesDir, domainName);
|
|
33
|
+
if (!existsSync(archiveDir)) {
|
|
34
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
37
|
+
const archiveMetadata = {
|
|
38
|
+
domain: domainName,
|
|
39
|
+
session,
|
|
40
|
+
timestamp: timestamp.toISOString(),
|
|
41
|
+
config: configManager.loadDomain(domainName)
|
|
42
|
+
};
|
|
43
|
+
const dateStr = timestamp.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, -5);
|
|
44
|
+
const archiveName = `${dateStr}.json`;
|
|
45
|
+
const archivePath = join(archiveDir, archiveName);
|
|
46
|
+
writeFileSync(archivePath, JSON.stringify(archiveMetadata, null, 2), "utf-8");
|
|
47
|
+
const domain = configManager.loadDomain(domainName);
|
|
48
|
+
if (domain.claudeConfig?.memory) {
|
|
49
|
+
const memoryArchiveDir = join(archiveDir, dateStr, "memory");
|
|
50
|
+
mkdirSync(memoryArchiveDir, { recursive: true });
|
|
51
|
+
for (const memoryFile of domain.claudeConfig.memory) {
|
|
52
|
+
const expandedPath = paths.expandPath(memoryFile);
|
|
53
|
+
if (existsSync(expandedPath)) {
|
|
54
|
+
const fileName = basename(expandedPath);
|
|
55
|
+
const destPath = join(memoryArchiveDir, fileName);
|
|
56
|
+
const content = readFileSync(expandedPath, "utf-8");
|
|
57
|
+
writeFileSync(destPath, content, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const stats = statSync(archivePath);
|
|
62
|
+
const sizeKB = Math.round(stats.size / 1024);
|
|
63
|
+
console.log(pc.green(`\u2705 Session archived successfully`));
|
|
64
|
+
console.log(pc.gray(`Archive: ${archivePath}`));
|
|
65
|
+
console.log(pc.gray(`Size: ${sizeKB} KB`));
|
|
66
|
+
console.log(pc.gray(`Session age: ${(init_session(), __toCommonJS(session_exports)).SessionManager.getSessionAge(session.started)}`));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(pc.red(`\u274C Failed to archive session: ${error}`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function listArchives(domainName) {
|
|
73
|
+
const archiveDir = join(paths.archivesDir, domainName);
|
|
74
|
+
if (!existsSync(archiveDir)) {
|
|
75
|
+
console.log(pc.yellow(`No archives found for domain '${domainName}'`));
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const files = readdirSync(archiveDir).filter((file) => file.endsWith(".json")).sort().reverse();
|
|
79
|
+
if (files.length === 0) {
|
|
80
|
+
console.log(pc.yellow(`No archives found for domain '${domainName}'`));
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
console.log(pc.cyan(`\u{1F4E6} Archives for domain '${domainName}':
|
|
84
|
+
`));
|
|
85
|
+
const archives = [];
|
|
86
|
+
for (const file of files.slice(0, 10)) {
|
|
87
|
+
const archivePath = join(archiveDir, file);
|
|
88
|
+
try {
|
|
89
|
+
const content = readFileSync(archivePath, "utf-8");
|
|
90
|
+
const metadata = JSON.parse(content);
|
|
91
|
+
const date = new Date(metadata.timestamp);
|
|
92
|
+
const dateStr = date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
|
93
|
+
console.log(` ${dateStr}`);
|
|
94
|
+
console.log(pc.gray(` Session: ${metadata.session.sessionId.substring(0, 8)}...`));
|
|
95
|
+
console.log(pc.gray(` File: ${file}`));
|
|
96
|
+
archives.push(metadata);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (files.length > 10) {
|
|
101
|
+
console.log(pc.gray(`
|
|
102
|
+
... and ${files.length - 10} more`));
|
|
103
|
+
}
|
|
104
|
+
return archives;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
archiveCommand,
|
|
109
|
+
listArchives
|
|
110
|
+
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
paths
|
|
3
|
+
} from "./chunk-A7YXSI66.js";
|
|
4
|
+
|
|
5
|
+
// src/core/process.ts
|
|
6
|
+
import { execSync, spawn } from "child_process";
|
|
7
|
+
import { platform } from "os";
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var ProcessManager = class {
|
|
11
|
+
platform = platform();
|
|
12
|
+
/**
|
|
13
|
+
* Find all Claude Code processes
|
|
14
|
+
*/
|
|
15
|
+
findClaudeProcesses() {
|
|
16
|
+
try {
|
|
17
|
+
switch (this.platform) {
|
|
18
|
+
case "darwin":
|
|
19
|
+
case "linux":
|
|
20
|
+
return this.findUnixProcesses();
|
|
21
|
+
case "win32":
|
|
22
|
+
return this.findWindowsProcesses();
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`Unsupported platform: ${this.platform}`);
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`Failed to find processes: ${error}`);
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find processes on Unix-like systems
|
|
33
|
+
*/
|
|
34
|
+
findUnixProcesses() {
|
|
35
|
+
try {
|
|
36
|
+
const output = execSync("ps aux", { encoding: "utf-8" });
|
|
37
|
+
const lines = output.split("\n");
|
|
38
|
+
const processes = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
if (line.includes("claude") && !line.includes("cs switch")) {
|
|
41
|
+
const parts = line.split(/\s+/);
|
|
42
|
+
if (parts.length >= 11 && parts[1]) {
|
|
43
|
+
const pid = parseInt(parts[1], 10);
|
|
44
|
+
const command = parts[10] || "";
|
|
45
|
+
const args = parts.slice(11);
|
|
46
|
+
if (command && (command.includes("claude") || args.some((arg) => arg && arg.includes("claude")))) {
|
|
47
|
+
processes.push({ pid, command, args });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return processes;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Unix process detection failed: ${error}`);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find processes on Windows using PowerShell (wmic is deprecated)
|
|
60
|
+
*/
|
|
61
|
+
findWindowsProcesses() {
|
|
62
|
+
try {
|
|
63
|
+
const output = execSync(
|
|
64
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*claude*' } | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"`,
|
|
65
|
+
{ encoding: "utf-8" }
|
|
66
|
+
);
|
|
67
|
+
const lines = output.split("\r\n").filter((line) => line.trim());
|
|
68
|
+
const processes = [];
|
|
69
|
+
for (let i = 1; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
if (!line) continue;
|
|
72
|
+
const match = line.match(/^"?(\d+)"?,"?(.*?)"?$/);
|
|
73
|
+
if (match) {
|
|
74
|
+
const pid = parseInt(match[1], 10);
|
|
75
|
+
const commandLine = match[2] || "";
|
|
76
|
+
if (commandLine.includes("claude") && !commandLine.includes("cs switch")) {
|
|
77
|
+
processes.push({
|
|
78
|
+
pid,
|
|
79
|
+
command: "claude",
|
|
80
|
+
args: commandLine.split(" ").slice(1)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return processes;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Windows process detection failed: ${error}`);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Kill a process by PID
|
|
93
|
+
*/
|
|
94
|
+
killProcess(pid, force = false) {
|
|
95
|
+
try {
|
|
96
|
+
if (this.platform === "win32") {
|
|
97
|
+
const flag = force ? "/F" : "";
|
|
98
|
+
execSync(`taskkill /PID ${pid} ${flag}`, { stdio: "ignore" });
|
|
99
|
+
} else {
|
|
100
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
101
|
+
process.kill(pid, signal);
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Kill all Claude processes (with timeout for graceful shutdown)
|
|
110
|
+
*/
|
|
111
|
+
async killAllClaudeProcesses(timeout = 5e3) {
|
|
112
|
+
const processes = this.findClaudeProcesses();
|
|
113
|
+
if (processes.length === 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`Found ${processes.length} Claude process(es) to terminate`);
|
|
117
|
+
for (const proc of processes) {
|
|
118
|
+
this.killProcess(proc.pid, false);
|
|
119
|
+
}
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 1e3)));
|
|
121
|
+
const remaining = this.findClaudeProcesses();
|
|
122
|
+
for (const proc of remaining) {
|
|
123
|
+
console.log(`Force killing process ${proc.pid}`);
|
|
124
|
+
this.killProcess(proc.pid, true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Spawn Claude with specific arguments
|
|
129
|
+
*/
|
|
130
|
+
spawnClaude(args, options) {
|
|
131
|
+
const claudeCmd = this.findClaudeExecutable();
|
|
132
|
+
if (!claudeCmd) {
|
|
133
|
+
throw new Error("Claude CLI not found. Please ensure it is installed and in your PATH.");
|
|
134
|
+
}
|
|
135
|
+
if (this.platform === "darwin") {
|
|
136
|
+
const cwd = options?.cwd || process.cwd();
|
|
137
|
+
const claudeCommand = `cd "${cwd}" && ${claudeCmd} ${args.join(" ")}`;
|
|
138
|
+
const script = `tell app "Terminal" to do script "${claudeCommand.replace(/"/g, '\\"')}"`;
|
|
139
|
+
const terminalProcess = spawn("osascript", ["-e", script], {
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: "ignore"
|
|
142
|
+
});
|
|
143
|
+
terminalProcess.unref();
|
|
144
|
+
return terminalProcess.pid || 0;
|
|
145
|
+
}
|
|
146
|
+
if (this.platform === "linux") {
|
|
147
|
+
const cwd = options?.cwd || process.cwd();
|
|
148
|
+
const claudeCommand = `${claudeCmd} ${args.join(" ")}`;
|
|
149
|
+
const terminals = ["gnome-terminal", "xterm", "konsole", "xfce4-terminal"];
|
|
150
|
+
for (const term of terminals) {
|
|
151
|
+
try {
|
|
152
|
+
const termProcess = spawn(term, ["--", "bash", "-c", `cd "${cwd}" && ${claudeCommand}`], {
|
|
153
|
+
detached: true,
|
|
154
|
+
stdio: "ignore"
|
|
155
|
+
});
|
|
156
|
+
termProcess.unref();
|
|
157
|
+
return termProcess.pid || 0;
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (this.platform === "win32") {
|
|
163
|
+
const cwd = options?.cwd || process.cwd();
|
|
164
|
+
const gitBash = this.findGitBash();
|
|
165
|
+
if (gitBash) {
|
|
166
|
+
const claudeCommand = `cd "${cwd.replace(/\\/g, "/")}" && ${claudeCmd} ${args.join(" ")}`;
|
|
167
|
+
const cmdProcess = spawn("cmd.exe", ["/c", "start", "", gitBash, "-c", claudeCommand], {
|
|
168
|
+
detached: true,
|
|
169
|
+
stdio: "ignore",
|
|
170
|
+
windowsHide: false
|
|
171
|
+
});
|
|
172
|
+
cmdProcess.unref();
|
|
173
|
+
return cmdProcess.pid || 0;
|
|
174
|
+
} else {
|
|
175
|
+
const claudeCommand = `cd /d "${cwd}" && ${claudeCmd} ${args.join(" ")}`;
|
|
176
|
+
const cmdProcess = spawn("cmd.exe", ["/c", "start", "cmd.exe", "/k", claudeCommand], {
|
|
177
|
+
detached: true,
|
|
178
|
+
stdio: "ignore",
|
|
179
|
+
windowsHide: false
|
|
180
|
+
});
|
|
181
|
+
cmdProcess.unref();
|
|
182
|
+
return cmdProcess.pid || 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const claudeProcess = spawn(claudeCmd, args, {
|
|
186
|
+
detached: true,
|
|
187
|
+
stdio: "ignore",
|
|
188
|
+
...options
|
|
189
|
+
});
|
|
190
|
+
claudeProcess.unref();
|
|
191
|
+
return claudeProcess.pid || 0;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Spawn a shell script in a new terminal window
|
|
195
|
+
*/
|
|
196
|
+
spawnTerminalScript(script, cwd) {
|
|
197
|
+
const scriptFile = join(paths.baseDir, "launch.sh");
|
|
198
|
+
writeFileSync(scriptFile, script, { mode: 493 });
|
|
199
|
+
if (this.platform === "darwin") {
|
|
200
|
+
const escapedPath = scriptFile.replace(/"/g, '\\"');
|
|
201
|
+
const appleScript = `tell app "Terminal" to do script "bash \\"${escapedPath}\\""`;
|
|
202
|
+
const proc2 = spawn("osascript", ["-e", appleScript], {
|
|
203
|
+
detached: true,
|
|
204
|
+
stdio: "ignore"
|
|
205
|
+
});
|
|
206
|
+
proc2.unref();
|
|
207
|
+
return proc2.pid || 0;
|
|
208
|
+
}
|
|
209
|
+
if (this.platform === "linux") {
|
|
210
|
+
const terminals = ["gnome-terminal", "xterm", "konsole", "xfce4-terminal"];
|
|
211
|
+
for (const term of terminals) {
|
|
212
|
+
try {
|
|
213
|
+
const proc2 = spawn(term, ["--", "bash", scriptFile], {
|
|
214
|
+
detached: true,
|
|
215
|
+
stdio: "ignore"
|
|
216
|
+
});
|
|
217
|
+
proc2.unref();
|
|
218
|
+
return proc2.pid || 0;
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (this.platform === "win32") {
|
|
224
|
+
const gitBash = this.findGitBash();
|
|
225
|
+
const bashCmd = gitBash || "bash";
|
|
226
|
+
const scriptPath = scriptFile.replace(/\\/g, "/");
|
|
227
|
+
const proc2 = spawn("cmd.exe", ["/c", "start", "", bashCmd, scriptPath], {
|
|
228
|
+
detached: true,
|
|
229
|
+
stdio: "ignore",
|
|
230
|
+
windowsHide: false
|
|
231
|
+
});
|
|
232
|
+
proc2.unref();
|
|
233
|
+
return proc2.pid || 0;
|
|
234
|
+
}
|
|
235
|
+
const proc = spawn("bash", [scriptFile], {
|
|
236
|
+
detached: true,
|
|
237
|
+
stdio: "ignore"
|
|
238
|
+
});
|
|
239
|
+
proc.unref();
|
|
240
|
+
return proc.pid || 0;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get the Claude executable path (public accessor)
|
|
244
|
+
*/
|
|
245
|
+
getClaudeExecutable() {
|
|
246
|
+
return this.findClaudeExecutable();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Find Git Bash executable on Windows
|
|
250
|
+
*/
|
|
251
|
+
findGitBash() {
|
|
252
|
+
if (this.platform !== "win32") return null;
|
|
253
|
+
const candidates = [
|
|
254
|
+
join(process.env.PROGRAMFILES || "C:\\Program Files", "Git", "bin", "bash.exe"),
|
|
255
|
+
join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Git", "bin", "bash.exe"),
|
|
256
|
+
join(process.env.LOCALAPPDATA || "", "Programs", "Git", "bin", "bash.exe"),
|
|
257
|
+
"C:\\Program Files\\Git\\bin\\bash.exe"
|
|
258
|
+
];
|
|
259
|
+
for (const candidate of candidates) {
|
|
260
|
+
if (candidate && existsSync(candidate)) {
|
|
261
|
+
return candidate;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const output = execSync("where bash", { encoding: "utf-8" }).trim();
|
|
266
|
+
const firstLine = output.split("\n")[0]?.trim();
|
|
267
|
+
if (firstLine && firstLine.toLowerCase().includes("git")) {
|
|
268
|
+
return firstLine;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Find Claude executable path
|
|
276
|
+
*/
|
|
277
|
+
findClaudeExecutable() {
|
|
278
|
+
try {
|
|
279
|
+
if (this.platform === "win32") {
|
|
280
|
+
const output = execSync("where claude", { encoding: "utf-8" }).trim();
|
|
281
|
+
const firstLine = output.split("\n")[0];
|
|
282
|
+
return firstLine || null;
|
|
283
|
+
} else {
|
|
284
|
+
const output = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
285
|
+
return output || null;
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
const commonPaths = this.platform === "win32" ? [
|
|
289
|
+
join(process.env.LOCALAPPDATA || "", "Programs", "Claude Code", "claude.exe"),
|
|
290
|
+
join(process.env.PROGRAMFILES || "C:\\Program Files", "Claude Code", "claude.exe"),
|
|
291
|
+
join(process.env.PROGRAMFILES || "C:\\Program Files", "Claude", "claude.exe"),
|
|
292
|
+
"C:\\Program Files\\Claude Code\\claude.exe",
|
|
293
|
+
"C:\\Program Files\\Claude\\claude.exe"
|
|
294
|
+
] : [
|
|
295
|
+
"/usr/local/bin/claude",
|
|
296
|
+
"/usr/bin/claude",
|
|
297
|
+
"/opt/homebrew/bin/claude"
|
|
298
|
+
];
|
|
299
|
+
for (const candidatePath of commonPaths) {
|
|
300
|
+
if (candidatePath && existsSync(candidatePath)) {
|
|
301
|
+
return candidatePath;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Write PID file for tracking
|
|
309
|
+
*/
|
|
310
|
+
writePidFile(domainName, pid) {
|
|
311
|
+
const pidFile = join(paths.baseDir, `${domainName}.pid`);
|
|
312
|
+
writeFileSync(pidFile, pid.toString(), "utf-8");
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Read PID file
|
|
316
|
+
*/
|
|
317
|
+
readPidFile(domainName) {
|
|
318
|
+
const pidFile = join(paths.baseDir, `${domainName}.pid`);
|
|
319
|
+
if (!existsSync(pidFile)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const content = readFileSync(pidFile, "utf-8");
|
|
324
|
+
return parseInt(content, 10);
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Delete PID file
|
|
331
|
+
*/
|
|
332
|
+
deletePidFile(domainName) {
|
|
333
|
+
const pidFile = join(paths.baseDir, `${domainName}.pid`);
|
|
334
|
+
if (existsSync(pidFile)) {
|
|
335
|
+
unlinkSync(pidFile);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check if a process is still running
|
|
340
|
+
*/
|
|
341
|
+
isProcessRunning(pid) {
|
|
342
|
+
try {
|
|
343
|
+
if (this.platform === "win32") {
|
|
344
|
+
const output = execSync(`tasklist /FI "PID eq ${pid}"`, { encoding: "utf-8" });
|
|
345
|
+
return output.includes(pid.toString());
|
|
346
|
+
} else {
|
|
347
|
+
process.kill(pid, 0);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Verify Claude is installed
|
|
356
|
+
*/
|
|
357
|
+
verifyClaudeInstalled() {
|
|
358
|
+
return this.findClaudeExecutable() !== null;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
var processManager = new ProcessManager();
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
ProcessManager,
|
|
365
|
+
processManager
|
|
366
|
+
};
|