browser-git-ops 0.0.0 → 0.0.2

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 (36) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +145 -146
  3. package/dist/git/githubAdapter.d.ts.map +1 -1
  4. package/dist/git/gitlabAdapter.d.ts.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1167 -13
  8. package/dist/index.js.map +7 -0
  9. package/dist/index.mjs +1143 -0
  10. package/dist/index.mjs.map +7 -0
  11. package/dist/virtualfs/indexedDbStorage.d.ts +62 -0
  12. package/dist/virtualfs/indexedDbStorage.d.ts.map +1 -0
  13. package/dist/virtualfs/opfsStorage.d.ts +66 -0
  14. package/dist/virtualfs/opfsStorage.d.ts.map +1 -0
  15. package/dist/virtualfs/storageBackend.d.ts +44 -0
  16. package/dist/virtualfs/storageBackend.d.ts.map +1 -0
  17. package/dist/virtualfs/virtualfs.d.ts +8 -7
  18. package/dist/virtualfs/virtualfs.d.ts.map +1 -1
  19. package/package.json +60 -47
  20. package/dist/git/adapter.js +0 -2
  21. package/dist/git/githubAdapter.js +0 -179
  22. package/dist/git/gitlabAdapter.js +0 -182
  23. package/dist/test/e2e/github.spec.d.ts +0 -2
  24. package/dist/test/e2e/github.spec.d.ts.map +0 -1
  25. package/dist/test/e2e/github.spec.js +0 -47
  26. package/dist/test/e2e/gitlab.spec.d.ts +0 -2
  27. package/dist/test/e2e/gitlab.spec.d.ts.map +0 -1
  28. package/dist/test/e2e/gitlab.spec.js +0 -34
  29. package/dist/test/e2e/virtualfs.spec.d.ts +0 -2
  30. package/dist/test/e2e/virtualfs.spec.d.ts.map +0 -1
  31. package/dist/test/e2e/virtualfs.spec.js +0 -409
  32. package/dist/virtualfs/persistence.d.ts +0 -149
  33. package/dist/virtualfs/persistence.d.ts.map +0 -1
  34. package/dist/virtualfs/persistence.js +0 -294
  35. package/dist/virtualfs/types.js +0 -2
  36. package/dist/virtualfs/virtualfs.js +0 -496
