contextswitch 0.1.5 → 0.1.7

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.
@@ -1,9 +1,10 @@
1
1
  import {
2
+ debug,
2
3
  paths
3
- } from "./chunk-A7YXSI66.js";
4
+ } from "./chunk-F36TGFK2.js";
4
5
 
5
6
  // src/core/process.ts
6
- import { execSync, spawn } from "child_process";
7
+ import { execFileSync, execSync, spawn } from "child_process";
7
8
  import { platform } from "os";
8
9
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
9
10
  import { join } from "path";
@@ -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("claude") && !commandLine.includes("cs switch")) {
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: "claude",
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,124 +127,119 @@ 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
144
  /**
129
- * Spawn Claude with specific arguments
145
+ * Spawn a shell script in a terminal.
146
+ * @param mode - 'window' (new window), 'tab' (new tab), or 'inline' (current terminal)
130
147
  */
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.");
148
+ spawnTerminalScript(script, _cwd, mode = "window") {
149
+ const scriptFile = join(paths.baseDir, `launch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.sh`);
150
+ debug("process", `Writing launch script to ${scriptFile} (mode: ${mode})`);
151
+ debug("process", `Script contents:
152
+ ${script}`);
153
+ writeFileSync(scriptFile, script, { mode: 493 });
154
+ if (mode === "inline") {
155
+ debug("process", "Running inline \u2014 taking over current terminal");
156
+ try {
157
+ execFileSync("bash", [scriptFile], { stdio: "inherit" });
158
+ } catch {
159
+ }
160
+ return 0;
135
161
  }
136
162
  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], {
163
+ const escapedPath = scriptFile.replace(/"/g, '\\"');
164
+ let appleScript;
165
+ if (mode === "tab") {
166
+ appleScript = [
167
+ 'tell application "Terminal"',
168
+ " activate",
169
+ ` tell application "System Events" to keystroke "t" using command down`,
170
+ ` do script "bash \\"${escapedPath}\\"" in front window`,
171
+ "end tell"
172
+ ].join("\n");
173
+ } else {
174
+ appleScript = `tell app "Terminal" to do script "bash \\"${escapedPath}\\""`;
175
+ }
176
+ const proc2 = spawn("osascript", ["-e", appleScript], {
141
177
  detached: true,
142
178
  stdio: "ignore"
143
179
  });
144
- terminalProcess.unref();
145
- return terminalProcess.pid || 0;
180
+ proc2.unref();
181
+ return proc2.pid || 0;
146
182
  }
147
183
  if (this.platform === "linux") {
148
- const cwd = options?.cwd || process.cwd();
149
- const claudeCommand = `${claudeCmd} ${args.join(" ")}`;
184
+ const tabFlag = mode === "tab";
150
185
  const terminals = ["gnome-terminal", "xterm", "konsole", "xfce4-terminal"];
151
186
  for (const term of terminals) {
152
187
  try {
153
- const termProcess = spawn(term, ["--", "bash", "-c", `cd "${cwd}" && ${claudeCommand}`], {
188
+ const args = term === "gnome-terminal" && tabFlag ? ["--tab", "--", "bash", scriptFile] : ["--", "bash", scriptFile];
189
+ const proc2 = spawn(term, args, {
154
190
  detached: true,
155
191
  stdio: "ignore"
156
192
  });
157
- termProcess.unref();
158
- return termProcess.pid || 0;
193
+ proc2.unref();
194
+ return proc2.pid || 0;
159
195
  } catch {
160
196
  }
161
197
  }
162
198
  }
163
199
  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], {
200
+ const psScriptFile = scriptFile.replace(/\.sh$/, ".ps1");
201
+ const psScript = this.convertToPowerShell(script);
202
+ writeFileSync(psScriptFile, psScript, { mode: 493 });
203
+ debug("process", `PowerShell script:
204
+ ${psScript}`);
205
+ const useTab = mode === "tab";
206
+ try {
207
+ const wtArgs = useTab ? ["-w", "0", "nt", "powershell", "-ExecutionPolicy", "Bypass", "-File", psScriptFile] : ["powershell", "-ExecutionPolicy", "Bypass", "-File", psScriptFile];
208
+ const proc2 = spawn("wt.exe", wtArgs, {
169
209
  detached: true,
170
210
  stdio: "ignore",
171
211
  windowsHide: false
172
212
  });
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], {
213
+ proc2.unref();
214
+ debug("process", "Launched via Windows Terminal (wt.exe)");
215
+ return proc2.pid || 0;
216
+ } catch {
217
+ debug("process", "Windows Terminal not available, falling back");
218
+ }
219
+ try {
220
+ const proc2 = spawn("cmd.exe", ["/c", "start", "powershell", "-ExecutionPolicy", "Bypass", "-File", psScriptFile], {
178
221
  detached: true,
179
222
  stdio: "ignore",
180
223
  windowsHide: false
181
224
  });
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
- /**
195
- * Spawn a shell script in a new terminal window
196
- */
197
- spawnTerminalScript(script, _cwd) {
198
- const scriptFile = join(paths.baseDir, "launch.sh");
199
- writeFileSync(scriptFile, script, { mode: 493 });
200
- if (this.platform === "darwin") {
201
- const escapedPath = scriptFile.replace(/"/g, '\\"');
202
- const appleScript = `tell app "Terminal" to do script "bash \\"${escapedPath}\\""`;
203
- const proc2 = spawn("osascript", ["-e", appleScript], {
204
- detached: true,
205
- stdio: "ignore"
206
- });
207
- proc2.unref();
208
- return proc2.pid || 0;
209
- }
210
- if (this.platform === "linux") {
211
- const terminals = ["gnome-terminal", "xterm", "konsole", "xfce4-terminal"];
212
- for (const term of terminals) {
213
- try {
214
- const proc2 = spawn(term, ["--", "bash", scriptFile], {
215
- detached: true,
216
- stdio: "ignore"
217
- });
218
- proc2.unref();
219
- return proc2.pid || 0;
220
- } catch {
221
- }
225
+ proc2.unref();
226
+ debug("process", "Launched via PowerShell (cmd start)");
227
+ return proc2.pid || 0;
228
+ } catch {
229
+ debug("process", "PowerShell launch failed, trying Git Bash");
222
230
  }
223
- }
224
- if (this.platform === "win32") {
225
231
  const gitBash = this.findGitBash();
226
- const bashCmd = gitBash || "bash";
227
- const scriptPath = scriptFile.replace(/\\/g, "/");
228
- const proc2 = spawn("cmd.exe", ["/c", "start", "", bashCmd, scriptPath], {
229
- detached: true,
230
- stdio: "ignore",
231
- windowsHide: false
232
- });
233
- proc2.unref();
234
- return proc2.pid || 0;
232
+ if (gitBash) {
233
+ const scriptPath = scriptFile.replace(/\\/g, "/");
234
+ const proc2 = spawn("cmd.exe", ["/c", "start", "", gitBash, scriptPath], {
235
+ detached: true,
236
+ stdio: "ignore",
237
+ windowsHide: false
238
+ });
239
+ proc2.unref();
240
+ debug("process", "Launched via Git Bash");
241
+ return proc2.pid || 0;
242
+ }
235
243
  }
236
244
  const proc = spawn("bash", [scriptFile], {
237
245
  detached: true,
@@ -249,6 +257,114 @@ var ProcessManager = class {
249
257
  /**
250
258
  * Find Git Bash executable on Windows
251
259
  */
260
+ /**
261
+ * Convert a simple bash launch script to PowerShell.
262
+ * Handles: cd, export, exec, echo, if/fi, and comments.
263
+ */
264
+ convertToPowerShell(bashScript) {
265
+ const lines = bashScript.split("\n");
266
+ const psLines = [];
267
+ for (const line of lines) {
268
+ const trimmed = line.trim();
269
+ if (trimmed.startsWith("#!/")) continue;
270
+ if (trimmed.startsWith("#")) {
271
+ psLines.push(trimmed);
272
+ continue;
273
+ }
274
+ if (trimmed === "") {
275
+ psLines.push("");
276
+ continue;
277
+ }
278
+ const cdMatch = trimmed.match(/^cd\s+(.+)$/);
279
+ if (cdMatch) {
280
+ const dir = cdMatch[1].replace(/^'|'$/g, "").replace(/'\\''/g, "'");
281
+ psLines.push(`Set-Location "${dir}"`);
282
+ continue;
283
+ }
284
+ const exportMatch = trimmed.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)="(.*)"$/);
285
+ if (exportMatch) {
286
+ psLines.push(`$env:${exportMatch[1]} = "${exportMatch[2]}"`);
287
+ continue;
288
+ }
289
+ if (trimmed.startsWith("exec ")) {
290
+ const cmd = trimmed.slice(5);
291
+ const parts = this.parseBashCommand(cmd);
292
+ psLines.push(`& ${parts.map((p) => `"${p}"`).join(" ")}`);
293
+ continue;
294
+ }
295
+ if (trimmed.startsWith("echo ")) {
296
+ const msg = trimmed.slice(5).replace(/^"|"$/g, "").replace(/^'|'$/g, "");
297
+ psLines.push(`Write-Host "${msg}"`);
298
+ continue;
299
+ }
300
+ if (trimmed.match(/^if\s+\[.*\];\s*then$/)) {
301
+ psLines.push("if ($LASTEXITCODE -ne 0) {");
302
+ continue;
303
+ }
304
+ if (trimmed === "fi") {
305
+ psLines.push("}");
306
+ continue;
307
+ }
308
+ if (trimmed === "RESUME_EXIT=$?") {
309
+ psLines.push("# Exit code captured automatically as $LASTEXITCODE");
310
+ continue;
311
+ }
312
+ const cmdLine = trimmed;
313
+ if (cmdLine.match(/^'?[A-Za-z\/\\:]/)) {
314
+ const parts = this.parseBashCommand(cmdLine);
315
+ psLines.push(`& ${parts.map((p) => `"${p}"`).join(" ")}`);
316
+ continue;
317
+ }
318
+ psLines.push(`# ${trimmed}`);
319
+ }
320
+ return psLines.join("\r\n");
321
+ }
322
+ /**
323
+ * Parse a bash command with shell-escaped single-quoted arguments into parts.
324
+ */
325
+ parseBashCommand(cmd) {
326
+ const parts = [];
327
+ let current = "";
328
+ let inSingleQuote = false;
329
+ let i = 0;
330
+ while (i < cmd.length) {
331
+ const ch = cmd[i];
332
+ if (inSingleQuote) {
333
+ if (ch === "'") {
334
+ if (cmd.slice(i, i + 4) === "'\\''") {
335
+ current += "'";
336
+ i += 4;
337
+ continue;
338
+ }
339
+ inSingleQuote = false;
340
+ i++;
341
+ continue;
342
+ }
343
+ current += ch;
344
+ i++;
345
+ } else {
346
+ if (ch === "'") {
347
+ inSingleQuote = true;
348
+ i++;
349
+ continue;
350
+ }
351
+ if (ch === " " || ch === " ") {
352
+ if (current.length > 0) {
353
+ parts.push(current);
354
+ current = "";
355
+ }
356
+ i++;
357
+ continue;
358
+ }
359
+ current += ch;
360
+ i++;
361
+ }
362
+ }
363
+ if (current.length > 0) {
364
+ parts.push(current);
365
+ }
366
+ return parts;
367
+ }
252
368
  findGitBash() {
253
369
  if (this.platform !== "win32") return null;
254
370
  const candidates = [
@@ -280,12 +396,15 @@ var ProcessManager = class {
280
396
  if (this.platform === "win32") {
281
397
  const output = execSync("where claude", { encoding: "utf-8" }).trim();
282
398
  const firstLine = output.split("\n")[0];
399
+ debug("process", `Found Claude via 'where': ${firstLine}`);
283
400
  return firstLine || null;
284
401
  } else {
285
402
  const output = execSync("which claude", { encoding: "utf-8" }).trim();
403
+ debug("process", `Found Claude via 'which': ${output}`);
286
404
  return output || null;
287
405
  }
288
406
  } catch {
407
+ debug("process", "Claude not found via PATH, checking common install locations");
289
408
  const commonPaths = this.platform === "win32" ? [
290
409
  join(process.env.LOCALAPPDATA || "", "Programs", "Claude Code", "claude.exe"),
291
410
  join(process.env.PROGRAMFILES || "C:\\Program Files", "Claude Code", "claude.exe"),
@@ -299,9 +418,11 @@ var ProcessManager = class {
299
418
  ];
300
419
  for (const candidatePath of commonPaths) {
301
420
  if (candidatePath && existsSync(candidatePath)) {
421
+ debug("process", `Found Claude at fallback path: ${candidatePath}`);
302
422
  return candidatePath;
303
423
  }
304
424
  }
425
+ debug("process", "Claude executable not found anywhere");
305
426
  return null;
306
427
  }
307
428
  }
@@ -340,6 +461,9 @@ var ProcessManager = class {
340
461
  * Check if a process is still running
341
462
  */
342
463
  isProcessRunning(pid) {
464
+ if (!Number.isInteger(pid) || pid <= 0) {
465
+ return false;
466
+ }
343
467
  try {
344
468
  if (this.platform === "win32") {
345
469
  const output = execSync(`tasklist /FI "PID eq ${pid}"`, { encoding: "utf-8" });
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  configManager
3
- } from "./chunk-YMFZWGZO.js";
3
+ } from "./chunk-72MK25T3.js";
4
4
  import {
5
5
  paths
6
- } from "./chunk-A7YXSI66.js";
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: configManager.loadDomain(domainName)
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-IWXAKW6Z.js");
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
- console.error(pc.red(`\u274C Failed to archive session: ${error}`));
63
- process.exit(1);
62
+ throw new Error(`Failed to archive session: ${error}`);
64
63
  }
65
64
  }
66
65
  function listArchives(domainName) {
@@ -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
  /**