feat: add agent-first minitool studio MVP
CI / verify (push) Has been cancelled

This commit is contained in:
2026-09-17 21:54:33 +08:00
commit 74a8f7cdf3
51 changed files with 3855 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env node
/** Audit deterministic mini-tool package and text-file size limits. */
import fs from "node:fs";
import path from "node:path";
const MIB = 1024 * 1024;
const ZIP_LIMIT = 10 * MIB;
const ZIP_RECOMMENDED = 2 * MIB;
const TEXT_FILE_WARNING = 2 * MIB;
const TEXT_TOTAL_WARNING = 5 * MIB;
const TEXT_SUFFIXES = new Set([".html", ".css", ".js", ".json"]);
function formatSize(size) {
return `${(size / MIB).toFixed(2)} MiB`;
}
function walkDirectory(root) {
const files = [];
function walk(current) {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
if (entry.name === ".git") continue;
const target = path.join(current, entry.name);
if (entry.isDirectory()) walk(target);
else if (entry.isFile()) files.push(target);
}
}
walk(root);
return files;
}
function auditDirectory(root) {
const errors = [];
const warnings = [];
const files = walkDirectory(root);
let textTotal = 0;
for (const file of files) {
if (!TEXT_SUFFIXES.has(path.extname(file).toLowerCase())) continue;
const size = fs.statSync(file).size;
const name = path.relative(root, file).split(path.sep).join("/");
textTotal += size;
if (size > TEXT_FILE_WARNING) {
warnings.push(`${name}: text file is ${formatSize(size)}; review parse and memory cost`);
}
}
if (textTotal > TEXT_TOTAL_WARNING) {
warnings.push(`uncompressed HTML/CSS/JS/JSON total is ${formatSize(textTotal)}; review for embedded databases or generated content`);
}
return { errors, warnings, count: files.length };
}
function auditZipSize(file) {
const errors = [];
const warnings = [];
const size = fs.statSync(file).size;
if (size > ZIP_LIMIT) errors.push(`${path.basename(file)}: zip is ${formatSize(size)}; hard limit is 10 MiB`);
else if (size > ZIP_RECOMMENDED) warnings.push(`${path.basename(file)}: zip is ${formatSize(size)}; recommended target is 2 MiB`);
return { errors, warnings, count: 1, note: "Node audit checks zip size only; artifact contents must be audited before packaging" };
}
function main() {
const target = process.argv[2];
if (!target) {
console.error("Usage: node audit_artifact.mjs <artifact-directory-or-zip>");
return 2;
}
const resolved = path.resolve(target);
if (!fs.existsSync(resolved)) {
console.error(`ERROR: path not found: ${resolved}`);
return 2;
}
let result;
try {
result = fs.statSync(resolved).isDirectory()
? auditDirectory(resolved)
: auditZipSize(resolved);
} catch (error) {
console.error(`ERROR: cannot inspect ${resolved}: ${error.message}`);
return 2;
}
for (const message of result.errors) console.log(`ERROR: ${message}`);
for (const message of result.warnings) console.log(`WARN: ${message}`);
if (result.note) console.log(`NOTE: ${result.note}`);
if (result.errors.length) {
console.log(`FAILED: ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
return 1;
}
console.log(`PASS: ${result.count} file(s), ${result.warnings.length} warning(s)`);
return 0;
}
process.exitCode = main();
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Audit deterministic mini-tool package and text-file size limits."""
from __future__ import annotations
import sys
import zipfile
from pathlib import Path, PurePosixPath
MIB = 1024 * 1024
ZIP_LIMIT = 10 * MIB
ZIP_RECOMMENDED = 2 * MIB
TEXT_FILE_WARNING = 2 * MIB
TEXT_TOTAL_WARNING = 5 * MIB
TEXT_SUFFIXES = {".html", ".css", ".js", ".json"}
def format_size(size: int) -> str:
return f"{size / MIB:.2f} MiB"
def audit_entries(entries: list[tuple[str, int]]) -> list[str]:
warnings: list[str] = []
text_total = 0
for name, size in entries:
if PurePosixPath(name).suffix.lower() not in TEXT_SUFFIXES:
continue
text_total += size
if size > TEXT_FILE_WARNING:
warnings.append(
f"{name}: text file is {format_size(size)}; review parse and memory cost"
)
if text_total > TEXT_TOTAL_WARNING:
warnings.append(
f"uncompressed HTML/CSS/JS/JSON total is {format_size(text_total)}; "
"review for embedded databases or generated content"
)
return warnings
def read_directory(path: Path) -> list[tuple[str, int]]:
return [
(file.relative_to(path).as_posix(), file.stat().st_size)
for file in sorted(path.rglob("*"))
if file.is_file() and ".git" not in file.relative_to(path).parts
]
def read_zip(path: Path) -> tuple[list[tuple[str, int]], list[str], list[str]]:
errors: list[str] = []
warnings: list[str] = []
if path.stat().st_size > ZIP_LIMIT:
errors.append(
f"{path.name}: zip is {format_size(path.stat().st_size)}; hard limit is 10 MiB"
)
elif path.stat().st_size > ZIP_RECOMMENDED:
warnings.append(
f"{path.name}: zip is {format_size(path.stat().st_size)}; recommended target is 2 MiB"
)
with zipfile.ZipFile(path) as archive:
infos = [info for info in archive.infolist() if not info.is_dir()]
entries = [(info.filename, info.file_size) for info in infos]
return entries, errors, warnings
def main() -> int:
if len(sys.argv) != 2:
print("Usage: python3 audit_artifact.py <artifact-directory-or-zip>")
return 2
path = Path(sys.argv[1]).resolve()
if not path.exists():
print(f"ERROR: path not found: {path}")
return 2
errors: list[str] = []
warnings: list[str] = []
try:
if path.is_dir():
entries = read_directory(path)
elif zipfile.is_zipfile(path):
entries, zip_errors, zip_warnings = read_zip(path)
errors.extend(zip_errors)
warnings.extend(zip_warnings)
else:
print(f"ERROR: expected a directory or zip file: {path}")
return 2
except (OSError, zipfile.BadZipFile) as exc:
print(f"ERROR: cannot inspect {path}: {exc}")
return 2
warnings.extend(audit_entries(entries))
for message in errors:
print(f"ERROR: {message}")
for message in warnings:
print(f"WARN: {message}")
if errors:
print(f"FAILED: {len(errors)} error(s), {len(warnings)} warning(s)")
return 1
print(f"PASS: {len(entries)} file(s), {len(warnings)} warning(s)")
return 0
if __name__ == "__main__":
raise SystemExit(main())