Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ npm run build # generated-artifact Pages build
npm run check:full # release-grade full generation, validation, test, desktop, and build path
```

The generator streams one theme at a time, reuses verified mode-and-size outputs, and reports cache, elapsed-time, and peak-RSS telemetry. Use `npm run generate -- --ids=qinglan-odyssey --summary` for a targeted regeneration, `npm run generate:check -- --concurrency=1 --summary` for a reproducibility readback, and `npm run generate:benchmark -- --scenario=warm` for the 60-second warm-cache budget.

### Method C: Install the desktop app manually

1. Download the build for your operating system and CPU from [GitHub Releases](https://github.com/rwang23/awesome-codex-theme/releases).
Expand Down Expand Up @@ -204,6 +206,8 @@ npm run test:generated
npm run test:ci
npm run generate
npm run generate:check
npm run generate -- --ids=qinglan-odyssey --summary
npm run generate:benchmark -- --scenario=warm
npm run validate
npm run check:full
npm run screenshots:capture
Expand Down
4 changes: 4 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ npm run build # 依赖生成产物的 Pages 构建
npm run check:full # 发布级完整生成、校验、测试、桌面检查与构建
```

生成器按主题流式处理,复用已经验证的 mode/尺寸产物,并报告缓存、耗时与峰值 RSS。用 `npm run generate -- --ids=qinglan-odyssey --summary` 定向重建,用 `npm run generate:check -- --concurrency=1 --summary` 回读可复现性,用 `npm run generate:benchmark -- --scenario=warm` 检查 60 秒的热缓存预算。

### 方式 C:手动安装桌面应用

