@tinycloud/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,13 +23,49 @@ var ExitCode = {
23
23
 
24
24
  // src/output/formatter.ts
25
25
  import ora from "ora";
26
+
27
+ // src/output/theme.ts
28
+ import chalk from "chalk";
29
+ var TC_PALETTE = {
30
+ primary: "#4473b9",
31
+ accent: "#5b9bd5",
32
+ success: "#2fba6a",
33
+ warn: "#e8a838",
34
+ error: "#d94040",
35
+ muted: "#808080",
36
+ dim: "#5a5a5a"
37
+ };
38
+ var theme = {
39
+ primary: chalk.hex(TC_PALETTE.primary),
40
+ accent: chalk.hex(TC_PALETTE.accent),
41
+ success: chalk.hex(TC_PALETTE.success),
42
+ warn: chalk.hex(TC_PALETTE.warn),
43
+ error: chalk.hex(TC_PALETTE.error),
44
+ muted: chalk.hex(TC_PALETTE.muted),
45
+ dim: chalk.hex(TC_PALETTE.dim),
46
+ heading: chalk.bold.hex(TC_PALETTE.primary),
47
+ command: chalk.hex(TC_PALETTE.accent),
48
+ brand: chalk.bold.hex(TC_PALETTE.primary),
49
+ label: chalk.bold,
50
+ value: chalk.white,
51
+ hint: chalk.italic.hex(TC_PALETTE.muted)
52
+ };
53
+
54
+ // src/output/formatter.ts
26
55
  function outputJson(data) {
27
56
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
28
57
  }
29
58
  function outputError(code, message) {
30
- process.stderr.write(
31
- JSON.stringify({ error: { code, message } }, null, 2) + "\n"
32
- );
59
+ if (isInteractive()) {
60
+ process.stderr.write(
61
+ `${theme.error("\u2717")} ${theme.label(code)}: ${message}
62
+ `
63
+ );
64
+ } else {
65
+ process.stderr.write(
66
+ JSON.stringify({ error: { code, message } }, null, 2) + "\n"
67
+ );
68
+ }
33
69
  }
34
70
  function isInteractive() {
35
71
  return Boolean(process.stdout.isTTY);
@@ -41,13 +77,56 @@ async function withSpinner(label, fn) {
41
77
  const spinner = ora(label).start();
42
78
  try {
43
79
  const result = await fn();
44
- spinner.succeed();
80
+ spinner.succeed(label);
45
81
  return result;
46
82
  } catch (error) {
47
- spinner.fail();
83
+ spinner.fail(label);
48
84
  throw error;
49
85
  }
50
86
  }
87
+ function shouldOutputJson() {
88
+ return !isInteractive() || process.argv.includes("--json");
89
+ }
90
+ function formatField(label, value) {
91
+ if (value === null || value === void 0) return ` ${theme.label(label + ":")} ${theme.muted("\u2014")}`;
92
+ if (typeof value === "boolean") {
93
+ return ` ${theme.label(label + ":")} ${value ? theme.success("yes") : theme.muted("no")}`;
94
+ }
95
+ return ` ${theme.label(label + ":")} ${theme.value(String(value))}`;
96
+ }
97
+ function formatTable(headers, rows) {
98
+ const widths = headers.map(
99
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length))
100
+ );
101
+ const headerLine = headers.map((h, i) => theme.label(h.padEnd(widths[i]))).join(" ");
102
+ const separator = widths.map((w) => theme.dim("\u2500".repeat(w))).join(" ");
103
+ const dataLines = rows.map(
104
+ (row) => row.map((cell, i) => (cell || "").padEnd(widths[i])).join(" ")
105
+ );
106
+ return [headerLine, separator, ...dataLines].join("\n");
107
+ }
108
+ function formatCheck(ok, label, detail) {
109
+ const icon = ok === "warn" ? theme.warn("\u26A0") : ok ? theme.success("\u2713") : theme.error("\u2717");
110
+ const detailStr = detail ? ` ${theme.muted(`(${detail})`)}` : "";
111
+ return `${icon} ${label}${detailStr}`;
112
+ }
113
+ function formatSection(title) {
114
+ return `
115
+ ${theme.heading(title)}`;
116
+ }
117
+ function formatBytes(bytes) {
118
+ if (bytes < 1024) return `${bytes} B`;
119
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
120
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
121
+ }
122
+ function formatTimeAgo(date) {
123
+ const d = typeof date === "string" ? new Date(date) : date;
124
+ const seconds = Math.floor((Date.now() - d.getTime()) / 1e3);
125
+ if (seconds < 60) return "just now";
126
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
127
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
128
+ return `${Math.floor(seconds / 86400)}d ago`;
129
+ }
51
130
 
52
131
  // src/output/errors.ts
