@orbital_mcp/patchrelay 0.1.0-alpha04 → 0.1.0-alpha05

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ package dev.patchrelay.compose
2
+
3
+ import androidx.compose.runtime.*
4
+ import androidx.compose.runtime.snapshots.Snapshot
5
+ import kotlinx.serialization.json.*
6
+ import kotlin.coroutines.EmptyCoroutineContext
7
+
8
+ private class RelayNode(val type: String) {
9
+ var id = ""
10
+ var text = ""
11
+ var style = "body"
12
+ var enabled = true
13
+ var value = ""
14
+ var checked = false
15
+ var event: ((String?) -> Unit)? = null
16
+ val children = mutableListOf<RelayNode>()
17
+ }
18
+
19
+ private class RelayApplier(root: RelayNode) : AbstractApplier<RelayNode>(root) {
20
+ override fun insertTopDown(index: Int, instance: RelayNode) { current.children.add(index, instance) }
21
+ override fun insertBottomUp(index: Int, instance: RelayNode) {}
22
+ override fun remove(index: Int, count: Int) { current.children.remove(index, count) }
23
+ override fun move(from: Int, to: Int, count: Int) { current.children.move(from, to, count) }
24
+ override fun onClear() { root.children.clear() }
25
+ }
26
+
27
+ /** Explicit render/event transactions: no frame loop, browser DOM or asynchronous effects. */
28
+ @OptIn(ExperimentalComposeApi::class)
29
+ class RelayComposition(private val screenId: String, private val backAction: () -> String? = { null },
30
+ private val content: @Composable () -> Unit) {
31
+ private val root = RelayNode("column")
32
+ private val recomposer = Recomposer(EmptyCoroutineContext)
33
+ private val composition = ControlledComposition(RelayApplier(root), recomposer)
34
+ private val observer = Snapshot.registerApplyObserver { values, _ ->
35
+ if (!composition.isDisposed) composition.recordModificationsOf(values)
36
+ }
37
+ private var started = false
38
+ private var active = false
39
+ private var actions = emptyMap<String, RelayNode>()
40
+
41
+ fun render(): String = transaction { renderTree() }
42
+
43
+ fun event(action: String, value: String? = null): String = transaction {
44
+ require(value == null || value.length <= 1024) { "Compose event value too large" }
45
+ val node = actions[action] ?: error("Unknown Compose action: $action")
46
+ require(node.enabled) { "Disabled Compose action" }
47
+ node.event!!(value)
48
+ renderTree()
49
+ }
50
+
51
+ private inline fun transaction(body: () -> String): String {
52
+ check(!composition.isDisposed && !active) { "Compose screen disposed or reentrant" }
53
+ active = true
54
+ try { return body() }
55
+ catch (failure: Throwable) { dispose(); throw failure }
56
+ finally { active = false }
57
+ }
58
+
59
+ private fun renderTree(): String {
60
+ if (!started) { started = true; composition.setContent(content) }
61
+ Snapshot.sendApplyNotifications()
62
+ var passes = 0
63
+ while (composition.hasInvalidations) {
64
+ check(++passes <= 16) { "Compose did not settle within 16 passes" }
65
+ val snapshot = Snapshot.takeMutableSnapshot({ composition.recordReadOf(it) }, { composition.recordWriteOf(it) })
66
+ val changed = try { snapshot.enter { composition.recompose() } }
67
+ finally { try { snapshot.apply().check() } finally { snapshot.dispose() } }
68
+ if (changed) { composition.applyChanges(); composition.applyLateChanges() }
69
+ composition.changesApplied()
70
+ Snapshot.notifyObjectsInitialized()
71
+ Snapshot.sendApplyNotifications()
72
+ }
73
+ val ids = mutableSetOf<String>()
74
+ val nextActions = mutableMapOf<String, RelayNode>()
75
+ fun children(node: RelayNode, depth: Int): JsonArray {
76
+ require(node.children.size in 1..32) { "Compose layout requires 1..32 children" }
77
+ return JsonArray(node.children.map { child ->
78
+ require(depth <= 8 && ids.size < 128) { "Compose screen complexity exceeded" }
79
+ require(child.id.matches(Regex("[a-z][a-z0-9_]{0,23}")) && ids.add(child.id)) { "Invalid or duplicate Compose widget ID: ${child.id}" }
80
+ buildJsonObject {
81
+ put("id", child.id); put("type", child.type)
82
+ if (child.type in setOf("column", "row", "card")) put("children", children(child, depth + 1))
83
+ else {
84
+ require(child.text.isNotBlank() && child.text.length <= 1024) { "Invalid Compose widget text" }
85
+ put("text", child.text)
86
+ if (child.type == "text") {
87
+ require(child.style in setOf("title", "body", "caption", "error"))
88
+ put("style", child.style)
89
+ } else {
90
+ val action = "c_" + child.id
91
+ nextActions[action] = child
92
+ put("action", action); put("enabled", child.enabled)
93
+ if (child.type == "input") { require(child.value.length <= 1024); put("value", child.value) }
94
+ if (child.type == "toggle") put("checked", child.checked)
95
+ }
96
+ }
97
+ }
98
+ })
99
+ }
100
+ val result = buildJsonObject {
101
+ put("screenId", screenId); put("type", "column"); put("children", children(root, 1))
102
+ backAction()?.let { require(it.matches(Regex("[a-z][a-z0-9_-]{0,31}"))); put("backAction", it) }
103
+ }.toString()
104
+ require(result.length <= 65536) { "Compose screen too large" }
105
+ actions = nextActions
106
+ return result
107
+ }
108
+
109
+ fun dispose() {
110
+ observer.dispose()
111
+ composition.dispose()
112
+ recomposer.cancel()
113
+ actions = emptyMap()
114
+ }
115
+ }
116
+
117
+ @Composable
118
+ private fun Node(type: String, id: String, text: String = "", style: String = "body", enabled: Boolean = true,
119
+ value: String = "", checked: Boolean = false, event: ((String?) -> Unit)? = null,
120
+ content: @Composable () -> Unit = {}) {
121
+ ComposeNode<RelayNode, RelayApplier>(factory = { RelayNode(type) }, update = {
122
+ set(id) { this.id = it }; set(text) { this.text = it }; set(style) { this.style = it }
123
+ set(enabled) { this.enabled = it }; set(value) { this.value = it }; set(checked) { this.checked = it }
124
+ set(event) { this.event = it }
125
+ }, content = content)
126
+ }
127
+
128
+ @Composable fun Text(id: String, text: String, style: String = "body") = Node("text", id, text, style)
129
+ @Composable fun Button(id: String, text: String, enabled: Boolean = true, onClick: () -> Unit) =
130
+ Node("button", id, text, enabled = enabled, event = { onClick() })
131
+ @Composable fun TextField(id: String, label: String, value: String, enabled: Boolean = true, onValueChange: (String) -> Unit) =
132
+ Node("input", id, label, enabled = enabled, value = value, event = { onValueChange(it ?: error("Missing input value")) })
133
+ @Composable fun Toggle(id: String, label: String, checked: Boolean, enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit) =
134
+ Node("toggle", id, label, enabled = enabled, checked = checked, event = {
135
+ require(it == "true" || it == "false") { "Invalid toggle value" }; onCheckedChange(it == "true")
136
+ })
137
+ @Composable fun Column(id: String, content: @Composable () -> Unit) = Node("column", id, content = content)
138
+ @Composable fun Row(id: String, content: @Composable () -> Unit) = Node("row", id, content = content)
139
+ @Composable fun Card(id: String, content: @Composable () -> Unit) = Node("card", id, content = content)
package/README.md CHANGED
@@ -1,174 +1,249 @@
1
- # PatchRelay CLI
2
-
3
- An installable `patchrelay` command. No Python installation or Python commands.
4
- Uses Node 22+ and the project's Gradle wrapper with JDK 17 and Android SDK 35.
5
-
6
- Experimental alpha package: `@orbital_mcp/patchrelay`. The Android SDK is a
7
- separate Maven dependency; installing it does not install this CLI.
8
-
9
- ## Install a built package
10
-
11
- ```sh
12
- npm install --global /path/to/orbital_mcp-patchrelay-0.1.0-alpha04.tgz
13
- patchrelay --help
14
- ```
15
-
16
- Public alpha installation:
17
-
18
- ```sh
19
- npm install --global @orbital_mcp/patchrelay@alpha
20
- ```
21
-
22
- Package page: https://www.npmjs.com/package/@orbital_mcp/patchrelay.
23
- The `alpha` tag identifies an experimental release rather than a stable one.
24
-
25
- Publisher setup requires `npm login`, verification that the account controls the
26
- `@orbital_mcp` scope, and `npm publish --access public --tag alpha` from the CLI
27
- package. Never paste npm credentials into this repository or an app. Ownership
28
- and authentication must be established before advertising a working public link.
29
-
30
- ## Use
31
-
32
- Run inside your prepared Android project:
33
-
34
- ```sh
35
- patchrelay init
36
- patchrelay baseline
37
- # Edit patchable screen Kotlin.
38
- patchrelay patch
39
- patchrelay catalog
40
- ```
41
-
42
- `init` asks for module names, GitHub hosting repository (or HTTPS hosting) and
43
- signing-key location, stable signed-catalog HTTPS URL and channel. It writes
44
- `patchrelay.config.json` without key contents and appends catalog URL/channel
45
- settings to `local.properties`, preserving existing SDK path settings.
46
- It also copies the Gradle build plugin and prints the one-time settings/app plugin
47
- hooks. Apply those hooks so normal `assembleRelease` and `bundleRelease` builds
48
- automatically preserve matching baseline records; no extra baseline command is
49
- then required. This repository's demo already has the hooks. The plugin remains
50
- limited to prepared screen modules, not arbitrary Kotlin screen rewriting.
51
- It adds `.patchrelay/` and `.keys/` to Git ignore rules without removing existing
52
- rules. Custom signing-key locations still need their own ignore protection.
53
- It does not generate/rotate keys, install SDK hooks, migrate ordinary native
54
- screens or configure private-host authentication. Review generated JS/APK/
55
- BuildConfig paths before building. A custom Kotlin/JS export name or Gradle
56
- variant needs corresponding settings changes.
57
-
58
- `baseline` asks for a unique record name, builds/tests the configured screen
59
- module, bundles its program, builds the APK, checks the native fingerprint and
60
- saves APK, program, compatibility JSON and private source ZIP under
61
- `.patchrelay/releases/baselines/NAME`. It changes the configured bundled JS asset
62
- as part of preparing that native build. Back up the entire baseline directory
63
- privately. Local output is not cloud backup. Do not publish the source ZIP.
64
-
65
- `patch` lists available baseline paths and asks you to choose one explicitly,
66
- then asks for a positive patch version. It never selects a record automatically.
67
- It rejects native/contract changes and altered source archives, compiles/tests
68
- the full current supported screen program and signs `screens.js` plus `patch.json`.
69
- Output directories are exclusive; prior releases are never overwritten.
70
-
71
- GitHub artifact URLs use the configured repository and the convention
72
- `patch-ROUTE_HASH-VERSION`; the route hash binds app ID, base version and exact
73
- native fingerprint, avoiding collisions between apps/builds. `--tag` overrides
74
- that release tag. For another HTTPS host,
75
- the command asks for the actual final `screens.js` URL. Nothing is uploaded and
76
- no hosted catalog is changed by these commands. The app's catalog endpoint is
77
- configured separately in its native build; it is not the JS artifact URL.
78
-
79
- Patch outputs are under `.patchrelay/releases/patches/ROUTE_HASH/VERSION`.
80
- A locked, local version record under `.patchrelay/versions` prevents reusing or
81
- lowering a version for the same exact base even with a different output folder.
82
- A failed write after reservation consumes that version; choose a higher one.
83
- Back up these records. They cannot detect versions published from another
84
- machine unless you retain the latest published records/catalog. All channels
85
- for an exact base share the version sequence.
86
-
87
- ## Stable update discovery
88
-
89
- `patchrelay catalog` asks for a signed patch manifest, the existing signed catalog
90
- (or explicit `new` for the first one), channel and final manifest HTTPS URL.
91
- It verifies signatures/artifact hashes, preserves routes for older bases and
92
- updates only that patch's exact app/base route. It rejects a route downgrade,
93
- wrong app/channel, ambiguous routes and more than 64 supported base builds.
94
- It writes a new signed `catalog.json` locally, never overwriting hosted data.
95
-
96
- Use one stable catalog URL per app/channel. Different apps can share one GitHub
97
- repository while using separate catalogs, signing keys and immutable patch
98
- releases. Upload/verify `screens.js` and its `patch.json` first, then replace
99
- the hosted catalog. Keep every artifact still referenced by an older base.
100
- GitHub asset replacement is not atomic; a brief failed check must leave the
101
- current app working. Private-source hosting still needs accessible HTTPS patch
102
- delivery or a separate authenticated gateway; GitHub credentials aren't embedded.
103
-
104
- The prepared host calls `updates.stageCatalog(catalogUrl, channel)` off the UI
105
- thread. The SDK verifies the catalog, selects the exact installed fingerprint,
106
- then verifies the selected manifest and downloads only the latest complete
107
- program. No match means no update. Existing version/signature checks still apply.
108
- This supports skipping intermediate patches, not arbitrary data migrations.
109
- The new demo checks once per process after healthy first rendering, silently;
110
- the downloaded patch activates on a later process start, not mid-session.
111
-
112
- Automation can supply explicit values:
113
-
114
- ```sh
115
- patchrelay baseline --name android-v1
116
- patchrelay patch --baseline .patchrelay/releases/baselines/android-v1/baseline.json --version 1
117
- ```
118
-
119
- There is no default signing key material and no default publisher repository.
120
- The configured `publicKeyFile` must contain the Base64 DER public key pinned in
121
- the app; signing refuses a mismatched private key. The key file format is the existing PatchRelay Base64 DER PKCS#8 RSA private key
122
- (minimum 3072 bits). Keep it out of Git and releases. The CLI signs locally;
123
- private keys are never sent to Maven, npm or the patch host.
124
-
125
- Normal Gradle baselines are stored at
126
- `.patchrelay/releases/baselines/APP_ID/VARIANT/apk-ARTIFACT_HASH/` or
127
- `aab-ARTIFACT_HASH/`. Select the desired `baseline.json` explicitly in `patch`.
128
- Both formats work for patch compilation; AAB files themselves are not installed
129
- by this tool. `patchrelay baseline` remains useful for a developer-named APK record.
130
-
131
- ## Recover a lost record
132
-
133
- ```sh
134
- patchrelay baseline --from YOUR-COMMIT
135
- ```
136
-
137
- You supply the record name and original APK when asked. Recovery checks out only
138
- that explicitly supplied revision into a detached worktree, using its project
139
- settings when present or the current module-layout settings otherwise. It uses
140
- the selected revision's runtime contract and sources. Current branch/edits remain
141
- untouched. The rebuilt APK must match the original APK hash exactly. Different
142
- signing/toolchain output is rejected; without a matching release, ship a new base.
143
- Recovery worktrees and build diagnostics stay under `.patchrelay/recovery/`.
144
-
145
- ## Project contract
146
-
147
- This CLI is independent of this repository's Python tools, but it is **not an
148
- automatic integration plugin for arbitrary Android apps**. Projects still need:
149
-
150
- - Prepared Kotlin/JS screens and explicit native hooks.
151
- - `runtime.json` format 1 / screen API 2 with app/base/screen identity, declared
152
- native inputs and text-normalization extensions.
153
- - Gradle generation of `HOST_SHA256`, `BASE_VERSION` and `APPLICATION_ID` in the
154
- configured BuildConfig, following the existing sorted SHA-256 contract.
155
- - An Android host using the SDK and configured trust keys/update endpoint.
156
-
157
- The CLI does not scan ordinary Kotlin to discover patchable methods. Multiple
158
- app/variant projects need separately reviewed configuration and release records.
159
- Baseline selection remains manual. Signed catalog authoring/routing is implemented;
160
- automatic remote uploads and arbitrary app integration are still pending.
161
-
162
- ## Build this CLI from source
163
-
164
- ```sh
165
- cd cli
166
- npm test
167
- npm pack
168
- ```
169
-
170
- The npm tarball contains CLI runtime files, the Gradle build-plugin source,
171
- documentation and license: no Android SDK/app implementation, signing keys,
172
- baseline archives or private R8 mappings.
173
- The CLI is JavaScript distributed to developers, so its runtime is inspectable.
174
- Android SDK AAR obfuscation is a separate build step, not protection for CLI code.
1
+ # PatchRelay CLI
2
+
3
+ An installable `patchrelay` command. No Python installation or Python commands.
4
+ Uses Node 22+ and the project's Gradle wrapper with JDK 17 and Android SDK 35.
5
+
6
+ Experimental alpha package: [`@orbital_mcp/patchrelay`](https://www.npmjs.com/package/@orbital_mcp/patchrelay) (CLI `0.1.0-alpha05`). The Android SDK is a
7
+ separate Maven dependency; installing it does not install this CLI. See the
8
+ [public quickstart](https://patchrelay.vercel.app/) and [hosting guide](https://patchrelay.vercel.app/hosting).
9
+
10
+ ## Install in a project
11
+
12
+ Requirements: Node.js 22+, JDK 17+, Android SDK 35, and the Android project's
13
+ Gradle wrapper. From the prepared Android project root:
14
+
15
+ ```sh
16
+ npm install --save-dev @orbital_mcp/patchrelay
17
+ npx patchrelay --help
18
+ npx patchrelay init
19
+ ```
20
+
21
+ `init` creates project config/build tooling and prints the Gradle plugin changes
22
+ to apply. Add the Android SDK separately from Maven Central. Build and install a
23
+ release APK with its stable catalog URL before publishing patches.
24
+
25
+ ## Run current source
26
+
27
+ ```powershell
28
+ node .\cli\bin\patchrelay.mjs --help
29
+ ```
30
+
31
+ Use this checkout's CLI for generated function contracts. Existing alpha installations predate these changes. A local package can be built with `npm pack ./cli`; no publication is needed. Signing keys are not package inputs. See [generated Kotlin integration](../docs/kotlin-workflow.md).
32
+
33
+ ## Use
34
+
35
+ Run inside your prepared Android project:
36
+
37
+ ```sh
38
+ patchrelay init
39
+ patchrelay baseline
40
+ # Edit patchable screen Kotlin.
41
+ patchrelay compare --baseline PATH_TO_BASELINE_JSON
42
+ patchrelay compare --baseline PATH_TO_BASELINE_JSON --diff
43
+ patchrelay patch
44
+ patchrelay catalog
45
+ ```
46
+
47
+ `init` asks for module names, GitHub repository or HTTPS patch-host base URL,
48
+ signing-key location, stable signed-catalog HTTPS URL, channel and an existing
49
+ local catalog (or `new` for the first release). It records generated JS/APK/
50
+ BuildConfig/key paths, the release APK baseline pointer, and a patch output folder
51
+ under `<screens-module>/build/patchrelay`. Kotlin sources are discovered recursively,
52
+ not entered one file at a time. It writes
53
+ `patchrelay.config.json` without key contents and appends catalog URL/channel
54
+ settings to `local.properties`, preserving existing SDK path settings.
55
+ It also copies the Gradle build plugin and prints the one-time settings/app plugin
56
+ hooks. Also apply `dev.patchrelay.functions` to the JVM/JS shared module and add that module as an Android dependency. Apply those hooks so normal `assembleRelease` and `bundleRelease` builds
57
+ automatically preserve matching baseline records; no extra baseline command is
58
+ then required. This repository's demo already has the hooks. The plugin remains
59
+ limited to prepared screen modules, not arbitrary Kotlin screen rewriting.
60
+ It adds `.patchrelay/` and `.keys/` to Git ignore rules without removing existing
61
+ rules. Custom signing-key locations still need their own ignore protection.
62
+ It does not generate/rotate keys, install SDK hooks, migrate ordinary native
63
+ screens or configure private-host authentication. Review generated JS/APK/
64
+ BuildConfig paths before building. A custom Kotlin/JS export name or Gradle
65
+ variant needs corresponding paths at setup (or edits to `patchrelay.config.json`).
66
+
67
+ ### Which files go where?
68
+
69
+ | File | Purpose | Upload to patch hosting? |
70
+ | --- | --- | --- |
71
+ | Kotlin source files | Developer writes screens, functions and navigation | No |
72
+ | `patchrelay.config.json`, `.patchrelay-tools/` | Setup creates project settings/build tools; commit with app source | No |
73
+ | Generated hooks, function contract, compiled build output | Build creates integration and program | No direct upload |
74
+ | Baseline directory (APK, JSON, bundled program, source ZIP) | Immutable private release backup | No |
75
+ | Private signing key | Developer-owned signing credential | Never |
76
+ | `screens.js` in the patch output directory | Complete compiled patch program | Yes, first |
77
+ | `patch.json` beside that program | Signed compatibility manifest | Yes, second |
78
+ | `catalog.json` from the catalog command | Signed discovery catalog | Yes, last, at stable catalog URL |
79
+
80
+ Upload the **patch output** `screens.js`, not a similarly named build file.
81
+ Output names are fixed; choose project input paths during setup and output folders
82
+ with `--out`. `patchrelay catalog` prints the exact three local paths and hosting
83
+ destinations. `patch` does not upload automatically; `patch --github` is the
84
+ GitHub Releases option. Retain older hosted patches while catalog routes
85
+ reference them. Public keys belong in the native app; private
86
+ keys and baseline archives stay in private storage.
87
+
88
+ `baseline` asks for a unique record name, builds/tests the configured screen
89
+ module, bundles its program, builds the APK, checks the native fingerprint and
90
+ saves APK, program, compatibility JSON and private source ZIP under
91
+ `.patchrelay/releases/baselines/NAME`. It changes the configured bundled JS asset
92
+ as part of preparing that native build. Back up the entire baseline directory
93
+ privately. Local output is not cloud backup. Do not publish the source ZIP.
94
+
95
+ `compare --baseline PATH` compares **all files recursively** under configured
96
+ native/screen inputs against that explicitly selected release. It lists every
97
+ `ADDED`, `MODIFIED` and `REMOVED` file, grouped by native/build inputs versus
98
+ supported screen inputs, with counts. There is no one-file or 100-file limit;
99
+ the test suite checks a 150-file change set. Missing declared inputs are reported.
100
+ Comparison follows baseline content hashes, including configured text line-ending
101
+ normalization, rather than the Git working tree or the last patch.
102
+
103
+ Add `--diff` to see actual added/deleted source lines for every changed file,
104
+ using the baseline's verified private source ZIP. It requires Git plus the JDK
105
+ `jar` tool, extracts sources into an isolated temporary directory and cleans it
106
+ afterward. Binary files get Git's binary-change notice. A normal comparison
107
+ performs no build, signing, version reservation or project writes. No application
108
+ source is changed by either mode. Files outside declared inputs are not included;
109
+ this is a released-app comparison, not a whole-repository or semantic-code audit.
110
+ Native changes require a new APK. Matching native hashes do not prove correctness:
111
+ function/signature compatibility and configured tests run during `patch`.
112
+
113
+ After setup and the first release APK build, the Gradle hook records an
114
+ `apk-record.json` pointer to the current baseline. `patch` uses that pointer,
115
+ reserves the next version, and writes `screens.js`, `patch.json` and an updated
116
+ `catalog.json` directly under `<screens-module>/build/patchrelay/patches`.
117
+ On a successful patch build, it replaces the previous generated patch folder;
118
+ the stable working catalog remains at its separate configured path.
119
+ For a non-GitHub host, setup stores the HTTPS base URL; patch files are
120
+ versioned under the generated release tag. The current signed catalog is kept at
121
+ the configured local catalog path so later patches preserve earlier base routes.
122
+ The command prints the three exact files and their hosting destinations. For a
123
+ personal server, upload `screens.js` and `patch.json` into the tag-named folder
124
+ under `patchBaseUrl`, then replace `catalog.json` at `catalogUrl` last. The APK
125
+ must be configured with the stable `catalogUrl`, never `patchBaseUrl`. See
126
+ [`docs/update-routing.md`](../docs/update-routing.md) for a concrete server layout
127
+ with multiple patch folders and the first-install checklist. Each run uses a new
128
+ tag folder; keep prior folders unchanged while the catalog references them. The
129
+ catalog URL stays the same and its signed contents are replaced after each upload.
130
+ Explicit `--baseline`, `--version`, `--url` and
131
+ `--out` still override the defaults. Native/contract changes require a new APK
132
+ baseline. Output directories are exclusive; prior releases are never overwritten.
133
+
134
+ GitHub artifact URLs use the configured repository and the convention
135
+ `patch-ROUTE_HASH-VERSION`; the route hash binds app ID, base version and exact
136
+ native fingerprint, avoiding collisions between apps/builds. `--tag` overrides
137
+ that release tag. Run `patch --github` to create a GitHub Release containing
138
+ `screens.js`, `patch.json` and the updated `catalog.json`. It uses the `origin`
139
+ GitHub repository and current branch, and publishes the catalog at the stable
140
+ `releases/latest/download/catalog.json` URL. GitHub credentials must be available
141
+ through Git Credential Manager or `GITHUB_TOKEN`. The command saves this catalog
142
+ URL in ignored `local.properties` for the next APK build. Install an APK built
143
+ with this URL before expecting it to discover release patches. For another HTTPS
144
+ host, the command prints exact upload paths and destinations. See
145
+ [`docs/update-routing.md`](../docs/update-routing.md) for how the app resolves
146
+ catalog → manifest → program and how to configure personal hosting.
147
+
148
+ Patch outputs from the configured one-command flow are under
149
+ `<screens-module>/build/patchrelay/patches`. `screens.js` is the complete
150
+ compiled program for the current supported screens, so it contains all current
151
+ screen changes together. Version numbers still increase for each exact APK base;
152
+ the local output folder is cleaned and reused, while the signed catalog records
153
+ the active manifest route.
154
+ A locked, local version record under `.patchrelay/versions` prevents reusing or
155
+ lowering a version for the same exact base even with a different output folder.
156
+ A failed write after reservation consumes that version; choose a higher one.
157
+ Back up these records. They cannot detect versions published from another
158
+ machine unless you retain the latest published records/catalog. All channels
159
+ for an exact base share the version sequence.
160
+
161
+ ## Stable update discovery
162
+
163
+ `patchrelay catalog` remains available for manual workflows. It asks for a signed
164
+ patch manifest, the existing signed catalog (or explicit `new` for the first
165
+ one), channel and final manifest HTTPS URL.
166
+ It verifies signatures/artifact hashes, preserves routes for older bases and
167
+ updates only that patch's exact app/base route. It rejects a route downgrade,
168
+ wrong app/channel, ambiguous routes and more than 64 supported base builds.
169
+ It writes a new signed `catalog.json` locally, never overwriting hosted data.
170
+
171
+ Use one stable catalog URL per app/channel. Different apps can share one GitHub
172
+ repository while using separate catalogs, signing keys and immutable patch
173
+ releases. Upload/verify `screens.js` and its `patch.json` first, then replace
174
+ the hosted catalog. Keep every artifact still referenced by an older base.
175
+ GitHub asset replacement is not atomic; a brief failed check must leave the
176
+ current app working. Private-source hosting still needs accessible HTTPS patch
177
+ delivery or a separate authenticated gateway; GitHub credentials aren't embedded.
178
+
179
+ The prepared host calls `updates.stageCatalog(catalogUrl, channel)` off the UI
180
+ thread. The SDK verifies the catalog, selects the exact installed fingerprint,
181
+ then verifies the selected manifest and downloads only the latest complete
182
+ program. No match means no update. Existing version/signature checks still apply.
183
+ This supports skipping intermediate patches, not arbitrary data migrations.
184
+ The new demo checks once per process after healthy first rendering, silently;
185
+ the downloaded patch activates on a later process start, not mid-session.
186
+
187
+ Automation can supply explicit values:
188
+
189
+ ```sh
190
+ patchrelay baseline --name android-v1
191
+ patchrelay patch --baseline .patchrelay/releases/baselines/android-v1/baseline.json --version 1
192
+ ```
193
+
194
+ There is no default signing key material and no default publisher repository.
195
+ The configured `publicKeyFile` must contain the Base64 DER public key pinned in
196
+ the app; signing refuses a mismatched private key. The key file format is the existing PatchRelay Base64 DER PKCS#8 RSA private key
197
+ (minimum 3072 bits). Keep it out of Git and releases. The CLI signs locally;
198
+ private keys are never sent to Maven, npm or the patch host.
199
+
200
+ Normal Gradle baselines are stored at
201
+ `.patchrelay/releases/baselines/APP_ID/VARIANT/apk-ARTIFACT_HASH/` or
202
+ `aab-ARTIFACT_HASH/`. Select the desired `baseline.json` explicitly in `patch`.
203
+ Both formats work for patch compilation; AAB files themselves are not installed
204
+ by this tool. `patchrelay baseline` remains useful for a developer-named APK record.
205
+
206
+ ## Recover a lost record
207
+
208
+ ```sh
209
+ patchrelay baseline --from YOUR-COMMIT
210
+ ```
211
+
212
+ You supply the record name and original APK when asked. Recovery checks out only
213
+ that explicitly supplied revision into a detached worktree, using its project
214
+ settings when present or the current module-layout settings otherwise. It uses
215
+ the selected revision's runtime contract and sources. Current branch/edits remain
216
+ untouched. The rebuilt APK must match the original APK hash exactly. Different
217
+ signing/toolchain output is rejected; without a matching release, ship a new base.
218
+ Recovery worktrees and build diagnostics stay under `.patchrelay/recovery/`.
219
+
220
+ ## Project contract
221
+
222
+ This CLI is independent of this repository's Python tools, but it is **not an
223
+ automatic integration plugin for arbitrary Android apps**. Projects still need:
224
+
225
+ - Prepared Kotlin/JS screens and explicit native hooks.
226
+ - `runtime.json` format 1 / screen API 2 with app/base/screen identity, declared
227
+ native inputs and text-normalization extensions.
228
+ - Gradle generation of `HOST_SHA256`, `BASE_VERSION` and `APPLICATION_ID` in the
229
+ configured BuildConfig, following the existing sorted SHA-256 contract.
230
+ - An Android host using the SDK and configured trust keys/update endpoint.
231
+
232
+ The CLI does not scan ordinary Kotlin to discover patchable methods. Multiple
233
+ app/variant projects need separately reviewed configuration and release records.
234
+ Baseline selection remains manual. Signed catalog authoring/routing is implemented;
235
+ automatic remote uploads and arbitrary app integration are still pending.
236
+
237
+ ## Build this CLI from source
238
+
239
+ ```sh
240
+ cd cli
241
+ npm test
242
+ npm pack
243
+ ```
244
+
245
+ The npm tarball contains CLI runtime files, the Gradle build-plugin source,
246
+ documentation and license: no Android SDK/app implementation, signing keys,
247
+ baseline archives or private R8 mappings.
248
+ The CLI is JavaScript distributed to developers, so its runtime is inspectable.
249
+ Android SDK AAR obfuscation is a separate build step, not protection for CLI code.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@orbital_mcp/patchrelay",
3
- "version": "0.1.0-alpha04",
3
+ "version": "0.1.0-alpha05",
4
4
  "description": "Build and sign patches for prepared PatchRelay Android projects",
5
5
  "type": "module",
6
6
  "bin": { "patchrelay": "bin/patchrelay.mjs" },
7
7
  "engines": { "node": ">=22" },
8
- "files": ["bin/", "src/", "README.md", "LICENSE", "gradle-plugin/build.gradle", "gradle-plugin/settings.gradle", "gradle-plugin/src/main/"],
8
+ "files": ["bin/", "src/", "README.md", "LICENSE", ".patchrelay-tools/build.gradle", ".patchrelay-tools/settings.gradle", ".patchrelay-tools/src/main/"],
9
9
  "scripts": {
10
10
  "test": "node --test test/*.test.mjs",
11
11
  "prepack": "node scripts/package.mjs"