contextswitch 0.1.5 → 0.1.6
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/dist/archive-MN425GUR.js +10 -0
- package/dist/{chunk-GHF4FLJV.js → chunk-BGARUR7R.js} +6 -7
- package/dist/{chunk-YMFZWGZO.js → chunk-D3DHIVER.js} +67 -32
- package/dist/{chunk-A7YXSI66.js → chunk-F36TGFK2.js} +21 -2
- package/dist/{chunk-756VUR5T.js → chunk-K7ISIY3Q.js} +31 -70
- package/dist/{chunk-KBKALWDX.js → chunk-MGIXKKM6.js} +3 -0
- package/dist/cli.js +89 -27
- package/dist/{process-E35QFSO6.js → process-X2SYCF5V.js} +2 -2
- package/dist/{reset-GZKUYPZR.js → reset-TSWWNV2Y.js} +8 -4
- package/dist/{session-IWXAKW6Z.js → session-H5HPE5OT.js} +1 -1
- package/dist/{switch-RZCXBTPC.js → switch-CM6GRYEA.js} +98 -79
- package/package.json +1 -1
- package/dist/archive-64CFJ3P5.js +0 -10
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configManager
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-D3DHIVER.js";
|
|
4
4
|
import {
|
|
5
5
|
paths
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-F36TGFK2.js";
|
|
7
7
|
|
|
8
8
|
// src/commands/archive.ts
|
|
9
9
|
import picocolors from "picocolors";
|
|
@@ -26,18 +26,18 @@ async function archiveCommand(domainName) {
|
|
|
26
26
|
if (!existsSync(archiveDir)) {
|
|
27
27
|
mkdirSync(archiveDir, { recursive: true });
|
|
28
28
|
}
|
|
29
|
+
const domain = configManager.loadDomain(domainName);
|
|
29
30
|
const timestamp = /* @__PURE__ */ new Date();
|
|
30
31
|
const archiveMetadata = {
|
|
31
32
|
domain: domainName,
|
|
32
33
|
session,
|
|
33
34
|
timestamp: timestamp.toISOString(),
|
|
34
|
-
config:
|
|
35
|
+
config: domain
|
|
35
36
|
};
|
|
36
37
|
const dateStr = timestamp.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, -5);
|
|
37
38
|
const archiveName = `${dateStr}.json`;
|
|
38
39
|
const archivePath = join(archiveDir, archiveName);
|
|
39
40
|
writeFileSync(archivePath, JSON.stringify(archiveMetadata, null, 2), "utf-8");
|
|
40
|
-
const domain = configManager.loadDomain(domainName);
|
|
41
41
|
if (domain.claudeConfig?.memory) {
|
|
42
42
|
const memoryArchiveDir = join(archiveDir, dateStr, "memory");
|
|
43
43
|
mkdirSync(memoryArchiveDir, { recursive: true });
|
|
@@ -56,11 +56,10 @@ async function archiveCommand(domainName) {
|
|
|
56
56
|
console.log(pc.green(`\u2705 Session archived successfully`));
|
|
57
57
|
console.log(pc.gray(`Archive: ${archivePath}`));
|
|
58
58
|
console.log(pc.gray(`Size: ${sizeKB} KB`));
|
|
59
|
-
const { SessionManager } = await import("./session-
|
|
59
|
+
const { SessionManager } = await import("./session-H5HPE5OT.js");
|
|
60
60
|
console.log(pc.gray(`Session age: ${SessionManager.getSessionAge(session.started)}`));
|
|
61
61
|
} catch (error) {
|
|
62
|
-
|
|
63
|
-
process.exit(1);
|
|
62
|
+
throw new Error(`Failed to archive session: ${error}`);
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
function listArchives(domainName) {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
debug,
|
|
2
3
|
paths
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
|
|
5
|
-
// src/core/config.ts
|
|
6
|
-
import { readFileSync, existsSync, writeFileSync, readdirSync, renameSync, unlinkSync } from "fs";
|
|
7
|
-
import { parse, stringify } from "yaml";
|
|
4
|
+
} from "./chunk-F36TGFK2.js";
|
|
8
5
|
|
|
9
6
|
// src/core/domain.ts
|
|
10
7
|
import { z } from "zod";
|
|
@@ -13,15 +10,35 @@ var MCPServerSchema = z.object({
|
|
|
13
10
|
args: z.array(z.string()).optional().describe("Arguments for the command"),
|
|
14
11
|
env: z.record(z.string()).optional().describe("Environment variables")
|
|
15
12
|
});
|
|
13
|
+
var DOMAIN_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
14
|
+
var ENV_VAR_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
15
|
+
function validateDomainName(name) {
|
|
16
|
+
if (!name || name.length === 0) {
|
|
17
|
+
return "Domain name cannot be empty";
|
|
18
|
+
}
|
|
19
|
+
if (name.length > 64) {
|
|
20
|
+
return "Domain name must be 64 characters or fewer";
|
|
21
|
+
}
|
|
22
|
+
if (!DOMAIN_NAME_REGEX.test(name)) {
|
|
23
|
+
return "Domain name must start with a letter or number and contain only letters, numbers, hyphens, underscores, and dots";
|
|
24
|
+
}
|
|
25
|
+
if (name === "." || name === "..") {
|
|
26
|
+
return 'Domain name cannot be "." or ".."';
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
16
30
|
var DomainConfigSchema = z.object({
|
|
17
|
-
name: z.string().min(1).describe("Domain identifier"),
|
|
31
|
+
name: z.string().min(1).regex(DOMAIN_NAME_REGEX, "Domain name must start with a letter or number and contain only letters, numbers, hyphens, underscores, and dots").describe("Domain identifier"),
|
|
18
32
|
workingDirectory: z.string().describe("Working directory for this domain"),
|
|
19
33
|
claudeConfig: z.object({
|
|
20
34
|
instructions: z.string().optional().describe("Path to CLAUDE.md file"),
|
|
21
35
|
memory: z.array(z.string()).optional().describe("Memory file paths")
|
|
22
36
|
}).optional(),
|
|
23
37
|
mcpServers: z.record(MCPServerSchema).optional().describe("MCP server configurations"),
|
|
24
|
-
env: z.record(
|
|
38
|
+
env: z.record(
|
|
39
|
+
z.string().regex(ENV_VAR_NAME_REGEX, "Environment variable names must start with a letter or underscore and contain only letters, digits, and underscores"),
|
|
40
|
+
z.string()
|
|
41
|
+
).optional().describe("Environment variables for this domain"),
|
|
25
42
|
extends: z.string().optional().describe("Parent domain to inherit from"),
|
|
26
43
|
metadata: z.object({
|
|
27
44
|
description: z.string().optional(),
|
|
@@ -56,6 +73,10 @@ var GlobalConfigSchema = z.object({
|
|
|
56
73
|
killTimeout: z.number().default(5e3).describe("Milliseconds to wait before force kill")
|
|
57
74
|
}).optional()
|
|
58
75
|
});
|
|
76
|
+
var SessionFallbackMarkerSchema = z.object({
|
|
77
|
+
domain: z.string().min(1).regex(DOMAIN_NAME_REGEX),
|
|
78
|
+
sessionId: z.string().min(1)
|
|
79
|
+
});
|
|
59
80
|
var DEFAULT_DOMAIN_TEMPLATE = {
|
|
60
81
|
name: "default",
|
|
61
82
|
workingDirectory: process.cwd(),
|
|
@@ -70,6 +91,15 @@ var DEFAULT_DOMAIN_TEMPLATE = {
|
|
|
70
91
|
tags: []
|
|
71
92
|
}
|
|
72
93
|
};
|
|
94
|
+
function validateEnvVarName(name) {
|
|
95
|
+
if (!name || name.length === 0) {
|
|
96
|
+
return "Environment variable name cannot be empty";
|
|
97
|
+
}
|
|
98
|
+
if (!ENV_VAR_NAME_REGEX.test(name)) {
|
|
99
|
+
return `Invalid environment variable name '${name}': must start with a letter or underscore and contain only letters, digits, and underscores`;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
73
103
|
function validateDomain(data) {
|
|
74
104
|
return DomainConfigSchema.parse(data);
|
|
75
105
|
}
|
|
@@ -86,30 +116,28 @@ function createInitialState() {
|
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
// src/core/config.ts
|
|
119
|
+
import { readFileSync, existsSync, writeFileSync, readdirSync, renameSync, unlinkSync } from "fs";
|
|
120
|
+
import { parse, stringify } from "yaml";
|
|
89
121
|
var ConfigManager = class {
|
|
90
|
-
globalConfig = null;
|
|
91
|
-
state = null;
|
|
92
|
-
domainCache = /* @__PURE__ */ new Map();
|
|
93
122
|
/**
|
|
94
123
|
* Load global configuration
|
|
95
124
|
*/
|
|
96
125
|
loadGlobalConfig() {
|
|
97
126
|
const configPath = paths.globalConfigFile;
|
|
98
127
|
if (!existsSync(configPath)) {
|
|
99
|
-
|
|
128
|
+
const defaultConfig = {
|
|
100
129
|
version: "1.0.0",
|
|
101
130
|
defaultDomain: "default",
|
|
102
131
|
autoArchive: true,
|
|
103
132
|
logLevel: "info"
|
|
104
133
|
};
|
|
105
|
-
this.saveGlobalConfig(
|
|
106
|
-
return
|
|
134
|
+
this.saveGlobalConfig(defaultConfig);
|
|
135
|
+
return defaultConfig;
|
|
107
136
|
}
|
|
108
137
|
try {
|
|
109
138
|
const content = readFileSync(configPath, "utf-8");
|
|
110
139
|
const data = parse(content);
|
|
111
|
-
|
|
112
|
-
return this.globalConfig;
|
|
140
|
+
return GlobalConfigSchema.parse(data);
|
|
113
141
|
} catch (error) {
|
|
114
142
|
throw new Error(`Failed to load global config: ${error}`);
|
|
115
143
|
}
|
|
@@ -123,23 +151,25 @@ var ConfigManager = class {
|
|
|
123
151
|
const content = stringify(config, { indent: 2 });
|
|
124
152
|
writeFileSync(tempPath, content, "utf-8");
|
|
125
153
|
renameSync(tempPath, configPath);
|
|
126
|
-
this.globalConfig = config;
|
|
127
154
|
}
|
|
128
155
|
/**
|
|
129
156
|
* Load state file
|
|
130
157
|
*/
|
|
131
158
|
loadState() {
|
|
132
159
|
const statePath = paths.stateFile;
|
|
160
|
+
debug("config", `Loading state from ${statePath}`);
|
|
133
161
|
if (!existsSync(statePath)) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
debug("config", "No state file found, creating initial state");
|
|
163
|
+
const initial = createInitialState();
|
|
164
|
+
this.saveState(initial);
|
|
165
|
+
return initial;
|
|
137
166
|
}
|
|
138
167
|
try {
|
|
139
168
|
const content = readFileSync(statePath, "utf-8");
|
|
140
169
|
const data = JSON.parse(content);
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
const state = validateState(data);
|
|
171
|
+
debug("config", `State loaded: activeDomain=${state.activeDomain}, sessions=${Object.keys(state.sessions || {}).join(",") || "none"}`);
|
|
172
|
+
return state;
|
|
143
173
|
} catch (error) {
|
|
144
174
|
throw new Error(`Failed to load state: ${error}`);
|
|
145
175
|
}
|
|
@@ -150,19 +180,23 @@ var ConfigManager = class {
|
|
|
150
180
|
saveState(state) {
|
|
151
181
|
const statePath = paths.stateFile;
|
|
152
182
|
const tempPath = `${statePath}.tmp`;
|
|
183
|
+
debug("config", `Saving state to ${statePath} (activeDomain=${state.activeDomain})`);
|
|
153
184
|
const content = JSON.stringify(state, null, 2);
|
|
154
185
|
writeFileSync(tempPath, content, "utf-8");
|
|
155
186
|
renameSync(tempPath, statePath);
|
|
156
|
-
this.state = state;
|
|
157
187
|
}
|
|
158
188
|
/**
|
|
159
189
|
* Load a domain configuration
|
|
160
190
|
*/
|
|
161
191
|
loadDomain(domainName) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
192
|
+
return this.loadDomainInternal(domainName, []);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Internal domain loader with cycle detection for inheritance chains.
|
|
196
|
+
*/
|
|
197
|
+
loadDomainInternal(domainName, ancestors) {
|
|
165
198
|
const domainPath = paths.domainConfigFile(domainName);
|
|
199
|
+
debug("config", `Loading domain '${domainName}' from ${domainPath}`);
|
|
166
200
|
if (!existsSync(domainPath)) {
|
|
167
201
|
throw new Error(`Domain '${domainName}' not found`);
|
|
168
202
|
}
|
|
@@ -171,7 +205,11 @@ var ConfigManager = class {
|
|
|
171
205
|
const data = parse(content);
|
|
172
206
|
let domain = validateDomain(data);
|
|
173
207
|
if (domain.extends) {
|
|
174
|
-
|
|
208
|
+
if (ancestors.includes(domain.extends)) {
|
|
209
|
+
throw new Error(`Circular inheritance detected: ${[...ancestors, domainName, domain.extends].join(" -> ")}`);
|
|
210
|
+
}
|
|
211
|
+
debug("config", `Domain '${domainName}' extends '${domain.extends}', merging`);
|
|
212
|
+
const parent = this.loadDomainInternal(domain.extends, [...ancestors, domainName]);
|
|
175
213
|
domain = this.mergeDomains(parent, domain);
|
|
176
214
|
}
|
|
177
215
|
domain.workingDirectory = paths.expandPath(domain.workingDirectory);
|
|
@@ -181,7 +219,6 @@ var ConfigManager = class {
|
|
|
181
219
|
if (domain.claudeConfig?.memory) {
|
|
182
220
|
domain.claudeConfig.memory = domain.claudeConfig.memory.map((p) => paths.expandPath(p));
|
|
183
221
|
}
|
|
184
|
-
this.domainCache.set(domainName, domain);
|
|
185
222
|
return domain;
|
|
186
223
|
} catch (error) {
|
|
187
224
|
throw new Error(`Failed to load domain '${domainName}': ${error}`);
|
|
@@ -196,7 +233,6 @@ var ConfigManager = class {
|
|
|
196
233
|
const content = stringify(domain, { indent: 2 });
|
|
197
234
|
writeFileSync(tempPath, content, "utf-8");
|
|
198
235
|
renameSync(tempPath, domainPath);
|
|
199
|
-
this.domainCache.set(domain.name, domain);
|
|
200
236
|
}
|
|
201
237
|
/**
|
|
202
238
|
* Create a new domain from template
|
|
@@ -241,7 +277,6 @@ var ConfigManager = class {
|
|
|
241
277
|
throw new Error(`Domain '${domainName}' not found`);
|
|
242
278
|
}
|
|
243
279
|
unlinkSync(domainPath);
|
|
244
|
-
this.domainCache.delete(domainName);
|
|
245
280
|
}
|
|
246
281
|
/**
|
|
247
282
|
* Merge two domain configurations (child overrides parent)
|
|
@@ -289,13 +324,13 @@ var ConfigManager = class {
|
|
|
289
324
|
* Clear cache
|
|
290
325
|
*/
|
|
291
326
|
clearCache() {
|
|
292
|
-
this.globalConfig = null;
|
|
293
|
-
this.state = null;
|
|
294
|
-
this.domainCache.clear();
|
|
295
327
|
}
|
|
296
328
|
};
|
|
297
329
|
var configManager = new ConfigManager();
|
|
298
330
|
|
|
299
331
|
export {
|
|
332
|
+
validateDomainName,
|
|
333
|
+
SessionFallbackMarkerSchema,
|
|
334
|
+
validateEnvVarName,
|
|
300
335
|
configManager
|
|
301
336
|
};
|
|
@@ -72,6 +72,9 @@ var Paths = class _Paths {
|
|
|
72
72
|
* Get domain config file path
|
|
73
73
|
*/
|
|
74
74
|
domainConfigFile(domainName) {
|
|
75
|
+
if (domainName.includes("/") || domainName.includes("\\") || domainName === ".." || domainName === ".") {
|
|
76
|
+
throw new Error(`Unsafe domain name: ${domainName}`);
|
|
77
|
+
}
|
|
75
78
|
return join(this.domainsDir, `${domainName}.yml`);
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
@@ -108,7 +111,9 @@ var Paths = class _Paths {
|
|
|
108
111
|
* Expand environment variables and ~ in paths
|
|
109
112
|
*/
|
|
110
113
|
expandPath(inputPath) {
|
|
111
|
-
if (inputPath
|
|
114
|
+
if (inputPath === "~") {
|
|
115
|
+
inputPath = homedir();
|
|
116
|
+
} else if (inputPath.startsWith("~/")) {
|
|
112
117
|
inputPath = join(homedir(), inputPath.slice(2));
|
|
113
118
|
}
|
|
114
119
|
inputPath = inputPath.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (match, varName) => {
|
|
@@ -158,6 +163,20 @@ var Paths = class _Paths {
|
|
|
158
163
|
};
|
|
159
164
|
var paths = Paths.instance;
|
|
160
165
|
|
|
166
|
+
// src/core/debug.ts
|
|
167
|
+
import picocolors from "picocolors";
|
|
168
|
+
var pc = picocolors;
|
|
169
|
+
var enabled = false;
|
|
170
|
+
function enableDebug() {
|
|
171
|
+
enabled = true;
|
|
172
|
+
}
|
|
173
|
+
function debug(context, message) {
|
|
174
|
+
if (!enabled) return;
|
|
175
|
+
console.log(pc.dim(`[debug:${context}] ${message}`));
|
|
176
|
+
}
|
|
177
|
+
|
|
161
178
|
export {
|
|
162
|
-
paths
|
|
179
|
+
paths,
|
|
180
|
+
enableDebug,
|
|
181
|
+
debug
|
|
163
182
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
debug,
|
|
2
3
|
paths
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-F36TGFK2.js";
|
|
4
5
|
|
|
5
6
|
// src/core/process.ts
|
|
6
7
|
import { execSync, spawn } from "child_process";
|
|
@@ -47,9 +48,11 @@ var ProcessManager = class {
|
|
|
47
48
|
const basename = command.split("/").pop() || "";
|
|
48
49
|
const isClaudeBinary = basename === "claude" || basename === "claude.exe";
|
|
49
50
|
if (isClaudeBinary) {
|
|
51
|
+
debug("process", `Found Claude process: PID=${pid} cmd=${command} args=${args.join(" ")}`);
|
|
50
52
|
processes.push({ pid, command, args });
|
|
51
53
|
}
|
|
52
54
|
}
|
|
55
|
+
debug("process", `Unix process scan found ${processes.length} Claude process(es)`);
|
|
53
56
|
return processes;
|
|
54
57
|
} catch (error) {
|
|
55
58
|
console.error(`Unix process detection failed: ${error}`);
|
|
@@ -73,11 +76,17 @@ var ProcessManager = class {
|
|
|
73
76
|
const match = line.match(/^"?(\d+)"?,"?(.*?)"?$/);
|
|
74
77
|
if (match) {
|
|
75
78
|
const pid = parseInt(match[1], 10);
|
|
79
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
76
80
|
const commandLine = match[2] || "";
|
|
77
|
-
if (commandLine.includes("
|
|
81
|
+
if (commandLine.includes("cs switch")) continue;
|
|
82
|
+
const exe = commandLine.split(/[\s"]+/)[0] || "";
|
|
83
|
+
const bin = exe.split(/[/\\]/).pop() || "";
|
|
84
|
+
const isClaudeBinary = bin === "claude" || bin === "claude.exe";
|
|
85
|
+
if (isClaudeBinary) {
|
|
86
|
+
debug("process", `Found Claude process: PID=${pid} cmd=${commandLine}`);
|
|
78
87
|
processes.push({
|
|
79
88
|
pid,
|
|
80
|
-
command:
|
|
89
|
+
command: exe,
|
|
81
90
|
args: commandLine.split(" ").slice(1)
|
|
82
91
|
});
|
|
83
92
|
}
|
|
@@ -93,6 +102,10 @@ var ProcessManager = class {
|
|
|
93
102
|
* Kill a process by PID
|
|
94
103
|
*/
|
|
95
104
|
killProcess(pid, force = false) {
|
|
105
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
106
|
+
debug("process", `Invalid PID: ${pid}`);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
96
109
|
try {
|
|
97
110
|
if (this.platform === "win32") {
|
|
98
111
|
const flag = force ? "/F" : "";
|
|
@@ -114,88 +127,28 @@ var ProcessManager = class {
|
|
|
114
127
|
if (processes.length === 0) {
|
|
115
128
|
return;
|
|
116
129
|
}
|
|
130
|
+
debug("process", `Found ${processes.length} Claude process(es) to terminate`);
|
|
117
131
|
console.log(`Found ${processes.length} Claude process(es) to terminate`);
|
|
118
132
|
for (const proc of processes) {
|
|
133
|
+
debug("process", `Sending SIGTERM to PID ${proc.pid}`);
|
|
119
134
|
this.killProcess(proc.pid, false);
|
|
120
135
|
}
|
|
121
136
|
await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 1e3)));
|
|
122
137
|
const remaining = this.findClaudeProcesses();
|
|
123
138
|
for (const proc of remaining) {
|
|
139
|
+
debug("process", `Force killing PID ${proc.pid} (still alive after grace period)`);
|
|
124
140
|
console.log(`Force killing process ${proc.pid}`);
|
|
125
141
|
this.killProcess(proc.pid, true);
|
|
126
142
|
}
|
|
127
143
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Spawn Claude with specific arguments
|
|
130
|
-
*/
|
|
131
|
-
spawnClaude(args, options) {
|
|
132
|
-
const claudeCmd = this.findClaudeExecutable();
|
|
133
|
-
if (!claudeCmd) {
|
|
134
|
-
throw new Error("Claude CLI not found. Please ensure it is installed and in your PATH.");
|
|
135
|
-
}
|
|
136
|
-
if (this.platform === "darwin") {
|
|
137
|
-
const cwd = options?.cwd || process.cwd();
|
|
138
|
-
const claudeCommand = `cd "${cwd}" && ${claudeCmd} ${args.join(" ")}`;
|
|
139
|
-
const script = `tell app "Terminal" to do script "${claudeCommand.replace(/"/g, '\\"')}"`;
|
|
140
|
-
const terminalProcess = spawn("osascript", ["-e", script], {
|
|
141
|
-
detached: true,
|
|
142
|
-
stdio: "ignore"
|
|
143
|
-
});
|
|
144
|
-
terminalProcess.unref();
|
|
145
|
-
return terminalProcess.pid || 0;
|
|
146
|
-
}
|
|
147
|
-
if (this.platform === "linux") {
|
|
148
|
-
const cwd = options?.cwd || process.cwd();
|
|
149
|
-
const claudeCommand = `${claudeCmd} ${args.join(" ")}`;
|
|
150
|
-
const terminals = ["gnome-terminal", "xterm", "konsole", "xfce4-terminal"];
|
|
151
|
-
for (const term of terminals) {
|
|
152
|
-
try {
|
|
153
|
-
const termProcess = spawn(term, ["--", "bash", "-c", `cd "${cwd}" && ${claudeCommand}`], {
|
|
154
|
-
detached: true,
|
|
155
|
-
stdio: "ignore"
|
|
156
|
-
});
|
|
157
|
-
termProcess.unref();
|
|
158
|
-
return termProcess.pid || 0;
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (this.platform === "win32") {
|
|
164
|
-
const cwd = options?.cwd || process.cwd();
|
|
165
|
-
const gitBash = this.findGitBash();
|
|
166
|
-
if (gitBash) {
|
|
167
|
-
const claudeCommand = `cd "${String(cwd).replace(/\\/g, "/")}" && ${claudeCmd} ${args.join(" ")}`;
|
|
168
|
-
const cmdProcess = spawn("cmd.exe", ["/c", "start", "", gitBash, "-c", claudeCommand], {
|
|
169
|
-
detached: true,
|
|
170
|
-
stdio: "ignore",
|
|
171
|
-
windowsHide: false
|
|
172
|
-
});
|
|
173
|
-
cmdProcess.unref();
|
|
174
|
-
return cmdProcess.pid || 0;
|
|
175
|
-
} else {
|
|
176
|
-
const claudeCommand = `cd /d "${cwd}" && ${claudeCmd} ${args.join(" ")}`;
|
|
177
|
-
const cmdProcess = spawn("cmd.exe", ["/c", "start", "cmd.exe", "/k", claudeCommand], {
|
|
178
|
-
detached: true,
|
|
179
|
-
stdio: "ignore",
|
|
180
|
-
windowsHide: false
|
|
181
|
-
});
|
|
182
|
-
cmdProcess.unref();
|
|
183
|
-
return cmdProcess.pid || 0;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const claudeProcess = spawn(claudeCmd, args, {
|
|
187
|
-
detached: true,
|
|
188
|
-
stdio: "ignore",
|
|
189
|
-
...options
|
|
190
|
-
});
|
|
191
|
-
claudeProcess.unref();
|
|
192
|
-
return claudeProcess.pid || 0;
|
|
193
|
-
}
|
|
194
144
|
/**
|
|
195
145
|
* Spawn a shell script in a new terminal window
|
|
196
146
|
*/
|
|
197
147
|
spawnTerminalScript(script, _cwd) {
|
|
198
|
-
const scriptFile = join(paths.baseDir,
|
|
148
|
+
const scriptFile = join(paths.baseDir, `launch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.sh`);
|
|
149
|
+
debug("process", `Writing launch script to ${scriptFile}`);
|
|
150
|
+
debug("process", `Script contents:
|
|
151
|
+
${script}`);
|
|
199
152
|
writeFileSync(scriptFile, script, { mode: 493 });
|
|
200
153
|
if (this.platform === "darwin") {
|
|
201
154
|
const escapedPath = scriptFile.replace(/"/g, '\\"');
|
|
@@ -280,12 +233,15 @@ var ProcessManager = class {
|
|
|
280
233
|
if (this.platform === "win32") {
|
|
281
234
|
const output = execSync("where claude", { encoding: "utf-8" }).trim();
|
|
282
235
|
const firstLine = output.split("\n")[0];
|
|
236
|
+
debug("process", `Found Claude via 'where': ${firstLine}`);
|
|
283
237
|
return firstLine || null;
|
|
284
238
|
} else {
|
|
285
239
|
const output = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
240
|
+
debug("process", `Found Claude via 'which': ${output}`);
|
|
286
241
|
return output || null;
|
|
287
242
|
}
|
|
288
243
|
} catch {
|
|
244
|
+
debug("process", "Claude not found via PATH, checking common install locations");
|
|
289
245
|
const commonPaths = this.platform === "win32" ? [
|
|
290
246
|
join(process.env.LOCALAPPDATA || "", "Programs", "Claude Code", "claude.exe"),
|
|
291
247
|
join(process.env.PROGRAMFILES || "C:\\Program Files", "Claude Code", "claude.exe"),
|
|
@@ -299,9 +255,11 @@ var ProcessManager = class {
|
|
|
299
255
|
];
|
|
300
256
|
for (const candidatePath of commonPaths) {
|
|
301
257
|
if (candidatePath && existsSync(candidatePath)) {
|
|
258
|
+
debug("process", `Found Claude at fallback path: ${candidatePath}`);
|
|
302
259
|
return candidatePath;
|
|
303
260
|
}
|
|
304
261
|
}
|
|
262
|
+
debug("process", "Claude executable not found anywhere");
|
|
305
263
|
return null;
|
|
306
264
|
}
|
|
307
265
|
}
|
|
@@ -340,6 +298,9 @@ var ProcessManager = class {
|
|
|
340
298
|
* Check if a process is still running
|
|
341
299
|
*/
|
|
342
300
|
isProcessRunning(pid) {
|
|
301
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
343
304
|
try {
|
|
344
305
|
if (this.platform === "win32") {
|
|
345
306
|
const output = execSync(`tasklist /FI "PID eq ${pid}"`, { encoding: "utf-8" });
|
|
@@ -59,6 +59,9 @@ var SessionManager = class {
|
|
|
59
59
|
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
60
60
|
}
|
|
61
61
|
const minutes = Math.floor(diffMs / (1e3 * 60));
|
|
62
|
+
if (minutes === 0) {
|
|
63
|
+
return "just now";
|
|
64
|
+
}
|
|
62
65
|
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
63
66
|
}
|
|
64
67
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
SessionFallbackMarkerSchema,
|
|
4
|
+
configManager,
|
|
5
|
+
validateDomainName
|
|
6
|
+
} from "./chunk-D3DHIVER.js";
|
|
5
7
|
import {
|
|
8
|
+
debug,
|
|
9
|
+
enableDebug,
|
|
6
10
|
paths
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-F36TGFK2.js";
|
|
8
12
|
|
|
9
13
|
// src/cli.ts
|
|
10
14
|
import { Command } from "commander";
|
|
@@ -20,7 +24,7 @@ function getVersion() {
|
|
|
20
24
|
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
21
25
|
return packageJson.version;
|
|
22
26
|
} catch {
|
|
23
|
-
return "0.1.
|
|
27
|
+
return "0.1.6";
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
function processSessionFallbackMarker() {
|
|
@@ -30,20 +34,19 @@ function processSessionFallbackMarker() {
|
|
|
30
34
|
}
|
|
31
35
|
try {
|
|
32
36
|
const content = readFileSync(markerPath, "utf-8");
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
const raw = JSON.parse(content);
|
|
38
|
+
const marker = SessionFallbackMarkerSchema.parse(raw);
|
|
39
|
+
const state = configManager.loadState();
|
|
40
|
+
const session = state.sessions?.[marker.domain];
|
|
41
|
+
if (session) {
|
|
42
|
+
session.sessionId = marker.sessionId;
|
|
43
|
+
session.started = (/* @__PURE__ */ new Date()).toISOString();
|
|
44
|
+
session.lastActive = (/* @__PURE__ */ new Date()).toISOString();
|
|
45
|
+
state.activeSession = marker.sessionId;
|
|
46
|
+
configManager.saveState(state);
|
|
47
|
+
console.log(pc.yellow(`Note: Previous session for '${marker.domain}' could not be resumed.`));
|
|
48
|
+
console.log(pc.yellow(`A new session (${marker.sessionId.substring(0, 8)}...) was started automatically.
|
|
45
49
|
`));
|
|
46
|
-
}
|
|
47
50
|
}
|
|
48
51
|
unlinkSync(markerPath);
|
|
49
52
|
} catch {
|
|
@@ -53,8 +56,15 @@ function processSessionFallbackMarker() {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
59
|
+
function requireValidDomainName(name) {
|
|
60
|
+
const err = validateDomainName(name);
|
|
61
|
+
if (err) {
|
|
62
|
+
console.error(pc.red(`\u274C Invalid domain name: ${err}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
var program = new Command();
|
|
57
|
-
program.name("cs").description("Domain-based context management for Claude Code CLI - Each domain represents a project with its own working directory and configuration").version(getVersion()).addHelpText("after", `
|
|
67
|
+
program.name("cs").description("Domain-based context management for Claude Code CLI - Each domain represents a project with its own working directory and configuration").version(getVersion()).option("--verbose", "Enable debug output for troubleshooting").addHelpText("after", `
|
|
58
68
|
Quick Start:
|
|
59
69
|
cs init Initialize ContextSwitch
|
|
60
70
|
cs domain add myproject -w /path/to/project Create a domain with a working directory
|
|
@@ -62,7 +72,18 @@ Quick Start:
|
|
|
62
72
|
cs list See all your domains
|
|
63
73
|
|
|
64
74
|
The --working-dir (-w) flag sets where Claude will run. If omitted, the current directory is used.
|
|
65
|
-
Run "cs domain add --help" for more details
|
|
75
|
+
Run "cs domain add --help" for more details.
|
|
76
|
+
|
|
77
|
+
Debug:
|
|
78
|
+
cs --verbose <command> Show detailed debug output
|
|
79
|
+
CS_DEBUG=1 cs <command> Same via environment variable`).hook("preAction", () => {
|
|
80
|
+
if (program.opts().verbose || process.env.CS_DEBUG === "1") {
|
|
81
|
+
enableDebug();
|
|
82
|
+
debug("cli", `ContextSwitch v${getVersion()}`);
|
|
83
|
+
debug("cli", `Platform: ${process.platform}`);
|
|
84
|
+
debug("cli", `Node: ${process.version}`);
|
|
85
|
+
debug("cli", `Config dir: ${paths.baseDir}`);
|
|
86
|
+
}
|
|
66
87
|
paths.ensureDirectories();
|
|
67
88
|
processSessionFallbackMarker();
|
|
68
89
|
});
|
|
@@ -127,6 +148,7 @@ program.command("status").description("Show current domain and session status").
|
|
|
127
148
|
console.log(pc.yellow('No active domain. Use "cs switch <domain>" to activate one.'));
|
|
128
149
|
return;
|
|
129
150
|
}
|
|
151
|
+
requireValidDomainName(domain);
|
|
130
152
|
const domainConfig = configManager.loadDomain(domain);
|
|
131
153
|
const session = state.sessions?.[domain];
|
|
132
154
|
console.log(pc.cyan(`\u{1F4CA} Domain Status: ${domain}
|
|
@@ -138,7 +160,7 @@ program.command("status").description("Show current domain and session status").
|
|
|
138
160
|
if (session) {
|
|
139
161
|
console.log(`
|
|
140
162
|
${pc.cyan("Session Information:")}`);
|
|
141
|
-
const { SessionManager } = await import("./session-
|
|
163
|
+
const { SessionManager } = await import("./session-H5HPE5OT.js");
|
|
142
164
|
console.log(SessionManager.formatSessionInfo(session));
|
|
143
165
|
} else {
|
|
144
166
|
console.log(pc.gray("\nNo active session for this domain."));
|
|
@@ -149,20 +171,47 @@ ${pc.cyan("Session Information:")}`);
|
|
|
149
171
|
}
|
|
150
172
|
});
|
|
151
173
|
program.command("switch <domain>").description("Switch to a different domain").option("-f, --force", "Force new session even if one exists").action(async (domain, options) => {
|
|
152
|
-
|
|
174
|
+
requireValidDomainName(domain);
|
|
175
|
+
const { switchCommand } = await import("./switch-CM6GRYEA.js");
|
|
153
176
|
await switchCommand(domain, options);
|
|
154
177
|
});
|
|
155
|
-
program.command("reset <domain>").description("Reset a domain session (archives current session)").option("--skip-archive", "Skip archiving the current session").action(async (domain, options) => {
|
|
156
|
-
|
|
178
|
+
program.command("reset <domain>").description("Reset a domain session (archives current session)").option("--skip-archive", "Skip archiving the current session").option("-f, --force", "Skip confirmation prompt").action(async (domain, options) => {
|
|
179
|
+
requireValidDomainName(domain);
|
|
180
|
+
if (!options.force) {
|
|
181
|
+
const enquirer = await import("enquirer");
|
|
182
|
+
const response = await enquirer.default.prompt({
|
|
183
|
+
type: "confirm",
|
|
184
|
+
name: "confirmed",
|
|
185
|
+
message: `Reset session for '${domain}'? This will clear the current session${options.skipArchive ? "" : " (session will be archived first)"}.`
|
|
186
|
+
});
|
|
187
|
+
const confirmed = response.confirmed;
|
|
188
|
+
if (!confirmed) {
|
|
189
|
+
console.log(pc.gray("Cancelled."));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const { resetCommand } = await import("./reset-TSWWNV2Y.js");
|
|
157
194
|
await resetCommand(domain, options);
|
|
158
195
|
});
|
|
159
196
|
program.command("archive <domain>").description("Archive the current session for a domain").action(async (domain) => {
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
requireValidDomainName(domain);
|
|
198
|
+
try {
|
|
199
|
+
const { archiveCommand } = await import("./archive-MN425GUR.js");
|
|
200
|
+
await archiveCommand(domain);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(pc.red(`\u274C ${error}`));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
162
205
|
});
|
|
163
206
|
var domainCmd = program.command("domain").description("Manage domains");
|
|
164
207
|
domainCmd.command("add <name>").description('Create a new domain\n\nExamples:\n cs domain add myapi --working-dir /path/to/project\n cs domain add frontend -w C:\\Users\\you\\app -d "React frontend"\n\nIf --working-dir is not specified, the current directory is used.').option("-w, --working-dir <path>", "Working directory for this domain (defaults to current directory)").option("-d, --description <text>", "Domain description").option("--extends <parent>", "Inherit from parent domain").action(async (name, options) => {
|
|
165
208
|
try {
|
|
209
|
+
const nameError = validateDomainName(name);
|
|
210
|
+
if (nameError) {
|
|
211
|
+
console.error(pc.red(`\u274C Invalid domain name: ${nameError}`));
|
|
212
|
+
console.log(pc.gray("Valid examples: myproject, backend-api, app_v2, my.project"));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
166
215
|
if (configManager.domainExists(name)) {
|
|
167
216
|
console.error(pc.red(`\u274C Domain '${name}' already exists`));
|
|
168
217
|
process.exit(1);
|
|
@@ -190,7 +239,7 @@ Next steps:`));
|
|
|
190
239
|
process.exit(1);
|
|
191
240
|
}
|
|
192
241
|
});
|
|
193
|
-
domainCmd.command("remove <name>").alias("rm").description("Remove a domain from ContextSwitch (does not affect working directory)").action(async (name) => {
|
|
242
|
+
domainCmd.command("remove <name>").alias("rm").description("Remove a domain from ContextSwitch (does not affect working directory)").option("-f, --force", "Skip confirmation prompt").action(async (name, options) => {
|
|
194
243
|
try {
|
|
195
244
|
if (!configManager.domainExists(name)) {
|
|
196
245
|
console.error(pc.red(`\u274C Domain '${name}' not found`));
|
|
@@ -202,6 +251,19 @@ domainCmd.command("remove <name>").alias("rm").description("Remove a domain from
|
|
|
202
251
|
console.log(pc.gray("Switch to a different domain first."));
|
|
203
252
|
process.exit(1);
|
|
204
253
|
}
|
|
254
|
+
if (!options.force) {
|
|
255
|
+
const enquirer = await import("enquirer");
|
|
256
|
+
const response = await enquirer.default.prompt({
|
|
257
|
+
type: "confirm",
|
|
258
|
+
name: "confirmed",
|
|
259
|
+
message: `Remove domain '${name}'? This will delete the domain configuration and session data.`
|
|
260
|
+
});
|
|
261
|
+
const confirmed = response.confirmed;
|
|
262
|
+
if (!confirmed) {
|
|
263
|
+
console.log(pc.gray("Cancelled."));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
205
267
|
if (state.sessions?.[name]) {
|
|
206
268
|
delete state.sessions[name];
|
|
207
269
|
configManager.saveState(state);
|
|
@@ -216,7 +278,7 @@ domainCmd.command("remove <name>").alias("rm").description("Remove a domain from
|
|
|
216
278
|
}
|
|
217
279
|
});
|
|
218
280
|
program.command("doctor").description("Check system configuration and diagnose issues").action(async () => {
|
|
219
|
-
const { processManager } = await import("./process-
|
|
281
|
+
const { processManager } = await import("./process-X2SYCF5V.js");
|
|
220
282
|
console.log(pc.cyan("\u{1FA7A} Running diagnostics...\n"));
|
|
221
283
|
const failures = [];
|
|
222
284
|
const claudeInstalled = processManager.verifyClaudeInstalled();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
archiveCommand
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BGARUR7R.js";
|
|
4
4
|
import {
|
|
5
5
|
configManager
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-D3DHIVER.js";
|
|
7
|
+
import "./chunk-F36TGFK2.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/reset.ts
|
|
10
10
|
import picocolors from "picocolors";
|
|
@@ -21,11 +21,15 @@ async function resetCommand(domainName, options = {}) {
|
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
console.log(pc.cyan(`\u{1F504} Resetting domain '${domainName}'...`));
|
|
24
|
-
if (
|
|
24
|
+
if (options.skipArchive) {
|
|
25
|
+
console.log(pc.gray("Skipping archive (--skip-archive)."));
|
|
26
|
+
} else {
|
|
25
27
|
const globalConfig = configManager.loadGlobalConfig();
|
|
26
28
|
if (globalConfig.autoArchive) {
|
|
27
29
|
console.log(pc.gray("Archiving current session..."));
|
|
28
30
|
await archiveCommand(domainName);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(pc.gray("Skipping archive (autoArchive is disabled in global config)."));
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
const newState = {
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SessionManager
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MGIXKKM6.js";
|
|
4
4
|
import {
|
|
5
5
|
processManager
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-K7ISIY3Q.js";
|
|
7
7
|
import {
|
|
8
|
-
configManager
|
|
9
|
-
|
|
8
|
+
configManager,
|
|
9
|
+
validateEnvVarName
|
|
10
|
+
} from "./chunk-D3DHIVER.js";
|
|
10
11
|
import {
|
|
12
|
+
debug,
|
|
11
13
|
paths
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-F36TGFK2.js";
|
|
13
15
|
|
|
14
16
|
// src/commands/switch.ts
|
|
15
17
|
import picocolors from "picocolors";
|
|
16
|
-
import { existsSync, writeFileSync, readFileSync, unlinkSync, copyFileSync, mkdirSync,
|
|
18
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync, copyFileSync, mkdirSync, symlinkSync, lstatSync, readlinkSync } from "fs";
|
|
17
19
|
import { join, dirname, basename } from "path";
|
|
18
20
|
var pc = picocolors;
|
|
19
21
|
function shellEscapeArg(arg) {
|
|
20
22
|
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
21
23
|
}
|
|
24
|
+
function isSymlinkBroken(path) {
|
|
25
|
+
try {
|
|
26
|
+
const lst = lstatSync(path);
|
|
27
|
+
return lst.isSymbolicLink() && !existsSync(path);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
async function switchCommand(domainName, options = {}) {
|
|
23
33
|
try {
|
|
24
|
-
processSessionFallback();
|
|
25
34
|
console.log(pc.cyan(`\u{1F504} Switching to domain '${domainName}'...`));
|
|
26
35
|
if (!processManager.verifyClaudeInstalled()) {
|
|
27
36
|
printClaudeInstallHelp();
|
|
@@ -58,21 +67,18 @@ async function switchCommand(domainName, options = {}) {
|
|
|
58
67
|
isNewSession = true;
|
|
59
68
|
console.log(pc.gray(`Creating new session ${sessionId.substring(0, 8)}...`));
|
|
60
69
|
}
|
|
70
|
+
debug("switch", `Session ID: ${sessionId} (${isNewSession ? "new" : "resuming"})`);
|
|
71
|
+
debug("switch", `Working directory: ${domain.workingDirectory}`);
|
|
61
72
|
await updateMCPConfig(domain);
|
|
62
73
|
await handleMemoryFiles(domain);
|
|
63
74
|
console.log(pc.gray("Starting Claude with new configuration..."));
|
|
64
|
-
let pid;
|
|
65
75
|
if (!isNewSession) {
|
|
66
76
|
const fallbackSessionId = SessionManager.generateSessionId(domainName, true);
|
|
67
|
-
|
|
77
|
+
debug("switch", `Resume with fallback: resume=${sessionId.substring(0, 8)}, fallback=${fallbackSessionId.substring(0, 8)}`);
|
|
78
|
+
spawnClaudeWithFallback(domain, sessionId, fallbackSessionId);
|
|
68
79
|
} else {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
env: {
|
|
72
|
-
...process.env,
|
|
73
|
-
...domain.env
|
|
74
|
-
}
|
|
75
|
-
});
|
|
80
|
+
debug("switch", `New session: ${sessionId.substring(0, 8)}`);
|
|
81
|
+
spawnClaudeNewSession(domain, sessionId);
|
|
76
82
|
}
|
|
77
83
|
const newState = {
|
|
78
84
|
...state,
|
|
@@ -81,13 +87,10 @@ async function switchCommand(domainName, options = {}) {
|
|
|
81
87
|
lastSwitch: (/* @__PURE__ */ new Date()).toISOString(),
|
|
82
88
|
sessions: {
|
|
83
89
|
...state.sessions,
|
|
84
|
-
[domainName]: SessionManager.createSessionRecord(domainName, sessionId
|
|
90
|
+
[domainName]: SessionManager.createSessionRecord(domainName, sessionId)
|
|
85
91
|
}
|
|
86
92
|
};
|
|
87
93
|
configManager.saveState(newState);
|
|
88
|
-
if (pid) {
|
|
89
|
-
processManager.writePidFile(domainName, pid);
|
|
90
|
-
}
|
|
91
94
|
console.log(pc.green(`\u2705 Switched to domain '${domainName}'`));
|
|
92
95
|
console.log(pc.gray(`Working directory: ${domain.workingDirectory}`));
|
|
93
96
|
if (domain.mcpServers && Object.keys(domain.mcpServers).length > 0) {
|
|
@@ -120,6 +123,7 @@ async function updateMCPConfig(domain) {
|
|
|
120
123
|
}
|
|
121
124
|
};
|
|
122
125
|
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
126
|
+
debug("switch", `MCP config written to ${mcpConfigPath} with servers: ${Object.keys(mcpConfig.servers || {}).join(", ") || "none"}`);
|
|
123
127
|
console.log(pc.gray("Updated MCP configuration"));
|
|
124
128
|
}
|
|
125
129
|
async function handleMemoryFiles(domain) {
|
|
@@ -131,38 +135,79 @@ async function handleMemoryFiles(domain) {
|
|
|
131
135
|
if (!existsSync(memoryDir)) {
|
|
132
136
|
mkdirSync(memoryDir, { recursive: true });
|
|
133
137
|
}
|
|
134
|
-
const incomingFileNames = /* @__PURE__ */ new Set();
|
|
135
138
|
for (const memoryPath of domain.claudeConfig.memory) {
|
|
136
139
|
const expandedPath = paths.expandPath(memoryPath);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
if (!existsSync(expandedPath)) {
|
|
141
|
+
console.log(pc.yellow(`Warning: Memory file not found: ${memoryPath}`));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const fileName = basename(expandedPath);
|
|
145
|
+
const destPath = join(memoryDir, fileName);
|
|
146
|
+
if (existsSync(destPath) || isSymlinkBroken(destPath)) {
|
|
143
147
|
try {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const lst = lstatSync(destPath);
|
|
149
|
+
if (lst.isSymbolicLink()) {
|
|
150
|
+
const target = readlinkSync(destPath);
|
|
151
|
+
if (target === expandedPath) {
|
|
152
|
+
console.log(pc.gray(`Memory file already linked: ${fileName}`));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
150
155
|
}
|
|
156
|
+
unlinkSync(destPath);
|
|
151
157
|
} catch {
|
|
158
|
+
try {
|
|
159
|
+
unlinkSync(destPath);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
152
162
|
}
|
|
153
163
|
}
|
|
164
|
+
try {
|
|
165
|
+
symlinkSync(expandedPath, destPath);
|
|
166
|
+
debug("switch", `Symlinked memory file: ${expandedPath} -> ${destPath}`);
|
|
167
|
+
console.log(pc.gray(`Linked memory file: ${fileName}`));
|
|
168
|
+
} catch (symlinkErr) {
|
|
169
|
+
debug("switch", `Symlink failed (${symlinkErr}), falling back to copy`);
|
|
170
|
+
copyFileSync(expandedPath, destPath);
|
|
171
|
+
console.log(pc.gray(`Copied memory file: ${fileName} (symlink not supported)`));
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
174
|
+
}
|
|
175
|
+
function escapeShellValue(val) {
|
|
176
|
+
return String(val).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
177
|
+
}
|
|
178
|
+
function buildEnvLines(env) {
|
|
179
|
+
if (!env) return "";
|
|
180
|
+
const lines = [];
|
|
181
|
+
for (const [k, v] of Object.entries(env)) {
|
|
182
|
+
const err = validateEnvVarName(k);
|
|
183
|
+
if (err) {
|
|
184
|
+
throw new Error(err);
|
|
160
185
|
}
|
|
161
|
-
|
|
162
|
-
const destPath = join(memoryDir, fileName);
|
|
163
|
-
copyFileSync(expandedPath, destPath);
|
|
164
|
-
console.log(pc.gray(`Loaded memory file: ${fileName}`));
|
|
186
|
+
lines.push(`export ${k}="${escapeShellValue(v)}"`);
|
|
165
187
|
}
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
function spawnClaudeNewSession(domain, sessionId) {
|
|
191
|
+
const claudeCmd = processManager.getClaudeExecutable();
|
|
192
|
+
if (!claudeCmd) {
|
|
193
|
+
throw new Error("Claude CLI not found");
|
|
194
|
+
}
|
|
195
|
+
const cwd = domain.workingDirectory;
|
|
196
|
+
const domainName = domain.name;
|
|
197
|
+
const pidFile = shellEscapeArg(join(paths.baseDir, `${domainName}.pid`));
|
|
198
|
+
const envLines = buildEnvLines(domain.env);
|
|
199
|
+
const script = [
|
|
200
|
+
"#!/bin/bash",
|
|
201
|
+
`cd ${shellEscapeArg(cwd)}`,
|
|
202
|
+
envLines,
|
|
203
|
+
"",
|
|
204
|
+
"# Launch Claude and capture its real PID",
|
|
205
|
+
`${shellEscapeArg(claudeCmd)} --session-id ${shellEscapeArg(sessionId)} &`,
|
|
206
|
+
"CLAUDE_PID=$!",
|
|
207
|
+
`echo $CLAUDE_PID > ${pidFile}`,
|
|
208
|
+
"wait $CLAUDE_PID"
|
|
209
|
+
].join("\n");
|
|
210
|
+
return processManager.spawnTerminalScript(script, cwd);
|
|
166
211
|
}
|
|
167
212
|
function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
|
|
168
213
|
const claudeCmd = processManager.getClaudeExecutable();
|
|
@@ -172,10 +217,7 @@ function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
|
|
|
172
217
|
const cwd = domain.workingDirectory;
|
|
173
218
|
const markerFile = getSessionFallbackPath();
|
|
174
219
|
const domainName = domain.name;
|
|
175
|
-
const
|
|
176
|
-
return String(val).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
177
|
-
};
|
|
178
|
-
const envLines = domain.env ? Object.entries(domain.env).map(([k, v]) => `export ${k}="${escapeShellValue(v)}"`).join("\n") : "";
|
|
220
|
+
const envLines = buildEnvLines(domain.env);
|
|
179
221
|
const markerJson = JSON.stringify({ domain: domainName, sessionId: fallbackSessionId });
|
|
180
222
|
const markerJsonEscaped = markerJson.replace(/'/g, "'\\''");
|
|
181
223
|
const escapedCwd = shellEscapeArg(cwd);
|
|
@@ -183,13 +225,17 @@ function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
|
|
|
183
225
|
const escapedResumeId = shellEscapeArg(resumeSessionId);
|
|
184
226
|
const escapedFallbackId = shellEscapeArg(fallbackSessionId);
|
|
185
227
|
const escapedMarkerFile = shellEscapeArg(markerFile);
|
|
228
|
+
const pidFile = shellEscapeArg(join(paths.baseDir, `${domainName}.pid`));
|
|
186
229
|
const script = [
|
|
187
230
|
"#!/bin/bash",
|
|
188
231
|
`cd ${escapedCwd}`,
|
|
189
232
|
envLines,
|
|
190
233
|
"",
|
|
191
|
-
"# Try to resume the existing session",
|
|
192
|
-
`${escapedClaudeCmd} --resume ${escapedResumeId}
|
|
234
|
+
"# Try to resume the existing session (background + wait to capture PID)",
|
|
235
|
+
`${escapedClaudeCmd} --resume ${escapedResumeId} &`,
|
|
236
|
+
"CLAUDE_PID=$!",
|
|
237
|
+
`echo $CLAUDE_PID > ${pidFile}`,
|
|
238
|
+
"wait $CLAUDE_PID",
|
|
193
239
|
"RESUME_EXIT=$?",
|
|
194
240
|
"",
|
|
195
241
|
"# If resume failed (non-zero exit), fall back to a new session",
|
|
@@ -201,7 +247,10 @@ function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
|
|
|
201
247
|
" # Write marker so cs can update state with the new session ID",
|
|
202
248
|
` echo '${markerJsonEscaped}' > ${escapedMarkerFile}`,
|
|
203
249
|
"",
|
|
204
|
-
` ${escapedClaudeCmd} --session-id ${escapedFallbackId}
|
|
250
|
+
` ${escapedClaudeCmd} --session-id ${escapedFallbackId} &`,
|
|
251
|
+
" CLAUDE_PID=$!",
|
|
252
|
+
` echo $CLAUDE_PID > ${pidFile}`,
|
|
253
|
+
" wait $CLAUDE_PID",
|
|
205
254
|
"fi"
|
|
206
255
|
].join("\n");
|
|
207
256
|
return processManager.spawnTerminalScript(script, cwd);
|
|
@@ -209,36 +258,6 @@ function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
|
|
|
209
258
|
function getSessionFallbackPath() {
|
|
210
259
|
return join(paths.baseDir, "session-fallback.json");
|
|
211
260
|
}
|
|
212
|
-
function processSessionFallback() {
|
|
213
|
-
const markerPath = getSessionFallbackPath();
|
|
214
|
-
if (!existsSync(markerPath)) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
try {
|
|
218
|
-
const content = readFileSync(markerPath, "utf-8");
|
|
219
|
-
const marker = JSON.parse(content);
|
|
220
|
-
if (marker.domain && marker.sessionId) {
|
|
221
|
-
const state = configManager.loadState();
|
|
222
|
-
const session = state.sessions?.[marker.domain];
|
|
223
|
-
if (session) {
|
|
224
|
-
session.sessionId = marker.sessionId;
|
|
225
|
-
session.started = (/* @__PURE__ */ new Date()).toISOString();
|
|
226
|
-
session.lastActive = (/* @__PURE__ */ new Date()).toISOString();
|
|
227
|
-
state.activeSession = marker.sessionId;
|
|
228
|
-
configManager.saveState(state);
|
|
229
|
-
console.log(pc.yellow(`Note: Previous session for '${marker.domain}' could not be resumed.`));
|
|
230
|
-
console.log(pc.yellow(`A new session (${marker.sessionId.substring(0, 8)}...) was started automatically.
|
|
231
|
-
`));
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
unlinkSync(markerPath);
|
|
235
|
-
} catch {
|
|
236
|
-
try {
|
|
237
|
-
unlinkSync(markerPath);
|
|
238
|
-
} catch {
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
261
|
function printClaudeInstallHelp() {
|
|
243
262
|
console.error(pc.red("\u274C Claude Code CLI not found\n"));
|
|
244
263
|
console.log(pc.white("ContextSwitch requires the Claude Code CLI to be installed.\n"));
|
package/package.json
CHANGED