Files
buzz/desktop/scripts/demo-build-config.mjs
Logan Johnson 4a9de1a3a1 feat(desktop): add isolated named demo builds (#6407)
🤖 I’m Larry, updating this description on Logan’s behalf.

## Summary

Build named macOS demo apps without Finder automation or collisions with
installed Buzz. `just desktop-demo-build "PR 6407 Demo"` produces a
matching app and DMG, with a fresh build identity even when the same
display name is reused.

- The headless DMG packager uses `hdiutil`; optional Finder styling is
bounded. The existing production release recipe is unchanged.
- Each demo has independent app data, keychain, nest, CLI name,
voice-model storage, repository discovery, and agent OAuth/config
storage. Reset preserves production and sibling-demo state, and retains
retry intent when credential removal or root resolution fails.
- Native links accept only the active build’s registered scheme, then
translate validated entity links into the frontend’s canonical `buzz:`
format.
- The recipe builds all six executable sidecars. Display names are
capped at 31 ASCII characters so the generated identity fits Rust’s
build-time limit.

**Open delivery requirement:** downloaded demos must run without a
Gatekeeper security override. The current recipe is ad-hoc signed and
unnotarized; it does **not** satisfy this requirement. Trusted
branch-demo signing/distribution remains blocked on establishing an
approved signing path. This PR is not being presented as complete
download-and-run delivery.

### Related issue

N/A — reported in the Buzz DMG-packaging workstream.

### Testing

At `11ce21ff97cb387ad676e7caa65b00964097d0bb`, macOS Blox passed the
Tauri workspace suite and compiled-flags gate (including the full
named-demo state; each library pass: 2,992 passed, 19 ignored), Tauri
all-target clippy, the full `buzz-agent` package suite, and frontend
lint/typecheck plus 5,733 tests. Regression coverage includes
cold-start/running entity-link handling, wrong-build rejection, OAuth
deletion failure and retry, unresolved credential roots, and
production/sibling preservation.

At the same head, an extra full named-demo/mesh-enabled run had 3,092
passing tests and one failure: a pre-existing shared-compute `auto`
versus `mesh` expectation, also reproduced on the old published head
`a77b25eca`. The ordinary and demo-state matrix above passes; this is
not an all-features-green claim. Live macOS Launch Services delivery
remains unverified.

GitHub CI completed with 30 successful checks and 9 skipped. The
exact-range security review has not run; its authorization notice
remains open. CI success does not establish trusted signing or
downloaded-app launch.

Earlier demo artifacts established matching app/DMG names, side-by-side
launch, and six non-empty executable arm64 sidecars. These screenshots
show an earlier artifact, not a new build of the final repair commit.
Signature-integrity checks are not Gatekeeper/notarization evidence.

<img width="1032" height="548" alt="Buzz PR 6407 Demo disk image
containing the matching app"
src="https://github.com/user-attachments/assets/bca0277e-db03-4308-b280-fcad55e6d601"
/>

<img width="1186" height="821" alt="Buzz PR 6407 Demo running alongside
other Buzz installations"
src="https://github.com/user-attachments/assets/b4bf4ae5-c341-4e15-8090-9d2ea7c623b6"
/>

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <loganj+sandbox-larry@squareup.com>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
2026-08-31 21:14:19 -04:00

95 lines
2.8 KiB
JavaScript

import { randomBytes } from "node:crypto";
import { writeFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
const PRODUCTION_IDENTIFIER = "xyz.block.buzz.app";
// The build ID suffix is 17 characters including its separator, and the Rust
// build contract caps the complete demo slug at 48 ASCII bytes.
const MAX_DEMO_SLUG_LENGTH = 48;
const DEMO_BUILD_ID_SUFFIX_LENGTH = 17;
const MAX_DEMO_NAME_LENGTH = MAX_DEMO_SLUG_LENGTH - DEMO_BUILD_ID_SUFFIX_LENGTH;
export const productionBuildIdentity = Object.freeze({
productName: "Buzz",
identifier: PRODUCTION_IDENTIFIER,
deepLinkScheme: "buzz",
keyringService: "buzz-desktop",
nestName: ".buzz",
cliName: "buzz",
});
export function demoBuildConfig(
rawName,
buildId = randomBytes(8).toString("hex"),
) {
if (typeof rawName !== "string") throw new Error("Demo name must be text");
const name = rawName.trim().replace(/\s+/g, " ");
if (!name) throw new Error("Demo name must not be empty");
if (name.length > MAX_DEMO_NAME_LENGTH) {
throw new Error(
`Demo name must be at most ${MAX_DEMO_NAME_LENGTH} characters`,
);
}
if (!/^[A-Za-z0-9][A-Za-z0-9 -]*$/.test(name)) {
throw new Error(
"Demo name may contain ASCII letters, numbers, spaces, and hyphens only",
);
}
if (!/^[a-f0-9]{16}$/.test(buildId)) {
throw new Error(
"Demo build ID must be sixteen lowercase hexadecimal characters",
);
}
const readableSlug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const slug = `${readableSlug}-${buildId}`;
const productName = `Buzz ${name}`;
return {
name,
slug,
productName,
dmgVolumeName: productName,
dmgFileStem: productName.replace(/ /g, "_"),
identifier: `${PRODUCTION_IDENTIFIER}.demo.${slug}`,
appDataIdentity: `${PRODUCTION_IDENTIFIER}.demo.${slug}`,
deepLinkScheme: `buzz-demo-${slug}`,
keyringService: `buzz-desktop-demo.${slug}`,
nestName: `.buzz-demo-${slug}`,
cliName: `buzz-demo-${slug}`,
tauriConfig: {
productName,
identifier: `${PRODUCTION_IDENTIFIER}.demo.${slug}`,
plugins: { "deep-link": { desktop: { schemes: [`buzz-demo-${slug}`] } } },
bundle: { targets: ["app"] },
},
};
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const [name, outputPath, buildId] = process.argv.slice(2);
if (!outputPath) {
console.error(
"Usage: demo-build-config.mjs <demo-name> <output-config-path>",
);
process.exit(2);
}
try {
const config = demoBuildConfig(name, buildId);
writeFileSync(
outputPath,
`${JSON.stringify(config.tauriConfig, null, 2)}\n`,
);
console.log(JSON.stringify(config));
} catch (error) {
console.error(`Invalid demo build: ${error.message}`);
process.exit(1);
}
}