img-subir 0.0.1

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.
@@ -0,0 +1,215 @@
1
+ type RasterFormat = "png" | "jpeg" | "webp" | "avif" | "gif";
2
+ type Format = RasterFormat | "svg";
3
+ type Kind = "ui" | "photo" | "graphic";
4
+ type PresetName = "web" | "github" | "screenshot" | "og" | "thumb" | "lossless";
5
+ type FormatChoice = "auto" | "keep" | RasterFormat;
6
+ type Status = "optimized" | "kept" | "failed" | "budget-missed";
7
+ interface Preset {
8
+ name: PresetName;
9
+ /** Longest allowed width in px (undefined = no limit). */
10
+ maxWidth?: number;
11
+ maxHeight?: number;
12
+ /** Output formats that may be tried. */
13
+ candidates: RasterFormat[];
14
+ /** SSIM floor per kind. Lossy candidates below this are rejected. */
15
+ floor: Record<Kind, number>;
16
+ /** Starting quality per lossy format, per kind. */
17
+ quality: Record<Kind, {
18
+ webp: number;
19
+ avif: number;
20
+ jpeg: number;
21
+ png: number;
22
+ }>;
23
+ /** Only keep AVIF if it is at least this fraction smaller than the best non-AVIF candidate. */
24
+ avifMinGain: number;
25
+ /** Skip AVIF above this many pixels (encode time grows fast). */
26
+ avifMaxPixels: number;
27
+ /** Also try lossless WebP for ui/graphic kinds. */
28
+ losslessWebp: boolean;
29
+ /** Only lossless candidates; no resize. */
30
+ lossless: boolean;
31
+ /** Force a kind for every file. */
32
+ forceKind?: Kind;
33
+ /** Don't rewrite if gain is below this percent. */
34
+ minGainPercent: number;
35
+ /** Notes shown in `img-subir presets`. */
36
+ note: string;
37
+ }
38
+ interface OptimizeOptions {
39
+ /** Files, directories or globs. */
40
+ paths: string[];
41
+ cwd?: string;
42
+ preset?: PresetName;
43
+ format?: FormatChoice;
44
+ kind?: "auto" | Kind;
45
+ maxWidth?: number;
46
+ maxHeight?: number;
47
+ quality?: number;
48
+ targetKb?: number;
49
+ /** Write into this directory (mirrors structure) instead of in place. */
50
+ out?: string;
51
+ /** Inserted before the extension, e.g. ".min". */
52
+ suffix?: string;
53
+ /** Keep originals in .img-subir-originals/ (default true). */
54
+ backup?: boolean;
55
+ /** Change the extension when the format changes (default true). */
56
+ rename?: boolean;
57
+ /** Update references in text files. true = default globs; string[] = custom globs. */
58
+ rewriteRefs?: boolean | string[];
59
+ dryRun?: boolean;
60
+ /** Skip SSIM checks; use fixed qualities. */
61
+ fast?: boolean;
62
+ concurrency?: number;
63
+ /** Per-file progress callback. */
64
+ onFile?: (result: FileResult) => void;
65
+ }
66
+ interface ImageInfo {
67
+ path: string;
68
+ format: Format;
69
+ width: number;
70
+ height: number;
71
+ bytes: number;
72
+ hasAlpha: boolean;
73
+ animated: boolean;
74
+ }
75
+ interface OutputInfo {
76
+ path: string;
77
+ format: Format;
78
+ width: number;
79
+ height: number;
80
+ bytes: number;
81
+ quality?: number;
82
+ lossless?: boolean;
83
+ }
84
+ interface Candidate {
85
+ format: Format;
86
+ bytes: number;
87
+ quality?: number;
88
+ lossless?: boolean;
89
+ ssim?: number;
90
+ passed: boolean;
91
+ }
92
+ interface FileResult {
93
+ input: ImageInfo;
94
+ output: OutputInfo | null;
95
+ kind: Kind | "svg" | "animated";
96
+ status: Status;
97
+ savedBytes: number;
98
+ savedPercent: number;
99
+ actions: string[];
100
+ candidates: Candidate[];
101
+ reason?: string;
102
+ error?: string;
103
+ /** Where the original went (backup path), if moved. */
104
+ backupPath?: string;
105
+ /** With --out, kept files are copied unchanged to their mirrored path. */
106
+ copiedTo?: string;
107
+ durationMs: number;
108
+ }
109
+ interface Rename {
110
+ from: string;
111
+ to: string;
112
+ }
113
+ interface RewrittenRef {
114
+ file: string;
115
+ from: string;
116
+ to: string;
117
+ count: number;
118
+ }
119
+ interface Totals {
120
+ files: number;
121
+ inputBytes: number;
122
+ outputBytes: number;
123
+ savedBytes: number;
124
+ savedPercent: number;
125
+ optimized: number;
126
+ kept: number;
127
+ failed: number;
128
+ budgetMissed: number;
129
+ durationMs: number;
130
+ }
131
+ interface Report {
132
+ schemaVersion: 1;
133
+ version: string;
134
+ preset: PresetName;
135
+ dryRun: boolean;
136
+ files: FileResult[];
137
+ totals: Totals;
138
+ renamed: Rename[];
139
+ rewrittenRefs: RewrittenRef[];
140
+ backupDirs: string[];
141
+ }
142
+ interface InspectResult {
143
+ input: ImageInfo;
144
+ kind: Kind | "svg" | "animated";
145
+ plan: {
146
+ targetWidth: number;
147
+ targetHeight: number;
148
+ candidates: string[];
149
+ floor: number | null;
150
+ };
151
+ }
152
+
153
+ declare function optimize(options: OptimizeOptions): Promise<Report>;
154
+ declare function inspect(paths: string[], options?: Omit<OptimizeOptions, "paths">): Promise<InspectResult[]>;
155
+ declare function fmtBytes(n: number): string;
156
+
157
+ declare const presets: Record<PresetName, Preset>;
158
+ declare const presetNames: PresetName[];
159
+ declare function getPreset(name: string | undefined): Preset;
160
+
161
+ declare const DEFAULT_REF_GLOBS: string[];
162
+ /**
163
+ * Rewrites references to renamed files inside text files under `roots`.
164
+ * Only whole-token matches are replaced, so `logo.png` never matches `logo.png.bak` or `mylogo.png`.
165
+ */
166
+ declare function rewriteRefs(renamed: Rename[], roots: string[], globs?: string[], dryRun?: boolean): Promise<RewrittenRef[]>;
167
+
168
+ declare const BACKUP_DIR = ".img-subir-originals";
169
+ interface Discovered {
170
+ files: string[];
171
+ /** Directories the user passed (used as rewrite roots). */
172
+ roots: string[];
173
+ }
174
+ /**
175
+ * @param excludeDirs absolute directories whose contents are skipped (e.g. the --out dir when it sits inside an input dir)
176
+ */
177
+ declare function discover(inputs: string[], cwd: string, excludeDirs?: string[]): Promise<Discovered>;
178
+
179
+ declare function probe(filePath: string): Promise<ImageInfo>;
180
+
181
+ interface ClassifyStats {
182
+ distinctColors: number;
183
+ flatShare: number;
184
+ /** Share of sampled pixels that are not fully opaque. 0 means the alpha channel is unused. */
185
+ transparentShare: number;
186
+ score: number;
187
+ }
188
+ /**
189
+ * Cheap heuristics, no ML. Samples a 256px thumbnail and measures how "flat" it is.
190
+ * UI screenshots: few distinct colors, long runs of identical pixels.
191
+ * Photos: the opposite.
192
+ */
193
+ declare function classify(info: ImageInfo, input?: string | Buffer): Promise<{
194
+ kind: Kind;
195
+ stats: ClassifyStats;
196
+ }>;
197
+
198
+ type FileConfig = Partial<Omit<OptimizeOptions, "paths" | "cwd" | "onFile">>;
199
+ /**
200
+ * Looks in cwd and its ancestors for a config file or a package.json "img-subir" key.
201
+ * A malformed config is an error, not a silent fallback to defaults.
202
+ */
203
+ declare function loadConfig(cwd: string): Promise<{
204
+ config: FileConfig;
205
+ source?: string;
206
+ }>;
207
+
208
+ declare const VERSION: string;
209
+
210
+ /** Thrown for bad invocations (unknown preset, missing path, invalid flag). The CLI maps it to exit code 2. */
211
+ declare class UsageError extends Error {
212
+ name: string;
213
+ }
214
+
215
+ export { BACKUP_DIR, type Candidate, DEFAULT_REF_GLOBS, type FileResult, type Format, type FormatChoice, type ImageInfo, type InspectResult, type Kind, type OptimizeOptions, type OutputInfo, type Preset, type PresetName, type RasterFormat, type Rename, type Report, type RewrittenRef, type Status, type Totals, UsageError, VERSION, classify, discover, fmtBytes, getPreset, inspect, loadConfig, optimize, presetNames, presets, probe, rewriteRefs };
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ import {
2
+ BACKUP_DIR,
3
+ DEFAULT_REF_GLOBS,
4
+ UsageError,
5
+ VERSION,
6
+ classify,
7
+ discover,
8
+ fmtBytes,
9
+ getPreset,
10
+ inspect,
11
+ loadConfig,
12
+ optimize,
13
+ presetNames,
14
+ presets,
15
+ probe,
16
+ rewriteRefs
17
+ } from "./chunk-CWZBNFPK.js";
18
+ export {
19
+ BACKUP_DIR,
20
+ DEFAULT_REF_GLOBS,
21
+ UsageError,
22
+ VERSION,
23
+ classify,
24
+ discover,
25
+ fmtBytes,
26
+ getPreset,
27
+ inspect,
28
+ loadConfig,
29
+ optimize,
30
+ presetNames,
31
+ presets,
32
+ probe,
33
+ rewriteRefs
34
+ };
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "img-subir",
3
+ "version": "0.0.1",
4
+ "description": "Agent-friendly CLI that optimizes images (PNG/JPEG/WebP/AVIF/GIF/SVG) for websites, READMEs and Subir pages.",
5
+ "keywords": [
6
+ "image",
7
+ "optimize",
8
+ "compress",
9
+ "webp",
10
+ "avif",
11
+ "sharp",
12
+ "svgo",
13
+ "cli",
14
+ "agent",
15
+ "screenshots"
16
+ ],
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "bin": {
23
+ "img-subir": "dist/cli.js"
24
+ },
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "skills",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "dev": "tsx src/cli.ts",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "pnpm build && pnpm test"
46
+ },
47
+ "dependencies": {
48
+ "citty": "^0.2.2",
49
+ "fast-glob": "^3.3.3",
50
+ "picocolors": "^1.1.1",
51
+ "sharp": "^0.35.4",
52
+ "ssim.js": "^3.5.0",
53
+ "svgo": "^4.1.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^26.6.2",
57
+ "tsup": "^8.5.1",
58
+ "tsx": "^4.23.15",
59
+ "typescript": "^5.9.3",
60
+ "vitest": "^5.0.1"
61
+ }
62
+ }
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: img-subir
3
+ description: Optimize images (PNG, JPEG, WebP, AVIF, GIF, SVG) for websites, READMEs, docs and Subir pages. Use before uploading or committing screenshots, marketing images or any raster/SVG assets. Cuts file size 70–95% with no visible loss, downsizes Retina screenshots, strips metadata, and can rewrite <img src> references when extensions change.
4
+ ---
5
+
6
+ # img-subir
7
+
8
+ Shrinks images so pages load fast and repos stay small. Non-interactive, safe to run blind.
9
+
10
+ ## When to use
11
+
12
+ - Before `subir upload`, before committing images to a repo, before adding screenshots to a README or PR.
13
+ - Whenever a directory contains `.png`, `.jpg`, `.gif` or `.svg` files larger than ~200 KB.
14
+
15
+ ## Commands
16
+
17
+ ```bash
18
+ npx img-subir <paths...> --json # website / Subir page (preset: web)
19
+ npx img-subir <paths...> --preset github --json # README / PR screenshots (never AVIF)
20
+ npx img-subir <paths...> --rewrite-refs --json # also fix src/href in html/md/tsx/css that point at renamed files
21
+ npx img-subir <paths...> --dry-run # preview only
22
+ npx img-subir inspect <paths...> # what would happen, no writes
23
+ npx img-subir doctor # verify encoders on this machine
24
+ ```
25
+
26
+ `<paths>` can be files, directories or globs. Directories are scanned recursively.
27
+
28
+ ## Defaults you should know
29
+
30
+ - Writes **in place**. Originals are moved to `.img-subir-originals/` next to them. Delete or ignore that folder before uploading/committing. Never re-run on it.
31
+ - Extensions change when the format changes (`shot.png` → `shot.webp`). Read `renamed[]` in the JSON and update references, or pass `--rewrite-refs`.
32
+ - Never writes a file that is larger than the input. Those show as `status: "kept"` with a `reason`. Kept is not an error.
33
+ - Retina screenshots are downscaled to 1920px (web) / 1600px (github). Pass `--max-width` to change.
34
+ - Metadata (EXIF, GPS, thumbnails) is always stripped.
35
+
36
+ ## Reading the JSON
37
+
38
+ ```jsonc
39
+ {
40
+ "files": [{ "input": {...}, "output": {...} | null, "status": "optimized|kept|failed|budget-missed", "savedPercent": 95.7, "actions": [...], "reason": "..." }],
41
+ "totals": { "inputBytes": 0, "outputBytes": 0, "savedPercent": 0, "optimized": 0, "kept": 0, "failed": 0 },
42
+ "renamed": [{ "from": "/abs/a.png", "to": "/abs/a.webp" }],
43
+ "rewrittenRefs": [{ "file": "/abs/index.html", "from": "...", "to": "...", "count": 2 }],
44
+ "backupDirs": ["/abs/assets/.img-subir-originals"]
45
+ }
46
+ ```
47
+
48
+ Exit codes: `0` ok · `1` a file failed (others still written) · `2` bad flags · `3` `--target-kb` could not be met within the quality floor (best effort was written; tell the user the size reached).
49
+
50
+ ## Rules
51
+
52
+ - Do not pass `--no-backup` unless the user asked for it.
53
+ - For social/OG cards use `--preset og` (JPEG/PNG only). For pixel-exact needs use `--preset lossless`.
54
+ - If output looks blurry to the user, re-run with `--preset screenshot` or `--kind ui`.
55
+ - Use `--out <dir>` when the user wants originals untouched; kept files are copied there too (`copiedTo`), so the output tree is complete. `--out` may not be the input directory itself.
56
+ - Unknown flags, missing values and malformed config exit with code 2 before touching any file.