chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# Analyze quotio repository for interesting patterns and features
# Usage: ./Scripts/analyze_quotio.sh [feature-area]
set -euo pipefail
AREA=${1:-all}
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}==> Fetching latest quotio...${NC}"
git fetch quotio 2>/dev/null || {
echo -e "${YELLOW}Adding quotio remote...${NC}"
git remote add quotio https://github.com/nguyenphutrong/quotio.git
git fetch quotio
}
remote_default_branch() {
local remote=$1
local branch=""
local candidate
branch=$(git symbolic-ref -q --short "refs/remotes/${remote}/HEAD" 2>/dev/null | sed "s#^${remote}/##" || true)
if [ -z "$branch" ]; then
branch=$(git remote show "$remote" 2>/dev/null | awk '/HEAD branch/ {print $NF; exit}' || true)
fi
if [ -n "$branch" ] && git rev-parse --verify -q "${remote}/${branch}" >/dev/null; then
echo "$branch"
return 0
fi
for candidate in main master; do
if git rev-parse --verify -q "${remote}/${candidate}" >/dev/null; then
echo "$candidate"
return 0
fi
done
echo -e "${RED}Error: Could not resolve default branch for remote '$remote'.${NC}" >&2
exit 1
}
QUOTIO_BRANCH=$(remote_default_branch quotio)
QUOTIO_REF="quotio/${QUOTIO_BRANCH}"
echo ""
echo -e "${GREEN}==> Quotio Repository Analysis (${QUOTIO_REF})${NC}"
echo ""
# Show recent activity
echo -e "${BLUE}Recent Activity (last 30 days):${NC}"
git log --oneline --graph "$QUOTIO_REF" --since="30 days ago" | head -20 || true
echo ""
# Analyze file structure
echo -e "${BLUE}File Structure:${NC}"
git ls-tree -r --name-only "$QUOTIO_REF" | grep -E '\.(swift|md)$' | head -30 || true
echo ""
# Find interesting patterns based on area
case $AREA in
"providers"|"all")
echo -e "${BLUE}Provider Implementations:${NC}"
git ls-tree -r --name-only "$QUOTIO_REF" | grep -i provider | head -20 || true
echo ""
;;
esac
case $AREA in
"ui"|"all")
echo -e "${BLUE}UI Components:${NC}"
git ls-tree -r --name-only "$QUOTIO_REF" | grep -iE '(view|ui|menu)' | head -20 || true
echo ""
;;
esac
case $AREA in
"auth"|"all")
echo -e "${BLUE}Authentication/Session:${NC}"
git ls-tree -r --name-only "$QUOTIO_REF" | grep -iE '(auth|session|cookie|login)' | head -20 || true
echo ""
;;
esac
# Show commit messages for pattern analysis
echo -e "${BLUE}Recent Commit Messages (for pattern analysis):${NC}"
git log --oneline "$QUOTIO_REF" --since="60 days ago" | head -30 || true
echo ""
# Create analysis report
REPORT_FILE="quotio-analysis-$(date +%Y%m%d).md"
cat > "$REPORT_FILE" << EOF
# Quotio Analysis Report
**Date:** $(date +%Y-%m-%d)
**Purpose:** Identify patterns and features for CodexBar fork inspiration
**Source ref:** \`$QUOTIO_REF\`
## Recent Activity
\`\`\`
$(git log --oneline --graph "$QUOTIO_REF" --since="30 days ago" | head -20 || true)
\`\`\`
## File Structure
\`\`\`
$(git ls-tree -r --name-only "$QUOTIO_REF" | grep -E '\.(swift|md)$' | head -50 || true)
\`\`\`
## Recent Commits
\`\`\`
$(git log --oneline "$QUOTIO_REF" --since="60 days ago" | head -30 || true)
\`\`\`
## Areas of Interest
### Providers
- [ ] Review provider implementations
- [ ] Compare with CodexBar approach
- [ ] Identify improvements
### UI/UX
- [ ] Menu bar organization
- [ ] Settings layout
- [ ] Status indicators
### Authentication
- [ ] Session management
- [ ] Cookie handling
- [ ] OAuth flows
### Multi-Account
- [ ] Account switching
- [ ] Account storage
- [ ] UI patterns
## Action Items
- [ ] Review specific files of interest
- [ ] Document patterns (not code)
- [ ] Create implementation plan
- [ ] Implement independently
## Notes
Remember: We're looking for PATTERNS and IDEAS, not copying code.
All implementations must be original and follow CodexBar conventions.
EOF
echo -e "${GREEN}Analysis report saved to: $REPORT_FILE${NC}"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo ""
echo "1. View specific files:"
echo " ${GREEN}git show $QUOTIO_REF:path/to/file${NC}"
echo ""
echo "2. Compare implementations:"
echo " ${GREEN}git diff main $QUOTIO_REF -- path/to/similar/file${NC}"
echo ""
echo "3. Review commit details:"
echo " ${GREEN}git log -p $QUOTIO_REF --since='30 days ago'${NC}"
echo ""
echo "4. Document patterns in:"
echo " ${GREEN}docs/QUOTIO_ANALYSIS.md${NC}"
echo ""
echo -e "${BLUE}Remember: Adapt patterns, don't copy code!${NC}"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
npx --yes tailwindcss@3.4.19 \
--config Scripts/tailwind.site.config.cjs \
--input Scripts/site-tailwind.input.css \
--output docs/site-utilities.css \
--minify
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
ICON_FILE=${1:-Icon.icon}
BASENAME=${2:-Icon}
OUT_ROOT=${3:-build/icon}
XCODE_APP=${XCODE_APP:-/Applications/Xcode.app}
ICTOOL="$XCODE_APP/Contents/Applications/Icon Composer.app/Contents/Executables/ictool"
if [[ ! -x "$ICTOOL" ]]; then
ICTOOL="$XCODE_APP/Contents/Applications/Icon Composer.app/Contents/Executables/icontool"
fi
if [[ ! -x "$ICTOOL" ]]; then
echo "ictool/icontool not found. Set XCODE_APP if Xcode is elsewhere." >&2
exit 1
fi
ICONSET_DIR="$OUT_ROOT/${BASENAME}.iconset"
TMP_DIR="$OUT_ROOT/tmp"
mkdir -p "$ICONSET_DIR" "$TMP_DIR"
MASTER_ART="$TMP_DIR/icon_art_824.png"
MASTER_1024="$TMP_DIR/icon_1024.png"
# Render inner art (no margin) with macOS Default appearance
"$ICTOOL" "$ICON_FILE" \
--export-preview macOS Default 824 824 1 -45 "$MASTER_ART"
# Pad to 1024x1024 with transparent border
sips --padToHeightWidth 1024 1024 "$MASTER_ART" --out "$MASTER_1024" >/dev/null
# Generate required sizes
sizes=(16 32 64 128 256 512 1024)
for sz in "${sizes[@]}"; do
out="$ICONSET_DIR/icon_${sz}x${sz}.png"
sips -z "$sz" "$sz" "$MASTER_1024" --out "$out" >/dev/null
if [[ "$sz" -ne 1024 ]]; then
dbl=$((sz*2))
out2="$ICONSET_DIR/icon_${sz}x${sz}@2x.png"
sips -z "$dbl" "$dbl" "$MASTER_1024" --out "$out2" >/dev/null
fi
done
# 512x512@2x already covered by 1024; ensure it exists
cp "$MASTER_1024" "$ICONSET_DIR/icon_512x512@2x.png"
iconutil -c icns "$ICONSET_DIR" -o Icon.icns
echo "Icon.icns generated at $(pwd)/Icon.icns"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
exec "$SCRIPT_DIR/mac-release" changelog-html "$@"
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const resources = path.join(repoRoot, "Sources/CodexBar/Resources");
const english = readCatalog("en");
const englishKeys = Object.keys(english).sort();
const strictLocales = ["ar", "ca", "fa", "th"];
const languageKeys = ["language_arabic", "language_persian", "language_thai"];
const isTest = process.argv.includes("--test");
function readCatalog(locale) {
const file = path.join(resources, `${locale}.lproj/Localizable.strings`);
if (!fs.existsSync(file)) return null;
const output = execFileSync("plutil", ["-convert", "json", "-o", "-", file], { encoding: "utf8" });
return JSON.parse(output);
}
function tokenSignature(value) {
// Exclude explicit `%%`, which does not consume an argument.
const withoutEscapedPercents = value.replace(/%%/g, "");
const printfRaw = withoutEscapedPercents.match(/%(?:\d+\$)?(?:\.\d+)?(?:@|d|f)/g) ?? [];
const printf = {};
let implicitIndex = 1;
for (const token of printfRaw) {
const match = token.match(/%(\d+)\$.*?([@df])/);
if (match) {
printf[Number.parseInt(match[1], 10)] = match[2];
} else {
printf[implicitIndex] = token.at(-1);
implicitIndex += 1;
}
}
return { printf, swift: swiftInterpolationTokens(value).sort() };
}
function formatKeyList(keys, limit = 12) {
const shown = keys.slice(0, limit).join(", ");
const remaining = keys.length - limit;
return remaining > 0 ? `${shown}, ... +${remaining} more` : shown;
}
function blankKeys(catalog, referenceKeys) {
return referenceKeys.filter((key) => Object.hasOwn(catalog, key) && !catalog[key]?.trim());
}
function swiftInterpolationTokens(value) {
const tokens = [];
for (let index = 0; index < value.length - 1; index += 1) {
if (value[index] !== "\\" || value[index + 1] !== "(") continue;
const start = index;
let depth = 1;
index += 2;
while (index < value.length && depth > 0) {
if (value[index] === "(") depth += 1;
if (value[index] === ")") depth -= 1;
index += 1;
}
tokens.push(value.slice(start, index));
index -= 1;
}
return tokens;
}
if (isTest) {
assertEqual(tokenSignature("%1$@ · %2$d"), tokenSignature("%2$d · %1$@"), "positional reorder");
assertNotEqual(tokenSignature("%1$@ · %2$d"), tokenSignature("%1$d · %2$@"), "positional type swap");
assertEqual(tokenSignature("%.0f%% used"), tokenSignature("%.0f%% verbraucht"), "escaped percent");
assertNotEqual(tokenSignature("\\(name): \\(usage)"), tokenSignature("\\(name): \\(value)"), "Swift tokens");
assertEqual(
tokenSignature("\\(self.store.metadata(for: self.provider).displayName) failed"),
tokenSignature("Fehler: \\(self.store.metadata(for: self.provider).displayName)"),
"nested Swift interpolation");
assertNotEqual(
tokenSignature("\\(self.store.metadata(for: self.provider).displayName) failed"),
tokenSignature("\\(self.store.metadata(for: self.provider) failed"),
"truncated Swift interpolation");
assertEqual(formatKeyList(["alpha", "beta"]), "alpha, beta", "short key list");
assertEqual(
formatKeyList(["alpha", "beta", "gamma", "delta"], 2),
"alpha, beta, ... +2 more",
"truncated key list");
assertEqual(
blankKeys({ alpha: "", beta: " ", gamma: "ok" }, ["alpha", "beta", "gamma", "delta"]),
["alpha", "beta"],
"blank keys");
console.log("app locale checker tests OK");
process.exit(0);
}
let hasErrors = false;
let checkedCount = 0;
for (const strictLocale of strictLocales) {
const dirPath = path.join(resources, `${strictLocale}.lproj`);
if (!fs.existsSync(dirPath)) {
console.error(`\x1b[31mError: Required strict locale catalog is completely missing: ${strictLocale}.lproj\x1b[0m`);
hasErrors = true;
}
}
for (const directory of fs.readdirSync(resources).filter((name) => name.endsWith(".lproj"))) {
const locale = directory.replace(/\.lproj$/, "");
if (locale === "en" || locale === "Base") continue;
const catalog = readCatalog(locale);
if (!catalog) continue;
checkedCount++;
const catalogKeys = Object.keys(catalog);
const emptyKeys = blankKeys(catalog, englishKeys);
// 1. Missing keys
const missingKeys = englishKeys.filter((key) => !catalogKeys.includes(key));
if (missingKeys.length > 0) {
const missingKeyList = formatKeyList(missingKeys);
if (strictLocales.includes(locale)) {
console.error(
`\x1b[31m[${locale}] Error: Missing ${missingKeys.length} keys in strict locale: ${missingKeyList}.\x1b[0m`);
hasErrors = true;
} else {
console.warn(`\x1b[33m[${locale}] Warning: Missing ${missingKeys.length} keys: ${missingKeyList}.\x1b[0m`);
}
}
const extraKeys = catalogKeys.filter((key) => !englishKeys.includes(key));
if (strictLocales.includes(locale) && extraKeys.length > 0) {
console.error(`\x1b[31m[${locale}] Error: Found ${extraKeys.length} extra keys in strict locale.\x1b[0m`);
hasErrors = true;
}
// Ensure critical language keys are present in ALL locales
for (const key of languageKeys) {
if (!catalog[key] || !catalog[key].trim()) {
console.error(`\x1b[31m[${locale}] Error: Missing critical language key "${key}".\x1b[0m`);
hasErrors = true;
}
}
if (emptyKeys.length > 0) {
console.error(
`\x1b[31m[${locale}] Error: Blank values for ${emptyKeys.length} keys: ${formatKeyList(emptyKeys)}.\x1b[0m`);
hasErrors = true;
}
// 2. Identical values count
let identicalCount = 0;
for (const key of englishKeys) {
if (!catalog[key]?.trim()) {
continue;
}
if (catalog[key] === english[key]) {
identicalCount++;
}
// 3. Format placeholder mismatch
const tEn = tokenSignature(english[key]);
const tLoc = tokenSignature(catalog[key]);
if (JSON.stringify(tEn) !== JSON.stringify(tLoc)) {
console.error(`\x1b[31m[${locale}] Error: Token mismatch for key "${key}"\x1b[0m`);
console.error(` en: ${english[key]} Tokens: ${JSON.stringify(tEn)}`);
console.error(` ${locale}: ${catalog[key]} Tokens: ${JSON.stringify(tLoc)}`);
hasErrors = true;
}
}
// Warn if identical translation count exceeds 15% of the total keys (approx > 150 out of 1050)
const identicalRatio = identicalCount / englishKeys.length;
if (identicalRatio > 0.15) {
console.warn(`\x1b[33m[${locale}] Warning: High number of identical translations: ${identicalCount}/${englishKeys.length} (${(identicalRatio * 100).toFixed(1)}%)\x1b[0m`);
}
}
if (hasErrors) {
console.error("\n\x1b[31mApp locale checks failed.\x1b[0m");
process.exit(1);
}
console.log(`\n\x1b[32mApp locales OK: Checked ${checkedCount} catalogs against ${englishKeys.length} English keys.\x1b[0m`);
function assertEqual(actual, expected, label) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}
function assertNotEqual(actual, expected, label) {
if (JSON.stringify(actual) === JSON.stringify(expected)) {
throw new Error(`${label}: signatures unexpectedly match`);
}
}
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const approvedRootDocumentation = new Set([
"README.md",
"CHANGELOG.md",
"LICENSE",
"VISION.md",
].map((relativePath) => path.join(repoRoot, relativePath)));
const readme = readText("README.md");
const readmeLinks = [
...markdownLinks(readme),
...markdownImageLinks(readme),
...htmlLinks(readme),
].filter(isRepositoryDocReference);
assert(readmeLinks.length > 0, "README.md has no local documentation links");
for (const link of readmeLinks) validateLocalDocLink(link, repoRoot, "README.md");
const providerLinks = inlineCodeDocLinks(readText("docs/providers.md"));
assert(providerLinks.length > 0, "docs/providers.md has no provider detail links");
for (const link of providerLinks) validateLocalDocLink(link, repoRoot, "docs/providers.md");
const docsLinks = markdownFiles("docs").flatMap((relativePath) => {
const markdown = readText(relativePath);
const links = [
...markdownLinks(markdown),
...markdownImageLinks(markdown),
...htmlLinks(markdown),
].filter(isLocalDocumentationReference);
return links.map((link) => ({ link, relativePath }));
});
for (const { link, relativePath } of docsLinks) {
validateLocalDocLink(link, path.join(repoRoot, path.dirname(relativePath)), relativePath);
}
console.log(
`documentation links OK: ${readmeLinks.length + providerLinks.length + docsLinks.length} local links`,
);
function readText(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
}
function markdownLinks(markdown) {
const source = markdownTextOutsideCode(markdown);
const links = [];
const inlinePattern = /(?<!!)\[(?:\\.|[^\]\\])+\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+(?:"[^"\n]*"|'[^'\n]*'|\([^)\n]*\)))?\s*\)/g;
for (const match of source.matchAll(inlinePattern)) {
links.push(encodeSpaces(match[1] ?? match[2]));
}
const referencePattern = /^\s*\[[^\]\n]+]:\s*(?:<([^>\n]+)>|([^\s]+))/gm;
for (const match of source.matchAll(referencePattern)) {
links.push(encodeSpaces(match[1] ?? match[2]));
}
return links;
}
function markdownImageLinks(markdown) {
const source = markdownTextOutsideCode(markdown);
const pattern = /!\[(?:\\.|[^\]\\])*\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+(?:"[^"\n]*"|'[^'\n]*'|\([^)\n]*\)))?\s*\)/g;
return [...source.matchAll(pattern)].map((match) => match[1] ?? match[2]);
}
function htmlLinks(markdown) {
const source = markdownTextOutsideCode(markdown);
const pattern = /<\s*(?:a|img)\b[^>]*?\b(?:href|src)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi;
return [...source.matchAll(pattern)].map((match) => match[1] ?? match[2] ?? match[3]);
}
function inlineCodeDocLinks(markdown) {
return markdown.split("\n").flatMap((line) => {
const trimmed = line.trim();
const prefix = "- Details: `";
if (!trimmed.startsWith(prefix)) return [];
const rest = trimmed.slice(prefix.length);
const end = rest.indexOf("`");
return end === -1 ? [] : [rest.slice(0, end)];
});
}
function validateLocalDocLink(rawLink, baseDirectory, sourceLabel) {
const sourcePath = path.join(repoRoot, sourceLabel);
const { absolutePath, fragment } = localDocPath(rawLink, baseDirectory, sourcePath);
assert(fs.existsSync(absolutePath), `${sourceLabel}: missing documentation target: ${rawLink}`);
if (path.extname(absolutePath).toLowerCase() !== ".md" || !fragment) return;
const anchors = markdownHeadingAnchors(readText(path.relative(repoRoot, absolutePath)));
assert(anchors.has(fragment), `${sourceLabel}: missing documentation anchor: ${rawLink}`);
}
function isRepositoryDocReference(rawLink) {
const parsed = parseRelativeURL(rawLink);
if (!parsed || parsed.protocol || parsed.host) return false;
let pathname = parsed.pathname;
while (pathname.startsWith("./")) pathname = pathname.slice(2);
return pathname === "docs" || pathname.startsWith("docs/");
}
function isLocalDocumentationReference(rawLink) {
const parsed = parseRelativeURL(rawLink);
if (!parsed || parsed.protocol || parsed.host) return false;
return Boolean(parsed.pathname || parsed.hash);
}
function localDocPath(rawLink, baseDirectory, sourcePath) {
const parsed = parseRelativeURL(rawLink);
assert(
parsed && !parsed.protocol && !parsed.host && (parsed.pathname || parsed.hash),
`invalid documentation URL: ${rawLink}`,
);
const rawPath = rawLink.split("#", 1)[0].split("?", 1)[0];
const decodedPath = decodeURIComponent(rawPath);
const absolutePath = decodedPath ? path.resolve(baseDirectory, decodedPath) : sourcePath;
const docsRoot = path.resolve(repoRoot, "docs");
const isInDocsTree = absolutePath === docsRoot || absolutePath.startsWith(`${docsRoot}${path.sep}`);
assert(
isInDocsTree || approvedRootDocumentation.has(absolutePath),
`documentation link escapes approved documentation roots: ${rawLink}`,
);
return { absolutePath, fragment: parsed.hash ? decodeURIComponent(parsed.hash.slice(1)) : "" };
}
function markdownFiles(relativeDir) {
const dir = path.join(repoRoot, relativeDir);
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
if (entry.name.startsWith(".") || entry.name === "node_modules") return [];
const relativePath = path.join(relativeDir, entry.name);
if (entry.isDirectory()) return markdownFiles(relativePath);
return entry.isFile() && entry.name.endsWith(".md") ? [relativePath] : [];
}).sort((a, b) => a.localeCompare(b));
}
function parseRelativeURL(rawLink) {
try {
const parsed = new URL(rawLink, "relative://repo/");
const isRelative = parsed.protocol === "relative:" && parsed.host === "repo";
return {
protocol: isRelative ? "" : parsed.protocol,
host: isRelative ? "" : parsed.host,
pathname: isRelative ? parsed.pathname.replace(/^\//, "") : parsed.pathname,
hash: parsed.hash,
};
} catch {
return null;
}
}
function markdownHeadingAnchors(markdown) {
const occurrences = new Map();
const anchors = new Set();
const source = markdownTextOutsideFencedCode(markdown);
for (const line of source.split("\n")) {
const trimmed = line.replace(/^[ \t]+/, "");
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(trimmed);
if (!match) continue;
const base = markdownHeadingSlug(match[2]);
if (!base) continue;
const occurrence = occurrences.get(base) ?? 0;
anchors.add(occurrence === 0 ? base : `${base}-${occurrence}`);
occurrences.set(base, occurrence + 1);
}
return anchors;
}
function markdownHeadingSlug(heading) {
const text = removeMarkdownFormatting(heading).toLowerCase();
let slug = "";
for (const char of text) {
if (/[\p{Letter}\p{Number}_-]/u.test(char)) {
slug += char;
} else if (/\s/u.test(char)) {
slug += "-";
}
}
return slug;
}
function removeMarkdownFormatting(text) {
return text
.replace(/`([^`]*)`/g, "$1")
.replace(/\[([^\]]+)]\([^)]+\)/g, "$1")
.replace(/[*_~]/g, "");
}
function markdownTextOutsideCode(markdown) {
return markdownTextOutsideFencedCode(markdown)
.split("\n")
.map(removeInlineCode)
.join("\n");
}
function markdownTextOutsideFencedCode(markdown) {
let fence = null;
return markdown.split("\n").map((line) => {
if (fence) {
if (isClosingFence(line, fence.marker, fence.count)) fence = null;
return "";
}
const openingFence = parseOpeningFence(line);
if (openingFence) {
fence = openingFence;
return "";
}
return line;
}).join("\n");
}
function parseOpeningFence(line) {
const match = /^( {0,3})([`~]{3,})(.*)$/.exec(line);
if (!match) return null;
const marker = match[2][0];
if (marker === "`" && match[3].includes("`")) return null;
return { marker, count: match[2].length };
}
function isClosingFence(line, marker, minimumCount) {
const escaped = marker === "`" ? "`" : "~";
const pattern = new RegExp(`^ {0,3}${escaped}{${minimumCount},}\\s*$`);
return pattern.test(line);
}
function removeInlineCode(line) {
return line.replace(/(?<!`)(`+)(?!`)(.*?)(?<!`)\1(?!`)/g, "");
}
function encodeSpaces(value) {
return value.replaceAll(" ", "%20");
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
exec "$SCRIPT_DIR/mac-release" check-assets "$@"
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { localeCatalog, localeMessages } from "../docs/site-locales.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const indexHtml = fs.readFileSync(path.join(repoRoot, "docs/index.html"), "utf8");
const providerSource = fs.readFileSync(
path.join(repoRoot, "Sources/CodexBarCore/Providers/Providers.swift"),
"utf8",
);
const providerEnumBody = providerSource.match(
/public enum UsageProvider:[^{]+\{([\s\S]*?)\n\}/,
)?.[1];
assert(providerEnumBody, "could not locate UsageProvider cases");
const providerIDs = [...providerEnumBody.matchAll(/^\s*case\s+(\w+)\s*$/gm)].map((match) => match[1]);
assert(providerIDs.length > 0, "UsageProvider must define at least one provider");
assertEqual(new Set(providerIDs).size, providerIDs.length, "UsageProvider IDs");
const providerCount = providerIDs.length;
const publicCountFiles = [
["README.md", `alt="CodexBar — every AI coding limit in your menu bar. ${providerCount} providers."`],
["docs/providers.md", `CodexBar currently registers ${providerCount} provider IDs.`],
["docs/social.html", `<strong>${providerCount} providers</strong>`],
["docs/llms.txt", `across ${providerCount} providers`],
];
for (const [relativePath, expectedText] of publicCountFiles) {
const contents = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
assert(contents.includes(expectedText), `${relativePath} must advertise ${providerCount} providers`);
}
assert(indexHtml.includes(`across ${providerCount} providers`), `index metadata must advertise ${providerCount} providers`);
assert(
indexHtml.includes(`across ${providerCount} AI coding providers`),
`index social metadata must advertise ${providerCount} providers`,
);
assert(
indexHtml.includes(`>${providerCount} providers,{mobileBreak}one menu bar</span>`),
`index provider heading must advertise ${providerCount} providers`,
);
assert(!indexHtml.includes("cdn.tailwindcss.com"), "site must not load Tailwind from a runtime CDN");
for (const match of indexHtml.matchAll(/<link rel="stylesheet" href="\.\/([^"?]+)(?:\?[^"']*)?"/g)) {
assert(fs.existsSync(path.join(repoRoot, "docs", match[1])), `missing local stylesheet ${match[1]}`);
}
const expectedCodes = [
"en", "zh-CN", "zh-TW", "ja-JP", "es", "pt-BR", "ko", "de", "fr", "ar", "it",
"vi", "nl", "tr", "uk", "ru", "id", "pl", "fa", "th", "gl", "ca", "sv",
];
const catalogCodes = localeCatalog.map((locale) => locale.code);
const appLanguageSource = fs.readFileSync(
path.join(repoRoot, "Sources/CodexBar/PreferencesGeneralPane.swift"),
"utf8",
);
assertEqual(catalogCodes, expectedCodes, "locale catalog");
assertEqual(
localeCatalog.filter((locale) => locale.direction === "rtl").map((locale) => locale.code),
["ar", "fa"],
"RTL locale catalog");
const appCatalogCodes = [...appLanguageSource.matchAll(/case \w+ = "([^"]+)"/g)]
.map((match) => match[1])
.filter(Boolean)
.map((code) => ({ "zh-Hans": "zh-CN", "zh-Hant": "zh-TW", ja: "ja-JP" })[code] ?? code);
assertEqual(appCatalogCodes, expectedCodes, "app language catalog");
const englishKeys = Object.keys(localeMessages.en).sort();
for (const locale of localeCatalog) {
const messages = localeMessages[locale.code];
assert(messages, `missing messages for ${locale.code}`);
assertEqual(Object.keys(messages).sort(), englishKeys, `${locale.code} message keys`);
for (const key of ["meta.description", "meta.ogDescription", "providers.title"]) {
const counts = [...messages[key].matchAll(/\d+/g)].map(Number);
assertEqual(counts[0], providerCount, `${locale.code}.${key} provider count`);
}
for (const key of englishKeys) {
assert(messages[key].trim(), `${locale.code}.${key} is blank`);
assertEqual(tokens(messages[key]), tokens(localeMessages.en[key]), `${locale.code}.${key} tokens`);
}
}
const referencedKeys = new Set();
for (const match of indexHtml.matchAll(/data-i18n(?:-rich|-aria-label|-title|-alt)?="([^"]+)"/g)) {
referencedKeys.add(match[1]);
}
for (const key of referencedKeys) {
assert(englishKeys.includes(key), `index.html references unknown locale key ${key}`);
}
const siteJs = fs.readFileSync(path.join(repoRoot, 'docs/site.js'), 'utf8');
const hasLanguagePicker = indexHtml.includes('id="language-picker-list"')
&& (indexHtml.includes('localeCatalog') || siteJs.includes('localeCatalog'));
assert(hasLanguagePicker, 'site must include the language picker backed by localeCatalog');
for (const code of catalogCodes) {
assert(indexHtml.includes(`href="https://codexbar.app/?lang=${code}"`), `missing hreflang URL for ${code}`);
}
const providerCards = [...indexHtml.matchAll(/<li class="provider-card"([^>]*)>([\s\S]*?)<\/li>/g)];
for (const [, attrs, body] of providerCards) {
if (!attrs.includes('hidden')) {
assert(body.includes('class="provider-card-link"'), 'provider cards must link to provider documentation');
assert(body.includes('class="provider-logo'), 'provider cards must use logo assets');
for (const match of body.matchAll(/src="\.\/([^"]+)"/g)) {
assert(fs.existsSync(path.join(repoRoot, 'docs', match[1])), `missing provider logo asset ${match[1]}`);
}
}
}
console.log(`app/site locales OK: ${catalogCodes.length} locales, ${englishKeys.length} site messages`);
function tokens(value) {
return [...value.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]).sort();
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function assertEqual(actual, expected, label) {
const actualJSON = JSON.stringify(actual);
const expectedJSON = JSON.stringify(expected);
if (actualJSON !== expectedJSON) {
throw new Error(`${label}: expected ${expectedJSON}, got ${actualJSON}`);
}
}
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MAX_BYTES=$((2 * 1024 * 1024))
failures=0
tracked_files=0
declare -a blob_paths=()
declare -a blob_ids=()
cd "$ROOT_DIR"
while IFS= read -r -d '' entry; do
metadata=${entry%%$'\t'*}
path=${entry#*$'\t'}
read -r mode object stage <<<"$metadata"
[[ "$stage" == "0" ]] || continue
tracked_files=$((tracked_files + 1))
case "$path" in
*.app | *.app/* | *.dSYM | *.dSYM/* | *.xcarchive/* | *.xcresult/* | *.ipa | *.zip | *.delta | *.dmg | \
*.pkg | *.tar.gz | *.tgz)
printf 'ERROR: generated artifact is tracked: %s\n' "$path" >&2
failures=$((failures + 1))
;;
esac
# Submodule entries name commits rather than file blobs.
[[ "$mode" == "160000" ]] && continue
blob_paths+=("$path")
blob_ids+=("$object")
done < <(git ls-files --stage -z)
if ((${#blob_ids[@]} > 0)); then
index=0
while read -r object type size; do
path=${blob_paths[$index]}
if [[ "$type" != "blob" ]]; then
printf 'ERROR: tracked index entry is not a readable blob: %q (%s)\n' "$path" "$object" >&2
failures=$((failures + 1))
index=$((index + 1))
continue
fi
if ((size > MAX_BYTES)); then
printf 'ERROR: tracked file exceeds %d bytes: %q (%d bytes)\n' "$MAX_BYTES" "$path" "$size" >&2
failures=$((failures + 1))
fi
index=$((index + 1))
done < <(printf '%s\n' "${blob_ids[@]}" | git cat-file --batch-check='%(objectname) %(objecttype) %(objectsize)')
fi
if ((failures > 0)); then
printf 'Repository size check failed with %d violation(s).\n' "$failures" >&2
printf 'Publish build/release artifacts outside Git and optimize required source assets.\n' >&2
exit 1
fi
printf 'repository size OK: %d tracked files, maximum %d bytes each\n' "$tracked_files" "$MAX_BYTES"
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Check for new changes in upstream repositories
# Usage: ./Scripts/check_upstreams.sh [upstream|quotio|all]
set -euo pipefail
TARGET=${1:-all}
DAYS=${2:-7}
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}==> Fetching upstream changes...${NC}"
if [ "$TARGET" = "all" ] || [ "$TARGET" = "upstream" ]; then
git fetch upstream 2>/dev/null || {
echo -e "${YELLOW}Adding upstream remote...${NC}"
git remote add upstream https://github.com/steipete/CodexBar.git
git fetch upstream
}
fi
if [ "$TARGET" = "all" ] || [ "$TARGET" = "quotio" ]; then
git fetch quotio 2>/dev/null || {
echo -e "${YELLOW}Adding quotio remote...${NC}"
git remote add quotio https://github.com/nguyenphutrong/quotio.git
git fetch quotio
}
fi
echo ""
remote_default_branch() {
local remote=$1
local branch=""
local candidate
branch=$(git symbolic-ref -q --short "refs/remotes/${remote}/HEAD" 2>/dev/null | sed "s#^${remote}/##" || true)
if [ -z "$branch" ]; then
branch=$(git remote show "$remote" 2>/dev/null | awk '/HEAD branch/ {print $NF; exit}' || true)
fi
if [ -n "$branch" ] && git rev-parse --verify -q "${remote}/${branch}" >/dev/null; then
echo "$branch"
return 0
fi
for candidate in main master; do
if git rev-parse --verify -q "${remote}/${candidate}" >/dev/null; then
echo "$candidate"
return 0
fi
done
echo -e "${RED}Error: Could not resolve default branch for remote '$remote'.${NC}" >&2
exit 1
}
# Check upstream (steipete)
if [ "$TARGET" = "all" ] || [ "$TARGET" = "upstream" ]; then
echo -e "${BLUE}==> Upstream (steipete/CodexBar) changes:${NC}"
UPSTREAM_BRANCH=$(remote_default_branch upstream)
UPSTREAM_REF="upstream/${UPSTREAM_BRANCH}"
UPSTREAM_COUNT=$(git log --oneline "main..${UPSTREAM_REF}" --no-merges 2>/dev/null | wc -l | tr -d ' ')
if [ "$UPSTREAM_COUNT" -gt 0 ]; then
echo -e "${GREEN}Found $UPSTREAM_COUNT new commits${NC}"
echo ""
git log --oneline --graph "main..${UPSTREAM_REF}" --no-merges | head -20 || true
echo ""
echo -e "${YELLOW}Files changed:${NC}"
git diff --stat "main..${UPSTREAM_REF}" | tail -20 || true
else
echo -e "${GREEN}No new commits (up to date)${NC}"
fi
echo ""
fi
# Check quotio
if [ "$TARGET" = "all" ] || [ "$TARGET" = "quotio" ]; then
echo -e "${BLUE}==> Quotio changes (last $DAYS days):${NC}"
QUOTIO_BRANCH=$(remote_default_branch quotio)
QUOTIO_REF="quotio/${QUOTIO_BRANCH}"
QUOTIO_COUNT=$(git log --oneline "$QUOTIO_REF" --since="$DAYS days ago" 2>/dev/null | wc -l | tr -d ' ')
if [ "$QUOTIO_COUNT" -gt 0 ]; then
echo -e "${GREEN}Found $QUOTIO_COUNT commits in last $DAYS days${NC}"
echo ""
git log --oneline --graph "$QUOTIO_REF" --since="$DAYS days ago" | head -20 || true
echo ""
echo -e "${YELLOW}Recent file changes:${NC}"
# Show changes from last 10 commits
git diff --stat "${QUOTIO_REF}~10..${QUOTIO_REF}" 2>/dev/null | tail -20 || echo "Unable to show diff"
else
echo -e "${GREEN}No new commits in last $DAYS days${NC}"
fi
echo ""
fi
# Summary
echo -e "${BLUE}==> Summary${NC}"
if [ "$TARGET" = "all" ] || [ "$TARGET" = "upstream" ]; then
echo "Upstream commits: $UPSTREAM_COUNT"
fi
if [ "$TARGET" = "all" ] || [ "$TARGET" = "quotio" ]; then
echo "Quotio commits (${DAYS}d): $QUOTIO_COUNT"
fi
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo " Review upstream: ./Scripts/review_upstream.sh upstream"
echo " Review quotio: ./Scripts/review_upstream.sh quotio"
echo " Detailed diff: git diff main..<resolved-remote>/<default-branch>"
echo " View quotio: ./Scripts/analyze_quotio.sh"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
changed_paths_file="${1:-}"
if [[ -z "$changed_paths_file" || ! -f "$changed_paths_file" ]]; then
printf 'Usage: %s <changed-paths-file>\n' "$(basename "$0")" >&2
exit 2
fi
macos_tests=false
macos_tests_reason=""
path_count=0
require_macos_tests() {
local path="$1"
local reason="$2"
macos_tests=true
if [[ -z "$macos_tests_reason" ]]; then
macos_tests_reason="${path}: ${reason}"
fi
}
classify_path() {
local path="$1"
[[ -z "$path" ]] && return
path_count=$((path_count + 1))
case "$path" in
AGENTS.md|docs/configuration.md)
require_macos_tests "$path" "changes contributor or runtime configuration contracts"
;;
*.md)
;;
docs/.nojekyll|docs/CNAME|docs/index.html|docs/llms.txt|docs/site-locales.mjs|docs/site.css|docs/site.js|docs/social.html|docs/social.png)
;;
docs/*.png|docs/*.jpg|docs/*.jpeg|docs/*.webp|docs/*.ico|docs/*.svg)
;;
*)
require_macos_tests "$path" "not covered by portable docs/site checks"
;;
esac
}
invalid_row=false
while IFS=$'\t' read -r status first_path second_path extra_path \
|| [[ -n "${status:-}${first_path:-}${second_path:-}${extra_path:-}" ]]
do
[[ -z "${status}${first_path:-}${second_path:-}${extra_path:-}" ]] && continue
case "$status" in
R*|C*)
if ! [[ "$status" =~ ^[RC][0-9]{1,3}$ ]] \
|| ((10#${status:1} > 100)) \
|| [[ -z "${first_path:-}" || -z "${second_path:-}" || -n "${extra_path:-}" ]]
then
invalid_row=true
break
fi
classify_path "$first_path"
classify_path "$second_path"
;;
A|D|M|T|U|X|B)
if [[ -z "${first_path:-}" || -n "${second_path:-}" || -n "${extra_path:-}" ]]; then
invalid_row=true
break
fi
classify_path "$first_path"
;;
*)
invalid_row=true
break
;;
esac
done < "$changed_paths_file"
if [[ "$invalid_row" == true ]]; then
printf 'Invalid git name-status row; refusing to skip macOS tests.\n' >&2
exit 2
fi
if [[ "$path_count" -eq 0 ]]; then
require_macos_tests '<empty diff>' 'no changed paths were reported'
fi
if [[ "$macos_tests" == true ]]; then
summary_reason="$macos_tests_reason"
else
summary_reason="docs/site-only changes covered by portable checks"
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
printf 'macos-tests=%s\n' "$macos_tests" >> "$GITHUB_OUTPUT"
printf 'macos-tests-reason=%s\n' "$summary_reason" >> "$GITHUB_OUTPUT"
printf 'changed-path-count=%s\n' "$path_count" >> "$GITHUB_OUTPUT"
fi
if [[ "$macos_tests" == true ]]; then
printf 'macOS Swift tests required for this change set: %s.\n' "$macos_tests_reason"
else
printf 'Skipping macOS Swift tests for docs/site-only changes covered by portable checks.\n'
fi
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""Run SwiftPM tests in suite shards so CI cannot hang inside one aggregate run."""
from __future__ import annotations
import argparse
import os
import re
import signal
import subprocess
import sys
import time
from collections.abc import Iterable
from dataclasses import dataclass
@dataclass(frozen=True)
class TestSelection:
name: str
filter_pattern: str
suite_name: str | None = None
@dataclass
class RunStats:
discovered_selections: int = 0
selected_selections: int = 0
selected_groups: int = 0
group_size: int = 0
shard_index: int | None = None
shard_count: int | None = None
discovery_seconds: float = 0
execution_seconds: float = 0
total_seconds: float = 0
first_pass_successful_groups: int = 0
first_pass_failed_groups: int = 0
full_group_retries: int = 0
timed_out_groups: int = 0
recovered_groups: int = 0
isolated_selection_retries: int = 0
def summary_rows(self) -> list[tuple[str, str]]:
shard = "none"
if self.shard_index is not None and self.shard_count is not None:
shard = f"{self.shard_index + 1}/{self.shard_count}"
return [
("Shard", shard),
("Group size", str(self.group_size)),
("Discovered selections", str(self.discovered_selections)),
("Selected selections", str(self.selected_selections)),
("Selected groups", str(self.selected_groups)),
("First-pass successful groups", str(self.first_pass_successful_groups)),
("First-pass failed groups", str(self.first_pass_failed_groups)),
("Full-group retries", str(self.full_group_retries)),
("Recovered groups", str(self.recovered_groups)),
("Timed out groups", str(self.timed_out_groups)),
("Isolated selection retries", str(self.isolated_selection_retries)),
("Discovery seconds", f"{self.discovery_seconds:.1f}"),
("Execution seconds", f"{self.execution_seconds:.1f}"),
("Total seconds", f"{self.total_seconds:.1f}"),
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--group-size", type=int, default=12)
parser.add_argument("--timeout", type=int, default=180)
parser.add_argument("--limit-groups", type=int)
parser.add_argument("--shard-index", type=int)
parser.add_argument("--shard-count", type=int)
parser.add_argument(
"--no-retry-non-timeout-failures",
action="store_false",
dest="retry_non_timeout_failures",
help="fail immediately when a group exits without timing out",
)
parser.add_argument("--list-only", action="store_true")
parser.add_argument("--swift-command", default="swift")
parser.add_argument("--swift-command-arg", action="append", default=[])
return parser.parse_args()
def run_command(command: list[str], timeout: int | None = None) -> int:
print(f"+ {' '.join(command)}", flush=True)
process = subprocess.Popen(command, start_new_session=True)
try:
return process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
print(f"::warning::Command timed out after {timeout}s: {' '.join(command)}", flush=True)
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
return 124
def swift_test_list(swift_command: list[str]) -> list[TestSelection]:
command = [*swift_command, "test", "list"]
try:
result = subprocess.run(command, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as error:
print(f"+ {swift_command[0]} test list", flush=True)
if error.stdout:
print(error.stdout, end="" if error.stdout.endswith("\n") else "\n", flush=True)
if error.stderr:
print(error.stderr, end="" if error.stderr.endswith("\n") else "\n", file=sys.stderr, flush=True)
raise
selections: set[TestSelection] = set()
unknown: list[str] = []
for line in result.stdout.splitlines():
top_level = re.fullmatch(r"(?P<module>[^.]+)\.(?:`(?P<display>.+)`|(?P<function>[^()/]+))\(\)", line)
if top_level is not None:
module = top_level.group("module")
test_name = top_level.group("display") or top_level.group("function")
selections.add(
TestSelection(
name=line,
# SwiftPM matches top-level Swift Testing functions by their display name,
# not the backtick-wrapped identifier printed by `swift test list`.
filter_pattern=rf"{re.escape(module)}\..*{re.escape(test_name)}",
)
)
continue
if "/" in line:
suite = line.split("/", 1)[0]
if "." in suite:
selections.add(
TestSelection(
name=suite,
filter_pattern=rf"^{re.escape(suite)}/",
suite_name=suite,
)
)
continue
unknown.append(line)
if unknown:
rendered = "\n".join(f"- {line}" for line in unknown)
raise RuntimeError(f"Unrecognized `swift test list` output:\n{rendered}")
return sorted(selections, key=lambda selection: selection.name)
def append_github_summary(stats: RunStats) -> None:
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if not summary_path:
return
with open(summary_path, "a", encoding="utf-8") as summary:
summary.write("### macOS Swift test timing\n\n")
summary.write("| Field | Value |\n")
summary.write("| --- | --- |\n")
for field, value in stats.summary_rows():
safe_value = value.replace("|", "\\|")
summary.write(f"| {field} | `{safe_value}` |\n")
summary.write("\n")
def print_timing_summary(stats: RunStats) -> None:
print("Swift test timing summary:", flush=True)
for field, value in stats.summary_rows():
print(f"- {field}: {value}", flush=True)
def chunks(items: list[TestSelection], size: int) -> Iterable[list[TestSelection]]:
for index in range(0, len(items), size):
yield items[index : index + size]
def shard_groups(groups: list[list[TestSelection]], shard_index: int | None, shard_count: int | None) -> list[list[TestSelection]]:
if shard_index is None and shard_count is None:
return groups
if shard_index is None or shard_count is None:
raise ValueError("--shard-index and --shard-count must be passed together")
if shard_count < 1:
raise ValueError("--shard-count must be positive")
if shard_index < 0 or shard_index >= shard_count:
raise ValueError("--shard-index must be in the range [0, --shard-count)")
return [group for index, group in enumerate(groups) if index % shard_count == shard_index]
def prioritized_suites(suites: list[TestSelection]) -> list[TestSelection]:
priority = ["CodexBarTests.CLIEntryTests"]
ordered = [suite for name in priority for suite in suites if suite.suite_name == name]
ordered.extend(suite for suite in suites if suite.suite_name not in priority)
return ordered
def filtered_suites_for_environment(suites: list[TestSelection]) -> list[TestSelection]:
if os.environ.get("GITHUB_ACTIONS") != "true" or sys.platform != "darwin":
return suites
# SwiftPM hangs before suite output for this executable-target suite on the Intel macOS runner.
# Linux CI still runs it in the full Swift test lane, and local macOS runs it directly.
skipped = {"CodexBarTests.CLIEntryTests"}
filtered = [suite for suite in suites if suite.suite_name not in skipped]
if len(filtered) != len(suites):
print(f"Skipping macOS CI-only suites: {', '.join(sorted(skipped))}", flush=True)
return filtered
def filter_for(suites: list[TestSelection]) -> str:
return rf"({'|'.join(suite.filter_pattern for suite in suites)})"
def run_group(suites: list[TestSelection], timeout: int, swift_command: list[str]) -> int:
return run_command(
[*swift_command, "test", "--skip-build", "--no-parallel", "--filter", filter_for(suites)],
timeout=timeout,
)
def retry_selections_individually(
suites: list[TestSelection],
timeout: int,
swift_command: list[str],
stats: RunStats,
) -> int:
for suite in suites:
stats.isolated_selection_retries += 1
print(f"::group::Swift test retry {suite.name}", flush=True)
retry_result = run_group([suite], timeout, swift_command)
print("::endgroup::", flush=True)
if retry_result != 0:
return retry_result
return 0
def main() -> int:
total_started = time.monotonic()
args = parse_args()
stats = RunStats(
group_size=args.group_size,
shard_index=args.shard_index,
shard_count=args.shard_count,
)
if args.group_size < 1:
print("--group-size must be positive", file=sys.stderr)
return 2
swift_command = [args.swift_command, *args.swift_command_arg]
result = 0
try:
discovery_started = time.monotonic()
try:
suites = prioritized_suites(filtered_suites_for_environment(swift_test_list(swift_command)))
finally:
stats.discovery_seconds = time.monotonic() - discovery_started
stats.discovered_selections = len(suites)
suite_groups = list(chunks(suites, args.group_size))
try:
suite_groups = shard_groups(suite_groups, args.shard_index, args.shard_count)
except ValueError as error:
print(str(error), file=sys.stderr)
result = 2
return result
if args.limit_groups is not None:
suite_groups = suite_groups[: args.limit_groups]
stats.selected_selections = sum(len(group) for group in suite_groups)
stats.selected_groups = len(suite_groups)
shard_suffix = ""
if args.shard_index is not None and args.shard_count is not None:
shard_suffix = f" in shard {args.shard_index + 1}/{args.shard_count}"
print(
f"Discovered {len(suites)} test selections; running {stats.selected_selections} selections "
f"in {len(suite_groups)} groups{shard_suffix}",
flush=True,
)
if args.list_only:
for group in suite_groups:
for suite in group:
print(suite.name)
return 0
if not suite_groups:
print("No test groups selected.", flush=True)
return 0
execution_started = time.monotonic()
for group_index, group in enumerate(suite_groups, start=1):
print(
f"::group::Swift test group {group_index}/{len(suite_groups)} "
f"({len(group)} selections)",
flush=True,
)
group_result = run_group(group, args.timeout, swift_command)
print("::endgroup::", flush=True)
if group_result == 0:
stats.first_pass_successful_groups += 1
continue
stats.first_pass_failed_groups += 1
group_timed_out = group_result == 124
if group_timed_out:
stats.timed_out_groups += 1
if len(group) == 1:
result = group_result
return result
if group_result != 124:
if not args.retry_non_timeout_failures:
result = group_result
return result
stats.full_group_retries += 1
print(f"Group {group_index} failed with exit code {group_result}; retrying group once", flush=True)
retry_result = run_group(group, args.timeout, swift_command)
if retry_result == 0:
stats.recovered_groups += 1
continue
if retry_result != 124:
result = retry_result
return result
group_timed_out = True
stats.timed_out_groups += 1
print(f"Group {group_index} timed out; retrying selections one at a time", flush=True)
retry_result = retry_selections_individually(group, args.timeout, swift_command, stats)
if retry_result != 0:
result = retry_result
return result
if group_timed_out:
stats.recovered_groups += 1
return result
finally:
stats.total_seconds = time.monotonic() - total_started
if "execution_started" in locals():
stats.execution_seconds = time.monotonic() - execution_started
if not args.list_only:
print_timing_summary(stats)
append_github_summary(stats)
if __name__ == "__main__":
raise SystemExit(main())
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
lint_result="${1:-}"
changes_result="${2:-}"
macos_tests_required="${3:-}"
macos_test_result="${4:-}"
if [[ "$lint_result" != "success" ]]; then
printf 'lint job finished with %s\n' "${lint_result:-<empty>}" >&2
exit 1
fi
if [[ "$changes_result" != "success" ]]; then
printf 'changes job finished with %s\n' "${changes_result:-<empty>}" >&2
exit 1
fi
case "${macos_tests_required}:${macos_test_result}" in
true:success)
printf 'Lint and macOS Swift test shards passed.\n'
;;
false:skipped)
printf 'Lint passed; macOS Swift tests skipped for docs/site-only changes.\n'
;;
*)
printf 'macOS test gate/result mismatch: required=%s result=%s\n' \
"${macos_tests_required:-<empty>}" "${macos_test_result:-<empty>}" >&2
exit 1
;;
esac
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env bash
# Reset CodexBar: kill running instances, build, package, relaunch, verify.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_BUNDLE="${ROOT_DIR}/CodexBar.app"
APP_PROCESS_PATTERN="CodexBar.app/Contents/MacOS/CodexBar"
DEBUG_PROCESS_PATTERN="${ROOT_DIR}/.build/debug/CodexBar"
RELEASE_PROCESS_PATTERN="${ROOT_DIR}/.build/release/CodexBar"
LOCK_KEY="$(printf '%s' "${ROOT_DIR}" | shasum -a 256 | cut -c1-8)"
LOCK_DIR="${TMPDIR:-/tmp}/codexbar-compile-and-run-${LOCK_KEY}"
LOCK_PID_FILE="${LOCK_DIR}/pid"
WAIT_FOR_LOCK=0
RUN_TESTS=0
DEBUG_LLDB=0
RELEASE_ARCHES=""
SIGNING_MODE="${CODEXBAR_SIGNING:-}"
CLEAR_ADHOC_KEYCHAIN=0
log() { printf '%s\n' "$*"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
delete_keychain_service_items() {
local service="$1"
security delete-generic-password -s "${service}" >/dev/null 2>&1 || true
while security delete-generic-password -s "${service}" >/dev/null 2>&1; do
:
done
}
# Ensure Swift >= 5.5 (required for --arch flag in swift build)
ensure_swift_version() {
local swift_output
local swift_ver
swift_output=$(swift --version 2>&1 || true)
if [[ "$swift_output" =~ (Apple[[:space:]]+)?Swift[[:space:]]+version[[:space:]]+([0-9]+)\.([0-9]+)(\.[0-9]+)? ]]; then
swift_ver="${BASH_REMATCH[2]}.${BASH_REMATCH[3]}${BASH_REMATCH[4]}"
else
fail "Swift >= 5.5 required (found ${swift_output:-none}). Install Xcode or update swiftly."
fi
local major minor
major=$(echo "$swift_ver" | cut -d. -f1)
minor=$(echo "$swift_ver" | cut -d. -f2)
if [[ "${major:-0}" -ge 6 ]] || { [[ "${major:-0}" -eq 5 ]] && [[ "${minor:-0}" -ge 5 ]]; }; then
return 0
fi
# Try Xcode toolchain
local xcrun_swift
xcrun_swift=$(xcrun --find swift 2>/dev/null || true)
if [[ -n "$xcrun_swift" && -x "$xcrun_swift" ]]; then
log "WARN: PATH swift is v${swift_ver}; switching to Xcode toolchain at $(dirname "$xcrun_swift")"
export PATH="$(dirname "$xcrun_swift"):$PATH"
return 0
fi
fail "Swift >= 5.5 required (found ${swift_ver:-none}). Install Xcode or update swiftly."
}
has_signing_identity() {
local identity="${1:-}"
if [[ -z "${identity}" ]]; then
return 1
fi
security find-identity -p codesigning -v 2>/dev/null | grep -F "${identity}" >/dev/null 2>&1
}
detect_codesigning_identity() {
local preferred_prefixes=(
"Developer ID Application:"
"Apple Development:"
"Apple Distribution:"
)
local prefix
local identities
identities="$(security find-identity -p codesigning -v 2>/dev/null || true)"
for prefix in "${preferred_prefixes[@]}"; do
awk -v prefix="${prefix}" '
index($0, "\"" prefix) {
sub(/^[^\"]*\"/, "")
sub(/\".*$/, "")
print
exit
}
' <<<"${identities}"
done | sed -n '1p'
}
export_team_id_from_identity() {
local identity="${1:-}"
if [[ -n "${APP_TEAM_ID:-}" || -z "${identity}" ]]; then
return
fi
local subject
subject="$(security find-certificate -c "${identity}" -p 2>/dev/null \
| openssl x509 -noout -subject -nameopt RFC2253 2>/dev/null || true)"
if [[ "${subject}" =~ (^|,)OU=([A-Z0-9]{10})(,|$) ]]; then
APP_TEAM_ID="${BASH_REMATCH[2]}"
export APP_TEAM_ID
return
fi
if [[ "${identity}" =~ \(([A-Z0-9]{10})\)$ ]]; then
APP_TEAM_ID="${BASH_REMATCH[1]}"
export APP_TEAM_ID
fi
}
resolve_signing_mode() {
if [[ -n "${SIGNING_MODE}" ]]; then
export_team_id_from_identity "${APP_IDENTITY:-}"
return
fi
if [[ -n "${APP_IDENTITY:-}" ]]; then
if has_signing_identity "${APP_IDENTITY}"; then
export_team_id_from_identity "${APP_IDENTITY}"
SIGNING_MODE="identity"
return
fi
log "WARN: APP_IDENTITY not found in Keychain; falling back to adhoc signing."
SIGNING_MODE="adhoc"
return
fi
local candidate=""
for candidate in \
"Developer ID Application: Peter Steinberger (Y5PE65HELJ)" \
"CodexBar Development"
do
if has_signing_identity "${candidate}"; then
APP_IDENTITY="${candidate}"
export APP_IDENTITY
export_team_id_from_identity "${APP_IDENTITY}"
SIGNING_MODE="identity"
return
fi
done
candidate="$(detect_codesigning_identity)"
if [[ -n "${candidate}" ]]; then
APP_IDENTITY="${candidate}"
export APP_IDENTITY
export_team_id_from_identity "${APP_IDENTITY}"
SIGNING_MODE="identity"
return
fi
SIGNING_MODE="adhoc"
}
run_step() {
local label="$1"; shift
log "==> ${label}"
if ! "$@"; then
fail "${label} failed"
fi
}
cleanup() {
if [[ -d "${LOCK_DIR}" ]]; then
rm -rf "${LOCK_DIR}"
fi
}
acquire_lock() {
while true; do
if mkdir "${LOCK_DIR}" 2>/dev/null; then
echo "$$" > "${LOCK_PID_FILE}"
return 0
fi
local existing_pid=""
if [[ -f "${LOCK_PID_FILE}" ]]; then
existing_pid="$(cat "${LOCK_PID_FILE}" 2>/dev/null || true)"
fi
if [[ -n "${existing_pid}" ]] && kill -0 "${existing_pid}" 2>/dev/null; then
if [[ "${WAIT_FOR_LOCK}" == "1" ]]; then
log "==> Another agent is compiling (pid ${existing_pid}); waiting..."
while kill -0 "${existing_pid}" 2>/dev/null; do
sleep 1
done
continue
fi
log "==> Another agent is compiling (pid ${existing_pid}); re-run with --wait."
exit 0
fi
rm -rf "${LOCK_DIR}"
done
}
trap cleanup EXIT INT TERM
kill_claude_probes() {
# CodexBar spawns `claude /usage` + `/status` in a PTY; if we kill the app mid-probe we can orphan them.
pkill -f "claude (/status|/usage) --allowed-tools" 2>/dev/null || true
sleep 0.2
pkill -9 -f "claude (/status|/usage) --allowed-tools" 2>/dev/null || true
}
kill_all_codexbar() {
is_running() {
pgrep -f "${APP_PROCESS_PATTERN}" >/dev/null 2>&1 \
|| pgrep -f "${DEBUG_PROCESS_PATTERN}" >/dev/null 2>&1 \
|| pgrep -f "${RELEASE_PROCESS_PATTERN}" >/dev/null 2>&1 \
|| pgrep -x "CodexBar" >/dev/null 2>&1
}
# Phase 1: request termination (give the app time to exit cleanly).
for _ in {1..25}; do
pkill -f "${APP_PROCESS_PATTERN}" 2>/dev/null || true
pkill -f "${DEBUG_PROCESS_PATTERN}" 2>/dev/null || true
pkill -f "${RELEASE_PROCESS_PATTERN}" 2>/dev/null || true
pkill -x "CodexBar" 2>/dev/null || true
if ! is_running; then
return 0
fi
sleep 0.2
done
# Phase 2: force kill any stragglers (avoids `open -n` creating multiple instances).
pkill -9 -f "${APP_PROCESS_PATTERN}" 2>/dev/null || true
pkill -9 -f "${DEBUG_PROCESS_PATTERN}" 2>/dev/null || true
pkill -9 -f "${RELEASE_PROCESS_PATTERN}" 2>/dev/null || true
pkill -9 -x "CodexBar" 2>/dev/null || true
for _ in {1..25}; do
if ! is_running; then
return 0
fi
sleep 0.2
done
fail "Failed to kill all CodexBar instances."
}
# 1) Ensure a single runner instance.
for arg in "$@"; do
case "${arg}" in
--wait|-w) WAIT_FOR_LOCK=1 ;;
--test|-t) RUN_TESTS=1 ;;
--debug-lldb) DEBUG_LLDB=1 ;;
--clear-adhoc-keychain) CLEAR_ADHOC_KEYCHAIN=1 ;;
--release-universal) RELEASE_ARCHES="arm64 x86_64" ;;
--release-arches=*) RELEASE_ARCHES="${arg#*=}" ;;
--help|-h)
log "Usage: $(basename "$0") [--wait] [--test] [--debug-lldb] [--clear-adhoc-keychain] [--release-universal] [--release-arches=\"arm64 x86_64\"]"
exit 0
;;
*)
;;
esac
done
ensure_swift_version
resolve_signing_mode
if [[ "${CLEAR_ADHOC_KEYCHAIN}" == "1" && "${SIGNING_MODE}" != "adhoc" ]]; then
fail "--clear-adhoc-keychain is only supported when using adhoc signing."
fi
if [[ "${SIGNING_MODE}" == "adhoc" ]]; then
log "==> Signing: adhoc (set APP_IDENTITY or install a dev cert to avoid keychain prompts)"
else
log "==> Signing: ${APP_IDENTITY:-Developer ID Application}"
fi
acquire_lock
# 2) Kill all running CodexBar instances (debug, release, bundled).
log "==> Killing existing CodexBar instances"
kill_all_codexbar
kill_claude_probes
# 2.5) Optionally delete keychain entries to avoid permission prompts with adhoc signing
# (adhoc signature changes on every build, making old keychain entries inaccessible)
if [[ "${SIGNING_MODE:-adhoc}" == "adhoc" && "${CLEAR_ADHOC_KEYCHAIN}" == "1" ]]; then
log "==> Clearing CodexBar keychain entries (adhoc signing)"
# Clear both the legacy keychain store and the current cache service when developers explicitly want a clean reset
# of CodexBar-owned keychain state for ad-hoc builds.
delete_keychain_service_items "com.steipete.CodexBar"
delete_keychain_service_items "com.steipete.codexbar.cache"
elif [[ "${SIGNING_MODE:-adhoc}" == "adhoc" ]]; then
log "==> Preserving CodexBar keychain entries (pass --clear-adhoc-keychain to reset adhoc keychain state)"
fi
# 3) Package (release build happens inside package_app.sh).
if [[ "${RUN_TESTS}" == "1" ]]; then
run_step "sharded swift tests" "${ROOT_DIR}/Scripts/test.sh"
fi
if [[ "${DEBUG_LLDB}" == "1" && -n "${RELEASE_ARCHES}" ]]; then
fail "--release-arches is only supported for release packaging"
fi
HOST_ARCH="$(uname -m)"
ARCHES_VALUE="${HOST_ARCH}"
if [[ -n "${RELEASE_ARCHES}" ]]; then
ARCHES_VALUE="${RELEASE_ARCHES}"
fi
PACKAGE_ENV=(
ARCHES="${ARCHES_VALUE}"
)
if [[ "${DEBUG_LLDB}" == "1" ]]; then
run_step "package app" env CODEXBAR_ALLOW_LLDB=1 "${PACKAGE_ENV[@]}" "${ROOT_DIR}/Scripts/package_app.sh" debug
else
if [[ -n "${SIGNING_MODE}" ]]; then
run_step "package app" env CODEXBAR_SIGNING="${SIGNING_MODE}" "${PACKAGE_ENV[@]}" "${ROOT_DIR}/Scripts/package_app.sh"
else
run_step "package app" env "${PACKAGE_ENV[@]}" "${ROOT_DIR}/Scripts/package_app.sh"
fi
fi
# 4) Launch the packaged app.
log "==> launch app"
if ! open "${APP_BUNDLE}"; then
log "WARN: launch app returned non-zero; falling back to direct binary launch."
"${APP_BUNDLE}/Contents/MacOS/CodexBar" >/dev/null 2>&1 &
disown
fi
# 5) Verify the app stays up for at least a moment (launch can be >1s on some systems).
for _ in {1..10}; do
if pgrep -f "${APP_PROCESS_PATTERN}" >/dev/null 2>&1; then
log "OK: CodexBar is running."
exit 0
fi
sleep 0.4
done
fail "App exited immediately. Check crash logs in Console.app (User Reports)."
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env swift
import Foundation
struct SurveyOptions {
var root: URL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".codex", isDirectory: true)
.appendingPathComponent("sessions", isDirectory: true)
var days: Double = 30
}
struct Survey {
var files = 0
var totalBytes: Int64 = 0
var lines = 0
var relevantLines = 0
var lineLengths: [Int] = []
var linesOver32KiB = 0
var linesOver256KiB = 0
var turnContextLines = 0
var turnContextOver32KiB = 0
var turnContextOver256KiB = 0
var turnContextModelOffsets: [Int] = []
var turnContextModelOffsetUnder32KiB = 0
var turnContextModelOffsetUnder256KiB = 0
var tokenCountLines = 0
var tokenCountMissingExplicitModel = 0
mutating func recordFile(byteCount: Int64) {
self.files += 1
self.totalBytes += byteCount
}
mutating func recordLine(_ line: Data) {
guard !line.isEmpty else { return }
let length = line.count
self.lines += 1
self.lineLengths.append(length)
if length > 32 * 1024 {
self.linesOver32KiB += 1
}
if length > 256 * 1024 {
self.linesOver256KiB += 1
}
let isRelevant = line.contains(Marker.eventMessage)
|| line.contains(Marker.turnContext)
|| line.contains(Marker.sessionMetadata)
if isRelevant {
self.relevantLines += 1
}
if line.contains(Marker.turnContext) {
self.turnContextLines += 1
if length > 32 * 1024 {
self.turnContextOver32KiB += 1
}
if length > 256 * 1024 {
self.turnContextOver256KiB += 1
}
if let offset = line.firstOffset(of: Marker.modelField)
?? line.firstOffset(of: Marker.modelNameField)
{
self.turnContextModelOffsets.append(offset)
if offset < 32 * 1024 {
self.turnContextModelOffsetUnder32KiB += 1
}
if offset < 256 * 1024 {
self.turnContextModelOffsetUnder256KiB += 1
}
}
}
if line.contains(Marker.tokenCount) {
self.tokenCountLines += 1
if !line.contains(Marker.modelField), !line.contains(Marker.modelNameField) {
self.tokenCountMissingExplicitModel += 1
}
}
}
}
enum Marker {
static let eventMessage = Data(#""type":"event_msg""#.utf8)
static let turnContext = Data(#""type":"turn_context""#.utf8)
static let sessionMetadata = Data(#""type":"session_meta""#.utf8)
static let tokenCount = Data(#""token_count""#.utf8)
static let modelField = Data(#""model""#.utf8)
static let modelNameField = Data(#""model_name""#.utf8)
}
extension Data {
func contains(_ marker: Data) -> Bool {
self.range(of: marker) != nil
}
func firstOffset(of marker: Data) -> Int? {
guard let range = self.range(of: marker) else { return nil }
return self.distance(from: self.startIndex, to: range.lowerBound)
}
}
func parseOptions(arguments: [String]) throws -> SurveyOptions {
var options = SurveyOptions()
var index = 1
while index < arguments.count {
switch arguments[index] {
case "--root":
index += 1
guard index < arguments.count else {
throw UsageError.message("--root requires a path")
}
options.root = URL(fileURLWithPath: expandTilde(arguments[index]), isDirectory: true)
case "--days":
index += 1
guard index < arguments.count, let days = Double(arguments[index]) else {
throw UsageError.message("--days requires a number")
}
options.days = days
case "--help", "-h":
printUsage()
Foundation.exit(0)
default:
throw UsageError.message("unknown argument: \(arguments[index])")
}
index += 1
}
return options
}
func expandTilde(_ path: String) -> String {
guard path == "~" || path.hasPrefix("~/") else { return path }
let home = FileManager.default.homeDirectoryForCurrentUser.path
if path == "~" {
return home
}
return home + String(path.dropFirst())
}
enum UsageError: Error, CustomStringConvertible {
case message(String)
var description: String {
switch self {
case let .message(text):
text
}
}
}
func printUsage() {
print(
"""
Usage: Scripts/cost_jsonl_shape_survey.swift [--root PATH] [--days N]
Scans local Codex JSONL logs and prints aggregate shape only. It does not
print prompts, tool payloads, model values, file paths, or raw log lines.
""")
}
func jsonlFiles(root: URL, modifiedSince cutoff: Date) -> [URL] {
guard let enumerator = FileManager.default.enumerator(
at: root,
includingPropertiesForKeys: [.isRegularFileKey, .contentModificationDateKey, .fileSizeKey],
options: [.skipsHiddenFiles]) else { return [] }
var files: [URL] = []
for case let fileURL as URL in enumerator {
guard fileURL.pathExtension == "jsonl" else { continue }
let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .contentModificationDateKey])
guard values?.isRegularFile == true else { continue }
guard let modifiedAt = values?.contentModificationDate, modifiedAt >= cutoff else { continue }
files.append(fileURL)
}
return files.sorted { $0.path < $1.path }
}
func validateRoot(_ root: URL) throws {
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: root.path, isDirectory: &isDirectory)
guard exists, isDirectory.boolValue else {
throw UsageError.message("root does not exist or is not a directory")
}
}
func scan(fileURL: URL, into survey: inout Survey) throws {
let values = try fileURL.resourceValues(forKeys: [.fileSizeKey])
survey.recordFile(byteCount: Int64(values.fileSize ?? 0))
let handle = try FileHandle(forReadingFrom: fileURL)
defer { try? handle.close() }
var current = Data()
current.reserveCapacity(4 * 1024)
while true {
let chunk = try handle.read(upToCount: 256 * 1024) ?? Data()
if chunk.isEmpty {
survey.recordLine(current)
break
}
var segmentStart = chunk.startIndex
while let newline = chunk[segmentStart...].firstIndex(of: 0x0A) {
current.append(contentsOf: chunk[segmentStart..<newline])
survey.recordLine(current)
current.removeAll(keepingCapacity: true)
segmentStart = chunk.index(after: newline)
}
if segmentStart < chunk.endIndex {
current.append(contentsOf: chunk[segmentStart..<chunk.endIndex])
}
}
}
func percentile(_ values: [Int], _ percentile: Double) -> Int {
guard !values.isEmpty else { return 0 }
let sorted = values.sorted()
let index = Int((percentile * Double(sorted.count - 1)).rounded())
return sorted[max(0, min(index, sorted.count - 1))]
}
func printSummary(_ survey: Survey, options: SurveyOptions) {
print("root: \(redactedRootDescription(options.root))")
print("window days: \(Int(options.days))")
print("files: \(survey.files)")
print("total bytes: \(survey.totalBytes)")
print("lines: \(survey.lines)")
print("relevant Codex scanner lines: \(survey.relevantLines)")
print(
"line length p50/p90/p95/p99/max: " +
"\(percentile(survey.lineLengths, 0.50)) / " +
"\(percentile(survey.lineLengths, 0.90)) / " +
"\(percentile(survey.lineLengths, 0.95)) / " +
"\(percentile(survey.lineLengths, 0.99)) / " +
"\(percentile(survey.lineLengths, 1.00)) bytes")
print("lines > 32 KiB: \(survey.linesOver32KiB)")
print("lines > 256 KiB: \(survey.linesOver256KiB)")
print("turn_context lines: \(survey.turnContextLines)")
print("turn_context lines > 32 KiB: \(survey.turnContextOver32KiB)")
print("turn_context lines > 256 KiB: \(survey.turnContextOver256KiB)")
print(
"turn_context model offset p50/p95/max: " +
"\(percentile(survey.turnContextModelOffsets, 0.50)) / " +
"\(percentile(survey.turnContextModelOffsets, 0.95)) / " +
"\(percentile(survey.turnContextModelOffsets, 1.00)) bytes")
print(
"turn_context model offset < 32 KiB: " +
"\(survey.turnContextModelOffsetUnder32KiB) / \(survey.turnContextLines)")
print(
"turn_context model offset < 256 KiB: " +
"\(survey.turnContextModelOffsetUnder256KiB) / \(survey.turnContextLines)")
print(
"token_count rows missing an explicit model: " +
"\(survey.tokenCountMissingExplicitModel) / \(survey.tokenCountLines)")
}
func redactedRootDescription(_ root: URL) -> String {
let defaultRoot = SurveyOptions().root.standardizedFileURL.path
let currentRoot = root.standardizedFileURL.path
if currentRoot == defaultRoot {
return "default Codex sessions"
}
return "custom root (redacted)"
}
do {
let options = try parseOptions(arguments: CommandLine.arguments)
try validateRoot(options.root)
let cutoff = Date().addingTimeInterval(-options.days * 24 * 60 * 60)
let files = jsonlFiles(root: options.root, modifiedSince: cutoff)
guard !files.isEmpty else {
throw UsageError.message("no .jsonl files found in the selected time window")
}
var survey = Survey()
for fileURL in files {
try scan(fileURL: fileURL, into: &survey)
}
printSummary(survey, options: options)
} catch let error as UsageError {
fputs("error: \(error.description)\n\n", stderr)
printUsage()
Foundation.exit(2)
} catch {
fputs("error: \(error.localizedDescription)\n", stderr)
Foundation.exit(1)
}
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const DOCS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'docs');
const EXCLUDED_DIRS = new Set(['archive', 'research']);
function walkMarkdownFiles(dir, base = dir) {
const entries = readdirSync(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (EXCLUDED_DIRS.has(entry.name)) continue;
files.push(...walkMarkdownFiles(fullPath, base));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
files.push(relative(base, fullPath));
}
}
return files.sort((a, b) => a.localeCompare(b));
}
function extractMetadata(fullPath) {
const content = readFileSync(fullPath, 'utf8');
if (!content.startsWith('---')) {
return { summary: null, readWhen: [], error: 'missing front matter' };
}
const endIndex = content.indexOf('\n---', 3);
if (endIndex === -1) {
return { summary: null, readWhen: [], error: 'unterminated front matter' };
}
const frontMatter = content.slice(3, endIndex).trim();
const lines = frontMatter.split('\n');
let summaryLine = null;
const readWhen = [];
let collectingReadWhen = false;
for (const rawLine of lines) {
const line = rawLine.trim();
if (line.startsWith('summary:')) {
summaryLine = line;
collectingReadWhen = false;
continue;
}
if (line.startsWith('read_when:')) {
collectingReadWhen = true;
const inline = line.slice('read_when:'.length).trim();
if (inline.startsWith('[') && inline.endsWith(']')) {
try {
const parsed = JSON.parse(inline.replace(/'/g, '"'));
if (Array.isArray(parsed)) {
parsed
.map((v) => String(v).trim())
.filter(Boolean)
.forEach((v) => readWhen.push(v));
}
} catch {
/* ignore malformed inline */
}
}
continue;
}
if (collectingReadWhen) {
if (line.startsWith('- ')) {
const hint = line.slice(2).trim();
if (hint) readWhen.push(hint);
} else if (line === '') {
// allow blank spacer lines inside list
} else {
collectingReadWhen = false;
}
}
}
if (!summaryLine) {
return { summary: null, readWhen, error: 'summary key missing' };
}
const summaryValue = summaryLine.slice('summary:'.length).trim();
const normalized = summaryValue.replace(/^['"]|['"]$/g, '').replace(/\s+/g, ' ').trim();
if (!normalized) {
return { summary: null, readWhen, error: 'summary is empty' };
}
return { summary: normalized, readWhen };
}
console.log('Listing all markdown files in docs folder:');
const markdownFiles = walkMarkdownFiles(DOCS_DIR);
for (const relativePath of markdownFiles) {
const fullPath = join(DOCS_DIR, relativePath);
const { summary, readWhen, error } = extractMetadata(fullPath);
if (summary) {
console.log(`${relativePath} - ${summary}`);
if (readWhen.length > 0) {
console.log(` Read when: ${readWhen.join('; ')}`);
}
} else {
const reason = error ? ` - [${error}]` : '';
console.log(`${relativePath}${reason}`);
}
}
console.log('\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above (React hooks, cache directives, database work, tests, etc.), read that doc before coding, and suggest new coverage when it is missing.');
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const docsDir = path.join(repoRoot, "docs");
const args = process.argv.slice(2);
const mode = parseMode(args);
const cname = fs.readFileSync(path.join(docsDir, "CNAME"), "utf8").trim();
const origin = "https://" + cname;
const productName = "CodexBar";
const source = "https://github.com/steipete/CodexBar";
const outputPath = path.join(docsDir, "llms.txt");
const pages = allHtml(docsDir)
.map((file) => {
const rel = path.relative(docsDir, file).replaceAll(path.sep, "/");
if (rel === "404.html" || rel === "social.html") return null;
const html = fs.readFileSync(file, "utf8");
return {
rel,
title: textContent(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]) || titleize(path.basename(rel, ".html")),
description: attr(html.match(/<meta\s+name=["']description["']\s+content=["']([^"']*)["'][^>]*>/i)?.[1] || ""),
};
})
.filter(Boolean)
.sort((a, b) => (a.rel === "index.html" ? -1 : b.rel === "index.html" ? 1 : a.rel.localeCompare(b.rel)));
const productDescription =
pages.find((page) => page.rel === "index.html")?.description ||
"CodexBar shows AI coding-provider usage limits in the macOS menu bar.";
const lines = [
"# " + productName,
"",
productDescription,
"",
"Canonical documentation:",
...pages.map((page) => "- " + page.title + ": " + pageUrl(page.rel) + (page.description ? " - " + page.description : "")),
"",
"Source: " + source,
"",
"Guidance for agents:",
"- Prefer the canonical documentation URLs above over README excerpts or package metadata.",
"- Fetch only the pages needed for the current task; this is an index, not a full-site corpus.",
"",
];
const output = lines.join("\n");
if (mode === "check") {
const current = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, "utf8") : null;
if (current !== output) {
console.error(`${path.relative(repoRoot, outputPath)} is out of date; run node Scripts/generate-llms.mjs`);
process.exit(1);
}
console.log("llms index OK: " + path.relative(repoRoot, outputPath));
} else {
fs.writeFileSync(outputPath, output, "utf8");
console.log("wrote " + path.relative(repoRoot, outputPath));
}
function parseMode(values) {
if (values.length === 0) return "write";
if (values.length === 1 && (values[0] === "write" || values[0] === "--write")) return "write";
if (values.length === 1 && (values[0] === "check" || values[0] === "--check")) return "check";
console.error("Usage: node Scripts/generate-llms.mjs [write|--write|check|--check]");
process.exit(2);
}
function allHtml(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.name === "node_modules" || entry.name.startsWith(".")) return [];
if (entry.isDirectory()) return allHtml(full);
return entry.name.endsWith(".html") ? [full] : [];
});
}
function pageUrl(rel) {
return rel === "index.html" ? origin + "/" : origin + "/" + rel;
}
function textContent(value) {
return attr(value || "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
}
function attr(value) {
return String(value || "")
.replace(/&mdash;/g, "-")
.replace(/&amp;/g, "&")
.replace(/&nbsp;/g, " ")
.replace(/&#39;/g, "'")
.replace(/&quot;/g, '"')
.trim();
}
function titleize(input) {
return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase());
}
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TOOLS_DIR="${ROOT_DIR}/.build/lint-tools"
BIN_DIR="${TOOLS_DIR}/bin"
SWIFTFORMAT_VERSION="0.61.1"
SWIFTLINT_VERSION="0.65.0"
SWIFTFORMAT_SHA256_DARWIN="b990400779aceb7d7020796eb9ba814d4480543f671d38fc0ff48cb72f04c584"
SWIFTLINT_SHA256_DARWIN="d6cb0aa7a2f5f1ef306fc9e37bcb54dc9a26facc8f7784ac0c3dd3eccf5c6ba6"
SWIFTFORMAT_SHA256_LINUX_X86_64="7bc8706e3fd51963f1f29eb99098ebdf482f3497fa527c68e6cf75cbee29c77a"
SWIFTLINT_SHA256_LINUX_X86_64="79306a34e5c7cc55a220cd108cbb861dcad5f10138dcdf261e2624ae8b0a486b"
SWIFTFORMAT_SHA256_LINUX_ARM64="42a35b557a6d56975fba3a48e78d39ab5388c8faac65d4819f25d3e20c7504c0"
SWIFTLINT_SHA256_LINUX_ARM64="12d3b84bc5b69ae13a99a5a5c79904f9ce25867f099f6368d0037854f9ee6c26"
log() { printf '%s\n' "$*"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
INSTALL_SWIFTFORMAT=false
INSTALL_SWIFTLINT=false
if [[ "$#" -eq 0 ]]; then
INSTALL_SWIFTFORMAT=true
INSTALL_SWIFTLINT=true
else
for tool in "$@"; do
case "$tool" in
all)
INSTALL_SWIFTFORMAT=true
INSTALL_SWIFTLINT=true
;;
swiftformat)
INSTALL_SWIFTFORMAT=true
;;
swiftlint)
INSTALL_SWIFTLINT=true
;;
*)
fail "Unknown lint tool '${tool}'. Usage: $(basename "$0") [all|swiftformat|swiftlint]..."
;;
esac
done
fi
sha256_value() {
local path="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$path" | awk '{print $1}'
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" | awk '{print $1}'
return 0
fi
fail "Missing shasum/sha256sum."
}
download_file() {
local url="$1"
local out="$2"
curl -fL --retry 3 --retry-connrefused --retry-delay 2 -o "$out" "$url"
}
install_zip_binary() {
local label="$1"
local url="$2"
local expected_sha="$3"
local binary_name="$4"
local installed_name="${5:-$binary_name}"
local tmp_zip
tmp_zip="$(mktemp -t "${label}.XXXX")"
local tmp_dir
tmp_dir="$(mktemp -d -t "${label}.XXXX")"
log "==> Downloading ${label}"
download_file "$url" "$tmp_zip"
local actual_sha
actual_sha="$(sha256_value "$tmp_zip")"
if [[ -n "$expected_sha" && "$actual_sha" != "$expected_sha" ]]; then
rm -f "$tmp_zip"
rm -rf "$tmp_dir"
fail "${label} SHA256 mismatch (expected ${expected_sha}, got ${actual_sha})"
fi
unzip -q "$tmp_zip" -d "$tmp_dir"
local extracted_path=""
if [[ -f "${tmp_dir}/${binary_name}" ]]; then
extracted_path="${tmp_dir}/${binary_name}"
else
extracted_path="$(find "$tmp_dir" -type f -name "$binary_name" | head -n 1 || true)"
fi
if [[ -z "$extracted_path" || ! -f "$extracted_path" ]]; then
rm -f "$tmp_zip"
rm -rf "$tmp_dir"
fail "${label} binary '${binary_name}' not found in archive"
fi
install -m 0755 "$extracted_path" "${BIN_DIR}/${installed_name}"
rm -f "$tmp_zip"
rm -rf "$tmp_dir"
}
mkdir -p "$BIN_DIR"
swiftformat_installed() {
[[ -x "${BIN_DIR}/swiftformat" ]] \
&& [[ "$("${BIN_DIR}/swiftformat" --version 2>/dev/null || true)" == "${SWIFTFORMAT_VERSION}" ]]
}
swiftlint_installed() {
[[ -x "${BIN_DIR}/swiftlint" ]] \
&& [[ "$("${BIN_DIR}/swiftlint" version 2>/dev/null || true)" == "${SWIFTLINT_VERSION}" ]]
}
if { [[ "$INSTALL_SWIFTFORMAT" != true ]] || swiftformat_installed; } \
&& { [[ "$INSTALL_SWIFTLINT" != true ]] || swiftlint_installed; }
then
log "==> Requested lint tools already installed"
exit 0
fi
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Darwin)
SWIFTFORMAT_URL="https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFTFORMAT_VERSION}/swiftformat.zip"
SWIFTLINT_URL="https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip"
if [[ "$INSTALL_SWIFTFORMAT" == true ]] && ! swiftformat_installed; then
install_zip_binary "SwiftFormat ${SWIFTFORMAT_VERSION}" "$SWIFTFORMAT_URL" "$SWIFTFORMAT_SHA256_DARWIN" "swiftformat"
fi
if [[ "$INSTALL_SWIFTLINT" == true ]] && ! swiftlint_installed; then
install_zip_binary "SwiftLint ${SWIFTLINT_VERSION}" "$SWIFTLINT_URL" "$SWIFTLINT_SHA256_DARWIN" "swiftlint"
fi
;;
Linux)
case "$ARCH" in
x86_64)
SWIFTFORMAT_URL="https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFTFORMAT_VERSION}/swiftformat_linux.zip"
SWIFTLINT_URL="https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/swiftlint_linux_amd64.zip"
SWIFTFORMAT_BINARY="swiftformat_linux"
SWIFTFORMAT_SHA256="$SWIFTFORMAT_SHA256_LINUX_X86_64"
SWIFTLINT_SHA256="$SWIFTLINT_SHA256_LINUX_X86_64"
;;
aarch64|arm64)
SWIFTFORMAT_URL="https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFTFORMAT_VERSION}/swiftformat_linux_aarch64.zip"
SWIFTLINT_URL="https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/swiftlint_linux_arm64.zip"
SWIFTFORMAT_BINARY="swiftformat_linux_aarch64"
SWIFTFORMAT_SHA256="$SWIFTFORMAT_SHA256_LINUX_ARM64"
SWIFTLINT_SHA256="$SWIFTLINT_SHA256_LINUX_ARM64"
;;
*)
fail "Unsupported Linux arch: ${ARCH}"
;;
esac
if { [[ "$INSTALL_SWIFTFORMAT" == true ]] && [[ -z "$SWIFTFORMAT_SHA256" ]]; } \
|| { [[ "$INSTALL_SWIFTLINT" == true ]] && [[ -z "$SWIFTLINT_SHA256" ]]; }
then
log "WARN: Linux SHA256 verification not configured for ${ARCH}; installing anyway."
fi
if [[ "$INSTALL_SWIFTFORMAT" == true ]] && ! swiftformat_installed; then
install_zip_binary "SwiftFormat ${SWIFTFORMAT_VERSION}" "$SWIFTFORMAT_URL" "$SWIFTFORMAT_SHA256" "$SWIFTFORMAT_BINARY" "swiftformat"
fi
if [[ "$INSTALL_SWIFTLINT" == true ]] && ! swiftlint_installed; then
install_zip_binary "SwiftLint ${SWIFTLINT_VERSION}" "$SWIFTLINT_URL" "$SWIFTLINT_SHA256" "swiftlint"
fi
;;
*)
fail "Unsupported OS: ${OS}"
;;
esac
log "==> Installed lint tools to ${BIN_DIR}"
if [[ "$INSTALL_SWIFTFORMAT" == true ]]; then
"${BIN_DIR}/swiftformat" --version
fi
if [[ "$INSTALL_SWIFTLINT" == true ]]; then
"${BIN_DIR}/swiftlint" version
fi
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
# Simple script to launch CodexBar (kills existing instance first)
# Usage: ./Scripts/launch.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
APP_PATH="$PROJECT_ROOT/CodexBar.app"
echo "==> Killing existing CodexBar instances"
pkill -x CodexBar || pkill -f CodexBar.app || true
sleep 0.5
if [[ ! -d "$APP_PATH" ]]; then
echo "ERROR: CodexBar.app not found at $APP_PATH"
echo "Run ./Scripts/package_app.sh first to build the app"
exit 1
fi
echo "==> Launching CodexBar from $APP_PATH"
open -n "$APP_PATH"
# Wait a moment and check if it's running
sleep 1
if pgrep -x CodexBar > /dev/null; then
echo "OK: CodexBar is running."
else
echo "ERROR: App exited immediately. Check crash logs in Console.app (User Reports)."
exit 1
fi
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN_DIR="${ROOT_DIR}/.build/lint-tools/bin"
ensure_swiftformat() {
"${ROOT_DIR}/Scripts/install_lint_tools.sh" swiftformat
}
ensure_swiftlint() {
"${ROOT_DIR}/Scripts/install_lint_tools.sh" swiftlint
}
check_codex_parser_hash() {
"${ROOT_DIR}/Scripts/regenerate-codex-parser-hash.sh" --check
}
check_package_product_paths() {
"${ROOT_DIR}/Scripts/test_package_product_paths.sh"
}
check_package_strip() {
"${ROOT_DIR}/Scripts/test_package_strip.sh"
}
check_package_signing() {
"${ROOT_DIR}/Scripts/test_package_signing.sh"
}
check_release_dsym_paths() {
"${ROOT_DIR}/Scripts/test_release_dsym_paths.sh"
}
check_sparkle_signing_paths() {
"${ROOT_DIR}/Scripts/test_sparkle_signing_paths.sh"
}
check_swift_test_sharding() {
"${ROOT_DIR}/Scripts/test_swift_test_sharding.sh"
}
check_ci_path_gate() {
"${ROOT_DIR}/Scripts/test_ci_path_gate.sh"
}
check_repository_size() {
"${ROOT_DIR}/Scripts/check_repository_size.sh"
"${ROOT_DIR}/Scripts/test_repository_size.sh"
}
check_shell_scripts() {
local count=0
local script
for script in "${ROOT_DIR}"/Scripts/*.sh "${ROOT_DIR}"/Scripts/mac-release; do
[[ -f "$script" ]] || continue
bash -n "$script"
count=$((count + 1))
done
printf 'shell scripts OK: %d files\n' "$count"
}
check_app_locales() {
node "${ROOT_DIR}/Scripts/check-app-locales.mjs" --test
node "${ROOT_DIR}/Scripts/check-app-locales.mjs"
}
check_site_locales() {
node "${ROOT_DIR}/Scripts/check-site-locales.mjs"
node --check "${ROOT_DIR}/docs/site.js"
}
check_documentation_links() {
node "${ROOT_DIR}/Scripts/check-documentation-links.mjs"
}
check_llms_index() {
node "${ROOT_DIR}/Scripts/generate-llms.mjs" --check
}
run_portable_checks() {
check_codex_parser_hash
check_package_product_paths
check_package_strip
check_package_signing
check_release_dsym_paths
check_sparkle_signing_paths
check_swift_test_sharding
check_ci_path_gate
check_repository_size
check_shell_scripts
check_documentation_links
check_llms_index
check_site_locales
}
run_swiftformat_lint() {
ensure_swiftformat
"${BIN_DIR}/swiftformat" Sources Tests --lint
}
run_swiftlint() {
ensure_swiftlint
"${BIN_DIR}/swiftlint" --strict
}
cmd="${1:-lint}"
case "$cmd" in
lint)
check_app_locales
run_portable_checks
run_swiftformat_lint
run_swiftlint
;;
lint-linux)
run_portable_checks
run_swiftlint
;;
lint-macos)
check_app_locales
run_swiftformat_lint
;;
format)
ensure_swiftformat
"${BIN_DIR}/swiftformat" Sources Tests
;;
*)
printf 'Usage: %s [lint|lint-linux|lint-macos|format]\n' "$(basename "$0")" >&2
exit 2
;;
esac
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
cd "$ROOT"
if [[ -n "${MAC_RELEASE_TOOL:-}" ]]; then
exec "$MAC_RELEASE_TOOL" "$@"
fi
for candidate in \
"$ROOT/../agent-scripts/skills/release-mac-app/scripts/mac-release" \
"$HOME/Projects/agent-scripts/skills/release-mac-app/scripts/mac-release"; do
if [[ -x "$candidate" ]]; then
exec "$candidate" "$@"
fi
done
cat >&2 <<'EOF'
Missing mac-release helper.
Clone agent-scripts next to this repo or set MAC_RELEASE_TOOL=/path/to/mac-release.
EOF
exit 127
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
exec "$SCRIPT_DIR/mac-release" make-appcast "$@"
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
mimo-usage — local token usage tracker for cc-mimo
Scans ~/.claude-envs/mimo/.claude/projects/**/*.jsonl session files,
sums input/output/cache tokens per time window (today/week/all),
writes to ~/.codexbar/mimo-local-usage.json, and prints a human-readable
summary by default.
Usage:
mimo-usage # show summary (also refreshes cache)
mimo-usage --update # refresh cache only, no output (for LaunchAgent/wrapper)
mimo-usage --json # JSON output
mimo-usage --short # 1-line status (for status line / widget)
"""
import json
import os
import sys
from pathlib import Path
from datetime import datetime, timedelta, timezone
MIMO_HOME = Path(os.environ.get("MIMO_CLAUDE_HOME", Path.home() / ".claude-envs" / "mimo")).expanduser()
PROJECTS_DIR = MIMO_HOME / ".claude" / "projects"
CACHE_PATH = Path(
os.environ.get("MIMO_LOCAL_USAGE_PATH", Path.home() / ".codexbar" / "mimo-local-usage.json")
).expanduser()
def parse_session_usage(jsonl_path: Path):
"""Yield (identity, timestamp_iso, usage_dict) for each assistant message with usage."""
try:
with jsonl_path.open() as f:
for line in f:
try:
d = json.loads(line)
ts = d.get("timestamp")
msg = d.get("message")
if not isinstance(msg, dict):
continue
usage = msg.get("usage")
if not isinstance(usage, dict):
continue
if not ts:
continue
metadata = d.get("metadata")
message_metadata = msg.get("metadata")
session_id = d.get("sessionId") or d.get("session_id")
if not session_id and isinstance(metadata, dict):
session_id = metadata.get("sessionId")
if not session_id and isinstance(message_metadata, dict):
session_id = message_metadata.get("sessionId")
message_id = msg.get("id")
request_id = d.get("requestId") or d.get("request_id")
identity = None
if all(isinstance(value, str) and value for value in (message_id, request_id)):
identity = ("request", message_id, request_id)
elif (
request_id is None
and isinstance(session_id, str)
and session_id
and isinstance(message_id, str)
and message_id
):
identity = ("legacy", session_id, message_id)
yield identity, ts, usage
except (json.JSONDecodeError, ValueError):
continue
except (OSError, IOError):
return
def aggregate_usage():
"""Scan all mimo session jsonls and return windowed token sums."""
now = datetime.now(timezone.utc)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
# Week starts on Monday 00:00 UTC
week_start = today_start - timedelta(days=today_start.weekday())
windows = {
"today": {"input": 0, "output": 0, "cache_read": 0, "cache_create": 0, "messages": 0},
"week": {"input": 0, "output": 0, "cache_read": 0, "cache_create": 0, "messages": 0},
"all_time": {"input": 0, "output": 0, "cache_read": 0, "cache_create": 0, "messages": 0},
}
sessions_scanned = 0
last_activity = None
keyed_rows = {}
unkeyed_rows = []
if not PROJECTS_DIR.exists():
return windows, sessions_scanned, last_activity
for jsonl in PROJECTS_DIR.rglob("*.jsonl"):
sessions_scanned += 1
for identity, ts_str, usage in parse_session_usage(jsonl):
try:
# Parse ISO timestamp (may end with Z)
ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
except (ValueError, TypeError):
continue
row = (ts, usage)
if identity is None:
unkeyed_rows.append(row)
else:
previous = keyed_rows.get(identity)
if previous is None or ts >= previous[0]:
keyed_rows[identity] = row
for ts, usage in [*keyed_rows.values(), *unkeyed_rows]:
input_t = int(usage.get("input_tokens", 0) or 0)
output_t = int(usage.get("output_tokens", 0) or 0)
cache_read_t = int(usage.get("cache_read_input_tokens", 0) or 0)
cache_create_t = int(usage.get("cache_creation_input_tokens", 0) or 0)
if last_activity is None or ts > last_activity:
last_activity = ts
# all_time
w = windows["all_time"]
w["input"] += input_t
w["output"] += output_t
w["cache_read"] += cache_read_t
w["cache_create"] += cache_create_t
w["messages"] += 1
if ts >= week_start:
w = windows["week"]
w["input"] += input_t
w["output"] += output_t
w["cache_read"] += cache_read_t
w["cache_create"] += cache_create_t
w["messages"] += 1
if ts >= today_start:
w = windows["today"]
w["input"] += input_t
w["output"] += output_t
w["cache_read"] += cache_read_t
w["cache_create"] += cache_create_t
w["messages"] += 1
return windows, sessions_scanned, last_activity
def write_cache(windows, sessions_scanned, last_activity):
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
payload = {
"updated_at": datetime.now(timezone.utc).isoformat(),
"last_activity": last_activity.isoformat() if last_activity else None,
"sessions_scanned": sessions_scanned,
"windows": windows,
"source": "local-jsonl-scan",
"note": "Local token accounting from cc-mimo session jsonl. Not a quota; mimo platform.xiaomimimo.com SSO cookie required for real quota.",
}
tmp = CACHE_PATH.with_suffix(".json.tmp")
tmp.write_text(json.dumps(payload, indent=2))
tmp.replace(CACHE_PATH)
return payload
def fmt_tokens(n: int) -> str:
if n >= 1_000_000:
return f"{n / 1_000_000:.1f}M"
if n >= 1_000:
return f"{n / 1_000:.1f}k"
return str(n)
def short_status(payload):
"""1-line status line."""
w = payload["windows"]["week"]
total = w["input"] + w["output"] + w["cache_read"] + w["cache_create"]
return f"mimo: {fmt_tokens(total)} tok this week ({w['messages']} msg)"
def human_summary(payload):
"""Multi-line human-readable summary."""
last = payload.get("last_activity")
if last:
try:
last_dt = datetime.fromisoformat(last)
ago = datetime.now(timezone.utc) - last_dt
if ago.total_seconds() < 60:
ago_str = "just now"
elif ago.total_seconds() < 3600:
ago_str = f"{int(ago.total_seconds() / 60)}m ago"
elif ago.total_seconds() < 86400:
ago_str = f"{int(ago.total_seconds() / 3600)}h ago"
else:
ago_str = f"{ago.days}d ago"
except (ValueError, TypeError):
ago_str = last
else:
ago_str = "never"
lines = [
"== MiMo (local tracker) ==",
f"Sessions scanned: {payload['sessions_scanned']}",
f"Last activity: {ago_str}",
"",
]
for window_name, label in [("today", "Today"), ("week", "This week"), ("all_time", "All time")]:
w = payload["windows"][window_name]
in_t = fmt_tokens(w["input"])
out_t = fmt_tokens(w["output"])
cr_t = fmt_tokens(w["cache_read"])
cc_t = fmt_tokens(w["cache_create"])
total = w["input"] + w["output"] + w["cache_read"] + w["cache_create"]
lines.append(f"{label:>10}: {fmt_tokens(total):>8} total | in={in_t} out={out_t} cache_r={cr_t} cache_c={cc_t} | msg={w['messages']}")
lines.append("")
lines.append("Note: this is local accounting from cc-mimo session jsonl.")
lines.append("Real platform quota requires Chrome cookie (cookieSource=manual).")
return "\n".join(lines)
def main():
args = sys.argv[1:]
quiet = "--update" in args
json_out = "--json" in args
short = "--short" in args
windows, sessions_scanned, last_activity = aggregate_usage()
payload = write_cache(windows, sessions_scanned, last_activity)
if quiet:
return 0
if json_out:
print(json.dumps(payload, indent=2))
return 0
if short:
print(short_status(payload))
return 0
print(human_summary(payload))
return 0
if __name__ == "__main__":
sys.exit(main())
+541
View File
@@ -0,0 +1,541 @@
#!/usr/bin/env bash
set -euo pipefail
resolve_package_signing_mode() {
local requested="${CODEXBAR_SIGNING:-adhoc}"
case "$requested" in
adhoc|identity) ;;
*)
echo "ERROR: Unsupported CODEXBAR_SIGNING: $requested (expected adhoc or identity)" >&2
return 1
;;
esac
SIGNING_MODE="$requested"
}
CONF=${1:-release}
ALLOW_LLDB=${CODEXBAR_ALLOW_LLDB:-0}
SIGNING_MODE=
resolve_package_signing_mode
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
LOWER_CONF=$(printf "%s" "$CONF" | tr '[:upper:]' '[:lower:]')
case "$LOWER_CONF" in
debug|release) ;;
*)
echo "ERROR: Unsupported build configuration: $CONF (expected debug or release)" >&2
exit 1
;;
esac
# Load version info
source "$ROOT/version.env"
source "$ROOT/Scripts/package_product_paths.sh"
source "$ROOT/Scripts/sparkle_signing_paths.sh"
# Clean build only when explicitly requested (slower).
if [[ "${CODEXBAR_FORCE_CLEAN:-0}" == "1" ]]; then
if [[ -d "$ROOT/.build" ]]; then
if command -v trash >/dev/null 2>&1; then
if ! trash "$ROOT/.build"; then
echo "WARN: trash .build failed; continuing with swift package clean." >&2
fi
else
rm -rf "$ROOT/.build" || echo "WARN: rm -rf .build failed; continuing with swift package clean." >&2
fi
fi
swift package clean >/dev/null 2>&1 || true
fi
# Build for host architecture by default; allow overriding via ARCHES (e.g., "arm64 x86_64" for universal).
ARCH_LIST=( ${ARCHES:-} )
if [[ ${#ARCH_LIST[@]} -eq 0 ]]; then
HOST_ARCH=$(uname -m)
case "$HOST_ARCH" in
arm64) ARCH_LIST=(arm64) ;;
x86_64) ARCH_LIST=(x86_64) ;;
*) ARCH_LIST=("$HOST_ARCH") ;;
esac
fi
patch_keyboard_shortcuts() {
local util_path="$ROOT/.build/checkouts/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift"
if [[ ! -f "$util_path" ]]; then
return 0
fi
if grep -q "keyboardShortcutsSafeBundle" "$util_path"; then
return 0
fi
chmod +w "$util_path" || true
python3 - "$util_path" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text()
if ".keyboardShortcutsSafeBundle" in text:
sys.exit(0)
text = text.replace(
'NSLocalizedString(self, bundle: .module, comment: self)',
'NSLocalizedString(self, bundle: .keyboardShortcutsSafeBundle, comment: self)',
)
inject = """
private extension Bundle {
/// Safe lookup that avoids the fatal trap in the autogenerated `Bundle.module`
/// when the resource bundle is not placed at the bundle root.
static let keyboardShortcutsSafeBundle: Bundle = {
#if os(macOS)
if let url = Bundle.main.url(forResource: "KeyboardShortcuts_KeyboardShortcuts", withExtension: "bundle"),
let bundle = Bundle(url: url) {
return bundle
}
let rootURL = Bundle.main.bundleURL.appendingPathComponent("KeyboardShortcuts_KeyboardShortcuts.bundle")
if let bundle = Bundle(url: rootURL) {
return bundle
}
#endif
let devURL = URL(fileURLWithPath: #file)
.deletingLastPathComponent() // Utilities.swift
.deletingLastPathComponent() // KeyboardShortcuts
.deletingLastPathComponent() // Sources
.appendingPathComponent("KeyboardShortcuts_KeyboardShortcuts.bundle")
if let bundle = Bundle(url: devURL) {
return bundle
}
return Bundle.main
}()
}
"""
marker = "}\n\n\nextension Data {"
if marker not in text:
raise SystemExit("Marker not found in Utilities.swift; patch failed.")
text = text.replace(marker, "}\n\n" + inject + "\n\nextension Data {")
path.write_text(text)
PY
}
KEYBOARD_SHORTCUTS_UTIL="$ROOT/.build/checkouts/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift"
if [[ ! -f "$KEYBOARD_SHORTCUTS_UTIL" ]]; then
swift build -c "$CONF" --arch "${ARCH_LIST[0]}"
fi
patch_keyboard_shortcuts
# Resolve SwiftPM's current output path without relying on a fixed build-system layout.
# The output variable keeps the per-arch cache in this shell instead of losing it to
# command substitution.
swiftpm_bin_path() {
local arch="$1"
local output_var="$2"
local cache_var="SWIFTPM_BIN_PATH_${arch//[^A-Za-z0-9]/_}"
if [[ -z "${!cache_var+set}" ]]; then
local resolved
if ! resolved=$(codexbar_swiftpm_bin_path "$CONF" "$arch"); then
return 1
fi
printf -v "$cache_var" '%s' "$resolved"
fi
printf -v "$output_var" '%s' "${!cache_var}"
}
binary_has_arch() {
local binary="$1"
local arch="$2"
[[ -f "$binary" ]] && lipo -archs "$binary" 2>/dev/null | tr ' ' '\n' | grep -qx "$arch"
}
# SwiftBuild can reuse one output directory for sequential per-arch builds. Snapshot
# each fresh slice before the next build can replace it.
PRODUCT_STAGE_ROOT="$ROOT/.build/package-products/$LOWER_CONF"
rm -rf "$PRODUCT_STAGE_ROOT"
stage_build_products() {
local arch="$1"
local bin_dir stage_dir name product
swiftpm_bin_path "$arch" bin_dir
stage_dir="$PRODUCT_STAGE_ROOT/$arch"
mkdir -p "$stage_dir"
for name in CodexBar CodexBarCLI CodexBarClaudeWatchdog; do
if ! product=$(codexbar_require_product_file "$bin_dir" "$name" "$arch"); then
return 1
fi
if ! binary_has_arch "$product" "$arch"; then
echo "ERROR: ${product} does not contain required architecture: ${arch}" >&2
return 1
fi
cp "$product" "$stage_dir/$name"
done
if [[ -d "$bin_dir/CodexBar.dSYM" ]]; then
cp -R "$bin_dir/CodexBar.dSYM" "$stage_dir/"
fi
}
for ARCH in "${ARCH_LIST[@]}"; do
swift build -c "$CONF" --arch "$ARCH"
stage_build_products "$ARCH"
done
APP_FINAL="$ROOT/CodexBar.app"
APP_STAGE="$ROOT/.build/package/CodexBar.app"
rm -rf "$APP_STAGE"
APP="$APP_STAGE"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/Frameworks"
mkdir -p "$APP/Contents/Helpers" "$APP/Contents/PlugIns"
# Convert new .icon bundle to .icns if present (macOS 14+/IconStudio export)
ICON_SOURCE="$ROOT/Icon.icon"
ICON_TARGET="$ROOT/Icon.icns"
if [[ -f "$ICON_SOURCE" ]]; then
iconutil --convert icns --output "$ICON_TARGET" "$ICON_SOURCE"
fi
BUNDLE_ID="com.steipete.codexbar"
FEED_URL="https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml"
AUTO_CHECKS=true
if [[ "$LOWER_CONF" == "debug" ]]; then
BUNDLE_ID="com.steipete.codexbar.debug"
FEED_URL=""
AUTO_CHECKS=false
fi
if [[ "$SIGNING_MODE" == "adhoc" ]]; then
FEED_URL=""
AUTO_CHECKS=false
fi
WIDGET_BUNDLE_ID="${BUNDLE_ID}.widget"
APP_TEAM_ID="${APP_TEAM_ID:-Y5PE65HELJ}"
APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar"
if [[ "$BUNDLE_ID" == *".debug"* ]]; then
APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar.debug"
fi
ENTITLEMENTS_DIR="$ROOT/.build/entitlements"
APP_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBar.entitlements"
WIDGET_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBarWidget.entitlements"
mkdir -p "$ENTITLEMENTS_DIR"
if [[ "$ALLOW_LLDB" == "1" && "$LOWER_CONF" != "debug" ]]; then
echo "ERROR: CODEXBAR_ALLOW_LLDB requires debug configuration" >&2
exit 1
fi
cat > "$APP_ENTITLEMENTS" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>${APP_GROUP_ID}</string>
</array>
$(if [[ "$ALLOW_LLDB" == "1" ]]; then echo " <key>com.apple.security.get-task-allow</key><true/>"; fi)
</dict>
</plist>
PLIST
cat > "$WIDGET_ENTITLEMENTS" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>${APP_GROUP_ID}</string>
</array>
</dict>
</plist>
PLIST
BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
cat > "$APP/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>CodexBar</string>
<key>CFBundleDisplayName</key><string>CodexBar</string>
<key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
<key>CFBundleExecutable</key><string>CodexBar</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>${MARKETING_VERSION}</string>
<key>CFBundleVersion</key><string>${BUILD_NUMBER}</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>LSUIElement</key><true/>
<key>CFBundleIconFile</key><string>Icon</string>
<key>NSHumanReadableCopyright</key><string>© 2026 Peter Steinberger. MIT License.</string>
<key>SUFeedURL</key><string>${FEED_URL}</string>
<key>SUPublicEDKey</key><string>AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=</string>
<key>SUEnableAutomaticChecks</key><${AUTO_CHECKS}/>
<key>CodexBuildTimestamp</key><string>${BUILD_TIMESTAMP}</string>
<key>CodexGitCommit</key><string>${GIT_COMMIT}</string>
<key>CodexBarTeamID</key><string>${APP_TEAM_ID}</string>
</dict>
</plist>
PLIST
# Resolve a built binary from the fresh per-arch snapshot or SwiftPM's reported directory.
resolve_binary_path() {
local name="$1"
local arch="$2"
local bin_dir candidate
swiftpm_bin_path "$arch" bin_dir
if ! candidate=$(codexbar_resolve_staged_or_reported_file \
"$PRODUCT_STAGE_ROOT" "$bin_dir" "$name" "$arch"); then
return 1
fi
if ! binary_has_arch "$candidate" "$arch"; then
echo "ERROR: ${candidate} does not contain required architecture: ${arch}" >&2
return 1
fi
echo "$candidate"
}
verify_binary_arches() {
local binary="$1"; shift
local expected=("$@")
local actual
actual=$(lipo -archs "$binary")
local actual_count expected_count
actual_count=$(wc -w <<<"$actual" | tr -d ' ')
expected_count=${#expected[@]}
if [[ "$actual_count" -ne "$expected_count" ]]; then
echo "ERROR: $binary arch mismatch (expected: ${expected[*]}, actual: ${actual})" >&2
exit 1
fi
for arch in "${expected[@]}"; do
if [[ "$actual" != *"$arch"* ]]; then
echo "ERROR: $binary missing arch $arch (have: ${actual})" >&2
exit 1
fi
done
}
install_binary() {
local name="$1"
local dest="$2"
local binaries=()
for arch in "${ARCH_LIST[@]}"; do
local src
if ! src=$(resolve_binary_path "$name" "$arch"); then
exit 1
fi
binaries+=("$src")
done
if [[ ${#ARCH_LIST[@]} -gt 1 ]]; then
lipo -create "${binaries[@]}" -output "$dest"
else
cp "${binaries[0]}" "$dest"
fi
chmod +x "$dest"
verify_binary_arches "$dest" "${ARCH_LIST[@]}"
}
strip_release_binary() {
local binary="$1"
if [[ "$LOWER_CONF" != "release" ]]; then
return 0
fi
if [[ ! -f "$binary" ]]; then
return 0
fi
xcrun strip -x "$binary"
}
ensure_widget_extension_project() {
local spec="$ROOT/WidgetExtension/project.yml"
local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj"
if [[ -f "$project_dir/project.pbxproj" ]]; then
return
fi
if ! command -v xcodegen >/dev/null 2>&1; then
echo "ERROR: Missing ${project_dir}; install xcodegen or restore the generated project." >&2
exit 1
fi
# The tracked project is authoritative. Regenerating it during packaging records the checkout
# directory's spelling in a package file reference and leaves release worktrees dirty.
xcodegen generate --spec "$spec" --project "$ROOT/WidgetExtension" --quiet
}
build_widget_extension() {
local xcode_conf="Release"
if [[ "$LOWER_CONF" == "debug" ]]; then
xcode_conf="Debug"
fi
ensure_widget_extension_project
local derived_dir="$ROOT/.build/xcode-widget-extension-${LOWER_CONF}"
local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj"
local build_log="$derived_dir/xcodebuild.log"
local timeout_seconds="${CODEXBAR_WIDGET_EXTENSION_TIMEOUT_SECONDS:-900}"
local archs="${ARCH_LIST[*]}"
mkdir -p "$derived_dir"
echo "Building CodexBarWidget Xcode extension (${xcode_conf}, ${archs})." >&2
xcodebuild \
-project "$project_dir" \
-scheme CodexBarWidgetExtension \
-configuration "$xcode_conf" \
-destination "generic/platform=macOS" \
-derivedDataPath "$derived_dir" \
-skipPackageUpdates \
-disableAutomaticPackageResolution \
-skipMacroValidation \
-skipPackagePluginValidation \
CODEXBAR_WIDGET_BUNDLE_ID="$WIDGET_BUNDLE_ID" \
CODEXBAR_TEAM_ID="$APP_TEAM_ID" \
MARKETING_VERSION="$MARKETING_VERSION" \
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \
CODE_SIGNING_ALLOWED=NO \
ARCHS="$archs" \
ONLY_ACTIVE_ARCH=NO \
build >"$build_log" 2>&1 &
local xcodebuild_pid=$!
local elapsed=0
while kill -0 "$xcodebuild_pid" 2>/dev/null; do
if [[ "$elapsed" -ge "$timeout_seconds" ]]; then
kill "$xcodebuild_pid" 2>/dev/null || true
wait "$xcodebuild_pid" 2>/dev/null || true
tail -80 "$build_log" >&2 || true
echo "ERROR: Timed out building CodexBarWidget extension after ${timeout_seconds}s" >&2
exit 1
fi
sleep 5
elapsed=$((elapsed + 5))
if (( elapsed > 0 && elapsed % 60 == 0 )); then
echo "Still building CodexBarWidget extension (${elapsed}s)..." >&2
fi
done
if ! wait "$xcodebuild_pid"; then
tail -120 "$build_log" >&2 || true
echo "ERROR: Failed to build CodexBarWidget extension" >&2
exit 1
fi
local appex="$derived_dir/Build/Products/${xcode_conf}/CodexBarWidget.appex"
if [[ ! -f "$appex/Contents/MacOS/CodexBarWidget" ]]; then
echo "ERROR: Missing Xcode-built CodexBarWidget.appex at ${appex}" >&2
exit 1
fi
echo "$appex"
}
install_widget_extension() {
local src_appex
src_appex="$(build_widget_extension)"
local widget_app="$APP/Contents/PlugIns/CodexBarWidget.appex"
rm -rf "$widget_app"
mkdir -p "$APP/Contents/PlugIns"
cp -R "$src_appex" "$widget_app"
verify_binary_arches "$widget_app/Contents/MacOS/CodexBarWidget" "${ARCH_LIST[@]}"
}
install_binary "CodexBar" "$APP/Contents/MacOS/CodexBar"
strip_release_binary "$APP/Contents/MacOS/CodexBar"
# Ship CodexBarCLI alongside the app for easy symlinking.
install_binary "CodexBarCLI" "$APP/Contents/Helpers/CodexBarCLI"
strip_release_binary "$APP/Contents/Helpers/CodexBarCLI"
# Watchdog helper: ensures `claude` probes die when CodexBar crashes/gets killed.
install_binary "CodexBarClaudeWatchdog" "$APP/Contents/Helpers/CodexBarClaudeWatchdog"
strip_release_binary "$APP/Contents/Helpers/CodexBarClaudeWatchdog"
install_widget_extension
strip_release_binary "$APP/Contents/PlugIns/CodexBarWidget.appex/Contents/MacOS/CodexBarWidget"
swiftpm_bin_path "${ARCH_LIST[0]}" PREFERRED_BUILD_DIR
# Embed Sparkle.framework
SPARKLE_SOURCE=$(codexbar_require_product_directory "$PREFERRED_BUILD_DIR" Sparkle.framework packaging)
cp -R "$SPARKLE_SOURCE" "$APP/Contents/Frameworks/"
chmod -R a+rX "$APP/Contents/Frameworks/Sparkle.framework"
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/CodexBar"
# Re-sign Sparkle and all nested components with the selected package identity.
SPARKLE="$APP/Contents/Frameworks/Sparkle.framework"
if [[ "$SIGNING_MODE" == "adhoc" ]]; then
CODESIGN_ID="-"
CODESIGN_ARGS=(--force --sign "$CODESIGN_ID")
elif [[ "$ALLOW_LLDB" == "1" ]]; then
CODESIGN_ID="-"
CODESIGN_ARGS=(--force --sign "$CODESIGN_ID")
else
CODESIGN_ID="${APP_IDENTITY:-Developer ID Application: Peter Steinberger (Y5PE65HELJ)}"
CODESIGN_ARGS=(--force --timestamp --options runtime --sign "$CODESIGN_ID")
fi
function resign() { codesign "${CODESIGN_ARGS[@]}" "$1"; }
# Validate Sparkle's nested layout before signing so framework layout drift fails clearly.
SPARKLE_SIGNING_TARGETS=$(codexbar_sparkle_signing_targets "$SPARKLE")
while IFS= read -r SPARKLE_TARGET; do
resign "$SPARKLE_TARGET"
done <<<"$SPARKLE_SIGNING_TARGETS"
if [[ -f "$ICON_TARGET" ]]; then
cp "$ICON_TARGET" "$APP/Contents/Resources/Icon.icns"
fi
# Bundle app resources (provider icons, etc.).
APP_RESOURCES_DIR="$ROOT/Sources/CodexBar/Resources"
if [[ -d "$APP_RESOURCES_DIR" ]]; then
cp -R "$APP_RESOURCES_DIR/." "$APP/Contents/Resources/"
fi
if [[ ! -f "$APP/Contents/Resources/Icon-classic.icns" ]]; then
echo "ERROR: Missing Icon-classic.icns in app bundle resources." >&2
exit 1
fi
# SwiftPM resource bundles (e.g. KeyboardShortcuts) are emitted next to the built binary.
shopt -s nullglob
SWIFTPM_BUNDLES=("${PREFERRED_BUILD_DIR}/"*.bundle)
shopt -u nullglob
if [[ ${#SWIFTPM_BUNDLES[@]} -gt 0 ]]; then
for bundle in "${SWIFTPM_BUNDLES[@]}"; do
bundle_name="$(basename "$bundle")"
cp -R "$bundle" "$APP/Contents/Resources/"
done
fi
if [[ ! -d "$APP/Contents/Resources/KeyboardShortcuts_KeyboardShortcuts.bundle" ]]; then
echo "ERROR: Missing KeyboardShortcuts SwiftPM resource bundle (Settings → Keyboard shortcut will crash)." >&2
echo "Expected: ${PREFERRED_BUILD_DIR}/KeyboardShortcuts_KeyboardShortcuts.bundle" >&2
exit 1
fi
# Ensure contents are writable before stripping attributes and signing.
chmod -R u+w "$APP"
# Strip extended attributes to prevent AppleDouble (._*) files that break code sealing
xattr -cr "$APP"
find "$APP" -name '._*' -delete
# Sign helper binaries if present
if [[ -f "${APP}/Contents/Helpers/CodexBarCLI" ]]; then
codesign "${CODESIGN_ARGS[@]}" "${APP}/Contents/Helpers/CodexBarCLI"
fi
if [[ -f "${APP}/Contents/Helpers/CodexBarClaudeWatchdog" ]]; then
codesign "${CODESIGN_ARGS[@]}" "${APP}/Contents/Helpers/CodexBarClaudeWatchdog"
fi
# Sign widget extension if present
if [[ -d "${APP}/Contents/PlugIns/CodexBarWidget.appex" ]]; then
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$WIDGET_ENTITLEMENTS" \
"$APP/Contents/PlugIns/CodexBarWidget.appex/Contents/MacOS/CodexBarWidget"
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$WIDGET_ENTITLEMENTS" \
"$APP/Contents/PlugIns/CodexBarWidget.appex"
fi
# Finally sign the app bundle itself
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$APP_ENTITLEMENTS" \
"$APP"
rm -rf "$APP_FINAL"
mv "$APP" "$APP_FINAL"
APP="$APP_FINAL"
echo "Created $APP"
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
codexbar_swiftpm_bin_path() {
local conf="$1"
shift
local command=(swift build --show-bin-path -c "$conf")
local arch
for arch in "$@"; do
command+=(--arch "$arch")
done
local path
if ! path=$("${command[@]}"); then
echo "ERROR: SwiftPM failed to report the ${conf} product directory for: $*" >&2
return 1
fi
if [[ -z "$path" ]]; then
echo "ERROR: SwiftPM reported an empty ${conf} product directory for: $*" >&2
return 1
fi
printf '%s\n' "$path"
}
codexbar_require_product_file() {
local bin_dir="$1"
local name="$2"
local arch_label="$3"
local product="$bin_dir/$name"
if [[ ! -f "$product" ]]; then
echo "ERROR: Missing ${name} for ${arch_label} at SwiftPM-reported path: ${product}" >&2
return 1
fi
printf '%s\n' "$product"
}
codexbar_require_product_directory() {
local bin_dir="$1"
local name="$2"
local context="$3"
local product="$bin_dir/$name"
if [[ ! -d "$product" ]]; then
echo "ERROR: Missing ${name} for ${context} at SwiftPM-reported path: ${product}" >&2
return 1
fi
printf '%s\n' "$product"
}
codexbar_resolve_staged_or_reported_file() {
local stage_root="$1"
local bin_dir="$2"
local name="$3"
local arch="$4"
local staged="$stage_root/$arch/$name"
if [[ -f "$staged" ]]; then
printf '%s\n' "$staged"
return
fi
codexbar_require_product_file "$bin_dir" "$name" "$arch"
}
codexbar_resolve_dsym_path() {
local stage_root="$1"
local bin_dir="$2"
local app_name="$3"
local arch="$4"
local staged="$stage_root/$arch/${app_name}.dSYM"
if [[ -d "$staged" ]]; then
printf '%s\n' "$staged"
return
fi
codexbar_require_product_directory "$bin_dir" "${app_name}.dSYM" "$arch"
}
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# Prepare a clean branch for upstream PR submission
# Usage: ./Scripts/prepare_upstream_pr.sh <feature-name>
set -e
FEATURE_NAME=$1
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
if [ -z "$FEATURE_NAME" ]; then
echo -e "${RED}Error: Feature name required${NC}"
echo "Usage: ./Scripts/prepare_upstream_pr.sh <feature-name>"
echo ""
echo "Examples:"
echo " ./Scripts/prepare_upstream_pr.sh fix-cursor-bonus"
echo " ./Scripts/prepare_upstream_pr.sh improve-cookie-handling"
exit 1
fi
BRANCH_NAME="upstream-pr/$FEATURE_NAME"
echo -e "${BLUE}==> Fetching latest upstream...${NC}"
git fetch upstream
echo -e "${BLUE}==> Creating upstream PR branch from upstream/main...${NC}"
git checkout upstream/main
git checkout -b "$BRANCH_NAME"
echo ""
echo -e "${GREEN}==> Branch created: $BRANCH_NAME${NC}"
echo ""
echo -e "${YELLOW}⚠️ IMPORTANT: This branch is for UPSTREAM submission${NC}"
echo ""
echo -e "${BLUE}Guidelines for upstream PRs:${NC}"
echo ""
echo "✅ DO include:"
echo " - Bug fixes that affect all users"
echo " - Performance improvements"
echo " - Provider enhancements (generic)"
echo " - Documentation improvements"
echo " - Test coverage"
echo ""
echo "❌ DO NOT include:"
echo " - Fork branding (About.swift, PreferencesAboutPane.swift)"
echo " - Fork-specific features (multi-account, etc.)"
echo " - References to topoffunnel.com"
echo " - Experimental features"
echo ""
echo -e "${BLUE}Next steps:${NC}"
echo ""
echo "1. Cherry-pick your commits (clean, no fork branding):"
echo " ${GREEN}git cherry-pick <commit-hash>${NC}"
echo ""
echo "2. Or manually apply changes:"
echo " ${GREEN}# Edit files${NC}"
echo " ${GREEN}git add <files>${NC}"
echo " ${GREEN}git commit -m 'fix: description'${NC}"
echo ""
echo "3. Ensure tests pass:"
echo " ${GREEN}make test${NC}"
echo ""
echo "4. Review changes:"
echo " ${GREEN}git diff upstream/main${NC}"
echo ""
echo "5. Push to your fork:"
echo " ${GREEN}git push origin $BRANCH_NAME${NC}"
echo ""
echo "6. Create PR on GitHub:"
echo " ${GREEN}https://github.com/steipete/CodexBar/compare/main...topoffunnel:$BRANCH_NAME${NC}"
echo ""
echo -e "${YELLOW}Remember: Keep PRs small and focused for better merge chances!${NC}"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SOURCE_DIR="${ROOT_DIR}/Sources/CodexBarCore/Vendored/CostUsage"
OUTPUT_FILE="${ROOT_DIR}/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift"
MODE="${1:-write}"
case "$MODE" in
write | --write)
CHECK_ONLY=0
;;
check | --check)
CHECK_ONLY=1
;;
*)
echo "Usage: $0 [write|--write|check|--check]" >&2
exit 2
;;
esac
if command -v shasum >/dev/null 2>&1; then
HASH_CMD=(shasum -a 256)
elif command -v sha256sum >/dev/null 2>&1; then
HASH_CMD=(sha256sum)
else
echo "error: shasum or sha256sum is required" >&2
exit 1
fi
FILE_LIST="$(mktemp)"
trap 'rm -f "$FILE_LIST"' EXIT
find "$SOURCE_DIR" \
-type f \
-name '*.swift' \
! -name '*Claude*' \
-print |
sed "s#^${ROOT_DIR}/##" |
LC_ALL=C sort >"$FILE_LIST"
HASH="$(
while IFS= read -r file; do
printf '== %s ==\n' "$file"
cat "${ROOT_DIR}/${file}"
printf '\n'
done <"$FILE_LIST" | "${HASH_CMD[@]}"
)"
HASH="${HASH%% *}"
SHORT_HASH="${HASH:0:16}"
render_generated() {
cat <<SWIFT
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.
enum CodexParserHash {
static let value = "$SHORT_HASH"
}
SWIFT
}
if [[ "$CHECK_ONLY" -eq 1 ]]; then
EXPECTED_FILE="$(mktemp)"
trap 'rm -f "$FILE_LIST" "$EXPECTED_FILE"' EXIT
render_generated >"$EXPECTED_FILE"
if ! cmp -s "$EXPECTED_FILE" "$OUTPUT_FILE"; then
echo "error: ${OUTPUT_FILE#${ROOT_DIR}/} is stale. Run Scripts/regenerate-codex-parser-hash.sh and commit the result." >&2
if [[ -f "$OUTPUT_FILE" ]]; then
diff -u "$OUTPUT_FILE" "$EXPECTED_FILE" >&2 || true
else
diff -u /dev/null "$EXPECTED_FILE" >&2 || true
fi
exit 1
fi
echo "Codex parser hash is current (${SHORT_HASH})"
exit 0
fi
mkdir -p "$(dirname "$OUTPUT_FILE")"
render_generated >"$OUTPUT_FILE"
echo "Updated ${OUTPUT_FILE#${ROOT_DIR}/} to ${SHORT_HASH}"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
exec "$SCRIPT_DIR/mac-release" release "$@"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
codexbar_release_arch_label() {
local raw="${1:-arm64 x86_64}"
local normalized
local has_arm64=0
local has_x86_64=0
local arch
normalized=$(printf "%s" "$raw" | tr ',' ' ')
for arch in $normalized; do
case "$arch" in
arm64) has_arm64=1 ;;
x86_64) has_x86_64=1 ;;
esac
done
if [[ "$has_arm64" == "1" && "$has_x86_64" == "1" ]]; then
printf "macos-universal"
return
fi
if [[ "$has_arm64" == "1" ]]; then
printf "macos-arm64"
return
fi
if [[ "$has_x86_64" == "1" ]]; then
printf "macos-x86_64"
return
fi
printf "macos-%s" "$(printf "%s" "$normalized" | tr ' ' '+')"
}
codexbar_app_zip_name() {
local version=$1
local arches="${2:-arm64 x86_64}"
printf "CodexBar-%s-%s.zip" "$(codexbar_release_arch_label "$arches")" "$version"
}
codexbar_dsym_zip_name() {
local version=$1
local arches="${2:-arm64 x86_64}"
printf "CodexBar-%s-%s.dSYM.zip" "$(codexbar_release_arch_label "$arches")" "$version"
}
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
codexbar_dsym_dwarf_path() {
local dsym_path="$1"
local app_name="$2"
local dwarf_path="${dsym_path}/Contents/Resources/DWARF/${app_name}"
if [[ ! -f "$dwarf_path" ]]; then
echo "Missing fresh dSYM for ${app_name} at: ${dwarf_path}" >&2
return 1
fi
printf '%s\n' "$dwarf_path"
}
codexbar_require_dsym_dwarf_for_arch() {
local dsym_path="$1"
local app_name="$2"
local arch="$3"
local dwarf_path
if ! dwarf_path=$(codexbar_dsym_dwarf_path "$dsym_path" "$app_name"); then
return 1
fi
if ! lipo -archs "$dwarf_path" | tr ' ' '\n' | grep -qx "$arch"; then
echo "dSYM at ${dwarf_path} does not contain required architecture: ${arch}" >&2
return 1
fi
printf '%s\n' "$dwarf_path"
}
codexbar_dwarf_uuid_for_arch() {
local path="$1"
local arch="$2"
local uuid
uuid=$(dwarfdump --uuid "$path" | awk -v arch="(${arch})" '$1 == "UUID:" && $3 == arch { print $2; exit }')
if [[ -z "$uuid" ]]; then
echo "Missing UUID for ${arch} in: ${path}" >&2
return 1
fi
printf '%s\n' "$uuid"
}
codexbar_verify_dsym_matches_binary() {
local app_binary="$1"
local dsym_dwarf="$2"
shift 2
local arch app_uuid dsym_uuid
if [[ ! -f "$app_binary" ]]; then
echo "Missing app binary for dSYM UUID verification: ${app_binary}" >&2
return 1
fi
if [[ ! -f "$dsym_dwarf" ]]; then
echo "Missing dSYM DWARF file for UUID verification: ${dsym_dwarf}" >&2
return 1
fi
for arch in "$@"; do
if ! app_uuid=$(codexbar_dwarf_uuid_for_arch "$app_binary" "$arch"); then
return 1
fi
if ! dsym_uuid=$(codexbar_dwarf_uuid_for_arch "$dsym_dwarf" "$arch"); then
return 1
fi
if [[ "$app_uuid" != "$dsym_uuid" ]]; then
echo "dSYM UUID mismatch for ${arch}: app=${app_uuid}, dSYM=${dsym_uuid}" >&2
return 1
fi
done
}
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Create a review branch for upstream changes
# Usage: ./Scripts/review_upstream.sh [upstream|quotio]
set -euo pipefail
UPSTREAM=${1:-upstream}
DATE=$(date +%Y%m%d)
BRANCH_NAME="upstream-sync/${UPSTREAM}-${DATE}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
if [ "$UPSTREAM" != "upstream" ] && [ "$UPSTREAM" != "quotio" ]; then
echo -e "${RED}Error: Must specify 'upstream' or 'quotio'${NC}"
echo "Usage: ./Scripts/review_upstream.sh [upstream|quotio]"
exit 1
fi
ensure_remote() {
local remote=$1
local url=$2
local origin_url
if git remote get-url "$remote" >/dev/null 2>&1; then
echo "$remote"
return 0
fi
if [ "$remote" = "upstream" ] && git remote get-url origin >/dev/null 2>&1; then
origin_url=$(git remote get-url origin)
case "$origin_url" in
https://github.com/steipete/CodexBar|https://github.com/steipete/CodexBar.git|git@github.com:steipete/CodexBar.git)
echo -e "${YELLOW}Remote 'upstream' missing; using origin for steipete/CodexBar.${NC}" >&2
echo "origin"
return 0
;;
*)
echo -e "${YELLOW}Remote 'upstream' missing; origin is not steipete/CodexBar, adding upstream.${NC}" >&2
;;
esac
fi
echo -e "${YELLOW}Adding $remote remote...${NC}" >&2
git remote add "$remote" "$url"
echo "$remote"
}
remote_default_branch() {
local remote=$1
local branch=""
local candidate
branch=$(git symbolic-ref -q --short "refs/remotes/${remote}/HEAD" 2>/dev/null | sed "s#^${remote}/##" || true)
if [ -z "$branch" ]; then
branch=$(git remote show "$remote" 2>/dev/null | awk '/HEAD branch/ {print $NF; exit}' || true)
fi
if [ -n "$branch" ] && git rev-parse --verify -q "${remote}/${branch}" >/dev/null; then
echo "$branch"
return 0
fi
for candidate in main master; do
if git rev-parse --verify -q "${remote}/${candidate}" >/dev/null; then
echo "$candidate"
return 0
fi
done
echo -e "${RED}Error: Could not resolve default branch for remote '$remote'.${NC}" >&2
exit 1
}
case "$UPSTREAM" in
upstream) REMOTE=$(ensure_remote upstream "https://github.com/steipete/CodexBar.git") ;;
quotio) REMOTE=$(ensure_remote quotio "https://github.com/nguyenphutrong/quotio.git") ;;
esac
echo -e "${BLUE}==> Fetching latest from $UPSTREAM...${NC}"
git fetch "$REMOTE" --prune
REMOTE_BRANCH=$(remote_default_branch "$REMOTE")
REMOTE_REF="${REMOTE}/${REMOTE_BRANCH}"
echo -e "${BLUE}==> Creating review branch for $UPSTREAM (${REMOTE_REF})...${NC}"
git switch main
git switch -c "$BRANCH_NAME"
echo ""
echo -e "${GREEN}==> Commits to review:${NC}"
git log --oneline --graph "main..${REMOTE_REF}" | head -30 || true
echo ""
echo -e "${GREEN}==> File changes summary:${NC}"
git diff --stat "main..${REMOTE_REF}"
echo ""
echo -e "${YELLOW}==> Review branch created: $BRANCH_NAME${NC}"
echo ""
echo -e "${BLUE}Next steps:${NC}"
echo ""
echo "1. Review commits in detail:"
echo " ${GREEN}git log -p main..$REMOTE_REF${NC}"
echo ""
echo "2. View specific files:"
echo " ${GREEN}git show $REMOTE_REF:path/to/file${NC}"
echo ""
echo "3. Cherry-pick specific commits:"
echo " ${GREEN}git cherry-pick <commit-hash>${NC}"
echo ""
echo "4. Or merge all changes:"
echo " ${GREEN}git merge $REMOTE_REF${NC}"
echo ""
echo "5. Test thoroughly:"
echo " ${GREEN}./Scripts/compile_and_run.sh${NC}"
echo ""
echo "6. If satisfied, merge to main:"
echo " ${GREEN}git checkout main && git merge $BRANCH_NAME${NC}"
echo ""
echo "7. Or discard review branch:"
echo " ${GREEN}git checkout main && git branch -D $BRANCH_NAME${NC}"
echo ""
# Create a review log file
LOG_FILE="upstream-review-${UPSTREAM}-${DATE}.txt"
echo "=== Upstream Review: $UPSTREAM @ $DATE ===" > "$LOG_FILE"
echo "" >> "$LOG_FILE"
echo "Commits:" >> "$LOG_FILE"
git log --oneline "main..${REMOTE_REF}" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
echo "File changes:" >> "$LOG_FILE"
git diff --stat "main..${REMOTE_REF}" >> "$LOG_FILE"
echo -e "${GREEN}Review log saved to: $LOG_FILE${NC}"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Setup stable development code signing to reduce keychain prompts
set -euo pipefail
echo "🔐 Setting up stable development code signing..."
echo ""
echo "This will create a self-signed certificate that stays consistent across rebuilds,"
echo "reducing keychain permission prompts."
echo ""
# Check if we already have a CodexBar development certificate
CERT_NAME="CodexBar Development"
if security find-certificate -c "$CERT_NAME" >/dev/null 2>&1; then
echo "✅ Certificate '$CERT_NAME' already exists!"
echo ""
echo "To use it, add this to your shell profile (~/.zshrc or ~/.bashrc):"
echo ""
echo " export APP_IDENTITY='$CERT_NAME'"
echo ""
echo "Then restart your terminal and rebuild with ./Scripts/compile_and_run.sh"
exit 0
fi
echo "Creating self-signed certificate '$CERT_NAME'..."
echo ""
# Create a temporary config file for the certificate
TEMP_CONFIG=$(mktemp)
trap "rm -f $TEMP_CONFIG" EXIT
cat > "$TEMP_CONFIG" <<EOF
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[ req_distinguished_name ]
CN = $CERT_NAME
O = CodexBar Development
C = US
[ v3_req ]
keyUsage = critical,digitalSignature
extendedKeyUsage = codeSigning
EOF
# Generate the certificate
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout /tmp/codexbar-dev.key -out /tmp/codexbar-dev.crt \
-config "$TEMP_CONFIG" 2>/dev/null
# Convert to PKCS12 format
openssl pkcs12 -export -out /tmp/codexbar-dev.p12 \
-inkey /tmp/codexbar-dev.key -in /tmp/codexbar-dev.crt \
-passout pass: 2>/dev/null
# Import into keychain
security import /tmp/codexbar-dev.p12 -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -T /usr/bin/security
# Clean up temporary files
rm -f /tmp/codexbar-dev.{key,crt,p12}
echo ""
echo "✅ Certificate created successfully!"
echo ""
echo "⚠️ IMPORTANT: You need to trust this certificate for code signing:"
echo ""
echo "1. Open Keychain Access.app"
echo "2. Find '$CERT_NAME' in the 'login' keychain"
echo "3. Double-click it"
echo "4. Expand 'Trust' section"
echo "5. Set 'Code Signing' to 'Always Trust'"
echo "6. Close the window (enter your password when prompted)"
echo ""
echo "Then add this to your shell profile (~/.zshrc or ~/.bashrc):"
echo ""
echo " export APP_IDENTITY='$CERT_NAME'"
echo ""
echo "Restart your terminal and rebuild with ./Scripts/compile_and_run.sh"
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="CodexBar"
APP_IDENTITY="Developer ID Application: Peter Steinberger (Y5PE65HELJ)"
APP_BUNDLE="CodexBar.app"
ROOT=$(cd "$(dirname "$0")/.." && pwd)
source "$ROOT/version.env"
source "$ROOT/Scripts/release_artifacts.sh"
source "$ROOT/Scripts/package_product_paths.sh"
source "$ROOT/Scripts/release_dsym_paths.sh"
verify_distribution_policy() {
local app=$1
if command -v syspolicy_check >/dev/null 2>&1; then
syspolicy_check distribution "$app"
else
spctl -a -t exec -vv "$app"
fi
}
# Allow building a universal binary if ARCHES is provided; default to universal (arm64 + x86_64).
ARCHES_VALUE=${ARCHES:-"arm64 x86_64"}
ZIP_NAME=$(codexbar_app_zip_name "$MARKETING_VERSION" "$ARCHES_VALUE")
DSYM_ZIP=$(codexbar_dsym_zip_name "$MARKETING_VERSION" "$ARCHES_VALUE")
if [[ -z "${APP_STORE_CONNECT_API_KEY_P8:-}" || -z "${APP_STORE_CONNECT_KEY_ID:-}" || -z "${APP_STORE_CONNECT_ISSUER_ID:-}" ]]; then
echo "Missing APP_STORE_CONNECT_* env vars (API key, key id, issuer id)." >&2
exit 1
fi
NOTARIZATION_TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-notarize.XXXXXX")
chmod 700 "$NOTARIZATION_TEMP_DIR"
API_KEY_PATH="$NOTARIZATION_TEMP_DIR/codexbar-api-key.p8"
NOTARIZATION_ZIP="$NOTARIZATION_TEMP_DIR/${APP_NAME}Notarize.zip"
trap 'rm -rf "$NOTARIZATION_TEMP_DIR"' EXIT
(
umask 077
printf '%s' "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > "$API_KEY_PATH"
)
chmod 600 "$API_KEY_PATH"
ARCH_LIST=( ${ARCHES_VALUE} )
ARCHES="${ARCHES_VALUE}" CODEXBAR_SIGNING=identity ./Scripts/package_app.sh release
ENTITLEMENTS_DIR="$ROOT/.build/entitlements"
APP_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBar.entitlements"
WIDGET_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBarWidget.entitlements"
echo "Signing with $APP_IDENTITY"
if [[ -f "$APP_BUNDLE/Contents/Helpers/CodexBarCLI" ]]; then
codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
"$APP_BUNDLE/Contents/Helpers/CodexBarCLI"
fi
if [[ -f "$APP_BUNDLE/Contents/Helpers/CodexBarClaudeWatchdog" ]]; then
codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
"$APP_BUNDLE/Contents/Helpers/CodexBarClaudeWatchdog"
fi
if [[ -d "$APP_BUNDLE/Contents/PlugIns/CodexBarWidget.appex" ]]; then
codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
--entitlements "$WIDGET_ENTITLEMENTS" \
"$APP_BUNDLE/Contents/PlugIns/CodexBarWidget.appex/Contents/MacOS/CodexBarWidget"
codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
--entitlements "$WIDGET_ENTITLEMENTS" \
"$APP_BUNDLE/Contents/PlugIns/CodexBarWidget.appex"
fi
codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
--entitlements "$APP_ENTITLEMENTS" \
"$APP_BUNDLE"
DITTO_BIN=${DITTO_BIN:-/usr/bin/ditto}
"$DITTO_BIN" --norsrc -c -k --keepParent "$APP_BUNDLE" "$NOTARIZATION_ZIP"
echo "Submitting for notarization"
xcrun notarytool submit "$NOTARIZATION_ZIP" \
--key "$API_KEY_PATH" \
--key-id "$APP_STORE_CONNECT_KEY_ID" \
--issuer "$APP_STORE_CONNECT_ISSUER_ID" \
--wait
echo "Stapling ticket"
xcrun stapler staple "$APP_BUNDLE"
# Strip any extended attributes that would create AppleDouble files when zipping
xattr -cr "$APP_BUNDLE"
find "$APP_BUNDLE" -name '._*' -delete
"$DITTO_BIN" --norsrc -c -k --keepParent "$APP_BUNDLE" "$ZIP_NAME"
verify_distribution_policy "$APP_BUNDLE"
stapler validate "$APP_BUNDLE"
echo "Packaging dSYM"
DSYM_STAGE_ROOT="$ROOT/.build/package-products/release"
DSYM_PATHS=()
for ARCH in "${ARCH_LIST[@]}"; do
STAGED_DSYM="$DSYM_STAGE_ROOT/$ARCH/${APP_NAME}.dSYM"
if [[ -d "$STAGED_DSYM" ]]; then
DSYM_PATHS+=("$STAGED_DSYM")
continue
fi
BIN_DIR=$(codexbar_swiftpm_bin_path release "$ARCH")
DSYM_PATHS+=("$(codexbar_resolve_dsym_path "$DSYM_STAGE_ROOT" "$BIN_DIR" "$APP_NAME" "$ARCH")")
done
DSYM_PATH="${DSYM_PATHS[0]}"
DSYM_DWARF_PATHS=()
for ((index = 0; index < ${#ARCH_LIST[@]}; index++)); do
ARCH="${ARCH_LIST[$index]}"
if ! ARCH_DSYM=$(codexbar_require_dsym_dwarf_for_arch "${DSYM_PATHS[$index]}" "$APP_NAME" "$ARCH"); then
exit 1
fi
DSYM_DWARF_PATHS+=("$ARCH_DSYM")
done
if [[ ${#ARCH_LIST[@]} -gt 1 ]]; then
MERGED_DSYM_ROOT="${DSYM_STAGE_ROOT}/${APP_NAME}.dSYM-universal"
MERGED_DSYM="${MERGED_DSYM_ROOT}/${APP_NAME}.dSYM"
rm -rf "$MERGED_DSYM_ROOT"
mkdir -p "$MERGED_DSYM_ROOT"
cp -R "$DSYM_PATH" "$MERGED_DSYM"
DWARF_PATH="${MERGED_DSYM}/Contents/Resources/DWARF/${APP_NAME}"
lipo -create "${DSYM_DWARF_PATHS[@]}" -output "$DWARF_PATH"
DSYM_PATH="$MERGED_DSYM"
fi
if [[ ! -d "$DSYM_PATH" ]]; then
echo "Missing dSYM at SwiftPM-reported path: $DSYM_PATH" >&2
exit 1
fi
codexbar_verify_dsym_matches_binary \
"$APP_BUNDLE/Contents/MacOS/$APP_NAME" \
"$DSYM_PATH/Contents/Resources/DWARF/$APP_NAME" \
"${ARCH_LIST[@]}"
"$DITTO_BIN" --norsrc -c -k --keepParent "$DSYM_PATH" "$DSYM_ZIP"
echo "Done: $ZIP_NAME"
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
codexbar_resolve_sparkle_version_child() {
local versions_dir="$1"
local candidate="$2"
local label="$3"
local versions_root resolved
versions_root=$(cd "$versions_dir" && pwd -P)
if ! resolved=$(cd "$candidate" 2>/dev/null && pwd -P); then
echo "ERROR: Sparkle ${label} does not resolve: ${candidate}" >&2
return 1
fi
if [[ "$(dirname "$resolved")" != "$versions_root" ]]; then
echo "ERROR: Sparkle ${label} resolves outside the framework versions directory: ${candidate}" >&2
return 1
fi
printf '%s\n' "$resolved"
}
codexbar_sparkle_version_dir() {
local sparkle="$1"
local versions_dir="${sparkle}/Versions"
if [[ -L "$sparkle" ]]; then
echo "ERROR: Sparkle framework root must not be a symlink: ${sparkle}" >&2
return 1
fi
if [[ -L "$versions_dir" ]]; then
echo "ERROR: Sparkle versions directory must not be a symlink: ${versions_dir}" >&2
return 1
fi
if [[ ! -d "$versions_dir" ]]; then
echo "ERROR: Missing Sparkle versions directory: ${versions_dir}" >&2
return 1
fi
if [[ -e "$versions_dir/Current" || -L "$versions_dir/Current" ]]; then
local current
if ! current=$(codexbar_resolve_sparkle_version_child "$versions_dir" "$versions_dir/Current" "Versions/Current"); then
return 1
fi
printf '%s\n' "$current"
return
fi
local version_dirs=()
local candidate
shopt -s nullglob
for candidate in "$versions_dir"/*; do
if [[ -d "$candidate" ]]; then
version_dirs+=("$candidate")
fi
done
shopt -u nullglob
case "${#version_dirs[@]}" in
0)
echo "ERROR: Sparkle framework has no version directory under: ${versions_dir}" >&2
return 1
;;
1)
local resolved
if ! resolved=$(codexbar_resolve_sparkle_version_child \
"$versions_dir" "${version_dirs[0]}" "version directory"); then
return 1
fi
printf '%s\n' "$resolved"
;;
*)
echo "ERROR: Sparkle framework has multiple version directories and no Versions/Current symlink: ${versions_dir}" >&2
return 1
;;
esac
}
codexbar_require_sparkle_signing_target() {
local path="$1"
local label="$2"
local trusted_root="$3"
local resolved trusted_root_resolved
if [[ -L "$path" ]]; then
echo "ERROR: Sparkle signing target must not be a symlink (${label}): ${path}" >&2
return 1
fi
if [[ ! -e "$path" ]]; then
echo "ERROR: Missing Sparkle signing target (${label}): ${path}" >&2
return 1
fi
if ! trusted_root_resolved=$(cd "$trusted_root" 2>/dev/null && pwd -P); then
echo "ERROR: Sparkle signing root does not resolve (${label}): ${trusted_root}" >&2
return 1
fi
if [[ -d "$path" ]]; then
resolved=$(cd "$path" && pwd -P)
else
resolved="$(cd "$(dirname "$path")" && pwd -P)/$(basename "$path")"
fi
if [[ "$resolved" != "$trusted_root_resolved" &&
"${resolved#"$trusted_root_resolved"/}" == "$resolved" ]]; then
echo "ERROR: Sparkle signing target resolves outside its trusted root (${label}): ${path}" >&2
return 1
fi
printf '%s\n' "$resolved"
}
codexbar_sparkle_signing_targets() {
local sparkle="$1"
local version_dir
if ! version_dir=$(codexbar_sparkle_version_dir "$sparkle"); then
return 1
fi
codexbar_require_sparkle_signing_target "$sparkle" "framework root" "$sparkle" || return 1
codexbar_require_sparkle_signing_target "$version_dir/Sparkle" "framework binary" "$version_dir" || return 1
codexbar_require_sparkle_signing_target "$version_dir/Autoupdate" "autoupdate tool" "$version_dir" || return 1
codexbar_require_sparkle_signing_target "$version_dir/Updater.app" "updater app" "$version_dir" || return 1
codexbar_require_sparkle_signing_target \
"$version_dir/Updater.app/Contents/MacOS/Updater" "updater executable" "$version_dir" || return 1
codexbar_require_sparkle_signing_target \
"$version_dir/XPCServices/Downloader.xpc" "downloader xpc" "$version_dir" || return 1
codexbar_require_sparkle_signing_target \
"$version_dir/XPCServices/Downloader.xpc/Contents/MacOS/Downloader" \
"downloader executable" "$version_dir" || return 1
codexbar_require_sparkle_signing_target \
"$version_dir/XPCServices/Installer.xpc" "installer xpc" "$version_dir" || return 1
codexbar_require_sparkle_signing_target \
"$version_dir/XPCServices/Installer.xpc/Contents/MacOS/Installer" \
"installer executable" "$version_dir" || return 1
codexbar_require_sparkle_signing_target "$version_dir" "framework version" "$version_dir" || return 1
codexbar_require_sparkle_signing_target "$sparkle" "framework root" "$sparkle" || return 1
}
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
content: ["./docs/index.html", "./docs/site.js"],
theme: {
extend: {
screens: {
tablet: "769px",
},
fontFamily: {
sans: ["Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "sans-serif"],
mono: ["SFMono-Regular", "SF Mono", "Menlo", "monospace"],
},
},
},
};
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GROUP_SIZE="${CODEXBAR_TEST_GROUP_SIZE:-12}"
SUITE_TIMEOUT="${CODEXBAR_TEST_SUITE_TIMEOUT:-180}"
RETRY_NON_TIMEOUT_FAILURES="${CODEXBAR_TEST_RETRY_NON_TIMEOUT_FAILURES:-1}"
cd "${ROOT_DIR}"
# Defense in depth: test processes also self-detect, but keep this explicit so runner changes cannot
# expose the user's login Keychain. Deliberate isolated Keychain tests must opt in by setting the allow flag.
if [[ "${CODEXBAR_ALLOW_TEST_KEYCHAIN_ACCESS:-}" != "1" ]]; then
export CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS=1
fi
ARGS=(
--group-size "${GROUP_SIZE}"
--timeout "${SUITE_TIMEOUT}"
)
case "${RETRY_NON_TIMEOUT_FAILURES}" in
0) ARGS+=(--no-retry-non-timeout-failures) ;;
1) ;;
*)
echo "CODEXBAR_TEST_RETRY_NON_TIMEOUT_FAILURES must be 0 or 1" >&2
exit 2
;;
esac
if [[ -n "${CODEXBAR_TEST_SHARD_INDEX:-}" || -n "${CODEXBAR_TEST_SHARD_COUNT:-}" ]]; then
ARGS+=(
--shard-index "${CODEXBAR_TEST_SHARD_INDEX:?CODEXBAR_TEST_SHARD_COUNT requires CODEXBAR_TEST_SHARD_INDEX}"
--shard-count "${CODEXBAR_TEST_SHARD_COUNT:?CODEXBAR_TEST_SHARD_INDEX requires CODEXBAR_TEST_SHARD_COUNT}"
)
fi
exec python3 "${ROOT_DIR}/Scripts/ci_swift_test_by_suite.py" "${ARGS[@]}" "$@"
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_gate() {
local expected="$1"
local name="$2"
local paths_file="${tmp_dir}/${name}.paths"
local output_file="${tmp_dir}/${name}.output"
shift 2
printf '%s\n' "$@" > "$paths_file"
GITHUB_OUTPUT="$output_file" "${ROOT_DIR}/Scripts/ci_macos_test_gate.sh" "$paths_file" >/dev/null
local actual
actual="$(sed -n 's/^macos-tests=//p' "$output_file")"
if [[ "$actual" != "$expected" ]]; then
printf '%s: expected macos-tests=%s, got %s\n' "$name" "$expected" "${actual:-<empty>}" >&2
exit 1
fi
local reason
reason="$(sed -n 's/^macos-tests-reason=//p' "$output_file")"
if [[ -z "$reason" ]]; then
printf '%s: expected macos-tests-reason output\n' "$name" >&2
exit 1
fi
local path_count
path_count="$(sed -n 's/^changed-path-count=//p' "$output_file")"
if ! [[ "$path_count" =~ ^[0-9]+$ ]]; then
printf '%s: expected numeric changed-path-count output, got %s\n' \
"$name" "${path_count:-<empty>}" >&2
exit 1
fi
if [[ "$expected" == false && "$reason" != "docs/site-only changes covered by portable checks" ]]; then
printf '%s: expected docs/site skip reason, got %s\n' "$name" "$reason" >&2
exit 1
fi
}
assert_gate false docs-only $'M\tdocs/providers.md' $'M\tREADME.md'
assert_gate true configuration-doc $'M\tdocs/configuration.md'
assert_gate true rename-to-configuration-doc $'R100\tdocs/old.md\tdocs/configuration.md'
assert_gate true rename-from-configuration-doc $'R100\tdocs/configuration.md\tdocs/new.md'
assert_gate true agents-contract $'M\tAGENTS.md'
assert_gate true rename-to-agents-contract $'R100\tdocs/old.md\tAGENTS.md'
assert_gate true rename-from-agents-contract $'R100\tAGENTS.md\tdocs/new.md'
assert_gate true source $'M\tSources/CodexBar/App.swift'
assert_gate false docs-site $'M\tdocs/index.html' $'M\tdocs/site.css' $'M\tdocs/site.js' \
$'M\tdocs/site-locales.mjs' $'M\tdocs/social.html' $'M\tdocs/social.png' \
$'M\tdocs/CNAME' $'M\tdocs/.nojekyll' $'M\tdocs/llms.txt'
assert_gate false docs-site-assets $'M\tdocs/icon.png' $'M\tdocs/logos/provider-logo.svg'
assert_gate true docs-unknown-code $'M\tdocs/custom-tool.js'
assert_gate true docs-site-with-config $'M\tdocs/site.css' $'M\tdocs/configuration.md'
assert_gate true empty
assert_gate true source-to-docs $'R100\tSources/CodexBar/App.swift\tdocs/App.md'
assert_gate true docs-to-source $'R100\tdocs/App.md\tSources/CodexBar/App.swift'
assert_gate false docs-to-site $'R100\tdocs/old.md\tdocs/site.css'
assert_gate_fails() {
local name="$1"
local paths_file="${tmp_dir}/${name}.paths"
local output_file="${tmp_dir}/${name}.output"
shift
printf '%s\n' "$@" > "$paths_file"
if GITHUB_OUTPUT="$output_file" "${ROOT_DIR}/Scripts/ci_macos_test_gate.sh" "$paths_file" >/dev/null 2>&1; then
printf '%s: malformed gate input unexpectedly succeeded\n' "$name" >&2
exit 1
fi
if [[ -s "$output_file" ]]; then
printf '%s: malformed gate input emitted an output\n' "$name" >&2
exit 1
fi
}
assert_gate_fails missing-rename-target $'R100\tREADME.md'
assert_gate_fails extra-modified-path $'M\tREADME.md\tdocs/configuration.md'
assert_gate_fails missing-rename-score $'R\tREADME.md\tdocs/README.md'
assert_gate_fails invalid-rename-score $'Rfoo\tREADME.md\tdocs/README.md'
assert_gate_fails out-of-range-rename-score $'R101\tREADME.md\tdocs/README.md'
unterminated_paths="${tmp_dir}/unterminated.paths"
unterminated_output="${tmp_dir}/unterminated.output"
printf '%s' $'M\tREADME.md\tdocs/configuration.md' > "$unterminated_paths"
if GITHUB_OUTPUT="$unterminated_output" \
"${ROOT_DIR}/Scripts/ci_macos_test_gate.sh" "$unterminated_paths" >/dev/null 2>&1
then
printf 'unterminated malformed gate input unexpectedly succeeded\n' >&2
exit 1
fi
if [[ -s "$unterminated_output" ]]; then
printf 'unterminated malformed gate input emitted an output\n' >&2
exit 1
fi
verify="${ROOT_DIR}/Scripts/ci_verify_test_jobs.sh"
"$verify" success success true success >/dev/null
"$verify" success success false skipped >/dev/null
assert_verify_fails() {
if "$verify" "$@" >/dev/null 2>&1; then
printf 'unexpected aggregate success: %s\n' "$*" >&2
exit 1
fi
}
assert_verify_fails success success true skipped
assert_verify_fails success success false success
assert_verify_fails success success "" skipped
assert_verify_fails failure success true success
assert_verify_fails success failure true success
printf 'CI macOS path gate tests passed.\n'
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
PREV_TAG=${1:?"pass previous release tag (e.g. v0.1.0)"}
CUR_TAG=${2:?"pass current release tag (e.g. v0.1.1)"}
PREV_VER=${PREV_TAG#v}
CUR_VER=${CUR_TAG#v}
APP_NAME="CodexBar"
ZIP_URL="https://github.com/steipete/CodexBar/releases/download/${PREV_TAG}/${APP_NAME}-macos-universal-${PREV_VER}.zip"
TMP_DIR=$(mktemp -d /tmp/codexbar-live.XXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
echo "Downloading previous release $PREV_TAG from $ZIP_URL"
curl --fail --location --output "$TMP_DIR/prev.zip" "$ZIP_URL"
echo "Installing previous release to /Applications/${APP_NAME}.app"
osascript -e 'tell application "CodexBar" to quit' >/dev/null 2>&1 || true
for _ in {1..20}; do
pgrep -x "$APP_NAME" >/dev/null || break
sleep 0.25
done
if pgrep -x "$APP_NAME" >/dev/null; then
echo "ERROR: ${APP_NAME} did not quit before replacement." >&2
exit 1
fi
rm -rf /Applications/${APP_NAME}.app
ditto -x -k "$TMP_DIR/prev.zip" "$TMP_DIR"
ditto "$TMP_DIR/${APP_NAME}.app" /Applications/${APP_NAME}.app
echo "Launching previous build…"
open -n /Applications/${APP_NAME}.app
sleep 4
cat <<'MSG'
Manual step: trigger "Check for Updates…" in the app and install the update.
Expect to land on the newly released version. When done, confirm below.
MSG
read -rp "Did the update succeed from ${PREV_TAG} to ${CUR_TAG}? (y/N) " answer
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
echo "Live update test NOT confirmed; failing per RUN_SPARKLE_UPDATE_TEST." >&2
exit 1
fi
installed_ver=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"/Applications/${APP_NAME}.app/Contents/Info.plist")
if [[ "$installed_ver" != "$CUR_VER" ]]; then
echo "Live update reported success but installed ${installed_ver}; expected ${CUR_VER}." >&2
exit 1
fi
echo "Live update test confirmed."
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
source "$ROOT/Scripts/package_product_paths.sh"
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-package-paths.XXXXXX")
trap 'rm -rf "$TEMP_DIR"' EXIT
NATIVE_DIR="$TEMP_DIR/.build/arm64-apple-macosx/release"
SWIFTBUILD_DIR="$TEMP_DIR/.build/out/Products/Release"
STAGE_ROOT="$TEMP_DIR/.build/package-products/release"
mkdir -p "$NATIVE_DIR/CodexBar.dSYM" "$SWIFTBUILD_DIR/Sparkle.framework" "$SWIFTBUILD_DIR/CodexBar.dSYM"
touch "$NATIVE_DIR/CodexBar" "$SWIFTBUILD_DIR/CodexBar"
native=$(codexbar_require_product_file "$NATIVE_DIR" CodexBar arm64)
[[ "$native" == "$NATIVE_DIR/CodexBar" ]]
swiftbuild=$(codexbar_require_product_file "$SWIFTBUILD_DIR" CodexBar arm64)
[[ "$swiftbuild" == "$SWIFTBUILD_DIR/CodexBar" ]]
framework=$(codexbar_require_product_directory "$SWIFTBUILD_DIR" Sparkle.framework packaging)
[[ "$framework" == "$SWIFTBUILD_DIR/Sparkle.framework" ]]
dsym=$(codexbar_require_product_directory "$SWIFTBUILD_DIR" CodexBar.dSYM release)
[[ "$dsym" == "$SWIFTBUILD_DIR/CodexBar.dSYM" ]]
resolved=$(codexbar_resolve_staged_or_reported_file "$STAGE_ROOT" "$SWIFTBUILD_DIR" CodexBar arm64)
[[ "$resolved" == "$SWIFTBUILD_DIR/CodexBar" ]]
resolved_dsym=$(codexbar_resolve_dsym_path "$STAGE_ROOT" "$SWIFTBUILD_DIR" CodexBar arm64)
[[ "$resolved_dsym" == "$SWIFTBUILD_DIR/CodexBar.dSYM" ]]
mkdir -p "$STAGE_ROOT/arm64/CodexBar.dSYM"
touch "$STAGE_ROOT/arm64/CodexBar"
staged=$(codexbar_resolve_staged_or_reported_file "$STAGE_ROOT" "$SWIFTBUILD_DIR" CodexBar arm64)
[[ "$staged" == "$STAGE_ROOT/arm64/CodexBar" ]]
staged_dsym=$(codexbar_resolve_dsym_path "$STAGE_ROOT" "$SWIFTBUILD_DIR" CodexBar arm64)
[[ "$staged_dsym" == "$STAGE_ROOT/arm64/CodexBar.dSYM" ]]
rm -rf "$STAGE_ROOT"
rm "$SWIFTBUILD_DIR/CodexBar"
if codexbar_resolve_staged_or_reported_file "$STAGE_ROOT" "$SWIFTBUILD_DIR" CodexBar arm64 \
2>"$TEMP_DIR/missing-file.log"; then
echo "ERROR: Missing reported product unexpectedly fell back to legacy output." >&2
exit 1
fi
grep -Fq "$SWIFTBUILD_DIR/CodexBar" "$TEMP_DIR/missing-file.log"
rm -rf "$SWIFTBUILD_DIR/Sparkle.framework"
if codexbar_require_product_directory "$SWIFTBUILD_DIR" Sparkle.framework packaging \
2>"$TEMP_DIR/missing-directory.log"; then
echo "ERROR: Missing reported framework was accepted." >&2
exit 1
fi
grep -Fq "$SWIFTBUILD_DIR/Sparkle.framework" "$TEMP_DIR/missing-directory.log"
rm -rf "$SWIFTBUILD_DIR/CodexBar.dSYM"
if codexbar_resolve_dsym_path "$STAGE_ROOT" "$SWIFTBUILD_DIR" CodexBar arm64 \
2>"$TEMP_DIR/missing-dsym.log"; then
echo "ERROR: Missing reported dSYM unexpectedly fell back to legacy output." >&2
exit 1
fi
grep -Fq "$SWIFTBUILD_DIR/CodexBar.dSYM" "$TEMP_DIR/missing-dsym.log"
swift() {
[[ "$*" == "build --show-bin-path -c release --arch arm64" ]]
printf '%s\n' "$SWIFTBUILD_DIR"
}
reported=$(codexbar_swiftpm_bin_path release arm64)
[[ "$reported" == "$SWIFTBUILD_DIR" ]]
swift() {
return 23
}
if codexbar_swiftpm_bin_path release arm64 2>"$TEMP_DIR/query.log"; then
echo "ERROR: SwiftPM bin-path query failure was ignored." >&2
exit 1
fi
grep -Fq "SwiftPM failed to report" "$TEMP_DIR/query.log"
swift() {
return 0
}
if codexbar_swiftpm_bin_path release arm64 2>"$TEMP_DIR/empty.log"; then
echo "ERROR: Empty SwiftPM bin path was accepted." >&2
exit 1
fi
grep -Fq "SwiftPM reported an empty" "$TEMP_DIR/empty.log"
echo "Package product path tests passed."
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
PACKAGE_SCRIPT="$ROOT/Scripts/package_app.sh"
RELEASE_SCRIPT="$ROOT/Scripts/sign-and-notarize.sh"
FUNCTIONS_FILE=$(mktemp "${TMPDIR:-/tmp}/codexbar-package-signing-functions.XXXXXX")
trap 'rm -f "$FUNCTIONS_FILE"' EXIT
python3 - "$PACKAGE_SCRIPT" "$FUNCTIONS_FILE" <<'PY'
import sys
from pathlib import Path
script = Path(sys.argv[1]).read_text()
start = script.index('resolve_package_signing_mode() {')
end = script.index('\n}\n', start) + 3
Path(sys.argv[2]).write_text(script[start:end])
PY
source "$FUNCTIONS_FILE"
unset CODEXBAR_SIGNING
SIGNING_MODE=
resolve_package_signing_mode
[[ "$SIGNING_MODE" == "adhoc" ]]
CODEXBAR_SIGNING=identity
resolve_package_signing_mode
[[ "$SIGNING_MODE" == "identity" ]]
CODEXBAR_SIGNING=invalid
if resolve_package_signing_mode 2>/dev/null; then
echo "Invalid package signing mode unexpectedly succeeded" >&2
exit 1
fi
grep -Fq 'CODEXBAR_SIGNING=identity ./Scripts/package_app.sh release' "$RELEASE_SCRIPT"
echo "Package signing tests passed."
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
PACKAGE_SCRIPT="$ROOT/Scripts/package_app.sh"
FUNCTIONS_FILE=$(mktemp "${TMPDIR:-/tmp}/codexbar-package-strip-functions.XXXXXX")
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-package-strip.XXXXXX")
trap 'rm -rf "$FUNCTIONS_FILE" "$TEMP_DIR"' EXIT
python3 - "$PACKAGE_SCRIPT" "$FUNCTIONS_FILE" <<'PY'
import sys
from pathlib import Path
script = Path(sys.argv[1]).read_text()
start = script.index('strip_release_binary() {')
end = script.index('\n}\n', start) + 3
Path(sys.argv[2]).write_text(script[start:end])
PY
xcrun() {
[[ "$1" == "strip" && "$2" == "-x" ]]
printf '%s\n' "$3" >> "$STRIP_LOG"
}
source "$FUNCTIONS_FILE"
binary="$TEMP_DIR/CodexBar"
touch "$binary"
STRIP_LOG="$TEMP_DIR/release.log"
LOWER_CONF=release
strip_release_binary "$binary"
grep -Fqx "$binary" "$STRIP_LOG"
STRIP_LOG="$TEMP_DIR/debug.log"
LOWER_CONF=debug
strip_release_binary "$binary"
[[ ! -e "$STRIP_LOG" ]]
STRIP_LOG="$TEMP_DIR/missing.log"
LOWER_CONF=release
strip_release_binary "$TEMP_DIR/MissingBinary"
[[ ! -e "$STRIP_LOG" ]]
echo "Package strip tests passed."
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
source "$ROOT/Scripts/release_dsym_paths.sh"
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-release-dsym-paths.XXXXXX")
trap 'rm -rf "$TEMP_DIR"' EXIT
make_dsym() {
local dsym_path="$1"
mkdir -p "$dsym_path/Contents/Resources/DWARF"
touch "$dsym_path/Contents/Resources/DWARF/CodexBar"
}
ARM_DSYM="$TEMP_DIR/CodexBar arm64.dSYM"
UNIVERSAL_DSYM="$TEMP_DIR/CodexBar universal.dSYM"
WRONG_ARCH_DSYM="$TEMP_DIR/CodexBar stale.dSYM"
MISSING_DWARF_DSYM="$TEMP_DIR/CodexBar missing.dSYM"
APP_BINARY="$TEMP_DIR/CodexBar.app"
MATCHING_DWARF="$TEMP_DIR/CodexBar matching"
MISMATCHED_DWARF="$TEMP_DIR/CodexBar mismatched"
MISSING_UUID_DWARF="$TEMP_DIR/CodexBar missing UUID"
make_dsym "$ARM_DSYM"
make_dsym "$UNIVERSAL_DSYM"
make_dsym "$WRONG_ARCH_DSYM"
mkdir -p "$MISSING_DWARF_DSYM/Contents/Resources/DWARF"
touch "$APP_BINARY" "$MATCHING_DWARF" "$MISMATCHED_DWARF" "$MISSING_UUID_DWARF"
lipo() {
[[ "$1" == "-archs" ]]
case "$2" in
"$ARM_DSYM/Contents/Resources/DWARF/CodexBar")
printf '%s\n' "arm64"
;;
"$UNIVERSAL_DSYM/Contents/Resources/DWARF/CodexBar")
printf '%s\n' "arm64 x86_64"
;;
"$WRONG_ARCH_DSYM/Contents/Resources/DWARF/CodexBar")
printf '%s\n' "x86_64"
;;
*)
echo "unexpected lipo path: $2" >&2
return 2
;;
esac
}
dwarfdump() {
[[ "$1" == "--uuid" ]]
case "$2" in
"$APP_BINARY" | "$MATCHING_DWARF")
printf '%s\n' \
"UUID: AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA (arm64) $2" \
"UUID: BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB (x86_64) $2"
;;
"$MISMATCHED_DWARF")
printf '%s\n' \
"UUID: AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA (arm64) $2" \
"UUID: CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC (x86_64) $2"
;;
"$MISSING_UUID_DWARF")
printf '%s\n' "UUID: AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA (arm64) $2"
;;
*)
echo "unexpected dwarfdump path: $2" >&2
return 2
;;
esac
}
arm_dwarf=$(codexbar_require_dsym_dwarf_for_arch "$ARM_DSYM" CodexBar arm64)
[[ "$arm_dwarf" == "$ARM_DSYM/Contents/Resources/DWARF/CodexBar" ]]
x86_dwarf=$(codexbar_require_dsym_dwarf_for_arch "$UNIVERSAL_DSYM" CodexBar x86_64)
[[ "$x86_dwarf" == "$UNIVERSAL_DSYM/Contents/Resources/DWARF/CodexBar" ]]
if codexbar_require_dsym_dwarf_for_arch "$MISSING_DWARF_DSYM" CodexBar arm64 \
2>"$TEMP_DIR/missing-dwarf.log"; then
echo "ERROR: Missing dSYM DWARF file was accepted." >&2
exit 1
fi
grep -Fq "$MISSING_DWARF_DSYM/Contents/Resources/DWARF/CodexBar" "$TEMP_DIR/missing-dwarf.log"
if codexbar_require_dsym_dwarf_for_arch "$WRONG_ARCH_DSYM" CodexBar arm64 \
2>"$TEMP_DIR/wrong-arch.log"; then
echo "ERROR: Wrong-architecture dSYM was accepted." >&2
exit 1
fi
grep -Fq "required architecture: arm64" "$TEMP_DIR/wrong-arch.log"
codexbar_verify_dsym_matches_binary "$APP_BINARY" "$MATCHING_DWARF" arm64 x86_64
if codexbar_verify_dsym_matches_binary "$APP_BINARY" "$MISMATCHED_DWARF" arm64 x86_64 \
2>"$TEMP_DIR/mismatched-uuid.log"; then
echo "ERROR: Mismatched dSYM UUID was accepted." >&2
exit 1
fi
grep -Fq "dSYM UUID mismatch for x86_64" "$TEMP_DIR/mismatched-uuid.log"
if codexbar_verify_dsym_matches_binary "$APP_BINARY" "$MISSING_UUID_DWARF" arm64 x86_64 \
2>"$TEMP_DIR/missing-uuid.log"; then
echo "ERROR: Missing dSYM UUID was accepted." >&2
exit 1
fi
grep -Fq "Missing UUID for x86_64" "$TEMP_DIR/missing-uuid.log"
echo "Release dSYM path tests passed."
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-repository-size.XXXXXX")
trap 'rm -rf "$TEMP_DIR"' EXIT
mkdir -p "$TEMP_DIR/Scripts"
cp "$ROOT_DIR/Scripts/check_repository_size.sh" "$TEMP_DIR/Scripts/"
git -C "$TEMP_DIR" init --quiet
empty_output=$("$TEMP_DIR/Scripts/check_repository_size.sh")
grep -Fq 'repository size OK: 0 tracked files' <<<"$empty_output"
printf 'small source file\n' > "$TEMP_DIR/source.txt"
git -C "$TEMP_DIR" add source.txt Scripts/check_repository_size.sh
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
dd if=/dev/zero of="$TEMP_DIR/untracked.bin" bs=1024 count=2049 2>/dev/null
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
dd if=/dev/zero of="$TEMP_DIR/boundary.bin" bs=1024 count=2048 2>/dev/null
git -C "$TEMP_DIR" add boundary.bin
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
printf 'x' >> "$TEMP_DIR/boundary.bin"
git -C "$TEMP_DIR" add boundary.bin
if "$TEMP_DIR/Scripts/check_repository_size.sh" >"$TEMP_DIR/large.log" 2>&1; then
printf 'ERROR: staged blob one byte above the limit was accepted.\n' >&2
exit 1
fi
grep -Fq 'tracked file exceeds 2097152 bytes: boundary.bin (2097153 bytes)' "$TEMP_DIR/large.log"
git -C "$TEMP_DIR" rm --cached --force --quiet boundary.bin
git -C "$TEMP_DIR" add untracked.bin
printf 'working tree is now small\n' > "$TEMP_DIR/untracked.bin"
if "$TEMP_DIR/Scripts/check_repository_size.sh" >"$TEMP_DIR/staged-large.log" 2>&1; then
printf 'ERROR: oversized staged blob was accepted after its working-tree file changed.\n' >&2
exit 1
fi
grep -Fq 'tracked file exceeds 2097152 bytes: untracked.bin (2098176 bytes)' "$TEMP_DIR/staged-large.log"
git -C "$TEMP_DIR" rm --cached --force --quiet untracked.bin
printf 'small staged blob\n' > "$TEMP_DIR/index-is-authoritative.bin"
git -C "$TEMP_DIR" add index-is-authoritative.bin
dd if=/dev/zero of="$TEMP_DIR/index-is-authoritative.bin" bs=1024 count=2049 2>/dev/null
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
odd_path=$'odd\nname.txt'
printf 'small source file\n' > "$TEMP_DIR/$odd_path"
git -C "$TEMP_DIR" add "$odd_path"
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
artifacts=(
"CodexBar 2.app/Contents/MacOS/CodexBar"
"CodexBar.dSYM/Contents/Info.plist"
"CodexBar.xcarchive/Products/Applications/CodexBar.app/Contents/Info.plist"
"CodexBar.xcresult/Data/data"
"CodexBar.ipa"
"CodexBar.zip"
"CodexBar.delta"
"CodexBar.dmg"
"CodexBar.pkg"
"CodexBar.tar.gz"
"CodexBar.tgz"
)
for artifact in "${artifacts[@]}"; do
mkdir -p "$TEMP_DIR/$(dirname "$artifact")"
printf 'release artifact\n' > "$TEMP_DIR/$artifact"
git -C "$TEMP_DIR" add -f "$artifact"
done
ln -s source.txt "$TEMP_DIR/CodexBar-latest.dmg"
git -C "$TEMP_DIR" add -f CodexBar-latest.dmg
rm "$TEMP_DIR/CodexBar.zip"
if "$TEMP_DIR/Scripts/check_repository_size.sh" >"$TEMP_DIR/artifact.log" 2>&1; then
printf 'ERROR: tracked release artifacts were accepted.\n' >&2
exit 1
fi
for artifact in "${artifacts[@]}" CodexBar-latest.dmg; do
grep -Fq "generated artifact is tracked: $artifact" "$TEMP_DIR/artifact.log"
done
printf 'Repository size tests passed.\n'
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
source "$ROOT/Scripts/sparkle_signing_paths.sh"
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-sparkle-signing.XXXXXX")
TEMP_DIR=$(cd "$TEMP_DIR" && pwd -P)
trap 'rm -rf "$TEMP_DIR"' EXIT
make_sparkle_version() {
local sparkle="$1"
local version="$2"
local version_dir="$sparkle/Versions/$version"
mkdir -p \
"$version_dir/Updater.app/Contents/MacOS" \
"$version_dir/XPCServices/Downloader.xpc/Contents/MacOS" \
"$version_dir/XPCServices/Installer.xpc/Contents/MacOS"
touch \
"$version_dir/Sparkle" \
"$version_dir/Autoupdate" \
"$version_dir/Updater.app/Contents/MacOS/Updater" \
"$version_dir/XPCServices/Downloader.xpc/Contents/MacOS/Downloader" \
"$version_dir/XPCServices/Installer.xpc/Contents/MacOS/Installer"
}
SINGLE="$TEMP_DIR/Single Sparkle.framework"
make_sparkle_version "$SINGLE" B
single_version=$(codexbar_sparkle_version_dir "$SINGLE")
[[ "$single_version" == "$SINGLE/Versions/B" ]]
single_targets=$(codexbar_sparkle_signing_targets "$SINGLE")
grep -Fqx "$SINGLE" <<<"$single_targets"
grep -Fqx "$SINGLE/Versions/B/Sparkle" <<<"$single_targets"
grep -Fqx "$SINGLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer" <<<"$single_targets"
CURRENT="$TEMP_DIR/Current Sparkle.framework"
make_sparkle_version "$CURRENT" A
make_sparkle_version "$CURRENT" C
ln -s C "$CURRENT/Versions/Current"
current_version=$(codexbar_sparkle_version_dir "$CURRENT")
[[ "$current_version" == "$CURRENT/Versions/C" ]]
rm "$CURRENT/Versions/C/Autoupdate"
if codexbar_sparkle_signing_targets "$CURRENT" >"$TEMP_DIR/missing-target.out" 2>"$TEMP_DIR/missing-target.log"; then
echo "ERROR: Missing Sparkle signing target was accepted." >&2
exit 1
fi
grep -Fq "Autoupdate" "$TEMP_DIR/missing-target.log"
AMBIGUOUS="$TEMP_DIR/Ambiguous Sparkle.framework"
make_sparkle_version "$AMBIGUOUS" A
make_sparkle_version "$AMBIGUOUS" B
if codexbar_sparkle_version_dir "$AMBIGUOUS" 2>"$TEMP_DIR/ambiguous.log"; then
echo "ERROR: Ambiguous Sparkle versions were accepted without Versions/Current." >&2
exit 1
fi
grep -Fq "multiple version directories" "$TEMP_DIR/ambiguous.log"
BROKEN_CURRENT="$TEMP_DIR/Broken Current Sparkle.framework"
make_sparkle_version "$BROKEN_CURRENT" B
ln -s Missing "$BROKEN_CURRENT/Versions/Current"
if codexbar_sparkle_version_dir "$BROKEN_CURRENT" 2>"$TEMP_DIR/broken-current.log"; then
echo "ERROR: Broken Sparkle Versions/Current was accepted." >&2
exit 1
fi
grep -Fq "Versions/Current does not resolve" "$TEMP_DIR/broken-current.log"
ESCAPING_CURRENT="$TEMP_DIR/Escaping Current Sparkle.framework"
OUTSIDE_SPARKLE="$TEMP_DIR/Outside Sparkle.framework"
make_sparkle_version "$ESCAPING_CURRENT" B
make_sparkle_version "$OUTSIDE_SPARKLE" C
ln -s "$OUTSIDE_SPARKLE/Versions/C" "$ESCAPING_CURRENT/Versions/Current"
if codexbar_sparkle_version_dir "$ESCAPING_CURRENT" 2>"$TEMP_DIR/escaping-current.log"; then
echo "ERROR: Escaping Sparkle Versions/Current was accepted." >&2
exit 1
fi
grep -Fq "outside the framework versions directory" "$TEMP_DIR/escaping-current.log"
SYMLINKED_VERSIONS="$TEMP_DIR/Symlinked Versions Sparkle.framework"
mkdir -p "$SYMLINKED_VERSIONS"
ln -s "$OUTSIDE_SPARKLE/Versions" "$SYMLINKED_VERSIONS/Versions"
if codexbar_sparkle_version_dir "$SYMLINKED_VERSIONS" 2>"$TEMP_DIR/symlinked-versions.log"; then
echo "ERROR: Symlinked Sparkle Versions directory was accepted." >&2
exit 1
fi
grep -Fq "versions directory must not be a symlink" "$TEMP_DIR/symlinked-versions.log"
SYMLINKED_FRAMEWORK="$TEMP_DIR/Symlinked Sparkle.framework"
ln -s "$OUTSIDE_SPARKLE" "$SYMLINKED_FRAMEWORK"
if codexbar_sparkle_version_dir "$SYMLINKED_FRAMEWORK" 2>"$TEMP_DIR/symlinked-framework.log"; then
echo "ERROR: Symlinked Sparkle framework root was accepted." >&2
exit 1
fi
grep -Fq "framework root must not be a symlink" "$TEMP_DIR/symlinked-framework.log"
SYMLINKED_TARGET="$TEMP_DIR/Symlinked Target Sparkle.framework"
make_sparkle_version "$SYMLINKED_TARGET" B
rm "$SYMLINKED_TARGET/Versions/B/Autoupdate"
ln -s "$OUTSIDE_SPARKLE/Versions/C/Autoupdate" "$SYMLINKED_TARGET/Versions/B/Autoupdate"
if codexbar_sparkle_signing_targets \
"$SYMLINKED_TARGET" >"$TEMP_DIR/symlinked-target.out" 2>"$TEMP_DIR/symlinked-target.log"; then
echo "ERROR: Symlinked Sparkle signing target was accepted." >&2
exit 1
fi
grep -Fq "signing target must not be a symlink" "$TEMP_DIR/symlinked-target.log"
ESCAPING_TARGET_PARENT="$TEMP_DIR/Escaping Target Parent Sparkle.framework"
make_sparkle_version "$ESCAPING_TARGET_PARENT" B
mv "$ESCAPING_TARGET_PARENT/Versions/B/XPCServices" "$TEMP_DIR/displaced-xpc-services"
ln -s "$OUTSIDE_SPARKLE/Versions/C/XPCServices" "$ESCAPING_TARGET_PARENT/Versions/B/XPCServices"
if codexbar_sparkle_signing_targets \
"$ESCAPING_TARGET_PARENT" >"$TEMP_DIR/escaping-target-parent.out" 2>"$TEMP_DIR/escaping-target-parent.log"; then
echo "ERROR: Sparkle signing target with an escaping parent was accepted." >&2
exit 1
fi
grep -Fq "signing target resolves outside its trusted root" "$TEMP_DIR/escaping-target-parent.log"
ESCAPING_SINGLE="$TEMP_DIR/Escaping Single Sparkle.framework"
mkdir -p "$ESCAPING_SINGLE/Versions"
ln -s "$OUTSIDE_SPARKLE/Versions/C" "$ESCAPING_SINGLE/Versions/B"
if codexbar_sparkle_version_dir "$ESCAPING_SINGLE" 2>"$TEMP_DIR/escaping-single.log"; then
echo "ERROR: Escaping single Sparkle version directory was accepted." >&2
exit 1
fi
grep -Fq "outside the framework versions directory" "$TEMP_DIR/escaping-single.log"
echo "Sparkle signing path tests passed."
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/codexbar-test-sharding.XXXXXX")"
trap 'rm -rf "${TEMP_DIR}"' EXIT
IFS= read -r -d '' FAKE_SWIFT_SCRIPT <<'EOF' || true
set -euo pipefail
printf '%s\n' "$*" >> "${FAKE_SWIFT_LOG}"
if [[ "$*" == "test list" ]]; then
if [[ "${FAKE_SWIFT_MODE:-success}" == "list_fail" ]]; then
sleep 0.25
printf 'test-list stdout marker\n'
printf 'test-list stderr marker\n' >&2
exit 42
fi
printf '%s\n' \
"CodexBarTests.Alpha/test_one()" \
"CodexBarTests.Alpha/test_two(argument:)" \
"CodexBarTests.Beta/test_two" \
"CodexBarTests.Gamma/test_three" \
"CodexBarTests.Delta/test_four" \
"CodexBarTests.Epsilon/test_five" \
"CodexBarTests.Zeta/test_six" \
"CodexBarTests.Eta/test_seven" \
"CodexBarTests.Theta/test_eight" \
'CodexBarTests.`top level works`()' \
'CodexBarTests.`top/level slash works`()'
exit 0
fi
is_group=0
if [[ "$*" == *"|"* ]]; then
is_group=1
fi
next_group_attempt() {
local attempt=0
if [[ -f "${FAKE_SWIFT_STATE}" ]]; then
read -r attempt < "${FAKE_SWIFT_STATE}"
fi
attempt=$((attempt + 1))
printf '%s\n' "${attempt}" > "${FAKE_SWIFT_STATE}"
printf '%s\n' "${attempt}"
}
case "${FAKE_SWIFT_MODE:-success}" in
group_fail_once)
if [[ "${is_group}" == "1" && "$(next_group_attempt)" == "1" ]]; then
exit 1
fi
;;
group_always_fail)
if [[ "${is_group}" == "1" ]]; then
exit 1
fi
;;
group_timeout)
if [[ "${is_group}" == "1" ]]; then
sleep 2
fi
;;
singleton_timeout)
if [[ "${is_group}" == "0" ]]; then
sleep 2
fi
;;
group_fail_then_timeout)
if [[ "${is_group}" == "1" ]]; then
attempt="$(next_group_attempt)"
if [[ "${attempt}" == "1" ]]; then
exit 1
fi
sleep 2
fi
;;
esac
EOF
reset_case() {
local name="$1"
export FAKE_SWIFT_LOG="${TEMP_DIR}/${name}-swift.log"
export FAKE_SWIFT_STATE="${TEMP_DIR}/${name}-state"
export GITHUB_STEP_SUMMARY="${TEMP_DIR}/${name}-summary.md"
rm -f "${FAKE_SWIFT_LOG}" "${FAKE_SWIFT_STATE}" "${GITHUB_STEP_SUMMARY}"
}
run_harness() {
python3 "${ROOT_DIR}/Scripts/ci_swift_test_by_suite.py" \
"$@" \
--swift-command /bin/bash \
--swift-command-arg=-c \
--swift-command-arg="${FAKE_SWIFT_SCRIPT}" \
--swift-command-arg=fake-swift
}
python3 - "${ROOT_DIR}/.github/workflows/ci.yml" <<'PY'
import pathlib
import re
import sys
workflow = pathlib.Path(sys.argv[1]).read_text()
job_match = re.search(r"(?ms)^ swift-test-macos:\n(?P<body>.*?)(?=^ [a-zA-Z0-9_-]+:|\Z)", workflow)
if not job_match:
raise SystemExit("swift-test-macos job not found in CI workflow")
job = job_match.group("body")
if not re.search(r"(?m)^\s+shard-index:\s+\[0,\s*1\]\s*$", job):
raise SystemExit("swift-test-macos must run exactly two shard indexes: [0, 1]")
if not re.search(r"(?m)^\s+shard-count:\s+\[2\]\s*$", job):
raise SystemExit("swift-test-macos shard-count must be [2]")
if "CODEXBAR_TEST_SHARD_INDEX=${{ matrix.shard-index }}" not in job:
raise SystemExit("swift-test-macos must pass matrix.shard-index to Scripts/test.sh")
if "CODEXBAR_TEST_SHARD_COUNT=${{ matrix.shard-count }}" not in job:
raise SystemExit("swift-test-macos must pass matrix.shard-count to Scripts/test.sh")
PY
reset_case retry
export FAKE_SWIFT_MODE=group_fail_once
run_harness --group-size 4 --timeout 10 > "${TEMP_DIR}/retry.log"
grep -Fq "failed with exit code 1; retrying group once" "${TEMP_DIR}/retry.log"
grep -Fq "Swift test timing summary:" "${TEMP_DIR}/retry.log"
grep -Fq '| Discovered selections | `10` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Selected selections | `10` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Selected groups | `3` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| First-pass successful groups | `2` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| First-pass failed groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Full-group retries | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Recovered groups | `1` |' "${GITHUB_STEP_SUMMARY}"
[[ "$(grep -c '^test --skip-build --no-parallel' "${FAKE_SWIFT_LOG}")" -eq 4 ]]
grep -Fq "CodexBarTests\\.Alpha" "${FAKE_SWIFT_LOG}"
grep -Fq "CodexBarTests\\.Beta" "${FAKE_SWIFT_LOG}"
grep -Fq "CodexBarTests\\..*top\\ level\\ works" "${FAKE_SWIFT_LOG}"
grep -Fq "CodexBarTests\\..*top/level\\ slash\\ works" "${FAKE_SWIFT_LOG}"
[[ "$(wc -l < "${FAKE_SWIFT_LOG}")" -eq 5 ]]
reset_case strict
export FAKE_SWIFT_MODE=group_fail_once
set +e
CODEXBAR_TEST_GROUP_SIZE=4 \
CODEXBAR_TEST_SUITE_TIMEOUT=10 \
CODEXBAR_TEST_RETRY_NON_TIMEOUT_FAILURES=0 \
"${ROOT_DIR}/Scripts/test.sh" \
--limit-groups 1 \
--swift-command /bin/bash \
--swift-command-arg=-c \
--swift-command-arg="${FAKE_SWIFT_SCRIPT}" \
--swift-command-arg=fake-swift \
> "${TEMP_DIR}/strict.log" 2>&1
strict_status=$?
set -e
[[ "${strict_status}" -eq 1 ]]
grep -Fq '| First-pass failed groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Full-group retries | `0` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Isolated selection retries | `0` |' "${GITHUB_STEP_SUMMARY}"
[[ "$(wc -l < "${FAKE_SWIFT_LOG}")" -eq 2 ]]
reset_case shard-0
export FAKE_SWIFT_MODE=success
run_harness --group-size 4 --timeout 10 --shard-index 0 --shard-count 2 > "${TEMP_DIR}/shard-0.log"
grep -Fq '| Shard | `1/2` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Selected selections | `6` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Selected groups | `2` |' "${GITHUB_STEP_SUMMARY}"
reset_case shard-1
run_harness --group-size 4 --timeout 10 --shard-index 1 --shard-count 2 > "${TEMP_DIR}/shard-1.log"
grep -Fq '| Shard | `2/2` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Selected selections | `4` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Selected groups | `1` |' "${GITHUB_STEP_SUMMARY}"
reset_case shard-list-0
run_harness --group-size 4 --timeout 10 --shard-index 0 --shard-count 2 --list-only \
> "${TEMP_DIR}/shard-list-0.log"
reset_case shard-list-1
run_harness --group-size 4 --timeout 10 --shard-index 1 --shard-count 2 --list-only \
> "${TEMP_DIR}/shard-list-1.log"
cat "${TEMP_DIR}/shard-list-0.log" "${TEMP_DIR}/shard-list-1.log" \
| grep -v '^Discovered ' \
| sort > "${TEMP_DIR}/shards-combined.log"
reset_case shard-list-all
run_harness --group-size 4 --timeout 10 --list-only \
| grep -v '^Discovered ' \
| sort > "${TEMP_DIR}/shards-expected.log"
diff -u "${TEMP_DIR}/shards-expected.log" "${TEMP_DIR}/shards-combined.log"
reset_case group-timeout
export FAKE_SWIFT_MODE=group_timeout
run_harness --group-size 4 --limit-groups 1 --timeout 1 > "${TEMP_DIR}/group-timeout.log"
grep -Fq "timed out; retrying selections one at a time" "${TEMP_DIR}/group-timeout.log"
grep -Fq '| Timed out groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Recovered groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Isolated selection retries | `4` |' "${GITHUB_STEP_SUMMARY}"
reset_case singleton-timeout
export FAKE_SWIFT_MODE=singleton_timeout
set +e
run_harness --group-size 1 --limit-groups 1 --timeout 1 > "${TEMP_DIR}/singleton-timeout.log" 2>&1
singleton_timeout_status=$?
set -e
[[ "${singleton_timeout_status}" -eq 124 ]]
grep -Fq '| Timed out groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Recovered groups | `0` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Isolated selection retries | `0` |' "${GITHUB_STEP_SUMMARY}"
reset_case retry-timeout
export FAKE_SWIFT_MODE=group_fail_then_timeout
run_harness --group-size 4 --limit-groups 1 --timeout 1 > "${TEMP_DIR}/retry-timeout.log"
grep -Fq '| Full-group retries | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Timed out groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Recovered groups | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Isolated selection retries | `4` |' "${GITHUB_STEP_SUMMARY}"
reset_case repeated-failure
export FAKE_SWIFT_MODE=group_always_fail
set +e
run_harness --group-size 4 --limit-groups 1 --timeout 10 > "${TEMP_DIR}/failure.log" 2>&1
failure_status=$?
set -e
[[ "${failure_status}" -eq 1 ]]
grep -Fq '| Full-group retries | `1` |' "${GITHUB_STEP_SUMMARY}"
grep -Fq '| Recovered groups | `0` |' "${GITHUB_STEP_SUMMARY}"
reset_case list-failure
export FAKE_SWIFT_MODE=list_fail
set +e
run_harness --group-size 1 --timeout 10 > "${TEMP_DIR}/list-failure.log" 2>&1
list_failure_status=$?
set -e
[[ "${list_failure_status}" -ne 0 ]]
grep -Fq "test-list stdout marker" "${TEMP_DIR}/list-failure.log"
grep -Fq "test-list stderr marker" "${TEMP_DIR}/list-failure.log"
grep -Eq -- '- Discovery seconds: 0\.[1-9]' "${TEMP_DIR}/list-failure.log"
grep -Fq '| Discovered selections | `0` |' "${GITHUB_STEP_SUMMARY}"
echo "Swift test sharding tests passed."
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=${1:?"usage: $0 <version>"}
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
first_line=$(grep -m1 '^## ' CHANGELOG.md | sed 's/^## //')
if [[ "$first_line" != ${VERSION}* ]]; then
echo "ERROR: Top CHANGELOG section is '$first_line' but expected '${VERSION} — …'" >&2
exit 1
fi
grep -q "^## ${VERSION} " CHANGELOG.md || {
echo "ERROR: No section for version ${VERSION} in CHANGELOG.md" >&2
exit 1
}
grep -q '^## [0-9]\+\.[0-9]\+\.[0-9].*Unreleased' CHANGELOG.md && {
echo "ERROR: Top section still labeled Unreleased; finalize changelog first." >&2
exit 1
}
echo "Changelog OK for ${VERSION}"
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env bash
# Isolated live verification for CodexBar #1844 / PR #1848.
# Uses only synthetic credentials under a disposable HOME and keychain.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
log() { printf '[verify-1844] %s\n' "$*"; }
ARTIFACT="$(mktemp -d "${TMPDIR:-/tmp}/codexbar-1844-verify.XXXXXX")"
chmod 700 "$ARTIFACT"
HOME_FIXTURE="$ARTIFACT/home"
KEYCHAIN="$ARTIFACT/claude-fixture.keychain-db"
KEYCHAIN_PASSWORD="codexbar-1844-synthetic-fixture"
CONFIG="$ARTIFACT/config.json"
CLI="${CODEXBAR_CLI:-$ROOT/CodexBar.app/Contents/Helpers/CodexBarCLI}"
APP="${CODEXBAR_APP_BINARY:-$ROOT/CodexBar.app/Contents/MacOS/CodexBar}"
MCP_PAYLOAD='{"mcpOAuth":{"plugin:synthetic":{"accessToken":"synthetic-mcp-token"}}}'
EXPIRED_PAYLOAD='{"claudeAiOauth":{"accessToken":"synthetic-expired-token","expiresAt":1000,"scopes":["user:profile"],"refreshToken":"synthetic-refresh-token"}}'
if [[ ! -x "$CLI" ]]; then
log "Missing packaged CLI: $CLI"
log "Run ./Scripts/package_app.sh, then retry."
exit 2
fi
if [[ ! -x "$APP" ]]; then
log "Missing packaged app binary: $APP"
exit 2
fi
cleanup() {
/usr/bin/security delete-keychain "$KEYCHAIN" >/dev/null 2>&1 || true
}
trap cleanup EXIT
log "Artifacts: $ARTIFACT"
log "Phase 1: focused integration tests"
{
swift test --filter ClaudeOAuthTests
swift test --filter ClaudeUsageTests
swift test --filter ClaudeOAuthDelegatedRefreshCoordinatorTests
swift test --filter 'expired claude CLI owner blocks background'
swift test --filter ClaudeOAuthCredentialsStoreSecurityCLITests
swift test --filter ClaudeOAuthCredentialsStoreIsolatedSecurityCLITests
swift test --filter ClaudeOAuthCredentialsStoreMCPOnlyGuardTests
} 2>&1 | tee "$ARTIFACT/integration-tests.log"
log "Phase 1 passed"
log "Phase 2: disposable HOME, keychain, credentials, config, and Claude CLI canary"
mkdir -p "$HOME_FIXTURE/.claude" "$HOME_FIXTURE/Library/Preferences" "$ARTIFACT/bin"
chmod 700 "$HOME_FIXTURE" "$HOME_FIXTURE/.claude" "$HOME_FIXTURE/Library" \
"$HOME_FIXTURE/Library/Preferences" "$ARTIFACT/bin"
printf '%s\n' "$EXPIRED_PAYLOAD" >"$HOME_FIXTURE/.claude/.credentials.json"
chmod 600 "$HOME_FIXTURE/.claude/.credentials.json"
printf '%s\n' '{"version":1,"providers":[{"id":"claude","enabled":true,"source":"oauth"}]}' >"$CONFIG"
chmod 600 "$CONFIG"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "args:" >>"$CODEXBAR_CLAUDE_INVOCATIONS"' \
'printf " %q" "$@" >>"$CODEXBAR_CLAUDE_INVOCATIONS"' \
'printf "\\n" >>"$CODEXBAR_CLAUDE_INVOCATIONS"' \
'if [[ "$*" == "auth status --json" ]]; then printf "{\"loggedIn\":true}\\n"; exit 0; fi' \
'if [[ "$*" == "--version" ]]; then printf "2.1.0\\n"; exit 0; fi' \
'if IFS= read -r line; then' \
' printf "stdin:%s\\n" "$line" >>"$CODEXBAR_CLAUDE_INVOCATIONS"' \
' if [[ "$line" == *"/status"* ]]; then printf touched >"$CODEXBAR_CLAUDE_TOUCH_CANARY"; fi' \
'fi' \
'exit 99' \
>"$ARTIFACT/bin/claude"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf touched >"$CODEXBAR_OPEN_TOUCH_CANARY"' \
'exit 99' \
>"$ARTIFACT/bin/open"
chmod 700 "$ARTIFACT/bin/claude" "$ARTIFACT/bin/open"
/usr/bin/security list-keychains -d user >"$ARTIFACT/keychains-before.txt"
/usr/bin/security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
/usr/bin/security set-keychain-settings -t 3600 "$KEYCHAIN"
/usr/bin/security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
/usr/bin/security add-generic-password \
-a codexbar-verify-1844 \
-s 'Claude Code-credentials' \
-w "$MCP_PAYLOAD" \
-A \
"$KEYCHAIN"
/usr/bin/security list-keychains -d user >"$ARTIFACT/keychains-after.txt"
if ! cmp -s "$ARTIFACT/keychains-before.txt" "$ARTIFACT/keychains-after.txt"; then
log "Phase 2 failed: creating the disposable keychain changed the user search list"
exit 1
fi
/usr/bin/security find-generic-password \
-s 'Claude Code-credentials' \
-w \
"$KEYCHAIN" >"$ARTIFACT/keychain-fixture.json"
cmp -s "$ARTIFACT/keychain-fixture.json" <(printf '%s\n' "$MCP_PAYLOAD")
PROC_LOG="$ARTIFACT/e2e-processes.log"
STDOUT="$ARTIFACT/e2e-stdout.json"
STDERR="$ARTIFACT/e2e-stderr.jsonl"
CANARY="$ARTIFACT/claude-status-canary"
INVOCATIONS="$ARTIFACT/claude-invocations.log"
OPEN_CANARY="$ARTIFACT/open-touch-canary"
: >"$PROC_LOG"
: >"$INVOCATIONS"
set +e
(
env \
HOME="$HOME_FIXTURE" \
CFFIXED_USER_HOME="$HOME_FIXTURE" \
CODEXBAR_CONFIG="$CONFIG" \
CODEXBAR_DISABLE_KEYCHAIN_ACCESS=1 \
CODEXBAR_CLAUDE_SECURITY_CLI_KEYCHAIN="$KEYCHAIN" \
CODEXBAR_CLAUDE_TOUCH_CANARY="$CANARY" \
CODEXBAR_CLAUDE_INVOCATIONS="$INVOCATIONS" \
CODEXBAR_OPEN_TOUCH_CANARY="$OPEN_CANARY" \
CODEXBAR_DEBUG_CLAUDE_OAUTH_FLOW=1 \
CLAUDE_CLI_PATH="$ARTIFACT/bin/claude" \
PATH="$ARTIFACT/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
"$CLI" usage --provider claude --source oauth --format json --pretty --log-level debug \
>"$STDOUT" 2>"$STDERR"
) &
PID=$!
while kill -0 "$PID" 2>/dev/null; do
{
date -u +%H:%M:%S
pgrep -P "$PID" -l 2>/dev/null || true
} >>"$PROC_LOG"
sleep 0.02
done
wait "$PID"
CLI_STATUS=$?
set -e
{
echo "# CodexBar #1844 isolated E2E verification"
echo "date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "candidate: $(git rev-parse HEAD)"
echo "packaged-cli: $CLI"
echo "cli-exit: $CLI_STATUS"
echo "default-keychain-search-list-unchanged: yes"
echo "real-home-referenced: no"
echo "claude-status-canary: $([[ -e "$CANARY" ]] && echo touched || echo untouched)"
echo "open-touch-canary: $([[ -e "$OPEN_CANARY" ]] && echo touched || echo untouched)"
echo
echo "## stdout"
cat "$STDOUT"
echo
echo "## stderr (filtered)"
rg -i 'mcp|delegated|expired|oauth|touch|open|only prompt|user action' "$STDERR" || true
echo
echo "## Claude CLI invocations"
cat "$INVOCATIONS"
echo
echo "## child processes"
cat "$PROC_LOG"
} | tee "$ARTIFACT/E2E-REPORT.md"
if [[ "$CLI_STATUS" -eq 0 ]]; then
log "Phase 2 failed: the MCP-only fixture unexpectedly produced successful OAuth usage"
exit 1
fi
if [[ -e "$CANARY" ]]; then
log "Phase 2 failed: delegated Claude CLI /status touch ran"
exit 1
fi
if [[ -e "$OPEN_CANARY" ]]; then
log "Phase 2 failed: browser/open helper ran"
exit 1
fi
if rg -q '/usr/bin/open|(^|/)open$|firefox|Google Chrome|Safari' "$PROC_LOG" 2>/dev/null; then
log "Phase 2 failed: an open helper or browser was a probe child"
exit 1
fi
if ! rg -qi 'MCP OAuth state only|mcpOAuthOnlyKeychain|MCP OAuth' "$STDERR" "$STDOUT"; then
log "Phase 2 failed: expected MCP-only fail-closed message not found"
exit 1
fi
log "Phase 2 passed: exact packaged CLI failed closed without delegated /status touch or browser child"
log "Phase 3: isolated packaged app runtime smoke"
APP_PROC_LOG="$ARTIFACT/app-processes.log"
APP_STDOUT="$ARTIFACT/app-stdout.log"
APP_STDERR="$ARTIFACT/app-stderr.log"
: >"$APP_PROC_LOG"
: >"$INVOCATIONS"
(
env \
HOME="$HOME_FIXTURE" \
CFFIXED_USER_HOME="$HOME_FIXTURE" \
CODEXBAR_CONFIG="$CONFIG" \
CODEXBAR_DISABLE_KEYCHAIN_ACCESS=1 \
CODEXBAR_CLAUDE_SECURITY_CLI_KEYCHAIN="$KEYCHAIN" \
CODEXBAR_CLAUDE_TOUCH_CANARY="$CANARY" \
CODEXBAR_CLAUDE_INVOCATIONS="$INVOCATIONS" \
CODEXBAR_OPEN_TOUCH_CANARY="$OPEN_CANARY" \
CODEXBAR_DEBUG_CLAUDE_OAUTH_FLOW=1 \
CLAUDE_CLI_PATH="$ARTIFACT/bin/claude" \
PATH="$ARTIFACT/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
"$APP" >"$APP_STDOUT" 2>"$APP_STDERR"
) &
APP_PID=$!
APP_OBSERVED_CLI=0
POST_DISCOVERY_TICKS=0
for _ in $(seq 1 1000); do
if ! kill -0 "$APP_PID" 2>/dev/null; then
log "Phase 3 failed: packaged app exited before the isolated startup smoke completed"
wait "$APP_PID" || true
exit 1
fi
{
date -u +%H:%M:%S
pgrep -P "$APP_PID" -l 2>/dev/null || true
} >>"$APP_PROC_LOG"
if rg -q '^args: --version$' "$INVOCATIONS"; then
APP_OBSERVED_CLI=1
POST_DISCOVERY_TICKS=$((POST_DISCOVERY_TICKS + 1))
if [[ "$POST_DISCOVERY_TICKS" -ge 250 ]]; then
break
fi
fi
sleep 0.02
done
kill "$APP_PID"
wait "$APP_PID" 2>/dev/null || true
if [[ "$APP_OBSERVED_CLI" -ne 1 ]]; then
log "Phase 3 failed: packaged app never exercised the isolated Claude CLI fixture"
exit 1
fi
if [[ -e "$CANARY" ]]; then
log "Phase 3 failed: packaged app invoked delegated Claude CLI /status touch"
exit 1
fi
if [[ -e "$OPEN_CANARY" ]]; then
log "Phase 3 failed: packaged app invoked browser/open helper"
exit 1
fi
if rg -q '/usr/bin/open|(^|/)open$|firefox|Google Chrome|Safari' "$APP_PROC_LOG" 2>/dev/null; then
log "Phase 3 failed: an open helper or browser was an app child"
exit 1
fi
{
echo
echo "## packaged app runtime"
echo "app-binary: $APP"
echo "isolated-claude-cli-discovery-observed: yes"
echo "post-discovery-observation-seconds: 5"
echo "app-stayed-running: yes"
echo "claude-status-canary: untouched"
echo "open-touch-canary: untouched"
echo "browser-child: none"
echo
echo "## packaged app Claude CLI invocations"
cat "$INVOCATIONS"
} | tee -a "$ARTIFACT/E2E-REPORT.md"
log "Phase 3 passed: packaged app exercised CLI discovery without delegated /status touch or browser child"
log "Report: $ARTIFACT/E2E-REPORT.md"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
exec "$SCRIPT_DIR/mac-release" verify-appcast "$@"