@remeic/ccm 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -23
- package/dist/index.js +355 -124
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -13,33 +13,37 @@
|
|
|
13
13
|
</table>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
## IMPORTANT: Claude Code ToS (Multi-Terminal Use)
|
|
17
|
-
|
|
18
|
-
> **Mandatory compliance rule for this project**
|
|
19
|
-
> Do **not** use the **same Claude account** from multiple terminals at the same time.
|
|
20
|
-
> We enforce: **1 account = 1 person = 1 active terminal session**.
|
|
21
|
-
|
|
22
|
-
- `ccm` isolates profiles. It does **not** grant any extra rights under Anthropic terms.
|
|
23
|
-
- For parallel work, use separate licensed accounts/seats (or API keys under Commercial Terms).
|
|
24
|
-
- No credential sharing, no shared account handoff, no "one account used by multiple people".
|
|
25
|
-
- This is an intentionally strict project rule to remove ambiguity and reduce compliance risk.
|
|
26
|
-
- The CLI also surfaces this via `ccm compliance` (alias: `ccm tos`) and after every successful `ccm create`.
|
|
27
|
-
|
|
28
|
-
Official basis (as of April 8, 2026):
|
|
29
|
-
- [Claude Code Legal & Compliance](https://code.claude.com/docs/en/legal-and-compliance): usage limits for Pro/Max assume ordinary, individual use; Anthropic may enforce auth restrictions without notice.
|
|
30
|
-
- [Anthropic Consumer Terms](https://www.anthropic.com/legal/consumer-terms): account credentials/API keys must not be shared or made available to others; Anthropic may suspend/terminate for material breach.
|
|
31
|
-
- [Anthropic Commercial Terms](https://www.anthropic.com/legal/commercial-terms): customer is responsible for all activity under its account.
|
|
32
16
|
|
|
33
17
|
[](https://github.com/remeic/ccm/actions/workflows/ci.yml)
|
|
34
18
|
[](https://www.npmjs.com/package/@remeic/ccm)
|
|
35
19
|
[](https://github.com/Remeic/homebrew-tap)
|
|
36
20
|
[](LICENSE)
|
|
37
|
-
[](https://nodejs.org)
|
|
38
22
|
[](https://codecov.io/github/Remeic/ccm)
|
|
39
23
|
[](https://dashboard.stryker-mutator.io/reports/github.com/Remeic/ccm/main)
|
|
40
24
|
|
|
41
25
|
Manage separate Claude Code profiles for personal, work, and client accounts without repeated logout/login cycles. `ccm` keeps each profile isolated so switching is immediate and parallel sessions stay clean.
|
|
42
26
|
|
|
27
|
+
### IMPORTANT: Claude Code ToS (Multi-Terminal Use)
|
|
28
|
+
|
|
29
|
+
> [!IMPORTANT]
|
|
30
|
+
> **Mandatory compliance rule for this project**
|
|
31
|
+
> Do **not** use the **same Claude account** from multiple terminals at the same time.
|
|
32
|
+
> We enforce: **1 account = 1 person = 1 active terminal session**.
|
|
33
|
+
>
|
|
34
|
+
> - `ccm` isolates profiles. It does **not** grant any extra rights under Anthropic terms.
|
|
35
|
+
> - For parallel work, use separate licensed accounts/seats (or API keys under Commercial Terms).
|
|
36
|
+
> - No credential sharing, no shared account handoff, no "one account used by multiple people".
|
|
37
|
+
> - This is an intentionally strict project rule to remove ambiguity and reduce compliance risk.
|
|
38
|
+
> - The CLI also surfaces this via `ccm compliance` (alias: `ccm tos`) and after every successful `ccm create`.
|
|
39
|
+
>
|
|
40
|
+
> Official basis (as of April 8, 2026):
|
|
41
|
+
> - [Claude Code Legal & Compliance](https://code.claude.com/docs/en/legal-and-compliance): usage limits for Pro/Max assume ordinary, individual use; Anthropic may enforce auth restrictions without notice.
|
|
42
|
+
> - [Anthropic Consumer Terms](https://www.anthropic.com/legal/consumer-terms): account credentials/API keys must not be shared or made available to others; Anthropic may suspend/terminate for material breach.
|
|
43
|
+
> - [Anthropic Commercial Terms](https://www.anthropic.com/legal/commercial-terms): customer is responsible for all activity under its account.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
43
47
|
<p align="center">
|
|
44
48
|
<img src="./docs/assets/intro.gif" alt="ccm in action in a terminal view" width="100%" />
|
|
45
49
|
</p>
|
|
@@ -47,7 +51,6 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
|
|
|
47
51
|
## Table of Contents
|
|
48
52
|
|
|
49
53
|
- [Table of Contents](#table-of-contents)
|
|
50
|
-
- [IMPORTANT: Claude Code ToS (Multi-Terminal Use)](#important-claude-code-tos-multi-terminal-use)
|
|
51
54
|
- [Why](#why)
|
|
52
55
|
- [Prerequisites](#prerequisites)
|
|
53
56
|
- [Install](#install)
|
|
@@ -59,6 +62,7 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
|
|
|
59
62
|
- [Different Browser per Profile](#different-browser-per-profile)
|
|
60
63
|
- [URL-Only Mode](#url-only-mode)
|
|
61
64
|
- [API Key Auth](#api-key-auth)
|
|
65
|
+
- [Copying Config Between Profiles](#copying-config-between-profiles)
|
|
62
66
|
- [How It Works](#how-it-works)
|
|
63
67
|
- [Architecture Overview](#architecture-overview)
|
|
64
68
|
- [Profile Isolation](#profile-isolation)
|
|
@@ -73,6 +77,7 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
|
|
|
73
77
|
- [Comparison](#comparison)
|
|
74
78
|
- [FAQ](#faq)
|
|
75
79
|
- [Contributing](#contributing)
|
|
80
|
+
- [Homebrew Releases](#homebrew-releases)
|
|
76
81
|
- [License](#license)
|
|
77
82
|
|
|
78
83
|
## Why
|
|
@@ -81,7 +86,7 @@ Claude Code stores authentication in a single config directory. If you use multi
|
|
|
81
86
|
|
|
82
87
|
## Prerequisites
|
|
83
88
|
|
|
84
|
-
- [Node.js](https://nodejs.org) >=
|
|
89
|
+
- [Node.js](https://nodejs.org) >= 24
|
|
85
90
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and available on your `PATH`
|
|
86
91
|
|
|
87
92
|
## Install
|
|
@@ -128,14 +133,15 @@ $ ccm use work
|
|
|
128
133
|
|
|
129
134
|
| Command | Description |
|
|
130
135
|
| -------------------------------------------------------- | ------------------------------------------------------------- |
|
|
131
|
-
| `ccm create <name> [-l label] [-b browser]`
|
|
136
|
+
| `ccm create <name> [-l label] [-b browser] [--from p]` | Create a profile. `--from` seeds non-auth config |
|
|
132
137
|
| `ccm compliance` / `ccm tos` | Show the compliance notice, disclaimer, and official sources |
|
|
133
138
|
| `ccm list` | List all profiles with auth status, including drifted entries |
|
|
134
|
-
| `ccm use <name> [
|
|
139
|
+
| `ccm use <name> [args...]` | Launch Claude Code. Extra args are passed to Claude |
|
|
135
140
|
| `ccm login <name> [--console] [-b browser] [--url-only]` | Authenticate a profile |
|
|
136
141
|
| `ccm status [name]` | Show auth status and storage state for one or all profiles |
|
|
137
|
-
| `ccm rename <old-name> <new-name>` | Rename a profile (config, directory, and browser wrapper)
|
|
142
|
+
| `ccm rename <old-name> <new-name>` | Rename a profile (config, directory, and browser wrapper) |
|
|
138
143
|
| `ccm remove <name> [-f]` | Remove a profile. `-f` skips confirmation |
|
|
144
|
+
| `ccm copy-config <source> <target> [--only x] [--dry-run] [-f]` | Copy non-auth config (settings/hooks/skills plugins) |
|
|
139
145
|
| `ccm run <name> -p <prompt>` | Run a prompt non-interactively |
|
|
140
146
|
|
|
141
147
|
## Compliance Notice
|
|
@@ -157,10 +163,13 @@ The notice is intentionally conservative:
|
|
|
157
163
|
|
|
158
164
|
## Passing Flags and Environment Variables
|
|
159
165
|
|
|
160
|
-
|
|
166
|
+
Extra args in `ccm use` are forwarded to `claude` (with or without the `--` separator). Env vars from your shell are inherited.
|
|
161
167
|
|
|
162
168
|
```bash
|
|
163
169
|
# Pass flags
|
|
170
|
+
ccm use work --resume e56f8bd7-a2bb-4c0e-9aa0-3eb7f717bf6a
|
|
171
|
+
|
|
172
|
+
# Pass flags (explicit separator)
|
|
164
173
|
ccm use work -- --dangerously-skip-permissions
|
|
165
174
|
|
|
166
175
|
# Env vars + flags
|
|
@@ -212,6 +221,40 @@ Suppresses auto-opening a browser. Claude prints the auth URL — copy it, open
|
|
|
212
221
|
ccm login work --console
|
|
213
222
|
```
|
|
214
223
|
|
|
224
|
+
### Copying Config Between Profiles
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Preview before applying
|
|
228
|
+
ccm copy-config work personal --dry-run
|
|
229
|
+
|
|
230
|
+
# Copy with interactive overwrite confirmation
|
|
231
|
+
ccm copy-config work personal
|
|
232
|
+
|
|
233
|
+
# Force overwrite without prompt
|
|
234
|
+
ccm copy-config work personal --force
|
|
235
|
+
|
|
236
|
+
# Copy only settings (or only plugins)
|
|
237
|
+
ccm copy-config work personal --only settings
|
|
238
|
+
ccm copy-config work personal --only plugins
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`copy-config` is intentionally conservative. It copies only non-auth profile config paths:
|
|
242
|
+
|
|
243
|
+
- `settings.json`
|
|
244
|
+
- `plugins/`
|
|
245
|
+
|
|
246
|
+
It does **not** copy runtime/auth state such as `.claude.json`, `sessions/`, `history.jsonl`,
|
|
247
|
+
`projects/`, `session-env/`, `telemetry/`, or top-level profile cache directories.
|
|
248
|
+
|
|
249
|
+
You can narrow the copy scope with `--only settings` or `--only plugins`. The option is repeatable and also supports comma-separated values (e.g. `--only settings,plugins`).
|
|
250
|
+
|
|
251
|
+
You can also bootstrap at creation time:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Create profile and seed non-auth config from an existing profile
|
|
255
|
+
ccm create personal --from work
|
|
256
|
+
```
|
|
257
|
+
|
|
215
258
|
## How It Works
|
|
216
259
|
|
|
217
260
|
### Architecture Overview
|
|
@@ -240,10 +283,12 @@ src/
|
|
|
240
283
|
│ ├── config.ts # Config file I/O with atomic writes
|
|
241
284
|
│ ├── profiles.ts # Profile directory management and validation
|
|
242
285
|
│ ├── profile-store.ts # Reconciled config/filesystem profile view
|
|
286
|
+
│ ├── profile-config-copy.ts # Config copy planner + transactional apply/rollback
|
|
243
287
|
│ ├── claude.ts # Claude binary discovery, spawning, auth status
|
|
244
288
|
│ ├── compliance.ts # Centralized compliance notice text and official sources
|
|
245
289
|
│ └── browsers.ts # Browser wrapper generation and validation
|
|
246
290
|
└── commands/
|
|
291
|
+
├── copy-config.ts # Copy non-auth config between profiles (dry-run + rollback)
|
|
247
292
|
├── compliance.ts # Dedicated compliance/TOS command
|
|
248
293
|
├── create.ts # Create profile (with rollback on failure)
|
|
249
294
|
├── list.ts # List profiles with auth status and drift state
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, renameSync, statSync, rmSync, unlinkSync, cpSync, readFileSync, readdirSync } from 'fs';
|
|
4
5
|
import { join, dirname, basename } from 'path';
|
|
5
6
|
import { homedir } from 'os';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { spawn, execFileSync } from 'child_process';
|
|
8
|
-
import { createInterface } from 'readline';
|
|
9
9
|
|
|
10
10
|
// src/lib/compliance.ts
|
|
11
11
|
var COMPLIANCE_CHECKED_AT = "2026-04-08";
|
|
@@ -73,8 +73,332 @@ var CCM_HOME = join(homedir(), ".ccm");
|
|
|
73
73
|
var PROFILES_DIR = join(CCM_HOME, "profiles");
|
|
74
74
|
var CONFIG_FILE = join(CCM_HOME, "config.json");
|
|
75
75
|
var BROWSERS_DIR = join(CCM_HOME, "browsers");
|
|
76
|
+
var MAX_PROFILE_NAME_LENGTH = 64;
|
|
77
|
+
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
78
|
+
var ProfileNameSchema = z.string().min(1).max(MAX_PROFILE_NAME_LENGTH).regex(PROFILE_NAME_PATTERN);
|
|
79
|
+
var ProfileMetaSchema = z.object({
|
|
80
|
+
name: ProfileNameSchema,
|
|
81
|
+
label: z.string().optional(),
|
|
82
|
+
browser: z.string().optional(),
|
|
83
|
+
createdAt: z.string()
|
|
84
|
+
}).passthrough();
|
|
85
|
+
var CcmConfigSchema = z.object({
|
|
86
|
+
profiles: z.record(z.string(), ProfileMetaSchema)
|
|
87
|
+
}).passthrough();
|
|
88
|
+
var ClaudeAuthStatusSchema = z.object({
|
|
89
|
+
loggedIn: z.boolean(),
|
|
90
|
+
authMethod: z.string(),
|
|
91
|
+
email: z.string().optional(),
|
|
92
|
+
orgName: z.string().optional(),
|
|
93
|
+
subscriptionType: z.string().optional(),
|
|
94
|
+
apiKeySource: z.string().optional()
|
|
95
|
+
}).passthrough();
|
|
76
96
|
|
|
77
|
-
// src/lib/
|
|
97
|
+
// src/lib/profiles.ts
|
|
98
|
+
function validateProfileName(name) {
|
|
99
|
+
const result = ProfileNameSchema.safeParse(name);
|
|
100
|
+
if (result.success) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (name.length > MAX_PROFILE_NAME_LENGTH) {
|
|
104
|
+
throw new Error(`Profile name too long (max ${MAX_PROFILE_NAME_LENGTH} chars).`);
|
|
105
|
+
}
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid profile name "${name}". Use only letters, numbers, hyphens, underscores.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function createProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
111
|
+
validateProfileName(name);
|
|
112
|
+
const dir = join(profilesDir, name);
|
|
113
|
+
if (existsSync(dir)) {
|
|
114
|
+
throw new Error(`Profile directory "${name}" already exists`);
|
|
115
|
+
}
|
|
116
|
+
mkdirSync(dir, { recursive: true });
|
|
117
|
+
return dir;
|
|
118
|
+
}
|
|
119
|
+
function renameProfileDir(oldName, newName, profilesDir = PROFILES_DIR) {
|
|
120
|
+
const oldDir = join(profilesDir, oldName);
|
|
121
|
+
const newDir = join(profilesDir, newName);
|
|
122
|
+
if (!existsSync(oldDir)) {
|
|
123
|
+
throw new Error(`Profile directory "${oldName}" does not exist`);
|
|
124
|
+
}
|
|
125
|
+
if (existsSync(newDir)) {
|
|
126
|
+
throw new Error(`Profile directory "${newName}" already exists`);
|
|
127
|
+
}
|
|
128
|
+
renameSync(oldDir, newDir);
|
|
129
|
+
}
|
|
130
|
+
function removeProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
131
|
+
const dir = join(profilesDir, name);
|
|
132
|
+
if (!existsSync(dir)) {
|
|
133
|
+
throw new Error(`Profile directory "${name}" does not exist`);
|
|
134
|
+
}
|
|
135
|
+
rmSync(dir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
function getStagedPath(path) {
|
|
138
|
+
const parentDir = dirname(path);
|
|
139
|
+
const baseName = basename(path);
|
|
140
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
141
|
+
const suffix = attempt === 0 ? `${process.pid}-${Date.now()}` : `${process.pid}-${Date.now()}-${attempt.toString()}`;
|
|
142
|
+
const candidate = join(parentDir, `.${baseName}.staged-${suffix}`);
|
|
143
|
+
if (!existsSync(candidate)) {
|
|
144
|
+
return candidate;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Could not allocate a temporary path for "${baseName}"`);
|
|
148
|
+
}
|
|
149
|
+
function stageProfileDirRemoval(name, profilesDir = PROFILES_DIR) {
|
|
150
|
+
const dir = join(profilesDir, name);
|
|
151
|
+
if (!existsSync(dir)) {
|
|
152
|
+
throw new Error(`Profile directory "${name}" does not exist`);
|
|
153
|
+
}
|
|
154
|
+
const stagedDir = getStagedPath(dir);
|
|
155
|
+
renameSync(dir, stagedDir);
|
|
156
|
+
return stagedDir;
|
|
157
|
+
}
|
|
158
|
+
function restoreStagedProfileDir(stagedDir, name, profilesDir = PROFILES_DIR) {
|
|
159
|
+
renameSync(stagedDir, join(profilesDir, name));
|
|
160
|
+
}
|
|
161
|
+
function finalizeStagedProfileDirRemoval(stagedDir) {
|
|
162
|
+
rmSync(stagedDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
function getProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
165
|
+
return join(profilesDir, name);
|
|
166
|
+
}
|
|
167
|
+
function profileExists(name, profilesDir = PROFILES_DIR) {
|
|
168
|
+
return existsSync(join(profilesDir, name));
|
|
169
|
+
}
|
|
170
|
+
function listProfileDirs(profilesDir = PROFILES_DIR) {
|
|
171
|
+
if (!existsSync(profilesDir)) return [];
|
|
172
|
+
return readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/lib/profile-config-copy.ts
|
|
176
|
+
var SUPPORTED_CONFIG_PATHS = ["settings.json", "plugins"];
|
|
177
|
+
function planProfileConfigCopy(sourceName, targetName, profilesDir = PROFILES_DIR, options = {}) {
|
|
178
|
+
validateProfileName(sourceName);
|
|
179
|
+
validateProfileName(targetName);
|
|
180
|
+
if (sourceName === targetName) {
|
|
181
|
+
throw new Error("Source and target profiles must be different");
|
|
182
|
+
}
|
|
183
|
+
if (!profileExists(sourceName, profilesDir)) {
|
|
184
|
+
throw new Error(`Profile "${sourceName}" does not exist`);
|
|
185
|
+
}
|
|
186
|
+
if (!profileExists(targetName, profilesDir)) {
|
|
187
|
+
throw new Error(`Profile "${targetName}" does not exist`);
|
|
188
|
+
}
|
|
189
|
+
const sourceDir = getProfileDir(sourceName, profilesDir);
|
|
190
|
+
const targetDir = getProfileDir(targetName, profilesDir);
|
|
191
|
+
const selectedPaths = normalizeSelectedPaths(options.only);
|
|
192
|
+
const operations = [];
|
|
193
|
+
for (const relativePath of selectedPaths) {
|
|
194
|
+
const sourcePath = join(sourceDir, relativePath);
|
|
195
|
+
if (!existsSync(sourcePath)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const kind = statSync(sourcePath).isDirectory() ? "directory" : "file";
|
|
199
|
+
const targetPath = join(targetDir, relativePath);
|
|
200
|
+
operations.push({
|
|
201
|
+
sourcePath,
|
|
202
|
+
targetPath,
|
|
203
|
+
relativePath,
|
|
204
|
+
kind,
|
|
205
|
+
overwrite: existsSync(targetPath)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (operations.length === 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`No supported configuration found in profile "${sourceName}". Supported paths: ${SUPPORTED_CONFIG_PATHS.join(", ")}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const overwriteCount = operations.filter((operation) => operation.overwrite).length;
|
|
214
|
+
return {
|
|
215
|
+
sourceName,
|
|
216
|
+
targetName,
|
|
217
|
+
operations,
|
|
218
|
+
overwriteCount,
|
|
219
|
+
createCount: operations.length - overwriteCount
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function normalizeSelectedPaths(only) {
|
|
223
|
+
if (!only || only.length === 0) {
|
|
224
|
+
return [...SUPPORTED_CONFIG_PATHS];
|
|
225
|
+
}
|
|
226
|
+
const selected = /* @__PURE__ */ new Set();
|
|
227
|
+
for (const entry of only) {
|
|
228
|
+
if (entry === "settings" || entry === "settings.json") {
|
|
229
|
+
selected.add("settings.json");
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (entry === "plugins") {
|
|
233
|
+
selected.add("plugins");
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`Unsupported --only target "${entry}". Allowed values: settings, plugins.`);
|
|
237
|
+
}
|
|
238
|
+
return SUPPORTED_CONFIG_PATHS.filter((path) => selected.has(path));
|
|
239
|
+
}
|
|
240
|
+
function applyProfileConfigCopy(plan) {
|
|
241
|
+
const stagedTargets = [];
|
|
242
|
+
const copiedTargets = [];
|
|
243
|
+
try {
|
|
244
|
+
for (const operation of plan.operations) {
|
|
245
|
+
if (operation.overwrite) {
|
|
246
|
+
const stagedPath = getStagedPath2(operation.targetPath);
|
|
247
|
+
renameSync(operation.targetPath, stagedPath);
|
|
248
|
+
stagedTargets.push({ targetPath: operation.targetPath, stagedPath });
|
|
249
|
+
}
|
|
250
|
+
mkdirSync(dirname(operation.targetPath), { recursive: true });
|
|
251
|
+
copyOperation(operation);
|
|
252
|
+
copiedTargets.push(operation.targetPath);
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const rollbackErrors = rollbackCopy(copiedTargets, stagedTargets);
|
|
256
|
+
if (rollbackErrors.length > 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Failed to copy profile configuration: ${formatError(error)}. Rollback failed: ${rollbackErrors.join("; ")}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`Failed to copy profile configuration: ${formatError(error)}`);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
for (const { stagedPath } of stagedTargets) {
|
|
265
|
+
rmSync(stagedPath, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
const rollbackErrors = rollbackCopy(copiedTargets, stagedTargets);
|
|
269
|
+
if (rollbackErrors.length > 0) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Failed to finalize profile configuration copy: ${formatError(error)}. Rollback failed: ${rollbackErrors.join("; ")}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
throw new Error(`Failed to finalize profile configuration copy: ${formatError(error)}`);
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
copiedCount: plan.operations.length,
|
|
278
|
+
overwrittenCount: plan.overwriteCount,
|
|
279
|
+
createdCount: plan.createCount
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function copyOperation(operation) {
|
|
283
|
+
const baseOptions = { errorOnExist: true, force: false };
|
|
284
|
+
if (operation.kind === "directory") {
|
|
285
|
+
cpSync(operation.sourcePath, operation.targetPath, { ...baseOptions, recursive: true });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
cpSync(operation.sourcePath, operation.targetPath, baseOptions);
|
|
289
|
+
}
|
|
290
|
+
function rollbackCopy(copiedTargets, stagedTargets) {
|
|
291
|
+
const rollbackErrors = [];
|
|
292
|
+
for (const copiedPath of [...copiedTargets].reverse()) {
|
|
293
|
+
try {
|
|
294
|
+
rmSync(copiedPath, { recursive: true, force: true });
|
|
295
|
+
} catch (error) {
|
|
296
|
+
rollbackErrors.push(`remove ${copiedPath}: ${formatError(error)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const { targetPath, stagedPath } of [...stagedTargets].reverse()) {
|
|
300
|
+
try {
|
|
301
|
+
if (existsSync(stagedPath)) {
|
|
302
|
+
renameSync(stagedPath, targetPath);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
rollbackErrors.push(`restore ${targetPath}: ${formatError(error)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return rollbackErrors;
|
|
309
|
+
}
|
|
310
|
+
function getStagedPath2(path) {
|
|
311
|
+
const parent = dirname(path);
|
|
312
|
+
const base = basename(path);
|
|
313
|
+
for (const attempt of Array.from({ length: 100 }, (_, index) => index)) {
|
|
314
|
+
const suffix = attempt === 0 ? `${process.pid}-${Date.now()}` : `${process.pid}-${Date.now()}-${attempt.toString()}`;
|
|
315
|
+
const candidate = join(parent, `.${base}.staged-${suffix}`);
|
|
316
|
+
if (!existsSync(candidate)) {
|
|
317
|
+
return candidate;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`Could not allocate a temporary path for "${base}"`);
|
|
321
|
+
}
|
|
322
|
+
function formatError(error) {
|
|
323
|
+
return error instanceof Error ? error.message : String(error);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/commands/copy-config.ts
|
|
327
|
+
function confirm(question) {
|
|
328
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
rl.question(question, (answer) => {
|
|
331
|
+
rl.close();
|
|
332
|
+
resolve(answer.toLowerCase() === "y");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function printPlan(plan) {
|
|
337
|
+
for (const operation of plan.operations) {
|
|
338
|
+
const mode = operation.overwrite ? "overwrite" : "create";
|
|
339
|
+
console.log(`- ${operation.relativePath} (${operation.kind}, ${mode})`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function collectOnlySelector(value, previous) {
|
|
343
|
+
const tokens = value.split(",").map((token) => token.trim());
|
|
344
|
+
if (tokens.some((token) => token.length === 0)) {
|
|
345
|
+
throw new Error("Invalid --only value. Empty entries are not allowed.");
|
|
346
|
+
}
|
|
347
|
+
return [...previous, ...tokens];
|
|
348
|
+
}
|
|
349
|
+
function resolvePlanOptions(only) {
|
|
350
|
+
if (!only || only.length === 0) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
return { only };
|
|
354
|
+
}
|
|
355
|
+
function registerCopyConfig(program2) {
|
|
356
|
+
program2.command("copy-config <source> <target>").description(
|
|
357
|
+
"Copy non-auth configuration (hooks, skills, settings) from one profile to another"
|
|
358
|
+
).option(
|
|
359
|
+
"--only <path>",
|
|
360
|
+
"Copy only selected path(s): settings, plugins (repeatable or comma-separated)",
|
|
361
|
+
collectOnlySelector,
|
|
362
|
+
[]
|
|
363
|
+
).option("--dry-run", "Preview what would be copied without writing files").option("-f, --force", "Skip overwrite confirmation and apply immediately").action(
|
|
364
|
+
async (source, target, opts) => {
|
|
365
|
+
try {
|
|
366
|
+
const plan = planProfileConfigCopy(
|
|
367
|
+
source,
|
|
368
|
+
target,
|
|
369
|
+
void 0,
|
|
370
|
+
resolvePlanOptions(opts.only)
|
|
371
|
+
);
|
|
372
|
+
if (opts.dryRun) {
|
|
373
|
+
console.log(
|
|
374
|
+
`Dry run: ${plan.operations.length} item(s) would be copied from "${source}" to "${target}"`
|
|
375
|
+
);
|
|
376
|
+
printPlan(plan);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (plan.overwriteCount > 0) {
|
|
380
|
+
console.log(
|
|
381
|
+
`! ${plan.overwriteCount} existing path(s) in "${target}" will be overwritten.`
|
|
382
|
+
);
|
|
383
|
+
if (!opts.force) {
|
|
384
|
+
const ok = await confirm("Continue? (y/N) ");
|
|
385
|
+
if (!ok) {
|
|
386
|
+
console.log("Cancelled");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const result = applyProfileConfigCopy(plan);
|
|
392
|
+
console.log(
|
|
393
|
+
`\x1B[32m\u2713\x1B[0m Copied ${result.copiedCount} item(s) from "${source}" to "${target}" (${result.createdCount} created, ${result.overwrittenCount} overwritten)`
|
|
394
|
+
);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}
|
|
78
402
|
var SHELL_METACHAR = /[;|&`$(){}<>\\!\n\r]/;
|
|
79
403
|
function validateBrowserCommand(cmd) {
|
|
80
404
|
if (SHELL_METACHAR.test(cmd)) {
|
|
@@ -97,7 +421,7 @@ exec ${browserCommand} "$@"
|
|
|
97
421
|
renameSync(tmp, scriptPath);
|
|
98
422
|
return scriptPath;
|
|
99
423
|
}
|
|
100
|
-
function
|
|
424
|
+
function getStagedPath3(path) {
|
|
101
425
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
102
426
|
const suffix = attempt === 0 ? `${process.pid}-${Date.now()}` : `${process.pid}-${Date.now()}-${attempt.toString()}`;
|
|
103
427
|
const candidate = `${path}.staged-${suffix}`;
|
|
@@ -130,7 +454,7 @@ function stageBrowserWrapperRemoval(profileName, browsersDir = BROWSERS_DIR) {
|
|
|
130
454
|
if (!existsSync(wrapperPath)) {
|
|
131
455
|
return void 0;
|
|
132
456
|
}
|
|
133
|
-
const stagedPath =
|
|
457
|
+
const stagedPath = getStagedPath3(wrapperPath);
|
|
134
458
|
renameSync(wrapperPath, stagedPath);
|
|
135
459
|
return stagedPath;
|
|
136
460
|
}
|
|
@@ -143,28 +467,6 @@ function finalizeStagedBrowserWrapperRemoval(stagedPath) {
|
|
|
143
467
|
function resolveBrowser(cliOverride, meta) {
|
|
144
468
|
return cliOverride ?? meta?.browser;
|
|
145
469
|
}
|
|
146
|
-
var MAX_PROFILE_NAME_LENGTH = 64;
|
|
147
|
-
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
148
|
-
var ProfileNameSchema = z.string().min(1).max(MAX_PROFILE_NAME_LENGTH).regex(PROFILE_NAME_PATTERN);
|
|
149
|
-
var ProfileMetaSchema = z.object({
|
|
150
|
-
name: ProfileNameSchema,
|
|
151
|
-
label: z.string().optional(),
|
|
152
|
-
browser: z.string().optional(),
|
|
153
|
-
createdAt: z.string()
|
|
154
|
-
}).passthrough();
|
|
155
|
-
var CcmConfigSchema = z.object({
|
|
156
|
-
profiles: z.record(z.string(), ProfileMetaSchema)
|
|
157
|
-
}).passthrough();
|
|
158
|
-
var ClaudeAuthStatusSchema = z.object({
|
|
159
|
-
loggedIn: z.boolean(),
|
|
160
|
-
authMethod: z.string(),
|
|
161
|
-
email: z.string().optional(),
|
|
162
|
-
orgName: z.string().optional(),
|
|
163
|
-
subscriptionType: z.string().optional(),
|
|
164
|
-
apiKeySource: z.string().optional()
|
|
165
|
-
}).passthrough();
|
|
166
|
-
|
|
167
|
-
// src/lib/config.ts
|
|
168
470
|
var DEFAULT_CONFIG = { profiles: {} };
|
|
169
471
|
function loadConfig(configFile = CONFIG_FILE) {
|
|
170
472
|
try {
|
|
@@ -216,97 +518,26 @@ function renameProfile(oldName, newName, configFile = CONFIG_FILE) {
|
|
|
216
518
|
function getProfile(name, configFile = CONFIG_FILE) {
|
|
217
519
|
return loadConfig(configFile).profiles[name];
|
|
218
520
|
}
|
|
219
|
-
function validateProfileName(name) {
|
|
220
|
-
const result = ProfileNameSchema.safeParse(name);
|
|
221
|
-
if (result.success) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (name.length > MAX_PROFILE_NAME_LENGTH) {
|
|
225
|
-
throw new Error(`Profile name too long (max ${MAX_PROFILE_NAME_LENGTH} chars).`);
|
|
226
|
-
}
|
|
227
|
-
throw new Error(
|
|
228
|
-
`Invalid profile name "${name}". Use only letters, numbers, hyphens, underscores.`
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
function createProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
232
|
-
validateProfileName(name);
|
|
233
|
-
const dir = join(profilesDir, name);
|
|
234
|
-
if (existsSync(dir)) {
|
|
235
|
-
throw new Error(`Profile directory "${name}" already exists`);
|
|
236
|
-
}
|
|
237
|
-
mkdirSync(dir, { recursive: true });
|
|
238
|
-
return dir;
|
|
239
|
-
}
|
|
240
|
-
function renameProfileDir(oldName, newName, profilesDir = PROFILES_DIR) {
|
|
241
|
-
const oldDir = join(profilesDir, oldName);
|
|
242
|
-
const newDir = join(profilesDir, newName);
|
|
243
|
-
if (!existsSync(oldDir)) {
|
|
244
|
-
throw new Error(`Profile directory "${oldName}" does not exist`);
|
|
245
|
-
}
|
|
246
|
-
if (existsSync(newDir)) {
|
|
247
|
-
throw new Error(`Profile directory "${newName}" already exists`);
|
|
248
|
-
}
|
|
249
|
-
renameSync(oldDir, newDir);
|
|
250
|
-
}
|
|
251
|
-
function removeProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
252
|
-
const dir = join(profilesDir, name);
|
|
253
|
-
if (!existsSync(dir)) {
|
|
254
|
-
throw new Error(`Profile directory "${name}" does not exist`);
|
|
255
|
-
}
|
|
256
|
-
rmSync(dir, { recursive: true });
|
|
257
|
-
}
|
|
258
|
-
function getStagedPath2(path) {
|
|
259
|
-
const parentDir = dirname(path);
|
|
260
|
-
const baseName = basename(path);
|
|
261
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
262
|
-
const suffix = attempt === 0 ? `${process.pid}-${Date.now()}` : `${process.pid}-${Date.now()}-${attempt.toString()}`;
|
|
263
|
-
const candidate = join(parentDir, `.${baseName}.staged-${suffix}`);
|
|
264
|
-
if (!existsSync(candidate)) {
|
|
265
|
-
return candidate;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
throw new Error(`Could not allocate a temporary path for "${baseName}"`);
|
|
269
|
-
}
|
|
270
|
-
function stageProfileDirRemoval(name, profilesDir = PROFILES_DIR) {
|
|
271
|
-
const dir = join(profilesDir, name);
|
|
272
|
-
if (!existsSync(dir)) {
|
|
273
|
-
throw new Error(`Profile directory "${name}" does not exist`);
|
|
274
|
-
}
|
|
275
|
-
const stagedDir = getStagedPath2(dir);
|
|
276
|
-
renameSync(dir, stagedDir);
|
|
277
|
-
return stagedDir;
|
|
278
|
-
}
|
|
279
|
-
function restoreStagedProfileDir(stagedDir, name, profilesDir = PROFILES_DIR) {
|
|
280
|
-
renameSync(stagedDir, join(profilesDir, name));
|
|
281
|
-
}
|
|
282
|
-
function finalizeStagedProfileDirRemoval(stagedDir) {
|
|
283
|
-
rmSync(stagedDir, { recursive: true });
|
|
284
|
-
}
|
|
285
|
-
function getProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
286
|
-
return join(profilesDir, name);
|
|
287
|
-
}
|
|
288
|
-
function profileExists(name, profilesDir = PROFILES_DIR) {
|
|
289
|
-
return existsSync(join(profilesDir, name));
|
|
290
|
-
}
|
|
291
|
-
function listProfileDirs(profilesDir = PROFILES_DIR) {
|
|
292
|
-
if (!existsSync(profilesDir)) return [];
|
|
293
|
-
return readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
294
|
-
}
|
|
295
521
|
|
|
296
522
|
// src/commands/create.ts
|
|
297
523
|
function registerCreate(program2) {
|
|
298
|
-
program2.command("create <name>").description("Create a new profile").option("-l, --label <label>", "Profile label").option("-b, --browser <path>", "Browser for OAuth login").action((name, opts) => {
|
|
524
|
+
program2.command("create <name>").description("Create a new profile").option("-l, --label <label>", "Profile label").option("-b, --browser <path>", "Browser for OAuth login").option("--from <profile>", "Initialize non-auth config from an existing profile").action((name, opts) => {
|
|
299
525
|
try {
|
|
300
526
|
createProfileDir(name);
|
|
301
|
-
|
|
527
|
+
let browser;
|
|
302
528
|
try {
|
|
529
|
+
browser = opts.browser ? ensureBrowserWrapper(name, opts.browser) : void 0;
|
|
530
|
+
if (opts.from) {
|
|
531
|
+
const plan = planProfileConfigCopy(opts.from, name);
|
|
532
|
+
applyProfileConfigCopy(plan);
|
|
533
|
+
}
|
|
303
534
|
addProfile({
|
|
304
535
|
name,
|
|
305
536
|
label: opts.label,
|
|
306
537
|
browser,
|
|
307
538
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
308
539
|
});
|
|
309
|
-
} catch (
|
|
540
|
+
} catch (setupErr) {
|
|
310
541
|
try {
|
|
311
542
|
removeProfileDir(name);
|
|
312
543
|
} catch {
|
|
@@ -315,7 +546,7 @@ function registerCreate(program2) {
|
|
|
315
546
|
removeBrowserWrapper(name);
|
|
316
547
|
} catch {
|
|
317
548
|
}
|
|
318
|
-
throw
|
|
549
|
+
throw setupErr;
|
|
319
550
|
}
|
|
320
551
|
console.log(`\x1B[32m\u2713\x1B[0m Profile "${name}" created`);
|
|
321
552
|
for (const line of getCompactComplianceNoticeLines()) {
|
|
@@ -439,7 +670,7 @@ function removeStoredProfile(name, configFile = CONFIG_FILE, profilesDir = PROFI
|
|
|
439
670
|
rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDir);
|
|
440
671
|
} catch (rollbackError) {
|
|
441
672
|
throw new Error(
|
|
442
|
-
`Failed to remove profile "${name}": ${
|
|
673
|
+
`Failed to remove profile "${name}": ${formatError2(error)}. Rollback failed: ${formatError2(
|
|
443
674
|
rollbackError
|
|
444
675
|
)}`
|
|
445
676
|
);
|
|
@@ -458,18 +689,18 @@ function removeStoredProfile(name, configFile = CONFIG_FILE, profilesDir = PROFI
|
|
|
458
689
|
try {
|
|
459
690
|
rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDir);
|
|
460
691
|
} catch (rollbackError) {
|
|
461
|
-
recoveryErrors.push(
|
|
692
|
+
recoveryErrors.push(formatError2(rollbackError));
|
|
462
693
|
}
|
|
463
694
|
if (profile.meta) {
|
|
464
695
|
try {
|
|
465
696
|
restoreConfigEntry(profile.meta, configFile);
|
|
466
697
|
} catch (configRestoreError) {
|
|
467
|
-
recoveryErrors.push(`config: ${
|
|
698
|
+
recoveryErrors.push(`config: ${formatError2(configRestoreError)}`);
|
|
468
699
|
}
|
|
469
700
|
}
|
|
470
701
|
if (recoveryErrors.length > 0) {
|
|
471
702
|
throw new Error(
|
|
472
|
-
`Failed to remove profile "${name}": ${
|
|
703
|
+
`Failed to remove profile "${name}": ${formatError2(error)}. Recovery failed: ${recoveryErrors.join(
|
|
473
704
|
"; "
|
|
474
705
|
)}`
|
|
475
706
|
);
|
|
@@ -504,7 +735,7 @@ function renameStoredProfile(oldName, newName, configFile = CONFIG_FILE, profile
|
|
|
504
735
|
rollbackRename(oldName, newName, profile.hasDirectory, false, profilesDir, browsersDir);
|
|
505
736
|
} catch (rollbackError) {
|
|
506
737
|
throw new Error(
|
|
507
|
-
`Failed to rename profile "${oldName}" to "${newName}": ${
|
|
738
|
+
`Failed to rename profile "${oldName}" to "${newName}": ${formatError2(error)}. Rollback failed: ${formatError2(rollbackError)}`
|
|
508
739
|
);
|
|
509
740
|
}
|
|
510
741
|
throw error;
|
|
@@ -517,7 +748,7 @@ function renameStoredProfile(oldName, newName, configFile = CONFIG_FILE, profile
|
|
|
517
748
|
rollbackRename(oldName, newName, profile.hasDirectory, hadWrapper, profilesDir, browsersDir);
|
|
518
749
|
} catch (rollbackError) {
|
|
519
750
|
throw new Error(
|
|
520
|
-
`Failed to rename profile "${oldName}" to "${newName}": ${
|
|
751
|
+
`Failed to rename profile "${oldName}" to "${newName}": ${formatError2(error)}. Rollback failed: ${formatError2(rollbackError)}`
|
|
521
752
|
);
|
|
522
753
|
}
|
|
523
754
|
throw error;
|
|
@@ -529,14 +760,14 @@ function rollbackRename(oldName, newName, hadDirectory, hadWrapper, profilesDir,
|
|
|
529
760
|
try {
|
|
530
761
|
renameBrowserWrapper(newName, oldName, browsersDir);
|
|
531
762
|
} catch (error) {
|
|
532
|
-
rollbackErrors.push(`browser wrapper: ${
|
|
763
|
+
rollbackErrors.push(`browser wrapper: ${formatError2(error)}`);
|
|
533
764
|
}
|
|
534
765
|
}
|
|
535
766
|
if (hadDirectory) {
|
|
536
767
|
try {
|
|
537
768
|
renameProfileDir(newName, oldName, profilesDir);
|
|
538
769
|
} catch (error) {
|
|
539
|
-
rollbackErrors.push(`profile dir: ${
|
|
770
|
+
rollbackErrors.push(`profile dir: ${formatError2(error)}`);
|
|
540
771
|
}
|
|
541
772
|
}
|
|
542
773
|
if (rollbackErrors.length > 0) {
|
|
@@ -551,14 +782,14 @@ function rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDi
|
|
|
551
782
|
try {
|
|
552
783
|
restoreStagedProfileDir(stagedDir, name, profilesDir);
|
|
553
784
|
} catch (error) {
|
|
554
|
-
rollbackErrors.push(`profile dir: ${
|
|
785
|
+
rollbackErrors.push(`profile dir: ${formatError2(error)}`);
|
|
555
786
|
}
|
|
556
787
|
}
|
|
557
788
|
if (stagedWrapper) {
|
|
558
789
|
try {
|
|
559
790
|
restoreStagedBrowserWrapper(stagedWrapper, name, browsersDir);
|
|
560
791
|
} catch (error) {
|
|
561
|
-
rollbackErrors.push(`browser wrapper: ${
|
|
792
|
+
rollbackErrors.push(`browser wrapper: ${formatError2(error)}`);
|
|
562
793
|
}
|
|
563
794
|
}
|
|
564
795
|
if (rollbackErrors.length > 0) {
|
|
@@ -571,7 +802,7 @@ function restoreConfigEntry(meta, configFile) {
|
|
|
571
802
|
}
|
|
572
803
|
addProfile(meta, configFile);
|
|
573
804
|
}
|
|
574
|
-
function
|
|
805
|
+
function formatError2(error) {
|
|
575
806
|
return error instanceof Error ? error.message : String(error);
|
|
576
807
|
}
|
|
577
808
|
|
|
@@ -636,7 +867,7 @@ function registerLogin(program2) {
|
|
|
636
867
|
}
|
|
637
868
|
});
|
|
638
869
|
}
|
|
639
|
-
function
|
|
870
|
+
function confirm2(question) {
|
|
640
871
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
641
872
|
return new Promise((resolve) => {
|
|
642
873
|
rl.question(question, (answer) => {
|
|
@@ -653,7 +884,7 @@ function registerRemove(program2) {
|
|
|
653
884
|
throw new Error(`Profile "${name}" does not exist`);
|
|
654
885
|
}
|
|
655
886
|
if (!opts.force) {
|
|
656
|
-
const ok = await
|
|
887
|
+
const ok = await confirm2(`Remove profile "${name}"? (y/N) `);
|
|
657
888
|
if (!ok) {
|
|
658
889
|
console.log("Cancelled");
|
|
659
890
|
return;
|
|
@@ -749,14 +980,13 @@ function registerStatus(program2) {
|
|
|
749
980
|
|
|
750
981
|
// src/commands/use.ts
|
|
751
982
|
function registerUse(program2) {
|
|
752
|
-
program2.command("use <name>").description("Launch Claude Code with a profile").allowUnknownOption().helpOption(false).action((name,
|
|
983
|
+
program2.command("use <name> [args...]").description("Launch Claude Code with a profile").allowUnknownOption().helpOption(false).action((name, args) => {
|
|
753
984
|
try {
|
|
754
985
|
if (!profileExists(name)) {
|
|
755
986
|
throw new Error(`Profile "${name}" does not exist. Create it first: ccm create ${name}`);
|
|
756
987
|
}
|
|
757
988
|
const dir = getProfileDir(name);
|
|
758
|
-
const
|
|
759
|
-
const child = spawnClaude(dir, passthroughArgs);
|
|
989
|
+
const child = spawnClaude(dir, args);
|
|
760
990
|
child.on("close", (code) => process.exit(code ?? 0));
|
|
761
991
|
} catch (e) {
|
|
762
992
|
console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -766,8 +996,9 @@ function registerUse(program2) {
|
|
|
766
996
|
}
|
|
767
997
|
|
|
768
998
|
// src/index.ts
|
|
769
|
-
var program = new Command().name("ccm").version("0.
|
|
999
|
+
var program = new Command().name("ccm").version("0.5.1").description("Manage multiple Claude Code profiles");
|
|
770
1000
|
registerCreate(program);
|
|
1001
|
+
registerCopyConfig(program);
|
|
771
1002
|
registerCompliance(program);
|
|
772
1003
|
registerList(program);
|
|
773
1004
|
registerRemove(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remeic/ccm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "nvm-like manager for Claude Code profiles",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Giulio Fagioli",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"prepare": "husky"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
45
|
+
"node": ">=24"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"commander": "^13.1.0",
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
"@stryker-mutator/core": "^9.6.0",
|
|
56
56
|
"@stryker-mutator/typescript-checker": "^9.6.0",
|
|
57
57
|
"@stryker-mutator/vitest-runner": "^9.6.0",
|
|
58
|
-
"@vitest/coverage-v8": "^
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
59
59
|
"husky": "^9.1.7",
|
|
60
60
|
"lint-staged": "^16.4.0",
|
|
61
61
|
"tsup": "^8.4.0",
|
|
62
62
|
"typescript": "^5.8.3",
|
|
63
|
-
"vitest": "^
|
|
63
|
+
"vitest": "^4.1.4"
|
|
64
64
|
}
|
|
65
65
|
}
|