1. 从 [GitHub Releases](https://github.com/rwang23/awesome-codex-theme/releases) 下载与你的系统和 CPU 匹配的版本。
Expand Down Expand Up @@ -204,6 +206,8 @@ npm run test:generated
npm run test:ci
npm run generate
npm run generate:check
npm run generate -- --ids=qinglan-odyssey --summary
npm run generate:benchmark -- --scenario=warm
npm run validate
npm run check:full
npm run screenshots:capture
Expand Down
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Reworked theme generation into a bounded, incremental pipeline with
source-art provenance checks, complete render fingerprints, atomic per-theme
writes, cache-aware progress, and reproducible benchmark commands.
- Changed the Gallery community section to clearly state that public community
submissions are not open and removed its hosted-community link. Added a
direct Theme Manager download action to the Gallery hero.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"art:generate": "node scripts/run-image-jobs.mjs",
"generate": "node scripts/generate-themes.mjs",
"generate:check": "node scripts/generate-themes.mjs --check",
"generate:benchmark": "node scripts/benchmark-generator.mjs",
"validate": "node scripts/ensure-generated.mjs && node scripts/validate.mjs",
"test": "npm run test:source",
"test:source": "node --test tests/source-contract.test.mjs tests/full-skin-runtime.test.mjs",
Expand Down
10 changes: 10 additions & 0 deletions schemas/registry.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
"required": [
"preview",
"previewSha256",
"previewBytes",
"previewRenderFingerprint",
"assetSha256",
"assetBytes",
"fullSkin",
Expand All @@ -196,6 +198,14 @@
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"previewBytes": {
"type": "integer",
"minimum": 1
},
"previewRenderFingerprint": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"assetSha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
Expand Down
39 changes: 38 additions & 1 deletion schemas/theme-pack.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,49 @@
"mode": {
"type": "object",
"additionalProperties": false,
"required": ["asset", "art", "tokens", "integrity", "nativeTheme"],
"required": ["asset", "preview", "art", "tokens", "integrity", "nativeTheme"],
"properties": {
"asset": {
"type": "string",
"pattern": "^assets/[a-z0-9-]+\\.png$"
},
"preview": {
"type": "object",
"additionalProperties": false,
"required": ["path", "integrity"],
"properties": {
"path": {
"type": "string",
"pattern": "^previews/(light|dark)\\.png$"
},
"integrity": {
"type": "object",
"additionalProperties": false,
"required": ["sha256", "bytes", "width", "height", "renderFingerprint"],
"properties": {
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"bytes": {
"type": "integer",
"minimum": 1,
"maximum": 16777216
},
"width": {
"const": 960
},
"height": {
"const": 540
},
"renderFingerprint": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
}
}
}
}
},
"art": {
"type": "object",
"additionalProperties": false,
Expand Down
85 changes: 85 additions & 0 deletions scripts/benchmark-generator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
generateRepository,
parseGenerationOptions,
} from "./lib/theme-generator.mjs";

const DEFAULT_WARM_LIMIT_MS = 60_000;

function usage() {
return [
"Usage: node scripts/benchmark-generator.mjs [--scenario=warm|cold|one-theme] [--ids=id[,id]] [--concurrency=1..4] [--max-ms=60000]",
"",
"All benchmark scenarios run in --check mode and never write generated files.",
"warm verifies cache reuse; cold forces rendering; one-theme requires --ids and forces only that theme.",
].join("\n");
}

function parseBenchmarkOptions(argv) {
let scenario = "warm";
let maxMs = DEFAULT_WARM_LIMIT_MS;
const generatorArgs = [];

for (const argument of argv) {
if (argument === "--help" || argument === "-h") return { help: true };
if (argument.startsWith("--scenario=")) {
scenario = argument.slice("--scenario=".length);
continue;
}
if (argument.startsWith("--max-ms=")) {
maxMs = Number.parseInt(argument.slice("--max-ms=".length), 10);
continue;
}
generatorArgs.push(argument);
}

if (!new Set(["warm", "cold", "one-theme"]).has(scenario)) {
throw new Error("--scenario must be warm, cold, or one-theme");
}
if (!Number.isInteger(maxMs) || maxMs < 1) throw new Error("--max-ms must be a positive integer");
const generator = parseGenerationOptions(["--check", ...generatorArgs]);
if (generator.help) return { help: true };
if (generator.force) throw new Error("The benchmark selects force mode from its scenario; omit --force");
if (scenario === "one-theme" && (!generator.ids || generator.ids.length !== 1)) {
throw new Error("--scenario=one-theme requires exactly one --ids value");
}

return {
generator: {
...generator,
force: scenario !== "warm",
},
maxMs,
scenario,
};
}

async function main() {
const options = parseBenchmarkOptions(process.argv.slice(2));
if (options.help) {
console.log(usage());
return;
}
const summary = await generateRepository({ options: options.generator });
if (summary.drift) throw new Error("Benchmark requires reproducible generated output:\n- " + summary.drift.join("\n- "));

const result = {
...summary,
budgetMs: options.maxMs,
scenario: options.scenario,
withinBudget: summary.elapsedMs <= options.maxMs,
};
console.log(JSON.stringify(result));
if (!result.withinBudget) {
process.exitCode = 1;
console.error(
"Generator benchmark exceeded " + options.maxMs + "ms: " + summary.elapsedMs + "ms (" + options.scenario + ").",
);
}
}

try {
await main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
28 changes: 28 additions & 0 deletions scripts/generate-theme-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { readFile } from "node:fs/promises";
import { parentPort } from "node:worker_threads";

import { decodePng, renderSourceArtImage } from "./lib/png.mjs";

function transferableBuffer(data) {
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
}

parentPort.on("message", async function ({ id, sourcePath, theme, outputs }) {
try {
const source = await readFile(sourcePath);
const decoded = decodePng(source);
const rendered = outputs.map(function (output) {
const data = renderSourceArtImage(decoded, theme, output.mode, output.width, output.height);
return {
key: output.key,
data: transferableBuffer(data),
};
});
parentPort.postMessage({ id, rendered }, rendered.map((output) => output.data));
} catch (error) {
parentPort.postMessage({
id,
error: error instanceof Error ? error.message : String(error),
});
}
});
Loading