elit 3.5.6 → 3.5.7
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/Cargo.toml +1 -1
- package/README.md +1 -1
- package/desktop/build.rs +83 -0
- package/desktop/icon.rs +106 -0
- package/desktop/lib.rs +2 -0
- package/desktop/main.rs +235 -0
- package/desktop/native_main.rs +128 -0
- package/desktop/native_renderer/action_widgets.rs +184 -0
- package/desktop/native_renderer/app_models.rs +171 -0
- package/desktop/native_renderer/app_runtime.rs +140 -0
- package/desktop/native_renderer/container_rendering.rs +610 -0
- package/desktop/native_renderer/content_widgets.rs +634 -0
- package/desktop/native_renderer/css_models.rs +371 -0
- package/desktop/native_renderer/embedded_surfaces.rs +414 -0
- package/desktop/native_renderer/form_controls.rs +516 -0
- package/desktop/native_renderer/interaction_dispatch.rs +89 -0
- package/desktop/native_renderer/runtime_support.rs +135 -0
- package/desktop/native_renderer/utilities.rs +495 -0
- package/desktop/native_renderer/vector_drawing.rs +491 -0
- package/desktop/native_renderer.rs +4122 -0
- package/desktop/runtime/external.rs +422 -0
- package/desktop/runtime/mod.rs +67 -0
- package/desktop/runtime/quickjs.rs +106 -0
- package/desktop/window.rs +383 -0
- package/package.json +6 -3
- package/dist/build.d.mts +0 -20
- package/dist/chokidar.d.mts +0 -134
- package/dist/cli.d.mts +0 -81
- package/dist/config.d.mts +0 -254
- package/dist/coverage.d.mts +0 -85
- package/dist/database.d.mts +0 -52
- package/dist/desktop.d.mts +0 -68
- package/dist/dom.d.mts +0 -87
- package/dist/el.d.mts +0 -208
- package/dist/fs.d.mts +0 -255
- package/dist/hmr.d.mts +0 -38
- package/dist/http.d.mts +0 -169
- package/dist/https.d.mts +0 -108
- package/dist/index.d.mts +0 -13
- package/dist/mime-types.d.mts +0 -48
- package/dist/native.d.mts +0 -136
- package/dist/path.d.mts +0 -163
- package/dist/router.d.mts +0 -49
- package/dist/runtime.d.mts +0 -97
- package/dist/server-D0Dp4R5z.d.mts +0 -449
- package/dist/server.d.mts +0 -7
- package/dist/state.d.mts +0 -117
- package/dist/style.d.mts +0 -232
- package/dist/test-reporter.d.mts +0 -77
- package/dist/test-runtime.d.mts +0 -122
- package/dist/test.d.mts +0 -39
- package/dist/types.d.mts +0 -586
- package/dist/universal.d.mts +0 -21
- package/dist/ws.d.mts +0 -200
- package/dist/wss.d.mts +0 -108
- package/src/build.ts +0 -362
- package/src/chokidar.ts +0 -427
- package/src/cli.ts +0 -1162
- package/src/config.ts +0 -509
- package/src/coverage.ts +0 -1479
- package/src/database.ts +0 -1410
- package/src/desktop-auto-render.ts +0 -317
- package/src/desktop-cli.ts +0 -1533
- package/src/desktop.ts +0 -99
- package/src/dev-build.ts +0 -340
- package/src/dom.ts +0 -901
- package/src/el.ts +0 -183
- package/src/fs.ts +0 -609
- package/src/hmr.ts +0 -149
- package/src/http.ts +0 -856
- package/src/https.ts +0 -411
- package/src/index.ts +0 -16
- package/src/mime-types.ts +0 -222
- package/src/mobile-cli.ts +0 -2313
- package/src/native-background.ts +0 -444
- package/src/native-border.ts +0 -343
- package/src/native-canvas.ts +0 -260
- package/src/native-cli.ts +0 -414
- package/src/native-color.ts +0 -904
- package/src/native-estimation.ts +0 -194
- package/src/native-grid.ts +0 -590
- package/src/native-interaction.ts +0 -1289
- package/src/native-layout.ts +0 -568
- package/src/native-link.ts +0 -76
- package/src/native-render-support.ts +0 -361
- package/src/native-spacing.ts +0 -231
- package/src/native-state.ts +0 -318
- package/src/native-strings.ts +0 -46
- package/src/native-transform.ts +0 -120
- package/src/native-types.ts +0 -439
- package/src/native-typography.ts +0 -254
- package/src/native-units.ts +0 -441
- package/src/native-vector.ts +0 -910
- package/src/native.ts +0 -5606
- package/src/path.ts +0 -493
- package/src/pm-cli.ts +0 -2498
- package/src/preview-build.ts +0 -294
- package/src/render-context.ts +0 -138
- package/src/router.ts +0 -260
- package/src/runtime.ts +0 -97
- package/src/server.ts +0 -2294
- package/src/state.ts +0 -556
- package/src/style.ts +0 -1790
- package/src/test-globals.d.ts +0 -184
- package/src/test-reporter.ts +0 -609
- package/src/test-runtime.ts +0 -1359
- package/src/test.ts +0 -368
- package/src/types.ts +0 -381
- package/src/universal.ts +0 -81
- package/src/wapk-cli.ts +0 -3213
- package/src/workspace-package.ts +0 -102
- package/src/ws.ts +0 -648
- package/src/wss.ts +0 -241
package/src/wapk-cli.ts
DELETED
|
@@ -1,3213 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
|
-
|
|
7
|
-
import { loadConfig, type WapkGoogleDriveConfig, type WapkLockConfig, type WapkRunConfig } from './config';
|
|
8
|
-
|
|
9
|
-
export type WapkRuntimeName = 'node' | 'bun' | 'deno';
|
|
10
|
-
|
|
11
|
-
export interface WapkHeader {
|
|
12
|
-
name: string;
|
|
13
|
-
version: string;
|
|
14
|
-
runtime: WapkRuntimeName;
|
|
15
|
-
entry: string;
|
|
16
|
-
scripts: Record<string, string>;
|
|
17
|
-
port?: number;
|
|
18
|
-
env?: Record<string, string>;
|
|
19
|
-
desktop?: Record<string, unknown>;
|
|
20
|
-
createdAt: string;
|
|
21
|
-
author?: string;
|
|
22
|
-
license?: string;
|
|
23
|
-
homepage?: string;
|
|
24
|
-
bugs?: string | { url: string };
|
|
25
|
-
repository?: string | { type?: string; url?: string };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface WapkFileEntry {
|
|
29
|
-
path: string;
|
|
30
|
-
content: Buffer;
|
|
31
|
-
mode: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface DecodedWapk {
|
|
35
|
-
version: number;
|
|
36
|
-
header: WapkHeader;
|
|
37
|
-
files: WapkFileEntry[];
|
|
38
|
-
lock?: {
|
|
39
|
-
password: true;
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface WapkCredentialsOptions {
|
|
44
|
-
password?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface ResolvedWapkCredentials {
|
|
48
|
-
password: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface ResolvedWapkGoogleDriveConfig {
|
|
52
|
-
fileId: string;
|
|
53
|
-
accessToken: string;
|
|
54
|
-
accessTokenEnv?: string;
|
|
55
|
-
supportsAllDrives?: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface WapkArchiveSnapshot {
|
|
59
|
-
buffer: Buffer;
|
|
60
|
-
signature?: string;
|
|
61
|
-
label?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface WapkArchiveHandle {
|
|
65
|
-
identifier: string;
|
|
66
|
-
label: string;
|
|
67
|
-
readSnapshot: () => Promise<WapkArchiveSnapshot>;
|
|
68
|
-
getSignature: () => Promise<string | undefined>;
|
|
69
|
-
writeBuffer: (buffer: Buffer) => Promise<WapkArchiveSnapshot>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface WapkLockMetadata {
|
|
73
|
-
cipher: 'aes-256-gcm';
|
|
74
|
-
kdf: 'scrypt';
|
|
75
|
-
salt: string;
|
|
76
|
-
iv: string;
|
|
77
|
-
tag: string;
|
|
78
|
-
user?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
type ParsedWapkEnvelope =
|
|
82
|
-
| { version: 1; payload: Buffer }
|
|
83
|
-
| { version: 2; payload: Buffer; lock: WapkLockMetadata };
|
|
84
|
-
|
|
85
|
-
export interface WapkLiveSyncController {
|
|
86
|
-
flush: () => Promise<void>;
|
|
87
|
-
stop: () => Promise<void>;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface PreparedWapkApp {
|
|
91
|
-
archivePath: string;
|
|
92
|
-
archiveLabel: string;
|
|
93
|
-
archiveHandle: WapkArchiveHandle;
|
|
94
|
-
archiveSignature?: string;
|
|
95
|
-
workDir: string;
|
|
96
|
-
entryPath: string;
|
|
97
|
-
header: WapkHeader;
|
|
98
|
-
runtime: WapkRuntimeName;
|
|
99
|
-
syncInterval?: number;
|
|
100
|
-
useWatcher?: boolean;
|
|
101
|
-
watchArchive?: boolean;
|
|
102
|
-
archiveSyncInterval?: number;
|
|
103
|
-
lock?: ResolvedWapkCredentials;
|
|
104
|
-
runtimeWasExplicitlyRequested?: boolean;
|
|
105
|
-
syncIncludesNodeModules: boolean;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
interface WapkLaunchCommand {
|
|
109
|
-
executable: string;
|
|
110
|
-
args: string[];
|
|
111
|
-
env: NodeJS.ProcessEnv;
|
|
112
|
-
label: string;
|
|
113
|
-
shell?: boolean;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
interface WapkProjectConfig {
|
|
117
|
-
name: string;
|
|
118
|
-
version: string;
|
|
119
|
-
runtime: WapkRuntimeName;
|
|
120
|
-
entry: string;
|
|
121
|
-
scripts: Record<string, string>;
|
|
122
|
-
port?: number;
|
|
123
|
-
env?: Record<string, string>;
|
|
124
|
-
desktop?: Record<string, unknown>;
|
|
125
|
-
lock?: WapkLockConfig;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const DEFAULT_WAPK_ENTRY_CANDIDATES = [
|
|
129
|
-
'src/main.ts',
|
|
130
|
-
'src/main.tsx',
|
|
131
|
-
'src/main.js',
|
|
132
|
-
'src/main.jsx',
|
|
133
|
-
'src/index.ts',
|
|
134
|
-
'src/index.tsx',
|
|
135
|
-
'src/index.js',
|
|
136
|
-
'src/index.jsx',
|
|
137
|
-
'main.ts',
|
|
138
|
-
'main.tsx',
|
|
139
|
-
'main.js',
|
|
140
|
-
'main.jsx',
|
|
141
|
-
'index.ts',
|
|
142
|
-
'index.tsx',
|
|
143
|
-
'index.js',
|
|
144
|
-
'index.jsx',
|
|
145
|
-
] as const;
|
|
146
|
-
|
|
147
|
-
const WAPK_MAGIC = Buffer.from('WAPK');
|
|
148
|
-
const WAPK_UNLOCKED_VERSION = 1;
|
|
149
|
-
const WAPK_LOCKED_VERSION = 2;
|
|
150
|
-
const WAPK_VERSION = WAPK_LOCKED_VERSION;
|
|
151
|
-
const DEFAULT_WAPK_PORT = 3000;
|
|
152
|
-
const DEFAULT_IGNORE = [
|
|
153
|
-
] as const;
|
|
154
|
-
|
|
155
|
-
export const WAPK_RUNTIMES: WapkRuntimeName[] = ['node', 'bun', 'deno'];
|
|
156
|
-
const RUNTIME_SYNC_IGNORE = new Set(['.git']);
|
|
157
|
-
const WAPK_CIPHER = 'aes-256-gcm';
|
|
158
|
-
const WAPK_KDF = 'scrypt';
|
|
159
|
-
const WAPK_KEY_LENGTH = 32;
|
|
160
|
-
const WAPK_SALT_LENGTH = 16;
|
|
161
|
-
const WAPK_IV_LENGTH = 12;
|
|
162
|
-
const WAPK_AUTH_TAG_LENGTH = 16;
|
|
163
|
-
const WAPK_SCRYPT_OPTIONS = { N: 16384, r: 8, p: 1 } as const;
|
|
164
|
-
const DEFAULT_GOOGLE_DRIVE_TOKEN_ENV = 'GOOGLE_DRIVE_ACCESS_TOKEN';
|
|
165
|
-
const DEFAULT_WAPK_ONLINE_URL_ENV = 'ELIT_WAPK_ONLINE_URL';
|
|
166
|
-
const DEFAULT_WAPK_ONLINE_URLS = ['https://wapk.d-osc.com/'] as const;
|
|
167
|
-
const WAPK_ONLINE_CREATE_PATH = '/api/shared-session/create';
|
|
168
|
-
const WAPK_ONLINE_READ_PATH = '/api/shared-session/read';
|
|
169
|
-
const WAPK_ONLINE_CLOSE_PATH = '/api/shared-session/close';
|
|
170
|
-
const WAPK_ONLINE_CLOSE_REASON = 'The host stopped sharing this session.';
|
|
171
|
-
const WAPK_ONLINE_KEEPALIVE_INTERVAL_MS = 1000;
|
|
172
|
-
const WAPK_ONLINE_PM_SHUTDOWN_ENV = 'ELIT_PM_WAPK_ONLINE_STDIN_SHUTDOWN';
|
|
173
|
-
const WAPK_ONLINE_PM_SHUTDOWN_COMMAND = '__ELIT_PM_WAPK_ONLINE_SHUTDOWN__';
|
|
174
|
-
|
|
175
|
-
interface WapkOnlineSharedSessionSnapshot {
|
|
176
|
-
originalName: string;
|
|
177
|
-
version: number;
|
|
178
|
-
locked: boolean;
|
|
179
|
-
header: WapkHeader;
|
|
180
|
-
files: Array<{
|
|
181
|
-
path: string;
|
|
182
|
-
mode: number;
|
|
183
|
-
content: string;
|
|
184
|
-
}>;
|
|
185
|
-
currentPath: string;
|
|
186
|
-
hostLabel: string;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
interface WapkOnlineCreateRequest {
|
|
190
|
-
snapshot: WapkOnlineSharedSessionSnapshot;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
interface WapkOnlineCreateResponse {
|
|
194
|
-
ok: boolean;
|
|
195
|
-
joinKey?: string;
|
|
196
|
-
adminToken?: string;
|
|
197
|
-
error?: string;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
interface WapkOnlineReadRequest {
|
|
201
|
-
joinKey: string;
|
|
202
|
-
hostToken: string;
|
|
203
|
-
knownRevision?: number;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
interface WapkOnlineReadResponse {
|
|
207
|
-
ok: boolean;
|
|
208
|
-
revision?: number;
|
|
209
|
-
changed?: boolean;
|
|
210
|
-
snapshot?: WapkOnlineSharedSessionSnapshot;
|
|
211
|
-
error?: string;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
interface WapkOnlineCloseRequest {
|
|
215
|
-
joinKey: string;
|
|
216
|
-
adminToken: string;
|
|
217
|
-
reason?: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
interface WapkOnlineCloseResponse {
|
|
221
|
-
ok: boolean;
|
|
222
|
-
error?: string;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
type WapkOnlineShutdownTrigger =
|
|
226
|
-
| { kind: 'signal'; signal: 'SIGINT' | 'SIGTERM' }
|
|
227
|
-
| { kind: 'pm' };
|
|
228
|
-
|
|
229
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
230
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function normalizeRuntime(value: unknown): WapkRuntimeName | undefined {
|
|
234
|
-
if (typeof value !== 'string') {
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const runtime = value.toLowerCase();
|
|
239
|
-
if (runtime === 'nodejs') {
|
|
240
|
-
return 'node';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return WAPK_RUNTIMES.includes(runtime as WapkRuntimeName)
|
|
244
|
-
? runtime as WapkRuntimeName
|
|
245
|
-
: undefined;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function normalizePort(value: unknown): number | undefined {
|
|
249
|
-
if (value === undefined || value === null || value === '') {
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const port = typeof value === 'number' ? value : Number(value);
|
|
254
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return Math.trunc(port);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function normalizeNonEmptyString(value: unknown): string | undefined {
|
|
262
|
-
if (typeof value !== 'string') {
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const normalized = value.trim();
|
|
267
|
-
return normalized.length > 0 ? normalized : undefined;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function normalizeStringMap(value: unknown): Record<string, string> | undefined {
|
|
271
|
-
if (!isRecord(value)) {
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const normalized: Record<string, string> = {};
|
|
276
|
-
for (const [key, entryValue] of Object.entries(value)) {
|
|
277
|
-
if (typeof entryValue === 'string') {
|
|
278
|
-
normalized[key] = entryValue;
|
|
279
|
-
} else if (typeof entryValue === 'number' || typeof entryValue === 'boolean') {
|
|
280
|
-
normalized[key] = String(entryValue);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return Object.keys(normalized).length > 0 ? normalized : {};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function normalizeDesktopConfig(value: unknown): Record<string, unknown> | undefined {
|
|
288
|
-
return isRecord(value) ? { ...value } : undefined;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function normalizeWapkLockConfig(value: unknown): WapkLockConfig | undefined {
|
|
292
|
-
if (!isRecord(value)) {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const password = normalizeNonEmptyString(value.password);
|
|
297
|
-
|
|
298
|
-
if (!password) {
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
password,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function normalizeWapkConfig(value: unknown): Partial<WapkProjectConfig> {
|
|
308
|
-
if (!isRecord(value)) {
|
|
309
|
-
return {};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
name: typeof value.name === 'string' ? value.name : undefined,
|
|
314
|
-
version: typeof value.version === 'string' ? value.version : undefined,
|
|
315
|
-
runtime: normalizeRuntime(value.runtime ?? value.engine),
|
|
316
|
-
entry: typeof value.entry === 'string' ? value.entry : undefined,
|
|
317
|
-
scripts: normalizeStringMap(value.scripts ?? value.script),
|
|
318
|
-
port: normalizePort(value.port),
|
|
319
|
-
env: normalizeStringMap(value.env),
|
|
320
|
-
desktop: normalizeDesktopConfig(value.desktop),
|
|
321
|
-
lock: normalizeWapkLockConfig(value.lock),
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function normalizeWapkGoogleDriveConfig(value: unknown): WapkGoogleDriveConfig | undefined {
|
|
326
|
-
if (!isRecord(value)) {
|
|
327
|
-
return undefined;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const normalized: WapkGoogleDriveConfig = {
|
|
331
|
-
fileId: normalizeNonEmptyString(value.fileId),
|
|
332
|
-
accessToken: normalizeNonEmptyString(value.accessToken),
|
|
333
|
-
accessTokenEnv: normalizeNonEmptyString(value.accessTokenEnv),
|
|
334
|
-
supportsAllDrives: normalizeBoolean(value.supportsAllDrives),
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
return Object.values(normalized).some((entry) => entry !== undefined)
|
|
338
|
-
? normalized
|
|
339
|
-
: undefined;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function normalizeBoolean(value: unknown): boolean | undefined {
|
|
343
|
-
return typeof value === 'boolean' ? value : undefined;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function normalizeSyncInterval(value: unknown): number | undefined {
|
|
347
|
-
if (value === undefined || value === null || value === '') {
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const interval = typeof value === 'number' ? value : Number(value);
|
|
352
|
-
if (!Number.isFinite(interval) || interval < 50) {
|
|
353
|
-
return undefined;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return Math.trunc(interval);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function normalizeWapkRunConfig(value: unknown): WapkRunConfig | undefined {
|
|
360
|
-
if (!isRecord(value)) {
|
|
361
|
-
return undefined;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const normalized: WapkRunConfig = {
|
|
365
|
-
file: normalizeNonEmptyString(value.file),
|
|
366
|
-
googleDrive: normalizeWapkGoogleDriveConfig(value.googleDrive),
|
|
367
|
-
online: normalizeBoolean(value.online),
|
|
368
|
-
onlineUrl: normalizeNonEmptyString(value.onlineUrl),
|
|
369
|
-
runtime: normalizeRuntime(value.runtime),
|
|
370
|
-
syncInterval: normalizeSyncInterval(value.syncInterval),
|
|
371
|
-
useWatcher: normalizeBoolean(value.useWatcher),
|
|
372
|
-
watchArchive: normalizeBoolean(value.watchArchive),
|
|
373
|
-
archiveSyncInterval: normalizeSyncInterval(value.archiveSyncInterval),
|
|
374
|
-
password: normalizeNonEmptyString(value.password),
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
return Object.values(normalized).some((entry) => entry !== undefined)
|
|
378
|
-
? normalized
|
|
379
|
-
: undefined;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function sanitizePackageName(name: string): string {
|
|
383
|
-
const sanitized = name
|
|
384
|
-
.trim()
|
|
385
|
-
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-')
|
|
386
|
-
.replace(/\s+/g, '-')
|
|
387
|
-
.replace(/-+/g, '-');
|
|
388
|
-
|
|
389
|
-
return sanitized.length > 0 ? sanitized : 'app';
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function ensureBufferRange(buffer: Buffer, start: number, length: number, field: string): void {
|
|
393
|
-
if (start < 0 || start + length > buffer.length) {
|
|
394
|
-
throw new Error(`Invalid WAPK file: truncated ${field}.`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function normalizeArchivePath(baseDir: string, value: string): string {
|
|
399
|
-
const resolvedPath = resolve(baseDir, value);
|
|
400
|
-
const relativePath = relative(baseDir, resolvedPath);
|
|
401
|
-
|
|
402
|
-
if (!relativePath || relativePath.startsWith('..') || isAbsolute(relativePath)) {
|
|
403
|
-
throw new Error(`WAPK entry must stay inside the package directory: ${value}`);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return relativePath.split('\\').join('/');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function stripQuotes(value: string): string {
|
|
410
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
411
|
-
return value.slice(1, -1);
|
|
412
|
-
}
|
|
413
|
-
return value;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function tokenizeCommand(command: string): string[] {
|
|
417
|
-
return (command.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map(stripQuotes);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function findScriptEntry(tokens: string[], startIndex: number): string | undefined {
|
|
421
|
-
const candidates = tokens.slice(startIndex).filter((token) => !token.startsWith('-'));
|
|
422
|
-
return candidates.length > 0 ? candidates[candidates.length - 1] : undefined;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function inferRuntimeAndEntryFromScript(script: string | undefined): { runtime?: WapkRuntimeName; entry?: string } {
|
|
426
|
-
if (!script) {
|
|
427
|
-
return {};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const tokens = tokenizeCommand(script);
|
|
431
|
-
if (tokens.length === 0) {
|
|
432
|
-
return {};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const command = tokens[0];
|
|
436
|
-
if (command === 'bun') {
|
|
437
|
-
if (tokens[1] === 'run') {
|
|
438
|
-
return { runtime: 'bun', entry: findScriptEntry(tokens, 2) };
|
|
439
|
-
}
|
|
440
|
-
return { runtime: 'bun', entry: findScriptEntry(tokens, 1) };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (command === 'deno' && tokens[1] === 'run') {
|
|
444
|
-
return { runtime: 'deno', entry: findScriptEntry(tokens, 2) };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (command === 'node' || command === 'nodejs') {
|
|
448
|
-
return { runtime: 'node', entry: findScriptEntry(tokens, 1) };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (command === 'tsx' || command === 'ts-node') {
|
|
452
|
-
return { runtime: 'node', entry: findScriptEntry(tokens, 1) };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return {};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function resolveBuildEntryCandidate(config: unknown): string | undefined {
|
|
459
|
-
if (!isRecord(config)) {
|
|
460
|
-
return undefined;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const builds = Array.isArray(config.build)
|
|
464
|
-
? config.build
|
|
465
|
-
: [config.build];
|
|
466
|
-
|
|
467
|
-
for (const build of builds) {
|
|
468
|
-
if (!isRecord(build)) {
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const entry = normalizeNonEmptyString(build.entry);
|
|
473
|
-
if (entry) {
|
|
474
|
-
return entry;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return undefined;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function resolveExistingWapkEntry(directory: string, candidates: readonly (string | undefined)[]): string | undefined {
|
|
482
|
-
for (const candidate of candidates) {
|
|
483
|
-
const normalizedCandidate = normalizeNonEmptyString(candidate);
|
|
484
|
-
if (!normalizedCandidate) {
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
const entry = normalizeArchivePath(directory, normalizedCandidate);
|
|
490
|
-
const entryPath = resolve(directory, entry);
|
|
491
|
-
if (existsSync(entryPath) && statSync(entryPath).isFile()) {
|
|
492
|
-
return entry;
|
|
493
|
-
}
|
|
494
|
-
} catch {
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return undefined;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function readJsonFile(filePath: string): Record<string, unknown> | undefined {
|
|
503
|
-
if (!existsSync(filePath)) {
|
|
504
|
-
return undefined;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
508
|
-
if (!isRecord(parsed)) {
|
|
509
|
-
throw new Error(`Expected a JSON object in ${filePath}`);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
return parsed;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function hasCredentialInput(value: WapkCredentialsOptions | undefined): boolean {
|
|
516
|
-
return Boolean(typeof value?.password === 'string' && value.password.length > 0);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function resolvePasswordFromInput(
|
|
520
|
-
value: WapkCredentialsOptions | undefined,
|
|
521
|
-
): string | undefined {
|
|
522
|
-
if (!value) {
|
|
523
|
-
return undefined;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (typeof value.password === 'string' && value.password.length > 0) {
|
|
527
|
-
return value.password;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return undefined;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function resolvePackLockCredentials(
|
|
534
|
-
configLock: WapkLockConfig | undefined,
|
|
535
|
-
overrideLock: WapkCredentialsOptions | undefined,
|
|
536
|
-
): ResolvedWapkCredentials | undefined {
|
|
537
|
-
const configPassword = normalizeNonEmptyString(configLock?.password);
|
|
538
|
-
const password = resolvePasswordFromInput(overrideLock)
|
|
539
|
-
?? configPassword;
|
|
540
|
-
const shouldLock = Boolean(configPassword) || hasCredentialInput(overrideLock);
|
|
541
|
-
|
|
542
|
-
if (!shouldLock) {
|
|
543
|
-
return undefined;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (!password) {
|
|
547
|
-
throw new Error('WAPK lock requires a password. Provide --password or config.wapk.lock.password.');
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return { password };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function resolveArchiveCredentials(
|
|
554
|
-
value: WapkCredentialsOptions | undefined,
|
|
555
|
-
): ResolvedWapkCredentials | undefined {
|
|
556
|
-
if (!hasCredentialInput(value)) {
|
|
557
|
-
return undefined;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const password = resolvePasswordFromInput(value);
|
|
561
|
-
if (!password) {
|
|
562
|
-
throw new Error('WAPK archive is password-protected. Provide --password to unlock it.');
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return { password };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function buildWapkAuthData(legacyUser?: string): Buffer {
|
|
569
|
-
return Buffer.from(legacyUser ? `WAPK:${legacyUser}` : 'WAPK', 'utf8');
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function encryptWapkPayload(payload: Buffer, lock: ResolvedWapkCredentials): { metadata: WapkLockMetadata; payload: Buffer } {
|
|
573
|
-
const salt = randomBytes(WAPK_SALT_LENGTH);
|
|
574
|
-
const iv = randomBytes(WAPK_IV_LENGTH);
|
|
575
|
-
const key = scryptSync(lock.password, salt, WAPK_KEY_LENGTH, WAPK_SCRYPT_OPTIONS);
|
|
576
|
-
const cipher = createCipheriv(WAPK_CIPHER, key, iv);
|
|
577
|
-
cipher.setAAD(buildWapkAuthData());
|
|
578
|
-
|
|
579
|
-
const encryptedPayload = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
580
|
-
const tag = cipher.getAuthTag();
|
|
581
|
-
|
|
582
|
-
return {
|
|
583
|
-
metadata: {
|
|
584
|
-
cipher: WAPK_CIPHER,
|
|
585
|
-
kdf: WAPK_KDF,
|
|
586
|
-
salt: salt.toString('base64'),
|
|
587
|
-
iv: iv.toString('base64'),
|
|
588
|
-
tag: tag.toString('base64'),
|
|
589
|
-
},
|
|
590
|
-
payload: encryptedPayload,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function decodeLockBuffer(value: string, expectedLength: number, field: string): Buffer {
|
|
595
|
-
const buffer = Buffer.from(value, 'base64');
|
|
596
|
-
if (buffer.length !== expectedLength) {
|
|
597
|
-
throw new Error(`Invalid WAPK file: bad ${field}.`);
|
|
598
|
-
}
|
|
599
|
-
return buffer;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function decryptWapkPayload(payload: Buffer, lock: WapkLockMetadata, credentials: ResolvedWapkCredentials): Buffer {
|
|
603
|
-
const salt = decodeLockBuffer(lock.salt, WAPK_SALT_LENGTH, 'lock salt');
|
|
604
|
-
const iv = decodeLockBuffer(lock.iv, WAPK_IV_LENGTH, 'lock iv');
|
|
605
|
-
const tag = decodeLockBuffer(lock.tag, WAPK_AUTH_TAG_LENGTH, 'lock auth tag');
|
|
606
|
-
const key = scryptSync(credentials.password, salt, WAPK_KEY_LENGTH, WAPK_SCRYPT_OPTIONS);
|
|
607
|
-
const decipher = createDecipheriv(WAPK_CIPHER, key, iv);
|
|
608
|
-
decipher.setAAD(buildWapkAuthData(lock.user));
|
|
609
|
-
decipher.setAuthTag(tag);
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
return Buffer.concat([decipher.update(payload), decipher.final()]);
|
|
613
|
-
} catch {
|
|
614
|
-
throw new Error('Invalid WAPK credentials.');
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function parseWapkLockMetadata(rawMetadata: unknown): WapkLockMetadata {
|
|
619
|
-
if (!isRecord(rawMetadata)) {
|
|
620
|
-
throw new Error('Invalid WAPK file: lock metadata must be an object.');
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (rawMetadata.cipher !== WAPK_CIPHER) {
|
|
624
|
-
throw new Error(`Unsupported WAPK cipher: ${String(rawMetadata.cipher)}`);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (rawMetadata.kdf !== WAPK_KDF) {
|
|
628
|
-
throw new Error(`Unsupported WAPK KDF: ${String(rawMetadata.kdf)}`);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (typeof rawMetadata.salt !== 'string' || typeof rawMetadata.iv !== 'string' || typeof rawMetadata.tag !== 'string') {
|
|
632
|
-
throw new Error('Invalid WAPK file: lock metadata is incomplete.');
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return {
|
|
636
|
-
cipher: WAPK_CIPHER,
|
|
637
|
-
kdf: WAPK_KDF,
|
|
638
|
-
salt: rawMetadata.salt,
|
|
639
|
-
iv: rawMetadata.iv,
|
|
640
|
-
tag: rawMetadata.tag,
|
|
641
|
-
user: normalizeNonEmptyString(rawMetadata.user),
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function parseWapkEnvelope(buffer: Buffer): ParsedWapkEnvelope {
|
|
646
|
-
let offset = 0;
|
|
647
|
-
|
|
648
|
-
ensureBufferRange(buffer, offset, 4, 'magic');
|
|
649
|
-
if (!buffer.slice(offset, offset + 4).equals(WAPK_MAGIC)) {
|
|
650
|
-
throw new Error('Invalid WAPK file: bad magic bytes.');
|
|
651
|
-
}
|
|
652
|
-
offset += 4;
|
|
653
|
-
|
|
654
|
-
ensureBufferRange(buffer, offset, 2, 'version');
|
|
655
|
-
const version = buffer.readUInt16LE(offset);
|
|
656
|
-
offset += 2;
|
|
657
|
-
if (version > WAPK_VERSION) {
|
|
658
|
-
throw new Error(`Unsupported WAPK version: ${version}`);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (version === WAPK_UNLOCKED_VERSION) {
|
|
662
|
-
return {
|
|
663
|
-
version: WAPK_UNLOCKED_VERSION,
|
|
664
|
-
payload: buffer.subarray(offset),
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
ensureBufferRange(buffer, offset, 4, 'lock metadata length');
|
|
669
|
-
const metadataLength = buffer.readUInt32LE(offset);
|
|
670
|
-
offset += 4;
|
|
671
|
-
|
|
672
|
-
ensureBufferRange(buffer, offset, metadataLength, 'lock metadata');
|
|
673
|
-
const metadata = JSON.parse(buffer.slice(offset, offset + metadataLength).toString('utf8'));
|
|
674
|
-
offset += metadataLength;
|
|
675
|
-
|
|
676
|
-
ensureBufferRange(buffer, offset, 4, 'encrypted payload length');
|
|
677
|
-
const payloadLength = buffer.readUInt32LE(offset);
|
|
678
|
-
offset += 4;
|
|
679
|
-
|
|
680
|
-
ensureBufferRange(buffer, offset, payloadLength, 'encrypted payload');
|
|
681
|
-
const payload = buffer.slice(offset, offset + payloadLength);
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
version: WAPK_LOCKED_VERSION,
|
|
685
|
-
payload,
|
|
686
|
-
lock: parseWapkLockMetadata(metadata),
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async function readWapkProjectConfig(directory: string): Promise<WapkProjectConfig> {
|
|
691
|
-
const packageJsonPath = join(directory, 'package.json');
|
|
692
|
-
const elitConfig = await loadConfig(directory);
|
|
693
|
-
const elitWapkConfig = normalizeWapkConfig(elitConfig?.wapk);
|
|
694
|
-
const packageJson = readJsonFile(packageJsonPath);
|
|
695
|
-
|
|
696
|
-
const packageScripts = normalizeStringMap(packageJson?.scripts) ?? {};
|
|
697
|
-
const selectedScripts = elitWapkConfig.scripts ?? packageScripts;
|
|
698
|
-
const inferred = inferRuntimeAndEntryFromScript(selectedScripts.start ?? packageScripts.start);
|
|
699
|
-
const name = typeof elitWapkConfig.name === 'string'
|
|
700
|
-
? elitWapkConfig.name
|
|
701
|
-
: typeof packageJson?.name === 'string'
|
|
702
|
-
? packageJson.name
|
|
703
|
-
: basename(directory);
|
|
704
|
-
const version = typeof elitWapkConfig.version === 'string'
|
|
705
|
-
? elitWapkConfig.version
|
|
706
|
-
: typeof packageJson?.version === 'string'
|
|
707
|
-
? packageJson.version
|
|
708
|
-
: '1.0.0';
|
|
709
|
-
const runtime = elitWapkConfig.runtime
|
|
710
|
-
?? inferred.runtime
|
|
711
|
-
?? 'node';
|
|
712
|
-
const configuredEntry = normalizeNonEmptyString(elitWapkConfig.entry);
|
|
713
|
-
const entry = configuredEntry
|
|
714
|
-
? normalizeArchivePath(directory, configuredEntry)
|
|
715
|
-
: resolveExistingWapkEntry(directory, [
|
|
716
|
-
typeof packageJson?.main === 'string' ? packageJson.main : undefined,
|
|
717
|
-
typeof packageJson?.module === 'string' ? packageJson.module : undefined,
|
|
718
|
-
inferred.entry,
|
|
719
|
-
resolveBuildEntryCandidate(elitConfig),
|
|
720
|
-
...DEFAULT_WAPK_ENTRY_CANDIDATES,
|
|
721
|
-
]);
|
|
722
|
-
|
|
723
|
-
if (!entry) {
|
|
724
|
-
throw new Error(
|
|
725
|
-
`WAPK entry could not be inferred. Set wapk.entry, package.json main, build.entry, or add one of: ${DEFAULT_WAPK_ENTRY_CANDIDATES.join(', ')}`,
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const entryPath = resolve(directory, entry);
|
|
730
|
-
|
|
731
|
-
if (!existsSync(entryPath) || !statSync(entryPath).isFile()) {
|
|
732
|
-
throw new Error(`WAPK entry not found: ${entryPath}`);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return {
|
|
736
|
-
name,
|
|
737
|
-
version,
|
|
738
|
-
runtime,
|
|
739
|
-
entry,
|
|
740
|
-
scripts: selectedScripts,
|
|
741
|
-
port: elitWapkConfig.port,
|
|
742
|
-
env: elitWapkConfig.env,
|
|
743
|
-
desktop: elitWapkConfig.desktop,
|
|
744
|
-
lock: elitWapkConfig.lock,
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function readIgnorePatterns(directory: string): string[] {
|
|
749
|
-
return readLineIgnorePatterns(join(directory, '.wapkignore'));
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function readLineIgnorePatterns(filePath: string): string[] {
|
|
753
|
-
if (!existsSync(filePath)) {
|
|
754
|
-
return [];
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
return readFileSync(filePath, 'utf8')
|
|
758
|
-
.split(/\r?\n/)
|
|
759
|
-
.map((line) => line.trim())
|
|
760
|
-
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function normalizePackageEntry(value: string): string | undefined {
|
|
764
|
-
const normalized = normalizeNonEmptyString(value)?.replace(/^[.][\\/]/, '').split('\\').join('/');
|
|
765
|
-
return normalized && normalized.length > 0 ? normalized : undefined;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function collectStringLeaves(value: unknown, target: Set<string>): void {
|
|
769
|
-
if (typeof value === 'string') {
|
|
770
|
-
const normalized = normalizePackageEntry(value);
|
|
771
|
-
if (normalized) {
|
|
772
|
-
target.add(normalized);
|
|
773
|
-
}
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (Array.isArray(value)) {
|
|
778
|
-
for (const entry of value) {
|
|
779
|
-
collectStringLeaves(entry, target);
|
|
780
|
-
}
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (!isRecord(value)) {
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
for (const entry of Object.values(value)) {
|
|
789
|
-
collectStringLeaves(entry, target);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function collectPackageTopLevelEntries(directory: string): string[] {
|
|
794
|
-
return readdirSync(directory, { withFileTypes: true })
|
|
795
|
-
.filter((entry) => entry.name !== 'node_modules' && entry.name !== '.git' && !entry.name.startsWith('.'))
|
|
796
|
-
.map((entry) => entry.name);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function collectLinkedPackageCandidates(directory: string): string[] {
|
|
800
|
-
const packageJson = readJsonFile(join(directory, 'package.json'));
|
|
801
|
-
if (!packageJson) {
|
|
802
|
-
return [];
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const candidates = new Set<string>(['package.json']);
|
|
806
|
-
const packageFiles = Array.isArray(packageJson.files)
|
|
807
|
-
? packageJson.files.filter((value): value is string => typeof value === 'string')
|
|
808
|
-
: [];
|
|
809
|
-
|
|
810
|
-
if (packageFiles.length > 0) {
|
|
811
|
-
for (const file of packageFiles) {
|
|
812
|
-
const normalized = normalizePackageEntry(file);
|
|
813
|
-
if (normalized) {
|
|
814
|
-
candidates.add(normalized);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
} else {
|
|
818
|
-
const runtimeEntries = new Set<string>();
|
|
819
|
-
for (const value of [packageJson.main, packageJson.module, packageJson.browser, packageJson.types, packageJson.typings]) {
|
|
820
|
-
collectStringLeaves(value, runtimeEntries);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (typeof packageJson.bin === 'string') {
|
|
824
|
-
collectStringLeaves(packageJson.bin, runtimeEntries);
|
|
825
|
-
} else if (isRecord(packageJson.bin)) {
|
|
826
|
-
collectStringLeaves(Object.values(packageJson.bin), runtimeEntries);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
collectStringLeaves(packageJson.exports, runtimeEntries);
|
|
830
|
-
|
|
831
|
-
for (const entry of runtimeEntries) {
|
|
832
|
-
const rootEntry = entry.includes('/') ? entry.split('/')[0] ?? entry : entry;
|
|
833
|
-
if (rootEntry) {
|
|
834
|
-
candidates.add(rootEntry);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (candidates.size === 1) {
|
|
839
|
-
for (const fallback of ['dist', 'src']) {
|
|
840
|
-
if (existsSync(join(directory, fallback))) {
|
|
841
|
-
candidates.add(fallback);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (candidates.size === 1) {
|
|
847
|
-
for (const fallbackEntry of collectPackageTopLevelEntries(directory)) {
|
|
848
|
-
candidates.add(fallbackEntry);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
for (const metadataFile of ['README.md', 'README', 'LICENSE', 'LICENSE.md', 'LICENSE.txt', 'LICENCE', 'CHANGELOG.md']) {
|
|
854
|
-
if (existsSync(join(directory, metadataFile))) {
|
|
855
|
-
candidates.add(metadataFile);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
return [...candidates];
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function prefixCollectedFiles(files: readonly WapkFileEntry[], prefix: string): WapkFileEntry[] {
|
|
863
|
-
return files.map((file) => ({
|
|
864
|
-
...file,
|
|
865
|
-
path: `${prefix}/${file.path}`,
|
|
866
|
-
}));
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function collectInstalledPackageNames(packageJson: Record<string, unknown>): string[] {
|
|
870
|
-
const packageNames = new Set<string>();
|
|
871
|
-
|
|
872
|
-
for (const dependencyGroup of [
|
|
873
|
-
packageJson.dependencies,
|
|
874
|
-
packageJson.optionalDependencies,
|
|
875
|
-
packageJson.peerDependencies,
|
|
876
|
-
]) {
|
|
877
|
-
if (!isRecord(dependencyGroup)) {
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
for (const packageName of Object.keys(dependencyGroup)) {
|
|
882
|
-
const normalizedPackageName = normalizeNonEmptyString(packageName);
|
|
883
|
-
if (normalizedPackageName) {
|
|
884
|
-
packageNames.add(normalizedPackageName);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
return [...packageNames];
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function resolveInstalledPackageDirectory(startDirectory: string, packageName: string): string | undefined {
|
|
893
|
-
let currentDirectory = resolve(startDirectory);
|
|
894
|
-
|
|
895
|
-
while (true) {
|
|
896
|
-
const candidate = join(currentDirectory, 'node_modules', ...packageName.split('/'));
|
|
897
|
-
if (existsSync(candidate)) {
|
|
898
|
-
return candidate;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const parentDirectory = dirname(currentDirectory);
|
|
902
|
-
if (parentDirectory === currentDirectory) {
|
|
903
|
-
return undefined;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
currentDirectory = parentDirectory;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function collectLinkedPackageFiles(
|
|
911
|
-
linkPath: string,
|
|
912
|
-
archivePrefix: string,
|
|
913
|
-
seenPackages: Set<string> = new Set(),
|
|
914
|
-
): WapkFileEntry[] {
|
|
915
|
-
const packageRoot = realpathSync(linkPath);
|
|
916
|
-
if (seenPackages.has(packageRoot)) {
|
|
917
|
-
return [];
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
seenPackages.add(packageRoot);
|
|
921
|
-
const candidates = collectLinkedPackageCandidates(packageRoot);
|
|
922
|
-
if (candidates.length === 0) {
|
|
923
|
-
return [];
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const ignorePatterns = ['node_modules', ...readLineIgnorePatterns(join(packageRoot, '.npmignore'))];
|
|
927
|
-
const collected = new Map<string, WapkFileEntry>();
|
|
928
|
-
|
|
929
|
-
for (const candidate of candidates) {
|
|
930
|
-
let relativeCandidate: string;
|
|
931
|
-
try {
|
|
932
|
-
relativeCandidate = normalizeArchivePath(packageRoot, candidate);
|
|
933
|
-
} catch {
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const fullCandidatePath = resolve(packageRoot, relativeCandidate);
|
|
938
|
-
if (!existsSync(fullCandidatePath)) {
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const stats = statSync(fullCandidatePath);
|
|
943
|
-
const entries = stats.isDirectory()
|
|
944
|
-
? prefixCollectedFiles(collectFiles(fullCandidatePath, packageRoot, ignorePatterns), archivePrefix)
|
|
945
|
-
: stats.isFile()
|
|
946
|
-
? [{
|
|
947
|
-
path: `${archivePrefix}/${relativeCandidate}`,
|
|
948
|
-
content: readFileSync(fullCandidatePath),
|
|
949
|
-
mode: stats.mode,
|
|
950
|
-
}]
|
|
951
|
-
: [];
|
|
952
|
-
|
|
953
|
-
for (const entry of entries) {
|
|
954
|
-
collected.set(entry.path, entry);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const packageJson = readJsonFile(join(packageRoot, 'package.json'));
|
|
959
|
-
if (packageJson) {
|
|
960
|
-
for (const dependencyName of collectInstalledPackageNames(packageJson)) {
|
|
961
|
-
const dependencyDirectory = resolveInstalledPackageDirectory(packageRoot, dependencyName);
|
|
962
|
-
if (!dependencyDirectory) {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const dependencyEntries = collectLinkedPackageFiles(
|
|
967
|
-
dependencyDirectory,
|
|
968
|
-
resolveLinkedDependencyArchivePrefix(archivePrefix, dependencyName),
|
|
969
|
-
seenPackages,
|
|
970
|
-
);
|
|
971
|
-
for (const entry of dependencyEntries) {
|
|
972
|
-
collected.set(entry.path, entry);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
return [...collected.values()];
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function resolveLinkedDependencyArchivePrefix(archivePrefix: string, dependencyName: string): string {
|
|
981
|
-
const normalizedPrefix = archivePrefix.split('\\').join('/');
|
|
982
|
-
const marker = '/node_modules/';
|
|
983
|
-
const markerIndex = normalizedPrefix.lastIndexOf(marker);
|
|
984
|
-
|
|
985
|
-
if (markerIndex === -1) {
|
|
986
|
-
return `node_modules/${dependencyName}`;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
return `${normalizedPrefix.slice(0, markerIndex + marker.length)}${dependencyName}`;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function shouldIgnore(relativePath: string, ignorePatterns: readonly string[]): boolean {
|
|
993
|
-
const pathParts = relativePath.split('/');
|
|
994
|
-
for (const pattern of ignorePatterns) {
|
|
995
|
-
if (relativePath === pattern || pathParts.includes(pattern)) {
|
|
996
|
-
return true;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
if (pattern.endsWith('*')) {
|
|
1000
|
-
const prefix = pattern.slice(0, -1);
|
|
1001
|
-
if (relativePath.startsWith(prefix) || pathParts.some((part) => part.startsWith(prefix))) {
|
|
1002
|
-
return true;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
if (pattern.startsWith('*.') && relativePath.endsWith(pattern.slice(1))) {
|
|
1007
|
-
return true;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
return false;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function collectFiles(directory: string, baseDirectory: string, ignorePatterns: readonly string[]): WapkFileEntry[] {
|
|
1015
|
-
const files: WapkFileEntry[] = [];
|
|
1016
|
-
const entries = readdirSync(directory, { withFileTypes: true });
|
|
1017
|
-
|
|
1018
|
-
for (const entry of entries) {
|
|
1019
|
-
const fullPath = join(directory, entry.name);
|
|
1020
|
-
const relativePath = relative(baseDirectory, fullPath).split('\\').join('/');
|
|
1021
|
-
if (shouldIgnore(relativePath, ignorePatterns)) {
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (entry.isSymbolicLink()) {
|
|
1026
|
-
try {
|
|
1027
|
-
const stats = statSync(fullPath);
|
|
1028
|
-
if (stats.isDirectory() && relativePath.split('/').includes('node_modules')) {
|
|
1029
|
-
files.push(...collectLinkedPackageFiles(fullPath, relativePath));
|
|
1030
|
-
} else if (stats.isFile()) {
|
|
1031
|
-
files.push({
|
|
1032
|
-
path: relativePath,
|
|
1033
|
-
content: readFileSync(fullPath),
|
|
1034
|
-
mode: stats.mode,
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
} catch {
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
continue;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (entry.isDirectory()) {
|
|
1045
|
-
files.push(...collectFiles(fullPath, baseDirectory, ignorePatterns));
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
if (!entry.isFile()) {
|
|
1050
|
-
continue;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
const stat = statSync(fullPath);
|
|
1054
|
-
files.push({
|
|
1055
|
-
path: relativePath,
|
|
1056
|
-
content: readFileSync(fullPath),
|
|
1057
|
-
mode: stat.mode,
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return files;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
function collectNestedEntryPackageFiles(sourceDirectory: string, entry: string): WapkFileEntry[] {
|
|
1065
|
-
const entryPath = resolve(sourceDirectory, entry);
|
|
1066
|
-
const entryPackageDirectory = findNearestPackageDirectory(dirname(entryPath), sourceDirectory);
|
|
1067
|
-
|
|
1068
|
-
if (!entryPackageDirectory || resolve(entryPackageDirectory) === resolve(sourceDirectory)) {
|
|
1069
|
-
return [];
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
const relativePackageDirectory = relative(sourceDirectory, entryPackageDirectory).split('\\').join('/');
|
|
1073
|
-
if (!relativePackageDirectory || relativePackageDirectory.startsWith('..') || isAbsolute(relativePackageDirectory)) {
|
|
1074
|
-
return [];
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
const packageJson = readJsonFile(join(entryPackageDirectory, 'package.json'));
|
|
1078
|
-
if (!packageJson) {
|
|
1079
|
-
return [];
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
const collected = new Map<string, WapkFileEntry>();
|
|
1083
|
-
const seenPackages = new Set<string>();
|
|
1084
|
-
|
|
1085
|
-
for (const dependencyName of collectInstalledPackageNames(packageJson)) {
|
|
1086
|
-
const dependencyDirectory = resolveInstalledPackageDirectory(entryPackageDirectory, dependencyName)
|
|
1087
|
-
?? resolveInstalledPackageDirectory(sourceDirectory, dependencyName);
|
|
1088
|
-
if (!dependencyDirectory) {
|
|
1089
|
-
continue;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
const dependencyEntries = collectLinkedPackageFiles(
|
|
1093
|
-
dependencyDirectory,
|
|
1094
|
-
`${relativePackageDirectory}/node_modules/${dependencyName}`,
|
|
1095
|
-
seenPackages,
|
|
1096
|
-
);
|
|
1097
|
-
|
|
1098
|
-
for (const entryFile of dependencyEntries) {
|
|
1099
|
-
collected.set(entryFile.path, entryFile);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
return [...collected.values()];
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function encodeWapkPayload(header: WapkHeader, files: readonly WapkFileEntry[]): Buffer {
|
|
1107
|
-
const headerBuffer = Buffer.from(JSON.stringify(header, null, 2), 'utf8');
|
|
1108
|
-
let totalSize = 4 + headerBuffer.length + 4;
|
|
1109
|
-
|
|
1110
|
-
for (const file of files) {
|
|
1111
|
-
const pathBuffer = Buffer.from(file.path, 'utf8');
|
|
1112
|
-
totalSize += 2 + pathBuffer.length + 4 + 4 + file.content.length;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const buffer = Buffer.allocUnsafe(totalSize);
|
|
1116
|
-
let offset = 0;
|
|
1117
|
-
|
|
1118
|
-
buffer.writeUInt32LE(headerBuffer.length, offset);
|
|
1119
|
-
offset += 4;
|
|
1120
|
-
headerBuffer.copy(buffer, offset);
|
|
1121
|
-
offset += headerBuffer.length;
|
|
1122
|
-
|
|
1123
|
-
buffer.writeUInt32LE(files.length, offset);
|
|
1124
|
-
offset += 4;
|
|
1125
|
-
|
|
1126
|
-
for (const file of files) {
|
|
1127
|
-
const pathBuffer = Buffer.from(file.path, 'utf8');
|
|
1128
|
-
buffer.writeUInt16LE(pathBuffer.length, offset);
|
|
1129
|
-
offset += 2;
|
|
1130
|
-
pathBuffer.copy(buffer, offset);
|
|
1131
|
-
offset += pathBuffer.length;
|
|
1132
|
-
buffer.writeUInt32LE(file.mode ?? 0o644, offset);
|
|
1133
|
-
offset += 4;
|
|
1134
|
-
buffer.writeUInt32LE(file.content.length, offset);
|
|
1135
|
-
offset += 4;
|
|
1136
|
-
file.content.copy(buffer, offset);
|
|
1137
|
-
offset += file.content.length;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
return buffer;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function encodeWapk(header: WapkHeader, files: readonly WapkFileEntry[], lock?: ResolvedWapkCredentials): Buffer {
|
|
1144
|
-
const payload = encodeWapkPayload(header, files);
|
|
1145
|
-
|
|
1146
|
-
if (!lock) {
|
|
1147
|
-
const buffer = Buffer.allocUnsafe(WAPK_MAGIC.length + 2 + payload.length);
|
|
1148
|
-
let offset = 0;
|
|
1149
|
-
|
|
1150
|
-
WAPK_MAGIC.copy(buffer, offset);
|
|
1151
|
-
offset += WAPK_MAGIC.length;
|
|
1152
|
-
buffer.writeUInt16LE(WAPK_UNLOCKED_VERSION, offset);
|
|
1153
|
-
offset += 2;
|
|
1154
|
-
payload.copy(buffer, offset);
|
|
1155
|
-
|
|
1156
|
-
return buffer;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const encrypted = encryptWapkPayload(payload, lock);
|
|
1160
|
-
const metadataBuffer = Buffer.from(JSON.stringify(encrypted.metadata), 'utf8');
|
|
1161
|
-
const buffer = Buffer.allocUnsafe(
|
|
1162
|
-
WAPK_MAGIC.length + 2 + 4 + metadataBuffer.length + 4 + encrypted.payload.length,
|
|
1163
|
-
);
|
|
1164
|
-
let offset = 0;
|
|
1165
|
-
|
|
1166
|
-
WAPK_MAGIC.copy(buffer, offset);
|
|
1167
|
-
offset += WAPK_MAGIC.length;
|
|
1168
|
-
buffer.writeUInt16LE(WAPK_LOCKED_VERSION, offset);
|
|
1169
|
-
offset += 2;
|
|
1170
|
-
buffer.writeUInt32LE(metadataBuffer.length, offset);
|
|
1171
|
-
offset += 4;
|
|
1172
|
-
metadataBuffer.copy(buffer, offset);
|
|
1173
|
-
offset += metadataBuffer.length;
|
|
1174
|
-
buffer.writeUInt32LE(encrypted.payload.length, offset);
|
|
1175
|
-
offset += 4;
|
|
1176
|
-
encrypted.payload.copy(buffer, offset);
|
|
1177
|
-
|
|
1178
|
-
return buffer;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function decodeWapkPayload(buffer: Buffer): Omit<DecodedWapk, 'version' | 'lock'> {
|
|
1182
|
-
let offset = 0;
|
|
1183
|
-
|
|
1184
|
-
ensureBufferRange(buffer, offset, 4, 'header length');
|
|
1185
|
-
const headerLength = buffer.readUInt32LE(offset);
|
|
1186
|
-
offset += 4;
|
|
1187
|
-
|
|
1188
|
-
ensureBufferRange(buffer, offset, headerLength, 'header');
|
|
1189
|
-
const rawHeader = JSON.parse(buffer.slice(offset, offset + headerLength).toString('utf8'));
|
|
1190
|
-
offset += headerLength;
|
|
1191
|
-
if (!isRecord(rawHeader)) {
|
|
1192
|
-
throw new Error('Invalid WAPK file: header must be an object.');
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
ensureBufferRange(buffer, offset, 4, 'file count');
|
|
1196
|
-
const fileCount = buffer.readUInt32LE(offset);
|
|
1197
|
-
offset += 4;
|
|
1198
|
-
|
|
1199
|
-
const header: WapkHeader = {
|
|
1200
|
-
name: typeof rawHeader.name === 'string' ? rawHeader.name : 'app',
|
|
1201
|
-
version: typeof rawHeader.version === 'string' ? rawHeader.version : '1.0.0',
|
|
1202
|
-
runtime: normalizeRuntime(rawHeader.runtime ?? rawHeader.engine) ?? 'node',
|
|
1203
|
-
entry: typeof rawHeader.entry === 'string' ? rawHeader.entry : 'index.js',
|
|
1204
|
-
scripts: normalizeStringMap(rawHeader.scripts) ?? {},
|
|
1205
|
-
port: normalizePort(rawHeader.port),
|
|
1206
|
-
env: normalizeStringMap(rawHeader.env),
|
|
1207
|
-
desktop: normalizeDesktopConfig(rawHeader.desktop),
|
|
1208
|
-
createdAt: typeof rawHeader.createdAt === 'string' ? rawHeader.createdAt : new Date(0).toISOString(),
|
|
1209
|
-
};
|
|
1210
|
-
|
|
1211
|
-
const files: WapkFileEntry[] = [];
|
|
1212
|
-
for (let index = 0; index < fileCount; index++) {
|
|
1213
|
-
ensureBufferRange(buffer, offset, 2, `file ${index + 1} path length`);
|
|
1214
|
-
const pathLength = buffer.readUInt16LE(offset);
|
|
1215
|
-
offset += 2;
|
|
1216
|
-
|
|
1217
|
-
ensureBufferRange(buffer, offset, pathLength, `file ${index + 1} path`);
|
|
1218
|
-
const pathValue = buffer.slice(offset, offset + pathLength).toString('utf8');
|
|
1219
|
-
offset += pathLength;
|
|
1220
|
-
|
|
1221
|
-
ensureBufferRange(buffer, offset, 4, `file ${index + 1} mode`);
|
|
1222
|
-
const mode = buffer.readUInt32LE(offset);
|
|
1223
|
-
offset += 4;
|
|
1224
|
-
|
|
1225
|
-
ensureBufferRange(buffer, offset, 4, `file ${index + 1} size`);
|
|
1226
|
-
const contentLength = buffer.readUInt32LE(offset);
|
|
1227
|
-
offset += 4;
|
|
1228
|
-
|
|
1229
|
-
ensureBufferRange(buffer, offset, contentLength, `file ${index + 1} content`);
|
|
1230
|
-
const content = Buffer.allocUnsafe(contentLength);
|
|
1231
|
-
buffer.copy(content, 0, offset, offset + contentLength);
|
|
1232
|
-
offset += contentLength;
|
|
1233
|
-
|
|
1234
|
-
files.push({ path: pathValue, content, mode });
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
return { header, files };
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function decodeWapk(buffer: Buffer, options: WapkCredentialsOptions = {}): DecodedWapk {
|
|
1241
|
-
const envelope = parseWapkEnvelope(buffer);
|
|
1242
|
-
|
|
1243
|
-
if (envelope.version === WAPK_UNLOCKED_VERSION) {
|
|
1244
|
-
return {
|
|
1245
|
-
version: envelope.version,
|
|
1246
|
-
...decodeWapkPayload(envelope.payload),
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const credentials = resolveArchiveCredentials(options);
|
|
1251
|
-
if (!credentials) {
|
|
1252
|
-
throw new Error('WAPK archive is password-protected. Provide --password to unlock it.');
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
return {
|
|
1256
|
-
version: envelope.version,
|
|
1257
|
-
...decodeWapkPayload(decryptWapkPayload(envelope.payload, envelope.lock, credentials)),
|
|
1258
|
-
lock: { password: true },
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function formatSize(bytes: number): string {
|
|
1263
|
-
if (bytes < 1024) {
|
|
1264
|
-
return `${bytes} B`;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (bytes < 1024 * 1024) {
|
|
1268
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
function filesEqual(left: readonly WapkFileEntry[], right: readonly WapkFileEntry[]): boolean {
|
|
1275
|
-
if (left.length !== right.length) {
|
|
1276
|
-
return false;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
for (let index = 0; index < left.length; index++) {
|
|
1280
|
-
const leftEntry = left[index];
|
|
1281
|
-
const rightEntry = right[index];
|
|
1282
|
-
if (leftEntry.path !== rightEntry.path || leftEntry.mode !== rightEntry.mode) {
|
|
1283
|
-
return false;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
if (!leftEntry.content.equals(rightEntry.content)) {
|
|
1287
|
-
return false;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
return true;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
function extractFiles(files: readonly WapkFileEntry[], destination: string): void {
|
|
1295
|
-
for (const file of files) {
|
|
1296
|
-
const filePath = join(destination, file.path);
|
|
1297
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
1298
|
-
writeFileSync(filePath, file.content);
|
|
1299
|
-
if (process.platform !== 'win32') {
|
|
1300
|
-
try {
|
|
1301
|
-
chmodSync(filePath, file.mode);
|
|
1302
|
-
} catch {
|
|
1303
|
-
continue;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
function collectRuntimeSyncFiles(
|
|
1310
|
-
directory: string,
|
|
1311
|
-
options: { includeNodeModules?: boolean } = {},
|
|
1312
|
-
): WapkFileEntry[] {
|
|
1313
|
-
return filterRuntimeSyncFiles(collectFiles(directory, directory, []), options);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
function isTypescriptRuntimeEntry(filePath: string): boolean {
|
|
1317
|
-
return /\.(?:ts|tsx|cts|mts)$/i.test(filePath);
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function commandExists(command: string): boolean {
|
|
1321
|
-
const result = spawnSync(command, ['--version'], {
|
|
1322
|
-
stdio: 'ignore',
|
|
1323
|
-
windowsHide: true,
|
|
1324
|
-
});
|
|
1325
|
-
|
|
1326
|
-
return !result.error;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
function resolveTsxExecutable(searchDirectories: readonly string[]): string | undefined {
|
|
1330
|
-
const executableName = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
|
|
1331
|
-
|
|
1332
|
-
for (const directory of searchDirectories) {
|
|
1333
|
-
const localPath = join(directory, 'node_modules', '.bin', executableName);
|
|
1334
|
-
if (existsSync(localPath)) {
|
|
1335
|
-
return localPath;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
return commandExists(executableName) ? executableName : undefined;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
function readUtf8FileIfExists(filePath: string): string | undefined {
|
|
1343
|
-
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
1344
|
-
return undefined;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
return readFileSync(filePath, 'utf8');
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
function hasLikelyWebAppAssets(directory: string): boolean {
|
|
1351
|
-
return [
|
|
1352
|
-
'public/index.html',
|
|
1353
|
-
'index.html',
|
|
1354
|
-
'dist/index.html',
|
|
1355
|
-
].some((relativePath) => existsSync(join(directory, ...relativePath.split('/'))));
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
function isLikelyBrowserEntry(entryPath: string): boolean {
|
|
1359
|
-
const source = readUtf8FileIfExists(entryPath);
|
|
1360
|
-
if (!source) {
|
|
1361
|
-
return false;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
return /dom\.render\s*\(|\bwindow\.|\bdocument\.|\bnavigator\.|\blocation\.|from\s+['"]elit\/dom['"]/.test(source);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
function resolveLocalBinExecutable(directory: string, command: string): string {
|
|
1368
|
-
if (command.includes('/') || command.includes('\\')) {
|
|
1369
|
-
return isAbsolute(command) ? command : resolve(directory, command);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
const localBinDirectory = join(directory, 'node_modules', '.bin');
|
|
1373
|
-
const directLocalPath = join(localBinDirectory, command);
|
|
1374
|
-
|
|
1375
|
-
if (process.platform === 'win32') {
|
|
1376
|
-
for (const extension of ['.cmd', '.exe']) {
|
|
1377
|
-
const localPath = join(localBinDirectory, `${command}${extension}`);
|
|
1378
|
-
if (existsSync(localPath)) {
|
|
1379
|
-
return localPath;
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
if (existsSync(directLocalPath)) {
|
|
1384
|
-
return directLocalPath;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
switch (command) {
|
|
1388
|
-
case 'npm':
|
|
1389
|
-
return 'npm.cmd';
|
|
1390
|
-
case 'npx':
|
|
1391
|
-
return 'npx.cmd';
|
|
1392
|
-
case 'pnpm':
|
|
1393
|
-
return 'pnpm.cmd';
|
|
1394
|
-
case 'pnpx':
|
|
1395
|
-
return 'pnpx.cmd';
|
|
1396
|
-
case 'yarn':
|
|
1397
|
-
return 'yarn.cmd';
|
|
1398
|
-
case 'tsx':
|
|
1399
|
-
return 'tsx.cmd';
|
|
1400
|
-
case 'elit':
|
|
1401
|
-
return 'elit.cmd';
|
|
1402
|
-
default:
|
|
1403
|
-
return command;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
if (existsSync(directLocalPath)) {
|
|
1408
|
-
return directLocalPath;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
return command;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
function resolvePackagedNodeBinScript(directory: string, command: string): string | undefined {
|
|
1415
|
-
if (command.includes('/') || command.includes('\\')) {
|
|
1416
|
-
return undefined;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
const packageDirectory = join(directory, 'node_modules', ...command.split('/'));
|
|
1420
|
-
const packageJson = readJsonFile(join(packageDirectory, 'package.json'));
|
|
1421
|
-
if (!packageJson) {
|
|
1422
|
-
return undefined;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
let binEntry: string | undefined;
|
|
1426
|
-
if (typeof packageJson.bin === 'string') {
|
|
1427
|
-
const packageName = normalizeNonEmptyString(packageJson.name);
|
|
1428
|
-
if (packageName === command) {
|
|
1429
|
-
binEntry = packageJson.bin;
|
|
1430
|
-
}
|
|
1431
|
-
} else if (isRecord(packageJson.bin)) {
|
|
1432
|
-
const directBin = packageJson.bin[command];
|
|
1433
|
-
if (typeof directBin === 'string') {
|
|
1434
|
-
binEntry = directBin;
|
|
1435
|
-
} else {
|
|
1436
|
-
const packageName = normalizeNonEmptyString(packageJson.name);
|
|
1437
|
-
if (packageName === command) {
|
|
1438
|
-
const entries = Object.values(packageJson.bin).filter((value): value is string => typeof value === 'string');
|
|
1439
|
-
if (entries.length === 1) {
|
|
1440
|
-
binEntry = entries[0];
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
const normalizedBinEntry = typeof binEntry === 'string'
|
|
1447
|
-
? normalizePackageEntry(binEntry)
|
|
1448
|
-
: undefined;
|
|
1449
|
-
if (!normalizedBinEntry) {
|
|
1450
|
-
return undefined;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
const binPath = resolve(packageDirectory, normalizedBinEntry);
|
|
1454
|
-
return existsSync(binPath) && statSync(binPath).isFile()
|
|
1455
|
-
? binPath
|
|
1456
|
-
: undefined;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
function prependLocalNodeModulesBinToPath(directory: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
1460
|
-
const localBinDirectory = join(directory, 'node_modules', '.bin');
|
|
1461
|
-
if (!existsSync(localBinDirectory)) {
|
|
1462
|
-
return env;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path') ?? 'PATH';
|
|
1466
|
-
const currentPath = env[pathKey];
|
|
1467
|
-
|
|
1468
|
-
return {
|
|
1469
|
-
...env,
|
|
1470
|
-
[pathKey]: currentPath ? `${localBinDirectory}${delimiter}${currentPath}` : localBinDirectory,
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
function hasShellMetacharacters(command: string): boolean {
|
|
1475
|
-
return /&&|\|\||[|;<>]/.test(command);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
export function shouldUseShellExecution(command: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
1479
|
-
return platform === 'win32' && /\.(?:cmd|bat|ps1)$/i.test(command);
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
function resolveWapkStartScript(prepared: PreparedWapkApp): string | undefined {
|
|
1483
|
-
const startScript = normalizeNonEmptyString(prepared.header.scripts.start);
|
|
1484
|
-
if (!startScript || prepared.runtimeWasExplicitlyRequested) {
|
|
1485
|
-
return undefined;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
if (!existsSync(join(prepared.workDir, 'package.json'))) {
|
|
1489
|
-
return undefined;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
if (isLikelyBrowserEntry(prepared.entryPath)) {
|
|
1493
|
-
return startScript;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
return hasLikelyWebAppAssets(prepared.workDir) && /\b(?:preview|serve|dev)\b/i.test(startScript)
|
|
1497
|
-
? startScript
|
|
1498
|
-
: undefined;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
function resolveWapkStartScriptLaunchCommand(
|
|
1502
|
-
prepared: PreparedWapkApp,
|
|
1503
|
-
env: NodeJS.ProcessEnv,
|
|
1504
|
-
startScript: string,
|
|
1505
|
-
): WapkLaunchCommand {
|
|
1506
|
-
const launchEnv = prependLocalNodeModulesBinToPath(prepared.workDir, env);
|
|
1507
|
-
|
|
1508
|
-
if (hasShellMetacharacters(startScript)) {
|
|
1509
|
-
return {
|
|
1510
|
-
executable: startScript,
|
|
1511
|
-
args: [],
|
|
1512
|
-
env: launchEnv,
|
|
1513
|
-
label: `scripts.start (${startScript})`,
|
|
1514
|
-
shell: true,
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
const tokens = tokenizeCommand(startScript);
|
|
1519
|
-
if (tokens.length === 0) {
|
|
1520
|
-
throw new Error('WAPK scripts.start is empty.');
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
const [command, ...args] = tokens;
|
|
1524
|
-
const packagedBinScript = resolvePackagedNodeBinScript(prepared.workDir, command);
|
|
1525
|
-
if (packagedBinScript) {
|
|
1526
|
-
return {
|
|
1527
|
-
executable: resolveWapkRuntimeExecutable('node'),
|
|
1528
|
-
args: [packagedBinScript, ...args],
|
|
1529
|
-
env: launchEnv,
|
|
1530
|
-
label: `scripts.start (${startScript})`,
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
const executable = resolveLocalBinExecutable(prepared.workDir, command);
|
|
1535
|
-
|
|
1536
|
-
return {
|
|
1537
|
-
executable,
|
|
1538
|
-
args,
|
|
1539
|
-
env: launchEnv,
|
|
1540
|
-
label: `scripts.start (${startScript})`,
|
|
1541
|
-
shell: shouldUseShellExecution(executable),
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function resolveWapkEntryLaunchCommand(
|
|
1546
|
-
prepared: PreparedWapkApp,
|
|
1547
|
-
env: NodeJS.ProcessEnv,
|
|
1548
|
-
): WapkLaunchCommand {
|
|
1549
|
-
const isNodeTypescriptEntry = prepared.runtime === 'node' && isTypescriptRuntimeEntry(prepared.entryPath);
|
|
1550
|
-
const tsxExecutable = isNodeTypescriptEntry
|
|
1551
|
-
? resolveTsxExecutable([prepared.workDir, process.cwd()])
|
|
1552
|
-
: undefined;
|
|
1553
|
-
|
|
1554
|
-
if (isNodeTypescriptEntry && !tsxExecutable) {
|
|
1555
|
-
throw new Error('TypeScript WAPK execution with runtime "node" requires tsx to be installed, or use runtime "bun".');
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
const executable = tsxExecutable ?? resolveWapkRuntimeExecutable(prepared.runtime);
|
|
1559
|
-
const args = tsxExecutable
|
|
1560
|
-
? [prepared.entryPath]
|
|
1561
|
-
: getWapkRuntimeArgs(prepared.runtime, prepared.entryPath);
|
|
1562
|
-
|
|
1563
|
-
return {
|
|
1564
|
-
executable,
|
|
1565
|
-
args,
|
|
1566
|
-
env,
|
|
1567
|
-
label: tsxExecutable ? 'entry via tsx' : 'entry',
|
|
1568
|
-
shell: shouldUseShellExecution(executable),
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
function resolveWapkLaunchCommand(
|
|
1573
|
-
prepared: PreparedWapkApp,
|
|
1574
|
-
env: NodeJS.ProcessEnv,
|
|
1575
|
-
): WapkLaunchCommand {
|
|
1576
|
-
const startScript = resolveWapkStartScript(prepared);
|
|
1577
|
-
if (startScript) {
|
|
1578
|
-
return resolveWapkStartScriptLaunchCommand(prepared, env, startScript);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
return resolveWapkEntryLaunchCommand(prepared, env);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
function filterRuntimeSyncFiles(
|
|
1585
|
-
files: readonly WapkFileEntry[],
|
|
1586
|
-
options: { includeNodeModules?: boolean } = {},
|
|
1587
|
-
): WapkFileEntry[] {
|
|
1588
|
-
return [...files]
|
|
1589
|
-
.filter((file) => {
|
|
1590
|
-
const firstPart = file.path.split('/')[0] ?? '';
|
|
1591
|
-
if (RUNTIME_SYNC_IGNORE.has(firstPart)) {
|
|
1592
|
-
return false;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
if (!options.includeNodeModules && firstPart === 'node_modules') {
|
|
1596
|
-
return false;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
return true;
|
|
1600
|
-
})
|
|
1601
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function getLocalArchiveSignature(archivePath: string): string | undefined {
|
|
1605
|
-
try {
|
|
1606
|
-
const stats = statSync(archivePath);
|
|
1607
|
-
return `${stats.size}:${stats.mtimeMs}`;
|
|
1608
|
-
} catch {
|
|
1609
|
-
return undefined;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function createGoogleDriveArchiveSignature(metadata: {
|
|
1614
|
-
modifiedTime?: string;
|
|
1615
|
-
size?: string;
|
|
1616
|
-
md5Checksum?: string;
|
|
1617
|
-
}): string | undefined {
|
|
1618
|
-
const signature = [metadata.modifiedTime, metadata.size, metadata.md5Checksum]
|
|
1619
|
-
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
1620
|
-
.join(':');
|
|
1621
|
-
|
|
1622
|
-
return signature.length > 0 ? signature : undefined;
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
async function readResponseMessage(response: Response): Promise<string> {
|
|
1626
|
-
try {
|
|
1627
|
-
const text = (await response.text()).trim();
|
|
1628
|
-
return text.length > 0 ? text.slice(0, 400) : response.statusText;
|
|
1629
|
-
} catch {
|
|
1630
|
-
return response.statusText;
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
function buildGoogleDriveFileUrl(
|
|
1635
|
-
fileId: string,
|
|
1636
|
-
params: Record<string, string | boolean | undefined>,
|
|
1637
|
-
): string {
|
|
1638
|
-
const url = new URL(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`);
|
|
1639
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1640
|
-
if (value === undefined) {
|
|
1641
|
-
continue;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
url.searchParams.set(key, typeof value === 'boolean' ? String(value) : value);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
return url.toString();
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
function buildGoogleDriveUploadUrl(
|
|
1651
|
-
fileId: string,
|
|
1652
|
-
params: Record<string, string | boolean | undefined>,
|
|
1653
|
-
): string {
|
|
1654
|
-
const url = new URL(`https://www.googleapis.com/upload/drive/v3/files/${encodeURIComponent(fileId)}`);
|
|
1655
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1656
|
-
if (value === undefined) {
|
|
1657
|
-
continue;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
url.searchParams.set(key, typeof value === 'boolean' ? String(value) : value);
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
return url.toString();
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
async function fetchGoogleDriveMetadata(config: ResolvedWapkGoogleDriveConfig): Promise<{
|
|
1667
|
-
name?: string;
|
|
1668
|
-
modifiedTime?: string;
|
|
1669
|
-
size?: string;
|
|
1670
|
-
md5Checksum?: string;
|
|
1671
|
-
}> {
|
|
1672
|
-
const response = await fetch(buildGoogleDriveFileUrl(config.fileId, {
|
|
1673
|
-
fields: 'id,name,modifiedTime,size,md5Checksum',
|
|
1674
|
-
supportsAllDrives: config.supportsAllDrives,
|
|
1675
|
-
}), {
|
|
1676
|
-
headers: {
|
|
1677
|
-
Authorization: `Bearer ${config.accessToken}`,
|
|
1678
|
-
},
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
if (!response.ok) {
|
|
1682
|
-
throw new Error(`Google Drive metadata request failed (${response.status}): ${await readResponseMessage(response)}`);
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
const payload = await response.json();
|
|
1686
|
-
return {
|
|
1687
|
-
name: normalizeNonEmptyString(payload?.name),
|
|
1688
|
-
modifiedTime: normalizeNonEmptyString(payload?.modifiedTime),
|
|
1689
|
-
size: normalizeNonEmptyString(payload?.size),
|
|
1690
|
-
md5Checksum: normalizeNonEmptyString(payload?.md5Checksum),
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
async function downloadGoogleDriveArchive(config: ResolvedWapkGoogleDriveConfig): Promise<WapkArchiveSnapshot> {
|
|
1695
|
-
const metadata = await fetchGoogleDriveMetadata(config);
|
|
1696
|
-
const response = await fetch(buildGoogleDriveFileUrl(config.fileId, {
|
|
1697
|
-
alt: 'media',
|
|
1698
|
-
supportsAllDrives: config.supportsAllDrives,
|
|
1699
|
-
}), {
|
|
1700
|
-
headers: {
|
|
1701
|
-
Authorization: `Bearer ${config.accessToken}`,
|
|
1702
|
-
},
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
if (!response.ok) {
|
|
1706
|
-
throw new Error(`Google Drive download failed (${response.status}): ${await readResponseMessage(response)}`);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
1710
|
-
return {
|
|
1711
|
-
buffer: Buffer.from(arrayBuffer),
|
|
1712
|
-
signature: createGoogleDriveArchiveSignature(metadata),
|
|
1713
|
-
label: metadata.name ?? `Google Drive:${config.fileId}`,
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
async function uploadGoogleDriveArchive(
|
|
1718
|
-
config: ResolvedWapkGoogleDriveConfig,
|
|
1719
|
-
buffer: Buffer,
|
|
1720
|
-
): Promise<WapkArchiveSnapshot> {
|
|
1721
|
-
const response = await fetch(buildGoogleDriveUploadUrl(config.fileId, {
|
|
1722
|
-
uploadType: 'media',
|
|
1723
|
-
fields: 'id,name,modifiedTime,size,md5Checksum',
|
|
1724
|
-
supportsAllDrives: config.supportsAllDrives,
|
|
1725
|
-
}), {
|
|
1726
|
-
method: 'PATCH',
|
|
1727
|
-
headers: {
|
|
1728
|
-
Authorization: `Bearer ${config.accessToken}`,
|
|
1729
|
-
'Content-Type': 'application/octet-stream',
|
|
1730
|
-
},
|
|
1731
|
-
body: new Uint8Array(buffer),
|
|
1732
|
-
});
|
|
1733
|
-
|
|
1734
|
-
if (!response.ok) {
|
|
1735
|
-
throw new Error(`Google Drive upload failed (${response.status}): ${await readResponseMessage(response)}`);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
const payload = await response.json();
|
|
1739
|
-
const metadata = {
|
|
1740
|
-
name: normalizeNonEmptyString(payload?.name),
|
|
1741
|
-
modifiedTime: normalizeNonEmptyString(payload?.modifiedTime),
|
|
1742
|
-
size: normalizeNonEmptyString(payload?.size),
|
|
1743
|
-
md5Checksum: normalizeNonEmptyString(payload?.md5Checksum),
|
|
1744
|
-
};
|
|
1745
|
-
|
|
1746
|
-
return {
|
|
1747
|
-
buffer,
|
|
1748
|
-
signature: createGoogleDriveArchiveSignature(metadata),
|
|
1749
|
-
label: metadata.name ?? `Google Drive:${config.fileId}`,
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
function resolveGoogleDriveAccessToken(config: WapkGoogleDriveConfig | undefined): string {
|
|
1754
|
-
const explicitToken = normalizeNonEmptyString(config?.accessToken);
|
|
1755
|
-
const configuredEnvName = normalizeNonEmptyString(config?.accessTokenEnv);
|
|
1756
|
-
const configuredEnvToken = configuredEnvName
|
|
1757
|
-
? normalizeNonEmptyString(process.env[configuredEnvName])
|
|
1758
|
-
: undefined;
|
|
1759
|
-
const defaultEnvToken = normalizeNonEmptyString(process.env[DEFAULT_GOOGLE_DRIVE_TOKEN_ENV]);
|
|
1760
|
-
|
|
1761
|
-
const token = explicitToken ?? configuredEnvToken ?? defaultEnvToken;
|
|
1762
|
-
if (token) {
|
|
1763
|
-
return token;
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
if (configuredEnvName) {
|
|
1767
|
-
throw new Error(`Google Drive access token not found in environment variable ${configuredEnvName}.`);
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
throw new Error(`Google Drive access token is required. Provide googleDrive.accessToken, googleDrive.accessTokenEnv, or set ${DEFAULT_GOOGLE_DRIVE_TOKEN_ENV}.`);
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
function parseGoogleDriveArchiveSpecifier(value: string): string | undefined {
|
|
1774
|
-
const match = value.match(/^(?:gdrive|google-drive):\/\/(.+)$/i);
|
|
1775
|
-
if (!match) {
|
|
1776
|
-
return undefined;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
const fileId = match[1]?.trim();
|
|
1780
|
-
return fileId && fileId.length > 0 ? fileId : undefined;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
function resolveGoogleDriveConfig(
|
|
1784
|
-
archiveSpecifier: string,
|
|
1785
|
-
googleDrive?: WapkGoogleDriveConfig,
|
|
1786
|
-
): ResolvedWapkGoogleDriveConfig | undefined {
|
|
1787
|
-
const fileId = normalizeNonEmptyString(googleDrive?.fileId) ?? parseGoogleDriveArchiveSpecifier(archiveSpecifier);
|
|
1788
|
-
if (!fileId) {
|
|
1789
|
-
return undefined;
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
return {
|
|
1793
|
-
fileId,
|
|
1794
|
-
accessToken: resolveGoogleDriveAccessToken(googleDrive),
|
|
1795
|
-
accessTokenEnv: normalizeNonEmptyString(googleDrive?.accessTokenEnv),
|
|
1796
|
-
supportsAllDrives: googleDrive?.supportsAllDrives ?? false,
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
function createLocalArchiveHandle(archivePath: string): WapkArchiveHandle {
|
|
1801
|
-
const resolvedArchivePath = resolve(archivePath);
|
|
1802
|
-
|
|
1803
|
-
const readLocalBuffer = (): Buffer => {
|
|
1804
|
-
if (!existsSync(resolvedArchivePath)) {
|
|
1805
|
-
throw new Error(`WAPK file not found: ${resolvedArchivePath}`);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
return readFileSync(resolvedArchivePath);
|
|
1809
|
-
};
|
|
1810
|
-
|
|
1811
|
-
return {
|
|
1812
|
-
identifier: resolvedArchivePath,
|
|
1813
|
-
label: basename(resolvedArchivePath),
|
|
1814
|
-
readSnapshot: async () => ({
|
|
1815
|
-
buffer: readLocalBuffer(),
|
|
1816
|
-
signature: getLocalArchiveSignature(resolvedArchivePath),
|
|
1817
|
-
label: basename(resolvedArchivePath),
|
|
1818
|
-
}),
|
|
1819
|
-
getSignature: async () => getLocalArchiveSignature(resolvedArchivePath),
|
|
1820
|
-
writeBuffer: async (buffer) => {
|
|
1821
|
-
writeFileSync(resolvedArchivePath, buffer);
|
|
1822
|
-
return {
|
|
1823
|
-
buffer,
|
|
1824
|
-
signature: getLocalArchiveSignature(resolvedArchivePath),
|
|
1825
|
-
label: basename(resolvedArchivePath),
|
|
1826
|
-
};
|
|
1827
|
-
},
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function createGoogleDriveArchiveHandle(config: ResolvedWapkGoogleDriveConfig): WapkArchiveHandle {
|
|
1832
|
-
const identifier = `gdrive://${config.fileId}`;
|
|
1833
|
-
const label = `Google Drive:${config.fileId}`;
|
|
1834
|
-
|
|
1835
|
-
return {
|
|
1836
|
-
identifier,
|
|
1837
|
-
label,
|
|
1838
|
-
readSnapshot: async () => downloadGoogleDriveArchive(config),
|
|
1839
|
-
getSignature: async () => createGoogleDriveArchiveSignature(await fetchGoogleDriveMetadata(config)),
|
|
1840
|
-
writeBuffer: async (buffer) => uploadGoogleDriveArchive(config, buffer),
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
function resolveArchiveHandle(archiveSpecifier: string, googleDrive?: WapkGoogleDriveConfig): WapkArchiveHandle {
|
|
1845
|
-
const googleDriveConfig = resolveGoogleDriveConfig(archiveSpecifier, googleDrive);
|
|
1846
|
-
if (googleDriveConfig) {
|
|
1847
|
-
return createGoogleDriveArchiveHandle(googleDriveConfig);
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
return createLocalArchiveHandle(archiveSpecifier);
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
function sanitizeOnlineArchiveFileName(label: string | undefined, fallback: string): string {
|
|
1854
|
-
const preferredName = normalizeNonEmptyString(label)
|
|
1855
|
-
?? normalizeNonEmptyString(basename(fallback))
|
|
1856
|
-
?? 'app.wapk';
|
|
1857
|
-
const sanitized = preferredName
|
|
1858
|
-
.replace(/[\\/:*?"<>|]+/g, '-')
|
|
1859
|
-
.trim();
|
|
1860
|
-
const fileName = sanitized.length > 0 ? sanitized : 'app.wapk';
|
|
1861
|
-
|
|
1862
|
-
return fileName.toLowerCase().endsWith('.wapk') ? fileName : `${fileName}.wapk`;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
function buildOnlineJoinUrl(baseUrl: URL, joinKey: string): string {
|
|
1866
|
-
const joinUrl = new URL(baseUrl.toString());
|
|
1867
|
-
joinUrl.search = '';
|
|
1868
|
-
joinUrl.hash = '';
|
|
1869
|
-
joinUrl.searchParams.set('join', joinKey);
|
|
1870
|
-
return joinUrl.toString();
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
async function probeOnlineLauncherUrl(url: string): Promise<boolean> {
|
|
1874
|
-
const controller = new AbortController();
|
|
1875
|
-
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
1876
|
-
|
|
1877
|
-
try {
|
|
1878
|
-
const response = await fetch(url, {
|
|
1879
|
-
method: 'GET',
|
|
1880
|
-
redirect: 'manual',
|
|
1881
|
-
signal: controller.signal,
|
|
1882
|
-
});
|
|
1883
|
-
return response.ok;
|
|
1884
|
-
} catch {
|
|
1885
|
-
return false;
|
|
1886
|
-
} finally {
|
|
1887
|
-
clearTimeout(timeout);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
function normalizeOnlineLauncherUrl(candidate: string, optionName: string): URL {
|
|
1892
|
-
let url: URL;
|
|
1893
|
-
|
|
1894
|
-
try {
|
|
1895
|
-
url = new URL(candidate);
|
|
1896
|
-
} catch {
|
|
1897
|
-
throw new Error(`${optionName} must be a valid http:// or https:// URL.`);
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
1901
|
-
throw new Error(`${optionName} must use http:// or https://.`);
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
return url;
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
async function resolveWapkOnlineLauncherUrl(explicitUrl?: string): Promise<URL> {
|
|
1908
|
-
const configuredUrl = normalizeNonEmptyString(explicitUrl)
|
|
1909
|
-
?? normalizeNonEmptyString(process.env[DEFAULT_WAPK_ONLINE_URL_ENV]);
|
|
1910
|
-
|
|
1911
|
-
if (configuredUrl) {
|
|
1912
|
-
const normalized = normalizeOnlineLauncherUrl(configuredUrl, explicitUrl ? '--online-url' : DEFAULT_WAPK_ONLINE_URL_ENV);
|
|
1913
|
-
if (!(await probeOnlineLauncherUrl(normalized.toString()))) {
|
|
1914
|
-
throw new Error(
|
|
1915
|
-
`Could not reach Elit Run at ${normalized.toString()}. Start an Elit Run server with npm run dev or npm run preview, or provide a reachable --online-url.`,
|
|
1916
|
-
);
|
|
1917
|
-
}
|
|
1918
|
-
return normalized;
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
for (const candidate of DEFAULT_WAPK_ONLINE_URLS) {
|
|
1922
|
-
if (await probeOnlineLauncherUrl(candidate)) {
|
|
1923
|
-
return new URL(candidate);
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
throw new Error(
|
|
1928
|
-
'Could not reach Elit Run on http://localhost:4177 or http://localhost:4179. Start an Elit Run server with npm run dev or npm run preview, or pass --online-url <url>.',
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
function encodeOnlineSharedSessionFileContent(content: Buffer): string {
|
|
1933
|
-
return content.toString('base64');
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
function createWapkOnlineSharedSessionSnapshot(
|
|
1937
|
-
archiveBuffer: Buffer,
|
|
1938
|
-
archiveLabel: string | undefined,
|
|
1939
|
-
archiveIdentifier: string,
|
|
1940
|
-
options: WapkCredentialsOptions,
|
|
1941
|
-
): WapkOnlineSharedSessionSnapshot {
|
|
1942
|
-
const decoded = decodeWapk(archiveBuffer, options);
|
|
1943
|
-
const originalName = sanitizeOnlineArchiveFileName(archiveLabel, archiveIdentifier);
|
|
1944
|
-
|
|
1945
|
-
return {
|
|
1946
|
-
originalName,
|
|
1947
|
-
version: decoded.version,
|
|
1948
|
-
locked: Boolean(decoded.lock),
|
|
1949
|
-
header: decoded.header,
|
|
1950
|
-
files: decoded.files.map((file) => ({
|
|
1951
|
-
path: file.path,
|
|
1952
|
-
mode: file.mode,
|
|
1953
|
-
content: encodeOnlineSharedSessionFileContent(file.content),
|
|
1954
|
-
})),
|
|
1955
|
-
currentPath: '/',
|
|
1956
|
-
hostLabel: decoded.header.name,
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
function decodeOnlineSharedSessionFileContent(content: string): Buffer {
|
|
1961
|
-
return Buffer.from(content, 'base64');
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
function createWapkFilesFromOnlineSharedSessionSnapshot(snapshot: WapkOnlineSharedSessionSnapshot): WapkFileEntry[] {
|
|
1965
|
-
return snapshot.files.map((file) => ({
|
|
1966
|
-
path: file.path,
|
|
1967
|
-
mode: file.mode,
|
|
1968
|
-
content: decodeOnlineSharedSessionFileContent(file.content),
|
|
1969
|
-
}));
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
async function createWapkOnlineSharedSession(
|
|
1973
|
-
launcherUrl: URL,
|
|
1974
|
-
snapshot: WapkOnlineSharedSessionSnapshot,
|
|
1975
|
-
): Promise<{ ok: true; joinKey: string; adminToken: string }> {
|
|
1976
|
-
const response = await fetch(new URL(WAPK_ONLINE_CREATE_PATH, launcherUrl), {
|
|
1977
|
-
method: 'POST',
|
|
1978
|
-
headers: {
|
|
1979
|
-
'content-type': 'application/json',
|
|
1980
|
-
},
|
|
1981
|
-
body: JSON.stringify({ snapshot } satisfies WapkOnlineCreateRequest),
|
|
1982
|
-
});
|
|
1983
|
-
|
|
1984
|
-
let payload: Partial<WapkOnlineCreateResponse> | null = null;
|
|
1985
|
-
try {
|
|
1986
|
-
payload = await response.json() as Partial<WapkOnlineCreateResponse>;
|
|
1987
|
-
} catch {
|
|
1988
|
-
payload = null;
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
const joinKey = normalizeNonEmptyString(payload?.joinKey);
|
|
1992
|
-
const adminToken = normalizeNonEmptyString(payload?.adminToken);
|
|
1993
|
-
|
|
1994
|
-
if (!response.ok || !payload?.ok || !joinKey || !adminToken) {
|
|
1995
|
-
throw new Error(payload?.error ?? `Could not create the online shared session (${response.status}).`);
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
return { ok: true, joinKey, adminToken };
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
async function readWapkOnlineSharedSessionSnapshot(
|
|
2002
|
-
launcherUrl: URL,
|
|
2003
|
-
session: { joinKey: string; adminToken: string },
|
|
2004
|
-
knownRevision: number,
|
|
2005
|
-
): Promise<{ revision: number; changed: boolean; snapshot: WapkOnlineSharedSessionSnapshot | null }> {
|
|
2006
|
-
const response = await fetch(new URL(WAPK_ONLINE_READ_PATH, launcherUrl), {
|
|
2007
|
-
method: 'POST',
|
|
2008
|
-
headers: {
|
|
2009
|
-
'content-type': 'application/json',
|
|
2010
|
-
},
|
|
2011
|
-
body: JSON.stringify({
|
|
2012
|
-
joinKey: session.joinKey,
|
|
2013
|
-
hostToken: session.adminToken,
|
|
2014
|
-
knownRevision,
|
|
2015
|
-
} satisfies WapkOnlineReadRequest),
|
|
2016
|
-
});
|
|
2017
|
-
|
|
2018
|
-
let payload: Partial<WapkOnlineReadResponse> | null = null;
|
|
2019
|
-
try {
|
|
2020
|
-
payload = await response.json() as Partial<WapkOnlineReadResponse>;
|
|
2021
|
-
} catch {
|
|
2022
|
-
payload = null;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
const revision = typeof payload?.revision === 'number' && Number.isInteger(payload.revision) && payload.revision >= 0
|
|
2026
|
-
? payload.revision
|
|
2027
|
-
: 0;
|
|
2028
|
-
const changed = payload?.changed === true;
|
|
2029
|
-
|
|
2030
|
-
if (!response.ok || !payload?.ok || typeof payload.changed !== 'boolean' || typeof payload.revision !== 'number') {
|
|
2031
|
-
throw new Error(payload?.error ?? `Could not read the online shared session snapshot (${response.status}).`);
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
if (!changed) {
|
|
2035
|
-
return {
|
|
2036
|
-
revision,
|
|
2037
|
-
changed: false,
|
|
2038
|
-
snapshot: null,
|
|
2039
|
-
};
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
if (!payload.snapshot) {
|
|
2043
|
-
throw new Error('The online shared session response did not include an updated snapshot.');
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
return {
|
|
2047
|
-
revision,
|
|
2048
|
-
changed: true,
|
|
2049
|
-
snapshot: payload.snapshot,
|
|
2050
|
-
};
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
async function applyWapkOnlineSharedSessionSnapshotToArchive(
|
|
2054
|
-
archiveHandle: WapkArchiveHandle,
|
|
2055
|
-
snapshot: WapkOnlineSharedSessionSnapshot,
|
|
2056
|
-
lock?: ResolvedWapkCredentials,
|
|
2057
|
-
): Promise<{ header: WapkHeader; signature?: string; label: string }> {
|
|
2058
|
-
return await writeWapkArchiveFromMemory(
|
|
2059
|
-
archiveHandle,
|
|
2060
|
-
snapshot.header,
|
|
2061
|
-
createWapkFilesFromOnlineSharedSessionSnapshot(snapshot),
|
|
2062
|
-
lock,
|
|
2063
|
-
);
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
async function closeWapkOnlineSharedSession(
|
|
2067
|
-
launcherUrl: URL,
|
|
2068
|
-
session: { joinKey: string; adminToken: string },
|
|
2069
|
-
): Promise<void> {
|
|
2070
|
-
const controller = new AbortController();
|
|
2071
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
2072
|
-
|
|
2073
|
-
try {
|
|
2074
|
-
const response = await fetch(new URL(WAPK_ONLINE_CLOSE_PATH, launcherUrl), {
|
|
2075
|
-
method: 'POST',
|
|
2076
|
-
headers: {
|
|
2077
|
-
'content-type': 'application/json',
|
|
2078
|
-
},
|
|
2079
|
-
body: JSON.stringify({
|
|
2080
|
-
joinKey: session.joinKey,
|
|
2081
|
-
adminToken: session.adminToken,
|
|
2082
|
-
reason: WAPK_ONLINE_CLOSE_REASON,
|
|
2083
|
-
} satisfies WapkOnlineCloseRequest),
|
|
2084
|
-
signal: controller.signal,
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
let payload: Partial<WapkOnlineCloseResponse> | null = null;
|
|
2088
|
-
try {
|
|
2089
|
-
payload = await response.json() as Partial<WapkOnlineCloseResponse>;
|
|
2090
|
-
} catch {
|
|
2091
|
-
payload = null;
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
if (!response.ok || !payload?.ok) {
|
|
2095
|
-
throw new Error(payload?.error ?? `Could not close the online shared session (${response.status}).`);
|
|
2096
|
-
}
|
|
2097
|
-
} finally {
|
|
2098
|
-
clearTimeout(timeout);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
function isPmWapkOnlineShutdownEnabled(): boolean {
|
|
2103
|
-
return process.env[WAPK_ONLINE_PM_SHUTDOWN_ENV] === '1' && Boolean(process.stdin) && !process.stdin.isTTY;
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
async function waitForWapkOnlineSessionShutdown(
|
|
2107
|
-
launcherUrl: URL,
|
|
2108
|
-
session: { joinKey: string; adminToken: string },
|
|
2109
|
-
archiveHandle: WapkArchiveHandle,
|
|
2110
|
-
lock?: ResolvedWapkCredentials,
|
|
2111
|
-
): Promise<number> {
|
|
2112
|
-
let snapshotRevision = 0;
|
|
2113
|
-
let snapshotSyncPending = false;
|
|
2114
|
-
let snapshotSyncPromise: Promise<void> = Promise.resolve();
|
|
2115
|
-
let lastSnapshotSyncError: string | null = null;
|
|
2116
|
-
|
|
2117
|
-
const syncGuestSnapshotUpdates = (): Promise<void> => {
|
|
2118
|
-
if (snapshotSyncPending) {
|
|
2119
|
-
return snapshotSyncPromise;
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
snapshotSyncPending = true;
|
|
2123
|
-
snapshotSyncPromise = (async () => {
|
|
2124
|
-
try {
|
|
2125
|
-
const result = await readWapkOnlineSharedSessionSnapshot(launcherUrl, session, snapshotRevision);
|
|
2126
|
-
if (!result.changed || !result.snapshot) {
|
|
2127
|
-
snapshotRevision = result.revision;
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
const writeResult = await applyWapkOnlineSharedSessionSnapshotToArchive(archiveHandle, result.snapshot, lock);
|
|
2132
|
-
snapshotRevision = result.revision;
|
|
2133
|
-
lastSnapshotSyncError = null;
|
|
2134
|
-
console.log(`[wapk] Applied guest changes back to ${writeResult.label}.`);
|
|
2135
|
-
} catch (error) {
|
|
2136
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2137
|
-
if (lastSnapshotSyncError !== message) {
|
|
2138
|
-
lastSnapshotSyncError = message;
|
|
2139
|
-
console.error(`[wapk] Could not sync guest changes back to ${archiveHandle.label}: ${message}`);
|
|
2140
|
-
}
|
|
2141
|
-
} finally {
|
|
2142
|
-
snapshotSyncPending = false;
|
|
2143
|
-
}
|
|
2144
|
-
})();
|
|
2145
|
-
|
|
2146
|
-
return snapshotSyncPromise;
|
|
2147
|
-
};
|
|
2148
|
-
|
|
2149
|
-
const shutdownTrigger = await new Promise<WapkOnlineShutdownTrigger>((resolve) => {
|
|
2150
|
-
const keepAlive = setInterval(() => {
|
|
2151
|
-
void syncGuestSnapshotUpdates();
|
|
2152
|
-
}, WAPK_ONLINE_KEEPALIVE_INTERVAL_MS);
|
|
2153
|
-
const pmManaged = isPmWapkOnlineShutdownEnabled();
|
|
2154
|
-
let stdinBuffer = '';
|
|
2155
|
-
|
|
2156
|
-
const cleanup = (): void => {
|
|
2157
|
-
clearInterval(keepAlive);
|
|
2158
|
-
process.off('SIGINT', onSigInt);
|
|
2159
|
-
process.off('SIGTERM', onSigTerm);
|
|
2160
|
-
if (pmManaged) {
|
|
2161
|
-
process.stdin.off('data', onStdinData);
|
|
2162
|
-
process.stdin.pause();
|
|
2163
|
-
}
|
|
2164
|
-
};
|
|
2165
|
-
|
|
2166
|
-
const finish = (trigger: WapkOnlineShutdownTrigger): void => {
|
|
2167
|
-
cleanup();
|
|
2168
|
-
resolve(trigger);
|
|
2169
|
-
};
|
|
2170
|
-
|
|
2171
|
-
const onSigInt = (): void => {
|
|
2172
|
-
finish({ kind: 'signal', signal: 'SIGINT' });
|
|
2173
|
-
};
|
|
2174
|
-
|
|
2175
|
-
const onSigTerm = (): void => {
|
|
2176
|
-
finish({ kind: 'signal', signal: 'SIGTERM' });
|
|
2177
|
-
};
|
|
2178
|
-
|
|
2179
|
-
const onStdinData = (chunk: Buffer | string): void => {
|
|
2180
|
-
stdinBuffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
2181
|
-
|
|
2182
|
-
const lines = stdinBuffer.split(/\r?\n/);
|
|
2183
|
-
stdinBuffer = lines.pop() ?? '';
|
|
2184
|
-
|
|
2185
|
-
for (const line of lines) {
|
|
2186
|
-
if (line.trim() === WAPK_ONLINE_PM_SHUTDOWN_COMMAND) {
|
|
2187
|
-
finish({ kind: 'pm' });
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
};
|
|
2192
|
-
|
|
2193
|
-
process.on('SIGINT', onSigInt);
|
|
2194
|
-
process.on('SIGTERM', onSigTerm);
|
|
2195
|
-
|
|
2196
|
-
if (pmManaged) {
|
|
2197
|
-
process.stdin.setEncoding('utf8');
|
|
2198
|
-
process.stdin.on('data', onStdinData);
|
|
2199
|
-
process.stdin.resume();
|
|
2200
|
-
}
|
|
2201
|
-
});
|
|
2202
|
-
|
|
2203
|
-
await syncGuestSnapshotUpdates();
|
|
2204
|
-
|
|
2205
|
-
if (shutdownTrigger.kind === 'pm') {
|
|
2206
|
-
console.log(`\n[wapk] PM requested shutdown for shared session ${session.joinKey}...`);
|
|
2207
|
-
} else {
|
|
2208
|
-
console.log(`\n[wapk] Closing shared session ${session.joinKey}...`);
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
try {
|
|
2212
|
-
await closeWapkOnlineSharedSession(launcherUrl, session);
|
|
2213
|
-
console.log('[wapk] Shared session closed.');
|
|
2214
|
-
} catch (error) {
|
|
2215
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2216
|
-
console.error(`[wapk] Could not close the shared session cleanly: ${message}`);
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
if (shutdownTrigger.kind === 'pm') {
|
|
2220
|
-
return 0;
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
return shutdownTrigger.signal === 'SIGINT' ? 130 : 143;
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
async function runWapkOnline(
|
|
2227
|
-
archiveSpecifier: string,
|
|
2228
|
-
options: {
|
|
2229
|
-
googleDrive?: WapkGoogleDriveConfig;
|
|
2230
|
-
onlineUrl?: string;
|
|
2231
|
-
password?: string;
|
|
2232
|
-
},
|
|
2233
|
-
): Promise<void> {
|
|
2234
|
-
const archiveHandle = resolveArchiveHandle(archiveSpecifier, options.googleDrive);
|
|
2235
|
-
const snapshot = await archiveHandle.readSnapshot();
|
|
2236
|
-
const fileName = sanitizeOnlineArchiveFileName(snapshot.label ?? archiveHandle.label, archiveHandle.identifier);
|
|
2237
|
-
const launcherUrl = await resolveWapkOnlineLauncherUrl(options.onlineUrl);
|
|
2238
|
-
const sharedSessionSnapshot = createWapkOnlineSharedSessionSnapshot(
|
|
2239
|
-
snapshot.buffer,
|
|
2240
|
-
snapshot.label ?? archiveHandle.label,
|
|
2241
|
-
archiveHandle.identifier,
|
|
2242
|
-
options.password ? { password: options.password } : {},
|
|
2243
|
-
);
|
|
2244
|
-
|
|
2245
|
-
console.log(`[wapk] Online handoff: ${fileName}`);
|
|
2246
|
-
console.log(`[wapk] Elit Run: ${launcherUrl.toString()}`);
|
|
2247
|
-
console.log('[wapk] Creating shared session...');
|
|
2248
|
-
|
|
2249
|
-
const response = await createWapkOnlineSharedSession(launcherUrl, sharedSessionSnapshot);
|
|
2250
|
-
const joinUrl = buildOnlineJoinUrl(launcherUrl, response.joinKey);
|
|
2251
|
-
const pmManaged = isPmWapkOnlineShutdownEnabled();
|
|
2252
|
-
const onlineArchiveLock = sharedSessionSnapshot.locked && options.password
|
|
2253
|
-
? resolveArchiveCredentials({ password: options.password })
|
|
2254
|
-
: undefined;
|
|
2255
|
-
|
|
2256
|
-
console.log(`[wapk] Share key: ${response.joinKey}`);
|
|
2257
|
-
console.log(`[wapk] Join URL: ${joinUrl}`);
|
|
2258
|
-
console.log(
|
|
2259
|
-
pmManaged
|
|
2260
|
-
? '[wapk] Session active. Use elit pm stop, restart, or delete to close the shared session.'
|
|
2261
|
-
: '[wapk] Session active. Press Ctrl+C to stop sharing and close the session.',
|
|
2262
|
-
);
|
|
2263
|
-
|
|
2264
|
-
process.exitCode = await waitForWapkOnlineSessionShutdown(launcherUrl, {
|
|
2265
|
-
joinKey: response.joinKey,
|
|
2266
|
-
adminToken: response.adminToken,
|
|
2267
|
-
}, archiveHandle, onlineArchiveLock);
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
async function writeWapkArchiveFromMemory(
|
|
2271
|
-
archiveHandle: WapkArchiveHandle,
|
|
2272
|
-
header: WapkHeader,
|
|
2273
|
-
files: readonly WapkFileEntry[],
|
|
2274
|
-
lock?: ResolvedWapkCredentials,
|
|
2275
|
-
): Promise<{ header: WapkHeader; signature?: string; label: string }> {
|
|
2276
|
-
const updatedHeader: WapkHeader = {
|
|
2277
|
-
...header,
|
|
2278
|
-
createdAt: new Date().toISOString(),
|
|
2279
|
-
};
|
|
2280
|
-
const snapshot = await archiveHandle.writeBuffer(encodeWapk(updatedHeader, files, lock));
|
|
2281
|
-
return {
|
|
2282
|
-
header: updatedHeader,
|
|
2283
|
-
signature: snapshot.signature,
|
|
2284
|
-
label: snapshot.label ?? archiveHandle.label,
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
function removeEmptyParentDirectories(directory: string, rootDir: string): void {
|
|
2289
|
-
let currentDir = directory;
|
|
2290
|
-
|
|
2291
|
-
while (true) {
|
|
2292
|
-
const relativeDir = relative(rootDir, currentDir);
|
|
2293
|
-
if (!relativeDir || relativeDir.startsWith('..') || isAbsolute(relativeDir)) {
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
try {
|
|
2298
|
-
if (readdirSync(currentDir).length > 0) {
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
rmSync(currentDir, { recursive: false, force: true });
|
|
2303
|
-
} catch {
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
currentDir = dirname(currentDir);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
function applyArchiveFilesToWorkDir(
|
|
2312
|
-
directory: string,
|
|
2313
|
-
files: readonly WapkFileEntry[],
|
|
2314
|
-
options: { includeNodeModules?: boolean } = {},
|
|
2315
|
-
): void {
|
|
2316
|
-
const existingFiles = collectRuntimeSyncFiles(directory, options);
|
|
2317
|
-
const nextPaths = new Set(files.map((file) => file.path));
|
|
2318
|
-
|
|
2319
|
-
for (const file of existingFiles) {
|
|
2320
|
-
if (nextPaths.has(file.path)) {
|
|
2321
|
-
continue;
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
const filePath = join(directory, ...file.path.split('/'));
|
|
2325
|
-
rmSync(filePath, { force: true });
|
|
2326
|
-
removeEmptyParentDirectories(dirname(filePath), directory);
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
extractFiles(files, directory);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
async function readArchiveRuntimeState(
|
|
2333
|
-
archiveHandle: WapkArchiveHandle,
|
|
2334
|
-
lock?: ResolvedWapkCredentials,
|
|
2335
|
-
options: { includeNodeModules?: boolean } = {},
|
|
2336
|
-
): Promise<{ header: WapkHeader; files: WapkFileEntry[]; signature?: string; label: string }> {
|
|
2337
|
-
const snapshot = await archiveHandle.readSnapshot();
|
|
2338
|
-
const decoded = decodeWapk(snapshot.buffer, lock ? { password: lock.password } : {});
|
|
2339
|
-
|
|
2340
|
-
return {
|
|
2341
|
-
header: decoded.header,
|
|
2342
|
-
files: filterRuntimeSyncFiles(decoded.files, options),
|
|
2343
|
-
signature: snapshot.signature,
|
|
2344
|
-
label: snapshot.label ?? archiveHandle.label,
|
|
2345
|
-
};
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
export function createWapkLiveSync(prepared: PreparedWapkApp): WapkLiveSyncController {
|
|
2349
|
-
const syncOptions = { includeNodeModules: prepared.syncIncludesNodeModules };
|
|
2350
|
-
let memoryFiles = collectRuntimeSyncFiles(prepared.workDir, syncOptions);
|
|
2351
|
-
const syncInterval = prepared.syncInterval ?? 300;
|
|
2352
|
-
const archiveSyncInterval = prepared.archiveSyncInterval ?? syncInterval;
|
|
2353
|
-
const watchArchive = prepared.watchArchive ?? true;
|
|
2354
|
-
let currentHeader = prepared.header;
|
|
2355
|
-
let currentArchiveLabel = prepared.archiveLabel;
|
|
2356
|
-
let stopped = false;
|
|
2357
|
-
let lastArchiveSignature = prepared.archiveSignature;
|
|
2358
|
-
let lastArchivePollAt = 0;
|
|
2359
|
-
let pendingOperation = Promise.resolve();
|
|
2360
|
-
|
|
2361
|
-
const reportSyncError = (error: unknown): void => {
|
|
2362
|
-
console.warn(
|
|
2363
|
-
`[wapk] Sync error for ${currentArchiveLabel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2364
|
-
);
|
|
2365
|
-
};
|
|
2366
|
-
|
|
2367
|
-
const pullArchiveChanges = async (): Promise<boolean> => {
|
|
2368
|
-
if (!watchArchive) {
|
|
2369
|
-
return false;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
const archiveSignature = await prepared.archiveHandle.getSignature();
|
|
2373
|
-
if (!archiveSignature || archiveSignature === lastArchiveSignature) {
|
|
2374
|
-
return false;
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
try {
|
|
2378
|
-
const archiveState = await readArchiveRuntimeState(prepared.archiveHandle, prepared.lock, syncOptions);
|
|
2379
|
-
lastArchiveSignature = archiveState.signature ?? archiveSignature;
|
|
2380
|
-
currentHeader = archiveState.header;
|
|
2381
|
-
currentArchiveLabel = archiveState.label;
|
|
2382
|
-
|
|
2383
|
-
if (filesEqual(memoryFiles, archiveState.files)) {
|
|
2384
|
-
return false;
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
applyArchiveFilesToWorkDir(prepared.workDir, archiveState.files, syncOptions);
|
|
2388
|
-
memoryFiles = archiveState.files;
|
|
2389
|
-
return true;
|
|
2390
|
-
} catch (error) {
|
|
2391
|
-
console.warn(
|
|
2392
|
-
`[wapk] Failed to pull external archive changes from ${currentArchiveLabel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2393
|
-
);
|
|
2394
|
-
return false;
|
|
2395
|
-
}
|
|
2396
|
-
};
|
|
2397
|
-
|
|
2398
|
-
const flush = (): Promise<void> => {
|
|
2399
|
-
pendingOperation = pendingOperation
|
|
2400
|
-
.catch(() => undefined)
|
|
2401
|
-
.then(async () => {
|
|
2402
|
-
if (stopped) return;
|
|
2403
|
-
|
|
2404
|
-
const nextFiles = collectRuntimeSyncFiles(prepared.workDir, syncOptions);
|
|
2405
|
-
const localDirty = !filesEqual(memoryFiles, nextFiles);
|
|
2406
|
-
const now = Date.now();
|
|
2407
|
-
const shouldPollArchive = watchArchive && (lastArchivePollAt === 0 || now - lastArchivePollAt >= archiveSyncInterval);
|
|
2408
|
-
|
|
2409
|
-
if (!localDirty && shouldPollArchive) {
|
|
2410
|
-
lastArchivePollAt = now;
|
|
2411
|
-
await pullArchiveChanges();
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
if (!localDirty) {
|
|
2416
|
-
return;
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
if (shouldPollArchive) {
|
|
2420
|
-
lastArchivePollAt = now;
|
|
2421
|
-
const archiveSignature = await prepared.archiveHandle.getSignature();
|
|
2422
|
-
if (archiveSignature && archiveSignature !== lastArchiveSignature) {
|
|
2423
|
-
console.warn(
|
|
2424
|
-
`[wapk] Both the workdir and ${currentArchiveLabel} changed; writing local workdir changes back to the archive.`,
|
|
2425
|
-
);
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
memoryFiles = nextFiles;
|
|
2430
|
-
const writeResult = await writeWapkArchiveFromMemory(prepared.archiveHandle, currentHeader, memoryFiles, prepared.lock);
|
|
2431
|
-
currentHeader = writeResult.header;
|
|
2432
|
-
currentArchiveLabel = writeResult.label;
|
|
2433
|
-
lastArchiveSignature = writeResult.signature ?? await prepared.archiveHandle.getSignature();
|
|
2434
|
-
});
|
|
2435
|
-
|
|
2436
|
-
return pendingOperation;
|
|
2437
|
-
};
|
|
2438
|
-
|
|
2439
|
-
const scheduleFlush = (): void => {
|
|
2440
|
-
void flush().catch(reportSyncError);
|
|
2441
|
-
};
|
|
2442
|
-
|
|
2443
|
-
if (prepared.useWatcher) {
|
|
2444
|
-
// Event-driven file watcher mode
|
|
2445
|
-
const watcher = watch(prepared.workDir, { recursive: true }, () => {
|
|
2446
|
-
scheduleFlush();
|
|
2447
|
-
});
|
|
2448
|
-
|
|
2449
|
-
const archiveTimer = watchArchive
|
|
2450
|
-
? setInterval(() => {
|
|
2451
|
-
scheduleFlush();
|
|
2452
|
-
}, archiveSyncInterval)
|
|
2453
|
-
: undefined;
|
|
2454
|
-
archiveTimer?.unref?.();
|
|
2455
|
-
|
|
2456
|
-
const stop = async (): Promise<void> => {
|
|
2457
|
-
if (stopped) return pendingOperation;
|
|
2458
|
-
|
|
2459
|
-
watcher.close();
|
|
2460
|
-
if (archiveTimer) {
|
|
2461
|
-
clearInterval(archiveTimer);
|
|
2462
|
-
}
|
|
2463
|
-
await flush();
|
|
2464
|
-
stopped = true;
|
|
2465
|
-
await pendingOperation;
|
|
2466
|
-
};
|
|
2467
|
-
|
|
2468
|
-
return { flush, stop };
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
// Polling mode (default)
|
|
2472
|
-
const timer = setInterval(scheduleFlush, watchArchive ? Math.min(syncInterval, archiveSyncInterval) : syncInterval);
|
|
2473
|
-
timer.unref?.();
|
|
2474
|
-
|
|
2475
|
-
const stop = async (): Promise<void> => {
|
|
2476
|
-
if (stopped) return;
|
|
2477
|
-
|
|
2478
|
-
clearInterval(timer);
|
|
2479
|
-
await flush();
|
|
2480
|
-
stopped = true;
|
|
2481
|
-
await pendingOperation;
|
|
2482
|
-
};
|
|
2483
|
-
|
|
2484
|
-
return { flush, stop };
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
function resolveRuntimeExecutable(runtime: WapkRuntimeName): string {
|
|
2488
|
-
const executableName = basename(process.execPath).toLowerCase();
|
|
2489
|
-
|
|
2490
|
-
if (runtime === 'node' && process.release?.name === 'node' && executableName.startsWith('node')) {
|
|
2491
|
-
return process.execPath;
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
if (runtime === 'bun' && process.versions?.bun && executableName.startsWith('bun')) {
|
|
2495
|
-
return process.execPath;
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
return runtime;
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
function ensureRuntimeAvailable(runtime: WapkRuntimeName, executable: string): void {
|
|
2502
|
-
const result = spawnSync(executable, ['--version'], {
|
|
2503
|
-
stdio: 'ignore',
|
|
2504
|
-
windowsHide: true,
|
|
2505
|
-
});
|
|
2506
|
-
|
|
2507
|
-
if (result.error) {
|
|
2508
|
-
if ((result.error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
2509
|
-
throw new Error(`WAPK runtime "${runtime}" was not found in PATH.`);
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
throw result.error;
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
function hasPackageJson(directory: string): boolean {
|
|
2517
|
-
const packageJsonPath = join(directory, 'package.json');
|
|
2518
|
-
return existsSync(packageJsonPath) && statSync(packageJsonPath).isFile();
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
|
-
function findNearestPackageDirectory(startDirectory: string, rootDirectory: string): string | undefined {
|
|
2522
|
-
let currentDirectory = resolve(startDirectory);
|
|
2523
|
-
const resolvedRootDirectory = resolve(rootDirectory);
|
|
2524
|
-
|
|
2525
|
-
while (true) {
|
|
2526
|
-
if (hasPackageJson(currentDirectory)) {
|
|
2527
|
-
return currentDirectory;
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
if (currentDirectory === resolvedRootDirectory) {
|
|
2531
|
-
return undefined;
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
const parentDirectory = dirname(currentDirectory);
|
|
2535
|
-
if (parentDirectory === currentDirectory) {
|
|
2536
|
-
return undefined;
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
currentDirectory = parentDirectory;
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
export function getWapkRuntimeArgs(runtime: WapkRuntimeName, entryPath: string): string[] {
|
|
2544
|
-
switch (runtime) {
|
|
2545
|
-
case 'bun':
|
|
2546
|
-
return ['run', entryPath];
|
|
2547
|
-
case 'deno':
|
|
2548
|
-
return ['run', '--allow-all', entryPath];
|
|
2549
|
-
default:
|
|
2550
|
-
return [entryPath];
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
export function resolveWapkRuntimeExecutable(runtime: WapkRuntimeName): string {
|
|
2555
|
-
const executable = resolveRuntimeExecutable(runtime);
|
|
2556
|
-
ensureRuntimeAvailable(runtime, executable);
|
|
2557
|
-
return executable;
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
export function readWapkArchive(wapkPath: string, options: WapkCredentialsOptions = {}): DecodedWapk {
|
|
2561
|
-
const archivePath = resolve(wapkPath);
|
|
2562
|
-
if (!existsSync(archivePath)) {
|
|
2563
|
-
throw new Error(`WAPK file not found: ${archivePath}`);
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
return decodeWapk(readFileSync(archivePath), options);
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
export async function packWapkDirectory(
|
|
2570
|
-
directory: string,
|
|
2571
|
-
options: WapkCredentialsOptions & { includeDeps?: boolean; outputPath?: string } = {},
|
|
2572
|
-
): Promise<string> {
|
|
2573
|
-
const sourceDirectory = resolve(directory);
|
|
2574
|
-
if (!existsSync(sourceDirectory) || !statSync(sourceDirectory).isDirectory()) {
|
|
2575
|
-
throw new Error(`WAPK source directory not found: ${sourceDirectory}`);
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
const config = await readWapkProjectConfig(sourceDirectory);
|
|
2579
|
-
const lock = resolvePackLockCredentials(config.lock, options);
|
|
2580
|
-
const outputPath = resolve(options.outputPath ?? join(process.cwd(), `${sanitizePackageName(config.name)}.wapk`));
|
|
2581
|
-
const relativeOutputPath = relative(sourceDirectory, outputPath).split('\\').join('/');
|
|
2582
|
-
const outputPathIsInsideSource = relativeOutputPath.length > 0
|
|
2583
|
-
&& !relativeOutputPath.startsWith('..')
|
|
2584
|
-
&& !isAbsolute(relativeOutputPath);
|
|
2585
|
-
const userIgnore = readIgnorePatterns(sourceDirectory);
|
|
2586
|
-
const ignorePatterns = [...DEFAULT_IGNORE, ...userIgnore];
|
|
2587
|
-
const collectedFiles = collectFiles(sourceDirectory, sourceDirectory, ignorePatterns);
|
|
2588
|
-
const nestedEntryPackageFiles = collectNestedEntryPackageFiles(sourceDirectory, config.entry);
|
|
2589
|
-
const fileMap = new Map<string, WapkFileEntry>();
|
|
2590
|
-
for (const file of [...collectedFiles, ...nestedEntryPackageFiles]) {
|
|
2591
|
-
if (outputPathIsInsideSource && file.path === relativeOutputPath) {
|
|
2592
|
-
continue;
|
|
2593
|
-
}
|
|
2594
|
-
fileMap.set(file.path, file);
|
|
2595
|
-
}
|
|
2596
|
-
const files = [...fileMap.values()];
|
|
2597
|
-
const header: WapkHeader = {
|
|
2598
|
-
name: config.name,
|
|
2599
|
-
version: config.version,
|
|
2600
|
-
runtime: config.runtime,
|
|
2601
|
-
entry: config.entry,
|
|
2602
|
-
scripts: config.scripts,
|
|
2603
|
-
port: config.port,
|
|
2604
|
-
env: config.env,
|
|
2605
|
-
desktop: config.desktop,
|
|
2606
|
-
createdAt: new Date().toISOString(),
|
|
2607
|
-
};
|
|
2608
|
-
|
|
2609
|
-
console.log(`Packing: ${config.name}@${config.version}`);
|
|
2610
|
-
console.log(`Runtime: ${config.runtime}`);
|
|
2611
|
-
console.log(`Entry: ${config.entry}`);
|
|
2612
|
-
if (lock) {
|
|
2613
|
-
console.log('Lock: enabled');
|
|
2614
|
-
}
|
|
2615
|
-
if (files.some((file) => file.path.split('/').includes('node_modules'))) {
|
|
2616
|
-
console.log('Deps: included');
|
|
2617
|
-
}
|
|
2618
|
-
if (options.includeDeps) {
|
|
2619
|
-
console.log('Note: --include-deps is no longer required; node_modules are packed by default');
|
|
2620
|
-
}
|
|
2621
|
-
console.log(`Files: ${files.length}`);
|
|
2622
|
-
|
|
2623
|
-
writeFileSync(outputPath, encodeWapk(header, files, lock));
|
|
2624
|
-
console.log(`Output: ${outputPath}`);
|
|
2625
|
-
return outputPath;
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
export function extractWapkArchive(
|
|
2629
|
-
wapkPath: string,
|
|
2630
|
-
outputDir = '.',
|
|
2631
|
-
options: WapkCredentialsOptions = {},
|
|
2632
|
-
): string {
|
|
2633
|
-
const archive = readWapkArchive(wapkPath, options);
|
|
2634
|
-
const destinationRoot = resolve(outputDir);
|
|
2635
|
-
const extractDirectory = join(destinationRoot, sanitizePackageName(archive.header.name));
|
|
2636
|
-
|
|
2637
|
-
mkdirSync(extractDirectory, { recursive: true });
|
|
2638
|
-
extractFiles(archive.files, extractDirectory);
|
|
2639
|
-
console.log(`Extracted ${archive.files.length} files to: ${extractDirectory}`);
|
|
2640
|
-
return extractDirectory;
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
export async function prepareWapkApp(
|
|
2644
|
-
wapkPath: string,
|
|
2645
|
-
options: WapkCredentialsOptions & {
|
|
2646
|
-
runtime?: WapkRuntimeName;
|
|
2647
|
-
runtimeWasExplicitlyRequested?: boolean;
|
|
2648
|
-
dependencySearchRoots?: string[];
|
|
2649
|
-
syncInterval?: number;
|
|
2650
|
-
useWatcher?: boolean;
|
|
2651
|
-
watchArchive?: boolean;
|
|
2652
|
-
archiveSyncInterval?: number;
|
|
2653
|
-
googleDrive?: WapkGoogleDriveConfig;
|
|
2654
|
-
} = {},
|
|
2655
|
-
): Promise<PreparedWapkApp> {
|
|
2656
|
-
const archiveHandle = resolveArchiveHandle(wapkPath, options.googleDrive);
|
|
2657
|
-
const archivePath = archiveHandle.identifier;
|
|
2658
|
-
const snapshot = await archiveHandle.readSnapshot();
|
|
2659
|
-
const buffer = snapshot.buffer;
|
|
2660
|
-
const envelope = parseWapkEnvelope(buffer);
|
|
2661
|
-
const lock = envelope.version === WAPK_LOCKED_VERSION
|
|
2662
|
-
? resolveArchiveCredentials(options)
|
|
2663
|
-
: undefined;
|
|
2664
|
-
const decoded = decodeWapk(buffer, options);
|
|
2665
|
-
const runtime = options.runtime ?? decoded.header.runtime;
|
|
2666
|
-
const workDir = mkdtempSync(join(tmpdir(), 'elit-wapk-'));
|
|
2667
|
-
extractFiles(decoded.files, workDir);
|
|
2668
|
-
const entryPath = resolve(workDir, decoded.header.entry);
|
|
2669
|
-
const entryPackageDirectory = findNearestPackageDirectory(dirname(entryPath), workDir);
|
|
2670
|
-
const syncIncludesNodeModules = existsSync(join(workDir, 'node_modules')) || Boolean(
|
|
2671
|
-
entryPackageDirectory && existsSync(join(entryPackageDirectory, 'node_modules')),
|
|
2672
|
-
);
|
|
2673
|
-
|
|
2674
|
-
if (!existsSync(entryPath) || !statSync(entryPath).isFile()) {
|
|
2675
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
2676
|
-
throw new Error(`WAPK entry not found after extraction: ${entryPath}`);
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
|
-
return {
|
|
2680
|
-
archivePath,
|
|
2681
|
-
archiveLabel: snapshot.label ?? archiveHandle.label,
|
|
2682
|
-
archiveHandle,
|
|
2683
|
-
archiveSignature: snapshot.signature,
|
|
2684
|
-
workDir,
|
|
2685
|
-
entryPath,
|
|
2686
|
-
header: decoded.header,
|
|
2687
|
-
runtime,
|
|
2688
|
-
syncInterval: options.syncInterval,
|
|
2689
|
-
useWatcher: options.useWatcher,
|
|
2690
|
-
watchArchive: options.watchArchive,
|
|
2691
|
-
archiveSyncInterval: options.archiveSyncInterval,
|
|
2692
|
-
lock,
|
|
2693
|
-
runtimeWasExplicitlyRequested: options.runtimeWasExplicitlyRequested,
|
|
2694
|
-
syncIncludesNodeModules,
|
|
2695
|
-
};
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
export async function runPreparedWapkApp(prepared: PreparedWapkApp): Promise<number> {
|
|
2699
|
-
const port = prepared.header.port ?? DEFAULT_WAPK_PORT;
|
|
2700
|
-
const env = {
|
|
2701
|
-
...process.env,
|
|
2702
|
-
...prepared.header.env,
|
|
2703
|
-
PORT: String(port),
|
|
2704
|
-
};
|
|
2705
|
-
|
|
2706
|
-
console.log(`[wapk] ${prepared.header.name}@${prepared.header.version}`);
|
|
2707
|
-
console.log(`[wapk] Runtime: ${prepared.runtime}`);
|
|
2708
|
-
console.log(`[wapk] Entry: ${prepared.header.entry}`);
|
|
2709
|
-
console.log(`[wapk] Workdir: ${prepared.workDir}`);
|
|
2710
|
-
|
|
2711
|
-
const sync = createWapkLiveSync(prepared);
|
|
2712
|
-
|
|
2713
|
-
let launch: WapkLaunchCommand;
|
|
2714
|
-
try {
|
|
2715
|
-
launch = resolveWapkLaunchCommand(prepared, env);
|
|
2716
|
-
} catch (error) {
|
|
2717
|
-
await sync.stop();
|
|
2718
|
-
rmSync(prepared.workDir, { recursive: true, force: true });
|
|
2719
|
-
throw error;
|
|
2720
|
-
}
|
|
2721
|
-
|
|
2722
|
-
if (launch.label !== 'entry') {
|
|
2723
|
-
console.log(`[wapk] Launch: ${launch.label}`);
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
const child = spawn(launch.executable, launch.args, {
|
|
2727
|
-
cwd: prepared.workDir,
|
|
2728
|
-
env: launch.env,
|
|
2729
|
-
stdio: 'inherit',
|
|
2730
|
-
shell: launch.shell,
|
|
2731
|
-
windowsHide: true,
|
|
2732
|
-
});
|
|
2733
|
-
|
|
2734
|
-
const onSigInt = (): void => {
|
|
2735
|
-
child.kill('SIGINT');
|
|
2736
|
-
};
|
|
2737
|
-
const onSigTerm = (): void => {
|
|
2738
|
-
child.kill('SIGTERM');
|
|
2739
|
-
};
|
|
2740
|
-
|
|
2741
|
-
process.on('SIGINT', onSigInt);
|
|
2742
|
-
process.on('SIGTERM', onSigTerm);
|
|
2743
|
-
|
|
2744
|
-
try {
|
|
2745
|
-
return await new Promise<number>((resolvePromise, rejectPromise) => {
|
|
2746
|
-
child.once('error', rejectPromise);
|
|
2747
|
-
child.once('close', (code) => resolvePromise(code ?? 1));
|
|
2748
|
-
});
|
|
2749
|
-
} finally {
|
|
2750
|
-
process.off('SIGINT', onSigInt);
|
|
2751
|
-
process.off('SIGTERM', onSigTerm);
|
|
2752
|
-
await sync.stop();
|
|
2753
|
-
rmSync(prepared.workDir, { recursive: true, force: true });
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
function inspectWapkArchive(wapkPath: string, options: WapkCredentialsOptions = {}): void {
|
|
2758
|
-
const archivePath = resolve(wapkPath);
|
|
2759
|
-
const buffer = readFileSync(archivePath);
|
|
2760
|
-
const envelope = parseWapkEnvelope(buffer);
|
|
2761
|
-
|
|
2762
|
-
console.log(`WAPK: ${basename(archivePath)}`);
|
|
2763
|
-
console.log(`Size: ${formatSize(buffer.length)}`);
|
|
2764
|
-
console.log(`Version: ${envelope.version}`);
|
|
2765
|
-
console.log(`Locked: ${envelope.version === WAPK_LOCKED_VERSION ? 'yes' : 'no'}`);
|
|
2766
|
-
|
|
2767
|
-
if (envelope.version === WAPK_LOCKED_VERSION) {
|
|
2768
|
-
if (!hasCredentialInput(options)) {
|
|
2769
|
-
console.log('Status: credentials required to inspect contents');
|
|
2770
|
-
return;
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
const decoded = decodeWapk(buffer, options);
|
|
2775
|
-
const totalContentSize = decoded.files.reduce((total, file) => total + file.content.length, 0);
|
|
2776
|
-
|
|
2777
|
-
console.log(`Name: ${decoded.header.name}`);
|
|
2778
|
-
console.log(`App: ${decoded.header.version}`);
|
|
2779
|
-
console.log(`Runtime: ${decoded.header.runtime}`);
|
|
2780
|
-
console.log(`Entry: ${decoded.header.entry}`);
|
|
2781
|
-
console.log(`Port: ${decoded.header.port ?? 'default'}`);
|
|
2782
|
-
console.log(`Created: ${decoded.header.createdAt}`);
|
|
2783
|
-
|
|
2784
|
-
if (decoded.header.env && Object.keys(decoded.header.env).length > 0) {
|
|
2785
|
-
console.log('Env:');
|
|
2786
|
-
for (const [key, value] of Object.entries(decoded.header.env)) {
|
|
2787
|
-
console.log(` ${key}=${value}`);
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
console.log(`Files: ${decoded.files.length}`);
|
|
2792
|
-
for (const file of [...decoded.files].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
2793
|
-
console.log(` ${formatSize(file.content.length).padStart(10)} ${file.path}`);
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
console.log(`Content: ${formatSize(totalContentSize)}`);
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
function printWapkHelp(): void {
|
|
2800
|
-
console.log([
|
|
2801
|
-
'',
|
|
2802
|
-
'WAPK packaging for Elit',
|
|
2803
|
-
'',
|
|
2804
|
-
'Usage:',
|
|
2805
|
-
' elit wapk [file.wapk]',
|
|
2806
|
-
' elit wapk gdrive://<fileId>',
|
|
2807
|
-
' elit wapk run [file.wapk]',
|
|
2808
|
-
' elit wapk run --google-drive-file-id <fileId> --google-drive-token-env <env>',
|
|
2809
|
-
' elit wapk run [file.wapk] --runtime node|bun|deno',
|
|
2810
|
-
' elit wapk run [file.wapk] --sync-interval 100',
|
|
2811
|
-
' elit wapk run [file.wapk] --watcher',
|
|
2812
|
-
' elit wapk run [file.wapk] --online',
|
|
2813
|
-
' elit wapk gdrive://<fileId> --online',
|
|
2814
|
-
' elit wapk pack [directory]',
|
|
2815
|
-
' elit wapk pack [directory] --password secret-123',
|
|
2816
|
-
' elit wapk inspect <file.wapk>',
|
|
2817
|
-
' elit wapk extract <file.wapk>',
|
|
2818
|
-
'',
|
|
2819
|
-
'Options:',
|
|
2820
|
-
' -r, --runtime <name> Runtime override: node, bun, deno',
|
|
2821
|
-
' --sync-interval <ms> Polling interval for live sync (ms, default 300)',
|
|
2822
|
-
' --archive-sync-interval <ms> Polling interval for reading archive source changes',
|
|
2823
|
-
' --watcher, --use-watcher Use event-driven file watcher instead of polling',
|
|
2824
|
-
' --archive-watch Pull external archive changes back into the temp workdir',
|
|
2825
|
-
' --no-archive-watch Disable external archive read sync',
|
|
2826
|
-
' --online Create an Elit Run share session, stay alive, and close on Ctrl+C',
|
|
2827
|
-
' --online-url <url> Elit Run URL (default: auto-detect localhost:4177 or localhost:4179)',
|
|
2828
|
-
' --google-drive-file-id <id> Run a remote .wapk directly from Google Drive',
|
|
2829
|
-
' --google-drive-token-env <name> Env var containing the Google Drive OAuth token',
|
|
2830
|
-
' --google-drive-access-token <value> OAuth token for Google Drive API calls',
|
|
2831
|
-
' --google-drive-shared-drive Include supportsAllDrives=true for shared drives',
|
|
2832
|
-
' --include-deps Legacy compatibility flag; node_modules are packed by default',
|
|
2833
|
-
' --password <value> Password for locking or unlocking the archive',
|
|
2834
|
-
' -h, --help Show this help',
|
|
2835
|
-
'',
|
|
2836
|
-
'Notes:',
|
|
2837
|
-
' - Pack reads wapk from elit.config.* and falls back to package.json.',
|
|
2838
|
-
' - Pack includes node_modules by default; use .wapkignore if you need to exclude them.',
|
|
2839
|
-
' - Run never installs dependencies automatically; archives must include the runtime dependencies they need.',
|
|
2840
|
-
' - Run mode can read config.wapk.run for default file/runtime/live-sync options.',
|
|
2841
|
-
' - Browser-style archives with scripts.start or wapk.script.start run that start script automatically.',
|
|
2842
|
-
' - Run mode keeps files in RAM and syncs changes both to and from the archive source.',
|
|
2843
|
-
' - Google Drive mode talks to the Drive API directly; no local archive file is required.',
|
|
2844
|
-
' - Online mode creates a shared session on Elit Run directly, keeps the CLI alive, and closes it on Ctrl+C.',
|
|
2845
|
-
' - Locked archives in online mode must provide --password so the CLI can build the shared snapshot.',
|
|
2846
|
-
' - Locked archives require the same password for run/extract/inspect.',
|
|
2847
|
-
' - Archives stay unlocked by default unless a password is provided.',
|
|
2848
|
-
' - Use --watcher for faster file change detection (less CPU usage).',
|
|
2849
|
-
' - Runtime commands use node, bun, or deno from PATH.',
|
|
2850
|
-
].join('\n'));
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
function readRequiredOptionValue(args: string[], index: number, option: string): string {
|
|
2854
|
-
const value = args[index];
|
|
2855
|
-
if (value === undefined) {
|
|
2856
|
-
throw new Error(`${option} requires a value.`);
|
|
2857
|
-
}
|
|
2858
|
-
return value;
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
function parseArchiveAccessArgs(args: string[], usage: string): { file: string } & WapkCredentialsOptions {
|
|
2862
|
-
let file: string | undefined;
|
|
2863
|
-
let password: string | undefined;
|
|
2864
|
-
|
|
2865
|
-
for (let index = 0; index < args.length; index++) {
|
|
2866
|
-
const arg = args[index];
|
|
2867
|
-
|
|
2868
|
-
switch (arg) {
|
|
2869
|
-
case '--password':
|
|
2870
|
-
password = readRequiredOptionValue(args, ++index, '--password');
|
|
2871
|
-
break;
|
|
2872
|
-
default:
|
|
2873
|
-
if (arg.startsWith('-')) {
|
|
2874
|
-
throw new Error(`Unknown WAPK option: ${arg}`);
|
|
2875
|
-
}
|
|
2876
|
-
if (file) {
|
|
2877
|
-
throw new Error(usage);
|
|
2878
|
-
}
|
|
2879
|
-
file = arg;
|
|
2880
|
-
break;
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
if (!file) {
|
|
2885
|
-
throw new Error(usage);
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
return { file, password };
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
function parseRunArgs(args: string[]): {
|
|
2892
|
-
file?: string;
|
|
2893
|
-
googleDrive?: WapkGoogleDriveConfig;
|
|
2894
|
-
runtime?: WapkRuntimeName;
|
|
2895
|
-
syncInterval?: number;
|
|
2896
|
-
useWatcher?: boolean;
|
|
2897
|
-
watchArchive?: boolean;
|
|
2898
|
-
archiveSyncInterval?: number;
|
|
2899
|
-
online?: boolean;
|
|
2900
|
-
onlineUrl?: string;
|
|
2901
|
-
} & WapkCredentialsOptions {
|
|
2902
|
-
let file: string | undefined;
|
|
2903
|
-
let googleDrive: WapkGoogleDriveConfig | undefined;
|
|
2904
|
-
let runtime: WapkRuntimeName | undefined;
|
|
2905
|
-
let syncInterval: number | undefined;
|
|
2906
|
-
let useWatcher: boolean | undefined;
|
|
2907
|
-
let watchArchive: boolean | undefined;
|
|
2908
|
-
let archiveSyncInterval: number | undefined;
|
|
2909
|
-
let online: boolean | undefined;
|
|
2910
|
-
let onlineUrl: string | undefined;
|
|
2911
|
-
let password: string | undefined;
|
|
2912
|
-
|
|
2913
|
-
for (let index = 0; index < args.length; index++) {
|
|
2914
|
-
const arg = args[index];
|
|
2915
|
-
switch (arg) {
|
|
2916
|
-
case '--runtime':
|
|
2917
|
-
case '-r': {
|
|
2918
|
-
const value = normalizeRuntime(readRequiredOptionValue(args, ++index, arg));
|
|
2919
|
-
if (!value) {
|
|
2920
|
-
throw new Error(`Unknown WAPK runtime: ${args[index]}`);
|
|
2921
|
-
}
|
|
2922
|
-
runtime = value;
|
|
2923
|
-
break;
|
|
2924
|
-
}
|
|
2925
|
-
case '--sync-interval': {
|
|
2926
|
-
const value = parseInt(readRequiredOptionValue(args, ++index, '--sync-interval'), 10);
|
|
2927
|
-
if (Number.isNaN(value) || value < 50) {
|
|
2928
|
-
throw new Error('--sync-interval must be a number >= 50 (milliseconds)');
|
|
2929
|
-
}
|
|
2930
|
-
syncInterval = value;
|
|
2931
|
-
break;
|
|
2932
|
-
}
|
|
2933
|
-
case '--archive-sync-interval': {
|
|
2934
|
-
const value = parseInt(readRequiredOptionValue(args, ++index, '--archive-sync-interval'), 10);
|
|
2935
|
-
if (Number.isNaN(value) || value < 50) {
|
|
2936
|
-
throw new Error('--archive-sync-interval must be a number >= 50 (milliseconds)');
|
|
2937
|
-
}
|
|
2938
|
-
archiveSyncInterval = value;
|
|
2939
|
-
break;
|
|
2940
|
-
}
|
|
2941
|
-
case '--use-watcher':
|
|
2942
|
-
case '--watcher': {
|
|
2943
|
-
useWatcher = true;
|
|
2944
|
-
break;
|
|
2945
|
-
}
|
|
2946
|
-
case '--archive-watch': {
|
|
2947
|
-
watchArchive = true;
|
|
2948
|
-
break;
|
|
2949
|
-
}
|
|
2950
|
-
case '--no-archive-watch': {
|
|
2951
|
-
watchArchive = false;
|
|
2952
|
-
break;
|
|
2953
|
-
}
|
|
2954
|
-
case '--online': {
|
|
2955
|
-
online = true;
|
|
2956
|
-
break;
|
|
2957
|
-
}
|
|
2958
|
-
case '--online-url': {
|
|
2959
|
-
online = true;
|
|
2960
|
-
onlineUrl = readRequiredOptionValue(args, ++index, '--online-url');
|
|
2961
|
-
break;
|
|
2962
|
-
}
|
|
2963
|
-
case '--google-drive-file-id': {
|
|
2964
|
-
googleDrive = {
|
|
2965
|
-
...googleDrive,
|
|
2966
|
-
fileId: readRequiredOptionValue(args, ++index, '--google-drive-file-id'),
|
|
2967
|
-
};
|
|
2968
|
-
break;
|
|
2969
|
-
}
|
|
2970
|
-
case '--google-drive-token-env': {
|
|
2971
|
-
googleDrive = {
|
|
2972
|
-
...googleDrive,
|
|
2973
|
-
accessTokenEnv: readRequiredOptionValue(args, ++index, '--google-drive-token-env'),
|
|
2974
|
-
};
|
|
2975
|
-
break;
|
|
2976
|
-
}
|
|
2977
|
-
case '--google-drive-access-token': {
|
|
2978
|
-
googleDrive = {
|
|
2979
|
-
...googleDrive,
|
|
2980
|
-
accessToken: readRequiredOptionValue(args, ++index, '--google-drive-access-token'),
|
|
2981
|
-
};
|
|
2982
|
-
break;
|
|
2983
|
-
}
|
|
2984
|
-
case '--google-drive-shared-drive': {
|
|
2985
|
-
googleDrive = {
|
|
2986
|
-
...googleDrive,
|
|
2987
|
-
supportsAllDrives: true,
|
|
2988
|
-
};
|
|
2989
|
-
break;
|
|
2990
|
-
}
|
|
2991
|
-
case '--password':
|
|
2992
|
-
password = readRequiredOptionValue(args, ++index, '--password');
|
|
2993
|
-
break;
|
|
2994
|
-
default:
|
|
2995
|
-
if (arg.startsWith('-')) {
|
|
2996
|
-
throw new Error(`Unknown WAPK option: ${arg}`);
|
|
2997
|
-
}
|
|
2998
|
-
if (file) {
|
|
2999
|
-
throw new Error('WAPK run accepts exactly one package file.');
|
|
3000
|
-
}
|
|
3001
|
-
file = arg;
|
|
3002
|
-
break;
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
return { file, googleDrive, runtime, syncInterval, useWatcher, watchArchive, archiveSyncInterval, online, onlineUrl, password };
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
function parsePackArgs(args: string[]): { directory: string; includeDeps: boolean } & WapkCredentialsOptions {
|
|
3010
|
-
let directory = '.';
|
|
3011
|
-
let includeDeps = false;
|
|
3012
|
-
let password: string | undefined;
|
|
3013
|
-
|
|
3014
|
-
for (let index = 0; index < args.length; index++) {
|
|
3015
|
-
const arg = args[index];
|
|
3016
|
-
|
|
3017
|
-
if (arg === '--include-deps') {
|
|
3018
|
-
includeDeps = true;
|
|
3019
|
-
continue;
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
if (arg === '--password') {
|
|
3023
|
-
password = readRequiredOptionValue(args, ++index, '--password');
|
|
3024
|
-
continue;
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
if (arg.startsWith('-')) {
|
|
3028
|
-
throw new Error(`Unknown WAPK option: ${arg}`);
|
|
3029
|
-
}
|
|
3030
|
-
|
|
3031
|
-
if (directory !== '.') {
|
|
3032
|
-
throw new Error('WAPK pack accepts at most one directory argument.');
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
directory = arg;
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
return { directory, includeDeps, password };
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
async function readConfiguredWapkRunDefaults(cwd: string): Promise<WapkRunConfig | undefined> {
|
|
3042
|
-
const config = await loadConfig(cwd);
|
|
3043
|
-
const runConfig = normalizeWapkRunConfig(config?.wapk?.run);
|
|
3044
|
-
const fallbackPassword = normalizeNonEmptyString(config?.wapk?.lock?.password);
|
|
3045
|
-
|
|
3046
|
-
if (!runConfig) {
|
|
3047
|
-
return fallbackPassword ? { password: fallbackPassword } : undefined;
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
if (!runConfig.password && fallbackPassword) {
|
|
3051
|
-
runConfig.password = fallbackPassword;
|
|
3052
|
-
}
|
|
3053
|
-
|
|
3054
|
-
if (runConfig.file && runConfig.googleDrive?.fileId) {
|
|
3055
|
-
throw new Error('config.wapk.run.file and config.wapk.run.googleDrive.fileId are mutually exclusive.');
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
return runConfig;
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
function mergeGoogleDriveRunConfig(
|
|
3062
|
-
cliConfig: WapkGoogleDriveConfig | undefined,
|
|
3063
|
-
defaultConfig: WapkGoogleDriveConfig | undefined,
|
|
3064
|
-
): WapkGoogleDriveConfig | undefined {
|
|
3065
|
-
if (!cliConfig && !defaultConfig) {
|
|
3066
|
-
return undefined;
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
const merged: WapkGoogleDriveConfig = {
|
|
3070
|
-
fileId: normalizeNonEmptyString(cliConfig?.fileId) ?? normalizeNonEmptyString(defaultConfig?.fileId),
|
|
3071
|
-
accessToken: normalizeNonEmptyString(cliConfig?.accessToken) ?? normalizeNonEmptyString(defaultConfig?.accessToken),
|
|
3072
|
-
accessTokenEnv: normalizeNonEmptyString(cliConfig?.accessTokenEnv) ?? normalizeNonEmptyString(defaultConfig?.accessTokenEnv),
|
|
3073
|
-
supportsAllDrives: cliConfig?.supportsAllDrives ?? defaultConfig?.supportsAllDrives,
|
|
3074
|
-
};
|
|
3075
|
-
|
|
3076
|
-
return Object.values(merged).some((entry) => entry !== undefined)
|
|
3077
|
-
? merged
|
|
3078
|
-
: undefined;
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
function resolveConfiguredWapkRunOptions(
|
|
3082
|
-
options: ReturnType<typeof parseRunArgs>,
|
|
3083
|
-
defaults: WapkRunConfig | undefined,
|
|
3084
|
-
): {
|
|
3085
|
-
file?: string;
|
|
3086
|
-
googleDrive?: WapkGoogleDriveConfig;
|
|
3087
|
-
runtime?: WapkRuntimeName;
|
|
3088
|
-
syncInterval?: number;
|
|
3089
|
-
useWatcher?: boolean;
|
|
3090
|
-
watchArchive?: boolean;
|
|
3091
|
-
archiveSyncInterval?: number;
|
|
3092
|
-
online: boolean;
|
|
3093
|
-
onlineUrl?: string;
|
|
3094
|
-
password?: string;
|
|
3095
|
-
} {
|
|
3096
|
-
const onlineUrl = options.onlineUrl ?? defaults?.onlineUrl;
|
|
3097
|
-
|
|
3098
|
-
return {
|
|
3099
|
-
file: options.file ?? defaults?.file,
|
|
3100
|
-
googleDrive: mergeGoogleDriveRunConfig(options.googleDrive, defaults?.googleDrive),
|
|
3101
|
-
runtime: options.runtime ?? defaults?.runtime,
|
|
3102
|
-
syncInterval: options.syncInterval ?? defaults?.syncInterval,
|
|
3103
|
-
useWatcher: options.useWatcher ?? defaults?.useWatcher,
|
|
3104
|
-
watchArchive: options.watchArchive ?? defaults?.watchArchive,
|
|
3105
|
-
archiveSyncInterval: options.archiveSyncInterval ?? defaults?.archiveSyncInterval,
|
|
3106
|
-
online: options.online ?? defaults?.online ?? Boolean(onlineUrl),
|
|
3107
|
-
onlineUrl,
|
|
3108
|
-
password: options.password ?? defaults?.password,
|
|
3109
|
-
};
|
|
3110
|
-
}
|
|
3111
|
-
|
|
3112
|
-
function resolveRunArchivePath(file: string, cwd: string): string {
|
|
3113
|
-
return isAbsolute(file) ? file : resolve(cwd, file);
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
function resolveRunArchiveSpecifier(
|
|
3117
|
-
file: string | undefined,
|
|
3118
|
-
googleDrive: WapkGoogleDriveConfig | undefined,
|
|
3119
|
-
cwd: string,
|
|
3120
|
-
): string | undefined {
|
|
3121
|
-
const googleDriveFileId = normalizeNonEmptyString(googleDrive?.fileId);
|
|
3122
|
-
if (googleDriveFileId) {
|
|
3123
|
-
if (file && !parseGoogleDriveArchiveSpecifier(file)) {
|
|
3124
|
-
throw new Error('WAPK run cannot use both a local archive file and googleDrive.fileId at the same time.');
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
return `gdrive://${googleDriveFileId}`;
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
if (!file) {
|
|
3131
|
-
return undefined;
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
return parseGoogleDriveArchiveSpecifier(file) ? file : resolveRunArchivePath(file, cwd);
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
export async function runWapkCommand(args: string[], cwd: string = process.cwd()): Promise<void> {
|
|
3138
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
3139
|
-
printWapkHelp();
|
|
3140
|
-
return;
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
if (args[0] === 'pack') {
|
|
3144
|
-
const options = parsePackArgs(args.slice(1));
|
|
3145
|
-
await packWapkDirectory(options.directory, {
|
|
3146
|
-
includeDeps: options.includeDeps,
|
|
3147
|
-
password: options.password,
|
|
3148
|
-
});
|
|
3149
|
-
return;
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
if (args[0] === 'inspect') {
|
|
3153
|
-
const options = parseArchiveAccessArgs(args.slice(1), 'Usage: elit wapk inspect <file.wapk>');
|
|
3154
|
-
inspectWapkArchive(options.file, options);
|
|
3155
|
-
return;
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
if (args[0] === 'extract') {
|
|
3159
|
-
const options = parseArchiveAccessArgs(args.slice(1), 'Usage: elit wapk extract <file.wapk>');
|
|
3160
|
-
extractWapkArchive(options.file, '.', options);
|
|
3161
|
-
return;
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
|
-
const parsedRunOptions = args[0] === 'run' ? parseRunArgs(args.slice(1)) : parseRunArgs(args);
|
|
3165
|
-
const configuredRunDefaults = await readConfiguredWapkRunDefaults(cwd);
|
|
3166
|
-
const runOptions = resolveConfiguredWapkRunOptions(parsedRunOptions, configuredRunDefaults);
|
|
3167
|
-
|
|
3168
|
-
const archiveSpecifier = resolveRunArchiveSpecifier(runOptions.file, runOptions.googleDrive, cwd);
|
|
3169
|
-
|
|
3170
|
-
if (!archiveSpecifier) {
|
|
3171
|
-
if (args.length === 0) {
|
|
3172
|
-
printWapkHelp();
|
|
3173
|
-
return;
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
|
-
throw new Error('Usage: elit wapk run <file.wapk>');
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
if (runOptions.online) {
|
|
3180
|
-
if (
|
|
3181
|
-
parsedRunOptions.runtime !== undefined
|
|
3182
|
-
|| parsedRunOptions.syncInterval !== undefined
|
|
3183
|
-
|| parsedRunOptions.useWatcher !== undefined
|
|
3184
|
-
|| parsedRunOptions.watchArchive !== undefined
|
|
3185
|
-
|| parsedRunOptions.archiveSyncInterval !== undefined
|
|
3186
|
-
) {
|
|
3187
|
-
console.warn('[wapk] --runtime, --sync-interval, --watcher, --archive-watch, and --archive-sync-interval are ignored with --online.');
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
await runWapkOnline(archiveSpecifier, {
|
|
3191
|
-
googleDrive: runOptions.googleDrive,
|
|
3192
|
-
onlineUrl: runOptions.onlineUrl,
|
|
3193
|
-
password: runOptions.password,
|
|
3194
|
-
});
|
|
3195
|
-
return;
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
const prepared = await prepareWapkApp(archiveSpecifier, {
|
|
3199
|
-
googleDrive: runOptions.googleDrive,
|
|
3200
|
-
runtime: runOptions.runtime,
|
|
3201
|
-
runtimeWasExplicitlyRequested: parsedRunOptions.runtime !== undefined,
|
|
3202
|
-
dependencySearchRoots: [cwd],
|
|
3203
|
-
syncInterval: runOptions.syncInterval,
|
|
3204
|
-
useWatcher: runOptions.useWatcher,
|
|
3205
|
-
watchArchive: runOptions.watchArchive,
|
|
3206
|
-
archiveSyncInterval: runOptions.archiveSyncInterval,
|
|
3207
|
-
password: runOptions.password,
|
|
3208
|
-
});
|
|
3209
|
-
const exitCode = await runPreparedWapkApp(prepared);
|
|
3210
|
-
if (exitCode !== 0) {
|
|
3211
|
-
process.exit(exitCode);
|
|
3212
|
-
}
|
|
3213
|
-
}
|