tambahannya
This commit is contained in:
57
bin/g3n.ts
57
bin/g3n.ts
@@ -1,7 +1,16 @@
|
||||
#!/usr/bin/env bun
|
||||
import minimist from "minimist";
|
||||
import path from "path";
|
||||
import { generateEnvTypes } from "../generate/env.generate.js";
|
||||
|
||||
import { generateEnvTypes } from "../generate/env.generate";
|
||||
import checkPort from "./src/port";
|
||||
import route from "./src/route";
|
||||
import not3 from "./src/not3";
|
||||
|
||||
interface CheckPortResult {
|
||||
port: number;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const args = minimist(process.argv.slice(2));
|
||||
|
||||
@@ -9,14 +18,20 @@ const help = `
|
||||
g3n [command] [options]
|
||||
|
||||
Commands:
|
||||
env Generate env.d.ts from .env file
|
||||
|
||||
env Generate env.d.ts from .env file
|
||||
scan-port Scan port range (default 3000-4000)
|
||||
route Generate routes.ts from AppRoutes.tsx
|
||||
Options:
|
||||
--env Path ke file .env (default: .env)
|
||||
--out Path file output (default: types/env.d.ts)
|
||||
--start Port awal scan (default: 3000)
|
||||
--end Port akhir scan (default: 4000)
|
||||
--host Host/IP target (default: 127.0.0.1)
|
||||
|
||||
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
|
||||
`;
|
||||
|
||||
(async () => {
|
||||
@@ -31,5 +46,41 @@ Examples:
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "scan-port") {
|
||||
const start: number = args.start ? parseInt(args.start, 10) : 3000;
|
||||
const end: number = args.end ? parseInt(args.end, 10) : 4000;
|
||||
const host: string = args.host || "localhost";
|
||||
|
||||
console.log(`🔍 Scan port ${start}-${end} di host ${host} ...`);
|
||||
|
||||
const ports: number[] = Array.from(
|
||||
{ length: end - start + 1 },
|
||||
(_, i) => start + i
|
||||
);
|
||||
|
||||
const results: CheckPortResult[] = await Promise.all(
|
||||
ports.map((p) => checkPort(p, host))
|
||||
);
|
||||
|
||||
results.filter((r) => r.open).forEach((r) => {
|
||||
console.log(`✅ Port ${r.port} sedang digunakan`);
|
||||
});
|
||||
|
||||
console.log("✅ Selesai");
|
||||
return;
|
||||
}
|
||||
|
||||
if(cmd === "route") {
|
||||
route();
|
||||
return;
|
||||
}
|
||||
|
||||
if(cmd === "note") {
|
||||
not3()
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(help);
|
||||
})();
|
||||
|
||||
|
||||
|
||||
275
bin/not3.ts
Executable file
275
bin/not3.ts
Executable file
@@ -0,0 +1,275 @@
|
||||
#!/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;
|
||||
}
|
||||
}
|
||||
277
bin/src/not3.ts
Normal file
277
bin/src/not3.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
#!/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;
|
||||
31
bin/src/port.ts
Normal file
31
bin/src/port.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bun
|
||||
import net from "net";
|
||||
|
||||
interface CheckPortResult {
|
||||
port: number;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export default function checkPort(port: number, host: string): Promise<CheckPortResult> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
socket.setTimeout(200);
|
||||
|
||||
socket.once("connect", () => {
|
||||
socket.destroy();
|
||||
resolve({ port, open: true });
|
||||
});
|
||||
|
||||
socket.once("timeout", () => {
|
||||
socket.destroy();
|
||||
resolve({ port, open: false });
|
||||
});
|
||||
|
||||
socket.once("error", () => {
|
||||
socket.destroy();
|
||||
resolve({ port, open: false });
|
||||
});
|
||||
|
||||
socket.connect(port, host);
|
||||
});
|
||||
}
|
||||
138
bin/src/route.ts
Normal file
138
bin/src/route.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bun
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as parser from "@babel/parser";
|
||||
import traverse from "@babel/traverse";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
const SRC_DIR = path.resolve(process.cwd(), "src");
|
||||
const APP_ROUTES_FILE = path.join(SRC_DIR, "AppRoutes.tsx");
|
||||
|
||||
interface RouteNode {
|
||||
path: string;
|
||||
children: RouteNode[];
|
||||
}
|
||||
|
||||
function getAttributePath(attrs: (t.JSXAttribute | t.JSXSpreadAttribute)[]) {
|
||||
const pathAttr = attrs.find(
|
||||
(attr) =>
|
||||
t.isJSXAttribute(attr) &&
|
||||
t.isJSXIdentifier(attr.name, { name: "path" })
|
||||
) as t.JSXAttribute | undefined;
|
||||
|
||||
if (pathAttr && t.isStringLiteral(pathAttr.value)) {
|
||||
return pathAttr.value.value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekursif baca node <Route> beserta anak-anaknya
|
||||
*/
|
||||
function extractRouteNodes(node: t.JSXElement): RouteNode | null {
|
||||
const opening = node.openingElement;
|
||||
|
||||
if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "Route") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentPath = getAttributePath(opening.attributes);
|
||||
|
||||
// cari anak-anak <Route>
|
||||
const children: RouteNode[] = [];
|
||||
for (const child of node.children) {
|
||||
if (t.isJSXElement(child)) {
|
||||
const childNode = extractRouteNodes(child);
|
||||
if (childNode) children.push(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
return { path: currentPath, children };
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten hasil rekursif jadi list path full
|
||||
*/
|
||||
function flattenRoutes(
|
||||
node: RouteNode,
|
||||
parentPath = ""
|
||||
): Record<string, string> {
|
||||
const record: Record<string, string> = {};
|
||||
|
||||
// gabung path parent + child
|
||||
let fullPath = node.path;
|
||||
if (fullPath) {
|
||||
if (parentPath) {
|
||||
if (fullPath === "/") {
|
||||
fullPath = parentPath;
|
||||
} else {
|
||||
fullPath = `${parentPath.replace(/\/$/, "")}/${fullPath.replace(
|
||||
/^\//,
|
||||
""
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
if (!fullPath.startsWith("/")) {
|
||||
fullPath = `/${fullPath}`;
|
||||
}
|
||||
record[fullPath] = fullPath;
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
Object.assign(record, flattenRoutes(child, fullPath || parentPath));
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
function extractRoutes(code: string): Record<string, string> {
|
||||
const ast = parser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript", "jsx"],
|
||||
});
|
||||
|
||||
const routes: Record<string, string> = {};
|
||||
|
||||
traverse(ast, {
|
||||
JSXElement(path) {
|
||||
const node = path.node;
|
||||
const opening = node.openingElement;
|
||||
if (t.isJSXIdentifier(opening.name) && opening.name.name === "Routes") {
|
||||
for (const child of node.children) {
|
||||
if (t.isJSXElement(child)) {
|
||||
const routeNode = extractRouteNodes(child);
|
||||
if (routeNode) {
|
||||
Object.assign(routes, flattenRoutes(routeNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export default function route() {
|
||||
if (!fs.existsSync(APP_ROUTES_FILE)) {
|
||||
console.error("❌ AppRoutes.tsx not found in src/");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const code = fs.readFileSync(APP_ROUTES_FILE, "utf-8");
|
||||
const routes = extractRoutes(code);
|
||||
|
||||
console.log("✅ Generated Routes:");
|
||||
console.log(routes);
|
||||
|
||||
const outPath = path.join(SRC_DIR, "clientRoutes.ts");
|
||||
fs.writeFileSync(
|
||||
outPath,
|
||||
`// AUTO-GENERATED FILE\nconst clientRoutes = ${JSON.stringify(
|
||||
routes,
|
||||
null,
|
||||
2
|
||||
)} as const;\n\nexport default clientRoutes;`
|
||||
);
|
||||
console.log(`📄 clientRoutes.ts generated at ${outPath}`);
|
||||
}
|
||||
Reference in New Issue
Block a user