53
132
  var CLIError = class extends Error {
@@ -81,6 +160,97 @@ function handleError(error) {
81
160
  process.exit(cliError.exitCode);
82
161
  }
83
162
 
163
+ // src/output/taglines.ts
164
+ var HOLIDAY_TAGLINES = [
165
+ { month: 1, day: 1, range: 1, tagline: "New year, new keys, same cloud." },
166
+ { month: 2, day: 14, tagline: "We love your data as much as you do." },
167
+ { month: 3, day: 14, tagline: "3.14159 reasons to encrypt everything." },
168
+ { month: 5, day: 4, tagline: "May the fourth be with your keys." },
169
+ { month: 10, day: 31, tagline: "Nothing scarier than plaintext secrets." },
170
+ { month: 12, day: 25, range: 2, tagline: "Unwrap your data, not your keys." },
171
+ { month: 12, day: 31, tagline: "Encrypt your resolutions." }
172
+ ];
173
+ var TAGLINES = [
174
+ // Professional
175
+ "Your data, your keys, your cloud.",
176
+ "Self-sovereign storage for the modern web.",
177
+ "The cloud you actually own.",
178
+ "Encrypted by default, decentralized by design.",
179
+ "Where your data answers only to you.",
180
+ "End-to-end encrypted. No exceptions.",
181
+ "Like S3 but you hold the keys.",
182
+ "Privacy isn't a feature. It's the architecture.",
183
+ "Sovereign storage, zero knowledge.",
184
+ "Your .env is safe here \u2014 we use real cryptography.",
185
+ // Playful / nerdy
186
+ "UCAN do anything.",
187
+ "Keys generated, delegations granted, data liberated.",
188
+ "Decentralized storage, centralized vibes.",
189
+ "Trust nobody, delegate everything.",
190
+ "sudo make me a sandwich, encrypted.",
191
+ "Have you tried turning your keys off and on again?",
192
+ "All your base are belong to you.",
193
+ "In UCAN we trust.",
194
+ "0 knowledge, 100% confidence.",
195
+ "Keeping secrets since 2024."
196
+ ];
197
+ function getHolidayTagline() {
198
+ const now = /* @__PURE__ */ new Date();
199
+ const month = now.getMonth() + 1;
200
+ const day = now.getDate();
201
+ for (const h of HOLIDAY_TAGLINES) {
202
+ const range = h.range ?? 0;
203
+ if (h.month === month && Math.abs(day - h.day) <= range) {
204
+ return h.tagline;
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+ function pickTagline() {
210
+ const holiday = getHolidayTagline();
211
+ if (holiday) return holiday;
212
+ return TAGLINES[Math.floor(Math.random() * TAGLINES.length)];
213
+ }
214
+
215
+ // src/output/banner.ts
216
+ import { execSync } from "child_process";
217
+ var bannerEmitted = false;
218
+ function resolveCommitHash() {
219
+ try {
220
+ return execSync("git rev-parse --short HEAD", {
221
+ encoding: "utf-8",
222
+ stdio: ["pipe", "pipe", "pipe"]
223
+ }).trim() || null;
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+ function formatBannerLine(version) {
229
+ const commit = resolveCommitHash();
230
+ const tagline = pickTagline();
231
+ const versionPart = `tc v${version}`;
232
+ const commitPart = commit ? ` (${commit})` : "";
233
+ const separator = " \u2014 ";
234
+ if (!isInteractive()) {
235
+ return `${versionPart}${commitPart}${separator}${tagline}`;
236
+ }
237
+ return [
238
+ theme.brand("\u2601\uFE0F tc"),
239
+ " ",
240
+ theme.muted(`v${version}`),
241
+ commit ? theme.dim(` (${commit})`) : "",
242
+ theme.dim(separator),
243
+ theme.primary(tagline)
244
+ ].join("");
245
+ }
246
+ function emitBanner(version) {
247
+ if (bannerEmitted) return;
248
+ if (!isInteractive()) return;
249
+ if (process.env.TC_HIDE_BANNER === "1") return;
250
+ bannerEmitted = true;
251
+ process.stderr.write(formatBannerLine(version) + "\n\n");
252
+ }
253
+
84
254
  // src/config/profiles.ts
85
255
  import { join as join2 } from "path";
86
256
  import { rm as rm2 } from "fs/promises";
@@ -346,6 +516,31 @@ function buildAuthUrl(did, options = {}) {
346
516
  async function callbackFlow(did, options = {}) {
347
517
  return new Promise((resolve, reject) => {
348
518
  let timeout;
519
+ let settled = false;
520
+ let rl;
521
+ function settle(result) {
522
+ if (settled) return;
523
+ settled = true;
524
+ clearTimeout(timeout);
525
+ server.close();
526
+ if (rl) {
527
+ rl.close();
528
+ }
529
+ if (result.data) {
530
+ resolve(result.data);
531
+ } else {
532
+ reject(result.error);
533
+ }
534
+ }
535
+ function parsePasteInput(input) {
536
+ const trimmed = input.trim();
537
+ try {
538
+ return JSON.parse(trimmed);
539
+ } catch {
540
+ const decoded = Buffer.from(trimmed, "base64").toString("utf-8");
541
+ return JSON.parse(decoded);
542
+ }
543
+ }
349
544
  const server = createServer((req, res) => {
350
545
  if (req.method === "POST" && req.url === "/callback") {
351
546
  let body = "";
@@ -360,13 +555,11 @@ async function callbackFlow(did, options = {}) {
360
555
  "Access-Control-Allow-Origin": "*"
361
556
  });
362
557
  res.end(JSON.stringify({ success: true }));
363
- clearTimeout(timeout);
364
- server.close();
365
- resolve(data);
558
+ settle({ data });
366
559
  } catch (err) {
367
560
  res.writeHead(400, { "Content-Type": "application/json" });
368
561
  res.end(JSON.stringify({ error: "Invalid JSON" }));
369
- reject(new Error("Invalid delegation data received"));
562
+ settle({ error: new Error("Invalid delegation data received") });
370
563
  }
371
564
  });
372
565
  } else if (req.method === "OPTIONS") {
@@ -384,7 +577,7 @@ async function callbackFlow(did, options = {}) {
384
577
  server.listen(0, "127.0.0.1", async () => {
385
578
  const addr = server.address();
386
579
  if (!addr || typeof addr === "string") {
387
- reject(new Error("Failed to start callback server"));
580
+ settle({ error: new Error("Failed to start callback server") });
388
581
  return;
389
582
  }
390
583
  const port = addr.port;
@@ -401,10 +594,26 @@ async function callbackFlow(did, options = {}) {
401
594
  server.close();
402
595
  throw new Error("Failed to open browser");
403
596
  }
597
+ if (isInteractive()) {
598
+ console.error(`
599
+ If the browser can't connect back, paste the delegation code here:`);
600
+ rl = createInterface({
601
+ input: process.stdin,
602
+ output: process.stderr
603
+ });
604
+ rl.on("line", (input) => {
605
+ if (settled) return;
606
+ try {
607
+ const data = parsePasteInput(input);
608
+ settle({ data });
609
+ } catch {
610
+ console.error("Invalid delegation code. Expected JSON or base64-encoded JSON. Try again:");
611
+ }
612
+ });
613
+ }
404
614
  });
405
615
  timeout = setTimeout(() => {
406
- server.close();
407
- reject(new Error("Authentication timed out after 5 minutes"));
616
+ settle({ error: new Error("Authentication timed out after 5 minutes") });
408
617
  }, 5 * 60 * 1e3);
409
618
  });
410
619
  }
@@ -564,15 +773,27 @@ function registerAuthCommand(program2) {
564
773
  } catch {
565
774
  profile = null;
566
775
  }
567
- outputJson({
568
- authenticated: session !== null,
569
- did: profile?.did ?? null,
570
- primaryDid: profile?.primaryDid ?? null,
571
- spaceId: profile?.spaceId ?? null,
572
- host: ctx.host,
573
- profile: ctx.profile,
574
- hasKey: hasKey !== null
575
- });
776
+ const authenticated = session !== null;
777
+ if (shouldOutputJson()) {
778
+ outputJson({
779
+ authenticated,
780
+ did: profile?.did ?? null,
781
+ primaryDid: profile?.primaryDid ?? null,
782
+ spaceId: profile?.spaceId ?? null,
783
+ host: ctx.host,
784
+ profile: ctx.profile,
785
+ hasKey: hasKey !== null
786
+ });
787
+ } else {
788
+ process.stdout.write(theme.heading("Authentication Status") + "\n");
789
+ process.stdout.write(formatField("Profile", ctx.profile) + "\n");
790
+ process.stdout.write(formatField("Authenticated", authenticated) + "\n");
791
+ process.stdout.write(formatField("Host", ctx.host) + "\n");
792
+ process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
793
+ process.stdout.write(formatField("Primary DID", profile?.primaryDid ?? null) + "\n");
794
+ process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
795
+ process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
796
+ }
576
797
  } catch (error) {
577
798
  handleError(error);
578
799
  }
@@ -583,14 +804,25 @@ function registerAuthCommand(program2) {
583
804
  const ctx = await ProfileManager.resolveContext(globalOpts);
584
805
  const profile = await ProfileManager.getProfile(ctx.profile);
585
806
  const session = await ProfileManager.getSession(ctx.profile);
586
- outputJson({
587
- profile: ctx.profile,
588
- did: profile.did,
589
- primaryDid: profile.primaryDid ?? null,
590
- spaceId: profile.spaceId ?? null,
591
- host: profile.host,
592
- authenticated: session !== null
593
- });
807
+ const authenticated = session !== null;
808
+ if (shouldOutputJson()) {
809
+ outputJson({
810
+ profile: ctx.profile,
811
+ did: profile.did,
812
+ primaryDid: profile.primaryDid ?? null,
813
+ spaceId: profile.spaceId ?? null,
814
+ host: profile.host,
815
+ authenticated
816
+ });
817
+ } else {
818
+ process.stdout.write(theme.heading("Identity") + "\n");
819
+ process.stdout.write(formatField("Profile", ctx.profile) + "\n");
820
+ process.stdout.write(formatField("DID", profile.did) + "\n");
821
+ process.stdout.write(formatField("Primary DID", profile.primaryDid ?? null) + "\n");
822
+ process.stdout.write(formatField("Space ID", profile.spaceId ?? null) + "\n");
823
+ process.stdout.write(formatField("Host", profile.host) + "\n");
824
+ process.stdout.write(formatField("Authenticated", authenticated) + "\n");
825
+ }
594
826
  } catch (error) {
595
827
  handleError(error);
596
828
  }
@@ -680,11 +912,16 @@ function registerKvCommand(program2) {
680
912
  process.stdout.write(content);
681
913
  return;
682
914
  }
683
- outputJson({
684
- key,
685
- data,
686
- metadata
687
- });
915
+ if (shouldOutputJson()) {
916
+ outputJson({
917
+ key,
918
+ data,
919
+ metadata
920
+ });
921
+ } else {
922
+ const content = typeof data === "string" ? data : JSON.stringify(data);
923
+ process.stdout.write(content + "\n");
924
+ }
688
925
  } catch (error) {
689
926
  handleError(error);
690
927
  }
@@ -748,11 +985,24 @@ function registerKvCommand(program2) {
748
985
  }
749
986
  const rawData = result.data.data ?? result.data;
750
987
  const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
751
- outputJson({
752
- keys: keyList,
753
- count: keyList.length,
754
- prefix: options.prefix ?? null
755
- });
988
+ if (shouldOutputJson()) {
989
+ outputJson({
990
+ keys: keyList,
991
+ count: keyList.length,
992
+ prefix: options.prefix ?? null
993
+ });
994
+ } else {
995
+ if (keyList.length === 0) {
996
+ process.stdout.write(theme.muted("No keys found.") + "\n");
997
+ } else {
998
+ const rows = keyList.map((e) => [
999
+ e.key || e,
1000
+ e.contentLength ? formatBytes(e.contentLength) : "\u2014",
1001
+ e.updatedAt ? formatTimeAgo(e.updatedAt) : "\u2014"
1002
+ ]);
1003
+ process.stdout.write(formatTable(["Key", "Size", "Updated"], rows) + "\n");
1004
+ }
1005
+ }
756
1006
  } catch (error) {
757
1007
  handleError(error);
758
1008
  }
@@ -793,7 +1043,20 @@ function registerSpaceCommand(program2) {
793
1043
  if (!result.ok) {
794
1044
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
795
1045
  }
796
- outputJson({ spaces: result.data, count: result.data.length });
1046
+ if (shouldOutputJson()) {
1047
+ outputJson({ spaces: result.data, count: result.data.length });
1048
+ } else {
1049
+ if (result.data.length === 0) {
1050
+ process.stdout.write(theme.muted("No spaces found.") + "\n");
1051
+ } else {
1052
+ const rows = result.data.map((s) => [
1053
+ s.id || s.spaceId || "\u2014",
1054
+ s.name || "\u2014",
1055
+ s.owner || "\u2014"
1056
+ ]);
1057
+ process.stdout.write(formatTable(["Space ID", "Name", "Owner"], rows) + "\n");
1058
+ }
1059
+ }
797
1060
  } catch (error) {
798
1061
  handleError(error);
799
1062
  }
@@ -1154,10 +1417,20 @@ function registerProfileCommand(program2) {
1154
1417
  }
1155
1418
  })
1156
1419
  );
1157
- outputJson({
1158
- profiles,
1159
- defaultProfile: config.defaultProfile
1160
- });
1420
+ if (shouldOutputJson()) {
1421
+ outputJson({
1422
+ profiles,
1423
+ defaultProfile: config.defaultProfile
1424
+ });
1425
+ } else {
1426
+ for (const p of profiles) {
1427
+ const marker = p.active ? theme.success("\u25CF ") : " ";
1428
+ const name = p.active ? theme.brand(p.name) : p.name;
1429
+ const host = theme.muted(p.host || "no host");
1430
+ process.stdout.write(`${marker}${name} ${host}
1431
+ `);
1432
+ }
1433
+ }
1161
1434
  } catch (error) {
1162
1435
  handleError(error);
1163
1436
  }
