@typed-assistant/builder 0.0.18 → 0.0.20

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 CHANGED
@@ -2,5 +2,6 @@
2
2
  module.exports = {
3
3
  root: true,
4
4
  extends: ["@typed-assistant/eslint-config/react-internal.js"],
5
+ plugins: ['html'],
5
6
  parserOptions: {},
6
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed-assistant/builder",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "exports": {
5
5
  "./appProcess": "./src/appProcess.tsx",
6
6
  "./bunInstall": "./src/bunInstall.tsx",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "ansi-to-html": "^0.7.2",
12
+ "@elysiajs/eden": "^0.8.1",
12
13
  "elysia": "^0.8.9",
13
14
  "@mdi/svg": "^7.3.67",
14
15
  "ignore": "^5.3.0",
@@ -18,12 +19,14 @@
18
19
  "devDependencies": {
19
20
  "@types/node": "^20.10.6",
20
21
  "@types/eslint": "^8.56.1",
22
+ "eslint-plugin-html": "^7.1.0",
21
23
  "eslint": "^8.56.0",
22
24
  "home-assistant-js-websocket": "^8.2.0",
25
+ "ts-toolbelt": "^9.6.0",
23
26
  "typescript": "^5.3.3",
24
27
  "@typed-assistant/eslint-config": "0.0.4",
25
- "@typed-assistant/logger": "0.0.5",
26
28
  "@typed-assistant/typescript-config": "0.0.4",
29
+ "@typed-assistant/logger": "0.0.6",
27
30
  "@typed-assistant/utils": "0.0.7"
28
31
  },
29
32
  "peerDependencies": {
@@ -46,16 +46,6 @@ async function killAndRestartApp(subprocesses: Processes) {
46
46
  return newSubprocesses
47
47
  }
48
48
 
49
- function setupWatcherInternal(...args: Parameters<typeof watch>) {
50
- const [directory, callback] = args
51
- if (typeof directory !== "string") throw new Error("Directory must be string")
52
-
53
- log("👀 Watching directory:", directory)
54
- const watcher = watch(directory, { recursive: true }, callback)
55
-
56
- return watcher
57
- }
58
-
59
49
  export async function setupWatcher(
60
50
  ...args: Parameters<typeof buildAndStartAppProcess>
61
51
  ) {
@@ -63,14 +53,17 @@ export async function setupWatcher(
63
53
  if (addonInfoError) {
64
54
  log(`🚨 Failed to get addon info: ${addonInfoError}`)
65
55
  }
66
- console.log("😅😅😅 ~ addonInfo:", Object.keys(addonInfo))
67
56
  await setupGitSync()
68
57
 
69
58
  let subprocesses = await buildAndStartAppProcess(...args)
70
59
 
71
- setupWatcherInternal(
72
- join(process.cwd(), "src"),
60
+ const directory = join(process.cwd(), "./src")
61
+ log("👀 Watching directory:", directory)
62
+ const watcher = watch(
63
+ directory,
64
+ { recursive: true },
73
65
  async function onFileChange(event, filename) {
66
+ console.log("😅😅😅 ~ filename:", filename)
74
67
  if (!filename) return
75
68
  if (shouldIgnoreFileOrFolder(filename)) return
76
69
  log(`⚠️ Change to ${filename} detected.`)
@@ -82,15 +75,16 @@ export async function setupWatcher(
82
75
  },
83
76
  )
84
77
 
85
- console.log(
86
- "😅😅😅 ~ addonInfo?.ingress_entry:",
87
- addonInfo?.data.ingress_entry,
88
- )
89
78
  startWebappServer({
90
79
  basePath: addonInfo?.data.ingress_entry ?? "",
91
80
  getSubprocesses: () => subprocesses,
92
81
  })
93
82
 
83
+ process.on("SIGINT", () => {
84
+ console.log("👋 Closing watcher...")
85
+ watcher.close()
86
+ })
87
+
94
88
  return subprocesses
95
89
  }
96
90
 
@@ -133,3 +127,10 @@ const getAddonInfo = async () => {
133
127
  data: { ingress_entry: string }
134
128
  }>("/addons/self/info")
135
129
  }
130
+
131
+ process.on("SIGINT", () => {
132
+ setTimeout(() => {
133
+ console.log("👋 ...")
134
+ process.exit(0)
135
+ }, 0)
136
+ })
@@ -2,23 +2,24 @@ import { log } from "@typed-assistant/logger"
2
2
  import Convert from "ansi-to-html"
3
3
  import type { Subprocess } from "bun"
4
4
  import { $ } from "bun"
5
- import type { Context } from "elysia"
6
- import { Elysia } from "elysia"
5
+ import { Elysia, t } from "elysia"
6
+ import { watch } from "fs"
7
7
  import { basename, join } from "path"
8
+ import type { List, String } from "ts-toolbelt"
8
9
 
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`)
10
+ const indexHtmlFilePath = `${import.meta.dir}/webserver/index.html` as const
11
+ const cssFile = `${import.meta.dir}/webserver/input.css` as const
12
+ const tsEntryPoint = `${import.meta.dir}/webserver/index.tsx` as const
13
+ const tailwindConfig =
14
+ `${import.meta.dir}/webserver/tailwind.config.js` as const
15
+ const cssOutputFile = join(
16
+ process.cwd(),
17
+ `./build/output.css`,
18
+ ) as `${string}/output.css`
15
19
 
16
20
  const convert = new Convert()
17
21
  const decoder = new TextDecoder()
18
22
 
19
- const getIngressPath = (req: Context["request"]) =>
20
- req.headers.get("x-ingress-path") ?? ""
21
-
22
23
  const readers = new Map<
23
24
  ReadableStream<Uint8Array>,
24
25
  ReadableStreamDefaultReader<Uint8Array>
@@ -37,6 +38,7 @@ const getReader = (stream: ReadableStream<Uint8Array>) => {
37
38
  }
38
39
 
39
40
  const subscribers = new Map<number, (message: string) => void>()
41
+ const logSubscribers = new Map<number, () => void>()
40
42
 
41
43
  let lastMessage = ""
42
44
 
@@ -49,7 +51,6 @@ export const startWebappServer = async ({
49
51
  app: Subprocess<"ignore", "pipe", "pipe">
50
52
  }
51
53
  }) => {
52
- console.log("😅😅😅 ~ basePath:", basePath)
53
54
  const buildResult = await Bun.build({
54
55
  entrypoints: [tsEntryPoint],
55
56
  outdir: "./build",
@@ -72,14 +73,14 @@ export const startWebappServer = async ({
72
73
  const indexHtml = (await Bun.file(indexHtmlFilePath).text())
73
74
  .replace(
74
75
  "{{ STYLESHEET }}",
75
- `${basePath}/assets/${basename(cssOutputFile)}`,
76
+ `${basePath}/assets/${getBaseName(cssOutputFile)}`,
76
77
  )
77
78
  .replace(
78
79
  "{{ SCRIPTS }}",
79
80
  buildResult.outputs
80
81
  .map(
81
82
  (output) =>
82
- `<script type="module" src="${basePath}/assets/${basename(output.path)}"></script>`,
83
+ `<script type="module" src="${basePath}/assets/${getBaseName(output.path)}"></script>`,
83
84
  )
84
85
  .join("\n"),
85
86
  )
@@ -92,9 +93,34 @@ export const startWebappServer = async ({
92
93
  headers: { "content-type": "text/html" },
93
94
  }),
94
95
  )
95
- .get("/terminal", Bun.file(terminalHtmlUrl))
96
- .get("/log.txt", Bun.file("./log.txt"))
96
+ .get(
97
+ "/log.txt",
98
+ async ({ query }) => {
99
+ return getLogsFromFile(query.limit)
100
+ },
101
+ {
102
+ query: t.Object({
103
+ limit: t.Optional(t.String()),
104
+ }),
105
+ },
106
+ )
107
+ .ws("/logsws", {
108
+ query: t.Object({
109
+ limit: t.Optional(t.String()),
110
+ }),
111
+ response: t.Object({ logs: t.Array(t.String()) }),
112
+ async open(ws) {
113
+ ws.send(await getLogsFromFile(ws.data.query.limit))
114
+ logSubscribers.set(ws.id, async () => {
115
+ ws.send(await getLogsFromFile(ws.data.query.limit))
116
+ })
117
+ },
118
+ close(ws) {
119
+ logSubscribers.delete(ws.id)
120
+ },
121
+ })
97
122
  .ws("/ws", {
123
+ response: t.String(),
98
124
  async open(ws) {
99
125
  ws.send("Connected successfully. Awaiting messages...")
100
126
  subscribers.set(ws.id, (message) => {
@@ -105,17 +131,16 @@ export const startWebappServer = async ({
105
131
  subscribers.delete(ws.id)
106
132
  },
107
133
  })
108
-
109
- server.get(
110
- `/assets/${basename(cssOutputFile)}`,
111
- () =>
112
- new Response(Bun.file(cssOutputFile), {
113
- headers: { "content-type": "text/css" },
114
- }),
115
- )
134
+ .get(
135
+ `/assets/${getBaseName(cssOutputFile)}`,
136
+ () =>
137
+ new Response(Bun.file(cssOutputFile), {
138
+ headers: { "content-type": "text/css" },
139
+ }),
140
+ )
116
141
  buildResult.outputs.forEach((output) => {
117
142
  server.get(
118
- `/assets/${basename(output.path)}`,
143
+ `/assets/${getBaseName(output.path)}`,
119
144
  () =>
120
145
  new Response(Bun.file(output.path), {
121
146
  headers: { "content-type": "text/javascript" },
@@ -126,6 +151,19 @@ export const startWebappServer = async ({
126
151
  server.listen(8099)
127
152
  log("🌐 Web server listening on port 8099")
128
153
 
154
+ const directory = join(process.cwd(), ".")
155
+ log("👀 Watching log.txt")
156
+ const watcher = watch(directory, function onFileChange(_event, filename) {
157
+ if (filename === "log.txt") {
158
+ logSubscribers.forEach((send) => send())
159
+ }
160
+ })
161
+
162
+ process.on("SIGINT", () => {
163
+ console.log("👋 Closing log watcher...")
164
+ watcher.close()
165
+ })
166
+
129
167
  // eslint-disable-next-line no-constant-condition
130
168
  while (true) {
131
169
  const stdoutReader = getReader(getSubprocesses().app.stdout)
@@ -147,4 +185,22 @@ export const startWebappServer = async ({
147
185
  }
148
186
  subscribers.forEach((send) => send(convertedMessage))
149
187
  }
188
+
189
+ return server
190
+ }
191
+
192
+ export type WebServer = Awaited<ReturnType<typeof startWebappServer>>
193
+
194
+ const getLogsFromFile = async (limit?: string) => {
195
+ try {
196
+ const lines = (await Bun.file("./log.txt").text()).split("\n")
197
+ const logFile = limit ? lines.slice(0, Number(limit)) : lines
198
+ return { logs: logFile }
199
+ } catch (e) {
200
+ return { logs: ["Error reading log.txt file"] }
201
+ }
202
+ }
203
+
204
+ const getBaseName = <const TString extends string>(path: TString) => {
205
+ return basename(path) as List.Last<String.Split<TString, "/">>
150
206
  }
@@ -0,0 +1,64 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+ import { Terminal } from "./Terminal"
3
+ import { app } from "./api"
4
+ import { useWS } from "./useWS"
5
+
6
+ const basePath = process.env.BASE_PATH ?? ""
7
+
8
+ const App = () => {
9
+ return (
10
+ <div className="grid grid-cols-3">
11
+ <div className="col-span-2">
12
+ <Terminal basePath={basePath} />
13
+ </div>
14
+ <div className="col-span-1">
15
+ <Logs />
16
+ </div>
17
+ </div>
18
+ )
19
+ }
20
+
21
+ const Logs = () => {
22
+ const [limit, setLimit] = useState(20)
23
+ const [logs, setLogs] = useState<string[]>([])
24
+
25
+ const ws = useWS({
26
+ subscribe: useCallback(
27
+ () => app.logsws.subscribe({ $query: { limit: limit.toString() } }),
28
+ [limit],
29
+ ),
30
+ onMessage: useCallback((event) => {
31
+ setLogs(JSON.parse(event.data).logs)
32
+ }, []),
33
+ })
34
+
35
+ return (
36
+ <div className="p-4 text-xs">
37
+ <div className="flex gap-4 mb-4 items-center justify-between">
38
+ <h2 className="mb-2 text-2xl">Logs</h2>
39
+ <div>
40
+ <div className="flex gap-2">
41
+ <label htmlFor="limit">Limit</label>
42
+ <input
43
+ className="border border-gray-300 rounded-md text-slate-800 px-2"
44
+ id="limit"
45
+ onChange={(e) => setLimit(Number(e.target.value))}
46
+ size={8}
47
+ value={limit}
48
+ />
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <pre className="overflow-x-auto">
54
+ <ul>
55
+ {logs.map((log) => (
56
+ <li key={log}>{log}</li>
57
+ ))}
58
+ </ul>
59
+ </pre>
60
+ </div>
61
+ )
62
+ }
63
+
64
+ export default App
@@ -1,43 +1,19 @@
1
- import { useEffect, useState } from "react"
1
+ import { useCallback, useState } from "react"
2
+ import { app } from "./api"
3
+ import { useWS } from "./useWS"
2
4
 
3
- const getWS = () => {
4
- const url = new URL(window.location.href)
5
- url.pathname = `${process.env.BASE_PATH}/ws`
6
- url.protocol = "ws:"
7
- const ws = new WebSocket(url)
8
-
9
- return ws
10
- }
11
-
12
- export function Terminal() {
5
+ export function Terminal({ basePath }: { basePath: string }) {
13
6
  const [content, setContent] = useState("")
14
- const [ws, setWS] = useState<WebSocket>(() => getWS())
15
-
16
- useEffect(() => {
17
- let timeout: NodeJS.Timeout
18
-
19
- ws.onclose = function () {
20
- timeout = setTimeout(() => {
21
- if (ws.readyState === WebSocket.OPEN) return
22
- setWS(getWS())
23
- }, 1000)
24
- }
25
-
26
- ws.onmessage = function (event) {
27
- setContent(event.data)
28
- }
29
-
30
- return () => {
31
- clearTimeout(timeout)
32
- ws.close()
33
- }
34
- }, [ws])
7
+ const ws = useWS({
8
+ subscribe: useCallback(() => app.ws.subscribe(), []),
9
+ onMessage: useCallback((event) => setContent(event.data), []),
10
+ })
35
11
 
36
12
  return (
37
13
  <>
38
14
  <h1 className="text-white text-2xl">
39
15
  Terminal{" "}
40
- {ws.readyState === WebSocket.OPEN ? (
16
+ {ws.ws.readyState === WebSocket.OPEN ? (
41
17
  <span className="py-1 px-2 rounded-sm bg-emerald-300 text-emerald-800 text-xs uppercase">
42
18
  Connected
43
19
  </span>
@@ -48,7 +24,7 @@ export function Terminal() {
48
24
  )}
49
25
  </h1>
50
26
  <p>
51
- Logs are also available at <a href="/log.txt">/log.txt</a>
27
+ <a href={`${basePath}/log.txt`}>View log.txt</a>
52
28
  </p>
53
29
 
54
30
  <pre className="" dangerouslySetInnerHTML={{ __html: content }} />
@@ -0,0 +1,7 @@
1
+ import { edenTreaty } from "@elysiajs/eden"
2
+ import type { WebServer } from "../setupWebserver"
3
+ import type { EdenTreaty } from "@elysiajs/eden/treaty"
4
+
5
+ export const app: EdenTreaty.Create<WebServer> = edenTreaty<WebServer>(
6
+ window.location.origin + process.env.BASE_PATH,
7
+ )
@@ -1,7 +1,7 @@
1
1
  import * as ReactDOM from "react-dom/client"
2
- import { Terminal } from "./Terminal"
2
+ import App from "./App"
3
3
 
4
4
  const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
5
- root.render(<Terminal />)
5
+ root.render(<App />)
6
6
 
7
7
  export default {}
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState } from "react"
2
+ import { app } from "./api"
3
+
4
+ export function useWS({
5
+ onMessage,
6
+ subscribe,
7
+ }: {
8
+ onMessage: (event: MessageEvent) => void
9
+ subscribe: () => ReturnType<(typeof app.ws | typeof app.logsws)["subscribe"]>
10
+ }) {
11
+ const [ws, setWS] = useState(subscribe)
12
+
13
+ useEffect(() => {
14
+ let timeout: NodeJS.Timeout
15
+
16
+ ws.ws.onclose = function () {
17
+ timeout = setTimeout(() => {
18
+ if (ws.ws.readyState === WebSocket.OPEN) return
19
+ setWS(subscribe)
20
+ }, 1000)
21
+ }
22
+
23
+ ws.ws.onmessage = function (event) {
24
+ onMessage(event)
25
+ }
26
+
27
+ return () => {
28
+ clearTimeout(timeout)
29
+ ws.ws.close()
30
+ }
31
+ }, [ws, onMessage, subscribe])
32
+
33
+ return ws
34
+ }
package/tsconfig.json CHANGED
@@ -1,5 +1,10 @@
1
1
  {
2
2
  "extends": "@typed-assistant/typescript-config/react-app.json",
3
+ "compilerOptions": {
4
+ "paths": {
5
+ "@elysiajs/eden": ["./node_modules/@elysiajs/eden/"],
6
+ },
7
+ },
3
8
  "include": ["src"],
4
- "exclude": ["node_modules", "dist"]
9
+ "exclude": ["node_modules", "dist"],
5
10
  }
@@ -1,55 +0,0 @@
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>