@muhammedaksam/easiarr 0.6.2 → 0.7.1
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 +1 -1
- package/src/api/portainer-api.ts +111 -0
- package/src/apps/registry.ts +57 -5
- package/src/compose/generator.ts +2 -1
- package/src/config/homepage-config.ts +247 -0
- package/src/config/manager.ts +31 -1
- package/src/config/schema.ts +13 -0
- package/src/docker/client.ts +135 -0
- package/src/ui/App.ts +15 -1
- package/src/ui/components/UpdateNotification.ts +101 -0
- package/src/ui/screens/ContainerControl.ts +509 -75
- package/src/ui/screens/HomepageSetup.ts +306 -0
- package/src/ui/screens/MainMenu.ts +17 -1
- package/src/ui/screens/QuickSetup.ts +2 -0
- package/src/utils/env.ts +23 -0
- package/src/utils/update-checker.ts +210 -0
package/src/docker/client.ts
CHANGED
|
@@ -127,3 +127,138 @@ export async function isDockerAvailable(): Promise<boolean> {
|
|
|
127
127
|
return false
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
// ==========================================
|
|
132
|
+
// Individual Container Operations
|
|
133
|
+
// ==========================================
|
|
134
|
+
|
|
135
|
+
export interface ContainerDetails {
|
|
136
|
+
id: string
|
|
137
|
+
name: string
|
|
138
|
+
service: string
|
|
139
|
+
status: "running" | "stopped" | "exited" | "paused"
|
|
140
|
+
state: string
|
|
141
|
+
health?: string
|
|
142
|
+
ports: string
|
|
143
|
+
uptime?: string
|
|
144
|
+
image: string
|
|
145
|
+
createdAt: string
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Start a specific container by service name
|
|
150
|
+
*/
|
|
151
|
+
export async function startContainer(service: string): Promise<{ success: boolean; output: string }> {
|
|
152
|
+
try {
|
|
153
|
+
const composePath = getComposePath()
|
|
154
|
+
const result = await $`docker compose -f ${composePath} start ${service}`.text()
|
|
155
|
+
return { success: true, output: result }
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return { success: false, output: String(error) }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stop a specific container by service name
|
|
163
|
+
*/
|
|
164
|
+
export async function stopContainer(service: string): Promise<{ success: boolean; output: string }> {
|
|
165
|
+
try {
|
|
166
|
+
const composePath = getComposePath()
|
|
167
|
+
const result = await $`docker compose -f ${composePath} stop ${service}`.text()
|
|
168
|
+
return { success: true, output: result }
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { success: false, output: String(error) }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Restart a specific container by service name
|
|
176
|
+
*/
|
|
177
|
+
export async function restartContainer(service: string): Promise<{ success: boolean; output: string }> {
|
|
178
|
+
try {
|
|
179
|
+
const composePath = getComposePath()
|
|
180
|
+
const result = await $`docker compose -f ${composePath} restart ${service}`.text()
|
|
181
|
+
return { success: true, output: result }
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return { success: false, output: String(error) }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get detailed info for a specific container
|
|
189
|
+
*/
|
|
190
|
+
export async function getContainerDetails(containerName: string): Promise<ContainerDetails | null> {
|
|
191
|
+
try {
|
|
192
|
+
const result =
|
|
193
|
+
await $`docker inspect ${containerName} --format '{"id":"{{.Id}}","name":"{{.Name}}","state":"{{.State.Status}}","health":"{{if .State.Health}}{{.State.Health.Status}}{{end}}","image":"{{.Config.Image}}","createdAt":"{{.Created}}"}'`.text()
|
|
194
|
+
|
|
195
|
+
if (!result.trim()) return null
|
|
196
|
+
|
|
197
|
+
const data = JSON.parse(result.trim())
|
|
198
|
+
|
|
199
|
+
// Get uptime from Status
|
|
200
|
+
const statusResult = await $`docker ps --filter "name=${containerName}" --format "{{.Status}}"`.text()
|
|
201
|
+
|
|
202
|
+
// Get port mappings
|
|
203
|
+
const portsResult = await $`docker port ${containerName} 2>/dev/null`.text()
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
id: data.id.substring(0, 12),
|
|
207
|
+
name: data.name.replace(/^\//, ""),
|
|
208
|
+
service: containerName,
|
|
209
|
+
status: data.state === "running" ? "running" : data.state === "exited" ? "exited" : "stopped",
|
|
210
|
+
state: data.state,
|
|
211
|
+
health: data.health || undefined,
|
|
212
|
+
ports: portsResult.trim() || "-",
|
|
213
|
+
uptime: statusResult.trim() || undefined,
|
|
214
|
+
image: data.image,
|
|
215
|
+
createdAt: data.createdAt,
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get container logs
|
|
224
|
+
*/
|
|
225
|
+
export async function getContainerLogs(
|
|
226
|
+
service: string,
|
|
227
|
+
lines: number = 50
|
|
228
|
+
): Promise<{ success: boolean; output: string }> {
|
|
229
|
+
try {
|
|
230
|
+
const composePath = getComposePath()
|
|
231
|
+
const result = await $`docker compose -f ${composePath} logs ${service} --tail ${lines} --no-color`.text()
|
|
232
|
+
return { success: true, output: result }
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return { success: false, output: String(error) }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Pull latest image for a specific service
|
|
240
|
+
*/
|
|
241
|
+
export async function pullServiceImage(service: string): Promise<{ success: boolean; output: string }> {
|
|
242
|
+
try {
|
|
243
|
+
const composePath = getComposePath()
|
|
244
|
+
const result = await $`docker compose -f ${composePath} pull ${service}`.text()
|
|
245
|
+
return { success: true, output: result }
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return { success: false, output: String(error) }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Recreate a specific service (pull + up)
|
|
253
|
+
*/
|
|
254
|
+
export async function recreateService(service: string): Promise<{ success: boolean; output: string }> {
|
|
255
|
+
try {
|
|
256
|
+
const composePath = getComposePath()
|
|
257
|
+
// Pull first, then recreate
|
|
258
|
+
await $`docker compose -f ${composePath} pull ${service}`.text()
|
|
259
|
+
const result = await $`docker compose -f ${composePath} up -d --force-recreate ${service}`.text()
|
|
260
|
+
return { success: true, output: result }
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return { success: false, output: String(error) }
|
|
263
|
+
}
|
|
264
|
+
}
|
package/src/ui/App.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { QuickSetup } from "./screens/QuickSetup"
|
|
|
12
12
|
import { AppManager } from "./screens/AppManager"
|
|
13
13
|
import { ContainerControl } from "./screens/ContainerControl"
|
|
14
14
|
import { AdvancedSettings } from "./screens/AdvancedSettings"
|
|
15
|
+
import { checkForUpdates } from "../utils/update-checker"
|
|
16
|
+
import { UpdateNotification } from "./components/UpdateNotification"
|
|
15
17
|
|
|
16
18
|
export type Screen = "main" | "quickSetup" | "appManager" | "containerControl" | "advancedSettings"
|
|
17
19
|
|
|
@@ -40,7 +42,19 @@ export class App {
|
|
|
40
42
|
if (!this.config) {
|
|
41
43
|
this.navigateTo("quickSetup")
|
|
42
44
|
} else {
|
|
43
|
-
|
|
45
|
+
// Check for updates before showing main menu
|
|
46
|
+
const updateInfo = await checkForUpdates()
|
|
47
|
+
|
|
48
|
+
if (updateInfo.updateAvailable) {
|
|
49
|
+
// Show update notification
|
|
50
|
+
const notification = new UpdateNotification(this.renderer, updateInfo, () => {
|
|
51
|
+
// After dismissing, show main menu
|
|
52
|
+
this.navigateTo("main")
|
|
53
|
+
})
|
|
54
|
+
this.renderer.root.add(notification)
|
|
55
|
+
} else {
|
|
56
|
+
this.navigateTo("main")
|
|
57
|
+
}
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
// Handle exit
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Notification Component
|
|
3
|
+
* Popup overlay showing new version available
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BoxRenderable, TextRenderable, CliRenderer, KeyEvent } from "@opentui/core"
|
|
7
|
+
import type { UpdateInfo } from "../../utils/update-checker"
|
|
8
|
+
|
|
9
|
+
export class UpdateNotification extends BoxRenderable {
|
|
10
|
+
private cliRenderer: CliRenderer
|
|
11
|
+
private updateInfo: UpdateInfo
|
|
12
|
+
private onDismiss: () => void
|
|
13
|
+
private keyHandler: (key: KeyEvent) => void
|
|
14
|
+
|
|
15
|
+
constructor(renderer: CliRenderer, updateInfo: UpdateInfo, onDismiss: () => void) {
|
|
16
|
+
super(renderer, {
|
|
17
|
+
id: "update-notification",
|
|
18
|
+
position: "absolute",
|
|
19
|
+
top: "50%",
|
|
20
|
+
left: "50%",
|
|
21
|
+
width: 50,
|
|
22
|
+
height: 12,
|
|
23
|
+
marginTop: -6, // Center vertically
|
|
24
|
+
marginLeft: -25, // Center horizontally
|
|
25
|
+
flexDirection: "column",
|
|
26
|
+
borderStyle: "rounded",
|
|
27
|
+
borderColor: "#50fa7b",
|
|
28
|
+
padding: 1,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
this.cliRenderer = renderer
|
|
32
|
+
this.updateInfo = updateInfo
|
|
33
|
+
this.onDismiss = onDismiss
|
|
34
|
+
|
|
35
|
+
this.buildContent()
|
|
36
|
+
|
|
37
|
+
// Key handler
|
|
38
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
39
|
+
if (key.name === "return" || key.name === "escape" || key.name === "q") {
|
|
40
|
+
this.dismiss()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
renderer.keyInput.on("keypress", this.keyHandler)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private buildContent(): void {
|
|
47
|
+
// Header
|
|
48
|
+
this.add(
|
|
49
|
+
new TextRenderable(this.cliRenderer, {
|
|
50
|
+
content: "🎉 Update Available!",
|
|
51
|
+
fg: "#50fa7b",
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Spacer
|
|
56
|
+
this.add(new BoxRenderable(this.cliRenderer, { height: 1 }))
|
|
57
|
+
|
|
58
|
+
// Version info
|
|
59
|
+
this.add(
|
|
60
|
+
new TextRenderable(this.cliRenderer, {
|
|
61
|
+
content: `Current: v${this.updateInfo.currentVersion}`,
|
|
62
|
+
fg: "#6272a4",
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
this.add(
|
|
67
|
+
new TextRenderable(this.cliRenderer, {
|
|
68
|
+
content: `Latest: ${this.updateInfo.latestVersion}`,
|
|
69
|
+
fg: "#f1fa8c",
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Spacer
|
|
74
|
+
this.add(new BoxRenderable(this.cliRenderer, { height: 1 }))
|
|
75
|
+
|
|
76
|
+
// Update command
|
|
77
|
+
this.add(
|
|
78
|
+
new TextRenderable(this.cliRenderer, {
|
|
79
|
+
content: "Run: bun update -g @muhammedaksam/easiarr",
|
|
80
|
+
fg: "#8be9fd",
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Spacer
|
|
85
|
+
this.add(new BoxRenderable(this.cliRenderer, { height: 1 }))
|
|
86
|
+
|
|
87
|
+
// Dismiss hint
|
|
88
|
+
this.add(
|
|
89
|
+
new TextRenderable(this.cliRenderer, {
|
|
90
|
+
content: "Press Enter or Esc to continue",
|
|
91
|
+
fg: "#6272a4",
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private dismiss(): void {
|
|
97
|
+
this.cliRenderer.keyInput.off("keypress", this.keyHandler)
|
|
98
|
+
this.destroy()
|
|
99
|
+
this.onDismiss()
|
|
100
|
+
}
|
|
101
|
+
}
|