@alashi/cli 0.11.0 → 0.12.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/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { A as UsageRecorder, E as budgetStatus, O as setAppBudget, b as loadApp, c as installApp, d as updateApp, f as addRegistry, h as removeRegistry, i as resolveAppConfig, n as createServer, o as exportApp, s as importApp, t as Store, u as removeApp } from "./store-VoJe_U1Q.mjs";
2
+ import { A as UsageRecorder, E as budgetStatus, N as AuthService, O as setAppBudget, b as loadApp, c as installApp, d as updateApp, f as addRegistry, h as removeRegistry, i as resolveAppConfig, n as createServer, o as exportApp, s as importApp, t as Store, u as removeApp } from "./store-BNZqhcwm.mjs";
3
3
  import { APP_NAME_PATTERN, AppSdkError } from "@alashi/apps-sdk";
4
4
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
5
  import { isAbsolute, join, relative, resolve } from "node:path";
@@ -48,6 +48,8 @@ async function serveDashboard(portArg, hostArg) {
48
48
  const port = portArg ? Number(portArg) : config.port ?? 4141;
49
49
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${portArg}`);
50
50
  const hostname = hostArg ?? config.host ?? "127.0.0.1";
51
+ const loopback = hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
52
+ const mode = config.auth?.mode ?? "auto";
51
53
  const server = createServer(store, new ClaudeCodeProvider());
52
54
  await server.scheduler.start();
53
55
  const httpServer = serve({
@@ -56,7 +58,12 @@ async function serveDashboard(portArg, hostArg) {
56
58
  hostname
57
59
  }, (info) => {
58
60
  console.log(`alashi dashboard at http://localhost:${info.port}`);
59
- if (hostname !== "127.0.0.1" && hostname !== "localhost") console.log(` WARNING: listening on ${hostname} — the dashboard has no authentication`);
61
+ if (mode === "off" && !loopback) console.log(` WARNING: listening on ${hostname} with auth.mode "off" — anyone who can reach this address owns the host`);
62
+ if (mode === "required" || mode === "auto" && !loopback) {
63
+ const secret = server.auth.issueBootToken();
64
+ console.log(` sign in: http://localhost:${info.port}/login?token=${secret}`);
65
+ console.log(" (one-time URL — mint reusable tokens with `alashi token create`)");
66
+ }
60
67
  });
