@omega.js/desktop 0.50.0 → 0.51.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 +1 -1
- 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 +42 -5
- 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 +144 -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 +25 -2
- 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 +121 -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 +226 -104
- 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 +264 -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 +445 -75
- package/dist/vendor/devkit/git-auth.js +73 -0
- package/dist/vendor/devkit/git-remote.js +95 -0
- package/dist/vendor/devkit/github-repo.js +290 -0
- package/dist/vendor/devkit/local.js +47 -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 +391 -0
- package/dist/vendor/devkit/preludes/index.js +120 -0
- package/dist/vendor/devkit/preludes/origin-heal.js +156 -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 +235 -50
- 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 +25 -17
- package/docs/runner.md +40 -5
- package/docs/shared/brands.md +12 -6
- package/docs/shared/breaking-changes.md +375 -21
- package/docs/shared/config.md +757 -199
- package/docs/shared/deploys.md +194 -91
- package/docs/shared/icons.md +18 -0
- package/docs/shared/local-dev.md +24 -6
- 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 +59 -33
- 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,391 @@
|
|
|
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
|
+
|
|
51
|
+
// Constants
|
|
52
|
+
const STAGING_DIR = 'omega_modules';
|
|
53
|
+
const SCOPE = '@omega.js/';
|
|
54
|
+
const FILE_SPEC = 'file:';
|
|
55
|
+
const DEP_BLOCKS = ['dependencies', 'devDependencies'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Is `target` inside `dir` (or the dir itself)?
|
|
59
|
+
* @param {string} dir - The install root.
|
|
60
|
+
* @param {string} target - An absolute path.
|
|
61
|
+
* @returns {boolean} True when the install already carries it.
|
|
62
|
+
*/
|
|
63
|
+
function isInside(dir, target) {
|
|
64
|
+
return target === dir || (target + path.sep).startsWith(dir + path.sep);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A `file:` spec for a path, always POSIX-spelled (npm reads no backslashes). */
|
|
68
|
+
function fileSpec(from, target) {
|
|
69
|
+
return FILE_SPEC + path.relative(from, target).split(path.sep).join('/');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a LINKED local package: the first node_modules entry walking up from
|
|
74
|
+
* `fromDir` (mirroring Node resolution, since npm workspaces and `omega i local`
|
|
75
|
+
* hoist to the brand/monorepo root), and only when that entry is a SYMLINK. A
|
|
76
|
+
* symlink IS the local-era shape, and the registry may have no copy of what it
|
|
77
|
+
* points at; a real directory is a registry install the remote can fetch itself.
|
|
78
|
+
* @param {string} name - Package name.
|
|
79
|
+
* @param {string} fromDir - Directory whose manifest declares it.
|
|
80
|
+
* @returns {string|null} Absolute real path of the linked checkout, or null.
|
|
81
|
+
*/
|
|
82
|
+
function resolveLinkedPackage(name, fromDir) {
|
|
83
|
+
let current = path.resolve(fromDir);
|
|
84
|
+
|
|
85
|
+
while (true) {
|
|
86
|
+
const candidate = path.join(current, 'node_modules', name);
|
|
87
|
+
const entry = jetpack.inspect(candidate);
|
|
88
|
+
|
|
89
|
+
if (entry) {
|
|
90
|
+
if (entry.type !== 'symlink') {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const real = fs.realpathSync(candidate);
|
|
94
|
+
return jetpack.exists(path.join(real, 'package.json')) ? real : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parent = path.dirname(current);
|
|
98
|
+
if (parent === current) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
current = parent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The workspace globs a manifest declares, in either supported spelling. */
|
|
106
|
+
function workspaceGlobs(pkg) {
|
|
107
|
+
const workspaces = (pkg || {}).workspaces;
|
|
108
|
+
if (!workspaces) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Array.isArray(workspaces) ? workspaces : (workspaces.packages || []);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Every manifest an install of `dir` resolves: the root's, then each workspace
|
|
117
|
+
* member's.
|
|
118
|
+
* @param {string} dir - The install root.
|
|
119
|
+
* @param {object} pkg - Its parsed package.json.
|
|
120
|
+
* @returns {Array<{ dir: string, file: string, pkg: object, root: boolean }>} The manifests.
|
|
121
|
+
*/
|
|
122
|
+
function collectManifests(dir, pkg) {
|
|
123
|
+
const manifests = [{ dir, file: path.join(dir, 'package.json'), pkg, root: true }];
|
|
124
|
+
|
|
125
|
+
for (const pattern of workspaceGlobs(pkg)) {
|
|
126
|
+
for (const match of jetpack.find(dir, { matching: `${pattern}/package.json`, files: true }) || []) {
|
|
127
|
+
const file = path.resolve(match);
|
|
128
|
+
// An install never reads a manifest out of node_modules as a member
|
|
129
|
+
if (file.split(path.sep).includes('node_modules')) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const member = jetpack.read(file, 'json');
|
|
134
|
+
if (member) {
|
|
135
|
+
manifests.push({ dir: path.dirname(file), file, pkg: member, root: false });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return manifests;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The nearest ancestor manifest declaring workspaces: the install root `dir`
|
|
145
|
+
* belonged to before it became one of its own, and therefore the home of the
|
|
146
|
+
* overrides it inherits.
|
|
147
|
+
* @param {string} dir - The install root being staged.
|
|
148
|
+
* @returns {{ dir: string, pkg: object }|null} The enclosing workspace root.
|
|
149
|
+
*/
|
|
150
|
+
function enclosingWorkspaceRoot(dir) {
|
|
151
|
+
let current = path.dirname(path.resolve(dir));
|
|
152
|
+
|
|
153
|
+
while (true) {
|
|
154
|
+
const pkg = jetpack.read(path.join(current, 'package.json'), 'json');
|
|
155
|
+
if (pkg && workspaceGlobs(pkg).length) {
|
|
156
|
+
return { dir: current, pkg };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parent = path.dirname(current);
|
|
160
|
+
if (parent === current) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
current = parent;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Every local package the install must carry, and where each one is named.
|
|
169
|
+
*
|
|
170
|
+
* Two shapes ride together: an ENTRY is one place a manifest names a package
|
|
171
|
+
* (respelled in place, or redirected by an override on the root), and a SOURCE
|
|
172
|
+
* is the package itself (packed or copied exactly once, however many entries
|
|
173
|
+
* name it).
|
|
174
|
+
*
|
|
175
|
+
* @param {object} options - Options.
|
|
176
|
+
* @param {string} options.dir - The install root being staged.
|
|
177
|
+
* @param {Array<object>} options.manifests - collectManifests() output.
|
|
178
|
+
* @returns {{ entries: Array<object>, sources: Array<{ name: string, from: string }> }}
|
|
179
|
+
* Sources in pack order: direct first, then the linked ones they pull in.
|
|
180
|
+
*/
|
|
181
|
+
function collectTargets({ dir, manifests }) {
|
|
182
|
+
const entries = [];
|
|
183
|
+
const sources = [];
|
|
184
|
+
const claimed = new Set();
|
|
185
|
+
const rootManifest = manifests[0];
|
|
186
|
+
|
|
187
|
+
const add = (entry) => {
|
|
188
|
+
entries.push(entry);
|
|
189
|
+
if (!claimed.has(entry.name)) {
|
|
190
|
+
claimed.add(entry.name);
|
|
191
|
+
sources.push({ name: entry.name, from: entry.from });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Direct: file: specs resolving OUTSIDE the dir, per manifest. A spec already
|
|
196
|
+
// pointing inside is what the install carries, so it stays as it is.
|
|
197
|
+
for (const manifest of manifests) {
|
|
198
|
+
for (const block of DEP_BLOCKS) {
|
|
199
|
+
for (const [name, spec] of Object.entries(manifest.pkg[block] || {})) {
|
|
200
|
+
if (!spec.startsWith(FILE_SPEC)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const from = path.resolve(manifest.dir, spec.slice(FILE_SPEC.length));
|
|
205
|
+
if (!isInside(dir, from)) {
|
|
206
|
+
add({ name, from, manifest, block });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Inherited: the overrides of the workspace root this dir is a member of, the
|
|
213
|
+
// ones npm would have applied to it there (the second hop's linked client).
|
|
214
|
+
const enclosing = enclosingWorkspaceRoot(dir);
|
|
215
|
+
const inherited = enclosing ? enclosing.pkg.overrides || {} : {};
|
|
216
|
+
const ownOverrides = rootManifest.pkg.overrides || {};
|
|
217
|
+
|
|
218
|
+
for (const [name, spec] of Object.entries({ ...inherited, ...ownOverrides })) {
|
|
219
|
+
if (typeof spec !== 'string' || !spec.startsWith(FILE_SPEC) || claimed.has(name)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const base = ownOverrides[name] === spec ? rootManifest.dir : enclosing.dir;
|
|
224
|
+
const from = path.resolve(base, spec.slice(FILE_SPEC.length));
|
|
225
|
+
if (!isInside(dir, from)) {
|
|
226
|
+
add({ name, from, manifest: rootManifest, block: null });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Transitive: each packed package's linked @omega.js runtime deps. A copied
|
|
231
|
+
// tarball has none to walk: whatever it needed was resolved a hop ago and
|
|
232
|
+
// rides along as an inherited override.
|
|
233
|
+
for (let index = 0; index < sources.length; index++) {
|
|
234
|
+
const source = sources[index];
|
|
235
|
+
if (source.from.endsWith('.tgz')) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const manifest = jetpack.read(path.join(source.from, 'package.json'), 'json');
|
|
240
|
+
for (const name of Object.keys((manifest || {}).dependencies || {})) {
|
|
241
|
+
if (!name.startsWith(SCOPE) || claimed.has(name)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const from = resolveLinkedPackage(name, source.from);
|
|
246
|
+
if (!from) {
|
|
247
|
+
continue; // Registry-installed (or absent): npm resolves it the normal way
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
add({ name, from, manifest: rootManifest, block: null });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { entries, sources };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Stage an install root's local packages into it.
|
|
259
|
+
*
|
|
260
|
+
* @param {object} options - Options.
|
|
261
|
+
* @param {string} options.dir - The install root: a brand root, a target, or a
|
|
262
|
+
* generated functions folder.
|
|
263
|
+
* @param {Function} [options.log] - One line per step (silent by default).
|
|
264
|
+
* @returns {Promise<{ staged: string[], restore: Function }>} The packed names,
|
|
265
|
+
* and the restore that puts the tree back exactly as it was found.
|
|
266
|
+
*/
|
|
267
|
+
async function stageLocalPackages({ dir, log = () => {} }) {
|
|
268
|
+
const root = path.resolve(dir);
|
|
269
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
270
|
+
const stagingPath = path.join(root, STAGING_DIR);
|
|
271
|
+
|
|
272
|
+
const pkg = jetpack.read(path.join(root, 'package.json'), 'json');
|
|
273
|
+
if (!pkg) {
|
|
274
|
+
return { staged: [], restore: async () => {} };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const manifests = collectManifests(root, pkg);
|
|
278
|
+
const rootManifest = manifests[0];
|
|
279
|
+
const { entries, sources } = collectTargets({ dir: root, manifests });
|
|
280
|
+
|
|
281
|
+
if (!sources.length) {
|
|
282
|
+
return { staged: [], restore: async () => {} };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Originals, restored verbatim after the deploy and after a failure
|
|
286
|
+
const touched = [...new Set(entries.map((entry) => entry.manifest))];
|
|
287
|
+
const originals = touched.map((manifest) => ({ file: manifest.file, contents: jetpack.read(manifest.file) }));
|
|
288
|
+
const originalLock = jetpack.read(lockPath);
|
|
289
|
+
|
|
290
|
+
const restore = async () => {
|
|
291
|
+
for (const original of originals) {
|
|
292
|
+
jetpack.write(original.file, original.contents);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (originalLock === undefined) {
|
|
296
|
+
jetpack.remove(lockPath);
|
|
297
|
+
} else {
|
|
298
|
+
jetpack.write(lockPath, originalLock);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
jetpack.remove(stagingPath);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
jetpack.remove(stagingPath);
|
|
306
|
+
jetpack.dir(stagingPath);
|
|
307
|
+
|
|
308
|
+
const tarballs = {};
|
|
309
|
+
|
|
310
|
+
for (const source of sources) {
|
|
311
|
+
// A `.tgz` was packed a hop ago, by the stage that produced the tree this
|
|
312
|
+
// one is staging out of: copying it keeps the artifact identical and asks
|
|
313
|
+
// nothing of sources that are a machine away.
|
|
314
|
+
if (source.from.endsWith('.tgz')) {
|
|
315
|
+
if (!jetpack.exists(source.from)) {
|
|
316
|
+
throw new Error(`Local dependency ${source.name} names a tarball that is not there: ${source.from}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const filename = path.basename(source.from);
|
|
320
|
+
jetpack.copy(source.from, path.join(stagingPath, filename));
|
|
321
|
+
tarballs[source.name] = filename;
|
|
322
|
+
log(` ${chalk.green('✓')} Copied ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!jetpack.exists(path.join(source.from, 'package.json'))) {
|
|
327
|
+
throw new Error(`Local dependency ${source.name} has no package.json at ${source.from}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
log(` ${chalk.dim('→')} Packing local package ${chalk.cyan(source.name)}...`);
|
|
331
|
+
|
|
332
|
+
const output = await powertools.execute(
|
|
333
|
+
`npm pack --pack-destination "${stagingPath}" --foreground-scripts=false`,
|
|
334
|
+
{ log: false, config: { cwd: source.from } },
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const filename = String(output).trim().split('\n').pop().trim();
|
|
338
|
+
if (!filename.endsWith('.tgz') || !jetpack.exists(path.join(stagingPath, filename))) {
|
|
339
|
+
throw new Error(`npm pack for ${source.name} did not produce a tarball (got: ${filename || 'nothing'})`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
tarballs[source.name] = filename;
|
|
343
|
+
log(` ${chalk.green('✓')} Staged ${chalk.cyan(source.name)} → ${STAGING_DIR}/${filename}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const overrides = {};
|
|
347
|
+
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
const tarball = path.join(stagingPath, tarballs[entry.name]);
|
|
350
|
+
|
|
351
|
+
if (entry.block) {
|
|
352
|
+
// Relative to the manifest that names it: a member's spec climbs out of
|
|
353
|
+
// its own folder, the root's does not.
|
|
354
|
+
entry.manifest.pkg[entry.block][entry.name] = fileSpec(entry.manifest.dir, tarball);
|
|
355
|
+
} else {
|
|
356
|
+
// A packed package still declares its own REGISTRY spec internally
|
|
357
|
+
// (`@omega.js/client: ^0.1.0`), so only an override redirects that
|
|
358
|
+
// nested resolution to the artifact beside it.
|
|
359
|
+
overrides[entry.name] = fileSpec(root, tarball);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (Object.keys(overrides).length > 0) {
|
|
364
|
+
rootManifest.pkg.overrides = Object.assign({}, rootManifest.pkg.overrides, overrides);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const manifest of touched) {
|
|
368
|
+
jetpack.write(manifest.file, `${JSON.stringify(manifest.pkg, null, 2)}\n`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// A remote install runs `npm ci`, so the uploaded lockfile must match the
|
|
372
|
+
// staged shape. `--prefix` is what makes npm treat THIS dir as the install
|
|
373
|
+
// root: run inside a workspace it would otherwise climb to the workspace
|
|
374
|
+
// root and write that one's lockfile instead.
|
|
375
|
+
log(` ${chalk.dim('→')} Regenerating package-lock.json for the staged shape...`);
|
|
376
|
+
jetpack.remove(lockPath);
|
|
377
|
+
await powertools.execute(
|
|
378
|
+
`npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix "${root}"`,
|
|
379
|
+
{ log: false, config: { cwd: root } },
|
|
380
|
+
);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// Loud and clean: the deploy stops here, and the tree goes back exactly as
|
|
383
|
+
// it was rather than sitting half-staged.
|
|
384
|
+
await restore();
|
|
385
|
+
throw new Error(`Local-package staging failed, nothing deployed and ${path.basename(root)}/ restored:\n${error.message}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { staged: sources.map((source) => source.name), restore };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
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,156 @@
|
|
|
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: `repo.org` stays the one typed value
|
|
36
|
+
* in `config/omega.json5`, and an owner GitHub reports that differs from it is
|
|
37
|
+
* stated in one line and nothing more. The prelude never writes config: which
|
|
38
|
+
* of the two is wrong (the config, or where the repo lives) is a human's call,
|
|
39
|
+
* and the manager's repo service only ensures repos UNDER `repo.org`.
|
|
40
|
+
*
|
|
41
|
+
* The WRITE is the one thing that fails loudly: a `git remote set-url` refused
|
|
42
|
+
* after a `get-url` just answered is not an external condition, it is a broken
|
|
43
|
+
* invariant, and the runner stops the boot on it.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const path = require('node:path');
|
|
47
|
+
const jetpack = require('fs-jetpack');
|
|
48
|
+
|
|
49
|
+
const { parseRemoteUrl, retargetRemoteUrl, remoteUrl, setRemoteUrl } = require('../git-remote.js');
|
|
50
|
+
const github = require('../github-repo.js');
|
|
51
|
+
|
|
52
|
+
// The one network read runs on EVERY verb boot, so a network that hangs (a
|
|
53
|
+
// captive portal, a dropped link) is bounded here and lands on the same silent
|
|
54
|
+
// `unreachable` branch an offline machine does.
|
|
55
|
+
const GITHUB_READ_TIMEOUT_MS = 10000;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {object} context - The prelude context.
|
|
59
|
+
* @param {string|null} context.brandRoot - The brand root of the invocation.
|
|
60
|
+
* @param {object} [context.config] - The composed omega config, when the caller has it.
|
|
61
|
+
* @param {function} [context.execFn] - Injectable git exec (tests).
|
|
62
|
+
* @param {function} [context.resolveRepo] - Injectable `(owner, name) => repo|null` (tests).
|
|
63
|
+
* @param {function} [context.log] - Injectable line printer (tests).
|
|
64
|
+
* @returns {{ healed: boolean, reason?: string, from?: string, to?: string, drift?: { owner: string, org: string } }}
|
|
65
|
+
*/
|
|
66
|
+
function run(context = {}) {
|
|
67
|
+
const { brandRoot, execFn } = context;
|
|
68
|
+
const log = context.log || console.log;
|
|
69
|
+
// Called through the module so the ONE network read has a seam a real CLI
|
|
70
|
+
// boot can stub too (the integration test patches it here).
|
|
71
|
+
const resolveRepo = context.resolveRepo || ((owner, name) => github.getRepo(owner, name, { timeout: GITHUB_READ_TIMEOUT_MS }));
|
|
72
|
+
|
|
73
|
+
if (!brandRoot) return { healed: false, reason: 'no-brand' };
|
|
74
|
+
|
|
75
|
+
// One stat, before anything else: most boots in this monorepo's own trees and
|
|
76
|
+
// in every fixture brand end here.
|
|
77
|
+
if (!jetpack.exists(path.join(brandRoot, '.git'))) return { healed: false, reason: 'no-git' };
|
|
78
|
+
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = remoteUrl({ dir: brandRoot, execFn });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return { healed: false, reason: 'no-origin' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const current = parseRemoteUrl(url);
|
|
87
|
+
if (!current) return { healed: false, reason: 'foreign-remote' };
|
|
88
|
+
|
|
89
|
+
const from = `${current.owner}/${current.repo}`;
|
|
90
|
+
|
|
91
|
+
// The ONE network read, on the slug the checkout carries: GitHub follows its
|
|
92
|
+
// own redirect and names where that repo lives now.
|
|
93
|
+
let resolved;
|
|
94
|
+
try {
|
|
95
|
+
resolved = resolveRepo(current.owner, current.repo);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { healed: false, reason: 'unreachable' };
|
|
98
|
+
}
|
|
99
|
+
if (!resolved) return { healed: false, reason: 'missing' };
|
|
100
|
+
|
|
101
|
+
const to = resolved.full_name;
|
|
102
|
+
let result;
|
|
103
|
+
|
|
104
|
+
if (to.toLowerCase() === from.toLowerCase()) {
|
|
105
|
+
result = { healed: false, reason: 'converged' };
|
|
106
|
+
} else {
|
|
107
|
+
setRemoteUrl({ dir: brandRoot, url: retargetRemoteUrl(url, to), execFn });
|
|
108
|
+
log(`omega: origin healed from ${from} to ${to}`);
|
|
109
|
+
result = { healed: true, from, to };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const drift = configDrift(brandRoot, context.config, to.split('/')[0]);
|
|
113
|
+
if (drift) {
|
|
114
|
+
log(`omega: origin lives under ${drift.owner} but repo.org is ${drift.org}: fix repo.org in config/omega.json5 or move the repo`);
|
|
115
|
+
result.drift = drift;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The gap between where the repo LIVES and the org the config types, or null
|
|
123
|
+
* when there is none to state. The config is loaded here when the caller has
|
|
124
|
+
* none, and an unloadable one answers "no drift": a config the boot cannot read
|
|
125
|
+
* is the VERB's failure to report, in its own words, never a prelude's. A brand
|
|
126
|
+
* declaring no `repo` block types no org, so there is nothing to disagree with.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} brandRoot - The brand root.
|
|
129
|
+
* @param {object} [config] - The composed config, when the caller has it.
|
|
130
|
+
* @param {string} owner - The owner GitHub resolved the repo under.
|
|
131
|
+
* @returns {{ owner: string, org: string }|null}
|
|
132
|
+
*/
|
|
133
|
+
function configDrift(brandRoot, config, owner) {
|
|
134
|
+
const { loadConfig, repoBlock } = require('../../config/index.js');
|
|
135
|
+
|
|
136
|
+
let block;
|
|
137
|
+
if (config) {
|
|
138
|
+
block = repoBlock(config);
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
block = repoBlock(loadConfig(brandRoot).config);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!block || block.org.toLowerCase() === owner.toLowerCase()) return null;
|
|
148
|
+
|
|
149
|
+
return { owner, org: block.org };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
name: 'origin-heal',
|
|
154
|
+
verbs: 'all',
|
|
155
|
+
run,
|
|
156
|
+
};
|