diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..6e24d0d
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,40 @@
+name: Documentation validation
+on:
+ pull_request:
+ paths:
+ - README.md
+ - CHANGELOG.md
+ - LICENSE
+ - THIRD-PARTY-NOTICES.txt
+ - docs/*.md
+ - docs/user-guide/**
+ - docs/development/**
+ - docs/architecture/**
+ - docs/images/**
+ - assets/icon.png
+ - update-service/catalog/README.md
+ - scripts/documentation/**
+ - scripts/testing/test_wiki_documentation.py
+ - .github/workflows/docs.yml
+ workflow_dispatch:
+permissions:
+ contents: read
+jobs:
+ wiki-preview:
+ name: Wiki preview (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@v7
+ with:
+ python-version: '3.12'
+ - name: Test local wiki generation and reconciliation
+ run: python scripts/testing/test_wiki_documentation.py
+ - name: Validate generated wiki preview
+ run: python scripts/documentation/wiki.py check --source-commit ${{ github.sha }}
diff --git a/README.md b/README.md
index 8d89229..e2d819c 100644
--- a/README.md
+++ b/README.md
@@ -2,20 +2,20 @@
-Install, update, launch, and build the Craft apps from Storytold in one place. Choose installation options for each app, keep multiple copies, and follow your downloads and builds in Activity.
-
-Version **1.0.0** includes a configurable signed updater, compatibility updates with fewer prompts, and improved macOS packaging. See the [changelog](CHANGELOG.md) for all changes.
+Install, update, launch, and build Storytold Craft apps in one place. Manage multiple installations, follow progress in Activity, and add supported desktop apps from GitHub.
Available for **Windows, Linux, and macOS**. This is an independent community project, not an official Storytold or Adobe application.
-**[Download the latest release](https://github.com/CryptoKey98/craft-apps-manager/releases/latest)** · [What's changed](CHANGELOG.md) · [Report a problem](https://github.com/CryptoKey98/craft-apps-manager/issues)
+**[Download](https://github.com/CryptoKey98/craft-apps-manager/releases/latest)** · [Documentation](docs/README.md) · [Changelog](CHANGELOG.md) · [Report a problem](https://github.com/CryptoKey98/craft-apps-manager/issues)
+
+Version **1.0.0** includes signed manager updates, compatibility updates with fewer prompts, and Linux TAR.GZ and macOS DMG downloads. See the [changelog](CHANGELOG.md) for the full release notes.

More screenshots
-Screenshots show the Windows 0.8.0 interface with a demo library, captured at 2× resolution. Some controls have changed since then; these images do not show every 0.8.2 feature.
+Screenshots show the Windows 0.8.0 interface with a demo library, captured at 2× resolution. Some controls have changed since then; the images do not show every current feature.
**Home** — launch installed apps and review updates.
@@ -45,180 +45,69 @@ Screenshots show the Windows 0.8.0 interface with a demo library, captured at 2
## Get started
-1. Download the package for your operating system from [Releases](https://github.com/CryptoKey98/craft-apps-manager/releases/latest).
-2. Install the package, or extract the portable ZIP into its own folder.
-3. Open Craft Apps Manager, choose an app, and click **Get**. Review its version, format, architecture, and destination before clicking **Install**.
+1. Download the package for your system from [Releases](https://github.com/CryptoKey98/craft-apps-manager/releases/latest).
+2. Install it, or extract the entire portable archive into its own folder.
+3. Open the manager, choose an app, and click **Get**. Review its version, format, architecture, and destination, then click **Install**.
-| Your system | Installer | Portable download |
+| Your system | Standard installation | Portable download |
| --- | --- | --- |
| Windows 64-bit / 32-bit | x64 / x86 MSI | x64 / x86 ZIP |
-| Ubuntu or Debian 64-bit / 32-bit | amd64 / i386 DEB | x64 / x86 ZIP |
-| Fedora or RPM-based Linux 64-bit / 32-bit | x86_64 / i686 RPM | x64 / x86 ZIP |
-| Arch Linux 64-bit (experimental) | x86_64 `.pkg.tar.zst` | x64 ZIP |
-| macOS 11+, Intel or Apple silicon | — | Universal ZIP |
-
-**Windows:** run the MSI, or open `CraftApps-Manager.exe` in the extracted ZIP. Keep the ZIP's bundled `workspace/tools/7zip` folder. Rust is needed only for building apps, not for running the manager.
-
-**Linux:** install the package matching your distribution, or extract the ZIP and run `craft-apps-manager`. Native package operations may ask for your administrator password. See [Linux setup](docs/user-guide/linux.md) for commands and dependencies.
-
-**macOS:** keep the extracted portable folder together, including `portable.txt` beside the `.app` bundle. The manager is not Apple-notarized; if macOS blocks its first launch, use **System Settings → Privacy & Security → Open Anyway**. See [macOS setup](docs/user-guide/macos.md) for the full instructions and the optional Terminal installer.
-
-## Everyday use
-
-### Install and manage apps
-
-- **Install options belong to each app.** Choose a version, release format, and architecture in its installation window. Only compatible combinations are offered; there are no global format or architecture controls in Settings.
-- **Open, update, or uninstall** from the app page. Launch options let you select an executable and add arguments.
-- **Keep multiple installations.** After the first install, use the **Installation** dropdown to select the copy you want to manage. **Add another installation…** opens the usual install window. Portable copies need separate folders; one native installer copy is supported. Apps may share their own user settings between copies.
-- **Identify portable copies by letter.** The installation selectors and desktop shortcuts use matching letters. Removing B leaves A and C unchanged; the next new copy uses B. Home's compact installation switcher shares the app page's selection.
-- **Move a copy** with **Move…** where supported: managed portable apps and supported Windows MSI installations. Linux native packages and macOS installer copies keep their system-managed locations.
-- **Hide apps** from the sidebar or Home context menu. Restore them in **Settings → Apps → App visibility**. Hiding does not uninstall an app or change its update selection.
-
-Updates and version changes use the selected installation's verified format and architecture. Unknown installation details are reported rather than guessed. Changing an existing copy's format or architecture requires removing that copy first, or adding a separate supported installation.
-
-### Updates and Activity
-
-**Check for updates** checks one app. **Update All** on Home updates the selected installed apps after a review; it never installs missing apps. Newly installed apps are selected automatically. With multiple copies, updates target the selected copy of each app.
-
-In **Settings → Updates**, configure startup checks, periodic checks while the manager is running, or hourly scheduled checks. Checks do not automatically download or install updates. The manager's own update check is separate and displays **Update available** in the header when a matching release is available.
-
-**Activity**, beside Settings, shows progress, cancellation, and results. Manager and Builder share operation records. Closing a dialog does not cancel its operation; use its Cancel button. Native package transactions may need to finish before cancellation can be confirmed.
-
-To hide the Activity button's notification badge, enable **Hide activity badge** in **Settings → General**. Unread activity highlights still appear inside the panel.
-
-**Approve administrator access once per batch** is enabled by default for eligible Windows and Linux installer batches. Linux uses PolicyKit with apt, dnf, or pacman. Individual installers can still show their own prompts. macOS uses its existing authorization workflow.
-
-Platform-specific manager hotfixes use build revisions. Only affected platforms are offered an update; About shows `Rev. 2` or higher when installed.
-
-**What's New** appears on first launch and after a version change. Reopen it with the info icon beside the manager version.
-
-### Installer cleanup
-
-Starting with **0.8.1**, downloaded setup files are automatically removed after a successful, verified installation or update. Cleanup appears in the operation's progress and Activity details.
-
-Older setup files left by previous versions are not automatically swept up. Recovery packages or files required for installer maintenance may be retained. Failed or cancelled installations do not run successful-install cleanup; deletion problems are reported without marking a verified installation as failed.
-
-Version 0.8.2 also removes an app's empty setup-download folder after cleanup. Additional portable library copies use flat folders such as `filmcraft-2`; uninstalling removes empty legacy wrapper folders while preserving user-selected parents and folders containing other files.
-
-### Run in the background
-
-Enable **Keep running in system tray when closed** in General settings to hide the window while checks continue. The tray offers **Open**, **Check for app updates**, and **Exit**.
-
-This needs a usable desktop tray. On Linux, some GNOME desktops require a compatible tray extension. If there is no tray host, closing quits normally so the manager cannot become inaccessible.
-
-Linux close-to-tray also needs X11 or XWayland. The manager prefers a reachable XWayland display when available. On native Wayland, the setting is disabled and closing quits normally.
-
-## Custom GitHub apps
-
-Paste a repository link into **Settings → Apps → Custom apps**. The setup dialog lets you review the detected download, package type, and architecture, and choose a download when several are possible.
+| Ubuntu or Debian 64-bit / 32-bit | amd64 / i386 DEB | x64 / x86 ZIP or TAR.GZ |
+| Fedora or RPM-based Linux 64-bit / 32-bit | x86_64 / i686 RPM | x64 / x86 ZIP or TAR.GZ |
+| Arch Linux 64-bit (experimental) | x86_64 `.pkg.tar.zst` | x64 ZIP or TAR.GZ |
+| macOS 11+, Intel or Apple silicon | Universal DMG | Universal ZIP |
-Adding a repository adds it to the list; it does not install the app. Removing it from the list does not uninstall it. A repository needs a compatible downloadable release for your system.
+**Windows:** run the MSI, or open `CraftApps-Manager.exe` from the extracted ZIP. Keep its bundled `workspace/tools/7zip` folder. Rust is not required to run the manager.
-The manager remembers verified package identities and your selected executable, even when their names differ from the GitHub repository. If a package contains several possible apps, choose the one you want to use. Background updates stop with an explanation when a new choice is required.
+**Linux:** install the package matching your distribution, or extract the portable archive and run `craft-apps-manager`. See [Linux setup](docs/user-guide/linux.md) for commands and desktop requirements.
-If a Windows installer leaves an existing program unchanged, you can explicitly select its verified registration. Supported Linux desktop launchers retain their arguments and per-app environment; arbitrary shell commands are not evaluated.
+**macOS:** open the DMG and drag the app into Applications, or keep the extracted ZIP folder together with `portable.txt` beside the app. The manager is not notarized; macOS may require **System Settings → Privacy & Security → Open Anyway**. See [macOS setup](docs/user-guide/macos.md).
-Linux native packages need a package-owned desktop launcher. Unsupported installer types or unusual launch wrappers may still need manual installation.
+## What you can do
-**Custom-app support is experimental.** Asset naming and installer registration vary, so some apps may not install, be detected, or uninstall reliably. Third-party apps are not included by default.
+- **Manage apps:** install, launch, update, downgrade, move, or uninstall supported apps.
+- **Keep multiple copies:** select which installation the app's actions apply to.
+- **Review updates:** Update All works on selected installed apps. Automatic checks notify you; they do not automatically install updates.
+- **Follow progress:** Manager and Builder share operation records, logs, and cancellation status in Activity.
+- **Build from source:** set up tools, build supported Craft apps, and choose a completed build to launch.
+- **Add GitHub apps:** review compatible downloads in **Settings → Apps → Custom apps**. Support is experimental and varies by package.
-An optional GitHub token can be saved under **Settings → Updates** to raise the API limit. It is stored in the operating system's credential store. Environment variables take priority over the saved token: `CRAFT_GITHUB_TOKEN`, then `GITHUB_TOKEN`. Authentication is sent only to `api.github.com`; don't post tokens in issues or logs.
+New Craft apps can appear after an app-list refresh without updating the manager. Adding or discovering an app does not install it.
-## Craft app discovery
+See [Using the manager](docs/user-guide/using-the-manager.md) for detailed instructions and the [changelog](CHANGELOG.md) for release history.
-The manager includes the main Craft apps, including DesignCraft, EffectCraft, FilmCraft, LightCraft, PhotoCraft, PDFCraft, VectorCraft, WordCraft, GridCraft, DeckCraft, CADCraft, and SoundCraft. ArtCraft has release support; ArtCraft X has source-build support. Available downloads depend on what each app publishes for your platform.
+## Your apps and data
-New Storytold Craft apps can be discovered without updating the manager. Use **Settings → Updates → App list → Refresh**. Startup discovery is enabled by default and uses a shared six-hour cache. Discovery adds apps to the list without installing them or selecting them for updates.
+Portable archives include `portable.txt`; new portable libraries use a `data` folder beside the manager. Move the whole extracted folder together. Existing external libraries stay external.
-Older PrintCraft files remain compatible with PDFCraft's library identity. App discovery and ArtCraft support were contributed by [carpaaaaa](https://github.com/carpaaaaa).
+Installed versions use your operating system's user-data location. The **Data path at the bottom of the window** shows the active library. Back up that folder before making major changes.
-## Portable folders and saved data
+See [Storage and portability](docs/user-guide/using-the-manager.md#storage-and-portability) for default paths, moving a library, and what does not move with it.
-Portable ZIPs include `portable.txt` and use portable storage automatically. A new portable library lives in a `data` folder beside the manager. Existing external libraries are preserved during migration.
+## Help and compatibility
-To move a portable manager, finish active operations, close **both Manager and Builder**, and move the **whole extracted folder**. Keep `portable.txt`, `portable-state.json`, and bundled resources with it. Managed paths inside that folder follow the move; external locations remain unchanged.
+Start with the [documentation index](docs/README.md), [Linux guide](docs/user-guide/linux.md), or [macOS guide](docs/user-guide/macos.md). If something fails, [report an issue](https://github.com/CryptoKey98/craft-apps-manager/issues) with your manager version/revision, operating system, installation type, steps, and exact error. Remove tokens and personal information from anything you share.
-Native installations, system-stored tokens, and desktop shortcuts do not move with the folder. Shortcuts may need recreation, and individual apps may store preferences elsewhere. Moving between operating systems does not convert apps or builds. Don't run an older manager against migrated portable metadata.
-
-| Manager installation | Default library location |
-| --- | --- |
-| Portable ZIP | `data` inside the portable folder; existing external libraries are retained |
-| Windows MSI | `%LOCALAPPDATA%\Craft Apps Manager` |
-| Linux system package | `$XDG_DATA_HOME/craft-apps-manager`, or `~/.local/share/craft-apps-manager` |
-| macOS installation using system storage | `~/Library/Application Support/Craft Apps Manager` |
-
-The **Data path at the bottom of the window** identifies your active library. **Settings → Folders** shows its locations and portable-storage information.
-
-
-What's inside the library?
-
-```text
-releases/ Portable apps and downloaded setup files
-sources/ Source archives and their index
-builds/ Finished builds, grouped by app
-logs/ Update and build logs
-backups/releases/ Portable app backups
-backups/sources/ Source backups
-workspace/ Build tools, extracted sources, and build cache
-runtime/ Staging, operation records, and migration recovery copies
-```
-
-`settings.json` records installations; `manager-settings.json` contains manager preferences. Folder changes that require a restart are shown in Settings.
-
-
-
-## Build apps and manage backups
-
-Use an app's tools to **Set up build tools**, download its source, and **Build** or **Rebuild**. Open the separate Builder through **Settings → Builds → Open builder window**. Build Options lets you choose which completed build the Launch button uses; that choice is shared between Manager and Builder.
-
-Builds use upstream source and lockfiles. Upstream compilation failures are shown in the log. Required tools depend on the app and platform; Windows Rust builds need Microsoft's C++ build tools, and ArtCraft X also needs frontend tools. Failed or cancelled builds keep their cache for a retry.
-
-**Build Options** can delete temporary cache from interrupted builds, even before a successful build exists. Cleanup is unavailable during a build and preserves completed builds and source ZIPs. The next build recreates its cache.
-
-Optional portable-app and source backups are configured in Settings. Use **Backups → Manage…** on an app page to restore or delete a selected backup. Native installer applications are not backed up by the manager. There is no universal automatic rollback for every installer.
-
-Uninstalling can optionally remove listed app profile folders. This is off by default, and some installations share profiles. Review the confirmation before deleting profile data.
-
-## Compatibility and help
-
-Windows x64 and x86 packages have been tested on 64-bit Windows. Linux x64 desktop tests cover Ubuntu 26.04, Fedora 44, and Arch; Arch remains experimental. Linux x86 launch tests used multilib on 64-bit VMs. Native 32-bit systems and Linux ARM64 have not been verified, and macOS desktop testing remains limited.
-
-Published Linux ZIP, DEB, and RPM builds use Ubuntu 22.04 as their baseline; Arch packages are built separately. A build made locally on a newer distribution may require newer system libraries.
-
-For a problem report, include the manager version, operating system, affected app, installation type, and relevant **Activity details** or logs. For detection issues, include `settings.json` from the Data folder. Redact personal paths and never share tokens or passwords.
-
-- [Linux installation and builds](docs/user-guide/linux.md)
-- [macOS installation and builds](docs/user-guide/macos.md)
-- [Windows packaging](docs/development/windows-packaging.md)
-- [Per-app installation behavior](docs/architecture/per-app-installation.md)
-- [Operations and cancellation](docs/architecture/operations.md)
-- [Windows batch approval](docs/architecture/windows-batch-admin.md) · [Linux batch approval](docs/architecture/linux-batch-admin.md)
-
-
-Upgrading from the old Craft Apps Updater
-
-The app was renamed to Craft Apps Manager in 0.3.2. Very old versions may reject the renamed repository or package, so download Manager manually. Close the old Updater and preserve your existing library when switching. Disable old scheduled tasks before configuring Manager's scheduled checks.
-
-
+Available app downloads depend on each upstream project. Linux desktop integration varies by distribution and display server; Arch packages remain experimental. Native 32-bit operating systems and Linux ARM64 have not been verified. Platform guides describe known limitations.
## Build this manager
-Development work on the next signed updater is documented in [updater operations](docs/development/updater-operations.md). Its hosted service remains inactive until separately configured and approved.
-
-The source is shared across Windows, Linux, and macOS. Install stable Rust and your platform's native prerequisites; see the platform guides above.
+Install stable Rust and your platform's prerequisites, then run from the repository root:
```sh
cargo build --release --locked --bin craft-apps-manager
-cargo test --locked
-cargo clippy --locked --all-targets -- -D warnings
```
-Source builds use the existing GitHub updater by default. The optional service updater is a Rust library compiled into the manager, not a separate download. To enable it, select `source = "service"` and a signed catalog URL in `updater.toml`, then build with `--features service-updater`. Enabling the feature alone keeps the GitHub updater. This is publisher configuration, not an extra setup step for people using packaged releases. See [updater configuration](docs/development/updater-operations.md#source-build-choice) for examples and signing requirements.
+**This builds the default GitHub-only updater.** For the official service updater on Windows:
+
+```powershell
+python scripts/lib/updater_config.py build --config updater.production.toml --target-os windows
+```
-On Windows, install Microsoft's **Desktop development with C++** workload. Platform integration lives in `src/platform`, native installers in `src/installations/packages/native`, scheduling in `src/scheduling`, and build tools in `src/builds/toolchains`. Build output is excluded from the source repository. Some integration checks need local tools; see the test instructions in the platform docs.
+The script runs Cargo with the selected configuration and required feature. On Linux or macOS, use `python3` and change `--target-os` to `linux` or `macos`. Builds write to `target/release`; the most recent build determines which updater that executable uses.
-For packaging and version preparation, see [Release preparation](docs/development/releases.md).
+See [Building and testing](docs/development/building.md) for prerequisites, executable locations, verification, and packaging. Maintainers should also read [Release preparation](docs/development/releases.md) and [Updater operations](docs/development/updater-operations.md).
## License
-MIT. Bundled 7-Zip files have separate license notices. Craft apps and their branding belong to their respective authors.
+[MIT](LICENSE). Third-party components and artwork have their own notices in [THIRD-PARTY-NOTICES.txt](THIRD-PARTY-NOTICES.txt) and the relevant asset folders.
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..b13d3dd
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,41 @@
+# Documentation
+
+Choose a guide for what you want to do. Downloaded releases do not require a Rust toolchain.
+
+## Install and use
+
+| Guide | What you will find |
+| --- | --- |
+| [Quick start](../README.md#get-started) | Downloads, package choices, and first launch |
+| [Using the manager](user-guide/using-the-manager.md) | Installations, updates, Activity, custom apps, builds, backups, and storage |
+| [Linux](user-guide/linux.md) | Native packages, portable archives, tray behavior, and required tools |
+| [macOS](user-guide/macos.md) | DMG and ZIP setup, first-launch approval, updates, and recovery |
+| [Changelog](../CHANGELOG.md) | Changes in each release |
+
+## Build and contribute
+
+| Guide | What you will find |
+| --- | --- |
+| [Building and testing](development/building.md) | Prerequisites, updater choices, tests, and local packages |
+| [Windows packaging](development/windows-packaging.md) | MSI layout, WiX, portable packaging, and upgrade identity |
+
+## Publish and maintain
+
+| Guide | What you will find |
+| --- | --- |
+| [Release preparation](development/releases.md) | Version preparation, CI, publication, revisions, and catalog-only recovery |
+| [Updater operations](development/updater-operations.md) | Configuration, signing, hosting, renewal, and recovery rules |
+| [Production catalog inputs](../update-service/catalog/README.md) | Checked-in policy files and production configuration |
+
+## Understand the architecture
+
+- [Per-app installations](architecture/per-app-installation.md): inventory and selection.
+- [Operations](architecture/operations.md): lifecycle records, cancellation, and cross-window observation.
+- [Windows batch approval](architecture/windows-batch-admin.md): elevated installer batches.
+- [Linux batch approval](architecture/linux-batch-admin.md): PolicyKit and native package batches.
+
+Older links directly under `docs/` remain as short redirects to these guides.
+
+## Editing and wiki previews
+
+These files are the maintained source for the planned GitHub Wiki mirror. Make corrections here so the repository and wiki do not become two separate sets of instructions. See [Wiki documentation preview](development/building.md#wiki-documentation-preview) to generate and test the local preview. Public syncing is not enabled yet.
diff --git a/docs/development/building.md b/docs/development/building.md
new file mode 100644
index 0000000..e692f57
--- /dev/null
+++ b/docs/development/building.md
@@ -0,0 +1,172 @@
+# Building and testing
+
+For contributors compiling the manager. To build a Craft app through the UI, see [Using the manager](../user-guide/using-the-manager.md#build-apps-and-manage-backups). Packaged manager releases do not require Rust.
+
+Run the commands below from the repository root. Use stable Rust installed through [rustup](https://rust-lang.org/tools/install/).
+
+## Prerequisites
+
+| Platform | Required for compiling the manager |
+| --- | --- |
+| Windows | Visual Studio Build Tools with **Desktop development with C++** and a Windows SDK |
+| Ubuntu/Debian | C/C++ build tools, pkg-config, OpenSSL and the native GUI development libraries below |
+| Fedora | C/C++ build tools, pkgconf, OpenSSL and the matching X11/Wayland/OpenGL development libraries |
+| Arch | `base-devel`, `pkgconf`, `openssl`, `libx11`, `libxi`, `libxrandr`, `libxcursor`, `libxkbcommon`, `wayland`, `libglvnd` |
+| macOS | Xcode Command Line Tools; both Rust targets below for a universal bundle |
+
+Ubuntu/Debian development dependencies:
+
+```sh
+sudo apt-get update
+sudo apt-get install -y build-essential pkg-config libx11-dev libxi-dev libxrandr-dev libxcursor-dev libxinerama-dev libxkbcommon-dev libwayland-dev libgl1-mesa-dev libssl-dev
+```
+
+These are build-machine dependencies. User installation requirements are in the [Linux](../user-guide/linux.md) and [macOS](../user-guide/macos.md) guides. Upstream Craft apps may require additional tools; the manager's tool setup handles supported app/platform combinations.
+
+## Choose the updater
+
+The updater choice is compiled into the executable. There is no end-user setting to switch it.
+
+### Default GitHub-only build
+
+With the checked-in `updater.toml` and no updater environment overrides:
+
+```sh
+cargo build --release --locked --bin craft-apps-manager
+```
+
+This keeps the original GitHub release discovery and update path. Enabling `--features service-updater` alone does not activate the service.
+
+### Official service-updater build
+
+On Windows:
+
+```powershell
+python scripts/lib/updater_config.py build --config updater.production.toml --target-os windows
+python scripts/lib/updater_config.py verify --config updater.production.toml --target-os windows --binary target/release/craft-apps-manager.exe
+```
+
+On Linux:
+
+```sh
+python3 scripts/lib/updater_config.py build --config updater.production.toml --target-os linux
+python3 scripts/lib/updater_config.py verify --config updater.production.toml --target-os linux --binary target/release/craft-apps-manager
+```
+
+On macOS, use the Linux commands with `--target-os macos`. A bare executable is useful for development; use the bundle packaging instructions below to test macOS self-updates.
+
+The helper runs Cargo with the selected TOML and required feature. Verification checks the compiled version, revision, updater source, repository, channel, public-key fingerprint and runtime configuration fingerprint. No private signing key is needed to build a client.
+
+Both build choices normally write the **same output file**:
+
+| Platform | Executable |
+| --- | --- |
+| Windows | `target/release/craft-apps-manager.exe` |
+| Linux/macOS | `target/release/craft-apps-manager` |
+
+The last build replaces the earlier build choice. Explicit `--target` builds go under `target//release`; `CARGO_TARGET_DIR` can change the base directory. Pass the same configuration each time you want a service build. Forks should supply their own TOML, repository and trust configuration; see [Updater configuration](updater-operations.md#source-build-choice).
+
+Launch the executable with `--builder` for the separate Builder window. For an isolated library, add `--root /absolute/path/to/test-library` (use a Windows absolute path on Windows).
+
+## Tests and static checks
+
+Close Manager and Builder before tests that acquire the shared operation lock. Run tests serially:
+
+```sh
+cargo fmt --check
+cargo test --locked --all-targets -- --test-threads=1
+cargo clippy --locked --all-targets -- -D warnings
+```
+
+For changes affecting the service updater, also run:
+
+```sh
+cargo test --locked --all-targets --features service-updater -- --test-threads=1
+cargo clippy --locked --all-targets --features service-updater -- -D warnings
+```
+
+Some tests require separately selected fixtures or tools and are intentionally ignored. Compilation and unit tests do not prove GUI behavior or actual installer transactions on another platform. See [Updater checks](updater-operations.md#local-checks) for protocol and Worker tests. Do not run native installer smoke tests against a real installation without a disposable test setup.
+
+## Package a local build
+
+Keep the same updater configuration for compilation and packaging; packaging rejects a mismatched executable fingerprint. For example, set `CRAFT_UPDATER_CONFIG=updater.production.toml` in the packaging process's environment when packaging the official service build. On PowerShell use `$env:CRAFT_UPDATER_CONFIG = 'updater.production.toml'`.
+
+Scripts refuse to overwrite existing portable archives. Use a new output directory or intentionally remove only disposable package output, never a library or an installed app.
+
+### Windows
+
+Build for `x86_64-pc-windows-msvc` or `i686-pc-windows-msvc` before running packaging scripts. For example:
+
+```powershell
+python scripts/lib/updater_config.py build --config updater.production.toml --target-os windows --target x86_64-pc-windows-msvc
+```
+
+Portable and MSI packaging require a matching 7-Zip SDK folder with its license files. MSI packaging also needs WiX. See [Windows packaging](windows-packaging.md) for commands and prerequisites.
+
+### Linux
+
+Build a native executable first. The portable scripts create a ZIP and then reuse its exact payload for TAR.GZ:
+
+```sh
+sh scripts/packaging/package-linux-zip.sh target/release/craft-apps-manager
+sh scripts/packaging/package-linux-tar.sh dist/linux/Craft-Apps-Manager-1.0.0-linux-x64.zip
+```
+
+Replace the archive's version and architecture with the actual output. TAR.GZ packaging currently requires ZIP in the selected package configuration.
+
+| Format | Script | Additional tools |
+| --- | --- | --- |
+| DEB | `scripts/packaging/package-linux-deb.sh` | `dpkg-dev` on Ubuntu/Debian |
+| RPM | `scripts/packaging/package-linux-rpm.sh` | `rpmbuild`, supplied by `rpm-build` on Fedora |
+| Arch | `scripts/packaging/package-linux-arch.sh` | Arch build environment; run as a regular user |
+
+Each native script accepts the executable path as its first argument and writes to `dist/linux` by default. Architectures are detected from the binary. DEB/RPM dependencies are derived from linked libraries; a local build on a newer distribution may require newer libraries than the published build.
+
+Published ZIP/TAR.GZ/DEB/RPM builds use Ubuntu 22.04; Arch packages are built separately on Arch. Linux x86 launch testing has used multilib on 64-bit Ubuntu/Fedora VMs, not a native 32-bit operating system. Linux ARM64 has not been qualified.
+
+To cross-build x86 on Ubuntu x64, enable the `i386` package architecture and install `gcc-multilib`, `libc6-dev-i386`, and matching GUI development libraries with `:i386`. Add `i686-unknown-linux-gnu` with rustup. Use `PKG_CONFIG_ALLOW_CROSS=1` and `PKG_CONFIG_LIBDIR=/usr/lib/i386-linux-gnu/pkgconfig:/usr/share/pkgconfig` for that target build. This does not create x86 releases for upstream apps that only publish x64.
+
+### macOS
+
+Build and package on macOS:
+
+```sh
+rustup target add aarch64-apple-darwin x86_64-apple-darwin
+./scripts/packaging/package-macos.sh
+sh scripts/packaging/package-macos-dmg.sh 'dist/macos/Craft Apps Manager.app' dist/macos/Craft-Apps-Manager-1.0.0-macos-universal.dmg
+```
+
+The first script compiles both architectures, combines them into a universal bundle, verifies both embedded updater identities, applies an ad-hoc signature, and creates a ZIP with `portable.txt`. The DMG script uses that same bundle in a drag-to-Applications layout. Adjust the example version to match Cargo.toml.
+
+DMG packaging needs Python 3.9+ with `venv` and access to PyPI. Pinned `dmgbuild` dependencies are installed into a temporary environment and removed afterward. These are packaging requirements, not prerequisites for running the app. Bundles are not Apple-notarized; see [First launch](../user-guide/macos.md#first-launch).
+
+## Wiki documentation preview
+
+Wiki pages are generated from the existing Markdown; edit the source guides, not the generated pages. Python 3.10+ and Git are sufficient. No extra Python packages, Rust build, or wiki credentials are needed for local checks.
+
+From the repository root on PowerShell:
+
+```powershell
+$sourceCommit = git rev-parse HEAD
+python scripts/testing/test_wiki_documentation.py
+python scripts/documentation/wiki.py check --source-commit $sourceCommit
+```
+
+On Linux/macOS:
+
+```sh
+python3 scripts/testing/test_wiki_documentation.py
+python3 scripts/documentation/wiki.py check --source-commit "$(git rev-parse HEAD)"
+```
+
+The preview is written to `target/wiki-preview/`. Open `Home.md` or `_Sidebar.md` to review the content and navigation. These are generated Markdown files, not a local copy of GitHub's wiki renderer. Image and page URLs target their eventual wiki locations; those links will not serve the unpublished preview. Source links point to the selected commit, so uncommitted edits are only visible in the local preview until committed.
+
+`scripts/documentation/wiki-manifest.json` lists the public pages and images to include. Add a new guide there deliberately. Unlisted files, credentials, and test output are excluded. Existing source files remain unchanged by generation.
+
+The validation workflow and local Git tests cannot publish. Automatic public syncing and credentials are a separate activation step. Local reconciliation preserves unrelated pages and refuses to overwrite directly edited generated files; move any intended correction into the source docs before resolving a conflict.
+
+## Next steps
+
+- [Release preparation](releases.md): validate version metadata and publish through GitHub Actions.
+- [Updater operations](updater-operations.md): configure a fork, signing, hosting, and update routes.
+- [Architecture](../README.md#understand-the-architecture): installation and operation design references.
diff --git a/docs/development/releases.md b/docs/development/releases.md
index 6907d4a..2bbe355 100644
--- a/docs/development/releases.md
+++ b/docs/development/releases.md
@@ -1,27 +1,97 @@
# Release preparation
-Cargo.toml is the version source for the application and packaging scripts. Do not edit version strings in Rust code or package scripts.
+For maintainers publishing the manager. Building a local executable is separate; see [Building and testing](building.md). Local validation does not publish anything.
-From the source directory:
+## Prepare the version and notes
+
+`Cargo.toml` is the application version source. The release tool coordinates its version with Cargo.lock, the README caption, and the changelog.
+
+From the repository root, replace the example version with the version you are preparing:
```sh
+cargo run --locked --features release-tools --bin release-tool -- bump 1.0.0 --dry-run
cargo run --locked --features release-tools --bin release-tool -- bump 1.0.0
```
-Add `--dry-run` to validate without writing. The tool updates the package version, its Cargo.lock entry, the README introduction, and a new changelog section. Dependency versions and older changelog entries stay intact. Replace the new changelog placeholder with the changes users will receive.
+Run the bump only for a version change, not to republish an existing version. Replace the new changelog placeholder with concise user-facing changes. Update What's New and review package/platform scope. Preserve older changelog entries.
-Validate the result:
+Validate before opening a PR:
```sh
cargo run --locked --features release-tools --bin release-tool -- check
-cargo fmt --check
cargo test --locked --features release-tools --bin release-tool
+python scripts/testing/test_validate_release.py
```
-CI checks these references on pull requests. The tag-triggered release workflow validates them before building packages and requires nonempty changelog notes. Stable three-part versions must fit Windows MSI limits: major and minor up to 255, patch up to 65535. Start with a clean working tree so the changes are easy to review.
+Run the relevant [application checks](building.md#tests-and-static-checks) too. Use `python3` on Linux/macOS. Stable versions have three numeric parts and must fit MSI limits: major/minor up to 255, patch up to 65535.
+
+Merge the reviewed changes only after required CI passes. Record the exact main commit that will be built.
+
+## Prepare service update policy
+
+For a service-enabled release, update the candidate policy and client profiles referenced by `updater.production.toml` before running Release. The workflow does not invent release routes or new package handlers. Candidate entries must describe the intended release, platform, architecture, package, layout, and any required bridge route.
+
+Release-mode catalog publication replaces new candidate entries' draft/build hashes with the actual published asset size and SHA-256 digest from GitHub's authenticated inventory. It independently downloads and hashes those packages before signing. Existing signed package identities are not rewritten. See [Updater operations](updater-operations.md#publishing-and-renewal) for trust, renewal, and retention requirements.
+
+## Publish through GitHub Actions
+
+Normal releases **publish automatically** after their package jobs succeed. You do not need to create a draft first.
+
+For a manual run, open **Actions → Release → Run workflow** and choose:
+
+| Input | Full-release value |
+| --- | --- |
+| Branch | `main` |
+| `tag` | `v` followed by the version, for example `v1.0.0` |
+| `revision` | `1` |
+| `platform` | `all` |
+| `publish_baseline` | Enabled (the default) |
+| `prepare_draft` | Disabled (the default) |
+| `source_ref` | The full 40-character SHA of the merged main commit |
+
+The workflow builds from that exact source commit. It validates notes and configuration, builds the selected packages, creates checksums and signed v1 metadata, and publishes the release targeting that commit. Tag-triggered runs also publish normally; a manual run does not need a pre-created Git tag. Starting publication should be an intentional maintainer action.
+
+With service publication enabled, the release then calls **Renew signed manager update catalog** in `release` mode. That verifies the published packages, signs the next catalog sequence, deploys the Worker, updates the stable GitHub mirror, and compares the public responses. A green package job alone does not prove this last stage succeeded.
+
+The workflow refuses to overwrite a published baseline. Do not rerun a successful full release just to repair its catalog, and do not replace published package bytes referenced by signed policy.
+
+### Optional preview
+
+Only when you intentionally want an unpublished preview, select `revision=1`, `platform=all`, `publish_baseline=false`, and `prepare_draft=true`. This creates a draft automatically if absent and cannot deploy update offers. Preflight still checks source configuration, trust, and profiles; final-package verification happens after packages exist. Existing immutable package names cannot be overwritten with different bytes.
+
+## Publish a platform revision
+
+A revision is a platform-specific rebuild of the same application version. Revision 1 is hidden in the display; 2 and later appear as `Rev. N`.
+
+Use the Release workflow with the existing version tag, an advancing revision, the affected platform, and the exact source commit. Keep `prepare_draft` off. The baseline release must already be public. Native package versions and the candidate catalog must describe the new build.
+
+Service-enabled revisions use distinct `-Rev-N` package names. Both signed v1 and v2 metadata can reference them; baseline URLs stay unchanged. Older manifest-aware clients can select the signed revision asset. Pre-manifest clients can first reach the compatible baseline, then update through the newer updater. GitHub-only publishing retains its separate compatibility-refresh behavior.
+
+Retain compatible downloads while older clients still need them. A signed catalog can change filenames or choose another already-supported format for service clients; it cannot teach an old binary a new handler. Keep any package required by a supported bridge route.
+
+## Recover a catalog-only failure
+
+If package publication succeeded but catalog signing or deployment failed:
+
+1. Inspect the failed step and verify that the release is already public.
+2. Correct the policy or publisher issue; keep existing package files intact.
+3. Merge the tested fix, then open **Actions → Renew signed manager update catalog → Run workflow**.
+4. Choose `main`, `mode=release`, and leave `force_renew` off.
+5. Check both signing and deployment, plus the public Worker and mirror.
+
+A failed-job rerun uses the original run's source and workflow. If the fix is in a newer commit, start a new catalog workflow from that corrected source instead. This does not rebuild packages or create another application release.
+
+For ordinary metadata expiry renewal without changing offers, use `mode=renew`. Scheduled renewal already runs daily; see [Renewal](updater-operations.md#publishing-and-renewal).
+
+## Confirm publication
-The release workflow can finish an existing draft for the matching tag, replacing its matching package assets and notes before publishing. It refuses to overwrite an already-published release; use a new version for that.
+- The intended release is public and marked latest where appropriate.
+- Expected packages, `SHA256SUMS.txt`, and `updates-v1.json` exist; baseline packages have no duplicate `update` copies.
+- Legacy metadata and the v2 catalog verify with the pinned public key.
+- Public package bodies match signed sizes and hashes.
+- Worker and stable mirror return identical valid catalog bytes; supported client profiles get the intended current/direct/bridge result.
+- Actual update transactions are qualified on the affected platforms when behavior changes. Metadata checks alone are not installer tests.
-Commit on a branch and open a pull request. After checks pass and the PR is merged, build packages from that commit, verify their versions and checksums, then create the matching vVERSION tag and GitHub release. Publishing requires separate approval. This tool never pushes, tags, uploads, or publishes.
+The release tool itself never commits, pushes, tags, uploads, or publishes.
-The release-tools feature is only for development; normal application and package builds exclude the tool.
+[Documentation index](../README.md)
diff --git a/docs/development/updater-operations.md b/docs/development/updater-operations.md
index 95225ee..35655fd 100644
--- a/docs/development/updater-operations.md
+++ b/docs/development/updater-operations.md
@@ -1,6 +1,15 @@
# Signed manager updater operations
-This describes the updater prepared for 1.0.0. It is not active in the published 0.9.0 manager. Production hosting uses the separate publisher configuration described below; these instructions do not authorize a release.
+For maintainers configuring, testing, and operating signed manager updates. Official 1.0.0 builds use the service updater; ordinary source builds default to the GitHub-only updater. Build choices are compiled in, not an end-user setting.
+
+| Task | Start here |
+| --- | --- |
+| Build a local executable | [Building and testing](building.md#choose-the-updater) |
+| Configure a source build or fork | [Source build choice](#source-build-choice) |
+| Publish a release or recover its catalog | [Release preparation](releases.md) |
+| Understand signing and hosting | [Client and hosting boundaries](#client-and-hosting-boundaries) |
+| Operate renewal | [Publishing and renewal](#publishing-and-renewal) |
+| Diagnose a failure | [Failure, recovery and retention](#failure-recovery-and-retention) |
## Client and hosting boundaries
@@ -65,23 +74,9 @@ Before release activation, publish the real release packages, verify their remot
This is for people compiling the manager. Packaged releases use their publisher's build choice; there is no additional user-facing settings control. With the checked-in `source = "github"` configuration and no source override, enabling `--features service-updater` alone keeps the existing GitHub updater. Endpoint values alone do not activate the service.
-To opt into the service on PowerShell (replace the example address with the publisher's real catalog address):
-
-```powershell
-$env:CRAFT_UPDATE_SOURCE = 'service'
-$env:CRAFT_UPDATE_CATALOG_URL = 'https://updates.example.com/v2/catalog.json'
-cargo build --release --locked --features service-updater --bin craft-apps-manager
-```
-
-On Linux/macOS:
+The exact GitHub-only, production service, and identity-verification commands are in [Building and testing](building.md#choose-the-updater). The build helper selects the TOML and required Cargo feature together. No private signing key is needed to compile a client.
-```sh
-CRAFT_UPDATE_SOURCE=service \
-CRAFT_UPDATE_CATALOG_URL=https://updates.example.com/v2/catalog.json \
-cargo build --release --locked --features service-updater --bin craft-apps-manager
-```
-
-To switch back, rebuild with `CRAFT_UPDATE_SOURCE=github`; endpoints do not need to be removed. This changes the newly compiled binary, not an already-running manager. No API token or private signing key is needed by the client. The catalog must match the binary's pinned public key, repository and channel. The isolated test Worker uses a different public test key and must not be used by ordinary production-key builds.
+For a fork, select your own TOML with `--config`; configure its repository, channel, pinned public key, and signed catalog URL. Supported environment overrides can also select these values at compile time. An empty endpoint override clears the file value. Do not point production-key clients at the isolated test Worker.
The choice is compiled into each binary. Replacing a custom build with an official update adopts the official build's updater choice. To retain a custom choice across releases, rebuild subsequent versions with the same option, or publish matching packages in a fork with `CRAFT_MANAGER_REPOSITORY` set to that fork. GitHub-only packages still require the original compatible release assets; v2-only transport/name migrations apply to service-enabled builds.
@@ -107,19 +102,16 @@ unknown fields, invalid paths, unsupported formats and missing service settings
fail. Public-key/profile/catalog paths are relative to the selected TOML and
must stay within the project. Never put private keys or account tokens in TOML.
Installing an official binary adopts that binary's embedded configuration.
-The future setup/key-generation script is not included in this change.
+Do not place private signing keys or account tokens in the configuration file.
-Keep `[publishing].enabled = false` until activation is approved. Activation
+For a new publisher, keep `[publishing].enabled = false` until activation is approved. Activation
requires service endpoints, supported-client `profiles_file`, `next_catalog_file`,
existing `mirror_tag` and matching repository/channel/public key. Exact release
assets, hashes and bridge routes remain signed catalog data. Legacy downloads
remain enabled by default. Linux tar.gz packaging currently reuses the verified
ZIP output and requires ZIP in its configured package list.
-`asset_name_pattern` controls immutable service names independently of client
-selection. It must retain distinct revision identities and cannot overwrite
-signed bridge assets. Only handlers already compiled into the manager are usable;
-changing the package list cannot add Flatpak or other unimplemented formats.
+The release workflow shares revision-1 compatibility packages between both updater protocols. Service revisions receive distinct filenames; do not overwrite a URL referenced by signed policy. Only handlers compiled into a client are usable. Changing the configured package list cannot add Flatpak or another unimplemented format.
Worker settings are generated from the same resolver, not a second editable
Wrangler file. `npm run dry-run` refreshes ignored `wrangler.generated.json` and
@@ -128,7 +120,7 @@ The renewal workflow checks daily at 08:17 UTC and only signs near expiry (or
for an explicitly requested release policy). Configuration does not authorize
an upload, production activation or deployment.
-Set the protected repository variables `CRAFT_UPDATE_V2_ENABLED` and `CRAFT_UPDATE_V2_DEPLOY_ENABLED` only after approval. Configure the `manager-update-production` environment with required reviewers, scoped Cloudflare credentials, account ID and an existing `[publishing].mirror_tag` in updater.toml. Keep signing access separate from deployment. Retain an offline key backup. The Worker receives only the public key and signed catalog, never the private signing key.
+For a new deployment, configure `CRAFT_UPDATE_V2_ENABLED`, `CRAFT_UPDATE_CATALOG_RENEW_ENABLED`, and `CRAFT_UPDATE_V2_DEPLOY_ENABLED` deliberately. Store the scoped Cloudflare token in the `manager-update-production` environment; the account ID and endpoint choices are repository variables. Configure environment reviewers if required by the project and keep an existing `[publishing].mirror_tag` in the selected publisher TOML. Keep signing access separate from deployment. Retain an offline key backup. The Worker receives only the public key and signed catalog, never the private signing key.
## Isolated test Worker
@@ -142,6 +134,9 @@ With explicit owner approval, a separate test Worker was deployed on 2026-10-16:
The initial service hosted metadata/routing fixtures only. It now also serves isolated Windows fixture packages for the transaction checks below; these are **not application releases**. Do not point a normal manager build at this endpoint or weaken its production trust. It does not change public update metadata, GitHub releases or production activation variables.
+
+Isolated fixture commands and their limits
+
From `update-service/`, regenerate its seven-day test catalog and validate before a separately authorized redeployment:
```text
@@ -190,15 +185,20 @@ Generated executables, packages, isolated installations and `evidence.json` stay
This test executable uses shared production metadata transport, signature/routing/trust, archive staging, build-identity and commit-authorization functions plus the existing executable replacement helper. Its fixture-only adapter retrieves signed hash/size/asset identities from the same test Worker's `/artifacts/` path. Production URL validation/downloads remain restricted to GitHub. Consequently, successful fixture transactions do **not** prove the production GitHub download path, the production-v2 authorization wrapper dispatch, the full GUI startup, retained old managers, native installers, or Linux/macOS runtime behavior. Qualify these separately using actual manager packages and retained client builds. Production binaries never acquire the public fixture key or test endpoint.
+
+
## Publishing and renewal
+Use [Release preparation](releases.md) for the workflow inputs and catalog-only recovery procedure. Normal Release runs publish directly; preview drafts are optional.
+
+
1. Build/test artifacts; preserve current legacy filenames, v1 signatures and native compatibility versions during migration. First install the server-aware release through that old compatible path. Managers that have never installed it still only check GitHub latest.
-2. When v2 publishing is enabled, snapshot immutable v2 asset copies **before** the v1 renaming pass. Publish package bytes before advertising them. Do not clobber a v2 URL with a new revision or delete a required historical bridge. Native package version remains separate from filename/display version.
+2. Publish one shared baseline package per platform/format; both signed protocols reference the same asset. Service-enabled revisions use distinct filenames and immutable upload checks. Publish package bytes before advertising them. Do not clobber a signed URL or delete a required historical bridge. Native package version remains separate from filename/display version.
3. Validate the explicit candidate catalog and every supported client profile through `update-catalog-tool`, which uses the actual Rust router. All supported profiles must reach the newest target. Unknown future capabilities must have retained compatible bridges.
-4. `update-catalog.py verify-assets` verifies remote sizes/digests and emits a short-lived receipt bound to candidate bytes. `build` requires that receipt, the authenticated previous catalog and a strictly higher sequence. First activation uses an explicit `--initial` instead of pretending a previous catalog exists. The signing key must match the application's pinned public key.
+4. Release mode binds new package entries to the authenticated published GitHub release inventory, so rebuilt packages do not reuse stale draft hashes. Existing signed identities are unchanged. `update-catalog.py verify-assets` independently downloads and verifies remote sizes/digests and emits a short-lived receipt bound to candidate bytes. `build` requires that receipt, the authenticated previous catalog and a strictly higher sequence. First activation uses an explicit `--initial` instead of pretending a previous catalog exists. The signing key must match the application's pinned public key.
5. Deploy identical envelope bytes atomically with Worker assets and mirror them on GitHub. Verify the public response. The initial activation is an explicitly approved manual operation; the renewal workflow requires an existing authoritative catalog and cannot bootstrap itself.
-`renew-update-catalog.yml` is disabled unless the activation variable is true. Its sign job authenticates the previous catalog before inspecting its package instructions; the separate protected deploy job requires the deployment variable as well. The workflow checks daily and renews only when the configured expiry threshold is reached. Renewal advances sequence and extends validity to the configured duration (30 days by default), even without binary changes. Release publishing calls the same serialized workflow with next-release policy. Enable workflow-failure notifications for maintainers; a failed renewal must be investigated before expiry. There is no indefinite expired-catalog bypass.
+`renew-update-catalog.yml` runs signing only when `CRAFT_UPDATE_CATALOG_RENEW_ENABLED` or `CRAFT_UPDATE_V2_ENABLED` is true; deployment separately requires `CRAFT_UPDATE_V2_DEPLOY_ENABLED`. Its sign job authenticates the previous catalog before inspecting its package instructions; the separate protected deploy job requires the deployment variable as well. The workflow checks daily at 08:17 UTC (01:17 Arizona time). With the production defaults, each signature is valid for 30 days and renewal begins when seven days or less remain. Routine renewal requires no manual action while Actions, credentials, and package URLs remain healthy. It renews only when the threshold is reached unless forced or publishing a release. Renewal advances sequence and extends validity to the configured duration (30 days by default), even without binary changes. Release publishing calls the same serialized workflow with next-release policy. Enable workflow-failure notifications for maintainers; a failed renewal must be investigated before expiry. There is no indefinite expired-catalog bypass.
## Failure, recovery and retention
@@ -206,7 +206,7 @@ Transport failure can use static/mirror metadata; signature, scope, expiry or se
Bridge restarts bypass the successful-result cache so they fetch current policy; failure/429 backoff remains in force. Immediately before replacement or handing control to a native installer, the updater rechecks expiry, signed selection and the highest accepted catalog sequence. A trust guard serializes that decision with metadata acceptance through the transaction. Once an external installer owns the operation, it may need to finish; a later policy change is not a promise of cancellation or rollback. Linux native success also requires the installed executable's actual version, revision and architecture to match the signed identity.
-Immutable asset uploads are preflighted as a group. Already-published identical bytes are left alone, different bytes stop the upload, and missing assets are uploaded without clobber. Mutable legacy downloads use a separate path. This protects existing signed catalogs even if a configured naming pattern accidentally reuses an older filename.
+Immutable asset uploads are preflighted as a group. Already-published identical bytes are left alone, different bytes stop the upload, and missing assets are uploaded without clobber. GitHub-only publishers retain a separate mutable legacy-download path; service-enabled baseline packages stay immutable. This protects existing signed catalogs even if a configured naming pattern accidentally reuses an older filename.
Keep helper/staging failure records until ownership and recovery needs are established. A failed native installer is not assumed rollback-safe. A DMG detach failure retains its exact cleanup record; never sweep arbitrary mounted volumes. Cancellation before commit leaves the installed program untouched; installation already handed to a native package manager may need to finish before the manager can report a verified result.
diff --git a/docs/user-guide/linux.md b/docs/user-guide/linux.md
index 94fdda6..92a47c9 100644
--- a/docs/user-guide/linux.md
+++ b/docs/user-guide/linux.md
@@ -1,119 +1,86 @@
-# Linux development build
+# Linux
-The Windows, Linux and macOS applications share the same Rust source. Linux testing and packages use their own folders and app library.
+For installing and using the manager on Linux. Compilation and packaging are covered in [Building and testing](../development/building.md).
-## Current support
+## Choose a download
-The first test system is Ubuntu 26.04 x86_64. AppImage releases are the default; Installer mode selects Debian packages on Ubuntu/Debian and RPM packages on Fedora/RHEL-family distributions. AppImage launch uses extraction mode, so FUSE is not required. Package installation and removal ask for desktop administrator authorization.
+Get a package from [Releases](https://github.com/CryptoKey98/craft-apps-manager/releases/latest). Choose x64 for a 64-bit Intel/AMD system; x86/i386/i686 packages are 32-bit. Linux ARM64 has not been verified.
-App updates, source downloads, backups, logs, launch settings and the source builder use the Linux platform layer. Hourly checks use systemd user timers and send desktop notifications; they never install updates automatically. Timers run during the user's session.
+| System | Native package | Portable alternative |
+| --- | --- | --- |
+| Ubuntu/Debian | `.deb` | ZIP or TAR.GZ |
+| Fedora/RPM-based distributions | `.rpm` | ZIP or TAR.GZ |
+| Arch/Arch-compatible distributions | `.pkg.tar.zst` (experimental, x64 only) | ZIP or TAR.GZ |
-Profile deletion remains disabled on Linux until each app's actual profile directories have been checked. Installer cancellation is available during downloading, but an authorized package transaction must finish to protect the package database.
+The manager's portable archive is different from the **AppImage** format available for many Craft apps.
-Installation selectors identify AppImages from their file headers, including older installations whose executable was renamed. Other portable packages keep the Portable label; installation letters and selections remain unchanged.
+## Install a native package
-Close-to-tray requires both a usable tray host and an X11/XWayland window. The manager prefers a reachable X11 display when one is available. Native Wayland windows cannot be hidden by the current window backend, so the setting is disabled there and closing quits normally. The Builder retains its normal backend selection.
+Open a terminal in the download folder. These examples use the 1.0.0 x64 filenames; substitute your downloaded file.
-## Arch Linux (experimental)
-
-Arch and distributions identifying as Arch-compatible use pacman for native
-package detection, installation, removal and manager updates. The local manager
-package is x86_64 only. Craft apps must provide a matching Arch package for
-Installer mode; otherwise select AppImage in the app's installation dialog. DEB and RPM files are
-never installed on Arch. Availability of individual Craft apps depends on their
-upstream releases.
-
-Build the manager on Linux with the commands below. On Arch, install `base-devel`,
-`pkgconf`, `libxkbcommon`, `wayland`, `libx11`, `libxi`, `libxrandr`,
-`libxcursor`, `libglvnd` and `openssl` first. For packaging, also install
-`libarchive`, `polkit`, `libnotify` and `xdg-utils`.
-
-Run the packaging script as a regular user on Arch:
+Ubuntu/Debian:
```sh
-sh scripts/packaging/package-linux-arch.sh target/release/craft-apps-manager
-sudo pacman -U ./dist/linux/craft-apps-manager-1.0.0-1-x86_64.pkg.tar.zst
+sudo apt install ./craft-apps-manager_1.0.0_amd64.deb
```
-The package includes Manager and Builder launchers and license notices. Removing
-it with `sudo pacman -R craft-apps-manager` preserves your library and settings.
-The manager's update button selects a native Arch package when one is present
-in a manager release and requests administrator approval through PolicyKit.
-Native manager installation and direct/bridge updates have been exercised on an Arch x86_64 VM. This does not establish compatibility with every Arch derivative or desktop configuration.
-
-Keep Arch fully updated before setting up build tools. The manager installs
-needed prerequisites with `pacman -S --needed`; it does not refresh the package
-database separately or perform a partial system upgrade.
-
-Tester checklist: launch Manager and Builder; install, launch and remove an
-AppImage; download sources; create and restore a backup; test notifications and
-scheduled checks; set up tools and build a selected app. Native manager upgrades
-need two native package versions for testing.
-Arch desktop and updater checks have used a KDE VM; other desktop environments still need separate qualification.
-
-The Check workflow also runs in an Arch Linux x86_64 container: formatting,
-tests, Clippy, a release build, native package creation, and pacman installation
-and removal of that package. This does not replace desktop testing of GUI,
-PolicyKit prompts, notifications, or scheduled operations.
-
-See the [pacman manual](https://man.archlinux.org/man/pacman.8.en) for native
-package operations.
-
-## Folders
-
-By default, the manager keeps its library in `$XDG_DATA_HOME/craft-apps-manager`, or `~/.local/share/craft-apps-manager` when that variable is unset. Use `--root /absolute/path` for a separate test library. Sources, builds, releases, backups and logs remain separate inside the library.
-
-## Build
-
-On Ubuntu, install the native GUI dependencies:
+Fedora:
```sh
-sudo apt-get update
-sudo apt-get install -y build-essential pkg-config libx11-dev libxi-dev libxrandr-dev libxcursor-dev libxinerama-dev libxkbcommon-dev libwayland-dev libgl1-mesa-dev libssl-dev p7zip-full
+sudo dnf install ./craft-apps-manager-1.0.0-0.2.x86_64.rpm
```
-Install Rust using the instructions at https://rust-lang.org/tools/install/, then run:
+Arch:
```sh
-cargo build --release --locked
-cargo test --locked --all-targets -- --test-threads=1
-cargo clippy --locked --all-targets -- -D warnings
+sudo pacman -U ./craft-apps-manager-1.0.0-1-x86_64.pkg.tar.zst
```
-The executable is `target/release/craft-apps-manager`. Launch it with `--builder` to open the builder. Set up build tools installs the selected app's prerequisites on Ubuntu/Debian and Fedora/RHEL-family systems with DNF; ArtCraft X additionally needs Node, npm and its desktop dependencies. Arch-compatible systems use pacman for prerequisite setup. Other distributions require manual prerequisite setup.
+Packages include Manager and Builder application-menu entries. Native package installation and removal require administrator approval. Removing the manager package keeps your library and settings.
-## Distribution testing
+On Arch, keep the system fully updated. Tool setup uses `pacman -S --needed` without a separate database refresh; do not perform a partial system upgrade.
-Both x64 and x86 (i686) manager builds have passed their source tests on Ubuntu 26.04. The x86 Manager and Builder also launch on Ubuntu 26.04 and Fedora 44 with the 32-bit runtime libraries installed. These are multilib tests on 64-bit VMs, not tests of a native 32-bit operating system. Fedora dependency resolution for the i686 RPM has been checked.
+## Use a portable archive
-To cross-build x86 on an Ubuntu x64 host, add the `i386` package architecture and install `gcc-multilib`, `libc6-dev-i386`, and the GUI development packages above with `:i386`. Add the Rust target with `rustup target add i686-unknown-linux-gnu`, then build with `PKG_CONFIG_ALLOW_CROSS=1 PKG_CONFIG_LIBDIR=/usr/lib/i386-linux-gnu/pkgconfig:/usr/share/pkgconfig cargo build --release --locked --target i686-unknown-linux-gnu`. The packaging scripts detect the binary architecture rather than the build host.
+Extract the **whole** ZIP or TAR.GZ into a writable folder, then open `craft-apps-manager` inside `Craft Apps Manager Linux`. For example:
-Run `sh scripts/packaging/package-linux-zip.sh path/to/craft-apps-manager` for a portable ZIP. The 32-bit manager only selects matching upstream x86 app assets; it cannot turn an upstream x64-only release into a 32-bit app.
+```sh
+tar -xzf Craft-Apps-Manager-1.0.0-linux-x64.tar.gz
+cd 'Craft Apps Manager Linux'
+./craft-apps-manager
+```
-Linux packages are experimental. Release DEB, RPM and portable binaries are built on Ubuntu 22.04; Arch packages are built separately on Arch. Earlier local test binaries built on Ubuntu 26.04 required glibc 2.43 or newer; that requirement does not describe every release package. Ubuntu 26.04 and Fedora 44 x86_64 have been used for desktop testing. Additional distribution testing is needed before claiming compatibility with older distributions. An Ubuntu 22.04 or 24.04 VM is a useful next test target. macOS packaging is documented separately.
+Keep `portable.txt` and the other extracted files together. If your ZIP extractor lost executable permissions, mark the manager executable in your file manager or run `chmod +x craft-apps-manager` from its folder. Do not run the manager itself with sudo.
-## Ubuntu and Debian development package
+New portable libraries use `data` beside the executable. Native installations use `$XDG_DATA_HOME/craft-apps-manager`, or `~/.local/share/craft-apps-manager` when unset. The Data path shows the active location. See [Storage and portability](using-the-manager.md#storage-and-portability) before moving files.
-Run `sh scripts/packaging/package-linux-deb.sh path/to/craft-apps-manager` on Ubuntu or Debian. It requires `dpkg-dev` and creates an `amd64` or `i386` DEB based on the executable's ELF architecture. Linked-library requirements are calculated from the binary, so a package built on a newer Ubuntu release may require newer system libraries.
+## Install and update apps
-Install the local package with `sudo apt install ./dist/linux/craft-apps-manager_1.0.0_amd64.deb` (or the `i386` package). The package includes both desktop launchers, the icon, and license notices. Removing it keeps the user's library and settings.
+Choose each app's available format and architecture in its installation dialog. AppImages run in extraction mode, so FUSE is not required. Installer mode uses the distribution's native package family; DEB and RPM are not installed on Arch. Upstream apps must publish the selected format.
-## Fedora RPM development package
+Custom native packages need inspection tools:
-Build a native Linux release binary, then run:
+| Package | Tools used |
+| --- | --- |
+| DEB | `dpkg-deb`, `bsdtar` (Ubuntu/Debian package `libarchive-tools`) |
+| RPM | `rpm`, `rpm2cpio`, `bsdtar` |
+| Arch | `bsdtar` (Arch package `libarchive`) |
-```sh
-sh scripts/packaging/package-linux-rpm.sh target/release/craft-apps-manager
-```
+The manager records package identity and a package-owned desktop executable. If several launchers are found, choose the app to track. Unsupported shell wrappers may require manual installation.
-The script requires `rpmbuild` (Fedora package `rpm-build`). It places the development RPM in `dist/linux`, with a system executable, manager and builder menu entries, icon, documentation and license notices. The RPM automatically records ELF library requirements; packages built on a newer distribution may not install on older systems.
+Manager updates use a matching native package for native installations, or a supported portable archive for portable installations. Native transactions use PolicyKit and apt, dnf, or pacman. Cancellation may have to wait for a package transaction to finish. Settings and library locations are retained.
-On Fedora, install the local RPM using:
+## Background checks and system tray
-```sh
-sudo dnf install ./dist/linux/craft-apps-manager-1.0.0-*.x86_64.rpm
-```
+Startup, periodic, and scheduled checks notify you; they do not automatically install updates. Hourly scheduled checks use systemd user timers during your session.
+
+Close-to-tray needs a usable tray host and an X11/XWayland window. Some GNOME desktops require a compatible tray extension. The manager prefers a reachable X11 display; on native Wayland the setting is disabled and closing quits normally. Without a usable tray, closing also quits so the manager cannot become inaccessible.
+
+## Limitations and help
-RPM packages are experimental during distribution testing. Removing the package leaves each user's app library intact. For a DEB/RPM installation, the manager update button downloads the matching native package and invokes apt-get or dnf with an administrator prompt. It verifies the installed package version before restarting, and preserves user settings and libraries. Portable ZIP installations use the separate executable-replacement update flow and require a writable installation folder.
+- Arch packages and Linux desktop integration remain experimental. Tested desktops include Ubuntu, Fedora, and Arch; behavior can differ elsewhere.
+- Published portable/DEB/RPM builds use Ubuntu 22.04. Local builds on newer distributions may need newer libraries.
+- x86 launch tests used multilib on 64-bit systems; native 32-bit systems and Linux ARM64 have not been qualified.
+- Profile deletion is disabled where app profile locations have not been verified.
-Custom GitHub native packages are inspected before installation. Debian packages use `dpkg-deb` and `bsdtar` (`libarchive-tools`); RPM packages use `rpm`, `rpm2cpio`, and `bsdtar`; Arch packages use `bsdtar` from `libarchive`. The manager records the package name and its desktop launcher's executable. If a package has several desktop launchers, choose the application to track. Shell-wrapper launch commands that cannot be tied to a package-owned executable are rejected with an explanation.
+For app actions, backups, builds, and troubleshooting, see [Using the manager](using-the-manager.md) and the [documentation index](../README.md).
diff --git a/docs/user-guide/macos.md b/docs/user-guide/macos.md
index 17355b3..eb4f726 100644
--- a/docs/user-guide/macos.md
+++ b/docs/user-guide/macos.md
@@ -1,49 +1,57 @@
# macOS
-The manager runs on macOS 11 or newer, on Apple silicon and Intel. It is experimental.
+The manager supports macOS 11 or newer on Intel and Apple silicon. Universal downloads contain both architectures. macOS support remains experimental.
-## Installing apps
+## Install the manager
-Each Craft app publishes one universal, notarized `*-macos-universal.dmg`. The manager uses it for both release formats:
+Choose a download from [Releases](https://github.com/CryptoKey98/craft-apps-manager/releases/latest):
-- **Installer** (default) copies the app into `/Applications`, or into `~/Applications` if `/Applications` is not writable. Apps you installed yourself are detected by their bundle name and `ai.storyteller.*` bundle identifier. Uninstall removes the app bundle.
-- **Portable app** copies the app into the library under `releases//`.
+- **DMG:** open it, drag **Craft Apps Manager** into **Applications**, then launch the copied app from Applications. Eject the mounted disk image afterward. Finder handles the copy and progress display.
+- **Portable ZIP:** extract the whole archive into a writable folder. Keep `portable.txt` beside `Craft Apps Manager.app`; launch the app from that folder.
-Before installing, the manager checks GitHub's SHA-256 digest for the DMG. It then verifies the copied app with `codesign --verify --deep --strict` and Gatekeeper (`spctl --assess`).
+A DMG is a disk image, not an installer wizard. Do not run the manager from the mounted DMG when testing updates.
-The library is stored in `~/Library/Application Support/Craft Apps Manager`. Hourly checks use launch agents in `~/Library/LaunchAgents/io.github.craft-apps-manager.*.plist`, and notifications appear through Notification Center.
+## First launch
-## Manager updates
+The manager has an ad-hoc signature and is not Apple-notarized. If macOS blocks the downloaded app, dismiss the warning, open **System Settings → Privacy & Security**, and use **Open Anyway** when available for that app. Follow macOS's confirmation prompts. Do not disable system-wide security checks.
+
+Rust and 7-Zip are not required to run the manager. Compilation prerequisites are in [Building and testing](../development/building.md#macos).
+
+## Apps and saved data
+
+For supported Craft app DMGs, the installation dialog offers:
-Craft Apps Manager.app can update itself. The new ZIP is checked against GitHub's SHA-256 digest, extracted into a private `.craft-manager-update-` folder beside the manager bundle and verified with `codesign`. Staging and rollback stay on the manager's filesystem even when the library is on another volume.
+- **Installer:** copies the app into `/Applications`, or `~/Applications` when `/Applications` is not writable.
+- **Portable:** keeps that app in the manager's library.
-After the manager closes, the old app bundle is moved into that folder as `previous.app` before the replacement is published. Failed swaps restore the previous manager when possible; if restoration fails, the previous bundle stays at that recovery location. The helper checks whether macOS accepts the restart request; this does not confirm that the application finished starting.
+Download integrity and bundle verification still apply. Custom app support depends on the upstream download and bundle identity. Uninstall removes the selected app bundle; app profile deletion is not supported on macOS.
+
+Installed managers normally use `~/Library/Application Support/Craft Apps Manager`. Portable managers use portable storage, with new libraries in `data` beside the app. The Data path identifies your actual library. See [Storage and portability](using-the-manager.md#storage-and-portability).
+
+Hourly scheduled checks use launch agents in `~/Library/LaunchAgents/io.github.craft-apps-manager.*.plist`. Checks notify through Notification Center and do not automatically install updates.
+
+## Manager updates
-A failed update is reported on the next manager startup, with a recovery path, and recorded outside the signed bundle in `~/Library/Application Support/Craft Apps Manager/self-update-result.json` and in the staging folder's `result.json`. Older library-based plans are rejected; reopen the manager and download again. The app must be in a folder you can write to, such as Applications. Development builds run outside a bundle and cannot update themselves.
+The updater checks the offered package's identity and integrity before replacement. Service-enabled builds can use supported ZIP or DMG payloads; older GitHub-only builds use their compatible update path. A supported archive-format change does not itself require manually changing your installation.
-The previous manager runs the update, so a fix to the updater applies only from the next update onwards. Managers 0.8.1 to 0.8.3 cannot update themselves when another app in the same folder contains symbolic links, which includes Electron apps such as Visual Studio Code. The update fails with `Archive contains a linked path` and the previous manager is restored. Download the macOS ZIP from the [latest release](https://github.com/CryptoKey98/craft-apps-manager/releases/latest) and replace `Craft Apps Manager.app` manually; your library stays in place. Later updates work from within the manager. Once the new version starts, you can delete the `.craft-manager-update-*` recovery folders that failed attempts left beside the app.
+Close active work before updating. Keep the app in a normal installed or portable location, not on a mounted DMG. A development executable outside an app bundle cannot use the bundle self-update flow. Protected locations may require administrator approval.
-Updates come from the GitHub repository set by `CRAFT_MANAGER_REPOSITORY` (owner/name) at build time. The default is `CryptoKey98/craft-apps-manager`. CI sets it to the repository being built, so a fork's packages update from the fork's releases.
+Staging and rollback stay beside the installed bundle even if the library is on another volume. Failed replacement attempts recovery and records the result. macOS accepting a restart request does not prove the app finished starting.
-## Not yet available on macOS
+### Older updater failures
-- Deleting app profile data on uninstall. The upstream profile paths have not been verified yet.
-- Shortcut files. App bundles open directly from Finder.
+Managers 0.8.1–0.8.3 could fail when another app in the same directory contained symbolic links, with `Archive contains a linked path`. The saved failure could also reappear on launch. These issues were fixed in 0.9.0.
-## Building
+If an affected updater still fails, manually download a current macOS package and replace the manager app, preserving your library and portable files. The saved error may appear once on the first fixed launch; dismiss it. Users already on 0.9.0 have the corrected updater.
-Install Rust with rustup and the Xcode Command Line Tools. Then build:
+Failures can leave a `.craft-manager-update-*` recovery folder beside the app. Check the error and retained `previous.app` before removing anything. Results may also be recorded in `~/Library/Application Support/Craft Apps Manager/self-update-result.json` and the staging folder's `result.json`. See [Updater recovery](../development/updater-operations.md#failure-recovery-and-retention) before deleting recovery data.
-```text
-rustup target add aarch64-apple-darwin x86_64-apple-darwin
-cargo test --locked -- --test-threads=1
-./scripts/packaging/package-macos.sh
-```
+## Builds and backups
-The script writes a universal `Craft Apps Manager.app` and a ZIP to `dist/macos`. The bundle has only an ad-hoc signature and is not notarized. The first time a downloaded copy is opened, macOS blocks it. Click **Done**, then go to **System Settings → Privacy & Security** and click **Open Anyway**. Right-click → **Open** no longer skips this check on macOS 15 and newer.
+Manager and Builder are available in both ZIP and DMG packages. Building Craft apps requires Rust, Xcode Command Line Tools, and app-specific tools. Native source ZIP extraction is internal and keeps archive safety checks; 7-Zip is optional.
-Source builds require Rust and Xcode Command Line Tools. Source ZIPs are extracted internally with archive safety checks; 7-Zip is not required. ArtCraft X also needs extra tools such as `node`, `cmake` and `nasm`, which setup can install through Homebrew. Builds produce a bare executable under `builds/`, not an app bundle.
+7-Zip is needed for compressed `.7z` backups and restoring existing `.7z` backups. Without it, the original backup is retained. Install `sevenzip` through Homebrew if needed. ArtCraft X needs additional tools such as Node, CMake, and NASM; supported setup can use Homebrew for those tools. Local app builds produce executables rather than complete app bundles.
-7-Zip is optional for compressed `.7z` backups and restoring existing `.7z` backups. Without it, the manager retains the original backup. Install `sevenzip` through Homebrew if you need those features.
+Shortcut files and app-profile deletion are not provided on macOS. Open app bundles directly in Finder.
-The manager's DMG uses a drag-to-Applications layout. Its packaging script installs pinned `dmgbuild` dependencies into a temporary Python environment, removed afterward; they are not app prerequisites. Packaging requires Python 3.9 or newer with `venv` and access to PyPI. Finder handles copying the app and any copy-progress display.
+[Using the manager](using-the-manager.md) · [Documentation index](../README.md)
diff --git a/docs/user-guide/using-the-manager.md b/docs/user-guide/using-the-manager.md
new file mode 100644
index 0000000..3d5e1d8
--- /dev/null
+++ b/docs/user-guide/using-the-manager.md
@@ -0,0 +1,130 @@
+# Using the manager
+
+Start with the [quick start](../../README.md#get-started). This guide covers everyday use; [building the manager](../development/building.md) is separate.
+
+## Installations
+
+- **Install options belong to each app.** Choose a version, release format, and architecture in its installation window. Only compatible combinations are offered; there are no global format or architecture controls in Settings.
+- **Open, update, or uninstall** from the app page. Launch options let you select an executable and add arguments.
+- **Keep multiple installations.** After the first install, use the **Installation** dropdown to select the copy you want to manage. **Add another installation…** opens the usual install window. Portable copies need separate folders; one native installer copy is supported. Apps may share their own user settings between copies.
+- **Identify portable copies by letter.** The installation selectors and desktop shortcuts use matching letters. Removing B leaves A and C unchanged; the next new copy uses B. Home's compact installation switcher shares the app page's selection.
+- **Move a copy** with **Move…** where supported: managed portable apps and supported Windows MSI installations. Linux native packages and macOS installer copies keep their system-managed locations.
+- **Hide apps** from the sidebar or Home context menu. Restore them in **Settings → Apps → App visibility**. Hiding does not uninstall an app or change its update selection.
+
+Updates and version changes use the selected installation's verified format and architecture. Unknown installation details are reported rather than guessed. Changing an existing copy's format or architecture requires removing that copy first, or adding a separate supported installation.
+
+## Updates and Activity
+
+**Check for updates** checks one app. **Update All** on Home updates the selected installed apps after a review; it never installs missing apps. Newly installed apps are selected automatically. With multiple copies, updates target the selected copy of each app.
+
+In **Settings → Updates**, configure startup checks, periodic checks while the manager is running, or hourly scheduled checks. Checks do not automatically download or install updates. The manager also checks for its own updates during periodic checks and displays **Update available** in the header when a matching release is available.
+
+**Activity**, beside Settings, shows progress, cancellation, and results. Manager and Builder share operation records. Closing a dialog does not cancel its operation; use its Cancel button. Closing the manager during an active operation requests cancellation and waits for it to stop, unless close-to-tray hides the window instead. Native package transactions may need to finish before cancellation can be confirmed.
+
+To hide the Activity button's notification badge, enable **Hide activity badge** in **Settings → General**. Unread activity highlights still appear inside the panel.
+
+**Approve administrator access once per batch** is enabled by default for eligible Windows and Linux installer batches. Linux uses PolicyKit with apt, dnf, or pacman. Individual installers can still show their own prompts. Eligible manager bridge updates can also reuse one administrator approval on macOS; the initial installation is separate.
+
+Platform-specific manager hotfixes use build revisions. Only affected platforms are offered an update; About shows `Rev. 2` or higher when installed.
+
+**What's New** appears on first launch and after a version or revision change. Reopen it with the info icon beside the manager version.
+
+## Installer cleanup
+
+Downloaded setup files are automatically removed after a successful, verified installation or update. Cleanup appears in the operation's progress and Activity details.
+
+Older setup files left by previous versions are not automatically swept up. Recovery packages or files required for installer maintenance may be retained. Failed or cancelled installations do not run successful-install cleanup; deletion problems are reported without marking a verified installation as failed.
+
+The manager also removes an app's empty setup-download folder after cleanup. Additional portable library copies use flat folders such as `filmcraft-2`; uninstalling removes empty legacy wrapper folders while preserving user-selected parents and folders containing other files.
+
+## Run in the background
+
+Enable **Keep running in system tray when closed** in General settings to hide the window while checks continue. The tray offers **Open**, **Check for app updates**, and **Exit**.
+
+This needs a usable desktop tray. On Linux, some GNOME desktops require a compatible tray extension. If there is no tray host, closing quits normally so the manager cannot become inaccessible.
+
+Linux close-to-tray also needs X11 or XWayland. The manager prefers a reachable XWayland display when available. On native Wayland, the setting is disabled and closing quits normally.
+
+## Custom GitHub apps
+
+Paste a repository link into **Settings → Apps → Custom apps**. The setup dialog lets you review the detected download, package type, and architecture, and choose a download when several are possible.
+
+Adding a repository adds it to the list; it does not install the app. Removing it from the list does not uninstall it. A repository needs a compatible downloadable release for your system.
+
+The manager remembers verified package identities and your selected executable, even when their names differ from the GitHub repository. If a package contains several possible apps, choose the one you want to use. Background updates stop with an explanation when a new choice is required.
+
+If a Windows installer leaves an existing program unchanged, you can explicitly select its verified registration. Supported Linux desktop launchers retain their arguments and per-app environment; arbitrary shell commands are not evaluated.
+
+Linux native packages need a package-owned desktop launcher. Unsupported installer types or unusual launch wrappers may still need manual installation.
+
+**Custom-app support is experimental.** Asset naming and installer registration vary, so some apps may not install, be detected, or uninstall reliably. Third-party apps are not included by default.
+
+An optional GitHub token can be saved under **Settings → Updates** to raise the API limit. It is stored in the operating system's credential store. Environment variables take priority over the saved token: `CRAFT_GITHUB_TOKEN`, then `GITHUB_TOKEN`. Authentication is sent only to `api.github.com`; don't post tokens in issues or logs.
+
+## Craft app discovery
+
+The manager includes the main Craft apps, including DesignCraft, EffectCraft, FilmCraft, LightCraft, PhotoCraft, PDFCraft, VectorCraft, WordCraft, GridCraft, DeckCraft, CADCraft, and SoundCraft. ArtCraft has release support; ArtCraft X has source-build support. Available downloads depend on what each app publishes for your platform.
+
+New Storytold Craft apps can be discovered without updating the manager. Use **Settings → Updates → App list → Refresh**. Startup discovery is enabled by default and uses a shared six-hour cache. Discovery adds apps to the list without installing them or selecting them for updates. The New badge is limited to recently created apps; a fresh manager installation does not make an old app new.
+
+Older PrintCraft files remain compatible with PDFCraft's library identity. App discovery and ArtCraft support were contributed by [carpaaaaa](https://github.com/carpaaaaa).
+
+## Storage and portability
+
+Portable archives include `portable.txt` and use portable storage automatically. A new portable library lives in a `data` folder beside the manager. Existing external libraries are preserved during migration.
+
+To move a portable manager, finish active operations, close **both Manager and Builder**, and move the **whole extracted folder**. Keep `portable.txt`, `portable-state.json`, and bundled resources with it. Managed paths inside that folder follow the move; external locations remain unchanged.
+
+Native installations, system-stored tokens, and desktop shortcuts do not move with the folder. Shortcuts may need recreation, and individual apps may store preferences elsewhere. Moving between operating systems does not convert apps or builds. Don't run an older manager against migrated portable metadata.
+
+| Manager installation | Default library location |
+| --- | --- |
+| Portable archive | `data` inside the portable folder; existing external libraries are retained |
+| Windows MSI | `%LOCALAPPDATA%\Craft Apps Manager` |
+| Linux system package | `$XDG_DATA_HOME/craft-apps-manager`, or `~/.local/share/craft-apps-manager` |
+| macOS installation using system storage | `~/Library/Application Support/Craft Apps Manager` |
+
+The **Data path at the bottom of the window** identifies your active library. **Settings → Folders** shows its locations and portable-storage information.
+
+
+What's inside the library?
+
+```text
+releases/ Portable apps and downloaded setup files
+sources/ Source archives and their index
+builds/ Finished builds, grouped by app
+logs/ Update and build logs
+backups/releases/ Portable app backups
+backups/sources/ Source backups
+workspace/ Build tools, extracted sources, and build cache
+runtime/ Staging, operation records, and migration recovery copies
+```
+
+`settings.json` records installations; `manager-settings.json` contains manager preferences. Folder changes that require a restart are shown in Settings.
+
+
+
+## Build apps and manage backups
+
+Use an app's tools to **Set up build tools**, download its source, and **Build** or **Rebuild**. Open the separate Builder through **Settings → Builds → Open builder window**. Build Options lets you choose which completed build the Launch button uses; that choice is shared between Manager and Builder.
+
+Builds use upstream source and lockfiles. Upstream compilation failures are shown in the log. Required tools depend on the app and platform; Windows Rust builds need Microsoft's C++ build tools, and ArtCraft X also needs frontend tools. Failed or cancelled builds keep their cache for a retry.
+
+**Build Options** can delete temporary cache from interrupted builds, even before a successful build exists. Cleanup is unavailable during a build and preserves completed builds and source ZIPs. The next build recreates its cache.
+
+Optional portable-app and source backups are configured in Settings. Use **Backups → Manage…** on an app page to restore or delete a selected backup. Native installer applications are not backed up by the manager. There is no universal automatic rollback for every installer.
+
+Uninstalling can optionally remove listed app profile folders. This is off by default, and some installations share profiles. Review the confirmation before deleting profile data.
+
+
+## Troubleshooting
+
+Activity shows operation results and access to logs. Include the manager version/revision, operating system, app, installation type, steps, and exact error when reporting a problem. For detection problems, maintainers may ask for `settings.json` from the active Data folder. Redact personal information and never share tokens or passwords.
+
+A custom repository must be active, not a fork, and have a stable compatible desktop release. CLI-only repositories are rejected. Some downloads need an explicit architecture or application choice. An API token can help with rate limits; it cannot add an upstream package that does not exist.
+
+### Upgrading from Craft Apps Updater
+
+The app was renamed in 0.3.2. Very old Updater versions may reject the renamed repository or packages and need a manual manager download. Close the old Updater and preserve its library when switching. Disable its old scheduled tasks before enabling scheduled checks in Manager.
+
+[Documentation index](../README.md)
diff --git a/scripts/documentation/wiki-manifest.json b/scripts/documentation/wiki-manifest.json
new file mode 100644
index 0000000..8229241
--- /dev/null
+++ b/scripts/documentation/wiki-manifest.json
@@ -0,0 +1,43 @@
+{
+ "schema": 1,
+ "repository": "CryptoKey98/craft-apps-manager",
+ "pages": [
+ {"source":"docs/README.md","destination":"Home.md","title":"Home","group":"Start here"},
+ {"source":"README.md","destination":"Getting-started.md","title":"Getting started","group":"Start here"},
+ {"source":"docs/user-guide/using-the-manager.md","destination":"Using-the-manager.md","title":"Using the manager","group":"Install and use"},
+ {"source":"docs/user-guide/linux.md","destination":"Linux.md","title":"Linux","group":"Install and use"},
+ {"source":"docs/user-guide/macos.md","destination":"macOS.md","title":"macOS","group":"Install and use"},
+ {"source":"docs/development/building.md","destination":"Building-and-testing.md","title":"Building and testing","group":"Build and contribute"},
+ {"source":"docs/development/windows-packaging.md","destination":"Windows-packaging.md","title":"Windows packaging","group":"Build and contribute"},
+ {"source":"docs/development/releases.md","destination":"Releases.md","title":"Releases","group":"Publish and maintain"},
+ {"source":"docs/development/updater-operations.md","destination":"Updater-operations.md","title":"Updater operations","group":"Publish and maintain"},
+ {"source":"update-service/catalog/README.md","destination":"Production-catalog.md","title":"Production catalog","group":"Publish and maintain"},
+ {"source":"docs/architecture/per-app-installation.md","destination":"Architecture-installations.md","title":"Installations","group":"Architecture"},
+ {"source":"docs/architecture/operations.md","destination":"Architecture-operations.md","title":"Operations and Activity","group":"Architecture"},
+ {"source":"docs/architecture/windows-batch-admin.md","destination":"Windows-batch-approval.md","title":"Windows batch approval","group":"Architecture"},
+ {"source":"docs/architecture/linux-batch-admin.md","destination":"Linux-batch-approval.md","title":"Linux batch approval","group":"Architecture"},
+ {"source":"CHANGELOG.md","destination":"Changelog.md","title":"Changelog","group":"Release history"}
+ ],
+ "assets": [
+ {"source":"assets/icon.png","destination":"images/icon.png"},
+ {"source":"docs/images/activity.png","destination":"images/activity.png"},
+ {"source":"docs/images/app-selection.png","destination":"images/app-selection.png"},
+ {"source":"docs/images/builder.png","destination":"images/builder.png"},
+ {"source":"docs/images/install.png","destination":"images/install.png"},
+ {"source":"docs/images/main.png","destination":"images/main.png"},
+ {"source":"docs/images/overview.png","destination":"images/overview.png"},
+ {"source":"docs/images/settings.png","destination":"images/settings.png"}
+ ],
+ "aliases": {
+ "docs/linux.md":"docs/user-guide/linux.md",
+ "docs/macos.md":"docs/user-guide/macos.md",
+ "docs/releases.md":"docs/development/releases.md",
+ "docs/updater-operations.md":"docs/development/updater-operations.md",
+ "docs/windows-packaging.md":"docs/development/windows-packaging.md",
+ "docs/per-app-installation.md":"docs/architecture/per-app-installation.md",
+ "docs/operations.md":"docs/architecture/operations.md",
+ "docs/windows-batch-admin.md":"docs/architecture/windows-batch-admin.md",
+ "docs/linux-batch-admin.md":"docs/architecture/linux-batch-admin.md"
+ },
+ "source_links": ["LICENSE", "THIRD-PARTY-NOTICES.txt"]
+}
diff --git a/scripts/documentation/wiki.py b/scripts/documentation/wiki.py
new file mode 100644
index 0000000..98bc278
--- /dev/null
+++ b/scripts/documentation/wiki.py
@@ -0,0 +1,308 @@
+"""Generate a local, allowlisted wiki preview. This module cannot publish."""
+import argparse
+import hashlib
+import json
+import posixpath
+import html
+from pathlib import Path, PurePosixPath
+import re
+import sys
+from urllib.parse import quote, unquote, urlsplit
+
+REPOSITORY = 'CryptoKey98/craft-apps-manager'
+WIKI = 'https://github.com/' + REPOSITORY + '/wiki'
+OWNERSHIP = '.craft-docs-sync.json'
+GENERATED = ''
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def relative(value):
+ if not isinstance(value, str) or not value or any(c in value for c in '\\:?#\r\n\x00'):
+ raise ValueError('Unsafe relative path: ' + repr(value))
+ path = PurePosixPath(value)
+ if path.is_absolute() or any(p in ('', '.', '..') for p in value.split('/')):
+ raise ValueError('Unsafe relative path: ' + value)
+ return path
+
+
+def linked(path):
+ try:
+ info = path.lstat()
+ except FileNotFoundError:
+ return False
+ return path.is_symlink() or bool(getattr(info, 'st_file_attributes', 0) & 0x400)
+
+
+def checked_path(root, name):
+ path = root
+ if linked(root):
+ raise ValueError('Linked root is not permitted')
+ for part in relative(name).parts:
+ path = path / part
+ if linked(path):
+ raise ValueError('Linked path is not permitted: ' + name)
+ if not path.resolve().is_relative_to(root.resolve()):
+ raise ValueError('Path escapes root')
+ return path
+
+
+def validate_manifest(manifest):
+ if manifest.get('schema') != 1 or manifest.get('repository') != REPOSITORY:
+ raise ValueError('Unexpected wiki manifest scope')
+ sources, destinations = set(), {'_sidebar.md', '_footer.md', OWNERSHIP}
+ for kind in ('pages', 'assets'):
+ if not isinstance(manifest.get(kind), list) or not 1 <= len(manifest[kind]) <= 128:
+ raise ValueError('Invalid manifest list')
+ for item in manifest[kind]:
+ source, dest = str(relative(item['source'])), str(relative(item['destination']))
+ public = (source in ('README.md', 'CHANGELOG.md', 'docs/README.md', 'update-service/catalog/README.md')
+ or source.startswith(('docs/user-guide/', 'docs/development/', 'docs/architecture/')))
+ if kind == 'pages':
+ if not public or not source.endswith('.md') or '/' in dest or not re.fullmatch(r'[A-Za-z0-9-]+\.md', dest):
+ raise ValueError('Not an approved public documentation path')
+ if any(not isinstance(item.get(k), str) or not re.fullmatch(r'[A-Za-z0-9 /-]+', item[k]) for k in ('title', 'group')):
+ raise ValueError('Invalid navigation title/group')
+ elif not ((source == 'assets/icon.png' or source.startswith('docs/images/')) and source.endswith('.png') and re.fullmatch(r'images/[A-Za-z0-9-]+\.png', dest)):
+ raise ValueError('Not an approved image path')
+ if any(part.startswith('.') for part in relative(source).parts):
+ raise ValueError('Private path is not permitted')
+ if source.casefold() in sources or dest.casefold() in destinations:
+ raise ValueError('Duplicate source/destination')
+ sources.add(source.casefold())
+ destinations.add(dest.casefold())
+ for alias, target in manifest.get('aliases', {}).items():
+ relative(alias)
+ if not alias.startswith('docs/') or target.casefold() not in sources:
+ raise ValueError('Invalid legacy alias')
+ if manifest.get('source_links') != ['LICENSE', 'THIRD-PARTY-NOTICES.txt']:
+ raise ValueError('Unexpected non-page export links')
+
+
+def load_manifest(path):
+ manifest = json.loads(path.read_text(encoding='utf-8'))
+ validate_manifest(manifest)
+ return manifest
+
+
+def rewrite_markdown(text, source, manifest, source_commit):
+ mapping = {p['source']: p['destination'] for p in manifest['pages'] + manifest['assets']}
+ def resolve(value):
+ value = html.unescape(value)
+ parsed = urlsplit(re.sub(r'\\([() ])', r'\1', value))
+ if parsed.scheme or parsed.netloc:
+ if parsed.scheme not in ('https', 'http', 'mailto'):
+ raise ValueError('Unsupported URL scheme in ' + source)
+ return value
+ path = unquote(parsed.path)
+ if path.startswith('/') or '\\' in path:
+ raise ValueError('Absolute local reference in ' + source)
+ target = posixpath.normpath(posixpath.join(posixpath.dirname(source), path)) if path else source
+ target = manifest.get('aliases', {}).get(target, target)
+ if target in mapping:
+ dest = mapping[target]
+ base = ('https://raw.githubusercontent.com/wiki/' + REPOSITORY + '/' + quote(dest, safe='/')
+ if dest.startswith('images/') else page_url(dest))
+ elif target in manifest['source_links']:
+ base = 'https://github.com/' + REPOSITORY + '/blob/' + source_commit + '/' + quote(target, safe='/')
+ else:
+ raise ValueError('Unresolved local reference in ' + source + ': ' + value)
+ return base + ('?' + parsed.query if parsed.query else '') + ('#' + parsed.fragment if parsed.fragment else '')
+ edits = [(a, b, resolve(text[a:b])) for a, b in link_spans(text)]
+ for a, b, value in sorted(edits, reverse=True):
+ text = text[:a] + value + text[b:]
+ return text
+
+
+def code_mask(text, mask_inline=True):
+ """Same-length text with fenced/indented and inline code hidden from link scanning."""
+ chars = list(text)
+ offset, fence = 0, None
+ for line in text.splitlines(keepends=True):
+ marker = re.match(r'^ {0,3}(`{3,}|~{3,})', line)
+ protected = fence is not None or marker is not None or line.startswith((' ', '\t'))
+ if fence:
+ if re.fullmatch(r' {0,3}' + re.escape(fence[0]) + '{' + str(fence[1]) + r',}\s*', line):
+ fence = None
+ elif marker:
+ fence = (marker[1][0], len(marker[1]))
+ if protected:
+ chars[offset:offset + len(line)] = ['\n' if c == '\n' else ' ' for c in line]
+ offset += len(line)
+ masked = ''.join(chars)
+ if not mask_inline:
+ return masked
+ i = 0
+ while i < len(masked):
+ if masked[i] == '`':
+ run = len(masked[i:]) - len(masked[i:].lstrip('`'))
+ end = re.search(r'(?\n]+)>|([^\s]+))', masked, re.M):
+ group = 1 if match[1] is not None else 2
+ spans.add(match.span(group))
+ # Markdown inline links, including images nested in linked labels.
+ for match in re.finditer(r'(?= len(masked) or masked[i] != '(':
+ continue
+ i += 1
+ while i < len(masked) and masked[i].isspace():
+ i += 1
+ start = i + (i < len(masked) and masked[i] == '<')
+ if i < len(masked) and masked[i] == '<':
+ end = masked.find('>', start)
+ if end < 0:
+ raise ValueError('Unclosed Markdown destination')
+ else:
+ depth, end = 0, i
+ while end < len(masked):
+ c = masked[end]
+ if c == '\\':
+ end += 2
+ continue
+ if (c == ')' and depth == 0) or (c.isspace() and depth == 0):
+ break
+ depth += (c == '(') - (c == ')')
+ end += 1
+ if end > start:
+ spans.add((start, end))
+ for tag in re.finditer(r'<(?:img|a)\b[^>]*>', masked, re.I):
+ for attr in re.finditer(r'\b(?:src|href)\s*=\s*([\'"])(.*?)\1', tag[0], re.I):
+ spans.add((tag.start() + attr.start(2), tag.start() + attr.end(2)))
+ ordered = sorted(spans)
+ if any(a[1] > b[0] for a, b in zip(ordered, ordered[1:])):
+ raise ValueError('Overlapping Markdown destinations')
+ return ordered
+
+
+def anchors(text):
+ seen, values = {}, set()
+ for heading in re.findall(r'^ {0,3}#{1,6}\s+(.+?)\s*#*$', code_mask(text, mask_inline=False), re.M):
+ slug = re.sub(r'[^\w -]', '', re.sub(r'<[^>]*>', '', html.unescape(heading).lower())).replace(' ', '-')
+ n = seen.get(slug, 0)
+ candidate = slug + (f'-{n}' if n else '')
+ while candidate in values:
+ n += 1
+ candidate = slug + f'-{n}'
+ seen[slug] = n + 1
+ values.add(candidate)
+ return values
+
+
+def validate_preview(files, manifest):
+ pages = {p['destination'][:-3]: p['destination'] for p in manifest['pages']}
+ for name, body in files.items():
+ if not name.endswith('.md'):
+ continue
+ text = body.decode('utf-8')
+ for a, b in link_spans(text):
+ value = html.unescape(text[a:b])
+ url = urlsplit(value)
+ if value.startswith(WIKI + '/'):
+ title = unquote(url.path.split('/wiki/', 1)[1])
+ dest = pages.get(title)
+ if dest is None or dest not in files:
+ raise ValueError('Unknown wiki target in ' + name + ': ' + value)
+ if url.fragment and unquote(url.fragment) not in anchors(files[dest].decode('utf-8')):
+ raise ValueError('Unknown heading in ' + name + ': ' + value)
+ elif value.startswith('https://raw.githubusercontent.com/wiki/' + REPOSITORY + '/'):
+ dest = unquote(url.path.split('/' + REPOSITORY + '/', 1)[1])
+ if dest not in files:
+ raise ValueError('Missing wiki image: ' + value)
+ elif not url.scheme:
+ raise ValueError('Untranslated local link in ' + name + ': ' + value)
+
+
+def page_url(destination):
+ return WIKI + '/' + quote(destination[:-3], safe='-')
+
+
+def ownership(files, source_commit):
+ return json.dumps({'schema': 1, 'repository': REPOSITORY, 'source_commit': source_commit,
+ 'files': {n: hashlib.sha256(b).hexdigest() for n, b in sorted(files.items())}}, indent=2).encode() + b'\n'
+
+
+def generate(root, manifest, source_commit):
+ validate_manifest(manifest)
+ if not re.fullmatch(r'[0-9a-f]{40}', source_commit):
+ raise ValueError('Use a full 40-character source commit SHA')
+ root = Path(root).absolute()
+ files = {}
+ for item in manifest['pages'] + manifest['assets']:
+ source = checked_path(root, item['source'])
+ if not source.is_file():
+ raise ValueError('Missing source: ' + item['source'])
+ body = source.read_bytes()
+ if source.suffix == '.md':
+ text = rewrite_markdown(body.decode('utf-8'), item['source'], manifest, source_commit)
+ url = 'https://github.com/' + REPOSITORY + '/blob/' + source_commit + '/' + quote(item['source'], safe='/')
+ body = (GENERATED + '\n\n' + text.rstrip() + '\n\n---\n\n[Edit this page in the source repository](' + url + ')\n').encode()
+ files[item['destination']] = body
+ sidebar, group = [], None
+ for item in manifest['pages']:
+ if item['group'] != group:
+ group = item['group']
+ sidebar.extend(['', '**' + group + '**', ''])
+ sidebar.append('- [' + item['title'] + '](' + page_url(item['destination']) + ')')
+ files['_Sidebar.md'] = ('\n'.join(sidebar).strip() + '\n').encode()
+ files['_Footer.md'] = ('Documentation is generated from the [source repository](https://github.com/' + REPOSITORY + '). Edit the original Markdown, not the wiki.\n').encode()
+ files[OWNERSHIP] = ownership(files, source_commit)
+ return files
+
+
+def prepare(root, manifest, source_commit, output):
+ root, output = Path(root).absolute(), Path(output).absolute()
+ if output != root / 'target/wiki-preview':
+ raise ValueError('Preview output must be target/wiki-preview')
+ checked_path(root, 'target/wiki-preview')
+ files = generate(root, manifest, source_commit)
+ validate_preview(files, manifest)
+ output.mkdir(parents=True, exist_ok=True)
+ from wiki_sync import plan_changes, apply_changes, validate_ownership
+ prior_path = checked_path(output, OWNERSHIP)
+ prior = json.loads(prior_path.read_text(encoding='utf-8')) if prior_path.is_file() else None
+ if prior is not None:
+ validate_ownership(prior)
+ allowed = set(files) | (set(prior['files']) if prior else set())
+ for path in output.rglob('*'):
+ name = path.relative_to(output).as_posix()
+ checked_path(output, name)
+ if path.is_file() and name not in allowed:
+ raise ValueError('Unlisted file in preview; left untouched: ' + name)
+ apply_changes(output, plan_changes(output, files, prior, set()))
+ return files
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('command', choices=('prepare', 'check'))
+ parser.add_argument('--source-commit', required=True)
+ args = parser.parse_args()
+ manifest = load_manifest(ROOT / 'scripts/documentation/wiki-manifest.json')
+ files = prepare(ROOT, manifest, args.source_commit, ROOT / 'target/wiki-preview')
+ print(f'Prepared {len(files)} local wiki files; nothing published.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/documentation/wiki_sync.py b/scripts/documentation/wiki_sync.py
new file mode 100644
index 0000000..9ce28df
--- /dev/null
+++ b/scripts/documentation/wiki_sync.py
@@ -0,0 +1,108 @@
+"""Ownership-safe local wiki reconciliation. No credentials or public Git transport."""
+import hashlib
+import json
+from pathlib import Path
+import re
+
+from wiki import OWNERSHIP, REPOSITORY, checked_path, linked
+
+
+def output_name(name, metadata=False):
+ if not isinstance(name, str) or not (
+ re.fullmatch(r'[A-Za-z0-9-]+\.md', name)
+ or name in ('_Sidebar.md', '_Footer.md')
+ or re.fullmatch(r'images/[A-Za-z0-9-]+\.png', name)
+ or (metadata and name == OWNERSHIP)
+ ):
+ raise ValueError('Invalid managed wiki filename: ' + repr(name))
+
+
+def validate_ownership(record):
+ if not isinstance(record, dict) or type(record.get('schema')) is not int or record['schema'] != 1 or record.get('repository') != REPOSITORY:
+ raise ValueError('Invalid wiki ownership scope/schema')
+ if not re.fullmatch(r'[0-9a-f]{40}', record.get('source_commit', '')):
+ raise ValueError('Invalid ownership source commit')
+ if not isinstance(record.get('files'), dict) or not 1 <= len(record['files']) <= 512:
+ raise ValueError('Invalid owned files')
+ seen = set()
+ for name, digest in record['files'].items():
+ output_name(name)
+ if name.casefold() in seen or not isinstance(digest, str) or not re.fullmatch('[0-9a-f]{64}', digest):
+ raise ValueError('Invalid/colliding owned file identity')
+ seen.add(name.casefold())
+
+
+def digest(path):
+ if path.exists() and not path.is_file():
+ raise ValueError('Managed path is not a regular file: ' + str(path))
+ return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
+
+
+def plan_changes(checkout, generated, previous_manifest, adopt):
+ checkout = Path(checkout).absolute()
+ if linked(checkout) or not checkout.is_dir():
+ raise ValueError('Wiki checkout must be a real directory')
+ if OWNERSHIP not in generated:
+ raise ValueError('Missing generated ownership')
+ next_record = json.loads(generated[OWNERSHIP])
+ validate_ownership(next_record)
+ if set(next_record['files']) != set(generated) - {OWNERSHIP}:
+ raise ValueError('Generated file list does not match ownership')
+ for name, body in generated.items():
+ output_name(name, metadata=True)
+ checked_path(checkout, name)
+ if not isinstance(body, bytes) or (name != OWNERSHIP and hashlib.sha256(body).hexdigest() != next_record['files'][name]):
+ raise ValueError('Generated bytes do not match ownership')
+ record_path = checked_path(checkout, OWNERSHIP)
+ if record_path.exists():
+ actual = json.loads(record_path.read_text(encoding='utf-8'))
+ if actual != previous_manifest:
+ raise ValueError('Existing ownership must be explicitly loaded and validated')
+ elif previous_manifest is not None:
+ raise ValueError('Missing ownership record')
+ elif any(p.is_file() and b'