chore: standardized code quality and formatting for tooling scripts
- Added `scripts/**/*.ts` to the linter and formatter inclusion list in `biome.json`. - Replaced manual array index searches with standard `indexOf` operations. - Cleaned up unused variables, obsolete helpers, and command-line flags. - Applied consistent formatting and code style across tooling scripts.
This commit is contained in:
@@ -56,6 +56,7 @@
|
||||
"packages/*/examples/**/*.ts",
|
||||
"packages/*/scripts/**/*.ts",
|
||||
"packages/*/*.ts",
|
||||
"scripts/**/*.ts",
|
||||
"!packages/natives/native/index.d.ts",
|
||||
"!**/vendor/**/*",
|
||||
"!**/node_modules/**/*",
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
|
||||
import * as path from "node:path";
|
||||
|
||||
const PROVIDER_FILE = path.join(
|
||||
import.meta.dir,
|
||||
"../packages/catalog/src/wire/gemini-headers.ts",
|
||||
);
|
||||
const PROVIDER_FILE = path.join(import.meta.dir, "../packages/catalog/src/wire/gemini-headers.ts");
|
||||
|
||||
interface VersionCheck {
|
||||
/** Human label for the report. */
|
||||
@@ -32,7 +29,10 @@ interface VersionCheck {
|
||||
}
|
||||
|
||||
/** Fetch latest non-prerelease tag from a GitHub repo. */
|
||||
async function fetchLatestGitHubRelease(repo: string, parseTag: (tag: string) => string | null): Promise<string | null> {
|
||||
async function fetchLatestGitHubRelease(
|
||||
repo: string,
|
||||
parseTag: (tag: string) => string | null,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// /releases/latest only returns non-prerelease, non-draft releases
|
||||
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
|
||||
@@ -53,7 +53,7 @@ const checks: VersionCheck[] = [
|
||||
name: "Gemini CLI",
|
||||
sourcePattern: /PI_AI_GEMINI_CLI_VERSION\s*\|\|\s*"(\d+\.\d+\.\d+)"/,
|
||||
repo: "google-gemini/gemini-cli",
|
||||
parseTag: (tag) => SEMVER_RE.exec(tag)?.[1] ?? null,
|
||||
parseTag: tag => SEMVER_RE.exec(tag)?.[1] ?? null,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function parseTargetVariants(): NativeBuildVariant[] {
|
||||
const rawVariants = (Bun.env.TARGET_VARIANTS ?? "").trim();
|
||||
if (!rawVariants) return [];
|
||||
|
||||
return rawVariants.split(/\s+/).map((rawVariant) => {
|
||||
return rawVariants.split(/\s+/).map(rawVariant => {
|
||||
const variant = variantConfigs[rawVariant as keyof typeof variantConfigs];
|
||||
if (!variant) {
|
||||
throw new Error(`Unsupported TARGET_VARIANTS entry: ${rawVariant}. Expected baseline or modern.`);
|
||||
|
||||
@@ -237,12 +237,8 @@ const workflowYaml = await Bun.file(WORKFLOW_PATH).text();
|
||||
const concurrencySection = workflowYaml.slice(workflowYaml.indexOf("\nconcurrency:") + 1);
|
||||
const groupRaw = /^\s*group:\s*(\S.*?)\s*$/m.exec(concurrencySection)?.[1];
|
||||
const cancelRaw = /^\s*cancel-in-progress:\s*(\S.*?)\s*$/m.exec(concurrencySection)?.[1];
|
||||
const groupTemplate =
|
||||
groupRaw && groupRaw.startsWith('"') && groupRaw.endsWith('"') ? groupRaw.slice(1, -1) : groupRaw;
|
||||
const cancelTemplate =
|
||||
cancelRaw && cancelRaw.startsWith('"') && cancelRaw.endsWith('"')
|
||||
? cancelRaw.slice(1, -1)
|
||||
: cancelRaw;
|
||||
const groupTemplate = groupRaw?.startsWith('"') && groupRaw.endsWith('"') ? groupRaw.slice(1, -1) : groupRaw;
|
||||
const cancelTemplate = cancelRaw?.startsWith('"') && cancelRaw.endsWith('"') ? cancelRaw.slice(1, -1) : cancelRaw;
|
||||
if (!groupTemplate || !cancelTemplate) {
|
||||
throw new Error("could not locate concurrency.group / cancel-in-progress in ci.yml");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import * as path from "node:path";
|
||||
|
||||
interface BinaryTarget {
|
||||
@@ -66,11 +66,11 @@ const targets: BinaryTarget[] = [
|
||||
];
|
||||
|
||||
function parseRequestedTargets(): Set<string> | null {
|
||||
const flagIndex = process.argv.findIndex(arg => arg === "--targets");
|
||||
const flagIndex = process.argv.indexOf("--targets");
|
||||
const flagValue =
|
||||
flagIndex >= 0
|
||||
? process.argv[flagIndex + 1]
|
||||
: process.argv.find(arg => arg.startsWith("--targets="))?.split("=", 2)[1] ?? Bun.env.RELEASE_TARGETS;
|
||||
: (process.argv.find(arg => arg.startsWith("--targets="))?.split("=", 2)[1] ?? Bun.env.RELEASE_TARGETS);
|
||||
|
||||
if (!flagValue) {
|
||||
return null;
|
||||
@@ -147,9 +147,7 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const buildEnv = shouldAdhocSignDarwinBinary(target)
|
||||
? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" }
|
||||
: Bun.env;
|
||||
const buildEnv = shouldAdhocSignDarwinBinary(target) ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } : Bun.env;
|
||||
await runCommand(buildCompileCommand(target), repoRoot, buildEnv);
|
||||
|
||||
// Bun 1.3.12 emits a truncated Mach-O signature on darwin builds.
|
||||
@@ -186,9 +184,7 @@ async function resetArtifacts(): Promise<void> {
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const requestedTargets = parseRequestedTargets();
|
||||
const selectedTargets = requestedTargets
|
||||
? targets.filter(target => requestedTargets.has(target.id))
|
||||
: targets;
|
||||
const selectedTargets = requestedTargets ? targets.filter(target => requestedTargets.has(target.id)) : targets;
|
||||
|
||||
if (requestedTargets) {
|
||||
const unknownTargets = [...requestedTargets].filter(
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
compareVersions,
|
||||
enumerateChangelogVersions,
|
||||
mergePackageSection,
|
||||
} from "./ci-release-notes";
|
||||
import { compareVersions, enumerateChangelogVersions, mergePackageSection } from "./ci-release-notes";
|
||||
|
||||
const FIXTURE = [
|
||||
"# Changelog",
|
||||
@@ -86,9 +82,7 @@ describe("mergePackageSection", () => {
|
||||
it("includes every silent-tag section above floor up to target inclusive", () => {
|
||||
const merged = mergePackageSection(FIXTURE, "15.12.4", "15.13.0");
|
||||
// 15.12.6 and 15.12.5 unique fingerprints must land.
|
||||
expect(merged).toContain(
|
||||
"Removed `writeLine`/`writeLineSync` from the public SessionStorageWriter contract.",
|
||||
);
|
||||
expect(merged).toContain("Removed `writeLine`/`writeLineSync` from the public SessionStorageWriter contract.");
|
||||
expect(merged).toContain("Added package-level exports for session context.");
|
||||
expect(merged).toContain("Changed terminal resize handling to paint only the visible viewport.");
|
||||
// 15.12.4 entry stays excluded — it is the floor.
|
||||
@@ -120,8 +114,7 @@ describe("mergePackageSection", () => {
|
||||
});
|
||||
|
||||
it("returns empty string when no version in the requested range carries body content", () => {
|
||||
const empty = ["# Changelog", "", "## [15.13.0] - 2026-06-14", "", "## [15.12.6] - 2026-06-14"]
|
||||
.join("\n");
|
||||
const empty = ["# Changelog", "", "## [15.13.0] - 2026-06-14", "", "## [15.12.6] - 2026-06-14"].join("\n");
|
||||
expect(mergePackageSection(empty, "15.12.5", "15.13.0")).toBe("");
|
||||
});
|
||||
|
||||
|
||||
+11
-13
@@ -39,8 +39,14 @@ const CATEGORY_ORDER = ["Breaking Changes", "Added", "Changed", "Fixed", "Remove
|
||||
|
||||
/** Compare two `X.Y.Z` (or `vX.Y.Z`) version strings; non-semver returns 0. */
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const am = a.replace(/^v/, "").trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
const bm = b.replace(/^v/, "").trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
const am = a
|
||||
.replace(/^v/, "")
|
||||
.trim()
|
||||
.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
const bm = b
|
||||
.replace(/^v/, "")
|
||||
.trim()
|
||||
.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!am || !bm) return 0;
|
||||
if (am[1] !== bm[1]) return Number(am[1]) - Number(bm[1]);
|
||||
if (am[2] !== bm[2]) return Number(am[2]) - Number(bm[2]);
|
||||
@@ -88,11 +94,7 @@ export function enumerateChangelogVersions(content: string): ChangelogVersionSpa
|
||||
* `floorExclusive === null` → take only the target version (legacy behavior).
|
||||
* Returns "" when no in-range version contributes any bullet.
|
||||
*/
|
||||
export function mergePackageSection(
|
||||
content: string,
|
||||
floorExclusive: string | null,
|
||||
targetInclusive: string,
|
||||
): string {
|
||||
export function mergePackageSection(content: string, floorExclusive: string | null, targetInclusive: string): string {
|
||||
const spans = enumerateChangelogVersions(content)
|
||||
.filter(v => {
|
||||
if (compareVersions(v.version, targetInclusive) > 0) return false;
|
||||
@@ -240,9 +242,7 @@ async function resolvePublishedFloorTag(targetVersion: string): Promise<string |
|
||||
async function main(): Promise<void> {
|
||||
const tagInput = process.argv[2] ?? process.env.GITHUB_REF_NAME ?? "";
|
||||
if (!tagInput) {
|
||||
console.error(
|
||||
"Error: version not provided. Pass as argv (e.g. `v15.4.3`) or set GITHUB_REF_NAME.",
|
||||
);
|
||||
console.error("Error: version not provided. Pass as argv (e.g. `v15.4.3`) or set GITHUB_REF_NAME.");
|
||||
process.exit(1);
|
||||
}
|
||||
const version = tagInput.replace(/^v/, "").trim();
|
||||
@@ -267,9 +267,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
console.warn(
|
||||
`No CHANGELOG entries found for version ${version}; writing empty release notes to ${outputPath}.`,
|
||||
);
|
||||
console.warn(`No CHANGELOG entries found for version ${version}; writing empty release notes to ${outputPath}.`);
|
||||
await Bun.write(outputPath, "");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import {
|
||||
type GeneratedLeafPackage,
|
||||
generateNpmPackages,
|
||||
LEAF_TARGETS,
|
||||
type GeneratedLeafPackage,
|
||||
} from "../packages/natives/scripts/gen-npm-packages.ts";
|
||||
|
||||
export interface PublishPackage {
|
||||
@@ -271,7 +271,12 @@ async function publishNativeLeafPackage(tag: string): Promise<void> {
|
||||
const pkgDir = path.join(repoRoot, pkg.dir);
|
||||
const coreManifest = (await Bun.file(path.join(pkgDir, "package.json")).json()) as PackageManifest;
|
||||
if (typeof coreManifest.version !== "string") throw new Error(`Missing version in ${pkg.dir}/package.json`);
|
||||
const leaves = await generateNpmPackages({ packageDir: pkgDir, dryRun: isDryRun, version: coreManifest.version, tags: [tag] });
|
||||
const leaves = await generateNpmPackages({
|
||||
packageDir: pkgDir,
|
||||
dryRun: isDryRun,
|
||||
version: coreManifest.version,
|
||||
tags: [tag],
|
||||
});
|
||||
const leaf = leaves[0];
|
||||
if (!leaf) throw new Error(`No native leaf generated for ${tag}`);
|
||||
await publishGeneratedLeafPackage(leaf);
|
||||
@@ -283,7 +288,9 @@ async function publishNativePackage(pkg: PublishPackage): Promise<void> {
|
||||
const name = manifest.name ?? path.basename(pkg.dir);
|
||||
if (isDryRun) {
|
||||
console.log(`DRY RUN native core manifest rewrite (${pkg.dir})`);
|
||||
console.log(JSON.stringify({ optionalDependencies: manifest.optionalDependencies, files: manifest.files }, null, "\t"));
|
||||
console.log(
|
||||
JSON.stringify({ optionalDependencies: manifest.optionalDependencies, files: manifest.files }, null, "\t"),
|
||||
);
|
||||
}
|
||||
await packAndPublish(pkgDir, name);
|
||||
}
|
||||
|
||||
+18
-30
@@ -59,7 +59,7 @@ const validModes: Record<Mode, true> = {
|
||||
};
|
||||
|
||||
// `chunkSize` splits a bucket's file list into that-many-file groups, each run as a
|
||||
// separate `bun --smol test` child process. A fresh process per chunk resets Bun's
|
||||
// separate `bun test` child process. A fresh process per chunk resets Bun's
|
||||
// heap and reaps any dangling spawned children between groups, keeping peak RSS
|
||||
// under the CI runner's OOM ceiling (a single 170–370-file invocation gets
|
||||
// SIGKILLed at 137). The singleton/global-state bucket is left whole: its suites
|
||||
@@ -100,8 +100,7 @@ const nativeAndIntegrationPackages = [
|
||||
// Packages the CI buckets deliberately skip but a local full run should still
|
||||
// cover. mnemopi's embedding suites need a ~270MB fastembed model absent from CI
|
||||
// runners (so it flakes/times out there); robomp-web lives under python/robomp
|
||||
// and is outside every CI TS bucket. Both run with `--smol` to bound RSS when
|
||||
// fanned out alongside everything else.
|
||||
// and is outside every CI TS bucket.
|
||||
const localOnlyWorkspacePackages = ["packages/mnemopi", "python/robomp/web"];
|
||||
|
||||
// Repo-level script tests. CI's `workspace` bucket only runs the concurrency
|
||||
@@ -194,13 +193,7 @@ const codingAgentUiContentMarkers = [
|
||||
"renderToString",
|
||||
];
|
||||
|
||||
const codingAgentRuntimeContentMarkers = [
|
||||
"AgentSession",
|
||||
"SessionManager",
|
||||
"AuthStorage",
|
||||
"Bun.sleep",
|
||||
"setTimeout(",
|
||||
];
|
||||
const codingAgentRuntimeContentMarkers = ["AgentSession", "SessionManager", "AuthStorage", "Bun.sleep", "setTimeout("];
|
||||
|
||||
let codingAgentTestPartitionPromise: Promise<CodingAgentTestPartition> | null = null;
|
||||
|
||||
@@ -211,16 +204,12 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function workspaceTestCommand(
|
||||
pkg: string,
|
||||
parallel: number,
|
||||
options: { smol?: boolean; extraArgs?: string[] } = {},
|
||||
): TestCommand {
|
||||
const { smol = false, extraArgs = [] } = options;
|
||||
function workspaceTestCommand(pkg: string, parallel: number, options: { extraArgs?: string[] } = {}): TestCommand {
|
||||
const { extraArgs = [] } = options;
|
||||
return {
|
||||
label: pkg,
|
||||
cwd: pkg,
|
||||
command: ["bun", ...(smol ? ["--smol"] : []), "test", `--parallel=${parallel}`, ...extraArgs],
|
||||
command: ["bun", "test", `--parallel=${parallel}`, ...extraArgs],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -273,10 +262,7 @@ function classifyCodingAgentTest(testFile: string, content: string): CodingAgent
|
||||
) {
|
||||
return "native";
|
||||
}
|
||||
if (
|
||||
matchesAnyPath(testFile, codingAgentUiPathPatterns) ||
|
||||
hasAnyMarker(content, codingAgentUiContentMarkers)
|
||||
) {
|
||||
if (matchesAnyPath(testFile, codingAgentUiPathPatterns) || hasAnyMarker(content, codingAgentUiContentMarkers)) {
|
||||
return "ui";
|
||||
}
|
||||
if (
|
||||
@@ -335,7 +321,7 @@ async function codingAgentTestCommands(bucket: CodingAgentBucket): Promise<TestC
|
||||
commands.push({
|
||||
label: `packages/coding-agent (${plan.label}; ${testFiles.length} files; parallel=${plan.parallel}${chunkLabel}; ${chunk.length} files)`,
|
||||
cwd: "packages/coding-agent",
|
||||
command: ["bun", "--smol", "test", `--parallel=${plan.parallel}`, ...onlyFailuresArgs, ...chunk],
|
||||
command: ["bun", "test", `--parallel=${plan.parallel}`, ...onlyFailuresArgs, ...chunk],
|
||||
});
|
||||
}
|
||||
return commands;
|
||||
@@ -383,12 +369,8 @@ async function commandsForMode(mode: Mode): Promise<TestCommand[]> {
|
||||
case "local-ts":
|
||||
return [
|
||||
...fastWorkspacePackages.map(pkg => workspaceTestCommand(pkg, 8, { extraArgs: onlyFailuresArgs })),
|
||||
...nativeAndIntegrationPackages.map(pkg =>
|
||||
workspaceTestCommand(pkg, 4, { smol: true, extraArgs: onlyFailuresArgs }),
|
||||
),
|
||||
...localOnlyWorkspacePackages.map(pkg =>
|
||||
workspaceTestCommand(pkg, 4, { smol: true, extraArgs: onlyFailuresArgs }),
|
||||
),
|
||||
...nativeAndIntegrationPackages.map(pkg => workspaceTestCommand(pkg, 4, { extraArgs: onlyFailuresArgs })),
|
||||
...localOnlyWorkspacePackages.map(pkg => workspaceTestCommand(pkg, 4, { extraArgs: onlyFailuresArgs })),
|
||||
...(await commandsForMode("coding-agent-heavy")),
|
||||
{
|
||||
label: "scripts",
|
||||
@@ -616,7 +598,11 @@ export function formatFailureReport(failures: ChunkOutcome[], total: number, rep
|
||||
const header = `${failures.length} of ${total} test chunk(s) FAILED`;
|
||||
const lines: string[] = ["", style.bold(style.red(`━━━ ${header} ━━━`))];
|
||||
for (const failure of failures) {
|
||||
lines.push("", style.bold(style.red(`✗ ${failure.label} (exit ${failure.exitCode})`)), style.dim(`$ ${failure.command}`));
|
||||
lines.push(
|
||||
"",
|
||||
style.bold(style.red(`✗ ${failure.label} (exit ${failure.exitCode})`)),
|
||||
style.dim(`$ ${failure.command}`),
|
||||
);
|
||||
const failing = extractFailingTests(failure.output);
|
||||
// Fully attributed only when every failure carries its own bun block;
|
||||
// otherwise (no markers, or a marker with no preceding frame — timeouts,
|
||||
@@ -703,7 +689,9 @@ async function runTestCommandsInParallel(commands: TestCommand[], concurrency: n
|
||||
}
|
||||
if (quiet) {
|
||||
const totalSeconds = (performance.now() - runStartedAt) / 1000;
|
||||
process.stdout.write(`${formatSummaryFooter(commands.length - failures.length, failures.length, totalSeconds)}\n`);
|
||||
process.stdout.write(
|
||||
`${formatSummaryFooter(commands.length - failures.length, failures.length, totalSeconds)}\n`,
|
||||
);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
process.exitCode = 1;
|
||||
|
||||
@@ -23,7 +23,7 @@ interface ReleaseAsset {
|
||||
function parseArgs(argv: readonly string[]): { tag: string; out: string | null } {
|
||||
const rest = [...argv];
|
||||
let out: string | null = null;
|
||||
const outIdx = rest.findIndex(a => a === "--out");
|
||||
const outIdx = rest.indexOf("--out");
|
||||
if (outIdx >= 0) {
|
||||
out = rest[outIdx + 1] ?? null;
|
||||
if (!out) throw new Error("--out requires a path");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
import { runClaudeTraceCommand, type ClaudeTraceCommandArgs } from "../packages/coding-agent/src/cli/claude-trace-cli";
|
||||
import { type ClaudeTraceCommandArgs, runClaudeTraceCommand } from "../packages/coding-agent/src/cli/claude-trace-cli";
|
||||
|
||||
const HELP = `Usage: bun scripts/claude-trace.ts [options]
|
||||
|
||||
|
||||
+19
-15
@@ -90,7 +90,10 @@ function parseNumber(text: string): number {
|
||||
}
|
||||
|
||||
function getCell(text: string, label: string): string | null {
|
||||
const re = new RegExp(`^\\|\\s*\\*?\\*?${escapeRegex(label)}\\*?\\*?\\s*\\|\\s*\\*?\\*?(.+?)\\*?\\*?\\s*\\|\\s*$`, "m");
|
||||
const re = new RegExp(
|
||||
`^\\|\\s*\\*?\\*?${escapeRegex(label)}\\*?\\*?\\s*\\|\\s*\\*?\\*?(.+?)\\*?\\*?\\s*\\|\\s*$`,
|
||||
"m",
|
||||
);
|
||||
const m = text.match(re);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
@@ -125,12 +128,6 @@ function parseRatePair(value: string | null): { numerator: number; denominator:
|
||||
};
|
||||
}
|
||||
|
||||
function parseFraction(value: string | null): { num: number; denom: number } {
|
||||
if (!value) return { num: 0, denom: 0 };
|
||||
const m = value.match(/([0-9,]+)\s*\/\s*([0-9,]+)/);
|
||||
return m ? { num: parseNumber(m[1]), denom: parseNumber(m[2]) } : { num: 0, denom: 0 };
|
||||
}
|
||||
|
||||
async function parseReport(file: string): Promise<ReportRow> {
|
||||
const text = await Bun.file(file).text();
|
||||
|
||||
@@ -221,11 +218,9 @@ function sortRows(rows: ReportRow[], by: typeof sortBy): ReportRow[] {
|
||||
};
|
||||
const cmp: Record<typeof sortBy, (a: ReportRow, b: ReportRow) => number> = {
|
||||
sep: (a, b) =>
|
||||
sepOrder.indexOf(a.sepSlug) - sepOrder.indexOf(b.sepSlug) ||
|
||||
modelOrder(a.model) - modelOrder(b.model),
|
||||
sepOrder.indexOf(a.sepSlug) - sepOrder.indexOf(b.sepSlug) || modelOrder(a.model) - modelOrder(b.model),
|
||||
model: (a, b) =>
|
||||
modelOrder(a.model) - modelOrder(b.model) ||
|
||||
sepOrder.indexOf(a.sepSlug) - sepOrder.indexOf(b.sepSlug),
|
||||
modelOrder(a.model) - modelOrder(b.model) || sepOrder.indexOf(a.sepSlug) - sepOrder.indexOf(b.sepSlug),
|
||||
task: (a, b) => b.taskSuccessPct - a.taskSuccessPct,
|
||||
edit: (a, b) => b.editSuccessPct - a.editSuccessPct,
|
||||
tokens: (a, b) => a.totalTokensAvg - b.totalTokensAvg,
|
||||
@@ -236,7 +231,9 @@ function sortRows(rows: ReportRow[], by: typeof sortBy): ReportRow[] {
|
||||
const entries = (
|
||||
await Promise.all(
|
||||
resolvedDirs.map(async d =>
|
||||
(await fs.readdir(d, { withFileTypes: true }))
|
||||
(
|
||||
await fs.readdir(d, { withFileTypes: true })
|
||||
)
|
||||
.filter(e => e.isFile() && e.name.endsWith(".md"))
|
||||
.map(e => path.join(d, e.name)),
|
||||
),
|
||||
@@ -283,13 +280,20 @@ function mergeRows(input: ReportRow[]): ReportRow[] {
|
||||
successfulRuns,
|
||||
taskSuccessPct: ratio(successfulRuns, totalRuns),
|
||||
verifiedPct: ratio(successfulRuns, totalRuns),
|
||||
editToolUsagePct: ratio(sumField(list, r => Math.round((r.editToolUsagePct / 100) * r.totalRuns)), totalRuns),
|
||||
editToolUsagePct: ratio(
|
||||
sumField(list, r => Math.round((r.editToolUsagePct / 100) * r.totalRuns)),
|
||||
totalRuns,
|
||||
),
|
||||
editSuccessPct: ratio(patchAttempts - patchFailures, patchAttempts),
|
||||
patchFailurePct: ratio(patchFailures, patchAttempts),
|
||||
patchFailures,
|
||||
patchAttempts,
|
||||
mutationIntentPct: list.reduce((a, r) => a + (Number.isFinite(r.mutationIntentPct) ? r.mutationIntentPct : 0), 0) / list.length,
|
||||
autocorrectFreePct: list.reduce((a, r) => a + (Number.isFinite(r.autocorrectFreePct) ? r.autocorrectFreePct : 0), 0) / list.length,
|
||||
mutationIntentPct:
|
||||
list.reduce((a, r) => a + (Number.isFinite(r.mutationIntentPct) ? r.mutationIntentPct : 0), 0) /
|
||||
list.length,
|
||||
autocorrectFreePct:
|
||||
list.reduce((a, r) => a + (Number.isFinite(r.autocorrectFreePct) ? r.autocorrectFreePct : 0), 0) /
|
||||
list.length,
|
||||
tasksAllPassing: sumField(list, r => r.tasksAllPassing),
|
||||
tasksFlakyFailing: sumField(list, r => r.tasksFlakyFailing),
|
||||
timeoutRuns: sumField(list, r => r.timeoutRuns),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { $ } from "bun";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import { collectPromotableAddedItemLines, fixChangelogContent, runChangelogFixer } from "./fix-changelogs";
|
||||
|
||||
describe("collectPromotableAddedItemLines", () => {
|
||||
@@ -82,7 +82,8 @@ describe("fixChangelogContent", () => {
|
||||
|
||||
expect(result.promotedItems).toBe(2);
|
||||
expect(result.mergedDuplicateHeadings).toBe(1);
|
||||
expect(result.content).toBe([
|
||||
expect(result.content).toBe(
|
||||
[
|
||||
"# Changelog",
|
||||
"",
|
||||
"## [Unreleased]",
|
||||
@@ -107,7 +108,8 @@ describe("fixChangelogContent", () => {
|
||||
"",
|
||||
"- Historical fix.",
|
||||
"",
|
||||
].join("\n"));
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops Unreleased items that already appear verbatim in a released section", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $, Glob } from "bun";
|
||||
import * as path from "node:path";
|
||||
import { $, Glob } from "bun";
|
||||
|
||||
const CHANGELOG_GLOB = "packages/*/CHANGELOG.md";
|
||||
const ORDERED_SECTION_TITLES = ["Breaking Changes", "Added", "Changed", "Fixed", "Removed"] as const;
|
||||
@@ -97,7 +97,6 @@ interface HistoricalReleaseRecovery {
|
||||
sectionsByTitle: Map<string, ReleaseSection>;
|
||||
}
|
||||
|
||||
|
||||
function isReleaseHeading(line: string): boolean {
|
||||
return /^## \[[^\]]+\]/.test(line);
|
||||
}
|
||||
@@ -377,7 +376,6 @@ function compactAdjacentListSpacing(lines: readonly string[]): string[] {
|
||||
return flattenedItems;
|
||||
}
|
||||
|
||||
|
||||
function normalizeSection(section: ReleaseSection): FixCounters {
|
||||
const counters: FixCounters = {
|
||||
promotedItems: 0,
|
||||
@@ -456,7 +454,6 @@ function sortReleaseSections(document: ChangelogDocument): void {
|
||||
document.sections = [...unreleasedSections, ...releasedSections];
|
||||
}
|
||||
|
||||
|
||||
function rebuildReleasedSectionsFromHistory(
|
||||
content: string,
|
||||
historicalSectionsByTitle: ReadonlyMap<string, ReleaseSection>,
|
||||
@@ -502,7 +499,6 @@ function rebuildReleasedSectionsFromHistory(
|
||||
return renderChangelog(document);
|
||||
}
|
||||
|
||||
|
||||
export function renderChangelog(document: ChangelogDocument): string {
|
||||
const output: string[] = [];
|
||||
const prefix = trimBlankLines(numberedText(document.prefixLines));
|
||||
@@ -590,7 +586,6 @@ function isAddedReleaseHeadingLine(line: string): boolean {
|
||||
return line.startsWith("+## [");
|
||||
}
|
||||
|
||||
|
||||
function itemKey(pathName: string, text: string): string {
|
||||
return `${pathName}\0${normalizeItemText(text)}`;
|
||||
}
|
||||
@@ -600,7 +595,6 @@ export function collectPromotableAddedItemLines(diffText: string): Map<string, S
|
||||
const removals: RemovedItemOccurrence[] = [];
|
||||
const addedReleaseHeadingHunks = new Set<string>();
|
||||
let currentPath = "";
|
||||
let oldLine = 0;
|
||||
let newLine = 0;
|
||||
let hunkIndex = -1;
|
||||
for (const rawLine of diffText.replace(/\r\n/g, "\n").split("\n")) {
|
||||
@@ -617,7 +611,6 @@ export function collectPromotableAddedItemLines(diffText: string): Map<string, S
|
||||
|
||||
const hunkMatch = rawLine.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
oldLine = Number(hunkMatch[1]);
|
||||
newLine = Number(hunkMatch[2]);
|
||||
hunkIndex++;
|
||||
continue;
|
||||
@@ -655,12 +648,10 @@ export function collectPromotableAddedItemLines(diffText: string): Map<string, S
|
||||
pairedWithAddition: false,
|
||||
});
|
||||
}
|
||||
oldLine++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (marker === " ") {
|
||||
oldLine++;
|
||||
newLine++;
|
||||
}
|
||||
}
|
||||
@@ -765,9 +756,7 @@ async function resolveSince(repoRoot: string, since: string | undefined): Promis
|
||||
*/
|
||||
async function recoveryTags(repoRoot: string): Promise<string[]> {
|
||||
const baseline = await changelogBaselineCommit(repoRoot);
|
||||
const listArgs = baseline
|
||||
? ["tag", "--contains", baseline, "--sort=v:refname"]
|
||||
: ["tag", "--sort=v:refname"];
|
||||
const listArgs = baseline ? ["tag", "--contains", baseline, "--sort=v:refname"] : ["tag", "--sort=v:refname"];
|
||||
return (await git(listArgs, repoRoot))
|
||||
.split("\n")
|
||||
.map(tag => tag.trim())
|
||||
@@ -826,7 +815,6 @@ async function collectHistoricalReleaseRecovery(
|
||||
return recoveryByPath;
|
||||
}
|
||||
|
||||
|
||||
export async function changelogPaths(repoRoot: string): Promise<string[]> {
|
||||
const glob = new Glob(CHANGELOG_GLOB);
|
||||
const paths: string[] = [];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
||||
/**
|
||||
* Codemod: rewrite relative test imports that reach into a package's `src/`
|
||||
* into the package's public subpath import.
|
||||
@@ -17,16 +19,13 @@
|
||||
* bun scripts/fix-test-imports.ts --write # apply the changes
|
||||
*/
|
||||
import { Glob } from "bun";
|
||||
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
const ROOT = resolve(import.meta.dir, "..");
|
||||
const WRITE = process.argv.includes("--write");
|
||||
|
||||
// Matches the module specifier of `from "x"`, `import "x"`, `import("x")`,
|
||||
// `require("x")` / `export ... from "x"` — but only when it starts with `./`/`../`.
|
||||
const SPEC_RE =
|
||||
/(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bimport\s+)(["'])((?:\.\.?\/)[^"']*)\2/g;
|
||||
const SPEC_RE = /(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bimport\s+)(["'])((?:\.\.?\/)[^"']*)\2/g;
|
||||
|
||||
// Source-module extensions. A specifier resolving to one of these has a public
|
||||
// `./*` -> `./src/*.ts` subpath; anything else (.json/.md/...) is an asset.
|
||||
@@ -148,7 +147,7 @@ const skippedAssets: string[] = [];
|
||||
|
||||
for (const file of [...files].sort()) {
|
||||
const { content, changes, skipped } = rewriteFile(file);
|
||||
if (skipped.length) skippedAssets.push(...skipped.map((s) => `${relative(ROOT, file)}: ${s}`));
|
||||
if (skipped.length) skippedAssets.push(...skipped.map(s => `${relative(ROOT, file)}: ${s}`));
|
||||
if (!changes.length) continue;
|
||||
changedFiles++;
|
||||
totalChanges += changes.length;
|
||||
@@ -158,9 +157,7 @@ for (const file of [...files].sort()) {
|
||||
if (WRITE) writeFileSync(file, content);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n${WRITE ? "Applied" : "Would apply"} ${totalChanges} rewrite(s) across ${changedFiles} file(s).`,
|
||||
);
|
||||
console.log(`\n${WRITE ? "Applied" : "Would apply"} ${totalChanges} rewrite(s) across ${changedFiles} file(s).`);
|
||||
if (skippedAssets.length) {
|
||||
console.log(`\nSkipped ${skippedAssets.length} asset import(s) into src (no public subpath):`);
|
||||
for (const s of skippedAssets) console.log(` ${s}`);
|
||||
|
||||
@@ -414,7 +414,7 @@ describe("inline-functions: type soundness", () => {
|
||||
describe("inline-functions: formatting safety", () => {
|
||||
test("wraps the tail exactly one indent level deeper than the inverted guard", () => {
|
||||
const { text } = run(
|
||||
'function dispatch(r: Runtime): void {\n\thandle(r.item);\n}\n' +
|
||||
"function dispatch(r: Runtime): void {\n\thandle(r.item);\n}\n" +
|
||||
'function handle(item: Item | null): void {\n\tif (item?.type !== "x") return;\n\ttouch(item);\n}\n',
|
||||
);
|
||||
// `if` sits at the function-body indent (one tab); its body is one deeper.
|
||||
|
||||
+21
-20
@@ -71,7 +71,6 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { $ } from "bun";
|
||||
import { IndentationText, Node, Project, SyntaxKind, VariableDeclarationKind } from "ts-morph";
|
||||
import type {
|
||||
CallExpression,
|
||||
Expression,
|
||||
@@ -81,6 +80,7 @@ import type {
|
||||
SourceFile,
|
||||
Statement,
|
||||
} from "ts-morph";
|
||||
import { IndentationText, Node, Project, SyntaxKind, VariableDeclarationKind } from "ts-morph";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -241,7 +241,8 @@ function isPureExpr(node: Node, strict: boolean): boolean {
|
||||
if (ASSIGNMENT_OPS.has(node.getOperatorToken().getText())) return false;
|
||||
return isPureExpr(node.getLeft(), strict) && isPureExpr(node.getRight(), strict);
|
||||
}
|
||||
if (Node.isTemplateExpression(node)) return node.getTemplateSpans().every((s) => isPureExpr(s.getExpression(), strict));
|
||||
if (Node.isTemplateExpression(node))
|
||||
return node.getTemplateSpans().every(s => isPureExpr(s.getExpression(), strict));
|
||||
// Defining a closure is itself side-effect-free; we substitute the value, never invoke it.
|
||||
if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) return true;
|
||||
return false;
|
||||
@@ -293,7 +294,7 @@ function argNeedsParens(node: Node): boolean {
|
||||
function subText(node: Node, edits: readonly Edit[]): string {
|
||||
const start = node.getStart();
|
||||
const end = node.getEnd();
|
||||
const local = edits.filter((e) => e.start >= start && e.end <= end).sort((a, b) => b.start - a.start);
|
||||
const local = edits.filter(e => e.start >= start && e.end <= end).sort((a, b) => b.start - a.start);
|
||||
let text = node.getText();
|
||||
for (const e of local) text = text.slice(0, e.start - start) + e.text + text.slice(e.end - start);
|
||||
return text;
|
||||
@@ -349,9 +350,9 @@ function wrap(part: NegatedExpr, minPrec: number): string {
|
||||
|
||||
/** Combine leading guard conditions into the single positive run-condition. */
|
||||
function combineGuards(guards: readonly Expression[], edits: readonly Edit[]): string {
|
||||
const parts = guards.map((g) => negate(g, edits));
|
||||
const parts = guards.map(g => negate(g, edits));
|
||||
if (parts.length === 1) return parts[0].text;
|
||||
return parts.map((p) => wrap(p, PREC_AND)).join(" && ");
|
||||
return parts.map(p => wrap(p, PREC_AND)).join(" && ");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -410,15 +411,16 @@ function isNameOnly(id: Identifier): boolean {
|
||||
* exception — it names a real value binding even though it lives in a type node.
|
||||
*/
|
||||
function isTypePositioned(id: Identifier): boolean {
|
||||
if (!id.getFirstAncestor((a) => Node.isTypeNode(a))) return false;
|
||||
return !id.getFirstAncestor((a) => Node.isTypeQuery(a));
|
||||
if (!id.getFirstAncestor(a => Node.isTypeNode(a))) return false;
|
||||
return !id.getFirstAncestor(a => Node.isTypeQuery(a));
|
||||
}
|
||||
|
||||
/** Free identifiers in the body that resolve outside it (module / global) — shadow-sensitive. */
|
||||
function computeFreeNames(fn: FunctionDeclaration, paramNames: readonly string[]): Set<string> {
|
||||
const body = fn.getBodyOrThrow();
|
||||
const local = new Set<string>(paramNames);
|
||||
for (const v of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) collectBindingNames(v.getNameNode(), local);
|
||||
for (const v of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration))
|
||||
collectBindingNames(v.getNameNode(), local);
|
||||
for (const f of body.getDescendantsOfKind(SyntaxKind.FunctionDeclaration)) {
|
||||
const n = f.getName();
|
||||
if (n) local.add(n);
|
||||
@@ -536,7 +538,7 @@ function asGuard(stmt: Statement): Expression | null {
|
||||
}
|
||||
|
||||
function nearestFunction(node: Node): Node | undefined {
|
||||
return node.getFirstAncestor((a) => isFunctionLike(a));
|
||||
return node.getFirstAncestor(a => isFunctionLike(a));
|
||||
}
|
||||
|
||||
/** Does an unlabeled/labeled `break`/`continue` target something outside `root`? */
|
||||
@@ -716,13 +718,16 @@ function analyze(fn: FunctionDeclaration, opts: Options): Candidate | null {
|
||||
const stmt = parent.getParent();
|
||||
if (!stmt || !Node.isExpressionStatement(stmt)) return skip(opts, name, "call result is used");
|
||||
const args = parent.getArguments();
|
||||
if (args.some((a) => Node.isSpreadElement(a))) return skip(opts, name, "call uses spread args");
|
||||
if (args.some(a => Node.isSpreadElement(a))) return skip(opts, name, "call uses spread args");
|
||||
if (args.length > params.length) return skip(opts, name, "call passes more args than params");
|
||||
callSites.push({ stmt, call: parent });
|
||||
}
|
||||
if (callSites.length === 0) return skip(opts, name, "no call sites");
|
||||
|
||||
const freeNames = computeFreeNames(fn, params.map((p) => p.name));
|
||||
const freeNames = computeFreeNames(
|
||||
fn,
|
||||
params.map(p => p.name),
|
||||
);
|
||||
for (const cs of callSites) {
|
||||
if (callSiteShadows(cs.call, freeNames)) return skip(opts, name, "would shadow a free identifier at a call site");
|
||||
}
|
||||
@@ -785,11 +790,7 @@ function reservedFor(call: CallExpression, reserved: Map<Node, Set<string>>): Se
|
||||
* land in the caller block — declarations nested in loops, blocks, or functions keep
|
||||
* their own scope and never collide, so they are left untouched.
|
||||
*/
|
||||
function renameCollidingLocals(
|
||||
candidate: Candidate,
|
||||
call: CallExpression,
|
||||
reserved: Map<Node, Set<string>>,
|
||||
): Edit[] {
|
||||
function renameCollidingLocals(candidate: Candidate, call: CallExpression, reserved: Map<Node, Set<string>>): Edit[] {
|
||||
const edits: Edit[] = [];
|
||||
const taken = reservedFor(call, reserved);
|
||||
const lo = candidate.tail[0].getStart();
|
||||
@@ -890,18 +891,18 @@ function buildReplacement(
|
||||
return {
|
||||
prefix,
|
||||
condition: combineGuards(candidate.guards, paramEdits),
|
||||
tail: candidate.tail.map((s) => subText(s, paramEdits)),
|
||||
tail: candidate.tail.map(s => subText(s, paramEdits)),
|
||||
};
|
||||
}
|
||||
|
||||
const allEdits = paramEdits.concat(renameCollidingLocals(candidate, call, reserved));
|
||||
return { prefix, condition: null, tail: candidate.tail.map((s) => subText(s, allEdits)) };
|
||||
return { prefix, condition: null, tail: candidate.tail.map(s => subText(s, allEdits)) };
|
||||
}
|
||||
|
||||
function applyReplacement(stmt: Statement, repl: Replacement): void {
|
||||
const parent = stmt.getParent();
|
||||
if (parent && Node.isStatemented(parent)) {
|
||||
const idx = parent.getStatements().findIndex((s) => s === stmt);
|
||||
const idx = parent.getStatements().indexOf(stmt);
|
||||
if (repl.condition === null) {
|
||||
const inserted = parent.insertStatements(idx, [...repl.prefix, ...repl.tail]);
|
||||
stmt.remove();
|
||||
@@ -934,7 +935,7 @@ function applyReplacement(stmt: Statement, repl: Replacement): void {
|
||||
function inlineCandidate(candidate: Candidate, strict: boolean): void {
|
||||
const reserved = new Map<Node, Set<string>>();
|
||||
// Build every replacement before mutating, so positions stay valid.
|
||||
const plans = candidate.callSites.map((cs) => ({
|
||||
const plans = candidate.callSites.map(cs => ({
|
||||
stmt: cs.stmt,
|
||||
repl: buildReplacement(candidate, cs.call, reserved, strict),
|
||||
}));
|
||||
|
||||
+7
-17
@@ -40,11 +40,10 @@ async function watchCI(): Promise<boolean> {
|
||||
|
||||
// Check job-level status for in-progress runs (fail fast on first job failure)
|
||||
const failedJobs: Array<{ workflow: string; job: string; jobId: number; conclusion: string }> = [];
|
||||
const inProgressRuns = runs.filter((r) => r.status === "in_progress" || r.status === "queued");
|
||||
const inProgressRuns = runs.filter(r => r.status === "in_progress" || r.status === "queued");
|
||||
|
||||
for (const run of inProgressRuns) {
|
||||
const jobsOutput =
|
||||
await $`gh run view ${run.databaseId} --json jobs`.quiet().nothrow().text();
|
||||
const jobsOutput = await $`gh run view ${run.databaseId} --json jobs`.quiet().nothrow().text();
|
||||
try {
|
||||
const { jobs } = JSON.parse(jobsOutput) as {
|
||||
jobs: Array<{ name: string; databaseId: number; status: string; conclusion: string | null }>;
|
||||
@@ -80,9 +79,9 @@ async function watchCI(): Promise<boolean> {
|
||||
}
|
||||
|
||||
// Check workflow-level status
|
||||
const pending = runs.filter((r) => r.status !== "completed");
|
||||
const failed = runs.filter((r) => r.status === "completed" && r.conclusion !== "success");
|
||||
const passed = runs.filter((r) => r.status === "completed" && r.conclusion === "success");
|
||||
const pending = runs.filter(r => r.status !== "completed");
|
||||
const failed = runs.filter(r => r.status === "completed" && r.conclusion !== "success");
|
||||
const passed = runs.filter(r => r.status === "completed" && r.conclusion === "success");
|
||||
|
||||
console.log(` ${passed.length} passed, ${pending.length} pending, ${failed.length} failed`);
|
||||
|
||||
@@ -257,10 +256,7 @@ async function cmdRelease(versionOrBump: string): Promise<void> {
|
||||
// Update @oh-my-pi/* catalog entries in root package.json
|
||||
console.log("Updating root catalog versions...");
|
||||
let rootPkgRaw = await Bun.file("package.json").text();
|
||||
rootPkgRaw = rootPkgRaw.replace(
|
||||
/("@oh-my-pi\/[^"]+":\s*)"[^"]+"/g,
|
||||
`$1"${version}"`,
|
||||
);
|
||||
rootPkgRaw = rootPkgRaw.replace(/("@oh-my-pi\/[^"]+":\s*)"[^"]+"/g, `$1"${version}"`);
|
||||
await Bun.write("package.json", rootPkgRaw);
|
||||
console.log(" Updated root catalog @oh-my-pi/* entries");
|
||||
|
||||
@@ -372,13 +368,7 @@ async function cmdRelease(versionOrBump: string): Promise<void> {
|
||||
const tagRef = `v${version}`;
|
||||
const sha = (await git(["rev-parse", "HEAD"]).text()).trim();
|
||||
await git(["tag", "-f", tagRef]);
|
||||
await git([
|
||||
"push",
|
||||
"--atomic",
|
||||
"origin",
|
||||
"refs/heads/main:refs/heads/main",
|
||||
`${sha}:refs/tags/${tagRef}`,
|
||||
]);
|
||||
await git(["push", "--atomic", "origin", "refs/heads/main:refs/heads/main", `${sha}:refs/tags/${tagRef}`]);
|
||||
console.log();
|
||||
|
||||
// 9. Watch CI
|
||||
|
||||
@@ -31,13 +31,22 @@
|
||||
|
||||
import * as path from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { type Api, AuthStorage, completeSimple, Effort, type Model, SqliteAuthCredentialStore, type Tool, type ToolCall } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
type Api,
|
||||
AuthStorage,
|
||||
completeSimple,
|
||||
Effort,
|
||||
type Model,
|
||||
SqliteAuthCredentialStore,
|
||||
type Tool,
|
||||
type ToolCall,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { type GeneratedProvider, getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
||||
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
changelogPaths,
|
||||
type ChangelogDocument,
|
||||
changelogPaths,
|
||||
type NumberedLine,
|
||||
parseChangelog,
|
||||
parseItems,
|
||||
@@ -143,12 +152,14 @@ interface RewrittenSection {
|
||||
}
|
||||
|
||||
const REWRITE_RESPONSE = z.object({
|
||||
sections: z.array(
|
||||
sections: z
|
||||
.array(
|
||||
z.object({
|
||||
category: z.enum(["Breaking Changes", "Added", "Changed", "Fixed", "Removed"]),
|
||||
items: z.array(z.string()),
|
||||
})
|
||||
).default([]),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
});
|
||||
|
||||
const REWRITE_PARAMETERS = {
|
||||
@@ -200,7 +211,10 @@ function validateRewrite(args: Record<string, unknown>): RewrittenSection[] {
|
||||
}
|
||||
|
||||
function normalizeRewriteItem(text: string): string[] {
|
||||
const lines = text.trim().split("\n").map(l => l.trimEnd());
|
||||
const lines = text
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(l => l.trimEnd());
|
||||
if (lines.length === 0) return [];
|
||||
const first = lines[0] ?? "";
|
||||
const content = first.startsWith("- ") ? first.slice(2) : first.startsWith("* ") ? first.slice(2) : first;
|
||||
@@ -212,7 +226,11 @@ function normalizeRewriteItem(text: string): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
async function requestRewrite(model: RewriteModel, packageName: string, unreleasedBody: string): Promise<RewrittenSection[]> {
|
||||
async function requestRewrite(
|
||||
model: RewriteModel,
|
||||
packageName: string,
|
||||
unreleasedBody: string,
|
||||
): Promise<RewrittenSection[]> {
|
||||
const userText = `Package: \`${packageName}\`
|
||||
|
||||
Original \`[Unreleased]\` section body:
|
||||
@@ -239,7 +257,9 @@ Consolidate and rewrite this content into user-visible release notes. Keep all p
|
||||
continue;
|
||||
}
|
||||
|
||||
const call = response.content.find((content): content is ToolCall => content.type === "toolCall" && content.name === "rewrite");
|
||||
const call = response.content.find(
|
||||
(content): content is ToolCall => content.type === "toolCall" && content.name === "rewrite",
|
||||
);
|
||||
if (!call) {
|
||||
lastError = "model returned no structured tool call";
|
||||
continue;
|
||||
@@ -248,7 +268,6 @@ Consolidate and rewrite this content into user-visible release notes. Keep all p
|
||||
return validateRewrite(call.arguments);
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error(`rewrite call failed for ${packageName}: ${lastError}`);
|
||||
@@ -278,11 +297,13 @@ interface RunResult {
|
||||
}
|
||||
|
||||
function applyRewrite(section: ReleaseSection, sections: RewrittenSection[]): void {
|
||||
section.subsections = sections.map(sec => {
|
||||
section.subsections = sections
|
||||
.map(sec => {
|
||||
const rawLines = sec.items.flatMap(normalizeRewriteItem);
|
||||
const lines: NumberedLine[] = rawLines.map(text => ({ text, lineNumber: 0 }));
|
||||
return { title: sec.category, lines };
|
||||
}).filter(sub => sub.lines.length > 0);
|
||||
})
|
||||
.filter(sub => sub.lines.length > 0);
|
||||
}
|
||||
|
||||
async function run(options: RunOptions): Promise<RunResult> {
|
||||
@@ -300,8 +321,6 @@ async function run(options: RunOptions): Promise<RunResult> {
|
||||
const i = pathIndex++;
|
||||
const changelogPath = paths[i];
|
||||
if (!changelogPath) continue;
|
||||
|
||||
try {
|
||||
const absolutePath = path.join(repoRoot, changelogPath);
|
||||
const content = await Bun.file(absolutePath).text();
|
||||
const document = parseChangelog(content);
|
||||
@@ -331,10 +350,6 @@ async function run(options: RunOptions): Promise<RunResult> {
|
||||
rewrittenCount,
|
||||
sections: rewritten,
|
||||
};
|
||||
} catch (error) {
|
||||
// Bubble errors from workers
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,4 +462,4 @@ if (import.meta.main) {
|
||||
await main();
|
||||
}
|
||||
|
||||
export { applyRewrite, collectEntries, run, type RunResult, unreleasedSection, validateRewrite };
|
||||
export { applyRewrite, collectEntries, type RunResult, run, unreleasedSection, validateRewrite };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Rewrite the natural-language prose of a prompt file into the terse
|
||||
* implementation-scratchpad voice (see `rewrite-system-prompt.style.md`),
|
||||
@@ -37,8 +38,8 @@
|
||||
* --dry-run classify + chunk, print a plan, make no network calls
|
||||
*/
|
||||
|
||||
import { parseArgs } from "node:util";
|
||||
import * as path from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import STYLE_GUIDE from "./rewrite-system-prompt.style.md" with { type: "text" };
|
||||
|
||||
const DEFAULT_INPUT = "packages/coding-agent/src/prompts/system/system-prompt.md";
|
||||
@@ -217,7 +218,7 @@ export function planRewrite(content: string): RewritePlan {
|
||||
if (isVerbatimLine(line)) continue;
|
||||
const { prefix, core, suffix } = peel(line);
|
||||
if (core.trim() === "") continue; // nothing rewritable after peeling
|
||||
const tokens = Array.from(core.matchAll(FRAGILE_RE), (m) => m[0]);
|
||||
const tokens = Array.from(core.matchAll(FRAGILE_RE), m => m[0]);
|
||||
prose.push({ lineIndex: i, prefix, suffix, core, tokens });
|
||||
}
|
||||
return { lines, prose };
|
||||
@@ -284,8 +285,8 @@ export async function rewriteAll(
|
||||
const groups = chunk(toRewrite, opts.chunkSize);
|
||||
|
||||
let done = 0;
|
||||
const resolved = await mapPool(groups, opts.concurrency, async (group) => {
|
||||
const items: RewriteItem[] = group.map((e) => ({
|
||||
const resolved = await mapPool(groups, opts.concurrency, async group => {
|
||||
const items: RewriteItem[] = group.map(e => ({
|
||||
id: e.lineIndex,
|
||||
text: e.core,
|
||||
tokens: e.tokens,
|
||||
@@ -296,7 +297,7 @@ export async function rewriteAll(
|
||||
} catch {
|
||||
map = new Map();
|
||||
}
|
||||
const out = group.map((entry) => {
|
||||
const out = group.map(entry => {
|
||||
const candidate = map.get(entry.lineIndex);
|
||||
return { entry, text: candidate ?? entry.core, ok: candidate != null };
|
||||
});
|
||||
@@ -392,7 +393,7 @@ const REWRITE_RESPONSE_FORMAT = {
|
||||
|
||||
/** Build a {@link RewriteChunk} backed by an OpenRouter chat-completions endpoint. */
|
||||
export function makeOpenRouterRewriter(opts: OpenRouterOptions): RewriteChunk {
|
||||
return async (items) => {
|
||||
return async items => {
|
||||
const result = new Map<number, string>();
|
||||
let pending = items;
|
||||
let lastErr: unknown;
|
||||
@@ -406,7 +407,7 @@ export function makeOpenRouterRewriter(opts: OpenRouterOptions): RewriteChunk {
|
||||
{ role: "system", content: opts.system },
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({ items: pending.map((p) => ({ id: p.id, text: p.text })) }),
|
||||
content: JSON.stringify({ items: pending.map(p => ({ id: p.id, text: p.text })) }),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -445,7 +446,7 @@ export function makeOpenRouterRewriter(opts: OpenRouterOptions): RewriteChunk {
|
||||
}
|
||||
if (pending.length > 0) {
|
||||
console.error(
|
||||
` ${pending.length} line(s) [${pending.map((p) => p.id).join(",")}] kept original: ${String(lastErr ?? "rewrite dropped a token")}`,
|
||||
` ${pending.length} line(s) [${pending.map(p => p.id).join(",")}] kept original: ${String(lastErr ?? "rewrite dropped a token")}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
|
||||
+3
-14
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun";
|
||||
import * as path from "node:path";
|
||||
import { $ } from "bun";
|
||||
|
||||
const RUST_AFFECTING_FILE_NAMES = [
|
||||
"Cargo.toml",
|
||||
@@ -21,16 +21,7 @@ const TASK_COMMANDS = {
|
||||
],
|
||||
"fix:rs": [
|
||||
["cargo", "fmt", "--all"],
|
||||
[
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--workspace",
|
||||
"--fix",
|
||||
"--allow-dirty",
|
||||
"--no-deps",
|
||||
"--allow-staged",
|
||||
"--allow-no-vcs",
|
||||
],
|
||||
["cargo", "clippy", "--workspace", "--fix", "--allow-dirty", "--no-deps", "--allow-staged", "--allow-no-vcs"],
|
||||
],
|
||||
"fmt:rs": [["cargo", "fmt", "--all"]],
|
||||
"lint:rs": [["cargo", "clippy", "--workspace", "--", "-D", "warnings"]],
|
||||
@@ -112,9 +103,7 @@ function isRustAffectingPath(changedPath: string): boolean {
|
||||
const normalized = changedPath.replace(/\\/g, "/");
|
||||
const fileName = normalized.slice(normalized.lastIndexOf("/") + 1);
|
||||
return (
|
||||
normalized.endsWith(".rs") ||
|
||||
normalized.startsWith(".cargo/") ||
|
||||
isOneOf(fileName, RUST_AFFECTING_FILE_NAMES)
|
||||
normalized.endsWith(".rs") || normalized.startsWith(".cargo/") || isOneOf(fileName, RUST_AFFECTING_FILE_NAMES)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,10 @@ it("scanFile recovers usage, turns, spawns, residency, and pruned result sizes",
|
||||
},
|
||||
}),
|
||||
// second user turn + final answer with no tools
|
||||
JSON.stringify({ type: "message", message: { role: "user", content: "thanks, also re-read it", timestamp: 4000 } }),
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: { role: "user", content: "thanks, also re-read it", timestamp: 4000 },
|
||||
}),
|
||||
asst({
|
||||
ts: 5000,
|
||||
usage: { input: 400, output: 40, cacheRead: 600, cost: 2.0 },
|
||||
|
||||
@@ -472,7 +472,9 @@ export async function scanFile(filePath: string): Promise<FileScan | undefined>
|
||||
const name = call?.name ?? (msg.toolName as string) ?? "?";
|
||||
const textBlob = contentText(msg.content);
|
||||
const truncated = TRUNCATED_RESULT_RE.exec(textBlob);
|
||||
const toks = truncated ? Math.max(estTokens(textBlob), Number.parseInt(truncated[1], 10)) : estTokens(textBlob);
|
||||
const toks = truncated
|
||||
? Math.max(estTokens(textBlob), Number.parseInt(truncated[1], 10))
|
||||
: estTokens(textBlob);
|
||||
const agg = tool(name);
|
||||
agg.resultToks += toks;
|
||||
if (msg.isError === true) {
|
||||
@@ -571,7 +573,7 @@ async function discoverGroups(opts: CliOptions): Promise<DiscoveredGroup[]> {
|
||||
}
|
||||
}
|
||||
for (const [id, main] of mains) {
|
||||
let childPaths: string[] = [];
|
||||
const childPaths: string[] = [];
|
||||
let mtime = main.mtime;
|
||||
if (subdirs.has(id)) {
|
||||
const dirPath = path.join(folderPath, id);
|
||||
@@ -616,7 +618,11 @@ async function scanGroup(d: DiscoveredGroup): Promise<SessionGroup | undefined>
|
||||
}
|
||||
|
||||
/** Run `fn` over `items` with bounded concurrency, preserving order. */
|
||||
async function mapPool<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
|
||||
async function mapPool<T, R>(
|
||||
items: readonly T[],
|
||||
limit: number,
|
||||
fn: (item: T, index: number) => Promise<R>,
|
||||
): Promise<R[]> {
|
||||
const out = new Array<R>(items.length);
|
||||
let next = 0;
|
||||
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||||
@@ -756,7 +762,8 @@ function buildDigest(g: SessionGroup): string {
|
||||
.slice(0, 8);
|
||||
if (repeats.length) {
|
||||
lines.push(`\n## Repeated reads of the same file (waste signal)`);
|
||||
for (const [p, r] of repeats) lines.push(`${p} ×${r.count} (~${fmtTok(r.toks)}tok total, ~${fmtTok(r.residency)} residency)`);
|
||||
for (const [p, r] of repeats)
|
||||
lines.push(`${p} ×${r.count} (~${fmtTok(r.toks)}tok total, ~${fmtTok(r.residency)} residency)`);
|
||||
}
|
||||
|
||||
if (m.topResults.length) {
|
||||
@@ -894,7 +901,16 @@ const SESSION_SCHEMA = {
|
||||
},
|
||||
headline: { type: "string", description: "one-sentence takeaway for this session" },
|
||||
},
|
||||
required: ["score", "multiTopic", "topics", "shouldHaveSplit", "handoffOpportunities", "spawnVerdicts", "waste", "headline"],
|
||||
required: [
|
||||
"score",
|
||||
"multiTopic",
|
||||
"topics",
|
||||
"shouldHaveSplit",
|
||||
"handoffOpportunities",
|
||||
"spawnVerdicts",
|
||||
"waste",
|
||||
"headline",
|
||||
],
|
||||
} as const;
|
||||
|
||||
interface AggregateFindings {
|
||||
@@ -1215,9 +1231,12 @@ function printVerdicts(res: AuditResult): void {
|
||||
for (const g of ordered) {
|
||||
const v = res.verdicts.get(g.id);
|
||||
if (!v) continue;
|
||||
console.log(`\n[${fmtMoney(g.usage.cost)}] "${clip(g.main.title ?? g.id, 60)}" (${g.folder}) — score ${v.score}/10`);
|
||||
console.log(
|
||||
`\n[${fmtMoney(g.usage.cost)}] "${clip(g.main.title ?? g.id, 60)}" (${g.folder}) — score ${v.score}/10`,
|
||||
);
|
||||
console.log(` ${v.headline}`);
|
||||
if (v.multiTopic) console.log(` topics: ${v.topics.join(" | ")}${v.shouldHaveSplit ? " → should have split" : ""}`);
|
||||
if (v.multiTopic)
|
||||
console.log(` topics: ${v.topics.join(" | ")}${v.shouldHaveSplit ? " → should have split" : ""}`);
|
||||
for (const h of v.handoffOpportunities) console.log(` handoff: ${h}`);
|
||||
for (const s of v.spawnVerdicts) {
|
||||
if (s.verdict === "good") continue;
|
||||
@@ -1319,7 +1338,9 @@ async function main(): Promise<void> {
|
||||
const sessionFilter = opts.session?.toLowerCase();
|
||||
const matched = sessionFilter
|
||||
? groups.filter(
|
||||
g => g.id.toLowerCase().includes(sessionFilter) || (g.main.title ?? "").toLowerCase().includes(sessionFilter),
|
||||
g =>
|
||||
g.id.toLowerCase().includes(sessionFilter) ||
|
||||
(g.main.title ?? "").toLowerCase().includes(sessionFilter),
|
||||
)
|
||||
: groups.filter(g => g.usage.cost >= opts.minCost);
|
||||
const candidates = matched.sort((a, b) => b.usage.cost - a.usage.cost).slice(0, opts.maxLlm);
|
||||
@@ -1365,7 +1386,9 @@ async function main(): Promise<void> {
|
||||
addUsage(res.classifierUsage, usage);
|
||||
process.stderr.write(` ✓ ${clip(g.main.title ?? g.id, 50)} (score ${verdict.score})\n`);
|
||||
} catch (err) {
|
||||
process.stderr.write(` ✗ ${clip(g.main.title ?? g.id, 50)}: ${err instanceof Error ? err.message : err}\n`);
|
||||
process.stderr.write(
|
||||
` ✗ ${clip(g.main.title ?? g.id, 50)}: ${err instanceof Error ? err.message : err}\n`,
|
||||
);
|
||||
}
|
||||
});
|
||||
await saveVerdictCache(cache);
|
||||
@@ -1402,7 +1425,9 @@ async function main(): Promise<void> {
|
||||
headline: v.headline,
|
||||
topics: v.topics,
|
||||
shouldHaveSplit: v.shouldHaveSplit,
|
||||
spawnIssues: v.spawnVerdicts.filter(s => s.verdict !== "good").map(s => ({ label: s.label, verdict: s.verdict })),
|
||||
spawnIssues: v.spawnVerdicts
|
||||
.filter(s => s.verdict !== "good")
|
||||
.map(s => ({ label: s.label, verdict: s.verdict })),
|
||||
waste: v.waste.map(w => ({ source: w.source, estTokens: w.estTokens, estUsd: round2(w.estUsd) })),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -105,7 +105,12 @@ function parseArgs(argv: readonly string[]): Options {
|
||||
opts.workflow = argv[++i];
|
||||
break;
|
||||
case "--only":
|
||||
opts.only = new Set((argv[++i] ?? "").split(",").map(s => s.trim()).filter(Boolean));
|
||||
opts.only = new Set(
|
||||
(argv[++i] ?? "")
|
||||
.split(",")
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
@@ -291,7 +296,10 @@ function placeholderReadme(name: string, target: NativeLeafTarget): string {
|
||||
async function publishNativeLeafPlaceholder(name: string, target: NativeLeafTarget, repo: string): Promise<boolean> {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-native-placeholder-"));
|
||||
try {
|
||||
await Bun.write(path.join(tmpDir, "package.json"), `${JSON.stringify(placeholderManifest(name, target, repo), null, "\t")}\n`);
|
||||
await Bun.write(
|
||||
path.join(tmpDir, "package.json"),
|
||||
`${JSON.stringify(placeholderManifest(name, target, repo), null, "\t")}\n`,
|
||||
);
|
||||
await Bun.write(path.join(tmpDir, "README.md"), placeholderReadme(name, target));
|
||||
return (await npmInteractive(["publish", tmpDir, "--access", "public"])) === 0;
|
||||
} finally {
|
||||
@@ -336,7 +344,9 @@ async function main(): Promise<void> {
|
||||
const workflow = opts.workflow;
|
||||
|
||||
if (!(await Bun.file(path.join(repoRoot, ".github", "workflows", workflow)).exists())) {
|
||||
console.warn(`Warning: .github/workflows/${workflow} not found; npm will still accept it, but OIDC won't match a non-existent workflow.`);
|
||||
console.warn(
|
||||
`Warning: .github/workflows/${workflow} not found; npm will still accept it, but OIDC won't match a non-existent workflow.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.dryRun) {
|
||||
@@ -369,7 +379,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
console.log("The first mutating npm operation triggers 2FA. When prompted, complete it and choose");
|
||||
console.log("'skip 2FA for the next 5 minutes' on the npm site so placeholder publishes and trust setup run unattended.\n");
|
||||
console.log(
|
||||
"'skip 2FA for the next 5 minutes' on the npm site so placeholder publishes and trust setup run unattended.\n",
|
||||
);
|
||||
|
||||
const outcomes = new Map<string, Outcome>();
|
||||
let bootstrapped = 0;
|
||||
|
||||
@@ -12,7 +12,7 @@ const INDEX_FILE = join(THEMES_DIR, "index.ts");
|
||||
|
||||
async function main() {
|
||||
const files = await readdir(THEMES_DIR);
|
||||
const jsonFiles = files.filter((f) => f.endsWith(".json")).sort();
|
||||
const jsonFiles = files.filter(f => f.endsWith(".json")).sort();
|
||||
|
||||
const imports: string[] = [];
|
||||
const exportEntries: string[] = [];
|
||||
|
||||
@@ -22,8 +22,8 @@ interface PackageInfo {
|
||||
|
||||
const packagesDir = join(process.cwd(), "packages");
|
||||
const packageDirs = readdirSync(packagesDir, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
// Read all package.json files and build version map
|
||||
const packages: Record<string, PackageInfo> = {};
|
||||
@@ -61,7 +61,8 @@ console.log("\n✅ All packages at same version (lockstep)");
|
||||
|
||||
// Update all inter-package dependencies
|
||||
let totalUpdates = 0;
|
||||
for (const [dir, pkg] of Object.entries(packages)) {
|
||||
for (const dir in packages) {
|
||||
const pkg = packages[dir];
|
||||
let updated = false;
|
||||
|
||||
// Check dependencies
|
||||
@@ -98,7 +99,7 @@ for (const [dir, pkg] of Object.entries(packages)) {
|
||||
|
||||
// Write if updated
|
||||
if (updated) {
|
||||
await Bun.write(pkg.path, JSON.stringify(pkg.data, null, "\t") + "\n");
|
||||
await Bun.write(pkg.path, `${JSON.stringify(pkg.data, null, "\t")}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ function parseEncoding(value: string | undefined): Encoding {
|
||||
const normalized = value.toLowerCase().replace(/-/g, "_");
|
||||
if (normalized === "o200k" || normalized === "o200k_base") return Encoding.O200kBase;
|
||||
if (normalized === "cl100k" || normalized === "cl100k_base") return Encoding.Cl100kBase;
|
||||
throw new Error(`Unknown encoding \"${value}\". Expected o200k_base or cl100k_base.`);
|
||||
throw new Error(`Unknown encoding "${value}". Expected o200k_base or cl100k_base.`);
|
||||
}
|
||||
|
||||
function parseCli(): CliOptions | null {
|
||||
@@ -116,7 +116,9 @@ function relativePath(filePath: string): string {
|
||||
|
||||
async function collectPromptPaths(positionals: readonly string[]): Promise<string[]> {
|
||||
if (positionals.length === 0) {
|
||||
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: TOOL_PROMPT_DIR, absolute: true, onlyFiles: true }));
|
||||
const files = await Array.fromAsync(
|
||||
new Bun.Glob("*.md").scan({ cwd: TOOL_PROMPT_DIR, absolute: true, onlyFiles: true }),
|
||||
);
|
||||
return files.sort((a, b) => relativePath(a).localeCompare(relativePath(b)));
|
||||
}
|
||||
|
||||
@@ -202,8 +204,6 @@ function renderContext(): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function estimatePrompt(filePath: string, encoding: Encoding): Promise<PromptEstimate> {
|
||||
const template = await Bun.file(filePath).text();
|
||||
const rendered = prompt.render(template, renderContext());
|
||||
@@ -221,7 +221,11 @@ function printTable(estimates: PromptEstimate[], encoding: Encoding): void {
|
||||
const totalTokens = rows.reduce((sum, row) => sum + row.tokens, 0);
|
||||
const totalChars = rows.reduce((sum, row) => sum + row.chars, 0);
|
||||
const totalLines = rows.reduce((sum, row) => sum + row.lines, 0);
|
||||
const tokenWidth = Math.max("tokens".length, String(totalTokens).length, ...rows.map(row => String(row.tokens).length));
|
||||
const tokenWidth = Math.max(
|
||||
"tokens".length,
|
||||
String(totalTokens).length,
|
||||
...rows.map(row => String(row.tokens).length),
|
||||
);
|
||||
const charWidth = Math.max("chars".length, String(totalChars).length, ...rows.map(row => String(row.chars).length));
|
||||
const lineWidth = Math.max("lines".length, String(totalLines).length, ...rows.map(row => String(row.lines).length));
|
||||
|
||||
@@ -237,7 +241,9 @@ function printTable(estimates: PromptEstimate[], encoding: Encoding): void {
|
||||
);
|
||||
}
|
||||
console.log(`${"-".repeat(tokenWidth)} ${"-".repeat(charWidth)} ${"-".repeat(lineWidth)} ${"-".repeat(6)}`);
|
||||
console.log(`${String(totalTokens).padStart(tokenWidth)} ${String(totalChars).padStart(charWidth)} ${String(totalLines).padStart(lineWidth)} TOTAL (${rows.length} prompts)`);
|
||||
console.log(
|
||||
`${String(totalTokens).padStart(tokenWidth)} ${String(totalChars).padStart(charWidth)} ${String(totalLines).padStart(lineWidth)} TOTAL (${rows.length} prompts)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
|
||||
@@ -10,7 +10,7 @@ Bun.plugin({
|
||||
name: "trace-loader",
|
||||
setup(build) {
|
||||
// Trace module resolution (doesn't interfere with loading)
|
||||
build.onResolve({ filter: /.*/ }, (args) => {
|
||||
build.onResolve({ filter: /.*/ }, args => {
|
||||
// Skip if already traced this path
|
||||
if (resolved.has(args.path)) {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user