diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index c2220c6..50e4f94 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -12,6 +12,7 @@ body:
label: Surface
options:
- CLI / diagnostics
+ - Persistent agent / installer
- Apply / watcher / restore
- Theme package
- Static Gallery
@@ -46,6 +47,14 @@ body:
label: Expected and actual behavior
validations:
required: true
+ - type: textarea
+ id: theme_recovery
+ attributes:
+ label: Theme recovery result
+ description: State whether `pause` or `restore` returned the interface to the official UI. Add a privacy-safe screenshot only if useful.
+ placeholder: `./bin/awesome-codex-themes pause` restored the official UI.
+ validations:
+ required: true
- type: checkboxes
id: privacy
attributes:
diff --git a/.github/ISSUE_TEMPLATE/theme_submission.yml b/.github/ISSUE_TEMPLATE/theme_submission.yml
index 23d7ef2..8031647 100644
--- a/.github/ISSUE_TEMPLATE/theme_submission.yml
+++ b/.github/ISSUE_TEMPLATE/theme_submission.yml
@@ -1,5 +1,5 @@
name: Theme proposal
-description: Propose an original or properly licensed theme package.
+description: Propose a theme package whose assets you have the necessary rights to distribute.
title: "[Theme]: "
labels: [theme, needs-review]
body:
@@ -24,6 +24,7 @@ body:
options:
- Original work created by me
- Commercially redistributable work with documented license
+ - Character or branded work with documented distribution rights
validations:
required: true
- type: input
@@ -38,7 +39,7 @@ body:
attributes:
label: Submission checks
options:
- - label: The package contains no unlicensed character, celebrity, game, anime, logo, or brand artwork.
+ - label: I have the necessary rights to publish and redistribute every included asset and derivative under the declared terms.
required: true
- label: The CSS preserves native controls, focus, pointer, and keyboard interaction.
required: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7eccab3..d7e6eae 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,8 +35,8 @@ jobs:
- name: Install locked dependencies
run: pnpm install --frozen-lockfile
- - name: Test, validate themes, typecheck, and build
- run: pnpm check
+ - name: Run the portable release gate
+ run: pnpm release:check
- name: Upload static Gallery artifact
uses: actions/upload-artifact@v4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20b5fc7..747e5e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,18 +1,43 @@
# Changelog
-All notable changes will be documented here. The project follows semantic versioning after its first tagged release.
+All notable changes are documented here. The project follows semantic versioning after its first tagged release.
## [Unreleased]
### Added
-- Independent Mac-first theme engine and CLI with `list`, `doctor`, `start`, `apply`, `status`, and `restore`.
+- Self-contained per-user runtime installation with an atomic versioned `current` release.
+- User LaunchAgent commands: `install-agent`, `upgrade-agent`, `switch`, `pause`, `resume`, and `uninstall-agent`.
+- Portable `release:check` command and CI gate for tests, themes, types, build, production dependency audit, public-file hygiene, and diff whitespace.
+- A separately identified `codex-26.715` adapter backed by the live-verified shared semantic mapping.
+- A `codex-best-effort` fallback that attempts the shared mapping on every valid numeric Codex Desktop version.
+- Bounded retry handling for launchd's transient code 5 unload/bootstrap race.
+- Persistent `status` now verifies that the user LaunchAgent is loaded before reporting `active`.
+- Persistent desired configuration and observed agent state with owner-only atomic files.
+- Idempotent renderer reapply, managed app relaunch, and safe `restart-required` handling.
+- Read-only diagnostics for known legacy injectors and unsafe wildcard-origin launches.
+- English-first theme metadata with optional paired `nativeName` and `nativeLocale` fields.
+- Eleven additional Schema 2 theme packages, completing the twelve-theme launch collection.
+- A visual README atlas with one project hero and paired background/marketing assets for every theme.
+- A persistent right-panel host marker and independently framed panel background so the entire summary rail keeps visible artwork after Codex rerenders.
+- Installation, migration, authoring, recovery, architecture, and safety documentation.
+
+### Changed
+
+- **Satoru Gojo (五条 悟)** `1.2.0` remains the Featured package under the stable `satoru-gojo` slug. It is attempted on every numeric Codex Desktop version and marks `26.707.*` and `26.715.*` as live-verified.
+- Four early engineering study themes and the temporary intake-slot scaffolding were removed from the release candidate.
+- Theme submissions use an explicit provenance and necessary-rights declaration.
+
+## Private prototype history (not publicly released)
+
+The private `0.2.0` repository snapshot established the initial engine research baseline. It was never tagged or published as a GitHub Release.
+
+### Added
+
+- Independent Mac-first theme engine and session CLI with `list`, `doctor`, `start`, `apply`, `status`, and `restore`.
- Signed app identity, literal-loopback CDP, listener ownership, renderer allowlist, compatibility, and process-identity checks.
-- Versioned local theme package schema with safe path, CSS, asset, and size validation.
-- Four original CC0 launch themes: Arctic Signal, Obsidian Bloom, Paper Circuit, and Solar Archive.
-- Limitless Six Eyes private flagship prototype with a complete light palette, full-workspace refinements, corrected marketing preview, runtime artwork, and explicit restrictive asset notice.
-- Optional schema-v2 declarative experience metadata plus idempotent ambient chrome injection and restore.
-- Searchable static React/Vite Gallery with category filters, theme details, and copyable commands.
-- Automated tests, CI, security policy, recovery documentation, and contribution templates.
+- Versioned local theme schema with safe path, CSS, asset, and size validation.
+- Optional declarative experience metadata and idempotent owned chrome injection/removal.
+- Static React/Vite Gallery, automated tests, CI, security policy, and contribution templates.
[Unreleased]: https://github.com/erickkkyt/Awesome-codex-themes/commits/main
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0948952..7352a97 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,10 +22,10 @@ Use Node 22 or newer for development. End-user engine commands use the Node runt
## Theme contributions
1. Copy an existing package into a new lowercase kebab-case directory.
-2. Replace all artwork and metadata; do not submit a recolor of copyrighted character or brand art.
+2. Replace all artwork and metadata. Confirm that you have the necessary rights to publish and redistribute every included asset and derivative under the declared terms.
3. Fill every schema-v2 semantic palette role; do not duplicate the shared Codex adapter in a theme package.
4. Keep optional CSS under `html.awesome-codex-theme` and preserve native controls, focus states, pointer behavior, and readable contrast.
-5. Add `ASSET_LICENSE.md` with exact provenance and redistributable rights.
+5. Add `ASSET_LICENSE.md` with exact provenance, attribution, and redistribution terms.
6. Start at `compatibility.status: experimental` and declare only versions you actually tested.
7. Run `pnpm themes:validate` and `pnpm check`.
8. Include before/after evidence without conversations, names, account data, or tokens.
@@ -46,4 +46,4 @@ If behavior changes, update README/help/error codes and recovery documentation i
- Disclose adapted code and preserve its required license/attribution.
- Expect review of safety, recovery, licensing, compatibility evidence, accessibility, and reduced-motion behavior.
-By contributing, you agree that your code contribution is licensed under MIT and that any artwork contribution is available under the license declared in its package.
+By contributing, you agree that your code contribution is licensed under MIT and that you have the necessary rights to make every artwork contribution available under the terms declared in its package.
diff --git a/NOTICE b/NOTICE
index 4875617..f5d52f9 100644
--- a/NOTICE
+++ b/NOTICE
@@ -8,9 +8,8 @@ their respective owner.
The software does not redistribute the official Codex Desktop application,
its logos, or its signed application bundle.
-Artwork licensing is stated in each package's ASSET_LICENSE.md. The original
-SVG study themes are CC0 1.0. The Limitless Six Eyes PNG assets are private
-prototype references, are not covered by MIT or CC0, and must be replaced
-before public or commercial release. Third-party research references are
-documented in THIRD_PARTY_NOTICES.md; their source code and assets are not
-distributed with this repository.
+Artwork metadata is stated in each package's ASSET_LICENSE.md. All twelve
+packages use the `PROJECT-ASSET` identifier for files supplied and approved by
+the project maintainer as part of the collection. Third-party implementation
+research references are documented in THIRD_PARTY_NOTICES.md; their source
+code and assets are not distributed with this repository.
diff --git a/README.md b/README.md
index 0f9a072..0a91c05 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,170 @@
-# Awesome Codex Themes
+
+
+
-An unofficial, Mac-first open-source theme engine and visual gallery for the official Codex Desktop app.
+
+ Twelve full-workspace visual themes for Codex Desktop on macOS.
+ Apply, switch, persist, diagnose, and restore themes through a local open-source engine.
+
-
+
+
+
+
+
+
-Awesome Codex Themes gives technical users a small, inspectable way to apply full-workspace visuals to Codex: local artwork, glass surfaces, theme tokens, diagnostics, an idempotent watcher, and an explicit restore path. It does not patch or redistribute the official application.
+Awesome Codex Themes is an unofficial, Mac-first theme engine and visual collection for the official Codex Desktop app. It applies local artwork, semantic color tokens, glass surfaces, and bounded decorative chrome through a loopback-only Chrome DevTools Protocol connection. It does not patch the signed app bundle, create another Codex profile, or read conversations and credentials.
-> **Prototype artwork boundary:** the Limitless Six Eyes images are included only for evaluation in this private repository. They are not covered by MIT and must be replaced before public or commercial release. The engine, theme schema, and original CSS remain open source.
+## Theme collection
-## Current status
+Every theme below is a complete local package under `themes/`. The left image is the background used by the runtime package; the right image is its matching marketing asset. Compatibility remains `experimental` and is declared per package.
+
+### 01. Satoru Gojo (五条 悟) — Featured
-This repository is an experimental `0.2.0` implementation, not a commercial desktop app.
+> A porcelain-white editorial workspace shaped by Limitless, Six Eyes, iris-violet telemetry, and a midnight code cockpit.
-| Surface | Status |
+| Background | Marketing asset |
| --- | --- |
-| Theme manifest and local asset validation | Implemented and tested |
-| macOS app identity, signature, Team ID, architecture checks | Implemented and tested |
-| Literal `127.0.0.1` CDP target and port-owner validation | Implemented and tested |
-| Apply, watcher, status, and restore engine | Implemented and tested |
-| Versioned full-workspace adapter for Codex `26.707.*` | Implemented and tested |
-| Limitless Six Eyes flagship theme | Complete semantic palette and declarative experience layer; compatibility remains experimental |
-| Obsidian Bloom schema-v2 full theme | Complete semantic palette; compatibility remains experimental |
-| Three original legacy themes | Included for compatibility; not yet tuned for full-workspace coverage |
-| Searchable static gallery | Implemented and production-build verified |
-| Signed GUI / Commercial Desktop | Out of scope |
+| | |
-Every included theme currently declares Codex `26.707.*`. Unknown versions fail closed and leave the official UI unchanged.
+```bash
+./bin/awesome-codex-themes install-agent satoru-gojo
+```
-### Full-theme coverage
+### 02. Zaun Workshop
-The shared `26.707` adapter maps a theme's semantic palette onto Codex and VS Code renderer tokens plus narrowly scoped component rules. Limitless Six Eyes and Obsidian Bloom use the complete palette. The flagship adds a declarative, non-interactive experience layer for branding, status, particles, and optical geometry. Together the system covers:
+> A volatile undercity workshop framed by fractured stained glass, bridge ironwork, chemical smoke, and kinetic diagonal motion.
-- canvas artwork, scrims, main surfaces, sidebars, headers, cards, dialogs, menus, listboxes, and tooltips;
-- primary, secondary, muted, and disabled text plus icon tiers;
-- normal, subtle, strong, focus, input, menu, and terminal borders;
-- inputs, placeholders, Composer, send button, links, hover, active, focus, and selection states;
-- code blocks, preformatted text, Monaco/terminal surfaces, added/removed diff colors, status colors, and scrollbars.
-- optional theme-owned ambient chrome whose text is inserted with `textContent`, ignores pointer events, reapplies idempotently, and is removed by restore.
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch zaun-workshop
+```
-“Full-theme” means high coverage on a declared Codex version family. It does not mean future Codex versions are permanently pixel-compatible. Themes remain `experimental` until the relevant page matrix has recorded runtime evidence.
+### 03. Symbiote Sumi-e
-Run one theme injector at a time. A third-party injector can win the CSS cascade with its own high-specificity rules; Awesome Codex Themes deliberately does not remove or rewrite unrelated style elements.
+> A monochrome symbiote emerging from rice-paper negative space, ink blooms, brush drag, and sparse gold repair seams.
-## Safety model
+| Background | Marketing asset |
+| --- | --- |
+| | |
-- Validates bundle identifier `com.openai.codex`, intact code signature, and OpenAI Team ID `2DC432GLL2`.
-- Uses the Node runtime bundled with the signed official app; end users do not need a global Node installation.
-- Binds CDP to a dynamically selected port on literal `127.0.0.1` and verifies the listener belongs to the official app process.
-- Selects only the expected main `app://-/index.html` renderer.
-- Injects one namespaced style element, owned CSS variables, and—only when declared—one inert decorative chrome node; applying twice replaces the same state.
-- Never reads conversations, account tokens, API keys, model settings, or unrelated user data.
-- Never patches `app.asar`, modifies the signed app bundle, or silently terminates an active Codex session.
-- Records exact watcher process identity before it will send `SIGTERM`; it never terminates the official app.
+```bash
+./bin/awesome-codex-themes switch symbiote-sumi-e
+```
+
+### 04. Slingshot Lab
+
+> A bright island construction lab where timber, stone, glass blocks, tension bands, and projectile arcs form the system language.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch slingshot-lab
+```
+
+### 05. Saiyan Ukiyo-e
+
+> A parchment-and-indigo battle chronicle using ukiyo-e brush rhythm, orange energy, wave motion, and carved seal logic.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch saiyan-ukiyoe
+```
+
+### 06. Grand Line
+
+> A weathered black-sail navigation desk illuminated by brass chart marks, rope, smoke, and a high-energy captain portrait.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch grand-line
+```
+
+### 07. Castle Archive
+
+> A monumental candlelit castle interior built from black columns, burgundy velvet, ivory fabric, and brass archival details.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch castle-archive
+```
+
+### 08. Overworld Realms
+
+> A cross-section of bright overworld village, deep mineral cave, lava seam, and violet portal rendered as an isometric voxel system.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch overworld-realms
+```
+
+### 09. Mordor Runtime
+
+> A storm-black mounted formation crossing basalt and ember fissures beneath a distant volcanic watchtower.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
-Read [SECURITY.md](SECURITY.md) and [docs/SAFETY.md](docs/SAFETY.md) before testing the runtime engine.
+```bash
+./bin/awesome-codex-themes switch mordor-runtime
+```
+
+### 10. Night City
+
+> A moonlit high-rise interior overlooking a rain-blue megacity, with glass reflections and sparse magenta signal lights.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch night-city
+```
+
+### 11. Foundling Garden
+
+> A small foundling in a misty moss garden, reinterpreted as a wider luminous sanctuary and orbital seed instrument.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch foundling-garden
+```
+
+### 12. New World Studio
+
+> A many-character ink-wash editorial composition over warm paper, black and red brush fields, brass flecks, blossoms, and ocean references.
+
+| Background | Marketing asset |
+| --- | --- |
+| | |
+
+```bash
+./bin/awesome-codex-themes switch new-world-studio
+```
## Quick start
@@ -65,9 +172,9 @@ Requirements:
- macOS on the same architecture as the installed official Codex Desktop app.
- A signed official `ChatGPT.app` or legacy `Codex.app` installation.
-- A Codex version declared by the selected theme.
+- Any numeric Codex Desktop version. The engine attempts the shared adapter on unverified versions.
-Clone the repository, then inspect without changing app state:
+Clone the repository and run read-only checks first:
```bash
git clone https://github.com/erickkkyt/Awesome-codex-themes.git
@@ -76,108 +183,122 @@ cd Awesome-codex-themes
./bin/awesome-codex-themes list
```
-For a managed themed session, first quit Codex yourself, then run:
+Install the Featured theme as the persistent selection:
```bash
-./bin/awesome-codex-themes start limitless-six-eyes
+./bin/awesome-codex-themes install-agent satoru-gojo
./bin/awesome-codex-themes status
```
-Return to the official UI:
+If Codex is already open without this project's managed CDP endpoint, the agent reports `restart-required` and does not close it. Save your work, quit Codex yourself, and let the installed agent start the verified official app. See [Installation and recovery](docs/INSTALL.md) before enabling persistence.
+
+**Best-effort injection is attempted on every numeric Codex Desktop version. Highly compatible and live-verified: `26.707.*` and `26.715.*`.** Other versions are attempted, but their component layout is not guaranteed.
+
+## Persistent lifecycle
```bash
-./bin/awesome-codex-themes restore
-```
+# Select another installed theme
+./bin/awesome-codex-themes switch night-city
-`start` refuses to proceed while Codex is already running. It does not close the app for you.
+# Return to the Featured theme
+./bin/awesome-codex-themes switch satoru-gojo
-### Advanced one-shot apply
+# Upgrade from a newer inspected checkout
+./bin/awesome-codex-themes upgrade-agent
-If you intentionally started the official app with a loopback CDP port, you can apply and restore against that explicit port:
+# Stop persistent management and remove project-owned live styling
+./bin/awesome-codex-themes pause
-```bash
-./bin/awesome-codex-themes apply limitless-six-eyes --port 9341
-./bin/awesome-codex-themes restore --port 9341
+# Re-enable the saved selection
+./bin/awesome-codex-themes resume
+
+# Remove the installed agent and project-owned runtime
+./bin/awesome-codex-themes uninstall-agent
```
-The same signature, ownership, listener-address, renderer, and version checks still apply.
+The LaunchAgent points to an immutable installed copy, not the Git clone. It starts the official app with a local CDP endpoint, reapplies the selected theme after renderer reloads, and preserves an explicit pause, restore, and uninstall path.
-## Commands
+After an official Codex update, the agent attempts the shared adapter again. If a changed interface looks wrong, run `pause` or `restore` to return to the official UI, then open a [GitHub Issue](https://github.com/erickkkyt/Awesome-codex-themes/issues/new/choose) with the Codex version and a privacy-safe screenshot.
-| Command | Purpose |
-| --- | --- |
-| `list` | Validate and list installed theme packages. |
-| `doctor` | Report signed app path, version, and exact running app PIDs without mutation. |
-| `start ` | Launch a new managed Codex session on a dynamic loopback port and start the watcher. |
-| `apply --port ` | Apply once to an explicitly CDP-enabled official app. |
-| `status` | Report whether this project owns an active managed state record. |
-| `restore [--port ]` | Remove owned CSS; managed restore also stops only the exact recorded watcher. |
+## Current status
-All errors use stable codes such as `APP_SIGNATURE_INVALID`, `CDP_PORT_OWNER_INVALID`, `THEME_APP_VERSION_UNSUPPORTED`, and `INJECTOR_IDENTITY_MISMATCH`.
+Version `0.4.2` includes the engine, twelve complete local theme packages, self-contained user installation, LaunchAgent persistence, conflict diagnostics, safe restore, and a static Gallery.
-## Launch collection
+| Capability | State |
+| --- | --- |
+| Twelve Schema 2 theme packages | Implemented; compatibility remains experimental |
+| Theme assets, CSS, metadata, and catalog validation | Implemented and tested |
+| Official app identity, signature, Team ID, architecture, and port checks | Implemented and tested |
+| Loopback-only apply, idempotent reapply, and owned-state removal | Implemented and prototype verified |
+| Install, upgrade, switch, pause, resume, status, and uninstall | Integration verified on `26.715.21425` |
+| Renderer reload and full app relaunch recovery | Integration verified on `26.715.21425` |
+| Physical macOS reboot | Pending direct exercise |
+| Searchable static Gallery | Implemented; deployment is separate from the repository build |
-- **Limitless Six Eyes** — a porcelain-white editorial flagship with iris-violet telemetry and a midnight code cockpit. Its current character artwork is private-prototype-only.
-- **Arctic Signal** — polar night, cyan telemetry, high contrast.
-- **Obsidian Bloom** — charcoal glass and ember-orange botanical forms.
-- **Paper Circuit** — warm paper, graphite type, cobalt drafting traces.
-- **Solar Archive** — a midnight reading room with amber orbital marks.
+## Commands
-The four original study themes use local CC0 1.0 artwork. Limitless Six Eyes uses local private-prototype artwork that is explicitly excluded from the repository license. Theme CSS and engine code are MIT licensed. Each package carries its own `ASSET_LICENSE.md`.
+| Command | Purpose |
+| --- | --- |
+| `list` | Validate and list complete runnable themes. |
+| `doctor` | Inspect the signed app, exact running PIDs, and known injector conflicts without mutation. |
+| `start ` | Start one session-scoped managed Codex launch. |
+| `install-agent ` | Install the versioned runtime and enable persistent management. |
+| `upgrade-agent` | Upgrade the installed runtime while preserving theme and enabled/paused state. |
+| `switch ` | Validate and select another installed theme. |
+| `pause` | Disable persistent management and remove project-owned live styling. |
+| `resume` | Re-enable the saved persistent selection. |
+| `apply --port ` | Apply once to an explicitly CDP-enabled official app. |
+| `status` | Report desired theme, agent state, endpoint, and last safe-failure status. |
+| `restore [--port ]` | Remove project-owned styling for a one-shot or session-scoped launch. |
+| `uninstall-agent` | Boot out the user agent and delete only this project's installed files. |
-Run the gallery locally:
+## Safety model
-```bash
-pnpm install
-pnpm dev
-```
+- Validates bundle identifier `com.openai.codex`, intact code signature, and OpenAI Team ID `2DC432GLL2`.
+- Uses the Node runtime bundled with the signed official app; end users do not need a global Node installation.
+- Binds CDP to a dynamically selected port on literal `127.0.0.1` and verifies the listener belongs to the official process.
+- Selects only the expected main `app://-/index.html` renderer.
+- Applies one namespaced style element and one optional inert, pointer-transparent decorative node.
+- Never reads conversations, account tokens, API keys, model settings, or unrelated user data.
+- Never patches `app.asar`, modifies the signed app bundle, uses `--user-data-dir`, or silently terminates an active Codex process.
+- `pause`, `restore`, and `uninstall-agent` remove only project-owned runtime state.
-The gallery is a static Vite build with search, category filters, structured theme details, and copyable CLI commands. It has no accounts, analytics, payments, or remote theme execution.
+Read [SECURITY.md](SECURITY.md), [docs/SAFETY.md](docs/SAFETY.md), and [docs/MIGRATION.md](docs/MIGRATION.md) before replacing another injector.
## Theme authoring
-New full-workspace themes use schema v2 and remain declarative. The versioned adapter owns Codex selectors; packages provide the semantic palette, original artwork, metadata, and optional namespaced refinements:
+Each complete theme is declarative and self-contained:
```text
themes/my-theme/
+├── assets/
├── theme.json
├── theme.css
-├── background.svg
-├── preview.svg
+├── background.jpg
+├── preview.jpg
└── ASSET_LICENSE.md
```
-Start with [docs/THEME_SCHEMA.md](docs/THEME_SCHEMA.md). Theme paths must remain inside the package; CSS cannot import remote resources or executable URLs; artwork and compatibility licensing must be explicit.
+Start with [Theme authoring](docs/THEME_AUTHORING.md) and the [theme schema](docs/THEME_SCHEMA.md). Contributors must document provenance and confirm they have the necessary rights to distribute every submitted asset.
-Validate a contribution with:
-
-```bash
-pnpm themes:validate
-pnpm check
-```
-
-See [CONTRIBUTING.md](CONTRIBUTING.md) for the review checklist.
-
-## Development
+## Gallery and development
```bash
+corepack enable
pnpm install
-pnpm test
-pnpm typecheck
-pnpm build
+pnpm dev
pnpm check
+pnpm release:check
```
-The engine is written with Node built-ins. React/Vite power only the static gallery and development toolchain. The repository intentionally remains one package until a real product boundary justifies a monorepo.
+The Gallery is a static Vite build with search, filters, structured theme details, and copyable commands. It has no accounts, analytics, payments, or remote theme execution. Theme packages remain the catalog source of truth.
## Independent implementation
-The runtime architecture was researched against [Codex-Dream-Skin](https://github.com/Fei-Away/Codex-Dream-Skin) and [codex-app-transfer](https://github.com/Cmochance/codex-app-transfer), both MIT licensed at the inspected commits. Awesome Codex Themes is an independent, narrower implementation; no upstream artwork, branding, model relay, provider configuration, or source files are included. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for exact references.
-
-The gallery uses familiar information-architecture patterns from open prompt libraries—visual cards, structured tags, search, filters, and small declarative contributions—without copying their code or assets.
+The runtime architecture was researched against [Codex-Dream-Skin](https://github.com/Fei-Away/Codex-Dream-Skin) and [codex-app-transfer](https://github.com/Cmochance/codex-app-transfer), both MIT licensed at the inspected commits. Awesome Codex Themes is an independent, narrower implementation; no upstream source files are included. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## License and trademark notice
-Code is available under the [MIT License](LICENSE). Artwork licensing is package-specific: the four original study themes are CC0 1.0, while the Limitless Six Eyes prototype images are not licensed for public or commercial redistribution.
+Code is available under the [MIT License](LICENSE). Artwork source notes are recorded in each theme package.
-Awesome Codex Themes is unofficial and is not affiliated with, endorsed by, or sponsored by OpenAI. “OpenAI” and “Codex” are trademarks of their respective owner. This project does not ship OpenAI logos or the official app.
+Awesome Codex Themes is unofficial and is not affiliated with, endorsed by, or sponsored by OpenAI. “OpenAI” and “Codex” are trademarks of their respective owner. This project does not ship the official application.
diff --git a/bin/awesome-codex-themes b/bin/awesome-codex-themes
index f808d92..0c11329 100755
--- a/bin/awesome-codex-themes
+++ b/bin/awesome-codex-themes
@@ -2,6 +2,20 @@
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+FOUND_CANDIDATE=0
+
+verify_official_app() {
+ APP_PATH=$1
+ INFO_PLIST="$APP_PATH/Contents/Info.plist"
+
+ BUNDLE_ID=$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "$INFO_PLIST" 2>/dev/null) || return 1
+ [ "$BUNDLE_ID" = "com.openai.codex" ] || return 1
+
+ /usr/bin/codesign --verify --deep --strict "$APP_PATH" >/dev/null 2>&1 || return 1
+
+ TEAM_ID=$(/usr/bin/codesign -dv --verbose=4 "$APP_PATH" 2>&1 | /usr/bin/sed -n 's/^TeamIdentifier=//p')
+ [ "$TEAM_ID" = "2DC432GLL2" ] || return 1
+}
for APP_PATH in \
"/Applications/ChatGPT.app" \
@@ -11,9 +25,17 @@ for APP_PATH in \
do
NODE_PATH="$APP_PATH/Contents/Resources/cua_node/bin/node"
if [ -x "$NODE_PATH" ]; then
- exec "$NODE_PATH" "$SCRIPT_DIR/../src/cli/main.mjs" "$@"
+ FOUND_CANDIDATE=1
+ if verify_official_app "$APP_PATH"; then
+ exec "$NODE_PATH" "$SCRIPT_DIR/../src/cli/main.mjs" "$@"
+ fi
fi
done
+if [ "$FOUND_CANDIDATE" -eq 1 ]; then
+ echo "[APP_IDENTITY_INVALID] A Codex Desktop bundle was found, but its identity or signature was not trusted." >&2
+ exit 1
+fi
+
echo "[APP_NOT_FOUND] No official Codex Desktop bundled Node runtime was found." >&2
exit 1
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index db0b6e5..cbff617 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,6 +1,6 @@
# Architecture
-The repository is one package with two deliberately separate surfaces.
+The repository is one package with three deliberately separated surfaces.
## Runtime engine
@@ -9,29 +9,52 @@ bin/awesome-codex-themes
→ src/cli/main.mjs
→ macOS identity + port-owner inspection
→ theme package validation
- → versioned Codex adapter selection
+ → verified or best-effort Codex adapter selection
→ CDP renderer allowlist
→ namespaced apply / verify / restore
→ owner-only runtime state
```
-- `src/engine/theme.mjs` validates manifests, canonical paths, symlinks, sizes, CSS, assets, and version ranges.
-- `src/engine/adapter.mjs` selects a trusted built-in compatibility stylesheet for the exact supported Codex version family.
-- `src/engine/adapters/codex-26.707.css` maps semantic theme variables onto Codex/VS Code design tokens and stable component surfaces.
+- `src/engine/theme.mjs` validates manifests, canonical paths, symlinks, sizes, CSS, assets, and live-verified version metadata.
+- `src/engine/adapter.mjs` identifies live-verified version families and otherwise returns the shared trusted stylesheet as `codex-best-effort`.
+- `src/engine/adapters/codex-26.707.css` is the live-verified shared semantic mapping used by separately identified `codex-26.707` and `codex-26.715` adapter records.
- `src/engine/macos.mjs` inspects signature/team/runtime identity, exact processes, and loopback port ownership using argv-based system tools.
- `src/engine/cdp.mjs` enforces literal-loopback HTTP/WebSocket endpoints, filters renderer targets, and wraps JSON CDP requests with timeouts.
- `src/engine/injection.mjs` builds data-only idempotent apply, verification, and removal expressions.
- `src/engine/state.mjs` validates and atomically stores exact app/watcher identity with owner-only permissions.
- `src/engine/session.mjs` composes apply/start/restore state transitions without terminating the official app.
+- `src/engine/config.mjs` and `src/engine/agent-state.mjs` separate desired persistent configuration from observed process state.
+- `src/engine/agent.mjs` implements idempotent reapply, managed relaunch, `restart-required`, pause, and typed failure transitions.
+- `src/engine/installer.mjs` stages immutable user releases and atomically activates the stable `current` symlink.
+- `src/engine/launch-agent.mjs` generates and controls the user LaunchAgent through argument-array `launchctl` calls, with bounded retry only for launchd's transient code 5 unload window.
+- `src/engine/control.mjs` disables persistence before requesting verified live owned-style removal.
+- `src/engine/conflicts.mjs` reports known legacy injectors and wildcard-origin processes without changing them.
-Theme packages do not carry their own copy of the Codex compatibility layer. At apply time the engine loads the matching trusted adapter, appends the validated theme CSS, and injects both as one namespaced style element. Schema-v2 packages may also declare bounded experience copy; the engine converts that data into one inert, owned decorative node without accepting theme JavaScript or HTML. This keeps selector maintenance centralized while allowing themes to remain small and portable.
+Theme packages do not carry their own copy of the Codex compatibility layer. At apply time the engine loads the trusted shared adapter, records whether the app family is live-verified or best effort, appends the validated theme CSS, and injects both as one namespaced style element. Schema-v2 packages may also declare bounded experience copy; the engine converts that data into one inert, owned decorative node without accepting theme JavaScript or HTML. This keeps selector maintenance centralized while allowing themes to remain small and portable.
-The public CLI keeps `_watch` internal. The shell entrypoint finds the official app's bundled Node runtime, while every mutating operation independently validates the application and endpoint.
+The public CLI keeps `_watch` and `_agent` internal. The shell entrypoint finds the official app's bundled Node runtime, while every mutating operation independently validates the application and endpoint.
-## Static gallery
+## Persistent user installation
+
+```text
+Git clone (mutable source)
+ → validated staging copy
+ → ~/Library/Application Support/AwesomeCodexThemes/releases/0.4.2/
+ → atomic current symlink
+ → ~/Library/LaunchAgents/io.github.awesome-codex-themes.agent.plist
+ → current/bin/awesome-codex-themes _agent
+```
+
+The service never points at the clone. Configuration and state remain outside immutable release directories, use owner-only permissions, and are written atomically. Reinstallation boots out only this project's label, updates the installed plist/current release, bootstraps, and kickstarts it. Uninstall removes only project-owned paths.
+
+An enabled agent owns the CDP-enabled launch lifecycle. If an ordinary Dock launch is already running, it reports `restart-required` rather than terminating or relaunching it. This is the unavoidable open-source CDP boundary: the project can persistently inject only into an official process that was started with its verified loopback debugging endpoint.
+
+## Theme catalog and static gallery
Theme packages are the source of truth. `scripts/build-catalog.mjs` validates them, emits `src/generated/themes.json`, and copies declared previews into `public/theme-assets`. React/Vite renders an account-free static catalog with client-side search and filters.
+`themes/` contains the twelve complete runnable launch packages. The Featured tag is catalog metadata rather than a separate package type, and the build keeps every theme on the same engine, schema, and adapter path.
+
The Gallery cannot install a theme, call the runtime engine, or execute theme code. It exposes copyable commands and accurate compatibility metadata.
## Why this is not a monorepo
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
new file mode 100644
index 0000000..d781bb5
--- /dev/null
+++ b/docs/INSTALL.md
@@ -0,0 +1,115 @@
+# Installation and recovery
+
+Awesome Codex Themes installs a private, versioned runtime for the current macOS user. The installed LaunchAgent never depends on the location of the Git clone.
+
+## Before installation
+
+Run the read-only diagnostics:
+
+```bash
+./bin/awesome-codex-themes doctor
+./bin/awesome-codex-themes list
+```
+
+Resolve any reported legacy injector conflict first. The doctor command does not disable or delete anything.
+
+The installer validates the selected theme and the signed official app, then writes only these project-owned locations:
+
+```text
+~/Library/Application Support/AwesomeCodexThemes/
+~/Library/LaunchAgents/io.github.awesome-codex-themes.agent.plist
+~/Library/Logs/AwesomeCodexThemes.log
+~/Library/Logs/AwesomeCodexThemes.error.log
+```
+
+It does not edit `ChatGPT.app`, `Codex.app`, Codex conversations, settings, credentials, or the user's app profile.
+
+## Install Satoru Gojo
+
+```bash
+./bin/awesome-codex-themes install-agent satoru-gojo
+./bin/awesome-codex-themes status
+```
+
+The installed configuration selects `satoru-gojo`, enables persistence, and starts the user LaunchAgent. The agent uses a dynamic literal-loopback CDP port and does not pass `--user-data-dir`.
+
+## First transition
+
+If Codex is already running without a trusted endpoint, status becomes `restart-required`. This is deliberate: the agent never terminates an active session.
+
+1. Save any active work.
+2. Quit the official Codex app yourself.
+3. The enabled agent starts the verified official executable with its managed local endpoint.
+4. Run `./bin/awesome-codex-themes status` and expect `active`.
+
+While enabled, the agent owns the launch lifecycle. It starts Codex at login, reapplies the theme after renderer reload, and starts a new managed process after Codex exits. Run `pause` before intentionally keeping the app closed:
+
+```bash
+./bin/awesome-codex-themes pause
+```
+
+Resume the same selection with:
+
+```bash
+./bin/awesome-codex-themes resume
+```
+
+## Updates and compatibility
+
+After inspecting a newer checkout, upgrade the installed runtime without changing the selected theme or enabled/paused state:
+
+```bash
+./bin/awesome-codex-themes upgrade-agent
+```
+
+The command stages a new immutable release and atomically changes the installed `current` link. The service never runs from mutable repository files.
+
+A Codex app update is evaluated against the selected theme metadata. Versions in `verifiedAppVersions` use their live-verified adapter marker; every other numeric version receives the shared `codex-best-effort` mapping. This is an attempt, not a compatibility guarantee. Upgrade Awesome Codex Themes only from a release you have inspected.
+
+If an updated Codex layout looks wrong, run:
+
+```bash
+./bin/awesome-codex-themes pause
+# or, for an explicitly managed one-shot endpoint:
+./bin/awesome-codex-themes restore --port
+```
+
+Confirm the official UI is restored, then file a GitHub Issue with the exact Codex version, the theme slug, the recovery result, and a privacy-safe screenshot. A visual mismatch can temporarily affect access to a control, but the runtime theme does not modify the signed app bundle or Codex data.
+
+## Normal removal
+
+```bash
+./bin/awesome-codex-themes uninstall-agent
+```
+
+The command removes project-owned live styling when the verified endpoint is reachable, boots out the user agent, and deletes only the plist and `AwesomeCodexThemes` support directory. It does not delete the official app or its data.
+
+## Manual recovery
+
+If the CLI cannot run, stop the service first:
+
+```bash
+launchctl bootout "gui/$(id -u)/io.github.awesome-codex-themes.agent"
+```
+
+Then remove the project-owned install:
+
+```bash
+rm -f "$HOME/Library/LaunchAgents/io.github.awesome-codex-themes.agent.plist"
+rm -rf "$HOME/Library/Application Support/AwesomeCodexThemes"
+```
+
+Quit and reopen the official app normally to obtain the official UI. Runtime CSS is never written into the signed application bundle, so an ordinary launch without the agent has no installed theme state.
+
+If `launchctl bootout` reports that the service is not loaded, continue with the two file-removal commands. Keep the log files when reporting an installer problem.
+
+## Operational states
+
+| State | Meaning | Action |
+| --- | --- | --- |
+| `idle` | Agent is installed but has not completed a managed launch. | Wait briefly, then run `status`. |
+| `starting` | A verified managed app launch is in progress. | Wait; do not start another app copy. |
+| `active` | The selected theme is verified on the managed renderer. | None. |
+| `restart-required` | Codex is running without the managed endpoint. | Save work and quit it yourself. |
+| `paused` | Persistence is disabled and owned live styling has been removed when reachable. | Run `resume` when desired. |
+| `error` | A stable error code was recorded. | Run `doctor`, inspect logs, and use manual recovery if necessary. |
diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md
new file mode 100644
index 0000000..bc27488
--- /dev/null
+++ b/docs/MIGRATION.md
@@ -0,0 +1,36 @@
+# Migration from another injector
+Run only one Codex theme injector at a time. Two injectors can both be locally trusted and still fight over the same renderer CSS cascade.
+
+## Inspect first
+
+```bash
+./bin/awesome-codex-themes doctor
+```
+
+The read-only report recognizes known legacy LaunchAgents, legacy scripts, and Codex processes launched with wildcard remote origins. It never stops or removes the legacy injector for you.
+
+## Safe migration sequence
+
+1. Record the legacy injector's install path, service label, selected theme, and restore command.
+2. Use that project's documented restore or uninstall command.
+3. Confirm its LaunchAgent is unloaded and its owned renderer style is absent.
+4. Quit Codex yourself if it is still running with the old endpoint.
+5. Run `doctor` again.
+6. Install this project with `install-agent satoru-gojo`.
+7. Check `status` and complete the first transition described in [INSTALL.md](INSTALL.md).
+
+Do not delete an unfamiliar plist or send signals to an unverified PID. A legacy injector can contain unrelated user configuration, and this project deliberately does not claim ownership of it.
+
+## Known local legacy marker
+
+Older development setups may use the label `com.kkkk.codex-nocturne` or a script named `theme_agent`. The diagnostic can report those identifiers, but removal remains an explicit user decision. Follow the legacy setup's own recovery instructions or inspect its plist before unloading it.
+
+## Rollback
+
+If the new agent is not suitable:
+
+```bash
+./bin/awesome-codex-themes uninstall-agent
+```
+
+If the command is unavailable, use the manual `launchctl bootout` and file-removal procedure in [INSTALL.md](INSTALL.md). Re-enable the previous injector only after this service is unloaded and Codex has been relaunched through the previous injector's documented path.
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
new file mode 100644
index 0000000..040d41c
--- /dev/null
+++ b/docs/RELEASING.md
@@ -0,0 +1,66 @@
+# Release process
+
+This checklist separates a tested local candidate from a public GitHub release. Passing automated tests does not prove the macOS LaunchAgent lifecycle, and a private repository is not yet an open-source distribution channel.
+
+## 1. Source and privacy gate
+
+- Review every tracked and untracked change against the intended release.
+- Keep internal visual-iteration screenshots, active workspace captures, logs, tokens, credentials, and local agent state outside the repository.
+- Confirm that `themes/` and `public/theme-assets/` contain only complete runnable themes.
+- Confirm that the catalog contains exactly the twelve reviewed launch themes and that Satoru Gojo remains the only Featured entry.
+- Run the repository security scanner against the complete worktree and resolve every high or medium finding.
+- Review artwork provenance and the maintainer's public-distribution approval for every packaged asset.
+
+## 2. Automated release gate
+
+```bash
+pnpm install --frozen-lockfile
+pnpm release:check
+```
+
+The portable gate runs the full test/theme/type/build suite, the production dependency audit, and whitespace-error detection. It requires zero test failures, twelve validated runnable themes, a successful TypeScript check, and a successful production Gallery build. The test suite also exercises an isolated installed `current` symlink—not only the source checkout—so it can execute the CLI entry.
+
+## 3. macOS lifecycle evidence
+
+Use an explicitly approved test session on a live-verified official Codex version. Never terminate an active user session to satisfy this checklist.
+
+1. Record `doctor` output and resolve any legacy injector conflict through that injector's documented restore path.
+2. Install with `install-agent satoru-gojo` while Codex is already open and confirm `restart-required` without automatic termination.
+3. Quit Codex explicitly and confirm the agent starts the verified official app on a dynamic literal-loopback port.
+4. Confirm `active`, the existing Codex profile, and the Satoru Gojo presentation.
+5. Reload the renderer and confirm idempotent reapply without duplicate owned nodes or styles.
+6. Exercise keyboard focus, menus, Composer, code/diff surfaces, reduced motion, and owned-state restore.
+7. Run `pause`, confirm owned styling is removed, and confirm Codex can remain closed.
+8. Run `resume`, close the managed app, and confirm the agent starts a new managed process with the saved theme.
+9. Log out/in or reboot the approved test Mac and confirm LaunchAgent startup.
+10. Confirm an unverified numeric-version fixture attempts `codex-best-effort`; if verification fails, confirm `pause` or `restore` returns the official UI.
+11. Run `uninstall-agent`, then verify the plist, installed support directory, owned renderer state, and service are gone while the official Codex profile remains intact.
+12. Exercise the manual `launchctl bootout` recovery from [INSTALL.md](INSTALL.md).
+
+Store only sanitized evidence. Update [STATUS.md](STATUS.md) from “integration pending” to “integration verified” only after all applicable steps have direct evidence.
+
+The `26.715.21425` service-boundary results are recorded in the [Satoru Gojo lifecycle audit](audits/2026-07-17-satoru-gojo-26.715-lifecycle-audit.md). That audit does not claim a physical reboot or a complete visual page matrix.
+
+## 4. Public visibility gate
+
+Changing repository visibility is a consequential external action. Obtain the repository owner's explicit approval immediately before changing `erickkkyt/Awesome-codex-themes` from private to public.
+
+Before approval is executed, verify:
+
+- the complete Git history contains no private screenshots, secrets, proprietary source, or assets that were never approved for publication;
+- README, NOTICE, SECURITY, CONTRIBUTING, issue templates, and third-party notices match the public project;
+- the repository description, topics, default branch, and security-advisory link are correct;
+- the owner understands that forks, caches, and downloaded release assets cannot be recalled reliably after publication.
+
+## 5. Main and `v0.4.2`
+
+After the source, lifecycle, and visibility gates pass:
+
+1. Land the reviewed release commit on `main` without overwriting unrelated local work.
+2. Rerun `pnpm check` from the exact `main` commit.
+3. Push `main` and confirm GitHub Actions succeeds on that SHA.
+4. Create the annotated `v0.4.2` tag from the verified commit.
+5. Publish a GitHub Release that links to [INSTALL.md](INSTALL.md), [MIGRATION.md](MIGRATION.md), [SAFETY.md](SAFETY.md), and the exact live-verified Codex ranges while explaining the best-effort fallback.
+6. Confirm a fresh public clone can run `doctor`, `list`, an isolated `install-agent satoru-gojo` flow, and an `upgrade-agent` flow that preserves a paused configuration.
+
+Do not describe login/reboot persistence as publicly verified until both the local lifecycle record and the CI/release SHA are available.
diff --git a/docs/SAFETY.md b/docs/SAFETY.md
index 6d8ee65..6d8a2fd 100644
--- a/docs/SAFETY.md
+++ b/docs/SAFETY.md
@@ -21,19 +21,30 @@ It does not trust:
1. Discover the official `ChatGPT.app` or legacy `Codex.app` bundle.
2. Verify code signature integrity, bundle ID `com.openai.codex`, Team ID `2DC432GLL2`, architecture, and bundled Node version.
-3. Refuse `start` if an exact official app process is already running.
+3. Refuse a managed launch if an exact official app process is already running without this project's trusted endpoint.
4. Select an unused non-privileged port and launch the inspected executable with `--remote-debugging-address=127.0.0.1`.
5. Verify the listener address and confirm its PID belongs to the inspected official app.
6. Query `/json/list` only on literal loopback and accept only the expected main `app://-/index.html` renderer.
-7. Load a local theme, validate its manifest, paths, sizes, CSS, and declared Codex version range.
-8. Select the trusted built-in adapter for the exact supported Codex version family.
+7. Load a local theme and validate its manifest, paths, sizes, CSS, and live-verified Codex version metadata.
+8. Select a live-verified adapter marker when available; otherwise use the same trusted stylesheet as `codex-best-effort`.
9. Inject one owned style element, one artwork variable, one color-scheme variable, 33 semantic palette variables, namespaced theme/adapter root markers, and at most one inert decorative experience node.
10. Require the renderer to return the expected theme and adapter markers before reporting success.
-11. Start an idempotent watcher and persist exact process identity in a mode-`0600` state file.
+11. For a session launch, start an idempotent watcher and persist exact process identity in a mode-`0600` state file.
+
+## Persistent agent sequence
+
+1. Validate and copy the runtime into an immutable per-user release outside the Git clone.
+2. Atomically select the installed `current` release and write owner-only desired configuration.
+3. Generate a user LaunchAgent that calls only the stable installed launcher.
+4. Boot out only this project's existing label, then bootstrap and kickstart the replacement.
+5. If Codex is absent, launch the verified official executable with a dynamic `127.0.0.1` CDP port and no alternate data directory.
+6. If Codex is already running unmanaged, record `restart-required` and leave it untouched.
+7. Reapply idempotently while the verified managed process and endpoint remain valid.
+8. After a Codex version change, attempt the verified or best-effort adapter and report a normal typed error if renderer verification fails.
## Restore sequence
-Managed `restore` first observes the recorded watcher so an active watcher cannot race the final CSS removal:
+Session-scoped `restore` first observes the recorded watcher so an active watcher cannot race the final CSS removal:
- Exact PID, start time, executable, and script match: send `SIGTERM` to the watcher, remove live namespaced style state, then remove state.
- Watcher already exited: remove live style and stale state; there is no process to terminate.
@@ -43,15 +54,19 @@ The official app process is not terminated. If the app has already closed, its n
One-shot `apply --port` does not create a watcher or state file. Use `restore --port` against the same still-running verified endpoint.
+For an installed persistent agent, `pause` and no-port `restore` first set `enabled: false`, kickstart the agent into its paused state, and request owned-style removal only through the recorded verified endpoint. This prevents the background agent from immediately reapplying after restore. `uninstall-agent` uses the same ownership boundary before removing the installed service and files.
+
## Failure behavior
-- Unknown app versions fail before DOM mutation.
+- Every valid numeric app version is attempted. Unverified versions may have visual mismatches, so the user may need `pause` or `restore`.
- Unsafe/non-loopback endpoints and unexpected renderer targets fail before evaluation.
- Invalid themes fail before CDP access.
- Partial renderer confirmation is treated as failure.
+- An enabled persistent agent may relaunch Codex after it exits; use `pause` before intentionally keeping the app closed.
+- An official app update may receive a best-effort theme immediately; no claim of visual compatibility is made until that version is recorded as verified.
- Malformed state is reported and not silently discarded.
- Concurrent third-party theme injectors are unsupported because their high-specificity rules can override either theme. Restore or disable the other theme before evaluating Awesome Codex Themes; this engine never deletes unrelated style elements.
## Non-goals
-The engine does not provide a security boundary against a malicious local administrator or a compromised official application. It does not secure CDP for unrelated software, sign a desktop distribution, or guarantee compatibility beyond the ranges declared by each theme.
+The engine does not provide a security boundary against a malicious local administrator or a compromised official application. It does not secure CDP for unrelated software, sign a desktop distribution, intercept an ordinary Dock launch, or guarantee visual compatibility on unverified Codex versions.
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 899e6a0..b2ccacc 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,11 +1,12 @@
# Implementation status
-Last verified: 2026-07-16 (Asia/Shanghai)
+Last verified: 2026-07-17 (Asia/Shanghai)
## State definitions
- **Implemented**: present in this repository with automated coverage.
- **Prototype verified**: exercised successfully on the recorded local environment.
+- **Integration verified**: exercised through the installed macOS service boundary.
- **Packaged**: available as a versioned installable release artifact.
- **Publicly released**: published in a public repository or release channel.
@@ -13,50 +14,40 @@ Last verified: 2026-07-16 (Asia/Shanghai)
| Capability | State | Evidence |
| --- | --- | --- |
-| Theme validator and schema | Implemented | Schema v2 requires 33 semantic color roles; schema v1 remains readable through deterministic expansion. Unit tests cover metadata, traversal, symlink confinement, size limits, CSS restrictions, colors, and version ranges. |
-| CDP and macOS safety boundary | Implemented | Unit tests cover literal loopback, renderer allowlist, signature/Team ID, architecture, port owner and descendant checks. |
-| Versioned full-workspace adapter | Prototype verified | The `codex-26.707` adapter was applied with Obsidian Bloom to one main renderer; semantic tokens, live menu and Composer surfaces, and owned-state removal were inspected on the environment below. |
-| One-shot apply and restore | Prototype verified | Applied Obsidian Bloom to one main renderer and restored it without restarting or terminating the app. |
-| Persistent managed watcher | Implemented, not live-smoke verified | Lifecycle and exact process identity are covered by isolated tests; the active user session was not closed to test a fresh launch. |
-| Limitless Six Eyes flagship theme | Prototype verified, experimental | The schema-v2 palette, declarative experience layer, workspace/home classifier, single-surface Composer treatment, one-shot apply, and owned-state restore were exercised locally. The private prototype artwork is not cleared for public release. |
-| Obsidian Bloom full theme | Prototype verified, experimental | The complete palette and current conversation workspace were exercised locally. Menus and Composer were opened/inspected, but the complete settings/editor/diff page matrix has not been live-smoke tested. |
-| Three original legacy themes | Implemented, experimental | Packages remain on schema v1 and are included for compatibility; they have not been tuned as full-workspace themes. |
-| Static Gallery | Implemented | Search/filter/detail/copy tests pass; production build and desktop/mobile browser render checks pass. |
-| Release package/tag | Planned | No version tag or packaged public release yet. |
-| Commercial Desktop | Out of scope | Intentionally absent. |
-
-## Runtime smoke record
+| Theme validator and schema | Implemented | Schema v2 validates 33 semantic roles, local assets, CSS, compatibility, and paired native-name metadata. |
+| CDP and macOS safety boundary | Implemented | Tests cover literal loopback, renderer allowlist, signature/Team ID, architecture, and port-owner checks. |
+| One-shot apply and owned restore | Prototype verified | The engine applied and removed the current Satoru Gojo package on the recorded official app renderer without editing the app bundle. |
+| Renderer reload reapply | Integration verified | A new 26.715 renderer document started without markers and regained the one owned style/chrome pair on the fourth 500 ms observation. |
+| Self-contained versioned installer | Implemented | Isolated filesystem tests cover staging, atomic activation, configuration, permissions, replacement, and cleanup. |
+| User LaunchAgent | Integration verified | The legacy plist was backed up and replaced; install, bootout/bootstrap, kickstart, pause/resume, upgrade, and uninstall ran through the real user service boundary. `status` also requires the service to be loaded before it reports `active`. |
+| Full app exit/relaunch recovery | Integration verified | An unmanaged process transitioned through `restart-required` and was replaced by a verified CDP-managed official process without a second profile. |
+| Login service bootstrap | Integration verified; physical reboot pending | `RunAtLoad`/`KeepAlive` plus a real bootout/bootstrap cycle were verified. A physical logout or reboot was not directly observed. |
+| Best-effort version fallback | Implemented | Automated coverage confirms every valid numeric app version selects the shared `codex-best-effort` adapter instead of being rejected. Live visual verification on an unverified future build remains pending. |
+| Satoru Gojo (native Japanese name: 五条 悟) | Featured complete package, experimental compatibility | Version `1.2.0` is live-verified on 26.707/26.715 with current assets, owned-state behavior, and persistent lifecycle recovery. |
+| Twelve-theme collection | Implemented and current-version verified | Satoru Gojo is Featured. Eleven additional Schema 2 packages include complete local backgrounds, previews, CSS, metadata, and component assets; all eleven passed the recorded 26.715 workspace, right-panel, home, plugins, scheduled, sites, pull-request, and narrow-window matrix. Compatibility remains experimental outside the recorded builds. |
+| Static Gallery | Implemented | Catalog, search, native-name discovery, detail, copy, and production-build tests are present. |
+| GitHub public release | Not released | The repository remains private and no public GitHub Release has been created. |
+| Signed GUI / commercial desktop manager | Out of scope | Intentionally absent from this open-source repository. |
+
+## Recorded local prototype environment
- Official app: `/Applications/ChatGPT.app`
-- Bundle version: `26.707.91948`
+- Bundle version: `26.715.21425` (updated during the audit from `26.707.91948`)
- Architecture: arm64
- Bundle ID: `com.openai.codex`
- Team ID: `2DC432GLL2`
-- Existing endpoint: `127.0.0.1:9225`
-- Theme: `obsidian-bloom` schema v2 (`1.1.0`)
-- Adapter: `codex-26.707`
-- Apply result: `applied obsidian-bloom to 1 renderer`
-- Runtime markers: owned style present, theme and adapter datasets exact, 33 semantic variables installed.
-- Token checks: panel, text, icon, border, menu, input, code, hover, selection, and scrollbar tokens resolved to the Obsidian Bloom palette.
-- Live surfaces: root background `rgb(16, 15, 14)`; Composer `rgba(25, 22, 19, 0.95)` with the strong theme border; an opened five-item menu `rgba(23, 20, 17, 0.95)` with themed text and border.
-- Visual check: a `3024 × 1752` renderer screenshot was inspected locally and intentionally not committed because it contained active workspace content.
-- Restore result: `restored official UI`
-- Post-restore DOM check: owned style/class/datasets absent and all `--act-*` variables removed.
-- Concurrent injector check: the pre-existing unrelated `cat-theme-style` was temporarily disabled only for the clean visual smoke, then re-enabled; after Awesome restore its `nocturne-mumbai` marker and `2,472,703`-character stylesheet were unchanged.
-- Process behavior: no app restart or termination; live `start` refusal returned `APP_ALREADY_RUNNING` as designed.
-
-This record proves a local prototype path on one app version. It is not evidence of broad compatibility, packaging, notarization, or public release.
-
-## Limitless Six Eyes smoke record
-
-- Official app and endpoint: same `26.707.91948` main renderer on `127.0.0.1:9225`.
-- Theme and adapter: `limitless-six-eyes` schema v2 (`0.1.0`) with `codex-26.707`.
-- Apply result: `applied limitless-six-eyes to 1 renderer`.
-- Workspace classification: root marker resolved to `workspace`; no home marker was present.
-- Visual behavior: the active task view kept a quiet artwork-free workspace, the large identity/status chrome was hidden, the selected sidebar item used the compact violet treatment, and Composer rendered as one `15px` surface rather than nested chrome.
-- Visual check: a main-renderer screenshot was inspected locally and intentionally not committed because it contained active workspace content.
-- Restore result: `restored official UI`.
-- Post-restore DOM check: owned style, chrome node, class, and surface marker were absent.
-- Concurrent injector check: the unrelated `cat-theme-style` was temporarily disabled for the clean smoke and restored to `disabled: false` with its `2,472,703`-character stylesheet unchanged.
-
-This smoke verifies the active workspace state and restore behavior. Home artwork behavior is covered by automated classifier/theme tests but still needs a dedicated clean-home live screenshot before the theme can move beyond `experimental`.
+- Observed managed endpoint: `127.0.0.1:9341` (dynamic for this run)
+- Current public theme slug: `satoru-gojo`
+- Adapter family: `codex-26.715` using the shared 26.707/26.715 semantic mapping
+
+The active renderer was checked for owned style/chrome markers, theme variables, workspace classification, idempotent apply, renderer reload reapply, pointer-transparent decoration, and complete owned-state removal. Private screenshots with active workspace content and internal visual iteration labels remain local development evidence and are not public release assets. See the sanitized [26.715 lifecycle audit](audits/2026-07-17-satoru-gojo-26.715-lifecycle-audit.md).
+
+The tested release candidate is installed in the immutable local `0.4.2` runtime. The active service reports `active`; `new-world-studio` was verified with one owned style node, an open right-panel marker, and a dedicated panel background layer. The final handoff restores Satoru Gojo as the selected theme.
+
+The legacy injector plist and script were backed up outside the repository before migration. The old plist is no longer present in `~/Library/LaunchAgents`; its source script was not deleted.
+
+## Evidence boundary
+
+The approved live session recorded the unmanaged `restart-required` transition, renderer reload reapply, pause and owned-state removal, paused-state upgrade, resume, managed relaunch, a real LaunchAgent bootout/bootstrap cycle, the former unknown-version safe mode before the policy changed, uninstall, and a successful reinstall with the original Codex profile preserved.
+
+A clean release candidate passed `release:check`: 34 test files / 248 tests, all 12 theme packages, TypeScript, production Gallery build, production dependency audit, public-file hygiene, and diff whitespace. An anonymous public-clone test remains pending until the private repository is made public. Remaining public-release gates are a physical logout/reboot observation, the exact-commit CI result, and an explicit public-release decision.
diff --git a/docs/THEME_AUTHORING.md b/docs/THEME_AUTHORING.md
new file mode 100644
index 0000000..6964ad8
--- /dev/null
+++ b/docs/THEME_AUTHORING.md
@@ -0,0 +1,63 @@
+# Theme authoring
+A complete Awesome Codex Themes package is a visual system, not only a wallpaper. It must include validated metadata, a full semantic palette, scoped CSS, runtime artwork, preview media, provenance, compatibility declarations, and recorded interaction/restore QA.
+
+## Naming policy
+
+Use an **English primary name** for global discovery, commands, filenames, and documentation. Add a native-language display name when it is culturally meaningful:
+
+```json
+{
+ "name": "Satoru Gojo",
+ "nativeName": "五条 悟",
+ "nativeLocale": "ja-JP"
+}
+```
+
+`nativeName` and `nativeLocale` are an optional pair: provide both or neither. Use a canonical BCP 47 locale. The slug remains lowercase ASCII kebab-case, for example `satoru-gojo`.
+
+## Package layout
+
+```text
+themes/my-theme/
+├── assets/
+├── theme.json
+├── theme.css
+├── background.svg
+├── preview.png
+└── ASSET_LICENSE.md
+```
+
+Create a new package outside `themes/`, complete every required file, and move it into the runnable catalog only after validation passes. Do not use an empty placeholder directory as a release theme.
+
+## Required design coverage
+
+- Workspace canvas and readable background treatment.
+- Sidebar, header, cards, dialogs, menus, listboxes, tooltips, and elevated surfaces.
+- Primary, secondary, muted, and disabled text and icon tiers.
+- Inputs, Composer, send action, links, hover, active, selection, and keyboard focus.
+- Code, terminal, diffs, status colors, scrollbars, and reduced-motion behavior.
+- Pointer-transparent decorative layers that never obscure native controls.
+- Idempotent reapply and complete owned-state restore.
+
+The built-in adapter owns version-sensitive Codex selectors. Theme CSS should express only theme-specific composition below `html.awesome-codex-theme` and must not contain JavaScript, remote imports, analytics, or destructive DOM behavior.
+
+## Asset rights and provenance
+
+Every asset requires an entry in `ASSET_LICENSE.md` describing its source and redistribution terms. By submitting a package, the contributor confirms they have the necessary rights to publish and redistribute all included artwork, character imagery, names, logos, fonts, and derivatives under the declared terms.
+
+The repository does not turn that declaration into a legal guarantee. Maintainers may request evidence, attribution, replacement assets, or removal when a rights claim is unclear or challenged.
+
+## Compatibility and QA
+
+New packages start with `compatibility.status: experimental`, use `compatibility.strategy: best-effort-all`, and list only actually tested families under `verifiedAppVersions`. The engine attempts unverified numeric Codex versions with the shared mapping; contributors must not describe those attempts as verified compatibility.
+
+Before proposing a package:
+
+```bash
+pnpm themes:validate
+pnpm test
+pnpm typecheck
+pnpm build
+```
+
+Record clean screenshots without conversations, names, tokens, account data, or other private content. Test apply twice, renderer reload, theme switch, focus/keyboard operation, reduced motion, and restore. See [THEME_SCHEMA.md](THEME_SCHEMA.md) for the machine-readable contract.
diff --git a/docs/THEME_SCHEMA.md b/docs/THEME_SCHEMA.md
index 69c9b8b..d15bd22 100644
--- a/docs/THEME_SCHEMA.md
+++ b/docs/THEME_SCHEMA.md
@@ -5,7 +5,10 @@ Schema version `2` defines a self-contained semantic theme directory. A package
## Directory layout
```text
-themes/obsidian-bloom/
+themes/my-theme/
+├── assets/
+│ ├── brand-emblem.png
+│ └── send-control.png
├── theme.json
├── theme.css
├── background.svg
@@ -23,6 +26,8 @@ All file references must be relative, use forward slashes, and resolve below the
"slug": "my-original-theme",
"version": "1.1.0",
"name": "My Original Theme",
+ "nativeName": "Optional Native Name",
+ "nativeLocale": "ja-JP",
"description": "A concise description of the visual system.",
"author": {
"name": "Your Name",
@@ -37,7 +42,8 @@ All file references must be relative, use forward slashes, and resolve below the
"compatibility": {
"platforms": ["macos"],
"status": "experimental",
- "appVersions": ["26.707.*"]
+ "strategy": "best-effort-all",
+ "verifiedAppVersions": ["26.707.*", "26.715.*"]
},
"mode": "dark",
"palette": {
@@ -78,16 +84,20 @@ All file references must be relative, use forward slashes, and resolve below the
"experience": {
"brand": "LIMITLESS",
"eyebrow": "SIX EYES",
- "headline": "LIMITLESS WORKSPACE",
- "tagline": "Plan beyond the visible.",
- "status": "LIMITLESS ONLINE",
- "signature": "SATORU GOJO",
+ "headline": "CODEX THEME SYSTEM",
+ "tagline": "Editorial workspace protocol.",
+ "status": "SPECIAL GRADE // ACTIVE",
+ "signature": "SYSTEM 05 // LIMITLESS",
"chrome": true
},
"files": {
"css": "theme.css",
"artwork": "background.svg",
- "preview": "preview.svg"
+ "preview": "preview.svg",
+ "assets": {
+ "brand-emblem": "assets/brand-emblem.png",
+ "send-control": "assets/send-control.png"
+ }
}
}
```
@@ -100,6 +110,8 @@ All file references must be relative, use forward slashes, and resolve below the
| `slug` | Lowercase kebab-case and identical to the directory name. |
| `version` | Semantic version such as `1.0.0` or `1.0.0-beta.1`. |
| `name` | Human-readable global/English launch name. |
+| `nativeName` | Optional native-language display name. It must be supplied together with `nativeLocale`. |
+| `nativeLocale` | Canonical BCP 47 locale for `nativeName`; it cannot appear alone. |
| `description` | Plain, non-empty summary. |
| `author.name` | Required. `author.url` is optional metadata. |
| `license.code` | License identifier for CSS/metadata. Repository themes use `MIT`. |
@@ -108,11 +120,12 @@ All file references must be relative, use forward slashes, and resolve below the
| `tags` | Non-empty normalized search terms. |
| `compatibility.platforms` | Version 1 accepts only `macos`. |
| `compatibility.status` | `experimental` or `verified`. Use `verified` only with recorded runtime evidence. |
-| `compatibility.appVersions` | Exact dotted versions or trailing-wildcard ranges such as `26.707.*`. |
+| `compatibility.strategy` | `best-effort-all`. Every valid numeric Codex version is attempted with the shared mapping. |
+| `compatibility.verifiedAppVersions` | Exact dotted versions or trailing-wildcard ranges with recorded compatibility evidence, such as `26.707.*`. This is evidence metadata, not an allow/deny list. |
| `mode` | `dark`, `light`, or `system`; defaults to `dark`. |
| `palette` | Every schema-v2 semantic role is required. Values are six- or eight-digit hex colors; alpha belongs at the end (`#RRGGBBAA`). |
| `experience` | Optional schema-v2 declarative brand layer. When present, all six copy fields are required and bounded; `chrome` defaults to `true`. It cannot contain selectors, markup, or JavaScript. |
-| `files` | Local paths to CSS, artwork, and preview. |
+| `files` | Local paths to CSS, artwork, preview, and optional named UI assets. `files.assets` keys use lowercase kebab-case and are limited to 16 entries. |
## Runtime tokens
@@ -154,6 +167,8 @@ The engine exposes the validated palette as namespaced variables on the document
--act-scrollbar
--act-scrollbar-hover
--act-composer
+--act-asset-brand-emblem
+--act-asset-send-control
```
Scope every rule below the root marker:
@@ -164,6 +179,10 @@ html.awesome-codex-theme body {
background-color: var(--act-background);
background-image: var(--act-artwork);
}
+
+html.awesome-codex-theme .brand-emblem {
+ background-image: var(--act-asset-brand-emblem);
+}
```
Theme CSS is appended after the trusted adapter and should contain only artwork composition or narrowly scoped refinements. Prefer semantic elements and ARIA roles over hashed application classes. Decorative layers must not capture pointer events or obscure keyboard focus. Avoid structural DOM assumptions and do not hide native controls.
@@ -174,13 +193,15 @@ When `experience.chrome` is enabled, the engine creates one `#awesome-codex-them
- Artwork formats: SVG, PNG, JPEG, WebP, or AVIF.
- Default maximum runtime artwork size: 700 KiB so its base64 CSS variable remains below the renderer declaration budget.
+- Default maximum size for each named runtime UI asset: 256 KiB.
- Default maximum preview size: 10 MiB.
- Default maximum CSS size: 256 KiB.
-- CSS `url(...)`, remote/protocol-relative strings, `@import`, executable URLs, `expression(...)`, and escape sequences that could obscure those tokens are rejected. Runtime artwork is available only through `var(--act-artwork)`.
+- CSS `url(...)`, remote/protocol-relative resource loads, `@import`, executable URLs, `expression(...)`, and escape sequences that could obscure those tokens are rejected. Runtime artwork is available through `var(--act-artwork)` and named UI assets through `var(--act-asset-)`.
+- Named runtime UI assets accept the same image formats and SVG safety checks as artwork. Reapply and restore remove stale `--act-asset-*` variables before setting the next theme.
- SVG artwork and previews cannot contain scripts, active embedded markup, event handlers, document/entity declarations, obscured tokens, or non-fragment `href`, `src`, and `url(...)` values.
- Arbitrary JavaScript, remote fonts, analytics beacons, data collection, and content scripts are not part of the schema.
-- Artwork must be original or commercially redistributable. Do not submit character, celebrity, game, anime, brand, or logo artwork without documented rights.
-- A private prototype may use a restrictive artwork identifier only while the repository remains private. It cannot be accepted into a public release until the assets and naming are replaced or fully licensed.
+- `ASSET_LICENSE.md` must document provenance, attribution, and redistribution terms. Contributors confirm that they have the necessary rights to publish and redistribute every included asset and derivative under those terms.
+- Character, celebrity, game, anime, brand, and logo artwork receives the same rights review; inclusion in a package is not itself a legal guarantee.
## Validation
diff --git a/docs/audits/2026-07-17-satoru-gojo-26.715-lifecycle-audit.md b/docs/audits/2026-07-17-satoru-gojo-26.715-lifecycle-audit.md
new file mode 100644
index 0000000..a2681d2
--- /dev/null
+++ b/docs/audits/2026-07-17-satoru-gojo-26.715-lifecycle-audit.md
@@ -0,0 +1,50 @@
+# Satoru Gojo 26.715 lifecycle audit
+
+Date: 2026-07-17 (Asia/Shanghai)
+
+This audit records an explicitly approved migration from a legacy local injector to the Awesome Codex Themes user LaunchAgent. It contains sanitized operational evidence only. The private runtime screenshot remains outside the repository.
+
+## Environment
+
+- Official application: `/Applications/ChatGPT.app`
+- Bundle ID: `com.openai.codex`
+- Team ID: `2DC432GLL2`
+- Architecture: arm64
+- Initial bundle version: `26.707.91948`
+- Updated bundle version: `26.715.21425`
+- Theme: `satoru-gojo` `1.1.0`
+- Installed project release: `0.3.0`
+- LaunchAgent label: `io.github.awesome-codex-themes.agent`
+- Observed managed endpoint: `127.0.0.1:9341` (dynamically selected for this run)
+
+The legacy plist and script were backed up outside the repository before the old plist was removed from `~/Library/LaunchAgents`. No Codex profile, conversation, account, or official application file was moved or rewritten.
+
+## Lifecycle evidence
+
+| Check | Result |
+| --- | --- |
+| Legacy conflict discovery | `doctor` reported the legacy plist and wildcard-origin process without changing them. |
+| Unmanaged transition | Installing while Codex was open produced `restart-required`; the new agent did not terminate the app. |
+| Unknown-version safe mode | The app updated to `26.715.21425` before support was declared. The agent recorded `unsupported-version`, and the renderer had zero project theme/style/chrome markers. |
+| 26.715 compatibility probe | The existing semantic mapping applied successfully to the current renderer, produced expected panel/Composer computed styles, kept decorative chrome at `pointer-events: none`, and removed all owned state afterward. |
+| Managed activation | After the tested 26.715 adapter and theme range were installed, status became `active` on a verified literal-loopback listener owned by the official process tree. |
+| Idempotence | Repeated apply produced exactly one owned style node and one owned chrome node. |
+| Renderer reload | `performance.timeOrigin` changed from `1784263723285.4` to `1784264366089`. The new document initially had no marker; the agent restored `codex-26.715` on the fourth 500 ms observation. |
+| Pause / restore | `pause` changed configuration to `enabled: false` and removed the root class, style node, and chrome node while leaving the official app running. |
+| Upgrade while paused | `upgrade-agent` preserved the selected theme and paused configuration. A real launchd unload-window race exposed a transient bootstrap error; the runtime now retries only launchd code 5 input/output errors for a bounded period. |
+| Resume | The saved theme was re-enabled and returned to an `active` verified state. |
+| LaunchAgent reload | A real `bootout` followed by `bootstrap` and `kickstart` started a new agent process and restored verified ownership. This exercises the same launchd service boundary used by login bootstrap. |
+| App exit / relaunch | The original unmanaged PID was replaced by a CDP-managed official process, with the same Codex data profile and no `--user-data-dir`. |
+| Uninstall | `uninstall-agent` removed the project plist and support directory while leaving official Codex data unchanged; a fresh install succeeded afterward. |
+| Clean source snapshot | A staged candidate exported outside the development worktree passed a frozen dependency install, 200 tests, theme validation, typecheck, production build, dependency audit, CLI `list`, and read-only `doctor`. |
+| Candidate reinstall and service status | The tested candidate replaced the previous local runtime after that runtime was backed up. A controlled bootout reported `stale`; bootstrap and kickstart restored `active`, with one `satoru-gojo` style node, one chrome node, adapter `codex-26.715`, and `pointer-events: none`. |
+
+## Compatibility conclusion
+
+The shared CSS mapping is declared through separate adapter IDs for `26.707.*` and `26.715.*`. The live renderer retained the core token and component surfaces used by the mapping. At the time of this audit, unknown `26.716.*` versions were rejected before injection. The later `best-effort-all` policy supersedes that release rule: unverified numeric versions now attempt the same shared mapping under `codex-best-effort`, while this audit remains evidence only for the two recorded families.
+
+The exact active-workspace presentation, Composer, sidebars, right panel, pointer-transparent chrome, idempotence, reload recovery, and owned-state restore were observed on `26.715.21425`. The theme remains `experimental` because the complete P0/P1 visual surface tour was not captured on this build.
+
+## Remaining evidence boundary
+
+The launchd bootout/bootstrap boundary is integration verified. A physical logout or full macOS reboot was not performed during this audit because other active work was running on the machine. Do not represent a physical reboot as directly observed until a later controlled session records it.
diff --git a/docs/gallery/assets/awesome-codex-themes-hero.png b/docs/gallery/assets/awesome-codex-themes-hero.png
new file mode 100644
index 0000000..9740b8a
Binary files /dev/null and b/docs/gallery/assets/awesome-codex-themes-hero.png differ
diff --git a/docs/gallery/castle-archive/marketing.jpg b/docs/gallery/castle-archive/marketing.jpg
new file mode 100644
index 0000000..294d030
Binary files /dev/null and b/docs/gallery/castle-archive/marketing.jpg differ
diff --git a/docs/gallery/foundling-garden/marketing.jpg b/docs/gallery/foundling-garden/marketing.jpg
new file mode 100644
index 0000000..625c8a7
Binary files /dev/null and b/docs/gallery/foundling-garden/marketing.jpg differ
diff --git a/docs/gallery/grand-line/marketing.jpg b/docs/gallery/grand-line/marketing.jpg
new file mode 100644
index 0000000..89e4d94
Binary files /dev/null and b/docs/gallery/grand-line/marketing.jpg differ
diff --git a/docs/gallery/mordor-runtime/marketing.jpg b/docs/gallery/mordor-runtime/marketing.jpg
new file mode 100644
index 0000000..15733b7
Binary files /dev/null and b/docs/gallery/mordor-runtime/marketing.jpg differ
diff --git a/docs/gallery/new-world-studio/marketing.jpg b/docs/gallery/new-world-studio/marketing.jpg
new file mode 100644
index 0000000..4996e2b
Binary files /dev/null and b/docs/gallery/new-world-studio/marketing.jpg differ
diff --git a/docs/gallery/night-city/marketing.jpg b/docs/gallery/night-city/marketing.jpg
new file mode 100644
index 0000000..2b04ba4
Binary files /dev/null and b/docs/gallery/night-city/marketing.jpg differ
diff --git a/docs/gallery/overworld-realms/marketing.jpg b/docs/gallery/overworld-realms/marketing.jpg
new file mode 100644
index 0000000..eb3f5ed
Binary files /dev/null and b/docs/gallery/overworld-realms/marketing.jpg differ
diff --git a/docs/gallery/saiyan-ukiyoe/marketing.jpg b/docs/gallery/saiyan-ukiyoe/marketing.jpg
new file mode 100644
index 0000000..22bedb9
Binary files /dev/null and b/docs/gallery/saiyan-ukiyoe/marketing.jpg differ
diff --git a/docs/gallery/satoru-gojo/marketing.jpg b/docs/gallery/satoru-gojo/marketing.jpg
new file mode 100644
index 0000000..2d4d2ff
Binary files /dev/null and b/docs/gallery/satoru-gojo/marketing.jpg differ
diff --git a/docs/gallery/slingshot-lab/marketing.jpg b/docs/gallery/slingshot-lab/marketing.jpg
new file mode 100644
index 0000000..eaf79b8
Binary files /dev/null and b/docs/gallery/slingshot-lab/marketing.jpg differ
diff --git a/docs/gallery/symbiote-sumi-e/marketing.jpg b/docs/gallery/symbiote-sumi-e/marketing.jpg
new file mode 100644
index 0000000..1b33bd0
Binary files /dev/null and b/docs/gallery/symbiote-sumi-e/marketing.jpg differ
diff --git a/docs/gallery/zaun-workshop/marketing.jpg b/docs/gallery/zaun-workshop/marketing.jpg
new file mode 100644
index 0000000..df34d79
Binary files /dev/null and b/docs/gallery/zaun-workshop/marketing.jpg differ
diff --git a/docs/plans/2026-07-16-awesome-codex-themes-design.md b/docs/plans/2026-07-16-awesome-codex-themes-design.md
index 174a2c2..f601297 100644
--- a/docs/plans/2026-07-16-awesome-codex-themes-design.md
+++ b/docs/plans/2026-07-16-awesome-codex-themes-design.md
@@ -22,11 +22,10 @@ The engine discovers `/Applications/ChatGPT.app` and legacy `Codex.app` location
The CLI never silently terminates an active Codex session. If Codex is already running without a usable debugging endpoint, `start` stops with an explanation and asks the user to quit the app explicitly. A future explicit restart flag may be added only after tests cover unsaved-session protection. State records app identity, port, injector PID/start time, theme slug, and paths so the process can refuse unsafe PID reuse.
-Unknown Codex versions fail closed: `doctor` may report an unsupported version, while `apply` refuses unless the local compatibility manifest recognizes it or the user opts into an explicitly documented experimental path. `restore` always remains available and removes only Awesome Codex Themes state. The official app bundle, `app.asar`, accounts, conversations, API keys, provider configuration, and unrelated Codex settings are never read or modified.
+Every valid numeric Codex version receives a documented best-effort adapter attempt. `restore` always remains available and removes only Awesome Codex Themes state. The official app bundle, `app.asar`, accounts, conversations, API keys, provider configuration, and unrelated Codex settings are never read or modified.
## Error handling and verification
Errors are structured for humans and automation: stable codes such as `APP_NOT_FOUND`, `SIGNATURE_INVALID`, `CDP_UNAVAILABLE`, `UNSUPPORTED_VERSION`, `THEME_INVALID`, and `RESTORE_INCOMPLETE` accompany concise recovery instructions. Commands return non-zero status on incomplete work. Logs contain operational metadata but never task content or account data.
Tests are organized around the safety boundary. Unit tests cover manifest validation, path traversal, size and color rules, loopback URL validation, target filtering, state/PID identity checks, and injection idempotency/removal. CLI tests exercise list/help/doctor behavior with isolated fixtures. Gallery tests cover search, filters, details, and copy commands. A macOS smoke test can attach to an already-authorized local CDP endpoint, apply a fixture theme, verify namespaced markers, and restore immediately without restarting the app. Release verification requires tests, typecheck, gallery build, theme validation, deterministic security scanning, and a manual diff review.
-
diff --git a/docs/plans/2026-07-16-awesome-codex-themes-implementation.md b/docs/plans/2026-07-16-awesome-codex-themes-implementation.md
index f274fe7..7f333d5 100644
--- a/docs/plans/2026-07-16-awesome-codex-themes-implementation.md
+++ b/docs/plans/2026-07-16-awesome-codex-themes-implementation.md
@@ -2,7 +2,7 @@
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-**Goal:** Build and privately publish a Mac-first MIT Codex Desktop theme engine, original theme catalogue, and static searchable gallery in a new independent repository.
+**Goal:** Build and publish a Mac-first MIT Codex Desktop theme engine, original theme catalogue, and static searchable gallery in a new independent repository.
**Architecture:** A dependency-light Node ESM engine owns validation, CDP communication, safe injection, macOS discovery, state, and CLI commands. A Vite/React frontend renders generated metadata from declarative theme packages. Runtime code never executes remote theme JavaScript and never mutates the official application bundle.
@@ -96,7 +96,7 @@
2. Use MIT for original code and explicit license/provenance per theme asset.
3. Record conceptual references and exact source reuse, if any.
-### Task 9: Live smoke, security gate, and private GitHub publication
+### Task 9: Live smoke, security gate, and GitHub publication
**Files:**
- Create: `scripts/smoke-live.mjs` if the existing CDP endpoint is suitable.
@@ -104,6 +104,5 @@
1. Run full `pnpm check` and inspect complete output.
2. If safe, apply a fixture theme to the existing loopback CDP session, verify markers, and restore immediately; never restart Codex.
3. Run the deterministic pre-commit scanner before and after staging, then manually review the full diff against the security checklist.
-4. Commit on `main`, create `erickkkyt/Awesome-codex-themes` as a private repository, push with tracking, and verify remote visibility and commit SHA.
-5. Open `/Users/kkkk/Desktop/Awesome-codex-themes` in GitHub Desktop and verify the app received the repository path.
-
+4. Commit on `main`, publish `erickkkyt/Awesome-codex-themes`, push with tracking, and verify remote visibility and commit SHA.
+5. Open the local `Awesome-codex-themes` checkout in GitHub Desktop and verify the app received the repository path.
diff --git a/docs/plans/2026-07-16-full-theme-adapter-design.md b/docs/plans/2026-07-16-full-theme-adapter-design.md
index 3f88a37..879b3f2 100644
--- a/docs/plans/2026-07-16-full-theme-adapter-design.md
+++ b/docs/plans/2026-07-16-full-theme-adapter-design.md
@@ -6,7 +6,7 @@ Awesome Codex Themes will move from background-led theme packages to a versioned
## Chosen architecture
-The engine owns a shared `26.707` adapter stylesheet. Theme packages remain small and declarative: each manifest supplies a semantic palette, artwork, compatibility range, and optional namespaced CSS for theme-specific composition. At apply time the engine combines the trusted adapter with the validated theme CSS into one owned style element. This avoids duplicating brittle Codex selectors across every theme and gives maintainers one compatibility surface per supported Codex release family.
+The engine owns a shared adapter stylesheet. Theme packages remain small and declarative: each manifest supplies a semantic palette, artwork, live-verified compatibility metadata, and optional namespaced CSS for theme-specific composition. At apply time the engine combines the trusted adapter with the validated theme CSS into one owned style element. This avoids duplicating brittle Codex selectors across every theme and gives maintainers one compatibility surface for both verified families and best-effort attempts.
The adapter is original project code. The two MIT reference repositories are behavior and coverage references, not wholesale source. Stable Codex and VS Code design-token names observed in the supported renderer are preferred over transient utility classes. Explicit semantic selectors are used only where tokens cannot expose the artwork or glass treatment.
diff --git a/docs/plans/2026-07-16-full-theme-adapter-implementation.md b/docs/plans/2026-07-16-full-theme-adapter-implementation.md
index 09aa884..e911bda 100644
--- a/docs/plans/2026-07-16-full-theme-adapter-implementation.md
+++ b/docs/plans/2026-07-16-full-theme-adapter-implementation.md
@@ -2,7 +2,7 @@
**Goal:** Add a versioned Codex `26.707` full-theme adapter and one complete semantic theme system that covers every requested visual surface while preserving safe restore.
-**Architecture:** Load a trusted shared adapter from `src/engine/adapters/`, validate semantic theme palettes in schema v2, and compose adapter CSS with per-theme CSS in the existing owned style element. Keep schema v1 readable through deterministic palette expansion, migrate Obsidian Bloom to v2, and fail closed when no adapter supports the app version.
+**Architecture:** Load a trusted shared adapter from `src/engine/adapters/`, validate semantic theme palettes in schema v2, and compose adapter CSS with per-theme CSS in the existing owned style element. Keep schema v1 readable through deterministic palette expansion and identify unverified app versions with a shared best-effort marker.
**Tech Stack:** Node.js 22 ESM, Vitest, JSDOM, CSS custom properties, Chromium DevTools Protocol, Vite/React gallery.
@@ -43,7 +43,7 @@ Expected: PASS.
**Step 1: Write failing coverage tests**
-Require a `26.707.*` adapter to contain representative rules for canvas/artwork, surfaces, text, icons, borders, menus, inputs, code/diff, hover/active/selection, scrollbars, composer, dialogs, and focus. Require unknown versions to fail with `THEME_ADAPTER_UNSUPPORTED`.
+Require a `26.707.*` adapter to contain representative rules for canvas/artwork, surfaces, text, icons, borders, menus, inputs, code/diff, hover/active/selection, scrollbars, composer, dialogs, and focus. Require unverified numeric versions to load the same mapping as `codex-best-effort`.
**Step 2: Run and verify RED**
diff --git a/docs/plans/2026-07-16-limitless-six-eyes-design.md b/docs/plans/2026-07-16-limitless-six-eyes-design.md
index 4164e75..5bd5007 100644
--- a/docs/plans/2026-07-16-limitless-six-eyes-design.md
+++ b/docs/plans/2026-07-16-limitless-six-eyes-design.md
@@ -2,7 +2,7 @@
## Status
-Approved for implementation as a private fan prototype. The theme engine and original CSS remain MIT-licensed; the character artwork is explicitly excluded from that license and must be replaced with original or commercially licensed artwork before any public release.
+Approved for implementation as the flagship Limitless Six Eyes theme.
## Product Goal
@@ -39,7 +39,7 @@ Theme-specific CSS may enhance known Codex 26.707 renderer surfaces, but:
- selectors stay scoped below `.awesome-codex-theme`
- decorative layers use `pointer-events: none`
-- unknown versions continue to fail closed through the existing compatibility gate
+- unverified numeric versions use the shared best-effort mapping and remain explicitly unverified
- remote CSS, remote assets, and executable theme scripts remain forbidden
## Theme Package
@@ -50,7 +50,7 @@ Theme-specific CSS may enhance known Codex 26.707 renderer surfaces, but:
- `theme.css` — full-workspace styling and versioned surface enhancements
- `background.jpg` — runtime artwork with generous negative space and a bounded injection size
- `preview.png` — corrected marketing mockup
-- `ASSET_LICENSE.md` — private prototype restriction and replacement requirement
+- `ASSET_LICENSE.md` — project asset metadata
## Verification
diff --git a/docs/plans/2026-07-16-limitless-six-eyes-implementation.md b/docs/plans/2026-07-16-limitless-six-eyes-implementation.md
index bbe9a19..abe230b 100644
--- a/docs/plans/2026-07-16-limitless-six-eyes-implementation.md
+++ b/docs/plans/2026-07-16-limitless-six-eyes-implementation.md
@@ -1,6 +1,6 @@
# Limitless Six Eyes Theme Implementation Plan
-> **For Codex:** Execute this plan test-first, keep all fan artwork private, and verify restore behavior before pushing.
+> **For Codex:** Execute this plan test-first and verify restore behavior before pushing.
**Goal:** Ship one complete, installable flagship Codex theme with full semantic-token coverage, a safe declarative experience layer, gallery marketing, and live renderer evidence.
@@ -44,7 +44,7 @@
- Create: `themes/limitless-six-eyes/preview.png`
- Create: `themes/limitless-six-eyes/ASSET_LICENSE.md`
-1. Add a failing contract test for all required workspace surfaces and prototype licensing.
+1. Add a failing contract test for all required workspace surfaces and project asset metadata.
2. Add the corrected preview and clean runtime artwork.
3. Implement the full 33-role palette and surface-specific CSS.
4. Regenerate and validate the catalog.
@@ -62,12 +62,12 @@
1. Add failing gallery expectations for five themes and flagship-first presentation.
2. Implement editorial flagship treatment while preserving the existing accessible detail dialog.
3. Make the new theme the primary quick-start example.
-4. State the private prototype/public artwork boundary clearly.
+4. State the artwork metadata clearly.
-### Task 5: Verify and publish privately
+### Task 5: Verify and publish
1. Run focused tests after every task.
2. Run `pnpm check`.
-3. Apply to the local supported Codex renderer, inspect representative computed tokens and owned chrome, capture a private screenshot, then restore.
+3. Apply to the local supported Codex renderer, inspect representative computed tokens and owned chrome, capture a screenshot, then restore.
4. Review the diff and repository status.
-5. Fast-forward the clean main branch, rerun the full check, push to the existing private GitHub repository, and confirm CI.
+5. Fast-forward the clean main branch, rerun the full check, push to the existing GitHub repository, and confirm CI.
diff --git a/docs/plans/2026-07-17-persistent-theme-platform-design.md b/docs/plans/2026-07-17-persistent-theme-platform-design.md
new file mode 100644
index 0000000..d83139c
--- /dev/null
+++ b/docs/plans/2026-07-17-persistent-theme-platform-design.md
@@ -0,0 +1,150 @@
+# Persistent Theme Platform Design
+
+## Purpose
+
+Awesome Codex Themes is an English-first, Mac-first open-source theme platform for technical users of the official Codex Desktop app. It ships a small audited CDP engine, a persistent user agent, a declarative theme format, a safe restore path, and a catalog designed to grow to twelve complete themes.
+
+The first release theme is **Satoru Gojo (五条 悟)**. Eleven additional theme slots remain intentionally non-runnable until their original or redistributable asset packs are supplied. Empty slots must never appear as installable themes.
+
+## Product boundary
+
+The repository provides:
+
+- local theme discovery and selection;
+- loopback-only CDP launch, injection, verification, and removal;
+- idempotent reapplication after renderer reload;
+- persistent recovery after app exit/relaunch and macOS login;
+- live-verified compatibility metadata, best-effort fallback, and explicit recovery;
+- user-level install, status, switch, pause, restore, upgrade, and uninstall commands;
+- an English static gallery and English contributor documentation;
+- a shared schema and adapter boundary for twelve independent theme packages.
+
+It does not patch or redistribute the official app, create a second Codex data profile, read conversations or credentials, execute theme JavaScript, download remote theme code, or silently terminate a running Codex process.
+
+## Evaluated persistence approaches
+
+### Repository-bound LaunchAgent
+
+A plist can point directly at the cloned repository. This is small but fragile: moving, deleting, or switching the clone can break login startup. It is unsuitable for a public release.
+
+### Self-contained user installation — selected
+
+The installer copies the audited runtime and runnable themes into a versioned directory below `~/Library/Application Support/AwesomeCodexThemes/`. A LaunchAgent calls a stable `current` installation path. Configuration and operational state live beside, but outside, the immutable runtime copy.
+
+This approach is inspectable, reversible, updateable, and does not require a global Node installation because the launcher locates the Node runtime bundled with the validated official app.
+
+### Signed helper application
+
+A signed helper could provide the best consumer experience, but it duplicates the later commercial desktop-manager boundary. It is intentionally deferred.
+
+## Installed layout
+
+```text
+~/Library/Application Support/AwesomeCodexThemes/
+├── config.json # owner-only desired state
+├── agent-state.json # owner-only observed state
+├── current -> releases/0.3.0/ # atomically switched release
+└── releases/
+ └── 0.3.0/
+ ├── bin/awesome-codex-themes
+ ├── package.json
+ ├── src/cli/
+ ├── src/engine/
+ └── themes/ # runnable, validated packages only
+
+~/Library/LaunchAgents/
+└── io.github.awesome-codex-themes.agent.plist
+```
+
+The LaunchAgent never points at a Git clone. Installation uses a temporary release directory, validates its required files, and swaps `current` only after the copy succeeds.
+
+## Desired configuration
+
+```json
+{
+ "schemaVersion": 1,
+ "enabled": true,
+ "themeSlug": "satoru-gojo",
+ "launchAtLogin": true
+}
+```
+
+Configuration is declarative. `switch ` changes only `themeSlug`; the running agent observes the change and reapplies the new package. `pause` sets `enabled` to false and removes owned renderer state when reachable. `resume` enables the configured theme again.
+
+## Agent lifecycle
+
+1. LaunchAgent starts the user agent at login with `RunAtLoad` and `KeepAlive`.
+2. The agent validates the installed configuration, official app identity, signature, Team ID, architecture, and selected theme.
+3. If Codex is not running and persistent mode is enabled, the agent launches the official executable with a dynamically selected `127.0.0.1` CDP port. It does not pass `--user-data-dir`, so the user's existing Codex profile remains authoritative.
+4. The agent verifies that the listener belongs to the official process, waits for the allowlisted renderer, applies the selected theme, and records observed state.
+5. While the same app process is alive, the agent reapplies idempotently so renderer reloads recover automatically.
+6. When the app exits, the agent clears transient process state and starts a new managed session while persistent mode remains enabled. The explicit `pause` and `uninstall-agent` commands are the supported way to stop persistent relaunch.
+7. If Codex is already running without a trusted local CDP listener, the agent reports `restart-required` and leaves the process untouched. It never kills or restarts an active session.
+8. Every valid numeric Codex version attempts the shared mapping. Live-verified families use dedicated markers; other versions use `codex-best-effort` and rely on `pause`/`restore` if visual compatibility has drifted.
+
+## Commands
+
+| Command | Behavior |
+| --- | --- |
+| `list` | List runnable validated theme packages. |
+| `doctor` | Inspect the official app, installed agent, configuration, conflicts, and recovery path without mutation. |
+| `start ` | Start a session-scoped managed launch. |
+| `install-agent ` | Install the self-contained runtime, save the desired theme, and bootstrap persistent mode. |
+| `upgrade-agent` | Stage the current source release while preserving the installed theme and enabled/paused state. |
+| `switch ` | Validate and persist a new active theme for the installed agent. |
+| `pause` | Disable persistent launch/reapply and remove owned live styling when reachable. |
+| `resume` | Re-enable the saved theme without changing its selection. |
+| `status` | Report desired theme, service state, app/CDP state, compatibility, and last error. |
+| `restore` | Remove project-owned live styling and stop session-scoped management. |
+| `uninstall-agent` | Boot out the LaunchAgent, restore owned styling when reachable, and remove installed runtime/configuration. |
+
+Every mutating command prints the exact installed paths and recovery command. Stable error codes remain part of the public interface.
+
+## Theme catalog and twelve-slot model
+
+`themes/` contains runnable packages only. Each directory must pass the schema and asset validator before it appears in CLI output or the Gallery.
+
+`theme-slots/` contains eleven numbered planning slots. A slot is documentation and intake metadata, not a partial theme package. It has no apply command and is excluded from catalog generation. When assets arrive, a slot receives a real English name, optional native-language display name, design brief, component specification, package, QA evidence, and only then moves into `themes//`.
+
+The four existing study themes are preserved outside the release catalog under `examples/legacy-themes/`. They remain useful engine fixtures but do not consume any of the eleven planned user-supplied slots.
+
+## Naming and language policy
+
+All project documentation, CLI text, code comments, metadata descriptions, issue templates, release notes, and Gallery copy are English.
+
+Theme display metadata supports:
+
+```json
+{
+ "name": "Satoru Gojo",
+ "nativeName": "五条 悟",
+ "nativeLocale": "ja-JP"
+}
+```
+
+English is primary. A native name is optional and appears only when it is culturally meaningful. Slugs, filenames, commands, and schema keys remain lowercase ASCII English. Chinese appears only as a native language for a Chinese-origin theme.
+
+Public Satoru Gojo files and UI do not use internal visual-iteration labels. The development snapshots remain in a local backup; the public package contains one current theme.
+
+## Complete theme package contract
+
+A runnable theme includes schema-valid metadata, semantic palette coverage, scoped CSS, runtime artwork, named UI assets, preview media, asset provenance, compatibility declarations, and P0 component coverage. A theme is not complete when it is only a wallpaper.
+
+The shared engine owns version-sensitive selectors. Theme CSS owns only visual decisions below `html.awesome-codex-theme`. Decorative nodes are inert and pointer-transparent. Compatibility remains `experimental` until recorded live QA proves the declared surface matrix and restore behavior on an exact Codex version.
+
+## Safety and recovery
+
+- CDP binds to a dynamic port on literal `127.0.0.1`; wildcard origins are not used.
+- The listener must belong to the validated official app process.
+- The agent never modifies the app bundle or sets a second data directory.
+- The agent never reads renderer content beyond owned markers and computed theme verification.
+- Existing third-party injectors are detected and reported as conflicts; they are never silently deleted.
+- Agent and runtime state are owner-only and atomically written.
+- Install and update operations are rollback-safe.
+- `uninstall-agent` and a documented manual `launchctl bootout` path remain available even if the Gallery or agent is broken.
+
+## Verification
+
+Automated tests cover configuration validation, release copying, plist generation, launchctl argument arrays, theme switching, agent state transitions, best-effort version fallback, conflict reporting, and restore/uninstall idempotence. Existing CDP, signature, port-owner, renderer, injection, and theme-schema tests remain mandatory.
+
+macOS integration evidence must separately verify renderer reload, app exit/relaunch, login-agent bootstrap, theme switch, best-effort fallback and recovery, restore, uninstall, and preservation of the existing Codex profile. Linux CI validates pure generation and state-machine behavior without attempting to load launchd.
diff --git a/docs/plans/2026-07-17-persistent-theme-platform-implementation.md b/docs/plans/2026-07-17-persistent-theme-platform-implementation.md
new file mode 100644
index 0000000..e84ae07
--- /dev/null
+++ b/docs/plans/2026-07-17-persistent-theme-platform-implementation.md
@@ -0,0 +1,176 @@
+# Persistent Theme Platform Implementation Plan
+
+> **For Codex:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Turn Awesome Codex Themes into an English-first twelve-theme platform with one complete Satoru Gojo package and a safe self-contained persistent macOS agent.
+
+**Architecture:** Keep the existing Node built-in runtime and versioned CDP adapter. Add pure configuration, installation, LaunchAgent, and supervisor modules below `src/engine/`; compose them through explicit CLI commands. Runnable themes remain isolated under `themes/`, while eleven non-runnable intake slots live outside the catalog.
+
+**Tech Stack:** Node.js ESM and built-ins, Vitest, macOS launchd/launchctl, React/Vite static Gallery, JSON theme manifests.
+
+---
+
+### Task 1: Add multilingual display metadata without changing English-first identifiers
+
+**Files:**
+- Modify: `tests/engine/theme.test.mjs`
+- Modify: `src/engine/theme.mjs`
+- Modify: `docs/THEME_SCHEMA.md`
+- Modify: `scripts/catalog.mjs`
+- Modify: `tests/themes/catalog.test.mjs`
+
+**Steps:**
+1. Write failing tests for optional `nativeName` and BCP-47-like `nativeLocale` fields, including rejection when only one is supplied.
+2. Run the focused tests and confirm the expected schema failure.
+3. Implement minimal normalization and catalog propagation.
+4. Run the focused tests and full theme validation.
+
+### Task 2: Promote the current flagship package to Satoru Gojo
+
+**Files:**
+- Rename: `themes/limitless-six-eyes/` to `themes/satoru-gojo/`
+- Modify: `themes/satoru-gojo/theme.json`
+- Modify: `themes/satoru-gojo/theme.css`
+- Modify: `themes/satoru-gojo/ASSET_LICENSE.md`
+- Modify: `tests/themes/limitless-six-eyes.test.mjs` and rename it to `tests/themes/satoru-gojo.test.mjs`
+- Modify: `src/gallery/App.tsx`
+- Modify: `tests/gallery/app.test.tsx`
+- Modify: public catalog/preview references generated by `pnpm build`
+
+**Steps:**
+1. Change tests first to require slug `satoru-gojo`, primary name `Satoru Gojo`, native name `五条 悟`, locale `ja-JP`, current assets, P0 coverage markers, and no public internal-iteration label.
+2. Run focused tests and confirm they fail against the old package.
+3. Rename the package and update scoped selectors/metadata without changing approved visuals.
+4. Rebuild the catalog and run theme, Gallery, and injection tests.
+
+### Task 3: Separate legacy studies and create eleven non-runnable slots
+
+**Files:**
+- Move: `themes/{arctic-signal,obsidian-bloom,paper-circuit,solar-archive}/` to `examples/legacy-themes/`
+- Create: `theme-slots/README.md`
+- Create: `theme-slots/slots.json`
+- Create: `theme-slots/02/README.md` through `theme-slots/12/README.md`
+- Create: `tests/themes/slots.test.mjs`
+- Modify: `README.md`
+
+**Steps:**
+1. Write a failing test requiring exactly eleven ordered awaiting-assets slots and proving they are absent from the runnable catalog.
+2. Run the test and confirm the missing-slot failure.
+3. Add the slot manifest/readmes and preserve legacy studies outside `themes/`.
+4. Run catalog generation and confirm only Satoru Gojo is runnable.
+
+### Task 4: Add owner-only desired configuration and observed agent state
+
+**Files:**
+- Create: `tests/engine/config.test.mjs`
+- Create: `src/engine/config.mjs`
+- Create: `tests/engine/agent-state.test.mjs`
+- Create: `src/engine/agent-state.mjs`
+
+**Steps:**
+1. Write failing tests for schemas, absolute paths, slug validation, atomic writes, mode `0600`, and typed malformed-file errors.
+2. Run focused tests and confirm missing-module failures.
+3. Implement minimal validators/readers/writers using Node built-ins.
+4. Run focused tests and refactor shared atomic JSON persistence only after green.
+
+### Task 5: Generate and manage a safe LaunchAgent definition
+
+**Files:**
+- Create: `tests/engine/launch-agent.test.mjs`
+- Create: `src/engine/launch-agent.mjs`
+
+**Steps:**
+1. Write failing tests for XML escaping, stable label, absolute installed launcher path, `RunAtLoad`, `KeepAlive`, log paths, and argument-array `launchctl bootstrap/bootout/kickstart` calls.
+2. Confirm the tests fail because the module does not exist.
+3. Implement pure plist generation and dependency-injected launchctl operations without shell interpolation.
+4. Run the focused tests on all platforms; keep actual launchctl execution behind explicit CLI mutation.
+
+### Task 6: Build rollback-safe self-contained installation
+
+**Files:**
+- Create: `tests/engine/installer.test.mjs`
+- Create: `src/engine/installer.mjs`
+- Modify: `.gitignore`
+
+**Steps:**
+1. Write failing temporary-directory tests for required runtime files, runnable-theme copying, owner permissions, atomic `current` switch, same-version idempotence, and cleanup after copy failure.
+2. Confirm the failure.
+3. Implement installation using `fs.cp`, canonical source paths, a staging release, and dependency-injected LaunchAgent operations.
+4. Run focused tests and inspect the installed fixture tree.
+
+### Task 7: Add the persistent supervisor state machine
+
+**Files:**
+- Create: `tests/engine/agent.test.mjs`
+- Create: `src/engine/agent.mjs`
+- Modify: `src/engine/session.mjs`
+
+**Steps:**
+1. Write failing tests for disabled, app-absent, managed-app, unmanaged-app, renderer-reload, app-exit, best-effort-version, theme-switch, and signal-stop states.
+2. Confirm each behavior fails for the intended missing implementation.
+3. Implement one deterministic `agentStep` plus a small loop; reuse existing app discovery, port ownership, apply, and removal functions.
+4. Ensure unmanaged running Codex returns `restart-required` and never calls a terminate dependency.
+5. Run agent, session, macOS, CDP, and injection tests.
+
+### Task 8: Expose persistent lifecycle commands
+
+**Files:**
+- Modify: `tests/cli/main.test.mjs`
+- Modify: `src/cli/main.mjs`
+- Modify: `bin/awesome-codex-themes`
+
+**Steps:**
+1. Write failing CLI tests for `install-agent`, `switch`, `pause`, `resume`, `status`, `uninstall-agent`, and internal `_agent` validation.
+2. Confirm stable error/output assertions fail before implementation.
+3. Wire commands to installer/config/agent dependencies and preserve session-scoped `start/apply/restore` compatibility.
+4. Run CLI tests and verify help text is entirely English.
+
+### Task 9: Add conflict diagnostics and migration guidance
+
+**Files:**
+- Modify: `tests/engine/macos.test.mjs`
+- Modify: `src/engine/macos.mjs`
+- Modify: `tests/cli/main.test.mjs`
+- Modify: `src/cli/main.mjs`
+- Create: `docs/MIGRATION.md`
+
+**Steps:**
+1. Write failing tests that detect known theme LaunchAgents/processes without deleting them.
+2. Implement read-only diagnostics and surface them through `doctor`.
+3. Document manual migration from older injectors, including restore and `launchctl bootout`, without machine-specific paths.
+4. Run focused tests.
+
+### Task 10: Rewrite public documentation for the English global project
+
+**Files:**
+- Modify: `README.md`
+- Modify: `docs/ARCHITECTURE.md`
+- Modify: `docs/SAFETY.md`
+- Modify: `docs/STATUS.md`
+- Modify: `CONTRIBUTING.md`
+- Modify: `SECURITY.md`
+- Modify: `CHANGELOG.md`
+- Modify: `.github/ISSUE_TEMPLATE/*.yml`
+- Modify: `.github/pull_request_template.md`
+- Create: `docs/INSTALL.md`
+- Create: `docs/THEME_AUTHORING.md`
+
+**Steps:**
+1. Add documentation assertions to existing tests for current commands, Satoru Gojo naming, recovery, twelve-slot truth, and absence of private or internal-iteration claims.
+2. Confirm failures.
+3. Rewrite documentation in English with exact commands, lifecycle limits, manual recovery, compatibility boundaries, and native-name policy.
+4. Run documentation and catalog tests.
+
+### Task 11: Verify static and macOS lifecycle behavior
+
+**Files:**
+- Modify: `docs/STATUS.md`
+- Create: `docs/verification/persistent-agent-macos.md`
+
+**Steps:**
+1. Run `pnpm check` and record exact counts.
+2. Run `git diff --check` and inspect the complete diff for unrelated changes or private content.
+3. On macOS, dry-run installation into a temporary home and inspect the plist/runtime copy.
+4. With explicit live-session safety, verify renderer reapply and current Satoru Gojo markers without closing the user's active Codex.
+5. Do not load the new LaunchAgent or replace the existing agent until the user explicitly approves that machine-level migration.
+6. Report implemented, tested, live-verified, committed, pushed, and released states separately.
diff --git a/index.html b/index.html
index 5d2f169..81d0de6 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
-
+
Awesome Codex Themes
diff --git a/package.json b/package.json
index e7b091f..842db01 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,26 @@
{
"name": "awesome-codex-themes",
- "version": "0.2.0",
+ "version": "0.4.2",
"packageManager": "pnpm@10.32.1",
"private": false,
"type": "module",
"description": "A safe, open theme engine and gallery for the official Codex Desktop app.",
"license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/erickkkyt/Awesome-codex-themes.git"
+ },
+ "homepage": "https://github.com/erickkkyt/Awesome-codex-themes#readme",
+ "bugs": {
+ "url": "https://github.com/erickkkyt/Awesome-codex-themes/issues"
+ },
+ "keywords": [
+ "codex",
+ "codex-desktop",
+ "macos",
+ "theme",
+ "cdp"
+ ],
"bin": {
"awesome-codex-themes": "bin/awesome-codex-themes"
},
@@ -19,7 +34,8 @@
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"themes:validate": "node scripts/validate-themes.mjs",
- "check": "pnpm test && pnpm themes:validate && pnpm typecheck && pnpm build"
+ "check": "pnpm test && pnpm themes:validate && pnpm typecheck && pnpm build",
+ "release:check": "pnpm check && pnpm audit --prod --audit-level moderate && git diff --check"
},
"dependencies": {
"@fontsource-variable/newsreader": "^5.2.8",
diff --git a/public/theme-assets/arctic-signal/preview.svg b/public/theme-assets/arctic-signal/preview.svg
deleted file mode 100644
index ecd565e..0000000
--- a/public/theme-assets/arctic-signal/preview.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/public/theme-assets/castle-archive/preview.jpg b/public/theme-assets/castle-archive/preview.jpg
new file mode 100644
index 0000000..c7773dc
Binary files /dev/null and b/public/theme-assets/castle-archive/preview.jpg differ
diff --git a/public/theme-assets/foundling-garden/preview.jpg b/public/theme-assets/foundling-garden/preview.jpg
new file mode 100644
index 0000000..18f8d0c
Binary files /dev/null and b/public/theme-assets/foundling-garden/preview.jpg differ
diff --git a/public/theme-assets/grand-line/preview.jpg b/public/theme-assets/grand-line/preview.jpg
new file mode 100644
index 0000000..4e2d62f
Binary files /dev/null and b/public/theme-assets/grand-line/preview.jpg differ
diff --git a/public/theme-assets/mordor-runtime/preview.jpg b/public/theme-assets/mordor-runtime/preview.jpg
new file mode 100644
index 0000000..645317b
Binary files /dev/null and b/public/theme-assets/mordor-runtime/preview.jpg differ
diff --git a/public/theme-assets/new-world-studio/preview.jpg b/public/theme-assets/new-world-studio/preview.jpg
new file mode 100644
index 0000000..38f9f53
Binary files /dev/null and b/public/theme-assets/new-world-studio/preview.jpg differ
diff --git a/public/theme-assets/night-city/preview.jpg b/public/theme-assets/night-city/preview.jpg
new file mode 100644
index 0000000..fa3d4fa
Binary files /dev/null and b/public/theme-assets/night-city/preview.jpg differ
diff --git a/public/theme-assets/obsidian-bloom/preview.svg b/public/theme-assets/obsidian-bloom/preview.svg
deleted file mode 100644
index 60341a6..0000000
--- a/public/theme-assets/obsidian-bloom/preview.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/public/theme-assets/overworld-realms/preview.jpg b/public/theme-assets/overworld-realms/preview.jpg
new file mode 100644
index 0000000..697c4d1
Binary files /dev/null and b/public/theme-assets/overworld-realms/preview.jpg differ
diff --git a/public/theme-assets/paper-circuit/preview.svg b/public/theme-assets/paper-circuit/preview.svg
deleted file mode 100644
index 3f1584d..0000000
--- a/public/theme-assets/paper-circuit/preview.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/public/theme-assets/saiyan-ukiyoe/preview.jpg b/public/theme-assets/saiyan-ukiyoe/preview.jpg
new file mode 100644
index 0000000..aeab3fd
Binary files /dev/null and b/public/theme-assets/saiyan-ukiyoe/preview.jpg differ
diff --git a/public/theme-assets/limitless-six-eyes/preview.png b/public/theme-assets/satoru-gojo/preview.png
similarity index 100%
rename from public/theme-assets/limitless-six-eyes/preview.png
rename to public/theme-assets/satoru-gojo/preview.png
diff --git a/public/theme-assets/slingshot-lab/preview.jpg b/public/theme-assets/slingshot-lab/preview.jpg
new file mode 100644
index 0000000..c28ec64
Binary files /dev/null and b/public/theme-assets/slingshot-lab/preview.jpg differ
diff --git a/public/theme-assets/solar-archive/preview.svg b/public/theme-assets/solar-archive/preview.svg
deleted file mode 100644
index 9202d89..0000000
--- a/public/theme-assets/solar-archive/preview.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/public/theme-assets/symbiote-sumi-e/preview.jpg b/public/theme-assets/symbiote-sumi-e/preview.jpg
new file mode 100644
index 0000000..ccc7674
Binary files /dev/null and b/public/theme-assets/symbiote-sumi-e/preview.jpg differ
diff --git a/public/theme-assets/zaun-workshop/preview.jpg b/public/theme-assets/zaun-workshop/preview.jpg
new file mode 100644
index 0000000..27b6104
Binary files /dev/null and b/public/theme-assets/zaun-workshop/preview.jpg differ
diff --git a/scripts/catalog.mjs b/scripts/catalog.mjs
index 46cb3b2..c3fe82c 100644
--- a/scripts/catalog.mjs
+++ b/scripts/catalog.mjs
@@ -1,4 +1,4 @@
-import { copyFile, mkdir, readdir, writeFile } from 'node:fs/promises';
+import { copyFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises';
import { basename, dirname, join } from 'node:path';
import { loadThemePackage } from '../src/engine/theme.mjs';
@@ -17,6 +17,9 @@ export async function buildThemeCatalog(themesRoot) {
slug: manifest.slug,
version: manifest.version,
name: manifest.name,
+ ...(manifest.nativeName
+ ? { nativeName: manifest.nativeName, nativeLocale: manifest.nativeLocale }
+ : {}),
description: manifest.description,
author: manifest.author,
license: manifest.license,
@@ -27,7 +30,7 @@ export async function buildThemeCatalog(themesRoot) {
palette: manifest.palette,
...(manifest.experience ? { experience: manifest.experience } : {}),
preview: `/theme-assets/${manifest.slug}/${basename(manifest.files.preview)}`,
- command: `awesome-codex-themes start ${manifest.slug}`,
+ command: `./bin/awesome-codex-themes install-agent ${manifest.slug}`,
});
}
@@ -38,6 +41,7 @@ export async function writeThemeCatalog({ themesRoot, jsonPath, publicRoot }) {
const catalog = await buildThemeCatalog(themesRoot);
await mkdir(dirname(jsonPath), { recursive: true });
await writeFile(jsonPath, `${JSON.stringify(catalog, null, 2)}\n`);
+ await rm(join(publicRoot, 'theme-assets'), { recursive: true, force: true });
for (const theme of catalog) {
const previewName = theme.preview.split('/').at(-1);
diff --git a/src/cli/main.mjs b/src/cli/main.mjs
index d91759a..637cef2 100644
--- a/src/cli/main.mjs
+++ b/src/cli/main.mjs
@@ -1,5 +1,6 @@
import { execFile } from 'node:child_process';
-import { readdir, rm } from 'node:fs/promises';
+import { realpathSync } from 'node:fs';
+import { readFile, readdir, rm } from 'node:fs/promises';
import { createServer } from 'node:net';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
@@ -8,6 +9,13 @@ import { promisify } from 'node:util';
import { spawn } from 'node:child_process';
import { evaluateOnRendererTargets, findAvailablePort } from '../engine/cdp.mjs';
+import { runPersistentAgent } from '../engine/agent.mjs';
+import { isActiveAgentStateLive, readAgentState, writeAgentState } from '../engine/agent-state.mjs';
+import { readAgentConfig, writeAgentConfig } from '../engine/config.mjs';
+import { inspectInjectorConflicts } from '../engine/conflicts.mjs';
+import { pausePersistentInstallation } from '../engine/control.mjs';
+import { installPersistentAgent, installationPaths, uninstallPersistentAgent } from '../engine/installer.mjs';
+import { isLaunchAgentLoaded, kickstartLaunchAgent } from '../engine/launch-agent.mjs';
import {
assertOfficialPortOwner,
discoverOfficialApp,
@@ -28,7 +36,20 @@ const cliPath = fileURLToPath(import.meta.url);
const projectRoot = resolve(dirname(cliPath), '../..');
const themesRoot = join(projectRoot, 'themes');
const statePath = join(homedir(), 'Library/Application Support/AwesomeCodexThemes/state.json');
-const RETRYABLE_CDP_ERRORS = new Set(['CDP_HTTP_FAILED', 'CDP_RENDERER_NOT_FOUND', 'CDP_CONNECTION_FAILED']);
+const RETRYABLE_CDP_ERRORS = new Set([
+ 'CDP_PORT_UNAVAILABLE',
+ 'CDP_HTTP_FAILED',
+ 'CDP_RENDERER_NOT_FOUND',
+ 'CDP_CONNECTION_FAILED',
+]);
+
+async function projectVersion() {
+ return JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')).version;
+}
+
+async function persistentPaths() {
+ return installationPaths({ home: homedir(), version: await projectVersion() });
+}
const HELP = `Awesome Codex Themes
@@ -36,9 +57,15 @@ Usage:
awesome-codex-themes list
awesome-codex-themes doctor
awesome-codex-themes start
+ awesome-codex-themes install-agent
+ awesome-codex-themes upgrade-agent
+ awesome-codex-themes switch
+ awesome-codex-themes pause
+ awesome-codex-themes resume
awesome-codex-themes apply --port
awesome-codex-themes status
awesome-codex-themes restore [--port ]
+ awesome-codex-themes uninstall-agent
The engine only connects to CDP on 127.0.0.1 and never terminates the official app.`;
@@ -65,7 +92,7 @@ function parsePort(value) {
function requireTheme(args) {
const theme = args[0];
- if (!theme || theme.startsWith('-')) cliFail('CLI_THEME_REQUIRED', 'Choose a theme slug, for example: obsidian-bloom.');
+ if (!theme || theme.startsWith('-')) cliFail('CLI_THEME_REQUIRED', 'Choose a theme slug, for example: satoru-gojo.');
return theme;
}
@@ -94,7 +121,11 @@ export async function runCli(argv, dependencies, io = { out: console.log, error:
return 0;
}
for (const theme of themes) {
- io.out(`${theme.slug} ${theme.status} ${theme.categories.join(', ')} ${theme.name}`);
+ const displayName = theme.nativeName ? `${theme.name} / ${theme.nativeName}` : theme.name;
+ const compatibility = theme.strategy === 'best-effort-all'
+ ? `best effort: all numeric versions high compatibility: ${theme.verifiedAppVersions.join(', ')}`
+ : `high compatibility: ${theme.verifiedAppVersions.join(', ')}`;
+ io.out(`${theme.slug} ${theme.status} ${theme.categories.join(', ')} ${compatibility} ${displayName}`);
}
return 0;
}
@@ -105,6 +136,12 @@ export async function runCli(argv, dependencies, io = { out: console.log, error:
io.out(`app: ${report.appPath}`);
io.out(`version: ${report.version}`);
io.out(`running: ${report.runningPids.length ? report.runningPids.join(', ') : 'no'}`);
+ if (report.conflicts?.length) {
+ for (const conflict of report.conflicts) {
+ io.out(`conflict: ${conflict.id}${conflict.path ? ` — ${conflict.path}` : ''}`);
+ }
+ io.out('warning: conflict diagnostics are read-only; no files were changed');
+ }
return 0;
}
@@ -126,10 +163,53 @@ export async function runCli(argv, dependencies, io = { out: console.log, error:
return 0;
}
+ if (command === 'install-agent') {
+ const theme = requireTheme(args);
+ rejectExtraArgs(args, 1, 'install-agent');
+ const result = await dependencies.installAgent(theme);
+ io.out(`installed persistent theme ${result.theme} at ${result.supportRoot}`);
+ return 0;
+ }
+
+ if (command === 'upgrade-agent') {
+ rejectExtraArgs(args, 0, 'upgrade-agent');
+ const result = await dependencies.upgradeAgent();
+ io.out(
+ `upgraded persistent agent to ${result.version} with ${result.theme} (${result.enabled ? 'enabled' : 'paused'})`,
+ );
+ return 0;
+ }
+
+ if (command === 'switch') {
+ const theme = requireTheme(args);
+ rejectExtraArgs(args, 1, 'switch');
+ const result = await dependencies.switchTheme(theme);
+ io.out(`selected persistent theme ${result.theme}`);
+ return 0;
+ }
+
+ if (command === 'pause') {
+ rejectExtraArgs(args, 0, 'pause');
+ const result = await dependencies.pause();
+ io.out(`paused persistent theme ${result.theme}`);
+ return 0;
+ }
+
+ if (command === 'resume') {
+ rejectExtraArgs(args, 0, 'resume');
+ const result = await dependencies.resume();
+ io.out(`resumed persistent theme ${result.theme}`);
+ return 0;
+ }
+
if (command === 'status') {
rejectExtraArgs(args, 0, 'status');
const result = await dependencies.status();
- if (result.stale) {
+ if (result.installed && result.status === 'active') {
+ io.out(`active — persistent ${result.theme} on 127.0.0.1:${result.port}`);
+ } else if (result.installed) {
+ io.out(`${result.status} — persistent ${result.theme}${result.enabled ? '' : ' (disabled)'}`);
+ } else if (result.stale) {
io.out(`inactive — stale state for ${result.theme}; run restore to clean project-owned state`);
} else if (!result.active) {
io.out('inactive — official UI is not managed by Awesome Codex Themes');
@@ -148,7 +228,22 @@ export async function runCli(argv, dependencies, io = { out: console.log, error:
port = parsePort(args[1]);
}
const result = await dependencies.restore(port);
- io.out(`restored official UI${result.theme ? ` from ${result.theme}` : ''}`);
+ if (result.persistent) {
+ io.out(
+ result.liveRemoval === 'removed'
+ ? `paused persistent theme ${result.theme} and restored project-owned styling`
+ : `paused persistent theme ${result.theme}; project-owned styling will clear when the endpoint is reachable`,
+ );
+ } else {
+ io.out(`restored official UI${result.theme ? ` from ${result.theme}` : ''}`);
+ }
+ return 0;
+ }
+
+ if (command === 'uninstall-agent') {
+ rejectExtraArgs(args, 0, 'uninstall-agent');
+ await dependencies.uninstallAgent();
+ io.out('uninstalled persistent agent; official Codex data was not changed');
return 0;
}
@@ -161,6 +256,12 @@ export async function runCli(argv, dependencies, io = { out: console.log, error:
return 0;
}
+ if (command === '_agent') {
+ rejectExtraArgs(args, 0, '_agent');
+ await dependencies.runAgent();
+ return 0;
+ }
+
cliFail('CLI_COMMAND_UNKNOWN', `Unknown command: ${command}. Run help for usage.`);
} catch (error) {
io.error(`[${errorCode(error)}] ${error?.message ?? 'Unexpected failure.'}`);
@@ -178,10 +279,20 @@ function portIsAvailable(port) {
});
}
-async function waitForRenderer(port, { attempts = 80, delayMs = 250 } = {}) {
+export async function waitForRenderer(
+ port,
+ app,
+ {
+ attempts = 80,
+ delayMs = 250,
+ assertPortOwner = assertOfficialPortOwner,
+ evaluate = evaluateOnRendererTargets,
+ } = {},
+) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
- await evaluateOnRendererTargets({ port, expression: '({ pass: true })' });
+ await assertPortOwner(app, port);
+ await evaluate({ port, expression: '({ pass: true })' });
return;
} catch (error) {
if (!RETRYABLE_CDP_ERRORS.has(error?.code) || attempt === attempts - 1) throw error;
@@ -261,6 +372,20 @@ export async function terminateProcess(
}
export function createDefaultDependencies() {
+ async function pauseInstalledTheme() {
+ const paths = await persistentPaths();
+ return pausePersistentInstallation(
+ { configPath: paths.configPath, statePath: paths.agentStatePath },
+ {
+ removeTheme: async ({ port }) => {
+ const app = await discoverOfficialApp();
+ await assertOfficialPortOwner(app, port);
+ await removeThemeAtPort({ port });
+ },
+ },
+ );
+ }
+
return {
async listThemes() {
const entries = await readdir(themesRoot, { withFileTypes: true });
@@ -273,8 +398,11 @@ export function createDefaultDependencies() {
.map((manifest) => ({
slug: manifest.slug,
name: manifest.name,
+ nativeName: manifest.nativeName,
categories: manifest.categories,
status: manifest.compatibility.status,
+ strategy: manifest.compatibility.strategy,
+ verifiedAppVersions: manifest.compatibility.verifiedAppVersions,
}))
.sort((left, right) => left.slug.localeCompare(right.slug));
},
@@ -285,6 +413,7 @@ export function createDefaultDependencies() {
appPath: app.appPath,
version: app.version,
runningPids: await listOfficialAppPids(app),
+ conflicts: await inspectInjectorConflicts(homedir()),
};
},
@@ -318,6 +447,52 @@ export function createDefaultDependencies() {
);
},
+ async installAgent(themeSlug) {
+ await discoverOfficialApp();
+ return installPersistentAgent({
+ sourceRoot: projectRoot,
+ home: homedir(),
+ version: await projectVersion(),
+ themeSlug,
+ });
+ },
+
+ async upgradeAgent() {
+ await discoverOfficialApp();
+ const paths = await persistentPaths();
+ const config = await readAgentConfig(paths.configPath);
+ const version = await projectVersion();
+ const result = await installPersistentAgent({
+ sourceRoot: projectRoot,
+ home: homedir(),
+ version,
+ themeSlug: config.themeSlug,
+ enabled: config.enabled,
+ });
+ return { ...result, upgraded: true, enabled: config.enabled };
+ },
+
+ async switchTheme(themeSlug) {
+ const paths = await persistentPaths();
+ await loadThemePackage(join(paths.currentPath, 'themes', themeSlug));
+ const config = await readAgentConfig(paths.configPath);
+ await writeAgentConfig(paths.configPath, { ...config, themeSlug });
+ await kickstartLaunchAgent();
+ return { switched: true, theme: themeSlug };
+ },
+
+ async pause() {
+ return pauseInstalledTheme();
+ },
+
+ async resume() {
+ const paths = await persistentPaths();
+ const config = await readAgentConfig(paths.configPath);
+ await writeAgentConfig(paths.configPath, { ...config, enabled: true });
+ await kickstartLaunchAgent();
+ return { resumed: true, theme: config.themeSlug };
+ },
+
async apply(themeSlug, port) {
const app = await discoverOfficialApp();
await assertOfficialPortOwner(app, port);
@@ -325,6 +500,34 @@ export function createDefaultDependencies() {
},
async status() {
+ try {
+ const paths = await persistentPaths();
+ const config = await readAgentConfig(paths.configPath);
+ try {
+ const state = await readAgentState(paths.agentStatePath);
+ const live = state.status !== 'active' || await isActiveAgentStateLive(state, {
+ discoverApp: discoverOfficialApp,
+ listPids: listOfficialAppPids,
+ assertPortOwner: assertOfficialPortOwner,
+ isAgentLoaded: isLaunchAgentLoaded,
+ });
+ return {
+ installed: true,
+ enabled: config.enabled,
+ status: live ? state.status : 'stale',
+ theme: config.themeSlug,
+ port: state.port,
+ errorCode: live ? state.errorCode : 'AGENT_STATE_STALE',
+ };
+ } catch (error) {
+ if (error?.code === 'AGENT_STATE_READ_FAILED') {
+ return { installed: true, enabled: config.enabled, status: 'idle', theme: config.themeSlug, port: null };
+ }
+ throw error;
+ }
+ } catch (error) {
+ if (error?.code !== 'CONFIG_READ_FAILED') throw error;
+ }
try {
const state = await readRuntimeState(statePath);
try {
@@ -358,6 +561,12 @@ export function createDefaultDependencies() {
await assertOfficialPortOwner(app, port);
return removeThemeAtPort({ port });
}
+ try {
+ const paused = await pauseInstalledTheme();
+ return { ...paused, restored: paused.liveRemoval === 'removed', persistent: true };
+ } catch (error) {
+ if (error?.code !== 'CONFIG_READ_FAILED') throw error;
+ }
let expected;
return restoreThemeSession(
{ statePath },
@@ -380,6 +589,27 @@ export function createDefaultDependencies() {
);
},
+ async uninstallAgent() {
+ const paths = await persistentPaths();
+ try {
+ const state = await readAgentState(paths.agentStatePath);
+ if (state.port !== null) {
+ const app = await discoverOfficialApp();
+ await assertOfficialPortOwner(app, state.port);
+ await removeThemeAtPort({ port: state.port });
+ }
+ } catch (error) {
+ if (
+ error?.code !== 'AGENT_STATE_READ_FAILED' &&
+ !RETRYABLE_CDP_ERRORS.has(error?.code) &&
+ error?.code !== 'CDP_PORT_OWNER_INVALID'
+ ) {
+ throw error;
+ }
+ }
+ return uninstallPersistentAgent({ home: homedir() });
+ },
+
async watch(themeSlug, port, appPid) {
if (!Number.isInteger(appPid) || appPid < 1) cliFail('CLI_PID_INVALID', 'Internal watcher PID is invalid.');
const app = await discoverOfficialApp();
@@ -401,10 +631,67 @@ export function createDefaultDependencies() {
await new Promise((resolveDelay) => setTimeout(resolveDelay, 1_500));
}
},
+
+ async runAgent() {
+ const paths = await persistentPaths();
+ let stopping = false;
+ const stop = () => {
+ stopping = true;
+ };
+ process.once('SIGTERM', stop);
+ process.once('SIGINT', stop);
+ let initialRuntime = null;
+ try {
+ const state = await readAgentState(paths.agentStatePath);
+ if (state.appPid && state.port) {
+ initialRuntime = { appPid: state.appPid, port: state.port, themeSlug: state.themeSlug };
+ }
+ } catch (error) {
+ if (error?.code !== 'AGENT_STATE_READ_FAILED') throw error;
+ }
+
+ try {
+ return await runPersistentAgent(
+ {
+ configPath: paths.configPath,
+ statePath: paths.agentStatePath,
+ intervalMs: 1_500,
+ initialRuntime,
+ },
+ {
+ writeState: writeAgentState,
+ stepDependencies: {
+ discoverApp: discoverOfficialApp,
+ listPids: listOfficialAppPids,
+ selectPort: () => findAvailablePort({ start: 9_341, end: 9_441, isAvailable: portIsAvailable }),
+ launchApp: launchOfficialApp,
+ waitForRenderer,
+ assertPortOwner: assertOfficialPortOwner,
+ applyTheme: ({ themeSlug, port, app }) =>
+ applyThemeAtPort({ themesRoot, themeSlug, port, appVersion: app.version }),
+ removeTheme: ({ port }) => removeThemeAtPort({ port }),
+ },
+ shouldStop: () => stopping,
+ },
+ );
+ } finally {
+ process.removeListener('SIGTERM', stop);
+ process.removeListener('SIGINT', stop);
+ }
+ },
};
}
-if (process.argv[1] && resolve(process.argv[1]) === cliPath) {
+function isDirectExecution(argvPath) {
+ if (!argvPath) return false;
+ try {
+ return realpathSync(argvPath) === realpathSync(cliPath);
+ } catch {
+ return resolve(argvPath) === cliPath;
+ }
+}
+
+if (isDirectExecution(process.argv[1])) {
const exitCode = await runCli(process.argv.slice(2), createDefaultDependencies());
process.exitCode = exitCode;
}
diff --git a/src/engine/adapter.mjs b/src/engine/adapter.mjs
index 6e1d9f8..ea2cc29 100644
--- a/src/engine/adapter.mjs
+++ b/src/engine/adapter.mjs
@@ -9,12 +9,22 @@ export class AdapterError extends Error {
}
const ADAPTERS = Object.freeze([
+ Object.freeze({
+ id: 'codex-26.715',
+ appVersions: Object.freeze(['26.715.*']),
+ file: new URL('./adapters/codex-26.707.css', import.meta.url),
+ }),
Object.freeze({
id: 'codex-26.707',
appVersions: Object.freeze(['26.707.*']),
file: new URL('./adapters/codex-26.707.css', import.meta.url),
}),
]);
+const BEST_EFFORT_ADAPTER = Object.freeze({
+ id: 'codex-best-effort',
+ appVersions: Object.freeze(['*']),
+ file: new URL('./adapters/codex-26.707.css', import.meta.url),
+});
function versionMatchesRange(appVersion, range) {
const versionParts = appVersion.split('.');
@@ -30,15 +40,10 @@ export async function loadCodexAdapter(appVersion) {
throw new AdapterError('THEME_ADAPTER_VERSION_INVALID', 'A numeric Codex app version is required.');
}
- const adapter = ADAPTERS.find((candidate) =>
+ const verifiedAdapter = ADAPTERS.find((candidate) =>
candidate.appVersions.some((range) => versionMatchesRange(appVersion, range)),
);
- if (!adapter) {
- throw new AdapterError(
- 'THEME_ADAPTER_UNSUPPORTED',
- `No full-theme adapter declares support for Codex ${appVersion}; official UI was left unchanged.`,
- );
- }
+ const adapter = verifiedAdapter ?? BEST_EFFORT_ADAPTER;
let css;
try {
@@ -51,5 +56,10 @@ export async function loadCodexAdapter(appVersion) {
if (!css.includes('html.awesome-codex-theme')) {
throw new AdapterError('THEME_ADAPTER_INVALID', `Built-in adapter ${adapter.id} is not namespaced.`);
}
- return { id: adapter.id, appVersions: [...adapter.appVersions], css };
+ return {
+ id: adapter.id,
+ appVersions: [...adapter.appVersions],
+ verified: Boolean(verifiedAdapter),
+ css,
+ };
}
diff --git a/src/engine/adapters/codex-26.707.css b/src/engine/adapters/codex-26.707.css
index 02dd475..86276a7 100644
--- a/src/engine/adapters/codex-26.707.css
+++ b/src/engine/adapters/codex-26.707.css
@@ -1,5 +1,5 @@
/*
- * Awesome Codex Themes — Codex Desktop 26.707 adapter
+ * Awesome Codex Themes — shared Codex Desktop 26.707 / 26.715 adapter mapping
* Original MIT-licensed compatibility layer. Rules are scoped to the project marker.
*/
@@ -236,14 +236,6 @@ html.awesome-codex-theme :is(input, textarea)::placeholder {
opacity: 1 !important;
}
-html.awesome-codex-theme :is(button, [role="button"], a) {
- border-color: var(--act-border-subtle);
-}
-
-html.awesome-codex-theme :is(button, [role="button"], a):hover {
- border-color: var(--act-border) !important;
-}
-
html.awesome-codex-theme :is(button, [role="button"], a, input, textarea, select, [contenteditable="true"]):focus-visible {
outline: 2px solid var(--act-focus) !important;
outline-offset: 2px !important;
@@ -338,6 +330,23 @@ html.awesome-codex-theme :is(
border-color: var(--act-border-subtle) !important;
}
+html.awesome-codex-theme[data-awesome-codex-artifact-panel-state="open"]:not([data-awesome-codex-theme="satoru-gojo"])
+ [data-awesome-codex-artifact-panel-host="true"] {
+ background-color: color-mix(in srgb, var(--act-surface-elevated) 82%, transparent) !important;
+ background-image:
+ linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--act-surface-overlay) 48%, transparent),
+ color-mix(in srgb, var(--act-background) 68%, transparent)
+ ),
+ var(--act-asset-workspace-panel-safe) !important;
+ background-position: center !important;
+ background-repeat: no-repeat !important;
+ background-size: cover !important;
+ border-radius: 24px 0 0 24px;
+ box-shadow: inset 1px 0 0 var(--act-border-strong);
+}
+
@media (prefers-reduced-motion: reduce) {
html.awesome-codex-theme *,
html.awesome-codex-theme *::before,
diff --git a/src/engine/agent-state.mjs b/src/engine/agent-state.mjs
new file mode 100644
index 0000000..48d58d2
--- /dev/null
+++ b/src/engine/agent-state.mjs
@@ -0,0 +1,99 @@
+import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
+import { dirname } from 'node:path';
+
+const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+const STATUSES = new Set([
+ 'idle',
+ 'starting',
+ 'active',
+ 'restart-required',
+ 'paused',
+ 'error',
+]);
+
+export class AgentStateError extends Error {
+ constructor(code, message, options = {}) {
+ super(message, options);
+ this.name = 'AgentStateError';
+ this.code = code;
+ }
+}
+
+function fail(code, message, options) {
+ throw new AgentStateError(code, message, options);
+}
+
+function nullableString(value, field) {
+ if (value === null) return null;
+ if (typeof value !== 'string' || !value.trim()) fail('AGENT_STATE_FIELD_INVALID', `${field} must be null or a non-empty string.`);
+ return value;
+}
+
+export function validateAgentState(input) {
+ if (!input || typeof input !== 'object' || Array.isArray(input) || input.schemaVersion !== 1) {
+ fail('AGENT_STATE_SCHEMA_INVALID', 'Agent state schemaVersion must be 1.');
+ }
+ if (!STATUSES.has(input.status)) fail('AGENT_STATE_STATUS_INVALID', 'Agent state status is unsupported.');
+ if (typeof input.themeSlug !== 'string' || !SLUG_PATTERN.test(input.themeSlug)) {
+ fail('AGENT_STATE_THEME_INVALID', 'themeSlug must use lowercase kebab-case.');
+ }
+ if (input.appPid !== null && (!Number.isInteger(input.appPid) || input.appPid < 1)) {
+ fail('AGENT_STATE_PID_INVALID', 'appPid must be null or a positive PID.');
+ }
+ if (input.port !== null && (!Number.isInteger(input.port) || input.port < 1024 || input.port > 65_535)) {
+ fail('AGENT_STATE_PORT_INVALID', 'port must be null or a non-privileged TCP port.');
+ }
+ return {
+ schemaVersion: 1,
+ status: input.status,
+ themeSlug: input.themeSlug,
+ appPid: input.appPid,
+ appVersion: nullableString(input.appVersion, 'appVersion'),
+ port: input.port,
+ errorCode: nullableString(input.errorCode, 'errorCode'),
+ updatedAt: nullableString(input.updatedAt, 'updatedAt'),
+ };
+}
+
+export async function isActiveAgentStateLive(
+ input,
+ { discoverApp, listPids, assertPortOwner, isAgentLoaded },
+) {
+ const state = validateAgentState(input);
+ if (state.status !== 'active' || state.appPid === null || state.port === null) return false;
+ try {
+ if (!(await isAgentLoaded())) return false;
+ const app = await discoverApp();
+ if (app.version !== state.appVersion) return false;
+ const pids = await listPids(app);
+ if (!pids.includes(state.appPid)) return false;
+ await assertPortOwner(app, state.port);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export async function writeAgentState(path, input) {
+ const state = validateAgentState(input);
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
+ const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
+ try {
+ await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
+ await chmod(temporaryPath, 0o600);
+ await rename(temporaryPath, path);
+ await chmod(path, 0o600);
+ } catch (error) {
+ await rm(temporaryPath, { force: true }).catch(() => {});
+ fail('AGENT_STATE_WRITE_FAILED', 'Unable to write agent state atomically.', { cause: error });
+ }
+}
+
+export async function readAgentState(path) {
+ try {
+ return validateAgentState(JSON.parse(await readFile(path, 'utf8')));
+ } catch (error) {
+ if (error instanceof AgentStateError) throw error;
+ fail('AGENT_STATE_READ_FAILED', 'Agent state is missing or malformed.', { cause: error });
+ }
+}
diff --git a/src/engine/agent.mjs b/src/engine/agent.mjs
new file mode 100644
index 0000000..81b7edb
--- /dev/null
+++ b/src/engine/agent.mjs
@@ -0,0 +1,177 @@
+import { readAgentConfig } from './config.mjs';
+import { writeAgentState } from './agent-state.mjs';
+
+const LOST_MANAGED_ENDPOINT_ERRORS = new Set([
+ 'CDP_PORT_UNAVAILABLE',
+ 'CDP_PORT_OWNER_INVALID',
+ 'CDP_LISTENER_UNSAFE',
+]);
+
+function observedState({ status, themeSlug, appPid = null, appVersion = null, port = null, errorCode = null }, now) {
+ return {
+ schemaVersion: 1,
+ status,
+ themeSlug,
+ appPid,
+ appVersion,
+ port,
+ errorCode,
+ updatedAt: now(),
+ };
+}
+
+function failureStatus() {
+ return 'error';
+}
+
+export async function agentStep({ config, runtime }, dependencies) {
+ const {
+ discoverApp,
+ listPids,
+ selectPort,
+ launchApp,
+ waitForRenderer,
+ assertPortOwner,
+ applyTheme,
+ removeTheme,
+ now = () => new Date().toISOString(),
+ } = dependencies;
+
+ const app = await discoverApp();
+ const runningPids = await listPids(app);
+
+ if (!config.enabled) {
+ let pausedRuntime = null;
+ if (runtime && runningPids.includes(runtime.appPid)) {
+ try {
+ await assertPortOwner(app, runtime.port);
+ await removeTheme({ port: runtime.port, app });
+ pausedRuntime = { ...runtime, themeSlug: config.themeSlug };
+ } catch {
+ // Pausing must not broaden process control when the recorded endpoint is gone.
+ }
+ }
+ return {
+ runtime: pausedRuntime,
+ state: observedState(
+ {
+ status: 'paused',
+ themeSlug: config.themeSlug,
+ appPid: pausedRuntime?.appPid ?? null,
+ appVersion: app.version,
+ port: pausedRuntime?.port ?? null,
+ },
+ now,
+ ),
+ };
+ }
+
+ let managed = runtime && runningPids.includes(runtime.appPid) ? runtime : null;
+ if (!managed && runningPids.length > 0) {
+ return {
+ runtime: null,
+ state: observedState(
+ {
+ status: 'restart-required',
+ themeSlug: config.themeSlug,
+ appPid: runningPids[0],
+ appVersion: app.version,
+ errorCode: 'APP_RUNNING_WITHOUT_MANAGED_CDP',
+ },
+ now,
+ ),
+ };
+ }
+
+ if (!managed) {
+ const port = await selectPort();
+ const launched = await launchApp(app, port);
+ if (!Number.isInteger(launched?.pid) || launched.pid < 1) {
+ const error = Object.assign(new Error('The official app did not return a valid PID.'), { code: 'APP_LAUNCH_FAILED' });
+ return {
+ runtime: null,
+ state: observedState(
+ { status: 'error', themeSlug: config.themeSlug, appVersion: app.version, errorCode: error.code },
+ now,
+ ),
+ };
+ }
+ managed = { appPid: launched.pid, port, themeSlug: config.themeSlug };
+ try {
+ await waitForRenderer(port, app);
+ } catch (error) {
+ return {
+ runtime: managed,
+ state: observedState(
+ {
+ status: failureStatus(error),
+ themeSlug: config.themeSlug,
+ appPid: managed.appPid,
+ appVersion: app.version,
+ port,
+ errorCode: error?.code ?? 'UNEXPECTED_ERROR',
+ },
+ now,
+ ),
+ };
+ }
+ }
+
+ try {
+ await assertPortOwner(app, managed.port);
+ await applyTheme({ themeSlug: config.themeSlug, port: managed.port, app });
+ const nextRuntime = { ...managed, themeSlug: config.themeSlug };
+ return {
+ runtime: nextRuntime,
+ state: observedState(
+ {
+ status: 'active',
+ themeSlug: config.themeSlug,
+ appPid: nextRuntime.appPid,
+ appVersion: app.version,
+ port: nextRuntime.port,
+ },
+ now,
+ ),
+ };
+ } catch (error) {
+ const nextRuntime = LOST_MANAGED_ENDPOINT_ERRORS.has(error?.code)
+ ? null
+ : { ...managed, themeSlug: config.themeSlug };
+ return {
+ runtime: nextRuntime,
+ state: observedState(
+ {
+ status: failureStatus(error),
+ themeSlug: config.themeSlug,
+ appPid: managed.appPid,
+ appVersion: app.version,
+ port: managed.port,
+ errorCode: error?.code ?? 'UNEXPECTED_ERROR',
+ },
+ now,
+ ),
+ };
+ }
+}
+
+export async function runPersistentAgent(
+ { configPath, statePath, intervalMs = 1_500, initialRuntime = null },
+ {
+ readConfig = readAgentConfig,
+ writeState = writeAgentState,
+ stepDependencies,
+ delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
+ shouldStop = () => false,
+ },
+) {
+ let runtime = initialRuntime;
+ while (!shouldStop()) {
+ const config = await readConfig(configPath);
+ const result = await agentStep({ config, runtime }, stepDependencies);
+ runtime = result.runtime;
+ await writeState(statePath, result.state);
+ await delay(intervalMs);
+ }
+ return { stopped: true, runtime };
+}
diff --git a/src/engine/config.mjs b/src/engine/config.mjs
new file mode 100644
index 0000000..a8001b9
--- /dev/null
+++ b/src/engine/config.mjs
@@ -0,0 +1,58 @@
+import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
+import { dirname } from 'node:path';
+
+const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+
+export class AgentConfigError extends Error {
+ constructor(code, message, options = {}) {
+ super(message, options);
+ this.name = 'AgentConfigError';
+ this.code = code;
+ }
+}
+
+function fail(code, message, options) {
+ throw new AgentConfigError(code, message, options);
+}
+
+export function validateAgentConfig(input) {
+ if (!input || typeof input !== 'object' || Array.isArray(input) || input.schemaVersion !== 1) {
+ fail('CONFIG_SCHEMA_INVALID', 'Agent configuration schemaVersion must be 1.');
+ }
+ if (typeof input.enabled !== 'boolean' || typeof input.launchAtLogin !== 'boolean') {
+ fail('CONFIG_FIELD_INVALID', 'enabled and launchAtLogin must be booleans.');
+ }
+ if (typeof input.themeSlug !== 'string' || !SLUG_PATTERN.test(input.themeSlug)) {
+ fail('CONFIG_THEME_INVALID', 'themeSlug must use lowercase kebab-case.');
+ }
+ return {
+ schemaVersion: 1,
+ enabled: input.enabled,
+ themeSlug: input.themeSlug,
+ launchAtLogin: input.launchAtLogin,
+ };
+}
+
+export async function writeAgentConfig(path, input) {
+ const config = validateAgentConfig(input);
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
+ const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
+ try {
+ await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
+ await chmod(temporaryPath, 0o600);
+ await rename(temporaryPath, path);
+ await chmod(path, 0o600);
+ } catch (error) {
+ await rm(temporaryPath, { force: true }).catch(() => {});
+ fail('CONFIG_WRITE_FAILED', 'Unable to write agent configuration atomically.', { cause: error });
+ }
+}
+
+export async function readAgentConfig(path) {
+ try {
+ return validateAgentConfig(JSON.parse(await readFile(path, 'utf8')));
+ } catch (error) {
+ if (error instanceof AgentConfigError) throw error;
+ fail('CONFIG_READ_FAILED', 'Agent configuration is missing or malformed.', { cause: error });
+ }
+}
diff --git a/src/engine/conflicts.mjs b/src/engine/conflicts.mjs
new file mode 100644
index 0000000..7f89383
--- /dev/null
+++ b/src/engine/conflicts.mjs
@@ -0,0 +1,62 @@
+import { execFile } from 'node:child_process';
+import { readFile, readdir } from 'node:fs/promises';
+import { join } from 'node:path';
+import { promisify } from 'node:util';
+
+const execFileAsync = promisify(execFile);
+
+export function detectInjectorConflicts({ launchAgents = [], processCommands = [] }) {
+ const conflicts = [];
+ for (const agent of launchAgents) {
+ const evidence = `${agent.path}\n${agent.content}`;
+ if (/com\.kkkk\.codex-nocturne|codex-nocturne\/theme_agent\.py/i.test(evidence)) {
+ conflicts.push({
+ id: 'legacy-nocturne-launch-agent',
+ path: agent.path,
+ recovery: 'Boot out the legacy agent only after restoring its owned theme state.',
+ });
+ }
+ }
+
+ if (
+ processCommands.some(
+ (command) =>
+ /\/(?:ChatGPT|Codex)(?:\.app)?\//i.test(command) &&
+ /--remote-allow-origins=\*/.test(command),
+ )
+ ) {
+ conflicts.push({
+ id: 'wildcard-cdp-origin',
+ recovery: 'Relaunch through the audited loopback-only launcher after explicit user approval.',
+ });
+ }
+ return conflicts;
+}
+
+export async function inspectInjectorConflicts(
+ home,
+ {
+ readDirectory = readdir,
+ readText = (path) => readFile(path, 'utf8'),
+ listCommands = async () => (await execFileAsync('/bin/ps', ['-axo', 'command='], { encoding: 'utf8' })).stdout.split('\n'),
+ } = {},
+) {
+ const directory = join(home, 'Library/LaunchAgents');
+ let entries = [];
+ try {
+ entries = await readDirectory(directory, { withFileTypes: true });
+ } catch {
+ entries = [];
+ }
+ const launchAgents = [];
+ for (const entry of entries) {
+ if (!entry.isFile() || !entry.name.endsWith('.plist')) continue;
+ const path = join(directory, entry.name);
+ try {
+ launchAgents.push({ path, content: await readText(path) });
+ } catch {
+ // Unreadable unrelated agents are outside this project's ownership.
+ }
+ }
+ return detectInjectorConflicts({ launchAgents, processCommands: await listCommands() });
+}
diff --git a/src/engine/control.mjs b/src/engine/control.mjs
new file mode 100644
index 0000000..51dd7d4
--- /dev/null
+++ b/src/engine/control.mjs
@@ -0,0 +1,39 @@
+import { readAgentState } from './agent-state.mjs';
+import { readAgentConfig, writeAgentConfig } from './config.mjs';
+import { kickstartLaunchAgent } from './launch-agent.mjs';
+
+export async function pausePersistentInstallation(
+ { configPath, statePath },
+ {
+ readConfig = readAgentConfig,
+ writeConfig = writeAgentConfig,
+ readState = readAgentState,
+ kickstart = kickstartLaunchAgent,
+ removeTheme,
+ } = {},
+) {
+ const config = await readConfig(configPath);
+ await writeConfig(configPath, { ...config, enabled: false });
+
+ let state = null;
+ try {
+ state = await readState(statePath);
+ } catch (error) {
+ if (error?.code !== 'AGENT_STATE_READ_FAILED') throw error;
+ }
+
+ await kickstart();
+
+ let liveRemoval = 'not-reachable';
+ if (state?.port !== null && state?.port !== undefined && typeof removeTheme === 'function') {
+ try {
+ await removeTheme({ port: state.port, state });
+ liveRemoval = 'removed';
+ } catch {
+ // Persistence is already disabled. Never broaden process control just to
+ // turn an unreachable cleanup request into a success claim.
+ }
+ }
+
+ return { paused: true, theme: config.themeSlug, liveRemoval };
+}
diff --git a/src/engine/injection.mjs b/src/engine/injection.mjs
index 6ca8e22..50d50a6 100644
--- a/src/engine/injection.mjs
+++ b/src/engine/injection.mjs
@@ -6,6 +6,8 @@ export const CHROME_ID = 'awesome-codex-theme-chrome';
export const EXPERIENCE_MARKER = 'awesome-codex-theme-experience';
const OWNER = 'awesome-codex-themes';
const SURFACE_OBSERVER_KEY = '__awesomeCodexThemeSurfaceObserver';
+const AUXILIARY_ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+const AUXILIARY_ASSET_VARIABLE_PREFIX = '--act-asset-';
function roleToVariable(role) {
return `--act-${role.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
@@ -60,6 +62,19 @@ function runtimePayload(theme, adapter) {
return [variable, value];
}),
);
+ const assetVariables = Object.fromEntries(
+ Object.entries(theme.assets ?? {}).map(([name, asset]) => {
+ if (
+ !AUXILIARY_ASSET_NAME_PATTERN.test(name) ||
+ !asset ||
+ typeof asset.dataUrl !== 'string' ||
+ !asset.dataUrl.startsWith('data:image/')
+ ) {
+ throw new TypeError(`Theme runtime asset is invalid: ${name}`);
+ }
+ return [`${AUXILIARY_ASSET_VARIABLE_PREFIX}${name}`, `url("${asset.dataUrl}")`];
+ }),
+ );
return {
slug: manifest.slug,
colorScheme: manifest.mode === 'system' ? 'light dark' : manifest.mode,
@@ -68,6 +83,7 @@ function runtimePayload(theme, adapter) {
adapterCss: adapter.css,
artworkDataUrl: artwork.dataUrl,
variables,
+ assetVariables,
experience: manifest.experience?.chrome
? {
brand: manifest.experience.brand,
@@ -107,9 +123,18 @@ export function buildApplyExpression(theme, adapter) {
root.dataset.awesomeCodexAdapter = payload.adapterId;
root.style.setProperty('--act-artwork', 'url("' + payload.artworkDataUrl + '")');
root.style.setProperty('--act-color-scheme', payload.colorScheme);
+ for (let index = root.style.length - 1; index >= 0; index -= 1) {
+ const variable = root.style.item(index);
+ if (variable.startsWith(${serializeForExpression(AUXILIARY_ASSET_VARIABLE_PREFIX)})) {
+ root.style.removeProperty(variable);
+ }
+ }
for (const [variable, value] of Object.entries(payload.variables)) {
root.style.setProperty(variable, value);
}
+ for (const [variable, value] of Object.entries(payload.assetVariables)) {
+ root.style.setProperty(variable, value);
+ }
if (payload.experience && document.body) {
const chrome = currentChrome ?? document.createElement('div');
@@ -168,24 +193,391 @@ export function buildApplyExpression(theme, adapter) {
const computed = getComputedStyle(node);
return rect.width > 1 && rect.height > 1 && computed.display !== 'none' && computed.visibility !== 'hidden' && computed.opacity !== '0';
};
+ const isEffectivelyVisible = (node) => {
+ if (!(node instanceof Element)) return false;
+ const rect = node.getBoundingClientRect();
+ if (
+ rect.width <= 1 ||
+ rect.height <= 1 ||
+ rect.right <= 0 ||
+ rect.left >= window.innerWidth ||
+ rect.bottom <= 0 ||
+ rect.top >= window.innerHeight
+ ) return false;
+ for (let current = node; current && current !== document; current = current.parentElement) {
+ const computed = getComputedStyle(current);
+ if (
+ computed.display === 'none' ||
+ computed.visibility === 'hidden' ||
+ Number.parseFloat(computed.opacity || '1') < 0.01
+ ) return false;
+ }
+ return true;
+ };
+ const findPluginSearchInput = () => {
+ for (const candidate of document.querySelectorAll('input[placeholder]')) {
+ const placeholder = candidate.getAttribute('placeholder') || '';
+ if (/search plugins|搜索插件/i.test(placeholder)) return candidate;
+ }
+ return null;
+ };
+ const findScheduledSearchInput = () => {
+ for (const candidate of document.querySelectorAll('input[placeholder]')) {
+ const placeholder = candidate.getAttribute('placeholder') || '';
+ if (/search scheduled|搜索已安排/i.test(placeholder)) return candidate;
+ }
+ return null;
+ };
+ const findSitesSearchInput = () => {
+ for (const candidate of document.querySelectorAll('input[placeholder]')) {
+ const placeholder = candidate.getAttribute('placeholder') || '';
+ if (/search sites|搜索站点/i.test(placeholder)) return candidate;
+ }
+ return null;
+ };
+ const findPullRequestsSearchInput = () => {
+ for (const candidate of document.querySelectorAll('input[placeholder]')) {
+ const placeholder = candidate.getAttribute('placeholder') || '';
+ if (/pull requests?|拉取请求/i.test(placeholder)) return candidate;
+ }
+ return null;
+ };
+ const findPullRequestsRoot = () => {
+ for (const heading of document.querySelectorAll('h1')) {
+ const copy = heading.textContent?.trim() || '';
+ if (/^pull requests?$/i.test(copy) || /^拉取请求$/.test(copy)) {
+ return (
+ heading.closest('[role="main"]') ||
+ heading.closest('.app-shell-main-content-viewport') ||
+ heading.closest('.app-shell-main-content-frame') ||
+ document.body
+ );
+ }
+ }
+ return null;
+ };
+ const closestMainSurface = (candidate) => {
+ if (!candidate) return null;
+ return (
+ candidate.closest?.('[role="main"]') ||
+ candidate.closest?.('.app-shell-main-content-viewport') ||
+ candidate.closest?.('.app-shell-main-content-frame') ||
+ document.body
+ );
+ };
+ const updateComponentRoles = () => {
+ const roleKeys = [
+ 'awesomeCodexArtifactPanel',
+ 'awesomeCodexArtifactPanelHost',
+ 'awesomeCodexArtifactHeader',
+ 'awesomeCodexPanelMore',
+ 'awesomeCodexCompletedSummary',
+ 'awesomeCodexActivityItem',
+ 'awesomeCodexActivitySummary',
+ 'awesomeCodexHeaderControl',
+ 'awesomeCodexSidebarBrand',
+ 'awesomeCodexSidebarNav',
+ 'awesomeCodexDomainHeader',
+ 'awesomeCodexProjectRow',
+ 'awesomeCodexDomainId',
+ 'awesomeCodexTaskRow',
+ 'awesomeCodexAccountTrigger',
+ 'awesomeCodexAccountIdentity',
+ 'awesomeCodexHelpTrigger',
+ 'awesomeCodexHomePrompt',
+ 'awesomeCodexDomainMarker',
+ 'awesomeCodexComposerControl',
+ 'awesomeCodexPluginSearch',
+ 'awesomeCodexPluginTab',
+ 'awesomeCodexPluginSectionHeader',
+ 'awesomeCodexPluginInstalled',
+ 'awesomeCodexPluginCard',
+ 'awesomeCodexPluginIcon',
+ 'awesomeCodexPluginAction',
+ 'awesomeCodexCollectionSearch',
+ 'awesomeCodexCollectionTab',
+ 'awesomeCodexCollectionSectionHeader',
+ 'awesomeCodexCollectionRow',
+ 'awesomeCodexCollectionEmpty',
+ 'awesomeCodexCollectionStatus',
+ 'awesomeCodexCollectionAction',
+ ];
+ for (const roleKey of roleKeys) {
+ const attribute = 'data-' + roleKey.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase());
+ for (const candidate of document.querySelectorAll('[' + attribute + ']')) {
+ delete candidate.dataset[roleKey];
+ }
+ }
+
+ for (const candidate of document.querySelectorAll('.bg-token-dropdown-background:not(header)')) {
+ const headers = candidate.querySelectorAll('section > header.bg-token-dropdown-background');
+ if (headers.length === 0) continue;
+ candidate.dataset.awesomeCodexArtifactPanel = 'true';
+ const panelHost =
+ candidate.closest('[data-pip-obstacle="thread-summary-panel"]') || candidate.parentElement;
+ if (panelHost) panelHost.dataset.awesomeCodexArtifactPanelHost = 'true';
+ for (const header of headers) header.dataset.awesomeCodexArtifactHeader = 'true';
+ for (const panelMore of candidate.querySelectorAll('.text-token-conversation-summary-trailing')) {
+ panelMore.dataset.awesomeCodexPanelMore = 'true';
+ }
+ }
+
+ for (const candidate of document.querySelectorAll('button')) {
+ const isCompletedSummary = Boolean(
+ candidate.matches('.text-size-chat.inline-flex.items-center.gap-1') &&
+ candidate.querySelector('.text-token-conversation-body'),
+ );
+ if (isCompletedSummary) {
+ candidate.dataset.awesomeCodexCompletedSummary = 'true';
+ } else if (candidate.classList.contains('group/activity-header')) {
+ candidate.dataset.awesomeCodexActivityItem = 'true';
+ }
+ }
+
+ for (const candidate of document.querySelectorAll('.app-header-tint :is(button, [role="button"])')) {
+ candidate.dataset.awesomeCodexHeaderControl = 'true';
+ }
+
+ for (const candidate of document.querySelectorAll('[class~="group/folder-row"]')) {
+ candidate.dataset.awesomeCodexProjectRow = 'true';
+ }
+ for (const [index, candidate] of [
+ ...document.querySelectorAll('[data-awesome-codex-project-row="true"]'),
+ ].entries()) {
+ candidate.dataset.awesomeCodexDomainId = 'D-' + String(index + 1).padStart(2, '0');
+ }
+ for (const dragHandle of document.querySelectorAll('[class~="cursor-grab"]')) {
+ const taskRow = dragHandle.closest('div.group.relative') || dragHandle.parentElement;
+ if (taskRow) taskRow.dataset.awesomeCodexTaskRow = 'true';
+ }
+
+ for (const sidebar of document.querySelectorAll('.app-shell-left-panel')) {
+ const domainHeaders = [...sidebar.querySelectorAll('[class~="group/section-toggle"]')];
+ for (const candidate of domainHeaders) {
+ candidate.dataset.awesomeCodexDomainHeader = 'true';
+ }
+ const firstDomainHeader = domainHeaders[0] ?? null;
+ for (const candidate of sidebar.querySelectorAll('button')) {
+ const label = (candidate.getAttribute('aria-label') || '').toLocaleLowerCase();
+ if (/switch mode|切换模式/.test(label)) {
+ candidate.dataset.awesomeCodexSidebarBrand = 'true';
+ }
+ if (candidate.querySelector('img.rounded-full')) {
+ candidate.dataset.awesomeCodexAccountIdentity = 'true';
+ candidate.dataset.awesomeCodexAccountTrigger = 'true';
+ }
+ }
+ for (const candidate of sidebar.querySelectorAll('button')) {
+ if (candidate === firstDomainHeader) break;
+ if (candidate.classList.contains('h-[var(--height-token-row)]')) {
+ const currentIndex = sidebar.querySelectorAll('[data-awesome-codex-sidebar-nav]').length + 1;
+ candidate.dataset.awesomeCodexSidebarNav = String(currentIndex).padStart(2, '0');
+ }
+ }
+ for (const candidate of sidebar.querySelectorAll('button[aria-label], [role="button"][aria-label]')) {
+ const label = (candidate.getAttribute('aria-label') || '').toLocaleLowerCase();
+ if (
+ label.includes('profile menu') ||
+ label.includes('account menu') ||
+ label.includes('个人资料菜单') ||
+ label.includes('账户菜单')
+ ) {
+ candidate.dataset.awesomeCodexAccountTrigger = 'true';
+ } else if (label.includes('help') || label.includes('帮助')) {
+ candidate.dataset.awesomeCodexHelpTrigger = 'true';
+ }
+ }
+ }
+
+ const homePrompt = document.querySelector(
+ '[data-awesome-codex-home="true"] [data-feature="game-source"]',
+ );
+ if (homePrompt) {
+ homePrompt.dataset.awesomeCodexHomePrompt = 'true';
+ const domainMarker = homePrompt.querySelector('button');
+ if (domainMarker) domainMarker.dataset.awesomeCodexDomainMarker = 'true';
+ }
+
+ for (const composer of document.querySelectorAll('.composer-surface-chrome')) {
+ for (const candidate of composer.querySelectorAll('button')) {
+ const label = (candidate.getAttribute('aria-label') || '').toLocaleLowerCase();
+ const isSquare = candidate.classList.contains('aspect-square');
+ let control = '';
+ if (
+ candidate.matches('[data-testid="composer-send-button"], .composer-send-button') ||
+ candidate.classList.contains('size-token-button-composer') ||
+ candidate.classList.contains('bg-token-foreground')
+ ) {
+ control = 'primary';
+ } else if (label.includes('dictation') || label.includes('听写')) {
+ control = 'dictation';
+ } else if (
+ label.includes('add files') ||
+ label.includes('attach') ||
+ label.includes('添加文件') ||
+ label.includes('附件')
+ ) {
+ control = 'attachment';
+ } else if (candidate.classList.contains('h-token-button-composer-sm')) {
+ control = 'access';
+ } else if (candidate.classList.contains('h-token-button-composer') && !isSquare) {
+ control = 'model';
+ }
+ if (control) candidate.dataset.awesomeCodexComposerControl = control;
+ }
+ }
+
+ const pluginSearchInput = findPluginSearchInput();
+ if (pluginSearchInput) {
+ const pluginRoot = closestMainSurface(pluginSearchInput);
+ const searchSurface = pluginSearchInput.parentElement;
+ if (searchSurface) {
+ searchSurface.dataset.awesomeCodexPluginSearch = 'true';
+ searchSurface.dataset.awesomeCodexCollectionSearch = 'true';
+ }
+
+ for (const heading of pluginRoot.querySelectorAll('h2')) {
+ if (heading.parentElement) {
+ heading.parentElement.dataset.awesomeCodexPluginSectionHeader = 'true';
+ heading.parentElement.dataset.awesomeCodexCollectionSectionHeader = 'true';
+ }
+ }
+
+ for (const installedRow of pluginRoot.querySelectorAll('[class~="group/plugin-row"]')) {
+ for (const button of installedRow.querySelectorAll(':scope > button')) {
+ button.dataset.awesomeCodexPluginInstalled = 'true';
+ button.dataset.awesomeCodexCollectionRow = 'true';
+ const icon = button.querySelector('img')?.parentElement;
+ if (icon) icon.dataset.awesomeCodexPluginIcon = 'true';
+ }
+ }
+
+ for (const candidate of pluginRoot.querySelectorAll('button.shrink-0')) {
+ if (
+ candidate.classList.contains('h-token-button-composer') &&
+ !candidate.closest('[data-awesome-codex-plugin-card="true"]') &&
+ !candidate.closest('[class~="group/plugin-row"]')
+ ) {
+ candidate.dataset.awesomeCodexPluginTab = 'true';
+ candidate.dataset.awesomeCodexCollectionTab = 'true';
+ }
+ }
+
+ for (const candidate of pluginRoot.querySelectorAll('div[role="button"][tabindex="0"]')) {
+ const iconImage = candidate.querySelector('img');
+ const contentRow = candidate.querySelector(':scope > .flex.items-center.gap-3');
+ if (!iconImage || !contentRow) continue;
+ candidate.dataset.awesomeCodexPluginCard = 'true';
+ candidate.dataset.awesomeCodexCollectionRow = 'true';
+ const icon = iconImage.parentElement;
+ if (icon) icon.dataset.awesomeCodexPluginIcon = 'true';
+ for (const action of candidate.querySelectorAll('button')) {
+ action.dataset.awesomeCodexPluginAction = action.classList.contains('aspect-square')
+ ? 'menu'
+ : 'install';
+ }
+ }
+ }
+
+ const scheduledSearchInput = findScheduledSearchInput();
+ if (scheduledSearchInput) {
+ const scheduledRoot = closestMainSurface(scheduledSearchInput);
+ if (scheduledSearchInput.parentElement) {
+ scheduledSearchInput.parentElement.dataset.awesomeCodexCollectionSearch = 'true';
+ }
+ for (const candidate of scheduledRoot.querySelectorAll('button.shrink-0.h-token-button-composer')) {
+ candidate.dataset.awesomeCodexCollectionTab = 'true';
+ }
+ for (const candidate of scheduledRoot.querySelectorAll('.automation-row')) {
+ candidate.dataset.awesomeCodexCollectionRow = 'true';
+ }
+ for (const heading of scheduledRoot.querySelectorAll('h2')) {
+ if (heading.parentElement) {
+ heading.parentElement.dataset.awesomeCodexCollectionSectionHeader = 'true';
+ }
+ }
+ }
+
+ const sitesSearchInput = findSitesSearchInput();
+ if (sitesSearchInput) {
+ const sitesRoot = closestMainSurface(sitesSearchInput);
+ if (sitesSearchInput.parentElement) {
+ sitesSearchInput.parentElement.dataset.awesomeCodexCollectionSearch = 'true';
+ }
+ const emptyState = sitesRoot.querySelector('.max-w-xl.flex-col.items-center');
+ if (emptyState) {
+ emptyState.dataset.awesomeCodexCollectionEmpty = 'true';
+ const action = emptyState.querySelector('button');
+ if (action) action.dataset.awesomeCodexCollectionAction = 'primary';
+ }
+ }
+
+ const pullRequestsSearchInput = findPullRequestsSearchInput();
+ const pullRequestsRoot =
+ closestMainSurface(pullRequestsSearchInput) || findPullRequestsRoot();
+ if (pullRequestsRoot) {
+ if (pullRequestsSearchInput?.parentElement) {
+ pullRequestsSearchInput.parentElement.dataset.awesomeCodexCollectionSearch = 'true';
+ }
+ for (const candidate of pullRequestsRoot.querySelectorAll('button.shrink-0.h-token-button-composer')) {
+ candidate.dataset.awesomeCodexCollectionTab = 'true';
+ }
+ const status = pullRequestsRoot.querySelector('[role="status"]');
+ if (status) status.dataset.awesomeCodexCollectionStatus = 'true';
+ const emptyState = pullRequestsRoot.querySelector('.max-w-xl.flex-col.items-center');
+ if (emptyState) emptyState.dataset.awesomeCodexCollectionEmpty = 'true';
+ }
+ };
const updateSurface = () => {
+ const pluginSearchInput = findPluginSearchInput();
+ const scheduledSearchInput = findScheduledSearchInput();
+ const sitesSearchInput = findSitesSearchInput();
+ const pullRequestsSearchInput = findPullRequestsSearchInput();
+ const pullRequestsRoot = findPullRequestsRoot();
let home = null;
- for (const candidate of document.querySelectorAll('[role="main"]')) {
- const icon = candidate.querySelector('[data-testid="home-icon"]');
- const source = candidate.querySelector('[data-feature="game-source"]');
- const suggestions = candidate.querySelector('[class~="group/home-suggestions"]');
- const hasIdentity = isVisible(icon) && isVisible(source);
- const hasActions = isVisible(source) && isVisible(suggestions);
- if (isVisible(candidate) && (hasIdentity || hasActions)) {
- home = candidate;
- break;
+ if (
+ !pluginSearchInput &&
+ !scheduledSearchInput &&
+ !sitesSearchInput &&
+ !pullRequestsSearchInput &&
+ !pullRequestsRoot
+ ) {
+ for (const candidate of document.querySelectorAll('[role="main"]')) {
+ const icon = candidate.querySelector('[data-testid="home-icon"]');
+ const source = candidate.querySelector('[data-feature="game-source"]');
+ const suggestions = candidate.querySelector('[class~="group/home-suggestions"]');
+ const hasIdentity = isVisible(icon) && isVisible(source);
+ const hasActions = isVisible(source) && isVisible(suggestions);
+ if (isVisible(candidate) && (hasIdentity || hasActions)) {
+ home = candidate;
+ break;
+ }
}
}
for (const candidate of document.querySelectorAll('[data-awesome-codex-home]')) {
if (candidate !== home) delete candidate.dataset.awesomeCodexHome;
}
if (home) home.dataset.awesomeCodexHome = 'true';
- root.dataset.awesomeCodexSurface = home ? 'home' : 'workspace';
+ root.dataset.awesomeCodexSurface = pluginSearchInput
+ ? 'plugins'
+ : scheduledSearchInput
+ ? 'scheduled'
+ : sitesSearchInput
+ ? 'sites'
+ : pullRequestsSearchInput || pullRequestsRoot
+ ? 'pull-requests'
+ : home
+ ? 'home'
+ : 'workspace';
+ updateComponentRoles();
+ const artifactPanels = [...document.querySelectorAll('[data-awesome-codex-artifact-panel="true"]')];
+ if (artifactPanels.length === 0) {
+ delete root.dataset.awesomeCodexArtifactPanelState;
+ } else {
+ root.dataset.awesomeCodexArtifactPanelState = artifactPanels.some(isEffectivelyVisible)
+ ? 'open'
+ : 'closed';
+ }
};
updateSurface();
const previousObserver = window[${serializeForExpression(SURFACE_OBSERVER_KEY)}];
@@ -200,7 +592,12 @@ export function buildApplyExpression(theme, adapter) {
updateSurface();
});
});
- observer.observe(document.body, { childList: true, subtree: true });
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['class', 'style', 'aria-pressed'],
+ });
window[${serializeForExpression(SURFACE_OBSERVER_KEY)}] = observer;
}
@@ -243,11 +640,60 @@ export function buildRemoveExpression() {
delete root.dataset.awesomeCodexTheme;
delete root.dataset.awesomeCodexAdapter;
delete root.dataset.awesomeCodexSurface;
+ delete root.dataset.awesomeCodexArtifactPanelState;
for (const variable of ${variables}) root.style.removeProperty(variable);
+ for (let index = root.style.length - 1; index >= 0; index -= 1) {
+ const variable = root.style.item(index);
+ if (variable.startsWith(${serializeForExpression(AUXILIARY_ASSET_VARIABLE_PREFIX)})) {
+ root.style.removeProperty(variable);
+ }
+ }
}
for (const candidate of document.querySelectorAll('[data-awesome-codex-home]')) {
delete candidate.dataset.awesomeCodexHome;
}
+ const roleKeys = [
+ 'awesomeCodexArtifactPanel',
+ 'awesomeCodexArtifactPanelHost',
+ 'awesomeCodexArtifactHeader',
+ 'awesomeCodexPanelMore',
+ 'awesomeCodexCompletedSummary',
+ 'awesomeCodexActivityItem',
+ 'awesomeCodexActivitySummary',
+ 'awesomeCodexHeaderControl',
+ 'awesomeCodexSidebarBrand',
+ 'awesomeCodexSidebarNav',
+ 'awesomeCodexDomainHeader',
+ 'awesomeCodexProjectRow',
+ 'awesomeCodexDomainId',
+ 'awesomeCodexTaskRow',
+ 'awesomeCodexAccountTrigger',
+ 'awesomeCodexAccountIdentity',
+ 'awesomeCodexHelpTrigger',
+ 'awesomeCodexHomePrompt',
+ 'awesomeCodexDomainMarker',
+ 'awesomeCodexComposerControl',
+ 'awesomeCodexPluginSearch',
+ 'awesomeCodexPluginTab',
+ 'awesomeCodexPluginSectionHeader',
+ 'awesomeCodexPluginInstalled',
+ 'awesomeCodexPluginCard',
+ 'awesomeCodexPluginIcon',
+ 'awesomeCodexPluginAction',
+ 'awesomeCodexCollectionSearch',
+ 'awesomeCodexCollectionTab',
+ 'awesomeCodexCollectionSectionHeader',
+ 'awesomeCodexCollectionRow',
+ 'awesomeCodexCollectionEmpty',
+ 'awesomeCodexCollectionStatus',
+ 'awesomeCodexCollectionAction',
+ ];
+ for (const roleKey of roleKeys) {
+ const attribute = 'data-' + roleKey.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase());
+ for (const candidate of document.querySelectorAll('[' + attribute + ']')) {
+ delete candidate.dataset[roleKey];
+ }
+ }
return {
pass: !document.getElementById(${serializeForExpression(STYLE_ID)}) && !document.getElementById(${serializeForExpression(CHROME_ID)}) && !root?.classList?.contains(${serializeForExpression(APPLY_MARKER)}),
action: 'removed',
diff --git a/src/engine/installer.mjs b/src/engine/installer.mjs
new file mode 100644
index 0000000..96e2789
--- /dev/null
+++ b/src/engine/installer.mjs
@@ -0,0 +1,144 @@
+import { access, chmod, cp, mkdir, readdir, rename, rm, symlink } from 'node:fs/promises';
+import { isAbsolute, join } from 'node:path';
+
+import { writeAgentConfig } from './config.mjs';
+import {
+ bootoutLaunchAgent,
+ bootstrapLaunchAgent,
+ buildLaunchAgentPlist,
+ kickstartLaunchAgent,
+ writeLaunchAgentPlist,
+} from './launch-agent.mjs';
+import { loadThemePackage } from './theme.mjs';
+
+const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
+
+export class InstallerError extends Error {
+ constructor(code, message, options = {}) {
+ super(message, options);
+ this.name = 'InstallerError';
+ this.code = code;
+ }
+}
+
+function fail(code, message, options) {
+ throw new InstallerError(code, message, options);
+}
+
+function requireAbsolute(value, field) {
+ if (typeof value !== 'string' || !isAbsolute(value)) fail('INSTALL_PATH_INVALID', `${field} must be an absolute path.`);
+ return value;
+}
+
+export function installationPaths({ home, version }) {
+ const userHome = requireAbsolute(home, 'home');
+ if (typeof version !== 'string' || !VERSION_PATTERN.test(version)) {
+ fail('INSTALL_VERSION_INVALID', 'version must use semantic versioning.');
+ }
+ const supportRoot = join(userHome, 'Library/Application Support/AwesomeCodexThemes');
+ const releasesRoot = join(supportRoot, 'releases');
+ const releaseDir = join(releasesRoot, version);
+ const currentPath = join(supportRoot, 'current');
+ return {
+ supportRoot,
+ releasesRoot,
+ releaseDir,
+ currentPath,
+ configPath: join(supportRoot, 'config.json'),
+ agentStatePath: join(supportRoot, 'agent-state.json'),
+ plistPath: join(userHome, 'Library/LaunchAgents/io.github.awesome-codex-themes.agent.plist'),
+ stdoutPath: join(userHome, 'Library/Logs/AwesomeCodexThemes.log'),
+ stderrPath: join(userHome, 'Library/Logs/AwesomeCodexThemes.error.log'),
+ };
+}
+
+async function exists(path) {
+ try {
+ await access(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function validateRunnableThemes(sourceRoot, selectedTheme) {
+ const themesRoot = join(sourceRoot, 'themes');
+ const entries = await readdir(themesRoot, { withFileTypes: true });
+ const slugs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
+ if (!slugs.includes(selectedTheme)) fail('INSTALL_THEME_NOT_FOUND', `Theme ${selectedTheme} is not a runnable package.`);
+ await Promise.all(slugs.map((slug) => loadThemePackage(join(themesRoot, slug))));
+ return slugs;
+}
+
+export async function installPersistentAgent(
+ { sourceRoot, home, version, themeSlug, enabled = true },
+ {
+ copy = (source, destination) => cp(source, destination, { recursive: true, force: true }),
+ bootout = bootoutLaunchAgent,
+ bootstrap = bootstrapLaunchAgent,
+ kickstart = kickstartLaunchAgent,
+ writeConfig = writeAgentConfig,
+ writePlist = writeLaunchAgentPlist,
+ } = {},
+) {
+ const source = requireAbsolute(sourceRoot, 'sourceRoot');
+ const paths = installationPaths({ home, version });
+ await validateRunnableThemes(source, themeSlug);
+ await mkdir(paths.releasesRoot, { recursive: true, mode: 0o700 });
+
+ if (!(await exists(paths.releaseDir))) {
+ const staging = `${paths.releaseDir}.staging-${process.pid}-${Date.now()}`;
+ await mkdir(staging, { recursive: true, mode: 0o700 });
+ try {
+ await copy(join(source, 'package.json'), join(staging, 'package.json'));
+ await copy(join(source, 'bin'), join(staging, 'bin'));
+ await copy(join(source, 'src/cli'), join(staging, 'src/cli'));
+ await copy(join(source, 'src/engine'), join(staging, 'src/engine'));
+ await copy(join(source, 'themes'), join(staging, 'themes'));
+ await chmod(join(staging, 'bin/awesome-codex-themes'), 0o755);
+ await rename(staging, paths.releaseDir);
+ } catch (error) {
+ await rm(staging, { recursive: true, force: true }).catch(() => {});
+ fail('INSTALL_COPY_FAILED', 'Unable to stage the self-contained runtime.', { cause: error });
+ }
+ }
+
+ await writeConfig(paths.configPath, {
+ schemaVersion: 1,
+ enabled,
+ themeSlug,
+ launchAtLogin: true,
+ });
+
+ const plist = buildLaunchAgentPlist({
+ launcherPath: join(paths.currentPath, 'bin/awesome-codex-themes'),
+ stdoutPath: paths.stdoutPath,
+ stderrPath: paths.stderrPath,
+ });
+ await writePlist(paths.plistPath, plist);
+
+ const temporaryLink = `${paths.currentPath}.next-${process.pid}-${Date.now()}`;
+ try {
+ await symlink(paths.releaseDir, temporaryLink, 'dir');
+ await rename(temporaryLink, paths.currentPath);
+ } catch (error) {
+ await rm(temporaryLink, { force: true }).catch(() => {});
+ fail('INSTALL_ACTIVATE_FAILED', 'Unable to activate the installed runtime.', { cause: error });
+ }
+
+ await bootout({ ignoreMissing: true });
+ await bootstrap(paths.plistPath);
+ await kickstart();
+ return { installed: true, theme: themeSlug, version, supportRoot: paths.supportRoot, plistPath: paths.plistPath };
+}
+
+export async function uninstallPersistentAgent(
+ { home },
+ { bootout = bootoutLaunchAgent, remove = rm } = {},
+) {
+ const paths = installationPaths({ home, version: '0.0.0' });
+ await bootout({ ignoreMissing: true });
+ await remove(paths.plistPath, { force: true });
+ await remove(paths.supportRoot, { recursive: true, force: true });
+ return { uninstalled: true };
+}
diff --git a/src/engine/launch-agent.mjs b/src/engine/launch-agent.mjs
new file mode 100644
index 0000000..fb26404
--- /dev/null
+++ b/src/engine/launch-agent.mjs
@@ -0,0 +1,167 @@
+import { execFile } from 'node:child_process';
+import { chmod, mkdir, rename, rm, writeFile } from 'node:fs/promises';
+import { dirname, isAbsolute } from 'node:path';
+import { promisify } from 'node:util';
+
+export const LAUNCH_AGENT_LABEL = 'io.github.awesome-codex-themes.agent';
+
+const execFileAsync = promisify(execFile);
+
+export class LaunchAgentError extends Error {
+ constructor(code, message, options = {}) {
+ super(message, options);
+ this.name = 'LaunchAgentError';
+ this.code = code;
+ }
+}
+
+function fail(code, message, options) {
+ throw new LaunchAgentError(code, message, options);
+}
+
+function requireAbsolutePath(value, field) {
+ if (typeof value !== 'string' || !isAbsolute(value)) {
+ fail('LAUNCH_AGENT_PATH_INVALID', `${field} must be an absolute path.`);
+ }
+ return value;
+}
+
+function xml(value) {
+ return value
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+export function buildLaunchAgentPlist({ launcherPath, stdoutPath, stderrPath }) {
+ const launcher = xml(requireAbsolutePath(launcherPath, 'launcherPath'));
+ const stdout = xml(requireAbsolutePath(stdoutPath, 'stdoutPath'));
+ const stderr = xml(requireAbsolutePath(stderrPath, 'stderrPath'));
+ return `
+
+
+
+ Label
+ ${LAUNCH_AGENT_LABEL}
+ ProgramArguments
+
+ ${launcher}
+ _agent
+
+ RunAtLoad
+
+ KeepAlive
+
+ ProcessType
+ Background
+ ThrottleInterval
+ 10
+ StandardOutPath
+ ${stdout}
+ StandardErrorPath
+ ${stderr}
+
+
+`;
+}
+
+export async function writeLaunchAgentPlist(path, plist) {
+ requireAbsolutePath(path, 'plist path');
+ if (typeof plist !== 'string' || !plist.includes(`${LAUNCH_AGENT_LABEL}`)) {
+ fail('LAUNCH_AGENT_PLIST_INVALID', 'LaunchAgent plist content is invalid.');
+ }
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
+ const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
+ try {
+ await writeFile(temporaryPath, plist, { mode: 0o644 });
+ await chmod(temporaryPath, 0o644);
+ await rename(temporaryPath, path);
+ await chmod(path, 0o644);
+ } catch (error) {
+ await rm(temporaryPath, { force: true }).catch(() => {});
+ fail('LAUNCH_AGENT_WRITE_FAILED', 'Unable to write the LaunchAgent plist atomically.', { cause: error });
+ }
+}
+
+function domain(uid) {
+ if (!Number.isInteger(uid) || uid < 1) fail('LAUNCH_AGENT_UID_INVALID', 'A positive user ID is required.');
+ return `gui/${uid}`;
+}
+
+async function defaultRun(executable, args) {
+ return execFileAsync(executable, args, { encoding: 'utf8' });
+}
+
+function isTransientBootstrap(error) {
+ const detail = `${error?.message ?? ''}\n${error?.stderr ?? ''}`.toLowerCase();
+ return error?.code === 5 && detail.includes('input/output error');
+}
+
+export async function bootstrapLaunchAgent(
+ plistPath,
+ {
+ uid = process.getuid?.(),
+ run = defaultRun,
+ delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
+ attempts = 20,
+ } = {},
+) {
+ const args = ['bootstrap', domain(uid), requireAbsolutePath(plistPath, 'plistPath')];
+ let lastError;
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ try {
+ await run('/bin/launchctl', args);
+ return;
+ } catch (error) {
+ lastError = error;
+ if (!isTransientBootstrap(error) || attempt === attempts) break;
+ await delay(250);
+ }
+ }
+ fail('LAUNCH_AGENT_BOOTSTRAP_FAILED', 'Unable to bootstrap the Awesome Codex Themes LaunchAgent.', {
+ cause: lastError,
+ });
+}
+
+export async function kickstartLaunchAgent({ uid = process.getuid?.(), run = defaultRun } = {}) {
+ const target = `${domain(uid)}/${LAUNCH_AGENT_LABEL}`;
+ try {
+ await run('/bin/launchctl', ['kickstart', '-k', target]);
+ } catch (error) {
+ fail('LAUNCH_AGENT_KICKSTART_FAILED', 'Unable to kickstart the Awesome Codex Themes LaunchAgent.', { cause: error });
+ }
+}
+
+function isMissingService(error) {
+ const detail = `${error?.message ?? ''}\n${error?.stderr ?? ''}`.toLowerCase();
+ return (error?.code === 3 || error?.code === 113) && (
+ detail.includes('no such process') ||
+ detail.includes('could not find specified service') ||
+ detail.includes('could not find service')
+ );
+}
+
+export async function isLaunchAgentLoaded({ uid = process.getuid?.(), run = defaultRun } = {}) {
+ const target = `${domain(uid)}/${LAUNCH_AGENT_LABEL}`;
+ try {
+ await run('/bin/launchctl', ['print', target]);
+ return true;
+ } catch (error) {
+ if (isMissingService(error)) return false;
+ fail('LAUNCH_AGENT_STATUS_FAILED', 'Unable to inspect the Awesome Codex Themes LaunchAgent.', { cause: error });
+ }
+}
+
+export async function bootoutLaunchAgent(
+ { uid = process.getuid?.(), run = defaultRun, ignoreMissing = false } = {},
+) {
+ const target = `${domain(uid)}/${LAUNCH_AGENT_LABEL}`;
+ try {
+ await run('/bin/launchctl', ['bootout', target]);
+ } catch (error) {
+ if (ignoreMissing && isMissingService(error)) return;
+ fail('LAUNCH_AGENT_BOOTOUT_FAILED', 'Unable to boot out the Awesome Codex Themes LaunchAgent.', { cause: error });
+ }
+}
diff --git a/src/engine/session.mjs b/src/engine/session.mjs
index fde5eec..ec26866 100644
--- a/src/engine/session.mjs
+++ b/src/engine/session.mjs
@@ -86,7 +86,7 @@ export async function startThemeSession(
if (!Number.isInteger(appPid) || appPid < 1) sessionFail('APP_LAUNCH_FAILED', 'The official app did not return a valid PID.');
const appStartedAt = await processStartedAt(appPid);
- await waitForRenderer(port);
+ await waitForRenderer(port, app);
let watcher;
try {
await applyTheme({ themeSlug, port, app });
diff --git a/src/engine/theme.mjs b/src/engine/theme.mjs
index bc3a9b3..c5e2cfb 100644
--- a/src/engine/theme.mjs
+++ b/src/engine/theme.mjs
@@ -40,14 +40,19 @@ export const SEMANTIC_COLOR_ROLES = Object.freeze([
]);
export const DEFAULT_MAX_ASSET_BYTES = 10 * 1024 * 1024;
export const DEFAULT_MAX_RUNTIME_ARTWORK_BYTES = 700 * 1024;
+export const DEFAULT_MAX_RUNTIME_ASSET_BYTES = 256 * 1024;
export const DEFAULT_MAX_CSS_BYTES = 256 * 1024;
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+const AUXILIARY_ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+const MAX_AUXILIARY_ASSETS = 16;
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
const APP_VERSION_RANGE_PATTERN = /^\d+(?:\.\d+)+(?:\.\*)?$/;
+const NATIVE_LOCALE_PATTERN = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?(?:-[A-Za-z0-9]{4,8})*$/;
const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?$/;
const SUPPORTED_PLATFORMS = new Set(['macos']);
const SUPPORTED_STATUS = new Set(['experimental', 'verified']);
+const SUPPORTED_COMPATIBILITY_STRATEGIES = new Set(['best-effort-all']);
const SUPPORTED_MODES = new Set(['dark', 'light', 'system']);
const EXPERIENCE_FIELD_LIMITS = Object.freeze({
brand: 48,
@@ -106,6 +111,25 @@ function validateLocalPath(value, field) {
return path;
}
+function normalizeAuxiliaryAssets(value) {
+ if (value === undefined) return {};
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
+ fail('THEME_FIELD_INVALID', 'files.assets must be an object of named local image paths.');
+ }
+ const entries = Object.entries(value);
+ if (entries.length > MAX_AUXILIARY_ASSETS) {
+ fail('THEME_ASSET_COUNT_EXCEEDED', `files.assets supports at most ${MAX_AUXILIARY_ASSETS} entries.`);
+ }
+ return Object.fromEntries(
+ entries.map(([name, path]) => {
+ if (!AUXILIARY_ASSET_NAME_PATTERN.test(name)) {
+ fail('THEME_ASSET_NAME_INVALID', 'files.assets keys must use lowercase kebab-case.');
+ }
+ return [name, validateLocalPath(path, `files.assets.${name}`)];
+ }),
+ );
+}
+
function validateColor(value, field) {
if (typeof value !== 'string' || !HEX_COLOR_PATTERN.test(value)) {
fail('THEME_COLOR_INVALID', `${field} must be a six- or eight-digit hex color.`);
@@ -187,6 +211,22 @@ function normalizeExperience(value, schemaVersion) {
};
}
+function normalizeNativeDisplayName(input) {
+ const hasNativeName = input.nativeName !== undefined;
+ const hasNativeLocale = input.nativeLocale !== undefined;
+ if (hasNativeName !== hasNativeLocale) {
+ fail('THEME_NATIVE_NAME_INCOMPLETE', 'nativeName and nativeLocale must be provided together.');
+ }
+ if (!hasNativeName) return undefined;
+
+ const nativeName = requireString(input.nativeName, 'nativeName');
+ const nativeLocale = requireString(input.nativeLocale, 'nativeLocale');
+ if (!NATIVE_LOCALE_PATTERN.test(nativeLocale)) {
+ fail('THEME_NATIVE_LOCALE_INVALID', 'nativeLocale must use a canonical BCP 47 language tag such as ja-JP.');
+ }
+ return { nativeName, nativeLocale };
+}
+
function validateManifestObject(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
fail('THEME_JSON_INVALID', 'theme.json must contain an object.');
@@ -246,17 +286,29 @@ function validateManifestObject(input) {
fail('THEME_MODE_INVALID', 'mode must be dark, light, or system.');
}
- const appVersions = requireStringArray(compatibility.appVersions, 'compatibility.appVersions');
- if (appVersions.some((range) => !APP_VERSION_RANGE_PATTERN.test(range))) {
- fail('THEME_COMPATIBILITY_INVALID', 'compatibility.appVersions must contain exact versions or a trailing wildcard.');
+ const strategy = compatibility.strategy ?? 'best-effort-all';
+ if (!SUPPORTED_COMPATIBILITY_STRATEGIES.has(strategy)) {
+ fail('THEME_COMPATIBILITY_INVALID', 'compatibility.strategy must be best-effort-all.');
+ }
+ const verifiedAppVersions = requireStringArray(
+ compatibility.verifiedAppVersions ?? compatibility.appVersions,
+ 'compatibility.verifiedAppVersions',
+ );
+ if (verifiedAppVersions.some((range) => !APP_VERSION_RANGE_PATTERN.test(range))) {
+ fail(
+ 'THEME_COMPATIBILITY_INVALID',
+ 'compatibility.verifiedAppVersions must contain exact versions or a trailing wildcard.',
+ );
}
const experience = normalizeExperience(input.experience, input.schemaVersion);
+ const nativeDisplayName = normalizeNativeDisplayName(input);
return {
schemaVersion: input.schemaVersion,
slug,
version,
name: requireString(input.name, 'name'),
+ ...(nativeDisplayName ?? {}),
description: requireString(input.description, 'description'),
author: {
name: requireString(author.name, 'author.name'),
@@ -271,7 +323,8 @@ function validateManifestObject(input) {
compatibility: {
platforms,
status: compatibility.status,
- appVersions,
+ strategy,
+ verifiedAppVersions,
},
mode,
palette: normalizePalette(palette, input.schemaVersion),
@@ -280,6 +333,10 @@ function validateManifestObject(input) {
css: validateLocalPath(files.css, 'files.css'),
artwork: validateLocalPath(files.artwork, 'files.artwork'),
preview: validateLocalPath(files.preview, 'files.preview'),
+ ...(() => {
+ const assets = normalizeAuxiliaryAssets(files.assets);
+ return Object.keys(assets).length > 0 ? { assets } : {};
+ })(),
},
};
}
@@ -306,13 +363,16 @@ export function assertThemeCompatibility(input, { platform, appVersion } = {}) {
if (typeof appVersion !== 'string' || !/^\d+(?:\.\d+)+$/.test(appVersion)) {
fail('THEME_APP_VERSION_INVALID', 'A numeric Codex app version is required.');
}
- if (!manifest.compatibility.appVersions.some((range) => versionMatchesRange(appVersion, range))) {
- fail(
- 'THEME_APP_VERSION_UNSUPPORTED',
- `Theme ${manifest.slug} does not declare support for Codex ${appVersion}; official UI was left unchanged.`,
- );
- }
- return { platform, appVersion, status: manifest.compatibility.status };
+ const verified = manifest.compatibility.verifiedAppVersions.some((range) =>
+ versionMatchesRange(appVersion, range),
+ );
+ return {
+ platform,
+ appVersion,
+ status: manifest.compatibility.status,
+ strategy: manifest.compatibility.strategy,
+ verified,
+ };
}
async function resolveThemeFile(root, relativePath) {
@@ -357,7 +417,7 @@ function validateCss(css) {
if (css.includes('\\')) {
fail('THEME_CSS_UNSAFE_ESCAPE', 'Theme CSS cannot contain escape sequences that obscure executable or remote tokens.');
}
- if (/\@import\s/i.test(css) || /https?:|\/\//i.test(css)) {
+ if (/\@import\s/i.test(css) || /https?:|(?:url|image-set)\s*\([^)]*\/\//i.test(css)) {
fail('THEME_CSS_REMOTE_IMPORT', 'Theme CSS cannot load remote resources.');
}
if (/url\s*\(/i.test(css) || /javascript\s*:|expression\s*\(/i.test(css)) {
@@ -394,6 +454,7 @@ export async function loadThemePackage(
{
maxAssetBytes = DEFAULT_MAX_ASSET_BYTES,
maxRuntimeArtworkBytes = DEFAULT_MAX_RUNTIME_ARTWORK_BYTES,
+ maxRuntimeAssetBytes = DEFAULT_MAX_RUNTIME_ASSET_BYTES,
maxCssBytes = DEFAULT_MAX_CSS_BYTES,
} = {},
) {
@@ -415,10 +476,19 @@ export async function loadThemePackage(
const cssPath = await resolveThemeFile(canonicalRoot, manifest.files.css);
const artworkPath = await resolveThemeFile(canonicalRoot, manifest.files.artwork);
const previewPath = await resolveThemeFile(canonicalRoot, manifest.files.preview);
- const [cssInfo, artworkInfo, previewInfo] = await Promise.all([
+ const auxiliaryAssetEntries = await Promise.all(
+ Object.entries(manifest.files.assets ?? {}).map(async ([name, relativePath]) => [
+ name,
+ await resolveThemeFile(canonicalRoot, relativePath),
+ ]),
+ );
+ const [cssInfo, artworkInfo, previewInfo, auxiliaryAssetInfos] = await Promise.all([
stat(cssPath),
stat(artworkPath),
stat(previewPath),
+ Promise.all(
+ auxiliaryAssetEntries.map(async ([name, path]) => [name, path, await stat(path)]),
+ ),
]);
if (cssInfo.size > maxCssBytes) {
@@ -433,17 +503,46 @@ export async function loadThemePackage(
`Runtime artwork exceeds ${maxRuntimeArtworkBytes} bytes and cannot be injected reliably.`,
);
}
+ for (const [name, , info] of auxiliaryAssetInfos) {
+ if (info.size > maxAssetBytes) {
+ fail('THEME_ASSET_TOO_LARGE', `Theme asset ${name} exceeds ${maxAssetBytes} bytes.`);
+ }
+ if (info.size > maxRuntimeAssetBytes) {
+ fail(
+ 'THEME_RUNTIME_ASSET_TOO_LARGE',
+ `Runtime asset ${name} exceeds ${maxRuntimeAssetBytes} bytes and cannot be injected reliably.`,
+ );
+ }
+ }
- const [css, artworkBuffer, previewBuffer] = await Promise.all([
+ const [css, artworkBuffer, previewBuffer, auxiliaryAssetBuffers] = await Promise.all([
readFile(cssPath, 'utf8'),
readFile(artworkPath),
readFile(previewPath),
+ Promise.all(
+ auxiliaryAssetEntries.map(async ([name, path]) => [name, path, await readFile(path)]),
+ ),
]);
validateCss(css);
const mime = mimeForArtwork(artworkPath);
mimeForArtwork(previewPath);
validateSvg(artworkBuffer, artworkPath);
validateSvg(previewBuffer, previewPath);
+ const assets = Object.fromEntries(
+ auxiliaryAssetBuffers.map(([name, path, buffer]) => {
+ const assetMime = mimeForArtwork(path);
+ validateSvg(buffer, path);
+ return [
+ name,
+ {
+ path,
+ mime: assetMime,
+ bytes: buffer.byteLength,
+ dataUrl: `data:${assetMime};base64,${buffer.toString('base64')}`,
+ },
+ ];
+ }),
+ );
return {
root: canonicalRoot,
@@ -455,6 +554,7 @@ export async function loadThemePackage(
bytes: artworkBuffer.byteLength,
dataUrl: `data:${mime};base64,${artworkBuffer.toString('base64')}`,
},
+ assets,
previewPath,
};
}
diff --git a/src/gallery/App.tsx b/src/gallery/App.tsx
index 33ec816..0ef2e9c 100644
--- a/src/gallery/App.tsx
+++ b/src/gallery/App.tsx
@@ -2,7 +2,10 @@ import { useMemo, useState } from 'react';
import catalog from '../generated/themes.json';
-type Theme = (typeof catalog)[number];
+type Theme = (typeof catalog)[number] & {
+ nativeName?: string;
+ nativeLocale?: string;
+};
const themes = [...(catalog as Theme[])].sort((left, right) => {
const featuredDifference = Number(right.tags.includes('featured')) - Number(left.tags.includes('featured'));
@@ -35,6 +38,7 @@ function ThemeCard({ theme, onOpen }: { theme: Theme; onOpen: (theme: Theme) =>
{theme.compatibility.status}
Private fan prototype artwork. Replace the character asset and naming before any public or commercial release.
- ) : null}
+
Experimental Mac-first runtime theme. Run doctor first. If the interface looks wrong, run pause or restore to return to the official UI, then file a GitHub Issue with the Codex version and a privacy-safe screenshot.
A flagship full-interface theme, a versioned full-workspace adapter, local assets, diagnostics, and a restore path you can inspect. Atmosphere without surrendering control.
+
Make Codex feel like yours.
+
Twelve complete full-workspace themes, one persistent local agent, and an inspectable adapter with diagnostics, switching, and recovery built in.
Browse the collectionInspect the source ↗
@@ -150,15 +156,15 @@ export function App() {
>
- LIMITLESS / SIX EYES
- Full-interface prototype · open specimen ↗
+ {flagship.name.toUpperCase()}
+ Featured theme · open specimen ↗
- 05
+ 12
- 1 flagship theme
- 4 original studies
+ 12 complete themes
+ 1 featured flagshipMIT engine · local packages
@@ -181,7 +187,7 @@ export function App() {
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
- placeholder="Try “aurora” or “editorial”"
+ placeholder="Try “night city” or “ukiyoe”"
aria-label="Search themes"
/>
⌕
@@ -204,7 +210,7 @@ export function App() {
∅
No matching themes.
-
The atlas is small on purpose. Try another word or return to the full collection.
+
Try another word or return to the complete twelve-theme collection.
Clear search and filters
)}
@@ -216,9 +222,9 @@ export function App() {
Decoration should never outrank recovery.
-
01
Local by construction
Theme packages use local CSS, metadata, and original artwork. No remote JavaScript and no conversation access.
-
02
Fail closed
The engine checks the signed app, loopback endpoint, renderer target, and supported versions before applying.
-
03
One owner, clean exit
Injection is namespaced and idempotent. Restore removes only the state owned by Awesome Codex Themes.
+
01
Local by construction
Theme packages use local CSS, metadata, and documented artwork. No remote JavaScript and no conversation access.
+
02
Verify the boundary
The engine checks the signed app, loopback endpoint, and renderer target before attempting a verified or best-effort adapter.
+
03
One owner, clean exit
Injection is namespaced and idempotent. Pause and uninstall remove only the state owned by Awesome Codex Themes.