@jumpgroup/laravel-tools 3.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/.claude/settings.local.json +59 -0
- package/README.md +378 -0
- package/bin/groups/cache.js +52 -0
- package/bin/groups/database.js +105 -0
- package/bin/groups/forge.js +272 -0
- package/bin/groups/local.js +78 -0
- package/bin/groups/media.js +110 -0
- package/bin/tools.js +23 -0
- package/docs/Changelog.md +267 -0
- package/docs/TODO.md +167 -0
- package/docs/releases/release_0.0.1.md +116 -0
- package/docs/releases/release_0.0.2.md +88 -0
- package/docs/releases/release_0.0.3.md +58 -0
- package/docs/releases/release_0.0.4.md +128 -0
- package/docs/releases/release_0.0.5.md +77 -0
- package/docs/releases/release_0.0.6.md +80 -0
- package/docs/releases/release_1.0.0.md +61 -0
- package/docs/releases/release_1.0.1.md +18 -0
- package/docs/releases/release_1.0.2.md +18 -0
- package/docs/releases/release_1.0.3.md +19 -0
- package/docs/releases/release_1.1.0.md +18 -0
- package/docs/releases/release_1.1.1.md +17 -0
- package/docs/releases/release_1.1.2.md +18 -0
- package/docs/releases/release_1.1.3.md +21 -0
- package/docs/releases/release_1.1.4.md +18 -0
- package/docs/releases/release_1.1.5.md +18 -0
- package/docs/releases/release_1.1.6.md +21 -0
- package/docs/releases/release_1.1.7.md +17 -0
- package/docs/releases/release_2.0.0.md +192 -0
- package/docs/releases/release_2.0.1.md +53 -0
- package/docs/releases/release_2.0.2.md +55 -0
- package/docs/releases/release_2.0.3.md +69 -0
- package/docs/releases/release_2.1.0.md +59 -0
- package/docs/releases/release_2.2.0.md +83 -0
- package/docs/releases/release_2.2.1.md +36 -0
- package/docs/releases/release_2.2.2.md +57 -0
- package/docs/releases/release_2.2.3.md +39 -0
- package/docs/releases/release_2.2.4.md +75 -0
- package/docs/releases/release_2.2.5.md +69 -0
- package/docs/releases/release_3.0.0.md +87 -0
- package/docs/releases/release_3.0.1.md +65 -0
- package/docs/releases/release_3.1.0.md +90 -0
- package/docs/releases/release_3.2.0.md +74 -0
- package/docs/releases/release_3.3.0.md +72 -0
- package/package.json +35 -0
- package/src/aws/bucket.js +287 -0
- package/src/aws/cloudfront.js +433 -0
- package/src/aws/config.js +39 -0
- package/src/aws/iam.js +189 -0
- package/src/cache.js +49 -0
- package/src/database.js +315 -0
- package/src/forge/client.js +43 -0
- package/src/forge/config.js +33 -0
- package/src/forge/provisioning.js +191 -0
- package/src/forge/servers.js +27 -0
- package/src/forge/sites.js +93 -0
- package/src/google/groupMembers.js +35 -0
- package/src/google/utilities.js +39 -0
- package/src/local/doctor.js +214 -0
- package/src/local/setup.js +398 -0
- package/src/media.js +143 -0
- package/src/stub/docker/mysql/my.cnf +6 -0
- package/src/stub/docker/php/local.ini +4 -0
- package/src/stub/docker/traefik/dynamic_conf.yml +4 -0
- package/src/stub/docker/traefik/traefik.yml +24 -0
- package/src/stub/docker-compose/php8.0/docker-compose.yml +78 -0
- package/src/stub/docker-compose/php8.1/docker-compose.yml +78 -0
- package/src/stub/docker-compose/php8.2/docker-compose.yml +78 -0
- package/src/stub/docker-compose/php8.3/docker-compose.yml +78 -0
- package/src/stub/docker-compose/php8.4/docker-compose.yml +78 -0
- package/src/stub/docker-compose.yml +78 -0
- package/src/utilities/command.js +137 -0
- package/src/utilities/dateUtils.js +7 -0
- package/src/utilities/fileUtils.js +36 -0
- package/src/utilities/google-drive.js +69 -0
- package/src/utilities/pathUtils.js +15 -0
- package/src/utilities/userInput.js +28 -0
- package/src/utilities/utilities.js +57 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { parse } from 'dotenv';
|
|
5
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
6
|
+
import { replaceSecrets, replaceFiles } from '@jumpgroup/secret-fetcher';
|
|
7
|
+
|
|
8
|
+
import { checkIfEnvFilesExist } from '../utilities/utilities.js';
|
|
9
|
+
import { executeCommand, executeCommandWithOutput, ensureCommandsAvailable } from '../utilities/command.js';
|
|
10
|
+
import { setupAWS } from '../media.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
13
|
+
const STUB_DIR = resolve(__dirname, '../../src/stub');
|
|
14
|
+
|
|
15
|
+
const VALID_PHP_VERSIONS = ['php8.0', 'php8.1', 'php8.2', 'php8.3', 'php8.4'];
|
|
16
|
+
const DEFAULT_PHP_VERSION = 'php8.4';
|
|
17
|
+
const SECRET_FETCHER_ERROR = 'Secret fetcher non è inizializzato, contattare Giada o Andrea';
|
|
18
|
+
|
|
19
|
+
const ensureSetupRepoPrerequisites = () => {
|
|
20
|
+
ensureCommandsAvailable([
|
|
21
|
+
{
|
|
22
|
+
command: 'docker',
|
|
23
|
+
args: ['compose', 'version'],
|
|
24
|
+
label: 'docker compose',
|
|
25
|
+
installHint: 'Installa Docker Desktop e verifica che `docker compose` funzioni dal terminale.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: 'mkcert',
|
|
29
|
+
args: ['-help'],
|
|
30
|
+
label: 'mkcert',
|
|
31
|
+
installHint: 'Installa mkcert ed esegui `mkcert -install` sulla tua macchina.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
command: 'sudo',
|
|
35
|
+
args: ['--version'],
|
|
36
|
+
label: 'sudo',
|
|
37
|
+
installHint: 'Il flusso locale richiede `sudo` per aggiornare `/etc/hosts` tramite `hostile`.',
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ensureSetupLaravelPrerequisites = () => {
|
|
43
|
+
ensureCommandsAvailable([
|
|
44
|
+
{
|
|
45
|
+
command: 'docker',
|
|
46
|
+
args: ['compose', 'version'],
|
|
47
|
+
label: 'docker compose',
|
|
48
|
+
installHint: 'Installa Docker Desktop e verifica che `docker compose` funzioni dal terminale.',
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ensureSecretFetcherExists = () => {
|
|
54
|
+
if (!existsSync('.secret-fetcher')) {
|
|
55
|
+
throw new Error(SECRET_FETCHER_ERROR);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const normalizeProjectName = (value) =>
|
|
60
|
+
value
|
|
61
|
+
.trim()
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[_\s]+/g, '-')
|
|
64
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
65
|
+
.replace(/-+/g, '-')
|
|
66
|
+
.replace(/^-|-$/g, '');
|
|
67
|
+
|
|
68
|
+
const readEnvExample = () => parse(readFileSync('.env.example'));
|
|
69
|
+
|
|
70
|
+
const updateProjectIdentityInEnvExample = (appName) => {
|
|
71
|
+
const content = readFileSync('.env.example', 'utf8');
|
|
72
|
+
const hasAppName = /^APP_NAME=.*$/m.test(content);
|
|
73
|
+
|
|
74
|
+
if (!hasAppName) {
|
|
75
|
+
throw new Error('APP_NAME non trovato in .env.example');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const localUrl = `https://api.${appName}.test`;
|
|
79
|
+
let updatedContent = content.replace(/^APP_NAME=.*$/m, `APP_NAME=${appName}`);
|
|
80
|
+
|
|
81
|
+
if (/^APP_URL=.*$/m.test(updatedContent)) {
|
|
82
|
+
updatedContent = updatedContent.replace(/^APP_URL=.*$/m, `APP_URL=${localUrl}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (/^ASSETS_URL=.*$/m.test(updatedContent)) {
|
|
86
|
+
updatedContent = updatedContent.replace(/^ASSETS_URL=.*$/m, `ASSETS_URL=${localUrl}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
writeFileSync('.env.example', updatedContent);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const generateLocalCertificates = async (appName) => {
|
|
93
|
+
const certsDir = join(resolve('.'), 'docker', 'certs');
|
|
94
|
+
await executeCommand('mkcert', [
|
|
95
|
+
'-cert-file',
|
|
96
|
+
'site.test.pem',
|
|
97
|
+
'-key-file',
|
|
98
|
+
'site.key',
|
|
99
|
+
`api.${appName}.test`,
|
|
100
|
+
`mail.${appName}.test`,
|
|
101
|
+
], { cwd: certsDir });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const runLaravelHealthCheck = async (appName) => {
|
|
105
|
+
try {
|
|
106
|
+
await executeCommandWithOutput('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'about']);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`Health check Laravel fallito:\n${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const runNoFilamentRecovery = async (appName) => {
|
|
113
|
+
console.log('🛠️ Tentativo recovery dipendenze/caches (setup senza Filament)...');
|
|
114
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'composer', 'install', '--no-interaction']);
|
|
115
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'package:discover']);
|
|
116
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'storage:link']);
|
|
117
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'optimize:clear']);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const setupProject = async () => {
|
|
121
|
+
checkIfEnvFilesExist();
|
|
122
|
+
ensureSecretFetcherExists();
|
|
123
|
+
|
|
124
|
+
const envExample = readEnvExample();
|
|
125
|
+
const currentAppName = envExample.APP_NAME;
|
|
126
|
+
|
|
127
|
+
if (!currentAppName) {
|
|
128
|
+
throw new Error('APP_NAME non trovato in .env.example');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const projectName = await input({
|
|
132
|
+
message: 'Nome progetto locale (usato per URL locale e naming Docker):',
|
|
133
|
+
default: currentAppName,
|
|
134
|
+
validate: (value) => {
|
|
135
|
+
if (!value.trim()) {
|
|
136
|
+
return 'Il nome progetto non puo\' essere vuoto.';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!normalizeProjectName(value)) {
|
|
140
|
+
return 'Il nome progetto deve contenere almeno un carattere valido.';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const normalizedAppName = normalizeProjectName(projectName);
|
|
148
|
+
|
|
149
|
+
if (normalizedAppName !== projectName.trim()) {
|
|
150
|
+
const shouldUseNormalizedName = await confirm({
|
|
151
|
+
message: `Il nome verra' salvato come slug locale "${normalizedAppName}". Confermi?`,
|
|
152
|
+
default: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!shouldUseNormalizedName) {
|
|
156
|
+
throw new Error('Operazione annullata. Inserisci un nome progetto gia\' compatibile con hostname e naming Docker.');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
updateProjectIdentityInEnvExample(normalizedAppName);
|
|
161
|
+
|
|
162
|
+
const setupMedia = await confirm({
|
|
163
|
+
message: 'Configurare anche media stack AWS (S3 + CloudFront)?',
|
|
164
|
+
default: false,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (setupMedia) {
|
|
168
|
+
console.log('☁️ Avvio setup media AWS...');
|
|
169
|
+
await setupAWS(normalizedAppName, { updateEnvExample: true });
|
|
170
|
+
} else {
|
|
171
|
+
console.log('⏭️ Setup media AWS saltato (puoi eseguirlo dopo con `laravel-tools media setup-general`).');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(`📛 Nome progetto locale impostato su: ${normalizedAppName}`);
|
|
175
|
+
console.log('\n✅ Setup progetto completato!');
|
|
176
|
+
console.log(' Prossimi passi:');
|
|
177
|
+
console.log(' 1. yarn repo-setup');
|
|
178
|
+
console.log(' 2. yarn start');
|
|
179
|
+
console.log(' 3. yarn setup-laravel\n');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// local setup-repo
|
|
184
|
+
//
|
|
185
|
+
// Flow:
|
|
186
|
+
// 1. Assert .env.example exists
|
|
187
|
+
// 2. Assert local prerequisites exist
|
|
188
|
+
// 3. Assert .secret-fetcher exists
|
|
189
|
+
// 4. Inject secrets from .env.example → .env
|
|
190
|
+
// 5. Copy stub files to project root, substituting ${VAR} tokens
|
|
191
|
+
// 6. Create docker/certs/ directory
|
|
192
|
+
// 7. Generate mkcert SSL certificates for {APP_NAME}.test and mail.{APP_NAME}.test
|
|
193
|
+
// directly as site.test.pem / site.key
|
|
194
|
+
// 8. Add {APP_NAME}.test and mail.{APP_NAME}.test to /etc/hosts
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
export const setupRepo = async (options = {}) => {
|
|
197
|
+
checkIfEnvFilesExist();
|
|
198
|
+
ensureSetupRepoPrerequisites();
|
|
199
|
+
ensureSecretFetcherExists();
|
|
200
|
+
|
|
201
|
+
const phpVersion = options.phpversion || DEFAULT_PHP_VERSION;
|
|
202
|
+
if (!VALID_PHP_VERSIONS.includes(phpVersion)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Versione PHP '${phpVersion}' non supportata. Valori ammessi: ${VALID_PHP_VERSIONS.join(', ')}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('🔐 Generazione di .env tramite secret fetcher...');
|
|
209
|
+
await replaceSecrets({ input: '.env.example', output: '.env' });
|
|
210
|
+
|
|
211
|
+
const env = parse(readFileSync('.env'));
|
|
212
|
+
const appName = env.APP_NAME;
|
|
213
|
+
const resolvedEnv = {
|
|
214
|
+
...env,
|
|
215
|
+
DB_DATABASE: env.DB_DATABASE || env.DB_NAME,
|
|
216
|
+
DB_NAME: env.DB_NAME || env.DB_DATABASE,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (!appName) {
|
|
220
|
+
throw new Error('APP_NAME non trovato in .env. Verifica .env.example e la configurazione di secret fetcher.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log(`📋 Progetto locale: ${appName} (${phpVersion})`);
|
|
224
|
+
|
|
225
|
+
console.log('📁 Copia degli stub Docker e Traefik...');
|
|
226
|
+
await replaceFiles(`${STUB_DIR}/**`, `${resolve('.')}/`, resolvedEnv);
|
|
227
|
+
|
|
228
|
+
// If a non-default PHP version is requested, overwrite docker-compose.yml
|
|
229
|
+
if (phpVersion !== DEFAULT_PHP_VERSION) {
|
|
230
|
+
const src = join(STUB_DIR, 'docker-compose', phpVersion, 'docker-compose.yml');
|
|
231
|
+
const content = readFileSync(src, 'utf8').replace(
|
|
232
|
+
/\$\{([^}]+)\}/g,
|
|
233
|
+
(_, key) => resolvedEnv[key] ?? `\${${key}}`
|
|
234
|
+
);
|
|
235
|
+
writeFileSync(join(resolve('.'), 'docker-compose.yml'), content);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Remove the docker-compose/ version subfolder — it's internal to laravel-tools
|
|
239
|
+
const versionDir = join(resolve('.'), 'docker-compose');
|
|
240
|
+
if (existsSync(versionDir)) {
|
|
241
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Ensure mysql/ and docker/certs/ are gitignored
|
|
245
|
+
const gitignorePath = join(resolve('.'), '.gitignore');
|
|
246
|
+
const gitignoreEntries = ['mysql/', 'docker/certs/'];
|
|
247
|
+
const gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
248
|
+
const missingEntries = gitignoreEntries.filter((entry) => !gitignoreContent.includes(entry));
|
|
249
|
+
if (missingEntries.length > 0) {
|
|
250
|
+
writeFileSync(gitignorePath, gitignoreContent + (gitignoreContent.endsWith('\n') ? '' : '\n') + missingEntries.join('\n') + '\n');
|
|
251
|
+
console.log(`📝 Aggiunte voci a .gitignore: ${missingEntries.join(', ')}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('📁 Creazione della cartella docker/certs...');
|
|
255
|
+
mkdirSync(join('docker', 'certs'), { recursive: true });
|
|
256
|
+
|
|
257
|
+
console.log(`🔒 Generazione dei certificati SSL per api.${appName}.test e mail.${appName}.test...`);
|
|
258
|
+
await generateLocalCertificates(appName);
|
|
259
|
+
|
|
260
|
+
console.log(`🌐 Aggiornamento di /etc/hosts con api.${appName}.test e mail.${appName}.test...`);
|
|
261
|
+
await executeCommand('sudo', ['hostile', 'set', '127.0.0.1', `api.${appName}.test`]);
|
|
262
|
+
await executeCommand('sudo', ['hostile', 'set', '127.0.0.1', `mail.${appName}.test`]);
|
|
263
|
+
|
|
264
|
+
console.log('📦 Installazione delle dipendenze PHP...');
|
|
265
|
+
await executeCommand('composer', ['install']);
|
|
266
|
+
|
|
267
|
+
console.log(`\n✅ Repo setup completo!`);
|
|
268
|
+
console.log(' Prossimi passi:');
|
|
269
|
+
console.log(' 1. yarn start');
|
|
270
|
+
console.log(' 2. yarn setup-laravel\n');
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// local user add
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
export const addUser = async () => {
|
|
277
|
+
if (!existsSync('.env')) {
|
|
278
|
+
throw new Error('.env non trovato. Completa prima il setup del progetto.');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const env = parse(readFileSync('.env'));
|
|
282
|
+
const appName = env.APP_NAME;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', '--version'], {
|
|
286
|
+
stdio: 'pipe',
|
|
287
|
+
});
|
|
288
|
+
} catch {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Il container ${appName}-api non e' in esecuzione. Avvia lo stack con: yarn start`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await executeCommand('docker', ['exec', '-it', `${appName}-api`, 'php', 'artisan', 'make:filament-user']);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// local setup-laravel
|
|
299
|
+
//
|
|
300
|
+
// Flow:
|
|
301
|
+
// 1. Assert .env exists (after setup-project + setup-repo)
|
|
302
|
+
// 2. Check if Laravel is already initialised (APP_KEY set)
|
|
303
|
+
// 3. Generate app key
|
|
304
|
+
// 4. Run migrations (+ optional seed)
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
export const setupLaravel = async () => {
|
|
307
|
+
if (!existsSync('.env')) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
'.env non trovato. Completa prima `laravel-tools local setup-project` e poi `yarn repo-setup`.'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
ensureSetupLaravelPrerequisites();
|
|
314
|
+
|
|
315
|
+
const env = parse(readFileSync('.env'));
|
|
316
|
+
const appName = env.APP_NAME;
|
|
317
|
+
|
|
318
|
+
// Check if the container is running
|
|
319
|
+
try {
|
|
320
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', '--version'], {
|
|
321
|
+
stdio: 'pipe',
|
|
322
|
+
});
|
|
323
|
+
} catch {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Il container ${appName}-api non e' in esecuzione. Avvia lo stack con: yarn start`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const installFilament = await confirm({ message: 'Installare Filament?', default: false });
|
|
330
|
+
|
|
331
|
+
if (env.APP_KEY && env.APP_KEY !== '') {
|
|
332
|
+
console.log('🔑 APP_KEY gia\' presente, salto key:generate.');
|
|
333
|
+
} else {
|
|
334
|
+
console.log('🔑 Generazione della chiave applicativa...');
|
|
335
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'key:generate']);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (installFilament) {
|
|
339
|
+
console.log('📦 Installazione di Filament...');
|
|
340
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'composer', 'require', 'filament/filament:^3.2', '-W']);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const migrationsDir = join(resolve('.'), 'database', 'migrations');
|
|
344
|
+
const hasSessionsMigration = existsSync(migrationsDir) &&
|
|
345
|
+
readdirSync(migrationsDir).some((f) => f.includes('create_sessions_table'));
|
|
346
|
+
|
|
347
|
+
if (!hasSessionsMigration) {
|
|
348
|
+
console.log('📋 Generazione della tabella sessioni...');
|
|
349
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'session:table']);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log('🗄️ Esecuzione delle migration...');
|
|
353
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'migrate', '--graceful']);
|
|
354
|
+
|
|
355
|
+
if (installFilament) {
|
|
356
|
+
console.log('🎨 Configurazione di Filament...');
|
|
357
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'filament:install', '--panels', '--no-interaction']);
|
|
358
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'filament:assets']);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log('🧹 Pulizia cache Laravel...');
|
|
362
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'optimize:clear']);
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await runLaravelHealthCheck(appName);
|
|
366
|
+
console.log('✅ Bootstrap Laravel verificato.');
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (!installFilament) {
|
|
369
|
+
await runNoFilamentRecovery(appName);
|
|
370
|
+
try {
|
|
371
|
+
await runLaravelHealthCheck(appName);
|
|
372
|
+
console.log('✅ Bootstrap Laravel verificato dopo recovery.');
|
|
373
|
+
} catch (recoveryError) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
'Bootstrap Laravel fallito anche dopo recovery senza Filament. ' +
|
|
376
|
+
'Controlla dipendenze Composer del progetto (Livewire/Filament) e rilancia.\n' +
|
|
377
|
+
recoveryError.message
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
throw new Error(
|
|
382
|
+
'Bootstrap Laravel fallito dopo installazione Filament. ' +
|
|
383
|
+
'Verifica che tutte le dipendenze Composer siano compatibili e rilancia.\n' +
|
|
384
|
+
error.message
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const seed = await confirm({ message: 'Eseguire anche i seeder?', default: false });
|
|
390
|
+
if (seed) {
|
|
391
|
+
console.log('🌱 Esecuzione dei seeder...');
|
|
392
|
+
await executeCommand('docker', ['exec', `${appName}-api`, 'php', 'artisan', 'db:seed']);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(`\n✅ Laravel setup completo!`);
|
|
396
|
+
console.log(` Apri: https://api.${appName}.test`);
|
|
397
|
+
console.log(` Mailpit: https://mail.${appName}.test\n`);
|
|
398
|
+
};
|
package/src/media.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import { addUpdateSecret } from '@jumpgroup/secret-fetcher';
|
|
4
|
+
|
|
5
|
+
import { applyCloudFrontReadPolicy, createBucket } from './aws/bucket.js';
|
|
6
|
+
import {
|
|
7
|
+
getDistributionByProjectTagStrict,
|
|
8
|
+
resolveOrCreateDistributionForProject,
|
|
9
|
+
} from './aws/cloudfront.js';
|
|
10
|
+
import { createIAMUser } from './aws/iam.js';
|
|
11
|
+
import { getAppName } from './utilities/command.js';
|
|
12
|
+
import { getAwsAccountId, getAwsRegion } from './aws/config.js';
|
|
13
|
+
|
|
14
|
+
const setOrAppendEnvVar = (content, key, value) => {
|
|
15
|
+
const normalizedValue = String(value ?? '');
|
|
16
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
17
|
+
if (regex.test(content)) {
|
|
18
|
+
return content.replace(regex, `${key}=${normalizedValue}`);
|
|
19
|
+
}
|
|
20
|
+
return `${content}${content.endsWith('\n') ? '' : '\n'}${key}=${normalizedValue}\n`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const writeMediaConfigToEnvExample = (mediaConfig) => {
|
|
24
|
+
if (!existsSync('.env.example')) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let content = readFileSync('.env.example', 'utf8');
|
|
29
|
+
|
|
30
|
+
const pairs = {
|
|
31
|
+
AWS_DEFAULT_REGION: mediaConfig.region,
|
|
32
|
+
AWS_BUCKET: mediaConfig.bucketName,
|
|
33
|
+
AWS_URL: `https://${mediaConfig.cloudfrontDomain}/`,
|
|
34
|
+
CLOUDFRONT_DISTRIBUTION_ID: mediaConfig.cloudfrontId,
|
|
35
|
+
CLOUDFRONT_DOMAIN: mediaConfig.cloudfrontDomain,
|
|
36
|
+
S3_SITE_BUCKET: mediaConfig.bucketName,
|
|
37
|
+
S3_UPLOADS_BUCKET_URL: `https://${mediaConfig.cloudfrontDomain}/`,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(pairs)) {
|
|
41
|
+
content = setOrAppendEnvVar(content, key, value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeFileSync('.env.example', content);
|
|
45
|
+
console.log('📝 Config media aggiornata in .env.example');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const pushMediaCredentialsToSecretFetcher = async (credentials) => {
|
|
49
|
+
if (!existsSync('.secret-fetcher')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const note = yaml.dump(credentials);
|
|
54
|
+
await addUpdateSecret({ note, env: 'site' });
|
|
55
|
+
console.log('🔐 Credenziali media inviate a secret-fetcher (env=site)');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const setupAWS = async (projectName, options = {}) => {
|
|
59
|
+
const resolvedProjectName = projectName || getAppName();
|
|
60
|
+
const region = getAwsRegion();
|
|
61
|
+
const dryRun = Boolean(options.dryRun);
|
|
62
|
+
|
|
63
|
+
console.log(`📦 Setup media stack per: ${resolvedProjectName}${dryRun ? ' [DRY-RUN]' : ''}`);
|
|
64
|
+
const bucket = await createBucket(resolvedProjectName, { dryRun });
|
|
65
|
+
const distribution = await resolveOrCreateDistributionForProject({
|
|
66
|
+
projectName: resolvedProjectName,
|
|
67
|
+
bucketName: bucket.name,
|
|
68
|
+
dryRun,
|
|
69
|
+
});
|
|
70
|
+
const accountId = await getAwsAccountId();
|
|
71
|
+
|
|
72
|
+
await applyCloudFrontReadPolicy({
|
|
73
|
+
bucketName: bucket.name,
|
|
74
|
+
distributionId: distribution.id,
|
|
75
|
+
accountId,
|
|
76
|
+
dryRun,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const credentials = await createIAMUser(resolvedProjectName, distribution.id, { dryRun });
|
|
80
|
+
|
|
81
|
+
const mediaConfig = {
|
|
82
|
+
region,
|
|
83
|
+
bucketName: bucket.name,
|
|
84
|
+
cloudfrontId: distribution.id,
|
|
85
|
+
cloudfrontDomain: distribution.domainName,
|
|
86
|
+
...credentials,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (options.updateEnvExample !== false && !dryRun) {
|
|
90
|
+
writeMediaConfigToEnvExample(mediaConfig);
|
|
91
|
+
} else if (dryRun) {
|
|
92
|
+
console.log('🧪 [dry-run] Skip update di .env.example');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!dryRun) {
|
|
96
|
+
await pushMediaCredentialsToSecretFetcher(credentials);
|
|
97
|
+
} else {
|
|
98
|
+
console.log('🧪 [dry-run] Skip push credenziali a secret-fetcher.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('✅ Media stack configurato.');
|
|
102
|
+
console.table([
|
|
103
|
+
{
|
|
104
|
+
bucket: mediaConfig.bucketName,
|
|
105
|
+
cloudfrontId: mediaConfig.cloudfrontId,
|
|
106
|
+
cloudfrontDomain: mediaConfig.cloudfrontDomain,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
return mediaConfig;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const setupIAM = async (projectName, cloudfrontId = null, options = {}) => {
|
|
114
|
+
const resolvedProjectName = projectName || getAppName();
|
|
115
|
+
const dryRun = Boolean(options.dryRun);
|
|
116
|
+
let resolvedCloudfrontId = cloudfrontId || null;
|
|
117
|
+
|
|
118
|
+
if (!resolvedCloudfrontId && !dryRun) {
|
|
119
|
+
const distribution = await getDistributionByProjectTagStrict(resolvedProjectName);
|
|
120
|
+
resolvedCloudfrontId = distribution?.id || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!resolvedCloudfrontId && dryRun) {
|
|
124
|
+
resolvedCloudfrontId = `DRYRUN-${resolvedProjectName}`;
|
|
125
|
+
console.log(
|
|
126
|
+
`🧪 [dry-run] Nessuna distribuzione risolta: uso CloudFront ID simulato ${resolvedCloudfrontId}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const credentials = await createIAMUser(resolvedProjectName, resolvedCloudfrontId, { dryRun });
|
|
131
|
+
if (!dryRun) {
|
|
132
|
+
await pushMediaCredentialsToSecretFetcher(credentials);
|
|
133
|
+
} else {
|
|
134
|
+
console.log('🧪 [dry-run] Skip push credenziali a secret-fetcher.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.table([credentials]);
|
|
138
|
+
return credentials;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Backward-compatible aliases (old laravel-tools naming).
|
|
142
|
+
export const setupMediaStack = setupAWS;
|
|
143
|
+
export const setupMediaIam = setupIAM;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## traefik.yml
|
|
2
|
+
## Static configuration
|
|
3
|
+
entryPoints:
|
|
4
|
+
http:
|
|
5
|
+
address: ":80"
|
|
6
|
+
http:
|
|
7
|
+
redirections:
|
|
8
|
+
entryPoint:
|
|
9
|
+
to: https
|
|
10
|
+
scheme: https
|
|
11
|
+
https:
|
|
12
|
+
address: ":443"
|
|
13
|
+
# Docker configuration backend
|
|
14
|
+
providers:
|
|
15
|
+
docker:
|
|
16
|
+
endpoint: "unix:///var/run/docker.sock"
|
|
17
|
+
network: api-network
|
|
18
|
+
exposedByDefault: false
|
|
19
|
+
file:
|
|
20
|
+
filename: /etc/traefik/dynamic_conf.yml
|
|
21
|
+
# API and dashboard configuration
|
|
22
|
+
api:
|
|
23
|
+
insecure: true
|
|
24
|
+
dashboard: true
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
services:
|
|
2
|
+
traefik:
|
|
3
|
+
image: "traefik:v3.6.11"
|
|
4
|
+
container_name: ${APP_NAME}-traefik
|
|
5
|
+
networks:
|
|
6
|
+
- api-network
|
|
7
|
+
ports:
|
|
8
|
+
- "80:80"
|
|
9
|
+
- "443:443"
|
|
10
|
+
- "8081:8080"
|
|
11
|
+
volumes:
|
|
12
|
+
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
|
13
|
+
- ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml
|
|
14
|
+
- ./docker/traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml
|
|
15
|
+
- ./docker/certs:/etc/traefik/certs
|
|
16
|
+
|
|
17
|
+
api:
|
|
18
|
+
image: jumpgroupit/laravel-image:0.2.2
|
|
19
|
+
container_name: ${APP_NAME}-api
|
|
20
|
+
command: "php artisan serve --host=0.0.0.0 --port 80"
|
|
21
|
+
restart: unless-stopped
|
|
22
|
+
environment:
|
|
23
|
+
SERVICE_NAME: app
|
|
24
|
+
SERVICE_TAGS: dev
|
|
25
|
+
working_dir: /var/www
|
|
26
|
+
volumes:
|
|
27
|
+
- ./:/var/www
|
|
28
|
+
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
|
29
|
+
- ./docker/certs:/etc/certs
|
|
30
|
+
networks:
|
|
31
|
+
api-network:
|
|
32
|
+
aliases:
|
|
33
|
+
- ${APP_NAME}.test
|
|
34
|
+
depends_on:
|
|
35
|
+
- db
|
|
36
|
+
- mailpit
|
|
37
|
+
labels:
|
|
38
|
+
- "traefik.enable=true"
|
|
39
|
+
- "traefik.http.routers.app.entrypoints=http,https"
|
|
40
|
+
- "traefik.http.routers.app.rule=Host(`${APP_NAME}.test`)"
|
|
41
|
+
- "traefik.http.routers.app.tls=true"
|
|
42
|
+
ports:
|
|
43
|
+
- 8000:80
|
|
44
|
+
|
|
45
|
+
db:
|
|
46
|
+
image: mysql:8.4
|
|
47
|
+
container_name: ${APP_NAME}-mysql
|
|
48
|
+
restart: unless-stopped
|
|
49
|
+
ports:
|
|
50
|
+
- "3306:3306"
|
|
51
|
+
environment:
|
|
52
|
+
- MYSQL_DATABASE=${DB_DATABASE}
|
|
53
|
+
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
|
54
|
+
volumes:
|
|
55
|
+
- ./docker/mysql/my.cnf:/etc/mysql/my.cnf
|
|
56
|
+
- ./docker/mysql/data:/var/lib/mysql:delegated
|
|
57
|
+
networks:
|
|
58
|
+
- api-network
|
|
59
|
+
|
|
60
|
+
mailpit:
|
|
61
|
+
image: axllent/mailpit:latest
|
|
62
|
+
container_name: ${APP_NAME}-mailpit
|
|
63
|
+
restart: unless-stopped
|
|
64
|
+
ports:
|
|
65
|
+
- "1025:1025"
|
|
66
|
+
- "8025:8025"
|
|
67
|
+
networks:
|
|
68
|
+
- api-network
|
|
69
|
+
labels:
|
|
70
|
+
- "traefik.enable=true"
|
|
71
|
+
- "traefik.http.routers.mailpit.entrypoints=http,https"
|
|
72
|
+
- "traefik.http.routers.mailpit.rule=Host(`mail.${APP_NAME}.test`)"
|
|
73
|
+
- "traefik.http.routers.mailpit.tls=true"
|
|
74
|
+
- "traefik.http.services.mailpit.loadbalancer.server.port=8025"
|
|
75
|
+
|
|
76
|
+
networks:
|
|
77
|
+
api-network:
|
|
78
|
+
driver: bridge
|