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.js CHANGED
@@ -65,9 +65,8 @@ var APIGitLib = (() => {
65
65
  const owner = segments[0] || "";
66
66
  const repo = segments[1] || "";
67
67
  if (!owner || !repo) throw new Error("invalid repository path");
68
- const options = { owner, repo };
68
+ const options = { owner, repo, branch: branchParameter || "main" };
69
69
  if (resolvedToken) options.token = resolvedToken;
70
- options.branch = branchParameter || "main";
71
70
  if (!/github\.com$/i.test(hostname)) {
72
71
  options.host = `${parsed.protocol}//${parsed.host}/api/v3`;
73
72
  }
@@ -76,14 +75,39 @@ var APIGitLib = (() => {
76
75
  function buildGitlabMeta(parsed, hostname, segments, resolvedToken, branchParameter) {
77
76
  const projectId = segments.join("/");
78
77
  if (!projectId) throw new Error("invalid repository path");
79
- const options = { projectId };
78
+ const options = { projectId, branch: branchParameter || "main" };
80
79
  if (resolvedToken) options.token = resolvedToken;
81
- options.branch = branchParameter || "main";
82
80
  if (!/gitlab\.com$/i.test(hostname)) {
83
81
  options.host = `${parsed.protocol}//${parsed.host}`;
84
82
  }
85
83
  return { type: "gitlab", opts: options };
86
84
  }
85
+ function buildUrlFromAdapterOptions(type, options) {
86
+ if (type === "github") return buildGithubUrl(options);
87
+ if (type === "gitlab") return buildGitlabUrl(options);
88
+ throw new Error(`unsupported adapter type: ${type}`);
89
+ }
90
+ function buildGithubUrl(options) {
91
+ const owner = options.owner || "";
92
+ const repo = options.repo || "";
93
+ if (!owner || !repo) throw new Error("owner and repo are required for github");
94
+ const host = options.host;
95
+ if (host) {
96
+ const baseUrl = host.replace(/\/api\/v\d+\/?$/i, "");
97
+ return `${baseUrl}/${owner}/${repo}`;
98
+ }
99
+ return `https://github.com/${owner}/${repo}`;
100
+ }
101
+ function buildGitlabUrl(options) {
102
+ const projectId = options.projectId || "";
103
+ if (!projectId) throw new Error("projectId is required for gitlab");
104
+ const host = options.host;
105
+ if (host) {
106
+ const trimmed = host.replace(/\/+$/, "");
107
+ return `${trimmed}/${projectId}`;
108
+ }
109
+ return `https://gitlab.com/${projectId}`;
110
+ }
87
111
 
88
112
  // src/virtualfs/opfsStorage.ts
89
113
  var ERR_OPFS_DIR_API = "OPFS directory API not available";
@@ -1049,6 +1073,10 @@ var APIGitLib = (() => {
1049
1073
  var RetryExhaustedError = class extends RetryableError {
1050
1074
  code = "RETRY_EXHAUSTED";
1051
1075
  cause;
1076
+ /**
1077
+ * @param {string} message error message
1078
+ * @param {any} [cause] original cause
1079
+ */
1052
1080
  constructor(message, cause) {
1053
1081
  super(message);
1054
1082
  this.name = "RetryExhaustedError";
@@ -1282,6 +1310,7 @@ var APIGitLib = (() => {
1282
1310
  * @param items items to map
1283
1311
  * @param mapper async mapper
1284
1312
  * @param concurrency concurrency limit
1313
+ * @param [options] options with optional controller
1285
1314
  * @returns Promise resolving to mapped results
1286
1315
  */
1287
1316
  mapWithConcurrency(items, mapper, concurrency = 5, options) {
@@ -1810,6 +1839,9 @@ var APIGitLib = (() => {
1810
1839
  async _buildFileMapFromHead(headSha) {
1811
1840
  const treeResponse = await this._fetchWithRetry(`${this.baseUrl}/git/trees/${headSha}${"?recursive=1"}`, { method: "GET", headers: this.headers }, 4, 300);
1812
1841
  const treeJ = await treeResponse.json();
1842
+ if (treeJ && treeJ.truncated === true) {
1843
+ this.logWarn("GitHub tree response was truncated. Some files may be missing. Consider using non-recursive tree fetching for large repositories.");
1844
+ }
1813
1845
  const files = treeJ && treeJ.tree ? treeJ.tree.filter((t) => t.type === "blob") : [];
1814
1846
  const shas = {};
1815
1847
  const fileMap = /* @__PURE__ */ new Map();
@@ -2104,6 +2136,12 @@ var APIGitLib = (() => {
2104
2136
  this._handleCreateBranchError(message, branchName);
2105
2137
  }
2106
2138
  }
2139
+ /**
2140
+ * Extract SHA from commit response text.
2141
+ * @param {string} text response text
2142
+ * @param {string} fallback fallback SHA value
2143
+ * @returns {string} extracted SHA or fallback
2144
+ */
2107
2145
  _extractShaFromCommitResponseText(text, fallback) {
2108
2146
  try {
2109
2147
  const data = text ? JSON.parse(text) : {};
@@ -2256,6 +2294,11 @@ var APIGitLib = (() => {
2256
2294
  const remote = await this._safeGetBranchHead(branch);
2257
2295
  return remote ?? branch;
2258
2296
  }
2297
+ /**
2298
+ * Safely fetch branch head SHA, returning null on failure.
2299
+ * @param {string} branch branch name
2300
+ * @returns {Promise<string|null>} head SHA or null
2301
+ */
2259
2302
  async _safeGetBranchHead(branch) {
2260
2303
  try {
2261
2304
  const branchResponse = await this.fetchWithRetry(`${this.baseUrl}/repository/branches/${encodeURIComponent(branch)}`, { method: "GET", headers: this.headers });
@@ -2271,14 +2314,25 @@ var APIGitLib = (() => {
2271
2314
  }
2272
2315
  /**
2273
2316
  * Fetch repository tree and build shas map and fileSet.
2317
+ * Paginates through all pages using offset-based pagination (per_page=100).
2274
2318
  * @param {string} branch branch name
2275
2319
  * @returns {Promise<{shas:Record<string,string>,fileSet:Set<string>}>}
2276
2320
  */
2277
2321
  async _fetchTreeAndBuildShas(branch) {
2278
- const treeResponse = await this.fetchWithRetry(`${this.baseUrl}/repository/tree?recursive=true&ref=${encodeURIComponent(branch)}`, { method: "GET", headers: this.headers });
2279
- const treeJ = await treeResponse.json();
2280
- const files = Array.isArray(treeJ) ? treeJ.filter((t) => t.type === "blob") : [];
2281
- return this._buildShasAndFileSet(files);
2322
+ const allFiles = [];
2323
+ let page = 1;
2324
+ const perPage = 100;
2325
+ while (true) {
2326
+ const url = `${this.baseUrl}/repository/tree?recursive=true&ref=${encodeURIComponent(branch)}&per_page=${perPage}&page=${page}`;
2327
+ const treeResponse = await this.fetchWithRetry(url, { method: "GET", headers: this.headers });
2328
+ const treeJ = await treeResponse.json();
2329
+ const entries = Array.isArray(treeJ) ? treeJ : [];
2330
+ allFiles.push(...entries.filter((t) => t.type === "blob"));
2331
+ const paging = this._parsePagingHeaders(treeResponse);
2332
+ if (!paging.nextPage) break;
2333
+ page = paging.nextPage;
2334
+ }
2335
+ return this._buildShasAndFileSet(allFiles);
2282
2336
  }
2283
2337
  /**
2284
2338
  * Fetch contents for requested paths from a FileSet with caching.
@@ -2310,6 +2364,7 @@ var APIGitLib = (() => {
2310
2364
  * @param {Record<string,string>} snapshot snapshot map
2311
2365
  * @param {string} p file path
2312
2366
  * @param {string} branch branch
2367
+ * @param {AbortSignal} [signal] optional abort signal
2313
2368
  * @returns {Promise<string|null>} file content or null
2314
2369
  */
2315
2370
  async _fetchFileContentForPath(cache, snapshot, p, branch, signal) {
@@ -2330,6 +2385,7 @@ var APIGitLib = (() => {
2330
2385
  * Fetch raw file content from GitLab; return null on failure.
2331
2386
  * @param {string} path file path
2332
2387
  * @param {string} branch branch name
2388
+ * @param {AbortSignal} [signal] optional abort signal
2333
2389
  * @returns {Promise<string|null>} file content or null
2334
2390
  */
2335
2391
  async _fetchFileRaw(path, branch, signal) {
@@ -2395,6 +2451,12 @@ var APIGitLib = (() => {
2395
2451
  }
2396
2452
  return null;
2397
2453
  }
2454
+ /**
2455
+ * Try running a single resolver, returning null on failure.
2456
+ * @param {Function} r resolver function
2457
+ * @param {string} reference commit-ish to resolve
2458
+ * @returns {Promise<string|null>} resolved sha or null
2459
+ */
2398
2460
  async _tryRunResolver(r, reference) {
2399
2461
  try {
2400
2462
  return await r(reference);
@@ -3732,47 +3794,111 @@ var APIGitLib = (() => {
3732
3794
  * Set adapter instance and persist adapter metadata into index file.
3733
3795
  * Supports overloads:
3734
3796
  * - setAdapter(meta: AdapterMeta)
3735
- * - setAdapter(type: string, url: string, token?: string)
3736
- * - setAdapter(url: string)
3737
- * @param {AdapterMeta|string} metaOrTypeOrUrl
3797
+ * - setAdapter(type: string, url: string, branch?: string, token?: string)
3798
+ * - setAdapter(url: string, branch?: string, token?: string)
3799
+ * @param {AdapterMeta|string} metaOrTypeOrUrl
3738
3800
  * @returns {Promise<void>}
3739
3801
  */
3740
3802
  async setAdapter(metaOrTypeOrUrl) {
3741
- const urlOrUndefined = arguments[1];
3742
- const tokenOrUndefined = arguments[2];
3743
- const meta = await this._parseAdapterArgs(metaOrTypeOrUrl, urlOrUndefined, tokenOrUndefined);
3744
- if (!meta || typeof meta.type !== "string" || meta.opts == null && meta.options == null) throw new Error("Adapter meta is required");
3745
- const normalized = { type: meta.type, opts: meta.opts || meta.options };
3746
- this.adapterMeta = normalized;
3803
+ const argument1 = arguments[1];
3804
+ const argument2 = arguments[2];
3805
+ const argument3 = arguments[3];
3806
+ const meta = this._parseAdapterArgs(metaOrTypeOrUrl, argument1, argument2, argument3);
3807
+ if (!meta || typeof meta.type !== "string") throw new Error("Adapter meta is required");
3808
+ this.adapterMeta = meta;
3747
3809
  await this._tryPersistAdapterMeta();
3748
3810
  }
3749
3811
  /**
3750
- * Parse arguments for `setAdapter` and return normalized meta object.
3751
- * Accepts AdapterMeta, (type, url, token?), or (url).
3812
+ * Parse arguments for `setAdapter` and return a fully normalized AdapterMeta.
3813
+ * The result always has {type, url, branch, token, opts} at the top level.
3814
+ * Accepts AdapterMeta, (type, url, branch?, token?), or (url, branch?, token?).
3752
3815
  * @param metaOrTypeOrUrl AdapterMeta or type or url
3753
- * @param urlOrUndefined optional url when first arg is type
3754
- * @param tokenOrUndefined optional token
3755
- * @returns normalized meta object
3816
+ * @param argument1 url (when first is type) OR branch (when first is url) OR undefined
3817
+ * @param argument2 branch (when first is type) OR token (when first is url) OR undefined
3818
+ * @param argument3 token (when first is type) OR undefined
3819
+ * @returns normalized AdapterMeta
3756
3820
  */
3757
- async _parseAdapterArgs(metaOrTypeOrUrl, urlOrUndefined, tokenOrUndefined) {
3758
- if (typeof metaOrTypeOrUrl === "object" && metaOrTypeOrUrl !== null && !urlOrUndefined) return metaOrTypeOrUrl;
3759
- if (typeof metaOrTypeOrUrl === "string" && typeof urlOrUndefined === "string") {
3760
- try {
3761
- const parsed = parseAdapterFromUrl(urlOrUndefined, tokenOrUndefined, metaOrTypeOrUrl);
3762
- return { type: parsed.type, opts: parsed.opts };
3763
- } catch (error) {
3764
- throw error;
3765
- }
3821
+ _parseAdapterArgs(metaOrTypeOrUrl, argument1, argument2, argument3) {
3822
+ if (typeof metaOrTypeOrUrl === "object" && metaOrTypeOrUrl !== null) {
3823
+ return this._normalizeFromMeta(metaOrTypeOrUrl);
3766
3824
  }
3767
- if (typeof metaOrTypeOrUrl === "string" && !urlOrUndefined) {
3825
+ const firstArgument = metaOrTypeOrUrl;
3826
+ const isTypeUrlForm = typeof argument1 === "string" && /^https?:\/\//i.test(argument1);
3827
+ if (isTypeUrlForm) {
3828
+ return this._normalizeFromTypeUrl(firstArgument, argument1, argument2, argument3);
3829
+ }
3830
+ return this._normalizeFromUrl(firstArgument, argument1, argument2);
3831
+ }
3832
+ /**
3833
+ * Normalize from AdapterMeta object – generate url from opts if missing.
3834
+ * @param meta raw AdapterMeta input
3835
+ * @returns fully normalized AdapterMeta
3836
+ */
3837
+ _normalizeFromMeta(meta) {
3838
+ const type = meta.type;
3839
+ const rawOptions = meta.opts || meta.options || {};
3840
+ const options = this._stripOptionsFields(rawOptions);
3841
+ let url = meta.url;
3842
+ if (!url) {
3768
3843
  try {
3769
- const parsed = parseAdapterFromUrl(metaOrTypeOrUrl);
3770
- return { type: parsed.type, opts: parsed.opts };
3771
- } catch (error) {
3772
- throw error;
3844
+ url = buildUrlFromAdapterOptions(type, options);
3845
+ } catch {
3846
+ url = void 0;
3773
3847
  }
3774
3848
  }
3775
- throw new Error("Adapter meta is required");
3849
+ const branch = meta.branch || rawOptions.branch || "main";
3850
+ const token = meta.token || rawOptions.token || void 0;
3851
+ return { type, url, branch, token, opts: options };
3852
+ }
3853
+ /**
3854
+ * Normalize from (type, url, branch?, token?) arguments.
3855
+ * @param type adapter type
3856
+ * @param url repository url
3857
+ * @param branch optional branch (defaults to 'main')
3858
+ * @param token optional token
3859
+ * @returns fully normalized AdapterMeta
3860
+ */
3861
+ _normalizeFromTypeUrl(type, url, branch, token) {
3862
+ const parsed = parseAdapterFromUrl(url, token, type);
3863
+ const options = this._stripOptionsFields(parsed.opts || {});
3864
+ return { type: parsed.type, url, branch: branch || "main", token, opts: options };
3865
+ }
3866
+ /**
3867
+ * Normalize from (url, branch?, token?) arguments.
3868
+ * @param url repository url
3869
+ * @param branch optional branch (defaults to 'main')
3870
+ * @param token optional token
3871
+ * @returns fully normalized AdapterMeta
3872
+ */
3873
+ _normalizeFromUrl(url, branch, token) {
3874
+ const parsed = parseAdapterFromUrl(url, token);
3875
+ const options = this._stripOptionsFields(parsed.opts || {});
3876
+ return { type: parsed.type, url, branch: branch || "main", token, opts: options };
3877
+ }
3878
+ /**
3879
+ * Strip branch/token from options to avoid duplication (they live at the top level).
3880
+ * Returns a new object with only host, owner, repo, projectId, etc.
3881
+ * @param options raw adapter options
3882
+ * @returns cleaned options without branch/token
3883
+ */
3884
+ _stripOptionsFields(options) {
3885
+ if (!options || typeof options !== "object") return {};
3886
+ const cleaned = { ...options };
3887
+ delete cleaned.branch;
3888
+ delete cleaned.token;
3889
+ delete cleaned.defaultBranch;
3890
+ delete cleaned.repositoryName;
3891
+ delete cleaned.repositoryId;
3892
+ return cleaned;
3893
+ }
3894
+ /**
3895
+ * Return the persisted branch name from adapterMeta (top-level or opts fallback).
3896
+ * Defaults to 'main' when not found.
3897
+ * @returns {string} persisted branch name
3898
+ */
3899
+ _getPersistedBranch() {
3900
+ if (!this.adapterMeta) return "main";
3901
+ return this.adapterMeta.branch || this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
3776
3902
  }
3777
3903
  /**
3778
3904
  * Try to inject the configured logger into the adapter instance (best-effort).
@@ -3859,6 +3985,9 @@ var APIGitLib = (() => {
3859
3985
  _instantiateAdapter(type, options) {
3860
3986
  try {
3861
3987
  const optionsWithLogger = { ...options || {} };
3988
+ if (this.adapterMeta && this.adapterMeta.token && !optionsWithLogger.token) {
3989
+ optionsWithLogger.token = this.adapterMeta.token;
3990
+ }
3862
3991
  if (this.logger) optionsWithLogger.logger = this.logger;
3863
3992
  if (type === "github") return new GitHubAdapter(optionsWithLogger);
3864
3993
  if (type === "gitlab") return new GitLabAdapter(optionsWithLogger);
@@ -3945,8 +4074,9 @@ var APIGitLib = (() => {
3945
4074
  * @returns {void}
3946
4075
  */
3947
4076
  _populateCommitShaFromMeta(stats) {
3948
- if (!stats.gitCommitSha && this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch) {
3949
- stats.gitCommitSha = this.adapterMeta.opts.branch;
4077
+ const branch = this._getPersistedBranch();
4078
+ if (!stats.gitCommitSha && branch && branch !== "main") {
4079
+ stats.gitCommitSha = branch;
3950
4080
  }
3951
4081
  }
3952
4082
  /**
@@ -3959,7 +4089,7 @@ var APIGitLib = (() => {
3959
4089
  if (!instAdapter || stats.gitCommitSha) return;
3960
4090
  if (typeof instAdapter.resolveRef !== "function") return;
3961
4091
  try {
3962
- const branch = this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
4092
+ const branch = this._getPersistedBranch();
3963
4093
  const resolved = await instAdapter.resolveRef(branch);
3964
4094
  if (resolved) stats.gitCommitSha = resolved;
3965
4095
  } catch (error) {
@@ -4511,7 +4641,7 @@ var APIGitLib = (() => {
4511
4641
  * @returns {Promise<{commitSha:string}>}
4512
4642
  */
4513
4643
  async _handlePushWithAdapter(input, adapter) {
4514
- const branch = input.ref || this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
4644
+ const branch = input.ref || this._getPersistedBranch();
4515
4645
  const messageWithKey = `${input.message}
4516
4646
 
4517
4647
  apigit-commit-key:${input.commitKey}`;
@@ -4585,13 +4715,13 @@ apigit-commit-key:${input.commitKey}`;
4585
4715
  return { ...pullResult, remote: normalized, remotePaths: Object.keys(normalized.shas || {}) };
4586
4716
  }
4587
4717
  /**
4588
- * Pull using the persisted adapterMeta.opts.branch (or 'main').
4718
+ * Pull using the persisted adapterMeta.branch (or 'main').
4589
4719
  * @param {Record<string,string>=} baseSnapshot optional base snapshot
4590
4720
  * @returns {Promise<any>} pull result
4591
4721
  */
4592
4722
  async _pullUsingPersistedBranch(baseSnapshot) {
4593
4723
  const instAdapter = await this.getAdapterInstance();
4594
- const branch = this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
4724
+ const branch = this._getPersistedBranch();
4595
4725
  await this._trySetBackendBranch(branch);
4596
4726
  const resolvedSha = await instAdapter.resolveRef(branch);
4597
4727
  const descriptor = await instAdapter.fetchSnapshot(resolvedSha);
@@ -4624,9 +4754,9 @@ apigit-commit-key:${input.commitKey}`;
4624
4754
  * @returns {Promise<void>}
4625
4755
  */
4626
4756
  async _persistAdapterBranchMeta(branch, _adapterInstance) {
4627
- const meta = this.adapterMeta && this.adapterMeta.opts ? { ...this.adapterMeta } : await this.getAdapter();
4757
+ const meta = this.adapterMeta ? { ...this.adapterMeta } : await this.getAdapter();
4628
4758
  if (!meta) return;
4629
- const newMeta = { ...meta || {}, opts: { ...meta.opts || {}, branch } };
4759
+ const newMeta = { ...meta || {}, branch, opts: { ...meta.opts || {} } };
4630
4760
  await this.setAdapter(newMeta);
4631
4761
  await this._trySetBackendBranch(branch);
4632
4762
  }
@@ -4794,7 +4924,7 @@ apigit-commit-key:${input.commitKey}`;
4794
4924
  async _resolveAdapterDefaultBranch(instAdapter) {
4795
4925
  if (this.adapterMeta && this.adapterMeta.opts && typeof instAdapter.resolveRef === "function") {
4796
4926
  try {
4797
- const defaultBranch = this.adapterMeta.opts.branch || "main";
4927
+ const defaultBranch = this._getPersistedBranch();
4798
4928
  const resolved = await instAdapter.resolveRef(defaultBranch);
4799
4929
  return resolved || null;
4800
4930
  } catch (error) {
@@ -4832,11 +4962,12 @@ apigit-commit-key:${input.commitKey}`;
4832
4962
  try {
4833
4963
  const have = await this._loadAdapterMetaIfNeeded();
4834
4964
  if (!have) return;
4835
- const options = this.adapterMeta && this.adapterMeta.opts || {};
4965
+ const existing = this.adapterMeta && this.adapterMeta.opts;
4966
+ const options = existing ? { ...existing } : {};
4836
4967
  options.defaultBranch = md.defaultBranch;
4837
4968
  if (md.name) options.repositoryName = md.name;
4838
4969
  if (md.id !== void 0) options.repositoryId = md.id;
4839
- this.adapterMeta.opts = options;
4970
+ this.adapterMeta.opts = { ...existing, ...options };
4840
4971
  await this._writeAdapterMetaToIndex();
4841
4972
  } catch (error) {
4842
4973
  if (typeof console !== "undefined" && console.debug) console.debug("persist repository metadata aborted", error);
@@ -4921,7 +5052,7 @@ apigit-commit-key:${input.commitKey}`;
4921
5052
  async _fetchSnapshotFromAdapterInstance() {
4922
5053
  const adapterInstance = await this.getAdapterInstance();
4923
5054
  if (adapterInstance && typeof adapterInstance.fetchSnapshot === "function") {
4924
- const branch = this.adapterMeta && this.adapterMeta.opts && this.adapterMeta.opts.branch || "main";
5055
+ const branch = this._getPersistedBranch();
4925
5056
  return await adapterInstance.fetchSnapshot(branch);
4926
5057
  }
4927
5058
  return null;
@@ -5269,7 +5400,8 @@ apigit-commit-key:${input.commitKey}`;
5269
5400
  if (meta.lastCommitKey) result.lastCommitKey = meta.lastCommitKey;
5270
5401
  if (meta.adapter) result.adapter = meta.adapter;
5271
5402
  try {
5272
- this.currentBranch = meta.adapter && meta.adapter.opts && meta.adapter.opts.branch ? meta.adapter.opts.branch : null;
5403
+ const adp = meta.adapter;
5404
+ this.currentBranch = adp && (adp.branch || adp.opts && adp.opts.branch) ? adp.branch || adp.opts.branch : null;
5273
5405
  } catch {
5274
5406
  this.currentBranch = null;
5275
5407
  }