engineering-memory 1.11.14 → 1.11.16

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.
@@ -0,0 +1,901 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { constants } from 'node:fs';
3
+ import { chmod, link, lstat, mkdtemp, open, readFile, realpath, rm, rmdir } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, relative, resolve, sep, posix, win32, parse } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { assertManagedPath, ensureManagedDirectory } from '../utilities/files.js';
7
+ import { WorktreeFileIssue, WorktreeFileStatus, } from './worktree-readiness-types.js';
8
+ import { parseGradleSigning } from './worktree-gradle.js';
9
+ const execute = promisify(execFile);
10
+ export async function worktreeGit(repoRoot, args, input) {
11
+ const running = execute('git', [args[0] === 'check-ignore' ? '--no-literal-pathspecs' : '--literal-pathspecs', ...args], {
12
+ cwd: repoRoot,
13
+ windowsHide: true,
14
+ timeout: 15000,
15
+ maxBuffer: 4 * 1024 * 1024,
16
+ });
17
+ running.child.stdin?.end(input);
18
+ return (await running).stdout;
19
+ }
20
+ export function localRelativePath(path) {
21
+ const normalized = path.replaceAll('\\', '/');
22
+ if (!normalized ||
23
+ normalized.length > 500 ||
24
+ normalized.startsWith('/') ||
25
+ /[\x00-\x1f\x7f<>:"|?*]/.test(normalized) ||
26
+ normalized
27
+ .split('/')
28
+ .some((part) => !part ||
29
+ part === '.' ||
30
+ part === '..' ||
31
+ /[. ]$/.test(part) ||
32
+ /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)))
33
+ throw new Error('Unsafe local path');
34
+ return normalized;
35
+ }
36
+ function within(root, path) {
37
+ const result = relative(root, path).split(sep).join('/');
38
+ return localRelativePath(result);
39
+ }
40
+ function identity(stat) {
41
+ return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
42
+ }
43
+ async function localFile(root, path) {
44
+ const target = await assertManagedPath(root, resolve(root, localRelativePath(path)), true);
45
+ let stat;
46
+ try {
47
+ stat = await lstat(target);
48
+ }
49
+ catch (error) {
50
+ if (error.code === 'ENOENT')
51
+ return null;
52
+ throw error;
53
+ }
54
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024)
55
+ throw new Error('Unsafe local file');
56
+ const handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
57
+ try {
58
+ if (identity(await handle.stat()) !== identity(stat))
59
+ throw new Error('Local file changed');
60
+ const content = await handle.readFile();
61
+ if (content.length > 4 * 1024 * 1024 || identity(await handle.stat()) !== identity(stat))
62
+ throw new Error('Local file changed');
63
+ await assertManagedPath(root, target, false);
64
+ if (identity(await lstat(target)) !== identity(stat))
65
+ throw new Error('Local file changed');
66
+ return { root, path, content, identity: identity(stat) };
67
+ }
68
+ finally {
69
+ await handle.close();
70
+ }
71
+ }
72
+ export async function assertWorktreeInputs(plan) {
73
+ if ((await worktreeGit(plan.repoRoot, ['rev-parse', 'HEAD'])).trim() !== plan.head)
74
+ throw new Error('Consumer source changed');
75
+ for (const source of [...plan.consumers, ...plan.files.flatMap((file) => file.sources)]) {
76
+ const current = await localFile(source.root, source.path);
77
+ if (!current || current.identity !== source.identity || !current.content.equals(source.content))
78
+ throw new Error('Local source changed');
79
+ }
80
+ }
81
+ export async function assertIgnoredTarget(repoRoot, path) {
82
+ localRelativePath(path);
83
+ if ((await worktreeGit(repoRoot, ['ls-files', '-z', '--', path])).length ||
84
+ (await worktreeGit(repoRoot, ['ls-tree', '-z', 'HEAD', '--', path])).length)
85
+ throw new Error('Runtime target is tracked');
86
+ const ignored = await worktreeGit(repoRoot, ['check-ignore', '-z', '--stdin'], path + '\0');
87
+ if (ignored !== path + '\0')
88
+ throw new Error('Runtime target is not ignored');
89
+ const segments = path.split('/');
90
+ let parent = repoRoot;
91
+ for (const segment of segments.slice(0, -1)) {
92
+ parent = resolve(parent, segment);
93
+ try {
94
+ await lstat(resolve(parent, '.git'));
95
+ throw new Error('Nested repository');
96
+ }
97
+ catch (error) {
98
+ if (error.code !== 'ENOENT')
99
+ throw error;
100
+ }
101
+ }
102
+ }
103
+ function decodeProperty(value) {
104
+ return value.replace(/\\(u[0-9a-fA-F]{4}|u[^\s]*|[\s\S])/g, (_all, escaped) => {
105
+ if (escaped[0] === 'u') {
106
+ if (!/^u[0-9a-fA-F]{4}$/.test(escaped))
107
+ throw new Error('Invalid properties escape');
108
+ return String.fromCharCode(parseInt(escaped.slice(1), 16));
109
+ }
110
+ return { t: '\t', r: '\r', n: '\n', f: '\f' }[escaped] ?? escaped;
111
+ });
112
+ }
113
+ function properties(content) {
114
+ const text = content.toString('latin1');
115
+ const result = new Map();
116
+ let cursor = 0;
117
+ while (cursor < text.length) {
118
+ const start = cursor;
119
+ let logical = '';
120
+ let continued = false;
121
+ do {
122
+ const ending = /\r\n|\r|\n/g;
123
+ ending.lastIndex = cursor;
124
+ const match = ending.exec(text);
125
+ let line = text.slice(cursor, match?.index ?? text.length);
126
+ cursor = match ? match.index + match[0].length : text.length;
127
+ if (!continued && /^[ \t\f]*[#!]/.test(line)) {
128
+ logical = '';
129
+ break;
130
+ }
131
+ if (continued)
132
+ line = line.replace(/^[ \t\f]+/, '');
133
+ const trailing = /\\+$/.exec(line)?.[0].length ?? 0;
134
+ continued = trailing % 2 === 1;
135
+ logical += continued ? line.slice(0, -1) : line;
136
+ } while (continued && cursor < text.length);
137
+ if (continued)
138
+ throw new Error('Incomplete properties continuation');
139
+ const match = /^[ \t\f]*((?:\\.|[^\s:=\\])+)[ \t\f]*(?:[:=][ \t\f]*)?(.*)$/.exec(logical);
140
+ if (!match || /^[ \t\f]*[#!]/.test(logical))
141
+ continue;
142
+ const name = decodeProperty(match[1]);
143
+ if (result.has(name))
144
+ throw new Error('Ambiguous duplicate property');
145
+ result.set(name, { value: decodeProperty(match[2]), start, end: cursor });
146
+ }
147
+ return result;
148
+ }
149
+ function propertyValue(value) {
150
+ return value
151
+ .split('')
152
+ .map((character) => {
153
+ const code = character.charCodeAt(0);
154
+ return code < 0x20 || code > 0x7e
155
+ ? '\\u' + code.toString(16).padStart(4, '0')
156
+ : /[\\:=#! ]/.test(character)
157
+ ? '\\' + character
158
+ : character;
159
+ })
160
+ .join('');
161
+ }
162
+ function unsupportedEnvironment() {
163
+ throw new Error('Unsupported environment loader');
164
+ }
165
+ function environmentTokens(text) {
166
+ const tokens = [];
167
+ let cursor = 0;
168
+ while (cursor < text.length) {
169
+ if (/\s/.test(text[cursor])) {
170
+ cursor++;
171
+ }
172
+ else if (text.startsWith('//', cursor)) {
173
+ while (cursor < text.length && !/[\r\n]/.test(text[cursor]))
174
+ cursor++;
175
+ }
176
+ else if (text.startsWith('/*', cursor)) {
177
+ const end = text.indexOf('*/', cursor + 2);
178
+ if (end < 0)
179
+ unsupportedEnvironment();
180
+ cursor = end + 2;
181
+ }
182
+ else if (['"', "'", '\x60'].includes(text[cursor])) {
183
+ const quote = text[cursor++];
184
+ const start = cursor;
185
+ let escaped = false;
186
+ while (cursor < text.length && text[cursor] !== quote) {
187
+ if (quote === '\x60' && text.startsWith('$' + '{', cursor))
188
+ unsupportedEnvironment();
189
+ if (text[cursor++] === '\\') {
190
+ escaped = true;
191
+ cursor++;
192
+ }
193
+ }
194
+ if (cursor >= text.length)
195
+ unsupportedEnvironment();
196
+ tokens.push({ text: '', literal: escaped ? undefined : text.slice(start, cursor) });
197
+ cursor++;
198
+ }
199
+ else {
200
+ if (text[cursor] === '/')
201
+ unsupportedEnvironment();
202
+ const word = /^[A-Za-z_$][\w$]*/.exec(text.slice(cursor))?.[0];
203
+ tokens.push({ text: word ?? text[cursor] });
204
+ cursor += word?.length ?? 1;
205
+ }
206
+ }
207
+ return tokens;
208
+ }
209
+ function environmentObject(tokens, start) {
210
+ if (tokens[start]?.text !== '{')
211
+ unsupportedEnvironment();
212
+ const fields = new Map();
213
+ let cursor = start + 1;
214
+ while (tokens[cursor]?.text !== '}') {
215
+ const key = tokens[cursor]?.literal ?? tokens[cursor]?.text;
216
+ if (!key ||
217
+ !/^[A-Za-z_$][\w$]*$/.test(key) ||
218
+ fields.has(key) ||
219
+ tokens[cursor + 1]?.text !== ':')
220
+ unsupportedEnvironment();
221
+ cursor += 2;
222
+ const begin = cursor;
223
+ const closing = [];
224
+ while (cursor < tokens.length) {
225
+ const token = tokens[cursor].text;
226
+ if (!closing.length && [',', '}'].includes(token))
227
+ break;
228
+ if (['(', '[', '{'].includes(token))
229
+ closing.push({ '(': ')', '[': ']', '{': '}' }[token]);
230
+ else if ([')', ']', '}'].includes(token) && closing.pop() !== token)
231
+ unsupportedEnvironment();
232
+ cursor++;
233
+ }
234
+ if (cursor === begin || cursor === tokens.length || closing.length)
235
+ unsupportedEnvironment();
236
+ fields.set(key, tokens.slice(begin, cursor));
237
+ if (tokens[cursor]?.text === ',')
238
+ cursor++;
239
+ }
240
+ return { fields, end: cursor + 1 };
241
+ }
242
+ function environmentArguments(tokens, open) {
243
+ if (tokens[open + 1]?.text === ')')
244
+ return new Map();
245
+ const { fields, end } = environmentObject(tokens, open + 1);
246
+ const close = tokens[end]?.text === ',' ? end + 1 : end;
247
+ if (tokens[close]?.text !== ')')
248
+ unsupportedEnvironment();
249
+ return fields;
250
+ }
251
+ function environmentImports(tokens, module) {
252
+ const names = new Map();
253
+ for (let index = 0; index < tokens.length; index++) {
254
+ if (tokens[index]?.text !== 'import' ||
255
+ tokens[index + 1]?.text === 'type' ||
256
+ tokens[index + 1]?.literal !== undefined ||
257
+ tokens[index + 1]?.text === '(')
258
+ continue;
259
+ let end = index + 1;
260
+ while (end < tokens.length && !['from', ';'].includes(tokens[end].text))
261
+ end++;
262
+ if (tokens[end]?.text !== 'from' || tokens[end + 1]?.literal !== module)
263
+ continue;
264
+ const specifier = tokens.slice(index + 1, end);
265
+ if (specifier.length === 1 && specifier[0]?.text)
266
+ names.set(specifier[0].text, 'default');
267
+ else if (specifier.length === 3 && specifier[0]?.text === '*' && specifier[1]?.text === 'as')
268
+ names.set(specifier[2].text, '*');
269
+ else if (specifier[0]?.text === '{' && specifier.at(-1)?.text === '}') {
270
+ for (let offset = 1; offset < specifier.length - 1;) {
271
+ const exported = specifier[offset++]?.text;
272
+ let alias = exported;
273
+ if (specifier[offset]?.text === 'as') {
274
+ alias = specifier[offset + 1]?.text;
275
+ offset += 2;
276
+ }
277
+ if (!exported || !alias || names.has(alias))
278
+ unsupportedEnvironment();
279
+ names.set(alias, exported);
280
+ if (specifier[offset]?.text === ',')
281
+ offset++;
282
+ else if (specifier[offset]?.text !== '}')
283
+ unsupportedEnvironment();
284
+ }
285
+ }
286
+ else
287
+ unsupportedEnvironment();
288
+ }
289
+ return names;
290
+ }
291
+ function literalEnvironmentValue(tokens) {
292
+ if (tokens?.length !== 1 || tokens[0]?.literal === undefined)
293
+ unsupportedEnvironment();
294
+ return tokens[0].literal;
295
+ }
296
+ function loaderEnvironmentFiles(text) {
297
+ const tokens = environmentTokens(text);
298
+ const nest = environmentImports(tokens, '@nestjs/config');
299
+ const dotenv = environmentImports(tokens, 'dotenv');
300
+ const files = [];
301
+ for (let index = 0; index < tokens.length; index++) {
302
+ const token = tokens[index].text;
303
+ if (token === 'require' &&
304
+ tokens[index + 1]?.text === '(' &&
305
+ tokens[index + 2]?.literal === 'dotenv' &&
306
+ tokens[index + 3]?.text === ')' &&
307
+ tokens[index - 1]?.text === '=' &&
308
+ ['const', 'let', 'var'].includes(tokens[index - 3]?.text ?? '')) {
309
+ dotenv.set(tokens[index - 2].text, '*');
310
+ }
311
+ }
312
+ for (let index = 0; index < tokens.length; index++) {
313
+ const token = tokens[index].text;
314
+ if (tokens[index - 1]?.text === '.')
315
+ continue;
316
+ if ((token === 'import' && tokens[index + 1]?.literal === 'dotenv/config') ||
317
+ (token === 'require' &&
318
+ tokens[index + 1]?.text === '(' &&
319
+ tokens[index + 2]?.literal === 'dotenv/config' &&
320
+ tokens[index + 3]?.text === ')')) {
321
+ files.push({ name: '.env', required: false });
322
+ continue;
323
+ }
324
+ let field;
325
+ let open;
326
+ if (nest.get(token) === 'ConfigModule' &&
327
+ tokens[index + 1]?.text === '.' &&
328
+ tokens[index + 2]?.text === 'forRoot' &&
329
+ tokens[index + 3]?.text === '(') {
330
+ field = 'envFilePath';
331
+ open = index + 3;
332
+ }
333
+ else if (['default', '*'].includes(dotenv.get(token) ?? '') &&
334
+ tokens[index + 1]?.text === '.' &&
335
+ tokens[index + 2]?.text === 'config' &&
336
+ tokens[index + 3]?.text === '(') {
337
+ field = 'path';
338
+ open = index + 3;
339
+ }
340
+ else if (dotenv.get(token) === 'config' && tokens[index + 1]?.text === '(') {
341
+ field = 'path';
342
+ open = index + 1;
343
+ }
344
+ else if (token === 'require' &&
345
+ tokens[index + 1]?.text === '(' &&
346
+ tokens[index + 2]?.literal === 'dotenv' &&
347
+ tokens[index + 3]?.text === ')' &&
348
+ tokens[index + 4]?.text === '.' &&
349
+ tokens[index + 5]?.text === 'config' &&
350
+ tokens[index + 6]?.text === '(') {
351
+ field = 'path';
352
+ open = index + 6;
353
+ }
354
+ else
355
+ continue;
356
+ const options = environmentArguments(tokens, open);
357
+ const ignored = options.get('ignoreEnvFile');
358
+ if (field === 'envFilePath' && ignored) {
359
+ if (ignored.length !== 1 || !['true', 'false'].includes(ignored[0].text))
360
+ unsupportedEnvironment();
361
+ if (ignored[0].text === 'true')
362
+ continue;
363
+ }
364
+ const selected = options.get(field);
365
+ const name = selected ? literalEnvironmentValue(selected) : '.env';
366
+ if (!/^(?:[^/]+\/)*\.env(?:\.local|\.development(?:\.local)?)?$/.test(name))
367
+ unsupportedEnvironment();
368
+ files.push({ name, required: Boolean(selected) });
369
+ }
370
+ return files;
371
+ }
372
+ function viteEnvironmentDirectory(text) {
373
+ const tokens = environmentTokens(text);
374
+ const imports = environmentImports(tokens, 'vite');
375
+ const exports = tokens.flatMap((token, index) => token.text === 'export' && tokens[index + 1]?.text === 'default'
376
+ ? [index + 2]
377
+ : token.text === 'module' &&
378
+ tokens[index + 1]?.text === '.' &&
379
+ tokens[index + 2]?.text === 'exports' &&
380
+ tokens[index + 3]?.text === '='
381
+ ? [index + 4]
382
+ : []);
383
+ if (exports.length !== 1)
384
+ unsupportedEnvironment();
385
+ let start = exports[0];
386
+ const wrapped = imports.get(tokens[start]?.text ?? '') === 'defineConfig';
387
+ if (wrapped) {
388
+ if (tokens[start + 1]?.text !== '(')
389
+ unsupportedEnvironment();
390
+ start += 2;
391
+ }
392
+ const { fields, end } = environmentObject(tokens, start);
393
+ let next = end;
394
+ if (wrapped) {
395
+ if (tokens[next]?.text === ',')
396
+ next++;
397
+ if (tokens[next++]?.text !== ')')
398
+ unsupportedEnvironment();
399
+ }
400
+ if (tokens[next]?.text === ';')
401
+ next++;
402
+ if (next !== tokens.length)
403
+ unsupportedEnvironment();
404
+ const envDir = fields.get('envDir');
405
+ if (envDir?.length === 1 && envDir[0]?.text === 'false')
406
+ return null;
407
+ const root = fields.has('root') ? literalEnvironmentValue(fields.get('root')) : '.';
408
+ const directory = envDir ? literalEnvironmentValue(envDir) : '.';
409
+ if ([root, directory].some((path) => isAbsolute(path) || win32.isAbsolute(path) || path.includes('\\')))
410
+ unsupportedEnvironment();
411
+ return posix.join(root, directory);
412
+ }
413
+ export async function planWorktreeFiles(sourceRoot, mainRoot, repoRoot, previousPaths, pinnedSources = {}) {
414
+ const plan = {
415
+ repoRoot,
416
+ head: (await worktreeGit(repoRoot, ['rev-parse', 'HEAD'])).trim(),
417
+ files: [],
418
+ consumers: [],
419
+ sources: { ...pinnedSources },
420
+ problems: [],
421
+ previousPaths,
422
+ externalDependencies: 0,
423
+ };
424
+ const problem = (path, reason) => {
425
+ if (!plan.problems.some((item) => item.path === path && item.reason === reason))
426
+ plan.problems.push({ path, reason });
427
+ };
428
+ const common = async (root) => {
429
+ const value = (await worktreeGit(root, ['rev-parse', '--path-format=absolute', '--git-common-dir'])).trim();
430
+ const canonical = await realpath(value);
431
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
432
+ };
433
+ const targetCommon = await common(repoRoot);
434
+ if ((await common(sourceRoot)) !== targetCommon || (await common(mainRoot)) !== targetCommon)
435
+ throw new Error('Runtime source is another clone');
436
+ const tracked = (await worktreeGit(repoRoot, ['ls-files', '-z'])).split('\0').filter(Boolean);
437
+ const supported = tracked.filter((path) => /(?:^|\/)(?:build|settings)\.gradle(?:\.kts)?$/.test(path) ||
438
+ /(?:^|\/)(?:package\.json|.*\.config\.[cm]?[jt]s|.*\.module\.ts)$/.test(path));
439
+ if (supported.length > 250)
440
+ throw new Error('Too many runtime consumers');
441
+ const consumers = new Map();
442
+ for (const path of supported) {
443
+ const file = await localFile(repoRoot, path);
444
+ if (!file || file.content.length > 256 * 1024)
445
+ throw new Error('Consumer unavailable');
446
+ plan.consumers.push(file);
447
+ consumers.set(path, file.content.toString('utf8'));
448
+ }
449
+ const choose = async (path, required) => {
450
+ localRelativePath(path);
451
+ const pinned = plan.sources[path];
452
+ const roots = pinned ? [pinned] : [...new Set([sourceRoot, mainRoot])];
453
+ for (const root of roots) {
454
+ if (root !== sourceRoot && root !== mainRoot)
455
+ throw new Error('Unknown runtime source');
456
+ const file = await localFile(root, path);
457
+ if (file) {
458
+ plan.sources[path] = root;
459
+ return file;
460
+ }
461
+ }
462
+ if (required || pinned)
463
+ problem(path, WorktreeFileIssue.Missing);
464
+ return null;
465
+ };
466
+ const add = async (file, content = file.content, dependencies = []) => {
467
+ await assertIgnoredTarget(repoRoot, file.path);
468
+ const existing = plan.files.find((item) => item.path === file.path);
469
+ if (existing && !existing.content.equals(content)) {
470
+ problem(file.path, WorktreeFileIssue.Conflict);
471
+ return;
472
+ }
473
+ if (existing)
474
+ existing.sources.push(...dependencies);
475
+ else
476
+ plan.files.push({ path: file.path, content, sources: [file, ...dependencies] });
477
+ };
478
+ const simple = async (path, required) => {
479
+ try {
480
+ const file = await choose(path, required);
481
+ if (file)
482
+ await add(file);
483
+ }
484
+ catch {
485
+ problem(path, WorktreeFileIssue.Unsafe);
486
+ }
487
+ };
488
+ const androidRoots = new Set();
489
+ for (const [buildPath, text] of consumers) {
490
+ if (!/(?:^|\/)build\.gradle(?:\.kts)?$/.test(buildPath))
491
+ continue;
492
+ const module = posix.dirname(buildPath);
493
+ const roots = tracked
494
+ .filter((path) => /(?:^|\/)settings\.gradle(?:\.kts)?$/.test(path) &&
495
+ (module === posix.dirname(path) ||
496
+ module.startsWith(posix.dirname(path) + '/') ||
497
+ posix.dirname(path) === '.'))
498
+ .map(posix.dirname)
499
+ .sort((a, b) => b.length - a.length);
500
+ const android = roots[0] ?? (buildPath.startsWith('android/') ? 'android' : '.');
501
+ if (/com\.android\.|com\.android\b|flutter|storeFile/.test(text))
502
+ androidRoots.add(android);
503
+ if (/com\.google\.gms\.google-services/.test(text))
504
+ await simple(posix.join(module, 'google-services.json'), true);
505
+ let signing;
506
+ try {
507
+ signing = parseGradleSigning(text);
508
+ }
509
+ catch {
510
+ problem(buildPath, WorktreeFileIssue.Unsupported);
511
+ continue;
512
+ }
513
+ if (!signing)
514
+ continue;
515
+ let path;
516
+ try {
517
+ path = within(repoRoot, resolve(repoRoot, signing.propertiesRoot ? android : module, signing.propertiesPath));
518
+ }
519
+ catch {
520
+ problem(buildPath, WorktreeFileIssue.Unsafe);
521
+ continue;
522
+ }
523
+ try {
524
+ const file = await choose(path, true);
525
+ if (!file)
526
+ continue;
527
+ const field = properties(file.content).get('storeFile');
528
+ if (!field?.value) {
529
+ problem(path, WorktreeFileIssue.Missing);
530
+ continue;
531
+ }
532
+ const base = signing.keyRoot ? android : module;
533
+ const absolute = isAbsolute(field.value) || win32.isAbsolute(field.value);
534
+ const keyPath = absolute
535
+ ? resolve(field.value)
536
+ : resolve(file.root, base, field.value.replaceAll('\\', '/'));
537
+ let keyRelative;
538
+ try {
539
+ keyRelative = within(file.root, keyPath);
540
+ }
541
+ catch {
542
+ const fromRoot = relative(file.root, keyPath);
543
+ if (!fromRoot.startsWith('..' + sep) && !isAbsolute(fromRoot))
544
+ throw new Error('Unsafe key reference');
545
+ if (keyPath.startsWith('\\\\') ||
546
+ (process.platform === 'win32' && !/^[A-Za-z]:[\\/]/.test(keyPath)))
547
+ throw new Error('Unsupported external key reference');
548
+ localRelativePath(relative(parse(keyPath).root, keyPath).split(sep).join('/'));
549
+ const external = await lstat(keyPath);
550
+ if (!external.isFile() || external.isSymbolicLink())
551
+ throw new Error('External key unavailable');
552
+ let output = file.content;
553
+ if (!absolute) {
554
+ const text = file.content.toString('latin1');
555
+ const ending = text.slice(field.start, field.end).endsWith('\r\n') ? '\r\n' : '\n';
556
+ output = Buffer.from(text.slice(0, field.start) +
557
+ 'storeFile=' +
558
+ propertyValue(keyPath.replaceAll('\\', '/')) +
559
+ ending +
560
+ text.slice(field.end), 'latin1');
561
+ }
562
+ await add(file, output);
563
+ plan.externalDependencies++;
564
+ continue;
565
+ }
566
+ const key = await localFile(file.root, keyRelative);
567
+ if (!key) {
568
+ problem(keyRelative, WorktreeFileIssue.Missing);
569
+ continue;
570
+ }
571
+ if (file.root !== sourceRoot && (await localFile(sourceRoot, keyRelative))) {
572
+ problem(path, WorktreeFileIssue.Conflict);
573
+ continue;
574
+ }
575
+ let output = file.content;
576
+ if (absolute) {
577
+ const text = file.content.toString('latin1');
578
+ const ending = text.slice(field.start, field.end).endsWith('\r\n') ? '\r\n' : '\n';
579
+ output = Buffer.from(text.slice(0, field.start) +
580
+ 'storeFile=' +
581
+ propertyValue(resolve(repoRoot, keyRelative).replaceAll('\\', '/')) +
582
+ ending +
583
+ text.slice(field.end), 'latin1');
584
+ }
585
+ await assertIgnoredTarget(repoRoot, path);
586
+ await add(key);
587
+ await add(file, output, [key]);
588
+ }
589
+ catch {
590
+ problem(path, WorktreeFileIssue.Unsafe);
591
+ }
592
+ }
593
+ for (const root of androidRoots)
594
+ await simple(posix.join(root, 'local.properties'), false);
595
+ const packageRoots = tracked
596
+ .filter((path) => posix.basename(path) === 'package.json')
597
+ .map(posix.dirname);
598
+ for (const [path, text] of consumers) {
599
+ if (posix.basename(path) !== 'package.json')
600
+ continue;
601
+ let manifest;
602
+ try {
603
+ manifest = JSON.parse(text);
604
+ }
605
+ catch {
606
+ continue;
607
+ }
608
+ const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
609
+ const root = posix.dirname(path);
610
+ const belongs = (name) => {
611
+ const owner = packageRoots
612
+ .filter((candidate) => candidate === '.' || name.startsWith(candidate + '/'))
613
+ .sort((a, b) => (b === '.' ? 0 : b.length) - (a === '.' ? 0 : a.length))[0];
614
+ return owner === root;
615
+ };
616
+ const loaders = [...consumers].filter(([name]) => /\.[cm]?[jt]s$/.test(name) && belongs(name));
617
+ const selected = [];
618
+ try {
619
+ if (dependencies.vite) {
620
+ const configs = loaders.filter(([name]) => posix.dirname(name) === root && /^vite\.config\./.test(posix.basename(name)));
621
+ if (configs.length > 1)
622
+ unsupportedEnvironment();
623
+ for (const script of Object.values(manifest.scripts ?? {})) {
624
+ const args = script.trim().split(/\s+/);
625
+ const vite = args.indexOf('vite');
626
+ if (vite < 0)
627
+ continue;
628
+ const tail = args.slice(vite + 1);
629
+ if (tail.some((arg) => /^(?:--(?:config|mode)|-c)(?:=|$)/.test(arg)) ||
630
+ tail
631
+ .filter((arg) => !arg.startsWith('-'))
632
+ .some((arg) => !['dev', 'serve', 'build', 'preview'].includes(arg)))
633
+ unsupportedEnvironment();
634
+ }
635
+ const directory = configs.length ? viteEnvironmentDirectory(configs[0][1]) : '.';
636
+ if (directory !== null) {
637
+ for (const name of ['.env', '.env.local', '.env.development', '.env.development.local'])
638
+ selected.push({ name: posix.join(directory, name), required: false });
639
+ }
640
+ }
641
+ else if (dependencies.next) {
642
+ for (const name of ['.env', '.env.local', '.env.development', '.env.development.local'])
643
+ selected.push({ name, required: false });
644
+ }
645
+ else if (dependencies.dotenv || dependencies['@nestjs/config']) {
646
+ for (const [, content] of loaders)
647
+ selected.push(...loaderEnvironmentFiles(content));
648
+ }
649
+ const paths = selected.map((file) => ({
650
+ path: within(repoRoot, resolve(repoRoot, root, file.name)),
651
+ required: file.required,
652
+ }));
653
+ for (const file of paths)
654
+ await simple(file.path, file.required);
655
+ }
656
+ catch {
657
+ problem(path, WorktreeFileIssue.Unsupported);
658
+ }
659
+ }
660
+ if (plan.files.length > 100 ||
661
+ plan.files.reduce((sum, file) => sum + file.content.length, 0) > 16 * 1024 * 1024)
662
+ throw new Error('Local preparation limit exceeded');
663
+ const ignored = (await worktreeGit(repoRoot, [
664
+ '--no-literal-pathspecs',
665
+ 'ls-files',
666
+ '--others',
667
+ '--ignored',
668
+ '--exclude-standard',
669
+ '-z',
670
+ '--',
671
+ ':(glob)**/key.properties',
672
+ ':(glob)**/local.properties',
673
+ ':(glob)**/*.jks',
674
+ ':(glob)**/*.keystore',
675
+ ':(glob)**/google-services.json',
676
+ ':(glob)**/.env',
677
+ ':(glob)**/.env.local',
678
+ ':(glob)**/.env.development',
679
+ ':(glob)**/.env.development.local',
680
+ ':(exclude,glob)**/node_modules/**',
681
+ ':(exclude,glob)**/build/**',
682
+ ':(exclude,glob)**/.gradle/**',
683
+ ':(exclude,glob)**/.dart_tool/**',
684
+ ]))
685
+ .split('\0')
686
+ .filter(Boolean);
687
+ for (const path of new Set([...previousPaths, ...ignored])) {
688
+ if (plan.files.some((file) => file.path === path))
689
+ continue;
690
+ try {
691
+ if (await localFile(repoRoot, path))
692
+ problem(path, WorktreeFileIssue.Stale);
693
+ }
694
+ catch {
695
+ problem(path, WorktreeFileIssue.Unsafe);
696
+ }
697
+ }
698
+ const ordered = [];
699
+ const ordering = new Set();
700
+ const order = (file) => {
701
+ if (ordered.includes(file))
702
+ return;
703
+ if (ordering.has(file.path))
704
+ throw new Error('Cyclic runtime dependency');
705
+ ordering.add(file.path);
706
+ for (const source of file.sources) {
707
+ const dependency = plan.files.find((item) => item.path === source.path && item !== file);
708
+ if (dependency)
709
+ order(dependency);
710
+ }
711
+ ordering.delete(file.path);
712
+ ordered.push(file);
713
+ };
714
+ plan.files.forEach(order);
715
+ plan.files = ordered;
716
+ return plan;
717
+ }
718
+ export async function createWorktreeStage(plan) {
719
+ const parent = dirname(plan.repoRoot);
720
+ await assertManagedPath(parent, parent, false);
721
+ const directory = await mkdtemp(resolve(parent, 'em-local-'));
722
+ const info = await lstat(directory);
723
+ return {
724
+ directory,
725
+ repoRoot: plan.repoRoot,
726
+ count: plan.files.length,
727
+ identity: [info.dev, info.ino].join(':'),
728
+ plan,
729
+ };
730
+ }
731
+ async function assertStage(stage, missing = false) {
732
+ if (dirname(stage.directory) !== dirname(stage.repoRoot) ||
733
+ !/^em-local-[A-Za-z0-9]+$/.test(stage.directory.slice(dirname(stage.directory).length + 1)) ||
734
+ !Number.isInteger(stage.count) ||
735
+ stage.count < 0 ||
736
+ stage.count > 100)
737
+ throw new Error('Unknown runtime staging directory');
738
+ const directory = await assertManagedPath(dirname(stage.repoRoot), stage.directory, missing);
739
+ let info;
740
+ try {
741
+ info = await lstat(directory);
742
+ }
743
+ catch (error) {
744
+ if (missing && error.code === 'ENOENT')
745
+ return null;
746
+ throw error;
747
+ }
748
+ if (!info.isDirectory() ||
749
+ info.isSymbolicLink() ||
750
+ [info.dev, info.ino].join(':') !== stage.identity)
751
+ throw new Error('Runtime staging directory changed');
752
+ return directory;
753
+ }
754
+ export async function stageWorktreeFiles(stage) {
755
+ const directory = (await assertStage(stage));
756
+ if (process.platform === 'win32') {
757
+ const account = await execute('whoami.exe', ['/user', '/fo', 'csv', '/nh'], {
758
+ timeout: 10000,
759
+ windowsHide: true,
760
+ maxBuffer: 8192,
761
+ });
762
+ const sid = /S-1-5-\d+(?:-\d+)+/.exec(account.stdout)?.[0];
763
+ if (!sid)
764
+ throw new Error('Local file access could not be restricted');
765
+ await execute('icacls.exe', [directory, '/inheritance:r', '/grant:r', '*' + sid + ':(OI)(CI)F'], {
766
+ timeout: 10000,
767
+ windowsHide: true,
768
+ maxBuffer: 8192,
769
+ });
770
+ }
771
+ else
772
+ await chmod(directory, 0o700);
773
+ for (const [index, file] of stage.plan.files.entries()) {
774
+ const handle = await open(resolve(directory, String(index)), 'wx', 0o600);
775
+ try {
776
+ await handle.writeFile(file.content);
777
+ await handle.sync();
778
+ }
779
+ finally {
780
+ await handle.close();
781
+ }
782
+ }
783
+ await assertWorktreeInputs(stage.plan);
784
+ }
785
+ export async function discardStagedWorktreeFiles(stage) {
786
+ const directory = await assertStage(stage, true);
787
+ if (!directory)
788
+ return;
789
+ for (let index = 0; index < stage.count; index++) {
790
+ const path = await assertManagedPath(directory, resolve(directory, String(index)), true);
791
+ await rm(path, { force: true });
792
+ }
793
+ await rmdir(directory);
794
+ }
795
+ export async function publishWorktreeFiles(stage) {
796
+ const { plan } = stage;
797
+ const result = {
798
+ status: WorktreeFileStatus.Attention,
799
+ copied: 0,
800
+ unchanged: 0,
801
+ problems: [...plan.problems],
802
+ externalDependencies: plan.externalDependencies,
803
+ };
804
+ const fail = (path, reason) => {
805
+ result.problems.push({ path, reason });
806
+ };
807
+ try {
808
+ await assertStage(stage);
809
+ for (const [index, file] of plan.files.entries()) {
810
+ const staged = await localFile(stage.directory, String(index));
811
+ if (!staged?.content.equals(file.content))
812
+ throw new Error('Staged runtime file changed');
813
+ }
814
+ await assertWorktreeInputs(plan);
815
+ }
816
+ catch {
817
+ fail('.', WorktreeFileIssue.SourceChanged);
818
+ return result;
819
+ }
820
+ const parents = new Map();
821
+ const exists = new Set();
822
+ for (const file of plan.files) {
823
+ try {
824
+ await assertIgnoredTarget(plan.repoRoot, file.path);
825
+ const target = await assertManagedPath(plan.repoRoot, resolve(plan.repoRoot, file.path), true);
826
+ await ensureManagedDirectory(plan.repoRoot, dirname(target));
827
+ const segments = file.path.split('/').slice(0, -1);
828
+ let parent = plan.repoRoot;
829
+ for (const segment of ['', ...segments]) {
830
+ if (segment)
831
+ parent = resolve(parent, segment);
832
+ const stat = await lstat(parent);
833
+ parents.set(parent, [stat.dev, stat.ino].join(':'));
834
+ }
835
+ try {
836
+ const stat = await lstat(target);
837
+ if (!stat.isFile() ||
838
+ stat.isSymbolicLink() ||
839
+ stat.size > 4 * 1024 * 1024 ||
840
+ !(await readFile(target)).equals(file.content)) {
841
+ fail(file.path, WorktreeFileIssue.Conflict);
842
+ }
843
+ else
844
+ exists.add(file.path);
845
+ }
846
+ catch (error) {
847
+ if (error.code !== 'ENOENT')
848
+ throw error;
849
+ }
850
+ }
851
+ catch {
852
+ fail(file.path, WorktreeFileIssue.Unsafe);
853
+ }
854
+ }
855
+ if (result.problems.length)
856
+ return result;
857
+ for (const [index, file] of plan.files.entries()) {
858
+ try {
859
+ for (const [parent, expected] of parents) {
860
+ await assertManagedPath(plan.repoRoot, parent, false);
861
+ const stat = await lstat(parent);
862
+ if ([stat.dev, stat.ino].join(':') !== expected)
863
+ throw new Error('Runtime parent changed');
864
+ }
865
+ await assertIgnoredTarget(plan.repoRoot, file.path);
866
+ const target = await assertManagedPath(plan.repoRoot, resolve(plan.repoRoot, file.path), true);
867
+ if (exists.has(file.path)) {
868
+ if (!(await readFile(target)).equals(file.content))
869
+ throw new Error('Runtime target changed');
870
+ result.unchanged++;
871
+ continue;
872
+ }
873
+ const source = await assertManagedPath(stage.directory, resolve(stage.directory, String(index)), false);
874
+ const bytes = await localFile(stage.directory, String(index));
875
+ if (!bytes?.content.equals(file.content))
876
+ throw new Error('Staged runtime file changed');
877
+ await link(source, target);
878
+ await assertManagedPath(plan.repoRoot, target, false);
879
+ const [published, staged] = await Promise.all([lstat(target), lstat(source)]);
880
+ if (published.ino !== staged.ino ||
881
+ published.dev !== staged.dev ||
882
+ !(await readFile(target)).equals(file.content))
883
+ throw new Error('Runtime target changed');
884
+ result.copied++;
885
+ }
886
+ catch {
887
+ fail(file.path, WorktreeFileIssue.Failed);
888
+ return result;
889
+ }
890
+ }
891
+ try {
892
+ await assertWorktreeInputs(plan);
893
+ }
894
+ catch {
895
+ fail('.', WorktreeFileIssue.SourceChanged);
896
+ return result;
897
+ }
898
+ result.status = plan.files.length ? WorktreeFileStatus.Prepared : WorktreeFileStatus.NotNeeded;
899
+ return result;
900
+ }
901
+ //# sourceMappingURL=worktree-preparation.js.map