Contributing
Keep app behavior, CLI commands, and documentation in sync.
New app and hosted binary releases target Apple silicon (arm64) only. Use native Apple silicon runners; existing Intel download objects are retained for compatibility.
Shared architecture
MorrowCore owns native discovery, provisioning, validation, lifecycle, and persistent state. MorrowApp supplies the macOS UI, and MorrowCLI exposes the same manager to Terminal.
Make shared behavior changes in the core so both interfaces remain consistent. Keep database creation as one operation; engine and version selection belong in the creation dialog.
App interface
DS.Surface owns the shared appearance palette. Light-mode cells use the warm gray #F5F4F4 from the reference, with slightly lighter neutral controls; dark mode uses charcoal equivalents. Buttons, menu controls, input fields, and brand-icon backplates share those colors. Hover, pressed, and disabled states adjust the neutral fill without tinting the whole surface. Keep outlines quiet and shadows shallow; avoid pure-white card blocks, broad floating shadows, or pane-specific color overrides. Geometry, typography, window material, and grouped action corners remain independent of this palette.
Use the shared DesignSystem components for controls. Action buttons use a subtle light surface that adapts to light and dark mode, a thin border, and medium-weight text with a restrained one-point shadow. SettingsCard publishes its size and coordinate space; full-width actions keep softly rounded interior corners and automatically increase the radius at the card edges. A single action rounds all corners, first and last actions follow top and bottom edges, and middle actions use a restrained three-point radius and card-edge corners expand to eleven points. Bottom action rows keep modest top corners and larger lower corners that follow the card. Compact action controls use a five-point radius. Use SettingsActionGroup for side-by-side actions; only their outer card-edge corners expand. Action rows/groups share balanced four-point padding and half-point dividers. Do not specify row positions or per-pane corner overrides.
Settings cells use settingsCellPadding() for fourteen-point side gutters and twelve-point top/bottom insets. SettingRow applies these insets before its 44-point minimum height, allowing titles, subtitles, and wrapped labels to grow naturally without squeezing text against the cell edges. Custom service rows and card content use the same helper and SettingsLayout tokens. Button labels keep their independent eleven-point inset; spacing changes must preserve the approved control appearance. Section gaps remain twelve points, and the window size stays fixed.
In the menu popover, use compact actions with the shared quiet neutral surface and borderless secondary icons. Icon backplates appear only on hover or press. Keep creation buttons centered and appropriately sized rather than stretching a heavy settings-card button across the menu.
Use MenuLayout and MenuServiceRow for menu geometry: 300-point width, 46-point rows, 24-point icons and action targets, 12-point horizontal insets, and a shared six-row viewport. Header/footer, empty state, separators, and preview rendering use the same tokens. Keep service names at 12 points and metadata at 10 points; compactness comes from spacing rather than unreadable text. Fix the menu to its intrinsic vertical size. Toasts belong to application windows and sheets; never attach a feedback overlay to the menu dropdown.
About centers a modest app identity above a Resources group with documentation, source, and issue-reporting links. Keep marketing text and implementation details out of this pane. Empty database views offer one creation action at the card's bottom edge. The menu footer contains only its settings and quit controls. Settings cards provide full-width actions for command installation and service creation.
Settings titles and icons remain pinned while detail content scrolls. The header samples the window's native material only after scrolling begins. Its alpha mask remains opaque through the title's midpoint, then fades smoothly to transparent. Keep the title outside the effect, avoid opaque fills or bottom borders, and disable the fade animation when Reduce Motion is enabled. The shared layout retains its existing window size and gutters in both appearances.
Service lifecycle actions use ServiceActionButton: start uses a play icon, stop uses a red stop icon, and secondary icons have tooltips and accessible labels. Use ServiceStatusView for plain live status. SettingsPageFeedback places messages at the top of their related page; serviceFeedback() supplies the same inline card to sheets. Use the shared ServiceLogViewer and configuration editor rather than another sidebar page or bespoke file opener. Runtime and database tiles resolve bundled brand marks through BrandIcons.
SettingsFeedbackCard uses SettingsCard, normal cell gutters, a small leading status icon, readable text, and an optional shared action and close control. Keep it in the content flow beneath the floating page title, without covering controls or adding a floating HUD. Light and dark appearances use the existing cell palette. Capture the originating page/sheet before starting asynchronous operations; return a sheet's outstanding message to its page when it closes. Success messages remain for at least six seconds of actual visibility, actionable messages for at least eight, and hovering pauses dismissal. Errors and VoiceOver messages remain until dismissed. Announce each message once from the model and respect Reduce Motion. An action must not dismiss a newer message created by that action.
Present application sheets through feedbackSheet(item:) or feedbackSheet(isPresented:) so inline feedback is supplied by the design system once and appears only in the originating sheet. Do not add another feedback host inside the sheet content. Setup-helper reminders belong to General and Sites, follow native permission status, and persist until access is restored. Users who have never requested local setup should not see an approval reminder.
General is one scrollable grouped page. Sidebar navigation groups Projects, Services, and App. Binary sourcing is automatic; keep catalog URLs and provider configuration out of normal preferences. Advanced configuration belongs to the CLI. Routine HTTPS changes use the project switch and padlock; show a card only when setup or a failure needs attention. Installation inventory and release catalogs are saved in a private cache; use explicit refresh for rescans, with a daily installation refresh at startup. Never cache running status as the source of truth: status still checks owned processes and native readiness.
Build and verify
To use Morrow, download the signed app. The steps below are for developing Morrow itself. Install a full version of Xcode; the scripts select its current macOS SDK.
git clone https://github.com/kkz6/Morrow.git
cd Morrow
./scripts/test.sh
./scripts/build-app.sh
./scripts/build-cli.sh
open build/Morrow.app --args --settings
Tests cover state, provider configuration, log reading, creation, duplicate prevention, and port validation. Real native integration checks should use an isolated MORROW_HOME and clean up their managed jobs.
Documentation workflow
The website is a Nuxt application in website/. Its source pages are Markdown in website/content/; generated HTML and search data are derived at build time.
cd website
npm ci
npm run dev
Use Node 24.15 or newer. To produce the static site:
npm run generate
npm run check
Changes pushed to main rebuild and publish the documentation through GitHub Pages. Keep feature changes and relevant documentation together in the same pull request. The docs checker catches missing pages, broken local links, and uncovered CLI command names.
Add a feature
- Implement shared behavior and expose it in the app and CLI where appropriate.
- Add meaningful verification for behavior that needs it.
- Update relevant guide pages, the CLI reference if commands changed, and the changelog.
- Run the documentation build and checker before publishing.
The repository’s AGENTS.md records this requirement for future coding sessions. It supports keeping documentation current; it does not generate unreviewed feature descriptions automatically.
Current scope
This developer preview includes local databases, PHP/Go/Flutter/Node/Python/Ruby runtimes, folder runtime selections, hosted Node management with existing nvm discovery, Mailpit, PHP/static/app-port hosting, and MinIO S3 buckets. Authenticated database configuration, backup/export and S3 backup delivery, remote server management, and RabbitMQ are future work.
Folder choices live in a separate private project-runtimes.json document so older running workers cannot discard them when saving the workspace. Shared validation, closest-ancestor resolution, installation, and PHP hosting projection belong in MorrowCore. The app uses cached inventory for selectors. Project selection writes command wrappers without changing global defaults or nvm's default alias. Preserve explicit PHP route choices when adopting folder selections, and reject forgetting a runtime still referenced by a folder. Deployment can restart the directory watcher to adopt the new resolver while leaving databases, mail, DNS, Caddy, and existing PHP pools running.
Dependency status
BinaryInstaller owns shared installation policy. Prefer validated existing copies and ManagedBinaryStore distributions; never execute a package manager. Legacy discovery may read existing installation paths but cannot install or update them. Catalog downloads require HTTPS, SHA-256, platform filtering, private staging, safe extraction, and actual version validation. Go has an official archive provider. Node uses the shared managed provider; nvm support is read-only discovery. Keep distribution libraries/resources relocatable; do not advertise an unpublished package as installable. See Binary downloads for catalog fields and current availability.
SDK packaging must preserve macOS framework bundle install names so generated applications can embed them normally. After relocating and signing contained native code, sign and verify the enclosing macOS frameworks and apps from the inside out. Keep iOS and other target-platform artifacts under their upstream signatures.
PHP source builds resolve Apple's iconv headers and library through the selected SDK's usr directory, alongside explicit SDK flags for system XML, curl, SQLite, and zlib. They must not discover those headers through a host package manager.
Static PHP intl linking includes ICU's I/O library before its internationalization, common, and data libraries. The complete library group is required for message formatting as well as the locale APIs.
Use the Notarization diagnostics workflow to retrieve Apple's rejection details through the existing temporary release profile. It uploads submission history and diagnostic JSON only; signing keys and passwords stay in GitHub's encrypted secrets and temporary keychain. A rejected package remains unpublished until a corrected submission is accepted.
Flutter distributions omit the optional .pub-preload-cache archives, which contain unsigned example executables and encrypted parser fixtures. Keep their dependency license notices under licenses/pub; preserve the SDK, Dart, engine resources, and upstream target-platform signatures. Pub fetches original packages normally when needed, without changing their upstream checksums. The package workflow retains Apple's rejection JSON and detailed log on failure.
The Nuxt toolchain currently reports upstream high-severity npm audit advisories involving its development dependencies. Patched Git parser versions are pinned through overrides. The published site contains static HTML, CSS, and client scripts; its development server stays on 127.0.0.1. Recheck advisories when updating dependencies rather than forcing an incompatible Nuxt downgrade.
Customer distribution
Build the complete app, including the bundled dev.morrow.helper.setup.plist, with a Developer ID identity:
MORROW_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./scripts/build-app.sh release
The build signs nested executables before the app, enables hardened runtime, and requests a timestamp when an identity is provided. Default development builds remain ad-hoc signed. The Signed macOS release workflow imports private signing material into a temporary keychain, builds with Xcode 27, submits to Apple, staples the accepted ticket, and packages an Apple silicon release with its SHA-256 digest. Its cleanup always removes temporary signing material. See Release signing and hosting for configuration and download publishing. Providing a signing variable alone does not notarize a local build.
Setup uses native SMAppService approval. Do not reintroduce AppleScript/Python elevation or cache a sudo password. Keep the setup helper's operations limited to validated local DNS and gateway installation.
Background-item attribution
Register the app with Launch Services before repairing attribution. BackgroundAttribution checks the app and launcher signatures and Apple team identifiers. The build rejects a supplied signing identity when those teams differ or are missing. Ad-hoc development builds remain supported, but cleanup must explain their grouping limitation and avoid converting more unsigned native workers into separately named Morrow entries.
To stage an app without replacing the bundle used by current services:
MORROW_APP_OUTPUT="$PWD/.build/staged/Morrow.app" ./scripts/build-app.sh release
Staging does not update build/morrow. Signing and deployment can be completed later; staging alone does not change macOS's allowed list. Never modify the global background-task database to hide entries.
The setup helper's attribution action changes only the protected gateway plist's association metadata. It validates user ownership and matching helper/gateway signing teams and does not invoke launchctl or local-domain setup. Attribution requests use zero ports so an older helper rejects them before changing any domains. Do not replace a pending domain-setup request with an attribution request.
Explicit signed-app cleanup re-registers an older native setup helper before submitting gateway repair. The private registration marker records the app path and executable digest, avoiding repeated re-registration when approval is pending. Use the asynchronous unregister completion before registering the replacement. This updates only the setup helper and never re-registers active server jobs. Preserve prior domain requests and responses for normal monitoring to finish.
Use the launcher's signed CodeDirectory digest for the registration marker. Signing timestamps change the file checksum even when executable code is identical; those changes should not replace an already approved helper.
On app startup, refresh a previously enabled helper only when that recorded executable digest or app path changed. Preserve a pending domain request, and do not register a helper for users who have never enabled setup. This keeps signed CLI updates from leaving an approved helper pinned to an obsolete executable requirement.
The build embeds the signed launcher's public digest in the setup daemon's environment before signing the app. This changes the launchd configuration checksum with the executable, so native re-registration can update the job configuration rather than reuse an obsolete disposition.
The production helper uses dev.morrow.helper.setup. The old development helper plist is retained only for native unregistration, never for new registration. Handle SMAppServiceErrorDomain code 1 as an approval requirement: Apple can return it before the status API reflects pending approval. Present the in-app explanation and link to native Login Items approval instead of reporting a generic failure or expecting a password dialog to appear automatically.
Read SMAppService status at startup, during monitoring, and when the app becomes active. Never use an operation-local boolean as permission state. Approval and working routing are separate: validate routing before reporting setup complete. Do not register helpers merely to refresh permission. BackgroundRegistrations may archive inactive orphan Morrow jobs and normalize retained definitions; it preserves running services and other apps' jobs. macOS controls historical list entries and final grouping.
Generated launchd jobs declare AssociatedBundleIdentifiers for Morrow. When the bundled CLI and app have valid matching Apple signing teams, database, mail, web, and S3 workers launch through Morrow's foreground service runner. Ad-hoc development builds launch native workers directly to avoid adding more ungrouped Morrow rows. The signed runner preserves environment/output, forwards stop signals to the owned native child, and exits with that child's status.
Customer executables must share Morrow's Developer Team ID for macOS attribution. macOS owns the final Login Items grouping; ad-hoc previews, old registrations, or standalone unsigned binaries can appear separately. Never modify the system background-task database or claim that separate server processes disappear. Existing services adopt updated job metadata when recreated or restarted; do not stop user databases merely to refresh a settings list.
Website design and publishing
The homepage and documentation share the website’s CSS tokens, Fraunces/Inter typography, slate and teal palette, native app imagery, and original workspace pattern. Keep navigation, search, downloads, mobile layouts, and keyboard focus consistent across both surfaces. Source Markdown stays in website/content; it remains the documentation source of truth.
npm run generate resolves the latest published Apple silicon ZIP from GitHub Releases. It uses the verified release in website/release.json if the API is unavailable. The same release metadata supplies the homepage, navigation, and Getting Started downloads. The documentation workflow also runs after a successful signed release, keeping download links current.
GitHub Pages publishes website/.output/public. MORROW_SITE_BASE_PATH and MORROW_SITE_URL repository variables select the deployment path and canonical URL. Keep the default /Morrow/ path until the custom domain is configured; use / with https://morrow.karti.dev/ after DNS is ready. Markdown links are generated for the selected base path. A custom subdomain uses a DNS-only CNAME to kkz6.github.io, with HTTPS enforced in GitHub Pages once the certificate is available.