@muhammedaksam/easiarr 1.4.0 → 1.4.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/dist/index.js +29837 -0
- package/package.json +15 -15
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -521
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/base-api.ts +0 -216
- package/src/api/bazarr-api.ts +0 -297
- package/src/api/cloudflare-api.ts +0 -493
- package/src/api/custom-format-api.ts +0 -224
- package/src/api/grafana-api.ts +0 -230
- package/src/api/heimdall-api.ts +0 -141
- package/src/api/homarr-api.ts +0 -209
- package/src/api/huntarr-api.ts +0 -457
- package/src/api/index.ts +0 -9
- package/src/api/jellyfin-api.ts +0 -389
- package/src/api/jellyseerr-api.ts +0 -559
- package/src/api/maintainerr-api.ts +0 -461
- package/src/api/naming-config.ts +0 -101
- package/src/api/overseerr-api.ts +0 -392
- package/src/api/plex-api.ts +0 -308
- package/src/api/portainer-api.ts +0 -374
- package/src/api/profilarr-api.ts +0 -391
- package/src/api/prowlarr-api.ts +0 -488
- package/src/api/qbittorrent-api.ts +0 -300
- package/src/api/quality-profile-api.ts +0 -272
- package/src/api/tautulli-api.ts +0 -244
- package/src/api/uptime-kuma-api.ts +0 -343
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1275
- package/src/compose/caddy-config.ts +0 -130
- package/src/compose/generator.ts +0 -287
- package/src/compose/index.ts +0 -7
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -187
- package/src/config/bookmarks-generator.ts +0 -160
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -381
- package/src/config/index.ts +0 -6
- package/src/config/manager.ts +0 -150
- package/src/config/recyclarr-config.ts +0 -181
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -86
- package/src/config/soularr-config.ts +0 -105
- package/src/config/trash-quality-definitions.ts +0 -213
- package/src/data/lidarr-custom-formats.ts +0 -127
- package/src/data/trash-profiles.ts +0 -333
- package/src/docker/client.ts +0 -312
- package/src/docker/index.ts +0 -18
- package/src/index.ts +0 -77
- package/src/setup/actions/arr-common.ts +0 -307
- package/src/setup/actions/dashboards.ts +0 -79
- package/src/setup/actions/download-clients.ts +0 -54
- package/src/setup/actions/jellyseerr.ts +0 -331
- package/src/setup/actions/media-servers.ts +0 -68
- package/src/setup/actions/monitoring.ts +0 -120
- package/src/setup/actions/prowlarr.ts +0 -137
- package/src/setup/actions/utilities.ts +0 -490
- package/src/setup/index.ts +0 -78
- package/src/setup/types.ts +0 -144
- package/src/structure/manager.ts +0 -129
- package/src/ui/App.ts +0 -111
- package/src/ui/components/ApplicationSelector.ts +0 -275
- package/src/ui/components/CredentialsForm.ts +0 -180
- package/src/ui/components/DownloadClientForm.ts +0 -114
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -160
- package/src/ui/components/UpdateNotification.ts +0 -102
- package/src/ui/index.ts +0 -7
- package/src/ui/screens/AdvancedSettings.ts +0 -188
- package/src/ui/screens/ApiKeyViewer.ts +0 -511
- package/src/ui/screens/AppConfigurator.ts +0 -543
- package/src/ui/screens/AppManager.ts +0 -284
- package/src/ui/screens/ContainerControl.ts +0 -581
- package/src/ui/screens/LogsViewer.ts +0 -478
- package/src/ui/screens/MainMenu.ts +0 -299
- package/src/ui/screens/MonitorDashboard.ts +0 -881
- package/src/ui/screens/SecretsEditor.ts +0 -216
- package/src/ui/screens/SettingsScreen.ts +0 -633
- package/src/ui/screens/index.ts +0 -22
- package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
- package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
- package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
- package/src/ui/screens/setup/HomepageSetup.ts +0 -165
- package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
- package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
- package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
- package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
- package/src/ui/screens/setup/QuickSetup.ts +0 -1169
- package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
- package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
- package/src/utils/arch.ts +0 -78
- package/src/utils/browser.ts +0 -27
- package/src/utils/categories.ts +0 -61
- package/src/utils/debug.ts +0 -84
- package/src/utils/env.ts +0 -138
- package/src/utils/index.ts +0 -8
- package/src/utils/logs.ts +0 -119
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
- package/src/utils/migrations.ts +0 -165
- package/src/utils/password.ts +0 -42
- package/src/utils/paths.ts +0 -33
- package/src/utils/unraid.ts +0 -103
- package/src/utils/update-checker.ts +0 -210
- package/src/utils/url-utils.ts +0 -38
package/src/utils/arch.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architecture Detection Utility
|
|
3
|
-
* Detects system architecture and checks app compatibility
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AppDefinition, Architecture } from "~/config/schema"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Get the current system architecture
|
|
10
|
-
*/
|
|
11
|
-
export function getSystemArch(): Architecture {
|
|
12
|
-
const arch = process.arch
|
|
13
|
-
switch (arch) {
|
|
14
|
-
case "x64":
|
|
15
|
-
case "ia32":
|
|
16
|
-
return "x64"
|
|
17
|
-
case "arm64":
|
|
18
|
-
return "arm64"
|
|
19
|
-
case "arm":
|
|
20
|
-
return "arm32"
|
|
21
|
-
default:
|
|
22
|
-
return "x64" // Default to x64 for unknown
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Check if an app is compatible with the given architecture
|
|
28
|
-
* Returns true if compatible (no issues), false if deprecated/broken
|
|
29
|
-
*/
|
|
30
|
-
export function isAppCompatible(app: AppDefinition, arch?: Architecture): boolean {
|
|
31
|
-
const systemArch = arch ?? getSystemArch()
|
|
32
|
-
|
|
33
|
-
if (!app.arch) {
|
|
34
|
-
return true // No arch restrictions = supports all
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check if explicitly deprecated
|
|
38
|
-
if (app.arch.deprecated?.includes(systemArch)) {
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// If supported list exists, check if current arch is in it
|
|
43
|
-
if (app.arch.supported && !app.arch.supported.includes(systemArch)) {
|
|
44
|
-
return false
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return true
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get warning message for an app on the current architecture
|
|
52
|
-
* Returns null if no warning
|
|
53
|
-
*/
|
|
54
|
-
export function getArchWarning(app: AppDefinition, arch?: Architecture): string | null {
|
|
55
|
-
const systemArch = arch ?? getSystemArch()
|
|
56
|
-
|
|
57
|
-
if (!app.arch) {
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (app.arch.deprecated?.includes(systemArch)) {
|
|
62
|
-
return app.arch.warning || `${app.name} has deprecated support for ${systemArch}`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (app.arch.supported && !app.arch.supported.includes(systemArch)) {
|
|
66
|
-
return `${app.name} does not support ${systemArch} architecture`
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Check if app is deprecated (but might still work)
|
|
74
|
-
*/
|
|
75
|
-
export function isAppDeprecated(app: AppDefinition, arch?: Architecture): boolean {
|
|
76
|
-
const systemArch = arch ?? getSystemArch()
|
|
77
|
-
return app.arch?.deprecated?.includes(systemArch) ?? false
|
|
78
|
-
}
|
package/src/utils/browser.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser Utilities
|
|
3
|
-
* Open URLs in the default browser
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { platform } from "node:os"
|
|
7
|
-
|
|
8
|
-
import { $ } from "bun"
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Open a URL in the default browser
|
|
12
|
-
*/
|
|
13
|
-
export async function openUrl(url: string): Promise<void> {
|
|
14
|
-
const os = platform()
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
if (os === "linux") {
|
|
18
|
-
await $`xdg-open ${url}`.quiet()
|
|
19
|
-
} else if (os === "darwin") {
|
|
20
|
-
await $`open ${url}`.quiet()
|
|
21
|
-
} else if (os === "win32") {
|
|
22
|
-
await $`cmd /c start ${url}`.quiet()
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Silently fail if browser can't be opened
|
|
26
|
-
}
|
|
27
|
-
}
|
package/src/utils/categories.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Download Category Utilities
|
|
3
|
-
* Shared category mappings for *arr apps and download clients
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AppId } from "~/config/schema"
|
|
7
|
-
|
|
8
|
-
// Category info for each *arr app
|
|
9
|
-
export interface CategoryInfo {
|
|
10
|
-
name: string
|
|
11
|
-
savePath: string
|
|
12
|
-
fieldName: string // Field name used in *arr download client config
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Master mapping of app IDs to their download categories
|
|
16
|
-
const CATEGORY_MAP: Partial<Record<AppId, CategoryInfo>> = {
|
|
17
|
-
radarr: { name: "movies", savePath: "/data/torrents/movies", fieldName: "movieCategory" },
|
|
18
|
-
sonarr: { name: "tv", savePath: "/data/torrents/tv", fieldName: "tvCategory" },
|
|
19
|
-
lidarr: { name: "music", savePath: "/data/torrents/music", fieldName: "musicCategory" },
|
|
20
|
-
readarr: { name: "books", savePath: "/data/torrents/books", fieldName: "bookCategory" },
|
|
21
|
-
whisparr: { name: "adult", savePath: "/data/torrents/adult", fieldName: "tvCategory" },
|
|
22
|
-
mylar3: { name: "comics", savePath: "/data/torrents/comics", fieldName: "category" },
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Get category name for an app (e.g., "movies" for radarr)
|
|
27
|
-
*/
|
|
28
|
-
export function getCategoryForApp(appId: AppId): string {
|
|
29
|
-
return CATEGORY_MAP[appId]?.name ?? "default"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get the field name used in *arr download client config for category
|
|
34
|
-
* (e.g., "movieCategory" for radarr, "tvCategory" for sonarr)
|
|
35
|
-
*/
|
|
36
|
-
export function getCategoryFieldName(appId: AppId): string {
|
|
37
|
-
return CATEGORY_MAP[appId]?.fieldName ?? "category"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get the save path for an app's downloads (e.g., "/data/torrents/movies" for radarr)
|
|
42
|
-
*/
|
|
43
|
-
export function getCategorySavePath(appId: AppId): string {
|
|
44
|
-
return CATEGORY_MAP[appId]?.savePath ?? "/data/torrents"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get full category info for an app
|
|
49
|
-
*/
|
|
50
|
-
export function getCategoryInfo(appId: AppId): CategoryInfo | undefined {
|
|
51
|
-
return CATEGORY_MAP[appId]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get all category infos for a list of enabled app IDs
|
|
56
|
-
*/
|
|
57
|
-
export function getCategoriesForApps(appIds: AppId[]): CategoryInfo[] {
|
|
58
|
-
return appIds
|
|
59
|
-
.map((id) => CATEGORY_MAP[id])
|
|
60
|
-
.filter((info): info is CategoryInfo => info !== undefined)
|
|
61
|
-
}
|
package/src/utils/debug.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debug logging utility for easiarr
|
|
3
|
-
*
|
|
4
|
-
* Enable debug logging via:
|
|
5
|
-
* - CLI flag: easiarr --debug or -d
|
|
6
|
-
* - Environment variable: EASIARR_DEBUG=1 bun dev
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { appendFileSync, writeFileSync } from "node:fs"
|
|
10
|
-
|
|
11
|
-
import { getConfigPath } from "./paths"
|
|
12
|
-
|
|
13
|
-
// Check CLI args for --debug flag
|
|
14
|
-
const hasDebugFlag = process.argv.includes("--debug") || process.argv.includes("-d")
|
|
15
|
-
const hasEnvDebug = process.env.EASIARR_DEBUG === "1" || process.env.EASIARR_DEBUG === "true"
|
|
16
|
-
|
|
17
|
-
export const DEBUG_ENABLED = hasDebugFlag || hasEnvDebug
|
|
18
|
-
|
|
19
|
-
// Save debug log to XDG config dir like other config files
|
|
20
|
-
const logFile = getConfigPath("debug.log")
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Initialize debug mode - clears old log file
|
|
24
|
-
*/
|
|
25
|
-
export function initDebug(): void {
|
|
26
|
-
if (!DEBUG_ENABLED) return
|
|
27
|
-
try {
|
|
28
|
-
writeFileSync(logFile, `=== easiarr Debug Log - ${new Date().toISOString()} ===\n`)
|
|
29
|
-
} catch {
|
|
30
|
-
// Ignore
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Sanitize sensitive fields from log messages
|
|
36
|
-
* Redacts passwords, tokens, API keys, secrets, and credentials
|
|
37
|
-
*/
|
|
38
|
-
export function sanitizeMessage(message: string): string {
|
|
39
|
-
// Match common sensitive field names in JSON format
|
|
40
|
-
// Covers: passwords, tokens, API keys, secrets, credentials, auth data
|
|
41
|
-
return message.replace(
|
|
42
|
-
/"(password|passwordConfirmation|Password|Pw|passwd|pass|apiKey|api_key|ApiKey|API_KEY|token|accessToken|access_token|refreshToken|refresh_token|bearerToken|jwtToken|jwt|secret|secretKey|secret_key|privateKey|private_key|credential|auth|authorization|authToken|client_secret|clientSecret|WIREGUARD_PRIVATE_KEY|TUNNEL_TOKEN|USERNAME_VPN|PASSWORD_VPN)":\s*"[^"]*"/gi,
|
|
43
|
-
'"$1":"[REDACTED]"'
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Log a debug message to debug.log file if debug mode is enabled
|
|
49
|
-
* Automatically sanitizes sensitive data (passwords, tokens, etc.)
|
|
50
|
-
*/
|
|
51
|
-
export function debugLog(category: string, message: string): void {
|
|
52
|
-
if (!DEBUG_ENABLED) return
|
|
53
|
-
|
|
54
|
-
const timestamp = new Date().toISOString()
|
|
55
|
-
const sanitizedMessage = sanitizeMessage(message)
|
|
56
|
-
const line = `[${timestamp}] [${category}] ${sanitizedMessage}\n`
|
|
57
|
-
try {
|
|
58
|
-
appendFileSync(logFile, line)
|
|
59
|
-
} catch {
|
|
60
|
-
// Ignore logging errors
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Log API request details for debugging
|
|
66
|
-
*/
|
|
67
|
-
export function debugRequest(method: string, url: string, body?: unknown): void {
|
|
68
|
-
if (!DEBUG_ENABLED) return
|
|
69
|
-
debugLog("API", `${method} ${url}`)
|
|
70
|
-
if (body) {
|
|
71
|
-
debugLog("API", `Body: ${JSON.stringify(body, null, 2)}`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Log API response details for debugging
|
|
77
|
-
*/
|
|
78
|
-
export function debugResponse(status: number, url: string, body?: string): void {
|
|
79
|
-
if (!DEBUG_ENABLED) return
|
|
80
|
-
debugLog("API", `Response ${status} from ${url}`)
|
|
81
|
-
if (body && body.length < 2000) {
|
|
82
|
-
debugLog("API", `Response Body: ${body}`)
|
|
83
|
-
}
|
|
84
|
-
}
|
package/src/utils/env.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Environment File Utilities
|
|
3
|
-
* Shared functions for reading/writing .env files
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
7
|
-
import { readFile, writeFile } from "node:fs/promises"
|
|
8
|
-
import { networkInterfaces } from "node:os"
|
|
9
|
-
|
|
10
|
-
import { getComposePath } from "~/config/manager"
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Get the local IP address of the Docker host
|
|
14
|
-
* Returns the first non-internal IPv4 address found
|
|
15
|
-
*/
|
|
16
|
-
export function getLocalIp(): string {
|
|
17
|
-
const nets = networkInterfaces()
|
|
18
|
-
|
|
19
|
-
for (const name of Object.keys(nets)) {
|
|
20
|
-
const interfaces = nets[name]
|
|
21
|
-
if (!interfaces) continue
|
|
22
|
-
|
|
23
|
-
for (const net of interfaces) {
|
|
24
|
-
// Skip internal (loopback) and non-IPv4 addresses
|
|
25
|
-
if (net.family === "IPv4" && !net.internal) {
|
|
26
|
-
return net.address
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return "localhost"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Get the path to the .env file
|
|
36
|
-
*/
|
|
37
|
-
export function getEnvPath(): string {
|
|
38
|
-
return getComposePath().replace("docker-compose.yml", ".env")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse an .env file into a key-value object
|
|
43
|
-
* Preserves existing values and handles multi-part values (e.g., with = in them)
|
|
44
|
-
*/
|
|
45
|
-
export function parseEnvFile(content: string): Record<string, string> {
|
|
46
|
-
const env: Record<string, string> = {}
|
|
47
|
-
|
|
48
|
-
for (const line of content.split("\n")) {
|
|
49
|
-
const trimmed = line.trim()
|
|
50
|
-
if (!trimmed || trimmed.startsWith("#")) continue
|
|
51
|
-
|
|
52
|
-
const [key, ...val] = trimmed.split("=")
|
|
53
|
-
if (key && val.length > 0) {
|
|
54
|
-
env[key.trim()] = val.join("=").trim()
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return env
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Serialize an env object to .env file format
|
|
63
|
-
*/
|
|
64
|
-
export function serializeEnv(env: Record<string, string>): string {
|
|
65
|
-
return Object.entries(env)
|
|
66
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
67
|
-
.join("\n")
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Read the .env file and return parsed key-value object
|
|
72
|
-
* Returns empty object if file doesn't exist
|
|
73
|
-
*/
|
|
74
|
-
export function readEnvSync(): Record<string, string> {
|
|
75
|
-
const envPath = getEnvPath()
|
|
76
|
-
if (!existsSync(envPath)) return {}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const content = readFileSync(envPath, "utf-8")
|
|
80
|
-
return parseEnvFile(content)
|
|
81
|
-
} catch {
|
|
82
|
-
return {}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Write to .env file synchronously, merging with existing values
|
|
88
|
-
* Preserves existing keys not in the updates object
|
|
89
|
-
*/
|
|
90
|
-
export function writeEnvSync(updates: Record<string, string>): void {
|
|
91
|
-
const envPath = getEnvPath()
|
|
92
|
-
const current = readEnvSync()
|
|
93
|
-
|
|
94
|
-
// Merge updates into current
|
|
95
|
-
const merged = { ...current, ...updates }
|
|
96
|
-
|
|
97
|
-
// Write back
|
|
98
|
-
const content = serializeEnv(merged)
|
|
99
|
-
writeFileSync(envPath, content, "utf-8")
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Read the .env file asynchronously
|
|
104
|
-
*/
|
|
105
|
-
export async function readEnv(): Promise<Record<string, string>> {
|
|
106
|
-
const envPath = getEnvPath()
|
|
107
|
-
if (!existsSync(envPath)) return {}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const content = await readFile(envPath, "utf-8")
|
|
111
|
-
return parseEnvFile(content)
|
|
112
|
-
} catch {
|
|
113
|
-
return {}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Write to .env file, merging with existing values
|
|
119
|
-
* Preserves existing keys not in the updates object
|
|
120
|
-
*/
|
|
121
|
-
export async function updateEnv(updates: Record<string, string>): Promise<void> {
|
|
122
|
-
const envPath = getEnvPath()
|
|
123
|
-
const current = await readEnv()
|
|
124
|
-
|
|
125
|
-
// Merge updates into current
|
|
126
|
-
const merged = { ...current, ...updates }
|
|
127
|
-
|
|
128
|
-
// Write back
|
|
129
|
-
const content = serializeEnv(merged)
|
|
130
|
-
await writeFile(envPath, content, "utf-8")
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get a specific value from .env
|
|
135
|
-
*/
|
|
136
|
-
export function getEnvValue(key: string): string | undefined {
|
|
137
|
-
return readEnvSync()[key]
|
|
138
|
-
}
|
package/src/utils/index.ts
DELETED
package/src/utils/logs.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Log File Management Utilities
|
|
3
|
-
* Handles saving and managing container logs to XDG config dir
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { mkdir, readdir, stat, writeFile } from "node:fs/promises"
|
|
7
|
-
import { join } from "node:path"
|
|
8
|
-
|
|
9
|
-
import { debugLog } from "./debug"
|
|
10
|
-
import { getConfigPath } from "./paths"
|
|
11
|
-
|
|
12
|
-
const LOGS_DIR = getConfigPath("logs")
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Get the logs directory path for a specific app
|
|
16
|
-
*/
|
|
17
|
-
export function getLogPath(appId: string): string {
|
|
18
|
-
return join(LOGS_DIR, appId)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Ensure the logs directory exists for an app
|
|
23
|
-
*/
|
|
24
|
-
async function ensureLogDir(appId: string): Promise<string> {
|
|
25
|
-
const logDir = getLogPath(appId)
|
|
26
|
-
await mkdir(logDir, { recursive: true })
|
|
27
|
-
return logDir
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Generate a timestamped log filename
|
|
32
|
-
*/
|
|
33
|
-
function generateLogFilename(): string {
|
|
34
|
-
const now = new Date()
|
|
35
|
-
const timestamp = now.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19)
|
|
36
|
-
return `${timestamp}.log`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Save container logs to a file
|
|
41
|
-
* @returns The path to the saved log file
|
|
42
|
-
*/
|
|
43
|
-
export async function saveLog(appId: string, content: string): Promise<string> {
|
|
44
|
-
const logDir = await ensureLogDir(appId)
|
|
45
|
-
const filename = generateLogFilename()
|
|
46
|
-
const filepath = join(logDir, filename)
|
|
47
|
-
|
|
48
|
-
await writeFile(filepath, content, "utf-8")
|
|
49
|
-
debugLog("Logs", `Saved log for ${appId} to ${filepath}`)
|
|
50
|
-
|
|
51
|
-
return filepath
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* List saved logs for an app
|
|
56
|
-
* @returns Array of log file info sorted by date (newest first)
|
|
57
|
-
*/
|
|
58
|
-
export async function listSavedLogs(
|
|
59
|
-
appId: string
|
|
60
|
-
): Promise<Array<{ filename: string; path: string; date: Date; size: number }>> {
|
|
61
|
-
const logDir = getLogPath(appId)
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const files = await readdir(logDir)
|
|
65
|
-
const logFiles = files.filter((f) => f.endsWith(".log"))
|
|
66
|
-
|
|
67
|
-
const fileInfos = await Promise.all(
|
|
68
|
-
logFiles.map(async (filename) => {
|
|
69
|
-
const path = join(logDir, filename)
|
|
70
|
-
const stats = await stat(path)
|
|
71
|
-
return {
|
|
72
|
-
filename,
|
|
73
|
-
path,
|
|
74
|
-
date: stats.mtime,
|
|
75
|
-
size: stats.size,
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
// Sort by date, newest first
|
|
81
|
-
return fileInfos.sort((a, b) => b.date.getTime() - a.date.getTime())
|
|
82
|
-
} catch {
|
|
83
|
-
// Directory doesn't exist yet
|
|
84
|
-
return []
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get the base logs directory path
|
|
90
|
-
*/
|
|
91
|
-
export function getLogsBaseDir(): string {
|
|
92
|
-
return LOGS_DIR
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Format bytes to human-readable size
|
|
97
|
-
*/
|
|
98
|
-
export function formatBytes(bytes: number): string {
|
|
99
|
-
if (bytes < 1024) return `${bytes} B`
|
|
100
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
101
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Format relative time (e.g., "2 hours ago")
|
|
106
|
-
*/
|
|
107
|
-
export function formatRelativeTime(date: Date): string {
|
|
108
|
-
const now = new Date()
|
|
109
|
-
const diffMs = now.getTime() - date.getTime()
|
|
110
|
-
const diffSecs = Math.floor(diffMs / 1000)
|
|
111
|
-
const diffMins = Math.floor(diffSecs / 60)
|
|
112
|
-
const diffHours = Math.floor(diffMins / 60)
|
|
113
|
-
const diffDays = Math.floor(diffHours / 24)
|
|
114
|
-
|
|
115
|
-
if (diffDays > 0) return `${diffDays}d ago`
|
|
116
|
-
if (diffHours > 0) return `${diffHours}h ago`
|
|
117
|
-
if (diffMins > 0) return `${diffMins}m ago`
|
|
118
|
-
return "just now"
|
|
119
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration: Rename env variables to new format
|
|
3
|
-
*
|
|
4
|
-
* OLD: GLOBAL_PASSWORD, QBITTORRENT_USER, etc.
|
|
5
|
-
* NEW: PASSWORD_GLOBAL, USERNAME_QBITTORRENT, etc.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
9
|
-
import { homedir } from "node:os"
|
|
10
|
-
import { join } from "node:path"
|
|
11
|
-
|
|
12
|
-
import { debugLog } from "~/utils/debug"
|
|
13
|
-
import { parseEnvFile, serializeEnv } from "~/utils/env"
|
|
14
|
-
|
|
15
|
-
const ENV_FILE = join(homedir(), ".easiarr", ".env")
|
|
16
|
-
|
|
17
|
-
export const name = "rename-env-variables"
|
|
18
|
-
|
|
19
|
-
const renames: [string, string][] = [
|
|
20
|
-
// Global credentials
|
|
21
|
-
["GLOBAL_PASSWORD", "PASSWORD_GLOBAL"],
|
|
22
|
-
["GLOBAL_USERNAME", "USERNAME_GLOBAL"],
|
|
23
|
-
// qBittorrent
|
|
24
|
-
["QBITTORRENT_PASSWORD", "PASSWORD_QBITTORRENT"],
|
|
25
|
-
["QBITTORRENT_USER", "USERNAME_QBITTORRENT"],
|
|
26
|
-
["QBITTORRENT_PASS", "PASSWORD_QBITTORRENT"], // Legacy alias
|
|
27
|
-
// Portainer
|
|
28
|
-
["PORTAINER_PASSWORD", "PASSWORD_PORTAINER"],
|
|
29
|
-
// PostgreSQL
|
|
30
|
-
["POSTGRESQL_USERNAME", "USERNAME_POSTGRESQL"],
|
|
31
|
-
["POSTGRESQL_PASSWORD", "PASSWORD_POSTGRESQL"],
|
|
32
|
-
// VPN
|
|
33
|
-
["VPN_USERNAME", "USERNAME_VPN"],
|
|
34
|
-
["VPN_PASSWORD", "PASSWORD_VPN"],
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
export function up(): boolean {
|
|
38
|
-
if (!existsSync(ENV_FILE)) {
|
|
39
|
-
debugLog("Migrations", "No .env file found, skipping migration")
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const content = readFileSync(ENV_FILE, "utf-8")
|
|
45
|
-
const env = parseEnvFile(content)
|
|
46
|
-
let changed = false
|
|
47
|
-
|
|
48
|
-
for (const [oldKey, newKey] of renames) {
|
|
49
|
-
if (env[oldKey] !== undefined && env[newKey] === undefined) {
|
|
50
|
-
env[newKey] = env[oldKey]
|
|
51
|
-
delete env[oldKey]
|
|
52
|
-
changed = true
|
|
53
|
-
debugLog("Migrations", `Renamed ${oldKey} → ${newKey}`)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (changed) {
|
|
58
|
-
writeFileSync(ENV_FILE, serializeEnv(env), "utf-8")
|
|
59
|
-
debugLog("Migrations", "Migration completed: env variables renamed")
|
|
60
|
-
return true
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
debugLog("Migrations", "No changes needed")
|
|
64
|
-
return false
|
|
65
|
-
} catch (e) {
|
|
66
|
-
debugLog("Migrations", `Migration error: ${e}`)
|
|
67
|
-
return false
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function down(): boolean {
|
|
72
|
-
if (!existsSync(ENV_FILE)) {
|
|
73
|
-
return false
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const content = readFileSync(ENV_FILE, "utf-8")
|
|
78
|
-
const env = parseEnvFile(content)
|
|
79
|
-
let changed = false
|
|
80
|
-
|
|
81
|
-
for (const [oldKey, newKey] of renames) {
|
|
82
|
-
// Reverse operation: restore oldKey from newKey
|
|
83
|
-
if (env[newKey] !== undefined && env[oldKey] === undefined) {
|
|
84
|
-
env[oldKey] = env[newKey]
|
|
85
|
-
delete env[newKey]
|
|
86
|
-
changed = true
|
|
87
|
-
debugLog("Migrations", `Rolled back ${newKey} → ${oldKey}`)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (changed) {
|
|
92
|
-
writeFileSync(ENV_FILE, serializeEnv(env), "utf-8")
|
|
93
|
-
debugLog("Migrations", "Rollback completed: env variables restored")
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return false
|
|
98
|
-
} catch (e) {
|
|
99
|
-
debugLog("Migrations", `Rollback error: ${e}`)
|
|
100
|
-
return false
|
|
101
|
-
}
|
|
102
|
-
}
|