@cats-inc/cats-platform 0.2.8 → 0.3.5

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 (205) hide show
  1. package/README.md +25 -1
  2. package/build/desktop/bootstrapPage.d.ts.map +1 -1
  3. package/build/desktop/bootstrapPage.js +26 -129
  4. package/build/desktop/bootstrapPage.js.map +1 -1
  5. package/build/desktop/cliInventoryProbe.d.ts +6 -0
  6. package/build/desktop/cliInventoryProbe.d.ts.map +1 -1
  7. package/build/desktop/cliInventoryProbe.js +6 -3
  8. package/build/desktop/cliInventoryProbe.js.map +1 -1
  9. package/build/desktop/contracts.d.ts +2 -0
  10. package/build/desktop/contracts.d.ts.map +1 -1
  11. package/build/desktop/contracts.js.map +1 -1
  12. package/build/desktop/main.js +95 -41
  13. package/build/desktop/main.js.map +1 -1
  14. package/build/desktop/preload.cjs +8 -6
  15. package/build/desktop/preload.cjs.map +1 -1
  16. package/build/desktop/providerManager.d.ts +46 -0
  17. package/build/desktop/providerManager.d.ts.map +1 -0
  18. package/build/desktop/providerManager.js +220 -0
  19. package/build/desktop/providerManager.js.map +1 -0
  20. package/build/desktop/providerSelection.d.ts +1 -0
  21. package/build/desktop/providerSelection.d.ts.map +1 -1
  22. package/build/desktop/providerSelection.js +7 -1
  23. package/build/desktop/providerSelection.js.map +1 -1
  24. package/build/desktop/setupAssets.d.ts.map +1 -1
  25. package/build/desktop/setupAssets.js +12 -0
  26. package/build/desktop/setupAssets.js.map +1 -1
  27. package/build/desktop/setupAudit.d.ts +3 -1
  28. package/build/desktop/setupAudit.d.ts.map +1 -1
  29. package/build/desktop/setupAudit.js +12 -8
  30. package/build/desktop/setupAudit.js.map +1 -1
  31. package/build/desktop/setupBridge.js +1 -1
  32. package/build/desktop/setupBridge.js.map +1 -1
  33. package/build/desktop/setupPrerequisites.d.ts +11 -0
  34. package/build/desktop/setupPrerequisites.d.ts.map +1 -0
  35. package/build/desktop/setupPrerequisites.js +47 -0
  36. package/build/desktop/setupPrerequisites.js.map +1 -0
  37. package/build/renderer/assets/ActiveSessionPermissionChip-BugyYoVY.js +1 -0
  38. package/build/renderer/assets/{App-BZA23O2J.js → App-1MFRMMld.js} +1 -1
  39. package/build/renderer/assets/{App-YqprEL2n.js → App-BWgnfutX.js} +1 -1
  40. package/build/renderer/assets/{App-rGw9J8PZ.js → App-Cm_tok6f.js} +1 -1
  41. package/build/renderer/assets/{WorkspaceAppRoutes-BCHCikmJ.js → WorkspaceAppRoutes-B8U5FWao.js} +1 -1
  42. package/build/renderer/assets/app-CqnsY5ed.css +1 -0
  43. package/build/renderer/assets/app-DTDTNtih.js +85 -0
  44. package/build/renderer/assets/{client-BdGHAUoK.js → client-DREGSZqQ.js} +1 -1
  45. package/build/renderer/assets/conversationSidebarRecentEntries-y_bEg3Cp.js +6 -0
  46. package/build/renderer/assets/{screenshotOverlay-DfBcolX-.js → screenshotOverlay-EF07cXvw.js} +1 -1
  47. package/build/renderer/assets/{useDraftSessionChips-D9BX6X92.js → useDraftSessionChips-BD6ZGG_y.js} +1 -1
  48. package/build/renderer/desktop/overlay/index.html +2 -2
  49. package/build/renderer/index.html +3 -3
  50. package/build/server/app/renderer/auth/api.d.ts.map +1 -1
  51. package/build/server/app/renderer/auth/api.js +4 -1
  52. package/build/server/app/renderer/auth/api.js.map +1 -1
  53. package/build/server/app/renderer/providerCatalogClient.d.ts.map +1 -1
  54. package/build/server/app/renderer/providerCatalogClient.js +19 -6
  55. package/build/server/app/renderer/providerCatalogClient.js.map +1 -1
  56. package/build/server/app/renderer/providerClientInvalidation.d.ts +6 -0
  57. package/build/server/app/renderer/providerClientInvalidation.d.ts.map +1 -0
  58. package/build/server/app/renderer/providerClientInvalidation.js +12 -0
  59. package/build/server/app/renderer/providerClientInvalidation.js.map +1 -0
  60. package/build/server/app/renderer/providerReadLoop.d.ts +5 -0
  61. package/build/server/app/renderer/providerReadLoop.d.ts.map +1 -0
  62. package/build/server/app/renderer/providerReadLoop.js +52 -0
  63. package/build/server/app/renderer/providerReadLoop.js.map +1 -0
  64. package/build/server/app/renderer/providerRegistryClient.d.ts +2 -1
  65. package/build/server/app/renderer/providerRegistryClient.d.ts.map +1 -1
  66. package/build/server/app/renderer/providerRegistryClient.js +33 -17
  67. package/build/server/app/renderer/providerRegistryClient.js.map +1 -1
  68. package/build/server/app/server/authGatePolicy.js +7 -0
  69. package/build/server/app/server/authGatePolicy.js.map +1 -1
  70. package/build/server/app/server/shutdown.d.ts.map +1 -1
  71. package/build/server/app/server/shutdown.js +3 -1
  72. package/build/server/app/server/shutdown.js.map +1 -1
  73. package/build/server/app/server/startup.d.ts +4 -3
  74. package/build/server/app/server/startup.d.ts.map +1 -1
  75. package/build/server/app/server/startup.js +8 -0
  76. package/build/server/app/server/startup.js.map +1 -1
  77. package/build/server/app/server/subscribeRoutes.d.ts.map +1 -1
  78. package/build/server/app/server/subscribeRoutes.js +5 -3
  79. package/build/server/app/server/subscribeRoutes.js.map +1 -1
  80. package/build/server/design/components/providerModelFieldsSupport.d.ts +2 -21
  81. package/build/server/design/components/providerModelFieldsSupport.d.ts.map +1 -1
  82. package/build/server/design/components/providerModelFieldsSupport.js +32 -36
  83. package/build/server/design/components/providerModelFieldsSupport.js.map +1 -1
  84. package/build/server/design/components/useProviderCatalogState.d.ts +1 -2
  85. package/build/server/design/components/useProviderCatalogState.d.ts.map +1 -1
  86. package/build/server/design/components/useProviderCatalogState.js +71 -152
  87. package/build/server/design/components/useProviderCatalogState.js.map +1 -1
  88. package/build/server/design/components/useProviderModelFieldActions.js +2 -2
  89. package/build/server/design/components/useProviderModelFieldActions.js.map +1 -1
  90. package/build/server/design/components/useProviderRegistryState.d.ts +1 -7
  91. package/build/server/design/components/useProviderRegistryState.d.ts.map +1 -1
  92. package/build/server/design/components/useProviderRegistryState.js +41 -91
  93. package/build/server/design/components/useProviderRegistryState.js.map +1 -1
  94. package/build/server/design/components/useProviderTargetReconciliation.d.ts +1 -0
  95. package/build/server/design/components/useProviderTargetReconciliation.d.ts.map +1 -1
  96. package/build/server/design/components/useProviderTargetReconciliation.js +1 -1
  97. package/build/server/design/components/useProviderTargetReconciliation.js.map +1 -1
  98. package/build/server/index.js +30 -0
  99. package/build/server/index.js.map +1 -1
  100. package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts +2 -1
  101. package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts.map +1 -1
  102. package/build/server/platform/orchestration/entitySubscriptions/channel.js +3 -18
  103. package/build/server/platform/orchestration/entitySubscriptions/channel.js.map +1 -1
  104. package/build/server/platform/orchestration/entitySubscriptions/index.d.ts +1 -0
  105. package/build/server/platform/orchestration/entitySubscriptions/index.d.ts.map +1 -1
  106. package/build/server/platform/orchestration/entitySubscriptions/index.js.map +1 -1
  107. package/build/server/products/chat/api/routeSupport.d.ts +9 -1
  108. package/build/server/products/chat/api/routeSupport.d.ts.map +1 -1
  109. package/build/server/products/chat/api/routeSupport.js +22 -0
  110. package/build/server/products/chat/api/routeSupport.js.map +1 -1
  111. package/build/server/products/chat/state/model/readModels.d.ts +1 -0
  112. package/build/server/products/chat/state/model/readModels.d.ts.map +1 -1
  113. package/build/server/products/chat/state/model/readModels.js +2 -1
  114. package/build/server/products/chat/state/model/readModels.js.map +1 -1
  115. package/build/server/products/shared/renderer/api/appShell.d.ts +1 -0
  116. package/build/server/products/shared/renderer/api/appShell.d.ts.map +1 -1
  117. package/build/server/products/shared/renderer/api/appShell.js +9 -0
  118. package/build/server/products/shared/renderer/api/appShell.js.map +1 -1
  119. package/build/server/runtime/client.d.ts.map +1 -1
  120. package/build/server/runtime/client.js +5 -1
  121. package/build/server/runtime/client.js.map +1 -1
  122. package/build/server/runtime/setup.d.ts +4 -0
  123. package/build/server/runtime/setup.d.ts.map +1 -1
  124. package/build/server/runtime/setup.js +3 -1
  125. package/build/server/runtime/setup.js.map +1 -1
  126. package/build/server/server/routes/providers.d.ts.map +1 -1
  127. package/build/server/server/routes/providers.js +21 -50
  128. package/build/server/server/routes/providers.js.map +1 -1
  129. package/build/server/shared/cliInteraction.d.ts +17 -0
  130. package/build/server/shared/cliInteraction.d.ts.map +1 -0
  131. package/build/server/shared/cliInteraction.js +102 -0
  132. package/build/server/shared/cliInteraction.js.map +1 -0
  133. package/build/server/shared/desktopRecoveryBridge.d.ts +2 -1
  134. package/build/server/shared/desktopRecoveryBridge.d.ts.map +1 -1
  135. package/build/server/shared/desktopRecoveryBridge.js.map +1 -1
  136. package/build/server/shared/providerCatalogData.d.ts +168 -96
  137. package/build/server/shared/providerCatalogData.d.ts.map +1 -1
  138. package/build/server/shared/providerCatalogData.js +68 -42
  139. package/build/server/shared/providerCatalogData.js.map +1 -1
  140. package/build/server/shared/providerRegistryWarnings.d.ts +4 -0
  141. package/build/server/shared/providerRegistryWarnings.d.ts.map +1 -1
  142. package/build/server/shared/providerRegistryWarnings.js +7 -0
  143. package/build/server/shared/providerRegistryWarnings.js.map +1 -1
  144. package/package.json +13 -6
  145. package/packages/provider-setup/manager.d.ts +79 -0
  146. package/packages/provider-setup/manager.js +441 -0
  147. package/scripts/README.md +23 -0
  148. package/scripts/build-desktop-installer.mjs +124 -16
  149. package/scripts/dev-server.mjs +4 -1
  150. package/scripts/linux/install-github-cli.sh +5 -0
  151. package/scripts/linux/install-node.sh +9 -3
  152. package/scripts/linux/node-cli-common.sh +87 -48
  153. package/scripts/linux/provider-cli-common.sh +80 -105
  154. package/scripts/linux/provider-version-common.sh +247 -0
  155. package/scripts/linux/test-linux-package-smoke.sh +2 -0
  156. package/scripts/linux/verified-npm-update.sh +20 -0
  157. package/scripts/macos/install-github-cli.sh +5 -0
  158. package/scripts/macos/install-node.sh +9 -3
  159. package/scripts/macos/node-cli-common.sh +87 -48
  160. package/scripts/macos/provider-cli-common.sh +80 -105
  161. package/scripts/macos/provider-version-common.sh +247 -0
  162. package/scripts/macos/test-macos-package-smoke.sh +2 -0
  163. package/scripts/macos/verified-npm-update.sh +20 -0
  164. package/scripts/testing/conversation-navigation-smoke-electron.cjs +134 -0
  165. package/scripts/testing/conversation-navigation-smoke-renderer.tsx +102 -0
  166. package/scripts/testing/conversation-navigation-smoke.mjs +67 -0
  167. package/scripts/validate-release-assets.mjs +4 -1
  168. package/scripts/windows/Install-Antigravity.ps1 +26 -3
  169. package/scripts/windows/Install-ClaudeCode.ps1 +25 -7
  170. package/scripts/windows/Install-CursorAgent.ps1 +45 -30
  171. package/scripts/windows/Install-Devin.ps1 +16 -47
  172. package/scripts/windows/Install-GitHubCli.ps1 +4 -0
  173. package/scripts/windows/Install-Goose.ps1 +18 -1
  174. package/scripts/windows/Install-Grok.ps1 +14 -1
  175. package/scripts/windows/Install-Junie.ps1 +32 -11
  176. package/scripts/windows/Install-KiroCli.ps1 +37 -13
  177. package/scripts/windows/Install-MetaCli.ps1 +8 -1
  178. package/scripts/windows/Install-Node.ps1 +6 -1
  179. package/scripts/windows/Setup-NodeGlobalPrefix.ps1 +10 -0
  180. package/scripts/windows/Test-WindowsInstallerSmoke.ps1 +2 -0
  181. package/scripts/windows/_HiddenProcess.ps1 +14 -3
  182. package/scripts/windows/_NativeInstallerSupport.ps1 +308 -0
  183. package/scripts/windows/_NpmCliInstaller.ps1 +35 -14
  184. package/scripts/windows/_PackagedUninstall.ps1 +9 -1
  185. package/scripts/windows/_VerifiedNpmUpdate.ps1 +25 -0
  186. package/build/renderer/assets/ActiveSessionPermissionChip-CAgN-tXv.js +0 -1
  187. package/build/renderer/assets/app-BV3UAQkR.css +0 -1
  188. package/build/renderer/assets/app-CVPqqfZo.js +0 -45
  189. package/build/renderer/assets/conversationSidebarRecentEntries-iqaLqA1n.js +0 -6
  190. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts +0 -48
  191. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts.map +0 -1
  192. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js +0 -535
  193. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js.map +0 -1
  194. package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts +0 -13
  195. package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts.map +0 -1
  196. package/build/server/design/components/useProviderRegistryAutoRecheck.js +0 -70
  197. package/build/server/design/components/useProviderRegistryAutoRecheck.js.map +0 -1
  198. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts +0 -19
  199. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts.map +0 -1
  200. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js +0 -53
  201. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js.map +0 -1
  202. package/build/server/products/shared/renderer/api/providers.d.ts +0 -17
  203. package/build/server/products/shared/renderer/api/providers.d.ts.map +0 -1
  204. package/build/server/products/shared/renderer/api/providers.js +0 -50
  205. package/build/server/products/shared/renderer/api/providers.js.map +0 -1
