@typed-assistant/builder 0.0.22 → 0.0.24
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/.eslintrc.js +2 -2
- package/package.json +8 -8
- package/src/appProcess.tsx +101 -70
- package/src/bunInstall.tsx +6 -16
- package/src/killProcess.tsx +32 -0
- package/src/pullChanges.tsx +3 -14
- package/src/setupGitPoller.tsx +19 -0
- package/src/setupWebhook.tsx +159 -0
- package/src/setupWebserver.tsx +5 -4
- package/src/webserver/useWS.tsx +1 -1
- package/tsconfig.json +1 -1
- package/src/getSpawnText.tsx +0 -14
package/.eslintrc.js
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-assistant/builder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"exports": {
|
|
5
5
|
"./appProcess": "./src/appProcess.tsx",
|
|
6
6
|
"./bunInstall": "./src/bunInstall.tsx",
|
|
7
|
-
"./getSpawnText": "./src/getSpawnText.tsx",
|
|
8
7
|
"./pullChanges": "./src/pullChanges.tsx"
|
|
9
8
|
},
|
|
10
9
|
"dependencies": {
|
|
@@ -14,7 +13,8 @@
|
|
|
14
13
|
"@mdi/svg": "^7.3.67",
|
|
15
14
|
"ignore": "^5.3.0",
|
|
16
15
|
"react": "^18",
|
|
17
|
-
"react-dom": "^18"
|
|
16
|
+
"react-dom": "^18",
|
|
17
|
+
"zod": "^3.22.4"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.10.6",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"home-assistant-js-websocket": "^8.2.0",
|
|
25
25
|
"ts-toolbelt": "^9.6.0",
|
|
26
26
|
"typescript": "^5.3.3",
|
|
27
|
-
"@typed-assistant/eslint-config": "0.0.
|
|
28
|
-
"@typed-assistant/
|
|
29
|
-
"@typed-assistant/
|
|
30
|
-
"@typed-assistant/
|
|
27
|
+
"@typed-assistant/eslint-config": "0.0.5",
|
|
28
|
+
"@typed-assistant/logger": "0.0.9",
|
|
29
|
+
"@typed-assistant/typescript-config": "0.0.5",
|
|
30
|
+
"@typed-assistant/utils": "0.0.8"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"home-assistant-js-websocket": "^8.2.0"
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"registry": "https://registry.npmjs.org/"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"lint": "eslint .
|
|
40
|
+
"lint": "tsc --noEmit && eslint ."
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/appProcess.tsx
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { generateTypes } from "@typed-assistant/types/generateTypes"
|
|
2
1
|
import { log } from "@typed-assistant/logger"
|
|
2
|
+
import { generateTypes } from "@typed-assistant/types/generateTypes"
|
|
3
|
+
import { getHassAPI, getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
|
|
4
|
+
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
3
5
|
import type { Subprocess } from "bun"
|
|
6
|
+
import { $ } from "bun"
|
|
4
7
|
import { readFileSync, watch } from "fs"
|
|
5
|
-
import { join, relative } from "path"
|
|
6
8
|
import ignore from "ignore"
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { join, relative } from "path"
|
|
10
|
+
import {
|
|
11
|
+
addKillListener,
|
|
12
|
+
callKillListeners,
|
|
13
|
+
callSoftKillListeners,
|
|
14
|
+
killSubprocess,
|
|
15
|
+
} from "./killProcess"
|
|
16
|
+
import { setupGitPoller } from "./setupGitPoller"
|
|
17
|
+
import { setupWebhook } from "./setupWebhook"
|
|
11
18
|
import { startWebappServer } from "./setupWebserver"
|
|
12
|
-
import { $ } from "bun"
|
|
13
19
|
|
|
14
20
|
type Processes = Awaited<ReturnType<typeof buildAndStartAppProcess>>
|
|
15
21
|
|
|
@@ -30,12 +36,6 @@ async function startApp(appSourceFile: string) {
|
|
|
30
36
|
})
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
async function kill(process: Subprocess) {
|
|
34
|
-
log(`💀 Killing process: ${process.pid}`)
|
|
35
|
-
process.kill()
|
|
36
|
-
await process.exited
|
|
37
|
-
}
|
|
38
|
-
|
|
39
39
|
let settingUp = { current: false }
|
|
40
40
|
async function killAndRestartApp(
|
|
41
41
|
entryFile: string,
|
|
@@ -45,7 +45,7 @@ async function killAndRestartApp(
|
|
|
45
45
|
if (settingUp.current) return subprocesses
|
|
46
46
|
log("♻️ Restarting app...")
|
|
47
47
|
settingUp.current = true
|
|
48
|
-
if (subprocesses.app) await
|
|
48
|
+
if (subprocesses.app) await killSubprocess(subprocesses.app)
|
|
49
49
|
const newSubprocesses = await buildAndStartAppProcess(entryFile, options)
|
|
50
50
|
settingUp.current = false
|
|
51
51
|
return newSubprocesses
|
|
@@ -62,7 +62,7 @@ const checkProcesses = async (
|
|
|
62
62
|
|
|
63
63
|
if (matches.length > 1) {
|
|
64
64
|
multipleProcessesErrorCount++
|
|
65
|
-
if (multipleProcessesErrorCount >
|
|
65
|
+
if (multipleProcessesErrorCount > 5) {
|
|
66
66
|
const message = `🚨 Multiple processes detected. Restarting TypedAssistant addon...`
|
|
67
67
|
log(message)
|
|
68
68
|
onProcessError?.(message)
|
|
@@ -74,7 +74,7 @@ const checkProcesses = async (
|
|
|
74
74
|
|
|
75
75
|
if (matches.length === 0) {
|
|
76
76
|
noProcessesErrorCount++
|
|
77
|
-
if (noProcessesErrorCount >
|
|
77
|
+
if (noProcessesErrorCount > 5) {
|
|
78
78
|
const message = `🚨 No processes detected. Restarting TypedAssistant addon...`
|
|
79
79
|
log(message)
|
|
80
80
|
onProcessError?.(message)
|
|
@@ -87,7 +87,7 @@ const checkProcesses = async (
|
|
|
87
87
|
setTimeout(() => checkProcesses(entryFile, { onProcessError }), 5000)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export async function
|
|
90
|
+
export async function setup({
|
|
91
91
|
entryFile,
|
|
92
92
|
mdiPaths,
|
|
93
93
|
onProcessError,
|
|
@@ -95,65 +95,50 @@ export async function setupWatcher({
|
|
|
95
95
|
entryFile: string
|
|
96
96
|
} & Parameters<typeof generateTypes>[0] &
|
|
97
97
|
Parameters<typeof checkProcesses>[1]) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await setupGitSync()
|
|
98
|
+
const addonInfo = await getAddonInfo()
|
|
99
|
+
const basePath = addonInfo?.data.ingress_entry ?? ""
|
|
100
|
+
const directoryToWatch = join(process.cwd(), "./src")
|
|
101
|
+
|
|
103
102
|
checkProcesses(entryFile, { onProcessError })
|
|
103
|
+
await setupGitSync()
|
|
104
104
|
|
|
105
105
|
let subprocesses = await buildAndStartAppProcess(entryFile, {
|
|
106
106
|
mdiPaths: mdiPaths,
|
|
107
107
|
})
|
|
108
|
-
|
|
109
|
-
const directory = join(process.cwd(), "./src")
|
|
110
|
-
log("👀 Watching directory:", directory)
|
|
111
|
-
const watcher = watch(
|
|
112
|
-
directory,
|
|
113
|
-
{ recursive: true },
|
|
114
|
-
async function onFileChange(event, filename) {
|
|
115
|
-
if (!filename) return
|
|
116
|
-
if (shouldIgnoreFileOrFolder(filename)) return
|
|
117
|
-
log(`⚠️ Change to ${filename} detected.`)
|
|
118
|
-
if (filename.endsWith("process.tsx")) {
|
|
119
|
-
await restartAddon()
|
|
120
|
-
} else {
|
|
121
|
-
subprocesses = await killAndRestartApp(
|
|
122
|
-
entryFile,
|
|
123
|
-
{ mdiPaths },
|
|
124
|
-
subprocesses,
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
)
|
|
129
|
-
|
|
130
108
|
startWebappServer({
|
|
131
|
-
basePath
|
|
109
|
+
basePath,
|
|
132
110
|
getSubprocesses: () => subprocesses,
|
|
133
111
|
})
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
112
|
+
setupWatcher({
|
|
113
|
+
directoryToWatch,
|
|
114
|
+
entryFile,
|
|
115
|
+
mdiPaths,
|
|
116
|
+
onSubprocessChange: (newSubprocesses) => {
|
|
117
|
+
subprocesses = newSubprocesses
|
|
118
|
+
},
|
|
119
|
+
getSubprocesses: () => subprocesses,
|
|
138
120
|
})
|
|
139
121
|
|
|
140
122
|
return subprocesses
|
|
141
123
|
}
|
|
142
124
|
|
|
143
|
-
const setupGitSync = async ({
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
125
|
+
const setupGitSync = async () => {
|
|
126
|
+
if (
|
|
127
|
+
!process.env.GITHUB_TOKEN ||
|
|
128
|
+
!process.env.GITHUB_USERNAME ||
|
|
129
|
+
!process.env.GITHUB_REPO
|
|
130
|
+
) {
|
|
131
|
+
log(
|
|
132
|
+
"⚠️ Cannot sync with Github without Github token, username, and repo details. Add these in the add-on configuration.",
|
|
133
|
+
)
|
|
134
|
+
return { error: {} }
|
|
135
|
+
}
|
|
136
|
+
if (process.env.HASS_EXTERNAL_URL) {
|
|
137
|
+
await setupWebhook()
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
log("⚠️ No HASS_EXTERNAL_URL found. Setting up git poller...")
|
|
141
|
+
await setupGitPoller()
|
|
157
142
|
}
|
|
158
143
|
|
|
159
144
|
const ig = ignore().add(
|
|
@@ -169,20 +154,66 @@ const restartAddon = async () => {
|
|
|
169
154
|
return
|
|
170
155
|
}
|
|
171
156
|
log("♻️ Restarting addon...")
|
|
172
|
-
await
|
|
157
|
+
await getSupervisorAPI(`/addons/self/restart`, { method: "POST" })
|
|
173
158
|
}
|
|
174
159
|
|
|
175
160
|
const getAddonInfo = async () => {
|
|
176
161
|
log("🔍 Getting addon info...")
|
|
177
162
|
|
|
178
|
-
|
|
163
|
+
const { data, error } = await withErrorHandling(getSupervisorAPI)<{
|
|
179
164
|
data: { ingress_entry: string }
|
|
180
165
|
}>("/addons/self/info")
|
|
166
|
+
|
|
167
|
+
if (error) log(`🚨 Failed to get addon info: ${error}`)
|
|
168
|
+
|
|
169
|
+
return data
|
|
181
170
|
}
|
|
182
171
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
172
|
+
function setupWatcher({
|
|
173
|
+
directoryToWatch,
|
|
174
|
+
entryFile,
|
|
175
|
+
mdiPaths,
|
|
176
|
+
onSubprocessChange,
|
|
177
|
+
getSubprocesses,
|
|
178
|
+
}: {
|
|
179
|
+
directoryToWatch: string
|
|
180
|
+
onSubprocessChange: (newSubprosses: {
|
|
181
|
+
app: Subprocess<"ignore", "pipe", "pipe">
|
|
182
|
+
}) => void
|
|
183
|
+
entryFile: string
|
|
184
|
+
mdiPaths: string[] | undefined
|
|
185
|
+
getSubprocesses: () => {
|
|
186
|
+
app: Subprocess<"ignore", "pipe", "pipe">
|
|
187
|
+
}
|
|
188
|
+
}) {
|
|
189
|
+
log("👀 Watching directory:", directoryToWatch)
|
|
190
|
+
const watcher = watch(
|
|
191
|
+
directoryToWatch,
|
|
192
|
+
{ recursive: true },
|
|
193
|
+
async function onFileChange(event, filename) {
|
|
194
|
+
if (!filename) return
|
|
195
|
+
if (shouldIgnoreFileOrFolder(filename)) return
|
|
196
|
+
log(`⚠️ Change to ${filename} detected.`)
|
|
197
|
+
if (filename.endsWith("process.tsx")) {
|
|
198
|
+
await restartAddon()
|
|
199
|
+
} else {
|
|
200
|
+
onSubprocessChange(
|
|
201
|
+
await killAndRestartApp(entryFile, { mdiPaths }, getSubprocesses()),
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
addKillListener(() => {
|
|
208
|
+
if (watcher) watcher.close()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return watcher
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.on("SIGINT", async () => {
|
|
215
|
+
log("👋 Exiting...")
|
|
216
|
+
await callSoftKillListeners()
|
|
217
|
+
await callKillListeners()
|
|
218
|
+
process.exit(0)
|
|
188
219
|
})
|
package/src/bunInstall.tsx
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { log } from "@typed-assistant/logger"
|
|
2
|
+
import { $ } from "bun"
|
|
2
3
|
|
|
3
4
|
export async function bunInstall() {
|
|
4
5
|
log("🏗️ Running bun install...")
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
])
|
|
11
|
-
await proc.exited
|
|
12
|
-
const bunInstallText = await Bun.readableStreamToText(proc.stdout)
|
|
13
|
-
if (proc.exitCode === 0) return { error: null }
|
|
14
|
-
return {
|
|
15
|
-
error: {
|
|
16
|
-
signalCode: proc.signalCode,
|
|
17
|
-
exitCode: proc.exitCode,
|
|
18
|
-
text: bunInstallText,
|
|
19
|
-
},
|
|
20
|
-
}
|
|
6
|
+
return $`bun install --frozen-lockfile --cache-dir=.bun-cache`
|
|
7
|
+
.text()
|
|
8
|
+
.catch((error) => {
|
|
9
|
+
log(`🚨 Failed to run bun install: ${error}`)
|
|
10
|
+
})
|
|
21
11
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { log } from "@typed-assistant/logger"
|
|
2
|
+
import type { Subprocess } from "bun"
|
|
3
|
+
import { $ } from "bun"
|
|
4
|
+
|
|
5
|
+
const killListeners: (() => void | Promise<void>)[] = []
|
|
6
|
+
const softKillListeners: (() => void | Promise<void>)[] = []
|
|
7
|
+
|
|
8
|
+
export const addKillListener = (listener: () => void | Promise<void>) => {
|
|
9
|
+
killListeners.push(listener)
|
|
10
|
+
}
|
|
11
|
+
export const addSoftKillListener = (listener: () => void | Promise<void>) => {
|
|
12
|
+
softKillListeners.push(listener)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function callKillListeners() {
|
|
16
|
+
log("👋 Calling kill listeners!")
|
|
17
|
+
await Promise.all(killListeners.map((listener) => listener()))
|
|
18
|
+
log("👋 Called all kill listeners!")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function callSoftKillListeners() {
|
|
22
|
+
log("👋 Calling soft kill listeners!")
|
|
23
|
+
await Promise.all(softKillListeners.map((listener) => listener()))
|
|
24
|
+
log("👋 Called all soft kill listeners!")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function killSubprocess(subprocess: Subprocess) {
|
|
28
|
+
log(`💀 Killing process: ${subprocess.pid}`)
|
|
29
|
+
await callSoftKillListeners()
|
|
30
|
+
subprocess.kill()
|
|
31
|
+
await subprocess.exited
|
|
32
|
+
}
|
package/src/pullChanges.tsx
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { log } from "@typed-assistant/logger"
|
|
2
|
-
import {
|
|
2
|
+
import { $ } from "bun"
|
|
3
3
|
import { bunInstall } from "./bunInstall"
|
|
4
4
|
|
|
5
5
|
export const pullChanges = async () => {
|
|
6
|
-
if (
|
|
7
|
-
!process.env.GITHUB_TOKEN ||
|
|
8
|
-
!process.env.GITHUB_USERNAME ||
|
|
9
|
-
!process.env.GITHUB_REPO
|
|
10
|
-
) {
|
|
11
|
-
log(
|
|
12
|
-
"⚠️ Cannot pull changes without GITHUB_TOKEN, GITHUB_USERNAME, and GITHUB_REPO environment variables.",
|
|
13
|
-
)
|
|
14
|
-
return { error: {} }
|
|
15
|
-
}
|
|
16
6
|
log("⬇️ Pulling changes...")
|
|
17
|
-
const gitPullText = await
|
|
7
|
+
const gitPullText = await $`git pull`.text()
|
|
18
8
|
const packageJSONUpdated = /package.json/.test(gitPullText)
|
|
19
9
|
const nothingNew = /Already up to date./.test(gitPullText)
|
|
20
10
|
if (nothingNew) {
|
|
@@ -25,8 +15,7 @@ export const pullChanges = async () => {
|
|
|
25
15
|
}
|
|
26
16
|
if (packageJSONUpdated) {
|
|
27
17
|
log(" 📦 package.json updated.")
|
|
28
|
-
|
|
29
|
-
if (error) throw new Error(error.text)
|
|
18
|
+
await bunInstall()
|
|
30
19
|
}
|
|
31
20
|
return {}
|
|
32
21
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { log } from "@typed-assistant/logger"
|
|
2
|
+
import { ONE_SECOND } from "@typed-assistant/utils/durations"
|
|
3
|
+
import { pullChanges } from "./pullChanges"
|
|
4
|
+
|
|
5
|
+
export const setupGitPoller = async ({
|
|
6
|
+
gitPullPollDuration,
|
|
7
|
+
}: {
|
|
8
|
+
/** Duration in seconds */
|
|
9
|
+
gitPullPollDuration?: number
|
|
10
|
+
} = {}) => {
|
|
11
|
+
const duration = gitPullPollDuration ?? 30
|
|
12
|
+
const { error } = await pullChanges()
|
|
13
|
+
if (error) return
|
|
14
|
+
log(` ⏳ Pulling changes again in ${duration} seconds...`)
|
|
15
|
+
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
setupGitPoller({ gitPullPollDuration })
|
|
18
|
+
}, duration * ONE_SECOND)
|
|
19
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { log } from "@typed-assistant/logger"
|
|
2
|
+
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
|
|
5
|
+
const webhookUrl = `${process.env.HASS_EXTERNAL_URL}/webhook`
|
|
6
|
+
|
|
7
|
+
const commonOptions = {
|
|
8
|
+
headers: {
|
|
9
|
+
Accept: "application/vnd.github+json",
|
|
10
|
+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
|
11
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const listRepoWebhooks = async () =>
|
|
16
|
+
withErrorHandling(() =>
|
|
17
|
+
fetch(
|
|
18
|
+
`https://api.github.com/repos/${process.env.GITHUB_USERNAME}/${process.env.GITHUB_REPO}/hooks`,
|
|
19
|
+
{ ...commonOptions },
|
|
20
|
+
)
|
|
21
|
+
.then(handleFetchError)
|
|
22
|
+
.then((d) => d?.json())
|
|
23
|
+
.then(z.array(Webhook).parse),
|
|
24
|
+
)()
|
|
25
|
+
|
|
26
|
+
const deleteRepoWebhook = async (id: number) =>
|
|
27
|
+
withErrorHandling(() =>
|
|
28
|
+
fetch(
|
|
29
|
+
`https://api.github.com/repos/${process.env.GITHUB_USERNAME}/${process.env.GITHUB_REPO}/hooks/${id}`,
|
|
30
|
+
{ ...commonOptions, method: "DELETE" },
|
|
31
|
+
),
|
|
32
|
+
)()
|
|
33
|
+
|
|
34
|
+
const deleteAllRepoWebhooks = async () => {
|
|
35
|
+
const { data: webhooks, error } = await listRepoWebhooks()
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
log("🚨 Failed fetching webhooks", error.message)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await Promise.all(
|
|
43
|
+
webhooks.map(async (webhook) => {
|
|
44
|
+
await deleteRepoWebhook(webhook.id)
|
|
45
|
+
log("🚮 Webhook deleted: ", webhook.config.url)
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const createRepoWebhook = async () =>
|
|
51
|
+
withErrorHandling(() =>
|
|
52
|
+
fetch(
|
|
53
|
+
`https://api.github.com/repos/${process.env.GITHUB_USERNAME}/${process.env.GITHUB_REPO}/hooks`,
|
|
54
|
+
{
|
|
55
|
+
...commonOptions,
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
name: "web",
|
|
59
|
+
active: true,
|
|
60
|
+
config: {
|
|
61
|
+
url: webhookUrl,
|
|
62
|
+
content_type: "json",
|
|
63
|
+
insecure_ssl: "0",
|
|
64
|
+
},
|
|
65
|
+
events: ["push"],
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
.then(handleFetchError)
|
|
70
|
+
.then((d) => d.json())
|
|
71
|
+
.then(Webhook.parse),
|
|
72
|
+
)()
|
|
73
|
+
|
|
74
|
+
const Webhook = z.object({
|
|
75
|
+
type: z.literal("Repository"),
|
|
76
|
+
id: z.number(),
|
|
77
|
+
name: z.literal("web"),
|
|
78
|
+
active: z.boolean(),
|
|
79
|
+
events: z.array(z.string()),
|
|
80
|
+
config: z.object({
|
|
81
|
+
content_type: z.string(),
|
|
82
|
+
insecure_ssl: z.enum(["0", "1"]),
|
|
83
|
+
url: z.string(),
|
|
84
|
+
}),
|
|
85
|
+
updated_at: z.string(),
|
|
86
|
+
created_at: z.string(),
|
|
87
|
+
url: z.string(),
|
|
88
|
+
test_url: z.string(),
|
|
89
|
+
ping_url: z.string(),
|
|
90
|
+
deliveries_url: z.string(),
|
|
91
|
+
last_response: z.object({
|
|
92
|
+
code: z.number().nullable(),
|
|
93
|
+
status: z.string().nullable(),
|
|
94
|
+
message: z.string().nullable(),
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
type Webhook = z.infer<typeof Webhook>
|
|
99
|
+
|
|
100
|
+
const handleFetchError = async (d: Response): Promise<Response> => {
|
|
101
|
+
if (!d.ok)
|
|
102
|
+
throw new Error(
|
|
103
|
+
d.status +
|
|
104
|
+
" " +
|
|
105
|
+
d.statusText +
|
|
106
|
+
(d.headers.get("Content-Type")?.includes("application/json")
|
|
107
|
+
? `:\n${JSON.stringify(await d.json(), null, 2)}`
|
|
108
|
+
: ""),
|
|
109
|
+
)
|
|
110
|
+
return d
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const retryTimeout = 2000
|
|
114
|
+
let retries = 0
|
|
115
|
+
export const setupWebhook = async (): Promise<void> => {
|
|
116
|
+
const { data: webhooks, error } = await listRepoWebhooks()
|
|
117
|
+
|
|
118
|
+
if (error) {
|
|
119
|
+
if (retries < 5) {
|
|
120
|
+
retries++
|
|
121
|
+
log(
|
|
122
|
+
`🔁 Failed fetching webhooks. Retrying setup in ${retryTimeout / 1000}s...`,
|
|
123
|
+
)
|
|
124
|
+
setTimeout(setupWebhook, retryTimeout)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
log("🚨 Failed fetching webhooks. Giving up.", error.message)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const webhookAlreadyExists = webhooks.some(
|
|
132
|
+
async (webhook) => webhook.config.url === webhookUrl,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (webhookAlreadyExists) {
|
|
136
|
+
log("🪝 Webhook already set up")
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { data: webhook, error: createError } = await createRepoWebhook()
|
|
141
|
+
|
|
142
|
+
if (createError) {
|
|
143
|
+
if (retries < 5) {
|
|
144
|
+
retries++
|
|
145
|
+
log(
|
|
146
|
+
`🔁 Failed creating webhook. Retrying setup in ${retryTimeout / 1000}s...`,
|
|
147
|
+
)
|
|
148
|
+
setTimeout(setupWebhook, retryTimeout)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
log("🚨 Failed creating webhook. Giving up.", createError.message)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log("🪝 Webhook created: ", webhook.config.url)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// await setupWebhook()
|
|
159
|
+
// await deleteAllRepoWebhooks()
|
package/src/setupWebserver.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { Elysia, t } from "elysia"
|
|
|
6
6
|
import { watch } from "fs"
|
|
7
7
|
import { basename, join } from "path"
|
|
8
8
|
import type { List, String } from "ts-toolbelt"
|
|
9
|
+
import { addKillListener, addSoftKillListener } from "./killProcess"
|
|
9
10
|
|
|
10
11
|
const indexHtmlFilePath = `${import.meta.dir}/webserver/index.html` as const
|
|
11
12
|
const cssFile = `${import.meta.dir}/webserver/input.css` as const
|
|
@@ -159,10 +160,9 @@ export const startWebappServer = async ({
|
|
|
159
160
|
}
|
|
160
161
|
})
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
console.log("👋 Closing log watcher...")
|
|
163
|
+
addKillListener(async () => {
|
|
164
164
|
watcher.close()
|
|
165
|
-
server.stop()
|
|
165
|
+
await server.stop()
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -170,7 +170,8 @@ export const startWebappServer = async ({
|
|
|
170
170
|
const stdoutReader = getReader(getSubprocesses().app.stdout)
|
|
171
171
|
const { value } = await stdoutReader.read()
|
|
172
172
|
|
|
173
|
-
const
|
|
173
|
+
const newLocal = decoder.decode(value)
|
|
174
|
+
const convertedMessage = convert.toHtml(newLocal)
|
|
174
175
|
if (convertedMessage !== "") {
|
|
175
176
|
lastMessage = convertedMessage
|
|
176
177
|
}
|
package/src/webserver/useWS.tsx
CHANGED
package/tsconfig.json
CHANGED
package/src/getSpawnText.tsx
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export async function getSpawnText(...args: Parameters<typeof Bun.spawn>) {
|
|
2
|
-
const { exitCode, exited, stderr, stdout } = Bun.spawn(...args)
|
|
3
|
-
await exited
|
|
4
|
-
if (exitCode === 0 || exitCode === null)
|
|
5
|
-
return await Bun.readableStreamToText(stdout)
|
|
6
|
-
if (!exitCode) return ""
|
|
7
|
-
throw new Error(
|
|
8
|
-
`Failed to run command: "${args.join(
|
|
9
|
-
" ",
|
|
10
|
-
)}". Exit code: ${exitCode}. Stderr: ${
|
|
11
|
-
stderr ? await Bun.readableStreamToText(stderr) : "None"
|
|
12
|
-
}`,
|
|
13
|
-
)
|
|
14
|
-
}
|