lake-cimg 1.0.1 → 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 CHANGED
@@ -10,7 +10,6 @@
10
10
  - 处理过程输出体积对比;失败时非零退出码
11
11
  - **`picture` 子命令**:从单张 PNG/JPEG/WebP 源图一次生成 **AVIF + WebP + JPEG**,配合前端 `<picture>` 做渐进增强(不支持 GIF 动图源)
12
12
  - **`scan-code` 子命令**:扫描源码中的图片引用并结合真实像素尺寸给出 CLS / 比例等建议(只读)
13
- - **Agent Skill**:通过 [`npx skills add`](https://www.npmjs.com/package/skills) 安装 [`skills/cimg-audit`](skills/cimg-audit/SKILL.md)(技能名 **`cimg-audit`**,好记),在 Cursor 等环境里用自然语言驱动 `npx lake-cimg@latest …`
14
13
 
15
14
  ## 环境要求
16
15
 
@@ -156,38 +155,6 @@ npx lake-cimg@latest scan-code [path] [--no-recursive] [--limit <n>] [--issues-o
156
155
 
157
156
  适合在构建脚本或 Node 服务中复用同一套逻辑。
158
157
 
159
- ## Agent Skill(Skills CLI / `npx skills add`)
160
-
161
- 本仓库按 [Skills CLI](https://www.npmjs.com/package/skills) 约定提供可安装 Skill,见目录 [`skills/`](skills/README.md)(与 [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) 的 `skills/<name>/SKILL.md` 布局一致)。技能目录名为 **`cimg-audit`**(短、与包名一致,便于 `-s cimg-audit`)。
162
-
163
- **何时需要 / 触发条件(选用本 Skill 的典型场景)**
164
-
165
- - 对话或任务涉及:**图片体积与格式**(WebP/AVIF)、**`<picture>` / `srcset`**、**LCP / CLS / layout shift**、首屏大图
166
- - 要先做**只读**引用审计再改代码:用 **`npx lake-cimg@latest scan-code`** 出 JSON,再按需改标签或压缩
167
- - 希望 Agent **按固定流程**:先 `scan-code` → 再按需 `npx lake-cimg@latest` 压缩或 `picture` 多格式输出
168
-
169
- 更细的英文说明见 [`skills/README.md` 的「When to use」一节](skills/README.md#when-to-use)。
170
-
171
- **从 GitHub 安装**(需已推送;将 `lake0090/lake-cimg` 换成你的 fork 若不同):
172
-
173
- ```bash
174
- # 安装到用户级(-g),仅本 skill(-s),跳过确认(-y)
175
- npx skills add lake0090/lake-cimg -s cimg-audit -g -y
176
-
177
- # 仅安装到当前项目
178
- npx skills add lake0090/lake-cimg -s cimg-audit -y
179
- ```
180
-
181
- **本地克隆开发时**(在仓库根目录执行):
182
-
183
- ```bash
184
- npx skills add . -s cimg-audit -y
185
- ```
186
-
187
- 可选:`--agent cursor` 等,见 `npx skills add --help`。Skill 正文在 [`skills/cimg-audit/SKILL.md`](skills/cimg-audit/SKILL.md),指导用 **`npx lake-cimg@latest scan-code`** 与压缩 / `picture` 子命令配合使用。
188
-
189
- 安装 Skill 后,在 Agent 对话里可直接让模型按 Skill 流程执行(例如:先 `scan-code` 再按需压缩);CLI 与 `npx` 均在本地执行,图片不会上传到云端。
190
-
191
158
  ## 常见问题
192
159
 
193
160
  - **写权限**:`-o` 指向的目录需可创建/写入;否则 sharp 或 `fs` 会报错。
@@ -198,7 +165,7 @@ npx skills add . -s cimg-audit -y
198
165
 
199
166
  - **Lighthouse 审计**:后续接入 [Lighthouse](https://developer.chrome.com/docs/lighthouse)(或 CI 中的 Lighthouse CI),对典型页面做性能 / 最佳实践等审计。
200
167
  - **审计前后对比**:在引入 `scan-code`、压缩、`picture` 等优化前后各跑一轮,保存报告(JSON/HTML),对比 LCP、CLS、资源体积等指标,量化改动效果。
201
- - **专项优化**:根据 Lighthouse 报告中的具体项(如 LCP 候选、未使用 CSS、图片尺寸等)做针对性迭代,与现有 CLI / Skill 工作流互补。
168
+ - **专项优化**:根据 Lighthouse 报告中的具体项(如 LCP 候选、未使用 CSS、图片尺寸等)做针对性迭代,与现有 CLI 工作流互补。
202
169
 
203
170
  ## 发布到 npm(维护者)
204
171
 
package/bin/cimg.js CHANGED
@@ -298,7 +298,7 @@ program
298
298
  .command("scan [input]")
299
299
  .description("扫描图片并给出优化建议(不修改文件)")
300
300
  .option("-r, --recursive", "递归处理子目录")
301
- .option("--json", "以 JSON 格式输出(适合 AI/脚本消费)")
301
+ .option("--json", "以 JSON 格式输出(适合脚本与工具消费)")
302
302
  .action(async function scanAction(input, opts) {
303
303
  if (!input || input.trim() === "") {
304
304
  this.outputHelp();
package/lib/compress.js CHANGED
@@ -17,6 +17,9 @@ import {
17
17
  encodeJpegBuffer,
18
18
  } from "./sharpHelpers.js";
19
19
 
20
+ /** 小于此体积跳过压缩 */
21
+ const THRESHOLD_SKIP_FOR_AGENT = 10 * 1024;
22
+
20
23
  /** Max concurrent tasks for processing images */
21
24
  const MAX_CONCURRENCY = Math.max(1, cpus().length);
22
25
 
@@ -104,7 +107,9 @@ export async function collectFiles(inputPath, recursive = false) {
104
107
  * originalHeight: number | null,
105
108
  * width: number | null,
106
109
  * height: number | null,
107
- * format: string | null
110
+ * format: string | null,
111
+ * skipped?: true,
112
+ * reason?: "small" | "larger"
108
113
  * }>}
109
114
  */
110
115
  export async function processOne(inputPath, options = {}) {
@@ -119,6 +124,21 @@ export async function processOne(inputPath, options = {}) {
119
124
 
120
125
  const inputBuffer = await readFile(inputPath);
121
126
  const sizeBefore = inputBuffer.length;
127
+ if (sizeBefore < THRESHOLD_SKIP_FOR_AGENT) {
128
+ return {
129
+ outPath,
130
+ sizeBefore,
131
+ sizeAfter: sizeBefore,
132
+ originalWidth: null,
133
+ originalHeight: null,
134
+ width: null,
135
+ height: null,
136
+ format: null,
137
+ skipped: true,
138
+ reason: "small",
139
+ };
140
+ }
141
+
122
142
  let pipeline = sharp(inputBuffer, { animated: true });
123
143
  const inputMeta = await pipeline.metadata();
124
144
 
@@ -128,8 +148,23 @@ export async function processOne(inputPath, options = {}) {
128
148
  ? await encodeWebpBuffer(pipeline, quality, { lossless: false })
129
149
  : await toFormatBuffer(pipeline, ext, quality);
130
150
 
131
- const outputMeta = await sharp(outputBuffer).metadata();
132
151
  const sizeAfter = outputBuffer.length;
152
+ if (sizeAfter > sizeBefore) {
153
+ return {
154
+ outPath,
155
+ sizeBefore,
156
+ sizeAfter,
157
+ originalWidth: inputMeta.width ?? null,
158
+ originalHeight: inputMeta.height ?? null,
159
+ width: null,
160
+ height: null,
161
+ format: null,
162
+ skipped: true,
163
+ reason: "larger",
164
+ };
165
+ }
166
+
167
+ const outputMeta = await sharp(outputBuffer).metadata();
133
168
  await mkdir(outBase, { recursive: true });
134
169
  await writeFile(outPath, outputBuffer);
135
170
  return {
@@ -166,7 +201,7 @@ async function toFormatBuffer(pipeline, ext, quality) {
166
201
  /**
167
202
  * Run batch compression.
168
203
  * @param {{ inputPath: string, outDir?: string | null, size?: number | null, quality?: number, recursive?: boolean }} options
169
- * @returns {Promise<{ success: number, failed: number }>} success/failed counts; throws if input invalid
204
+ * @returns {Promise<{ success: number, failed: number, skipped: number }>} counts; throws if input invalid
170
205
  */
171
206
  export async function run(options) {
172
207
  const {
@@ -208,13 +243,30 @@ export async function run(options) {
208
243
 
209
244
  let success = 0;
210
245
  let failed = 0;
246
+ let skipped = 0;
211
247
  let totalBefore = 0;
212
248
  let totalAfter = 0;
213
249
  for (let i = 0; i < files.length; i++) {
214
250
  const r = results[i];
215
251
  const fp = files[i];
216
252
  if (r.status === "fulfilled") {
217
- const { outPath, sizeBefore, sizeAfter } = r.value;
253
+ const v = r.value;
254
+ if (v.skipped) {
255
+ skipped++;
256
+ if (v.reason === "small") {
257
+ console.log(` ⏭ ${fp}`);
258
+ console.log(
259
+ ` 不处理:原图 ${formatSize(v.sizeBefore)} 小于 10KB(无需压缩)`
260
+ );
261
+ } else {
262
+ console.log(` ⏭ ${fp}`);
263
+ console.log(
264
+ ` 不处理:压缩后 ${formatSize(v.sizeAfter)} 大于原图 ${formatSize(v.sizeBefore)}(已保留原图、未写出新文件)`
265
+ );
266
+ }
267
+ continue;
268
+ }
269
+ const { outPath, sizeBefore, sizeAfter } = v;
218
270
  totalBefore += sizeBefore;
219
271
  totalAfter += sizeAfter;
220
272
  console.log(` ✅ ${fp} → ${basename(outPath)}`);
@@ -225,9 +277,10 @@ export async function run(options) {
225
277
  failed++;
226
278
  }
227
279
  }
228
- console.log(`\n完成:成功 ${success},失败 ${failed}。`);
280
+ const skipPart = skipped > 0 ? `,跳过 ${skipped}(<10KB 或压缩后更大)` : "";
281
+ console.log(`\n完成:成功 ${success},失败 ${failed}${skipPart}。`);
229
282
  if (success > 0 && totalBefore > 0) {
230
283
  console.log(`合计:${formatCompare(totalBefore, totalAfter)}`);
231
284
  }
232
- return { success, failed };
285
+ return { success, failed, skipped };
233
286
  }
package/lib/scan.js CHANGED
@@ -7,6 +7,8 @@ import sharp from "sharp";
7
7
  import { collectFiles } from "./compress.js";
8
8
 
9
9
  const KB = 1024;
10
+ /** 小于此体积一般无压缩收益,建议标记为不处理 */
11
+ const THRESHOLD_SKIP_FOR_AGENT = 10 * KB;
10
12
  const THRESHOLD_LARGE_NON_WEBP = 100 * KB;
11
13
  const THRESHOLD_LARGE_WEBP = 500 * KB;
12
14
  const MAX_EDGE = 1920;
@@ -25,6 +27,9 @@ function formatSize(bytes) {
25
27
  * @returns {string}
26
28
  */
27
29
  function computeSuggestion(sizeBytes, format, width, height) {
30
+ if (sizeBytes < THRESHOLD_SKIP_FOR_AGENT) {
31
+ return "不处理:原图 <10KB(体积过小,无需压缩)";
32
+ }
28
33
  const fmt = (format ?? "").toLowerCase();
29
34
  if (fmt !== "webp" && sizeBytes > THRESHOLD_LARGE_NON_WEBP) {
30
35
  return "convert to webp";
@@ -895,7 +895,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
895
895
  fmt
896
896
  ) {
897
897
  issues.push("suggest_modern_format");
898
- hints.push("可考虑 WebP/AVIF 或 cimg picture 子命令生成多格式 <picture>");
898
+ hints.push("可考虑 WebP 格式");
899
899
  }
900
900
 
901
901
  const row = {
package/package.json CHANGED
@@ -1,15 +1,13 @@
1
1
  {
2
2
  "name": "lake-cimg",
3
- "version": "1.0.1",
4
- "description": "Batch image compression to WebP — CLI plus Agent Skills for frontend image audit (scan-code, picture stack)",
3
+ "version": "1.1.0",
4
+ "description": "Batch image compression to WebP — CLI with picture stack and scan-code for frontend image audit",
5
5
  "keywords": [
6
6
  "image",
7
7
  "compress",
8
8
  "webp",
9
9
  "sharp",
10
- "agent-skills",
11
- "skills",
12
- "cursor"
10
+ "cli"
13
11
  ],
14
12
  "repository": {
15
13
  "type": "git",
@@ -23,8 +21,7 @@
23
21
  "files": [
24
22
  "bin",
25
23
  "lib",
26
- "README.md",
27
- "skills"
24
+ "README.md"
28
25
  ],
29
26
  "publishConfig": {
30
27
  "access": "public"
package/skills/README.md DELETED
@@ -1,40 +0,0 @@
1
- # Skills (Skills CLI)
2
-
3
- This directory follows the layout expected by **[Skills CLI](https://www.npmjs.com/package/skills)** (`npx skills add`), same idea as [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills): each subfolder is one skill with a `SKILL.md`.
4
-
5
- | Skill | Summary |
6
- | --- | --- |
7
- | [cimg-audit](./cimg-audit/SKILL.md) | Audit `<img>` / Pug `img()` / imports / `url()` for CLS, aspect ratio, and format hints via `npx lake-cimg@latest scan-code` |
8
-
9
- ### When to use
10
-
11
- Skill id **`cimg-audit`** — use it when the conversation or task touches:
12
-
13
- - **Performance / layout:** CLS, layout shift, LCP, hero images, `fetchPriority`
14
- - **Markup / assets:** `<img>` dimensions, `aspect-ratio`, `<picture>`, `srcset` / `sizes`, WebP / AVIF
15
- - **Workflow:** run **`scan-code`** first (read-only JSON), then fix markup and/or compress with **`npx lake-cimg@latest`** / **`picture`**
16
-
17
- The skill name `cimg-audit` is short for discovery; the YAML `description` in `SKILL.md` is what agents match against.
18
-
19
- Install from GitHub (after push):
20
-
21
- ```bash
22
- npx skills add lake0090/lake-cimg -s cimg-audit -g -y
23
- ```
24
-
25
- Install only this repo’s skill into the **current project** (run from another repo):
26
-
27
- ```bash
28
- npx skills add lake0090/lake-cimg -s cimg-audit -y
29
- ```
30
-
31
- Develop locally from a clone of this repository:
32
-
33
- ```bash
34
- cd /path/to/lake-cimg
35
- npx skills add . -s cimg-audit -y
36
- ```
37
-
38
- Use `-g` for a user-level install (`~/.cursor/skills/` etc., depending on agent). See `npx skills add --help` for `--agent` (e.g. `cursor`).
39
-
40
- Browse the registry at [skills.sh](https://skills.sh/).
@@ -1,63 +0,0 @@
1
- ---
2
- name: cimg-audit
3
- description: >-
4
- Audits HTML, Vue, Pug, JS, TS, TSX, and JSX image references against intrinsic
5
- dimensions for CLS risk, aspect-ratio mismatches, and modern-format hints.
6
- Use when optimizing images, fixing layout shift, LCP heroes, picture/srcset,
7
- or running lake-cimg scan-code via npx lake-cimg@latest.
8
- ---
9
-
10
- # cimg image audit
11
-
12
- Run **`npx lake-cimg@latest`** from the **project root** (no global install).
13
-
14
- <a id="invoke-scan-code"></a>
15
-
16
- ## Invoke `scan-code`
17
-
18
- Use **one** command starting with `npx` — **not** `cd … && npx …` (PowerShell 5.1 on Windows does not support `&&`).
19
-
20
- | `path` | Behavior |
21
- | --- | --- |
22
- | *(omitted)* | Scans **`.`** (current working directory). Run from repo root to cover the tree. |
23
- | **Directory** | Recursively scans supported sources under that folder (honors `--no-recursive`). |
24
- | **Single file** | Only that file. Extension must be `.html`, `.htm`, `.vue`, `.pug`, `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, or `.jsx`. |
25
-
26
- **Output:** stdout is **only** pretty-printed JSON: `items[]` with `issues`, `hints`, `snippet`, `intrinsicWidth` / `intrinsicHeight` when metadata was read, plus top-level `summary` and scan metadata. **`--issues-only`** drops rows with empty `issues`. For **`missing_dimensions`**, use **`intrinsicWidth` / `intrinsicHeight`** on the same item and/or the English hint that repeats them.
27
-
28
- **Examples:**
29
-
30
- ```bash
31
- npx lake-cimg@latest scan-code
32
- npx lake-cimg@latest scan-code /absolute/path/to/src
33
- npx lake-cimg@latest scan-code /absolute/path/to/about.pug
34
- ```
35
-
36
- More flags: `npx lake-cimg@latest scan-code --help` (e.g. `--limit`, `--issues-only`, `--no-recursive`). Prefer an **absolute** `path` if the shell cwd may not be the repo root.
37
-
38
- ## Workflow
39
-
40
- - [ ] **1. Scan (read-only):** `npx lake-cimg@latest scan-code [path]` — see **Invoke `scan-code`** above.
41
- - [ ] **2. Triage:** Prefer **`hints`** for what to change; use **`issues`** codes to group or filter. Common codes: `missing_dimensions`, `aspect_ratio_mismatch`, `suggest_modern_format` (default: **single WebP** — point `src` / `srcset` at `.webp` after exporting; use **`<picture>`** with AVIF/WebP + legacy fallback **only if the user explicitly asks** for multi-format markup or old-browser JPEG/PNG fallback), `needs_manual_review`, `missing_src`, `cannot_resolve`, `cannot_read_metadata`.
42
- - [ ] **3. Fix then optimize:** Apply markup using [reference.md](reference.md) (`<img>` first; `<picture>` only when the user requires it). For **all** raster refs that need format or responsive delivery, not only one hero row. Compress / emit WebP: `npx lake-cimg@latest <path> [options]`. **Optional** full stack for `<picture>` when requested: `npx lake-cimg@latest picture <input> -O <outDir>` (details: package [README.md](../../README.md)).
43
-
44
- ## Rules of thumb
45
-
46
- - **Aspect ratio:** Match display ratio to intrinsic (w÷h), or use **`object-fit`** + explicit box / `aspect-ratio` for crop/letterbox.
47
- - **CLS:** Add `width` and `height` to `<img>`, or use CSS `aspect-ratio`. For `missing_dimensions`, fill in the exact `intrinsicWidth` / `intrinsicHeight`.
48
- - **Responsive:** **`srcset` + `sizes`** on `<img>` (works with a single WebP). **`<picture>`** only when the user wants multiple formats in HTML; each `<source type="…">` must match the real file type. Width variants: separate files or `picture … -s <px>` when building a stack.
49
- - **LCP:** At most one hero per view: **`fetchPriority="high"`**, **`loading="eager"`**; lazy-load the rest.
50
- - **Alt:** Describe content and purpose; no keyword stuffing; **`alt=""`** only for decorative images.
51
-
52
- ## What the scanner cannot resolve
53
-
54
- Dynamic **`src`** without a static path → **`needs_manual_review`**. **`http(s):`**, **`data:`**, and path aliases (**`@/`**, **`~/`**, etc.) → **`cannot_resolve`** (no alias map reads; no network fetch).
55
-
56
- If **`hints`** mention alias skip: use **`rawRef`** and map the alias via Vite/webpack/tsconfig/Nuxt config. With a **real filesystem path**, run `npx lake-cimg@latest scan <resolved-path>` on assets or re-run **`scan-code`** on markup that uses resolvable relative paths. If unresolved, triage without intrinsic dimensions.
57
-
58
- ## Other CLI (after scan-code)
59
-
60
- | Command | Role |
61
- | --- | --- |
62
- | `npx lake-cimg@latest <path>` | Compress / WebP — see `npx lake-cimg@latest --help` (`-o`, `-s`, `-q`, `-r`). |
63
- | `npx lake-cimg@latest picture <input> -O <outDir>` | One raster → AVIF + WebP + JPEG **when the user wants `<picture>` / multi-format**; not the default if single WebP is enough. |
@@ -1,49 +0,0 @@
1
- # Image markup templates
2
-
3
- Supplement for [SKILL.md](SKILL.md). Copy-paste starting points; adjust paths, dimensions, and `sizes` to match your assets and layout.
4
-
5
- ## Basic `<img>`
6
-
7
- **When:** Single raster URL; reserve space for CLS. **Check:** `width` / `height` match intrinsic pixels (or use `aspect-ratio` for fluid); meaningful `alt` or `alt=""` if decorative.
8
-
9
- ```html
10
- <img
11
- srcset="
12
- maine-coon-nap-320w.webp 320w,
13
- maine-coon-nap-480w.webp 480w,
14
- maine-coon-nap-800w.webp 800w
15
- "
16
- sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px"
17
- src="maine-coon-nap-800w.webp"
18
- alt="A watercolor illustration of a maine coon napping leisurely in front of a fireplace"
19
- />
20
- ```
21
-
22
- ## `<picture>` + `srcset` + `sizes`
23
-
24
- **When:** AVIF/WebP stack with JPEG (or similar) fallback; responsive widths. **Check:** Each `<source type="…">` matches the real file type; `sizes` matches breakpoints; `<img>` is the final fallback with matching `srcset` / `sizes`.
25
-
26
- ```html
27
- <picture>
28
- <source
29
- type="image/avif"
30
- srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
31
- sizes="(max-width: 600px) 100vw, 50vw"
32
- />
33
- <source
34
- type="image/webp"
35
- srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
36
- sizes="(max-width: 600px) 100vw, 50vw"
37
- />
38
- <img
39
- src="hero-800.jpg"
40
- srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
41
- sizes="(max-width: 600px) 100vw, 50vw"
42
- width="1200"
43
- height="600"
44
- alt="Describe subject and purpose on this page"
45
- loading="lazy"
46
- decoding="async"
47
- />
48
- </picture>
49
- ```