@typed-assistant/builder 0.0.21 → 0.0.23
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 +151 -68
- package/src/bunInstall.tsx +6 -16
- package/src/killProcess.tsx +32 -0
- package/src/pullChanges.tsx +4 -15
- package/src/setupGitPoller.tsx +19 -0
- package/src/setupWebhook.tsx +159 -0
- package/src/setupWebserver.tsx +5 -4
- package/src/webserver/App.tsx +51 -12
- package/src/webserver/AppSection.tsx +1 -1
- 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.23",
|
|
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/utils": "0.0.
|
|
30
|
-
"@typed-assistant/
|
|
27
|
+
"@typed-assistant/eslint-config": "0.0.5",
|
|
28
|
+
"@typed-assistant/typescript-config": "0.0.5",
|
|
29
|
+
"@typed-assistant/utils": "0.0.8",
|
|
30
|
+
"@typed-assistant/logger": "0.0.9"
|
|
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,26 +1,33 @@
|
|
|
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
19
|
|
|
13
20
|
type Processes = Awaited<ReturnType<typeof buildAndStartAppProcess>>
|
|
14
21
|
|
|
15
22
|
async function buildAndStartAppProcess(
|
|
16
|
-
appSourceFile
|
|
17
|
-
options
|
|
23
|
+
appSourceFile: string,
|
|
24
|
+
options: Parameters<typeof generateTypes>[0],
|
|
18
25
|
) {
|
|
19
26
|
await generateTypes({ mdiPaths: options?.mdiPaths })
|
|
20
27
|
return { app: await startApp(appSourceFile) }
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
async function startApp(appSourceFile: string
|
|
30
|
+
async function startApp(appSourceFile: string) {
|
|
24
31
|
log("🚀 Starting app...")
|
|
25
32
|
const path = join(process.cwd(), appSourceFile)
|
|
26
33
|
return Bun.spawn(["bun", path], {
|
|
@@ -29,79 +36,109 @@ async function startApp(appSourceFile: string = "src/entry.tsx") {
|
|
|
29
36
|
})
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
async function kill(process: Subprocess) {
|
|
33
|
-
log(`💀 Killing process: ${process.pid}`)
|
|
34
|
-
process.kill()
|
|
35
|
-
await process.exited
|
|
36
|
-
}
|
|
37
|
-
|
|
38
39
|
let settingUp = { current: false }
|
|
39
|
-
async function killAndRestartApp(
|
|
40
|
+
async function killAndRestartApp(
|
|
41
|
+
entryFile: string,
|
|
42
|
+
options: Parameters<typeof buildAndStartAppProcess>[1],
|
|
43
|
+
subprocesses: Processes,
|
|
44
|
+
) {
|
|
40
45
|
if (settingUp.current) return subprocesses
|
|
41
46
|
log("♻️ Restarting app...")
|
|
42
47
|
settingUp.current = true
|
|
43
|
-
if (subprocesses.app) await
|
|
44
|
-
const newSubprocesses = await buildAndStartAppProcess()
|
|
48
|
+
if (subprocesses.app) await killSubprocess(subprocesses.app)
|
|
49
|
+
const newSubprocesses = await buildAndStartAppProcess(entryFile, options)
|
|
45
50
|
settingUp.current = false
|
|
46
51
|
return newSubprocesses
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
let multipleProcessesErrorCount = 0
|
|
55
|
+
let noProcessesErrorCount = 0
|
|
56
|
+
const checkProcesses = async (
|
|
57
|
+
entryFile: string,
|
|
58
|
+
{ onProcessError }: { onProcessError?: (message: string) => void },
|
|
59
|
+
) => {
|
|
60
|
+
const ps = await $`ps -f`.text()
|
|
61
|
+
const matches = ps.match(new RegExp(`bun .+${entryFile}`, "gmi")) ?? []
|
|
62
|
+
|
|
63
|
+
if (matches.length > 1) {
|
|
64
|
+
multipleProcessesErrorCount++
|
|
65
|
+
if (multipleProcessesErrorCount > 5) {
|
|
66
|
+
const message = `🚨 Multiple processes detected. Restarting TypedAssistant addon...`
|
|
67
|
+
log(message)
|
|
68
|
+
onProcessError?.(message)
|
|
69
|
+
restartAddon()
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
multipleProcessesErrorCount = 0
|
|
55
73
|
}
|
|
56
|
-
await setupGitSync()
|
|
57
74
|
|
|
58
|
-
|
|
75
|
+
if (matches.length === 0) {
|
|
76
|
+
noProcessesErrorCount++
|
|
77
|
+
if (noProcessesErrorCount > 5) {
|
|
78
|
+
const message = `🚨 No processes detected. Restarting TypedAssistant addon...`
|
|
79
|
+
log(message)
|
|
80
|
+
onProcessError?.(message)
|
|
81
|
+
restartAddon()
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
noProcessesErrorCount = 0
|
|
85
|
+
}
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
87
|
+
setTimeout(() => checkProcesses(entryFile, { onProcessError }), 5000)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function setup({
|
|
91
|
+
entryFile,
|
|
92
|
+
mdiPaths,
|
|
93
|
+
onProcessError,
|
|
94
|
+
}: {
|
|
95
|
+
entryFile: string
|
|
96
|
+
} & Parameters<typeof generateTypes>[0] &
|
|
97
|
+
Parameters<typeof checkProcesses>[1]) {
|
|
98
|
+
const addonInfo = await getAddonInfo()
|
|
99
|
+
const basePath = addonInfo?.data.ingress_entry ?? ""
|
|
100
|
+
const directoryToWatch = join(process.cwd(), "./src")
|
|
101
|
+
|
|
102
|
+
checkProcesses(entryFile, { onProcessError })
|
|
103
|
+
await setupGitSync()
|
|
77
104
|
|
|
105
|
+
let subprocesses = await buildAndStartAppProcess(entryFile, {
|
|
106
|
+
mdiPaths: mdiPaths,
|
|
107
|
+
})
|
|
78
108
|
startWebappServer({
|
|
79
|
-
basePath
|
|
109
|
+
basePath,
|
|
80
110
|
getSubprocesses: () => subprocesses,
|
|
81
111
|
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
setupWatcher({
|
|
113
|
+
directoryToWatch,
|
|
114
|
+
entryFile,
|
|
115
|
+
mdiPaths,
|
|
116
|
+
onSubprocessChange: (newSubprocesses) => {
|
|
117
|
+
subprocesses = newSubprocesses
|
|
118
|
+
},
|
|
119
|
+
getSubprocesses: () => subprocesses,
|
|
86
120
|
})
|
|
87
121
|
|
|
88
122
|
return subprocesses
|
|
89
123
|
}
|
|
90
124
|
|
|
91
|
-
const setupGitSync = async ({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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()
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
const ig = ignore().add(
|
|
@@ -123,14 +160,60 @@ const restartAddon = async () => {
|
|
|
123
160
|
const getAddonInfo = async () => {
|
|
124
161
|
log("🔍 Getting addon info...")
|
|
125
162
|
|
|
126
|
-
|
|
163
|
+
const { data, error } = await withErrorHandling(getSupervisorAPI)<{
|
|
127
164
|
data: { ingress_entry: string }
|
|
128
165
|
}>("/addons/self/info")
|
|
166
|
+
|
|
167
|
+
if (error) log(`🚨 Failed to get addon info: ${error}`)
|
|
168
|
+
|
|
169
|
+
return data
|
|
129
170
|
}
|
|
130
171
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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)
|
|
136
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
|
-
|
|
7
|
-
|
|
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
|
-
log("⬇️ Pulling changes...")
|
|
17
|
-
const gitPullText = await getSpawnText(["git", "pull"])
|
|
6
|
+
log("⬇️ Pulling changes...")
|
|
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/App.tsx
CHANGED
|
@@ -20,7 +20,12 @@ const App = () => {
|
|
|
20
20
|
|
|
21
21
|
const Logs = () => {
|
|
22
22
|
const [limit, setLimit] = useState(50)
|
|
23
|
-
const [
|
|
23
|
+
const [dateTimeVisibility, setDateTimeVisibility] = useState<
|
|
24
|
+
"hidden" | "timeOnly" | "visible"
|
|
25
|
+
>("timeOnly")
|
|
26
|
+
const [logs, setLogs] = useState<
|
|
27
|
+
{ date: string; time: string; message: string }[]
|
|
28
|
+
>([])
|
|
24
29
|
|
|
25
30
|
const ws = useWS({
|
|
26
31
|
subscribe: useCallback(
|
|
@@ -28,7 +33,13 @@ const Logs = () => {
|
|
|
28
33
|
[limit],
|
|
29
34
|
),
|
|
30
35
|
onMessage: useCallback((event) => {
|
|
31
|
-
setLogs(
|
|
36
|
+
setLogs(
|
|
37
|
+
(JSON.parse(event.data).logs as string[]).map((log: string) => {
|
|
38
|
+
const [date, time, message] =
|
|
39
|
+
log.match(/\[(.+), (.+)\] +(.+)/)?.slice(1) ?? []
|
|
40
|
+
return { date: date ?? "", time: time ?? "", message: message ?? log }
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
32
43
|
}, []),
|
|
33
44
|
})
|
|
34
45
|
|
|
@@ -39,15 +50,34 @@ const Logs = () => {
|
|
|
39
50
|
<h2 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
40
51
|
Logs <WSIndicator ws={ws.ws} />
|
|
41
52
|
</h2>
|
|
42
|
-
<div className="flex gap-2">
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
<div className="flex flex-wrap gap-2">
|
|
54
|
+
<div className="flex gap-2">
|
|
55
|
+
<label htmlFor="dateTimeVisibility">Date/Time</label>
|
|
56
|
+
<select
|
|
57
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
58
|
+
id="dateTimeVisibility"
|
|
59
|
+
onChange={(e) =>
|
|
60
|
+
setDateTimeVisibility(
|
|
61
|
+
e.target.value as typeof dateTimeVisibility,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
value={dateTimeVisibility}
|
|
65
|
+
>
|
|
66
|
+
<option value="hidden">Hidden</option>
|
|
67
|
+
<option value="timeOnly">Time only</option>
|
|
68
|
+
<option value="visible">Visible</option>
|
|
69
|
+
</select>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex gap-2">
|
|
72
|
+
<label htmlFor="limit">Limit</label>
|
|
73
|
+
<input
|
|
74
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
75
|
+
id="limit"
|
|
76
|
+
onChange={(e) => setLimit(Number(e.target.value))}
|
|
77
|
+
size={8}
|
|
78
|
+
value={limit}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
51
81
|
</div>
|
|
52
82
|
</>
|
|
53
83
|
)}
|
|
@@ -55,7 +85,16 @@ const Logs = () => {
|
|
|
55
85
|
<pre>
|
|
56
86
|
<ul>
|
|
57
87
|
{logs.map((log) => (
|
|
58
|
-
<li key={log
|
|
88
|
+
<li key={log.date + log.time + log.message} className="">
|
|
89
|
+
<span className="text-slate-400 mr-2">
|
|
90
|
+
{dateTimeVisibility === "hidden"
|
|
91
|
+
? null
|
|
92
|
+
: dateTimeVisibility === "timeOnly"
|
|
93
|
+
? log.time
|
|
94
|
+
: `${log.date} ${log.time}`}
|
|
95
|
+
</span>
|
|
96
|
+
{log.message}
|
|
97
|
+
</li>
|
|
59
98
|
))}
|
|
60
99
|
</ul>
|
|
61
100
|
</pre>
|
|
@@ -7,7 +7,7 @@ export const AppSection = ({
|
|
|
7
7
|
}) => {
|
|
8
8
|
return (
|
|
9
9
|
<div className="p-4 text-xs h-full max-h-dvh w-dvw md:w-auto overflow-x-auto flex flex-col">
|
|
10
|
-
<div className="flex gap-4 mb-4 items-center justify-between">
|
|
10
|
+
<div className="flex flex-wrap gap-4 mb-4 items-center justify-between">
|
|
11
11
|
{renderHeader()}
|
|
12
12
|
</div>
|
|
13
13
|
<div className="overflow-x-auto">{children}</div>
|
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
|
-
}
|