Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
161 lines
4.0 KiB
JavaScript
161 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
import { basename } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { buildLimitedGithubReleaseBody } from "./lib/githubReleaseBody.mjs";
|
|
|
|
const SECTION_START = "<!-- tutti-desktop-download-links:start -->";
|
|
const SECTION_END = "<!-- tutti-desktop-download-links:end -->";
|
|
|
|
function normalizeBaseUrl(value) {
|
|
return value.replace(/\/+$/, "");
|
|
}
|
|
|
|
function escapeRegex(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function removeManagedSection(body) {
|
|
const managedSectionPattern = new RegExp(
|
|
`\\n*${escapeRegex(SECTION_START)}[\\s\\S]*?${escapeRegex(SECTION_END)}\\n*`,
|
|
"g"
|
|
);
|
|
|
|
return body.replace(managedSectionPattern, "\n").trimEnd();
|
|
}
|
|
|
|
function findAssetName(assetNames, pattern) {
|
|
return assetNames.find((name) => {
|
|
pattern.lastIndex = 0;
|
|
return pattern.test(name);
|
|
});
|
|
}
|
|
|
|
function resolveDesktopDownloadLinks(
|
|
assetNames,
|
|
releaseAssetPath,
|
|
releaseAssetBaseUrl
|
|
) {
|
|
const desktopAssets = [
|
|
{
|
|
label: "macOS Apple Silicon (arm64)",
|
|
pattern: /-mac-arm64\.dmg$/i
|
|
},
|
|
{
|
|
label: "macOS Intel (x64)",
|
|
pattern: /-mac-x64\.dmg$/i
|
|
},
|
|
{
|
|
label: "macOS Universal",
|
|
pattern: /-mac-universal\.dmg$/i
|
|
},
|
|
{
|
|
label: "Windows (x64, signed)",
|
|
pattern: /-win-x64\.exe$/i
|
|
}
|
|
];
|
|
const normalizedBaseUrl = normalizeBaseUrl(releaseAssetBaseUrl);
|
|
const normalizedAssetPath = releaseAssetPath
|
|
.split("/")
|
|
.filter(Boolean)
|
|
.map(encodeURIComponent)
|
|
.join("/");
|
|
|
|
return desktopAssets
|
|
.map(({ label, pattern }) => {
|
|
const assetName = findAssetName(assetNames, pattern);
|
|
if (!assetName) {
|
|
return null;
|
|
}
|
|
|
|
const assetUrl = `${normalizedBaseUrl}/${normalizedAssetPath}/${encodeURIComponent(assetName)}`;
|
|
return `- [${label}](${assetUrl})`;
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function buildUpdatedReleaseBody({
|
|
assetNames,
|
|
existingBody,
|
|
releaseAssetBaseUrl,
|
|
releaseTag,
|
|
releaseAssetPath = releaseTag
|
|
}) {
|
|
const cleanedBody = removeManagedSection(existingBody);
|
|
|
|
if (!releaseAssetPath || !releaseAssetBaseUrl) {
|
|
return buildLimitedGithubReleaseBody({ existingBody: cleanedBody });
|
|
}
|
|
|
|
const directDownloadLinks = resolveDesktopDownloadLinks(
|
|
[...assetNames].sort((left, right) => left.localeCompare(right)),
|
|
releaseAssetPath,
|
|
releaseAssetBaseUrl
|
|
);
|
|
|
|
if (directDownloadLinks.length === 0) {
|
|
return buildLimitedGithubReleaseBody({ existingBody: cleanedBody });
|
|
}
|
|
|
|
const downloadSection = [
|
|
SECTION_START,
|
|
"### Direct Downloads",
|
|
...directDownloadLinks,
|
|
SECTION_END
|
|
].join("\n");
|
|
return buildLimitedGithubReleaseBody({
|
|
existingBody: cleanedBody,
|
|
trailingSections: [downloadSection]
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const [existingBodyPath, assetDirPath, outputBodyPath] =
|
|
process.argv.slice(2);
|
|
|
|
if (!existingBodyPath || !assetDirPath || !outputBodyPath) {
|
|
throw new Error(
|
|
"Usage: node apps/desktop/scripts/upsert-release-download-links.mjs <existing-body-path> <asset-dir> <output-body-path>"
|
|
);
|
|
}
|
|
|
|
const releaseTag = (process.env.RELEASE_TAG ?? "").trim();
|
|
const releaseAssetPath = (
|
|
process.env.RELEASE_ASSET_PATH ?? releaseTag
|
|
).trim();
|
|
const releaseAssetBaseUrl = (process.env.RELEASE_ASSET_BASE_URL ?? "").trim();
|
|
const existingBody = await readFile(existingBodyPath, "utf8");
|
|
const assetNames = (await readdir(assetDirPath)).map((name) =>
|
|
basename(name)
|
|
);
|
|
const nextBody = buildUpdatedReleaseBody({
|
|
assetNames,
|
|
existingBody,
|
|
releaseAssetBaseUrl,
|
|
releaseAssetPath,
|
|
releaseTag
|
|
});
|
|
|
|
await writeFile(outputBodyPath, nextBody, "utf8");
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
) {
|
|
void main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
export {
|
|
SECTION_END,
|
|
SECTION_START,
|
|
buildUpdatedReleaseBody,
|
|
findAssetName,
|
|
removeManagedSection,
|
|
resolveDesktopDownloadLinks
|
|
};
|