@alashi/cli 0.11.0 → 0.13.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,12 +1,93 @@
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-DoAGOynU.mjs";
3
+ import { createRequire } from "node:module";
3
4
  import { APP_NAME_PATTERN, AppSdkError } from "@alashi/apps-sdk";
4
5
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
6
  import { isAbsolute, join, relative, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
8
+ import { homedir } from "node:os";
7
9
  import { ClaudeCodeProvider } from "@alashi/provider-claude";
8
10
  import { serve } from "@hono/node-server";
9
11
  import { Command } from "commander";
12
+ //#region src/banner.ts
13
+ const THEME = {
14
+ logo: {
15
+ rgb: [
16
+ 143,
17
+ 216,
18
+ 216
19
+ ],
20
+ ansi: "36"
21
+ },
22
+ accent: {
23
+ rgb: [
24
+ 79,
25
+ 224,
26
+ 210
27
+ ],
28
+ ansi: "96"
29
+ },
30
+ dim: {
31
+ rgb: [
32
+ 126,
33
+ 147,
34
+ 171
35
+ ],
36
+ ansi: "90"
37
+ }
38
+ };
39
+ function makeTint(color) {
40
+ if (!color) return (_tone, text) => text;
41
+ const truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
42
+ return (tone, text) => {
43
+ const { rgb, ansi } = THEME[tone];
44
+ return `${truecolor ? `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m` : `\x1b[${ansi}m`}${text}\x1b[0m`;
45
+ };
46
+ }
47
+ const LOGO = [
48
+ " _ _ _",
49
+ " __ _| |__ _ __| |_ (_)",
50
+ " / _` | / _` (_-< ' \\| |",
51
+ " \\__,_|_\\__,_/__/_||_|_|"
52
+ ];
53
+ function tilde(path) {
54
+ const home = homedir();
55
+ return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
56
+ }
57
+ /** Pure layout; printBanner adds the timing. Exported for tests. */
58
+ function renderBanner(info, color) {
59
+ const tint = makeTint(color);
60
+ const jobs = `${info.jobCount} ${info.jobCount === 1 ? "job" : "jobs"} on cron`;
61
+ const stat = (label, value) => ` ${tint("dim", label.padEnd(12))}${value}`;
62
+ return [
63
+ ...LOGO.slice(0, -1).map((row) => tint("accent", row)),
64
+ `${tint("accent", LOGO[LOGO.length - 1] ?? "")} ${tint("dim", `アラシ v${info.version}`)}`,
65
+ tint("dim", " the harness is the kernel. this is the OS."),
66
+ "",
67
+ stat("kernel", tint("logo", info.kernel)),
68
+ stat("apps", `${info.appCount} installed · ${jobs}`),
69
+ stat("state", tilde(info.stateDir)),
70
+ stat("dashboard", tint("accent", info.url)),
71
+ ...(info.notes ?? []).map((note) => ` ${note}`),
72
+ ""
73
+ ];
74
+ }
75
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
76
+ /**
77
+ * Boot banner for `alashi serve`. On an interactive terminal the lines reveal
78
+ * one by one (logo fast, stats slower); piped or NO_COLOR output gets the same
79
+ * text instantly and uncolored, so logs and greps stay clean.
80
+ */
81
+ async function printBanner(info, out = process.stdout) {
82
+ const color = out.isTTY === true && process.env.NO_COLOR === void 0;
83
+ const animate = color && process.env.CI === void 0;
84
+ const lines = renderBanner(info, color);
85
+ for (const [index, line] of lines.entries()) {
86
+ out.write(`${line}\n`);
87
+ if (animate) await sleep(index < LOGO.length ? 30 : 65);
88
+ }
89
+ }
90
+ //#endregion
10
91
  //#region src/create.ts
11
92
  const TEMPLATE_DIR = fileURLToPath(new URL("../templates/basic-app", import.meta.url));
12
93
  const RENAMES = { gitignore: ".gitignore" };
@@ -48,15 +129,34 @@ async function serveDashboard(portArg, hostArg) {
48
129
  const port = portArg ? Number(portArg) : config.port ?? 4141;
49
130
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${portArg}`);
50
131
  const hostname = hostArg ?? config.host ?? "127.0.0.1";
51
- const server = createServer(store, new ClaudeCodeProvider());
132
+ const loopback = hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
133
+ const mode = config.auth?.mode ?? "auto";
134
+ const provider = new ClaudeCodeProvider();
135
+ const server = createServer(store, provider);
52
136
  await server.scheduler.start();
137
+ const appCount = (await store.listInstalled()).length;
138
+ const { version } = createRequire(import.meta.url)("../package.json");
53
139
  const httpServer = serve({
54
140
  fetch: server.app.fetch,
55
141
  port,
56
142
  hostname
57
143
  }, (info) => {
58
- 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`);
144
+ const notes = [];
145
+ if (mode === "off" && !loopback) notes.push(`WARNING: listening on ${hostname} with auth.mode "off" — anyone who can reach this address owns the host`);
146
+ if (mode === "required" || mode === "auto" && !loopback) {
147
+ const secret = server.auth.issueBootToken();
148
+ notes.push(`sign in: http://localhost:${info.port}/login?token=${secret}`);
149
+ notes.push("(one-time URL — mint reusable tokens with `alashi token create`)");
150
+ }
151
+ printBanner({
152
+ version,
153
+ kernel: provider.name,
154
+ appCount,
155
+ jobCount: server.scheduler.jobCount,
156
+ stateDir: store.root,
157
+ url: `http://localhost:${info.port}`,
158
+ notes
159
+ });
60
160
  });
