@omega.js/desktop 0.53.0 → 0.54.1

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.
Files changed (161) hide show
  1. package/README.md +38 -38
  2. package/dist/cli-run.js +4 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/cdp/client.js +1 -1
  5. package/dist/commands/cdp.js +1 -1
  6. package/dist/commands/clean.js +2 -3
  7. package/dist/commands/dev.js +25 -0
  8. package/dist/commands/lib/ensure-target.js +12 -17
  9. package/dist/commands/lib/migrate.js +17 -0
  10. package/dist/commands/logs.js +1 -1
  11. package/dist/commands/release.js +1 -1
  12. package/dist/commands/test.js +4 -4
  13. package/dist/commands/update.js +5 -4
  14. package/dist/defaults/.github/workflows/build.yml +18 -18
  15. package/dist/defaults/_.gitignore +0 -2
  16. package/dist/defaults/_mas/README.md +3 -3
  17. package/dist/defaults/config/certs/README.md +1 -1
  18. package/dist/defaults/config/omega.json5 +36 -36
  19. package/dist/defaults/docs/README.md +3 -3
  20. package/dist/defaults/gulpfile.js +1 -1
  21. package/dist/defaults/hooks/build/post.js +1 -1
  22. package/dist/defaults/hooks/build/pre.js +1 -1
  23. package/dist/defaults/hooks/notarize/post.js +2 -2
  24. package/dist/defaults/hooks/release/post.js +1 -1
  25. package/dist/defaults/hooks/release/pre.js +1 -1
  26. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  27. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  28. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  29. package/dist/defaults/src/integrations/context-menu/index.js +11 -11
  30. package/dist/defaults/src/integrations/menu/index.js +5 -5
  31. package/dist/defaults/src/integrations/tray/index.js +9 -9
  32. package/dist/defaults/src/main.js +2 -2
  33. package/dist/defaults/src/preload.js +1 -1
  34. package/dist/defaults/test/README.md +3 -3
  35. package/dist/defaults/test/_init.js +1 -1
  36. package/dist/gulp/tasks/audit.js +5 -8
  37. package/dist/lib/restart-manager/index.js +1 -1
  38. package/dist/lib/restart-manager/install.js +1 -1
  39. package/dist/lib/restart-manager/protocol.js +1 -1
  40. package/dist/main.js +4 -3
  41. package/dist/preload.js +1 -1
  42. package/dist/test/suites/build/audit.test.js +20 -7
  43. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  44. package/dist/test/suites/build/cli.test.js +28 -0
  45. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  46. package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
  47. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  48. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  49. package/dist/test/suites/build/deploy-hook.test.js +4 -2
  50. package/dist/test/suites/build/dev-verb.test.js +67 -0
  51. package/dist/test/suites/build/ensure-target.test.js +11 -3
  52. package/dist/test/suites/build/merge-line-files.test.js +6 -6
  53. package/dist/test/suites/build/migrate.test.js +29 -0
  54. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  55. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  56. package/dist/test/suites/build/runner.test.js +9 -8
  57. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  58. package/dist/test/suites/build/validate-config.test.js +13 -2
  59. package/dist/test/suites/build/verb-logs.test.js +20 -0
  60. package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
  61. package/dist/utils/build-pipeline.js +4 -4
  62. package/dist/utils/runner-env.js +13 -28
  63. package/dist/vendor/config/company.js +46 -14
  64. package/dist/vendor/config/defaults.js +30 -7
  65. package/dist/vendor/config/edit.js +25 -3
  66. package/dist/vendor/config/env-delivery.js +1 -1
  67. package/dist/vendor/config/env-schema.js +3 -6
  68. package/dist/vendor/config/env.js +34 -22
  69. package/dist/vendor/config/index.js +13 -17
  70. package/dist/vendor/config/load.js +15 -7
  71. package/dist/vendor/config/repo.js +10 -27
  72. package/dist/vendor/config/schema-client.js +64 -0
  73. package/dist/vendor/config/schema-cloud.js +38 -0
  74. package/dist/vendor/config/schema-manager.js +118 -0
  75. package/dist/vendor/config/schema-overrides.js +68 -0
  76. package/dist/vendor/config/schema.js +99 -152
  77. package/dist/vendor/config/validate.js +97 -77
  78. package/dist/vendor/devkit/agents-md.js +233 -0
  79. package/dist/vendor/devkit/attach-log-file.js +15 -1
  80. package/dist/vendor/devkit/ci-workflows.js +30 -30
  81. package/dist/vendor/devkit/cli-router.js +13 -7
  82. package/dist/vendor/devkit/defaults-engine.js +9 -43
  83. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  84. package/dist/vendor/devkit/env-lines.js +183 -0
  85. package/dist/vendor/devkit/local.js +62 -10
  86. package/dist/vendor/devkit/lockfile.js +32 -13
  87. package/dist/vendor/devkit/logger.js +7 -2
  88. package/dist/vendor/devkit/merge-line-files.js +219 -176
  89. package/dist/vendor/devkit/omega-bin.js +208 -111
  90. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  91. package/dist/vendor/devkit/preludes/index.js +1 -0
  92. package/dist/vendor/devkit/target-picker.js +45 -0
  93. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  94. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  95. package/dist/vendor/devkit/update.js +15 -15
  96. package/dist/vendor/devkit/verb-scripts.js +40 -0
  97. package/dist/vendor/devkit/verbs.js +170 -0
  98. package/package.json +18 -24
  99. package/dist/commands/install.js +0 -37
  100. package/dist/defaults/AGENTS.md +0 -119
  101. package/dist/defaults/CLAUDE.md +0 -1
  102. package/dist/vendor/config/env-retired.js +0 -137
  103. package/dist/vendor/config/retired-keys.js +0 -635
  104. package/docs/analytics.md +0 -140
  105. package/docs/app-state.md +0 -92
  106. package/docs/audit.md +0 -69
  107. package/docs/auth.md +0 -284
  108. package/docs/auto-updater.md +0 -243
  109. package/docs/boot-sequence.md +0 -44
  110. package/docs/build-system.md +0 -169
  111. package/docs/cdp-debugging.md +0 -169
  112. package/docs/common-mistakes.md +0 -21
  113. package/docs/config-schema.md +0 -120
  114. package/docs/context-menu.md +0 -112
  115. package/docs/context.md +0 -81
  116. package/docs/css.md +0 -84
  117. package/docs/deep-link.md +0 -186
  118. package/docs/environment-detection.md +0 -112
  119. package/docs/fontawesome.md +0 -109
  120. package/docs/hooks.md +0 -89
  121. package/docs/icons.md +0 -79
  122. package/docs/index.md +0 -328
  123. package/docs/installer-options.md +0 -165
  124. package/docs/ipc.md +0 -61
  125. package/docs/lib-modules.md +0 -53
  126. package/docs/logging.md +0 -227
  127. package/docs/menu.md +0 -160
  128. package/docs/releasing.md +0 -239
  129. package/docs/remote-config.md +0 -118
  130. package/docs/remote-scripts.md +0 -144
  131. package/docs/restart-manager.md +0 -144
  132. package/docs/runner.md +0 -290
  133. package/docs/sentry.md +0 -97
  134. package/docs/shared/agent-docs.md +0 -89
  135. package/docs/shared/analytics.md +0 -612
  136. package/docs/shared/brands.md +0 -57
  137. package/docs/shared/breaking-changes.md +0 -917
  138. package/docs/shared/config.md +0 -1948
  139. package/docs/shared/deploys.md +0 -341
  140. package/docs/shared/icons.md +0 -219
  141. package/docs/shared/local-dev.md +0 -167
  142. package/docs/shared/logging.md +0 -205
  143. package/docs/shared/monitoring.md +0 -167
  144. package/docs/shared/publishing.md +0 -187
  145. package/docs/shared/rulings.md +0 -34
  146. package/docs/shared/testing.md +0 -147
  147. package/docs/shared/theming.md +0 -629
  148. package/docs/shared/translation.md +0 -342
  149. package/docs/shared/updates.md +0 -61
  150. package/docs/signing.md +0 -293
  151. package/docs/startup.md +0 -142
  152. package/docs/storage.md +0 -59
  153. package/docs/templating.md +0 -101
  154. package/docs/test-boot-layer.md +0 -157
  155. package/docs/test-framework.md +0 -362
  156. package/docs/themes.md +0 -149
  157. package/docs/tooltips.md +0 -99
  158. package/docs/tray.md +0 -164
  159. package/docs/usage.md +0 -58
  160. package/docs/verts.md +0 -62
  161. package/docs/windows.md +0 -149
