@tinycloud/cli 0.0.0-beta-20260401001229

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2793 @@
1
+ // src/index.ts
2
+ import { readFileSync as readFileSync2 } from "fs";
3
+ import { Command } from "commander";
4
+
5
+ // src/config/constants.ts
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ var CONFIG_DIR = join(homedir(), ".tinycloud");
9
+ var PROFILES_DIR = join(CONFIG_DIR, "profiles");
10
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
11
+ var DEFAULT_HOST = "https://node.tinycloud.xyz";
12
+ var DEFAULT_PROFILE = "default";
13
+ var DEFAULT_CHAIN_ID = 1;
14
+ var ExitCode = {
15
+ SUCCESS: 0,
16
+ ERROR: 1,
17
+ USAGE_ERROR: 2,
18
+ AUTH_REQUIRED: 3,
19
+ NOT_FOUND: 4,
20
+ PERMISSION_DENIED: 5,
21
+ NETWORK_ERROR: 6,
22
+ NODE_ERROR: 7
23
+ };
24
+
25
+ // src/output/formatter.ts
26
+ import ora from "ora";
27
+
28
+ // src/output/theme.ts
29
+ import chalk from "chalk";
30
+ var TC_PALETTE = {
31
+ primary: "#4473b9",
32
+ accent: "#5b9bd5",
33
+ success: "#2fba6a",
34
+ warn: "#e8a838",
35
+ error: "#d94040",
36
+ muted: "#808080",
37
+ dim: "#5a5a5a"
38
+ };
39
+ var theme = {
40
+ primary: chalk.hex(TC_PALETTE.primary),
41
+ accent: chalk.hex(TC_PALETTE.accent),
42
+ success: chalk.hex(TC_PALETTE.success),
43
+ warn: chalk.hex(TC_PALETTE.warn),
44
+ error: chalk.hex(TC_PALETTE.error),
45
+ muted: chalk.hex(TC_PALETTE.muted),
46
+ dim: chalk.hex(TC_PALETTE.dim),
47
+ heading: chalk.bold.hex(TC_PALETTE.primary),
48
+ command: chalk.hex(TC_PALETTE.accent),
49
+ brand: chalk.bold.hex(TC_PALETTE.primary),
50
+ label: chalk.bold,
51
+ value: chalk.white,
52
+ hint: chalk.italic.hex(TC_PALETTE.muted)
53
+ };
54
+
55
+ // src/output/formatter.ts
56
+ function outputJson(data) {
57
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
58
+ }
59
+ function outputError(code, message) {
60
+ if (isInteractive()) {
61
+ process.stderr.write(
62
+ `${theme.error("\u2717")} ${theme.label(code)}: ${message}
63
+ `
64
+ );
65
+ } else {
66
+ process.stderr.write(
67
+ JSON.stringify({ error: { code, message } }, null, 2) + "\n"
68
+ );
69
+ }
70
+ }
71
+ function isInteractive() {
72
+ return Boolean(process.stdout.isTTY);
73
+ }
74
+ async function withSpinner(label, fn) {
75
+ if (!isInteractive()) {
76
+ return fn();
77
+ }
78
+ const spinner = ora(label).start();
79
+ try {
80
+ const result = await fn();
81
+ spinner.succeed(label);
82
+ return result;
83
+ } catch (error) {
84
+ spinner.fail(label);
85
+ throw error;
86
+ }
87
+ }
88
+ function shouldOutputJson() {
89
+ return !isInteractive() || process.argv.includes("--json");
90
+ }
91
+ function formatField(label, value) {
92
+ if (value === null || value === void 0) return ` ${theme.label(label + ":")} ${theme.muted("\u2014")}`;
93
+ if (typeof value === "boolean") {
94
+ return ` ${theme.label(label + ":")} ${value ? theme.success("yes") : theme.muted("no")}`;
95
+ }
96
+ return ` ${theme.label(label + ":")} ${theme.value(String(value))}`;
97
+ }
98
+ function formatTable(headers, rows) {
99
+ const widths = headers.map(
100
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length))
101
+ );
102
+ const headerLine = headers.map((h, i) => theme.label(h.padEnd(widths[i]))).join(" ");
103
+ const separator = widths.map((w) => theme.dim("\u2500".repeat(w))).join(" ");
104
+ const dataLines = rows.map(
105
+ (row) => row.map((cell, i) => (cell || "").padEnd(widths[i])).join(" ")
106
+ );
107
+ return [headerLine, separator, ...dataLines].join("\n");
108
+ }
109
+ function formatCheck(ok, label, detail) {
110
+ const icon = ok === "warn" ? theme.warn("\u26A0") : ok ? theme.success("\u2713") : theme.error("\u2717");
111
+ const detailStr = detail ? ` ${theme.muted(`(${detail})`)}` : "";
112
+ return `${icon} ${label}${detailStr}`;
113
+ }
114
+ function formatSection(title) {
115
+ return `
116
+ ${theme.heading(title)}`;
117
+ }
118
+ function formatBytes(bytes) {
119
+ if (bytes < 1024) return `${bytes} B`;
120
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
121
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
122
+ }
123
+ function formatTimeAgo(date) {
124
+ const d = typeof date === "string" ? new Date(date) : date;
125
+ const seconds = Math.floor((Date.now() - d.getTime()) / 1e3);
126
+ if (seconds < 60) return "just now";
127
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
128
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
129
+ return `${Math.floor(seconds / 86400)}d ago`;
130
+ }
131
+
132
+ // src/output/errors.ts
133
+ var CLIError = class extends Error {
134
+ constructor(code, message, exitCode = ExitCode.ERROR) {
135
+ super(message);
136
+ this.code = code;
137
+ this.exitCode = exitCode;
138
+ this.name = "CLIError";
139
+ }
140
+ };
141
+ function wrapError(error) {
142
+ if (error instanceof CLIError) return error;
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
145
+ return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
146
+ }
147
+ if (message.includes("NOT_FOUND") || message.includes("KV_NOT_FOUND")) {
148
+ return new CLIError("NOT_FOUND", message, ExitCode.NOT_FOUND);
149
+ }
150
+ if (message.includes("PERMISSION_DENIED")) {
151
+ return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
152
+ }
153
+ if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
154
+ return new CLIError("NETWORK_ERROR", message, ExitCode.NETWORK_ERROR);
155
+ }
156
+ return new CLIError("ERROR", message, ExitCode.ERROR);
157
+ }
158
+ function handleError(error) {
159
+ const cliError = wrapError(error);
160
+ outputError(cliError.code, cliError.message);
161
+ process.exit(cliError.exitCode);
162
+ }
163
+
164
+ // src/output/taglines.ts
165
+ var HOLIDAY_TAGLINES = [
166
+ { month: 1, day: 1, range: 1, tagline: "New year, new keys, same cloud." },
167
+ { month: 2, day: 14, tagline: "We love your data as much as you do." },
168
+ { month: 3, day: 14, tagline: "3.14159 reasons to encrypt everything." },
169
+ { month: 5, day: 4, tagline: "May the fourth be with your keys." },
170
+ { month: 10, day: 31, tagline: "Nothing scarier than plaintext secrets." },
171
+ { month: 12, day: 25, range: 2, tagline: "Unwrap your data, not your keys." },
172
+ { month: 12, day: 31, tagline: "Encrypt your resolutions." }
173
+ ];
174
+ var TAGLINES = [
175
+ // Professional
176
+ "Your data, your keys, your cloud.",
177
+ "Self-sovereign storage for the modern web.",
178
+ "The cloud you actually own.",
179
+ "Encrypted by default, decentralized by design.",
180
+ "Where your data answers only to you.",
181
+ "End-to-end encrypted. No exceptions.",
182
+ "Like S3 but you hold the keys.",
183
+ "Privacy isn't a feature. It's the architecture.",
184
+ "Sovereign storage, zero knowledge.",
185
+ "Your .env is safe here \u2014 we use real cryptography.",
186
+ // Playful / nerdy
187
+ "UCAN do anything.",
188
+ "Keys generated, delegations granted, data liberated.",
189
+ "Decentralized storage, centralized vibes.",
190
+ "Trust nobody, delegate everything.",
191
+ "sudo make me a sandwich, encrypted.",
192
+ "Have you tried turning your keys off and on again?",
193
+ "All your base are belong to you.",
194
+ "In UCAN we trust.",
195
+ "0 knowledge, 100% confidence.",
196
+ "Keeping secrets since 2024."
197
+ ];
198
+ function getHolidayTagline() {
199
+ const now = /* @__PURE__ */ new Date();
200
+ const month = now.getMonth() + 1;
201
+ const day = now.getDate();
202
+ for (const h of HOLIDAY_TAGLINES) {
203
+ const range = h.range ?? 0;
204
+ if (h.month === month && Math.abs(day - h.day) <= range) {
205
+ return h.tagline;
206
+ }
207
+ }
208
+ return null;
209
+ }
210
+ function pickTagline() {
211
+ const holiday = getHolidayTagline();
212
+ if (holiday) return holiday;
213
+ return TAGLINES[Math.floor(Math.random() * TAGLINES.length)];
214
+ }
215
+
216
+ // src/output/banner.ts
217
+ import { execSync } from "child_process";
218
+ var bannerEmitted = false;
219
+ function resolveCommitHash() {
220
+ try {
221
+ return execSync("git rev-parse --short HEAD", {
222
+ encoding: "utf-8",
223
+ stdio: ["pipe", "pipe", "pipe"]
224
+ }).trim() || null;
225
+ } catch {
226
+ return null;
227
+ }
228
+ }
229
+ function formatBannerLine(version2) {
230
+ const commit = resolveCommitHash();
231
+ const tagline = pickTagline();
232
+ const versionPart = `tc v${version2}`;
233
+ const commitPart = commit ? ` (${commit})` : "";
234
+ const separator = " \u2014 ";
235
+ if (!isInteractive()) {
236
+ return `${versionPart}${commitPart}${separator}${tagline}`;
237
+ }
238
+ return [
239
+ theme.brand("\u2601\uFE0F tc"),
240
+ " ",
241
+ theme.muted(`v${version2}`),
242
+ commit ? theme.dim(` (${commit})`) : "",
243
+ theme.dim(separator),
244
+ theme.primary(tagline)
245
+ ].join("");
246
+ }
247
+ function emitBanner(version2) {
248
+ if (bannerEmitted) return;
249
+ if (!isInteractive()) return;
250
+ if (process.env.TC_HIDE_BANNER === "1") return;
251
+ bannerEmitted = true;
252
+ process.stderr.write(formatBannerLine(version2) + "\n\n");
253
+ }
254
+
255
+ // src/config/profiles.ts
256
+ import { join as join2 } from "path";
257
+ import { rm as rm2 } from "fs/promises";
258
+
259
+ // src/config/storage.ts
260
+ import { readFile, writeFile, stat, mkdir, rm, readdir } from "fs/promises";
261
+ import { dirname } from "path";
262
+ async function readJson(filePath) {
263
+ try {
264
+ const data = await readFile(filePath, "utf-8");
265
+ return JSON.parse(data);
266
+ } catch (err) {
267
+ if (err.code === "ENOENT") {
268
+ return null;
269
+ }
270
+ throw err;
271
+ }
272
+ }
273
+ async function writeJson(filePath, data) {
274
+ await mkdir(dirname(filePath), { recursive: true });
275
+ await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
276
+ }
277
+ async function fileExists(filePath) {
278
+ try {
279
+ await stat(filePath);
280
+ return true;
281
+ } catch (err) {
282
+ if (err.code === "ENOENT") {
283
+ return false;
284
+ }
285
+ throw err;
286
+ }
287
+ }
288
+ async function ensureDir(dirPath) {
289
+ await mkdir(dirPath, { recursive: true });
290
+ }
291
+ async function removeDir(dirPath) {
292
+ await rm(dirPath, { recursive: true, force: true });
293
+ }
294
+ async function listDirs(dirPath) {
295
+ try {
296
+ const entries = await readdir(dirPath, { withFileTypes: true });
297
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
298
+ } catch (err) {
299
+ if (err.code === "ENOENT") {
300
+ return [];
301
+ }
302
+ throw err;
303
+ }
304
+ }
305
+
306
+ // src/config/profiles.ts
307
+ var ProfileManager = class _ProfileManager {
308
+ // ── Initialization ──────────────────────────────────────────────────
309
+ /**
310
+ * Creates ~/.tinycloud/ and ~/.tinycloud/profiles/ if they don't exist.
311
+ */
312
+ static async ensureConfigDir() {
313
+ await ensureDir(CONFIG_DIR);
314
+ await ensureDir(PROFILES_DIR);
315
+ }
316
+ // ── Global config ───────────────────────────────────────────────────
317
+ /**
318
+ * Reads config.json. Returns a default config if the file is missing.
319
+ */
320
+ static async getConfig() {
321
+ const config = await readJson(CONFIG_FILE);
322
+ if (!config) {
323
+ return { defaultProfile: DEFAULT_PROFILE, version: 1 };
324
+ }
325
+ return config;
326
+ }
327
+ /**
328
+ * Writes the global config to config.json.
329
+ */
330
+ static async setConfig(config) {
331
+ await _ProfileManager.ensureConfigDir();
332
+ await writeJson(CONFIG_FILE, config);
333
+ }
334
+ // ── Profile CRUD ────────────────────────────────────────────────────
335
+ /**
336
+ * Returns the profile config for the given name.
337
+ * Throws CLIError if the profile doesn't exist.
338
+ */
339
+ static async getProfile(name) {
340
+ const profilePath = join2(PROFILES_DIR, name, "profile.json");
341
+ const profile = await readJson(profilePath);
342
+ if (!profile) {
343
+ throw new CLIError(
344
+ "PROFILE_NOT_FOUND",
345
+ `Profile "${name}" does not exist. Run \`tc init\` or \`tc profile create ${name}\` first.`
346
+ );
347
+ }
348
+ return profile;
349
+ }
350
+ /**
351
+ * Saves a profile config, creating the profile directory if needed.
352
+ */
353
+ static async setProfile(name, data) {
354
+ const profileDir = join2(PROFILES_DIR, name);
355
+ await ensureDir(profileDir);
356
+ await writeJson(join2(profileDir, "profile.json"), data);
357
+ }
358
+ /**
359
+ * Returns true if a profile directory exists.
360
+ */
361
+ static async profileExists(name) {
362
+ return fileExists(join2(PROFILES_DIR, name, "profile.json"));
363
+ }
364
+ /**
365
+ * Returns an array of profile directory names.
366
+ */
367
+ static async listProfiles() {
368
+ return listDirs(PROFILES_DIR);
369
+ }
370
+ /**
371
+ * Deletes a profile directory.
372
+ * Throws if trying to delete the current default profile.
373
+ */
374
+ static async deleteProfile(name) {
375
+ const config = await _ProfileManager.getConfig();
376
+ if (config.defaultProfile === name) {
377
+ throw new CLIError(
378
+ "PROFILE_DELETE_DEFAULT",
379
+ `Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
380
+ );
381
+ }
382
+ const profileDir = join2(PROFILES_DIR, name);
383
+ await removeDir(profileDir);
384
+ }
385
+ // ── Key management ──────────────────────────────────────────────────
386
+ /**
387
+ * Returns the parsed JWK for a profile, or null if no key exists.
388
+ */
389
+ static async getKey(name) {
390
+ return readJson(join2(PROFILES_DIR, name, "key.json"));
391
+ }
392
+ /**
393
+ * Saves a JWK key for a profile.
394
+ */
395
+ static async setKey(name, jwk) {
396
+ const profileDir = join2(PROFILES_DIR, name);
397
+ await ensureDir(profileDir);
398
+ await writeJson(join2(profileDir, "key.json"), jwk);
399
+ }
400
+ // ── Session management ──────────────────────────────────────────────
401
+ /**
402
+ * Returns the parsed session for a profile, or null if none exists.
403
+ */
404
+ static async getSession(name) {
405
+ return readJson(join2(PROFILES_DIR, name, "session.json"));
406
+ }
407
+ /**
408
+ * Saves session data for a profile.
409
+ */
410
+ static async setSession(name, session) {
411
+ const profileDir = join2(PROFILES_DIR, name);
412
+ await ensureDir(profileDir);
413
+ await writeJson(join2(profileDir, "session.json"), session);
414
+ }
415
+ /**
416
+ * Removes the session file for a profile.
417
+ */
418
+ static async clearSession(name) {
419
+ const sessionPath = join2(PROFILES_DIR, name, "session.json");
420
+ try {
421
+ await rm2(sessionPath);
422
+ } catch (err) {
423
+ if (err.code !== "ENOENT") {
424
+ throw err;
425
+ }
426
+ }
427
+ }
428
+ // ── Cache management ────────────────────────────────────────────────
429
+ /**
430
+ * Returns the path to the profile's cache directory, creating it if needed.
431
+ */
432
+ static async getCacheDir(name) {
433
+ const cacheDir = join2(PROFILES_DIR, name, "cache");
434
+ await ensureDir(cacheDir);
435
+ return cacheDir;
436
+ }
437
+ // ── Resolution helpers ──────────────────────────────────────────────
438
+ /**
439
+ * Resolves the full CLI context from flags, env vars, and config.
440
+ *
441
+ * Profile resolution: options.profile > TC_PROFILE env > config.defaultProfile > "default"
442
+ * Host resolution: options.host > TC_HOST env > profile.host > DEFAULT_HOST
443
+ */
444
+ static async resolveContext(options) {
445
+ const config = await _ProfileManager.getConfig();
446
+ const profile = options.profile ?? process.env.TC_PROFILE ?? config.defaultProfile ?? DEFAULT_PROFILE;
447
+ let profileHost;
448
+ try {
449
+ const profileConfig = await _ProfileManager.getProfile(profile);
450
+ profileHost = profileConfig.host;
451
+ } catch {
452
+ }
453
+ const host = options.host ?? process.env.TC_HOST ?? profileHost ?? DEFAULT_HOST;
454
+ return {
455
+ profile,
456
+ host,
457
+ verbose: options.verbose ?? false,
458
+ noCache: options.noCache ?? false,
459
+ quiet: options.quiet ?? false
460
+ };
461
+ }
462
+ };
463
+
464
+ // src/auth/local-key.ts
465
+ import { TCWSessionManager, initPanicHook } from "@tinycloud/node-sdk-wasm";
466
+ import { PrivateKeySigner } from "@tinycloud/node-sdk";
467
+ import { randomBytes } from "crypto";
468
+ var wasmInitialized = false;
469
+ function ensureWasm() {
470
+ if (!wasmInitialized) {
471
+ initPanicHook();
472
+ wasmInitialized = true;
473
+ }
474
+ }
475
+ function generateKey() {
476
+ ensureWasm();
477
+ const mgr = new TCWSessionManager();
478
+ const keyId = mgr.createSessionKey("cli");
479
+ const jwkStr = mgr.jwk(keyId);
480
+ if (!jwkStr) throw new Error("Failed to generate key");
481
+ const jwk = JSON.parse(jwkStr);
482
+ const did = mgr.getDID(keyId);
483
+ return { jwk, did };
484
+ }
485
+ function generateEthereumPrivateKey() {
486
+ const keyBytes = randomBytes(32);
487
+ return "0x" + keyBytes.toString("hex");
488
+ }
489
+ async function deriveAddress(privateKey) {
490
+ const signer = new PrivateKeySigner(privateKey);
491
+ return signer.getAddress();
492
+ }
493
+ function addressToDID(address, chainId = 1) {
494
+ return `did:pkh:eip155:${chainId}:${address}`;
495
+ }
496
+ async function generateLocalIdentity(chainId = 1) {
497
+ const privateKey = generateEthereumPrivateKey();
498
+ const address = await deriveAddress(privateKey);
499
+ const did = addressToDID(address, chainId);
500
+ return { privateKey, address, did };
501
+ }
502
+ async function localKeySignIn(options) {
503
+ const { TinyCloudNode: TinyCloudNode2 } = await import("@tinycloud/node-sdk");
504
+ const node = new TinyCloudNode2({
505
+ privateKey: options.privateKey,
506
+ host: options.host,
507
+ autoCreateSpace: true
508
+ });
509
+ await node.signIn();
510
+ const address = await new PrivateKeySigner(options.privateKey).getAddress();
511
+ return {
512
+ spaceId: node.spaceId ?? "",
513
+ address,
514
+ chainId: 1
515
+ };
516
+ }
517
+
518
+ // src/auth/browser-auth.ts
519
+ import { createServer } from "http";
520
+ import { createInterface } from "readline";
521
+ var OPENKEY_BASE = "https://openkey.so";
522
+ async function startAuthFlow(did, options = {}) {
523
+ if (options.paste) {
524
+ return pasteFlow(did, options);
525
+ }
526
+ try {
527
+ return await callbackFlow(did, options);
528
+ } catch {
529
+ if (isInteractive()) {
530
+ console.error("Could not open browser. Falling back to manual paste mode.");
531
+ return pasteFlow(did, options);
532
+ }
533
+ throw new Error("Cannot open browser in non-interactive mode. Use --paste flag.");
534
+ }
535
+ }
536
+ function buildAuthUrl(did, options = {}) {
537
+ const params = new URLSearchParams();
538
+ params.set("did", did);
539
+ if (options.callback) {
540
+ params.set("callback", options.callback);
541
+ }
542
+ if (options.jwk) {
543
+ const jwkB64 = Buffer.from(JSON.stringify(options.jwk)).toString("base64url");
544
+ params.set("jwk", jwkB64);
545
+ }
546
+ if (options.host) {
547
+ params.set("host", options.host);
548
+ }
549
+ return `${OPENKEY_BASE}/delegate?${params.toString()}`;
550
+ }
551
+ async function callbackFlow(did, options = {}) {
552
+ return new Promise((resolve3, reject) => {
553
+ let timeout;
554
+ let settled = false;
555
+ let rl;
556
+ function settle(result) {
557
+ if (settled) return;
558
+ settled = true;
559
+ clearTimeout(timeout);
560
+ server.close();
561
+ if (rl) {
562
+ rl.close();
563
+ }
564
+ if (result.data) {
565
+ resolve3(result.data);
566
+ } else {
567
+ reject(result.error);
568
+ }
569
+ }
570
+ function parsePasteInput(input) {
571
+ const trimmed = input.trim();
572
+ try {
573
+ return JSON.parse(trimmed);
574
+ } catch {
575
+ const decoded = Buffer.from(trimmed, "base64").toString("utf-8");
576
+ return JSON.parse(decoded);
577
+ }
578
+ }
579
+ const server = createServer((req, res) => {
580
+ if (req.method === "POST" && req.url === "/callback") {
581
+ let body = "";
582
+ req.on("data", (chunk) => {
583
+ body += chunk.toString();
584
+ });
585
+ req.on("end", () => {
586
+ try {
587
+ const data = JSON.parse(body);
588
+ res.writeHead(200, {
589
+ "Content-Type": "application/json",
590
+ "Access-Control-Allow-Origin": "*"
591
+ });
592
+ res.end(JSON.stringify({ success: true }));
593
+ settle({ data });
594
+ } catch (err) {
595
+ res.writeHead(400, { "Content-Type": "application/json" });
596
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
597
+ settle({ error: new Error("Invalid delegation data received") });
598
+ }
599
+ });
600
+ } else if (req.method === "OPTIONS") {
601
+ res.writeHead(204, {
602
+ "Access-Control-Allow-Origin": "*",
603
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
604
+ "Access-Control-Allow-Headers": "Content-Type"
605
+ });
606
+ res.end();
607
+ } else {
608
+ res.writeHead(404);
609
+ res.end();
610
+ }
611
+ });
612
+ server.listen(0, "127.0.0.1", async () => {
613
+ const addr = server.address();
614
+ if (!addr || typeof addr === "string") {
615
+ settle({ error: new Error("Failed to start callback server") });
616
+ return;
617
+ }
618
+ const port = addr.port;
619
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
620
+ const authUrl = buildAuthUrl(did, { ...options, callback: callbackUrl });
621
+ if (isInteractive()) {
622
+ console.error(`Opening browser for authentication...`);
623
+ console.error(`If the browser doesn't open, visit: ${authUrl}`);
624
+ }
625
+ try {
626
+ const open = (await import("open")).default;
627
+ await open(authUrl);
628
+ } catch {
629
+ server.close();
630
+ throw new Error("Failed to open browser");
631
+ }
632
+ if (isInteractive()) {
633
+ console.error(`
634
+ If the browser can't connect back, paste the delegation code here:`);
635
+ rl = createInterface({
636
+ input: process.stdin,
637
+ output: process.stderr
638
+ });
639
+ rl.on("line", (input) => {
640
+ if (settled) return;
641
+ try {
642
+ const data = parsePasteInput(input);
643
+ settle({ data });
644
+ } catch {
645
+ console.error("Invalid delegation code. Expected JSON or base64-encoded JSON. Try again:");
646
+ }
647
+ });
648
+ }
649
+ });
650
+ timeout = setTimeout(() => {
651
+ settle({ error: new Error("Authentication timed out after 5 minutes") });
652
+ }, 5 * 60 * 1e3);
653
+ });
654
+ }
655
+ async function pasteFlow(did, options = {}) {
656
+ const authUrl = buildAuthUrl(did, options);
657
+ console.error(`
658
+ Open this URL in a browser to authenticate:
659
+ `);
660
+ console.error(` ${authUrl}
661
+ `);
662
+ const rl = createInterface({
663
+ input: process.stdin,
664
+ output: process.stderr
665
+ });
666
+ return new Promise((resolve3, reject) => {
667
+ rl.question("Paste delegation code: ", (input) => {
668
+ rl.close();
669
+ try {
670
+ const data = JSON.parse(input.trim());
671
+ resolve3(data);
672
+ } catch {
673
+ try {
674
+ const decoded = Buffer.from(input.trim(), "base64").toString("utf-8");
675
+ const data = JSON.parse(decoded);
676
+ resolve3(data);
677
+ } catch {
678
+ reject(new Error("Invalid delegation code. Expected JSON or base64-encoded JSON."));
679
+ }
680
+ }
681
+ });
682
+ });
683
+ }
684
+
685
+ // src/commands/init.ts
686
+ function registerInitCommand(program2) {
687
+ program2.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").action(async (options, cmd) => {
688
+ try {
689
+ const globalOpts = cmd.optsWithGlobals();
690
+ const profileName = options.name;
691
+ const host = options.host ?? globalOpts.host ?? DEFAULT_HOST;
692
+ if (await ProfileManager.profileExists(profileName)) {
693
+ throw new CLIError(
694
+ "PROFILE_EXISTS",
695
+ `Profile "${profileName}" already exists. Use \`tc profile delete ${profileName}\` first or choose a different name.`,
696
+ ExitCode.ERROR
697
+ );
698
+ }
699
+ await ProfileManager.ensureConfigDir();
700
+ const { jwk, did } = await withSpinner("Generating key...", async () => {
701
+ return generateKey();
702
+ });
703
+ await ProfileManager.setKey(profileName, jwk);
704
+ const profileConfig = {
705
+ name: profileName,
706
+ host,
707
+ chainId: DEFAULT_CHAIN_ID,
708
+ spaceName: "default",
709
+ did,
710
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
711
+ };
712
+ await ProfileManager.setProfile(profileName, profileConfig);
713
+ const config = await ProfileManager.getConfig();
714
+ if (profileName === "default" || !await ProfileManager.profileExists(config.defaultProfile)) {
715
+ await ProfileManager.setConfig({ ...config, defaultProfile: profileName });
716
+ }
717
+ if (options.keyOnly) {
718
+ outputJson({
719
+ profile: profileName,
720
+ did,
721
+ host,
722
+ authenticated: false
723
+ });
724
+ return;
725
+ }
726
+ const delegationData = await startAuthFlow(did, {
727
+ paste: options.paste,
728
+ jwk,
729
+ host
730
+ });
731
+ await ProfileManager.setSession(profileName, delegationData);
732
+ await ProfileManager.setProfile(profileName, {
733
+ ...profileConfig,
734
+ spaceId: delegationData.spaceId,
735
+ primaryDid: delegationData.primaryDid
736
+ });
737
+ outputJson({
738
+ profile: profileName,
739
+ did,
740
+ host,
741
+ spaceId: delegationData.spaceId,
742
+ authenticated: true
743
+ });
744
+ } catch (error) {
745
+ handleError(error);
746
+ }
747
+ });
748
+ }
749
+
750
+ // src/commands/auth.ts
751
+ import { createInterface as createInterface2 } from "readline";
752
+ async function promptAuthMethod() {
753
+ if (!isInteractive()) {
754
+ return "local";
755
+ }
756
+ const rl = createInterface2({
757
+ input: process.stdin,
758
+ output: process.stderr
759
+ });
760
+ return new Promise((resolve3) => {
761
+ process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
762
+ process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
763
+ `);
764
+ process.stderr.write(` ${theme.accent("2)")} Local key ${theme.muted("(Ethereum private key, for agents/CI)")}
765
+
766
+ `);
767
+ rl.question("Enter choice [1]: ", (answer) => {
768
+ rl.close();
769
+ const trimmed = answer.trim();
770
+ if (trimmed === "2" || trimmed.toLowerCase() === "local") {
771
+ resolve3("local");
772
+ } else {
773
+ resolve3("openkey");
774
+ }
775
+ });
776
+ });
777
+ }
778
+ function registerAuthCommand(program2) {
779
+ const auth = program2.command("auth").description("Authentication management");
780
+ auth.command("login").description("Authenticate with TinyCloud").option("--paste", "Use manual paste mode instead of browser callback").option("--method <method>", "Authentication method: local or openkey").action(async (options, cmd) => {
781
+ try {
782
+ const globalOpts = cmd.optsWithGlobals();
783
+ const ctx = await ProfileManager.resolveContext(globalOpts);
784
+ let method;
785
+ if (options.method) {
786
+ if (options.method !== "local" && options.method !== "openkey") {
787
+ throw new CLIError(
788
+ "INVALID_METHOD",
789
+ `Invalid auth method "${options.method}". Use "local" or "openkey".`,
790
+ ExitCode.USAGE_ERROR
791
+ );
792
+ }
793
+ method = options.method;
794
+ } else {
795
+ method = await promptAuthMethod();
796
+ }
797
+ if (method === "local") {
798
+ await handleLocalAuth(ctx.profile, ctx.host);
799
+ } else {
800
+ await handleOpenKeyAuth(ctx.profile, ctx.host, options.paste);
801
+ }
802
+ } catch (error) {
803
+ handleError(error);
804
+ }
805
+ });
806
+ auth.command("logout").description("Clear session (keep key)").action(async (_options, cmd) => {
807
+ try {
808
+ const globalOpts = cmd.optsWithGlobals();
809
+ const ctx = await ProfileManager.resolveContext(globalOpts);
810
+ await ProfileManager.clearSession(ctx.profile);
811
+ outputJson({ profile: ctx.profile, authenticated: false });
812
+ } catch (error) {
813
+ handleError(error);
814
+ }
815
+ });
816
+ auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
817
+ try {
818
+ const globalOpts = cmd.optsWithGlobals();
819
+ const ctx = await ProfileManager.resolveContext(globalOpts);
820
+ const hasKey = await ProfileManager.getKey(ctx.profile);
821
+ const session = await ProfileManager.getSession(ctx.profile);
822
+ let profile;
823
+ try {
824
+ profile = await ProfileManager.getProfile(ctx.profile);
825
+ } catch {
826
+ profile = null;
827
+ }
828
+ const authenticated = session !== null;
829
+ if (shouldOutputJson()) {
830
+ outputJson({
831
+ authenticated,
832
+ did: profile?.did ?? null,
833
+ primaryDid: profile?.primaryDid ?? null,
834
+ spaceId: profile?.spaceId ?? null,
835
+ host: ctx.host,
836
+ profile: ctx.profile,
837
+ hasKey: hasKey !== null,
838
+ authMethod: profile?.authMethod ?? null,
839
+ address: profile?.address ?? null
840
+ });
841
+ } else {
842
+ process.stdout.write(theme.heading("Authentication Status") + "\n");
843
+ process.stdout.write(formatField("Profile", ctx.profile) + "\n");
844
+ process.stdout.write(formatField("Authenticated", authenticated) + "\n");
845
+ process.stdout.write(formatField("Auth Method", profile?.authMethod ?? null) + "\n");
846
+ process.stdout.write(formatField("Host", ctx.host) + "\n");
847
+ process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
848
+ process.stdout.write(formatField("Primary DID", profile?.primaryDid ?? null) + "\n");
849
+ process.stdout.write(formatField("Address", profile?.address ?? null) + "\n");
850
+ process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
851
+ process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
852
+ }
853
+ } catch (error) {
854
+ handleError(error);
855
+ }
856
+ });
857
+ auth.command("whoami").description("Show identity information").action(async (_options, cmd) => {
858
+ try {
859
+ const globalOpts = cmd.optsWithGlobals();
860
+ const ctx = await ProfileManager.resolveContext(globalOpts);
861
+ const profile = await ProfileManager.getProfile(ctx.profile);
862
+ const session = await ProfileManager.getSession(ctx.profile);
863
+ const authenticated = session !== null;
864
+ if (shouldOutputJson()) {
865
+ outputJson({
866
+ profile: ctx.profile,
867
+ did: profile.did,
868
+ primaryDid: profile.primaryDid ?? null,
869
+ spaceId: profile.spaceId ?? null,
870
+ host: profile.host,
871
+ authenticated,
872
+ authMethod: profile.authMethod ?? null,
873
+ address: profile.address ?? null
874
+ });
875
+ } else {
876
+ process.stdout.write(theme.heading("Identity") + "\n");
877
+ process.stdout.write(formatField("Profile", ctx.profile) + "\n");
878
+ process.stdout.write(formatField("DID", profile.did) + "\n");
879
+ process.stdout.write(formatField("Primary DID", profile.primaryDid ?? null) + "\n");
880
+ process.stdout.write(formatField("Auth Method", profile.authMethod ?? null) + "\n");
881
+ process.stdout.write(formatField("Address", profile.address ?? null) + "\n");
882
+ process.stdout.write(formatField("Space ID", profile.spaceId ?? null) + "\n");
883
+ process.stdout.write(formatField("Host", profile.host) + "\n");
884
+ process.stdout.write(formatField("Authenticated", authenticated) + "\n");
885
+ }
886
+ } catch (error) {
887
+ handleError(error);
888
+ }
889
+ });
890
+ }
891
+ async function handleLocalAuth(profileName, host) {
892
+ const profile = await ProfileManager.getProfile(profileName).catch(() => null);
893
+ let privateKey;
894
+ let address;
895
+ let did;
896
+ if (profile?.authMethod === "local" && profile.privateKey && profile.address) {
897
+ privateKey = profile.privateKey;
898
+ address = profile.address;
899
+ did = profile.did;
900
+ if (isInteractive()) {
901
+ process.stderr.write(theme.muted("Using existing local key") + "\n");
902
+ process.stderr.write(formatField("Address", address) + "\n");
903
+ }
904
+ } else {
905
+ const identity = await withSpinner("Generating Ethereum key...", async () => {
906
+ return generateLocalIdentity(DEFAULT_CHAIN_ID);
907
+ });
908
+ privateKey = identity.privateKey;
909
+ address = identity.address;
910
+ did = identity.did;
911
+ if (isInteractive()) {
912
+ process.stderr.write("\n" + theme.heading("Local Key Generated") + "\n");
913
+ process.stderr.write(formatField("Address", address) + "\n");
914
+ process.stderr.write(formatField("DID", did) + "\n\n");
915
+ }
916
+ }
917
+ const hasKey = await ProfileManager.getKey(profileName);
918
+ if (!hasKey) {
919
+ const { jwk } = await withSpinner("Generating session key...", async () => {
920
+ return generateKey();
921
+ });
922
+ await ProfileManager.setKey(profileName, jwk);
923
+ }
924
+ const sessionResult = await withSpinner("Signing in...", async () => {
925
+ return localKeySignIn({ privateKey, host });
926
+ });
927
+ await ProfileManager.setSession(profileName, {
928
+ authMethod: "local",
929
+ address,
930
+ chainId: DEFAULT_CHAIN_ID,
931
+ spaceId: sessionResult.spaceId
932
+ });
933
+ await ProfileManager.setProfile(profileName, {
934
+ name: profileName,
935
+ host,
936
+ chainId: DEFAULT_CHAIN_ID,
937
+ spaceName: "default",
938
+ did,
939
+ primaryDid: did,
940
+ spaceId: sessionResult.spaceId,
941
+ createdAt: profile?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
942
+ authMethod: "local",
943
+ privateKey,
944
+ address
945
+ });
946
+ outputJson({
947
+ authenticated: true,
948
+ profile: profileName,
949
+ did,
950
+ address,
951
+ spaceId: sessionResult.spaceId,
952
+ authMethod: "local"
953
+ });
954
+ }
955
+ async function handleOpenKeyAuth(profileName, host, paste) {
956
+ const key = await ProfileManager.getKey(profileName);
957
+ if (!key) {
958
+ throw new CLIError(
959
+ "NO_KEY",
960
+ `No key found for profile "${profileName}". Run \`tc init\` first.`,
961
+ ExitCode.AUTH_REQUIRED
962
+ );
963
+ }
964
+ const profile = await ProfileManager.getProfile(profileName);
965
+ const delegationData = await startAuthFlow(profile.did, {
966
+ paste,
967
+ jwk: key,
968
+ host
969
+ });
970
+ await ProfileManager.setSession(profileName, delegationData);
971
+ const updatedProfile = {
972
+ ...profile,
973
+ authMethod: "openkey"
974
+ };
975
+ if (delegationData.spaceId) {
976
+ updatedProfile.spaceId = delegationData.spaceId;
977
+ updatedProfile.primaryDid = delegationData.primaryDid;
978
+ }
979
+ await ProfileManager.setProfile(profileName, updatedProfile);
980
+ outputJson({
981
+ authenticated: true,
982
+ profile: profileName,
983
+ did: profile.did,
984
+ spaceId: delegationData.spaceId,
985
+ authMethod: "openkey"
986
+ });
987
+ }
988
+
989
+ // src/commands/kv.ts
990
+ import { readFile as readFile2 } from "fs/promises";
991
+ import { writeFile as writeFile2 } from "fs/promises";
992
+
993
+ // src/lib/sdk.ts
994
+ import { TinyCloudNode } from "@tinycloud/node-sdk";
995
+ async function createSDKInstance(ctx, options) {
996
+ const profile = await ProfileManager.getProfile(ctx.profile);
997
+ const session = await ProfileManager.getSession(ctx.profile);
998
+ const key = await ProfileManager.getKey(ctx.profile);
999
+ const effectivePrivateKey = options?.privateKey ?? profile.privateKey;
1000
+ if (!key && !effectivePrivateKey) {
1001
+ throw new CLIError(
1002
+ "AUTH_REQUIRED",
1003
+ `No key found for profile "${ctx.profile}". Run \`tc init\` first.`,
1004
+ ExitCode.AUTH_REQUIRED
1005
+ );
1006
+ }
1007
+ if (profile.authMethod === "local" && effectivePrivateKey) {
1008
+ const node2 = new TinyCloudNode({
1009
+ host: ctx.host,
1010
+ privateKey: effectivePrivateKey
1011
+ });
1012
+ await node2.signIn();
1013
+ return node2;
1014
+ }
1015
+ const node = new TinyCloudNode({
1016
+ host: ctx.host,
1017
+ privateKey: options?.privateKey
1018
+ });
1019
+ if (options?.privateKey) {
1020
+ await node.signIn();
1021
+ } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
1022
+ await node.restoreSession({
1023
+ delegationHeader: session.delegationHeader,
1024
+ delegationCid: session.delegationCid,
1025
+ spaceId: session.spaceId,
1026
+ jwk: session.jwk ?? key,
1027
+ verificationMethod: session.verificationMethod ?? profile.did,
1028
+ address: session.address,
1029
+ chainId: session.chainId
1030
+ });
1031
+ }
1032
+ return node;
1033
+ }
1034
+ async function ensureAuthenticated(ctx, options) {
1035
+ const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
1036
+ if (profile?.authMethod === "local" && profile.privateKey) {
1037
+ return createSDKInstance(ctx, { privateKey: profile.privateKey });
1038
+ }
1039
+ const session = await ProfileManager.getSession(ctx.profile);
1040
+ if (!session) {
1041
+ throw new CLIError(
1042
+ "AUTH_REQUIRED",
1043
+ `Not authenticated. Run \`tc auth login\` or \`tc init\` first.`,
1044
+ ExitCode.AUTH_REQUIRED
1045
+ );
1046
+ }
1047
+ return createSDKInstance(ctx, options);
1048
+ }
1049
+
1050
+ // src/commands/kv.ts
1051
+ async function readStdin() {
1052
+ const chunks = [];
1053
+ for await (const chunk of process.stdin) {
1054
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1055
+ }
1056
+ return Buffer.concat(chunks);
1057
+ }
1058
+ function registerKvCommand(program2) {
1059
+ const kv = program2.command("kv").description("Key-value store operations");
1060
+ kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").action(async (key, options, cmd) => {
1061
+ try {
1062
+ const globalOpts = cmd.optsWithGlobals();
1063
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1064
+ const node = await ensureAuthenticated(ctx);
1065
+ const result = await withSpinner(`Getting ${key}...`, () => node.kv.get(key));
1066
+ if (!result.ok) {
1067
+ if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
1068
+ throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
1069
+ }
1070
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1071
+ }
1072
+ const data = result.data.data;
1073
+ const metadata = result.data.headers ?? {};
1074
+ if (options.output) {
1075
+ const content = typeof data === "string" ? data : JSON.stringify(data);
1076
+ await writeFile2(options.output, content);
1077
+ outputJson({ key, written: options.output });
1078
+ return;
1079
+ }
1080
+ if (options.raw) {
1081
+ const content = typeof data === "string" ? data : JSON.stringify(data);
1082
+ process.stdout.write(content);
1083
+ return;
1084
+ }
1085
+ if (shouldOutputJson()) {
1086
+ outputJson({
1087
+ key,
1088
+ data,
1089
+ metadata
1090
+ });
1091
+ } else {
1092
+ const content = typeof data === "string" ? data : JSON.stringify(data);
1093
+ process.stdout.write(content + "\n");
1094
+ }
1095
+ } catch (error) {
1096
+ handleError(error);
1097
+ }
1098
+ });
1099
+ kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").action(async (key, value, options, cmd) => {
1100
+ try {
1101
+ const globalOpts = cmd.optsWithGlobals();
1102
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1103
+ const node = await ensureAuthenticated(ctx);
1104
+ let putValue;
1105
+ const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
1106
+ if (sources.length === 0) {
1107
+ throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
1108
+ }
1109
+ if (sources.length > 1) {
1110
+ throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
1111
+ }
1112
+ if (options.file) {
1113
+ putValue = await readFile2(options.file);
1114
+ } else if (options.stdin) {
1115
+ putValue = await readStdin();
1116
+ } else {
1117
+ try {
1118
+ putValue = JSON.parse(value);
1119
+ } catch {
1120
+ putValue = value;
1121
+ }
1122
+ }
1123
+ const result = await withSpinner(`Writing ${key}...`, () => node.kv.put(key, putValue));
1124
+ if (!result.ok) {
1125
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1126
+ }
1127
+ outputJson({ key, written: true });
1128
+ } catch (error) {
1129
+ handleError(error);
1130
+ }
1131
+ });
1132
+ kv.command("delete <key>").description("Delete a key").action(async (key, _options, cmd) => {
1133
+ try {
1134
+ const globalOpts = cmd.optsWithGlobals();
1135
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1136
+ const node = await ensureAuthenticated(ctx);
1137
+ const result = await withSpinner(`Deleting ${key}...`, () => node.kv.delete(key));
1138
+ if (!result.ok) {
1139
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1140
+ }
1141
+ outputJson({ key, deleted: true });
1142
+ } catch (error) {
1143
+ handleError(error);
1144
+ }
1145
+ });
1146
+ kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").action(async (options, cmd) => {
1147
+ try {
1148
+ const globalOpts = cmd.optsWithGlobals();
1149
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1150
+ const node = await ensureAuthenticated(ctx);
1151
+ const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
1152
+ const result = await withSpinner("Listing keys...", () => node.kv.list(listOptions));
1153
+ if (!result.ok) {
1154
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1155
+ }
1156
+ const rawData = result.data.data ?? result.data;
1157
+ const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
1158
+ if (shouldOutputJson()) {
1159
+ outputJson({
1160
+ keys: keyList,
1161
+ count: keyList.length,
1162
+ prefix: options.prefix ?? null
1163
+ });
1164
+ } else {
1165
+ if (keyList.length === 0) {
1166
+ process.stdout.write(theme.muted("No keys found.") + "\n");
1167
+ } else {
1168
+ const rows = keyList.map((e) => [
1169
+ e.key || e,
1170
+ e.contentLength ? formatBytes(e.contentLength) : "\u2014",
1171
+ e.updatedAt ? formatTimeAgo(e.updatedAt) : "\u2014"
1172
+ ]);
1173
+ process.stdout.write(formatTable(["Key", "Size", "Updated"], rows) + "\n");
1174
+ }
1175
+ }
1176
+ } catch (error) {
1177
+ handleError(error);
1178
+ }
1179
+ });
1180
+ kv.command("head <key>").description("Get metadata for a key (no body)").action(async (key, _options, cmd) => {
1181
+ try {
1182
+ const globalOpts = cmd.optsWithGlobals();
1183
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1184
+ const node = await ensureAuthenticated(ctx);
1185
+ const result = await withSpinner(`Checking ${key}...`, () => node.kv.head(key));
1186
+ if (!result.ok) {
1187
+ if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
1188
+ outputJson({ key, exists: false, metadata: {} });
1189
+ return;
1190
+ }
1191
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1192
+ }
1193
+ outputJson({
1194
+ key,
1195
+ exists: true,
1196
+ metadata: result.data.headers ?? {}
1197
+ });
1198
+ } catch (error) {
1199
+ handleError(error);
1200
+ }
1201
+ });
1202
+ }
1203
+
1204
+ // src/commands/space.ts
1205
+ function registerSpaceCommand(program2) {
1206
+ const space = program2.command("space").description("Space management");
1207
+ space.command("list").description("List spaces").action(async (_options, cmd) => {
1208
+ try {
1209
+ const globalOpts = cmd.optsWithGlobals();
1210
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1211
+ const node = await ensureAuthenticated(ctx);
1212
+ const result = await node.spaces.list();
1213
+ if (!result.ok) {
1214
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1215
+ }
1216
+ if (shouldOutputJson()) {
1217
+ outputJson({ spaces: result.data, count: result.data.length });
1218
+ } else {
1219
+ if (result.data.length === 0) {
1220
+ process.stdout.write(theme.muted("No spaces found.") + "\n");
1221
+ } else {
1222
+ const rows = result.data.map((s) => [
1223
+ s.id || s.spaceId || "\u2014",
1224
+ s.name || "\u2014",
1225
+ s.owner || "\u2014"
1226
+ ]);
1227
+ process.stdout.write(formatTable(["Space ID", "Name", "Owner"], rows) + "\n");
1228
+ }
1229
+ }
1230
+ } catch (error) {
1231
+ handleError(error);
1232
+ }
1233
+ });
1234
+ space.command("create <name>").description("Create a new space").action(async (name, _options, cmd) => {
1235
+ try {
1236
+ const globalOpts = cmd.optsWithGlobals();
1237
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1238
+ const node = await ensureAuthenticated(ctx);
1239
+ const result = await node.spaces.create(name);
1240
+ if (!result.ok) {
1241
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1242
+ }
1243
+ outputJson({ spaceId: result.data.id, name });
1244
+ } catch (error) {
1245
+ handleError(error);
1246
+ }
1247
+ });
1248
+ space.command("info [space-id]").description("Get space info").action(async (spaceId, _options, cmd) => {
1249
+ try {
1250
+ const globalOpts = cmd.optsWithGlobals();
1251
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1252
+ const node = await ensureAuthenticated(ctx);
1253
+ const targetId = spaceId ?? node.spaceId;
1254
+ if (!targetId) {
1255
+ throw new CLIError("NO_SPACE", "No space ID specified and no active space", ExitCode.ERROR);
1256
+ }
1257
+ const profile = await ProfileManager.getProfile(ctx.profile);
1258
+ outputJson({
1259
+ spaceId: targetId,
1260
+ name: profile.spaceName,
1261
+ owner: node.did,
1262
+ host: ctx.host
1263
+ });
1264
+ } catch (error) {
1265
+ handleError(error);
1266
+ }
1267
+ });
1268
+ space.command("switch <name>").description("Switch active space").action(async (name, _options, cmd) => {
1269
+ try {
1270
+ const globalOpts = cmd.optsWithGlobals();
1271
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1272
+ const profile = await ProfileManager.getProfile(ctx.profile);
1273
+ await ProfileManager.setProfile(ctx.profile, { ...profile, spaceName: name });
1274
+ outputJson({ profile: ctx.profile, spaceName: name, switched: true });
1275
+ } catch (error) {
1276
+ handleError(error);
1277
+ }
1278
+ });
1279
+ }
1280
+
1281
+ // src/lib/duration.ts
1282
+ function parseDuration(input) {
1283
+ const match = input.match(/^(\d+)(m|h|d|w)$/);
1284
+ if (match) {
1285
+ const value = parseInt(match[1], 10);
1286
+ const unit = match[2];
1287
+ const multipliers = {
1288
+ m: 60 * 1e3,
1289
+ h: 60 * 60 * 1e3,
1290
+ d: 24 * 60 * 60 * 1e3,
1291
+ w: 7 * 24 * 60 * 60 * 1e3
1292
+ };
1293
+ return value * multipliers[unit];
1294
+ }
1295
+ const date = new Date(input);
1296
+ if (!isNaN(date.getTime())) {
1297
+ const ms = date.getTime() - Date.now();
1298
+ if (ms <= 0) {
1299
+ throw new Error(`Expiry date "${input}" is in the past`);
1300
+ }
1301
+ return ms;
1302
+ }
1303
+ throw new Error(`Invalid duration: "${input}". Use format like "1h", "7d", or an ISO date.`);
1304
+ }
1305
+ function parseExpiry(input) {
1306
+ return new Date(Date.now() + parseDuration(input));
1307
+ }
1308
+
1309
+ // src/commands/delegation.ts
1310
+ function registerDelegationCommand(program2) {
1311
+ const delegation = program2.command("delegation").description("Manage delegations");
1312
+ delegation.command("create").description("Create a delegation").requiredOption("--to <did>", "Recipient DID").requiredOption("--path <path>", "KV path scope").requiredOption("--actions <actions>", "Comma-separated actions (e.g., kv/get,kv/list)").option("--expiry <duration>", "Expiry duration (e.g., 1h, 7d, ISO date)", "1h").action(async (options, cmd) => {
1313
+ try {
1314
+ const globalOpts = cmd.optsWithGlobals();
1315
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1316
+ const node = await ensureAuthenticated(ctx);
1317
+ const actions = options.actions.split(",").map((a) => {
1318
+ const trimmed = a.trim();
1319
+ return trimmed.startsWith("tinycloud.") ? trimmed : `tinycloud.${trimmed}`;
1320
+ });
1321
+ const expiry = parseExpiry(options.expiry);
1322
+ const result = await node.delegationManager.create({
1323
+ delegateDID: options.to,
1324
+ path: options.path,
1325
+ actions,
1326
+ expiry
1327
+ });
1328
+ if (!result.ok) {
1329
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1330
+ }
1331
+ outputJson({
1332
+ cid: result.data.cid,
1333
+ delegateDid: options.to,
1334
+ path: options.path,
1335
+ actions,
1336
+ expiry: expiry.toISOString()
1337
+ });
1338
+ } catch (error) {
1339
+ handleError(error);
1340
+ }
1341
+ });
1342
+ delegation.command("list").description("List delegations").option("--granted", "Show only delegations I've granted").option("--received", "Show only delegations I've received").action(async (options, cmd) => {
1343
+ try {
1344
+ const globalOpts = cmd.optsWithGlobals();
1345
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1346
+ const node = await ensureAuthenticated(ctx);
1347
+ const result = await node.delegationManager.list();
1348
+ if (!result.ok) {
1349
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1350
+ }
1351
+ let delegations = result.data;
1352
+ if (options.granted) {
1353
+ const myDid = node.did;
1354
+ delegations = delegations.filter((d) => d.delegatorDID === myDid);
1355
+ } else if (options.received) {
1356
+ const myDid = node.did;
1357
+ delegations = delegations.filter((d) => d.delegateDID === myDid || d.delegateDID?.includes(myDid));
1358
+ }
1359
+ outputJson({
1360
+ delegations: delegations.map((d) => ({
1361
+ cid: d.cid,
1362
+ delegatee: d.delegateDID,
1363
+ delegator: d.delegatorDID,
1364
+ path: d.path,
1365
+ actions: d.actions,
1366
+ expiry: d.expiry instanceof Date ? d.expiry.toISOString() : d.expiry
1367
+ })),
1368
+ count: delegations.length
1369
+ });
1370
+ } catch (error) {
1371
+ handleError(error);
1372
+ }
1373
+ });
1374
+ delegation.command("info <cid>").description("Get delegation details").action(async (cid, _options, cmd) => {
1375
+ try {
1376
+ const globalOpts = cmd.optsWithGlobals();
1377
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1378
+ const node = await ensureAuthenticated(ctx);
1379
+ const result = await node.delegationManager.get(cid);
1380
+ if (!result.ok) {
1381
+ throw new CLIError("NOT_FOUND", `Delegation "${cid}" not found`, ExitCode.NOT_FOUND);
1382
+ }
1383
+ outputJson(result.data);
1384
+ } catch (error) {
1385
+ handleError(error);
1386
+ }
1387
+ });
1388
+ delegation.command("revoke <cid>").description("Revoke a delegation").action(async (cid, _options, cmd) => {
1389
+ try {
1390
+ const globalOpts = cmd.optsWithGlobals();
1391
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1392
+ const node = await ensureAuthenticated(ctx);
1393
+ const result = await node.delegationManager.revoke(cid);
1394
+ if (!result.ok) {
1395
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1396
+ }
1397
+ outputJson({ cid, revoked: true });
1398
+ } catch (error) {
1399
+ handleError(error);
1400
+ }
1401
+ });
1402
+ }
1403
+
1404
+ // src/commands/share.ts
1405
+ function registerShareCommand(program2) {
1406
+ const share = program2.command("share").description("Share data with others");
1407
+ share.command("create").description("Create a share link").requiredOption("--path <path>", "KV path scope").option("--actions <actions>", "Comma-separated actions", "kv/get").option("--expiry <duration>", "Expiry duration", "7d").option("--web-link", "Generate a web UI link for non-technical recipients").action(async (options, cmd) => {
1408
+ try {
1409
+ const globalOpts = cmd.optsWithGlobals();
1410
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1411
+ const node = await ensureAuthenticated(ctx);
1412
+ const actions = options.actions.split(",").map((a) => {
1413
+ const trimmed = a.trim();
1414
+ return trimmed.startsWith("tinycloud.") ? trimmed : `tinycloud.${trimmed}`;
1415
+ });
1416
+ const expiry = parseExpiry(options.expiry);
1417
+ const result = await node.sharing.generate({
1418
+ path: options.path,
1419
+ actions,
1420
+ expiry
1421
+ });
1422
+ if (!result.ok) {
1423
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1424
+ }
1425
+ const output = {
1426
+ token: result.data.token ?? result.data.cid,
1427
+ shareData: result.data.encodedData ?? result.data.url,
1428
+ path: options.path,
1429
+ actions,
1430
+ expiry: expiry.toISOString()
1431
+ };
1432
+ if (options.webLink) {
1433
+ const shareData = result.data.encodedData ?? result.data.url ?? "";
1434
+ output.webLink = `https://openkey.cloud/share?data=${encodeURIComponent(shareData)}`;
1435
+ }
1436
+ outputJson(output);
1437
+ } catch (error) {
1438
+ handleError(error);
1439
+ }
1440
+ });
1441
+ share.command("receive [data]").description("Receive a share").option("--stdin", "Read share data from stdin").action(async (data, options, cmd) => {
1442
+ try {
1443
+ const globalOpts = cmd.optsWithGlobals();
1444
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1445
+ const node = await ensureAuthenticated(ctx);
1446
+ let shareData;
1447
+ if (options.stdin) {
1448
+ const chunks = [];
1449
+ for await (const chunk of process.stdin) {
1450
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1451
+ }
1452
+ shareData = Buffer.concat(chunks).toString("utf-8").trim();
1453
+ } else if (data) {
1454
+ shareData = data;
1455
+ } else {
1456
+ throw new CLIError("USAGE_ERROR", "Must provide share data or use --stdin", ExitCode.USAGE_ERROR);
1457
+ }
1458
+ const result = await node.sharing.receive(shareData);
1459
+ if (!result.ok) {
1460
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1461
+ }
1462
+ outputJson({
1463
+ received: true,
1464
+ spaceId: result.data.spaceId,
1465
+ path: result.data.path,
1466
+ actions: result.data.actions
1467
+ });
1468
+ } catch (error) {
1469
+ handleError(error);
1470
+ }
1471
+ });
1472
+ share.command("list").description("List active shares").action(async (_options, cmd) => {
1473
+ try {
1474
+ const globalOpts = cmd.optsWithGlobals();
1475
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1476
+ const node = await ensureAuthenticated(ctx);
1477
+ const result = await node.sharing.list();
1478
+ if (!result.ok) {
1479
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1480
+ }
1481
+ outputJson({ shares: result.data, count: result.data.length });
1482
+ } catch (error) {
1483
+ handleError(error);
1484
+ }
1485
+ });
1486
+ share.command("revoke <token>").description("Revoke a share").action(async (token, _options, cmd) => {
1487
+ try {
1488
+ const globalOpts = cmd.optsWithGlobals();
1489
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1490
+ const node = await ensureAuthenticated(ctx);
1491
+ const result = await node.sharing.revoke(token);
1492
+ if (!result.ok) {
1493
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1494
+ }
1495
+ outputJson({ token, revoked: true });
1496
+ } catch (error) {
1497
+ handleError(error);
1498
+ }
1499
+ });
1500
+ }
1501
+
1502
+ // src/commands/node.ts
1503
+ function registerNodeCommand(program2) {
1504
+ const node = program2.command("node").description("Node health and info");
1505
+ node.command("health").description("Check node health").action(async (_options, cmd) => {
1506
+ try {
1507
+ const globalOpts = cmd.optsWithGlobals();
1508
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1509
+ const start = Date.now();
1510
+ const response = await fetch(`${ctx.host}/healthz`);
1511
+ const latencyMs = Date.now() - start;
1512
+ outputJson({
1513
+ healthy: response.ok,
1514
+ host: ctx.host,
1515
+ latencyMs
1516
+ });
1517
+ } catch (error) {
1518
+ if (error instanceof TypeError && error.message.includes("fetch")) {
1519
+ outputJson({ healthy: false, host: (await ProfileManager.resolveContext(cmd.optsWithGlobals())).host, error: "Connection refused" });
1520
+ } else {
1521
+ handleError(error);
1522
+ }
1523
+ }
1524
+ });
1525
+ node.command("version").description("Get node version").action(async (_options, cmd) => {
1526
+ try {
1527
+ const globalOpts = cmd.optsWithGlobals();
1528
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1529
+ const response = await fetch(`${ctx.host}/info`);
1530
+ if (!response.ok) {
1531
+ throw new CLIError("NODE_ERROR", `Node returned ${response.status}`, ExitCode.NODE_ERROR);
1532
+ }
1533
+ const data = await response.json();
1534
+ outputJson({ ...data, host: ctx.host });
1535
+ } catch (error) {
1536
+ handleError(error);
1537
+ }
1538
+ });
1539
+ node.command("status").description("Combined health and version info").action(async (_options, cmd) => {
1540
+ try {
1541
+ const globalOpts = cmd.optsWithGlobals();
1542
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1543
+ const start = Date.now();
1544
+ const [healthRes, versionRes] = await Promise.allSettled([
1545
+ fetch(`${ctx.host}/healthz`),
1546
+ fetch(`${ctx.host}/info`)
1547
+ ]);
1548
+ const latencyMs = Date.now() - start;
1549
+ const healthy = healthRes.status === "fulfilled" && healthRes.value.ok;
1550
+ let versionData = {};
1551
+ if (versionRes.status === "fulfilled" && versionRes.value.ok) {
1552
+ versionData = await versionRes.value.json();
1553
+ }
1554
+ outputJson({
1555
+ healthy,
1556
+ host: ctx.host,
1557
+ latencyMs,
1558
+ ...versionData
1559
+ });
1560
+ } catch (error) {
1561
+ handleError(error);
1562
+ }
1563
+ });
1564
+ }
1565
+
1566
+ // src/commands/profile.ts
1567
+ import { createInterface as createInterface3 } from "readline";
1568
+ function registerProfileCommand(program2) {
1569
+ const profile = program2.command("profile").description("Profile management");
1570
+ profile.command("list").description("List all profiles").action(async (_options, cmd) => {
1571
+ try {
1572
+ const globalOpts = cmd.optsWithGlobals();
1573
+ const config = await ProfileManager.getConfig();
1574
+ const names = await ProfileManager.listProfiles();
1575
+ const profiles = await Promise.all(
1576
+ names.map(async (name) => {
1577
+ try {
1578
+ const p = await ProfileManager.getProfile(name);
1579
+ return {
1580
+ name: p.name,
1581
+ host: p.host,
1582
+ did: p.did,
1583
+ active: name === config.defaultProfile
1584
+ };
1585
+ } catch {
1586
+ return { name, host: null, did: null, active: name === config.defaultProfile };
1587
+ }
1588
+ })
1589
+ );
1590
+ if (shouldOutputJson()) {
1591
+ outputJson({
1592
+ profiles,
1593
+ defaultProfile: config.defaultProfile
1594
+ });
1595
+ } else {
1596
+ for (const p of profiles) {
1597
+ const marker = p.active ? theme.success("\u25CF ") : " ";
1598
+ const name = p.active ? theme.brand(p.name) : p.name;
1599
+ const host = theme.muted(p.host || "no host");
1600
+ process.stdout.write(`${marker}${name} ${host}
1601
+ `);
1602
+ }
1603
+ }
1604
+ } catch (error) {
1605
+ handleError(error);
1606
+ }
1607
+ });
1608
+ profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").action(async (name, options, cmd) => {
1609
+ try {
1610
+ const globalOpts = cmd.optsWithGlobals();
1611
+ const host = options.host ?? globalOpts.host ?? "https://node.tinycloud.xyz";
1612
+ if (await ProfileManager.profileExists(name)) {
1613
+ throw new CLIError("PROFILE_EXISTS", `Profile "${name}" already exists`, ExitCode.ERROR);
1614
+ }
1615
+ await ProfileManager.ensureConfigDir();
1616
+ const { jwk, did } = generateKey();
1617
+ await ProfileManager.setKey(name, jwk);
1618
+ await ProfileManager.setProfile(name, {
1619
+ name,
1620
+ host,
1621
+ chainId: 1,
1622
+ spaceName: "default",
1623
+ did,
1624
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1625
+ });
1626
+ outputJson({ profile: name, did, host, created: true });
1627
+ } catch (error) {
1628
+ handleError(error);
1629
+ }
1630
+ });
1631
+ profile.command("show [name]").description("Show profile details").action(async (name, _options, cmd) => {
1632
+ try {
1633
+ const globalOpts = cmd.optsWithGlobals();
1634
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1635
+ const profileName = name ?? ctx.profile;
1636
+ const p = await ProfileManager.getProfile(profileName);
1637
+ const hasKey = await ProfileManager.getKey(profileName) !== null;
1638
+ const hasSession = await ProfileManager.getSession(profileName) !== null;
1639
+ const config = await ProfileManager.getConfig();
1640
+ const isDefault = profileName === config.defaultProfile;
1641
+ if (shouldOutputJson()) {
1642
+ outputJson({
1643
+ ...p,
1644
+ hasKey,
1645
+ hasSession,
1646
+ isDefault
1647
+ });
1648
+ } else {
1649
+ process.stdout.write(`${theme.heading(p.name)}${isDefault ? theme.success(" (default)") : ""}
1650
+ `);
1651
+ process.stdout.write(formatField("Host", p.host) + "\n");
1652
+ process.stdout.write(formatField("DID", p.did) + "\n");
1653
+ process.stdout.write(formatField("Space", p.spaceId || null) + "\n");
1654
+ process.stdout.write(formatField("Key", hasKey) + "\n");
1655
+ process.stdout.write(formatField("Session", hasSession) + "\n");
1656
+ process.stdout.write(formatField("Created", p.createdAt) + "\n");
1657
+ }
1658
+ } catch (error) {
1659
+ handleError(error);
1660
+ }
1661
+ });
1662
+ profile.command("switch <name>").description("Set default profile").action(async (name, _options, cmd) => {
1663
+ try {
1664
+ if (!await ProfileManager.profileExists(name)) {
1665
+ throw new CLIError("PROFILE_NOT_FOUND", `Profile "${name}" does not exist`, ExitCode.NOT_FOUND);
1666
+ }
1667
+ const config = await ProfileManager.getConfig();
1668
+ await ProfileManager.setConfig({ ...config, defaultProfile: name });
1669
+ outputJson({ defaultProfile: name, switched: true });
1670
+ } catch (error) {
1671
+ handleError(error);
1672
+ }
1673
+ });
1674
+ profile.command("delete <name>").description("Delete a profile").action(async (name, _options, cmd) => {
1675
+ try {
1676
+ if (isInteractive()) {
1677
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
1678
+ const answer = await new Promise((resolve3) => {
1679
+ rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve3);
1680
+ });
1681
+ rl.close();
1682
+ if (answer.toLowerCase() !== "y") {
1683
+ outputJson({ profile: name, deleted: false, reason: "Cancelled by user" });
1684
+ return;
1685
+ }
1686
+ }
1687
+ await ProfileManager.deleteProfile(name);
1688
+ outputJson({ profile: name, deleted: true });
1689
+ } catch (error) {
1690
+ handleError(error);
1691
+ }
1692
+ });
1693
+ }
1694
+
1695
+ // src/commands/completion.ts
1696
+ function registerCompletionCommand(program2) {
1697
+ const completion = program2.command("completion").description("Generate shell completions");
1698
+ completion.command("bash").description("Output bash completions").action(() => {
1699
+ const script = generateBashCompletion();
1700
+ process.stdout.write(script);
1701
+ });
1702
+ completion.command("zsh").description("Output zsh completions").action(() => {
1703
+ const script = generateZshCompletion();
1704
+ process.stdout.write(script);
1705
+ });
1706
+ completion.command("fish").description("Output fish completions").action(() => {
1707
+ const script = generateFishCompletion();
1708
+ process.stdout.write(script);
1709
+ });
1710
+ }
1711
+ function generateBashCompletion() {
1712
+ return `# tc bash completion
1713
+ _tc_completions() {
1714
+ local cur prev commands subcommands
1715
+ COMPREPLY=()
1716
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1717
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1718
+
1719
+ commands="init auth kv space delegation share node profile completion"
1720
+
1721
+ case "\${COMP_WORDS[1]}" in
1722
+ auth) subcommands="login logout status whoami" ;;
1723
+ kv) subcommands="get put delete list head" ;;
1724
+ space) subcommands="list create info switch" ;;
1725
+ delegation) subcommands="create list info revoke" ;;
1726
+ share) subcommands="create receive list revoke" ;;
1727
+ node) subcommands="health version status" ;;
1728
+ profile) subcommands="list create show switch delete" ;;
1729
+ completion) subcommands="bash zsh fish" ;;
1730
+ *) COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") ); return ;;
1731
+ esac
1732
+
1733
+ if [ \${COMP_CWORD} -eq 2 ]; then
1734
+ COMPREPLY=( $(compgen -W "\${subcommands}" -- "\${cur}") )
1735
+ fi
1736
+ }
1737
+ complete -F _tc_completions tc
1738
+ `;
1739
+ }
1740
+ function generateZshCompletion() {
1741
+ return `#compdef tc
1742
+
1743
+ _tc() {
1744
+ local -a commands
1745
+ commands=(
1746
+ 'init:Initialize a new TinyCloud profile'
1747
+ 'auth:Authentication management'
1748
+ 'kv:Key-value store operations'
1749
+ 'space:Space management'
1750
+ 'delegation:Manage delegations'
1751
+ 'share:Share data with others'
1752
+ 'node:Node health and info'
1753
+ 'profile:Profile management'
1754
+ 'completion:Generate shell completions'
1755
+ )
1756
+
1757
+ _arguments -C \\
1758
+ '(-p --profile)'{-p,--profile}'[Profile to use]:profile:' \\
1759
+ '(-H --host)'{-H,--host}'[TinyCloud node URL]:url:' \\
1760
+ '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' \\
1761
+ '--no-cache[Disable caching]' \\
1762
+ '(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]' \\
1763
+ '1:command:->cmd' \\
1764
+ '*::arg:->args'
1765
+
1766
+ case $state in
1767
+ cmd)
1768
+ _describe 'command' commands
1769
+ ;;
1770
+ args)
1771
+ case $words[1] in
1772
+ auth) _values 'subcommand' login logout status whoami ;;
1773
+ kv) _values 'subcommand' get put delete list head ;;
1774
+ space) _values 'subcommand' list create info switch ;;
1775
+ delegation) _values 'subcommand' create list info revoke ;;
1776
+ share) _values 'subcommand' create receive list revoke ;;
1777
+ node) _values 'subcommand' health version status ;;
1778
+ profile) _values 'subcommand' list create show switch delete ;;
1779
+ completion) _values 'subcommand' bash zsh fish ;;
1780
+ esac
1781
+ ;;
1782
+ esac
1783
+ }
1784
+
1785
+ _tc
1786
+ `;
1787
+ }
1788
+ function generateFishCompletion() {
1789
+ return `# tc fish completion
1790
+ set -l commands init auth kv space delegation share node profile completion
1791
+
1792
+ # Disable file completion by default
1793
+ complete -c tc -f
1794
+
1795
+ # Top-level commands
1796
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a init -d "Initialize a new TinyCloud profile"
1797
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a auth -d "Authentication management"
1798
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a kv -d "Key-value store operations"
1799
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a space -d "Space management"
1800
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a delegation -d "Manage delegations"
1801
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a share -d "Share data with others"
1802
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a node -d "Node health and info"
1803
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Profile management"
1804
+ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
1805
+
1806
+ # Subcommands
1807
+ complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
1808
+ complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
1809
+ complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
1810
+ complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
1811
+ complete -c tc -n "__fish_seen_subcommand_from share" -a "create receive list revoke"
1812
+ complete -c tc -n "__fish_seen_subcommand_from node" -a "health version status"
1813
+ complete -c tc -n "__fish_seen_subcommand_from profile" -a "list create show switch delete"
1814
+ complete -c tc -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
1815
+
1816
+ # Global options
1817
+ complete -c tc -l profile -s p -d "Profile to use"
1818
+ complete -c tc -l host -s H -d "TinyCloud node URL"
1819
+ complete -c tc -l verbose -s v -d "Enable verbose output"
1820
+ complete -c tc -l no-cache -d "Disable caching"
1821
+ complete -c tc -l quiet -s q -d "Suppress non-essential output"
1822
+ `;
1823
+ }
1824
+
1825
+ // src/commands/vault.ts
1826
+ import { readFile as readFile3 } from "fs/promises";
1827
+ import { writeFile as writeFile3 } from "fs/promises";
1828
+ import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
1829
+ async function readStdin2() {
1830
+ const chunks = [];
1831
+ for await (const chunk of process.stdin) {
1832
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1833
+ }
1834
+ return Buffer.concat(chunks);
1835
+ }
1836
+ function resolvePrivateKey(options) {
1837
+ const key = options.privateKey || process.env.TC_PRIVATE_KEY;
1838
+ if (!key) {
1839
+ throw new CLIError(
1840
+ "AUTH_REQUIRED",
1841
+ "Private key required. Use --private-key <hex> or set TC_PRIVATE_KEY env var.",
1842
+ ExitCode.AUTH_REQUIRED
1843
+ );
1844
+ }
1845
+ return key;
1846
+ }
1847
+ async function unlockVault(node, privateKey) {
1848
+ const signer = new PrivateKeySigner2(privateKey);
1849
+ const result = await node.vault.unlock(signer);
1850
+ if (result && !result.ok) {
1851
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1852
+ }
1853
+ }
1854
+ function registerVaultCommand(program2) {
1855
+ const vault = program2.command("vault").description("Encrypted vault operations");
1856
+ vault.command("unlock").description("Verify vault unlock works").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
1857
+ try {
1858
+ const globalOpts = cmd.optsWithGlobals();
1859
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1860
+ const privateKey = resolvePrivateKey(options);
1861
+ const node = await ensureAuthenticated(ctx, { privateKey });
1862
+ await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
1863
+ outputJson({ unlocked: true });
1864
+ } catch (error) {
1865
+ handleError(error);
1866
+ }
1867
+ });
1868
+ vault.command("put <key> [value]").description("Encrypt and store a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, value, options, cmd) => {
1869
+ try {
1870
+ const globalOpts = cmd.optsWithGlobals();
1871
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1872
+ const privateKey = resolvePrivateKey(options);
1873
+ const node = await ensureAuthenticated(ctx, { privateKey });
1874
+ await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
1875
+ let putValue;
1876
+ const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
1877
+ if (sources.length === 0) {
1878
+ throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
1879
+ }
1880
+ if (sources.length > 1) {
1881
+ throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
1882
+ }
1883
+ if (options.file) {
1884
+ putValue = new Uint8Array(await readFile3(options.file));
1885
+ } else if (options.stdin) {
1886
+ putValue = new Uint8Array(await readStdin2());
1887
+ } else {
1888
+ putValue = value;
1889
+ }
1890
+ const result = await withSpinner(`Writing ${key}...`, () => node.vault.put(key, putValue));
1891
+ if (!result.ok) {
1892
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1893
+ }
1894
+ outputJson({ key, written: true });
1895
+ } catch (error) {
1896
+ handleError(error);
1897
+ }
1898
+ });
1899
+ vault.command("get <key>").description("Decrypt and retrieve a value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
1900
+ try {
1901
+ const globalOpts = cmd.optsWithGlobals();
1902
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1903
+ const privateKey = resolvePrivateKey(options);
1904
+ const node = await ensureAuthenticated(ctx, { privateKey });
1905
+ await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
1906
+ const result = await withSpinner(`Getting ${key}...`, () => node.vault.get(key));
1907
+ if (!result.ok) {
1908
+ if (result.error.code === "NOT_FOUND") {
1909
+ throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
1910
+ }
1911
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1912
+ }
1913
+ const data = result.data.data ?? result.data;
1914
+ if (options.output) {
1915
+ const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
1916
+ await writeFile3(options.output, content);
1917
+ outputJson({ key, written: options.output });
1918
+ return;
1919
+ }
1920
+ if (options.raw) {
1921
+ const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
1922
+ process.stdout.write(content);
1923
+ return;
1924
+ }
1925
+ outputJson({
1926
+ key,
1927
+ data: data instanceof Uint8Array ? Buffer.from(data).toString("base64") : data
1928
+ });
1929
+ } catch (error) {
1930
+ handleError(error);
1931
+ }
1932
+ });
1933
+ vault.command("delete <key>").description("Delete an encrypted key").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
1934
+ try {
1935
+ const globalOpts = cmd.optsWithGlobals();
1936
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1937
+ const privateKey = resolvePrivateKey(options);
1938
+ const node = await ensureAuthenticated(ctx, { privateKey });
1939
+ await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
1940
+ const result = await withSpinner(`Deleting ${key}...`, () => node.vault.delete(key));
1941
+ if (!result.ok) {
1942
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1943
+ }
1944
+ outputJson({ key, deleted: true });
1945
+ } catch (error) {
1946
+ handleError(error);
1947
+ }
1948
+ });
1949
+ vault.command("list").description("List vault keys").option("--prefix <prefix>", "Filter by key prefix").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
1950
+ try {
1951
+ const globalOpts = cmd.optsWithGlobals();
1952
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1953
+ const privateKey = resolvePrivateKey(options);
1954
+ const node = await ensureAuthenticated(ctx, { privateKey });
1955
+ await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
1956
+ const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
1957
+ const result = await withSpinner("Listing vault keys...", () => node.vault.list(listOptions));
1958
+ if (!result.ok) {
1959
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1960
+ }
1961
+ const keys = result.data.data ?? result.data;
1962
+ const keyList = Array.isArray(keys) ? keys : [];
1963
+ outputJson({
1964
+ keys: keyList,
1965
+ count: keyList.length,
1966
+ prefix: options.prefix ?? null
1967
+ });
1968
+ } catch (error) {
1969
+ handleError(error);
1970
+ }
1971
+ });
1972
+ vault.command("head <key>").description("Get metadata for a vault key").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
1973
+ try {
1974
+ const globalOpts = cmd.optsWithGlobals();
1975
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1976
+ const privateKey = resolvePrivateKey(options);
1977
+ const node = await ensureAuthenticated(ctx, { privateKey });
1978
+ await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
1979
+ const result = await withSpinner(`Checking ${key}...`, () => node.vault.head(key));
1980
+ if (!result.ok) {
1981
+ if (result.error.code === "NOT_FOUND") {
1982
+ outputJson({ key, exists: false, metadata: {} });
1983
+ return;
1984
+ }
1985
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1986
+ }
1987
+ outputJson({
1988
+ key,
1989
+ exists: true,
1990
+ metadata: result.data.headers ?? result.data
1991
+ });
1992
+ } catch (error) {
1993
+ handleError(error);
1994
+ }
1995
+ });
1996
+ }
1997
+
1998
+ // src/commands/secrets.ts
1999
+ import { readFile as readFile4 } from "fs/promises";
2000
+ import { writeFile as writeFile4 } from "fs/promises";
2001
+ import { PrivateKeySigner as PrivateKeySigner3 } from "@tinycloud/node-sdk";
2002
+ var SECRETS_PREFIX = "secrets/";
2003
+ async function readStdin3() {
2004
+ const chunks = [];
2005
+ for await (const chunk of process.stdin) {
2006
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
2007
+ }
2008
+ return Buffer.concat(chunks);
2009
+ }
2010
+ function resolvePrivateKey2(options) {
2011
+ const key = options.privateKey || process.env.TC_PRIVATE_KEY;
2012
+ if (!key) {
2013
+ throw new CLIError(
2014
+ "AUTH_REQUIRED",
2015
+ "Private key required. Use --private-key <hex> or set TC_PRIVATE_KEY env var.",
2016
+ ExitCode.AUTH_REQUIRED
2017
+ );
2018
+ }
2019
+ return key;
2020
+ }
2021
+ async function unlockVault2(node, privateKey) {
2022
+ const signer = new PrivateKeySigner3(privateKey);
2023
+ const result = await node.vault.unlock(signer);
2024
+ if (result && !result.ok) {
2025
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2026
+ }
2027
+ }
2028
+ function registerSecretsCommand(program2) {
2029
+ const secrets = program2.command("secrets").description("Encrypted secrets management");
2030
+ secrets.command("list").description("List secrets").option("--space <spaceId>", "Space to list secrets from (for delegated access)").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
2031
+ try {
2032
+ const globalOpts = cmd.optsWithGlobals();
2033
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2034
+ const privateKey = resolvePrivateKey2(options);
2035
+ const node = await ensureAuthenticated(ctx, { privateKey });
2036
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
2037
+ if (options.space) {
2038
+ throw new CLIError(
2039
+ "NOT_IMPLEMENTED",
2040
+ `Listing secrets from a delegated space (${options.space}) is not yet supported at the SDK level. The vault service currently operates on the space bound to the active session. SDK support for cross-space vault operations is planned.`,
2041
+ ExitCode.ERROR
2042
+ );
2043
+ }
2044
+ const result = await withSpinner("Listing secrets...", () => node.vault.list({ prefix: SECRETS_PREFIX }));
2045
+ if (!result.ok) {
2046
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2047
+ }
2048
+ const keys = result.data.data ?? result.data;
2049
+ const keyList = Array.isArray(keys) ? keys : [];
2050
+ const secretNames = keyList.map(
2051
+ (k) => typeof k === "string" && k.startsWith(SECRETS_PREFIX) ? k.slice(SECRETS_PREFIX.length) : k
2052
+ );
2053
+ outputJson({
2054
+ secrets: secretNames,
2055
+ count: secretNames.length,
2056
+ ...options.space ? { space: options.space } : {}
2057
+ });
2058
+ } catch (error) {
2059
+ handleError(error);
2060
+ }
2061
+ });
2062
+ secrets.command("get <name>").description("Get a secret value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
2063
+ try {
2064
+ const globalOpts = cmd.optsWithGlobals();
2065
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2066
+ const privateKey = resolvePrivateKey2(options);
2067
+ const node = await ensureAuthenticated(ctx, { privateKey });
2068
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
2069
+ const vaultKey = `${SECRETS_PREFIX}${name}`;
2070
+ const result = await withSpinner(`Getting secret ${name}...`, () => node.vault.get(vaultKey));
2071
+ if (!result.ok) {
2072
+ if (result.error.code === "NOT_FOUND") {
2073
+ throw new CLIError("NOT_FOUND", `Secret "${name}" not found`, ExitCode.NOT_FOUND);
2074
+ }
2075
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2076
+ }
2077
+ const data = result.data.data ?? result.data;
2078
+ let value;
2079
+ if (typeof data === "string") {
2080
+ try {
2081
+ const parsed = JSON.parse(data);
2082
+ value = parsed.value;
2083
+ } catch {
2084
+ value = data;
2085
+ }
2086
+ } else if (data instanceof Uint8Array) {
2087
+ try {
2088
+ const parsed = JSON.parse(Buffer.from(data).toString("utf-8"));
2089
+ value = parsed.value;
2090
+ } catch {
2091
+ value = Buffer.from(data).toString("utf-8");
2092
+ }
2093
+ } else {
2094
+ value = data.value ?? data;
2095
+ }
2096
+ if (options.output) {
2097
+ await writeFile4(options.output, value);
2098
+ outputJson({ name, written: options.output });
2099
+ return;
2100
+ }
2101
+ if (options.raw) {
2102
+ process.stdout.write(value);
2103
+ return;
2104
+ }
2105
+ outputJson({ name, value });
2106
+ } catch (error) {
2107
+ handleError(error);
2108
+ }
2109
+ });
2110
+ secrets.command("put <name> [value]").description("Store a secret").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, value, options, cmd) => {
2111
+ try {
2112
+ const globalOpts = cmd.optsWithGlobals();
2113
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2114
+ const privateKey = resolvePrivateKey2(options);
2115
+ const node = await ensureAuthenticated(ctx, { privateKey });
2116
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
2117
+ let secretValue;
2118
+ const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
2119
+ if (sources.length === 0) {
2120
+ throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
2121
+ }
2122
+ if (sources.length > 1) {
2123
+ throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
2124
+ }
2125
+ if (options.file) {
2126
+ secretValue = await readFile4(options.file, "utf-8");
2127
+ } else if (options.stdin) {
2128
+ secretValue = (await readStdin3()).toString("utf-8");
2129
+ } else {
2130
+ secretValue = value;
2131
+ }
2132
+ const payload = JSON.stringify({
2133
+ value: secretValue,
2134
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2135
+ });
2136
+ const vaultKey = `${SECRETS_PREFIX}${name}`;
2137
+ const result = await withSpinner(`Storing secret ${name}...`, () => node.vault.put(vaultKey, payload));
2138
+ if (!result.ok) {
2139
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2140
+ }
2141
+ outputJson({ name, written: true });
2142
+ } catch (error) {
2143
+ handleError(error);
2144
+ }
2145
+ });
2146
+ secrets.command("delete <name>").description("Delete a secret").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
2147
+ try {
2148
+ const globalOpts = cmd.optsWithGlobals();
2149
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2150
+ const privateKey = resolvePrivateKey2(options);
2151
+ const node = await ensureAuthenticated(ctx, { privateKey });
2152
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
2153
+ const vaultKey = `${SECRETS_PREFIX}${name}`;
2154
+ const result = await withSpinner(`Deleting secret ${name}...`, () => node.vault.delete(vaultKey));
2155
+ if (!result.ok) {
2156
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2157
+ }
2158
+ outputJson({ name, deleted: true });
2159
+ } catch (error) {
2160
+ handleError(error);
2161
+ }
2162
+ });
2163
+ secrets.command("manage").description("Open the TinyCloud Secrets Manager in your browser").action(async () => {
2164
+ try {
2165
+ const open = (await import("open")).default;
2166
+ await open("https://secrets.tinycloud.xyz");
2167
+ outputJson({ opened: "https://secrets.tinycloud.xyz" });
2168
+ } catch (error) {
2169
+ handleError(error);
2170
+ }
2171
+ });
2172
+ }
2173
+
2174
+ // src/commands/vars.ts
2175
+ import { readFile as readFile5 } from "fs/promises";
2176
+ import { writeFile as writeFile5 } from "fs/promises";
2177
+ var VARIABLES_PREFIX = "variables/";
2178
+ async function readStdin4() {
2179
+ const chunks = [];
2180
+ for await (const chunk of process.stdin) {
2181
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
2182
+ }
2183
+ return Buffer.concat(chunks);
2184
+ }
2185
+ function resolvePrivateKey3(options) {
2186
+ const key = options.privateKey || process.env.TC_PRIVATE_KEY;
2187
+ if (!key) {
2188
+ throw new CLIError(
2189
+ "AUTH_REQUIRED",
2190
+ "Private key required. Use --private-key <hex> or set TC_PRIVATE_KEY env var.",
2191
+ ExitCode.AUTH_REQUIRED
2192
+ );
2193
+ }
2194
+ return key;
2195
+ }
2196
+ function registerVarsCommand(program2) {
2197
+ const vars = program2.command("vars").description("Plaintext variable management");
2198
+ vars.command("list").description("List variables").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
2199
+ try {
2200
+ const globalOpts = cmd.optsWithGlobals();
2201
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2202
+ const privateKey = resolvePrivateKey3(options);
2203
+ const node = await ensureAuthenticated(ctx, { privateKey });
2204
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2205
+ const result = await withSpinner("Listing variables...", () => prefixedKv.list());
2206
+ if (!result.ok) {
2207
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2208
+ }
2209
+ const rawData = result.data.data ?? result.data;
2210
+ const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
2211
+ outputJson({
2212
+ variables: keyList,
2213
+ count: keyList.length
2214
+ });
2215
+ } catch (error) {
2216
+ handleError(error);
2217
+ }
2218
+ });
2219
+ vars.command("get <name>").description("Get a variable value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
2220
+ try {
2221
+ const globalOpts = cmd.optsWithGlobals();
2222
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2223
+ const privateKey = resolvePrivateKey3(options);
2224
+ const node = await ensureAuthenticated(ctx, { privateKey });
2225
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2226
+ const result = await withSpinner(`Getting variable ${name}...`, () => prefixedKv.get(name));
2227
+ if (!result.ok) {
2228
+ if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
2229
+ throw new CLIError("NOT_FOUND", `Variable "${name}" not found`, ExitCode.NOT_FOUND);
2230
+ }
2231
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2232
+ }
2233
+ const data = result.data.data;
2234
+ let value;
2235
+ if (typeof data === "string") {
2236
+ try {
2237
+ const parsed = JSON.parse(data);
2238
+ value = parsed.value;
2239
+ } catch {
2240
+ value = data;
2241
+ }
2242
+ } else if (data && typeof data === "object" && "value" in data) {
2243
+ value = data.value;
2244
+ } else {
2245
+ value = typeof data === "string" ? data : JSON.stringify(data);
2246
+ }
2247
+ if (options.output) {
2248
+ await writeFile5(options.output, value);
2249
+ outputJson({ name, written: options.output });
2250
+ return;
2251
+ }
2252
+ if (options.raw) {
2253
+ process.stdout.write(value);
2254
+ return;
2255
+ }
2256
+ outputJson({ name, value });
2257
+ } catch (error) {
2258
+ handleError(error);
2259
+ }
2260
+ });
2261
+ vars.command("put <name> [value]").description("Set a variable").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, value, options, cmd) => {
2262
+ try {
2263
+ const globalOpts = cmd.optsWithGlobals();
2264
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2265
+ const privateKey = resolvePrivateKey3(options);
2266
+ const node = await ensureAuthenticated(ctx, { privateKey });
2267
+ let varValue;
2268
+ const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
2269
+ if (sources.length === 0) {
2270
+ throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
2271
+ }
2272
+ if (sources.length > 1) {
2273
+ throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
2274
+ }
2275
+ if (options.file) {
2276
+ varValue = await readFile5(options.file, "utf-8");
2277
+ } else if (options.stdin) {
2278
+ varValue = (await readStdin4()).toString("utf-8");
2279
+ } else {
2280
+ varValue = value;
2281
+ }
2282
+ const payload = {
2283
+ value: varValue,
2284
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2285
+ };
2286
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2287
+ const result = await withSpinner(`Setting variable ${name}...`, () => prefixedKv.put(name, payload));
2288
+ if (!result.ok) {
2289
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2290
+ }
2291
+ outputJson({ name, written: true });
2292
+ } catch (error) {
2293
+ handleError(error);
2294
+ }
2295
+ });
2296
+ vars.command("delete <name>").description("Delete a variable").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
2297
+ try {
2298
+ const globalOpts = cmd.optsWithGlobals();
2299
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2300
+ const privateKey = resolvePrivateKey3(options);
2301
+ const node = await ensureAuthenticated(ctx, { privateKey });
2302
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2303
+ const result = await withSpinner(`Deleting variable ${name}...`, () => prefixedKv.delete(name));
2304
+ if (!result.ok) {
2305
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2306
+ }
2307
+ outputJson({ name, deleted: true });
2308
+ } catch (error) {
2309
+ handleError(error);
2310
+ }
2311
+ });
2312
+ }
2313
+
2314
+ // src/commands/doctor.ts
2315
+ function registerDoctorCommand(program2) {
2316
+ program2.command("doctor").description("Run diagnostic checks").action(async (_options, cmd) => {
2317
+ try {
2318
+ const globalOpts = cmd.optsWithGlobals();
2319
+ const checks = [];
2320
+ const nodeVersion = process.version;
2321
+ const nodeOk = parseInt(nodeVersion.slice(1)) >= 18;
2322
+ checks.push({ name: "Node.js", ok: nodeOk, detail: nodeVersion });
2323
+ let profileName = globalOpts.profile;
2324
+ let profileOk = false;
2325
+ let profileDetail = "";
2326
+ try {
2327
+ const config = await ProfileManager.getConfig();
2328
+ profileName = profileName || config.defaultProfile;
2329
+ const profile = await ProfileManager.getProfile(profileName);
2330
+ profileOk = true;
2331
+ profileDetail = `"${profileName}" at ${profile.host}`;
2332
+ } catch {
2333
+ profileDetail = profileName ? `"${profileName}" not found` : "no profiles configured";
2334
+ }
2335
+ checks.push({ name: "Profile", ok: profileOk, detail: profileDetail });
2336
+ let keyOk = false;
2337
+ let keyDetail = "";
2338
+ if (profileOk && profileName) {
2339
+ try {
2340
+ const key = await ProfileManager.getKey(profileName);
2341
+ keyOk = key !== null;
2342
+ if (keyOk) {
2343
+ const profile = await ProfileManager.getProfile(profileName);
2344
+ keyDetail = profile.did ? `${profile.did.slice(0, 20)}...` : "key found";
2345
+ } else {
2346
+ keyDetail = "no key \u2014 run tc init";
2347
+ }
2348
+ } catch {
2349
+ keyDetail = "error reading key";
2350
+ }
2351
+ } else {
2352
+ keyDetail = "skipped (no profile)";
2353
+ }
2354
+ checks.push({ name: "Key", ok: keyOk, detail: keyDetail });
2355
+ let sessionOk = false;
2356
+ let sessionDetail = "";
2357
+ if (profileOk && profileName) {
2358
+ try {
2359
+ const session = await ProfileManager.getSession(profileName);
2360
+ sessionOk = session !== null;
2361
+ sessionDetail = sessionOk ? "active" : "no session \u2014 run tc auth login";
2362
+ } catch {
2363
+ sessionDetail = "error reading session";
2364
+ }
2365
+ } else {
2366
+ sessionDetail = "skipped (no profile)";
2367
+ }
2368
+ checks.push({ name: "Session", ok: sessionOk, detail: sessionDetail });
2369
+ let nodeReachable = false;
2370
+ let nodeDetail = "";
2371
+ try {
2372
+ const host = profileOk && profileName ? (await ProfileManager.getProfile(profileName)).host : globalOpts.host || DEFAULT_HOST;
2373
+ const start = Date.now();
2374
+ const response = await fetch(`${host}/health`);
2375
+ const latency = Date.now() - start;
2376
+ nodeReachable = response.ok;
2377
+ nodeDetail = nodeReachable ? `${host} (${latency}ms)` : `${host} returned ${response.status}`;
2378
+ } catch (e) {
2379
+ nodeDetail = `unreachable \u2014 ${e instanceof Error ? e.message : "connection failed"}`;
2380
+ }
2381
+ checks.push({ name: "Node", ok: nodeReachable, detail: nodeDetail });
2382
+ let spaceOk = false;
2383
+ let spaceDetail = "";
2384
+ if (sessionOk && profileName) {
2385
+ try {
2386
+ const profile = await ProfileManager.getProfile(profileName);
2387
+ spaceOk = Boolean(profile.spaceId);
2388
+ spaceDetail = spaceOk ? `${profile.spaceId.slice(0, 16)}...` : "no space \u2014 run tc space create";
2389
+ } catch {
2390
+ spaceDetail = "error checking space";
2391
+ }
2392
+ } else {
2393
+ spaceDetail = "skipped (no session)";
2394
+ }
2395
+ checks.push({ name: "Space", ok: spaceOk, detail: spaceDetail });
2396
+ const result = {
2397
+ checks,
2398
+ healthy: checks.every((c) => c.ok)
2399
+ };
2400
+ if (shouldOutputJson()) {
2401
+ outputJson(result);
2402
+ } else {
2403
+ process.stderr.write(formatSection("Diagnostics") + "\n");
2404
+ for (const check of checks) {
2405
+ process.stdout.write(formatCheck(check.ok, check.name, check.detail) + "\n");
2406
+ }
2407
+ process.stdout.write("\n");
2408
+ if (result.healthy) {
2409
+ process.stdout.write(theme.success("All checks passed.") + "\n");
2410
+ } else {
2411
+ const failed = checks.filter((c) => !c.ok).length;
2412
+ process.stdout.write(theme.warn(`${failed} check${failed > 1 ? "s" : ""} need attention.`) + "\n");
2413
+ }
2414
+ }
2415
+ } catch (error) {
2416
+ handleError(error);
2417
+ }
2418
+ });
2419
+ }
2420
+
2421
+ // src/commands/sql.ts
2422
+ import { writeFile as writeFile6 } from "fs/promises";
2423
+ import { resolve } from "path";
2424
+ function registerSqlCommand(program2) {
2425
+ const sql = program2.command("sql").description("SQL database operations");
2426
+ sql.command("query <sql>").description("Run a SELECT query").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
2427
+ try {
2428
+ const globalOpts = cmd.optsWithGlobals();
2429
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2430
+ const node = await ensureAuthenticated(ctx);
2431
+ const params = options.params ? JSON.parse(options.params) : void 0;
2432
+ const result = await withSpinner(
2433
+ "Running query...",
2434
+ () => node.sql.db(options.db).query(sqlStr, params)
2435
+ );
2436
+ if (!result.ok) {
2437
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2438
+ }
2439
+ const { columns, rows, rowCount } = result.data;
2440
+ if (shouldOutputJson()) {
2441
+ outputJson({ columns, rows, rowCount });
2442
+ } else {
2443
+ if (rows.length === 0) {
2444
+ process.stdout.write(theme.muted("No rows returned.") + "\n");
2445
+ } else {
2446
+ const stringRows = rows.map(
2447
+ (row) => row.map((v) => v === null ? "NULL" : String(v))
2448
+ );
2449
+ process.stdout.write(formatTable(columns, stringRows) + "\n");
2450
+ process.stdout.write(theme.muted(`
2451
+ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
2452
+ }
2453
+ }
2454
+ } catch (error) {
2455
+ handleError(error);
2456
+ }
2457
+ });
2458
+ sql.command("execute <sql>").description("Run INSERT/UPDATE/DELETE/DDL statement").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
2459
+ try {
2460
+ const globalOpts = cmd.optsWithGlobals();
2461
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2462
+ const node = await ensureAuthenticated(ctx);
2463
+ const params = options.params ? JSON.parse(options.params) : void 0;
2464
+ const result = await withSpinner(
2465
+ "Executing statement...",
2466
+ () => node.sql.db(options.db).execute(sqlStr, params)
2467
+ );
2468
+ if (!result.ok) {
2469
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2470
+ }
2471
+ outputJson({
2472
+ changes: result.data.changes,
2473
+ lastInsertRowId: result.data.lastInsertRowId
2474
+ });
2475
+ } catch (error) {
2476
+ handleError(error);
2477
+ }
2478
+ });
2479
+ sql.command("export").description("Export database as binary file").option("--db <name>", "Database name", "default").option("-o, --output <file>", "Output file path", "export.db").action(async (options, cmd) => {
2480
+ try {
2481
+ const globalOpts = cmd.optsWithGlobals();
2482
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2483
+ const node = await ensureAuthenticated(ctx);
2484
+ const result = await withSpinner(
2485
+ "Exporting database...",
2486
+ () => node.sql.db(options.db).export()
2487
+ );
2488
+ if (!result.ok) {
2489
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2490
+ }
2491
+ const blob = result.data;
2492
+ const buffer = Buffer.from(await blob.arrayBuffer());
2493
+ const outputPath = resolve(options.output);
2494
+ await writeFile6(outputPath, buffer);
2495
+ outputJson({
2496
+ file: outputPath,
2497
+ size: blob.size,
2498
+ sizeHuman: formatBytes(blob.size)
2499
+ });
2500
+ } catch (error) {
2501
+ handleError(error);
2502
+ }
2503
+ });
2504
+ }
2505
+
2506
+ // src/commands/duckdb.ts
2507
+ import { readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
2508
+ import { resolve as resolve2 } from "path";
2509
+ function registerDuckdbCommand(program2) {
2510
+ const duckdb = program2.command("duckdb").description("DuckDB database operations");
2511
+ duckdb.command("query <sql>").description("Run a SELECT query").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
2512
+ try {
2513
+ const globalOpts = cmd.optsWithGlobals();
2514
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2515
+ const node = await ensureAuthenticated(ctx);
2516
+ const params = options.params ? JSON.parse(options.params) : void 0;
2517
+ const result = await withSpinner(
2518
+ "Running query...",
2519
+ () => node.duckdb.db(options.db).query(sqlStr, params)
2520
+ );
2521
+ if (!result.ok) {
2522
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2523
+ }
2524
+ const { columns, rows, rowCount } = result.data;
2525
+ if (shouldOutputJson()) {
2526
+ outputJson({ columns, rows, rowCount });
2527
+ } else {
2528
+ if (rows.length === 0) {
2529
+ process.stdout.write(theme.muted("No rows returned.") + "\n");
2530
+ } else {
2531
+ const stringRows = rows.map(
2532
+ (row) => row.map((v) => v === null ? "NULL" : String(v))
2533
+ );
2534
+ process.stdout.write(formatTable(columns, stringRows) + "\n");
2535
+ process.stdout.write(theme.muted(`
2536
+ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
2537
+ }
2538
+ }
2539
+ } catch (error) {
2540
+ handleError(error);
2541
+ }
2542
+ });
2543
+ duckdb.command("execute <sql>").description("Run INSERT/UPDATE/DELETE/DDL statement").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
2544
+ try {
2545
+ const globalOpts = cmd.optsWithGlobals();
2546
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2547
+ const node = await ensureAuthenticated(ctx);
2548
+ const params = options.params ? JSON.parse(options.params) : void 0;
2549
+ const result = await withSpinner(
2550
+ "Executing statement...",
2551
+ () => node.duckdb.db(options.db).execute(sqlStr, params)
2552
+ );
2553
+ if (!result.ok) {
2554
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2555
+ }
2556
+ outputJson({ changes: result.data.changes });
2557
+ } catch (error) {
2558
+ handleError(error);
2559
+ }
2560
+ });
2561
+ duckdb.command("describe").description("Show database schema (tables, columns, views)").option("--db <name>", "Database name", "default").action(async (options, cmd) => {
2562
+ try {
2563
+ const globalOpts = cmd.optsWithGlobals();
2564
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2565
+ const node = await ensureAuthenticated(ctx);
2566
+ const result = await withSpinner(
2567
+ "Describing schema...",
2568
+ () => node.duckdb.db(options.db).describe()
2569
+ );
2570
+ if (!result.ok) {
2571
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2572
+ }
2573
+ const schema = result.data;
2574
+ if (shouldOutputJson()) {
2575
+ outputJson(schema);
2576
+ } else {
2577
+ const { tables, views } = schema;
2578
+ if (tables.length === 0 && views.length === 0) {
2579
+ process.stdout.write(theme.muted("No tables or views found.") + "\n");
2580
+ return;
2581
+ }
2582
+ if (tables.length > 0) {
2583
+ process.stdout.write(theme.label("Tables:") + "\n\n");
2584
+ for (const table of tables) {
2585
+ process.stdout.write(` ${theme.value(table.name)}
2586
+ `);
2587
+ const colRows = table.columns.map((col) => [
2588
+ col.name,
2589
+ col.type,
2590
+ col.nullable ? "YES" : "NO"
2591
+ ]);
2592
+ const colTable = formatTable(["Column", "Type", "Nullable"], colRows);
2593
+ process.stdout.write(colTable.split("\n").map((l) => " " + l).join("\n") + "\n\n");
2594
+ }
2595
+ }
2596
+ if (views.length > 0) {
2597
+ process.stdout.write(theme.label("Views:") + "\n\n");
2598
+ const viewRows = views.map((v) => [v.name, v.sql]);
2599
+ process.stdout.write(formatTable(["View", "SQL"], viewRows) + "\n");
2600
+ }
2601
+ }
2602
+ } catch (error) {
2603
+ handleError(error);
2604
+ }
2605
+ });
2606
+ duckdb.command("export").description("Export database as binary file").option("--db <name>", "Database name", "default").option("-o, --output <file>", "Output file path", "export.duckdb").action(async (options, cmd) => {
2607
+ try {
2608
+ const globalOpts = cmd.optsWithGlobals();
2609
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2610
+ const node = await ensureAuthenticated(ctx);
2611
+ const result = await withSpinner(
2612
+ "Exporting database...",
2613
+ () => node.duckdb.db(options.db).export()
2614
+ );
2615
+ if (!result.ok) {
2616
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2617
+ }
2618
+ const blob = result.data;
2619
+ const buffer = Buffer.from(await blob.arrayBuffer());
2620
+ const outputPath = resolve2(options.output);
2621
+ await writeFile7(outputPath, buffer);
2622
+ outputJson({
2623
+ file: outputPath,
2624
+ size: blob.size,
2625
+ sizeHuman: formatBytes(blob.size)
2626
+ });
2627
+ } catch (error) {
2628
+ handleError(error);
2629
+ }
2630
+ });
2631
+ duckdb.command("import <file>").description("Import a DuckDB database file").option("--db <name>", "Database name", "default").action(async (file, options, cmd) => {
2632
+ try {
2633
+ const globalOpts = cmd.optsWithGlobals();
2634
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2635
+ const node = await ensureAuthenticated(ctx);
2636
+ const filePath = resolve2(file);
2637
+ const bytes = new Uint8Array(await readFile6(filePath));
2638
+ const result = await withSpinner(
2639
+ "Importing database...",
2640
+ () => node.duckdb.db(options.db).import(bytes)
2641
+ );
2642
+ if (!result.ok) {
2643
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2644
+ }
2645
+ outputJson({
2646
+ file: filePath,
2647
+ size: bytes.byteLength,
2648
+ sizeHuman: formatBytes(bytes.byteLength),
2649
+ imported: true
2650
+ });
2651
+ } catch (error) {
2652
+ handleError(error);
2653
+ }
2654
+ });
2655
+ }
2656
+
2657
+ // src/commands/upgrade.ts
2658
+ import { execSync as execSync2 } from "child_process";
2659
+ import { readFileSync } from "fs";
2660
+ var PACKAGE_NAME = "@tinycloud/cli";
2661
+ function getCurrentVersion() {
2662
+ const pkg = JSON.parse(
2663
+ readFileSync(new URL("../package.json", import.meta.url), "utf-8")
2664
+ );
2665
+ return pkg.version;
2666
+ }
2667
+ async function getLatestVersion() {
2668
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
2669
+ if (!res.ok) {
2670
+ throw new Error(`Failed to fetch latest version: ${res.status} ${res.statusText}`);
2671
+ }
2672
+ const data = await res.json();
2673
+ return data.version;
2674
+ }
2675
+ function detectPackageManager() {
2676
+ try {
2677
+ const bunGlobals = execSync2("bun pm ls -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2678
+ if (bunGlobals.includes(PACKAGE_NAME)) {
2679
+ return "bun";
2680
+ }
2681
+ } catch {
2682
+ }
2683
+ try {
2684
+ const npmGlobals = execSync2("npm ls -g --depth=0", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2685
+ if (npmGlobals.includes(PACKAGE_NAME)) {
2686
+ return "npm";
2687
+ }
2688
+ } catch {
2689
+ }
2690
+ return "bun";
2691
+ }
2692
+ function registerUpgradeCommand(program2) {
2693
+ program2.command("upgrade").description("Upgrade the TinyCloud CLI to the latest version").action(async () => {
2694
+ try {
2695
+ const current = getCurrentVersion();
2696
+ process.stderr.write(theme.muted("Checking for updates...") + "\n");
2697
+ const latest = await getLatestVersion();
2698
+ if (current === latest) {
2699
+ process.stdout.write(theme.success(`Already on latest version (${current})`) + "\n");
2700
+ return;
2701
+ }
2702
+ process.stdout.write(`Current: ${theme.warn(current)} \u2192 Latest: ${theme.success(latest)}
2703
+ `);
2704
+ const pm = detectPackageManager();
2705
+ const cmd = pm === "bun" ? `bun install -g ${PACKAGE_NAME}@latest` : `npm install -g ${PACKAGE_NAME}@latest`;
2706
+ process.stderr.write(theme.muted(`Upgrading via ${pm}...`) + "\n\n");
2707
+ try {
2708
+ execSync2(cmd, { stdio: "inherit" });
2709
+ process.stdout.write("\n" + theme.success(`Upgraded to ${latest}`) + "\n");
2710
+ } catch {
2711
+ process.stderr.write("\n" + theme.warn("Automatic upgrade failed.") + "\n");
2712
+ process.stderr.write(theme.muted("Try running manually:") + "\n");
2713
+ process.stderr.write(` ${theme.command(`bun install -g ${PACKAGE_NAME}@latest`)}
2714
+ `);
2715
+ process.stderr.write(theme.muted(" or") + "\n");
2716
+ process.stderr.write(` ${theme.command(`npm install -g ${PACKAGE_NAME}@latest`)}
2717
+ `);
2718
+ }
2719
+ } catch (error) {
2720
+ handleError(error);
2721
+ }
2722
+ });
2723
+ }
2724
+
2725
+ // src/index.ts
2726
+ var { version } = JSON.parse(
2727
+ readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
2728
+ );
2729
+ var program = new Command();
2730
+ program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version(version).option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output").option("--json", "Force JSON output");
2731
+ program.hook("preAction", async (thisCommand) => {
2732
+ const opts = thisCommand.optsWithGlobals();
2733
+ if (!opts.quiet) {
2734
+ emitBanner(version);
2735
+ }
2736
+ const commandName = thisCommand.name();
2737
+ const parentName = thisCommand.parent?.name();
2738
+ const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
2739
+ const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade"].includes(commandName) || fullCommand === "profile create";
2740
+ if (!skipGuard && !opts.quiet && isInteractive()) {
2741
+ try {
2742
+ const config = await ProfileManager.getConfig();
2743
+ const profileName = opts.profile || config.defaultProfile;
2744
+ const hasProfile = await ProfileManager.profileExists(profileName);
2745
+ if (!hasProfile) {
2746
+ process.stderr.write(theme.warn("\u26A0 No profile configured.") + " " + theme.muted("Run: tc init") + "\n\n");
2747
+ } else {
2748
+ const key = await ProfileManager.getKey(profileName);
2749
+ if (!key) {
2750
+ process.stderr.write(theme.warn("\u26A0 No key found.") + " " + theme.muted("Run: tc init") + "\n\n");
2751
+ }
2752
+ }
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ });
2757
+ registerInitCommand(program);
2758
+ registerAuthCommand(program);
2759
+ registerKvCommand(program);
2760
+ registerSpaceCommand(program);
2761
+ registerDelegationCommand(program);
2762
+ registerShareCommand(program);
2763
+ registerNodeCommand(program);
2764
+ registerProfileCommand(program);
2765
+ registerCompletionCommand(program);
2766
+ registerVaultCommand(program);
2767
+ registerSecretsCommand(program);
2768
+ registerVarsCommand(program);
2769
+ registerDoctorCommand(program);
2770
+ registerSqlCommand(program);
2771
+ registerDuckdbCommand(program);
2772
+ registerUpgradeCommand(program);
2773
+ program.addHelpText("afterAll", () => {
2774
+ if (!process.stdout.isTTY) return "";
2775
+ return `
2776
+ ${theme.heading("Examples:")}
2777
+ ${theme.command("tc init")} ${theme.muted("Set up a profile and generate keys")}
2778
+ ${theme.command("tc auth login")} ${theme.muted("Authenticate via browser")}
2779
+ ${theme.command('tc kv put greeting "Hello"')} ${theme.muted("Store a value")}
2780
+ ${theme.command("tc kv list")} ${theme.muted("List all keys")}
2781
+ ${theme.command("tc delegation create --to did:pkh:...")} ${theme.muted("Grant access to another user")}
2782
+ ${theme.command("tc space list")} ${theme.muted("Show your spaces")}
2783
+
2784
+ ${theme.muted("Docs:")} ${theme.accent("https://docs.tinycloud.xyz/cli")}
2785
+ ${theme.muted("Repo:")} ${theme.accent("https://github.com/tinycloudlabs/web-sdk")}
2786
+ `;
2787
+ });
2788
+ try {
2789
+ await program.parseAsync(process.argv);
2790
+ } catch (error) {
2791
+ handleError(error);
2792
+ }
2793
+ //# sourceMappingURL=index.js.map