claude-multiacc 2.0.29 → 2.0.30
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/docs/ACCOUNT_OPERATIONS.md +9 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/common.sh +4 -1
- package/package.json +4 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/test_account_seeding.py +112 -0
|
@@ -131,6 +131,15 @@ manually via:
|
|
|
131
131
|
claude-accounts sync # and codex-accounts sync for the codex pool
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
Account creation and sync seed missing settings from the machine's default provider
|
|
135
|
+
directory and link its global instructions: `~/.claude/CLAUDE.md` for Claude,
|
|
136
|
+
`~/.codex/AGENTS.md` for Codex. Existing account settings (including models) and
|
|
137
|
+
instruction files or links are preserved. The addon does not generate instructions
|
|
138
|
+
or enable/disable agents. To share newly created global instructions with existing
|
|
139
|
+
accounts, run `claude-accounts sync --no-server` and/or `codex-accounts sync --no-server`.
|
|
140
|
+
This only repairs accounts in the manifest, leaving retired directories untouched.
|
|
141
|
+
Global instruction files remain machine-local; sync does not copy them between hosts.
|
|
142
|
+
|
|
134
143
|
Targets are the resolved sync target — env override, else the manifest's
|
|
135
144
|
`server`/`server_root`/`server_repo` (the Linux box), see *Pointing sync somewhere else*
|
|
136
145
|
below — plus an optional `peers` array for additional machines (e.g. a second Mac):
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/common.sh
CHANGED
|
@@ -677,7 +677,7 @@ codex_share_state_index() { # $1 acct dir — seed time: whatever index names ex
|
|
|
677
677
|
}
|
|
678
678
|
|
|
679
679
|
# Seed an account config dir so headless runs never prompt.
|
|
680
|
-
# claude: stripped .claude.json, copied settings.json, shared projects/
|
|
680
|
+
# claude: stripped .claude.json, copied settings.json, shared CLAUDE.md + projects/
|
|
681
681
|
# (shared history => --continue/--resume work regardless of picked account).
|
|
682
682
|
# codex: copied config.toml (carries project trust + settings, no identity),
|
|
683
683
|
# shared sessions/ symlink (=> `codex resume` finds every session) with the
|
|
@@ -718,6 +718,9 @@ PYEOF
|
|
|
718
718
|
if [ ! -f "$d/settings.json" ] && [ -f "$HOME/.claude/settings.json" ]; then
|
|
719
719
|
cp "$HOME/.claude/settings.json" "$d/settings.json" 2>/dev/null || true
|
|
720
720
|
fi
|
|
721
|
+
if [ ! -e "$d/CLAUDE.md" ] && [ -f "$HOME/.claude/CLAUDE.md" ]; then
|
|
722
|
+
ln -s "$HOME/.claude/CLAUDE.md" "$d/CLAUDE.md" 2>/dev/null || true
|
|
723
|
+
fi
|
|
721
724
|
if [ ! -e "$d/projects" ] && [ -d "$HOME/.claude/projects" ]; then
|
|
722
725
|
ln -s "$HOME/.claude/projects" "$d/projects" 2>/dev/null || true
|
|
723
726
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"postinstall": "node scripts/postinstall.mjs",
|
|
58
|
-
"test": "
|
|
58
|
+
"test": "npm run test:core && npm run test:seeding && npm run test:commands",
|
|
59
|
+
"test:core": "bash tests/run-tests.sh && python3 tests/test_selector.py",
|
|
60
|
+
"test:seeding": "python3 tests/test_account_seeding.py",
|
|
59
61
|
"test:commands": "python3 tests/test_packaged_commands.py"
|
|
60
62
|
},
|
|
61
63
|
"dependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Account seeding preserves settings and shares machine-local global instructions."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
13
|
+
PROVIDERS = (("claude", "CLAUDE.md", "settings.json"), ("codex", "AGENTS.md", "config.toml"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AccountSeedingTests(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
self.work = tempfile.TemporaryDirectory()
|
|
19
|
+
self.addCleanup(self.work.cleanup)
|
|
20
|
+
self.home = Path(self.work.name)
|
|
21
|
+
self.env = {**os.environ, "HOME": str(self.home), "CLAUDE_MULTIACC_PYTHON": "python3"}
|
|
22
|
+
for provider, _, _ in PROVIDERS:
|
|
23
|
+
(self.home / f".{provider}").mkdir()
|
|
24
|
+
self.env[f"{provider.upper()}_ACCOUNTS_ROOT"] = str(self.home / f".{provider}-accounts")
|
|
25
|
+
|
|
26
|
+
def seed(self, provider, directory=None):
|
|
27
|
+
account = directory or self.home / f".{provider}-accounts" / "acct-01"
|
|
28
|
+
subprocess.run(
|
|
29
|
+
["bash", "-c", '. "$1"; seed_account_dir "$2"', "seed", str(REPO / "lib/common.sh"), str(account)],
|
|
30
|
+
env={**self.env, "MULTIACC_PROVIDER": provider}, check=True, capture_output=True, text=True,
|
|
31
|
+
)
|
|
32
|
+
return account
|
|
33
|
+
|
|
34
|
+
def test_instructions_are_shared_and_updates_remain_visible(self):
|
|
35
|
+
for provider, name, _ in PROVIDERS:
|
|
36
|
+
with self.subTest(provider=provider):
|
|
37
|
+
source = self.home / f".{provider}" / name
|
|
38
|
+
source.write_text("Global preferences\n")
|
|
39
|
+
account = self.seed(provider)
|
|
40
|
+
self.assertTrue((account / name).is_symlink())
|
|
41
|
+
self.assertEqual((account / name).resolve(), source.resolve())
|
|
42
|
+
source.write_text("Updated preferences\n")
|
|
43
|
+
self.seed(provider)
|
|
44
|
+
self.assertEqual((account / name).read_text(), "Updated preferences\n")
|
|
45
|
+
|
|
46
|
+
def test_existing_files_and_links_are_preserved(self):
|
|
47
|
+
for provider, name, _ in PROVIDERS:
|
|
48
|
+
with self.subTest(provider=provider):
|
|
49
|
+
(self.home / f".{provider}" / name).write_text("Global preferences\n")
|
|
50
|
+
account = self.seed(provider)
|
|
51
|
+
target = account / name
|
|
52
|
+
target.unlink()
|
|
53
|
+
target.write_text("Account preferences\n")
|
|
54
|
+
self.seed(provider)
|
|
55
|
+
self.assertFalse(target.is_symlink())
|
|
56
|
+
self.assertEqual(target.read_text(), "Account preferences\n")
|
|
57
|
+
target.unlink()
|
|
58
|
+
local = self.home / f"{provider}-local.md"
|
|
59
|
+
target.symlink_to(local)
|
|
60
|
+
self.seed(provider)
|
|
61
|
+
self.assertEqual(target.readlink(), local) # Dangling custom link is also owned by the user.
|
|
62
|
+
local.write_text("Custom preferences\n")
|
|
63
|
+
self.seed(provider)
|
|
64
|
+
self.assertEqual(target.read_text(), "Custom preferences\n")
|
|
65
|
+
|
|
66
|
+
def test_missing_global_file_never_creates_a_dangling_or_self_link(self):
|
|
67
|
+
for provider, name, _ in PROVIDERS:
|
|
68
|
+
with self.subTest(provider=provider):
|
|
69
|
+
account = self.seed(provider)
|
|
70
|
+
self.assertFalse(os.path.lexists(account / name))
|
|
71
|
+
base = self.home / f".{provider}"
|
|
72
|
+
self.seed(provider, base)
|
|
73
|
+
self.assertFalse(os.path.lexists(base / name))
|
|
74
|
+
(base / name).write_text("Global preferences\n")
|
|
75
|
+
self.seed(provider, base)
|
|
76
|
+
self.assertFalse((base / name).is_symlink())
|
|
77
|
+
|
|
78
|
+
def test_existing_settings_and_model_are_not_overwritten(self):
|
|
79
|
+
for provider, _, name in PROVIDERS:
|
|
80
|
+
with self.subTest(provider=provider):
|
|
81
|
+
source = self.home / f".{provider}" / name
|
|
82
|
+
source.write_text('{"model":"global"}\n' if provider == "claude" else 'model = "global"\n')
|
|
83
|
+
account = self.seed(provider)
|
|
84
|
+
self.assertEqual((account / name).read_bytes(), source.read_bytes())
|
|
85
|
+
original = '{"model":"account"}\n' if provider == "claude" else 'model = "account"\n'
|
|
86
|
+
(account / name).write_text(original)
|
|
87
|
+
self.seed(provider)
|
|
88
|
+
self.assertEqual((account / name).read_text(), original)
|
|
89
|
+
|
|
90
|
+
def test_local_sync_repairs_only_manifest_accounts(self):
|
|
91
|
+
for provider, name, _ in PROVIDERS:
|
|
92
|
+
with self.subTest(provider=provider):
|
|
93
|
+
(self.home / f".{provider}" / name).write_text("Global preferences\n")
|
|
94
|
+
root = self.home / f".{provider}-accounts"
|
|
95
|
+
(root / "acct-01").mkdir(parents=True)
|
|
96
|
+
(root / "acct-99").mkdir()
|
|
97
|
+
(root / "accounts.json").write_text(json.dumps({
|
|
98
|
+
"version": 1, "server": "none",
|
|
99
|
+
"accounts": [{"id": "acct-01", "email": "fixture@example.test", "home": "mac"}],
|
|
100
|
+
}))
|
|
101
|
+
for _ in range(2):
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
[str(REPO / "bin" / f"{provider}-accounts"), "sync", "--no-server"],
|
|
104
|
+
env=self.env, check=True, capture_output=True, text=True,
|
|
105
|
+
)
|
|
106
|
+
self.assertIn("nothing pushed", result.stdout)
|
|
107
|
+
self.assertTrue((root / "acct-01" / name).is_symlink())
|
|
108
|
+
self.assertFalse(os.path.lexists(root / "acct-99" / name))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
unittest.main()
|