@@ -1194,12 +1467,24 @@ function registerProfileCommand(program2) {
1194
1467
  const hasKey = await ProfileManager.getKey(profileName) !== null;
1195
1468
  const hasSession = await ProfileManager.getSession(profileName) !== null;
1196
1469
  const config = await ProfileManager.getConfig();
1197
- outputJson({
1198
- ...p,
1199
- hasKey,
1200
- hasSession,
1201
- isDefault: profileName === config.defaultProfile
1202
- });
1470
+ const isDefault = profileName === config.defaultProfile;
1471
+ if (shouldOutputJson()) {
1472
+ outputJson({
1473
+ ...p,
1474
+ hasKey,
1475
+ hasSession,
1476
+ isDefault
1477
+ });
1478
+ } else {
1479
+ process.stdout.write(`${theme.heading(p.name)}${isDefault ? theme.success(" (default)") : ""}
1480
+ `);
1481
+ process.stdout.write(formatField("Host", p.host) + "\n");
1482
+ process.stdout.write(formatField("DID", p.did) + "\n");
1483
+ process.stdout.write(formatField("Space", p.spaceId || null) + "\n");
1484
+ process.stdout.write(formatField("Key", hasKey) + "\n");
1485
+ process.stdout.write(formatField("Session", hasSession) + "\n");
1486
+ process.stdout.write(formatField("Created", p.createdAt) + "\n");
1487
+ }
1203
1488
  } catch (error) {
1204
1489
  handleError(error);
1205
1490
  }