@@ -37,8 +37,9 @@ Options:
37
37
  --skip-mobile Skip the mobile bundle (\`expo export\`). Also honored via
38
38
  CATS_SKIP_MOBILE=1 in the environment or .env.
39
39
  --release Build an official release package: embed the release
40
- descriptor, require platform signing credentials, and
41
- allow signing identity discovery. Requires a GitHub
40
+ descriptor, require platform signing credentials (plus
41
+ macOS notarization credentials), and allow signing
42
+ identity discovery. Requires a GitHub
42
43
  Actions tag run whose tag matches the package version.
43
44
  Also honored via CATS_DESKTOP_RELEASE_MODE=1.
44
45
  --preview Build an unsupported unsigned GitHub prerelease preview.
@@ -46,6 +47,12 @@ Options:
46
47
  omits the official descriptor, and disables signing
47
48
  identity discovery. Also honored via
48
49
  CATS_DESKTOP_PREVIEW_MODE=1.
50
+ --sign Let local packaging use a signing identity from the
51
+ OS keychain. Off by default so an unconfigured machine
52
+ cannot pick up an unrelated certificate. Ignored by
53
+ --release and --preview, which always sign when their
54
+ credentials are present. Also honored via
55
+ CATS_DESKTOP_SIGN_LOCAL=1.
49
56
  --publish <never|always> Publish policy, default never. Publishing requires
50
57
  --release or --preview plus a GitHub token. Also honored
51
58
  via CATS_DESKTOP_PUBLISH.
@@ -99,6 +106,7 @@ export function parseArgs(argv, env = process.env) {
99
106
  let skipMobile = parseBooleanFlag(env.CATS_SKIP_MOBILE);
100
107
  let releaseMode = parseBooleanFlag(env.CATS_DESKTOP_RELEASE_MODE);
101
108
  let previewMode = parseBooleanFlag(env.CATS_DESKTOP_PREVIEW_MODE);
109
+ let allowLocalSigning = parseBooleanFlag(env.CATS_DESKTOP_SIGN_LOCAL);
102
110
  let publish = resolvePublishPolicy(env.CATS_DESKTOP_PUBLISH);
103
111
  // GITHUB_REF_NAME is a runner-provided default. A workflow `env:` block
104
112
  // cannot override anything in the GITHUB_ namespace, and a preview run is
@@ -118,6 +126,7 @@ export function parseArgs(argv, env = process.env) {
118
126
  skipMobile,
119
127
  releaseMode,
120
128
  previewMode,
129
+ allowLocalSigning,
121
130
  publish,
122
131
  tag,
123
132
  ...(appsLock ? { appsLock } : {}),
@@ -144,6 +153,14 @@ export function parseArgs(argv, env = process.env) {
144
153
  previewMode = false;
145
154
  continue;
146
155
  }
156
+ if (value === '--sign') {
157
+ allowLocalSigning = true;
158
+ continue;
159
+ }
160
+ if (value === '--no-sign') {
161
+ allowLocalSigning = false;
162
+ continue;
163
+ }
147
164
  if (value === '--publish') {
148
165
  publish = resolvePublishPolicy(argv[index + 1] ?? '');
149
166
  index += 1;
@@ -194,6 +211,7 @@ export function parseArgs(argv, env = process.env) {
194
211
  skipMobile,
195
212
  releaseMode,
196
213
  previewMode,
214
+ allowLocalSigning,
197
215
  publish,
198
216
  tag,
199
217
  ...(appsLock ? { appsLock } : {}),
@@ -241,28 +259,45 @@ async function resolveCommandInvocation(command, args) {
241
259
  };
242
260
  }
243
261
 
262
+ const MACOS_NOTARIZATION_CREDENTIAL_KEYS = [
263
+ 'APPLE_API_KEY',
264
+ 'APPLE_API_KEY_ID',
265
+ 'APPLE_API_ISSUER',
266
+ ];
267
+
244
268
  const SIGNING_CREDENTIAL_KEYS = [
245
269
  'WIN_CSC_LINK',
246
270
  'CSC_LINK',
247
271
  'WIN_CSC_KEY_PASSWORD',
248
272
  'CSC_KEY_PASSWORD',
273
+ ...MACOS_NOTARIZATION_CREDENTIAL_KEYS,
249
274
  ];
250
275
 
251
276
  /**
252
277
  * Local and test packaging must never reach for a signing identity, because an
253
278
  * unconfigured machine would otherwise pick up an unrelated certificate from
254
- * the OS keychain. Release mode leaves identity discovery to the workflow so a
255
- * configured certificate can actually be used.
279
+ * the OS keychain. Both guarded workflow paths leave identity discovery alone
280
+ * so a configured certificate can actually be used.
281
+ *
282
+ * ADR-117: a preview is signed on any platform whose credentials exist, so
283
+ * preview mode is a signing path too. Signing is artifact trust; it says
284
+ * nothing about whether the build claims official release identity.
285
+ *
286
+ * `--sign` is the local escape hatch. It is an explicit request rather than an
287
+ * inference from the environment, because a developer machine may hold
288
+ * unrelated certificates.
256
289
  *
257
- * Empty credential variables are dropped in both modes: electron-builder treats
290
+ * Empty credential variables are dropped in every mode: electron-builder treats
258
291
  * an empty CSC_LINK as a relative path and resolves it against the project
259
292
  * root.
260
293
  */
261
294
  export function buildInstallerEnvironment(baseEnv = process.env, options = {}) {
262
- const releaseMode = options.releaseMode === true;
295
+ const signingAllowed = options.releaseMode === true
296
+ || options.previewMode === true
297
+ || options.allowLocalSigning === true;
263
298
  const env = { ...baseEnv };
264
299
 
265
- if (!releaseMode) {
300
+ if (!signingAllowed) {
266
301
  env.CSC_IDENTITY_AUTO_DISCOVERY = 'false';
267
302
  }
268
303
 
@@ -286,6 +321,22 @@ export function hasMacosSigningCredentials(env = process.env) {
286
321
  return typeof link === 'string' && link.trim() !== '';
287
322
  }
288
323
 
324
+ /**
325
+ * electron-builder reads notarization credentials straight from the
326
+ * environment and only warns when they are absent, so a misconfigured release
327
+ * would otherwise produce a signed but un-notarized app. Gatekeeper rejects
328
+ * that on every machine except the one that built it.
329
+ *
330
+ * App Store Connect API keys are used rather than an Apple ID and
331
+ * app-specific password: they are revocable on their own and survive an Apple
332
+ * ID password change, which a CI secret has no way to notice.
333
+ */
334
+ export function hasMacosNotarizationCredentials(env = process.env) {
335
+ return MACOS_NOTARIZATION_CREDENTIAL_KEYS.every(
336
+ (key) => typeof env[key] === 'string' && env[key].trim() !== '',
337
+ );
338
+ }
339
+
289
340
  /**
290
341
  * Signing is a release gate, not a nice-to-have.
291
342
  *
@@ -302,16 +353,53 @@ export function resolveSigningProblems({ env = process.env, target } = {}) {
302
353
  }];
303
354
  }
304
355
 
305
- if (target === 'macos' && !hasMacosSigningCredentials(env)) {
306
- return [{
307
- code: 'release_macos_signing_missing',
308
- message: 'An official macOS release requires CSC_LINK.',
309
- }];
356
+ if (target === 'macos') {
357
+ const problems = [];
358
+ if (!hasMacosSigningCredentials(env)) {
359
+ problems.push({
360
+ code: 'release_macos_signing_missing',
361
+ message: 'An official macOS release requires CSC_LINK.',
362
+ });
363
+ }
364
+ if (!hasMacosNotarizationCredentials(env)) {
365
+ problems.push({
366
+ code: 'release_macos_notarization_missing',
367
+ message: 'An official macOS release requires APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER.',
368
+ });
369
+ }
370
+ return problems;
310
371
  }
311
372
 
312
373
  return [];
313
374
  }
314
375
 
376
+ /**
377
+ * ADR-117: trust is resolved per platform from the credentials actually
378
+ * present. The build states which it got, because a silently unsigned artifact
379
+ * is indistinguishable from a signed one until a user tries to install it.
380
+ */
381
+ export function describeArtifactTrust(target, env = process.env) {
382
+ if (target === 'linux') {
383
+ return 'unsigned (Linux packages are not signed)';
384
+ }
385
+
386
+ if (target === 'windows') {
387
+ return hasWindowsSigningCredentials(env)
388
+ ? 'signed'
389
+ : 'unsigned (no Windows certificate configured)';
390
+ }
391
+
392
+ const signed = hasMacosSigningCredentials(env);
393
+ const notarized = hasMacosNotarizationCredentials(env);
394
+ if (signed && notarized) {
395
+ return 'signed and notarized';
396
+ }
397
+ if (signed) {
398
+ return 'signed but NOT notarized (no App Store Connect API key) — Gatekeeper will reject the download';
399
+ }
400
+ return 'unsigned (no Developer ID certificate)';
401
+ }
402
+
315
403
  /**
316
404
  * Release and preview publication are only meaningful inside the guarded
317
405
  * workflow. Anything else could publish from an unreviewed tree, so the
@@ -648,8 +736,14 @@ export function electronBuilderArgs(target, archOverride, formatOverride, option
648
736
  }
649
737
 
650
738
  const releaseMode = options.releaseMode === true;
739
+ const previewMode = options.previewMode === true;
740
+ // Signing overrides key off artifact trust, not release identity, so both
741
+ // guarded paths qualify. Local packaging never does, even with `--sign`:
742
+ // an ad-hoc local build has no reason to reach Apple's notary service.
743
+ const signedBuild = releaseMode || previewMode;
651
744
  const publish = resolvePublishPolicy(options.publish ?? 'never');
652
745
  const signWindowsExecutable = options.signWindowsExecutable === true;
746
+ const notarizeMacos = options.notarizeMacos === true;
653
747
  const formats = normalizeFormats(target, formatOverride);
654
748
  const platformFlag = target === 'windows' ? '--win' : target === 'macos' ? '--mac' : '--linux';
655
749
  const args = ['electron-builder', platformFlag];
@@ -667,10 +761,17 @@ export function electronBuilderArgs(target, archOverride, formatOverride, option
667
761
  // package.json pins signAndEditExecutable to false so unsigned local builds
668
762
  // avoid the winCodeSign download. A release build with real credentials has
669
763
  // to opt back in, and only then.
670
- if (releaseMode && target === 'windows' && signWindowsExecutable) {
764
+ if (signedBuild && target === 'windows' && signWindowsExecutable) {
671
765
  args.push('-c.win.signAndEditExecutable=true');
672
766
  }
673
767
 
768
+ // package.json pins mac.notarize to false so an unsigned preview never waits
769
+ // on Apple and a half-configured environment cannot quietly ship an
770
+ // un-notarized build. A release build with real credentials opts back in.
771
+ if (signedBuild && target === 'macos' && notarizeMacos) {
772
+ args.push('-c.mac.notarize=true');
773
+ }
774
+
674
775
  args.push('--publish', publish);
675
776
  return args;
676
777
  }
@@ -687,7 +788,11 @@ async function main() {
687
788
  // in an OS temporary directory so retries never redownload a moving asset.
688
789
  const selectedApps = parsed.appsLock ? await resolveAppLock(resolve(parsed.appsLock)) : null;
689
790
  const appLockForBuild = selectedApps ? await materializeAppSelection(selectedApps) : null;
690
- const envOptions = { releaseMode: parsed.releaseMode };
791
+ const envOptions = {
792
+ releaseMode: parsed.releaseMode,
793
+ previewMode: parsed.previewMode,
794
+ allowLocalSigning: parsed.allowLocalSigning,
795
+ };
691
796
 
692
797
  if (parsed.releaseMode && parsed.previewMode) {
693
798
  throw new Error('--release and --preview are mutually exclusive build modes.');
@@ -717,10 +822,11 @@ async function main() {
717
822
  .join('\n')}`,
718
823
  );
