#!/usr/bin/env bun import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { execSync } from 'child_process'; import { version } from '../package.json' assert { type: 'json' }; const CONFIG_FILE = path.join(os.homedir(), '.note.conf'); interface Config { TOKEN?: string; REPO?: string; URL?: string; } // --- Config management --- function createDefaultConfig(): void { const defaultConfig = `TOKEN= REPO= URL=https://cld-dkr-makuro-seafile.wibudev.com/api2 `; fs.writeFileSync(CONFIG_FILE, defaultConfig); } function editConfig(): void { if (!fs.existsSync(CONFIG_FILE)) { createDefaultConfig(); } const editor = process.env.EDITOR || 'vim'; try { execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' }); } catch (error) { console.error('❌ Failed to open editor'); process.exit(1); } } function loadConfig(): Config { if (!fs.existsSync(CONFIG_FILE)) { console.error(`⚠️ Config file not found at ${CONFIG_FILE}`); console.error('Run: bun note.ts config to create/edit it.'); process.exit(1); } const configContent = fs.readFileSync(CONFIG_FILE, 'utf8'); const config: Config = {}; for (const line of configContent.split('\n')) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const [key, ...valueParts] = trimmed.split('='); if (key && valueParts.length > 0) { let value = valueParts.join('='); // Remove surrounding quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } config[key as keyof Config] = value; } } } if (!config.TOKEN || !config.REPO) { console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`); process.exit(1); } if (!config.URL) { console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`); process.exit(1); } return config; } // --- HTTP helpers --- async function fetchWithAuth(config: Config, url: string, options: RequestInit = {}): Promise { const headers = { 'Authorization': `Token ${config.TOKEN}`, ...options.headers }; try { const response = await fetch(url, { ...options, headers }); if (!response.ok) { console.error(`❌ Request failed: ${response.status} ${response.statusText}`); console.error(`🔍 URL: ${url}`); console.error(`🔍 Headers:`, headers); // Try to get response body for more details try { const errorText = await response.text(); console.error(`🔍 Response body: ${errorText}`); } catch (e) { console.error('🔍 Could not read response body'); } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } catch (error) { if (error instanceof Error && error.message.includes('HTTP')) { // Already handled above process.exit(1); } console.error('❌ Network request failed:', error); process.exit(1); } } // --- Commands --- async function testConnection(config: Config): Promise { console.log('🔍 Testing connection...'); try { // Test basic API endpoint const response = await fetchWithAuth(config, `${config.URL}/ping/`); const result = await response.text(); console.log(`✅ API connection successful: ${result}`); } catch (error) { console.log('⚠️ API ping failed, trying repo access...'); try { // Try accessing the repo directly const response = await fetchWithAuth(config, `${config.URL}/${config.REPO}/`); const result = await response.text(); console.log(`✅ Repo access successful`); } catch (error) { console.error('❌ Both API ping and repo access failed'); } } } async function listFiles(config: Config): Promise { const url = `${config.URL}/${config.REPO}/dir/?p=/`; const response = await fetchWithAuth(config, url); try { const files = await response.json(); for (const file of files as { name: string }[]) { console.log(file.name); } } catch (error) { console.error('❌ Failed to parse response'); process.exit(1); } } async function catFile(config: Config, fileName: string): Promise { const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`); const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); const contentResponse = await fetchWithAuth(config, downloadUrl); const content = await contentResponse.text(); console.log(content); } async function uploadFile(config: Config, localFile: string, remoteFile?: string): Promise { if (!fs.existsSync(localFile)) { console.error(`❌ File not found: ${localFile}`); process.exit(1); } const remoteName = remoteFile || path.basename(localFile); // Get upload URL const uploadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/upload-link/?p=/`); const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, ''); // Create FormData const formData = new FormData(); const fileContent = fs.readFileSync(localFile); const blob = new Blob([fileContent]); formData.append('file', blob, remoteName); formData.append('filename', remoteName); formData.append('parent_dir', '/'); try { await fetchWithAuth(config, uploadUrl, { method: 'POST', body: formData }); console.log(`✅ Uploaded ${localFile} → ${remoteName}`); } catch (error) { console.error('❌ Upload failed'); process.exit(1); } } async function removeFile(config: Config, fileName: string): Promise { const url = `${config.URL}/${config.REPO}/file/?p=/${fileName}`; await fetchWithAuth(config, url, { method: 'DELETE' }); console.log(`🗑️ Removed ${fileName}`); } async function moveFile(config: Config, oldName: string, newName: string): Promise { const url = `${config.URL}/${config.REPO}/file/?p=/${oldName}`; const formData = new FormData(); formData.append('operation', 'rename'); formData.append('newname', newName); try { await fetchWithAuth(config, url, { method: 'POST', body: formData }); console.log(`✏️ Renamed ${oldName} → ${newName}`); } catch (error) { console.error('❌ Rename failed'); process.exit(1); } } async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise { const localName = localFile || remoteFile; // Get download URL const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`); const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); try { const fileResponse = await fetchWithAuth(config, downloadUrl); const arrayBuffer = await fileResponse.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); fs.writeFileSync(localName, buffer); console.log(`⬇️ Downloaded ${remoteFile} → ${localName}`); } catch (error) { console.error('❌ Download failed'); process.exit(1); } } async function getFileLink(config: Config, fileName: string): Promise { try { // Get the direct download/view link for the file const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`); const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); console.log(`🔗 Link for ${fileName}:`); console.log(downloadUrl); } catch (error) { console.error('❌ Failed to get file link'); process.exit(1); } } function showHelp(): void { console.log(`note - simple CLI for Seafile notes Usage: bun note.ts ls List files bun note.ts cat Show file content bun note.ts cp [remote] Upload file bun note.ts rm Remove file bun note.ts mv Rename/move file bun note.ts get [local] Download file bun note.ts link Get file link/URL bun note.ts test Test API connection bun note.ts config Edit config (~/.note.conf) Config (~/.note.conf): TOKEN=your_seafile_token REPO=repos/ URL=your_seafile_url/api2 Version: ${version}`); } // --- Main --- async function not3(): Promise { const args = process.argv.slice(2); const cmd = args[0] || 'help'; // Handle config command if (cmd === 'config') { editConfig(); return; } // Load config for other commands const config = loadConfig(); switch (cmd) { case 'test': await testConnection(config); break; case 'ls': await listFiles(config); break; case 'cat': if (!args[1]) { console.error('Usage: bun note.ts cat '); process.exit(1); } await catFile(config, args[1]); break; case 'cp': if (!args[1]) { console.error('Usage: bun note.ts cp [remote_file]'); process.exit(1); } await uploadFile(config, args[1], args[2]); break; case 'rm': if (!args[1]) { console.error('Usage: bun note.ts rm '); process.exit(1); } await removeFile(config, args[1]); break; case 'mv': if (!args[1] || !args[2]) { console.error('Usage: bun note.ts mv '); process.exit(1); } await moveFile(config, args[1], args[2]); break; case 'get': if (!args[1]) { console.error('Usage: bun note.ts get [local_file]'); process.exit(1); } await downloadFile(config, args[1], args[2]); break; case 'link': if (!args[1]) { console.error('Usage: bun note.ts link '); process.exit(1); } await getFileLink(config, args[1]); break; case 'help': default: showHelp(); break; } } // Run the main function not3().catch((error) => { console.error('❌ Error:', error); process.exit(1); });