WIP - Common package

This commit is contained in:
Nicolas Meienberger 2022-06-02 22:12:51 +02:00
parent 31c9295a76
commit 123aaee235
41 changed files with 593 additions and 584 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
**/node_modules/
**/.next/
/node_modules/
/.next/

View File

@ -3,12 +3,12 @@
"available": true,
"port": 8104,
"id": "adguard",
"categories": ["network", "security"],
"description": "Adguard is the best way to get rid of annoying ads and online tracking and protect your computer from malware. Make your web surfing fast, safe and ad-free.",
"short_desc": "World's most advanced adblocker!",
"author": "ArneNaessens",
"source": "https://github.com/AdguardTeam",
"image": "https://avatars.githubusercontent.com/u/8361145?s=200&v=4",
"cagegories": ["network", "security"],
"requirements": {
"ports": [53]
},

View File

@ -37,6 +37,7 @@ services:
volumes:
- ${PWD}/packages/dashboard:/app
- /app/node_modules
- /app/.next
labels:
traefik.enable: true
traefik.http.routers.dashboard.rule: PathPrefix("/") # Host(`tipi.local`) &&

View File

@ -0,0 +1,4 @@
node_modules/
dist/
*.cjs
dist/

View File

@ -0,0 +1,18 @@
module.exports = {
env: { node: true, jest: true },
extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'import', 'react'],
rules: {
'arrow-body-style': 0,
'no-restricted-exports': 0,
'max-len': [1, { code: 200 }],
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
},
};

2
packages/common/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,6 @@
module.exports = {
singleQuote: true,
semi: true,
trailingComma: 'all',
printWidth: 200,
};

View File

@ -0,0 +1,23 @@
{
"name": "@runtipi/common",
"version": "0.2.7",
"main": "./dist/index.js",
"files": [
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc -b tsconfig.build.json"
},
"author": "",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.14.38",
"typescript": "4.6.4"
},
"dependencies": {},
"description": "",
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,13 @@
import { AppCategoriesEnum } from '../types';
// Icons should come from FontAwesome https://react-icons.github.io/react-icons/icons?name=fa
export const APP_CATEGORIES = [
{ name: 'Network', id: AppCategoriesEnum.NETWORK, icon: 'FaNetworkWired' },
{ name: 'Media', id: AppCategoriesEnum.MEDIA, icon: 'FaVideo' },
{ name: 'Development', id: AppCategoriesEnum.DEVELOPMENT, icon: 'FaCode' },
{ name: 'Automation', id: AppCategoriesEnum.AUTOMATION, icon: 'FaRobot' },
{ name: 'Social', id: AppCategoriesEnum.SOCIAL, icon: 'FaUserFriends' },
{ name: 'Utilities', id: AppCategoriesEnum.UTILITIES, icon: 'FaWrench' },
{ name: 'Photography', id: AppCategoriesEnum.PHOTOGRAPHY, icon: 'FaCamera' },
{ name: 'Security', id: AppCategoriesEnum.SECURITY, icon: 'FaShieldAlt' },
];

View File

@ -0,0 +1 @@
export * from './app.constants';

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './constants';

View File

@ -0,0 +1,60 @@
export enum AppCategoriesEnum {
NETWORK = 'network',
MEDIA = 'media',
DEVELOPMENT = 'development',
AUTOMATION = 'automation',
SOCIAL = 'social',
UTILITIES = 'utilities',
PHOTOGRAPHY = 'photography',
SECURITY = 'security',
}
export enum FieldTypes {
text = 'text',
password = 'password',
email = 'email',
number = 'number',
fqdn = 'fqdn',
ip = 'ip',
fqdnip = 'fqdnip',
url = 'url',
}
interface FormField {
type: FieldTypes;
label: string;
max?: number;
min?: number;
hint?: string;
required?: boolean;
env_variable: string;
}
export enum AppStatusEnum {
RUNNING = 'running',
STOPPED = 'stopped',
INSTALLING = 'installing',
UNINSTALLING = 'uninstalling',
STOPPING = 'stopping',
STARTING = 'starting',
}
export interface AppConfig {
id: string;
available: boolean;
port: number;
name: string;
requirements?: {
ports?: number[];
};
description: string;
version: string;
image: string;
form_fields: Record<string, FormField>;
short_desc: string;
author: string;
source: string;
installed: boolean;
categories: AppCategoriesEnum[];
status: AppStatusEnum;
}

