@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed-assistant/builder",
3
- "version": "0.0.9",
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"
@@ -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], { env: { FORCE_COLOR: "1" } })
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>