@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.
Files changed (78) hide show
  1. package/.claude/settings.local.json +59 -0
  2. package/README.md +378 -0
  3. package/bin/groups/cache.js +52 -0
  4. package/bin/groups/database.js +105 -0
  5. package/bin/groups/forge.js +272 -0
  6. package/bin/groups/local.js +78 -0
  7. package/bin/groups/media.js +110 -0
  8. package/bin/tools.js +23 -0
  9. package/docs/Changelog.md +267 -0
  10. package/docs/TODO.md +167 -0
  11. package/docs/releases/release_0.0.1.md +116 -0
  12. package/docs/releases/release_0.0.2.md +88 -0
  13. package/docs/releases/release_0.0.3.md +58 -0
  14. package/docs/releases/release_0.0.4.md +128 -0
  15. package/docs/releases/release_0.0.5.md +77 -0
  16. package/docs/releases/release_0.0.6.md +80 -0
  17. package/docs/releases/release_1.0.0.md +61 -0
  18. package/docs/releases/release_1.0.1.md +18 -0
  19. package/docs/releases/release_1.0.2.md +18 -0
  20. package/docs/releases/release_1.0.3.md +19 -0
  21. package/docs/releases/release_1.1.0.md +18 -0
  22. package/docs/releases/release_1.1.1.md +17 -0
  23. package/docs/releases/release_1.1.2.md +18 -0
  24. package/docs/releases/release_1.1.3.md +21 -0
  25. package/docs/releases/release_1.1.4.md +18 -0
  26. package/docs/releases/release_1.1.5.md +18 -0
  27. package/docs/releases/release_1.1.6.md +21 -0
  28. package/docs/releases/release_1.1.7.md +17 -0
  29. package/docs/releases/release_2.0.0.md +192 -0
  30. package/docs/releases/release_2.0.1.md +53 -0
  31. package/docs/releases/release_2.0.2.md +55 -0
  32. package/docs/releases/release_2.0.3.md +69 -0
  33. package/docs/releases/release_2.1.0.md +59 -0
  34. package/docs/releases/release_2.2.0.md +83 -0
  35. package/docs/releases/release_2.2.1.md +36 -0
  36. package/docs/releases/release_2.2.2.md +57 -0
  37. package/docs/releases/release_2.2.3.md +39 -0
  38. package/docs/releases/release_2.2.4.md +75 -0
  39. package/docs/releases/release_2.2.5.md +69 -0
  40. package/docs/releases/release_3.0.0.md +87 -0
  41. package/docs/releases/release_3.0.1.md +65 -0
  42. package/docs/releases/release_3.1.0.md +90 -0
  43. package/docs/releases/release_3.2.0.md +74 -0
  44. package/docs/releases/release_3.3.0.md +72 -0
  45. package/package.json +35 -0
  46. package/src/aws/bucket.js +287 -0
  47. package/src/aws/cloudfront.js +433 -0
  48. package/src/aws/config.js +39 -0
  49. package/src/aws/iam.js +189 -0
  50. package/src/cache.js +49 -0
  51. package/src/database.js +315 -0
  52. package/src/forge/client.js +43 -0
  53. package/src/forge/config.js +33 -0
  54. package/src/forge/provisioning.js +191 -0
  55. package/src/forge/servers.js +27 -0
  56. package/src/forge/sites.js +93 -0
  57. package/src/google/groupMembers.js +35 -0
  58. package/src/google/utilities.js +39 -0
  59. package/src/local/doctor.js +214 -0
  60. package/src/local/setup.js +398 -0
  61. package/src/media.js +143 -0
  62. package/src/stub/docker/mysql/my.cnf +6 -0
  63. package/src/stub/docker/php/local.ini +4 -0
  64. package/src/stub/docker/traefik/dynamic_conf.yml +4 -0
  65. package/src/stub/docker/traefik/traefik.yml +24 -0
  66. package/src/stub/docker-compose/php8.0/docker-compose.yml +78 -0
  67. package/src/stub/docker-compose/php8.1/docker-compose.yml +78 -0
  68. package/src/stub/docker-compose/php8.2/docker-compose.yml +78 -0
  69. package/src/stub/docker-compose/php8.3/docker-compose.yml +78 -0
  70. package/src/stub/docker-compose/php8.4/docker-compose.yml +78 -0
  71. package/src/stub/docker-compose.yml +78 -0
  72. package/src/utilities/command.js +137 -0
  73. package/src/utilities/dateUtils.js +7 -0
  74. package/src/utilities/fileUtils.js +36 -0
  75. package/src/utilities/google-drive.js +69 -0
  76. package/src/utilities/pathUtils.js +15 -0
  77. package/src/utilities/userInput.js +28 -0
  78. 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,6 @@
1
+ [mysqld]
2
+ max_allowed_packet = 256M
3
+
4
+ # Uncomment to log all queries (impacts performance)
5
+ # general_log = 1
6
+ # general_log_file=/var/lib/mysql/general.log
@@ -0,0 +1,4 @@
1
+ upload_max_filesize = 40M
2
+ post_max_size = 40M
3
+ max_execution_time = 180
4
+ memory_limit = 3000M
@@ -0,0 +1,4 @@
1
+ tls:
2
+ certificates:
3
+ - certFile: "/etc/traefik/certs/site.test.pem"
4
+ keyFile: "/etc/traefik/certs/site.key"
@@ -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