From 8fec022a1fd017daf4781149539cd008a02b1da8 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Wed, 24 Sep 2025 17:42:33 +0800 Subject: [PATCH] tambahan --- .gitignore | 34 +++++ README.md | 15 +++ bin/not3.ts | 351 ++++++++++++++++++++++++++++++++++++++++++++++++++ bun.lock | 29 +++++ index.ts | 1 + package.json | 15 +++ tsconfig.json | 29 +++++ 7 files changed, 474 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bin/not3.ts create mode 100644 bun.lock create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..507ece1 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# not3 + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bin/not3.ts b/bin/not3.ts new file mode 100755 index 0000000..630f21b --- /dev/null +++ b/bin/not3.ts @@ -0,0 +1,351 @@ +#!/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); +}); \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..6bb0086 --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "not3", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8970d91 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "not3", + "version": "1.0.4", + "module": "index.ts", + "type": "module", + "bin": { + "not3": "bin/not3.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}