fylzap 1.1.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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # fylzap
2
+
3
+ Convert files from your terminal — PDF, DOCX, images and more.
4
+
5
+ ## Install
6
+ ```bash
7
+ npm install -g fylzap
8
+ # or run without installing:
9
+ npx fylzap document.pdf
10
+ ```
11
+
12
+ ## Setup
13
+
14
+ Get your API key from [yourapp.com/dashboard](https://yourapp.com/dashboard), then:
15
+ ```bash
16
+ fylzap config --key fc_live_your_key_here
17
+ ```
18
+
19
+ ## Usage
20
+ ```bash
21
+ # Convert PDF to Markdown
22
+ fylzap document.pdf
23
+
24
+ # Specify output format
25
+ fylzap document.pdf --output txt
26
+
27
+ # Convert image with OCR
28
+ fylzap scan.png --output txt --language eng
29
+
30
+ # Save to specific directory
31
+ fylzap document.pdf --out-dir ./output
32
+
33
+ # Use AI mode for better PDF quality
34
+ fylzap document.pdf --ai
35
+
36
+ # Multiple files
37
+ fylzap *.pdf --output md --out-dir ./converted
38
+
39
+ # One-time key (without saving)
40
+ fylzap document.pdf --key fc_live_xxx
41
+ ```
42
+
43
+ ## Supported Formats
44
+
45
+ | Input | Output |
46
+ |-------|--------|
47
+ | PDF | Markdown, Text |
48
+ | Word (DOCX) | Markdown, Text |
49
+ | CSV | JSON |
50
+ | JSON | CSV |
51
+ | Images (JPG, PNG, WEBP, etc.) | Text (OCR) |
52
+
53
+ ## Commands
54
+
55
+ | Command | Description |
56
+ |---------|-------------|
57
+ | `fylzap <file>` | Convert a file |
58
+ | `fylzap config --key <key>` | Save API key |
59
+ | `fylzap config --show` | Show current config |
60
+ | `fylzap formats` | List supported formats |
61
+
62
+ ## Environment Variables
63
+ ```bash
64
+ FYLZAP_API_KEY=fc_live_xxx # API key
65
+ FYLZAP_API_URL=https://... # Custom API URL
66
+ ```
@@ -0,0 +1,10 @@
1
+ interface ConvertOptions {
2
+ output: string;
3
+ outDir?: string;
4
+ language: string;
5
+ ai: boolean;
6
+ key?: string;
7
+ }
8
+ export declare function convertFile(filePath: string, options: ConvertOptions): Promise<void>;
9
+ export declare function convertMultiple(files: string[], options: ConvertOptions): Promise<void>;
10
+ export {};
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.convertFile = convertFile;
7
+ exports.convertMultiple = convertMultiple;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const form_data_1 = __importDefault(require("form-data"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ const progress_1 = require("../utils/progress");
14
+ const formatter_1 = require("../utils/formatter");
15
+ const config_1 = require("../utils/config");
16
+ const SUPPORTED_INPUTS = [
17
+ "pdf", "docx",
18
+ "md",
19
+ "csv", "json",
20
+ "yaml", "yml",
21
+ "jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp",
22
+ ];
23
+ const SUPPORTED_OUTPUTS = {
24
+ pdf: ["md", "txt", "json"],
25
+ docx: ["md", "txt"],
26
+ md: ["pdf", "epub"],
27
+ csv: ["json"],
28
+ json: ["csv"],
29
+ yaml: ["md"],
30
+ yml: ["md"],
31
+ jpg: ["txt"], jpeg: ["txt"], png: ["txt"],
32
+ gif: ["txt"], bmp: ["txt"], tiff: ["txt"], webp: ["txt"],
33
+ };
34
+ async function convertFile(filePath, options) {
35
+ const absPath = path_1.default.resolve(filePath);
36
+ if (!fs_1.default.existsSync(absPath)) {
37
+ (0, formatter_1.printError)(`File not found: ${filePath}`);
38
+ process.exit(1);
39
+ }
40
+ const ext = path_1.default.extname(absPath).slice(1).toLowerCase();
41
+ if (!SUPPORTED_INPUTS.includes(ext)) {
42
+ (0, formatter_1.printError)(`Unsupported file type: .${ext}`);
43
+ (0, formatter_1.printInfo)(`Supported inputs: ${SUPPORTED_INPUTS.join(", ")}`);
44
+ process.exit(1);
45
+ }
46
+ // Validate output format for input type
47
+ const validOutputs = SUPPORTED_OUTPUTS[ext] ?? [];
48
+ if (!validOutputs.includes(options.output)) {
49
+ (0, formatter_1.printError)(`Cannot convert .${ext} to .${options.output}`);
50
+ (0, formatter_1.printInfo)(`Valid outputs for .${ext}: ${validOutputs.join(", ")}`);
51
+ process.exit(1);
52
+ }
53
+ const apiKey = options.key ?? (0, config_1.getApiKey)();
54
+ if (!apiKey) {
55
+ (0, formatter_1.printError)("No API key found.");
56
+ (0, formatter_1.printInfo)("Set your key with: fylzap config --key fc_live_xxx");
57
+ (0, formatter_1.printInfo)("Or set env var: FYLZAP_API_KEY=fc_live_xxx");
58
+ process.exit(1);
59
+ }
60
+ const apiUrl = (0, config_1.getApiUrl)();
61
+ const outputExt = options.output;
62
+ const filename = path_1.default.basename(absPath);
63
+ const saveDir = (0, formatter_1.getSaveDir)(options.outDir);
64
+ const baseName = path_1.default.basename(absPath, path_1.default.extname(absPath));
65
+ (0, formatter_1.ensureDir)(saveDir);
66
+ const progress = (0, progress_1.createProgressBar)(filename);
67
+ progress.start();
68
+ const startTime = Date.now();
69
+ try {
70
+ progress.update(10, `Uploading ${filename}...`);
71
+ const form = new form_data_1.default();
72
+ form.append("files", fs_1.default.createReadStream(absPath), filename);
73
+ form.append("outputType", outputExt);
74
+ form.append("splitMode", "whole");
75
+ form.append("useAI", options.ai.toString());
76
+ const imageExts = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp"];
77
+ if (imageExts.includes(ext) && outputExt === "txt") {
78
+ form.append("ocrLanguage", options.language);
79
+ }
80
+ progress.update(30, "Converting...");
81
+ const response = await axios_1.default.post(`${apiUrl}/api/converter`, form, {
82
+ headers: {
83
+ ...form.getHeaders(),
84
+ Authorization: `Bearer ${apiKey}`,
85
+ },
86
+ responseType: "arraybuffer",
87
+ maxContentLength: Infinity,
88
+ maxBodyLength: Infinity,
89
+ onUploadProgress: (e) => {
90
+ if (e.total) {
91
+ const pct = Math.round((e.loaded / e.total) * 30) + 10;
92
+ progress.update(pct, "Uploading...");
93
+ }
94
+ },
95
+ });
96
+ progress.update(80, "Saving output...");
97
+ const contentType = response.headers["content-type"] ?? "";
98
+ const isZip = contentType.includes("application/zip");
99
+ const outExt = isZip ? "zip" : outputExt;
100
+ const outFilename = `${baseName}.${outExt}`;
101
+ const outPath = path_1.default.join(saveDir, outFilename);
102
+ fs_1.default.writeFileSync(outPath, Buffer.from(response.data));
103
+ progress.update(100, "Done!");
104
+ progress.stop();
105
+ const elapsed = Date.now() - startTime;
106
+ (0, formatter_1.printSuccess)(absPath, outPath, elapsed);
107
+ if (isZip) {
108
+ (0, formatter_1.printInfo)(`Output is a ZIP (contains markdown + images) → ${outPath}`);
109
+ }
110
+ }
111
+ catch (error) {
112
+ progress.stop();
113
+ if (axios_1.default.isAxiosError(error)) {
114
+ if (error.response?.status === 401) {
115
+ (0, formatter_1.printError)("Invalid or expired API key.");
116
+ (0, formatter_1.printInfo)("Get a new key at: https://fylzap.com/dashboard");
117
+ }
118
+ else if (error.response?.status === 429) {
119
+ (0, formatter_1.printError)("Rate limit reached. Upgrade your plan for more conversions.");
120
+ (0, formatter_1.printInfo)("Upgrade at: https://fylzap.com/pricing");
121
+ }
122
+ else if (error.response?.status === 413) {
123
+ (0, formatter_1.printError)("File too large for your current plan.");
124
+ (0, formatter_1.printInfo)("Upgrade at: https://fylzap.com/pricing");
125
+ }
126
+ else {
127
+ const msg = error.response?.data
128
+ ? Buffer.from(error.response.data).toString()
129
+ : error.message;
130
+ (0, formatter_1.printError)(`Conversion failed: ${msg}`);
131
+ }
132
+ }
133
+ else {
134
+ (0, formatter_1.printError)(`Unexpected error: ${error.message}`);
135
+ }
136
+ process.exit(1);
137
+ }
138
+ }
139
+ async function convertMultiple(files, options) {
140
+ const results = { success: 0, failed: 0 };
141
+ for (const file of files) {
142
+ try {
143
+ await convertFile(file, options);
144
+ results.success++;
145
+ }
146
+ catch {
147
+ results.failed++;
148
+ }
149
+ }
150
+ if (files.length > 1) {
151
+ console.log(chalk_1.default.gray(`\n Summary: `) +
152
+ chalk_1.default.green(`${results.success} converted`) +
153
+ (results.failed > 0 ? chalk_1.default.red(` · ${results.failed} failed`) : ""));
154
+ }
155
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const convert_1 = require("./commands/convert");
10
+ const config_1 = require("./utils/config");
11
+ const formatter_1 = require("./utils/formatter");
12
+ const glob_1 = require("glob");
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name("fylzap")
16
+ .description("Convert files from your terminal — PDF, DOCX, Markdown, images and more")
17
+ .version("1.1.0");
18
+ // ── Convert command (default) ─────────────────────────────────────────────────
19
+ program
20
+ .command("convert <files...>", { isDefault: true })
21
+ .description("Convert one or more files")
22
+ .option("-o, --output <type>", "Output format: md, txt, json, csv, pdf, epub", "md")
23
+ .option("-d, --out-dir <dir>", "Output directory (default: current directory)")
24
+ .option("-l, --language <code>", "OCR language code (for image→txt)", "eng")
25
+ .option("--ai", "Use AI mode for PDF→MD (better quality, slower)")
26
+ .option("-k, --key <apikey>", "API key (overrides saved key)")
27
+ .action(async (files, options) => {
28
+ (0, formatter_1.printBanner)();
29
+ // Expand globs on Windows (Unix handles this in shell)
30
+ const expanded = [];
31
+ for (const pattern of files) {
32
+ if (pattern.includes("*")) {
33
+ const matches = (0, glob_1.globSync)(pattern);
34
+ if (matches.length === 0) {
35
+ (0, formatter_1.printError)(`No files matched: ${pattern}`);
36
+ process.exit(1);
37
+ }
38
+ expanded.push(...matches);
39
+ }
40
+ else {
41
+ expanded.push(pattern);
42
+ }
43
+ }
44
+ if (expanded.length === 1) {
45
+ await (0, convert_1.convertFile)(expanded[0], {
46
+ output: options.output,
47
+ outDir: options.outDir,
48
+ language: options.language,
49
+ ai: !!options.ai,
50
+ key: options.key,
51
+ });
52
+ }
53
+ else {
54
+ (0, formatter_1.printInfo)(`Converting ${expanded.length} files...\n`);
55
+ await (0, convert_1.convertMultiple)(expanded, {
56
+ output: options.output,
57
+ outDir: options.outDir,
58
+ language: options.language,
59
+ ai: !!options.ai,
60
+ key: options.key,
61
+ });
62
+ }
63
+ });
64
+ // ── Config command ────────────────────────────────────────────────────────────
65
+ program
66
+ .command("config")
67
+ .description("Configure fylzap settings")
68
+ .option("-k, --key <apikey>", "Save your API key")
69
+ .option("-u, --url <url>", "Set custom API URL (for self-hosted)")
70
+ .option("--show", "Show current config")
71
+ .option("--clear", "Clear all saved config")
72
+ .action((options) => {
73
+ if (options.key) {
74
+ if (!options.key.startsWith("fc_live_")) {
75
+ (0, formatter_1.printError)("Invalid API key format. Keys must start with fc_live_");
76
+ process.exit(1);
77
+ }
78
+ (0, config_1.setApiKey)(options.key);
79
+ console.log(chalk_1.default.green(" ✓ API key saved"));
80
+ return;
81
+ }
82
+ if (options.url) {
83
+ (0, config_1.setApiUrl)(options.url);
84
+ console.log(chalk_1.default.green(` ✓ API URL set to: ${options.url}`));
85
+ return;
86
+ }
87
+ if (options.clear) {
88
+ (0, config_1.clearConfig)();
89
+ console.log(chalk_1.default.green(" ✓ Config cleared"));
90
+ return;
91
+ }
92
+ if (options.show) {
93
+ const key = (0, config_1.getApiKey)();
94
+ const url = (0, config_1.getApiUrl)();
95
+ console.log(chalk_1.default.gray(" API Key: ") + (key ? chalk_1.default.white(key.substring(0, 20) + "...") : chalk_1.default.red("not set")));
96
+ console.log(chalk_1.default.gray(" API URL: ") + chalk_1.default.white(url));
97
+ return;
98
+ }
99
+ program.commands.find(c => c.name() === "config")?.help();
100
+ });
101
+ // ── Formats command ───────────────────────────────────────────────────────────
102
+ program
103
+ .command("formats")
104
+ .description("List supported input and output formats")
105
+ .action(() => {
106
+ (0, formatter_1.printBanner)();
107
+ console.log(chalk_1.default.bold(" Supported conversions:\n"));
108
+ const formats = [
109
+ ["PDF (.pdf)", "Markdown (.md), Text (.txt), JSON (.json)"],
110
+ ["Word (.docx)", "Markdown (.md), Text (.txt)"],
111
+ ["Markdown (.md)", "PDF (.pdf), EPUB (.epub)"],
112
+ ["CSV (.csv)", "JSON (.json)"],
113
+ ["JSON (.json)", "CSV (.csv)"],
114
+ ["OpenAPI/YAML (.yaml)", "Markdown (.md) — API docs"],
115
+ ["Image (.jpg, .png, etc)", "Text via OCR (.txt)"],
116
+ ];
117
+ formats.forEach(([input, output]) => {
118
+ console.log(chalk_1.default.cyan(` ${input.padEnd(35)}`) +
119
+ chalk_1.default.gray("→ ") +
120
+ chalk_1.default.white(output));
121
+ });
122
+ console.log();
123
+ console.log(chalk_1.default.gray(" Flags:"));
124
+ console.log(chalk_1.default.gray(" --ai ") + chalk_1.default.white("AI-powered PDF→MD (Pro/Enterprise)"));
125
+ console.log(chalk_1.default.gray(" --language ") + chalk_1.default.white("OCR language: eng, ara, urd, fra, ger, spa..."));
126
+ console.log();
127
+ });
128
+ program.parse(process.argv);
@@ -0,0 +1,5 @@
1
+ export declare function getApiKey(): string | undefined;
2
+ export declare function setApiKey(key: string): void;
3
+ export declare function getApiUrl(): string;
4
+ export declare function setApiUrl(url: string): void;
5
+ export declare function clearConfig(): void;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getApiKey = getApiKey;
7
+ exports.setApiKey = setApiKey;
8
+ exports.getApiUrl = getApiUrl;
9
+ exports.setApiUrl = setApiUrl;
10
+ exports.clearConfig = clearConfig;
11
+ const conf_1 = __importDefault(require("conf"));
12
+ const config = new conf_1.default({
13
+ projectName: "fylzap",
14
+ defaults: {
15
+ apiUrl: "https://yourapp.com", // ← replace with your actual domain
16
+ },
17
+ });
18
+ function getApiKey() {
19
+ return process.env.FYLZAP_API_KEY ?? config.get("apiKey");
20
+ }
21
+ function setApiKey(key) {
22
+ config.set("apiKey", key);
23
+ }
24
+ function getApiUrl() {
25
+ return process.env.FYLZAP_API_URL ?? config.get("apiUrl") ?? "https://yourapp.com";
26
+ }
27
+ function setApiUrl(url) {
28
+ config.set("apiUrl", url);
29
+ }
30
+ function clearConfig() {
31
+ config.clear();
32
+ }
@@ -0,0 +1,6 @@
1
+ export declare function printBanner(): void;
2
+ export declare function printSuccess(inputFile: string, outputFile: string, ms: number): void;
3
+ export declare function printError(message: string): void;
4
+ export declare function printInfo(message: string): void;
5
+ export declare function getSaveDir(outputDir?: string): string;
6
+ export declare function ensureDir(dir: string): void;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.printBanner = printBanner;
7
+ exports.printSuccess = printSuccess;
8
+ exports.printError = printError;
9
+ exports.printInfo = printInfo;
10
+ exports.getSaveDir = getSaveDir;
11
+ exports.ensureDir = ensureDir;
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const fs_1 = __importDefault(require("fs"));
15
+ function printBanner() {
16
+ console.log(chalk_1.default.bold.blue("\n fylzap") + chalk_1.default.gray(" — file converter CLI"));
17
+ console.log(chalk_1.default.gray(" ─────────────────────────────\n"));
18
+ }
19
+ function printSuccess(inputFile, outputFile, ms) {
20
+ console.log(chalk_1.default.green(" ✓") +
21
+ chalk_1.default.white(` ${path_1.default.basename(inputFile)}`) +
22
+ chalk_1.default.gray(` → `) +
23
+ chalk_1.default.white(path_1.default.basename(outputFile)) +
24
+ chalk_1.default.gray(` (${(ms / 1000).toFixed(1)}s)`));
25
+ }
26
+ function printError(message) {
27
+ console.log(chalk_1.default.red(" ✗ ") + chalk_1.default.white(message));
28
+ }
29
+ function printInfo(message) {
30
+ console.log(chalk_1.default.gray(" ℹ ") + chalk_1.default.white(message));
31
+ }
32
+ function getSaveDir(outputDir) {
33
+ return outputDir
34
+ ? path_1.default.resolve(outputDir)
35
+ : process.cwd();
36
+ }
37
+ function ensureDir(dir) {
38
+ if (!fs_1.default.existsSync(dir)) {
39
+ fs_1.default.mkdirSync(dir, { recursive: true });
40
+ }
41
+ }
@@ -0,0 +1,5 @@
1
+ export declare function createProgressBar(filename: string): {
2
+ start: () => void;
3
+ update: (pct: number, status: string) => void;
4
+ stop: () => void;
5
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createProgressBar = createProgressBar;
7
+ const cli_progress_1 = __importDefault(require("cli-progress"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ function createProgressBar(filename) {
10
+ const bar = new cli_progress_1.default.SingleBar({
11
+ format: chalk_1.default.cyan("{bar}") + " {percentage}% | {status}",
12
+ barCompleteChar: "█",
13
+ barIncompleteChar: "░",
14
+ hideCursor: true,
15
+ }, cli_progress_1.default.Presets.shades_classic);
16
+ return {
17
+ start: () => bar.start(100, 0, { status: `Converting ${filename}...` }),
18
+ update: (pct, status) => bar.update(pct, { status }),
19
+ stop: () => bar.stop(),
20
+ };
21
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "fylzap",
3
+ "version": "1.1.0",
4
+ "description": "Convert files from your terminal — PDF, DOCX, images and more",
5
+ "keywords": ["converter", "pdf", "markdown", "docx", "ocr", "cli"],
6
+ "author": "Muhammad Ali Raza",
7
+ "license": "MIT",
8
+ "homepage": "https://fylzap.com",
9
+ "main": "dist/index.js",
10
+ "bin": {
11
+ "fylzap": "dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "ts-node src/index.ts",
16
+ "start": "node dist/index.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "axios": "^1.6.0",
21
+ "chalk": "^4.1.2",
22
+ "cli-progress": "^3.12.0",
23
+ "commander": "^11.0.0",
24
+ "conf": "^10.2.0",
25
+ "form-data": "^4.0.0",
26
+ "glob": "^13.0.6",
27
+ "ora": "^5.4.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/cli-progress": "^3.11.0",
31
+ "@types/glob": "^8.1.0",
32
+ "@types/node": "^20.0.0",
33
+ "ts-node": "^10.9.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=16.0.0"
38
+ }
39
+ }
@@ -0,0 +1,184 @@
1
+ import axios from "axios";
2
+ import FormData from "form-data";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import chalk from "chalk";
6
+ import { createProgressBar } from "../utils/progress";
7
+ import { printSuccess, printError, printInfo, getSaveDir, ensureDir } from "../utils/formatter";
8
+ import { getApiKey, getApiUrl } from "../utils/config";
9
+
10
+ const SUPPORTED_INPUTS = [
11
+ "pdf", "docx",
12
+ "md",
13
+ "csv", "json",
14
+ "yaml", "yml",
15
+ "jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp",
16
+ ];
17
+
18
+ const SUPPORTED_OUTPUTS: Record<string, string[]> = {
19
+ pdf: ["md", "txt", "json"],
20
+ docx: ["md", "txt"],
21
+ md: ["pdf", "epub"],
22
+ csv: ["json"],
23
+ json: ["csv"],
24
+ yaml: ["md"],
25
+ yml: ["md"],
26
+ jpg: ["txt"], jpeg: ["txt"], png: ["txt"],
27
+ gif: ["txt"], bmp: ["txt"], tiff: ["txt"], webp: ["txt"],
28
+ };
29
+
30
+ interface ConvertOptions {
31
+ output: string;
32
+ outDir?: string;
33
+ language: string;
34
+ ai: boolean;
35
+ key?: string;
36
+ }
37
+
38
+ export async function convertFile(
39
+ filePath: string,
40
+ options: ConvertOptions
41
+ ): Promise<void> {
42
+ const absPath = path.resolve(filePath);
43
+ if (!fs.existsSync(absPath)) {
44
+ printError(`File not found: ${filePath}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const ext = path.extname(absPath).slice(1).toLowerCase();
49
+ if (!SUPPORTED_INPUTS.includes(ext)) {
50
+ printError(`Unsupported file type: .${ext}`);
51
+ printInfo(`Supported inputs: ${SUPPORTED_INPUTS.join(", ")}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ // Validate output format for input type
56
+ const validOutputs = SUPPORTED_OUTPUTS[ext] ?? [];
57
+ if (!validOutputs.includes(options.output)) {
58
+ printError(`Cannot convert .${ext} to .${options.output}`);
59
+ printInfo(`Valid outputs for .${ext}: ${validOutputs.join(", ")}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ const apiKey = options.key ?? getApiKey();
64
+ if (!apiKey) {
65
+ printError("No API key found.");
66
+ printInfo("Set your key with: fylzap config --key fc_live_xxx");
67
+ printInfo("Or set env var: FYLZAP_API_KEY=fc_live_xxx");
68
+ process.exit(1);
69
+ }
70
+
71
+ const apiUrl = getApiUrl();
72
+ const outputExt = options.output;
73
+ const filename = path.basename(absPath);
74
+ const saveDir = getSaveDir(options.outDir);
75
+ const baseName = path.basename(absPath, path.extname(absPath));
76
+
77
+ ensureDir(saveDir);
78
+
79
+ const progress = createProgressBar(filename);
80
+ progress.start();
81
+ const startTime = Date.now();
82
+
83
+ try {
84
+ progress.update(10, `Uploading ${filename}...`);
85
+
86
+ const form = new FormData();
87
+ form.append("files", fs.createReadStream(absPath), filename);
88
+ form.append("outputType", outputExt);
89
+ form.append("splitMode", "whole");
90
+ form.append("useAI", options.ai.toString());
91
+
92
+ const imageExts = ["jpg","jpeg","png","gif","bmp","tiff","webp"];
93
+ if (imageExts.includes(ext) && outputExt === "txt") {
94
+ form.append("ocrLanguage", options.language);
95
+ }
96
+
97
+ progress.update(30, "Converting...");
98
+
99
+ const response = await axios.post(`${apiUrl}/api/converter`, form, {
100
+ headers: {
101
+ ...form.getHeaders(),
102
+ Authorization: `Bearer ${apiKey}`,
103
+ },
104
+ responseType: "arraybuffer",
105
+ maxContentLength: Infinity,
106
+ maxBodyLength: Infinity,
107
+ onUploadProgress: (e) => {
108
+ if (e.total) {
109
+ const pct = Math.round((e.loaded / e.total) * 30) + 10;
110
+ progress.update(pct, "Uploading...");
111
+ }
112
+ },
113
+ });
114
+
115
+ progress.update(80, "Saving output...");
116
+
117
+ const contentType = response.headers["content-type"] ?? "";
118
+ const isZip = contentType.includes("application/zip");
119
+ const outExt = isZip ? "zip" : outputExt;
120
+ const outFilename = `${baseName}.${outExt}`;
121
+ const outPath = path.join(saveDir, outFilename);
122
+
123
+ fs.writeFileSync(outPath, Buffer.from(response.data));
124
+
125
+ progress.update(100, "Done!");
126
+ progress.stop();
127
+
128
+ const elapsed = Date.now() - startTime;
129
+ printSuccess(absPath, outPath, elapsed);
130
+
131
+ if (isZip) {
132
+ printInfo(`Output is a ZIP (contains markdown + images) → ${outPath}`);
133
+ }
134
+
135
+ } catch (error: any) {
136
+ progress.stop();
137
+
138
+ if (axios.isAxiosError(error)) {
139
+ if (error.response?.status === 401) {
140
+ printError("Invalid or expired API key.");
141
+ printInfo("Get a new key at: https://fylzap.com/dashboard");
142
+ } else if (error.response?.status === 429) {
143
+ printError("Rate limit reached. Upgrade your plan for more conversions.");
144
+ printInfo("Upgrade at: https://fylzap.com/pricing");
145
+ } else if (error.response?.status === 413) {
146
+ printError("File too large for your current plan.");
147
+ printInfo("Upgrade at: https://fylzap.com/pricing");
148
+ } else {
149
+ const msg = error.response?.data
150
+ ? Buffer.from(error.response.data).toString()
151
+ : error.message;
152
+ printError(`Conversion failed: ${msg}`);
153
+ }
154
+ } else {
155
+ printError(`Unexpected error: ${error.message}`);
156
+ }
157
+
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ export async function convertMultiple(
163
+ files: string[],
164
+ options: ConvertOptions
165
+ ): Promise<void> {
166
+ const results = { success: 0, failed: 0 };
167
+
168
+ for (const file of files) {
169
+ try {
170
+ await convertFile(file, options);
171
+ results.success++;
172
+ } catch {
173
+ results.failed++;
174
+ }
175
+ }
176
+
177
+ if (files.length > 1) {
178
+ console.log(
179
+ chalk.gray(`\n Summary: `) +
180
+ chalk.green(`${results.success} converted`) +
181
+ (results.failed > 0 ? chalk.red(` · ${results.failed} failed`) : "")
182
+ );
183
+ }
184
+ }
package/src/index.ts ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import { convertFile, convertMultiple } from "./commands/convert";
6
+ import { setApiKey, setApiUrl, getApiKey, getApiUrl, clearConfig } from "./utils/config";
7
+ import { printBanner, printInfo, printError } from "./utils/formatter";
8
+ import { globSync } from "glob";
9
+ import path from "path";
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name("fylzap")
15
+ .description("Convert files from your terminal — PDF, DOCX, Markdown, images and more")
16
+ .version("1.1.0");
17
+
18
+ // ── Convert command (default) ─────────────────────────────────────────────────
19
+ program
20
+ .command("convert <files...>", { isDefault: true })
21
+ .description("Convert one or more files")
22
+ .option("-o, --output <type>", "Output format: md, txt, json, csv, pdf, epub", "md")
23
+ .option("-d, --out-dir <dir>", "Output directory (default: current directory)")
24
+ .option("-l, --language <code>", "OCR language code (for image→txt)", "eng")
25
+ .option("--ai", "Use AI mode for PDF→MD (better quality, slower)")
26
+ .option("-k, --key <apikey>", "API key (overrides saved key)")
27
+ .action(async (files: string[], options) => {
28
+ printBanner();
29
+
30
+ // Expand globs on Windows (Unix handles this in shell)
31
+ const expanded: string[] = [];
32
+ for (const pattern of files) {
33
+ if (pattern.includes("*")) {
34
+ const matches = globSync(pattern);
35
+ if (matches.length === 0) {
36
+ printError(`No files matched: ${pattern}`);
37
+ process.exit(1);
38
+ }
39
+ expanded.push(...matches);
40
+ } else {
41
+ expanded.push(pattern);
42
+ }
43
+ }
44
+
45
+ if (expanded.length === 1) {
46
+ await convertFile(expanded[0], {
47
+ output: options.output,
48
+ outDir: options.outDir,
49
+ language: options.language,
50
+ ai: !!options.ai,
51
+ key: options.key,
52
+ });
53
+ } else {
54
+ printInfo(`Converting ${expanded.length} files...\n`);
55
+ await convertMultiple(expanded, {
56
+ output: options.output,
57
+ outDir: options.outDir,
58
+ language: options.language,
59
+ ai: !!options.ai,
60
+ key: options.key,
61
+ });
62
+ }
63
+ });
64
+
65
+ // ── Config command ────────────────────────────────────────────────────────────
66
+ program
67
+ .command("config")
68
+ .description("Configure fylzap settings")
69
+ .option("-k, --key <apikey>", "Save your API key")
70
+ .option("-u, --url <url>", "Set custom API URL (for self-hosted)")
71
+ .option("--show", "Show current config")
72
+ .option("--clear", "Clear all saved config")
73
+ .action((options) => {
74
+ if (options.key) {
75
+ if (!options.key.startsWith("fc_live_")) {
76
+ printError("Invalid API key format. Keys must start with fc_live_");
77
+ process.exit(1);
78
+ }
79
+ setApiKey(options.key);
80
+ console.log(chalk.green(" ✓ API key saved"));
81
+ return;
82
+ }
83
+
84
+ if (options.url) {
85
+ setApiUrl(options.url);
86
+ console.log(chalk.green(` ✓ API URL set to: ${options.url}`));
87
+ return;
88
+ }
89
+
90
+ if (options.clear) {
91
+ clearConfig();
92
+ console.log(chalk.green(" ✓ Config cleared"));
93
+ return;
94
+ }
95
+
96
+ if (options.show) {
97
+ const key = getApiKey();
98
+ const url = getApiUrl();
99
+ console.log(chalk.gray(" API Key: ") + (key ? chalk.white(key.substring(0, 20) + "...") : chalk.red("not set")));
100
+ console.log(chalk.gray(" API URL: ") + chalk.white(url));
101
+ return;
102
+ }
103
+
104
+ program.commands.find(c => c.name() === "config")?.help();
105
+ });
106
+
107
+ // ── Formats command ───────────────────────────────────────────────────────────
108
+ program
109
+ .command("formats")
110
+ .description("List supported input and output formats")
111
+ .action(() => {
112
+ printBanner();
113
+ console.log(chalk.bold(" Supported conversions:\n"));
114
+ const formats = [
115
+ ["PDF (.pdf)", "Markdown (.md), Text (.txt), JSON (.json)"],
116
+ ["Word (.docx)", "Markdown (.md), Text (.txt)"],
117
+ ["Markdown (.md)", "PDF (.pdf), EPUB (.epub)"],
118
+ ["CSV (.csv)", "JSON (.json)"],
119
+ ["JSON (.json)", "CSV (.csv)"],
120
+ ["OpenAPI/YAML (.yaml)", "Markdown (.md) — API docs"],
121
+ ["Image (.jpg, .png, etc)", "Text via OCR (.txt)"],
122
+ ];
123
+ formats.forEach(([input, output]) => {
124
+ console.log(
125
+ chalk.cyan(` ${input.padEnd(35)}`) +
126
+ chalk.gray("→ ") +
127
+ chalk.white(output)
128
+ );
129
+ });
130
+ console.log();
131
+ console.log(chalk.gray(" Flags:"));
132
+ console.log(chalk.gray(" --ai ") + chalk.white("AI-powered PDF→MD (Pro/Enterprise)"));
133
+ console.log(chalk.gray(" --language ") + chalk.white("OCR language: eng, ara, urd, fra, ger, spa..."));
134
+ console.log();
135
+ });
136
+
137
+ program.parse(process.argv);
@@ -0,0 +1,33 @@
1
+ import Conf from "conf";
2
+
3
+ interface FylzapConfig {
4
+ apiKey?: string;
5
+ apiUrl?: string;
6
+ }
7
+
8
+ const config = new Conf<FylzapConfig>({
9
+ projectName: "fylzap",
10
+ defaults: {
11
+ apiUrl: "https://yourapp.com", // ← replace with your actual domain
12
+ },
13
+ });
14
+
15
+ export function getApiKey(): string | undefined {
16
+ return process.env.FYLZAP_API_KEY ?? config.get("apiKey");
17
+ }
18
+
19
+ export function setApiKey(key: string): void {
20
+ config.set("apiKey", key);
21
+ }
22
+
23
+ export function getApiUrl(): string {
24
+ return process.env.FYLZAP_API_URL ?? config.get("apiUrl") ?? "https://yourapp.com";
25
+ }
26
+
27
+ export function setApiUrl(url: string): void {
28
+ config.set("apiUrl", url);
29
+ }
30
+
31
+ export function clearConfig(): void {
32
+ config.clear();
33
+ }
@@ -0,0 +1,38 @@
1
+ import chalk from "chalk";
2
+ import path from "path";
3
+ import fs from "fs";
4
+
5
+ export function printBanner() {
6
+ console.log(chalk.bold.blue("\n fylzap") + chalk.gray(" — file converter CLI"));
7
+ console.log(chalk.gray(" ─────────────────────────────\n"));
8
+ }
9
+
10
+ export function printSuccess(inputFile: string, outputFile: string, ms: number) {
11
+ console.log(
12
+ chalk.green(" ✓") +
13
+ chalk.white(` ${path.basename(inputFile)}`) +
14
+ chalk.gray(` → `) +
15
+ chalk.white(path.basename(outputFile)) +
16
+ chalk.gray(` (${(ms / 1000).toFixed(1)}s)`)
17
+ );
18
+ }
19
+
20
+ export function printError(message: string) {
21
+ console.log(chalk.red(" ✗ ") + chalk.white(message));
22
+ }
23
+
24
+ export function printInfo(message: string) {
25
+ console.log(chalk.gray(" ℹ ") + chalk.white(message));
26
+ }
27
+
28
+ export function getSaveDir(outputDir?: string): string {
29
+ return outputDir
30
+ ? path.resolve(outputDir)
31
+ : process.cwd();
32
+ }
33
+
34
+ export function ensureDir(dir: string): void {
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ }
@@ -0,0 +1,20 @@
1
+ import cliProgress from "cli-progress";
2
+ import chalk from "chalk";
3
+
4
+ export function createProgressBar(filename: string) {
5
+ const bar = new cliProgress.SingleBar(
6
+ {
7
+ format: chalk.cyan("{bar}") + " {percentage}% | {status}",
8
+ barCompleteChar: "█",
9
+ barIncompleteChar: "░",
10
+ hideCursor: true,
11
+ },
12
+ cliProgress.Presets.shades_classic
13
+ );
14
+
15
+ return {
16
+ start: () => bar.start(100, 0, { status: `Converting ${filename}...` }),
17
+ update: (pct: number, status: string) => bar.update(pct, { status }),
18
+ stop: () => bar.stop(),
19
+ };
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }