@www.hyperlinks.space/program-kit 1.2.3
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 +53 -0
- package/api/ai.ts +111 -0
- package/api/base.ts +117 -0
- package/api/blockchain.ts +58 -0
- package/api/bot.ts +19 -0
- package/api/ping.ts +41 -0
- package/api/releases.ts +162 -0
- package/api/telegram.ts +65 -0
- package/api/tsconfig.json +17 -0
- package/app/_layout.tsx +135 -0
- package/app/ai.tsx +39 -0
- package/app/components/GlobalBottomBar.tsx +447 -0
- package/app/components/GlobalBottomBarWeb.tsx +362 -0
- package/app/components/GlobalLogoBar.tsx +108 -0
- package/app/components/GlobalLogoBarFallback.tsx +66 -0
- package/app/components/GlobalLogoBarWithFallback.tsx +24 -0
- package/app/components/HyperlinksSpaceLogo.tsx +29 -0
- package/app/components/Telegram.tsx +648 -0
- package/app/components/telegramWebApp.ts +359 -0
- package/app/fonts.ts +12 -0
- package/app/index.tsx +102 -0
- package/app/theme.ts +117 -0
- package/app.json +60 -0
- package/assets/icon.ico +0 -0
- package/assets/images/favicon.png +0 -0
- package/blockchain/coffee.ts +217 -0
- package/blockchain/router.ts +44 -0
- package/bot/format.ts +143 -0
- package/bot/grammy.ts +52 -0
- package/bot/responder.ts +620 -0
- package/bot/webhook.ts +262 -0
- package/database/messages.ts +128 -0
- package/database/start.ts +133 -0
- package/database/users.ts +46 -0
- package/docs/ai_and_search_bar_input.md +94 -0
- package/docs/ai_bot_messages.md +124 -0
- package/docs/backlogs/medium_term_backlog.md +26 -0
- package/docs/backlogs/short_term_backlog.md +39 -0
- package/docs/blue_bar_tackling.md +143 -0
- package/docs/bot_async_streaming.md +174 -0
- package/docs/build_and_install.md +129 -0
- package/docs/database_messages.md +34 -0
- package/docs/fonts.md +18 -0
- package/docs/releases.md +201 -0
- package/docs/releases_github_actions.md +188 -0
- package/docs/scalability.md +34 -0
- package/docs/security_plan_raw.md +244 -0
- package/docs/security_raw.md +345 -0
- package/docs/timing_raw.md +63 -0
- package/docs/tma_logo_bar_jump_investigation.md +69 -0
- package/docs/update.md +205 -0
- package/docs/wallets_hosting_architecture.md +257 -0
- package/eas.json +47 -0
- package/eslint.config.js +10 -0
- package/fullREADME.md +159 -0
- package/global.css +67 -0
- package/npmReadMe.md +53 -0
- package/package.json +214 -0
- package/scripts/load-env.ts +17 -0
- package/scripts/migrate-db.ts +16 -0
- package/scripts/program-kit-init.cjs +58 -0
- package/scripts/run-bot-local.ts +30 -0
- package/scripts/set-webhook.ts +67 -0
- package/scripts/test-api-base.ts +12 -0
- package/telegram/post.ts +328 -0
- package/tsconfig.json +17 -0
- package/vercel.json +7 -0
- package/windows/after-sign-windows-icon.cjs +13 -0
- package/windows/build-layout.cjs +72 -0
- package/windows/build-with-progress.cjs +88 -0
- package/windows/build.cjs +2247 -0
- package/windows/cleanup-legacy-appdata-installs.ps1 +91 -0
- package/windows/cleanup-legacy-windows-shortcuts.ps1 +46 -0
- package/windows/cleanup.cjs +200 -0
- package/windows/embed-windows-exe-icon.cjs +55 -0
- package/windows/extractAppPackage.nsh +150 -0
- package/windows/forge/README.md +41 -0
- package/windows/forge/forge.config.js +138 -0
- package/windows/forge/make-with-stamp.cjs +65 -0
- package/windows/forge-cleanup.cjs +255 -0
- package/windows/hsp-app-process.ps1 +63 -0
- package/windows/installer-hooks.nsi +373 -0
- package/windows/product-brand.cjs +42 -0
- package/windows/remove-orphan-uninstall-registry.ps1 +67 -0
- package/windows/run-installed-with-icon-debug.cmd +20 -0
- package/windows/run-win-electron-builder.cjs +46 -0
- package/windows/updater-dialog.html +143 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick check that app/api/base.ts works. Run: npx tsx scripts/test-api-base.ts
|
|
3
|
+
* In Node (no window), getApiBaseUrl() uses Vercel env or falls back to http://localhost:3000.
|
|
4
|
+
*/
|
|
5
|
+
import { getApiBaseUrl, buildApiUrl } from "../api/base.js";
|
|
6
|
+
|
|
7
|
+
const base = getApiBaseUrl();
|
|
8
|
+
const full = buildApiUrl("/api/telegram");
|
|
9
|
+
|
|
10
|
+
console.log("[api/base] getApiBaseUrl():", base);
|
|
11
|
+
console.log("[api/base] buildApiUrl('/api/telegram'):", full);
|
|
12
|
+
console.log("[api/base] OK");
|
package/telegram/post.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/telegram handler (init data verify + DB upsert).
|
|
3
|
+
* Loaded only for POST so GET stays fast. Lives under app/telegram; api/telegram.ts imports this.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import {
|
|
7
|
+
normalizeUsername,
|
|
8
|
+
upsertUserFromTma,
|
|
9
|
+
} from '../database/users.js';
|
|
10
|
+
|
|
11
|
+
const LOG_TAG = '[api/telegram]';
|
|
12
|
+
|
|
13
|
+
function log(msg: string, detail?: Record<string, unknown>) {
|
|
14
|
+
const payload = detail ? ` ${JSON.stringify(detail)}` : '';
|
|
15
|
+
console.log(`${LOG_TAG} ${msg}${payload}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function logErr(msg: string, err: unknown) {
|
|
19
|
+
console.error(
|
|
20
|
+
`${LOG_TAG} ${msg}`,
|
|
21
|
+
err instanceof Error ? err.message : err,
|
|
22
|
+
);
|
|
23
|
+
if (err instanceof Error && err.stack) console.error(err.stack);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type TelegramUserPayload = {
|
|
27
|
+
id?: number;
|
|
28
|
+
username?: string;
|
|
29
|
+
first_name?: string;
|
|
30
|
+
last_name?: string;
|
|
31
|
+
language_code?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type VerifiedInitData = {
|
|
36
|
+
auth_date?: string;
|
|
37
|
+
query_id?: string;
|
|
38
|
+
user?: TelegramUserPayload;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const TELEGRAM_WEBAPP_PUBLIC_KEY_RAW = Buffer.from(
|
|
43
|
+
'e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d',
|
|
44
|
+
'hex',
|
|
45
|
+
);
|
|
46
|
+
const ED25519_SPKI_HEADER = Buffer.from(
|
|
47
|
+
'302a300506032b6570032100',
|
|
48
|
+
'hex',
|
|
49
|
+
);
|
|
50
|
+
const TELEGRAM_WEBAPP_PUBLIC_KEY = crypto.createPublicKey({
|
|
51
|
+
key: Buffer.concat([
|
|
52
|
+
ED25519_SPKI_HEADER,
|
|
53
|
+
Buffer.from([0]),
|
|
54
|
+
TELEGRAM_WEBAPP_PUBLIC_KEY_RAW,
|
|
55
|
+
]),
|
|
56
|
+
format: 'der',
|
|
57
|
+
type: 'spki',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function verifyTelegramWebAppInitData(
|
|
61
|
+
initData: string,
|
|
62
|
+
botToken: string,
|
|
63
|
+
maxAgeSeconds: number = 24 * 3600,
|
|
64
|
+
): VerifiedInitData | null {
|
|
65
|
+
if (!initData || !botToken) return null;
|
|
66
|
+
try {
|
|
67
|
+
const params = new URLSearchParams(initData);
|
|
68
|
+
const data: Record<string, string> = {};
|
|
69
|
+
for (const [key, value] of params.entries()) data[key] = value;
|
|
70
|
+
|
|
71
|
+
const authDateStr = data['auth_date'];
|
|
72
|
+
if (authDateStr) {
|
|
73
|
+
const authDate = Number(authDateStr);
|
|
74
|
+
if (!Number.isFinite(authDate)) return null;
|
|
75
|
+
const now = Math.floor(Date.now() / 1000);
|
|
76
|
+
if (authDate > now + 60) return null;
|
|
77
|
+
if (maxAgeSeconds != null && now - authDate > maxAgeSeconds) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const receivedHash = data['hash'];
|
|
83
|
+
const receivedSignature = data['signature'];
|
|
84
|
+
|
|
85
|
+
if (receivedHash) {
|
|
86
|
+
const dataForHash = { ...data };
|
|
87
|
+
delete dataForHash['hash'];
|
|
88
|
+
const sorted = Object.keys(dataForHash)
|
|
89
|
+
.sort()
|
|
90
|
+
.map((k) => `${k}=${dataForHash[k]}`)
|
|
91
|
+
.join('\n');
|
|
92
|
+
const dataCheckString = Buffer.from(sorted, 'utf8');
|
|
93
|
+
const secretKey = crypto
|
|
94
|
+
.createHmac('sha256', 'WebAppData')
|
|
95
|
+
.update(botToken)
|
|
96
|
+
.digest();
|
|
97
|
+
const computedHash = crypto
|
|
98
|
+
.createHmac('sha256', secretKey)
|
|
99
|
+
.update(dataCheckString)
|
|
100
|
+
.digest('hex');
|
|
101
|
+
const valid =
|
|
102
|
+
receivedHash.length === computedHash.length &&
|
|
103
|
+
crypto.timingSafeEqual(
|
|
104
|
+
Buffer.from(receivedHash, 'hex'),
|
|
105
|
+
Buffer.from(computedHash, 'hex'),
|
|
106
|
+
);
|
|
107
|
+
if (!valid) return null;
|
|
108
|
+
} else if (receivedSignature) {
|
|
109
|
+
const botId = botToken.split(':')[0];
|
|
110
|
+
if (!botId) return null;
|
|
111
|
+
const dataForSig = { ...data };
|
|
112
|
+
delete dataForSig['hash'];
|
|
113
|
+
delete dataForSig['signature'];
|
|
114
|
+
const sorted = Object.keys(dataForSig)
|
|
115
|
+
.sort()
|
|
116
|
+
.map((k) => `${k}=${dataForSig[k]}`)
|
|
117
|
+
.join('\n');
|
|
118
|
+
const dataCheckString = `${botId}:WebAppData\n${sorted}`;
|
|
119
|
+
const base64 = receivedSignature
|
|
120
|
+
.replace(/-/g, '+')
|
|
121
|
+
.replace(/_/g, '/');
|
|
122
|
+
const pad = (4 - (base64.length % 4)) % 4;
|
|
123
|
+
const sigBuffer = Buffer.from(
|
|
124
|
+
base64 + '='.repeat(pad),
|
|
125
|
+
'base64',
|
|
126
|
+
);
|
|
127
|
+
const ok = crypto.verify(
|
|
128
|
+
null,
|
|
129
|
+
Buffer.from(dataCheckString, 'utf8'),
|
|
130
|
+
TELEGRAM_WEBAPP_PUBLIC_KEY,
|
|
131
|
+
sigBuffer,
|
|
132
|
+
);
|
|
133
|
+
if (!ok) return null;
|
|
134
|
+
} else return null;
|
|
135
|
+
|
|
136
|
+
const result: VerifiedInitData = { ...data };
|
|
137
|
+
delete result['hash'];
|
|
138
|
+
delete result['signature'];
|
|
139
|
+
if (data.user) {
|
|
140
|
+
try {
|
|
141
|
+
result.user = JSON.parse(data.user) as TelegramUserPayload;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Get JSON body from Web Request or Node req (Vercel may pass either). */
|
|
153
|
+
async function getBody(
|
|
154
|
+
request:
|
|
155
|
+
| Request
|
|
156
|
+
| {
|
|
157
|
+
json?: () => Promise<unknown>;
|
|
158
|
+
body?: unknown;
|
|
159
|
+
on?: (e: string, fn: (c: Buffer) => void) => void;
|
|
160
|
+
},
|
|
161
|
+
): Promise<unknown> {
|
|
162
|
+
if (
|
|
163
|
+
typeof (request as { json?: () => Promise<unknown> }).json ===
|
|
164
|
+
'function'
|
|
165
|
+
) {
|
|
166
|
+
return (request as Request).json();
|
|
167
|
+
}
|
|
168
|
+
const req = request as {
|
|
169
|
+
body?: unknown;
|
|
170
|
+
on?: (e: string, fn: (c: Buffer) => void) => void;
|
|
171
|
+
};
|
|
172
|
+
if (req.body != null && typeof req.body === 'object') {
|
|
173
|
+
return req.body;
|
|
174
|
+
}
|
|
175
|
+
if (typeof req.on === 'function') {
|
|
176
|
+
const chunks: Buffer[] = [];
|
|
177
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
178
|
+
(req as NodeJS.ReadableStream).on(
|
|
179
|
+
'data',
|
|
180
|
+
(c: Buffer) => chunks.push(c),
|
|
181
|
+
);
|
|
182
|
+
(req as NodeJS.ReadableStream).on('end', () => {
|
|
183
|
+
try {
|
|
184
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
185
|
+
resolve(raw ? JSON.parse(raw) : null);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
reject(e);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
(req as NodeJS.ReadableStream).on('error', reject);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handlePost(
|
|
197
|
+
request: Request | { json?: () => Promise<unknown>; body?: unknown },
|
|
198
|
+
): Promise<Response> {
|
|
199
|
+
const startMs = Date.now();
|
|
200
|
+
log('post_start', { elapsedMs: 0 });
|
|
201
|
+
|
|
202
|
+
let body: any;
|
|
203
|
+
try {
|
|
204
|
+
body = await getBody(request);
|
|
205
|
+
if (body == null) throw new Error('no_body');
|
|
206
|
+
log('body_parsed', { elapsedMs: Date.now() - startMs });
|
|
207
|
+
} catch (e) {
|
|
208
|
+
logErr('body_parse_failed', e);
|
|
209
|
+
return new Response(
|
|
210
|
+
JSON.stringify({ ok: false, error: 'bad_json' }),
|
|
211
|
+
{
|
|
212
|
+
status: 400,
|
|
213
|
+
headers: { 'content-type': 'application/json' },
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const initData =
|
|
219
|
+
typeof body?.initData === 'string' ? body.initData : '';
|
|
220
|
+
if (!initData) {
|
|
221
|
+
log('reject', {
|
|
222
|
+
reason: 'missing_initData',
|
|
223
|
+
elapsedMs: Date.now() - startMs,
|
|
224
|
+
});
|
|
225
|
+
return new Response(
|
|
226
|
+
JSON.stringify({ ok: false, error: 'missing_initData' }),
|
|
227
|
+
{
|
|
228
|
+
status: 400,
|
|
229
|
+
headers: { 'content-type': 'application/json' },
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
log('initData_received', {
|
|
234
|
+
initDataLength: initData.length,
|
|
235
|
+
elapsedMs: Date.now() - startMs,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const botToken = (process.env.BOT_TOKEN || '').trim();
|
|
239
|
+
if (!botToken) {
|
|
240
|
+
log('reject', {
|
|
241
|
+
reason: 'bot_token_not_configured',
|
|
242
|
+
elapsedMs: Date.now() - startMs,
|
|
243
|
+
});
|
|
244
|
+
return new Response(
|
|
245
|
+
JSON.stringify({ ok: false, error: 'bot_token_not_configured' }),
|
|
246
|
+
{
|
|
247
|
+
status: 500,
|
|
248
|
+
headers: { 'content-type': 'application/json' },
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const verifyStart = Date.now();
|
|
254
|
+
const verified = verifyTelegramWebAppInitData(initData, botToken);
|
|
255
|
+
log('verify_done', {
|
|
256
|
+
ok: !!verified,
|
|
257
|
+
verifyMs: Date.now() - verifyStart,
|
|
258
|
+
elapsedMs: Date.now() - startMs,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!verified) {
|
|
262
|
+
log('reject', {
|
|
263
|
+
reason: 'invalid_initdata',
|
|
264
|
+
elapsedMs: Date.now() - startMs,
|
|
265
|
+
});
|
|
266
|
+
return new Response(
|
|
267
|
+
JSON.stringify({ ok: false, error: 'invalid_initdata' }),
|
|
268
|
+
{
|
|
269
|
+
status: 401,
|
|
270
|
+
headers: { 'content-type': 'application/json' },
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const user: TelegramUserPayload =
|
|
276
|
+
verified.user && typeof verified.user === 'object'
|
|
277
|
+
? (verified.user as TelegramUserPayload)
|
|
278
|
+
: {};
|
|
279
|
+
const telegramUsername = normalizeUsername(user.username);
|
|
280
|
+
if (!telegramUsername) {
|
|
281
|
+
log('reject', {
|
|
282
|
+
reason: 'username_required',
|
|
283
|
+
elapsedMs: Date.now() - startMs,
|
|
284
|
+
});
|
|
285
|
+
return new Response(
|
|
286
|
+
JSON.stringify({ ok: false, error: 'username_required' }),
|
|
287
|
+
{
|
|
288
|
+
status: 400,
|
|
289
|
+
headers: { 'content-type': 'application/json' },
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
log('username_ok', {
|
|
294
|
+
telegramUsername,
|
|
295
|
+
elapsedMs: Date.now() - startMs,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const locale =
|
|
299
|
+
typeof user.language_code === 'string'
|
|
300
|
+
? user.language_code
|
|
301
|
+
: null;
|
|
302
|
+
const dbStart = Date.now();
|
|
303
|
+
try {
|
|
304
|
+
await upsertUserFromTma({ telegramUsername, locale });
|
|
305
|
+
log('db_upsert_done', {
|
|
306
|
+
dbMs: Date.now() - dbStart,
|
|
307
|
+
elapsedMs: Date.now() - startMs,
|
|
308
|
+
});
|
|
309
|
+
} catch (e) {
|
|
310
|
+
logErr('db_upsert_failed', e);
|
|
311
|
+
return new Response(
|
|
312
|
+
JSON.stringify({ ok: false, error: 'db_error' }),
|
|
313
|
+
{
|
|
314
|
+
status: 500,
|
|
315
|
+
headers: { 'content-type': 'application/json' },
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
log('success', {
|
|
321
|
+
telegramUsername,
|
|
322
|
+
totalMs: Date.now() - startMs,
|
|
323
|
+
});
|
|
324
|
+
return new Response(
|
|
325
|
+
JSON.stringify({ ok: true, telegram_username: telegramUsername }),
|
|
326
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
327
|
+
);
|
|
328
|
+
}
|
package/tsconfig.json
ADDED
package/vercel.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* electron-builder afterSign (Windows): embed icon via app-builder rcedit (see embed-windows-exe-icon.cjs).
|
|
3
|
+
*/
|
|
4
|
+
const { embedWindowsExeIcon } = require("./embed-windows-exe-icon.cjs");
|
|
5
|
+
|
|
6
|
+
module.exports = async (context) => {
|
|
7
|
+
if (context.electronPlatformName !== "win32") return;
|
|
8
|
+
await embedWindowsExeIcon({
|
|
9
|
+
appOutDir: context.appOutDir,
|
|
10
|
+
projectDir: context.packager.projectDir,
|
|
11
|
+
productFilename: context.packager.appInfo.productFilename,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows electron-builder layout:
|
|
3
|
+
* - Staging: releases/builder/build_MMDDYYYY_HHMM/eb-output/ (deleted after cleanup)
|
|
4
|
+
* - Final: releases/builder/build_MMDDYYYY_HHMM/<productSlug from package.json>Installer_<stamp>.exe
|
|
5
|
+
* releases/builder/build_MMDDYYYY_HHMM/<RELEASE_BUILD_DEV_DIRNAME>/ (zip, yml, unpacked, etc.)
|
|
6
|
+
* - BUILD_STAMP matches the folder id (required by package.json nsis.artifactName).
|
|
7
|
+
*/
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
/** Non-installer artifacts live here (same name for builder + Forge; CI expects this path). */
|
|
12
|
+
const RELEASE_BUILD_DEV_DIRNAME = "dev";
|
|
13
|
+
|
|
14
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
15
|
+
|
|
16
|
+
function makeDefaultBuildName(d = new Date()) {
|
|
17
|
+
return (
|
|
18
|
+
"build_" +
|
|
19
|
+
pad(d.getMonth() + 1) +
|
|
20
|
+
pad(d.getDate()) +
|
|
21
|
+
d.getFullYear() +
|
|
22
|
+
"_" +
|
|
23
|
+
pad(d.getHours()) +
|
|
24
|
+
pad(d.getMinutes())
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** build_03292026_1938 -> 03292026_1938 */
|
|
29
|
+
function stampFromBuildName(buildName) {
|
|
30
|
+
if (!/^build_\d{8}_\d{4}$/.test(buildName)) return null;
|
|
31
|
+
return buildName.slice("build_".length);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} appDir - path to app/ (directory with package.json)
|
|
36
|
+
* @returns {{ buildName: string, buildStamp: string, buildDir: string, ebOutputDir: string }}
|
|
37
|
+
*/
|
|
38
|
+
function resolveBuildLayout(appDir) {
|
|
39
|
+
const envBuildId = process.env.RELEASE_BUILD_ID?.trim();
|
|
40
|
+
const buildName =
|
|
41
|
+
envBuildId && /^build_\d{8}_\d{4}$/.test(envBuildId) ? envBuildId : makeDefaultBuildName();
|
|
42
|
+
const buildStamp = process.env.BUILD_STAMP?.trim() || stampFromBuildName(buildName);
|
|
43
|
+
if (!buildStamp) {
|
|
44
|
+
throw new Error(`[build-layout] could not derive BUILD_STAMP for buildName=${buildName}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const releasesDir = path.join(appDir, "releases");
|
|
48
|
+
const buildDir = path.join(releasesDir, "builder", buildName);
|
|
49
|
+
const ebOutputDir = path.join(buildDir, "eb-output");
|
|
50
|
+
|
|
51
|
+
return { buildName, buildStamp, releasesDir, buildDir, ebOutputDir };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureCleanEbOutput(ebOutputDir) {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(ebOutputDir)) {
|
|
57
|
+
fs.rmSync(ebOutputDir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
fs.mkdirSync(ebOutputDir, { recursive: true });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.warn("[build-layout] ensureCleanEbOutput:", e?.message || e);
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
RELEASE_BUILD_DEV_DIRNAME,
|
|
68
|
+
resolveBuildLayout,
|
|
69
|
+
ensureCleanEbOutput,
|
|
70
|
+
makeDefaultBuildName,
|
|
71
|
+
stampFromBuildName,
|
|
72
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs electron-builder --win with stable pass-through logging.
|
|
3
|
+
* Usage: node windows/build-with-progress.cjs [--pack] [--verbose]
|
|
4
|
+
* --pack Skip "npm run build", run only electron-builder (pack:win style).
|
|
5
|
+
* --verbose Add DEBUG=electron-builder.
|
|
6
|
+
*/
|
|
7
|
+
const { spawn } = require("child_process");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { resolveBuildLayout, ensureCleanEbOutput } = require("./build-layout.cjs");
|
|
10
|
+
|
|
11
|
+
const appDir = path.join(__dirname, "..");
|
|
12
|
+
const isPack = process.argv.includes("--pack");
|
|
13
|
+
const isVerbose = process.argv.includes("--verbose");
|
|
14
|
+
|
|
15
|
+
const ebCli = require.resolve("electron-builder/cli.js");
|
|
16
|
+
|
|
17
|
+
function relForConfig(p) {
|
|
18
|
+
return path.relative(appDir, p).split(path.sep).join("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function run(command, args, opts = {}) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const { pipeOutput: usePipe, env: optsEnv, shell = true } = opts;
|
|
24
|
+
const env = { ...process.env, ...(optsEnv || {}) };
|
|
25
|
+
if (isVerbose) env.DEBUG = "electron-builder";
|
|
26
|
+
const child = spawn(command, args, {
|
|
27
|
+
cwd: appDir,
|
|
28
|
+
env,
|
|
29
|
+
stdio: usePipe ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
30
|
+
shell,
|
|
31
|
+
});
|
|
32
|
+
if (!usePipe) {
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
if (code !== 0) reject(new Error(`Exit ${code}`));
|
|
35
|
+
else resolve();
|
|
36
|
+
});
|
|
37
|
+
child.on("error", reject);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
child.stdout.on("data", (chunk) => {
|
|
41
|
+
const s = chunk.toString();
|
|
42
|
+
process.stdout.write(s);
|
|
43
|
+
});
|
|
44
|
+
child.stderr.on("data", (chunk) => {
|
|
45
|
+
const s = chunk.toString();
|
|
46
|
+
process.stderr.write(s);
|
|
47
|
+
});
|
|
48
|
+
child.on("close", (code, signal) => {
|
|
49
|
+
if (code !== 0) reject(new Error(`Exit ${code}`));
|
|
50
|
+
else resolve();
|
|
51
|
+
});
|
|
52
|
+
child.on("error", (err) => {
|
|
53
|
+
reject(err);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
(async () => {
|
|
59
|
+
try {
|
|
60
|
+
if (!isPack) {
|
|
61
|
+
console.log("Running: npm run build\n");
|
|
62
|
+
await run("npm", ["run", "build"], { stdio: "inherit" });
|
|
63
|
+
}
|
|
64
|
+
const layout = resolveBuildLayout(appDir);
|
|
65
|
+
ensureCleanEbOutput(layout.ebOutputDir);
|
|
66
|
+
const outArg = `--config.directories.output=${relForConfig(layout.ebOutputDir)}`;
|
|
67
|
+
const ebEnv = {
|
|
68
|
+
...process.env,
|
|
69
|
+
BUILD_STAMP: layout.buildStamp,
|
|
70
|
+
RELEASE_BUILD_ID: layout.buildName,
|
|
71
|
+
HSP_EB_OUTPUT: relForConfig(layout.ebOutputDir),
|
|
72
|
+
};
|
|
73
|
+
if (isVerbose) ebEnv.DEBUG = "electron-builder";
|
|
74
|
+
// Never publish from this script: CI has no GH_TOKEN unless set, and releases are created via gh workflow + cleanup (latest.yml).
|
|
75
|
+
console.log(`\nBuild: ${layout.buildName} stamp: ${layout.buildStamp}`);
|
|
76
|
+
console.log(`Output: ${relForConfig(layout.ebOutputDir)}`);
|
|
77
|
+
console.log("Running: electron-builder --win --publish never\n");
|
|
78
|
+
await run(process.execPath, [ebCli, "--win", "--publish", "never", outArg], {
|
|
79
|
+
pipeOutput: true,
|
|
80
|
+
env: ebEnv,
|
|
81
|
+
shell: false,
|
|
82
|
+
});
|
|
83
|
+
console.log("\nRunning: windows/cleanup.cjs\n");
|
|
84
|
+
await run("node", [path.join(__dirname, "cleanup.cjs")], { env: ebEnv });
|
|
85
|
+
} catch (e) {
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
})();
|