@memberjunction/open-app-engine 6.2.0-edge.4 → 6.2.0-edge.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/github/github-client.d.ts +8 -5
  2. package/dist/github/github-client.d.ts.map +1 -1
  3. package/dist/github/github-client.js +405 -44
  4. package/dist/github/github-client.js.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/install/config-manager.d.ts.map +1 -1
  10. package/dist/install/config-manager.js +7 -0
  11. package/dist/install/config-manager.js.map +1 -1
  12. package/dist/install/install-orchestrator.d.ts.map +1 -1
  13. package/dist/install/install-orchestrator.js +20 -17
  14. package/dist/install/install-orchestrator.js.map +1 -1
  15. package/dist/install/package-manager.d.ts.map +1 -1
  16. package/dist/install/package-manager.js.map +1 -1
  17. package/dist/install/temp-dir.d.ts +2 -0
  18. package/dist/install/temp-dir.d.ts.map +1 -0
  19. package/dist/install/temp-dir.js +8 -0
  20. package/dist/install/temp-dir.js.map +1 -0
  21. package/dist/manifest/manifest-schema.d.ts +207 -64
  22. package/dist/manifest/manifest-schema.d.ts.map +1 -1
  23. package/dist/manifest/manifest-schema.js +20 -0
  24. package/dist/manifest/manifest-schema.js.map +1 -1
  25. package/dist/manifest/package-platform.d.ts +8 -0
  26. package/dist/manifest/package-platform.d.ts.map +1 -0
  27. package/dist/manifest/package-platform.js +14 -0
  28. package/dist/manifest/package-platform.js.map +1 -0
  29. package/dist/scaffold/app-scaffolder.d.ts +60 -0
  30. package/dist/scaffold/app-scaffolder.d.ts.map +1 -0
  31. package/dist/scaffold/app-scaffolder.js +786 -0
  32. package/dist/scaffold/app-scaffolder.js.map +1 -0
  33. package/package.json +6 -6
@@ -31,13 +31,16 @@ export declare class GitHubAccessError extends Error {
31
31
  readonly Status: number;
32
32
  constructor(status: number, message: string);
33
33
  }
34
- export declare function FetchManifestFromGitHub(repoUrl: string, version: string | undefined, options: GitHubClientOptions, subpath?: string): Promise<ManifestFetchResult>;
34
+ export declare function FetchManifestFromGitHub(repoUrl: string, version: string | undefined, options: GitHubClientOptions, subpath?: string, appName?: string): Promise<ManifestFetchResult>;
35
35
  export declare function ListGitHubReleases(repoUrl: string, options: GitHubClientOptions): Promise<GitHubRelease[]>;
36
- export declare function DownloadMigrations(repoUrl: string, version: string | undefined, migrationsPath: string, localDir: string, options: GitHubClientOptions, subpath?: string): Promise<MigrationDownloadResult>;
37
- export declare function GetLatestVersion(repoUrl: string, options: GitHubClientOptions, subpath?: string): Promise<string | null>;
38
- export declare function ListGitHubTags(repoUrl: string, options: GitHubClientOptions, subpath?: string): Promise<string[]>;
36
+ export declare function ExtractSqlFilesFromTarGz(tarGzBuffer: Buffer, cleanPath: string, localDir: string): string[];
37
+ export declare function DownloadMigrations(repoUrl: string, version: string | undefined, migrationsPath: string, localDir: string, options: GitHubClientOptions, subpath?: string, appName?: string): Promise<MigrationDownloadResult>;
38
+ export declare function DescribeUnresolvedVersion(repoUrl: string, options: GitHubClientOptions, subpath?: string): Promise<string>;
39
+ export declare function GetLatestVersion(repoUrl: string, options: GitHubClientOptions, subpath?: string, appName?: string): Promise<string | null>;
40
+ export declare function FindVersionTagName(repoUrl: string, options: GitHubClientOptions, version: string, subpath?: string, appName?: string): Promise<string | null>;
41
+ export declare function ListGitHubTags(repoUrl: string, options: GitHubClientOptions, subpath?: string, appName?: string): Promise<string[]>;
39
42
  export declare function ClearGitHubTagCache(): void;
