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.
@@ -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):
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/ symlink
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.29",
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": "bash tests/run-tests.sh && python3 tests/test_selector.py && npm run test:commands",
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": {
@@ -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()