@stacksjs/desktop-build 0.75.390 → 0.75.398

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ import type { AgentHandler } from './agent';
2
+ export declare function desktopAgentPaths(appName: string, home?: string): DesktopAgentPaths;
3
+ /** Take the lock, or return false when another live copy of the app holds it. A lock left by a crash is taken over. */
4
+ export declare function acquireInstanceLock(lockFile: string, pid?: number): boolean;
5
+ export declare function releaseInstanceLock(lockFile: string, pid?: number): void;
6
+ /**
7
+ * Launched from Finder there is no terminal, and what the app prints would go
8
+ * nowhere: send it to a log file instead. From a terminal the developer keeps
9
+ * stdout and stderr. Returns whether it redirected.
10
+ */
11
+ export declare function logToFileWhenDetached(logFile: string, isTTY?: boolean): boolean;
12
+ /** The manifest next to a bundled executable (`Contents/Resources`), or the one `STACKS_DESKTOP_AGENT_MANIFEST` names. */
13
+ export declare function findAgentManifest(execPath?: string, env?: NodeJS.ProcessEnv): string | null;
14
+ export declare function readAgentManifest(file: string): DesktopAgentManifest;
15
+ /** The host's full command line: where the agent is and where the screens are, then the window. */
16
+ export declare function agentHostCommand(manifest: Pick<DesktopAgentManifest, 'host' | 'nativeBundle' | 'hostArguments'>, agentUrl: string): string[];
17
+ /**
18
+ * The app's routes and actions, answered in process by the Stacks router.
19
+ *
20
+ * Booted the way `buddy dev:api` boots: the user's config, then the models
21
+ * and framework primitives on `globalThis` (actions use them unimported),
22
+ * then the routes. A bundled app may not have the auto-import barrel the
23
+ * project generated, which is only a warning here: its agent routes import
24
+ * what they use.
25
+ */
26
+ export declare function stacksAgentHandler(options: StacksAgentHandlerOptions): Promise<AgentHandler>;
27
+ /**
28
+ * The launcher's entrypoint, written by the build next to the screens and
29
+ * compiled (or, in development, run) from there. It names the agent's own
30
+ * route module by absolute path so the bundler can follow it: whatever that
31
+ * module imports is inside the built app.
32
+ */
33
+ export declare function renderAgentEntry(options?: { agentEntry?: string, launcherModule?: string, routes?: Array<{ file: string, prefix?: string, middleware?: string[] }>, actions?: Record<string, string> }): string;
34
+ /**
35
+ * Lock, log, agent, host; resolves with the exit code once the window's
36
+ * process ends (or 0 at once when another copy is already running, which
37
+ * brings its window forward the moment the user activates it).
38
+ */
39
+ export declare function runDesktopAgentApp(options: RunDesktopAgentAppOptions): Promise<number>;
40
+ export declare const AGENT_MANIFEST: 'desktop-agent.json';
41
+ export declare const AGENT_MANIFEST_ENV: 'STACKS_DESKTOP_AGENT_MANIFEST';
42
+ /**
43
+ * The executable of a desktop app that runs its own API (`macos.agent`). It
44
+ * holds the single-instance lock, starts the agent, and spawns Craft's AppKit
45
+ * host pointed at it; when the window's process exits, so does the app.
46
+ *
47
+ * The same code runs in two places. `buddy dev:desktop` runs it from source
48
+ * in the project, so the app's routes load exactly as `buddy dev:api` loads
49
+ * them. `buddy build:desktop` compiles it into `<App>.app/Contents/MacOS`,
50
+ * with the host beside it as `craft-runtime`.
51
+ */
52
+ /** `Contents/Resources/desktop-agent.json`, written by the build; in development, a file the dev command writes. */
53
+ export declare interface DesktopAgentManifest {
54
+ appName: string
55
+ bundleId: string
56
+ version: string
57
+ host: string
58
+ nativeBundle: string
59
+ hostArguments: string[]
60
+ port?: number
61
+ staticDir?: string
62
+ projectRoot?: string
63
+ appRoutes: boolean
64
+ }
65
+ export declare interface DesktopAgentPaths {
66
+ dataDir: string
67
+ portFile: string
68
+ lockFile: string
69
+ logFile: string
70
+ }
71
+ /** A route file baked into the launcher, mounted the way `app/Routes.ts` mounts it. */
72
+ export declare interface AgentRouteFile {
73
+ load: () => Promise<unknown>
74
+ prefix?: string
75
+ middleware?: string[]
76
+ }
77
+ export declare interface StacksAgentHandlerOptions {
78
+ entry?: () => Promise<unknown>
79
+ appRoutes: boolean
80
+ routes?: AgentRouteFile[]
81
+ actions?: Record<string, () => Promise<{ default?: unknown }>>
82
+ }
83
+ export declare interface RunDesktopAgentAppOptions {
84
+ manifest: DesktopAgentManifest
85
+ handler: () => Promise<AgentHandler>
86
+ paths?: DesktopAgentPaths
87
+ logWhenDetached?: boolean
88
+ spawn?: (command: string[]) => { exited: Promise<number>, kill: () => void }
89
+ }
@@ -0,0 +1,24 @@
1
+ import{appendFileSync,existsSync,mkdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,isAbsolute,join,resolve}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{format}from"node:util";import{startDesktopAgent}from"./agent";export const AGENT_MANIFEST="desktop-agent.json",AGENT_MANIFEST_ENV="STACKS_DESKTOP_AGENT_MANIFEST";export function desktopAgentPaths(appName,home=homedir()){const dataDir=join(home,"Library","Application Support",appName);return{dataDir,portFile:join(dataDir,"agent.port"),lockFile:join(dataDir,"agent.pid"),logFile:join(home,"Library","Logs",`${appName}.log`)}}export function acquireInstanceLock(lockFile,pid=process.pid){mkdirSync(dirname(lockFile),{recursive:!0});if(existsSync(lockFile)){const holder=Number(readFileSync(lockFile,"utf8").trim());if(holder&&holder!==pid)try{process.kill(holder,0);return!1}catch(error){if(error.code==="EPERM")return!1}}writeFileSync(lockFile,String(pid));return!0}export function releaseInstanceLock(lockFile,pid=process.pid){try{if(readFileSync(lockFile,"utf8").trim()===String(pid))rmSync(lockFile)}catch{}}export function logToFileWhenDetached(logFile,isTTY=Boolean(process.stdout.isTTY)){if(isTTY)return!1;mkdirSync(dirname(logFile),{recursive:!0});for(const level of["log","info","warn","error","debug"])console[level]=(...args)=>{try{appendFileSync(logFile,`${new Date().toISOString()} ${level==="log"?"":`[${level}] `}${format(...args)}
2
+ `)}catch{}};return!0}export function findAgentManifest(execPath=process.execPath,env=process.env){if(env[AGENT_MANIFEST_ENV])return resolve(env[AGENT_MANIFEST_ENV]);const bundled=join(dirname(execPath),"..","Resources",AGENT_MANIFEST);return existsSync(bundled)?bundled:null}export function readAgentManifest(file){const manifest=JSON.parse(readFileSync(file,"utf8")),base=dirname(file),at=(path)=>isAbsolute(path)?path:resolve(base,path);return{...manifest,host:at(manifest.host),nativeBundle:at(manifest.nativeBundle),...manifest.staticDir?{staticDir:at(manifest.staticDir)}:{}}}export function agentHostCommand(manifest,agentUrl){return[manifest.host,"--api-url",agentUrl,"--native-bundle",manifest.nativeBundle,...manifest.hostArguments]}export async function stacksAgentHandler(options){try{const{overridesReady}=await import("@stacksjs/config");await overridesReady}catch(error){console.warn(`[agent] config not loaded: ${error instanceof Error?error.message:String(error)}`)}if(options.appRoutes)try{const{injectGlobalAutoImports}=await import("@stacksjs/server");await injectGlobalAutoImports()}catch(error){console.warn(`[agent] models and framework globals not injected: ${error instanceof Error?error.message:String(error)}`)}const{provideActions,route,serverResponse}=await import("@stacksjs/router");if(options.actions)provideActions(options.actions);const module=options.entry?await options.entry():void 0;if(typeof module?.default==="function")return module.default;if(options.appRoutes)return(request)=>serverResponse(request);for(const file of options.routes??[])if(file.prefix||file.middleware?.length)await route.group({prefix:file.prefix,middleware:file.middleware?.length?file.middleware:void 0},async()=>{await file.load()});else await file.load();return(request)=>route.handleRequest(request)}export function renderAgentEntry(options={}){const launcher=options.launcherModule??fileURLToPath(import.meta.url),entry=options.agentEntry?`() => import(${JSON.stringify(options.agentEntry)})`:"undefined",routes=options.routes?.length?`[
3
+ ${options.routes.map((file)=>` { load: () => import(${JSON.stringify(file.file)})${file.prefix?`, prefix: ${JSON.stringify(file.prefix)}`:""}${file.middleware?.length?`, middleware: ${JSON.stringify(file.middleware)}`:""} },`).join(`
4
+ `)}
5
+ ]`:"[]",actionEntries=Object.entries(options.actions??{}),actions=actionEntries.length?`{
6
+ ${actionEntries.map(([name,file])=>` ${JSON.stringify(name)}: () => import(${JSON.stringify(file)}),`).join(`
7
+ `)}
8
+ }`:"{}";return`// Written by buddy build:desktop and buddy dev:desktop (macos.agent). Do not edit.
9
+ import process from 'node:process'
10
+ import { findAgentManifest, readAgentManifest, runDesktopAgentApp, stacksAgentHandler } from ${JSON.stringify(launcher)}
11
+
12
+ const file = findAgentManifest()
13
+ if (!file) {
14
+ console.error('No desktop-agent.json beside this executable, and STACKS_DESKTOP_AGENT_MANIFEST is not set.')
15
+ process.exit(1)
16
+ }
17
+ const manifest = readAgentManifest(file)
18
+ process.exit(await runDesktopAgentApp({
19
+ manifest,
20
+ handler: () => stacksAgentHandler({ entry: ${entry}, appRoutes: manifest.appRoutes, routes: ${routes}, actions: ${actions} }),
21
+ }))
22
+ `}export async function runDesktopAgentApp(options){const{manifest}=options,paths=options.paths??desktopAgentPaths(manifest.appName),tag=`[${manifest.appName}]`;if(options.logWhenDetached!==!1)logToFileWhenDetached(paths.logFile);if(!acquireInstanceLock(paths.lockFile)){console.log(`${tag} already running`);return 0}if(!existsSync(manifest.host)){console.error(`${tag} the AppKit host is missing: ${manifest.host}`);releaseInstanceLock(paths.lockFile);return 1}if(!existsSync(manifest.nativeBundle)){console.error(`${tag} the native screens are missing: ${manifest.nativeBundle}`);releaseInstanceLock(paths.lockFile);return 1}let agent;try{if(manifest.projectRoot&&manifest.appRoutes)process.chdir(manifest.projectRoot);agent=await startDesktopAgent({handler:await options.handler(),name:manifest.appName,version:manifest.version,port:manifest.port,portFile:paths.portFile,staticDir:manifest.staticDir})}catch(error){console.error(`${tag} could not start: ${error instanceof Error?error.stack??error.message:String(error)}`);releaseInstanceLock(paths.lockFile);return 1}console.log(`${tag} ${manifest.version} serving on ${agent.url}`);const command=agentHostCommand(manifest,agent.url),host=options.spawn?options.spawn(command):(()=>{const child=Bun.spawn(command,{stdio:["ignore","inherit","pipe"]});new Response(child.stderr).text().then((stderr)=>{const lines=stderr.split(`
23
+ `).filter((line)=>line.trim());if(lines.length)console.error(`[craft] ${lines.join(`
24
+ `)}`)});return{exited:child.exited,kill:()=>child.kill()}})();let stopped=!1;const stop=()=>{if(stopped)return;stopped=!0;host.kill();agent.stop();releaseInstanceLock(paths.lockFile)},onSignal=()=>{stop();process.exit(0)};for(const signal of["SIGTERM","SIGINT","SIGHUP"])process.on(signal,onSignal);const code=await host.exited;console.log(`${tag} window closed (${code})`);for(const signal of["SIGTERM","SIGINT","SIGHUP"])process.off(signal,onSignal);stop();return 0}
@@ -0,0 +1,42 @@
1
+ import type { Server } from 'bun';
2
+ /** A fresh session token: 192 random bits. */
3
+ export declare function newAgentToken(): string;
4
+ /** Whether a Host header names this agent: 127.0.0.1 or localhost, on its port. Nothing else, not even another loopback alias. */
5
+ export declare function isAgentHost(host: string | null, port: number): boolean;
6
+ export declare function sessionCookie(name: string, token: string): string;
7
+ /** Whether the request carries this session's cookie. */
8
+ export declare function hasSessionCookie(request: Request, name: string, token: string): boolean;
9
+ /** The port remembered from the last launch, or 0 (any free port) when there is none worth trying. */
10
+ export declare function rememberedPort(portFile: string | undefined): number;
11
+ export declare function startDesktopAgent(options: DesktopAgentOptions): Promise<DesktopAgent>;
12
+ export declare interface DesktopAgentOptions {
13
+ handler: AgentHandler
14
+ name?: string
15
+ version?: string
16
+ port?: number
17
+ portFile?: string
18
+ cookieName?: string
19
+ staticDir?: string
20
+ }
21
+ export declare interface DesktopAgent {
22
+ port: number
23
+ url: string
24
+ server: Server<undefined>
25
+ readonly token: string
26
+ rotateToken: () => string
27
+ stop: () => void
28
+ }
29
+ /**
30
+ * The process behind a desktop app that runs its own API (`macos.agent`): a
31
+ * loopback HTTP server that serves the app's routes in process, with Craft's
32
+ * AppKit host as its window.
33
+ *
34
+ * Only the host the agent started may call it. The host fetches
35
+ * `/native/session` first and gets the session cookie (HttpOnly, so the
36
+ * screens' JavaScript never holds it; SameSite=Strict, so another site's form
37
+ * or fetch never sends it). Every other request must carry that cookie and
38
+ * name this server in its Host header, so a DNS-rebinding page - whose Host
39
+ * is its own name, not 127.0.0.1 - cannot pose as the app. The token is new
40
+ * on every launch, so a cookie from an earlier run is worthless.
41
+ */
42
+ export type AgentHandler = (request: Request) => Response | Promise<Response>;
package/dist/agent.js ADDED
@@ -0,0 +1 @@
1
+ import{randomBytes}from"node:crypto";import{existsSync,mkdirSync,readFileSync,statSync,writeFileSync}from"node:fs";import{dirname,extname,join,normalize,sep}from"node:path";const MIME={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".ico":"image/x-icon",".woff2":"font/woff2",".woff":"font/woff",".txt":"text/plain; charset=utf-8"};export function newAgentToken(){return randomBytes(24).toString("hex")}export function isAgentHost(host,port){return host===`127.0.0.1:${port}`||host===`localhost:${port}`}export function sessionCookie(name,token){return`${name}=${token}; Path=/; HttpOnly; SameSite=Strict`}export function hasSessionCookie(request,name,token){const cookies=request.headers.get("cookie");if(!cookies)return!1;return cookies.split(/;\s*/).includes(`${name}=${token}`)}export function rememberedPort(portFile){if(!portFile)return 0;try{const port=Number(readFileSync(portFile,"utf8").trim());return Number.isInteger(port)&&port>1024&&port<65536?port:0}catch{return 0}}function json(data,status=200){return new Response(JSON.stringify(data),{status,headers:{"content-type":"application/json; charset=utf-8","cache-control":"no-store"}})}export async function startDesktopAgent(options){const cookieName=options.cookieName??"stacks_desktop";let token=newAgentToken(),port=0;function serveStatic(pathname){if(!options.staticDir)return null;const root=normalize(options.staticDir);let clean;try{clean=decodeURIComponent(pathname).replace(/\/+$/,"")||"/index"}catch{return null}const candidates=clean==="/index"?["index.html"]:[`${clean}.html`,`${clean}/index.html`,clean];for(const candidate of candidates){const file=normalize(join(root,candidate));if(file!==root&&!file.startsWith(`${root}${sep}`))continue;if(!existsSync(file)||!statSync(file).isFile())continue;const headers={"content-type":MIME[extname(file)]??"application/octet-stream"};if(extname(file)===".html"){headers["cache-control"]="no-store";headers["set-cookie"]=sessionCookie(cookieName,token)}return new Response(Bun.file(file),{headers})}return null}async function fetch(request){const url=new URL(request.url),host=request.headers.get("host");if(url.pathname==="/native/session"){if(request.method!=="GET")return json({error:"Method not allowed"},405);if(!isAgentHost(host,port))return json({error:"Forbidden"},403);const response=json({name:options.name??null,version:options.version??null,renderer:"appkit"});response.headers.set("set-cookie",sessionCookie(cookieName,token));return response}if(!isAgentHost(host,port))return json({error:"Forbidden"},403);if(request.method==="GET"&&!url.pathname.startsWith("/api/")){const file=serveStatic(url.pathname);if(file)return file}if(!hasSessionCookie(request,cookieName,token))return json({error:"Forbidden"},403);try{return await options.handler(request)}catch(error){const message=error instanceof Error?error.message:String(error);console.error(`[agent] ${request.method} ${url.pathname}: ${message}`);return json({error:message},500)}}const serve=(wanted)=>Bun.serve({hostname:"127.0.0.1",port:wanted,idleTimeout:0,fetch}),wanted=options.port??rememberedPort(options.portFile);let server;try{server=serve(wanted)}catch(error){if(wanted===0)throw error;server=serve(0)}port=server.port??0;if(options.portFile)try{mkdirSync(dirname(options.portFile),{recursive:!0});writeFileSync(options.portFile,String(port))}catch{}return{port,url:`http://127.0.0.1:${port}`,server,get token(){return token},rotateToken(){token=newAgentToken();return token},stop(){server.stop(!0)}}}
package/dist/index.d.ts CHANGED
@@ -325,6 +325,10 @@ export declare interface CodesignOptions {
325
325
  }
