@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/commands/download.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import ora from 'ora';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { getApiUrl, getApiKey } from '../config.js';
|
|
6
7
|
import { createSDKClient } from '../sdk.js';
|
|
7
8
|
import { formatBytes } from '../lib/output.js';
|
|
8
|
-
import {
|
|
9
|
-
import { decryptBuffer } from '../crypto.js';
|
|
10
|
-
import { loadMasterKey } from '../lib/keyring.js';
|
|
11
|
-
import { sealDecrypt, getSuiKeypair } from '../seal.js';
|
|
9
|
+
import { sealDecrypt, sealDecryptMetadata, getSuiKeypair } from '../seal.js';
|
|
12
10
|
|
|
13
11
|
export async function downloadCommand(fileId: string, options: {
|
|
14
12
|
output?: string;
|
|
13
|
+
stdout?: boolean;
|
|
15
14
|
}, program: Command) {
|
|
16
15
|
const apiUrl = getApiUrl(program.opts().apiUrl);
|
|
17
16
|
const apiKey = getApiKey(program.opts().apiKey);
|
|
18
17
|
const sdk = createSDKClient(apiUrl, apiKey);
|
|
19
|
-
|
|
18
|
+
// When piping to stdout, send spinner to stderr to avoid polluting the binary stream
|
|
19
|
+
const spinnerStream = options.stdout ? process.stderr : process.stdout;
|
|
20
|
+
const spinner = ora({ text: 'Fetching file info...', stream: spinnerStream as NodeJS.WritableStream });
|
|
20
21
|
spinner.start();
|
|
21
22
|
|
|
22
23
|
try {
|
|
@@ -54,10 +55,9 @@ export async function downloadCommand(fileId: string, options: {
|
|
|
54
55
|
const arrayBuf = await response.arrayBuffer();
|
|
55
56
|
let fileBuffer: Buffer = Buffer.from(new Uint8Array(arrayBuf));
|
|
56
57
|
|
|
57
|
-
// Decrypt if needed —
|
|
58
|
+
// Decrypt if needed — SEAL-encrypted (shared vault)
|
|
58
59
|
if (encryption?.encrypted) {
|
|
59
60
|
if ('type' in encryption && encryption.type === 'seal') {
|
|
60
|
-
// SEAL-encrypted (shared vault)
|
|
61
61
|
spinner.text = 'SEAL decrypting...';
|
|
62
62
|
const keypair = getSuiKeypair();
|
|
63
63
|
if (!keypair) {
|
|
@@ -71,32 +71,42 @@ export async function downloadCommand(fileId: string, options: {
|
|
|
71
71
|
const vault = await sdk.vaults.get(fileInfo.vaultId);
|
|
72
72
|
const decrypted = await sealDecrypt(new Uint8Array(fileBuffer), vault, keypair);
|
|
73
73
|
fileBuffer = Buffer.from(decrypted);
|
|
74
|
-
} else {
|
|
75
|
-
// Passphrase-encrypted (private vault)
|
|
76
|
-
spinner.text = 'Decrypting...';
|
|
77
|
-
const masterKey = loadMasterKey();
|
|
78
|
-
if (!masterKey) {
|
|
79
|
-
spinner.fail('Encryption session not unlocked. Run: tusky encryption unlock');
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
// Narrow to passphrase type
|
|
83
|
-
const enc = encryption as { type: 'passphrase'; encrypted: true; wrappedKey: string; iv: string; plaintextChecksumSha256: string | null };
|
|
84
|
-
fileBuffer = decryptBuffer(
|
|
85
|
-
fileBuffer,
|
|
86
|
-
enc.wrappedKey,
|
|
87
|
-
enc.iv,
|
|
88
|
-
masterKey,
|
|
89
|
-
enc.plaintextChecksumSha256 ?? undefined,
|
|
90
|
-
);
|
|
91
74
|
}
|
|
92
75
|
}
|
|
93
76
|
|
|
94
|
-
|
|
77
|
+
|
|
78
|
+
// Output: stdout or disk
|
|
95
79
|
const fileInfo = await sdk.files.getStatus(fileId);
|
|
96
|
-
const outputPath = options.output || join(process.cwd(), fileInfo.name);
|
|
97
|
-
writeFileSync(outputPath, fileBuffer);
|
|
98
80
|
|
|
99
|
-
|
|
81
|
+
// Resolve the real filename — decrypt if nameEncrypted
|
|
82
|
+
let displayName = fileInfo.name;
|
|
83
|
+
if (fileInfo.nameEncrypted) {
|
|
84
|
+
try {
|
|
85
|
+
if (fileInfo.encryptedMetadata) {
|
|
86
|
+
// Shared vault — decrypt with SEAL keypair
|
|
87
|
+
const keypair = getSuiKeypair();
|
|
88
|
+
if (keypair) {
|
|
89
|
+
const fileObj = await sdk.files.get(fileId);
|
|
90
|
+
const vault = await sdk.vaults.get(fileObj.vaultId);
|
|
91
|
+
const meta = await sealDecryptMetadata(fileInfo.encryptedMetadata, vault, keypair);
|
|
92
|
+
displayName = meta.n;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Fall back to placeholder if decryption fails
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if (options.stdout) {
|
|
102
|
+
// Pipe raw bytes to stdout — stop spinner first so it doesn't interleave
|
|
103
|
+
spinner.stop();
|
|
104
|
+
process.stdout.write(fileBuffer);
|
|
105
|
+
} else {
|
|
106
|
+
const outputPath = options.output || join(process.cwd(), displayName);
|
|
107
|
+
writeFileSync(outputPath, fileBuffer);
|
|
108
|
+
spinner.succeed(`Downloaded -> ${outputPath} (${formatBytes(fileBuffer.length)})`);
|
|
109
|
+
}
|
|
100
110
|
} catch (err: any) {
|
|
101
111
|
spinner.fail(`Download failed: ${err.message}`);
|
|
102
112
|
}
|
package/src/commands/export.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tusky export` — Export file metadata for disaster recovery.
|
|
3
3
|
*
|
|
4
|
-
* Dumps all file records (including Walrus blob IDs
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Dumps all file records (including Walrus blob IDs and epoch info)
|
|
5
|
+
* to a JSON file. This allows users to recover their data directly
|
|
6
|
+
* from Walrus if the Tusky service is unavailable.
|
|
7
7
|
*
|
|
8
8
|
* The export contains everything needed to:
|
|
9
9
|
* 1. Fetch files from Walrus aggregators using blobId
|
|
10
|
-
* 2.
|
|
11
|
-
* 3. Renew Walrus epochs using blobObjectId + a Sui wallet
|
|
10
|
+
* 2. Renew Walrus epochs using blobObjectId + a Sui wallet
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import type { Command } from 'commander';
|
|
@@ -35,16 +34,12 @@ export interface ExportedFile {
|
|
|
35
34
|
walrusBlobObjectId: string | null;
|
|
36
35
|
/** Epoch when Walrus storage expires (renew before this) */
|
|
37
36
|
walrusEpochEnd: string | null;
|
|
38
|
-
/** Whether the file is client-side encrypted */
|
|
37
|
+
/** Whether the file is client-side encrypted (SEAL for shared vaults) */
|
|
39
38
|
encrypted: boolean;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
/** SHA-256 of the plaintext — for integrity verification after decryption */
|
|
45
|
-
plaintextChecksumSha256: string | null;
|
|
46
|
-
/** Original plaintext size in bytes (before encryption) */
|
|
47
|
-
plaintextSizeBytes: number | null;
|
|
39
|
+
/** Whether the filename is encrypted */
|
|
40
|
+
nameEncrypted: boolean;
|
|
41
|
+
/** SEAL-encrypted filename+mimeType blob (shared vaults) */
|
|
42
|
+
encryptedMetadata: string | null;
|
|
48
43
|
createdAt: string;
|
|
49
44
|
}
|
|
50
45
|
|
|
@@ -56,16 +51,6 @@ export interface ExportManifest {
|
|
|
56
51
|
email: string;
|
|
57
52
|
plan: string;
|
|
58
53
|
};
|
|
59
|
-
/** Account encryption params — needed to derive master key from passphrase */
|
|
60
|
-
encryption: {
|
|
61
|
-
setupComplete: boolean;
|
|
62
|
-
/** PBKDF2 salt (base64) */
|
|
63
|
-
salt: string | null;
|
|
64
|
-
/** HMAC verifier for passphrase validation (base64) */
|
|
65
|
-
verifier: string | null;
|
|
66
|
-
/** Master key wrapped with passphrase-derived key (base64) */
|
|
67
|
-
encryptedMasterKey: string | null;
|
|
68
|
-
};
|
|
69
54
|
walrusNetwork: string;
|
|
70
55
|
walrusAggregatorUrl: string;
|
|
71
56
|
totalFiles: number;
|
|
@@ -79,10 +64,11 @@ export interface ExportManifest {
|
|
|
79
64
|
export function registerExportCommand(program: Command) {
|
|
80
65
|
program
|
|
81
66
|
.command('export')
|
|
82
|
-
.description('Export file metadata for disaster recovery (Walrus blob IDs,
|
|
67
|
+
.description('Export file metadata for disaster recovery (Walrus blob IDs, epoch info)')
|
|
83
68
|
.option('-o, --output <path>', 'Output file path', 'tusky-export.json')
|
|
69
|
+
.option('--stdout', 'Write JSON manifest to stdout instead of a file')
|
|
84
70
|
.option('--vault <vaultId>', 'Export only files from a specific vault')
|
|
85
|
-
.action(async (options: { output: string; vault?: string }) => {
|
|
71
|
+
.action(async (options: { output: string; stdout?: boolean; vault?: string }) => {
|
|
86
72
|
const apiUrl = getApiUrl(program.opts().apiUrl);
|
|
87
73
|
const apiKey = getApiKey(program.opts().apiKey);
|
|
88
74
|
const sdk = createSDKClient(apiUrl, apiKey);
|
|
@@ -91,9 +77,8 @@ export function registerExportCommand(program: Command) {
|
|
|
91
77
|
spinner.start();
|
|
92
78
|
|
|
93
79
|
try {
|
|
94
|
-
// Fetch account
|
|
80
|
+
// Fetch account
|
|
95
81
|
const account = await sdk.account.get();
|
|
96
|
-
const encryptionParams = await sdk.account.getEncryptionParams();
|
|
97
82
|
|
|
98
83
|
// Fetch all vaults
|
|
99
84
|
spinner.text = 'Fetching vaults...';
|
|
@@ -139,10 +124,8 @@ export function registerExportCommand(program: Command) {
|
|
|
139
124
|
walrusBlobObjectId: file.walrusBlobObjectId,
|
|
140
125
|
walrusEpochEnd: file.walrusEpochEnd,
|
|
141
126
|
encrypted: file.encrypted,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
plaintextChecksumSha256: file.plaintextChecksumSha256,
|
|
145
|
-
plaintextSizeBytes: file.plaintextSizeBytes,
|
|
127
|
+
nameEncrypted: file.nameEncrypted ?? false,
|
|
128
|
+
encryptedMetadata: file.encryptedMetadata ?? null,
|
|
146
129
|
createdAt: file.createdAt,
|
|
147
130
|
});
|
|
148
131
|
}
|
|
@@ -168,12 +151,6 @@ export function registerExportCommand(program: Command) {
|
|
|
168
151
|
email: account.email,
|
|
169
152
|
plan: account.planName,
|
|
170
153
|
},
|
|
171
|
-
encryption: {
|
|
172
|
-
setupComplete: encryptionParams.setupComplete,
|
|
173
|
-
salt: encryptionParams.salt ?? null,
|
|
174
|
-
verifier: encryptionParams.verifier ?? null,
|
|
175
|
-
encryptedMasterKey: encryptionParams.encryptedMasterKey ?? null,
|
|
176
|
-
},
|
|
177
154
|
walrusNetwork: isMainnet ? 'mainnet' : 'testnet',
|
|
178
155
|
walrusAggregatorUrl,
|
|
179
156
|
totalFiles: exportedFiles.length,
|
|
@@ -191,30 +168,6 @@ export function registerExportCommand(program: Command) {
|
|
|
191
168
|
' OR: walrus read <walrusBlobId> -o <filename>',
|
|
192
169
|
' OR: tusky rehydrate <walrusBlobId> -o <filename>',
|
|
193
170
|
'',
|
|
194
|
-
'--- DECRYPT PRIVATE VAULT FILES ---',
|
|
195
|
-
'Files with encrypted=true are AES-256-GCM encrypted.',
|
|
196
|
-
'',
|
|
197
|
-
'Option A — Using the Tusky CLI decrypt command (easiest):',
|
|
198
|
-
' tusky decrypt <encrypted-file> --export <this-file.json> -o <output>',
|
|
199
|
-
' This reads the wrappedKey/iv from this manifest and prompts for your passphrase.',
|
|
200
|
-
' You can also pass --passphrase <passphrase> or set TUSKYDP_PASSWORD env var.',
|
|
201
|
-
' If the Tusky API is still reachable, you can skip the export and use:',
|
|
202
|
-
' tusky decrypt <encrypted-file> --file-id <fileId> -o <output>',
|
|
203
|
-
'',
|
|
204
|
-
'Option B — Using the standalone script (zero dependencies, fully offline):',
|
|
205
|
-
' node tusky-decrypt.mjs <encrypted-file> <output> <passphrase> <salt> <wrappedKey> <iv> [encryptedMasterKey]',
|
|
206
|
-
' The salt and encryptedMasterKey come from your account encryption params.',
|
|
207
|
-
'',
|
|
208
|
-
'Option C — Manual decryption (any language with AES-256-GCM + PBKDF2):',
|
|
209
|
-
' 1. Derive wrapping key: PBKDF2(passphrase, salt, 600000 iterations, SHA-256, 32 bytes)',
|
|
210
|
-
' 2. If your account has an encryptedMasterKey, unwrap it: AES-256-GCM-decrypt(encryptedMasterKey, wrappingKey)',
|
|
211
|
-
' Otherwise the wrapping key IS the master key (legacy accounts)',
|
|
212
|
-
' 3. Unwrap the file key: AES-256-GCM-decrypt(wrappedKey, masterKey)',
|
|
213
|
-
' wrappedKey format: [12-byte IV | ciphertext | 16-byte auth tag]',
|
|
214
|
-
' 4. Decrypt the file: AES-256-GCM-decrypt(fileData, fileKey, encryptionIv)',
|
|
215
|
-
' File data format: [ciphertext | 16-byte auth tag]',
|
|
216
|
-
' 5. Verify integrity: SHA-256(plaintext) should match plaintextChecksumSha256',
|
|
217
|
-
'',
|
|
218
171
|
'--- RENEW WALRUS EPOCHS ---',
|
|
219
172
|
'Files with a walrusBlobObjectId can have their storage extended:',
|
|
220
173
|
' walrus extend <walrusBlobObjectId> --epochs 5',
|
|
@@ -223,27 +176,30 @@ export function registerExportCommand(program: Command) {
|
|
|
223
176
|
},
|
|
224
177
|
};
|
|
225
178
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const hotOnly = exportedFiles.filter((f) => !f.walrusBlobId && f.status === 'hot').length;
|
|
179
|
+
if (options.stdout) {
|
|
180
|
+
spinner.stop();
|
|
181
|
+
process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
|
|
182
|
+
} else {
|
|
183
|
+
// Write to disk
|
|
184
|
+
const outputPath = resolve(options.output);
|
|
185
|
+
writeFileSync(outputPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
186
|
+
spinner.succeed(`Exported ${exportedFiles.length} files from ${vaults.length} vault(s)`);
|
|
187
|
+
console.log(chalk.dim(` Output: ${outputPath}`));
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
238
190
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
191
|
+
if (!options.stdout) {
|
|
192
|
+
// Summary stats (only for disk output — stdout is clean JSON)
|
|
193
|
+
const withBlob = exportedFiles.filter((f) => f.walrusBlobId).length;
|
|
194
|
+
const hotOnly = exportedFiles.filter((f) => !f.walrusBlobId && f.status === 'hot').length;
|
|
195
|
+
|
|
196
|
+
console.log(` On Walrus: ${withBlob} files (recoverable from Walrus network)`);
|
|
197
|
+
if (hotOnly > 0) {
|
|
198
|
+
console.log(chalk.yellow(` Hot only: ${hotOnly} files (NOT yet on Walrus — only in Tusky hot cache)`));
|
|
199
|
+
}
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(chalk.dim(' See the "recovery.instructions" field inside for full recovery steps.'));
|
|
243
202
|
}
|
|
244
|
-
console.log('');
|
|
245
|
-
console.log(chalk.yellow(' Store this file securely — it contains encryption keys.'));
|
|
246
|
-
console.log(chalk.dim(' See the "recovery.instructions" field inside for full recovery steps.'));
|
|
247
203
|
} catch (err: any) {
|
|
248
204
|
spinner.fail(`Export failed: ${err.message}`);
|
|
249
205
|
}
|
package/src/commands/files.ts
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { cliConfig } from '../config.js';
|
|
5
5
|
import { getSDKClient } from '../sdk.js';
|
|
6
|
-
import { createTable, formatBytes, formatDate } from '../lib/output.js';
|
|
6
|
+
import { createTable, formatBytes, formatDate, shortId } from '../lib/output.js';
|
|
7
7
|
import { resolveVault } from '../lib/resolve.js';
|
|
8
8
|
|
|
9
9
|
export function registerFileCommands(program: Command) {
|
|
@@ -12,6 +12,7 @@ export function registerFileCommands(program: Command) {
|
|
|
12
12
|
.description('List files')
|
|
13
13
|
.option('--sort <field>', 'Sort by: name, size, date', 'date')
|
|
14
14
|
.option('--limit <n>', 'Max results', '50')
|
|
15
|
+
.option('--folder <folder-id>', 'Filter by folder ID')
|
|
15
16
|
.option('-f, --follow', 'Watch mode: refresh every 3 seconds')
|
|
16
17
|
.action(async (vaultRef: string | undefined, options) => {
|
|
17
18
|
const sdk = getSDKClient(program);
|
|
@@ -27,6 +28,7 @@ export function registerFileCommands(program: Command) {
|
|
|
27
28
|
const fetchAndRender = async (): Promise<void> => {
|
|
28
29
|
const { files } = await sdk.files.list({
|
|
29
30
|
vaultId,
|
|
31
|
+
folderId: options.folder,
|
|
30
32
|
limit: parseInt(options.limit),
|
|
31
33
|
sortBy: sortMap[options.sort] as 'createdAt' | 'name' | 'sizeBytes',
|
|
32
34
|
order: 'desc',
|
|
@@ -42,7 +44,12 @@ export function registerFileCommands(program: Command) {
|
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
const
|
|
47
|
+
const showFolder = !options.folder && files.some(f => f.folderId);
|
|
48
|
+
const headers = showFolder
|
|
49
|
+
? ['Name', 'Size', 'Status', 'Folder', 'Uploaded', 'ID']
|
|
50
|
+
: ['Name', 'Size', 'Status', 'Uploaded', 'ID'];
|
|
51
|
+
|
|
52
|
+
const table = createTable(headers);
|
|
46
53
|
for (const f of files) {
|
|
47
54
|
const statusColors: Record<string, typeof chalk.green> = {
|
|
48
55
|
hot: chalk.green,
|
|
@@ -53,13 +60,17 @@ export function registerFileCommands(program: Command) {
|
|
|
53
60
|
};
|
|
54
61
|
const statusColor = statusColors[f.status] || chalk.white;
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
const row = [
|
|
57
64
|
f.name,
|
|
58
|
-
formatBytes(f.
|
|
65
|
+
formatBytes(f.sizeBytes),
|
|
59
66
|
statusColor(f.status),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
];
|
|
68
|
+
if (showFolder) {
|
|
69
|
+
row.push(f.folderId ? chalk.dim(shortId(f.folderId)) : chalk.dim('-'));
|
|
70
|
+
}
|
|
71
|
+
row.push(formatDate(f.createdAt));
|
|
72
|
+
row.push(chalk.dim(f.id));
|
|
73
|
+
table.push(row);
|
|
63
74
|
}
|
|
64
75
|
console.log(table.toString());
|
|
65
76
|
};
|
|
@@ -102,14 +113,26 @@ export function registerFileCommands(program: Command) {
|
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
console.log(`Name: ${file.name}`);
|
|
105
|
-
console.log(`Size: ${formatBytes(file.
|
|
116
|
+
console.log(`Size: ${formatBytes(file.sizeBytes)}`);
|
|
106
117
|
console.log(`MIME: ${file.mimeType}`);
|
|
107
118
|
console.log(`Status: ${file.status}`);
|
|
108
119
|
console.log(`Encrypted: ${file.encrypted ? 'Yes' : 'No'}`);
|
|
109
120
|
console.log(`Vault: ${file.vaultId}`);
|
|
110
|
-
if (file.folderId)
|
|
121
|
+
if (file.folderId) {
|
|
122
|
+
// Try to resolve folder name
|
|
123
|
+
try {
|
|
124
|
+
const folder = await sdk.folders.get(file.folderId);
|
|
125
|
+
console.log(`Folder: ${folder.name} (${file.folderId})`);
|
|
126
|
+
} catch {
|
|
127
|
+
console.log(`Folder: ${file.folderId}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
111
130
|
if (file.walrusBlobId) console.log(`Walrus Blob: ${file.walrusBlobId}`);
|
|
131
|
+
if (file.walrusBlobObjectId) console.log(`Blob Object: ${file.walrusBlobObjectId}`);
|
|
112
132
|
if (file.lastSyncError) console.log(`Sync Error: ${chalk.red(file.lastSyncError)}`);
|
|
133
|
+
if (file.autoRenew) console.log(`Auto-Renew: ${chalk.green('On')}`);
|
|
134
|
+
if (file.ppuEpochEnd) console.log(`PPU Epoch: ${new Date(file.ppuEpochEnd).toLocaleString()}`);
|
|
135
|
+
if (file.hotUntil) console.log(`Hot Until: ${new Date(file.hotUntil).toLocaleString()}`);
|
|
113
136
|
console.log(`Uploaded: ${new Date(file.createdAt).toISOString()}`);
|
|
114
137
|
console.log(`ID: ${file.id}`);
|
|
115
138
|
});
|
|
@@ -165,7 +188,7 @@ export function registerFileCommands(program: Command) {
|
|
|
165
188
|
|
|
166
189
|
// rm command
|
|
167
190
|
program.command('rm <file-ids...>')
|
|
168
|
-
.description('Delete one or more files')
|
|
191
|
+
.description('Delete one or more files (moves to trash)')
|
|
169
192
|
.option('--force', 'Skip confirmation prompt')
|
|
170
193
|
.action(async (fileIds: string[], options) => {
|
|
171
194
|
const sdk = getSDKClient(program);
|
|
@@ -174,7 +197,7 @@ export function registerFileCommands(program: Command) {
|
|
|
174
197
|
const answers = await inquirer.prompt([{
|
|
175
198
|
type: 'confirm',
|
|
176
199
|
name: 'confirm',
|
|
177
|
-
message: `Delete ${fileIds.length} file(s)?
|
|
200
|
+
message: `Delete ${fileIds.length} file(s)? Files will be moved to trash.`,
|
|
178
201
|
default: false,
|
|
179
202
|
}]);
|
|
180
203
|
if (!answers.confirm) return;
|
|
@@ -189,4 +212,65 @@ export function registerFileCommands(program: Command) {
|
|
|
189
212
|
}
|
|
190
213
|
}
|
|
191
214
|
});
|
|
215
|
+
|
|
216
|
+
// mv command — move file to a folder
|
|
217
|
+
program.command('mv <file-id>')
|
|
218
|
+
.description('Move a file to a different folder (or vault root)')
|
|
219
|
+
.option('--folder <folder-id>', 'Target folder ID')
|
|
220
|
+
.option('--root', 'Move to vault root (no folder)')
|
|
221
|
+
.action(async (fileId: string, options) => {
|
|
222
|
+
const sdk = getSDKClient(program);
|
|
223
|
+
|
|
224
|
+
if (!options.folder && !options.root) {
|
|
225
|
+
console.error(chalk.red('Specify --folder <id> or --root'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const folderId = options.root ? null : options.folder;
|
|
230
|
+
await sdk.files.move(fileId, { folderId });
|
|
231
|
+
console.log(chalk.green(folderId ? `File moved to folder ${folderId}` : 'File moved to vault root'));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// extend command — extend PPU storage epochs
|
|
235
|
+
program.command('extend <file-id>')
|
|
236
|
+
.description('Extend PPU storage by purchasing additional epochs')
|
|
237
|
+
.requiredOption('--epochs <n>', 'Number of additional epochs to purchase')
|
|
238
|
+
.action(async (fileId: string, options) => {
|
|
239
|
+
const sdk = getSDKClient(program);
|
|
240
|
+
const epochs = parseInt(options.epochs);
|
|
241
|
+
if (isNaN(epochs) || epochs <= 0) {
|
|
242
|
+
console.error(chalk.red('Epochs must be a positive integer.'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = await sdk.files.extend(fileId, { epochs });
|
|
247
|
+
console.log(chalk.green(`Extended ${fileId} by ${epochs} epoch(s).`));
|
|
248
|
+
if (result.payment) {
|
|
249
|
+
console.log(` WAL cost: ${result.payment.walMist} MIST`);
|
|
250
|
+
}
|
|
251
|
+
if (result.file.ppuEpochEnd) {
|
|
252
|
+
console.log(` New epoch end: ${new Date(result.file.ppuEpochEnd).toLocaleString()}`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// auto-renew command
|
|
257
|
+
program.command('auto-renew <file-id>')
|
|
258
|
+
.description('Toggle auto-renewal for a file\'s Walrus storage')
|
|
259
|
+
.option('--on', 'Enable auto-renew')
|
|
260
|
+
.option('--off', 'Disable auto-renew')
|
|
261
|
+
.action(async (fileId: string, options) => {
|
|
262
|
+
const sdk = getSDKClient(program);
|
|
263
|
+
|
|
264
|
+
if (!options.on && !options.off) {
|
|
265
|
+
// Show current status
|
|
266
|
+
const file = await sdk.files.get(fileId);
|
|
267
|
+
console.log(`Auto-Renew: ${file.autoRenew ? chalk.green('On') : chalk.dim('Off')}`);
|
|
268
|
+
console.log(chalk.dim('Use --on or --off to change.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const autoRenew = !!options.on;
|
|
273
|
+
await sdk.files.setAutoRenew(fileId, { autoRenew });
|
|
274
|
+
console.log(chalk.green(`Auto-renew ${autoRenew ? 'enabled' : 'disabled'} for ${fileId}`));
|
|
275
|
+
});
|
|
192
276
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { cliConfig } from '../config.js';
|
|
5
|
+
import { getSDKClientFromParent } from '../sdk.js';
|
|
6
|
+
import { createTable, formatDate, shortId } from '../lib/output.js';
|
|
7
|
+
import { resolveVault } from '../lib/resolve.js';
|
|
8
|
+
|
|
9
|
+
export function registerFolderCommands(program: Command) {
|
|
10
|
+
const folder = program.command('folder').description('Manage folders within vaults');
|
|
11
|
+
|
|
12
|
+
// ── create ──────────────────────────────────────────────────────────
|
|
13
|
+
folder.command('create <name>')
|
|
14
|
+
.description('Create a folder in a vault')
|
|
15
|
+
.requiredOption('--vault <vault>', 'Target vault (ID, slug, or name)')
|
|
16
|
+
.option('--parent <parent-id>', 'Parent folder ID (for nested folders)')
|
|
17
|
+
.action(async (name: string, options) => {
|
|
18
|
+
const sdk = getSDKClientFromParent(folder);
|
|
19
|
+
const vaultId = await resolveVault(sdk, options.vault);
|
|
20
|
+
|
|
21
|
+
const created = await sdk.folders.create({
|
|
22
|
+
vaultId,
|
|
23
|
+
parentId: options.parent,
|
|
24
|
+
name,
|
|
25
|
+
});
|
|
26
|
+
console.log(chalk.green(`Folder "${created.name}" created (${created.id})`));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ── list ────────────────────────────────────────────────────────────
|
|
30
|
+
folder.command('list')
|
|
31
|
+
.description('List folders in a vault')
|
|
32
|
+
.requiredOption('--vault <vault>', 'Target vault (ID, slug, or name)')
|
|
33
|
+
.option('--parent <parent-id>', 'Parent folder ID (list children of this folder)')
|
|
34
|
+
.action(async (options) => {
|
|
35
|
+
const sdk = getSDKClientFromParent(folder);
|
|
36
|
+
const root = folder.parent || folder;
|
|
37
|
+
const format = root.opts().format || cliConfig.get('outputFormat');
|
|
38
|
+
const vaultId = await resolveVault(sdk, options.vault);
|
|
39
|
+
|
|
40
|
+
const folders = await sdk.folders.list({ vaultId, parentId: options.parent });
|
|
41
|
+
|
|
42
|
+
if (format === 'json') {
|
|
43
|
+
console.log(JSON.stringify(folders, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (folders.length === 0) {
|
|
48
|
+
console.log(chalk.dim('No folders found.'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const table = createTable(['Name', 'Parent', 'Created', 'ID']);
|
|
53
|
+
for (const f of folders) {
|
|
54
|
+
table.push([
|
|
55
|
+
f.name,
|
|
56
|
+
f.parentId ? chalk.dim(shortId(f.parentId)) : chalk.dim('(root)'),
|
|
57
|
+
formatDate(f.createdAt),
|
|
58
|
+
chalk.dim(shortId(f.id)),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
console.log(table.toString());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── info ────────────────────────────────────────────────────────────
|
|
65
|
+
folder.command('info <folder-id>')
|
|
66
|
+
.description('Show folder details')
|
|
67
|
+
.action(async (folderId: string) => {
|
|
68
|
+
const sdk = getSDKClientFromParent(folder);
|
|
69
|
+
const root = folder.parent || folder;
|
|
70
|
+
const format = root.opts().format || cliConfig.get('outputFormat');
|
|
71
|
+
const f = await sdk.folders.get(folderId);
|
|
72
|
+
|
|
73
|
+
if (format === 'json') {
|
|
74
|
+
console.log(JSON.stringify(f, null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`Name: ${f.name}`);
|
|
79
|
+
console.log(`ID: ${f.id}`);
|
|
80
|
+
console.log(`Vault: ${f.vaultId}`);
|
|
81
|
+
console.log(`Parent: ${f.parentId || chalk.dim('(root)')}`);
|
|
82
|
+
console.log(`Created: ${new Date(f.createdAt).toLocaleString()}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── contents ────────────────────────────────────────────────────────
|
|
86
|
+
folder.command('contents <folder-id>')
|
|
87
|
+
.description('List folder contents (files and subfolders)')
|
|
88
|
+
.action(async (folderId: string) => {
|
|
89
|
+
const sdk = getSDKClientFromParent(folder);
|
|
90
|
+
const root = folder.parent || folder;
|
|
91
|
+
const format = root.opts().format || cliConfig.get('outputFormat');
|
|
92
|
+
const contents = await sdk.folders.getContents(folderId);
|
|
93
|
+
|
|
94
|
+
if (format === 'json') {
|
|
95
|
+
console.log(JSON.stringify(contents, null, 2));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (contents.folders.length === 0 && contents.files.length === 0) {
|
|
100
|
+
console.log(chalk.dim('Folder is empty.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (contents.folders.length > 0) {
|
|
105
|
+
console.log(chalk.bold('Subfolders:'));
|
|
106
|
+
const folderTable = createTable(['Name', 'Created', 'ID']);
|
|
107
|
+
for (const f of contents.folders) {
|
|
108
|
+
folderTable.push([
|
|
109
|
+
f.name,
|
|
110
|
+
formatDate(f.createdAt),
|
|
111
|
+
chalk.dim(shortId(f.id)),
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
console.log(folderTable.toString());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (contents.files.length > 0) {
|
|
118
|
+
if (contents.folders.length > 0) console.log('');
|
|
119
|
+
console.log(chalk.bold('Files:'));
|
|
120
|
+
const { formatBytes } = await import('../lib/output.js');
|
|
121
|
+
const fileTable = createTable(['Name', 'Size', 'Status', 'Created', 'ID']);
|
|
122
|
+
const statusColors: Record<string, typeof chalk.green> = {
|
|
123
|
+
hot: chalk.green, synced: chalk.blue, cold: chalk.yellow,
|
|
124
|
+
error: chalk.red, uploading: chalk.dim,
|
|
125
|
+
};
|
|
126
|
+
for (const f of contents.files) {
|
|
127
|
+
const sc = statusColors[f.status] || chalk.white;
|
|
128
|
+
fileTable.push([
|
|
129
|
+
f.name,
|
|
130
|
+
formatBytes(f.sizeBytes),
|
|
131
|
+
sc(f.status),
|
|
132
|
+
formatDate(f.createdAt),
|
|
133
|
+
chalk.dim(shortId(f.id)),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
console.log(fileTable.toString());
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── rename ──────────────────────────────────────────────────────────
|
|
141
|
+
folder.command('rename <folder-id> <new-name>')
|
|
142
|
+
.description('Rename a folder')
|
|
143
|
+
.action(async (folderId: string, newName: string) => {
|
|
144
|
+
const sdk = getSDKClientFromParent(folder);
|
|
145
|
+
await sdk.folders.update(folderId, { name: newName });
|
|
146
|
+
console.log(chalk.green(`Folder renamed to "${newName}"`));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── delete ──────────────────────────────────────────────────────────
|
|
150
|
+
folder.command('delete <folder-id>')
|
|
151
|
+
.description('Delete a folder (must be empty)')
|
|
152
|
+
.option('--force', 'Skip confirmation prompt')
|
|
153
|
+
.action(async (folderId: string, options) => {
|
|
154
|
+
const sdk = getSDKClientFromParent(folder);
|
|
155
|
+
|
|
156
|
+
if (!options.force) {
|
|
157
|
+
const answers = await inquirer.prompt([{
|
|
158
|
+
type: 'confirm',
|
|
159
|
+
name: 'confirm',
|
|
160
|
+
message: 'Delete folder? It must be empty.',
|
|
161
|
+
default: false,
|
|
162
|
+
}]);
|
|
163
|
+
if (!answers.confirm) return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await sdk.folders.delete(folderId);
|
|
167
|
+
console.log(chalk.green('Folder deleted.'));
|
|
168
|
+
});
|
|
169
|
+
}
|