snowkit 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +142 -2
- package/bin/snowkit.js +19 -0
- package/package.json +45 -4
- package/src/cli.js +177 -0
- package/src/engine/colorkit.js +338 -0
- package/src/engine/cron.js +148 -0
- package/src/engine/devtools-kit.js +1964 -0
- package/src/engine/docpdf-layout.js +1188 -0
- package/src/engine/docpdf-render.js +106 -0
- package/src/engine/html.js +59 -0
- package/src/engine/jsonkit.js +1241 -0
- package/src/engine/md-render.js +596 -0
- package/src/engine/mdpdf.js +113 -0
- package/src/engine/qrkit.js +1205 -0
- package/src/engine/readability.js +100 -0
- package/src/engine/sqlformat.js +425 -0
- package/src/engine/texttools-kit.js +623 -0
- package/src/files.js +132 -0
- package/src/pdf.js +297 -0
- package/src/qr.js +100 -0
- package/src/server.js +50 -0
- package/src/text.js +189 -0
- package/src/tools.js +223 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Snowkit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Snowkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Private tools that run on your computer.
|
|
4
|
+
|
|
5
|
+
Snowkit merges and splits PDFs, makes QR codes, tidies JSON and SQL, counts words and more. It works from the command line, and it plugs into AI apps that support the Model Context Protocol (MCP), so your assistant can do real work on your files.
|
|
6
|
+
|
|
7
|
+
Your files never leave your computer. Snowkit has no account, sends nothing over the internet and keeps no copies. It reads the files you point it to and writes the results next to them.
|
|
8
|
+
|
|
9
|
+
## What it can do
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `pdf_merge` | Combines two or more PDFs into one, in the order you give |
|
|
14
|
+
| `pdf_split` | Splits a PDF into one file per page, every few pages, or by page ranges |
|
|
15
|
+
| `pdf_extract_pages` | Copies chosen pages into a new PDF, which also reorders or drops pages |
|
|
16
|
+
| `pdf_rotate` | Turns all or some pages by 90, 180 or 270 degrees |
|
|
17
|
+
| `pdf_info` | Shows page count, page sizes, PDF version, form fields and document details |
|
|
18
|
+
| `markdown_to_pdf` | Turns Markdown into a clean, paginated PDF |
|
|
19
|
+
| `qr_code` | Makes a QR code for a link or any text, as SVG or PNG |
|
|
20
|
+
| `json_format` | Pretty-prints or minifies JSON, keeping large numbers exact |
|
|
21
|
+
| `json_validate` | Checks JSON, and optionally checks it against a JSON Schema |
|
|
22
|
+
| `sql_format` | Formats or minifies SQL |
|
|
23
|
+
| `text_stats` | Counts words, characters and sentences, with reading time and readability |
|
|
24
|
+
| `text_transform` | Changes case, makes URL slugs, or removes duplicate lines |
|
|
25
|
+
| `color_convert` | Converts a colour between hex, RGB, HSL, HSV and OKLCH, and checks contrast |
|
|
26
|
+
| `cron_explain` | Explains a cron schedule in plain English and lists the next runs |
|
|
27
|
+
|
|
28
|
+
Each result ends with a link to the matching tool on [snowkit.io](https://snowkit.io), in case you would rather see it in your browser.
|
|
29
|
+
|
|
30
|
+
## Use it with your AI app
|
|
31
|
+
|
|
32
|
+
You need [Node.js](https://nodejs.org) 18 or newer. There is nothing else to install: the commands below fetch Snowkit the first time it runs.
|
|
33
|
+
|
|
34
|
+
### Claude Desktop
|
|
35
|
+
|
|
36
|
+
Open **Settings**, then **Developer**, then **Edit Config**, and add Snowkit to `claude_desktop_config.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"snowkit": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "snowkit"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Restart Claude Desktop. If it cannot find `npx` on Windows, use `"command": "cmd"` and `"args": ["/c", "npx", "-y", "snowkit"]`.
|
|
50
|
+
|
|
51
|
+
### Claude Code
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
claude mcp add snowkit -- npx -y snowkit
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Cursor
|
|
58
|
+
|
|
59
|
+
Open **Cursor Settings**, then **MCP**, then **Add new MCP server**, or add this to `~/.cursor/mcp.json` (or `.cursor/mcp.json` inside a project):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"snowkit": {
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "snowkit"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Gemini CLI
|
|
73
|
+
|
|
74
|
+
Add this to `~/.gemini/settings.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"snowkit": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": ["-y", "snowkit"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Any other MCP app
|
|
88
|
+
|
|
89
|
+
Snowkit is a standard MCP server over standard input and output. Point your app at the command `npx -y snowkit`.
|
|
90
|
+
|
|
91
|
+
Then just ask, for example:
|
|
92
|
+
|
|
93
|
+
- "Merge invoice-march.pdf and invoice-april.pdf in my Downloads folder."
|
|
94
|
+
- "Pull pages 3 to 5 out of contract.pdf."
|
|
95
|
+
- "Make a QR code PNG for https://example.com and save it to my desktop."
|
|
96
|
+
- "Is this JSON valid?"
|
|
97
|
+
|
|
98
|
+
## Use it from the command line
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npx snowkit pdf merge a.pdf b.pdf -o both.pdf
|
|
102
|
+
npx snowkit pdf split report.pdf --every 2
|
|
103
|
+
npx snowkit pdf split report.pdf --range 1-3 --range 4-
|
|
104
|
+
npx snowkit pdf extract report.pdf --pages "1, 4-6"
|
|
105
|
+
npx snowkit pdf rotate scan.pdf --angle 90 --pages 2
|
|
106
|
+
npx snowkit pdf info report.pdf
|
|
107
|
+
npx snowkit md2pdf notes.md --page letter --numbers
|
|
108
|
+
npx snowkit qr "https://example.com" -o qr.png
|
|
109
|
+
npx snowkit json format data.json --sort
|
|
110
|
+
npx snowkit json validate data.json --schema schema.json
|
|
111
|
+
npx snowkit sql format query.sql
|
|
112
|
+
npx snowkit text stats essay.txt
|
|
113
|
+
npx snowkit text case title headings.txt
|
|
114
|
+
npx snowkit text dedupe emails.txt --ignore-case
|
|
115
|
+
npx snowkit color "#336699" --against white
|
|
116
|
+
npx snowkit cron "*/15 9-17 * * 1-5"
|
|
117
|
+
npx snowkit --help
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Text commands read the file you name, or standard input when you pipe text in, so they fit into scripts:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
curl -s https://api.example.com/items | npx snowkit json format
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Install it once with `npm install -g snowkit` and you can drop the `npx`.
|
|
127
|
+
|
|
128
|
+
## How it treats your files
|
|
129
|
+
|
|
130
|
+
- It only reads the files you name and only writes where you ask, or next to the original when you do not say.
|
|
131
|
+
- It never replaces an existing file unless you set `overwrite` (or pass `--overwrite` on the command line).
|
|
132
|
+
- It never connects to the internet. Every tool runs on your own computer.
|
|
133
|
+
- Password-protected PDFs are left alone, with a clear message.
|
|
134
|
+
|
|
135
|
+
## Good to know
|
|
136
|
+
|
|
137
|
+
- `markdown_to_pdf` uses built-in fonts that cover Western European languages. Images in the Markdown are left out.
|
|
138
|
+
- `pdf_rotate` keeps everything else in the file as it was. Merging, splitting and extracting copy the pages themselves, so bookmarks and document details from the originals are not carried over.
|
|
139
|
+
- Errors explain what went wrong and how to fix it, so your assistant can try again sensibly.
|
|
140
|
+
|
|
141
|
+
## Licence
|
|
142
|
+
|
|
143
|
+
MIT. See [LICENSE](LICENSE).
|
package/bin/snowkit.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (!argv.length || (argv.length === 1 && argv[0] === "mcp")) {
|
|
8
|
+
const { startServer } = await import("../src/server.js");
|
|
9
|
+
if (process.stdin.isTTY) process.stderr.write("Snowkit " + version + " is waiting for an AI app on standard input. Press Ctrl+C to stop, or run snowkit --help for the command line tools.\n");
|
|
10
|
+
try {
|
|
11
|
+
await startServer(version);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
process.stderr.write("Snowkit could not start: " + String((e && e.message) || e) + "\n");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
const { main } = await import("../src/cli.js");
|
|
18
|
+
process.exitCode = await main(argv, version);
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snowkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Private tools that run on your computer. Merge, split and rotate PDFs, make QR codes, format JSON and SQL, and more, from the command line or any MCP client. Your files never leave your machine.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Snowkit",
|
|
8
|
+
"homepage": "https://snowkit.io",
|
|
9
|
+
"bin": {
|
|
10
|
+
"snowkit": "bin/snowkit.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/tools.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"mcp-server",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"pdf",
|
|
29
|
+
"merge-pdf",
|
|
30
|
+
"split-pdf",
|
|
31
|
+
"qr-code",
|
|
32
|
+
"json",
|
|
33
|
+
"sql-formatter",
|
|
34
|
+
"cli",
|
|
35
|
+
"privacy",
|
|
36
|
+
"offline"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test",
|
|
40
|
+
"sync-engine": "node scripts/sync-engine.mjs"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
44
|
+
"pdf-lib": "1.17.1",
|
|
45
|
+
"zod": "4.6.5"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { TOOLS, runTool, ToolError } from "./tools.js";
|
|
3
|
+
import { CASES } from "./text.js";
|
|
4
|
+
|
|
5
|
+
const VALUE_FLAGS = new Set(["output", "pages", "angle", "range", "every", "page", "title", "level", "size", "indent", "case", "schema", "count", "against", "format"]);
|
|
6
|
+
const BOOL_FLAGS = new Set(["overwrite", "json", "sort", "minify", "numbers", "utc", "ignore-case", "remove-empty", "keep-spaces", "help", "version"]);
|
|
7
|
+
const SHORT = { o: "output", h: "help", v: "version" };
|
|
8
|
+
|
|
9
|
+
export class UsageError extends Error {}
|
|
10
|
+
|
|
11
|
+
export function parseArgs(argv) {
|
|
12
|
+
const pos = [];
|
|
13
|
+
const flags = { range: [] };
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const a = argv[i];
|
|
16
|
+
if (a === "--") {
|
|
17
|
+
pos.push(...argv.slice(i + 1));
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
let key = null;
|
|
21
|
+
let value;
|
|
22
|
+
if (/^--[a-z]/.test(a)) {
|
|
23
|
+
const eq = a.indexOf("=");
|
|
24
|
+
key = eq > 0 ? a.slice(2, eq) : a.slice(2);
|
|
25
|
+
if (eq > 0) value = a.slice(eq + 1);
|
|
26
|
+
} else if (/^-[a-z]$/i.test(a)) {
|
|
27
|
+
key = SHORT[a.slice(1)];
|
|
28
|
+
if (!key) throw new UsageError("Unknown option " + a + ".");
|
|
29
|
+
} else {
|
|
30
|
+
pos.push(a);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (BOOL_FLAGS.has(key)) {
|
|
34
|
+
if (value !== undefined) throw new UsageError("--" + key + " does not take a value.");
|
|
35
|
+
flags[key] = true;
|
|
36
|
+
} else if (VALUE_FLAGS.has(key)) {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
if (i + 1 >= argv.length) throw new UsageError("--" + key + " needs a value.");
|
|
39
|
+
value = argv[++i];
|
|
40
|
+
}
|
|
41
|
+
if (key === "range") flags.range.push(value);
|
|
42
|
+
else flags[key] = value;
|
|
43
|
+
} else throw new UsageError("Unknown option --" + key + ".");
|
|
44
|
+
}
|
|
45
|
+
return { pos, flags };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const HELP = `Snowkit: private tools that run on your computer.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
snowkit start the MCP server for AI apps (same as snowkit mcp)
|
|
52
|
+
snowkit pdf merge <a.pdf> <b.pdf> [more.pdf...] [-o out.pdf]
|
|
53
|
+
snowkit pdf split <file.pdf> [--every 2 | --range 1-3 --range 4-] [-o folder]
|
|
54
|
+
snowkit pdf extract <file.pdf> --pages "1-3, 7" [-o out.pdf]
|
|
55
|
+
snowkit pdf rotate <file.pdf> --angle 90|180|270|-90 [--pages "2, 4-6"] [-o out.pdf]
|
|
56
|
+
snowkit pdf info <file.pdf>
|
|
57
|
+
snowkit md2pdf <notes.md> [-o notes.pdf] [--page letter|a4|legal|a5] [--title "..."] [--numbers]
|
|
58
|
+
snowkit qr "<text or link>" [-o qr.svg|qr.png] [--level L|M|Q|H] [--size 512]
|
|
59
|
+
snowkit json format [file.json] [--indent 2] [--sort] [--minify] [-o out.json]
|
|
60
|
+
snowkit json validate [file.json] [--schema schema.json]
|
|
61
|
+
snowkit sql format [file.sql] [--case upper|lower|preserve] [--indent 2] [--minify] [-o out.sql]
|
|
62
|
+
snowkit text stats [file.txt]
|
|
63
|
+
snowkit text case <${CASES.join("|")}> [file.txt]
|
|
64
|
+
snowkit text slug [file.txt]
|
|
65
|
+
snowkit text dedupe [file.txt] [--ignore-case] [--remove-empty] [--keep-spaces]
|
|
66
|
+
snowkit color "<colour>" [--against "<colour>"]
|
|
67
|
+
snowkit cron "<expression>" [--count 5] [--utc]
|
|
68
|
+
snowkit tools list the tools AI apps can use
|
|
69
|
+
|
|
70
|
+
Text commands read the file given, or standard input when it is piped.
|
|
71
|
+
Results that are files are saved next to the input unless -o is given.
|
|
72
|
+
Existing files are never replaced unless you add --overwrite.
|
|
73
|
+
Add --json to print the full result as JSON.
|
|
74
|
+
|
|
75
|
+
Nothing you open with Snowkit leaves your computer. https://snowkit.io`;
|
|
76
|
+
|
|
77
|
+
function readStdin() {
|
|
78
|
+
if (process.stdin.isTTY) throw new UsageError("Give a file, or pipe text in.");
|
|
79
|
+
return readFileSync(0, "utf8").replace(/^/, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function textSource(file) {
|
|
83
|
+
return file && file !== "-" ? { input: file } : { text: readStdin() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function num(value, name) {
|
|
87
|
+
if (value === undefined) return undefined;
|
|
88
|
+
const n = Number(value);
|
|
89
|
+
if (!Number.isFinite(n)) throw new UsageError("--" + name + " needs a number.");
|
|
90
|
+
return n;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function need(value, what) {
|
|
94
|
+
if (value === undefined || value === "") throw new UsageError("Missing " + what + ". Run snowkit --help for examples.");
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function plan(pos, f) {
|
|
99
|
+
const [cmd, sub, ...rest] = pos;
|
|
100
|
+
const base = { overwrite: f.overwrite || undefined };
|
|
101
|
+
const clean = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
102
|
+
if (cmd === "pdf") {
|
|
103
|
+
const files = rest;
|
|
104
|
+
if (sub === "merge") return ["pdf_merge", clean({ ...base, inputs: files, output: f.output })];
|
|
105
|
+
if (sub === "split") return ["pdf_split", clean({ ...base, input: need(files[0], "PDF file"), every: num(f.every, "every"), ranges: f.range.length ? f.range : undefined, output_folder: f.output })];
|
|
106
|
+
if (sub === "extract") return ["pdf_extract_pages", clean({ ...base, input: need(files[0], "PDF file"), pages: need(f.pages, "--pages"), output: f.output })];
|
|
107
|
+
if (sub === "rotate") return ["pdf_rotate", clean({ ...base, input: need(files[0], "PDF file"), angle: num(need(f.angle, "--angle"), "angle"), pages: f.pages, output: f.output })];
|
|
108
|
+
if (sub === "info") return ["pdf_info", { input: need(files[0], "PDF file") }];
|
|
109
|
+
throw new UsageError("Choose a PDF command: merge, split, extract, rotate or info.");
|
|
110
|
+
}
|
|
111
|
+
if (cmd === "md2pdf") return ["markdown_to_pdf", clean({ ...base, input: need(sub, "Markdown file"), output: f.output, page: f.page, title: f.title, page_numbers: f.numbers || undefined })];
|
|
112
|
+
if (cmd === "qr") return ["qr_code", clean({ ...base, text: need(sub, "text or link"), output: f.output, format: f.format, level: f.level, size: num(f.size, "size") })];
|
|
113
|
+
if (cmd === "json") {
|
|
114
|
+
if (sub === "format") return ["json_format", clean({ ...base, ...textSource(rest[0]), indent: num(f.indent, "indent"), sort_keys: f.sort || undefined, minify: f.minify || undefined, output: f.output })];
|
|
115
|
+
if (sub === "validate") return ["json_validate", clean({ ...textSource(rest[0]), schema_path: f.schema })];
|
|
116
|
+
throw new UsageError("Choose a JSON command: format or validate.");
|
|
117
|
+
}
|
|
118
|
+
if (cmd === "sql") {
|
|
119
|
+
if (sub !== "format") throw new UsageError("Use snowkit sql format [file.sql].");
|
|
120
|
+
return ["sql_format", clean({ ...base, ...textSource(rest[0]), keyword_case: f.case, indent: num(f.indent, "indent"), minify: f.minify || undefined, output: f.output })];
|
|
121
|
+
}
|
|
122
|
+
if (cmd === "text") {
|
|
123
|
+
if (sub === "stats") return ["text_stats", textSource(rest[0])];
|
|
124
|
+
if (sub === "case") return ["text_transform", clean({ ...base, action: need(rest[0], "case name, such as title"), ...textSource(rest[1]), output: f.output })];
|
|
125
|
+
if (sub === "slug") return ["text_transform", clean({ ...base, action: "slug", ...textSource(rest[0]), output: f.output })];
|
|
126
|
+
if (sub === "dedupe") return ["text_transform", clean({ ...base, action: "dedupe", ...textSource(rest[0]), ignore_case: f["ignore-case"] || undefined, remove_empty: f["remove-empty"] || undefined, trim: f["keep-spaces"] ? false : undefined, output: f.output })];
|
|
127
|
+
throw new UsageError("Choose a text command: stats, case, slug or dedupe.");
|
|
128
|
+
}
|
|
129
|
+
if (cmd === "color" || cmd === "colour") return ["color_convert", clean({ color: need(sub, "colour"), against: f.against })];
|
|
130
|
+
if (cmd === "cron") return ["cron_explain", clean({ expression: need(sub, "cron expression"), count: num(f.count, "count"), utc: f.utc || undefined })];
|
|
131
|
+
throw new UsageError("Unknown command “" + cmd + "”. Run snowkit --help to see them all.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function main(argv, version, io = { out: process.stdout, err: process.stderr }) {
|
|
135
|
+
const say = (stream, text) => stream.write(String(text).replace(/\n?$/, "\n"));
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = parseArgs(argv);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
say(io.err, e.message);
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
const { pos, flags } = parsed;
|
|
144
|
+
if (flags.version) {
|
|
145
|
+
say(io.out, version);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
if (flags.help || pos[0] === "help") {
|
|
149
|
+
say(io.out, HELP);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (pos[0] === "tools") {
|
|
153
|
+
const width = Math.max(...TOOLS.map((t) => t.name.length));
|
|
154
|
+
say(io.out, TOOLS.map((t) => t.name.padEnd(width + 2) + t.description).join("\n"));
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const [name, args] = plan(pos, flags);
|
|
159
|
+
const result = await runTool(name, args);
|
|
160
|
+
if (flags.json) say(io.out, JSON.stringify(result.data, null, 2));
|
|
161
|
+
else if (result.output !== undefined) io.out.write(result.output);
|
|
162
|
+
else say(io.out, result.text);
|
|
163
|
+
if (result.data && result.data.valid === false) return 1;
|
|
164
|
+
return 0;
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (e instanceof UsageError) {
|
|
167
|
+
say(io.err, e.message);
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
if (e instanceof ToolError) {
|
|
171
|
+
say(io.err, e.message);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
say(io.err, "Something went wrong: " + String((e && e.message) || e));
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
}
|