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 +85 -135
- package/dist/archive-MN425GUR.js +10 -0
- package/dist/{chunk-2LUEUGBV.js → chunk-BGARUR7R.js} +7 -14
- package/dist/{chunk-XGE4JP55.js → chunk-D3DHIVER.js} +73 -40
- package/dist/{chunk-A7YXSI66.js → chunk-F36TGFK2.js} +21 -2
- package/dist/{chunk-56TY2J6E.js → chunk-K7ISIY3Q.js} +43 -81
- package/dist/chunk-MGIXKKM6.js +103 -0
- package/dist/cli.js +100 -28
- package/dist/{process-WBBCEFGG.js → process-X2SYCF5V.js} +2 -3
- package/dist/{reset-3DSXZKRH.js → reset-TSWWNV2Y.js} +9 -13
- package/dist/session-H5HPE5OT.js +6 -0
- package/dist/{switch-MWKYEYHE.js → switch-CM6GRYEA.js} +125 -69
- package/package.json +1 -1
- package/dist/archive-3IGWZBSO.js +0 -12
- package/dist/chunk-PNKVD2UK.js +0 -26
- package/dist/chunk-UKMZ4CUZ.js +0 -116
- package/dist/session-YAMF4YD7.js +0 -9
package/README.md
CHANGED
|
@@ -1,100 +1,123 @@
|
|
|
1
1
|
# ContextSwitch
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
#
|
|
23
|
-
|
|
24
|
-
./install.sh
|
|
27
|
+
# See all domains
|
|
28
|
+
cs list
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
## Commands
|
|
28
32
|
|
|
29
|
-
###
|
|
30
|
-
```bash
|
|
31
|
-
# Install dependencies
|
|
32
|
-
npm install
|
|
33
|
+
### Core
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
echo "alias cs='node $(pwd)/dist/cli.js'" >> ~/.zshrc
|
|
39
|
-
source ~/.zshrc
|
|
42
|
+
**switch** accepts an optional flag:
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
cs
|
|
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
|
-
|
|
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
|
-
#
|
|
49
|
-
|
|
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
|
-
#
|
|
52
|
-
|
|
69
|
+
# Inherit settings from another domain
|
|
70
|
+
cs domain add staging --extends backend
|
|
71
|
+
```
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
node dist/cli.js switch backend
|
|
73
|
+
If `--working-dir` is omitted, the current directory is used.
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
node dist/cli.js status
|
|
75
|
+
**reset** options:
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
82
|
+
### Diagnostics
|
|
65
83
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
- macOS: `~/Library/Application Support/contextswitch/`
|
|
101
|
+
- Linux: `~/.config/contextswitch/`
|
|
102
|
+
- Windows: `%APPDATA%\contextswitch\`
|
|
81
103
|
|
|
82
|
-
|
|
83
|
-
- `cs --help` - Show help information
|
|
84
|
-
- `cs --version` - Show version
|
|
104
|
+
## How It Works
|
|
85
105
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configManager
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
59
|
+
const { SessionManager } = await import("./session-H5HPE5OT.js");
|
|
60
|
+
console.log(pc.gray(`Session age: ${SessionManager.getSessionAge(session.started)}`));
|
|
67
61
|
} catch (error) {
|
|
68
|
-
|
|
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-
|
|
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,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
|
-
|
|
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(
|
|
109
|
-
return
|
|
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
|
-
|
|
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(
|
|
127
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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(
|
|
201
|
-
|
|
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
|
|
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
|
};
|