@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/mcp.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
|
9
9
|
import { join, dirname, resolve } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import { getApiUrl, getApiKey, cliConfig } from '../config.js';
|
|
12
|
+
import { getApiUrl, getApiKey, cliConfig, getSuiPrivateKey } from '../config.js';
|
|
13
13
|
import { startMcpServer } from '../mcp/server.js';
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -93,15 +93,16 @@ function getTuskyBinaryCommand(): { command: string; args: string[] } {
|
|
|
93
93
|
return { command: 'npx', args: ['-y', '@tuskydp/cli', 'mcp', 'serve'] };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function buildMcpServerEntry(apiKey?: string, apiUrl?: string) {
|
|
96
|
+
function buildMcpServerEntry(apiKey?: string, apiUrl?: string, suiPrivKey?: string) {
|
|
97
97
|
const { command, args } = getTuskyBinaryCommand();
|
|
98
98
|
|
|
99
99
|
const env: Record<string, string> = {};
|
|
100
100
|
if (apiKey) env.TUSKYDP_API_KEY = apiKey;
|
|
101
101
|
if (apiUrl) env.TUSKYDP_API_URL = apiUrl;
|
|
102
102
|
|
|
103
|
-
//
|
|
104
|
-
|
|
103
|
+
// Include Sui private key if provided or stored in config
|
|
104
|
+
const resolvedSuiKey = suiPrivKey || getSuiPrivateKey() || '';
|
|
105
|
+
env.TUSKYDP_SUI_PRIVATE_KEY = resolvedSuiKey;
|
|
105
106
|
|
|
106
107
|
const entry: Record<string, unknown> = {
|
|
107
108
|
command,
|
|
@@ -158,7 +159,8 @@ export function registerMcpCommands(program: Command) {
|
|
|
158
159
|
.option('--target <target>', 'Target client: claude-code, claude-desktop, cursor', 'claude-code')
|
|
159
160
|
.option('--api-key <key>', 'API key to embed (defaults to stored key)')
|
|
160
161
|
.option('--api-url <url>', 'API URL override')
|
|
161
|
-
.
|
|
162
|
+
.option('--sui-priv-key <key>', 'Sui Ed25519 private key for shared vault access (defaults to stored key)')
|
|
163
|
+
.action(async (options: { target: string; apiKey?: string; apiUrl?: string; suiPrivKey?: string }) => {
|
|
162
164
|
const targets = getTargets();
|
|
163
165
|
const target = targets.find((t) => t.name === options.target);
|
|
164
166
|
|
|
@@ -177,8 +179,8 @@ export function registerMcpCommands(program: Command) {
|
|
|
177
179
|
// Only include non-default values
|
|
178
180
|
const resolvedApiUrl = apiUrl && apiUrl !== 'https://api.tusky.ai' ? apiUrl : undefined;
|
|
179
181
|
|
|
180
|
-
// Build the server entry
|
|
181
|
-
const serverEntry = buildMcpServerEntry(apiKey, resolvedApiUrl);
|
|
182
|
+
// Build the server entry (SUI key: flag > config)
|
|
183
|
+
const serverEntry = buildMcpServerEntry(apiKey, resolvedApiUrl, options.suiPrivKey);
|
|
182
184
|
|
|
183
185
|
// Read existing config
|
|
184
186
|
const config = readJsonFile(target.configPath);
|
|
@@ -205,9 +207,13 @@ export function registerMcpCommands(program: Command) {
|
|
|
205
207
|
console.log('');
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
const suiKey = options.suiPrivKey || getSuiPrivateKey();
|
|
211
|
+
if (!suiKey) {
|
|
212
|
+
console.log(chalk.yellow(' Note: No Sui key found. Set it up for shared vault access:'));
|
|
213
|
+
console.log(chalk.dim(' Run: tusky sui setup <suiprivkey1...>'));
|
|
214
|
+
console.log(chalk.dim(' Then re-run: tusky mcp install-config'));
|
|
215
|
+
console.log('');
|
|
216
|
+
}
|
|
211
217
|
|
|
212
218
|
if (options.target === 'claude-desktop') {
|
|
213
219
|
console.log(chalk.dim(' Restart Claude Desktop to pick up the changes.'));
|
|
@@ -1,14 +1,17 @@
|
|
|
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 { resolveWalrusConfig } from '@tuskydp/shared/walrus-networks.js';
|
|
5
6
|
import { formatBytes } from '../lib/output.js';
|
|
6
|
-
import { createSpinner } from '../lib/progress.js';
|
|
7
7
|
|
|
8
8
|
export async function rehydrateCommand(blobId: string, options: {
|
|
9
9
|
output?: string;
|
|
10
|
+
stdout?: boolean;
|
|
10
11
|
}, _program: Command) {
|
|
11
|
-
|
|
12
|
+
// When piping to stdout, send spinner to stderr to avoid polluting the binary stream
|
|
13
|
+
const spinnerStream = options.stdout ? process.stderr : process.stdout;
|
|
14
|
+
const spinner = ora({ text: 'Fetching blob from Walrus...', stream: spinnerStream as NodeJS.WritableStream });
|
|
12
15
|
spinner.start();
|
|
13
16
|
|
|
14
17
|
try {
|
|
@@ -24,12 +27,16 @@ export async function rehydrateCommand(blobId: string, options: {
|
|
|
24
27
|
const arrayBuf = await response.arrayBuffer();
|
|
25
28
|
const fileBuffer = Buffer.from(new Uint8Array(arrayBuf));
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (options.stdout) {
|
|
31
|
+
spinner.stop();
|
|
32
|
+
process.stdout.write(fileBuffer);
|
|
33
|
+
} else {
|
|
34
|
+
// Sanitize blob ID for use as filename (replace non-filesystem-safe characters)
|
|
35
|
+
const safeBlobId = blobId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
36
|
+
const outputPath = options.output || join(process.cwd(), `blob-${safeBlobId}`);
|
|
37
|
+
writeFileSync(outputPath, fileBuffer);
|
|
38
|
+
spinner.succeed(`Downloaded -> ${outputPath} (${formatBytes(fileBuffer.length)})`);
|
|
39
|
+
}
|
|
33
40
|
} catch (err: any) {
|
|
34
41
|
spinner.fail(`Rehydrate failed: ${err.message}`);
|
|
35
42
|
process.exit(1);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { cliConfig } from '../config.js';
|
|
4
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
5
|
+
|
|
6
|
+
export function registerSuiCommands(program: Command) {
|
|
7
|
+
const sui = program
|
|
8
|
+
.command('sui')
|
|
9
|
+
.description('Manage Sui wallet key for shared vault access');
|
|
10
|
+
|
|
11
|
+
// ── tusky sui setup ──────────────────────────────────────────────────
|
|
12
|
+
sui
|
|
13
|
+
.command('setup <private-key>')
|
|
14
|
+
.description('Save a Sui Ed25519 private key to config for shared vault operations')
|
|
15
|
+
.action((privateKey: string) => {
|
|
16
|
+
let address: string;
|
|
17
|
+
try {
|
|
18
|
+
const keypair = Ed25519Keypair.fromSecretKey(privateKey);
|
|
19
|
+
address = keypair.toSuiAddress();
|
|
20
|
+
} catch {
|
|
21
|
+
console.error(chalk.red('Invalid Sui private key. Expected suiprivkey1... format.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
cliConfig.set('suiPrivateKey', privateKey);
|
|
26
|
+
console.log(chalk.green('Sui private key saved to config.'));
|
|
27
|
+
console.log(chalk.dim(` Sui address: ${address}`));
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(chalk.yellow(' Security note: The key is stored in your local config file.'));
|
|
30
|
+
console.log(chalk.dim(` Location: ${cliConfig.path}`));
|
|
31
|
+
console.log(chalk.dim(' Run `tusky sui clear` to remove it.'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── tusky sui show ───────────────────────────────────────────────────
|
|
35
|
+
sui
|
|
36
|
+
.command('show')
|
|
37
|
+
.description('Show the configured Sui address (does not reveal the private key)')
|
|
38
|
+
.action(() => {
|
|
39
|
+
const envKey = process.env.TUSKYDP_SUI_PRIVATE_KEY;
|
|
40
|
+
const configKey = cliConfig.get('suiPrivateKey');
|
|
41
|
+
const key = envKey || configKey;
|
|
42
|
+
const source = envKey ? 'env var (TUSKYDP_SUI_PRIVATE_KEY)' : 'config';
|
|
43
|
+
|
|
44
|
+
if (!key) {
|
|
45
|
+
console.log(chalk.dim('No Sui private key configured.'));
|
|
46
|
+
console.log(chalk.dim('Run: tusky sui setup <private-key>'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const keypair = Ed25519Keypair.fromSecretKey(key);
|
|
52
|
+
const address = keypair.toSuiAddress();
|
|
53
|
+
console.log(`Sui address: ${address}`);
|
|
54
|
+
console.log(chalk.dim(`Source: ${source}`));
|
|
55
|
+
} catch {
|
|
56
|
+
console.error(chalk.red('Stored Sui key is invalid. Run: tusky sui setup <private-key>'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── tusky sui clear ──────────────────────────────────────────────────
|
|
62
|
+
sui
|
|
63
|
+
.command('clear')
|
|
64
|
+
.description('Remove the stored Sui private key from config')
|
|
65
|
+
.action(() => {
|
|
66
|
+
cliConfig.delete('suiPrivateKey');
|
|
67
|
+
console.log(chalk.green('Sui private key removed from config.'));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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, formatBytes, formatDate, shortId } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
export function registerTrashCommands(program: Command) {
|
|
9
|
+
const trash = program.command('trash').description('Manage trashed items');
|
|
10
|
+
|
|
11
|
+
// ── list ────────────────────────────────────────────────────────────
|
|
12
|
+
trash.command('list')
|
|
13
|
+
.description('List trashed files and vaults')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
const sdk = getSDKClientFromParent(trash);
|
|
16
|
+
const root = trash.parent || trash;
|
|
17
|
+
const format = root.opts().format || cliConfig.get('outputFormat');
|
|
18
|
+
const trashed = await sdk.trash.list();
|
|
19
|
+
|
|
20
|
+
if (format === 'json') {
|
|
21
|
+
console.log(JSON.stringify(trashed, null, 2));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const totalItems = trashed.files.length + trashed.vaults.length;
|
|
26
|
+
if (totalItems === 0) {
|
|
27
|
+
console.log(chalk.dim('Trash is empty.'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (trashed.vaults.length > 0) {
|
|
32
|
+
console.log(chalk.bold('Trashed Vaults:'));
|
|
33
|
+
const table = createTable(['Name', 'Visibility', 'Files', 'Size', 'Deleted', 'ID']);
|
|
34
|
+
for (const v of trashed.vaults) {
|
|
35
|
+
table.push([
|
|
36
|
+
v.name,
|
|
37
|
+
v.visibility,
|
|
38
|
+
String(v.fileCount),
|
|
39
|
+
formatBytes(v.totalSizeBytes),
|
|
40
|
+
v.deletedAt ? formatDate(v.deletedAt) : 'N/A',
|
|
41
|
+
chalk.dim(shortId(v.id)),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
console.log(table.toString());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (trashed.files.length > 0) {
|
|
48
|
+
if (trashed.vaults.length > 0) console.log('');
|
|
49
|
+
console.log(chalk.bold('Trashed Files:'));
|
|
50
|
+
const table = createTable(['Name', 'Size', 'Status', 'Deleted', 'ID']);
|
|
51
|
+
for (const f of trashed.files) {
|
|
52
|
+
table.push([
|
|
53
|
+
f.name,
|
|
54
|
+
formatBytes(f.sizeBytes),
|
|
55
|
+
f.status,
|
|
56
|
+
f.deletedAt ? formatDate(f.deletedAt) : 'N/A',
|
|
57
|
+
chalk.dim(shortId(f.id)),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
console.log(table.toString());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.dim(`\n${totalItems} item(s) in trash. Items are permanently deleted after 7 days.`));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── restore ─────────────────────────────────────────────────────────
|
|
67
|
+
trash.command('restore <id>')
|
|
68
|
+
.description('Restore a trashed item (file or vault)')
|
|
69
|
+
.action(async (id: string) => {
|
|
70
|
+
const sdk = getSDKClientFromParent(trash);
|
|
71
|
+
const result = await sdk.trash.restore(id);
|
|
72
|
+
console.log(chalk.green(`Restored ${result.type || 'item'}: ${id}`));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── delete ──────────────────────────────────────────────────────────
|
|
76
|
+
trash.command('delete <id>')
|
|
77
|
+
.description('Permanently delete a single trashed item')
|
|
78
|
+
.option('--force', 'Skip confirmation prompt')
|
|
79
|
+
.action(async (id: string, options) => {
|
|
80
|
+
const sdk = getSDKClientFromParent(trash);
|
|
81
|
+
|
|
82
|
+
if (!options.force) {
|
|
83
|
+
const answers = await inquirer.prompt([{
|
|
84
|
+
type: 'confirm',
|
|
85
|
+
name: 'confirm',
|
|
86
|
+
message: 'Permanently delete this item? This cannot be undone.',
|
|
87
|
+
default: false,
|
|
88
|
+
}]);
|
|
89
|
+
if (!answers.confirm) return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await sdk.trash.delete(id);
|
|
93
|
+
console.log(chalk.green(`Permanently deleted: ${id}`));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── empty ───────────────────────────────────────────────────────────
|
|
97
|
+
trash.command('empty')
|
|
98
|
+
.description('Permanently delete ALL trashed items')
|
|
99
|
+
.option('--force', 'Skip confirmation prompt')
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
const sdk = getSDKClientFromParent(trash);
|
|
102
|
+
|
|
103
|
+
if (!options.force) {
|
|
104
|
+
const answers = await inquirer.prompt([{
|
|
105
|
+
type: 'confirm',
|
|
106
|
+
name: 'confirm',
|
|
107
|
+
message: 'Permanently delete ALL trashed items? This cannot be undone.',
|
|
108
|
+
default: false,
|
|
109
|
+
}]);
|
|
110
|
+
if (!answers.confirm) return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await sdk.trash.empty();
|
|
114
|
+
console.log(chalk.green('Trash emptied.'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Default action for `tusky trash` with no subcommand → run list
|
|
118
|
+
trash.action(async () => {
|
|
119
|
+
await trash.commands.find(c => c.name() === 'list')?.parseAsync([], { from: 'user' });
|
|
120
|
+
});
|
|
121
|
+
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -9,9 +9,16 @@ import { createSDKClient } from '../sdk.js';
|
|
|
9
9
|
import { resolveVault } from '../lib/resolve.js';
|
|
10
10
|
import { formatBytes } from '../lib/output.js';
|
|
11
11
|
import { createSpinner } from '../lib/progress.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import { isSealConfigured, sealEncrypt, sealEncryptMetadata, getSuiKeypair } from '../seal.js';
|
|
13
|
+
|
|
14
|
+
async function readStdin(): Promise<Buffer> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const chunks: Buffer[] = [];
|
|
17
|
+
process.stdin.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
18
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks)));
|
|
19
|
+
process.stdin.on('error', reject);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
async function expandPaths(paths: string[], recursive?: boolean): Promise<string[]> {
|
|
17
24
|
const result: string[] = [];
|
|
@@ -44,25 +51,97 @@ async function expandPaths(paths: string[], recursive?: boolean): Promise<string
|
|
|
44
51
|
export async function uploadCommand(paths: string[], options: {
|
|
45
52
|
vault?: string;
|
|
46
53
|
recursive?: boolean;
|
|
54
|
+
folder?: string;
|
|
55
|
+
content?: string;
|
|
56
|
+
stdin?: boolean;
|
|
57
|
+
name?: string;
|
|
47
58
|
}, program: Command) {
|
|
48
59
|
const apiUrl = getApiUrl(program.opts().apiUrl);
|
|
49
60
|
const apiKey = getApiKey(program.opts().apiKey);
|
|
50
61
|
const sdk = createSDKClient(apiUrl, apiKey);
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let masterKey: Buffer | null = null;
|
|
58
|
-
if (isPrivate) {
|
|
59
|
-
masterKey = loadMasterKey();
|
|
60
|
-
if (!masterKey) {
|
|
61
|
-
console.error(chalk.red('Encryption session not unlocked. Run: tusky encryption unlock'));
|
|
63
|
+
// ── Inline content / stdin mode ──────────────────────────────────
|
|
64
|
+
if (options.content !== undefined || options.stdin) {
|
|
65
|
+
if (!options.name) {
|
|
66
|
+
console.error(chalk.red('--name <filename> is required when using --content or --stdin'));
|
|
62
67
|
process.exit(1);
|
|
63
68
|
}
|
|
69
|
+
|
|
70
|
+
const fileName = options.name;
|
|
71
|
+
let fileBuffer: Buffer;
|
|
72
|
+
if (options.stdin) {
|
|
73
|
+
fileBuffer = await readStdin();
|
|
74
|
+
} else {
|
|
75
|
+
fileBuffer = Buffer.from(options.content as string, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const vaultId = await resolveVault(sdk, options.vault);
|
|
79
|
+
const vault = await sdk.vaults.get(vaultId);
|
|
80
|
+
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
81
|
+
|
|
82
|
+
const spinner = createSpinner(`Uploading ${fileName}`);
|
|
83
|
+
spinner.start();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const mimeType = lookup(fileName) || 'application/octet-stream';
|
|
87
|
+
let uploadBody: Buffer;
|
|
88
|
+
let encryptionMeta: {
|
|
89
|
+
sealIdentity?: string;
|
|
90
|
+
sealEncryptedObject?: string;
|
|
91
|
+
encryptedMetadata?: string;
|
|
92
|
+
} = {};
|
|
93
|
+
|
|
94
|
+
if (isShared) {
|
|
95
|
+
spinner.text = `SEAL encrypting ${fileName}...`;
|
|
96
|
+
const fileNonce = randomUUID();
|
|
97
|
+
const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
|
|
98
|
+
uploadBody = Buffer.from(sealResult.encryptedData);
|
|
99
|
+
encryptionMeta = {
|
|
100
|
+
sealIdentity: sealResult.sealIdentity,
|
|
101
|
+
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
102
|
+
encryptedMetadata: await sealEncryptMetadata(fileName, mimeType, vault, fileNonce),
|
|
103
|
+
};
|
|
104
|
+
} else {
|
|
105
|
+
uploadBody = fileBuffer;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
spinner.text = `Requesting upload URL for ${fileName}...`;
|
|
109
|
+
const { fileId, uploadUrl } = await sdk.files.requestUpload({
|
|
110
|
+
name: fileName,
|
|
111
|
+
mimeType,
|
|
112
|
+
sizeBytes: uploadBody.length,
|
|
113
|
+
vaultId,
|
|
114
|
+
...(options.folder ? { folderId: options.folder } : {}),
|
|
115
|
+
...encryptionMeta,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
spinner.text = `Uploading ${fileName} (${formatBytes(uploadBody.length)})...`;
|
|
119
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
120
|
+
method: 'PUT',
|
|
121
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
122
|
+
body: new Uint8Array(uploadBody),
|
|
123
|
+
});
|
|
124
|
+
if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.status}`);
|
|
125
|
+
|
|
126
|
+
spinner.text = `Confirming ${fileName}...`;
|
|
127
|
+
await sdk.files.confirmUpload(fileId);
|
|
128
|
+
|
|
129
|
+
spinner.succeed(`${fileName} -> ${fileId} (${formatBytes(uploadBody.length)})`);
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
spinner.fail(`Failed: ${fileName} — ${err.message}`);
|
|
132
|
+
if (err.statusCode === 402) {
|
|
133
|
+
console.error(chalk.yellow(' This may require topping up your wallet balance.'));
|
|
134
|
+
console.error(chalk.dim(' Check your wallet: tusky wallet info'));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
64
138
|
}
|
|
65
139
|
|
|
140
|
+
// ── File path mode (original behavior) ───────────────────────────
|
|
141
|
+
const vaultId = await resolveVault(sdk, options.vault);
|
|
142
|
+
const vault = await sdk.vaults.get(vaultId);
|
|
143
|
+
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
144
|
+
|
|
66
145
|
if (isShared) {
|
|
67
146
|
const keypair = getSuiKeypair();
|
|
68
147
|
if (!keypair) {
|
|
@@ -91,25 +170,12 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
91
170
|
|
|
92
171
|
let uploadBody: Buffer;
|
|
93
172
|
let encryptionMeta: {
|
|
94
|
-
wrappedKey?: string;
|
|
95
|
-
encryptionIv?: string;
|
|
96
|
-
plaintextSizeBytes?: number;
|
|
97
|
-
plaintextChecksumSha256?: string;
|
|
98
173
|
sealIdentity?: string;
|
|
99
174
|
sealEncryptedObject?: string;
|
|
175
|
+
encryptedMetadata?: string;
|
|
100
176
|
} = {};
|
|
101
177
|
|
|
102
|
-
if (
|
|
103
|
-
spinner.text = `Encrypting ${basename(filePath)}...`;
|
|
104
|
-
const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
|
|
105
|
-
uploadBody = ciphertext;
|
|
106
|
-
encryptionMeta = {
|
|
107
|
-
wrappedKey,
|
|
108
|
-
encryptionIv: iv,
|
|
109
|
-
plaintextSizeBytes: stat.size,
|
|
110
|
-
plaintextChecksumSha256: plaintextChecksum,
|
|
111
|
-
};
|
|
112
|
-
} else if (isShared) {
|
|
178
|
+
if (isShared) {
|
|
113
179
|
spinner.text = `SEAL encrypting ${basename(filePath)}...`;
|
|
114
180
|
const fileNonce = randomUUID();
|
|
115
181
|
const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
|
|
@@ -117,7 +183,7 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
117
183
|
encryptionMeta = {
|
|
118
184
|
sealIdentity: sealResult.sealIdentity,
|
|
119
185
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
120
|
-
|
|
186
|
+
encryptedMetadata: await sealEncryptMetadata(basename(filePath), mimeType, vault, fileNonce),
|
|
121
187
|
};
|
|
122
188
|
} else {
|
|
123
189
|
uploadBody = fileBuffer;
|
|
@@ -130,6 +196,7 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
130
196
|
mimeType,
|
|
131
197
|
sizeBytes: uploadBody.length,
|
|
132
198
|
vaultId,
|
|
199
|
+
...(options.folder ? { folderId: options.folder } : {}),
|
|
133
200
|
...encryptionMeta,
|
|
134
201
|
});
|
|
135
202
|
|
|
@@ -154,13 +221,13 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
154
221
|
// Surface PPU payment details if present (402 errors)
|
|
155
222
|
if (err.statusCode === 402) {
|
|
156
223
|
console.error(chalk.yellow(' This may require topping up your wallet balance.'));
|
|
157
|
-
console.error(chalk.dim(' Check your wallet: tusky
|
|
224
|
+
console.error(chalk.dim(' Check your wallet: tusky wallet info'));
|
|
158
225
|
}
|
|
159
226
|
}
|
|
160
227
|
}
|
|
161
228
|
|
|
162
229
|
if (filePaths.length > 1) {
|
|
163
|
-
const label =
|
|
230
|
+
const label = isShared ? 'shared' : 'public';
|
|
164
231
|
console.log(`\nUploaded ${successCount} file(s) (${formatBytes(totalSize)}) to vault "${vault.name}" [${label}]`);
|
|
165
232
|
}
|
|
166
233
|
}
|
package/src/commands/vault.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { resolveVault } from '../lib/resolve.js';
|
|
|
8
8
|
|
|
9
9
|
/** Map vault visibility to a display label. */
|
|
10
10
|
function visibilityLabel(v: string): string {
|
|
11
|
-
if (v === 'private') return 'private';
|
|
12
11
|
if (v === 'shared') return 'shared';
|
|
13
12
|
return 'public';
|
|
14
13
|
}
|
|
@@ -18,34 +17,14 @@ export function registerVaultCommands(program: Command) {
|
|
|
18
17
|
|
|
19
18
|
// ── create ──────────────────────────────────────────────────────────
|
|
20
19
|
vault.command('create <name>')
|
|
21
|
-
.description('Create a new vault (
|
|
22
|
-
.option('--public', 'Create as public vault (unencrypted, shareable via URL)')
|
|
20
|
+
.description('Create a new vault (public by default)')
|
|
23
21
|
.option('--shared', 'Create as shared vault (SEAL-encrypted, requires linked Sui address)')
|
|
24
22
|
.option('--description <text>', 'Vault description')
|
|
25
23
|
.action(async (name: string, options) => {
|
|
26
24
|
const sdk = getSDKClientFromParent(vault);
|
|
27
|
-
let visibility: 'public' | '
|
|
28
|
-
if (options.public) visibility = 'public';
|
|
25
|
+
let visibility: 'public' | 'shared' = 'public';
|
|
29
26
|
if (options.shared) visibility = 'shared';
|
|
30
27
|
|
|
31
|
-
if (options.public && options.shared) {
|
|
32
|
-
console.error(chalk.red('Cannot use both --public and --shared.'));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (visibility === 'private') {
|
|
37
|
-
try {
|
|
38
|
-
const { setupComplete } = await sdk.account.getEncryptionParams();
|
|
39
|
-
if (!setupComplete) {
|
|
40
|
-
console.log(chalk.red('Encryption must be set up before creating private vaults.'));
|
|
41
|
-
console.log(chalk.dim(' Run: tusky encryption setup'));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// If endpoint fails, proceed anyway
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
28
|
if (visibility === 'shared') {
|
|
50
29
|
try {
|
|
51
30
|
const acct = await sdk.account.get();
|