@omega.js/desktop 0.51.0 → 0.52.0

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.
@@ -58,7 +58,7 @@ stale.
58
58
  | web | `.github/workflows/build.yml` (authoritative, regenerated by the target scaffold every verb runs) | `workflow_dispatch` | `gh-pages` on the target's OWN WEBSITE REPO, `<brand.id>-<target name>` under `repo.org` ([#883](https://github.com/Omega-JS-Stack/omega/issues/883), `@omega.js/config`'s `websiteRepo`): the job runs the framework's own `omega deploy --direct`, so CI and a hand deploy take one code path, and no workflow input names a repo |
59
59
  | extension | `.github/workflows/publish.yml` (authoritative via defaults engine) | same | the stores the brand DECLARES ([#867](https://github.com/Omega-JS-Stack/omega/issues/867): a declared store with no credential refuses, one whose listing does not exist yet prints the manual step) + the package zips attached to a GitHub release `<target name>-v<version>` (`extension-v<version>` for the default name) in the brand's ONE public releases repo, `<brand.id>-releases` ([#883](https://github.com/Omega-JS-Stack/omega/issues/883); D9 addendum 2: durable artifact channel, no 7-day purge). The release is made by the publish TASK, in node through the devkit `gh` wrapper, so the workflow types no repo name and carries the brand's cross-repo `GH_TOKEN`, never `secrets.GITHUB_TOKEN` (the extension is a declared consumer of that key in the env schema, delivery `ci`, so its own secrets step arms the repo with it rather than waiting for a sibling target's). It runs right after the zip check and BEFORE the store gate: the releases repo is the channel the brand owns, so a brand with no store credentials, or one failed store upload, still publishes its zips. A FIRST Firefox publish (no `targets.<name>.listings.firefox.id`) also signs with `--amo-metadata` ([#884](https://github.com/Omega-JS-Stack/omega/issues/884)): AMO will not create a listing without a summary (`brand.description`, cut to 250), categories (`targets.extension.categories`, default `['alerts-updates']`) and a license (the target `package.json` `license`, `UNLICENSED` listing as `all-rights-reserved`), so the deploy that creates the listing carries them and every update carries none. The id the listing is created under is not the store's gift: it is the brand's own derived value, pinned into the brand config as `targets.<name>.listings.firefox.id` by the extension's LOCAL scaffold before the snapshot is pushed, and the runner writes no config at all ([#893](https://github.com/Omega-JS-Stack/omega/issues/893)). The three store listing IDS are config, not repo secrets: only the store API credentials ride the secrets block |
60
60
  | desktop | `.github/workflows/build.yml` | `workflow_dispatch` (carrying the `platforms` input), the same one trigger its three siblings carry ([#880](https://github.com/Omega-JS-Stack/omega/issues/880): the manual-only holdout, converged; [#923](https://github.com/Omega-JS-Stack/omega/issues/923): narrowed to the single event, below) | GitHub releases in the brand's ONE public releases repo, always `<brand.id>-releases` under `repo.org` (`@omega.js/config`'s `releasesRepo`, [#799](https://github.com/Omega-JS-Stack/omega/issues/799); the owner/repo overrides are retired, [#883](https://github.com/Omega-JS-Stack/omega/issues/883)): electron-builder's publish block, the signed-Windows upload and the autoupdater feed all name it, and the deploy precheck provisions it public through the devkit `ensureRepo`. Each published asset is linked from the site at `/download/<platform>/<format>` (`/download/mac/dmg`, `/download/windows/nsis`, `/download/linux/deb`, [#867](https://github.com/Omega-JS-Stack/omega/issues/867)); the segments those three replaced (`mac/universal`, `windows/universal`, `linux/debian`) keep their pages as redirects to the same file, and `/download/linux/snap` goes to the brand's Snap Store listing |
61
- | backend | `.github/workflows/deploy.yml` (template at `packages/backend/src/defaults/.github/workflows/deploy.yml`, composed into the brand root as `backend-deploy.yml` like the other three) | `workflow_dispatch` | the Firebase deploy the runner performs with `omega deploy --direct` (the framework bin by path, below) (checkout, setup-node, the firewall step, `sfw npm install`, the target `.env` written from the generated secrets block, the service-account file, the Firebase CLI, `gcloud` authenticated with that file, then the verb) |
61
+ | backend | `.github/workflows/deploy.yml` (template at `packages/backend/src/defaults/.github/workflows/deploy.yml`, composed into the brand root as `backend-deploy.yml` like the other three) | `workflow_dispatch` | the Firebase deploy the runner performs with `omega deploy --direct` (the framework bin by path, below) (checkout, setup-node, the firewall step, `sfw npm ci`, the target `.env` written from the generated secrets block, the service-account file, the Firebase CLI, `gcloud` authenticated with that file, then the verb) |
62
62
 
63
63
  Existing consumers converge on their next omega verb (both scaffold
64
64
  engines treat workflow files as framework-owned overwrites).
@@ -73,6 +73,8 @@ engines treat workflow files as framework-owned overwrites).
73
73
 
74
74
  **All four templates pin the SAME actions** ([#880](https://github.com/Omega-JS-Stack/omega/issues/880)): `actions/checkout@v7` and `actions/setup-node@v7` on every one of the four, and the family major for each of the rest wherever a template reaches for one (`actions/upload-artifact@v7`, `actions/download-artifact@v8`, `actions/cache@v6`), so an action upgrade is one edit for the family rather than four to remember, and the same test file pins both halves. A checkout is `fetch-depth: 1` wherever no job reads git HISTORY, which today is everywhere: nothing in a desktop release reads history (electron-builder publishes over the API), and the extension build's `git config` and `git archive HEAD` are both answered by a depth-1 clone. Each template also carries the sibling header (the deliberate-deploys framing plus the regenerated-by-ensureTarget line), a version-logging step, and a `timeout-minutes` on every job, so no run can hold a runner for GitHub's six-hour default.
75
75
 
76
+ **One install step on every lane: `sfw npm ci {{ installWorkspace }}`** ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)): all four templates install with `npm ci`, composed in a brand as `sfw npm ci --workspace .` (desktop's cmd-shelled Windows legs spell it `sfw npm.cmd ci`, and its self-hosted signing box runs the same `npm.cmd ci` without the firewall, above). `npm ci` installs the brand lockfile exactly as the snapshot carried it and REFUSES one that disagrees with the manifests, where `npm install` re-resolves around it: that difference is how one 0.51.0 deploy saw web follow a stale lock's local-era link entries and never land its bin while desktop, the one template already on `npm ci`, refused the same lock. The lockfile the runner installs is therefore the deploy's contract, and the registry lane gates it before anything is pushed (below). `ci-workflows.test.js` pins the install line on all four templates.
77
+
76
78
  **The playground deploys like any other brand** ([#872](https://github.com/Omega-JS-Stack/omega/issues/872), retiring the [#802](https://github.com/Omega-JS-Stack/omega/issues/802) rehearsal repo): the playground is a DIRECTORY inside this monorepo, so the workflows it composes at `brands/playground-omega/.github/workflows/` sit where GitHub never looks, and the run had nowhere to execute. That is exactly what a NESTED brand is, and the one lane answers it for every brand shaped that way: `npx omega deploy` in `brands/playground-omega/targets/desktop` force-pushes the brand folder to `Omega-JS-Stack/playground-omega` at `omega-deploy` ([#915](https://github.com/Omega-JS-Stack/omega/issues/915): the mirror repo's `main` now holds the composed workflow files and nothing else, because that is where GitHub registers them from), waits for GitHub to list `desktop-build.yml` there, and dispatches it against the deploy branch. Nothing of the monorepo's own history goes with the snapshot. The private `playground-rehearsal` repo, the two `scripts/playground-desktop-*.js` generators, their tests and the `npm run rehearse:desktop` command are all gone: the playground gets no treatment a brand does not, and the thing that used to be rehearsed is now the lane itself.
77
79
 
78
80
  **A web deploy fills its own base path** ([#358](https://github.com/Omega-JS-Stack/omega/issues/358)): the target url set → the site serves at that domain's root (the CNAME both lanes publish cannot carry a path) → `OMEGA_PATH_PREFIX=/`; unset → the default project address `https://<owner>.github.io/<brand.id>-<target>/` → `/<brand.id>-<target>/`, from the same WEBSITE REPO the direct plan pushes to. `--direct` sets it around the build it runs itself, and CI runs that same lane; the scaffolded `build.yml` also prints it through the SAME function (`@omega.js/web/deploy`'s `targetPathPrefix()`) into `$GITHUB_ENV` before the step that consumes it. `GITHUB_REPOSITORY` names the repo the run CHECKED OUT (the source repo), so it names nothing here. An explicitly exported `OMEGA_PATH_PREFIX` wins (publisher machinery like workkit supplies its own); `omega dev` and a bare `omega build` stay at the root ([#355](https://github.com/Omega-JS-Stack/omega/issues/355)).
@@ -83,7 +85,7 @@ engines treat workflow files as framework-owned overwrites).
83
85
 
84
86
  **Cache headers come from the EDGE, not from hosting** ([#751](https://github.com/Omega-JS-Stack/omega/issues/751)). A web deploy publishes to GitHub Pages, and GitHub Pages has no header configuration of any kind — nothing in `packages/web/src` or `packages/manager/src` writes a hosting config for the built site, and the one `firebase.json` a brand carries belongs to the BACKEND target (`packages/backend/templates/firebase.json`), and it declares `hosting.public: dist/public` plus the `omega_api` rewrites for `api.<domain>` — the API host only, and with no `headers` block. So the lifetime of a content-hashed bundle is set by Cloudflare cache rules, which the manager's edge service ships as framework defaults — hashed `/assets` for a year, HTML for a minute ([docs/manager/edge.md](../manager/edge.md)). A brand whose site does not sit behind Cloudflare gets GitHub Pages' own defaults and has nothing to tune.
85
87
 
86
- **The secrets those workflows read come from `.env`** ([#189](https://github.com/Omega-JS-Stack/omega/issues/189)): web's `omega deploy` precheck publishes the target's COMPOSED env: `composeTargetEnv({ targetDir, target: 'web' })`, the brand root's `.env` schema-filtered to the target's keys under an optional target `.env`, files only ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)), to the brand repo's Actions secrets through `@omega.js/devkit/actions-secrets` (the `gh` CLI, values on stdin, never logged) and regenerates the workflow's env block, since [#627](https://github.com/Omega-JS-Stack/omega/issues/627) both the block and the published set derive from ONE primitive in `@omega.js/config/env-delivery` (`deliveredKeys(target, modes, { values })`), read off the env schema's `delivery` declarations AND the target's composed PRODUCTION values, one `KEY: ${{ secrets.KEY }}` line per delivered key. Three kinds of key ride it: a schema-NAMED delivery; a `match` FAMILY member the schema knows only as a shape, expanded from the composed env, which is how the OAuth `CONNECTIONS_*` credentials finally reach a dispatched backend deploy ([#876](https://github.com/Omega-JS-Stack/omega/issues/876)); and a CUSTOM key the schema never declared, a consumer's own line in the brand `.env` or its `.env.production`, whose workflow step used to fail silently for want of it ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)). The one SKIP is a key that stays local: a `machineLocal` path, and a declared key whose delivery for this target is the laptop's own `.env` (plus the two names a composed set may never claim: a workflow-owned one, and a `GITHUB_`-prefixed one GitHub refuses as a secret). A custom key takes its target's FILE mode, written into the `.env` the backend runner builds and reaching the runner env alone on web, desktop and the extension. Still never a raw `.env` scan: the composed set is schema-filtered for every key the schema knows. And both halves (what the push publishes, what the backend workflow's `.env` writer names) read that ONE set inside a run, so they cannot drift apart. Extension gained the same precheck step ([#680](https://github.com/Omega-JS-Stack/omega/issues/680)), desktop's joined them ([#682](https://github.com/Omega-JS-Stack/omega/issues/682)), and backend's arrived with its workflow ([#872](https://github.com/Omega-JS-Stack/omega/issues/872): its set adds `OMEGA_SERVICE_ACCOUNT_JSON`, the key file's own bytes, and the workflow writes the target `.env` back out of the block). There is no per-framework BIND any more and no standalone `omega push-secrets` verb ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): ONE function, `publishTargetSecrets({ targetDir, target, logger, dryRun })`, is called by each framework's deploy precheck and by the manage walk's `repo` service (`secrets` op), and the per-target shape differences live in one seam table inside it (desktop base64-encodes a secret that names a FILE and derives its signing paths from the signing tree; backend brings the service-account key as an extra secret; web and the extension bring none). `--dry-run` prints the key NAMES that would publish, the derived lines, and any refusal, and sends nothing ([#895](https://github.com/Omega-JS-Stack/omega/issues/895)); a refusal still throws, because a plan that cannot be made is loud. No target mints a PAT for this; `gh`'s auth session is the credential. So a CI build has exactly the keys the brand delivers to it, with no hand-created secrets and no hand-edited workflow. The step is `fatal` on all four frameworks: a refused or half publish stops the deploy before the dispatch. `--no-secrets` opts out; a repo-less/remote-less brand, a CI run, or a checkout that isn't the brand's declared repo skips loudly. **A step that runs only when a secret EXISTS gates on `env`, never on the secret** ([#715](https://github.com/Omega-JS-Stack/omega/issues/715)): the `secrets` context is available in no `if` expression, and one `if: ${{ secrets.X != '' }}` makes GitHub refuse to parse the whole file: a zero-job "workflow file issue" run on every push, however the workflow's own triggers are set. The key goes in the workflow-level `env:` block and the step reads `if: env.X != ''`; consumers heal on the next verb's ensureTarget. Web's Cloudflare purge no longer needs even that: the gate moved INTO the deploy verb with the purge itself ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)), which reads the env value and skips with a line when it is empty. Details: [docs/web/index.md](../web/index.md).
88
+ **The secrets those workflows read come from `.env`** ([#189](https://github.com/Omega-JS-Stack/omega/issues/189)): web's `omega deploy` precheck publishes the target's COMPOSED env: `composeTargetEnv({ targetDir, target: 'web' })`, the brand root's `.env` schema-filtered to the target's keys under an optional target `.env`, files only ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)), to the brand repo's Actions secrets through `@omega.js/devkit/actions-secrets` (the `gh` CLI, values on stdin, never logged) and regenerates the workflow's env block, since [#627](https://github.com/Omega-JS-Stack/omega/issues/627) both the block and the published set derive from ONE primitive in `@omega.js/config/env-delivery` (`deliveredKeys(target, modes, { values })`), read off the env schema's `delivery` declarations AND the target's composed PRODUCTION values, one `KEY: ${{ secrets.KEY }}` line per delivered key. Three kinds of key ride it: a schema-NAMED delivery; a `match` FAMILY member the schema knows only as a shape, expanded from the composed env, which is how the OAuth `CONNECTIONS_*` credentials finally reach a dispatched backend deploy ([#876](https://github.com/Omega-JS-Stack/omega/issues/876)); and a CUSTOM key the schema never declared, a consumer's own line in the brand `.env` or its `.env.production`, whose workflow step used to fail silently for want of it ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)). The one SKIP is a key that stays local: a `machineLocal` path, and a declared key whose delivery for this target is the laptop's own `.env` (plus the two names a composed set may never claim: a workflow-owned one, and a `GITHUB_`-prefixed one GitHub refuses as a secret). A custom key takes its target's FILE mode, written into the `.env` the backend runner builds and reaching the runner env alone on web, desktop and the extension. Still never a raw `.env` scan: the composed set is schema-filtered for every key the schema knows. And both halves (what the push publishes, what the backend workflow's `.env` writer names) read that ONE set inside a run, so they cannot drift apart. Extension gained the same precheck step ([#680](https://github.com/Omega-JS-Stack/omega/issues/680)), desktop's joined them ([#682](https://github.com/Omega-JS-Stack/omega/issues/682)), and backend's arrived with its workflow ([#872](https://github.com/Omega-JS-Stack/omega/issues/872): its set adds `OMEGA_SERVICE_ACCOUNT_JSON`, the key file's own bytes, and the workflow writes the target `.env` back out of the block). There is no per-framework BIND any more and no standalone `omega push-secrets` verb ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): ONE function, `publishTargetSecrets({ targetDir, target, logger, dryRun })`, is called by each framework's deploy precheck and by the manage walk's `repo` service (`secrets` op), and the per-target shape differences live in one seam table inside it (desktop base64-encodes a secret that names a FILE and derives its signing paths from the signing tree; backend brings the service-account key as an extra secret; web and the extension bring none). `--dry-run` prints the key NAMES that would publish, the derived lines, and any refusal, and sends nothing ([#895](https://github.com/Omega-JS-Stack/omega/issues/895)); a refusal still throws, because a plan that cannot be made is loud. No target mints a PAT for this; `gh`'s auth session is the credential. So a CI build has exactly the keys the brand delivers to it, with no hand-created secrets and no hand-edited workflow. The step is `fatal` on all four frameworks: a refused or half publish stops the deploy before the dispatch. `--no-secrets` opts out; a repo-less/remote-less brand or a CI run skips loudly, and a checkout whose `origin` is not the derived source repo REFUSES on the one drift line of the [origin gate](#the-origin-gate-934), because arming another repo's Actions with this brand's secrets is the harm itself (a nested brand, whose remote is the enclosing repo's by construction, publishes to the derived repo without the check). **A step that runs only when a secret EXISTS gates on `env`, never on the secret** ([#715](https://github.com/Omega-JS-Stack/omega/issues/715)): the `secrets` context is available in no `if` expression, and one `if: ${{ secrets.X != '' }}` makes GitHub refuse to parse the whole file: a zero-job "workflow file issue" run on every push, however the workflow's own triggers are set. The key goes in the workflow-level `env:` block and the step reads `if: env.X != ''`; consumers heal on the next verb's ensureTarget. Web's Cloudflare purge no longer needs even that: the gate moved INTO the deploy verb with the purge itself ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)), which reads the env value and skips with a line when it is empty. Details: [docs/web/index.md](../web/index.md).
87
89
 
88
90
  ## The mac signing ladder fails loudly at every rung ([#891](https://github.com/Omega-JS-Stack/omega/issues/891))
89
91
 
@@ -132,7 +134,7 @@ Every target runs the SAME three beats ([#872](https://github.com/Omega-JS-Stack
132
134
  | backend | `deploy.yml` | the Firebase deploy from this machine: artifact cleanup-policy pre-step, the pack step, `firebase deploy`, public-invoker IAM fix (this is what the runner itself runs) | `--dry-run`, `--direct`, `--only <targets>` pass-through (e.g. `--only hosting` deploys on Spark where functions would demand Blaze) |
133
135
  | backend, `projectType: 'custom'` | REFUSED: a custom-server backend has no Cloud Functions to publish ([#584](https://github.com/Omega-JS-Stack/omega/issues/584)). The container host is the publisher; see the Render lane below | REFUSED | — |
134
136
 
135
- **A brand whose cloud project is SHARED never deploys its backend** ([#882](https://github.com/Omega-JS-Stack/omega/issues/882)): `cloud.shared: true` (`config/omega.json5` → `cloud`) makes the backend verb quit at its first gate, ahead of the scaffold, the precheck and the version assert, printing ONE line (`Skipping the backend deploy: cloud.shared is true (config/omega.json5 → cloud), and a shared project is never deployed from a brand`) and exiting 0. Every lane takes it, because the PROJECT is the reason: a laptop `omega deploy`, the brand-root fan-out (which reads the exit 0, moves on to the group and ticks the target in its summary), and the composed workflow's own `omega deploy --direct` step, which makes a dispatch of that workflow a green no-op. Nothing changes about the workflow itself: it still composes for the target like every other one, so the skip lives in exactly one place instead of two that could disagree. Why the verb rather than a per-target switch: a shared project belongs to several brands at once, the manage cycle already limits itself to the per-brand operations there ([the cloud service](../manager/cloud.md)), and a deploy would publish this brand's functions and rules over the project every one of those brands runs on. It is a SKIP, not a refusal: the deploy lane refuses (exit 1) only what would ship broken, such as version drift or an empty required secret.
137
+ **A brand whose cloud project is SHARED never deploys its backend** ([#882](https://github.com/Omega-JS-Stack/omega/issues/882)): `cloud.shared: true` (`config/omega.json5` → `cloud`) makes the backend verb quit at its first gate, ahead of the scaffold, the precheck and the version assert, printing ONE line (`Skipping the backend deploy: cloud.shared is true (config/omega.json5 → cloud), and a shared project is never deployed from a brand`) and exiting 0. Every lane takes it, because the PROJECT is the reason: a laptop `omega deploy`, the brand-root fan-out (which reads the exit 0, moves on to the group and ticks the target in its summary), and the composed workflow's own `omega deploy --direct` step, which makes a dispatch of that workflow a green no-op. Nothing changes about the workflow itself: it still composes for the target like every other one, so the skip lives in exactly one place instead of two that could disagree. Why the verb rather than a per-target switch: a shared project belongs to several brands at once, the manage cycle already limits itself to the per-brand operations there ([the cloud service](../manager/cloud.md)), and a deploy would publish this brand's functions and rules over the project every one of those brands runs on. It is a SKIP, not a refusal: the deploy lane refuses (exit 1) only what would ship broken, such as version drift, a lockfile that disagrees with the manifests, or an empty required secret.
136
138
 
137
139
  Every one of them runs the OMEGA license check once on the way — the production build for web/desktop/extension, the deploy itself for backend — and bakes the verdict into that artifact. What each target does with it, and what a keyless verdict costs: [publishing.md § The license check](publishing.md#the-license-check-320).
138
140
 
@@ -192,7 +194,7 @@ fully offline).
192
194
 
193
195
  ## Local frameworks in production artifacts: the ONE pack step ([#872](https://github.com/Omega-JS-Stack/omega/issues/872))
194
196
 
195
- A brand linked to the local monorepo (`file:` specs) ships the LOCAL framework code to production with no npm publish involved. Supported on purpose (Ian 2026-07-20: iterate fast on unproven framework changes without burning registry versions), and since [#872](https://github.com/Omega-JS-Stack/omega/issues/872) it is ONE mechanism instead of four: the tarballs travel with the snapshot and the RUNNER does a plain `npm install`. No clone, no `preinstall`, nothing framework-aware on the box.
197
+ A brand linked to the local monorepo (`file:` specs) ships the LOCAL framework code to production with no npm publish involved. Supported on purpose (Ian 2026-07-20: iterate fast on unproven framework changes without burning registry versions), and since [#872](https://github.com/Omega-JS-Stack/omega/issues/872) it is ONE mechanism instead of four: the tarballs travel with the snapshot and the RUNNER does a plain `npm ci` against the lockfile the pack step regenerated. No clone, no `preinstall`, nothing framework-aware on the box.
196
198
 
197
199
  ### The lane matrix
198
200
 
@@ -201,7 +203,7 @@ the two refusals it still owes:
201
203
 
202
204
  | The brand tree | Mode | Ref | Why |
203
205
  |---|---|---|---|
204
- | any brand with a git repo (nested or its own toplevel, linked or registry clean) | `snapshot` | `omega-deploy` | the deploy branch IS what CI builds, for every brand and every starter of a deploy, so the brand's real history never carries packed tarballs and nobody's uncommitted tree is committed to reach a runner. `nested` and `linked` stay on the lane as FACTS (the behind check skips a nested brand, the pack step runs for a linked one, the secrets publisher reads `nested`), never as a second lane |
206
+ | any brand with a git repo (nested or its own toplevel, linked or registry clean) | `snapshot` | `omega-deploy` | the deploy branch IS what CI builds, for every brand and every starter of a deploy, so the brand's real history never carries packed tarballs and nobody's uncommitted tree is committed to reach a runner. `nested` and `linked` stay on the lane as FACTS (the behind check skips a nested brand, the pack step runs for a linked one, the lockfile gate for an unlinked one, the secrets publisher reads `nested`), never as a second lane |
205
207
  | **linked** with no git repo at all | REFUSED | | a snapshot is built out of a git index, so the lane says so by name (`git init` the brand, or `omega i live` for registry versions) instead of dying later on a raw `fatal: not a git repository` |
206
208
  | plain, no git repo at all | `dispatch` | `omega-deploy` | nothing to snapshot FROM and nothing linked that would need to (the lane carries `repo: false`), so the lane is the wait and the dispatch alone, on whatever the branch already holds |
207
209
 
@@ -211,15 +213,36 @@ the two refusals it still owes:
211
213
 
212
214
  - **What it packs**: every `file:` dep pointing OUTSIDE `dir`, read from `dir/package.json` AND from every workspace member manifest; plus, transitively, every `@omega.js` runtime dep of a packed package that resolves through a node_modules SYMLINK (the local-era shape workspaces and `omega i local` produce, which the registry may have no copy of).
213
215
  - **Where the tarballs land**: `dir/omega_modules/*.tgz`, one `npm pack` each. Pack runs the package's REAL prepare, so the tarball reflects current source and is self-contained (cp241 vendored every publishable, which is what makes this work for any linked @omega.js dep).
214
- - **How the manifests are respelled**: each manifest's own direct spec becomes a path relative to THAT manifest (`file:../../omega_modules/x.tgz` from `targets/web`); transitive ones become npm `overrides` in the ROOT manifest, since only an override redirects a NESTED requirement to the artifact beside it. Then `dir/package-lock.json` is deleted and regenerated (`npm install --package-lock-only --ignore-scripts --no-audit --no-fund`), so lock regeneration never asks the registry for an unpublished package.
216
+ - **How the manifests are respelled**: each manifest's own direct spec becomes a path relative to THAT manifest (`file:../../omega_modules/x.tgz` from `targets/web`); transitive ones become npm `overrides` in the ROOT manifest, since only an override redirects a NESTED requirement to the artifact beside it. Then `dir/package-lock.json` is deleted and regenerated through devkit's ONE lock helper, `regenerateLockfile({ root })` in `src/lockfile.js` (`npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix <root>`, through `safeInstall`; the same helper `omega i live` and `omega i local` call, [#938](https://github.com/Omega-JS-Stack/omega/issues/938)), so lock regeneration never asks the registry for an unpublished package.
215
217
  - **The restore**: the call returns `{ staged, restore }`, and `restore` rewrites every touched manifest and the lockfile byte-for-byte and removes `omega_modules/`. Any failure inside the lane restores FIRST and then throws with the lane named, so nothing deploys off a half-staged tree.
216
218
  - **Idempotent across hops**: a `file:` spec that already points at a `.tgz` (a dep or an override, the manifest's own or inherited from the workspace root) is COPIED into this dir's `omega_modules/` and respelled, never packed again. That second hop is how the runner's backend `omega deploy --direct` stages its `functions/` folder out of the snapshot's tarballs, and how a local `@omega.js/client` override reaches Cloud Build.
217
219
 
218
220
  Cloud Build is why the backend needed this first: the buildpack installs only what sits inside the uploaded functions folder, with `npm ci` against a lockfile, so a `file:` spec pointing anywhere outside it can never deploy as-is. The Artifact Registry cleanup policy is still ensured before deploying, because firebase-tools otherwise exits 1 AFTER a successful functions deploy, which would skip the public-invoker fix.
219
221
 
222
+ ### The registry lane's lockfile gate ([#938](https://github.com/Omega-JS-Stack/omega/issues/938))
223
+
224
+ A registry (unlinked) snapshot ships the brand's OWN `package-lock.json` untouched, and the runner's `npm ci` installs exactly what it says. So before anything else in that lane but the [origin gate](#the-origin-gate-934) (before the default branch is read or healed, before a workflow file or the snapshot is pushed) `assertBrandLockfile({ root })` in `@omega.js/devkit/brand-version`, beside the version drift gate, reads the lock against every `@omega.js/*` registry spec in the brand root's `package.json` and each `targets/*/package.json`:
225
+
226
+ - **The lock must exist.** A brand with none refuses (`<root> has no package-lock.json`).
227
+ - **Each dependency must be locked as a REGISTRY entry**, found where npm would resolve it (the target's own `targets/<name>/node_modules/<pkg>` first, then the hoisted `node_modules/<pkg>`): not absent, not `link: true` (the local era's symlink into a checkout), and not a `resolved` that is a path (a packed `file:` tarball or a directory) rather than a registry URL.
228
+ - **At a version that satisfies the manifest's spec** (exact, `^`, `~`, `>=`, x-ranges and `*`, read by devkit's `update.js` range reader), so a lock at 0.50.0 under a 0.51.0 spec refuses.
229
+ - **No stale lock entry for a folder no longer on disk declares a non-registry `@omega.js` spec.** A renamed target leaves its old workspace entry behind (`targets/website` after the move to `targets/web`), and npm keeps honoring the `file:` spec it declares, re-creating the link on the next lock-only run.
230
+
231
+ The refusal lists every disagreeing entry as `<manifest>: <package> <spec> <why>` and names the one fix: **run `omega i live` in the brand**, which regenerates the brand's own lockfile from its manifests ([local-dev.md](local-dev.md)). It runs on `--dry-run` too, because it reads only disk, the same as the version gate. The linked lane never runs it: its pack step deletes and regenerates the lock for the staged shape, and a brand with no repo pushes no lockfile at all. In a brand-root fan-out the gate runs once, in the run's one delivery, and each target's dry run runs it for itself.
232
+
233
+ ### The origin gate ([#934](https://github.com/Omega-JS-Stack/omega/issues/934))
234
+
235
+ Every lane acts on the DERIVED source repo, `<brand.id>-omega` under `repo.org`: the workflow compose, the snapshot push and the dispatch all address it. A checkout whose `origin` names another repo (a transfer or a rename nobody carried into the config) would push the mirror to one repo while the clone points at another, silently. So before anything else in every lane, the lockfile gate included, `assertOriginMatches({ dir })` in `@omega.js/devkit/git-remote` reads the brand's OWN `origin` and compares its whole slug with `sourceRepo(config).slug` through `@omega.js/config`'s `repoDrift` (case-insensitive, GitHub's own comparison), against the brand's production config, the one every dispatch address reads. A mismatch refuses with the one line the boot prelude also prints:
236
+
237
+ ```
238
+ origin is <slug> but config derives <derived>: fix repo.org in config/omega.json5 or move the repo
239
+ ```
240
+
241
+ It runs on `--dry-run` too, because it only reads. It runs on the dispatch-only lane as well, since that dispatch still addresses the derived repo; a brand outside git has no origin to disagree, which the gate answers as nothing to compare. The same holds for no `.git` AT the brand root (a nested brand such as `brands/playground-omega`, whose git toplevel is somebody else's repo), no `origin` at all (a brand nobody has pushed yet), and a non-GitHub remote. In a brand-root fan-out the gate runs once, in the run's one delivery, and each target's dry run runs it for itself. `omega manage` refuses on the same line, in the repo service before any ensure ([manager/repo.md](../manager/repo.md)), and so does the secrets precheck `publishTargetSecrets` (each framework's deploy precheck, and the walk's `secrets` op), through the same `assertOriginMatches`, where it used to warn and skip on a comparison of its own. Which gate speaks first depends on the run, and both print the same line: in a brand-root fan-out the run's one delivery lane refuses before any target's precheck starts; in a single-target `omega deploy` the secrets precheck runs ahead of that target's lane and refuses first.
242
+
220
243
  ### The snapshot push
221
244
 
222
- `deployViaDispatch` runs the lane in this order: read the repo's DEFAULT branch once and heal it when published output has taken it over (a `gh-pages` default moves back to `main` here, before any workflow file is written: [#922](https://github.com/Omega-JS-Stack/omega/issues/922), once per repo, and never on a dry run, which returns before the delivery reads anything at all), refuse a checkout behind it (skipped for a nested brand, whose git toplevel is somebody else's repo), push the composed workflow files to that branch when they differ (`pushWorkflowFiles`), pack (when the tree is linked), push the folder to `omega-deploy` (`@omega.js/devkit/deploy-snapshot`'s `pushSnapshot`), restore, wait for the REF to carry the pushed sha (`waitForRef`, [#902](https://github.com/Omega-JS-Stack/omega/issues/902)), wait for the workflow (`waitForWorkflow`), dispatch.
245
+ `deployViaDispatch` runs the lane in this order: the origin gate above; on a registry (unlinked) lane, the lockfile gate above; read the repo's DEFAULT branch once and heal it when published output has taken it over (a `gh-pages` default moves back to `main` here, before any workflow file is written: [#922](https://github.com/Omega-JS-Stack/omega/issues/922), once per repo, and never on a dry run, which returns before the delivery reads anything at all), refuse a checkout behind it (skipped for a nested brand, whose git toplevel is somebody else's repo), push the composed workflow files to that branch when they differ (`pushWorkflowFiles`), pack (when the tree is linked), push the folder to `omega-deploy` (`@omega.js/devkit/deploy-snapshot`'s `pushSnapshot`), restore, wait for the REF to carry the pushed sha (`waitForRef`, [#902](https://github.com/Omega-JS-Stack/omega/issues/902)), wait for the workflow (`waitForWorkflow`), dispatch.
223
246
 
224
247
  **A `snapshot` sha skips everything up to the ref wait** ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): it says the caller already put this run's snapshot on the lane's ref, so the verb keeps the wait and the dispatch and its dispatch line names the sha (`Dispatched <workflow> (snapshot lane, ref omega-deploy @ <sha7>)`). The brand-root fan-out is its ONE caller, through `--snapshot=<sha>` on all four verbs; on a brand with no repo it is an error, because nothing was pushed to skip.
225
248
 
@@ -84,11 +84,13 @@ Unchanged contract for consumers, now monorepo-backed: `mgr i local` (web, deskt
84
84
 
85
85
  **Linking is brand-tree-wide by construction (cp194):** npm resolves the WHOLE workspace tree on any install anchored in a brand monorepo, so linking one target while a sibling still carries an unpublished registry spec (`@omega.js/backend: *`) 404s before anything links — only reachable in a brand OUTSIDE the omega monorepo, the real consumer topology. `linkLocalPackages()` therefore flips every target's `@omega.js/*` specs to `file:` first (dev/prod placement preserved; specs computed from REAL paths so symlinked/aliased dirs can't dangle), then runs ONE `npm install` for the tree. One call from any target links the whole brand; reruns all-skip.
86
86
 
87
+ **Both flips regenerate the brand's OWN lockfile** ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)): after the tree install, `omega i local` and `omega i live` run devkit's one lock helper, `regenerateLockfile({ root })` (`npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix <brand root>`, the same helper the deploy's pack step calls). The reason is nesting: every in-repo brand is itself a workspace of the omega monorepo (`brands/*`), so a plain `npm install` in the brand climbs to the monorepo root and writes THAT lockfile, leaving the brand's own describing whatever it described before; `--prefix` makes npm treat the brand as the install root. Before it runs, the helper first prunes every lock entry for a folder no longer on disk (a renamed target's old workspace entry, everything nested under it, and every link pointing into it), because npm keeps honoring what such an entry declares; then it drops every `@omega.js/*` lock entry the deploy's lockfile gate would refuse (a `link: true` entry and its link target, a path-resolved entry, a version outside the spec), because npm alone KEEPS a locked link whose checkout already sits at the published version, and every other pin stays as it was. `omega i live` also heals when there is nothing to flip: specs already on registry versions under a lock that still disagrees get the lock regenerated alone, since that verb is the fix the gate names ([deploys.md](deploys.md)). A failed install restores every flipped manifest; a failed regeneration runs after a successful install, so it leaves the manifests and the installed tree as they agree and only puts the lock back byte for byte, failing loudly as a lock regeneration.
88
+
87
89
  **The brand root itself is part of the tree (cp195):** onboard scaffolds `@omega.js/manager` into the brand root's devDependencies — the omega-bin dispatcher resolves brand-level verbs (`omega dev`, manage, the scaffolded `start`/`manage` scripts) FROM the brand root, and without the declaration nothing installs the manager outside the monorepo (inside it, workspace hoisting masked the gap). `linkLocalPackages()` links it like any target dep (`discoverTargets` already includes the brand root).
88
90
 
89
91
  **Outside brands may COMMIT relative `file:` specs — the real brand did in the local era (cp229):** `../omega-omega` declared every `@omega.js/*` dep as `file:../../../omega/packages/<name>` (brand root: `file:../omega/packages/manager`), the same pattern the in-repo brands still use at their own depth. Clone the two repos side by side and a plain `npm install` links the whole tree with zero linker involvement; `linkLocalPackages()` all-skips because the specs already resolve into the monorepo. Since the first publish (0.50.0) the real brand pins the exact registry version instead; the in-repo brands stay on `file:` specs by ruling (2026-09-10).
90
92
 
91
- **A linked brand reaches CI as TARBALLS, not as a checkout** ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): a runner has nothing at the path `file:` specs name, so `omega deploy` packs every linked package into the brand's `omega_modules/` and pushes that snapshot to the brand repo, and the runner installs it with a plain `npm install`. Nothing is asked of the consumer or of the CI tool, and nothing framework-aware runs on the box. The lane, the pack step and the two hops: [deploys.md](deploys.md).
93
+ **A linked brand reaches CI as TARBALLS, not as a checkout** ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): a runner has nothing at the path `file:` specs name, so `omega deploy` packs every linked package into the brand's `omega_modules/` and pushes that snapshot to the brand repo, and the runner installs it with a plain `npm ci`. Nothing is asked of the consumer or of the CI tool, and nothing framework-aware runs on the box. The lane, the pack step and the two hops: [deploys.md](deploys.md).
92
94
 
93
95
  **`npx omega` is only ever run inside an installed target** ([#881](https://github.com/Omega-JS-Stack/omega/issues/881)): with no local bin npx fetches a stranger's package named `omega` from the registry, and the plugin's npx hook refuses that in agent shells.
94
96
 
@@ -110,8 +112,8 @@ The vendor tool derives the split from the host's package.json: anything in `dep
110
112
  | `resolveMonorepoRoot()` | env → self-location walk-up → conventional path; throws with guidance if none |
111
113
  | `findBrandRoot(dir)` / `discoverTargets(root)` | brand-root walk-up / brand root + `targets/*` list |
112
114
  | `frameworkPackagesOf(targetDir)` | `@omega.js/*` deps incl. `functions/package.json`, with dev/prod placement + owning dir |
113
- | `linkLocalPackages({ dir, monorepoRoot, logger, dryRun })` | idempotent brand-tree file:-link (spec flip + one install, transactional — a failed install restores every manifest); returns `[{ name, dir, target, action: link\|skip\|missing }]` across the tree |
114
- | `restoreRegistrySpecs({ dir, logger, dryRun, range })` | the publish-day INVERSE (`omega i live` in every framework): every `file:` spec flips to the EXACT `<linked version>` (read from the file: target — no monorepo needed, no caret: the family is lockstep, [#794](https://github.com/Omega-JS-Stack/omega/issues/794)) or the explicit `range`, used verbatim; then one registry install, same transactional restore on failure |
115
+ | `linkLocalPackages({ dir, monorepoRoot, logger, dryRun })` | idempotent brand-tree file:-link (spec flip + one install + the brand's own lockfile regenerated, transactional: a failed install restores every manifest); returns `[{ name, dir, target, action: link\|skip\|missing }]` across the tree |
116
+ | `restoreRegistrySpecs({ dir, logger, dryRun, range })` | the publish-day INVERSE (`omega i live` in every framework): every `file:` spec flips to the EXACT `<linked version>` (read from the file: target, so no monorepo needed; no caret: the family is lockstep, [#794](https://github.com/Omega-JS-Stack/omega/issues/794)) or the explicit `range`, used verbatim; then one registry install and the brand's own lockfile regenerated (alone, when nothing flipped but the lock disagrees), same transactional restore on failure |
115
117
  | `resolveLinkedMonorepo(brandRoot)` | the monorepo a brand's `@omega.js/*` deps actually RESOLVE into, or null for a registry install |
116
118
  | `startMonorepoWatch({ monorepoRoot, logger })` | lock-aware, SESSION-SCOPED spawn of the root watch (dies with the caller); `{ alreadyRunning, pid, child, ready }` — `ready` resolves with `'ready'`, `'exit'` or `'timeout'` on BOTH branches (#670, #622) |
117
119
  | `awaitRunningWatchReady({ monorepoRoot, pid, logger })` | the same verdict for a watch this process did not start: its tee'd log + the propagation marker, 120s bound |
package/docs/signing.md CHANGED
@@ -204,7 +204,7 @@ Behavior:
204
204
  - Files only — the shell is never a source — and an empty value never claims a key, so unset keys simply don't publish.
205
205
  - Auto-detects "is this a path?" — relative or absolute paths ending in `.p12`/`.pem`/`.cer`/`.p8`/`.provisionprofile`/`.crt`/`.key`/`.json` that exist on disk (target root first, brand root second) get base64-encoded.
206
206
  - Publishes to the brand's SOURCE repo (`repo.org` + `brand.id` → `<brand.id>-omega`), and REFUSES unless the checkout's own remote IS that repo: a fork, a template clone or a vendored target never arms a stranger's Actions with your certificates.
207
- - No PAT: the credential is `gh`'s auth session. A missing or signed-out `gh` fails loudly with install/auth instructions; a CI run, an empty cascade, a remote-less checkout or a repo mismatch skips loudly.
207
+ - No PAT: the credential is `gh`'s auth session. A missing or signed-out `gh` fails loudly with install/auth instructions; a CI run, an empty cascade or a remote-less checkout skips loudly, and a checkout whose `origin` is not the derived source repo refuses on the one drift line (`origin is <slug> but config derives <derived>: ...`, [#934](https://github.com/Omega-JS-Stack/omega/issues/934)).
208
208
  - `CSC_LINK` and `APPLE_API_KEY` are DERIVED, never typed ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): the env load resolved them from the signing tree, so the publish sends those files' bytes. A typed value still wins, and a key the config REQUIRES that neither the tree nor the `.env` answers STOPS the deploy (the step is `fatal`), naming both tier paths it looked in and `omega manage --service certificates`, publishing nothing.
209
209
  - Logs key NAMES only; never a value.
210
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omega.js/desktop",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "OMEGA desktop framework — build, test, and package Electron apps for macOS, Windows, and Linux",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -155,7 +155,7 @@
155
155
  "@fortawesome/fontawesome-free": "^7.3.0",
156
156
  "@inquirer/prompts": "^8.5.2",
157
157
  "@octokit/rest": "^22.0.1",
158
- "@omega.js/client": "0.51.0",
158
+ "@omega.js/client": "0.52.0",
159
159
  "@popperjs/core": "^2.11.8",
160
160
  "@sentry/electron": "^7.13.0",
161
161
  "chalk": "^5.6.2",