openpetswithchatandmcp/apps/desktop/scripts/release-local.mjs
OpenPets Dev 6ab3bb64d8 feat(rebrand): rename OpenPets to FamiliarOS and pets to familiars
- Rename all user-facing and technical identifiers from OpenPets/Pet to FamiliarOS/Familiar.
- Rename packages from @open-pets/* to @familiaros/*; rename install-pet/pet-format packages.
- Rename plugin IDs and directories from openpets.* to familiaros.*.
- Rename IPC namespace from openpets:* to familiaros:* and state filenames from openpets-* to familiaros-* with legacy migration.
- Rename source files (pet-window, built-in-pet, default-pet-controller, etc.) to familiar equivalents.
- Update locales (en, es-419, ja, ko, pt-BR, zh-Hans, zh-Hant) and tray/pet context menu strings.
- Add Familiar naming feature: preference, settings input, tray menu display.
- Update assets and packaging config; all desktop tests pass.
2026-06-17 01:42:08 +00:00

248 lines
12 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { basename, dirname, extname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const scriptsDir = dirname(fileURLToPath(import.meta.url));
const desktopDir = resolve(scriptsDir, "..");
const repoRoot = resolve(desktopDir, "../..");
const outputDir = join(desktopDir, "dist-electron");
const repository = "alvinunreal/familiaros";
const allowedArgs = new Set([
"--dry-run",
"--yes",
"--include-optional",
"--include-mac-zip",
"--include-win-portable",
"--include-linux-deb",
"--include-linux-rpm",
"--include-linux-targz",
"--include-experimental-arm",
"--skip-checks",
"--help",
]);
const rawArgs = process.argv.slice(2).filter((arg) => arg !== "--");
const unknownArgs = rawArgs.filter((arg) => !allowedArgs.has(arg));
if (unknownArgs.length > 0) throw new Error(`Unknown release option(s): ${unknownArgs.join(", ")}`);
const args = new Set(rawArgs);
const dryRun = args.has("--dry-run");
const yes = args.has("--yes");
const includeOptional = args.has("--include-optional");
const includeMacZip = includeOptional || args.has("--include-mac-zip");
const includeWinPortable = includeOptional || args.has("--include-win-portable");
const includeLinuxDeb = includeOptional || args.has("--include-linux-deb");
const includeLinuxRpm = includeOptional || args.has("--include-linux-rpm");
const includeLinuxTarGz = includeOptional || args.has("--include-linux-targz");
const includeExperimentalArm = args.has("--include-experimental-arm");
const skipChecks = args.has("--skip-checks");
if (skipChecks && yes) throw new Error("Refusing to create a release with --skip-checks. Run checks before using --yes.");
if (args.has("--help")) {
printHelp();
process.exit(0);
}
const desktopPackageJson = readJson(join(desktopDir, "package.json"));
const version = desktopPackageJson.version;
const tag = `v${version}`;
main();
function main() {
preflight();
if (!skipChecks) {
run("pnpm", ["build"], { cwd: repoRoot });
run("pnpm", ["--filter", "@familiaros/desktop", "check"], { cwd: repoRoot });
}
run("node", ["scripts/clean-package-output.cjs"], { cwd: desktopDir });
mkdirSync(outputDir, { recursive: true });
for (const build of createBuildPlan()) {
run("pnpm", ["exec", "electron-builder", ...build.args, "--publish", "never"], { cwd: desktopDir });
}
const postBuildStatus = getGitStatusIgnoringPackageOutput();
if (postBuildStatus) throw new Error(`Build/checks changed tracked or source files. Commit or revert them before releasing.\n${postBuildStatus}`);
const artifacts = collectArtifacts(outputDir);
if (artifacts.length === 0) throw new Error("No release artifacts were produced.");
const checksumsPath = writeChecksums(artifacts);
const uploadArtifacts = [...artifacts, checksumsPath];
console.log("\nRelease artifacts:");
for (const artifact of uploadArtifacts) console.log(`- ${relative(repoRoot, artifact)}`);
if (!yes && !dryRun) {
throw new Error("Re-run with --yes to create the draft GitHub release after reviewing the artifact list.");
}
if (dryRun) {
console.log(`\nDry run complete. Would create draft release ${tag} in ${repository}.`);
return;
}
const target = commandOutput("git", ["rev-parse", "HEAD"], { cwd: repoRoot }).trim();
run("gh", ["release", "create", tag, "--repo", repository, "--target", target, "--title", `FamiliarOS ${tag}`, "--notes", defaultReleaseNotes()], { cwd: repoRoot });
run("gh", ["release", "upload", tag, "--repo", repository, ...uploadArtifacts], { cwd: repoRoot });
console.log(`\nPublished release created: https://github.com/${repository}/releases/tag/${tag}`);
console.log("Published releases are visible to the app update checker.");
}
function preflight() {
if (process.platform !== "darwin") throw new Error("This local release script is intended to run from macOS.");
if (!isStableSemver(version) || version === "0.0.0") {
throw new Error(`Desktop package version must be a stable non-zero semver version. Current: ${version}`);
}
requireCommand("pnpm", ["--version"]);
requireCommand("gh", ["--version"]);
run("gh", ["auth", "status", "--hostname", "github.com"], { cwd: repoRoot });
const remoteUrl = commandOutput("git", ["remote", "get-url", "origin"], { cwd: repoRoot }).trim();
if (!remoteUrl.includes(repository)) {
throw new Error(`Expected origin remote to point at ${repository}. Current origin: ${remoteUrl}`);
}
const status = commandOutput("git", ["status", "--porcelain"], { cwd: repoRoot }).trim();
if (status) throw new Error(`Git working tree must be clean before release.\n${status}`);
run("git", ["rev-parse", "--verify", "HEAD"], { cwd: repoRoot });
const upstream = commandOutput("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], { cwd: repoRoot }).trim();
if (!upstream) throw new Error("Release branch must have an upstream remote branch.");
run("git", ["fetch", "--tags", "origin"], { cwd: repoRoot });
const localHead = commandOutput("git", ["rev-parse", "HEAD"], { cwd: repoRoot }).trim();
const remoteHead = commandOutput("git", ["rev-parse", upstream], { cwd: repoRoot }).trim();
if (localHead !== remoteHead) throw new Error(`HEAD must be pushed to ${upstream} before release.`);
if (commandSucceeds("git", ["rev-parse", "--verify", `refs/tags/${tag}`], { cwd: repoRoot })) {
throw new Error(`Git tag already exists locally: ${tag}`);
}
if (commandSucceeds("git", ["ls-remote", "--exit-code", "--tags", "origin", tag], { cwd: repoRoot })) {
throw new Error(`Git tag already exists on origin: ${tag}`);
}
if (commandSucceeds("gh", ["release", "view", tag, "--repo", repository], { cwd: repoRoot })) {
throw new Error(`GitHub release already exists: ${tag}`);
}
}
function createBuildPlan() {
const plan = [
{ name: "mac dmg x64+arm64", args: ["--mac", "dmg", "--x64", "--arm64"] },
{ name: "windows nsis x64", args: ["--win", "nsis", "--x64"] },
{ name: "linux AppImage x64", args: ["--linux", "AppImage", "--x64"] },
];
if (includeMacZip) plan.push({ name: "mac zip x64+arm64", args: ["--mac", "zip", "--x64", "--arm64"] });
if (includeWinPortable) plan.push({ name: "windows portable x64", args: ["--win", "portable", "--x64"] });
if (includeLinuxDeb) plan.push({ name: "linux deb x64", args: ["--linux", "deb", "--x64"] });
if (includeLinuxRpm) plan.push({ name: "linux rpm x64", args: ["--linux", "rpm", "--x64"] });
if (includeLinuxTarGz) plan.push({ name: "linux tar.gz x64", args: ["--linux", "tar.gz", "--x64"] });
if (includeExperimentalArm) {
plan.push({ name: "windows nsis arm64", args: ["--win", "nsis", "--arm64"] });
plan.push({ name: "linux AppImage arm64", args: ["--linux", "AppImage", "--arm64"] });
}
console.log("Build plan:");
for (const build of plan) console.log(`- ${build.name}`);
return plan;
}
function collectArtifacts(dir) {
const allowedNames = new Set(["SHA256SUMS"]);
const allowedExtensions = new Set([".dmg", ".zip", ".exe", ".AppImage", ".deb", ".rpm"]);
const artifacts = [];
for (const entry of readdirSync(dir)) {
const filePath = join(dir, entry);
const stat = statSync(filePath);
if (!stat.isFile()) continue;
const name = basename(filePath);
if (allowedNames.has(name) || allowedExtensions.has(extname(name)) || name.endsWith(".tar.gz")) artifacts.push(filePath);
}
return artifacts.filter((path) => basename(path) !== "SHA256SUMS").sort();
}
function writeChecksums(artifacts) {
const lines = artifacts.map((artifact) => `${sha256(artifact)} ${basename(artifact)}`);
const checksumsPath = join(outputDir, "SHA256SUMS");
writeFileSync(checksumsPath, `${lines.join("\n")}\n`);
return checksumsPath;
}
function sha256(filePath) {
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function getGitStatusIgnoringPackageOutput() {
return commandOutput("git", ["status", "--porcelain"], { cwd: repoRoot })
.split("\n")
.filter((line) => line.trim() && !line.includes("apps/desktop/dist-electron/"))
.join("\n");
}
function isStableSemver(value) {
return /^\d+\.\d+\.\d+$/.test(value);
}
function requireCommand(command, args) {
if (!commandSucceeds(command, args, { cwd: repoRoot })) throw new Error(`Required command is unavailable: ${command}`);
}
function commandSucceeds(command, args, options) {
return spawnSync(command, args, { cwd: options.cwd, stdio: "ignore" }).status === 0;
}
function commandOutput(command, args, options) {
const result = spawnSync(command, args, { cwd: options.cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout}`);
return result.stdout;
}
function run(command, args, options) {
console.log(`\n$ ${command} ${args.join(" ")}`);
const result = spawnSync(command, args, { cwd: options.cwd, stdio: "inherit" });
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}.`);
}
function defaultReleaseNotes() {
return [
`FamiliarOS ${tag} ships the first official plugin release with polished desktop plugin management.`,
"",
"## New: FamiliarOS Plugins",
"",
"FamiliarOS now includes a first-party plugin platform for bundled desktop companion behaviors.",
"",
"## Included plugins",
"",
"- Ambient Companion — gentle idle check-ins and ambient familiar moments, enabled by default.",
"- Break Buddy — friendly stretch, hydrate, and rest reminders, enabled by default.",
"- Familiar Pal — playful familiar interactions and quick companion actions, enabled by default.",
"- Focus Buddy — passive focus-session controls available from the familiar menu, enabled by default.",
"- GitHub Notifications — developer/advanced public repository release and failed-workflow notifications, disabled by default. No GitHub login, token, or private repository access is used.",
"",
"## Plugin management",
"",
"- New polished Plugins window with install, enable, configure, update, reload, and uninstall actions.",
"- Friendly plugin configuration UI; no JSON editing required.",
"- Plugin permissions, network hosts, and official plugin icons are explicit.",
"- JavaScript plugins run in a sandboxed renderer with a narrow FamiliarOS SDK.",
"",
"## Developer notes",
"",
"- Official plugin source now lives under the repository-level plugins/official directory.",
"- Local plugin development is available through explicit developer mode and pnpm dev:desktop:plugins.",
"- Legacy sample plugins were removed from public discovery.",
"- This release includes optional desktop artifacts: macOS ZIP, Windows portable, Linux DEB/RPM, and Linux tar.gz.",
"",
"## Known limitations",
"",
"- GitHub Notifications supports public repositories only in this release.",
"- Desktop artifacts are currently unsigned, so OS security warnings may appear.",
].join("\n");
}
function printHelp() {
console.log(`Usage: pnpm release:desktop -- --yes\n\nBuilds local desktop release artifacts, creates a published GitHub release, and uploads artifacts.\n\nDefault targets:\n - macOS dmg x64+arm64\n - Windows nsis x64\n - Linux AppImage x64\n\nOptions:\n --yes create the published GitHub release after building\n --dry-run run checks/builds and print what would be released\n --skip-checks skip pnpm build and desktop check\n --include-optional include all optional x64 targets below\n --include-mac-zip also build macOS zip x64+arm64\n --include-win-portable also build Windows portable x64\n --include-linux-deb also build Linux deb x64\n --include-linux-rpm also build Linux rpm x64\n --include-linux-targz also build Linux tar.gz x64\n --include-experimental-arm also build Windows/Linux ARM64 artifacts\n`);
}