61
68
  const shutdown = () => {
62
69
  httpServer.close();
@@ -65,7 +72,7 @@ async function serveDashboard(portArg, hostArg) {
65
72
  process.on("SIGINT", shutdown);
66
73
  process.on("SIGTERM", shutdown);
67
74
  }
68
- program.command("serve", { isDefault: true }).option("-p, --port <port>", "port to listen on (default: config port or 4141)").option("--host <host>", "address to bind (default: 127.0.0.1; the dashboard has no auth)").description("Serve the web dashboard (the default when no command is given)").action(async (options) => serveDashboard(options.port, options.host));
75
+ program.command("serve", { isDefault: true }).option("-p, --port <port>", "port to listen on (default: config port or 4141)").option("--host <host>", "address to bind (default: 127.0.0.1; non-loopback binds require sign-in)").description("Serve the web dashboard (the default when no command is given)").action(async (options) => serveDashboard(options.port, options.host));
69
76
  program.command("create").argument("<name>", "app name (lowercase letters, digits, hyphens)").argument("[directory]", "target directory (default: ./<name>)").description("Scaffold a new app from the basic template").action(async (name, directory) => {
70
77
  const target = directory ?? name;
71
78
  const files = await scaffoldApp(name, resolve(target));
@@ -78,17 +85,27 @@ program.command("create").argument("<name>", "app name (lowercase letters, digit
78
85
  console.log(" alashi # dashboard at http://localhost:4141");
79
86
  });
80
87
  program.command("install").argument("<source>", "app name (via registry), git URL, or local path").description("Install an app").action(async (source) => {
81
- const record = await installApp(store, source);
88
+ const { record, warnings } = await installApp(store, source);
82
89
  console.log(`Installed ${record.name}@${record.version} (${record.source.type})`);
83
90
  console.log(` data: ${store.appDataDir(record.name)}`);
84
91
  console.log(` config: ${store.appConfigPath(record.name)}`);
92
+ printManifestWarnings(warnings);
85
93
  await printSetupHints(record.name);
86
94
  });
95
+ /**
96
+ * Unknown manifest keys never fail a command — the host strips them and runs.
97
+ * Surfacing them here is how an author notices a typo or a too-new field at
98
+ * install time instead of only in the server log. Making them an *error* for
99
+ * authors is `alashi validate` (card #22).
100
+ */
101
+ function printManifestWarnings(warnings) {
102
+ for (const warning of warnings) console.log(` warning: ${warning}`);
103
+ }
87
104
  /** After install: what still needs configuring, and which integrations exist. */
88
105
  async function printSetupHints(name) {
89
106
  let manifest;
90
107
  try {
91
- ({manifest} = await loadApp(store.appRepoDir(name)));
108
+ ({manifest} = await loadApp(store.appRepoDir(name), { warn: () => {} }));
92
109
  } catch {
93
110
  return;
94
111
  }
@@ -102,8 +119,9 @@ async function printSetupHints(name) {
102
119
  if (status.missing.length > 0 || optional.length > 0) console.log(` configure via the dashboard settings page or the config file above`);
103
120
  }
104
121
  program.command("update").argument("<name>", "installed app name").description("Update an installed app").action(async (name) => {
105
- const record = await updateApp(store, name);
122
+ const { record, warnings } = await updateApp(store, name);
106
123
  console.log(`Updated ${record.name} to ${record.version}`);
124
+ printManifestWarnings(warnings);
107
125
  });
108
126
  program.command("remove").argument("<name>", "installed app name").option("--purge", "also delete the app data and config").description("Remove an installed app (keeps data unless --purge)").action(async (name, options) => {
109
127
  await removeApp(store, name, { purge: options.purge ?? false });
@@ -138,6 +156,7 @@ program.command("list").option("--json", "machine-readable output").description(
138
156
  return {
139
157
  ...record,
140
158
  todayCostUsd: budget?.spentUsd ?? 0,
159
+ recorded: budget?.recorded ?? false,
141
160
  overBudget: budget?.overBudget ?? false,
142
161
  ...budget?.dailyUsd !== void 0 && { dailyBudgetUsd: budget.dailyUsd }
143
162
  };
@@ -151,9 +170,9 @@ program.command("list").option("--json", "machine-readable output").description(
151
170
  for (const record of records) {
152
171
  const origin = record.source.type === "git" ? record.source.url : record.source.path;
153
172
  const budget = spend.get(record.name);
154
- const today = (budget?.spentUsd ?? 0).toFixed(4);
155
173
  const cap = budget?.dailyUsd === void 0 ? "" : ` of $${budget.dailyUsd.toFixed(2)} cap${budget.overBudget ? " (OVER BUDGET)" : ""}`;
156
- console.log(`${record.name}@${record.version} ${record.source.type} ${origin} $${today} today${cap}`);
174
+ const today = budget?.recorded ? `$${budget.spentUsd.toFixed(4)} today` : "— today";
175
+ console.log(`${record.name}@${record.version} ${record.source.type} ${origin} ${today}${cap}`);
157
176
  }
158
177
  });
159
178
  program.command("budget").argument("<name>", "installed app name").argument("[dailyUsd]", "daily cap in USD, or \"none\" to remove it (omit to just show)").description("Show or set an app's host-enforced daily AI spend cap").action(async (name, dailyUsd) => {
@@ -164,7 +183,7 @@ program.command("budget").argument("<name>", "installed app name").argument("[da
164
183
  await setAppBudget(store, name, amount);
165
184
  }
166
185
  const status = await budgetStatus(store, new UsageRecorder(store), name);
167
- const spent = `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)`;
186
+ const spent = status.recorded ? `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)` : "no spend recorded yet (the host meters only sessions opened through ctx.ai)";
168
187
  if (status.dailyUsd === void 0) {
169
188
  console.log(`${name}: no daily cap — ${spent}`);
170
189
  return;
@@ -172,6 +191,28 @@ program.command("budget").argument("<name>", "installed app name").argument("[da
172
191
  console.log(`${name}: $${status.dailyUsd.toFixed(2)} daily cap — ${spent}, $${(status.remainingUsd ?? 0).toFixed(4)} left`);
173
192
  if (status.overBudget) console.log(" OVER BUDGET: new AI sessions are refused until 00:00 UTC");
174
193
  });
194
+ const token = program.command("token").description("Manage owner bearer tokens for the dashboard/API (see config.auth)");
195
+ token.command("create").argument("[label]", "a name for the token (e.g. \"laptop\", \"ci\")").description("Mint an owner token; the secret is printed once and never stored").action(async (label) => {
196
+ const { id, secret } = await new AuthService(store).mintToken(label);
197
+ console.log(secret);
198
+ console.log(` id: ${id}${label ? ` (${label})` : ""} — sign in at /login or send it as "Authorization: Bearer ..."`);
199
+ });
200
+ token.command("list").description("List tokens (ids and labels only; secrets are never stored)").action(async () => {
201
+ const tokens = await new AuthService(store).listTokens();
202
+ if (tokens.length === 0) {
203
+ console.log("No tokens. Mint one with `alashi token create [label]`.");
204
+ return;
205
+ }
206
+ for (const entry of tokens) {
207
+ const label = entry.label ? ` ${entry.label}` : "";
208
+ const used = entry.lastUsedAt ? `, last used ${entry.lastUsedAt}` : ", never used";
209
+ console.log(`${entry.id}${label} — created ${entry.createdAt}${used}`);
210
+ }
211
+ });
212
+ token.command("revoke").argument("<id>", "token id (see `alashi token list`)").description("Revoke a token").action(async (id) => {
213
+ if (!await new AuthService(store).revokeToken(id)) throw new Error(`No token with id "${id}"`);
214
+ console.log(`Revoked ${id}`);
215
+ });
175
216
  const registry = program.command("registry").description("Manage app registries");
176
217
  registry.command("add").argument("<name>", "registry name").argument("<url>", "index.json URL or git repo URL").action(async (name, url) => {
177
218
  const index = await addRegistry(store, name, url);
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/create.ts","../src/cli.ts"],"sourcesContent":["import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { APP_NAME_PATTERN, AppSdkError } from '@alashi/apps-sdk';\n\n// Works from both src/ (tests) and dist/ (published build) — the templates\n// dir sits one level up in the package root either way.\nconst TEMPLATE_DIR = fileURLToPath(new URL('../templates/basic-app', import.meta.url));\n\n// npm strips .gitignore from published packages, so it ships unprefixed.\nconst RENAMES: Record<string, string> = { gitignore: '.gitignore' };\n\n/**\n * Scaffolds a new app from the basic-app template into targetDir,\n * substituting the app name. Returns the created files, relative to\n * targetDir. Refuses to write into a non-empty directory.\n */\nexport async function scaffoldApp(name: string, targetDir: string): Promise<string[]> {\n if (!APP_NAME_PATTERN.test(name)) {\n throw new AppSdkError(\n `Invalid app name \"${name}\": use lowercase letters, digits, and hyphens (max 64 chars)`,\n );\n }\n const existing = await readdir(targetDir).catch(() => undefined);\n if (existing && existing.length > 0) {\n throw new AppSdkError(`Directory is not empty: ${targetDir}`);\n }\n\n const written: string[] = [];\n async function copyDir(rel: string): Promise<void> {\n const entries = await readdir(join(TEMPLATE_DIR, rel), { withFileTypes: true });\n await mkdir(join(targetDir, rel), { recursive: true });\n for (const entry of entries) {\n const relPath = join(rel, entry.name);\n if (entry.isDirectory()) {\n await copyDir(relPath);\n continue;\n }\n const content = await readFile(join(TEMPLATE_DIR, relPath), 'utf8');\n const outPath = join(rel, RENAMES[entry.name] ?? entry.name);\n await writeFile(join(targetDir, outPath), content.replaceAll('__APP_NAME__', name), 'utf8');\n written.push(outPath);\n }\n }\n await copyDir('');\n return written.sort();\n}\n","#!/usr/bin/env node\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { ClaudeCodeProvider } from '@alashi/provider-claude';\nimport { serve } from '@hono/node-server';\nimport { Command } from 'commander';\nimport { budgetStatus, setAppBudget } from './budget.js';\nimport { scaffoldApp } from './create.js';\nimport { installApp, removeApp, updateApp } from './installer.js';\nimport { loadApp } from './loader.js';\nimport { exportApp, importApp } from './portability.js';\nimport { addRegistry, removeRegistry } from './registry.js';\nimport { resolveAppConfig } from './runtime.js';\nimport { createServer } from './server.js';\nimport { Store } from './store.js';\nimport { UsageRecorder } from './usage.js';\n\nconst program = new Command();\nconst store = new Store();\n\nprogram.name('alashi').description('Install, manage, and run Claude Code-powered apps');\n\nasync function serveDashboard(portArg?: string, hostArg?: string): Promise<void> {\n const config = await store.readGlobalConfig();\n const port = portArg ? Number(portArg) : (config.port ?? 4141);\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(`Invalid port: ${portArg}`);\n }\n // Loopback by default: the dashboard has no auth, so it must not be\n // LAN-reachable unless the user explicitly opts in with --host.\n const hostname = hostArg ?? config.host ?? '127.0.0.1';\n const server = createServer(store, new ClaudeCodeProvider());\n await server.scheduler.start();\n const httpServer = serve({ fetch: server.app.fetch, port, hostname }, (info) => {\n console.log(`alashi dashboard at http://localhost:${info.port}`);\n if (hostname !== '127.0.0.1' && hostname !== 'localhost') {\n console.log(` WARNING: listening on ${hostname} — the dashboard has no authentication`);\n }\n });\n const shutdown = (): void => {\n httpServer.close();\n void server.close().finally(() => process.exit(0));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\nprogram\n .command('serve', { isDefault: true })\n .option('-p, --port <port>', 'port to listen on (default: config port or 4141)')\n .option('--host <host>', 'address to bind (default: 127.0.0.1; the dashboard has no auth)')\n .description('Serve the web dashboard (the default when no command is given)')\n .action(async (options: { port?: string; host?: string }) =>\n serveDashboard(options.port, options.host),\n );\n\nprogram\n .command('create')\n .argument('<name>', 'app name (lowercase letters, digits, hyphens)')\n .argument('[directory]', 'target directory (default: ./<name>)')\n .description('Scaffold a new app from the basic template')\n .action(async (name: string, directory?: string) => {\n const target = directory ?? name;\n const files = await scaffoldApp(name, resolve(target));\n console.log(`Created ${name}/`);\n for (const file of files) console.log(` ${file}`);\n console.log('\\nNext steps:');\n console.log(` cd ${target}`);\n console.log(' npm install');\n console.log(' alashi install .');\n console.log(' alashi # dashboard at http://localhost:4141');\n });\n\nprogram\n .command('install')\n .argument('<source>', 'app name (via registry), git URL, or local path')\n .description('Install an app')\n .action(async (source: string) => {\n const record = await installApp(store, source);\n console.log(`Installed ${record.name}@${record.version} (${record.source.type})`);\n console.log(` data: ${store.appDataDir(record.name)}`);\n console.log(` config: ${store.appConfigPath(record.name)}`);\n await printSetupHints(record.name);\n });\n\n/** After install: what still needs configuring, and which integrations exist. */\nasync function printSetupHints(name: string): Promise<void> {\n let manifest: Awaited<ReturnType<typeof loadApp>>['manifest'];\n try {\n ({ manifest } = await loadApp(store.appRepoDir(name)));\n } catch {\n return; // install already succeeded; hints are best-effort\n }\n const { status } = await resolveAppConfig(store, manifest);\n if (status.missing.length > 0) {\n console.log(` requires: ${status.missing.join(', ')}`);\n }\n const optional = status.integrations.filter((i) => i.optional && !i.configured);\n if (optional.length > 0) {\n const list = optional.map((i) => `${i.name} (needs ${i.missing.join(', ')})`).join(', ');\n console.log(` optional integrations: ${list}`);\n }\n if (status.missing.length > 0 || optional.length > 0) {\n console.log(` configure via the dashboard settings page or the config file above`);\n }\n}\n\nprogram\n .command('update')\n .argument('<name>', 'installed app name')\n .description('Update an installed app')\n .action(async (name: string) => {\n const record = await updateApp(store, name);\n console.log(`Updated ${record.name} to ${record.version}`);\n });\n\nprogram\n .command('remove')\n .argument('<name>', 'installed app name')\n .option('--purge', 'also delete the app data and config')\n .description('Remove an installed app (keeps data unless --purge)')\n .action(async (name: string, options: { purge?: boolean }) => {\n await removeApp(store, name, { purge: options.purge ?? false });\n console.log(options.purge ? `Removed ${name} (purged)` : `Removed ${name} (data kept)`);\n });\n\n/** Prints archive paths the way the user would type them. */\nfunction displayPath(path: string): string {\n const rel = relative(process.cwd(), path);\n return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? path : `./${rel}`;\n}\n\nprogram\n .command('export')\n .argument('<name>', 'installed app name')\n .option('-o, --out <file>', 'archive path (default: ./<name>-<version>.tar.gz)')\n .description('Pack an app container and its chat transcripts into a tar.gz')\n .action(async (name: string, options: { out?: string }) => {\n const result = await exportApp(store, new ClaudeCodeProvider(), name, {\n ...(options.out && { out: options.out }),\n });\n for (const warning of result.warnings) console.log(` warning: ${warning}`);\n console.log(\n `Exported ${result.record.name}@${result.record.version} -> ${displayPath(result.archivePath)} (${result.transcriptCount} transcripts)`,\n );\n if (result.record.source.type === 'local') {\n console.log(' note: repo/ was captured as real files, not as the local symlink');\n }\n });\n\n// `import` is a reserved word, so the handler cannot be named after it; the\n// commander command string itself is fine.\nasync function importArchive(file: string): Promise<void> {\n const result = await importApp(store, new ClaudeCodeProvider(), file);\n for (const warning of result.warnings) console.log(` warning: ${warning}`);\n console.log(\n `Imported ${result.record.name}@${result.record.version} (${result.transcriptCount} transcripts restored)`,\n );\n console.log(` data: ${store.appDataDir(result.record.name)}`);\n console.log(` config: ${store.appConfigPath(result.record.name)}`);\n}\n\nprogram\n .command('import')\n .argument('<file>', 'archive produced by alashi export')\n .description('Restore an app container and its chat transcripts from a tar.gz')\n .action(importArchive);\n\nprogram\n .command('list')\n .option('--json', 'machine-readable output')\n .description('List installed apps')\n .action(async (options: { json?: boolean }) => {\n const usage = new UsageRecorder(store);\n const records = await store.listInstalled();\n const spend = new Map(\n await Promise.all(\n records.map(\n async (record) => [record.name, await budgetStatus(store, usage, record.name)] as const,\n ),\n ),\n );\n if (options.json) {\n console.log(\n JSON.stringify(\n records.map((record) => {\n const budget = spend.get(record.name);\n return {\n ...record,\n todayCostUsd: budget?.spentUsd ?? 0,\n overBudget: budget?.overBudget ?? false,\n ...(budget?.dailyUsd !== undefined && { dailyBudgetUsd: budget.dailyUsd }),\n };\n }),\n null,\n 2,\n ),\n );\n return;\n }\n if (records.length === 0) {\n console.log('No apps installed. Try: alashi install <name|git-url|path>');\n return;\n }\n for (const record of records) {\n const origin = record.source.type === 'git' ? record.source.url : record.source.path;\n const budget = spend.get(record.name);\n const today = (budget?.spentUsd ?? 0).toFixed(4);\n const cap =\n budget?.dailyUsd === undefined\n ? ''\n : ` of $${budget.dailyUsd.toFixed(2)} cap${budget.overBudget ? ' (OVER BUDGET)' : ''}`;\n console.log(\n `${record.name}@${record.version} ${record.source.type} ${origin} $${today} today${cap}`,\n );\n }\n });\n\nprogram\n .command('budget')\n .argument('<name>', 'installed app name')\n .argument('[dailyUsd]', 'daily cap in USD, or \"none\" to remove it (omit to just show)')\n .description(\"Show or set an app's host-enforced daily AI spend cap\")\n .action(async (name: string, dailyUsd?: string) => {\n if (!(await store.readAppRecord(name))) throw new Error(`App \"${name}\" is not installed`);\n if (dailyUsd !== undefined) {\n const cleared = dailyUsd === 'none' || dailyUsd === 'off';\n const amount = cleared ? undefined : Number(dailyUsd);\n if (amount !== undefined && (!Number.isFinite(amount) || amount < 0)) {\n throw new Error(`Invalid daily spend cap: ${dailyUsd}`);\n }\n await setAppBudget(store, name, amount);\n }\n const status = await budgetStatus(store, new UsageRecorder(store), name);\n const spent = `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)`;\n if (status.dailyUsd === undefined) {\n console.log(`${name}: no daily cap — ${spent}`);\n return;\n }\n console.log(\n `${name}: $${status.dailyUsd.toFixed(2)} daily cap — ${spent}, $${(status.remainingUsd ?? 0).toFixed(4)} left`,\n );\n if (status.overBudget)\n console.log(' OVER BUDGET: new AI sessions are refused until 00:00 UTC');\n });\n\nconst registry = program.command('registry').description('Manage app registries');\n\nregistry\n .command('add')\n .argument('<name>', 'registry name')\n .argument('<url>', 'index.json URL or git repo URL')\n .action(async (name: string, url: string) => {\n const index = await addRegistry(store, name, url);\n console.log(`Added registry \"${name}\" (${index.apps.length} apps)`);\n });\n\nregistry\n .command('remove')\n .argument('<name>', 'registry name')\n .action(async (name: string) => {\n await removeRegistry(store, name);\n console.log(`Removed registry \"${name}\"`);\n });\n\nregistry.command('list').action(async () => {\n const config = await store.readGlobalConfig();\n if (config.registries.length === 0) {\n console.log('No registries configured.');\n return;\n }\n for (const r of config.registries) console.log(`${r.name} ${r.url}`);\n});\n\nprogram.parseAsync().catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : err);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/create.ts","../src/cli.ts"],"sourcesContent":["import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { APP_NAME_PATTERN, AppSdkError } from '@alashi/apps-sdk';\n\n// Works from both src/ (tests) and dist/ (published build) — the templates\n// dir sits one level up in the package root either way.\nconst TEMPLATE_DIR = fileURLToPath(new URL('../templates/basic-app', import.meta.url));\n\n// npm strips .gitignore from published packages, so it ships unprefixed.\nconst RENAMES: Record<string, string> = { gitignore: '.gitignore' };\n\n/**\n * Scaffolds a new app from the basic-app template into targetDir,\n * substituting the app name. Returns the created files, relative to\n * targetDir. Refuses to write into a non-empty directory.\n */\nexport async function scaffoldApp(name: string, targetDir: string): Promise<string[]> {\n if (!APP_NAME_PATTERN.test(name)) {\n throw new AppSdkError(\n `Invalid app name \"${name}\": use lowercase letters, digits, and hyphens (max 64 chars)`,\n );\n }\n const existing = await readdir(targetDir).catch(() => undefined);\n if (existing && existing.length > 0) {\n throw new AppSdkError(`Directory is not empty: ${targetDir}`);\n }\n\n const written: string[] = [];\n async function copyDir(rel: string): Promise<void> {\n const entries = await readdir(join(TEMPLATE_DIR, rel), { withFileTypes: true });\n await mkdir(join(targetDir, rel), { recursive: true });\n for (const entry of entries) {\n const relPath = join(rel, entry.name);\n if (entry.isDirectory()) {\n await copyDir(relPath);\n continue;\n }\n const content = await readFile(join(TEMPLATE_DIR, relPath), 'utf8');\n const outPath = join(rel, RENAMES[entry.name] ?? entry.name);\n await writeFile(join(targetDir, outPath), content.replaceAll('__APP_NAME__', name), 'utf8');\n written.push(outPath);\n }\n }\n await copyDir('');\n return written.sort();\n}\n","#!/usr/bin/env node\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { ClaudeCodeProvider } from '@alashi/provider-claude';\nimport { serve } from '@hono/node-server';\nimport { Command } from 'commander';\nimport { AuthService } from './auth.js';\nimport { budgetStatus, setAppBudget } from './budget.js';\nimport { scaffoldApp } from './create.js';\nimport { installApp, removeApp, updateApp } from './installer.js';\nimport { loadApp } from './loader.js';\nimport { exportApp, importApp } from './portability.js';\nimport { addRegistry, removeRegistry } from './registry.js';\nimport { resolveAppConfig } from './runtime.js';\nimport { createServer } from './server.js';\nimport { Store } from './store.js';\nimport { UsageRecorder } from './usage.js';\n\nconst program = new Command();\nconst store = new Store();\n\nprogram.name('alashi').description('Install, manage, and run Claude Code-powered apps');\n\nasync function serveDashboard(portArg?: string, hostArg?: string): Promise<void> {\n const config = await store.readGlobalConfig();\n const port = portArg ? Number(portArg) : (config.port ?? 4141);\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(`Invalid port: ${portArg}`);\n }\n // Loopback by default: only an explicit --host (or config) exposes the\n // dashboard beyond this machine, and then auth is in front of it.\n const hostname = hostArg ?? config.host ?? '127.0.0.1';\n const loopback = hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '::1';\n const mode = config.auth?.mode ?? 'auto';\n const server = createServer(store, new ClaudeCodeProvider());\n await server.scheduler.start();\n const httpServer = serve({ fetch: server.app.fetch, port, hostname }, (info) => {\n console.log(`alashi dashboard at http://localhost:${info.port}`);\n if (mode === 'off' && !loopback) {\n console.log(\n ` WARNING: listening on ${hostname} with auth.mode \"off\" — anyone who can reach this address owns the host`,\n );\n }\n // Local browsing on `auto` needs no credential (the console heuristic\n // covers it); print a login URL whenever something else will need one.\n if (mode === 'required' || (mode === 'auto' && !loopback)) {\n const secret = server.auth.issueBootToken();\n console.log(` sign in: http://localhost:${info.port}/login?token=${secret}`);\n console.log(' (one-time URL — mint reusable tokens with `alashi token create`)');\n }\n });\n const shutdown = (): void => {\n httpServer.close();\n void server.close().finally(() => process.exit(0));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\nprogram\n .command('serve', { isDefault: true })\n .option('-p, --port <port>', 'port to listen on (default: config port or 4141)')\n .option(\n '--host <host>',\n 'address to bind (default: 127.0.0.1; non-loopback binds require sign-in)',\n )\n .description('Serve the web dashboard (the default when no command is given)')\n .action(async (options: { port?: string; host?: string }) =>\n serveDashboard(options.port, options.host),\n );\n\nprogram\n .command('create')\n .argument('<name>', 'app name (lowercase letters, digits, hyphens)')\n .argument('[directory]', 'target directory (default: ./<name>)')\n .description('Scaffold a new app from the basic template')\n .action(async (name: string, directory?: string) => {\n const target = directory ?? name;\n const files = await scaffoldApp(name, resolve(target));\n console.log(`Created ${name}/`);\n for (const file of files) console.log(` ${file}`);\n console.log('\\nNext steps:');\n console.log(` cd ${target}`);\n console.log(' npm install');\n console.log(' alashi install .');\n console.log(' alashi # dashboard at http://localhost:4141');\n });\n\nprogram\n .command('install')\n .argument('<source>', 'app name (via registry), git URL, or local path')\n .description('Install an app')\n .action(async (source: string) => {\n const { record, warnings } = await installApp(store, source);\n console.log(`Installed ${record.name}@${record.version} (${record.source.type})`);\n console.log(` data: ${store.appDataDir(record.name)}`);\n console.log(` config: ${store.appConfigPath(record.name)}`);\n printManifestWarnings(warnings);\n await printSetupHints(record.name);\n });\n\n/**\n * Unknown manifest keys never fail a command — the host strips them and runs.\n * Surfacing them here is how an author notices a typo or a too-new field at\n * install time instead of only in the server log. Making them an *error* for\n * authors is `alashi validate` (card #22).\n */\nfunction printManifestWarnings(warnings: string[]): void {\n for (const warning of warnings) console.log(` warning: ${warning}`);\n}\n\n/** After install: what still needs configuring, and which integrations exist. */\nasync function printSetupHints(name: string): Promise<void> {\n let manifest: Awaited<ReturnType<typeof loadApp>>['manifest'];\n try {\n // Warnings were already printed from the install result; stay quiet here.\n ({ manifest } = await loadApp(store.appRepoDir(name), { warn: () => {} }));\n } catch {\n return; // install already succeeded; hints are best-effort\n }\n const { status } = await resolveAppConfig(store, manifest);\n if (status.missing.length > 0) {\n console.log(` requires: ${status.missing.join(', ')}`);\n }\n const optional = status.integrations.filter((i) => i.optional && !i.configured);\n if (optional.length > 0) {\n const list = optional.map((i) => `${i.name} (needs ${i.missing.join(', ')})`).join(', ');\n console.log(` optional integrations: ${list}`);\n }\n if (status.missing.length > 0 || optional.length > 0) {\n console.log(` configure via the dashboard settings page or the config file above`);\n }\n}\n\nprogram\n .command('update')\n .argument('<name>', 'installed app name')\n .description('Update an installed app')\n .action(async (name: string) => {\n const { record, warnings } = await updateApp(store, name);\n console.log(`Updated ${record.name} to ${record.version}`);\n printManifestWarnings(warnings);\n });\n\nprogram\n .command('remove')\n .argument('<name>', 'installed app name')\n .option('--purge', 'also delete the app data and config')\n .description('Remove an installed app (keeps data unless --purge)')\n .action(async (name: string, options: { purge?: boolean }) => {\n await removeApp(store, name, { purge: options.purge ?? false });\n console.log(options.purge ? `Removed ${name} (purged)` : `Removed ${name} (data kept)`);\n });\n\n/** Prints archive paths the way the user would type them. */\nfunction displayPath(path: string): string {\n const rel = relative(process.cwd(), path);\n return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? path : `./${rel}`;\n}\n\nprogram\n .command('export')\n .argument('<name>', 'installed app name')\n .option('-o, --out <file>', 'archive path (default: ./<name>-<version>.tar.gz)')\n .description('Pack an app container and its chat transcripts into a tar.gz')\n .action(async (name: string, options: { out?: string }) => {\n const result = await exportApp(store, new ClaudeCodeProvider(), name, {\n ...(options.out && { out: options.out }),\n });\n for (const warning of result.warnings) console.log(` warning: ${warning}`);\n console.log(\n `Exported ${result.record.name}@${result.record.version} -> ${displayPath(result.archivePath)} (${result.transcriptCount} transcripts)`,\n );\n if (result.record.source.type === 'local') {\n console.log(' note: repo/ was captured as real files, not as the local symlink');\n }\n });\n\n// `import` is a reserved word, so the handler cannot be named after it; the\n// commander command string itself is fine.\nasync function importArchive(file: string): Promise<void> {\n const result = await importApp(store, new ClaudeCodeProvider(), file);\n for (const warning of result.warnings) console.log(` warning: ${warning}`);\n console.log(\n `Imported ${result.record.name}@${result.record.version} (${result.transcriptCount} transcripts restored)`,\n );\n console.log(` data: ${store.appDataDir(result.record.name)}`);\n console.log(` config: ${store.appConfigPath(result.record.name)}`);\n}\n\nprogram\n .command('import')\n .argument('<file>', 'archive produced by alashi export')\n .description('Restore an app container and its chat transcripts from a tar.gz')\n .action(importArchive);\n\nprogram\n .command('list')\n .option('--json', 'machine-readable output')\n .description('List installed apps')\n .action(async (options: { json?: boolean }) => {\n const usage = new UsageRecorder(store);\n const records = await store.listInstalled();\n const spend = new Map(\n await Promise.all(\n records.map(\n async (record) => [record.name, await budgetStatus(store, usage, record.name)] as const,\n ),\n ),\n );\n if (options.json) {\n console.log(\n JSON.stringify(\n records.map((record) => {\n const budget = spend.get(record.name);\n return {\n ...record,\n todayCostUsd: budget?.spentUsd ?? 0,\n recorded: budget?.recorded ?? false,\n overBudget: budget?.overBudget ?? false,\n ...(budget?.dailyUsd !== undefined && { dailyBudgetUsd: budget.dailyUsd }),\n };\n }),\n null,\n 2,\n ),\n );\n return;\n }\n if (records.length === 0) {\n console.log('No apps installed. Try: alashi install <name|git-url|path>');\n return;\n }\n for (const record of records) {\n const origin = record.source.type === 'git' ? record.source.url : record.source.path;\n const budget = spend.get(record.name);\n const cap =\n budget?.dailyUsd === undefined\n ? ''\n : ` of $${budget.dailyUsd.toFixed(2)} cap${budget.overBudget ? ' (OVER BUDGET)' : ''}`;\n // An em dash where the host has no ledger for the app at all; a real\n // $0.0000 only once something has actually been metered.\n const today = budget?.recorded ? `$${budget.spentUsd.toFixed(4)} today` : '— today';\n console.log(\n `${record.name}@${record.version} ${record.source.type} ${origin} ${today}${cap}`,\n );\n }\n });\n\nprogram\n .command('budget')\n .argument('<name>', 'installed app name')\n .argument('[dailyUsd]', 'daily cap in USD, or \"none\" to remove it (omit to just show)')\n .description(\"Show or set an app's host-enforced daily AI spend cap\")\n .action(async (name: string, dailyUsd?: string) => {\n if (!(await store.readAppRecord(name))) throw new Error(`App \"${name}\" is not installed`);\n if (dailyUsd !== undefined) {\n const cleared = dailyUsd === 'none' || dailyUsd === 'off';\n const amount = cleared ? undefined : Number(dailyUsd);\n if (amount !== undefined && (!Number.isFinite(amount) || amount < 0)) {\n throw new Error(`Invalid daily spend cap: ${dailyUsd}`);\n }\n await setAppBudget(store, name, amount);\n }\n const status = await budgetStatus(store, new UsageRecorder(store), name);\n // \"Never metered\" is not \"spent nothing\": the host counts only sessions the\n // app opens through ctx.ai, so say so rather than printing a made-up zero.\n const spent = status.recorded\n ? `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)`\n : 'no spend recorded yet (the host meters only sessions opened through ctx.ai)';\n if (status.dailyUsd === undefined) {\n console.log(`${name}: no daily cap — ${spent}`);\n return;\n }\n console.log(\n `${name}: $${status.dailyUsd.toFixed(2)} daily cap — ${spent}, $${(status.remainingUsd ?? 0).toFixed(4)} left`,\n );\n if (status.overBudget)\n console.log(' OVER BUDGET: new AI sessions are refused until 00:00 UTC');\n });\n\nconst token = program\n .command('token')\n .description('Manage owner bearer tokens for the dashboard/API (see config.auth)');\n\ntoken\n .command('create')\n .argument('[label]', 'a name for the token (e.g. \"laptop\", \"ci\")')\n .description('Mint an owner token; the secret is printed once and never stored')\n .action(async (label?: string) => {\n const { id, secret } = await new AuthService(store).mintToken(label);\n console.log(secret);\n console.log(\n ` id: ${id}${label ? ` (${label})` : ''} — sign in at /login or send it as \"Authorization: Bearer ...\"`,\n );\n });\n\ntoken\n .command('list')\n .description('List tokens (ids and labels only; secrets are never stored)')\n .action(async () => {\n const tokens = await new AuthService(store).listTokens();\n if (tokens.length === 0) {\n console.log('No tokens. Mint one with `alashi token create [label]`.');\n return;\n }\n for (const entry of tokens) {\n const label = entry.label ? ` ${entry.label}` : '';\n const used = entry.lastUsedAt ? `, last used ${entry.lastUsedAt}` : ', never used';\n console.log(`${entry.id}${label} — created ${entry.createdAt}${used}`);\n }\n });\n\ntoken\n .command('revoke')\n .argument('<id>', 'token id (see `alashi token list`)')\n .description('Revoke a token')\n .action(async (id: string) => {\n if (!(await new AuthService(store).revokeToken(id))) {\n throw new Error(`No token with id \"${id}\"`);\n }\n console.log(`Revoked ${id}`);\n });\n\nconst registry = program.command('registry').description('Manage app registries');\n\nregistry\n .command('add')\n .argument('<name>', 'registry name')\n .argument('<url>', 'index.json URL or git repo URL')\n .action(async (name: string, url: string) => {\n const index = await addRegistry(store, name, url);\n console.log(`Added registry \"${name}\" (${index.apps.length} apps)`);\n });\n\nregistry\n .command('remove')\n .argument('<name>', 'registry name')\n .action(async (name: string) => {\n await removeRegistry(store, name);\n console.log(`Removed registry \"${name}\"`);\n });\n\nregistry.command('list').action(async () => {\n const config = await store.readGlobalConfig();\n if (config.registries.length === 0) {\n console.log('No registries configured.');\n return;\n }\n for (const r of config.registries) console.log(`${r.name} ${r.url}`);\n});\n\nprogram.parseAsync().catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : err);\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;;;;;AAOA,MAAM,eAAe,cAAc,IAAI,IAAI,0BAA0B,YAAY,GAAG,CAAC;AAGrF,MAAM,UAAkC,EAAE,WAAW,aAAa;;;;;;AAOlE,eAAsB,YAAY,MAAc,WAAsC;CACpF,IAAI,CAAC,iBAAiB,KAAK,IAAI,GAC7B,MAAM,IAAI,YACR,qBAAqB,KAAK,6DAC5B;CAEF,MAAM,WAAW,MAAM,QAAQ,SAAS,CAAC,CAAC,YAAY,KAAA,CAAS;CAC/D,IAAI,YAAY,SAAS,SAAS,GAChC,MAAM,IAAI,YAAY,2BAA2B,WAAW;CAG9D,MAAM,UAAoB,CAAC;CAC3B,eAAe,QAAQ,KAA4B;EACjD,MAAM,UAAU,MAAM,QAAQ,KAAK,cAAc,GAAG,GAAG,EAAE,eAAe,KAAK,CAAC;EAC9E,MAAM,MAAM,KAAK,WAAW,GAAG,GAAG,EAAE,WAAW,KAAK,CAAC;EACrD,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,UAAU,KAAK,KAAK,MAAM,IAAI;GACpC,IAAI,MAAM,YAAY,GAAG;IACvB,MAAM,QAAQ,OAAO;IACrB;GACF;GACA,MAAM,UAAU,MAAM,SAAS,KAAK,cAAc,OAAO,GAAG,MAAM;GAClE,MAAM,UAAU,KAAK,KAAK,QAAQ,MAAM,SAAS,MAAM,IAAI;GAC3D,MAAM,UAAU,KAAK,WAAW,OAAO,GAAG,QAAQ,WAAW,gBAAgB,IAAI,GAAG,MAAM;GAC1F,QAAQ,KAAK,OAAO;EACtB;CACF;CACA,MAAM,QAAQ,EAAE;CAChB,OAAO,QAAQ,KAAK;AACtB;;;AC7BA,MAAM,UAAU,IAAI,QAAQ;AAC5B,MAAM,QAAQ,IAAI,MAAM;AAExB,QAAQ,KAAK,QAAQ,CAAC,CAAC,YAAY,mDAAmD;AAEtF,eAAe,eAAe,SAAkB,SAAiC;CAC/E,MAAM,SAAS,MAAM,MAAM,iBAAiB;CAC5C,MAAM,OAAO,UAAU,OAAO,OAAO,IAAK,OAAO,QAAQ;CACzD,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAChD,MAAM,IAAI,MAAM,iBAAiB,SAAS;CAI5C,MAAM,WAAW,WAAW,OAAO,QAAQ;CAC3C,MAAM,WAAW,aAAa,eAAe,aAAa,eAAe,aAAa;CACtF,MAAM,OAAO,OAAO,MAAM,QAAQ;CAClC,MAAM,SAAS,aAAa,OAAO,IAAI,mBAAmB,CAAC;CAC3D,MAAM,OAAO,UAAU,MAAM;CAC7B,MAAM,aAAa,MAAM;EAAE,OAAO,OAAO,IAAI;EAAO;EAAM;CAAS,IAAI,SAAS;EAC9E,QAAQ,IAAI,wCAAwC,KAAK,MAAM;EAC/D,IAAI,SAAS,SAAS,CAAC,UACrB,QAAQ,IACN,2BAA2B,SAAS,wEACtC;EAIF,IAAI,SAAS,cAAe,SAAS,UAAU,CAAC,UAAW;GACzD,MAAM,SAAS,OAAO,KAAK,eAAe;GAC1C,QAAQ,IAAI,+BAA+B,KAAK,KAAK,eAAe,QAAQ;GAC5E,QAAQ,IAAI,oEAAoE;EAClF;CACF,CAAC;CACD,MAAM,iBAAuB;EAC3B,WAAW,MAAM;EACjB,OAAY,MAAM,CAAC,CAAC,cAAc,QAAQ,KAAK,CAAC,CAAC;CACnD;CACA,QAAQ,GAAG,UAAU,QAAQ;CAC7B,QAAQ,GAAG,WAAW,QAAQ;AAChC;AAEA,QACG,QAAQ,SAAS,EAAE,WAAW,KAAK,CAAC,CAAC,CACrC,OAAO,qBAAqB,kDAAkD,CAAC,CAC/E,OACC,iBACA,0EACF,CAAC,CACA,YAAY,gEAAgE,CAAC,CAC7E,OAAO,OAAO,YACb,eAAe,QAAQ,MAAM,QAAQ,IAAI,CAC3C;AAEF,QACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,+CAA+C,CAAC,CACnE,SAAS,eAAe,sCAAsC,CAAC,CAC/D,YAAY,4CAA4C,CAAC,CACzD,OAAO,OAAO,MAAc,cAAuB;CAClD,MAAM,SAAS,aAAa;CAC5B,MAAM,QAAQ,MAAM,YAAY,MAAM,QAAQ,MAAM,CAAC;CACrD,QAAQ,IAAI,WAAW,KAAK,EAAE;CAC9B,KAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI,KAAK,MAAM;CACjD,QAAQ,IAAI,eAAe;CAC3B,QAAQ,IAAI,QAAQ,QAAQ;CAC5B,QAAQ,IAAI,eAAe;CAC3B,QAAQ,IAAI,oBAAoB;CAChC,QAAQ,IAAI,kDAAkD;AAChE,CAAC;AAEH,QACG,QAAQ,SAAS,CAAC,CAClB,SAAS,YAAY,iDAAiD,CAAC,CACvE,YAAY,gBAAgB,CAAC,CAC7B,OAAO,OAAO,WAAmB;CAChC,MAAM,EAAE,QAAQ,aAAa,MAAM,WAAW,OAAO,MAAM;CAC3D,QAAQ,IAAI,aAAa,OAAO,KAAK,GAAG,OAAO,QAAQ,IAAI,OAAO,OAAO,KAAK,EAAE;CAChF,QAAQ,IAAI,aAAa,MAAM,WAAW,OAAO,IAAI,GAAG;CACxD,QAAQ,IAAI,aAAa,MAAM,cAAc,OAAO,IAAI,GAAG;CAC3D,sBAAsB,QAAQ;CAC9B,MAAM,gBAAgB,OAAO,IAAI;AACnC,CAAC;;;;;;;AAQH,SAAS,sBAAsB,UAA0B;CACvD,KAAK,MAAM,WAAW,UAAU,QAAQ,IAAI,cAAc,SAAS;AACrE;;AAGA,eAAe,gBAAgB,MAA6B;CAC1D,IAAI;CACJ,IAAI;EAEF,CAAC,CAAE,YAAa,MAAM,QAAQ,MAAM,WAAW,IAAI,GAAG,EAAE,YAAY,CAAC,EAAE,CAAC;CAC1E,QAAQ;EACN;CACF;CACA,MAAM,EAAE,WAAW,MAAM,iBAAiB,OAAO,QAAQ;CACzD,IAAI,OAAO,QAAQ,SAAS,GAC1B,QAAQ,IAAI,eAAe,OAAO,QAAQ,KAAK,IAAI,GAAG;CAExD,MAAM,WAAW,OAAO,aAAa,QAAQ,MAAM,EAAE,YAAY,CAAC,EAAE,UAAU;CAC9E,IAAI,SAAS,SAAS,GAAG;EACvB,MAAM,OAAO,SAAS,KAAK,MAAM,GAAG,EAAE,KAAK,UAAU,EAAE,QAAQ,KAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI;EACvF,QAAQ,IAAI,4BAA4B,MAAM;CAChD;CACA,IAAI,OAAO,QAAQ,SAAS,KAAK,SAAS,SAAS,GACjD,QAAQ,IAAI,sEAAsE;AAEtF;AAEA,QACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,oBAAoB,CAAC,CACxC,YAAY,yBAAyB,CAAC,CACtC,OAAO,OAAO,SAAiB;CAC9B,MAAM,EAAE,QAAQ,aAAa,MAAM,UAAU,OAAO,IAAI;CACxD,QAAQ,IAAI,WAAW,OAAO,KAAK,MAAM,OAAO,SAAS;CACzD,sBAAsB,QAAQ;AAChC,CAAC;AAEH,QACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,oBAAoB,CAAC,CACxC,OAAO,WAAW,qCAAqC,CAAC,CACxD,YAAY,qDAAqD,CAAC,CAClE,OAAO,OAAO,MAAc,YAAiC;CAC5D,MAAM,UAAU,OAAO,MAAM,EAAE,OAAO,QAAQ,SAAS,MAAM,CAAC;CAC9D,QAAQ,IAAI,QAAQ,QAAQ,WAAW,KAAK,aAAa,WAAW,KAAK,aAAa;AACxF,CAAC;;AAGH,SAAS,YAAY,MAAsB;CACzC,MAAM,MAAM,SAAS,QAAQ,IAAI,GAAG,IAAI;CACxC,OAAO,QAAQ,MAAM,IAAI,WAAW,IAAI,KAAK,WAAW,GAAG,IAAI,OAAO,KAAK;AAC7E;AAEA,QACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,oBAAoB,CAAC,CACxC,OAAO,oBAAoB,mDAAmD,CAAC,CAC/E,YAAY,8DAA8D,CAAC,CAC3E,OAAO,OAAO,MAAc,YAA8B;CACzD,MAAM,SAAS,MAAM,UAAU,OAAO,IAAI,mBAAmB,GAAG,MAAM,EACpE,GAAI,QAAQ,OAAO,EAAE,KAAK,QAAQ,IAAI,EACxC,CAAC;CACD,KAAK,MAAM,WAAW,OAAO,UAAU,QAAQ,IAAI,cAAc,SAAS;CAC1E,QAAQ,IACN,YAAY,OAAO,OAAO,KAAK,GAAG,OAAO,OAAO,QAAQ,MAAM,YAAY,OAAO,WAAW,EAAE,IAAI,OAAO,gBAAgB,cAC3H;CACA,IAAI,OAAO,OAAO,OAAO,SAAS,SAChC,QAAQ,IAAI,oEAAoE;AAEpF,CAAC;AAIH,eAAe,cAAc,MAA6B;CACxD,MAAM,SAAS,MAAM,UAAU,OAAO,IAAI,mBAAmB,GAAG,IAAI;CACpE,KAAK,MAAM,WAAW,OAAO,UAAU,QAAQ,IAAI,cAAc,SAAS;CAC1E,QAAQ,IACN,YAAY,OAAO,OAAO,KAAK,GAAG,OAAO,OAAO,QAAQ,IAAI,OAAO,gBAAgB,uBACrF;CACA,QAAQ,IAAI,aAAa,MAAM,WAAW,OAAO,OAAO,IAAI,GAAG;CAC/D,QAAQ,IAAI,aAAa,MAAM,cAAc,OAAO,OAAO,IAAI,GAAG;AACpE;AAEA,QACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,mCAAmC,CAAC,CACvD,YAAY,iEAAiE,CAAC,CAC9E,OAAO,aAAa;AAEvB,QACG,QAAQ,MAAM,CAAC,CACf,OAAO,UAAU,yBAAyB,CAAC,CAC3C,YAAY,qBAAqB,CAAC,CAClC,OAAO,OAAO,YAAgC;CAC7C,MAAM,QAAQ,IAAI,cAAc,KAAK;CACrC,MAAM,UAAU,MAAM,MAAM,cAAc;CAC1C,MAAM,QAAQ,IAAI,IAChB,MAAM,QAAQ,IACZ,QAAQ,IACN,OAAO,WAAW,CAAC,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO,OAAO,IAAI,CAAC,CAC/E,CACF,CACF;CACA,IAAI,QAAQ,MAAM;EAChB,QAAQ,IACN,KAAK,UACH,QAAQ,KAAK,WAAW;GACtB,MAAM,SAAS,MAAM,IAAI,OAAO,IAAI;GACpC,OAAO;IACL,GAAG;IACH,cAAc,QAAQ,YAAY;IAClC,UAAU,QAAQ,YAAY;IAC9B,YAAY,QAAQ,cAAc;IAClC,GAAI,QAAQ,aAAa,KAAA,KAAa,EAAE,gBAAgB,OAAO,SAAS;GAC1E;EACF,CAAC,GACD,MACA,CACF,CACF;EACA;CACF;CACA,IAAI,QAAQ,WAAW,GAAG;EACxB,QAAQ,IAAI,4DAA4D;EACxE;CACF;CACA,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,SAAS,OAAO,OAAO,SAAS,QAAQ,OAAO,OAAO,MAAM,OAAO,OAAO;EAChF,MAAM,SAAS,MAAM,IAAI,OAAO,IAAI;EACpC,MAAM,MACJ,QAAQ,aAAa,KAAA,IACjB,KACA,QAAQ,OAAO,SAAS,QAAQ,CAAC,EAAE,MAAM,OAAO,aAAa,mBAAmB;EAGtF,MAAM,QAAQ,QAAQ,WAAW,IAAI,OAAO,SAAS,QAAQ,CAAC,EAAE,UAAU;EAC1E,QAAQ,IACN,GAAG,OAAO,KAAK,GAAG,OAAO,QAAQ,IAAI,OAAO,OAAO,KAAK,IAAI,OAAO,IAAI,QAAQ,KACjF;CACF;AACF,CAAC;AAEH,QACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,oBAAoB,CAAC,CACxC,SAAS,cAAc,gEAA8D,CAAC,CACtF,YAAY,uDAAuD,CAAC,CACpE,OAAO,OAAO,MAAc,aAAsB;CACjD,IAAI,CAAE,MAAM,MAAM,cAAc,IAAI,GAAI,MAAM,IAAI,MAAM,QAAQ,KAAK,mBAAmB;CACxF,IAAI,aAAa,KAAA,GAAW;EAE1B,MAAM,SADU,aAAa,UAAU,aAAa,QAC3B,KAAA,IAAY,OAAO,QAAQ;EACpD,IAAI,WAAW,KAAA,MAAc,CAAC,OAAO,SAAS,MAAM,KAAK,SAAS,IAChE,MAAM,IAAI,MAAM,4BAA4B,UAAU;EAExD,MAAM,aAAa,OAAO,MAAM,MAAM;CACxC;CACA,MAAM,SAAS,MAAM,aAAa,OAAO,IAAI,cAAc,KAAK,GAAG,IAAI;CAGvE,MAAM,QAAQ,OAAO,WACjB,IAAI,OAAO,SAAS,QAAQ,CAAC,EAAE,YAAY,OAAO,KAAK,UACvD;CACJ,IAAI,OAAO,aAAa,KAAA,GAAW;EACjC,QAAQ,IAAI,GAAG,KAAK,mBAAmB,OAAO;EAC9C;CACF;CACA,QAAQ,IACN,GAAG,KAAK,KAAK,OAAO,SAAS,QAAQ,CAAC,EAAE,eAAe,MAAM,MAAM,OAAO,gBAAgB,EAAA,CAAG,QAAQ,CAAC,EAAE,MAC1G;CACA,IAAI,OAAO,YACT,QAAQ,IAAI,4DAA4D;AAC5E,CAAC;AAEH,MAAM,QAAQ,QACX,QAAQ,OAAO,CAAC,CAChB,YAAY,oEAAoE;AAEnF,MACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,WAAW,gDAA4C,CAAC,CACjE,YAAY,kEAAkE,CAAC,CAC/E,OAAO,OAAO,UAAmB;CAChC,MAAM,EAAE,IAAI,WAAW,MAAM,IAAI,YAAY,KAAK,CAAC,CAAC,UAAU,KAAK;CACnE,QAAQ,IAAI,MAAM;CAClB,QAAQ,IACN,SAAS,KAAK,QAAQ,KAAK,MAAM,KAAK,GAAG,+DAC3C;AACF,CAAC;AAEH,MACG,QAAQ,MAAM,CAAC,CACf,YAAY,6DAA6D,CAAC,CAC1E,OAAO,YAAY;CAClB,MAAM,SAAS,MAAM,IAAI,YAAY,KAAK,CAAC,CAAC,WAAW;CACvD,IAAI,OAAO,WAAW,GAAG;EACvB,QAAQ,IAAI,yDAAyD;EACrE;CACF;CACA,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,UAAU;EAChD,MAAM,OAAO,MAAM,aAAa,eAAe,MAAM,eAAe;EACpE,QAAQ,IAAI,GAAG,MAAM,KAAK,MAAM,aAAa,MAAM,YAAY,MAAM;CACvE;AACF,CAAC;AAEH,MACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,QAAQ,oCAAoC,CAAC,CACtD,YAAY,gBAAgB,CAAC,CAC7B,OAAO,OAAO,OAAe;CAC5B,IAAI,CAAE,MAAM,IAAI,YAAY,KAAK,CAAC,CAAC,YAAY,EAAE,GAC/C,MAAM,IAAI,MAAM,qBAAqB,GAAG,EAAE;CAE5C,QAAQ,IAAI,WAAW,IAAI;AAC7B,CAAC;AAEH,MAAM,WAAW,QAAQ,QAAQ,UAAU,CAAC,CAAC,YAAY,uBAAuB;AAEhF,SACG,QAAQ,KAAK,CAAC,CACd,SAAS,UAAU,eAAe,CAAC,CACnC,SAAS,SAAS,gCAAgC,CAAC,CACnD,OAAO,OAAO,MAAc,QAAgB;CAC3C,MAAM,QAAQ,MAAM,YAAY,OAAO,MAAM,GAAG;CAChD,QAAQ,IAAI,mBAAmB,KAAK,KAAK,MAAM,KAAK,OAAO,OAAO;AACpE,CAAC;AAEH,SACG,QAAQ,QAAQ,CAAC,CACjB,SAAS,UAAU,eAAe,CAAC,CACnC,OAAO,OAAO,SAAiB;CAC9B,MAAM,eAAe,OAAO,IAAI;CAChC,QAAQ,IAAI,qBAAqB,KAAK,EAAE;AAC1C,CAAC;AAEH,SAAS,QAAQ,MAAM,CAAC,CAAC,OAAO,YAAY;CAC1C,MAAM,SAAS,MAAM,MAAM,iBAAiB;CAC5C,IAAI,OAAO,WAAW,WAAW,GAAG;EAClC,QAAQ,IAAI,2BAA2B;EACvC;CACF;CACA,KAAK,MAAM,KAAK,OAAO,YAAY,QAAQ,IAAI,GAAG,EAAE,KAAK,IAAI,EAAE,KAAK;AACtE,CAAC;AAED,QAAQ,WAAW,CAAC,CAAC,OAAO,QAAiB;CAC3C,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,GAAG;CACtD,QAAQ,WAAW;AACrB,CAAC"}