@oh-my-ghaad/gitlab 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Chris Griffing
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @oh-my-ghaad/adapter-github
2
+
3
+ GitHub adapter for Oh My GHAAD, providing GitHub-specific implementation for using GitHub repositories as JSON databases.
4
+
5
+ ## Overview
6
+
7
+ The GitHub adapter implements the core adapter interface to provide:
8
+ - GitHub repository access
9
+ - File operations (create, read, update, delete)
10
+ - OAuth integration
11
+ - Repository installation handling
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @oh-my-ghaad/adapter-github @oh-my-ghaad/core
17
+ # or
18
+ yarn add @oh-my-ghaad/adapter-github @oh-my-ghaad/core
19
+ # or
20
+ pnpm add @oh-my-ghaad/adapter-github @oh-my-ghaad/core
21
+ ```
22
+
23
+ ## Basic Usage
24
+
25
+ ```typescript
26
+ import { Engine } from '@oh-my-ghaad/core';
27
+ import { GithubAdapter } from '@oh-my-ghaad/adapter-github';
28
+
29
+ // Create the engine instance with the GitHub adapter
30
+ const engine = new Engine({
31
+ adapters: [
32
+ new GithubAdapter({
33
+ clientId: "YOUR_GITHUB_CLIENT_ID",
34
+ redirectUri: process.env.YOUR_GITHUB_REDIRECT_URI!,
35
+ accessManagementUrl:
36
+ "https://github.com/apps/[YOUR_GITHUB_APP_NAME]/installations/new",
37
+ }),
38
+ ],
39
+ collections: [/* your collections */],
40
+ appConfig: {
41
+ persisted: true
42
+ }
43
+ });
44
+ ```
45
+
46
+ ## Configuration
47
+
48
+ The GitHub adapter requires the following configuration:
49
+
50
+ - `clientId`: The client ID of your GitHub app
51
+ - `redirectUri`: The redirect URI of your GitHub app
52
+ - `accessManagementUrl`: The URL to the access management page of your GitHub app
53
+
54
+ ## OAUTH
55
+
56
+ You will need to set up an OAuth app on GitHub. You will also need to set up a backend server to handle the OAuth flow since GitHub does not allow the implicit flow for OAuth apps.
57
+
58
+ See GitHub's [OAuth App documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) for more details.
59
+
60
+ ### Example Server Setup
61
+
62
+ ```typescript
63
+ export async function POST(request: Request) {
64
+ const { code } = await request.json();
65
+
66
+ if (!code) {
67
+ return new Response("Missing code", { status: 400 });
68
+ }
69
+
70
+ const response = await fetch("https://github.com/login/oauth/access_token", {
71
+ method: "POST",
72
+ headers: {
73
+ Accept: "application/json",
74
+ "Content-Type": "application/json",
75
+ },
76
+ body: JSON.stringify({
77
+ client_id: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID!,
78
+ client_secret: process.env.GITHUB_CLIENT_SECRET!,
79
+ code: code,
80
+ redirect_uri: process.env.NEXT_PUBLIC_GITHUB_REDIRECT_URI!,
81
+ }),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ return new Response("Failed to fetch token", { status: 500 });
86
+ }
87
+
88
+ const responseJson = await response.json();
89
+
90
+ if (!responseJson.access_token) {
91
+ return new Response("Failed to fetch token", { status: 500 });
92
+ }
93
+
94
+ return new Response(JSON.stringify(responseJson));
95
+ }
96
+
97
+ ```
98
+
99
+
100
+ ## Related Packages
101
+
102
+ - [@oh-my-ghaad/core](../core/README.md) - Core functionality
103
+ - [@oh-my-ghaad/react](../react/README.md) - React integration
104
+
105
+ For more examples and detailed usage, see the [main README](../../README.md) and the [demo app](../../apps/demo-app/README.md).
package/dist/index.cjs ADDED
@@ -0,0 +1,320 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ GitlabAdapter: () => GitlabAdapter
23
+ });
24
+ module.exports = __toCommonJS(index_exports);
25
+
26
+ // src/adapter.ts
27
+ var import_core = require("@oh-my-ghaad/core");
28
+ var import_rest = require("@gitbeaker/rest");
29
+
30
+ // src/utils.ts
31
+ function base64ToUtf8(base64String) {
32
+ try {
33
+ const byteCharacters = atob(base64String);
34
+ const byteNumbers = new Array(byteCharacters.length);
35
+ for (let i = 0; i < byteCharacters.length; i++) {
36
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
37
+ }
38
+ const byteArray = new Uint8Array(byteNumbers);
39
+ const decoder = new TextDecoder("utf-8");
40
+ return decoder.decode(byteArray);
41
+ } catch (e) {
42
+ console.error("Error decoding base64 with TextDecoder:", e);
43
+ return null;
44
+ }
45
+ }
46
+ function utf8ToBase64(utf8String) {
47
+ try {
48
+ const encoder = new TextEncoder();
49
+ const byteArray = encoder.encode(utf8String);
50
+ return btoa(String.fromCharCode(...byteArray));
51
+ } catch (e) {
52
+ console.error("Error encoding string as base64:", e);
53
+ return null;
54
+ }
55
+ }
56
+
57
+ // src/adapter.ts
58
+ var GitlabAdapter = class extends import_core.Adapter {
59
+ name = "GitLab";
60
+ icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
61
+ primaryColor = "#FC6D26";
62
+ secondaryColor = "#000000";
63
+ oauthUrl = "https://gitlab.com/oauth/authorize";
64
+ baseUrl = "https://gitlab.com";
65
+ api;
66
+ constructor(props) {
67
+ super(props);
68
+ this.api = new import_rest.Gitlab({
69
+ host: this.baseUrl,
70
+ oauthToken: this.token
71
+ });
72
+ }
73
+ setToken(token) {
74
+ super.setToken(token);
75
+ this.api = new import_rest.Gitlab({
76
+ host: this.baseUrl,
77
+ oauthToken: this.token
78
+ });
79
+ }
80
+ async fetchRepositories() {
81
+ const repositoriesOrError = await this.api.Projects.all({
82
+ membership: true,
83
+ perPage: 100
84
+ }).catch((error) => {
85
+ console.error(error);
86
+ return error;
87
+ });
88
+ if (repositoriesOrError instanceof Error) {
89
+ if (repositoriesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
90
+ this.unauthorizedHandler();
91
+ }
92
+ throw new Error("Failed to fetch projects");
93
+ }
94
+ const repositories = repositoriesOrError.map((repository) => ({
95
+ id: repository.id,
96
+ org: repository.namespace.path,
97
+ name: repository.path,
98
+ url: repository.web_url
99
+ }));
100
+ return repositories.flat();
101
+ }
102
+ async fetchFile(path) {
103
+ const fileOrError = await this.api.RepositoryFiles.show(
104
+ this.repo,
105
+ path,
106
+ "HEAD"
107
+ ).catch((error) => {
108
+ console.error(error);
109
+ return error;
110
+ });
111
+ if (fileOrError instanceof Error) {
112
+ if (fileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
113
+ this.unauthorizedHandler();
114
+ }
115
+ throw new Error("Failed to fetch file");
116
+ }
117
+ if (!fileOrError.content) {
118
+ throw new Error("Failed to fetch file");
119
+ }
120
+ const content = base64ToUtf8(fileOrError.content);
121
+ if (!content) {
122
+ console.error("Failed to decode file");
123
+ }
124
+ return content || "";
125
+ }
126
+ async fetchDirectory(directoryPath) {
127
+ const filesOrError = await this.api.Repositories.allRepositoryTrees(
128
+ this.repo,
129
+ {
130
+ path: directoryPath
131
+ }
132
+ ).catch((error) => {
133
+ console.error(error);
134
+ return error;
135
+ });
136
+ if (filesOrError instanceof Error) {
137
+ if (filesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
138
+ this.unauthorizedHandler();
139
+ }
140
+ throw new Error("Failed to fetch directory");
141
+ }
142
+ if (!Array.isArray(filesOrError)) {
143
+ throw new Error("Path is not a directory");
144
+ }
145
+ return Promise.all(
146
+ filesOrError.filter((file) => file.path.endsWith(".json")).map((file) => {
147
+ return this.fetchFile(file.path);
148
+ })
149
+ );
150
+ }
151
+ async createFile(path, content, message) {
152
+ const branchesOrError = await this.api.Branches.all(this.repo).catch(
153
+ (error) => {
154
+ console.error(error);
155
+ return error;
156
+ }
157
+ );
158
+ if (branchesOrError instanceof Error) {
159
+ if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
160
+ this.unauthorizedHandler();
161
+ }
162
+ throw new Error("Failed to fetch default branch");
163
+ }
164
+ const branch = branchesOrError.find((branch2) => branch2.default);
165
+ if (!branch) {
166
+ throw new Error("Failed to find default branch");
167
+ }
168
+ const existingFileOrError = await this.api.RepositoryFiles.show(
169
+ this.repo,
170
+ path,
171
+ branch.name
172
+ ).catch((error) => {
173
+ console.error(error);
174
+ return error;
175
+ });
176
+ if (existingFileOrError instanceof Error) {
177
+ if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
178
+ this.unauthorizedHandler();
179
+ }
180
+ throw new Error("Failed to fetch existing file");
181
+ }
182
+ if (existingFileOrError) {
183
+ throw new Error("File already exists");
184
+ }
185
+ const createdFileOrError = await this.api.Commits.create(
186
+ this.repo,
187
+ branch.name,
188
+ message || `Create file: ${path}`,
189
+ [
190
+ {
191
+ action: "create",
192
+ filePath: path,
193
+ content: utf8ToBase64(content),
194
+ encoding: "base64"
195
+ }
196
+ ]
197
+ ).catch((error) => {
198
+ console.error(error);
199
+ return error;
200
+ });
201
+ if (createdFileOrError instanceof Error) {
202
+ if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
203
+ this.unauthorizedHandler();
204
+ }
205
+ throw new Error("Failed to create file");
206
+ }
207
+ return;
208
+ }
209
+ async updateFile(path, content, message) {
210
+ const branchesOrError = await this.api.Branches.all(this.repo).catch(
211
+ (error) => {
212
+ console.error(error);
213
+ return error;
214
+ }
215
+ );
216
+ if (branchesOrError instanceof Error) {
217
+ if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
218
+ this.unauthorizedHandler();
219
+ }
220
+ throw new Error("Failed to fetch default branch");
221
+ }
222
+ const branch = branchesOrError.find((branch2) => branch2.default);
223
+ if (!branch) {
224
+ throw new Error("Failed to find default branch");
225
+ }
226
+ const existingFileOrError = await this.api.RepositoryFiles.show(
227
+ this.repo,
228
+ path,
229
+ branch.name
230
+ ).catch((error) => {
231
+ console.error(error);
232
+ return error;
233
+ });
234
+ if (existingFileOrError instanceof Error) {
235
+ if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
236
+ this.unauthorizedHandler();
237
+ }
238
+ throw new Error("Failed to fetch existing file");
239
+ }
240
+ if (!existingFileOrError) {
241
+ throw new Error("File does not exist");
242
+ }
243
+ const createdFileOrError = await this.api.Commits.create(
244
+ this.repo,
245
+ branch.name,
246
+ message || `Create file: ${path}`,
247
+ [
248
+ {
249
+ action: "update",
250
+ filePath: path,
251
+ content: utf8ToBase64(content),
252
+ encoding: "base64"
253
+ }
254
+ ]
255
+ ).catch((error) => {
256
+ console.error(error);
257
+ return error;
258
+ });
259
+ if (createdFileOrError instanceof Error) {
260
+ if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
261
+ this.unauthorizedHandler();
262
+ }
263
+ throw new Error("Failed to create file");
264
+ }
265
+ return;
266
+ }
267
+ async deleteFile(path, message) {
268
+ const responseOrError = await this.api.RepositoryFiles.remove(
269
+ this.repo,
270
+ path,
271
+ "HEAD",
272
+ message || `Delete file: ${path}`
273
+ ).catch((error) => {
274
+ console.error(error);
275
+ return error;
276
+ });
277
+ if (responseOrError instanceof Error) {
278
+ if (responseOrError.cause.response.status === 401 && this.unauthorizedHandler) {
279
+ this.unauthorizedHandler();
280
+ }
281
+ throw new Error("Failed to delete file");
282
+ }
283
+ return;
284
+ }
285
+ // TODO: Why did I add this method?
286
+ async createCommit({}) {
287
+ return;
288
+ }
289
+ // -----------------------------------------------------------------------------
290
+ // TODO: Pull Requests and File History are not yet implemented or used in the demo app yet
291
+ // -----------------------------------------------------------------------------
292
+ async fetchFileHistory(path) {
293
+ return [];
294
+ }
295
+ async fetchPullRequests() {
296
+ return [];
297
+ }
298
+ async createPullRequest() {
299
+ return;
300
+ }
301
+ async fetchPullRequest() {
302
+ return {
303
+ id: "",
304
+ title: "",
305
+ description: "",
306
+ commitSha: "",
307
+ createdAt: ""
308
+ };
309
+ }
310
+ async updatePullRequest() {
311
+ return;
312
+ }
313
+ async deletePullRequest() {
314
+ return;
315
+ }
316
+ };
317
+ // Annotate the CommonJS export names for ESM import in node:
318
+ 0 && (module.exports = {
319
+ GitlabAdapter
320
+ });
@@ -0,0 +1,39 @@
1
+ import { Adapter, IAdapter, CommitRequest } from '@oh-my-ghaad/core';
2
+
3
+ declare class GitlabAdapter extends Adapter implements IAdapter {
4
+ readonly name = "GitLab";
5
+ readonly icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
6
+ readonly primaryColor = "#FC6D26";
7
+ readonly secondaryColor = "#000000";
8
+ readonly oauthUrl = "https://gitlab.com/oauth/authorize";
9
+ readonly baseUrl = "https://gitlab.com";
10
+ private api;
11
+ constructor(props: any);
12
+ setToken(token: string | null): void;
13
+ fetchRepositories(): Promise<{
14
+ id: number;
15
+ org: string;
16
+ name: string;
17
+ url: string;
18
+ }[]>;
19
+ fetchFile(path: string): Promise<string>;
20
+ fetchDirectory(directoryPath: string): Promise<string[]>;
21
+ createFile(path: string, content: string, message?: string): Promise<void>;
22
+ updateFile(path: string, content: string, message?: string): Promise<void>;
23
+ deleteFile(path: string, message?: string): Promise<void>;
24
+ createCommit({}: CommitRequest): Promise<void>;
25
+ fetchFileHistory(path: string): Promise<any[]>;
26
+ fetchPullRequests(): Promise<any[]>;
27
+ createPullRequest(): Promise<void>;
28
+ fetchPullRequest(): Promise<{
29
+ id: string;
30
+ title: string;
31
+ description: string;
32
+ commitSha: string;
33
+ createdAt: string;
34
+ }>;
35
+ updatePullRequest(): Promise<void>;
36
+ deletePullRequest(): Promise<void>;
37
+ }
38
+
39
+ export { GitlabAdapter };
@@ -0,0 +1,39 @@
1
+ import { Adapter, IAdapter, CommitRequest } from '@oh-my-ghaad/core';
2
+
3
+ declare class GitlabAdapter extends Adapter implements IAdapter {
4
+ readonly name = "GitLab";
5
+ readonly icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
6
+ readonly primaryColor = "#FC6D26";
7
+ readonly secondaryColor = "#000000";
8
+ readonly oauthUrl = "https://gitlab.com/oauth/authorize";
9
+ readonly baseUrl = "https://gitlab.com";
10
+ private api;
11
+ constructor(props: any);
12
+ setToken(token: string | null): void;
13
+ fetchRepositories(): Promise<{
14
+ id: number;
15
+ org: string;
16
+ name: string;
17
+ url: string;
18
+ }[]>;
19
+ fetchFile(path: string): Promise<string>;
20
+ fetchDirectory(directoryPath: string): Promise<string[]>;
21
+ createFile(path: string, content: string, message?: string): Promise<void>;
22
+ updateFile(path: string, content: string, message?: string): Promise<void>;
23
+ deleteFile(path: string, message?: string): Promise<void>;
24
+ createCommit({}: CommitRequest): Promise<void>;
25
+ fetchFileHistory(path: string): Promise<any[]>;
26
+ fetchPullRequests(): Promise<any[]>;
27
+ createPullRequest(): Promise<void>;
28
+ fetchPullRequest(): Promise<{
29
+ id: string;
30
+ title: string;
31
+ description: string;
32
+ commitSha: string;
33
+ createdAt: string;
34
+ }>;
35
+ updatePullRequest(): Promise<void>;
36
+ deletePullRequest(): Promise<void>;
37
+ }
38
+
39
+ export { GitlabAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,294 @@
1
+ // src/adapter.ts
2
+ import { Adapter } from "@oh-my-ghaad/core";
3
+ import { Gitlab } from "@gitbeaker/rest";
4
+
5
+ // src/utils.ts
6
+ function base64ToUtf8(base64String) {
7
+ try {
8
+ const byteCharacters = atob(base64String);
9
+ const byteNumbers = new Array(byteCharacters.length);
10
+ for (let i = 0; i < byteCharacters.length; i++) {
11
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
12
+ }
13
+ const byteArray = new Uint8Array(byteNumbers);
14
+ const decoder = new TextDecoder("utf-8");
15
+ return decoder.decode(byteArray);
16
+ } catch (e) {
17
+ console.error("Error decoding base64 with TextDecoder:", e);
18
+ return null;
19
+ }
20
+ }
21
+ function utf8ToBase64(utf8String) {
22
+ try {
23
+ const encoder = new TextEncoder();
24
+ const byteArray = encoder.encode(utf8String);
25
+ return btoa(String.fromCharCode(...byteArray));
26
+ } catch (e) {
27
+ console.error("Error encoding string as base64:", e);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // src/adapter.ts
33
+ var GitlabAdapter = class extends Adapter {
34
+ name = "GitLab";
35
+ icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
36
+ primaryColor = "#FC6D26";
37
+ secondaryColor = "#000000";
38
+ oauthUrl = "https://gitlab.com/oauth/authorize";
39
+ baseUrl = "https://gitlab.com";
40
+ api;
41
+ constructor(props) {
42
+ super(props);
43
+ this.api = new Gitlab({
44
+ host: this.baseUrl,
45
+ oauthToken: this.token
46
+ });
47
+ }
48
+ setToken(token) {
49
+ super.setToken(token);
50
+ this.api = new Gitlab({
51
+ host: this.baseUrl,
52
+ oauthToken: this.token
53
+ });
54
+ }
55
+ async fetchRepositories() {
56
+ const repositoriesOrError = await this.api.Projects.all({
57
+ membership: true,
58
+ perPage: 100
59
+ }).catch((error) => {
60
+ console.error(error);
61
+ return error;
62
+ });
63
+ if (repositoriesOrError instanceof Error) {
64
+ if (repositoriesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
65
+ this.unauthorizedHandler();
66
+ }
67
+ throw new Error("Failed to fetch projects");
68
+ }
69
+ const repositories = repositoriesOrError.map((repository) => ({
70
+ id: repository.id,
71
+ org: repository.namespace.path,
72
+ name: repository.path,
73
+ url: repository.web_url
74
+ }));
75
+ return repositories.flat();
76
+ }
77
+ async fetchFile(path) {
78
+ const fileOrError = await this.api.RepositoryFiles.show(
79
+ this.repo,
80
+ path,
81
+ "HEAD"
82
+ ).catch((error) => {
83
+ console.error(error);
84
+ return error;
85
+ });
86
+ if (fileOrError instanceof Error) {
87
+ if (fileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
88
+ this.unauthorizedHandler();
89
+ }
90
+ throw new Error("Failed to fetch file");
91
+ }
92
+ if (!fileOrError.content) {
93
+ throw new Error("Failed to fetch file");
94
+ }
95
+ const content = base64ToUtf8(fileOrError.content);
96
+ if (!content) {
97
+ console.error("Failed to decode file");
98
+ }
99
+ return content || "";
100
+ }
101
+ async fetchDirectory(directoryPath) {
102
+ const filesOrError = await this.api.Repositories.allRepositoryTrees(
103
+ this.repo,
104
+ {
105
+ path: directoryPath
106
+ }
107
+ ).catch((error) => {
108
+ console.error(error);
109
+ return error;
110
+ });
111
+ if (filesOrError instanceof Error) {
112
+ if (filesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
113
+ this.unauthorizedHandler();
114
+ }
115
+ throw new Error("Failed to fetch directory");
116
+ }
117
+ if (!Array.isArray(filesOrError)) {
118
+ throw new Error("Path is not a directory");
119
+ }
120
+ return Promise.all(
121
+ filesOrError.filter((file) => file.path.endsWith(".json")).map((file) => {
122
+ return this.fetchFile(file.path);
123
+ })
124
+ );
125
+ }
126
+ async createFile(path, content, message) {
127
+ const branchesOrError = await this.api.Branches.all(this.repo).catch(
128
+ (error) => {
129
+ console.error(error);
130
+ return error;
131
+ }
132
+ );
133
+ if (branchesOrError instanceof Error) {
134
+ if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
135
+ this.unauthorizedHandler();
136
+ }
137
+ throw new Error("Failed to fetch default branch");
138
+ }
139
+ const branch = branchesOrError.find((branch2) => branch2.default);
140
+ if (!branch) {
141
+ throw new Error("Failed to find default branch");
142
+ }
143
+ const existingFileOrError = await this.api.RepositoryFiles.show(
144
+ this.repo,
145
+ path,
146
+ branch.name
147
+ ).catch((error) => {
148
+ console.error(error);
149
+ return error;
150
+ });
151
+ if (existingFileOrError instanceof Error) {
152
+ if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
153
+ this.unauthorizedHandler();
154
+ }
155
+ throw new Error("Failed to fetch existing file");
156
+ }
157
+ if (existingFileOrError) {
158
+ throw new Error("File already exists");
159
+ }
160
+ const createdFileOrError = await this.api.Commits.create(
161
+ this.repo,
162
+ branch.name,
163
+ message || `Create file: ${path}`,
164
+ [
165
+ {
166
+ action: "create",
167
+ filePath: path,
168
+ content: utf8ToBase64(content),
169
+ encoding: "base64"
170
+ }
171
+ ]
172
+ ).catch((error) => {
173
+ console.error(error);
174
+ return error;
175
+ });
176
+ if (createdFileOrError instanceof Error) {
177
+ if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
178
+ this.unauthorizedHandler();
179
+ }
180
+ throw new Error("Failed to create file");
181
+ }
182
+ return;
183
+ }
184
+ async updateFile(path, content, message) {
185
+ const branchesOrError = await this.api.Branches.all(this.repo).catch(
186
+ (error) => {
187
+ console.error(error);
188
+ return error;
189
+ }
190
+ );
191
+ if (branchesOrError instanceof Error) {
192
+ if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
193
+ this.unauthorizedHandler();
194
+ }
195
+ throw new Error("Failed to fetch default branch");
196
+ }
197
+ const branch = branchesOrError.find((branch2) => branch2.default);
198
+ if (!branch) {
199
+ throw new Error("Failed to find default branch");
200
+ }
201
+ const existingFileOrError = await this.api.RepositoryFiles.show(
202
+ this.repo,
203
+ path,
204
+ branch.name
205
+ ).catch((error) => {
206
+ console.error(error);
207
+ return error;
208
+ });
209
+ if (existingFileOrError instanceof Error) {
210
+ if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
211
+ this.unauthorizedHandler();
212
+ }
213
+ throw new Error("Failed to fetch existing file");
214
+ }
215
+ if (!existingFileOrError) {
216
+ throw new Error("File does not exist");
217
+ }
218
+ const createdFileOrError = await this.api.Commits.create(
219
+ this.repo,
220
+ branch.name,
221
+ message || `Create file: ${path}`,
222
+ [
223
+ {
224
+ action: "update",
225
+ filePath: path,
226
+ content: utf8ToBase64(content),
227
+ encoding: "base64"
228
+ }
229
+ ]
230
+ ).catch((error) => {
231
+ console.error(error);
232
+ return error;
233
+ });
234
+ if (createdFileOrError instanceof Error) {
235
+ if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
236
+ this.unauthorizedHandler();
237
+ }
238
+ throw new Error("Failed to create file");
239
+ }
240
+ return;
241
+ }
242
+ async deleteFile(path, message) {
243
+ const responseOrError = await this.api.RepositoryFiles.remove(
244
+ this.repo,
245
+ path,
246
+ "HEAD",
247
+ message || `Delete file: ${path}`
248
+ ).catch((error) => {
249
+ console.error(error);
250
+ return error;
251
+ });
252
+ if (responseOrError instanceof Error) {
253
+ if (responseOrError.cause.response.status === 401 && this.unauthorizedHandler) {
254
+ this.unauthorizedHandler();
255
+ }
256
+ throw new Error("Failed to delete file");
257
+ }
258
+ return;
259
+ }
260
+ // TODO: Why did I add this method?
261
+ async createCommit({}) {
262
+ return;
263
+ }
264
+ // -----------------------------------------------------------------------------
265
+ // TODO: Pull Requests and File History are not yet implemented or used in the demo app yet
266
+ // -----------------------------------------------------------------------------
267
+ async fetchFileHistory(path) {
268
+ return [];
269
+ }
270
+ async fetchPullRequests() {
271
+ return [];
272
+ }
273
+ async createPullRequest() {
274
+ return;
275
+ }
276
+ async fetchPullRequest() {
277
+ return {
278
+ id: "",
279
+ title: "",
280
+ description: "",
281
+ commitSha: "",
282
+ createdAt: ""
283
+ };
284
+ }
285
+ async updatePullRequest() {
286
+ return;
287
+ }
288
+ async deletePullRequest() {
289
+ return;
290
+ }
291
+ };
292
+ export {
293
+ GitlabAdapter
294
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@oh-my-ghaad/gitlab",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "types": "dist/index.d.ts",
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@gitbeaker/rest": "^43.5.0",
13
+ "@oh-my-ghaad/core": "0.0.1"
14
+ },
15
+ "devDependencies": {
16
+ "tsup": "^8.5.0",
17
+ "typescript": "^5.8.3"
18
+ },
19
+ "scripts": {
20
+ "dev": "tsup src/index.ts --watch --dts --dts-resolve --format esm,cjs",
21
+ "build": "tsup src/index.ts --dts --dts-resolve --format esm,cjs",
22
+ "prepublish": "pnpm run build",
23
+ "test": "echo \"Error: no test specified\" && exit 1"
24
+ }
25
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,346 @@
1
+ import { Adapter } from "@oh-my-ghaad/core";
2
+ import type { CommitRequest, IAdapter } from "@oh-my-ghaad/core";
3
+ import { GitbeakerRequestError, Gitlab } from "@gitbeaker/rest";
4
+ import { base64ToUtf8, utf8ToBase64 } from "./utils";
5
+ // import { base64ToUtf8, utf8ToBase64 } from "./utils";
6
+
7
+ // type EndpointPath = keyof Endpoints;
8
+
9
+ export class GitlabAdapter extends Adapter implements IAdapter {
10
+ readonly name = "GitLab";
11
+ readonly icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
12
+ readonly primaryColor = "#FC6D26";
13
+ readonly secondaryColor = "#000000";
14
+ readonly oauthUrl = "https://gitlab.com/oauth/authorize";
15
+ readonly baseUrl = "https://gitlab.com";
16
+
17
+ private api: InstanceType<typeof Gitlab>;
18
+
19
+ constructor(props) {
20
+ super(props);
21
+
22
+ this.api = new Gitlab({
23
+ host: this.baseUrl,
24
+ oauthToken: this.token,
25
+ });
26
+ }
27
+
28
+ setToken(token: string | null) {
29
+ super.setToken(token);
30
+
31
+ this.api = new Gitlab({
32
+ host: this.baseUrl,
33
+ oauthToken: this.token,
34
+ });
35
+ }
36
+
37
+ async fetchRepositories() {
38
+ const repositoriesOrError = await this.api.Projects.all({
39
+ membership: true,
40
+ perPage: 100,
41
+ }).catch((error: GitbeakerRequestError) => {
42
+ console.error(error);
43
+ return error;
44
+ });
45
+
46
+ if (repositoriesOrError instanceof Error) {
47
+ if (
48
+ repositoriesOrError.cause.response.status === 401 &&
49
+ this.unauthorizedHandler
50
+ ) {
51
+ this.unauthorizedHandler();
52
+ }
53
+ throw new Error("Failed to fetch projects");
54
+ }
55
+
56
+ const repositories = repositoriesOrError.map((repository) => ({
57
+ id: repository.id,
58
+ org: repository.namespace.path,
59
+ name: repository.path,
60
+ url: repository.web_url as string,
61
+ }));
62
+
63
+ return repositories.flat();
64
+ }
65
+
66
+ async fetchFile(path: string) {
67
+ const fileOrError = await this.api.RepositoryFiles.show(
68
+ this.repo,
69
+ path,
70
+ "HEAD"
71
+ ).catch((error: GitbeakerRequestError) => {
72
+ console.error(error);
73
+ return error;
74
+ });
75
+
76
+ if (fileOrError instanceof Error) {
77
+ if (
78
+ fileOrError.cause.response.status === 401 &&
79
+ this.unauthorizedHandler
80
+ ) {
81
+ this.unauthorizedHandler();
82
+ }
83
+ throw new Error("Failed to fetch file");
84
+ }
85
+
86
+ if (!fileOrError.content) {
87
+ throw new Error("Failed to fetch file");
88
+ }
89
+ const content = base64ToUtf8(fileOrError.content);
90
+ if (!content) {
91
+ console.error("Failed to decode file");
92
+ }
93
+
94
+ return content || "";
95
+ }
96
+
97
+ async fetchDirectory(directoryPath: string) {
98
+ const filesOrError = await this.api.Repositories.allRepositoryTrees(
99
+ this.repo,
100
+ {
101
+ path: directoryPath,
102
+ }
103
+ ).catch((error: GitbeakerRequestError) => {
104
+ console.error(error);
105
+ return error;
106
+ });
107
+
108
+ if (filesOrError instanceof Error) {
109
+ if (
110
+ filesOrError.cause.response.status === 401 &&
111
+ this.unauthorizedHandler
112
+ ) {
113
+ this.unauthorizedHandler();
114
+ }
115
+ throw new Error("Failed to fetch directory");
116
+ }
117
+
118
+ if (!Array.isArray(filesOrError)) {
119
+ throw new Error("Path is not a directory");
120
+ }
121
+
122
+ return Promise.all(
123
+ filesOrError
124
+ .filter((file) => file.path.endsWith(".json"))
125
+ .map((file) => {
126
+ return this.fetchFile(file.path);
127
+ })
128
+ );
129
+ }
130
+
131
+ async createFile(path: string, content: string, message?: string) {
132
+ const branchesOrError = await this.api.Branches.all(this.repo).catch(
133
+ (error: GitbeakerRequestError) => {
134
+ console.error(error);
135
+ return error;
136
+ }
137
+ );
138
+
139
+ if (branchesOrError instanceof Error) {
140
+ if (
141
+ branchesOrError.cause.response.status === 401 &&
142
+ this.unauthorizedHandler
143
+ ) {
144
+ this.unauthorizedHandler();
145
+ }
146
+ throw new Error("Failed to fetch default branch");
147
+ }
148
+
149
+ const branch = branchesOrError.find((branch) => branch.default);
150
+
151
+ if (!branch) {
152
+ throw new Error("Failed to find default branch");
153
+ }
154
+
155
+ const existingFileOrError = await this.api.RepositoryFiles.show(
156
+ this.repo,
157
+ path,
158
+ branch.name
159
+ ).catch((error: GitbeakerRequestError) => {
160
+ console.error(error);
161
+ return error;
162
+ });
163
+
164
+ if (existingFileOrError instanceof Error) {
165
+ if (
166
+ existingFileOrError.cause.response.status === 401 &&
167
+ this.unauthorizedHandler
168
+ ) {
169
+ this.unauthorizedHandler();
170
+ }
171
+ throw new Error("Failed to fetch existing file");
172
+ }
173
+
174
+ if (existingFileOrError) {
175
+ throw new Error("File already exists");
176
+ }
177
+
178
+ const createdFileOrError = await this.api.Commits.create(
179
+ this.repo,
180
+ branch.name,
181
+ message || `Create file: ${path}`,
182
+ [
183
+ {
184
+ action: "create",
185
+ filePath: path,
186
+ content: utf8ToBase64(content),
187
+ encoding: "base64",
188
+ },
189
+ ]
190
+ ).catch((error: GitbeakerRequestError) => {
191
+ console.error(error);
192
+ return error;
193
+ });
194
+
195
+ if (createdFileOrError instanceof Error) {
196
+ if (
197
+ createdFileOrError.cause.response.status === 401 &&
198
+ this.unauthorizedHandler
199
+ ) {
200
+ this.unauthorizedHandler();
201
+ }
202
+ throw new Error("Failed to create file");
203
+ }
204
+
205
+ return;
206
+ }
207
+
208
+ async updateFile(path: string, content: string, message?: string) {
209
+ const branchesOrError = await this.api.Branches.all(this.repo).catch(
210
+ (error: GitbeakerRequestError) => {
211
+ console.error(error);
212
+ return error;
213
+ }
214
+ );
215
+
216
+ if (branchesOrError instanceof Error) {
217
+ if (
218
+ branchesOrError.cause.response.status === 401 &&
219
+ this.unauthorizedHandler
220
+ ) {
221
+ this.unauthorizedHandler();
222
+ }
223
+ throw new Error("Failed to fetch default branch");
224
+ }
225
+
226
+ const branch = branchesOrError.find((branch) => branch.default);
227
+
228
+ if (!branch) {
229
+ throw new Error("Failed to find default branch");
230
+ }
231
+
232
+ const existingFileOrError = await this.api.RepositoryFiles.show(
233
+ this.repo,
234
+ path,
235
+ branch.name
236
+ ).catch((error: GitbeakerRequestError) => {
237
+ console.error(error);
238
+ return error;
239
+ });
240
+
241
+ if (existingFileOrError instanceof Error) {
242
+ if (
243
+ existingFileOrError.cause.response.status === 401 &&
244
+ this.unauthorizedHandler
245
+ ) {
246
+ this.unauthorizedHandler();
247
+ }
248
+ throw new Error("Failed to fetch existing file");
249
+ }
250
+
251
+ if (!existingFileOrError) {
252
+ throw new Error("File does not exist");
253
+ }
254
+
255
+ const createdFileOrError = await this.api.Commits.create(
256
+ this.repo,
257
+ branch.name,
258
+ message || `Create file: ${path}`,
259
+ [
260
+ {
261
+ action: "update",
262
+ filePath: path,
263
+ content: utf8ToBase64(content),
264
+ encoding: "base64",
265
+ },
266
+ ]
267
+ ).catch((error: GitbeakerRequestError) => {
268
+ console.error(error);
269
+ return error;
270
+ });
271
+
272
+ if (createdFileOrError instanceof Error) {
273
+ if (
274
+ createdFileOrError.cause.response.status === 401 &&
275
+ this.unauthorizedHandler
276
+ ) {
277
+ this.unauthorizedHandler();
278
+ }
279
+ throw new Error("Failed to create file");
280
+ }
281
+
282
+ return;
283
+ }
284
+
285
+ async deleteFile(path: string, message?: string) {
286
+ const responseOrError = await this.api.RepositoryFiles.remove(
287
+ this.repo,
288
+ path,
289
+ "HEAD",
290
+ message || `Delete file: ${path}`
291
+ ).catch((error: GitbeakerRequestError) => {
292
+ console.error(error);
293
+ return error;
294
+ });
295
+
296
+ if (responseOrError instanceof Error) {
297
+ if (
298
+ responseOrError.cause.response.status === 401 &&
299
+ this.unauthorizedHandler
300
+ ) {
301
+ this.unauthorizedHandler();
302
+ }
303
+ throw new Error("Failed to delete file");
304
+ }
305
+
306
+ return;
307
+ }
308
+
309
+ // TODO: Why did I add this method?
310
+ async createCommit({}: CommitRequest) {
311
+ return;
312
+ }
313
+
314
+ // -----------------------------------------------------------------------------
315
+ // TODO: Pull Requests and File History are not yet implemented or used in the demo app yet
316
+ // -----------------------------------------------------------------------------
317
+ async fetchFileHistory(path: string) {
318
+ return [];
319
+ }
320
+
321
+ async fetchPullRequests() {
322
+ return [];
323
+ }
324
+
325
+ async createPullRequest() {
326
+ return;
327
+ }
328
+
329
+ async fetchPullRequest() {
330
+ return {
331
+ id: "",
332
+ title: "",
333
+ description: "",
334
+ commitSha: "",
335
+ createdAt: "",
336
+ };
337
+ }
338
+
339
+ async updatePullRequest() {
340
+ return;
341
+ }
342
+
343
+ async deletePullRequest() {
344
+ return;
345
+ }
346
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./adapter";
package/src/utils.ts ADDED
@@ -0,0 +1,32 @@
1
+ export function base64ToUtf8(base64String: string): string | null {
2
+ try {
3
+ // Decode base64 to a byte array (Uint8Array)
4
+ const byteCharacters = atob(base64String);
5
+ const byteNumbers = new Array(byteCharacters.length);
6
+ for (let i = 0; i < byteCharacters.length; i++) {
7
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
8
+ }
9
+ const byteArray = new Uint8Array(byteNumbers);
10
+
11
+ // Decode the byte array as UTF-8
12
+ const decoder = new TextDecoder("utf-8");
13
+ return decoder.decode(byteArray);
14
+ } catch (e) {
15
+ console.error("Error decoding base64 with TextDecoder:", e);
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export function utf8ToBase64(utf8String: string): string | null {
21
+ try {
22
+ // Encode the string as UTF-8
23
+ const encoder = new TextEncoder();
24
+ const byteArray = encoder.encode(utf8String);
25
+
26
+ // Convert the byte array to base64 and return it
27
+ return btoa(String.fromCharCode(...byteArray));
28
+ } catch (e) {
29
+ console.error("Error encoding string as base64:", e);
30
+ return null;
31
+ }
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ }
7
+ }