@typed-assistant/builder 0.0.51 → 0.0.53

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.51",
3
+ "version": "0.0.53",
4
4
  "exports": {
5
5
  "./appProcess": "./src/appProcess.tsx",
6
6
  "./bunInstall": "./src/bunInstall.tsx",
@@ -22,12 +22,13 @@
22
22
  "eslint-plugin-html": "^7.1.0",
23
23
  "eslint": "^8.56.0",
24
24
  "home-assistant-js-websocket": "^8.2.0",
25
+ "tailwind-merge": "^2.2.1",
25
26
  "ts-toolbelt": "^9.6.0",
26
27
  "typescript": "^5.3.3",
27
- "@typed-assistant/eslint-config": "0.0.8",
28
- "@typed-assistant/logger": "0.0.16",
29
- "@typed-assistant/typescript-config": "0.0.8",
30
- "@typed-assistant/utils": "0.0.14"
28
+ "@typed-assistant/eslint-config": "0.0.9",
29
+ "@typed-assistant/logger": "0.0.17",
30
+ "@typed-assistant/typescript-config": "0.0.9",
31
+ "@typed-assistant/utils": "0.0.15"
31
32
  },
32
33
  "peerDependencies": {
33
34
  "home-assistant-js-websocket": "^8.2.0"
@@ -24,8 +24,8 @@ export async function setup({
24
24
  onProcessError,
25
25
  }: {
26
26
  entryFile: string
27
- } & Parameters<typeof generateTypes>[0] &
28
- Pick<Parameters<typeof checkProcesses>[1], "onProcessError">) {
27
+ onProcessError: (message: string, addonUrl: string) => void
28
+ } & Parameters<typeof generateTypes>[0]) {
29
29
  const addonInfo = await getAddonInfo()
30
30
  const basePath = addonInfo?.data.ingress_entry ?? ""
31
31
  const slug = addonInfo?.data.slug ?? ""
@@ -40,6 +40,13 @@ export async function setup({
40
40
  startWebappServer({
41
41
  basePath,
42
42
  getSubprocesses: () => subprocesses,
43
+ onRestartAppRequest: async () => {
44
+ subprocesses = await killAndRestartApp(
45
+ entryFile,
46
+ { mdiPaths },
47
+ subprocesses,
48
+ )
49
+ },
43
50
  })
44
51
  setupWatcher({
45
52
  directoryToWatch,
@@ -51,7 +58,24 @@ export async function setup({
51
58
  getSubprocesses: () => subprocesses,
52
59
  })
53
60
 
54
- checkProcesses(entryFile, { addonUrl, onProcessError })
61
+ checkProcesses(entryFile, {
62
+ onMultiProcessError: (ps) => {
63
+ const message = `Multiple processes detected. Restarting addon...`
64
+ logger.fatal({ additionalDetails: ps, emoji: "🚨" }, message)
65
+ onProcessError?.(message, addonUrl)
66
+ restartAddon()
67
+ },
68
+ onNoProcessError: async (ps) => {
69
+ const message = `No processes detected. Restarting app...`
70
+ logger.fatal({ additionalDetails: ps, emoji: "🚨" }, message)
71
+ onProcessError?.(message, addonUrl)
72
+ subprocesses = await killAndRestartApp(
73
+ entryFile,
74
+ { mdiPaths },
75
+ subprocesses,
76
+ )
77
+ },
78
+ })
55
79
  await setupGitSync(webhookUrl)
56
80
 
57
81
  return subprocesses
@@ -96,11 +120,11 @@ let noProcessesErrorCount = 0
96
120
  const checkProcesses = async (
97
121
  entryFile: string,
98
122
  {
99
- addonUrl,
100
- onProcessError,
123
+ onMultiProcessError,
124
+ onNoProcessError,
101
125
  }: {
102
- addonUrl: string
103
- onProcessError?: (message: string, addonUrl: string) => void
126
+ onMultiProcessError?: (psOutput: string) => void | Promise<void>
127
+ onNoProcessError?: (psOutput: string) => void | Promise<void>
104
128
  },
105
129
  ) => {
106
130
  const ps = await $`ps -f`.text()
@@ -109,9 +133,7 @@ const checkProcesses = async (
109
133
  if (matches.length > 1) {
110
134
  multipleProcessesErrorCount++
111
135
  if (multipleProcessesErrorCount > 5) {
112
- const message = `Multiple processes detected. Check the logs...`
113
- logger.fatal({ additionalDetails: ps, emoji: "🚨" }, message)
114
- onProcessError?.(message, addonUrl)
136
+ await onMultiProcessError?.(ps)
115
137
  return
116
138
  }
117
139
  } else {
@@ -121,9 +143,7 @@ const checkProcesses = async (
121
143
  if (matches.length === 0) {
122
144
  noProcessesErrorCount++
123
145
  if (noProcessesErrorCount > 5) {
124
- const message = `No processes detected. Check the logs...`
125
- logger.fatal({ additionalDetails: ps, emoji: "🚨" }, message)
126
- onProcessError?.(message, addonUrl)
146
+ await onNoProcessError?.(ps)
127
147
  return
128
148
  }
129
149
  } else {
@@ -131,7 +151,7 @@ const checkProcesses = async (
131
151
  }
132
152
 
133
153
  setTimeout(
134
- () => checkProcesses(entryFile, { addonUrl, onProcessError }),
154
+ () => checkProcesses(entryFile, { onMultiProcessError, onNoProcessError }),
135
155
  5000,
136
156
  )
137
157
  }
@@ -57,11 +57,13 @@ let lastMessage = ""
57
57
  export const startWebappServer = async ({
58
58
  basePath,
59
59
  getSubprocesses,
60
+ onRestartAppRequest,
60
61
  }: {
61
62
  basePath: string
62
63
  getSubprocesses: () => {
63
64
  app: Subprocess<"ignore", "pipe", "pipe">
64
65
  }
66
+ onRestartAppRequest: () => void
65
67
  }) => {
66
68
  const buildResult = await Bun.build({
67
69
  entrypoints: [tsEntryPoint],
@@ -105,6 +107,10 @@ export const startWebappServer = async ({
105
107
  headers: { "content-type": "text/html" },
106
108
  }),
107
109
  )
110
+ .get("/restart-app", async () => {
111
+ onRestartAppRequest()
112
+ return { message: "Restarting app..." }
113
+ })
108
114
  .get("/restart-addon", async () => {
109
115
  await killSubprocess(getSubprocesses().app)
110
116
  restartAddon()
@@ -7,10 +7,10 @@ const App = () => {
7
7
  return (
8
8
  <div className="grid md:grid-cols-3">
9
9
  <div className="col-span-2">
10
- <Terminal basePath={basePath} />
10
+ <Terminal />
11
11
  </div>
12
12
  <div className="col-span-1">
13
- <Logs />
13
+ <Logs basePath={basePath} />
14
14
  </div>
15
15
  </div>
16
16
  )
@@ -7,8 +7,9 @@ import { useWS } from "./useWS"
7
7
  import { getPrettyTimestamp } from "@typed-assistant/utils/getPrettyTimestamp"
8
8
  import { levels } from "@typed-assistant/logger/levels"
9
9
  import type { LogSchema } from "@typed-assistant/logger"
10
+ import { buttonStyle } from "./styles"
10
11
 
11
- export const Logs = () => {
12
+ export const Logs = ({ basePath }: { basePath: string }) => {
12
13
  const [limit, setLimit] = useState(200)
13
14
  const [level, setLevel] = useState<
14
15
  "trace" | "debug" | "info" | "warn" | "error" | "fatal"
@@ -39,6 +40,11 @@ export const Logs = () => {
39
40
  <h2 className="mb-2 text-2xl flex items-baseline gap-3">
40
41
  Logs <WSIndicator ws={ws.ws} />
41
42
  </h2>
43
+
44
+ <a className={buttonStyle} href={`${basePath}/log.txt?limit=500`}>
45
+ View raw log.txt
46
+ </a>
47
+
42
48
  <div className="flex flex-wrap gap-2">
43
49
  <div className="flex gap-2">
44
50
  <label htmlFor="dateTimeVisibility">Date/Time</label>
@@ -1,10 +1,75 @@
1
- import { useCallback, useState } from "react"
1
+ import { useCallback, useEffect, useState } from "react"
2
2
  import { app } from "./api"
3
3
  import { useWS } from "./useWS"
4
4
  import { WSIndicator } from "./WSIndicator"
5
5
  import { AppSection } from "./AppSection"
6
+ import { buttonStyle } from "./styles"
7
+ import { twMerge } from "tailwind-merge"
6
8
 
7
- export function Terminal({ basePath }: { basePath: string }) {
9
+ type ButtonAsyncProps = {
10
+ onClick: () => Promise<{ error?: unknown }>
11
+ stateLabels: {
12
+ idle: string
13
+ loading?: string
14
+ error?: string
15
+ success?: string
16
+ }
17
+ }
18
+
19
+ const ButtonAsync = ({
20
+ onClick,
21
+ stateLabels: stateLabelsProp,
22
+ }: ButtonAsyncProps) => {
23
+ const stateLabels = {
24
+ error: "Error",
25
+ loading: "Loading...",
26
+ success: "Success",
27
+ ...stateLabelsProp,
28
+ }
29
+ const [state, setState] = useState<"idle" | "loading" | "error" | "success">(
30
+ "idle",
31
+ )
32
+
33
+ useEffect(() => {
34
+ if (state !== "loading" && state !== "idle") {
35
+ const timeout = setTimeout(() => setState("idle"), 2000)
36
+
37
+ return () => {
38
+ clearTimeout(timeout)
39
+ }
40
+ }
41
+ }, [state])
42
+
43
+ const handleClick = async () => {
44
+ if (state !== "idle") return
45
+ setState("loading")
46
+
47
+ const { error } = await onClick()
48
+
49
+ if (error) {
50
+ setState("error")
51
+ return
52
+ }
53
+
54
+ setState("success")
55
+ }
56
+
57
+ return (
58
+ <button
59
+ className={twMerge(
60
+ buttonStyle,
61
+ state === "loading" && "bg-blue-300 text-blue-800",
62
+ state === "error" && "bg-red-300 text-red-800",
63
+ state === "success" && "bg-green-300 text-green-800",
64
+ )}
65
+ onClick={handleClick}
66
+ >
67
+ {stateLabels[state]}
68
+ </button>
69
+ )
70
+ }
71
+
72
+ export function Terminal() {
8
73
  const [content, setContent] = useState("")
9
74
  const ws = useWS({
10
75
  subscribe: useCallback(() => app.ws.subscribe(), []),
@@ -19,12 +84,22 @@ export function Terminal({ basePath }: { basePath: string }) {
19
84
  TypedAssistant <WSIndicator ws={ws.ws} />
20
85
  </h1>
21
86
  <div className="flex flex-wrap gap-2">
22
- <a className={buttonStyle} href={`${basePath}/restart-addon`}>
23
- Restart addon
24
- </a>
25
- <a className={buttonStyle} href={`${basePath}/log.txt?limit=500`}>
26
- View log.txt
27
- </a>
87
+ <ButtonAsync
88
+ onClick={() => app["restart-app"].get()}
89
+ stateLabels={{
90
+ idle: "Restart app",
91
+ loading: "Restarting app...",
92
+ success: "App restarted",
93
+ }}
94
+ />
95
+ <ButtonAsync
96
+ onClick={() => app["restart-addon"].get()}
97
+ stateLabels={{
98
+ idle: "Restart addon",
99
+ loading: "Restarting addon...",
100
+ success: "Addon restarted",
101
+ }}
102
+ />
28
103
  </div>
29
104
  </>
30
105
  )}
@@ -34,4 +109,52 @@ export function Terminal({ basePath }: { basePath: string }) {
34
109
  )
35
110
  }
36
111
 
37
- const buttonStyle = "bg-slate-800 text-white px-3 py-1 rounded-md"
112
+ const RestartAppButton = () => {
113
+ const [state, setState] = useState<"idle" | "loading" | "error" | "success">(
114
+ "idle",
115
+ )
116
+
117
+ useEffect(() => {
118
+ if (state !== "loading" && state !== "idle") {
119
+ const timeout = setTimeout(() => setState("idle"), 2000)
120
+
121
+ return () => {
122
+ clearTimeout(timeout)
123
+ }
124
+ }
125
+ }, [state])
126
+
127
+ const onClick = async () => {
128
+ if (state !== "idle") return
129
+ setState("loading")
130
+
131
+ const { data, error } = await app["restart-app"].get()
132
+
133
+ if (error) {
134
+ setState("error")
135
+ return
136
+ }
137
+
138
+ setState("success")
139
+ }
140
+
141
+ return (
142
+ <button
143
+ className={twMerge(
144
+ buttonStyle,
145
+ state === "loading" && "bg-blue-500",
146
+ state === "error" && "bg-red-500",
147
+ state === "success" && "bg-green-500",
148
+ )}
149
+ onClick={onClick}
150
+ >
151
+ {state === "loading"
152
+ ? "Restarting app..."
153
+ : state === "error"
154
+ ? "Error restarting app"
155
+ : state === "success"
156
+ ? "App restarted"
157
+ : "Restart app"}
158
+ </button>
159
+ )
160
+ }
@@ -0,0 +1 @@
1
+ export const buttonStyle = "bg-slate-800 text-white px-3 py-1 rounded-md"