claude-util 0.1.1__py3-none-any.whl
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.
- claude_cli/__init__.py +3 -0
- claude_cli/__main__.py +5 -0
- claude_cli/commands/__init__.py +0 -0
- claude_cli/commands/account.py +280 -0
- claude_cli/commands/config.py +28 -0
- claude_cli/commands/history.py +32 -0
- claude_cli/commands/init.py +136 -0
- claude_cli/commands/run.py +78 -0
- claude_cli/commands/status.py +44 -0
- claude_cli/commands/usage.py +80 -0
- claude_cli/commands/use.py +41 -0
- claude_cli/core/__init__.py +0 -0
- claude_cli/core/account.py +184 -0
- claude_cli/core/auth.py +76 -0
- claude_cli/core/config.py +43 -0
- claude_cli/core/history.py +63 -0
- claude_cli/core/usage.py +138 -0
- claude_cli/main.py +78 -0
- claude_cli/models/__init__.py +0 -0
- claude_cli/models/config.py +23 -0
- claude_cli/models/usage.py +51 -0
- claude_cli/ui/__init__.py +21 -0
- claude_cli/ui/console.py +46 -0
- claude_cli/ui/prompts.py +120 -0
- claude_cli/ui/tables.py +212 -0
- claude_cli/utils/__init__.py +0 -0
- claude_cli/utils/completers.py +19 -0
- claude_cli/utils/formatters.py +17 -0
- claude_cli/utils/validators.py +27 -0
- claude_util-0.1.1.dist-info/METADATA +20 -0
- claude_util-0.1.1.dist-info/RECORD +33 -0
- claude_util-0.1.1.dist-info/WHEEL +4 -0
- claude_util-0.1.1.dist-info/entry_points.txt +4 -0
claude_cli/__init__.py
ADDED
claude_cli/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""claude-cli account — Account management sub-commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from claude_cli.core.account import (
|
|
10
|
+
add_account,
|
|
11
|
+
get_account_dir,
|
|
12
|
+
get_default_account,
|
|
13
|
+
list_accounts,
|
|
14
|
+
remove_account,
|
|
15
|
+
rename_account,
|
|
16
|
+
set_default_account,
|
|
17
|
+
setup_symlinks,
|
|
18
|
+
ensure_shared_dir,
|
|
19
|
+
)
|
|
20
|
+
from claude_cli.core.auth import check_auth_status, trigger_login
|
|
21
|
+
from claude_cli.core.config import ACCOUNTS_DIR, SHARED_DIR, load_config
|
|
22
|
+
from claude_cli.ui import console, error, info, print_detail, print_header, success
|
|
23
|
+
from claude_cli.ui.prompts import confirm, select_account, select_from_list, text_input
|
|
24
|
+
from claude_cli.ui.tables import print_accounts_table
|
|
25
|
+
from claude_cli.utils.completers import complete_account_name, complete_tier
|
|
26
|
+
from claude_cli.utils.formatters import format_date
|
|
27
|
+
from claude_cli.utils.validators import validate_account_name, validate_label
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(no_args_is_help=True)
|
|
30
|
+
|
|
31
|
+
TIER_CHOICES = ["pro", "max", "team", "enterprise"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("list")
|
|
35
|
+
def list_command(
|
|
36
|
+
output: Optional[str] = typer.Option(None, "--output", "-O", help="Output format: table, json"),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""List all configured accounts."""
|
|
39
|
+
config = load_config()
|
|
40
|
+
if not config.accounts:
|
|
41
|
+
info("No accounts configured. Run 'claude-cli init' to get started.")
|
|
42
|
+
raise typer.Exit(0)
|
|
43
|
+
|
|
44
|
+
if output == "json":
|
|
45
|
+
import json
|
|
46
|
+
|
|
47
|
+
data = {
|
|
48
|
+
name: {
|
|
49
|
+
"label": acc.label,
|
|
50
|
+
"tier": acc.tier,
|
|
51
|
+
"created_at": acc.created_at.isoformat(),
|
|
52
|
+
"is_default": name == config.default,
|
|
53
|
+
"auth_status": check_auth_status(name),
|
|
54
|
+
}
|
|
55
|
+
for name, acc in config.accounts.items()
|
|
56
|
+
}
|
|
57
|
+
console.print_json(json.dumps(data))
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
auth_statuses = {name: check_auth_status(name) for name in config.accounts}
|
|
61
|
+
print_accounts_table(config, auth_statuses)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("add")
|
|
65
|
+
def add_command(
|
|
66
|
+
name: Optional[str] = typer.Argument(None, help="Account name slug"),
|
|
67
|
+
label: Optional[str] = typer.Option(None, "--label", "-l", help="Display label"),
|
|
68
|
+
tier: Optional[str] = typer.Option(
|
|
69
|
+
None, "--tier", "-t", help="Subscription tier", autocompletion=complete_tier
|
|
70
|
+
),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Add a new account profile."""
|
|
73
|
+
accounts = list_accounts()
|
|
74
|
+
|
|
75
|
+
if not name:
|
|
76
|
+
while True:
|
|
77
|
+
name = text_input("Account name (slug):")
|
|
78
|
+
if name is None:
|
|
79
|
+
raise typer.Exit(130)
|
|
80
|
+
name = name.strip().lower()
|
|
81
|
+
if not validate_account_name(name):
|
|
82
|
+
error("Invalid name. Use lowercase letters, digits, hyphens. Cannot be 'shared'.")
|
|
83
|
+
continue
|
|
84
|
+
if name in accounts:
|
|
85
|
+
error(f"Account '{name}' already exists.")
|
|
86
|
+
continue
|
|
87
|
+
break
|
|
88
|
+
else:
|
|
89
|
+
if not validate_account_name(name):
|
|
90
|
+
error("Invalid account name. Use lowercase letters, digits, hyphens.")
|
|
91
|
+
raise typer.Exit(2)
|
|
92
|
+
if name in accounts:
|
|
93
|
+
error(f"Account '{name}' already exists.")
|
|
94
|
+
raise typer.Exit(2)
|
|
95
|
+
|
|
96
|
+
if not label:
|
|
97
|
+
label = text_input("Display label:")
|
|
98
|
+
if label is None:
|
|
99
|
+
raise typer.Exit(130)
|
|
100
|
+
if not validate_label(label):
|
|
101
|
+
error("Label cannot be empty.")
|
|
102
|
+
raise typer.Exit(2)
|
|
103
|
+
|
|
104
|
+
if not tier:
|
|
105
|
+
tier = select_from_list("Subscription tier:", TIER_CHOICES)
|
|
106
|
+
if tier is None:
|
|
107
|
+
raise typer.Exit(130)
|
|
108
|
+
|
|
109
|
+
if tier not in TIER_CHOICES:
|
|
110
|
+
error(f"Invalid tier '{tier}'. Choose from: {', '.join(TIER_CHOICES)}")
|
|
111
|
+
raise typer.Exit(2)
|
|
112
|
+
|
|
113
|
+
add_account(name, label, tier)
|
|
114
|
+
|
|
115
|
+
console.print("\n Opening browser for Claude login...")
|
|
116
|
+
if trigger_login(name):
|
|
117
|
+
success("Authenticated successfully")
|
|
118
|
+
else:
|
|
119
|
+
error("Login failed or claude binary not found on PATH")
|
|
120
|
+
info(f"You can login later with: claude-cli account login {name}")
|
|
121
|
+
|
|
122
|
+
success(f'Account "{name}" created')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("remove")
|
|
126
|
+
def remove_command(
|
|
127
|
+
name: Optional[str] = typer.Argument(
|
|
128
|
+
None, help="Account name to remove", autocompletion=complete_account_name
|
|
129
|
+
),
|
|
130
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Remove an account profile (credentials only, shared files untouched)."""
|
|
133
|
+
accounts = list_accounts()
|
|
134
|
+
if not accounts:
|
|
135
|
+
info("No accounts configured.")
|
|
136
|
+
raise typer.Exit(0)
|
|
137
|
+
|
|
138
|
+
if not name:
|
|
139
|
+
name = select_account("Select account to remove:")
|
|
140
|
+
if name is None:
|
|
141
|
+
raise typer.Exit(130)
|
|
142
|
+
|
|
143
|
+
if name not in accounts:
|
|
144
|
+
error(f"Account '{name}' not found.")
|
|
145
|
+
raise typer.Exit(4)
|
|
146
|
+
|
|
147
|
+
if not yes:
|
|
148
|
+
console.print(f'\n [warning]\u26a0 This will remove account "{name}" and its credentials.[/warning]')
|
|
149
|
+
console.print(" Shared settings, rules, and memory are NOT affected.\n")
|
|
150
|
+
proceed = confirm("Continue?", default=False)
|
|
151
|
+
if not proceed:
|
|
152
|
+
info("Cancelled.")
|
|
153
|
+
raise typer.Exit(0)
|
|
154
|
+
|
|
155
|
+
remove_account(name)
|
|
156
|
+
success(f'Account "{name}" removed')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command("rename")
|
|
160
|
+
def rename_command(
|
|
161
|
+
old_name: Optional[str] = typer.Argument(
|
|
162
|
+
None, help="Current account name", autocompletion=complete_account_name
|
|
163
|
+
),
|
|
164
|
+
new_name: Optional[str] = typer.Argument(None, help="New account name"),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Rename an account slug."""
|
|
167
|
+
accounts = list_accounts()
|
|
168
|
+
if not accounts:
|
|
169
|
+
info("No accounts configured.")
|
|
170
|
+
raise typer.Exit(0)
|
|
171
|
+
|
|
172
|
+
if not old_name:
|
|
173
|
+
old_name = select_account("Select account to rename:")
|
|
174
|
+
if old_name is None:
|
|
175
|
+
raise typer.Exit(130)
|
|
176
|
+
|
|
177
|
+
if old_name not in accounts:
|
|
178
|
+
error(f"Account '{old_name}' not found.")
|
|
179
|
+
raise typer.Exit(4)
|
|
180
|
+
|
|
181
|
+
if not new_name:
|
|
182
|
+
new_name = text_input("New account name (slug):")
|
|
183
|
+
if new_name is None:
|
|
184
|
+
raise typer.Exit(130)
|
|
185
|
+
new_name = new_name.strip().lower()
|
|
186
|
+
|
|
187
|
+
if not validate_account_name(new_name):
|
|
188
|
+
error("Invalid new name. Use lowercase letters, digits, hyphens.")
|
|
189
|
+
raise typer.Exit(2)
|
|
190
|
+
if new_name in accounts:
|
|
191
|
+
error(f"Account '{new_name}' already exists.")
|
|
192
|
+
raise typer.Exit(2)
|
|
193
|
+
|
|
194
|
+
rename_account(old_name, new_name)
|
|
195
|
+
success(f"Account renamed: {old_name} \u2192 {new_name}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command("login")
|
|
199
|
+
def login_command(
|
|
200
|
+
name: Optional[str] = typer.Argument(
|
|
201
|
+
None, help="Account name (default: active account)", autocompletion=complete_account_name
|
|
202
|
+
),
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Re-authenticate (refresh OAuth) for an account."""
|
|
205
|
+
accounts = list_accounts()
|
|
206
|
+
|
|
207
|
+
if not name:
|
|
208
|
+
# Try default first, otherwise interactive select
|
|
209
|
+
name = get_default_account()
|
|
210
|
+
if not name:
|
|
211
|
+
if not accounts:
|
|
212
|
+
error("No accounts configured. Run 'claude-cli init' first.")
|
|
213
|
+
raise typer.Exit(2)
|
|
214
|
+
name = select_account("Select account to login:")
|
|
215
|
+
if name is None:
|
|
216
|
+
raise typer.Exit(130)
|
|
217
|
+
|
|
218
|
+
if name not in accounts:
|
|
219
|
+
error(f"Account '{name}' not found.")
|
|
220
|
+
raise typer.Exit(4)
|
|
221
|
+
|
|
222
|
+
console.print(f"\n Opening browser for Claude login (account: {name})...")
|
|
223
|
+
if trigger_login(name):
|
|
224
|
+
success("Authenticated successfully")
|
|
225
|
+
else:
|
|
226
|
+
error("Login failed or claude binary not found on PATH")
|
|
227
|
+
raise typer.Exit(3)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command("current")
|
|
231
|
+
def current_command() -> None:
|
|
232
|
+
"""Show the active account configuration."""
|
|
233
|
+
config = load_config()
|
|
234
|
+
default = config.default
|
|
235
|
+
|
|
236
|
+
if not default or default not in config.accounts:
|
|
237
|
+
info("No active account. Run 'claude-cli use <name>' to set one.")
|
|
238
|
+
raise typer.Exit(0)
|
|
239
|
+
|
|
240
|
+
account = config.accounts[default]
|
|
241
|
+
auth = check_auth_status(default)
|
|
242
|
+
if auth == "valid":
|
|
243
|
+
status = "\u2713 logged in"
|
|
244
|
+
elif auth == "expired":
|
|
245
|
+
status = "\u2717 expired"
|
|
246
|
+
else:
|
|
247
|
+
status = "\u2717 none"
|
|
248
|
+
|
|
249
|
+
print_header("Current Account")
|
|
250
|
+
print_detail("Name", default)
|
|
251
|
+
print_detail("Label", account.label)
|
|
252
|
+
print_detail("Tier", account.tier)
|
|
253
|
+
print_detail("Status", status)
|
|
254
|
+
print_detail("Config Dir", str(get_account_dir(default)))
|
|
255
|
+
print_detail("Shared Dir", str(SHARED_DIR))
|
|
256
|
+
print_detail("Created", format_date(account.created_at))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@app.command("repair")
|
|
260
|
+
def repair_command(
|
|
261
|
+
name: Optional[str] = typer.Argument(
|
|
262
|
+
None, help="Account name (default: all accounts)", autocompletion=complete_account_name
|
|
263
|
+
),
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Repair symlinks for account(s) — re-links shared items like commands, skills, etc."""
|
|
266
|
+
accounts = list_accounts()
|
|
267
|
+
if not accounts:
|
|
268
|
+
info("No accounts configured.")
|
|
269
|
+
raise typer.Exit(0)
|
|
270
|
+
|
|
271
|
+
ensure_shared_dir()
|
|
272
|
+
|
|
273
|
+
targets = [name] if name else accounts
|
|
274
|
+
for acct in targets:
|
|
275
|
+
if acct not in accounts:
|
|
276
|
+
error(f"Account '{acct}' not found.")
|
|
277
|
+
raise typer.Exit(4)
|
|
278
|
+
account_dir = get_account_dir(acct)
|
|
279
|
+
setup_symlinks(account_dir)
|
|
280
|
+
success(f"Repaired symlinks for '{acct}'")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""claude-cli config — Global CLI configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from claude_cli.core.config import ACCOUNTS_DIR, CONFIG_FILE, SHARED_DIR, load_config
|
|
8
|
+
from claude_cli.ui import console, print_detail, print_header
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("show")
|
|
14
|
+
def show_command() -> None:
|
|
15
|
+
"""Show global configuration details."""
|
|
16
|
+
config = load_config()
|
|
17
|
+
print_header("Global Configuration")
|
|
18
|
+
print_detail("Config File", str(CONFIG_FILE))
|
|
19
|
+
print_detail("Shared Dir", str(SHARED_DIR))
|
|
20
|
+
print_detail("Accounts Dir", str(ACCOUNTS_DIR))
|
|
21
|
+
print_detail("Default Account", config.default or "none")
|
|
22
|
+
print_detail("Total Accounts", str(len(config.accounts)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("path")
|
|
26
|
+
def path_command() -> None:
|
|
27
|
+
"""Print the config file path (useful for scripting)."""
|
|
28
|
+
console.print(str(CONFIG_FILE))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""claude-cli history — View command history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from claude_cli.core.history import clear_history, get_history
|
|
10
|
+
from claude_cli.ui import info, success
|
|
11
|
+
from claude_cli.ui.tables import print_history_table
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(invoke_without_command=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.callback(invoke_without_command=True)
|
|
17
|
+
def history_command(
|
|
18
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Number of entries to show"),
|
|
19
|
+
clear: bool = typer.Option(False, "--clear", help="Clear all history"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""View CLI command history."""
|
|
22
|
+
if clear:
|
|
23
|
+
clear_history()
|
|
24
|
+
success("History cleared.")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
entries = get_history(limit=limit)
|
|
28
|
+
if not entries:
|
|
29
|
+
info("No command history yet.")
|
|
30
|
+
raise typer.Exit(0)
|
|
31
|
+
|
|
32
|
+
print_history_table(entries)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""claude-cli init — Interactive setup wizard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from claude_cli.core.account import (
|
|
11
|
+
add_account,
|
|
12
|
+
ensure_shared_dir,
|
|
13
|
+
get_account_dir,
|
|
14
|
+
migrate_existing_claude_dir,
|
|
15
|
+
set_default_account,
|
|
16
|
+
)
|
|
17
|
+
from claude_cli.core.auth import trigger_login
|
|
18
|
+
from claude_cli.core.config import config_exists
|
|
19
|
+
from claude_cli.ui import console, error, info, success
|
|
20
|
+
from claude_cli.ui.prompts import confirm, select_from_list, text_input
|
|
21
|
+
from claude_cli.utils.validators import validate_account_name, validate_label
|
|
22
|
+
|
|
23
|
+
TIER_CHOICES = ["pro", "max", "team", "enterprise"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def init_command() -> None:
|
|
27
|
+
"""Interactive setup wizard — create your first (or next) account."""
|
|
28
|
+
is_first_run = not config_exists()
|
|
29
|
+
|
|
30
|
+
if is_first_run:
|
|
31
|
+
console.print("\n[bold]Welcome to Claude CLI![/bold]")
|
|
32
|
+
console.print(" Let's set up your first account.\n")
|
|
33
|
+
else:
|
|
34
|
+
console.print("\n[bold]Adding a new Claude account profile...[/bold]\n")
|
|
35
|
+
|
|
36
|
+
# Migration offer on first run
|
|
37
|
+
if is_first_run:
|
|
38
|
+
claude_dir = Path.home() / ".claude"
|
|
39
|
+
if claude_dir.exists():
|
|
40
|
+
console.print(f" Existing Claude config found at [highlight]{claude_dir}[/highlight]\n")
|
|
41
|
+
migrate = confirm("Migrate existing config to claude-cli?", default=True)
|
|
42
|
+
if migrate is None:
|
|
43
|
+
raise typer.Exit(130)
|
|
44
|
+
if migrate:
|
|
45
|
+
creds_path = migrate_existing_claude_dir()
|
|
46
|
+
success("Shared files moved to ~/.claude-cli/shared/")
|
|
47
|
+
success("Backup saved to ~/.claude.bak/")
|
|
48
|
+
console.print()
|
|
49
|
+
|
|
50
|
+
# Prompt for account name for existing credentials
|
|
51
|
+
name = _prompt_account_name()
|
|
52
|
+
label = _prompt_label()
|
|
53
|
+
tier = _prompt_tier()
|
|
54
|
+
|
|
55
|
+
account_dir = add_account(name, label, tier)
|
|
56
|
+
|
|
57
|
+
# Move existing credentials into the account
|
|
58
|
+
if creds_path and creds_path.exists():
|
|
59
|
+
shutil.move(str(creds_path), str(account_dir / ".credentials.json"))
|
|
60
|
+
success(f'Account "{name}" created from existing config')
|
|
61
|
+
else:
|
|
62
|
+
info("No existing credentials found. Logging in...")
|
|
63
|
+
if not trigger_login(name):
|
|
64
|
+
error("Login failed or claude binary not found")
|
|
65
|
+
|
|
66
|
+
set_default_account(name)
|
|
67
|
+
success("Set as default")
|
|
68
|
+
|
|
69
|
+
# Offer to add another
|
|
70
|
+
console.print()
|
|
71
|
+
another = confirm("Add another account now?", default=False)
|
|
72
|
+
if another:
|
|
73
|
+
_add_new_account()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Normal flow: add a new account
|
|
77
|
+
ensure_shared_dir()
|
|
78
|
+
_add_new_account()
|
|
79
|
+
|
|
80
|
+
# On first run, auto-set as default
|
|
81
|
+
if is_first_run:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
set_as_default = confirm("Set as default account?", default=True)
|
|
85
|
+
if set_as_default is None:
|
|
86
|
+
raise typer.Exit(130)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _add_new_account() -> None:
|
|
90
|
+
"""Prompt for account details and create it."""
|
|
91
|
+
name = _prompt_account_name()
|
|
92
|
+
label = _prompt_label()
|
|
93
|
+
tier = _prompt_tier()
|
|
94
|
+
|
|
95
|
+
add_account(name, label, tier)
|
|
96
|
+
|
|
97
|
+
console.print("\n Opening browser for Claude login...")
|
|
98
|
+
if trigger_login(name):
|
|
99
|
+
success("Authenticated successfully")
|
|
100
|
+
else:
|
|
101
|
+
error("Login failed or claude binary not found on PATH")
|
|
102
|
+
info("You can login later with: claude-cli account login " + name)
|
|
103
|
+
|
|
104
|
+
set_default_account(name)
|
|
105
|
+
success(f'Account "{name}" created and set as default')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _prompt_account_name() -> str:
|
|
109
|
+
"""Prompt for a valid account name slug."""
|
|
110
|
+
while True:
|
|
111
|
+
name = text_input("Account name (slug):")
|
|
112
|
+
if name is None:
|
|
113
|
+
raise typer.Exit(130)
|
|
114
|
+
name = name.strip().lower()
|
|
115
|
+
if validate_account_name(name):
|
|
116
|
+
return name
|
|
117
|
+
error("Invalid name. Use lowercase letters, digits, hyphens. Cannot be 'shared'.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _prompt_label() -> str:
|
|
121
|
+
"""Prompt for an account display label."""
|
|
122
|
+
while True:
|
|
123
|
+
label = text_input("Display label:")
|
|
124
|
+
if label is None:
|
|
125
|
+
raise typer.Exit(130)
|
|
126
|
+
if validate_label(label):
|
|
127
|
+
return label.strip()
|
|
128
|
+
error("Label cannot be empty.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _prompt_tier() -> str:
|
|
132
|
+
"""Prompt for subscription tier."""
|
|
133
|
+
tier = select_from_list("Subscription tier:", TIER_CHOICES)
|
|
134
|
+
if tier is None:
|
|
135
|
+
raise typer.Exit(130)
|
|
136
|
+
return tier
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""claude-cli run — Launch Claude Code with account selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from claude_cli.core.account import get_account_dir, get_default_account, list_accounts
|
|
12
|
+
from claude_cli.ui import error, info
|
|
13
|
+
from claude_cli.ui.prompts import select_account
|
|
14
|
+
from claude_cli.utils.completers import complete_account_name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _launch(name: str, args: list[str]) -> None:
|
|
18
|
+
"""Exec into claude with the given account's config dir."""
|
|
19
|
+
accounts = list_accounts()
|
|
20
|
+
if name not in accounts:
|
|
21
|
+
error(f"Account '{name}' not found.")
|
|
22
|
+
raise typer.Exit(4)
|
|
23
|
+
|
|
24
|
+
claude_bin = shutil.which("claude")
|
|
25
|
+
if not claude_bin:
|
|
26
|
+
error("'claude' binary not found on PATH.")
|
|
27
|
+
info("Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code")
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
account_dir = get_account_dir(name)
|
|
31
|
+
env = os.environ.copy()
|
|
32
|
+
env["CLAUDE_CONFIG_DIR"] = str(account_dir)
|
|
33
|
+
os.execvpe(claude_bin, [claude_bin] + args, env)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def quick_run_command(
|
|
37
|
+
args: Optional[list[str]] = typer.Argument(None, help="Arguments passed through to claude"),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Launch Claude Code with the current default account (no prompt)."""
|
|
40
|
+
accounts = list_accounts()
|
|
41
|
+
if not accounts:
|
|
42
|
+
error("No accounts configured. Run 'claude-cli init' first.")
|
|
43
|
+
raise typer.Exit(2)
|
|
44
|
+
|
|
45
|
+
name = get_default_account()
|
|
46
|
+
if not name:
|
|
47
|
+
error("No default account set. Run 'claude-cli use <name>' or 'claude-cli run'.")
|
|
48
|
+
raise typer.Exit(2)
|
|
49
|
+
|
|
50
|
+
_launch(name, list(args) if args else [])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_command(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
account: Optional[str] = typer.Option(
|
|
56
|
+
None, "--account", "-a",
|
|
57
|
+
help="Account to use (skip interactive prompt)",
|
|
58
|
+
autocompletion=complete_account_name,
|
|
59
|
+
),
|
|
60
|
+
args: Optional[list[str]] = typer.Argument(None, help="Arguments passed through to claude"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Launch Claude Code — interactively select account (or use -a to specify)."""
|
|
63
|
+
if ctx.invoked_subcommand is not None:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
accounts = list_accounts()
|
|
67
|
+
if not accounts:
|
|
68
|
+
error("No accounts configured. Run 'claude-cli init' first.")
|
|
69
|
+
raise typer.Exit(2)
|
|
70
|
+
|
|
71
|
+
if account:
|
|
72
|
+
name = account
|
|
73
|
+
else:
|
|
74
|
+
name = select_account("Select account:", default=get_default_account())
|
|
75
|
+
if name is None:
|
|
76
|
+
raise typer.Exit(130)
|
|
77
|
+
|
|
78
|
+
_launch(name, list(args) if args else [])
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""claude-cli status — Dashboard overview of all accounts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from claude_cli.core.auth import check_auth_status
|
|
8
|
+
from claude_cli.core.config import SHARED_DIR, load_config
|
|
9
|
+
from claude_cli.ui import console, info
|
|
10
|
+
from claude_cli.ui.tables import print_status_table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def status_command() -> None:
|
|
14
|
+
"""Quick overview of all accounts — a dashboard view."""
|
|
15
|
+
config = load_config()
|
|
16
|
+
|
|
17
|
+
if not config.accounts:
|
|
18
|
+
info("No accounts configured. Run 'claude-cli init' to get started.")
|
|
19
|
+
raise typer.Exit(0)
|
|
20
|
+
|
|
21
|
+
auth_statuses = {name: check_auth_status(name) for name in config.accounts}
|
|
22
|
+
print_status_table(config, auth_statuses)
|
|
23
|
+
|
|
24
|
+
# Summary line
|
|
25
|
+
active = config.default or "none"
|
|
26
|
+
total = len(config.accounts)
|
|
27
|
+
console.print(f" Active: [bold]{active}[/bold] | {total} account(s) configured")
|
|
28
|
+
|
|
29
|
+
# Shared files check
|
|
30
|
+
shared_items = {
|
|
31
|
+
"settings": SHARED_DIR / "settings.json",
|
|
32
|
+
"rules": SHARED_DIR / "rules",
|
|
33
|
+
"agents": SHARED_DIR / "agents",
|
|
34
|
+
"MCP": SHARED_DIR / ".claude.json",
|
|
35
|
+
}
|
|
36
|
+
checks = []
|
|
37
|
+
for label, path in shared_items.items():
|
|
38
|
+
marker = "\u2713" if path.exists() else "\u2717"
|
|
39
|
+
checks.append(f"{label} {marker}")
|
|
40
|
+
console.print(f" Shared: {' '.join(checks)}", style="dim")
|
|
41
|
+
|
|
42
|
+
console.print()
|
|
43
|
+
console.print(' Run "claude-cli use <name>" to switch.', style="dim")
|
|
44
|
+
console.print(' Run "claude-cli account login <name>" to re-authenticate.', style="dim")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""claude-cli usage — View usage information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from claude_cli.core.account import get_default_account, list_accounts
|
|
10
|
+
from claude_cli.core.usage import get_all_usage_info, get_usage_info, open_usage_page
|
|
11
|
+
from claude_cli.ui import console, error, info
|
|
12
|
+
from claude_cli.ui.prompts import select_account
|
|
13
|
+
from claude_cli.ui.tables import print_usage_detail, print_usage_summary_table
|
|
14
|
+
from claude_cli.utils.completers import complete_account_name
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("show")
|
|
20
|
+
def show_command(
|
|
21
|
+
name: Optional[str] = typer.Argument(
|
|
22
|
+
None, help="Account name", autocompletion=complete_account_name
|
|
23
|
+
),
|
|
24
|
+
all_accounts: bool = typer.Option(False, "--all", "-a", help="Show all accounts"),
|
|
25
|
+
output: Optional[str] = typer.Option(None, "--output", "-O", help="Output format: table, json"),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Show usage for one or all accounts."""
|
|
28
|
+
if all_accounts:
|
|
29
|
+
usage_list = get_all_usage_info()
|
|
30
|
+
if not usage_list:
|
|
31
|
+
info("No accounts configured.")
|
|
32
|
+
raise typer.Exit(0)
|
|
33
|
+
|
|
34
|
+
if output == "json":
|
|
35
|
+
import json
|
|
36
|
+
|
|
37
|
+
data = [u.model_dump(mode="json") for u in usage_list]
|
|
38
|
+
console.print_json(json.dumps(data))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
print_usage_summary_table(usage_list)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Single account
|
|
45
|
+
if not name:
|
|
46
|
+
name = get_default_account()
|
|
47
|
+
if not name:
|
|
48
|
+
accounts = list_accounts()
|
|
49
|
+
if not accounts:
|
|
50
|
+
error("No accounts configured.")
|
|
51
|
+
raise typer.Exit(2)
|
|
52
|
+
name = select_account("Select account:")
|
|
53
|
+
if name is None:
|
|
54
|
+
raise typer.Exit(130)
|
|
55
|
+
|
|
56
|
+
accounts = list_accounts()
|
|
57
|
+
if name not in accounts:
|
|
58
|
+
error(f"Account '{name}' not found.")
|
|
59
|
+
raise typer.Exit(4)
|
|
60
|
+
|
|
61
|
+
usage = get_usage_info(name)
|
|
62
|
+
|
|
63
|
+
if output == "json":
|
|
64
|
+
import json
|
|
65
|
+
|
|
66
|
+
console.print_json(json.dumps(usage.model_dump(mode="json")))
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
print_usage_detail(usage)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("open")
|
|
73
|
+
def open_command(
|
|
74
|
+
name: Optional[str] = typer.Argument(
|
|
75
|
+
None, help="Account name", autocompletion=complete_account_name
|
|
76
|
+
),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Open the Claude usage page in the browser."""
|
|
79
|
+
open_usage_page()
|
|
80
|
+
info("Opened Claude usage page in browser.")
|