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
@@ -1,179 +0,0 @@
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.mapWithConcurrency = exports.processResponseWithDelay = exports.getDelayForResponse = exports.classifyStatus = exports.fetchWithRetry = exports.GitHubAdapter = exports.NonRetryableError = exports.RetryableError = void 0;
7
- const crypto_1 = __importDefault(require("crypto"));
8
- /**
9
- * リトライ可能なエラー。
10
- */
11
- class RetryableError extends Error {
12
- }
13
- exports.RetryableError = RetryableError;
14
- /**
15
- * リトライ不可能なエラー。
16
- */
17
- class NonRetryableError extends Error {
18
- }
19
- exports.NonRetryableError = NonRetryableError;
20
- /**
21
- * 指定ミリ秒だけ sleep するユーティリティ
22
- * @param ms ミリ秒
23
- */
24
- function sleep(ms) {
25
- return new Promise((res) => setTimeout(res, ms));
26
- }
27
- /**
28
- * fetch を再試行付きで実行するユーティリティ。
29
- * 5xx や 429 はリトライ対象、それ以外は NonRetryableError を投げる。
30
- * @param input RequestInfo
31
- * @param init RequestInit
32
- * @param attempts 試行回数
33
- * @param baseDelay ベースの遅延(ms)
34
- */
35
- /* istanbul ignore next */
36
- async function fetchWithRetry(input, init, attempts = 4, baseDelay = 300) {
37
- let lastErr;
38
- for (let i = 0; i < attempts; i++) {
39
- try {
40
- const res = await fetch(input, init);
41
- return await processResponseWithDelay(res, i, baseDelay);
42
- /* istanbul ignore next */
43
- }
44
- catch (err) {
45
- if (err instanceof NonRetryableError)
46
- throw err;
47
- lastErr = err;
48
- await sleep(getDelayForResponse(null, i, baseDelay));
49
- }
50
- }
51
- throw new RetryableError(`Failed after ${attempts} attempts: ${lastErr}`);
52
- }
53
- exports.fetchWithRetry = fetchWithRetry;
54
- function classifyStatus(status) {
55
- return status >= 500 || status === 429;
56
- }
57
- exports.classifyStatus = classifyStatus;
58
- function getDelayForResponse(res, i, baseDelay) {
59
- if (!res)
60
- return baseDelay * Math.pow(2, i) + Math.random() * 100;
61
- const retryAfter = res.headers.get('Retry-After');
62
- return retryAfter ? Number(retryAfter) * 1000 : baseDelay * Math.pow(2, i) + Math.random() * 100;
63
- }
64
- exports.getDelayForResponse = getDelayForResponse;
65
- async function processResponseWithDelay(res, i, baseDelay) {
66
- if (res.ok)
67
- return res;
68
- if (classifyStatus(res.status)) {
69
- await sleep(getDelayForResponse(res, i, baseDelay));
70
- throw new RetryableError(`HTTP ${res.status}`);
71
- }
72
- const txt = await res.text().catch(() => '');
73
- throw new NonRetryableError(`HTTP ${res.status}: ${txt}`);
74
- }
75
- exports.processResponseWithDelay = processResponseWithDelay;
76
- /**
77
- * 非同期マップを並列実行するユーティリティ
78
- * @param items 入力配列
79
- * @param mapper マッピング関数
80
- * @param concurrency 同時実行数
81
- */
82
- /* istanbul ignore next */
83
- function mapWithConcurrency(items, mapper, concurrency = 5) {
84
- const results = [];
85
- let idx = 0;
86
- const runners = [];
87
- const run = async () => {
88
- while (idx < items.length) {
89
- const i = idx++;
90
- if (i >= items.length)
91
- break;
92
- const r = await mapper(items[i]);
93
- results[i] = r;
94
- }
95
- };
96
- for (let i = 0; i < Math.min(concurrency, items.length); i++)
97
- runners.push(run());
98
- return Promise.all(runners).then(() => results);
99
- }
100
- exports.mapWithConcurrency = mapWithConcurrency;
101
- class GitHubAdapter {
102
- opts;
103
- baseUrl;
104
- headers;
105
- _fetchWithRetry;
106
- // simple in-memory blob cache: contentSha -> blobSha
107
- blobCache = new Map();
108
- constructor(opts) {
109
- this.opts = opts;
110
- this.baseUrl = `https://api.github.com/repos/${opts.owner}/${opts.repo}`;
111
- this.headers = {
112
- Authorization: `token ${opts.token}`,
113
- Accept: 'application/vnd.github+json',
114
- 'Content-Type': 'application/json',
115
- };
116
- this._fetchWithRetry = fetchWithRetry;
117
- }
118
- async createBlobs(changes, concurrency = 5) {
119
- const tasks = changes.filter((c) => c.type === 'create' || c.type === 'update');
120
- const mapper = async (ch) => {
121
- // compute simple content hash to enable cache lookup
122
- const contentHash = crypto_1.default.createHash('sha1').update(ch.content || '', 'utf8').digest('hex');
123
- const cached = this.blobCache.get(contentHash);
124
- if (cached)
125
- return { path: ch.path, sha: cached };
126
- const body = JSON.stringify({ content: ch.content, encoding: 'utf-8' });
127
- const res = await this._fetchWithRetry(`${this.baseUrl}/git/blobs`, { method: 'POST', headers: this.headers, body }, 4, 300);
128
- const j = await res.json();
129
- if (!j.sha)
130
- throw new NonRetryableError('blob response missing sha');
131
- this.blobCache.set(contentHash, j.sha);
132
- return { path: ch.path, sha: j.sha };
133
- };
134
- const results = await mapWithConcurrency(tasks, mapper, concurrency);
135
- const map = {};
136
- for (const r of results)
137
- map[r.path] = r.sha;
138
- return map;
139
- }
140
- async createTree(changes, baseTreeSha) {
141
- const tree = [];
142
- for (const c of changes) {
143
- if (c.type === 'delete') {
144
- tree.push({ path: c.path, mode: '100644', sha: null });
145
- }
146
- else {
147
- if (!c.blobSha)
148
- throw new NonRetryableError(`missing blobSha for ${c.path}`);
149
- tree.push({ path: c.path, mode: '100644', type: 'blob', sha: c.blobSha });
150
- }
151
- }
152
- const body = { tree };
153
- if (baseTreeSha)
154
- body.base_tree = baseTreeSha;
155
- const res = await this._fetchWithRetry(`${this.baseUrl}/git/trees`, { method: 'POST', headers: this.headers, body: JSON.stringify(body) }, 4, 300);
156
- const j = await res.json();
157
- if (!j.sha)
158
- throw new NonRetryableError('createTree response missing sha');
159
- return j.sha;
160
- }
161
- async createCommit(message, parentSha, treeSha) {
162
- const body = JSON.stringify({ message, tree: treeSha, parents: [parentSha] });
163
- const res = await this._fetchWithRetry(`${this.baseUrl}/git/commits`, { method: 'POST', headers: this.headers, body }, 4, 300);
164
- const j = await res.json();
165
- if (!j.sha)
166
- throw new NonRetryableError('createCommit response missing sha');
167
- return j.sha;
168
- }
169
- async updateRef(ref, commitSha, force = false) {
170
- const body = JSON.stringify({ sha: commitSha, force });
171
- const res = await this._fetchWithRetry(`${this.baseUrl}/git/refs/${ref}`, { method: 'PATCH', headers: this.headers, body }, 4, 300);
172
- if (!res.ok) {
173
- const txt = await res.text().catch(() => '');
174
- throw new NonRetryableError(`updateRef failed: ${res.status} ${txt}`);
175
- }
176
- }
177
- }
178
- exports.GitHubAdapter = GitHubAdapter;
179
- exports.default = GitHubAdapter;
@@ -1,182 +0,0 @@
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.GitLabAdapter = void 0;
7
- const crypto_1 = __importDefault(require("crypto"));
8
- /**
9
- *
10
- */
11
- class GitLabAdapter {
12
- opts;
13
- baseUrl;
14
- headers;
15
- pendingActions = null;
16
- maxRetries = 3;
17
- baseBackoff = 300;
18
- /**
19
- * GitLabAdapter を初期化します。
20
- * @param {GLOpts} opts 設定オブジェクト
21
- */
22
- constructor(opts) {
23
- this.opts = opts;
24
- const host = opts.host || 'https://gitlab.com';
25
- this.baseUrl = `${host}/api/v4/projects/${encodeURIComponent(opts.projectId)}`;
26
- this.headers = { 'PRIVATE-TOKEN': opts.token, 'Content-Type': 'application/json' };
27
- }
28
- /**
29
- * コンテンツから sha1 を算出します。
30
- * @param {string} content コンテンツ
31
- * @returns {string} sha1 ハッシュ
32
- */
33
- shaOf(content) {
34
- return crypto_1.default.createHash('sha1').update(content, 'utf8').digest('hex');
35
- }
36
- /**
37
- * 変更一覧から blob sha のマップを作成します(疑似実装)。
38
- * @param {any[]} changes 変更一覧
39
- * @returns {Promise<Record<string,string>>} path->sha マップ
40
- */
41
- async createBlobs(changes) {
42
- const map = {};
43
- for (const c of changes) {
44
- if (c.type === 'create' || c.type === 'update')
45
- map[c.path] = this.shaOf(c.content);
46
- }
47
- return map;
48
- }
49
- /**
50
- * 互換用のツリー作成。実際には actions を保持しておき、マーカーを返します。
51
- * @param {any[]} _changes 変更一覧
52
- * @param {string} [_baseTreeSha] ベースツリー(未使用)
53
- * @returns {Promise<string>} マーカー文字列
54
- */
55
- async createTree(_changes, _baseTreeSha) {
56
- // Store actions for later commit; return marker token
57
- const actions = (_changes || []).map((c) => {
58
- if (c.type === 'delete')
59
- return { action: 'delete', file_path: c.path };
60
- if (c.type === 'create')
61
- return { action: 'create', file_path: c.path, content: c.content };
62
- return { action: 'update', file_path: c.path, content: c.content };
63
- });
64
- this.pendingActions = actions;
65
- return `gitlab-tree-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
66
- }
67
- /**
68
- * createTree で保持した actions があればコミットし、なければ parentSha を返します。
69
- * @param {string} message コミットメッセージ
70
- * @param {string} parentSha 親コミット SHA
71
- * @param {string} _treeSha ツリー SHA(未使用)
72
- * @returns {Promise<string>} 新規コミット SHA または parentSha
73
- */
74
- async createCommit(message, parentSha, _treeSha) {
75
- // If pendingActions exist (created via createTree), use commits API
76
- if (this.pendingActions && this.pendingActions.length > 0) {
77
- const branch = 'main';
78
- const res = await this.createCommitWithActions(branch, message, this.pendingActions.map((a) => ({ type: a.action === 'delete' ? 'delete' : a.action === 'create' ? 'create' : 'update', path: a.file_path, content: a.content })));
79
- this.pendingActions = null;
80
- return res;
81
- }
82
- // Fallback: no-op commit (return parentSha)
83
- return parentSha;
84
- }
85
- /**
86
- * actions を用いて GitLab のコミット API を呼び出します。
87
- * @param {string} branch ブランチ名
88
- * @param {string} message コミットメッセージ
89
- * @param {{type:string,path:string,content?:string}[]} changes 変更一覧
90
- * @returns {Promise<any>} コミット応答(id など)
91
- */
92
- async createCommitWithActions(branch, message, changes) {
93
- const url = `${this.baseUrl}/repository/commits`;
94
- const actions = changes.map((c) => {
95
- if (c.type === 'delete')
96
- return { action: 'delete', file_path: c.path };
97
- if (c.type === 'create')
98
- return { action: 'create', file_path: c.path, content: c.content };
99
- return { action: 'update', file_path: c.path, content: c.content };
100
- });
101
- /**
102
- * GitLab のコミット API を呼び出します。
103
- * @param {string} branch ブランチ名
104
- * @param {string} message コミットメッセージ
105
- * @param {{type:string,path:string,content?:string}[]} changes 変更一覧
106
- * @returns {Promise<any>} 作成されたコミットの識別子
107
- */
108
- const body = JSON.stringify({ branch, commit_message: message, actions });
109
- const res = await this.fetchWithRetry(url, { method: 'POST', headers: this.headers, body });
110
- const text = await res.text().catch(() => '');
111
- let j = null;
112
- try {
113
- j = text ? JSON.parse(text) : null;
114
- }
115
- catch (err) {
116
- throw new Error(`GitLab commit invalid JSON response: ${text}`);
117
- }
118
- // validate expected fields (GitLab returns 'id' for commit id)
119
- if (!j || (!j.id && !j.commit)) {
120
- throw new Error(`GitLab commit unexpected response: ${JSON.stringify(j)}`);
121
- }
122
- return j.id || j.commit || j;
123
- }
124
- /**
125
- * fetch をリトライ付きで実行します。
126
- * @param {string} url リクエスト URL
127
- * @param {RequestInit} opts fetch オプション
128
- * @param {number} [retries] 最大リトライ回数
129
- * @returns {Promise<Response>} レスポンス
130
- */
131
- async fetchWithRetry(url, opts, retries = this.maxRetries) {
132
- for (let attempt = 1; attempt <= retries; attempt++) {
133
- try {
134
- const res = await fetch(url, opts);
135
- if (!this.isRetryableStatus(res.status))
136
- return res;
137
- if (attempt === retries)
138
- return res;
139
- const wait = this.backoffMs(attempt);
140
- await new Promise((r) => setTimeout(r, wait));
141
- }
142
- catch (err) {
143
- if (attempt === retries)
144
- throw err;
145
- const wait = this.backoffMs(attempt);
146
- await new Promise((r) => setTimeout(r, wait));
147
- }
148
- }
149
- // should not reach here
150
- throw new Error('fetchWithRetry: unexpected exit');
151
- }
152
- /**
153
- * ステータスが再試行対象か判定します。
154
- * @param {number} status ステータスコード
155
- * @returns {boolean}
156
- */
157
- isRetryableStatus(status) {
158
- return status === 429 || (status >= 500 && status < 600);
159
- }
160
- /**
161
- * バックオフ時間を計算します。
162
- * @param {number} attempt 試行回数(1..)
163
- * @returns {number} ミリ秒
164
- */
165
- backoffMs(attempt) {
166
- const base = this.baseBackoff * Math.pow(2, attempt - 1);
167
- const jitter = Math.floor(Math.random() * base * 0.3);
168
- return base + jitter;
169
- }
170
- /**
171
- * リファレンス更新は不要なため noop 実装です。
172
- * @param {string} _ref ref 名
173
- * @param {string} _commitSha コミット SHA
174
- * @param {boolean} [_force]
175
- * @returns {Promise<void>}
176
- */
177
- async updateRef(_ref, _commitSha, _force = false) {
178
- // Not required when using commits API
179
- }
180
- }
181
- exports.GitLabAdapter = GitLabAdapter;
182
- exports.default = GitLabAdapter;
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=github.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"github.spec.d.ts","sourceRoot":"","sources":["../../../test/e2e/github.spec.ts"],"names":[],"mappings":""}
@@ -1,47 +0,0 @@
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
- const test_1 = require("@playwright/test");
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
- // runtime: load built library from dist (compiled tests live under dist/test/e2e)
10
- // @ts-ignore: require of built JS module for e2e runtime
11
- const { GitHubAdapter } = require('../../index.js');
12
- const cfgPath = path_1.default.join(__dirname, 'github.config.json');
13
- (0, test_1.test)('github adapter smoke (skips if config missing)', async () => {
14
- if (!fs_1.default.existsSync(cfgPath)) {
15
- test_1.test.skip(true, 'github.config.json not found — fill test/e2e/github.config.json based on example');
16
- return;
17
- }
18
- const raw = fs_1.default.readFileSync(cfgPath, 'utf8');
19
- const cfg = JSON.parse(raw);
20
- if (!cfg.token || cfg.token.startsWith('ghp_replace')) {
21
- test_1.test.skip(true, 'github token not set in config');
22
- return;
23
- }
24
- const adapter = new GitHubAdapter({ owner: cfg.owner, repo: cfg.repo, token: cfg.token });
25
- // simple smoke: create a small blob and then delete it via a simulated tree/commit flow
26
- const tmpPath = `e2e-test-${Date.now()}.txt`;
27
- const changes = [{ type: 'create', path: tmpPath, content: 'playwright test content' }];
28
- const blobMap = await adapter.createBlobs(changes, 2);
29
- (0, test_1.expect)(blobMap[tmpPath]).toBeTruthy();
30
- // create tree and commit require a parent SHA — attempt to get default branch ref
31
- // fetch default branch ref
32
- const res = await fetch(`https://api.github.com/repos/${cfg.owner}/${cfg.repo}`, { headers: { Authorization: `token ${cfg.token}`, Accept: 'application/vnd.github+json' } });
33
- (0, test_1.expect)(res.ok).toBeTruthy();
34
- const repoInfo = await res.json();
35
- const defaultBranch = repoInfo.default_branch || 'main';
36
- // get commit sha of default branch
37
- const refRes = await fetch(`https://api.github.com/repos/${cfg.owner}/${cfg.repo}/git/ref/heads/${defaultBranch}`, { headers: { Authorization: `token ${cfg.token}`, Accept: 'application/vnd.github+json' } });
38
- (0, test_1.expect)(refRes.ok).toBeTruthy();
39
- const refInfo = await refRes.json();
40
- const parentSha = refInfo.object.sha;
41
- const treeSha = await adapter.createTree([{ ...changes[0], blobSha: blobMap[tmpPath] }], undefined);
42
- (0, test_1.expect)(treeSha).toBeTruthy();
43
- const commitSha = await adapter.createCommit('playwright e2e test', parentSha, treeSha);
44
- (0, test_1.expect)(commitSha).toBeTruthy();
45
- // cleanup: attempt to update ref back to parentSha (do not force)
46
- await adapter.updateRef(`heads/${defaultBranch}`, commitSha, false);
47
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=gitlab.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gitlab.spec.d.ts","sourceRoot":"","sources":["../../../test/e2e/gitlab.spec.ts"],"names":[],"mappings":""}
@@ -1,34 +0,0 @@
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
- const test_1 = require("@playwright/test");
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
- // runtime: load built library from dist (compiled tests live under dist/test/e2e)
10
- // @ts-ignore: require of built JS module for e2e runtime
11
- const { GitLabAdapter } = require('../../index.js');
12
- const cfgPath = path_1.default.join(__dirname, 'gitlab.config.json');
13
- (0, test_1.test)('gitlab adapter smoke (skips if config missing)', async () => {
14
- if (!fs_1.default.existsSync(cfgPath)) {
15
- test_1.test.skip(true, 'gitlab.config.json not found — fill test/e2e/gitlab.config.json based on example');
16
- return;
17
- }
18
- const raw = fs_1.default.readFileSync(cfgPath, 'utf8');
19
- const cfg = JSON.parse(raw);
20
- if (!cfg.token || cfg.token.startsWith('glpat_replace')) {
21
- test_1.test.skip(true, 'gitlab token not set in config');
22
- return;
23
- }
24
- const adapter = new GitLabAdapter({ projectId: cfg.projectId, token: cfg.token, host: cfg.host });
25
- const tmpPath = `e2e-gitlab-${Date.now()}.txt`;
26
- const changes = [{ type: 'create', path: tmpPath, content: 'gitlab e2e test content' }];
27
- // create commit that adds the file
28
- const branch = 'main'; // default; could be parameterized
29
- const commit = await adapter.createCommitWithActions(branch, 'playwright gitlab e2e create', changes);
30
- (0, test_1.expect)(commit).toBeTruthy();
31
- // cleanup: delete the file in another commit
32
- const del = await adapter.createCommitWithActions(branch, 'playwright gitlab e2e delete', [{ type: 'delete', path: tmpPath }]);
33
- (0, test_1.expect)(del).toBeTruthy();
34
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=virtualfs.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"virtualfs.spec.d.ts","sourceRoot":"","sources":["../../../test/e2e/virtualfs.spec.ts"],"names":[],"mappings":""}