@cats-inc/cats-platform 0.1.1 → 0.1.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 (152) hide show
  1. package/.env.example +2 -2
  2. package/README.md +6 -0
  3. package/build/desktop/bootstrapPage.d.ts.map +1 -1
  4. package/build/desktop/bootstrapPage.js +81 -4
  5. package/build/desktop/bootstrapPage.js.map +1 -1
  6. package/build/desktop/config.d.ts +11 -1
  7. package/build/desktop/config.d.ts.map +1 -1
  8. package/build/desktop/config.js +5 -2
  9. package/build/desktop/config.js.map +1 -1
  10. package/build/desktop/contracts.d.ts +66 -11
  11. package/build/desktop/contracts.d.ts.map +1 -1
  12. package/build/desktop/contracts.js +50 -1
  13. package/build/desktop/contracts.js.map +1 -1
  14. package/build/desktop/hostState.d.ts +4 -3
  15. package/build/desktop/hostState.d.ts.map +1 -1
  16. package/build/desktop/hostState.js +22 -30
  17. package/build/desktop/hostState.js.map +1 -1
  18. package/build/desktop/hostVersion.d.ts +1 -0
  19. package/build/desktop/hostVersion.d.ts.map +1 -1
  20. package/build/desktop/hostVersion.js +29 -5
  21. package/build/desktop/hostVersion.js.map +1 -1
  22. package/build/desktop/main.js +234 -37
  23. package/build/desktop/main.js.map +1 -1
  24. package/build/desktop/packaging.d.ts.map +1 -1
  25. package/build/desktop/packaging.js +10 -4
  26. package/build/desktop/packaging.js.map +1 -1
  27. package/build/desktop/preload.cjs +29 -0
  28. package/build/desktop/preload.cjs.map +1 -1
  29. package/build/desktop/processSupervisor.d.ts +2 -0
  30. package/build/desktop/processSupervisor.d.ts.map +1 -1
  31. package/build/desktop/processSupervisor.js +28 -13
  32. package/build/desktop/processSupervisor.js.map +1 -1
  33. package/build/desktop/readiness.d.ts +2 -2
  34. package/build/desktop/readiness.d.ts.map +1 -1
  35. package/build/desktop/readiness.js +2 -2
  36. package/build/desktop/readiness.js.map +1 -1
  37. package/build/desktop/releaseDescriptor.d.ts +85 -0
  38. package/build/desktop/releaseDescriptor.d.ts.map +1 -0
  39. package/build/desktop/releaseDescriptor.js +246 -0
  40. package/build/desktop/releaseDescriptor.js.map +1 -0
  41. package/build/desktop/tray.d.ts +1 -0
  42. package/build/desktop/tray.d.ts.map +1 -1
  43. package/build/desktop/tray.js +26 -0
  44. package/build/desktop/tray.js.map +1 -1
  45. package/build/desktop/trayMenu.d.ts +22 -1
  46. package/build/desktop/trayMenu.d.ts.map +1 -1
  47. package/build/desktop/trayMenu.js +61 -0
  48. package/build/desktop/trayMenu.js.map +1 -1
  49. package/build/desktop/updateInstallHandoff.d.ts +30 -0
  50. package/build/desktop/updateInstallHandoff.d.ts.map +1 -0
  51. package/build/desktop/updateInstallHandoff.js +39 -0
  52. package/build/desktop/updateInstallHandoff.js.map +1 -0
  53. package/build/desktop/updateIpc.d.ts +42 -0
  54. package/build/desktop/updateIpc.d.ts.map +1 -0
  55. package/build/desktop/updateIpc.js +71 -0
  56. package/build/desktop/updateIpc.js.map +1 -0
  57. package/build/desktop/updateManager.d.ts +76 -0
  58. package/build/desktop/updateManager.d.ts.map +1 -0
  59. package/build/desktop/updateManager.js +342 -0
  60. package/build/desktop/updateManager.js.map +1 -0
  61. package/build/desktop/updateNotifications.d.ts +64 -0
  62. package/build/desktop/updateNotifications.d.ts.map +1 -0
  63. package/build/desktop/updateNotifications.js +143 -0
  64. package/build/desktop/updateNotifications.js.map +1 -0
  65. package/build/desktop/updaterAdapter.d.ts +72 -0
  66. package/build/desktop/updaterAdapter.d.ts.map +1 -0
  67. package/build/desktop/updaterAdapter.js +198 -0
  68. package/build/desktop/updaterAdapter.js.map +1 -0
  69. package/build/renderer/assets/{ActiveSessionPermissionChip-BYr8cDpb.js → ActiveSessionPermissionChip-BoDkKkmi.js} +1 -1
  70. package/build/renderer/assets/{App-BbuTWT6Y.js → App-Bs1eRarv.js} +1 -1
  71. package/build/renderer/assets/{App-B13AzNko.js → App-C4S1Wa9f.js} +1 -1
  72. package/build/renderer/assets/{App-n-RtGh42.js → App-CNJj5gi9.js} +1 -1
  73. package/build/renderer/assets/{WorkspaceAppRoutes-BktWrQK-.js → WorkspaceAppRoutes-CFAuhWGF.js} +1 -1
  74. package/build/renderer/assets/app-uJJx-k1S.js +43 -0
  75. package/build/renderer/assets/{conversationSidebarRecentEntries-BNPz3X1V.js → conversationSidebarRecentEntries-CPGopayy.js} +1 -1
  76. package/build/renderer/assets/{useDraftSessionChips-3z_Je4ju.js → useDraftSessionChips-BsOdYfWw.js} +1 -1
  77. package/build/renderer/index.html +1 -1
  78. package/build/server/app/renderer/settings/settingsDesktopErrorLabels.d.ts +5 -0
  79. package/build/server/app/renderer/settings/settingsDesktopErrorLabels.d.ts.map +1 -0
  80. package/build/server/app/renderer/settings/settingsDesktopErrorLabels.js +16 -0
  81. package/build/server/app/renderer/settings/settingsDesktopErrorLabels.js.map +1 -0
  82. package/build/server/app/renderer/settings/settingsDesktopPreferences.d.ts +11 -0
  83. package/build/server/app/renderer/settings/settingsDesktopPreferences.d.ts.map +1 -0
  84. package/build/server/app/renderer/settings/settingsDesktopPreferences.js +30 -0
  85. package/build/server/app/renderer/settings/settingsDesktopPreferences.js.map +1 -0
  86. package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.d.ts +31 -0
  87. package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.d.ts.map +1 -0
  88. package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.js +101 -0
  89. package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.js.map +1 -0
  90. package/build/server/app/server/index.js +1 -1
  91. package/build/server/app/server/index.js.map +1 -1
  92. package/build/server/app/server/platformSetupRoutes.d.ts.map +1 -1
  93. package/build/server/app/server/platformSetupRoutes.js +42 -5
  94. package/build/server/app/server/platformSetupRoutes.js.map +1 -1
  95. package/build/server/app/server/requestRouter.d.ts.map +1 -1
  96. package/build/server/app/server/requestRouter.js +2 -0
  97. package/build/server/app/server/requestRouter.js.map +1 -1
  98. package/build/server/design/components/runtimeChips.d.ts +1 -1
  99. package/build/server/design/components/runtimeChips.d.ts.map +1 -1
  100. package/build/server/design/components/runtimeChips.js +4 -1
  101. package/build/server/design/components/runtimeChips.js.map +1 -1
  102. package/build/server/index.js +12 -1
  103. package/build/server/index.js.map +1 -1
  104. package/build/server/platform/auth/sessionSecretProvisioning.d.ts +23 -0
  105. package/build/server/platform/auth/sessionSecretProvisioning.d.ts.map +1 -0
  106. package/build/server/platform/auth/sessionSecretProvisioning.js +291 -0
  107. package/build/server/platform/auth/sessionSecretProvisioning.js.map +1 -0
  108. package/build/server/products/code/livePreview/realProcessAdapter.d.ts.map +1 -1
  109. package/build/server/products/code/livePreview/realProcessAdapter.js +7 -1
  110. package/build/server/products/code/livePreview/realProcessAdapter.js.map +1 -1
  111. package/build/server/products/code/state/appExport.d.ts.map +1 -1
  112. package/build/server/products/code/state/appExport.js +2 -0
  113. package/build/server/products/code/state/appExport.js.map +1 -1
  114. package/build/server/shared/desktopRecoveryBridge.d.ts +46 -0
  115. package/build/server/shared/desktopRecoveryBridge.d.ts.map +1 -1
  116. package/build/server/shared/desktopRecoveryBridge.js +20 -0
  117. package/build/server/shared/desktopRecoveryBridge.js.map +1 -1
  118. package/build/server/shared/i18n/catalogs/en.d.ts.map +1 -1
  119. package/build/server/shared/i18n/catalogs/en.js +37 -0
  120. package/build/server/shared/i18n/catalogs/en.js.map +1 -1
  121. package/build/server/shared/i18n/catalogs/zh-TW.d.ts.map +1 -1
  122. package/build/server/shared/i18n/catalogs/zh-TW.js +35 -0
  123. package/build/server/shared/i18n/catalogs/zh-TW.js.map +1 -1
  124. package/build/server/shared/i18n/index.d.ts +35 -0
  125. package/build/server/shared/i18n/index.d.ts.map +1 -1
  126. package/build/server/shared/i18n/messageKeys.d.ts +35 -0
  127. package/build/server/shared/i18n/messageKeys.d.ts.map +1 -1
  128. package/build/server/shared/i18n/messageKeys.js +35 -0
  129. package/build/server/shared/i18n/messageKeys.js.map +1 -1
  130. package/build/server/shared/loadProjectEnvFile.d.ts.map +1 -1
  131. package/build/server/shared/loadProjectEnvFile.js +18 -19
  132. package/build/server/shared/loadProjectEnvFile.js.map +1 -1
  133. package/build/server/shared/platformChildProcessEnv.d.ts +2 -0
  134. package/build/server/shared/platformChildProcessEnv.d.ts.map +1 -0
  135. package/build/server/shared/platformChildProcessEnv.js +22 -0
  136. package/build/server/shared/platformChildProcessEnv.js.map +1 -0
  137. package/package.json +19 -27
  138. package/scripts/build-desktop-installer.mjs +438 -37
  139. package/scripts/build-test-ui.mjs +13 -2
  140. package/scripts/generate-desktop-release-descriptor.mjs +258 -0
  141. package/scripts/validate-release-assets.mjs +422 -0
  142. package/scripts/validate-release-version.mjs +217 -0
  143. package/scripts/windows/Test-WindowsInstallerSmoke.ps1 +55 -1
  144. package/build/desktop/update.d.ts +0 -24
  145. package/build/desktop/update.d.ts.map +0 -1
  146. package/build/desktop/update.js +0 -173
  147. package/build/desktop/update.js.map +0 -1
  148. package/build/renderer/assets/app-DnVQMpW_.js +0 -43
  149. package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.d.ts +0 -5
  150. package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.d.ts.map +0 -1
  151. package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.js +0 -11
  152. package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.js.map +0 -1
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // Script: generate-desktop-release-descriptor.mjs
4
+ // Description: Emit the non-secret provenance descriptor that marks a packaged
5
+ // build as an official tag-gated desktop release. The desktop
6
+ // host refuses to advertise self-update without it.
7
+ //
8
+ // Usage: node scripts/generate-desktop-release-descriptor.mjs [options]
9
+ //
10
+ // Options:
11
+ // --tag <vX.Y.Z> Release tag. Defaults to GITHUB_REF_NAME.
12
+ // --commit <sha> Source commit. Defaults to GITHUB_SHA.
13
+ // --repository <o/r> GitHub owner/repo. Defaults to GITHUB_REPOSITORY.
14
+ // --platform <windows|macos|linux>
15
+ // Target platform. Defaults to the current platform.
16
+ // --output <path> Descriptor path. Defaults to the packaged location.
17
+ // --help Show this help text.
18
+ //
19
+ // The descriptor contains no secrets. It is provenance only: which tag, which
20
+ // commit, which platform, which channel, and which update provider produced
21
+ // the package. Local packaging never generates it.
22
+
23
+ import process from 'node:process';
24
+ import { mkdir, writeFile } from 'node:fs/promises';
25
+ import { dirname, resolve } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ import { parseStableReleaseTag } from './validate-release-version.mjs';
29
+
30
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
31
+ const PROJECT_ROOT = resolve(dirname(SCRIPT_PATH), '..');
32
+
33
+ // The descriptor ships inside build/desktop, which package.json already lists
34
+ // in build.files, so it lands in the asar without extra packaging rules.
35
+ export const DESCRIPTOR_RELATIVE_PATH = 'build/desktop/release-descriptor.json';
36
+
37
+ export const DESCRIPTOR_SCHEMA_VERSION = 1;
38
+
39
+ const COMMIT_SHA = /^[0-9a-f]{40}$/;
40
+ const GITHUB_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
41
+
42
+ const PLATFORM_ALIASES = {
43
+ windows: 'windows',
44
+ win32: 'windows',
45
+ win: 'windows',
46
+ macos: 'macos',
47
+ darwin: 'macos',
48
+ mac: 'macos',
49
+ linux: 'linux',
50
+ };
51
+
52
+ function printHelp() {
53
+ process.stdout.write(`Usage: node scripts/generate-desktop-release-descriptor.mjs [options]
54
+
55
+ Options:
56
+ --tag <vX.Y.Z> Release tag. Defaults to GITHUB_REF_NAME.
57
+ --commit <sha> Source commit. Defaults to GITHUB_SHA.
58
+ --repository <o/r> GitHub owner/repo. Defaults to GITHUB_REPOSITORY.
59
+ --platform <windows|macos|linux>
60
+ Target platform. Defaults to the current platform.
61
+ --output <path> Descriptor path. Defaults to ${DESCRIPTOR_RELATIVE_PATH}.
62
+ --help Show this help text.
63
+
64
+ Writes the non-secret official release descriptor consumed by the desktop
65
+ update capability check. Local packaging must not run this script.
66
+ `);
67
+ }
68
+
69
+ // Explicit rather than derived from the flag name, because --runtime-commit
70
+ // does not slice into a valid camelCase key.
71
+ const VALUE_FLAGS = {
72
+ '--tag': 'tag',
73
+ '--commit': 'commit',
74
+ '--repository': 'repository',
75
+ '--platform': 'platform',
76
+ '--runtime-commit': 'runtimeCommit',
77
+ '--kind': 'kind',
78
+ '--output': 'output',
79
+ };
80
+
81
+ export const RELEASE_KINDS = ['official', 'preview'];
82
+
83
+ export function resolveDescriptorPlatform(value) {
84
+ const candidate = typeof value === 'string' ? value.trim().toLowerCase() : '';
85
+ return PLATFORM_ALIASES[candidate] ?? null;
86
+ }
87
+
88
+ export function parseArgs(argv, env = process.env) {
89
+ const options = {
90
+ help: false,
91
+ tag: (env.GITHUB_REF_NAME ?? '').trim(),
92
+ commit: (env.GITHUB_SHA ?? '').trim(),
93
+ repository: (env.GITHUB_REPOSITORY ?? '').trim(),
94
+ platform: env.CATS_DESKTOP_RELEASE_PLATFORM ?? process.platform,
95
+ runtimeCommit: (env.CATS_DESKTOP_RUNTIME_COMMIT ?? '').trim(),
96
+ // Official unless the caller says otherwise, so a missing flag can never
97
+ // silently downgrade a stable release into an unsigned preview.
98
+ kind: (env.CATS_DESKTOP_RELEASE_KIND ?? 'official').trim(),
99
+ output: DESCRIPTOR_RELATIVE_PATH,
100
+ };
101
+
102
+ for (let index = 0; index < argv.length; index += 1) {
103
+ const value = argv[index];
104
+ if (value === '--help' || value === '-h') {
105
+ return { ...options, help: true };
106
+ }
107
+ const key = VALUE_FLAGS[value];
108
+ if (key !== undefined) {
109
+ options[key] = (argv[index + 1] ?? '').trim();
110
+ index += 1;
111
+ continue;
112
+ }
113
+ throw new Error(`Unknown option: ${value}`);
114
+ }
115
+
116
+ return options;
117
+ }
118
+
119
+ /**
120
+ * Builds the descriptor and reports every invalid input at once. Returning
121
+ * problems instead of throwing keeps the release workflow output readable when
122
+ * more than one environment variable is missing.
123
+ */
124
+ export function buildReleaseDescriptor({
125
+ tag,
126
+ commit,
127
+ repository,
128
+ platform,
129
+ runtimeCommit,
130
+ kind = 'official',
131
+ generatedAt = null,
132
+ } = {}) {
133
+ const problems = [];
134
+
135
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
136
+ if (!RELEASE_KINDS.includes(normalizedKind)) {
137
+ problems.push({
138
+ code: 'descriptor_kind_invalid',
139
+ message: `Release kind '${kind ?? ''}' is not official or preview.`,
140
+ });
141
+ }
142
+
143
+ const parsedTag = parseStableReleaseTag(tag);
144
+ if (!parsedTag.ok) {
145
+ problems.push({ code: `descriptor_${parsedTag.code}`, message: parsedTag.message });
146
+ }
147
+
148
+ const normalizedCommit = typeof commit === 'string' ? commit.trim().toLowerCase() : '';
149
+ if (!COMMIT_SHA.test(normalizedCommit)) {
150
+ problems.push({
151
+ code: 'descriptor_commit_invalid',
152
+ message: `Source commit '${commit ?? ''}' is not a full 40-character sha.`,
153
+ });
154
+ }
155
+
156
+ const normalizedRepository = typeof repository === 'string' ? repository.trim() : '';
157
+ if (!GITHUB_REPOSITORY.test(normalizedRepository)) {
158
+ problems.push({
159
+ code: 'descriptor_repository_invalid',
160
+ message: `Repository '${repository ?? ''}' is not in owner/name form.`,
161
+ });
162
+ }
163
+
164
+ const normalizedPlatform = resolveDescriptorPlatform(platform);
165
+ if (normalizedPlatform === null) {
166
+ problems.push({
167
+ code: 'descriptor_platform_invalid',
168
+ message: `Platform '${platform ?? ''}' is not windows, macos, or linux.`,
169
+ });
170
+ }
171
+
172
+ // The packaged runtime is a separate repository, so the platform commit
173
+ // alone does not identify what shipped. Recording it makes a re-run of the
174
+ // same Cats tag verifiable rather than merely hopeful.
175
+ const normalizedRuntimeCommit = typeof runtimeCommit === 'string'
176
+ ? runtimeCommit.trim().toLowerCase()
177
+ : '';
178
+ if (!COMMIT_SHA.test(normalizedRuntimeCommit)) {
179
+ problems.push({
180
+ code: 'descriptor_runtime_commit_invalid',
181
+ message: `Runtime commit '${runtimeCommit ?? ''}' is not a full 40-character sha.`,
182
+ });
183
+ }
184
+
185
+ if (problems.length > 0) {
186
+ return { ok: false, descriptor: null, problems };
187
+ }
188
+
189
+ return {
190
+ ok: true,
191
+ problems: [],
192
+ descriptor: {
193
+ schemaVersion: DESCRIPTOR_SCHEMA_VERSION,
194
+ kind: normalizedKind,
195
+ tag: parsedTag.tag,
196
+ version: parsedTag.version,
197
+ commit: normalizedCommit,
198
+ platform: normalizedPlatform,
199
+ // SPEC-111 restricts Phase 1 publishing to the stable channel, so the
200
+ // descriptor states it rather than accepting it as an input.
201
+ channel: 'stable',
202
+ provider: 'github_release',
203
+ repository: normalizedRepository,
204
+ runtimeCommit: normalizedRuntimeCommit,
205
+ generatedAt,
206
+ },
207
+ };
208
+ }
209
+
210
+ export function serializeReleaseDescriptor(descriptor) {
211
+ return `${JSON.stringify(descriptor, null, 2)}\n`;
212
+ }
213
+
214
+ export async function writeReleaseDescriptor(descriptor, outputPath, projectRoot = PROJECT_ROOT) {
215
+ const absolutePath = resolve(projectRoot, outputPath);
216
+ await mkdir(dirname(absolutePath), { recursive: true });
217
+ await writeFile(absolutePath, serializeReleaseDescriptor(descriptor), 'utf8');
218
+ return absolutePath;
219
+ }
220
+
221
+ async function main() {
222
+ const parsed = parseArgs(process.argv.slice(2));
223
+ if (parsed.help) {
224
+ printHelp();
225
+ return;
226
+ }
227
+
228
+ const result = buildReleaseDescriptor({
229
+ tag: parsed.tag,
230
+ commit: parsed.commit,
231
+ repository: parsed.repository,
232
+ platform: parsed.platform,
233
+ runtimeCommit: parsed.runtimeCommit,
234
+ kind: parsed.kind,
235
+ generatedAt: new Date().toISOString(),
236
+ });
237
+
238
+ if (!result.ok) {
239
+ for (const problem of result.problems) {
240
+ process.stderr.write(`[release-descriptor] ${problem.code}: ${problem.message}\n`);
241
+ }
242
+ process.exitCode = 1;
243
+ return;
244
+ }
245
+
246
+ const writtenPath = await writeReleaseDescriptor(result.descriptor, parsed.output);
247
+ process.stdout.write(
248
+ `[release-descriptor] wrote ${result.descriptor.tag} `
249
+ + `(${result.descriptor.platform}) to ${writtenPath}\n`,
250
+ );
251
+ }
252
+
253
+ if (resolve(process.argv[1] ?? '') === SCRIPT_PATH) {
254
+ void main().catch((error) => {
255
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
256
+ process.exitCode = 1;
257
+ });
258
+ }
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // Script: validate-release-assets.mjs
4
+ // Description: Verify that every file referenced by the generated update
5
+ // metadata was actually produced, and that each platform's
6
+ // primary user-facing artifact is present, before a draft
7
+ // GitHub Release is published.
8
+ //
9
+ // Usage: node scripts/validate-release-assets.mjs --root <dir>
10
+ //
11
+ // Options:
12
+ // --root <dir> Directory to scan recursively. Defaults to release/.
13
+ // --json Emit the machine-readable result instead of prose.
14
+ // --help Show this help text.
15
+ //
16
+ // Exit codes:
17
+ // 0 every required artifact and every referenced file exists
18
+ // 1 metadata is missing, unreadable, or references a file that was not built
19
+
20
+ import process from 'node:process';
21
+ import { readFile, readdir, stat } from 'node:fs/promises';
22
+ import { basename, dirname, join, resolve } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ import yaml from 'js-yaml';
26
+
27
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
28
+ const PROJECT_ROOT = resolve(dirname(SCRIPT_PATH), '..');
29
+
30
+ /**
31
+ * electron-updater publishes one metadata file per platform. Each names the
32
+ * primary artifact the updater will download, so a missing metadata file means
33
+ * that platform cannot self-update even if its installer uploaded fine.
34
+ */
35
+ export const DESKTOP_RELEASE_MATRIX = [
36
+ { platform: 'windows', formats: ['nsis'], arches: ['x64'] },
37
+ // macOS ships x64 only; Apple Silicon runs it under Rosetta 2.
38
+ { platform: 'macos', formats: ['dmg', 'zip'], arches: ['x64'] },
39
+ // Linux ships an arm64 .deb. electron-updater installs it with dpkg, which
40
+ // needs elevation -- unlike the per-user Windows path.
41
+ { platform: 'linux', formats: ['deb'], arches: ['arm64'] },
42
+ ];
43
+
44
+ const METADATA_BASENAMES = {
45
+ windows: 'latest',
46
+ macos: 'latest-mac',
47
+ linux: 'latest-linux',
48
+ };
49
+
50
+ /**
51
+ * Derives the channel file each platform will emit.
52
+ *
53
+ * electron-builder suffixes the name with the architecture for anything other
54
+ * than x64, so an arm64 Linux target writes `latest-linux-arm64.yml`. Hardcoding
55
+ * the three unsuffixed names meant a retarget reported the real file as an
56
+ * unknown artifact *and* the expected file as missing -- two errors for one
57
+ * naming rule.
58
+ */
59
+ export function expectedMetadataFiles(matrix = DESKTOP_RELEASE_MATRIX) {
60
+ return matrix.map((entry) => {
61
+ const base = METADATA_BASENAMES[entry.platform];
62
+ const [arch] = entry.arches;
63
+ return arch === 'x64' || arch === 'universal' ? `${base}.yml` : `${base}-${arch}.yml`;
64
+ });
65
+ }
66
+
67
+ export const UPDATE_METADATA_FILES = expectedMetadataFiles();
68
+
69
+ export const PRIMARY_ARTIFACT_PATTERNS = [
70
+ { platform: 'windows', pattern: /\.exe$/iu },
71
+ { platform: 'macos', pattern: /\.dmg$/iu },
72
+ { platform: 'linux', pattern: /\.deb$/iu },
73
+ ];
74
+
75
+ function printHelp() {
76
+ process.stdout.write(`Usage: node scripts/validate-release-assets.mjs [options]
77
+
78
+ Options:
79
+ --root <dir> Directory to scan recursively. Defaults to release/.
80
+ --json Emit the machine-readable result instead of prose.
81
+ --help Show this help text.
82
+
83
+ Fails when generated update metadata references a file that is not present, or
84
+ when a platform's primary user-facing artifact is missing.
85
+ `);
86
+ }
87
+
88
+ export function parseArgs(argv) {
89
+ const options = { help: false, root: 'release', json: false };
90
+
91
+ for (let index = 0; index < argv.length; index += 1) {
92
+ const value = argv[index];
93
+ if (value === '--help' || value === '-h') {
94
+ return { ...options, help: true };
95
+ }
96
+ if (value === '--root') {
97
+ options.root = (argv[index + 1] ?? '').trim() || 'release';
98
+ index += 1;
99
+ continue;
100
+ }
101
+ if (value === '--json') {
102
+ options.json = true;
103
+ continue;
104
+ }
105
+ throw new Error(`Unknown option: ${value}`);
106
+ }
107
+
108
+ return options;
109
+ }
110
+
111
+ /**
112
+ * Collects the file names referenced by one metadata document. Both the
113
+ * top-level `path` and each `files[].url` are checked, because electron-updater
114
+ * reads whichever the client version supports.
115
+ */
116
+ export function collectReferencedFiles(document) {
117
+ if (typeof document !== 'object' || document === null) {
118
+ return [];
119
+ }
120
+
121
+ const referenced = new Set();
122
+ const record = document;
123
+
124
+ if (typeof record.path === 'string' && record.path.trim() !== '') {
125
+ referenced.add(basename(decodeURIComponent(record.path.trim())));
126
+ }
127
+
128
+ if (Array.isArray(record.files)) {
129
+ for (const entry of record.files) {
130
+ if (typeof entry?.url === 'string' && entry.url.trim() !== '') {
131
+ referenced.add(basename(decodeURIComponent(entry.url.trim())));
132
+ }
133
+ }
134
+ }
135
+
136
+ return [...referenced];
137
+ }
138
+
139
+ /**
140
+ * The declared release set.
141
+ *
142
+ * This is the single source of truth for what an official release may contain.
143
+ * It is not a statement that other formats or architectures are undesirable —
144
+ * it is a statement of what this release publishes, so `--publish always`
145
+ * cannot quietly upload something the release page never promised.
146
+ *
147
+ * Widening the release set is one edit here plus the matching workflow matrix
148
+ * entry. A drift test keeps the two aligned, so adding, say, a Linux arm64
149
+ * AppImage is a deliberate two-line change rather than something the validator
150
+ * refuses on principle.
151
+ */
152
+ const FORMAT_EXTENSIONS = {
153
+ nsis: ['.exe'],
154
+ dmg: ['.dmg'],
155
+ // The updater archive is named from this repository's artifactName template,
156
+ // which produces Cats-<version>-x64.zip rather than the -mac.zip default some
157
+ // electron-builder setups emit. The name carries no platform marker, so match
158
+ // it by extension and rely on the per-platform format list above.
159
+ zip: ['.zip'],
160
+ AppImage: ['.AppImage'],
161
+ deb: ['.deb'],
162
+ 'tar.gz': ['.tar.gz'],
163
+ pkg: ['.pkg'],
164
+ };
165
+
166
+ // electron-builder spells the same architecture differently per target.
167
+ const ARCH_TOKENS = {
168
+ x64: ['x64', 'x86_64', 'amd64'],
169
+ arm64: ['arm64', 'aarch64'],
170
+ ia32: ['ia32', 'i386'],
171
+ armv7l: ['armv7l'],
172
+ universal: ['universal'],
173
+ };
174
+
175
+ const ALL_ARCH_TOKENS = Object.values(ARCH_TOKENS).flat();
176
+
177
+ function releasedExtensions(matrix = DESKTOP_RELEASE_MATRIX) {
178
+ return matrix.flatMap((entry) => entry.formats.flatMap(
179
+ (format) => FORMAT_EXTENSIONS[format] ?? [],
180
+ ));
181
+ }
182
+
183
+ /**
184
+ * Resolves which platform an artifact belongs to from its extension.
185
+ *
186
+ * Architecture has to be judged per platform, not across the whole matrix. With
187
+ * Windows on x64 and Linux on arm64, a flattened token list accepts an x64 .deb
188
+ * and an arm64 .exe -- each is released *somewhere*, just not where it was built.
189
+ */
190
+ function platformForArtifact(name, matrix = DESKTOP_RELEASE_MATRIX) {
191
+ const lowered = stripBlockmap(name).toLowerCase();
192
+ for (const entry of matrix) {
193
+ for (const format of entry.formats) {
194
+ for (const extension of FORMAT_EXTENSIONS[format] ?? []) {
195
+ if (lowered.endsWith(extension.toLowerCase())) {
196
+ return entry;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+
204
+ function archTokensFor(entry) {
205
+ return entry.arches.flatMap((arch) => ARCH_TOKENS[arch] ?? [arch]);
206
+ }
207
+
208
+ function isUpdateMetadata(name) {
209
+ return UPDATE_METADATA_FILES.includes(name);
210
+ }
211
+
212
+ function stripBlockmap(name) {
213
+ return name.endsWith('.blockmap') ? name.slice(0, -'.blockmap'.length) : name;
214
+ }
215
+
216
+ export function resolveMissingPrimaryArtifacts(fileNames) {
217
+ return PRIMARY_ARTIFACT_PATTERNS
218
+ .filter(({ pattern }) => !fileNames.some((name) => pattern.test(name)))
219
+ .map(({ platform }) => platform);
220
+ }
221
+
222
+ /**
223
+ * Flags artifacts whose format is not part of the declared release set.
224
+ * Update metadata and the blockmaps that accompany a released artifact are
225
+ * always allowed.
226
+ */
227
+ export function resolveDisallowedArtifacts(fileNames, matrix = DESKTOP_RELEASE_MATRIX) {
228
+ const extensions = releasedExtensions(matrix);
229
+
230
+ return fileNames.filter((name) => {
231
+ if (isUpdateMetadata(name)) {
232
+ return false;
233
+ }
234
+ const base = stripBlockmap(name);
235
+ return !extensions.some((extension) => base.toLowerCase().endsWith(extension.toLowerCase()));
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Flags artifacts built for an architecture the declared release set does not
241
+ * include. An artifact whose name carries no recognizable architecture token is
242
+ * left alone rather than guessed at.
243
+ */
244
+ export function resolveUnreleasedArchitectureArtifacts(
245
+ fileNames,
246
+ matrix = DESKTOP_RELEASE_MATRIX,
247
+ ) {
248
+ return fileNames.filter((name) => {
249
+ if (isUpdateMetadata(name)) {
250
+ return false;
251
+ }
252
+ const entry = platformForArtifact(name, matrix);
253
+ if (entry === null) {
254
+ // Its format is not released at all; resolveDisallowedArtifacts owns that.
255
+ return false;
256
+ }
257
+ const released = archTokensFor(entry).map((token) => token.toLowerCase());
258
+ const lowered = name.toLowerCase();
259
+ const present = ALL_ARCH_TOKENS.filter(
260
+ (token) => new RegExp(`(^|[-_.])${token}([-_.]|$)`, 'iu').test(lowered),
261
+ );
262
+ if (present.length === 0) {
263
+ return false;
264
+ }
265
+ return !present.some((token) => released.includes(token.toLowerCase()));
266
+ });
267
+ }
268
+
269
+ export function resolveMissingMetadataFiles(fileNames) {
270
+ const present = new Set(fileNames);
271
+ return UPDATE_METADATA_FILES.filter((name) => !present.has(name));
272
+ }
273
+
274
+ async function listFilesRecursively(root) {
275
+ const found = [];
276
+
277
+ async function walk(directory) {
278
+ let entries;
279
+ try {
280
+ entries = await readdir(directory, { withFileTypes: true });
281
+ } catch {
282
+ return;
283
+ }
284
+ for (const entry of entries) {
285
+ const entryPath = join(directory, entry.name);
286
+ if (entry.isDirectory()) {
287
+ await walk(entryPath);
288
+ continue;
289
+ }
290
+ found.push(entryPath);
291
+ }
292
+ }
293
+
294
+ const rootStat = await stat(root).catch(() => null);
295
+ if (rootStat === null) {
296
+ return [];
297
+ }
298
+ await walk(root);
299
+ return found;
300
+ }
301
+
302
+ export function validateReleaseAssets({ files, metadataDocuments }) {
303
+ const problems = [];
304
+ const fileNames = files.map((path) => basename(path));
305
+
306
+ for (const platform of resolveMissingPrimaryArtifacts(fileNames)) {
307
+ problems.push({
308
+ code: 'primary_artifact_missing',
309
+ message: `No primary user-facing artifact was produced for ${platform}.`,
310
+ });
311
+ }
312
+
313
+ for (const name of resolveMissingMetadataFiles(fileNames)) {
314
+ problems.push({
315
+ code: 'update_metadata_missing',
316
+ message: `Update metadata ${name} was not produced.`,
317
+ });
318
+ }
319
+
320
+ for (const name of resolveDisallowedArtifacts(fileNames)) {
321
+ problems.push({
322
+ code: 'artifact_outside_release_contract',
323
+ message: `${name} is not one of the declared release artifacts. `
324
+ + 'Add its format to DESKTOP_RELEASE_MATRIX if it should ship.',
325
+ });
326
+ }
327
+
328
+ for (const name of resolveUnreleasedArchitectureArtifacts(fileNames)) {
329
+ problems.push({
330
+ code: 'artifact_architecture_not_released',
331
+ message: `${name} targets an architecture the declared release set does not include. `
332
+ + 'Add it to DESKTOP_RELEASE_MATRIX if it should ship.',
333
+ });
334
+ }
335
+
336
+ const present = new Set(fileNames);
337
+ for (const { name, document, error } of metadataDocuments) {
338
+ if (error) {
339
+ problems.push({
340
+ code: 'update_metadata_unreadable',
341
+ message: `Update metadata ${name} could not be parsed: ${error}`,
342
+ });
343
+ continue;
344
+ }
345
+
346
+ const referenced = collectReferencedFiles(document);
347
+ if (referenced.length === 0) {
348
+ problems.push({
349
+ code: 'update_metadata_empty',
350
+ message: `Update metadata ${name} references no artifact.`,
351
+ });
352
+ continue;
353
+ }
354
+
355
+ for (const target of referenced) {
356
+ if (!present.has(target)) {
357
+ problems.push({
358
+ code: 'referenced_artifact_missing',
359
+ message: `Update metadata ${name} references ${target}, which was not produced.`,
360
+ });
361
+ }
362
+ }
363
+ }
364
+
365
+ return { ok: problems.length === 0, problems, fileNames: [...present].sort() };
366
+ }
367
+
368
+ async function main() {
369
+ const parsed = parseArgs(process.argv.slice(2));
370
+ if (parsed.help) {
371
+ printHelp();
372
+ return;
373
+ }
374
+
375
+ const root = resolve(PROJECT_ROOT, parsed.root);
376
+ const files = await listFilesRecursively(root);
377
+
378
+ const metadataDocuments = [];
379
+ for (const path of files) {
380
+ if (!UPDATE_METADATA_FILES.includes(basename(path))) {
381
+ continue;
382
+ }
383
+ try {
384
+ metadataDocuments.push({
385
+ name: basename(path),
386
+ document: yaml.load(await readFile(path, 'utf8')),
387
+ error: null,
388
+ });
389
+ } catch (error) {
390
+ metadataDocuments.push({
391
+ name: basename(path),
392
+ document: null,
393
+ error: error instanceof Error ? error.message : String(error),
394
+ });
395
+ }
396
+ }
397
+
398
+ const result = validateReleaseAssets({ files, metadataDocuments });
399
+
400
+ if (parsed.json) {
401
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
402
+ } else if (result.ok) {
403
+ process.stdout.write(
404
+ `[validate-release-assets] ${result.fileNames.length} files verified in ${root}.\n`,
405
+ );
406
+ } else {
407
+ for (const problem of result.problems) {
408
+ process.stderr.write(`[validate-release-assets] ${problem.code}: ${problem.message}\n`);
409
+ }
410
+ }
411
+
412
+ if (!result.ok) {
413
+ process.exitCode = 1;
414
+ }
415
+ }
416
+
417
+ if (resolve(process.argv[1] ?? '') === SCRIPT_PATH) {
418
+ void main().catch((error) => {
419
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
420
+ process.exitCode = 1;
421
+ });
422
+ }