@tuskydp/cli 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/src/commands/account.d.ts.map +1 -1
  3. package/dist/src/commands/account.js +0 -1
  4. package/dist/src/commands/account.js.map +1 -1
  5. package/dist/src/commands/auth.d.ts.map +1 -1
  6. package/dist/src/commands/auth.js +8 -5
  7. package/dist/src/commands/auth.js.map +1 -1
  8. package/dist/src/commands/download.d.ts +1 -0
  9. package/dist/src/commands/download.d.ts.map +1 -1
  10. package/dist/src/commands/download.js +35 -22
  11. package/dist/src/commands/download.js.map +1 -1
  12. package/dist/src/commands/export.d.ts +9 -24
  13. package/dist/src/commands/export.d.ts.map +1 -1
  14. package/dist/src/commands/export.js +31 -59
  15. package/dist/src/commands/export.js.map +1 -1
  16. package/dist/src/commands/files.d.ts.map +1 -1
  17. package/dist/src/commands/files.js +91 -12
  18. package/dist/src/commands/files.js.map +1 -1
  19. package/dist/src/commands/folder.d.ts +3 -0
  20. package/dist/src/commands/folder.d.ts.map +1 -0
  21. package/dist/src/commands/folder.js +151 -0
  22. package/dist/src/commands/folder.js.map +1 -0
  23. package/dist/src/commands/mcp.d.ts.map +1 -1
  24. package/dist/src/commands/mcp.js +15 -9
  25. package/dist/src/commands/mcp.js.map +1 -1
  26. package/dist/src/commands/rehydrate.d.ts +1 -0
  27. package/dist/src/commands/rehydrate.d.ts.map +1 -1
  28. package/dist/src/commands/rehydrate.js +15 -7
  29. package/dist/src/commands/rehydrate.js.map +1 -1
  30. package/dist/src/commands/sui.d.ts +3 -0
  31. package/dist/src/commands/sui.d.ts.map +1 -0
  32. package/dist/src/commands/sui.js +64 -0
  33. package/dist/src/commands/sui.js.map +1 -0
  34. package/dist/src/commands/trash.d.ts +3 -0
  35. package/dist/src/commands/trash.d.ts.map +1 -0
  36. package/dist/src/commands/trash.js +109 -0
  37. package/dist/src/commands/trash.js.map +1 -0
  38. package/dist/src/commands/upload.d.ts +4 -0
  39. package/dist/src/commands/upload.d.ts.map +1 -1
  40. package/dist/src/commands/upload.js +82 -27
  41. package/dist/src/commands/upload.js.map +1 -1
  42. package/dist/src/commands/vault.d.ts.map +1 -1
  43. package/dist/src/commands/vault.js +2 -24
  44. package/dist/src/commands/vault.js.map +1 -1
  45. package/dist/src/commands/wallet.d.ts +3 -0
  46. package/dist/src/commands/wallet.d.ts.map +1 -0
  47. package/dist/src/commands/wallet.js +126 -0
  48. package/dist/src/commands/wallet.js.map +1 -0
  49. package/dist/src/commands/webhook.d.ts +3 -0
  50. package/dist/src/commands/webhook.d.ts.map +1 -0
  51. package/dist/src/commands/webhook.js +172 -0
  52. package/dist/src/commands/webhook.js.map +1 -0
  53. package/dist/src/config.d.ts +2 -2
  54. package/dist/src/config.d.ts.map +1 -1
  55. package/dist/src/config.js +2 -3
  56. package/dist/src/config.js.map +1 -1
  57. package/dist/src/index.js +19 -9
  58. package/dist/src/index.js.map +1 -1
  59. package/dist/src/lib/resolve.d.ts.map +1 -1
  60. package/dist/src/lib/resolve.js +4 -5
  61. package/dist/src/lib/resolve.js.map +1 -1
  62. package/dist/src/mcp/context.d.ts +1 -9
  63. package/dist/src/mcp/context.d.ts.map +1 -1
  64. package/dist/src/mcp/context.js +1 -2
  65. package/dist/src/mcp/context.js.map +1 -1
  66. package/dist/src/mcp/server.d.ts.map +1 -1
  67. package/dist/src/mcp/server.js +2 -59
  68. package/dist/src/mcp/server.js.map +1 -1
  69. package/dist/src/mcp/tools/account.d.ts.map +1 -1
  70. package/dist/src/mcp/tools/account.js +1 -3
  71. package/dist/src/mcp/tools/account.js.map +1 -1
  72. package/dist/src/mcp/tools/files.d.ts +2 -3
  73. package/dist/src/mcp/tools/files.d.ts.map +1 -1
  74. package/dist/src/mcp/tools/files.js +46 -49
  75. package/dist/src/mcp/tools/files.js.map +1 -1
  76. package/dist/src/mcp/tools/vaults.js +2 -2
  77. package/dist/src/mcp/tools/vaults.js.map +1 -1
  78. package/dist/src/seal.d.ts +16 -0
  79. package/dist/src/seal.d.ts.map +1 -1
  80. package/dist/src/seal.js +23 -0
  81. package/dist/src/seal.js.map +1 -1
  82. package/dist/src/tui/files-panel.d.ts +31 -2
  83. package/dist/src/tui/files-panel.d.ts.map +1 -1
  84. package/dist/src/tui/files-panel.js +119 -13
  85. package/dist/src/tui/files-panel.js.map +1 -1
  86. package/dist/src/tui/index.d.ts.map +1 -1
  87. package/dist/src/tui/index.js +252 -48
  88. package/dist/src/tui/index.js.map +1 -1
  89. package/dist/src/tui/overview.d.ts.map +1 -1
  90. package/dist/src/tui/overview.js +21 -9
  91. package/dist/src/tui/overview.js.map +1 -1
  92. package/dist/src/tui/trash-screen.d.ts +4 -0
  93. package/dist/src/tui/trash-screen.d.ts.map +1 -0
  94. package/dist/src/tui/trash-screen.js +190 -0
  95. package/dist/src/tui/trash-screen.js.map +1 -0
  96. package/dist/src/tui/vaults-panel.d.ts +8 -0
  97. package/dist/src/tui/vaults-panel.d.ts.map +1 -1
  98. package/dist/src/tui/vaults-panel.js +45 -6
  99. package/dist/src/tui/vaults-panel.js.map +1 -1
  100. package/dist/src/version.d.ts +2 -0
  101. package/dist/src/version.d.ts.map +1 -0
  102. package/dist/src/version.js +21 -0
  103. package/dist/src/version.js.map +1 -0
  104. package/package.json +3 -3
  105. package/src/__tests__/seal.test.ts +7 -54
  106. package/src/commands/account.ts +0 -1
  107. package/src/commands/auth.ts +7 -5
  108. package/src/commands/download.ts +38 -28
  109. package/src/commands/export.ts +37 -81
  110. package/src/commands/files.ts +95 -11
  111. package/src/commands/folder.ts +169 -0
  112. package/src/commands/mcp.ts +16 -10
  113. package/src/commands/rehydrate.ts +15 -8
  114. package/src/commands/sui.ts +69 -0
  115. package/src/commands/trash.ts +121 -0
  116. package/src/commands/upload.ts +98 -31
  117. package/src/commands/vault.ts +2 -23
  118. package/src/commands/wallet.ts +183 -0
  119. package/src/commands/webhook.ts +193 -0
  120. package/src/config.ts +3 -4
  121. package/src/index.ts +19 -10
  122. package/src/lib/resolve.ts +3 -4
  123. package/src/mcp/context.ts +1 -11
  124. package/src/mcp/server.ts +2 -70
  125. package/src/mcp/tools/account.ts +1 -3
  126. package/src/mcp/tools/files.ts +50 -63
  127. package/src/mcp/tools/vaults.ts +3 -3
  128. package/src/seal.ts +34 -1
  129. package/src/tui/files-panel.ts +140 -14
  130. package/src/tui/index.ts +264 -52
  131. package/src/tui/overview.ts +20 -9
  132. package/src/tui/trash-screen.ts +203 -0
  133. package/src/tui/vaults-panel.ts +55 -6
  134. package/src/version.ts +21 -0
  135. package/vitest.config.ts +1 -0
  136. package/dist/src/client.d.ts +0 -120
  137. package/dist/src/client.d.ts.map +0 -1
  138. package/dist/src/client.js +0 -152
  139. package/dist/src/client.js.map +0 -1
  140. package/dist/src/commands/decrypt.d.ts +0 -15
  141. package/dist/src/commands/decrypt.d.ts.map +0 -1
  142. package/dist/src/commands/decrypt.js +0 -224
  143. package/dist/src/commands/decrypt.js.map +0 -1
  144. package/dist/src/commands/encryption.d.ts +0 -3
  145. package/dist/src/commands/encryption.d.ts.map +0 -1
  146. package/dist/src/commands/encryption.js +0 -254
  147. package/dist/src/commands/encryption.js.map +0 -1
  148. package/dist/src/crypto.d.ts +0 -16
  149. package/dist/src/crypto.d.ts.map +0 -1
  150. package/dist/src/crypto.js +0 -95
  151. package/dist/src/crypto.js.map +0 -1
  152. package/dist/src/lib/keyring.d.ts +0 -4
  153. package/dist/src/lib/keyring.d.ts.map +0 -1
  154. package/dist/src/lib/keyring.js +0 -49
  155. package/dist/src/lib/keyring.js.map +0 -1
  156. package/src/__tests__/crypto.test.ts +0 -315
  157. package/src/commands/decrypt.ts +0 -276
  158. package/src/commands/encryption.ts +0 -305
  159. package/src/crypto.ts +0 -130
  160. package/src/lib/keyring.ts +0 -50
