/** * Locate the `craft` binary. * * Craft owns this contract. An explicit path or `CRAFT_BIN` wins, then the * pantry-installed `craft` on PATH. Keeping this as a direct delegation avoids * Stacks drifting back into checkout and node_modules probing. */ export declare function resolveCraftBinary(explicit?: string | undefined): string; export declare function resolveCraftExecutable(explicit?: string | undefined, findOnPath?: CraftBinaryLocator): string; /** * Whether this application supplies its own desktop launcher. * * The framework launcher opens a Craft window on a URL from `desktop.json`, * which is right for a hosted Stacks app and wrong for a local-first one — a * disk cleaner, a log viewer, anything whose data is the machine it runs on. * Those need to start something locally and open a window on that, and until * an app could replace the launcher its only option was to reimplement * `build:desktop` and `build:dmg` outside the framework. */ export declare function hasUserlandDesktopLauncher(projectRoot?: string): boolean; /** * The `bun build` arguments that compile a desktop launcher. * * A compiled executable autoloads `bunfig.toml` and `.env` from its working * directory by default, which is wrong for one that ships to users: its cwd * is wherever it happens to be started. Launched from an app's project * directory - by launchd with a `WorkingDirectory`, `open` from a terminal, * or a developer testing the build - it applied the app's bunfig preloads to * itself and died with `preload not found "@stacksjs/env/plugin.js"` before * running a line. From any other directory it silently loaded that * directory's `.env` into the launcher and everything it spawns. */ export declare function desktopLauncherCompileArgs(options: DesktopLauncherCompileOptions): string[]; export declare function resolveDesktopLauncher(projectRoot?: string): string; export declare function xmlEscape(value: string): string; /** * Serialise a JSON value as a plist fragment. * * Applications declare extra `Info.plist` entries as JSON rather than XML so a * typo cannot produce a bundle macOS silently refuses to launch. The keys that * matter most are the `NS*UsageDescription` strings: they are the sentences a * person reads in a permission prompt, and without them macOS shows a generic * prompt or refuses the request outright. */ export declare function plistValue(value: unknown, indent?: string): string; /** * Render an application's declared `Info.plist` entries, dropping any key the * bundle must own. Returns the ignored keys so the caller can say so. */ export declare function renderUserlandPlistEntries(declared: Record): { xml: string, ignored: string[] }; /** * Render the `Info.plist` for the packaged `.app`. * * Every value an application chooses is escaped on the way in. It used to be * interpolated raw, which made an app name as ordinary as `Repro & Co` emit * `Repro & Co` - not well-formed XML, so macOS could not read * the bundle's `Info.plist` at all and the app would not launch. `build:dmg` * reported success either way, because nothing read back the file it had just * written; the corruption first showed up as an app that silently refused to * open. The entries an app declares in `app/Desktop/Info.plist.json` were * already escaped by `renderUserlandPlistEntries`. The keys every build writes, * which are the ones carrying a name and a version, were not. * * It lives here, rather than inline in the packaging script, so the output can * be asserted directly instead of by building a DMG and mounting it. */ export declare function renderInfoPlist(options: InfoPlistOptions): { xml: string, ignored: string[] }; export declare function resolveDevWindowUrl(port: number, options?: OpenDevWindowOptions): string; export declare function craftDevCommand(port: number, options?: OpenDevWindowOptions): string[]; export declare function createUpdateManifestUrl(baseUrl: string, channel?: string): string; export declare function openDevWindow(port: number, options?: OpenDevWindowOptions, launcher?: CraftLauncher): Promise; /** Whether `path` is a Mach-O executable, by its first four bytes. */ export declare function isMachO(path: string): boolean; /** * Where a file `build:desktop` emitted belongs inside a `.app`. * * `codesign` treats everything in `Contents/MacOS` as code. The manifest, * provenance and checksums used to sit there beside the launcher, so signing * the launcher or the bundle failed with "code object is not signed at all. * In subcomponent: .../Contents/MacOS/desktop.json" - no bundle this produced * could ever be signed, which went unnoticed because nothing had signed one. * Executables go in MacOS; everything else in Resources. */ export declare function bundleDirectoryFor(path: string): 'MacOS' | 'Resources'; /** * The version the packaged app declares. * * This used to be `process.env.DESKTOP_APP_VERSION || '0.0.0'`, and nothing in * the framework set that variable, so every app that did not export it itself * shipped `CFBundleShortVersionString` of `0.0.0` - the version Finder shows, * and the one a Sparkle-style updater compares to decide whether an update is * an update. Every build looked like the same build. The DMG's filename and * volume name carried it too, so `MyApp-0.0.0.dmg` overwrote the last release. * * The application states its version in `package.json`, which is where the * framework reads every other thing about it, so reading it here is the whole * fix. An explicit `DESKTOP_APP_VERSION` still wins: a pipeline stamping a * build number or a release candidate is naming something `package.json` does * not know. * * It is used verbatim rather than normalised. Apple documents * `CFBundleShortVersionString` as three integers, and a semver prerelease such * as `1.4.0-rc.2` is not that, but silently publishing an app as `1.4.0` when * its project says `1.4.0-rc.2` would be the worse surprise: the two are * different builds and an updater has to be able to tell them apart. */ export declare function desktopAppVersion(options?: DesktopAppVersionOptions): string; /** * The volume name for the app's DMG, which must never be the app's own name. * * `hdiutil create -volname -srcfolder ` stages the bundle at * `/Volumes//.app`. On a Mac where that app is installed in * `/Applications` and TCC-managed - it carries `com.apple.macl` and * `com.apple.provenance` - macOS refuses to let a process without App * Management rights create a bundle that would shadow it, and imaging fails * with "Operation not permitted" (stacksjs/stacks#2884). The bundle is complete * and correct by then; only the final step dies. * * It is the path rather than the content: the same bundle images fine under any * other volume name, and a plain `cp -R` into that path is refused the same * way, so it is not an hdiutil quirk. And it hits exactly the people most * likely to be building, since it takes having installed the app you are * working on. * * `${appName} ${version}` sidesteps it and is the usual convention anyway - * most shipped DMGs mount as "AppName 1.2.3". hdiutil does not truncate long * names (36 characters round-trip intact, probed against the real tool), so * appending the version cannot fold the name back onto the app's. * * An explicit override is returned as given, collision included: the caller is * warned at build time rather than quietly overruled, because rewriting what * somebody typed is how a build becomes unpredictable. */ export declare function dmgVolumeName(options: DmgVolumeOptions): string; /** * Everything `build:dmg` needs to name and sign a bundle, from flags first and * the environment second. * * `build:dmg` took no flags at all: every one of these values could only be * given as a `DESKTOP_*` environment variable, so packaging a signed release * from a terminal meant exporting four of them, and anything scripted around * that became a wrapper whose only job was to set variables and call `buddy`. * Which is how an application ends up owning a build script (see * stacksjs/stacks#2884, #2885, #2889, #2890 for what that script was working * around). * * The sibling command for the Mac App Store, `desktop:apple`, already resolved * its inputs this way - `options.appVersion || env('APPLE_APP_VERSION') || * metadata.version` - with a flag for each. This brings direct distribution to * the same shape, so the two commands no longer disagree about how a Mac build * is configured. * * The environment remains the right answer in CI, where these arrive as * secrets; a flag is the right answer at a terminal. Neither is privileged over * the application's own `package.json` by accident: a flag and a variable are * both something a person stated for this build, so they win, and the manifest * and `package.json` answer when nobody did. */ export declare function resolveDmgPackaging(inputs?: DmgPackagingInputs): DmgPackaging; /** * The `codesign` invocation for one target in a distributable bundle: * hardened runtime (notarization rejects anything without it) and a secure * timestamp (notarization also rejects "The signature does not include a * secure timestamp"). Ad-hoc signing (`-`) cannot be timestamped. */ export declare function codesignArgs(options: CodesignOptions): string[]; export declare function looksLikeBunExecutable(path: string, read?: (path: string) => { size: number, tail: string } | null): boolean; /** * What a bundle is paying to ship more than one Bun-compiled executable. * * Every one of them embeds a complete copy of the Bun runtime. An app that * splits its launcher, its server and a worker into three binaries ships three * copies — about 180 MB — and nothing says so: the bundle is simply large, and * a large desktop app looks normal. * * Returns null when there is at most one, which is the case worth saying * nothing about. */ export declare function describeRuntimeDuplication(runtimes: BundledRuntime[]): string | null; /** Where an application puts a launcher of its own. */ export declare const USERLAND_LAUNCHER: 'app/Desktop/launcher.ts'; /** * Keys the bundle itself must control. * * Letting an app rewrite its own identifier or executable from `Info.plist.json` * produces a bundle that does not match what was signed — which fails at * launch, long after the build said it succeeded. */ export declare const RESERVED_PLIST_KEYS: ReadonlySet; /** * The trailer `bun build --compile` writes into every standalone executable. * * Present in a Bun single-file binary and absent from a native one, which is * what makes it usable to tell the two apart inside a finished bundle. */ export declare const BUN_EXECUTABLE_MARKER: '---- Bun!'; export declare interface Desktop { app: unknown core: unknown dpi: unknown event: unknown image: unknown menu: unknown mocks: unknown path: unknown tray: unknown webview: unknown webviewWindow: unknown window: unknown } // let _desktop: Desktop | undefined // export async function getDesktop(): Promise { // if (_desktop) return _desktop // const tauri = await import('@tauri-apps/api') // _desktop = { // app: tauri.app, // core: tauri.core, // dpi: tauri.dpi, // event: tauri.event, // image: tauri.image, // menu: tauri.menu, // mocks: tauri.mocks, // path: tauri.path, // tray: tauri.tray, // webview: tauri.webview, // webviewWindow: tauri.webviewWindow, // window: tauri.window, // } // return _desktop // } export declare interface OpenDevWindowOptions { url?: string title?: string width?: number height?: number darkMode?: boolean hotReload?: boolean nativeSidebar?: boolean sidebarWidth?: number sidebarConfig?: unknown devTools?: boolean craftBin?: string systemTray?: boolean hideDockIcon?: boolean menubarOnly?: boolean } /** * The launcher entrypoint `buddy build:desktop` compiles into the native * bundle. * * `app/Desktop/launcher.ts` wins when present, the same way anything else * under `app/` overrides its framework default. Otherwise: inside this monorepo * the TypeScript source is right there, while a consumer app only has the * published package, which ships `dist/launcher.js` — so resolve whichever * exists rather than assuming the monorepo layout, which is what previously * made desktop builds impossible outside this repo. */ export declare interface DesktopLauncherCompileOptions { entry: string outfile: string minify?: boolean } export declare interface InfoPlistOptions { appName: string bundleId: string version: string executable: string iconFile?: string ownsLauncher: boolean declared?: Record } /** One Bun-compiled executable found in a bundle. */ export declare interface BundledRuntime { name: string bytes: number } export declare interface DesktopAppVersionOptions { override?: string declared?: unknown } export declare interface DmgVolumeOptions { appName: string version?: string override?: string } /** * The flags `build:dmg` accepts, as `parseOptions()` hands them over. * * Every value is `unknown` rather than `string` because these have not been * validated yet: a flag declared but passed without an argument arrives as * boolean `true`, which is the case `supplied` exists to reject. * * The index signature is what `parseOptions()` actually returns - every flag on * the command line, including `--verbose` and the rest of `CliOptions`, not * only the six read here. Narrowing it to just these six would describe a * value this is never passed. */ export declare interface DmgPackagingFlags { appName?: unknown appVersion?: unknown bundleId?: unknown signingIdentity?: unknown notaryProfile?: unknown volumeName?: unknown [flag: string]: unknown } export declare interface DmgPackagingInputs { flags?: DmgPackagingFlags env?: Record manifestTitle?: string declaredVersion?: unknown } export declare interface DmgPackaging { appName: string bundleId: string version: string volumeName: string signingIdentity?: string notaryProfile?: string } export declare interface CodesignOptions { identity: string target: string entitlements?: string } export type CraftLauncher = (command: string[]) => void | Promise; export type CraftBinaryLocator = (binary: string) => string | null; export * from './invites'; export * from './updater'; export * from './support';