contextswitch 0.1.4 → 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/README.md CHANGED
@@ -1,100 +1,123 @@
1
1
  # ContextSwitch
2
2
 
3
- Domain-based context management for Claude Code CLI. Switch between different project contexts while preserving session history.
3
+ Manage domain-based contexts for Claude Code CLI. Switch between projects without losing session history -- each domain remembers where you left off via Claude's `--resume` flag.
4
4
 
5
- ## Features
5
+ ## Installation
6
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
7
+ **Prerequisites**: Node.js 18 or later, [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
13
8
 
14
- ## Installation
9
+ ```bash
10
+ npm install -g contextswitch
11
+ ```
12
+
13
+ This installs the `cs` command globally.
14
+
15
+ ## Quick Start
15
16
 
16
- ### Quick Install
17
17
  ```bash
18
- # Clone the repository
19
- git clone https://github.com/yourusername/contextswitch.git
20
- cd contextswitch
18
+ # Initialize ContextSwitch (creates a default domain)
19
+ cs init
20
+
21
+ # Create a domain for a project
22
+ cs domain add backend --working-dir ~/projects/api
23
+
24
+ # Switch to that domain (launches Claude in the domain's working directory)
25
+ cs switch backend
21
26
 
22
- # Run the installation script
23
- chmod +x install.sh
24
- ./install.sh
27
+ # See all domains
28
+ cs list
25
29
  ```
26
30
 
27
- This sets up the `cs` command for easy use. For other installation methods, see [INSTALLATION.md](INSTALLATION.md).
31
+ ## Commands
28
32
 
29
- ### Manual Install
30
- ```bash
31
- # Install dependencies
32
- npm install
33
+ ### Core
33
34
 
34
- # Build the project
35
- npm run build:node
35
+ | Command | Description |
36
+ |---|---|
37
+ | `cs init` | Initialize ContextSwitch and create a default domain |
38
+ | `cs switch <domain>` | Switch to a domain (kills any running Claude, launches a new one) |
39
+ | `cs list` | List all domains (`ls` also works) |
40
+ | `cs status [domain]` | Show session info for the current or specified domain |
36
41
 
37
- # Add alias to your shell
38
- echo "alias cs='node $(pwd)/dist/cli.js'" >> ~/.zshrc
39
- source ~/.zshrc
42
+ **switch** accepts an optional flag:
40
43
 
41
- # Now you can use 'cs' command
42
- cs --help
44
+ ```bash
45
+ cs switch backend # Resume existing session if one exists
46
+ cs switch backend --force # Force a brand-new session
43
47
  ```
44
48
 
45
- ## Quick Start
49
+ ### Domain Management
50
+
51
+ | Command | Description |
52
+ |---|---|
53
+ | `cs domain add <name>` | Create a new domain |
54
+ | `cs domain remove <name>` | Delete a domain (`rm` also works) |
55
+ | `cs reset <domain>` | Reset a domain's session, archiving the current one first |
56
+ | `cs archive <domain>` | Snapshot the current session without resetting |
57
+
58
+ **domain add** options:
46
59
 
47
60
  ```bash
48
- # Initialize ContextSwitch
49
- node dist/cli.js init
61
+ # Unix
62
+ cs domain add backend --working-dir ~/projects/api
63
+ cs domain add frontend -w ~/projects/app --description "React frontend"
64
+
65
+ # Windows (Git Bash)
66
+ cs domain add backend --working-dir C:/Users/you/projects/api
67
+ cs domain add frontend -w C:/Users/you/projects/app -d "React frontend"
50
68
 
51
- # Create a new domain for your project (working directory is REQUIRED)
52
- node dist/cli.js domain add backend --working-dir ~/projects/api
69
+ # Inherit settings from another domain
70
+ cs domain add staging --extends backend
71
+ ```
53
72
 
54
- # The working directory is where Claude will operate when you switch to this domain
55
- node dist/cli.js switch backend
73
+ If `--working-dir` is omitted, the current directory is used.
56
74
 
57
- # Check status
58
- node dist/cli.js status
75
+ **reset** options:
59
76
 
60
- # List all domains
61
- node dist/cli.js list
77
+ ```bash
78
+ cs reset backend # Archive current session, then reset
79
+ cs reset backend --skip-archive # Reset without archiving
62
80
  ```
63
81
 
64
- ## Commands
82
+ ### Diagnostics
65
83
 
66
- ### Core Commands
84
+ | Command | Description |
85
+ |---|---|
86
+ | `cs doctor` | Check Claude CLI installation, config directory, running processes |
87
+ | `cs --help` | Show help |
88
+ | `cs --version` | Show version |
67
89
 
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
90
+ ## Platform Support
72
91
 
73
- ### Domain Management
92
+ | Platform | Status | Notes |
93
+ |---|---|---|
94
+ | macOS | Supported | Primary development platform |
95
+ | Linux | Supported | |
96
+ | Windows | Partial | Git Bash required; some process management features may not work |
74
97
 
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
98
+ Config and state are stored in the platform-appropriate location:
79
99
 
80
- ### Diagnostics
100
+ - macOS: `~/Library/Application Support/contextswitch/`
101
+ - Linux: `~/.config/contextswitch/`
102
+ - Windows: `%APPDATA%\contextswitch\`
81
103
 
82
- - `cs doctor` - Check system configuration
83
- - `cs --help` - Show help information
84
- - `cs --version` - Show version
104
+ ## How It Works
85
105
 
86
- ## Domain Configuration
106
+ **Domains** are named project contexts. Each domain has a working directory, an optional description, optional MCP server configuration, and a session history. Domain config is stored as a YAML file and can be edited directly.
87
107
 
88
- Domains are stored as YAML files in the configuration directory. Example domain:
108
+ **Sessions** are Claude conversation sessions identified by a UUID. When you run `cs switch`, ContextSwitch generates (or retrieves) a session ID for that domain and launches Claude with `--resume <sessionId>`, so Claude picks up where the previous conversation left off. Session IDs are deterministic -- the same domain always maps to the same base session ID.
109
+
110
+ **MCP config** at `~/.config/claude/mcp/config.json` is updated on each switch to reflect the MCP servers defined in the active domain's config. This lets each domain have its own set of MCP tools.
111
+
112
+ ### Domain Config Example
113
+
114
+ Domains are YAML files in the config directory. You can edit them directly after creating a domain with `cs domain add`:
89
115
 
90
116
  ```yaml
91
117
  name: backend
92
118
  workingDirectory: ~/projects/api
93
-
94
- claudeConfig:
95
- instructions: ./CLAUDE.md
96
- memory:
97
- - ./context/api-overview.md
119
+ metadata:
120
+ description: Backend API project
98
121
 
99
122
  mcpServers:
100
123
  filesystem:
@@ -107,79 +130,6 @@ env:
107
130
  NODE_ENV: development
108
131
  ```
109
132
 
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
133
  ## License
184
134
 
185
- MIT
135
+ MIT
@@ -0,0 +1,10 @@
1
+ import {
2
+ archiveCommand,
3
+ listArchives
4
+ } from "./chunk-BGARUR7R.js";
5
+ import "./chunk-D3DHIVER.js";
6
+ import "./chunk-F36TGFK2.js";
7
+ export {
8
+ archiveCommand,
9
+ listArchives
10
+ };
@@ -1,16 +1,9 @@
1
1
  import {
2
2
  configManager
3
- } from "./chunk-XGE4JP55.js";
4
- import {
5
- init_session,
6
- session_exports
7
- } from "./chunk-UKMZ4CUZ.js";
3
+ } from "./chunk-D3DHIVER.js";
8
4
  import {
9
5
  paths
10
- } from "./chunk-A7YXSI66.js";
11
- import {
12
- __toCommonJS
13
- } from "./chunk-PNKVD2UK.js";
6
+ } from "./chunk-F36TGFK2.js";
14
7
 
15
8
  // src/commands/archive.ts
16
9
  import picocolors from "picocolors";
@@ -33,18 +26,18 @@ async function archiveCommand(domainName) {
33
26
  if (!existsSync(archiveDir)) {
34
27
  mkdirSync(archiveDir, { recursive: true });
35
28
  }
29
+ const domain = configManager.loadDomain(domainName);
36
30
  const timestamp = /* @__PURE__ */ new Date();
37
31
  const archiveMetadata = {
38
32
  domain: domainName,
39
33
  session,
40
34
  timestamp: timestamp.toISOString(),
41
- config: configManager.loadDomain(domainName)
35
+ config: domain
42
36
  };
43
37
  const dateStr = timestamp.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, -5);
44
38
  const archiveName = `${dateStr}.json`;
45
39
  const archivePath = join(archiveDir, archiveName);
46
40
  writeFileSync(archivePath, JSON.stringify(archiveMetadata, null, 2), "utf-8");
47
- const domain = configManager.loadDomain(domainName);
48
41
  if (domain.claudeConfig?.memory) {
49
42
  const memoryArchiveDir = join(archiveDir, dateStr, "memory");
50
43
  mkdirSync(memoryArchiveDir, { recursive: true });
@@ -63,10 +56,10 @@ async function archiveCommand(domainName) {
63
56
  console.log(pc.green(`\u2705 Session archived successfully`));
64
57
  console.log(pc.gray(`Archive: ${archivePath}`));
65
58
  console.log(pc.gray(`Size: ${sizeKB} KB`));
66
- console.log(pc.gray(`Session age: ${(init_session(), __toCommonJS(session_exports)).SessionManager.getSessionAge(session.started)}`));
59
+ const { SessionManager } = await import("./session-H5HPE5OT.js");
60
+ console.log(pc.gray(`Session age: ${SessionManager.getSessionAge(session.started)}`));
67
61
  } catch (error) {
68
- console.error(pc.red(`\u274C Failed to archive session: ${error}`));
69
- process.exit(1);
62
+ throw new Error(`Failed to archive session: ${error}`);
70
63
  }
71
64
  }
72
65
  function listArchives(domainName) {
@@ -1,10 +1,7 @@
1
1
  import {
2
+ debug,
2
3
  paths
3
- } from "./chunk-A7YXSI66.js";
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(z.string()).optional().describe("Environment variables for this domain"),
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,33 +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
- if (this.globalConfig) {
98
- return this.globalConfig;
99
- }
100
126
  const configPath = paths.globalConfigFile;
101
127
  if (!existsSync(configPath)) {
102
- this.globalConfig = {
128
+ const defaultConfig = {
103
129
  version: "1.0.0",
104
130
  defaultDomain: "default",
105
131
  autoArchive: true,
106
132
  logLevel: "info"
107
133
  };
108
- this.saveGlobalConfig(this.globalConfig);
109
- return this.globalConfig;
134
+ this.saveGlobalConfig(defaultConfig);
135
+ return defaultConfig;
110
136
  }
111
137
  try {
112
138
  const content = readFileSync(configPath, "utf-8");
113
139
  const data = parse(content);
114
- this.globalConfig = GlobalConfigSchema.parse(data);
115
- return this.globalConfig;
140
+ return GlobalConfigSchema.parse(data);
116
141
  } catch (error) {
117
142
  throw new Error(`Failed to load global config: ${error}`);
118
143
  }
@@ -122,28 +147,29 @@ var ConfigManager = class {
122
147
  */
123
148
  saveGlobalConfig(config) {
124
149
  const configPath = paths.globalConfigFile;
150
+ const tempPath = `${configPath}.tmp`;
125
151
  const content = stringify(config, { indent: 2 });
126
- writeFileSync(configPath, content, "utf-8");
127
- this.globalConfig = config;
152
+ writeFileSync(tempPath, content, "utf-8");
153
+ renameSync(tempPath, configPath);
128
154
  }
129
155
  /**
130
156
  * Load state file
131
157
  */
132
158
  loadState() {
133
- if (this.state) {
134
- return this.state;
135
- }
136
159
  const statePath = paths.stateFile;
160
+ debug("config", `Loading state from ${statePath}`);
137
161
  if (!existsSync(statePath)) {
138
- this.state = createInitialState();
139
- this.saveState(this.state);
140
- return this.state;
162
+ debug("config", "No state file found, creating initial state");
163
+ const initial = createInitialState();
164
+ this.saveState(initial);
165
+ return initial;
141
166
  }
142
167
  try {
143
168
  const content = readFileSync(statePath, "utf-8");
144
169
  const data = JSON.parse(content);
145
- this.state = validateState(data);
146
- return this.state;
170
+ const state = validateState(data);
171
+ debug("config", `State loaded: activeDomain=${state.activeDomain}, sessions=${Object.keys(state.sessions || {}).join(",") || "none"}`);
172
+ return state;
147
173
  } catch (error) {
148
174
  throw new Error(`Failed to load state: ${error}`);
149
175
  }
@@ -154,19 +180,23 @@ var ConfigManager = class {
154
180
  saveState(state) {
155
181
  const statePath = paths.stateFile;
156
182
  const tempPath = `${statePath}.tmp`;
183
+ debug("config", `Saving state to ${statePath} (activeDomain=${state.activeDomain})`);
157
184
  const content = JSON.stringify(state, null, 2);
158
185
  writeFileSync(tempPath, content, "utf-8");
159
186
  renameSync(tempPath, statePath);
160
- this.state = state;
161
187
  }
162
188
  /**
163
189
  * Load a domain configuration
164
190
  */
165
191
  loadDomain(domainName) {
166
- if (this.domainCache.has(domainName)) {
167
- return this.domainCache.get(domainName);
168
- }
192
+ return this.loadDomainInternal(domainName, []);
193
+ }
194
+ /**
195
+ * Internal domain loader with cycle detection for inheritance chains.
196
+ */
197
+ loadDomainInternal(domainName, ancestors) {
169
198
  const domainPath = paths.domainConfigFile(domainName);
199
+ debug("config", `Loading domain '${domainName}' from ${domainPath}`);
170
200
  if (!existsSync(domainPath)) {
171
201
  throw new Error(`Domain '${domainName}' not found`);
172
202
  }
@@ -175,7 +205,11 @@ var ConfigManager = class {
175
205
  const data = parse(content);
176
206
  let domain = validateDomain(data);
177
207
  if (domain.extends) {
178
- const parent = this.loadDomain(domain.extends);
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]);
179
213
  domain = this.mergeDomains(parent, domain);
180
214
  }
181
215
  domain.workingDirectory = paths.expandPath(domain.workingDirectory);
@@ -185,7 +219,6 @@ var ConfigManager = class {
185
219
  if (domain.claudeConfig?.memory) {
186
220
  domain.claudeConfig.memory = domain.claudeConfig.memory.map((p) => paths.expandPath(p));
187
221
  }
188
- this.domainCache.set(domainName, domain);
189
222
  return domain;
190
223
  } catch (error) {
191
224
  throw new Error(`Failed to load domain '${domainName}': ${error}`);
@@ -196,9 +229,10 @@ var ConfigManager = class {
196
229
  */
197
230
  saveDomain(domain) {
198
231
  const domainPath = paths.domainConfigFile(domain.name);
232
+ const tempPath = `${domainPath}.tmp`;
199
233
  const content = stringify(domain, { indent: 2 });
200
- writeFileSync(domainPath, content, "utf-8");
201
- this.domainCache.set(domain.name, domain);
234
+ writeFileSync(tempPath, content, "utf-8");
235
+ renameSync(tempPath, domainPath);
202
236
  }
203
237
  /**
204
238
  * Create a new domain from template
@@ -243,7 +277,6 @@ var ConfigManager = class {
243
277
  throw new Error(`Domain '${domainName}' not found`);
244
278
  }
245
279
  unlinkSync(domainPath);
246
- this.domainCache.delete(domainName);
247
280
  }
248
281
  /**
249
282
  * Merge two domain configurations (child overrides parent)
@@ -291,13 +324,13 @@ var ConfigManager = class {
291
324
  * Clear cache
292
325
  */
293
326
  clearCache() {
294
- this.globalConfig = null;
295
- this.state = null;
296
- this.domainCache.clear();
297
327
  }
298
328
  };
299
329
  var configManager = new ConfigManager();
300
330
 
301
331
  export {
332
+ validateDomainName,
333
+ SessionFallbackMarkerSchema,
334
+ validateEnvVarName,
302
335
  configManager
303
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.startsWith("~/")) {
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
  };