@toonstrip/cli 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/dist/bin.d.ts +2 -0
- package/dist/bin.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pack-validate.d.ts +5 -0
- package/dist/pack-validate.js +84 -0
- package/dist/png-dimensions.d.ts +5 -0
- package/dist/png-dimensions.js +14 -0
- package/package.json +27 -0
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { validatePack } from "./pack-validate.js";
|
|
3
|
+
function usage() {
|
|
4
|
+
console.error("Usage: toonstrip pack validate <dir>");
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
const [command, subcommand, arg] = process.argv.slice(2);
|
|
8
|
+
if (command === "pack" && subcommand === "validate") {
|
|
9
|
+
if (!arg)
|
|
10
|
+
usage();
|
|
11
|
+
const result = validatePack(arg);
|
|
12
|
+
if (result.ok) {
|
|
13
|
+
console.log(`OK: ${arg} is a valid pack`);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
console.error(`FAIL: ${arg} is not a valid pack`);
|
|
17
|
+
for (const error of result.errors)
|
|
18
|
+
console.error(` - ${error}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
usage();
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toonstrip pack validate <dir>` — checks a pack against the contract in
|
|
3
|
+
* TOONSTRIP_DEV_PLAN.md §6: `pack.json` shape, that every declared character's
|
|
4
|
+
* sheet + manifest exist and every character has at least the minimum
|
|
5
|
+
* required pose (NEUTRAL), and that every declared backdrop file exists with
|
|
6
|
+
* dimensions matching what `pack.json` declares. Modeled on grues-in-comic's
|
|
7
|
+
* `art/test/roster.test.ts` gate-2 inventory check.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { readPngDimensions } from "./png-dimensions.js";
|
|
12
|
+
const REQUIRED_EMOTIONS = ["NEUTRAL"];
|
|
13
|
+
function fail(errors, message) {
|
|
14
|
+
errors.push(message);
|
|
15
|
+
}
|
|
16
|
+
export function validatePack(dir) {
|
|
17
|
+
const errors = [];
|
|
18
|
+
const packJsonPath = join(dir, "pack.json");
|
|
19
|
+
if (!existsSync(packJsonPath)) {
|
|
20
|
+
return { ok: false, errors: [`${packJsonPath}: pack.json not found`] };
|
|
21
|
+
}
|
|
22
|
+
let pack;
|
|
23
|
+
try {
|
|
24
|
+
pack = JSON.parse(readFileSync(packJsonPath, "utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { ok: false, errors: [`${packJsonPath}: invalid JSON (${err.message})`] };
|
|
28
|
+
}
|
|
29
|
+
for (const field of ["name", "version", "license"]) {
|
|
30
|
+
if (!pack[field])
|
|
31
|
+
fail(errors, `pack.json: missing required field "${field}"`);
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(pack.characters))
|
|
34
|
+
fail(errors, `pack.json: "characters" must be an array`);
|
|
35
|
+
if (!Array.isArray(pack.backdrops))
|
|
36
|
+
fail(errors, `pack.json: "backdrops" must be an array`);
|
|
37
|
+
const seenIds = new Set();
|
|
38
|
+
for (const entry of pack.characters ?? []) {
|
|
39
|
+
if (seenIds.has(entry.id))
|
|
40
|
+
fail(errors, `character "${entry.id}" declared more than once`);
|
|
41
|
+
seenIds.add(entry.id);
|
|
42
|
+
const manifestPath = join(dir, entry.manifest);
|
|
43
|
+
const sheetPath = join(dir, entry.sheet);
|
|
44
|
+
if (!existsSync(manifestPath)) {
|
|
45
|
+
fail(errors, `character "${entry.id}": manifest not found at ${entry.manifest}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!existsSync(sheetPath)) {
|
|
49
|
+
fail(errors, `character "${entry.id}": sheet not found at ${entry.sheet}`);
|
|
50
|
+
}
|
|
51
|
+
let avatar;
|
|
52
|
+
try {
|
|
53
|
+
avatar = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
fail(errors, `character "${entry.id}": invalid manifest JSON (${err.message})`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const faces = avatar.faces ?? [];
|
|
60
|
+
for (const required of REQUIRED_EMOTIONS) {
|
|
61
|
+
if (!faces.some((f) => f.kind === "face" && f.emotion === required)) {
|
|
62
|
+
fail(errors, `character "${entry.id}": missing required "${required}" face`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const backdrop of pack.backdrops ?? []) {
|
|
67
|
+
const filePath = join(dir, backdrop.file);
|
|
68
|
+
if (!existsSync(filePath)) {
|
|
69
|
+
fail(errors, `backdrop "${backdrop.id}": file not found at ${backdrop.file}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const dims = readPngDimensions(filePath);
|
|
74
|
+
if (dims.width !== backdrop.width || dims.height !== backdrop.height) {
|
|
75
|
+
fail(errors, `backdrop "${backdrop.id}": pack.json declares ${backdrop.width}x${backdrop.height}, ` +
|
|
76
|
+
`file is ${dims.width}x${dims.height}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
fail(errors, `backdrop "${backdrop.id}": ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { ok: errors.length === 0, errors };
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
3
|
+
/** Reads a PNG's declared width/height straight out of its IHDR chunk. */
|
|
4
|
+
export function readPngDimensions(path) {
|
|
5
|
+
const buf = readFileSync(path);
|
|
6
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
7
|
+
throw new Error(`${path}: not a PNG file`);
|
|
8
|
+
}
|
|
9
|
+
// IHDR is always the first chunk: 8-byte signature, 4-byte length, 4-byte
|
|
10
|
+
// "IHDR" tag, then width (4 bytes) and height (4 bytes), both big-endian.
|
|
11
|
+
const width = buf.readUInt32BE(16);
|
|
12
|
+
const height = buf.readUInt32BE(20);
|
|
13
|
+
return { width, height };
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toonstrip/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"toonstrip": "./dist/bin.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.9.0",
|
|
21
|
+
"typescript": "^5.6.3",
|
|
22
|
+
"vitest": "^2.1.4"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
27
|
+
}
|