ai-profile-switch 0.0.0-stage → 2.2.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/LICENSE +21 -0
- package/README.ko.md +92 -0
- package/README.md +91 -2
- package/bin/cli.mjs +22 -0
- package/package.json +41 -4
- package/src/commands/add.mjs +32 -0
- package/src/commands/init.mjs +108 -0
- package/src/commands/install-shell.mjs +14 -0
- package/src/commands/language.mjs +38 -0
- package/src/commands/list.mjs +35 -0
- package/src/commands/login.mjs +43 -0
- package/src/commands/manage.mjs +152 -0
- package/src/commands/mcp/index.mjs +28 -0
- package/src/commands/migrate.mjs +57 -0
- package/src/commands/remove.mjs +34 -0
- package/src/commands/rename.mjs +19 -0
- package/src/commands/share.mjs +60 -0
- package/src/commands/shell.mjs +220 -0
- package/src/commands/update.mjs +53 -0
- package/src/commands/use.mjs +16 -0
- package/src/index.mjs +92 -0
- package/src/lib/account.mjs +47 -0
- package/src/lib/config.mjs +78 -0
- package/src/lib/constants.mjs +44 -0
- package/src/lib/i18n.mjs +65 -0
- package/src/lib/json-fs.mjs +30 -0
- package/src/lib/profile.mjs +406 -0
- package/src/lib/prompt.mjs +187 -0
- package/src/lib/self-update.mjs +235 -0
- package/src/lib/shared.mjs +157 -0
- package/src/lib/shell.mjs +228 -0
- package/src/lib/ui.mjs +114 -0
- package/src/lib/updater.mjs +55 -0
- package/src/locales/en.mjs +265 -0
- package/src/locales/ko.mjs +265 -0
- package/src/shell-templates/integration.fish +32 -0
- package/src/shell-templates/integration.ps1 +78 -0
- package/src/shell-templates/integration.sh +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 claude-switch contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ai-profile-switch
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ai-profile-switch)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
회사·개인처럼 여러 Claude Code 계정을 한 컴퓨터에 두고, `claude` 를 실행할 때마다 골라 씁니다. 프로필마다 로그인·기록·설정이 따로 있고, 같이 쓸 것만 골라서 나눕니다. 실행에 필요한 외부 패키지는 없습니다.
|
|
7
|
+
|
|
8
|
+
[English](./README.md) · [전체 가이드](https://ftery0.github.io/ai-profile-switch/ko/guide/)
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
예전 이름은 `claude-account-switch` 입니다. [기존 사용자](#기존-사용자)를 참고하세요.
|
|
13
|
+
|
|
14
|
+
## 설치
|
|
15
|
+
|
|
16
|
+
Node.js 18.19+ 또는 20.10+ 와 [Claude Code](https://code.claude.com/docs/en/setup) 가 필요합니다.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx ai-profile-switch@latest init
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`work, personal` 처럼 프로필 이름을 쉼표로 적고, 새 터미널을 엽니다.
|
|
23
|
+
|
|
24
|
+
## 평소에 쓰는 명령
|
|
25
|
+
|
|
26
|
+
터미널 프롬프트에서 실행합니다. Claude 채팅 안이라면 먼저 `/exit` 로 나옵니다.
|
|
27
|
+
|
|
28
|
+
| 명령 | 하는 일 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `claude` | 계정을 골라 그 계정으로 Claude Code 실행 |
|
|
31
|
+
| `cpf` | 메뉴에서 프로필 바꾸기·추가. **프로필 관리하기**에서 프로필마다 로그인 계정·사용량을 보고 이름 바꾸기·다시 로그인·지우기 |
|
|
32
|
+
| `cpf work` | Claude 를 켜지 않고 `work` 로 전환 |
|
|
33
|
+
| `cpf --list` | 프로필 목록과 지금 쓰는 프로필 |
|
|
34
|
+
| `cpf add work2` | 프로필 만들기. 그다음 `claude` 에서 골라 로그인 |
|
|
35
|
+
| `cpf rename wrok2 work2` | 이름 바꾸기. 그 프로필의 Claude 세션을 먼저 닫음 |
|
|
36
|
+
| `cpf remove work2` | 확인 후 프로필 삭제 |
|
|
37
|
+
| `cpf login work2` | `work2` 만 다른 Claude 계정으로 다시 로그인. 다른 프로필은 그대로 |
|
|
38
|
+
| `cpf language ko` | 화면을 한국어로. `en` 은 영어, `auto` 는 시스템 설정을 따름 |
|
|
39
|
+
|
|
40
|
+
- `claude` 선택 화면에는 프로필마다 이메일·요금제와, Claude Code 가 마지막으로 저장한 5시간·주간 사용량이 나옵니다. 숫자 키로 바로 고르고 Esc 로 취소합니다.
|
|
41
|
+
- 화면 언어는 터미널 언어(`LANG`)나 시스템 언어를 따라 한국어·영어 중에서 정해집니다.
|
|
42
|
+
- 전환은 다음 실행부터 적용됩니다. 이미 켜진 세션은 원래 계정을 그대로 씁니다.
|
|
43
|
+
- 스크립트에서는 `claude` 가 묻지 않고 지금 프로필로 실행합니다.
|
|
44
|
+
- Bash·zsh 프롬프트에서 `command claude` 를 치면 프로필 없이 원래 Claude 가 실행됩니다.
|
|
45
|
+
|
|
46
|
+
## 프로필끼리 설정 나누기
|
|
47
|
+
|
|
48
|
+
처음에는 `settings.json` 과 `commands` 만 같이 씁니다.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cpf share # 무엇을 나누는지, 프로필마다 맞춰져 있는지
|
|
52
|
+
cpf share add CLAUDE.md # CLAUDE.md 하나를 모든 프로필에서
|
|
53
|
+
cpf share add skills # 개인 스킬 나누기
|
|
54
|
+
cpf share remove skills # 나누기 그만. 프로필마다 복사본이 남음
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- 나눌 수 있는 것: `CLAUDE.md` `keybindings.json` `skills` `agents` `rules` `hooks` `output-styles` `workflows` `themes`
|
|
58
|
+
- 항상 프로필마다 따로: 로그인, `.claude.json`(MCP 서버·프로젝트 신뢰), 기록, `projects`, 플러그인
|
|
59
|
+
- 내용이 다른 프로필 파일은 덮어쓰지 않고 경고만 합니다.
|
|
60
|
+
|
|
61
|
+
## 원래 쓰던 계정 가져오기
|
|
62
|
+
|
|
63
|
+
처음 `init` 할 때 지금 쓰는 `~/.claude` 를 프로필로 복사할 수 있습니다. 나중에 하려면 Claude 세션을 닫고 실행합니다.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx ai-profile-switch@latest migrate work --from ~/.claude
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
원래 폴더는 그대로 남습니다. macOS 키체인의 로그인 정보는 복사되지 않아서, 처음 한 번 다시 로그인하라고 할 수 있습니다.
|
|
70
|
+
|
|
71
|
+
## 업데이트
|
|
72
|
+
|
|
73
|
+
셸이 하루에 한 번 업데이트를 확인하고, 다음 실행부터 새 버전을 씁니다. 켜져 있는 세션에는 영향이 없습니다.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx ai-profile-switch@latest update # 바로 업데이트. --check 는 확인만
|
|
77
|
+
npx ai-profile-switch@latest install-shell # 셸 명령 고치기·새로 깔기
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
자동 업데이트를 끄려면 `CLAUDE_SWITCH_DISABLE_AUTO_UPDATE=1` 을 설정합니다.
|
|
81
|
+
|
|
82
|
+
## 기존 사용자
|
|
83
|
+
|
|
84
|
+
`claude-account-switch` 나 1.x 를 쓰셨다면 `npx ai-profile-switch@latest install-shell` 을 한 번 실행하고 새 터미널을 엽니다. 프로필과 로그인은 그대로이고, `claude-account-switch` 명령도 별칭으로 남습니다.
|
|
85
|
+
|
|
86
|
+
## 더 보기
|
|
87
|
+
|
|
88
|
+
- [명령어 전체와 가져오기 자세히](https://ftery0.github.io/ai-profile-switch/ko/guide/commands)
|
|
89
|
+
- 플랫폼별 안내: [macOS](docs/setup-macos.ko.md) · [Linux](docs/setup-linux.ko.md) · [Windows](docs/setup-windows.ko.md)
|
|
90
|
+
- 이 소스를 직접 쓰려면 레포 폴더에서 `node bin/cli.mjs install-shell` 을 실행합니다.
|
|
91
|
+
|
|
92
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ai-profile-switch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ai-profile-switch)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Keep several Claude Code accounts on one machine, such as work and personal, and pick one each time you run `claude`. Each profile has its own sign-in, history, and settings. You share only what you choose. No runtime dependencies.
|
|
7
|
+
|
|
8
|
+
[한국어](./README.ko.md) · [Full guide](https://ftery0.github.io/ai-profile-switch/guide/)
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
Formerly `claude-account-switch`. See [existing users](#existing-users).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
You need Node.js 18.19+ or 20.10+ and [Claude Code](https://code.claude.com/docs/en/setup).
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx ai-profile-switch@latest init
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Type profile names such as `work, personal`, then open a new terminal.
|
|
23
|
+
|
|
24
|
+
## Everyday use
|
|
25
|
+
|
|
26
|
+
Run these at the terminal prompt. If you are in the Claude chat, enter `/exit` first.
|
|
27
|
+
|
|
28
|
+
| Command | What it does |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `claude` | Pick an account and start Claude Code with it |
|
|
31
|
+
| `cpf` | Open a menu to switch or add profiles. **Manage profiles** shows each one's sign-in and usage, and renames, re-signs, or deletes it |
|
|
32
|
+
| `cpf work` | Make `work` active without starting Claude |
|
|
33
|
+
| `cpf --list` | Show profiles and the active one |
|
|
34
|
+
| `cpf add work2` | Create a profile. Then run `claude`, pick it, and sign in |
|
|
35
|
+
| `cpf rename wrok2 work2` | Rename a profile. Close its Claude sessions first |
|
|
36
|
+
| `cpf remove work2` | Delete a profile after confirmation |
|
|
37
|
+
| `cpf login work2` | Sign `work2` in to another Claude account. Other profiles keep theirs |
|
|
38
|
+
| `cpf language ko` | Show screens in Korean. `en` for English, `auto` to follow the system |
|
|
39
|
+
|
|
40
|
+
- The `claude` picker shows each profile's email, plan, and the 5-hour and weekly usage Claude Code last saved. Press a number to pick at once, or Esc to cancel.
|
|
41
|
+
- Screens appear in Korean or English, following your terminal language (`LANG`) or system language.
|
|
42
|
+
- Switching applies to the next launch. Running sessions keep their account.
|
|
43
|
+
- In scripts, `claude` uses the active profile without asking.
|
|
44
|
+
- At a Bash or zsh prompt, `command claude` runs the original Claude without a profile.
|
|
45
|
+
|
|
46
|
+
## Share settings across profiles
|
|
47
|
+
|
|
48
|
+
Only `settings.json` and `commands` are shared at first.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cpf share # What is shared, and whether each profile is in sync
|
|
52
|
+
cpf share add CLAUDE.md # Use one CLAUDE.md in every profile
|
|
53
|
+
cpf share add skills # Share personal skills
|
|
54
|
+
cpf share remove skills # Stop sharing. Each profile keeps a copy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Shareable: `CLAUDE.md` `keybindings.json` `skills` `agents` `rules` `hooks` `output-styles` `workflows` `themes`
|
|
58
|
+
- Always per profile: sign-in, `.claude.json` (MCP servers, project trust), history, `projects`, plugins
|
|
59
|
+
- A profile file with different content is never overwritten. You get a warning instead.
|
|
60
|
+
|
|
61
|
+
## Bring in your existing account
|
|
62
|
+
|
|
63
|
+
On first setup, `init` can copy your current `~/.claude` into a profile. To do it later, close your Claude sessions and run:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx ai-profile-switch@latest migrate work --from ~/.claude
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The original folder stays as it is. macOS Keychain sign-ins are not copied, so Claude may ask you to sign in once.
|
|
70
|
+
|
|
71
|
+
## Updates
|
|
72
|
+
|
|
73
|
+
The shell checks for updates once a day and uses the new version from the next launch. Running sessions are not affected.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx ai-profile-switch@latest update # Update now. Add --check to only check
|
|
77
|
+
npx ai-profile-switch@latest install-shell # Repair or refresh the shell commands
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Set `CLAUDE_SWITCH_DISABLE_AUTO_UPDATE=1` to turn automatic updates off.
|
|
81
|
+
|
|
82
|
+
## Existing users
|
|
83
|
+
|
|
84
|
+
Coming from `claude-account-switch` or 1.x? Run `npx ai-profile-switch@latest install-shell` once, then open a new terminal. Your profiles and sign-ins stay, and `claude-account-switch` remains an alias.
|
|
85
|
+
|
|
86
|
+
## More
|
|
87
|
+
|
|
88
|
+
- [Command reference and migration details](https://ftery0.github.io/ai-profile-switch/guide/commands)
|
|
89
|
+
- Platform guides: [macOS](docs/setup-macos.md) · [Linux](docs/setup-linux.md) · [Windows](docs/setup-windows.md)
|
|
90
|
+
- To use this source checkout, run `node bin/cli.mjs install-shell` from the repository root.
|
|
91
|
+
|
|
92
|
+
[MIT](./LICENSE)
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const profiles = join(homedir(), '.claude-profiles');
|
|
9
|
+
const installedEntry = join(profiles, '_runtime', 'bin', 'cli.mjs');
|
|
10
|
+
const pointer = join(profiles, '_runtime-current.json');
|
|
11
|
+
let entry = join(dirname(fileURLToPath(import.meta.url)), '../src/index.mjs');
|
|
12
|
+
if (existsSync(installedEntry) && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(installedEntry) && existsSync(pointer)) {
|
|
13
|
+
if (lstatSync(pointer).isSymbolicLink()) throw new Error('Runtime pointer must not be a symlink');
|
|
14
|
+
const { directory } = JSON.parse(readFileSync(pointer, 'utf8'));
|
|
15
|
+
if (typeof directory !== 'string' || !/^v[\w.-]+$/.test(directory)) throw new Error('Invalid runtime pointer');
|
|
16
|
+
const versions = join(profiles, '_runtime-updates');
|
|
17
|
+
const runtime = join(versions, directory);
|
|
18
|
+
if (lstatSync(versions).isSymbolicLink() || lstatSync(runtime).isSymbolicLink()) throw new Error('Runtime must not be a symlink');
|
|
19
|
+
entry = join(runtime, 'src/index.mjs');
|
|
20
|
+
}
|
|
21
|
+
const { run } = await import(pathToFileURL(entry).href);
|
|
22
|
+
run(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-profile-switch",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "Profile switching for AI coding CLIs, starting with Claude Code (formerly claude-account-switch)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-profile-switch": "bin/cli.mjs",
|
|
8
|
+
"claude-account-switch": "bin/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": "^18.19.0 || >=20.10.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"docs:dev": "vitepress dev website",
|
|
20
|
+
"docs:build": "vitepress build website",
|
|
21
|
+
"docs:preview": "vitepress preview website"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai",
|
|
25
|
+
"claude",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"multi-account",
|
|
28
|
+
"profile-manager",
|
|
29
|
+
"cli"
|
|
30
|
+
],
|
|
31
|
+
"author": "haejun <haejun7388@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/ftery0/ai-profile-switch.git"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"vitepress": "^1.6.4"
|
|
39
|
+
},
|
|
40
|
+
"claudeAccountSwitch": {
|
|
41
|
+
"runtimeProtocol": 1
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { color, success, userError } from '../lib/ui.mjs';
|
|
2
|
+
import { createProfile, validateProfileName, profileExists } from '../lib/profile.mjs';
|
|
3
|
+
import { readMeta } from '../lib/config.mjs';
|
|
4
|
+
import { syncShared } from '../lib/shared.mjs';
|
|
5
|
+
import { t } from '../lib/i18n.mjs';
|
|
6
|
+
import { reportConflicts } from './share.mjs';
|
|
7
|
+
|
|
8
|
+
// "My Work" → "my-work", offered when a typed name is not allowed.
|
|
9
|
+
function suggestName(name) {
|
|
10
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function add(name) {
|
|
14
|
+
const err = validateProfileName(name);
|
|
15
|
+
if (err) {
|
|
16
|
+
const suggestion = name ? suggestName(name) : '';
|
|
17
|
+
throw userError(err, suggestion && suggestion !== name && !validateProfileName(suggestion)
|
|
18
|
+
? [{ label: t('hint.try'), command: `cpf add ${suggestion}` }] : []);
|
|
19
|
+
}
|
|
20
|
+
const meta = readMeta();
|
|
21
|
+
if (profileExists(name) || meta.profiles.includes(name)) {
|
|
22
|
+
throw new Error(t('profile.exists', { name }));
|
|
23
|
+
}
|
|
24
|
+
createProfile(name, meta.shareSettings !== false);
|
|
25
|
+
reportConflicts(syncShared([name]));
|
|
26
|
+
success(t('add.created', { name }));
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(` ${color.dim(t('add.next'))}`);
|
|
29
|
+
console.log(` 1. ${t('add.step1', { command: color.cyan('claude') })}`);
|
|
30
|
+
console.log(` 2. ${t('add.step2', { name: color.bold(name) })}`);
|
|
31
|
+
console.log(` 3. ${t('add.step3')}`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { color, intro, outro, success, sym, warn } from '../lib/ui.mjs';
|
|
4
|
+
import * as prompt from '../lib/prompt.mjs';
|
|
5
|
+
import { readMeta, writeMeta } from '../lib/config.mjs';
|
|
6
|
+
import { createProfile, profileExists, validateProfileName, migrateDir, migrationSource } from '../lib/profile.mjs';
|
|
7
|
+
import { HOME, DEFAULT_CLAUDE_DIR, IS_WINDOWS } from '../lib/constants.mjs';
|
|
8
|
+
import { installAllShells } from '../lib/shell.mjs';
|
|
9
|
+
import { t } from '../lib/i18n.mjs';
|
|
10
|
+
|
|
11
|
+
export function existingClaudeSources() {
|
|
12
|
+
const candidates = [
|
|
13
|
+
process.env.CLAUDE_CONFIG_DIR,
|
|
14
|
+
DEFAULT_CLAUDE_DIR,
|
|
15
|
+
join(HOME, '.claude-work'),
|
|
16
|
+
join(HOME, '.claude-personal'),
|
|
17
|
+
].filter(Boolean);
|
|
18
|
+
return [...new Set(candidates.map(path => resolve(path)))].filter(path => {
|
|
19
|
+
const source = migrationSource(path);
|
|
20
|
+
return existsSync(source.directory) || existsSync(source.userConfig);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function init() {
|
|
25
|
+
const current = readMeta();
|
|
26
|
+
if (current.profiles.length > 0) {
|
|
27
|
+
warn(t('init.already'));
|
|
28
|
+
console.log(` ${color.dim(t('init.profiles'))} ${current.profiles.join(', ')}`);
|
|
29
|
+
console.log(` ${color.dim(t('init.active'))} ${current.activeProfile || t('common.none')}`);
|
|
30
|
+
console.log(` ${color.dim(t('init.addHint'))} ${color.cyan('cpf add <name>')}`);
|
|
31
|
+
console.log(` ${color.dim(t('init.repairHint'))} ${color.cyan('npx ai-profile-switch@latest install-shell')}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
intro(t('init.title'));
|
|
36
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('init.tagline'))}`);
|
|
37
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('init.closeSessions'))}`);
|
|
38
|
+
console.log(color.dim(sym.rail));
|
|
39
|
+
const names = await askProfileNames();
|
|
40
|
+
const activeProfile = names.length === 1
|
|
41
|
+
? names[0]
|
|
42
|
+
: await prompt.select(t('init.activeQuestion'), names);
|
|
43
|
+
if (!activeProfile) return;
|
|
44
|
+
const shareSettings = await prompt.confirm(t('init.shareQuestion'), true);
|
|
45
|
+
const sources = existingClaudeSources();
|
|
46
|
+
let migration;
|
|
47
|
+
if (sources.length > 0) {
|
|
48
|
+
const source = await prompt.select(t('init.copyQuestion'), [
|
|
49
|
+
...sources.map(path => ({ label: path, value: path })),
|
|
50
|
+
{ label: t('init.skip'), value: '__skip__' },
|
|
51
|
+
]);
|
|
52
|
+
if (!source) return;
|
|
53
|
+
if (source !== '__skip__') {
|
|
54
|
+
const target = names.length === 1 ? names[0] : await prompt.select(t('migrate.target'), names);
|
|
55
|
+
if (!target) return;
|
|
56
|
+
migration = { source, target };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
if (profileExists(name)) throw new Error(t('init.dirExists', { name }));
|
|
61
|
+
}
|
|
62
|
+
outro('');
|
|
63
|
+
if (migration) {
|
|
64
|
+
const { backupPath } = migrateDir(migration.source, migration.target, shareSettings);
|
|
65
|
+
success(t('migrate.copied', { source: migration.source, name: migration.target }));
|
|
66
|
+
if (backupPath) warn(t('migrate.backup', { path: backupPath }));
|
|
67
|
+
}
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
if (migration?.target !== name) createProfile(name, shareSettings);
|
|
70
|
+
success(t('init.ready', { name }));
|
|
71
|
+
}
|
|
72
|
+
writeMeta({ ...readMeta(), activeProfile, shareSettings });
|
|
73
|
+
const { newlyInstalled, alreadyInstalled } = installAllShells();
|
|
74
|
+
const shells = [...newlyInstalled, ...alreadyInstalled];
|
|
75
|
+
if (shells.length > 0) success(t('init.shells', { shells: shells.join(', ') }));
|
|
76
|
+
success(t('init.activeDone', { name: activeProfile }));
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(` ${t('init.openTerminal', { command: color.cyan('claude') })}`);
|
|
79
|
+
if (names.length > 1) {
|
|
80
|
+
const other = names.find(name => name !== activeProfile);
|
|
81
|
+
const command = IS_WINDOWS ? `cpf ${other}; claude` : `cpf ${other} && claude`;
|
|
82
|
+
console.log(` ${t('init.otherProfile')} ${color.cyan(command)}`);
|
|
83
|
+
}
|
|
84
|
+
if (migration) warn(t('migrate.preserved'));
|
|
85
|
+
console.log();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// One prompt takes every name, such as "work, personal".
|
|
89
|
+
async function askProfileNames() {
|
|
90
|
+
while (true) {
|
|
91
|
+
const names = (await prompt.text(t('init.names'), 'work, personal'))
|
|
92
|
+
.split(',').map(name => name.trim()).filter(Boolean);
|
|
93
|
+
const problem = profileNamesProblem(names);
|
|
94
|
+
if (!problem) return names;
|
|
95
|
+
console.log(` ${color.red(problem)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function profileNamesProblem(names) {
|
|
100
|
+
if (!names.length) return validateProfileName('');
|
|
101
|
+
for (const [index, name] of names.entries()) {
|
|
102
|
+
const error = validateProfileName(name);
|
|
103
|
+
if (error) return `${name}: ${error}`;
|
|
104
|
+
if (names.indexOf(name) !== index) return t('init.duplicate', { name });
|
|
105
|
+
if (profileExists(name)) return t('init.used', { name });
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { PROFILES_DIR } from '../lib/constants.mjs';
|
|
3
|
+
import { installAllShells } from '../lib/shell.mjs';
|
|
4
|
+
import { success, info } from '../lib/ui.mjs';
|
|
5
|
+
import { t } from '../lib/i18n.mjs';
|
|
6
|
+
|
|
7
|
+
export async function installShell() {
|
|
8
|
+
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
9
|
+
const { newlyInstalled, alreadyInstalled } = installAllShells();
|
|
10
|
+
const shells = [...newlyInstalled, ...alreadyInstalled];
|
|
11
|
+
success(t('install.done'));
|
|
12
|
+
if (shells.length) success(t('install.shells', { shells: shells.join(', ') }));
|
|
13
|
+
info(t('install.next'));
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as prompt from '../lib/prompt.mjs';
|
|
2
|
+
import { setLanguagePreference } from '../lib/config.mjs';
|
|
3
|
+
import { LANGUAGES } from '../lib/constants.mjs';
|
|
4
|
+
import { LANGUAGE_NAMES, detectLanguage, language as currentLanguage, savedLanguage, setLanguage, t } from '../lib/i18n.mjs';
|
|
5
|
+
import { color, info, success } from '../lib/ui.mjs';
|
|
6
|
+
|
|
7
|
+
const AUTO = 'auto';
|
|
8
|
+
|
|
9
|
+
function save(choice) {
|
|
10
|
+
setLanguagePreference(choice === AUTO ? null : choice);
|
|
11
|
+
const effective = choice === AUTO ? detectLanguage({ saved: null }) : choice;
|
|
12
|
+
setLanguage(effective);
|
|
13
|
+
success(choice === AUTO
|
|
14
|
+
? t('language.savedAuto', { name: LANGUAGE_NAMES[effective] })
|
|
15
|
+
: t('language.saved', { name: LANGUAGE_NAMES[effective] }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function language(args = []) {
|
|
19
|
+
if (args.length > 1) throw new Error(t('language.usage'));
|
|
20
|
+
const [choice] = args;
|
|
21
|
+
if (choice !== undefined) {
|
|
22
|
+
if (choice !== AUTO && !LANGUAGES.includes(choice)) throw new Error(t('language.usage'));
|
|
23
|
+
save(choice);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const saved = savedLanguage();
|
|
27
|
+
if (!process.stdin.isTTY) {
|
|
28
|
+
const source = t(saved ? 'language.sourceSaved' : 'language.sourceAuto');
|
|
29
|
+
info(`${t('language.current', { name: LANGUAGE_NAMES[currentLanguage()] })} ${color.dim(`(${source})`)}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const system = LANGUAGE_NAMES[detectLanguage({ saved: null })];
|
|
33
|
+
const selected = await prompt.select(t('language.title'), [
|
|
34
|
+
{ label: t('language.auto'), value: AUTO, hint: color.dim(t('language.autoHint', { name: system })) },
|
|
35
|
+
...LANGUAGES.map(code => ({ label: LANGUAGE_NAMES[code], value: code })),
|
|
36
|
+
], { initial: saved ? LANGUAGES.indexOf(saved) + 1 : 0 });
|
|
37
|
+
if (selected) save(selected);
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { color, info } from '../lib/ui.mjs';
|
|
2
|
+
import { listProfiles, profileDir } from '../lib/profile.mjs';
|
|
3
|
+
import { getActiveProfile } from '../lib/config.mjs';
|
|
4
|
+
import { HOME } from '../lib/constants.mjs';
|
|
5
|
+
import { accountInfo } from '../lib/account.mjs';
|
|
6
|
+
import { t } from '../lib/i18n.mjs';
|
|
7
|
+
|
|
8
|
+
export async function list() {
|
|
9
|
+
const profiles = listProfiles();
|
|
10
|
+
|
|
11
|
+
if (profiles.length === 0) {
|
|
12
|
+
info(`${t('profiles.none')} ${t('hint.add')} cpf add work · ${t('hint.setup')} npx ai-profile-switch@latest init`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const active = getActiveProfile();
|
|
17
|
+
const width = Math.max(...profiles.map(name => name.length));
|
|
18
|
+
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(` ${color.bold(t('list.title'))}`);
|
|
21
|
+
console.log();
|
|
22
|
+
for (const name of profiles) {
|
|
23
|
+
const isActive = name === active;
|
|
24
|
+
const marker = isActive ? color.green(' * ') : ' ';
|
|
25
|
+
const label = isActive ? color.bold(name.padEnd(width)) : name.padEnd(width);
|
|
26
|
+
const { email, plan } = accountInfo(name);
|
|
27
|
+
const account = [email, plan].filter(Boolean).join(' · ');
|
|
28
|
+
const dir = profileDir(name);
|
|
29
|
+
const shown = dir.startsWith(HOME) ? `~${dir.slice(HOME.length)}` : dir;
|
|
30
|
+
console.log(` ${marker}${label} ${account ? `${account} ` : ''}${color.dim(shown)}`);
|
|
31
|
+
}
|
|
32
|
+
console.log();
|
|
33
|
+
console.log(` ${t('list.active', { name: color.green(active || t('common.none')) })}`);
|
|
34
|
+
console.log();
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { accountInfo } from '../lib/account.mjs';
|
|
3
|
+
import { t } from '../lib/i18n.mjs';
|
|
4
|
+
import { assertRegisteredProfile, profileDir } from '../lib/profile.mjs';
|
|
5
|
+
import { color, info, success, userError, warn } from '../lib/ui.mjs';
|
|
6
|
+
import { accountSummary, resolveClaudeBinary } from './shell.mjs';
|
|
7
|
+
|
|
8
|
+
function runAuthLogin(bin, name) {
|
|
9
|
+
const cmd = bin.endsWith('.js') ? process.execPath : bin;
|
|
10
|
+
const args = bin.endsWith('.js') ? [bin, 'auth', 'login'] : ['auth', 'login'];
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
const child = spawn(cmd, args, {
|
|
13
|
+
stdio: 'inherit', shell: false,
|
|
14
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: profileDir(name) },
|
|
15
|
+
});
|
|
16
|
+
child.once('error', () => resolve(1));
|
|
17
|
+
child.once('close', code => resolve(code ?? 1));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Signs one profile in again with Claude Code's own login; other profiles keep theirs.
|
|
22
|
+
// explained: the menu already showed the current sign-in and what will happen.
|
|
23
|
+
export async function login(name, { explained = false } = {}) {
|
|
24
|
+
if (!name) throw new Error(t('login.usage'));
|
|
25
|
+
assertRegisteredProfile(name);
|
|
26
|
+
const bin = resolveClaudeBinary();
|
|
27
|
+
if (!bin) throw userError(t('launch.notFound'), [{ label: t('launch.install'), text: 'https://code.claude.com/docs/en/setup' }]);
|
|
28
|
+
const before = accountSummary(accountInfo(name));
|
|
29
|
+
if (!explained) {
|
|
30
|
+
console.log(` ${color.dim(t('login.current'))} ${before || color.dim(t('account.notSignedIn'))}`);
|
|
31
|
+
warn(t('login.closeSessions', { name }));
|
|
32
|
+
}
|
|
33
|
+
console.log(` ${color.dim(t('login.opening'))}`);
|
|
34
|
+
const code = await runAuthLogin(bin, name);
|
|
35
|
+
if (code !== 0) throw new Error(t('login.failed', { code }));
|
|
36
|
+
const after = accountSummary(accountInfo(name));
|
|
37
|
+
if (!after) warn(t('login.notSaved', { name }));
|
|
38
|
+
else if (after === before) info(t('login.same', { name, account: after }));
|
|
39
|
+
else {
|
|
40
|
+
success(t('login.done', { name, account: color.bold(after) }));
|
|
41
|
+
console.log(` ${color.dim(t('login.usageLater'))}`);
|
|
42
|
+
}
|
|
43
|
+
}
|