@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 +3 -3
- package/src/appProcess.tsx +63 -11
- package/src/pullChanges.tsx +2 -2
- package/src/setupWebserver.tsx +1 -0
- package/src/webserver/App.tsx +68 -27
- package/src/webserver/AppSection.tsx +16 -0
- package/src/webserver/Terminal.tsx +12 -20
- package/src/webserver/WSIndicator.tsx +17 -0
- package/src/webserver/index.html +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-assistant/builder",
|
|
3
|
-
"version": "0.0.
|
|
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/
|
|
30
|
-
"@typed-assistant/
|
|
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"
|
package/src/appProcess.tsx
CHANGED
|
@@ -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
|
|
17
|
-
options
|
|
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
|
|
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(
|
|
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
|
-
|
|
50
|
-
|
|
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(
|
|
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(
|
|
121
|
+
subprocesses = await killAndRestartApp(
|
|
122
|
+
entryFile,
|
|
123
|
+
{ mdiPaths },
|
|
124
|
+
subprocesses,
|
|
125
|
+
)
|
|
74
126
|
}
|
|
75
127
|
},
|
|
76
128
|
)
|
package/src/pullChanges.tsx
CHANGED
|
@@ -9,11 +9,11 @@ export const pullChanges = async () => {
|
|
|
9
9
|
!process.env.GITHUB_REPO
|
|
10
10
|
) {
|
|
11
11
|
log(
|
|
12
|
-
"⚠️
|
|
12
|
+
"⚠️ Cannot pull changes without GITHUB_TOKEN, GITHUB_USERNAME, and GITHUB_REPO environment variables.",
|
|
13
13
|
)
|
|
14
14
|
return { error: {} }
|
|
15
15
|
}
|
|
16
|
-
log("⬇️
|
|
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)
|
package/src/setupWebserver.tsx
CHANGED
package/src/webserver/App.tsx
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { useCallback,
|
|
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
|
|
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(
|
|
23
|
-
const [
|
|
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(
|
|
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
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<pre
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<pre>
|
|
54
86
|
<ul>
|
|
55
87
|
{logs.map((log) => (
|
|
56
|
-
<li key={log
|
|
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
|
-
</
|
|
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(
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
}
|
package/src/webserver/index.html
CHANGED
|
@@ -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>
|