@bordoni/claudep 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/README.md +16 -0
- package/claudep.ts +283 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-09-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
9
19
|
## [0.3.0] - 2026-09-10
|
|
10
20
|
|
|
11
21
|
### Added
|
|
@@ -76,7 +86,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
76
86
|
- Keychain isolation per config dir verified against Claude Code 2.1.259: the service name is `Claude Code-credentials-<sha256(dir)[0:8]>`.
|
|
77
87
|
- 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
88
|
|
|
79
|
-
[Unreleased]: https://github.com/bordoni/claudep/compare/0.
|
|
89
|
+
[Unreleased]: https://github.com/bordoni/claudep/compare/0.4.0...HEAD
|
|
90
|
+
[0.4.0]: https://github.com/bordoni/claudep/compare/0.3.0...0.4.0
|
|
80
91
|
[0.3.0]: https://github.com/bordoni/claudep/compare/0.2.1...0.3.0
|
|
81
92
|
[0.2.1]: https://github.com/bordoni/claudep/compare/0.2.0...0.2.1
|
|
82
93
|
[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:
|
package/claudep.ts
CHANGED
|
@@ -317,27 +317,87 @@ export const SEED_KEYS = [
|
|
|
317
317
|
] as const;
|
|
318
318
|
|
|
319
319
|
export const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
320
|
+
|
|
321
|
+
/** Shells with a directory-pin hook, and the subset with tab completions. */
|
|
322
|
+
export type Shell = "zsh" | "bash" | "fish" | "powershell";
|
|
323
|
+
export const SHELLS: readonly Shell[] = ["zsh", "bash", "fish", "powershell"];
|
|
324
|
+
export const COMPLETION_SHELLS = ["zsh", "bash", "fish"] as const;
|
|
325
|
+
export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
|
|
326
|
+
/** What `claudep env --shell` accepts. */
|
|
327
|
+
export const ENV_SHELLS = ["sh", "zsh", "bash", "fish", "powershell"] as const;
|
|
328
|
+
|
|
329
|
+
export type ValueFlag = { name: string; values?: readonly string[] };
|
|
330
|
+
export type Command = {
|
|
331
|
+
name: string;
|
|
332
|
+
/** One line, no colons: it becomes the zsh _describe and fish -d text. */
|
|
333
|
+
desc: string;
|
|
334
|
+
aliases?: readonly string[];
|
|
335
|
+
/** What the first positional completes to. */
|
|
336
|
+
arg?: "profile" | "dir" | readonly string[];
|
|
337
|
+
flags?: readonly string[];
|
|
338
|
+
valueFlags?: readonly ValueFlag[];
|
|
339
|
+
/** A real command that the top-level completion does not offer. */
|
|
340
|
+
hidden?: boolean;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** One table for the dispatcher's reserved words, every parseFlags call and
|
|
344
|
+
* the completion scripts, so none of them can drift from the others. */
|
|
345
|
+
export const COMMANDS: readonly Command[] = [
|
|
346
|
+
{
|
|
347
|
+
name: "init",
|
|
348
|
+
desc: "create or update a profile and log in",
|
|
349
|
+
arg: "profile",
|
|
350
|
+
flags: ["--sso", "--console", "--copy-mcp", "--no-login", "--force"],
|
|
351
|
+
valueFlags: [{ name: "--email" }, { name: "--alias" }],
|
|
352
|
+
},
|
|
353
|
+
{ name: "run", desc: "run claude with a profile", arg: "profile", hidden: true },
|
|
354
|
+
{ name: "list", desc: "show every profile and who it is logged in as", aliases: ["ls"], flags: ["--json"] },
|
|
355
|
+
{ name: "status", desc: "login state for one profile", arg: "profile", flags: ["--json"] },
|
|
356
|
+
{ name: "current", desc: "which profile this shell is on, and why", flags: ["--json", "--name"] },
|
|
357
|
+
{
|
|
358
|
+
name: "env",
|
|
359
|
+
desc: "print the CLAUDE_CONFIG_DIR pin, or the unset, for your shell",
|
|
360
|
+
arg: "profile",
|
|
361
|
+
flags: ["--unset"],
|
|
362
|
+
valueFlags: [{ name: "--shell", values: ENV_SHELLS }],
|
|
363
|
+
},
|
|
364
|
+
{ name: "alias", desc: "write a shim command that runs claudep with a profile", arg: "profile" },
|
|
365
|
+
{ name: "doctor", desc: "verify symlinks, keychain entry, unclassified files, Claude Code version", arg: "profile" },
|
|
366
|
+
{
|
|
367
|
+
name: "rm",
|
|
368
|
+
desc: "log out and delete a profile",
|
|
369
|
+
aliases: ["remove"],
|
|
370
|
+
arg: "profile",
|
|
371
|
+
flags: ["--keep-login", "--yes"],
|
|
372
|
+
},
|
|
373
|
+
{ name: "local", desc: "pin this directory tree to a profile", arg: "profile", flags: ["--remove", "--force"] },
|
|
374
|
+
{ name: "resolve", desc: "print the profile pinned for a directory", arg: "dir", flags: ["--json"] },
|
|
375
|
+
{ name: "shell-init", desc: "print the hook that applies pins on cd", arg: SHELLS },
|
|
376
|
+
{ name: "completion", desc: "print tab completions for a shell", arg: COMPLETION_SHELLS },
|
|
377
|
+
{ name: "help", desc: "show the help" },
|
|
378
|
+
{ name: "version", desc: "print the version", aliases: ["--version"] },
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
/** The words a completion offers for a command: its name and the aliases
|
|
382
|
+
* that are words (a `--version` alias is a flag, not a word). */
|
|
383
|
+
export function commandWords(c: Command): string[] {
|
|
384
|
+
return [c.name, ...(c.aliases ?? [])].filter((w) => !w.startsWith("-"));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Profile names that would collide with the dispatcher. Derived from the
|
|
388
|
+
* table plus the two names for the base. */
|
|
320
389
|
export const RESERVED = new Set([
|
|
321
390
|
"default",
|
|
322
391
|
"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",
|
|
392
|
+
...COMMANDS.flatMap((c) => [c.name, ...(c.aliases ?? [])]).filter((w) => NAME_RE.test(w)),
|
|
339
393
|
]);
|
|
340
394
|
|
|
395
|
+
/** The boolean and value flag names a command accepts, for parseFlags. */
|
|
396
|
+
export function flagSpec(name: string): [readonly string[], readonly string[]] {
|
|
397
|
+
const c = COMMANDS.find((x) => x.name === name);
|
|
398
|
+
return [c?.flags ?? [], (c?.valueFlags ?? []).map((v) => v.name)];
|
|
399
|
+
}
|
|
400
|
+
|
|
341
401
|
// ---------------------------------------------------------------------------
|
|
342
402
|
// Output helpers
|
|
343
403
|
// ---------------------------------------------------------------------------
|
|
@@ -817,7 +877,7 @@ export function parseFlags(args: string[], boolNames: readonly string[], strName
|
|
|
817
877
|
// ---------------------------------------------------------------------------
|
|
818
878
|
|
|
819
879
|
async function cmdInit(L: Layout, args: string[]): Promise<void> {
|
|
820
|
-
const f = parseFlags(args,
|
|
880
|
+
const f = parseFlags(args, ...flagSpec("init"));
|
|
821
881
|
const name = f.rest[0];
|
|
822
882
|
if (!name)
|
|
823
883
|
die(
|
|
@@ -1003,7 +1063,7 @@ function rowJson(row: Row): JsonObject {
|
|
|
1003
1063
|
}
|
|
1004
1064
|
|
|
1005
1065
|
async function cmdList(L: Layout, args: string[]): Promise<void> {
|
|
1006
|
-
const f = parseFlags(args,
|
|
1066
|
+
const f = parseFlags(args, ...flagSpec("list"));
|
|
1007
1067
|
const names = ["default", ...listProfileNames(L)];
|
|
1008
1068
|
const rows = await collectRows(L, names);
|
|
1009
1069
|
if (f.bools.has("--json")) {
|
|
@@ -1017,7 +1077,7 @@ async function cmdList(L: Layout, args: string[]): Promise<void> {
|
|
|
1017
1077
|
}
|
|
1018
1078
|
|
|
1019
1079
|
async function cmdStatus(L: Layout, args: string[]): Promise<void> {
|
|
1020
|
-
const f = parseFlags(args,
|
|
1080
|
+
const f = parseFlags(args, ...flagSpec("status"));
|
|
1021
1081
|
const name = f.rest[0];
|
|
1022
1082
|
if (!name) die("usage: claudep status <name> [--json]");
|
|
1023
1083
|
if (name !== "default" && !profileExists(L, name)) die(`profile "${name}" does not exist`);
|
|
@@ -1042,7 +1102,7 @@ export function envUnsetHint(syntax: EnvSyntax): string {
|
|
|
1042
1102
|
}
|
|
1043
1103
|
|
|
1044
1104
|
function cmdEnv(L: Layout, args: string[]): void {
|
|
1045
|
-
const f = parseFlags(args,
|
|
1105
|
+
const f = parseFlags(args, ...flagSpec("env"));
|
|
1046
1106
|
const forced = f.strs.get("--shell");
|
|
1047
1107
|
const syntax =
|
|
1048
1108
|
forced === undefined ? shellSyntax(process.env, L.platform, parentProcessName(L.platform)) : envSyntaxOf(forced);
|
|
@@ -1070,7 +1130,7 @@ export function currentLabel(cur: Current): string {
|
|
|
1070
1130
|
}
|
|
1071
1131
|
|
|
1072
1132
|
function cmdCurrent(L: Layout, args: string[]): void {
|
|
1073
|
-
const f = parseFlags(args,
|
|
1133
|
+
const f = parseFlags(args, ...flagSpec("current"));
|
|
1074
1134
|
const cur = currentProfile(L);
|
|
1075
1135
|
const label = currentLabel(cur);
|
|
1076
1136
|
if (f.bools.has("--name")) {
|
|
@@ -1100,7 +1160,7 @@ function cmdCurrent(L: Layout, args: string[]): void {
|
|
|
1100
1160
|
}
|
|
1101
1161
|
|
|
1102
1162
|
function cmdResolve(L: Layout, args: string[]): void {
|
|
1103
|
-
const f = parseFlags(args,
|
|
1163
|
+
const f = parseFlags(args, ...flagSpec("resolve"));
|
|
1104
1164
|
const start = f.rest[0] ?? process.cwd();
|
|
1105
1165
|
const pin = resolvePin(start);
|
|
1106
1166
|
if (!pin || pin.name === "") process.exit(1);
|
|
@@ -1111,7 +1171,7 @@ function cmdResolve(L: Layout, args: string[]): void {
|
|
|
1111
1171
|
}
|
|
1112
1172
|
|
|
1113
1173
|
function cmdLocal(L: Layout, args: string[]): void {
|
|
1114
|
-
const f = parseFlags(args,
|
|
1174
|
+
const f = parseFlags(args, ...flagSpec("local"));
|
|
1115
1175
|
const file = join(process.cwd(), PIN_FILE);
|
|
1116
1176
|
if (f.bools.has("--remove")) {
|
|
1117
1177
|
if (!existsSync(file)) die(`no ${PIN_FILE} in ${process.cwd()}`);
|
|
@@ -1140,9 +1200,8 @@ function cmdLocal(L: Layout, args: string[]): void {
|
|
|
1140
1200
|
|
|
1141
1201
|
/** The shell hook. Pure parameter expansion and builtins: it runs on every
|
|
1142
1202
|
* 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"];
|
|
1203
|
+
* subprocess is allowed here. Logic mirrors resolvePin(). `Shell` and
|
|
1204
|
+
* `SHELLS` live next to the command table. */
|
|
1146
1205
|
|
|
1147
1206
|
/** The shell to name in hints and to default `shell-init` to. */
|
|
1148
1207
|
export function defaultShell(env: Env = process.env, platform: NodeJS.Platform = process.platform): Shell {
|
|
@@ -1423,6 +1482,186 @@ function cmdShellInit(L: Layout, args: string[]): void {
|
|
|
1423
1482
|
process.stdout.write(shellInit(shell as Shell, L.profilesRoot, L.platform));
|
|
1424
1483
|
}
|
|
1425
1484
|
|
|
1485
|
+
// ---------------------------------------------------------------------------
|
|
1486
|
+
// Tab completions, generated from COMMANDS. Profile names come from a glob
|
|
1487
|
+
// over the profiles root at completion time; no script ever runs claudep.
|
|
1488
|
+
// ---------------------------------------------------------------------------
|
|
1489
|
+
|
|
1490
|
+
/** The one rc-file line that loads the completions. */
|
|
1491
|
+
export function completionHint(shell: CompletionShell): string {
|
|
1492
|
+
return shell === "fish" ? "claudep completion fish | source" : `eval "$(claudep completion ${shell})"`;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const visibleCommands = () => COMMANDS.filter((c) => !c.hidden);
|
|
1496
|
+
const isList = (arg: Command["arg"]): arg is readonly string[] => arg !== undefined && typeof arg !== "string";
|
|
1497
|
+
|
|
1498
|
+
export function completionScript(shell: CompletionShell, profilesRoot: string): string {
|
|
1499
|
+
if (shell === "zsh") return zshCompletion(profilesRoot);
|
|
1500
|
+
if (shell === "bash") return bashCompletion(profilesRoot);
|
|
1501
|
+
return fishCompletion(profilesRoot);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** Dual-mode: `eval "$(claudep completion zsh)"` after compinit registers it
|
|
1505
|
+
* with compdef; saved as `_claudep` on fpath, the #compdef line and the
|
|
1506
|
+
* funcstack check make it autoload. Names come from a glob with (N/:t):
|
|
1507
|
+
* nothing on an empty root, directories only, basename. */
|
|
1508
|
+
function zshCompletion(profilesRoot: string): string {
|
|
1509
|
+
const q = `'${profilesRoot.replace(/'/g, `'\\''`)}'`;
|
|
1510
|
+
const noColon = (s: string) => s.replace(/:/g, "\\:");
|
|
1511
|
+
const cmds = visibleCommands().flatMap((c) => commandWords(c).map((w) => ` '${w}:${noColon(c.desc)}'`));
|
|
1512
|
+
const cases = COMMANDS.filter((c) => c.arg || c.flags || c.valueFlags).map((c) => {
|
|
1513
|
+
const specs: string[] = [];
|
|
1514
|
+
if (c.arg === "profile") specs.push("'1:profile:_claudep_profiles'");
|
|
1515
|
+
else if (c.arg === "dir") specs.push("'1:directory:_files -/'");
|
|
1516
|
+
else if (isList(c.arg)) specs.push(`'1:shell:(${c.arg.join(" ")})'`);
|
|
1517
|
+
if (c.name === "run") specs.push("'*:claude arguments:'");
|
|
1518
|
+
for (const f of c.flags ?? []) specs.push(`'${f}'`);
|
|
1519
|
+
for (const v of c.valueFlags ?? [])
|
|
1520
|
+
specs.push(`'${v.name}=-:${v.name.slice(2)}:${v.values ? `(${v.values.join(" ")})` : ""}'`);
|
|
1521
|
+
return ` ${commandWords(c).join("|")}) _arguments ${specs.join(" ")} ;;`;
|
|
1522
|
+
});
|
|
1523
|
+
return `#compdef claudep
|
|
1524
|
+
# claudep completions. Load them after compinit in ~/.zshrc: ${completionHint("zsh")}
|
|
1525
|
+
_claudep_profiles() {
|
|
1526
|
+
local -a names
|
|
1527
|
+
names=(default ${q}/*(N/:t))
|
|
1528
|
+
_describe -t profiles 'profile' names
|
|
1529
|
+
}
|
|
1530
|
+
_claudep() {
|
|
1531
|
+
local -a cmds
|
|
1532
|
+
cmds=(
|
|
1533
|
+
${cmds.join("\n")}
|
|
1534
|
+
)
|
|
1535
|
+
_arguments -C '1:command:->cmd' '*::arg:->args'
|
|
1536
|
+
case $state in
|
|
1537
|
+
cmd)
|
|
1538
|
+
_describe -t commands 'claudep command' cmds
|
|
1539
|
+
_claudep_profiles
|
|
1540
|
+
;;
|
|
1541
|
+
args)
|
|
1542
|
+
case $words[1] in
|
|
1543
|
+
${cases.join("\n")}
|
|
1544
|
+
esac
|
|
1545
|
+
;;
|
|
1546
|
+
esac
|
|
1547
|
+
}
|
|
1548
|
+
if [ "\${funcstack[1]}" = "_claudep" ]; then _claudep "$@"; else compdef _claudep claudep; fi
|
|
1549
|
+
`;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** bash 3.2 safe: no mapfile, no compopt. The root's backslashes become
|
|
1553
|
+
* slashes so a Git Bash user with a C:\\ root still globs. */
|
|
1554
|
+
function bashCompletion(profilesRoot: string): string {
|
|
1555
|
+
const q = `'${profilesRoot.replace(/'/g, `'\\''`)}'`;
|
|
1556
|
+
const top = [...visibleCommands().flatMap(commandWords), "default"].join(" ");
|
|
1557
|
+
const flagCases = COMMANDS.filter((c) => c.flags || c.valueFlags).map(
|
|
1558
|
+
(c) =>
|
|
1559
|
+
` ${commandWords(c).join("|")}) words="${[...(c.flags ?? []), ...(c.valueFlags ?? []).map((v) => v.name)].join(" ")}" ;;`,
|
|
1560
|
+
);
|
|
1561
|
+
const valued = COMMANDS.flatMap((c) => (c.valueFlags ?? []).filter((v) => v.values));
|
|
1562
|
+
const valueBranches = valued.map(
|
|
1563
|
+
(v, i) => ` ${i === 0 ? "if" : "elif"} [ "$prev" = "${v.name}" ]; then words="${(v.values ?? []).join(" ")}"`,
|
|
1564
|
+
);
|
|
1565
|
+
const argCases = COMMANDS.filter((c) => c.arg).map((c) => {
|
|
1566
|
+
const names = commandWords(c).join("|");
|
|
1567
|
+
if (c.arg === "profile") return ` ${names}) words="default $(_claudep_profiles)" ;;`;
|
|
1568
|
+
if (c.arg === "dir") return ` ${names}) COMPREPLY=($(compgen -d -- "$cur")); return 0 ;;`;
|
|
1569
|
+
return ` ${names}) words="${isList(c.arg) ? c.arg.join(" ") : ""}" ;;`;
|
|
1570
|
+
});
|
|
1571
|
+
return `# claudep completions. Load them from ~/.bashrc: ${completionHint("bash")}
|
|
1572
|
+
_claudep_profiles() {
|
|
1573
|
+
local root=${q} d
|
|
1574
|
+
root=\${root//\\\\//}
|
|
1575
|
+
for d in "$root"/*/; do
|
|
1576
|
+
d=\${d%/}
|
|
1577
|
+
[ -d "$d" ] && printf '%s\\n' "\${d##*/}"
|
|
1578
|
+
done
|
|
1579
|
+
}
|
|
1580
|
+
_claudep() {
|
|
1581
|
+
local cur prev cmd words=""
|
|
1582
|
+
cur=\${COMP_WORDS[COMP_CWORD]}
|
|
1583
|
+
prev=\${COMP_WORDS[COMP_CWORD-1]}
|
|
1584
|
+
cmd=\${COMP_WORDS[1]}
|
|
1585
|
+
COMPREPLY=()
|
|
1586
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
1587
|
+
COMPREPLY=($(compgen -W "${top} $(_claudep_profiles)" -- "$cur"))
|
|
1588
|
+
return 0
|
|
1589
|
+
fi
|
|
1590
|
+
case $cur in
|
|
1591
|
+
-*)
|
|
1592
|
+
case $cmd in
|
|
1593
|
+
${flagCases.join("\n")}
|
|
1594
|
+
esac
|
|
1595
|
+
;;
|
|
1596
|
+
*)
|
|
1597
|
+
${valueBranches.join("\n")}
|
|
1598
|
+
${valueBranches.length ? "elif" : "if"} [ "$COMP_CWORD" -eq 2 ]; then
|
|
1599
|
+
case $cmd in
|
|
1600
|
+
${argCases.join("\n")}
|
|
1601
|
+
esac
|
|
1602
|
+
fi
|
|
1603
|
+
;;
|
|
1604
|
+
esac
|
|
1605
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
1606
|
+
}
|
|
1607
|
+
complete -F _claudep claudep
|
|
1608
|
+
`;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/** The two conditions are written out over `commandline -opc` instead of
|
|
1612
|
+
* fish's __fish_use_subcommand and __fish_seen_subcommand_from, whose
|
|
1613
|
+
* names moved between versions. `for` over an unmatched glob runs zero
|
|
1614
|
+
* times, so an empty root offers only `default`. */
|
|
1615
|
+
function fishCompletion(profilesRoot: string): string {
|
|
1616
|
+
const lines: string[] = [];
|
|
1617
|
+
for (const c of visibleCommands())
|
|
1618
|
+
for (const w of commandWords(c))
|
|
1619
|
+
lines.push(`complete -c claudep -n '__claudep_at 1' -a ${w} -d ${fishQuote(c.desc)}`);
|
|
1620
|
+
lines.push("complete -c claudep -n '__claudep_at 1' -a '(__claudep_profiles)' -d profile");
|
|
1621
|
+
const profileCmds = COMMANDS.filter((c) => c.arg === "profile")
|
|
1622
|
+
.flatMap(commandWords)
|
|
1623
|
+
.join(" ");
|
|
1624
|
+
lines.push(
|
|
1625
|
+
`complete -c claudep -n '__claudep_cmd ${profileCmds}; and __claudep_at 2' -a '(__claudep_profiles)' -d profile`,
|
|
1626
|
+
);
|
|
1627
|
+
for (const c of COMMANDS) {
|
|
1628
|
+
const ws = commandWords(c).join(" ");
|
|
1629
|
+
if (isList(c.arg))
|
|
1630
|
+
lines.push(`complete -c claudep -n '__claudep_cmd ${ws}; and __claudep_at 2' -a '${c.arg.join(" ")}'`);
|
|
1631
|
+
if (c.arg === "dir")
|
|
1632
|
+
lines.push(`complete -c claudep -n '__claudep_cmd ${ws}; and __claudep_at 2' -a '(__fish_complete_directories)'`);
|
|
1633
|
+
for (const f of c.flags ?? []) lines.push(`complete -c claudep -n '__claudep_cmd ${ws}' -l ${f.slice(2)}`);
|
|
1634
|
+
for (const v of c.valueFlags ?? [])
|
|
1635
|
+
lines.push(
|
|
1636
|
+
`complete -c claudep -n '__claudep_cmd ${ws}' -l ${v.name.slice(2)} -x${v.values ? ` -a '${v.values.join(" ")}'` : ""}`,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
return `# claudep completions. Load them from ~/.config/fish/config.fish: ${completionHint("fish")}
|
|
1640
|
+
function __claudep_profiles
|
|
1641
|
+
echo default
|
|
1642
|
+
for d in ${fishQuote(profilesRoot)}/*/
|
|
1643
|
+
string replace -r -- '^.*/([^/]+)/$' '$1' $d
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1646
|
+
function __claudep_at -a n
|
|
1647
|
+
test (count (commandline -opc)) -eq $n
|
|
1648
|
+
end
|
|
1649
|
+
function __claudep_cmd
|
|
1650
|
+
set -l t (commandline -opc)
|
|
1651
|
+
test (count $t) -ge 2; and contains -- $t[2] $argv
|
|
1652
|
+
end
|
|
1653
|
+
complete -c claudep -f
|
|
1654
|
+
${lines.join("\n")}
|
|
1655
|
+
`;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function cmdCompletion(L: Layout, args: string[]): void {
|
|
1659
|
+
const shell = args[0] ?? defaultShell(process.env, L.platform);
|
|
1660
|
+
if (!COMPLETION_SHELLS.includes(shell as CompletionShell))
|
|
1661
|
+
die(`no completions for "${shell}". Use ${COMPLETION_SHELLS.join(", ")}`);
|
|
1662
|
+
process.stdout.write(completionScript(shell as CompletionShell, L.profilesRoot));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1426
1665
|
/** A POSIX-style CLAUDE_CONFIG_DIR on Windows is one claude.exe cannot read. */
|
|
1427
1666
|
function msysConfigDirWarning(L: Layout, env: Env = process.env): string | undefined {
|
|
1428
1667
|
const cfg = env.CLAUDE_CONFIG_DIR;
|
|
@@ -1523,7 +1762,7 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
|
|
|
1523
1762
|
}
|
|
1524
1763
|
|
|
1525
1764
|
async function cmdRm(L: Layout, args: string[]): Promise<void> {
|
|
1526
|
-
const f = parseFlags(args,
|
|
1765
|
+
const f = parseFlags(args, ...flagSpec("rm"));
|
|
1527
1766
|
const name = f.rest[0];
|
|
1528
1767
|
if (!name) die("usage: claudep rm <name> [--keep-login] [--yes]");
|
|
1529
1768
|
const dir = profileDir(L, name);
|
|
@@ -1582,8 +1821,14 @@ async function cmdRm(L: Layout, args: string[]): Promise<void> {
|
|
|
1582
1821
|
}
|
|
1583
1822
|
|
|
1584
1823
|
function help(L: Layout): void {
|
|
1824
|
+
console.log(helpText(L));
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/** Hand-written on purpose: the layout says more than the table can. A test
|
|
1828
|
+
* checks that every command in COMMANDS appears in it. */
|
|
1829
|
+
export function helpText(L: Layout): string {
|
|
1585
1830
|
const root = shortHome(L.profilesRoot, L.home, L.platform);
|
|
1586
|
-
|
|
1831
|
+
return `${c.bold("claudep")} ${c.dim(version())}: run Claude Code under separate accounts on one machine
|
|
1587
1832
|
|
|
1588
1833
|
${c.bold("USAGE")}
|
|
1589
1834
|
claudep <name> [claude args…] run claude with profile <name> (alias for "run")
|
|
@@ -1596,13 +1841,18 @@ ${c.bold("USAGE")}
|
|
|
1596
1841
|
claudep alias <name> <command> write a shim so "<command>" == "claudep <name>"
|
|
1597
1842
|
claudep doctor [name] verify symlinks, keychain entry, unclassified files, Claude Code version
|
|
1598
1843
|
claudep rm <name> [--keep-login] log out and delete a profile (base is never touched)
|
|
1844
|
+
claudep help show this text
|
|
1599
1845
|
claudep --version print the version
|
|
1600
1846
|
|
|
1601
1847
|
${c.bold("DIRECTORY PINS")}
|
|
1602
1848
|
claudep local <name> [--force] write ./${PIN_FILE} so this tree uses <name>; --remove deletes it
|
|
1603
1849
|
claudep local show the pin that applies to the current directory
|
|
1604
1850
|
claudep resolve [dir] [--json] print the profile pinned for a directory (exit 1 when none)
|
|
1605
|
-
claudep shell-init [
|
|
1851
|
+
claudep shell-init [${SHELLS.join("|")}] print the hook that applies pins on cd; load it from your rc file
|
|
1852
|
+
|
|
1853
|
+
${c.bold("TAB COMPLETION")}
|
|
1854
|
+
claudep completion [${COMPLETION_SHELLS.join("|")}] print completions for commands, flags and profile names;
|
|
1855
|
+
load them from your rc file (after compinit in zsh)
|
|
1606
1856
|
|
|
1607
1857
|
${c.bold("INIT OPTIONS")}
|
|
1608
1858
|
--sso force the SSO login flow (Enterprise orgs)
|
|
@@ -1625,6 +1875,7 @@ ${c.bold("EXAMPLES")}
|
|
|
1625
1875
|
eval "$(claudep shell-init zsh)" # in .zshrc: shells follow ${PIN_FILE} pins on cd
|
|
1626
1876
|
${hookHint("fish")} # the same line for config.fish
|
|
1627
1877
|
${hookHint("powershell")} # the same line for $PROFILE
|
|
1878
|
+
${completionHint("zsh")} # in .zshrc after compinit: tab completion
|
|
1628
1879
|
|
|
1629
1880
|
${c.bold("HOW IT WORKS")}
|
|
1630
1881
|
~/.claude stays exactly as it is and remains the "default" profile. Each named profile is a
|
|
@@ -1649,7 +1900,7 @@ ${c.bold("ENVIRONMENT")}
|
|
|
1649
1900
|
CLAUDEP_AUTO set by the hook next to CLAUDE_CONFIG_DIR; marks the pin as hook-managed
|
|
1650
1901
|
|
|
1651
1902
|
${c.dim("claudep is an independent, unofficial tool. It is not affiliated with, endorsed by or supported by Anthropic.")}
|
|
1652
|
-
|
|
1903
|
+
`;
|
|
1653
1904
|
}
|
|
1654
1905
|
|
|
1655
1906
|
// ---------------------------------------------------------------------------
|
|
@@ -1685,6 +1936,8 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
1685
1936
|
return cmdResolve(L, args);
|
|
1686
1937
|
case "shell-init":
|
|
1687
1938
|
return cmdShellInit(L, args);
|
|
1939
|
+
case "completion":
|
|
1940
|
+
return cmdCompletion(L, args);
|
|
1688
1941
|
case "version":
|
|
1689
1942
|
case "--version":
|
|
1690
1943
|
case "-v":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bordoni/claudep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|