@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 +15 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +871 -0
- package/package.json +45 -8
- package/index.js +0 -1
package/README.md
ADDED
package/dist/index.d.mts
ADDED
|
@@ -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.
|
|
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
|