61
161
  const shutdown = () => {
62
162
  httpServer.close();
@@ -65,7 +165,7 @@ async function serveDashboard(portArg, hostArg) {
65
165
  process.on("SIGINT", shutdown);
66
166
  process.on("SIGTERM", shutdown);
67
167
  }
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));
168
+ 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
169
  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
170
  const target = directory ?? name;
71
171
  const files = await scaffoldApp(name, resolve(target));
@@ -78,17 +178,27 @@ program.command("create").argument("<name>", "app name (lowercase letters, digit
78
178
  console.log(" alashi # dashboard at http://localhost:4141");
79
179
  });
80
180
  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);
181
+ const { record, warnings } = await installApp(store, source);
82
182
  console.log(`Installed ${record.name}@${record.version} (${record.source.type})`);
83
183
  console.log(` data: ${store.appDataDir(record.name)}`);
84
184
  console.log(` config: ${store.appConfigPath(record.name)}`);
185
+ printManifestWarnings(warnings);
85
186
  await printSetupHints(record.name);
86
187
  });
188
+ /**
189
+ * Unknown manifest keys never fail a command — the host strips them and runs.
190
+ * Surfacing them here is how an author notices a typo or a too-new field at
191
+ * install time instead of only in the server log. Making them an *error* for
192
+ * authors is `alashi validate` (card #22).
193
+ */
194
+ function printManifestWarnings(warnings) {
195
+ for (const warning of warnings) console.log(` warning: ${warning}`);
196
+ }
87
197
  /** After install: what still needs configuring, and which integrations exist. */
