claude-account-sync 0.2.0 → 0.3.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/README.md +41 -29
- package/dist/ui/api.js +73 -5
- package/dist/webui/assets/index-BDxkKXQV.css +1 -0
- package/dist/webui/assets/index-XneMINpm.js +143 -0
- package/dist/webui/index.html +2 -2
- package/package.json +1 -1
- package/dist/webui/assets/index-DZPGFb5T.css +0 -1
- package/dist/webui/assets/index-Dr2jwana.js +0 -136
package/README.md
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
Run Claude Code with several accounts — personal subscription, work OAuth, API key, alternative providers — each in its own `CLAUDE_CONFIG_DIR`? Then you know the pain: MCP server lists drift apart, skills get shared via hand-made symlinks, API keys end up in plaintext in your `.zshrc`, and setting up a second machine means an afternoon of copy-paste.
|
|
6
6
|
|
|
7
|
-
The `
|
|
7
|
+
The `clp` command (also available as `ccprofiles`) fixes that:
|
|
8
8
|
|
|
9
9
|
- 🔎 **Adopt** your existing `.claude*` directories into a declarative manifest — zero manual config
|
|
10
10
|
- 🧩 **Manage MCP servers** across profiles: drift matrix, add/remove everywhere at once, sync one profile's set to others
|
|
11
|
-
- 🔐 **Secrets out of your rc files** — macOS Keychain / libsecret / encrypted file, with `
|
|
11
|
+
- 🔐 **Secrets out of your rc files** — macOS Keychain / libsecret / encrypted file, with `clp secrets migrate` to clean up existing plaintext keys
|
|
12
12
|
- 🖥️ **Replicate to another machine over LAN** — PIN pairing, end-to-end encrypted, no cloud, works macOS ↔ Windows ↔ Linux ↔ WSL
|
|
13
|
-
-
|
|
13
|
+
- 🖼️ **Web dashboard** — `clp ui` opens a local browser panel to manage everything visually
|
|
14
|
+
- 📦 **Offline bundles** for the no-network case (`clp export setup.ccb`)
|
|
14
15
|
- 🛟 **Safe by design**: surgical config edits only, automatic backups, `--dry-run` everywhere, never touches your sessions/history
|
|
15
16
|
|
|
16
17
|
## Install
|
|
@@ -21,7 +22,7 @@ Requires **Node ≥ 20** (you have it — Claude Code needs it too).
|
|
|
21
22
|
npm install -g claude-account-sync
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
This installs two equivalent commands: `
|
|
25
|
+
This installs two equivalent commands: `clp` (short) and `ccprofiles` (full). The docs below use `clp`.
|
|
25
26
|
|
|
26
27
|
<details><summary>Install from source instead</summary>
|
|
27
28
|
|
|
@@ -37,26 +38,28 @@ cd packages/cli && npm link
|
|
|
37
38
|
## Quickstart
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
clp adopt --yes # REQUIRED FIRST: scan ~/.claude* and build the manifest
|
|
42
|
+
clp list # see all your profiles
|
|
43
|
+
clp doctor # find broken links & plaintext keys
|
|
44
|
+
clp secrets migrate # move API keys from .zshrc into the OS keychain
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
Everything except `list`, `adopt`, and `doctor` needs the manifest, so `
|
|
47
|
+
Everything except `list`, `adopt`, and `doctor` needs the manifest, so `clp adopt --yes` is always step one — commands will remind you if you skip it.
|
|
48
|
+
|
|
49
|
+
Prefer a UI? `clp ui` opens the whole thing in your browser (see below).
|
|
47
50
|
|
|
48
51
|
### Manage MCP servers
|
|
49
52
|
|
|
50
53
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
clp mcp list # server × profile drift matrix
|
|
55
|
+
clp mcp add shadcn --all --command npx --args "shadcn@latest,mcp"
|
|
56
|
+
clp mcp sync --from oauth --to office,z # make profiles match
|
|
54
57
|
```
|
|
55
58
|
|
|
56
59
|
### New profile for a new account
|
|
57
60
|
|
|
58
61
|
```bash
|
|
59
|
-
|
|
62
|
+
clp create work --from oauth # dir + launcher fn + copied MCP set
|
|
60
63
|
# restart shell, then:
|
|
61
64
|
cl-work # launches claude with CLAUDE_CONFIG_DIR=~/.claude-work
|
|
62
65
|
```
|
|
@@ -65,13 +68,13 @@ cl-work # launches claude with CLAUDE_CONFIG_DIR=~/.cla
|
|
|
65
68
|
|
|
66
69
|
```bash
|
|
67
70
|
# machine A (source of truth)
|
|
68
|
-
|
|
71
|
+
clp serve --allow-secrets
|
|
69
72
|
# → ccprofiles sync server on port 51234
|
|
70
73
|
# → pairing PIN: 123456
|
|
71
74
|
|
|
72
75
|
# machine B
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
clp pair 192.168.1.10 --port 51234 --pin 123456 --name mac
|
|
77
|
+
clp sync --from mac --with-secrets
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
Manifest, MCP servers, skills, commands, launcher functions, and (opt-in) secrets all arrive — rendered for the local OS: PowerShell profile functions and junctions on Windows, `.zshrc`/`.bashrc` functions and symlinks elsewhere.
|
|
@@ -81,21 +84,29 @@ Two things intentionally don't travel:
|
|
|
81
84
|
- **OAuth sessions** — you still run `/login` once per account on the new machine (Anthropic session state is machine-bound; syncing it would be wrong).
|
|
82
85
|
- The **`default` profile has no `cl-*` launcher** — it's what plain `claude` already launches; only the named profiles get launcher functions.
|
|
83
86
|
|
|
87
|
+
## Web dashboard
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
clp ui # opens http://127.0.0.1:<port>/?t=<token> in your browser
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A local panel to manage profiles, MCP servers (interactive drift matrix), secrets (add / reveal / delete / migrate), sync, and doctor — everything the CLI does. It's **localhost-only** and guarded by a per-launch session token plus an Origin check, so nothing off your machine (and no website in your browser) can reach the API. Pass `--no-open` to just print the URL, or `--port <n>` to pin the port.
|
|
94
|
+
|
|
84
95
|
## How it works
|
|
85
96
|
|
|
86
97
|
Three layers of state:
|
|
87
98
|
|
|
88
|
-
1. **Live state** — your actual `.claude*` dirs and shell rc files. Claude Code owns these;
|
|
99
|
+
1. **Live state** — your actual `.claude*` dirs and shell rc files. Claude Code owns these; the tool edits only the keys it manages (`mcpServers`, its marked rc block, its links).
|
|
89
100
|
2. **Manifest** — `~/.ccprofiles/manifest.yaml`, a platform-neutral declaration (paths templated as `{home}`, secrets referenced as `secret://name`). Versioned with local git commits; safe to share.
|
|
90
|
-
3. **Secrets store** — per-machine keychain: macOS Keychain, Linux `secret-tool` (libsecret), or an AES-256-GCM encrypted file as fallback (native Windows and headless Linux — set `CCPROFILES_PASSPHRASE` in your environment for it). Values never appear in the manifest, bundles, or rc files;
|
|
101
|
+
3. **Secrets store** — per-machine keychain: macOS Keychain, Linux `secret-tool` (libsecret), or an AES-256-GCM encrypted file as fallback (native Windows and headless Linux — set `CCPROFILES_PASSPHRASE` in your environment for it). Values never appear in the manifest, bundles, or rc files; launcher functions resolve them at run time by calling the CLI.
|
|
91
102
|
|
|
92
|
-
> ⚠️ `
|
|
103
|
+
> ⚠️ `clp secrets set <name> <value>` takes the value as an argument, which lands in your shell history — prefer `clp secrets migrate` (reads from rc files) or clear history after. Interactive prompting is on the roadmap.
|
|
93
104
|
|
|
94
|
-
`
|
|
105
|
+
`clp status` shows drift between manifest and live; `clp apply` reconciles (with backups under `~/.ccprofiles/backups/`); `clp snapshot` goes the other way (live → manifest).
|
|
95
106
|
|
|
96
107
|
### Sync security model
|
|
97
108
|
|
|
98
|
-
Pairing performs an X25519 ECDH key exchange authenticated by the 6-digit PIN shown on the serving device (HMAC confirmation both ways — a MITM on your network cannot complete pairing without the PIN, and the client verifies the server too). All subsequent payloads are AES-256-GCM encrypted with the pairing key. Secrets transfer additionally requires the server to opt in with `--allow-secrets`, and values go straight into the receiving machine's keychain.
|
|
109
|
+
Pairing performs an X25519 ECDH key exchange authenticated by the 6-digit PIN shown on the serving device (HMAC confirmation both ways — a MITM on your network cannot complete pairing without the PIN, and the client verifies the server too). All subsequent payloads are AES-256-GCM encrypted with the pairing key. Secrets transfer additionally requires the server to opt in with `--allow-secrets`, and values go straight into the receiving machine's keychain. Pairing locks after 5 wrong PINs.
|
|
99
110
|
|
|
100
111
|
## Commands
|
|
101
112
|
|
|
@@ -107,6 +118,7 @@ Pairing performs an X25519 ECDH key exchange authenticated by the 6-digit PIN sh
|
|
|
107
118
|
| Manifest | `status` · `apply` · `snapshot` |
|
|
108
119
|
| Sync | `serve [--allow-secrets]` · `pair <host> --port n --pin p` · `devices` · `sync --from dev [--with-secrets]` |
|
|
109
120
|
| Bundle | `export <file>` · `import <file>` |
|
|
121
|
+
| Dashboard | `ui [--port n] [--no-open]` |
|
|
110
122
|
|
|
111
123
|
All mutating commands support `--dry-run`. Every mutation backs up the files it touches to `~/.ccprofiles/backups/<timestamp>/` first.
|
|
112
124
|
|
|
@@ -114,33 +126,33 @@ All mutating commands support `--dry-run`. Every mutation backs up the files it
|
|
|
114
126
|
|
|
115
127
|
| Symptom | Fix |
|
|
116
128
|
|---|---|
|
|
117
|
-
| `error: no manifest yet` | Run `
|
|
118
|
-
| `zsh: command not found:
|
|
119
|
-
| ``cannot reach <host> — is `ccprofiles serve` running?`` | Start `
|
|
129
|
+
| `error: no manifest yet` | Run `clp adopt --yes` first — it builds the manifest from your existing profiles |
|
|
130
|
+
| `zsh: command not found: clp` | Not linked/installed — see Install; if just linked, run `rehash` |
|
|
131
|
+
| ``cannot reach <host> — is `ccprofiles serve` running?`` | Start `clp serve` on the other device; check you're on the same network and the port matches |
|
|
120
132
|
| `encrypted-file backend requires a passphrase` | Set `CCPROFILES_PASSPHRASE` (Windows / Linux without libsecret) |
|
|
121
133
|
| Profile shows account `-` after sync | Expected — run `/login` inside that profile once; OAuth sessions don't sync |
|
|
122
134
|
| Something went wrong after `apply` | Restore from `~/.ccprofiles/backups/<latest>/` |
|
|
123
135
|
|
|
124
136
|
## Roadmap
|
|
125
137
|
|
|
126
|
-
-
|
|
127
|
-
-
|
|
138
|
+
- mDNS auto-discovery for `clp devices`
|
|
139
|
+
- Pair devices from the dashboard (currently CLI-only)
|
|
128
140
|
- Windows Credential Manager (DPAPI) secrets backend
|
|
129
141
|
- Interactive prompts (`secrets set` without echoing, `adopt` confirmation)
|
|
130
142
|
|
|
131
143
|
## Development
|
|
132
144
|
|
|
133
|
-
npm-workspaces monorepo: `packages/core` (library) + `packages/cli` (commander wrapper).
|
|
145
|
+
npm-workspaces monorepo: `packages/core` (library) + `packages/cli` (commander wrapper) + `packages/ui` (dashboard). The published CLI is [`claude-account-sync`](https://www.npmjs.com/package/claude-account-sync); the library is [`ccprofiles-core`](https://www.npmjs.com/package/ccprofiles-core).
|
|
134
146
|
|
|
135
147
|
```bash
|
|
136
148
|
npm install
|
|
137
149
|
npm test # vitest — unit + e2e (incl. an in-process two-machine sync test)
|
|
138
|
-
npm run build
|
|
150
|
+
npm run build # builds core + cli + the dashboard, bundled into the CLI
|
|
139
151
|
```
|
|
140
152
|
|
|
141
153
|
## Related projects
|
|
142
154
|
|
|
143
|
-
Not to be confused with [samhvw8/claude-code-profile](https://github.com/samhvw8/claude-code-profile) (the `ccp` command) — a great tool for a **different** job: a central hub of reusable skills/agents/hooks/commands, with profiles for different *workflows*. It explicitly does **not** handle MCP servers, secrets, LAN sync, or multiple *accounts* — which are exactly this tool's focus. The two are complementary; this one deliberately uses the `
|
|
155
|
+
Not to be confused with [samhvw8/claude-code-profile](https://github.com/samhvw8/claude-code-profile) (the `ccp` command) — a great tool for a **different** job: a central hub of reusable skills/agents/hooks/commands, with profiles for different *workflows*. It explicitly does **not** handle MCP servers, secrets, LAN sync, or multiple *accounts* — which are exactly this tool's focus. The two are complementary; this one deliberately uses the `clp` / `ccprofiles` commands to avoid clashing with its `ccp`.
|
|
144
156
|
|
|
145
157
|
## License
|
|
146
158
|
|
package/dist/ui/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { discoverProfiles, buildManifest, saveManifest, planApply, executeApply, ensureRootGitignore, loadManifest, loadDevices, fetchRemote, fetchSecrets, writeAssets, parseManifest, backupFiles, } from 'ccprofiles-core';
|
|
1
|
+
import { discoverProfiles, buildManifest, saveManifest, planApply, executeApply, ensureRootGitignore, loadManifest, loadDevices, fetchRemote, fetchSecrets, writeAssets, parseManifest, backupFiles, renderRcBlock, upsertManagedBlock, atomicWrite, BEGIN_MARK, END_MARK, assertSafeManifest, } from 'ccprofiles-core';
|
|
2
2
|
import { existsSync, readFileSync, lstatSync, readlinkSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
@@ -25,6 +25,15 @@ export function buildRoutes(ctx) {
|
|
|
25
25
|
throw new HttpError(409, e.message);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
// Guard mutation routes: never persist a manifest that would interpolate unsafely into the rc file.
|
|
29
|
+
function assertSafe(m) {
|
|
30
|
+
try {
|
|
31
|
+
assertSafeManifest(m);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
throw new HttpError(400, e.message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
function targetsOf(m, targets) {
|
|
29
38
|
if (targets === 'all')
|
|
30
39
|
return m.profiles.map(p => p.name);
|
|
@@ -59,6 +68,7 @@ export function buildRoutes(ctx) {
|
|
|
59
68
|
account: lp.account, mcp: Object.keys(lp.mcpServers).length,
|
|
60
69
|
launcher: decl?.launcher ?? (name === 'default' ? null : `cl-${name}`),
|
|
61
70
|
adopted: !!decl,
|
|
71
|
+
env: decl?.env ?? {}, links: decl?.links ?? {}, mcpNames: decl?.mcp ?? [],
|
|
62
72
|
};
|
|
63
73
|
});
|
|
64
74
|
sendJson(res, 200, rows);
|
|
@@ -78,22 +88,48 @@ export function buildRoutes(ctx) {
|
|
|
78
88
|
links: src ? { ...src.links } : (m.hub ? { skills: 'hub', commands: 'hub' } : {}),
|
|
79
89
|
mcp: src ? [...src.mcp] : [],
|
|
80
90
|
});
|
|
91
|
+
assertSafe(m);
|
|
81
92
|
await saveManifest(ctx.manifestRoot, m);
|
|
82
93
|
await applyAndReport(m);
|
|
83
94
|
sendJson(res, 200, { ok: true });
|
|
84
95
|
});
|
|
85
96
|
add('PATCH', /^\/api\/profiles\/([^/]+)$/, async (mtch, req, res) => {
|
|
86
97
|
const m = await mustManifest();
|
|
87
|
-
const
|
|
98
|
+
const name = decodeURIComponent(mtch[1]);
|
|
99
|
+
const pr = m.profiles.find(p => p.name === name);
|
|
88
100
|
if (!pr)
|
|
89
|
-
throw new HttpError(404, `unknown profile: ${
|
|
101
|
+
throw new HttpError(404, `unknown profile: ${name}`);
|
|
90
102
|
const body = await readJson(req);
|
|
91
|
-
if (body.env)
|
|
103
|
+
if (body.env) {
|
|
104
|
+
if (!Object.values(body.env).every(v => typeof v === 'string'))
|
|
105
|
+
throw new HttpError(400, 'env values must be strings');
|
|
92
106
|
pr.env = body.env;
|
|
93
|
-
|
|
107
|
+
}
|
|
108
|
+
if (body.links) {
|
|
109
|
+
if (!Object.values(body.links).every(v => typeof v === 'string'))
|
|
110
|
+
throw new HttpError(400, 'links values must be strings');
|
|
94
111
|
pr.links = body.links;
|
|
112
|
+
}
|
|
95
113
|
if (body.launcher !== undefined)
|
|
96
114
|
pr.launcher = body.launcher;
|
|
115
|
+
assertSafe(m);
|
|
116
|
+
await saveManifest(ctx.manifestRoot, m);
|
|
117
|
+
await applyAndReport(m);
|
|
118
|
+
sendJson(res, 200, { ok: true });
|
|
119
|
+
});
|
|
120
|
+
add('DELETE', /^\/api\/profiles\/([^/]+)$/, async (mtch, _req, res) => {
|
|
121
|
+
const m = await mustManifest();
|
|
122
|
+
const name = decodeURIComponent(mtch[1]);
|
|
123
|
+
const idx = m.profiles.findIndex(p => p.name === name);
|
|
124
|
+
if (idx === -1)
|
|
125
|
+
throw new HttpError(404, `unknown profile: ${name}`);
|
|
126
|
+
if (m.hub === name)
|
|
127
|
+
throw new HttpError(400, `profile "${name}" is the hub — change the hub first`);
|
|
128
|
+
m.profiles.splice(idx, 1);
|
|
129
|
+
for (const s of Object.keys(m.mcpServers))
|
|
130
|
+
if (!m.profiles.some(p => p.mcp.includes(s)))
|
|
131
|
+
delete m.mcpServers[s];
|
|
132
|
+
assertSafe(m);
|
|
97
133
|
await saveManifest(ctx.manifestRoot, m);
|
|
98
134
|
await applyAndReport(m);
|
|
99
135
|
sendJson(res, 200, { ok: true });
|
|
@@ -137,6 +173,35 @@ export function buildRoutes(ctx) {
|
|
|
137
173
|
}
|
|
138
174
|
sendJson(res, 200, { problems });
|
|
139
175
|
});
|
|
176
|
+
// ── shell rc ────────────────────────────────────────────────────────────────
|
|
177
|
+
function currentRcBlock() {
|
|
178
|
+
if (!existsSync(ctx.platform.rcFile))
|
|
179
|
+
return null;
|
|
180
|
+
const rc = readFileSync(ctx.platform.rcFile, 'utf8');
|
|
181
|
+
const start = rc.indexOf(BEGIN_MARK);
|
|
182
|
+
const end = rc.indexOf(END_MARK);
|
|
183
|
+
if (start === -1 || end === -1 || end < start)
|
|
184
|
+
return null;
|
|
185
|
+
return rc.slice(start, end + END_MARK.length);
|
|
186
|
+
}
|
|
187
|
+
add('GET', /^\/api\/rc$/, async (_m, _req, res) => {
|
|
188
|
+
const m = await mustManifest();
|
|
189
|
+
const rendered = renderRcBlock(m, ctx.platform);
|
|
190
|
+
const current = currentRcBlock();
|
|
191
|
+
sendJson(res, 200, { rcFile: ctx.platform.rcFile, current, rendered, inSync: current === rendered });
|
|
192
|
+
});
|
|
193
|
+
add('POST', /^\/api\/rc$/, async (_m, _req, res) => {
|
|
194
|
+
const m = await mustManifest();
|
|
195
|
+
const rendered = renderRcBlock(m, ctx.platform);
|
|
196
|
+
let rc = '';
|
|
197
|
+
let backupDir = null;
|
|
198
|
+
if (existsSync(ctx.platform.rcFile)) {
|
|
199
|
+
rc = readFileSync(ctx.platform.rcFile, 'utf8');
|
|
200
|
+
backupDir = await backupFiles([ctx.platform.rcFile], ctx.backupRoot, stamp());
|
|
201
|
+
}
|
|
202
|
+
await atomicWrite(ctx.platform.rcFile, upsertManagedBlock(rc, rendered));
|
|
203
|
+
sendJson(res, 200, { ok: true, backupDir });
|
|
204
|
+
});
|
|
140
205
|
// ── mcp ─────────────────────────────────────────────────────────────────────
|
|
141
206
|
add('GET', /^\/api\/mcp$/, async (_m, _req, res) => {
|
|
142
207
|
const m = await mustManifest();
|
|
@@ -160,6 +225,7 @@ export function buildRoutes(ctx) {
|
|
|
160
225
|
if (!pr.mcp.includes(name))
|
|
161
226
|
pr.mcp.push(name);
|
|
162
227
|
}
|
|
228
|
+
assertSafe(m);
|
|
163
229
|
await saveManifest(ctx.manifestRoot, m);
|
|
164
230
|
await applyAndReport(m);
|
|
165
231
|
sendJson(res, 200, { ok: true });
|
|
@@ -174,6 +240,7 @@ export function buildRoutes(ctx) {
|
|
|
174
240
|
}
|
|
175
241
|
if (!m.profiles.some(p => p.mcp.includes(name)))
|
|
176
242
|
delete m.mcpServers[name];
|
|
243
|
+
assertSafe(m);
|
|
177
244
|
await saveManifest(ctx.manifestRoot, m);
|
|
178
245
|
await applyAndReport(m);
|
|
179
246
|
sendJson(res, 200, { ok: true });
|
|
@@ -189,6 +256,7 @@ export function buildRoutes(ctx) {
|
|
|
189
256
|
continue;
|
|
190
257
|
m.profiles.find(p => p.name === t).mcp = [...src.mcp];
|
|
191
258
|
}
|
|
259
|
+
assertSafe(m);
|
|
192
260
|
await saveManifest(ctx.manifestRoot, m);
|
|
193
261
|
await applyAndReport(m);
|
|
194
262
|
sendJson(res, 200, { ok: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--popover: 0 0% 100%;--popover-foreground: 240 10% 3.9%;--primary: 240 5.9% 10%;--primary-foreground: 0 0% 98%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 240 5.9% 10%;--radius: .5rem}.dark{--background: 240 10% 3.9%;--foreground: 0 0% 98%;--card: 240 10% 3.9%;--card-foreground: 0 0% 98%;--popover: 240 10% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 240 5.9% 10%;--secondary: 240 3.7% 15.9%;--secondary-foreground: 0 0% 98%;--muted: 240 3.7% 15.9%;--muted-foreground: 240 5% 64.9%;--accent: 240 3.7% 15.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 240 3.7% 15.9%;--input: 240 3.7% 15.9%;--ring: 240 4.9% 83.9%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-family:ui-sans-serif,system-ui,-apple-system,sans-serif}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1\/2{left:50%}.right-4{right:1rem}.top-1\/2{top:50%}.top-4{top:1rem}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mb-1\.5{margin-bottom:.375rem}.ml-0\.5{margin-left:.125rem}.ml-3{margin-left:.75rem}.mt-1\.5{margin-top:.375rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-screen{height:100vh}.max-h-\[85vh\]{max-height:85vh}.w-32{width:8rem}.w-4{width:1rem}.w-52{width:13rem}.w-56{width:14rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-input{border-color:hsl(var(--input))}.border-transparent{border-color:transparent}.bg-accent{background-color:hsl(var(--accent))}.bg-background{background-color:hsl(var(--background))}.bg-black\/70{background-color:#000000b3}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-green-500\/10{background-color:#22c55e1a}.bg-primary{background-color:hsl(var(--primary))}.bg-red-500\/10{background-color:#ef44441a}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pl-5{padding-left:1.25rem}.pt-1{padding-top:.25rem}.text-left{text-align:left}.text-center{text-align:center}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-accent-foreground{color:hsl(var(--accent-foreground))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-foreground{color:hsl(var(--foreground))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.underline-offset-4{text-underline-offset:4px}.opacity-70{opacity:.7}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0px}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-accent\/50:hover{background-color:hsl(var(--accent) / .5)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group.toaster .group-\[\.toaster\]\:border-border{border-color:hsl(var(--border))}.group.toaster .group-\[\.toaster\]\:bg-background{background-color:hsl(var(--background))}.group.toaster .group-\[\.toaster\]\:text-foreground{color:hsl(var(--foreground))}.group.toaster .group-\[\.toaster\]\:shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}@media (min-width: 640px){.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:text-left{text-align:left}}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|