@petercjl/tuda 0.0.0-bootstrap.20261112 → 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.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: using-tuda
3
+ description: Use Tuda for image and video creation, editing, boards and assets only when the user explicitly invokes /using-tuda or $using-tuda. Do not select this skill automatically.
4
+ metadata:
5
+ version: "0.1.0"
6
+ ---
7
+
8
+ # Using Tuda
9
+
10
+ Use this skill only when the user invokes `/using-tuda`, `$using-tuda`, or explicitly asks to use it. If no task is provided, ask what they want to create, edit, or manage. Invocation does not authorize unrelated operations or additional paid generations.
11
+
12
+
13
+ Use the target-owned CLI when `.runtime/dist/main.js` is present: resolve the Skill directory from the host and run `node <Skill directory>/.runtime/dist/main.js`. Otherwise use the installed `tuda` command. Node.js 22.12+ and Tuda desktop are the runtime prerequisites. Use the same entry for every command below. Installation targets only the current Agent; host names describe adapters, not eligibility.
14
+
15
+ Use the `tuda` CLI through your shell tool. App configuration and credentials are reused automatically; never read settings files, decrypt credentials, or configure a second model connection.
16
+
17
+ ## Input, method and output
18
+
19
+ Input is the user’s requested board, assets and operation. Discover the exact live CLI schema, inspect the target, execute the authorized operation, then verify the persisted board and artifact. Output includes board/node IDs, task IDs for generation, managed/exported paths and remaining limitations. Model calls require the user’s requested generation; local board operations do not require model credentials.
20
+
21
+ Read [capabilities.json](capabilities.json) and [the generic adapter](adapters/generic.json) to resolve command execution when entering a new host. A missing terminal, Node runtime or desktop returns a capability error with the missing prerequisite.
22
+
23
+ ## Start
24
+
25
+ 1. Run `tuda status --json` and `tuda providers list --json`. If the command is missing, open Tuda → Settings → Coding Agent and repair the CLI installation. For an existing terminal whose PATH is stale, use the absolute launcher path shown there or open a new terminal.
26
+ 2. Check the returned `ok` field and process exit code. Provider metadata reports saved configuration, not a guarantee that its remote service is available. If no usable image provider exists, guide the user to App model settings.
27
+ 3. Run `tuda tools list --json` for the exact operation schemas. All workspace operations are accessible through `tuda call <operation> --input '<JSON>' --json`. Read [references/cli.md](references/cli.md) for examples.
28
+
29
+ ## Create and edit
30
+
31
+ - A user request to generate authorizes that generation. Use one output unless variants were requested. Do not generate images merely to test a connection.
32
+ - Use `tuda image generate --prompt "…" --out ./result.png --json`. Omit `--provider` to use the App default; use IDs from provider discovery for an explicit selection. `tuda models list --provider <id> --json` lists the saved models.
33
+ - Pass local references as repeated `--reference <file>` options. The CLI imports them as managed assets. Edit with `tuda image edit --input <file> --prompt "…" --out <new-file> --json`; preserve the original.
34
+ - Use explicit `--board <id>` for an existing board named by the user. Inspect its contents first. Without --board, image and video commands use the default board configured in Settings → Coding Agent. When unset, they reuse or create CLI 创作. A deleted default board must be reselected before generating.
35
+ - Normal image and video commands wait for completion. `--async` returns task acceptance only; follow with `tuda tasks wait <id> --json` (add `--kind video` for videos). Ctrl+C or timeout ends waiting, not the provider task.
36
+ - Never blindly resubmit an operation after a lost response. Use a stable `--request-id` for image generation and query it as a task ID. Report interrupted or uncertain remote state instead of creating another paid task.
37
+ - When generation succeeds but export fails, reuse `tasks wait <id> --out …`; do not regenerate. Existing export files are protected unless the user explicitly requests replacement with `--overwrite`.
38
+
39
+ ## Slice contact sheets and sprite grids
40
+
41
+ Use Tuda's local `image slice` / `slice_image` operation for cutting existing images; do not write Python, Pillow, ImageMagick or other agent-side cropping scripts. This operation reuses the App slicer without a model call, preserves the original, saves managed PNGs and adds the results to the target board.
42
+
43
+ - Inspect the source and confirm its actual row/column layout. For the 12-avatar example (3 rows × 4 columns): `tuda image slice --path <managed-path> --board <id> --rows 3 --columns 4 --out ./avatars --json`.
44
+ - Use `--node <id>` for an existing board image, or `--input <local-file>` to import and slice a local source. Supply exactly one source option. Omit `--board` to use the configured CLI default board.
45
+ - `--margin` trims the outside edges and `--gap` excludes equal gutters, measured in source pixels. Rows and columns are each limited to 1–8. Results follow reading order; odd dimensions can produce cells differing by one pixel.
46
+ - For uneven layouts or a single crop, pass `--crops '[{"x":10,"y":20,"width":300,"height":300}]'` instead of grid flags. Coordinates are original-image pixels; at most 64 regions. Review boundaries for clipped subjects before cutting.
47
+ - Slicing does not resize pixels, remove backgrounds or detect subjects automatically. Do not claim uniform dimensions or transparency unless verified. If requested cuts cannot preserve the subjects, adjust regions with the user instead of generating replacements.
48
+ - Return the managed paths/node IDs and optional exportedPaths. Export failure leaves slices on the board; recover with `assets export`, without slicing again. For ZIP delivery call `export_assets` with only the returned slice paths.
49
+
50
+ ## Boards, video and assets
51
+
52
+ - `tuda boards list|show|create|open`, `tuda assets list|import|export`, and `tuda video generate` cover common paths. See CLI help for arguments.
53
+ - Use the exact `tools list` schemas through `tuda call` for move, resize, arrange, duplicate, version selection, recoverable trash/restore, video remix/extend and stopping video polling. These operations retain the App's validation rules.
54
+ - Import files with `tuda assets import --input <file> --board <id> --json`. Advanced operations require returned managed paths; never substitute arbitrary filesystem paths.
55
+ - Video completion requires a managed local output path. Provider URLs are temporary and are not deliverables. Stopping local polling does not cancel a remote job or its billing.
56
+ - Export only requested results. Return task ID, board ID, managed and exported paths, warnings and validation limits; never expose tokens or provider credentials.
57
+
58
+ ## Video and narration configuration
59
+
60
+ When the user asks to configure models, use `providers list --kind video|audio`, `providers show <id>` and `models list --provider <id>` for discovery. Use `providers set --kind video|audio --provider <id>` for incremental changes, `models set --kind video|audio --provider <id> --model <id>` for the saved default model, and `providers default <id> --kind video|audio` for the default service. These commands save configuration without generating media. Read [references/cli.md](references/cli.md) for examples.
61
+
62
+ Audio is an independent OpenAI-compatible TTS configuration with model, voice, base URL and speech endpoint; this version does not generate speech or support music/sound-effect models. Reuse existing credentials by omitting key flags. For new credentials use `--api-key-env <name>` or `--api-key-stdin`; never include raw keys in command arguments, chat, or `call --input`. Do not read settings files or decrypt credentials. Saving or listing a model does not prove remote availability.
63
+
64
+ ## Saved annotation and mask requests
65
+
66
+ - Use `tuda call list_modification_requests --json` and `get_modification_request` to read a user's saved edit brief. Inspect its fixed source version and export the annotation/mask with `tuda assets export --path <managed-path> --out <local-file>` for visual inspection. Never substitute the currently active canvas version.
67
+ - A needs-input request must be clarified with the user before `clarify_modification_request`. Saving or clarifying does not call a model.
68
+ - Atomically `claim_modification_request` using a unique agent-session owner. The claim token is returned only by this call. Do not print or share it. Lists and detail views intentionally omit it.
69
+ - Execute `edit_image` with boardId, the request's sourcePath, reviewed prompt, modificationRequestId and claimToken. The App validates the claim, carries mask/reference inputs and binds one durable execution key. Do not use the convenience `image edit --input` for this path: it would re-import the fixed source.
70
+ - Wait for the returned task. Pass its actual succeeded `job.id` and managed output paths to `finish_modification_request` with the same requestId and claimToken. Completion appends results once to the original node and notifies its real board. Record errors with the same finish operation; do not fabricate results or auto-retry an unknown paid outcome.
71
+ - `tuda tasks cancel <id>` cancels a task only before provider submission. A running reply explicitly means remote cancellation and billing were not stopped. Ctrl+C stops waiting only. Unknown is not success: inspect history and provider state before submitting again.
72
+
73
+ ## Optional command review metadata
74
+
75
+ `tuda guard descriptor` reads the bundled HOL Guard contribution source offline. It does not install Guard, enable protection, or grant permissions. Generation, editing, output writes, configuration installation and generic operation calls have different effects. Do not classify every `tuda call` or `tasks wait --out` as read-only. Guard's global policy remains authoritative; a descriptor does not isolate provider budgets or replace the user's authorization.
76
+
77
+ ## Validate delivery
78
+
79
+ Generation success is distinct from delivery acceptance. For exact sizes, batches and editing work, read [references/visual-qa.md](references/visual-qa.md). Check actual file dimensions, original-resolution content and corresponding board metadata; a requested size is not proof of output size. `resize_image` affects canvas display width, not physical pixels. Layout checks find node overlaps, not clipped content. Use the bundled validation script when needed.
80
+
81
+ When a paid output fails validation, retain the task and board context, report the concrete failure and ask before an additional generation unless the user already authorized it. Do not require another permission for read-only inspection or the work already requested.
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "portable-skill-adapter",
3
+ "schema_version": "1.0.0",
4
+ "platform": "generic",
5
+ "mappings": [
6
+ {
7
+ "capability_id": "command.execute",
8
+ "status": "implemented",
9
+ "features": [
10
+ "node22",
11
+ "local-process",
12
+ "json-output",
13
+ "filesystem"
14
+ ],
15
+ "implementation": {
16
+ "kind": "cli",
17
+ "discovery": "Resolve Node.js from the current host; use the selected Skill directory .runtime/dist/main.js or the installed tuda executable. Probe version --json and capabilities --json."
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Using Tuda"
3
+ short_description: "手动调用 Tuda,生成编辑图片视频、管理画板素材并验证交付"
4
+ default_prompt: "Use $using-tuda to complete my request with Tuda and validate the delivered assets."
5
+ policy:
6
+ allow_implicit_invocation: false
@@ -0,0 +1,44 @@
1
+ {
2
+ "schema": "portable-skill-capabilities",
3
+ "schema_version": "1.0.0",
4
+ "skill": "using-tuda",
5
+ "target_platforms": [
6
+ "generic"
7
+ ],
8
+ "capabilities": [
9
+ {
10
+ "id": "command.execute",
11
+ "purpose": "Execute the Tuda CLI and read structured results",
12
+ "required": true,
13
+ "required_features": [
14
+ "node22",
15
+ "local-process",
16
+ "json-output",
17
+ "filesystem"
18
+ ],
19
+ "input_fields": [
20
+ "argv",
21
+ "cwd"
22
+ ],
23
+ "output_fields": [
24
+ "exit_code",
25
+ "stdout",
26
+ "stderr"
27
+ ],
28
+ "side_effects": {
29
+ "local_write": "selected workspace and exported assets",
30
+ "external_write": "user-authorized model calls",
31
+ "cost": "provider-dependent; generation requires user authorization",
32
+ "external_mutation": true,
33
+ "may_cost_money": true,
34
+ "authorization": "Model generation requires user authorization; local operations affect only the requested workspace."
35
+ },
36
+ "errors": [
37
+ "CAPABILITY_UNAVAILABLE",
38
+ "AUTH_REQUIRED",
39
+ "PROVIDER_FAILURE",
40
+ "OUTPUT_CONTRACT_FAILED"
41
+ ]
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,63 @@
1
+ # CLI reference
2
+
3
+ Run `tuda help` and `tuda tools list --json` for the installed version's contract. `--json` prints `{ schemaVersion, ok, data }` or `{ schemaVersion, ok: false, error }`; progress goes to stderr.
4
+
5
+ ```bash
6
+ tuda providers list --kind image --json
7
+ tuda models list --provider <provider-id> --json
8
+ tuda image generate --prompt "A quiet forest" --n 1 --out ./forest.png --json
9
+ tuda image edit --input ./forest.png --prompt "Change the season to autumn" --out ./autumn.png --json
10
+ tuda tasks get <task-id> --json
11
+ tuda tasks wait <task-id> --out ./result.png --json
12
+ tuda video generate --prompt "Slow camera movement over the forest" --async --json
13
+ tuda tasks wait <video-task-id> --kind video --json
14
+ tuda boards list --json
15
+ tuda boards show <board-id> --json
16
+ tuda call get_board_layout --input '{"boardId":"<board-id>"}' --json
17
+ tuda call arrange_images --input '{"boardId":"<board-id>","nodeIds":["<a>","<b>"],"mode":"grid"}' --json
18
+ tuda call generate_video --input '{"boardId":"<board-id>","prompt":"Slow camera movement","mode":"generate","referenceImagePaths":["<managed-path>"]}' --json
19
+ tuda assets export --path <managed-path> --out ./image.png --json
20
+ ```
21
+
22
+ For JSON with shell-sensitive text, construct arguments with your shell's proper escaping. Never interpolate untrusted prompts into executable shell code. Shell-safe argument arrays are preferred when available.
23
+
24
+ Video and TTS configuration (requires an App version exposing `configure_provider`):
25
+
26
+ ```bash
27
+ tuda providers show <provider-id> --json
28
+ tuda models set --kind video --provider <provider-id> --model <video-model-id> --json
29
+ tuda providers set --kind audio --provider narration --name "Narration" --base-url https://api.openai.com --model gpt-4o-mini-tts --voice nova --api-key-env OPENAI_API_KEY --default --json
30
+ tuda providers set --kind audio --provider narration --voice alloy --json
31
+ tuda providers default narration --kind audio --json
32
+ tuda providers remove narration --kind audio --json
33
+ ```
34
+
35
+ `providers set` upserts by ID, generating one when omitted. New video providers require `--type ark|fal|openrouter|minimax`, `--base-url` and `--model`. Audio uses `openai-compatible`, with `--speech-endpoint /v1/audio/speech` by default; the endpoint is relative to the configured base URL. Audio configuration is persisted only; there is no speech-generation command yet. Repeated `--saved-model` replaces the saved list and includes the selected default model. Omitted fields, including credentials, are preserved. Use `--default` to select the service as its kind's default. Built-in official providers cannot be changed.
36
+
37
+ Credentials are accepted through `--api-key-env <variable>` or piped `--api-key-stdin`; never pass raw keys in arguments or `call --input`. `--clear-api-key` explicitly clears a key. These flags are mutually exclusive. Queries return only credential presence. `--connection-provider <image-provider-id>` reuses a matching fal/OpenRouter image connection for video, and cannot be combined with setting a standalone key. Use `--connection-provider none` to detach first. Configuration does not submit generation or verify remote availability.
38
+
39
+ `image generate` supports `--provider`, `--model`, `--size`, `--quality`, `--background`, `--style`, `--n`, repeatable `--reference`, `--board`, `--async`, `--request-id` and `--out`. Supported values depend on the App's provider adapter. `models list` returns saved configuration; it does not probe a remote catalogue.
40
+
41
+ File imports (`--reference`, edit `--input`, and `assets import --input`) accept non-empty files up to 20 MiB (20,971,520 bytes) each. Image/video data URLs have a separate decoded-byte budget; their Base64 text does not use the prompt's 100,000-character allowance. Imports preserve the original bytes without automatic compression. Oversize errors identify the file and byte limit; reduce the file or import it through the App and pass its managed path with `tuda call`. Provider-specific reference limits still apply. The App backend must include this fix; updating only the CLI cannot repair an older App's import validation.
42
+
43
+ Task states `succeeded` (images) and `completed` (video) indicate a generated result; `failed`, `interrupted` and `cancelled` are unsuccessful terminal states. Poll uncertain tasks rather than resubmitting them. `stop_video_polling` stops this App session's polling only.
44
+
45
+ `tuda call` exposes the entire native workspace operation catalogue, including board CRUD and layout, image/video generation, references, media import, exports and recoverable image trash. Import image/video operations accept explicit data URLs; they are intended for programmatic callers. Prefer file-based CLI imports in an agent workflow so base64 does not enter the conversation.
46
+
47
+ Without `--board`, generation uses the default board in Settings → Coding Agent; when unset it reuses or creates `CLI 创作`. An explicit `--board` overrides this preference. A deleted default board causes an error before submission; choose a new default or provide `--board`.
48
+
49
+ ### 本地切图
50
+
51
+ ```sh
52
+ tuda image slice --path <受管图片路径> --board <画板ID> --rows 3 --columns 4 --out ./avatars --json
53
+ tuda image slice --input ./sheet.png --rows 3 --columns 4 --margin 12 --gap 8 --json
54
+ tuda image slice --node <节点ID> --board <画板ID> --crops '[{"x":10,"y":20,"width":300,"height":300}]' --json
55
+ ```
56
+
57
+ 复用 App 本地切图,不调用模型;保留原图,切片保存为受管 PNG 并加入画板。源参数三选一,宫格与 crops 互斥;每轴最多 8 格、最多 64 个区域。坐标、边距和间距均为原图像素;不自动去底、识别主体或统一输出尺寸。返回 images、crops、nodeIds 和可选 exportedPaths;--out 为目录,已有文件默认不覆盖。高级入口为 `tuda call slice_image --input '<JSON>' --json`,精确 schema 见 tools list。
58
+
59
+ ## Reusable image templates
60
+
61
+ Use `tuda templates list|show <id>` to inspect custom templates shared with the App. Create with `templates create --name <name> --prompt <fixed-rules>`; update with `templates update <id> --prompt <rules>` (optional `--expected-prompt <previous-rules>`). `--input <JSON-file>` supports structured fields. Saving templates does not call a model.
62
+
63
+ Use `image generate --template <id> --prompt <run-content>` to reuse fixed rules without editing the template. Edit-mode templates require `image edit --template <id> --input <file> --prompt <instruction>`. Generation retains the source template ID. Raw operations: `list_templates`, `get_template`, `create_template`, `update_template`; `generate_image`/`edit_image` accept `templateId`. Built-in presets are not included. Legacy local templates migrate when the upgraded workspace first opens; keep App and CLI versions aligned.
@@ -0,0 +1,59 @@
1
+ # Tuda Visual QA
2
+
3
+ Use this checklist for multi-image batches, UI screens, exact-size requests, edited versions, and any task where clipping matters.
4
+
5
+ ## Output Contract
6
+
7
+ Record these values before generation:
8
+
9
+ - Expected result count.
10
+ - One canonical pixel size when exact dimensions matter; otherwise one canonical aspect ratio.
11
+ - Whether every result must have identical pixel dimensions. Default to yes for a batch.
12
+ - Critical-content safe area. When the user does not specify one, keep critical text and controls outside the outer 4% of image width and 3% of image height.
13
+ - Which existing image, if any, defines the source dimensions and visual system.
14
+
15
+ Use a single source of truth. Do not combine a `size` argument such as `1024x1536` with a prompt requesting a different ratio such as `375x812`.
16
+
17
+ ## Dimension Validation
18
+
19
+ 1. Read the output paths and reported node/version dimensions from `get_generation_status` and `get_board`.
20
+ 2. Read physical file dimensions with the bundled validator. Compare them with the corresponding node and active-version metadata. Any mismatch is a hard failure because the board can fit the new pixels into a stale frame and crop the result.
21
+ 3. Compare active-version dimensions with their source versions after every edit.
22
+ 4. For a batch, run the bundled validator:
23
+
24
+ ```bash
25
+ node scripts/validate-image-batch.mjs --expected 1080x1920 image-1.png image-2.png
26
+ node scripts/validate-image-batch.mjs --ratio 9:16 image-1.png image-2.png
27
+ ```
28
+
29
+ The validator requires identical dimensions by default. Use `--allow-mixed-dimensions` only when the user explicitly accepts variable sizes.
30
+
31
+ 5. Treat a nonzero validator exit as a delivery failure. Report physical and metadata dimensions separately and do not retry a paid operation automatically.
32
+
33
+ ## Full-Resolution Visual Check
34
+
35
+ Inspect every output at original resolution when an image inspection tool is available. Check:
36
+
37
+ - All four edges for clipped glyphs, status indicators, labels, menus, and controls.
38
+ - Critical content stays inside the declared safe area.
39
+ - The first and last rows, bottom navigation, and primary action are fully visible.
40
+ - Text does not overflow, wrap unexpectedly, merge with adjacent content, or become unreadable.
41
+ - Edited versions do not shift, zoom, or crop compared with their source frame.
42
+ - Repeated screens use consistent status-bar, content, and navigation insets.
43
+
44
+ Do not infer these properties from prompt wording.
45
+
46
+ ## Board Check
47
+
48
+ 1. Call `get_board_layout` and require no overlapping node pairs.
49
+ 2. Compare every node's `width`, `height`, `displayWidth`, active version, and version dimensions with the physical file dimensions. Do not accept stale frame metadata.
50
+ 3. Open or fit the board, then inspect the board presentation at a useful zoom when a visual control tool is available.
51
+ 4. Verify that the board preview uses the complete image rather than a cropped frame. Pay special attention to outputs with a different aspect ratio from their neighbors.
52
+
53
+ `overlappingNodePairs: []` proves only that node rectangles do not overlap. It says nothing about internal image cropping.
54
+
55
+ ## Acceptance States
56
+
57
+ - **Pass:** Tasks succeeded, dimensions match the contract, full-resolution checks pass, and board presentation is complete.
58
+ - **Validation failed:** The provider returned images, but dimensions, safe areas, version framing, or visual integrity failed.
59
+ - **Unverified:** Required visual inspection was unavailable. Report the missing check; do not describe the output as uncropped or delivery-ready.
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ function usage(message) {
8
+ if (message) process.stderr.write(`${message}\n\n`);
9
+ process.stderr.write(
10
+ "Usage: node scripts/validate-image-batch.mjs [--expected WIDTHxHEIGHT] [--ratio W:H] " +
11
+ "[--tolerance DECIMAL] [--allow-mixed-dimensions] <image...>\n"
12
+ );
13
+ process.exit(2);
14
+ }
15
+
16
+ function parsePositiveNumber(value, label) {
17
+ const number = Number(value);
18
+ if (!Number.isFinite(number) || number <= 0) usage(`${label} must be a positive number.`);
19
+ return number;
20
+ }
21
+
22
+ function parsePair(value, separator, label) {
23
+ const parts = value.toLowerCase().split(separator);
24
+ if (parts.length !== 2) usage(`${label} must use ${separator === "x" ? "WIDTHxHEIGHT" : "W:H"}.`);
25
+ return parts.map((part) => parsePositiveNumber(part, label));
26
+ }
27
+
28
+ function parseArgs(argv) {
29
+ const options = {
30
+ expected: null,
31
+ ratio: null,
32
+ tolerance: 0.001,
33
+ allowMixedDimensions: false,
34
+ paths: []
35
+ };
36
+
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const argument = argv[index];
39
+ if (argument === "--expected") {
40
+ options.expected = parsePair(argv[++index] ?? "", "x", "--expected");
41
+ } else if (argument === "--ratio") {
42
+ const [width, height] = parsePair(argv[++index] ?? "", ":", "--ratio");
43
+ options.ratio = width / height;
44
+ } else if (argument === "--tolerance") {
45
+ options.tolerance = parsePositiveNumber(argv[++index] ?? "", "--tolerance");
46
+ } else if (argument === "--allow-mixed-dimensions") {
47
+ options.allowMixedDimensions = true;
48
+ } else if (argument.startsWith("--")) {
49
+ usage(`Unknown option: ${argument}`);
50
+ } else {
51
+ options.paths.push(argument);
52
+ }
53
+ }
54
+
55
+ if (options.paths.length === 0) usage("At least one image path is required.");
56
+ return options;
57
+ }
58
+
59
+ function pngDimensions(buffer) {
60
+ const signature = "89504e470d0a1a0a";
61
+ if (buffer.length < 24 || buffer.subarray(0, 8).toString("hex") !== signature) return null;
62
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20), format: "png" };
63
+ }
64
+
65
+ function jpegDimensions(buffer) {
66
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return null;
67
+ const startOfFrameMarkers = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf]);
68
+ let offset = 2;
69
+
70
+ while (offset + 4 <= buffer.length) {
71
+ if (buffer[offset] !== 0xff) {
72
+ offset += 1;
73
+ continue;
74
+ }
75
+ while (offset < buffer.length && buffer[offset] === 0xff) offset += 1;
76
+ const marker = buffer[offset];
77
+ offset += 1;
78
+ if (marker === 0xd8 || marker === 0xd9) continue;
79
+ if (offset + 2 > buffer.length) break;
80
+ const segmentLength = buffer.readUInt16BE(offset);
81
+ if (segmentLength < 2 || offset + segmentLength > buffer.length) break;
82
+ if (startOfFrameMarkers.has(marker)) {
83
+ if (segmentLength < 7) break;
84
+ return { width: buffer.readUInt16BE(offset + 5), height: buffer.readUInt16BE(offset + 3), format: "jpeg" };
85
+ }
86
+ offset += segmentLength;
87
+ }
88
+ return null;
89
+ }
90
+
91
+ async function dimensionsFor(filePath) {
92
+ const buffer = await readFile(filePath);
93
+ const dimensions = pngDimensions(buffer) ?? jpegDimensions(buffer);
94
+ if (!dimensions) throw new Error("unsupported image format; expected PNG or JPEG");
95
+ if (dimensions.width < 1 || dimensions.height < 1) throw new Error("invalid image dimensions");
96
+ return { path: path.resolve(filePath), ...dimensions, ratio: dimensions.width / dimensions.height };
97
+ }
98
+
99
+ const options = parseArgs(process.argv.slice(2));
100
+ const results = [];
101
+ const failures = [];
102
+
103
+ for (const filePath of options.paths) {
104
+ try {
105
+ results.push(await dimensionsFor(filePath));
106
+ } catch (error) {
107
+ failures.push({ path: path.resolve(filePath), reason: error instanceof Error ? error.message : String(error) });
108
+ }
109
+ }
110
+
111
+ if (results.length > 0) {
112
+ const canonical = results[0];
113
+ for (const result of results) {
114
+ if (options.expected) {
115
+ const [expectedWidth, expectedHeight] = options.expected;
116
+ if (result.width !== expectedWidth || result.height !== expectedHeight) {
117
+ failures.push({ path: result.path, reason: `expected ${expectedWidth}x${expectedHeight}, got ${result.width}x${result.height}` });
118
+ }
119
+ }
120
+ if (!options.allowMixedDimensions && (result.width !== canonical.width || result.height !== canonical.height)) {
121
+ failures.push({ path: result.path, reason: `batch dimensions differ from ${canonical.width}x${canonical.height}` });
122
+ }
123
+ if (options.ratio !== null && Math.abs(result.ratio - options.ratio) > options.tolerance) {
124
+ failures.push({ path: result.path, reason: `aspect ratio ${result.ratio.toFixed(6)} differs from ${options.ratio.toFixed(6)}` });
125
+ }
126
+ }
127
+ }
128
+
129
+ const report = {
130
+ ok: failures.length === 0,
131
+ expected: options.expected ? `${options.expected[0]}x${options.expected[1]}` : null,
132
+ ratio: options.ratio,
133
+ tolerance: options.tolerance,
134
+ requireIdenticalDimensions: !options.allowMixedDimensions,
135
+ images: results.map((result) => ({
136
+ path: result.path,
137
+ format: result.format,
138
+ width: result.width,
139
+ height: result.height,
140
+ ratio: Number(result.ratio.toFixed(6))
141
+ })),
142
+ failures
143
+ };
144
+
145
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
146
+ if (!report.ok) process.exitCode = 1;