88
198
  async function printSetupHints(name) {
89
199
  let manifest;
90
200
  try {
91
- ({manifest} = await loadApp(store.appRepoDir(name)));
201
+ ({manifest} = await loadApp(store.appRepoDir(name), { warn: () => {} }));
92
202
  } catch {
93
203
  return;
94
204
  }
@@ -102,8 +212,9 @@ async function printSetupHints(name) {
102
212
  if (status.missing.length > 0 || optional.length > 0) console.log(` configure via the dashboard settings page or the config file above`);
103
213
  }
104
214
  program.command("update").argument("<name>", "installed app name").description("Update an installed app").action(async (name) => {
105
- const record = await updateApp(store, name);
215
+ const { record, warnings } = await updateApp(store, name);
106
216
  console.log(`Updated ${record.name} to ${record.version}`);
217
+ printManifestWarnings(warnings);
107
218
  });
108
219
  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
220
  await removeApp(store, name, { purge: options.purge ?? false });
@@ -138,6 +249,7 @@ program.command("list").option("--json", "machine-readable output").description(
138
249
  return {
139
250
  ...record,
140
251
  todayCostUsd: budget?.spentUsd ?? 0,
252
+ recorded: budget?.recorded ?? false,
141
253
  overBudget: budget?.overBudget ?? false,
142
254
  ...budget?.dailyUsd !== void 0 && { dailyBudgetUsd: budget.dailyUsd }
143
255
  };
@@ -151,9 +263,9 @@ program.command("list").option("--json", "machine-readable output").description(
151
263
  for (const record of records) {
152
264
  const origin = record.source.type === "git" ? record.source.url : record.source.path;
153
265
  const budget = spend.get(record.name);
154
- const today = (budget?.spentUsd ?? 0).toFixed(4);
155
266
  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}`);
267
+ const today = budget?.recorded ? `$${budget.spentUsd.toFixed(4)} today` : "— today";
268
+ console.log(`${record.name}@${record.version} ${record.source.type} ${origin} ${today}${cap}`);
157
269
  }
158
270
  });
159
271
  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 +276,7 @@ program.command("budget").argument("<name>", "installed app name").argument("[da
164
276
  await setAppBudget(store, name, amount);
165
277
  }
166
278
  const status = await budgetStatus(store, new UsageRecorder(store), name);
167
- const spent = `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)`;
279
+ 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
280
  if (status.dailyUsd === void 0) {
169
281
  console.log(`${name}: no daily cap — ${spent}`);
170
282
  return;
@@ -172,6 +284,28 @@ program.command("budget").argument("<name>", "installed app name").argument("[da
172
284
  console.log(`${name}: $${status.dailyUsd.toFixed(2)} daily cap — ${spent}, $${(status.remainingUsd ?? 0).toFixed(4)} left`);
173
285
  if (status.overBudget) console.log(" OVER BUDGET: new AI sessions are refused until 00:00 UTC");
174
286
  });
287
+ const token = program.command("token").description("Manage owner bearer tokens for the dashboard/API (see config.auth)");
288
+ 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) => {
289
+ const { id, secret } = await new AuthService(store).mintToken(label);
290
+ console.log(secret);
291
+ console.log(` id: ${id}${label ? ` (${label})` : ""} — sign in at /login or send it as "Authorization: Bearer ..."`);
292
+ });
293
+ token.command("list").description("List tokens (ids and labels only; secrets are never stored)").action(async () => {
294
+ const tokens = await new AuthService(store).listTokens();
295
+ if (tokens.length === 0) {
296
+ console.log("No tokens. Mint one with `alashi token create [label]`.");
297
+ return;
298
+ }
299
+ for (const entry of tokens) {
300
+ const label = entry.label ? ` ${entry.label}` : "";
301
+ const used = entry.lastUsedAt ? `, last used ${entry.lastUsedAt}` : ", never used";
302
+ console.log(`${entry.id}${label} — created ${entry.createdAt}${used}`);
303
+ }
304
+ });
305
+ token.command("revoke").argument("<id>", "token id (see `alashi token list`)").description("Revoke a token").action(async (id) => {
306
+ if (!await new AuthService(store).revokeToken(id)) throw new Error(`No token with id "${id}"`);
307
+ console.log(`Revoked ${id}`);
308
+ });
175
309
  const registry = program.command("registry").description("Manage app registries");
176
310
  registry.command("add").argument("<name>", "registry name").argument("<url>", "index.json URL or git repo URL").action(async (name, url) => {
177
311
  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/banner.ts","../src/create.ts","../src/cli.ts"],"sourcesContent":["import { homedir } from 'node:os';\n\n/** Everything the serve banner reports about the freshly booted host. */\nexport interface BannerInfo {\n version: string;\n /** Provider name — the \"kernel\" in OS terms (e.g. \"claude-code\"). */\n kernel: string;\n appCount: number;\n jobCount: number;\n stateDir: string;\n url: string;\n /** Auth warnings / sign-in URLs, printed after the stats block. */\n notes?: string[];\n}\n\n// Palette from alashi-dev.github.io: cyan #8fd8d8, cyan-hi #4fe0d2, dim #7e93ab.\nconst THEME = {\n logo: { rgb: [143, 216, 216], ansi: '36' },\n accent: { rgb: [79, 224, 210], ansi: '96' },\n dim: { rgb: [126, 147, 171], ansi: '90' },\n} as const;\n\ntype Tone = keyof typeof THEME;\n\nfunction makeTint(color: boolean): (tone: Tone, text: string) => string {\n if (!color) return (_tone, text) => text;\n const truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? '');\n return (tone, text) => {\n const { rgb, ansi } = THEME[tone];\n const open = truecolor ? `\\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m` : `\\x1b[${ansi}m`;\n return `${open}${text}\\x1b[0m`;\n };\n}\n\nconst LOGO = [\n ' _ _ _',\n ' __ _| |__ _ __| |_ (_)',\n ' / _` | / _` (_-< \\' \\\\| |',\n ' \\\\__,_|_\\\\__,_/__/_||_|_|',\n];\n\nfunction tilde(path: string): string {\n const home = homedir();\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\n/** Pure layout; printBanner adds the timing. Exported for tests. */\nexport function renderBanner(info: BannerInfo, color: boolean): string[] {\n const tint = makeTint(color);\n const jobs = `${info.jobCount} ${info.jobCount === 1 ? 'job' : 'jobs'} on cron`;\n const stat = (label: string, value: string): string =>\n ` ${tint('dim', label.padEnd(12))}${value}`;\n return [\n ...LOGO.slice(0, -1).map((row) => tint('accent', row)),\n `${tint('accent', LOGO[LOGO.length - 1] ?? '')} ${tint('dim', `アラシ v${info.version}`)}`,\n tint('dim', ' the harness is the kernel. this is the OS.'),\n '',\n stat('kernel', tint('logo', info.kernel)),\n stat('apps', `${info.appCount} installed · ${jobs}`),\n stat('state', tilde(info.stateDir)),\n stat('dashboard', tint('accent', info.url)),\n ...(info.notes ?? []).map((note) => ` ${note}`),\n '',\n ];\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((done) => setTimeout(done, ms));\n\n/**\n * Boot banner for `alashi serve`. On an interactive terminal the lines reveal\n * one by one (logo fast, stats slower); piped or NO_COLOR output gets the same\n * text instantly and uncolored, so logs and greps stay clean.\n */\nexport async function printBanner(\n info: BannerInfo,\n out: NodeJS.WriteStream = process.stdout,\n): Promise<void> {\n const color = out.isTTY === true && process.env.NO_COLOR === undefined;\n const animate = color && process.env.CI === undefined;\n const lines = renderBanner(info, color);\n for (const [index, line] of lines.entries()) {\n out.write(`${line}\\n`);\n if (animate) await sleep(index < LOGO.length ? 30 : 65);\n }\n}\n","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 { createRequire } from 'node:module';\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 { printBanner } from './banner.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 provider = new ClaudeCodeProvider();\n const server = createServer(store, provider);\n await server.scheduler.start();\n const appCount = (await store.listInstalled()).length;\n const { version } = createRequire(import.meta.url)('../package.json') as { version: string };\n const httpServer = serve({ fetch: server.app.fetch, port, hostname }, (info) => {\n const notes: string[] = [];\n if (mode === 'off' && !loopback) {\n notes.push(\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 notes.push(`sign in: http://localhost:${info.port}/login?token=${secret}`);\n notes.push('(one-time URL — mint reusable tokens with `alashi token create`)');\n }\n void printBanner({\n version,\n kernel: provider.name,\n appCount,\n jobCount: server.scheduler.jobCount,\n stateDir: store.root,\n url: `http://localhost:${info.port}`,\n notes,\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":"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"}