ai-profile-switch 2.2.1 → 2.2.2
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.ko.md +5 -0
- package/README.md +5 -0
- package/package.json +6 -1
- package/src/commands/manage.mjs +3 -2
- package/src/commands/shell.mjs +5 -3
- package/src/lib/account.mjs +81 -6
- package/src/lib/profile.mjs +32 -0
- package/src/lib/prompt.mjs +33 -5
- package/src/locales/en.mjs +4 -0
- package/src/locales/ko.mjs +4 -0
package/README.ko.md
CHANGED
|
@@ -42,6 +42,7 @@ npx ai-profile-switch@latest init
|
|
|
42
42
|
- 전환은 다음 실행부터 적용됩니다. 이미 켜진 세션은 원래 계정을 그대로 씁니다.
|
|
43
43
|
- 스크립트에서는 `claude` 가 묻지 않고 지금 프로필로 실행합니다.
|
|
44
44
|
- Bash·zsh 프롬프트에서 `command claude` 를 치면 프로필 없이 원래 Claude 가 실행됩니다.
|
|
45
|
+
- IDE 확장(VS Code, JetBrains)과 데스크톱 앱은 셸을 거치지 않아 기본 `~/.claude` 를 계속 씁니다.
|
|
45
46
|
|
|
46
47
|
## 프로필끼리 설정 나누기
|
|
47
48
|
|
|
@@ -89,4 +90,8 @@ npx ai-profile-switch@latest install-shell # 셸 명령 고치기·새로 깔
|
|
|
89
90
|
- 플랫폼별 안내: [macOS](docs/setup-macos.ko.md) · [Linux](docs/setup-linux.ko.md) · [Windows](docs/setup-windows.ko.md)
|
|
90
91
|
- 이 소스를 직접 쓰려면 레포 폴더에서 `node bin/cli.mjs install-shell` 을 실행합니다.
|
|
91
92
|
|
|
93
|
+
## 기여하기
|
|
94
|
+
|
|
95
|
+
버그 신고, 수정, 문서, 다른 AI 코딩 CLI 지원 모두 환영합니다. [CONTRIBUTING.md](CONTRIBUTING.md)(영어)를 먼저 읽고, [good first issue](https://github.com/ftery0/ai-profile-switch/labels/good%20first%20issue) 나 [new tool](https://github.com/ftery0/ai-profile-switch/labels/new%20tool) 라벨이 붙은 이슈에서 골라 주세요. 이슈와 PR 은 영어로 씁니다. 보안 문제는 공개 이슈 대신 [SECURITY.md](SECURITY.md) 에 적힌 대로 알려 주세요.
|
|
96
|
+
|
|
92
97
|
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Run these at the terminal prompt. If you are in the Claude chat, enter `/exit` f
|
|
|
42
42
|
- Switching applies to the next launch. Running sessions keep their account.
|
|
43
43
|
- In scripts, `claude` uses the active profile without asking.
|
|
44
44
|
- At a Bash or zsh prompt, `command claude` runs the original Claude without a profile.
|
|
45
|
+
- IDE extensions (VS Code, JetBrains) and the desktop app don't go through the shell, so they keep using the default `~/.claude`.
|
|
45
46
|
|
|
46
47
|
## Share settings across profiles
|
|
47
48
|
|
|
@@ -89,4 +90,8 @@ Coming from `claude-account-switch` or 1.x? Run `npx ai-profile-switch@latest in
|
|
|
89
90
|
- Platform guides: [macOS](docs/setup-macos.md) · [Linux](docs/setup-linux.md) · [Windows](docs/setup-windows.md)
|
|
90
91
|
- To use this source checkout, run `node bin/cli.mjs install-shell` from the repository root.
|
|
91
92
|
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
Bug reports, fixes, docs, and support for more AI coding CLIs are all welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), then pick an issue labeled [good first issue](https://github.com/ftery0/ai-profile-switch/labels/good%20first%20issue) or [new tool](https://github.com/ftery0/ai-profile-switch/labels/new%20tool). Please report security problems privately, as described in [SECURITY.md](SECURITY.md).
|
|
96
|
+
|
|
92
97
|
[MIT](./LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-profile-switch",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Profile switching for AI coding CLIs, starting with Claude Code (formerly claude-account-switch)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test",
|
|
19
|
+
"test:e2e": "bash test/e2e/single-user/run.sh",
|
|
19
20
|
"docs:dev": "vitepress dev website",
|
|
20
21
|
"docs:build": "vitepress build website",
|
|
21
22
|
"docs:preview": "vitepress preview website"
|
|
@@ -34,6 +35,10 @@
|
|
|
34
35
|
"type": "git",
|
|
35
36
|
"url": "git+https://github.com/ftery0/ai-profile-switch.git"
|
|
36
37
|
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/ftery0/ai-profile-switch/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://ftery0.github.io/ai-profile-switch/",
|
|
37
42
|
"devDependencies": {
|
|
38
43
|
"vitepress": "^1.6.4"
|
|
39
44
|
},
|
package/src/commands/manage.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as prompt from '../lib/prompt.mjs';
|
|
2
2
|
import { readMeta } from '../lib/config.mjs';
|
|
3
3
|
import { color, displayWidth, error, intro, sym, warn } from '../lib/ui.mjs';
|
|
4
|
-
import { accountInfo } from '../lib/account.mjs';
|
|
4
|
+
import { accountInfo, refreshUsage } from '../lib/account.mjs';
|
|
5
5
|
import { HOME } from '../lib/constants.mjs';
|
|
6
6
|
import { LANGUAGE_NAMES, language as currentLanguage, relativeTime, t } from '../lib/i18n.mjs';
|
|
7
7
|
import { checkRename, profileDir } from '../lib/profile.mjs';
|
|
@@ -74,7 +74,8 @@ export async function manage(args = []) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function pickProfile(question, meta) {
|
|
77
|
+
async function pickProfile(question, meta) {
|
|
78
|
+
await refreshUsage(meta.profiles);
|
|
78
79
|
return prompt.select(question, profileChoices(meta.profiles, meta.activeProfile), {
|
|
79
80
|
initial: Math.max(0, meta.profiles.indexOf(meta.activeProfile)), numbered: true, back: true,
|
|
80
81
|
});
|
package/src/commands/shell.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { HOME, IS_WINDOWS } from '../lib/constants.mjs';
|
|
|
7
7
|
import { installAllShells } from '../lib/shell.mjs';
|
|
8
8
|
import { listProfiles, profileDir, assertRegisteredProfile } from '../lib/profile.mjs';
|
|
9
9
|
import { color, colorFor, intro, meter, outro, success, sym, userError, warn } from '../lib/ui.mjs';
|
|
10
|
-
import { accountInfo } from '../lib/account.mjs';
|
|
10
|
+
import { accountInfo, refreshUsage } from '../lib/account.mjs';
|
|
11
11
|
import { relativeTime, t } from '../lib/i18n.mjs';
|
|
12
12
|
import { syncShared } from '../lib/shared.mjs';
|
|
13
13
|
import { reportConflicts } from './share.mjs';
|
|
@@ -118,8 +118,9 @@ export function accountSummary({ email, plan }) {
|
|
|
118
118
|
export function usageLine({ usage }) {
|
|
119
119
|
if (!usage) return color.dim(t('account.noUsage'));
|
|
120
120
|
const parts = [];
|
|
121
|
-
|
|
122
|
-
if (usage.
|
|
121
|
+
const reset = key => (usage.reset?.[key] ? ` ${color.dim(t('account.reset'))}` : '');
|
|
122
|
+
if (usage.fiveHour !== null) parts.push(`${t('account.fiveHour')} ${meter(usage.fiveHour)}${reset('fiveHour')}`);
|
|
123
|
+
if (usage.weekly !== null) parts.push(`${t('account.weekly')} ${meter(usage.weekly)}${reset('weekly')}`);
|
|
123
124
|
if (usage.savedAt) parts.push(color.dim(t('account.savedAt', { time: relativeTime(usage.savedAt) })));
|
|
124
125
|
return parts.join(' ');
|
|
125
126
|
}
|
|
@@ -162,6 +163,7 @@ async function selectProfile({ launching = false } = {}) {
|
|
|
162
163
|
}
|
|
163
164
|
return selected;
|
|
164
165
|
}
|
|
166
|
+
await refreshUsage(profiles);
|
|
165
167
|
const choices = profileChoices(profiles, active);
|
|
166
168
|
intro('ai-profile-switch', '', output);
|
|
167
169
|
const selected = await prompt.select(t('picker.question'), choices, {
|
package/src/lib/account.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Reads what Claude Code saved in a profile, for display only. Nothing is written,
|
|
2
2
|
// and any field with an unexpected shape is left out instead of failing.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
3
5
|
import { readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
4
7
|
import { join } from 'node:path';
|
|
5
8
|
import { PROFILES_DIR } from './constants.mjs';
|
|
6
9
|
|
|
@@ -20,16 +23,88 @@ function percent(window) {
|
|
|
20
23
|
return typeof value === 'number' && Number.isFinite(value) ? Math.min(100, Math.max(0, Math.round(value))) : null;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
// A window whose reset time has passed starts again from zero.
|
|
27
|
+
function hasReset(window, now) {
|
|
28
|
+
const resetsAt = typeof window?.resets_at === 'string' ? Date.parse(window.resets_at) : NaN;
|
|
29
|
+
return Number.isFinite(resetsAt) && resetsAt <= now;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
// Claude Code keeps usage in an undocumented cache; it may change or disappear.
|
|
24
|
-
function usage(cached, account) {
|
|
33
|
+
function usage(cached, account, now = Date.now()) {
|
|
25
34
|
if (!cached || typeof cached !== 'object') return null;
|
|
26
35
|
if (typeof cached.accountUuid === 'string' && typeof account.accountUuid === 'string'
|
|
27
36
|
&& cached.accountUuid !== account.accountUuid) return null;
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
37
|
+
const windows = { fiveHour: cached.utilization?.five_hour, weekly: cached.utilization?.seven_day };
|
|
38
|
+
const reset = { fiveHour: false, weekly: false };
|
|
39
|
+
const values = {};
|
|
40
|
+
for (const [key, window] of Object.entries(windows)) {
|
|
41
|
+
values[key] = percent(window);
|
|
42
|
+
if (values[key] !== null && hasReset(window, now)) [values[key], reset[key]] = [0, true];
|
|
43
|
+
}
|
|
44
|
+
if (values.fiveHour === null && values.weekly === null) return null;
|
|
31
45
|
const savedAt = Number.isFinite(cached.fetchedAtMs) ? cached.fetchedAtMs : null;
|
|
32
|
-
return { fiveHour, weekly, savedAt };
|
|
46
|
+
return { fiveHour: values.fiveHour, weekly: values.weekly, savedAt, reset };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Usage fetched in this run, by profile name. It wins over the saved cache.
|
|
50
|
+
const live = new Map();
|
|
51
|
+
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
52
|
+
const FRESH_MS = 60_000;
|
|
53
|
+
|
|
54
|
+
// Claude Code stores each config folder's sign-in under a keychain item named
|
|
55
|
+
// after the folder path; elsewhere, or without a keychain, in .credentials.json.
|
|
56
|
+
function keychainService(dir) {
|
|
57
|
+
return `Claude Code-credentials-${createHash('sha256').update(dir).digest('hex').slice(0, 8)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readKeychain(dir) {
|
|
61
|
+
return new Promise(resolve => {
|
|
62
|
+
execFile('security', ['find-generic-password', '-s', keychainService(dir), '-w'], { timeout: 1000 },
|
|
63
|
+
(err, stdout) => resolve(err ? null : stdout));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readCredentials(dir) {
|
|
68
|
+
const fromKeychain = process.platform === 'darwin' ? await readKeychain(dir) : null;
|
|
69
|
+
return fromKeychain ?? readFile(join(dir, '.credentials.json'), 'utf8').catch(() => null);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// An expired token is skipped, not refreshed: refreshing could sign Claude Code out.
|
|
73
|
+
function accessToken(raw, now) {
|
|
74
|
+
try {
|
|
75
|
+
const oauth = JSON.parse(raw)?.claudeAiOauth;
|
|
76
|
+
if (typeof oauth?.accessToken !== 'string' || !oauth.accessToken) return null;
|
|
77
|
+
if (Number.isFinite(oauth.expiresAt) && oauth.expiresAt <= now + FRESH_MS) return null;
|
|
78
|
+
return oauth.accessToken;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Asks the usage API for each signed-in profile at once. Anything slow or failing
|
|
85
|
+
// keeps the saved value, so the screen never waits more than timeoutMs.
|
|
86
|
+
export async function refreshUsage(names, { read = readCredentials, request = fetch, timeoutMs = 1500 } = {}) {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
89
|
+
const due = names.filter(name => !(now - (live.get(name)?.fetchedAtMs ?? 0) < FRESH_MS)
|
|
90
|
+
&& readConfig(join(PROFILES_DIR, name)).oauthAccount);
|
|
91
|
+
const fetchOne = async name => {
|
|
92
|
+
try {
|
|
93
|
+
const token = accessToken(await read(join(PROFILES_DIR, name)), now);
|
|
94
|
+
if (!token || signal.aborted) return;
|
|
95
|
+
const response = await request(USAGE_URL, {
|
|
96
|
+
headers: { Authorization: `Bearer ${token}`, 'anthropic-beta': 'oauth-2025-04-20' }, signal,
|
|
97
|
+
});
|
|
98
|
+
const utilization = response.ok ? await response.json() : null;
|
|
99
|
+
if (utilization && typeof utilization === 'object' && !signal.aborted) {
|
|
100
|
+
live.set(name, { utilization, fetchedAtMs: Date.now() });
|
|
101
|
+
}
|
|
102
|
+
} catch { /* keep the saved value */ }
|
|
103
|
+
};
|
|
104
|
+
let timer;
|
|
105
|
+
const deadline = new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); });
|
|
106
|
+
await Promise.race([Promise.all(due.map(fetchOne)), deadline]);
|
|
107
|
+
clearTimeout(timer);
|
|
33
108
|
}
|
|
34
109
|
|
|
35
110
|
export function accountInfo(name) {
|
|
@@ -41,7 +116,7 @@ export function accountInfo(name) {
|
|
|
41
116
|
return {
|
|
42
117
|
email: typeof account.emailAddress === 'string' && account.emailAddress ? account.emailAddress : null,
|
|
43
118
|
plan: Object.hasOwn(PLANS, account.organizationType) ? PLANS[account.organizationType] : null,
|
|
44
|
-
usage: usage(config.cachedUsageUtilization, account),
|
|
119
|
+
usage: usage(live.get(name) ?? config.cachedUsageUtilization, account),
|
|
45
120
|
lastUsed,
|
|
46
121
|
};
|
|
47
122
|
}
|
package/src/lib/profile.mjs
CHANGED
|
@@ -231,6 +231,37 @@ function validateJson(source) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Claude Code saves absolute paths in its plugin indexes. Copied as they are, a profile would keep
|
|
235
|
+
// loading plugins from the source folder and break once that folder is removed.
|
|
236
|
+
const PLUGIN_INDEXES = ['installed_plugins.json', 'known_marketplaces.json'];
|
|
237
|
+
|
|
238
|
+
function repointPlugins(plugins, sourceDir, targetDir) {
|
|
239
|
+
if (!pathExists(plugins) || !lstatSync(plugins).isDirectory()) return;
|
|
240
|
+
const to = join(targetDir, 'plugins');
|
|
241
|
+
const prefixes = [...new Set([join(sourceDir, 'plugins'), join(canonicalPath(sourceDir), 'plugins')])];
|
|
242
|
+
const move = value => {
|
|
243
|
+
if (typeof value === 'string') {
|
|
244
|
+
const from = prefixes.find(prefix => value === prefix
|
|
245
|
+
|| value.startsWith(prefix + sep) || value.startsWith(prefix + '/'));
|
|
246
|
+
return from ? to + value.slice(from.length) : value;
|
|
247
|
+
}
|
|
248
|
+
if (Array.isArray(value)) return value.map(move);
|
|
249
|
+
if (value && typeof value === 'object') {
|
|
250
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, move(item)]));
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
};
|
|
254
|
+
for (const name of PLUGIN_INDEXES) {
|
|
255
|
+
const path = join(plugins, name);
|
|
256
|
+
if (!pathExists(path) || !lstatSync(path).isFile()) continue;
|
|
257
|
+
const text = readFileSync(path, 'utf8');
|
|
258
|
+
let index;
|
|
259
|
+
try { index = JSON.parse(text); } catch { continue; }
|
|
260
|
+
const moved = JSON.stringify(move(index), null, 2);
|
|
261
|
+
if (moved !== JSON.stringify(index, null, 2)) writeFileSync(path, moved + '\n');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
234
265
|
function stageEntry(source, destination) {
|
|
235
266
|
if (!pathExists(source)) return;
|
|
236
267
|
if (pathExists(destination)) {
|
|
@@ -293,6 +324,7 @@ function prepareMigration(source, profileName, shareSettings, stage) {
|
|
|
293
324
|
stageEntry(join(source.directory, entry), join(stagedProfile, entry));
|
|
294
325
|
if (!pathExists(join(stagedProfile, entry))) mkdirSync(join(stagedProfile, entry));
|
|
295
326
|
}
|
|
327
|
+
repointPlugins(join(stagedProfile, 'plugins'), source.directory, target);
|
|
296
328
|
for (const entry of [...SHARED_FILES, ...SHARED_DIRS]) {
|
|
297
329
|
const shared = shareSettings ? stagedShared : stagedProfile;
|
|
298
330
|
const from = join(source.directory, entry);
|
package/src/lib/prompt.mjs
CHANGED
|
@@ -110,6 +110,7 @@ export async function select(message, choices, {
|
|
|
110
110
|
return new Promise((resolve) => {
|
|
111
111
|
let selected = Math.max(0, Math.min(initial, items.length - 1));
|
|
112
112
|
let height = 0;
|
|
113
|
+
let top = 0;
|
|
113
114
|
const cursor = cursorControl(output);
|
|
114
115
|
const rail = color.dim(sym.rail);
|
|
115
116
|
const indent = ' '.repeat(numbered ? 5 : 2);
|
|
@@ -117,17 +118,22 @@ export async function select(message, choices, {
|
|
|
117
118
|
? t(back ? 'prompt.keysNumberedBack' : 'prompt.keysNumbered', { arrows: sym.arrows, max: items.length })
|
|
118
119
|
: t(back ? 'prompt.keysBack' : 'prompt.keys', { arrows: sym.arrows });
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
// The choices from index from up to to, with details under the ones detailed(i) picks.
|
|
122
|
+
const rows = (detailed = () => true, from = 0, to = items.length) => {
|
|
121
123
|
const lines = [];
|
|
122
|
-
|
|
124
|
+
if (from > 0) lines.push(`${rail} ${color.dim(t('prompt.moreAbove', { count: from }))}`);
|
|
125
|
+
items.slice(from, to).forEach((item, offset) => {
|
|
126
|
+
const i = from + offset;
|
|
123
127
|
const number = numbered ? `${i + 1} ` : '';
|
|
124
128
|
const head = i === selected
|
|
125
129
|
? color.cyan(`${sym.cursor} ${number}${item.label}`)
|
|
126
130
|
: ` ${numbered ? `${color.dim(String(i + 1))} ` : ''}${item.label}`;
|
|
127
131
|
lines.push(`${rail} ${head}${item.hint ? ` ${item.hint}` : ''}`);
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
if (!detailed(i) || !item.details?.length) return;
|
|
133
|
+
for (const detail of item.details) lines.push(`${rail} ${indent}${detail}`);
|
|
134
|
+
lines.push(rail);
|
|
130
135
|
});
|
|
136
|
+
if (to < items.length) lines.push(`${rail} ${color.dim(t('prompt.moreBelow', { count: items.length - to }))}`);
|
|
131
137
|
lines.push(`${rail} ${color.dim(keys)}`, color.dim(sym.railEnd));
|
|
132
138
|
return lines;
|
|
133
139
|
};
|
|
@@ -137,10 +143,32 @@ export async function select(message, choices, {
|
|
|
137
143
|
+ Math.max(1, Math.ceil(displayWidth(line) / (output.columns || Infinity))), 0);
|
|
138
144
|
const header = [`${color.cyan(sym.step)} ${message}`, ...body.map(line => `${rail} ${line}`)];
|
|
139
145
|
|
|
146
|
+
// The cursor can't move up past the top of the window, so a menu taller than the window
|
|
147
|
+
// can't be redrawn in place: every redraw would push another copy of its first rows into
|
|
148
|
+
// the scrollback. It shrinks to fit instead: details only under the selected choice, then
|
|
149
|
+
// only the choices around it. The row under the menu is where the cursor waits.
|
|
150
|
+
const fits = lines => screenRows(header) + screenRows(lines) < (output.rows || Infinity);
|
|
151
|
+
const layout = () => {
|
|
152
|
+
const full = rows();
|
|
153
|
+
if (fits(full)) return full;
|
|
154
|
+
const shown = (from, to) => rows(i => i === selected, from, to);
|
|
155
|
+
if (fits(shown(0, items.length))) return shown(0, items.length);
|
|
156
|
+
const end = from => {
|
|
157
|
+
let to = Math.max(from, selected) + 1;
|
|
158
|
+
while (to < items.length && fits(shown(from, to + 1))) to++;
|
|
159
|
+
return to;
|
|
160
|
+
};
|
|
161
|
+
top = Math.min(top, selected);
|
|
162
|
+
while (top < selected && !fits(shown(top, end(top)))) top++;
|
|
163
|
+
return shown(top, end(top));
|
|
164
|
+
};
|
|
165
|
+
|
|
140
166
|
const render = () => {
|
|
141
167
|
if (height) output.write(cursor.up(height));
|
|
142
|
-
const lines =
|
|
168
|
+
const lines = layout();
|
|
143
169
|
for (const line of lines) output.write(`${cursor.clearLine}${line}\n`);
|
|
170
|
+
// A shorter menu than last time leaves old rows below it.
|
|
171
|
+
output.write(cursor.clearDown);
|
|
144
172
|
height = screenRows(lines);
|
|
145
173
|
};
|
|
146
174
|
|
package/src/locales/en.mjs
CHANGED
|
@@ -6,6 +6,8 @@ export default {
|
|
|
6
6
|
'prompt.keysNumbered': '{arrows} move · 1-{max} pick · Enter select · Esc cancel',
|
|
7
7
|
'prompt.keysBack': '{arrows} move · Enter select · Esc back',
|
|
8
8
|
'prompt.keysNumberedBack': '{arrows} move · 1-{max} pick · Enter select · Esc back',
|
|
9
|
+
'prompt.moreAbove': '{count} more above',
|
|
10
|
+
'prompt.moreBelow': '{count} more below',
|
|
9
11
|
'prompt.cancelled': 'cancelled',
|
|
10
12
|
'prompt.yes': 'Yes',
|
|
11
13
|
'prompt.no': 'No',
|
|
@@ -26,6 +28,7 @@ export default {
|
|
|
26
28
|
'account.weekly': 'Weekly',
|
|
27
29
|
'account.noUsage': 'No usage saved yet',
|
|
28
30
|
'account.savedAt': 'as of {time}',
|
|
31
|
+
'account.reset': 'reset',
|
|
29
32
|
'account.notSignedIn': 'Not signed in yet',
|
|
30
33
|
|
|
31
34
|
'profile.empty': 'Profile name cannot be empty',
|
|
@@ -259,6 +262,7 @@ Without the shell commands, use: npx ai-profile-switch@latest manage [arguments]
|
|
|
259
262
|
update [--check] Update this tool; --check only checks
|
|
260
263
|
share [add|remove] Share CLAUDE.md, skills, agents, … across profiles
|
|
261
264
|
mcp [list] Show where old MCP settings went
|
|
265
|
+
--version, -v Show the installed version
|
|
262
266
|
|
|
263
267
|
Run any command with --help for its usage.
|
|
264
268
|
`,
|
package/src/locales/ko.mjs
CHANGED
|
@@ -6,6 +6,8 @@ export default {
|
|
|
6
6
|
'prompt.keysNumbered': '{arrows} 이동 · 1-{max} 바로 선택 · Enter 선택 · Esc 취소',
|
|
7
7
|
'prompt.keysBack': '{arrows} 이동 · Enter 선택 · Esc 뒤로',
|
|
8
8
|
'prompt.keysNumberedBack': '{arrows} 이동 · 1-{max} 바로 선택 · Enter 선택 · Esc 뒤로',
|
|
9
|
+
'prompt.moreAbove': '위에 {count}개 더',
|
|
10
|
+
'prompt.moreBelow': '아래에 {count}개 더',
|
|
9
11
|
'prompt.cancelled': '취소했어요',
|
|
10
12
|
'prompt.yes': '예',
|
|
11
13
|
'prompt.no': '아니요',
|
|
@@ -26,6 +28,7 @@ export default {
|
|
|
26
28
|
'account.weekly': '주간',
|
|
27
29
|
'account.noUsage': '아직 저장된 사용량 없음',
|
|
28
30
|
'account.savedAt': '{time} 기준',
|
|
31
|
+
'account.reset': '초기화됨',
|
|
29
32
|
'account.notSignedIn': '아직 로그인하지 않았어요',
|
|
30
33
|
|
|
31
34
|
'profile.empty': '프로필 이름을 적어 주세요',
|
|
@@ -259,6 +262,7 @@ claude 를 실행하면 프로필을 골라 Claude Code 를 시작해요.
|
|
|
259
262
|
update [--check] 이 도구 업데이트 (--check 는 확인만)
|
|
260
263
|
share [add|remove] CLAUDE.md·skills·agents 등을 같이 쓰기
|
|
261
264
|
mcp [list] 예전 MCP 설정이 어디로 갔는지 보기
|
|
265
|
+
--version, -v 설치된 버전 보기
|
|
262
266
|
|
|
263
267
|
명령 뒤에 --help 를 붙이면 사용법이 나와요.
|
|
264
268
|
`,
|