@typed-assistant/builder 0.0.20 → 0.0.22

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.20",
3
+ "version": "0.0.22",
4
4
  "exports": {
5
5
  "./appProcess": "./src/appProcess.tsx",
6
6
  "./bunInstall": "./src/bunInstall.tsx",
@@ -26,8 +26,8 @@
26
26
  "typescript": "^5.3.3",
27
27
  "@typed-assistant/eslint-config": "0.0.4",
28
28
  "@typed-assistant/typescript-config": "0.0.4",
29
- "@typed-assistant/logger": "0.0.6",
30
- "@typed-assistant/utils": "0.0.7"
29
+ "@typed-assistant/utils": "0.0.7",
30
+ "@typed-assistant/logger": "0.0.8"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "home-assistant-js-websocket": "^8.2.0"
@@ -9,18 +9,19 @@ import { getHassAPI, getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
9
9
  import { pullChanges } from "./pullChanges"
10
10
  import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
11
11
  import { startWebappServer } from "./setupWebserver"
12
+ import { $ } from "bun"
12
13
 
13
14
  type Processes = Awaited<ReturnType<typeof buildAndStartAppProcess>>
14
15
 
15
16
  async function buildAndStartAppProcess(
16
- appSourceFile?: string,
17
- options?: Parameters<typeof generateTypes>[0],
17
+ appSourceFile: string,
18
+ options: Parameters<typeof generateTypes>[0],
18
19
  ) {
19
20
  await generateTypes({ mdiPaths: options?.mdiPaths })
20
21
  return { app: await startApp(appSourceFile) }
21
22
  }
22
23
 
23
- async function startApp(appSourceFile: string = "src/entry.tsx") {
24
+ async function startApp(appSourceFile: string) {
24
25
  log("🚀 Starting app...")
25
26
  const path = join(process.cwd(), appSourceFile)
26
27
  return Bun.spawn(["bun", path], {
@@ -36,26 +37,74 @@ async function kill(process: Subprocess) {
36
37
  }
37
38
 
38
39
  let settingUp = { current: false }
39
- async function killAndRestartApp(subprocesses: Processes) {
40
+ async function killAndRestartApp(
41
+ entryFile: string,
42
+ options: Parameters<typeof buildAndStartAppProcess>[1],
43
+ subprocesses: Processes,
44
+ ) {
40
45
  if (settingUp.current) return subprocesses
41
46
  log("♻️ Restarting app...")
42
47
  settingUp.current = true
43
48
  if (subprocesses.app) await kill(subprocesses.app)
44
- const newSubprocesses = await buildAndStartAppProcess()
49
+ const newSubprocesses = await buildAndStartAppProcess(entryFile, options)
45
50
  settingUp.current = false
46
51
  return newSubprocesses
47
52
  }
48
53
 
49
- export async function setupWatcher(
50
- ...args: Parameters<typeof buildAndStartAppProcess>
51
- ) {
54
+ let multipleProcessesErrorCount = 0
55
+ let noProcessesErrorCount = 0
56
+ const checkProcesses = async (
57
+ entryFile: string,
58
+ { onProcessError }: { onProcessError?: (message: string) => void },
59
+ ) => {
60
+ const ps = await $`ps -f`.text()
61
+ const matches = ps.match(new RegExp(`bun .+${entryFile}`, "gmi")) ?? []
62
+
63
+ if (matches.length > 1) {
64
+ multipleProcessesErrorCount++
65
+ if (multipleProcessesErrorCount > 3) {
66
+ const message = `🚨 Multiple processes detected. Restarting TypedAssistant addon...`
67
+ log(message)
68
+ onProcessError?.(message)
69
+ restartAddon()
70
+ }
71
+ } else {
72
+ multipleProcessesErrorCount = 0
73
+ }
74
+
75
+ if (matches.length === 0) {
76
+ noProcessesErrorCount++
77
+ if (noProcessesErrorCount > 3) {
78
+ const message = `🚨 No processes detected. Restarting TypedAssistant addon...`
79
+ log(message)
80
+ onProcessError?.(message)
81
+ restartAddon()
82
+ }
83
+ } else {
84
+ noProcessesErrorCount = 0
85
+ }
86
+
87
+ setTimeout(() => checkProcesses(entryFile, { onProcessError }), 5000)
88
+ }
89
+
90
+ export async function setupWatcher({
91
+ entryFile,
92
+ mdiPaths,
93
+ onProcessError,
94
+ }: {
95
+ entryFile: string
96
+ } & Parameters<typeof generateTypes>[0] &
97
+ Parameters<typeof checkProcesses>[1]) {
52
98
  const { data: addonInfo, error: addonInfoError } = await getAddonInfo()
53
99
  if (addonInfoError) {
54
100
  log(`🚨 Failed to get addon info: ${addonInfoError}`)
55
101
  }
56
102
  await setupGitSync()
103
+ checkProcesses(entryFile, { onProcessError })
57
104
 
58
- let subprocesses = await buildAndStartAppProcess(...args)
105
+ let subprocesses = await buildAndStartAppProcess(entryFile, {
106
+ mdiPaths: mdiPaths,
107
+ })
59
108
 
60
109
  const directory = join(process.cwd(), "./src")
61
110
  log("👀 Watching directory:", directory)
@@ -63,14 +112,17 @@ export async function setupWatcher(
63
112
  directory,
64
113
  { recursive: true },
65
114
  async function onFileChange(event, filename) {
66
- console.log("😅😅😅 ~ filename:", filename)
67
115
  if (!filename) return
68
116
  if (shouldIgnoreFileOrFolder(filename)) return
69
117
  log(`⚠️ Change to ${filename} detected.`)
70
118
  if (filename.endsWith("process.tsx")) {
71
119
  await restartAddon()
72
120
  } else {
73
- subprocesses = await killAndRestartApp(subprocesses)
121
+ subprocesses = await killAndRestartApp(
122
+ entryFile,
123
+ { mdiPaths },
124
+ subprocesses,
125
+ )
74
126
  }
75
127
  },
76
128
  )
@@ -9,11 +9,11 @@ export const pullChanges = async () => {
9
9
  !process.env.GITHUB_REPO
10
10
  ) {
11
11
  log(
12
- "⚠️ Cannot pull changes without GITHUB_TOKEN, GITHUB_USERNAME, and GITHUB_REPO environment variables.",
12
+ "⚠️ Cannot pull changes without GITHUB_TOKEN, GITHUB_USERNAME, and GITHUB_REPO environment variables.",
13
13
  )
14
14
  return { error: {} }
15
15
  }
16
- log("⬇️ Pulling changes...")
16
+ log("⬇️ Pulling changes...")
17
17
  const gitPullText = await getSpawnText(["git", "pull"])
18
18
  const packageJSONUpdated = /package.json/.test(gitPullText)
19
19
  const nothingNew = /Already up to date./.test(gitPullText)
@@ -162,6 +162,7 @@ export const startWebappServer = async ({
162
162
  process.on("SIGINT", () => {
163
163
  console.log("👋 Closing log watcher...")
164
164
  watcher.close()
165
+ server.stop()
165
166
  })
166
167
 
167
168
  // eslint-disable-next-line no-constant-condition
@@ -1,15 +1,15 @@
1
- import { useCallback, useEffect, useState } from "react"
1
+ import { useCallback, useState } from "react"
2
+ import { AppSection } from "./AppSection"
2
3
  import { Terminal } from "./Terminal"
4
+ import { WSIndicator } from "./WSIndicator"
3
5
  import { app } from "./api"
4
6
  import { useWS } from "./useWS"
5
7
 
6
- const basePath = process.env.BASE_PATH ?? ""
7
-
8
8
  const App = () => {
9
9
  return (
10
- <div className="grid grid-cols-3">
10
+ <div className="grid md:grid-cols-3">
11
11
  <div className="col-span-2">
12
- <Terminal basePath={basePath} />
12
+ <Terminal />
13
13
  </div>
14
14
  <div className="col-span-1">
15
15
  <Logs />
@@ -19,8 +19,13 @@ const App = () => {
19
19
  }
20
20
 
21
21
  const Logs = () => {
22
- const [limit, setLimit] = useState(20)
23
- const [logs, setLogs] = useState<string[]>([])
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
+ >([])
24
29
 
25
30
  const ws = useWS({
26
31
  subscribe: useCallback(
@@ -28,36 +33,72 @@ const Logs = () => {
28
33
  [limit],
29
34
  ),
30
35
  onMessage: useCallback((event) => {
31
- setLogs(JSON.parse(event.data).logs)
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
+ )
32
43
  }, []),
33
44
  })
34
45
 
35
46
  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
- />
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>
49
81
  </div>
50
- </div>
51
- </div>
52
-
53
- <pre className="overflow-x-auto">
82
+ </>
83
+ )}
84
+ >
85
+ <pre>
54
86
  <ul>
55
87
  {logs.map((log) => (
56
- <li key={log}>{log}</li>
88
+ <li key={log.date + 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>
57
98
  ))}
58
99
  </ul>
59
100
  </pre>
60
- </div>
101
+ </AppSection>
61
102
  )
62
103
  }
63
104
 
@@ -0,0 +1,16 @@
1
+ export const AppSection = ({
2
+ renderHeader,
3
+ children,
4
+ }: {
5
+ renderHeader: () => JSX.Element
6
+ children: JSX.Element
7
+ }) => {
8
+ return (
9
+ <div className="p-4 text-xs h-full max-h-dvh w-dvw md:w-auto overflow-x-auto flex flex-col">
10
+ <div className="flex flex-wrap gap-4 mb-4 items-center justify-between">
11
+ {renderHeader()}
12
+ </div>
13
+ <div className="overflow-x-auto">{children}</div>
14
+ </div>
15
+ )
16
+ }
@@ -1,8 +1,10 @@
1
1
  import { useCallback, useState } from "react"
2
2
  import { app } from "./api"
3
3
  import { useWS } from "./useWS"
4
+ import { WSIndicator } from "./WSIndicator"
5
+ import { AppSection } from "./AppSection"
4
6
 
5
- export function Terminal({ basePath }: { basePath: string }) {
7
+ export function Terminal() {
6
8
  const [content, setContent] = useState("")
7
9
  const ws = useWS({
8
10
  subscribe: useCallback(() => app.ws.subscribe(), []),
@@ -10,24 +12,14 @@ export function Terminal({ basePath }: { basePath: string }) {
10
12
  })
11
13
 
12
14
  return (
13
- <>
14
- <h1 className="text-white text-2xl">
15
- Terminal{" "}
16
- {ws.ws.readyState === WebSocket.OPEN ? (
17
- <span className="py-1 px-2 rounded-sm bg-emerald-300 text-emerald-800 text-xs uppercase">
18
- Connected
19
- </span>
20
- ) : (
21
- <span className="py-1 px-2 rounded-sm bg-rose-300 text-rose-800 text-xs uppercase">
22
- Disconnected
23
- </span>
24
- )}
25
- </h1>
26
- <p>
27
- <a href={`${basePath}/log.txt`}>View log.txt</a>
28
- </p>
29
-
30
- <pre className="" dangerouslySetInnerHTML={{ __html: content }} />
31
- </>
15
+ <AppSection
16
+ renderHeader={() => (
17
+ <h1 className="mb-2 text-2xl flex items-baseline gap-3">
18
+ TypedAssistant <WSIndicator ws={ws.ws} />
19
+ </h1>
20
+ )}
21
+ >
22
+ <pre dangerouslySetInnerHTML={{ __html: content }} />
23
+ </AppSection>
32
24
  )
33
25
  }
@@ -0,0 +1,17 @@
1
+ export function WSIndicator({ ws }: { ws: WebSocket }) {
2
+ return ws.readyState === WebSocket.OPEN ? (
3
+ <div
4
+ title="Connected"
5
+ className="w-4 h-4 rounded-full bg-emerald-300 text-emerald-800 text-xs uppercase"
6
+ >
7
+ <span className="sr-only">Connected</span>
8
+ </div>
9
+ ) : (
10
+ <div
11
+ title="Disconnected"
12
+ className="w-4 h-4 rounded-full bg-rose-300 text-rose-800 text-xs uppercase"
13
+ >
14
+ <span className="sr-only">Disconnected</span>
15
+ </div>
16
+ )
17
+ }
@@ -2,8 +2,8 @@
2
2
  <head>
3
3
  <link rel="stylesheet" href="{{ STYLESHEET }}" />
4
4
  </head>
5
- <body class="bg-slate-950 text-white h-full">
6
- <div id="root"></div>
5
+ <body class="bg-slate-950 text-white h-full max-h-dvh">
6
+ <div id="root" class="h-full max-h-dvh"></div>
7
7
  {{ SCRIPTS }}
8
8
  </body>
9
9
  </html>