@bordoni/claudep 0.3.0 → 0.4.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/CHANGELOG.md +28 -1
- package/README.md +21 -1
- package/claudep.ts +368 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.1] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `claudep doctor` warns when your Claude Code asks for approval on every auto-memory write in a profile. Since 2.1.280 a write through the shared `projects/` symlink is judged at `~/.claude/projects`, which auto mode cannot approve ([anthropics/claude-code#98044](https://github.com/anthropics/claude-code/issues/98044)). The README explains it.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Caught up with Claude Code 2.1.280, whose list of per-instance files grew. `state/`, `policy-limits.json.stamp.json`, the `.signature.json` files next to `policy-limits.json` and `remote-settings.json`, `daemon.lock`, `shares/`, `storage-v2/` and about twenty-five caches and locks are known-private, so `doctor` stops calling them unclassified.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `loop.md`, which Claude Code keeps per instance, is no longer shared by the rule that links every top-level `*.md`. `claudep doctor` names any `loop.md` symlink an older claudep left in a profile and tells you to remove it.
|
|
22
|
+
- `claudep doctor` on Linux and Windows no longer calls a profile "not logged in yet" when it has no `.credentials.json` but Claude Code reports it logged in. Claude Code can keep the login elsewhere, such as Windows Credential Manager behind a feature flag.
|
|
23
|
+
|
|
24
|
+
## [0.4.0] - 2026-09-14
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `claudep completion zsh|bash|fish` prints tab completions for every subcommand, its flags, the values `--shell`, `shell-init` and `completion` take, and your profile names. Load it with `eval "$(claudep completion zsh)"` after `compinit`, `eval "$(claudep completion bash)"`, or `claudep completion fish | source`. Profile names are read from `~/.claudep` when you press Tab, so the script never runs claudep.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Breaking:** `completion` is now a reserved word. A profile named `completion` stops resolving; recreate it under another name. Reserved words, flag parsing and the completion scripts now come from one command table, so they cannot drift apart.
|
|
33
|
+
|
|
9
34
|
## [0.3.0] - 2026-09-10
|
|
10
35
|
|
|
11
36
|
### Added
|
|
@@ -76,7 +101,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
76
101
|
- Keychain isolation per config dir verified against Claude Code 2.1.259: the service name is `Claude Code-credentials-<sha256(dir)[0:8]>`.
|
|
77
102
|
- Test suite with `bun test`, a sandboxed `$HOME`, a fake `claude` and `security` on PATH, and a real-shell test for the hook. CI runs typecheck, lint and tests on macOS and Linux.
|
|
78
103
|
|
|
79
|
-
[Unreleased]: https://github.com/bordoni/claudep/compare/0.
|
|
104
|
+
[Unreleased]: https://github.com/bordoni/claudep/compare/0.4.1...HEAD
|
|
105
|
+
[0.4.1]: https://github.com/bordoni/claudep/compare/0.4.0...0.4.1
|
|
106
|
+
[0.4.0]: https://github.com/bordoni/claudep/compare/0.3.0...0.4.0
|
|
80
107
|
[0.3.0]: https://github.com/bordoni/claudep/compare/0.2.1...0.3.0
|
|
81
108
|
[0.2.1]: https://github.com/bordoni/claudep/compare/0.2.0...0.2.1
|
|
82
109
|
[0.2.0]: https://github.com/bordoni/claudep/compare/0.1.1...0.2.0
|
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ claudep rm <name> [--keep-login] log out and delete a profile (base is never
|
|
|
64
64
|
claudep local <name> | --remove pin the current directory tree to a profile (see below)
|
|
65
65
|
claudep resolve [dir] print the profile pinned for a directory
|
|
66
66
|
claudep shell-init [zsh|bash|fish|powershell] print the hook that applies pins on cd
|
|
67
|
+
claudep completion [zsh|bash|fish] print tab completions for commands, flags and profile names
|
|
67
68
|
claudep --version
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -104,6 +105,21 @@ From then on, `cd` into a pinned tree sets `CLAUDE_CONFIG_DIR` for that profile
|
|
|
104
105
|
|
|
105
106
|
`claudep current` tells you which profile the shell is on and how it got there (hook, manual pin, or nothing). `claudep list` adds the same line at the bottom.
|
|
106
107
|
|
|
108
|
+
## Tab completion
|
|
109
|
+
|
|
110
|
+
One line in your rc file completes subcommands, their flags, the values `--shell` and `shell-init` take, and your profile names:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
eval "$(claudep completion zsh)" # ~/.zshrc, after compinit
|
|
114
|
+
eval "$(claudep completion bash)" # ~/.bashrc
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```fish
|
|
118
|
+
claudep completion fish | source # ~/.config/fish/config.fish
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Profile names come from a directory listing of `~/.claudep` at the moment you press Tab, so the script never runs claudep and a new profile shows up without reloading anything. In zsh the same output also works as a file: `claudep completion zsh > ~/.zfunc/_claudep` with `~/.zfunc` on `fpath` before `compinit`, if you prefer autoloading over `eval`.
|
|
122
|
+
|
|
107
123
|
## Show the profile in your prompt or statusline
|
|
108
124
|
|
|
109
125
|
In a shell prompt use the variable itself; no command runs:
|
|
@@ -139,7 +155,11 @@ The shared list is an allowlist, so an account-specific file cannot leak across
|
|
|
139
155
|
|
|
140
156
|
- The first time you open a repository under a new profile you will re-accept folder trust, and claude.ai connectors need their OAuth redone in that profile.
|
|
141
157
|
- Two profiles running at the same time write the same `settings.json` and `plugins/`. That is the same situation as two terminals today.
|
|
142
|
-
- Background sessions and the daemon are tied to `~/.claude`. As of Claude Code 2.1.
|
|
158
|
+
- Background sessions and the daemon are tied to `~/.claude`. As of Claude Code 2.1.280, `claude daemon install` refuses to run with `CLAUDE_CONFIG_DIR` set, and the launcher skips the daemon for `claude --bg` under a profile. Anthropic's docs describe a separate background supervisor per config dir; I have not verified that here.
|
|
159
|
+
- Since Claude Code 2.1.280, every auto-memory write in a profile asks for approval, and auto mode cannot give it. The memory directory sits under the shared `projects/` symlink, and Claude Code now checks a write at the path it lands on, `~/.claude/projects/...`, which it treats as sensitive. This is [anthropics/claude-code#98044](https://github.com/anthropics/claude-code/issues/98044), open upstream. `claudep doctor` warns while your Claude Code is affected. The default profile is not.
|
|
160
|
+
- Skills and plugins synced from claude.ai land in the shared `skills/synced/` and `plugins/synced/`, in one folder per account, so profiles do not mix them. The `syncClaudeAiSkills` and `syncClaudeAiPlugins` switches live in the shared `settings.json` and apply to every profile.
|
|
161
|
+
- Console sign-ins made without an API key live in `~/.config/anthropic`, outside every config dir, so all profiles see the same ones. `ANTHROPIC_PROFILE` picks which one Claude Code uses.
|
|
162
|
+
- In VS Code, `CLAUDE_CONFIG_DIR` in `claudeCode.environmentVariables` must be an absolute path. The session list ignores it when `claudeProcessWrapper` is set.
|
|
143
163
|
- Set `CLAUDE_PROFILES_DIR` to move the profiles root. Keep it out of iCloud or Dropbox; `.claude.json` is rewritten constantly and sync tools create conflict copies.
|
|
144
164
|
- Never put `CLAUDE_CONFIG_DIR` in a `settings.json` `env` block. Claude Code detects that mismatch and disables features; `claudep doctor` reports it.
|
|
145
165
|
- `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN` override the login in any config dir, always in `-p` mode. `claudep <name>` and `claudep doctor` warn when one is set and leave it alone.
|
package/claudep.ts
CHANGED
|
@@ -250,8 +250,9 @@ export const SHARED_DIRS = [
|
|
|
250
250
|
"workflows",
|
|
251
251
|
] as const;
|
|
252
252
|
|
|
253
|
-
/** Per-profile state. First block is Claude Code's own runtime-state list
|
|
254
|
-
*
|
|
253
|
+
/** Per-profile state. First block is Claude Code's own runtime-state list as of
|
|
254
|
+
* 2.1.263, then observed extras, then what 2.1.280 added to that list. Never
|
|
255
|
+
* shared, and a name here is never shared by the top-level *.md rule either. */
|
|
255
256
|
export const KNOWN_PRIVATE = new Set<string>([
|
|
256
257
|
".claude.json",
|
|
257
258
|
".claude.json.backup",
|
|
@@ -301,6 +302,42 @@ export const KNOWN_PRIVATE = new Set<string>([
|
|
|
301
302
|
"launch.json",
|
|
302
303
|
"scheduled_tasks.json",
|
|
303
304
|
"seed-admin",
|
|
305
|
+
// Added to Claude Code's runtime-state list by 2.1.280 (checked 2026-09-30).
|
|
306
|
+
"state",
|
|
307
|
+
"policy-limits.json.stamp.json",
|
|
308
|
+
"policy-limits.json.signature.json",
|
|
309
|
+
"policy-limits.json.signature-iat.json",
|
|
310
|
+
"remote-settings.json.signature.json",
|
|
311
|
+
"remote-settings.json.signature-iat.json",
|
|
312
|
+
"remote-settings-consent.json",
|
|
313
|
+
"remote-settings-helper-consent",
|
|
314
|
+
"hfi-auth.json",
|
|
315
|
+
"shares",
|
|
316
|
+
"storage-v2",
|
|
317
|
+
"daemon.lock",
|
|
318
|
+
"gh-pr-status-cache.json",
|
|
319
|
+
"active-time.json",
|
|
320
|
+
"server-sessions.json",
|
|
321
|
+
"image-cache",
|
|
322
|
+
"file-transfers",
|
|
323
|
+
"computer-use.lock",
|
|
324
|
+
"server.lock",
|
|
325
|
+
"downloads",
|
|
326
|
+
"scratch",
|
|
327
|
+
"traces",
|
|
328
|
+
"startup-perf",
|
|
329
|
+
"feedback-bundles",
|
|
330
|
+
"ccr",
|
|
331
|
+
"bridge-spawn",
|
|
332
|
+
"local-settings",
|
|
333
|
+
"project-settings",
|
|
334
|
+
"remote",
|
|
335
|
+
"systemd",
|
|
336
|
+
"api-dumps",
|
|
337
|
+
"dump-prompts",
|
|
338
|
+
"antproto.json",
|
|
339
|
+
".cc-writes",
|
|
340
|
+
"loop.md",
|
|
304
341
|
]);
|
|
305
342
|
|
|
306
343
|
/** Keys copied from the base .claude.json into a fresh profile so first-run
|
|
@@ -317,27 +354,87 @@ export const SEED_KEYS = [
|
|
|
317
354
|
] as const;
|
|
318
355
|
|
|
319
356
|
export const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
357
|
+
|
|
358
|
+
/** Shells with a directory-pin hook, and the subset with tab completions. */
|
|
359
|
+
export type Shell = "zsh" | "bash" | "fish" | "powershell";
|
|
360
|
+
export const SHELLS: readonly Shell[] = ["zsh", "bash", "fish", "powershell"];
|
|
361
|
+
export const COMPLETION_SHELLS = ["zsh", "bash", "fish"] as const;
|
|
362
|
+
export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
|
|
363
|
+
/** What `claudep env --shell` accepts. */
|
|
364
|
+
export const ENV_SHELLS = ["sh", "zsh", "bash", "fish", "powershell"] as const;
|
|
365
|
+
|
|
366
|
+
export type ValueFlag = { name: string; values?: readonly string[] };
|
|
367
|
+
export type Command = {
|
|
368
|
+
name: string;
|
|
369
|
+
/** One line, no colons: it becomes the zsh _describe and fish -d text. */
|
|
370
|
+
desc: string;
|
|
371
|
+
aliases?: readonly string[];
|
|
372
|
+
/** What the first positional completes to. */
|
|
373
|
+
arg?: "profile" | "dir" | readonly string[];
|
|
374
|
+
flags?: readonly string[];
|
|
375
|
+
valueFlags?: readonly ValueFlag[];
|
|
376
|
+
/** A real command that the top-level completion does not offer. */
|
|
377
|
+
hidden?: boolean;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/** One table for the dispatcher's reserved words, every parseFlags call and
|
|
381
|
+
* the completion scripts, so none of them can drift from the others. */
|
|
382
|
+
export const COMMANDS: readonly Command[] = [
|
|
383
|
+
{
|
|
384
|
+
name: "init",
|
|
385
|
+
desc: "create or update a profile and log in",
|
|
386
|
+
arg: "profile",
|
|
387
|
+
flags: ["--sso", "--console", "--copy-mcp", "--no-login", "--force"],
|
|
388
|
+
valueFlags: [{ name: "--email" }, { name: "--alias" }],
|
|
389
|
+
},
|
|
390
|
+
{ name: "run", desc: "run claude with a profile", arg: "profile", hidden: true },
|
|
391
|
+
{ name: "list", desc: "show every profile and who it is logged in as", aliases: ["ls"], flags: ["--json"] },
|
|
392
|
+
{ name: "status", desc: "login state for one profile", arg: "profile", flags: ["--json"] },
|
|
393
|
+
{ name: "current", desc: "which profile this shell is on, and why", flags: ["--json", "--name"] },
|
|
394
|
+
{
|
|
395
|
+
name: "env",
|
|
396
|
+
desc: "print the CLAUDE_CONFIG_DIR pin, or the unset, for your shell",
|
|
397
|
+
arg: "profile",
|
|
398
|
+
flags: ["--unset"],
|
|
399
|
+
valueFlags: [{ name: "--shell", values: ENV_SHELLS }],
|
|
400
|
+
},
|
|
401
|
+
{ name: "alias", desc: "write a shim command that runs claudep with a profile", arg: "profile" },
|
|
402
|
+
{ name: "doctor", desc: "verify symlinks, keychain entry, unclassified files, Claude Code version", arg: "profile" },
|
|
403
|
+
{
|
|
404
|
+
name: "rm",
|
|
405
|
+
desc: "log out and delete a profile",
|
|
406
|
+
aliases: ["remove"],
|
|
407
|
+
arg: "profile",
|
|
408
|
+
flags: ["--keep-login", "--yes"],
|
|
409
|
+
},
|
|
410
|
+
{ name: "local", desc: "pin this directory tree to a profile", arg: "profile", flags: ["--remove", "--force"] },
|
|
411
|
+
{ name: "resolve", desc: "print the profile pinned for a directory", arg: "dir", flags: ["--json"] },
|
|
412
|
+
{ name: "shell-init", desc: "print the hook that applies pins on cd", arg: SHELLS },
|
|
413
|
+
{ name: "completion", desc: "print tab completions for a shell", arg: COMPLETION_SHELLS },
|
|
414
|
+
{ name: "help", desc: "show the help" },
|
|
415
|
+
{ name: "version", desc: "print the version", aliases: ["--version"] },
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
/** The words a completion offers for a command: its name and the aliases
|
|
419
|
+
* that are words (a `--version` alias is a flag, not a word). */
|
|
420
|
+
export function commandWords(c: Command): string[] {
|
|
421
|
+
return [c.name, ...(c.aliases ?? [])].filter((w) => !w.startsWith("-"));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Profile names that would collide with the dispatcher. Derived from the
|
|
425
|
+
* table plus the two names for the base. */
|
|
320
426
|
export const RESERVED = new Set([
|
|
321
427
|
"default",
|
|
322
428
|
"base",
|
|
323
|
-
|
|
324
|
-
"run",
|
|
325
|
-
"list",
|
|
326
|
-
"ls",
|
|
327
|
-
"status",
|
|
328
|
-
"env",
|
|
329
|
-
"doctor",
|
|
330
|
-
"rm",
|
|
331
|
-
"remove",
|
|
332
|
-
"alias",
|
|
333
|
-
"help",
|
|
334
|
-
"current",
|
|
335
|
-
"local",
|
|
336
|
-
"resolve",
|
|
337
|
-
"shell-init",
|
|
338
|
-
"version",
|
|
429
|
+
...COMMANDS.flatMap((c) => [c.name, ...(c.aliases ?? [])]).filter((w) => NAME_RE.test(w)),
|
|
339
430
|
]);
|
|
340
431
|
|
|
432
|
+
/** The boolean and value flag names a command accepts, for parseFlags. */
|
|
433
|
+
export function flagSpec(name: string): [readonly string[], readonly string[]] {
|
|
434
|
+
const c = COMMANDS.find((x) => x.name === name);
|
|
435
|
+
return [c?.flags ?? [], (c?.valueFlags ?? []).map((v) => v.name)];
|
|
436
|
+
}
|
|
437
|
+
|
|
341
438
|
// ---------------------------------------------------------------------------
|
|
342
439
|
// Output helpers
|
|
343
440
|
// ---------------------------------------------------------------------------
|
|
@@ -402,7 +499,8 @@ export function sharedItems(base: string): SharedItem[] {
|
|
|
402
499
|
for (const f of SHARED_FILES) push(f, "file");
|
|
403
500
|
if (existsSync(base)) {
|
|
404
501
|
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
405
|
-
if (entry.name.endsWith(".md") && !entry.isDirectory()
|
|
502
|
+
if (entry.name.endsWith(".md") && !entry.isDirectory() && !KNOWN_PRIVATE.has(entry.name))
|
|
503
|
+
push(entry.name, "file");
|
|
406
504
|
}
|
|
407
505
|
}
|
|
408
506
|
for (const d of SHARED_DIRS) push(d, "dir");
|
|
@@ -497,6 +595,18 @@ export function linkState(
|
|
|
497
595
|
return existsSync(dest) ? "ok" : "broken";
|
|
498
596
|
}
|
|
499
597
|
|
|
598
|
+
/** Known-private names in a profile that are still symlinks into the base.
|
|
599
|
+
* An older claudep linked every top-level *.md, which once included loop.md;
|
|
600
|
+
* link() never deletes, so doctor names them for the user to remove. */
|
|
601
|
+
export function staleSharedLinks(base: string, dir: string, platform: NodeJS.Platform = process.platform): string[] {
|
|
602
|
+
if (!existsSync(dir)) return [];
|
|
603
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
604
|
+
.filter((e) => e.isSymbolicLink() && KNOWN_PRIVATE.has(e.name))
|
|
605
|
+
.filter((e) => samePath(readlinkSync(join(dir, e.name)), join(base, e.name), platform))
|
|
606
|
+
.map((e) => e.name)
|
|
607
|
+
.sort();
|
|
608
|
+
}
|
|
609
|
+
|
|
500
610
|
/** What to tell the user when the OS refused to create a symlink. */
|
|
501
611
|
export function symlinkDeniedHint(platform: NodeJS.Platform, name: string): string {
|
|
502
612
|
if (platform === "win32")
|
|
@@ -538,8 +648,23 @@ export function authEnvOverrides(env: Env = process.env, platform: NodeJS.Platfo
|
|
|
538
648
|
* dir. Before it every config dir shared one login. */
|
|
539
649
|
export const MIN_CLAUDE_VERSION = "2.1.144";
|
|
540
650
|
|
|
651
|
+
/** From this Claude Code on, a write through a symlink is also judged at the
|
|
652
|
+
* path it lands on. Auto memory in a profile whose projects/ is shared lands
|
|
653
|
+
* in ~/.claude/projects and asks for approval on every write, which auto mode
|
|
654
|
+
* cannot give (anthropics/claude-code#98044). No fixed version yet. */
|
|
655
|
+
export const MEMORY_SYMLINK_PROMPT_FROM = "2.1.280";
|
|
656
|
+
export const MEMORY_SYMLINK_ISSUE = "https://github.com/anthropics/claude-code/issues/98044";
|
|
657
|
+
|
|
541
658
|
export type Version = [number, number, number];
|
|
542
659
|
|
|
660
|
+
/** True when this Claude Code prompts on every auto-memory write through the
|
|
661
|
+
* shared projects/ symlink. An unknown version is not reported. */
|
|
662
|
+
export function memoryWritesPrompt(v: Version | undefined, shared: readonly SharedItem[]): boolean {
|
|
663
|
+
const from = parseVersion(MEMORY_SYMLINK_PROMPT_FROM);
|
|
664
|
+
if (!v || !from || versionBelow(v, from)) return false;
|
|
665
|
+
return shared.some((s) => s.name === "projects");
|
|
666
|
+
}
|
|
667
|
+
|
|
543
668
|
/** The first x.y.z in `claude --version` output. */
|
|
544
669
|
export function parseVersion(text: string): Version | undefined {
|
|
545
670
|
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
@@ -817,7 +942,7 @@ export function parseFlags(args: string[], boolNames: readonly string[], strName
|
|
|
817
942
|
// ---------------------------------------------------------------------------
|
|
818
943
|
|
|
819
944
|
async function cmdInit(L: Layout, args: string[]): Promise<void> {
|
|
820
|
-
const f = parseFlags(args,
|
|
945
|
+
const f = parseFlags(args, ...flagSpec("init"));
|
|
821
946
|
const name = f.rest[0];
|
|
822
947
|
if (!name)
|
|
823
948
|
die(
|
|
@@ -1003,7 +1128,7 @@ function rowJson(row: Row): JsonObject {
|
|
|
1003
1128
|
}
|
|
1004
1129
|
|
|
1005
1130
|
async function cmdList(L: Layout, args: string[]): Promise<void> {
|
|
1006
|
-
const f = parseFlags(args,
|
|
1131
|
+
const f = parseFlags(args, ...flagSpec("list"));
|
|
1007
1132
|
const names = ["default", ...listProfileNames(L)];
|
|
1008
1133
|
const rows = await collectRows(L, names);
|
|
1009
1134
|
if (f.bools.has("--json")) {
|
|
@@ -1017,7 +1142,7 @@ async function cmdList(L: Layout, args: string[]): Promise<void> {
|
|
|
1017
1142
|
}
|
|
1018
1143
|
|
|
1019
1144
|
async function cmdStatus(L: Layout, args: string[]): Promise<void> {
|
|
1020
|
-
const f = parseFlags(args,
|
|
1145
|
+
const f = parseFlags(args, ...flagSpec("status"));
|
|
1021
1146
|
const name = f.rest[0];
|
|
1022
1147
|
if (!name) die("usage: claudep status <name> [--json]");
|
|
1023
1148
|
if (name !== "default" && !profileExists(L, name)) die(`profile "${name}" does not exist`);
|
|
@@ -1042,7 +1167,7 @@ export function envUnsetHint(syntax: EnvSyntax): string {
|
|
|
1042
1167
|
}
|
|
1043
1168
|
|
|
1044
1169
|
function cmdEnv(L: Layout, args: string[]): void {
|
|
1045
|
-
const f = parseFlags(args,
|
|
1170
|
+
const f = parseFlags(args, ...flagSpec("env"));
|
|
1046
1171
|
const forced = f.strs.get("--shell");
|
|
1047
1172
|
const syntax =
|
|
1048
1173
|
forced === undefined ? shellSyntax(process.env, L.platform, parentProcessName(L.platform)) : envSyntaxOf(forced);
|
|
@@ -1070,7 +1195,7 @@ export function currentLabel(cur: Current): string {
|
|
|
1070
1195
|
}
|
|
1071
1196
|
|
|
1072
1197
|
function cmdCurrent(L: Layout, args: string[]): void {
|
|
1073
|
-
const f = parseFlags(args,
|
|
1198
|
+
const f = parseFlags(args, ...flagSpec("current"));
|
|
1074
1199
|
const cur = currentProfile(L);
|
|
1075
1200
|
const label = currentLabel(cur);
|
|
1076
1201
|
if (f.bools.has("--name")) {
|
|
@@ -1100,7 +1225,7 @@ function cmdCurrent(L: Layout, args: string[]): void {
|
|
|
1100
1225
|
}
|
|
1101
1226
|
|
|
1102
1227
|
function cmdResolve(L: Layout, args: string[]): void {
|
|
1103
|
-
const f = parseFlags(args,
|
|
1228
|
+
const f = parseFlags(args, ...flagSpec("resolve"));
|
|
1104
1229
|
const start = f.rest[0] ?? process.cwd();
|
|
1105
1230
|
const pin = resolvePin(start);
|
|
1106
1231
|
if (!pin || pin.name === "") process.exit(1);
|
|
@@ -1111,7 +1236,7 @@ function cmdResolve(L: Layout, args: string[]): void {
|
|
|
1111
1236
|
}
|
|
1112
1237
|
|
|
1113
1238
|
function cmdLocal(L: Layout, args: string[]): void {
|
|
1114
|
-
const f = parseFlags(args,
|
|
1239
|
+
const f = parseFlags(args, ...flagSpec("local"));
|
|
1115
1240
|
const file = join(process.cwd(), PIN_FILE);
|
|
1116
1241
|
if (f.bools.has("--remove")) {
|
|
1117
1242
|
if (!existsSync(file)) die(`no ${PIN_FILE} in ${process.cwd()}`);
|
|
@@ -1140,9 +1265,8 @@ function cmdLocal(L: Layout, args: string[]): void {
|
|
|
1140
1265
|
|
|
1141
1266
|
/** The shell hook. Pure parameter expansion and builtins: it runs on every
|
|
1142
1267
|
* directory change (zsh chpwd) or prompt (bash PROMPT_COMMAND), so no
|
|
1143
|
-
* subprocess is allowed here. Logic mirrors resolvePin().
|
|
1144
|
-
|
|
1145
|
-
export const SHELLS: readonly Shell[] = ["zsh", "bash", "fish", "powershell"];
|
|
1268
|
+
* subprocess is allowed here. Logic mirrors resolvePin(). `Shell` and
|
|
1269
|
+
* `SHELLS` live next to the command table. */
|
|
1146
1270
|
|
|
1147
1271
|
/** The shell to name in hints and to default `shell-init` to. */
|
|
1148
1272
|
export function defaultShell(env: Env = process.env, platform: NodeJS.Platform = process.platform): Shell {
|
|
@@ -1423,6 +1547,186 @@ function cmdShellInit(L: Layout, args: string[]): void {
|
|
|
1423
1547
|
process.stdout.write(shellInit(shell as Shell, L.profilesRoot, L.platform));
|
|
1424
1548
|
}
|
|
1425
1549
|
|
|
1550
|
+
// ---------------------------------------------------------------------------
|
|
1551
|
+
// Tab completions, generated from COMMANDS. Profile names come from a glob
|
|
1552
|
+
// over the profiles root at completion time; no script ever runs claudep.
|
|
1553
|
+
// ---------------------------------------------------------------------------
|
|
1554
|
+
|
|
1555
|
+
/** The one rc-file line that loads the completions. */
|
|
1556
|
+
export function completionHint(shell: CompletionShell): string {
|
|
1557
|
+
return shell === "fish" ? "claudep completion fish | source" : `eval "$(claudep completion ${shell})"`;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const visibleCommands = () => COMMANDS.filter((c) => !c.hidden);
|
|
1561
|
+
const isList = (arg: Command["arg"]): arg is readonly string[] => arg !== undefined && typeof arg !== "string";
|
|
1562
|
+
|
|
1563
|
+
export function completionScript(shell: CompletionShell, profilesRoot: string): string {
|
|
1564
|
+
if (shell === "zsh") return zshCompletion(profilesRoot);
|
|
1565
|
+
if (shell === "bash") return bashCompletion(profilesRoot);
|
|
1566
|
+
return fishCompletion(profilesRoot);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Dual-mode: `eval "$(claudep completion zsh)"` after compinit registers it
|
|
1570
|
+
* with compdef; saved as `_claudep` on fpath, the #compdef line and the
|
|
1571
|
+
* funcstack check make it autoload. Names come from a glob with (N/:t):
|
|
1572
|
+
* nothing on an empty root, directories only, basename. */
|
|
1573
|
+
function zshCompletion(profilesRoot: string): string {
|
|
1574
|
+
const q = `'${profilesRoot.replace(/'/g, `'\\''`)}'`;
|
|
1575
|
+
const noColon = (s: string) => s.replace(/:/g, "\\:");
|
|
1576
|
+
const cmds = visibleCommands().flatMap((c) => commandWords(c).map((w) => ` '${w}:${noColon(c.desc)}'`));
|
|
1577
|
+
const cases = COMMANDS.filter((c) => c.arg || c.flags || c.valueFlags).map((c) => {
|
|
1578
|
+
const specs: string[] = [];
|
|
1579
|
+
if (c.arg === "profile") specs.push("'1:profile:_claudep_profiles'");
|
|
1580
|
+
else if (c.arg === "dir") specs.push("'1:directory:_files -/'");
|
|
1581
|
+
else if (isList(c.arg)) specs.push(`'1:shell:(${c.arg.join(" ")})'`);
|
|
1582
|
+
if (c.name === "run") specs.push("'*:claude arguments:'");
|
|
1583
|
+
for (const f of c.flags ?? []) specs.push(`'${f}'`);
|
|
1584
|
+
for (const v of c.valueFlags ?? [])
|
|
1585
|
+
specs.push(`'${v.name}=-:${v.name.slice(2)}:${v.values ? `(${v.values.join(" ")})` : ""}'`);
|
|
1586
|
+
return ` ${commandWords(c).join("|")}) _arguments ${specs.join(" ")} ;;`;
|
|
1587
|
+
});
|
|
1588
|
+
return `#compdef claudep
|
|
1589
|
+
# claudep completions. Load them after compinit in ~/.zshrc: ${completionHint("zsh")}
|
|
1590
|
+
_claudep_profiles() {
|
|
1591
|
+
local -a names
|
|
1592
|
+
names=(default ${q}/*(N/:t))
|
|
1593
|
+
_describe -t profiles 'profile' names
|
|
1594
|
+
}
|
|
1595
|
+
_claudep() {
|
|
1596
|
+
local -a cmds
|
|
1597
|
+
cmds=(
|
|
1598
|
+
${cmds.join("\n")}
|
|
1599
|
+
)
|
|
1600
|
+
_arguments -C '1:command:->cmd' '*::arg:->args'
|
|
1601
|
+
case $state in
|
|
1602
|
+
cmd)
|
|
1603
|
+
_describe -t commands 'claudep command' cmds
|
|
1604
|
+
_claudep_profiles
|
|
1605
|
+
;;
|
|
1606
|
+
args)
|
|
1607
|
+
case $words[1] in
|
|
1608
|
+
${cases.join("\n")}
|
|
1609
|
+
esac
|
|
1610
|
+
;;
|
|
1611
|
+
esac
|
|
1612
|
+
}
|
|
1613
|
+
if [ "\${funcstack[1]}" = "_claudep" ]; then _claudep "$@"; else compdef _claudep claudep; fi
|
|
1614
|
+
`;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** bash 3.2 safe: no mapfile, no compopt. The root's backslashes become
|
|
1618
|
+
* slashes so a Git Bash user with a C:\\ root still globs. */
|
|
1619
|
+
function bashCompletion(profilesRoot: string): string {
|
|
1620
|
+
const q = `'${profilesRoot.replace(/'/g, `'\\''`)}'`;
|
|
1621
|
+
const top = [...visibleCommands().flatMap(commandWords), "default"].join(" ");
|
|
1622
|
+
const flagCases = COMMANDS.filter((c) => c.flags || c.valueFlags).map(
|
|
1623
|
+
(c) =>
|
|
1624
|
+
` ${commandWords(c).join("|")}) words="${[...(c.flags ?? []), ...(c.valueFlags ?? []).map((v) => v.name)].join(" ")}" ;;`,
|
|
1625
|
+
);
|
|
1626
|
+
const valued = COMMANDS.flatMap((c) => (c.valueFlags ?? []).filter((v) => v.values));
|
|
1627
|
+
const valueBranches = valued.map(
|
|
1628
|
+
(v, i) => ` ${i === 0 ? "if" : "elif"} [ "$prev" = "${v.name}" ]; then words="${(v.values ?? []).join(" ")}"`,
|
|
1629
|
+
);
|
|
1630
|
+
const argCases = COMMANDS.filter((c) => c.arg).map((c) => {
|
|
1631
|
+
const names = commandWords(c).join("|");
|
|
1632
|
+
if (c.arg === "profile") return ` ${names}) words="default $(_claudep_profiles)" ;;`;
|
|
1633
|
+
if (c.arg === "dir") return ` ${names}) COMPREPLY=($(compgen -d -- "$cur")); return 0 ;;`;
|
|
1634
|
+
return ` ${names}) words="${isList(c.arg) ? c.arg.join(" ") : ""}" ;;`;
|
|
1635
|
+
});
|
|
1636
|
+
return `# claudep completions. Load them from ~/.bashrc: ${completionHint("bash")}
|
|
1637
|
+
_claudep_profiles() {
|
|
1638
|
+
local root=${q} d
|
|
1639
|
+
root=\${root//\\\\//}
|
|
1640
|
+
for d in "$root"/*/; do
|
|
1641
|
+
d=\${d%/}
|
|
1642
|
+
[ -d "$d" ] && printf '%s\\n' "\${d##*/}"
|
|
1643
|
+
done
|
|
1644
|
+
}
|
|
1645
|
+
_claudep() {
|
|
1646
|
+
local cur prev cmd words=""
|
|
1647
|
+
cur=\${COMP_WORDS[COMP_CWORD]}
|
|
1648
|
+
prev=\${COMP_WORDS[COMP_CWORD-1]}
|
|
1649
|
+
cmd=\${COMP_WORDS[1]}
|
|
1650
|
+
COMPREPLY=()
|
|
1651
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
1652
|
+
COMPREPLY=($(compgen -W "${top} $(_claudep_profiles)" -- "$cur"))
|
|
1653
|
+
return 0
|
|
1654
|
+
fi
|
|
1655
|
+
case $cur in
|
|
1656
|
+
-*)
|
|
1657
|
+
case $cmd in
|
|
1658
|
+
${flagCases.join("\n")}
|
|
1659
|
+
esac
|
|
1660
|
+
;;
|
|
1661
|
+
*)
|
|
1662
|
+
${valueBranches.join("\n")}
|
|
1663
|
+
${valueBranches.length ? "elif" : "if"} [ "$COMP_CWORD" -eq 2 ]; then
|
|
1664
|
+
case $cmd in
|
|
1665
|
+
${argCases.join("\n")}
|
|
1666
|
+
esac
|
|
1667
|
+
fi
|
|
1668
|
+
;;
|
|
1669
|
+
esac
|
|
1670
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
1671
|
+
}
|
|
1672
|
+
complete -F _claudep claudep
|
|
1673
|
+
`;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/** The two conditions are written out over `commandline -opc` instead of
|
|
1677
|
+
* fish's __fish_use_subcommand and __fish_seen_subcommand_from, whose
|
|
1678
|
+
* names moved between versions. `for` over an unmatched glob runs zero
|
|
1679
|
+
* times, so an empty root offers only `default`. */
|
|
1680
|
+
function fishCompletion(profilesRoot: string): string {
|
|
1681
|
+
const lines: string[] = [];
|
|
1682
|
+
for (const c of visibleCommands())
|
|
1683
|
+
for (const w of commandWords(c))
|
|
1684
|
+
lines.push(`complete -c claudep -n '__claudep_at 1' -a ${w} -d ${fishQuote(c.desc)}`);
|
|
1685
|
+
lines.push("complete -c claudep -n '__claudep_at 1' -a '(__claudep_profiles)' -d profile");
|
|
1686
|
+
const profileCmds = COMMANDS.filter((c) => c.arg === "profile")
|
|
1687
|
+
.flatMap(commandWords)
|
|
1688
|
+
.join(" ");
|
|
1689
|
+
lines.push(
|
|
1690
|
+
`complete -c claudep -n '__claudep_cmd ${profileCmds}; and __claudep_at 2' -a '(__claudep_profiles)' -d profile`,
|
|
1691
|
+
);
|
|
1692
|
+
for (const c of COMMANDS) {
|
|
1693
|
+
const ws = commandWords(c).join(" ");
|
|
1694
|
+
if (isList(c.arg))
|
|
1695
|
+
lines.push(`complete -c claudep -n '__claudep_cmd ${ws}; and __claudep_at 2' -a '${c.arg.join(" ")}'`);
|
|
1696
|
+
if (c.arg === "dir")
|
|
1697
|
+
lines.push(`complete -c claudep -n '__claudep_cmd ${ws}; and __claudep_at 2' -a '(__fish_complete_directories)'`);
|
|
1698
|
+
for (const f of c.flags ?? []) lines.push(`complete -c claudep -n '__claudep_cmd ${ws}' -l ${f.slice(2)}`);
|
|
1699
|
+
for (const v of c.valueFlags ?? [])
|
|
1700
|
+
lines.push(
|
|
1701
|
+
`complete -c claudep -n '__claudep_cmd ${ws}' -l ${v.name.slice(2)} -x${v.values ? ` -a '${v.values.join(" ")}'` : ""}`,
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
return `# claudep completions. Load them from ~/.config/fish/config.fish: ${completionHint("fish")}
|
|
1705
|
+
function __claudep_profiles
|
|
1706
|
+
echo default
|
|
1707
|
+
for d in ${fishQuote(profilesRoot)}/*/
|
|
1708
|
+
string replace -r -- '^.*/([^/]+)/$' '$1' $d
|
|
1709
|
+
end
|
|
1710
|
+
end
|
|
1711
|
+
function __claudep_at -a n
|
|
1712
|
+
test (count (commandline -opc)) -eq $n
|
|
1713
|
+
end
|
|
1714
|
+
function __claudep_cmd
|
|
1715
|
+
set -l t (commandline -opc)
|
|
1716
|
+
test (count $t) -ge 2; and contains -- $t[2] $argv
|
|
1717
|
+
end
|
|
1718
|
+
complete -c claudep -f
|
|
1719
|
+
${lines.join("\n")}
|
|
1720
|
+
`;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function cmdCompletion(L: Layout, args: string[]): void {
|
|
1724
|
+
const shell = args[0] ?? defaultShell(process.env, L.platform);
|
|
1725
|
+
if (!COMPLETION_SHELLS.includes(shell as CompletionShell))
|
|
1726
|
+
die(`no completions for "${shell}". Use ${COMPLETION_SHELLS.join(", ")}`);
|
|
1727
|
+
process.stdout.write(completionScript(shell as CompletionShell, L.profilesRoot));
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1426
1730
|
/** A POSIX-style CLAUDE_CONFIG_DIR on Windows is one claude.exe cannot read. */
|
|
1427
1731
|
function msysConfigDirWarning(L: Layout, env: Env = process.env): string | undefined {
|
|
1428
1732
|
const cfg = env.CLAUDE_CONFIG_DIR;
|
|
@@ -1433,6 +1737,7 @@ function msysConfigDirWarning(L: Layout, env: Env = process.env): string | undef
|
|
|
1433
1737
|
async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
|
|
1434
1738
|
const names = args[0] ? [args[0]] : listProfileNames(L);
|
|
1435
1739
|
let problems = 0;
|
|
1740
|
+
let v: Version | undefined;
|
|
1436
1741
|
const launch = findClaude();
|
|
1437
1742
|
if (launch && launch.kind !== "ps1") {
|
|
1438
1743
|
const { out } = await captureClaude(L, undefined, ["--version"]);
|
|
@@ -1441,7 +1746,7 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
|
|
|
1441
1746
|
console.log(
|
|
1442
1747
|
`${c.dim("·")} claude is the npm cmd shim and runs through cmd.exe; the native installer's claude.exe avoids that hop`,
|
|
1443
1748
|
);
|
|
1444
|
-
|
|
1749
|
+
v = parseVersion(out);
|
|
1445
1750
|
const floor = parseVersion(MIN_CLAUDE_VERSION);
|
|
1446
1751
|
if (L.platform === "darwin" && v && floor && versionBelow(v, floor)) {
|
|
1447
1752
|
bad(
|
|
@@ -1476,6 +1781,10 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
|
|
|
1476
1781
|
if (unclassified.length) {
|
|
1477
1782
|
warn(`base items neither shared nor known-private (they stay per-profile): ${unclassified.join(", ")}`);
|
|
1478
1783
|
}
|
|
1784
|
+
if (memoryWritesPrompt(v, shared))
|
|
1785
|
+
warn(
|
|
1786
|
+
`Claude Code ${v?.join(".")} asks for approval on every auto-memory write in a profile, because projects/ is shared through a symlink; auto mode cannot approve it. Known upstream bug: ${MEMORY_SYMLINK_ISSUE}`,
|
|
1787
|
+
);
|
|
1479
1788
|
|
|
1480
1789
|
for (const name of names) {
|
|
1481
1790
|
const dir = profileDir(L, name);
|
|
@@ -1505,13 +1814,23 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
|
|
|
1505
1814
|
ok(`${shared.length} shared item(s) checked`);
|
|
1506
1815
|
const strays = readdirSync(dir).filter((n) => !sharedNames.has(n) && !KNOWN_PRIVATE.has(n) && !n.endsWith(".md"));
|
|
1507
1816
|
if (strays.length) warn(`unexpected private items: ${strays.join(", ")}`);
|
|
1817
|
+
for (const n of staleSharedLinks(L.base, dir, L.platform)) {
|
|
1818
|
+
bad(
|
|
1819
|
+
`${n}: linked to the base by an older claudep, but it is per-profile state now. Remove the symlink: ${join(dir, n)}`,
|
|
1820
|
+
);
|
|
1821
|
+
problems++;
|
|
1822
|
+
}
|
|
1823
|
+
const s = await authStatus(L, dir);
|
|
1508
1824
|
if (L.platform === "darwin") {
|
|
1509
1825
|
const svc = keychainService(dir);
|
|
1510
1826
|
if (await keychainHas(svc)) ok(`keychain item "${svc}" present`);
|
|
1511
1827
|
else warn(`no keychain item "${svc}". Not logged in yet (claudep ${name} auth login)`);
|
|
1512
1828
|
} else if (credentialsFileHas(dir)) ok(".credentials.json present");
|
|
1829
|
+
else if (s.loggedIn)
|
|
1830
|
+
console.log(
|
|
1831
|
+
`${c.dim("·")} no .credentials.json in the profile; Claude Code keeps this login elsewhere (on Windows, possibly Credential Manager)`,
|
|
1832
|
+
);
|
|
1513
1833
|
else warn(`no .credentials.json in the profile. Not logged in yet (claudep ${name} auth login)`);
|
|
1514
|
-
const s = await authStatus(L, dir);
|
|
1515
1834
|
if (s.loggedIn) ok(`logged in as ${s.email ?? "?"} (${s.orgName ?? "?"}, ${s.subscriptionType ?? "?"})`);
|
|
1516
1835
|
else warn("not logged in");
|
|
1517
1836
|
}
|
|
@@ -1523,7 +1842,7 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
|
|
|
1523
1842
|
}
|
|
1524
1843
|
|
|
1525
1844
|
async function cmdRm(L: Layout, args: string[]): Promise<void> {
|
|
1526
|
-
const f = parseFlags(args,
|
|
1845
|
+
const f = parseFlags(args, ...flagSpec("rm"));
|
|
1527
1846
|
const name = f.rest[0];
|
|
1528
1847
|
if (!name) die("usage: claudep rm <name> [--keep-login] [--yes]");
|
|
1529
1848
|
const dir = profileDir(L, name);
|
|
@@ -1582,8 +1901,14 @@ async function cmdRm(L: Layout, args: string[]): Promise<void> {
|
|
|
1582
1901
|
}
|
|
1583
1902
|
|
|
1584
1903
|
function help(L: Layout): void {
|
|
1904
|
+
console.log(helpText(L));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/** Hand-written on purpose: the layout says more than the table can. A test
|
|
1908
|
+
* checks that every command in COMMANDS appears in it. */
|
|
1909
|
+
export function helpText(L: Layout): string {
|
|
1585
1910
|
const root = shortHome(L.profilesRoot, L.home, L.platform);
|
|
1586
|
-
|
|
1911
|
+
return `${c.bold("claudep")} ${c.dim(version())}: run Claude Code under separate accounts on one machine
|
|
1587
1912
|
|
|
1588
1913
|
${c.bold("USAGE")}
|
|
1589
1914
|
claudep <name> [claude args…] run claude with profile <name> (alias for "run")
|
|
@@ -1596,13 +1921,18 @@ ${c.bold("USAGE")}
|
|
|
1596
1921
|
claudep alias <name> <command> write a shim so "<command>" == "claudep <name>"
|
|
1597
1922
|
claudep doctor [name] verify symlinks, keychain entry, unclassified files, Claude Code version
|
|
1598
1923
|
claudep rm <name> [--keep-login] log out and delete a profile (base is never touched)
|
|
1924
|
+
claudep help show this text
|
|
1599
1925
|
claudep --version print the version
|
|
1600
1926
|
|
|
1601
1927
|
${c.bold("DIRECTORY PINS")}
|
|
1602
1928
|
claudep local <name> [--force] write ./${PIN_FILE} so this tree uses <name>; --remove deletes it
|
|
1603
1929
|
claudep local show the pin that applies to the current directory
|
|
1604
1930
|
claudep resolve [dir] [--json] print the profile pinned for a directory (exit 1 when none)
|
|
1605
|
-
claudep shell-init [
|
|
1931
|
+
claudep shell-init [${SHELLS.join("|")}] print the hook that applies pins on cd; load it from your rc file
|
|
1932
|
+
|
|
1933
|
+
${c.bold("TAB COMPLETION")}
|
|
1934
|
+
claudep completion [${COMPLETION_SHELLS.join("|")}] print completions for commands, flags and profile names;
|
|
1935
|
+
load them from your rc file (after compinit in zsh)
|
|
1606
1936
|
|
|
1607
1937
|
${c.bold("INIT OPTIONS")}
|
|
1608
1938
|
--sso force the SSO login flow (Enterprise orgs)
|
|
@@ -1625,6 +1955,7 @@ ${c.bold("EXAMPLES")}
|
|
|
1625
1955
|
eval "$(claudep shell-init zsh)" # in .zshrc: shells follow ${PIN_FILE} pins on cd
|
|
1626
1956
|
${hookHint("fish")} # the same line for config.fish
|
|
1627
1957
|
${hookHint("powershell")} # the same line for $PROFILE
|
|
1958
|
+
${completionHint("zsh")} # in .zshrc after compinit: tab completion
|
|
1628
1959
|
|
|
1629
1960
|
${c.bold("HOW IT WORKS")}
|
|
1630
1961
|
~/.claude stays exactly as it is and remains the "default" profile. Each named profile is a
|
|
@@ -1649,7 +1980,7 @@ ${c.bold("ENVIRONMENT")}
|
|
|
1649
1980
|
CLAUDEP_AUTO set by the hook next to CLAUDE_CONFIG_DIR; marks the pin as hook-managed
|
|
1650
1981
|
|
|
1651
1982
|
${c.dim("claudep is an independent, unofficial tool. It is not affiliated with, endorsed by or supported by Anthropic.")}
|
|
1652
|
-
|
|
1983
|
+
`;
|
|
1653
1984
|
}
|
|
1654
1985
|
|
|
1655
1986
|
// ---------------------------------------------------------------------------
|
|
@@ -1685,6 +2016,8 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
1685
2016
|
return cmdResolve(L, args);
|
|
1686
2017
|
case "shell-init":
|
|
1687
2018
|
return cmdShellInit(L, args);
|
|
2019
|
+
case "completion":
|
|
2020
|
+
return cmdCompletion(L, args);
|
|
1688
2021
|
case "version":
|
|
1689
2022
|
case "--version":
|
|
1690
2023
|
case "-v":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bordoni/claudep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Unofficial, not affiliated with Anthropic. Run Claude Code under separate accounts on one machine (work + personal) with isolated logins and shared config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|