@fluxup/install 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2026
2
+ Diovani Bernardi da Motta
3
+
4
+ All rights reserved.
5
+
6
+ This software and associated documentation files (the "Software") are the
7
+ exclusive property of the Authors.
8
+
9
+ No permission is granted to use, copy, modify, merge, publish, distribute,
10
+ sublicense, and/or sell copies of the Software, in whole or in part, without
11
+ explicit prior written permission from the Authors.
12
+
13
+ The Software may not be used for commercial purposes without a separate
14
+ written commercial agreement signed by the Authors.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ ## Visao geral
2
+
3
+ Este repositório centraliza o processo de instalação da plataforma FluxUp em servidores Docker on-premises.
4
+ Contém a CLI de instalação interativa (`npx @fluxup/install`), o `docker-compose.yml` baseado em imagens pré-publicadas no GHCR, e a configuração de nginx que serve o frontend e faz proxy para os serviços internos.
5
+
6
+ > Contexto de negócio completo: [github.com/fluxup-platform/profile](https://github.com/fluxup-platform/profile/blob/main/README.md)
7
+
8
+ ## Tecnologias
9
+
10
+ | Tecnologia | Versão mínima | Papel |
11
+ |---|---|---|
12
+ | Node.js | 18 | Runtime da CLI npx |
13
+ | Docker Engine | 20.10 | Execução dos containers |
14
+ | Docker Compose | v2 | Orquestração dos serviços |
15
+ | AWS SDK for JS | v3 | Download do S3 e acesso ao Secrets Manager |
16
+ | nginx | alpine | Reverse proxy e servidor de arquivos estáticos |
17
+
18
+ ---
19
+
20
+ ## Arquitetura
21
+
22
+ O instalador opera em 8 etapas sequenciais:
23
+
24
+ 1. **Preflight** — verifica `docker` e `docker compose` v2 no servidor
25
+ 2. **Wizard** — coleta variáveis de ambiente via prompts interativos por grupo
26
+ 3. **Secrets Manager** — busca o token GHCR no AWS Secrets Manager (em memória, nunca em disco)
27
+ 4. **GHCR Login** — `docker login ghcr.io --password-stdin` com o token obtido
28
+ 5. **Download S3** — baixa o `docker-compose.yml` do bucket S3 via SDK
29
+ 6. **Geração do .env** — cria `<dir>/.env` com permissão `0600` (sem token GHCR)
30
+ 7. **Pull + Up** — `docker compose pull` seguido de `docker compose up -d`
31
+ 8. **Status** — exibe containers ativos e URLs de acesso
32
+
33
+ O nginx embute o build do Angular frontend como imagem GHCR. O servidor on-premises não precisa de código-fonte — apenas Docker.
34
+
35
+ ---
36
+
37
+ ## Como executar a instalação
38
+
39
+ ### Pré-requisitos no servidor
40
+
41
+ - Docker Engine ≥ 20.10 instalado e em execução
42
+ - Docker Compose v2 disponível (`docker compose version`)
43
+ - Node.js ≥ 18 disponível no `PATH` (apenas para o npx — pode ser removido após instalar)
44
+ - Acesso à internet: GHCR, bucket S3 e AWS Secrets Manager
45
+
46
+ ### Execução
47
+
48
+ ```bash
49
+ npx @fluxup/install
50
+ ```
51
+
52
+ O wizard guiará pela configuração de todas as variáveis necessárias.
53
+
54
+ ### Flags disponíveis
55
+
56
+ | Flag | Descrição |
57
+ |------|-----------|
58
+ | `--dir <path>` | Diretório de instalação (padrão: `./fluxup`) |
59
+ | `--non-interactive` | Lê variáveis já exportadas no shell — útil para CI/automação |
60
+ | `--no-pull` | Pula o `docker compose pull` |
61
+
62
+ ### Modo não-interativo (CI/automação)
63
+
64
+ ```bash
65
+ export GHCR_USERNAME="Fluxup Deploy Bot"
66
+ export GHCR_SECRET_NAME="fluxup/ghcr-token"
67
+ export IMAGE_TAG="1.4.3"
68
+ export S3_BUCKET="meu-bucket"
69
+ export S3_KEY="releases/1.4.3/docker-compose.yml"
70
+ export AWS_REGION="us-east-1"
71
+ export AWS_ACCESS_KEY_ID="..."
72
+ export AWS_SECRET_ACCESS_KEY="..."
73
+ # ... demais variáveis
74
+
75
+ npx @fluxup/install --non-interactive --dir /opt/fluxup
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Configuração do secret GHCR no AWS Secrets Manager
81
+
82
+ Crie um secret com valor JSON:
83
+
84
+ ```json
85
+ { "token": "ghp_..." }
86
+ ```
87
+
88
+ O nome do secret é configurado via `GHCR_SECRET_NAME` (padrão: `fluxup/ghcr-token`).
89
+ O token **nunca** é armazenado em disco — apenas em memória durante o login.
90
+
91
+ ---
92
+
93
+ ## Estrutura do repositório
94
+
95
+ ```
96
+ compose/docker-compose.yml — compose baseado em imagens (fonte; CI publica no S3)
97
+ nginx/Dockerfile — build nginx + Angular frontend (usado só pelo CI)
98
+ nginx/nginx.conf — configuração do nginx (proxy + SPA fallback)
99
+ bin/cli.js — entrypoint da CLI
100
+ src/
101
+ preflight.js — verifica pré-requisitos Docker
102
+ wizard.js — prompts interativos agrupados
103
+ env.schema.js — definição declarativa de todas as variáveis
104
+ secrets.js — busca token GHCR no AWS Secrets Manager
105
+ ghcr.js — docker login via --password-stdin
106
+ s3.js — download do compose do bucket S3
107
+ env.js — geração do .env
108
+ docker.js — docker compose pull / up / ps
109
+ .github/workflows/
110
+ publish-nginx.yml — publica ghcr.io/fluxup-platform/fluxup-nginx:<ver>
111
+ upload-compose.yml — sobe compose no S3 em cada release tag
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Release
117
+
118
+ Em cada tag `vX.Y.Z`, os workflows CI executam automaticamente:
119
+
120
+ 1. `publish-nginx.yml` — builda o nginx com o frontend Angular e publica `ghcr.io/fluxup-platform/fluxup-nginx:X.Y.Z` no GHCR
121
+ 2. `upload-compose.yml` — sobe `compose/docker-compose.yml` para `s3://<bucket>/releases/X.Y.Z/docker-compose.yml`
122
+
123
+ O instalador então usa `IMAGE_TAG=X.Y.Z` e `S3_KEY=releases/X.Y.Z/docker-compose.yml` para instalar a versão correta.
124
+
125
+ ---
126
+
127
+ ## Contribuição
128
+
129
+ 1. Leia o [CONTRIBUTING.md](CONTRIBUTING.md) antes de começar.
130
+ 2. Crie uma branch de feature: `git checkout -b feature/descricao-curta`.
131
+ 3. Valide localmente: `npm install && node bin/cli.js --help`.
132
+ 4. Abra um pull request com descrição objetiva das mudanças.
133
+
134
+ ---
135
+
136
+ ## Autores
137
+
138
+ - [Diovani Motta](https://github.com/diovanibmotta)
139
+ - [João Cristofoloni](https://github.com/IJhonC)
140
+ - [João Puel](https://github.com/joaopuel)
141
+ - [Jonas Magalhães](https://github.com/JonasVMagalhaes)
142
+
143
+ ## Contato
144
+
145
+ Dúvidas ou sugestões: [contato@fluxup.com.br](mailto:contato@fluxup.com.br)
package/bin/cli.js ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fluxup-install — FluxUp on-premises Docker installation CLI.
4
+ *
5
+ * Usage:
6
+ * npx @fluxup/install
7
+ * npx @fluxup/install --dir /opt/fluxup
8
+ * npx @fluxup/install --non-interactive (reads env vars already exported in the shell)
9
+ * npx @fluxup/install --no-pull (skips docker compose pull)
10
+ */
11
+
12
+ import { join, resolve } from 'node:path';
13
+ import { mkdirSync } from 'node:fs';
14
+ import * as p from '@clack/prompts';
15
+ import pc from 'picocolors';
16
+
17
+ import { preflight } from '../src/preflight.js';
18
+ import { runWizard } from '../src/wizard.js';
19
+ import { fetchGhcrToken } from '../src/secrets.js';
20
+ import { loginGhcr } from '../src/ghcr.js';
21
+ import { downloadCompose } from '../src/s3.js';
22
+ import { writeEnvFile } from '../src/env.js';
23
+ import { pullImages, upServices, showStatus } from '../src/docker.js';
24
+
25
+ // ─── Flag parsing ─────────────────────────────────────────────────────────────
26
+ const args = process.argv.slice(2);
27
+ const flags = {
28
+ dir: args[args.indexOf('--dir') + 1] ?? './fluxup',
29
+ nonInteractive: args.includes('--non-interactive'),
30
+ noPull: args.includes('--no-pull'),
31
+ };
32
+
33
+ const installDir = resolve(flags.dir);
34
+
35
+ // ─── Main ────────────────────────────────────────────────────────────────────
36
+ async function main() {
37
+ console.log('');
38
+ console.log(pc.bgBlue(pc.white(' FluxUp Install ')));
39
+ console.log(pc.dim(`Install directory: ${installDir}\n`));
40
+
41
+ // 1. Preflight
42
+ p.log.step('Checking prerequisites...');
43
+ await preflight();
44
+
45
+ // 2. Wizard (or read from existing env vars in --non-interactive mode)
46
+ let answers;
47
+ if (flags.nonInteractive) {
48
+ p.log.info('Non-interactive mode: reading environment variables from shell.');
49
+ answers = Object.fromEntries(
50
+ process.env
51
+ ? Object.entries(process.env).filter(([k]) => k.match(/^(GHCR|IMAGE|S3|AWS|DB|JPA|NGINX|BACKEND)/))
52
+ : []
53
+ );
54
+ } else {
55
+ answers = await runWizard();
56
+ }
57
+
58
+ // 3. Fetch GHCR token from AWS Secrets Manager (in-memory only — never written to disk)
59
+ p.log.step('Fetching GHCR token from AWS Secrets Manager...');
60
+ let ghcrToken;
61
+ try {
62
+ ghcrToken = await fetchGhcrToken({
63
+ secretName: answers.GHCR_SECRET_NAME,
64
+ region: answers.AWS_REGION,
65
+ accessKeyId: answers.AWS_ACCESS_KEY_ID,
66
+ secretAccessKey: answers.AWS_SECRET_ACCESS_KEY,
67
+ });
68
+ p.log.success('GHCR token retrieved from Secrets Manager.');
69
+ } catch (err) {
70
+ p.log.error(`Failed to fetch GHCR token: ${err.message}`);
71
+ p.log.warn('Check your AWS credentials and the secret name (GHCR_SECRET_NAME).');
72
+ process.exit(1);
73
+ }
74
+
75
+ // 4. GHCR login
76
+ p.log.step('Authenticating with GHCR...');
77
+ try {
78
+ await loginGhcr({ username: answers.GHCR_USERNAME, token: ghcrToken });
79
+ } catch (err) {
80
+ p.log.error(`GHCR login failed: ${err.message}`);
81
+ process.exit(1);
82
+ }
83
+
84
+ // Discard token from memory
85
+ ghcrToken = null;
86
+
87
+ // 5. Create install directory and download docker-compose.yml from S3
88
+ mkdirSync(installDir, { recursive: true });
89
+ const composePath = join(installDir, 'docker-compose.yml');
90
+
91
+ p.log.step('Downloading docker-compose.yml from S3...');
92
+ try {
93
+ await downloadCompose({
94
+ bucket: answers.S3_BUCKET,
95
+ key: answers.S3_KEY,
96
+ region: answers.S3_REGION ?? answers.AWS_REGION,
97
+ accessKeyId: answers.S3_AWS_ACCESS_KEY_ID,
98
+ secretAccessKey: answers.S3_AWS_SECRET_ACCESS_KEY,
99
+ destPath: composePath,
100
+ });
101
+ } catch (err) {
102
+ p.log.error(`Failed to download compose from S3: ${err.message}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ // 6. Write .env (without GHCR token)
107
+ p.log.step('Generating .env file...');
108
+ const envPath = writeEnvFile(installDir, answers);
109
+
110
+ // 7. Pull + Up
111
+ try {
112
+ if (!flags.noPull) {
113
+ await pullImages(composePath, envPath);
114
+ }
115
+ await upServices(composePath, envPath);
116
+ } catch (err) {
117
+ p.log.error(`Docker Compose failed: ${err.message}`);
118
+ process.exit(1);
119
+ }
120
+
121
+ // 8. Post-install
122
+ await showStatus(composePath, envPath);
123
+
124
+ const port = answers.NGINX_PORT ?? '80';
125
+ console.log('');
126
+ p.outro(
127
+ pc.green('✔ FluxUp installed successfully!') +
128
+ `\n\n Frontend: ${pc.cyan(`http://<server>:${port}/`)}\n` +
129
+ ` API: ${pc.cyan(`http://<server>:${port}/api/`)}\n` +
130
+ ` BFF: ${pc.cyan(`http://<server>:${port}/bff/`)}\n`
131
+ );
132
+ }
133
+
134
+ main().catch((err) => {
135
+ console.error(pc.red('\nUnexpected error:'), err);
136
+ process.exit(1);
137
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@fluxup/install",
3
+ "version": "1.0.0",
4
+ "description": "FluxUp on-premises Docker installation CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "fluxup-install": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "scripts": {
14
+ "lint": "node --check bin/cli.js src/*.js",
15
+ "semantic-release": "semantic-release"
16
+ },
17
+ "dependencies": {
18
+ "@aws-sdk/client-s3": "^3.600.0",
19
+ "@aws-sdk/client-secrets-manager": "^3.600.0",
20
+ "@clack/prompts": "^0.9.0",
21
+ "execa": "^9.3.0",
22
+ "picocolors": "^1.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@semantic-release/changelog": "^6.0.3",
26
+ "@semantic-release/git": "^10.0.1",
27
+ "@semantic-release/github": "^10.3.5",
28
+ "@semantic-release/npm": "^12.0.1",
29
+ "semantic-release": "^24.1.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/docker.js ADDED
@@ -0,0 +1,48 @@
1
+ import { execa } from 'execa';
2
+ import pc from 'picocolors';
3
+
4
+ /**
5
+ * Runs docker compose with the given files and arguments.
6
+ */
7
+ function compose(composePath, envPath, args) {
8
+ return execa(
9
+ 'docker',
10
+ ['compose', '-f', composePath, '--env-file', envPath, ...args],
11
+ { stdout: 'inherit', stderr: 'inherit' }
12
+ );
13
+ }
14
+
15
+ /**
16
+ * Pulls all images declared in the compose file.
17
+ *
18
+ * @param {string} composePath
19
+ * @param {string} envPath
20
+ */
21
+ export async function pullImages(composePath, envPath) {
22
+ console.log(pc.bold('\nPulling images...'));
23
+ await compose(composePath, envPath, ['pull']);
24
+ console.log(pc.green('✔') + ' Pull complete.');
25
+ }
26
+
27
+ /**
28
+ * Starts all services in detached mode.
29
+ *
30
+ * @param {string} composePath
31
+ * @param {string} envPath
32
+ */
33
+ export async function upServices(composePath, envPath) {
34
+ console.log(pc.bold('\nStarting containers...'));
35
+ await compose(composePath, envPath, ['up', '-d']);
36
+ console.log(pc.green('✔') + ' Containers running.');
37
+ }
38
+
39
+ /**
40
+ * Displays container status.
41
+ *
42
+ * @param {string} composePath
43
+ * @param {string} envPath
44
+ */
45
+ export async function showStatus(composePath, envPath) {
46
+ console.log(pc.bold('\nContainer status:'));
47
+ await compose(composePath, envPath, ['ps']);
48
+ }
package/src/env.js ADDED
@@ -0,0 +1,44 @@
1
+ import { writeFileSync, chmodSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ENV_SCHEMA } from './env.schema.js';
4
+ import pc from 'picocolors';
5
+
6
+ /**
7
+ * Writes the .env file to the install directory.
8
+ * The GHCR token is NOT included — it is always fetched from Secrets Manager at runtime.
9
+ * Permission 0600 applied (owner read-only).
10
+ *
11
+ * @param {string} dir Directory where .env will be created
12
+ * @param {Record<string, string>} answers Wizard answers
13
+ * @returns {string} Path to the created .env file
14
+ */
15
+ export function writeEnvFile(dir, answers) {
16
+ const envPath = join(dir, '.env');
17
+
18
+ const lines = ENV_SCHEMA
19
+ .filter((f) => !f.key.startsWith('GHCR_SECRET') || true) // includes GHCR_SECRET_NAME (name, not the token)
20
+ .map((f) => {
21
+ const val = answers[f.key] ?? '';
22
+ return `${f.key}=${val}`;
23
+ });
24
+
25
+ const content = [
26
+ '# Generated by fluxup-install — DO NOT commit',
27
+ '# GHCR token is not here — fetched from AWS Secrets Manager at runtime',
28
+ '',
29
+ ...lines,
30
+ '',
31
+ ].join('\n');
32
+
33
+ writeFileSync(envPath, content, { encoding: 'utf8' });
34
+
35
+ try {
36
+ chmodSync(envPath, 0o600);
37
+ } catch {
38
+ // Windows does not support chmod — silently ignored
39
+ }
40
+
41
+ console.log(pc.green('✔') + ` .env created at ${envPath}`);
42
+
43
+ return envPath;
44
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Declarative definition of all environment variables collected by the wizard.
3
+ *
4
+ * Fields:
5
+ * key — variable name in .env
6
+ * prompt — text displayed to the user
7
+ * default — default value (string) or undefined
8
+ * secret — true = masked input
9
+ * group — visual grouping in the wizard
10
+ */
11
+ export const ENV_SCHEMA = [
12
+ // ─── GHCR ────────────────────────────────────────────────────────────────
13
+ {
14
+ group: 'GHCR (GitHub Container Registry)',
15
+ key: 'GHCR_USERNAME',
16
+ prompt: 'GHCR username (e.g. Fluxup Deploy Bot)',
17
+ default: 'Fluxup Deploy Bot',
18
+ },
19
+ {
20
+ group: 'GHCR (GitHub Container Registry)',
21
+ key: 'IMAGE_TAG',
22
+ prompt: 'Docker image tag (e.g. 1.4.3)',
23
+ default: 'latest',
24
+ },
25
+ {
26
+ group: 'GHCR (GitHub Container Registry)',
27
+ key: 'GHCR_SECRET_NAME',
28
+ prompt: 'Name/ARN of the AWS Secrets Manager secret that holds the GHCR token',
29
+ default: 'fluxup/ghcr-token',
30
+ },
31
+
32
+ // ─── S3 ──────────────────────────────────────────────────────────────────
33
+ {
34
+ group: 'S3 (docker-compose source)',
35
+ key: 'S3_BUCKET',
36
+ prompt: 'S3 bucket name that stores docker-compose.yml',
37
+ default: 'fluxup-installer',
38
+ },
39
+ {
40
+ group: 'S3 (docker-compose source)',
41
+ key: 'S3_KEY',
42
+ prompt: 'Object key in the bucket (e.g. v1.4.3/docker-compose.yml)',
43
+ default: 'docker-compose.yml',
44
+ },
45
+ {
46
+ group: 'S3 (docker-compose source)',
47
+ key: 'S3_REGION',
48
+ prompt: 'AWS region of the S3 bucket',
49
+ default: 'us-east-1',
50
+ },
51
+ {
52
+ group: 'S3 (docker-compose source)',
53
+ key: 'S3_AWS_ACCESS_KEY_ID',
54
+ prompt: 'AWS Access Key ID for the installer S3 bucket (read-only user)',
55
+ },
56
+ {
57
+ group: 'S3 (docker-compose source)',
58
+ key: 'S3_AWS_SECRET_ACCESS_KEY',
59
+ prompt: 'AWS Secret Access Key for the installer S3 bucket (read-only user)',
60
+ secret: true,
61
+ },
62
+
63
+ // ─── AWS Credentials ─────────────────────────────────────────────────────
64
+ {
65
+ group: 'AWS Credentials',
66
+ key: 'AWS_REGION',
67
+ prompt: 'Default AWS region',
68
+ default: 'us-east-1',
69
+ },
70
+ {
71
+ group: 'AWS Credentials',
72
+ key: 'AWS_ACCESS_KEY_ID',
73
+ prompt: 'AWS Access Key ID',
74
+ },
75
+ {
76
+ group: 'AWS Credentials',
77
+ key: 'AWS_SECRET_ACCESS_KEY',
78
+ prompt: 'AWS Secret Access Key',
79
+ secret: true,
80
+ },
81
+ {
82
+ group: 'AWS Credentials',
83
+ key: 'AWS_STS_BROKER_ROLE_ARN',
84
+ prompt: 'STS Broker IAM Role ARN',
85
+ },
86
+ {
87
+ group: 'AWS Credentials',
88
+ key: 'AWS_SECRETSMANAGER_READER_ROLE_ARN',
89
+ prompt: 'Secrets Manager reader IAM Role ARN',
90
+ },
91
+
92
+ // ─── Database ────────────────────────────────────────────────────────────
93
+ {
94
+ group: 'Database (PostgreSQL)',
95
+ key: 'DB_NAME',
96
+ prompt: 'Database name',
97
+ default: 'fluxup',
98
+ },
99
+ {
100
+ group: 'Database (PostgreSQL)',
101
+ key: 'DB_USERNAME',
102
+ prompt: 'Database username',
103
+ default: 'fluxup',
104
+ },
105
+ {
106
+ group: 'Database (PostgreSQL)',
107
+ key: 'DB_PASSWORD',
108
+ prompt: 'Database password',
109
+ secret: true,
110
+ },
111
+ {
112
+ group: 'Database (PostgreSQL)',
113
+ key: 'JPA_DDL_AUTO',
114
+ prompt: 'JPA DDL Auto (update | validate | none)',
115
+ default: 'update',
116
+ },
117
+
118
+ // ─── DynamoDB ────────────────────────────────────────────────────────────
119
+ {
120
+ group: 'DynamoDB',
121
+ key: 'AWS_DAILY_JOBS_TABLE',
122
+ prompt: 'DynamoDB daily jobs table name',
123
+ },
124
+ {
125
+ group: 'DynamoDB',
126
+ key: 'AWS_DYNAMODB_DAILY_JOBS_ROLE_ARN',
127
+ prompt: 'DynamoDB access IAM Role ARN',
128
+ },
129
+
130
+ // ─── SQS ─────────────────────────────────────────────────────────────────
131
+ {
132
+ group: 'SQS',
133
+ key: 'AWS_SQS_CONSUMER_ROLE_ARN',
134
+ prompt: 'SQS consumer IAM Role ARN',
135
+ },
136
+ {
137
+ group: 'SQS',
138
+ key: 'AWS_SQS_PUBLISHER_ROLE_ARN',
139
+ prompt: 'SQS publisher IAM Role ARN',
140
+ },
141
+ {
142
+ group: 'SQS',
143
+ key: 'AWS_DAILY_REQUEST_QUEUE_URL',
144
+ prompt: 'Daily request SQS queue URL',
145
+ },
146
+ {
147
+ group: 'SQS',
148
+ key: 'AWS_DAILY_RESPONSE_QUEUE_URL',
149
+ prompt: 'Daily response SQS queue URL',
150
+ },
151
+ {
152
+ group: 'SQS',
153
+ key: 'AWS_DAILY_DLQ_URL',
154
+ prompt: 'Dead Letter Queue SQS URL',
155
+ },
156
+
157
+ // ─── Network ─────────────────────────────────────────────────────────────
158
+ {
159
+ group: 'Network',
160
+ key: 'NGINX_PORT',
161
+ prompt: 'nginx HTTP port on the host',
162
+ default: '80',
163
+ },
164
+ {
165
+ group: 'Network',
166
+ key: 'BACKEND_API_TIMEOUT',
167
+ prompt: 'Backend API timeout in ms',
168
+ default: '5000',
169
+ },
170
+ ];
package/src/ghcr.js ADDED
@@ -0,0 +1,22 @@
1
+ import { execa } from 'execa';
2
+ import pc from 'picocolors';
3
+
4
+ /**
5
+ * Authenticates with GHCR using docker login --password-stdin.
6
+ * The token stays in memory only (piped directly to the docker process).
7
+ *
8
+ * @param {object} opts
9
+ * @param {string} opts.username GHCR username
10
+ * @param {string} opts.token GHCR token (fetched from AWS Secrets Manager)
11
+ */
12
+ export async function loginGhcr({ username, token }) {
13
+ console.log(pc.dim(` docker login ghcr.io -u "${username}" --password-stdin`));
14
+
15
+ await execa('docker', ['login', 'ghcr.io', '-u', username, '--password-stdin'], {
16
+ input: token,
17
+ stdout: 'inherit',
18
+ stderr: 'inherit',
19
+ });
20
+
21
+ console.log(pc.green('✔') + ' GHCR login successful.');
22
+ }
@@ -0,0 +1,41 @@
1
+ import { execa } from 'execa';
2
+ import pc from 'picocolors';
3
+
4
+ /**
5
+ * Checks that docker and docker compose v2 are available.
6
+ * Aborts with a clear message if any prerequisite is missing.
7
+ */
8
+ export async function preflight() {
9
+ const checks = [
10
+ {
11
+ cmd: 'docker',
12
+ args: ['--version'],
13
+ label: 'Docker',
14
+ hint: 'Install Docker Engine: https://docs.docker.com/engine/install/',
15
+ },
16
+ {
17
+ cmd: 'docker',
18
+ args: ['compose', 'version'],
19
+ label: 'Docker Compose v2',
20
+ hint: 'Compose v2 ships with Docker Engine >= 20.10. Update Docker.',
21
+ },
22
+ ];
23
+
24
+ let ok = true;
25
+
26
+ for (const { cmd, args, label, hint } of checks) {
27
+ try {
28
+ const { stdout } = await execa(cmd, args);
29
+ console.log(pc.green('✔') + ' ' + label + ' — ' + stdout.trim().split('\n')[0]);
30
+ } catch {
31
+ console.error(pc.red('✘') + ' ' + label + ' not found.');
32
+ console.error(' ' + pc.yellow(hint));
33
+ ok = false;
34
+ }
35
+ }
36
+
37
+ if (!ok) {
38
+ console.error(pc.red('\nMissing prerequisites. Install the required tools and try again.'));
39
+ process.exit(1);
40
+ }
41
+ }
package/src/s3.js ADDED
@@ -0,0 +1,45 @@
1
+ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
2
+ import { createWriteStream, mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import pc from 'picocolors';
6
+
7
+ /**
8
+ * Downloads docker-compose.yml from an S3 bucket to disk.
9
+ *
10
+ * @param {object} opts
11
+ * @param {string} opts.bucket
12
+ * @param {string} opts.key
13
+ * @param {string} opts.region
14
+ * @param {string} opts.accessKeyId
15
+ * @param {string} opts.secretAccessKey
16
+ * @param {string} opts.destPath Local destination path (e.g. ./fluxup/docker-compose.yml)
17
+ */
18
+ export async function downloadCompose({
19
+ bucket,
20
+ key,
21
+ region,
22
+ accessKeyId,
23
+ secretAccessKey,
24
+ destPath,
25
+ }) {
26
+ const client = new S3Client({
27
+ region,
28
+ credentials: { accessKeyId, secretAccessKey },
29
+ });
30
+
31
+ console.log(pc.dim(` s3://${bucket}/${key} → ${destPath}`));
32
+
33
+ const { Body } = await client.send(
34
+ new GetObjectCommand({ Bucket: bucket, Key: key })
35
+ );
36
+
37
+ if (!Body) {
38
+ throw new Error(`Object s3://${bucket}/${key} returned no Body.`);
39
+ }
40
+
41
+ mkdirSync(dirname(destPath), { recursive: true });
42
+ await pipeline(Body, createWriteStream(destPath));
43
+
44
+ console.log(pc.green('✔') + ` docker-compose.yml saved to ${destPath}`);
45
+ }
package/src/secrets.js ADDED
@@ -0,0 +1,42 @@
1
+ import {
2
+ SecretsManagerClient,
3
+ GetSecretValueCommand,
4
+ } from '@aws-sdk/client-secrets-manager';
5
+
6
+ /**
7
+ * Fetches the GHCR token from AWS Secrets Manager at runtime.
8
+ * The token is kept in memory only — never written to disk.
9
+ *
10
+ * @param {object} opts
11
+ * @param {string} opts.secretName Secret name or ARN (e.g. 'fluxup/ghcr-token')
12
+ * @param {string} opts.region AWS region
13
+ * @param {string} opts.accessKeyId
14
+ * @param {string} opts.secretAccessKey
15
+ * @returns {Promise<string>} GHCR token
16
+ */
17
+ export async function fetchGhcrToken({ secretName, region, accessKeyId, secretAccessKey }) {
18
+ const client = new SecretsManagerClient({
19
+ region,
20
+ credentials: {
21
+ accessKeyId,
22
+ secretAccessKey,
23
+ },
24
+ });
25
+
26
+ const response = await client.send(
27
+ new GetSecretValueCommand({ SecretId: secretName })
28
+ );
29
+
30
+ const raw = response.SecretString;
31
+ if (!raw) {
32
+ throw new Error(`Secret "${secretName}" has no SecretString.`);
33
+ }
34
+
35
+ // Supports secret as JSON { "token": "ghp_..." } or plain string
36
+ try {
37
+ const parsed = JSON.parse(raw);
38
+ return parsed.token ?? parsed.GHCR_TOKEN ?? raw;
39
+ } catch {
40
+ return raw.trim();
41
+ }
42
+ }
package/src/wizard.js ADDED
@@ -0,0 +1,58 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { ENV_SCHEMA } from './env.schema.js';
4
+
5
+ /**
6
+ * Collects all environment variables via grouped interactive prompts.
7
+ * Fields marked with secret:true use masked input.
8
+ *
9
+ * @param {Record<string, string>} existing Pre-existing values (e.g. from --non-interactive env vars)
10
+ * @returns {Promise<Record<string, string>>}
11
+ */
12
+ export async function runWizard(existing = {}) {
13
+ const answers = { ...existing };
14
+
15
+ const groups = [...new Set(ENV_SCHEMA.map((e) => e.group))];
16
+
17
+ p.intro(pc.bgBlue(pc.white(' FluxUp Install — Configuration ')));
18
+
19
+ for (const group of groups) {
20
+ const fields = ENV_SCHEMA.filter((e) => e.group === group);
21
+
22
+ p.log.step(pc.bold(group));
23
+
24
+ for (const field of fields) {
25
+ if (answers[field.key] !== undefined && answers[field.key] !== '') {
26
+ p.log.info(`${field.key} = ${field.secret ? '***' : answers[field.key]} (kept)`);
27
+ continue;
28
+ }
29
+
30
+ const value = field.secret
31
+ ? await p.password({
32
+ message: field.prompt,
33
+ validate(v) {
34
+ if (!field.default && !v) return 'Required.';
35
+ },
36
+ })
37
+ : await p.text({
38
+ message: field.prompt,
39
+ defaultValue: field.default,
40
+ placeholder: field.default ?? '',
41
+ validate(v) {
42
+ if (!field.default && !v) return 'Required.';
43
+ },
44
+ });
45
+
46
+ if (p.isCancel(value)) {
47
+ p.cancel('Installation cancelled.');
48
+ process.exit(0);
49
+ }
50
+
51
+ answers[field.key] = value || field.default || '';
52
+ }
53
+ }
54
+
55
+ p.log.success('Configuration collected.');
56
+
57
+ return answers;
58
+ }