@remeic/ccm 0.3.0 → 0.5.0
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 +89 -3
- package/dist/index.js +419 -121
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,16 +13,37 @@
|
|
|
13
13
|
</table>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
[](https://github.com/remeic/ccm/actions/workflows/ci.yml)
|
|
17
18
|
[](https://www.npmjs.com/package/@remeic/ccm)
|
|
18
19
|
[](https://github.com/Remeic/homebrew-tap)
|
|
19
20
|
[](LICENSE)
|
|
20
21
|
[](https://nodejs.org)
|
|
21
22
|
[](https://codecov.io/github/Remeic/ccm)
|
|
22
|
-
[](https://dashboard.stryker-mutator.io/reports/github.com/Remeic/ccm/main)
|
|
23
24
|
|
|
24
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.
|
|
25
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
|
+
|
|
26
47
|
<p align="center">
|
|
27
48
|
<img src="./docs/assets/intro.gif" alt="ccm in action in a terminal view" width="100%" />
|
|
28
49
|
</p>
|
|
@@ -35,11 +56,13 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
|
|
|
35
56
|
- [Install](#install)
|
|
36
57
|
- [Quick Start](#quick-start)
|
|
37
58
|
- [Commands](#commands)
|
|
59
|
+
- [Compliance Notice](#compliance-notice)
|
|
38
60
|
- [Passing Flags and Environment Variables](#passing-flags-and-environment-variables)
|
|
39
61
|
- [Multi-Account Login](#multi-account-login)
|
|
40
62
|
- [Different Browser per Profile](#different-browser-per-profile)
|
|
41
63
|
- [URL-Only Mode](#url-only-mode)
|
|
42
64
|
- [API Key Auth](#api-key-auth)
|
|
65
|
+
- [Copying Config Between Profiles](#copying-config-between-profiles)
|
|
43
66
|
- [How It Works](#how-it-works)
|
|
44
67
|
- [Architecture Overview](#architecture-overview)
|
|
45
68
|
- [Profile Isolation](#profile-isolation)
|
|
@@ -54,6 +77,7 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
|
|
|
54
77
|
- [Comparison](#comparison)
|
|
55
78
|
- [FAQ](#faq)
|
|
56
79
|
- [Contributing](#contributing)
|
|
80
|
+
- [Homebrew Releases](#homebrew-releases)
|
|
57
81
|
- [License](#license)
|
|
58
82
|
|
|
59
83
|
## Why
|
|
@@ -91,6 +115,11 @@ The installed command remains `ccm`.
|
|
|
91
115
|
```
|
|
92
116
|
$ ccm create work
|
|
93
117
|
✓ Profile "work" created
|
|
118
|
+
! Compliance notice
|
|
119
|
+
ccm isolates profiles but does not expand Anthropic usage rights.
|
|
120
|
+
Use each Claude account with one person and one active terminal session.
|
|
121
|
+
Do not share credentials or API keys across users or parallel operators.
|
|
122
|
+
Details: ccm compliance
|
|
94
123
|
Next: ccm login work
|
|
95
124
|
|
|
96
125
|
$ ccm login work
|
|
@@ -104,15 +133,34 @@ $ ccm use work
|
|
|
104
133
|
|
|
105
134
|
| Command | Description |
|
|
106
135
|
| -------------------------------------------------------- | ------------------------------------------------------------- |
|
|
107
|
-
| `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 |
|
|
137
|
+
| `ccm compliance` / `ccm tos` | Show the compliance notice, disclaimer, and official sources |
|
|
108
138
|
| `ccm list` | List all profiles with auth status, including drifted entries |
|
|
109
139
|
| `ccm use <name> [-- args]` | Launch Claude Code. Args after `--` are passed to Claude |
|
|
110
140
|
| `ccm login <name> [--console] [-b browser] [--url-only]` | Authenticate a profile |
|
|
111
141
|
| `ccm status [name]` | Show auth status and storage state for one or all profiles |
|
|
112
|
-
| `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) |
|
|
113
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) |
|
|
114
145
|
| `ccm run <name> -p <prompt>` | Run a prompt non-interactively |
|
|
115
146
|
|
|
147
|
+
## Compliance Notice
|
|
148
|
+
|
|
149
|
+
Every successful `ccm create` prints a short compliance warning. Users can re-open the full text at any time with:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
ccm compliance
|
|
153
|
+
ccm tos
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The notice is intentionally conservative:
|
|
157
|
+
|
|
158
|
+
- `ccm` is a profile-isolation tool, not a license-expansion tool.
|
|
159
|
+
- This project treats each Claude account as single-user and single-session.
|
|
160
|
+
- Shared credentials, shared operators, and parallel use of the same account are out of scope.
|
|
161
|
+
- If a team needs parallel access, they should use separate seats/accounts or API-key-based access under applicable Anthropic commercial terms.
|
|
162
|
+
- The notice is compliance guidance, not legal advice. Users remain responsible for reviewing Anthropic terms for their exact workflow.
|
|
163
|
+
|
|
116
164
|
## Passing Flags and Environment Variables
|
|
117
165
|
|
|
118
166
|
Everything after `--` in `ccm use` is forwarded to `claude`. Env vars from your shell are inherited.
|
|
@@ -170,6 +218,40 @@ Suppresses auto-opening a browser. Claude prints the auth URL — copy it, open
|
|
|
170
218
|
ccm login work --console
|
|
171
219
|
```
|
|
172
220
|
|
|
221
|
+
### Copying Config Between Profiles
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Preview before applying
|
|
225
|
+
ccm copy-config work personal --dry-run
|
|
226
|
+
|
|
227
|
+
# Copy with interactive overwrite confirmation
|
|
228
|
+
ccm copy-config work personal
|
|
229
|
+
|
|
230
|
+
# Force overwrite without prompt
|
|
231
|
+
ccm copy-config work personal --force
|
|
232
|
+
|
|
233
|
+
# Copy only settings (or only plugins)
|
|
234
|
+
ccm copy-config work personal --only settings
|
|
235
|
+
ccm copy-config work personal --only plugins
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`copy-config` is intentionally conservative. It copies only non-auth profile config paths:
|
|
239
|
+
|
|
240
|
+
- `settings.json`
|
|
241
|
+
- `plugins/`
|
|
242
|
+
|
|
243
|
+
It does **not** copy runtime/auth state such as `.claude.json`, `sessions/`, `history.jsonl`,
|
|
244
|
+
`projects/`, `session-env/`, `telemetry/`, or top-level profile cache directories.
|
|
245
|
+
|
|
246
|
+
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`).
|
|
247
|
+
|
|
248
|
+
You can also bootstrap at creation time:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Create profile and seed non-auth config from an existing profile
|
|
252
|
+
ccm create personal --from work
|
|
253
|
+
```
|
|
254
|
+
|
|
173
255
|
## How It Works
|
|
174
256
|
|
|
175
257
|
### Architecture Overview
|
|
@@ -198,9 +280,13 @@ src/
|
|
|
198
280
|
│ ├── config.ts # Config file I/O with atomic writes
|
|
199
281
|
│ ├── profiles.ts # Profile directory management and validation
|
|
200
282
|
│ ├── profile-store.ts # Reconciled config/filesystem profile view
|
|
283
|
+
│ ├── profile-config-copy.ts # Config copy planner + transactional apply/rollback
|
|
201
284
|
│ ├── claude.ts # Claude binary discovery, spawning, auth status
|
|
285
|
+
│ ├── compliance.ts # Centralized compliance notice text and official sources
|
|
202
286
|
│ └── browsers.ts # Browser wrapper generation and validation
|
|
203
287
|
└── commands/
|
|
288
|
+
├── copy-config.ts # Copy non-auth config between profiles (dry-run + rollback)
|
|
289
|
+
├── compliance.ts # Dedicated compliance/TOS command
|
|
204
290
|
├── create.ts # Create profile (with rollback on failure)
|
|
205
291
|
├── list.ts # List profiles with auth status and drift state
|
|
206
292
|
├── login.ts # Authenticate via Claude TUI or --console
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,404 @@
|
|
|
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
|
+
// src/lib/compliance.ts
|
|
11
|
+
var COMPLIANCE_CHECKED_AT = "2026-04-08";
|
|
12
|
+
var COMPLIANCE_SOURCES = [
|
|
13
|
+
{
|
|
14
|
+
label: "Claude Code Legal & Compliance",
|
|
15
|
+
url: "https://code.claude.com/docs/en/legal-and-compliance",
|
|
16
|
+
summary: "Pro and Max usage limits assume ordinary, individual use. Anthropic may enforce authentication restrictions without notice."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "Anthropic Consumer Terms",
|
|
20
|
+
url: "https://www.anthropic.com/legal/consumer-terms",
|
|
21
|
+
summary: "Account login information, API keys, and account credentials must not be shared or made available to anyone else."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Anthropic Commercial Terms",
|
|
25
|
+
url: "https://www.anthropic.com/legal/commercial-terms",
|
|
26
|
+
summary: "Commercial customers are responsible for all activity and fees incurred under their account."
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
function buildRuleLines() {
|
|
30
|
+
return [
|
|
31
|
+
"Project compliance boundary:",
|
|
32
|
+
"- ccm isolates profiles, but it does not grant any extra rights under Anthropic terms.",
|
|
33
|
+
"- Treat each Claude account as single-user and single-session: 1 account = 1 person = 1 active terminal session.",
|
|
34
|
+
"- Do not share account credentials or API keys, and do not hand off the same account between multiple operators.",
|
|
35
|
+
"- For parallel operators, separate seats/accounts or API-key-based access under Commercial Terms are required."
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function getCompactComplianceNoticeLines() {
|
|
39
|
+
return [
|
|
40
|
+
"\x1B[33m!\x1B[0m Compliance notice",
|
|
41
|
+
" ccm isolates profiles but does not expand Anthropic usage rights.",
|
|
42
|
+
" Use each Claude account with one person and one active terminal session.",
|
|
43
|
+
" Do not share credentials or API keys across users or parallel operators.",
|
|
44
|
+
" Details: ccm compliance"
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function getFullComplianceNoticeLines() {
|
|
48
|
+
const sourceLines = COMPLIANCE_SOURCES.flatMap((source) => [
|
|
49
|
+
`- ${source.label}: ${source.url}`,
|
|
50
|
+
` ${source.summary}`
|
|
51
|
+
]);
|
|
52
|
+
return [
|
|
53
|
+
"Claude Code compliance notice",
|
|
54
|
+
"This project publishes an intentionally conservative operational rule to reduce misuse and ambiguity.",
|
|
55
|
+
...buildRuleLines(),
|
|
56
|
+
"Important:",
|
|
57
|
+
"- This notice is compliance guidance for ccm users. It is not legal advice and it does not replace review of Anthropic terms for your specific use case.",
|
|
58
|
+
"- If you are building a product, automation, or shared workflow around Claude access, verify your model with Anthropic before deployment.",
|
|
59
|
+
`Official sources reviewed on ${COMPLIANCE_CHECKED_AT}:`,
|
|
60
|
+
...sourceLines
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/commands/compliance.ts
|
|
65
|
+
function registerCompliance(program2) {
|
|
66
|
+
program2.command("compliance").alias("tos").description("Show the Claude Code compliance notice and official source links").action(() => {
|
|
67
|
+
for (const line of getFullComplianceNoticeLines()) {
|
|
68
|
+
console.log(line);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
10
72
|
var CCM_HOME = join(homedir(), ".ccm");
|
|
11
73
|
var PROFILES_DIR = join(CCM_HOME, "profiles");
|
|
12
74
|
var CONFIG_FILE = join(CCM_HOME, "config.json");
|
|
13
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();
|
|
96
|
+
|
|
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
|
+
}
|
|
14
174
|
|
|
15
|
-
// src/lib/
|
|
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
|
+
}
|
|
16
402
|
var SHELL_METACHAR = /[;|&`$(){}<>\\!\n\r]/;
|
|
17
403
|
function validateBrowserCommand(cmd) {
|
|
18
404
|
if (SHELL_METACHAR.test(cmd)) {
|
|
@@ -35,7 +421,7 @@ exec ${browserCommand} "$@"
|
|
|
35
421
|
renameSync(tmp, scriptPath);
|
|
36
422
|
return scriptPath;
|
|
37
423
|
}
|
|
38
|
-
function
|
|
424
|
+
function getStagedPath3(path) {
|
|
39
425
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
40
426
|
const suffix = attempt === 0 ? `${process.pid}-${Date.now()}` : `${process.pid}-${Date.now()}-${attempt.toString()}`;
|
|
41
427
|
const candidate = `${path}.staged-${suffix}`;
|
|
@@ -68,7 +454,7 @@ function stageBrowserWrapperRemoval(profileName, browsersDir = BROWSERS_DIR) {
|
|
|
68
454
|
if (!existsSync(wrapperPath)) {
|
|
69
455
|
return void 0;
|
|
70
456
|
}
|
|
71
|
-
const stagedPath =
|
|
457
|
+
const stagedPath = getStagedPath3(wrapperPath);
|
|
72
458
|
renameSync(wrapperPath, stagedPath);
|
|
73
459
|
return stagedPath;
|
|
74
460
|
}
|
|
@@ -81,28 +467,6 @@ function finalizeStagedBrowserWrapperRemoval(stagedPath) {
|
|
|
81
467
|
function resolveBrowser(cliOverride, meta) {
|
|
82
468
|
return cliOverride ?? meta?.browser;
|
|
83
469
|
}
|
|
84
|
-
var MAX_PROFILE_NAME_LENGTH = 64;
|
|
85
|
-
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
86
|
-
var ProfileNameSchema = z.string().min(1).max(MAX_PROFILE_NAME_LENGTH).regex(PROFILE_NAME_PATTERN);
|
|
87
|
-
var ProfileMetaSchema = z.object({
|
|
88
|
-
name: ProfileNameSchema,
|
|
89
|
-
label: z.string().optional(),
|
|
90
|
-
browser: z.string().optional(),
|
|
91
|
-
createdAt: z.string()
|
|
92
|
-
}).passthrough();
|
|
93
|
-
var CcmConfigSchema = z.object({
|
|
94
|
-
profiles: z.record(z.string(), ProfileMetaSchema)
|
|
95
|
-
}).passthrough();
|
|
96
|
-
var ClaudeAuthStatusSchema = z.object({
|
|
97
|
-
loggedIn: z.boolean(),
|
|
98
|
-
authMethod: z.string(),
|
|
99
|
-
email: z.string().optional(),
|
|
100
|
-
orgName: z.string().optional(),
|
|
101
|
-
subscriptionType: z.string().optional(),
|
|
102
|
-
apiKeySource: z.string().optional()
|
|
103
|
-
}).passthrough();
|
|
104
|
-
|
|
105
|
-
// src/lib/config.ts
|
|
106
470
|
var DEFAULT_CONFIG = { profiles: {} };
|
|
107
471
|
function loadConfig(configFile = CONFIG_FILE) {
|
|
108
472
|
try {
|
|
@@ -154,97 +518,26 @@ function renameProfile(oldName, newName, configFile = CONFIG_FILE) {
|
|
|
154
518
|
function getProfile(name, configFile = CONFIG_FILE) {
|
|
155
519
|
return loadConfig(configFile).profiles[name];
|
|
156
520
|
}
|
|
157
|
-
function validateProfileName(name) {
|
|
158
|
-
const result = ProfileNameSchema.safeParse(name);
|
|
159
|
-
if (result.success) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (name.length > MAX_PROFILE_NAME_LENGTH) {
|
|
163
|
-
throw new Error(`Profile name too long (max ${MAX_PROFILE_NAME_LENGTH} chars).`);
|
|
164
|
-
}
|
|
165
|
-
throw new Error(
|
|
166
|
-
`Invalid profile name "${name}". Use only letters, numbers, hyphens, underscores.`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
function createProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
170
|
-
validateProfileName(name);
|
|
171
|
-
const dir = join(profilesDir, name);
|
|
172
|
-
if (existsSync(dir)) {
|
|
173
|
-
throw new Error(`Profile directory "${name}" already exists`);
|
|
174
|
-
}
|
|
175
|
-
mkdirSync(dir, { recursive: true });
|
|
176
|
-
return dir;
|
|
177
|
-
}
|
|
178
|
-
function renameProfileDir(oldName, newName, profilesDir = PROFILES_DIR) {
|
|
179
|
-
const oldDir = join(profilesDir, oldName);
|
|
180
|
-
const newDir = join(profilesDir, newName);
|
|
181
|
-
if (!existsSync(oldDir)) {
|
|
182
|
-
throw new Error(`Profile directory "${oldName}" does not exist`);
|
|
183
|
-
}
|
|
184
|
-
if (existsSync(newDir)) {
|
|
185
|
-
throw new Error(`Profile directory "${newName}" already exists`);
|
|
186
|
-
}
|
|
187
|
-
renameSync(oldDir, newDir);
|
|
188
|
-
}
|
|
189
|
-
function removeProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
190
|
-
const dir = join(profilesDir, name);
|
|
191
|
-
if (!existsSync(dir)) {
|
|
192
|
-
throw new Error(`Profile directory "${name}" does not exist`);
|
|
193
|
-
}
|
|
194
|
-
rmSync(dir, { recursive: true });
|
|
195
|
-
}
|
|
196
|
-
function getStagedPath2(path) {
|
|
197
|
-
const parentDir = dirname(path);
|
|
198
|
-
const baseName = basename(path);
|
|
199
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
200
|
-
const suffix = attempt === 0 ? `${process.pid}-${Date.now()}` : `${process.pid}-${Date.now()}-${attempt.toString()}`;
|
|
201
|
-
const candidate = join(parentDir, `.${baseName}.staged-${suffix}`);
|
|
202
|
-
if (!existsSync(candidate)) {
|
|
203
|
-
return candidate;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
throw new Error(`Could not allocate a temporary path for "${baseName}"`);
|
|
207
|
-
}
|
|
208
|
-
function stageProfileDirRemoval(name, profilesDir = PROFILES_DIR) {
|
|
209
|
-
const dir = join(profilesDir, name);
|
|
210
|
-
if (!existsSync(dir)) {
|
|
211
|
-
throw new Error(`Profile directory "${name}" does not exist`);
|
|
212
|
-
}
|
|
213
|
-
const stagedDir = getStagedPath2(dir);
|
|
214
|
-
renameSync(dir, stagedDir);
|
|
215
|
-
return stagedDir;
|
|
216
|
-
}
|
|
217
|
-
function restoreStagedProfileDir(stagedDir, name, profilesDir = PROFILES_DIR) {
|
|
218
|
-
renameSync(stagedDir, join(profilesDir, name));
|
|
219
|
-
}
|
|
220
|
-
function finalizeStagedProfileDirRemoval(stagedDir) {
|
|
221
|
-
rmSync(stagedDir, { recursive: true });
|
|
222
|
-
}
|
|
223
|
-
function getProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
224
|
-
return join(profilesDir, name);
|
|
225
|
-
}
|
|
226
|
-
function profileExists(name, profilesDir = PROFILES_DIR) {
|
|
227
|
-
return existsSync(join(profilesDir, name));
|
|
228
|
-
}
|
|
229
|
-
function listProfileDirs(profilesDir = PROFILES_DIR) {
|
|
230
|
-
if (!existsSync(profilesDir)) return [];
|
|
231
|
-
return readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
232
|
-
}
|
|
233
521
|
|
|
234
522
|
// src/commands/create.ts
|
|
235
523
|
function registerCreate(program2) {
|
|
236
|
-
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) => {
|
|
237
525
|
try {
|
|
238
526
|
createProfileDir(name);
|
|
239
|
-
|
|
527
|
+
let browser;
|
|
240
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
|
+
}
|
|
241
534
|
addProfile({
|
|
242
535
|
name,
|
|
243
536
|
label: opts.label,
|
|
244
537
|
browser,
|
|
245
538
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
246
539
|
});
|
|
247
|
-
} catch (
|
|
540
|
+
} catch (setupErr) {
|
|
248
541
|
try {
|
|
249
542
|
removeProfileDir(name);
|
|
250
543
|
} catch {
|
|
@@ -253,9 +546,12 @@ function registerCreate(program2) {
|
|
|
253
546
|
removeBrowserWrapper(name);
|
|
254
547
|
} catch {
|
|
255
548
|
}
|
|
256
|
-
throw
|
|
549
|
+
throw setupErr;
|
|
257
550
|
}
|
|
258
551
|
console.log(`\x1B[32m\u2713\x1B[0m Profile "${name}" created`);
|
|
552
|
+
for (const line of getCompactComplianceNoticeLines()) {
|
|
553
|
+
console.log(line);
|
|
554
|
+
}
|
|
259
555
|
console.log(` Next: ccm login ${name}`);
|
|
260
556
|
} catch (e) {
|
|
261
557
|
console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -374,7 +670,7 @@ function removeStoredProfile(name, configFile = CONFIG_FILE, profilesDir = PROFI
|
|
|
374
670
|
rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDir);
|
|
375
671
|
} catch (rollbackError) {
|
|
376
672
|
throw new Error(
|
|
377
|
-
`Failed to remove profile "${name}": ${
|
|
673
|
+
`Failed to remove profile "${name}": ${formatError2(error)}. Rollback failed: ${formatError2(
|
|
378
674
|
rollbackError
|
|
379
675
|
)}`
|
|
380
676
|
);
|
|
@@ -393,18 +689,18 @@ function removeStoredProfile(name, configFile = CONFIG_FILE, profilesDir = PROFI
|
|
|
393
689
|
try {
|
|
394
690
|
rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDir);
|
|
395
691
|
} catch (rollbackError) {
|
|
396
|
-
recoveryErrors.push(
|
|
692
|
+
recoveryErrors.push(formatError2(rollbackError));
|
|
397
693
|
}
|
|
398
694
|
if (profile.meta) {
|
|
399
695
|
try {
|
|
400
696
|
restoreConfigEntry(profile.meta, configFile);
|
|
401
697
|
} catch (configRestoreError) {
|
|
402
|
-
recoveryErrors.push(`config: ${
|
|
698
|
+
recoveryErrors.push(`config: ${formatError2(configRestoreError)}`);
|
|
403
699
|
}
|
|
404
700
|
}
|
|
405
701
|
if (recoveryErrors.length > 0) {
|
|
406
702
|
throw new Error(
|
|
407
|
-
`Failed to remove profile "${name}": ${
|
|
703
|
+
`Failed to remove profile "${name}": ${formatError2(error)}. Recovery failed: ${recoveryErrors.join(
|
|
408
704
|
"; "
|
|
409
705
|
)}`
|
|
410
706
|
);
|
|
@@ -439,7 +735,7 @@ function renameStoredProfile(oldName, newName, configFile = CONFIG_FILE, profile
|
|
|
439
735
|
rollbackRename(oldName, newName, profile.hasDirectory, false, profilesDir, browsersDir);
|
|
440
736
|
} catch (rollbackError) {
|
|
441
737
|
throw new Error(
|
|
442
|
-
`Failed to rename profile "${oldName}" to "${newName}": ${
|
|
738
|
+
`Failed to rename profile "${oldName}" to "${newName}": ${formatError2(error)}. Rollback failed: ${formatError2(rollbackError)}`
|
|
443
739
|
);
|
|
444
740
|
}
|
|
445
741
|
throw error;
|
|
@@ -452,7 +748,7 @@ function renameStoredProfile(oldName, newName, configFile = CONFIG_FILE, profile
|
|
|
452
748
|
rollbackRename(oldName, newName, profile.hasDirectory, hadWrapper, profilesDir, browsersDir);
|
|
453
749
|
} catch (rollbackError) {
|
|
454
750
|
throw new Error(
|
|
455
|
-
`Failed to rename profile "${oldName}" to "${newName}": ${
|
|
751
|
+
`Failed to rename profile "${oldName}" to "${newName}": ${formatError2(error)}. Rollback failed: ${formatError2(rollbackError)}`
|
|
456
752
|
);
|
|
457
753
|
}
|
|
458
754
|
throw error;
|
|
@@ -464,14 +760,14 @@ function rollbackRename(oldName, newName, hadDirectory, hadWrapper, profilesDir,
|
|
|
464
760
|
try {
|
|
465
761
|
renameBrowserWrapper(newName, oldName, browsersDir);
|
|
466
762
|
} catch (error) {
|
|
467
|
-
rollbackErrors.push(`browser wrapper: ${
|
|
763
|
+
rollbackErrors.push(`browser wrapper: ${formatError2(error)}`);
|
|
468
764
|
}
|
|
469
765
|
}
|
|
470
766
|
if (hadDirectory) {
|
|
471
767
|
try {
|
|
472
768
|
renameProfileDir(newName, oldName, profilesDir);
|
|
473
769
|
} catch (error) {
|
|
474
|
-
rollbackErrors.push(`profile dir: ${
|
|
770
|
+
rollbackErrors.push(`profile dir: ${formatError2(error)}`);
|
|
475
771
|
}
|
|
476
772
|
}
|
|
477
773
|
if (rollbackErrors.length > 0) {
|
|
@@ -486,14 +782,14 @@ function rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDi
|
|
|
486
782
|
try {
|
|
487
783
|
restoreStagedProfileDir(stagedDir, name, profilesDir);
|
|
488
784
|
} catch (error) {
|
|
489
|
-
rollbackErrors.push(`profile dir: ${
|
|
785
|
+
rollbackErrors.push(`profile dir: ${formatError2(error)}`);
|
|
490
786
|
}
|
|
491
787
|
}
|
|
492
788
|
if (stagedWrapper) {
|
|
493
789
|
try {
|
|
494
790
|
restoreStagedBrowserWrapper(stagedWrapper, name, browsersDir);
|
|
495
791
|
} catch (error) {
|
|
496
|
-
rollbackErrors.push(`browser wrapper: ${
|
|
792
|
+
rollbackErrors.push(`browser wrapper: ${formatError2(error)}`);
|
|
497
793
|
}
|
|
498
794
|
}
|
|
499
795
|
if (rollbackErrors.length > 0) {
|
|
@@ -506,7 +802,7 @@ function restoreConfigEntry(meta, configFile) {
|
|
|
506
802
|
}
|
|
507
803
|
addProfile(meta, configFile);
|
|
508
804
|
}
|
|
509
|
-
function
|
|
805
|
+
function formatError2(error) {
|
|
510
806
|
return error instanceof Error ? error.message : String(error);
|
|
511
807
|
}
|
|
512
808
|
|
|
@@ -571,7 +867,7 @@ function registerLogin(program2) {
|
|
|
571
867
|
}
|
|
572
868
|
});
|
|
573
869
|
}
|
|
574
|
-
function
|
|
870
|
+
function confirm2(question) {
|
|
575
871
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
576
872
|
return new Promise((resolve) => {
|
|
577
873
|
rl.question(question, (answer) => {
|
|
@@ -588,7 +884,7 @@ function registerRemove(program2) {
|
|
|
588
884
|
throw new Error(`Profile "${name}" does not exist`);
|
|
589
885
|
}
|
|
590
886
|
if (!opts.force) {
|
|
591
|
-
const ok = await
|
|
887
|
+
const ok = await confirm2(`Remove profile "${name}"? (y/N) `);
|
|
592
888
|
if (!ok) {
|
|
593
889
|
console.log("Cancelled");
|
|
594
890
|
return;
|
|
@@ -701,8 +997,10 @@ function registerUse(program2) {
|
|
|
701
997
|
}
|
|
702
998
|
|
|
703
999
|
// src/index.ts
|
|
704
|
-
var program = new Command().name("ccm").version("0.
|
|
1000
|
+
var program = new Command().name("ccm").version("0.5.0").description("Manage multiple Claude Code profiles");
|
|
705
1001
|
registerCreate(program);
|
|
1002
|
+
registerCopyConfig(program);
|
|
1003
|
+
registerCompliance(program);
|
|
706
1004
|
registerList(program);
|
|
707
1005
|
registerRemove(program);
|
|
708
1006
|
registerRename(program);
|