@uwu/flora-cli 0.0.0 → 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.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @uwu/flora-cli
2
+
3
+ TypeScript CLI for flora runtime management.
4
+
5
+ ## Generate API types
6
+
7
+ ```bash
8
+ pnpm --filter @uwu/flora-cli run generate:api
9
+ ```
10
+
11
+ ## Build
12
+
13
+ ```bash
14
+ pnpm --filter @uwu/flora-cli run build
15
+ ```
@@ -0,0 +1 @@
1
+ export { };
package/dist/index.mjs ADDED
@@ -0,0 +1,871 @@
1
+ #!/usr/bin/env node
2
+ import process$1 from "node:process";
3
+ import { defineCommand, runMain } from "citty";
4
+ import { colors } from "consola/utils";
5
+ import path from "node:path";
6
+ import { loadConfig } from "c12";
7
+ import Conf from "conf";
8
+ import { readFile, stat } from "node:fs/promises";
9
+ import ignore from "ignore";
10
+ import { glob } from "tinyglobby";
11
+ import { createApiClient } from "@uwu/flora-api-client";
12
+ import { createConsola } from "consola";
13
+ import { cancel, isCancel, text } from "@clack/prompts";
14
+ import { zipSync } from "fflate";
15
+
16
+ //#region package.json
17
+ var name = "@uwu/flora-cli";
18
+ var version = "0.1.0";
19
+ var description = "flora command line interface";
20
+
21
+ //#endregion
22
+ //#region src/lib/types.ts
23
+ const DEFAULT_API_URL = "http://localhost:3000/api";
24
+
25
+ //#endregion
26
+ //#region src/lib/config.ts
27
+ const store = new Conf({
28
+ projectName: "flora",
29
+ configName: "cli",
30
+ defaults: {
31
+ apiUrl: DEFAULT_API_URL,
32
+ token: void 0
33
+ }
34
+ });
35
+ function loadConfig$1() {
36
+ return {
37
+ apiUrl: store.get("apiUrl") ?? "http://localhost:3000/api",
38
+ token: store.get("token")
39
+ };
40
+ }
41
+ function saveConfig(config) {
42
+ store.set(config);
43
+ }
44
+ async function loadProjectConfig(cwd = process.cwd()) {
45
+ const { config } = await loadConfig({
46
+ cwd,
47
+ name: "flora",
48
+ configFile: "flora.config",
49
+ rcFile: false,
50
+ packageJson: false,
51
+ defaults: { deploy: {} }
52
+ });
53
+ return config.deploy ?? {};
54
+ }
55
+
56
+ //#endregion
57
+ //#region src/lib/files.ts
58
+ const ALLOWED_EXTENSIONS = new Set([
59
+ ".ts",
60
+ ".tsx",
61
+ ".js",
62
+ ".jsx",
63
+ ".mjs",
64
+ ".cts"
65
+ ]);
66
+ const EXTRA_FILE_NAMES = new Set([
67
+ "package.json",
68
+ "pnpm-lock.yaml",
69
+ "package-lock.json",
70
+ "yarn.lock",
71
+ "bun.lockb",
72
+ "tsconfig.json"
73
+ ]);
74
+ const SKIP_DIRS = new Set([
75
+ "node_modules",
76
+ "target",
77
+ "dist",
78
+ ".output",
79
+ ".next",
80
+ ".nuxt",
81
+ ".svelte-kit",
82
+ "build",
83
+ "out",
84
+ ".turbo",
85
+ ".cache",
86
+ "coverage",
87
+ ".parcel-cache",
88
+ ".vite",
89
+ ".git"
90
+ ]);
91
+ async function collectFiles(root) {
92
+ const rootAbs = path.resolve(root);
93
+ const ignorePatterns = [...SKIP_DIRS].map((dir) => `**/${dir}/**`);
94
+ const relPaths = await glob([...[...ALLOWED_EXTENSIONS].map((ext) => `**/*${ext}`), ...[...EXTRA_FILE_NAMES].map((fileName) => `**/${fileName}`)], {
95
+ cwd: rootAbs,
96
+ dot: true,
97
+ onlyFiles: true,
98
+ ignore: ignorePatterns,
99
+ followSymbolicLinks: false
100
+ });
101
+ const ignoreMatcher = await buildIgnoreMatcher(rootAbs, ignorePatterns);
102
+ const files = [];
103
+ for (const rel of relPaths) {
104
+ if (ignoreMatcher.ignores(rel)) continue;
105
+ const contents = await readFile(path.join(rootAbs, rel), "utf8");
106
+ files.push({
107
+ path: rel.replace(/\\/g, "/"),
108
+ contents
109
+ });
110
+ }
111
+ if (files.length === 0) throw new Error(`No files found under ${rootAbs}`);
112
+ return files;
113
+ }
114
+ async function buildIgnoreMatcher(rootAbs, ignorePatterns) {
115
+ const matcher = ignore();
116
+ const ignoreFiles = await glob([
117
+ ".gitignore",
118
+ ".ignore",
119
+ "**/.gitignore",
120
+ "**/.ignore"
121
+ ], {
122
+ cwd: rootAbs,
123
+ dot: true,
124
+ onlyFiles: true,
125
+ ignore: ignorePatterns,
126
+ followSymbolicLinks: false
127
+ });
128
+ ignoreFiles.sort((a, b) => depth(a) - depth(b) || a.localeCompare(b));
129
+ for (const rel of ignoreFiles) {
130
+ const content = await readFile(path.join(rootAbs, rel), "utf8");
131
+ const dir = path.posix.dirname(rel);
132
+ const prefix = dir === "." ? "" : `${dir}/`;
133
+ const patterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((pattern) => toRootPattern(pattern, prefix)).filter((pattern) => Boolean(pattern));
134
+ matcher.add(patterns);
135
+ }
136
+ return matcher;
137
+ }
138
+ function toRootPattern(rawPattern, prefix) {
139
+ const negated = rawPattern.startsWith("!");
140
+ let pattern = negated ? rawPattern.slice(1) : rawPattern;
141
+ if (!pattern) return null;
142
+ const rooted = pattern.startsWith("/");
143
+ if (rooted) pattern = pattern.slice(1);
144
+ if (!pattern) return null;
145
+ const withTree = pattern.endsWith("/") ? `${pattern}**` : pattern;
146
+ const fullPattern = rooted || withTree.includes("/") ? `${prefix}${withTree}` : `${prefix}**/${withTree}`;
147
+ return negated ? `!${fullPattern}` : fullPattern;
148
+ }
149
+ function depth(relPath) {
150
+ return relPath.split("/").length;
151
+ }
152
+ function toRelative(filePath, root) {
153
+ const rel = path.relative(root, filePath).replace(/\\/g, "/");
154
+ if (!rel || rel.startsWith("..")) throw new Error(`Entry file is not inside ${root}`);
155
+ return rel;
156
+ }
157
+
158
+ //#endregion
159
+ //#region src/lib/http.ts
160
+ function authHeaders(config) {
161
+ if (!config.token) throw new Error("Missing API token; run `flora login --token <token>`");
162
+ return { authorization: `Bearer ${config.token}` };
163
+ }
164
+ async function expectOk(promise) {
165
+ const { data, error, response } = await promise;
166
+ if (!response.ok) {
167
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") throw new Error(error.message);
168
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
169
+ }
170
+ return data;
171
+ }
172
+
173
+ //#endregion
174
+ //#region src/lib/logger.ts
175
+ const logger = createConsola({ formatOptions: {
176
+ colors: true,
177
+ date: false,
178
+ compact: false
179
+ } });
180
+
181
+ //#endregion
182
+ //#region src/lib/prompts.ts
183
+ async function promptIfMissing(value, message) {
184
+ if (value) return value;
185
+ if (!process.stdout.isTTY) throw new Error(`missing required value: ${message}`);
186
+ const result = await text({ message });
187
+ if (isCancel(result)) {
188
+ cancel("Canceled");
189
+ process.exit(1);
190
+ }
191
+ const next = String(result).trim();
192
+ if (!next) throw new Error(`missing required value: ${message}`);
193
+ return next;
194
+ }
195
+
196
+ //#endregion
197
+ //#region src/lib/zip.ts
198
+ const MAX_FILE_COUNT = 1e3;
199
+ const INCLUDE_PATTERNS = ["src/**"];
200
+ const INCLUDE_FILES = ["package.json", "flora.config.ts"];
201
+ async function zipProject(root) {
202
+ const rootAbs = path.resolve(root);
203
+ const sourceFiles = await glob(INCLUDE_PATTERNS, {
204
+ cwd: rootAbs,
205
+ onlyFiles: true,
206
+ followSymbolicLinks: false,
207
+ ignore: ["node_modules/**", ".git/**"]
208
+ });
209
+ const topLevelFiles = [];
210
+ for (const file of INCLUDE_FILES) if ((await stat(path.join(rootAbs, file)).catch(() => null))?.isFile()) topLevelFiles.push(file);
211
+ const allFiles = [...sourceFiles, ...topLevelFiles];
212
+ if (allFiles.length === 0) throw new Error(`No files found under ${rootAbs}`);
213
+ if (allFiles.length > MAX_FILE_COUNT) throw new Error(`Project has ${allFiles.length} files, exceeding limit of ${MAX_FILE_COUNT}`);
214
+ const entries = {};
215
+ for (const rel of allFiles) {
216
+ const contents = await readFile(path.join(rootAbs, rel));
217
+ entries[rel.replace(/\\/g, "/")] = new Uint8Array(contents);
218
+ }
219
+ return {
220
+ zip: zipSync(entries),
221
+ fileCount: allFiles.length
222
+ };
223
+ }
224
+
225
+ //#endregion
226
+ //#region src/commands/deployments.ts
227
+ const BUILD_SSE_TIMEOUT = 6e4;
228
+ async function deploy(config, guildArg, entryArg, root) {
229
+ const guild = await promptIfMissing(guildArg, "Guild ID");
230
+ const projectConfig = await loadProjectConfig();
231
+ const entry = entryArg ?? projectConfig.entry ?? "src/main.ts";
232
+ const projectRoot = root ?? projectConfig.root ?? ".";
233
+ const projectRootAbs = path.resolve(projectRoot);
234
+ const entryRel = toRelative(path.resolve(projectRootAbs, entry), projectRootAbs);
235
+ logger.info("Uploading project...");
236
+ const { zip, fileCount } = await zipProject(projectRoot);
237
+ const zipSize = formatBytes(zip.byteLength);
238
+ logger.success(`Upload complete (${fileCount} files, ${zipSize})`);
239
+ const formData = new FormData();
240
+ formData.append("guild_id", guild);
241
+ formData.append("entry", entryRel);
242
+ formData.append("project_zip", new Blob([zip]), "project.zip");
243
+ const baseUrl = config.apiUrl;
244
+ const headers = authHeaders(config);
245
+ const createRes = await fetch(`${baseUrl}/builds`, {
246
+ method: "POST",
247
+ headers,
248
+ body: formData
249
+ });
250
+ if (!createRes.ok) {
251
+ const body = await createRes.text().catch(() => "");
252
+ throw new Error(`Build creation failed (${createRes.status}): ${body}`);
253
+ }
254
+ const { build_id } = await createRes.json();
255
+ logger.info(`Building... (${build_id})`);
256
+ if (!await streamBuildLogs(`${baseUrl}/builds/${build_id}/logs`, headers, BUILD_SSE_TIMEOUT)) {
257
+ logger.warn(`Build still running: ${build_id}`);
258
+ logger.info(`Run ${colors.cyan("flora builds tail")} ${colors.yellow(build_id)} to follow logs.`);
259
+ return;
260
+ }
261
+ const buildRes = await fetch(`${baseUrl}/builds/${build_id}`, { headers });
262
+ if (!buildRes.ok) throw new Error(`Failed to fetch build result: ${buildRes.status}`);
263
+ const build = await buildRes.json();
264
+ if (build.status === "failed") throw new Error(`Build failed: ${build.error ?? "unknown error"}`);
265
+ if (build.status !== "done") throw new Error(`Build not finished: ${build.status}`);
266
+ if (!build.artifact?.bundle) throw new Error("Build produced no artifact bundle");
267
+ const files = await collectFiles(projectRootAbs);
268
+ const deployRes = await fetch(`${baseUrl}/deployments/${build.guild_id}`, {
269
+ method: "POST",
270
+ headers: {
271
+ ...headers,
272
+ "content-type": "application/json",
273
+ "x-flora-deploy-source": "cli"
274
+ },
275
+ body: JSON.stringify({
276
+ entry: build.entry,
277
+ files,
278
+ bundle: build.artifact.bundle,
279
+ source_map: build.artifact.source_map ? {
280
+ path: "bundle.js.map",
281
+ contents: build.artifact.source_map
282
+ } : void 0
283
+ })
284
+ });
285
+ if (!deployRes.ok) {
286
+ const body = await deployRes.text().catch(() => "");
287
+ throw new Error(`Deployment apply failed (${deployRes.status}): ${body}`);
288
+ }
289
+ logger.success(`Deployed guild ${build.guild_id}`);
290
+ logger.info(`${colors.cyan("•")} ${colors.cyan("entry:")} ${build.entry}`);
291
+ logger.info(`${colors.gray("•")} ${colors.gray("updated:")} ${(/* @__PURE__ */ new Date()).toISOString()}`);
292
+ }
293
+ async function streamBuildLogs(url, headers, timeout) {
294
+ const controller = new AbortController();
295
+ const timer = setTimeout(() => controller.abort(), timeout);
296
+ try {
297
+ const res = await fetch(url, {
298
+ headers,
299
+ signal: controller.signal
300
+ });
301
+ if (!res.ok || !res.body) return false;
302
+ const reader = res.body.getReader();
303
+ const decoder = new TextDecoder();
304
+ let buffer = "";
305
+ while (true) {
306
+ const { value, done } = await reader.read();
307
+ if (done) break;
308
+ buffer += decoder.decode(value, { stream: true });
309
+ for (;;) {
310
+ const eventEnd = buffer.indexOf("\n\n");
311
+ if (eventEnd < 0) break;
312
+ const event = buffer.slice(0, eventEnd);
313
+ buffer = buffer.slice(eventEnd + 2);
314
+ for (const line of event.split("\n")) {
315
+ if (line.startsWith("event: done")) return true;
316
+ if (line.startsWith("data: ")) {
317
+ const data = line.slice(6);
318
+ logger.log(` ${colors.cyan("↳")} ${colors.dim(data)}`);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return true;
324
+ } catch (err) {
325
+ if (err instanceof DOMException && err.name === "AbortError") return false;
326
+ throw err;
327
+ } finally {
328
+ clearTimeout(timer);
329
+ }
330
+ }
331
+ function formatBytes(bytes) {
332
+ if (bytes < 1024) return `${bytes}B`;
333
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}kb`;
334
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
335
+ }
336
+ async function get(config, guildArg) {
337
+ const guild = await promptIfMissing(guildArg, "Guild ID");
338
+ const deployment = await expectOk(createApiClient(config).GET("/deployments/{guild_id}", {
339
+ params: { path: { guild_id: guild } },
340
+ headers: authHeaders(config)
341
+ }));
342
+ logger.log(`Guild ${deployment.guild_id}\n entry: ${deployment.entry}\n created: ${deployment.created_at}\n updated: ${deployment.updated_at}`);
343
+ }
344
+ async function list(_config) {
345
+ logger.warn("`flora deployments list` was removed; use `flora deployments get --guild <guild_id>`");
346
+ }
347
+ async function health(config) {
348
+ const response = await expectOk(createApiClient(config).GET("/health/", {
349
+ headers: authHeaders(config),
350
+ parseAs: "text"
351
+ }));
352
+ logger.log(`${response}`);
353
+ }
354
+
355
+ //#endregion
356
+ //#region src/commands/kv.ts
357
+ async function createStore(config, guildArg, nameArg) {
358
+ const guild = await promptIfMissing(guildArg, "Guild ID");
359
+ const name = await promptIfMissing(nameArg, "Store name");
360
+ const response = await expectOk(createApiClient(config).POST("/kv/stores", {
361
+ headers: authHeaders(config),
362
+ body: {
363
+ guild_id: guild,
364
+ store_name: name
365
+ }
366
+ }));
367
+ logger.log(`Created KV store '${response.store.store_name}' for guild ${response.store.guild_id}`);
368
+ }
369
+ async function listStores(config, guildArg) {
370
+ const guild = await promptIfMissing(guildArg, "Guild ID");
371
+ const stores = await expectOk(createApiClient(config).GET("/kv/stores", {
372
+ headers: authHeaders(config),
373
+ params: { query: { guild_id: guild } }
374
+ }));
375
+ if (stores.length === 0) {
376
+ logger.log(`No KV stores found for guild ${guild}`);
377
+ return;
378
+ }
379
+ logger.log(`KV stores for guild ${guild}:`);
380
+ for (const store of stores) logger.log(` - ${store.store_name}`);
381
+ }
382
+ async function deleteStore(config, guildArg, nameArg) {
383
+ const guild = await promptIfMissing(guildArg, "Guild ID");
384
+ const name = await promptIfMissing(nameArg, "Store name");
385
+ await expectOk(createApiClient(config).DELETE("/kv/stores/{guild_id}/{store_name}", {
386
+ headers: authHeaders(config),
387
+ params: { path: {
388
+ guild_id: guild,
389
+ store_name: name
390
+ } }
391
+ }));
392
+ logger.log(`Deleted KV store '${name}' for guild ${guild}`);
393
+ }
394
+ async function setValue(config, guildArg, storeArg, keyArg, valueArg, expiration, metadata) {
395
+ const guild = await promptIfMissing(guildArg, "Guild ID");
396
+ const store = await promptIfMissing(storeArg, "Store name");
397
+ const key = await promptIfMissing(keyArg, "Key");
398
+ const value = await promptIfMissing(valueArg, "Value");
399
+ const metadataValue = metadata ? JSON.parse(metadata) : void 0;
400
+ await expectOk(createApiClient(config).PUT("/kv/{guild_id}/{store_name}/{key}", {
401
+ headers: authHeaders(config),
402
+ params: { path: {
403
+ guild_id: guild,
404
+ store_name: store,
405
+ key
406
+ } },
407
+ body: {
408
+ value,
409
+ expiration,
410
+ metadata: metadataValue
411
+ }
412
+ }));
413
+ logger.log(`Set ${key}=${value} in store '${store}' for guild ${guild}`);
414
+ }
415
+ async function getValue(config, guildArg, storeArg, keyArg) {
416
+ const guild = await promptIfMissing(guildArg, "Guild ID");
417
+ const store = await promptIfMissing(storeArg, "Store name");
418
+ const key = await promptIfMissing(keyArg, "Key");
419
+ const response = await expectOk(createApiClient(config).GET("/kv/{guild_id}/{store_name}/{key}", {
420
+ headers: authHeaders(config),
421
+ params: { path: {
422
+ guild_id: guild,
423
+ store_name: store,
424
+ key
425
+ } }
426
+ }));
427
+ if (response.value == null) {
428
+ logger.log(`Key '${key}' not found`);
429
+ return;
430
+ }
431
+ logger.log(`${response.value}`);
432
+ }
433
+ async function deleteValue(config, guildArg, storeArg, keyArg) {
434
+ const guild = await promptIfMissing(guildArg, "Guild ID");
435
+ const store = await promptIfMissing(storeArg, "Store name");
436
+ const key = await promptIfMissing(keyArg, "Key");
437
+ await expectOk(createApiClient(config).DELETE("/kv/{guild_id}/{store_name}/{key}", {
438
+ headers: authHeaders(config),
439
+ params: { path: {
440
+ guild_id: guild,
441
+ store_name: store,
442
+ key
443
+ } }
444
+ }));
445
+ logger.log(`Deleted key '${key}' from store '${store}' for guild ${guild}`);
446
+ }
447
+ async function listKeys(config, guildArg, storeArg, prefix, limit, cursor) {
448
+ const guild = await promptIfMissing(guildArg, "Guild ID");
449
+ const store = await promptIfMissing(storeArg, "Store name");
450
+ const response = await expectOk(createApiClient(config).GET("/kv/{guild_id}/{store_name}", {
451
+ headers: authHeaders(config),
452
+ params: {
453
+ path: {
454
+ guild_id: guild,
455
+ store_name: store
456
+ },
457
+ query: {
458
+ prefix,
459
+ limit,
460
+ cursor
461
+ }
462
+ }
463
+ }));
464
+ if (response.keys.length === 0) {
465
+ logger.log(`No keys found in store '${store}'`);
466
+ return;
467
+ }
468
+ logger.log(`Keys in store '${store}' (${response.keys.length} shown):`);
469
+ for (const key of response.keys) {
470
+ const expires = key.expiration ? ` (expires: ${key.expiration})` : "";
471
+ const meta = key.metadata ? ` [metadata: ${JSON.stringify(key.metadata)}]` : "";
472
+ logger.log(` - ${key.name}${expires}${meta}`);
473
+ }
474
+ if (!("list_complete" in response ? response.list_complete : response.listComplete) && response.cursor) logger.log(`More keys available. Use --cursor ${response.cursor}`);
475
+ }
476
+
477
+ //#endregion
478
+ //#region src/commands/login.ts
479
+ async function login(tokenArg) {
480
+ const token = await promptIfMissing(tokenArg, "API token");
481
+ saveConfig({
482
+ ...loadConfig$1(),
483
+ token
484
+ });
485
+ logger.log("Saved token to config");
486
+ }
487
+
488
+ //#endregion
489
+ //#region src/lib/logs.ts
490
+ function printLogEntry(entry) {
491
+ const dt = new Date(entry.timestamp);
492
+ const timestamp = Number.isNaN(dt.getTime()) ? String(entry.timestamp) : dt.toISOString().replace("T", " ").replace("Z", "");
493
+ const level = colorLevel(entry.level);
494
+ const guild = entry.guild_id ?? "-";
495
+ logger.log(`${timestamp} ${level} [${guild}] ${entry.target}: ${entry.message}`);
496
+ }
497
+ function colorLevel(level) {
498
+ switch (level) {
499
+ case "error": return "\x1B[31mERROR\x1B[0m";
500
+ case "warn": return "\x1B[33mWARN\x1B[0m";
501
+ case "info": return "\x1B[32mINFO\x1B[0m";
502
+ case "debug": return "\x1B[34mDEBUG\x1B[0m";
503
+ case "trace": return "\x1B[90mTRACE\x1B[0m";
504
+ default: return level;
505
+ }
506
+ }
507
+ async function streamSseLogs(response, onLog) {
508
+ if (!response.body) throw new Error("SSE stream missing response body");
509
+ const reader = response.body.getReader();
510
+ const decoder = new TextDecoder();
511
+ let buffer = "";
512
+ while (true) {
513
+ const { value, done } = await reader.read();
514
+ if (done) break;
515
+ buffer += decoder.decode(value, { stream: true });
516
+ for (;;) {
517
+ const eventEnd = buffer.indexOf("\n\n");
518
+ if (eventEnd < 0) break;
519
+ const event = buffer.slice(0, eventEnd);
520
+ buffer = buffer.slice(eventEnd + 2);
521
+ for (const line of event.split("\n")) {
522
+ if (!line.startsWith("data: ")) continue;
523
+ const raw = line.slice(6);
524
+ try {
525
+ onLog(JSON.parse(raw));
526
+ } catch {}
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ //#endregion
533
+ //#region src/commands/logs.ts
534
+ async function logs(config, guild, limit = 100) {
535
+ const client = createApiClient(config);
536
+ const entries = guild ? await expectOk(client.GET("/logs/{guild_id}", {
537
+ params: {
538
+ path: { guild_id: guild },
539
+ query: { limit }
540
+ },
541
+ headers: authHeaders(config)
542
+ })) : await expectOk(client.GET("/logs", {
543
+ params: { query: { limit } },
544
+ headers: authHeaders(config)
545
+ }));
546
+ if (entries.length === 0) {
547
+ logger.log("No logs found");
548
+ return;
549
+ }
550
+ for (const entry of entries) printLogEntry(entry);
551
+ }
552
+ async function streamLogs(config, guild) {
553
+ const headers = authHeaders(config);
554
+ const streamPath = guild ? `/logs/${guild}/stream` : "/logs/stream";
555
+ const response = await fetch(`${config.apiUrl}${streamPath}`, { headers });
556
+ if (!response.ok) throw new Error(`Stream request failed: ${response.status} ${response.statusText}`);
557
+ logger.log("Streaming logs... (press Ctrl+C to stop)");
558
+ await streamSseLogs(response, printLogEntry);
559
+ }
560
+
561
+ //#endregion
562
+ //#region src/index.ts
563
+ function positional(args, index) {
564
+ const value = (Array.isArray(args._) ? args._ : [])[index];
565
+ return typeof value === "string" ? value : void 0;
566
+ }
567
+ function resolveConfig(args) {
568
+ const config = loadConfig$1();
569
+ const argApiUrl = args["api"];
570
+ const apiUrl = (typeof argApiUrl === "string" ? argApiUrl : void 0) ?? process$1.env.FLORA_API_URL;
571
+ if (apiUrl) config.apiUrl = apiUrl;
572
+ return config;
573
+ }
574
+ const kvCommand = defineCommand({
575
+ meta: {
576
+ name: "kv",
577
+ description: "KV store management"
578
+ },
579
+ subCommands: {
580
+ "create-store": defineCommand({
581
+ args: {
582
+ api: {
583
+ type: "string",
584
+ required: false,
585
+ alias: "a"
586
+ },
587
+ guild: {
588
+ type: "string",
589
+ required: false
590
+ },
591
+ name: {
592
+ type: "string",
593
+ required: false
594
+ }
595
+ },
596
+ async run({ args }) {
597
+ await createStore(resolveConfig(args), args.guild, args.name);
598
+ }
599
+ }),
600
+ "list-stores": defineCommand({
601
+ args: {
602
+ api: {
603
+ type: "string",
604
+ required: false,
605
+ alias: "a"
606
+ },
607
+ guild: {
608
+ type: "string",
609
+ required: false
610
+ }
611
+ },
612
+ async run({ args }) {
613
+ await listStores(resolveConfig(args), args.guild);
614
+ }
615
+ }),
616
+ "delete-store": defineCommand({
617
+ args: {
618
+ api: {
619
+ type: "string",
620
+ required: false,
621
+ alias: "a"
622
+ },
623
+ guild: {
624
+ type: "string",
625
+ required: false
626
+ },
627
+ name: {
628
+ type: "string",
629
+ required: false
630
+ }
631
+ },
632
+ async run({ args }) {
633
+ await deleteStore(resolveConfig(args), args.guild, args.name);
634
+ }
635
+ }),
636
+ set: defineCommand({
637
+ args: {
638
+ api: {
639
+ type: "string",
640
+ required: false,
641
+ alias: "a"
642
+ },
643
+ guild: {
644
+ type: "string",
645
+ required: false
646
+ },
647
+ store: {
648
+ type: "string",
649
+ required: false
650
+ },
651
+ key: {
652
+ type: "string",
653
+ required: false
654
+ },
655
+ expiration: {
656
+ type: "string",
657
+ required: false
658
+ },
659
+ metadata: {
660
+ type: "string",
661
+ required: false
662
+ }
663
+ },
664
+ async run({ args }) {
665
+ const config = resolveConfig(args);
666
+ const value = positional(args, 0);
667
+ const expiration = args.expiration ? Number(args.expiration) : void 0;
668
+ await setValue(config, args.guild, args.store, args.key, value, expiration, args.metadata);
669
+ }
670
+ }),
671
+ get: defineCommand({
672
+ args: {
673
+ api: {
674
+ type: "string",
675
+ required: false,
676
+ alias: "a"
677
+ },
678
+ guild: {
679
+ type: "string",
680
+ required: false
681
+ },
682
+ store: {
683
+ type: "string",
684
+ required: false
685
+ }
686
+ },
687
+ async run({ args }) {
688
+ const config = resolveConfig(args);
689
+ const key = positional(args, 0);
690
+ await getValue(config, args.guild, args.store, key);
691
+ }
692
+ }),
693
+ delete: defineCommand({
694
+ args: {
695
+ api: {
696
+ type: "string",
697
+ required: false,
698
+ alias: "a"
699
+ },
700
+ guild: {
701
+ type: "string",
702
+ required: false
703
+ },
704
+ store: {
705
+ type: "string",
706
+ required: false
707
+ }
708
+ },
709
+ async run({ args }) {
710
+ const config = resolveConfig(args);
711
+ const key = positional(args, 0);
712
+ await deleteValue(config, args.guild, args.store, key);
713
+ }
714
+ }),
715
+ "list-keys": defineCommand({
716
+ args: {
717
+ api: {
718
+ type: "string",
719
+ required: false,
720
+ alias: "a"
721
+ },
722
+ guild: {
723
+ type: "string",
724
+ required: false
725
+ },
726
+ store: {
727
+ type: "string",
728
+ required: false
729
+ },
730
+ prefix: {
731
+ type: "string",
732
+ required: false
733
+ },
734
+ limit: {
735
+ type: "string",
736
+ required: false
737
+ },
738
+ cursor: {
739
+ type: "string",
740
+ required: false
741
+ }
742
+ },
743
+ async run({ args }) {
744
+ await listKeys(resolveConfig(args), args.guild, args.store, args.prefix, args.limit ? Number(args.limit) : void 0, args.cursor);
745
+ }
746
+ })
747
+ }
748
+ });
749
+ runMain(defineCommand({
750
+ meta: {
751
+ name,
752
+ description,
753
+ version
754
+ },
755
+ args: { api: {
756
+ type: "string",
757
+ required: false,
758
+ alias: "a"
759
+ } },
760
+ subCommands: {
761
+ deploy: defineCommand({
762
+ args: {
763
+ api: {
764
+ type: "string",
765
+ required: false,
766
+ alias: "a"
767
+ },
768
+ guild: {
769
+ type: "string",
770
+ required: false
771
+ },
772
+ root: {
773
+ type: "string",
774
+ required: false
775
+ }
776
+ },
777
+ async run({ args }) {
778
+ const config = resolveConfig(args);
779
+ const entry = positional(args, 0);
780
+ await deploy(config, args.guild, entry, args.root);
781
+ }
782
+ }),
783
+ get: defineCommand({
784
+ args: {
785
+ api: {
786
+ type: "string",
787
+ required: false,
788
+ alias: "a"
789
+ },
790
+ guild: {
791
+ type: "string",
792
+ required: false
793
+ }
794
+ },
795
+ async run({ args }) {
796
+ await get(resolveConfig(args), args.guild);
797
+ }
798
+ }),
799
+ list: defineCommand({
800
+ args: { api: {
801
+ type: "string",
802
+ required: false,
803
+ alias: "a"
804
+ } },
805
+ async run({ args }) {
806
+ await list(resolveConfig(args));
807
+ }
808
+ }),
809
+ health: defineCommand({
810
+ args: { api: {
811
+ type: "string",
812
+ required: false,
813
+ alias: "a"
814
+ } },
815
+ async run({ args }) {
816
+ await health(resolveConfig(args));
817
+ }
818
+ }),
819
+ login: defineCommand({
820
+ args: { token: {
821
+ type: "string",
822
+ required: false
823
+ } },
824
+ async run({ args }) {
825
+ const positionalToken = positional(args, 0);
826
+ await login(args.token ?? positionalToken);
827
+ }
828
+ }),
829
+ logs: defineCommand({
830
+ args: {
831
+ api: {
832
+ type: "string",
833
+ required: false,
834
+ alias: "a"
835
+ },
836
+ guild: {
837
+ type: "string",
838
+ required: false
839
+ },
840
+ follow: {
841
+ type: "boolean",
842
+ required: false,
843
+ alias: "f"
844
+ },
845
+ limit: {
846
+ type: "string",
847
+ required: false,
848
+ alias: "n"
849
+ }
850
+ },
851
+ async run({ args }) {
852
+ const config = resolveConfig(args);
853
+ const follow = Boolean(args.follow);
854
+ const limit = args.limit ? Number(args.limit) : 100;
855
+ if (follow) {
856
+ await streamLogs(config, args.guild);
857
+ return;
858
+ }
859
+ await logs(config, args.guild, limit);
860
+ }
861
+ }),
862
+ kv: kvCommand
863
+ }
864
+ })).catch((error) => {
865
+ const message = error instanceof Error ? error.message : String(error);
866
+ logger.error(message);
867
+ process$1.exit(1);
868
+ });
869
+
870
+ //#endregion
871
+ export { };
package/package.json CHANGED
@@ -1,14 +1,51 @@
1
1
  {
2
2
  "name": "@uwu/flora-cli",
3
- "version": "0.0.0",
4
- "description": "Placeholder for @uwu/flora-cli",
5
- "main": "index.js",
6
- "scripts": {},
7
- "keywords": [],
8
- "author": "taskylizard (https://www.npmjs.com/~taskylizard)",
9
- "license": "UNLICENSED",
3
+ "version": "0.1.0",
10
4
  "private": false,
5
+ "description": "flora command line interface",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/uwu/flora",
9
+ "directory": "packages/cli"
10
+ },
11
+ "bin": {
12
+ "flora": "./dist/index.mjs"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "package.json"
18
+ ],
19
+ "type": "module",
11
20
  "publishConfig": {
12
21
  "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "vp pack",
25
+ "dev": "vp pack --watch",
26
+ "test": "vp test",
27
+ "typecheck": "tsgo --noEmit",
28
+ "release": "bumpp",
29
+ "prepublishOnly": "pnpm run build"
30
+ },
31
+ "dependencies": {
32
+ "@clack/prompts": "^1.0.1",
33
+ "@uwu/flora-api-client": "workspace:*",
34
+ "c12": "^3.3.2",
35
+ "citty": "^0.2.1",
36
+ "conf": "^15.1.0",
37
+ "consola": "^3.4.2",
38
+ "fflate": "^0.8.2",
39
+ "ignore": "^7.0.5",
40
+ "rolldown": "^0.15.1",
41
+ "tinyglobby": "^0.2.15"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "catalog:",
45
+ "@typescript/native-preview": "catalog:",
46
+ "bumpp": "^11.0.0",
47
+ "typescript": "catalog:",
48
+ "vite-plus": "catalog:",
49
+ "vitest": "catalog:"
13
50
  }
14
- }
51
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // Placeholder