Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
294 lines
7.6 KiB
JavaScript
294 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { execFileSync } from "node:child_process";
|
||
import { existsSync, readFileSync } from "node:fs";
|
||
import { dirname, extname, join, resolve } from "node:path";
|
||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||
|
||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||
const defaultWorkspaceRoot = resolve(scriptDirectory, "../..");
|
||
const supportedExtensions = new Set([".png"]);
|
||
|
||
const directoryBudgets = [
|
||
{
|
||
prefix: "apps/desktop/src/renderer/src/assets/workspace-canvas/dock/",
|
||
maxLongEdge: 192,
|
||
maxBytes: 96 * 1024
|
||
},
|
||
{
|
||
prefix: "packages/agent/gui/app/renderer/assets/icons/agents/",
|
||
maxLongEdge: 192,
|
||
maxBytes: 96 * 1024
|
||
},
|
||
{
|
||
prefix: "packages/agent/gui/app/renderer/assets/icons/agent-vinyls/",
|
||
maxLongEdge: 128,
|
||
maxBytes: 96 * 1024
|
||
},
|
||
{
|
||
prefix: "apps/desktop/src/renderer/src/features/app-update/assets/",
|
||
maxLongEdge: 64,
|
||
maxBytes: 32 * 1024
|
||
}
|
||
];
|
||
|
||
const exactBudgets = new Map(
|
||
[
|
||
[
|
||
"apps/desktop/src/renderer/src/assets/account-plans/reward-toast-bg.png",
|
||
560,
|
||
512 * 1024
|
||
],
|
||
[
|
||
"apps/desktop/src/renderer/src/assets/workspace-canvas/dock/default/tutti.png",
|
||
192,
|
||
96 * 1024,
|
||
6
|
||
],
|
||
[
|
||
"packages/agent/gui/app/renderer/assets/icons/agent-vinyl-player-chassis.png",
|
||
192,
|
||
96 * 1024
|
||
],
|
||
[
|
||
"packages/agent/gui/app/renderer/assets/icons/agents/manage-agent-tutti.png",
|
||
192,
|
||
96 * 1024,
|
||
6
|
||
],
|
||
[
|
||
"packages/agent/gui/app/renderer/assets/icons/agent-vinyl-tonearm.png",
|
||
192,
|
||
96 * 1024
|
||
],
|
||
[
|
||
"packages/agent/gui/app/renderer/assets/icons/user-avatar-placeholder.png",
|
||
128,
|
||
64 * 1024
|
||
],
|
||
[
|
||
"packages/browser/workbench-node/src/assets/workspace-dock-website.png",
|
||
192,
|
||
96 * 1024
|
||
],
|
||
[
|
||
"packages/workspace/issue-manager/src/assets/workspace-dock-task.png",
|
||
192,
|
||
96 * 1024
|
||
],
|
||
[
|
||
"packages/workspace/file-manager/src/assets/workspace-archive-fallback.png",
|
||
256,
|
||
128 * 1024
|
||
],
|
||
[
|
||
"packages/commerce/web/src/assets/registration-credits-bg.png",
|
||
560,
|
||
512 * 1024
|
||
],
|
||
["packages/commerce/web/src/assets/star-free.png", 64, 32 * 1024],
|
||
["packages/commerce/web/src/assets/star-lite.png", 64, 32 * 1024],
|
||
["packages/commerce/web/src/assets/star-pro.png", 64, 32 * 1024],
|
||
["packages/commerce/web/src/assets/star-ultra.png", 64, 32 * 1024],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-at.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-clipboard.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-electric-plug.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-joystick.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-satellite-antenna.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-toolbox.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/icon-window-layout.png",
|
||
64,
|
||
32 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/logo1.png",
|
||
128,
|
||
64 * 1024
|
||
],
|
||
[
|
||
"services/tuttid/builtin-apps/tutti-onboarding/public/assets/tone-light.png",
|
||
128,
|
||
64 * 1024
|
||
]
|
||
].map(([path, maxLongEdge, maxBytes, requiredPngColorType]) => [
|
||
path,
|
||
{
|
||
maxLongEdge,
|
||
maxBytes,
|
||
...(requiredPngColorType === undefined ? {} : { requiredPngColorType })
|
||
}
|
||
])
|
||
);
|
||
|
||
export function runtimeImageBudgetForPath(path) {
|
||
const normalized = normalizePath(path);
|
||
const exact = exactBudgets.get(normalized);
|
||
if (exact) {
|
||
return exact;
|
||
}
|
||
|
||
return directoryBudgets.find(({ prefix }) => normalized.startsWith(prefix));
|
||
}
|
||
|
||
export function isRuntimeImageBudgetPath(path) {
|
||
return (
|
||
supportedExtensions.has(extname(path).toLowerCase()) &&
|
||
runtimeImageBudgetForPath(path) !== undefined
|
||
);
|
||
}
|
||
|
||
export function readPngDimensions(content) {
|
||
if (
|
||
content.length < 24 ||
|
||
!content
|
||
.subarray(0, 8)
|
||
.equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ||
|
||
content.toString("ascii", 12, 16) !== "IHDR"
|
||
) {
|
||
throw new Error("not a valid PNG header");
|
||
}
|
||
|
||
const width = content.readUInt32BE(16);
|
||
const height = content.readUInt32BE(20);
|
||
if (width === 0 || height === 0) {
|
||
throw new Error("PNG dimensions must be positive");
|
||
}
|
||
return { width, height };
|
||
}
|
||
|
||
export function analyzeRuntimeImage({ path, content }) {
|
||
const budget = runtimeImageBudgetForPath(path);
|
||
if (!budget) {
|
||
return [];
|
||
}
|
||
|
||
let dimensions;
|
||
try {
|
||
dimensions = readPngDimensions(content);
|
||
} catch (error) {
|
||
return [`${path}: ${error.message}`];
|
||
}
|
||
|
||
const diagnostics = [];
|
||
const longEdge = Math.max(dimensions.width, dimensions.height);
|
||
if (longEdge > budget.maxLongEdge) {
|
||
diagnostics.push(
|
||
`${path}: ${dimensions.width}×${dimensions.height} exceeds ${budget.maxLongEdge}px long-edge budget`
|
||
);
|
||
}
|
||
if (content.length > budget.maxBytes) {
|
||
diagnostics.push(
|
||
`${path}: ${formatBytes(content.length)} exceeds ${formatBytes(budget.maxBytes)} file budget`
|
||
);
|
||
}
|
||
if (
|
||
budget.requiredPngColorType !== undefined &&
|
||
content[25] !== budget.requiredPngColorType
|
||
) {
|
||
diagnostics.push(
|
||
`${path}: uses PNG color type ${content[25] ?? "unknown"}; requires RGBA color type 6 to preserve gradients and transparency`
|
||
);
|
||
}
|
||
return diagnostics;
|
||
}
|
||
|
||
export function checkRuntimeImageBudgets({
|
||
workspaceRoot = defaultWorkspaceRoot,
|
||
staged = false
|
||
} = {}) {
|
||
const paths = listPaths({ workspaceRoot, staged });
|
||
const diagnostics = paths.flatMap((path) =>
|
||
analyzeRuntimeImage({
|
||
path,
|
||
content: readContent({ workspaceRoot, path, staged })
|
||
})
|
||
);
|
||
|
||
return { diagnostics, paths };
|
||
}
|
||
|
||
function listPaths({ workspaceRoot, staged }) {
|
||
const args = staged
|
||
? ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"]
|
||
: ["ls-files", "--cached", "--others", "--exclude-standard", "-z"];
|
||
return execFileSync("git", args, {
|
||
cwd: workspaceRoot,
|
||
encoding: "utf8"
|
||
})
|
||
.split("\0")
|
||
.filter(Boolean)
|
||
.map(normalizePath)
|
||
.filter(isRuntimeImageBudgetPath)
|
||
.filter((path) => staged || existsSync(join(workspaceRoot, path)));
|
||
}
|
||
|
||
function readContent({ workspaceRoot, path, staged }) {
|
||
if (staged) {
|
||
return execFileSync("git", ["show", `:${path}`], {
|
||
cwd: workspaceRoot,
|
||
maxBuffer: 20 * 1024 * 1024
|
||
});
|
||
}
|
||
return readFileSync(join(workspaceRoot, path));
|
||
}
|
||
|
||
function normalizePath(path) {
|
||
return path.replaceAll("\\", "/");
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
return `${Math.ceil(bytes / 1024)} KiB`;
|
||
}
|
||
|
||
function runCli() {
|
||
const staged = process.argv.includes("--staged");
|
||
const { diagnostics, paths } = checkRuntimeImageBudgets({ staged });
|
||
|
||
if (diagnostics.length > 0) {
|
||
console.error(
|
||
"Runtime image budget check failed. Keep design masters under design-assets/runtime-images/originals and commit a bounded runtime copy:"
|
||
);
|
||
for (const diagnostic of diagnostics) {
|
||
console.error(`- ${diagnostic}`);
|
||
}
|
||
process.exitCode = 1;
|
||
return;
|
||
}
|
||
|
||
console.log(
|
||
`Runtime image budgets passed (${paths.length} ${staged ? "staged " : ""}assets)`
|
||
);
|
||
}
|
||
|
||
if (
|
||
process.argv[1] &&
|
||
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
||
) {
|
||
runCli();
|
||
}
|