claude-account-sync 0.2.1 → 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/dist/commands/bundle.d.ts +3 -0
- package/dist/commands/bundle.js +4 -2
- package/dist/commands/manifest.d.ts +3 -0
- package/dist/commands/manifest.js +14 -4
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +3 -2
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +21 -5
- package/dist/commands/secrets.d.ts +14 -0
- package/dist/commands/secrets.js +37 -3
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.js +17 -8
- package/dist/context.d.ts +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/plan.d.ts +6 -0
- package/dist/plan.js +19 -0
- package/dist/ui/api.d.ts +3 -0
- package/dist/ui/api.js +160 -22
- package/dist/ui/command.d.ts +3 -0
- package/dist/ui/http.d.ts +20 -0
- package/dist/ui/server.d.ts +9 -0
- package/dist/ui/static.d.ts +2 -0
- package/dist/ui/token.d.ts +3 -0
- package/dist/webui/assets/index-B9Ee16GZ.css +1 -0
- package/dist/webui/assets/index-dw8cq3Ri.js +143 -0
- package/dist/webui/index.html +2 -2
- package/package.json +32 -7
- package/dist/webui/assets/index-DZPGFb5T.css +0 -1
- package/dist/webui/assets/index-Dr2jwana.js +0 -136
package/dist/ui/api.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { discoverProfiles, buildManifest, saveManifest,
|
|
1
|
+
import { discoverProfiles, buildManifest, saveManifest, executeApply, ensureRootGitignore, loadManifest, loadDevices, fetchRemote, fetchSecrets, writeAssets, parseManifest, backupFiles, renderRcBlock, upsertManagedBlock, atomicWrite, BEGIN_MARK, END_MARK, assertSafeManifest, preserveSecretRefs, } 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';
|
|
5
5
|
import { requireManifest } from '../context.js';
|
|
6
|
-
import { secretsStore, migrateRcSecrets } from '../commands/secrets.js';
|
|
6
|
+
import { secretsStore, migrateRcSecrets, migrateSettingsSecrets, KEY_VARS } from '../commands/secrets.js';
|
|
7
7
|
import { sendJson, readJson, HttpError } from './http.js';
|
|
8
|
+
import { planActions, planActionsPreflight } from '../plan.js';
|
|
8
9
|
function stamp() { return new Date().toISOString().replace(/[:.]/g, '-'); }
|
|
9
10
|
function profileName(dirName) {
|
|
10
11
|
return dirName === '.claude' ? 'default' : dirName.slice('.claude-'.length);
|
|
@@ -12,8 +13,20 @@ function profileName(dirName) {
|
|
|
12
13
|
export function buildRoutes(ctx) {
|
|
13
14
|
const routes = [];
|
|
14
15
|
const add = (method, pattern, handler) => routes.push({ method, pattern, handler });
|
|
16
|
+
// A missing secret ref is a recoverable/user-facing state (not a server error) — surface as 409.
|
|
17
|
+
async function planActionsOr409(m) {
|
|
18
|
+
try {
|
|
19
|
+
return await planActions(ctx, m);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
const msg = e.message;
|
|
23
|
+
if (/secret not found/.test(msg))
|
|
24
|
+
throw new HttpError(409, msg);
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
async function applyAndReport(m) {
|
|
16
|
-
const actions =
|
|
29
|
+
const actions = await planActionsOr409(m);
|
|
17
30
|
return executeApply(actions, { backupRoot: ctx.backupRoot, stamp: stamp() });
|
|
18
31
|
}
|
|
19
32
|
// requireManifest throws a plain Error; surface "no manifest yet" as a 409 the UI can detect.
|
|
@@ -25,6 +38,15 @@ export function buildRoutes(ctx) {
|
|
|
25
38
|
throw new HttpError(409, e.message);
|
|
26
39
|
}
|
|
27
40
|
}
|
|
41
|
+
// Guard mutation routes: never persist a manifest that would interpolate unsafely into the rc file.
|
|
42
|
+
function assertSafe(m) {
|
|
43
|
+
try {
|
|
44
|
+
assertSafeManifest(m);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
throw new HttpError(400, e.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
28
50
|
function targetsOf(m, targets) {
|
|
29
51
|
if (targets === 'all')
|
|
30
52
|
return m.profiles.map(p => p.name);
|
|
@@ -36,6 +58,11 @@ export function buildRoutes(ctx) {
|
|
|
36
58
|
// ── adopt / profiles / status / apply / doctor ──────────────────────────────
|
|
37
59
|
add('POST', /^\/api\/adopt$/, async (_m, _req, res) => {
|
|
38
60
|
const manifest = buildManifest(await discoverProfiles(ctx.home), ctx.platform);
|
|
61
|
+
if (existsSync(join(ctx.manifestRoot, 'manifest.yaml'))) {
|
|
62
|
+
const oldM = await loadManifest(ctx.manifestRoot);
|
|
63
|
+
let store = null;
|
|
64
|
+
await preserveSecretRefs(manifest, oldM, async (name) => { store ??= await secretsStore(ctx); return store.get(name); });
|
|
65
|
+
}
|
|
39
66
|
if (!existsSync(join(ctx.manifestRoot, '.git'))) {
|
|
40
67
|
try {
|
|
41
68
|
execFileSync('git', ['init', ctx.manifestRoot], { stdio: 'ignore' });
|
|
@@ -59,6 +86,8 @@ export function buildRoutes(ctx) {
|
|
|
59
86
|
account: lp.account, mcp: Object.keys(lp.mcpServers).length,
|
|
60
87
|
launcher: decl?.launcher ?? (name === 'default' ? null : `cl-${name}`),
|
|
61
88
|
adopted: !!decl,
|
|
89
|
+
env: decl?.env ?? {}, links: decl?.links ?? {}, mcpNames: decl?.mcp ?? [],
|
|
90
|
+
settingsEnv: decl?.settingsEnv ?? {}, liveSettingsEnv: lp.settingsEnv,
|
|
62
91
|
};
|
|
63
92
|
});
|
|
64
93
|
sendJson(res, 200, rows);
|
|
@@ -77,30 +106,69 @@ export function buildRoutes(ctx) {
|
|
|
77
106
|
name, dir: `{home}/.claude-${name}`, launcher: `cl-${name}`, auth: 'env', env: {},
|
|
78
107
|
links: src ? { ...src.links } : (m.hub ? { skills: 'hub', commands: 'hub' } : {}),
|
|
79
108
|
mcp: src ? [...src.mcp] : [],
|
|
109
|
+
settingsEnv: {},
|
|
80
110
|
});
|
|
111
|
+
assertSafe(m);
|
|
81
112
|
await saveManifest(ctx.manifestRoot, m);
|
|
82
113
|
await applyAndReport(m);
|
|
83
114
|
sendJson(res, 200, { ok: true });
|
|
84
115
|
});
|
|
85
116
|
add('PATCH', /^\/api\/profiles\/([^/]+)$/, async (mtch, req, res) => {
|
|
86
117
|
const m = await mustManifest();
|
|
87
|
-
const
|
|
118
|
+
const name = decodeURIComponent(mtch[1]);
|
|
119
|
+
const pr = m.profiles.find(p => p.name === name);
|
|
88
120
|
if (!pr)
|
|
89
|
-
throw new HttpError(404, `unknown profile: ${
|
|
121
|
+
throw new HttpError(404, `unknown profile: ${name}`);
|
|
90
122
|
const body = await readJson(req);
|
|
91
|
-
if (body.env)
|
|
123
|
+
if (body.env) {
|
|
124
|
+
if (!Object.values(body.env).every(v => typeof v === 'string'))
|
|
125
|
+
throw new HttpError(400, 'env values must be strings');
|
|
92
126
|
pr.env = body.env;
|
|
93
|
-
|
|
127
|
+
}
|
|
128
|
+
if (body.links) {
|
|
129
|
+
if (!Object.values(body.links).every(v => typeof v === 'string'))
|
|
130
|
+
throw new HttpError(400, 'links values must be strings');
|
|
94
131
|
pr.links = body.links;
|
|
132
|
+
}
|
|
95
133
|
if (body.launcher !== undefined)
|
|
96
134
|
pr.launcher = body.launcher;
|
|
135
|
+
if (body.settingsEnv) {
|
|
136
|
+
for (const v of Object.values(body.settingsEnv))
|
|
137
|
+
if (typeof v !== 'string')
|
|
138
|
+
throw new HttpError(400, 'settingsEnv values must be strings');
|
|
139
|
+
pr.settingsEnv = body.settingsEnv;
|
|
140
|
+
}
|
|
141
|
+
assertSafe(m);
|
|
142
|
+
try {
|
|
143
|
+
await planActionsPreflight(ctx, m);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
throw new HttpError(400, e.message);
|
|
147
|
+
}
|
|
148
|
+
await saveManifest(ctx.manifestRoot, m);
|
|
149
|
+
await applyAndReport(m);
|
|
150
|
+
sendJson(res, 200, { ok: true });
|
|
151
|
+
});
|
|
152
|
+
add('DELETE', /^\/api\/profiles\/([^/]+)$/, async (mtch, _req, res) => {
|
|
153
|
+
const m = await mustManifest();
|
|
154
|
+
const name = decodeURIComponent(mtch[1]);
|
|
155
|
+
const idx = m.profiles.findIndex(p => p.name === name);
|
|
156
|
+
if (idx === -1)
|
|
157
|
+
throw new HttpError(404, `unknown profile: ${name}`);
|
|
158
|
+
if (m.hub === name)
|
|
159
|
+
throw new HttpError(400, `profile "${name}" is the hub — change the hub first`);
|
|
160
|
+
m.profiles.splice(idx, 1);
|
|
161
|
+
for (const s of Object.keys(m.mcpServers))
|
|
162
|
+
if (!m.profiles.some(p => p.mcp.includes(s)))
|
|
163
|
+
delete m.mcpServers[s];
|
|
164
|
+
assertSafe(m);
|
|
97
165
|
await saveManifest(ctx.manifestRoot, m);
|
|
98
166
|
await applyAndReport(m);
|
|
99
167
|
sendJson(res, 200, { ok: true });
|
|
100
168
|
});
|
|
101
169
|
add('GET', /^\/api\/status$/, async (_m, _req, res) => {
|
|
102
170
|
const m = await mustManifest();
|
|
103
|
-
const actions =
|
|
171
|
+
const actions = await planActionsOr409(m);
|
|
104
172
|
const r = await executeApply(actions, { backupRoot: ctx.backupRoot, stamp: stamp(), dryRun: true });
|
|
105
173
|
sendJson(res, 200, { inSync: actions.length === 0, pending: r.performed });
|
|
106
174
|
});
|
|
@@ -112,7 +180,10 @@ export function buildRoutes(ctx) {
|
|
|
112
180
|
add('GET', /^\/api\/doctor$/, async (_m, _req, res) => {
|
|
113
181
|
const problems = [];
|
|
114
182
|
const live = await discoverProfiles(ctx.home);
|
|
115
|
-
|
|
183
|
+
let man = null;
|
|
184
|
+
if (existsSync(join(ctx.manifestRoot, 'manifest.yaml')))
|
|
185
|
+
man = await loadManifest(ctx.manifestRoot);
|
|
186
|
+
for (const lp of live) {
|
|
116
187
|
for (const nm of readdirSync(lp.dir)) {
|
|
117
188
|
const f = join(lp.dir, nm);
|
|
118
189
|
try {
|
|
@@ -121,22 +192,57 @@ export function buildRoutes(ctx) {
|
|
|
121
192
|
}
|
|
122
193
|
catch { /* skip */ }
|
|
123
194
|
}
|
|
195
|
+
const decl = man?.profiles.find(p => p.name === profileName(lp.dirName)) ?? null;
|
|
196
|
+
for (const varName of KEY_VARS) {
|
|
197
|
+
if (lp.settingsEnv[varName] && !decl?.settingsEnv[varName])
|
|
198
|
+
problems.push(`plaintext token ${varName} in ${join(lp.dir, 'settings.json')} — adopt profile then run: secrets migrate`);
|
|
199
|
+
if (decl?.settingsEnv[varName] && !decl.settingsEnv[varName].startsWith('secret://'))
|
|
200
|
+
problems.push(`plaintext token ${varName} in manifest for profile "${profileName(lp.dirName)}" — run: secrets migrate`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
124
203
|
if (existsSync(ctx.platform.rcFile)) {
|
|
125
204
|
const rc = readFileSync(ctx.platform.rcFile, 'utf8');
|
|
126
205
|
const outside = rc.split('# >>> ccprofiles managed >>>')[0] + (rc.split('# <<< ccprofiles managed <<<')[1] ?? '');
|
|
127
206
|
if (/sk-ant-/.test(outside))
|
|
128
207
|
problems.push(`plaintext Anthropic key in ${ctx.platform.rcFile} — run secrets migrate`);
|
|
129
208
|
}
|
|
130
|
-
if (
|
|
131
|
-
const
|
|
132
|
-
for (const pr of m.profiles) {
|
|
209
|
+
if (man)
|
|
210
|
+
for (const pr of man.profiles) {
|
|
133
211
|
const dir = pr.dir.replace('{home}', ctx.home);
|
|
134
212
|
if (!existsSync(dir))
|
|
135
213
|
problems.push(`manifest profile "${pr.name}" missing on disk: ${dir} — run apply`);
|
|
136
214
|
}
|
|
137
|
-
}
|
|
138
215
|
sendJson(res, 200, { problems });
|
|
139
216
|
});
|
|
217
|
+
// ── shell rc ────────────────────────────────────────────────────────────────
|
|
218
|
+
function currentRcBlock() {
|
|
219
|
+
if (!existsSync(ctx.platform.rcFile))
|
|
220
|
+
return null;
|
|
221
|
+
const rc = readFileSync(ctx.platform.rcFile, 'utf8');
|
|
222
|
+
const start = rc.indexOf(BEGIN_MARK);
|
|
223
|
+
const end = rc.indexOf(END_MARK);
|
|
224
|
+
if (start === -1 || end === -1 || end < start)
|
|
225
|
+
return null;
|
|
226
|
+
return rc.slice(start, end + END_MARK.length);
|
|
227
|
+
}
|
|
228
|
+
add('GET', /^\/api\/rc$/, async (_m, _req, res) => {
|
|
229
|
+
const m = await mustManifest();
|
|
230
|
+
const rendered = renderRcBlock(m, ctx.platform);
|
|
231
|
+
const current = currentRcBlock();
|
|
232
|
+
sendJson(res, 200, { rcFile: ctx.platform.rcFile, current, rendered, inSync: current === rendered });
|
|
233
|
+
});
|
|
234
|
+
add('POST', /^\/api\/rc$/, async (_m, _req, res) => {
|
|
235
|
+
const m = await mustManifest();
|
|
236
|
+
const rendered = renderRcBlock(m, ctx.platform);
|
|
237
|
+
let rc = '';
|
|
238
|
+
let backupDir = null;
|
|
239
|
+
if (existsSync(ctx.platform.rcFile)) {
|
|
240
|
+
rc = readFileSync(ctx.platform.rcFile, 'utf8');
|
|
241
|
+
backupDir = await backupFiles([ctx.platform.rcFile], ctx.backupRoot, stamp());
|
|
242
|
+
}
|
|
243
|
+
await atomicWrite(ctx.platform.rcFile, upsertManagedBlock(rc, rendered));
|
|
244
|
+
sendJson(res, 200, { ok: true, backupDir });
|
|
245
|
+
});
|
|
140
246
|
// ── mcp ─────────────────────────────────────────────────────────────────────
|
|
141
247
|
add('GET', /^\/api\/mcp$/, async (_m, _req, res) => {
|
|
142
248
|
const m = await mustManifest();
|
|
@@ -160,6 +266,7 @@ export function buildRoutes(ctx) {
|
|
|
160
266
|
if (!pr.mcp.includes(name))
|
|
161
267
|
pr.mcp.push(name);
|
|
162
268
|
}
|
|
269
|
+
assertSafe(m);
|
|
163
270
|
await saveManifest(ctx.manifestRoot, m);
|
|
164
271
|
await applyAndReport(m);
|
|
165
272
|
sendJson(res, 200, { ok: true });
|
|
@@ -174,6 +281,7 @@ export function buildRoutes(ctx) {
|
|
|
174
281
|
}
|
|
175
282
|
if (!m.profiles.some(p => p.mcp.includes(name)))
|
|
176
283
|
delete m.mcpServers[name];
|
|
284
|
+
assertSafe(m);
|
|
177
285
|
await saveManifest(ctx.manifestRoot, m);
|
|
178
286
|
await applyAndReport(m);
|
|
179
287
|
sendJson(res, 200, { ok: true });
|
|
@@ -189,6 +297,7 @@ export function buildRoutes(ctx) {
|
|
|
189
297
|
continue;
|
|
190
298
|
m.profiles.find(p => p.name === t).mcp = [...src.mcp];
|
|
191
299
|
}
|
|
300
|
+
assertSafe(m);
|
|
192
301
|
await saveManifest(ctx.manifestRoot, m);
|
|
193
302
|
await applyAndReport(m);
|
|
194
303
|
sendJson(res, 200, { ok: true });
|
|
@@ -214,12 +323,31 @@ export function buildRoutes(ctx) {
|
|
|
214
323
|
sendJson(res, 200, { ok: true });
|
|
215
324
|
});
|
|
216
325
|
add('DELETE', /^\/api\/secrets\/([^/]+)$/, async (mtch, _req, res) => {
|
|
326
|
+
const name = decodeURIComponent(mtch[1]);
|
|
327
|
+
const ref = `secret://${name}`;
|
|
328
|
+
if (existsSync(join(ctx.manifestRoot, 'manifest.yaml'))) {
|
|
329
|
+
const m = await loadManifest(ctx.manifestRoot);
|
|
330
|
+
for (const pr of m.profiles) {
|
|
331
|
+
for (const [k, v] of Object.entries(pr.env))
|
|
332
|
+
if (v === ref)
|
|
333
|
+
throw new HttpError(409, `secret "${name}" is referenced by profile "${pr.name}" (${k}) — detach it first`);
|
|
334
|
+
for (const [k, v] of Object.entries(pr.settingsEnv))
|
|
335
|
+
if (v === ref)
|
|
336
|
+
throw new HttpError(409, `secret "${name}" is referenced by profile "${pr.name}" (${k}) — detach it first`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
217
339
|
const store = await secretsStore(ctx);
|
|
218
|
-
await store.delete(
|
|
340
|
+
await store.delete(name);
|
|
219
341
|
sendJson(res, 200, { ok: true });
|
|
220
342
|
});
|
|
221
343
|
add('POST', /^\/api\/secrets\/migrate$/, async (_m, _req, res) => {
|
|
222
|
-
|
|
344
|
+
const migrated = [...await migrateRcSecrets(ctx), ...await migrateSettingsSecrets(ctx)];
|
|
345
|
+
// Apply so the manifest's newly-minted secret:// refs are (re-)resolved and written back out.
|
|
346
|
+
if (migrated.length && existsSync(join(ctx.manifestRoot, 'manifest.yaml'))) {
|
|
347
|
+
const m = await loadManifest(ctx.manifestRoot);
|
|
348
|
+
await applyAndReport(m);
|
|
349
|
+
}
|
|
350
|
+
sendJson(res, 200, { migrated });
|
|
223
351
|
});
|
|
224
352
|
// ── devices / sync ──────────────────────────────────────────────────────────
|
|
225
353
|
add('GET', /^\/api\/devices$/, async (_m, _req, res) => {
|
|
@@ -232,13 +360,8 @@ export function buildRoutes(ctx) {
|
|
|
232
360
|
throw new HttpError(400, `unknown device: ${from}`);
|
|
233
361
|
const { manifestYaml, assets } = await fetchRemote(device);
|
|
234
362
|
const m = parseManifest(manifestYaml);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await saveManifest(ctx.manifestRoot, m);
|
|
238
|
-
await writeAssets(assets, m, ctx.platform);
|
|
239
|
-
}
|
|
240
|
-
const actions = planApply(m, await discoverProfiles(ctx.home), ctx.platform);
|
|
241
|
-
const r = await executeApply(actions, { backupRoot: ctx.backupRoot, stamp: stamp(), dryRun: !!dryRun });
|
|
363
|
+
// Fetch+store secrets BEFORE touching local state, so a preflight below that needs
|
|
364
|
+
// a just-transferred secret succeeds instead of failing after the manifest is overwritten.
|
|
242
365
|
let secrets = [];
|
|
243
366
|
if (withSecrets) {
|
|
244
367
|
const values = await fetchSecrets(device, []);
|
|
@@ -249,6 +372,21 @@ export function buildRoutes(ctx) {
|
|
|
249
372
|
}
|
|
250
373
|
secrets = Object.keys(values);
|
|
251
374
|
}
|
|
375
|
+
// Validate the pulled manifest resolves cleanly before saving/applying anything — a peer
|
|
376
|
+
// manifest with a settingsEnv secret ref we don't have must not overwrite local state.
|
|
377
|
+
try {
|
|
378
|
+
await planActionsPreflight(ctx, m);
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
throw new HttpError(409, e.message);
|
|
382
|
+
}
|
|
383
|
+
if (!dryRun) {
|
|
384
|
+
await backupFiles([join(ctx.manifestRoot, 'manifest.yaml')], ctx.backupRoot, stamp());
|
|
385
|
+
await saveManifest(ctx.manifestRoot, m);
|
|
386
|
+
await writeAssets(assets, m, ctx.platform);
|
|
387
|
+
}
|
|
388
|
+
const actions = await planActions(ctx, m);
|
|
389
|
+
const r = await executeApply(actions, { backupRoot: ctx.backupRoot, stamp: stamp(), dryRun: !!dryRun });
|
|
252
390
|
sendJson(res, 200, { performed: r.performed, secrets });
|
|
253
391
|
});
|
|
254
392
|
return routes;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export declare class HttpError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
constructor(status: number, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class BadRequest extends HttpError {
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function readBody(req: IncomingMessage): Promise<string>;
|
|
10
|
+
export declare function readJson<T = any>(req: IncomingMessage): Promise<T>;
|
|
11
|
+
export declare function sendJson(res: ServerResponse, code: number, body: unknown): void;
|
|
12
|
+
export type Route = {
|
|
13
|
+
method: string;
|
|
14
|
+
pattern: RegExp;
|
|
15
|
+
handler: (m: RegExpMatchArray, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export declare function matchRoute(routes: Route[], method: string, path: string): {
|
|
18
|
+
route: Route;
|
|
19
|
+
match: RegExpMatchArray;
|
|
20
|
+
} | null;
|
|
@@ -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-40{width:10rem}.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}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,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}.pt-2{padding-top:.5rem}.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}
|