chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
196
third_party/pi/packages/coding-agent/src/utils/changelog.ts
vendored
Normal file
196
third_party/pi/packages/coding-agent/src/utils/changelog.ts
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
import path from "node:path";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
|
||||
export interface ChangelogEntry {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const GITHUB_REPO = "earendil-works/pi";
|
||||
const CHANGELOG_LINK_BASE_PATH = "packages/coding-agent";
|
||||
const LEGACY_REPO_RE = /^https:\/\/github\.com\/(?:badlogic|earendil-works)\/pi-mono(?=\/|$)/;
|
||||
const URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
||||
const INLINE_MARKDOWN_LINK_RE = /(!?\[[^\]\n]+\]\()([^\s)]+)((?:\s+[^)]*)?\))/g;
|
||||
|
||||
function entryVersion(entry: ChangelogEntry): string {
|
||||
return `${entry.major}.${entry.minor}.${entry.patch}`;
|
||||
}
|
||||
|
||||
function normalizeTag(version: string | ChangelogEntry): string {
|
||||
const versionString = typeof version === "string" ? version : entryVersion(version);
|
||||
return versionString.startsWith("v") ? versionString : `v${versionString}`;
|
||||
}
|
||||
|
||||
function splitLocalTarget(target: string): { fragment: string; pathPart: string; query: string } {
|
||||
const hashIndex = target.indexOf("#");
|
||||
const beforeHash = hashIndex === -1 ? target : target.slice(0, hashIndex);
|
||||
const fragment = hashIndex === -1 ? "" : target.slice(hashIndex);
|
||||
const queryIndex = beforeHash.indexOf("?");
|
||||
|
||||
if (queryIndex === -1) {
|
||||
return { fragment, pathPart: beforeHash, query: "" };
|
||||
}
|
||||
|
||||
return {
|
||||
fragment,
|
||||
pathPart: beforeHash.slice(0, queryIndex),
|
||||
query: beforeHash.slice(queryIndex),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePathPart(value: string): string {
|
||||
return value.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function resolveRepositoryPath(targetPath: string): string | undefined {
|
||||
const normalizedTarget = normalizePathPart(targetPath);
|
||||
const joined = normalizedTarget.startsWith("/")
|
||||
? path.posix.normalize(normalizedTarget.replace(/^\/+/, ""))
|
||||
: path.posix.normalize(path.posix.join(CHANGELOG_LINK_BASE_PATH, normalizedTarget));
|
||||
|
||||
if (joined === "." || joined.startsWith("../") || joined === "..") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return joined;
|
||||
}
|
||||
|
||||
function isDirectoryTarget(originalPath: string, repositoryPath: string): boolean {
|
||||
if (originalPath.endsWith("/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const basename = path.posix.basename(repositoryPath);
|
||||
return !basename.includes(".");
|
||||
}
|
||||
|
||||
function normalizeChangelogLinkTarget(target: string, tag: string): string {
|
||||
let canonicalTarget = target.replace(LEGACY_REPO_RE, `https://github.com/${GITHUB_REPO}`);
|
||||
const repoUrl = `https://github.com/${GITHUB_REPO}`;
|
||||
|
||||
for (const route of ["blob", "tree"]) {
|
||||
for (const branch of ["main", "master"]) {
|
||||
const floatingRefPrefix = `${repoUrl}/${route}/${branch}/`;
|
||||
if (canonicalTarget.startsWith(floatingRefPrefix)) {
|
||||
canonicalTarget = `${repoUrl}/${route}/${tag}/${canonicalTarget.slice(floatingRefPrefix.length)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canonicalTarget.startsWith("#") || canonicalTarget.startsWith("//") || URL_SCHEME_RE.test(canonicalTarget)) {
|
||||
return canonicalTarget;
|
||||
}
|
||||
|
||||
const { fragment, pathPart, query } = splitLocalTarget(canonicalTarget);
|
||||
if (!pathPart) {
|
||||
return canonicalTarget;
|
||||
}
|
||||
|
||||
const repositoryPath = resolveRepositoryPath(pathPart);
|
||||
if (!repositoryPath) {
|
||||
return canonicalTarget;
|
||||
}
|
||||
|
||||
const route = isDirectoryTarget(pathPart, repositoryPath) ? "tree" : "blob";
|
||||
return `https://github.com/${GITHUB_REPO}/${route}/${tag}/${encodeURI(repositoryPath)}${query}${fragment}`;
|
||||
}
|
||||
|
||||
export function normalizeChangelogLinks(markdown: string, version: string | ChangelogEntry): string {
|
||||
const tag = normalizeTag(version);
|
||||
return markdown.replace(INLINE_MARKDOWN_LINK_RE, (_match, prefix, target, suffix) => {
|
||||
return `${prefix}${normalizeChangelogLinkTarget(target, tag)}${suffix}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse changelog entries from CHANGELOG.md
|
||||
* Scans for ## lines and collects content until next ## or EOF
|
||||
*/
|
||||
export function parseChangelog(changelogPath: string): ChangelogEntry[] {
|
||||
if (!existsSync(changelogPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(changelogPath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const entries: ChangelogEntry[] = [];
|
||||
|
||||
let currentLines: string[] = [];
|
||||
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// Check if this is a version header (## [x.y.z] ...)
|
||||
if (line.startsWith("## ")) {
|
||||
// Save previous entry if exists
|
||||
if (currentVersion && currentLines.length > 0) {
|
||||
entries.push({
|
||||
...currentVersion,
|
||||
content: currentLines.join("\n").trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// Try to parse version from this line
|
||||
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
||||
if (versionMatch) {
|
||||
currentVersion = {
|
||||
major: Number.parseInt(versionMatch[1], 10),
|
||||
minor: Number.parseInt(versionMatch[2], 10),
|
||||
patch: Number.parseInt(versionMatch[3], 10),
|
||||
};
|
||||
currentLines = [line];
|
||||
} else {
|
||||
// Reset if we can't parse version
|
||||
currentVersion = null;
|
||||
currentLines = [];
|
||||
}
|
||||
} else if (currentVersion) {
|
||||
// Collect lines for current version
|
||||
currentLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last entry
|
||||
if (currentVersion && currentLines.length > 0) {
|
||||
entries.push({
|
||||
...currentVersion,
|
||||
content: currentLines.join("\n").trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
} catch (error) {
|
||||
console.error(`Warning: Could not parse changelog: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
||||
*/
|
||||
export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number {
|
||||
if (v1.major !== v2.major) return v1.major - v2.major;
|
||||
if (v1.minor !== v2.minor) return v1.minor - v2.minor;
|
||||
return v1.patch - v2.patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries newer than lastVersion
|
||||
*/
|
||||
export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] {
|
||||
// Parse lastVersion
|
||||
const parts = lastVersion.split(".").map(Number);
|
||||
const last: ChangelogEntry = {
|
||||
major: parts[0] || 0,
|
||||
minor: parts[1] || 0,
|
||||
patch: parts[2] || 0,
|
||||
content: "",
|
||||
};
|
||||
|
||||
return entries.filter((entry) => compareVersions(entry, last) > 0);
|
||||
}
|
||||
|
||||
// Re-export getChangelogPath from paths.ts for convenience
|
||||
export { getChangelogPath } from "../config.ts";
|
||||
Reference in New Issue
Block a user