@typed-assistant/builder 0.0.10 → 0.0.12

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.10",
3
+ "version": "0.0.12",
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
  },
@@ -18,8 +20,8 @@
18
20
  "home-assistant-js-websocket": "^8.2.0",
19
21
  "typescript": "^5.3.3",
20
22
  "@typed-assistant/eslint-config": "0.0.4",
21
- "@typed-assistant/logger": "0.0.5",
22
23
  "@typed-assistant/typescript-config": "0.0.4",
24
+ "@typed-assistant/logger": "0.0.5",
23
25
  "@typed-assistant/utils": "0.0.7"
24
26
  },
25
27
  "peerDependencies": {
@@ -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: { ...process.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) {
@@ -56,10 +60,10 @@ export async function setupWatcher(
56
60
  ...args: Parameters<typeof buildAndStartAppProcess>
57
61
  ) {
58
62
  const { data: addonInfo, error: addonInfoError } = await getAddonInfo()
63
+ console.log("😅😅😅 ~ addonInfo:", addonInfo)
59
64
  if (addonInfoError) {
60
65
  log(`🚨 Failed to get addon info: ${addonInfoError}`)
61
66
  }
62
- console.log("â„šī¸ ~ addonInfo:", addonInfo)
63
67
  await setupGitSync()
64
68
 
65
69
  let subprocesses = await buildAndStartAppProcess(...args)
@@ -78,6 +82,8 @@ export async function setupWatcher(
78
82
  },
79
83
  )
80
84
 
85
+ startWebappServer({ getSubprocesses: () => subprocesses })
86
+
81
87
  return subprocesses
82
88
  }
83
89
 
@@ -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,146 @@
1
+ import { log } from "@typed-assistant/logger"
2
+ import Convert from "ansi-to-html"
3
+ import type { Subprocess } from "bun"
4
+ import { $ } from "bun"
5
+ import type { Context } from "elysia"
6
+ import { Elysia } from "elysia"
7
+ import { basename, join } from "path"
8
+
9
+ const indexHtmlFilePath = `${import.meta.dir}/webserver/index.html`
10
+ const cssFile = `${import.meta.dir}/webserver/input.css`
11
+ const terminalHtmlUrl = `${import.meta.dir}/webserver/terminal.html`
12
+ const tsEntryPoint = `${import.meta.dir}/webserver/index.tsx`
13
+ const tailwindConfig = `${import.meta.dir}/webserver/tailwind.config.js`
14
+ const cssOutputFile = join(process.cwd(), `./build/output.css`)
15
+
16
+ const convert = new Convert()
17
+ const decoder = new TextDecoder()
18
+
19
+ const getIngressPath = (req: Context["request"]) =>
20
+ req.headers.get("x-ingress-path") ?? ""
21
+
22
+ const readers = new Map<
23
+ ReadableStream<Uint8Array>,
24
+ ReadableStreamDefaultReader<Uint8Array>
25
+ >()
26
+
27
+ const getReader = (stream: ReadableStream<Uint8Array>) => {
28
+ const cachedReader = readers.get(stream)
29
+ if (!cachedReader) {
30
+ readers.forEach((_reader, cachedStream) => {
31
+ readers.delete(cachedStream)
32
+ })
33
+ }
34
+ const reader = cachedReader ?? stream.getReader()
35
+ readers.set(stream, reader)
36
+ return reader
37
+ }
38
+
39
+ const subscribers = new Map<number, (message: string) => void>()
40
+
41
+ let lastMessage = ""
42
+
43
+ export const startWebappServer = async ({
44
+ getSubprocesses,
45
+ }: {
46
+ getSubprocesses: () => {
47
+ app: Subprocess<"ignore", "pipe", "pipe">
48
+ }
49
+ }) => {
50
+ const buildResult = await Bun.build({
51
+ entrypoints: [tsEntryPoint],
52
+ outdir: "./build",
53
+ define: {
54
+ "process.env.BASE_PATH": "'lmao'",
55
+ },
56
+ })
57
+ if (!buildResult.success) {
58
+ for (const message of buildResult.logs) {
59
+ // Bun will pretty print the message object
60
+ console.error(message)
61
+ }
62
+ throw new Error("Build failed")
63
+ }
64
+ log("đŸ› ī¸ Web server built successfully")
65
+
66
+ await $`bunx tailwindcss -c ${tailwindConfig} -i ${cssFile} -o ${cssOutputFile}`.quiet()
67
+ log("💄 Tailwind built successfully")
68
+
69
+ const indexHtml = (await Bun.file(indexHtmlFilePath).text())
70
+ .replace("{{ STYLESHEET }}", `/assets/${basename(cssOutputFile)}`)
71
+ .replace(
72
+ "{{ SCRIPTS }}",
73
+ buildResult.outputs
74
+ .map(
75
+ (output) =>
76
+ `<script type="module" src="/assets/${basename(output.path)}"></script>`,
77
+ )
78
+ .join("\n"),
79
+ )
80
+
81
+ const server = new Elysia()
82
+ .get("/", ({ request }) => {
83
+ getIngressPath(request)
84
+ return new Response(
85
+ `
86
+ <html>
87
+ <meta charset="UTF-8">
88
+ <body>
89
+ <a href="${getIngressPath(request)}/log.txt">Logs</a>
90
+ <a href="${getIngressPath(request)}/terminal">Terminal</a>
91
+ </body>
92
+ </html>
93
+ `,
94
+ { headers: { "content-type": "text/html" } },
95
+ )
96
+ })
97
+ .get("/terminal", Bun.file(terminalHtmlUrl))
98
+ .get(
99
+ "/terminal2",
100
+ () =>
101
+ new Response(indexHtml, {
102
+ headers: { "content-type": "text/html" },
103
+ }),
104
+ )
105
+ .get("/log.txt", Bun.file("./log.txt"))
106
+ .ws("/ws", {
107
+ async open(ws) {
108
+ ws.send("Connected successfully. Awaiting messages...")
109
+ subscribers.set(ws.id, (message) => {
110
+ ws.send(message)
111
+ })
112
+ },
113
+ close(ws) {
114
+ subscribers.delete(ws.id)
115
+ },
116
+ })
117
+
118
+ server.get(`/assets/${basename(cssOutputFile)}`, Bun.file(cssOutputFile))
119
+ buildResult.outputs.forEach((output) => {
120
+ server.get(`/assets/${basename(output.path)}`, Bun.file(output.path))
121
+ })
122
+
123
+ server.listen(8099)
124
+
125
+ // eslint-disable-next-line no-constant-condition
126
+ while (true) {
127
+ const stdoutReader = getReader(getSubprocesses().app.stdout)
128
+ const { value } = await stdoutReader.read()
129
+
130
+ const convertedMessage = convert.toHtml(decoder.decode(value))
131
+ if (convertedMessage !== "") {
132
+ lastMessage = convertedMessage
133
+ }
134
+ if (convertedMessage === "") {
135
+ subscribers.forEach((send) =>
136
+ send(
137
+ "Process is returning an empty string. This was the last non-empty message:\n\n" +
138
+ lastMessage,
139
+ ),
140
+ )
141
+ await new Promise((resolve) => setTimeout(resolve, 1000))
142
+ continue
143
+ }
144
+ subscribers.forEach((send) => send(convertedMessage))
145
+ }
146
+ }
@@ -0,0 +1,64 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ const basePath = process.env.BASE_PATH
4
+ console.log("😅😅😅 ~ basePath:", basePath)
5
+
6
+ const getWS = () => {
7
+ const url = new URL(window.location.href)
8
+ const endPathname = url.pathname.split("/")
9
+ url.pathname = url.pathname.replace(
10
+ `/${endPathname[endPathname.length - 1]}`,
11
+ "/ws",
12
+ )
13
+ url.protocol = "ws:"
14
+ const ws = new WebSocket(url)
15
+
16
+ return ws
17
+ }
18
+
19
+ export function Terminal() {
20
+ const [content, setContent] = useState("")
21
+ const [ws, setWS] = useState<WebSocket>(() => getWS())
22
+
23
+ useEffect(() => {
24
+ let timeout: NodeJS.Timeout
25
+
26
+ ws.onclose = function () {
27
+ timeout = setTimeout(() => {
28
+ if (ws.readyState === WebSocket.OPEN) return
29
+ setWS(getWS())
30
+ }, 1000)
31
+ }
32
+
33
+ ws.onmessage = function (event) {
34
+ setContent(event.data)
35
+ }
36
+
37
+ return () => {
38
+ clearTimeout(timeout)
39
+ ws.close()
40
+ }
41
+ }, [ws])
42
+
43
+ return (
44
+ <>
45
+ <h1 className="text-white text-2xl">
46
+ Terminal{" "}
47
+ {ws.readyState === WebSocket.OPEN ? (
48
+ <span className="py-1 px-2 rounded-sm bg-emerald-300 text-emerald-800 text-xs uppercase">
49
+ Connected
50
+ </span>
51
+ ) : (
52
+ <span className="py-1 px-2 rounded-sm bg-rose-300 text-rose-800 text-xs uppercase">
53
+ Disconnected
54
+ </span>
55
+ )}
56
+ </h1>
57
+ <p>
58
+ Logs are also available at <a href="/log.txt">/log.txt</a>
59
+ </p>
60
+
61
+ <pre className="" dangerouslySetInnerHTML={{ __html: content }} />
62
+ </>
63
+ )
64
+ }
@@ -0,0 +1,9 @@
1
+ <html class="h-full">
2
+ <head>
3
+ <link rel="stylesheet" href="{{ STYLESHEET }}" />
4
+ </head>
5
+ <body class="bg-slate-950 text-white h-full">
6
+ <div id="root"></div>
7
+ {{ SCRIPTS }}
8
+ </body>
9
+ </html>
@@ -0,0 +1,7 @@
1
+ import * as ReactDOM from "react-dom/client"
2
+ import { Terminal } from "./Terminal"
3
+
4
+ const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
5
+ root.render(<Terminal />)
6
+
7
+ export default {}
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,13 @@
1
+ import { join } from "path"
2
+
3
+ // eslint-disable-next-line no-undef
4
+ const content = [join(__dirname, "./**/*.tsx"), join(__dirname, "./**/*.html")]
5
+
6
+ /** @type {import('tailwindcss').Config} */
7
+ export default {
8
+ content,
9
+ theme: {
10
+ extend: {},
11
+ },
12
+ plugins: [],
13
+ }
@@ -0,0 +1,55 @@
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>
11
+ function log(msg) {
12
+ document.getElementById("log").innerHTML = msg + "\n"
13
+ }
14
+
15
+ const getWS = () => {
16
+ const url = new URL(window.location.href)
17
+ url.pathname = url.pathname.replace("/terminal", "/ws")
18
+ url.protocol = "ws:"
19
+ return new WebSocket(
20
+ // "ws://192.168.86.11:8123/api/hassio_ingress/wmQTEkorulChwnbeWV1GvPSwBsEpGzYlyLR70rdHzH0/ws",
21
+ // "ws://localhost:8099/ws",
22
+ url,
23
+ )
24
+ }
25
+
26
+ const setupWs = (ws) => {
27
+ ws.onopen = function () {
28
+ console.log("CONNECT")
29
+ }
30
+ ws.onclose = function () {
31
+ console.log("DISCONNECT")
32
+ document.getElementById("log").innerHTML +=
33
+ "Disconnected. Retrying..." + "\n"
34
+ retry()
35
+ }
36
+ ws.onmessage = function (event) {
37
+ log(event.data)
38
+ }
39
+ }
40
+
41
+ var ws = getWS()
42
+ console.log("😅😅😅 ~ ws:", ws)
43
+ setupWs(ws)
44
+
45
+ const retry = () => {
46
+ console.log("Retrying in 1 second...")
47
+ setTimeout(() => {
48
+ if (ws.readyState === WebSocket.OPEN) {
49
+ return
50
+ }
51
+ ws = getWS()
52
+ setupWs(ws)
53
+ }, 1000)
54
+ }
55
+ </script>