tambahakan banyak

This commit is contained in:
bipproduction
2025-09-25 07:43:07 +08:00
parent a8a700f09a
commit 5b592d52a5
11 changed files with 563 additions and 568 deletions

View File

@@ -5,7 +5,8 @@ import path from "path";
import { generateEnvTypes } from "../generate/env.generate";
import checkPort from "./src/port";
import route from "./src/route";
import not3 from "./src/not3";
import compose from "./src/compose";
import generateDockerfile from "./src/docker-file";
interface CheckPortResult {
port: number;
@@ -21,6 +22,7 @@ Commands:
env Generate env.d.ts from .env file
scan-port Scan port range (default 3000-4000)
route Generate routes.ts from AppRoutes.tsx
compose Generate compose.yml from name
Options:
--env Path ke file .env (default: .env)
--out Path file output (default: types/env.d.ts)
@@ -32,7 +34,7 @@ Examples:
g3n env --env .env.local --out src/types/env.d.ts
g3n scan-port --start 7700 --end 7800 --host 127.0.0.1
g3n route
`;
g3n compose <name>`;
(async () => {
const cmd = args._[0];
@@ -75,8 +77,17 @@ Examples:
return;
}
if(cmd === "note") {
not3()
if (cmd === "compose") {
if (!args._[1]) {
console.error("❌ Name is required");
return;
}
compose(args._[1] as string);
return;
}
if (cmd === "docker-file") {
generateDockerfile();
return;
}

View File

@@ -1,275 +0,0 @@
#!/usr/bin/env bun
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execSync } from 'child_process';
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
interface Config {
TOKEN?: string;
REPO?: string;
URL?: string;
}
// --- Check dependencies ---
function checkDependency(cmd: string): boolean {
try {
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
function checkDependencies(): void {
const deps = ['curl', 'jq'];
for (const cmd of deps) {
if (!checkDependency(cmd)) {
console.error(`❌ Missing dependency: ${cmd} (please install it)`);
process.exit(1);
}
}
}
// --- 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: node 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) {
const value = valueParts.join('=');
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);
}
// Set default URL if not provided
if (!config.URL) {
console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`);
process.exit(1);
}
return config;
}
// --- HTTP helpers ---
function curlCommand(config: Config, options: string): string {
try {
return execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options}`,
{ encoding: 'utf8' });
} catch (error) {
console.error('❌ Request failed');
process.exit(1);
}
}
function curlCommandSilent(config: Config, options: string): void {
try {
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options} >/dev/null`,
{ stdio: 'ignore' });
} catch (error) {
console.error('❌ Request failed');
process.exit(1);
}
}
// --- Commands ---
function listFiles(config: Config): void {
const response = curlCommand(config, `"${config.URL}/${config.REPO}/dir/?p=/"`);
try {
const files = JSON.parse(response);
for (const file of files) {
console.log(file.name);
}
} catch (error) {
console.error('❌ Failed to parse response');
process.exit(1);
}
}
function catFile(config: Config, fileName: string): void {
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
const content = curlCommand(config, `"${downloadUrl}"`);
console.log(content);
}
function uploadFile(config: Config, localFile: string, remoteFile?: string): void {
if (!fs.existsSync(localFile)) {
console.error(`❌ File not found: ${localFile}`);
process.exit(1);
}
const remoteName = remoteFile || path.basename(localFile);
const uploadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/upload-link/?p=/"`);
const uploadUrl = uploadUrlResponse.replace(/"/g, '');
try {
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" \
-F "file=@${localFile}" \
-F "filename=${remoteName}" \
-F "parent_dir=/" \
"${uploadUrl}" >/dev/null`, { stdio: 'ignore' });
console.log(`✅ Uploaded ${localFile}${remoteName}`);
} catch (error) {
console.error('❌ Upload failed');
process.exit(1);
}
}
function removeFile(config: Config, fileName: string): void {
curlCommandSilent(config, `-X DELETE "${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
console.log(`🗑️ Removed ${fileName}`);
}
function moveFile(config: Config, oldName: string, newName: string): void {
try {
execSync(`curl -s -X POST -H "Authorization: Token ${config.TOKEN}" \
-d "operation=rename" \
-d "newname=${newName}" \
"${config.URL}/${config.REPO}/file/?p=/${oldName}" >/dev/null`, { stdio: 'ignore' });
console.log(`✏️ Renamed ${oldName}${newName}`);
} catch (error) {
console.error('❌ Rename failed');
process.exit(1);
}
}
function downloadFile(config: Config, remoteFile: string, localFile?: string): void {
const localName = localFile || remoteFile;
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${remoteFile}"`);
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
try {
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" "${downloadUrl}" -o "${localName}"`,
{ stdio: 'ignore' });
console.log(`⬇️ Downloaded ${remoteFile}${localName}`);
} catch (error) {
console.error('❌ Download failed');
process.exit(1);
}
}
function showHelp(): void {
console.log(`note - simple CLI for Seafile notes
Usage:
node note.ts ls List files
node note.ts cat <file> Show file content
node note.ts cp <local> [remote] Upload file
node note.ts rm <remote> Remove file
node note.ts mv <old> <new> Rename/move file
node note.ts get <remote> [local] Download file
node note.ts config Edit config (~/.note.conf)
Config (~/.note.conf):
TOKEN=your_seafile_token
REPO=repos/<repo-id>
URL=your_seafile_url/api2`);
}
// --- Main ---
function not3(): void {
const args = process.argv.slice(2);
const cmd = args[0] || 'help';
// Handle config command without dependency check
if (cmd === 'config') {
editConfig();
return;
}
// Check dependencies for other commands
checkDependencies();
// Load config for other commands
const config = loadConfig();
switch (cmd) {
case 'ls':
listFiles(config);
break;
case 'cat':
if (!args[1]) {
console.error('Usage: node note.ts cat <file>');
process.exit(1);
}
catFile(config, args[1]);
break;
case 'cp':
if (!args[1]) {
console.error('Usage: node note.ts cp <local_file> [remote_file]');
process.exit(1);
}
uploadFile(config, args[1], args[2]);
break;
case 'rm':
if (!args[1]) {
console.error('Usage: node note.ts rm <remote_file>');
process.exit(1);
}
removeFile(config, args[1]);
break;
case 'mv':
if (!args[1] || !args[2]) {
console.error('Usage: node note.ts mv <old_name> <new_name>');
process.exit(1);
}
moveFile(config, args[1], args[2]);
break;
case 'get':
if (!args[1]) {
console.error('Usage: node note.ts get <remote_file> [local_file]');
process.exit(1);
}
downloadFile(config, args[1], args[2]);
break;
case 'help':
default:
showHelp();
break;
}
}

