@typed-assistant/builder 0.0.9 â 0.0.11
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 +6 -4
- package/src/appProcess.tsx +13 -2
- package/src/setupWebserver.tsx +103 -0
- package/src/webserver/terminal.html +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-assistant/builder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"exports": {
|
|
5
5
|
"./appProcess": "./src/appProcess.tsx",
|
|
6
6
|
"./bunInstall": "./src/bunInstall.tsx",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"./pullChanges": "./src/pullChanges.tsx"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
+
"ansi-to-html": "^0.7.2",
|
|
12
|
+
"elysia": "^0.8.9",
|
|
11
13
|
"@mdi/svg": "^7.3.67",
|
|
12
14
|
"ignore": "^5.3.0"
|
|
13
15
|
},
|
|
@@ -17,10 +19,10 @@
|
|
|
17
19
|
"eslint": "^8.56.0",
|
|
18
20
|
"home-assistant-js-websocket": "^8.2.0",
|
|
19
21
|
"typescript": "^5.3.3",
|
|
20
|
-
"@typed-assistant/typescript-config": "0.0.4",
|
|
21
|
-
"@typed-assistant/utils": "0.0.7",
|
|
22
22
|
"@typed-assistant/eslint-config": "0.0.4",
|
|
23
|
-
"@typed-assistant/logger": "0.0.5"
|
|
23
|
+
"@typed-assistant/logger": "0.0.5",
|
|
24
|
+
"@typed-assistant/typescript-config": "0.0.4",
|
|
25
|
+
"@typed-assistant/utils": "0.0.7"
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
26
28
|
"home-assistant-js-websocket": "^8.2.0"
|
package/src/appProcess.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { ONE_SECOND } from "@typed-assistant/utils/durations"
|
|
|
8
8
|
import { getHassAPI, getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
|
|
9
9
|
import { pullChanges } from "./pullChanges"
|
|
10
10
|
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
11
|
+
import { startWebappServer } from "./setupWebserver"
|
|
11
12
|
|
|
12
13
|
type Processes = Awaited<ReturnType<typeof buildAndStartAppProcess>>
|
|
13
14
|
|
|
@@ -22,7 +23,10 @@ async function buildAndStartAppProcess(
|
|
|
22
23
|
async function startApp(appSourceFile: string = "src/entry.tsx") {
|
|
23
24
|
log("đ Starting app...")
|
|
24
25
|
const path = join(process.cwd(), appSourceFile)
|
|
25
|
-
return Bun.spawn(["bun", path], {
|
|
26
|
+
return Bun.spawn(["bun", path], {
|
|
27
|
+
stderr: "pipe",
|
|
28
|
+
env: { ...process.env, FORCE_COLOR: "1" },
|
|
29
|
+
})
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
async function kill(process: Subprocess) {
|
|
@@ -59,7 +63,6 @@ export async function setupWatcher(
|
|
|
59
63
|
if (addonInfoError) {
|
|
60
64
|
log(`đ¨ Failed to get addon info: ${addonInfoError}`)
|
|
61
65
|
}
|
|
62
|
-
console.log("âšī¸ ~ addonInfo:", addonInfo)
|
|
63
66
|
await setupGitSync()
|
|
64
67
|
|
|
65
68
|
let subprocesses = await buildAndStartAppProcess(...args)
|
|
@@ -78,9 +81,12 @@ export async function setupWatcher(
|
|
|
78
81
|
},
|
|
79
82
|
)
|
|
80
83
|
|
|
84
|
+
startWebappServer({getSubprocesses: () => subprocesses})
|
|
85
|
+
|
|
81
86
|
return subprocesses
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
|
|
84
90
|
const setupGitSync = async ({
|
|
85
91
|
gitPullPollDuration,
|
|
86
92
|
}: {
|
|
@@ -104,6 +110,11 @@ const shouldIgnoreFileOrFolder = (filename: string) =>
|
|
|
104
110
|
ig.ignores(relative(process.cwd(), filename))
|
|
105
111
|
|
|
106
112
|
const restartAddon = async () => {
|
|
113
|
+
if (!process.env.SUPERVISOR_TOKEN) {
|
|
114
|
+
log("âģī¸ Can't restart addon. Exiting...")
|
|
115
|
+
process.exit(0)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
107
118
|
log("âģī¸ Restarting addon...")
|
|
108
119
|
await getHassAPI(`http://supervisor/addons/self/restart`, { method: "POST" })
|
|
109
120
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Convert from "ansi-to-html"
|
|
2
|
+
import type { Context } from "elysia"
|
|
3
|
+
import { Elysia, t } from "elysia"
|
|
4
|
+
import type { Subprocess } from "bun"
|
|
5
|
+
import terminalHtmlUrl from "./webserver/terminal.html" with { type: "url" }
|
|
6
|
+
|
|
7
|
+
const convert = new Convert()
|
|
8
|
+
const decoder = new TextDecoder()
|
|
9
|
+
|
|
10
|
+
const getIngressPath = (req: Context["request"]) =>
|
|
11
|
+
req.headers.get("x-ingress-path") ?? ""
|
|
12
|
+
|
|
13
|
+
const readers = new Map<
|
|
14
|
+
ReadableStream<Uint8Array>,
|
|
15
|
+
ReadableStreamDefaultReader<Uint8Array>
|
|
16
|
+
>()
|
|
17
|
+
|
|
18
|
+
const getReader = (stream: ReadableStream<Uint8Array>) => {
|
|
19
|
+
const cachedReader = readers.get(stream)
|
|
20
|
+
if (!cachedReader) {
|
|
21
|
+
readers.forEach((_reader, cachedStream) => {
|
|
22
|
+
readers.delete(cachedStream)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
const reader = cachedReader ?? stream.getReader()
|
|
26
|
+
readers.set(stream, reader)
|
|
27
|
+
return reader
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const subscribers = new Map<number, (message: string) => void>()
|
|
31
|
+
|
|
32
|
+
let lastMessage = ""
|
|
33
|
+
|
|
34
|
+
export const startWebappServer = async ({
|
|
35
|
+
getSubprocesses,
|
|
36
|
+
}: {
|
|
37
|
+
getSubprocesses: () => {
|
|
38
|
+
app: Subprocess<"ignore", "pipe", "pipe">
|
|
39
|
+
}
|
|
40
|
+
}) => {
|
|
41
|
+
new Elysia()
|
|
42
|
+
.get("/", ({ request }) => {
|
|
43
|
+
getIngressPath(request)
|
|
44
|
+
return new Response(
|
|
45
|
+
`
|
|
46
|
+
<html>
|
|
47
|
+
<meta charset="UTF-8">
|
|
48
|
+
<body>
|
|
49
|
+
<a href="${getIngressPath(request)}/log.txt">Logs</a>
|
|
50
|
+
<a href="${getIngressPath(request)}/terminal">Terminal</a>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
53
|
+
`,
|
|
54
|
+
{ headers: { "content-type": "text/html" } },
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
.get("/terminal", Bun.file(terminalHtmlUrl))
|
|
58
|
+
.get("/log.txt", Bun.file("./log.txt"))
|
|
59
|
+
.get("/*", ({ request }) => {
|
|
60
|
+
getIngressPath(request)
|
|
61
|
+
return "Hello Elysia"
|
|
62
|
+
})
|
|
63
|
+
.ws("/ws", {
|
|
64
|
+
body: t.Object({
|
|
65
|
+
id: t.String(),
|
|
66
|
+
}),
|
|
67
|
+
async open(ws) {
|
|
68
|
+
ws.send("Connected successfully. Awaiting messages...")
|
|
69
|
+
subscribers.set(ws.id, (message) => {
|
|
70
|
+
ws.send(message)
|
|
71
|
+
})
|
|
72
|
+
console.log("đ
đ
đ
~ ws.id:", ws.id)
|
|
73
|
+
},
|
|
74
|
+
close(ws, code, message) {
|
|
75
|
+
console.log("ws closed", code, message, ws.id)
|
|
76
|
+
console.log("đ
đ
đ
~ ws.id:", ws.id)
|
|
77
|
+
subscribers.delete(ws.id)
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
.listen(8099)
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line no-constant-condition
|
|
83
|
+
while (true) {
|
|
84
|
+
const stdoutReader = getReader(getSubprocesses().app.stdout)
|
|
85
|
+
const { value } = await stdoutReader.read()
|
|
86
|
+
|
|
87
|
+
const convertedMessage = convert.toHtml(decoder.decode(value))
|
|
88
|
+
if (convertedMessage !== "") {
|
|
89
|
+
lastMessage = convertedMessage
|
|
90
|
+
}
|
|
91
|
+
if (convertedMessage === "") {
|
|
92
|
+
subscribers.forEach((send) =>
|
|
93
|
+
send(
|
|
94
|
+
"Process is returning an empty string. This was the last non-empty message:\n\n" +
|
|
95
|
+
lastMessage,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
subscribers.forEach((send) => send(convertedMessage))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<h1>Terminal</h1>
|
|
3
|
+
<pre id="log"></pre>
|
|
4
|
+
<style>
|
|
5
|
+
html {
|
|
6
|
+
background: #000;
|
|
7
|
+
color: #fff;
|
|
8
|
+
}
|
|
9
|
+
</style>
|
|
10
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ansi-to-html/0.7.2/ansi_to_html.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
function log(msg) {
|
|
13
|
+
document.getElementById("log").innerHTML = msg + "\n"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getWS = () => {
|
|
17
|
+
const url = new URL(window.location.href)
|
|
18
|
+
url.pathname = url.pathname.replace("/terminal", "/ws")
|
|
19
|
+
url.protocol = "ws:"
|
|
20
|
+
return new WebSocket(
|
|
21
|
+
// "ws://192.168.86.11:8123/api/hassio_ingress/wmQTEkorulChwnbeWV1GvPSwBsEpGzYlyLR70rdHzH0/ws",
|
|
22
|
+
// "ws://localhost:8099/ws",
|
|
23
|
+
url,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const setupWs = (ws) => {
|
|
28
|
+
ws.onopen = function () {
|
|
29
|
+
console.log("CONNECT")
|
|
30
|
+
}
|
|
31
|
+
ws.onclose = function () {
|
|
32
|
+
console.log("DISCONNECT")
|
|
33
|
+
document.getElementById("log").innerHTML +=
|
|
34
|
+
"Disconnected. Retrying..." + "\n"
|
|
35
|
+
retry()
|
|
36
|
+
}
|
|
37
|
+
ws.onmessage = function (event) {
|
|
38
|
+
log(event.data)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var ws = getWS()
|
|
43
|
+
console.log("đ
đ
đ
~ ws:", ws)
|
|
44
|
+
setupWs(ws)
|
|
45
|
+
|
|
46
|
+
const retry = () => {
|
|
47
|
+
console.log("Retrying in 1 second...")
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
ws = getWS()
|
|
53
|
+
setupWs(ws)
|
|
54
|
+
}, 1000)
|
|
55
|
+
}
|
|
56
|
+
</script>
|