View File

@ -0,0 +1 @@
export * from './app.types';

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["**/*.ts", "**/*.tsx"]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": false,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "preserve",
"incremental": false,
"declaration": true,
"outDir": "./dist"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "jest.config.cjs"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,2 +1,2 @@
node_modules/
.next/
.next/

View File

@ -1,4 +1,4 @@
FROM node:18-buster-slim
FROM node:18
WORKDIR /app

View File

@ -2,6 +2,13 @@
const { NODE_ENV, INTERNAL_IP } = process.env;
const nextConfig = {
webpackDevMiddleware: (config) => {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
};
return config;
},
reactStrictMode: true,
env: {
INTERNAL_IP: INTERNAL_IP,

View File

@ -9,10 +9,11 @@
"lint": "next lint"
},
"dependencies": {
"@chakra-ui/react": "^2.0.2",
"@chakra-ui/react": "^2.1.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@fontsource/open-sans": "^4.5.8",
"@runtipi/common": "^0.2.7",
"axios": "^0.26.1",
"clsx": "^1.1.1",
"final-form": "^4.20.6",
@ -38,11 +39,11 @@
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint-plugin-import": "^2.25.3",
"autoprefixer": "^10.4.4",
"eslint": "8.12.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "12.1.4",
"eslint-plugin-import": "^2.25.3",
"postcss": "^8.4.12",
"tailwindcss": "^3.0.23",
"typescript": "4.6.4"

View File

@ -1,9 +1,9 @@
import React from 'react';
import { FiPauseCircle, FiPlayCircle } from 'react-icons/fi';
import { AppStatus as TAppStatus } from '../../core/types';
import { AppStatusEnum } from '@runtipi/common';
const AppStatus: React.FC<{ status: TAppStatus }> = ({ status }) => {
if (status === 'running') {
const AppStatus: React.FC<{ status: AppStatusEnum }> = ({ status }) => {
if (status === AppStatusEnum.RUNNING) {
return (
<>
<FiPlayCircle className="text-green-500 mr-1" size={20} />

View File

@ -2,7 +2,7 @@ import { Box, SlideFade, Image, useColorModeValue } from '@chakra-ui/react';
import Link from 'next/link';
import React from 'react';
import { FiChevronRight } from 'react-icons/fi';
import { AppConfig } from '../../core/types';
import { AppConfig } from '@runtipi/common';
import AppStatus from './AppStatus';
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {

View File

@ -1,5 +1,5 @@
import validator from 'validator';
import { AppConfig, FieldTypes } from '../../core/types';
import { AppConfig, FieldTypes } from '@runtipi/common';
const validateField = (field: AppConfig['form_fields'][0], value: string): string | undefined => {
if (field.required && !value) {

View File

@ -1,107 +1,22 @@
import validator from 'validator';
// import validator from 'validator';
interface IFormField {
name: string;
type: string;
required: boolean;
description?: string;
placeholder?: string;
validate?: (value: string) => boolean;
}
// interface IFormField {
// name: string;
// type: string;
// required: boolean;
// description?: string;
// placeholder?: string;
// validate?: (value: string) => boolean;
// }
interface IAppConfig {
id: string;
name: string;
description: string;
logo: string;
url: string;
color: string;
install_form: { fields: IFormField[] };
}
// interface IAppConfig {
// id: string;
// name: string;
// description: string;
// logo: string;
// url: string;
// color: string;
// install_form: { fields: IFormField[] };
// }
const APP_ANONADDY: IAppConfig = {
id: 'anonaddy',
name: 'Anonaddy',
description: 'Create Unlimited Email Aliases For Free',
url: 'https://anonaddy.com/',
color: '#00a8ff',
logo: 'https://anonaddy.com/favicon.ico',
install_form: {
fields: [
{
name: 'API Key',
type: 'text',
placeholder: 'API Key',
required: true,
validate: (value: string) => validator.isBase64(value),
},
{
name: 'Return Path',
type: 'text',
description: 'The email address that bounces will be sent to',
placeholder: 'Return Path',
required: false,
validate: (value: string) => validator.isEmail(value),
},
{
name: 'Admin Username',
type: 'text',
description: 'The username of the admin user',
placeholder: 'Admin Username',
required: true,
},
{
name: 'Enable Registration',
type: 'boolean',
description: 'Allow users to register',
placeholder: 'Enable Registration',
required: false,
},
{
name: 'Domain',
type: 'text',
description: 'The domain that will be used for the email address',
placeholder: 'Domain',
required: true,
validate: (value: string) => validator.isFQDN(value),
},
{
name: 'Hostname',
type: 'text',
description: 'The hostname that will be used for the email address',
placeholder: 'Hostname',
required: true,
validate: (value: string) => validator.isFQDN(value),
},
{
name: 'Secret',
type: 'text',
description: 'The secret that will be used for the email address',
placeholder: 'Secret',
required: true,
},
{
name: 'From Name',
type: 'text',
description: 'The name that will be used for the email address',
placeholder: 'From Name',
required: true,
validate: (value: string) => validator.isLength(value, { min: 1, max: 64 }),
},
{
name: 'From Address',
type: 'text',
description: 'The email address that will be used for the email address',
placeholder: 'From Address',
required: true,
validate: (value: string) => validator.isEmail(value),
},
],
},
};
const APPS_CONFIG = {
available: [APP_ANONADDY],
};
export default APPS_CONFIG;
export {};

View File

@ -1,57 +1,9 @@
export enum FieldTypes {
text = 'text',
password = 'password',
email = 'email',
number = 'number',
fqdn = 'fqdn',
ip = 'ip',
fqdnip = 'fqdnip',
url = 'url',
}
interface FormField {
type: FieldTypes;
label: string;
max?: number;
min?: number;
hint?: string;
required?: boolean;
env_variable: string;
}
export interface AppConfig {
id: string;
port: number;
requirements?: {
ports?: number[];
};
name: string;
description: string;
version: string;
image: string;
form_fields: Record<string, FormField>;
short_desc: string;
author: string;
source: string;
installed: boolean;
status: AppStatus;
}
export enum RequestStatus {
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
LOADING = 'LOADING',
}
export enum AppStatus {
RUNNING = 'running',
STOPPED = 'stopped',
INSTALLING = 'installing',
UNINSTALLING = 'uninstalling',
STOPPING = 'stopping',
STARTING = 'starting',
}
export interface IUser {
name: string;
email: string;

View File

@ -1,7 +1,7 @@
import { Button } from '@chakra-ui/react';
import React from 'react';
import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
import { AppConfig, AppStatus } from '../../../core/types';
import { AppConfig, AppStatusEnum } from '@runtipi/common';
interface IProps {
app: AppConfig;
@ -16,7 +16,7 @@ interface IProps {
const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
const hasSettings = Object.keys(app.form_fields).length > 0;
if (app?.installed && app.status === AppStatus.STOPPED) {
if (app?.installed && app.status === AppStatusEnum.STOPPED) {
return (
<div className="flex flex-wrap justify-center">
<Button onClick={onStart} width={160} colorScheme="green" className="mt-3 mr-2">
@ -35,7 +35,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
)}
</div>
);
} else if (app?.installed && app.status === AppStatus.RUNNING) {
} else if (app?.installed && app.status === AppStatusEnum.RUNNING) {
return (
<div>
<Button onClick={onOpen} width={160} colorScheme="gray" className="mt-3 mr-2">
@ -48,7 +48,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
</Button>
</div>
);
} else if (app.status === AppStatus.INSTALLING || app.status === AppStatus.UNINSTALLING || app.status === AppStatus.STARTING || app.status === AppStatus.STOPPING) {
} else if (app.status === AppStatusEnum.INSTALLING || app.status === AppStatusEnum.UNINSTALLING || app.status === AppStatusEnum.STARTING || app.status === AppStatusEnum.STOPPING) {
return (
<div className="flex items-center flex-col md:flex-row">
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">

View File

@ -3,7 +3,7 @@ import React from 'react';
import { Form, Field } from 'react-final-form';
import FormInput from '../../../components/Form/FormInput';
import { validateAppConfig } from '../../../components/Form/validators';
import { AppConfig } from '../../../core/types';
import { AppConfig } from '@runtipi/common';
import { objectKeys } from '../../../utils/typescript';
interface IProps {

View File

@ -1,6 +1,6 @@
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import React from 'react';
import { AppConfig } from '../../../core/types';
import { AppConfig } from '@runtipi/common';
import InstallForm from './InstallForm';
interface IProps {

View File

@ -1,6 +1,6 @@
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import React from 'react';
import { AppConfig } from '../../../core/types';
import { AppConfig } from '@runtipi/common';
interface IProps {
app: AppConfig;

View File

@ -1,6 +1,6 @@
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import React from 'react';
import { AppConfig } from '../../../core/types';
import { AppConfig } from '@runtipi/common';
interface IProps {
app: AppConfig;

View File

@ -2,7 +2,7 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve
import React, { useEffect } from 'react';
import useSWR from 'swr';
import fetcher from '../../../core/fetcher';
import { AppConfig } from '../../../core/types';
import { AppConfig } from '@runtipi/common';
import InstallForm from './InstallForm';
interface IProps {

View File

@ -1,7 +1,7 @@
import { SlideFade, Image, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
import React from 'react';
import { FiExternalLink } from 'react-icons/fi';
import { AppConfig } from '../../../core/types';
import { AppConfig } from '@runtipi/common';
import { useAppsStore } from '../../../state/appsStore';
import { useSytemStore } from '../../../state/systemStore';
import AppActions from '../components/AppActions';
@ -88,7 +88,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
};
const handleOpen = () => {
window.open(`http://${internalIp}:${app.port}`, '_blank');
window.open(`http://${internalIp}:${app.port}`, '_blank', 'noreferrer');
};
return (

View File

@ -7,27 +7,23 @@ import { useAppsStore } from '../../state/appsStore';
import AppTile from '../../components/AppTile';
const Apps: NextPage = () => {
const { available, installed, fetch, status } = useAppsStore((state) => state);
const { fetch, status, apps } = useAppsStore((state) => state);
useEffect(() => {
fetch();
}, [fetch]);
const installedCount: number = installed().length || 0;
const loading = status === RequestStatus.LOADING && !installed && !available;
const installed = apps.filter((app) => app.installed);
const installedCount: number = installed.length || 0;
const loading = status === RequestStatus.LOADING && installedCount === 0;
return (
<Layout loading={loading}>
<Flex className="flex-col">
{installedCount > 0 && <h1 className="font-bold text-3xl mb-5">Your Apps ({installedCount})</h1>}
<SimpleGrid minChildWidth="400px" spacing="20px">
{installed().map((app) => (
<AppTile key={app.name} app={app} />
))}
</SimpleGrid>
{available().length && <h1 className="font-bold text-3xl mb-5 mt-5">Available Apps</h1>}
<SimpleGrid minChildWidth="400px" spacing="20px">
{available().map((app) => (
{installed.map((app) => (
<AppTile key={app.name} app={app} />
))}
</SimpleGrid>

View File

@ -1,13 +1,12 @@
import produce from 'immer';
import create, { GetState, SetState } from 'zustand';
import create, { SetState } from 'zustand';
import api from '../core/api';
import { AppConfig, AppStatus, RequestStatus } from '../core/types';
import { AppConfig, AppStatusEnum } from '@runtipi/common';
import { RequestStatus } from '../core/types';
type AppsStore = {
apps: AppConfig[];
status: RequestStatus;
installed: () => AppConfig[];
available: () => AppConfig[];
fetch: () => void;
getApp: (id: string) => AppConfig | undefined;
fetchApp: (id: string) => void;
@ -19,11 +18,10 @@ type AppsStore = {
};
type Set = SetState<AppsStore>;
type Get = GetState<AppsStore>;
const sortApps = (apps: AppConfig[]) => apps.sort((a, b) => a.name.localeCompare(b.name));
const setAppStatus = (appId: string, status: AppStatus, set: Set) => {
const setAppStatus = (appId: string, status: AppStatusEnum, set: Set) => {
set((state) => {
return produce(state, (draft) => {
const app = draft.apps.find((a) => a.id === appId);
@ -32,11 +30,6 @@ const setAppStatus = (appId: string, status: AppStatus, set: Set) => {
});
};
const installed = (get: Get) => {
const i = get().apps.filter((app) => app.installed);
return i;
};
/**
* Fetch one app and add it to the list of apps.
* @param appId
@ -59,10 +52,6 @@ const fetchApp = async (appId: string, set: Set) => {
export const useAppsStore = create<AppsStore>((set, get) => ({
apps: [],
status: RequestStatus.LOADING,
installed: () => installed(get),
available: () => {
return get().apps.filter((app) => !app.installed);
},
fetchApp: async (appId: string) => fetchApp(appId, set),
fetch: async () => {
set({ status: RequestStatus.LOADING });
@ -72,13 +61,15 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
method: 'get',
});
set({ apps: sortApps(response), status: RequestStatus.SUCCESS });
const apps = sortApps(response);
set({ apps, status: RequestStatus.SUCCESS });
},
getApp: (appId: string) => {
return get().apps.find((app) => app.id === appId);
},
install: async (appId: string, form?: Record<string, string>) => {
setAppStatus(appId, AppStatus.INSTALLING, set);
setAppStatus(appId, AppStatusEnum.INSTALLING, set);
await api.fetch({
endpoint: `/apps/install/${appId}`,
@ -98,7 +89,7 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
await get().fetchApp(appId);
},
uninstall: async (appId: string) => {
setAppStatus(appId, AppStatus.UNINSTALLING, set);
setAppStatus(appId, AppStatusEnum.UNINSTALLING, set);
await api.fetch({
endpoint: `/apps/uninstall/${appId}`,
@ -107,7 +98,7 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
await get().fetchApp(appId);
},
stop: async (appId: string) => {
setAppStatus(appId, AppStatus.STOPPING, set);
setAppStatus(appId, AppStatusEnum.STOPPING, set);
await api.fetch({
endpoint: `/apps/stop/${appId}`,
@ -116,7 +107,7 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
await get().fetchApp(appId);
},
start: async (appId: string) => {
setAppStatus(appId, AppStatus.STARTING, set);
setAppStatus(appId, AppStatusEnum.STARTING, set);
await api.fetch({
endpoint: `/apps/start/${appId}`,

View File

@ -21,6 +21,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@runtipi/common": "^0.2.7",
"argon2": "^0.28.5",
"axios": "^0.26.1",
"compression": "^1.7.4",
@ -57,7 +58,7 @@
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.22.0",
"concurrently": "^7.1.0",
"esbuild": "^0.14.32",
"esbuild": "^0.14.38",
"eslint": "^8.13.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0",

View File

@ -1,41 +1,5 @@
export enum FieldTypes {
text = 'text',
password = 'password',
email = 'email',
number = 'number',
fqdn = 'fqdn',
}
interface FormField {
type: FieldTypes;
label: string;
max?: number;
min?: number;
required?: boolean;
env_variable: string;
}
export type Maybe<T> = T | null | undefined;
export interface AppConfig {
id: string;
available: boolean;
port: number;
name: string;
requirements?: {
ports?: number[];
};
description: string;
version: string;
image: string;
form_fields: Record<string, FormField>;
short_desc: string;
author: string;
source: string;
installed: boolean;
status: 'running' | 'stopped';
}
export interface IUser {
email: string;
name: string;

View File

@ -1,7 +1,7 @@
import AppsService from '../apps.service';
import fs from 'fs';
import config from '../../../config';
import { AppConfig, FieldTypes } from '../../../config/types';
import { AppConfig, FieldTypes } from '@runtipi/common';
import childProcess from 'child_process';
jest.mock('fs');

View File

@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from 'express';
import { AppConfig } from '@runtipi/common';
import AppsService from './apps.service';
import { AppConfig } from '../../config/types';
import { getInitalFormValues } from './apps.helpers';
const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {

View File

@ -1,6 +1,6 @@
import portUsed from 'tcp-port-used';
import p from 'p-iteration';
import { AppConfig } from '../../config/types';
import { AppConfig } from '@runtipi/common';
import { fileExists, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
import InternalIp from 'internal-ip';
import config from '../../config';

View File

@ -1,6 +1,6 @@
import si from 'systeminformation';
import { AppConfig } from '../../config/types';
import { createFolder, fileExists, readJsonFile } from '../fs/fs.helpers';
import { AppConfig, AppStatusEnum } from '@runtipi/common';
import { createFolder, fileExists, readFile, readJsonFile } from '../fs/fs.helpers';
import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
const startApp = async (appName: string): Promise<void> => {
@ -61,7 +61,8 @@ const listApps = async (): Promise<AppConfig[]> => {
apps.forEach((app) => {
app.installed = installed.includes(app.id);
app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as 'running') || 'stopped';
app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
app.description = readFile(`/apps/${app.id}/description.md`);
});
return apps;
@ -74,7 +75,7 @@ const getAppInfo = async (id: string): Promise<AppConfig> => {
const state = getStateFile();
const installed: string[] = state.installed.split(' ').filter(Boolean);
configFile.installed = installed.includes(id);
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as 'running') || 'stopped';
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
return configFile;
};

File diff suppressed because it is too large Load Diff