@@ -1540,9 +1825,458 @@ function registerVaultCommand(program2) {
1540
1825
  });
1541
1826
  }
1542
1827
 
1828
+ // src/commands/secrets.ts
1829
+ import { readFile as readFile4 } from "fs/promises";
1830
+ import { writeFile as writeFile4 } from "fs/promises";
1831
+ import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
1832
+ var SECRETS_PREFIX = "secrets/";
1833
+ async function readStdin3() {
1834
+ const chunks = [];
1835
+ for await (const chunk of process.stdin) {
1836
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1837
+ }
1838
+ return Buffer.concat(chunks);
1839
+ }
1840
+ function resolvePrivateKey2(options) {
1841
+ const key = options.privateKey || process.env.TC_PRIVATE_KEY;
1842
+ if (!key) {
1843
+ throw new CLIError(
1844
+ "AUTH_REQUIRED",
1845
+ "Private key required. Use --private-key <hex> or set TC_PRIVATE_KEY env var.",
1846
+ ExitCode.AUTH_REQUIRED
1847
+ );
1848
+ }
1849
+ return key;
1850
+ }
1851
+ async function unlockVault2(node, privateKey) {
1852
+ const signer = new PrivateKeySigner2(privateKey);
1853
+ const result = await node.vault.unlock(signer);
1854
+ if (result && !result.ok) {
1855
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1856
+ }
1857
+ }
1858
+ function registerSecretsCommand(program2) {
1859
+ const secrets = program2.command("secrets").description("Encrypted secrets management");
1860
+ 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) => {
1861
+ try {
1862
+ const globalOpts = cmd.optsWithGlobals();
1863
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1864
+ const privateKey = resolvePrivateKey2(options);
1865
+ const node = await ensureAuthenticated(ctx, { privateKey });
1866
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
1867
+ if (options.space) {
1868
+ throw new CLIError(
1869
+ "NOT_IMPLEMENTED",
1870
+ `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.`,
1871
+ ExitCode.ERROR
1872
+ );
1873
+ }
1874
+ const result = await withSpinner("Listing secrets...", () => node.vault.list({ prefix: SECRETS_PREFIX }));
1875
+ if (!result.ok) {
1876
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1877
+ }
1878
+ const keys = result.data.data ?? result.data;
1879
+ const keyList = Array.isArray(keys) ? keys : [];
1880
+ const secretNames = keyList.map(
1881
+ (k) => typeof k === "string" && k.startsWith(SECRETS_PREFIX) ? k.slice(SECRETS_PREFIX.length) : k
1882
+ );
1883
+ outputJson({
1884
+ secrets: secretNames,
1885
+ count: secretNames.length,
1886
+ ...options.space ? { space: options.space } : {}
1887
+ });
1888
+ } catch (error) {
1889
+ handleError(error);
1890
+ }
1891
+ });
1892
+ 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) => {
1893
+ try {
1894
+ const globalOpts = cmd.optsWithGlobals();
1895
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1896
+ const privateKey = resolvePrivateKey2(options);
1897
+ const node = await ensureAuthenticated(ctx, { privateKey });
1898
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
1899
+ const vaultKey = `${SECRETS_PREFIX}${name}`;
1900
+ const result = await withSpinner(`Getting secret ${name}...`, () => node.vault.get(vaultKey));
1901
+ if (!result.ok) {
1902
+ if (result.error.code === "NOT_FOUND") {
1903
+ throw new CLIError("NOT_FOUND", `Secret "${name}" not found`, ExitCode.NOT_FOUND);
1904
+ }
1905
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1906
+ }
1907
+ const data = result.data.data ?? result.data;
1908
+ let value;
1909
+ if (typeof data === "string") {
1910
+ try {
1911
+ const parsed = JSON.parse(data);
1912
+ value = parsed.value;
1913
+ } catch {
1914
+ value = data;
1915
+ }
1916
+ } else if (data instanceof Uint8Array) {
1917
+ try {
1918
+ const parsed = JSON.parse(Buffer.from(data).toString("utf-8"));
1919
+ value = parsed.value;
1920
+ } catch {
1921
+ value = Buffer.from(data).toString("utf-8");
1922
+ }
1923
+ } else {
1924
+ value = data.value ?? data;
1925
+ }
1926
+ if (options.output) {
1927
+ await writeFile4(options.output, value);
1928
+ outputJson({ name, written: options.output });
1929
+ return;
1930
+ }
1931
+ if (options.raw) {
1932
+ process.stdout.write(value);
1933
+ return;
1934
+ }
1935
+ outputJson({ name, value });
1936
+ } catch (error) {
1937
+ handleError(error);
1938
+ }
1939
+ });
1940
+ 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) => {
1941
+ try {
1942
+ const globalOpts = cmd.optsWithGlobals();
1943
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1944
+ const privateKey = resolvePrivateKey2(options);
1945
+ const node = await ensureAuthenticated(ctx, { privateKey });
1946
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
1947
+ let secretValue;
1948
+ const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
1949
+ if (sources.length === 0) {
1950
+ throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
1951
+ }
1952
+ if (sources.length > 1) {
1953
+ throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
1954
+ }
1955
+ if (options.file) {
1956
+ secretValue = await readFile4(options.file, "utf-8");
1957
+ } else if (options.stdin) {
1958
+ secretValue = (await readStdin3()).toString("utf-8");
1959
+ } else {
1960
+ secretValue = value;
1961
+ }
1962
+ const payload = JSON.stringify({
1963
+ value: secretValue,
1964
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1965
+ });
1966
+ const vaultKey = `${SECRETS_PREFIX}${name}`;
1967
+ const result = await withSpinner(`Storing secret ${name}...`, () => node.vault.put(vaultKey, payload));
1968
+ if (!result.ok) {
1969
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1970
+ }
1971
+ outputJson({ name, written: true });
1972
+ } catch (error) {
1973
+ handleError(error);
1974
+ }
1975
+ });
1976
+ 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) => {
1977
+ try {
1978
+ const globalOpts = cmd.optsWithGlobals();
1979
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1980
+ const privateKey = resolvePrivateKey2(options);
1981
+ const node = await ensureAuthenticated(ctx, { privateKey });
1982
+ await withSpinner("Unlocking vault...", () => unlockVault2(node, privateKey));
1983
+ const vaultKey = `${SECRETS_PREFIX}${name}`;
1984
+ const result = await withSpinner(`Deleting secret ${name}...`, () => node.vault.delete(vaultKey));
1985
+ if (!result.ok) {
1986
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
1987
+ }
1988
+ outputJson({ name, deleted: true });
1989
+ } catch (error) {
1990
+ handleError(error);
1991
+ }
1992
+ });
1993
+ secrets.command("manage").description("Open the TinyCloud Secrets Manager in your browser").action(async () => {
1994
+ try {
1995
+ const open = (await import("open")).default;
1996
+ await open("https://secrets.tinycloud.xyz");
1997
+ outputJson({ opened: "https://secrets.tinycloud.xyz" });
1998
+ } catch (error) {
1999
+ handleError(error);
2000
+ }
2001
+ });
2002
+ }
2003
+
2004
+ // src/commands/vars.ts
2005
+ import { readFile as readFile5 } from "fs/promises";
2006
+ import { writeFile as writeFile5 } from "fs/promises";
2007
+ var VARIABLES_PREFIX = "variables/";
2008
+ async function readStdin4() {
2009
+ const chunks = [];
2010
+ for await (const chunk of process.stdin) {
2011
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
2012
+ }
2013
+ return Buffer.concat(chunks);
2014
+ }
2015
+ function resolvePrivateKey3(options) {
2016
+ const key = options.privateKey || process.env.TC_PRIVATE_KEY;
2017
+ if (!key) {
2018
+ throw new CLIError(
2019
+ "AUTH_REQUIRED",
2020
+ "Private key required. Use --private-key <hex> or set TC_PRIVATE_KEY env var.",
2021
+ ExitCode.AUTH_REQUIRED
2022
+ );
2023
+ }
2024
+ return key;
2025
+ }
2026
+ function registerVarsCommand(program2) {
2027
+ const vars = program2.command("vars").description("Plaintext variable management");
2028
+ vars.command("list").description("List variables").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
2029
+ try {
2030
+ const globalOpts = cmd.optsWithGlobals();
2031
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2032
+ const privateKey = resolvePrivateKey3(options);
2033
+ const node = await ensureAuthenticated(ctx, { privateKey });
2034
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2035
+ const result = await withSpinner("Listing variables...", () => prefixedKv.list());
2036
+ if (!result.ok) {
2037
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2038
+ }
2039
+ const rawData = result.data.data ?? result.data;
2040
+ const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
2041
+ outputJson({
2042
+ variables: keyList,
2043
+ count: keyList.length
2044
+ });
2045
+ } catch (error) {
2046
+ handleError(error);
2047
+ }
2048
+ });
2049
+ 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) => {
2050
+ try {
2051
+ const globalOpts = cmd.optsWithGlobals();
2052
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2053
+ const privateKey = resolvePrivateKey3(options);
2054
+ const node = await ensureAuthenticated(ctx, { privateKey });
2055
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2056
+ const result = await withSpinner(`Getting variable ${name}...`, () => prefixedKv.get(name));
2057
+ if (!result.ok) {
2058
+ if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
2059
+ throw new CLIError("NOT_FOUND", `Variable "${name}" not found`, ExitCode.NOT_FOUND);
2060
+ }
2061
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2062
+ }
2063
+ const data = result.data.data;
2064
+ let value;
2065
+ if (typeof data === "string") {
2066
+ try {
2067
+ const parsed = JSON.parse(data);
2068
+ value = parsed.value;
2069
+ } catch {
2070
+ value = data;
2071
+ }
2072
+ } else if (data && typeof data === "object" && "value" in data) {
2073
+ value = data.value;
2074
+ } else {
2075
+ value = typeof data === "string" ? data : JSON.stringify(data);
2076
+ }
2077
+ if (options.output) {
2078
+ await writeFile5(options.output, value);
2079
+ outputJson({ name, written: options.output });
2080
+ return;
2081
+ }
2082
+ if (options.raw) {
2083
+ process.stdout.write(value);
2084
+ return;
2085
+ }
2086
+ outputJson({ name, value });
2087
+ } catch (error) {
2088
+ handleError(error);
2089
+ }
2090
+ });
2091
+ 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) => {
2092
+ try {
2093
+ const globalOpts = cmd.optsWithGlobals();
2094
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2095
+ const privateKey = resolvePrivateKey3(options);
2096
+ const node = await ensureAuthenticated(ctx, { privateKey });
2097
+ let varValue;
2098
+ const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
2099
+ if (sources.length === 0) {
2100
+ throw new CLIError("USAGE_ERROR", "Must provide a value, --file, or --stdin", ExitCode.USAGE_ERROR);
2101
+ }
2102
+ if (sources.length > 1) {
2103
+ throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
2104
+ }
2105
+ if (options.file) {
2106
+ varValue = await readFile5(options.file, "utf-8");
2107
+ } else if (options.stdin) {
2108
+ varValue = (await readStdin4()).toString("utf-8");
2109
+ } else {
2110
+ varValue = value;
2111
+ }
2112
+ const payload = {
2113
+ value: varValue,
2114
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2115
+ };
2116
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2117
+ const result = await withSpinner(`Setting variable ${name}...`, () => prefixedKv.put(name, payload));
2118
+ if (!result.ok) {
2119
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2120
+ }
2121
+ outputJson({ name, written: true });
2122
+ } catch (error) {
2123
+ handleError(error);
2124
+ }
2125
+ });
2126
+ 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) => {
2127
+ try {
2128
+ const globalOpts = cmd.optsWithGlobals();
2129
+ const ctx = await ProfileManager.resolveContext(globalOpts);
2130
+ const privateKey = resolvePrivateKey3(options);
2131
+ const node = await ensureAuthenticated(ctx, { privateKey });
2132
+ const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
2133
+ const result = await withSpinner(`Deleting variable ${name}...`, () => prefixedKv.delete(name));
2134
+ if (!result.ok) {
2135
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2136
+ }
2137
+ outputJson({ name, deleted: true });
2138
+ } catch (error) {
2139
+ handleError(error);
2140
+ }
2141
+ });
2142
+ }
2143
+
2144
+ // src/commands/doctor.ts
2145
+ function registerDoctorCommand(program2) {
2146
+ program2.command("doctor").description("Run diagnostic checks").action(async (_options, cmd) => {
2147
+ try {
2148
+ const globalOpts = cmd.optsWithGlobals();
2149
+ const checks = [];
2150
+ const nodeVersion = process.version;
2151
+ const nodeOk = parseInt(nodeVersion.slice(1)) >= 18;
2152
+ checks.push({ name: "Node.js", ok: nodeOk, detail: nodeVersion });
2153
+ let profileName = globalOpts.profile;
2154
+ let profileOk = false;
2155
+ let profileDetail = "";
2156
+ try {
2157
+ const config = await ProfileManager.getConfig();
2158
+ profileName = profileName || config.defaultProfile;
2159
+ const profile = await ProfileManager.getProfile(profileName);
2160
+ profileOk = true;
2161
+ profileDetail = `"${profileName}" at ${profile.host}`;
2162
+ } catch {
2163
+ profileDetail = profileName ? `"${profileName}" not found` : "no profiles configured";
2164
+ }
2165
+ checks.push({ name: "Profile", ok: profileOk, detail: profileDetail });
2166
+ let keyOk = false;
2167
+ let keyDetail = "";
2168
+ if (profileOk && profileName) {
2169
+ try {
2170
+ const key = await ProfileManager.getKey(profileName);
2171
+ keyOk = key !== null;
2172
+ if (keyOk) {
2173
+ const profile = await ProfileManager.getProfile(profileName);
2174
+ keyDetail = profile.did ? `${profile.did.slice(0, 20)}...` : "key found";
2175
+ } else {
2176
+ keyDetail = "no key \u2014 run tc init";
2177
+ }
2178
+ } catch {
2179
+ keyDetail = "error reading key";
2180
+ }
2181
+ } else {
2182
+ keyDetail = "skipped (no profile)";
2183
+ }
2184
+ checks.push({ name: "Key", ok: keyOk, detail: keyDetail });
2185
+ let sessionOk = false;
2186
+ let sessionDetail = "";
2187
+ if (profileOk && profileName) {
2188
+ try {
2189
+ const session = await ProfileManager.getSession(profileName);
2190
+ sessionOk = session !== null;
2191
+ sessionDetail = sessionOk ? "active" : "no session \u2014 run tc auth login";
2192
+ } catch {
2193
+ sessionDetail = "error reading session";
2194
+ }
2195
+ } else {
2196
+ sessionDetail = "skipped (no profile)";
2197
+ }
2198
+ checks.push({ name: "Session", ok: sessionOk, detail: sessionDetail });
2199
+ let nodeReachable = false;
2200
+ let nodeDetail = "";
2201
+ try {
2202
+ const host = profileOk && profileName ? (await ProfileManager.getProfile(profileName)).host : globalOpts.host || DEFAULT_HOST;
2203
+ const start = Date.now();
2204
+ const response = await fetch(`${host}/health`);
2205
+ const latency = Date.now() - start;
2206
+ nodeReachable = response.ok;
2207
+ nodeDetail = nodeReachable ? `${host} (${latency}ms)` : `${host} returned ${response.status}`;
2208
+ } catch (e) {
2209
+ nodeDetail = `unreachable \u2014 ${e instanceof Error ? e.message : "connection failed"}`;
2210
+ }
2211
+ checks.push({ name: "Node", ok: nodeReachable, detail: nodeDetail });
2212
+ let spaceOk = false;
2213
+ let spaceDetail = "";
2214
+ if (sessionOk && profileName) {
2215
+ try {
2216
+ const profile = await ProfileManager.getProfile(profileName);
2217
+ spaceOk = Boolean(profile.spaceId);
2218
+ spaceDetail = spaceOk ? `${profile.spaceId.slice(0, 16)}...` : "no space \u2014 run tc space create";
2219
+ } catch {
2220
+ spaceDetail = "error checking space";
2221
+ }
2222
+ } else {
2223
+ spaceDetail = "skipped (no session)";
2224
+ }
2225
+ checks.push({ name: "Space", ok: spaceOk, detail: spaceDetail });
2226
+ const result = {
2227
+ checks,
2228
+ healthy: checks.every((c) => c.ok)
2229
+ };
2230
+ if (shouldOutputJson()) {
2231
+ outputJson(result);
2232
+ } else {
2233
+ process.stderr.write(formatSection("Diagnostics") + "\n");
2234
+ for (const check of checks) {
2235
+ process.stdout.write(formatCheck(check.ok, check.name, check.detail) + "\n");
2236
+ }
2237
+ process.stdout.write("\n");
2238
+ if (result.healthy) {
2239
+ process.stdout.write(theme.success("All checks passed.") + "\n");
2240
+ } else {
2241
+ const failed = checks.filter((c) => !c.ok).length;
2242
+ process.stdout.write(theme.warn(`${failed} check${failed > 1 ? "s" : ""} need attention.`) + "\n");
2243
+ }
2244
+ }
2245
+ } catch (error) {
2246
+ handleError(error);
2247
+ }
2248
+ });
2249
+ }
2250
+
1543
2251
  // src/index.ts
