anhedral 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -269
- package/assets/anhedral-cli-init-technical.svg +322 -0
- package/assets/anhedral-cli-init.svg +257 -173
- package/dist/bin.js +28 -191
- package/dist/dependencies.d.ts +7 -57
- package/dist/dependencies.js +96 -308
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -4
- package/dist/platforms/desktop.js +401 -0
- package/dist/platforms/extension.js +401 -0
- package/dist/platforms/mobile.js +232 -0
- package/dist/project.d.ts +0 -26
- package/dist/standard-products.d.ts +2 -0
- package/dist/standard-products.js +6 -0
- package/dist/standard.d.ts +18 -0
- package/dist/standard.js +578 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/application-stack-standard.md +700 -0
- package/package.json +34 -51
- package/CHANGELOG.md +0 -178
- package/CONTRIBUTING.md +0 -77
- package/SECURITY.md +0 -37
- package/SKILL.md +0 -339
- package/assets/images/svg/ably-symbol-color.svg +0 -8
- package/bin/anhedral.js +0 -2
- package/dist/architecture/contributions.d.ts +0 -28
- package/dist/architecture/contributions.js +0 -125
- package/dist/architecture/index.d.ts +0 -12
- package/dist/architecture/index.js +0 -6
- package/dist/architecture/manifest.d.ts +0 -53
- package/dist/architecture/manifest.js +0 -283
- package/dist/architecture/modules.d.ts +0 -41
- package/dist/architecture/modules.js +0 -187
- package/dist/architecture/plan.d.ts +0 -58
- package/dist/architecture/plan.js +0 -145
- package/dist/architecture/templates.d.ts +0 -10
- package/dist/architecture/templates.js +0 -23
- package/dist/cli.d.ts +0 -32
- package/dist/cli.js +0 -281
- package/dist/composers/root.d.ts +0 -6
- package/dist/composers/root.js +0 -35
- package/dist/print.d.ts +0 -8
- package/dist/print.js +0 -48
- package/dist/prompts.d.ts +0 -6
- package/dist/prompts.js +0 -41
- package/dist/scaffold.d.ts +0 -64
- package/dist/scaffold.js +0 -2102
- package/dist/template-source.d.ts +0 -20
- package/dist/template-source.js +0 -180
- package/dist/templates/api.d.ts +0 -2
- package/dist/templates/api.js +0 -3187
- package/dist/templates/assets-private-proxy.d.ts +0 -3
- package/dist/templates/assets-private-proxy.js +0 -277
- package/dist/templates/desktop.js +0 -792
- package/dist/templates/electron-updater.d.ts +0 -3
- package/dist/templates/electron-updater.js +0 -358
- package/dist/templates/extension.js +0 -856
- package/dist/templates/first-run.d.ts +0 -2
- package/dist/templates/first-run.js +0 -167
- package/dist/templates/mobile.js +0 -861
- package/dist/templates/shared.d.ts +0 -2
- package/dist/templates/shared.js +0 -577
- package/dist/templates/web.d.ts +0 -2
- package/dist/templates/web.js +0 -569
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +0 -8
- package/dist/ui.d.ts +0 -33
- package/dist/ui.js +0 -199
- package/dist/workspace-config.d.ts +0 -11
- package/dist/workspace-config.js +0 -372
- package/docs/NORTHSTAR.md +0 -114
- package/docs/conventions.md +0 -94
- package/docs/engineering-principles.md +0 -88
- package/docs/master-stack-map.md +0 -199
- package/docs/output-tree-contract.md +0 -493
- package/docs/references/manual-scaffolding.md +0 -421
- package/docs/references/provisioning.md +0 -577
- package/scripts/render-master-stack-map.mjs +0 -510
- package/templates/api-fastify/apps/api/tsconfig.json +0 -20
- package/templates/catalog.json +0 -23
- package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
- package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
- package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
- package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
- package/templates/web-next/apps/web/next-env.d.ts +0 -6
- /package/dist/{templates → platforms}/desktop.d.ts +0 -0
- /package/dist/{templates → platforms}/extension.d.ts +0 -0
- /package/dist/{templates → platforms}/mobile.d.ts +0 -0
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { TOOLCHAIN_DEPENDENCIES } from '../dependencies.js';
|
|
3
|
-
import { anhedralPrint } from '../print.js';
|
|
4
|
-
import { childPackageName } from '../render.js';
|
|
5
|
-
import { writeFile } from '../util.js';
|
|
6
|
-
function normalizedResourceName(projectName) {
|
|
7
|
-
const unscoped = projectName.replace(/^@[^/]+\//, '');
|
|
8
|
-
const normalized = unscoped.toLowerCase()
|
|
9
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
10
|
-
.replace(/-+/g, '-')
|
|
11
|
-
.replace(/^-+|-+$/g, '') || 'anhedral';
|
|
12
|
-
return normalized.slice(0, 48).replace(/-+$/g, '') || 'anhedral';
|
|
13
|
-
}
|
|
14
|
-
export function desktopUpdatesBucketName(projectName) {
|
|
15
|
-
return `${normalizedResourceName(projectName)}-desktop-updates`;
|
|
16
|
-
}
|
|
17
|
-
export function scaffoldElectronUpdater(root, options) {
|
|
18
|
-
const workerDir = path.join(root, 'apps/desktop-updater-worker');
|
|
19
|
-
const desktopDir = path.join(root, 'apps/desktop');
|
|
20
|
-
const wrangler = `pnpm dlx wrangler@${TOOLCHAIN_DEPENDENCIES.wrangler}`;
|
|
21
|
-
const bucketName = desktopUpdatesBucketName(options.projectName);
|
|
22
|
-
anhedralPrint.section('Electron updater (private R2 + Cloudflare Worker)');
|
|
23
|
-
anhedralPrint.step('Writing desktop update channel');
|
|
24
|
-
writeFile(path.join(workerDir, 'package.json'), JSON.stringify({
|
|
25
|
-
name: childPackageName(options.projectName, 'desktop-updater-worker'),
|
|
26
|
-
version: '0.1.0',
|
|
27
|
-
private: true,
|
|
28
|
-
type: 'module',
|
|
29
|
-
scripts: {
|
|
30
|
-
check: 'node --check src/index.js && node --test tests/*.test.js',
|
|
31
|
-
test: 'node --test tests/*.test.js',
|
|
32
|
-
dev: `${wrangler} dev`,
|
|
33
|
-
deploy: `${wrangler} deploy`,
|
|
34
|
-
types: `${wrangler} types`,
|
|
35
|
-
},
|
|
36
|
-
}, null, 2) + '\n');
|
|
37
|
-
writeFile(path.join(workerDir, 'wrangler.jsonc'), `{
|
|
38
|
-
// Replace both updates.example.com values with a hostname in your active Cloudflare zone.
|
|
39
|
-
"name": "desktop-updater",
|
|
40
|
-
"main": "src/index.js",
|
|
41
|
-
"compatibility_date": "2026-07-22",
|
|
42
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
43
|
-
"workers_dev": false,
|
|
44
|
-
"preview_urls": false,
|
|
45
|
-
"routes": [{ "pattern": "updates.example.com", "custom_domain": true }],
|
|
46
|
-
"vars": { "UPDATE_HOSTNAME": "updates.example.com", "UPDATE_PREFIX": "releases" },
|
|
47
|
-
"r2_buckets": [{ "binding": "UPDATES", "bucket_name": "${bucketName}" }],
|
|
48
|
-
"observability": {
|
|
49
|
-
"enabled": true,
|
|
50
|
-
"logs": { "enabled": true, "head_sampling_rate": 1 }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
`);
|
|
54
|
-
writeFile(path.join(workerDir, 'src/index.js'), `/**
|
|
55
|
-
* Electron update delivery from a private R2 bucket.
|
|
56
|
-
*
|
|
57
|
-
* The bucket has no public r2.dev or bucket custom domain. This Worker is the
|
|
58
|
-
* only public read path. Release CI uploads signed installers, blockmaps, and
|
|
59
|
-
* update metadata with Wrangler; the Worker never permits writes or listings.
|
|
60
|
-
*/
|
|
61
|
-
const ARTIFACT_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
62
|
-
const METADATA_CACHE_CONTROL = "no-store";
|
|
63
|
-
|
|
64
|
-
export default {
|
|
65
|
-
async fetch(request, env) {
|
|
66
|
-
const method = request.method.toUpperCase();
|
|
67
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
68
|
-
return textResponse("Method Not Allowed", 405, { allow: "GET, HEAD" });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const url = new URL(request.url);
|
|
72
|
-
if (url.hostname !== env.UPDATE_HOSTNAME) return textResponse("Forbidden", 403);
|
|
73
|
-
const key = decodeObjectKey(url.pathname);
|
|
74
|
-
const prefix = env.UPDATE_PREFIX.replace(/^\\/+|\\/+$/g, "") + "/";
|
|
75
|
-
if (!key || !key.startsWith(prefix)) return textResponse("Not Found", 404);
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
if (method === "HEAD") {
|
|
79
|
-
const object = await env.UPDATES.head(key);
|
|
80
|
-
if (!object) return textResponse("Not Found", 404);
|
|
81
|
-
return new Response(null, { status: 200, headers: objectHeaders(object, key) });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const object = await env.UPDATES.get(key, {
|
|
85
|
-
onlyIf: request.headers,
|
|
86
|
-
range: request.headers,
|
|
87
|
-
});
|
|
88
|
-
if (!object) return textResponse("Not Found", 404);
|
|
89
|
-
|
|
90
|
-
const headers = objectHeaders(object, key);
|
|
91
|
-
if (!("body" in object)) {
|
|
92
|
-
const status = failedConditionalStatus(request.headers);
|
|
93
|
-
headers.delete("content-length");
|
|
94
|
-
return new Response(null, { status, headers });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const isRange = request.headers.has("range") && object.range;
|
|
98
|
-
if (isRange) {
|
|
99
|
-
const returned = normalizeRange(object.range, object.size);
|
|
100
|
-
headers.set("content-range", "bytes " + returned.offset + "-" + (returned.offset + returned.length - 1) + "/" + object.size);
|
|
101
|
-
headers.set("content-length", String(returned.length));
|
|
102
|
-
}
|
|
103
|
-
return new Response(object.body, { status: isRange ? 206 : 200, headers });
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error(JSON.stringify({
|
|
106
|
-
message: "desktop update delivery failed",
|
|
107
|
-
key,
|
|
108
|
-
error: error instanceof Error ? error.message : String(error),
|
|
109
|
-
}));
|
|
110
|
-
return textResponse("Update delivery failed", 500);
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
function decodeObjectKey(pathname) {
|
|
116
|
-
const parts = [];
|
|
117
|
-
for (const rawPart of pathname.replace(/^\\/+/, "").split("/")) {
|
|
118
|
-
if (!rawPart) continue;
|
|
119
|
-
let part;
|
|
120
|
-
try {
|
|
121
|
-
part = decodeURIComponent(rawPart);
|
|
122
|
-
} catch {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
if (!part || part === "." || part === ".." || part.includes("/") || part.includes("\\\\")) return null;
|
|
126
|
-
parts.push(part);
|
|
127
|
-
}
|
|
128
|
-
return parts.join("/") || null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function objectHeaders(object, key) {
|
|
132
|
-
const headers = new Headers();
|
|
133
|
-
object.writeHttpMetadata(headers);
|
|
134
|
-
headers.set("accept-ranges", "bytes");
|
|
135
|
-
headers.set("cache-control", key.endsWith(".yml") ? METADATA_CACHE_CONTROL : ARTIFACT_CACHE_CONTROL);
|
|
136
|
-
headers.set("content-length", String(object.size));
|
|
137
|
-
headers.set("etag", object.httpEtag);
|
|
138
|
-
headers.set("x-content-type-options", "nosniff");
|
|
139
|
-
headers.set("content-security-policy", "default-src 'none'");
|
|
140
|
-
if (object.uploaded instanceof Date) headers.set("last-modified", object.uploaded.toUTCString());
|
|
141
|
-
headers.delete("set-cookie");
|
|
142
|
-
return headers;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function failedConditionalStatus(headers) {
|
|
146
|
-
if (headers.has("if-none-match")) return 304;
|
|
147
|
-
if (!headers.has("if-match") && !headers.has("if-unmodified-since") && headers.has("if-modified-since")) return 304;
|
|
148
|
-
return 412;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function normalizeRange(range, objectSize) {
|
|
152
|
-
if (typeof range.suffix === "number") {
|
|
153
|
-
const length = Math.min(Math.max(0, range.suffix), objectSize);
|
|
154
|
-
return { offset: objectSize - length, length };
|
|
155
|
-
}
|
|
156
|
-
const offset = Math.min(Math.max(0, range.offset ?? 0), objectSize);
|
|
157
|
-
const available = objectSize - offset;
|
|
158
|
-
const length = typeof range.length === "number"
|
|
159
|
-
? Math.min(Math.max(0, range.length), available)
|
|
160
|
-
: available;
|
|
161
|
-
return { offset, length };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function textResponse(body, status, extraHeaders = {}) {
|
|
165
|
-
return new Response(body, {
|
|
166
|
-
status,
|
|
167
|
-
headers: { "cache-control": "no-store", "content-type": "text/plain; charset=utf-8", ...extraHeaders },
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
`);
|
|
171
|
-
writeFile(path.join(workerDir, 'tests/worker.test.js'), `import assert from 'node:assert/strict';
|
|
172
|
-
import test from 'node:test';
|
|
173
|
-
import worker from '../src/index.js';
|
|
174
|
-
|
|
175
|
-
function storedObject(body = new TextEncoder().encode('release-bytes'), overrides = {}) {
|
|
176
|
-
return {
|
|
177
|
-
body,
|
|
178
|
-
size: body.byteLength,
|
|
179
|
-
httpEtag: '"synthetic-etag"',
|
|
180
|
-
uploaded: new Date('2026-07-22T00:00:00.000Z'),
|
|
181
|
-
writeHttpMetadata(headers) { headers.set('content-type', 'application/octet-stream'); },
|
|
182
|
-
...overrides,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function environment(overrides = {}) {
|
|
187
|
-
const object = storedObject();
|
|
188
|
-
return {
|
|
189
|
-
UPDATE_HOSTNAME: 'updates.example.com',
|
|
190
|
-
UPDATE_PREFIX: 'releases',
|
|
191
|
-
UPDATES: {
|
|
192
|
-
async head() { return object; },
|
|
193
|
-
async get() { return object; },
|
|
194
|
-
},
|
|
195
|
-
...overrides,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
test('allows only read methods on the configured host and release prefix', async () => {
|
|
200
|
-
const env = environment();
|
|
201
|
-
assert.equal((await worker.fetch(new Request('https://updates.example.com/releases/mac/arm64/app.zip', { method: 'POST' }), env)).status, 405);
|
|
202
|
-
assert.equal((await worker.fetch(new Request('https://other.example.com/releases/mac/arm64/app.zip'), env)).status, 403);
|
|
203
|
-
assert.equal((await worker.fetch(new Request('https://updates.example.com/private/app.zip'), env)).status, 404);
|
|
204
|
-
assert.equal((await worker.fetch(new Request('https://updates.example.com/releases/%2Fprivate.zip'), env)).status, 404);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('streams artifacts and handles HEAD without exposing a body', async () => {
|
|
208
|
-
const env = environment();
|
|
209
|
-
const response = await worker.fetch(new Request('https://updates.example.com/releases/mac/arm64/app.zip'), env);
|
|
210
|
-
assert.equal(response.status, 200);
|
|
211
|
-
assert.equal(await response.text(), 'release-bytes');
|
|
212
|
-
assert.equal(response.headers.get('cache-control'), 'public, max-age=31536000, immutable');
|
|
213
|
-
assert.equal(response.headers.get('etag'), '"synthetic-etag"');
|
|
214
|
-
|
|
215
|
-
const head = await worker.fetch(new Request('https://updates.example.com/releases/mac/arm64/app.zip', { method: 'HEAD' }), env);
|
|
216
|
-
assert.equal(head.status, 200);
|
|
217
|
-
assert.equal(await head.text(), '');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('returns ranges and conditional responses with safe cache policy', async () => {
|
|
221
|
-
const ranged = storedObject(new TextEncoder().encode('le'), { size: 13, range: { offset: 2, length: 2 } });
|
|
222
|
-
const rangeResponse = await worker.fetch(new Request('https://updates.example.com/releases/mac/arm64/app.zip', {
|
|
223
|
-
headers: { range: 'bytes=2-3' },
|
|
224
|
-
}), environment({ UPDATES: { async head() { return ranged; }, async get() { return ranged; } } }));
|
|
225
|
-
assert.equal(rangeResponse.status, 206);
|
|
226
|
-
assert.equal(rangeResponse.headers.get('content-range'), 'bytes 2-3/13');
|
|
227
|
-
assert.equal(rangeResponse.headers.get('content-length'), '2');
|
|
228
|
-
|
|
229
|
-
const notModified = storedObject(new Uint8Array(), { size: 13 });
|
|
230
|
-
delete notModified.body;
|
|
231
|
-
const conditional = await worker.fetch(new Request('https://updates.example.com/releases/mac/arm64/latest-mac.yml', {
|
|
232
|
-
headers: { 'if-none-match': '"synthetic-etag"' },
|
|
233
|
-
}), environment({ UPDATES: { async head() { return notModified; }, async get() { return notModified; } } }));
|
|
234
|
-
assert.equal(conditional.status, 304);
|
|
235
|
-
assert.equal(conditional.headers.get('cache-control'), 'no-store');
|
|
236
|
-
assert.equal(conditional.headers.has('content-length'), false);
|
|
237
|
-
});
|
|
238
|
-
`);
|
|
239
|
-
writeFile(path.join(desktopDir, 'scripts/publish-updates.mjs'), `import { readdirSync, statSync } from 'node:fs';
|
|
240
|
-
import path from 'node:path';
|
|
241
|
-
import { spawnSync } from 'node:child_process';
|
|
242
|
-
|
|
243
|
-
const WRANGLER_VERSION = ${JSON.stringify(TOOLCHAIN_DEPENDENCIES.wrangler)};
|
|
244
|
-
const BUCKET_NAME = ${JSON.stringify(bucketName)};
|
|
245
|
-
const RELEASE_DIRECTORY = path.resolve('apps/desktop/release');
|
|
246
|
-
const ALLOWED_PLATFORMS = new Set(['mac', 'win', 'linux']);
|
|
247
|
-
const ALLOWED_ARCHITECTURES = new Set(['x64', 'arm64']);
|
|
248
|
-
const UPLOAD_COMMAND = process.env.ANHEDRAL_DESKTOP_UPDATE_UPLOAD_COMMAND || (process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm');
|
|
249
|
-
const UPLOAD_ARGS_PREFIX = process.env.ANHEDRAL_DESKTOP_UPDATE_UPLOAD_ARGS_PREFIX
|
|
250
|
-
? JSON.parse(process.env.ANHEDRAL_DESKTOP_UPDATE_UPLOAD_ARGS_PREFIX)
|
|
251
|
-
: [];
|
|
252
|
-
if (!Array.isArray(UPLOAD_ARGS_PREFIX) || !UPLOAD_ARGS_PREFIX.every((value) => typeof value === 'string')) {
|
|
253
|
-
throw new Error('ANHEDRAL_DESKTOP_UPDATE_UPLOAD_ARGS_PREFIX must be a JSON string array');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function option(name) {
|
|
257
|
-
const index = process.argv.indexOf('--' + name);
|
|
258
|
-
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const platform = option('platform');
|
|
262
|
-
const arch = option('arch');
|
|
263
|
-
if (!platform || !ALLOWED_PLATFORMS.has(platform)) {
|
|
264
|
-
throw new Error('--platform must be one of: mac, win, linux');
|
|
265
|
-
}
|
|
266
|
-
if (!arch || !ALLOWED_ARCHITECTURES.has(arch)) {
|
|
267
|
-
throw new Error('--arch must be one of: x64, arm64');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const metadataName = platform === 'mac' ? 'latest-mac.yml' : platform === 'linux' ? 'latest-linux.yml' : 'latest.yml';
|
|
271
|
-
const artifactExtensions = platform === 'mac'
|
|
272
|
-
? ['.dmg', '.zip', '.blockmap']
|
|
273
|
-
: platform === 'win'
|
|
274
|
-
? ['.exe', '.zip', '.blockmap']
|
|
275
|
-
: ['.AppImage', '.deb', '.blockmap'];
|
|
276
|
-
const files = readdirSync(RELEASE_DIRECTORY)
|
|
277
|
-
.filter((name) => name === metadataName || artifactExtensions.some((extension) => name.endsWith(extension)))
|
|
278
|
-
.filter((name) => name === metadataName || name.includes('-' + arch + '.'))
|
|
279
|
-
.filter((name) => statSync(path.join(RELEASE_DIRECTORY, name)).isFile());
|
|
280
|
-
if (!files.includes(metadataName)) throw new Error('Missing electron-updater metadata: ' + metadataName);
|
|
281
|
-
if (!files.some((name) => name !== metadataName && !name.endsWith('.blockmap'))) {
|
|
282
|
-
throw new Error('No desktop installer artifact was found for ' + platform + '/' + arch);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Publish immutable artifacts first and the mutable channel metadata last.
|
|
286
|
-
files.sort((left, right) => Number(left === metadataName) - Number(right === metadataName) || left.localeCompare(right));
|
|
287
|
-
for (const name of files) {
|
|
288
|
-
const source = path.join(RELEASE_DIRECTORY, name);
|
|
289
|
-
const objectPath = BUCKET_NAME + '/releases/' + platform + '/' + arch + '/' + name;
|
|
290
|
-
const metadata = name.endsWith('.yml');
|
|
291
|
-
const args = [
|
|
292
|
-
'dlx',
|
|
293
|
-
'wrangler@' + WRANGLER_VERSION,
|
|
294
|
-
'r2',
|
|
295
|
-
'object',
|
|
296
|
-
'put',
|
|
297
|
-
objectPath,
|
|
298
|
-
'--remote',
|
|
299
|
-
'--file=' + source,
|
|
300
|
-
'--content-type=' + contentType(name),
|
|
301
|
-
'--cache-control=' + (metadata ? 'no-store' : 'public, max-age=31536000, immutable'),
|
|
302
|
-
];
|
|
303
|
-
const result = spawnSync(UPLOAD_COMMAND, [...UPLOAD_ARGS_PREFIX, ...args], {
|
|
304
|
-
cwd: process.cwd(),
|
|
305
|
-
stdio: 'inherit',
|
|
306
|
-
});
|
|
307
|
-
if (result.error) throw result.error;
|
|
308
|
-
if (result.status !== 0) throw new Error('Unable to upload desktop update object: ' + objectPath);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function contentType(name) {
|
|
312
|
-
if (name.endsWith('.yml')) return 'text/yaml; charset=utf-8';
|
|
313
|
-
if (name.endsWith('.blockmap')) return 'application/octet-stream';
|
|
314
|
-
if (name.endsWith('.zip')) return 'application/zip';
|
|
315
|
-
if (name.endsWith('.dmg')) return 'application/x-apple-diskimage';
|
|
316
|
-
if (name.endsWith('.exe')) return 'application/vnd.microsoft.portable-executable';
|
|
317
|
-
if (name.endsWith('.deb')) return 'application/vnd.debian.binary-package';
|
|
318
|
-
return 'application/octet-stream';
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
console.log('Published ' + files.length + ' desktop update files to ' + BUCKET_NAME + '.');
|
|
322
|
-
`);
|
|
323
|
-
writeFile(path.join(root, 'cloudflare/desktop-updates.md'), `# Electron desktop update channel
|
|
324
|
-
|
|
325
|
-
The \`${bucketName}\` R2 bucket stays private. The generated
|
|
326
|
-
\`desktop-updater\` Worker is the only public read path and serves signed
|
|
327
|
-
installers, blockmaps, and electron-updater channel metadata through a
|
|
328
|
-
Cloudflare-managed custom domain such as \`updates.example.com\`.
|
|
329
|
-
|
|
330
|
-
Set the same HTTPS origin in \`apps/desktop/electron-builder.env\` as
|
|
331
|
-
\`DESKTOP_UPDATE_BASE_URL\`. electron-builder expands platform and architecture
|
|
332
|
-
macros below \`/releases/<os>/<arch>\`, and the packaged application reads that
|
|
333
|
-
URL from its generated \`app-update.yml\` file. Keep the bucket's \`r2.dev\` URL
|
|
334
|
-
and R2 bucket custom-domain access disabled.
|
|
335
|
-
|
|
336
|
-
Publish signed artifacts before channel metadata. The generated publisher does
|
|
337
|
-
this ordering automatically so an update check cannot discover a release whose
|
|
338
|
-
installer or blockmap is not yet available.
|
|
339
|
-
|
|
340
|
-
Provision and release from the repository root:
|
|
341
|
-
|
|
342
|
-
\`\`\`sh
|
|
343
|
-
pnpm desktop:updates:cloudflare:login
|
|
344
|
-
# First replace updates.example.com in apps/desktop-updater-worker/wrangler.jsonc.
|
|
345
|
-
pnpm desktop:updates:first-provision
|
|
346
|
-
cp apps/desktop/electron-builder.env.example apps/desktop/electron-builder.env
|
|
347
|
-
pnpm desktop:updates:build:mac
|
|
348
|
-
pnpm desktop:updates:publish --platform mac --arch arm64
|
|
349
|
-
\`\`\`
|
|
350
|
-
|
|
351
|
-
Build each operating system on its native, signing-enabled CI runner. Increase
|
|
352
|
-
the desktop package version for every release and upload each architecture from
|
|
353
|
-
a clean \`apps/desktop/release\` directory. The publisher rejects unknown
|
|
354
|
-
platforms/architectures and selects only artifacts carrying the requested
|
|
355
|
-
architecture in the generated filename.
|
|
356
|
-
`);
|
|
357
|
-
anhedralPrint.done('Electron desktop update channel written');
|
|
358
|
-
}
|