@foundation0/git 1.0.0 → 1.2.0

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 (115) hide show
  1. package/{packages/git/mcp → mcp}/cli.mjs +0 -0
  2. package/package.json +43 -13
  3. package/.codex.example/config.toml +0 -10
  4. package/.env.example +0 -10
  5. package/packages/fs/README.md +0 -47
  6. package/packages/fs/node_modules/.bin/f0-git-mcp +0 -21
  7. package/packages/fs/node_modules/.bin/f0-git-mcp-server +0 -21
  8. package/packages/fs/node_modules/.bin/f0-git-mcp-server.CMD +0 -12
  9. package/packages/fs/node_modules/.bin/f0-git-mcp-server.ps1 +0 -41
  10. package/packages/fs/node_modules/.bin/f0-git-mcp.CMD +0 -12
  11. package/packages/fs/node_modules/.bin/f0-git-mcp.ps1 +0 -41
  12. package/packages/fs/node_modules/.bin/tsc +0 -21
  13. package/packages/fs/node_modules/.bin/tsc.CMD +0 -12
  14. package/packages/fs/node_modules/.bin/tsc.ps1 +0 -41
  15. package/packages/fs/node_modules/.bin/tsserver +0 -21
  16. package/packages/fs/node_modules/.bin/tsserver.CMD +0 -12
  17. package/packages/fs/node_modules/.bin/tsserver.ps1 +0 -41
  18. package/packages/fs/node_modules/.bin/vite +0 -21
  19. package/packages/fs/node_modules/.bin/vite.CMD +0 -12
  20. package/packages/fs/node_modules/.bin/vite.ps1 +0 -41
  21. package/packages/fs/node_modules/.bin/vitest +0 -21
  22. package/packages/fs/node_modules/.bin/vitest.CMD +0 -12
  23. package/packages/fs/node_modules/.bin/vitest.ps1 +0 -41
  24. package/packages/fs/package.json +0 -28
  25. package/packages/fs/src/cli.ts +0 -74
  26. package/packages/fs/src/git-fs.ts +0 -705
  27. package/packages/fs/src/index.ts +0 -33
  28. package/packages/fs/src/mount.ts +0 -297
  29. package/packages/fs/tsconfig.json +0 -7
  30. package/packages/git/mcp/tests/e2e/git-mcp-e2e.spec.ts +0 -157
  31. package/packages/git/mcp/tests/e2e/server.fixture.ts +0 -109
  32. package/packages/git/node_modules/.bin/tsc +0 -21
  33. package/packages/git/node_modules/.bin/tsc.CMD +0 -12
  34. package/packages/git/node_modules/.bin/tsc.ps1 +0 -41
  35. package/packages/git/node_modules/.bin/tsserver +0 -21
  36. package/packages/git/node_modules/.bin/tsserver.CMD +0 -12
  37. package/packages/git/node_modules/.bin/tsserver.ps1 +0 -41
  38. package/packages/git/node_modules/.bin/vite +0 -21
  39. package/packages/git/node_modules/.bin/vite.CMD +0 -12
  40. package/packages/git/node_modules/.bin/vite.ps1 +0 -41
  41. package/packages/git/node_modules/.bin/vitest +0 -21
  42. package/packages/git/node_modules/.bin/vitest.CMD +0 -12
  43. package/packages/git/node_modules/.bin/vitest.ps1 +0 -41
  44. package/packages/git/node_modules/.vite/vitest/results.json +0 -1
  45. package/packages/git/package.json +0 -60
  46. package/packages/git/scripts/create-issue.mjs +0 -93
  47. package/packages/git/scripts/extract-git-spec.mjs +0 -234
  48. package/packages/git/scripts/fetch-gitea-swagger.mjs +0 -22
  49. package/packages/git/tests/api.spec.ts +0 -55
  50. package/packages/git/tests/e2e/git-service-feature-e2e.spec.ts +0 -232
  51. package/packages/git/tests/git-service-api-object.spec.ts +0 -97
  52. package/packages/git/tests/git-service-feature-matrix.spec.ts +0 -182
  53. package/packages/git/tests/issue-dependencies.spec.ts +0 -81
  54. package/packages/git/tsconfig.json +0 -7
  55. package/packages/git/vitest.config.ts +0 -7
  56. package/packages/utils/package.json +0 -9
  57. package/packages/utils/src/awk.ts +0 -6
  58. package/packages/utils/src/cat.ts +0 -6
  59. package/packages/utils/src/cd.ts +0 -6
  60. package/packages/utils/src/chgrp.ts +0 -6
  61. package/packages/utils/src/chmod.ts +0 -6
  62. package/packages/utils/src/chown.ts +0 -6
  63. package/packages/utils/src/cp.ts +0 -6
  64. package/packages/utils/src/curl.ts +0 -6
  65. package/packages/utils/src/cut.ts +0 -6
  66. package/packages/utils/src/date.ts +0 -6
  67. package/packages/utils/src/echo.ts +0 -6
  68. package/packages/utils/src/find.ts +0 -6
  69. package/packages/utils/src/grep.ts +0 -6
  70. package/packages/utils/src/gunzip.ts +0 -6
  71. package/packages/utils/src/gzip.ts +0 -6
  72. package/packages/utils/src/head.ts +0 -6
  73. package/packages/utils/src/hostname.ts +0 -6
  74. package/packages/utils/src/index.ts +0 -37
  75. package/packages/utils/src/ls.ts +0 -6
  76. package/packages/utils/src/mkdir.ts +0 -6
  77. package/packages/utils/src/mv.ts +0 -6
  78. package/packages/utils/src/ping.ts +0 -6
  79. package/packages/utils/src/pwd.ts +0 -6
  80. package/packages/utils/src/rm.ts +0 -6
  81. package/packages/utils/src/rmdir.ts +0 -6
  82. package/packages/utils/src/sed.ts +0 -6
  83. package/packages/utils/src/sort.ts +0 -6
  84. package/packages/utils/src/tail.ts +0 -6
  85. package/packages/utils/src/tar.ts +0 -6
  86. package/packages/utils/src/touch.ts +0 -6
  87. package/packages/utils/src/tr.ts +0 -6
  88. package/packages/utils/src/uname.ts +0 -6
  89. package/packages/utils/src/uniq.ts +0 -6
  90. package/packages/utils/src/unzip.ts +0 -6
  91. package/packages/utils/src/util.ts +0 -4
  92. package/packages/utils/src/wc.ts +0 -6
  93. package/packages/utils/src/wget.ts +0 -6
  94. package/packages/utils/src/whoami.ts +0 -6
  95. package/packages/utils/src/zip.ts +0 -6
  96. package/pnpm-workspace.yaml +0 -2
  97. package/tsconfig.base.json +0 -12
  98. /package/{packages/git/README.md → README.md} +0 -0
  99. /package/{packages/git/gitea-swagger.json → gitea-swagger.json} +0 -0
  100. /package/{packages/git/mcp → mcp}/README.md +0 -0
  101. /package/{packages/git/mcp → mcp}/src/cli.ts +0 -0
  102. /package/{packages/git/mcp → mcp}/src/client.ts +0 -0
  103. /package/{packages/git/mcp → mcp}/src/index.ts +0 -0
  104. /package/{packages/git/mcp → mcp}/src/server.ts +0 -0
  105. /package/{packages/git/src → src}/api.ts +0 -0
  106. /package/{packages/git/src → src}/git-service-api.ts +0 -0
  107. /package/{packages/git/src → src}/git-service-feature-spec.generated.ts +0 -0
  108. /package/{packages/git/src → src}/index.ts +0 -0
  109. /package/{packages/git/src → src}/issue-dependencies.ts +0 -0
  110. /package/{packages/git/src → src}/platform/config.ts +0 -0
  111. /package/{packages/git/src → src}/platform/gitea-adapter.ts +0 -0
  112. /package/{packages/git/src → src}/platform/gitea-rules.ts +0 -0
  113. /package/{packages/git/src → src}/platform/index.ts +0 -0
  114. /package/{packages/git/src → src}/repository.ts +0 -0
  115. /package/{packages/git/src → src}/spec-mock.ts +0 -0
