browser-git-ops 0.0.7 → 0.0.8

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/dist/index.mjs CHANGED
@@ -33,9 +33,8 @@ function buildGithubMeta(parsed, hostname, segments, resolvedToken, branchParame
33
33
  const owner = segments[0] || "";
34
34
  const repo = segments[1] || "";
35
35
  if (!owner || !repo) throw new Error("invalid repository path");
36
- const options = { owner, repo };
36
+ const options = { owner, repo, branch: branchParameter || "main" };
37
37
  if (resolvedToken) options.token = resolvedToken;
38
- options.branch = branchParameter || "main";
39
38
  if (!/github\.com$/i.test(hostname)) {
40
39
  options.host = `${parsed.protocol}//${parsed.host}/api/v3`;
41
40
  }
@@ -44,14 +43,39 @@ function buildGithubMeta(parsed, hostname, segments, resolvedToken, branchParame
44
43
  function buildGitlabMeta(parsed, hostname, segments, resolvedToken, branchParameter) {
45
44
  const projectId = segments.join("/");
46
45
  if (!projectId) throw new Error("invalid repository path");
47
- const options = { projectId };
46
+ const options = { projectId, branch: branchParameter || "main" };
48
47
  if (resolvedToken) options.token = resolvedToken;
49
- options.branch = branchParameter || "main";
50
48
  if (!/gitlab\.com$/i.test(hostname)) {
51
49
  options.host = `${parsed.protocol}//${parsed.host}`;
52
50
  }
53
51
  return { type: "gitlab", opts: options };
54
52
  }
53
+ function buildUrlFromAdapterOptions(type, options) {
54
+ if (type === "github") return buildGithubUrl(options);
55
+ if (type === "gitlab") return buildGitlabUrl(options);
56
+ throw new Error(`unsupported adapter type: ${type}`);
57
+ }
58
+ function buildGithubUrl(options) {
59
+ const owner = options.owner || "";
60
+ const repo = options.repo || "";
61
+ if (!owner || !repo) throw new Error("owner and repo are required for github");
62
+ const host = options.host;
63
+ if (host) {
64
+ const baseUrl = host.replace(/\/api\/v\d+\/?$/i, "");
65
+ return `${baseUrl}/${owner}/${repo}`;
66
+ }
67
+ return `https://github.com/${owner}/${repo}`;
68
+ }
69
+ function buildGitlabUrl(options) {
70
+ const projectId = options.projectId || "";
71
+ if (!projectId) throw new Error("projectId is required for gitlab");
72
+ const host = options.host;
73
+ if (host) {
74
+ const trimmed = host.replace(/\/+$/, "");
75
+ return `${trimmed}/${projectId}`;
76
+ }
77
+ return `https://gitlab.com/${projectId}`;
78
+ }
55
79
 
56
80
  // src/virtualfs/opfsStorage.ts
57
81
  var ERR_OPFS_DIR_API = "OPFS directory API not available";
@@ -1017,6 +1041,10 @@ var NonRetryableError = class extends Error {
1017
1041
  var RetryExhaustedError = class extends RetryableError {
1018
1042
  code = "RETRY_EXHAUSTED";
1019
1043
  cause;
1044
+ /**
1045
+ * @param {string} message error message
1046
+ * @param {any} [cause] original cause
1047
+ */
1020
1048
  constructor(message, cause) {
1021
1049
  super(message);
1022
1050
  this.name = "RetryExhaustedError";
@@ -1250,6 +1278,7 @@ var AbstractGitAdapter = class {
1250
1278
  * @param items items to map
1251
1279
  * @param mapper async mapper
1252
1280
  * @param concurrency concurrency limit
1281
+ * @param [options] options with optional controller
1253
1282
  * @returns Promise resolving to mapped results
1254
1283
  */
1255
1284
  mapWithConcurrency(items, mapper, concurrency = 5, options) {
@@ -1778,6 +1807,9 @@ var GitHubAdapter = class extends abstractAdapter_default {
1778
1807
  async _buildFileMapFromHead(headSha) {
1779
1808
  const treeResponse = await this._fetchWithRetry(`${this.baseUrl}/git/trees/${headSha}${"?recursive=1"}`, { method: "GET", headers: this.headers }, 4, 300);
1780
1809
  const treeJ = await treeResponse.json();
1810
+ if (treeJ && treeJ.truncated === true) {
1811
+ this.logWarn("GitHub tree response was truncated. Some files may be missing. Consider using non-recursive tree fetching for large repositories.");
1812
+ }
1781
1813
  const files = treeJ && treeJ.tree ? treeJ.tree.filter((t) => t.type === "blob") : [];
1782
1814
  const shas = {};
1783
1815
  const fileMap = /* @__PURE__ */ new Map();
@@ -2072,6 +2104,12 @@ var GitLabAdapter = class extends abstractAdapter_default {
2072
2104
  this._handleCreateBranchError(message, branchName);
2073
2105
  }
2074
2106
  }
2107
+ /**
2108
+ * Extract SHA from commit response text.
2109
+ * @param {string} text response text
2110
+ * @param {string} fallback fallback SHA value
2111
+ * @returns {string} extracted SHA or fallback
2112
+ */
2075
2113
  _extractShaFromCommitResponseText(text, fallback) {
2076
2114
  try {
2077
2115
  const data = text ? JSON.parse(text) : {};
@@ -2224,6 +2262,11 @@ var GitLabAdapter = class extends abstractAdapter_default {
2224
2262
  const remote = await this._safeGetBranchHead(branch);
2225
2263
  return remote ?? branch;
2226
2264
  }
2265
+ /**
2266
+ * Safely fetch branch head SHA, returning null on failure.
2267
+ * @param {string} branch branch name
2268
+ * @returns {Promise<string|null>} head SHA or null
2269
+ */
2227
2270
  async _safeGetBranchHead(branch) {
2228
2271
  try {
2229
2272
  const branchResponse = await this.fetchWithRetry(`${this.baseUrl}/repository/branches/${encodeURIComponent(branch)}`, { method: "GET", headers: this.headers });
@@ -2239,14 +2282,25 @@ var GitLabAdapter = class extends abstractAdapter_default {
2239
2282
  }
2240
2283
  /**
2241
2284
  * Fetch repository tree and build shas map and fileSet.
2285
+ * Paginates through all pages using offset-based pagination (per_page=100).
2242
2286
  * @param {string} branch branch name
2243
2287
  * @returns {Promise<{shas:Record<string,string>,fileSet:Set<string>}>}
2244
2288
  */
2245
2289
  async _fetchTreeAndBuildShas(branch) {
2246
- const treeResponse = await this.fetchWithRetry(`${this.baseUrl}/repository/tree?recursive=true&ref=${encodeURIComponent(branch)}`, { method: "GET", headers: this.headers });
2247
- const treeJ = await treeResponse.json();
2248
- const files = Array.isArray(treeJ) ? treeJ.filter((t) => t.type === "blob") : [];
2249
- return this._buildShasAndFileSet(files);
2290
+ const allFiles = [];
2291
+ let page = 1;
2292
+ const perPage = 100;
2293
+ while (true) {
2294
+ const url = `${this.baseUrl}/repository/tree?recursive=true&ref=${encodeURIComponent(branch)}&per_page=${perPage}&page=${page}`;
2295
+ const treeResponse = await this.fetchWithRetry(url, { method: "GET", headers: this.headers });
2296
+ const treeJ = await treeResponse.json();
2297
+ const entries = Array.isArray(treeJ) ? treeJ : [];
2298
+ allFiles.push(...entries.filter((t) => t.type === "blob"));
2299
+ const paging = this._parsePagingHeaders(treeResponse);
2300
+ if (!paging.nextPage) break;
2301
+ page = paging.nextPage;
2302
+ }
2303
+ return this._buildShasAndFileSet(allFiles);
2250
2304
  }
2251
2305
  /**
2252
2306
  * Fetch contents for requested paths from a FileSet with caching.
@@ -2278,6 +2332,7 @@ var GitLabAdapter = class extends abstractAdapter_default {
2278
2332
  * @param {Record<string,string>} snapshot snapshot map
2279
2333
  * @param {string} p file path
2280
2334
  * @param {string} branch branch
2335
+ * @param {AbortSignal} [signal] optional abort signal
2281
2336
  * @returns {Promise<string|null>} file content or null
2282
2337
  */
2283
2338
  async _fetchFileContentForPath(cache, snapshot, p, branch, signal) {
@@ -2298,6 +2353,7 @@ var GitLabAdapter = class extends abstractAdapter_default {
2298
2353
  * Fetch raw file content from GitLab; return null on failure.
2299
2354
  * @param {string} path file path
2300
2355
  * @param {string} branch branch name
2356
+ * @param {AbortSignal} [signal] optional abort signal
2301
2357
  * @returns {Promise<string|null>} file content or null
2302
2358
  */
2303
2359
  async _fetchFileRaw(path, branch, signal) {
@@ -2363,6 +2419,12 @@ var GitLabAdapter = class extends abstractAdapter_default {
2363
2419
  }
2364
2420
  return null;
2365
2421
  }
2422
+ /**
2423
+ * Try running a single resolver, returning null on failure.
2424
+ * @param {Function} r resolver function
2425
+ * @param {string} reference commit-ish to resolve
2426
+ * @returns {Promise<string|null>} resolved sha or null
2427
+ */
2366
2428
  async _tryRunResolver(r, reference) {
2367
2429
  try {
2368
2430
  return await r(reference);
@@ -3700,47 +3762,111 @@ var VirtualFS = class {
3700
3762
  * Set adapter instance and persist adapter metadata into index file.
3701
3763
  * Supports overloads:
3702
3764
  * - setAdapter(meta: AdapterMeta)
3703
- * - setAdapter(type: string, url: string, token?: string)
3704
- * - setAdapter(url: string)
3705
- * @param {AdapterMeta|string} metaOrTypeOrUrl
3765
+ * - setAdapter(type: string, url: string, branch?: string, token?: string)
3766
+ * - setAdapter(url: string, branch?: string, token?: string)
3767
+ * @param {AdapterMeta|string} metaOrTypeOrUrl
3706
3768
  * @returns {Promise<void>}
3707
3769
  */
3708
3770
  async setAdapter(metaOrTypeOrUrl) {
3709
- const urlOrUndefined = arguments[1];
3710
- const tokenOrUndefined = arguments[2];
3711
- const meta = await this._parseAdapterArgs(metaOrTypeOrUrl, urlOrUndefined, tokenOrUndefined);
3712
- if (!meta || typeof meta.type !== "string" || meta.opts == null && meta.options == null) throw new Error("Adapter meta is required");
3713
- const normalized = { type: meta.type, opts: meta.opts || meta.options };
3714
- this.adapterMeta = normalized;
3771
+ const argument1 = arguments[1];
3772
+ const argument2 = arguments[2];
3773
+ const argument3 = arguments[3];
3774
+ const meta = this._parseAdapterArgs(metaOrTypeOrUrl, argument1, argument2, argument3);
3775
+ if (!meta || typeof meta.type !== "string") throw new Error("Adapter meta is required");
3776
+ this.adapterMeta = meta;
3715
3777
  await this._tryPersistAdapterMeta();
3716
3778
  }
3717
3779
  /**
3718
- * Parse arguments for `setAdapter` and return normalized meta object.
3719
- * Accepts AdapterMeta, (type, url, token?), or (url).
3780
+ * Parse arguments for `setAdapter` and return a fully normalized AdapterMeta.
3781
+ * The result always has {type, url, branch, token, opts} at the top level.
3782
+ * Accepts AdapterMeta, (type, url, branch?, token?), or (url, branch?, token?).
3720
3783
  * @param metaOrTypeOrUrl AdapterMeta or type or url
3721
- * @param urlOrUndefined optional url when first arg is type
3722
- * @param tokenOrUndefined optional token
3723
- * @returns normalized meta object
3784
+ * @param argument1 url (when first is type) OR branch (when first is url) OR undefined
3785
+ * @param argument2 branch (when first is type) OR token (when first is url) OR undefined
3786
+ * @param argument3 token (when first is type) OR undefined
3787
+ * @returns normalized AdapterMeta
3724
3788
  */
3725
- async _parseAdapterArgs(metaOrTypeOrUrl, urlOrUndefined, tokenOrUndefined) {
3726
- if (typeof metaOrTypeOrUrl === "object" && metaOrTypeOrUrl !== null && !urlOrUndefined) return metaOrTypeOrUrl;
3727
- if (typeof metaOrTypeOrUrl === "string" && typeof urlOrUndefined === "string") {
3728
- try {
3729
- const parsed = parseAdapterFromUrl(urlOrUndefined, tokenOrUndefined, metaOrTypeOrUrl);
3730
- return { type: parsed.type, opts: parsed.opts };
3731
- } catch (error) {
3732
- throw error;
3733
- }
3789
+ _parseAdapterArgs(metaOrTypeOrUrl, argument1, argument2, argument3) {
3790
+ if (typeof metaOrTypeOrUrl === "object" && metaOrTypeOrUrl !== null) {
3791
+ return this._normalizeFromMeta(metaOrTypeOrUrl);
3734
3792
  }
3735
- if (typeof metaOrTypeOrUrl === "string" && !urlOrUndefined) {
3793
+ const firstArgument = metaOrTypeOrUrl;
3794
+ const isTypeUrlForm = typeof argument1 === "string" && /^https?:\/\//i.test(argument1);
3795
+ if (isTypeUrlForm) {
3796
+ return this._normalizeFromTypeUrl(firstArgument, argument1, argument2, argument3);
3797
+ }
3798
+ return this._normalizeFromUrl(firstArgument, argument1, argument2);
3799
+ }
3800
+ /**
3801
+ * Normalize from AdapterMeta object – generate url from opts if missing.
3802
+ * @param meta raw AdapterMeta input
3803
+ * @returns fully normalized AdapterMeta
3804
+ */
3805
+ _normalizeFromMeta(meta) {
3806
+ const type = meta.type;
3807
+ const rawOptions = meta.opts || meta.options || {};
3808
+ const options = this._stripOptionsFields(rawOptions);
3809
+ let url = meta.url;
3810
+ if (!url) {
3736
3811
  try {
3737
- const parsed = parseAdapterFromUrl(metaOrTypeOrUrl);
3738
- return { type: parsed.type, opts: parsed.opts };
3739
- } catch (error) {
3740
- throw error;
3812
+ url = buildUrlFromAdapterOptions(type, options);
3813
+ } catch {
3814
+ url = void 0;
3741
3815
  }
3742
3816
  }
3743
- throw new Error("Adapter meta is required");
3817
+ const branch = meta.branch || rawOptions.branch || "main";
3818
+ const token = meta.token || rawOptions.token || void 0;
3819
+ return { type, url, branch, token, opts: options };
3820
+ }
3821
+ /**
3822
+ * Normalize from (type, url, branch?, token?) arguments.
3823
+ * @param type adapter type
3824
+ * @param url repository url
3825
+ * @param branch optional branch (defaults to 'main')
3826
+ * @param token optional token
3827
+ * @returns fully normalized AdapterMeta
3828
+ */
3829
+ _normalizeFromTypeUrl(type, url, branch, token) {
3830
+ const parsed = parseAdapterFromUrl(url, token, type);
3831
+ const options = this._stripOptionsFields(parsed.opts || {});
3832
+ return { type: parsed.type, url, branch: branch || "main", token, opts: options };
3833
+ }
3834
+ /**
3835
+ * Normalize from (url, branch?, token?) arguments.
3836
+ * @param url repository url
3837
+ * @param branch optional branch (defaults to 'main')
3838
+ * @param token optional token
3839
+ * @returns fully normalized AdapterMeta
3840
+ */
3841
+ _normalizeFromUrl(url, branch, token) {
3842
+ const parsed = parseAdapterFromUrl(url, token);
3843
+ const options = this._stripOptionsFields(parsed.opts || {});
3844
+ return { type: parsed.type, url, branch: branch || "main", token, opts: options };
3845
+ }
3846
+ /**
3847
+ * Strip branch/token from options to avoid duplication (they live at the top level).
3848
+ * Returns a new object with only host, owner, repo, projectId, etc.
3849
+ * @param options raw adapter options
3850
+ * @returns cleaned options without branch/token
3851
+ */
3852
+ _stripOptionsFields(options) {
3853
+ if (!options || typeof options !== "object") return {};
3854
+ const cleaned = { ...options };
3855
+ delete cleaned.branch;
3856
+ delete cleaned.token;
3857
+ delete cleaned.defaultBranch;
3858
+ delete cleaned.repositoryName;
3859
+ delete cleaned.repositoryId;
3860
+ return cleaned;
3861
+ }
3862
+ /**
3863
+ * Return the persisted branch name from adapterMeta (top-level or opts fallback).
3864
+ * Defaults to 'main' when not found.
3865
+ * @returns {string} persisted branch name
3866
+ */
3867
+ _getPersistedBranch() {
3868
+ if (!this.adapterMeta) return "main";
3869
+ return this.adapterMeta.branch || this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
3744
3870
  }
3745
3871
  /**
3746
3872
  * Try to inject the configured logger into the adapter instance (best-effort).
@@ -3827,6 +3953,9 @@ var VirtualFS = class {
3827
3953
  _instantiateAdapter(type, options) {
3828
3954
  try {
3829
3955
  const optionsWithLogger = { ...options || {} };
3956
+ if (this.adapterMeta && this.adapterMeta.token && !optionsWithLogger.token) {
3957
+ optionsWithLogger.token = this.adapterMeta.token;
3958
+ }
3830
3959
  if (this.logger) optionsWithLogger.logger = this.logger;
3831
3960
  if (type === "github") return new GitHubAdapter(optionsWithLogger);
3832
3961
  if (type === "gitlab") return new GitLabAdapter(optionsWithLogger);
@@ -3913,8 +4042,9 @@ var VirtualFS = class {
3913
4042
  * @returns {void}
3914
4043
  */
3915
4044
  _populateCommitShaFromMeta(stats) {
3916
- if (!stats.gitCommitSha && this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch) {
3917
- stats.gitCommitSha = this.adapterMeta.opts.branch;
4045
+ const branch = this._getPersistedBranch();
4046
+ if (!stats.gitCommitSha && branch && branch !== "main") {
4047
+ stats.gitCommitSha = branch;
3918
4048
  }
3919
4049
  }
3920
4050
  /**
@@ -3927,7 +4057,7 @@ var VirtualFS = class {
3927
4057
  if (!instAdapter || stats.gitCommitSha) return;
3928
4058
  if (typeof instAdapter.resolveRef !== "function") return;
3929
4059
  try {
3930
- const branch = this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
4060
+ const branch = this._getPersistedBranch();
3931
4061
  const resolved = await instAdapter.resolveRef(branch);
3932
4062
  if (resolved) stats.gitCommitSha = resolved;
3933
4063
  } catch (error) {
@@ -4479,7 +4609,7 @@ var VirtualFS = class {
4479
4609
  * @returns {Promise<{commitSha:string}>}
4480
4610
  */
4481
4611
  async _handlePushWithAdapter(input, adapter) {
4482
- const branch = input.ref || this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
4612
+ const branch = input.ref || this._getPersistedBranch();
4483
4613
  const messageWithKey = `${input.message}
4484
4614
 
4485
4615
  apigit-commit-key:${input.commitKey}`;
@@ -4553,13 +4683,13 @@ apigit-commit-key:${input.commitKey}`;
4553
4683
  return { ...pullResult, remote: normalized, remotePaths: Object.keys(normalized.shas || {}) };
4554
4684
  }
4555
4685
  /**
4556
- * Pull using the persisted adapterMeta.opts.branch (or 'main').
4686
+ * Pull using the persisted adapterMeta.branch (or 'main').
4557
4687
  * @param {Record<string,string>=} baseSnapshot optional base snapshot
4558
4688
  * @returns {Promise<any>} pull result
4559
4689
  */
4560
4690
  async _pullUsingPersistedBranch(baseSnapshot) {
4561
4691
  const instAdapter = await this.getAdapterInstance();
4562
- const branch = this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
4692
+ const branch = this._getPersistedBranch();
4563
4693
  await this._trySetBackendBranch(branch);
4564
4694
  const resolvedSha = await instAdapter.resolveRef(branch);
4565
4695
  const descriptor = await instAdapter.fetchSnapshot(resolvedSha);
@@ -4592,9 +4722,9 @@ apigit-commit-key:${input.commitKey}`;
4592
4722
  * @returns {Promise<void>}
4593
4723
  */
4594
4724
  async _persistAdapterBranchMeta(branch, _adapterInstance) {
4595
- const meta = this.adapterMeta && this.adapterMeta.opts ? { ...this.adapterMeta } : await this.getAdapter();
4725
+ const meta = this.adapterMeta ? { ...this.adapterMeta } : await this.getAdapter();
4596
4726
  if (!meta) return;
4597
- const newMeta = { ...meta || {}, opts: { ...meta.opts || {}, branch } };
4727
+ const newMeta = { ...meta || {}, branch, opts: { ...meta.opts || {} } };
4598
4728
  await this.setAdapter(newMeta);
4599
4729
  await this._trySetBackendBranch(branch);
4600
4730
  }
@@ -4762,7 +4892,7 @@ apigit-commit-key:${input.commitKey}`;
4762
4892
  async _resolveAdapterDefaultBranch(instAdapter) {
4763
4893
  if (this.adapterMeta && this.adapterMeta.opts && typeof instAdapter.resolveRef === "function") {
4764
4894
  try {
4765
- const defaultBranch = this.adapterMeta.opts.branch || "main";
4895
+ const defaultBranch = this._getPersistedBranch();
4766
4896
  const resolved = await instAdapter.resolveRef(defaultBranch);
4767
4897
  return resolved || null;
4768
4898
  } catch (error) {
@@ -4800,11 +4930,12 @@ apigit-commit-key:${input.commitKey}`;
4800
4930
  try {
4801
4931
  const have = await this._loadAdapterMetaIfNeeded();
4802
4932
  if (!have) return;
4803
- const options = this.adapterMeta && this.adapterMeta.opts || {};
4933
+ const existing = this.adapterMeta && this.adapterMeta.opts;
4934
+ const options = existing ? { ...existing } : {};
4804
4935
  options.defaultBranch = md.defaultBranch;
4805
4936
  if (md.name) options.repositoryName = md.name;
4806
4937
  if (md.id !== void 0) options.repositoryId = md.id;
4807
- this.adapterMeta.opts = options;
4938
+ this.adapterMeta.opts = { ...existing, ...options };
4808
4939
  await this._writeAdapterMetaToIndex();
4809
4940
  } catch (error) {
4810
4941
  if (typeof console !== "undefined" && console.debug) console.debug("persist repository metadata aborted", error);
@@ -4889,7 +5020,7 @@ apigit-commit-key:${input.commitKey}`;
4889
5020
  async _fetchSnapshotFromAdapterInstance() {
4890
5021
  const adapterInstance = await this.getAdapterInstance();
4891
5022
  if (adapterInstance && typeof adapterInstance.fetchSnapshot === "function") {
4892
- const branch = this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
5023
+ const branch = this._getPersistedBranch();
4893
5024
  return await adapterInstance.fetchSnapshot(branch);
4894
5025
  }
4895
5026
  return null;
@@ -5237,7 +5368,8 @@ var IndexedDatabaseStorage = class IndexedDatabaseStorage2 {
5237
5368
  if (meta.lastCommitKey) result.lastCommitKey = meta.lastCommitKey;
5238
5369
  if (meta.adapter) result.adapter = meta.adapter;
5239
5370
  try {
5240
- this.currentBranch = meta.adapter && meta.adapter.opts && meta.adapter.opts.branch ? meta.adapter.opts.branch : null;
5371
+ const adp = meta.adapter;
5372
+ this.currentBranch = adp && (adp.branch || adp.opts && adp.opts.branch) ? adp.branch || adp.opts.branch : null;
5241
5373
  } catch {
5242
5374
  this.currentBranch = null;
5243
5375
  }