@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,191 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { get, post } from './client.js';
|
|
4
|
+
|
|
5
|
+
const REGIONS = [
|
|
6
|
+
{ name: 'EU Frankfurt ā eu-central-1', value: 'eu-central-1' },
|
|
7
|
+
{ name: 'US West Oregon ā us-west-2', value: 'us-west-2' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const PHP_VERSION_MAP = {
|
|
11
|
+
'8.4': 'php84',
|
|
12
|
+
'8.3': 'php83',
|
|
13
|
+
'8.2': 'php82',
|
|
14
|
+
'8.1': 'php81',
|
|
15
|
+
'8.0': 'php80',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SERVER_POLL_INTERVAL_MS = 20000;
|
|
19
|
+
const SERVER_POLL_TIMEOUT_MS = 25 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
export const getAwsCredential = async () => {
|
|
22
|
+
const data = await get('/credentials');
|
|
23
|
+
const credentials = data.credentials || [];
|
|
24
|
+
const aws = credentials.find((c) => c.type === 'aws' || c.name?.toLowerCase().includes('aws'));
|
|
25
|
+
if (!aws) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Nessuna credenziale AWS trovata su Forge. ' +
|
|
28
|
+
'Aggiungi un provider AWS in Forge ā Profile ā Server Providers.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return aws;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const getSizesForRegion = async (credentialId, region) => {
|
|
35
|
+
try {
|
|
36
|
+
const data = await get(`/regions?provider=aws`);
|
|
37
|
+
const regions = data.regions?.aws || [];
|
|
38
|
+
const found = regions.find((r) => r.id === region);
|
|
39
|
+
if (found?.sizes?.length > 0) return found.sizes;
|
|
40
|
+
} catch {
|
|
41
|
+
// fall through to defaults
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
{ id: '253736', name: '1 GB RAM Ā· t3.micro Ā· 2 vCPU' },
|
|
46
|
+
{ id: '253836', name: '2 GB RAM Ā· t3.small Ā· 2 vCPU' },
|
|
47
|
+
{ id: '253700', name: '4 GB RAM Ā· t3.medium Ā· 2 vCPU' },
|
|
48
|
+
{ id: '253689', name: '8 GB RAM Ā· t2.large Ā· 2 vCPU' },
|
|
49
|
+
];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const readPhpVersionFromComposer = () => {
|
|
53
|
+
if (!existsSync('composer.json')) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'composer.json non trovato. Esegui il comando dalla root del progetto Laravel.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const composer = JSON.parse(readFileSync('composer.json', 'utf8'));
|
|
60
|
+
const phpConstraint = composer.require?.php;
|
|
61
|
+
|
|
62
|
+
if (!phpConstraint) {
|
|
63
|
+
throw new Error('Versione PHP non trovata in composer.json ā require.php.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const match = phpConstraint.match(/(\d+)\.(\d+)/);
|
|
67
|
+
if (!match) {
|
|
68
|
+
throw new Error(`Impossibile leggere la versione PHP dal constraint: ${phpConstraint}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const version = `${match[1]}.${match[2]}`;
|
|
72
|
+
const forgeVersion = PHP_VERSION_MAP[version];
|
|
73
|
+
|
|
74
|
+
if (!forgeVersion) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Versione PHP ${version} non supportata da Forge. ` +
|
|
77
|
+
`Versioni supportate: ${Object.keys(PHP_VERSION_MAP).join(', ')}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { version, forgeVersion };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const readAppName = () => {
|
|
85
|
+
try {
|
|
86
|
+
const raw = readFileSync('.env.example', 'utf8');
|
|
87
|
+
const match = raw.match(/^APP_NAME=(.+)$/m);
|
|
88
|
+
if (!match) throw new Error('APP_NAME non trovato');
|
|
89
|
+
return match[1].trim().replace(/['"]/g, '');
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error('Impossibile leggere APP_NAME da .env.example.');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const resolveServerName = (baseName, existingServers) => {
|
|
96
|
+
const names = existingServers.map((s) => s.name);
|
|
97
|
+
|
|
98
|
+
if (!names.includes(baseName)) return baseName;
|
|
99
|
+
|
|
100
|
+
let n = 2;
|
|
101
|
+
while (names.includes(`${baseName}-${n}`)) n++;
|
|
102
|
+
return `${baseName}-${n}`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const promptServerConfig = async (credentialId, existingServers = []) => {
|
|
106
|
+
const appName = readAppName();
|
|
107
|
+
const { version, forgeVersion } = readPhpVersionFromComposer();
|
|
108
|
+
|
|
109
|
+
console.log(`\nš¦ PHP rilevato da composer.json: ${version} ā Forge: ${forgeVersion}`);
|
|
110
|
+
console.log(`š Regione: EU Milan (eu-south-1)`);
|
|
111
|
+
|
|
112
|
+
const environment = await select({
|
|
113
|
+
message: 'Ambiente:',
|
|
114
|
+
choices: [
|
|
115
|
+
{ name: 'staging', value: 'staging' },
|
|
116
|
+
{ name: 'production', value: 'production' },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const baseName = `${appName}-${environment}`;
|
|
121
|
+
const serverName = resolveServerName(baseName, existingServers);
|
|
122
|
+
|
|
123
|
+
const confirmed = await confirm({
|
|
124
|
+
message: `Nome server: ${serverName} ā confermi?`,
|
|
125
|
+
default: true,
|
|
126
|
+
});
|
|
127
|
+
if (!confirmed) {
|
|
128
|
+
throw new Error('Operazione annullata.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('ā³ Recupero piani disponibili...');
|
|
132
|
+
const sizes = await getSizesForRegion(credentialId, 'eu-south-1');
|
|
133
|
+
|
|
134
|
+
const size = await select({
|
|
135
|
+
message: 'Piano EC2:',
|
|
136
|
+
choices: sizes.map((s) => ({ name: s.name || s.size, value: s.id })),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const installRedis = await confirm({
|
|
140
|
+
message: 'Installare Redis?',
|
|
141
|
+
default: false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
name: serverName,
|
|
146
|
+
region: 'eu-south-1',
|
|
147
|
+
size,
|
|
148
|
+
php_version: forgeVersion,
|
|
149
|
+
install_redis: installRedis,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const createServer = async (credentialId, config) => {
|
|
154
|
+
const payload = {
|
|
155
|
+
provider: 'aws',
|
|
156
|
+
credential_id: credentialId,
|
|
157
|
+
type: 'app',
|
|
158
|
+
database_type: 'mysql8',
|
|
159
|
+
ubuntu_version: '24.04',
|
|
160
|
+
...config,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const data = await post('/servers', payload);
|
|
164
|
+
return data.server;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const pollServerReady = async (serverId) => {
|
|
168
|
+
const start = Date.now();
|
|
169
|
+
|
|
170
|
+
process.stdout.write('ā³ Provisioning in corso');
|
|
171
|
+
|
|
172
|
+
while (Date.now() - start < SERVER_POLL_TIMEOUT_MS) {
|
|
173
|
+
await new Promise((r) => setTimeout(r, SERVER_POLL_INTERVAL_MS));
|
|
174
|
+
|
|
175
|
+
const data = await get(`/servers/${serverId}`);
|
|
176
|
+
const server = data.server;
|
|
177
|
+
|
|
178
|
+
process.stdout.write('.');
|
|
179
|
+
|
|
180
|
+
if (server.is_ready) {
|
|
181
|
+
process.stdout.write('\n');
|
|
182
|
+
return server;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
process.stdout.write('\n');
|
|
187
|
+
throw new Error(
|
|
188
|
+
'Timeout provisioning server (25 min). ' +
|
|
189
|
+
'Controlla lo stato su Forge e riprova se necessario.'
|
|
190
|
+
);
|
|
191
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { get, del } from './client.js';
|
|
3
|
+
|
|
4
|
+
export const listServers = async () => {
|
|
5
|
+
const data = await get('/servers');
|
|
6
|
+
return data.servers || [];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const deleteServer = async (serverId) => {
|
|
10
|
+
await del(`/servers/${serverId}`);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const pickServer = async () => {
|
|
14
|
+
const servers = await listServers();
|
|
15
|
+
|
|
16
|
+
if (servers.length === 0) {
|
|
17
|
+
throw new Error('Nessun server trovato sull\'account Forge.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return await select({
|
|
21
|
+
message: 'Seleziona il server:',
|
|
22
|
+
choices: servers.map((s) => ({
|
|
23
|
+
name: `${s.name} (${s.ip_address}) ā ${s.type}`,
|
|
24
|
+
value: s,
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { select, password, confirm } from '@inquirer/prompts';
|
|
2
|
+
import { get, post, put } from './client.js';
|
|
3
|
+
|
|
4
|
+
export const listSites = async (serverId) => {
|
|
5
|
+
const data = await get(`/servers/${serverId}/sites`);
|
|
6
|
+
return data.sites || [];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const pickSite = async (serverId) => {
|
|
10
|
+
const sites = await listSites(serverId);
|
|
11
|
+
|
|
12
|
+
if (sites.length === 0) {
|
|
13
|
+
throw new Error('Nessun sito trovato su questo server.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return await select({
|
|
17
|
+
message: 'Seleziona il sito:',
|
|
18
|
+
choices: sites.map((s) => ({
|
|
19
|
+
name: `${s.name} ā ${s.status}`,
|
|
20
|
+
value: s,
|
|
21
|
+
})),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const deploySite = async (serverId, siteId) => {
|
|
26
|
+
await post(`/servers/${serverId}/sites/${siteId}/deployment/deploy`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getDeploymentLog = async (serverId, siteId) => {
|
|
30
|
+
const data = await get(`/servers/${serverId}/sites/${siteId}/deployment/log`);
|
|
31
|
+
return data.content || data || '';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const getEnv = async (serverId, siteId) => {
|
|
35
|
+
const data = await get(`/servers/${serverId}/sites/${siteId}/env`);
|
|
36
|
+
return typeof data === 'string' ? data : (data.content ?? '');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const setEnv = async (serverId, siteId, content) => {
|
|
40
|
+
await put(`/servers/${serverId}/sites/${siteId}/env`, { content });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const confirmDestructiveEnvWrite = async () => {
|
|
44
|
+
const confirmed = await confirm({
|
|
45
|
+
message: '\x1b[31mDISTRUTTIVO: Se confermi l\'env remote verrĆ sovrascritto! Sicuro di voler continuare?\x1b[0m',
|
|
46
|
+
default: false,
|
|
47
|
+
});
|
|
48
|
+
return confirmed;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const SITE_POLL_INTERVAL_MS = 5000;
|
|
52
|
+
const SITE_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
export const createSite = async (serverId, payload) => {
|
|
55
|
+
const data = await post(`/servers/${serverId}/sites`, payload);
|
|
56
|
+
return data.site;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const pollSiteInstalled = async (serverId, siteId) => {
|
|
60
|
+
const start = Date.now();
|
|
61
|
+
|
|
62
|
+
process.stdout.write('ā³ Attendo installazione sito');
|
|
63
|
+
|
|
64
|
+
while (Date.now() - start < SITE_POLL_TIMEOUT_MS) {
|
|
65
|
+
await new Promise((r) => setTimeout(r, SITE_POLL_INTERVAL_MS));
|
|
66
|
+
|
|
67
|
+
const data = await get(`/servers/${serverId}/sites/${siteId}`);
|
|
68
|
+
const site = data.site;
|
|
69
|
+
|
|
70
|
+
process.stdout.write('.');
|
|
71
|
+
|
|
72
|
+
if (site.status === 'installed') {
|
|
73
|
+
process.stdout.write('\n');
|
|
74
|
+
return site;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
process.stdout.write('\n');
|
|
79
|
+
throw new Error('Timeout installazione sito (5 min). Controlla lo stato su Forge.');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const installGitRepo = async (serverId, siteId, payload) => {
|
|
83
|
+
await post(`/servers/${serverId}/sites/${siteId}/git`, payload);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const getDeployScript = async (serverId, siteId) => {
|
|
87
|
+
const data = await get(`/servers/${serverId}/sites/${siteId}/deployment/script`);
|
|
88
|
+
return data.content || '';
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const updateDeployScript = async (serverId, siteId, content) => {
|
|
92
|
+
await put(`/servers/${serverId}/sites/${siteId}/deployment/script`, { content });
|
|
93
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { domain, getLocalKeys } from './utilities.js';
|
|
2
|
+
|
|
3
|
+
export const getMembersOfGroupEmail = async (groupKey, groupSecret, email) => {
|
|
4
|
+
let resolvedGroupKey = groupKey;
|
|
5
|
+
let resolvedGroupSecret = groupSecret;
|
|
6
|
+
|
|
7
|
+
if (!resolvedGroupKey || !resolvedGroupSecret) {
|
|
8
|
+
const secretFetcherOptions = await getLocalKeys();
|
|
9
|
+
resolvedGroupKey = secretFetcherOptions.groupKey;
|
|
10
|
+
resolvedGroupSecret = secretFetcherOptions.groupSecret;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!resolvedGroupKey || !resolvedGroupSecret) {
|
|
14
|
+
throw new Error('Missing groupKey/groupSecret in .secret-fetcher');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const resolvedEmail = email || `${resolvedGroupKey}@jumpgroup.it`;
|
|
18
|
+
const url = `${domain}groups/${resolvedEmail}/members?word=${resolvedGroupKey}&encrypted_word=${resolvedGroupSecret}`;
|
|
19
|
+
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
method: 'GET',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch group members (${response.status} ${response.statusText})`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const payload = await response.json();
|
|
30
|
+
if (!payload?.members || !Array.isArray(payload.members)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return payload.members;
|
|
35
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { getMembersOfGroupEmail } from './groupMembers.js';
|
|
4
|
+
|
|
5
|
+
export const domain = 'https://gmailgroupalias.jumpgroup.it/api/';
|
|
6
|
+
|
|
7
|
+
export const getLocalKeys = async () => {
|
|
8
|
+
if (!existsSync('.secret-fetcher')) {
|
|
9
|
+
throw new Error('No .secret-fetcher file found');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const parsed = config({ path: '.secret-fetcher' }).parsed;
|
|
13
|
+
if (!parsed) {
|
|
14
|
+
throw new Error('Could not read .secret-fetcher');
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getPeopleName = async () => {
|
|
20
|
+
const secretFetcherOptions = await getLocalKeys();
|
|
21
|
+
const groupKey = secretFetcherOptions.groupKey;
|
|
22
|
+
const groupSecret = secretFetcherOptions.groupSecret;
|
|
23
|
+
|
|
24
|
+
const techTeamMembers = await getMembersOfGroupEmail(
|
|
25
|
+
groupKey,
|
|
26
|
+
groupSecret,
|
|
27
|
+
'tech@jumpgroup.it'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const uniqueNames = [...new Set(
|
|
31
|
+
techTeamMembers
|
|
32
|
+
.map((member) => member?.email)
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map((email) => email.split('@')[0].split('.')[0])
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
)].sort();
|
|
37
|
+
|
|
38
|
+
return uniqueNames.map((name) => ({ name, value: name }));
|
|
39
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { parse } from 'dotenv';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
const ICONS = {
|
|
6
|
+
ok: 'ā
',
|
|
7
|
+
fail: 'ā',
|
|
8
|
+
warn: 'ā ļø ',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const runCommand = (command, args = []) =>
|
|
12
|
+
spawnSync(command, args, {
|
|
13
|
+
stdio: 'pipe',
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const checkCommandAvailable = (command, args = ['--version']) => {
|
|
18
|
+
const result = runCommand(command, args);
|
|
19
|
+
return !result.error && result.status === 0;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const checkDockerContainerState = (containerName) => {
|
|
23
|
+
const result = runCommand('docker', ['inspect', '-f', '{{.State.Running}}', containerName]);
|
|
24
|
+
if (result.status !== 0) {
|
|
25
|
+
return {
|
|
26
|
+
exists: false,
|
|
27
|
+
running: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
exists: true,
|
|
33
|
+
running: result.stdout.trim() === 'true',
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const readEnvFile = (path) => {
|
|
38
|
+
if (!existsSync(path)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return parse(readFileSync(path, 'utf8'));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const createCollector = () => {
|
|
50
|
+
const checks = [];
|
|
51
|
+
return {
|
|
52
|
+
add: (status, label, details = '') => {
|
|
53
|
+
checks.push({ status, label, details });
|
|
54
|
+
},
|
|
55
|
+
getChecks: () => checks,
|
|
56
|
+
counts: () => {
|
|
57
|
+
const failures = checks.filter((c) => c.status === 'fail').length;
|
|
58
|
+
const warnings = checks.filter((c) => c.status === 'warn').length;
|
|
59
|
+
const ok = checks.filter((c) => c.status === 'ok').length;
|
|
60
|
+
return { ok, failures, warnings };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const printResults = (checks) => {
|
|
66
|
+
for (const check of checks) {
|
|
67
|
+
const icon = ICONS[check.status];
|
|
68
|
+
if (check.details) {
|
|
69
|
+
console.log(`${icon} ${check.label} ā ${check.details}`);
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`${icon} ${check.label}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const runLocalDoctor = async () => {
|
|
77
|
+
const collector = createCollector();
|
|
78
|
+
|
|
79
|
+
const envExampleExists = existsSync('.env.example');
|
|
80
|
+
const secretFetcherExists = existsSync('.secret-fetcher');
|
|
81
|
+
const envExists = existsSync('.env');
|
|
82
|
+
const dockerComposeExists = existsSync('docker-compose.yml');
|
|
83
|
+
const certExists = existsSync('docker/certs/site.test.pem');
|
|
84
|
+
const certKeyExists = existsSync('docker/certs/site.key');
|
|
85
|
+
const laravelToolsConfigExists = existsSync('laravel-tools.yml');
|
|
86
|
+
|
|
87
|
+
collector.add(
|
|
88
|
+
envExampleExists ? 'ok' : 'fail',
|
|
89
|
+
'.env.example',
|
|
90
|
+
envExampleExists ? 'presente' : 'mancante'
|
|
91
|
+
);
|
|
92
|
+
collector.add(
|
|
93
|
+
secretFetcherExists ? 'ok' : 'fail',
|
|
94
|
+
'.secret-fetcher',
|
|
95
|
+
secretFetcherExists ? 'presente' : 'mancante'
|
|
96
|
+
);
|
|
97
|
+
collector.add(envExists ? 'ok' : 'warn', '.env', envExists ? 'presente' : 'mancante');
|
|
98
|
+
collector.add(
|
|
99
|
+
dockerComposeExists ? 'ok' : 'warn',
|
|
100
|
+
'docker-compose.yml',
|
|
101
|
+
dockerComposeExists ? 'presente' : 'mancante'
|
|
102
|
+
);
|
|
103
|
+
collector.add(
|
|
104
|
+
certExists && certKeyExists ? 'ok' : 'warn',
|
|
105
|
+
'Certificati locali (docker/certs/site.test.pem + site.key)',
|
|
106
|
+
certExists && certKeyExists ? 'presenti' : 'mancanti'
|
|
107
|
+
);
|
|
108
|
+
collector.add(
|
|
109
|
+
laravelToolsConfigExists ? 'ok' : 'warn',
|
|
110
|
+
'laravel-tools.yml',
|
|
111
|
+
laravelToolsConfigExists
|
|
112
|
+
? 'presente (comandi remoti disponibili)'
|
|
113
|
+
: 'mancante (comandi remoti DB/cache non disponibili)'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const envExample = readEnvFile('.env.example');
|
|
117
|
+
if (!envExample) {
|
|
118
|
+
collector.add('fail', 'Parsing .env.example', 'impossibile leggere il file');
|
|
119
|
+
} else {
|
|
120
|
+
const required = ['APP_NAME', 'APP_URL', 'ASSETS_URL', 'DB_USERNAME', 'DB_PASSWORD'];
|
|
121
|
+
for (const key of required) {
|
|
122
|
+
collector.add(
|
|
123
|
+
envExample[key] ? 'ok' : 'fail',
|
|
124
|
+
`.env.example:${key}`,
|
|
125
|
+
envExample[key] ? 'ok' : 'mancante o vuoto'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hasDbName = Boolean(envExample.DB_DATABASE || envExample.DB_NAME);
|
|
130
|
+
collector.add(
|
|
131
|
+
hasDbName ? 'ok' : 'fail',
|
|
132
|
+
'.env.example:DB_DATABASE/DB_NAME',
|
|
133
|
+
hasDbName ? 'ok' : 'entrambi mancanti o vuoti'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const env = readEnvFile('.env');
|
|
138
|
+
if (envExists && !env) {
|
|
139
|
+
collector.add('fail', 'Parsing .env', 'impossibile leggere il file');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const dockerAvailable = checkCommandAvailable('docker', ['compose', 'version']);
|
|
143
|
+
const mkcertAvailable = checkCommandAvailable('mkcert', ['-help']);
|
|
144
|
+
const composerAvailable = checkCommandAvailable('composer', ['--version']);
|
|
145
|
+
const sudoAvailable = checkCommandAvailable('sudo', ['--version']);
|
|
146
|
+
|
|
147
|
+
collector.add(
|
|
148
|
+
dockerAvailable ? 'ok' : 'fail',
|
|
149
|
+
'docker compose',
|
|
150
|
+
dockerAvailable ? 'disponibile' : 'non disponibile'
|
|
151
|
+
);
|
|
152
|
+
collector.add(
|
|
153
|
+
mkcertAvailable ? 'ok' : 'warn',
|
|
154
|
+
'mkcert',
|
|
155
|
+
mkcertAvailable ? 'disponibile' : 'non disponibile'
|
|
156
|
+
);
|
|
157
|
+
collector.add(
|
|
158
|
+
composerAvailable ? 'ok' : 'warn',
|
|
159
|
+
'composer',
|
|
160
|
+
composerAvailable ? 'disponibile' : 'non disponibile'
|
|
161
|
+
);
|
|
162
|
+
collector.add(
|
|
163
|
+
sudoAvailable ? 'ok' : 'warn',
|
|
164
|
+
'sudo',
|
|
165
|
+
sudoAvailable ? 'disponibile' : 'non disponibile'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (dockerAvailable) {
|
|
169
|
+
const dockerDaemon = runCommand('docker', ['info']);
|
|
170
|
+
const daemonOk = dockerDaemon.status === 0;
|
|
171
|
+
collector.add(
|
|
172
|
+
daemonOk ? 'ok' : 'fail',
|
|
173
|
+
'Docker daemon',
|
|
174
|
+
daemonOk ? 'in esecuzione' : 'non raggiungibile'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const appName = env?.APP_NAME || envExample?.APP_NAME;
|
|
178
|
+
if (!appName) {
|
|
179
|
+
collector.add('warn', 'Container checks', 'APP_NAME non disponibile');
|
|
180
|
+
} else if (daemonOk) {
|
|
181
|
+
const apiState = checkDockerContainerState(`${appName}-api`);
|
|
182
|
+
const mysqlState = checkDockerContainerState(`${appName}-mysql`);
|
|
183
|
+
|
|
184
|
+
collector.add(
|
|
185
|
+
apiState.exists ? (apiState.running ? 'ok' : 'warn') : 'warn',
|
|
186
|
+
`${appName}-api`,
|
|
187
|
+
apiState.exists
|
|
188
|
+
? apiState.running
|
|
189
|
+
? 'container in esecuzione'
|
|
190
|
+
: 'container presente ma fermo'
|
|
191
|
+
: 'container non trovato'
|
|
192
|
+
);
|
|
193
|
+
collector.add(
|
|
194
|
+
mysqlState.exists ? (mysqlState.running ? 'ok' : 'warn') : 'warn',
|
|
195
|
+
`${appName}-mysql`,
|
|
196
|
+
mysqlState.exists
|
|
197
|
+
? mysqlState.running
|
|
198
|
+
? 'container in esecuzione'
|
|
199
|
+
: 'container presente ma fermo'
|
|
200
|
+
: 'container non trovato'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('\n𩺠Local doctor report\n');
|
|
206
|
+
printResults(collector.getChecks());
|
|
207
|
+
|
|
208
|
+
const { ok, failures, warnings } = collector.counts();
|
|
209
|
+
console.log(`\nš Risultato: ${ok} ok, ${warnings} warning, ${failures} errori`);
|
|
210
|
+
|
|
211
|
+
if (failures > 0) {
|
|
212
|
+
throw new Error('Local doctor ha trovato errori bloccanti.');
|
|
213
|
+
}
|
|
214
|
+
};
|