@@ -1,38 +1,19 @@
1
1
  /**
2
- * omega-bin — the context-aware dispatcher behind every framework's `omega`/`omg` bin.
3
- *
4
- * Problem: every OMEGA framework ships the same bin names (`omega`, `omg`, `mgr`).
5
- * In a brand monorepo with several targets, npm hoists all of them to the brand root
6
- * and only ONE framework's file wins the node_modules/.bin link — so the bin that
7
- * actually runs is arbitrary. This module makes any winner correct: it resolves
8
- * the context that owns the CALLER'S cwd — nearest first, walking up — and runs
9
- * THAT context's CLI:
10
- *
11
- * - an OMEGA PACKAGE'S OWN ROOT (package.json `name` is `@omega.js/*`) →
12
- * that package's own CLI, or a refusal when it ships none. Checked FIRST,
13
- * ahead of the dependency rule: a framework devDepends on other frameworks
14
- * (extension and desktop on @omega.js/web for vendorAssets, the manager on
15
- * @omega.js/backend), and reading those as a target made `omega test` in
16
- * packages/extension run WEB's CLI, which scaffolded a web target into the
17
- * package ([#757](https://github.com/Omega-JS-Stack/omega/issues/757))
18
- * - a TARGET (nearest package.json declaring a framework, including the
19
- * backend's functions/ layout) → that framework's CLI
20
- * - a BRAND ROOT (config/omega.json5 with no framework declared nearer)
21
- * → @omega.js/manager's CLI, so `omega test` at a brand root fans out
22
- * over the brand's targets instead of guessing one framework
23
- *
24
- * Contract: every dispatch target exposes `exports['./cli']` → a module with
25
- * `run()` that parses process.argv itself. Framework bin shims call
26
- * `run({ hostName, hostRun })` — hostRun executes the host's own CLI via a
27
- * RELATIVE require (vendor-safe), so the host never resolves itself by name.
28
- *
29
- * This module stays stdlib-only (fs/path/module): it is vendored into every
30
- * framework dist and must never assume another package is installed.
2
+ * omega-bin: the context-aware dispatcher behind every framework's `omega`/`omg`/`mgr`
3
+ * bin. npm links one arbitrary winner of those bins, so this resolves the ROOT that owns
4
+ * the cwd (a brand root, a standalone target, the monorepo root) and runs THAT root's CLI.
5
+ * Below a root, a verb runs in place only inside a target of one of its owner frameworks;
6
+ * any other verb refuses and prints the root form. Stdlib plus its data
7
+ * siblings (verbs.js, target-picker.js): it is vendored into every framework dist.
8
+ * The contract: docs/devkit/index.md.
31
9
  */