40
- export declare function ValidateGitHubTag(repoUrl: string, version: string, options: GitHubClientOptions, subpath?: string): Promise<{
43
+ export declare function ValidateGitHubTag(repoUrl: string, version: string, options: GitHubClientOptions, subpath?: string, appName?: string): Promise<{
41
44
  Exists: boolean;
42
45
  ErrorMessage?: string;
43
46
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"github-client.d.ts","sourceRoot":"","sources":["../../src/github/github-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"github-client.d.ts","sourceRoot":"","sources":["../../src/github/github-client.ts"],"names":[],"mappings":"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"}
@@ -1,29 +1,91 @@
1
1
  import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
+ import { gunzipSync } from 'node:zlib';
3
4
  import { Octokit } from '@octokit/rest';
4
5
  import semver from 'semver';
6
+ function trimSlashes(value) {
7
+ let start = 0;
8
+ let end = value.length;
9
+ while (start < end && value.charCodeAt(start) === 47)
10
+ start++;
11
+ while (end > start && value.charCodeAt(end - 1) === 47)
12
+ end--;
13
+ return value.slice(start, end);
14
+ }
15
+ function SubpathOrUndefined(value) {
16
+ return value === undefined ? undefined : trimSlashes(value);
17
+ }
18
+ function TrimLeadingSlashes(value) {
19
+ let start = 0;
20
+ while (start < value.length && value.charCodeAt(start) === 47) {
21
+ start++;
22
+ }
23
+ return value.slice(start);
24
+ }
25
+ function TrimSlashes(value) {
26
+ let start = 0;
27
+ let end = value.length;
28
+ while (start < end && value.charCodeAt(start) === 47) {
29
+ start++;
30
+ }
31
+ while (end > start && value.charCodeAt(end - 1) === 47) {
32
+ end--;
33
+ }
34
+ return value.slice(start, end);
35
+ }
5
36
  export function ParseGitHubUrl(repoUrl) {
6
- const match = repoUrl.match(/github\.com\/([^/?#]+)\/([^/?#]+)((?:\/[^?#]+)*)/);
7
- if (!match) {
37
+ const hostIdx = repoUrl.indexOf('github.com/');
38
+ if (hostIdx < 0) {
39
+ return null;
40
+ }
41
+ const afterHost = repoUrl.slice(hostIdx + 'github.com/'.length);
42
+ let endIdx = afterHost.length;
43
+ const qIdx = afterHost.indexOf('?');
44
+ if (qIdx >= 0 && qIdx < endIdx)
45
+ endIdx = qIdx;
46
+ const hIdx = afterHost.indexOf('#');
47
+ if (hIdx >= 0 && hIdx < endIdx)
48
+ endIdx = hIdx;
49
+ const pathPart = TrimSlashes(afterHost.slice(0, endIdx));
50
+ const firstSlash = pathPart.indexOf('/');
51
+ if (firstSlash <= 0) {
52
+ return null;
53
+ }
54
+ const owner = pathPart.slice(0, firstSlash);
55
+ const remainder = pathPart.slice(firstSlash + 1);
56
+ const secondSlash = remainder.indexOf('/');
57
+ const rawRepo = secondSlash >= 0 ? remainder.slice(0, secondSlash) : remainder;
58
+ if (!rawRepo) {
59
+ return null;
60
+ }
61
+ const repo = rawRepo.endsWith('.git') ? rawRepo.slice(0, -4) : rawRepo;
62
+ if (!repo) {
8
63
  return null;
9
64
  }
10
- const owner = match[1];
11
- const repo = match[2].replace(/\.git$/, '');
12
- const rawSubpath = (match[3] ?? '').replace(/^\/+|\/+$/g, '');
65
+ const rawSubpath = secondSlash >= 0 ? TrimSlashes(remainder.slice(secondSlash + 1)) : '';
13
66
  const subpath = rawSubpath.length > 0 ? rawSubpath : undefined;
14
67
  return { Owner: owner, Repo: repo, Subpath: subpath };
15
68
  }
16
69
  function ResolveToken(repoUrl, options) {
17
70
  if (options.TokenMap) {
71
+ const entries = Object.entries(options.TokenMap);
18
72
  const normalized = normalizeRepoUrl(repoUrl);
19
- for (const [mapUrl, mapToken] of Object.entries(options.TokenMap)) {
20
- if (normalizeRepoUrl(mapUrl) === normalized) {
21
- return mapToken;
22
- }
73
+ const exact = entries.find(([mapUrl]) => normalizeRepoUrl(mapUrl) === normalized);
74
+ if (exact) {
75
+ return exact[1];
76
+ }
77
+ const repoKey = repositoryKey(repoUrl);
78
+ const sameRepo = repoKey ? entries.find(([mapUrl]) => repositoryKey(mapUrl) === repoKey) : undefined;
79
+ if (sameRepo) {
80
+ return sameRepo[1];
23
81
  }
24
82
  }
25
83
  return options.Token;
26
84
  }
85
+ function repositoryKey(url) {
86
+ const parsed = ParseGitHubUrl(url);
87
+ return parsed ? `${parsed.Owner}/${parsed.Repo}`.toLowerCase() : null;
88
+ }
27
89
  function normalizeRepoUrl(url) {
28
90
  return url.replace(/\.git$/, '').replace(/\/$/, '').toLowerCase();
29
91
  }
@@ -84,6 +146,38 @@ function ThrowIfRateLimitedOrForbidden(error, context) {
84
146
  `This is NOT the same as "no versions found" — check your GitHub token and rate limit.`);
85
147
  }
86
148
  }
149
+ async function ProbeRepoVisibility(repoUrl, parsed, options) {
150
+ try {
151
+ await CreateOctokit(repoUrl, options).repos.get({ owner: parsed.Owner, repo: parsed.Repo });
152
+ return { State: 'Readable' };
153
+ }
154
+ catch (error) {
155
+ if (OctokitStatus(error) === 404) {
156
+ return { State: 'NotReadable' };
157
+ }
158
+ return { State: 'Undetermined', Reason: error instanceof Error ? error.message : String(error) };
159
+ }
160
+ }
161
+ const CONFIGURE_CREDENTIAL_REMEDY = 'set GITHUB_TOKEN in the environment, or add the repo to openApps.github.tokens (or set openApps.github.token) in mj.config.cjs';
162
+ function UnreadableRepoMessage(repoUrl, parsed, options) {
163
+ const target = `${parsed.Owner}/${parsed.Repo}`;
164
+ return ResolveToken(repoUrl, options)
165
+ ? `Cannot read ${target}. The repository does not exist, or the GitHub credential supplied does not grant access to it — check the token is valid and carries 'repo' scope for ${target}.`
166
+ : `Cannot read ${target}. The repository is private or does not exist, and no GitHub credential was supplied — ${CONFIGURE_CREDENTIAL_REMEDY}, then retry.`;
167
+ }
168
+ async function DescribeNotFound(repoUrl, parsed, options, describeMissingTarget) {
169
+ const visibility = await ProbeRepoVisibility(repoUrl, parsed, options);
170
+ switch (visibility.State) {
171
+ case 'Readable':
172
+ return describeMissingTarget();
173
+ case 'NotReadable':
174
+ return UnreadableRepoMessage(repoUrl, parsed, options);
175
+ case 'Undetermined':
176
+ return `Could not confirm ${parsed.Owner}/${parsed.Repo} is readable: ${visibility.Reason}. `
177
+ + `If it is private, a GitHub credential may be required — ${CONFIGURE_CREDENTIAL_REMEDY}. `
178
+ + `If it is readable, then: ${describeMissingTarget()}`;
179
+ }
180
+ }
87
181
  async function FetchFileContent(octokit, owner, repo, path, ref) {
88
182
  const { data } = await octokit.repos.getContent({ owner, repo, path, ref });
89
183
  if (Array.isArray(data) || data.type !== 'file') {
@@ -103,9 +197,22 @@ async function ListDirectory(octokit, owner, repo, path, ref) {
103
197
  return data.map(item => ({ name: item.name, path: item.path, type: item.type, sha: item.sha }));
104
198
  }
105
199
  function ScopedTagPrefix(subpath) {
106
- const s = subpath?.replace(/^\/+|\/+$/g, '');
200
+ const s = SubpathOrUndefined(subpath);
107
201
  return s ? s.replace(/\//g, '-') : undefined;
108
202
  }
203
+ function ScopedTagCandidates(subpath, appName) {
204
+ const out = [];
205
+ const folder = ScopedTagPrefix(subpath);
206
+ if (folder)
207
+ out.push(folder);
208
+ const name = appName?.trim();
209
+ if (name && name !== folder)
210
+ out.push(name);
211
+ return out;
212
+ }
213
+ function EscapeForRegExp(v) {
214
+ return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
215
+ }
109
216
  function ResolveRef(version, subpath) {
110
217
  if (!version)
111
218
  return 'HEAD';
@@ -114,15 +221,18 @@ function ResolveRef(version, subpath) {
114
221
  return prefix ? `${prefix}@${v}` : `v${v}`;
115
222
  }
116
223
  function ComposeRepoPath(effectiveSubpath, relativePath) {
117
- return [effectiveSubpath, relativePath.replace(/^\/|\/$/g, '')].filter(Boolean).join('/');
224
+ return [effectiveSubpath, TrimSlashes(relativePath)].filter(Boolean).join('/');
118
225
  }
119
- export async function FetchManifestFromGitHub(repoUrl, version, options, subpath) {
226
+ export async function FetchManifestFromGitHub(repoUrl, version, options, subpath, appName) {
120
227
  const parsed = ParseGitHubUrl(repoUrl);
121
228
  if (!parsed) {
122
229
  return { Success: false, ErrorMessage: `Invalid GitHub URL: ${repoUrl}` };
123
230
  }
124
- const effectiveSubpath = (subpath ?? parsed.Subpath)?.replace(/^\/+|\/+$/g, '');
125
- const ref = ResolveRef(version, effectiveSubpath);
231
+ const effectiveSubpath = SubpathOrUndefined(subpath ?? parsed.Subpath);
232
+ const ref = version
233
+ ? (await FindVersionTagName(repoUrl, options, version, effectiveSubpath, appName))
234
+ ?? ResolveRef(version, effectiveSubpath)
235
+ : ResolveRef(version, effectiveSubpath);
126
236
  const manifestPath = ComposeRepoPath(effectiveSubpath, 'mj-app.json');
127
237
  try {
128
238
  const content = await FetchFileContent(CreateOctokit(repoUrl, options), parsed.Owner, parsed.Repo, manifestPath, ref);
@@ -130,7 +240,10 @@ export async function FetchManifestFromGitHub(repoUrl, version, options, subpath
130
240
  }
131
241
  catch (error) {
132
242
  if (OctokitStatus(error) === 404) {
133
- return { Success: false, ErrorMessage: `${manifestPath} not found in ${parsed.Owner}/${parsed.Repo} at ref ${ref}` };
243
+ return {
244
+ Success: false,
245
+ ErrorMessage: await DescribeNotFound(repoUrl, parsed, options, () => `${manifestPath} not found in ${parsed.Owner}/${parsed.Repo} at ref ${ref}`),
246
+ };
134
247
  }
135
248
  const message = error instanceof Error ? error.message : String(error);
136
249
  return { Success: false, ErrorMessage: `Failed to fetch manifest: ${message}` };
@@ -168,7 +281,7 @@ async function listSqlFilesRecursive(octokit, owner, repo, root, ref, currentDir
168
281
  const out = [];
169
282
  for (const item of items) {
170
283
  if (item.type === 'file' && item.name.endsWith('.sql')) {
171
- out.push({ path: item.path, relativePath: item.path.slice(root.length).replace(/^\/+/, '') });
284
+ out.push({ path: item.path, relativePath: TrimLeadingSlashes(item.path.slice(root.length)) });
172
285
  }
173
286
  else if (item.type === 'dir') {
174
287
  out.push(...await listSqlFilesRecursive(octokit, owner, repo, root, ref, item.path, depth + 1));
@@ -176,18 +289,207 @@ async function listSqlFilesRecursive(octokit, owner, repo, root, ref, currentDir
176
289
  }
177
290
  return out;
178
291
  }
179
- export async function DownloadMigrations(repoUrl, version, migrationsPath, localDir, options, subpath) {
292
+ function IsZeroBlock(block) {
293
+ for (let i = 0; i < block.length; i++) {
294
+ if (block[i] !== 0)
295
+ return false;
296
+ }
297
+ return true;
298
+ }
299
+ function ReadNullTerminatedString(buf) {
300
+ const nulIdx = buf.indexOf(0);
301
+ return buf.subarray(0, nulIdx >= 0 ? nulIdx : buf.length).toString('utf-8');
302
+ }
303
+ function ReadUstarPath(header) {
304
+ const name = ReadNullTerminatedString(header.subarray(0, 100));
305
+ const prefix = ReadNullTerminatedString(header.subarray(345, 500));
306
+ return prefix ? `${prefix}/${name}` : name;
307
+ }
308
+ function ParseTarSize(sizeField) {
309
+ if ((sizeField[0] & 0x80) !== 0) {
310
+ let value = 0;
311
+ for (let i = 1; i < sizeField.length; i++) {
312
+ value = value * 256 + sizeField[i];
313
+ }
314
+ if (!Number.isSafeInteger(value) || value < 0) {
315
+ throw new Error(`Corrupt tar archive: invalid binary entry size ${value}`);
316
+ }
317
+ return value;
318
+ }
319
+ const octal = ReadNullTerminatedString(sizeField).trim();
320
+ if (!octal)
321
+ return 0;
322
+ if (!/^[0-7]+$/.test(octal)) {
323
+ throw new Error(`Corrupt tar archive: invalid octal size '${octal}'`);
324
+ }
325
+ const parsed = parseInt(octal, 8);
326
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
327
+ throw new Error(`Corrupt tar archive: invalid entry size ${parsed}`);
328
+ }
329
+ return parsed;
330
+ }
331
+ function ParsePaxHeader(paxBuf) {
332
+ const result = {};
333
+ let pos = 0;
334
+ while (pos < paxBuf.length) {
335
+ const spaceIdx = paxBuf.indexOf(0x20, pos);
336
+ if (spaceIdx < 0)
337
+ break;
338
+ const recordLen = parseInt(paxBuf.subarray(pos, spaceIdx).toString('ascii'), 10);
339
+ if (!Number.isFinite(recordLen) || recordLen <= 0 || pos + recordLen > paxBuf.length)
340
+ break;
341
+ const body = paxBuf.subarray(spaceIdx + 1, pos + recordLen - 1).toString('utf-8');
342
+ const eqIdx = body.indexOf('=');
343
+ if (eqIdx > 0) {
344
+ const key = body.slice(0, eqIdx);
345
+ const val = body.slice(eqIdx + 1);
346
+ if (key === 'path') {
347
+ result.Path = val;
348
+ }
349
+ else if (key === 'size') {
350
+ const trimmedVal = val.trim();
351
+ if (!/^\d+$/.test(trimmedVal)) {
352
+ throw new Error(`Corrupt tar archive: invalid PAX size '${val}'`);
353
+ }
354
+ const parsedSize = parseInt(trimmedVal, 10);
355
+ if (!Number.isSafeInteger(parsedSize) || parsedSize < 0) {
356
+ throw new Error(`Corrupt tar archive: invalid PAX size ${parsedSize}`);
357
+ }
358
+ result.Size = parsedSize;
359
+ }
360
+ }
361
+ pos += recordLen;
362
+ }
363
+ return result;
364
+ }
365
+ function ToTarballBuffer(data) {
366
+ if (Buffer.isBuffer(data))
367
+ return data;
368
+ if (data instanceof ArrayBuffer)
369
+ return Buffer.from(data);
370
+ if (ArrayBuffer.isView(data))
371
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
372
+ if (typeof data === 'string')
373
+ return Buffer.from(data, 'binary');
374
+ throw new Error('Unexpected tarball response data type from GitHub API');
375
+ }
376
+ export function ExtractSqlFilesFromTarGz(tarGzBuffer, cleanPath, localDir) {
377
+ const tarBuffer = gunzipSync(tarGzBuffer, { maxOutputLength: 512 * 1024 * 1024 });
378
+ const normalizedRoot = TrimSlashes(cleanPath);
379
+ const rootPrefix = normalizedRoot ? `${normalizedRoot}/` : '';
380
+ const extractedFiles = [];
381
+ let offset = 0;
382
+ let paxPathOverride;
383
+ let paxSizeOverride;
384
+ let gnuLongPathOverride;
385
+ while (offset + 512 <= tarBuffer.length) {
386
+ const header = tarBuffer.subarray(offset, offset + 512);
387
+ offset += 512;
388
+ if (IsZeroBlock(header)) {
389
+ break;
390
+ }
391
+ const typeFlagByte = header[156];
392
+ const typeFlag = typeFlagByte === 0 ? '0' : String.fromCharCode(typeFlagByte);
393
+ const entrySize = paxSizeOverride ?? ParseTarSize(header.subarray(124, 136));
394
+ if (!Number.isSafeInteger(entrySize) || entrySize < 0) {
395
+ throw new Error(`Corrupt tar archive: invalid entry size ${entrySize}`);
396
+ }
397
+ const dataOffset = offset;
398
+ const paddedSize = Math.ceil(entrySize / 512) * 512;
399
+ offset += paddedSize;
400
+ if (dataOffset + entrySize > tarBuffer.length) {
401
+ throw new Error('Corrupt tar archive: entry extends past end of buffer');
402
+ }
403
+ if (typeFlag === 'x') {
404
+ const parsedPax = ParsePaxHeader(tarBuffer.subarray(dataOffset, dataOffset + entrySize));
405
+ if (parsedPax.Path !== undefined)
406
+ paxPathOverride = parsedPax.Path;
407
+ if (parsedPax.Size !== undefined)
408
+ paxSizeOverride = parsedPax.Size;
409
+ continue;
410
+ }
411
+ if (typeFlag === 'L') {
412
+ gnuLongPathOverride = ReadNullTerminatedString(tarBuffer.subarray(dataOffset, dataOffset + entrySize));
413
+ continue;
414
+ }
415
+ if (typeFlag === 'g' || typeFlag === 'K') {
416
+ continue;
417
+ }
418
+ const rawEntryPath = paxPathOverride ?? gnuLongPathOverride ?? ReadUstarPath(header);
419
+ paxPathOverride = undefined;
420
+ paxSizeOverride = undefined;
421
+ gnuLongPathOverride = undefined;
422
+ if (typeFlag !== '0') {
423
+ continue;
424
+ }
425
+ const firstSlash = rawEntryPath.indexOf('/');
426
+ if (firstSlash < 0) {
427
+ continue;
428
+ }
429
+ const repoRelativePath = rawEntryPath.slice(firstSlash + 1);
430
+ if (!repoRelativePath.startsWith(rootPrefix) || !repoRelativePath.endsWith('.sql')) {
431
+ continue;
432
+ }
433
+ const relativePath = TrimLeadingSlashes(repoRelativePath.slice(rootPrefix.length));
434
+ if (!relativePath) {
435
+ continue;
436
+ }
437
+ if (relativePath.includes('\\')) {
438
+ throw new Error(`Refusing to extract migration path with backslash separator: '${relativePath}'`);
439
+ }
440
+ const segments = relativePath.split('/').filter(Boolean);
441
+ if (segments.some(seg => seg === '..' || seg === '.')) {
442
+ throw new Error(`Refusing to extract migration path with traversal segment: '${relativePath}'`);
443
+ }
444
+ const depth = segments.length - 1;
445
+ if (depth > MAX_MIGRATION_DIRECTORY_DEPTH) {
446
+ throw new Error(`Migration directory nesting exceeds ${MAX_MIGRATION_DIRECTORY_DEPTH} levels under '${normalizedRoot}' — `
447
+ + `refusing to walk further. No real migration layout is this deep.`);
448
+ }
449
+ const target = join(localDir, ...segments);
450
+ const targetDir = dirname(target);
451
+ if (!existsSync(targetDir)) {
452
+ mkdirSync(targetDir, { recursive: true });
453
+ }
454
+ writeFileSync(target, tarBuffer.subarray(dataOffset, dataOffset + entrySize));
455
+ extractedFiles.push(segments.join('/'));
456
+ }
457
+ return extractedFiles;
458
+ }
459
+ export async function DownloadMigrations(repoUrl, version, migrationsPath, localDir, options, subpath, appName) {
180
460
  const parsed = ParseGitHubUrl(repoUrl);
181
461
  if (!parsed) {
182
462
  return { Success: false, ErrorMessage: `Invalid GitHub URL: ${repoUrl}` };
183
463
  }
184
- const effectiveSubpath = (subpath ?? parsed.Subpath)?.replace(/^\/+|\/+$/g, '');
185
- const ref = ResolveRef(version, effectiveSubpath);
464
+ const effectiveSubpath = SubpathOrUndefined(subpath ?? parsed.Subpath);
465
+ const ref = version
466
+ ? (await FindVersionTagName(repoUrl, options, version, effectiveSubpath, appName))
467
+ ?? ResolveRef(version, effectiveSubpath)
468
+ : ResolveRef(version, effectiveSubpath);
186
469
  const cleanPath = ComposeRepoPath(effectiveSubpath, migrationsPath);
187
470
  const octokit = CreateOctokit(repoUrl, options);
188
471
  try {
189
- const sqlFiles = await listSqlFilesRecursive(octokit, parsed.Owner, parsed.Repo, cleanPath, ref);
190
- if (sqlFiles.length === 0) {
472
+ let downloadedFiles;
473
+ const canDownloadTarball = typeof octokit.repos.downloadTarballArchive === 'function';
474
+ if (canDownloadTarball) {
475
+ const tarGzBuffer = await FetchRepoTarball(octokit, repoUrl, parsed, ref, options);
476
+ downloadedFiles = ExtractSqlFilesFromTarGz(tarGzBuffer, cleanPath, localDir);
477
+ }
478
+ else {
479
+ const sqlFiles = await listSqlFilesRecursive(octokit, parsed.Owner, parsed.Repo, cleanPath, ref);
480
+ downloadedFiles = [];
481
+ for (const file of sqlFiles) {
482
+ const content = await FetchFileContent(octokit, parsed.Owner, parsed.Repo, file.path, ref);
483
+ const target = join(localDir, file.relativePath);
484
+ const targetDir = dirname(target);
485
+ if (!existsSync(targetDir)) {
486
+ mkdirSync(targetDir, { recursive: true });
487
+ }
488
+ writeFileSync(target, content, 'utf-8');
489
+ downloadedFiles.push(file.relativePath);
490
+ }
491
+ }
492
+ if (downloadedFiles.length === 0) {
191
493
  return {
192
494
  Success: false,
193
495
  ErrorMessage: `No .sql files found under '${cleanPath}' (ref: ${ref ?? 'default branch'}). `
@@ -195,17 +497,6 @@ export async function DownloadMigrations(repoUrl, version, migrationsPath, local
195
497
  + `check 'migrations.directory' in mj-app.json and that the tag's tree contains it.`,
196
498
  };
197
499
  }
198
- const downloadedFiles = [];
199
- for (const file of sqlFiles) {
200
- const content = await FetchFileContent(octokit, parsed.Owner, parsed.Repo, file.path, ref);
201
- const target = join(localDir, file.relativePath);
202
- const targetDir = dirname(target);
203
- if (!existsSync(targetDir)) {
204
- mkdirSync(targetDir, { recursive: true });
205
- }
206
- writeFileSync(target, content, 'utf-8');
207
- downloadedFiles.push(file.relativePath);
208
- }
209
500
  return { Success: true, LocalPath: localDir, Files: downloadedFiles };
210
501
  }
211
502
  catch (error) {
@@ -213,8 +504,16 @@ export async function DownloadMigrations(repoUrl, version, migrationsPath, local
213
504
  return { Success: false, ErrorMessage: `Failed to download migrations: ${message}` };
214
505
  }
215
506
  }
216
- export async function GetLatestVersion(repoUrl, options, subpath) {
217
- if (!ScopedTagPrefix(subpath ?? ParseGitHubUrl(repoUrl)?.Subpath)) {
507
+ export async function DescribeUnresolvedVersion(repoUrl, options, subpath) {
508
+ const parsed = ParseGitHubUrl(repoUrl);
509
+ if (!parsed) {
510
+ return `Invalid GitHub URL: ${repoUrl}`;
511
+ }
512
+ const where = subpath ? `${parsed.Owner}/${parsed.Repo} (${subpath})` : `${parsed.Owner}/${parsed.Repo}`;
513
+ return DescribeNotFound(repoUrl, parsed, options, () => `Could not determine target version: no version tags or releases found in ${where}`);
514
+ }
515
+ export async function GetLatestVersion(repoUrl, options, subpath, appName) {
516
+ if (ScopedTagCandidates(subpath ?? ParseGitHubUrl(repoUrl)?.Subpath, appName).length === 0) {
218
517
  const releases = await ListGitHubReleases(repoUrl, options);
219
518
  const versioned = releases
220
519
  .filter(r => !r.PreRelease && !r.Draft)
@@ -226,26 +525,52 @@ export async function GetLatestVersion(repoUrl, options, subpath) {
226
525
  return candidates.sort(semver.rcompare)[0];
227
526
  }
228
527
  }
229
- const tags = await ListGitHubTags(repoUrl, options, subpath);
528
+ const tags = await ListGitHubTags(repoUrl, options, subpath, appName);
230
529
  if (tags.length > 0) {
231
530
  const stableTag = tags.find(t => !IsPrereleaseVersion(t));
232
531
  return (stableTag ?? tags[0]).replace(/^v/, '');
233
532
  }
234
533
  return null;
235
534
  }
236
- export async function ListGitHubTags(repoUrl, options, subpath) {
535
+ export async function FindVersionTagName(repoUrl, options, version, subpath, appName) {
536
+ const parsed = ParseGitHubUrl(repoUrl);
537
+ if (!parsed)
538
+ return null;
539
+ const candidates = ScopedTagCandidates(subpath ?? parsed.Subpath, appName);
540
+ if (candidates.length === 0)
541
+ return null;
542
+ const want = version.replace(/^v/, '');
543
+ const pattern = new RegExp(`^(?:@[^/]+/)?(?:${candidates.map(EscapeForRegExp).join('|')})@(.+)$`);
544
+ try {
545
+ for (const name of await FetchRepoTagNames(repoUrl, parsed, options)) {
546
+ const m = name.match(pattern);
547
+ if (m && m[1].replace(/^v/, '') === want)
548
+ return name;
549
+ }
550
+ return null;
551
+ }
552
+ catch (error) {
553
+ ThrowIfRateLimitedOrForbidden(error, 'resolving version tag');
554
+ return null;
555
+ }
556
+ }
557
+ export async function ListGitHubTags(repoUrl, options, subpath, appName) {
237
558
  const parsed = ParseGitHubUrl(repoUrl);
238
559
  if (!parsed) {
239
560
  return [];
240
561
  }
241
- const prefix = ScopedTagPrefix(subpath ?? parsed.Subpath);
562
+ const candidates = ScopedTagCandidates(subpath ?? parsed.Subpath, appName);
563
+ const prefix = candidates.length > 0 ? candidates[0] : undefined;
564
+ const isMultiAppRepo = ScopedTagPrefix(subpath ?? parsed.Subpath) !== undefined;
242
565
  const SEMVER_PATTERN = '\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)*)?';
243
- const pattern = prefix
244
- ? new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@(${SEMVER_PATTERN})$`)
245
- : new RegExp(`^(v?${SEMVER_PATTERN})$`);
566
+ const scopedPattern = prefix
567
+ ? new RegExp(`^(?:@[^/]+/)?(?:${candidates.map(EscapeForRegExp).join('|')})@(${SEMVER_PATTERN})$`)
568
+ : null;
569
+ const repoWidePattern = isMultiAppRepo ? null : new RegExp(`^(v?${SEMVER_PATTERN})$`);
570
+ const matchVersion = (name) => (scopedPattern ? name.match(scopedPattern)?.[1] : undefined) ?? (repoWidePattern ? name.match(repoWidePattern)?.[1] : undefined);
246
571
  try {
247
572
  return (await FetchRepoTagNames(repoUrl, parsed, options))
248
- .map(name => name.match(pattern)?.[1])
573
+ .map(name => matchVersion(name))
249
574
  .filter((v) => v != null)
250
575
  .map(tag => ({ Tag: tag, Core: SemverCore(tag) }))
251
576
  .filter((t) => t.Core !== null)
@@ -260,10 +585,42 @@ export async function ListGitHubTags(repoUrl, options, subpath) {
260
585
  const TAG_CACHE_TTL_MS = 60_000;
261
586
  const tagListCache = new Map();
262
587
  const releaseListCache = new Map();
588
+ const tarballCache = new Map();
589
+ const TARBALL_CACHE_MAX_ENTRIES = 2;
263
590
  const FETCH_CACHE_MAX_ENTRIES = 64;
264
591
  export function ClearGitHubTagCache() {
265
592
  tagListCache.clear();
266
593
  releaseListCache.clear();
594
+ tarballCache.clear();
595
+ }
596
+ async function FetchRepoTarball(octokit, repoUrl, parsed, ref, options) {
597
+ const cacheKey = `${FetchCacheKey(repoUrl, parsed, options)}\u0000${ref}`;
598
+ const now = Date.now();
599
+ const cached = tarballCache.get(cacheKey);
600
+ if (cached && cached.ExpiresAt > now) {
601
+ return await cached.Value;
602
+ }
603
+ for (const [key, entry] of tarballCache) {
604
+ if (entry.ExpiresAt <= now)
605
+ tarballCache.delete(key);
606
+ }
607
+ while (tarballCache.size >= TARBALL_CACHE_MAX_ENTRIES) {
608
+ const oldest = tarballCache.keys().next();
609
+ if (oldest.done)
610
+ break;
611
+ tarballCache.delete(oldest.value);
612
+ }
613
+ const inFlight = (async () => {
614
+ const response = await octokit.repos.downloadTarballArchive({
615
+ owner: parsed.Owner,
616
+ repo: parsed.Repo,
617
+ ref,
618
+ });
619
+ return ToTarballBuffer(response.data);
620
+ })();
621
+ tarballCache.set(cacheKey, { ExpiresAt: now + TAG_CACHE_TTL_MS, Value: inFlight });
622
+ inFlight.catch(() => { tarballCache.delete(cacheKey); });
623
+ return await inFlight;
267
624
  }
268
625
  async function MemoizedFetch(cache, cacheKey, fetcher) {
269
626
  const now = Date.now();
@@ -296,19 +653,23 @@ async function FetchRepoTagNames(repoUrl, parsed, options) {
296
653
  return data.map(t => t.name);
297
654
  });
298
655
  }
299
- export async function ValidateGitHubTag(repoUrl, version, options, subpath) {
656
+ export async function ValidateGitHubTag(repoUrl, version, options, subpath, appName) {
300
657
  const parsed = ParseGitHubUrl(repoUrl);
301
658
  if (!parsed) {
302
659
  return { Exists: false, ErrorMessage: `Invalid GitHub URL: ${repoUrl}` };
303
660
  }
304
- const tag = ResolveRef(version, subpath ?? parsed.Subpath);
661
+ const tag = (await FindVersionTagName(repoUrl, options, version, subpath ?? parsed.Subpath, appName))
662
+ ?? ResolveRef(version, subpath ?? parsed.Subpath);
305
663
  try {
306
664
  await CreateOctokit(repoUrl, options).git.getRef({ owner: parsed.Owner, repo: parsed.Repo, ref: `tags/${tag}` });
307
665
  return { Exists: true };
308
666
  }
309
667
  catch (error) {
310
668
  if (OctokitStatus(error) === 404) {
311
- return { Exists: false, ErrorMessage: `Tag '${tag}' not found in ${parsed.Owner}/${parsed.Repo}. Available versions can be checked at ${repoUrl}/tags` };
669
+ return {
670
+ Exists: false,
671
+ ErrorMessage: await DescribeNotFound(repoUrl, parsed, options, () => `Tag '${tag}' not found in ${parsed.Owner}/${parsed.Repo}. Available versions can be checked at https://github.com/${parsed.Owner}/${parsed.Repo}/tags`),
672
+ };
312
673
  }
313
674
  const message = error instanceof Error ? error.message : String(error);
314
675
  return { Exists: false, ErrorMessage: `Failed to validate tag '${tag}': ${message}` };