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/ui/api.js CHANGED
@@ -1,10 +1,11 @@
1
- import { discoverProfiles, buildManifest, saveManifest, planApply, executeApply, ensureRootGitignore, loadManifest, loadDevices, fetchRemote, fetchSecrets, writeAssets, parseManifest, backupFiles, } from 'ccprofiles-core';
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 = planApply(m, await discoverProfiles(ctx.home), ctx.platform);
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 pr = m.profiles.find(p => p.name === mtch[1]);
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: ${mtch[1]}`);
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
- if (body.links)
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 = planApply(m, await discoverProfiles(ctx.home), ctx.platform);
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
- for (const lp of live)
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 (existsSync(join(ctx.manifestRoot, 'manifest.yaml'))) {
131
- const m = await loadManifest(ctx.manifestRoot);
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(decodeURIComponent(mtch[1]));
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
- sendJson(res, 200, { migrated: await migrateRcSecrets(ctx) });
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
- if (!dryRun) {
236
- await backupFiles([join(ctx.manifestRoot, 'manifest.yaml')], ctx.backupRoot, stamp());
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,3 @@
1
+ import type { Command } from 'commander';
2
+ import type { CliContext } from '../context.js';
3
+ export declare function registerUiCommand(program: Command, ctx: CliContext): void;
@@ -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,9 @@
1
+ import type { CliContext } from '../context.js';
2
+ export declare function startUiServer(ctx: CliContext, opts: {
3
+ port?: number;
4
+ token: string;
5
+ uiDir: string;
6
+ }): Promise<{
7
+ port: number;
8
+ close: () => Promise<void>;
9
+ }>;
@@ -0,0 +1,2 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ export declare function serveStatic(res: ServerResponse, urlPath: string, uiDir: string): void;
@@ -0,0 +1,3 @@
1
+ export declare function newUiToken(): string;
2
+ export declare function tokenOk(provided: string | undefined, expected: string): boolean;
3
+ export declare function originOk(origin: string | undefined, port: number): boolean;
@@ -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}