326
326
  export type CraftLauncher = (command: string[]) => void | Promise<void>;
327
327
  export type CraftBinaryLocator = (binary: string) => string | null;
328
+ export * from './agent';
329
+ export * from './agent-launcher';
328
330
  export * from './invites';
329
331
  export * from './updater';
330
332
  export * from './support';
333
+ export * from './macos-app';
334
+ export * from './macos-bundle';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import{closeSync,existsSync,openSync,readSync,statSync}from"node:fs";import{dirname,join}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{craftBinaryNotFoundMessage,resolveCraftBinary as resolveCraftNativeBinary}from"craft-native";export*from"./invites";export*from"./updater";export*from"./support";export function resolveCraftBinary(explicit=process.env.CRAFT_BIN){return resolveCraftNativeBinary(explicit)}export function resolveCraftExecutable(explicit=process.env.CRAFT_BIN,findOnPath=(binary)=>Bun.which(binary)){const resolved=resolveCraftBinary(explicit);if(resolved!=="craft")return resolved;const executable=findOnPath(resolved);if(!executable)throw Error(craftBinaryNotFoundMessage(resolved));return executable}export const USERLAND_LAUNCHER="app/Desktop/launcher.ts";export function hasUserlandDesktopLauncher(projectRoot=process.cwd()){return existsSync(join(projectRoot,USERLAND_LAUNCHER))}export function desktopLauncherCompileArgs(options){return["build","--compile",...options.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",options.entry,"--outfile",options.outfile]}export function resolveDesktopLauncher(projectRoot=process.cwd()){const userland=join(projectRoot,USERLAND_LAUNCHER);if(existsSync(userland))return userland;const here=dirname(fileURLToPath(import.meta.url)),launcher=[join(here,"launcher.ts"),join(here,"launcher.js")].find((candidate)=>existsSync(candidate));if(!launcher)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return launcher}export const RESERVED_PLIST_KEYS=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);export function xmlEscape(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}export function plistValue(value,indent=""){if(typeof value==="string")return`<string>${xmlEscape(value)}</string>`;if(typeof value==="boolean")return value?"<true/>":"<false/>";if(typeof value==="number"){if(!Number.isFinite(value))throw Error(`Unsupported Info.plist number: ${value}`);return Number.isInteger(value)?`<integer>${value}</integer>`:`<real>${value}</real>`}if(Array.isArray(value)){if(value.length===0)return"<array/>";return`<array>
1
+ import{closeSync,existsSync,openSync,readSync,statSync}from"node:fs";import{dirname,join}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{craftBinaryNotFoundMessage,resolveCraftBinary as resolveCraftNativeBinary}from"craft-native";export*from"./agent";export*from"./agent-launcher";export*from"./invites";export*from"./updater";export*from"./support";export*from"./macos-app";export*from"./macos-bundle";export function resolveCraftBinary(explicit=process.env.CRAFT_BIN){return resolveCraftNativeBinary(explicit)}export function resolveCraftExecutable(explicit=process.env.CRAFT_BIN,findOnPath=(binary)=>Bun.which(binary)){const resolved=resolveCraftBinary(explicit);if(resolved!=="craft")return resolved;const executable=findOnPath(resolved);if(!executable)throw Error(craftBinaryNotFoundMessage(resolved));return executable}export const USERLAND_LAUNCHER="app/Desktop/launcher.ts";export function hasUserlandDesktopLauncher(projectRoot=process.cwd()){return existsSync(join(projectRoot,USERLAND_LAUNCHER))}export function desktopLauncherCompileArgs(options){return["build","--compile",...options.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",options.entry,"--outfile",options.outfile]}export function resolveDesktopLauncher(projectRoot=process.cwd()){const userland=join(projectRoot,USERLAND_LAUNCHER);if(existsSync(userland))return userland;const here=dirname(fileURLToPath(import.meta.url)),launcher=[join(here,"launcher.ts"),join(here,"launcher.js")].find((candidate)=>existsSync(candidate));if(!launcher)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return launcher}export const RESERVED_PLIST_KEYS=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);export function xmlEscape(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}export function plistValue(value,indent=""){if(typeof value==="string")return`<string>${xmlEscape(value)}</string>`;if(typeof value==="boolean")return value?"<true/>":"<false/>";if(typeof value==="number"){if(!Number.isFinite(value))throw Error(`Unsupported Info.plist number: ${value}`);return Number.isInteger(value)?`<integer>${value}</integer>`:`<real>${value}</real>`}if(Array.isArray(value)){if(value.length===0)return"<array/>";return`<array>
2
2
  ${value.map((item)=>`${indent} ${plistValue(item,`${indent} `)}`).join(`
3
3
  `)}
4
4
  ${indent}</array>`}if(value&&typeof value==="object"){const entries=Object.entries(value);if(entries.length===0)return"<dict/>";return`<dict>
@@ -0,0 +1,94 @@
1
+ import type { DesktopConfig, MacosAgentConfig, MacosDesktopConfig } from '@stacksjs/types';
2
+ /** `config/desktop.ts` of a project, or null when it has none or no `macos` section. */
3
+ export declare function loadDesktopConfig(root: string): Promise<DesktopConfig | null>;
4
+ /** Whether the project declares a native macOS app. */
5
+ export declare function hasMacosDesktop(root: string): Promise<boolean>;
6
+ export declare function loadMacosDesktop(root: string, packageVersion?: string): Promise<ResolvedMacosDesktop | null>;
7
+ export declare function resolveMacosDesktop(config: MacosDesktopConfig, packageVersion?: string): ResolvedMacosDesktop;
8
+ /** Why a config cannot build, as a list; empty when it can. */
9
+ export declare function validateMacosDesktop(config: ResolvedMacosDesktop): string[];
10
+ /** Screen name to its `.stx` source, relative to the project root. */
11
+ export declare function macosScreens(config: Pick<MacosDesktopConfig, 'nativeScreens' | 'nativeScreensDir'>): Record<string, string>;
12
+ /** What the host reads from `Contents/Resources/craft-app.json` when Finder launches it with no arguments. */
13
+ export declare function renderHostConfig(config: ResolvedMacosDesktop): string;
14
+ /** The host's command line, for a bare executable run during development. */
15
+ export declare function hostArguments(config: ResolvedMacosDesktop, nativeBundle: string): string[];
16
+ /**
17
+ * The host's command line under an agent, less `--api-url` and
18
+ * `--native-bundle`, which the launcher adds once it knows its port and where
19
+ * the screens are. No `--no-session`: the host asks the agent for its session.
20
+ */
21
+ export declare function agentHostArguments(config: ResolvedMacosDesktop): string[];
22
+ /** The prompt sentences `permissions: { camera: true }` and its siblings use when the app gives none. */
23
+ export declare function defaultUsageDescriptions(appName: string): Record<'camera' | 'microphone' | 'location', string>;
24
+ /** The `NS*UsageDescription` keys `macos.permissions` asks for. An explicit `usageDescriptions` entry wins. */
25
+ export declare function permissionUsageDescriptions(config: Pick<ResolvedMacosDesktop, 'appName' | 'permissions'>): Record<string, string>;
26
+ export declare function renderMacosInfoPlist(config: ResolvedMacosDesktop, options?: MacosInfoPlistOptions): { xml: string, ignored: string[] };
27
+ /**
28
+ * The signed app's entitlements. The sandbox and outbound network are what
29
+ * the Mac App Store requires of an app that only talks to a server; the
30
+ * application identifier and keychain access group are what let the app keep
31
+ * its sign-in in the data-protection Keychain, and need a team. A development
32
+ * build carries neither (it is signed ad hoc, which cannot hold them).
33
+ *
34
+ * `permissions` adds a device entitlement for each device the app asks for,
35
+ * sandboxed or not: the hardened runtime refuses the camera, microphone and
36
+ * location without one. Folders, printing and the network are sandbox
37
+ * entitlements, so they only appear in a sandboxed app.
38
+ *
39
+ * Under `agent` the executable is Bun, whose JIT the hardened runtime blocks
40
+ * unless it is allowed, and the agent listens on 127.0.0.1, which a
41
+ * sandboxed app may only do with `network.server`.
42
+ */
43
+ export declare function renderMacosEntitlements(config: ResolvedMacosDesktop, distribution: MacosDistribution): string;
44
+ /**
45
+ * The entitlements of the AppKit host when an agent launches it from inside
46
+ * the bundle. A sandboxed parent's child may hold only the sandbox and
47
+ * `inherit` (it then runs in the parent's sandbox, with the parent's
48
+ * permissions); unsandboxed, it needs its own device entitlements, because
49
+ * the host is the process whose web views use the camera and microphone.
50
+ */
51
+ export declare function renderMacosHostEntitlements(config: ResolvedMacosDesktop): string;
52
+ /** `PrivacyInfo.xcprivacy`: what the app collects and which required-reason APIs it uses. */
53
+ export declare function renderPrivacyManifest(config: Pick<ResolvedMacosDesktop, 'privacy'>): string;
54
+ /** Where the build writes a project's app, relative to the project root unless absolute. */
55
+ export declare function macosOutputDirectory(root: string, config: Pick<MacosDesktopConfig, 'output'>): string;
56
+ /**
57
+ * The native macOS app (`config/desktop.ts` `macos`): Craft's AppKit host
58
+ * drawing the project's stx screens, with no launcher and no web view. Every
59
+ * function here is a pure function of the config, so the bundle's contents can
60
+ * be asserted without building or signing anything; `build/desktop-macos.ts`
61
+ * does the building.
62
+ */
63
+ export declare const MACOS_DEFAULTS: {
64
+ minimumSystemVersion: '13.0';
65
+ category: 'public.app-category.productivity';
66
+ nativeScreensDir: 'resources/native';
67
+ initialScreen: 'Main';
68
+ window: {
69
+ width: 1180;
70
+ height: 760;
71
+ minWidth: 820;
72
+ minHeight: 520
73
+ }
74
+ };
75
+ /** What `defaultBrowser` declares the app opens, by Uniform Type Identifier. */
76
+ export declare const BROWSER_DOCUMENT_TYPES: ReadonlyArray<{ name: string, types: string[] }>;
77
+ /** The icon sizes `iconutil` wants in an `.iconset`, from one 1024 pixel PNG. */
78
+ export declare const ICONSET_SIZES: ReadonlyArray<{ file: string, pixels: number }>;
79
+ export declare interface ResolvedMacosDesktop extends Omit<MacosDesktopConfig, 'agent'> {
80
+ agent: MacosAgentConfig | null
81
+ url: string
82
+ version: string
83
+ buildNumber: string
84
+ minimumSystemVersion: string
85
+ category: string
86
+ nativeScreensDir: string
87
+ initialScreen: string
88
+ sandbox: boolean
89
+ executable: string
90
+ }
91
+ export declare interface MacosInfoPlistOptions {
92
+ iconFile?: string
93
+ }
94
+ export type MacosDistribution = 'development' | 'app-store';
@@ -0,0 +1,35 @@
1
+ import{existsSync}from"node:fs";import{join,resolve}from"node:path";import{pathToFileURL}from"node:url";import{plistValue,renderUserlandPlistEntries,xmlEscape}from"./index";export const MACOS_DEFAULTS={minimumSystemVersion:"13.0",category:"public.app-category.productivity",nativeScreensDir:"resources/native",initialScreen:"Main",window:{width:1180,height:760,minWidth:820,minHeight:520}};export async function loadDesktopConfig(root){const file=join(root,"config/desktop.ts");if(!existsSync(file))return null;return(await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default??null}export async function hasMacosDesktop(root){return Boolean((await loadDesktopConfig(root))?.macos)}export async function loadMacosDesktop(root,packageVersion="0.0.0"){const config=await loadDesktopConfig(root);return config?.macos?resolveMacosDesktop(config.macos,packageVersion):null}export function resolveMacosDesktop(config,packageVersion="0.0.0"){const version=config.version??packageVersion,agent=config.agent?config.agent===!0?{}:{...config.agent}:null;return{...config,agent,url:agent?"http://127.0.0.1":config.url??"",version,buildNumber:config.buildNumber??version.split(/[-+]/)[0],minimumSystemVersion:config.minimumSystemVersion??MACOS_DEFAULTS.minimumSystemVersion,category:config.category??MACOS_DEFAULTS.category,nativeScreensDir:config.nativeScreensDir??MACOS_DEFAULTS.nativeScreensDir,initialScreen:config.initialScreen??(Array.isArray(config.nativeScreens)?config.nativeScreens.includes("Main")?"Main":config.nativeScreens[0]:Object.keys(config.nativeScreens).includes("Main")?"Main":Object.keys(config.nativeScreens)[0])??MACOS_DEFAULTS.initialScreen,sandbox:config.sandbox!==!1,executable:config.appName}}export function validateMacosDesktop(config){const errors=[];if(!config.appName?.trim())errors.push("macos.appName is required");if(!/^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z0-9.-]+$/.test(config.bundleId??""))errors.push("macos.bundleId must be a reverse-DNS identifier such as com.example.app");if(config.teamId!==void 0&&!/^[A-Z0-9]{10}$/.test(config.teamId))errors.push("macos.teamId must be the 10-character Apple Developer team ID");if(!/^\d+(?:\.\d+){0,2}$/.test(config.version))errors.push(`macos.version must be one to three numeric components, not "${config.version}"`);if(!/^[A-Za-z0-9.-]+$/.test(config.buildNumber))errors.push("macos.buildNumber must contain only letters, numbers, periods and hyphens");if(!/^\d+(?:\.\d+){0,2}$/.test(config.minimumSystemVersion))errors.push("macos.minimumSystemVersion must be a macOS version such as 13.0");if(!/^public\.app-category\.[a-z][a-z0-9-]*$/.test(config.category))errors.push("macos.category must be an LSApplicationCategoryType such as public.app-category.productivity");if(!config.agent)try{const url=new URL(config.url);if(!["http:","https:"].includes(url.protocol))errors.push("macos.url must be an http(s) origin")}catch{errors.push("macos.url must be the server origin the app talks to, e.g. https://example.com, or turn on macos.agent to run the API inside the app")}else if(config.agent.port!==void 0&&!(Number.isInteger(config.agent.port)&&config.agent.port>1024&&config.agent.port<65536))errors.push("macos.agent.port must be a port above 1024");const lights=config.window?.trafficLights;if(lights!==void 0&&!(Array.isArray(lights)&&lights.length===2&&lights.every((value)=>Number.isFinite(value)&&value>=0)))errors.push("macos.window.trafficLights must be [x, y], two points from the window's top-left");for(const[index,item]of(config.dockMenu??[]).entries()){if(!item?.title?.trim())errors.push(`macos.dockMenu[${index}] needs a title`);if(!item?.action?.trim())errors.push(`macos.dockMenu[${index}] needs an action`)}const names=Object.keys(macosScreens(config));if(!names.length)errors.push("macos.nativeScreens must name at least one screen");if(names.length&&!names.includes(config.initialScreen))errors.push(`macos.initialScreen "${config.initialScreen}" is not one of the native screens (${names.join(", ")})`);for(const scheme of config.urlSchemes??[])if(!/^[a-z][a-z0-9+.-]*$/.test(scheme))errors.push(`macos.urlSchemes: "${scheme}" is not a valid URL scheme`);return errors}export function macosScreens(config){const dir=config.nativeScreensDir??MACOS_DEFAULTS.nativeScreensDir;if(Array.isArray(config.nativeScreens))return Object.fromEntries(config.nativeScreens.map((name)=>[name,`${dir}/${name}.stx`]));return{...config.nativeScreens}}export function renderHostConfig(config){const window={...MACOS_DEFAULTS.window,...config.window};return`${JSON.stringify({...config.agent?{}:{apiUrl:config.url.replace(/\/+$/,"")},title:config.appName,route:config.initialScreen,session:Boolean(config.agent),titlebarStyle:window.titlebarStyle??"standard",width:window.width,height:window.height,minWidth:window.minWidth,minHeight:window.minHeight,...window.trafficLights?{trafficLights:window.trafficLights}:{},...config.webkit?{webkit:!0}:{},...config.dockMenu?.length?{dockMenu:config.dockMenu.map((item)=>({title:item.title,action:item.action}))}:{}},null,2)}
2
+ `}export function hostArguments(config,nativeBundle){return["--api-url",config.url.replace(/\/+$/,""),"--native-bundle",nativeBundle,...windowArguments(config),"--no-session"]}export function agentHostArguments(config){return windowArguments(config)}function windowArguments(config){const window={...MACOS_DEFAULTS.window,...config.window};return["--title",config.appName,"--route",config.initialScreen,"--titlebar-style",window.titlebarStyle??"standard","--width",String(window.width),"--height",String(window.height),"--min-width",String(window.minWidth),"--min-height",String(window.minHeight),...window.trafficLights?["--traffic-lights",window.trafficLights.join(",")]:[]]}const OWNED_KEYS=["CFBundleIdentifier","CFBundleExecutable","CFBundleName","CFBundleDisplayName","CFBundleShortVersionString","CFBundleVersion","CFBundlePackageType","LSApplicationCategoryType","LSMinimumSystemVersion","CFBundleIconFile","CFBundleURLTypes"];export const BROWSER_DOCUMENT_TYPES=[{name:"HTML document",types:["public.html"]},{name:"XHTML document",types:["public.xhtml"]},{name:"Web archive",types:["com.apple.webarchive"]},{name:"PDF document",types:["com.adobe.pdf"]}];export function defaultUsageDescriptions(appName){return{camera:`${appName} uses the camera when you allow a page or feature to.`,microphone:`${appName} uses the microphone when you allow a page or feature to.`,location:`${appName} uses your location when you allow a page or feature to.`}}export function permissionUsageDescriptions(config){const permissions=config.permissions??{},defaults=defaultUsageDescriptions(config.appName),sentence=(value,fallback)=>typeof value==="string"&&value.trim()?value:value?fallback:void 0,usage={},camera=sentence(permissions.camera,defaults.camera);if(camera)usage.NSCameraUsageDescription=camera;const microphone=sentence(permissions.microphone,defaults.microphone);if(microphone)usage.NSMicrophoneUsageDescription=microphone;const location=sentence(permissions.location,defaults.location);if(location){usage.NSLocationUsageDescription=location;usage.NSLocationWhenInUseUsageDescription=location}return usage}export function renderMacosInfoPlist(config,options={}){const owns=config.defaultBrowser?[...OWNED_KEYS,"CFBundleDocumentTypes"]:OWNED_KEYS,declared={...permissionUsageDescriptions(config),...config.usageDescriptions,...config.infoPlist};if(config.defaultBrowser&&declared.NSAppTransportSecurity===void 0)declared.NSAppTransportSecurity={NSAllowsArbitraryLoadsInWebContent:!0};const{xml:extra,ignored}=renderUserlandPlistEntries(Object.fromEntries(Object.entries(declared).filter(([key])=>!owns.includes(key)))),owned=Object.keys(declared).filter((key)=>owns.includes(key)),urlTypes=[];if(config.urlSchemes?.length)urlTypes.push({CFBundleURLName:config.bundleId,CFBundleURLSchemes:config.urlSchemes});if(config.defaultBrowser)urlTypes.push({CFBundleURLName:"Web site URL",CFBundleTypeRole:"Viewer",CFBundleURLSchemes:["http","https"]});const schemes=urlTypes.length?` <key>CFBundleURLTypes</key>${plistValue(urlTypes," ")}
3
+ `:"",documents=config.defaultBrowser?` <key>CFBundleDocumentTypes</key>${plistValue(BROWSER_DOCUMENT_TYPES.map((type)=>({CFBundleTypeName:type.name,CFBundleTypeRole:"Viewer",LSHandlerRank:"Default",LSItemContentTypes:type.types}))," ")}
4
+ `:"",icon=options.iconFile?` <key>CFBundleIconFile</key><string>${xmlEscape(options.iconFile)}</string>
5
+ `:"";return{xml:`<?xml version="1.0" encoding="UTF-8"?>
6
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
7
+ <plist version="1.0">
8
+ <dict>
9
+ <key>CFBundleDevelopmentRegion</key><string>en</string>
10
+ <key>CFBundleDisplayName</key><string>${xmlEscape(config.appName)}</string>
11
+ <key>CFBundleExecutable</key><string>${xmlEscape(config.executable)}</string>
12
+ <key>CFBundleIdentifier</key><string>${xmlEscape(config.bundleId)}</string>
13
+ <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
14
+ <key>CFBundleName</key><string>${xmlEscape(config.appName)}</string>
15
+ <key>CFBundlePackageType</key><string>APPL</string>
16
+ <key>CFBundleShortVersionString</key><string>${xmlEscape(config.version)}</string>
17
+ <key>CFBundleVersion</key><string>${xmlEscape(config.buildNumber)}</string>
18
+ <key>LSApplicationCategoryType</key><string>${xmlEscape(config.category)}</string>
19
+ <key>LSMinimumSystemVersion</key><string>${xmlEscape(config.minimumSystemVersion)}</string>
20
+ <key>NSHighResolutionCapable</key><true/>
21
+ <key>NSSupportsAutomaticGraphicsSwitching</key><true/>
22
+ <key>ITSAppUsesNonExemptEncryption</key><false/>
23
+ ${icon}${schemes}${documents}${extra}</dict>
24
+ </plist>
25
+ `,ignored:[...ignored,...owned]}}export function renderMacosEntitlements(config,distribution){const entitlements={},permissions=config.permissions??{};if(config.sandbox){entitlements["com.apple.security.app-sandbox"]=!0;if(permissions.network!==!1)entitlements["com.apple.security.network.client"]=!0;if(config.agent)entitlements["com.apple.security.network.server"]=!0;if(permissions.downloads)entitlements[permissions.downloads==="read-only"?"com.apple.security.files.downloads.read-only":"com.apple.security.files.downloads.read-write"]=!0;if(permissions.printing)entitlements["com.apple.security.print"]=!0}if(permissions.camera)entitlements["com.apple.security.device.camera"]=!0;if(permissions.microphone)entitlements["com.apple.security.device.audio-input"]=!0;if(permissions.location)entitlements["com.apple.security.personal-information.location"]=!0;if(config.agent){entitlements["com.apple.security.cs.allow-jit"]=!0;entitlements["com.apple.security.cs.allow-unsigned-executable-memory"]=!0;entitlements["com.apple.security.cs.disable-library-validation"]=!0}if(distribution==="app-store"){if(!config.teamId)throw Error("macos.teamId is required to sign for the Mac App Store");entitlements["com.apple.application-identifier"]=`${config.teamId}.${config.bundleId}`;entitlements["com.apple.developer.team-identifier"]=config.teamId;entitlements["keychain-access-groups"]=[`${config.teamId}.${config.bundleId}`]}Object.assign(entitlements,config.entitlements);return renderEntitlementsPlist(entitlements)}export function renderMacosHostEntitlements(config){if(config.sandbox)return renderEntitlementsPlist({"com.apple.security.app-sandbox":!0,"com.apple.security.inherit":!0});const permissions=config.permissions??{},entitlements={};if(permissions.camera)entitlements["com.apple.security.device.camera"]=!0;if(permissions.microphone)entitlements["com.apple.security.device.audio-input"]=!0;if(permissions.location)entitlements["com.apple.security.personal-information.location"]=!0;return renderEntitlementsPlist(entitlements)}function renderEntitlementsPlist(entitlements){return`<?xml version="1.0" encoding="UTF-8"?>
26
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
27
+ <plist version="1.0">
28
+ ${plistValue(entitlements)}
29
+ </plist>
30
+ `}export function renderPrivacyManifest(config){const privacy=config.privacy??{},manifest={NSPrivacyTracking:privacy.tracking===!0,NSPrivacyTrackingDomains:privacy.trackingDomains??[],NSPrivacyCollectedDataTypes:(privacy.collectedDataTypes??[]).map((item)=>({NSPrivacyCollectedDataType:item.type,NSPrivacyCollectedDataTypeLinked:item.linked===!0,NSPrivacyCollectedDataTypeTracking:item.tracking===!0,NSPrivacyCollectedDataTypePurposes:item.purposes})),NSPrivacyAccessedAPITypes:(privacy.accessedApiTypes??[]).map((item)=>({NSPrivacyAccessedAPIType:item.type,NSPrivacyAccessedAPITypeReasons:item.reasons}))};return`<?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0">
33
+ ${plistValue(manifest)}
34
+ </plist>
35
+ `}export const ICONSET_SIZES=[16,32,128,256,512].flatMap((size)=>[{file:`icon_${size}x${size}.png`,pixels:size},{file:`icon_${size}x${size}@2x.png`,pixels:size*2}]);export function macosOutputDirectory(root,config){return config.output?resolve(root,config.output):join(root,"storage/framework/desktop/macos")}
@@ -0,0 +1,54 @@
1
+ import type { DesktopAgentManifest } from './agent-launcher';
2
+ import type { MacosDistribution, ResolvedMacosDesktop } from './macos-app';
3
+ /** A `.icns` from a square PNG: the sizes `iconutil` wants, resized with `sips`. */
4
+ export declare function makeIcns(png: string, output: string, scratch: string, run?: BundleCommand): void;
5
+ /**
6
+ * Lay out and sign the `.app`: the host as the executable, the compiled
7
+ * screens and host settings in Resources, then `codesign` with the entitlements
8
+ * for the chosen distribution. The same function builds the development app and
9
+ * the Mac App Store app, so what runs locally is what ships.
10
+ */
11
+ export declare function assembleMacosApp(options: AssembleMacosAppOptions): AssembledMacosApp;
12
+ /**
13
+ * The first "Apple Development" identity in `security find-identity -v -p codesigning`
14
+ * output, by its hash (a name can be shared by two certificates).
15
+ */
16
+ export declare function parseDevelopmentIdentity(output: string): string | undefined;
17
+ /**
18
+ * The identity a development build is signed with, when the machine has one.
19
+ *
20
+ * Ad hoc signing gives every rebuild a new code hash, and a Keychain item's
21
+ * access list trusts the code that wrote it: the next launch of a rebuilt app
22
+ * stops on a "wants to use your confidential information" prompt, with no
23
+ * window until someone answers it. A stable certificate keeps the app's
24
+ * designated requirement the same across rebuilds, so the item stays trusted.
25
+ */
26
+ export declare function findDevelopmentIdentity(read?: () => string): string | undefined;
27
+ /** The AppKit host's name inside `Contents/MacOS` when an agent launcher is the executable. */
28
+ export declare const AGENT_HOST_EXECUTABLE: 'craft-runtime';
29
+ export declare const runBundleCommand: BundleCommand;
30
+ export declare interface MacosAgentBundle {
31
+ launcher: string
32
+ manifest: Omit<DesktopAgentManifest, 'host' | 'nativeBundle'>
33
+ }
34
+ export declare interface AssembleMacosAppOptions {
35
+ config: ResolvedMacosDesktop
36
+ root: string
37
+ host: string
38
+ nativeBundle: string
39
+ output: string
40
+ distribution: MacosDistribution
41
+ provisioningProfile?: string
42
+ signingIdentity?: string
43
+ keychain?: string
44
+ unsandboxed?: boolean
45
+ agent?: MacosAgentBundle
46
+ run?: BundleCommand
47
+ }
48
+ export declare interface AssembledMacosApp {
49
+ appPath: string
50
+ entitlementsPath: string
51
+ ignoredPlistKeys: string[]
52
+ notes: string[]
53
+ }
54
+ export type BundleCommand = (args: string[]) => void;
@@ -0,0 +1,3 @@
1
+ import{chmodSync,copyFileSync,cpSync,existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";import{AGENT_MANIFEST}from"./agent-launcher";import{ICONSET_SIZES,renderHostConfig,renderMacosEntitlements,renderMacosHostEntitlements,renderMacosInfoPlist,renderPrivacyManifest}from"./macos-app";export const AGENT_HOST_EXECUTABLE="craft-runtime",runBundleCommand=(args)=>{const result=Bun.spawnSync(args,{stdout:"pipe",stderr:"pipe"});if(result.exitCode!==0)throw Error(`${args[0]} ${args.slice(1).join(" ")} exited with ${result.exitCode}: ${result.stderr.toString().trim()||result.stdout.toString().trim()}`)};export function makeIcns(png,output,scratch,run=runBundleCommand){const iconset=join(scratch,"AppIcon.iconset");rmSync(iconset,{recursive:!0,force:!0});mkdirSync(iconset,{recursive:!0});for(const{file,pixels}of ICONSET_SIZES)run(["sips","-z",String(pixels),String(pixels),png,"--out",join(iconset,file)]);run(["iconutil","-c","icns",iconset,"-o",output]);rmSync(iconset,{recursive:!0,force:!0})}export function assembleMacosApp(options){const run=options.run??runBundleCommand,{config}=options,notes=[],appPath=join(options.output,`${config.appName}.app`),contents=join(appPath,"Contents"),macos=join(contents,"MacOS"),resources=join(contents,"Resources");rmSync(appPath,{recursive:!0,force:!0});mkdirSync(macos,{recursive:!0});mkdirSync(resources,{recursive:!0});if(options.agent){if(!config.agent)throw Error("An agent launcher was given for an app whose config has no macos.agent");copyFileSync(options.agent.launcher,join(macos,config.executable));copyFileSync(options.host,join(macos,AGENT_HOST_EXECUTABLE));chmodSync(join(macos,AGENT_HOST_EXECUTABLE),493);const manifest={...options.agent.manifest,host:`../MacOS/${AGENT_HOST_EXECUTABLE}`,nativeBundle:"native-screen.js"};if(options.agent.manifest.staticDir){cpSync(options.agent.manifest.staticDir,join(resources,"web"),{recursive:!0});manifest.staticDir="web"}writeFileSync(join(resources,AGENT_MANIFEST),`${JSON.stringify(manifest,null,2)}
2
+ `)}else copyFileSync(options.host,join(macos,config.executable));chmodSync(join(macos,config.executable),493);copyFileSync(options.nativeBundle,join(resources,"native-screen.js"));writeFileSync(join(resources,"craft-app.json"),renderHostConfig(config));writeFileSync(join(resources,"PrivacyInfo.xcprivacy"),renderPrivacyManifest(config));let iconFile;if(config.icon){const icon=resolve(options.root,config.icon);if(!existsSync(icon))throw Error(`macos.icon does not exist: ${icon}`);if(icon.endsWith(".icns"))copyFileSync(icon,join(resources,"AppIcon.icns"));else makeIcns(icon,join(resources,"AppIcon.icns"),options.output,run);iconFile="AppIcon"}else notes.push("No macos.icon: the app has the generic icon, which the Mac App Store rejects.");const plist=renderMacosInfoPlist(config,{iconFile});writeFileSync(join(contents,"Info.plist"),plist.xml);if(options.distribution==="app-store"){if(!options.provisioningProfile)throw Error("A Mac App Store build needs a provisioning profile");copyFileSync(options.provisioningProfile,join(contents,"embedded.provisionprofile"))}const signed=options.unsandboxed?{...config,sandbox:!1}:config,entitlementsPath=join(options.output,`${config.appName}.entitlements`);writeFileSync(entitlementsPath,renderMacosEntitlements(signed,options.distribution));const hostEntitlements=join(options.output,`${config.appName}.host.entitlements`);if(options.agent)writeFileSync(hostEntitlements,renderMacosHostEntitlements(signed));run(["plutil","-lint",join(contents,"Info.plist"),join(resources,"PrivacyInfo.xcprivacy"),entitlementsPath,...options.agent?[hostEntitlements]:[]]);const identity=options.signingIdentity||"-";if(options.agent)run(["codesign","--force","--options","runtime",...identity==="-"?[]:["--timestamp"],"--entitlements",hostEntitlements,...options.keychain?["--keychain",options.keychain]:[],"--sign",identity,join(macos,AGENT_HOST_EXECUTABLE)]);run(["codesign","--force","--options","runtime",...identity==="-"?[]:["--timestamp"],"--entitlements",entitlementsPath,...options.keychain?["--keychain",options.keychain]:[],"--sign",identity,appPath]);run(["codesign","--verify","--deep","--strict",appPath]);return{appPath,entitlementsPath,ignoredPlistKeys:plist.ignored,notes}}export function parseDevelopmentIdentity(output){for(const line of output.split(`
3
+ `)){const match=/^\s*\d+\)\s+([0-9A-F]{40})\s+"Apple Development:/.exec(line);if(match)return match[1]}return}export function findDevelopmentIdentity(read=()=>Bun.spawnSync(["security","find-identity","-v","-p","codesigning"],{stdout:"pipe",stderr:"ignore"}).stdout.toString()){try{return parseDevelopmentIdentity(read())}catch{return}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/desktop-build",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.390",
5
+ "version": "0.75.398",
6
6
  "description": "Build and launch Craft desktop applications for Stacks",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -73,11 +73,29 @@
73
73
  "prepublishOnly": "bun run build"
74
74
  },
75
75
  "dependencies": {
76
+ "@stacksjs/config": "0.75.398",
77
+ "@stacksjs/router": "0.75.398",
78
+ "@stacksjs/server": "0.75.398",
76
79
  "craft-native": ">=0.0.341"
77
80
  },
81
+ "peerDependencies": {
82
+ "@stacksjs/config": "0.75.398",
83
+ "@stacksjs/router": "0.75.398",
84
+ "@stacksjs/server": "0.75.398"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "@stacksjs/config": {
88
+ "optional": true
89
+ },
90
+ "@stacksjs/router": {
91
+ "optional": true
92
+ },
93
+ "@stacksjs/server": {
94
+ "optional": true
95
+ }
96
+ },
78
97
  "devDependencies": {
79
- "@stacksjs/build": "0.75.390",
80
- "better-dx": "^0.2.24",
81
- "@stacksjs/server": "0.75.390"
98
+ "@stacksjs/build": "0.75.398",
99
+ "better-dx": "^0.2.24"
82
100
  }
83
101
  }