@typed-assistant/builder 0.0.36 → 0.0.38
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/package.json +3 -3
- package/src/appProcess.tsx +57 -70
- package/src/bunInstall.tsx +3 -3
- package/src/getAddonInfo.tsx +0 -1
- package/src/killProcess.tsx +4 -6
- package/src/pullChanges.tsx +5 -5
- package/src/restartAddon.tsx +3 -3
- package/src/setupGitPoller.tsx +4 -3
- package/src/setupWebhook.tsx +9 -12
- package/src/setupWebserver.tsx +9 -6
- package/src/webserver/App.tsx +4 -90
- package/src/webserver/Logs.tsx +120 -0
- package/src/webserver/Terminal.tsx +16 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-assistant/builder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"exports": {
|
|
5
5
|
"./appProcess": "./src/appProcess.tsx",
|
|
6
6
|
"./bunInstall": "./src/bunInstall.tsx",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"ts-toolbelt": "^9.6.0",
|
|
26
26
|
"typescript": "^5.3.3",
|
|
27
27
|
"@typed-assistant/eslint-config": "0.0.8",
|
|
28
|
-
"@typed-assistant/logger": "0.0.
|
|
28
|
+
"@typed-assistant/logger": "0.0.13",
|
|
29
29
|
"@typed-assistant/typescript-config": "0.0.8",
|
|
30
|
-
"@typed-assistant/utils": "0.0.
|
|
30
|
+
"@typed-assistant/utils": "0.0.14"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"home-assistant-js-websocket": "^8.2.0"
|
package/src/appProcess.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { generateTypes } from "@typed-assistant/types/generateTypes"
|
|
3
3
|
import type { Subprocess } from "bun"
|
|
4
4
|
import { $ } from "bun"
|
|
@@ -18,6 +18,44 @@ import { restartAddon } from "./restartAddon"
|
|
|
18
18
|
import { getAddonInfo as getAddonInfoAPI } from "./getAddonInfo"
|
|
19
19
|
import debounce from "debounce"
|
|
20
20
|
|
|
21
|
+
export async function setup({
|
|
22
|
+
entryFile,
|
|
23
|
+
mdiPaths,
|
|
24
|
+
onProcessError,
|
|
25
|
+
}: {
|
|
26
|
+
entryFile: string
|
|
27
|
+
} & Parameters<typeof generateTypes>[0] &
|
|
28
|
+
Pick<Parameters<typeof checkProcesses>[1], "onProcessError">) {
|
|
29
|
+
const addonInfo = await getAddonInfo()
|
|
30
|
+
const basePath = addonInfo?.data.ingress_entry ?? ""
|
|
31
|
+
const slug = addonInfo?.data.slug ?? ""
|
|
32
|
+
const directoryToWatch = join(process.cwd(), "./src")
|
|
33
|
+
const addonUrl = `${slug}/ingress`
|
|
34
|
+
|
|
35
|
+
checkProcesses(entryFile, { addonUrl, onProcessError })
|
|
36
|
+
await setupGitSync()
|
|
37
|
+
|
|
38
|
+
let subprocesses = await buildAndStartAppProcess(entryFile, {
|
|
39
|
+
mdiPaths: mdiPaths,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
startWebappServer({
|
|
43
|
+
basePath,
|
|
44
|
+
getSubprocesses: () => subprocesses,
|
|
45
|
+
})
|
|
46
|
+
setupWatcher({
|
|
47
|
+
directoryToWatch,
|
|
48
|
+
entryFile,
|
|
49
|
+
mdiPaths,
|
|
50
|
+
onSubprocessChange: (newSubprocesses) => {
|
|
51
|
+
subprocesses = newSubprocesses
|
|
52
|
+
},
|
|
53
|
+
getSubprocesses: () => subprocesses,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return subprocesses
|
|
57
|
+
}
|
|
58
|
+
|
|
21
59
|
type Processes = Awaited<ReturnType<typeof buildAndStartAppProcess>>
|
|
22
60
|
|
|
23
61
|
async function buildAndStartAppProcess(
|
|
@@ -29,7 +67,7 @@ async function buildAndStartAppProcess(
|
|
|
29
67
|
}
|
|
30
68
|
|
|
31
69
|
async function startApp(appSourceFile: string) {
|
|
32
|
-
|
|
70
|
+
logger.info("🚀 Starting app...")
|
|
33
71
|
const path = join(process.cwd(), appSourceFile)
|
|
34
72
|
return Bun.spawn(["bun", path], {
|
|
35
73
|
stderr: "pipe",
|
|
@@ -44,7 +82,7 @@ async function killAndRestartApp(
|
|
|
44
82
|
subprocesses: Processes,
|
|
45
83
|
) {
|
|
46
84
|
if (settingUp.current) return subprocesses
|
|
47
|
-
|
|
85
|
+
logger.fatal("♻️ Restarting app...")
|
|
48
86
|
settingUp.current = true
|
|
49
87
|
if (subprocesses.app) await killSubprocess(subprocesses.app)
|
|
50
88
|
const newSubprocesses = await buildAndStartAppProcess(entryFile, options)
|
|
@@ -59,29 +97,20 @@ const checkProcesses = async (
|
|
|
59
97
|
{
|
|
60
98
|
addonUrl,
|
|
61
99
|
onProcessError,
|
|
62
|
-
restartAddonUrl,
|
|
63
100
|
}: {
|
|
64
101
|
addonUrl: string
|
|
65
|
-
onProcessError?: (
|
|
66
|
-
message: string,
|
|
67
|
-
addonUrl: string,
|
|
68
|
-
restartAddonUrl?: string,
|
|
69
|
-
) => void
|
|
70
|
-
restartAddonUrl: string
|
|
102
|
+
onProcessError?: (message: string, addonUrl: string) => void
|
|
71
103
|
},
|
|
72
104
|
) => {
|
|
73
105
|
const ps = await $`ps -f`.text()
|
|
74
106
|
const matches = ps.match(new RegExp(`bun .+${entryFile}`, "gmi")) ?? []
|
|
75
|
-
onProcessError?.("message", addonUrl, restartAddonUrl)
|
|
76
107
|
|
|
77
108
|
if (matches.length > 1) {
|
|
78
109
|
multipleProcessesErrorCount++
|
|
79
110
|
if (multipleProcessesErrorCount > 5) {
|
|
80
|
-
const message = `🚨 Multiple processes detected.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
onProcessError?.(message, addonUrl, restartAddonUrl)
|
|
84
|
-
// restartAddon()
|
|
111
|
+
const message = `🚨 Multiple processes detected. Check the logs...`
|
|
112
|
+
logger.fatal(message, ps)
|
|
113
|
+
onProcessError?.(message, addonUrl)
|
|
85
114
|
return
|
|
86
115
|
}
|
|
87
116
|
} else {
|
|
@@ -91,11 +120,9 @@ const checkProcesses = async (
|
|
|
91
120
|
if (matches.length === 0) {
|
|
92
121
|
noProcessesErrorCount++
|
|
93
122
|
if (noProcessesErrorCount > 5) {
|
|
94
|
-
const message = `🚨 No processes detected.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
onProcessError?.(message, addonUrl, restartAddonUrl)
|
|
98
|
-
// restartAddon()
|
|
123
|
+
const message = `🚨 No processes detected. Check the logs...`
|
|
124
|
+
logger.fatal(message, ps)
|
|
125
|
+
onProcessError?.(message, addonUrl)
|
|
99
126
|
return
|
|
100
127
|
}
|
|
101
128
|
} else {
|
|
@@ -103,56 +130,17 @@ const checkProcesses = async (
|
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
setTimeout(
|
|
106
|
-
() =>
|
|
107
|
-
checkProcesses(entryFile, { addonUrl, onProcessError, restartAddonUrl }),
|
|
133
|
+
() => checkProcesses(entryFile, { addonUrl, onProcessError }),
|
|
108
134
|
5000,
|
|
109
135
|
)
|
|
110
136
|
}
|
|
111
137
|
|
|
112
|
-
export async function setup({
|
|
113
|
-
entryFile,
|
|
114
|
-
mdiPaths,
|
|
115
|
-
onProcessError,
|
|
116
|
-
}: {
|
|
117
|
-
entryFile: string
|
|
118
|
-
} & Parameters<typeof generateTypes>[0] &
|
|
119
|
-
Pick<Parameters<typeof checkProcesses>[1], "onProcessError">) {
|
|
120
|
-
const addonInfo = await getAddonInfo()
|
|
121
|
-
const basePath = addonInfo?.data.ingress_entry ?? ""
|
|
122
|
-
const directoryToWatch = join(process.cwd(), "./src")
|
|
123
|
-
const addonUrl = basePath
|
|
124
|
-
const restartAddonUrl = `${basePath}/restart-addon`
|
|
125
|
-
|
|
126
|
-
checkProcesses(entryFile, { addonUrl, onProcessError, restartAddonUrl })
|
|
127
|
-
await setupGitSync()
|
|
128
|
-
|
|
129
|
-
let subprocesses = await buildAndStartAppProcess(entryFile, {
|
|
130
|
-
mdiPaths: mdiPaths,
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
startWebappServer({
|
|
134
|
-
basePath,
|
|
135
|
-
getSubprocesses: () => subprocesses,
|
|
136
|
-
})
|
|
137
|
-
setupWatcher({
|
|
138
|
-
directoryToWatch,
|
|
139
|
-
entryFile,
|
|
140
|
-
mdiPaths,
|
|
141
|
-
onSubprocessChange: (newSubprocesses) => {
|
|
142
|
-
subprocesses = newSubprocesses
|
|
143
|
-
},
|
|
144
|
-
getSubprocesses: () => subprocesses,
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
return subprocesses
|
|
148
|
-
}
|
|
149
|
-
|
|
150
138
|
const getAddonInfo = async () => {
|
|
151
|
-
|
|
139
|
+
logger.info("🔍 Getting addon info...")
|
|
152
140
|
|
|
153
141
|
const { data, error } = await getAddonInfoAPI()
|
|
154
142
|
|
|
155
|
-
if (error)
|
|
143
|
+
if (error) logger.error(`🚨 Failed to get addon info: ${error}`)
|
|
156
144
|
|
|
157
145
|
return data
|
|
158
146
|
}
|
|
@@ -163,7 +151,7 @@ const setupGitSync = async () => {
|
|
|
163
151
|
!process.env.GITHUB_USERNAME ||
|
|
164
152
|
!process.env.GITHUB_REPO
|
|
165
153
|
) {
|
|
166
|
-
|
|
154
|
+
logger.warn(
|
|
167
155
|
"⚠️ Cannot sync with Github without Github token, username, and repo details. Add these in the add-on configuration.",
|
|
168
156
|
)
|
|
169
157
|
return { error: {} }
|
|
@@ -172,7 +160,7 @@ const setupGitSync = async () => {
|
|
|
172
160
|
await setupWebhook()
|
|
173
161
|
return
|
|
174
162
|
}
|
|
175
|
-
|
|
163
|
+
logger.warn("⚠️ No HASS_EXTERNAL_URL found. Setting up git poller...")
|
|
176
164
|
await setupGitPoller()
|
|
177
165
|
}
|
|
178
166
|
|
|
@@ -199,15 +187,14 @@ function setupWatcher({
|
|
|
199
187
|
app: Subprocess<"ignore", "pipe", "pipe">
|
|
200
188
|
}
|
|
201
189
|
}) {
|
|
202
|
-
|
|
190
|
+
logger.info("👀 Watching directory:", directoryToWatch)
|
|
203
191
|
const watcher = watch(
|
|
204
192
|
directoryToWatch,
|
|
205
193
|
{ recursive: true },
|
|
206
194
|
debounce(async function onFileChange(event, filename) {
|
|
207
|
-
console.log("😅😅😅 ~ event:", event)
|
|
208
195
|
if (!filename) return
|
|
209
196
|
if (shouldIgnoreFileOrFolder(filename)) return
|
|
210
|
-
|
|
197
|
+
logger.info(`⚠️ Change to ${filename} detected.`)
|
|
211
198
|
if (filename.endsWith("process.tsx")) {
|
|
212
199
|
await killSubprocess(getSubprocesses().app)
|
|
213
200
|
await restartAddon()
|
|
@@ -227,13 +214,13 @@ function setupWatcher({
|
|
|
227
214
|
}
|
|
228
215
|
|
|
229
216
|
process.on("SIGINT", async () => {
|
|
230
|
-
|
|
217
|
+
logger.fatal("👋 Exiting...")
|
|
231
218
|
await callSoftKillListeners()
|
|
232
219
|
await callKillListeners()
|
|
233
220
|
process.exit(0)
|
|
234
221
|
})
|
|
235
222
|
process.on("SIGTERM", async () => {
|
|
236
|
-
|
|
223
|
+
logger.fatal("👋 Exiting...")
|
|
237
224
|
await callSoftKillListeners()
|
|
238
225
|
await callKillListeners()
|
|
239
226
|
process.exit(0)
|
package/src/bunInstall.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { $ } from "bun"
|
|
3
3
|
|
|
4
4
|
export async function bunInstall() {
|
|
5
|
-
|
|
5
|
+
logger.info("🏗️ Running bun install...")
|
|
6
6
|
return $`bun install --frozen-lockfile --cache-dir=.bun-cache`
|
|
7
7
|
.text()
|
|
8
8
|
.catch((error) => {
|
|
9
|
-
|
|
9
|
+
logger.error(`🚨 Failed to run bun install: ${error}`)
|
|
10
10
|
})
|
|
11
11
|
}
|
package/src/getAddonInfo.tsx
CHANGED
package/src/killProcess.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import type { Subprocess } from "bun"
|
|
3
3
|
import { $ } from "bun"
|
|
4
4
|
|
|
@@ -13,19 +13,17 @@ export const addSoftKillListener = (listener: () => void | Promise<void>) => {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export async function callKillListeners() {
|
|
16
|
-
log("👋 Calling kill listeners!")
|
|
17
16
|
await Promise.all(killListeners.map((listener) => listener()))
|
|
18
|
-
|
|
17
|
+
logger.info("👋 Called all kill listeners!")
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export async function callSoftKillListeners() {
|
|
22
|
-
log("👋 Calling soft kill listeners!")
|
|
23
21
|
await Promise.all(softKillListeners.map((listener) => listener()))
|
|
24
|
-
|
|
22
|
+
logger.info("👋 Called all soft kill listeners!")
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export async function killSubprocess(subprocess: Subprocess) {
|
|
28
|
-
|
|
26
|
+
logger.fatal(`💀 Killing process: ${subprocess.pid}`)
|
|
29
27
|
await callSoftKillListeners()
|
|
30
28
|
subprocess.kill()
|
|
31
29
|
await subprocess.exited
|
package/src/pullChanges.tsx
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { $ } from "bun"
|
|
3
3
|
import { bunInstall } from "./bunInstall"
|
|
4
4
|
|
|
5
5
|
export const pullChanges = async () => {
|
|
6
|
-
|
|
6
|
+
logger.info("⬇️ Pulling changes...")
|
|
7
7
|
const gitPullText = await $`git pull`.text()
|
|
8
8
|
const packageJSONUpdated = /package.json/.test(gitPullText)
|
|
9
9
|
const nothingNew = /Already up to date./.test(gitPullText)
|
|
10
10
|
if (nothingNew) {
|
|
11
|
-
|
|
11
|
+
logger.info(" 👌 No new changes.")
|
|
12
12
|
return {}
|
|
13
13
|
} else {
|
|
14
|
-
|
|
14
|
+
logger.info(" 👍 Changes pulled.")
|
|
15
15
|
}
|
|
16
16
|
if (packageJSONUpdated) {
|
|
17
|
-
|
|
17
|
+
logger.info(" 📦 package.json updated.")
|
|
18
18
|
await bunInstall()
|
|
19
19
|
}
|
|
20
20
|
return {}
|
package/src/restartAddon.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
|
|
3
3
|
|
|
4
4
|
export const restartAddon = async () => {
|
|
5
5
|
if (!process.env.SUPERVISOR_TOKEN) {
|
|
6
|
-
|
|
6
|
+
logger.fatal("♻️ Can't restart addon. Exiting...")
|
|
7
7
|
process.exit(1)
|
|
8
8
|
return
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
logger.fatal("♻️ Restarting addon...")
|
|
11
11
|
await getSupervisorAPI(`/addons/self/restart`, { method: "POST" })
|
|
12
12
|
}
|
package/src/setupGitPoller.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { ONE_SECOND } from "@typed-assistant/utils/durations"
|
|
3
3
|
import { pullChanges } from "./pullChanges"
|
|
4
|
+
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
4
5
|
|
|
5
6
|
export const setupGitPoller = async ({
|
|
6
7
|
gitPullPollDuration,
|
|
@@ -9,9 +10,9 @@ export const setupGitPoller = async ({
|
|
|
9
10
|
gitPullPollDuration?: number
|
|
10
11
|
} = {}) => {
|
|
11
12
|
const duration = gitPullPollDuration ?? 30
|
|
12
|
-
const { error } = await pullChanges()
|
|
13
|
+
const { error } = await withErrorHandling(pullChanges)()
|
|
13
14
|
if (error) return
|
|
14
|
-
|
|
15
|
+
logger.info(` ⏳ Pulling changes again in ${duration} seconds...`)
|
|
15
16
|
|
|
16
17
|
setTimeout(() => {
|
|
17
18
|
setupGitPoller({ gitPullPollDuration })
|
package/src/setupWebhook.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { handleFetchError } from "@typed-assistant/utils/getHassAPI"
|
|
3
3
|
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
4
4
|
import { z } from "zod"
|
|
@@ -36,14 +36,14 @@ const deleteAllRepoWebhooks = async () => {
|
|
|
36
36
|
const { data: webhooks, error } = await listRepoWebhooks()
|
|
37
37
|
|
|
38
38
|
if (error) {
|
|
39
|
-
|
|
39
|
+
logger.error("🚨 Failed fetching webhooks", error.message)
|
|
40
40
|
return
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
await Promise.all(
|
|
44
44
|
webhooks.map(async (webhook) => {
|
|
45
45
|
await deleteRepoWebhook(webhook.id)
|
|
46
|
-
|
|
46
|
+
logger.info("🚮 Webhook deleted: ", webhook.config.url)
|
|
47
47
|
}),
|
|
48
48
|
)
|
|
49
49
|
}
|
|
@@ -106,13 +106,13 @@ export const setupWebhook = async (): Promise<void> => {
|
|
|
106
106
|
if (error) {
|
|
107
107
|
if (retries < 5) {
|
|
108
108
|
retries++
|
|
109
|
-
|
|
109
|
+
logger.error(
|
|
110
110
|
`🔁 Failed fetching webhooks. Retrying setup in ${retryTimeout / 1000}s...`,
|
|
111
111
|
)
|
|
112
112
|
setTimeout(setupWebhook, retryTimeout)
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
logger.error("🚨 Failed fetching webhooks. Giving up.", error.message)
|
|
116
116
|
return
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -121,7 +121,7 @@ export const setupWebhook = async (): Promise<void> => {
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
if (webhookAlreadyExists) {
|
|
124
|
-
|
|
124
|
+
logger.info("🪝 Webhook already set up")
|
|
125
125
|
return
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -130,18 +130,15 @@ export const setupWebhook = async (): Promise<void> => {
|
|
|
130
130
|
if (createError) {
|
|
131
131
|
if (retries < 5) {
|
|
132
132
|
retries++
|
|
133
|
-
|
|
133
|
+
logger.error(
|
|
134
134
|
`🔁 Failed creating webhook. Retrying setup in ${retryTimeout / 1000}s...`,
|
|
135
135
|
)
|
|
136
136
|
setTimeout(setupWebhook, retryTimeout)
|
|
137
137
|
return
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
logger.error("🚨 Failed creating webhook. Giving up.", createError.message)
|
|
140
140
|
return
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
logger.info("🪝 Webhook created: ", webhook.config.url)
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
// await setupWebhook()
|
|
147
|
-
// await deleteAllRepoWebhooks()
|
package/src/setupWebserver.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import Convert from "ansi-to-html"
|
|
3
3
|
import type { Subprocess } from "bun"
|
|
4
4
|
import { $ } from "bun"
|
|
@@ -77,10 +77,10 @@ export const startWebappServer = async ({
|
|
|
77
77
|
}
|
|
78
78
|
throw new Error("Build failed")
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
logger.info("🛠️ Web server built successfully")
|
|
81
81
|
|
|
82
82
|
await $`bunx tailwindcss -c ${tailwindConfig} -i ${cssFile} -o ${cssOutputFile}`.quiet()
|
|
83
|
-
|
|
83
|
+
logger.info("💄 Tailwind built successfully")
|
|
84
84
|
|
|
85
85
|
const indexHtml = (await Bun.file(indexHtmlFilePath).text())
|
|
86
86
|
.replace(
|
|
@@ -108,6 +108,7 @@ export const startWebappServer = async ({
|
|
|
108
108
|
.get("/restart-addon", async () => {
|
|
109
109
|
await killSubprocess(getSubprocesses().app)
|
|
110
110
|
restartAddon()
|
|
111
|
+
return { message: "Restarting addon..." }
|
|
111
112
|
})
|
|
112
113
|
.get("/addon-info", async () => {
|
|
113
114
|
const { data, error } = await getAddonInfo()
|
|
@@ -176,10 +177,10 @@ export const startWebappServer = async ({
|
|
|
176
177
|
})
|
|
177
178
|
|
|
178
179
|
server.listen(8099)
|
|
179
|
-
|
|
180
|
+
logger.info("🌐 Web server listening on port 8099")
|
|
180
181
|
|
|
181
182
|
const directory = join(process.cwd(), ".")
|
|
182
|
-
|
|
183
|
+
logger.info("👀 Watching log.txt")
|
|
183
184
|
const watcher = watch(directory, function onFileChange(_event, filename) {
|
|
184
185
|
if (filename === "log.txt") {
|
|
185
186
|
logSubscribers.forEach((send) => send())
|
|
@@ -226,7 +227,9 @@ export type WebServer = Awaited<ReturnType<typeof startWebappServer>>
|
|
|
226
227
|
const getLogsFromFile = async (limit?: string) => {
|
|
227
228
|
try {
|
|
228
229
|
const lines = (await Bun.file("./log.txt").text()).split("\n")
|
|
229
|
-
const logFile = limit
|
|
230
|
+
const logFile = limit
|
|
231
|
+
? lines.slice(lines.length - Number(limit), lines.length - 1)
|
|
232
|
+
: lines
|
|
230
233
|
return { logs: logFile }
|
|
231
234
|
} catch (e) {
|
|
232
235
|
return { logs: ["Error reading log.txt file"] }
|
package/src/webserver/App.tsx
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { useCallback, useState } from "react"
|
|
2
|
-
import { AppSection } from "./AppSection"
|
|
3
1
|
import { Terminal } from "./Terminal"
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { Logs } from "./Logs"
|
|
3
|
+
|
|
4
|
+
const basePath = process.env.BASE_PATH ?? ""
|
|
7
5
|
|
|
8
6
|
const App = () => {
|
|
9
7
|
return (
|
|
10
8
|
<div className="grid md:grid-cols-3">
|
|
11
9
|
<div className="col-span-2">
|
|
12
|
-
<Terminal />
|
|
10
|
+
<Terminal basePath={basePath} />
|
|
13
11
|
</div>
|
|
14
12
|
<div className="col-span-1">
|
|
15
13
|
<Logs />
|
|
@@ -18,88 +16,4 @@ const App = () => {
|
|
|
18
16
|
)
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
const Logs = () => {
|
|
22
|
-
const [limit, setLimit] = useState(50)
|
|
23
|
-
const [dateTimeVisibility, setDateTimeVisibility] = useState<
|
|
24
|
-
"hidden" | "timeOnly" | "visible"
|
|
25
|
-
>("timeOnly")
|
|
26
|
-
const [logs, setLogs] = useState<
|
|
27
|
-
{ date: string; time: string; message: string }[]
|
|
28
|
-
>([])
|
|
29
|
-
|
|
30
|
-
const ws = useWS({
|
|
31
|
-
subscribe: useCallback(
|
|
32
|
-
() => app.logsws.subscribe({ $query: { limit: limit.toString() } }),
|
|
33
|
-
[limit],
|
|
34
|
-
),
|
|
35
|
-
onMessage: useCallback((event) => {
|
|
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
|
-
)
|
|
43
|
-
}, []),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<AppSection
|
|
48
|
-
renderHeader={() => (
|
|
49
|
-
<>
|
|
50
|
-
<h2 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
51
|
-
Logs <WSIndicator ws={ws.ws} />
|
|
52
|
-
</h2>
|
|
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>
|
|
81
|
-
</div>
|
|
82
|
-
</>
|
|
83
|
-
)}
|
|
84
|
-
>
|
|
85
|
-
<pre>
|
|
86
|
-
<ul>
|
|
87
|
-
{logs.map((log, index) => (
|
|
88
|
-
<li key={(log.date ?? index) + 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>
|
|
98
|
-
))}
|
|
99
|
-
</ul>
|
|
100
|
-
</pre>
|
|
101
|
-
</AppSection>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
19
|
export default App
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useCallback, useState } from "react"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { AppSection } from "./AppSection"
|
|
4
|
+
import { WSIndicator } from "./WSIndicator"
|
|
5
|
+
import { app } from "./api"
|
|
6
|
+
import { useWS } from "./useWS"
|
|
7
|
+
import { getPrettyTimestamp } from "@typed-assistant/utils/getPrettyTimestamp"
|
|
8
|
+
import { levels } from "@typed-assistant/logger/levels"
|
|
9
|
+
|
|
10
|
+
const LogSchema = z.object({
|
|
11
|
+
level: z.number(),
|
|
12
|
+
time: z.number(),
|
|
13
|
+
pid: z.number(),
|
|
14
|
+
hostname: z.string(),
|
|
15
|
+
msg: z.string(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
type LogSchema = z.infer<typeof LogSchema>
|
|
19
|
+
|
|
20
|
+
export const Logs = () => {
|
|
21
|
+
const [limit, setLimit] = useState(50)
|
|
22
|
+
const [level, setLevel] = useState<
|
|
23
|
+
"trace" | "debug" | "info" | "warn" | "error" | "fatal"
|
|
24
|
+
>("trace")
|
|
25
|
+
const [dateTimeVisibility, setDateTimeVisibility] = useState<
|
|
26
|
+
"hidden" | "timeOnly" | "visible"
|
|
27
|
+
>("timeOnly")
|
|
28
|
+
const [logs, setLogs] = useState<LogSchema[]>([])
|
|
29
|
+
|
|
30
|
+
const ws = useWS({
|
|
31
|
+
subscribe: useCallback(
|
|
32
|
+
() => app.logsws.subscribe({ $query: { limit: limit.toString() } }),
|
|
33
|
+
[limit],
|
|
34
|
+
),
|
|
35
|
+
onMessage: useCallback((event) => {
|
|
36
|
+
setLogs(
|
|
37
|
+
(JSON.parse(event.data).logs as string[]).map((log: string) =>
|
|
38
|
+
LogSchema.parse(JSON.parse(log)),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
}, []),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AppSection
|
|
46
|
+
renderHeader={() => (
|
|
47
|
+
<>
|
|
48
|
+
<h2 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
49
|
+
Logs <WSIndicator ws={ws.ws} />
|
|
50
|
+
</h2>
|
|
51
|
+
<div className="flex flex-wrap gap-2">
|
|
52
|
+
<div className="flex gap-2">
|
|
53
|
+
<label htmlFor="dateTimeVisibility">Date/Time</label>
|
|
54
|
+
<select
|
|
55
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
56
|
+
id="dateTimeVisibility"
|
|
57
|
+
onChange={(e) =>
|
|
58
|
+
setDateTimeVisibility(
|
|
59
|
+
e.target.value as typeof dateTimeVisibility,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
value={dateTimeVisibility}
|
|
63
|
+
>
|
|
64
|
+
<option value="hidden">Hidden</option>
|
|
65
|
+
<option value="timeOnly">Time only</option>
|
|
66
|
+
<option value="visible">Visible</option>
|
|
67
|
+
</select>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex gap-2">
|
|
70
|
+
<label htmlFor="dateTimeVisibility">Level</label>
|
|
71
|
+
<select
|
|
72
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
73
|
+
id="level"
|
|
74
|
+
onChange={(e) => setLevel(e.target.value as typeof level)}
|
|
75
|
+
value={level}
|
|
76
|
+
>
|
|
77
|
+
<option value="trace">Trace</option>
|
|
78
|
+
<option value="debug">Debug</option>
|
|
79
|
+
<option value="info">Info</option>
|
|
80
|
+
<option value="warn">Warn</option>
|
|
81
|
+
<option value="error">Error</option>
|
|
82
|
+
<option value="fatal">Fatal</option>
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<label htmlFor="limit">Limit</label>
|
|
87
|
+
<input
|
|
88
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
89
|
+
id="limit"
|
|
90
|
+
onChange={(e) => setLimit(Number(e.target.value))}
|
|
91
|
+
size={8}
|
|
92
|
+
value={limit}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<pre>
|
|
100
|
+
<ul>
|
|
101
|
+
{logs
|
|
102
|
+
.filter((log) => log.level >= (levels[level] ?? 0))
|
|
103
|
+
.sort((a, b) => b.time - a.time)
|
|
104
|
+
.map((log, index) => (
|
|
105
|
+
<li key={(log.time ?? index) + log.time + log.msg} className="">
|
|
106
|
+
<span className="text-slate-400 mr-2">
|
|
107
|
+
{dateTimeVisibility === "hidden"
|
|
108
|
+
? null
|
|
109
|
+
: dateTimeVisibility === "timeOnly"
|
|
110
|
+
? new Date(log.time).toLocaleTimeString("en-GB")
|
|
111
|
+
: getPrettyTimestamp(log.time)}
|
|
112
|
+
</span>
|
|
113
|
+
{log.msg}
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
</pre>
|
|
118
|
+
</AppSection>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -4,7 +4,7 @@ import { useWS } from "./useWS"
|
|
|
4
4
|
import { WSIndicator } from "./WSIndicator"
|
|
5
5
|
import { AppSection } from "./AppSection"
|
|
6
6
|
|
|
7
|
-
export function Terminal() {
|
|
7
|
+
export function Terminal({ basePath }: { basePath: string }) {
|
|
8
8
|
const [content, setContent] = useState("")
|
|
9
9
|
const ws = useWS({
|
|
10
10
|
subscribe: useCallback(() => app.ws.subscribe(), []),
|
|
@@ -14,12 +14,24 @@ export function Terminal() {
|
|
|
14
14
|
return (
|
|
15
15
|
<AppSection
|
|
16
16
|
renderHeader={() => (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<>
|
|
18
|
+
<h1 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
19
|
+
TypedAssistant <WSIndicator ws={ws.ws} />
|
|
20
|
+
</h1>
|
|
21
|
+
<div className="flex flex-wrap gap-2">
|
|
22
|
+
<a className={buttonStyle} href={`${basePath}/restart-addon`}>
|
|
23
|
+
Restart addon
|
|
24
|
+
</a>
|
|
25
|
+
<a className={buttonStyle} href={`${basePath}/log.txt?limit=500`}>
|
|
26
|
+
View log.txt
|
|
27
|
+
</a>
|
|
28
|
+
</div>
|
|
29
|
+
</>
|
|
20
30
|
)}
|
|
21
31
|
>
|
|
22
32
|
<pre dangerouslySetInnerHTML={{ __html: content }} />
|
|
23
33
|
</AppSection>
|
|
24
34
|
)
|
|
25
35
|
}
|
|
36
|
+
|
|
37
|
+
const buttonStyle = "bg-slate-800 text-white px-3 py-1 rounded-md"
|