@@ -1,705 +0,0 @@
1
- import { randomUUID } from 'node:crypto'
2
- import {
3
- type GitServiceApi,
4
- type GitServiceApiExecutionResult,
5
- type GitServiceApiFactoryOptions,
6
- type GitServiceApiMethod,
7
- createGitServiceApi,
8
- } from '@foundation0/git'
9
-
10
- type GitFsQuery = Record<string, string | number | boolean>
11
- type WriteData = string | Uint8Array | ArrayBuffer
12
- type FileKind = 'file' | 'directory'
13
-
14
- export type GitFsOptions = Omit<GitServiceApiFactoryOptions, 'defaultOwner' | 'defaultRepo' | 'config'> & {
15
- api?: GitServiceApi
16
- config?: GitServiceApiFactoryOptions['config']
17
- defaultOwner?: string
18
- defaultRepo?: string
19
- defaultCommitMessage?: string
20
- }
21
-
22
- type ReadFileOptions = {
23
- encoding?: BufferEncoding
24
- ref?: string
25
- }
26
-
27
- type WriteFileOptions = {
28
- encoding?: BufferEncoding | 'base64'
29
- message?: string
30
- branch?: string
31
- author?: unknown
32
- committer?: unknown
33
- flag?: string
34
- overwrite?: boolean
35
- }
36
-
37
- type ReaddirOptions = {
38
- withFileTypes?: boolean
39
- ref?: string
40
- }
41
-
42
- type RmOptions = {
43
- recursive?: boolean
44
- force?: boolean
45
- }
46
-
47
- type GitFsError = NodeJS.ErrnoException
48
-
49
- export interface GitFsDirentLike {
50
- name: string
51
- isFile: () => boolean
52
- isDirectory: () => boolean
53
- isSymbolicLink: () => boolean
54
- isBlockDevice: () => boolean
55
- isCharacterDevice: () => boolean
56
- isFIFO: () => boolean
57
- isSocket: () => boolean
58
- isUnknown: () => boolean
59
- }
60
-
61
- export interface GitFsStats {
62
- isFile: () => boolean
63
- isDirectory: () => boolean
64
- isSymbolicLink: () => boolean
65
- isFIFO: () => boolean
66
- isCharacterDevice: () => boolean
67
- isBlockDevice: () => boolean
68
- isSocket: () => boolean
69
- isUnknown: () => boolean
70
- dev: number
71
- ino: number
72
- mode: number
73
- nlink: number
74
- uid: number
75
- gid: number
76
- rdev: number
77
- size: number
78
- blksize: number
79
- blocks: number
80
- atimeMs: number
81
- mtimeMs: number
82
- ctimeMs: number
83
- birthtimeMs: number
84
- atime: Date
85
- mtime: Date
86
- ctime: Date
87
- birthtime: Date
88
- }
89
-
90
- interface ParsedPath {
91
- owner: string
92
- repo: string
93
- repoPath: string
94
- }
95
-
96
- interface GitContentEntry {
97
- name: string
98
- path: string
99
- type?: 'file' | 'dir' | string
100
- sha?: string
101
- size?: number
102
- content?: string
103
- encoding?: string
104
- }
105
-
106
- const createFsError = (code: string, message: string): GitFsError => {
107
- const error = new Error(message) as GitFsError
108
- error.code = code
109
- return error
110
- }
111
-
112
- const normalizePath = (value: string): string => value.replace(/\\/g, '/').trim()
113
-
114
- const toSegments = (value: string): string[] =>
115
- normalizePath(value)
116
- .split('/')
117
- .map((part) => part.trim())
118
- .filter(Boolean)
119
-
120
- const normalizeRepoPath = (repoPath: string): string => repoPath.replace(/^\/+|\/+$/g, '')
121
-
122
- const parsePath = (
123
- value: string,
124
- defaults: { defaultOwner?: string; defaultRepo?: string },
125
- ): ParsedPath => {
126
- const parts = toSegments(value)
127
-
128
- if (parts.length === 0) {
129
- if (!defaults.defaultOwner || !defaults.defaultRepo) {
130
- throw createFsError('ENOENT', `Path requires a repository root or default owner/repo: ${value}`)
131
- }
132
-
133
- return {
134
- owner: defaults.defaultOwner,
135
- repo: defaults.defaultRepo,
136
- repoPath: '',
137
- }
138
- }
139
-
140
- if (parts.length === 1) {
141
- if (!defaults.defaultOwner || !defaults.defaultRepo) {
142
- throw createFsError('ENOENT', `Path requires owner/repo prefix: ${value}`)
143
- }
144
-
145
- return {
146
- owner: defaults.defaultOwner,
147
- repo: defaults.defaultRepo,
148
- repoPath: normalizeRepoPath(parts[0]),
149
- }
150
- }
151
-
152
- return {
153
- owner: parts[0],
154
- repo: parts[1],
155
- repoPath: normalizeRepoPath(parts.slice(2).join('/')),
156
- }
157
- }
158
-
159
- const isRecord = (value: unknown): value is Record<string, unknown> =>
160
- typeof value === 'object' && value !== null
161
-
162
- const parseEntries = (body: unknown): GitContentEntry[] => {
163
- if (!Array.isArray(body)) {
164
- return []
165
- }
166
-
167
- return body
168
- .map((entry): GitContentEntry | null => {
169
- if (!isRecord(entry)) {
170
- return null
171
- }
172
-
173
- const name = typeof entry.name === 'string' ? entry.name : undefined
174
- if (!name) {
175
- return null
176
- }
177
-
178
- return {
179
- name,
180
- path: typeof entry.path === 'string' ? entry.path : name,
181
- type: typeof entry.type === 'string' ? entry.type : undefined,
182
- sha: typeof entry.sha === 'string' ? entry.sha : undefined,
183
- size: typeof entry.size === 'number' ? entry.size : undefined,
184
- content: typeof entry.content === 'string' ? entry.content : undefined,
185
- encoding: typeof entry.encoding === 'string' ? entry.encoding : undefined,
186
- }
187
- })
188
- .filter((entry): entry is GitContentEntry => entry !== null)
189
- }
190
-
191
- const parseFileEntry = (body: unknown): GitContentEntry | null => {
192
- if (!isRecord(body)) {
193
- return null
194
- }
195
-
196
- const name = typeof body.name === 'string'
197
- ? body.name
198
- : typeof body.path === 'string'
199
- ? body.path
200
- : ''
201
- if (!name) {
202
- return null
203
- }
204
-
205
- return {
206
- name,
207
- path: typeof body.path === 'string' ? body.path : name,
208
- type: typeof body.type === 'string' ? body.type : undefined,
209
- sha: typeof body.sha === 'string' ? body.sha : undefined,
210
- size: typeof body.size === 'number' ? body.size : undefined,
211
- content: typeof body.content === 'string' ? body.content : undefined,
212
- encoding: typeof body.encoding === 'string' ? body.encoding : undefined,
213
- }
214
- }
215
-
216
- const makeStats = (kind: FileKind, entry?: GitContentEntry): GitFsStats => {
217
- const isDir = kind === 'directory'
218
- const size = isDir ? 0 : (entry?.size ?? 0)
219
- const timestamp = Date.now()
220
-
221
- return {
222
- dev: 0,
223
- ino: 0,
224
- mode: isDir ? 0o040755 : 0o100644,
225
- nlink: isDir ? 2 : 1,
226
- uid: 0,
227
- gid: 0,
228
- rdev: 0,
229
- size,
230
- blksize: 4096,
231
- blocks: Math.max(1, Math.ceil(size / 4096)),
232
- atimeMs: timestamp,
233
- mtimeMs: timestamp,
234
- ctimeMs: timestamp,
235
- birthtimeMs: timestamp,
236
- atime: new Date(timestamp),
237
- mtime: new Date(timestamp),
238
- ctime: new Date(timestamp),
239
- birthtime: new Date(timestamp),
240
- isFile: () => !isDir,
241
- isDirectory: () => isDir,
242
- isSymbolicLink: () => false,
243
- isFIFO: () => false,
244
- isCharacterDevice: () => false,
245
- isBlockDevice: () => false,
246
- isSocket: () => false,
247
- isUnknown: () => false,
248
- }
249
- }
250
-
251
- const makeDirent = (entry: GitContentEntry): GitFsDirentLike => {
252
- const isDir = entry.type === 'dir'
253
- return {
254
- name: entry.name,
255
- isFile: () => !isDir,
256
- isDirectory: () => isDir,
257
- isSymbolicLink: () => false,
258
- isBlockDevice: () => false,
259
- isCharacterDevice: () => false,
260
- isFIFO: () => false,
261
- isSocket: () => false,
262
- isUnknown: () => false,
263
- }
264
- }
265
-
266
- const responseToError = (status: number, action: string, path: string): GitFsError => {
267
- if (status === 404) {
268
- return createFsError('ENOENT', `${action}: no such path ${path}`)
269
- }
270
-
271
- if (status === 401 || status === 403) {
272
- return createFsError('EACCES', `${action}: access denied for ${path}`)
273
- }
274
-
275
- if (status === 409) {
276
- return createFsError('EEXIST', `${action}: conflict ${path}`)
277
- }
278
-
279
- if (status >= 500) {
280
- return createFsError('EIO', `${action}: remote API failed for ${path} (${status})`)
281
- }
282
-
283
- return createFsError('EIO', `${action}: remote API error for ${path} (${status})`)
284
- }
285
-
286
- const toGitPath = (path: ParsedPath): string => `${path.owner}/${path.repo}/${path.repoPath}`
287
-
288
- const encodeForGit = (value: WriteData, encoding: BufferEncoding | 'base64' | undefined): string => {
289
- if (typeof value === 'string') {
290
- return Buffer.from(value, encoding === 'base64' ? 'base64' : encoding ?? 'utf8').toString('base64')
291
- }
292
-
293
- if (value instanceof ArrayBuffer) {
294
- return Buffer.from(value).toString('base64')
295
- }
296
-
297
- return Buffer.from(value).toString('base64')
298
- }
299
-
300
- const decodeFromGit = (entry: GitContentEntry): Buffer => {
301
- if (typeof entry.content === 'string') {
302
- return Buffer.from(entry.content.replace(/\s+/g, ''), 'base64')
303
- }
304
-
305
- return Buffer.from([])
306
- }
307
-
308
- const commitMessage = (operation: string, gitPath: string, explicit?: string, defaults?: string): string => {
309
- if (explicit) {
310
- return explicit
311
- }
312
-
313
- if (defaults) {
314
- return defaults
315
- }
316
-
317
- return `${operation} ${gitPath} [${randomUUID().slice(0, 6)}]`
318
- }
319
-
320
- interface GitFsAsyncApi {
321
- rmdir(path: string): Promise<void>
322
- readdir(path: string, options?: ReaddirOptions): Promise<string[] | GitFsDirentLike[]>
323
- readFile(path: string, options?: ReadFileOptions): Promise<Buffer | string>
324
- writeFile(path: string, data: WriteData, options?: WriteFileOptions): Promise<void>
325
- appendFile(path: string, data: WriteData, options?: WriteFileOptions): Promise<void>
326
- mkdir(path: string): Promise<void>
327
- rm(path: string, options?: RmOptions): Promise<void>
328
- unlink(path: string): Promise<void>
329
- rename(oldPath: string, newPath: string): Promise<void>
330
- copyFile(src: string, dest: string): Promise<void>
331
- stat(path: string): Promise<GitFsStats>
332
- lstat(path: string): Promise<GitFsStats>
333
- exists(path: string): Promise<boolean>
334
- access(path: string): Promise<void>
335
- }
336
-
337
- export interface GitFsInstance extends GitFsAsyncApi {
338
- api: GitServiceApi
339
- promises: GitFsAsyncApi
340
- rmdirSync(path: string): never
341
- readFileSync(path: string): never
342
- writeFileSync(path: string): never
343
- appendFileSync(path: string): never
344
- readdirSync(path: string): never
345
- mkdirSync(path: string): never
346
- rmSync(path: string): never
347
- unlinkSync(path: string): never
348
- renameSync(oldPath: string, newPath: string): never
349
- copyFileSync(src: string, dest: string): never
350
- statSync(path: string): never
351
- lstatSync(path: string): never
352
- existsSync(path: string): never
353
- accessSync(path: string): never
354
- }
355
-
356
- const buildGitApi = (options: GitFsOptions): GitServiceApi => {
357
- if (options.api) {
358
- return options.api
359
- }
360
-
361
- return createGitServiceApi({
362
- config: options.config,
363
- defaultOwner: options.defaultOwner,
364
- defaultRepo: options.defaultRepo,
365
- })
366
- }
367
-
368
- const unsupportedSync = (): never => {
369
- throw createFsError('ENOSYS', 'Git filesystem sync APIs are not supported')
370
- }
371
-
372
- export const createGitFs = (options: GitFsOptions = {}): GitFsInstance => {
373
- const api = buildGitApi(options)
374
- const defaults = { defaultOwner: options.defaultOwner, defaultRepo: options.defaultRepo }
375
- const defaultMessage = options.defaultCommitMessage
376
-
377
- const callContentsMethod = async (
378
- action: 'list' | 'view' | 'create' | 'update' | 'delete',
379
- target: ParsedPath,
380
- query?: GitFsQuery,
381
- payload?: Record<string, unknown>,
382
- ): Promise<GitServiceApiExecutionResult> => {
383
- const actionMethod = api.repo.contents[action] as GitServiceApiMethod
384
- const args: unknown[] = [target.owner, target.repo, target.repoPath || '']
385
-
386
- const request: Record<string, unknown> = {}
387
- if (query && Object.keys(query).length > 0) {
388
- request.query = query
389
- }
390
-
391
- if (payload !== undefined) {
392
- request.data = payload
393
- }
394
-
395
- if (Object.keys(request).length > 0) {
396
- args.push(request)
397
- }
398
-
399
- const response = await actionMethod(...args)
400
- if (!response.ok) {
401
- throw responseToError(response.status, action, toGitPath(target))
402
- }
403
-
404
- return response
405
- }
406
-
407
- const listEntries = async (target: ParsedPath, query?: GitFsQuery): Promise<GitContentEntry[]> => {
408
- const response = await callContentsMethod('list', target, query)
409
- return parseEntries(response.body)
410
- }
411
-
412
- const readEntry = async (
413
- target: ParsedPath,
414
- query?: GitFsQuery,
415
- ): Promise<GitContentEntry | null> => {
416
- const response = await callContentsMethod('view', target, query)
417
- if (Array.isArray(response.body)) {
418
- return null
419
- }
420
-
421
- return parseFileEntry(response.body)
422
- }
423
-
424
- const resolveRefQuery = (options?: { ref?: string }): GitFsQuery | undefined => {
425
- if (!options?.ref) {
426
- return undefined
427
- }
428
-
429
- return { ref: options.ref }
430
- }
431
-
432
- const rmdir: GitFsAsyncApi['rmdir'] = async () => {
433
- throw createFsError('ENOSYS', 'rmdir is not supported for Git-backed contents')
434
- }
435
-
436
- const readdir: GitFsAsyncApi['readdir'] = async (path, options) => {
437
- const target = parsePath(path, defaults)
438
- const entries = await listEntries(target, resolveRefQuery(options))
439
- const filtered = entries.filter((entry) => entry.name.length > 0)
440
-
441
- if (options?.withFileTypes) {
442
- return filtered.map((entry) => makeDirent(entry))
443
- }
444
-
445
- return filtered.map((entry) => entry.name)
446
- }
447
-
448
- const readFile: GitFsAsyncApi['readFile'] = async (path, options) => {
449
- const target = parsePath(path, defaults)
450
- const response = await callContentsMethod('view', target, resolveRefQuery(options))
451
- if (Array.isArray(response.body)) {
452
- throw createFsError('EISDIR', `Path is a directory: ${toGitPath(target)}`)
453
- }
454
-
455
- const entry = parseFileEntry(response.body)
456
- if (!entry) {
457
- throw createFsError('ENOENT', `No such file: ${toGitPath(target)}`)
458
- }
459
-
460
- if (entry.type === 'dir') {
461
- throw createFsError('EISDIR', `Path is a directory: ${toGitPath(target)}`)
462
- }
463
-
464
- const data = decodeFromGit(entry)
465
- if (options?.encoding) {
466
- return data.toString(options.encoding)
467
- }
468
-
469
- return data
470
- }
471
-
472
- const writeFile: GitFsAsyncApi['writeFile'] = async (path, data, options = {}) => {
473
- const target = parsePath(path, defaults)
474
- const existing = await readEntry(target).catch((error) => {
475
- if ((error as GitFsError).code === 'ENOENT') {
476
- return null
477
- }
478
-
479
- throw error
480
- })
481
-
482
- if (existing?.type === 'dir') {
483
- throw createFsError('EISDIR', `Cannot write a file over directory: ${toGitPath(target)}`)
484
- }
485
-
486
- if (existing && options.overwrite === false) {
487
- throw createFsError('EEXIST', `File exists: ${toGitPath(target)}`)
488
- }
489
-
490
- const payload = {
491
- content: encodeForGit(data, options.encoding),
492
- message: commitMessage(
493
- existing ? 'overwrite' : 'file',
494
- toGitPath(target),
495
- options.message,
496
- defaultMessage,
497
- ),
498
- sha: existing?.sha,
499
- branch: options.branch,
500
- author: options.author,
501
- committer: options.committer,
502
- }
503
-
504
- if (existing) {
505
- if (!payload.sha) {
506
- throw createFsError('EINVAL', `No sha available for update: ${toGitPath(target)}`)
507
- }
508
-
509
- await callContentsMethod('update', target, undefined, payload)
510
- return
511
- }
512
-
513
- await callContentsMethod('create', target, undefined, payload)
514
- }
515
-
516
- const appendFile: GitFsAsyncApi['appendFile'] = async (path, data, options) => {
517
- const current = await readFile(path).catch((error) => {
518
- if ((error as GitFsError).code === 'ENOENT') {
519
- return Buffer.from([])
520
- }
521
-
522
- throw error
523
- })
524
- const base = typeof current === 'string' ? Buffer.from(current) : current
525
- const payload = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data)
526
- await writeFile(path, Buffer.concat([base, payload]), options)
527
- }
528
-
529
- const mkdir: GitFsAsyncApi['mkdir'] = async () => {
530
- throw createFsError('ENOSYS', 'mkdir is not supported by Git contents API')
531
- }
532
-
533
- const rm: GitFsAsyncApi['rm'] = async (path, options = {}) => {
534
- if (options.recursive) {
535
- throw createFsError('ENOSYS', 'recursive rm is not supported for Git-backed FS')
536
- }
537
-
538
- await unlink(path)
539
- }
540
-
541
- const unlink: GitFsAsyncApi['unlink'] = async (path) => {
542
- const target = parsePath(path, defaults)
543
- const existing = await readEntry(target)
544
- if (!existing) {
545
- throw createFsError('ENOENT', `No such file: ${toGitPath(target)}`)
546
- }
547
-
548
- if (existing.type === 'dir') {
549
- throw createFsError('EISDIR', `Cannot unlink directory: ${toGitPath(target)}`)
550
- }
551
-
552
- if (!existing.sha) {
553
- throw createFsError('EINVAL', `No sha available for delete: ${toGitPath(target)}`)
554
- }
555
-
556
- await callContentsMethod('delete', target, undefined, {
557
- message: commitMessage('delete', toGitPath(target), undefined, defaultMessage),
558
- sha: existing.sha,
559
- })
560
- }
561
-
562
- const rename: GitFsAsyncApi['rename'] = async (oldPath, newPath) => {
563
- const content = await readFile(oldPath)
564
- await writeFile(newPath, typeof content === 'string' ? content : Buffer.from(content))
565
- await unlink(oldPath)
566
- }
567
-
568
- const copyFile: GitFsAsyncApi['copyFile'] = async (src, dest) => {
569
- const content = await readFile(src)
570
- await writeFile(dest, typeof content === 'string' ? content : Buffer.from(content))
571
- }
572
-
573
- const stat: GitFsAsyncApi['stat'] = async (path) => {
574
- const target = parsePath(path, defaults)
575
-
576
- if (target.repoPath === '') {
577
- return makeStats('directory')
578
- }
579
-
580
- try {
581
- await listEntries(target)
582
- return makeStats('directory')
583
- } catch (error) {
584
- const code = (error as GitFsError).code
585
- if (code !== 'ENOENT' && code !== 'ENOTDIR') {
586
- throw error
587
- }
588
- }
589
-
590
- const existing = await readEntry(target)
591
- if (!existing) {
592
- throw createFsError('ENOENT', `No such path: ${toGitPath(target)}`)
593
- }
594
-
595
- if (existing.type === 'dir') {
596
- return makeStats('directory', existing)
597
- }
598
-
599
- return makeStats('file', existing)
600
- }
601
-
602
- const lstat: GitFsAsyncApi['lstat'] = async (path) => {
603
- return stat(path)
604
- }
605
-
606
- const existsAsync: GitFsAsyncApi['exists'] = async (path) => {
607
- try {
608
- const target = parsePath(path, defaults)
609
- const entry = await readEntry(target)
610
- return Boolean(entry)
611
- } catch (error) {
612
- if ((error as GitFsError).code === 'ENOENT') {
613
- return false
614
- }
615
-
616
- throw error
617
- }
618
- }
619
-
620
- const access: GitFsAsyncApi['access'] = async (path) => {
621
- const found = await existsAsync(path)
622
- if (!found) {
623
- throw createFsError('ENOENT', `No such path: ${path}`)
624
- }
625
- }
626
-
627
- const asyncApi: GitFsAsyncApi = {
628
- rmdir,
629
- readdir,
630
- readFile,
631
- writeFile,
632
- appendFile,
633
- mkdir,
634
- rm,
635
- unlink,
636
- rename,
637
- copyFile,
638
- stat,
639
- lstat,
640
- exists: existsAsync,
641
- access,
642
- }
643
-
644
- const syncUnsupported = {
645
- rmdirSync: unsupportedSync,
646
- readFileSync: unsupportedSync,
647
- writeFileSync: unsupportedSync,
648
- appendFileSync: unsupportedSync,
649
- readdirSync: unsupportedSync,
650
- mkdirSync: unsupportedSync,
651
- rmSync: unsupportedSync,
652
- unlinkSync: unsupportedSync,
653
- renameSync: unsupportedSync,
654
- copyFileSync: unsupportedSync,
655
- statSync: unsupportedSync,
656
- lstatSync: unsupportedSync,
657
- existsSync: unsupportedSync,
658
- accessSync: unsupportedSync,
659
- }
660
-
661
- return {
662
- api,
663
- ...asyncApi,
664
- ...syncUnsupported,
665
- promises: asyncApi,
666
- }
667
- }
668
-
669
- export const defaultGitFs: GitFsInstance = createGitFs({
670
- config: {
671
- platform: 'GITEA',
672
- },
673
- defaultOwner: process.env.GITEA_TEST_OWNER ?? 'example-org',
674
- defaultRepo: process.env.GITEA_TEST_REPO ?? 'example-repo',
675
- })
676
-
677
- export const promises = defaultGitFs.promises
678
- export const readdir = defaultGitFs.readdir
679
- export const readFile = defaultGitFs.readFile
680
- export const writeFile = defaultGitFs.writeFile
681
- export const appendFile = defaultGitFs.appendFile
682
- export const mkdir = defaultGitFs.mkdir
683
- export const rm = defaultGitFs.rm
684
- export const unlink = defaultGitFs.unlink
685
- export const rename = defaultGitFs.rename
686
- export const copyFile = defaultGitFs.copyFile
687
- export const stat = defaultGitFs.stat
688
- export const lstat = defaultGitFs.lstat
689
- export const exists = defaultGitFs.exists
690
- export const access = defaultGitFs.access
691
- export const rmdir = defaultGitFs.rmdir
692
- export const readFileSync = defaultGitFs.readFileSync
693
- export const writeFileSync = defaultGitFs.writeFileSync
694
- export const appendFileSync = defaultGitFs.appendFileSync
695
- export const readdirSync = defaultGitFs.readdirSync
696
- export const mkdirSync = defaultGitFs.mkdirSync
697
- export const rmSync = defaultGitFs.rmSync
698
- export const rmdirSync = defaultGitFs.rmdirSync
699
- export const unlinkSync = defaultGitFs.unlinkSync
700
- export const renameSync = defaultGitFs.renameSync
701
- export const copyFileSync = defaultGitFs.copyFileSync
702
- export const statSync = defaultGitFs.statSync
703
- export const lstatSync = defaultGitFs.lstatSync
704
- export const existsSync = defaultGitFs.existsSync
705
- export const accessSync = defaultGitFs.accessSync