1544
2252
  var program = new Command();
1545
- program.name("tc").description("TinyCloud CLI").version("0.1.0").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");
2253
+ program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version("0.1.0").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");
2254
+ program.hook("preAction", async (thisCommand) => {
2255
+ const opts = thisCommand.optsWithGlobals();
2256
+ if (!opts.quiet) {
2257
+ emitBanner("0.1.1");
2258
+ }
2259
+ const commandName = thisCommand.name();
2260
+ const parentName = thisCommand.parent?.name();
2261
+ const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
2262
+ const skipGuard = ["tc", "init", "doctor", "completion", "help"].includes(commandName) || fullCommand === "profile create";
2263
+ if (!skipGuard && !opts.quiet && isInteractive()) {
2264
+ try {
2265
+ const config = await ProfileManager.getConfig();
2266
+ const profileName = opts.profile || config.defaultProfile;
2267
+ const hasProfile = await ProfileManager.profileExists(profileName);
2268
+ if (!hasProfile) {
2269
+ process.stderr.write(theme.warn("\u26A0 No profile configured.") + " " + theme.muted("Run: tc init") + "\n\n");
2270
+ } else {
2271
+ const key = await ProfileManager.getKey(profileName);
2272
+ if (!key) {
2273
+ process.stderr.write(theme.warn("\u26A0 No key found.") + " " + theme.muted("Run: tc init") + "\n\n");
2274
+ }
2275
+ }
2276
+ } catch {
2277
+ }
2278
+ }
2279
+ });
1546
2280
  registerInitCommand(program);
