@remeic/ccm 0.2.2 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +47 -1
  2. package/dist/index.js +180 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,13 +13,30 @@
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
  [![CI](https://github.com/remeic/ccm/actions/workflows/ci.yml/badge.svg)](https://github.com/remeic/ccm/actions/workflows/ci.yml)
17
34
  [![npm version](https://img.shields.io/npm/v/%40remeic%2Fccm.svg)](https://www.npmjs.com/package/@remeic/ccm)
18
35
  [![Homebrew tap](https://img.shields.io/badge/Homebrew-tap-FBB040?logo=homebrew&logoColor=white)](https://github.com/Remeic/homebrew-tap)
19
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
20
37
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
21
38
  [![codecov](https://codecov.io/github/Remeic/ccm/graph/badge.svg?token=E16LCLDHYV)](https://codecov.io/github/Remeic/ccm)
22
- [![mutation testing](https://img.shields.io/badge/mutation%20testing-100%25-brightgreen)](https://stryker-mutator.io/)
39
+ [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FRemeic%2Fccm%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/Remeic/ccm/main)
23
40
 
24
41
  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
42
 
@@ -30,11 +47,13 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
30
47
  ## Table of Contents
31
48
 
32
49
  - [Table of Contents](#table-of-contents)
50
+ - [IMPORTANT: Claude Code ToS (Multi-Terminal Use)](#important-claude-code-tos-multi-terminal-use)
33
51
  - [Why](#why)
34
52
  - [Prerequisites](#prerequisites)
35
53
  - [Install](#install)
36
54
  - [Quick Start](#quick-start)
37
55
  - [Commands](#commands)
56
+ - [Compliance Notice](#compliance-notice)
38
57
  - [Passing Flags and Environment Variables](#passing-flags-and-environment-variables)
39
58
  - [Multi-Account Login](#multi-account-login)
40
59
  - [Different Browser per Profile](#different-browser-per-profile)
@@ -91,6 +110,11 @@ The installed command remains `ccm`.
91
110
  ```
92
111
  $ ccm create work
93
112
  ✓ Profile "work" created
113
+ ! Compliance notice
114
+ ccm isolates profiles but does not expand Anthropic usage rights.
115
+ Use each Claude account with one person and one active terminal session.
116
+ Do not share credentials or API keys across users or parallel operators.
117
+ Details: ccm compliance
94
118
  Next: ccm login work
95
119
 
96
120
  $ ccm login work
@@ -105,13 +129,32 @@ $ ccm use work
105
129
  | Command | Description |
106
130
  | -------------------------------------------------------- | ------------------------------------------------------------- |
107
131
  | `ccm create <name> [-l label] [-b browser]` | Create a profile. `-b` sets the browser for OAuth |
132
+ | `ccm compliance` / `ccm tos` | Show the compliance notice, disclaimer, and official sources |
108
133
  | `ccm list` | List all profiles with auth status, including drifted entries |
109
134
  | `ccm use <name> [-- args]` | Launch Claude Code. Args after `--` are passed to Claude |
110
135
  | `ccm login <name> [--console] [-b browser] [--url-only]` | Authenticate a profile |
111
136
  | `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) |
112
138
  | `ccm remove <name> [-f]` | Remove a profile. `-f` skips confirmation |
113
139
  | `ccm run <name> -p <prompt>` | Run a prompt non-interactively |
114
140
 
141
+ ## Compliance Notice
142
+
143
+ Every successful `ccm create` prints a short compliance warning. Users can re-open the full text at any time with:
144
+
145
+ ```bash
146
+ ccm compliance
147
+ ccm tos
148
+ ```
149
+
150
+ The notice is intentionally conservative:
151
+
152
+ - `ccm` is a profile-isolation tool, not a license-expansion tool.
153
+ - This project treats each Claude account as single-user and single-session.
154
+ - Shared credentials, shared operators, and parallel use of the same account are out of scope.
155
+ - If a team needs parallel access, they should use separate seats/accounts or API-key-based access under applicable Anthropic commercial terms.
156
+ - The notice is compliance guidance, not legal advice. Users remain responsible for reviewing Anthropic terms for their exact workflow.
157
+
115
158
  ## Passing Flags and Environment Variables
116
159
 
117
160
  Everything after `--` in `ccm use` is forwarded to `claude`. Env vars from your shell are inherited.
@@ -198,13 +241,16 @@ src/
198
241
  │ ├── profiles.ts # Profile directory management and validation
199
242
  │ ├── profile-store.ts # Reconciled config/filesystem profile view
200
243
  │ ├── claude.ts # Claude binary discovery, spawning, auth status
244
+ │ ├── compliance.ts # Centralized compliance notice text and official sources
201
245
  │ └── browsers.ts # Browser wrapper generation and validation
202
246
  └── commands/
247
+ ├── compliance.ts # Dedicated compliance/TOS command
203
248
  ├── create.ts # Create profile (with rollback on failure)
204
249
  ├── list.ts # List profiles with auth status and drift state
205
250
  ├── login.ts # Authenticate via Claude TUI or --console
206
251
  ├── use.ts # Launch Claude with profile config dir
207
252
  ├── status.ts # Show auth status and drift state
253
+ ├── rename.ts # Rename profile with atomic rollback
208
254
  ├── remove.ts # Remove profile with staged rollback
209
255
  └── run.ts # Run prompt with specific profile
210
256
  ```
package/dist/index.js CHANGED
@@ -7,6 +7,68 @@ import { z } from 'zod';
7
7
  import { spawn, execFileSync } from 'child_process';
8
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");
@@ -48,6 +110,14 @@ function getStagedPath(path) {
48
110
  function getBrowserWrapperPath(profileName, browsersDir = BROWSERS_DIR) {
49
111
  return join(browsersDir, `${profileName}.sh`);
50
112
  }
113
+ function renameBrowserWrapper(oldName, newName, browsersDir = BROWSERS_DIR) {
114
+ const oldPath = getBrowserWrapperPath(oldName, browsersDir);
115
+ if (!existsSync(oldPath)) {
116
+ return;
117
+ }
118
+ const newPath = getBrowserWrapperPath(newName, browsersDir);
119
+ renameSync(oldPath, newPath);
120
+ }
51
121
  function removeBrowserWrapper(profileName, browsersDir = BROWSERS_DIR) {
52
122
  const wrapperPath = getBrowserWrapperPath(profileName, browsersDir);
53
123
  if (!existsSync(wrapperPath)) {
@@ -131,6 +201,18 @@ function removeProfile(name, configFile = CONFIG_FILE) {
131
201
  delete config.profiles[name];
132
202
  saveConfig(config, configFile);
133
203
  }
204
+ function renameProfile(oldName, newName, configFile = CONFIG_FILE) {
205
+ const config = loadConfig(configFile);
206
+ if (!config.profiles[oldName]) {
207
+ throw new Error(`Profile "${oldName}" not found`);
208
+ }
209
+ if (config.profiles[newName]) {
210
+ throw new Error(`Profile "${newName}" already exists`);
211
+ }
212
+ config.profiles[newName] = { ...config.profiles[oldName], name: newName };
213
+ delete config.profiles[oldName];
214
+ saveConfig(config, configFile);
215
+ }
134
216
  function getProfile(name, configFile = CONFIG_FILE) {
135
217
  return loadConfig(configFile).profiles[name];
136
218
  }
@@ -155,6 +237,17 @@ function createProfileDir(name, profilesDir = PROFILES_DIR) {
155
237
  mkdirSync(dir, { recursive: true });
156
238
  return dir;
157
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
+ }
158
251
  function removeProfileDir(name, profilesDir = PROFILES_DIR) {
159
252
  const dir = join(profilesDir, name);
160
253
  if (!existsSync(dir)) {
@@ -225,6 +318,9 @@ function registerCreate(program2) {
225
318
  throw configErr;
226
319
  }
227
320
  console.log(`\x1B[32m\u2713\x1B[0m Profile "${name}" created`);
321
+ for (const line of getCompactComplianceNoticeLines()) {
322
+ console.log(line);
323
+ }
228
324
  console.log(` Next: ccm login ${name}`);
229
325
  } catch (e) {
230
326
  console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
@@ -288,8 +384,6 @@ function getAuthStatus(profileDir) {
288
384
  });
289
385
  });
290
386
  }
291
-
292
- // src/lib/profile-store.ts
293
387
  function getProfileState(hasConfig, hasDirectory) {
294
388
  if (hasConfig && hasDirectory) return "ready";
295
389
  if (hasDirectory) return "orphaned";
@@ -383,6 +477,74 @@ function removeStoredProfile(name, configFile = CONFIG_FILE, profilesDir = PROFI
383
477
  throw error;
384
478
  }
385
479
  }
480
+ function renameStoredProfile(oldName, newName, configFile = CONFIG_FILE, profilesDir = PROFILES_DIR, browsersDir = BROWSERS_DIR) {
481
+ validateProfileName(newName);
482
+ if (oldName === newName) {
483
+ throw new Error("Old and new profile names are the same");
484
+ }
485
+ const profile = getStoredProfile(oldName, configFile, profilesDir);
486
+ if (!profile) {
487
+ throw new Error(`Profile "${oldName}" does not exist`);
488
+ }
489
+ if (getProfile(newName, configFile)) {
490
+ throw new Error(`Profile "${newName}" already exists`);
491
+ }
492
+ if (profileExists(newName, profilesDir)) {
493
+ throw new Error(`Profile directory "${newName}" already exists`);
494
+ }
495
+ if (profile.hasDirectory) {
496
+ renameProfileDir(oldName, newName, profilesDir);
497
+ }
498
+ const hadWrapper = existsSync(getBrowserWrapperPath(oldName, browsersDir));
499
+ if (hadWrapper) {
500
+ try {
501
+ renameBrowserWrapper(oldName, newName, browsersDir);
502
+ } catch (error) {
503
+ try {
504
+ rollbackRename(oldName, newName, profile.hasDirectory, false, profilesDir, browsersDir);
505
+ } catch (rollbackError) {
506
+ throw new Error(
507
+ `Failed to rename profile "${oldName}" to "${newName}": ${formatError(error)}. Rollback failed: ${formatError(rollbackError)}`
508
+ );
509
+ }
510
+ throw error;
511
+ }
512
+ }
513
+ try {
514
+ renameProfile(oldName, newName, configFile);
515
+ } catch (error) {
516
+ try {
517
+ rollbackRename(oldName, newName, profile.hasDirectory, hadWrapper, profilesDir, browsersDir);
518
+ } catch (rollbackError) {
519
+ throw new Error(
520
+ `Failed to rename profile "${oldName}" to "${newName}": ${formatError(error)}. Rollback failed: ${formatError(rollbackError)}`
521
+ );
522
+ }
523
+ throw error;
524
+ }
525
+ }
526
+ function rollbackRename(oldName, newName, hadDirectory, hadWrapper, profilesDir, browsersDir) {
527
+ const rollbackErrors = [];
528
+ if (hadWrapper) {
529
+ try {
530
+ renameBrowserWrapper(newName, oldName, browsersDir);
531
+ } catch (error) {
532
+ rollbackErrors.push(`browser wrapper: ${formatError(error)}`);
533
+ }
534
+ }
535
+ if (hadDirectory) {
536
+ try {
537
+ renameProfileDir(newName, oldName, profilesDir);
538
+ } catch (error) {
539
+ rollbackErrors.push(`profile dir: ${formatError(error)}`);
540
+ }
541
+ }
542
+ if (rollbackErrors.length > 0) {
543
+ throw new Error(
544
+ `Rollback failed for rename "${oldName}" to "${newName}": ${rollbackErrors.join("; ")}`
545
+ );
546
+ }
547
+ }
386
548
  function rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDir) {
387
549
  const rollbackErrors = [];
388
550
  if (stagedDir) {
@@ -507,6 +669,19 @@ function registerRemove(program2) {
507
669
  });
508
670
  }
509
671
 
672
+ // src/commands/rename.ts
673
+ function registerRename(program2) {
674
+ program2.command("rename <old-name> <new-name>").description("Rename a profile").action((oldName, newName) => {
675
+ try {
676
+ renameStoredProfile(oldName, newName);
677
+ console.log(`\x1B[32m\u2713\x1B[0m Profile "${oldName}" renamed to "${newName}"`);
678
+ } catch (e) {
679
+ console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
680
+ process.exit(1);
681
+ }
682
+ });
683
+ }
684
+
510
685
  // src/commands/run.ts
511
686
  function registerRun(program2) {
512
687
  program2.command("run <name>").description("Run Claude Code with a prompt using a profile").requiredOption("-p, --prompt <prompt>", "Prompt to send").action((name, opts) => {
@@ -591,10 +766,12 @@ function registerUse(program2) {
591
766
  }
592
767
 
593
768
  // src/index.ts
594
- var program = new Command().name("ccm").version("0.2.2").description("Manage multiple Claude Code profiles");
769
+ var program = new Command().name("ccm").version("0.4.0").description("Manage multiple Claude Code profiles");
595
770
  registerCreate(program);
771
+ registerCompliance(program);
596
772
  registerList(program);
597
773
  registerRemove(program);
774
+ registerRename(program);
598
775
  registerLogin(program);
599
776
  registerStatus(program);
600
777
  registerUse(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remeic/ccm",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "nvm-like manager for Claude Code profiles",
5
5
  "license": "MIT",
6
6
  "author": "Giulio Fagioli",