chore(release): reconcile v3.8.50 provenance and notes

This commit is contained in:
diegosouzapw
2026-08-23 12:03:51 -03:00
committed by Xiangzhe
parent 931fd8b2bc
commit 6dfa0bb921
516 changed files with 5661 additions and 10813 deletions

View File

@@ -63,7 +63,7 @@ export function verifyFrozenMap(before = {}, after = {}, label = "frozen") {
if (typeof prev !== "number") {
// note key
if (!has) problems.push(`${label}: note "${key}" was deleted — notes must be preserved`);
else if (after[key] !== prev)
else if (!jsonEqual(after[key], prev))
problems.push(`${label}: note "${key}" was rewritten — notes must be preserved verbatim`);
continue;
}

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env node
/**
* @file extract-credentials.mjs
* @description Print Raycast Pro credentials from local macOS install (redacted preview).
*
* Usage: node scripts/raycast/extract-credentials.mjs
*
* @changes
* - [2026-07-27] [Composer] - CLI credential extractor for local Raycast
*/
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
const RAYCAST_SALT = "yvkwWXzxPPBAqY2tmaKrB*DvYjjMaeEf";
const RAYCAST_SUPPORT = join(homedir(), "Library", "Application Support", "com.raycast.macos");
const RAYCAST_DB = join(RAYCAST_SUPPORT, "raycast-enc.sqlite");
function redact(s, keep = 8) {
if (!s || s.length <= keep * 2) return "***";
return `${s.slice(0, keep)}${s.slice(-4)}`;
}
function readKeychain(account) {
return JSON.parse(
execFileSync("security", ["find-generic-password", "-s", "Raycast", "-a", account, "-w"], {
encoding: "utf-8",
}).trim()
);
}
function dbPassphrase() {
const keyHex = execFileSync(
"security",
["find-generic-password", "-s", "Raycast", "-a", "database_key", "-w"],
{ encoding: "utf-8" }
).trim();
return createHash("sha256")
.update(keyHex + RAYCAST_SALT)
.digest("hex");
}
function queryDb(sql) {
const tmpDir = mkdtempSync(join(tmpdir(), "raycast-extract-"));
const tmpDb = join(tmpDir, "db.sqlite");
copyFileSync(RAYCAST_DB, tmpDb);
for (const ext of ["-wal", "-shm"]) {
const src = RAYCAST_DB + ext;
if (existsSync(src)) copyFileSync(src, tmpDb + ext);
}
const passphrase = dbPassphrase();
const input = `PRAGMA key = '${passphrase}';\n.mode json\n${sql}`;
const out = execFileSync("sqlcipher", [tmpDb], { input, encoding: "utf-8" });
for (const ext of ["", "-wal", "-shm"]) {
try {
unlinkSync(tmpDb + ext);
} catch {}
}
try {
rmdirSync(tmpDir);
} catch {}
const jsonStr = out.startsWith("ok\n") ? out.slice(3) : out;
return JSON.parse(jsonStr.trim() || "[]");
}
if (process.platform !== "darwin") {
console.error("macOS only");
process.exit(1);
}
const store = readKeychain("raycast-store_credentials");
const token = store?.oauth?.access_token;
if (!token) {
console.error("No Raycast bearer token in Keychain — open Raycast and sign in");
process.exit(1);
}
const users = queryDb("SELECT analyticsId, email, username, hasProFeatures, hasBetterAI FROM user LIMIT 1;");
const user = users[0] || {};
const deviceId =
user.analyticsId ||
JSON.parse(readFileSync(join(RAYCAST_SUPPORT, "posthog.distinctId"), "utf-8"))["posthog.distinctId"];
console.log(JSON.stringify({
accessTokenPreview: redact(token),
accessToken: token,
deviceId,
aid: deviceId,
email: user.email || store?.user?.email,
username: user.username || store?.user?.username,
hasProFeatures: !!user.hasProFeatures,
hasBetterAI: !!user.hasBetterAI,
sources: {
bearer: "Keychain Raycast / raycast-store_credentials",
deviceId: "raycast-enc.sqlite user.analyticsId",
},
}, null, 2));

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env node
/**
* @file usage-benchmark.mjs
* @description Battle-test Raycast Pro usage via OmniRoute local endpoint.
*
* Env (required):
* OMNIROUTE_URL default http://127.0.0.1:20128/v1
* OMNIROUTE_API_KEY OmniRoute API key (if REQUIRE_API_KEY)
*
* Env (optional — direct Raycast probe without OmniRoute):
* RAYCAST_BEARER_TOKEN
* RAYCAST_DEVICE_ID
* RAYCAST_AID
* RAYCAST_SIG_SECRET
*
* Usage:
* node scripts/raycast/usage-benchmark.mjs --models 5 --rounds 3
* node scripts/raycast/usage-benchmark.mjs --model openai-gpt-5-mini --rounds 10
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast Pro usage benchmark script
*/
import { createHmac, createHash } from "node:crypto";
const args = process.argv.slice(2);
function arg(name, fallback) {
const i = args.indexOf(`--${name}`);
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
}
const rounds = Number(arg("rounds", "3"));
const model = arg("model", "");
const modelCount = Number(arg("models", "5"));
const omnirouteUrl = (process.env.OMNIROUTE_URL || "http://127.0.0.1:20128/v1").replace(/\/$/, "");
const apiKey = process.env.OMNIROUTE_API_KEY || "";
const RAYCAST_CHAT_URL = "https://backend.raycast.com/api/v1/ai/chat_completions";
const RAYCAST_MODELS_URL = "https://backend.raycast.com/api/v1/ai/models";
const SIG_SECRET =
process.env.RAYCAST_SIG_SECRET ||
"6bc455473576ce2cd6f70426caff867aabbe3f7291c1a79681af5e8ce0ca1408";
function rot13rot5(input) {
return input.replace(/[A-Za-z0-9]/g, (char) => {
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65 + 13) % 26) + 65);
if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97 + 13) % 26) + 97);
return String.fromCharCode(((code - 48 + 5) % 10) + 48);
});
}
function signatureV2(timestamp, deviceId, payload, secret) {
const bodyHash = createHash("sha256").update(payload).digest("hex");
const message = [timestamp, deviceId, bodyHash].map(rot13rot5).join(".");
return createHmac("sha256", secret).update(message).digest("hex");
}
function raycastJwt(aid, secret) {
const iat = Date.now() / 1000;
const header = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString("base64url");
const payload = Buffer.from(JSON.stringify({ aid, exp: iat + 60, iat })).toString("base64url");
const signature = createHmac("sha256", secret)
.update(`${header}.${payload}`)
.digest("base64url");
return `${header}.${payload}.${signature}`;
}
function raycastHeaders(payload) {
const bearerToken = process.env.RAYCAST_BEARER_TOKEN;
const deviceId = process.env.RAYCAST_DEVICE_ID;
const aid = process.env.RAYCAST_AID;
if (!bearerToken || !deviceId || !aid) {
throw new Error("Set RAYCAST_BEARER_TOKEN, RAYCAST_DEVICE_ID, RAYCAST_AID for direct probe");
}
const timestamp = Math.floor(Date.now() / 1000).toString();
return {
Accept: "application/json",
Authorization: `Bearer ${bearerToken}`,
"X-Raycast-Timestamp": timestamp,
"X-Raycast-DeviceId": deviceId,
"Content-Type": "application/json",
"X-Raycast-Signature-v2": signatureV2(timestamp, deviceId, payload, SIG_SECRET),
"X-Raycast-Signature": raycastJwt(aid, SIG_SECRET),
"X-Raycast-Experimental": "chatBranching, mcpHTTPServer",
"User-Agent": "Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))",
};
}
async function fetchRaycastModels() {
const payload = "{}";
const res = await fetch(RAYCAST_MODELS_URL, { method: "GET", headers: raycastHeaders(payload) });
const text = await res.text();
if (!res.ok) throw new Error(`models [${res.status}]: ${text.slice(0, 200)}`);
const data = JSON.parse(text);
return (data.models || []).map((m) => m.id);
}
async function chatOmniroute(modelId, prompt) {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
const started = Date.now();
const res = await fetch(`${omnirouteUrl}/chat/completions`, {
method: "POST",
headers,
body: JSON.stringify({
model: `raycast/${modelId}`,
messages: [{ role: "user", content: prompt }],
stream: false,
max_tokens: 32,
}),
});
const ms = Date.now() - started;
const body = await res.text();
return { ok: res.ok, status: res.status, ms, body: body.slice(0, 300) };
}
async function main() {
console.log(`OmniRoute: ${omnirouteUrl}`);
console.log(`Rounds per model: ${rounds}`);
let models = [];
if (model) {
models = [model];
} else if (process.env.RAYCAST_BEARER_TOKEN) {
models = (await fetchRaycastModels()).slice(0, modelCount);
console.log(`Direct Raycast model probe — testing ${models.length} models via OmniRoute`);
} else {
models = ["openai-gpt-5-mini"];
console.log("No RAYCAST_* env — using default model openai-gpt-5-mini via OmniRoute combo id");
}
const results = [];
for (const modelId of models) {
let ok = 0;
let fail = 0;
const latencies = [];
for (let i = 0; i < rounds; i++) {
const prompt = `Raycast benchmark round ${i + 1} — reply with exactly: pong`;
try {
const r = await chatOmniroute(modelId, prompt);
latencies.push(r.ms);
if (r.ok) ok++;
else {
fail++;
console.error(` FAIL ${modelId} #${i + 1} [${r.status}]: ${r.body}`);
}
} catch (err) {
fail++;
console.error(` ERR ${modelId} #${i + 1}:`, err.message);
}
}
const avg = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : 0;
results.push({ modelId, ok, fail, avgMs: avg });
console.log(`${modelId}: ${ok}/${rounds} ok, avg ${avg}ms`);
}
console.log("\nSummary:");
console.table(results);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -20,7 +20,8 @@
// allowed). Credit format stays the repo norm: "(#PR — thanks @user)".
//
// Usage:
// node scripts/release/aggregate-changelog.mjs [--dry-run]
// node scripts/release/aggregate-changelog.mjs --version <version> [--dry-run]
// --version exact CHANGELOG release section to update (for example, 3.8.50);
// --dry-run print the would-be CHANGELOG.md to stdout and list fragments;
// touch nothing.
//
@@ -44,6 +45,12 @@ export const SECTIONS = Object.freeze({
const SKIP_FILES = new Set(["README.md", ".gitkeep"]);
function assertTargetVersion(version) {
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
throw new Error('target version is required (for example, { version: "3.8.50" })');
}
}
/**
* Validate one fragment's text. Returns null when OK, or a human-readable error.
* Pure — unit-tested.
@@ -57,6 +64,15 @@ export function validateFragmentText(text) {
return 'fragment must start with a markdown bullet ("- ")';
}
if (/^(<{7}|={7}|>{7})/m.test(body)) return "fragment contains merge-conflict markers";
if (/#(?:PRNUM|PENDING)\b|\/pull\/(?:PRNUM|PENDING)(?:[/?#)]|$)/i.test(body)) {
return "fragment contains an unresolved PR placeholder";
}
for (const match of body.matchAll(
/\[([^\]\n]+)\]\(https:\/\/github\.com\/diegosouzapw\/OmniRoute\/pull\/(\d+)\/?(?:[?#][^)]*)?\)/g
)) {
const expected = `#${match[2]}`;
if (match[1].trim() !== expected) return `pull link label must be "${expected}"`;
}
return null;
}
@@ -88,27 +104,72 @@ export function collectFragments(root) {
}
/**
* Append bullets at the END of a living-section heading's bullet block (before the
* next "##"/"###" heading). Operates on the FIRST occurrence of the heading — in this
* repo's CHANGELOG the living cycle section always appears first. Pure — unit-tested.
* Throws when a needed heading is missing (the release captain adds the heading; the
* script never invents structure).
* Append bullets at the END of a target version's section-heading blocks. Pure — unit-tested.
* The version is mandatory because Unreleased and released sections intentionally reuse the
* same headings.
*/
export function insertBullets(changelogText, bulletsBySection) {
export function insertBullets(changelogText, bulletsBySection, { version } = {}) {
assertTargetVersion(version);
let lines = changelogText.split("\n");
const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const targetHeading = new RegExp(`^## \\[${escapedVersion}\\](?:\\s|$)`);
const targetMatches = lines.flatMap((line, index) => (targetHeading.test(line) ? [index] : []));
if (targetMatches.length === 0) {
throw new Error(`target version [${version}] not found in CHANGELOG.md`);
}
if (targetMatches.length > 1) {
throw new Error(`target version [${version}] appears ${targetMatches.length} times`);
}
const targetStart = targetMatches[0];
let targetEnd = lines.findIndex((line, index) => index > targetStart && /^##\s/.test(line));
if (targetEnd === -1) targetEnd = lines.length;
const insertions = [];
const targetBody = `\n${lines
.slice(targetStart + 1, targetEnd)
.join("\n")
.trimEnd()}\n`;
const seenFragmentText = new Map();
for (const [section, heading] of Object.entries(SECTIONS)) {
const bullets = (bulletsBySection[section] || []).map((b) => b.text ?? b);
const entries = bulletsBySection[section] || [];
const bullets = entries.map((entry) => {
const text = String(entry.text ?? entry).trimEnd();
const file = entry.file || `${section} fragment`;
const firstFile = seenFragmentText.get(text);
if (firstFile) {
throw new Error(`duplicate fragment content in ${firstFile} and ${file}`);
}
seenFragmentText.set(text, file);
if (targetBody.includes(`\n${text}\n`)) {
throw new Error(`fragment content is already present in [${version}]: ${file}`);
}
return text;
});
if (bullets.length === 0) continue;
const headIdx = lines.findIndex((l) => l.trim() === heading);
if (headIdx === -1) {
const headingMatches = lines.flatMap((line, index) =>
index > targetStart && index < targetEnd && line.trim() === heading ? [index] : []
);
if (headingMatches.length === 0) {
throw new Error(
`heading "${heading}" not found in CHANGELOG.md — add it to the living section before aggregating ${section} fragments`
`heading "${heading}" not found inside target version [${version}] before aggregating ${section} fragments`
);
}
if (headingMatches.length > 1) {
throw new Error(
`heading "${heading}" appears ${headingMatches.length} times inside target version [${version}]`
);
}
insertions.push({ headIdx: headingMatches[0], bullets });
}
// Work from the bottom up so earlier insertions cannot invalidate later section indexes.
for (const { headIdx, bullets } of insertions.sort((a, b) => b.headIdx - a.headIdx)) {
// End of this section's block: last non-empty line before the next heading.
let nextHead = lines.length;
for (let i = headIdx + 1; i < lines.length; i++) {
if (/^##/.test(lines[i])) {
let nextHead = targetEnd;
for (let i = headIdx + 1; i < targetEnd; i++) {
if (/^#{2,3}\s/.test(lines[i])) {
nextHead = i;
break;
}
@@ -125,7 +186,8 @@ export function insertBullets(changelogText, bulletsBySection) {
* Aggregate fragments into CHANGELOG.md. Returns a summary object. When dryRun is
* true nothing is written or deleted.
*/
export function aggregate({ root = ROOT, dryRun = false } = {}) {
export function aggregate({ root = ROOT, version, dryRun = false } = {}) {
assertTargetVersion(version);
const collected = collectFragments(root);
if (collected.invalid.length > 0) {
const detail = collected.invalid.map((i) => `${i.file}: ${i.error}`).join("\n");
@@ -134,7 +196,7 @@ export function aggregate({ root = ROOT, dryRun = false } = {}) {
const total = collected.features.length + collected.fixes.length + collected.maintenance.length;
const changelogPath = join(root, "CHANGELOG.md");
const before = readFileSync(changelogPath, "utf8");
const after = total === 0 ? before : insertBullets(before, collected);
const after = insertBullets(before, collected, { version });
if (!dryRun && total > 0) {
writeFileSync(changelogPath, after);
for (const section of Object.keys(SECTIONS)) {
@@ -144,24 +206,67 @@ export function aggregate({ root = ROOT, dryRun = false } = {}) {
return { total, collected, changed: total > 0, after };
}
function main() {
const dryRun = process.argv.includes("--dry-run");
const result = aggregate({ dryRun });
function parseCliArgs(argv) {
let version;
let dryRun = false;
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--dry-run") {
dryRun = true;
continue;
}
if (arg === "--version") {
if (version !== undefined) throw new Error("--version may only be provided once");
const value = argv[++i];
if (!value || value.startsWith("--")) {
throw new Error("--version <version> is required");
}
version = value;
continue;
}
throw new Error(`unknown argument: ${arg}`);
}
if (!version) throw new Error("--version <version> is required");
return { version, dryRun };
}
export function main(
argv = process.argv.slice(2),
{ root = ROOT, stdout = process.stdout, stderr = process.stderr } = {}
) {
let args;
let result;
try {
args = parseCliArgs(argv);
result = aggregate({ root, ...args });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
stderr.write(`[aggregate-changelog] error: ${message}\n`);
return 2;
}
const log = args.dryRun ? stderr : stdout;
if (args.dryRun) {
stdout.write(result.after);
if (!result.after.endsWith("\n")) stdout.write("\n");
}
if (result.total === 0) {
console.log("[aggregate-changelog] no fragments to aggregate — nothing to do.");
log.write("[aggregate-changelog] no fragments to aggregate — nothing to do.\n");
return 0;
}
for (const section of Object.keys(SECTIONS)) {
for (const { file } of result.collected[section]) {
console.log(`[aggregate-changelog] ${dryRun ? "would aggregate" : "aggregated"} ${file}`);
log.write(
`[aggregate-changelog] ${args.dryRun ? "would aggregate" : "aggregated"} ${file}\n`
);
}
}
console.log(
`[aggregate-changelog] ${result.total} fragment(s) → CHANGELOG.md${dryRun ? " (dry-run, nothing written)" : " (fragments deleted — commit CHANGELOG.md + deletions together)"}`
log.write(
`[aggregate-changelog] ${result.total} fragment(s) → CHANGELOG.md${args.dryRun ? " (dry-run, nothing written)" : " (fragments deleted — commit CHANGELOG.md + deletions together)"}\n`
);
return 0;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
process.exit(main());
process.exitCode = main();
}

View File

@@ -65,7 +65,10 @@ export function parseContributors(sectionText) {
if (!agg.has(handle)) agg.set(handle, new Set());
for (const r of refs) agg.get(handle).add(r);
};
const handlesIn = (s) => [...s.matchAll(/@([A-Za-z0-9_-]+)/g)].map((m) => m[1]);
// A slash is a contributor separator only when another @handle follows it. This prevents
// GitHub App identities such as `@app/dependabot` from being truncated and credited as `@app`.
const handlesIn = (s) =>
[...s.matchAll(/@([A-Za-z0-9_-]+)(?=$|[\s,;:.)\]}>]|\/\s*@)/g)].map((m) => m[1]);
const refsIn = (s) => [...s.matchAll(/#(\d+)/g)].map((m) => Number(m[1]));
for (const raw of sectionText.split("\n")) {
@@ -98,7 +101,9 @@ export function parseContributors(sectionText) {
}
// (3) "Extracted from #N by @X" (links already collapsed by the preprocessing above)
for (const em of line.matchAll(/[Ee]xtracted from #(\d+)\s+by\s+@([A-Za-z0-9_-]+)/g)) {
for (const em of line.matchAll(
/[Ee]xtracted from #(\d+)\s+by\s+@([A-Za-z0-9_-]+)(?![A-Za-z0-9_/-])/g
)) {
add(em[2], [Number(em[1])]);
}
}