@tuskydp/cli 0.2.1 → 0.3.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/CHANGELOG.md +20 -0
- package/dist/src/commands/decrypt.d.ts.map +1 -1
- package/dist/src/commands/decrypt.js +53 -21
- package/dist/src/commands/decrypt.js.map +1 -1
- package/dist/src/commands/download.d.ts +1 -0
- package/dist/src/commands/download.d.ts.map +1 -1
- package/dist/src/commands/download.js +81 -11
- package/dist/src/commands/download.js.map +1 -1
- package/dist/src/commands/export.d.ts +6 -0
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +29 -17
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/files.d.ts.map +1 -1
- package/dist/src/commands/files.js +89 -10
- package/dist/src/commands/files.js.map +1 -1
- package/dist/src/commands/folder.d.ts +3 -0
- package/dist/src/commands/folder.d.ts.map +1 -0
- package/dist/src/commands/folder.js +151 -0
- package/dist/src/commands/folder.js.map +1 -0
- package/dist/src/commands/rehydrate.d.ts +1 -0
- package/dist/src/commands/rehydrate.d.ts.map +1 -1
- package/dist/src/commands/rehydrate.js +15 -7
- package/dist/src/commands/rehydrate.js.map +1 -1
- package/dist/src/commands/trash.d.ts +3 -0
- package/dist/src/commands/trash.d.ts.map +1 -0
- package/dist/src/commands/trash.js +109 -0
- package/dist/src/commands/trash.js.map +1 -0
- package/dist/src/commands/upload.d.ts +4 -0
- package/dist/src/commands/upload.d.ts.map +1 -1
- package/dist/src/commands/upload.js +104 -3
- package/dist/src/commands/upload.js.map +1 -1
- package/dist/src/commands/wallet.d.ts +3 -0
- package/dist/src/commands/wallet.d.ts.map +1 -0
- package/dist/src/commands/wallet.js +126 -0
- package/dist/src/commands/wallet.js.map +1 -0
- package/dist/src/commands/webhook.d.ts +3 -0
- package/dist/src/commands/webhook.d.ts.map +1 -0
- package/dist/src/commands/webhook.js +172 -0
- package/dist/src/commands/webhook.js.map +1 -0
- package/dist/src/crypto.d.ts +16 -0
- package/dist/src/crypto.d.ts.map +1 -1
- package/dist/src/crypto.js +26 -0
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/index.js +17 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +2 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/files.d.ts.map +1 -1
- package/dist/src/mcp/tools/files.js +40 -5
- package/dist/src/mcp/tools/files.js.map +1 -1
- package/dist/src/seal.d.ts +16 -0
- package/dist/src/seal.d.ts.map +1 -1
- package/dist/src/seal.js +23 -0
- package/dist/src/seal.js.map +1 -1
- package/dist/src/tui/files-panel.d.ts +31 -1
- package/dist/src/tui/files-panel.d.ts.map +1 -1
- package/dist/src/tui/files-panel.js +118 -11
- package/dist/src/tui/files-panel.js.map +1 -1
- package/dist/src/tui/index.d.ts.map +1 -1
- package/dist/src/tui/index.js +272 -33
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/overview.d.ts.map +1 -1
- package/dist/src/tui/overview.js +24 -8
- package/dist/src/tui/overview.js.map +1 -1
- package/dist/src/tui/trash-screen.d.ts +4 -0
- package/dist/src/tui/trash-screen.d.ts.map +1 -0
- package/dist/src/tui/trash-screen.js +190 -0
- package/dist/src/tui/trash-screen.js.map +1 -0
- package/dist/src/tui/vaults-panel.d.ts +8 -0
- package/dist/src/tui/vaults-panel.d.ts.map +1 -1
- package/dist/src/tui/vaults-panel.js +45 -6
- package/dist/src/tui/vaults-panel.js.map +1 -1
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +21 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +3 -3
- package/src/commands/decrypt.ts +56 -23
- package/src/commands/download.ts +82 -11
- package/src/commands/export.ts +35 -19
- package/src/commands/files.ts +93 -9
- package/src/commands/folder.ts +169 -0
- package/src/commands/rehydrate.ts +15 -8
- package/src/commands/trash.ts +121 -0
- package/src/commands/upload.ts +126 -3
- package/src/commands/wallet.ts +183 -0
- package/src/commands/webhook.ts +193 -0
- package/src/crypto.ts +35 -0
- package/src/index.ts +17 -6
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools/files.ts +43 -5
- package/src/seal.ts +34 -1
- package/src/tui/files-panel.ts +139 -11
- package/src/tui/index.ts +289 -33
- package/src/tui/overview.ts +22 -8
- package/src/tui/trash-screen.ts +203 -0
- package/src/tui/vaults-panel.ts +55 -6
- package/src/version.ts +21 -0
- package/vitest.config.ts +1 -0
- package/dist/src/client.d.ts +0 -120
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js +0 -152
- package/dist/src/client.js.map +0 -1
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,21 @@ 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 { encryptBuffer, decryptBuffer } from '../crypto.js';
|
|
19
20
|
import { loadMasterKey } from '../lib/keyring.js';
|
|
21
|
+
import { isSealConfigured, sealEncrypt, sealDecrypt, getSuiKeypair } from '../seal.js';
|
|
20
22
|
import { showAuthScreen } from './auth-screen.js';
|
|
21
23
|
import { VaultsPanel, VaultItem } from './vaults-panel.js';
|
|
22
24
|
import { FilesPanel } from './files-panel.js';
|
|
23
25
|
import { StatusBar } from './status-bar.js';
|
|
24
26
|
import { showOverview } from './overview.js';
|
|
27
|
+
import { showTrashScreen } from './trash-screen.js';
|
|
25
28
|
import { confirmDialog, textInputDialog, selectDialog, detailPopup, fileBrowserDialog } from './dialogs.js';
|
|
26
29
|
import { formatBytes, formatDate, formatRow, showError, showMessage, statusColor, statusColorClose, getCurrentTheme, cycleTheme, Theme } from './helpers.js';
|
|
27
30
|
|
|
28
31
|
function formatDetailTable(rows: [string, string][]): string {
|
|
29
32
|
const labelW = Math.max(...rows.map(([l]) => l.length));
|
|
30
|
-
return rows.map(([label, value]) => ` {bold}${label.padEnd(labelW)}{/bold}
|
|
33
|
+
return rows.map(([label, value]) => ` {bold}${label.padEnd(labelW)}{/bold} | ${value}`).join('\n');
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
type FocusTarget = 'vaults' | 'files';
|
|
@@ -74,6 +77,7 @@ export async function launchTui() {
|
|
|
74
77
|
|
|
75
78
|
const statusBar = new StatusBar(screen);
|
|
76
79
|
const vaultsPanel = new VaultsPanel(screen, sdk, leftBox, (vault) => {
|
|
80
|
+
filesPanel.resetFolderNav();
|
|
77
81
|
filesPanel.loadForVault(vault.id, vault.name);
|
|
78
82
|
});
|
|
79
83
|
const filesPanel = new FilesPanel(screen, sdk, rightBox);
|
|
@@ -85,11 +89,11 @@ export async function launchTui() {
|
|
|
85
89
|
if (target === 'vaults') {
|
|
86
90
|
vaultsPanel.focus();
|
|
87
91
|
filesPanel.blur();
|
|
88
|
-
statusBar.setHints('n:new
|
|
92
|
+
statusBar.setHints('n:new <->:switch u:upload o:overview T:trash ?:help');
|
|
89
93
|
} else {
|
|
90
94
|
filesPanel.focus();
|
|
91
95
|
vaultsPanel.blur();
|
|
92
|
-
statusBar.setHints('u:upload
|
|
96
|
+
statusBar.setHints('d:download u:upload f:folder R:remove r:retry T:trash ?:help');
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -124,28 +128,74 @@ export async function launchTui() {
|
|
|
124
128
|
}
|
|
125
129
|
});
|
|
126
130
|
|
|
131
|
+
// ── Create vault (n) — now with shared option ──────────────────────
|
|
127
132
|
screen.key(['n'], async () => {
|
|
128
133
|
const name = await textInputDialog(screen, 'Vault name');
|
|
129
134
|
if (!name) {
|
|
130
135
|
setFocus(currentFocus);
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
|
-
const visIdx = await selectDialog(screen, 'Visibility', ['private', 'public']);
|
|
138
|
+
const visIdx = await selectDialog(screen, 'Visibility', ['private', 'public', 'shared']);
|
|
134
139
|
if (visIdx < 0) {
|
|
135
140
|
setFocus(currentFocus);
|
|
136
141
|
return;
|
|
137
142
|
}
|
|
138
|
-
const
|
|
143
|
+
const visMap = ['private', 'public', 'shared'] as const;
|
|
144
|
+
const visibility = visMap[visIdx];
|
|
145
|
+
|
|
139
146
|
try {
|
|
140
|
-
|
|
147
|
+
if (visibility === 'shared') {
|
|
148
|
+
// Validate Sui address is linked
|
|
149
|
+
const acct = await sdk.account.get();
|
|
150
|
+
if (!(acct as any).suiAddress) {
|
|
151
|
+
showError(screen, 'Link a Sui address first: tusky account link-sui <addr>');
|
|
152
|
+
setFocus(currentFocus);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const created = await sdk.vaults.create({ name, visibility });
|
|
141
157
|
await vaultsPanel.load();
|
|
142
|
-
|
|
158
|
+
let msg = `Vault "${name}" created [${visibility}]`;
|
|
159
|
+
if (visibility === 'shared' && created.sealAllowlistObjectId) {
|
|
160
|
+
msg += `\nSEAL allowlist: ${created.sealAllowlistObjectId}`;
|
|
161
|
+
}
|
|
162
|
+
showMessage(screen, msg);
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
showError(screen, err.message);
|
|
165
|
+
}
|
|
166
|
+
setFocus(currentFocus);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── Create folder (f) ──────────────────────────────────────────────
|
|
170
|
+
screen.key(['f'], async () => {
|
|
171
|
+
if (currentFocus !== 'files') return;
|
|
172
|
+
const vaultId = filesPanel.getCurrentVaultId();
|
|
173
|
+
if (!vaultId) {
|
|
174
|
+
showError(screen, 'Select a vault first');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const name = await textInputDialog(screen, 'Folder name');
|
|
179
|
+
if (!name) {
|
|
180
|
+
setFocus(currentFocus);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await sdk.folders.create({
|
|
186
|
+
vaultId,
|
|
187
|
+
parentId: filesPanel.getCurrentFolderId() || undefined,
|
|
188
|
+
name,
|
|
189
|
+
});
|
|
190
|
+
await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
|
|
191
|
+
showMessage(screen, `Folder "${name}" created`);
|
|
143
192
|
} catch (err: any) {
|
|
144
193
|
showError(screen, err.message);
|
|
145
194
|
}
|
|
146
195
|
setFocus(currentFocus);
|
|
147
196
|
});
|
|
148
197
|
|
|
198
|
+
// ── Upload (u) — with SEAL support and folder awareness ────────────
|
|
149
199
|
screen.key(['u'], async () => {
|
|
150
200
|
const vaultId = filesPanel.getCurrentVaultId();
|
|
151
201
|
if (!vaultId) {
|
|
@@ -176,6 +226,7 @@ export async function launchTui() {
|
|
|
176
226
|
try {
|
|
177
227
|
const vault = await sdk.vaults.get(vaultId);
|
|
178
228
|
const isPrivate = vault.visibility === 'private';
|
|
229
|
+
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
179
230
|
let masterKey: Buffer | null = null;
|
|
180
231
|
|
|
181
232
|
if (isPrivate) {
|
|
@@ -187,8 +238,18 @@ export async function launchTui() {
|
|
|
187
238
|
}
|
|
188
239
|
}
|
|
189
240
|
|
|
241
|
+
if (isShared) {
|
|
242
|
+
const keypair = getSuiKeypair();
|
|
243
|
+
if (!keypair) {
|
|
244
|
+
showError(screen, 'TUSKYDP_SUI_PRIVATE_KEY env var required for shared vault uploads');
|
|
245
|
+
setFocus(currentFocus);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
190
250
|
let successCount = 0;
|
|
191
251
|
const total = filePaths.length;
|
|
252
|
+
const currentFolderId = filesPanel.getCurrentFolderId();
|
|
192
253
|
|
|
193
254
|
for (const filePath of filePaths) {
|
|
194
255
|
const fileName = basename(filePath);
|
|
@@ -205,6 +266,8 @@ export async function launchTui() {
|
|
|
205
266
|
encryptionIv?: string;
|
|
206
267
|
plaintextSizeBytes?: number;
|
|
207
268
|
plaintextChecksumSha256?: string;
|
|
269
|
+
sealIdentity?: string;
|
|
270
|
+
sealEncryptedObject?: string;
|
|
208
271
|
} = {};
|
|
209
272
|
|
|
210
273
|
if (isPrivate && masterKey) {
|
|
@@ -216,6 +279,17 @@ export async function launchTui() {
|
|
|
216
279
|
plaintextSizeBytes: stat.size,
|
|
217
280
|
plaintextChecksumSha256: plaintextChecksum,
|
|
218
281
|
};
|
|
282
|
+
} else if (isShared) {
|
|
283
|
+
statusBar.setHints(`SEAL encrypting ${successCount + 1}/${total}: ${fileName}...`);
|
|
284
|
+
screen.render();
|
|
285
|
+
const fileNonce = randomUUID();
|
|
286
|
+
const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
|
|
287
|
+
uploadBody = Buffer.from(sealResult.encryptedData);
|
|
288
|
+
encryptionMeta = {
|
|
289
|
+
sealIdentity: sealResult.sealIdentity,
|
|
290
|
+
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
291
|
+
plaintextSizeBytes: stat.size,
|
|
292
|
+
};
|
|
219
293
|
} else {
|
|
220
294
|
uploadBody = fileBuffer;
|
|
221
295
|
}
|
|
@@ -225,6 +299,7 @@ export async function launchTui() {
|
|
|
225
299
|
mimeType,
|
|
226
300
|
sizeBytes: uploadBody.length,
|
|
227
301
|
vaultId,
|
|
302
|
+
...(currentFolderId ? { folderId: currentFolderId } : {}),
|
|
228
303
|
...encryptionMeta,
|
|
229
304
|
});
|
|
230
305
|
|
|
@@ -240,7 +315,8 @@ export async function launchTui() {
|
|
|
240
315
|
successCount++;
|
|
241
316
|
}
|
|
242
317
|
|
|
243
|
-
|
|
318
|
+
const label = isPrivate ? 'private' : isShared ? 'shared/SEAL' : 'public';
|
|
319
|
+
showMessage(screen, `Uploaded ${successCount} file(s) [${label}]`);
|
|
244
320
|
await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
|
|
245
321
|
await vaultsPanel.load();
|
|
246
322
|
} catch (err: any) {
|
|
@@ -249,10 +325,115 @@ export async function launchTui() {
|
|
|
249
325
|
setFocus(currentFocus);
|
|
250
326
|
});
|
|
251
327
|
|
|
252
|
-
|
|
328
|
+
// ── Download (d) — download selected file to local disk ─────────────
|
|
329
|
+
screen.key(['d'], async () => {
|
|
330
|
+
if (currentFocus !== 'files') return;
|
|
331
|
+
const file = filesPanel.getSelected();
|
|
332
|
+
if (!file) {
|
|
333
|
+
showError(screen, 'Select a file to download');
|
|
334
|
+
setFocus(currentFocus);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
statusBar.setHints(`Downloading "${file.name}"...`);
|
|
339
|
+
screen.render();
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
// Get download URL + encryption info
|
|
343
|
+
let dlResponse = await sdk.files.getDownloadUrl(file.id);
|
|
344
|
+
let { downloadUrl, status: dlStatus, encryption } = dlResponse;
|
|
345
|
+
|
|
346
|
+
// Poll if rehydrating from cold storage
|
|
347
|
+
if (dlStatus === 'rehydrating') {
|
|
348
|
+
statusBar.setHints(`Rehydrating "${file.name}" from Walrus...`);
|
|
349
|
+
screen.render();
|
|
350
|
+
for (let i = 0; i < 30; i++) {
|
|
351
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
352
|
+
const check = await sdk.files.getDownloadUrl(file.id);
|
|
353
|
+
if (check.status === 'ready') {
|
|
354
|
+
downloadUrl = check.downloadUrl;
|
|
355
|
+
encryption = check.encryption;
|
|
356
|
+
dlStatus = 'ready';
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (dlStatus !== 'ready') {
|
|
361
|
+
showError(screen, 'File rehydration timed out. Try again later.');
|
|
362
|
+
setFocus(currentFocus);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!downloadUrl) {
|
|
368
|
+
showError(screen, 'No download URL available.');
|
|
369
|
+
setFocus(currentFocus);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Download the bytes
|
|
374
|
+
statusBar.setHints(`Downloading "${file.name}"...`);
|
|
375
|
+
screen.render();
|
|
376
|
+
const response = await fetch(downloadUrl);
|
|
377
|
+
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
|
|
378
|
+
const arrayBuf = await response.arrayBuffer();
|
|
379
|
+
let fileBuffer: Buffer = Buffer.from(new Uint8Array(arrayBuf));
|
|
380
|
+
|
|
381
|
+
// Decrypt if needed
|
|
382
|
+
if (encryption?.encrypted) {
|
|
383
|
+
if ('type' in encryption && encryption.type === 'seal') {
|
|
384
|
+
statusBar.setHints(`SEAL decrypting "${file.name}"...`);
|
|
385
|
+
screen.render();
|
|
386
|
+
const keypair = getSuiKeypair();
|
|
387
|
+
if (!keypair) {
|
|
388
|
+
showError(screen, 'TUSKYDP_SUI_PRIVATE_KEY env var required for shared vault files');
|
|
389
|
+
setFocus(currentFocus);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const fileInfo = await sdk.files.get(file.id);
|
|
393
|
+
const vault = await sdk.vaults.get(fileInfo.vaultId);
|
|
394
|
+
const decrypted = await sealDecrypt(new Uint8Array(fileBuffer), vault, keypair);
|
|
395
|
+
fileBuffer = Buffer.from(decrypted);
|
|
396
|
+
} else {
|
|
397
|
+
statusBar.setHints(`Decrypting "${file.name}"...`);
|
|
398
|
+
screen.render();
|
|
399
|
+
const masterKey = loadMasterKey();
|
|
400
|
+
if (!masterKey) {
|
|
401
|
+
showError(screen, 'Encryption not unlocked. Run: tusky encryption unlock');
|
|
402
|
+
setFocus(currentFocus);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const enc = encryption as { type: 'passphrase'; encrypted: true; wrappedKey: string; iv: string; plaintextChecksumSha256: string | null };
|
|
406
|
+
fileBuffer = decryptBuffer(
|
|
407
|
+
fileBuffer,
|
|
408
|
+
enc.wrappedKey,
|
|
409
|
+
enc.iv,
|
|
410
|
+
masterKey,
|
|
411
|
+
enc.plaintextChecksumSha256 ?? undefined,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Write to disk
|
|
417
|
+
const outputPath = join(process.cwd(), file.name);
|
|
418
|
+
writeFileSync(outputPath, fileBuffer);
|
|
419
|
+
|
|
420
|
+
showMessage(screen, `Downloaded "${file.name}" (${formatBytes(fileBuffer.length)})\n-> ${outputPath}`);
|
|
421
|
+
} catch (err: any) {
|
|
422
|
+
showError(screen, err.message);
|
|
423
|
+
}
|
|
424
|
+
setFocus(currentFocus);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ── Remove (R = Shift+r) — delete selected item ───────────────────
|
|
428
|
+
screen.key(['S-r'], async () => {
|
|
253
429
|
if (currentFocus === 'vaults') {
|
|
254
430
|
const vault = vaultsPanel.getSelected();
|
|
255
431
|
if (!vault) return;
|
|
432
|
+
if (vault.isSharedMembership) {
|
|
433
|
+
showError(screen, 'Cannot delete a vault you are a member of (not owner)');
|
|
434
|
+
setFocus(currentFocus);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
256
437
|
const confirmed = await confirmDialog(screen, `Delete vault "${vault.name}" and ALL its files?`);
|
|
257
438
|
if (confirmed) {
|
|
258
439
|
try {
|
|
@@ -266,27 +447,45 @@ export async function launchTui() {
|
|
|
266
447
|
}
|
|
267
448
|
setFocus(currentFocus);
|
|
268
449
|
} else {
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
450
|
+
const item = filesPanel.getSelectedItem();
|
|
451
|
+
if (!item) return;
|
|
452
|
+
|
|
453
|
+
if (item.type === 'folder') {
|
|
454
|
+
const confirmed = await confirmDialog(screen, `Delete folder "${item.folder.name}"?`);
|
|
455
|
+
if (confirmed) {
|
|
456
|
+
try {
|
|
457
|
+
await sdk.folders.delete(item.folder.id);
|
|
458
|
+
const vaultId = filesPanel.getCurrentVaultId();
|
|
459
|
+
if (vaultId) {
|
|
460
|
+
await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
|
|
461
|
+
}
|
|
462
|
+
showMessage(screen, `Folder "${item.folder.name}" deleted`);
|
|
463
|
+
} catch (err: any) {
|
|
464
|
+
showError(screen, err.message);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} else if (item.type === 'file') {
|
|
468
|
+
const confirmed = await confirmDialog(screen, `Delete file "${item.file.name}"?`);
|
|
469
|
+
if (confirmed) {
|
|
470
|
+
try {
|
|
471
|
+
await sdk.files.delete(item.file.id);
|
|
472
|
+
const vaultId = filesPanel.getCurrentVaultId();
|
|
473
|
+
if (vaultId) {
|
|
474
|
+
await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
|
|
475
|
+
await vaultsPanel.load();
|
|
476
|
+
}
|
|
477
|
+
showMessage(screen, `File "${item.file.name}" moved to trash`);
|
|
478
|
+
} catch (err: any) {
|
|
479
|
+
showError(screen, err.message);
|
|
279
480
|
}
|
|
280
|
-
showMessage(screen, `File "${file.name}" deleted`);
|
|
281
|
-
} catch (err: any) {
|
|
282
|
-
showError(screen, err.message);
|
|
283
481
|
}
|
|
284
482
|
}
|
|
285
483
|
setFocus(currentFocus);
|
|
286
484
|
}
|
|
287
485
|
});
|
|
288
486
|
|
|
289
|
-
|
|
487
|
+
// ── Enter / Space — detail popup or folder navigation ──────────────
|
|
488
|
+
screen.key(['enter', 'space'], async () => {
|
|
290
489
|
if (currentFocus === 'vaults') {
|
|
291
490
|
const vault = vaultsPanel.getSelected();
|
|
292
491
|
if (vault) {
|
|
@@ -298,11 +497,45 @@ export async function launchTui() {
|
|
|
298
497
|
['Files', String(vault.fileCount)],
|
|
299
498
|
['Size', formatBytes(vault.totalSizeBytes)],
|
|
300
499
|
];
|
|
500
|
+
if (vault.description) {
|
|
501
|
+
rows.push(['Description', vault.description]);
|
|
502
|
+
}
|
|
503
|
+
if (vault.createdAt) {
|
|
504
|
+
rows.push(['Created', formatDate(vault.createdAt)]);
|
|
505
|
+
}
|
|
506
|
+
if (vault.isSharedMembership) {
|
|
507
|
+
rows.push(['Your Role', vault.memberRole || 'member']);
|
|
508
|
+
}
|
|
509
|
+
if (vault.visibility === 'shared') {
|
|
510
|
+
rows.push(['', '']);
|
|
511
|
+
rows.push(['{cyan-fg}SEAL Config{/cyan-fg}', '']);
|
|
512
|
+
rows.push(['Allowlist', vault.sealAllowlistObjectId || 'N/A']);
|
|
513
|
+
rows.push(['Package', vault.sealPackageId || 'N/A']);
|
|
514
|
+
rows.push(['Threshold', String(vault.sealThreshold ?? 'N/A')]);
|
|
515
|
+
rows.push(['Key Servers', String(vault.sealKeyServerIds?.length ?? 0)]);
|
|
516
|
+
}
|
|
301
517
|
detailPopup(screen, vault.name, formatDetailTable(rows));
|
|
302
518
|
}
|
|
303
519
|
} else {
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
520
|
+
const item = filesPanel.getSelectedItem();
|
|
521
|
+
if (!item) return;
|
|
522
|
+
|
|
523
|
+
if (item.type === 'parent') {
|
|
524
|
+
// Navigate up
|
|
525
|
+
await filesPanel.navigateUp();
|
|
526
|
+
setFocus(currentFocus);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (item.type === 'folder') {
|
|
531
|
+
// Navigate into folder
|
|
532
|
+
await filesPanel.navigateToFolder(item.folder.id, item.folder.name);
|
|
533
|
+
setFocus(currentFocus);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (item.type === 'file') {
|
|
538
|
+
const file = item.file;
|
|
306
539
|
const open = statusColor(file.status);
|
|
307
540
|
const close = statusColorClose(file.status);
|
|
308
541
|
const rows: [string, string][] = [
|
|
@@ -316,11 +549,21 @@ export async function launchTui() {
|
|
|
316
549
|
['Blob ID', file.walrusBlobId || 'N/A'],
|
|
317
550
|
['Blob Object', file.walrusBlobObjectId || 'N/A'],
|
|
318
551
|
];
|
|
552
|
+
if (file.folderId) {
|
|
553
|
+
rows.push(['Folder ID', file.folderId]);
|
|
554
|
+
}
|
|
555
|
+
if (file.autoRenew) {
|
|
556
|
+
rows.push(['Auto-Renew', '{green-fg}On{/green-fg}']);
|
|
557
|
+
}
|
|
558
|
+
if (file.ppuEpochEnd) {
|
|
559
|
+
rows.push(['PPU Epoch', formatDate(file.ppuEpochEnd)]);
|
|
560
|
+
}
|
|
319
561
|
detailPopup(screen, file.name, formatDetailTable(rows));
|
|
320
562
|
}
|
|
321
563
|
}
|
|
322
564
|
});
|
|
323
565
|
|
|
566
|
+
// ── Retry (r) ──────────────────────────────────────────────────────
|
|
324
567
|
screen.key(['r'], async () => {
|
|
325
568
|
if (currentFocus === 'files') {
|
|
326
569
|
const file = filesPanel.getSelected();
|
|
@@ -343,11 +586,21 @@ export async function launchTui() {
|
|
|
343
586
|
}
|
|
344
587
|
});
|
|
345
588
|
|
|
589
|
+
// ── Overview (o) ───────────────────────────────────────────────────
|
|
346
590
|
screen.key(['o'], async () => {
|
|
347
591
|
await showOverview(screen, sdk);
|
|
348
592
|
setFocus(currentFocus);
|
|
349
593
|
});
|
|
350
594
|
|
|
595
|
+
// ── Trash (T = Shift+t) ───────────────────────────────────────────
|
|
596
|
+
screen.key(['S-t'], async () => {
|
|
597
|
+
await showTrashScreen(screen, sdk);
|
|
598
|
+
// Reload vaults panel in case restores happened
|
|
599
|
+
await vaultsPanel.load();
|
|
600
|
+
setFocus(currentFocus);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ── Logout (L = Shift+l) ──────────────────────────────────────────
|
|
351
604
|
screen.key(['S-l'], async () => {
|
|
352
605
|
const confirmed = await confirmDialog(screen, 'Log out?');
|
|
353
606
|
if (confirmed) {
|
|
@@ -388,19 +641,22 @@ export async function launchTui() {
|
|
|
388
641
|
'{bold}{cyan-fg}Tusky TUI — Keyboard Shortcuts{/cyan-fg}{/bold}',
|
|
389
642
|
'',
|
|
390
643
|
'{bold}Navigation{/bold}',
|
|
391
|
-
'
|
|
644
|
+
' <-/-> Switch between vaults/files',
|
|
392
645
|
' Tab Switch between vaults/files',
|
|
393
646
|
' Up/Down Navigate list',
|
|
647
|
+
' Enter Open folder / view details',
|
|
394
648
|
'',
|
|
395
649
|
'{bold}Actions{/bold}',
|
|
396
|
-
' n Create new vault',
|
|
397
|
-
'
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'
|
|
650
|
+
' n Create new vault (private/public/shared)',
|
|
651
|
+
' f Create folder in current vault',
|
|
652
|
+
' u Upload file to current vault/folder',
|
|
653
|
+
' d Download selected file',
|
|
654
|
+
' R Remove selected item (trash for files)',
|
|
655
|
+
' r Retry failed file sync',
|
|
401
656
|
'',
|
|
402
657
|
'{bold}Screens{/bold}',
|
|
403
658
|
' o Overview (account, storage, files)',
|
|
659
|
+
' T Trash management (list, restore, delete)',
|
|
404
660
|
' t Cycle theme',
|
|
405
661
|
' L Logout',
|
|
406
662
|
' ? This help',
|
package/src/tui/overview.ts
CHANGED
|
@@ -74,13 +74,27 @@ 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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
const vaultRows = vaults.map((v) => {
|
|
78
|
+
let vis: string;
|
|
79
|
+
let visTag: string;
|
|
80
|
+
if (v.visibility === 'private') {
|
|
81
|
+
vis = 'private';
|
|
82
|
+
visTag = '{yellow-fg}private{/yellow-fg}';
|
|
83
|
+
} else if (v.visibility === 'shared') {
|
|
84
|
+
vis = 'shared';
|
|
85
|
+
visTag = '{magenta-fg}shared{/magenta-fg}';
|
|
86
|
+
} else {
|
|
87
|
+
vis = 'public';
|
|
88
|
+
visTag = '{green-fg}public{/green-fg}';
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
name: v.name || '',
|
|
92
|
+
vis,
|
|
93
|
+
visTag,
|
|
94
|
+
files: String(v.fileCount || 0),
|
|
95
|
+
size: formatBytes(v.totalSizeBytes || 0),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
84
98
|
const vNameW = Math.max(4, ...vaultRows.map((r) => r.name.length));
|
|
85
99
|
const vVisW = 7; // "private" is longest
|
|
86
100
|
const vFilesW = Math.max(5, ...vaultRows.map((r) => r.files.length));
|
|
@@ -122,7 +136,7 @@ export async function showOverview(
|
|
|
122
136
|
content += ` ${'Name'.padEnd(maxNameW)} ${'Size'.padStart(fSizeW)} ${'Status'.padEnd(fStatW)} ${'Date'.padEnd(fDateW)}\n`;
|
|
123
137
|
content += ` ${'─'.repeat(maxNameW)} ${'─'.repeat(fSizeW)} ${'─'.repeat(fStatW)} ${'─'.repeat(fDateW)}\n`;
|
|
124
138
|
for (const r of fileRows) {
|
|
125
|
-
const truncName = r.name.length > maxNameW ? r.name.slice(0, maxNameW - 1) + '
|
|
139
|
+
const truncName = r.name.length > maxNameW ? r.name.slice(0, maxNameW - 1) + '...' : r.name;
|
|
126
140
|
const statusPad = ' '.repeat(Math.max(0, fStatW - r.status.length));
|
|
127
141
|
content += ` ${truncName.padEnd(maxNameW)} ${r.size.padStart(fSizeW)} ${r.statusTag}${statusPad} ${r.date}\n`;
|
|
128
142
|
}
|