@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.
@@ -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
- this.navigateTo("main")
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
+ }