@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.
- package/CHANGELOG.md +39 -0
- package/dist/src/commands/account.d.ts.map +1 -1
- package/dist/src/commands/account.js +0 -1
- package/dist/src/commands/account.js.map +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +8 -5
- package/dist/src/commands/auth.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 +35 -22
- package/dist/src/commands/download.js.map +1 -1
- package/dist/src/commands/export.d.ts +9 -24
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +31 -59
- 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 +91 -12
- 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/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +15 -9
- package/dist/src/commands/mcp.js.map +1 -1
- 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/sui.d.ts +3 -0
- package/dist/src/commands/sui.d.ts.map +1 -0
- package/dist/src/commands/sui.js +64 -0
- package/dist/src/commands/sui.js.map +1 -0
- 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 +82 -27
- package/dist/src/commands/upload.js.map +1 -1
- package/dist/src/commands/vault.d.ts.map +1 -1
- package/dist/src/commands/vault.js +2 -24
- package/dist/src/commands/vault.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/config.d.ts +2 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +2 -3
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.js +19 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/resolve.d.ts.map +1 -1
- package/dist/src/lib/resolve.js +4 -5
- package/dist/src/lib/resolve.js.map +1 -1
- package/dist/src/mcp/context.d.ts +1 -9
- package/dist/src/mcp/context.d.ts.map +1 -1
- package/dist/src/mcp/context.js +1 -2
- package/dist/src/mcp/context.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +2 -59
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/account.d.ts.map +1 -1
- package/dist/src/mcp/tools/account.js +1 -3
- package/dist/src/mcp/tools/account.js.map +1 -1
- package/dist/src/mcp/tools/files.d.ts +2 -3
- package/dist/src/mcp/tools/files.d.ts.map +1 -1
- package/dist/src/mcp/tools/files.js +46 -49
- package/dist/src/mcp/tools/files.js.map +1 -1
- package/dist/src/mcp/tools/vaults.js +2 -2
- package/dist/src/mcp/tools/vaults.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 -2
- package/dist/src/tui/files-panel.d.ts.map +1 -1
- package/dist/src/tui/files-panel.js +119 -13
- 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 +252 -48
- 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 +21 -9
- 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/__tests__/seal.test.ts +7 -54
- package/src/commands/account.ts +0 -1
- package/src/commands/auth.ts +7 -5
- package/src/commands/download.ts +38 -28
- package/src/commands/export.ts +37 -81
- package/src/commands/files.ts +95 -11
- package/src/commands/folder.ts +169 -0
- package/src/commands/mcp.ts +16 -10
- package/src/commands/rehydrate.ts +15 -8
- package/src/commands/sui.ts +69 -0
- package/src/commands/trash.ts +121 -0
- package/src/commands/upload.ts +98 -31
- package/src/commands/vault.ts +2 -23
- package/src/commands/wallet.ts +183 -0
- package/src/commands/webhook.ts +193 -0
- package/src/config.ts +3 -4
- package/src/index.ts +19 -10
- package/src/lib/resolve.ts +3 -4
- package/src/mcp/context.ts +1 -11
- package/src/mcp/server.ts +2 -70
- package/src/mcp/tools/account.ts +1 -3
- package/src/mcp/tools/files.ts +50 -63
- package/src/mcp/tools/vaults.ts +3 -3
- package/src/seal.ts +34 -1
- package/src/tui/files-panel.ts +140 -14
- package/src/tui/index.ts +264 -52
- package/src/tui/overview.ts +20 -9
- 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/dist/src/commands/decrypt.d.ts +0 -15
- package/dist/src/commands/decrypt.d.ts.map +0 -1
- package/dist/src/commands/decrypt.js +0 -224
- package/dist/src/commands/decrypt.js.map +0 -1
- package/dist/src/commands/encryption.d.ts +0 -3
- package/dist/src/commands/encryption.d.ts.map +0 -1
- package/dist/src/commands/encryption.js +0 -254
- package/dist/src/commands/encryption.js.map +0 -1
- package/dist/src/crypto.d.ts +0 -16
- package/dist/src/crypto.d.ts.map +0 -1
- package/dist/src/crypto.js +0 -95
- package/dist/src/crypto.js.map +0 -1
- package/dist/src/lib/keyring.d.ts +0 -4
- package/dist/src/lib/keyring.d.ts.map +0 -1
- package/dist/src/lib/keyring.js +0 -49
- package/dist/src/lib/keyring.js.map +0 -1
- package/src/__tests__/crypto.test.ts +0 -315
- package/src/commands/decrypt.ts +0 -276
- package/src/commands/encryption.ts +0 -305
- package/src/crypto.ts +0 -130
- 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 {
|
|
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}
|
|
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
|
|
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
|
|
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', ['
|
|
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
|
|
141
|
+
const visMap = ['public', 'shared'] as const;
|
|
142
|
+
const visibility = visMap[visIdx];
|
|
143
|
+
|
|
139
144
|
try {
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
179
|
-
let masterKey: Buffer | null = null;
|
|
226
|
+
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
180
227
|
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
if (!
|
|
184
|
-
showError(screen, '
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
plaintextSizeBytes?: number;
|
|
207
|
-
plaintextChecksumSha256?: string;
|
|
252
|
+
sealIdentity?: string;
|
|
253
|
+
sealEncryptedObject?: string;
|
|
208
254
|
} = {};
|
|
209
255
|
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
270
|
-
if (!
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
if (
|
|
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.
|
|
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
|
-
'
|
|
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
|
-
'
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'
|
|
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',
|
package/src/tui/overview.ts
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
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) + '
|
|
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
|
}
|