@omega.js/desktop 0.50.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.
- package/README.md +2 -2
- package/dist/assets/css/tokens/_index.scss +1 -1
- package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +4 -1
- package/dist/assets/themes/base/_includes/frontend/sections/footer.html +13 -6
- package/dist/assets/themes/base/_includes/frontend/sections/nav.html +10 -8
- package/dist/assets/themes/base/_includes/global/sections/account.html +3 -1
- package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +12 -8
- package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +10 -6
- package/dist/assets/themes/base/_includes/global/sections/page-header.html +8 -4
- package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +24 -24
- package/dist/assets/themes/base/_layouts/frontend/pages/about.html +10 -10
- package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +35 -35
- package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +4 -4
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +1 -1
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +4 -4
- package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +3 -3
- package/dist/assets/themes/base/_layouts/frontend/pages/download.html +34 -33
- package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +11 -11
- package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +1 -1
- package/dist/assets/themes/base/_layouts/frontend/pages/status.html +1 -1
- package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +9 -7
- package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +5 -3
- package/dist/assets/themes/base/_sections/about/letter/section.html +1 -1
- package/dist/assets/themes/base/_sections/about/letter/section.json5 +4 -4
- package/dist/assets/themes/base/_sections/marketing/bento/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +15 -15
- package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +1 -1
- package/dist/assets/themes/base/_sections/marketing/hero/section.html +6 -6
- package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +10 -10
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +2 -2
- package/dist/assets/themes/base/_sections/marketing/stats/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +5 -5
- package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +2 -2
- package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +10 -10
- package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +10 -10
- package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +2 -2
- package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +4 -4
- package/dist/build.js +69 -28
- package/dist/cli-run.js +20 -13
- package/dist/cli.js +12 -7
- package/dist/commands/build.js +0 -2
- package/dist/commands/deploy.js +116 -44
- package/dist/commands/finalize-release.js +5 -4
- package/dist/commands/launch.js +12 -7
- package/dist/commands/lib/deploy-precheck.js +66 -34
- package/dist/commands/lib/ensure-target.js +71 -11
- package/dist/commands/package.js +0 -1
- package/dist/commands/publish.js +11 -4
- package/dist/commands/release.js +99 -252
- package/dist/commands/runner.js +176 -42
- package/dist/commands/sign-windows.js +46 -15
- package/dist/commands/test.js +4 -3
- package/dist/commands/validate-certs.js +291 -115
- package/dist/config/page-template.html +4 -0
- package/dist/defaults/.github/workflows/build.yml +146 -64
- package/dist/defaults/AGENTS.md +3 -2
- package/dist/defaults/_.gitignore +4 -2
- package/dist/defaults/config/certs/README.md +25 -45
- package/dist/defaults/config/omega.json5 +66 -32
- package/dist/defaults/hooks/deploy/pre.js +10 -0
- package/dist/defaults/src/assets/scss/main.scss +12 -2
- package/dist/defaults/src/integrations/tray/index.js +1 -1
- package/dist/gulp/main.js +13 -25
- package/dist/gulp/tasks/audit.js +18 -6
- package/dist/gulp/tasks/build-config.js +181 -60
- package/dist/gulp/tasks/bundle.js +108 -36
- package/dist/gulp/tasks/release.js +86 -4
- package/dist/gulp/tasks/sass.js +11 -0
- package/dist/hooks/lib/notarize-tools.js +137 -0
- package/dist/hooks/notarize-artifacts.js +57 -0
- package/dist/hooks/notarize.js +59 -8
- package/dist/lib/auth-persistence.js +25 -0
- package/dist/lib/client-bridge.js +11 -8
- package/dist/lib/deep-link.js +15 -8
- package/dist/lib/protocol.js +7 -1
- package/dist/lib/restart-manager/install.js +6 -3
- package/dist/lib/sign-helpers/auto-unlock.js +65 -27
- package/dist/lib/sign-helpers/console-lock.js +34 -0
- package/dist/lib/sign-helpers/exec-with-limit.js +68 -0
- package/dist/lib/sign-helpers/resolve-icons.js +6 -4
- package/dist/lib/tray.js +7 -6
- package/dist/main.js +16 -0
- package/dist/preload.js +8 -0
- package/dist/renderer.js +16 -4
- package/dist/runner/job-started.js +104 -0
- package/dist/test/fixtures/consumer-app/config/omega.json5 +7 -0
- package/dist/test/fixtures/consumer-app/package.json +1 -1
- package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +7 -1
- package/dist/test/harness/main-entry.js +2 -1
- package/dist/test/harness/renderer-preload.js +12 -3
- package/dist/test/runners/boot.js +101 -11
- package/dist/test/runners/electron.js +1 -1
- package/dist/test/suites/boot/consumer-app-boots.test.js +54 -0
- package/dist/test/suites/build/audit.test.js +45 -6
- package/dist/test/suites/build/auth-persistence-resolve.test.js +119 -0
- package/dist/test/suites/build/auto-unlock.test.js +105 -0
- package/dist/test/suites/build/boot-runner-timeout.test.js +253 -0
- package/dist/test/suites/build/brand-scss.test.js +106 -0
- package/dist/test/suites/build/build-config.test.js +146 -36
- package/dist/test/suites/build/build-json-bake.test.js +245 -0
- package/dist/test/suites/build/build-verbs.test.js +48 -15
- package/dist/test/suites/build/build-workflow-jobs.test.js +190 -0
- package/dist/test/suites/build/cli.test.js +32 -10
- package/dist/test/suites/build/config-schema.test.js +4 -4
- package/dist/test/suites/build/console-lock.test.js +51 -0
- package/dist/test/suites/build/defaults-scaffold.test.js +71 -2
- package/dist/test/suites/build/deploy-direct.test.js +241 -0
- package/dist/test/suites/build/deploy-dispatch.test.js +287 -0
- package/dist/test/suites/build/deploy-hook.test.js +169 -0
- package/dist/test/suites/build/ensure-target.test.js +14 -2
- package/dist/test/suites/build/env-delivery.test.js +19 -10
- package/dist/test/suites/build/env-watch.test.js +18 -7
- package/dist/test/suites/build/esm-only-dependency.test.js +127 -0
- package/dist/test/suites/build/exec-with-limit.test.js +53 -0
- package/dist/test/suites/build/finalize-release.test.js +2 -2
- package/dist/test/suites/build/get-config.test.js +120 -8
- package/dist/test/suites/build/github-utils.test.js +12 -6
- package/dist/test/suites/build/license-stamp.test.js +6 -4
- package/dist/test/suites/build/manager.test.js +88 -68
- package/dist/test/suites/build/manifest-deps.test.js +116 -0
- package/dist/test/suites/build/merge-line-files.test.js +27 -4
- package/dist/test/suites/build/notarize-artifacts.test.js +135 -0
- package/dist/test/suites/build/notarize-tools.test.js +38 -0
- package/dist/test/suites/build/notarize.test.js +207 -0
- package/dist/test/suites/build/release-pipeline.test.js +38 -60
- package/dist/test/suites/build/release-skipped-upload.test.js +107 -0
- package/dist/test/suites/build/resolve-icons.test.js +35 -35
- package/dist/test/suites/build/runner-job-guard.test.js +182 -0
- package/dist/test/suites/build/runner.test.js +635 -5
- package/dist/test/suites/build/sentry.test.js +8 -3
- package/dist/test/suites/build/setup-scripts.test.js +3 -0
- package/dist/test/suites/build/sign-windows.test.js +92 -8
- package/dist/test/suites/build/test-stealth.test.js +17 -11
- package/dist/test/suites/build/url-helpers.test.js +37 -17
- package/dist/test/suites/build/validate-certs.test.js +428 -55
- package/dist/test/suites/build/validate-config.test.js +3 -3
- package/dist/test/suites/main/auth-flow.test.js +12 -0
- package/dist/test/suites/main/auth-persistence.test.js +14 -17
- package/dist/test/suites/main/auto-updater.test.js +2 -2
- package/dist/test/suites/main/boot-sequence.test.js +1 -1
- package/dist/test/suites/main/client-bridge.integration.test.js +5 -82
- package/dist/test/suites/main/client-bridge.test.js +19 -6
- package/dist/test/suites/main/deep-link.test.js +54 -0
- package/dist/test/suites/main/startup-paths-and-ua.test.js +1 -1
- package/dist/test/suites/main/url-helpers.test.js +81 -72
- package/dist/test/suites/renderer/cross-context-helpers.test.js +19 -16
- package/dist/utils/build-pipeline.js +10 -10
- package/dist/utils/github.js +12 -51
- package/dist/utils/load-env.js +66 -0
- package/dist/utils/mode-helpers.js +43 -111
- package/dist/utils/platform.js +37 -0
- package/dist/utils/runner-env.js +2 -1
- package/dist/utils/runner-job-guard.js +149 -0
- package/dist/utils/ship-keys.js +52 -0
- package/dist/utils/test-stealth.js +5 -3
- package/dist/utils/url-helpers.js +33 -17
- package/dist/vendor/config/bundle-id.js +53 -0
- package/dist/vendor/config/client-config.js +141 -0
- package/dist/vendor/config/company.js +334 -15
- package/dist/vendor/config/dev-facts.js +48 -0
- package/dist/vendor/config/env-delivery.js +231 -9
- package/dist/vendor/config/env-retired.js +137 -0
- package/dist/vendor/config/env-rules.js +22 -3
- package/dist/vendor/config/env-schema.js +234 -117
- package/dist/vendor/config/env.js +55 -26
- package/dist/vendor/config/environment.js +189 -0
- package/dist/vendor/config/hooks.js +13 -11
- package/dist/vendor/config/index.js +125 -44
- package/dist/vendor/config/load.js +366 -115
- package/dist/vendor/config/merge.js +2 -2
- package/dist/vendor/config/order.js +3 -3
- package/dist/vendor/config/platforms.js +276 -0
- package/dist/vendor/config/repo.js +249 -103
- package/dist/vendor/config/retired-keys.js +232 -27
- package/dist/vendor/config/schema.js +525 -99
- package/dist/vendor/config/site-global.js +63 -53
- package/dist/vendor/config/targets.js +187 -0
- package/dist/vendor/config/validate.js +119 -59
- package/dist/vendor/devkit/argv.js +118 -0
- package/dist/vendor/devkit/attach-log-file.js +21 -13
- package/dist/vendor/devkit/brand-tokens.js +278 -0
- package/dist/vendor/devkit/brand-version.js +425 -0
- package/dist/vendor/devkit/build-json.js +91 -0
- package/dist/vendor/devkit/bundle.js +48 -0
- package/dist/vendor/devkit/certificate-expiry.js +108 -0
- package/dist/vendor/devkit/certs.js +16 -194
- package/dist/vendor/devkit/ci-workflows.js +124 -8
- package/dist/vendor/devkit/cli-router.js +3 -3
- package/dist/vendor/devkit/defaults-engine.js +69 -7
- package/dist/vendor/devkit/deploy-follow.js +297 -0
- package/dist/vendor/devkit/deploy-precheck.js +23 -8
- package/dist/vendor/devkit/deploy-record.js +11 -27
- package/dist/vendor/devkit/deploy-snapshot.js +661 -0
- package/dist/vendor/devkit/deploy.js +505 -75
- package/dist/vendor/devkit/git-auth.js +73 -0
- package/dist/vendor/devkit/git-remote.js +172 -0
- package/dist/vendor/devkit/github-repo.js +290 -0
- package/dist/vendor/devkit/local.js +98 -3
- package/dist/vendor/devkit/lockfile.js +127 -0
- package/dist/vendor/devkit/merge-line-files.js +23 -16
- package/dist/vendor/devkit/omega-bin.js +18 -3
- package/dist/vendor/devkit/pack-local.js +388 -0
- package/dist/vendor/devkit/preludes/index.js +120 -0
- package/dist/vendor/devkit/preludes/origin-heal.js +142 -0
- package/dist/vendor/devkit/service-account.js +43 -0
- package/dist/vendor/devkit/ship-plan.js +112 -0
- package/dist/vendor/devkit/signing-env.js +180 -0
- package/dist/vendor/devkit/signing-tree.js +92 -0
- package/dist/vendor/devkit/target-seams.js +142 -0
- package/dist/vendor/devkit/target-secrets.js +231 -57
- package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
- package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
- package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
- package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
- package/dist/vendor/monitoring/env.js +20 -10
- package/dist/vendor/monitoring/main.js +1 -1
- package/dist/vendor/monitoring/preload.js +1 -1
- package/dist/vendor/monitoring/renderer.js +1 -1
- package/docs/analytics.md +1 -1
- package/docs/auto-updater.md +5 -5
- package/docs/boot-sequence.md +1 -1
- package/docs/build-system.md +15 -7
- package/docs/client-bridge.md +9 -7
- package/docs/config-schema.md +4 -4
- package/docs/css.md +8 -2
- package/docs/deep-link.md +12 -4
- package/docs/environment-detection.md +32 -24
- package/docs/hooks.md +3 -1
- package/docs/icons.md +7 -7
- package/docs/index.md +61 -23
- package/docs/installer-options.md +24 -21
- package/docs/logging.md +5 -5
- package/docs/releasing.md +29 -19
- package/docs/runner.md +51 -14
- package/docs/shared/brands.md +12 -6
- package/docs/shared/breaking-changes.md +375 -21
- package/docs/shared/config.md +764 -199
- package/docs/shared/deploys.md +217 -91
- package/docs/shared/icons.md +18 -0
- package/docs/shared/local-dev.md +28 -8
- package/docs/shared/logging.md +9 -6
- package/docs/shared/monitoring.md +27 -13
- package/docs/shared/publishing.md +3 -3
- package/docs/shared/rulings.md +2 -2
- package/docs/shared/testing.md +1 -1
- package/docs/shared/theming.md +26 -1
- package/docs/shared/translation.md +49 -7
- package/docs/shared/updates.md +1 -1
- package/docs/signing.md +60 -34
- package/docs/test-framework.md +10 -5
- package/docs/themes.md +15 -1
- package/package.json +18 -12
- package/bin/omega-desktop +0 -2
- package/dist/commands/push-secrets.js +0 -141
- package/dist/test/suites/build/deliver-certs.test.js +0 -95
- package/dist/test/suites/build/derive-signing-env.test.js +0 -122
- package/dist/test/suites/build/push-secrets.test.js +0 -226
- package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
- package/dist/utils/deliver-certs.js +0 -69
- package/dist/utils/derive-signing-env.js +0 -56
- package/dist/utils/resolve-signing-cert.js +0 -175
- package/dist/vendor/config/desktop-artifacts.js +0 -110
- package/dist/vendor/config/instances.js +0 -208
- /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
- /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
- /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pack-local: make a tree's `file:` dependencies installable somewhere else
|
|
3
|
+
* ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
|
|
4
|
+
*
|
|
5
|
+
* A remote install can only resolve what travels with it: a dependency spelled
|
|
6
|
+
* `file:../../../../packages/web` resolves to nothing on a runner or inside
|
|
7
|
+
* Cloud Build, and an install that runs `npm ci` also demands a
|
|
8
|
+
* package-lock.json matching the uploaded shape.
|
|
9
|
+
*
|
|
10
|
+
* This stages an INSTALL ROOT in place: packs each LOCAL package the install
|
|
11
|
+
* needs into `<dir>/omega_modules/*.tgz` (npm pack runs the package's own
|
|
12
|
+
* prepare, so the tarball always reflects current source), respells every
|
|
13
|
+
* manifest that names one, and regenerates the lockfile against that shape.
|
|
14
|
+
* `restore()` puts each touched manifest and the lockfile back verbatim and
|
|
15
|
+
* removes the tarball folder, and a FAILING stage restores before it throws,
|
|
16
|
+
* so a stopped deploy never leaves a half-staged tree.
|
|
17
|
+
*
|
|
18
|
+
* `dir` is any install root: a brand root with workspaces, a standalone target,
|
|
19
|
+
* or a backend's generated functions folder. What gets packed is three things,
|
|
20
|
+
* not one:
|
|
21
|
+
*
|
|
22
|
+
* 1. the outside-the-dir `file:` deps of `<dir>/package.json` AND of every
|
|
23
|
+
* workspace member manifest (npm resolves the whole workspace on any
|
|
24
|
+
* install, so one linked member breaks the install of a clean one). Each
|
|
25
|
+
* manifest's spec is respelled relative to ITSELF.
|
|
26
|
+
* 2. transitively, each packed package's own @omega.js runtime deps that
|
|
27
|
+
* resolve through a node_modules SYMLINK. @omega.js/client is a real
|
|
28
|
+
* runtime dependency of @omega.js/backend, never vendored and unpublished
|
|
29
|
+
* under the publish latch, so packing only the framework left the lock
|
|
30
|
+
* regen asking the registry for it (404, no deploy, #331). Those are
|
|
31
|
+
* redirected by an `overrides` entry on the ROOT manifest, npm honoring
|
|
32
|
+
* overrides at the install root only.
|
|
33
|
+
* 3. the overrides the ENCLOSING workspace root carries, when `dir` is itself
|
|
34
|
+
* inside a staged tree. A member installed on its own is its own install
|
|
35
|
+
* root, so an override it inherited has to travel with it.
|
|
36
|
+
*
|
|
37
|
+
* A spec that already points at a `.tgz` is COPIED, never packed again: that is
|
|
38
|
+
* the SECOND HOP, where the runner stages a target out of the snapshot it was
|
|
39
|
+
* handed and the sources are a machine away. It is also what makes the module
|
|
40
|
+
* idempotent, a staged dir spelling every local dep inside itself and therefore
|
|
41
|
+
* collecting nothing on a re-stage.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// Libraries
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const path = require('path');
|
|
47
|
+
const jetpack = require('fs-jetpack');
|
|
48
|
+
const powertools = require('node-powertools');
|
|
49
|
+
const chalk = require('chalk').default;
|
|
50
|
+
const { regenerateLockfile } = require('./lockfile.js');
|
|
51
|
+
|
|
52
|
+
// Constants
|
|
53
|
+
const STAGING_DIR = 'omega_modules';
|
|
54
|
+
const SCOPE = '@omega.js/';
|
|
55
|
+
const FILE_SPEC = 'file:';
|
|
56
|
+
const DEP_BLOCKS = ['dependencies', 'devDependencies'];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Is `target` inside `dir` (or the dir itself)?
|
|
60
|
+
* @param {string} dir - The install root.
|
|
61
|
+
* @param {string} target - An absolute path.
|
|
62
|
+
* @returns {boolean} True when the install already carries it.
|
|
63
|
+
*/
|
|
64
|
+
function isInside(dir, target) {
|
|
65
|
+
return target === dir || (target + path.sep).startsWith(dir + path.sep);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A `file:` spec for a path, always POSIX-spelled (npm reads no backslashes). */
|
|
69
|
+
function fileSpec(from, target) {
|
|
70
|
+
return FILE_SPEC + path.relative(from, target).split(path.sep).join('/');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a LINKED local package: the first node_modules entry walking up from
|
|
75
|
+
* `fromDir` (mirroring Node resolution, since npm workspaces and `omega i local`
|
|
76
|
+
* hoist to the brand/monorepo root), and only when that entry is a SYMLINK. A
|
|
77
|
+
* symlink IS the local-era shape, and the registry may have no copy of what it
|
|
78
|
+
* points at; a real directory is a registry install the remote can fetch itself.
|
|
79
|
+
* @param {string} name - Package name.
|
|
80
|
+
* @param {string} fromDir - Directory whose manifest declares it.
|
|
81
|
+
* @returns {string|null} Absolute real path of the linked checkout, or null.
|
|
82
|
+
*/
|
|
83
|
+
function resolveLinkedPackage(name, fromDir) {
|
|
84
|
+
let current = path.resolve(fromDir);
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
const candidate = path.join(current, 'node_modules', name);
|
|
88
|
+
const entry = jetpack.inspect(candidate);
|
|
89
|
+
|
|
90
|
+
if (entry) {
|
|
91
|
+
if (entry.type !== 'symlink') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const real = fs.realpathSync(candidate);
|
|
95
|
+
return jetpack.exists(path.join(real, 'package.json')) ? real : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parent = path.dirname(current);
|
|
99
|
+
if (parent === current) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The workspace globs a manifest declares, in either supported spelling. */
|
|
107
|
+
function workspaceGlobs(pkg) {
|
|
108
|
+
const workspaces = (pkg || {}).workspaces;
|
|
109
|
+
if (!workspaces) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Array.isArray(workspaces) ? workspaces : (workspaces.packages || []);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Every manifest an install of `dir` resolves: the root's, then each workspace
|
|
118
|
+
* member's.
|
|
119
|
+
* @param {string} dir - The install root.
|
|
120
|
+
* @param {object} pkg - Its parsed package.json.
|
|
121
|
+
* @returns {Array<{ dir: string, file: string, pkg: object, root: boolean }>} The manifests.
|
|
122
|
+
*/
|
|
123
|
+
function collectManifests(dir, pkg) {
|
|
124
|
+
const manifests = [{ dir, file: path.join(dir, 'package.json'), pkg, root: true }];
|
|
125
|
+
|
|
126
|
+
for (const pattern of workspaceGlobs(pkg)) {
|
|
127
|
+
for (const match of jetpack.find(dir, { matching: `${pattern}/package.json`, files: true }) || []) {
|
|
128
|
+
const file = path.resolve(match);
|
|
129
|
+
// An install never reads a manifest out of node_modules as a member
|
|
130
|
+
if (file.split(path.sep).includes('node_modules')) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const member = jetpack.read(file, 'json');
|
|
135
|
+
if (member) {
|
|
136
|
+
manifests.push({ dir: path.dirname(file), file, pkg: member, root: false });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return manifests;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The nearest ancestor manifest declaring workspaces: the install root `dir`
|
|
146
|
+
* belonged to before it became one of its own, and therefore the home of the
|
|
147
|
+
* overrides it inherits.
|
|
148
|
+
* @param {string} dir - The install root being staged.
|
|
149
|
+
* @returns {{ dir: string, pkg: object }|null} The enclosing workspace root.
|
|
150
|
+
*/
|
|
151
|
+
function enclosingWorkspaceRoot(dir) {
|
|
152
|
+
let current = path.dirname(path.resolve(dir));
|
|
153
|
+
|
|
154
|
+
while (true) {
|
|
155
|
+
const pkg = jetpack.read(path.join(current, 'package.json'), 'json');
|
|
156
|
+
if (pkg && workspaceGlobs(pkg).length) {
|
|
157
|
+
return { dir: current, pkg };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const parent = path.dirname(current);
|
|
161
|
+
if (parent === current) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
current = parent;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Every local package the install must carry, and where each one is named.
|
|
170
|
+
*
|
|
171
|
+
* Two shapes ride together: an ENTRY is one place a manifest names a package
|
|
172
|
+
* (respelled in place, or redirected by an override on the root), and a SOURCE
|
|
173
|
+
* is the package itself (packed or copied exactly once, however many entries
|
|
174
|
+
* name it).
|
|
175
|
+
*
|
|
176
|
+
* @param {object} options - Options.
|
|
177
|
+
* @param {string} options.dir - The install root being staged.
|
|
178
|
+
* @param {Array<object>} options.manifests - collectManifests() output.
|
|
179
|
+
* @returns {{ entries: Array<object>, sources: Array<{ name: string, from: string }> }}
|
|
180
|
+
* Sources in pack order: direct first, then the linked ones they pull in.
|
|
181
|
+
*/
|
|
182
|
+
function collectTargets({ dir, manifests }) {
|
|
183
|
+
const entries = [];
|
|
184
|
+
const sources = [];
|
|
185
|
+
const claimed = new Set();
|
|
186
|
+
const rootManifest = manifests[0];
|
|
187
|
+
|
|
188
|
+
const add = (entry) => {
|
|
189
|
+
entries.push(entry);
|
|
190
|
+
if (!claimed.has(entry.name)) {
|
|
191
|
+
claimed.add(entry.name);
|
|
192
|
+
sources.push({ name: entry.name, from: entry.from });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Direct: file: specs resolving OUTSIDE the dir, per manifest. A spec already
|
|
197
|
+
// pointing inside is what the install carries, so it stays as it is.
|
|
198
|
+
for (const manifest of manifests) {
|
|
199
|
+
for (const block of DEP_BLOCKS) {
|
|
200
|
+
for (const [name, spec] of Object.entries(manifest.pkg[block] || {})) {
|
|
201
|
+
if (!spec.startsWith(FILE_SPEC)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const from = path.resolve(manifest.dir, spec.slice(FILE_SPEC.length));
|
|
206
|
+
if (!isInside(dir, from)) {
|
|
207
|
+
add({ name, from, manifest, block });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Inherited: the overrides of the workspace root this dir is a member of, the
|
|
214
|
+
// ones npm would have applied to it there (the second hop's linked client).
|
|
215
|
+
const enclosing = enclosingWorkspaceRoot(dir);
|
|
216
|
+
const inherited = enclosing ? enclosing.pkg.overrides || {} : {};
|
|
217
|
+
const ownOverrides = rootManifest.pkg.overrides || {};
|
|
218
|
+
|
|
219
|
+
for (const [name, spec] of Object.entries({ ...inherited, ...ownOverrides })) {
|
|
220
|
+
if (typeof spec !== 'string' || !spec.startsWith(FILE_SPEC) || claimed.has(name)) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const base = ownOverrides[name] === spec ? rootManifest.dir : enclosing.dir;
|
|
225
|
+
const from = path.resolve(base, spec.slice(FILE_SPEC.length));
|
|
226
|
+
if (!isInside(dir, from)) {
|
|
227
|
+
add({ name, from, manifest: rootManifest, block: null });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Transitive: each packed package's linked @omega.js runtime deps. A copied
|
|
232
|
+
// tarball has none to walk: whatever it needed was resolved a hop ago and
|
|
233
|
+
// rides along as an inherited override.
|
|
234
|
+
for (let index = 0; index < sources.length; index++) {
|
|
235
|
+
const source = sources[index];
|
|
236
|
+
if (source.from.endsWith('.tgz')) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const manifest = jetpack.read(path.join(source.from, 'package.json'), 'json');
|
|
241
|
+
for (const name of Object.keys((manifest || {}).dependencies || {})) {
|
|
242
|
+
if (!name.startsWith(SCOPE) || claimed.has(name)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const from = resolveLinkedPackage(name, source.from);
|
|
247
|
+
if (!from) {
|
|
248
|
+
continue; // Registry-installed (or absent): npm resolves it the normal way
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
add({ name, from, manifest: rootManifest, block: null });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { entries, sources };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Stage an install root's local packages into it.
|
|
260
|
+
*
|
|
261
|
+
* @param {object} options - Options.
|
|
262
|
+
* @param {string} options.dir - The install root: a brand root, a target, or a
|
|
263
|
+
* generated functions folder.
|
|
264
|
+
* @param {Function} [options.log] - One line per step (silent by default).
|
|
265
|
+
* @returns {Promise<{ staged: string[], restore: Function }>} The packed names,
|
|
266
|
+
* and the restore that puts the tree back exactly as it was found.
|
|
267
|
+
*/
|
|
268
|
+
async function stageLocalPackages({ dir, log = () => {} }) {
|
|
269
|
+
const root = path.resolve(dir);
|
|
270
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
271
|
+
const stagingPath = path.join(root, STAGING_DIR);
|
|
272
|
+
|
|
273
|
+
const pkg = jetpack.read(path.join(root, 'package.json'), 'json');
|
|
274
|
+
if (!pkg) {
|
|
275
|
+
return { staged: [], restore: async () => {} };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const manifests = collectManifests(root, pkg);
|
|
279
|
+
const rootManifest = manifests[0];
|
|
280
|
+
const { entries, sources } = collectTargets({ dir: root, manifests });
|
|
281
|
+
|
|
282
|
+
if (!sources.length) {
|
|
283
|
+
return { staged: [], restore: async () => {} };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Originals, restored verbatim after the deploy and after a failure
|
|
287
|
+
const touched = [...new Set(entries.map((entry) => entry.manifest))];
|
|
288
|
+
const originals = touched.map((manifest) => ({ file: manifest.file, contents: jetpack.read(manifest.file) }));
|
|
289
|
+
const originalLock = jetpack.read(lockPath);
|
|
290
|
+
|
|
291
|
+
const restore = async () => {
|
|
292
|
+
for (const original of originals) {
|
|
293
|
+
jetpack.write(original.file, original.contents);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (originalLock === undefined) {
|
|
297
|
+
jetpack.remove(lockPath);
|
|
298
|
+
} else {
|
|
299
|
+
jetpack.write(lockPath, originalLock);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
jetpack.remove(stagingPath);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
jetpack.remove(stagingPath);
|
|
307
|
+
jetpack.dir(stagingPath);
|
|
308
|
+
|
|
309
|
+
const tarballs = {};
|
|
310
|
+
|
|
311
|
+
for (const source of sources) {
|
|
312
|
+
// A `.tgz` was packed a hop ago, by the stage that produced the tree this
|
|
313
|
+
// one is staging out of: copying it keeps the artifact identical and asks
|
|
314
|
+
// nothing of sources that are a machine away.
|
|
315
|
+
if (source.from.endsWith('.tgz')) {
|
|
316
|
+
if (!jetpack.exists(source.from)) {
|
|
317
|
+
throw new Error(`Local dependency ${source.name} names a tarball that is not there: ${source.from}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const filename = path.basename(source.from);
|
|
321
|
+
jetpack.copy(source.from, path.join(stagingPath, filename));
|
|
322
|
+
tarballs[source.name] = filename;
|
|
323
|
+
log(` ${chalk.green('✓')} Copied ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!jetpack.exists(path.join(source.from, 'package.json'))) {
|
|
328
|
+
throw new Error(`Local dependency ${source.name} has no package.json at ${source.from}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
log(` ${chalk.dim('→')} Packing local package ${chalk.cyan(source.name)}...`);
|
|
332
|
+
|
|
333
|
+
const output = await powertools.execute(
|
|
334
|
+
`npm pack --pack-destination "${stagingPath}" --foreground-scripts=false`,
|
|
335
|
+
{ log: false, config: { cwd: source.from } },
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const filename = String(output).trim().split('\n').pop().trim();
|
|
339
|
+
if (!filename.endsWith('.tgz') || !jetpack.exists(path.join(stagingPath, filename))) {
|
|
340
|
+
throw new Error(`npm pack for ${source.name} did not produce a tarball (got: ${filename || 'nothing'})`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
tarballs[source.name] = filename;
|
|
344
|
+
log(` ${chalk.green('✓')} Staged ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const overrides = {};
|
|
348
|
+
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const tarball = path.join(stagingPath, tarballs[entry.name]);
|
|
351
|
+
|
|
352
|
+
if (entry.block) {
|
|
353
|
+
// Relative to the manifest that names it: a member's spec climbs out of
|
|
354
|
+
// its own folder, the root's does not.
|
|
355
|
+
entry.manifest.pkg[entry.block][entry.name] = fileSpec(entry.manifest.dir, tarball);
|
|
356
|
+
} else {
|
|
357
|
+
// A packed package still declares its own REGISTRY spec internally
|
|
358
|
+
// (`@omega.js/client: ^0.1.0`), so only an override redirects that
|
|
359
|
+
// nested resolution to the artifact beside it.
|
|
360
|
+
overrides[entry.name] = fileSpec(root, tarball);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (Object.keys(overrides).length > 0) {
|
|
365
|
+
rootManifest.pkg.overrides = Object.assign({}, rootManifest.pkg.overrides, overrides);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const manifest of touched) {
|
|
369
|
+
jetpack.write(manifest.file, `${JSON.stringify(manifest.pkg, null, 2)}\n`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// A remote install runs `npm ci`, so the uploaded lockfile must match the
|
|
373
|
+
// staged shape, regenerated from scratch (the one helper keeps it THIS
|
|
374
|
+
// dir's lock, never an enclosing workspace root's).
|
|
375
|
+
log(` ${chalk.dim('→')} Regenerating package-lock.json for the staged shape...`);
|
|
376
|
+
jetpack.remove(lockPath);
|
|
377
|
+
await regenerateLockfile({ root });
|
|
378
|
+
} catch (error) {
|
|
379
|
+
// Loud and clean: the deploy stops here, and the tree goes back exactly as
|
|
380
|
+
// it was rather than sitting half-staged.
|
|
381
|
+
await restore();
|
|
382
|
+
throw new Error(`Local-package staging failed, nothing deployed and ${path.basename(root)}/ restored:\n${error.message}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { staged: sources.map((source) => source.name), restore };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = { stageLocalPackages, STAGING_DIR };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runPreludes: the BOOT PRELUDE list every verb passes through
|
|
3
|
+
* ([#890](https://github.com/Omega-JS-Stack/omega/issues/890)).
|
|
4
|
+
*
|
|
5
|
+
* One seam already sits above every verb on all five CLIs: the `freshnessBoot`
|
|
6
|
+
* call that heals a stale locally-linked dist before anything runs. Anything
|
|
7
|
+
* else the whole CLI surface must settle first had nowhere to live, so it lived
|
|
8
|
+
* nowhere: a brand clone pointing at a redirected GitHub repo was checked by no
|
|
9
|
+
* verb at all. This is that place: the preludes run right after the freshness
|
|
10
|
+
* boot, in list order, before the verb.
|
|
11
|
+
*
|
|
12
|
+
* Each prelude is ONE file with one concern, exporting `{ name, verbs, run }`,
|
|
13
|
+
* and declares what it targets: `'all'`, one verb name, or a list of them. The
|
|
14
|
+
* runner filters by the verb about to run, as TYPED, before a framework's
|
|
15
|
+
* alias table resolves it, since the tables are the frameworks' and this runs
|
|
16
|
+
* above them, so an `'all'` prelude is the one that covers `-v` and a bare
|
|
17
|
+
* `omega` too.
|
|
18
|
+
*
|
|
19
|
+
* A prelude is CHEAP and NON-INTERACTIVE: local reads, at most one network
|
|
20
|
+
* call, never a prompt. Anything heavier is a manage op in its own service,
|
|
21
|
+
* where a walk reports it. A prelude that stops the run THROWS with one line of
|
|
22
|
+
* why, which this runner prints before exiting 1, the same presentation the
|
|
23
|
+
* freshness boot's loud stops use, so a refused boot reads the same whichever
|
|
24
|
+
* step refused it.
|
|
25
|
+
*
|
|
26
|
+
* Nothing here caches per process: a dispatcher hop that re-enters a `run()`
|
|
27
|
+
* runs the preludes again, and every prelude being idempotent, the second pass
|
|
28
|
+
* is a local read that finds its work already done.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// The list. Order is the boot order.
|
|
32
|
+
const PRELUDES = [
|
|
33
|
+
require('./origin-heal.js'),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The verb a prelude list filters on: the first positional of the invocation.
|
|
38
|
+
* A flag there (`--version`) is no verb: the CLI is about to run its default
|
|
39
|
+
* command, which only an `'all'` prelude covers.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} [verb] - The raw first argument.
|
|
42
|
+
* @returns {string|null} The verb, or null when the invocation names none.
|
|
43
|
+
*/
|
|
44
|
+
function normalizeVerb(verb) {
|
|
45
|
+
return typeof verb === 'string' && verb.length > 0 && !verb.startsWith('-') ? verb : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Does this prelude target the verb about to run?
|
|
50
|
+
*
|
|
51
|
+
* @param {{ name: string, verbs: string|string[] }} prelude - The prelude.
|
|
52
|
+
* @param {string|null} verb - The normalized verb.
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
* @throws {Error} When the prelude declares no usable `verbs`. A list entry
|
|
55
|
+
* nobody can target is a programmer error, and a silent skip would make it
|
|
56
|
+
* look like a prelude that simply never applies.
|
|
57
|
+
*/
|
|
58
|
+
function targetsVerb(prelude, verb) {
|
|
59
|
+
const { verbs } = prelude;
|
|
60
|
+
if (verbs === 'all') return true;
|
|
61
|
+
|
|
62
|
+
const list = typeof verbs === 'string' ? [verbs] : verbs;
|
|
63
|
+
if (!Array.isArray(list) || list.length === 0 || list.some((entry) => typeof entry !== 'string')) {
|
|
64
|
+
throw new Error(`Boot prelude "${prelude.name}" declares no verbs: \`verbs\` is 'all', one verb name, or a list of verb names.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return verb !== null && list.includes(verb);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run every prelude the verb selects, in list order.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} [options]
|
|
74
|
+
* @param {string} [options.verb] - The verb about to run (the CLI's first positional).
|
|
75
|
+
* @param {string} [options.targetDir] - The invocation's directory (default: cwd).
|
|
76
|
+
* @param {string|null} [options.brandRoot] - The brand root; resolved from `targetDir` when absent.
|
|
77
|
+
* @param {object} [options.config] - The composed omega config, when the caller already has it.
|
|
78
|
+
* @param {Array<{ name: string, verbs: string|string[], run: function }>} [options.preludes] - The list (default: PRELUDES).
|
|
79
|
+
* @returns {{ ran: string[] }} The preludes that ran, in the order they ran.
|
|
80
|
+
*/
|
|
81
|
+
function runPreludes(options = {}) {
|
|
82
|
+
const preludes = options.preludes || PRELUDES;
|
|
83
|
+
const verb = normalizeVerb(options.verb);
|
|
84
|
+
const targetDir = options.targetDir || process.cwd();
|
|
85
|
+
const brandRoot = options.brandRoot === undefined ? resolveBrandRoot(targetDir) : options.brandRoot;
|
|
86
|
+
|
|
87
|
+
// Filtering first, and over the WHOLE list: a malformed `verbs` is raised
|
|
88
|
+
// before any prelude has run, not halfway through the boot.
|
|
89
|
+
const selected = preludes.filter((prelude) => targetsVerb(prelude, verb));
|
|
90
|
+
const ran = [];
|
|
91
|
+
|
|
92
|
+
for (const prelude of selected) {
|
|
93
|
+
try {
|
|
94
|
+
prelude.run({ verb, brandRoot, targetDir, config: options.config });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`omega: ${error.message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
ran.push(prelude.name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ran };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The brand root of an invocation, or null outside a brand (this monorepo's own
|
|
107
|
+
* packages, a fresh directory). Required lazily: `@omega.js/config` is loaded by
|
|
108
|
+
* most verbs anyway, and a boot that resolves nothing should not pay for it.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} targetDir - The invocation's directory.
|
|
111
|
+
* @returns {string|null}
|
|
112
|
+
*/
|
|
113
|
+
function resolveBrandRoot(targetDir) {
|
|
114
|
+
return require('../../config/index.js').resolveBrandRoot(targetDir);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
runPreludes,
|
|
119
|
+
PRELUDES,
|
|
120
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The origin heal: a brand whose `origin` names a repo GitHub has since
|
|
3
|
+
* redirected is pointed at the address GitHub answers with
|
|
4
|
+
* ([#890](https://github.com/Omega-JS-Stack/omega/issues/890)).
|
|
5
|
+
*
|
|
6
|
+
* A transfer or a rename leaves every clone pointing at the old address. Git
|
|
7
|
+
* follows GitHub's redirect and keeps working, so nothing ever complains, and
|
|
8
|
+
* the stale remote surfaces later as a push to a repo the brand no longer is
|
|
9
|
+
* (seen on the `omega-omega` transfer into `Omega-JS-Stack`, 2026-09-12).
|
|
10
|
+
*
|
|
11
|
+
* The REDIRECT is the authority, not the config: `gh api repos/<owner>/<name>`
|
|
12
|
+
* on the slug the checkout already carries follows GitHub's own 301 and answers
|
|
13
|
+
* the CURRENT `full_name`, so a brand is healed onto wherever its repo actually
|
|
14
|
+
* moved, config or no config. That costs one GitHub read per boot, which is the
|
|
15
|
+
* price of healing from the truth instead of from a typed value.
|
|
16
|
+
*
|
|
17
|
+
* The decision, in the order it is cheapest to answer:
|
|
18
|
+
* - no brand root: nothing to compare, and not one read made;
|
|
19
|
+
* - no `.git` AT the brand root: a no-op, never a walk up, and answered by one
|
|
20
|
+
* stat before anything else is read. Git resolves a repo by walking up, and
|
|
21
|
+
* a brand nested in another repo (a fixture brand inside this monorepo, a
|
|
22
|
+
* target checked out under someone else's tree) would otherwise have the
|
|
23
|
+
* ENCLOSING repo's remote read and rewritten;
|
|
24
|
+
* - no `origin`, or an origin that is not a GitHub remote: a no-op. A
|
|
25
|
+
* checkout nobody has pushed yet, and a brand hosted elsewhere, are
|
|
26
|
+
* expected external conditions, not failures;
|
|
27
|
+
* - a repo GitHub does not have (a 404: it is gone), or a GitHub it cannot
|
|
28
|
+
* reach (offline, no `gh` auth): a silent no-op. Rewriting a remote on an
|
|
29
|
+
* answer nobody got would take a working checkout and break it;
|
|
30
|
+
* - GitHub answering the slug the remote already names (compared
|
|
31
|
+
* case-insensitively, GitHub's own comparison): converged, silent;
|
|
32
|
+
* - anything else: the remote is rewritten to the answered `full_name`,
|
|
33
|
+
* keeping its own url form, and the heal is stated in one line.
|
|
34
|
+
*
|
|
35
|
+
* Then DRIFT, on whatever the answer was: the WHOLE slug GitHub answers against
|
|
36
|
+
* the source repo the config derives (`@omega.js/config`'s `repoDrift`, so a
|
|
37
|
+
* rename drifts as surely as a transfer,
|
|
38
|
+
* [#934](https://github.com/Omega-JS-Stack/omega/issues/934)), stated in one
|
|
39
|
+
* line and nothing more. Never fatal: this runs before every verb, and a fatal
|
|
40
|
+
* boot would lock out the verbs that fix the mismatch. The verbs that ACT on
|
|
41
|
+
* the derived repo (the manage walk, the deploy) refuse on the same line
|
|
42
|
+
* instead. The prelude never writes config: which of the two is wrong (the
|
|
43
|
+
* config, or where the repo lives) is a human's call.
|
|
44
|
+
*
|
|
45
|
+
* The WRITE is the one thing that fails loudly: a `git remote set-url` refused
|
|
46
|
+
* after a `get-url` just answered is not an external condition, it is a broken
|
|
47
|
+
* invariant, and the runner stops the boot on it.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
const { readOrigin, retargetRemoteUrl, setRemoteUrl } = require('../git-remote.js');
|
|
51
|
+
const github = require('../github-repo.js');
|
|
52
|
+
|
|
53
|
+
// The one network read runs on EVERY verb boot, so a network that hangs (a
|
|
54
|
+
// captive portal, a dropped link) is bounded here and lands on the same silent
|
|
55
|
+
// `unreachable` branch an offline machine does.
|
|
56
|
+
const GITHUB_READ_TIMEOUT_MS = 10000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {object} context - The prelude context.
|
|
60
|
+
* @param {string|null} context.brandRoot - The brand root of the invocation.
|
|
61
|
+
* @param {object} [context.config] - The composed omega config, when the caller has it.
|
|
62
|
+
* @param {function} [context.execFn] - Injectable git exec (tests).
|
|
63
|
+
* @param {function} [context.resolveRepo] - Injectable `(owner, name) => repo|null` (tests).
|
|
64
|
+
* @param {function} [context.log] - Injectable line printer (tests).
|
|
65
|
+
* @returns {{ healed: boolean, reason?: string, from?: string, to?: string, drift?: { origin: string, derived: string } }}
|
|
66
|
+
*/
|
|
67
|
+
function run(context = {}) {
|
|
68
|
+
const { brandRoot, execFn } = context;
|
|
69
|
+
const log = context.log || console.log;
|
|
70
|
+
// Called through the module so the ONE network read has a seam a real CLI
|
|
71
|
+
// boot can stub too (the integration test patches it here).
|
|
72
|
+
const resolveRepo = context.resolveRepo || ((owner, name) => github.getRepo(owner, name, { timeout: GITHUB_READ_TIMEOUT_MS }));
|
|
73
|
+
|
|
74
|
+
if (!brandRoot) return { healed: false, reason: 'no-brand' };
|
|
75
|
+
|
|
76
|
+
// One stat before anything else (most boots in this monorepo's own trees and
|
|
77
|
+
// in every fixture brand end there), then the remote itself.
|
|
78
|
+
const current = readOrigin({ dir: brandRoot, execFn });
|
|
79
|
+
if (!current.slug) return { healed: false, reason: current.reason };
|
|
80
|
+
|
|
81
|
+
const { url, slug: from } = current;
|
|
82
|
+
|
|
83
|
+
// The ONE network read, on the slug the checkout carries: GitHub follows its
|
|
84
|
+
// own redirect and names where that repo lives now.
|
|
85
|
+
let resolved;
|
|
86
|
+
try {
|
|
87
|
+
resolved = resolveRepo(current.owner, current.repo);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { healed: false, reason: 'unreachable' };
|
|
90
|
+
}
|
|
91
|
+
if (!resolved) return { healed: false, reason: 'missing' };
|
|
92
|
+
|
|
93
|
+
const to = resolved.full_name;
|
|
94
|
+
let result;
|
|
95
|
+
|
|
96
|
+
if (to.toLowerCase() === from.toLowerCase()) {
|
|
97
|
+
result = { healed: false, reason: 'converged' };
|
|
98
|
+
} else {
|
|
99
|
+
setRemoteUrl({ dir: brandRoot, url: retargetRemoteUrl(url, to), execFn });
|
|
100
|
+
log(`omega: origin healed from ${from} to ${to}`);
|
|
101
|
+
result = { healed: true, from, to };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const config = composedConfig(brandRoot, context.config);
|
|
105
|
+
const { repoDrift, sourceRepo } = require('../../config/index.js');
|
|
106
|
+
const drift = config ? repoDrift(to, config) : null;
|
|
107
|
+
|
|
108
|
+
if (drift) {
|
|
109
|
+
log(`omega: ${drift}`);
|
|
110
|
+
result.drift = { origin: to, derived: sourceRepo(config).slug };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The composed config the drift is read against: the caller's when it has one,
|
|
118
|
+
* else loaded here, and an unloadable one answers null ("no drift"): a config
|
|
119
|
+
* the boot cannot read is the VERB's failure to report, in its own words, never
|
|
120
|
+
* a prelude's.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} brandRoot - The brand root.
|
|
123
|
+
* @param {object} [config] - The composed config, when the caller has it.
|
|
124
|
+
* @returns {object|null}
|
|
125
|
+
*/
|
|
126
|
+
function composedConfig(brandRoot, config) {
|
|
127
|
+
if (config) return config;
|
|
128
|
+
|
|
129
|
+
const { loadConfig } = require('../../config/index.js');
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
return loadConfig(brandRoot).config;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
name: 'origin-heal',
|
|
140
|
+
verbs: 'all',
|
|
141
|
+
run,
|
|
142
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Firebase service-account key's ONE lookup
|
|
3
|
+
* ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)), and the ONE
|
|
4
|
+
* spelling of every path under the brand's secrets folder
|
|
5
|
+
* ([#897](https://github.com/Omega-JS-Stack/omega/issues/897)).
|
|
6
|
+
*
|
|
7
|
+
* The key is a FILE the cloud manage service mints, never a `.env` value, so
|
|
8
|
+
* everything that needs it (the backend stage, the deploy-roles read, the
|
|
9
|
+
* secrets publish that sends it to a runner) resolves the same two candidates
|
|
10
|
+
* in the same order: the target root's own, then the brand's secrets folder.
|
|
11
|
+
* It lived in the backend's stage helper while the backend was its only reader;
|
|
12
|
+
* the shared secrets publisher reads it now, and devkit never requires a
|
|
13
|
+
* framework, so the lookup lives here and the backend imports it.
|
|
14
|
+
*
|
|
15
|
+
* The Google OAuth client the authentication service writes obeys the same
|
|
16
|
+
* folder rule, so it is named here too: a rename of the folder is one edit,
|
|
17
|
+
* not a hunt through every manager and backend reader.
|
|
18
|
+
*/
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const jetpack = require('fs-jetpack');
|
|
21
|
+
const { findBrandRoot } = require('../config/index.js');
|
|
22
|
+
|
|
23
|
+
// The brand-level home of every minted credential, and the two files in it.
|
|
24
|
+
const SECRETS_DIR = path.join('.omega', 'secrets');
|
|
25
|
+
const SERVICE_ACCOUNT_REL = path.join(SECRETS_DIR, 'service-account.json');
|
|
26
|
+
const GOOGLE_OAUTH_REL = path.join(SECRETS_DIR, 'google-oauth.json');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The service-account key a target deploys with, or null when none is minted.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} projectDir - The target root.
|
|
32
|
+
* @returns {string|null} Absolute path to the key file.
|
|
33
|
+
*/
|
|
34
|
+
function resolveServiceAccountPath(projectDir) {
|
|
35
|
+
const brandRoot = findBrandRoot(projectDir);
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
path.join(projectDir, 'service-account.json'),
|
|
39
|
+
brandRoot ? path.join(brandRoot, SERVICE_ACCOUNT_REL) : null,
|
|
40
|
+
].filter(Boolean).find((candidate) => jetpack.exists(candidate)) || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { resolveServiceAccountPath, SECRETS_DIR, SERVICE_ACCOUNT_REL, GOOGLE_OAUTH_REL };
|