79
bin/src/assets/Dockerfile Normal file
View File

@@ -0,0 +1,79 @@
FROM ubuntu:22.04 AS dev
ENV DEBIAN_FRONTEND=noninteractive
# --- Install runtime dependencies ---
RUN apt-get update && apt-get install -y --no-install-recommends \
curl git unzip ca-certificates openssh-server bash tini vim docker.io tmux \
&& rm -rf /var/lib/apt/lists/*
# --- Install Node.js 22 ---
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash \
&& cp /root/.bun/bin/bun /usr/local/bin/bun \
&& cp /root/.bun/bin/bunx /usr/local/bin/bunx \
&& bun --version
# --- Create non-root user `bip` ---
ARG SSH_USER=bip
RUN useradd -ms /bin/bash $SSH_USER \
&& mkdir -p /home/$SSH_USER/.ssh \
&& chmod 700 /home/$SSH_USER/.ssh \
&& chown -R $SSH_USER:$SSH_USER /home/$SSH_USER/.ssh
# --- Configure SSH ---
RUN mkdir -p /var/run/sshd \
&& sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config \
&& sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config \
&& echo "AllowUsers $SSH_USER" >> /etc/ssh/sshd_config
# Copy deploy script (milik user bip)
COPY --chown=$SSH_USER:$SSH_USER deploy /usr/local/bin/deploy
RUN chmod +x /usr/local/bin/deploy
# Authorized keys mount point
VOLUME ["/home/$SSH_USER/.ssh"]
# Expose SSH port
EXPOSE 22
# Use Tini as entrypoint for signal handling
ENTRYPOINT ["/usr/bin/tini", "--"]
# Start SSH daemon in foreground
CMD ["/usr/sbin/sshd", "-D"]
FROM ubuntu:22.04 AS prod
ENV DEBIAN_FRONTEND=noninteractive
# --- Install runtime dependencies ---
RUN apt-get update && apt-get install -y --no-install-recommends \
curl git unzip ca-certificates bash tini \
&& rm -rf /var/lib/apt/lists/*
# --- Install Node.js 22 ---
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash \
&& cp /root/.bun/bin/bun /usr/local/bin/bun \
&& cp /root/.bun/bin/bunx /usr/local/bin/bunx \
&& bun --version
# --- Set working dir ---
WORKDIR /app/current
# Expose port (ubah sesuai app)
EXPOSE 3000
# Use Tini as entrypoint for signal handling
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["bun", "run", "start"]

237
bin/src/assets/deploy Normal file
View File

@@ -0,0 +1,237 @@
#!/usr/bin/env bash
set -euo pipefail
# =====================================================
# Deploy Script WhatsApp Server (dijalankan di dalam container prod)
# =====================================================
CONFIG_FILE="/app/.deploy.conf"
ENV_FILE="/app/.env"
DOCKER_HOST_PROXY=""
# --- Default config ---
GIT_URL_DEFAULT=""
BRANCH_DEFAULT=""
SERVICE_DEFAULT=""
# =====================================================
# Utils
# =====================================================
log() {
echo "[ $(date '+%Y-%m-%d %H:%M:%S') ] $*"
}
# =====================================================
# Load config
# =====================================================
if [ ! -f "$CONFIG_FILE" ]; then
# Kalau ada env DEPLOY_SERVICE, jadikan default SERVICE
SERVICE_DEFAULT="${DEPLOY_SERVICE:-}"
cat > "$CONFIG_FILE" <<EOF
GIT_URL="$GIT_URL_DEFAULT"
BRANCH="$BRANCH_DEFAULT"
SERVICE="$SERVICE_DEFAULT"
EOF
log "⚠️ File .deploy.conf belum ada ($CONFIG_FILE), membuat dengan default"
fi
# Source config
# shellcheck disable=SC1090
source "$CONFIG_FILE"
# Kalau SERVICE kosong, fallback ke DEPLOY_SERVICE
if [ -z "${SERVICE:-}" ] && [ -n "${DEPLOY_SERVICE:-}" ]; then
SERVICE="$DEPLOY_SERVICE"
fi
# Set docker proxy kalau SERVICE sudah ada
if [ -n "${SERVICE:-}" ]; then
DOCKER_HOST_PROXY="tcp://$SERVICE-docker-proxy:2375"
fi
ensure_config() {
if [ -z "${GIT_URL:-}" ] || [ -z "${BRANCH:-}" ] || [ -z "${SERVICE:-}" ]; then
log "⚠️ Config tidak lengkap, pastikan ada di $CONFIG_FILE"
exit 1
fi
}
# =====================================================
# Fungsi Edit Config
# =====================================================
edit_config() {
vi "$CONFIG_FILE"
log "✅ Perubahan tersimpan di $CONFIG_FILE"
}
# =====================================================
# Fungsi Edit .env
# =====================================================
edit_env() {
vi "$ENV_FILE"
log "✅ Perubahan tersimpan di $ENV_FILE"
}
# =====================================================
# Fungsi Show .env
# =====================================================
show_env() {
if [ -f "$ENV_FILE" ]; then
echo "=== 📄 Isi $ENV_FILE ==="
cat "$ENV_FILE"
echo "========================="
else
echo "⚠️ File .env belum ada ($ENV_FILE)"
fi
}
show_log() {
docker -H "$DOCKER_HOST_PROXY" logs "$SERVICE-prod"
}
# =====================================================
# Direktori Deploy
# =====================================================
WORKDIR="/app"
NEW_DIR="$WORKDIR/current-new"
OLD_DIR="$WORKDIR/current-old"
CURR_DIR="$WORKDIR/current"
# --- Default Flags ---
IS_DB_PUSH=false
IS_DB_SEED=false
IS_BUILD=true
IS_CACHE=true
# --- Parse args ---
COMMAND=${1:-""}
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--db-push) IS_DB_PUSH=true ;;
--seed) IS_DB_SEED=true ;;
--build) IS_BUILD=true ;;
--no-build) IS_BUILD=false ;;
--cache) IS_CACHE=true ;;
--no-cache) IS_CACHE=false ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
shift
done
# =====================================================
# Fungsi Deploy Start
# =====================================================
deploy_start() {
ensure_config
log "🚀 Mulai proses deploy..."
log "Gunakan repo: $GIT_URL (branch: $BRANCH, service: $SERVICE)"
if [ -d "$NEW_DIR" ]; then
log "hapus folder current-new ..."
rm -rf "$NEW_DIR"
fi
log "git clone ..."
git clone --depth 1 --branch "$BRANCH" "$GIT_URL" "$NEW_DIR"
if [ -f "$ENV_FILE" ]; then
log "copy .env ..."
cp "$ENV_FILE" "$NEW_DIR/"
fi
if [ "$IS_CACHE" = "true" ] && [ -d "$OLD_DIR/node_modules" ]; then
log "copy node_modules ..."
cp -r "$OLD_DIR/node_modules" "$NEW_DIR/"
fi
if [ "$IS_CACHE" = "true" ] && [ -d "$OLD_DIR/generated/prisma" ]; then
log "copy prisma ..."
mkdir -p "$NEW_DIR/generated"
cp -r "$OLD_DIR/generated/prisma" "$NEW_DIR/generated/"
fi
cd "$NEW_DIR"
if [ "$IS_CACHE" = "true" ] && [ -f "$OLD_DIR/bun.lock" ]; then
log "using cached dependencies ..."
bun install --frozen-lockfile
else
bun install
fi
if [ "$IS_DB_PUSH" = "true" ]; then
log "prisma db push ..."
bun x prisma db push
fi
if [ "$IS_DB_SEED" = "true" ]; then
log "seeding database ..."
bun x prisma db seed
fi
if [ "$IS_BUILD" = "true" ]; then
log "building app ..."
bun run build
fi
if [ -d "$OLD_DIR" ]; then
log "hapus current-old ..."
rm -rf "$OLD_DIR"
fi
if [ -d "$CURR_DIR" ]; then
log "backup current -> current-old ..."
mv "$CURR_DIR" "$OLD_DIR"
fi
log "deploy current-new -> current ..."
mv "$NEW_DIR" "$CURR_DIR"
log "restart service ..."
docker -H "$DOCKER_HOST_PROXY" restart "$SERVICE-prod"
}
# =====================================================
# Fungsi Deploy Restart
# =====================================================
deploy_restart() {
ensure_config
log "restart service ..."
docker -H "$DOCKER_HOST_PROXY" restart "$SERVICE-prod"
}
# =====================================================
# Main
# =====================================================
case "$COMMAND" in
start)
deploy_start
;;
restart)
deploy_restart
;;
config)
edit_config
;;
env)
edit_env
;;
log)
show_log
;;
show-env)
show_env
;;
show-config)
echo "📌 Git URL : $GIT_URL"
echo "📌 Branch : $BRANCH"
echo "📌 Service : $SERVICE"
;;
*)
echo "Usage: deploy {start|restart|config|env|show-env|show-config} [--db-push] [--seed] [--build|--no-build] [--cache|--no-cache]"
exit 1
;;
esac

134
bin/src/compose.ts Normal file
View File

@@ -0,0 +1,134 @@
import fs from "fs/promises";
const text = (name: string) => {
return `
services:
${name}-docker-proxy:
image: tecnativa/docker-socket-proxy
container_name: ${name}-docker-proxy
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
CONTAINERS: 1
POST: 1
PING: 1
networks:
- ${name}
${name}-dev:
image: bip/dev:latest
build:
dockerfile: Dockerfile
context: .
target: dev
container_name: ${name}-dev
restart: unless-stopped
volumes:
- ./data/app:/app
- ./data/ssh/authorized_keys:/home/bip/.ssh/authorized_keys:ro
networks:
- ${name}
depends_on:
${name}-postgres:
condition: service_healthy
${name}-prod:
build:
dockerfile: Dockerfile
context: .
target: prod
image: bip/prod:latest
container_name: ${name}-prod
restart: unless-stopped
volumes:
- ./data/app:/app
networks:
- ${name}
depends_on:
${name}-postgres:
condition: service_healthy
${name}-postgres:
image: postgres:16
container_name: ${name}-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=bip
- POSTGRES_PASSWORD=Production_123
- POSTGRES_DB=${name}
volumes:
- ./data/postgres:/var/lib/postgresql/data
networks:
- ${name}
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U bip -d ${name}']
interval: 5s
timeout: 5s
retries: 5
${name}-frpc:
image: snowdreamtech/frpc:latest
container_name: ${name}-frpc
restart: always
volumes:
- ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro
networks:
- ${name}
networks:
${name}:
driver: bridge
`
}
const generate = (name: string) => {
return `
#!/bin/bash
echo "Generating directory..."
mkdir -p data data/app data/postgres data/frpc data/ssh
echo "Generating authorized_keys..."
touch data/ssh/authorized_keys
echo "Generating frpc.toml..."
touch data/frpc/frpc.toml
echo "Generating frpc.toml content..."
cat > data/frpc/frpc.toml <<EOF
[common]
server_addr = "85.31.224.xxx"
server_port = 7000
transport.tcp_mux = true
transport.pool_count = 5
transport.tls.enable = true
auth_token = ""
[ssh-cld-dkr-staging-${name}.wibudev.com]
type = tcp
local_ip = ${name}-dev
local_port = 22
remote_port = 5102
[postgres-cld-dkr-staging-${name}.wibudev.com]
type = tcp
local_ip = ${name}-postgres
local_port = 5432
remote_port = 5202
[cld-dkr-staging-${name}.wibudev.com]
type = http
local_ip = ${name}-prod
local_port = 3000
custom_domains = "cld-dkr-staging-${name}.wibudev.com"
EOF
`
}
async function compose(name: string) {
const composeFile = text(name);
await fs.writeFile(`./compose.yml`, composeFile);
Bun.spawnSync(["bash", "-c", generate(name)]);
}
export default compose

17
bin/src/docker-file.ts Normal file
View File

@@ -0,0 +1,17 @@
import fs from "fs/promises";
import path from "path";
const dockerfile = Bun.file(path.join(__dirname, "./assets/Dockerfile"));
const deployFile = Bun.file(path.join(__dirname, "./assets/deploy"));
async function generateDockerfile() {
const dockerfileText = await dockerfile.text();
const deployText = await deployFile.text();
await fs.writeFile("./Dockerfile", dockerfileText);
await fs.writeFile("./deploy", deployText);
}
export default generateDockerfile;

View File

@@ -1,277 +0,0 @@
#!/usr/bin/env bun
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execSync } from 'child_process';
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
interface Config {
TOKEN?: string;
REPO?: string;
URL?: string;
}
// --- Check dependencies ---
function checkDependency(cmd: string): boolean {
try {
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
function checkDependencies(): void {
const deps = ['curl', 'jq'];
for (const cmd of deps) {
if (!checkDependency(cmd)) {
console.error(`❌ Missing dependency: ${cmd} (please install it)`);
process.exit(1);
}
}
}
// --- 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: node 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) {
const value = valueParts.join('=');
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);
}
// Set default URL if not provided
if (!config.URL) {
console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`);
process.exit(1);
}
return config;
}
// --- HTTP helpers ---
function curlCommand(config: Config, options: string): string {
try {
return execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options}`,
{ encoding: 'utf8' });
} catch (error) {
console.error('❌ Request failed');
process.exit(1);
}
}
function curlCommandSilent(config: Config, options: string): void {
try {
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options} >/dev/null`,
{ stdio: 'ignore' });
} catch (error) {
console.error('❌ Request failed');
process.exit(1);
}
}
// --- Commands ---
function listFiles(config: Config): void {
const response = curlCommand(config, `"${config.URL}/${config.REPO}/dir/?p=/"`);
try {
const files = JSON.parse(response);
for (const file of files) {
console.log(file.name);
}
} catch (error) {
console.error('❌ Failed to parse response');
process.exit(1);
}
}
function catFile(config: Config, fileName: string): void {
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
const content = curlCommand(config, `"${downloadUrl}"`);
console.log(content);
}
function uploadFile(config: Config, localFile: string, remoteFile?: string): void {
if (!fs.existsSync(localFile)) {
console.error(`❌ File not found: ${localFile}`);
process.exit(1);
}
const remoteName = remoteFile || path.basename(localFile);
const uploadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/upload-link/?p=/"`);
const uploadUrl = uploadUrlResponse.replace(/"/g, '');
try {
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" \
-F "file=@${localFile}" \
-F "filename=${remoteName}" \
-F "parent_dir=/" \
"${uploadUrl}" >/dev/null`, { stdio: 'ignore' });
console.log(`✅ Uploaded ${localFile}${remoteName}`);
} catch (error) {
console.error('❌ Upload failed');
process.exit(1);
}
}
function removeFile(config: Config, fileName: string): void {
curlCommandSilent(config, `-X DELETE "${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
console.log(`🗑️ Removed ${fileName}`);
}
function moveFile(config: Config, oldName: string, newName: string): void {
try {
execSync(`curl -s -X POST -H "Authorization: Token ${config.TOKEN}" \
-d "operation=rename" \
-d "newname=${newName}" \
"${config.URL}/${config.REPO}/file/?p=/${oldName}" >/dev/null`, { stdio: 'ignore' });
console.log(`✏️ Renamed ${oldName}${newName}`);
} catch (error) {
console.error('❌ Rename failed');
process.exit(1);
}
}
function downloadFile(config: Config, remoteFile: string, localFile?: string): void {
const localName = localFile || remoteFile;
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${remoteFile}"`);
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
try {
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" "${downloadUrl}" -o "${localName}"`,
{ stdio: 'ignore' });
console.log(`⬇️ Downloaded ${remoteFile}${localName}`);
} catch (error) {
console.error('❌ Download failed');
process.exit(1);
}
}
function showHelp(): void {
console.log(`note - simple CLI for Seafile notes
Usage:
node note.ts ls List files
node note.ts cat <file> Show file content
node note.ts cp <local> [remote] Upload file
node note.ts rm <remote> Remove file
node note.ts mv <old> <new> Rename/move file
node note.ts get <remote> [local] Download file
node note.ts config Edit config (~/.note.conf)
Config (~/.note.conf):
TOKEN=your_seafile_token
REPO=repos/<repo-id>
URL=your_seafile_url/api2`);
}
// --- Main ---
function not3(): void {
const args = process.argv.slice(2);
const cmd = args[0] || 'help';
// Handle config command without dependency check
if (cmd === 'config') {
editConfig();
return;
}
// Check dependencies for other commands
checkDependencies();
// Load config for other commands
const config = loadConfig();
switch (cmd) {
case 'ls':
listFiles(config);
break;
case 'cat':
if (!args[1]) {
console.error('Usage: node note.ts cat <file>');
process.exit(1);
}
catFile(config, args[1]);
break;
case 'cp':
if (!args[1]) {
console.error('Usage: node note.ts cp <local_file> [remote_file]');
process.exit(1);
}
uploadFile(config, args[1], args[2]);
break;
case 'rm':
if (!args[1]) {
console.error('Usage: node note.ts rm <remote_file>');
process.exit(1);
}
removeFile(config, args[1]);
break;
case 'mv':
if (!args[1] || !args[2]) {
console.error('Usage: node note.ts mv <old_name> <new_name>');
process.exit(1);
}
moveFile(config, args[1], args[2]);
break;
case 'get':
if (!args[1]) {
console.error('Usage: node note.ts get <remote_file> [local_file]');
process.exit(1);
}
downloadFile(config, args[1], args[2]);
break;
case 'help':
default:
showHelp();
break;
}
}
export default not3;

View File

@@ -9,6 +9,7 @@
"@babel/types": "^7.28.2",
"@types/babel__traverse": "^7.28.0",
"@types/minimist": "^1.2.5",
"dedent": "^1.7.0",
"dotenv": "^17.2.1",
"minimist": "^1.2.8",
},
@@ -50,6 +51,8 @@
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
"dotenv": ["dotenv@17.2.1", "", {}, "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],

75
compose.yml Normal file
View File

@@ -0,0 +1,75 @@
services:
hipmi-docker-proxy:
image: tecnativa/docker-socket-proxy
container_name: hipmi-docker-proxy
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
CONTAINERS: 1
POST: 1
PING: 1
networks:
- hipmi
hipmi-dev:
image: bip/dev:latest
build:
dockerfile: Dockerfile
context: .
target: dev
container_name: hipmi-dev
restart: unless-stopped
volumes:
- ./data/app:/app
- ./data/ssh/authorized_keys:/home/bip/.ssh/authorized_keys:ro
networks:
- hipmi
depends_on:
hipmi-postgres:
condition: service_healthy
hipmi-prod:
build:
dockerfile: Dockerfile
context: .
target: prod
image: bip/prod:latest
container_name: hipmi-prod
restart: unless-stopped
volumes:
- ./data/app:/app
networks:
- hipmi
depends_on:
hipmi-postgres:
condition: service_healthy
hipmi-postgres:
image: postgres:16
container_name: hipmi-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=bip
- POSTGRES_PASSWORD=Production_123
- POSTGRES_DB=hipmi
volumes:
- ./data/postgres:/var/lib/postgresql/data
networks:
- hipmi
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U bip -d hipmi']
interval: 5s
timeout: 5s
retries: 5
hipmi-frpc:
image: snowdreamtech/frpc:latest
container_name: hipmi-frpc
restart: always
volumes:
- ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro
networks:
- hipmi
networks:
hipmi:
driver: bridge

View File

@@ -3,8 +3,7 @@
"version": "1.0.4",
"type": "module",
"bin": {
"g3n": "./bin/g3n.ts",
"not3": "./bin/not3.ts"
"g3n": "./bin/g3n.ts"
},
"peerDependencies": {
"typescript": "^5"
@@ -15,6 +14,7 @@
"@babel/types": "^7.28.2",
"@types/babel__traverse": "^7.28.0",
"@types/minimist": "^1.2.5",
"dedent": "^1.7.0",
"dotenv": "^17.2.1",
"minimist": "^1.2.8"
}

9
x.ts
View File

@@ -61,12 +61,6 @@ function loadConfig(): Config {
}
}
// Debug: show loaded config (without showing actual token for security)
// console.log(`🔍 Config loaded:`);
// console.log(` TOKEN: ${config.TOKEN ? '[SET]' : '[NOT SET]'}`);
// console.log(` REPO: ${config.REPO || '[NOT SET]'}`);
// console.log(` URL: ${config.URL || '[NOT SET]'}`);
if (!config.TOKEN || !config.REPO) {
console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`);
process.exit(1);
@@ -87,9 +81,6 @@ async function fetchWithAuth(config: Config, url: string, options: RequestInit =
...options.headers
};
// Debug: log the URL being called
// console.log(`🔍 Calling URL: ${url}`);
try {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {