@lark-sh/cli 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +1063 -0
  2. package/package.json +35 -0
package/dist/index.js ADDED
@@ -0,0 +1,1063 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/auth/login.ts
7
+ import http from "http";
8
+ import open from "open";
9
+
10
+ // src/config.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import os from "os";
14
+ var CONFIG_DIR = path.join(os.homedir(), ".lark");
15
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
16
+ function ensureDir() {
17
+ if (!fs.existsSync(CONFIG_DIR)) {
18
+ fs.mkdirSync(CONFIG_DIR, { mode: 448, recursive: true });
19
+ }
20
+ }
21
+ function readConfig() {
22
+ try {
23
+ const data = fs.readFileSync(CONFIG_FILE, "utf-8");
24
+ return JSON.parse(data);
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+ function writeConfig(config) {
30
+ ensureDir();
31
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
32
+ mode: 384
33
+ });
34
+ }
35
+ function getSession() {
36
+ return readConfig().session;
37
+ }
38
+ function setSession(session) {
39
+ const config = readConfig();
40
+ config.session = session;
41
+ writeConfig(config);
42
+ }
43
+ function clearSession() {
44
+ const config = readConfig();
45
+ delete config.session;
46
+ delete config.adminTokens;
47
+ writeConfig(config);
48
+ }
49
+ function getDefaultProject() {
50
+ return readConfig().defaultProject;
51
+ }
52
+ function setDefaultProject(projectId) {
53
+ const config = readConfig();
54
+ config.defaultProject = projectId;
55
+ writeConfig(config);
56
+ }
57
+ function getCachedAdminToken(projectId) {
58
+ const config = readConfig();
59
+ const entry = config.adminTokens?.[projectId];
60
+ if (!entry) return void 0;
61
+ if (entry.expiresAt > Date.now() + 5 * 60 * 1e3) {
62
+ return entry.token;
63
+ }
64
+ return void 0;
65
+ }
66
+ function setCachedAdminToken(projectId, token, expiresAt) {
67
+ const config = readConfig();
68
+ if (!config.adminTokens) config.adminTokens = {};
69
+ config.adminTokens[projectId] = { token, expiresAt };
70
+ writeConfig(config);
71
+ }
72
+
73
+ // src/utils.ts
74
+ var ApiError = class extends Error {
75
+ status;
76
+ constructor(status, message) {
77
+ super(message);
78
+ this.name = "ApiError";
79
+ this.status = status;
80
+ }
81
+ };
82
+ function resolveProject(explicit) {
83
+ const projectId = explicit || getDefaultProject();
84
+ if (!projectId) {
85
+ error(
86
+ "No project specified. Use --project <id> or run: lark config set-project <id>"
87
+ );
88
+ }
89
+ return projectId;
90
+ }
91
+ function readStdin() {
92
+ return new Promise((resolve, reject) => {
93
+ const chunks = [];
94
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
95
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
96
+ process.stdin.on("error", reject);
97
+ });
98
+ }
99
+ function error(message) {
100
+ console.error(`Error: ${message}`);
101
+ process.exit(1);
102
+ }
103
+ function jsonError(message, status) {
104
+ console.log(JSON.stringify({ error: message, ...status ? { status } : {} }));
105
+ process.exit(1);
106
+ }
107
+ function handleError(err, json) {
108
+ if (err instanceof ApiError) {
109
+ if (json) {
110
+ jsonError(err.message, err.status);
111
+ }
112
+ if (err.status === 401) {
113
+ error("Not authenticated. Run: lark login");
114
+ }
115
+ error(err.message);
116
+ }
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ if (json) {
119
+ jsonError(msg);
120
+ }
121
+ error(msg);
122
+ }
123
+
124
+ // src/api/admin.ts
125
+ var API_BASE = "https://db.lark.sh";
126
+ async function adminRequest(method, path2, body) {
127
+ const session = getSession();
128
+ if (!session) {
129
+ throw new ApiError(401, "Not authenticated. Run: lark login");
130
+ }
131
+ const headers = {
132
+ "Content-Type": "application/json",
133
+ Accept: "application/json",
134
+ Cookie: `lark_session=${session}`
135
+ };
136
+ const res = await fetch(`${API_BASE}${path2}`, {
137
+ method,
138
+ headers,
139
+ body: body !== void 0 ? JSON.stringify(body) : void 0
140
+ });
141
+ if (!res.ok) {
142
+ let message = "Request failed";
143
+ try {
144
+ const err = await res.json();
145
+ message = err.error || err.message || message;
146
+ } catch {
147
+ }
148
+ throw new ApiError(res.status, message);
149
+ }
150
+ return res.json();
151
+ }
152
+ var admin = {
153
+ getMe: () => adminRequest("GET", "/me"),
154
+ logout: () => adminRequest("POST", "/auth/logout"),
155
+ getProjects: () => adminRequest("GET", "/projects"),
156
+ createProject: (name) => adminRequest("POST", "/projects", { name }),
157
+ getProject: (id) => adminRequest("GET", `/projects/${id}`),
158
+ updateProject: (id, updates) => adminRequest("PATCH", `/projects/${id}`, updates),
159
+ deleteProject: (id, confirm) => adminRequest("DELETE", `/projects/${id}`, { confirm }),
160
+ regenerateSecret: (id) => adminRequest("POST", `/projects/${id}/regenerate-secret`),
161
+ getAdminToken: (projectId, database) => adminRequest("POST", `/projects/${projectId}/admin-token`, database ? { database } : {}),
162
+ getDatabases: (projectId, opts) => {
163
+ const params = new URLSearchParams();
164
+ if (opts?.limit) params.set("limit", String(opts.limit));
165
+ if (opts?.offset) params.set("offset", String(opts.offset));
166
+ if (opts?.search) params.set("search", opts.search);
167
+ const q = params.toString();
168
+ return adminRequest(
169
+ "GET",
170
+ `/projects/${projectId}/databases${q ? `?${q}` : ""}`
171
+ );
172
+ },
173
+ createDatabase: (projectId, id) => adminRequest("POST", `/projects/${projectId}/databases`, { id }),
174
+ deleteDatabase: (projectId, dbId) => adminRequest("DELETE", `/projects/${projectId}/databases/${dbId}`),
175
+ getDashboard: (projectId, opts) => {
176
+ const params = new URLSearchParams();
177
+ if (opts?.start) params.set("start", opts.start);
178
+ if (opts?.end) params.set("end", opts.end);
179
+ const q = params.toString();
180
+ return adminRequest("GET", `/projects/${projectId}/dashboard${q ? `?${q}` : ""}`);
181
+ },
182
+ getEvents: (projectId, opts) => {
183
+ const params = new URLSearchParams();
184
+ if (opts?.limit) params.set("limit", String(opts.limit));
185
+ if (opts?.offset) params.set("offset", String(opts.offset));
186
+ const q = params.toString();
187
+ return adminRequest(
188
+ "GET",
189
+ `/projects/${projectId}/events${q ? `?${q}` : ""}`
190
+ );
191
+ },
192
+ getBilling: (projectId, period) => {
193
+ const params = new URLSearchParams();
194
+ if (period) params.set("period", period);
195
+ const q = params.toString();
196
+ return adminRequest("GET", `/projects/${projectId}/billing${q ? `?${q}` : ""}`);
197
+ }
198
+ };
199
+
200
+ // src/auth/login.ts
201
+ var DASHBOARD_URL = "https://dashboard.lark.sh";
202
+ async function login() {
203
+ return new Promise((resolve, reject) => {
204
+ const server = http.createServer((req, res) => {
205
+ const url = new URL(req.url, `http://localhost`);
206
+ if (url.pathname !== "/callback") {
207
+ res.writeHead(404);
208
+ res.end("Not found");
209
+ return;
210
+ }
211
+ const token = url.searchParams.get("token");
212
+ if (!token) {
213
+ res.writeHead(400);
214
+ res.end("Missing token");
215
+ reject(new Error("Authorization callback missing token"));
216
+ server.close();
217
+ return;
218
+ }
219
+ setSession(token);
220
+ res.writeHead(200, { "Content-Type": "text/html" });
221
+ res.end(`
222
+ <!DOCTYPE html>
223
+ <html>
224
+ <head><title>Lark CLI</title></head>
225
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #364f6b; color: white;">
226
+ <div style="text-align: center;">
227
+ <h1>CLI Authorized!</h1>
228
+ <p>You can close this window and return to the terminal.</p>
229
+ </div>
230
+ </body>
231
+ </html>
232
+ `);
233
+ admin.getMe().then((user) => {
234
+ resolve({ name: user.name, email: user.email });
235
+ }).catch((err) => {
236
+ reject(err);
237
+ }).finally(() => {
238
+ server.close();
239
+ });
240
+ });
241
+ server.listen(0, "127.0.0.1", () => {
242
+ const addr = server.address();
243
+ if (!addr || typeof addr === "string") {
244
+ reject(new Error("Failed to start local server"));
245
+ return;
246
+ }
247
+ const port = addr.port;
248
+ const authUrl = `${DASHBOARD_URL}/cli-auth?port=${port}`;
249
+ console.log("Opening browser to authorize...");
250
+ open(authUrl).catch(() => {
251
+ console.log(`
252
+ If the browser didn't open, visit:
253
+ ${authUrl}
254
+ `);
255
+ });
256
+ });
257
+ server.on("error", reject);
258
+ const timeout = setTimeout(() => {
259
+ server.close();
260
+ reject(new Error("Login timed out after 5 minutes"));
261
+ }, 5 * 60 * 1e3);
262
+ timeout.unref();
263
+ });
264
+ }
265
+
266
+ // src/commands/auth.ts
267
+ function registerAuthCommands(program2) {
268
+ program2.command("login").description(
269
+ "Log in to Lark via your browser.\n\nOpens the Lark dashboard where you authorize the CLI.\nYour session is saved to ~/.lark/config.json.\n\nExample:\n $ lark login"
270
+ ).action(async () => {
271
+ try {
272
+ const { name, email } = await login();
273
+ console.log(`Logged in as ${name} (${email})`);
274
+ } catch (err) {
275
+ handleError(err, false);
276
+ }
277
+ });
278
+ program2.command("logout").description(
279
+ "Log out and clear the stored session.\n\nExample:\n $ lark logout"
280
+ ).action(async () => {
281
+ const json = program2.opts().json;
282
+ try {
283
+ const session = getSession();
284
+ if (session) {
285
+ try {
286
+ await admin.logout();
287
+ } catch {
288
+ }
289
+ }
290
+ clearSession();
291
+ if (json) {
292
+ console.log(JSON.stringify({ success: true }));
293
+ } else {
294
+ console.log("Logged out.");
295
+ }
296
+ } catch (err) {
297
+ handleError(err, json);
298
+ }
299
+ });
300
+ program2.command("whoami").description(
301
+ "Show the currently logged-in user.\n\nExamples:\n $ lark whoami\n $ lark whoami --json"
302
+ ).action(async () => {
303
+ const json = program2.opts().json;
304
+ try {
305
+ const user = await admin.getMe();
306
+ if (json) {
307
+ console.log(JSON.stringify(user, null, 2));
308
+ } else {
309
+ console.log(`${user.name} (${user.email})`);
310
+ }
311
+ } catch (err) {
312
+ handleError(err, json);
313
+ }
314
+ });
315
+ }
316
+
317
+ // src/commands/config.ts
318
+ function registerConfigCommands(program2) {
319
+ const config = program2.command("config").description(
320
+ "Manage CLI configuration.\n\nSettings are stored in ~/.lark/config.json."
321
+ );
322
+ config.command("set-project <id>").description(
323
+ "Set the default project for all commands.\n\nOnce set, you can omit --project from other commands.\nThe project ID is verified before saving.\n\nExamples:\n $ lark config set-project my-app\n $ lark config set-project cli-test"
324
+ ).action(async (id) => {
325
+ const json = program2.opts().json;
326
+ try {
327
+ const project = await admin.getProject(id);
328
+ setDefaultProject(id);
329
+ if (json) {
330
+ console.log(JSON.stringify({ defaultProject: id, name: project.name }));
331
+ } else {
332
+ console.log(`Default project set to: ${project.name} (${id})`);
333
+ }
334
+ } catch (err) {
335
+ handleError(err, json);
336
+ }
337
+ });
338
+ config.command("show").description(
339
+ "Show current CLI configuration.\n\nExample:\n $ lark config show"
340
+ ).action(() => {
341
+ const json = program2.opts().json;
342
+ const defaultProject = getDefaultProject();
343
+ if (json) {
344
+ console.log(JSON.stringify({ defaultProject: defaultProject || null }));
345
+ } else {
346
+ console.log(`Default project: ${defaultProject || "(not set)"}`);
347
+ }
348
+ });
349
+ }
350
+
351
+ // src/output.ts
352
+ function table(rows, columns) {
353
+ if (rows.length === 0) {
354
+ console.log("(none)");
355
+ return;
356
+ }
357
+ const cols = columns || Object.keys(rows[0]);
358
+ const widths = cols.map((col) => col.length);
359
+ const formatted = rows.map(
360
+ (row) => cols.map((col, i) => {
361
+ const val = formatValue(row[col]);
362
+ widths[i] = Math.max(widths[i], val.length);
363
+ return val;
364
+ })
365
+ );
366
+ console.log(cols.map((col, i) => col.toUpperCase().padEnd(widths[i])).join(" "));
367
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
368
+ for (const row of formatted) {
369
+ console.log(row.map((val, i) => val.padEnd(widths[i])).join(" "));
370
+ }
371
+ }
372
+ function keyValue(pairs) {
373
+ const maxKey = Math.max(...Object.keys(pairs).map((k) => k.length));
374
+ for (const [key, value] of Object.entries(pairs)) {
375
+ console.log(`${key.padEnd(maxKey)} ${formatValue(value)}`);
376
+ }
377
+ }
378
+ function formatValue(val) {
379
+ if (val === null || val === void 0) return "-";
380
+ if (typeof val === "boolean") return val ? "yes" : "no";
381
+ if (typeof val === "number") return String(val);
382
+ return String(val);
383
+ }
384
+ function formatBytes(bytes) {
385
+ if (bytes === 0) return "0 B";
386
+ const units = ["B", "KB", "MB", "GB", "TB"];
387
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
388
+ const val = bytes / Math.pow(1024, i);
389
+ return `${val.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
390
+ }
391
+ function formatLatency(us) {
392
+ if (us < 1e3) return `${us.toFixed(0)}\xB5s`;
393
+ if (us < 1e6) return `${(us / 1e3).toFixed(1)}ms`;
394
+ return `${(us / 1e6).toFixed(2)}s`;
395
+ }
396
+ function formatTime(ts) {
397
+ const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
398
+ return d.toLocaleString();
399
+ }
400
+
401
+ // src/commands/projects.ts
402
+ function registerProjectCommands(program2) {
403
+ const projects = program2.command("projects").description(
404
+ 'Manage Lark projects.\n\nA project is the top-level container for databases, rules, and settings.\nEach project gets a unique ID (DNS-compatible slug) and a secret key.\n\nExamples:\n $ lark projects list\n $ lark projects create "My App"\n $ lark projects show my-app'
405
+ );
406
+ projects.command("list").description(
407
+ "List all your projects.\n\nExamples:\n $ lark projects list\n $ lark projects list --json"
408
+ ).action(async () => {
409
+ const json = program2.opts().json;
410
+ try {
411
+ const { projects: list } = await admin.getProjects();
412
+ if (json) {
413
+ console.log(JSON.stringify(list, null, 2));
414
+ return;
415
+ }
416
+ table(
417
+ list.map((p) => ({
418
+ id: p.id,
419
+ name: p.name,
420
+ databases: p.database_count,
421
+ created: formatTime(p.created_at)
422
+ }))
423
+ );
424
+ } catch (err) {
425
+ handleError(err, json);
426
+ }
427
+ });
428
+ projects.command("create <name>").description(
429
+ 'Create a new project.\n\nThe name is used to generate a DNS-compatible project ID.\nFor example, "My App" becomes "my-app".\n\nExamples:\n $ lark projects create "My App"\n $ lark projects create "Game Server" --json'
430
+ ).action(async (name) => {
431
+ const json = program2.opts().json;
432
+ try {
433
+ const project = await admin.createProject(name);
434
+ if (json) {
435
+ console.log(JSON.stringify(project, null, 2));
436
+ return;
437
+ }
438
+ console.log(`Created project: ${project.name} (${project.id})`);
439
+ } catch (err) {
440
+ handleError(err, json);
441
+ }
442
+ });
443
+ projects.command("show [id]").description(
444
+ "Show project details including secret key and settings.\n\nUses the default project if no ID is given.\n\nExamples:\n $ lark projects show\n $ lark projects show my-app\n $ lark projects show --json"
445
+ ).action(async (id) => {
446
+ const json = program2.opts().json;
447
+ try {
448
+ const projectId = resolveProject(id || program2.opts().project);
449
+ const project = await admin.getProject(projectId);
450
+ if (json) {
451
+ console.log(JSON.stringify(project, null, 2));
452
+ return;
453
+ }
454
+ keyValue({
455
+ "ID": project.id,
456
+ "Name": project.name,
457
+ "Secret Key": project.secret_key,
458
+ "Ephemeral": project.ephemeral,
459
+ "Auto Create": project.auto_create,
460
+ "Firebase Compat": project.firebase_compat_enabled,
461
+ "Databases": project.database_count ?? 0,
462
+ "Active DBs": project.active_database_count ?? 0,
463
+ "Created": formatTime(project.created_at)
464
+ });
465
+ } catch (err) {
466
+ handleError(err, json);
467
+ }
468
+ });
469
+ projects.command("update [id]").description(
470
+ 'Update project settings.\n\nPass one or more flags to change settings.\nUses the default project if no ID is given.\n\nExamples:\n $ lark projects update --name "New Name"\n $ lark projects update my-app --ephemeral false\n $ lark projects update --auto-create true --ephemeral true'
471
+ ).option("--name <name>", "Project name").option("--ephemeral <bool>", "Enable/disable ephemeral mode (true/false)").option("--auto-create <bool>", "Enable/disable auto-create databases (true/false)").option("--firebase-compat <bool>", "Enable/disable Firebase compatibility (true/false)").option("--firebase-project-id <id>", "Firebase project ID").action(async (id, opts) => {
472
+ const json = program2.opts().json;
473
+ try {
474
+ const projectId = resolveProject(id || program2.opts().project);
475
+ const updates = {};
476
+ if (opts.name !== void 0) updates.name = opts.name;
477
+ if (opts.ephemeral !== void 0) updates.ephemeral = opts.ephemeral === "true";
478
+ if (opts.autoCreate !== void 0) updates.auto_create = opts.autoCreate === "true";
479
+ if (opts.firebaseCompat !== void 0) updates.firebase_compat_enabled = opts.firebaseCompat === "true";
480
+ if (opts.firebaseProjectId !== void 0) updates.firebase_project_id = opts.firebaseProjectId;
481
+ if (Object.keys(updates).length === 0) {
482
+ if (json) {
483
+ console.log(JSON.stringify({ error: "No update flags provided" }));
484
+ } else {
485
+ console.error("Error: No update flags provided. See --help for options.");
486
+ }
487
+ process.exit(1);
488
+ }
489
+ const project = await admin.updateProject(projectId, updates);
490
+ if (json) {
491
+ console.log(JSON.stringify(project, null, 2));
492
+ return;
493
+ }
494
+ console.log(`Updated project: ${project.name} (${project.id})`);
495
+ } catch (err) {
496
+ handleError(err, json);
497
+ }
498
+ });
499
+ projects.command("delete [id]").description(
500
+ "Delete a project and all its databases.\n\nThis is irreversible. You must pass --confirm with the project ID.\n\nExamples:\n $ lark projects delete my-app --confirm my-app\n $ lark projects delete --confirm cli-test"
501
+ ).option("--confirm <id>", "Confirm deletion by passing the project ID").action(async (id, opts) => {
502
+ const json = program2.opts().json;
503
+ try {
504
+ const projectId = resolveProject(id || program2.opts().project);
505
+ if (!opts.confirm) {
506
+ if (json) {
507
+ console.log(JSON.stringify({ error: `Pass --confirm ${projectId} to delete` }));
508
+ } else {
509
+ console.error(`Error: Pass --confirm ${projectId} to confirm deletion.`);
510
+ }
511
+ process.exit(1);
512
+ }
513
+ await admin.deleteProject(projectId, opts.confirm);
514
+ if (json) {
515
+ console.log(JSON.stringify({ success: true }));
516
+ } else {
517
+ console.log(`Deleted project: ${projectId}`);
518
+ }
519
+ } catch (err) {
520
+ handleError(err, json);
521
+ }
522
+ });
523
+ projects.command("regenerate-secret [id]").description(
524
+ "Regenerate the project secret key.\n\nThe old key is immediately invalidated. Any clients using it\nwill need to be updated.\n\nExamples:\n $ lark projects regenerate-secret\n $ lark projects regenerate-secret my-app"
525
+ ).action(async (id) => {
526
+ const json = program2.opts().json;
527
+ try {
528
+ const projectId = resolveProject(id || program2.opts().project);
529
+ const result = await admin.regenerateSecret(projectId);
530
+ if (json) {
531
+ console.log(JSON.stringify(result, null, 2));
532
+ } else {
533
+ console.log(`New secret key: ${result.secret_key}`);
534
+ }
535
+ } catch (err) {
536
+ handleError(err, json);
537
+ }
538
+ });
539
+ }
540
+
541
+ // src/commands/databases.ts
542
+ function registerDatabaseCommands(program2) {
543
+ const databases = program2.command("databases").alias("db").description(
544
+ 'Manage databases within a project.\n\nDatabases hold your real-time data. Use "lark data" commands\nto read and write data within a database.\n\nShorthand: "lark db" works the same as "lark databases".\n\nExamples:\n $ lark databases list\n $ lark db create my-database\n $ lark db list --search "user"'
545
+ );
546
+ databases.command("list").description(
547
+ 'List databases in the current project.\n\nExamples:\n $ lark databases list\n $ lark databases list --search "chat"\n $ lark databases list --limit 10 --offset 20\n $ lark --project my-app databases list'
548
+ ).option("--search <query>", "Filter databases by ID").option("--limit <n>", "Max results to return", "50").option("--offset <n>", "Number of results to skip", "0").action(async (opts) => {
549
+ const json = program2.opts().json;
550
+ try {
551
+ const projectId = resolveProject(program2.opts().project);
552
+ const result = await admin.getDatabases(projectId, {
553
+ search: opts.search,
554
+ limit: parseInt(opts.limit),
555
+ offset: parseInt(opts.offset)
556
+ });
557
+ if (json) {
558
+ console.log(JSON.stringify(result, null, 2));
559
+ return;
560
+ }
561
+ console.log(`Total: ${result.total}`);
562
+ if (result.databases.length === 0) {
563
+ console.log("(no databases)");
564
+ return;
565
+ }
566
+ table(
567
+ result.databases.map((db) => ({
568
+ id: db.id,
569
+ status: db.status,
570
+ created: formatTime(db.created_at),
571
+ "last activity": db.last_activity ? formatTime(db.last_activity) : "-"
572
+ }))
573
+ );
574
+ } catch (err) {
575
+ handleError(err, json);
576
+ }
577
+ });
578
+ databases.command("create <id>").description(
579
+ "Create a new database.\n\nThe ID must be a valid identifier (lowercase, hyphens allowed).\n\nExamples:\n $ lark databases create users\n $ lark databases create game-state\n $ lark db create chat-rooms --json"
580
+ ).action(async (id) => {
581
+ const json = program2.opts().json;
582
+ try {
583
+ const projectId = resolveProject(program2.opts().project);
584
+ const db = await admin.createDatabase(projectId, id);
585
+ if (json) {
586
+ console.log(JSON.stringify(db, null, 2));
587
+ } else {
588
+ console.log(`Created database: ${db.id}`);
589
+ }
590
+ } catch (err) {
591
+ handleError(err, json);
592
+ }
593
+ });
594
+ databases.command("delete <id>").description(
595
+ "Delete a database and all its data.\n\nThis is irreversible.\n\nExamples:\n $ lark databases delete old-data\n $ lark db delete test-db"
596
+ ).action(async (id) => {
597
+ const json = program2.opts().json;
598
+ try {
599
+ const projectId = resolveProject(program2.opts().project);
600
+ await admin.deleteDatabase(projectId, id);
601
+ if (json) {
602
+ console.log(JSON.stringify({ success: true }));
603
+ } else {
604
+ console.log(`Deleted database: ${id}`);
605
+ }
606
+ } catch (err) {
607
+ handleError(err, json);
608
+ }
609
+ });
610
+ }
611
+
612
+ // src/commands/data.ts
613
+ import fs2 from "fs";
614
+
615
+ // src/auth/token.ts
616
+ async function getAdminToken(projectId) {
617
+ const cached = getCachedAdminToken(projectId);
618
+ if (cached) return cached;
619
+ const { token } = await admin.getAdminToken(projectId);
620
+ const expiresAt = Date.now() + 55 * 60 * 1e3;
621
+ setCachedAdminToken(projectId, token, expiresAt);
622
+ return token;
623
+ }
624
+
625
+ // src/api/data.ts
626
+ async function dataRequest(projectId, method, database, path2, body) {
627
+ const token = await getAdminToken(projectId);
628
+ const normalizedPath = path2.startsWith("/") ? path2 : `/${path2}`;
629
+ const url = `https://${projectId}.larkdb.net/${database}${normalizedPath}.json?v=2&auth=${token}`;
630
+ const headers = {
631
+ "Content-Type": "application/json",
632
+ Accept: "application/json"
633
+ };
634
+ const res = await fetch(url, {
635
+ method,
636
+ headers,
637
+ body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0
638
+ });
639
+ if (!res.ok) {
640
+ let message = "Request failed";
641
+ try {
642
+ const err = await res.json();
643
+ message = err.error || message;
644
+ } catch {
645
+ message = await res.text().catch(() => message);
646
+ }
647
+ throw new ApiError(res.status, message);
648
+ }
649
+ return res.json();
650
+ }
651
+
652
+ // src/commands/data.ts
653
+ function registerDataCommands(program2) {
654
+ const data = program2.command("data").description(
655
+ `Read and write data in your databases.
656
+
657
+ Data is organized as a JSON tree. Use paths like "/" for the
658
+ root, "/users/alice" for nested data, etc.
659
+
660
+ Examples:
661
+ $ lark data get mydb /
662
+ $ lark data set mydb /users/alice '{"name": "Alice"}'
663
+ $ lark data watch mydb /messages`
664
+ );
665
+ data.command("get <database> <path>").description(
666
+ 'Get data at a path.\n\nReturns the JSON value at the given path. Use "/" to get\nthe entire database.\n\nArguments:\n database Database ID (e.g. "users")\n path Path in the database (e.g. "/", "/users/alice")\n\nExamples:\n $ lark data get mydb /\n $ lark data get mydb /users/alice\n $ lark data get mydb /settings/theme --json'
667
+ ).action(async (database, path2) => {
668
+ const json = program2.opts().json;
669
+ try {
670
+ const projectId = resolveProject(program2.opts().project);
671
+ const result = await dataRequest(projectId, "GET", database, path2);
672
+ console.log(JSON.stringify(result, null, 2));
673
+ } catch (err) {
674
+ handleError(err, json);
675
+ }
676
+ });
677
+ data.command("set <database> <path> <value>").description(
678
+ `Set (overwrite) data at a path.
679
+
680
+ Replaces whatever exists at the path with the given JSON value.
681
+ Use "-" as the value to read JSON from stdin.
682
+
683
+ Arguments:
684
+ database Database ID (e.g. "users")
685
+ path Path in the database (e.g. "/users/alice")
686
+ value JSON value, or "-" to read from stdin
687
+
688
+ Examples:
689
+ $ lark data set mydb / '{"key": "value"}'
690
+ $ lark data set mydb /users/alice '{"name": "Alice", "score": 100}'
691
+ $ cat data.json | lark data set mydb /path -`
692
+ ).action(async (database, path2, value) => {
693
+ const json = program2.opts().json;
694
+ try {
695
+ const projectId = resolveProject(program2.opts().project);
696
+ const body = value === "-" ? await readStdin() : value;
697
+ const parsed = JSON.parse(body);
698
+ const result = await dataRequest(projectId, "PUT", database, path2, parsed);
699
+ if (json) {
700
+ console.log(JSON.stringify(result, null, 2));
701
+ } else {
702
+ console.log("OK");
703
+ }
704
+ } catch (err) {
705
+ handleError(err, json);
706
+ }
707
+ });
708
+ data.command("update <database> <path> <value>").description(
709
+ `Update (merge) data at a path.
710
+
711
+ Merges the given JSON into the existing data. Only the specified
712
+ keys are changed; other keys are preserved. Use "-" to read
713
+ from stdin.
714
+
715
+ Arguments:
716
+ database Database ID (e.g. "users")
717
+ path Path in the database (e.g. "/users/alice")
718
+ value JSON to merge, or "-" to read from stdin
719
+
720
+ Examples:
721
+ $ lark data update mydb /users/alice '{"score": 150}'
722
+ $ lark data update mydb /config '{"theme": "dark"}'
723
+ $ echo '{"online": true}' | lark data update mydb /users/alice -`
724
+ ).action(async (database, path2, value) => {
725
+ const json = program2.opts().json;
726
+ try {
727
+ const projectId = resolveProject(program2.opts().project);
728
+ const body = value === "-" ? await readStdin() : value;
729
+ const parsed = JSON.parse(body);
730
+ const result = await dataRequest(projectId, "PATCH", database, path2, parsed);
731
+ if (json) {
732
+ console.log(JSON.stringify(result, null, 2));
733
+ } else {
734
+ console.log("OK");
735
+ }
736
+ } catch (err) {
737
+ handleError(err, json);
738
+ }
739
+ });
740
+ data.command("push <database> <path> <value>").description(
741
+ `Push data to a list, generating a unique key.
742
+
743
+ Appends the value under an auto-generated key (like a list push).
744
+ Returns the generated key name. Use "-" to read from stdin.
745
+
746
+ Arguments:
747
+ database Database ID (e.g. "chat")
748
+ path Path to the list (e.g. "/messages")
749
+ value JSON value, or "-" to read from stdin
750
+
751
+ Examples:
752
+ $ lark data push chat /messages '{"from": "Alice", "text": "Hello"}'
753
+ $ echo '{"event": "login"}' | lark data push mydb /logs -`
754
+ ).action(async (database, path2, value) => {
755
+ const json = program2.opts().json;
756
+ try {
757
+ const projectId = resolveProject(program2.opts().project);
758
+ const body = value === "-" ? await readStdin() : value;
759
+ const parsed = JSON.parse(body);
760
+ const result = await dataRequest(projectId, "POST", database, path2, parsed);
761
+ if (json) {
762
+ console.log(JSON.stringify(result, null, 2));
763
+ } else {
764
+ console.log(JSON.stringify(result, null, 2));
765
+ }
766
+ } catch (err) {
767
+ handleError(err, json);
768
+ }
769
+ });
770
+ data.command("delete <database> <path>").description(
771
+ 'Delete data at a path.\n\nRemoves the value at the path. Parent nodes that become empty\nmay also be removed.\n\nArguments:\n database Database ID (e.g. "users")\n path Path to delete (e.g. "/users/alice")\n\nExamples:\n $ lark data delete mydb /users/alice\n $ lark data delete mydb /temp'
772
+ ).action(async (database, path2) => {
773
+ const json = program2.opts().json;
774
+ try {
775
+ const projectId = resolveProject(program2.opts().project);
776
+ await dataRequest(projectId, "DELETE", database, path2);
777
+ if (json) {
778
+ console.log(JSON.stringify({ success: true }));
779
+ } else {
780
+ console.log("Deleted.");
781
+ }
782
+ } catch (err) {
783
+ handleError(err, json);
784
+ }
785
+ });
786
+ data.command("export <database> [path]").description(
787
+ 'Export database data as JSON.\n\nPrints the data as formatted JSON. Use -o to write to a file\ninstead of stdout. Path defaults to "/" (entire database).\n\nArguments:\n database Database ID (e.g. "users")\n path Path to export (default: "/")\n\nExamples:\n $ lark data export mydb\n $ lark data export mydb /users -o users.json\n $ lark data export mydb / -o backup.json'
788
+ ).option("-o, --output <file>", "Write to file instead of stdout").action(async (database, path2, opts) => {
789
+ const json = program2.opts().json;
790
+ try {
791
+ const projectId = resolveProject(program2.opts().project);
792
+ const result = await dataRequest(projectId, "GET", database, path2 || "/");
793
+ const output = JSON.stringify(result, null, 2);
794
+ if (opts.output) {
795
+ fs2.writeFileSync(opts.output, output + "\n");
796
+ if (!json) {
797
+ console.log(`Exported to ${opts.output}`);
798
+ }
799
+ } else {
800
+ console.log(output);
801
+ }
802
+ } catch (err) {
803
+ handleError(err, json);
804
+ }
805
+ });
806
+ data.command("import <database> [path]").description(
807
+ 'Import data from a JSON file.\n\nOverwrites data at the given path with the contents of the file.\nPath defaults to "/" (entire database).\n\nArguments:\n database Database ID (e.g. "users")\n path Path to import into (default: "/")\n\nExamples:\n $ lark data import mydb -f backup.json\n $ lark data import mydb /users -f users.json'
808
+ ).requiredOption("-f, --file <file>", "JSON file to import").action(async (database, path2, opts) => {
809
+ const json = program2.opts().json;
810
+ try {
811
+ const projectId = resolveProject(program2.opts().project);
812
+ const content = fs2.readFileSync(opts.file, "utf-8");
813
+ const parsed = JSON.parse(content);
814
+ await dataRequest(projectId, "PUT", database, path2 || "/", parsed);
815
+ if (json) {
816
+ console.log(JSON.stringify({ success: true }));
817
+ } else {
818
+ console.log(`Imported ${opts.file} to ${database}:${path2 || "/"}`);
819
+ }
820
+ } catch (err) {
821
+ handleError(err, json);
822
+ }
823
+ });
824
+ data.command("watch <database> <path>").description(
825
+ 'Watch for real-time changes via SSE streaming.\n\nStreams live updates as they happen. Each change is printed\nas a line with the event type and data. Press Ctrl+C to stop.\n\nWith --json, outputs one JSON object per line (NDJSON).\n\nArguments:\n database Database ID (e.g. "chat")\n path Path to watch (e.g. "/messages")\n\nExamples:\n $ lark data watch mydb /\n $ lark data watch chat /messages\n $ lark data watch mydb /users --json'
826
+ ).action(async (database, path2) => {
827
+ const jsonMode = program2.opts().json;
828
+ try {
829
+ const projectId = resolveProject(program2.opts().project);
830
+ const token = await getAdminToken(projectId);
831
+ const normalizedPath = path2.startsWith("/") ? path2 : `/${path2}`;
832
+ const url = `https://${projectId}.larkdb.net/${database}${normalizedPath}.json?v=2&auth=${token}`;
833
+ const res = await fetch(url, {
834
+ headers: { Accept: "text/event-stream" }
835
+ });
836
+ if (!res.ok || !res.body) {
837
+ throw new Error(`Failed to connect to SSE stream (${res.status})`);
838
+ }
839
+ if (!jsonMode) {
840
+ console.log(`Watching ${database}:${path2} \u2014 press Ctrl+C to stop
841
+ `);
842
+ }
843
+ const reader = res.body.getReader();
844
+ const decoder = new TextDecoder();
845
+ let buffer = "";
846
+ let eventType = "";
847
+ let eventData = "";
848
+ while (true) {
849
+ const { done, value } = await reader.read();
850
+ if (done) break;
851
+ buffer += decoder.decode(value, { stream: true });
852
+ const lines = buffer.split("\n");
853
+ buffer = lines.pop();
854
+ for (const line of lines) {
855
+ if (line.startsWith("event:")) {
856
+ eventType = line.slice(6).trim();
857
+ } else if (line.startsWith("data:")) {
858
+ eventData += line.slice(5).trim();
859
+ } else if (line === "") {
860
+ if (eventType && eventData) {
861
+ if (jsonMode) {
862
+ try {
863
+ const parsed = JSON.parse(eventData);
864
+ console.log(JSON.stringify({ event: eventType, data: parsed }));
865
+ } catch {
866
+ console.log(JSON.stringify({ event: eventType, data: eventData }));
867
+ }
868
+ } else {
869
+ console.log(`[${eventType}] ${eventData}`);
870
+ }
871
+ }
872
+ eventType = "";
873
+ eventData = "";
874
+ }
875
+ }
876
+ }
877
+ } catch (err) {
878
+ handleError(err, jsonMode);
879
+ }
880
+ });
881
+ }
882
+
883
+ // src/commands/dashboard.ts
884
+ function registerDashboardCommands(program2) {
885
+ program2.command("dashboard [id]").description(
886
+ "Show project dashboard with metrics and recent events.\n\nDisplays CCU, bandwidth, operations, and latency for the\ngiven time range (defaults to last 24 hours).\n\nUses the default project if no ID is given.\n\nExamples:\n $ lark dashboard\n $ lark dashboard my-app\n $ lark dashboard --start 2026-02-01 --end 2026-02-15\n $ lark dashboard --json"
887
+ ).option("--start <date>", 'Start date (ISO format, e.g. "2026-02-01")').option("--end <date>", 'End date (ISO format, e.g. "2026-02-15")').action(async (id, opts) => {
888
+ const json = program2.opts().json;
889
+ try {
890
+ const projectId = resolveProject(id || program2.opts().project);
891
+ const data = await admin.getDashboard(projectId, opts);
892
+ if (json) {
893
+ console.log(JSON.stringify(data, null, 2));
894
+ return;
895
+ }
896
+ console.log(`Dashboard: ${data.project.name} (${data.project.id})`);
897
+ console.log(`Period: ${data.time_range.start} to ${data.time_range.end}
898
+ `);
899
+ if (data.current) {
900
+ console.log("--- Current ---");
901
+ keyValue({
902
+ "CCU": data.current.ccu,
903
+ "Peak CCU Today": data.current.peak_ccu_today,
904
+ "Bytes In Today": formatBytes(data.current.bytes_in_today),
905
+ "Bytes Out Today": formatBytes(data.current.bytes_out_today),
906
+ "Writes Today": data.current.writes_today,
907
+ "Reads Today": data.current.reads_today,
908
+ "Permission Denials": data.current.permission_denials
909
+ });
910
+ console.log();
911
+ }
912
+ if (data.summary) {
913
+ console.log("--- Summary ---");
914
+ keyValue({
915
+ "Peak CCU": data.summary.peak_ccu,
916
+ "Total Bytes In": formatBytes(data.summary.total_bytes_in),
917
+ "Total Bytes Out": formatBytes(data.summary.total_bytes_out),
918
+ "Total Writes": data.summary.total_writes,
919
+ "Total Reads": data.summary.total_reads,
920
+ "Total Events": data.summary.total_events,
921
+ "Avg Latency": formatLatency(data.summary.avg_latency_us)
922
+ });
923
+ console.log();
924
+ }
925
+ if (data.recent_events && data.recent_events.length > 0) {
926
+ console.log("--- Recent Events ---");
927
+ table(
928
+ data.recent_events.slice(0, 10).map((e) => ({
929
+ time: formatTime(e.ts),
930
+ database: e.database_id,
931
+ type: e.event_type,
932
+ message: e.message
933
+ }))
934
+ );
935
+ }
936
+ } catch (err) {
937
+ handleError(err, json);
938
+ }
939
+ });
940
+ program2.command("events [id]").description(
941
+ "Show project events (database connections, errors, etc.).\n\nUses the default project if no ID is given.\n\nExamples:\n $ lark events\n $ lark events my-app --limit 50\n $ lark events --json"
942
+ ).option("--limit <n>", "Max results to return", "25").option("--offset <n>", "Number of results to skip", "0").action(async (id, opts) => {
943
+ const json = program2.opts().json;
944
+ try {
945
+ const projectId = resolveProject(id || program2.opts().project);
946
+ const result = await admin.getEvents(projectId, {
947
+ limit: parseInt(opts.limit),
948
+ offset: parseInt(opts.offset)
949
+ });
950
+ if (json) {
951
+ console.log(JSON.stringify(result, null, 2));
952
+ return;
953
+ }
954
+ if (result.events.length === 0) {
955
+ console.log("No events.");
956
+ return;
957
+ }
958
+ table(
959
+ result.events.map((e) => ({
960
+ id: e.id,
961
+ time: formatTime(e.ts),
962
+ database: e.database_id,
963
+ type: e.event_type,
964
+ message: e.message
965
+ }))
966
+ );
967
+ } catch (err) {
968
+ handleError(err, json);
969
+ }
970
+ });
971
+ program2.command("billing [id]").description(
972
+ "Show project billing info for a given month.\n\nDefaults to the current billing period.\n\nExamples:\n $ lark billing\n $ lark billing my-app\n $ lark billing --period 2026-01\n $ lark billing --json"
973
+ ).option("--period <month>", 'Billing period in YYYY-MM format (e.g. "2026-01")').action(async (id, opts) => {
974
+ const json = program2.opts().json;
975
+ try {
976
+ const projectId = resolveProject(id || program2.opts().project);
977
+ const data = await admin.getBilling(projectId, opts.period);
978
+ if (json) {
979
+ console.log(JSON.stringify(data, null, 2));
980
+ return;
981
+ }
982
+ keyValue({
983
+ "Period": data.period_start,
984
+ "Peak CCU": data.peak_ccu,
985
+ "Total Bandwidth": formatBytes(data.total_bandwidth),
986
+ "Total Storage": formatBytes(data.total_storage)
987
+ });
988
+ } catch (err) {
989
+ handleError(err, json);
990
+ }
991
+ });
992
+ }
993
+
994
+ // src/commands/rules.ts
995
+ import fs3 from "fs";
996
+ function registerRulesCommands(program2) {
997
+ const rules = program2.command("rules").description(
998
+ "Manage project security rules.\n\nRules control read/write access to your database paths.\nThey use JSON5 format and follow Firebase-style syntax.\n\nExamples:\n $ lark rules get\n $ lark rules set -f rules.json"
999
+ );
1000
+ rules.command("get [id]").description(
1001
+ "Get the current security rules.\n\nPrints the raw rules JSON. Use --json to get a structured\nresponse with both the raw string and parsed object.\n\nExamples:\n $ lark rules get\n $ lark rules get my-app\n $ lark rules get --json\n $ lark rules get > rules-backup.json"
1002
+ ).action(async (id) => {
1003
+ const json = program2.opts().json;
1004
+ try {
1005
+ const projectId = resolveProject(id || program2.opts().project);
1006
+ const project = await admin.getProject(projectId);
1007
+ if (json) {
1008
+ console.log(JSON.stringify({ rules_json: project.rules_json, rules: project.rules }, null, 2));
1009
+ return;
1010
+ }
1011
+ if (project.rules_json) {
1012
+ console.log(project.rules_json);
1013
+ } else {
1014
+ console.log("No rules configured.");
1015
+ }
1016
+ } catch (err) {
1017
+ handleError(err, json);
1018
+ }
1019
+ });
1020
+ rules.command("set [id]").description(
1021
+ `Set security rules from a file or stdin.
1022
+
1023
+ Accepts JSON5 format. Use -f to read from a file,
1024
+ or pipe rules via stdin.
1025
+
1026
+ Examples:
1027
+ $ lark rules set -f rules.json
1028
+ $ lark rules set my-app -f rules.json5
1029
+ $ cat rules.json | lark rules set
1030
+ $ echo '{"rules": {".read": true}}' | lark rules set`
1031
+ ).option("-f, --file <file>", "Rules file (JSON5 format)").action(async (id, opts) => {
1032
+ const json = program2.opts().json;
1033
+ try {
1034
+ const projectId = resolveProject(id || program2.opts().project);
1035
+ let rulesJson;
1036
+ if (opts.file) {
1037
+ rulesJson = fs3.readFileSync(opts.file, "utf-8");
1038
+ } else {
1039
+ rulesJson = await readStdin();
1040
+ }
1041
+ const project = await admin.updateProject(projectId, { rules_json: rulesJson.trim() });
1042
+ if (json) {
1043
+ console.log(JSON.stringify({ rules_json: project.rules_json, rules: project.rules }, null, 2));
1044
+ } else {
1045
+ console.log("Rules updated.");
1046
+ }
1047
+ } catch (err) {
1048
+ handleError(err, json);
1049
+ }
1050
+ });
1051
+ }
1052
+
1053
+ // src/index.ts
1054
+ var program = new Command();
1055
+ program.name("lark").description("CLI for Lark.sh \u2014 manage projects, databases, and data from the terminal.\n\nGet started:\n $ lark login\n $ lark config set-project <id>\n $ lark data get mydb /\n\nDocs: https://docs.larksh.com (agent-friendly)\nMCP server: https://docs.larksh.com/mcp (provides the SearchLark tool)").version("0.1.0").option("--json", "Output results as machine-readable JSON").option("--project <id>", 'Project ID (overrides the default set by "config set-project")');
1056
+ registerAuthCommands(program);
1057
+ registerConfigCommands(program);
1058
+ registerProjectCommands(program);
1059
+ registerDatabaseCommands(program);
1060
+ registerDataCommands(program);
1061
+ registerDashboardCommands(program);
1062
+ registerRulesCommands(program);
1063
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lark-sh/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for Lark.sh — manage projects, databases, and data from the terminal",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lark-sh/lark-admin",
10
+ "directory": "cli"
11
+ },
12
+ "homepage": "https://lark.sh",
13
+ "keywords": ["lark", "larksh", "realtime", "database", "cli"],
14
+ "bin": {
15
+ "lark": "./dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "dev": "tsup --watch",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "dependencies": {
27
+ "commander": "^12.1.0",
28
+ "open": "^10.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "tsup": "^8.1.0",
33
+ "typescript": "^5.5.0"
34
+ }
35
+ }