@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
@@ -122,8 +122,8 @@ module.exports = defineCases({
122
122
  ctx.expect(decode.run).toContain('if [ -n "${{ secrets.CSC_LINK }}" ]; then');
123
123
  ctx.expect(decode.run).toContain('if [ -n "${{ secrets.APPLE_API_KEY }}" ]; then');
124
124
  // The standalone verb is gone (#891): the deploy precheck pushes them.
125
- ctx.expect(decode.run).toContain('No CSC_LINK secret: run omega deploy from targets/desktop');
126
- ctx.expect(decode.run).toContain('No APPLE_API_KEY secret: run omega deploy from targets/desktop');
125
+ ctx.expect(decode.run).toContain('No CSC_LINK secret: run omega deploy at the brand root');
126
+ ctx.expect(decode.run).toContain('No APPLE_API_KEY secret: run omega deploy at the brand root');
127
127
  ctx.expect(decode.run).not.toContain('push-secrets');
128
128
  ctx.expect(decode.run.match(/exit 1/g).length).toBe(2);
129
129
  ctx.expect(decode.run).not.toContain('this build will not be signed');
@@ -145,6 +145,34 @@ module.exports = defineCases({
145
145
  }
146
146
  },
147
147
  },
148
+ {
149
+ name: 'update: --dry-run reaches devkit runUpdate as dryRun, and a bare run installs',
150
+ run: async (ctx) => {
151
+ const devkitUpdate = require('../../../vendor/devkit/update.js');
152
+ const { BOOLEAN_FLAGS } = require(path.join(root, 'dist', 'cli-run.js'));
153
+ const { parseArgv } = require('../../../vendor/devkit/argv.js');
154
+ const commandFile = path.join(root, 'dist', 'commands', 'update.js');
155
+
156
+ // The command destructures runUpdate at load, so the stub lands before a fresh require
157
+ async function run(args) {
158
+ const original = devkitUpdate.runUpdate;
159
+ const seen = [];
160
+ devkitUpdate.runUpdate = async (options) => { seen.push(options); };
161
+ delete require.cache[commandFile];
162
+ try {
163
+ await require(commandFile)(parseArgv(args, { booleans: BOOLEAN_FLAGS }));
164
+ } finally {
165
+ devkitUpdate.runUpdate = original;
166
+ delete require.cache[commandFile];
167
+ }
168
+ ctx.expect(seen.length).toBe(1);
169
+ return seen[0];
170
+ }
171
+
172
+ ctx.expect((await run(['update', '--dry-run'])).dryRun).toBe(true);
173
+ ctx.expect(Boolean((await run(['update'])).dryRun)).toBe(false);
174
+ },
175
+ },
148
176
  {
149
177
  name: 'mirrored aliases: -t routes to test, -d routes to deploy (wave-6 D6)',
150
178
  run: (ctx) => {
@@ -0,0 +1,22 @@
1
+ // Every defaults file is copied into a consumer's repo, whose own guard may
2
+ // refuse an em dash (U+2014) at commit time. So no defaults file carries one.
3
+
4
+ const path = require('path');
5
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
6
+ const { dashedFiles } = require('../../../vendor/devkit/test/dashed-files.js');
7
+
8
+ const DEFAULTS = path.join(__dirname, '..', '..', '..', 'defaults');
9
+
10
+ module.exports = defineCases({
11
+ type: 'group',
12
+ layer: 'build',
13
+ description: 'defaults tree carries no em dash',
14
+ tests: [
15
+ {
16
+ name: 'no defaults file carries an em dash',
17
+ run: (ctx) => {
18
+ ctx.expect(dashedFiles(DEFAULTS)).toEqual([]);
19
+ },
20
+ },
21
+ ],
22
+ });
@@ -34,10 +34,11 @@ module.exports = defineCases({
34
34
  // .env and every verb delivers them.
35
35
  ctx.expect(jetpack.exists(path.join(tmp, '.env'))).toBe(false);
36
36
  ctx.expect(jetpack.exists(path.join(tmp, '.gitignore'))).toBeTruthy();
37
- // The agent-docs chain (#63): AGENTS.md carries the content, CLAUDE.md is
38
- // the one-line `@AGENTS.md` pointer.
39
- ctx.expect(jetpack.read(path.join(tmp, 'AGENTS.md'))).toContain('node_modules/@omega.js/AGENTS.md');
40
- ctx.expect(jetpack.read(path.join(tmp, 'CLAUDE.md')).trim()).toBe('@AGENTS.md');
37
+ // A standalone project root gets the same AGENTS.md a brand root does:
38
+ // the manager import under Default, its notes under Custom. Claude Code reads
39
+ // AGENTS.md natively, so no CLAUDE.md scaffolds beside it.
40
+ ctx.expect(jetpack.read(path.join(tmp, 'AGENTS.md'))).toBe('<!-- ========== Default Values ========== -->\n@node_modules/@omega.js/manager/AGENTS.md\n\n<!-- ========== Custom Values ========== -->\n');
41
+ ctx.expect(jetpack.exists(path.join(tmp, 'CLAUDE.md'))).toBe(false);
41
42
  ctx.expect(jetpack.exists(path.join(tmp, '_mas'))).toBe(false);
42
43
  // `_`-prefixed FILENAMES are not archives — test/_init.js ships.
43
44
  ctx.expect(jetpack.exists(path.join(tmp, 'test', '_init.js'))).toBeTruthy();
@@ -213,6 +214,20 @@ module.exports = defineCases({
213
214
  ctx.expect(jetpack.exists(path.join(targetDir, 'config', 'omega.json5'))).toBe(false);
214
215
  },
215
216
  },
217
+ {
218
+ name: 'an old-template AGENTS.md converges, keeping the consumer notes under the Custom marker',
219
+ run: async (ctx) => {
220
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-defaults-'));
221
+ jetpack.write(path.join(tmp, 'AGENTS.md'),
222
+ `${DEFAULT_MARKER}\n# OMEGA Desktop consumer project\nframework guidance\n\n${CUSTOM_MARKER}\n\n## Project-specific notes\n\nAdd anything specific to THIS project here. Edits below this line are preserved across framework re-syncs.\n\nOur deploy needs the VPN up.\n`);
223
+ const warnings = [];
224
+ const applied = await copyDefaults(tmp, { log: () => {}, warn: (m) => warnings.push(m), error: () => {} });
225
+
226
+ ctx.expect(jetpack.read(path.join(tmp, 'AGENTS.md'))).toBe('<!-- ========== Default Values ========== -->\n@node_modules/@omega.js/manager/AGENTS.md\n\n<!-- ========== Custom Values ========== -->\nOur deploy needs the VPN up.\n');
227
+ ctx.expect(applied.merged.includes('AGENTS.md')).toBe(true);
228
+ ctx.expect(warnings.some((m) => m.includes('Converged AGENTS.md'))).toBe(true);
229
+ },
230
+ },
216
231
  {
217
232
  name: 'brand target scaffolds NO per-target docs (brand doc unification: the brand root is the doc home)',
218
233
  run: async (ctx) => {
@@ -240,7 +255,6 @@ module.exports = defineCases({
240
255
  // Standalone scaffold first (no brand config yet) — per-target docs land.
241
256
  await copyDefaults(targetDir);
242
257
  ctx.expect(jetpack.exists(path.join(targetDir, 'AGENTS.md'))).toBeTruthy();
243
- ctx.expect(jetpack.exists(path.join(targetDir, 'CLAUDE.md'))).toBeTruthy();
244
258
 
245
259
  // Wrap it in a brand monorepo: the next setup sweeps the untouched docs.
246
260
  jetpack.write(path.join(tmp, 'brand', 'config', 'omega.json5'), "{ brand: { id: 'acme', name: 'Acme' } }\n");
@@ -26,10 +26,10 @@ const COMMANDS = path.join(__dirname, '..', '..', '..', 'commands');
26
26
  const DEPLOY = path.join(COMMANDS, 'deploy.js');
27
27
  const WORKFLOW = path.join(__dirname, '..', '..', '..', 'defaults', '.github', 'workflows', 'build.yml');
28
28
 
29
- // The scripts every target gets from the scaffold, read from the one place that
30
- // declares them: expanding the workflow's command through THESE is what ties
29
+ // The scripts every target gets from the scaffold, derived the one way the
30
+ // scaffold derives them: expanding the workflow's command through THESE is what ties
31
31
  // the runner's step to the verb this laptop runs.
32
- const SCRIPTS = require(path.join(__dirname, '..', '..', '..', '..', 'package.json')).projectScripts;
32
+ const SCRIPTS = require('../../../vendor/devkit/verb-scripts.js').projectScripts(require(path.join(__dirname, '..', '..', '..', '..', 'package.json')));
33
33
 
34
34
  const { directPlatform } = require(DEPLOY);
35
35
 
@@ -147,6 +147,7 @@ module.exports = defineCases({
147
147
  const resolved = require.resolve(DEPLOY);
148
148
  const cached = require.cache[resolved];
149
149
  delete require.cache[resolved];
150
+ const restoreTee = attachLogFile.mark();
150
151
 
151
152
  try {
152
153
  const deploy = require(resolved);
@@ -159,7 +160,7 @@ module.exports = defineCases({
159
160
  ctx.expect(ran).toEqual(['ensure-target']);
160
161
  } finally {
161
162
  // The verb tees this process' writers now (#873): hand them back.
162
- attachLogFile.detach();
163
+ restoreTee();
163
164
  for (const restore of restores) restore();
164
165
  if (cached) require.cache[resolved] = cached;
165
166
  else delete require.cache[resolved];
@@ -206,6 +207,7 @@ module.exports = defineCases({
206
207
  const resolved = require.resolve(DEPLOY);
207
208
  const cached = require.cache[resolved];
208
209
  delete require.cache[resolved];
210
+ const restoreTee = attachLogFile.mark();
209
211
 
210
212
  try {
211
213
  const deploy = require(resolved);
@@ -229,7 +231,7 @@ module.exports = defineCases({
229
231
  ctx.expect(script.split(' ').filter((word) => word.startsWith('--'))).toEqual(['--local']);
230
232
  } finally {
231
233
  // The verb tees this process' writers now (#873): hand them back.
232
- attachLogFile.detach();
234
+ restoreTee();
233
235
  deployRecord.recordDeploy = recordDeploy;
234
236
  for (const restore of restores) restore();
235
237
  if (cached) require.cache[resolved] = cached;
@@ -108,6 +108,7 @@ async function runRelease(follower, options = {}, token = {}) {
108
108
  else process.env.GH_TOKEN = envToken;
109
109
  process.chdir(targetDir);
110
110
  delete require.cache[require.resolve(RELEASE)];
111
+ const restoreTee = attachLogFile.mark();
111
112
 
112
113
  let error = null;
113
114
  try {
@@ -117,7 +118,7 @@ async function runRelease(follower, options = {}, token = {}) {
117
118
  } finally {
118
119
  // The verb tees this process' writers: hand them back before the next
119
120
  // suite prints through a fixture that is about to be gone.
120
- attachLogFile.detach();
121
+ restoreTee();
121
122
  console.log = previousLog;
122
123
  process.chdir(previousCwd);
123
124
  if (previousToken === undefined) delete process.env.GH_TOKEN;
@@ -69,6 +69,7 @@ async function runDeploy(options) {
69
69
  const origCwd = process.cwd();
70
70
 
71
71
  delete require.cache[resolved];
72
+ const restoreTee = attachLogFile.mark();
72
73
  global.__omegaDeployHookRan = ran;
73
74
 
74
75
  try {
@@ -80,7 +81,7 @@ async function runDeploy(options) {
80
81
  return ran;
81
82
  } finally {
82
83
  // The verb tees this process' writers now (#873): hand them back.
83
- attachLogFile.detach();
84
+ restoreTee();
84
85
  process.chdir(origCwd);
85
86
  delete global.__omegaDeployHookRan;
86
87
  for (const restore of restores) restore();
@@ -116,6 +117,7 @@ async function runDriftedDeploy() {
116
117
  const origCwd = process.cwd();
117
118
 
118
119
  delete require.cache[resolved];
120
+ const restoreTee = attachLogFile.mark();
119
121
 
120
122
  try {
121
123
  const deploy = require(resolved);
@@ -124,7 +126,7 @@ async function runDriftedDeploy() {
124
126
  return ran;
125
127
  } finally {
126
128
  // The verb tees this process' writers now (#873): hand them back.
127
- attachLogFile.detach();
129
+ restoreTee();
128
130
  process.chdir(origCwd);
129
131
  for (const restore of restores) restore();
130
132
  if (cached) require.cache[resolved] = cached;
@@ -0,0 +1,67 @@
1
+ // Build-layer tests for the `dev` verb, what the target's `start` script runs:
2
+ // clean, then the gulp default task (build, then launch electron), with the
3
+ // `--` flags it was given forwarded to the gulp lane. No build-mode flag, and
4
+ // never a shell-out back to `npm start` (that script IS this verb).
5
+
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
9
+
10
+ const SRC = path.join(__dirname, '..', '..', '..');
11
+ const pipeline = require(path.join(SRC, 'utils', 'build-pipeline.js'));
12
+ const dev = require(path.join(SRC, 'commands', 'dev.js'));
13
+
14
+ const stepsOf = (plan) => plan.steps.map((s) => (s.task ? `${s.type}:${s.task}` : s.type));
15
+
16
+ module.exports = defineCases({
17
+ type: 'suite',
18
+ layer: 'build',
19
+ description: 'dev verb: clean, then the gulp default task',
20
+ tests: [
21
+ {
22
+ name: 'dev: clean → gulp default, no build-mode flag',
23
+ run: (ctx) => {
24
+ const plan = dev.plan({}, []);
25
+ ctx.expect(stepsOf(plan)).toEqual(['clean', 'gulp:default']);
26
+ ctx.expect(plan.env.OMEGA_BUILD_MODE).toBe(undefined);
27
+ },
28
+ },
29
+ {
30
+ name: 'dev forwards its `--` flags to the gulp lane (`npm start -- --remote-debugging-port=9222`)',
31
+ run: (ctx) => {
32
+ const plan = dev.plan({}, ['dev', '--remote-debugging-port=9222', 'stray', '--debug']);
33
+ ctx.expect(plan.steps[1].args).toEqual(['--remote-debugging-port=9222', '--debug']);
34
+ ctx.expect(pipeline.gulpCommand('default', plan.steps[1].args))
35
+ .toBe('npm run gulp -- default --remote-debugging-port=9222 --debug');
36
+ },
37
+ },
38
+ {
39
+ name: 'runPipeline runs the dev plan in order and leaves the build-mode flag unset',
40
+ run: async (ctx) => {
41
+ const log = [];
42
+ const record = (type) => (options, step) => { log.push(step.task ? `${type}:${step.task}` : type); };
43
+ const previous = process.env.OMEGA_BUILD_MODE;
44
+ delete process.env.OMEGA_BUILD_MODE;
45
+ try {
46
+ await pipeline.runPipeline(dev.plan({}, []), {}, { clean: record('clean'), gulp: record('gulp') });
47
+ ctx.expect(process.env.OMEGA_BUILD_MODE).toBe(undefined);
48
+ } finally {
49
+ if (previous !== undefined) process.env.OMEGA_BUILD_MODE = previous;
50
+ }
51
+ ctx.expect(log).toEqual(['clean', 'gulp:default']);
52
+ },
53
+ },
54
+ {
55
+ name: 'the CLI answers dev and its aliases, and the verb never shells back to `npm start`',
56
+ run: (ctx) => {
57
+ const { aliases } = require(path.join(SRC, 'cli.js')).config;
58
+ ctx.expect(aliases.dev).toEqual(['serve', 'start', '--dev']);
59
+
60
+ const source = fs.readFileSync(path.join(SRC, 'commands', 'dev.js'), 'utf8')
61
+ .replace(/\/\/[^\n]*/g, '')
62
+ .replace(/\/\*[\s\S]*?\*\//g, '');
63
+ ctx.expect(/npm (run )?start/.test(source)).toBe(false);
64
+ },
65
+ },
66
+ ],
67
+ });
@@ -67,7 +67,7 @@ module.exports = defineCases({
67
67
  },
68
68
  },
69
69
  {
70
- name: 'refuses a workspace root, loudly, without writing a single file (#699)',
70
+ name: 'refuses a workspace root, loudly, leaving its AGENTS.md and .gitignore byte-identical (#699, #973)',
71
71
  run: async (ctx) => {
72
72
  // The accident: `omega deploy` at a workspace root scaffolded a whole
73
73
  // desktop target into it — gulpfile, src/, hooks/, workflows, rewritten
@@ -78,6 +78,12 @@ module.exports = defineCases({
78
78
  manifest.workspaces = ['packages/*'];
79
79
  jetpack.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
80
80
  const before = jetpack.read(manifestPath);
81
+ // A root's hand-authored AGENTS.md and .gitignore are what the marker
82
+ // merge rewrites in place: `#` headings and blank lines are its comments.
83
+ const agentsDoc = '# Monorepo\n\n## HARD RULES\n\n1. Rule one.\n';
84
+ const ignoreFile = '# Dependencies\nnode_modules/\n\n# Scratch\n.temp/\n';
85
+ jetpack.write(path.join(tmp, 'AGENTS.md'), agentsDoc);
86
+ jetpack.write(path.join(tmp, '.gitignore'), ignoreFile);
81
87
 
82
88
  try {
83
89
  let refusal = null;
@@ -92,9 +98,11 @@ module.exports = defineCases({
92
98
  ctx.expect(refusal.message).toMatch(/declares "workspaces"/);
93
99
  ctx.expect(refusal.message.includes(tmp)).toBe(true);
94
100
 
95
- // Nothing scaffolded, and the manifest is byte-identical.
96
- ctx.expect(jetpack.list(tmp)).toEqual(['package.json']);
101
+ // Nothing scaffolded, and every root file is byte-identical.
102
+ ctx.expect(jetpack.list(tmp).sort()).toEqual(['.gitignore', 'AGENTS.md', 'package.json']);
97
103
  ctx.expect(jetpack.read(manifestPath)).toBe(before);
104
+ ctx.expect(jetpack.read(path.join(tmp, 'AGENTS.md'))).toBe(agentsDoc);
105
+ ctx.expect(jetpack.read(path.join(tmp, '.gitignore'))).toBe(ignoreFile);
98
106
  } finally {
99
107
  fs.rmSync(tmp, { recursive: true, force: true });
100
108
  }
@@ -222,17 +222,17 @@ ${CUSTOM_MARKER}
222
222
  },
223
223
  },
224
224
  {
225
- name: 'normalizeEnvLine: empty value stays unquoted',
225
+ name: 'normalizeEnvLine: an empty value becomes KEY=""',
226
226
  run: (ctx) => {
227
- ctx.expect(normalizeEnvLine('KEY=')).toBe('KEY=');
228
- ctx.expect(normalizeEnvLine('KEY= ')).toBe('KEY=');
227
+ ctx.expect(normalizeEnvLine('KEY=')).toBe('KEY=""');
228
+ ctx.expect(normalizeEnvLine('KEY= ')).toBe('KEY=""');
229
229
  },
230
230
  },
231
231
  {
232
- name: 'normalizeEnvLine: escapes embedded double quotes and backslashes',
232
+ name: 'normalizeEnvLine: keeps embedded quotes and backslashes as dotenv reads them',
233
233
  run: (ctx) => {
234
- ctx.expect(normalizeEnvLine('KEY=he said "hi"')).toBe('KEY="he said \\"hi\\""');
235
- ctx.expect(normalizeEnvLine('KEY=back\\slash')).toBe('KEY="back\\\\slash"');
234
+ ctx.expect(normalizeEnvLine('KEY=he said "hi"')).toBe('KEY="he said "hi""');
235
+ ctx.expect(normalizeEnvLine('KEY=back\\slash')).toBe('KEY="back\\slash"');
236
236
  },
237
237
  },
238
238
  {
@@ -0,0 +1,29 @@
1
+ // migrateTarget: the desktop leg of the brand root's `omega migrate`. Desktop
2
+ // carries no legacy move today, yet exposes the same entry as every framework,
3
+ // so the manager's walk never special-cases a framework name.
4
+
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+
9
+ const SRC = path.join(__dirname, '..', '..', '..');
10
+ const { migrateTarget } = require(path.join(SRC, 'commands', 'lib', 'migrate.js'));
11
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
12
+
13
+ module.exports = defineCases({
14
+ type: 'group',
15
+ layer: 'build',
16
+ description: 'migrateTarget: the desktop leg answers the shared shape, empty',
17
+ tests: [
18
+ {
19
+ name: 'report mode and execute both answer three empty lists',
20
+ run: (ctx) => {
21
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-migrate-'));
22
+
23
+ ctx.expect(migrateTarget(tmp)).toEqual({ due: [], changed: [], errors: [] });
24
+ ctx.expect(migrateTarget(tmp, { execute: true })).toEqual({ due: [], changed: [], errors: [] });
25
+ ctx.expect(fs.readdirSync(tmp)).toEqual([]);
26
+ },
27
+ },
28
+ ],
29
+ });
@@ -13,6 +13,7 @@
13
13
  const path = require('path');
14
14
  const fs = require('fs');
15
15
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
16
+ const { projectScripts } = require('../../../vendor/devkit/verb-scripts.js');
16
17
 
17
18
  module.exports = defineCases({
18
19
  type: 'suite',
@@ -26,7 +27,7 @@ module.exports = defineCases({
26
27
  const peers = pkg.peerDependencies || {};
27
28
 
28
29
  const tools = new Set();
29
- for (const script of Object.values(pkg.projectScripts || {})) {
30
+ for (const script of Object.values(projectScripts(pkg))) {
30
31
  for (const match of String(script).matchAll(/npx\s+([a-z0-9@/_-]+)/g)) {
31
32
  const tool = match[1];
32
33
  if (tool !== 'omega' && !tool.startsWith('@omega.js/')) {
@@ -44,7 +45,7 @@ module.exports = defineCases({
44
45
  name: 'the build-mode env flags live in the CLI verbs, not in the scaffolded scripts',
45
46
  run: (ctx) => {
46
47
  const pkg = require(path.join(__dirname, '..', '..', '..', '..', 'package.json'));
47
- const scripts = pkg.projectScripts || {};
48
+ const scripts = projectScripts(pkg);
48
49
 
49
50
  for (const script of Object.values(scripts)) {
50
51
  ctx.expect(String(script).includes('cross-env')).toBe(false);
@@ -58,7 +59,9 @@ module.exports = defineCases({
58
59
  ctx.expect(scripts.build).toBe('omega build');
59
60
  ctx.expect(scripts.package).toBe('omega package');
60
61
  ctx.expect(scripts['package:quick']).toBe('omega package --quick');
61
- ctx.expect(scripts.publish).toBe('omega publish');
62
+ // A single-target command is the CLI's, reached from the brand root, never a script
63
+ ctx.expect(scripts.publish).toBe(undefined);
64
+ ctx.expect(scripts.release).toBe(undefined);
62
65
  ctx.expect(scripts['release:local']).toBe('omega publish --local');
63
66
  },
64
67
  },
@@ -79,12 +82,5 @@ module.exports = defineCases({
79
82
  ctx.expect(source.includes('cross-env')).toBe(false);
80
83
  },
81
84
  },
82
- {
83
- name: 'ensure-target syncs projectScripts unconditionally — consumers heal on the next verb',
84
- run: (ctx) => {
85
- const source = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'lib', 'ensure-target.js'), 'utf8');
86
- ctx.expect(/project\.scripts\[key\] = package\.projectScripts\[key\]/.test(source)).toBe(true);
87
- },
88
- },
89
85
  ],
90
86
  });
@@ -0,0 +1,73 @@
1
+ // runner-env-write tests: the box .env writer rewrites a key in place, one line
2
+ // per key, and every case runs against its own temp home, never the box's.
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const jetpack = require('fs-jetpack');
8
+ const runnerEnv = require('../../../utils/runner-env.js');
9
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
10
+
11
+ module.exports = defineCases({
12
+ type: 'group',
13
+ layer: 'build',
14
+ description: 'runner-env writeRunnerEnvValues',
15
+ tests: [
16
+ {
17
+ name: 'a write replaces every line assigning the key, a placeholder above a set line included',
18
+ run: (ctx) => {
19
+ // dotenv reads a key's LAST line, so a later line left behind would
20
+ // win over the new value and the read-back would refuse the write.
21
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
22
+ try {
23
+ jetpack.write(path.join(home, '.env'), '# GH_TOKEN=""\nGH_TOKEN="old"\nSIGNTOOL_PATH="s"\n');
24
+ runnerEnv.writeRunnerEnvValues(home, { GH_TOKEN: 'new' });
25
+
26
+ const written = jetpack.read(path.join(home, '.env'));
27
+ ctx.expect((written.match(/^\s*#?\s*GH_TOKEN\s*=/gm) || []).length).toBe(1);
28
+ ctx.expect(written).toContain('SIGNTOOL_PATH="s"');
29
+ const back = {};
30
+ runnerEnv.loadRunnerEnv({ home, env: back });
31
+ ctx.expect(back.GH_TOKEN).toBe('new');
32
+ } finally {
33
+ jetpack.remove(home);
34
+ }
35
+ },
36
+ },
37
+ {
38
+ name: 'a write replaces a later export or indented assignment of the key',
39
+ run: (ctx) => {
40
+ for (const later of ['export GH_TOKEN="b"', ' GH_TOKEN="b"']) {
41
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
42
+ try {
43
+ jetpack.write(path.join(home, '.env'), `GH_TOKEN="a"\n${later}\n`);
44
+ runnerEnv.writeRunnerEnvValues(home, { GH_TOKEN: 'new' });
45
+
46
+ const written = jetpack.read(path.join(home, '.env'));
47
+ ctx.expect((written.match(/^\s*(?:export\s+)?GH_TOKEN\s*=/gm) || []).length).toBe(1);
48
+ const back = {};
49
+ runnerEnv.loadRunnerEnv({ home, env: back });
50
+ ctx.expect(back.GH_TOKEN).toBe('new');
51
+ } finally {
52
+ jetpack.remove(home);
53
+ }
54
+ }
55
+ },
56
+ },
57
+ {
58
+ name: 'a write replaces a multi-line quoted value whole',
59
+ run: (ctx) => {
60
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
61
+ try {
62
+ jetpack.write(path.join(home, '.env'), 'GH_TOKEN="first\nsecond"\nSIGNTOOL_PATH="s"\n');
63
+ runnerEnv.writeRunnerEnvValues(home, { GH_TOKEN: 'new' });
64
+
65
+ const written = jetpack.read(path.join(home, '.env'));
66
+ ctx.expect(written).toBe('GH_TOKEN="new"\nSIGNTOOL_PATH="s"\n');
67
+ } finally {
68
+ jetpack.remove(home);
69
+ }
70
+ },
71
+ },
72
+ ],
73
+ });
@@ -1279,16 +1279,17 @@ module.exports = defineCases({
1279
1279
  ctx.expect(back.WIN_CSC_KEY_PASSWORD).toBe('p in spaces #1');
1280
1280
  ctx.expect(back.NEW_KEY).toBe('appended');
1281
1281
 
1282
- // A value carrying a double quote cannot be written at all — dotenv
1283
- // has no escape for it — so it is refused naming the key and the char.
1282
+ // A `"` is written raw and reads back whole. A quote followed by # would
1283
+ // not, so it is refused naming the key, never the value, file untouched.
1284
+ runnerEnv.writeRunnerEnvValues(home, { WIN_CSC_KEY_PASSWORD: 'p in "quotes"' });
1285
+ delete back.WIN_CSC_KEY_PASSWORD;
1286
+ runnerEnv.loadRunnerEnv({ home, env: back });
1287
+ ctx.expect(back.WIN_CSC_KEY_PASSWORD).toBe('p in "quotes"');
1284
1288
  let refused;
1285
- try {
1286
- runnerEnv.writeRunnerEnvValues(home, { WIN_CSC_KEY_PASSWORD: 'p in "quotes"' });
1287
- } catch (e) { refused = e; }
1288
- ctx.expect(refused).toBeDefined();
1289
+ try { runnerEnv.writeRunnerEnvValues(home, { WIN_CSC_KEY_PASSWORD: 'p "x" #tail' }); } catch (e) { refused = e; }
1289
1290
  ctx.expect(refused.message).toContain('WIN_CSC_KEY_PASSWORD');
1290
- ctx.expect(refused.message).toContain('"');
1291
- ctx.expect(jetpack.read(path.join(home, '.env'))).toContain('WIN_CSC_KEY_PASSWORD="p in spaces #1"');
1291
+ ctx.expect(refused.message).not.toContain('#tail');
1292
+ ctx.expect(jetpack.read(path.join(home, '.env'))).toContain('WIN_CSC_KEY_PASSWORD="p in "quotes""');
1292
1293
  } finally {
1293
1294
  jetpack.remove(home);
1294
1295
  }
@@ -12,6 +12,7 @@ const path = require('path');
12
12
  const fs = require('fs');
13
13
  const os = require('os');
14
14
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
15
+ const { verbScripts } = require('../../../vendor/devkit/verb-scripts.js');
15
16
 
16
17
  const SRC = path.join(__dirname, '..', '..', '..');
17
18
  const SETUP_PATH = path.join(SRC, 'commands', 'lib', 'ensure-target.js');
@@ -134,6 +135,32 @@ module.exports = defineCases({
134
135
  }
135
136
  },
136
137
  },
138
+ {
139
+ name: 'the verb scripts derive from the verb table, `start` among them',
140
+ run: async (ctx) => {
141
+ const tmp = stageProject(CONSUMER_PKG);
142
+
143
+ try {
144
+ await inProject(tmp, async (setup) => {
145
+ setup.setupScripts();
146
+
147
+ const { scripts } = JSON.parse(fs.readFileSync(path.join(tmp, 'package.json'), 'utf8'));
148
+ const derived = verbScripts('@omega.js/desktop');
149
+ for (const [verb, command] of Object.entries(derived)) {
150
+ ctx.expect(scripts[verb]).toBe(command);
151
+ }
152
+ ctx.expect(scripts.start).toBe('omega dev');
153
+ ctx.expect(scripts.gulp).toBe('gulp');
154
+
155
+ // No verb script is typed by hand: the manifest declares only what the table cannot derive
156
+ const declared = require(path.join(SRC, '..', 'package.json')).projectScripts;
157
+ ctx.expect(Object.keys(declared).filter((key) => key in derived)).toEqual([]);
158
+ });
159
+ } finally {
160
+ fs.rmSync(tmp, { recursive: true, force: true });
161
+ }
162
+ },
163
+ },
137
164
  {
138
165
  name: 'a target still carrying the `npx omega` spelling heals to the bare verb (#748)',
139
166
  run: async (ctx) => {
@@ -48,8 +48,8 @@ module.exports = defineCases({
48
48
  name: 'desktop refinements apply only with target: desktop',
49
49
  run: (ctx) => {
50
50
  const cfg = { ...VALID, startup: { mode: 'tray-only' } };
51
- // Without a target, startup.mode is just an unknown key — silent.
52
- ctx.expect(validateConfig(cfg).errors).toEqual([]);
51
+ // Without a target, startup.mode is a key nobody declared: the strict schema names it.
52
+ ctx.expect(validateConfig(cfg).errors.map((e) => e.split(' ')[0])).toEqual(['config.startup.mode']);
53
53
  // With the desktop target, the enum refinement fires.
54
54
  const { errors } = validateConfig(cfg, { target: 'desktop' });
55
55
  ctx.expect(errors.some((e) => e.includes('startup.mode') && e.includes('not allowed'))).toBe(true);
@@ -70,6 +70,17 @@ module.exports = defineCases({
70
70
  ctx.expect(errors.some((e) => e.includes('looks like a secret'))).toBe(true);
71
71
  },
72
72
  },
73
+ {
74
+ // The boot (main.js) and the audit validate a BAKED config, which carries
75
+ // the build facts; only `decorated: true` declares them.
76
+ name: 'a baked config passes as decorated, and its build facts fail an authored load',
77
+ run: (ctx) => {
78
+ const baked = { ...VALID, runtime: 'electron', environment: 'production', version: '1.0.0', buildTime: 1, target: 'desktop' };
79
+ ctx.expect(validateConfig(baked, { target: 'desktop', decorated: true }).errors).toEqual([]);
80
+ const authored = validateConfig(baked, { target: 'desktop' }).errors.map((e) => e.split(' ')[0]);
81
+ ctx.expect(authored).toEqual(['config.runtime', 'config.environment', 'config.version', 'config.buildTime', 'config.target']);
82
+ },
83
+ },
73
84
  {
74
85
  name: 'formatErrors renders a numbered block',
75
86
  run: (ctx) => {
@@ -10,6 +10,10 @@
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
13
+ const { runTestVerbUnderOuterTee } = require('../../../vendor/devkit/test/run-verb-under-tee.js');
14
+
15
+ const COMMANDS = path.join(__dirname, '..', '..', '..', 'commands');
16
+ const PREFIX = 'desktop-test-verb-';
13
17
 
14
18
  module.exports = defineCases({
15
19
  type: 'suite',
@@ -39,5 +43,21 @@ module.exports = defineCases({
39
43
  ctx.expect(line).toBe(marker);
40
44
  },
41
45
  },
46
+ {
47
+ // The verb takes off only the layer its own attach pushed: a declined
48
+ // attach pushes none, and a blind pop would eat the tee underneath.
49
+ name: 'a verb whose attach declined leaves the outer tee receiving (no-match exit)',
50
+ run: async (ctx) => {
51
+ const { outer } = await runTestVerbUnderOuterTee({ commands: COMMANDS, prefix: PREFIX, positionals: ['project:does-not-exist'] });
52
+ ctx.expect(outer.includes('after the verb')).toBe(true);
53
+ },
54
+ },
55
+ {
56
+ name: 'a verb whose attach declined leaves the outer tee receiving (green exit)',
57
+ run: async (ctx) => {
58
+ const { outer } = await runTestVerbUnderOuterTee({ commands: COMMANDS, prefix: PREFIX, positionals: [] });
59
+ ctx.expect(outer.includes('after the verb')).toBe(true);
60
+ },
61
+ },
42
62
  ],
43
63
  });
@@ -49,7 +49,7 @@ module.exports = defineCases({
49
49
  },
50
50
  {
51
51
  // wave-5 F4: ipc.on used to return ipcRenderer (stripped to a useless
52
- // object across the contextBridge) — docs/ipc.md promises an unsubscribe fn.
52
+ // object across the contextBridge) — docs/desktop/ipc.md promises an unsubscribe fn.
53
53
  name: 'ipc.on returns an unsubscribe function (docs contract)',
54
54
  run: (ctx) => {
55
55
  const off = window.desktop.ipc.on('omega-test:noop-channel', () => {});