@fluxup/installer 1.3.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,154 @@
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/installer`), 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 10 etapas sequenciais:
23
+
24
+ 1. **Preflight** — verifica `docker` e `docker compose` v2 no servidor
25
+ 2. **Wizard** — coleta variáveis de configuração via prompts interativos (DB, rede, S3, GHCR)
26
+ 3. **SSM Parameter Store** — lê ARNs de IAM roles, URLs de filas SQS e nome da tabela DynamoDB via `GetParametersByPath`; nenhuma credencial estática é coletada
27
+ 4. **Secrets Manager** — busca o token GHCR no AWS Secrets Manager (em memória, nunca em disco)
28
+ 5. **GHCR Login** — `docker login ghcr.io --password-stdin` com o token obtido
29
+ 6. **Download S3** — baixa o `docker-compose.yml` template do bucket S3 via SDK
30
+ 7. **Render compose** — substitui os placeholders SSM (`${AWS_STS_BROKER_ROLE_ARN}` etc.) por valores literais no YAML; `${DB_*}`, `${NGINX_*}` permanecem para resolução pelo Docker Compose
31
+ 8. **Geração do .env** — cria `<dir>/.env` com permissão `0600` (apenas config do wizard; sem ARNs ou credenciais AWS)
32
+ 9. **Pull + Up** — `docker compose pull` seguido de `docker compose up -d`
33
+ 10. **Status** — exibe containers ativos e URLs de acesso
34
+
35
+ O nginx embute o build do Angular frontend como imagem GHCR. O servidor on-premises não precisa de código-fonte — apenas Docker.
36
+
37
+ ---
38
+
39
+ ## Como executar a instalação
40
+
41
+ ### Pré-requisitos no servidor
42
+
43
+ - Docker Engine ≥ 20.10 instalado e em execução
44
+ - Docker Compose v2 disponível (`docker compose version`)
45
+ - Node.js ≥ 18 disponível no `PATH` (apenas para o npx — pode ser removido após instalar)
46
+ - Acesso à internet: GHCR, bucket S3 e AWS Secrets Manager
47
+
48
+ ### Execução
49
+
50
+ ```bash
51
+ npx @fluxup/installer
52
+ ```
53
+
54
+ O wizard guiará pela configuração de todas as variáveis necessárias.
55
+
56
+ ### Flags disponíveis
57
+
58
+ | Flag | Descrição |
59
+ |------|-----------|
60
+ | `--dir <path>` | Diretório de instalação (padrão: `./fluxup`) |
61
+ | `--non-interactive` | Lê variáveis já exportadas no shell — útil para CI/automação |
62
+ | `--no-pull` | Pula o `docker compose pull` |
63
+
64
+ ### Modo não-interativo (CI/automação)
65
+
66
+ ```bash
67
+ export GHCR_USERNAME="Fluxup Deploy Bot"
68
+ export GHCR_SECRET_NAME="fluxup/ghcr-token"
69
+ export IMAGE_TAG="1.4.3"
70
+ export S3_BUCKET="fluxup-installer"
71
+ export S3_KEY="releases/v1.4.3/docker-compose.yml"
72
+ export AWS_REGION="us-east-1"
73
+ export AWS_SSM_PREFIX="/fluxup"
74
+ # DB e rede
75
+ export DB_NAME="fluxup"
76
+ export DB_USERNAME="fluxup"
77
+ export DB_PASSWORD="..."
78
+ export DB_PORT="5432"
79
+ export DB_BIND_HOST="127.0.0.1"
80
+ export JPA_DDL_AUTO="update"
81
+ export NGINX_PORT="80"
82
+ export BACKEND_API_TIMEOUT="5000"
83
+
84
+ # Credenciais AWS via instance profile (IMDSv2) — não definir AWS_ACCESS_KEY_ID/SECRET
85
+ npx @fluxup/installer --non-interactive --dir /opt/fluxup
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Configuração do secret GHCR no AWS Secrets Manager
91
+
92
+ Crie um secret com valor JSON:
93
+
94
+ ```json
95
+ { "token": "ghp_..." }
96
+ ```
97
+
98
+ O nome do secret é configurado via `GHCR_SECRET_NAME` (padrão: `fluxup/ghcr-token`).
99
+ O token **nunca** é armazenado em disco — apenas em memória durante o login.
100
+
101
+ ---
102
+
103
+ ## Estrutura do repositório
104
+
105
+ ```
106
+ compose/docker-compose.yml — compose baseado em imagens (fonte; CI publica no S3)
107
+ nginx/Dockerfile — build nginx + Angular frontend (usado só pelo CI)
108
+ nginx/nginx.conf — configuração do nginx (proxy + SPA fallback)
109
+ bin/cli.js — entrypoint da CLI
110
+ src/
111
+ preflight.js — verifica pré-requisitos Docker
112
+ wizard.js — prompts interativos agrupados
113
+ env.schema.js — definição declarativa de variáveis do wizard
114
+ aws-resources.js — lê ARNs e URLs do AWS SSM Parameter Store
115
+ compose-renderer.js — substitui valores SSM diretamente no YAML
116
+ secrets.js — busca token GHCR no AWS Secrets Manager
117
+ ghcr.js — docker login via --password-stdin
118
+ s3.js — download do compose do bucket S3
119
+ env.js — geração do .env (config do wizard)
120
+ docker.js — docker compose pull / up / ps
121
+ .github/workflows/
122
+ publish-nginx.yml — publica ghcr.io/fluxup-platform/fluxup-nginx:<ver>
123
+ upload-compose.yml — sobe compose no S3 em cada release tag
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Release
129
+
130
+ Em cada tag `vX.Y.Z`, os workflows CI executam automaticamente:
131
+
132
+ 1. `publish-nginx.yml` — builda o nginx com o frontend Angular e publica `ghcr.io/fluxup-platform/fluxup-nginx:X.Y.Z` no GHCR
133
+ 2. `upload-compose.yml` — sobe `compose/docker-compose.yml` para `s3://<bucket>/releases/X.Y.Z/docker-compose.yml`
134
+
135
+ 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.
136
+
137
+ ---
138
+
139
+ ## Contribuição
140
+
141
+ 1. Leia o [CONTRIBUTING.md](CONTRIBUTING.md) antes de começar.
142
+ 2. Crie uma branch de feature: `git checkout -b feature/descricao-curta`.
143
+ 3. Valide localmente: `npm install && node bin/cli.js --help`.
144
+ 4. Abra um pull request com descrição objetiva das mudanças.
145
+
146
+ ---
147
+
148
+ ## Autores
149
+
150
+ - [Diovani Motta](https://github.com/diovanibmotta)
151
+
152
+ ## Contato
153
+
154
+ Dúvidas ou sugestões: [contato@fluxup.com.br](mailto:contato@fluxup.com.br)
package/bin/cli.js ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fluxup-installer — FluxUp on-premises Docker installation CLI.
4
+ *
5
+ * Usage:
6
+ * npx @fluxup/installer
7
+ * npx @fluxup/installer --dir /opt/fluxup
8
+ * npx @fluxup/installer --non-interactive (reads env vars already exported in the shell)
9
+ * npx @fluxup/installer --no-pull (skips docker compose pull)
10
+ *
11
+ * AWS authentication:
12
+ * The installer uses the default AWS credential chain (instance profile / environment /
13
+ * shared credentials file). No static AWS credentials are collected or stored.
14
+ * The host must have an IAM role with permissions to read from SSM Parameter Store,
15
+ * S3 (compose bucket), and Secrets Manager (GHCR token secret).
16
+ */
17
+
18
+ import { join, resolve } from 'node:path';
19
+ import { mkdirSync } from 'node:fs';
20
+ import * as p from '@clack/prompts';
21
+ import pc from 'picocolors';
22
+
23
+ import { preflight } from '../src/preflight.js';
24
+ import { runWizard } from '../src/wizard.js';
25
+ import { resolveAwsResources } from '../src/aws-resources.js';
26
+ import { fetchGhcrToken } from '../src/secrets.js';
27
+ import { loginGhcr } from '../src/ghcr.js';
28
+ import { downloadCompose } from '../src/s3.js';
29
+ import { writeEnvFile } from '../src/env.js';
30
+ import { renderCompose } from '../src/compose-renderer.js';
31
+ import { pullImages, upServices, showStatus } from '../src/docker.js';
32
+
33
+ // ─── Flag parsing ─────────────────────────────────────────────────────────────
34
+ const args = process.argv.slice(2);
35
+ const flags = {
36
+ dir: args[args.indexOf('--dir') + 1] ?? './fluxup',
37
+ nonInteractive: args.includes('--non-interactive'),
38
+ noPull: args.includes('--no-pull'),
39
+ };
40
+
41
+ const installDir = resolve(flags.dir);
42
+
43
+ // ─── Main ────────────────────────────────────────────────────────────────────
44
+ async function main() {
45
+ console.log('');
46
+ console.log(pc.bgBlue(pc.white(' FluxUp Install ')));
47
+ console.log(pc.dim(`Install directory: ${installDir}\n`));
48
+
49
+ // 1. Preflight
50
+ p.log.step('Checking prerequisites...');
51
+ await preflight();
52
+
53
+ // 2. Wizard (or read from existing env vars in --non-interactive mode)
54
+ let answers;
55
+ if (flags.nonInteractive) {
56
+ p.log.info('Non-interactive mode: reading environment variables from shell.');
57
+ answers = Object.fromEntries(
58
+ process.env
59
+ ? Object.entries(process.env).filter(([k]) => k.match(/^(GHCR|IMAGE|S3|AWS|DB|JPA|NGINX|BACKEND)/))
60
+ : []
61
+ );
62
+ } else {
63
+ answers = await runWizard();
64
+ }
65
+
66
+ // 3. Resolve AWS resource identifiers from SSM Parameter Store
67
+ p.log.step('Resolving AWS resource identifiers from SSM Parameter Store...');
68
+ let awsResources;
69
+ try {
70
+ awsResources = await resolveAwsResources({
71
+ prefix: answers.AWS_SSM_PREFIX,
72
+ region: answers.AWS_REGION,
73
+ });
74
+ } catch (err) {
75
+ p.log.error(`Failed to resolve AWS resources from SSM: ${err.message}`);
76
+ p.log.warn(
77
+ 'Ensure the host has an IAM role with ssm:GetParametersByPath permission\n' +
78
+ `and that all parameters exist under: ${answers.AWS_SSM_PREFIX}`
79
+ );
80
+ process.exit(1);
81
+ }
82
+
83
+ // 4. Fetch GHCR token from AWS Secrets Manager (in-memory only — never written to disk)
84
+ p.log.step('Fetching GHCR token from AWS Secrets Manager...');
85
+ let ghcrToken;
86
+ try {
87
+ ghcrToken = await fetchGhcrToken({
88
+ secretName: answers.GHCR_SECRET_NAME,
89
+ region: answers.AWS_REGION,
90
+ });
91
+ p.log.success('GHCR token retrieved from Secrets Manager.');
92
+ } catch (err) {
93
+ p.log.error(`Failed to fetch GHCR token: ${err.message}`);
94
+ p.log.warn('Check the IAM role permissions and the secret name (GHCR_SECRET_NAME).');
95
+ process.exit(1);
96
+ }
97
+
98
+ // 5. GHCR login
99
+ p.log.step('Authenticating with GHCR...');
100
+ try {
101
+ await loginGhcr({ username: answers.GHCR_USERNAME, token: ghcrToken });
102
+ } catch (err) {
103
+ p.log.error(`GHCR login failed: ${err.message}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ // Discard token from memory
108
+ ghcrToken = null;
109
+
110
+ // 6. Create install directory and download docker-compose.yml from S3
111
+ mkdirSync(installDir, { recursive: true });
112
+ const composePath = join(installDir, 'docker-compose.yml');
113
+
114
+ p.log.step('Downloading docker-compose.yml from S3...');
115
+ try {
116
+ await downloadCompose({
117
+ bucket: answers.S3_BUCKET,
118
+ key: answers.S3_KEY,
119
+ region: answers.S3_REGION || answers.AWS_REGION,
120
+ destPath: composePath,
121
+ });
122
+ } catch (err) {
123
+ p.log.error(`Failed to download compose from S3: ${err.message}`);
124
+ process.exit(1);
125
+ }
126
+
127
+ // 7. Inline SSM-resolved values directly into docker-compose.yml
128
+ // ARNs, queue URLs, and table names are baked in as literal values so the compose
129
+ // file is self-contained. ${DB_*}, ${NGINX_*} etc. remain as placeholders resolved
130
+ // from .env at runtime.
131
+ p.log.step('Injecting SSM values into docker-compose.yml...');
132
+ try {
133
+ renderCompose(composePath, awsResources);
134
+ } catch (err) {
135
+ p.log.error(`Failed to inject SSM values into docker-compose.yml: ${err.message}`);
136
+ p.log.warn('Check for placeholder/key drift between compose template and SSM mapping.');
137
+ process.exit(1);
138
+ }
139
+
140
+ // 8. Write .env (wizard-collected config only — SSM values already inlined above)
141
+ p.log.step('Generating .env file...');
142
+ const envPath = writeEnvFile(installDir, answers);
143
+
144
+ // 9. Pull + Up
145
+ try {
146
+ if (!flags.noPull) {
147
+ await pullImages(composePath, envPath);
148
+ }
149
+ await upServices(composePath, envPath);
150
+ } catch (err) {
151
+ p.log.error(`Docker Compose failed: ${err.message}`);
152
+ process.exit(1);
153
+ }
154
+
155
+ // 10. Post-install
156
+ await showStatus(composePath, envPath);
157
+
158
+ const port = answers.NGINX_PORT ?? '80';
159
+ console.log('');
160
+ p.outro(
161
+ pc.green('✔ FluxUp installed successfully!') +
162
+ `\n\n Frontend: ${pc.cyan(`http://<server>:${port}/`)}\n` +
163
+ ` API: ${pc.cyan(`http://<server>:${port}/api/`)}\n` +
164
+ ` BFF: ${pc.cyan(`http://<server>:${port}/bff/`)}\n`
165
+ );
166
+ }
167
+
168
+ main().catch((err) => {
169
+ console.error(pc.red('\nUnexpected error:'), err);
170
+ process.exit(1);
171
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@fluxup/installer",
3
+ "version": "1.3.0",
4
+ "description": "FluxUp on-premises Docker installation CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "fluxup-installer": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "scripts": {
14
+ "lint": "node --check bin/cli.js src/*.js",
15
+ "check:exact-deps": "node scripts/check-exact-deps.js",
16
+ "build": "npm run lint && npm run check:exact-deps",
17
+ "semantic-release": "semantic-release"
18
+ },
19
+ "dependencies": {
20
+ "@aws-sdk/client-s3": "3.1063.0",
21
+ "@aws-sdk/client-secrets-manager": "3.1063.0",
22
+ "@aws-sdk/client-ssm": "3.1063.0",
23
+ "@clack/prompts": "0.9.1",
24
+ "execa": "9.6.1",
25
+ "picocolors": "1.1.1"
26
+ },
27
+ "devDependencies": {
28
+ "@semantic-release/changelog": "6.0.3",
29
+ "@semantic-release/git": "10.0.1",
30
+ "@semantic-release/github": "10.3.5",
31
+ "@semantic-release/npm": "12.0.2",
32
+ "conventional-changelog-conventionalcommits": "9.3.1",
33
+ "semantic-release": "24.2.9"
34
+ },
35
+ "engines": {
36
+ "node": ">=18",
37
+ "npm": ">=7"
38
+ },
39
+ "license": "UNLICENSED",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,74 @@
1
+ import { SSMClient, GetParametersByPathCommand } from '@aws-sdk/client-ssm';
2
+ import pc from 'picocolors';
3
+
4
+ /**
5
+ * Maps SSM parameter paths (relative to the prefix) to .env variable names.
6
+ * Infrastructure must publish these parameters before running the installer.
7
+ */
8
+ const SSM_PARAM_MAP = {
9
+ '/iam/sts-broker-role-arn': 'AWS_STS_BROKER_ROLE_ARN',
10
+ '/iam/secretsmanager-reader-role-arn': 'AWS_SECRETSMANAGER_READER_ROLE_ARN',
11
+ '/iam/dynamodb-role-arn': 'AWS_DYNAMODB_DAILY_JOBS_ROLE_ARN',
12
+ '/iam/sqs-consumer-role-arn': 'AWS_SQS_CONSUMER_ROLE_ARN',
13
+ '/iam/sqs-publisher-role-arn': 'AWS_SQS_PUBLISHER_ROLE_ARN',
14
+ '/dynamodb/daily-jobs-table': 'AWS_DAILY_JOBS_TABLE',
15
+ '/sqs/daily-request-queue-url': 'AWS_DAILY_REQUEST_QUEUE_URL',
16
+ '/sqs/daily-response-queue-url': 'AWS_DAILY_RESPONSE_QUEUE_URL',
17
+ '/sqs/daily-dlq-url': 'AWS_DAILY_DLQ_URL',
18
+ };
19
+
20
+ /**
21
+ * Resolves all required AWS resource identifiers from SSM Parameter Store.
22
+ * Uses the default AWS credential chain — no explicit credentials required.
23
+ * On EC2, this resolves via the instance profile (IMDSv2).
24
+ *
25
+ * @param {object} opts
26
+ * @param {string} opts.prefix SSM path prefix (e.g. '/fluxup')
27
+ * @param {string} opts.region AWS region
28
+ * @returns {Promise<Record<string, string>>} Map of env-var names to resolved values
29
+ */
30
+ export async function resolveAwsResources({ prefix, region }) {
31
+ const client = new SSMClient({ region });
32
+ const path = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
33
+
34
+ console.log(pc.dim(` Fetching SSM parameters under ${path}/...`));
35
+
36
+ const resolved = {};
37
+ let nextToken;
38
+
39
+ do {
40
+ const { Parameters, NextToken } = await client.send(
41
+ new GetParametersByPathCommand({
42
+ Path: path,
43
+ Recursive: true,
44
+ WithDecryption: true,
45
+ NextToken: nextToken,
46
+ })
47
+ );
48
+
49
+ for (const param of Parameters ?? []) {
50
+ const relative = param.Name.slice(path.length);
51
+ const envKey = SSM_PARAM_MAP[relative];
52
+ if (envKey) {
53
+ resolved[envKey] = param.Value;
54
+ }
55
+ }
56
+
57
+ nextToken = NextToken;
58
+ } while (nextToken);
59
+
60
+ const missing = Object.entries(SSM_PARAM_MAP)
61
+ .filter(([, envKey]) => !resolved[envKey])
62
+ .map(([ssmPath, envKey]) => `${path}${ssmPath} → ${envKey}`);
63
+
64
+ if (missing.length > 0) {
65
+ throw new Error(
66
+ `The following SSM parameters were not found:\n` +
67
+ missing.map((m) => ` • ${m}`).join('\n') +
68
+ `\n\nEnsure infrastructure has published all required parameters under ${path}/`
69
+ );
70
+ }
71
+
72
+ console.log(pc.green('✔') + ` Resolved ${Object.keys(resolved).length} AWS resource identifiers from SSM.`);
73
+ return resolved;
74
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import pc from 'picocolors';
3
+
4
+ /**
5
+ * Substitutes SSM-resolved values directly into the compose YAML.
6
+ *
7
+ * Only SSM keys are inlined — DB credentials, ports, and other wizard-collected
8
+ * vars remain as ${VAR} placeholders resolved by Docker Compose from .env at runtime.
9
+ *
10
+ * This makes the compose file self-contained for AWS resource identifiers:
11
+ * reading the file shows the actual ARNs/URLs without needing to cross-reference .env.
12
+ *
13
+ * @param {string} composePath Path to the docker-compose.yml on disk
14
+ * @param {Record<string, string>} ssmValues Map of env-var name → resolved value from SSM
15
+ */
16
+ export function renderCompose(composePath, ssmValues) {
17
+ let content = readFileSync(composePath, 'utf8');
18
+ const notApplied = [];
19
+
20
+ for (const [key, value] of Object.entries(ssmValues)) {
21
+ const token = `\${${key}}`;
22
+ if (!content.includes(token)) {
23
+ notApplied.push(key);
24
+ continue;
25
+ }
26
+ content = content.replaceAll(token, value);
27
+ }
28
+
29
+ if (notApplied.length > 0) {
30
+ throw new Error(
31
+ `Compose placeholders not found for resolved SSM keys: ${notApplied.join(', ')}`
32
+ );
33
+ }
34
+
35
+ writeFileSync(composePath, content, { encoding: 'utf8' });
36
+
37
+ console.log(
38
+ pc.green('✔') +
39
+ ` Inlined ${Object.keys(ssmValues).length} SSM values into docker-compose.yml`
40
+ );
41
+ }
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,46 @@
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
+ *
9
+ * Only schema-defined keys are written (wizard-collected values: GHCR, S3, AWS, DB, network).
10
+ * AWS resource identifiers (ARNs, queue URLs, table names) are NOT stored here —
11
+ * they are inlined directly into docker-compose.yml by compose-renderer.js at install time.
12
+ *
13
+ * The GHCR token is NOT included — fetched from Secrets Manager at runtime.
14
+ * AWS static credentials are NOT included — services authenticate via IAM instance profile.
15
+ * Permission 0600 applied (owner read-only).
16
+ *
17
+ * @param {string} dir Directory where .env will be created
18
+ * @param {Record<string, string>} answers Wizard answers (schema keys only)
19
+ * @returns {string} Path to the created .env file
20
+ */
21
+ export function writeEnvFile(dir, answers) {
22
+ const envPath = join(dir, '.env');
23
+
24
+ const schemaLines = ENV_SCHEMA.map((f) => `${f.key}=${answers[f.key] ?? ''}`);
25
+
26
+ const content = [
27
+ '# Generated by fluxup-installer — DO NOT commit',
28
+ '# AWS credentials are NOT stored here — services authenticate via IAM instance profile',
29
+ '# AWS resource identifiers (ARNs, queue URLs) are inlined in docker-compose.yml',
30
+ '',
31
+ ...schemaLines,
32
+ '',
33
+ ].join('\n');
34
+
35
+ writeFileSync(envPath, content, { encoding: 'utf8' });
36
+
37
+ try {
38
+ chmodSync(envPath, 0o600);
39
+ } catch {
40
+ // Windows does not support chmod — silently ignored
41
+ }
42
+
43
+ console.log(pc.green('✔') + ` .env created at ${envPath}`);
44
+
45
+ return envPath;
46
+ }
@@ -0,0 +1,121 @@
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
+ * AWS resource identifiers (ARNs, queue URLs, table names) are NOT collected here.
12
+ * They are resolved at install time from AWS SSM Parameter Store via aws-resources.js.
13
+ * AWS credentials are NOT collected — services authenticate via IAM instance profile.
14
+ */
15
+ export const ENV_SCHEMA = [
16
+ // ─── GHCR ────────────────────────────────────────────────────────────────
17
+ {
18
+ group: 'GHCR (GitHub Container Registry)',
19
+ key: 'GHCR_USERNAME',
20
+ prompt: 'GHCR username (e.g. Fluxup Deploy Bot)',
21
+ default: 'Fluxup Deploy Bot',
22
+ },
23
+ {
24
+ group: 'GHCR (GitHub Container Registry)',
25
+ key: 'IMAGE_TAG',
26
+ prompt: 'Installer image tag used for the nginx service (e.g. 1.4.3)',
27
+ default: 'latest',
28
+ },
29
+ {
30
+ group: 'GHCR (GitHub Container Registry)',
31
+ key: 'GHCR_SECRET_NAME',
32
+ prompt: 'Name/ARN of the AWS Secrets Manager secret that holds the GHCR token',
33
+ default: 'fluxup/ghcr-token',
34
+ },
35
+
36
+ // ─── S3 ──────────────────────────────────────────────────────────────────
37
+ {
38
+ group: 'S3 (docker-compose source)',
39
+ key: 'S3_BUCKET',
40
+ prompt: 'S3 bucket name that stores docker-compose.yml',
41
+ default: 'fluxup-installer',
42
+ },
43
+ {
44
+ group: 'S3 (docker-compose source)',
45
+ key: 'S3_KEY',
46
+ prompt: 'Object key in the bucket (e.g. v1.4.3/docker-compose.yml)',
47
+ default: 'docker-compose.yml',
48
+ },
49
+ {
50
+ group: 'S3 (docker-compose source)',
51
+ key: 'S3_REGION',
52
+ prompt: 'AWS region of the S3 bucket (leave blank to use AWS_REGION)',
53
+ default: '',
54
+ },
55
+
56
+ // ─── AWS ─────────────────────────────────────────────────────────────────
57
+ {
58
+ group: 'AWS',
59
+ key: 'AWS_REGION',
60
+ prompt: 'Default AWS region',
61
+ default: 'us-east-1',
62
+ },
63
+ {
64
+ group: 'AWS',
65
+ key: 'AWS_SSM_PREFIX',
66
+ prompt: 'SSM Parameter Store prefix where AWS resource identifiers are stored',
67
+ default: '/fluxup',
68
+ },
69
+
70
+ // ─── Database ────────────────────────────────────────────────────────────
71
+ {
72
+ group: 'Database (PostgreSQL)',
73
+ key: 'DB_NAME',
74
+ prompt: 'Database name',
75
+ default: 'fluxup',
76
+ },
77
+ {
78
+ group: 'Database (PostgreSQL)',
79
+ key: 'DB_USERNAME',
80
+ prompt: 'Database username',
81
+ default: 'fluxup',
82
+ },
83
+ {
84
+ group: 'Database (PostgreSQL)',
85
+ key: 'DB_PASSWORD',
86
+ prompt: 'Database password',
87
+ secret: true,
88
+ },
89
+ {
90
+ group: 'Database (PostgreSQL)',
91
+ key: 'DB_PORT',
92
+ prompt: 'Database host port (exposed on the host machine)',
93
+ default: '5432',
94
+ },
95
+ {
96
+ group: 'Database (PostgreSQL)',
97
+ key: 'DB_BIND_HOST',
98
+ prompt: 'Host address the DB port binds to (127.0.0.1 = localhost only; 0.0.0.0 = all interfaces)',
99
+ default: '127.0.0.1',
100
+ },
101
+ {
102
+ group: 'Database (PostgreSQL)',
103
+ key: 'JPA_DDL_AUTO',
104
+ prompt: 'JPA DDL Auto (update | validate | none)',
105
+ default: 'update',
106
+ },
107
+
108
+ // ─── Network ─────────────────────────────────────────────────────────────
109
+ {
110
+ group: 'Network',
111
+ key: 'NGINX_PORT',
112
+ prompt: 'nginx HTTP port on the host',
113
+ default: '80',
114
+ },
115
+ {
116
+ group: 'Network',
117
+ key: 'BACKEND_API_TIMEOUT',
118
+ prompt: 'Backend API timeout in ms',
119
+ default: '5000',
120
+ },
121
+ ];
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,35 @@
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
+ * Uses the default AWS credential chain — on EC2 this resolves via the instance
10
+ * profile (IMDSv2). No explicit credentials are required or accepted.
11
+ *
12
+ * @param {object} opts
13
+ * @param {string} opts.bucket
14
+ * @param {string} opts.key
15
+ * @param {string} opts.region
16
+ * @param {string} opts.destPath Local destination path (e.g. ./fluxup/docker-compose.yml)
17
+ */
18
+ export async function downloadCompose({ bucket, key, region, destPath }) {
19
+ const client = new S3Client({ region });
20
+
21
+ console.log(pc.dim(` s3://${bucket}/${key} → ${destPath}`));
22
+
23
+ const { Body } = await client.send(
24
+ new GetObjectCommand({ Bucket: bucket, Key: key })
25
+ );
26
+
27
+ if (!Body) {
28
+ throw new Error(`Object s3://${bucket}/${key} returned no Body.`);
29
+ }
30
+
31
+ mkdirSync(dirname(destPath), { recursive: true });
32
+ await pipeline(Body, createWriteStream(destPath));
33
+
34
+ console.log(pc.green('✔') + ` docker-compose.yml saved to ${destPath}`);
35
+ }
package/src/secrets.js ADDED
@@ -0,0 +1,35 @@
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
+ * Uses the default AWS credential chain — on EC2 this resolves via the instance
9
+ * profile (IMDSv2). The token is kept in memory only — never written to disk.
10
+ *
11
+ * @param {object} opts
12
+ * @param {string} opts.secretName Secret name or ARN (e.g. 'fluxup/ghcr-token')
13
+ * @param {string} opts.region AWS region
14
+ * @returns {Promise<string>} GHCR token
15
+ */
16
+ export async function fetchGhcrToken({ secretName, region }) {
17
+ const client = new SecretsManagerClient({ region });
18
+
19
+ const response = await client.send(
20
+ new GetSecretValueCommand({ SecretId: secretName })
21
+ );
22
+
23
+ const raw = response.SecretString;
24
+ if (!raw) {
25
+ throw new Error(`Secret "${secretName}" has no SecretString.`);
26
+ }
27
+
28
+ // Supports secret as JSON { "token": "ghp_..." } or plain string
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ return parsed.token ?? parsed.GHCR_TOKEN ?? raw;
32
+ } catch {
33
+ return raw.trim();
34
+ }
35
+ }
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
+ }