package/src/tui/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import blessed from 'blessed';
2
- import { readFileSync, statSync, readdirSync } from 'fs';
2
+ import { readFileSync, writeFileSync, statSync, readdirSync } from 'fs';
3
+ import { randomUUID } from 'crypto';
3
4
  import { basename, join, resolve } from 'path';
4
5
 
5
6
  function collectFiles(dir: string): string[] {
@@ -15,19 +16,19 @@ function collectFiles(dir: string): string[] {
15
16
  import { lookup } from 'mime-types';
16
17
  import { createSDKClient } from '../sdk.js';
17
18
  import { cliConfig, getApiUrl } from '../config.js';
18
- import { encryptBuffer } from '../crypto.js';
19
- import { loadMasterKey } from '../lib/keyring.js';
19
+ import { isSealConfigured, sealEncrypt, sealDecrypt, getSuiKeypair } from '../seal.js';
20
20
  import { showAuthScreen } from './auth-screen.js';
21
21
  import { VaultsPanel, VaultItem } from './vaults-panel.js';
22
22
  import { FilesPanel } from './files-panel.js';
23
23
  import { StatusBar } from './status-bar.js';
24
24
  import { showOverview } from './overview.js';
25
+ import { showTrashScreen } from './trash-screen.js';
25
26
  import { confirmDialog, textInputDialog, selectDialog, detailPopup, fileBrowserDialog } from './dialogs.js';
26
27
  import { formatBytes, formatDate, formatRow, showError, showMessage, statusColor, statusColorClose, getCurrentTheme, cycleTheme, Theme } from './helpers.js';
27
28
 
28
29
  function formatDetailTable(rows: [string, string][]): string {
29
30
  const labelW = Math.max(...rows.map(([l]) => l.length));
30
- return rows.map(([label, value]) => ` {bold}${label.padEnd(labelW)}{/bold} ${value}`).join('\n');
31
+ return rows.map(([label, value]) => ` {bold}${label.padEnd(labelW)}{/bold} | ${value}`).join('\n');
31
32
  }
32
33
 
33
34
  type FocusTarget = 'vaults' | 'files';
@@ -74,6 +75,7 @@ export async function launchTui() {
74
75
 
75
76
  const statusBar = new StatusBar(screen);
76
77
  const vaultsPanel = new VaultsPanel(screen, sdk, leftBox, (vault) => {
78
+ filesPanel.resetFolderNav();
77
79
  filesPanel.loadForVault(vault.id, vault.name);
78
80
  });
79
81
  const filesPanel = new FilesPanel(screen, sdk, rightBox);
@@ -85,11 +87,11 @@ export async function launchTui() {
85
87
  if (target === 'vaults') {
86
88
  vaultsPanel.focus();
87
89
  filesPanel.blur();
88
- statusBar.setHints('n:new ←→:switch u:upload o:overview ?:help');
90
+ statusBar.setHints('n:new <->:switch u:upload o:overview T:trash ?:help');
89
91
  } else {
90
92
  filesPanel.focus();
91
93
  vaultsPanel.blur();
92
- statusBar.setHints('u:upload d:delete r:retry ←→:switch Enter:detail ?:help');
94
+ statusBar.setHints('d:download u:upload f:folder R:remove r:retry T:trash ?:help');
93
95
  }
94
96
  }
95
97
 
@@ -124,28 +126,74 @@ export async function launchTui() {
124
126
  }
125
127
  });
126
128
 
129
+ // ── Create vault (n) — now with shared option ──────────────────────
127
130
  screen.key(['n'], async () => {
128
131
  const name = await textInputDialog(screen, 'Vault name');
129
132
  if (!name) {
130
133
  setFocus(currentFocus);
131
134
  return;
132
135
  }
133
- const visIdx = await selectDialog(screen, 'Visibility', ['private', 'public']);
136
+ const visIdx = await selectDialog(screen, 'Visibility', ['public', 'shared']);
134
137
  if (visIdx < 0) {
135
138
  setFocus(currentFocus);
136
139
  return;
137
140
  }
138
- const visibility = visIdx === 0 ? 'private' as const : 'public' as const;
141
+ const visMap = ['public', 'shared'] as const;
142
+ const visibility = visMap[visIdx];
143
+
139
144
  try {
140
- await sdk.vaults.create({ name, visibility });
145
+ if (visibility === 'shared') {
146
+ // Validate Sui address is linked
147
+ const acct = await sdk.account.get();
148
+ if (!(acct as any).suiAddress) {
149
+ showError(screen, 'Link a Sui address first: tusky account link-sui <addr>');
150
+ setFocus(currentFocus);
151
+ return;
152
+ }
153
+ }
154
+ const created = await sdk.vaults.create({ name, visibility });
141
155
  await vaultsPanel.load();
142
- showMessage(screen, `Vault "${name}" created`);
156
+ let msg = `Vault "${name}" created [${visibility}]`;
157
+ if (visibility === 'shared' && created.sealAllowlistObjectId) {
158
+ msg += `\nSEAL allowlist: ${created.sealAllowlistObjectId}`;
159
+ }
160
+ showMessage(screen, msg);
161
+ } catch (err: any) {
162
+ showError(screen, err.message);
163
+ }
164
+ setFocus(currentFocus);
165
+ });
166
+
167
+ // ── Create folder (f) ──────────────────────────────────────────────
168
+ screen.key(['f'], async () => {
169
+ if (currentFocus !== 'files') return;
170
+ const vaultId = filesPanel.getCurrentVaultId();
171
+ if (!vaultId) {
172
+ showError(screen, 'Select a vault first');
173
+ return;
174
+ }
175
+
176
+ const name = await textInputDialog(screen, 'Folder name');
177
+ if (!name) {
178
+ setFocus(currentFocus);
179
+ return;
180
+ }
181
+
182
+ try {
183
+ await sdk.folders.create({
184
+ vaultId,
185
+ parentId: filesPanel.getCurrentFolderId() || undefined,
186
+ name,
187
+ });
188
+ await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
189
+ showMessage(screen, `Folder "${name}" created`);
143
190
  } catch (err: any) {
144
191
  showError(screen, err.message);
145
192
  }
146
193
  setFocus(currentFocus);
147
194
  });
148
195
 
196
+ // ── Upload (u) — with SEAL support and folder awareness ────────────
149
197
  screen.key(['u'], async () => {
150
198
  const vaultId = filesPanel.getCurrentVaultId();
151
199
  if (!vaultId) {
@@ -175,13 +223,12 @@ export async function launchTui() {
175
223
 
176
224
  try {
177
225
  const vault = await sdk.vaults.get(vaultId);
178
- const isPrivate = vault.visibility === 'private';
179
- let masterKey: Buffer | null = null;
226
+ const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
180
227
 
181
- if (isPrivate) {
182
- masterKey = loadMasterKey();
183
- if (!masterKey) {
184
- showError(screen, 'Encryption session not unlocked. Run: tusky encryption unlock');
228
+ if (isShared) {
229
+ const keypair = getSuiKeypair();
230
+ if (!keypair) {
231
+ showError(screen, 'TUSKYDP_SUI_PRIVATE_KEY env var required for shared vault uploads');
185
232
  setFocus(currentFocus);
186
233
  return;
187
234
  }
@@ -189,6 +236,7 @@ export async function launchTui() {
189
236
 
190
237
  let successCount = 0;
191
238
  const total = filePaths.length;
239
+ const currentFolderId = filesPanel.getCurrentFolderId();
192
240
 
193
241
  for (const filePath of filePaths) {
194
242
  const fileName = basename(filePath);
@@ -201,20 +249,19 @@ export async function launchTui() {
201
249
 
202
250
  let uploadBody: Buffer;
203
251
  let encryptionMeta: {
204
- wrappedKey?: string;
205
- encryptionIv?: string;
206
- plaintextSizeBytes?: number;
207
- plaintextChecksumSha256?: string;
252
+ sealIdentity?: string;
253
+ sealEncryptedObject?: string;
208
254
  } = {};
209
255
 
210
- if (isPrivate && masterKey) {
211
- const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
212
- uploadBody = ciphertext;
256
+ if (isShared) {
257
+ statusBar.setHints(`SEAL encrypting ${successCount + 1}/${total}: ${fileName}...`);
258
+ screen.render();
259
+ const fileNonce = randomUUID();
260
+ const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
261
+ uploadBody = Buffer.from(sealResult.encryptedData);
213
262
  encryptionMeta = {
214
- wrappedKey,
215
- encryptionIv: iv,
216
- plaintextSizeBytes: stat.size,
217
- plaintextChecksumSha256: plaintextChecksum,
263
+ sealIdentity: sealResult.sealIdentity,
264
+ sealEncryptedObject: sealResult.sealEncryptedObject,
218
265
  };
219
266
  } else {
220
267
  uploadBody = fileBuffer;
@@ -225,6 +272,7 @@ export async function launchTui() {
225
272
  mimeType,
226
273
  sizeBytes: uploadBody.length,
227
274
  vaultId,
275
+ ...(currentFolderId ? { folderId: currentFolderId } : {}),
228
276
  ...encryptionMeta,
229
277
  });
230
278
 
@@ -240,7 +288,8 @@ export async function launchTui() {
240
288
  successCount++;
241
289
  }
242
290
 
243
- showMessage(screen, `Uploaded ${successCount} file(s)`);
291
+ const label = isShared ? 'shared/SEAL' : 'public';
292
+ showMessage(screen, `Uploaded ${successCount} file(s) [${label}]`);
244
293
  await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
245
294
  await vaultsPanel.load();
246
295
  } catch (err: any) {
@@ -249,10 +298,98 @@ export async function launchTui() {
249
298
  setFocus(currentFocus);
250
299
  });
251
300
 
252
- screen.key(['d', 'delete'], async () => {
301
+ // ── Download (d) download selected file to local disk ─────────────
302
+ screen.key(['d'], async () => {
303
+ if (currentFocus !== 'files') return;
304
+ const file = filesPanel.getSelected();
305
+ if (!file) {
306
+ showError(screen, 'Select a file to download');
307
+ setFocus(currentFocus);
308
+ return;
309
+ }
310
+
311
+ statusBar.setHints(`Downloading "${file.name}"...`);
312
+ screen.render();
313
+
314
+ try {
315
+ // Get download URL + encryption info
316
+ let dlResponse = await sdk.files.getDownloadUrl(file.id);
317
+ let { downloadUrl, status: dlStatus, encryption } = dlResponse;
318
+
319
+ // Poll if rehydrating from cold storage
320
+ if (dlStatus === 'rehydrating') {
321
+ statusBar.setHints(`Rehydrating "${file.name}" from Walrus...`);
322
+ screen.render();
323
+ for (let i = 0; i < 30; i++) {
324
+ await new Promise(r => setTimeout(r, 2000));
325
+ const check = await sdk.files.getDownloadUrl(file.id);
326
+ if (check.status === 'ready') {
327
+ downloadUrl = check.downloadUrl;
328
+ encryption = check.encryption;
329
+ dlStatus = 'ready';
330
+ break;
331
+ }
332
+ }
333
+ if (dlStatus !== 'ready') {
334
+ showError(screen, 'File rehydration timed out. Try again later.');
335
+ setFocus(currentFocus);
336
+ return;
337
+ }
338
+ }
339
+
340
+ if (!downloadUrl) {
341
+ showError(screen, 'No download URL available.');
342
+ setFocus(currentFocus);
343
+ return;
344
+ }
345
+
346
+ // Download the bytes
347
+ statusBar.setHints(`Downloading "${file.name}"...`);
348
+ screen.render();
349
+ const response = await fetch(downloadUrl);
350
+ if (!response.ok) throw new Error(`Download failed: ${response.status}`);
351
+ const arrayBuf = await response.arrayBuffer();
352
+ let fileBuffer: Buffer = Buffer.from(new Uint8Array(arrayBuf));
353
+
354
+ // Decrypt if needed — SEAL-encrypted (shared vault)
355
+ if (encryption?.encrypted) {
356
+ if ('type' in encryption && encryption.type === 'seal') {
357
+ statusBar.setHints(`SEAL decrypting "${file.name}"...`);
358
+ screen.render();
359
+ const keypair = getSuiKeypair();
360
+ if (!keypair) {
361
+ showError(screen, 'TUSKYDP_SUI_PRIVATE_KEY env var required for shared vault files');
362
+ setFocus(currentFocus);
363
+ return;
364
+ }
365
+ const fileInfo = await sdk.files.get(file.id);
366
+ const vault = await sdk.vaults.get(fileInfo.vaultId);
367
+ const decrypted = await sealDecrypt(new Uint8Array(fileBuffer), vault, keypair);
368
+ fileBuffer = Buffer.from(decrypted);
369
+ }
370
+ }
371
+
372
+ // Write to disk
373
+ const outputPath = join(process.cwd(), file.name);
374
+ writeFileSync(outputPath, fileBuffer);
375
+
376
+ showMessage(screen, `Downloaded "${file.name}" (${formatBytes(fileBuffer.length)})\n-> ${outputPath}`);
377
+ } catch (err: any) {
378
+ showError(screen, err.message);
379
+ }
380
+ setFocus(currentFocus);
381
+ });
382
+
383
+ // ── Remove (R = Shift+r) — delete selected item ───────────────────
384
+ screen.key(['S-r'], async () => {
253
385
  if (currentFocus === 'vaults') {
254
386
  const vault = vaultsPanel.getSelected();
255
387
  if (!vault) return;
388
+ if (vault.isSharedMembership) {
389
+ showError(screen, 'Cannot delete a vault you are a member of (not owner)');
390
+ setFocus(currentFocus);
391
+ return;
392
+ }
256
393
  const confirmed = await confirmDialog(screen, `Delete vault "${vault.name}" and ALL its files?`);
257
394
  if (confirmed) {
258
395
  try {
@@ -266,27 +403,45 @@ export async function launchTui() {
266
403
  }
267
404
  setFocus(currentFocus);
268
405
  } else {
269
- const file = filesPanel.getSelected();
270
- if (!file) return;
271
- const confirmed = await confirmDialog(screen, `Delete file "${file.name}"?`);
272
- if (confirmed) {
273
- try {
274
- await sdk.files.delete(file.id);
275
- const vaultId = filesPanel.getCurrentVaultId();
276
- if (vaultId) {
277
- await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
278
- await vaultsPanel.load();
406
+ const item = filesPanel.getSelectedItem();
407
+ if (!item) return;
408
+
409
+ if (item.type === 'folder') {
410
+ const confirmed = await confirmDialog(screen, `Delete folder "${item.folder.name}"?`);
411
+ if (confirmed) {
412
+ try {
413
+ await sdk.folders.delete(item.folder.id);
414
+ const vaultId = filesPanel.getCurrentVaultId();
415
+ if (vaultId) {
416
+ await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
417
+ }
418
+ showMessage(screen, `Folder "${item.folder.name}" deleted`);
419
+ } catch (err: any) {
420
+ showError(screen, err.message);
421
+ }
422
+ }
423
+ } else if (item.type === 'file') {
424
+ const confirmed = await confirmDialog(screen, `Delete file "${item.file.name}"?`);
425
+ if (confirmed) {
426
+ try {
427
+ await sdk.files.delete(item.file.id);
428
+ const vaultId = filesPanel.getCurrentVaultId();
429
+ if (vaultId) {
430
+ await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
431
+ await vaultsPanel.load();
432
+ }
433
+ showMessage(screen, `File "${item.file.name}" moved to trash`);
434
+ } catch (err: any) {
435
+ showError(screen, err.message);
279
436
  }
280
- showMessage(screen, `File "${file.name}" deleted`);
281
- } catch (err: any) {
282
- showError(screen, err.message);
283
437
  }
284
438
  }
285
439
  setFocus(currentFocus);
286
440
  }
287
441
  });
288
442
 
289
- screen.key(['enter'], () => {
443
+ // ── Enter / Space — detail popup or folder navigation ──────────────
444
+ screen.key(['enter', 'space'], async () => {
290
445
  if (currentFocus === 'vaults') {
291
446
  const vault = vaultsPanel.getSelected();
292
447
  if (vault) {
@@ -298,29 +453,73 @@ export async function launchTui() {
298
453
  ['Files', String(vault.fileCount)],
299
454
  ['Size', formatBytes(vault.totalSizeBytes)],
300
455
  ];
456
+ if (vault.description) {
457
+ rows.push(['Description', vault.description]);
458
+ }
459
+ if (vault.createdAt) {
460
+ rows.push(['Created', formatDate(vault.createdAt)]);
461
+ }
462
+ if (vault.isSharedMembership) {
463
+ rows.push(['Your Role', vault.memberRole || 'member']);
464
+ }
465
+ if (vault.visibility === 'shared') {
466
+ rows.push(['', '']);
467
+ rows.push(['{cyan-fg}SEAL Config{/cyan-fg}', '']);
468
+ rows.push(['Allowlist', vault.sealAllowlistObjectId || 'N/A']);
469
+ rows.push(['Package', vault.sealPackageId || 'N/A']);
470
+ rows.push(['Threshold', String(vault.sealThreshold ?? 'N/A')]);
471
+ rows.push(['Key Servers', String(vault.sealKeyServerIds?.length ?? 0)]);
472
+ }
301
473
  detailPopup(screen, vault.name, formatDetailTable(rows));
302
474
  }
303
475
  } else {
304
- const file = filesPanel.getSelected();
305
- if (file) {
476
+ const item = filesPanel.getSelectedItem();
477
+ if (!item) return;
478
+
479
+ if (item.type === 'parent') {
480
+ // Navigate up
481
+ await filesPanel.navigateUp();
482
+ setFocus(currentFocus);
483
+ return;
484
+ }
485
+
486
+ if (item.type === 'folder') {
487
+ // Navigate into folder
488
+ await filesPanel.navigateToFolder(item.folder.id, item.folder.name);
489
+ setFocus(currentFocus);
490
+ return;
491
+ }
492
+
493
+ if (item.type === 'file') {
494
+ const file = item.file;
306
495
  const open = statusColor(file.status);
307
496
  const close = statusColorClose(file.status);
308
497
  const rows: [string, string][] = [
309
498
  ['Name', file.name],
310
499
  ['ID', file.id],
311
500
  ['MIME', file.mimeType],
312
- ['Size', formatBytes(file.plaintextSizeBytes ?? file.sizeBytes)],
501
+ ['Size', formatBytes(file.sizeBytes)],
313
502
  ['Status', `${open}${file.status}${close}`],
314
503
  ['Encrypted', file.encrypted ? 'Yes' : 'No'],
315
504
  ['Uploaded', file.createdAt ? formatDate(file.createdAt) : 'N/A'],
316
505
  ['Blob ID', file.walrusBlobId || 'N/A'],
317
506
  ['Blob Object', file.walrusBlobObjectId || 'N/A'],
318
507
  ];
508
+ if (file.folderId) {
509
+ rows.push(['Folder ID', file.folderId]);
510
+ }
511
+ if (file.autoRenew) {
512
+ rows.push(['Auto-Renew', '{green-fg}On{/green-fg}']);
513
+ }
514
+ if (file.ppuEpochEnd) {
515
+ rows.push(['PPU Epoch', formatDate(file.ppuEpochEnd)]);
516
+ }
319
517
  detailPopup(screen, file.name, formatDetailTable(rows));
320
518
  }
321
519
  }
322
520
  });
323
521
 
522
+ // ── Retry (r) ──────────────────────────────────────────────────────
324
523
  screen.key(['r'], async () => {
325
524
  if (currentFocus === 'files') {
326
525
  const file = filesPanel.getSelected();
@@ -343,11 +542,21 @@ export async function launchTui() {
343
542
  }
344
543
  });
345
544
 
545
+ // ── Overview (o) ───────────────────────────────────────────────────
346
546
  screen.key(['o'], async () => {
347
547
  await showOverview(screen, sdk);
348
548
  setFocus(currentFocus);
349
549
  });
350
550
 
551
+ // ── Trash (T = Shift+t) ───────────────────────────────────────────
552
+ screen.key(['S-t'], async () => {
553
+ await showTrashScreen(screen, sdk);
554
+ // Reload vaults panel in case restores happened
555
+ await vaultsPanel.load();
556
+ setFocus(currentFocus);
557
+ });
558
+
559
+ // ── Logout (L = Shift+l) ──────────────────────────────────────────
351
560
  screen.key(['S-l'], async () => {
352
561
  const confirmed = await confirmDialog(screen, 'Log out?');
353
562
  if (confirmed) {
@@ -388,19 +597,22 @@ export async function launchTui() {
388
597
  '{bold}{cyan-fg}Tusky TUI — Keyboard Shortcuts{/cyan-fg}{/bold}',
389
598
  '',
390
599
  '{bold}Navigation{/bold}',
391
- ' ←/→ Switch between vaults/files',
600
+ ' <-/-> Switch between vaults/files',
392
601
  ' Tab Switch between vaults/files',
393
602
  ' Up/Down Navigate list',
603
+ ' Enter Open folder / view details',
394
604
  '',
395
605
  '{bold}Actions{/bold}',
396
- ' n Create new vault',
397
- ' u Upload file to current vault',
398
- ' d/Delete Delete selected item',
399
- ' r Retry failed file',
400
- ' Space/Enter View details',
606
+ ' n Create new vault (public/shared)',
607
+ ' f Create folder in current vault',
608
+ ' u Upload file to current vault/folder',
609
+ ' d Download selected file',
610
+ ' R Remove selected item (trash for files)',
611
+ ' r Retry failed file sync',
401
612
  '',
402
613
  '{bold}Screens{/bold}',
403
614
  ' o Overview (account, storage, files)',
615
+ ' T Trash management (list, restore, delete)',
404
616
  ' t Cycle theme',
405
617
  ' L Logout',
406
618
  ' ? This help',
@@ -74,13 +74,24 @@ export async function showOverview(
74
74
 
75
75
  if (vaults.length > 0) {
76
76
  // Calculate column widths for vaults table
77
- const vaultRows = vaults.map((v) => ({
78
- name: v.name || '',
79
- vis: v.visibility === 'private' ? 'private' : 'public',
80
- visTag: v.visibility === 'private' ? '{yellow-fg}private{/yellow-fg}' : '{green-fg}public{/green-fg}',
81
- files: String(v.fileCount || 0),
82
- size: formatBytes(v.totalSizeBytes || 0),
83
- }));
77
+ const vaultRows = vaults.map((v) => {
78
+ let vis: string;
79
+ let visTag: string;
80
+ if (v.visibility === 'shared') {
81
+ vis = 'shared';
82
+ visTag = '{magenta-fg}shared{/magenta-fg}';
83
+ } else {
84
+ vis = 'public';
85
+ visTag = '{green-fg}public{/green-fg}';
86
+ }
87
+ return {
88
+ name: v.name || '',
89
+ vis,
90
+ visTag,
91
+ files: String(v.fileCount || 0),
92
+ size: formatBytes(v.totalSizeBytes || 0),
93
+ };
94
+ });
84
95
  const vNameW = Math.max(4, ...vaultRows.map((r) => r.name.length));
85
96
  const vVisW = 7; // "private" is longest
86
97
  const vFilesW = Math.max(5, ...vaultRows.map((r) => r.files.length));
@@ -105,7 +116,7 @@ export async function showOverview(
105
116
  // Calculate column widths for files table
106
117
  const fileRows = files.map((f) => ({
107
118
  name: f.name || '',
108
- size: formatBytes(f.plaintextSizeBytes ?? f.sizeBytes ?? 0),
119
+ size: formatBytes(f.sizeBytes ?? 0),
109
120
  status: f.status || '',
110
121
  statusTag: `${statusColor(f.status)}${f.status}${statusColorClose(f.status)}`,
111
122
  date: f.createdAt ? formatDate(f.createdAt) : 'N/A',
@@ -122,7 +133,7 @@ export async function showOverview(
122
133
  content += ` ${'Name'.padEnd(maxNameW)} ${'Size'.padStart(fSizeW)} ${'Status'.padEnd(fStatW)} ${'Date'.padEnd(fDateW)}\n`;
123
134
  content += ` ${'─'.repeat(maxNameW)} ${'─'.repeat(fSizeW)} ${'─'.repeat(fStatW)} ${'─'.repeat(fDateW)}\n`;
124
135
  for (const r of fileRows) {
125
- const truncName = r.name.length > maxNameW ? r.name.slice(0, maxNameW - 1) + '' : r.name;
136
+ const truncName = r.name.length > maxNameW ? r.name.slice(0, maxNameW - 1) + '...' : r.name;
126
137
  const statusPad = ' '.repeat(Math.max(0, fStatW - r.status.length));
127
138
  content += ` ${truncName.padEnd(maxNameW)} ${r.size.padStart(fSizeW)} ${r.statusTag}${statusPad} ${r.date}\n`;
128
139
  }