32
10
 
33
11
  const fs = require('fs');
34
12
  const path = require('path');
35
13
  const { createRequire } = require('module');
14
+ const { spawnSync } = require('child_process');
15
+ const { VERBS, TARGET_ORDER, tokensOf, findVerb } = require('./verbs.js');
16
+ const { PICKER_FLAG, takePicker } = require('./target-picker.js');
36
17
 
37
18
  const FRAMEWORKS = [
38
19
  '@omega.js/web',
@@ -55,24 +36,10 @@ const OMEGA_SCOPE = '@omega.js/';
55
36
  // the canonical list ([#307](https://github.com/Omega-JS-Stack/omega/issues/307)).
56
37
  const TARGET_SUBDIRS = ['functions', 'dist'];
57
38
 
58
- // The only verbs allowed to run with NO target context. The fallback exists for
59
- // the bootstrap case (`npx omega onboard` in a fresh clone, #276) and for verbs
60
- // that only READ; every other verb runs its target scaffold first (#675), so a
61
- // run in a directory that owns no target WRITES one there — that is how a stray
62
- // `omega deploy` at a workspace root scaffolded a whole desktop target into it
63
- // ([#699](https://github.com/Omega-JS-Stack/omega/issues/699)).
64
- //
65
- // Tokens, not command names: a flag-style alias (`omega --deploy`, `omega -v`)
66
- // selects a verb too, so every spelling of an allowed verb is listed —
67
- // onboard/help/version across the router frameworks and the manager, plus
68
- // backend's `cwd` and desktop's `logs`, the two read-only reporters.
69
- const CONTEXTLESS_VERBS = new Set([
70
- 'onboard', 'create', 'new', '-o', '--onboard',
71
- 'help', 'h', '-h', '--help',
72
- 'version', 'v', '-v', '--version',
73
- 'cwd',
74
- 'logs', 'log', '--logs', 'logs:read', 'logs:tail', 'logs:stream',
75
- ]);
39
+ // The verbs that run with NO target context, every spelling (a flag-style alias
40
+ // selects a verb too): the registry's contextless rows. Any other verb run where
41
+ // no target exists would scaffold one into the cwd.
42
+ const CONTEXTLESS_VERBS = new Set(VERBS.filter((entry) => entry.scope === 'contextless').flatMap(tokensOf));
76
43
 
77
44
  // The signing box's verbs. A box that hosts the Windows EV signing runner is a
78
45
  // MACHINE, not a project: `omega runner` and `omega sign-windows` read their
@@ -82,7 +49,7 @@ const CONTEXTLESS_VERBS = new Set([
82
49
  // desktop resolved from the cwd (a monorepo checkout, a brand root) — and
83
50
  // refuse naming the install when it is nowhere. They scaffold nothing
84
51
  // ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)).
85
- const BOX_VERBS = new Set(['runner', 'sign-windows']);
52
+ const BOX_VERBS = new Set(VERBS.filter((entry) => entry.scope === 'box').map((entry) => entry.name));
86
53
  const DESKTOP = '@omega.js/desktop';
87
54
 
88
55
  /**
@@ -156,47 +123,51 @@ function isBrandRoot(dir) {
156
123
  }
157
124
 
158
125
  /**
159
- * Walk up from startDir to the nearest dispatch context. At each level, in
160
- * order: the dir's own package.json being an OMEGA package, then declaring a
161
- * framework, then brand-root-ness. IDENTITY beats dependency — a framework
162
- * devDepends on other frameworks, and reading those as a target dispatched a
163
- * framework's own root to a sibling framework's CLI (#757). Framework checks
164
- * then come before the brand check so a standalone consumer (framework dep AND
165
- * its own config/omega.json5 in one dir) dispatches as a target, not a brand.
166
- * The CLI entry normalizes a functions/ cwd up to the target root
167
- * (muscle-memory `cd functions` still works).
168
- *
169
- * The walk is BOUNDED at the nearest `.git` (that directory is still checked
170
- * first): past the repo boundary is somebody else's tree, never this dir's
171
- * dispatch context. Same bound as @omega.js/config's resolveBrandRoot and the
172
- * Claude plugin's inject hook.
126
+ * Is `dir` the OMEGA monorepo root? Stdlib twin of devkit local.js's
127
+ * isMonorepoRoot, duplicated on purpose: the dispatcher cannot require local.js.
128
+ */
129
+ function isMonorepoRoot(dir) {
130
+ const pkg = readPackage(dir);
131
+ return !!pkg && pkg.name === 'omega' && fs.existsSync(path.join(dir, 'packages', 'devkit', 'package.json'));
132
+ }
133
+
134
+ /**
135
+ * Walk up from startDir to the nearest dispatch context: a dir declaring a
136
+ * framework (a target), then a brand root, then the monorepo root. An `@omega.js/*`
137
+ * package's own root is never a target (its devDependencies on sibling frameworks
138
+ * would read as one), so the walk passes it and keeps the nearest for the monorepo's
139
+ * root form. The walk stops at the nearest `.git`, that directory still checked.
173
140
  *
174
- * @returns {{ kind: 'self', name: string, dir: string }
175
- * | { kind: 'framework', name: string, dir: string }
176
- * | { kind: 'brand', dir: string } | null} dir = the package's own root
177
- * (self) / where the framework dep is declared (framework) / the brand root
178
- * (brand)
141
+ * @returns {{ kind: 'framework', name: string, dir: string }
142
+ * | { kind: 'brand', dir: string }
143
+ * | { kind: 'monorepo', dir: string, package?: { name: string, dir: string } } | null}
179
144
  */
180
145
  function findTarget(startDir) {
181
146
  let dir = path.resolve(startDir);
147
+ let omegaPackage = null;
182
148
  while (true) {
183
149
  const pkg = readPackage(dir);
150
+ const isOmegaPackage = !!pkg && typeof pkg.name === 'string' && pkg.name.startsWith(OMEGA_SCOPE);
151
+
152
+ if (isOmegaPackage) {
153
+ omegaPackage = omegaPackage || { name: pkg.name, dir };
154
+ } else {
155
+ const matches = frameworksOf(pkg);
156
+ if (matches.length > 1) {
157
+ // Targets are one-framework-per-target by design: a multi-framework
158
+ // manifest dispatches by FRAMEWORKS order, which must never be silent.
159
+ console.error(`omega: ${dir} declares ${matches.length} frameworks (${matches.join(', ')}): dispatching ${matches[0]}`);
160
+ }
161
+ const own = matches[0] || null;
162
+ if (own) return { kind: 'framework', name: own, dir };
184
163
 
185
- if (pkg && typeof pkg.name === 'string' && pkg.name.startsWith(OMEGA_SCOPE)) {
186
- return { kind: 'self', name: pkg.name, dir };
187
- }
188
-
189
- const matches = frameworksOf(pkg);
190
- if (matches.length > 1) {
191
- // Targets are one-framework-per-target by design — a multi-framework
192
- // manifest dispatches by FRAMEWORKS order, which must never be silent.
193
- console.error(`omega: ${dir} declares ${matches.length} frameworks (${matches.join(', ')}) — dispatching ${matches[0]}`);
164
+ if (isBrandRoot(dir)) {
165
+ return { kind: 'brand', dir };
166
+ }
194
167
  }
195
- const own = matches[0] || null;
196
- if (own) return { kind: 'framework', name: own, dir };
197
168
 
198
- if (isBrandRoot(dir)) {
199
- return { kind: 'brand', dir };
169
+ if (isMonorepoRoot(dir)) {
170
+ return omegaPackage ? { kind: 'monorepo', dir, package: omegaPackage } : { kind: 'monorepo', dir };
200
171
  }
201
172
 
202
173
  // Repo boundary — stop here rather than statting out through the host
@@ -209,6 +180,54 @@ function findTarget(startDir) {
209
180
  }
210
181
  }
211
182
 
183
+ /**
184
+ * The root a context runs its verbs from, what kind of root it is, and the
185
+ * `--target=` word that picks the cwd's target or package there (null when the
186
+ * cwd sits in none).
187
+ *
188
+ * @returns {{ dir: string, kind: 'brand'|'target'|'monorepo', target: string|null }}
189
+ */
190
+ function rootOf(context, cwd) {
191
+ if (context.kind === 'framework') {
192
+ const targetsDir = path.dirname(context.dir);
193
+ const brandRoot = path.dirname(targetsDir);
194
+ // A target under a brand's targets/ runs from the brand root; a standalone target is its own root
195
+ if (path.basename(targetsDir) === 'targets' && isBrandRoot(brandRoot)) {
196
+ return { dir: brandRoot, kind: 'brand', target: path.basename(context.dir) };
197
+ }
198
+ return { dir: context.dir, kind: 'target', target: null };
199
+ }
200
+
201
+ if (context.kind === 'monorepo') {
202
+ return { dir: context.dir, kind: 'monorepo', target: context.package ? path.basename(context.package.dir) : null };
203
+ }
204
+
205
+ // A brand: a dir under targets/ with no framework dep is still a target (a custom one)
206
+ const [top, name] = path.relative(context.dir, cwd).split(path.sep);
207
+ return { dir: context.dir, kind: 'brand', target: top === 'targets' && name ? name : null };
208
+ }
209
+
210
+ /** Does the verb this token selects run in place below its root? */
211
+ function ownsInPlace(context, token) {
212
+ // Only in a TARGET of one of its owner frameworks (a brand target or a standalone project); framework source is never a target
213
+ if (context.kind !== 'framework') return false;
214
+ // Every row the token selects counts: `serve` is web's dev alias and the backend's own verb
215
+ return VERBS.some((row) => tokensOf(row).includes(token) && row.owners.includes(context.name));
216
+ }
217
+
218
+ /** Refuse a verb run below its root, printing the one command that runs it. */
219
+ function refuseOutsideRoot(verb, argv, root, row) {
220
+ const rest = argv.filter((arg, index) => index !== argv.indexOf(verb));
221
+ // A brand-wide verb (only the manager owns it) runs once for the whole brand, so it picks no target
222
+ const brandWide = root.kind === 'brand' && !!row && row.owners.every((owner) => owner === MANAGER);
223
+ const picker = root.target && !brandWide ? ` --${PICKER_FLAG}=${root.target}` : '';
224
+ const form = root.kind === 'monorepo'
225
+ ? monorepoForm(root.dir, verb, rest, root.target)
226
+ : [`npx omega ${verb}${picker}`, ...rest].join(' ');
227
+ console.error(`omega: refusing to run "${verb}" in ${process.cwd()}: this verb runs at the ${root.kind} root. Run: cd ${root.dir} && ${form}`);
228
+ process.exit(1);
229
+ }
230
+
212
231
  /**
213
232
  * Resolve a dispatch target's ./cli from where it is declared.
214
233
  * @returns {{ cliPath: string } | { error: Error }} — callers decide whether an
@@ -249,20 +268,117 @@ function verbOf(argv) {
249
268
  return argv.find((arg) => !arg.startsWith('-')) || argv[0] || null;
250
269
  }
251
270
 
252
- async function run({ hostName, hostRun, argv = process.argv.slice(2) }) {
253
- const target = findTarget(process.cwd());
271
+ /**
272
+ * The framework packages, in the registry's target order: their CLI's `test` runs
273
+ * their own suite. The manager's `test` is the brand fan-out, so its suite is `npm test`.
274
+ */
275
+ function monorepoPackages(root) {
276
+ const packagesDir = path.join(root, 'packages');
277
+ const rank = (dirName) => (TARGET_ORDER.includes(dirName) ? TARGET_ORDER.indexOf(dirName) : TARGET_ORDER.length);
278
+ return fs.readdirSync(packagesDir)
279
+ .map((dirName) => ({ dirName, dir: path.join(packagesDir, dirName), manifest: readPackage(path.join(packagesDir, dirName)) }))
280
+ .filter((pkg) => pkg.manifest && FRAMEWORKS.includes(pkg.manifest.name) && pkg.manifest.bin && pkg.manifest.bin.omega)
281
+ .sort((a, b) => rank(a.dirName) - rank(b.dirName) || a.dirName.localeCompare(b.dirName));
282
+ }
283
+
284
+ /**
285
+ * The ONE command the monorepo root accepts for a verb, shared by the root's own
286
+ * refusal and the in-package refusal so the two cannot drift: `test` picks a
287
+ * framework package (any other package's suite is its own workspace's npm test),
288
+ * `dev` is the root watch, and every other verb is a workspace script.
289
+ *
290
+ * @param {string} root - The monorepo root.
291
+ * @param {string} verb - The verb as typed.
292
+ * @param {string[]} rest - The args after the verb.
293
+ * @param {string|null} pkg - The package dir name the cwd sits in, if any.
294
+ * @returns {string}
295
+ */
296
+ function monorepoForm(root, verb, rest, pkg) {
297
+ const entry = findVerb(verb);
298
+ if (entry && entry.name === 'test') {
299
+ if (pkg && !monorepoPackages(root).some((candidate) => candidate.dirName === pkg)) return `npm test --workspace packages/${pkg}`;
300
+ return [`npx omega test --${PICKER_FLAG}=${pkg || '<package>'}`, ...rest].join(' ');
301
+ }
302
+ if (entry && entry.name === 'dev') return 'npm start';
303
+ return `npm run ${entry ? entry.name : verb} --workspaces --if-present`;
304
+ }
305
+
306
+ /**
307
+ * The monorepo root: `test` runs each package `--target=` picks through its own
308
+ * CLI file, never the dispatcher (which refuses inside a package), in the package
309
+ * dir, stopping at the first failure; every other verb is a root npm script and
310
+ * refuses naming it.
311
+ */
312
+ function runMonorepoRoot({ root, argv, verb, spawn }) {
313
+ const entry = findVerb(verb);
314
+ if (!entry || entry.name !== 'test') {
315
+ console.error(`omega: the monorepo root runs only \`npx omega test --${PICKER_FLAG}=<package>\`; for "${verb}" run the root npm script: ${monorepoForm(root, verb, [])}`);
316
+ process.exit(1);
317
+ }
318
+
319
+ const packages = monorepoPackages(root);
320
+ const known = packages.map((pkg) => pkg.dirName).join(', ');
321
+ const { tokens, rest } = takePicker(argv.filter((arg, index) => index !== argv.indexOf(verb)));
322
+
323
+ // The monorepo has no "all" default: the whole battery is `npm test`
324
+ if (tokens.length === 0) {
325
+ console.error(`omega: pick the packages to test: npx omega test --${PICKER_FLAG}=<package>[,...] [scope], e.g. npx omega test --${PICKER_FLAG}=web framework:. The packages are ${known}; the whole battery is npm test.`);
326
+ process.exit(1);
327
+ }
328
+
329
+ const matches = (pkg, token) => token === pkg.dirName || token === pkg.manifest.name;
330
+ const unknown = tokens.filter((token) => !packages.some((pkg) => matches(pkg, token)));
331
+ if (unknown.length > 0) {
332
+ console.error(`omega: unknown --${PICKER_FLAG} ${unknown.map((token) => `"${token}"`).join(', ')}: the monorepo's packages are ${known}. Nothing ran.`);
333
+ process.exit(1);
334
+ }
335
+
336
+ for (const pkg of packages.filter((candidate) => tokens.some((token) => matches(candidate, token)))) {
337
+ const { cliPath, error } = tryResolveCli(pkg.manifest.name, pkg.dir);
338
+ if (!cliPath) {
339
+ console.error(`omega: ${pkg.manifest.name}'s './cli' did not resolve (${error.message}; an unbuilt dist? run its prepare). Nothing ran for it.`);
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+
344
+ console.error(`omega: ${pkg.manifest.name}: omega test ${rest.join(' ')}`.trimEnd());
345
+ const result = spawn(process.execPath, [cliPath, 'test', ...rest], {
346
+ cwd: pkg.dir,
347
+ stdio: 'inherit',
348
+ });
349
+
350
+ // A child killed by a signal (or never spawned) has no status, and failed all the same
351
+ const code = typeof result.status === 'number' ? result.status : 1;
352
+ if (code !== 0) {
353
+ process.exitCode = code;
354
+ return;
355
+ }
356
+ }
357
+ }
358
+
359
+ async function run({ hostName, hostRun, argv = process.argv.slice(2), spawn = spawnSync }) {
360
+ const cwd = process.cwd();
361
+ const target = findTarget(cwd);
362
+ const verb = verbOf(argv);
363
+ // A contextless or box verb has no root to run from, and a bare `omega` prints help
364
+ const rootless = verb === null || CONTEXTLESS_VERBS.has(verb) || boxVerbOf(argv) !== null;
365
+
366
+ if (target && !rootless) {
367
+ const root = rootOf(target, cwd);
368
+ const row = findVerb(verb);
369
+ if (cwd !== root.dir && !ownsInPlace(target, verb)) refuseOutsideRoot(verb, argv, root, row);
370
+ }
254
371
 
255
372
  // No context — the bootstrap case (a verb run in a fresh directory has
256
373
  // no framework dep yet, by definition). Run the INSTALLED @omega.js/manager
257
374
  // when one resolves, else the HOST framework's CLI exactly like the
258
375
  // pre-dispatcher bins did, and say which one so a hoist-winner at a brand
259
376
  // root is never a silent mystery.
260
- if (!target) {
377
+ if (!target || (target.kind === 'monorepo' && rootless)) {
261
378
  // …but ONLY for a verb that cannot write. The fallback hands a mutating verb
262
379
  // to whichever framework won npm's bin link, and that verb scaffolds its own
263
380
  // target into the cwd — a directory that owns no target is never where that
264
381
  // should land (#699). Twin of ensure-target's refusal (devkit scaffold-guard.js).
265
- const verb = verbOf(argv);
266
382
  const boxVerb = boxVerbOf(argv);
267
383
  if (boxVerb) {
268
384
  if (hostName === DESKTOP) return hostRun();
@@ -276,7 +392,7 @@ async function run({ hostName, hostRun, argv = process.argv.slice(2) }) {
276
392
  }
277
393
  if (verb && !CONTEXTLESS_VERBS.has(verb)) {
278
394
  console.error(`omega: refusing to run "${verb}" — ${process.cwd()} is not inside an OMEGA target (no framework dependency and no config/omega.json5 above it). Nothing was scaffolded.`);
279
- console.error('Run it from a target directory, or `npx omega onboard` to create one here. Without a target, only onboard (create, new), help, version, cwd and logs run; the signing box\'s runner and sign-windows run through @omega.js/desktop.');
395
+ console.error('Run it from a root (a brand root or a standalone target), or `npx omega onboard` to create one here. Without a target, only onboard (create, new), help, version, cwd and logs run; the signing box\'s runner and sign-windows run through @omega.js/desktop.');
280
396
  process.exit(1);
281
397
  }
282
398
 
@@ -298,27 +414,8 @@ async function run({ hostName, hostRun, argv = process.argv.slice(2) }) {
298
414
  return hostRun();
299
415
  }
300
416
 
301
- // An OMEGA package's OWN root — run ITS CLI, so `omega test` in
302
- // packages/extension runs the extension's self-test. A package is never a
303
- // target: nothing here may reach the host fallback, whose ensure-target would
304
- // scaffold a consumer project into the framework source tree (#757).
305
- if (target.kind === 'self') {
306
- if (target.name === hostName) return hostRun();
307
-
308
- const { cliPath, error } = tryResolveCli(target.name, target.dir);
309
- if (!cliPath) {
310
- // Two different facts share one refusal: a package that exports no
311
- // `./cli` at all (config, devkit, ...) versus one that declares it but
312
- // cannot resolve it right now (an unbuilt dist, a fresh clone before
313
- // `npm install`). Name the one that applies.
314
- const reason = error && error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED'
315
- ? 'which ships no CLI'
316
- : `whose './cli' did not resolve (${error ? error.message : 'unknown'}; an unbuilt dist? run its prepare)`;
317
- console.error(`omega: refusing to run — ${target.dir} is the ${target.name} package itself, ${reason}, and an OMEGA package is never a target. Nothing was scaffolded.`);
318
- process.exit(1);
319
- }
320
- return require(cliPath).run();
321
- }
417
+ // Below the monorepo root every verb refused above, so this is the root, which runs `test` over its packages
418
+ if (target.kind === 'monorepo') return runMonorepoRoot({ root: target.dir, argv, verb, spawn });
322
419
 
323
420
  // A brand root — the manager owns brand-level commands (`omega test` fans
324
421
  // out over targets/*). Resolve it from the brand root and hand over.
@@ -338,23 +435,23 @@ async function run({ hostName, hostRun, argv = process.argv.slice(2) }) {
338
435
  if (hostName === MANAGER) {
339
436
  console.error(`omega: brand-shaped directory at ${target.dir} with no installed ${MANAGER} — running the bundled ${hostName} (run npm install to use the pinned version)`);
340
437
  } else {
341
- console.error(`omega: brand-shaped directory at ${target.dir} but ${MANAGER} is not installed — running ${hostName} instead (install ${MANAGER} at the brand root, or \`mgr i local\` for a monorepo link, if this really is a brand)`);
438
+ console.error(`omega: brand-shaped directory at ${target.dir} but ${MANAGER} is not installed, running ${hostName} instead (install ${MANAGER} at the brand root if this really is a brand)`);
342
439
  }
343
440
  return hostRun();
344
441
  }
345
442
  return require(cliPath).run();
346
443
  }
347
444
 
348
- // The bin that won npm's .bin link belongs to this target's framework — run it directly.
445
+ // The bin that won npm's .bin link belongs to this target's framework: run it directly.
349
446
  if (target.name === hostName) {
350
447
  return hostRun();
351
448
  }
352
449
 
353
- // The target belongs to a DIFFERENT framework — resolve its CLI from where the
450
+ // The target belongs to a DIFFERENT framework: resolve its CLI from where the
354
451
  // dependency is declared and hand over.
355
452
  const cliPath = resolveCli(target.name, target.dir,
356
- 'Is the framework installed? Try npm install, or `mgr i local` for a monorepo link.');
453
+ 'Is the framework installed? Try npm install, or `npx omega i local` at the brand root for a monorepo link.');
357
454
  return require(cliPath).run();
358
455
  }
359
456
 
360
- module.exports = { run, findTarget, isBrandRoot, verbOf, isBoxVerbArgv, TARGET_SUBDIRS, CONTEXTLESS_VERBS, BOX_VERBS, FRAMEWORKS, MANAGER };
457
+ module.exports = { run, findTarget, isBrandRoot, isMonorepoRoot, verbOf, isBoxVerbArgv, TARGET_SUBDIRS, CONTEXTLESS_VERBS, BOX_VERBS, FRAMEWORKS, MANAGER };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The docs sync: a brand whose @omega.js/manager is a local checkout gets that
3
+ * manager's docs/ synced from the monorepo's docs/ before every verb, so the
4
+ * brand's agents read the live map through the manager's AGENTS.md.
5
+ *
6
+ * A published manager ships the docs of its own version and is left alone, as
7
+ * is an invocation outside a brand. The sync writes only files whose content
8
+ * changed, so a boot with nothing to do reads the tree and writes nothing.
9
+ */
10
+
11
+ const path = require('path');
12
+ const { resolvePackageRealDir, isLocalCheckout, isMonorepoRoot } = require('../local.js');
13
+
14
+ const MANAGER = '@omega.js/manager';
15
+
16
+ /**
17
+ * @param {object} context - The prelude context.
18
+ * @param {string|null} context.brandRoot - The brand root of the invocation.
19
+ * @param {function} [context.log] - Injectable line printer (tests).
20
+ * @returns {{ synced: boolean, reason?: string, written?: string[], removed?: string[] }}
21
+ */
22
+ function run(context = {}) {
23
+ const { brandRoot } = context;
24
+ const log = context.log || console.log;
25
+ if (!brandRoot) {
26
+ return { synced: false, reason: 'no-brand' };
27
+ }
28
+
29
+ const managerDir = resolvePackageRealDir(MANAGER, brandRoot);
30
+ if (!managerDir) {
31
+ return { synced: false, reason: 'no-manager' };
32
+ }
33
+ // A checkout lives at <monorepo>/packages/manager, the layout every link points at.
34
+ const monorepoRoot = path.dirname(path.dirname(managerDir));
35
+ if (!isLocalCheckout(managerDir) || !isMonorepoRoot(monorepoRoot)) {
36
+ return { synced: false, reason: 'published' };
37
+ }
38
+
39
+ // The checkout's own docs lane, so the sync matches the monorepo's code.
40
+ const { syncDocs } = require(path.join(monorepoRoot, 'packages', 'devkit', 'tools', 'vendor-docs.js'));
41
+ const { written, removed } = syncDocs({ monorepoRoot, packageDir: managerDir });
42
+ if (written.length + removed.length > 0) {
43
+ log(`omega: synced the linked manager's docs from the monorepo (${written.length} written, ${removed.length} removed)`);
44
+ }
45
+ return { synced: true, written, removed };
46
+ }
47
+
48
+ module.exports = {
49
+ name: 'docs-sync',
50
+ verbs: 'all',
51
+ run,
52
+ };
@@ -31,6 +31,7 @@
31
31
  // The list. Order is the boot order.
32
32
  const PRELUDES = [
33
33
  require('./origin-heal.js'),
34
+ require('./docs-sync.js'),
34
35
  ];
35
36
 
36
37
  /**
@@ -0,0 +1,45 @@
1
+ /**
2
+ * target-picker: the ONE spelling and parse of the root picker, `--target=<a,b>`.
3
+ * A brand root picks targets with it and the monorepo root picks packages with
4
+ * it, so the flag name and its comma list can never drift between the two.
5
+ * Stdlib-free: the dispatcher that requires it is vendored into every framework dist.
6
+ */
7
+
8
+ // The flag that spells the picker on every root verb
9
+ const PICKER_FLAG = 'target';
10
+
11
+ /**
12
+ * A comma list of target tokens, trimmed and emptied of blanks.
13
+ *
14
+ * @param {string} [value] - the raw flag value
15
+ * @returns {string[]}
16
+ */
17
+ function parseTargetTokens(value) {
18
+ return String(value || '').split(',').map((part) => part.trim()).filter(Boolean);
19
+ }
20
+
21
+ /**
22
+ * Split the picker (`--target=a,b` or `--target a,b`) out of raw args, keeping
23
+ * every other arg verbatim and in order: those are the ones that forward.
24
+ *
25
+ * @param {string[]} args - raw args, as typed
26
+ * @returns {{ tokens: string[], rest: string[] }}
27
+ */
28
+ function takePicker(args) {
29
+ const tokens = [];
30
+ const rest = [];
31
+ for (let index = 0; index < args.length; index++) {
32
+ const arg = args[index];
33
+ if (arg.startsWith(`--${PICKER_FLAG}=`)) {
34
+ tokens.push(...parseTargetTokens(arg.slice(PICKER_FLAG.length + 3)));
35
+ } else if (arg === `--${PICKER_FLAG}`) {
36
+ tokens.push(...parseTargetTokens(args[index + 1]));
37
+ index++;
38
+ } else {
39
+ rest.push(arg);
40
+ }
41
+ }
42
+ return { tokens, rest };
43
+ }
44
+
45
+ module.exports = { PICKER_FLAG, parseTargetTokens, takePicker };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The files under a tree that carry an em dash (U+2014).
3
+ *
4
+ * Every file a framework copies into a consumer's repo meets that repo's own
5
+ * commit guard, which may refuse the character, so each emitted tree pins
6
+ * itself with an equality on `dashedFiles(ROOT)` against `[]`.
7
+ *
8
+ * A walk that finds almost nothing means a wrong root, and a pin over an empty
9
+ * list would pass vacuously, so that walk throws instead. Node builtins only:
10
+ * the backend vendors this file as a raw asset, with no specifier rewrite.
11
+ */
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ // Fewer files than this under a root means the walk missed its tree.
16
+ const MIN_FILES = 6;
17
+
18
+ /**
19
+ * @param {string} root - Absolute directory to walk
20
+ * @returns {string[]} Root-relative paths of the files carrying U+2014, sorted
21
+ */
22
+ function dashedFiles(root) {
23
+ const files = fs.readdirSync(root, { recursive: true })
24
+ .map((entry) => path.join(root, entry))
25
+ .filter((file) => fs.statSync(file).isFile());
26
+
27
+ if (files.length < MIN_FILES) {
28
+ throw new Error(`dashedFiles: the walk of ${root} found ${files.length} files, so the pin would pass vacuously`);
29
+ }
30
+
31
+ return files
32
+ .filter((file) => fs.readFileSync(file, 'utf8').includes('\u2014'))
33
+ .map((file) => path.relative(root, file))
34
+ .sort();
35
+ }
36
+
37
+ module.exports = { dashedFiles };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Run a framework's `omega test` verb in process under an outer log tee, the
3
+ * one harness the desktop and extension verb-logs suites share.
4
+ *
5
+ * The verb's attach is DECLINED (CI set for the call) so it pushes no layer of
6
+ * its own; the result says whether the tee underneath still receives after
7
+ * the verb returns, which is what a blind detach would break.
8
+ */
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const Module = require('module');
13
+ const attachLogFile = require('../attach-log-file.js');
14
+
15
+ /**
16
+ * @param {object} options
17
+ * @param {string} options.commands - The framework's commands dir (holds test.js and lib/ensure-target.js)
18
+ * @param {string} options.prefix - The tmpdir prefix for the empty consumer dir
19
+ * @param {string[]} options.positionals - The verb's `_` after `test`
20
+ * @returns {Promise<{ outer: string, exitCodes: number[] }>} The outer log and any process.exit codes
21
+ */
22
+ async function runTestVerbUnderOuterTee({ commands, prefix, positionals }) {
23
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
24
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'test-verb-fixture' }));
25
+ const outerPath = path.join(dir, 'outer.log');
26
+
27
+ // ensureTarget scaffolds a real target; the verb destructures it at load.
28
+ const ensurePath = require.resolve(path.join(commands, 'lib', 'ensure-target.js'));
29
+ const cachedEnsure = require.cache[ensurePath];
30
+ const stub = new Module(ensurePath);
31
+ stub.filename = ensurePath;
32
+ stub.loaded = true;
33
+ stub.exports = { ensureTarget: async () => {} };
34
+ require.cache[ensurePath] = stub;
35
+ const verbPath = require.resolve(path.join(commands, 'test.js'));
36
+ const cachedVerb = require.cache[verbPath];
37
+ delete require.cache[verbPath];
38
+
39
+ const prior = { cwd: process.cwd(), exitCode: process.exitCode, exit: process.exit, log: console.log, CI: process.env.CI };
40
+ const exitCodes = [];
41
+ const restoreTee = attachLogFile.mark();
42
+ attachLogFile(outerPath, { env: {} });
43
+
44
+ try {
45
+ process.chdir(dir);
46
+ console.log = () => {};
47
+ // Stubbed only because the green path ends the process.
48
+ process.exit = (code) => { exitCodes.push(code); };
49
+ process.env.CI = 'true';
50
+ await require(verbPath)({ _: ['test', ...positionals] });
51
+ process.stdout.write('after the verb\n');
52
+ } finally {
53
+ if (prior.CI === undefined) delete process.env.CI;
54
+ else process.env.CI = prior.CI;
55
+ process.exit = prior.exit;
56
+ console.log = prior.log;
57
+ process.exitCode = prior.exitCode;
58
+ process.chdir(prior.cwd);
59
+ restoreTee();
60
+ if (cachedEnsure) require.cache[ensurePath] = cachedEnsure;
61
+ else delete require.cache[ensurePath];
62
+ if (cachedVerb) require.cache[verbPath] = cachedVerb;
63
+ else delete require.cache[verbPath];
64
+ }
65
+
66
+ const outer = fs.readFileSync(outerPath, 'utf8');
67
+ fs.rmSync(dir, { recursive: true, force: true });
68
+ return { outer, exitCodes };
69
+ }
70
+
71
+ module.exports = { runTestVerbUnderOuterTee };