719
824
  }
720
- const buildKind = parsed.releaseMode ? 'official' : 'unsigned preview';
825
+ const buildKind = parsed.releaseMode ? 'official' : 'preview';
721
826
  process.stdout.write(
722
827
  `[build-desktop-installer] ${buildKind} ${resolvedTarget} build for `
723
- + `${process.env.GITHUB_REF_NAME} (publish=${parsed.publish}).\n`,
828
+ + `${process.env.GITHUB_REF_NAME} (publish=${parsed.publish}, `
829
+ + `trust=${describeArtifactTrust(resolvedTarget, process.env)}).\n`,
724
830
  );
725
831
  }
726
832
 
@@ -786,8 +892,10 @@ async function main() {
786
892
  'npx',
787
893
  electronBuilderArgs(resolvedTarget, parsed.arch, parsed.format, {
788
894
  releaseMode: parsed.releaseMode,
895
+ previewMode: parsed.previewMode,
789
896
  publish: parsed.publish,
790
897
  signWindowsExecutable: hasWindowsSigningCredentials(process.env),
898
+ notarizeMacos: hasMacosNotarizationCredentials(process.env),
791
899
  }),
792
900
  PROJECT_ROOT,
793
901
  {},
@@ -26,7 +26,10 @@ let crashCount = 0;
26
26
 
27
27
  function spawnServer() {
28
28
  intentionalRestart = false;
29
- child = spawn(process.execPath, ['--import', 'tsx', entry], {
29
+ child = spawn(process.execPath, [
30
+ '--import', 'tsx', entry,
31
+ '--startup-mode', 'app-managed', '--managed-by', 'dev-server', '--ready-output', 'plain',
32
+ ], {
30
33
  stdio: 'inherit',
31
34
  env: process.env,
32
35
  });
@@ -60,6 +60,11 @@ declare -a applied_changes=()
60
60
  declare -a warnings=()
61
61
  declare -a manual_steps=()
62
62
 
63
+ # A Desktop-launched helper may inherit PATH from before gh was installed.
64
+ # Include our fallback destination and the supported Homebrew locations in this
65
+ # process only; check mode must not edit the user's shell profile.
66
+ export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
67
+
63
68
  resolve_gh_path() {
64
69
  command -v gh 2>/dev/null || true
65
70
  }
@@ -90,7 +90,7 @@ is_node_installed() {
90
90
  if [ "$skip_node_probe" = 'true' ]; then
91
91
  return 1
92
92
  fi
93
- command -v node >/dev/null 2>&1
93
+ command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1
94
94
  }
95
95
 
96
96
  json_array_from_lines() {
@@ -149,6 +149,12 @@ JSON
149
149
  exit "$exit_code"
150
150
  }
151
151
 
152
+ # Desktop's environment may predate nvm installation. Load it before read-only
153
+ # detection too; a bundled or standalone node without npm is not this substrate.
154
+ if [ "$install_state" = 'auto' ] && [ "$skip_node_probe" = 'false' ]; then
155
+ load_nvm
156
+ fi
157
+
152
158
  # Mode: check
153
159
  if [ "$mode" = 'check' ]; then
154
160
  if is_node_installed; then
@@ -214,10 +220,10 @@ case "$mode" in
214
220
  ;;
215
221
  esac
216
222
 
217
- if command -v node >/dev/null 2>&1; then
223
+ if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
218
224
  emit_result 'ready' 'true' 0
219
225
  else
220
- warnings+=('Node binary still missing after install.')
226
+ warnings+=('Node.js or npm is still missing after install.')
221
227
  manual_steps+=('Open a new terminal so PATH picks up the nvm-managed node binary.')
222
228
  emit_result 'failed' 'false' 1
223
229
  fi
@@ -8,6 +8,7 @@ readonly CATS_PLATFORM_UNIX_NODE_COMMON_SH=1
8
8
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
9
  # shellcheck source=provider-cli-common.sh
10
10
  . "$SCRIPT_DIR/provider-cli-common.sh"
11
+ . "$SCRIPT_DIR/verified-npm-update.sh"
11
12
 
12
13
  normalize_npm_registry() {
13
14
  case "$1" in
@@ -52,25 +53,51 @@ ensure_node_and_npm() {
52
53
  printf 'npm is required but was not found.\n' >&2
53
54
  return 1
54
55
  fi
56
+ # Desktop invokes each helper in a fresh non-login shell. Read the configured
57
+ # prefix, never source arbitrary shell startup files to recover its commands.
58
+ local configured_prefix
59
+ configured_prefix="$(npm config get prefix 2>/dev/null || true)"
60
+ [ -z "$configured_prefix" ] || prepend_path_if_missing "$configured_prefix/bin"
61
+ return 0
62
+ }
63
+
64
+ has_explicit_npm_prefix() {
65
+ [ -z "${NPM_CONFIG_PREFIX:-${npm_config_prefix:-}}" ] || return 0
66
+ local npmrc="${NPM_CONFIG_USERCONFIG:-${npm_config_userconfig:-$HOME/.npmrc}}"
67
+ [ -f "$npmrc" ] && grep -qE '^[[:space:]]*prefix[[:space:]]*=' "$npmrc"
68
+ }
69
+
70
+ preferred_npm_prefix() {
71
+ local existing canonical_home
72
+ existing="$(npm config get prefix 2>/dev/null || true)"
73
+ if [ -n "$existing" ] && has_explicit_npm_prefix; then printf '%s\n' "$existing"; return 0; fi
74
+ if [ -d "$existing" ] && [ -w "$existing" ]; then
75
+ existing="$(cd -P -- "$existing" && pwd -P)"
76
+ canonical_home="$(cd -P -- "$HOME" && pwd -P)"
77
+ case "$existing" in "$canonical_home"/*) printf '%s\n' "$existing"; return 0 ;; esac
78
+ fi
79
+ printf '%s\n' "$HOME/.npm-global"
55
80
  }
56
81
 
57
82
  ensure_npm_global_path_export() {
58
- local shell_rc="$1"
83
+ local shell_rc="$1" prefix="${2:-$HOME/.npm-global}" export_line
84
+ printf -v export_line 'export PATH=%q:$PATH' "$prefix/bin"
59
85
  append_line_if_missing "$shell_rc" '# Added by cats-platform self-hosted npm helpers' '# Added by cats-platform self-hosted npm helpers'
60
- append_line_if_missing "$shell_rc" 'export PATH="$HOME/.npm-global/bin:$PATH"' 'export PATH="$HOME/.npm-global/bin:$PATH"'
61
- prepend_path_if_missing "$HOME/.npm-global/bin"
86
+ append_line_if_missing "$shell_rc" "$export_line" "$export_line"
87
+ prepend_path_if_missing "$prefix/bin"
62
88
  }
63
89
 
64
90
  node_prefix_ready() {
65
- local prefix
91
+ local prefix desired_prefix
66
92
  local registry
67
93
  local shell_rc
68
94
 
69
95
  prefix="$(npm config get prefix 2>/dev/null || printf '')"
70
96
  registry="$(normalize_npm_registry "$(npm config get registry 2>/dev/null || printf '')")"
71
97
  shell_rc="$(detect_shell_rc)"
98
+ desired_prefix="$(preferred_npm_prefix)"
72
99
 
73
- if nvm_is_active; then
100
+ if nvm_is_active && ! has_explicit_npm_prefix; then
74
101
  if npmrc_has_prefix_conflict; then
75
102
  return 1
76
103
  fi
@@ -78,7 +105,7 @@ node_prefix_ready() {
78
105
  return $?
79
106
  fi
80
107
 
81
- if [ "$prefix" != "$HOME/.npm-global" ]; then
108
+ if [ "$prefix" != "$desired_prefix" ]; then
82
109
  return 1
83
110
  fi
84
111
 
@@ -86,11 +113,11 @@ node_prefix_ready() {
86
113
  return 1
87
114
  fi
88
115
 
89
- if [[ ":$PATH:" == *":$HOME/.npm-global/bin:"* ]]; then
116
+ if [[ ":$PATH:" == *":$desired_prefix/bin:"* ]]; then
90
117
  return 0
91
118
  fi
92
119
 
93
- grep -Fq 'export PATH="$HOME/.npm-global/bin:$PATH"' "$shell_rc" 2>/dev/null
120
+ return 1
94
121
  }
95
122
 
96
123
  node_prefix_help() {
@@ -119,7 +146,7 @@ run_node_prefix_setup() {
119
146
  local force='false'
120
147
  local emit_json='false'
121
148
  local shell_rc
122
- local prefix
149
+ local prefix desired_prefix
123
150
  local registry
124
151
  local status='ready'
125
152
  local execution_mode='apply'
@@ -170,6 +197,7 @@ run_node_prefix_setup() {
170
197
  execution_mode='apply'
171
198
  fi
172
199
 
200
+ desired_prefix="$HOME/.npm-global"
173
201
  load_nvm_if_present
174
202
  if ! ensure_node_and_npm; then
175
203
  if [ "$emit_json" = 'true' ]; then
@@ -178,7 +206,7 @@ run_node_prefix_setup() {
178
206
  printf '"mode":"%s",' "$execution_mode"
179
207
  printf '"status":"failed",'
180
208
  printf '"restartRequired":false,'
181
- printf '"desiredPrefix":"%s",' "$(json_escape "$HOME/.npm-global")"
209
+ printf '"desiredPrefix":"%s",' "$(json_escape "$desired_prefix")"
182
210
  printf '"shellRc":"%s",' "$(json_escape "$(detect_shell_rc)")"
183
211
  printf '"plannedActions":[],'
184
212
  printf '"appliedChanges":[],'
@@ -191,10 +219,11 @@ run_node_prefix_setup() {
191
219
  fi
192
220
 
193
221
  shell_rc="$(detect_shell_rc)"
222
+ desired_prefix="$(preferred_npm_prefix)"
194
223
  prefix="$(npm config get prefix 2>/dev/null || printf '')"
195
224
  registry="$(normalize_npm_registry "$(npm config get registry 2>/dev/null || printf '')")"
196
225
 
197
- if nvm_is_active; then
226
+ if nvm_is_active && ! has_explicit_npm_prefix; then
198
227
  if npmrc_has_prefix_conflict; then
199
228
  planned_actions+=('clear_npm_prefix_conflict')
200
229
  fi
@@ -202,13 +231,13 @@ run_node_prefix_setup() {
202
231
  planned_actions+=('set_npm_registry')
203
232
  fi
204
233
  else
205
- if [ "$prefix" != "$HOME/.npm-global" ]; then
234
+ if [ "$prefix" != "$desired_prefix" ]; then
206
235
  planned_actions+=('set_npm_prefix')
207
236
  fi
208
237
  if [ "$registry" != 'https://registry.npmjs.org/' ]; then
209
238
  planned_actions+=('set_npm_registry')
210
239
  fi
211
- if [[ ":$PATH:" != *":$HOME/.npm-global/bin:"* ]]; then
240
+ if [[ ":$PATH:" != *":$desired_prefix/bin:"* ]]; then
212
241
  planned_actions+=('repair_npm_global_path')
213
242
  fi
214
243
  fi
@@ -224,7 +253,7 @@ run_node_prefix_setup() {
224
253
  printf '"mode":"check",'
225
254
  printf '"status":"%s",' "$status"
226
255
  printf '"restartRequired":false,'
227
- printf '"desiredPrefix":"%s",' "$(json_escape "$HOME/.npm-global")"
256
+ printf '"desiredPrefix":"%s",' "$(json_escape "$desired_prefix")"
228
257
  printf '"shellRc":"%s",' "$(json_escape "$shell_rc")"
229
258
  printf '"plannedActions":'
230
259
  json_string_array "${planned_actions[@]}"
@@ -251,7 +280,7 @@ run_node_prefix_setup() {
251
280
  printf '"mode":"%s",' "$execution_mode"
252
281
  printf '"status":"ready",'
253
282
  printf '"restartRequired":false,'
254
- printf '"desiredPrefix":"%s",' "$(json_escape "$HOME/.npm-global")"
283
+ printf '"desiredPrefix":"%s",' "$(json_escape "$desired_prefix")"
255
284
  printf '"shellRc":"%s",' "$(json_escape "$shell_rc")"
256
285
  printf '"plannedActions":[],'
257
286
  printf '"appliedChanges":[],'
@@ -265,7 +294,7 @@ run_node_prefix_setup() {
265
294
  return 0
266
295
  fi
267
296
 
268
- if nvm_is_active; then
297
+ if nvm_is_active && ! has_explicit_npm_prefix; then
269
298
  nvm use --delete-prefix "$(node -v)" --silent >/dev/null 2>&1 || true
270
299
  npm config delete prefix >/dev/null 2>&1 || true
271
300
  npm config delete globalconfig >/dev/null 2>&1 || true
@@ -275,10 +304,10 @@ run_node_prefix_setup() {
275
304
  applied_changes+=('clear_npm_prefix_conflict')
276
305
  fi
277
306
  else
278
- mkdir -p "$HOME/.npm-global"
279
- npm config set prefix "$HOME/.npm-global"
307
+ mkdir -p "$desired_prefix"
308
+ npm config set prefix "$desired_prefix"
280
309
  npm config set registry 'https://registry.npmjs.org/'
281
- ensure_npm_global_path_export "$shell_rc"
310
+ ensure_npm_global_path_export "$shell_rc" "$desired_prefix"
282
311
  applied_changes+=('set_npm_prefix' 'set_npm_registry' 'repair_npm_global_path')
283
312
  fi
284
313
 
@@ -294,7 +323,7 @@ run_node_prefix_setup() {
294
323
  printf '"mode":"%s",' "$execution_mode"
295
324
  printf '"status":"%s",' "$status"
296
325
  printf '"restartRequired":false,'
297
- printf '"desiredPrefix":"%s",' "$(json_escape "$HOME/.npm-global")"
326
+ printf '"desiredPrefix":"%s",' "$(json_escape "$desired_prefix")"
298
327
  printf '"shellRc":"%s",' "$(json_escape "$shell_rc")"
299
328
  printf '"plannedActions":'
300
329
  json_string_array "${planned_actions[@]}"
@@ -340,8 +369,8 @@ remove_superseded_npm_packages() {
340
369
  while IFS= read -r legacy; do
341
370
  [ -n "$legacy" ] || continue
342
371
  npm list -g "$legacy" --depth=0 >/dev/null 2>&1 || continue
343
- printf 'Removing superseded package %s (replaced by %s).\n' "$legacy" "$package_name"
344
- npm uninstall -g "$legacy" >/dev/null 2>&1 || true
372
+ printf 'Removing superseded package %s (replaced by %s).\n' "$legacy" "$package_name" >&2
373
+ npm uninstall -g "$legacy" >/dev/null 2>&1 || return 1
345
374
  done <<EOF
346
375
  $(node_cli_superseded_packages "$package_name")
347
376
  EOF
@@ -1081,9 +1110,6 @@ run_npm_cli_provider() {
1081
1110
  shift
1082
1111
  done
1083
1112
 
1084
- if [ "$check_only" != 'true' ] && [ "$uninstall" != 'true' ]; then
1085
- remove_superseded_npm_packages "$package_name"
1086
- fi
1087
1113
 
1088
1114
  if [ "$force" = 'true' ]; then
1089
1115
  upgrade='false'
@@ -1134,7 +1160,17 @@ run_npm_cli_provider() {
1134
1160
  if command -v "$command_name" >/dev/null 2>&1 || npm list -g "$package_name" --depth=0 >/dev/null 2>&1; then
1135
1161
  installed='true'
1136
1162
  command_path="$(command -v "$command_name" 2>/dev/null || true)"
1137
- detected_version="$(npm list -g --depth=0 --json "$package_name" 2>/dev/null | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | head -1)"
1163
+ detected_version="$( (npm list -g --depth=0 --json "$package_name" 2>/dev/null || true) | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{console.log(JSON.parse(s).dependencies?.[process.argv[1]]?.version||"")}catch{}})' "$package_name")"
1164
+ fi
1165
+
1166
+ if [ "$installed" = true ] && [ -z "$detected_version" ]; then
1167
+ local legacy
1168
+ while IFS= read -r legacy; do
1169
+ [ -n "$legacy" ] || continue
1170
+ if npm list -g "$legacy" --depth=0 >/dev/null 2>&1; then installed=false; break; fi
1171
+ done <<LEGACY_EOF
1172
+ $(node_cli_superseded_packages "$package_name")
1173
+ LEGACY_EOF
1138
1174
  fi
1139
1175
 
1140
1176
  if [ "$execution_mode" = 'uninstall' ]; then
@@ -1208,13 +1244,15 @@ run_npm_cli_provider() {
1208
1244
  return 0
1209
1245
  fi
1210
1246
 
1211
- local is_outdated='false'
1212
- if [ "$upgrade" = 'true' ] || [ "$force" = 'true' ]; then
1213
- if [ "$installed" = 'true' ]; then
1214
- if npm outdated -g "$package_name" --json 2>/dev/null | grep -q "\"$package_name\""; then
1215
- is_outdated='true'
1216
- fi
1217
- fi
1247
+ local is_outdated=false expected_version=''
1248
+ if [ "$check_only" = false ] && [ "$uninstall" = false ] && [ "$dry_run" = false ] && { [ "$upgrade" = true ] || [ "$force" = true ] || [ "$installed" = false ]; }; then
1249
+ if [ "$upgrade" = true ]; then cats_upgrade_npm || return 1; fi
1250
+ expected_version="$(npm view "${package_name}@latest" version --fetch-retries=0 --fetch-timeout=10000 --loglevel=error)" || { printf 'Provider version query failed.\n' >&2; return 1; }
1251
+ [[ "$expected_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]] || { printf 'Invalid provider version metadata.\n' >&2; return 1; }
1252
+ local comparison
1253
+ comparison="$(cats_version_compare "$detected_version" "$expected_version")"
1254
+ if [ -z "$detected_version" ] || [ "$comparison" = -1 ]; then is_outdated=true
1255
+ elif [ "$comparison" = 0 ] && [[ "$detected_version" == *-* ]] && [[ "$expected_version" != *-* ]]; then is_outdated=true; fi
1218
1256
  fi
1219
1257
 
1220
1258
  local planned_action='skip'
@@ -1293,25 +1331,26 @@ run_npm_cli_provider() {
1293
1331
  fi
1294
1332
 
1295
1333
  local applied=()
1296
- case "$planned_action" in
1297
- install)
1298
- npm install -g --include=optional "$package_name" >/dev/null
1299
- applied+=("${package_name}:install")
1300
- ;;
1301
- upgrade)
1302
- npm install -g --include=optional "${package_name}@latest" >/dev/null
1303
- applied+=("${package_name}:upgrade")
1304
- ;;
1305
- reinstall)
1306
- npm install -g --include=optional "$package_name" --force >/dev/null
1307
- applied+=("${package_name}:reinstall")
1308
- ;;
1309
- esac
1334
+ if [ "$dry_run" = true ]; then
1335
+ printf '{"helper":"%s","mode":"%s","status":"preview","plannedActions":["%s:%s"],"appliedChanges":[],"warnings":[],"manualSteps":[],"interruptions":[]}\n' "$helper_id" "$execution_mode" "$package_name" "$planned_action"
1336
+ return 0
1337
+ fi
1338
+ if [ "$check_only" != 'true' ] && [ "$dry_run" != 'true' ]; then
1339
+ remove_superseded_npm_packages "$package_name"
1340
+ fi
1341
+ npm install -g --include=optional --engine-strict --fetch-retries=0 --fetch-timeout=30000 "${package_name}@${expected_version}" >/dev/null || return 1
1342
+ applied+=("${package_name}:${planned_action}")
1343
+ hash -r
1310
1344
 
1311
1345
  local final_command_path
1312
1346
  final_command_path="$(command -v "$command_name" 2>/dev/null || true)"
1313
1347
  local final_version
1314
- final_version="$(npm list -g --depth=0 --json "$package_name" 2>/dev/null | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | head -1)"
1348
+ final_version="$(npm list -g --depth=0 --json "$package_name" 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{console.log(JSON.parse(s).dependencies?.[process.argv[1]]?.version||"")}catch{}})' "$package_name")"
1349
+
1350
+ if [ -z "$final_command_path" ] || [ "$final_version" != "$expected_version" ]; then
1351
+ printf 'Provider verification failed. Check npm prefix and PATH.\n' >&2
1352
+ return 1
1353
+ fi
1315
1354
 
1316
1355
  if [ "$emit_json" = 'true' ]; then
1317
1356
  printf '{'