Compare commits
2 Commits
90ca2c2156
...
8b62f72f93
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b62f72f93 | |||
| 0989f1ca30 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
config.toml
|
||||||
|
|||||||
3
REAME.md
Normal file
3
REAME.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
|
github fine-grained permissions required: `metadata`, `content`
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
"description": "Schedule issues in Gitea or GitHub using issue templates",
|
"description": "Schedule issues in Gitea or GitHub using issue templates",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
||||||
"author": "Luís Figueiredo (TxFig)",
|
"author": "Luís Figueiredo (TxFig)",
|
||||||
"homepage": "https://gitea.alluna.pt/TxFig/issue-scheduler",
|
"homepage": "https://gitea.alluna.pt/TxFig/issue-scheduler",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -12,15 +11,18 @@
|
|||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/main.js"
|
"start": "node dist/main.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.9.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"toml": "^3.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -7,13 +7,20 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
toml:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^24.9.1
|
||||||
|
version: 24.9.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4
|
version: 3.2.4(@types/node@24.9.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -295,6 +302,9 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/node@24.9.1':
|
||||||
|
resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==}
|
||||||
|
|
||||||
'@vitest/expect@3.2.4':
|
'@vitest/expect@3.2.4':
|
||||||
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
||||||
|
|
||||||
@@ -460,11 +470,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
toml@3.0.0:
|
||||||
|
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@7.16.0:
|
||||||
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
vite-node@3.2.4:
|
vite-node@3.2.4:
|
||||||
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
@@ -699,6 +715,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/node@24.9.1':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@vitest/expect@3.2.4':
|
'@vitest/expect@3.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.2
|
'@types/chai': 5.2.2
|
||||||
@@ -707,13 +727,13 @@ snapshots:
|
|||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4(vite@7.1.9)':
|
'@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.9.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.4
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.1.9
|
vite: 7.1.9(@types/node@24.9.1)
|
||||||
|
|
||||||
'@vitest/pretty-format@3.2.4':
|
'@vitest/pretty-format@3.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -886,15 +906,19 @@ snapshots:
|
|||||||
|
|
||||||
tinyspy@4.0.4: {}
|
tinyspy@4.0.4: {}
|
||||||
|
|
||||||
|
toml@3.0.0: {}
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
vite-node@3.2.4:
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
|
vite-node@3.2.4(@types/node@24.9.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
vite: 7.1.9
|
vite: 7.1.9(@types/node@24.9.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
@@ -909,7 +933,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
vite@7.1.9:
|
vite@7.1.9(@types/node@24.9.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.10
|
esbuild: 0.25.10
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -918,13 +942,14 @@ snapshots:
|
|||||||
rollup: 4.52.4
|
rollup: 4.52.4
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 24.9.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
vitest@3.2.4:
|
vitest@3.2.4(@types/node@24.9.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.2
|
'@types/chai': 5.2.2
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.4
|
||||||
'@vitest/mocker': 3.2.4(vite@7.1.9)
|
'@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.9.1))
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.4
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.4
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.4
|
||||||
@@ -942,9 +967,11 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vite: 7.1.9
|
vite: 7.1.9(@types/node@24.9.1)
|
||||||
vite-node: 3.2.4
|
vite-node: 3.2.4(@types/node@24.9.1)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.9.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { URL } from "url"
|
import { URL } from "url"
|
||||||
|
|
||||||
import { BaseApi } from "./index.js"
|
import { BaseApi } from "./index.js"
|
||||||
|
import type { Result } from "../result.js";
|
||||||
|
import * as result from "../result.js"
|
||||||
|
|
||||||
|
|
||||||
// /api/swagger
|
// /api/swagger
|
||||||
const Prefix = "/api/v1"
|
const Prefix = "/api/v1"
|
||||||
const IssueTemplatesUrl = (user: string, repo: string) =>
|
const urls = {
|
||||||
`${Prefix}/repos/${user}/${repo}/issue_templates`
|
IssueTemplatesUrl: (user: string, repo: string) =>
|
||||||
|
`${Prefix}/repos/${user}/${repo}/issue_templates`,
|
||||||
|
FileContentUrl: (user: string, repo: string, filepath: string) =>
|
||||||
|
`${Prefix}/repos/${user}/${repo}/contents/${filepath}`
|
||||||
|
}
|
||||||
|
|
||||||
type GiteaIssueTemplatesResponse = {
|
namespace GiteaApi {
|
||||||
name: string,
|
export type IssueTemplatesResponse = {
|
||||||
title: string,
|
name: string,
|
||||||
about: string,
|
title: string,
|
||||||
labels: string[] | null,
|
about: string,
|
||||||
assignees: string[] | null,
|
labels: string[] | null,
|
||||||
ref: string,
|
assignees: string[] | null,
|
||||||
content: string,
|
ref: string,
|
||||||
body: unknown | null,
|
content: string,
|
||||||
file_name: string
|
body: unknown | null,
|
||||||
}[]
|
file_name: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
class Gitea extends BaseApi {
|
class Gitea extends BaseApi {
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
@@ -30,20 +38,30 @@ class Gitea extends BaseApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIssueTemplates() {
|
async getIssueTemplates(): Promise<Result<string[], string>> {
|
||||||
const url = new URL(IssueTemplatesUrl(this.user, this.repo), this.host)
|
const url = new URL(
|
||||||
|
urls.IssueTemplatesUrl(this.user, this.repo),
|
||||||
|
this.host)
|
||||||
const response = await fetch(url, { headers: this.headers })
|
const response = await fetch(url, { headers: this.headers })
|
||||||
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200: break
|
case 200: break
|
||||||
case 404: return { error: "Issue templates not found" }
|
case 404: return result.error("Issue templates not found")
|
||||||
default: return { error: "Unknown status code" }
|
default: return result.error("Unknown status code")
|
||||||
}
|
}
|
||||||
|
|
||||||
const json: GiteaIssueTemplatesResponse = await response.json()
|
const json: GiteaApi.IssueTemplatesResponse = await response.json()
|
||||||
return {
|
return result.value(
|
||||||
value: json.map(template => template.file_name)
|
json.map(template => template.file_name)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileContent(filepath: string): Promise<Result<string[], string>> {
|
||||||
|
const url = new URL(
|
||||||
|
urls.FileContentUrl(this.user, this.repo, filepath),
|
||||||
|
this.host)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { URL } from "url"
|
import { BaseApi } from "./index.js"
|
||||||
|
import type { Result } from "../result.js"
|
||||||
import {BaseApi, type Result} from "./index.js"
|
import * as result from "../result.js"
|
||||||
|
|
||||||
|
|
||||||
// docs.github.com/en/rest
|
// docs.github.com/en/rest
|
||||||
/*
|
|
||||||
Folders searched by gitea:
|
|
||||||
"ISSUE_TEMPLATE"
|
|
||||||
"issue_template"
|
|
||||||
".gitea/ISSUE_TEMPLATE"
|
|
||||||
".gitea/issue_template"
|
|
||||||
".github/ISSUE_TEMPLATE"
|
|
||||||
".github/issue_template"
|
|
||||||
*/
|
|
||||||
|
|
||||||
const IssueTemplateFolders = [
|
const IssueTemplateFolders = [
|
||||||
"ISSUE_TEMPLATE",
|
"ISSUE_TEMPLATE",
|
||||||
"issue_template",
|
"issue_template",
|
||||||
@@ -23,36 +13,38 @@ const IssueTemplateFolders = [
|
|||||||
".github/issue_template"
|
".github/issue_template"
|
||||||
]
|
]
|
||||||
|
|
||||||
// /repos/${user}/${repo} -> .default_branch
|
|
||||||
// /repos/${user}/${repo}/git/trees/{branch}?recursive=1 -> search for templates
|
|
||||||
|
|
||||||
const Prefix = "https://api.github.com"
|
const Prefix = "https://api.github.com"
|
||||||
// https://docs.github.com/en/rest/repos/repos#get-a-repository
|
const urls = {
|
||||||
const RepoInfoUrl = (user: string, repo: string) =>
|
// https://docs.github.com/en/rest/repos/repos#get-a-repository
|
||||||
`${Prefix}/repos/${user}/${repo}`
|
RepoInfoUrl: (user: string, repo: string) =>
|
||||||
interface GitHubRepoInfoResponse {
|
`${Prefix}/repos/${user}/${repo}`,
|
||||||
default_branch: string
|
|
||||||
// ...
|
// https://docs.github.com/en/rest/git/trees#get-a-tree
|
||||||
|
TreeUrl: (user: string, repo: string, branch: string) =>
|
||||||
|
`${Prefix}/repos/${user}/${repo}/git/trees/${branch}?recursive=1`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.github.com/en/rest/git/trees#get-a-tree
|
namespace GitHubApi {
|
||||||
const TreeUrl = (user: string, repo: string, branch: string) =>
|
export interface RepoInfoResponse {
|
||||||
`${Prefix}/repos/${user}/${repo}/git/trees/${branch}?recursive=1`
|
default_branch: string
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
interface GitHubTreeResponse {
|
export interface TreeResponse {
|
||||||
sha: string
|
|
||||||
url: string
|
|
||||||
tree: {
|
|
||||||
path: string
|
|
||||||
mode: string
|
|
||||||
type: "tree" | "blob"
|
|
||||||
sha: string
|
sha: string
|
||||||
size: number
|
|
||||||
url: string
|
url: string
|
||||||
}[]
|
tree: {
|
||||||
}
|
path: string
|
||||||
|
mode: string
|
||||||
|
type: "tree" | "blob"
|
||||||
|
sha: string
|
||||||
|
size: number
|
||||||
|
url: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
// fine-grained tokens required permissions: metadata, content
|
}
|
||||||
|
|
||||||
class GitHub extends BaseApi {
|
class GitHub extends BaseApi {
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
@@ -64,53 +56,67 @@ class GitHub extends BaseApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultBranch(): Promise<Result<string, string>> {
|
private async getDefaultBranch(): Promise<Result<string, string>> {
|
||||||
const url = new URL(RepoInfoUrl(this.user, this.repo), this.host)
|
const url = urls.RepoInfoUrl(this.user, this.repo)
|
||||||
const response = await fetch(url, { headers: this.headers })
|
const response = await fetch(url, { headers: this.headers })
|
||||||
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 301: return { error: "Moved permanently" }
|
case 301: return result.error("Moved permanently")
|
||||||
case 403: return { error: "Forbidden" }
|
case 403: return result.error("Forbidden")
|
||||||
case 404: return { error: "Resource not found" }
|
case 404: return result.error("Resource not found")
|
||||||
case 200: break
|
case 200: break
|
||||||
default: return { error: "Unknown status code" }
|
default: return result.error("Unknown status code")
|
||||||
}
|
}
|
||||||
|
|
||||||
const json: GitHubRepoInfoResponse = await response.json()
|
const json: GitHubApi.RepoInfoResponse = await response.json()
|
||||||
return { value: json.default_branch }
|
return result.value(json.default_branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRepoFiles(): Promise<Result<string[], string>> {
|
private async getRepoFiles(): Promise<Result<string[], string>> {
|
||||||
const branch = await this.getDefaultBranch()
|
const branch = await this.getDefaultBranch()
|
||||||
if (!branch.value) return { error: `Default branch error: ${branch.error}` }
|
if (!branch.success) return result.error(`Default branch error: ${branch.error}`)
|
||||||
|
|
||||||
const url = new URL(TreeUrl(this.user, this.repo, branch.value), this.host)
|
const url = urls.TreeUrl(this.user, this.repo, branch.value)
|
||||||
const response = await fetch(url, { headers: this.headers })
|
let response: Response
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { headers: this.headers })
|
||||||
|
} catch (err) {
|
||||||
|
return result.error(`Fetch Error: ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200: break
|
case 200: break
|
||||||
case 404: return { error: "Resource not found" }
|
case 404: return result.error("Resource not found")
|
||||||
case 409: return { error: "Conflict" }
|
case 409: return result.error("Conflict")
|
||||||
case 422: return { error: "Validation failed, or the endpoint has been spammed" }
|
case 422: return result.error("Validation failed, or the endpoint has been spammed")
|
||||||
default: return { error: "Unknown status code" }
|
default: return result.error("Unknown status code")
|
||||||
}
|
}
|
||||||
|
|
||||||
const json: GitHubTreeResponse = await response.json()
|
let json: GitHubApi.TreeResponse
|
||||||
return {
|
try {
|
||||||
value: json.tree
|
json = await response.json()
|
||||||
.filter(entry => entry.type == "blob")
|
} catch (error) {
|
||||||
.map(entry => entry.path)
|
return result.error(`Json Error: ${error}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.value(json.tree
|
||||||
|
.filter(entry => entry.type == "blob")
|
||||||
|
.map(entry => entry.path)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIssueTemplates() {
|
async getIssueTemplates(): Promise<Result<string[], string>> {
|
||||||
const files = await this.getRepoFiles()
|
const files = await this.getRepoFiles()
|
||||||
if (!files.value) return { error: `Repo files error: ${files.error}` }
|
if (!files.success) return result.error(`Repo files error: ${files.error}`)
|
||||||
const issue_templates = files.value.filter(
|
const issue_templates = files.value.filter(
|
||||||
file => IssueTemplateFolders.some(folder => file.startsWith(folder))
|
file => IssueTemplateFolders.some(folder => file.startsWith(folder))
|
||||||
)
|
)
|
||||||
|
|
||||||
return { value: issue_templates }
|
return result.value(issue_templates)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileContent(filepath: string): Promise<Result<string[], string>> {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
export type Result<Type, Err> = {
|
import type { Result } from "../result.js"
|
||||||
value: Type,
|
|
||||||
error?: null
|
|
||||||
} | {
|
|
||||||
value?: null,
|
|
||||||
error: Err
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class BaseApi {
|
export abstract class BaseApi {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -18,4 +13,5 @@ export abstract class BaseApi {
|
|||||||
* @returns Array of filepath's
|
* @returns Array of filepath's
|
||||||
*/
|
*/
|
||||||
abstract getIssueTemplates(): Promise<Result<string[], string>>
|
abstract getIssueTemplates(): Promise<Result<string[], string>>
|
||||||
|
abstract getFileContent(filepath: string): Promise<Result<string, string>>
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/config.ts
Normal file
91
src/config.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import fs from "node:fs/promises"
|
||||||
|
import * as toml from "toml"
|
||||||
|
import type { Result } from "./result.js"
|
||||||
|
import * as result from "./result.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface Repository {
|
||||||
|
url: URL
|
||||||
|
token: string
|
||||||
|
client?: "gitea" | "github"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUrl(url: string) {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUrl(url: unknown): Result<undefined, string> {
|
||||||
|
if (!url)
|
||||||
|
return result.error("Missing url")
|
||||||
|
if (typeof url !== "string")
|
||||||
|
return result.error("Url must be a string")
|
||||||
|
if (!isValidUrl(url))
|
||||||
|
return result.error("Invalid URL")
|
||||||
|
return result.value(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateToken(token: unknown): Result<undefined, string> {
|
||||||
|
if (!token)
|
||||||
|
return result.error("Missing token")
|
||||||
|
if (typeof token !== "string")
|
||||||
|
return result.error("Token must be a string")
|
||||||
|
return result.value(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateClient(client: unknown): Result<undefined, string> {
|
||||||
|
if (!client)
|
||||||
|
return result.value(undefined)
|
||||||
|
if (typeof client !== "string")
|
||||||
|
return result.error("Client must be a string")
|
||||||
|
if (!["gitea", "github"].includes(client)) {
|
||||||
|
return result.error("Client must be \"gitea\" or \"github\"")
|
||||||
|
}
|
||||||
|
return result.value(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateConfig(config: Record<string, any>): Result<Repository[], string> {
|
||||||
|
if (!("repo" in config) ||
|
||||||
|
!Array.isArray(config["repo"])
|
||||||
|
) return result.error("Missing repo array of tables")
|
||||||
|
|
||||||
|
for (let i = 0; i < config["repo"].length; i++) {
|
||||||
|
const repo = config["repo"][i]
|
||||||
|
const url = validateUrl(repo["url"])
|
||||||
|
if (!url.success)
|
||||||
|
return result.error(`Invalid Config (Repo#${i + 1}): ${url.error}`)
|
||||||
|
repo["url"] = new URL(repo["url"])
|
||||||
|
|
||||||
|
const token = validateToken(repo["token"])
|
||||||
|
if (!token.success)
|
||||||
|
return result.error(`Invalid Config (Repo#${i + 1}): ${token.error}`)
|
||||||
|
|
||||||
|
const client = validateClient(repo["client"])
|
||||||
|
if (!client.success)
|
||||||
|
return result.error(`Invalid Config (Repo#${i + 1}): ${client.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value(config["repo"])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConfig(config_filepath: string): Promise<Result<Repository[], string>> {
|
||||||
|
let configFile: string
|
||||||
|
try {
|
||||||
|
configFile = await fs.readFile(config_filepath, "utf8")
|
||||||
|
} catch (err) {
|
||||||
|
return result.error(`Failed to open config file: "${config_filepath}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: Record<string, any>
|
||||||
|
try {
|
||||||
|
config = toml.parse(configFile)
|
||||||
|
} catch (err) {
|
||||||
|
return result.error(`Failed to parse config file. ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateConfig(config)
|
||||||
|
}
|
||||||
94
src/index.ts
94
src/index.ts
@@ -1,22 +1,90 @@
|
|||||||
import * as cli from "./cli.js"
|
import * as cli from "./cli.js"
|
||||||
|
import { getConfig, type Repository } from "./config.js"
|
||||||
|
|
||||||
|
import type { Result } from "./result.js"
|
||||||
const config = cli.parseArgs(process.argv)
|
import * as result from "./result.js"
|
||||||
// console.log(config)
|
|
||||||
|
|
||||||
import type { BaseApi } from "./apis";
|
import type { BaseApi } from "./apis";
|
||||||
import Gitea from "./apis/gitea.js"
|
import Gitea from "./apis/gitea.js"
|
||||||
import GitHub from "./apis/github.js";
|
import GitHub from "./apis/github.js";
|
||||||
|
|
||||||
|
|
||||||
let api: BaseApi
|
function identifyGitClient(repo: Repository): "gitea" | "github" {
|
||||||
api = new Gitea(
|
if (repo.client)
|
||||||
"http://localhost:3000", "...",
|
return repo.client
|
||||||
"admin", "test_repo"
|
|
||||||
)
|
|
||||||
api = new GitHub(
|
|
||||||
"https://api.github.com", "...",
|
|
||||||
"TxFig", "test-api")
|
|
||||||
|
|
||||||
const result = await api.getIssueTemplates()
|
if (repo.url.host === "github.com")
|
||||||
console.log(result.value ?? result.error)
|
return "github"
|
||||||
|
|
||||||
|
return "gitea"
|
||||||
|
}
|
||||||
|
|
||||||
|
function instantiateApi(repo: Repository): Result<InstanceType<typeof BaseApi>, string> {
|
||||||
|
const client = identifyGitClient(repo)
|
||||||
|
const host = repo.url.origin
|
||||||
|
const token = repo.token
|
||||||
|
const path_parts = repo.url.pathname
|
||||||
|
.split("/")
|
||||||
|
.filter(part => part && part != "/")
|
||||||
|
if (path_parts.length != 2)
|
||||||
|
return result.error("Invalid repo url")
|
||||||
|
const [user, repo_name] = path_parts as [string, string]
|
||||||
|
|
||||||
|
const ApiClass = client === "gitea" ? Gitea : GitHub
|
||||||
|
return result.value(
|
||||||
|
new ApiClass(host, token, user, repo_name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTemplate(template_file: string): Promise<Result<undefined, string>> {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return result.value(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRepo(repo: Repository): Promise<Result<undefined, string>> {
|
||||||
|
const api = instantiateApi(repo)
|
||||||
|
if (!api.success)
|
||||||
|
return result.error(`instantiateApi: ${api.error}`)
|
||||||
|
|
||||||
|
const templates = await api.value.getIssueTemplates()
|
||||||
|
if (!templates.success)
|
||||||
|
return result.error(`getIssueTemplates: ${templates.error}`)
|
||||||
|
|
||||||
|
for (const template of templates.value) {
|
||||||
|
const result = await handleTemplate(template)
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const cliConfig = cli.parseArgs(process.argv)
|
||||||
|
|
||||||
|
const config = await getConfig(cliConfig.config_filepath)
|
||||||
|
if (!config.success) {
|
||||||
|
console.error(config.error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise_results = await Promise.allSettled(
|
||||||
|
config.value.map(repo => handleRepo(repo))
|
||||||
|
)
|
||||||
|
for (const p_result of promise_results) {
|
||||||
|
if (p_result.status === "rejected") {
|
||||||
|
console.error(p_result.reason)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p_result.value.success) {
|
||||||
|
console.error(p_result.value.error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(p_result.value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main()
|
||||||
|
|||||||
16
src/result.ts
Normal file
16
src/result.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export type Result<TValue, TError> = {
|
||||||
|
value: TValue,
|
||||||
|
success: true
|
||||||
|
} | {
|
||||||
|
error: TError,
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function value<TValue, TError>(val: TValue): Result<TValue, TError> {
|
||||||
|
return { value: val, success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error<TValue, TError>(val: TError): Result<TValue, TError> {
|
||||||
|
return { error: val, success: false }
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user