[中文] [English]
Thanks for your interest in MacTools. Please keep each contribution small and clear: explain the problem, provide verifiable changes, and avoid mixing unrelated refactors into the same pull request.
Unless a file is clearly identified as third-party material under separate terms, contributions accepted into MacTools are licensed under GPL-3.0-only, consistent with the repository's licensing policy. By submitting a contribution, you confirm that you have the right to provide it under those terms.
- Bug reports should include reproduction steps, expected behavior, actual behavior, macOS version, and relevant logs or screenshots.
- Feature suggestions should describe the use case, target users, and expected interaction. For large plugins or interaction changes, open an issue first to align on scope.
- Changes involving file deletion, system permissions, global shortcuts, display control, signing, or update flows should explain risks, safeguards, and rollback options.
- Xcode and
xcodegenare required. The project supports macOS 14.0 and later. - First-time setup: run
make setup, then editLocalConfig.xcconfigand fill inDEVELOPMENT_TEAMand a stable, non-placeholderBUNDLE_IDENTIFIER_PREFIX. Debug builds fail early when either value is missing so macOS cannot register a malformed duplicate app identity. - Use
make runfor local app testing. It installs the canonical Debug app at~/Applications/MacTools Dev.appand unregisters otherMacTools Devbuild copies from LaunchServices. - Common commands:
make generategenerates the Xcode project,make buildvalidates compilation, andmake runinstalls the verified Debug bundle at~/Applications/MacTools Dev.appbefore running it locally. - Plugin development:
make runincrementally builds the app and plugins, then syncs the latest Debug plugin packages to the local development marketplace. A full sync moves packages absent from the current checkout into the recoverable Debug quarantine; a filteredPLUGIN=...sync leaves unrelated packages untouched.make sync-debug-pluginsonly syncs already built plugins.make build-pluginis reserved for validating dynamic plugin packages or release flows; to build one plugin, runmake build-plugin PLUGIN=calendar. - Do not commit local or generated files:
MacTools.xcodeproj,MacTools.xcworkspace,LocalConfig.xcconfig,build/, orscripts/release.local.env.
Sources/App/: app entry point, menu bar status item, settings pages, and window routing.Sources/Core/: shared infrastructure such as the plugin host, dynamic plugin loading, shortcuts, permissions, logging, and updates.Sources/MacToolsPluginKit/: plugin APIs, declarative UI models, and runtime context.Plugins/<PluginName>/: plugin manifest, source code, bundle entry point, resources, and adjacent tests.Tests/: XCTest coverage for shared App/Core logic. Plugin tests should live inside the corresponding plugin directory when possible.project.yml: root XcodeGen project source, only for the App, PluginKit, and shared aggregate entry points. Plugin targets are generated automatically.Plugins/<PluginName>/project.yml: optional per-plugin build overrides, only when a plugin needs extra frameworks, include paths, bundle resources, helper/tool targets, or target overrides.docs/plugins/: plugin packages, catalogs, local debugging, and release flow documentation.docs/icon-gallery/: checked-in menu-bar icon catalog, previews, animation frames, and archives; rendering-mode rules are documented indocs/icon-gallery.md.docs/superpowers/: larger product, interaction, or implementation design documents.
- Add new plugins under
Plugins/<PluginName>/with at leastplugin.json,Sources/, andBundle/. - Ordinary plugins only need to define
plugin.json, source code, and a bundle entry point.make generatescansPlugins/*/plugin.jsonand generates localConfigs/GeneratedPlugins.yml; do not edit generated files manually. - Features that require macOS app extensions, such as Finder Sync, must add the extension target to the root
project.ymland embed it in the main app. Use the dynamic plugin only for the MacTools panel/settings surface. - Command workflows for adding and updating plugins are documented in the Development Steps section of
docs/plugins/local-native-plugins.md. - Keep documentation short and task-focused. User-visible behavior changes should update
README.mdor the relevant file underdocs/; plugin package, catalog, or release flow changes should updatedocs/plugins/. - Keep
CHANGELOG.mdas the canonical release history. Do not editSources/Resources/ReleaseHistory.jsonby hand; release preparation regenerates it, andpython3 scripts/changelog.py export-historyrepairs it after intentional historical edits. - Icon gallery assets must explicitly declare
renderingMode; usetemplateonly for black artwork on transparency, and useoriginalfor color or grayscale detail. Third-party static assets must pin their upstream revision and catalog mapping indocs/icon-gallery/sources/manifest.json, with the corresponding license underSources/Resources/ThirdPartyNotices/. Run the gallery generation and related tests after catalog changes. - Plugins implement
MacToolsPlugin; menu panel plugins implementPluginPrimaryPanel, and component panel plugins implementPluginComponentPanel. plugin.json.idmust be stable, readable, and exactly match the runtimePluginMetadata.id; each plugin package should return exactly one plugin instance.- Plugin data is preserved by default on uninstall. A plugin that stores sensitive payloads and must crypto-shred them should declare
uninstallDataPolicy: removePrivateData, usePluginPrivateDataKeychainIdentityfor its encryption key, and leave cleanup to the host; failed cleanup must finish before the same plugin can be reinstalled, and lifecycle changes must test both recovery failures and successful cleanup. - Register newly consumed PluginKit APIs in
scripts/tests/test_plugin_minimum_host_compatibility.py, including optional protocols. The inventory retains API introduction versions for older ABI checks; current PluginKit v6 packages require MacTools 1.3.0 or later. Keeping an older protocol's witness table unchanged does not make new symbols loadable by older hosts. - Plugin display state should be expressed through
PluginPanelState,PluginPanelDetail,PluginPanelControl, and related models. Do not bypass the existing panel framework. - Prefer
PluginSettingsPage.formwith declarative sections and typed controls. Use a custom form section for a complex region andPluginSettingsPage.workspaceonly for task-oriented managers or editors that need the full content area. Permissions, shortcuts, page chrome, search, validation, and backgrounds remain host-owned. - Publish executable plugin capabilities through stable
PluginActionProvidingdefinitions. Keep action discovery and execution in the host-owned registry/executor, and keep ordinary global bindings inShortcutAssignmentService; workflows, Run Links, and Action Grid must reference those actions instead of defining parallel command, shortcut, or URL paths. The architecture and automated test matrix are documented indocs/actions-automation.md. - A plugin that composes an existing provider's canonical action may adopt
PluginActionExecutionHostContextConsuming; do not call the provider implementation or duplicate its system write directly. Use its live lookup, guarded execution, and explicit provider-settings navigation so host availability, safety, confirmation, and diagnostics remain authoritative. Verify against fresh provider snapshots rather than debounced UI catalog state, and keep navigation user-initiated. Mac Settings is the reference implementation indocs/plugins/mac-settings.md. - The action-execution bridge follows the same ABI and minimum-host checks. Include every public bridge type in
scripts/tests/test_plugin_minimum_host_compatibility.pywhen extending it, and keep consuming manifests on the first compatible host release. - Keep composed providers such as Mac Settings in the shared manifest/runtime action snapshot tests. Disclose setting-specific permissions in setup guidance when they are not a requirement for every action.
- Preserve selected policies separately from their current effect: Appearance's
set-modeaction models Auto / Light / Dark, while the quick toggle describes the rendered dark state. Composed settings, profiles, and Undo must use the policy action. Optional System Settings links must use known pane/anchor pairs, not a generic app-opening fallback. - Mac Settings release scope is enforced in its catalog factory. Before restoring a deferred setting, complete its criteria in the release scope and backlog, then update the scope tests and documentation together. Keep deferred definitions available for profile validation without exposing their adapters to execution.
- A plugin may implement
PluginActionExposureProvidingto veto a canonical action on a host-owned system surface such as App Intents. Treat.automaticas delegation to the host's conservative eligibility policy, never as an allowlist override. Unknown surfaces and provider failures fail closed, and the executor rechecks the live policy immediately before starting the action. Run Link policy remains a separate contract. - When a plugin adopts an action-surface or other newly exported PluginKit type, set its
plugin.json.minHostVersionto the first compatible app release. Compatibility metadata belongs in the source manifest. When local plugin validation requires that unreleased host version,MARKETING_VERSIONmay predeclare it; the app release helper treats a source version ahead of the latest app tag as the default release target and remains responsible for advancingCURRENT_PROJECT_VERSION. Release tooling owns plugin package version bumps. - If ordinary plugin resources rarely change, prefer bundling them into the executable. If extra bundle resources are needed, declare the smallest necessary differences in the plugin's own
project.yml. - Custom plugin settings views must reuse
MacToolsPluginKit.PluginSettingsThemeand.pluginSettingsCardBackground(.standard/.recessed). Do not copy private plugin settings styles, and do not make plugins depend onSources/App/SettingsStyle.swift. - Call
onStateChange?()after plugin state changes. Long-running scans, file system work, and system calls should not block the main thread for extended periods. - User-facing copy is primarily Chinese. Keep it concise, clear, and close to native macOS wording.
- Localize user-facing copy with
.xcstrings. App/Core copy belongs underSources/Resources/Localization, PluginKit copy underSources/MacToolsPluginKit/Resources, and plugin copy underPlugins/<PluginName>/Resources. Pluginplugin.jsonfiles should keepdisplayName/summaryas fallbacks and addlocalizedMetadatafor marketplace and unloaded-plugin presentation. Pre-install product, capability, privacy, setup, and relationship metadata belongs in the sameplugin.json; followdocs/plugins/plugin-manifest.schema.json. Declare localized product copy once under the source-onlyproductStringstable, using@displayName,@summary,@localizable.<key>,@standardAction.<key>,@standardSetup.requirements.<key>, or all 11 locale values, and make every localized product field reference@productStrings.<key>. Keep referenced screenshots underMarketplaceAssets/, and never add a parallel marketplace manifest or machine-local dynamic action entries. - Keep current
plugin.jsonruntime envelopes complete. Generated package manifests must contain expanded localization values and match their source metadata; do not edit package copies independently. Legacy manifests must still include runtime-decodablecapabilitiesandpermissions; omitting newer product fields is supported only for PluginKit versions below 5 through the explicit local-debug compatibility flag and must never be used for release catalog generation. - When merging new plugins with product-metadata changes, register their factories in
PluginRuntimeActionSnapshotTestsand preserve independent runtime policies such asuninstallDataPolicyalongside the product fields. - New plugins should provide localization whenever practical, at minimum for panel copy, settings copy, permission text, and plugin metadata.
- Prefer Apple native frameworks. When adding system frameworks, private include paths, or helper executables inside a plugin bundle, declare the smallest necessary differences in the plugin's own
project.yml. Bundle resource executables that need separate signing should be listed inplugin.json.package.signPaths. - System power and session-ending actions must use native macOS confirmation flows, remain foreground-only, and avoid immediate restart or shutdown events that can discard unsaved work.
- Plugins that use private Apple frameworks must load them dynamically at runtime and validate the required classes and selectors. Do not statically link private frameworks, and surface unsupported-system errors instead of crashing.
- Plugins that intercept pointer events must declare Accessibility permission, stop their event tap on deactivation, and re-enable a tap disabled by macOS.
- Plugins that move or resize windows must use public Accessibility APIs for ordinary position and size writes, revalidate the focused window immediately before writing, calculate against the current display visible frame, and keep pure multi-display geometry independently testable. Capabilities unavailable through Accessibility may use a narrowly scoped, dynamically loaded, version-gated private API after review and must fail closed when unsupported.
- Behavioral changes should add or update adjacent XCTest coverage. Test files should be named
<TypeName>Tests.swift. - Full test command:
xcodebuild -project MacTools.xcodeproj -scheme MacTools -configuration Debug -derivedDataPath build/DerivedData test -quiet. - Single test class: append
-only-testing:MacToolsTests/<TestClassName>to the full test command. - File system tests should use temporary directories or fake stores. Disk cleanup tests must not delete real user directories.
- Keep the PR focused, and explain the purpose, verification, and user impact.
- Prefer English for commit messages, pull request titles/descriptions, and issues.
- Build or tests have passed. If they could not be run, explain why in the PR.
- User-visible behavior changes are reflected in
README.mdor the relevant design documentation. - User-visible app or plugin changes include a concise English changelog fragment in
changes/unreleased/*.md. - Plugin manifest
capabilities.settings(none,form, orworkspace) matches the runtimesettingsPagelayout. - Rich manifest static and dynamic action descriptors match the runtime provider/action identity, risk, permissions, external policy, automation eligibility, and parameter portability.
- High-risk features cover safety checks, error states, and missing-permission cases.
- The PR does not include unrelated formatting, generated files, local configuration, certificates, or release credentials.
- New or updated third-party material is recorded in
Sources/Resources/ThirdPartyNotices/manifest.jsonwith an exact upstream revision, affected products, source paths, and retained license text.
Nightly isolation also covers Activity Bar sockets/hook registrations and CLI/broker service identities. Keep the stable identifiers unchanged, keep the hardware listener lock shared, and add cross-channel coexistence tests when introducing another process-global namespace.
- Releases are handled by maintainers. Do not create tags, publish GitHub Releases, or commit release artifacts in ordinary contributions.
- For GitHub-based releases, prefer
Actions->Prepare Release. Entertype, targetversion, and whether torelease; whenreleaseis enabled, the workflow continues to the actual release workflow after bumping, committing, and creating the tag. - For quick releases, prefer
make release. The command interactively choosesapporplugin, analyzes the nextpatch/minor/majorversion, previews the bump, then only after confirmation runsgit pull --rebase, lightweight checks, version updates, commit, tag creation, and tag push. - App releases update
MARKETING_VERSIONandCURRENT_PROJECT_VERSIONinConfigs/AppVersion.xcconfig. The app and embedded extensions inherit this shared version config. After pushing av*.*.*tag, theReleaseworkflow builds, signs, notarizes, uploads the DMG, marks the stable App release as GitHub Latest, and updates the Appcast plus website download metadata. - App signing must sign the embedded
MacToolsCLIBrokerexecutable before the outer app. For an inline workflow-only fix, retry fromActions->Release->Run workflowonmainwith the existing tag; see App release recovery. Re-running an old job does not pick up workflow fixes. - Public Nightly releases are maintainer-operated snapshots of
main. They use a separate app identity, storage scope, privileged helper paths/signing identifiers, Keychain services, update feed, and complete same-commit plugin catalog. The optional Apple silicon CLI is signed, notarized, and published separately from the app with the same version and build; the release verifier requires the exact Nightly CLI identifier, signing team,arm64architecture, system-only dynamic dependencies, executable mode, archive contents including the unchanged root GPL license, checksum, andversion --jsonmetadata. Nightly plugin versions are generated assource-major.run.attempt; feature contributions must not bump source manifests for Nightly. After two successful manual update runs, maintainers may enable the daily schedule withENABLE_NIGHTLY_RELEASES=true. The build job skips unchanged scheduled runs by comparing against the deployed appcast's source, excluding generateddocs/nightly/**; manual runs always publish. Publishable runs then pass one immutable artifact through a credential-free CLI verification job before the separate publication job receives repository write access. Partial retries reuse the producing build's artifact ID and version metadata; see Nightly recovery for when a full rebuild is required. Rollback refs must be ancestors ofmainand support the workflow's current Nightly release interface and CLI testing guide. Missing or indeterminate previous metadata does not prevent publication. Failed draft releases are removed by the creating run, and the next successful run also removes abandoned drafts in the workflow-ownednightly-<run>-<attempt>namespace. Preserve existing stable names when adding channel-specific storage or helpers. - Plugin releases push a
plugins-*batch tag. The defaultautomode uses the production catalog to find new plugins, already bumped plugins, and package-related plugin changes; it updatesplugin.json.versionwhen needed, then thePlugin Releaseworkflow builds plugins and merges the signed catalog. Plugin batch releases are never marked as GitHub Latest. Catalog signing uses the same Foundation canonicalization as runtime verification, and the workflow validates the configured Ed25519 key pair before building. When a batch raises individual plugins'minHostVersion, publish and verify the signed plugin catalog before releasing that host version; the catalog keeps its oldest schema-compatible host floor and older apps leave newer entries unavailable. - On first launch, a new app version checks installed plugins and automatically updates them from the signed production catalog. It does not normally install plugins the user has not installed. The only exception is a host-declared feature-extraction migration: when an installed source plugin still owns a legacy preference, the host may runtime-validate the replacement package and update the source as one rollback-protected operation. Manually installing that replacement also coordinates retirement of an older source package, even before the legacy preference has been written.
- Non-interactive examples:
make release ARGS="--type app --version 1.0.7 --yes"ormake release ARGS="--type plugin --version 1.0.10 --plugin-mode selected --plugin calendar --yes". - Add
--dry-runto preview the steps. The working tree must be clean before a real release. - Before local release builds, copy
scripts/release.local.env.sampletoscripts/release.local.envand fill in at leastDEVELOPER_ID_APPLICATION. - If Apple notarization is needed, store credentials first with
xcrun notarytool store-credentials. - Version numbers default to
MARKETING_VERSIONandCURRENT_PROJECT_VERSIONinConfigs/AppVersion.xcconfig. - Local production builds can still use the lower-level script:
./scripts/release-local.sh; before publishing to GitHub Releases, rungh auth login, then./scripts/release-local.sh --publish. - Plugin library releases are triggered by
plugins-*batch tags through thePlugin Releaseworkflow. Within one PluginKit ABI and catalog-schema compatibility line, plugins with bumped versions are built and uploaded, then merged into that line's catalog. Changes underSources/MacToolsPluginKit/require rebuilding and bumping every plugin so the catalog cannot retain binaries linked against an older shared framework. The standardmake releaseflow performs these manifest bumps in the release commit; feature PRs should not pre-bump unrelated plugins. The first release of a new ABI or schema line also rebuilds every plugin and writes a separate catalog. MacTools through 1.1.6 keeps reading the immutable PluginKit v4 catalog atdocs/plugins/v4/catalog.json; MacTools 1.2.0 keeps reading PluginKit v5 schema 2 atdocs/plugins/v5/catalog.json; MacTools 1.3.0 and later use PluginKit v6/schema 3 atdocs/plugins/v6/catalog.json. Publish the compatible plugin batch and catalog first, wait for Pages to serve the committed signed catalog, and only then prepare or publish the corresponding app. The app release helper and final release workflow fail closed unless that deployed catalog exactly matches the committed catalog and has a valid signature. The catalog private key, Developer ID certificate, and GitHub token must come from CI secrets or local environment variables. - GitHub Actions build and release configuration is documented in
docs/github-actions.md; plugin catalog, package structure, and batch release flows are documented indocs/plugins/plugin-catalog.md.
For the PluginKit v6 migration, source manifests declare pluginKitVersion: 6 and minHostVersion: "1.3.0". Leave plugin package versions, Configs/AppVersion.xcconfig, signed catalogs, and compiled release notes to make release; do not pre-bump them in the ABI migration change. Run make release for plugins first (auto selects all plugins), wait for the v6 catalog commit and Pages deployment, then run the app release. CI and make ci check the frozen v6 client, including settings row, option, and control layouts.