package/dist/index.js CHANGED
@@ -1,14 +1,1168 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.default = exports.GitLabAdapter = exports.GitHubAdapter = exports.VirtualFS = void 0;
7
- var virtualfs_1 = require("./virtualfs/virtualfs");
8
- Object.defineProperty(exports, "VirtualFS", { enumerable: true, get: function () { return __importDefault(virtualfs_1).default; } });
9
- var githubAdapter_1 = require("./git/githubAdapter");
10
- Object.defineProperty(exports, "GitHubAdapter", { enumerable: true, get: function () { return __importDefault(githubAdapter_1).default; } });
11
- var gitlabAdapter_1 = require("./git/gitlabAdapter");
12
- Object.defineProperty(exports, "GitLabAdapter", { enumerable: true, get: function () { return __importDefault(gitlabAdapter_1).default; } });
13
- var virtualfs_2 = require("./virtualfs/virtualfs");
14
- Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(virtualfs_2).default; } });
2
+ var APIGitLib = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ GitHubAdapter: () => githubAdapter_default,
25
+ GitLabAdapter: () => gitlabAdapter_default,
26
+ IndexedDbStorage: () => indexedDbStorage_default,
27
+ OpfsStorage: () => opfsStorage_default,
28
+ VirtualFS: () => virtualfs_default,
29
+ default: () => virtualfs_default
30
+ });
31
+
32
+ // src/virtualfs/opfsStorage.ts
33
+ var ERR_OPFS_DIR_API = "OPFS directory API not available";
34
+ var OpfsStorage = class {
35
+ /**
36
+ * 同期的に OPFS 利用可否を判定します(レガシーヒントも含む)。
37
+ * @returns {boolean} 利用可能なら true
38
+ */
39
+ static canUse() {
40
+ let ok = false;
41
+ try {
42
+ const nav = globalThis.navigator;
43
+ if (nav && nav.storage && typeof nav.storage.persist === "function")
44
+ ok = true;
45
+ if (nav && nav.storage && typeof nav.storage.getDirectory === "function")
46
+ ok = true;
47
+ } catch (_) {
48
+ }
49
+ if (!ok && globalThis.originPrivateFileSystem && typeof globalThis.originPrivateFileSystem.getDirectory === "function")
50
+ ok = true;
51
+ return ok;
52
+ }
53
+ /** コンストラクタ(OPFS は初期化不要) */
54
+ constructor() {
55
+ }
56
+ /**
57
+ * 初期化(OPFS はランタイム判定のみ)
58
+ * @returns {Promise<void>} 初期化完了時に解決
59
+ */
60
+ async init() {
61
+ }
62
+ // legacy canUseOpfs removed; use static canUse() instead
63
+ /**
64
+ * OPFS のルートディレクトリハンドルを取得します。失敗時は null を返す。
65
+ * @returns {Promise<any|null>} ルートハンドルまたは null
66
+ */
67
+ async getOpfsRoot() {
68
+ const nav = globalThis.navigator;
69
+ if (nav && nav.storage && typeof nav.storage.getDirectory === "function") {
70
+ try {
71
+ return await nav.storage.getDirectory();
72
+ } catch (_) {
73
+ }
74
+ }
75
+ try {
76
+ if (globalThis.originPrivateFileSystem && typeof globalThis.originPrivateFileSystem.getDirectory === "function") {
77
+ return await globalThis.originPrivateFileSystem.getDirectory();
78
+ }
79
+ } catch (_) {
80
+ return null;
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * 指定パスのディレクトリを再帰的に作成して返す
86
+ * @returns {Promise<any>} 生成されたディレクトリハンドル
87
+ */
88
+ async ensureDir(root, parts) {
89
+ let dir = root;
90
+ for (const p of parts) {
91
+ if (dir && typeof dir.getDirectoryHandle === "function") {
92
+ dir = await dir.getDirectoryHandle(p, { create: true });
93
+ } else if (dir && typeof dir.getDirectory === "function") {
94
+ dir = await dir.getDirectory(p, { create: true });
95
+ } else {
96
+ throw new Error(ERR_OPFS_DIR_API);
97
+ }
98
+ }
99
+ return dir;
100
+ }
101
+ /**
102
+ * index を読み出す
103
+ * @returns {Promise<IndexFile|null>} 読み出した IndexFile、存在しなければ null
104
+ */
105
+ async readIndex() {
106
+ try {
107
+ const root = await this.getOpfsRoot();
108
+ if (!root)
109
+ return null;
110
+ const fh = await root.getFileHandle("index");
111
+ const file = await fh.getFile();
112
+ const txt = await file.text();
113
+ return txt ? JSON.parse(txt) : null;
114
+ } catch (_) {
115
+ return null;
116
+ }
117
+ }
118
+ /**
119
+ * index を書き込む
120
+ * @returns {Promise<void>} 書込完了時に解決
121
+ */
122
+ async writeIndex(index) {
123
+ const root = await this.getOpfsRoot();
124
+ if (!root)
125
+ throw new Error("OPFS not available");
126
+ const fh = await root.getFileHandle("index", { create: true });
127
+ const writable = await fh.createWritable();
128
+ await writable.write(JSON.stringify(index));
129
+ await writable.close();
130
+ }
131
+ /**
132
+ * blob を書き込む
133
+ * @returns {Promise<void>} 書込完了時に解決
134
+ */
135
+ async writeBlob(filepath, content) {
136
+ const root = await this.getOpfsRoot();
137
+ if (!root)
138
+ throw new Error("OPFS not available");
139
+ const parts = filepath.split("/");
140
+ if (parts.length > 1) {
141
+ const dirParts = parts.slice(0, parts.length - 1);
142
+ const parent = await this.ensureDir(root, dirParts);
143
+ const fh2 = await parent.getFileHandle(parts[parts.length - 1], { create: true });
144
+ const writable2 = await fh2.createWritable();
145
+ await writable2.write(content);
146
+ await writable2.close();
147
+ return;
148
+ }
149
+ const fh = await root.getFileHandle(filepath, { create: true });
150
+ const writable = await fh.createWritable();
151
+ await writable.write(content);
152
+ await writable.close();
153
+ }
154
+ /**
155
+ * blob を読み出す
156
+ * @returns {Promise<string|null>} ファイル内容、存在しなければ null
157
+ */
158
+ async readBlob(filepath) {
159
+ try {
160
+ const root = await this.getOpfsRoot();
161
+ if (!root)
162
+ return null;
163
+ const parts = filepath.split("/");
164
+ const dir = await this.traverseDir(root, parts.slice(0, parts.length - 1));
165
+ const fh = await dir.getFileHandle(parts[parts.length - 1]);
166
+ const file = await fh.getFile();
167
+ const txt = await file.text();
168
+ return txt ?? null;
169
+ } catch (_) {
170
+ return null;
171
+ }
172
+ }
173
+ /**
174
+ * blob を削除する
175
+ * @returns {Promise<void>} 削除完了時に解決
176
+ */
177
+ async deleteBlob(filepath) {
178
+ try {
179
+ const root = await this.getOpfsRoot();
180
+ if (!root)
181
+ return;
182
+ const parts = filepath.split("/");
183
+ const dir = await this.traverseDir(root, parts.slice(0, parts.length - 1));
184
+ const name = parts[parts.length - 1];
185
+ if (typeof dir.removeEntry === "function") {
186
+ await dir.removeEntry(name);
187
+ return;
188
+ }
189
+ if (typeof dir.getFileHandle === "function") {
190
+ await this.tryRemoveFileHandle(dir, name);
191
+ return;
192
+ }
193
+ } catch (_) {
194
+ }
195
+ }
196
+ /**
197
+ * Traverse into nested directories without creating them.
198
+ * @param root The starting directory handle
199
+ * @param parts Path parts to traverse
200
+ * @returns The final directory handle
201
+ */
202
+ async traverseDir(root, parts) {
203
+ let dir = root;
204
+ for (const p of parts) {
205
+ if (dir && typeof dir.getDirectoryHandle === "function") {
206
+ dir = await dir.getDirectoryHandle(p);
207
+ } else if (dir && typeof dir.getDirectory === "function") {
208
+ dir = await dir.getDirectory(p);
209
+ } else {
210
+ throw new Error(ERR_OPFS_DIR_API);
211
+ }
212
+ }
213
+ return dir;
214
+ }
215
+ /**
216
+ * Try to remove a file via its file handle.
217
+ * @returns {Promise<boolean>} true when removed, false otherwise
218
+ */
219
+ async tryRemoveFileHandle(dir, name) {
220
+ try {
221
+ const fh = await dir.getFileHandle(name);
222
+ if (fh && typeof fh.remove === "function") {
223
+ await fh.remove();
224
+ return true;
225
+ }
226
+ } catch (_) {
227
+ }
228
+ return false;
229
+ }
230
+ };
231
+ var opfsStorage_default = OpfsStorage;
232
+
233
+ // src/virtualfs/virtualfs.ts
234
+ var VirtualFS = class {
235
+ storageDir;
236
+ base = /* @__PURE__ */ new Map();
237
+ workspace = /* @__PURE__ */ new Map();
238
+ tombstones = /* @__PURE__ */ new Map();
239
+ index = { head: "", entries: {} };
240
+ backend;
241
+ /**
242
+ * VirtualFS のインスタンスを初期化します。
243
+ * @param options オプション
244
+ * @returns {void}
245
+ */
246
+ constructor(options) {
247
+ this.storageDir = options?.storageDir;
248
+ if (options?.backend)
249
+ this.backend = options.backend;
250
+ else
251
+ this.backend = new OpfsStorage();
252
+ }
253
+ /**
254
+ * コンテンツから SHA1 を計算します。
255
+ * @param {string} content コンテンツ
256
+ * @returns {string} 計算された SHA
257
+ */
258
+ async shaOf(content) {
259
+ const encoder = new TextEncoder();
260
+ const data = encoder.encode(content);
261
+ const hashBuffer = await crypto.subtle.digest("SHA-1", data);
262
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
263
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
264
+ }
265
+ /**
266
+ * VirtualFS の初期化を行います(バックエンド初期化と index 読み込み)。
267
+ * @returns {Promise<void>}
268
+ */
269
+ async init() {
270
+ await this.backend.init();
271
+ await this.loadIndex();
272
+ }
273
+ /**
274
+ * 永続化レイヤーから index を読み込み、内部マップを初期化します。
275
+ * @returns {Promise<void>}
276
+ */
277
+ async loadIndex() {
278
+ try {
279
+ const raw = await this.backend.readIndex();
280
+ if (raw)
281
+ this.index = raw;
282
+ for (const [p, e] of Object.entries(this.index.entries)) {
283
+ if (e.baseSha) {
284
+ this.base.set(p, { sha: e.baseSha, content: "" });
285
+ }
286
+ if (e.workspaceSha) {
287
+ this.workspace.set(p, { sha: e.workspaceSha, content: "" });
288
+ }
289
+ }
290
+ } catch (err) {
291
+ this.index = { head: "", entries: {} };
292
+ await this.saveIndex();
293
+ }
294
+ }
295
+ /**
296
+ * 内部インデックスを永続化します。
297
+ * @returns {Promise<void>}
298
+ */
299
+ async saveIndex() {
300
+ await this.backend.writeIndex(this.index);
301
+ }
302
+ /**
303
+ *
304
+ */
305
+ /**
306
+ * ワークスペースにファイルを書き込みます(ローカル編集)。
307
+ * @param {string} filepath ファイルパス
308
+ * @param {string} content コンテンツ
309
+ * @returns {Promise<void>}
310
+ */
311
+ async writeWorkspace(filepath, content) {
312
+ const sha = await this.shaOf(content);
313
+ this.workspace.set(filepath, { sha, content });
314
+ const now = Date.now();
315
+ const existing = this.index.entries[filepath];
316
+ const state = existing && existing.baseSha ? "modified" : "added";
317
+ this.index.entries[filepath] = {
318
+ path: filepath,
319
+ state,
320
+ baseSha: existing?.baseSha,
321
+ workspaceSha: sha,
322
+ updatedAt: now
323
+ };
324
+ await this.backend.writeBlob(filepath, content);
325
+ await this.saveIndex();
326
+ }
327
+ /**
328
+ * ワークスペース上のファイルを削除します(トゥームストーン作成を含む)。
329
+ * @param {string} filepath ファイルパス
330
+ * @returns {Promise<void>}
331
+ */
332
+ async deleteWorkspace(filepath) {
333
+ const entry = this.index.entries[filepath];
334
+ const now = Date.now();
335
+ if (entry && entry.baseSha) {
336
+ this.tombstones.set(filepath, { path: filepath, baseSha: entry.baseSha, deletedAt: now });
337
+ this.index.entries[filepath] = {
338
+ path: filepath,
339
+ state: "deleted",
340
+ baseSha: entry.baseSha,
341
+ updatedAt: now
342
+ };
343
+ } else {
344
+ delete this.index.entries[filepath];
345
+ this.workspace.delete(filepath);
346
+ await this.backend.deleteBlob(filepath);
347
+ }
348
+ await this.saveIndex();
349
+ }
350
+ /**
351
+ * rename を delete + create の合成で行うヘルパ
352
+ * @param from 元パス
353
+ * @param to 新パス
354
+ */
355
+ async renameWorkspace(from, to) {
356
+ const w = this.workspace.get(from);
357
+ const content = w ? w.content : this.base.get(from)?.content ?? null;
358
+ if (content === null)
359
+ throw new Error("source not found");
360
+ await this.writeWorkspace(to, content);
361
+ await this.deleteWorkspace(from);
362
+ }
363
+ /**
364
+ * ワークスペース/ベースからファイル内容を読み出します。
365
+ * @param {string} filepath ファイルパス
366
+ * @returns {Promise<string|null>} ファイル内容または null
367
+ */
368
+ async readWorkspace(filepath) {
369
+ const w = this.workspace.get(filepath);
370
+ if (w)
371
+ return w.content;
372
+ const blob = await this.backend.readBlob(filepath);
373
+ if (blob !== null)
374
+ return blob;
375
+ const b = this.base.get(filepath);
376
+ if (b && b.content)
377
+ return b.content;
378
+ return null;
379
+ }
380
+ /**
381
+ * リモートのベーススナップショットを適用します。
382
+ * @param {{[path:string]:string}} snapshot path->content のマップ
383
+ * @param {string} headSha リモート HEAD
384
+ * @returns {Promise<void>}
385
+ */
386
+ async applyBaseSnapshot(snapshot, headSha) {
387
+ this.base.clear();
388
+ for (const [p, c] of Object.entries(snapshot)) {
389
+ this.base.set(p, { sha: await this.shaOf(c), content: c });
390
+ await this.backend.writeBlob(p, c);
391
+ }
392
+ for (const [p, be] of this.base.entries()) {
393
+ const existing = this.index.entries[p];
394
+ if (!existing) {
395
+ this.index.entries[p] = {
396
+ path: p,
397
+ state: "base",
398
+ baseSha: be.sha,
399
+ updatedAt: Date.now()
400
+ };
401
+ } else if (existing.state === "base") {
402
+ existing.baseSha = be.sha;
403
+ existing.updatedAt = Date.now();
404
+ }
405
+ }
406
+ this.index.head = headSha;
407
+ await this.saveIndex();
408
+ }
409
+ /**
410
+ * インデックス情報を返します。
411
+ * @returns {IndexFile}
412
+ */
413
+ getIndex() {
414
+ return this.index;
415
+ }
416
+ /**
417
+ * 登録されているパス一覧を返します。
418
+ * @returns {string[]}
419
+ */
420
+ listPaths() {
421
+ return Object.keys(this.index.entries);
422
+ }
423
+ /**
424
+ * tombstone を返します。
425
+ * @returns {TombstoneEntry[]}
426
+ */
427
+ getTombstones() {
428
+ return Array.from(this.tombstones.values());
429
+ }
430
+ /**
431
+ * tombstone を返します。
432
+ * @returns {TombstoneEntry[]}
433
+ */
434
+ /**
435
+ * ワークスペースとインデックスから変更セットを生成します。
436
+ * @returns {Promise<Array<{type:string,path:string,content?:string,baseSha?:string}>>} 変更リスト
437
+ */
438
+ async getChangeSet() {
439
+ const changes = [];
440
+ changes.push(...this._changesFromTombstones());
441
+ changes.push(...this._changesFromIndexEntries());
442
+ return changes;
443
+ }
444
+ /**
445
+ * tombstone からの削除変更リストを生成します。
446
+ * @returns {Array<{type:'delete',path:string,baseSha:string}>}
447
+ */
448
+ _changesFromTombstones() {
449
+ const out = [];
450
+ for (const t of this.tombstones.values())
451
+ out.push({ type: "delete", path: t.path, baseSha: t.baseSha });
452
+ return out;
453
+ }
454
+ /**
455
+ * index entries から create/update の変更リストを生成します。
456
+ * @returns {Array<{type:'create'|'update',path:string,content:string,baseSha?:string}>}
457
+ */
458
+ _changesFromIndexEntries() {
459
+ const out = [];
460
+ out.push(...this._changesFromAddedEntries());
461
+ out.push(...this._changesFromModifiedEntries());
462
+ return out;
463
+ }
464
+ /**
465
+ * 追加状態のエントリから create 変更を生成します。
466
+ * @returns {Array<{type:'create',path:string,content:string}>}
467
+ */
468
+ _changesFromAddedEntries() {
469
+ const out = [];
470
+ for (const [p, e] of Object.entries(this.index.entries)) {
471
+ if (e.state === "added") {
472
+ const w = this.workspace.get(p);
473
+ if (w)
474
+ out.push({ type: "create", path: p, content: w.content });
475
+ }
476
+ }
477
+ return out;
478
+ }
479
+ /**
480
+ * 変更状態のエントリから update 変更を生成します。
481
+ * @returns {Array<{type:'update',path:string,content:string,baseSha:string}>}
482
+ */
483
+ _changesFromModifiedEntries() {
484
+ const out = [];
485
+ for (const [p, e] of Object.entries(this.index.entries)) {
486
+ if (e.state === "modified") {
487
+ const w = this.workspace.get(p);
488
+ if (w && e.baseSha)
489
+ out.push({ type: "update", path: p, content: w.content, baseSha: e.baseSha });
490
+ }
491
+ }
492
+ return out;
493
+ }
494
+ /**
495
+ * リモートスナップショットからの差分取り込み時に、単一パスを評価して
496
+ * 必要なら conflicts に追加、もしくは base を更新します。
497
+ * @returns {Promise<void>}
498
+ */
499
+ async _handleRemotePath(p, remoteSha, baseSnapshot, conflicts) {
500
+ const idxEntry = this.index.entries[p];
501
+ const localWorkspace = this.workspace.get(p);
502
+ const localBase = this.base.get(p);
503
+ if (!idxEntry)
504
+ return await this._handleRemoteNew(p, remoteSha, baseSnapshot, conflicts, localWorkspace, localBase);
505
+ return await this._handleRemoteExisting(p, idxEntry, remoteSha, baseSnapshot, conflicts, localWorkspace);
506
+ }
507
+ /**
508
+ * リモートに存在するがローカルにないパスを処理します。
509
+ * @returns {Promise<void>}
510
+ */
511
+ async _handleRemoteNew(p, remoteSha, baseSnapshot, conflicts, localWorkspace, localBase) {
512
+ if (localWorkspace) {
513
+ conflicts.push({ path: p, remoteSha, workspaceSha: localWorkspace.sha, baseSha: localBase?.sha });
514
+ } else {
515
+ const content = baseSnapshot[p];
516
+ this.base.set(p, { sha: remoteSha, content });
517
+ this.index.entries[p] = { path: p, state: "base", baseSha: remoteSha, updatedAt: Date.now() };
518
+ await this.backend.writeBlob(p, content);
519
+ }
520
+ }
521
+ /**
522
+ * リモートに存在し、かつローカルにエントリがあるパスを処理します。
523
+ * @returns {Promise<void>}
524
+ */
525
+ async _handleRemoteExisting(p, idxEntry, remoteSha, baseSnapshot, conflicts, localWorkspace) {
526
+ const baseSha = idxEntry.baseSha;
527
+ if (baseSha === remoteSha)
528
+ return;
529
+ if (!localWorkspace || localWorkspace.sha === baseSha) {
530
+ const content = baseSnapshot[p];
531
+ this.base.set(p, { sha: remoteSha, content });
532
+ idxEntry.baseSha = remoteSha;
533
+ idxEntry.state = "base";
534
+ idxEntry.updatedAt = Date.now();
535
+ await this.backend.writeBlob(p, content);
536
+ } else {
537
+ conflicts.push({ path: p, baseSha, remoteSha, workspaceSha: localWorkspace?.sha });
538
+ }
539
+ }
540
+ /**
541
+ * ローカルに対する変更(create/update/delete)を適用するヘルパー
542
+ * @param {any} ch 変更オブジェクト
543
+ * @returns {Promise<void>}
544
+ */
545
+ async _applyChangeLocally(ch) {
546
+ if (ch.type === "create" || ch.type === "update") {
547
+ const sha = await this.shaOf(ch.content);
548
+ this.base.set(ch.path, { sha, content: ch.content });
549
+ const entry = this.index.entries[ch.path] || { path: ch.path };
550
+ entry.baseSha = sha;
551
+ entry.state = "base";
552
+ entry.updatedAt = Date.now();
553
+ entry.workspaceSha = void 0;
554
+ this.index.entries[ch.path] = entry;
555
+ await this.backend.writeBlob(ch.path, ch.content);
556
+ this.workspace.delete(ch.path);
557
+ } else if (ch.type === "delete") {
558
+ delete this.index.entries[ch.path];
559
+ this.base.delete(ch.path);
560
+ this.tombstones.delete(ch.path);
561
+ await this.backend.deleteBlob(ch.path);
562
+ this.workspace.delete(ch.path);
563
+ }
564
+ }
565
+ /**
566
+ * リモート側で削除されたエントリをローカルに反映します。
567
+ * @returns {Promise<void>}
568
+ */
569
+ async _handleRemoteDeletion(p, e, _remoteShas, conflicts) {
570
+ const localWorkspace = this.workspace.get(p);
571
+ if (!localWorkspace || localWorkspace.sha === e.baseSha) {
572
+ delete this.index.entries[p];
573
+ this.base.delete(p);
574
+ await this.backend.deleteBlob(p);
575
+ } else {
576
+ conflicts.push({ path: p, baseSha: e.baseSha, workspaceSha: localWorkspace?.sha });
577
+ }
578
+ }
579
+ /**
580
+ * GitLab 風の actions ベースコミットフローで push を実行します。
581
+ * @returns {Promise<{commitSha:string}>}
582
+ */
583
+ async _pushWithActions(adapter, input, branch) {
584
+ const commitSha = await adapter.createCommitWithActions(branch, input.message, input.changes);
585
+ try {
586
+ await adapter.updateRef(`heads/${branch}`, commitSha);
587
+ } catch (e) {
588
+ }
589
+ for (const ch of input.changes) {
590
+ await this._applyChangeLocally(ch);
591
+ }
592
+ this.index.head = commitSha;
593
+ await this.saveIndex();
594
+ return { commitSha };
595
+ }
596
+ /**
597
+ * GitHub 風の blob/tree/commit フローで push を実行します。
598
+ * @returns {Promise<{commitSha:string}>}
599
+ */
600
+ async _pushWithGitHubFlow(adapter, input, branch) {
601
+ const blobMap = await adapter.createBlobs(input.changes);
602
+ const changesWithBlob = input.changes.map((c) => ({ ...c, blobSha: blobMap[c.path] }));
603
+ const treeSha = await adapter.createTree(changesWithBlob);
604
+ const commitSha = await adapter.createCommit(input.message, input.parentSha, treeSha);
605
+ try {
606
+ await adapter.updateRef(`heads/${branch}`, commitSha);
607
+ } catch (e) {
608
+ }
609
+ for (const ch of input.changes) {
610
+ await this._applyChangeLocally(ch);
611
+ }
612
+ this.index.head = commitSha;
613
+ await this.saveIndex();
614
+ return { commitSha };
615
+ }
616
+ /**
617
+ * リモートのスナップショットを取り込み、コンフリクト情報を返します。
618
+ * @param {string} remoteHead リモート HEAD
619
+ * @param {{[path:string]:string}} baseSnapshot path->content マップ
620
+ * @returns {Promise<{conflicts:Array<import('./types').ConflictEntry>}>}
621
+ */
622
+ async pull(remoteHead, baseSnapshot) {
623
+ const conflicts = [];
624
+ const remoteShas = {};
625
+ for (const [p, c] of Object.entries(baseSnapshot)) {
626
+ remoteShas[p] = await this.shaOf(c);
627
+ }
628
+ for (const [p, remoteSha] of Object.entries(remoteShas)) {
629
+ await this._handleRemotePath(p, remoteSha, baseSnapshot, conflicts);
630
+ }
631
+ for (const [p, e] of Object.entries(this.index.entries)) {
632
+ if (!(p in remoteShas)) {
633
+ await this._handleRemoteDeletion(p, e, remoteShas, conflicts);
634
+ }
635
+ }
636
+ if (conflicts.length === 0) {
637
+ this.index.head = remoteHead;
638
+ await this.saveIndex();
639
+ }
640
+ return { conflicts };
641
+ }
642
+ /**
643
+ * 変更をコミットしてリモートへ反映します。adapter が無ければローカルシミュレーションします。
644
+ * @param {import('./types').CommitInput} input コミット入力
645
+ * @param {import('../git/adapter').GitAdapter} [adapter] 任意のアダプタ
646
+ * @returns {Promise<{commitSha:string}>}
647
+ */
648
+ async push(input, adapter) {
649
+ if (input.parentSha !== this.index.head) {
650
+ throw new Error("HEAD changed. pull required");
651
+ }
652
+ if (!input.commitKey) {
653
+ input.commitKey = await this.shaOf(input.parentSha + JSON.stringify(input.changes));
654
+ }
655
+ if (!input.changes || input.changes.length === 0)
656
+ throw new Error("No changes to commit");
657
+ if (adapter) {
658
+ const branch = input.ref || "main";
659
+ const messageWithKey = `${input.message}
660
+
661
+ apigit-commit-key:${input.commitKey}`;
662
+ if (adapter.createCommitWithActions) {
663
+ input.message = messageWithKey;
664
+ const res2 = await this._pushWithActions(adapter, input, branch);
665
+ this.index.lastCommitKey = input.commitKey;
666
+ return res2;
667
+ }
668
+ input.message = messageWithKey;
669
+ const res = await this._pushWithGitHubFlow(adapter, input, branch);
670
+ this.index.lastCommitKey = input.commitKey;
671
+ return res;
672
+ }
673
+ const commitSha = await this.shaOf(input.parentSha + "|" + input.commitKey);
674
+ for (const ch of input.changes) {
675
+ await this._applyChangeLocally(ch);
676
+ }
677
+ this.index.head = commitSha;
678
+ this.index.lastCommitKey = input.commitKey;
679
+ await this.saveIndex();
680
+ return { commitSha };
681
+ }
682
+ };
683
+ var virtualfs_default = VirtualFS;
684
+
685
+ // src/virtualfs/indexedDbStorage.ts
686
+ var IndexedDbStorage = class {
687
+ /**
688
+ * 環境に IndexedDB が存在するかを同期検査します。
689
+ * @returns {boolean} 利用可能なら true
690
+ */
691
+ static canUse() {
692
+ try {
693
+ return !!globalThis.indexedDB;
694
+ } catch (_) {
695
+ return false;
696
+ }
697
+ }
698
+ dbName = "apigit_storage";
699
+ dbPromise;
700
+ /** コンストラクタ */
701
+ constructor() {
702
+ this.dbPromise = this.openDb();
703
+ }
704
+ /**
705
+ * 初期化: DB をオープンするまで待つ
706
+ * @returns {Promise<void>} 初期化完了時に解決
707
+ */
708
+ async init() {
709
+ await this.dbPromise;
710
+ }
711
+ /**
712
+ * DB を開いて objectStore を初期化する
713
+ * @returns {Promise<IDBDatabase>} Opened IDBDatabase
714
+ */
715
+ openDb() {
716
+ return new Promise((resolve, reject) => {
717
+ const idb = globalThis.indexedDB;
718
+ if (!idb)
719
+ return reject(new Error("IndexedDB is not available"));
720
+ const req = idb.open(this.dbName, 1);
721
+ function handleUpgrade(ev) {
722
+ const db = ev.target.result;
723
+ if (!db.objectStoreNames.contains("blobs"))
724
+ db.createObjectStore("blobs");
725
+ if (!db.objectStoreNames.contains("index"))
726
+ db.createObjectStore("index");
727
+ }
728
+ req.onupgradeneeded = handleUpgrade;
729
+ function makeVersionChangeHandler(dbParam) {
730
+ return function() {
731
+ try {
732
+ dbParam.close();
733
+ } catch (_) {
734
+ }
735
+ };
736
+ }
737
+ function handleSuccess() {
738
+ const db = req.result;
739
+ try {
740
+ db.onversionchange = makeVersionChangeHandler(db);
741
+ } catch (_) {
742
+ }
743
+ resolve(db);
744
+ }
745
+ function handleError() {
746
+ reject(req.error);
747
+ }
748
+ req.onsuccess = handleSuccess;
749
+ req.onerror = handleError;
750
+ });
751
+ }
752
+ /**
753
+ * トランザクションラッパー。cb 内の処理をトランザクションで実行し、
754
+ * 必要なら再試行します。
755
+ */
756
+ /**
757
+ * トランザクションラッパー。cb 内の処理をトランザクションで実行し、必要なら再試行します。
758
+ * @returns {Promise<void>} トランザクション処理完了時に解決
759
+ */
760
+ async tx(storeName, mode, cb) {
761
+ const attempt = async () => {
762
+ const db = await this.dbPromise;
763
+ return new Promise((resolve, reject) => {
764
+ let tx;
765
+ try {
766
+ tx = db.transaction(storeName, mode);
767
+ } catch (err) {
768
+ return reject(err);
769
+ }
770
+ const storeObj = tx.objectStore(storeName);
771
+ function handleTxComplete() {
772
+ resolve();
773
+ }
774
+ function handleTxError() {
775
+ reject(tx.error);
776
+ }
777
+ Promise.resolve(cb(storeObj)).then(() => {
778
+ tx.oncomplete = handleTxComplete;
779
+ tx.onerror = handleTxError;
780
+ }).catch(reject);
781
+ });
782
+ };
783
+ try {
784
+ return await attempt();
785
+ } catch (err) {
786
+ const isInvalidState = err && (err.name === "InvalidStateError" || /closing/i.test(String(err.message || "")));
787
+ if (isInvalidState) {
788
+ this.dbPromise = this.openDb();
789
+ return await attempt();
790
+ }
791
+ throw err;
792
+ }
793
+ }
794
+ // legacy canUseOpfs removed; use static canUse() instead
795
+ /**
796
+ * index を読み出す
797
+ * @returns {Promise<IndexFile|null>} 読み出した IndexFile、存在しなければ null
798
+ */
799
+ async readIndex() {
800
+ const db = await this.dbPromise;
801
+ return new Promise((resolve, reject) => {
802
+ const tx = db.transaction("index", "readonly");
803
+ const store = tx.objectStore("index");
804
+ const req = store.get("index");
805
+ function handleIndexSuccess() {
806
+ resolve(req.result ?? null);
807
+ }
808
+ function handleIndexError() {
809
+ reject(req.error);
810
+ }
811
+ req.onsuccess = handleIndexSuccess;
812
+ req.onerror = handleIndexError;
813
+ });
814
+ }
815
+ /**
816
+ * index を書き込む
817
+ * @returns {Promise<void>} 書込完了時に解決
818
+ */
819
+ async writeIndex(index) {
820
+ await this.tx("index", "readwrite", (store) => {
821
+ store.put(index, "index");
822
+ });
823
+ }
824
+ /**
825
+ * blob を書き込む
826
+ * @returns {Promise<void>} 書込完了時に解決
827
+ */
828
+ async writeBlob(filepath, content) {
829
+ await this.tx("blobs", "readwrite", (store) => {
830
+ store.put(content, filepath);
831
+ });
832
+ }
833
+ /**
834
+ * blob を読み出す
835
+ * @returns {Promise<string|null>} ファイル内容、存在しなければ null
836
+ */
837
+ async readBlob(filepath) {
838
+ const db = await this.dbPromise;
839
+ return new Promise((resolve, reject) => {
840
+ const tx = db.transaction("blobs", "readonly");
841
+ const store = tx.objectStore("blobs");
842
+ const req = store.get(filepath);
843
+ function handleBlobSuccess() {
844
+ resolve(req.result ?? null);
845
+ }
846
+ function handleBlobError() {
847
+ reject(req.error);
848
+ }
849
+ req.onsuccess = handleBlobSuccess;
850
+ req.onerror = handleBlobError;
851
+ });
852
+ }
853
+ /**
854
+ * blob を削除する
855
+ * @returns {Promise<void>} 削除完了時に解決
856
+ */
857
+ async deleteBlob(filepath) {
858
+ await this.tx("blobs", "readwrite", (store) => {
859
+ store.delete(filepath);
860
+ });
861
+ }
862
+ };
863
+ var indexedDbStorage_default = IndexedDbStorage;
864
+
865
+ // src/git/githubAdapter.ts
866
+ var RetryableError = class extends Error {
867
+ };
868
+ var NonRetryableError = class extends Error {
869
+ };
870
+ function sleep(ms) {
871
+ return new Promise((res) => setTimeout(res, ms));
872
+ }
873
+ async function fetchWithRetry(input, init, attempts = 4, baseDelay = 300) {
874
+ let lastErr;
875
+ for (let i = 0; i < attempts; i++) {
876
+ try {
877
+ const res = await fetch(input, init);
878
+ return await processResponseWithDelay(res, i, baseDelay);
879
+ } catch (err) {
880
+ if (err instanceof NonRetryableError)
881
+ throw err;
882
+ lastErr = err;
883
+ await sleep(getDelayForResponse(null, i, baseDelay));
884
+ }
885
+ }
886
+ throw new RetryableError(`Failed after ${attempts} attempts: ${lastErr}`);
887
+ }
888
+ function classifyStatus(status) {
889
+ return status >= 500 || status === 429;
890
+ }
891
+ function getDelayForResponse(res, i, baseDelay) {
892
+ if (!res)
893
+ return baseDelay * Math.pow(2, i) + Math.random() * 100;
894
+ const retryAfter = res.headers.get("Retry-After");
895
+ return retryAfter ? Number(retryAfter) * 1e3 : baseDelay * Math.pow(2, i) + Math.random() * 100;
896
+ }
897
+ async function processResponseWithDelay(res, i, baseDelay) {
898
+ if (res.ok)
899
+ return res;
900
+ if (classifyStatus(res.status)) {
901
+ await sleep(getDelayForResponse(res, i, baseDelay));
902
+ throw new RetryableError(`HTTP ${res.status}`);
903
+ }
904
+ const txt = await res.text().catch(() => "");
905
+ throw new NonRetryableError(`HTTP ${res.status}: ${txt}`);
906
+ }
907
+ function mapWithConcurrency(items, mapper, concurrency = 5) {
908
+ const results = [];
909
+ let idx = 0;
910
+ const runners = [];
911
+ const run = async () => {
912
+ while (idx < items.length) {
913
+ const i = idx++;
914
+ if (i >= items.length)
915
+ break;
916
+ const r = await mapper(items[i]);
917
+ results[i] = r;
918
+ }
919
+ };
920
+ for (let i = 0; i < Math.min(concurrency, items.length); i++)
921
+ runners.push(run());
922
+ return Promise.all(runners).then(() => results);
923
+ }
924
+ var GitHubAdapter = class {
925
+ constructor(opts) {
926
+ this.opts = opts;
927
+ this.baseUrl = `https://api.github.com/repos/${opts.owner}/${opts.repo}`;
928
+ this.headers = {
929
+ Authorization: `token ${opts.token}`,
930
+ Accept: "application/vnd.github+json",
931
+ "Content-Type": "application/json"
932
+ };
933
+ this._fetchWithRetry = fetchWithRetry;
934
+ }
935
+ baseUrl;
936
+ headers;
937
+ _fetchWithRetry;
938
+ // simple in-memory blob cache: contentSha -> blobSha
939
+ blobCache = /* @__PURE__ */ new Map();
940
+ async createBlobs(changes, concurrency = 5) {
941
+ const tasks = changes.filter((c) => c.type === "create" || c.type === "update");
942
+ const mapper = async (ch) => {
943
+ const encoder = new TextEncoder();
944
+ const data = encoder.encode(ch.content || "");
945
+ const buf = await crypto.subtle.digest("SHA-1", data);
946
+ const contentHash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
947
+ const cached = this.blobCache.get(contentHash);
948
+ if (cached)
949
+ return { path: ch.path, sha: cached };
950
+ const body = JSON.stringify({ content: ch.content, encoding: "utf-8" });
951
+ const res = await this._fetchWithRetry(`${this.baseUrl}/git/blobs`, { method: "POST", headers: this.headers, body }, 4, 300);
952
+ const j = await res.json();
953
+ if (!j.sha)
954
+ throw new NonRetryableError("blob response missing sha");
955
+ this.blobCache.set(contentHash, j.sha);
956
+ return { path: ch.path, sha: j.sha };
957
+ };
958
+ const results = await mapWithConcurrency(tasks, mapper, concurrency);
959
+ const map = {};
960
+ for (const r of results)
961
+ map[r.path] = r.sha;
962
+ return map;
963
+ }
964
+ async createTree(changes, baseTreeSha) {
965
+ const tree = [];
966
+ for (const c of changes) {
967
+ if (c.type === "delete") {
968
+ tree.push({ path: c.path, mode: "100644", sha: null });
969
+ } else {
970
+ if (!c.blobSha)
971
+ throw new NonRetryableError(`missing blobSha for ${c.path}`);
972
+ tree.push({ path: c.path, mode: "100644", type: "blob", sha: c.blobSha });
973
+ }
974
+ }
975
+ const body = { tree };
976
+ if (baseTreeSha)
977
+ body.base_tree = baseTreeSha;
978
+ const res = await this._fetchWithRetry(`${this.baseUrl}/git/trees`, { method: "POST", headers: this.headers, body: JSON.stringify(body) }, 4, 300);
979
+ const j = await res.json();
980
+ if (!j.sha)
981
+ throw new NonRetryableError("createTree response missing sha");
982
+ return j.sha;
983
+ }
984
+ async createCommit(message, parentSha, treeSha) {
985
+ const body = JSON.stringify({ message, tree: treeSha, parents: [parentSha] });
986
+ const res = await this._fetchWithRetry(`${this.baseUrl}/git/commits`, { method: "POST", headers: this.headers, body }, 4, 300);
987
+ const j = await res.json();
988
+ if (!j.sha)
989
+ throw new NonRetryableError("createCommit response missing sha");
990
+ return j.sha;
991
+ }
992
+ async updateRef(ref, commitSha, force = false) {
993
+ const body = JSON.stringify({ sha: commitSha, force });
994
+ const res = await this._fetchWithRetry(`${this.baseUrl}/git/refs/${ref}`, { method: "PATCH", headers: this.headers, body }, 4, 300);
995
+ if (!res.ok) {
996
+ const txt = await res.text().catch(() => "");
997
+ throw new NonRetryableError(`updateRef failed: ${res.status} ${txt}`);
998
+ }
999
+ }
1000
+ };
1001
+ var githubAdapter_default = GitHubAdapter;
1002
+
1003
+ // src/git/gitlabAdapter.ts
1004
+ var GitLabAdapter = class {
1005
+ /**
1006
+ * GitLabAdapter を初期化します。
1007
+ * @param {GLOpts} opts 設定オブジェクト
1008
+ */
1009
+ constructor(opts) {
1010
+ this.opts = opts;
1011
+ const host = opts.host || "https://gitlab.com";
1012
+ this.baseUrl = `${host}/api/v4/projects/${encodeURIComponent(opts.projectId)}`;
1013
+ this.headers = { "PRIVATE-TOKEN": opts.token, "Content-Type": "application/json" };
1014
+ }
1015
+ baseUrl;
1016
+ headers;
1017
+ pendingActions = null;
1018
+ maxRetries = 3;
1019
+ baseBackoff = 300;
1020
+ /**
1021
+ * コンテンツから sha1 を算出します。
1022
+ * @param {string} content コンテンツ
1023
+ * @returns {string} sha1 ハッシュ
1024
+ */
1025
+ async shaOf(content) {
1026
+ const encoder = new TextEncoder();
1027
+ const data = encoder.encode(content);
1028
+ const buf = await crypto.subtle.digest("SHA-1", data);
1029
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
1030
+ }
1031
+ /**
1032
+ * 変更一覧から blob sha のマップを作成します(疑似実装)。
1033
+ * @param {any[]} changes 変更一覧
1034
+ * @returns {Promise<Record<string,string>>} path->sha マップ
1035
+ */
1036
+ async createBlobs(changes) {
1037
+ const map = {};
1038
+ for (const c of changes) {
1039
+ if (c.type === "create" || c.type === "update")
1040
+ map[c.path] = await this.shaOf(c.content);
1041
+ }
1042
+ return map;
1043
+ }
1044
+ /**
1045
+ * 互換用のツリー作成。実際には actions を保持しておき、マーカーを返します。
1046
+ * @param {any[]} _changes 変更一覧
1047
+ * @param {string} [_baseTreeSha] ベースツリー(未使用)
1048
+ * @returns {Promise<string>} マーカー文字列
1049
+ */
1050
+ async createTree(_changes, _baseTreeSha) {
1051
+ const actions = (_changes || []).map((c) => {
1052
+ if (c.type === "delete")
1053
+ return { action: "delete", file_path: c.path };
1054
+ if (c.type === "create")
1055
+ return { action: "create", file_path: c.path, content: c.content };
1056
+ return { action: "update", file_path: c.path, content: c.content };
1057
+ });
1058
+ this.pendingActions = actions;
1059
+ return `gitlab-tree-${Date.now()}-${Math.floor(Math.random() * 1e5)}`;
1060
+ }
1061
+ /**
1062
+ * createTree で保持した actions があればコミットし、なければ parentSha を返します。
1063
+ * @param {string} message コミットメッセージ
1064
+ * @param {string} parentSha 親コミット SHA
1065
+ * @param {string} _treeSha ツリー SHA(未使用)
1066
+ * @returns {Promise<string>} 新規コミット SHA または parentSha
1067
+ */
1068
+ async createCommit(message, parentSha, _treeSha) {
1069
+ if (this.pendingActions && this.pendingActions.length > 0) {
1070
+ const branch = "main";
1071
+ const res = await this.createCommitWithActions(
1072
+ branch,
1073
+ message,
1074
+ this.pendingActions.map((a) => ({ type: a.action === "delete" ? "delete" : a.action === "create" ? "create" : "update", path: a.file_path, content: a.content }))
1075
+ );
1076
+ this.pendingActions = null;
1077
+ return res;
1078
+ }
1079
+ return parentSha;
1080
+ }
1081
+ /**
1082
+ * actions を用いて GitLab のコミット API を呼び出します。
1083
+ * @param {string} branch ブランチ名
1084
+ * @param {string} message コミットメッセージ
1085
+ * @param {{type:string,path:string,content?:string}[]} changes 変更一覧
1086
+ * @returns {Promise<any>} コミット応答(id など)
1087
+ */
1088
+ async createCommitWithActions(branch, message, changes) {
1089
+ const url = `${this.baseUrl}/repository/commits`;
1090
+ const actions = changes.map((c) => {
1091
+ if (c.type === "delete")
1092
+ return { action: "delete", file_path: c.path };
1093
+ if (c.type === "create")
1094
+ return { action: "create", file_path: c.path, content: c.content };
1095
+ return { action: "update", file_path: c.path, content: c.content };
1096
+ });
1097
+ const body = JSON.stringify({ branch, commit_message: message, actions });
1098
+ const res = await this.fetchWithRetry(url, { method: "POST", headers: this.headers, body });
1099
+ const text = await res.text().catch(() => "");
1100
+ let j = null;
1101
+ try {
1102
+ j = text ? JSON.parse(text) : null;
1103
+ } catch (err) {
1104
+ throw new Error(`GitLab commit invalid JSON response: ${text}`);
1105
+ }
1106
+ if (!j || !j.id && !j.commit) {
1107
+ throw new Error(`GitLab commit unexpected response: ${JSON.stringify(j)}`);
1108
+ }
1109
+ return j.id || j.commit || j;
1110
+ }
1111
+ /**
1112
+ * fetch をリトライ付きで実行します。
1113
+ * @param {string} url リクエスト URL
1114
+ * @param {RequestInit} opts fetch オプション
1115
+ * @param {number} [retries] 最大リトライ回数
1116
+ * @returns {Promise<Response>} レスポンス
1117
+ */
1118
+ async fetchWithRetry(url, opts, retries = this.maxRetries) {
1119
+ for (let attempt = 1; attempt <= retries; attempt++) {
1120
+ try {
1121
+ const res = await fetch(url, opts);
1122
+ if (!this.isRetryableStatus(res.status))
1123
+ return res;
1124
+ if (attempt === retries)
1125
+ return res;
1126
+ const wait = this.backoffMs(attempt);
1127
+ await new Promise((r) => setTimeout(r, wait));
1128
+ } catch (err) {
1129
+ if (attempt === retries)
1130
+ throw err;
1131
+ const wait = this.backoffMs(attempt);
1132
+ await new Promise((r) => setTimeout(r, wait));
1133
+ }
1134
+ }
1135
+ throw new Error("fetchWithRetry: unexpected exit");
1136
+ }
1137
+ /**
1138
+ * ステータスが再試行対象か判定します。
1139
+ * @param {number} status ステータスコード
1140
+ * @returns {boolean}
1141
+ */
1142
+ isRetryableStatus(status) {
1143
+ return status === 429 || status >= 500 && status < 600;
1144
+ }
1145
+ /**
1146
+ * バックオフ時間を計算します。
1147
+ * @param {number} attempt 試行回数(1..)
1148
+ * @returns {number} ミリ秒
1149
+ */
1150
+ backoffMs(attempt) {
1151
+ const base = this.baseBackoff * Math.pow(2, attempt - 1);
1152
+ const jitter = Math.floor(Math.random() * base * 0.3);
1153
+ return base + jitter;
1154
+ }
1155
+ /**
1156
+ * リファレンス更新は不要なため noop 実装です。
1157
+ * @param {string} _ref ref 名
1158
+ * @param {string} _commitSha コミット SHA
1159
+ * @param {boolean} [_force]
1160
+ * @returns {Promise<void>}
1161
+ */
1162
+ async updateRef(_ref, _commitSha, _force = false) {
1163
+ }
1164
+ };
1165
+ var gitlabAdapter_default = GitLabAdapter;
1166
+ return __toCommonJS(src_exports);
1167
+ })();
1168
+ //# sourceMappingURL=index.js.map