curlie 1.0.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,202 @@
1
+ # nodeCurl
2
+
3
+ A curl-like CLI tool for Node.js - transfer data from HTTP, HTTPS, FTP servers and local files.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g curlie
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ nodecurl [options] <url>
15
+ nodecurl ftp://<host>/<path>
16
+ nodecurl file://<path>
17
+ ```
18
+
19
+ ## Options
20
+
21
+ ### HTTP Options
22
+
23
+ | Flag | Description |
24
+ |------|-------------|
25
+ | `-X, --request <method>` | HTTP method (GET, POST, PUT, DELETE, etc) |
26
+ | `-d, --data <data>` | HTTP POST data |
27
+ | `--json <json>` | JSON data (sets Content-Type: application/json) |
28
+ | `-H, --header <header>` | Add custom header (Header: Value) |
29
+ | `-A, --user-agent <name>` | User-Agent string |
30
+ | `-e, --referer <URL>` | Referer URL |
31
+ | `-b, --cookie <data>` | Cookie string or @filename |
32
+ | `-c, --cookie-jar <file>` | Save cookies to file |
33
+ | `-i, --include` | Include response headers in output |
34
+ | `-v, --verbose` | Show request/response details |
35
+ | `-k, --insecure` | Allow insecure SSL connections |
36
+ | `-L, --location` | Follow redirects |
37
+ | `--max-redirs <num>` | Max redirects to follow |
38
+ | `--retry <num>` | Retry on transient errors |
39
+ | `--limit-rate <speed>` | Limit transfer speed |
40
+
41
+ ### Output Options
42
+
43
+ | Flag | Description |
44
+ |------|-------------|
45
+ | `-o, --output <file>` | Write to file |
46
+ | `-O, --remote-name` | Write using remote filename |
47
+ | `--output-dir <dir>` | Output directory |
48
+ | `-I, --head` | Fetch headers only |
49
+ | `-R, --remote-time` | Preserve remote timestamp |
50
+ | `--no-clobber` | Don't overwrite files |
51
+ | `--create-dirs` | Create directories |
52
+
53
+ ### Connection Options
54
+
55
+ | Flag | Description |
56
+ |------|-------------|
57
+ | `--max-time <seconds>` | Total timeout |
58
+ | `--connect-timeout <seconds>` | Connection timeout |
59
+ | `--limit-rate <speed>` | Speed limit (e.g. 1M, 100K) |
60
+ | `-Z, --parallel <urls>` | Parallel downloads |
61
+ | `--parallel-max <num>` | Max parallel connections |
62
+
63
+ ### DNS Options
64
+
65
+ | Flag | Description |
66
+ |------|-------------|
67
+ | `--resolve <host:port:addr>` | Custom address for host:port |
68
+ | `--dns-servers <addrs>` | DNS servers (comma-separated) |
69
+ | `--doh-url <url>` | DNS-over-HTTPS URL |
70
+ | `-4, --ipv4` | IPv4 only |
71
+ | `-6, --ipv6` | IPv6 only |
72
+
73
+ ### FTP Options
74
+
75
+ | Flag | Description |
76
+ |------|-------------|
77
+ | `-u, --user <user:password>` | Authentication |
78
+ | `-l, --list-only` | List directory contents |
79
+ | `-I, --head` | Get file metadata only |
80
+ | `-o, --output <file>` | Download to local file |
81
+ | `-T, --upload <file>` | Upload local file |
82
+ | `-a, --append` | Append to remote file |
83
+ | `-Q, --quote <command>` | Command before transfer |
84
+ | `-v, --verbose` | Show protocol details |
85
+
86
+ ### Authentication
87
+
88
+ | Flag | Description |
89
+ |------|-------------|
90
+ | `-u, --user <user:password>` | Server user and password |
91
+ | `-k, --insecure` | Allow insecure server connections |
92
+
93
+ ## Examples
94
+
95
+ ### Basic Requests
96
+
97
+ ```bash
98
+ # Simple GET request
99
+ nodecurl https://example.com
100
+
101
+ # With SSL skip (insecure)
102
+ nodecurl -k https://example.com
103
+
104
+ # POST with data
105
+ nodecurl -X POST -d "name=test&value=123" https://api.example.com
106
+
107
+ # POST with JSON
108
+ nodecurl --json '{"name":"test"}' https://api.example.com
109
+ ```
110
+
111
+ ### File Downloads
112
+
113
+ ```bash
114
+ # Save to specific file
115
+ nodecurl -o page.html https://example.com/page
116
+
117
+ # Save with remote filename
118
+ nodecurl -O https://example.com/file.txt
119
+
120
+ # Save to directory
121
+ nodecurl --output-dir ./downloads https://example.com/file
122
+ ```
123
+
124
+ ### Headers & Cookies
125
+
126
+ ```bash
127
+ # Custom headers
128
+ nodecurl -H "Authorization: Bearer token" https://api.example.com
129
+
130
+ # Custom User-Agent
131
+ nodecurl -A "MyBot/1.0" https://example.com
132
+
133
+ # Save cookies
134
+ nodecurl -c cookies.txt https://example.com
135
+
136
+ # Send cookies
137
+ nodecurl -b cookies.txt https://example.com
138
+ ```
139
+
140
+ ### Redirects
141
+
142
+ ```bash
143
+ # Follow redirects (default)
144
+ nodecurl -L https://example.com/redirect
145
+
146
+ # Limit redirects
147
+ nodecurl -L --max-redirs 3 https://example.com/redirect
148
+ ```
149
+
150
+ ### FTP
151
+
152
+ ```bash
153
+ # List directory
154
+ nodecurl ftp://ftp.example.com/
155
+
156
+ # Download file
157
+ nodecurl -o local.txt ftp://ftp.example.com/file.txt
158
+
159
+ # Upload file
160
+ nodecurl -T local.txt ftp://ftp.example.com/upload/
161
+ ```
162
+
163
+ ### Advanced
164
+
165
+ ```bash
166
+ # Custom DNS resolve
167
+ nodecurl --resolve example.com:443:127.0.0.1 https://example.com
168
+
169
+ # DNS-over-HTTPS
170
+ nodecurl --doh-url https://dns.google/resolve https://example.com
171
+
172
+ # Speed limit
173
+ nodecurl --limit-rate 1M https://example.com/large-file
174
+
175
+ # Parallel downloads
176
+ nodecurl -Z url1 url2 url3
177
+
178
+ # Verbose output
179
+ nodecurl -v https://example.com
180
+ ```
181
+
182
+ ## Help
183
+
184
+ ```bash
185
+ nodecurl -h # Show all options
186
+ nodecurl http -h # HTTP options only
187
+ nodecurl ftp -h # FTP options only
188
+ nodecurl dns -h # DNS options only
189
+ ```
190
+
191
+ ## Exit Codes
192
+
193
+ - `0` - Success
194
+ - `1` - General error
195
+ - `2` - Parse error
196
+ - `3` - Network error
197
+ - `4` - SSL error
198
+ - `28` - Operation timeout
199
+
200
+ ## License
201
+
202
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { parseFlags } from "../lib/flags.js";
3
+ import { ftpList } from "../lib/ftpHandler.js";
4
+ import { expandLocalVars, isFileURL, isFtp, showHelp } from "../lib/utils.js";
5
+ import { readLocalFile, createFile, readHeaders } from "../lib/fileHandler.js";
6
+ import { parallelRequests, request } from "../lib/httpHandler.js";
7
+
8
+ const args = process.argv.slice(2);
9
+ const flags = parseFlags(args);
10
+ const nonFlagArgs = args.filter((arg) => !arg.startsWith("-"));
11
+ const module = nonFlagArgs[0];
12
+
13
+ let url = args.find(
14
+ (arg) =>
15
+ arg.startsWith("http://") ||
16
+ arg.startsWith("https://") ||
17
+ arg.startsWith("file://") ||
18
+ arg.startsWith("ftp://"),
19
+ );
20
+
21
+ let urls = args.filter((item) => !item.startsWith("--")) || [];
22
+
23
+ url = expandLocalVars(url);
24
+
25
+ if (flags.user === undefined) flags.user = "anonymous:anonymous";
26
+
27
+ const main = async () => {
28
+ if (!url && !flags.help && !flags.parallel) {
29
+ console.log("Error: Usage: ./index.js [options] <url>");
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+
34
+ if (flags.help) {
35
+ showHelp(module);
36
+ return;
37
+ }
38
+
39
+ if (isFtp(url)) {
40
+ await ftpList(url, flags);
41
+ return;
42
+ }
43
+
44
+ if (isFileURL(url)) {
45
+ if (flags.upload) {
46
+ createFile(url, flags);
47
+ } else if (flags.head) {
48
+ await readHeaders(url, flags);
49
+ } else {
50
+ readLocalFile(url, flags);
51
+ }
52
+ return;
53
+ }
54
+
55
+ if (flags.parallel) {
56
+ await parallelRequests(urls, flags);
57
+ } else {
58
+ let method = flags.method || (flags.upload ? "PUT" : "GET");
59
+ await request(url, method, flags);
60
+ }
61
+ };
62
+
63
+ (async () => {
64
+ try {
65
+ await main();
66
+ } catch (error) {
67
+ console.error("Error:", error.message);
68
+ process.exit(1);
69
+ }
70
+ })();
@@ -0,0 +1 @@
1
+ hello ftp
@@ -0,0 +1,7 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import { defineConfig } from "eslint/config";
4
+
5
+ export default defineConfig([
6
+ { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
7
+ ]);
@@ -0,0 +1,141 @@
1
+ import dns from "dns";
2
+ import https from "https";
3
+ import { URL } from "url";
4
+
5
+ export function createDnsLookup(flags = {}) {
6
+ const resolveMap = parseResolveFlags(flags.resolve);
7
+
8
+ const customServers = flags.dnsServers || null;
9
+ const useIPv4 = !!flags.ipv4;
10
+ const useIPv6 = !!flags.ipv6;
11
+
12
+ if (customServers) {
13
+ try {
14
+ const servers = customServers.flatMap((s) => s.split(","));
15
+ dns.setServers(servers);
16
+ } catch (err) {
17
+ console.warn("[dns] failed to set DNS servers:", err.message);
18
+ }
19
+ }
20
+
21
+ if (flags.dohUrl) {
22
+ return function lookup(hostname, opts = {}, cb) {
23
+ dohLookup(flags.dohUrl, hostname, opts, useIPv4, useIPv6, cb);
24
+ };
25
+ }
26
+
27
+ if (resolveMap.size > 0) {
28
+ return function lookup(hostname, opts = {}, cb) {
29
+ const port = opts.port;
30
+ const key = port ? `${hostname}:${port}` : null;
31
+
32
+ if (key && resolveMap.has(key)) {
33
+ const ip = resolveMap.get(key);
34
+ const family = ip.includes(":") ? 6 : 4;
35
+
36
+ if (opts.all) {
37
+ return cb(null, [{ address: ip, family }]);
38
+ }
39
+
40
+ return cb(null, ip, family);
41
+ }
42
+
43
+ defaultLookup(hostname, opts, useIPv4, useIPv6, cb);
44
+ };
45
+ }
46
+
47
+ return function lookup(hostname, opts = {}, cb) {
48
+ defaultLookup(hostname, opts, useIPv4, useIPv6, cb);
49
+ };
50
+ }
51
+
52
+ function parseResolveFlags(list) {
53
+ const map = new Map();
54
+ if (!list) return map;
55
+
56
+ if (typeof list === "string") list = [list];
57
+
58
+ for (const entry of list) {
59
+ const [host, port, addr] = entry.split(":");
60
+ if (host && port && addr) {
61
+ map.set(`${host}:${port}`, addr);
62
+ }
63
+ }
64
+ return map;
65
+ }
66
+
67
+
68
+ function defaultLookup(hostname, opts, force4, force6, cb) {
69
+ const family = force4 ? 4 : force6 ? 6 : undefined;
70
+
71
+ dns.lookup(hostname, { ...opts, family }, (err, res, fam) => {
72
+ if (err) {
73
+ console.error("[dns] lookup error:", err.message);
74
+ return cb(err);
75
+ }
76
+
77
+ if (Array.isArray(res)) {
78
+ if (opts.all) {
79
+ return cb(null, res);
80
+ }
81
+
82
+ const first = res[0];
83
+ return cb(null, first.address, first.family);
84
+ }
85
+
86
+ cb(null, res, fam);
87
+ });
88
+ }
89
+
90
+ function dohLookup(dohUrl, hostname, opts, force4, force6, cb) {
91
+ try {
92
+ const wantIPv6 = force6 && !force4;
93
+ const rrType = wantIPv6 ? 28 : 1;
94
+
95
+ const url = new URL(dohUrl);
96
+ url.searchParams.set("name", hostname);
97
+ url.searchParams.set("type", wantIPv6 ? "AAAA" : "A");
98
+
99
+ https
100
+ .get(url, { headers: { accept: "application/dns-json" } }, (res) => {
101
+ let body = "";
102
+
103
+ res.on("data", (chunk) => (body += chunk));
104
+ res.on("end", () => {
105
+ try {
106
+ const json = JSON.parse(body);
107
+
108
+ const answer = json.Answer?.find(
109
+ (a) =>
110
+ a.type === rrType &&
111
+ typeof a.data === "string" &&
112
+ isValidIP(a.data),
113
+ );
114
+
115
+ if (!answer) {
116
+ return cb(new Error("[dns:doh] no usable DNS answer"));
117
+ }
118
+
119
+ const address = answer.data;
120
+ const family = wantIPv6 ? 6 : 4;
121
+
122
+ if (opts.all) {
123
+ return cb(null, [{ address, family }]);
124
+ }
125
+
126
+ cb(null, address, family);
127
+ } catch (err) {
128
+ cb(err);
129
+ }
130
+ });
131
+ })
132
+ .on("error", cb);
133
+ } catch (err) {
134
+ cb(err);
135
+ }
136
+ }
137
+
138
+
139
+ function isValidIP(ip) {
140
+ return typeof ip === "string" && (ip.includes(".") || ip.includes(":"));
141
+ }
@@ -0,0 +1,77 @@
1
+ import fs from "fs";
2
+ import { writeOutput, expandLocalVars } from "./utils.js";
3
+ import { stat as statAsync } from "fs/promises";
4
+
5
+ export function readLocalFile(url, flags) {
6
+ const filePath = url.replace("file://", "");
7
+
8
+ if (!fs.existsSync(filePath)) {
9
+ console.error("File does not exist:", filePath);
10
+ process.exit(1);
11
+ }
12
+
13
+ const stat = fs.statSync(filePath);
14
+
15
+ if (flags.head) {
16
+ writeOutput(
17
+ `Status: 200 OK\nContent-Length: ${stat.size}\nContent-Type: text/plain\n`,
18
+ flags,
19
+ );
20
+ return;
21
+ }
22
+
23
+ if (flags["list-only"]) {
24
+ if (!stat.isDirectory()) {
25
+ console.error("-l works only on directories");
26
+ process.exit(1);
27
+ }
28
+ const items = fs.readdirSync(filePath).join("\n");
29
+ writeOutput(items, flags);
30
+ return;
31
+ }
32
+
33
+ let content = fs.readFileSync(filePath);
34
+ writeOutput(content, flags);
35
+ }
36
+
37
+ export function createFile(url, flags) {
38
+ const targetPath = url.replace("file://", "");
39
+ let data;
40
+
41
+ if (flags.upload === "-") {
42
+ data = fs.readFileSync(0);
43
+ } else {
44
+ const uploadPath = expandLocalVars(flags.upload);
45
+ if (!fs.existsSync(uploadPath)) {
46
+ console.error("Upload file does not exist:", uploadPath);
47
+ process.exit(1);
48
+ }
49
+ data = fs.readFileSync(uploadPath);
50
+ }
51
+
52
+ const mode = flags["create-file-mode"]
53
+ ? parseInt(flags["create-file-mode"], 8)
54
+ : 0o644;
55
+
56
+ fs.writeFileSync(targetPath, data, { mode });
57
+ fs.chmodSync(targetPath, mode);
58
+ console.log(`Created file: ${targetPath} (${mode.toString(8)})`);
59
+ }
60
+
61
+ export async function readHeaders(url, flags) {
62
+ const filePath = url.replace("file://", "");
63
+ try {
64
+ const stats = await statAsync(filePath);
65
+
66
+ const headers =
67
+ `Status: 200 OK\n` +
68
+ `Content-Length: ${stats.size}\n` +
69
+ `Last-Modified: ${stats.mtime.toUTCString()}\n`;
70
+
71
+ // writeOutput(headers, flags);
72
+ console.log(headers)
73
+ } catch (err) {
74
+ console.error("File not found:", filePath);
75
+ process.exit(1);
76
+ }
77
+ }
package/lib/flags.js ADDED
@@ -0,0 +1,141 @@
1
+ const FLAG_DEFS = {
2
+ "-o": { key: "output", type: "string" },
3
+ "-l": { key: "listOnly", type: "boolean" },
4
+ "--list-only": { key: "listOnly", type: "boolean" },
5
+ "-4": { key: "ipv4", type: "boolean" },
6
+ "-6": { key: "ipv6", type: "boolean" },
7
+ "-h": { key: "help", type: "boolean" },
8
+
9
+ // output module
10
+ "-O": { key: "remoteName", type: "boolean" },
11
+ "-I": { key: "head", type: "boolean" },
12
+ "-R": { key: "preserveTimeStamp", type: "boolean" },
13
+ "--create-dirs": { key: "createDirs", type: "boolean" },
14
+ "--create-file-mode": { key: "create-file-mode", type: "boolean" },
15
+ "--no-clobber": { key: "noClobber", type: "boolean" },
16
+ "-N": { key: "noBuffer", type: "boolean" },
17
+ "--output-dir": { key: "outputDir", type: "string" },
18
+
19
+ // ftp module
20
+ "-Q": { key: "quote", type: "array" },
21
+ "-a": { key: "append", type: "boolean" },
22
+ "--append": { key: "append", type: "boolean" },
23
+
24
+ "--retry": { key: "retry", type: "number" },
25
+
26
+ // connection module
27
+ "--limit-rate": { key: "rateLimit", type: "number" },
28
+ "--max-time": { key: "maxTimeout", type: "number" },
29
+ "--connect-timeout": { key: "timeout", type: "number" },
30
+ "-Y": { key: "speedLimit", type: "number" },
31
+ "--speed-limit": { key: "speedLimit", type: "number" },
32
+ "-y": { key: "speedTime", type: "number" },
33
+ "--speed-time": { key: "speedTime", type: "number" },
34
+ "-Z": { key: "parallel", type: "array" },
35
+ "--parallel": { key: "parallel", type: "array" },
36
+ "--parallel-max": { key: "parallelMax", type: "number" },
37
+ "--parallel-immediate": { key: "parallelImmediate", type: "boolean" },
38
+ "--max-filesize": { key: "maxFilesize", type: "number" },
39
+ // ---------------- //
40
+
41
+ // DNS / resolving
42
+ "--dns-servers": { key: "dnsServers", type: "array" },
43
+ "--doh-url": { key: "dohUrl", type: "string" },
44
+ "--doh-insecure": { key: "dohInsecure", type: "boolean" },
45
+ "--doh-cert-status": { key: "dohCertStatus", type: "boolean" },
46
+
47
+ "--connect-to": { key: "connectTo", type: "array" },
48
+ "--dns-interface": { key: "dnsInterface", type: "string" },
49
+ "--dns-ipv4-addr": { key: "dnsIpv4Addr", type: "string" },
50
+ "--dns-ipv6-addr": { key: "dnsIpv6Addr", type: "string" },
51
+ // ----------------- //
52
+
53
+ "-u": { key: "user", type: "string" },
54
+ "-T": { key: "upload", type: "string" },
55
+ "--upload-file": { key: "upload", type: "string" },
56
+
57
+ "-X": { key: "method", type: "string" },
58
+ "-d": { key: "data", type: "string" },
59
+ "--data": { key: "data", type: "string" },
60
+ "--json": { key: "json", type: "string" },
61
+ "-H": { key: "headers", type: "array" },
62
+ "--header": { key: "headers", type: "array" },
63
+ "--POST": { key: "POST", type: "boolean" },
64
+ "--PUT": { key: "PUT", type: "boolean" },
65
+ "--DELETE": { key: "DELETE", type: "boolean" },
66
+ "--GET": { key: "GET", type: "boolean" },
67
+ "--resolve": { key: "resolve", type: "array" },
68
+
69
+ // SSL/TLS
70
+ "-k": { key: "insecure", type: "boolean" },
71
+ "--insecure": { key: "insecure", type: "boolean" },
72
+
73
+ // Redirects
74
+ "-L": { key: "location", type: "boolean" },
75
+ "--location": { key: "location", type: "boolean" },
76
+ "--max-redirs": { key: "maxRedirs", type: "number" },
77
+
78
+ // Headers
79
+ "-A": { key: "userAgent", type: "string" },
80
+ "--user-agent": { key: "userAgent", type: "string" },
81
+ "-e": { key: "referer", type: "string" },
82
+ "--referer": { key: "referer", type: "string" },
83
+
84
+ // Output
85
+ "-i": { key: "include", type: "boolean" },
86
+ "--include": { key: "include", type: "boolean" },
87
+ "-v": { key: "verbose", type: "boolean" },
88
+ "--verbose": { key: "verbose", type: "boolean" },
89
+
90
+ // Cookies
91
+ "-c": { key: "cookieJar", type: "string" },
92
+ "--cookie-jar": { key: "cookieJar", type: "string" },
93
+ "-b": { key: "cookie", type: "string" },
94
+ "--cookie": { key: "cookie", type: "string" },
95
+ };
96
+
97
+ export function parseFlags(args) {
98
+ const flags = {};
99
+
100
+ for (let i = 0; i < args.length; i++) {
101
+ let arg = args[i];
102
+ let value;
103
+
104
+ // Handle --key=value
105
+ if (arg.includes("=")) {
106
+ const parts = arg.split("=");
107
+ arg = parts[0];
108
+ value = parts.slice(1).join("=");
109
+ }
110
+
111
+ const def = FLAG_DEFS[arg];
112
+ if (!def) continue;
113
+
114
+ switch (def.type) {
115
+ case "boolean":
116
+ flags[def.key] = true;
117
+ break;
118
+
119
+ case "string":
120
+ case "number":
121
+ if (value === undefined) value = args[++i];
122
+ flags[def.key] = def.type === "number" ? Number(value) : value;
123
+ break;
124
+
125
+ case "array":
126
+ if (value === undefined) value = args[++i];
127
+ flags[def.key] ??= [];
128
+ if (def.key === "headers") {
129
+ // Parse "Key: Value" to object
130
+ const [k, ...v] = value.split(":");
131
+ flags.headers ??= {};
132
+ flags.headers[k.trim()] = v.join(":").trim();
133
+ } else {
134
+ flags[def.key].push(value);
135
+ }
136
+ break;
137
+ }
138
+ }
139
+
140
+ return flags;
141
+ }