1547
2281
  registerAuthCommand(program);
1548
2282
  registerKvCommand(program);
@@ -1553,6 +2287,24 @@ registerNodeCommand(program);
1553
2287
  registerProfileCommand(program);
1554
2288
  registerCompletionCommand(program);
1555
2289
  registerVaultCommand(program);
2290
+ registerSecretsCommand(program);
2291
+ registerVarsCommand(program);
2292
+ registerDoctorCommand(program);
2293
+ program.addHelpText("afterAll", () => {
2294
+ if (!process.stdout.isTTY) return "";
2295
+ return `
2296
+ ${theme.heading("Examples:")}
2297
+ ${theme.command("tc init")} ${theme.muted("Set up a profile and generate keys")}
2298
+ ${theme.command("tc auth login")} ${theme.muted("Authenticate via browser")}
2299
+ ${theme.command('tc kv put greeting "Hello"')} ${theme.muted("Store a value")}
2300
+ ${theme.command("tc kv list")} ${theme.muted("List all keys")}
2301
+ ${theme.command("tc delegation create --to did:pkh:...")} ${theme.muted("Grant access to another user")}
2302
+ ${theme.command("tc space list")} ${theme.muted("Show your spaces")}
2303
+
2304
+ ${theme.muted("Docs:")} ${theme.accent("https://docs.tinycloud.xyz/cli")}
2305
+ ${theme.muted("Repo:")} ${theme.accent("https://github.com/tinycloudlabs/web-sdk")}
2306
+ `;
2307
+ });
1556
2308
  try {
1557
2309
  await program.parseAsync(process.argv);
1558
2310
  } catch (error) {