[READ-ONLY] Mirror of https://github.com/FoxxMD/komodo-import. Import existing compose stacks into Komodo foxxmd.github.io/komodo-import
compose docker import komodo toml
0

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3 from FoxxMD/compose

feat: Re-architect and simplify stacks parsing to support parsed compose project

authored by

Matt Foxx and committed by
GitHub
(Aug 21, 2025, 3:22 PM EDT) dc9d377f 0a689f24

+2832 -743
+6
.devcontainer/compose.mounts.yml
··· 1 + version: '3.7' 2 + services: 3 + app: 4 + #volumes: 5 + # # Add local mounts here 6 + # - /home/myUser/homelab:/filesOnServer
+44
.devcontainer/compose.yml
··· 1 + version: '3.7' 2 + services: 3 + # Update this to the name of the service you want to work with in your docker-compose.yml file 4 + app: 5 + extends: 6 + file: compose.mounts.yml 7 + service: app 8 + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 9 + # folder. Note that the path of the Dockerfile and context is relative to the *primary* 10 + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" 11 + # array). The sample below assumes your primary file is in the root of your project. 12 + # 13 + # build: 14 + # context: . 15 + # dockerfile: .devcontainer/Dockerfile 16 + 17 + image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm 18 + 19 + volumes: 20 + # Update this to wherever you want VS Code to mount the folder of your project 21 + - ..:/workspaces:cached 22 + command: sleep infinity 23 + 24 + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 25 + # cap_add: 26 + # - SYS_PTRACE 27 + # security_opt: 28 + # - seccomp:unconfined 29 + 30 + # Overrides default command so things don't shut down after the process ends. 31 + #command: sleep infinity 32 + socket-proxy: 33 + image: lscr.io/linuxserver/socket-proxy:latest 34 + environment: 35 + - CONTAINERS=1 36 + - INFO=1 37 + - POST=0 38 + - PING=1 39 + - VERSION=1 40 + volumes: 41 + - /var/run/docker.sock:/var/run/docker.sock:ro 42 + read_only: true 43 + tmpfs: 44 + - /run
+7 -1
.devcontainer/devcontainer.json
··· 3 3 { 4 4 "name": "Node.js", 5 5 // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 - "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm", 6 + "dockerComposeFile": [ 7 + "compose.yml" 8 + ], 9 + 10 + "service": "app", 11 + 12 + "workspaceFolder": "/workspaces", 7 13 8 14 // Features to add to the dev container. More info: https://containers.dev/features. 9 15 // "features": {},
+3 -1
.gitignore
··· 137 137 .github/act/out 138 138 tmp-* 139 139 140 - config/*.toml 140 + config/*.toml 141 + 142 + .devcontainer/compose.mounts.yml
+1 -1
Dockerfile
··· 8 8 NODE_ENV="production" \ 9 9 IS_DOCKER=true \ 10 10 COLORED_STD=true \ 11 - FILES_ON_SERVER_DIR=/filesOnServer 11 + MOUNT_DIR=/filesOnServer 12 12 13 13 FROM base as builder 14 14
+7 -12
compose.yaml
··· 2 2 komodo-import: 3 3 image: foxxmd/komodo-import:latest 4 4 volumes: 5 - ## ParentDirectory:FILES_ON_SERVER_DIR 6 - - ./myprojects:/filesOnServer 7 - 8 - ## optional, TOML is saved to file here when OUTPUT_DIR env is present 9 - #- ./output:/output 5 + ## Mount the parent directory where all subfolders are compose projects 6 + - /home/myUser/homelab:/filesOnServer 10 7 environment: 11 - - TZ=America/New_York 8 + ## Same as host directory mounted above 9 + - HOST_DIR=/home/myUser/homelab 10 + ## where stacks are generated from. 'dir' or 'compose' 11 + - STACKS_FROM=dir 12 12 ## Komodo Server name to use for generated Stacks 13 13 - SERVER_NAME=my-cool-server 14 - ## ParentDirectory on the host use as Stack Run Directory prefix 15 - - HOST_PARENT_PATH=/my/cool/path 16 14 17 15 ## optional 18 - #- FILES_ON_SERVER_DIR=/filesOnServer 16 + #- TZ=America/New_York 19 17 #- IMAGE_REGISTRY_PROVIDER= 20 18 #- IMAGE_REGISTRY_ACCOUNT= 21 19 #- AUTO_UPDATE=false ··· 23 21 #- KOMODO_ENV_NAME= 24 22 #- COMPOSE_FILE_GLOB= 25 23 #- ENV_FILE_GLOB= 26 - 27 - ## only saves to file if this ENV is present 28 - #- OUTPUT_DIR=/output 29 24 restart: no
+15
docsite/docs/help.mdx
··· 1 + --- 2 + title: 'Help and FAQ' 3 + --- 4 + 5 + ## Where To Start? 6 + 7 + If you are overwhelmed by the docs or not sure where to begin you should try out the [**Quickstart** guide](../quickstart). It uses step-by-step questions with interactive fields/buttons to help customize Komodo Import for your machine. 8 + 9 + ## Further Help with Komodo Import 10 + 11 + If the [**Quickstart** guide](../quickstart) and [Usage docs](../usage/overview) do not cover what you want to do with Komodo Import or you do not fully understand how to use it please search the repository's [Github Discussions](https://github.com/FoxxMD/komodo-import/discussions) and if your question is not answered then [open a New Discussion](https://github.com/FoxxMD/komodo-import/discussions/new/choose). 12 + 13 + ## Found a Bug or Error? 14 + 15 + If Komodo Import is not behaving as you expect it to or is outputting errors that break functionality please search [Github Issues](https://github.com/FoxxMD/komodo-import/issues) and [open a New Issue](https://github.com/FoxxMD/komodo-import/issues/new) if you cannot find a solution to your problem.
+22 -157
docsite/docs/installation/installation.mdx
··· 7 7 import TabItem from '@theme/TabItem'; 8 8 import CodeBlock from '@theme/CodeBlock'; 9 9 import ComposeStack from '!!raw-loader!../../../compose.yaml'; 10 + import ConsoleOutput from '../../src/components/QuickStartSnippets/consoleOutput.mdx' 11 + import TreeDirectory from '../../src/components/snippets/_directoryTree.mdx' 10 12 11 13 ## Docker 12 14 ··· 45 47 46 48 :::tip 47 49 48 - See the [**Quick Start Guide**](../quickstart.mdx) for another guided docker-compose example 50 + Use the [**Quick Start Guide**](../quickstart.mdx) for an interactive setup! 49 51 50 52 ::: 51 53 ··· 54 56 * You have [Komodo](https://komo.do/) already setup 55 57 * You have installed [Periphery](https://komo.do/docs/connect-servers) on a machine 56 58 * The Komodo Server name for this machine is `my-cool-server` 57 - * The machine is currently using dockge and has all of its compose projects located at `/home/myUser/homelab` like... 58 - * `/home/myUser/homelab/immich` 59 - * `/home/myUser/homelab/plex` 60 - * etc... 59 + * The machine is currently using dockge and its projects directory looks like this: 61 60 62 - <Tabs groupId="runType" queryString> 63 - <TabItem value="docker" label="Docker"> 64 - ```bash 65 - docker run --name komodo-import -e "SERVER_NAME=my-cool-server" -e "HOST_PARENT_PATH=/home/myUser/homelab" -v /home/myUser/homelab:/filesOnServer foxxmd/komodo-import 66 - ``` 67 - </TabItem> 68 - <TabItem value="docker-compose" label="Docker Compose"> 69 - See [`docker-compose.yml`](#docker) sample above for more options and annotations. 61 + <TreeDirectory/> 70 62 71 - ```yaml title="docker-compose.yml" 72 - services: 73 - komodo-import: 74 - image: foxxmd/komodo-import:latest 75 - volumes: 76 - ## ParentDirectory:FILES_ON_SERVER_DIR 77 - - /home/myUser/homelab:/filesOnServer:ro 78 - environment: 79 - - TZ=America/New_York 80 - ## Komodo Server name to use for generated Stacks 81 - - SERVER_NAME=my-cool-server 82 - ## ParentDirectory on the host use as Stack Run Directory prefix 83 - - HOST_PARENT_PATH=/home/myUser/homelab 84 - restart: no 85 - ``` 86 - ```shell 87 - docker compose up --no-log-prefix 88 - ``` 89 - </TabItem> 90 - </Tabs> 91 - 92 - <details> 93 - 94 - <summary>Output</summary> 95 - 96 - ```shell 97 - [2025-08-11 14:06:19.080 -0400] INFO : [Init] Debug Mode: NO 98 - [2025-08-11 14:06:19.094 -0400] INFO : [App] Version: 0.1.0 99 - [2025-08-11 14:06:19.095 -0400] INFO : [App] Files On Server Dir ENV: /filesOnServer -> Resolved: /filesOnServer 100 - [2025-08-11 14:06:19.096 -0400] INFO : [App] [Files On Server] Processing Stacks for 6 folders: 101 - /filesOnServer/compose.yaml 102 - /filesOnServer/immich 103 - /filesOnServer/jellyfin 104 - /filesOnServer/octoprint 105 - /filesOnServer/plex 106 - /filesOnServer/uptime-kuma 107 - [2025-08-11 14:06:19.096 -0400] INFO : [App] [Files On Server] Compose File Glob: **/{compose,docker-compose}*.y?(a)ml 108 - [2025-08-11 14:06:19.097 -0400] INFO : [App] [Files On Server] Env Glob: **/.env 109 - [2025-08-11 14:06:19.097 -0400] INFO : [App] [Files On Server] [compose] Found Stack 'compose' at dir /filesOnServer/compose.yaml 110 - [2025-08-11 14:06:19.103 -0400] WARN : [App] [Files On Server] [compose] Did not find any files patterns matching compose glob 111 - [2025-08-11 14:06:19.103 -0400] INFO : [App] [Files On Server] [compose] Stack config complete 112 - [2025-08-11 14:06:19.104 -0400] INFO : [App] [Files On Server] [immich] Found Stack 'immich' at dir /filesOnServer/immich 113 - [2025-08-11 14:06:19.107 -0400] INFO : [App] [Files On Server] [immich] Found 1 files matching compose glob: 114 - docker/docker-compose.yaml 115 - [2025-08-11 14:06:19.107 -0400] INFO : [App] [Files On Server] [immich] Using file(s): docker/docker-compose.yaml 116 - [2025-08-11 14:06:19.108 -0400] INFO : [App] [Files On Server] [immich] Stack config complete 117 - [2025-08-11 14:06:19.108 -0400] INFO : [App] [Files On Server] [jellyfin] Found Stack 'jellyfin' at dir /filesOnServer/jellyfin 118 - [2025-08-11 14:06:19.109 -0400] INFO : [App] [Files On Server] [jellyfin] Found 3 files matching compose glob: 119 - compose.yaml 120 - docker-compose.yaml 121 - docker/docker-compose.yaml 122 - [2025-08-11 14:06:19.110 -0400] INFO : [App] [Files On Server] [jellyfin] Using file: compose.yaml but not writing to file_paths since this is the Komodo default 123 - [2025-08-11 14:06:19.111 -0400] INFO : [App] [Files On Server] [jellyfin] Found 1 env files matching pattern **/.env: 124 - .env 125 - [2025-08-11 14:06:19.111 -0400] INFO : [App] [Files On Server] [jellyfin] Using .komodoEnv for Komodo-written env file 126 - [2025-08-11 14:06:19.111 -0400] INFO : [App] [Files On Server] [jellyfin] Stack config complete 127 - [2025-08-11 14:06:19.111 -0400] INFO : [App] [Files On Server] [octoprint] Found Stack 'octoprint' at dir /filesOnServer/octoprint 128 - [2025-08-11 14:06:19.112 -0400] WARN : [App] [Files On Server] [octoprint] Did not find any files patterns matching compose glob 129 - [2025-08-11 14:06:19.112 -0400] INFO : [App] [Files On Server] [octoprint] Stack config complete 130 - [2025-08-11 14:06:19.113 -0400] INFO : [App] [Files On Server] [plex] Found Stack 'plex' at dir /filesOnServer/plex 131 - [2025-08-11 14:06:19.114 -0400] INFO : [App] [Files On Server] [plex] Found 1 files matching compose glob: 132 - compose.yaml 133 - [2025-08-11 14:06:19.114 -0400] INFO : [App] [Files On Server] [plex] Using file: compose.yaml but not writing to file_paths since this is the Komodo default 134 - [2025-08-11 14:06:19.115 -0400] INFO : [App] [Files On Server] [plex] Stack config complete 135 - [2025-08-11 14:06:19.115 -0400] INFO : [App] [Files On Server] [uptime-kuma] Found Stack 'uptime-kuma' at dir /filesOnServer/uptime-kuma 136 - [2025-08-11 14:06:19.116 -0400] INFO : [App] [Files On Server] [uptime-kuma] Found 1 files matching compose glob: 137 - compose.yaml 138 - [2025-08-11 14:06:19.116 -0400] INFO : [App] [Files On Server] [uptime-kuma] Using file: compose.yaml but not writing to file_paths since this is the Komodo default 139 - [2025-08-11 14:06:19.117 -0400] INFO : [App] [Files On Server] [uptime-kuma] Found 1 env files matching pattern **/.env: 140 - .env 141 - [2025-08-11 14:06:19.117 -0400] INFO : [App] [Files On Server] [uptime-kuma] Using .komodoEnv for Komodo-written env file 142 - [2025-08-11 14:06:19.117 -0400] INFO : [App] [Files On Server] [uptime-kuma] Stack config complete 143 - [2025-08-11 14:06:19.118 -0400] INFO : [App] [Files On Server] Built Stack configs for 6 folders 144 - [2025-08-11 14:06:19.119 -0400] INFO : [App] Copy the text between the scissors to use as the *Resource File* contents within your Resource Sync 145 - 146 - ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ 147 - [[stack]] 148 - name = "compose" 149 - 150 - [stack.config] 151 - server = "my-cool-server" 152 - run_directory = "/home/myUser/homelab/compose" 153 - files_on_host = true 154 - auto_update = false 155 - poll_for_updates = false 156 - 157 - [[stack]] 158 - name = "immich" 159 - 160 - [stack.config] 161 - server = "my-cool-server" 162 - run_directory = "/home/myUser/homelab/immich" 163 - files_on_host = true 164 - auto_update = false 165 - poll_for_updates = false 166 - file_paths = [ "docker/docker-compose.yaml" ] 167 - 168 - [[stack]] 169 - name = "jellyfin" 170 - 171 - [stack.config] 172 - server = "my-cool-server" 173 - run_directory = "/home/myUser/homelab/jellyfin" 174 - files_on_host = true 175 - auto_update = false 176 - poll_for_updates = false 177 - env_file_path = ".komodoEnv" 178 - additional_env_files = [ ".env" ] 179 - 180 - [[stack]] 181 - name = "octoprint" 182 - 183 - [stack.config] 184 - server = "my-cool-server" 185 - run_directory = "/home/myUser/homelab/octoprint" 186 - files_on_host = true 187 - auto_update = false 188 - poll_for_updates = false 189 - 190 - [[stack]] 191 - name = "plex" 192 - 193 - [stack.config] 194 - server = "my-cool-server" 195 - run_directory = "/home/myUser/homelab/plex" 196 - files_on_host = true 197 - auto_update = false 198 - poll_for_updates = false 199 - 200 - [[stack]] 201 - name = "uptime-kuma" 202 - 203 - [stack.config] 204 - server = "my-cool-server" 205 - run_directory = "/home/myUser/homelab/uptime-kuma" 206 - files_on_host = true 207 - auto_update = false 208 - poll_for_updates = false 209 - env_file_path = ".komodoEnv" 210 - additional_env_files = [ ".env" ] 211 - ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ 212 - [2025-08-11 14:06:19.119 -0400] INFO : [App] Done! 63 + ```yaml title="docker-compose.yml" 64 + services: 65 + komodo-import: 66 + image: foxxmd/komodo-import:latest 67 + volumes: 68 + - /home/myUser/homelab:/filesOnServer:ro 69 + environment: 70 + - TZ=America/New_York 71 + ## Komodo Server name to use for generated Stacks 72 + - SERVER_NAME=my-cool-server 73 + ## ParentDirectory on the host use as Stack Run Directory prefix 74 + - HOST_DIR=/home/myUser/homelab 75 + ## generate stacks from mounted directory subfolders 76 + - STACKS_FROM=dir 77 + restart: no 213 78 ``` 214 79 215 - </details> 80 + <ConsoleOutput/> 216 81 217 82 ## Local Installation 218 83 ··· 232 97 #### Usage Examples 233 98 234 99 ```shell 235 - SERVER_NAME=my-server HOST_PARENT_PATH=/my/parent/folder node src/index.js 100 + SERVER_NAME=my-cool-server STACKS_FROM=dir MOUNT_DIR=/my/parent/folder node src/index.js 236 101 ```
+23 -150
docsite/docs/quickstart.mdx
··· 5 5 6 6 import Tabs from '@theme/Tabs'; 7 7 import TabItem from '@theme/TabItem'; 8 + import QuickstartCompose from "../src/components/QuickstartCompose"; 8 9 9 - ## Scenario 10 + ## Prerequisites 10 11 11 12 * You have Komodo already setup 12 13 * You have installed [Periphery](https://komo.do/docs/connect-servers) on a machine 13 14 * The Komodo [Server](https://komo.do/docs/resources#server) name is `my-cool-server` 14 - * The machine is currently using dockge and has all of its compose projects located in one folder like this: 15 15 16 - ``` 17 - . 18 - └── /home/myUser/homelab/ 19 - ├── immich/ 20 - │ └── compose.yaml 21 - ├── plex/ 22 - │ └── docker-compose.yaml 23 - └── uptime-kuma/ 24 - └── compose.yaml 25 - ``` 26 - 27 - You want to import all of the existing compose projects into Komodo as [Files On Server](../usage/overview#files-on-server) [Stacks](https://komo.do/docs/resources#stack) attached to `my-cool-server`. 16 + ## Create Your Scenario 28 17 29 - :::tip 30 - 31 - Using git repositories instead of plain folders? Check out the [Git Repo Stack docs](../usage/overview#git-repo) 32 - 33 - ::: 18 + **Follow the directions below, select your options, and fill in all fields to generate a customized Docker Compose file for your specific scenario.** 34 19 35 - ## Create Docker Compose File 20 + <QuickstartCompose/> 36 21 37 - Use the [example `compose.yaml` file from the repository](https://github.com/FoxxMD/komodo-import/blob/main/compose.yaml) as a starting point. 22 + ## Generate Stacks 38 23 39 - * Bind `/home/myUser/homelab/` into the container at `/filesOnServer` so Komodo Import can scan the parent folder (this is the same for plain folder and git repo stack methods) 40 - * Define `HOST_PARENT_PATH=/home/myUser/homelab` so Komodo Import knows how to create Run Directory in a Komodo Stack correctly 24 + Run the customized `compose.yaml` from above with `docker compose` to generate Komodo Stacks from existing projects on your machine! 41 25 42 - ```yaml 43 - services: 44 - komodo-import: 45 - image: foxxmd/komodo-import:latest 46 - volumes: 47 - ## ParentDirectory:FILES_ON_SERVER_DIR 48 - - /home/myUser/homelab:/filesOnServer 49 - 50 - ## optional, TOML is saved to file here when OUTPUT_DIR env is present 51 - #- ./output:/output 52 - environment: 53 - - TZ=America/New_York 54 - ## Komodo Server name to use for generated Stacks 55 - - SERVER_NAME=my-cool-server 56 - ## ParentDirectory on the host use as Stack Run Directory prefix 57 - - HOST_PARENT_PATH=/home/myUser/homelab 58 - restart: no 26 + ```bash 27 + # in the same directory as compose.yaml... 28 + docker compose up --no-log-prefix 59 29 ``` 60 30 61 - ## Choose Desired Output 31 + How the Stacks will be generated is determined by what you chose in **Step 4**. Refer to the expandable **Examples** in that step. 62 32 63 - <Tabs groupId="output" queryString> 64 - <TabItem value="console" label="Console"> 65 - This [Output](../usage/overview#outputs) renders the Sync Resource TOML to docker logs. Run komodo-import with `--no-log-prefix` to get the generated TOML output for a Sync Resource. 33 + ## Next Steps 66 34 67 - ```shell 68 - docker compose up --no-log-prefix 69 - ``` 70 - ``` 71 - [2025-08-11 14:06:19.080 -0400] INFO : [Init] Debug Mode: NO 72 - [2025-08-11 14:06:19.094 -0400] INFO : [App] Version: 0.1.0 73 - [2025-08-11 14:06:19.095 -0400] INFO : [App] Files On Server Dir ENV: /filesOnServer -> Resolved: /filesOnServer 74 - [2025-08-11 14:06:19.096 -0400] INFO : [App] [Files On Server] Processing Stacks for 2 folders: 75 - /filesOnServer/immich 76 - /filesOnServer/plex 77 - /filesOnServer/uptime-kuma 78 - [2025-08-11 14:06:19.096 -0400] INFO : [App] [Files On Server] Compose File Glob: **/{compose,docker-compose}*.y?(a)ml 79 - [2025-08-11 14:06:19.097 -0400] INFO : [App] [Files On Server] Env Glob: **/.env 80 - [2025-08-11 14:06:19.097 -0400] INFO : [App] [Files On Server] [compose] Found Stack 'compose' at dir /filesOnServer/compose.yaml 81 - [2025-08-11 14:06:19.103 -0400] WARN : [App] [Files On Server] [compose] Did not find any files patterns matching compose glob 82 - [2025-08-11 14:06:19.103 -0400] INFO : [App] [Files On Server] [compose] Stack config complete 35 + There are many more options (environmental variables) that can be used to customize how Stacks are generated, all found in [Usage](../usage/overview#stacks) docs. Some customizations that might interest you: 83 36 84 - ... 85 - 86 - [2025-08-11 14:06:19.118 -0400] INFO : [App] [Files On Server] Built Stack configs for 3 folders 87 - [2025-08-11 14:06:19.119 -0400] INFO : [App] Copy the text between the scissors to use as the *Resource File* contents within your Resource Sync 37 + * [Common Stack options](../usage/overview#stack-configuration) 38 + * Set image registry provider/account 39 + * Set defaults for polling for new images (`POLL_FOR_UPDATE`) and auto updating (`AUTO_UPDATE`) 40 + * Set what compose files (`COMPOSE_FILE_GLOB`) and .env files (`ENV_FILE_GLOB`) to include, per Stack 41 + * For **Directory** generated stacks... 42 + * Include (`FOLDER_GLOB`) or exclude (`FOLDER_IGNORE_GLOB`) folders based on patterns 43 + * Configure Komodo Import to use the [Komodo API](../usage/overview#komodo-api) for better integration 44 + * Uses existing Linked Repos for Git Based Stacks 45 + * Automatically configures Git Based Stacks to use existing, private Git Provider if git URL domains match 88 46 89 - ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ 90 - 91 - [[stack]] 92 - name = "immich" 93 - 94 - [stack.config] 95 - server = "my-cool-server" 96 - run_directory = "/home/myUser/homelab/immich" 97 - files_on_host = true 98 - auto_update = false 99 - poll_for_updates = false 100 - file_paths = [ "docker/docker-compose.yaml" ] 101 - 102 - [[stack]] 103 - name = "plex" 104 - 105 - [stack.config] 106 - server = "my-cool-server" 107 - run_directory = "/home/myUser/homelab/plex" 108 - files_on_host = true 109 - auto_update = false 110 - poll_for_updates = false 111 - 112 - [[stack]] 113 - name = "uptime-kuma" 114 - 115 - [stack.config] 116 - server = "my-cool-server" 117 - run_directory = "/home/myUser/homelab/uptime-kuma" 118 - files_on_host = true 119 - auto_update = false 120 - poll_for_updates = false 121 - env_file_path = ".komodoEnv" 122 - additional_env_files = [ ".env" ] 123 - ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ 124 - [2025-08-11 14:06:19.119 -0400] INFO : [App] Done! 125 - ``` 126 - 127 - Now, [manually create a Sync Resource](../usage/resourceSync#create-sync-resource) and Execute to import your projects. 128 - 129 - Or, use [Sync Api](./?output=sync-api#choose-desired-output) output instead to directly write the Sync Resource to Komodo. 130 - </TabItem> 131 - <TabItem value="sync-api" label="Sync API"> 132 - 133 - This [Output](../usage/overview#outputs) writes the Sync Resource directly to your Komodo instance (but does not execute it!) 134 - 135 - Create an [API Key and Secret](../usage/overview#api-sync) in Komodo, then add it to your Komodo Import `compose.yaml`: 136 - 137 - ```yaml 138 - services: 139 - komodo-import: 140 - image: foxxmd/komodo-import:latest 141 - volumes: 142 - ## ParentDirectory:FILES_ON_SERVER_DIR 143 - - /home/myUser/homelab:/filesOnServer 144 - 145 - ## optional, TOML is saved to file here when OUTPUT_DIR env is present 146 - #- ./output:/output 147 - environment: 148 - - TZ=America/New_York 149 - ## Komodo Server name to use for generated Stacks 150 - - SERVER_NAME=my-cool-server 151 - ## ParentDirectory on the host use as Stack Run Directory prefix 152 - - HOST_PARENT_PATH=/home/myUser/homelab 153 - 154 - - KOMODO_URL=http://192.168.0.101:9120 155 - - API_KEY=K-3A6btIPZYeBuD5ebSa9uD5ebuD5ebHIjYxT5sc 156 - - API_SECRET=S-qnaXD1frutYlfC2ZYlfCSzqiYlfC1WYlfCVR34Yj4 157 - restart: no 158 - ``` 159 - 160 - Run komodo-import to create the Sync Resource 161 - 162 - ```shell 163 - $ docker compose up --no-log-prefix 164 - ... 165 - [2025-08-11 17:46:54.030 -0400] INFO : [App] [Sync API] Using 'komodo-import' as Sync Name 166 - [2025-08-11 17:46:54.032 -0400] VERBOSE: [App] [Sync API] KOMODO_URL: http://192.168.0.101:9120 | Normalized: http://192.168.0.101:9120/ 167 - [2025-08-11 17:46:54.411 -0400] INFO : [App] [Sync API] Resource Sync created. 168 - [2025-08-11 17:46:54.411 -0400] INFO : [App] [Sync API] Resource Sync URL: http://192.168.0.101:9120/resource-syncs/689a41a04ed1b642f3053f57 169 - ``` 170 - 171 - Then continue to [verifying the Sync Resource](../usage/resourceSync#verify-sync-changes) in Komodo and Executing it to import your projects. 172 - 173 - </TabItem> 174 - </Tabs> 47 + Additionally, review the [Sync Resource](../usage/resourceSync) guide to learn how to use them for creating Stacks in Komodo.
+254 -164
docsite/docs/usage/overview.mdx
··· 8 8 import CodeBlock from '@theme/CodeBlock'; 9 9 import AIOExample from "../../src/components/AIOExample"; 10 10 import FileExample from "../../src/components/FileExample"; 11 + import TreeDirectory from '../../src/components/snippets/_directoryTree.mdx' 12 + import TreeCompose from '../../src/components/snippets/_composeTree.mdx' 13 + import ComposeExample from '../../src/components/snippets/_composeExample.mdx' 11 14 12 15 ## Key Concepts 13 16 ··· 34 37 These are where and how KI puts the Resources created from [Inputs](#inputs): 35 38 36 39 * Log to output (docker logs, node console) 37 - * Log to file 38 40 * Export to Komodo API (create Resource Sync, etc.) 39 41 40 42 ## Inputs ··· 43 45 44 46 KI can create Komodo [Stack](https://komo.do/docs/resources#stack) Resources from existing resources on your machine. 45 47 46 - ##### Common Stack Configuration 48 + It will automatically detect what type of Stack to create based on the presence of git folders/data: 49 + 50 + * Komodo **Files On Server** Stack => compose project/folder without a `.git` folder 51 + * Komodo **Git Repo** Stacks => compose project/folder with a `.git` folder or with a parent folder containing a `.git` folder 52 + 53 + #### Stack Sources 54 + 55 + KI can generate Stacks from existing projects on your machine using one of two methods: 56 + 57 + <Tabs groupId="stacksFrom" queryString> 58 + <TabItem value="dir" label="Directory"> 59 + For **Directory** (`STACKS_FROM=dir`), Komodo Import will try to generate Stacks from each immediate **subfolder** inside the directory mounted into the container. 60 + 61 + 62 + <details> 63 + 64 + <summary>Example</summary> 65 + 66 + <TreeDirectory/> 67 + 68 + Using `STACKS_FROM=dir` with [host directory](#host-directory) `/home/myUser/homelab` would produce Stacks for `immich` `plex` and `uptime-kuma`. 69 + 70 + </details> 71 + 72 + </TabItem> 73 + <TabItem value="compose" label="Compose Projects"> 74 + For **Compose Projects** (`STACKS_FROM=compose`), Komodo Import will try to generate Stacks from existing projects on your machine that were created with `docker compose up`. 75 + 76 + <details> 77 + 78 + <summary>Example</summary> 47 79 48 - These environmental variables are applicable to all Stack types below. 80 + <ComposeExample/> 49 81 50 - <details> 82 + * Using `STACKS_FROM=dir` with [host directory](#host-directory) `/home/myUser` would produce Stacks for `immich` `plex` and `project1`. 83 + * It **would not** generate a Stack for `owncloud` since it was not created with `docker compose up` yet. 84 + 85 + </details> 86 + 87 + To use **Compose Projects** you must give Komodo Import access to Docker. This can be done by mounting `docker.sock` into the container or connecting it to a proxy service like [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy). 88 + 89 + <details> 90 + 91 + <summary>Using `socket-proxy` (recommended)</summary> 92 + 93 + Add a socket-proxy service to your compose file and set the env `DOCKER_HOST` so Komodo Import knows how to connect to Docker: 94 + 95 + ```yaml title="compose.yaml" 96 + services: 97 + komodo-import: 98 + # ... 99 + environment: 100 + # ... 101 + # add to existing environment 102 + - DOCKER_HOST=tcp://socket-proxy:2375 103 + socket-proxy: 104 + image: lscr.io/linuxserver/socket-proxy:latest 105 + environment: 106 + - CONTAINERS=1 107 + - INFO=1 108 + - POST=0 109 + - PING=1 110 + - VERSION=1 111 + volumes: 112 + - /var/run/docker.sock:/var/run/docker.sock:ro 113 + read_only: true 114 + tmpfs: 115 + - /run 116 + ``` 117 + </details> 51 118 52 - <summary>Environmental Variables</summary> 119 + <details> 53 120 54 - | ENV | Required | Default | Description | 55 - | :------------------------ | :------- | -------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | 56 - | `SERVER_NAME` | ☑️ | | The name of the Komodo [Server](https://komo.do/docs/resources#server) where a Stack is located | 57 - | `HOST_PARENT_PATH` | ☑️ | | The parent directory on the **host** where stack folders are located. | 58 - | `WRITE_ENV` | - | `false` | Write the contents of found .env files to Komodo **Environment**. Likely want this as `true`. | 59 - | `FOLDER_GLOB` | - | `*` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for finding folders in the top-level folder given to Komodo Import | 60 - | `FOLDER_IGNORE_GLOB` | - | | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use to ignore folders in the top-level folder given to Komodo Import | 61 - | `COMPOSE_FILE_GLOB` | - | `**/{compose,docker-compose}*.y?(a)ml` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for finding files for **Files Paths** in Stack config | 62 - | `ENV_FILE_GLOB` | - | `**/.env` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for finding files for **Additional Env Files** in Stack config | 63 - | `KOMODO_ENV_NAME` | - | `.komodoenv` | If existing .env files are found, and `WRITE_ENV=false`, then this name will be used for the .env that Komodo writes using its own Environment section | 64 - | `IMAGE_REGISTRY_PROVIDER` | - | | Name of Image Registry to use | 65 - | `IMAGE_REGISTRY_ACCOUNT` | - | | Image Registry account to use | 66 - | `POLL_FOR_UPDATE` | - | | Poll for newer images? | 67 - | `AUTO_UPDATE` | - | | Auto Update stack? | 121 + <summary>Mounting `docker.sock`</summary> 68 122 69 - </details> 123 + Mount `/var/run/docker.sock` as a volume into the container: 70 124 71 - #### Files On Server 125 + ```yaml title="compose.yaml" 126 + services: 127 + komodo-import: 128 + # ... 129 + volumes: 130 + # ... 131 + # add to existing volumes 132 + - /var/run/docker.sock:/var/run/docker.sock:ro 133 + ``` 134 + </details> 135 + </TabItem> 136 + </Tabs> 72 137 73 - Use existing, non git-based [Docker Compose projects](https://docs.docker.com/compose/how-tos/project-name/) (`compose.yaml`) as **Files On Server** mode Komodo Stacks. 138 + #### Host Directory 74 139 75 - KI will generate multiple Stacks from all folders within the given folder. This is similar to how [dockge](https://github.com/louislam/dockge) works. 140 + The directory on the **Host** machine that should be mounted into the Komodo Import container depends on which [Stack Source](#stack-sources) you are using. 76 141 77 - <details> 142 + * The host directory should be mounted to `/filesOnServer` in the container. 143 + * **The host directory should also be set for `HOST_DIR` env.** 78 144 79 - <summary>Example</summary> 145 + <Tabs groupId="stacksFrom" queryString> 146 + <TabItem value="dir" label="Directory"> 147 + For **Directory** (`STACKS_FROM=dir`) use the **parent folder** containing all of the immediate **subfolders** that should be generated as Stacks. 80 148 81 - If your folder structure looks like this 149 + <details> 82 150 83 - ``` 84 - . 85 - └── /home/myUser/homelab/ 86 - ├── immich/ 87 - │ └── compose.yaml 88 - ├── plex/ 89 - │ └── docker-compose.yaml 90 - └── uptime-kuma/ 91 - └── compose.yaml 92 - ``` 151 + <summary>Example</summary> 93 152 94 - and you use `/home/myUser/homelab/` with KI then it will output stacks for 153 + <TreeDirectory/> 95 154 96 - * **immich** with Run Directory `/home/myUser/homelab/immich` 97 - * **plex** with Run Directory `/home/myUser/homelab/plex` 98 - * **uptime-kuma** with Run Directory `/home/myUser/homelab/uptime-kuma` 155 + Directory should be `home/myUser/homelab`: 99 156 100 - </details> 157 + ```yaml title="compose.yaml" 158 + services: 159 + komodo-import: 160 + # ... 161 + volumes: 162 + - /home/myUser/homelab:/filesOnServer 163 + environment: 164 + - HOST_DIR=/home/myUser/homelab 165 + ``` 166 + </details> 101 167 102 - ###### Project Files Detection {#project-file-detection} 168 + </TabItem> 169 + <TabItem value="compose" label="Compose Projects"> 170 + For **Compose Projects** (`STACKS_FROM=compose`) use the closest **parent folder** that contains all discovered compose project Config Files (`docker compose ls`). 103 171 104 - Komodo Import makes some reasonable assumptions about which `compose.yaml` and `.env` files you want to use within each project's folder. These can be overriden using the `*_GLOB` environmental varibales from the [Configuration](#fos-config) below. 172 + <details> 105 173 106 - <details> 174 + <summary>Example</summary> 107 175 108 - <summary>File Detection and Behavior Details</summary> 176 + <TreeCompose/> 109 177 110 - * Compose Files 111 - * Default pattern 112 - * Looks for `compose.y(a)ml` and `docker-compose.y(a)ml` 113 - * Files may have interim names like `compose.dev.yaml` 114 - * Can be overridden with `COMPOSE_FILE_GLOB` env 115 - * Will prioritize `compose.yaml` over `docker-compose.yaml` 116 - * Will choose the file with the shortest path EX 117 - * Prioritizes `./compose.yaml` over `./aFolder/compose.yaml` 118 - * .env Files 119 - * Default pattern 120 - * Adds all `.env` files found at top-level or sub-folders 121 - * Can be overridden with `ENV_FILE_GLOB` env 122 - * If a `.env` file is found then configures Komodo to write it's own Environment section to `.komodoenv` instead of `.env` 123 - * Komodo env name can be overridden with `KOMODO_ENV_NAME` 178 + Directory should be `/home/myUser` because projects are found under both `localDev` and `homeLab`: 124 179 125 - Komodo Import will **log** which files it detects and indicate which ones it will use for each project. These can be manually modified in the generated TOML, [API Sync Resource](#api-sync), or after creating the Stack in Komodo. 180 + ```yaml itle="compose.yaml" 181 + services: 182 + komodo-import: 183 + # ... 184 + volumes: 185 + - /home/myUser:/filesOnServer 186 + environment: 187 + - HOST_DIR=/home/myUser 188 + ``` 189 + </details> 190 + </TabItem> 191 + </Tabs> 126 192 127 - </details> 193 + #### Stack Configuration 128 194 129 - ##### Configuration {#fos-config} 195 + The configuration and behaviors below are applicable to all Stacks: 130 196 131 197 <details> 132 198 133 199 <summary>Environmental Variables</summary> 134 200 135 - Use the [**Common Stack Configuration environmental variables**](#common-stack-configuration) 201 + | ENV | Required | Default | Description | 202 + | :------------------------ | :------- | -------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | 203 + | `SERVER_NAME` | ☑️ | | The name of the Komodo [Server](https://komo.do/docs/resources#server) where a Stack is located | 204 + | `STACKS_FROM` | ☑️ | `dir` | What [**Stack Sources**](#stack-sources) to generate Stacks from? `dir` or `compose` | 205 + | `HOST_DIR` | ☑️ | | The parent directory on the **host** that mounted into the Komodo Import container | 206 + | `WRITE_ENV` | - | `true` | Write the contents of found .env files to Komodo **Environment**. Likely want this as `true`. | 207 + | `COMPOSE_FILE_GLOB` | - | `**/{compose,docker-compose}*.y?(a)ml` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for finding files for **Files Paths** in Stack config | 208 + | `ENV_FILE_GLOB` | - | `**/.env` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for finding files for **Additional Env Files** in Stack config | 209 + | `KOMODO_ENV_NAME` | - | `.komodoenv` | If existing .env files are found, and `WRITE_ENV=false`, then this name will be used for the .env that Komodo writes using its own Environment section | 210 + | `IMAGE_REGISTRY_PROVIDER` | - | | Name of Image Registry to use | 211 + | `IMAGE_REGISTRY_ACCOUNT` | - | | Image Registry account to use | 212 + | `POLL_FOR_UPDATE` | - | | Poll for newer images? | 213 + | `AUTO_UPDATE` | - | | Auto Update stack? | 136 214 137 215 </details> 138 216 139 - For [docker](../../installation#docker) usage the folder with your projects should be mounted to `/filesOnServer` in the container. 217 + <details markdown="1"> 140 218 141 - #### Git Repo 219 + <summary>Compose File Detection</summary> 142 220 143 - Use existing, **git repository** folders containing [Docker Compose projects](https://docs.docker.com/compose/how-tos/project-name/) (`compose.yaml`) as **Git Repo** mode Komodo Stacks. 221 + * Default pattern 222 + * Looks for `compose.y(a)ml` and `docker-compose.y(a)ml` 223 + * Files may have interim names like `compose.dev.yaml` 224 + * Can be overridden with `COMPOSE_FILE_GLOB` env for non-compose project stack sources 225 + * Will prioritize `compose.yaml` over `docker-compose.yaml` 226 + * Will choose the file with the shortest path EX 227 + * Prioritizes `./compose.yaml` over `./aFolder/compose.yaml` 144 228 145 - KI will generate multiple Stacks from all folders within the given folder. Each folder detected as a git repository will have all required fields in a Git Repo Komodo Stack configured based on information from the local repository. 229 + Komodo Import will **log** which files it detects and indicate which ones it will use for each project. 230 + 231 + :::note 232 + 233 + When using [Compose Projects Stack Source](./?stacksFrom=compose#stack-sources) compose files are automatically parsed from the running projects. 234 + 235 + ::: 236 + 237 + </details> 238 + 239 + <details> 240 + 241 + <summary>`.env` File Detection</summary> 242 + 243 + * Default pattern 244 + * Adds all `.env` files found at top-level or sub-folders 245 + * Can be overridden with `ENV_FILE_GLOB` env 246 + * If a `.env` file is found and `WRITE_ENV=false` then a Stack is configured to write it's own Environment section to `.komodoenv` instead of `.env` 247 + * Komodo env name can be overridden with `KOMODO_ENV_NAME` 248 + 249 + Komodo Import will **log** which files it detects and indicate which ones it will use for each project. 250 + 251 + </details> 252 + 253 + 254 + There is additional configuration available depending on the [Stack Source](#stack-sources): 255 + 256 + <Tabs groupId="stacksFrom" queryString> 257 + <TabItem value="dir" label="Directory"> 258 + 259 + * The `_GLOB` patterns are for matching folder names within the parent folder, not full directory paths 260 + * The default pattern matches all folder names except those starting with a `.` 261 + 262 + | ENV | Required | Default | Description | 263 + | :------------------- | :------- | ------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | 264 + | `FOLDER_GLOB` | - | `*` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for finding folders in the top-level folder given to Komodo Import | 265 + | `FOLDER_IGNORE_GLOB` | - | | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use to ignore folders in the top-level folder given to Komodo Import | 266 + 267 + </TabItem> 268 + <TabItem value="compose" label="Compose Projects"> 269 + 270 + * The `_GLOB` patterns are for matching **full directory paths**, not just a single folder name 271 + * The default pattern matches all directories except those that have any folder starting with a `.` 272 + * Komodo Import will **always** ignore projects created by Komodo itself (projects found in `/etc/komodo` or `/home/USER/komodo`) 273 + 274 + | ENV | Required | Default | Description | 275 + | :------------------------- | :------- | ------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | 276 + | `COMPOSE_FILE_GLOB` | - | `**/**` | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use for matching a project's **full directory** on the host | 277 + | `COMPOSE_FILE_IGNORE_GLOB` | - | | The [glob](https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer) pattern to use to ignore a project's **full directory** on the host | 278 + 279 + </TabItem> 280 + </Tabs> 281 + 282 + #### Git Repo Stacks 283 + 284 + If a parsed folder (or parent of it) contains a `.git` folder and it has a valid tracked branch + remote, then it will generate a **Git Repo Stack**. 146 285 147 286 <details> 148 287 149 - <summary>How Repository Parsing Works</summary> 288 + <summary>How Git Repository Parsing Works</summary> 150 289 151 290 * A folder is only built as a Git Repo stack if... 152 - * It contains a valid `.git` folder 291 + * It contains a valid `.git` folder or a parent folder contains a `.git` folder 153 292 * The git repo has a valid remote 154 293 * The current checked out local branch is tracking a remote branch 155 - * If any of the above are not true then the folder is instead parsed as a [Files On Server](#files-on-server) Stack 156 - * Additionally, if [**Komodo API is configured**](#komodo-api) then... 157 - * If Komodo has a [Repo](https://komo.do/docs/resources#repo) Resource that matches the remote, then the Git Repo Stack uses a Linked Repo 158 - * If Komodo has a Git Provider matching the remote URL domain then that is used, along with the Git Provider user (private git is automatically detected) 294 + * If any of the above are not true then the folder is instead parsed as a Files On Server Stack 159 295 160 296 </details> 161 297 ··· 182 318 └── compose.yaml 183 319 ``` 184 320 185 - You run Komodo Import with the `homelab` directory: 186 - 187 - `docker run ... -e "HOST_PARENT_PATH=/home/myUser/homelab" -v /home/myUser/homelab:/filesOnServer foxxmd/komodo-import` 188 - 189 321 * **immich** is created as a Git Repo Stack with 190 322 * Git Provider: `git.mydomain.com` 191 323 * Repo: `myUser/immich` ··· 196 328 * Run Directory: `/home/myUser/homelab/uptime-kuma` 197 329 </details> 198 330 199 - ###### Monorepos 331 + Additionally, if [**Komodo API is configured**](#komodo-api) then... 200 332 201 - Komodo Import can also handle **monorepos.** Mount the top-level repository folder to the usual `/filesOnServer` location and it will automatically treat sub-folders as Git Repo Stacks using the top-level repository. 333 + * If Komodo has a [Repo](https://komo.do/docs/resources#repo) Resource that matches the remote, then the Git Repo Stack uses a Linked Repo 334 + * If Komodo has a Git Provider matching the remote URL domain then that is used, along with the Git Provider user (private git is automatically detected) 202 335 203 - <details> 336 + ###### Monorepos 204 337 205 - <summary>Example</summary> 338 + Komodo Import also handles **monorepos** IE one git repository with many folders containing projects. The only requirement to ensure this works is that the git repo "root" directory must be [mounted into Komodo Import.](#host-directory) 206 339 207 - You have a repository at `https://git.mydomain.com/myUser/homelab` and each folder in the repo is a different stack. 340 + <Tabs groupId="stacksFrom" queryString> 341 + <TabItem value="dir" label="Directory"> 208 342 209 - Your folder structure (on the host machine) looks like this: 343 + For [**Directory** Stack Source](./?stacksFrom=dir#stack-sources) use the root git repository directory as the [host directory](#host-directory). 210 344 211 - ``` 212 - /home/myUser/homelab 213 - ├── .git 214 - ├── immich 215 - │ └── compose.yaml 216 - ├── plex 217 - │ └── docker-compose.yaml 218 - └── uptime-kuma 219 - └── compose.yaml 220 - ``` 345 + <details> 221 346 222 - You run Komodo Import with the `homelab` directory: 347 + <summary>Example</summary> 223 348 224 - `docker run ... -v /home/myUser/homelab:/filesOnServer foxxmd/komodo-import` 225 - 226 - * **immich** is created as a Git Repo Stack with 227 - * Git Provider: `git.mydomain.com` 228 - * Repo: `myUser/homelab` 229 - * Run Directory: `immich` 230 - * **plex** is created as a Git Repo Stack with 231 - * Git Provider: `git.mydomain.com` 232 - * Repo: `myUser/homelab` 233 - * Run Directory: `plex` 234 - * **uptime-kuma** is created as a Files On Server Stack 235 - * Git Provider: `git.mydomain.com` 236 - * Repo: `myUser/homelab` 237 - * Run Directory: `uptime-kuma` 238 - 239 - </details> 240 - 241 - <details> 242 - 243 - <summary>Nested Monorepo</summary> 244 - 245 - If your stack folders are not the immediate sub-folders in your repository then specify the _relative directory from git root_ as `GIT_STACKS_DIR` to specify what parent folder to use: 349 + ``` 350 + /home/myUser/homelab 351 + ├── .git 352 + ├── immich 353 + │ └── compose.yaml 354 + ├── plex 355 + │ └── docker-compose.yaml 356 + └── uptime-kuma 357 + └── compose.yaml 358 + ``` 246 359 247 - ``` 248 - /home/myUser/homelab 249 - ├── .git 250 - ├── myStacks 251 - │ ├── immich 252 - │ │ └── compose.yaml 253 - │ ├── plex 254 - │ │ └── docker-compose.yaml 255 - │ └── uptime-kuma 256 - │ └── compose.yaml 257 - └── somethingElse 258 - ``` 360 + Use `/home/myUser/homelab` as the [host directory](#host-directory) because it contains `.git` folder. 259 361 260 - Use `GIT_STACKS_DIR=myStacks` with the above example. 362 + </details> 261 363 262 - </details> 364 + If the directory containing your projects is not the root directory then use the ENV `SCAN_DIR` to set the path to use for projects _relative_ to the [host directory](#host-directory): 263 365 264 - #### Configuration 366 + <details> 265 367 368 + <summary>Example</summary> 266 369 267 - <details> 370 + ``` 371 + /home/myUser/homelab 372 + ├── .git 373 + ├── myStacks <---- parent folder containing projects 374 + │ ├── immich 375 + │ │ └── compose.yaml 376 + │ ├── plex 377 + │ │ └── docker-compose.yaml 378 + │ └── uptime-kuma 379 + │ └── compose.yaml 380 + └── somethingElse 381 + ``` 268 382 269 - <summary>Environmental Variables</summary> 383 + * Use `/home/myUser/homelab` as the [host directory](#host-directory) because it contains `.git` folder 384 + * Use `SCAN_DIR=myStacks` -- the folder Komodo Import will parse projects from, _relative_ to the host directory `/home/myUser/homelab` 385 + </details> 270 386 271 - Combine with [**Common Stack Configuration environmental variables**](#common-stack-configuration) 387 + </TabItem> 388 + <TabItem value="compose" label="Compose Projects"> 272 389 273 - | ENV | Required | Default | Description | 274 - | :--------------- | :------- | ------- | :---------------------------------------------------------------------------- | 275 - | `GIT_STACKS_DIR` | - | | Set sub-folder to use for stacks when generating from a [monorepo](#monorepo) | 390 + Komodo Import will automatically determine if a project is in a monorepo by scanning all parent directories for `.git` folder. Just make sure the mounted [host directory](#host-directory) contains the monorepo folder at some level. 276 391 277 - </details> 392 + </TabItem> 393 + </Tabs> 278 394 279 395 ## Outputs 280 396 ··· 336 452 | `SYNC_NAME` | - | komodo-import | Name of Sync to create/modify | 337 453 338 454 </details> 339 - 340 - ### File 341 - 342 - Komodo Import will attempt to write the generated output to a `.toml` file if the ENV `OUTPUT_DIR` is present. This should be the *directory* (not file) that the generated file should be written to. 343 - 344 - Bind mount a folder into the container and set `OUTPUT_DIR` like in the example below: 345 - 346 - <details> 347 - 348 - <summary>File Output Example</summary> 349 - 350 - ```yaml 351 - services: 352 - komodo-import: 353 - # ... 354 - environment: 355 - # ... 356 - - OUTPUT_DIR=/output 357 - volumes: 358 - # ... 359 - - /my/host/folder:/output 360 - ``` 361 - 362 - </details> 363 - 364 - Use this with [manually creating Sync Resources](../resourceSync#create-sync-resource) 365 455 366 456 ## Komodo API 367 457
+2 -2
docsite/docs/usage/resourceSync.mdx
··· 18 18 Komodo Import gives you the TOML representation of Resources not yet in Komodo. You can use this to bulk "create" Resources that connect to your existing compose projects, container, etc, by **Executing** a Sync Resource created with Komodo Import. 19 19 20 20 :::tip 21 - **If you are copy-pasting [console Output](../overview#console) or [file Output](../overview#file)** then proceed to [Create Sync Resource](#create-sync-resource) to **manually** create a Sync Resource from the TOML Komodo Import generated for you. 21 + **If you are copy-pasting [console Output](../overview#console)** then proceed to [**Create Sync Resource**](#create-sync-resource) to manually create a Sync Resource from the TOML Komodo Import generated for you. 22 22 ::: 23 23 24 24 :::tip 25 - **If you used [API Output](../overview#api-sync)** then find the Sync Resource KI made for you and skip to [Verify Sync Changes](#verify-sync-changes). 25 + **If you used [API Output](../overview#api-sync)** then find the Sync Resource KI made for you and skip to [**Verify Sync Changes**](#verify-sync-changes). 26 26 ::: 27 27 28 28 ### Create Sync Resource
+30 -1
docsite/docusaurus.config.ts
··· 87 87 'docusaurus-theme-github-codeblock' 88 88 ], 89 89 plugins: [ 90 + 91 + // Custom plugin to modify Webpack config 92 + function myPlugin(context, options) { 93 + return { 94 + name: 'custom-webpack-plugin', 95 + configureWebpack(config, isServer, utils, content) { 96 + return { 97 + module: { 98 + rules: [ 99 + { 100 + resourceQuery: /raw/, 101 + type: "asset/source", 102 + }, 103 + ], 104 + }, 105 + }; 106 + }, 107 + }; 108 + }, 109 + [ 110 + '@docusaurus/plugin-ideal-image', 111 + { 112 + quality: 70, 113 + max: 1030, // max resized image's size. 114 + min: 640, // min resized image's size. if original is lower, use that size. 115 + steps: 2, // the max number of images generated between min and max (inclusive) 116 + disableInDev: false, 117 + }, 118 + ], 90 119 ], 91 120 themeConfig: 92 121 { ··· 152 181 prism: { 153 182 theme: themes.themes.github, 154 183 darkTheme: themes.themes.dracula, 155 - additionalLanguages: ['json','json5','typescript', 'docker', 'bash', 'ini'] 184 + additionalLanguages: ['json','json5','typescript', 'docker', 'bash', 'ini','yaml'] 156 185 }, 157 186 colorMode: { 158 187 defaultMode: 'dark',
+552 -1
docsite/package-lock.json
··· 10 10 "dependencies": { 11 11 "@docusaurus/core": "3.8.1", 12 12 "@docusaurus/faster": "3.8.1", 13 + "@docusaurus/plugin-ideal-image": "^3.8.1", 13 14 "@docusaurus/preset-classic": "3.8.1", 14 15 "@easyops-cn/docusaurus-search-local": "0.51.1", 15 16 "@mdx-js/react": "^3.0.0", ··· 20 21 "prism-react-renderer": "^2.3.0", 21 22 "raw-loader": "^4.0.2", 22 23 "react": "^19.0.0", 23 - "react-dom": "^19.0.0" 24 + "react-dom": "^19.0.0", 25 + "use-debounce": "^10.0.5", 26 + "yaml": "^2.8.1" 24 27 }, 25 28 "devDependencies": { 26 29 "@docusaurus/module-type-aliases": "^3.6.3", ··· 3347 3350 "node": ">=18.0" 3348 3351 } 3349 3352 }, 3353 + "node_modules/@docusaurus/lqip-loader": { 3354 + "version": "3.8.1", 3355 + "resolved": "https://registry.npmjs.org/@docusaurus/lqip-loader/-/lqip-loader-3.8.1.tgz", 3356 + "integrity": "sha512-wSc/TDw6TjKle9MnFO4yqbc9120GIt6YIMT5obqThGcDcBXtkwUsSnw0ghEk22VXqAsgAxD/cGCp6O0SegRtYA==", 3357 + "license": "MIT", 3358 + "dependencies": { 3359 + "@docusaurus/logger": "3.8.1", 3360 + "file-loader": "^6.2.0", 3361 + "lodash": "^4.17.21", 3362 + "sharp": "^0.32.3", 3363 + "tslib": "^2.6.0" 3364 + }, 3365 + "engines": { 3366 + "node": ">=18.0" 3367 + } 3368 + }, 3350 3369 "node_modules/@docusaurus/mdx-loader": { 3351 3370 "version": "3.8.1", 3352 3371 "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.8.1.tgz", ··· 3590 3609 "react-dom": "^18.0.0 || ^19.0.0" 3591 3610 } 3592 3611 }, 3612 + "node_modules/@docusaurus/plugin-ideal-image": { 3613 + "version": "3.8.1", 3614 + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-ideal-image/-/plugin-ideal-image-3.8.1.tgz", 3615 + "integrity": "sha512-Y+ts2dAvBFqLjt5VjpEn15Ct4D93RyZXcpdU3gtrrQETg2V2aSRP4jOXexoUzJACIOG5IWjEXCUeaoVT9o7GFQ==", 3616 + "license": "MIT", 3617 + "dependencies": { 3618 + "@docusaurus/core": "3.8.1", 3619 + "@docusaurus/lqip-loader": "3.8.1", 3620 + "@docusaurus/responsive-loader": "^1.7.0", 3621 + "@docusaurus/theme-translations": "3.8.1", 3622 + "@docusaurus/types": "3.8.1", 3623 + "@docusaurus/utils-validation": "3.8.1", 3624 + "sharp": "^0.32.3", 3625 + "tslib": "^2.6.0", 3626 + "webpack": "^5.88.1" 3627 + }, 3628 + "engines": { 3629 + "node": ">=18.0" 3630 + }, 3631 + "peerDependencies": { 3632 + "jimp": "*", 3633 + "react": "^18.0.0 || ^19.0.0", 3634 + "react-dom": "^18.0.0 || ^19.0.0" 3635 + }, 3636 + "peerDependenciesMeta": { 3637 + "jimp": { 3638 + "optional": true 3639 + } 3640 + } 3641 + }, 3593 3642 "node_modules/@docusaurus/plugin-sitemap": { 3594 3643 "version": "3.8.1", 3595 3644 "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.1.tgz", ··· 3665 3714 "peerDependencies": { 3666 3715 "react": "^18.0.0 || ^19.0.0", 3667 3716 "react-dom": "^18.0.0 || ^19.0.0" 3717 + } 3718 + }, 3719 + "node_modules/@docusaurus/responsive-loader": { 3720 + "version": "1.7.1", 3721 + "resolved": "https://registry.npmjs.org/@docusaurus/responsive-loader/-/responsive-loader-1.7.1.tgz", 3722 + "integrity": "sha512-jAebZ43f8GVpZSrijLGHVVp7Y0OMIPRaL+HhiIWQ+f/b72lTsKLkSkOVHEzvd2psNJ9lsoiM3gt6akpak6508w==", 3723 + "license": "BSD-3-Clause", 3724 + "dependencies": { 3725 + "loader-utils": "^2.0.0" 3726 + }, 3727 + "engines": { 3728 + "node": ">=12" 3729 + }, 3730 + "peerDependencies": { 3731 + "jimp": "*", 3732 + "sharp": "*" 3733 + }, 3734 + "peerDependenciesMeta": { 3735 + "jimp": { 3736 + "optional": true 3737 + }, 3738 + "sharp": { 3739 + "optional": true 3740 + } 3668 3741 } 3669 3742 }, 3670 3743 "node_modules/@docusaurus/theme-classic": { ··· 6321 6394 "postcss": "^8.1.0" 6322 6395 } 6323 6396 }, 6397 + "node_modules/b4a": { 6398 + "version": "1.6.7", 6399 + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", 6400 + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", 6401 + "license": "Apache-2.0" 6402 + }, 6324 6403 "node_modules/babel-loader": { 6325 6404 "version": "9.2.1", 6326 6405 "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", ··· 6409 6488 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 6410 6489 "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 6411 6490 }, 6491 + "node_modules/bare-events": { 6492 + "version": "2.6.1", 6493 + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz", 6494 + "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==", 6495 + "license": "Apache-2.0", 6496 + "optional": true 6497 + }, 6498 + "node_modules/bare-fs": { 6499 + "version": "4.2.1", 6500 + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.2.1.tgz", 6501 + "integrity": "sha512-mELROzV0IhqilFgsl1gyp48pnZsaV9xhQapHLDsvn4d4ZTfbFhcghQezl7FTEDNBcGqLUnNI3lUlm6ecrLWdFA==", 6502 + "license": "Apache-2.0", 6503 + "optional": true, 6504 + "dependencies": { 6505 + "bare-events": "^2.5.4", 6506 + "bare-path": "^3.0.0", 6507 + "bare-stream": "^2.6.4" 6508 + }, 6509 + "engines": { 6510 + "bare": ">=1.16.0" 6511 + }, 6512 + "peerDependencies": { 6513 + "bare-buffer": "*" 6514 + }, 6515 + "peerDependenciesMeta": { 6516 + "bare-buffer": { 6517 + "optional": true 6518 + } 6519 + } 6520 + }, 6521 + "node_modules/bare-os": { 6522 + "version": "3.6.2", 6523 + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", 6524 + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", 6525 + "license": "Apache-2.0", 6526 + "optional": true, 6527 + "engines": { 6528 + "bare": ">=1.14.0" 6529 + } 6530 + }, 6531 + "node_modules/bare-path": { 6532 + "version": "3.0.0", 6533 + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", 6534 + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", 6535 + "license": "Apache-2.0", 6536 + "optional": true, 6537 + "dependencies": { 6538 + "bare-os": "^3.0.1" 6539 + } 6540 + }, 6541 + "node_modules/bare-stream": { 6542 + "version": "2.7.0", 6543 + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", 6544 + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", 6545 + "license": "Apache-2.0", 6546 + "optional": true, 6547 + "dependencies": { 6548 + "streamx": "^2.21.0" 6549 + }, 6550 + "peerDependencies": { 6551 + "bare-buffer": "*", 6552 + "bare-events": "*" 6553 + }, 6554 + "peerDependenciesMeta": { 6555 + "bare-buffer": { 6556 + "optional": true 6557 + }, 6558 + "bare-events": { 6559 + "optional": true 6560 + } 6561 + } 6562 + }, 6563 + "node_modules/base64-js": { 6564 + "version": "1.5.1", 6565 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 6566 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 6567 + "funding": [ 6568 + { 6569 + "type": "github", 6570 + "url": "https://github.com/sponsors/feross" 6571 + }, 6572 + { 6573 + "type": "patreon", 6574 + "url": "https://www.patreon.com/feross" 6575 + }, 6576 + { 6577 + "type": "consulting", 6578 + "url": "https://feross.org/support" 6579 + } 6580 + ], 6581 + "license": "MIT" 6582 + }, 6412 6583 "node_modules/batch": { 6413 6584 "version": "0.6.1", 6414 6585 "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", ··· 6433 6604 }, 6434 6605 "funding": { 6435 6606 "url": "https://github.com/sponsors/sindresorhus" 6607 + } 6608 + }, 6609 + "node_modules/bl": { 6610 + "version": "4.1.0", 6611 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 6612 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 6613 + "license": "MIT", 6614 + "dependencies": { 6615 + "buffer": "^5.5.0", 6616 + "inherits": "^2.0.4", 6617 + "readable-stream": "^3.4.0" 6436 6618 } 6437 6619 }, 6438 6620 "node_modules/body-parser": { ··· 6570 6752 }, 6571 6753 "engines": { 6572 6754 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 6755 + } 6756 + }, 6757 + "node_modules/buffer": { 6758 + "version": "5.7.1", 6759 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 6760 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 6761 + "funding": [ 6762 + { 6763 + "type": "github", 6764 + "url": "https://github.com/sponsors/feross" 6765 + }, 6766 + { 6767 + "type": "patreon", 6768 + "url": "https://www.patreon.com/feross" 6769 + }, 6770 + { 6771 + "type": "consulting", 6772 + "url": "https://feross.org/support" 6773 + } 6774 + ], 6775 + "license": "MIT", 6776 + "dependencies": { 6777 + "base64-js": "^1.3.1", 6778 + "ieee754": "^1.1.13" 6573 6779 } 6574 6780 }, 6575 6781 "node_modules/buffer-from": { ··· 6853 7059 "fsevents": "~2.3.2" 6854 7060 } 6855 7061 }, 7062 + "node_modules/chownr": { 7063 + "version": "1.1.4", 7064 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 7065 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 7066 + "license": "ISC" 7067 + }, 6856 7068 "node_modules/chrome-trace-event": { 6857 7069 "version": "1.0.4", 6858 7070 "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", ··· 6983 7195 "url": "https://github.com/sponsors/wooorm" 6984 7196 } 6985 7197 }, 7198 + "node_modules/color": { 7199 + "version": "4.2.3", 7200 + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 7201 + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 7202 + "license": "MIT", 7203 + "dependencies": { 7204 + "color-convert": "^2.0.1", 7205 + "color-string": "^1.9.0" 7206 + }, 7207 + "engines": { 7208 + "node": ">=12.5.0" 7209 + } 7210 + }, 6986 7211 "node_modules/color-convert": { 6987 7212 "version": "2.0.1", 6988 7213 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", ··· 6998 7223 "version": "1.1.4", 6999 7224 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 7000 7225 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 7226 + }, 7227 + "node_modules/color-string": { 7228 + "version": "1.9.1", 7229 + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 7230 + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 7231 + "license": "MIT", 7232 + "dependencies": { 7233 + "color-name": "^1.0.0", 7234 + "simple-swizzle": "^0.2.2" 7235 + } 7001 7236 }, 7002 7237 "node_modules/colord": { 7003 7238 "version": "2.9.3", ··· 8247 8482 "node": ">=0.10.0" 8248 8483 } 8249 8484 }, 8485 + "node_modules/end-of-stream": { 8486 + "version": "1.4.5", 8487 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 8488 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 8489 + "license": "MIT", 8490 + "dependencies": { 8491 + "once": "^1.4.0" 8492 + } 8493 + }, 8250 8494 "node_modules/enhanced-resolve": { 8251 8495 "version": "5.18.0", 8252 8496 "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", ··· 8606 8850 "url": "https://github.com/sindresorhus/execa?sponsor=1" 8607 8851 } 8608 8852 }, 8853 + "node_modules/expand-template": { 8854 + "version": "2.0.3", 8855 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 8856 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 8857 + "license": "(MIT OR WTFPL)", 8858 + "engines": { 8859 + "node": ">=6" 8860 + } 8861 + }, 8609 8862 "node_modules/express": { 8610 8863 "version": "4.21.2", 8611 8864 "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", ··· 8715 8968 "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 8716 8969 "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 8717 8970 }, 8971 + "node_modules/fast-fifo": { 8972 + "version": "1.3.2", 8973 + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", 8974 + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", 8975 + "license": "MIT" 8976 + }, 8718 8977 "node_modules/fast-glob": { 8719 8978 "version": "3.3.3", 8720 8979 "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", ··· 9035 9294 "node": ">= 0.6" 9036 9295 } 9037 9296 }, 9297 + "node_modules/fs-constants": { 9298 + "version": "1.0.0", 9299 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 9300 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 9301 + "license": "MIT" 9302 + }, 9038 9303 "node_modules/fs-extra": { 9039 9304 "version": "11.3.0", 9040 9305 "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", ··· 9146 9411 "funding": { 9147 9412 "url": "https://github.com/sponsors/sindresorhus" 9148 9413 } 9414 + }, 9415 + "node_modules/github-from-package": { 9416 + "version": "0.0.0", 9417 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 9418 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 9419 + "license": "MIT" 9149 9420 }, 9150 9421 "node_modules/github-slugger": { 9151 9422 "version": "1.5.0", ··· 9930 10201 "postcss": "^8.1.0" 9931 10202 } 9932 10203 }, 10204 + "node_modules/ieee754": { 10205 + "version": "1.2.1", 10206 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 10207 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 10208 + "funding": [ 10209 + { 10210 + "type": "github", 10211 + "url": "https://github.com/sponsors/feross" 10212 + }, 10213 + { 10214 + "type": "patreon", 10215 + "url": "https://www.patreon.com/feross" 10216 + }, 10217 + { 10218 + "type": "consulting", 10219 + "url": "https://feross.org/support" 10220 + } 10221 + ], 10222 + "license": "BSD-3-Clause" 10223 + }, 9933 10224 "node_modules/ignore": { 9934 10225 "version": "5.3.2", 9935 10226 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", ··· 13215 13506 "url": "https://github.com/sponsors/ljharb" 13216 13507 } 13217 13508 }, 13509 + "node_modules/mkdirp-classic": { 13510 + "version": "0.5.3", 13511 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 13512 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 13513 + "license": "MIT" 13514 + }, 13218 13515 "node_modules/mrmime": { 13219 13516 "version": "2.0.1", 13220 13517 "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", ··· 13260 13557 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 13261 13558 } 13262 13559 }, 13560 + "node_modules/napi-build-utils": { 13561 + "version": "2.0.0", 13562 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 13563 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", 13564 + "license": "MIT" 13565 + }, 13263 13566 "node_modules/negotiator": { 13264 13567 "version": "0.6.4", 13265 13568 "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", ··· 13283 13586 "lower-case": "^2.0.2", 13284 13587 "tslib": "^2.0.3" 13285 13588 } 13589 + }, 13590 + "node_modules/node-abi": { 13591 + "version": "3.75.0", 13592 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", 13593 + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", 13594 + "license": "MIT", 13595 + "dependencies": { 13596 + "semver": "^7.3.5" 13597 + }, 13598 + "engines": { 13599 + "node": ">=10" 13600 + } 13601 + }, 13602 + "node_modules/node-addon-api": { 13603 + "version": "6.1.0", 13604 + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", 13605 + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", 13606 + "license": "MIT" 13286 13607 }, 13287 13608 "node_modules/node-emoji": { 13288 13609 "version": "2.2.0", ··· 15352 15673 "postcss": "^8.4.31" 15353 15674 } 15354 15675 }, 15676 + "node_modules/prebuild-install": { 15677 + "version": "7.1.3", 15678 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 15679 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 15680 + "license": "MIT", 15681 + "dependencies": { 15682 + "detect-libc": "^2.0.0", 15683 + "expand-template": "^2.0.3", 15684 + "github-from-package": "0.0.0", 15685 + "minimist": "^1.2.3", 15686 + "mkdirp-classic": "^0.5.3", 15687 + "napi-build-utils": "^2.0.0", 15688 + "node-abi": "^3.3.0", 15689 + "pump": "^3.0.0", 15690 + "rc": "^1.2.7", 15691 + "simple-get": "^4.0.0", 15692 + "tar-fs": "^2.0.0", 15693 + "tunnel-agent": "^0.6.0" 15694 + }, 15695 + "bin": { 15696 + "prebuild-install": "bin.js" 15697 + }, 15698 + "engines": { 15699 + "node": ">=10" 15700 + } 15701 + }, 15702 + "node_modules/prebuild-install/node_modules/tar-fs": { 15703 + "version": "2.1.3", 15704 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", 15705 + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", 15706 + "license": "MIT", 15707 + "dependencies": { 15708 + "chownr": "^1.1.1", 15709 + "mkdirp-classic": "^0.5.2", 15710 + "pump": "^3.0.0", 15711 + "tar-stream": "^2.1.4" 15712 + } 15713 + }, 15714 + "node_modules/prebuild-install/node_modules/tar-stream": { 15715 + "version": "2.2.0", 15716 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 15717 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 15718 + "license": "MIT", 15719 + "dependencies": { 15720 + "bl": "^4.0.3", 15721 + "end-of-stream": "^1.4.1", 15722 + "fs-constants": "^1.0.0", 15723 + "inherits": "^2.0.3", 15724 + "readable-stream": "^3.1.1" 15725 + }, 15726 + "engines": { 15727 + "node": ">=6" 15728 + } 15729 + }, 15355 15730 "node_modules/pretty-error": { 15356 15731 "version": "4.0.0", 15357 15732 "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", ··· 15454 15829 "license": "MIT", 15455 15830 "engines": { 15456 15831 "node": ">= 0.10" 15832 + } 15833 + }, 15834 + "node_modules/pump": { 15835 + "version": "3.0.3", 15836 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", 15837 + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", 15838 + "license": "MIT", 15839 + "dependencies": { 15840 + "end-of-stream": "^1.1.0", 15841 + "once": "^1.3.1" 15457 15842 } 15458 15843 }, 15459 15844 "node_modules/punycode": { ··· 16748 17133 "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", 16749 17134 "license": "MIT" 16750 17135 }, 17136 + "node_modules/sharp": { 17137 + "version": "0.32.6", 17138 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", 17139 + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", 17140 + "hasInstallScript": true, 17141 + "license": "Apache-2.0", 17142 + "dependencies": { 17143 + "color": "^4.2.3", 17144 + "detect-libc": "^2.0.2", 17145 + "node-addon-api": "^6.1.0", 17146 + "prebuild-install": "^7.1.1", 17147 + "semver": "^7.5.4", 17148 + "simple-get": "^4.0.1", 17149 + "tar-fs": "^3.0.4", 17150 + "tunnel-agent": "^0.6.0" 17151 + }, 17152 + "engines": { 17153 + "node": ">=14.15.0" 17154 + }, 17155 + "funding": { 17156 + "url": "https://opencollective.com/libvips" 17157 + } 17158 + }, 16751 17159 "node_modules/shebang-command": { 16752 17160 "version": "2.0.0", 16753 17161 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", ··· 16859 17267 "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 16860 17268 "license": "ISC" 16861 17269 }, 17270 + "node_modules/simple-concat": { 17271 + "version": "1.0.1", 17272 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 17273 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 17274 + "funding": [ 17275 + { 17276 + "type": "github", 17277 + "url": "https://github.com/sponsors/feross" 17278 + }, 17279 + { 17280 + "type": "patreon", 17281 + "url": "https://www.patreon.com/feross" 17282 + }, 17283 + { 17284 + "type": "consulting", 17285 + "url": "https://feross.org/support" 17286 + } 17287 + ], 17288 + "license": "MIT" 17289 + }, 17290 + "node_modules/simple-get": { 17291 + "version": "4.0.1", 17292 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 17293 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 17294 + "funding": [ 17295 + { 17296 + "type": "github", 17297 + "url": "https://github.com/sponsors/feross" 17298 + }, 17299 + { 17300 + "type": "patreon", 17301 + "url": "https://www.patreon.com/feross" 17302 + }, 17303 + { 17304 + "type": "consulting", 17305 + "url": "https://feross.org/support" 17306 + } 17307 + ], 17308 + "license": "MIT", 17309 + "dependencies": { 17310 + "decompress-response": "^6.0.0", 17311 + "once": "^1.3.1", 17312 + "simple-concat": "^1.0.0" 17313 + } 17314 + }, 17315 + "node_modules/simple-swizzle": { 17316 + "version": "0.2.2", 17317 + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 17318 + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 17319 + "license": "MIT", 17320 + "dependencies": { 17321 + "is-arrayish": "^0.3.1" 17322 + } 17323 + }, 17324 + "node_modules/simple-swizzle/node_modules/is-arrayish": { 17325 + "version": "0.3.2", 17326 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 17327 + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 17328 + "license": "MIT" 17329 + }, 16862 17330 "node_modules/sirv": { 16863 17331 "version": "2.0.4", 16864 17332 "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", ··· 17056 17524 "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", 17057 17525 "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", 17058 17526 "license": "MIT" 17527 + }, 17528 + "node_modules/streamx": { 17529 + "version": "2.22.1", 17530 + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", 17531 + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", 17532 + "license": "MIT", 17533 + "dependencies": { 17534 + "fast-fifo": "^1.3.2", 17535 + "text-decoder": "^1.1.0" 17536 + }, 17537 + "optionalDependencies": { 17538 + "bare-events": "^2.2.0" 17539 + } 17059 17540 }, 17060 17541 "node_modules/string_decoder": { 17061 17542 "version": "1.3.0", ··· 17285 17766 "node": ">=6" 17286 17767 } 17287 17768 }, 17769 + "node_modules/tar-fs": { 17770 + "version": "3.1.0", 17771 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", 17772 + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", 17773 + "license": "MIT", 17774 + "dependencies": { 17775 + "pump": "^3.0.0", 17776 + "tar-stream": "^3.1.5" 17777 + }, 17778 + "optionalDependencies": { 17779 + "bare-fs": "^4.0.1", 17780 + "bare-path": "^3.0.0" 17781 + } 17782 + }, 17783 + "node_modules/tar-stream": { 17784 + "version": "3.1.7", 17785 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", 17786 + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", 17787 + "license": "MIT", 17788 + "dependencies": { 17789 + "b4a": "^1.6.4", 17790 + "fast-fifo": "^1.2.0", 17791 + "streamx": "^2.15.0" 17792 + } 17793 + }, 17288 17794 "node_modules/terser": { 17289 17795 "version": "5.37.0", 17290 17796 "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", ··· 17367 17873 "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 17368 17874 "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 17369 17875 }, 17876 + "node_modules/text-decoder": { 17877 + "version": "1.2.3", 17878 + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", 17879 + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", 17880 + "license": "Apache-2.0", 17881 + "dependencies": { 17882 + "b4a": "^1.6.4" 17883 + } 17884 + }, 17370 17885 "node_modules/thunky": { 17371 17886 "version": "1.1.0", 17372 17887 "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", ··· 17443 17958 "version": "2.8.1", 17444 17959 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 17445 17960 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 17961 + }, 17962 + "node_modules/tunnel-agent": { 17963 + "version": "0.6.0", 17964 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 17965 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 17966 + "license": "Apache-2.0", 17967 + "dependencies": { 17968 + "safe-buffer": "^5.0.1" 17969 + }, 17970 + "engines": { 17971 + "node": "*" 17972 + } 17446 17973 }, 17447 17974 "node_modules/type-fest": { 17448 17975 "version": "2.19.0", ··· 17899 18426 "funding": { 17900 18427 "type": "opencollective", 17901 18428 "url": "https://opencollective.com/webpack" 18429 + } 18430 + }, 18431 + "node_modules/use-debounce": { 18432 + "version": "10.0.5", 18433 + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz", 18434 + "integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==", 18435 + "license": "MIT", 18436 + "engines": { 18437 + "node": ">= 16.0.0" 18438 + }, 18439 + "peerDependencies": { 18440 + "react": "*" 17902 18441 } 17903 18442 }, 17904 18443 "node_modules/util-deprecate": { ··· 18607 19146 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 18608 19147 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 18609 19148 "license": "ISC" 19149 + }, 19150 + "node_modules/yaml": { 19151 + "version": "2.8.1", 19152 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", 19153 + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", 19154 + "license": "ISC", 19155 + "bin": { 19156 + "yaml": "bin.mjs" 19157 + }, 19158 + "engines": { 19159 + "node": ">= 14.6" 19160 + } 18610 19161 }, 18611 19162 "node_modules/yocto-queue": { 18612 19163 "version": "1.2.1",
+4 -1
docsite/package.json
··· 16 16 "dependencies": { 17 17 "@docusaurus/core": "3.8.1", 18 18 "@docusaurus/faster": "3.8.1", 19 + "@docusaurus/plugin-ideal-image": "^3.8.1", 19 20 "@docusaurus/preset-classic": "3.8.1", 20 21 "@easyops-cn/docusaurus-search-local": "0.51.1", 21 22 "@mdx-js/react": "^3.0.0", ··· 26 27 "prism-react-renderer": "^2.3.0", 27 28 "raw-loader": "^4.0.2", 28 29 "react": "^19.0.0", 29 - "react-dom": "^19.0.0" 30 + "react-dom": "^19.0.0", 31 + "use-debounce": "^10.0.5", 32 + "yaml": "^2.8.1" 30 33 }, 31 34 "devDependencies": { 32 35 "@docusaurus/module-type-aliases": "^3.6.3",
+84
docsite/src/components/Button.tsx
··· 1 + import React, { ReactNode, CSSProperties } from 'react'; 2 + import clsx from 'clsx'; 3 + import Link from '@docusaurus/Link'; 4 + 5 + // Define the Button type to control the props that can be passed to the Button component. 6 + export type ButtonProps = { 7 + // The size prop can be one of the following values: 'sm', 'lg', 'small', 'medium', 'large', or null. 8 + // We'll convert 'small' to 'sm' and 'large' to 'lg' in the component. 'medium' will be considered null. 9 + size?: 'sm' | 'lg' | 'small' | 'medium' | 'large' | null; 10 + // The outline prop is a boolean that determines if the button should be an outline button. 11 + outline?: boolean; 12 + // The variant prop is a string that determines the color of the button. 13 + // It can be one of the following values: 'primary', 'secondary', 'danger', 'warning', 'success', 'info', 'link', or any other string. 14 + // The default value is 'primary'. 15 + variant: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'link' | string; 16 + // The block prop is a boolean that determines if the button should be a block-level button. 17 + block?: boolean; 18 + // The disabled prop is a boolean that determines if the button should be disabled. 19 + disabled?: boolean; 20 + // The className prop is a string that allows you to add custom classes to the button. 21 + className?: string; 22 + // The style prop is an object that allows you to add custom styles to the button. 23 + style?: CSSProperties; 24 + // The link prop is a string that determines the URL the button should link to. 25 + link: string; 26 + // The label prop is a string that determines the text of the button. 27 + label: string; 28 + onClick?: () => any 29 + } 30 + 31 + // Button component that accepts the specified props. 32 + export default function Button ({ 33 + size = null, 34 + outline = false, 35 + variant = 'primary', 36 + block = false, 37 + disabled = false, 38 + className, 39 + style = {}, 40 + link, 41 + label, 42 + onClick = () => null 43 + }: ButtonProps) { 44 + // Map the size prop values to corresponding CSS classes. 45 + const sizeMap = { 46 + sm: 'sm', 47 + small: 'sm', 48 + lg: 'lg', 49 + large: 'lg', 50 + medium: null, 51 + }; 52 + const buttonSize = size ? sizeMap[size] : ''; 53 + const sizeClass = buttonSize ? `button--${buttonSize}` : ''; 54 + const outlineClass = outline ? 'button--outline' : ''; 55 + const variantClass = variant ? `button--${variant}` : ''; 56 + const blockClass = block ? 'button--block' : ''; 57 + const disabledClass = disabled ? 'disabled' : ''; 58 + // If the button is disabled, set the destination to null. 59 + const destination = disabled ? null : link; 60 + return ( 61 + <Link to={destination}> 62 + <button 63 + className={clsx( 64 + 'button', 65 + sizeClass, 66 + outlineClass, 67 + variantClass, 68 + blockClass, 69 + disabledClass, 70 + className 71 + )} 72 + style={style} 73 + role='button' 74 + aria-disabled={disabled} 75 + onClick={(e) => { 76 + e.preventDefault(); 77 + onClick() 78 + }} 79 + > 80 + {label} 81 + </button> 82 + </Link> 83 + ); 84 + }
+82
docsite/src/components/ButtonGroup.tsx
··· 1 + import React, { ReactNode, CSSProperties } from 'react'; 2 + import clsx from 'clsx'; 3 + import Link from '@docusaurus/Link'; 4 + import Button, { ButtonProps } from './Button'; 5 + 6 + const leftButton = { 7 + borderRadius: 'var(--ifm-button-border-radius) 0 0 var(--ifm-button-border-radius)' 8 + } 9 + 10 + const rightButton = { 11 + borderRadius: '0 var(--ifm-button-border-radius) var(--ifm-button-border-radius) 0' 12 + } 13 + 14 + const middleButton = { 15 + borderRadius: '0' 16 + } 17 + 18 + // Define the Button type to control the props that can be passed to the Button component. 19 + type ButtonGroup = { 20 + options: [string, string][] 21 + value?: string 22 + defaultValue?: string 23 + // The size prop can be one of the following values: 'sm', 'lg', 'small', 'medium', 'large', or null. 24 + // We'll convert 'small' to 'sm' and 'large' to 'lg' in the component. 'medium' will be considered null. 25 + size?: 'sm' | 'lg' | 'small' | 'medium' | 'large' | null; 26 + 27 + variant: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'link' | string; 28 + // The block prop is a boolean that determines if the button should be a block-level button. 29 + block?: boolean; 30 + // The disabled prop is a boolean that determines if the button should be disabled. 31 + disabled?: boolean; 32 + // The className prop is a string that allows you to add custom classes to the button. 33 + className?: string; 34 + // The style prop is an object that allows you to add custom styles to the button. 35 + style?: CSSProperties; 36 + onChange?: (val: string) => any 37 + } 38 + 39 + // Button component that accepts the specified props. 40 + export default function ButtonGroup ({ 41 + size = null, 42 + variant = 'primary', 43 + block = false, 44 + disabled = false, 45 + className, 46 + style, 47 + options, 48 + value, 49 + defaultValue, 50 + onChange = () => null 51 + }: ButtonGroup) { 52 + 53 + if(options === undefined || options.length === 0) { 54 + return null; 55 + } 56 + 57 + const buttons = options.map((opt, index) => { 58 + const props: ButtonProps = { 59 + style: middleButton, 60 + outline: true, 61 + size, 62 + variant, 63 + label: opt[1], 64 + link: '#', 65 + onClick: () => onChange(opt[0]) 66 + }; 67 + if(index === 0) { 68 + props.style = leftButton; 69 + } else if(index === options.length - 1) { 70 + props.style = rightButton; 71 + } 72 + 73 + if((value ?? defaultValue) === opt[0]) { 74 + props.outline = false; 75 + } 76 + 77 + return <Button key={index} {...props}/> 78 + 79 + }); 80 + 81 + return <span>{buttons}</span>; 82 + }
+41
docsite/src/components/QuickStartSnippets/apiOutput.mdx
··· 1 + import Image from '@theme/IdealImage'; 2 + import syncExample from './sync-resource-file.png' 3 + 4 + <details> 5 + 6 + <summary>Komodo Example</summary> 7 + 8 + ```shell 9 + docker compose up --no-log-prefix 10 + ``` 11 + ``` 12 + INFO : [Init] Debug Mode: NO 13 + INFO : [App] Version: 0.1.0 14 + INFO : [App] Files On Server Dir ENV: /filesOnServer -> Resolved: /filesOnServer 15 + INFO : [App] [Files On Server] Processing Stacks for 2 folders: 16 + /filesOnServer/immich 17 + /filesOnServer/plex 18 + /filesOnServer/uptime-kuma 19 + INFO : [App] [Files On Server] Compose File Glob: **/{compose,docker-compose}*.y?(a)ml 20 + INFO : [App] [Files On Server] Env Glob: **/.env 21 + INFO : [App] [Files On Server] [compose] Found Stack 'compose' at dir /filesOnServer/compose.yaml 22 + WARN : [App] [Files On Server] [compose] Did not find any files patterns matching compose glob 23 + INFO : [App] [Files On Server] [compose] Stack config complete 24 + 25 + ... 26 + 27 + INFO : [App] [Files On Server] Built Stack configs for 3 folders 28 + INFO : [App] Copy the text between the scissors to use as the *Resource File* contents within your Resource Sync 29 + 30 + ... 31 + 32 + INFO : [App] [Sync API] Using 'komodo-import' as Sync Name 33 + VERBOSE: [App] [Sync API] KOMODO_URL: http://192.168.0.101:9120 | Normalized: http://192.168.0.101:9120/ 34 + INFO : [App] [Sync API] Resource Sync created. 35 + INFO : [App] [Sync API] Resource Sync URL: http://192.168.0.101:9120/resource-syncs/689a41a04ed1b642f3053f57 36 + ``` 37 + 38 + The [Sync Resource](../usage/resourceSync#verify-sync-changes) is automatically created for you (but not executed!) 39 + 40 + <Image img={syncExample}/> 41 + </details>
+67
docsite/src/components/QuickStartSnippets/consoleOutput.mdx
··· 1 + <details> 2 + 3 + <summary>Console Output Example</summary> 4 + 5 + ```shell 6 + docker compose up --no-log-prefix 7 + ``` 8 + ``` 9 + INFO : [Init] Debug Mode: NO 10 + INFO : [App] Version: 0.1.0 11 + INFO : [App] Files On Server Dir ENV: /filesOnServer -> Resolved: /filesOnServer 12 + INFO : [App] [Files On Server] Processing Stacks for 2 folders: 13 + /filesOnServer/immich 14 + /filesOnServer/plex 15 + /filesOnServer/uptime-kuma 16 + INFO : [App] [Files On Server] Compose File Glob: **/{compose,docker-compose}*.y?(a)ml 17 + INFO : [App] [Files On Server] Env Glob: **/.env 18 + INFO : [App] [Files On Server] [compose] Found Stack 'compose' at dir /filesOnServer/compose.yaml 19 + WARN : [App] [Files On Server] [compose] Did not find any files patterns matching compose glob 20 + INFO : [App] [Files On Server] [compose] Stack config complete 21 + 22 + ... 23 + 24 + INFO : [App] [Files On Server] Built Stack configs for 3 folders 25 + INFO : [App] Copy the text between the scissors to use as the *Resource File* contents within your Resource Sync 26 + 27 + ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ 28 + 29 + [[stack]] 30 + name = "immich" 31 + 32 + [stack.config] 33 + server = "my-cool-server" 34 + run_directory = "/home/myUser/homelab/immich" 35 + files_on_host = true 36 + auto_update = false 37 + poll_for_updates = false 38 + file_paths = [ "docker/docker-compose.yaml" ] 39 + 40 + [[stack]] 41 + name = "plex" 42 + 43 + [stack.config] 44 + server = "my-cool-server" 45 + run_directory = "/home/myUser/homelab/plex" 46 + files_on_host = true 47 + auto_update = false 48 + poll_for_updates = false 49 + 50 + [[stack]] 51 + name = "uptime-kuma" 52 + 53 + [stack.config] 54 + server = "my-cool-server" 55 + run_directory = "/home/myUser/homelab/uptime-kuma" 56 + files_on_host = true 57 + auto_update = false 58 + poll_for_updates = false 59 + env_file_path = ".komodoEnv" 60 + additional_env_files = [ ".env" ] 61 + ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ ✂️ 62 + INFO : [App] Done! 63 + ``` 64 + 65 + Copy the text between the scissors and [create a new Sync Resource in Komodo](../usage/resourceSync/#create-sync-resource) 66 + 67 + </details>
+21
docsite/src/components/QuickStartSnippets/hostDir.mdx
··· 1 + For `STACKS_FROM=dir` this should be the immediate parent directory containing all your project folders. 2 + 3 + <details> 4 + 5 + <summary>Example</summary> 6 + 7 + ``` 8 + . 9 + └── /home/myUser/homelab/ 10 + ├── immich/ 11 + │ └── compose.yaml 12 + ├── plex/ 13 + │ ├── docker-compose.yaml 14 + │ └── .git 15 + └── uptime-kuma/ 16 + └── compose.yaml 17 + ``` 18 + 19 + Then **Host Directory** should be `/home/myUser/homelab` 20 + 21 + </details>
+33
docsite/src/components/QuickStartSnippets/hostDirCompose.mdx
··· 1 + When using `STACKS_FROM=compose` this should be the the most top-level directory that all projects are contained within, at some level. 2 + 3 + <details> 4 + 5 + <summary>Example</summary> 6 + 7 + `docker compose ls` reveals these projects: 8 + 9 + ``` 10 + NAME STATUS CONFIG FILES 11 + immich running /home/myUser/homelab/immich/compose.yaml 12 + plex running /home/myUser/homelab/plex/docker-compose.yaml 13 + project1 running /home/myUser/localDev/myProjects/project1/compose.yaml 14 + ``` 15 + 16 + ``` 17 + . 18 + └── /home/myUser 19 + ├── homeLab 20 + │ ├── immich 21 + │ │ └── compose.yaml 22 + │ └── plex 23 + │ ├── docker-compose.yaml 24 + │ └── .git 25 + └── localDev 26 + └── myProjects 27 + └── project1 28 + └── compose.yaml 29 + ``` 30 + 31 + Then **Host Directory** should be `/home/myUser` 32 + 33 + </details>
docsite/src/components/QuickStartSnippets/sync-resource-file.png

This is a binary file and will not be displayed.

+287
docsite/src/components/QuickstartCompose.tsx
··· 1 + import React, { Fragment, useState, useEffect, PropsWithChildren, ReactHTMLElement, useCallback } from "react" 2 + import CodeBlock from '@theme/CodeBlock'; 3 + import CodeInline from '@theme/CodeInline'; 4 + import Admonition from '@theme/Admonition'; 5 + import MDXContent from "@theme/MDXContent"; 6 + import ErrorBoundary from "@docusaurus/ErrorBoundary" 7 + import Button from './Button'; 8 + import ButtonGroup from './ButtonGroup'; 9 + import Error from "@theme/Error" 10 + import {Document, parseDocument} from 'yaml'; 11 + import {YAMLMap, YAMLSeq, Pair, Scalar} from "yaml"; 12 + import { useDebounce } from "use-debounce"; 13 + //import ComposeExample from '!!raw-loader!@site/static/compose.txt'; 14 + //import ComposeExample from './compose.yaml?raw'; 15 + 16 + // to make this work need to create plugin to override webpack loader 17 + // https://github.com/facebook/docusaurus/issues/4445#issuecomment-1980332815 18 + // https://github.com/facebook/docusaurus/issues/4445#issuecomment-1249276192 19 + import ComposeExample from './compose.yaml?raw'; 20 + import SocketProxyExample from './socket-proxy.yaml?raw'; 21 + import HostComposeSnippet from './QuickStartSnippets/hostDirCompose.mdx'; 22 + import HostSnippet from './QuickStartSnippets/hostDir.mdx'; 23 + import ConsoleOutput from './QuickStartSnippets/consoleOutput.mdx'; 24 + import ApiOutput from './QuickStartSnippets/apiOutput.mdx'; 25 + 26 + const doc = parseDocument(ComposeExample); 27 + const proxyCollection = parseDocument(SocketProxyExample); 28 + 29 + interface ComposeStateData { 30 + autoUpdate?: boolean 31 + pollUpdate?: boolean 32 + stacksFrom?: 'dir' | 'compose', 33 + api?: boolean 34 + //hostDir?: string 35 + } 36 + 37 + export interface AIOProps { 38 + data?: string 39 + client?: boolean 40 + name?: string 41 + } 42 + 43 + type CheckboxProps = PropsWithChildren<{ 44 + id: string 45 + checked?: boolean 46 + onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void 47 + }> 48 + 49 + const Checkbox = (props: CheckboxProps) => { 50 + 51 + const { 52 + id, 53 + checked, 54 + children, 55 + onChange = () => null 56 + } = props; 57 + 58 + return <div className="margin-vert--sm"> 59 + <input className="margin-right--sm" type="checkbox" id="vehicle3" name="vehicle3" onChange={onChange} checked={checked ?? false}/> 60 + <label htmlFor="vehicle3">{children}</label> 61 + </div> 62 + } 63 + 64 + const QuickstartCompose = (props: AIOProps) => { 65 + const { 66 + data, 67 + name, 68 + client = false 69 + } = props; 70 + 71 + const [composeState, setCompose] = useState({ 72 + stacksFrom: 'dir', 73 + api: false 74 + } as ComposeStateData); 75 + const [userDoc, setUserDoc] = useState(doc); 76 + 77 + const [hostInputValue, setHostInputValue] = React.useState(""); 78 + const [hostValue] = useDebounce(hostInputValue, 500); 79 + 80 + const handleHostInputChange = (event) => { 81 + setHostInputValue(event.target.value); 82 + } 83 + 84 + const [serverInputValue, setServerInputValue] = React.useState(""); 85 + const [serverValue] = useDebounce(serverInputValue, 500); 86 + 87 + const handleServerInputChange = (event) => { 88 + setServerInputValue(event.target.value); 89 + } 90 + 91 + const [komodoUrlInputValue, setKomodoUrlInputValue] = React.useState(""); 92 + const [komodoUrlValue] = useDebounce(komodoUrlInputValue, 500); 93 + 94 + const handleKomodoUrlInputChange = (event) => { 95 + setKomodoUrlInputValue(event.target.value); 96 + } 97 + 98 + const [apiKeyInputValue, setApikeyInputValue] = React.useState(""); 99 + const [apiKeyValue] = useDebounce(apiKeyInputValue, 500); 100 + 101 + const handleApikeyInputChange = (event) => { 102 + setApikeyInputValue(event.target.value); 103 + } 104 + 105 + const [apiSecretInputValue, setApiSecretInputValue] = React.useState(""); 106 + const [apiSecretValue] = useDebounce(apiSecretInputValue, 500); 107 + 108 + const handleApiSecretInputChange = (event) => { 109 + setApiSecretInputValue(event.target.value); 110 + } 111 + 112 + const komodoApiCallback = useCallback(() => { 113 + setCompose({...composeState, api: !composeState.api}); 114 + }, [composeState, setCompose]); 115 + 116 + useEffect(() => { 117 + 118 + const modifiedDoc = doc.clone(); 119 + 120 + const hostPath = hostValue !== undefined && hostValue.trim() !== '' ? hostValue : '/home/myUser/homelab'; 121 + const hostPathS = new Scalar(`${hostPath}:/filesOnServer`); 122 + hostPathS.commentBefore = `# ${composeState.stacksFrom === 'dir' ? 'Parent directory where all subfolders are compose projects' : 'Top-most directory that contains all folders (at any level) that compose projects are found in'}` 123 + 124 + modifiedDoc.setIn(['services','komodo-import','volumes', 0], hostPathS); 125 + 126 + modifiedDoc.setIn(['services','komodo-import','environment'], new YAMLSeq()); 127 + 128 + const seq = modifiedDoc.getIn(['services','komodo-import','environment']) as YAMLSeq; 129 + 130 + 131 + const hostS = new Scalar(`HOST_PARENT_PATH=${hostPath}`); 132 + hostS.commentBefore = `# Same as mounted directory above` 133 + seq.add(hostS); 134 + 135 + const stacksFromS = new Scalar(`STACKS_FROM=${composeState.stacksFrom}`); 136 + stacksFromS.commentBefore = '# Determines what sources to generate Stacks from' 137 + stacksFromS.comment = `# ${composeState.stacksFrom === 'dir' ? 'Generate stacks from subfolders in directory' : 'Generate stacks from compose projects'}` 138 + seq.add(stacksFromS); 139 + 140 + const serverS = new Scalar(`SERVER_NAME=${serverValue !== undefined && serverValue.trim() !== '' ? serverValue : 'my-cool-server'}`); 141 + serverS.commentBefore = '# Name of Server for this machine, in Komodo' 142 + seq.add(serverS); 143 + 144 + if(composeState.api) { 145 + const urlS = new Scalar(`KOMODO_URL=${komodoUrlValue === '' ? 'http://192.168.KOMOMDO.IP:8120' : komodoUrlValue}`); 146 + urlS.spaceBefore = true; 147 + urlS.commentBefore = '# Configuration for interacting with Komodo API'; 148 + seq.add(urlS); 149 + seq.add(`API_KEY=${apiKeyValue === '' ? 'K-3A6btIPZYeBu_EXAMPLE_KEY' : apiKeyValue}`); 150 + seq.add(`API_SECRET=${apiSecretValue === '' ? 'S-qnaXD1frutYlfC2ZYl_EXAMPLE_SECRET' : apiSecretValue}`); 151 + const syncS = new Scalar('OUTPUT_API_SYNC=true'); 152 + syncS.comment = 'Create Sync Resource in Komodo' 153 + seq.add(syncS); 154 + } 155 + 156 + if(composeState.stacksFrom === 'compose') { 157 + const dockerS = new Scalar('DOCKER_HOST=tcp://socket-proxy:2375'); 158 + dockerS.spaceBefore = true; 159 + dockerS.commentBefore = 'Tells Komodo Import how to connect to Docker'; 160 + 161 + seq.add(dockerS); 162 + const d = new YAMLSeq(); 163 + d.add('socket-proxy'); 164 + modifiedDoc.setIn(['services', 'komodo-import', 'depends_on'], d); 165 + modifiedDoc.setIn(['services', 'socket-proxy'], proxyCollection.get('socket-proxy')); 166 + const proxy = modifiedDoc.getIn(['services', 'socket-proxy']) as YAMLMap; 167 + proxy.commentBefore = `# used to communicate with docker daemon` 168 + } 169 + 170 + 171 + // if(composeState.autoUpdate) { 172 + // seq.add('AUTO_UPDATE=true') 173 + // } 174 + // if(composeState.pollUpdate) { 175 + // seq.add('POLL_FOR_UPDATE=true') 176 + // } 177 + 178 + 179 + // @ts-ignore 180 + setUserDoc(modifiedDoc); 181 + 182 + }, [composeState, hostValue, serverValue, apiKeyValue, apiSecretValue, komodoUrlValue, setUserDoc]); 183 + 184 + 185 + 186 + //const setOptByName = (name: string) => (e: React.ChangeEvent<HTMLInputElement>) => setCompose({...composeState, [name]: e.currentTarget.checked}); 187 + 188 + return <Fragment> 189 + 190 + <h3>1. What <a href="../usage/overview#stack-sources">Stack Sources</a> to generate Stacks from?</h3> 191 + 192 + <p> 193 + <ButtonGroup 194 + options={[['dir','Directory'], ['compose','Compose Projects']]} 195 + defaultValue="dir" 196 + value={composeState.stacksFrom} 197 + variant="primary" 198 + size="lg" 199 + onChange={(val) => setCompose({...composeState, stacksFrom: val as ('compose' | 'dir')})} 200 + /> 201 + </p> 202 + 203 + <span style={{display: composeState.stacksFrom === 'dir' ? 'inherit' : 'none'}}> 204 + <Admonition type="info"> 205 + For <strong>Directory</strong> (<CodeInline>STACKS_FROM=dir</CodeInline>) Komodo Import will try to generate Stacks from each <strong>subfolder</strong> inside the directory mounted into the container. 206 + </Admonition> 207 + </span> 208 + <span style={{display: composeState.stacksFrom === 'compose' ? 'inherit' : 'none'}}> 209 + <Admonition type="info"> 210 + For <strong>Compose Projects</strong> (<CodeInline>STACKS_FROM=compose</CodeInline>) Komodo Import will try to generate Stacks from existing projects on your machine that were created with <CodeInline>docker compose up</CodeInline> 211 + </Admonition> 212 + </span> 213 + 214 + <h3>2. Where are your existing projects located on your machine?</h3> 215 + 216 + <p> 217 + <label className="margin-right--sm" htmlFor="hostDir"><strong><a href="../usage/overview#host-directory">Host Directory:</a></strong></label> 218 + <input id="hostDir" type="text" placeholder="/home/myUser/homelab" onChange={handleHostInputChange} value={hostInputValue}/> 219 + </p> 220 + 221 + {composeState.stacksFrom === 'compose' ? <MDXContent><HostComposeSnippet/></MDXContent> : <MDXContent><HostSnippet/></MDXContent>} 222 + 223 + <h3>3. What is the name of this Komodo Server?</h3> 224 + 225 + <p>Once Komodo Periphery agent was added to this machine you connected it to Komodo as a <a href="https://komo.do/docs/resources#server"><strong>Server</strong></a>. Specify the name you gave it.</p> 226 + 227 + <p> 228 + <label className="margin-right--sm" htmlFor="serverName"><strong>Server Name:</strong></label> 229 + <input id="serverName" type="text" placeholder="my-cool-server" onChange={handleServerInputChange} value={serverInputValue}/> 230 + </p> 231 + 232 + <h3>4. Where should generated Stacks be created?</h3> 233 + 234 + <p>Choose one or more <a href="../usage/overview#outputs-1">Outputs</a>:</p> 235 + 236 + <p><Button className="margin-right--md" label="Console" link="#" variant="primary" disabled/> <Button label="Komodo" link="#" variant="primary" 237 + outline={!composeState.api} 238 + onClick={komodoApiCallback}/></p> 239 + 240 + <p>Generated Stacks are <strong>always</strong> output to docker logs as a <a href="../usage/resourceSync#create-sync-resource">Sync Resource</a>.</p> 241 + 242 + <MDXContent><ConsoleOutput/></MDXContent> 243 + 244 + <div style={{display: composeState.api ? 'inherit' : 'none'}}> 245 + <p>Komodo Import can, additionally, create a <a href="../usage/resourceSync#create-sync-resource">Sync Resource</a> with the generated Stacks in Komodo for you. This method <strong>only</strong> creates the Resource, it does not execute or change your existing Stacks.</p> 246 + 247 + <p>Create an <a href="../usage/overview#api-sync">API Key and Secret</a>, then specify them below.</p> 248 + <p> 249 + <label className="margin-right--sm" htmlFor="url"><strong>Komodo URL:</strong></label> 250 + <input id="url" type="text" placeholder="http://192.168.0.101:8120" onChange={handleKomodoUrlInputChange} value={komodoUrlInputValue}/> 251 + </p> 252 + <p> 253 + <label className="margin-right--sm" htmlFor="apiKey"><strong>Komodo API Key:</strong></label> 254 + <input id="apiKey" type="text" placeholder="K-3A6btIPZYeBuD5ebSa9uD5ebuD5ebHIjYxT5sc" onChange={handleApikeyInputChange} value={apiKeyInputValue}/> 255 + </p> 256 + <p> 257 + <label className="margin-right--sm" htmlFor="apiSecret"><strong>Komodo API Secret:</strong></label> 258 + <input id="apiSecret" type="text" placeholder="S-qnaXD1frutYlfC2ZYlfCSzqiYlfC1WYlfCVR34Yj4" onChange={handleApiSecretInputChange} value={apiSecretInputValue}/> 259 + </p> 260 + <MDXContent><ApiOutput/></MDXContent> 261 + </div> 262 + 263 + <h3>5. Save your customized Compose file</h3> 264 + 265 + <p>The file below has been customized for your specific machine! Save this somewhere on the machine:</p> 266 + 267 + {/* <Checkbox id="autoUpdate" checked={composeState.autoUpdate} onChange={setOptByName('autoUpdate')}>Auto Update</Checkbox> 268 + <Checkbox id="pollUpdate" checked={composeState.pollUpdate} onChange={setOptByName('pollUpdate')}>Poll For Update?</Checkbox> */} 269 + <CodeBlock title="compose.yaml" language="yaml">{userDoc.toString()}</CodeBlock> 270 + </Fragment>; 271 + } 272 + 273 + const WrappedQuickstartCompose = (props: AIOProps) => { 274 + return <ErrorBoundary 275 + fallback={({error}) => ( 276 + <div> 277 + <Admonition type="danger"> 278 + <p>Interactive Quickstart crashed because of error! Sorry.</p> 279 + <CodeBlock>{error.stack}</CodeBlock> 280 + </Admonition> 281 + </div> 282 + )} 283 + ><QuickstartCompose {...props} /></ErrorBoundary> 284 + } 285 + 286 + 287 + export default WrappedQuickstartCompose;
+7
docsite/src/components/compose.yaml
··· 1 + services: 2 + komodo-import: 3 + image: foxxmd/komodo-import:latest 4 + restart: no 5 + volumes: 6 + - ./home/myUser/homelab:/filesOnServer 7 + environment:
+11
docsite/src/components/snippets/_composeExample.mdx
··· 1 + import TreeCompose from './_composeTree.mdx'; 2 + 3 + `docker compose ls` reveals these projects: 4 + 5 + ``` 6 + NAME STATUS CONFIG FILES 7 + immich running /home/myUser/homelab/immich/compose.yaml 8 + plex running /home/myUser/homelab/plex/docker-compose.yaml 9 + project1 running /home/myUser/localDev/myProjects/project1/compose.yaml 10 + ``` 11 + <TreeCompose/>
+16
docsite/src/components/snippets/_composeTree.mdx
··· 1 + ``` 2 + . 3 + └── /home/myUser 4 + ├── homeLab 5 + │ ├── immich 6 + │ │ └── compose.yaml 7 + │ ├── plex 8 + │ │ ├── docker-compose.yaml 9 + │ │ └── .git 10 + │ └── owncloud 11 + │ └── compose.yaml 12 + └── localDev 13 + └── myProjects 14 + └── project1 15 + └── compose.yaml 16 + ```
+11
docsite/src/components/snippets/_directoryTree.mdx
··· 1 + ``` 2 + . 3 + └── /home/myUser/homelab/ 4 + ├── immich/ 5 + │ └── compose.yaml 6 + ├── plex/ 7 + │ ├── docker-compose.yaml 8 + │ └── .git 9 + └── uptime-kuma/ 10 + └── compose.yaml 11 + ```
+13
docsite/src/components/socket-proxy.yaml
··· 1 + socket-proxy: 2 + image: lscr.io/linuxserver/socket-proxy:latest 3 + environment: 4 + - CONTAINERS=1 5 + - INFO=1 6 + - POST=0 7 + - PING=1 8 + - VERSION=1 9 + volumes: 10 + - /var/run/docker.sock:/var/run/docker.sock:ro 11 + read_only: true 12 + tmpfs: 13 + - /run
+3 -3
docsite/src/pages/index.mdx
··· 20 20 21 21 * Generate Resources from... 22 22 * [x] Stack 23 - * [x] [Files On Server](./docs/usage/overview#files-on-server) Stacks from plain folders 24 - * [x] [Git-based](./docs/usage/overview#git-repo) Stacks from folders containing git repository 23 + * [x] [Files On Server](./docs/usage/overview#stacks) Stacks from plain folders 24 + * [x] [Git-based](./docs/usage/overview#git-repo-stacks) Stacks from folders containing git repository 25 + * [x] Use running [Docker Compose projects](./docs/usage/overview?stacksFrom=compose#stack-sources) or a [specific directory](./docs/usage/overview?stacksFrom=dir#stack-sources) of compose projects 25 26 * Generate To... 26 27 * [x] TOML 27 28 * [x] in [Docker Logs](./docs/usage/overview#console) 28 - * [x] in [File](./docs/usage/overview#file) 29 29 * [x] Import directly with Komodo API 30 30 * [x] [Create/modify Resource Sync](./docs/usage/overview#api-sync) 31 31 * [ ] Create/modify Stacks
+7 -7
example/compose.yaml
··· 1 1 services: 2 2 komodo-import: 3 - image: foxxmd/komodo-import:edge 3 + image: foxxmd/komodo-import:latest 4 4 volumes: 5 - ## ParentDirectory:FILES_ON_SERVER_DIR 5 + ## The top-level folder compose projects on the host live in 6 6 - .:/filesOnServer 7 7 8 - ## optional, TOML is saved to file here 9 - #- ./config:/config 10 8 environment: 11 - - TZ=America/New_York 9 + ## Same path as the host folder mounted above 10 + - HOST_DIR=/my/cool/path 12 11 ## Komodo Server name to use for generated Stacks 13 12 - SERVER_NAME=my-cool-server 14 - ## ParentDirectory on the host use as Stack Run Directory prefix 15 - - HOST_PARENT_PATH=/my/cool/path 13 + 14 + - TZ=America/New_York 15 + 16 16 restart: no
+573 -29
package-lock.json
··· 16 16 "common-tags": "^1.8.2", 17 17 "dayjs": "^1.11.10", 18 18 "deepmerge": "^4.3.1", 19 + "dockerode": "^4.0.7", 19 20 "dotenv": "^10.0.0", 20 21 "execa": "^9.6.0", 21 22 "glob": "^11.0.3", 22 23 "komodo_client": "^1.18.4", 24 + "minimatch": "^10.0.3", 23 25 "normalize-url": "^8.0.2", 24 26 "object-hash": "^3.0.0", 25 27 "p-event": "^6.0.0", ··· 59 61 "node": ">=18.19.1", 60 62 "npm": ">=10.2.4" 61 63 } 64 + }, 65 + "node_modules/@balena/dockerignore": { 66 + "version": "1.0.2", 67 + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", 68 + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", 69 + "license": "Apache-2.0" 62 70 }, 63 71 "node_modules/@dword-design/chdir": { 64 72 "version": "2.1.4", ··· 546 554 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 547 555 } 548 556 }, 557 + "node_modules/@eslint/config-array/node_modules/minimatch": { 558 + "version": "3.1.2", 559 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 560 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 561 + "dev": true, 562 + "license": "ISC", 563 + "dependencies": { 564 + "brace-expansion": "^1.1.7" 565 + }, 566 + "engines": { 567 + "node": "*" 568 + } 569 + }, 549 570 "node_modules/@eslint/config-helpers": { 550 571 "version": "0.3.1", 551 572 "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", ··· 591 612 }, 592 613 "funding": { 593 614 "url": "https://opencollective.com/eslint" 615 + } 616 + }, 617 + "node_modules/@eslint/eslintrc/node_modules/minimatch": { 618 + "version": "3.1.2", 619 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 620 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 621 + "dev": true, 622 + "license": "ISC", 623 + "dependencies": { 624 + "brace-expansion": "^1.1.7" 625 + }, 626 + "engines": { 627 + "node": "*" 594 628 } 595 629 }, 596 630 "node_modules/@eslint/js": { ··· 693 727 "npm": ">=9.3.0" 694 728 } 695 729 }, 730 + "node_modules/@grpc/grpc-js": { 731 + "version": "1.13.4", 732 + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", 733 + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", 734 + "license": "Apache-2.0", 735 + "dependencies": { 736 + "@grpc/proto-loader": "^0.7.13", 737 + "@js-sdsl/ordered-map": "^4.4.2" 738 + }, 739 + "engines": { 740 + "node": ">=12.10.0" 741 + } 742 + }, 743 + "node_modules/@grpc/proto-loader": { 744 + "version": "0.7.15", 745 + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", 746 + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", 747 + "license": "Apache-2.0", 748 + "dependencies": { 749 + "lodash.camelcase": "^4.3.0", 750 + "long": "^5.0.0", 751 + "protobufjs": "^7.2.5", 752 + "yargs": "^17.7.2" 753 + }, 754 + "bin": { 755 + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" 756 + }, 757 + "engines": { 758 + "node": ">=6" 759 + } 760 + }, 761 + "node_modules/@grpc/proto-loader/node_modules/ansi-regex": { 762 + "version": "5.0.1", 763 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 764 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 765 + "license": "MIT", 766 + "engines": { 767 + "node": ">=8" 768 + } 769 + }, 770 + "node_modules/@grpc/proto-loader/node_modules/cliui": { 771 + "version": "8.0.1", 772 + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 773 + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 774 + "license": "ISC", 775 + "dependencies": { 776 + "string-width": "^4.2.0", 777 + "strip-ansi": "^6.0.1", 778 + "wrap-ansi": "^7.0.0" 779 + }, 780 + "engines": { 781 + "node": ">=12" 782 + } 783 + }, 784 + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { 785 + "version": "8.0.0", 786 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 787 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 788 + "license": "MIT" 789 + }, 790 + "node_modules/@grpc/proto-loader/node_modules/string-width": { 791 + "version": "4.2.3", 792 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 793 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 794 + "license": "MIT", 795 + "dependencies": { 796 + "emoji-regex": "^8.0.0", 797 + "is-fullwidth-code-point": "^3.0.0", 798 + "strip-ansi": "^6.0.1" 799 + }, 800 + "engines": { 801 + "node": ">=8" 802 + } 803 + }, 804 + "node_modules/@grpc/proto-loader/node_modules/strip-ansi": { 805 + "version": "6.0.1", 806 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 807 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 808 + "license": "MIT", 809 + "dependencies": { 810 + "ansi-regex": "^5.0.1" 811 + }, 812 + "engines": { 813 + "node": ">=8" 814 + } 815 + }, 816 + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { 817 + "version": "7.0.0", 818 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 819 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 820 + "license": "MIT", 821 + "dependencies": { 822 + "ansi-styles": "^4.0.0", 823 + "string-width": "^4.1.0", 824 + "strip-ansi": "^6.0.0" 825 + }, 826 + "engines": { 827 + "node": ">=10" 828 + }, 829 + "funding": { 830 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 831 + } 832 + }, 833 + "node_modules/@grpc/proto-loader/node_modules/yargs": { 834 + "version": "17.7.2", 835 + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 836 + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 837 + "license": "MIT", 838 + "dependencies": { 839 + "cliui": "^8.0.1", 840 + "escalade": "^3.1.1", 841 + "get-caller-file": "^2.0.5", 842 + "require-directory": "^2.1.1", 843 + "string-width": "^4.2.3", 844 + "y18n": "^5.0.5", 845 + "yargs-parser": "^21.1.1" 846 + }, 847 + "engines": { 848 + "node": ">=12" 849 + } 850 + }, 851 + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { 852 + "version": "21.1.1", 853 + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 854 + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 855 + "license": "ISC", 856 + "engines": { 857 + "node": ">=12" 858 + } 859 + }, 696 860 "node_modules/@humanfs/core": { 697 861 "version": "0.19.1", 698 862 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 797 961 "node": ">=12" 798 962 } 799 963 }, 964 + "node_modules/@js-sdsl/ordered-map": { 965 + "version": "4.4.2", 966 + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", 967 + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", 968 + "license": "MIT", 969 + "funding": { 970 + "type": "opencollective", 971 + "url": "https://opencollective.com/js-sdsl" 972 + } 973 + }, 800 974 "node_modules/@nodelib/fs.scandir": { 801 975 "version": "2.1.5", 802 976 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", ··· 834 1008 "engines": { 835 1009 "node": ">= 8" 836 1010 } 1011 + }, 1012 + "node_modules/@protobufjs/aspromise": { 1013 + "version": "1.1.2", 1014 + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 1015 + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", 1016 + "license": "BSD-3-Clause" 1017 + }, 1018 + "node_modules/@protobufjs/base64": { 1019 + "version": "1.1.2", 1020 + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 1021 + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", 1022 + "license": "BSD-3-Clause" 1023 + }, 1024 + "node_modules/@protobufjs/codegen": { 1025 + "version": "2.0.4", 1026 + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 1027 + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", 1028 + "license": "BSD-3-Clause" 1029 + }, 1030 + "node_modules/@protobufjs/eventemitter": { 1031 + "version": "1.1.0", 1032 + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 1033 + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", 1034 + "license": "BSD-3-Clause" 1035 + }, 1036 + "node_modules/@protobufjs/fetch": { 1037 + "version": "1.1.0", 1038 + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 1039 + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 1040 + "license": "BSD-3-Clause", 1041 + "dependencies": { 1042 + "@protobufjs/aspromise": "^1.1.1", 1043 + "@protobufjs/inquire": "^1.1.0" 1044 + } 1045 + }, 1046 + "node_modules/@protobufjs/float": { 1047 + "version": "1.0.2", 1048 + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 1049 + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", 1050 + "license": "BSD-3-Clause" 1051 + }, 1052 + "node_modules/@protobufjs/inquire": { 1053 + "version": "1.1.0", 1054 + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 1055 + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", 1056 + "license": "BSD-3-Clause" 1057 + }, 1058 + "node_modules/@protobufjs/path": { 1059 + "version": "1.1.2", 1060 + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 1061 + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", 1062 + "license": "BSD-3-Clause" 1063 + }, 1064 + "node_modules/@protobufjs/pool": { 1065 + "version": "1.1.0", 1066 + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 1067 + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", 1068 + "license": "BSD-3-Clause" 1069 + }, 1070 + "node_modules/@protobufjs/utf8": { 1071 + "version": "1.1.0", 1072 + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 1073 + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", 1074 + "license": "BSD-3-Clause" 837 1075 }, 838 1076 "node_modules/@sec-ant/readable-stream": { 839 1077 "version": "0.4.1", ··· 1161 1399 "version": "20.19.10", 1162 1400 "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", 1163 1401 "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", 1164 - "dev": true, 1165 1402 "license": "MIT", 1166 1403 "dependencies": { 1167 1404 "undici-types": "~6.21.0" ··· 1945 2182 "dev": true, 1946 2183 "license": "Python-2.0" 1947 2184 }, 2185 + "node_modules/asn1": { 2186 + "version": "0.2.6", 2187 + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", 2188 + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", 2189 + "license": "MIT", 2190 + "dependencies": { 2191 + "safer-buffer": "~2.1.0" 2192 + } 2193 + }, 1948 2194 "node_modules/assertion-error": { 1949 2195 "version": "1.1.0", 1950 2196 "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", ··· 2014 2260 ], 2015 2261 "license": "MIT" 2016 2262 }, 2263 + "node_modules/bcrypt-pbkdf": { 2264 + "version": "1.0.2", 2265 + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 2266 + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", 2267 + "license": "BSD-3-Clause", 2268 + "dependencies": { 2269 + "tweetnacl": "^0.14.3" 2270 + } 2271 + }, 2017 2272 "node_modules/binary-extensions": { 2018 2273 "version": "2.3.0", 2019 2274 "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", ··· 2027 2282 "url": "https://github.com/sponsors/sindresorhus" 2028 2283 } 2029 2284 }, 2285 + "node_modules/bl": { 2286 + "version": "4.1.0", 2287 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 2288 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 2289 + "license": "MIT", 2290 + "dependencies": { 2291 + "buffer": "^5.5.0", 2292 + "inherits": "^2.0.4", 2293 + "readable-stream": "^3.4.0" 2294 + } 2295 + }, 2296 + "node_modules/bl/node_modules/buffer": { 2297 + "version": "5.7.1", 2298 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 2299 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 2300 + "funding": [ 2301 + { 2302 + "type": "github", 2303 + "url": "https://github.com/sponsors/feross" 2304 + }, 2305 + { 2306 + "type": "patreon", 2307 + "url": "https://www.patreon.com/feross" 2308 + }, 2309 + { 2310 + "type": "consulting", 2311 + "url": "https://feross.org/support" 2312 + } 2313 + ], 2314 + "license": "MIT", 2315 + "dependencies": { 2316 + "base64-js": "^1.3.1", 2317 + "ieee754": "^1.1.13" 2318 + } 2319 + }, 2320 + "node_modules/bl/node_modules/readable-stream": { 2321 + "version": "3.6.2", 2322 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 2323 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 2324 + "license": "MIT", 2325 + "dependencies": { 2326 + "inherits": "^2.0.3", 2327 + "string_decoder": "^1.1.1", 2328 + "util-deprecate": "^1.0.1" 2329 + }, 2330 + "engines": { 2331 + "node": ">= 6" 2332 + } 2333 + }, 2030 2334 "node_modules/brace-expansion": { 2031 2335 "version": "1.1.12", 2032 2336 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", ··· 2080 2384 "dependencies": { 2081 2385 "base64-js": "^1.3.1", 2082 2386 "ieee754": "^1.2.1" 2387 + } 2388 + }, 2389 + "node_modules/buildcheck": { 2390 + "version": "0.0.6", 2391 + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", 2392 + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", 2393 + "optional": true, 2394 + "engines": { 2395 + "node": ">=10.0.0" 2083 2396 } 2084 2397 }, 2085 2398 "node_modules/call-bind": { ··· 2255 2568 "node": ">= 6" 2256 2569 } 2257 2570 }, 2571 + "node_modules/chownr": { 2572 + "version": "1.1.4", 2573 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 2574 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 2575 + "license": "ISC" 2576 + }, 2258 2577 "node_modules/clean-git-ref": { 2259 2578 "version": "2.0.1", 2260 2579 "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", ··· 2385 2704 "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 2386 2705 "dev": true, 2387 2706 "license": "MIT" 2707 + }, 2708 + "node_modules/cpu-features": { 2709 + "version": "0.0.10", 2710 + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", 2711 + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", 2712 + "hasInstallScript": true, 2713 + "optional": true, 2714 + "dependencies": { 2715 + "buildcheck": "~0.0.6", 2716 + "nan": "^2.19.0" 2717 + }, 2718 + "engines": { 2719 + "node": ">=10.0.0" 2720 + } 2388 2721 }, 2389 2722 "node_modules/crc-32": { 2390 2723 "version": "1.2.2", ··· 2548 2881 "dev": true, 2549 2882 "license": "MIT" 2550 2883 }, 2884 + "node_modules/docker-modem": { 2885 + "version": "5.0.6", 2886 + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", 2887 + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", 2888 + "license": "Apache-2.0", 2889 + "dependencies": { 2890 + "debug": "^4.1.1", 2891 + "readable-stream": "^3.5.0", 2892 + "split-ca": "^1.0.1", 2893 + "ssh2": "^1.15.0" 2894 + }, 2895 + "engines": { 2896 + "node": ">= 8.0" 2897 + } 2898 + }, 2899 + "node_modules/docker-modem/node_modules/readable-stream": { 2900 + "version": "3.6.2", 2901 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 2902 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 2903 + "license": "MIT", 2904 + "dependencies": { 2905 + "inherits": "^2.0.3", 2906 + "string_decoder": "^1.1.1", 2907 + "util-deprecate": "^1.0.1" 2908 + }, 2909 + "engines": { 2910 + "node": ">= 6" 2911 + } 2912 + }, 2913 + "node_modules/dockerode": { 2914 + "version": "4.0.7", 2915 + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.7.tgz", 2916 + "integrity": "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA==", 2917 + "license": "Apache-2.0", 2918 + "dependencies": { 2919 + "@balena/dockerignore": "^1.0.2", 2920 + "@grpc/grpc-js": "^1.11.1", 2921 + "@grpc/proto-loader": "^0.7.13", 2922 + "docker-modem": "^5.0.6", 2923 + "protobufjs": "^7.3.2", 2924 + "tar-fs": "~2.1.2", 2925 + "uuid": "^10.0.0" 2926 + }, 2927 + "engines": { 2928 + "node": ">= 8.0" 2929 + } 2930 + }, 2551 2931 "node_modules/dotenv": { 2552 2932 "version": "10.0.0", 2553 2933 "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", ··· 2671 3051 "version": "3.2.0", 2672 3052 "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 2673 3053 "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 2674 - "dev": true, 2675 3054 "license": "MIT", 2676 3055 "engines": { 2677 3056 "node": ">=6" ··· 2796 3175 }, 2797 3176 "funding": { 2798 3177 "url": "https://opencollective.com/eslint" 3178 + } 3179 + }, 3180 + "node_modules/eslint/node_modules/minimatch": { 3181 + "version": "3.1.2", 3182 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 3183 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 3184 + "dev": true, 3185 + "license": "ISC", 3186 + "dependencies": { 3187 + "brace-expansion": "^1.1.7" 3188 + }, 3189 + "engines": { 3190 + "node": "*" 2799 3191 } 2800 3192 }, 2801 3193 "node_modules/espree": { ··· 3133 3525 "url": "https://github.com/sponsors/isaacs" 3134 3526 } 3135 3527 }, 3528 + "node_modules/fs-constants": { 3529 + "version": "1.0.0", 3530 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 3531 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 3532 + "license": "MIT" 3533 + }, 3136 3534 "node_modules/fs.realpath": { 3137 3535 "version": "1.0.0", 3138 3536 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", ··· 3168 3566 "version": "2.0.5", 3169 3567 "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 3170 3568 "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 3171 - "dev": true, 3172 3569 "license": "ISC", 3173 3570 "engines": { 3174 3571 "node": "6.* || 8.* || >= 10.*" ··· 3293 3690 "node": ">=10.13.0" 3294 3691 } 3295 3692 }, 3296 - "node_modules/glob/node_modules/minimatch": { 3297 - "version": "10.0.3", 3298 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", 3299 - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", 3300 - "license": "ISC", 3301 - "dependencies": { 3302 - "@isaacs/brace-expansion": "^5.0.0" 3303 - }, 3304 - "engines": { 3305 - "node": "20 || >=22" 3306 - }, 3307 - "funding": { 3308 - "url": "https://github.com/sponsors/isaacs" 3309 - } 3310 - }, 3311 3693 "node_modules/globals": { 3312 3694 "version": "14.0.0", 3313 3695 "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", ··· 3511 3893 "version": "2.0.4", 3512 3894 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 3513 3895 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 3514 - "dev": true, 3515 3896 "license": "ISC" 3516 3897 }, 3517 3898 "node_modules/is-binary-path": { ··· 3798 4179 "funding": { 3799 4180 "url": "https://github.com/sponsors/sindresorhus" 3800 4181 } 4182 + }, 4183 + "node_modules/lodash.camelcase": { 4184 + "version": "4.3.0", 4185 + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 4186 + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", 4187 + "license": "MIT" 3801 4188 }, 3802 4189 "node_modules/lodash.merge": { 3803 4190 "version": "4.6.2", ··· 3823 4210 "url": "https://github.com/sponsors/sindresorhus" 3824 4211 } 3825 4212 }, 4213 + "node_modules/long": { 4214 + "version": "5.3.2", 4215 + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", 4216 + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", 4217 + "license": "Apache-2.0" 4218 + }, 3826 4219 "node_modules/loupe": { 3827 4220 "version": "2.3.7", 3828 4221 "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", ··· 3890 4283 } 3891 4284 }, 3892 4285 "node_modules/minimatch": { 3893 - "version": "3.1.2", 3894 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 3895 - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 3896 - "dev": true, 4286 + "version": "10.0.3", 4287 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", 4288 + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", 3897 4289 "license": "ISC", 3898 4290 "dependencies": { 3899 - "brace-expansion": "^1.1.7" 4291 + "@isaacs/brace-expansion": "^5.0.0" 3900 4292 }, 3901 4293 "engines": { 3902 - "node": "*" 4294 + "node": "20 || >=22" 4295 + }, 4296 + "funding": { 4297 + "url": "https://github.com/sponsors/isaacs" 3903 4298 } 3904 4299 }, 3905 4300 "node_modules/minimist": { ··· 3929 4324 "engines": { 3930 4325 "node": ">=16 || 14 >=14.17" 3931 4326 } 4327 + }, 4328 + "node_modules/mkdirp-classic": { 4329 + "version": "0.5.3", 4330 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 4331 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 4332 + "license": "MIT" 3932 4333 }, 3933 4334 "node_modules/mocha": { 3934 4335 "version": "10.8.2", ··· 4032 4433 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 4033 4434 "license": "MIT" 4034 4435 }, 4436 + "node_modules/nan": { 4437 + "version": "2.23.0", 4438 + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", 4439 + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", 4440 + "license": "MIT", 4441 + "optional": true 4442 + }, 4035 4443 "node_modules/natural-compare": { 4036 4444 "version": "1.4.0", 4037 4445 "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", ··· 4090 4498 "license": "MIT", 4091 4499 "engines": { 4092 4500 "node": ">=4" 4501 + } 4502 + }, 4503 + "node_modules/nodemon/node_modules/minimatch": { 4504 + "version": "3.1.2", 4505 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 4506 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 4507 + "dev": true, 4508 + "license": "ISC", 4509 + "dependencies": { 4510 + "brace-expansion": "^1.1.7" 4511 + }, 4512 + "engines": { 4513 + "node": "*" 4093 4514 } 4094 4515 }, 4095 4516 "node_modules/nodemon/node_modules/supports-color": { ··· 4549 4970 ], 4550 4971 "license": "MIT" 4551 4972 }, 4973 + "node_modules/protobufjs": { 4974 + "version": "7.5.4", 4975 + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", 4976 + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", 4977 + "hasInstallScript": true, 4978 + "license": "BSD-3-Clause", 4979 + "dependencies": { 4980 + "@protobufjs/aspromise": "^1.1.2", 4981 + "@protobufjs/base64": "^1.1.2", 4982 + "@protobufjs/codegen": "^2.0.4", 4983 + "@protobufjs/eventemitter": "^1.1.0", 4984 + "@protobufjs/fetch": "^1.1.0", 4985 + "@protobufjs/float": "^1.0.2", 4986 + "@protobufjs/inquire": "^1.1.0", 4987 + "@protobufjs/path": "^1.1.2", 4988 + "@protobufjs/pool": "^1.1.0", 4989 + "@protobufjs/utf8": "^1.1.0", 4990 + "@types/node": ">=13.7.0", 4991 + "long": "^5.0.0" 4992 + }, 4993 + "engines": { 4994 + "node": ">=12.0.0" 4995 + } 4996 + }, 4552 4997 "node_modules/pstree.remy": { 4553 4998 "version": "1.1.8", 4554 4999 "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", ··· 4655 5100 "version": "2.1.1", 4656 5101 "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 4657 5102 "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 4658 - "dev": true, 4659 5103 "license": "MIT", 4660 5104 "engines": { 4661 5105 "node": ">=0.10.0" ··· 4728 5172 }, 4729 5173 "funding": { 4730 5174 "url": "https://github.com/sponsors/isaacs" 5175 + } 5176 + }, 5177 + "node_modules/rimraf/node_modules/minimatch": { 5178 + "version": "3.1.2", 5179 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 5180 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 5181 + "dev": true, 5182 + "license": "ISC", 5183 + "dependencies": { 5184 + "brace-expansion": "^1.1.7" 5185 + }, 5186 + "engines": { 5187 + "node": "*" 4731 5188 } 4732 5189 }, 4733 5190 "node_modules/run-parallel": { ··· 4783 5240 "node": ">=10" 4784 5241 } 4785 5242 }, 5243 + "node_modules/safer-buffer": { 5244 + "version": "2.1.2", 5245 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 5246 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 5247 + "license": "MIT" 5248 + }, 4786 5249 "node_modules/secure-json-parse": { 4787 5250 "version": "2.7.0", 4788 5251 "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", ··· 4995 5458 "atomic-sleep": "^1.0.0" 4996 5459 } 4997 5460 }, 5461 + "node_modules/split-ca": { 5462 + "version": "1.0.1", 5463 + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", 5464 + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", 5465 + "license": "ISC" 5466 + }, 4998 5467 "node_modules/split2": { 4999 5468 "version": "4.2.0", 5000 5469 "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", ··· 5004 5473 "node": ">= 10.x" 5005 5474 } 5006 5475 }, 5476 + "node_modules/ssh2": { 5477 + "version": "1.16.0", 5478 + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", 5479 + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", 5480 + "hasInstallScript": true, 5481 + "dependencies": { 5482 + "asn1": "^0.2.6", 5483 + "bcrypt-pbkdf": "^1.0.2" 5484 + }, 5485 + "engines": { 5486 + "node": ">=10.16.0" 5487 + }, 5488 + "optionalDependencies": { 5489 + "cpu-features": "~0.0.10", 5490 + "nan": "^2.20.0" 5491 + } 5492 + }, 5007 5493 "node_modules/string_decoder": { 5008 5494 "version": "1.3.0", 5009 5495 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", ··· 5146 5632 "node": ">=8" 5147 5633 } 5148 5634 }, 5635 + "node_modules/tar-fs": { 5636 + "version": "2.1.3", 5637 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", 5638 + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", 5639 + "license": "MIT", 5640 + "dependencies": { 5641 + "chownr": "^1.1.1", 5642 + "mkdirp-classic": "^0.5.2", 5643 + "pump": "^3.0.0", 5644 + "tar-stream": "^2.1.4" 5645 + } 5646 + }, 5647 + "node_modules/tar-stream": { 5648 + "version": "2.2.0", 5649 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 5650 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 5651 + "license": "MIT", 5652 + "dependencies": { 5653 + "bl": "^4.0.3", 5654 + "end-of-stream": "^1.4.1", 5655 + "fs-constants": "^1.0.0", 5656 + "inherits": "^2.0.3", 5657 + "readable-stream": "^3.1.1" 5658 + }, 5659 + "engines": { 5660 + "node": ">=6" 5661 + } 5662 + }, 5663 + "node_modules/tar-stream/node_modules/readable-stream": { 5664 + "version": "3.6.2", 5665 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 5666 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 5667 + "license": "MIT", 5668 + "dependencies": { 5669 + "inherits": "^2.0.3", 5670 + "string_decoder": "^1.1.1", 5671 + "util-deprecate": "^1.0.1" 5672 + }, 5673 + "engines": { 5674 + "node": ">= 6" 5675 + } 5676 + }, 5149 5677 "node_modules/thread-stream": { 5150 5678 "version": "3.1.0", 5151 5679 "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", ··· 5269 5797 "fsevents": "~2.3.3" 5270 5798 } 5271 5799 }, 5800 + "node_modules/tweetnacl": { 5801 + "version": "0.14.5", 5802 + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 5803 + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", 5804 + "license": "Unlicense" 5805 + }, 5272 5806 "node_modules/type-check": { 5273 5807 "version": "0.4.0", 5274 5808 "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", ··· 5485 6019 "version": "6.21.0", 5486 6020 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 5487 6021 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 5488 - "dev": true, 5489 6022 "license": "MIT" 5490 6023 }, 5491 6024 "node_modules/unicorn-magic": { ··· 5514 6047 "version": "1.0.2", 5515 6048 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 5516 6049 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 5517 - "dev": true, 5518 6050 "license": "MIT" 6051 + }, 6052 + "node_modules/uuid": { 6053 + "version": "10.0.0", 6054 + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", 6055 + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", 6056 + "funding": [ 6057 + "https://github.com/sponsors/broofa", 6058 + "https://github.com/sponsors/ctavan" 6059 + ], 6060 + "license": "MIT", 6061 + "bin": { 6062 + "uuid": "dist/bin/uuid" 6063 + } 5519 6064 }, 5520 6065 "node_modules/which": { 5521 6066 "version": "2.0.2", ··· 5686 6231 "version": "5.0.8", 5687 6232 "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 5688 6233 "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 5689 - "dev": true, 5690 6234 "license": "ISC", 5691 6235 "engines": { 5692 6236 "node": ">=10"
+2
package.json
··· 38 38 "common-tags": "^1.8.2", 39 39 "dayjs": "^1.11.10", 40 40 "deepmerge": "^4.3.1", 41 + "dockerode": "^4.0.7", 41 42 "dotenv": "^10.0.0", 42 43 "execa": "^9.6.0", 43 44 "glob": "^11.0.3", 44 45 "komodo_client": "^1.18.4", 46 + "minimatch": "^10.0.3", 45 47 "normalize-url": "^8.0.2", 46 48 "object-hash": "^3.0.0", 47 49 "p-event": "^6.0.0",
+38
src/builders/stack/composeUtils.ts
··· 1 + import { childLogger, Logger } from "@foxxmd/logging"; 2 + import { Container } from "../../common/dockerApi.js"; 3 + import { StackCandidate, StackCandidateCompose } from "../../common/infrastructure/atomic.js"; 4 + 5 + export const consolidateComposeStacks = (containers: Container[], parentLogger: Logger): StackCandidateCompose[] => { 6 + const stacks: Record<string, StackCandidateCompose> = {}; 7 + const logger = childLogger(parentLogger, 'Compose Parser') 8 + 9 + logger.verbose('Finding Compose projects for reference...'); 10 + 11 + for (const c of containers) { 12 + const name = c.Labels['com.docker.compose.project']; 13 + if (name !== undefined && stacks[name] === undefined) { 14 + const candidate: StackCandidateCompose = { 15 + discovered: 'compose', 16 + projectName: name, 17 + workingDir: c.Labels['com.docker.compose.project.working_dir'], 18 + path: c.Labels['com.docker.compose.project.working_dir'], 19 + composeFilePaths: c.Labels['com.docker.compose.project.config_files'].split(','), 20 + state: c.State 21 + } 22 + let ok = true; 23 + for (const [k, v] of Object.entries(candidate)) { 24 + if (v === undefined) { 25 + logger.warn(`Cannot use ${name} compose project because ${k} label is missing`); 26 + ok = false; 27 + } 28 + } 29 + if (!ok) { 30 + continue; 31 + } 32 + stacks[name] = candidate; 33 + } 34 + } 35 + const s = Object.values(stacks); 36 + logger.verbose(`Found ${s.length} candidate Compose projects`); 37 + return s; 38 + }
+11 -4
src/builders/stack/filesOnServer.ts
··· 4 4 import { parse, ParsedPath, sep, join } from 'path'; 5 5 import { TomlStack } from "../../common/infrastructure/tomlObjects.js"; 6 6 import { readText } from "../../common/utils/io.js"; 7 - import { isDebugMode, removeUndefinedKeys } from "../../common/utils/utils.js"; 7 + import { isDebugMode, removeRootPathSeparator, removeUndefinedKeys } from "../../common/utils/utils.js"; 8 8 import { DEFAULT_COMPOSE_GLOB, DEFAULT_ENV_GLOB, parseEnvConfig, selectComposeFiles, selectEnvFiles } from "./stackUtils.js"; 9 9 10 10 export type BuildFileStackOptions = FilesOnServerConfig & { logger: Logger }; ··· 21 21 pollForUpdate, 22 22 server, 23 23 hostParentPath, 24 + composeFiles = [], 25 + projectName, 24 26 writeEnv = false, 25 27 } = options; 26 28 ··· 36 38 37 39 try { 38 40 stack = { 39 - name: folderName, 41 + name: projectName ?? folderName, 40 42 config: { 41 43 server, 42 44 run_directory: join(hostParentPath, folderName), ··· 48 50 } 49 51 }; 50 52 51 - stack.config.file_paths = await selectComposeFiles(composeFileGlob, path, logger); 53 + if(composeFiles.length > 0) { 54 + stack.config.file_paths = composeFiles.map(x => removeRootPathSeparator(x.replace(path, ''))); 55 + } else { 56 + stack.config.file_paths = await selectComposeFiles(composeFileGlob, path, logger); 57 + } 58 + 52 59 53 60 stack.config = { 54 61 ...stack.config, ··· 68 75 throw new Error(`Error occurred while processing Stack for folder ${folderName}`, { cause: e }); 69 76 } finally { 70 77 if (logJson) { 71 - logger.debug(`Stack Config: ${JSON.stringify(stack)}}`); 78 + logger.debug(`Stack Config: ${JSON.stringify(stack)}`); 72 79 } 73 80 } 74 81 }
+69 -54
src/builders/stack/gitStack.ts
··· 1 1 import { Logger, childLogger } from "@foxxmd/logging"; 2 - import { FilesOnServerConfig, GitStackConfig, GitStackLinkedConfig, GitStackStandaloneConfig } from "../../common/infrastructure/config/stackConfig.js"; 2 + import { GitStackConfig } from "../../common/infrastructure/config/stackConfig.js"; 3 3 import { _PartialStackConfig } from 'komodo_client/dist/types.js'; 4 - import { parse, ParsedPath, sep, join } from 'path'; 4 + import { parse, ParsedPath, sep, join, dirname, resolve } from 'path'; 5 5 import { TomlStack } from "../../common/infrastructure/tomlObjects.js"; 6 - import { dirHasGitConfig, findFilesRecurive, readDirectories, readText, sortComposePaths } from "../../common/utils/io.js"; 7 - import { stripIndents } from "common-tags"; 8 - import { isDebugMode, removeUndefinedKeys } from "../../common/utils/utils.js"; 6 + import { findPathRecuriveParently } from "../../common/utils/io.js"; 7 + import { isDebugMode, removeRootPathSeparator, removeUndefinedKeys } from "../../common/utils/utils.js"; 9 8 import { DEFAULT_COMPOSE_GLOB, DEFAULT_ENV_GLOB, parseEnvConfig, selectComposeFiles, selectEnvFiles } from "./stackUtils.js"; 10 9 import { detectGitRepo, GitRepoData, komodoRepoFromRemote, matchGitDataWithKomodo, matchRemote, RemoteInfo } from "../../common/utils/git.js"; 11 10 import { SimpleError } from "../../common/errors.js"; 12 - import { readFile } from "fs/promises"; 13 11 14 12 export type BuildGitStackOptions = GitStackConfig & { logger: Logger }; 15 13 ··· 24 22 autoUpdate, 25 23 pollForUpdate, 26 24 server, 27 - inMonorepo = false, 28 - hostParentPath, 25 + projectName, 26 + composeFiles = [], 29 27 writeEnv = false, 30 28 } = options 31 29 32 - let gitStackConfig: _PartialStackConfig; 30 + let gitStackConfig: _PartialStackConfig = {} 33 31 34 32 const pathInfo: ParsedPath = parse(path); 35 33 36 34 const folderName = `${pathInfo.name}${pathInfo.ext !== '' ? pathInfo.ext : ''}`; 37 35 38 - const monoRepoPath = inMonorepo ? (hostParentPath !== undefined ? join(hostParentPath, folderName) : undefined) : undefined; 36 + let repoRunDir: string; 39 37 40 38 const logger = childLogger(options.logger, [folderName, 'Git']); 41 39 42 - if (inMonorepo === false) { 40 + let nonGitReasons: string[] = []; 43 41 44 - let gitData: GitRepoData; 45 - try { 46 - gitData = await detectGitRepo(path); 47 - } catch (e) { 48 - if (e instanceof SimpleError) { 49 - throw new SimpleError(`Unable to parse path as git repo: ${e.message}`); 50 - } else { 51 - throw e; 42 + let gitData: GitRepoData; 43 + try { 44 + gitData = await detectGitRepo(path); 45 + logger.info(`Current directory is a Git repo: Branch ${gitData[0].branch} | Remote ${gitData[1].remote} | URL ${gitData[1].url}`); 46 + } catch (e) { 47 + if (e instanceof SimpleError) { 48 + nonGitReasons.push(`Current dir is not a git repo => ${e.message}`); 49 + } else { 50 + throw e; 51 + } 52 + } 53 + 54 + if(gitData === undefined) { 55 + const parentGitPath = await findPathRecuriveParently(path, '.git'); 56 + if (parentGitPath !== undefined) { 57 + const parentGitDir = dirname(parentGitPath) 58 + try { 59 + gitData = await detectGitRepo(parentGitDir); 60 + logger.info(`Detected parent path ${parentGitDir} is a Git repo: Branch ${gitData[0].branch} | Remote ${gitData[1].remote} | URL ${gitData[1].url}`); 61 + logger.info('Will treat current directory as the run directory for this repo'); 62 + repoRunDir = removeRootPathSeparator(path.replace(parentGitDir, '')); 63 + gitStackConfig.run_directory = repoRunDir; 64 + } catch (e) { 65 + if (e instanceof SimpleError) { 66 + // really shouldn't get here... 67 + nonGitReasons.push(`Parent dir ${parentGitDir} is not a git repo => ${e.message}`); 68 + } else { 69 + throw e; 70 + } 52 71 } 53 72 } 54 - logger.info(`Folder has tracked branch '${gitData[0].branch}' with valid remote '${gitData[1].remote}'`); 73 + } 74 + 75 + if(gitData === undefined) { 76 + throw new SimpleError(`Could not parse as a git repo: ${nonGitReasons.join(' | ')}`); 77 + } 55 78 56 - const [provider, repo, repoHint] = await matchGitDataWithKomodo(gitData); 79 + const [provider, repo, repoHint] = await matchGitDataWithKomodo(gitData); 80 + if (repo === undefined) { 81 + logger.verbose(`No linked repo because ${repoHint}`); 82 + const [domain, repo] = komodoRepoFromRemote(gitData[1].url); 57 83 if (repo === undefined) { 58 - logger.verbose(`No linked repo because ${repoHint}`); 59 - const [domain, repo] = komodoRepoFromRemote(gitData[1].url); 60 - if (repo === undefined) { 61 - throw new Error(`Could not parse repo from Remote URL ${gitData[1].url}`); 62 - } 63 - logger.debug(`Parsed Repo '${repo}' with ${provider?.domain !== undefined ? `provider` : 'URL'} domain '${provider?.domain ?? domain}' from Remote URL ${gitData[1].url}`); 64 - if (provider?.username !== undefined) { 65 - logger.debug(`Using provider username ${provider.username}`); 66 - } 67 - gitStackConfig = { 68 - git_provider: provider?.domain ?? domain, 69 - git_account: provider?.username, 70 - repo 71 - }; 72 - } else { 73 - logger.verbose(`Using linked repo ${repo.name}}`); 74 - gitStackConfig = { 75 - linked_repo: repo.name, 76 - } 84 + throw new Error(`Could not parse repo from Remote URL ${gitData[1].url}`); 85 + } 86 + logger.debug(`Parsed Repo '${repo}' with ${provider?.domain !== undefined ? `provider` : 'URL'} domain '${provider?.domain ?? domain}' from Remote URL ${gitData[1].url}`); 87 + if (provider?.username !== undefined) { 88 + logger.debug(`Using provider username ${provider.username}`); 77 89 } 90 + gitStackConfig = { 91 + ...gitStackConfig, 92 + git_provider: provider?.domain ?? domain, 93 + git_account: provider?.username, 94 + repo 95 + }; 78 96 } else { 79 - gitStackConfig = removeUndefinedKeys( 80 - { 81 - linked_repo: (options as GitStackLinkedConfig).linked_repo, 82 - git_provider: (options as GitStackStandaloneConfig).git_provider, 83 - git_account: (options as GitStackStandaloneConfig).git_account, 84 - repo: (options as GitStackStandaloneConfig).repo, 85 - run_directory: hostParentPath !== undefined ? join(hostParentPath, folderName) : undefined 86 - } 87 - ) 97 + logger.verbose(`Using linked repo ${repo.name}}`); 98 + gitStackConfig. linked_repo = repo.name; 88 99 } 89 100 90 101 let stack: TomlStack; ··· 92 103 93 104 try { 94 105 stack = { 95 - name: folderName, 106 + name: projectName ?? folderName, 96 107 config: { 97 108 server, 98 109 registry_account: imageRegistryAccount, ··· 103 114 } 104 115 }; 105 116 106 - const composePaths = await selectComposeFiles(composeFileGlob, path, logger); 107 - if(composePaths !== undefined) { 108 - stack.config.file_paths = composePaths.map(x => inMonorepo ? join(monoRepoPath, x) : x); 117 + if(composeFiles.length > 0) { 118 + stack.config.file_paths = composeFiles.map(x => removeRootPathSeparator(x.replace(path, ''))); 119 + } else { 120 + const composePaths = await selectComposeFiles(composeFileGlob, path, logger); 121 + if(composePaths !== undefined) { 122 + stack.config.file_paths = composePaths; 123 + } 109 124 } 110 125 111 126 stack.config = { ··· 113 128 ...await(parseEnvConfig(path, { 114 129 envFileGlob, 115 130 writeEnv, 116 - pathPrefix: monoRepoPath, 131 + pathPrefix: repoRunDir, 117 132 komodoEnvName, 118 133 logger 119 134 }))
+115 -115
src/builders/stack/stackBuilder.ts
··· 1 - import { AnyStackConfig, FilesOnServerConfig, GitStackConfig } from "../../common/infrastructure/config/stackConfig.js"; 1 + import { AnyStackConfig } from "../../common/infrastructure/config/stackConfig.js"; 2 2 import { TomlStack } from "../../common/infrastructure/tomlObjects.js"; 3 - import { promises } from 'fs'; 4 - import { findFolders, pathExistsAndIsReadable } from "../../common/utils/io.js"; 3 + import { findFolders } from "../../common/utils/io.js"; 5 4 import { childLogger, Logger } from "@foxxmd/logging"; 6 - import { formatIntoColumns, isUndefinedOrEmptyString, parseBool } from "../../common/utils/utils.js"; 7 - import { detectGitRepo, GitRepoData, komodoRepoFromRemote, matchGitDataWithKomodo } from "../../common/utils/git.js"; 5 + import { formatIntoColumns, parseBool } from "../../common/utils/utils.js"; 8 6 import { buildGitStack } from "./gitStack.js"; 9 7 import { join as joinPath, parse, ParsedPath } from 'path'; 10 - import { DEFAULT_COMPOSE_GLOB, DEFAULT_ENV_GLOB } from "./stackUtils.js"; 11 8 import { buildFileStack } from "./filesOnServer.js"; 12 9 import { SimpleError } from "../../common/errors.js"; 13 - import { DEFAULT_GLOB_FOLDER } from "../../common/infrastructure/atomic.js"; 10 + import { DEFAULT_GLOB_COMPOSE, DEFAULT_GLOB_FOLDER, KOMODO_GLOB_IGNORE_COMPOSE, DirectoryConfig, StackCandidateCompose, StackSourceOfTruth } from "../../common/infrastructure/atomic.js"; 11 + import { DockerApi } from "../../common/dockerApi.js"; 12 + import { consolidateComposeStacks } from "./composeUtils.js"; 13 + import { DEFAULT_ENV_GLOB } from "./stackUtils.js"; 14 + import { Minimatch, minimatch } from "minimatch"; 15 + import { stripIndents } from "common-tags"; 14 16 15 - export const buildStacksFromPath = async (path: string, options: AnyStackConfig, parentLogger: Logger): Promise<TomlStack[]> => { 17 + export class StackBuilder { 16 18 17 - const { 18 - composeFileGlob = DEFAULT_COMPOSE_GLOB, 19 - envFileGlob = DEFAULT_ENV_GLOB, 20 - folderGlob, 21 - ignoreFolderGlob 22 - } = options; 19 + logger: Logger; 20 + stackConfigOptions: AnyStackConfig; 23 21 24 - let stackOptions = { 25 - ...options, 26 - writeEnv: parseBool(process.env.WRITE_ENV, false) 27 - }; 22 + dirData: DirectoryConfig; 28 23 29 - const logger = childLogger(parentLogger, 'Stacks'); 30 - let topDir: string; 31 - try { 32 - topDir = await promises.realpath(path); 33 - logger.info(`Top Dir: ${path} -> Resolved: ${topDir}`); 34 - pathExistsAndIsReadable(topDir) 35 - } catch (e) { 36 - throw new Error(`Could not access ${path}.${parseBool(process.env.IS_DOCKER) ? ' This is the path *in container* that is read so make sure you have mounted it on the host!' : ''}`); 37 - } 24 + composeCandidateStacks: StackCandidateCompose[] = []; 25 + 26 + docker: DockerApi; 38 27 39 - let gitData: GitRepoData; 40 - try { 41 - gitData = await detectGitRepo(path); 42 - logger.verbose(`Detected top-level dir ${topDir} is a Git repo: Branch ${gitData[0].branch} | Remote ${gitData[1].remote} | URL ${gitData[1].url}`); 43 - logger.verbose('Will treat this as a monorepo -- all subfolders will be built as Git-Repo Stacks using the same repo with Run Directory relative to repo root.'); 44 - } catch (e) { 45 - if (e instanceof SimpleError) { 46 - logger.verbose(`Top-level dir is not a git repo: ${e.message}`); 47 - logger.verbose('Subfolders will be individually detected as Git repo or files-on-server Stacks') 48 - } else { 49 - throw e; 50 - } 28 + constructor(stacksConfig: AnyStackConfig, dirs: DirectoryConfig, logger: Logger) { 29 + this.stackConfigOptions = stacksConfig; 30 + this.dirData = dirs; 31 + this.logger = childLogger(logger, 'Stacks'); 32 + this.docker = new DockerApi(); 51 33 } 52 34 53 - let stacksDir: string = topDir; 54 - if (!isUndefinedOrEmptyString(process.env.GIT_STACKS_DIR) && gitData !== undefined) { 55 - try { 56 - stacksDir = await promises.realpath(joinPath(topDir, process.env.GIT_STACKS_DIR)); 57 - logger.info(`Git Stack Dir: ${process.env.GIT_STACKS_DIR} -> Resolved: ${stacksDir}`); 58 - pathExistsAndIsReadable(stacksDir) 59 - } catch (e) { 60 - throw new Error(`Could not access ${stacksDir}.${parseBool(process.env.IS_DOCKER) ? ' This is the path *in container* that is read so make sure you have mounted it on the host!' : ''}`); 35 + parseComposeProjects = async () => { 36 + const containers = await this.docker.getContainers({ label: 'com.docker.compose.project' }); 37 + if (containers.length > 0) { 38 + this.composeCandidateStacks = consolidateComposeStacks(containers, this.logger); 61 39 } 62 40 } 63 41 64 - let stacks: TomlStack[] = []; 42 + buildStacks = async (sourceOfTruth: StackSourceOfTruth): Promise<TomlStack[]> => { 65 43 66 - const dirs = await findFolders(stacksDir, folderGlob, ignoreFolderGlob) 67 - const folderPaths = dirs.map(x => joinPath(stacksDir, x)); 44 + this.logger.info(`Using ${sourceOfTruth === 'compose' ? 'parsed Compose projects' : 'child folders in SCAN_DIR'} to generate Stacks.`); 68 45 69 - logger.info(`Folder Glob: ${folderGlob ?? DEFAULT_GLOB_FOLDER}`); 70 - logger.info(`Folder Ignore Glob${ignoreFolderGlob === undefined ? ' N/A ' : `: ${DEFAULT_GLOB_FOLDER}`}`); 71 - logger.info(`Compose File Glob: ${composeFileGlob}`); 72 - logger.info(`Env Glob: ${envFileGlob}`); 73 - logger.info(`Processing Stacks for ${dirs.length} folders in ${stacksDir}:\n${formatIntoColumns(dirs, 3)}`); 46 + const { 47 + folderGlob = DEFAULT_GLOB_FOLDER, 48 + ignoreFolderGlob, 49 + envFileGlob = DEFAULT_ENV_GLOB, 50 + composeGlob = DEFAULT_GLOB_COMPOSE, 51 + ignoreComposeGlob 52 + } = this.stackConfigOptions; 74 53 75 - let hostParentPathVerified = false; 54 + let folderPaths: string[] = []; 76 55 77 - if (gitData !== undefined) { 56 + const writeEnvGlobal = parseBool(process.env.WRITE_ENV, true); 57 + const writeEnvFolder = parseBool(process.env.WRITE_ENV_DIR, writeEnvGlobal); 58 + const writeEnvGit = parseBool(process.env.WRITE_ENV_GIT, writeEnvGlobal); 78 59 79 - let gitStackConfig: Partial<GitStackConfig> = { 80 - inMonorepo: true 81 - } 82 - try { 83 - const [provider, linkedRepo, repoHint] = await matchGitDataWithKomodo(gitData); 84 - if (repoHint !== undefined) { 85 - logger.warn(`All Stacks will be built without a linked repo: ${repoHint}`); 86 - } 87 - const [domain, repo] = komodoRepoFromRemote(gitData[1].url) 88 - if (repo === undefined) { 89 - gitStackConfig = { 90 - ...options, 91 - git_provider: provider?.domain ?? domain, 92 - git_account: provider?.username, 93 - repo 94 - }; 95 - } else { 96 - gitStackConfig = { 97 - ...options, 98 - linked_repo: linkedRepo.name, 60 + this.logger.info(`Env Glob: ${envFileGlob}`); 61 + this.logger.info(`Write .env to Komodo Environment -- Dir Stack: ${writeEnvFolder} | Git Stack: ${writeEnvGit} `); 62 + 63 + if (sourceOfTruth === 'compose') { 64 + this.logger.info(`Compose Working Dir Glob: ${composeGlob}`); 65 + this.logger.info(`Compose Working Dir Ignore Glob: ${ignoreComposeGlob ?? 'N/A'}`); 66 + 67 + await this.parseComposeProjects(); 68 + 69 + const includeGlob = new Minimatch(composeGlob); 70 + const excludeGlob = ignoreComposeGlob === undefined ? undefined : new Minimatch(ignoreComposeGlob); 71 + const komodoGlob = new Minimatch(KOMODO_GLOB_IGNORE_COMPOSE); 72 + 73 + for (const c of this.composeCandidateStacks) { 74 + if (!c.workingDir.includes(this.dirData.host)) { 75 + this.logger.warn(`Compose project '${c.projectName}' working dir '${c.workingDir}' is not present in Host Dir, cannot use project`); 76 + continue; 99 77 } 78 + 79 + if(!includeGlob.match(c.workingDir)) { 80 + this.logger.verbose(`Compose project '${c.projectName}' working dir '${c.workingDir}' did not match file glob`); 81 + continue; 82 + } 83 + if(excludeGlob !== undefined && excludeGlob.match(c.workingDir)) { 84 + this.logger.verbose(`Compose project '${c.projectName}' working dir '${c.workingDir}' matches ignore file glob, skipping`); 85 + continue; 86 + } 87 + if(komodoGlob.match(c.workingDir)) { 88 + this.logger.verbose(`Compose project '${c.projectName}' working dir '${c.workingDir}' is in known komodo directory, skipping`); 89 + continue; 90 + } 91 + const convertedPath = c.workingDir.replace(this.dirData.host, this.dirData.mount); 92 + this.logger.debug(stripIndents`Compose project '${c.projectName}' is valid! 93 + Host Working Dir => ${c.workingDir} 94 + Komodo Import Mount Path => ${convertedPath}`); 95 + folderPaths.push(convertedPath); 100 96 } 101 - } catch (e) { 102 - throw new Error('Cannot use top-level git for Stacks', { cause: e }); 97 + this.logger.verbose(`Got ${folderPaths.length} valid compose project directories`); 98 + } else { 99 + this.logger.info(`Folder Glob: ${folderGlob}`); 100 + this.logger.info(`Folder Ignore Glob: ${ignoreFolderGlob ?? 'N/A'}`); 101 + 102 + await this.parseComposeProjects(); 103 + const dirs = await findFolders(this.dirData.scan, folderGlob, ignoreFolderGlob) 104 + folderPaths = dirs.map(x => joinPath(this.dirData.scan, x)); 105 + this.logger.verbose(`Got ${folderPaths.length} folders in ${this.dirData.scan}:\n${formatIntoColumns(dirs, 3)}`); 103 106 } 104 107 105 - stackOptions = { 106 - ...stackOptions, 107 - ...gitStackConfig, 108 - inMonorepo: true 109 - } 110 - } 108 + let stacks: TomlStack[] = []; 109 + 110 + let stackOptions = { 111 + ...this.stackConfigOptions, 112 + writeEnv: parseBool(process.env.WRITE_ENV, false) 113 + }; 114 + 115 + for (const f of folderPaths) { 111 116 112 - for (const f of folderPaths) { 117 + let composeFiles: string[] = []; 118 + let projectName: string; 113 119 114 - const pathInfo: ParsedPath = parse(f); 115 - const folderLogger = childLogger(logger, `${pathInfo.name}${pathInfo.ext !== '' ? pathInfo.ext : ''}`); 120 + const folderStackOptions = {...stackOptions}; 116 121 117 - if (gitData !== undefined) { 118 - try { 119 - stacks.push(await buildGitStack(f, 120 - { 121 - ...stackOptions, 122 - logger, 123 - hostParentPath: stacksDir === topDir ? undefined : stacksDir.replace(topDir, '').replace(/^\//, '') 124 - })); 125 - } catch (e) { 126 - folderLogger.error(new Error(`Unable to build Git Stack for folder ${f}`, { cause: e })); 122 + const matchedProject = this.composeCandidateStacks.find(x => x.workingDir.replace(this.dirData.host, this.dirData.mount) === f); 123 + if(matchedProject !== undefined) { 124 + projectName = matchedProject.projectName; 125 + composeFiles = matchedProject.composeFilePaths.map(x => x.replace(matchedProject.workingDir, '')); 126 + folderStackOptions.projectName = projectName; 127 + folderStackOptions.composeFiles = composeFiles; 127 128 } 128 - } else { 129 + 130 + 131 + const pathInfo: ParsedPath = parse(f); 132 + const folderLogger = childLogger(this.logger, `${pathInfo.name}${pathInfo.ext !== '' ? pathInfo.ext : ''}`); 129 133 130 134 try { 131 135 stacks.push(await buildGitStack(f, { 132 - inMonorepo: false, 133 - ...stackOptions, 134 - logger 136 + ...folderStackOptions, 137 + writeEnv: writeEnvGit, 138 + logger: this.logger, 135 139 })); 136 140 continue; 137 141 } catch (e) { 138 142 if (e instanceof SimpleError) { 139 - if (e.message.includes('does not have a .git folder')) { 140 - folderLogger.debug('Falling back to Files-On-Server, not a git repo'); 141 - } else { 142 - folderLogger.verbose(`Falling back to Files-On-Server => ${e.message}`); 143 - } 143 + folderLogger.verbose(`Falling back to Files-On-Server => ${e.message}`); 144 144 } else { 145 145 folderLogger.error(new Error(`Unable to build Git Stack for folder ${f}`, { cause: e })); 146 146 continue; 147 147 } 148 148 } 149 149 150 - if (!hostParentPathVerified) { 151 - if (isUndefinedOrEmptyString(stackOptions.hostParentPath)) { 152 - throw new Error('env HOST_PARENT_PATH is not set'); 153 - } 154 - hostParentPathVerified = true; 155 - } 156 150 try { 157 - stacks.push(await buildFileStack(f, { ...stackOptions, logger })); 151 + stacks.push(await buildFileStack(f, { 152 + ...folderStackOptions, 153 + writeEnv: writeEnvFolder, 154 + hostParentPath: this.dirData.scan, 155 + logger: this.logger })); 158 156 } catch (e) { 159 157 folderLogger.error(new Error(`Unable to build Files-On-Server Stack for folder ${f}`, { cause: e })); 160 158 } 159 + 161 160 } 161 + 162 + return stacks; 162 163 } 163 - return stacks; 164 164 }
+1 -1
src/common/KomodoApi.ts
··· 23 23 cachedGitProviders?: ListGitProviderAccountsResponse; 24 24 25 25 constructor(logger?: Logger, options?: KomodoApiOptions) { 26 - this.logger = childLogger(logger ?? initLogger()[0], 'Komodo API'); 26 + this.logger = childLogger(logger ?? initLogger(), 'Komodo API'); 27 27 this.options = options; 28 28 if(process.env.KOMODO_URL !== undefined) { 29 29 this.urlData = normalizeWebAddress(process.env.KOMODO_URL);
+61
src/common/dockerApi.ts
··· 1 + import { childLogger, Logger } from '@foxxmd/logging'; 2 + import Docker from 'dockerode' 3 + import { initLogger } from './logging.js'; 4 + 5 + /** 6 + * There are tragically few, modern docker api libraries for typescript 7 + * 8 + * https://github.com/leonitousconforti/the-moby-effect looks good but has a dependency on effect-ts 9 + * 10 + * https://gitlab.com/thyran-npm-packages/docker-api looks good as well but does not support http/tcp for daemon connection, only physical socket file 11 + * https://gitlab.com/thyran-npm-packages/docker-api/-/issues/1 12 + * 13 + * https://github.com/apocas/dockerode is the only one that works for any type of daemon connection but its ancient, uses prototyping, and @types/dockerode doesn't work 14 + */ 15 + 16 + export interface Container { 17 + Id: string 18 + Image: string 19 + Labels: Record<ContainerLabels, string> 20 + State: 'running' | string 21 + } 22 + 23 + export type ContainerLabels = 24 + 'com.docker.compose.project' | 25 + 'com.docker.compose.project.config_files' | 26 + 'com.docker.compose.project.working_dir' | 27 + 'com.docker.compose.service' | 28 + 'com.docker.compose.depends_on' | 29 + 'com.docker.compose.config-hash' | 30 + string 31 + ; 32 + 33 + export class DockerApi { 34 + logger: Logger; 35 + api: any | null; 36 + 37 + constructor(logger?: Logger) { 38 + this.logger = childLogger(logger ?? initLogger(), 'Docker API'); 39 + } 40 + 41 + init = async () => { 42 + if (this.api !== null) { 43 + try { 44 + const api = new Docker(); 45 + await api.ping(); 46 + this.api = api; 47 + } catch (e) { 48 + this.logger.warn(new Error('Could not use Docker API', { cause: e })); 49 + this.api = null; 50 + } 51 + } 52 + } 53 + 54 + getContainers = async (filters?: Record<string, string>): Promise<Container[]> => { 55 + await this.init(); 56 + if (this.api !== null) { 57 + return await this.api.listContainers(filters); 58 + } 59 + return []; 60 + } 61 + }
+34 -1
src/common/infrastructure/atomic.ts
··· 11 11 raw: string 12 12 } 13 13 14 - export const DEFAULT_GLOB_FOLDER = '*'; 14 + export const DEFAULT_GLOB_FOLDER = '*'; 15 + 16 + export const DEFAULT_GLOB_COMPOSE = '**/**'; 17 + 18 + export const KOMODO_GLOB_IGNORE_COMPOSE = '/{etc,home/*}/komodo/**'; 19 + 20 + export type StackDiscoveryMethod = 'compose' | 'monorepo' | 'folder'; 21 + 22 + export type StackSourceOfTruth = 'compose' | 'dir'; 23 + 24 + export interface StackCandidate { 25 + path: string 26 + discovered: StackDiscoveryMethod 27 + projectName: string 28 + } 29 + 30 + export interface StackCandidateCompose extends StackCandidate { 31 + discovered: 'compose' 32 + workingDir: string 33 + composeFilePaths: string[] 34 + state: 'running' | string 35 + } 36 + 37 + export interface DirectoryConfigValues { 38 + mountVal?: string 39 + hostVal?: string 40 + scanVal?: string 41 + } 42 + 43 + export interface DirectoryConfig { 44 + mount: string 45 + host: string 46 + scan: string 47 + }
+2
src/common/infrastructure/config/common.ts
··· 9 9 envFileGlob?: string 10 10 folderGlob?: string 11 11 ignoreFolderGlob?: string 12 + composeGlob?: string 13 + ignoreComposeGlob?: string 12 14 }
+2
src/common/infrastructure/config/stackConfig.ts
··· 3 3 export interface CommonStackConfig extends CommonImportOptions { 4 4 writeEnv?: boolean 5 5 hostParentPath?: string, 6 + composeFiles?: string[] 7 + projectName?: string 6 8 } 7 9 8 10 export interface FilesOnServerConfig extends CommonStackConfig {
+75 -2
src/common/utils/io.ts
··· 1 1 import { accessSync, constants, promises } from "fs"; 2 - import pathUtil from "path"; 2 + import pathUtil, { join, dirname } from "path"; 3 3 import { glob, GlobOptionsWithFileTypesUnset } from 'glob'; 4 4 import clone from 'clone'; 5 - import { DEFAULT_GLOB_FOLDER } from "../infrastructure/atomic.js"; 5 + import { DEFAULT_GLOB_FOLDER, DirectoryConfig, DirectoryConfigValues } from "../infrastructure/atomic.js"; 6 + import { parseBool } from "./utils.js"; 6 7 7 8 export async function writeFile(path: any, text: any) { 8 9 try { ··· 79 80 } 80 81 } 81 82 83 + export const findPathRecuriveParently = async (fromDir: string, path: string) => { 84 + 85 + let currDirectory = fromDir; 86 + let parentDirectory = fromDir; 87 + let firstRun = true; 88 + 89 + // https://hals.app/blog/recursively-read-parent-folder-nodejs/ 90 + while (firstRun || currDirectory !== parentDirectory) { 91 + 92 + firstRun = false; 93 + 94 + const files = await glob(path, { 95 + cwd: parentDirectory, 96 + }); 97 + 98 + if(files.length > 0) { 99 + return join(parentDirectory, path); 100 + } 101 + 102 + // The trick is here: 103 + // Using path.dirname() of a directory returns the parent directory! 104 + currDirectory = parentDirectory 105 + parentDirectory = dirname(parentDirectory); 106 + } 107 + 108 + return undefined; 109 + } 110 + 82 111 export const sortComposePaths = (p: string[]): string[] => { 83 112 const paths = clone(p); 84 113 paths.sort((a, b) => { ··· 144 173 145 174 export const dirHasGitConfig = (paths: string[]): boolean => { 146 175 return paths.some(x => x === '.git'); 176 + } 177 + 178 + export const parseDirectoryConfig = async (dirConfig: DirectoryConfigValues = {}): Promise<[DirectoryConfigValues, DirectoryConfig]> => { 179 + const { 180 + mountVal = process.env.MOUNT_DIR, 181 + hostVal = process.env.HOST_DIR ?? mountVal, 182 + scanVal = process.env.SCAN_DIR ?? mountVal 183 + } = dirConfig; 184 + 185 + const configVal = { 186 + mountVal, 187 + hostVal, 188 + scanVal 189 + }; 190 + 191 + let mountDir: string, 192 + hostDir: string, 193 + scanDir: string; 194 + 195 + try { 196 + mountDir = await promises.realpath(mountVal); 197 + pathExistsAndIsReadable(mountDir) 198 + } catch (e) { 199 + throw new Error(`Could not access directory for MOUNT_DIR ${mountVal}.${parseBool(process.env.IS_DOCKER) ? ' This is the path *in container* that is read so make sure you have mounted it on the host!' : ''}`); 200 + } 201 + 202 + if (hostVal === mountVal) { 203 + hostDir = mountDir; 204 + } else { 205 + hostDir = hostVal; 206 + } 207 + 208 + if (scanVal === mountVal) { 209 + scanDir = mountDir; 210 + } else { 211 + try { 212 + scanDir = await promises.realpath(join(mountDir, scanVal)); 213 + pathExistsAndIsReadable(scanDir) 214 + } catch (e) { 215 + throw new Error(`Could not access directory for SCAN_DIR '${scanVal}' -- this should be the *relative* path from HOST_DIR that we look for stack-folders in.`); 216 + } 217 + } 218 + 219 + return [configVal, { host: hostDir, scan: scanDir, mount: mountDir }] 147 220 }
+9 -2
src/common/utils/utils.ts
··· 1 1 import { searchAndReplace } from "@foxxmd/regex-buddy-core"; 2 2 import { stripIndents } from "common-tags"; 3 + import path, {sep} from 'path'; 3 4 4 5 export const isDebugMode = (): boolean => process.env.DEBUG_MODE === 'true'; 5 6 ··· 19 20 throw new Error(`'${value.toString()}' is not a boolean value.`); 20 21 } 21 22 22 - 23 23 export const removeUndefinedKeys = <T extends Record<string, any>>(obj: T): T | undefined => { 24 24 const newObj: any = {}; 25 25 Object.keys(obj).forEach((key) => { 26 26 if(Array.isArray(obj[key])) { 27 27 newObj[key] = obj[key]; 28 28 } else if (obj[key] === Object(obj[key])) { 29 - newObj[key] = removeUndefinedKeys(obj[key]); 29 + newObj[key] = obj[key]; 30 30 } else if (obj[key] !== undefined) { 31 31 newObj[key] = obj[key]; 32 32 } ··· 91 91 }); 92 92 93 93 return transformed; 94 + } 95 + 96 + export const removeRootPathSeparator = (pathStr: string): string => { 97 + if(pathStr.at(0) === sep) { 98 + return pathStr.substring(1); 99 + } 100 + return pathStr; 94 101 }
+20 -4
src/index.ts
··· 16 16 import { exportToFile } from './exporters/exportToFile.js'; 17 17 import { exportToSync } from './exporters/exportToApiSync.js'; 18 18 import { getDefaultKomodoApi } from './common/utils/komodo.js'; 19 - import { buildStacksFromPath } from './builders/stack/stackBuilder.js'; 19 + import { StackBuilder } from './builders/stack/stackBuilder.js'; 20 + import { parseDirectoryConfig } from './common/utils/io.js'; 21 + import { DirectoryConfig, DirectoryConfigValues } from './common/infrastructure/atomic.js'; 20 22 21 23 dayjs.extend(utc) 22 24 dayjs.extend(timezone); ··· 34 36 initLogger.error(appError); 35 37 } 36 38 }); 37 - 38 - const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); 39 39 40 40 try { 41 41 ··· 56 56 57 57 getDefaultKomodoApi(logger); 58 58 59 + let dirData: [DirectoryConfigValues, DirectoryConfig]; 60 + try { 61 + dirData = await parseDirectoryConfig(); 62 + logger.info(`Mount Dir : ${dirData[0].mountVal} -> Resolved: ${dirData[1].mount}`); 63 + logger.info(`Host Dir : ${dirData[0].hostVal} -> Resolved: ${dirData[1].host}`); 64 + logger.info(`Scan Dir : ${dirData[0].scanVal} -> Resolved: ${dirData[1].scan}`); 65 + } catch (e) { 66 + throw new Error('Could not parse required directories', {cause: e}); 67 + } 68 + 59 69 const importOptions: CommonImportOptions = { 60 70 server: process.env.SERVER_NAME, 61 71 imageRegistryProvider: process.env.IMAGE_REGISTRY_PROVIDER, ··· 79 89 hostParentPath: process.env.HOST_PARENT_PATH 80 90 }; 81 91 92 + const stackBuilder = new StackBuilder(filesOnServerConfig, dirData[1], logger); 93 + 82 94 let stacks: TomlStack[] = []; 83 - stacks = await buildStacksFromPath(process.env.FILES_ON_SERVER_DIR, filesOnServerConfig, logger); 95 + let stackModeVal = (process.env.STACKS_FROM ?? 'dir').toLocaleLowerCase(); 96 + if(stackModeVal !== 'compose' && stackModeVal !== 'dir') { 97 + throw new Error(`STACKS_FROM must be either 'compose' or 'dir'`); 98 + } 99 + stacks = await stackBuilder.buildStacks(stackModeVal);//await buildStacksFromPath(process.env.FILES_ON_SERVER_DIR, filesOnServerConfig, logger); 84 100 85 101 if (stacks.length === 0) { 86 102 logger.info('No Stacks found! Nothing to do.');
+20 -3
tests/filesOnServer.test.ts
··· 1 1 import { describe, it } from 'mocha'; 2 2 import chai, { expect } from 'chai'; 3 3 import withLocalTmpDir from 'with-local-tmp-dir'; 4 - import { mkdir, writeFile, chmod, constants } from 'node:fs/promises'; 4 + import { mkdir, writeFile } from 'node:fs/promises'; 5 5 import path from "path"; 6 6 import { loggerTest } from "@foxxmd/logging"; 7 7 import { buildFileStack, BuildFileStackOptions } from '../src/builders/stack/filesOnServer.js'; 8 - import { FilesOnServerConfig } from '../src/common/infrastructure/config/stackConfig.js'; 9 - import { stripIndents } from 'common-tags'; 10 8 11 9 const DEFAULT_FOS_PATH = '/my/cool' 12 10 const DEFAULT_SERVER = 'test-server'; ··· 115 113 116 114 expect(data.config.file_paths).to.not.be.undefined; 117 115 expect(data.config.file_paths.length).eq(2); 116 + }, { unsafeCleanup: true }); 117 + }); 118 + 119 + it(`includes all compose files when provided as arg`, async function () { 120 + await withLocalTmpDir(async () => { 121 + 122 + const dir = path.join(process.cwd(), 'test_1'); 123 + await mkdir(path.join(dir, 'nested'), {recursive: true}); 124 + await writeFile(path.join(dir, 'nested', 'docker-compose.yaml'), ''); 125 + 126 + const data = await buildFileStack(dir, { 127 + ...defaultFOSConfig, 128 + composeFileGlob: '**/{compose,docker-compose}*.yaml', 129 + composeFiles: [path.join(dir, 'nested', 'docker-compose.yaml')] 130 + }); 131 + 132 + expect(data.config.file_paths).to.not.be.undefined; 133 + expect(data.config.file_paths.length).eq(1); 134 + expect(data.config.file_paths[0]).eq('nested/docker-compose.yaml') 118 135 }, { unsafeCleanup: true }); 119 136 }); 120 137
+147 -27
tests/git.test.ts
··· 11 11 import git from 'isomorphic-git'; 12 12 import fs from 'fs'; 13 13 import { execa, Options } from 'execa'; 14 + import { faker } from '@faker-js/faker'; 15 + import { readText } from '../src/common/utils/io.js'; 14 16 15 17 chai.use(chaiAsPromised); 16 18 ··· 215 217 }); 216 218 }); 217 219 218 - // describe('#GitStack', function () { 220 + describe('#GitStack', function () { 219 221 220 - // it(`creates a valid stack`, async function () { 221 - // await withLocalTmpDir(async () => { 222 - // const dir = path.join(process.cwd(), 'test_1'); 223 - // await mkdir(dir); 224 - // const localDir = path.join(dir, 'local'); 225 - // //await mkdir(localDir); 226 - // const remoteDir = path.join(dir, 'remote'); 227 - // await mkdir(remoteDir); 228 - // await git.init({ fs, dir: remoteDir }); 229 - // await git.commit({ 230 - // fs, 231 - // dir: remoteDir, 232 - // author: { 233 - // name: 'Mr. Test', 234 - // email: 'mrtest@example.com', 235 - // }, 236 - // message: 'Added the a.txt file' 237 - // }); 238 - // // cannot yet clone local repo with isomorphic git 239 - // // https://github.com/isomorphic-git/isomorphic-git/issues/1263 240 - // await execa({ cwd: dir })`git clone remote local` 241 - // await expect(buildGitStack(localDir, defaultGitStandaloneConfig)).to.eventually.be.fulfilled; 242 - // }, { unsafeCleanup: true }); 243 - // }); 222 + it(`creates a valid stack from top-dir git`, async function () { 223 + await withLocalTmpDir(async () => { 224 + const dir = path.join(process.cwd(), 'test_1'); 225 + await mkdir(dir); 226 + await git.init({ fs, dir }); 227 + await git.commit({ 228 + fs, 229 + dir, 230 + author: { 231 + name: 'Mr. Test', 232 + email: 'mrtest@example.com', 233 + }, 234 + message: 'Added the a.txt file' 235 + }); 236 + const rc = await readText(path.join(dir, '.git', 'config')); 237 + const [fc, fakeData] = fakeGitConfig(); 238 + await writeFile(path.join(dir, '.git', 'config'), `${rc}\n${fc}`); 244 239 245 - // }); 246 - }); 240 + const stack = await buildGitStack(dir, defaultGitStandaloneConfig); 241 + expect(stack.config.repo).eq(`${fakeData.user}/${fakeData.repo}`); 242 + 243 + }, { unsafeCleanup: true }); 244 + }); 245 + 246 + it(`uses relative path for compose files`, async function () { 247 + await withLocalTmpDir(async () => { 248 + const dir = path.join(process.cwd(), 'test_1'); 249 + await mkdir(dir); 250 + await mkdir(path.join(dir, 'nested')) 251 + await writeFile(path.join(dir, 'nested', 'compose.yml'), ''); 252 + await git.init({ fs, dir }); 253 + await git.commit({ 254 + fs, 255 + dir, 256 + author: { 257 + name: 'Mr. Test', 258 + email: 'mrtest@example.com', 259 + }, 260 + message: 'Added the a.txt file' 261 + }); 262 + 263 + const rc = await readText(path.join(dir, '.git', 'config')); 264 + const [fc, fakeData] = fakeGitConfig(); 265 + await writeFile(path.join(dir, '.git', 'config'), `${rc}\n${fc}`); 266 + 267 + const stack = await buildGitStack(dir, defaultGitStandaloneConfig); 268 + expect(stack.config.file_paths).to.not.be.undefined; 269 + expect(stack.config.file_paths[0]).eq('nested/compose.yml'); 270 + 271 + }, { unsafeCleanup: true }); 272 + }); 273 + 274 + it(`sets relative path from parent when git is above dir`, async function () { 275 + await withLocalTmpDir(async () => { 276 + const dir = path.join(process.cwd(), 'test_1'); 277 + await mkdir(dir); 278 + await mkdir(path.join(dir, 'nested', 'docker'), {recursive: true}) 279 + await writeFile(path.join(dir, 'nested', 'docker', 'compose.yml'), ''); 280 + await git.init({ fs, dir }); 281 + await git.commit({ 282 + fs, 283 + dir, 284 + author: { 285 + name: 'Mr. Test', 286 + email: 'mrtest@example.com', 287 + }, 288 + message: 'Added the a.txt file' 289 + }); 290 + 291 + const rc = await readText(path.join(dir, '.git', 'config')); 292 + const [fc, fakeData] = fakeGitConfig(); 293 + await writeFile(path.join(dir, '.git', 'config'), `${rc}\n${fc}`); 294 + 295 + const stack = await buildGitStack(path.join(dir, 'nested'), defaultGitStandaloneConfig); 296 + expect(stack.config.run_directory).to.not.be.undefined; 297 + expect(stack.config.run_directory).eq('nested'); 298 + expect(stack.config.file_paths).to.not.be.undefined; 299 + expect(stack.config.file_paths[0]).eq('docker/compose.yml'); 300 + 301 + }, { unsafeCleanup: true }); 302 + }); 303 + 304 + it(`determines relative path for compose files when argument provided`, async function () { 305 + await withLocalTmpDir(async () => { 306 + const dir = path.join(process.cwd(), 'test_1'); 307 + await mkdir(dir); 308 + await mkdir(path.join(dir, 'nested', 'docker'), {recursive: true}) 309 + await writeFile(path.join(dir, 'nested', 'docker', 'compose.yml'), ''); 310 + await git.init({ fs, dir }); 311 + await git.commit({ 312 + fs, 313 + dir, 314 + author: { 315 + name: 'Mr. Test', 316 + email: 'mrtest@example.com', 317 + }, 318 + message: 'Added the a.txt file' 319 + }); 320 + 321 + const rc = await readText(path.join(dir, '.git', 'config')); 322 + const [fc, fakeData] = fakeGitConfig(); 323 + await writeFile(path.join(dir, '.git', 'config'), `${rc}\n${fc}`); 324 + 325 + const stack = await buildGitStack(path.join(dir, 'nested'), { 326 + ...defaultGitStandaloneConfig, 327 + composeFiles: [path.join(dir, 'nested', 'docker', 'compose.yml')] 328 + }); 329 + expect(stack.config.run_directory).to.not.be.undefined; 330 + expect(stack.config.run_directory).eq('nested'); 331 + expect(stack.config.file_paths).to.not.be.undefined; 332 + expect(stack.config.file_paths[0]).eq('docker/compose.yml'); 333 + 334 + }, { unsafeCleanup: true }); 335 + }); 336 + 337 + }); 338 + }); 339 + 340 + interface FakeGitConfig { 341 + user?: string 342 + repo?: string 343 + domain?: string 344 + } 345 + const fakeGitConfig = (options: FakeGitConfig = {}): [string, Required<FakeGitConfig>] => { 346 + 347 + const { 348 + domain = 'github.com', 349 + user = faker.internet.userName(), 350 + repo = `${faker.animal.type()}-${faker.word.adjective()}` 351 + } = options; 352 + 353 + const used: Required<FakeGitConfig> = { 354 + domain, 355 + user, 356 + repo 357 + }; 358 + 359 + const configContents = `[remote "origin"] 360 + url = https://${domain}/${user}/${repo} 361 + fetch = +refs/heads/*:refs/remotes/origin/* 362 + [branch "master"] 363 + remote = origin 364 + merge = refs/heads/master`; 365 + return [configContents, used] 366 + }