@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,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.3
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
@@ -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.1.0
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
@@ -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.4
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
@@ -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.5
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
@@ -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.5
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
@@ -0,0 +1,137 @@
1
+ import { spawn, spawnSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { parse } from 'dotenv';
4
+ import yaml from 'js-yaml';
5
+
6
+ // Runs a shell command, streaming stdout/stderr to the terminal.
7
+ // Returns a Promise that resolves on exit code 0, rejects otherwise.
8
+ export const executeCommand = (command, args = [], options = {}) => {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(command, args, { stdio: 'inherit', ...options });
11
+
12
+ child.on('close', (code) => {
13
+ if (code === 0) resolve();
14
+ else reject(new Error(`Command '${command}' exited with code ${code}`));
15
+ });
16
+
17
+ child.on('error', (err) => {
18
+ reject(new Error(`Failed to start command '${command}': ${err.message}`));
19
+ });
20
+ });
21
+ };
22
+
23
+ export const executeCommandWithOutput = (command, args = [], options = {}) => {
24
+ return new Promise((resolve, reject) => {
25
+ const child = spawn(command, args, { ...options, stdio: ['inherit', 'pipe', 'pipe'] });
26
+
27
+ let stdout = '';
28
+ let stderr = '';
29
+
30
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
31
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
32
+
33
+ child.on('close', (code) => {
34
+ if (code === 0) resolve(stdout);
35
+ else reject(new Error(`Command '${command}' exited with code ${code}:\n${stderr || stdout}`));
36
+ });
37
+
38
+ child.on('error', (err) => {
39
+ reject(new Error(`Failed to start command '${command}': ${err.message}`));
40
+ });
41
+ });
42
+ };
43
+
44
+ const canRunCommand = (command, args = []) => {
45
+ const result = spawnSync(command, args, { stdio: 'ignore' });
46
+ return !result.error;
47
+ };
48
+
49
+ export const ensureCommandsAvailable = (checks) => {
50
+ const missing = checks.filter(({ command, args = [] }) => !canRunCommand(command, args));
51
+
52
+ if (missing.length === 0) {
53
+ return;
54
+ }
55
+
56
+ const formatted = missing
57
+ .map(({ label, command, installHint }) => {
58
+ const base = label || command;
59
+ return installHint ? `- ${base}: ${installHint}` : `- ${base}`;
60
+ })
61
+ .join('\n');
62
+
63
+ throw new Error(`Missing required local dependencies:\n${formatted}`);
64
+ };
65
+
66
+ const readLaravelToolsConfig = () => {
67
+ try {
68
+ return yaml.load(readFileSync('laravel-tools.yml', 'utf8')) || {};
69
+ } catch {
70
+ throw new Error(
71
+ 'laravel-tools.yml not found. Create it in the project root with server configuration.\n' +
72
+ 'Example:\n' +
73
+ ' staging:\n' +
74
+ ' host: 1.2.3.4\n' +
75
+ ' user: deployer\n' +
76
+ ' production:\n' +
77
+ ' host: 5.6.7.8\n' +
78
+ ' user: deployer\n'
79
+ );
80
+ }
81
+ };
82
+
83
+ // Reads the server IP for a given environment from laravel-tools.yml.
84
+ // In trellis-tools this came from trellis/hosts/{env}.yml::ansible_host.
85
+ // In laravel-tools it comes from laravel-tools.yml::{env}.host (set manually
86
+ // for now; will be written automatically once the deploy tool is decided).
87
+ export const getIp = (environment) => {
88
+ const config = readLaravelToolsConfig();
89
+ const host = config[environment]?.host;
90
+ if (!host) {
91
+ throw new Error(
92
+ `No host configured for '${environment}' in laravel-tools.yml`
93
+ );
94
+ }
95
+ return host;
96
+ };
97
+
98
+ // Reads the SSH user for a given environment from laravel-tools.yml.
99
+ // trellis-tools hardcoded 'web' (a Trellis-provisioned user).
100
+ // For Laravel this depends on how the server is provisioned (e.g. 'deployer').
101
+ export const getSshUser = (environment) => {
102
+ const config = readLaravelToolsConfig();
103
+ const user = config[environment]?.user;
104
+ if (!user) {
105
+ throw new Error(
106
+ `No user configured for '${environment}' in laravel-tools.yml`
107
+ );
108
+ }
109
+ return user;
110
+ };
111
+
112
+ export const getServerPath = (environment) => {
113
+ const config = readLaravelToolsConfig();
114
+ const path = config[environment]?.path;
115
+ if (!path) {
116
+ throw new Error(
117
+ `No path configured for '${environment}' in laravel-tools.yml.\n` +
118
+ `Add it under the environment key, e.g.:\n` +
119
+ ` ${environment}:\n` +
120
+ ` path: /home/forge/your-site-domain.com`
121
+ );
122
+ }
123
+ return path;
124
+ };
125
+
126
+ // Reads APP_NAME from .env.example.
127
+ // In trellis-tools this also fell back to wordpress_sites.yml — not needed for Laravel.
128
+ export const getAppName = () => {
129
+ try {
130
+ const raw = readFileSync('.env.example');
131
+ const env = parse(raw);
132
+ if (!env.APP_NAME) throw new Error('APP_NAME not set');
133
+ return env.APP_NAME;
134
+ } catch {
135
+ throw new Error('Could not read APP_NAME from .env.example');
136
+ }
137
+ };
@@ -0,0 +1,7 @@
1
+ export const getCurrentDateString = () => {
2
+ const now = new Date();
3
+ const year = now.getFullYear();
4
+ const month = String(now.getMonth() + 1).padStart(2, '0');
5
+ const day = String(now.getDate()).padStart(2, '0');
6
+ return `${year}${month}${day}`;
7
+ };
@@ -0,0 +1,36 @@
1
+ import { readdirSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export const find10LatestFiles = (directory, envFilter = null, usernameFilter = null) => {
5
+ try {
6
+ const files = readdirSync(directory)
7
+ .filter((f) => f.endsWith('.sql'))
8
+ .filter((f) => !envFilter || f.includes(`_${envFilter}_`) || f.includes(`_${envFilter}.`))
9
+ .filter((f) => !usernameFilter || f.includes(`_${usernameFilter}.`))
10
+ .map((f) => {
11
+ const fullPath = join(directory, f);
12
+ const stats = statSync(fullPath);
13
+ const sizeMb = (stats.size / 1024 / 1024).toFixed(2);
14
+ const date = stats.mtime.toLocaleDateString('it-IT');
15
+ return {
16
+ name: f,
17
+ path: fullPath,
18
+ mtime: stats.mtime,
19
+ size: sizeMb,
20
+ display: `${f} (${sizeMb} MB — ${date})`,
21
+ };
22
+ })
23
+ .sort((a, b) => b.mtime - a.mtime)
24
+ .slice(0, 10);
25
+
26
+ return files;
27
+ } catch {
28
+ return [];
29
+ }
30
+ };
31
+
32
+ export const filterDatabaseFiles = (files, env, username = null) => {
33
+ return files
34
+ .filter((f) => f.name.includes(`_${env}_`) || f.name.includes(`_${env}.`))
35
+ .filter((f) => !username || f.name.includes(`_${username}.`));
36
+ };
@@ -0,0 +1,69 @@
1
+ import { existsSync, readdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { input } from '@inquirer/prompts';
4
+ import keytar from 'keytar';
5
+
6
+ const SERVICE_NAME = 'laravel-tools';
7
+ const ACCOUNT_NAME = 'google-drive-directory';
8
+
9
+ // Attempts to locate the Google Drive shared drives directory automatically.
10
+ // Google Drive for Desktop (macOS) mounts under ~/Library/CloudStorage/GoogleDrive-{email}/.
11
+ // Checks for both Italian ('Drive condivisi') and English ('Shared drives') folder names.
12
+ const findGoogleDriveDirectory = () => {
13
+ const home = homedir();
14
+
15
+ // Google Drive for Desktop (current)
16
+ const cloudStorage = `${home}/Library/CloudStorage`;
17
+ if (existsSync(cloudStorage)) {
18
+ try {
19
+ const entries = readdirSync(cloudStorage);
20
+ const gdEntry = entries.find((e) => e.startsWith('GoogleDrive-'));
21
+ if (gdEntry) {
22
+ const base = `${cloudStorage}/${gdEntry}`;
23
+ for (const name of ['Drive condivisi', 'Drive Condivisi', 'Shared drives']) {
24
+ const candidate = `${base}/${name}`;
25
+ if (existsSync(candidate)) return candidate;
26
+ }
27
+ }
28
+ } catch {
29
+ // If we can't read the directory, fall through to legacy paths
30
+ }
31
+ }
32
+
33
+ // Legacy Google Drive paths
34
+ for (const candidate of [
35
+ `${home}/Google Drive/Drive condivisi`,
36
+ `${home}/Google Drive/Shared drives`,
37
+ `${home}/Drive Condivisi`,
38
+ ]) {
39
+ if (existsSync(candidate)) return candidate;
40
+ }
41
+
42
+ return null;
43
+ };
44
+
45
+ // Returns the Google Drive shared drives base path.
46
+ // Checks OS keyring first, then tries to find automatically, then asks the user.
47
+ // Path is saved to the OS keyring so subsequent calls don't prompt again.
48
+ export const getGoogleDriveDirectory = async () => {
49
+ const stored = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
50
+ if (stored && existsSync(stored)) return stored;
51
+
52
+ const found = findGoogleDriveDirectory();
53
+ if (found) {
54
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, found);
55
+ return found;
56
+ }
57
+
58
+ console.log('⚠️ Impossibile trovare la cartella di Google Drive automaticamente.');
59
+ const path = await input({
60
+ message: 'Inserisci il percorso della cartella Drive Condivisi: ',
61
+ });
62
+
63
+ if (!existsSync(path)) {
64
+ throw new Error(`❌ Directory non trovata: ${path}`);
65
+ }
66
+
67
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, path);
68
+ return path;
69
+ };
@@ -0,0 +1,15 @@
1
+ import { mkdirSync, existsSync } from 'fs';
2
+ import { getGoogleDriveDirectory } from './google-drive.js';
3
+
4
+
5
+ export const ensureDirectoryExists = (dirPath) => {
6
+ if (!existsSync(dirPath)) {
7
+ mkdirSync(dirPath, { recursive: true });
8
+ }
9
+ };
10
+
11
+ export const resolveDatabasePath = async (appname, customPath = null) => {
12
+ if (customPath) return customPath;
13
+ const baseDir = await getGoogleDriveDirectory();
14
+ return `${baseDir}/Tech/DumpsDB/${appname}`;
15
+ };
@@ -0,0 +1,28 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+
3
+ export const askYesNo = async (message) => {
4
+ return await confirm({ message });
5
+ };
6
+
7
+ // Standard database filename conventions — mirrors trellis-tools exactly.
8
+ // AMS mode: {app}_ams{YEAR}_{env}.sql
9
+ // Standard: {app}_{env}_{YYYYMMDD}[_{username}].sql
10
+ // Custom: {customName}.sql
11
+ export const generateDatabaseFilename = (
12
+ appname,
13
+ env,
14
+ date,
15
+ customName = null,
16
+ amsMode = false,
17
+ username = null
18
+ ) => {
19
+ if (amsMode) {
20
+ const year = new Date().getFullYear();
21
+ return `${appname}_ams${year}_${env}.sql`;
22
+ }
23
+ if (customName) {
24
+ return customName.endsWith('.sql') ? customName : `${customName}.sql`;
25
+ }
26
+ const base = `${appname}_${env}_${date}`;
27
+ return username ? `${base}_${username}.sql` : `${base}.sql`;
28
+ };