@francescomalatesta/laravel-forge-mcp 0.6.0 → 0.8.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/README.md +19 -0
- package/dist/tools/databases/create-database-user.js +65 -0
- package/dist/tools/databases/create-database.js +63 -0
- package/dist/tools/databases/delete-database-user.js +40 -0
- package/dist/tools/databases/delete-database.js +40 -0
- package/dist/tools/databases/list-database-users.js +49 -0
- package/dist/tools/databases/list-databases.js +49 -0
- package/dist/tools/databases/shared.js +61 -0
- package/dist/tools/databases/sync-databases.js +28 -0
- package/dist/tools/databases/update-database-root-password.js +32 -0
- package/dist/tools/databases/update-database-user.js +39 -0
- package/dist/tools/php/config-files.js +14 -0
- package/dist/tools/php/get-php-config.js +38 -0
- package/dist/tools/php/get-php-settings.js +54 -0
- package/dist/tools/php/install-php-version.js +56 -0
- package/dist/tools/php/list-php-versions.js +47 -0
- package/dist/tools/php/set-default-php-version.js +54 -0
- package/dist/tools/php/set-php-opcache.js +52 -0
- package/dist/tools/php/settings.js +7 -0
- package/dist/tools/php/shared.js +72 -0
- package/dist/tools/php/uninstall-php-version.js +48 -0
- package/dist/tools/php/update-php-config.js +62 -0
- package/dist/tools/php/update-php-limits.js +64 -0
- package/dist/tools/php/upgrade-php-version.js +31 -0
- package/dist/tools/registry.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,25 @@ npm run build
|
|
|
117
117
|
| `forge_archive_server` | servers | Archive a server (Forge stops managing it) |
|
|
118
118
|
| `forge_unarchive_server` | servers | Restore an archived server |
|
|
119
119
|
| `forge_delete_server` | servers | Delete a server (optionally keeping it at the provider) |
|
|
120
|
+
| `forge_list_php_versions` | servers | Installed PHP versions |
|
|
121
|
+
| `forge_get_php_settings` | servers | Default CLI and site versions, upload and execution limits, OPcache |
|
|
122
|
+
| `forge_install_php_version` | servers | Install a PHP version (optionally as default) and wait until installed |
|
|
123
|
+
| `forge_upgrade_php_version` | servers | Update a PHP version to its latest patch release |
|
|
124
|
+
| `forge_uninstall_php_version` | servers | Uninstall a PHP version |
|
|
125
|
+
| `forge_set_default_php_version` | servers | Default PHP version for the CLI or for new sites |
|
|
126
|
+
| `forge_update_php_limits` | servers | Max upload size and max execution time |
|
|
127
|
+
| `forge_set_php_opcache` | servers | Enable or disable OPcache |
|
|
128
|
+
| `forge_get_php_config` | servers | FPM or CLI php.ini, or the FPM pool configuration |
|
|
129
|
+
| `forge_update_php_config` | servers | Replace a PHP configuration file |
|
|
130
|
+
| `forge_list_databases` | databases | Database schemas on a server |
|
|
131
|
+
| `forge_create_database` | databases | Create a database (optionally with a user) and wait until installed |
|
|
132
|
+
| `forge_delete_database` | databases | Drop a database |
|
|
133
|
+
| `forge_sync_databases` | databases | Import databases created outside Forge |
|
|
134
|
+
| `forge_list_database_users` | databases | Database users on a server |
|
|
135
|
+
| `forge_create_database_user` | databases | Create a user with access to databases (by ID or name) |
|
|
136
|
+
| `forge_update_database_user` | databases | Change a user's password or databases |
|
|
137
|
+
| `forge_delete_database_user` | databases | Delete a database user |
|
|
138
|
+
| `forge_update_database_root_password` | databases | Change the database root password |
|
|
120
139
|
| `forge_list_deployments` | deployments | Deployments of a site or of every site on a server |
|
|
121
140
|
| `forge_get_deployment` | deployments | A deployment with the end of its log |
|
|
122
141
|
| `forge_get_deployment_status` | deployments | Whether a deployment is running |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { operationOutput, outcome, waitFor, waitInput } from '../shared/async.js';
|
|
5
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
6
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
7
|
+
import { databaseOutput, databasePhase, databaseRefsInput, databaseUsersPath, formatDatabase, passwordInput, readOne, resolveDatabaseIds, } from './shared.js';
|
|
8
|
+
const CHECK_WITH = 'forge_list_database_users';
|
|
9
|
+
export const createDatabaseUser = defineTool({
|
|
10
|
+
name: 'forge_create_database_user',
|
|
11
|
+
title: 'Create database user',
|
|
12
|
+
description: 'Create a database user, optionally with access to some databases (by ID or name) and read-only. Waits until it is installed unless `wait` is false.',
|
|
13
|
+
toolset: 'databases',
|
|
14
|
+
operations: [
|
|
15
|
+
'organizations.servers.database.users.store',
|
|
16
|
+
'organizations.servers.database.users.show',
|
|
17
|
+
'organizations.servers.database.schemas.index',
|
|
18
|
+
],
|
|
19
|
+
permissions: ['server:create-databases', 'server:view'],
|
|
20
|
+
readOnly: false,
|
|
21
|
+
destructive: false,
|
|
22
|
+
idempotent: false,
|
|
23
|
+
async: true,
|
|
24
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
25
|
+
inputSchema: {
|
|
26
|
+
organization: organizationInput,
|
|
27
|
+
server: serverInput,
|
|
28
|
+
name: z.string().min(1).describe('User name.'),
|
|
29
|
+
password: passwordInput,
|
|
30
|
+
databases: databaseRefsInput.optional(),
|
|
31
|
+
read_only: z.boolean().optional().describe('Grant read-only access.'),
|
|
32
|
+
...waitInput(120),
|
|
33
|
+
},
|
|
34
|
+
outputSchema: {
|
|
35
|
+
...operationOutput,
|
|
36
|
+
user: databaseOutput.nullable(),
|
|
37
|
+
},
|
|
38
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
39
|
+
const org = organization(args.organization);
|
|
40
|
+
const databaseIds = args.databases ? await resolveDatabaseIds(client, org, args.server, args.databases, signal) : undefined;
|
|
41
|
+
const base = databaseUsersPath(org, args.server);
|
|
42
|
+
const response = await client.post(base, {
|
|
43
|
+
body: { name: args.name, password: args.password, database_ids: databaseIds, read_only: args.read_only },
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
const initial = response.data?.data ? flattenSingle(response.data) : undefined;
|
|
47
|
+
const action = `create the database user ${args.name}`;
|
|
48
|
+
if (!initial || !args.wait) {
|
|
49
|
+
return {
|
|
50
|
+
structured: { status: 'queued', check_with: CHECK_WITH, user: initial ? formatDatabase(initial) : null },
|
|
51
|
+
summary: `Forge accepted the request to ${action}. Check it with ${CHECK_WITH}.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const result = await waitFor({
|
|
55
|
+
initial,
|
|
56
|
+
poll: () => readOne(client, `${base}/${encodeURIComponent(initial.id)}`, signal),
|
|
57
|
+
phase: (user) => databasePhase(user.status),
|
|
58
|
+
describe: (user) => `Database user ${user.name} is ${user.status}`,
|
|
59
|
+
timeoutSeconds: args.timeout_seconds,
|
|
60
|
+
context: { sleep, progress },
|
|
61
|
+
});
|
|
62
|
+
const done = outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
63
|
+
return { structured: { ...done.structured, user: formatDatabase(result.value ?? initial) }, summary: done.summary };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { ToolInputError } from '../errors.js';
|
|
5
|
+
import { operationOutput, outcome, waitFor, waitInput } from '../shared/async.js';
|
|
6
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
7
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
8
|
+
import { databaseOutput, databasePhase, databasesPath, formatDatabase, passwordInput, readOne } from './shared.js';
|
|
9
|
+
const CHECK_WITH = 'forge_list_databases';
|
|
10
|
+
export const createDatabase = defineTool({
|
|
11
|
+
name: 'forge_create_database',
|
|
12
|
+
title: 'Create database',
|
|
13
|
+
description: 'Create a database schema on a server, optionally with a new user that has access to it (`user` + `password`). Waits until it is installed unless `wait` is false. Connect it to a site by setting DB_* variables with forge_set_site_env_vars.',
|
|
14
|
+
toolset: 'databases',
|
|
15
|
+
operations: ['organizations.servers.database.schemas.store', 'organizations.servers.database.schemas.show'],
|
|
16
|
+
permissions: ['server:create-databases', 'server:view'],
|
|
17
|
+
readOnly: false,
|
|
18
|
+
destructive: false,
|
|
19
|
+
idempotent: false,
|
|
20
|
+
async: true,
|
|
21
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
22
|
+
inputSchema: {
|
|
23
|
+
organization: organizationInput,
|
|
24
|
+
server: serverInput,
|
|
25
|
+
name: z.string().min(1).describe('Database name.'),
|
|
26
|
+
user: z.string().min(1).optional().describe('Also create a user with access to this database.'),
|
|
27
|
+
password: passwordInput.optional().describe('Password of the new user (required with `user`, never returned).'),
|
|
28
|
+
backup_configuration_ids: z.array(z.number().int()).optional().describe('Backup configurations to add the database to.'),
|
|
29
|
+
...waitInput(120),
|
|
30
|
+
},
|
|
31
|
+
outputSchema: {
|
|
32
|
+
...operationOutput,
|
|
33
|
+
database: databaseOutput.nullable(),
|
|
34
|
+
},
|
|
35
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
36
|
+
if (Boolean(args.user) !== Boolean(args.password)) {
|
|
37
|
+
throw new ToolInputError('Pass both `user` and `password` to create a user with the database, or neither.');
|
|
38
|
+
}
|
|
39
|
+
const base = databasesPath(organization(args.organization), args.server);
|
|
40
|
+
const response = await client.post(base, {
|
|
41
|
+
body: { name: args.name, user: args.user, password: args.password, backup_configuration_ids: args.backup_configuration_ids },
|
|
42
|
+
signal,
|
|
43
|
+
});
|
|
44
|
+
const initial = response.data?.data ? flattenSingle(response.data) : undefined;
|
|
45
|
+
const action = `create the database ${args.name}${args.user ? ` with user ${args.user}` : ''}`;
|
|
46
|
+
if (!initial || !args.wait) {
|
|
47
|
+
return {
|
|
48
|
+
structured: { status: 'queued', check_with: CHECK_WITH, database: initial ? formatDatabase(initial) : null },
|
|
49
|
+
summary: `Forge accepted the request to ${action}. Check it with ${CHECK_WITH}.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const result = await waitFor({
|
|
53
|
+
initial,
|
|
54
|
+
poll: () => readOne(client, `${base}/${encodeURIComponent(initial.id)}`, signal),
|
|
55
|
+
phase: (database) => databasePhase(database.status),
|
|
56
|
+
describe: (database) => `Database ${database.name} is ${database.status}`,
|
|
57
|
+
timeoutSeconds: args.timeout_seconds,
|
|
58
|
+
context: { sleep, progress },
|
|
59
|
+
});
|
|
60
|
+
const done = outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
61
|
+
return { structured: { ...done.structured, database: formatDatabase(result.value ?? initial) }, summary: done.summary };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, orGone, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { databaseUserInput, databaseUsersPath, readOne } from './shared.js';
|
|
5
|
+
const CHECK_WITH = 'forge_list_database_users';
|
|
6
|
+
export const deleteDatabaseUser = defineTool({
|
|
7
|
+
name: 'forge_delete_database_user',
|
|
8
|
+
title: 'Delete database user',
|
|
9
|
+
description: 'Delete a database user. Applications connecting with it lose database access.',
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.users.destroy', 'organizations.servers.database.users.show'],
|
|
12
|
+
permissions: ['server:delete-databases', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: 'Check the user ID with forge_list_database_users.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
user: databaseUserInput,
|
|
22
|
+
...waitInput(120),
|
|
23
|
+
},
|
|
24
|
+
outputSchema: operationOutput,
|
|
25
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
26
|
+
const path = `${databaseUsersPath(organization(args.organization), args.server)}/${encodeURIComponent(String(args.user))}`;
|
|
27
|
+
await client.delete(path, { signal });
|
|
28
|
+
const action = `delete database user ${args.user}`;
|
|
29
|
+
if (!args.wait)
|
|
30
|
+
return queued(action, CHECK_WITH);
|
|
31
|
+
const result = await waitFor({
|
|
32
|
+
poll: () => orGone(() => readOne(client, path, signal)),
|
|
33
|
+
phase: (user) => (user === null ? 'completed' : 'pending'),
|
|
34
|
+
describe: (user) => `Database user is ${user?.status ?? 'removing'}`,
|
|
35
|
+
timeoutSeconds: args.timeout_seconds,
|
|
36
|
+
context: { sleep, progress },
|
|
37
|
+
});
|
|
38
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, orGone, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { databaseInput, databasesPath, readOne } from './shared.js';
|
|
5
|
+
const CHECK_WITH = 'forge_list_databases';
|
|
6
|
+
export const deleteDatabase = defineTool({
|
|
7
|
+
name: 'forge_delete_database',
|
|
8
|
+
title: 'Delete database',
|
|
9
|
+
description: 'Drop a database schema from the server with all its data. This cannot be undone: make sure a backup exists.',
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.schemas.destroy', 'organizations.servers.database.schemas.show'],
|
|
12
|
+
permissions: ['server:delete-databases', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: 'Check the database ID with forge_list_databases.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
database: databaseInput,
|
|
22
|
+
...waitInput(120),
|
|
23
|
+
},
|
|
24
|
+
outputSchema: operationOutput,
|
|
25
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
26
|
+
const path = `${databasesPath(organization(args.organization), args.server)}/${encodeURIComponent(String(args.database))}`;
|
|
27
|
+
await client.delete(path, { signal });
|
|
28
|
+
const action = `delete database ${args.database}`;
|
|
29
|
+
if (!args.wait)
|
|
30
|
+
return queued(action, CHECK_WITH);
|
|
31
|
+
const result = await waitFor({
|
|
32
|
+
poll: () => orGone(() => readOne(client, path, signal)),
|
|
33
|
+
phase: (database) => (database === null ? 'completed' : 'pending'),
|
|
34
|
+
describe: (database) => `Database is ${database?.status ?? 'removing'}`,
|
|
35
|
+
timeoutSeconds: args.timeout_seconds,
|
|
36
|
+
context: { sleep, progress },
|
|
37
|
+
});
|
|
38
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { organizationInput, paginationInput, paginationOutput, paginationSummary, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { databaseOutput, databaseUserInput, databaseUsersPath, formatDatabase, readOne } from './shared.js';
|
|
7
|
+
const SORT = ['name', '-name', 'created_at', '-created_at', 'updated_at', '-updated_at'];
|
|
8
|
+
export const listDatabaseUsers = defineTool({
|
|
9
|
+
name: 'forge_list_database_users',
|
|
10
|
+
title: 'List database users',
|
|
11
|
+
description: 'List the database users on a server, or get one with `user`. Passwords and granted databases are not exposed by Forge.',
|
|
12
|
+
toolset: 'databases',
|
|
13
|
+
operations: ['organizations.servers.database.users.index', 'organizations.servers.database.users.show'],
|
|
14
|
+
permissions: ['server:view'],
|
|
15
|
+
readOnly: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
user: databaseUserInput.optional().describe('Return only this user.'),
|
|
21
|
+
name: z.string().min(1).optional().describe('Filter by name.'),
|
|
22
|
+
status: z.string().min(1).optional().describe('Filter by status, e.g. "installed".'),
|
|
23
|
+
sort: z.array(z.enum(SORT)).min(1).optional(),
|
|
24
|
+
...paginationInput,
|
|
25
|
+
},
|
|
26
|
+
outputSchema: {
|
|
27
|
+
users: z.array(databaseOutput),
|
|
28
|
+
...paginationOutput,
|
|
29
|
+
},
|
|
30
|
+
async handler(args, { client, organization, signal }) {
|
|
31
|
+
const base = databaseUsersPath(organization(args.organization), args.server);
|
|
32
|
+
if (args.user !== undefined) {
|
|
33
|
+
const user = formatDatabase(await readOne(client, `${base}/${encodeURIComponent(String(args.user))}`, signal));
|
|
34
|
+
return { structured: { users: [user], next_cursor: null, has_more: false }, summary: `Database user ${user.name} is ${user.status}.` };
|
|
35
|
+
}
|
|
36
|
+
const response = await client.get(base, {
|
|
37
|
+
query: { filter: { name: args.name, status: args.status }, sort: args.sort, page: { size: args.page_size, cursor: args.cursor } },
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
const page = flattenCollection(response.data);
|
|
41
|
+
const users = page.items.map(formatDatabase);
|
|
42
|
+
return {
|
|
43
|
+
structured: { users, next_cursor: page.nextCursor, has_more: page.nextCursor !== null },
|
|
44
|
+
summary: users.length === 0
|
|
45
|
+
? 'No database users found.'
|
|
46
|
+
: `Found ${users.length} database user(s): ${users.map((u) => u.name).join(', ')}.${paginationSummary(page.nextCursor)}`,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { organizationInput, paginationInput, paginationOutput, paginationSummary, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { databaseInput, databaseOutput, databasesPath, formatDatabase, readOne } from './shared.js';
|
|
7
|
+
const SORT = ['name', '-name', 'created_at', '-created_at', 'updated_at', '-updated_at'];
|
|
8
|
+
export const listDatabases = defineTool({
|
|
9
|
+
name: 'forge_list_databases',
|
|
10
|
+
title: 'List databases',
|
|
11
|
+
description: 'List the database schemas on a server (MySQL, MariaDB or Postgres), or get one with `database`.',
|
|
12
|
+
toolset: 'databases',
|
|
13
|
+
operations: ['organizations.servers.database.schemas.index', 'organizations.servers.database.schemas.show'],
|
|
14
|
+
permissions: ['server:view'],
|
|
15
|
+
readOnly: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
database: databaseInput.optional().describe('Return only this database.'),
|
|
21
|
+
name: z.string().min(1).optional().describe('Filter by name.'),
|
|
22
|
+
status: z.string().min(1).optional().describe('Filter by status, e.g. "installed".'),
|
|
23
|
+
sort: z.array(z.enum(SORT)).min(1).optional(),
|
|
24
|
+
...paginationInput,
|
|
25
|
+
},
|
|
26
|
+
outputSchema: {
|
|
27
|
+
databases: z.array(databaseOutput),
|
|
28
|
+
...paginationOutput,
|
|
29
|
+
},
|
|
30
|
+
async handler(args, { client, organization, signal }) {
|
|
31
|
+
const base = databasesPath(organization(args.organization), args.server);
|
|
32
|
+
if (args.database !== undefined) {
|
|
33
|
+
const database = formatDatabase(await readOne(client, `${base}/${encodeURIComponent(String(args.database))}`, signal));
|
|
34
|
+
return { structured: { databases: [database], next_cursor: null, has_more: false }, summary: `Database ${database.name} is ${database.status}.` };
|
|
35
|
+
}
|
|
36
|
+
const response = await client.get(base, {
|
|
37
|
+
query: { filter: { name: args.name, status: args.status }, sort: args.sort, page: { size: args.page_size, cursor: args.cursor } },
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
const page = flattenCollection(response.data);
|
|
41
|
+
const databases = page.items.map(formatDatabase);
|
|
42
|
+
return {
|
|
43
|
+
structured: { databases, next_cursor: page.nextCursor, has_more: page.nextCursor !== null },
|
|
44
|
+
summary: databases.length === 0
|
|
45
|
+
? 'No databases found.'
|
|
46
|
+
: `Found ${databases.length} database(s): ${databases.map((d) => d.name).join(', ')}.${paginationSummary(page.nextCursor)}`,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection, flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
import { ToolInputError } from '../errors.js';
|
|
4
|
+
import { phaseOf } from '../shared/async.js';
|
|
5
|
+
import { idInput, pick } from '../shared/schemas.js';
|
|
6
|
+
import { serverPath } from '../servers/shared.js';
|
|
7
|
+
export function databasesPath(org, server) {
|
|
8
|
+
return `${serverPath(org, server)}/database/schemas`;
|
|
9
|
+
}
|
|
10
|
+
export function databaseUsersPath(org, server) {
|
|
11
|
+
return `${serverPath(org, server)}/database/users`;
|
|
12
|
+
}
|
|
13
|
+
export const databaseInput = idInput('Database ID. Use forge_list_databases to find it.');
|
|
14
|
+
export const databaseUserInput = idInput('Database user ID. Use forge_list_database_users to find it.');
|
|
15
|
+
export const databaseRefsInput = z
|
|
16
|
+
.array(z.union([z.number().int().nonnegative(), z.string().min(1)]))
|
|
17
|
+
.describe('Databases by ID or by name, e.g. [12, "shop"].');
|
|
18
|
+
export const passwordInput = z.string().min(8).describe('Password (never returned by any tool).');
|
|
19
|
+
const FIELDS = ['id', 'name', 'status', 'created_at', 'updated_at'];
|
|
20
|
+
export const databaseOutput = z.looseObject({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
name: z.string().nullable(),
|
|
23
|
+
status: z.string().nullable().describe('installing, installed, removing (users also: updating).'),
|
|
24
|
+
created_at: z.string().nullable(),
|
|
25
|
+
updated_at: z.string().nullable(),
|
|
26
|
+
});
|
|
27
|
+
export function formatDatabase(flat) {
|
|
28
|
+
return pick(flat, FIELDS);
|
|
29
|
+
}
|
|
30
|
+
export async function readOne(client, path, signal) {
|
|
31
|
+
const response = await client.get(path, { signal });
|
|
32
|
+
return flattenSingle(response.data);
|
|
33
|
+
}
|
|
34
|
+
/** Databases and database users share the same transitional statuses. */
|
|
35
|
+
export function databasePhase(status) {
|
|
36
|
+
return phaseOf(status, { pending: ['installing', 'updating', 'removing'] });
|
|
37
|
+
}
|
|
38
|
+
/** Resolves database IDs or names to IDs, failing with the names that do not exist. */
|
|
39
|
+
export async function resolveDatabaseIds(client, org, server, refs, signal) {
|
|
40
|
+
const ids = [];
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const ref of refs) {
|
|
43
|
+
if (typeof ref === 'number' || /^\d+$/.test(ref)) {
|
|
44
|
+
ids.push(Number(ref));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const response = await client.get(databasesPath(org, server), {
|
|
48
|
+
query: { filter: { name: ref }, page: { size: 100 } },
|
|
49
|
+
signal,
|
|
50
|
+
});
|
|
51
|
+
const match = flattenCollection(response.data).items.find((database) => database.name === ref);
|
|
52
|
+
if (match)
|
|
53
|
+
ids.push(Number(match.id));
|
|
54
|
+
else
|
|
55
|
+
missing.push(ref);
|
|
56
|
+
}
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
throw new ToolInputError(`Database(s) not found on server ${server}: ${missing.join(', ')}. Check the names with forge_list_databases.`);
|
|
59
|
+
}
|
|
60
|
+
return ids;
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, queued } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
5
|
+
import { databasesPath } from './shared.js';
|
|
6
|
+
export const syncDatabases = defineTool({
|
|
7
|
+
name: 'forge_sync_databases',
|
|
8
|
+
title: 'Synchronize databases',
|
|
9
|
+
description: 'Make Forge pick up databases created outside Forge (e.g. from a SQL client), so they can be managed and backed up.',
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.schemas.synchronizations.store'],
|
|
12
|
+
permissions: ['server:create-databases'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: false,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
},
|
|
22
|
+
outputSchema: operationOutput,
|
|
23
|
+
async handler(args, { client, organization, signal }) {
|
|
24
|
+
await client.post(`${databasesPath(organization(args.organization), args.server)}/synchronizations`, { signal });
|
|
25
|
+
// Which databases will appear is unknown in advance, so there is nothing to wait for.
|
|
26
|
+
return queued('synchronize the databases', 'forge_list_databases');
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT, serverPath } from '../servers/shared.js';
|
|
5
|
+
import { passwordInput } from './shared.js';
|
|
6
|
+
export const updateDatabaseRootPassword = defineTool({
|
|
7
|
+
name: 'forge_update_database_root_password',
|
|
8
|
+
title: 'Change database root password',
|
|
9
|
+
description: "Change the password of the database server's root user (the `forge` user on Forge-provisioned servers). Applications or tools using the old root password stop connecting.",
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.password.update'],
|
|
12
|
+
permissions: ['server:manage-services'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
password: passwordInput.describe('New root password (never returned).'),
|
|
21
|
+
},
|
|
22
|
+
outputSchema: {
|
|
23
|
+
updated: z.boolean(),
|
|
24
|
+
},
|
|
25
|
+
async handler(args, { client, organization, signal }) {
|
|
26
|
+
await client.put(`${serverPath(organization(args.organization), args.server)}/database/password`, {
|
|
27
|
+
body: { password: args.password },
|
|
28
|
+
signal,
|
|
29
|
+
});
|
|
30
|
+
return { structured: { updated: true }, summary: 'Database root password changed.' };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { ToolInputError } from '../errors.js';
|
|
3
|
+
import { operationOutput, queued } from '../shared/async.js';
|
|
4
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { databaseRefsInput, databaseUserInput, databaseUsersPath, passwordInput, resolveDatabaseIds } from './shared.js';
|
|
6
|
+
export const updateDatabaseUser = defineTool({
|
|
7
|
+
name: 'forge_update_database_user',
|
|
8
|
+
title: 'Update database user',
|
|
9
|
+
description: "Change a database user's password and/or the databases it can access. `databases` replaces the whole list (by ID or name): include the ones it should keep.",
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.users.update', 'organizations.servers.database.schemas.index'],
|
|
12
|
+
permissions: ['server:create-databases', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: false,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: 'Check the user ID with forge_list_database_users.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
user: databaseUserInput,
|
|
22
|
+
password: passwordInput.optional().describe('New password (never returned).'),
|
|
23
|
+
databases: databaseRefsInput.optional().describe('Every database the user should access, by ID or name (replaces the list).'),
|
|
24
|
+
},
|
|
25
|
+
outputSchema: operationOutput,
|
|
26
|
+
async handler(args, { client, organization, signal }) {
|
|
27
|
+
if (args.password === undefined && args.databases === undefined) {
|
|
28
|
+
throw new ToolInputError('Pass `password` and/or `databases`.');
|
|
29
|
+
}
|
|
30
|
+
const org = organization(args.organization);
|
|
31
|
+
const databaseIds = args.databases ? await resolveDatabaseIds(client, org, args.server, args.databases, signal) : undefined;
|
|
32
|
+
await client.put(`${databaseUsersPath(org, args.server)}/${encodeURIComponent(String(args.user))}`, {
|
|
33
|
+
body: { password: args.password, database_ids: databaseIds },
|
|
34
|
+
signal,
|
|
35
|
+
});
|
|
36
|
+
// Neither passwords nor granted databases are exposed by the API: nothing to verify.
|
|
37
|
+
return queued(`update database user ${args.user}`, 'forge_list_database_users', '(the status shows "updating" while it runs)');
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
export const phpConfigInput = z
|
|
4
|
+
.enum(['fpm', 'cli', 'pool'])
|
|
5
|
+
.describe('"fpm": php.ini used by PHP-FPM (web requests); "cli": php.ini of the `php` command; "pool": PHP-FPM pool configuration (workers, user).');
|
|
6
|
+
export const poolUserInput = z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Pool only: the Linux user of an isolated site\'s pool (default: the "forge" pool).');
|
|
11
|
+
export async function readPhpConfig(client, path, user, signal) {
|
|
12
|
+
const response = await client.get(path, { query: { user }, signal });
|
|
13
|
+
return flattenSingle(response.data).configuration ?? '';
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
5
|
+
import { phpConfigInput, poolUserInput, readPhpConfig } from './config-files.js';
|
|
6
|
+
import { phpVersionRefInput, phpVersionsPath, resolvePhpVersionId } from './shared.js';
|
|
7
|
+
export const getPhpConfig = defineTool({
|
|
8
|
+
name: 'forge_get_php_config',
|
|
9
|
+
title: 'Get PHP configuration file',
|
|
10
|
+
description: 'Read a configuration file of an installed PHP version: the FPM or CLI php.ini, or the PHP-FPM pool configuration.',
|
|
11
|
+
toolset: 'servers',
|
|
12
|
+
operations: [
|
|
13
|
+
'organizations.servers.php.versions.configs.fpm.show',
|
|
14
|
+
'organizations.servers.php.versions.configs.cli.show',
|
|
15
|
+
'organizations.servers.php.versions.configs.pool.show',
|
|
16
|
+
'organizations.servers.php.versions.index',
|
|
17
|
+
],
|
|
18
|
+
permissions: ['server:view'],
|
|
19
|
+
readOnly: true,
|
|
20
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
21
|
+
inputSchema: {
|
|
22
|
+
organization: organizationInput,
|
|
23
|
+
server: serverInput,
|
|
24
|
+
php_version: phpVersionRefInput,
|
|
25
|
+
config: phpConfigInput,
|
|
26
|
+
user: poolUserInput,
|
|
27
|
+
},
|
|
28
|
+
outputSchema: {
|
|
29
|
+
content: z.string().describe('Content of the configuration file.'),
|
|
30
|
+
},
|
|
31
|
+
async handler(args, { client, organization, signal }) {
|
|
32
|
+
const org = organization(args.organization);
|
|
33
|
+
const id = await resolvePhpVersionId(client, org, args.server, args.php_version, signal);
|
|
34
|
+
const path = `${phpVersionsPath(org, args.server)}/${encodeURIComponent(id)}/configs/${args.config}`;
|
|
35
|
+
const content = await readPhpConfig(client, path, args.config === 'pool' ? args.user : undefined, signal);
|
|
36
|
+
return { structured: { content }, summary: `The ${args.config} configuration has ${content.trimEnd().split('\n').length} line(s).` };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
5
|
+
import { readPhpSetting } from './settings.js';
|
|
6
|
+
export const getPhpSettings = defineTool({
|
|
7
|
+
name: 'forge_get_php_settings',
|
|
8
|
+
title: 'Get PHP settings',
|
|
9
|
+
description: "Get a server's PHP settings in one call: default CLI version, default version for new sites, max upload size, max execution time and whether OPcache is enabled.",
|
|
10
|
+
toolset: 'servers',
|
|
11
|
+
operations: [
|
|
12
|
+
'organizations.servers.php.cli-version.show',
|
|
13
|
+
'organizations.servers.php.site-version.show',
|
|
14
|
+
'organizations.servers.php.max-upload-size.show',
|
|
15
|
+
'organizations.servers.php.max-execution-time.show',
|
|
16
|
+
'organizations.servers.php.opcache.show',
|
|
17
|
+
],
|
|
18
|
+
permissions: ['server:view'],
|
|
19
|
+
readOnly: true,
|
|
20
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
21
|
+
inputSchema: {
|
|
22
|
+
organization: organizationInput,
|
|
23
|
+
server: serverInput,
|
|
24
|
+
},
|
|
25
|
+
outputSchema: {
|
|
26
|
+
cli_version: z.string().nullable().describe('Version used by the `php` command, e.g. "8.3".'),
|
|
27
|
+
site_version: z.string().nullable().describe('Default version for new sites.'),
|
|
28
|
+
max_upload_size: z.number().nullable().describe('Max upload size in MB.'),
|
|
29
|
+
max_execution_time: z.number().nullable().describe('Max execution time in seconds.'),
|
|
30
|
+
opcache_enabled: z.boolean().nullable(),
|
|
31
|
+
},
|
|
32
|
+
async handler(args, { client, organization, signal }) {
|
|
33
|
+
const org = organization(args.organization);
|
|
34
|
+
const read = (name) => readPhpSetting(client, org, args.server, name, signal);
|
|
35
|
+
const [cli, site, upload, execution, opcache] = await Promise.all([
|
|
36
|
+
read('cli-version'),
|
|
37
|
+
read('site-version'),
|
|
38
|
+
read('max-upload-size'),
|
|
39
|
+
read('max-execution-time'),
|
|
40
|
+
read('opcache'),
|
|
41
|
+
]);
|
|
42
|
+
const settings = {
|
|
43
|
+
cli_version: cli.version ?? null,
|
|
44
|
+
site_version: site.version ?? null,
|
|
45
|
+
max_upload_size: upload.max_upload_size ?? null,
|
|
46
|
+
max_execution_time: execution.max_execution_time ?? null,
|
|
47
|
+
opcache_enabled: opcache.opcache_enabled ?? null,
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
structured: settings,
|
|
51
|
+
summary: `PHP CLI ${settings.cli_version}, sites ${settings.site_version}, upload ${settings.max_upload_size} MB, execution ${settings.max_execution_time}s, OPcache ${settings.opcache_enabled ? 'on' : 'off'}.`,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { operationOutput, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
4
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { PHP_VERSIONS } from '../sites/shared.js';
|
|
7
|
+
import { findPhpVersion, phpVersionOutput, phpVersionPhase, phpVersionsPath, versionNumber } from './shared.js';
|
|
8
|
+
const CHECK_WITH = 'forge_list_php_versions';
|
|
9
|
+
export const installPhpVersion = defineTool({
|
|
10
|
+
name: 'forge_install_php_version',
|
|
11
|
+
title: 'Install PHP version',
|
|
12
|
+
description: 'Install another PHP version on a server, optionally making it the default for the CLI and/or new sites. Waits until it is installed unless `wait` is false.',
|
|
13
|
+
toolset: 'servers',
|
|
14
|
+
operations: ['organizations.servers.php.versions.store', 'organizations.servers.php.versions.index'],
|
|
15
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
16
|
+
readOnly: false,
|
|
17
|
+
destructive: false,
|
|
18
|
+
idempotent: false,
|
|
19
|
+
async: true,
|
|
20
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
21
|
+
inputSchema: {
|
|
22
|
+
organization: organizationInput,
|
|
23
|
+
server: serverInput,
|
|
24
|
+
version: z.enum(PHP_VERSIONS).describe('Version to install, e.g. "php84".'),
|
|
25
|
+
cli_default: z.boolean().optional().describe('Make it the version used by the `php` command.'),
|
|
26
|
+
site_default: z.boolean().optional().describe('Make it the default for new sites.'),
|
|
27
|
+
...waitInput(300),
|
|
28
|
+
},
|
|
29
|
+
outputSchema: {
|
|
30
|
+
...operationOutput,
|
|
31
|
+
php_version: phpVersionOutput.nullable(),
|
|
32
|
+
},
|
|
33
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
34
|
+
const org = organization(args.organization);
|
|
35
|
+
await client.post(phpVersionsPath(org, args.server), {
|
|
36
|
+
body: { version: args.version, cli_default: args.cli_default, site_default: args.site_default },
|
|
37
|
+
signal,
|
|
38
|
+
});
|
|
39
|
+
const action = `install ${args.version}`;
|
|
40
|
+
const number = versionNumber(args.version);
|
|
41
|
+
if (!args.wait || !number) {
|
|
42
|
+
const accepted = queued(action, CHECK_WITH);
|
|
43
|
+
return { ...accepted, structured: { ...accepted.structured, php_version: null } };
|
|
44
|
+
}
|
|
45
|
+
// Forge returns no body: follow the version once it appears in the list.
|
|
46
|
+
const result = await waitFor({
|
|
47
|
+
poll: () => findPhpVersion(client, org, args.server, number, signal).then((found) => found ?? null),
|
|
48
|
+
phase: (found) => (found ? phpVersionPhase(found.status) : 'pending'),
|
|
49
|
+
describe: (found) => `PHP ${number} is ${found?.status ?? 'queued'}`,
|
|
50
|
+
timeoutSeconds: args.timeout_seconds,
|
|
51
|
+
context: { sleep, progress },
|
|
52
|
+
});
|
|
53
|
+
const done = outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
54
|
+
return { structured: { ...done.structured, php_version: result.value ?? null }, summary: done.summary };
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { organizationInput, paginationInput, paginationOutput, paginationSummary, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { formatPhpVersion, phpVersionOutput, phpVersionsPath } from './shared.js';
|
|
7
|
+
const SORT = ['version', '-version', 'status', '-status', 'created_at', '-created_at', 'updated_at', '-updated_at'];
|
|
8
|
+
export const listPhpVersions = defineTool({
|
|
9
|
+
name: 'forge_list_php_versions',
|
|
10
|
+
title: 'List PHP versions',
|
|
11
|
+
description: 'List the PHP versions installed on a server with their IDs and status. Default CLI and site versions are in forge_get_php_settings.',
|
|
12
|
+
toolset: 'servers',
|
|
13
|
+
operations: ['organizations.servers.php.versions.index'],
|
|
14
|
+
permissions: ['server:view'],
|
|
15
|
+
readOnly: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
version: z.string().min(1).optional().describe('Filter by version, e.g. "8.3".'),
|
|
21
|
+
status: z.string().min(1).optional().describe('Filter by status, e.g. "installed".'),
|
|
22
|
+
sort: z.array(z.enum(SORT)).min(1).optional(),
|
|
23
|
+
...paginationInput,
|
|
24
|
+
},
|
|
25
|
+
outputSchema: {
|
|
26
|
+
php_versions: z.array(phpVersionOutput),
|
|
27
|
+
...paginationOutput,
|
|
28
|
+
},
|
|
29
|
+
async handler(args, { client, organization, signal }) {
|
|
30
|
+
const response = await client.get(phpVersionsPath(organization(args.organization), args.server), {
|
|
31
|
+
query: {
|
|
32
|
+
filter: { version: args.version, status: args.status },
|
|
33
|
+
sort: args.sort,
|
|
34
|
+
page: { size: args.page_size, cursor: args.cursor },
|
|
35
|
+
},
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
38
|
+
const page = flattenCollection(response.data);
|
|
39
|
+
const versions = page.items.map(formatPhpVersion);
|
|
40
|
+
return {
|
|
41
|
+
structured: { php_versions: versions, next_cursor: page.nextCursor, has_more: page.nextCursor !== null },
|
|
42
|
+
summary: versions.length === 0
|
|
43
|
+
? 'No PHP versions found.'
|
|
44
|
+
: `Installed: ${versions.map((v) => `${v.version} (${v.status}, ID ${v.id})`).join(', ')}.${paginationSummary(page.nextCursor)}`,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { ToolInputError } from '../errors.js';
|
|
4
|
+
import { operationOutput, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
5
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
6
|
+
import { SERVER_NOT_FOUND_HINT, serverPath } from '../servers/shared.js';
|
|
7
|
+
import { readPhpSetting } from './settings.js';
|
|
8
|
+
import { versionNumber } from './shared.js';
|
|
9
|
+
const CHECK_WITH = 'forge_get_php_settings';
|
|
10
|
+
export const setDefaultPhpVersion = defineTool({
|
|
11
|
+
name: 'forge_set_default_php_version',
|
|
12
|
+
title: 'Set default PHP version',
|
|
13
|
+
description: 'Set the PHP version used by the `php` command on the server ("cli") or the default for new sites ("site"). The version must be installed (forge_list_php_versions). Existing sites keep their version: change it with forge_update_site.',
|
|
14
|
+
toolset: 'servers',
|
|
15
|
+
operations: [
|
|
16
|
+
'organizations.servers.php.cli-version.update',
|
|
17
|
+
'organizations.servers.php.site-version.update',
|
|
18
|
+
'organizations.servers.php.cli-version.show',
|
|
19
|
+
'organizations.servers.php.site-version.show',
|
|
20
|
+
],
|
|
21
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
22
|
+
readOnly: false,
|
|
23
|
+
destructive: false,
|
|
24
|
+
idempotent: true,
|
|
25
|
+
async: true,
|
|
26
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
27
|
+
inputSchema: {
|
|
28
|
+
organization: organizationInput,
|
|
29
|
+
server: serverInput,
|
|
30
|
+
target: z.enum(['cli', 'site']).describe('"cli" for the `php` command, "site" for new sites.'),
|
|
31
|
+
php_version: z.string().min(1).describe('Version, e.g. "8.3" or "php83".'),
|
|
32
|
+
...waitInput(120),
|
|
33
|
+
},
|
|
34
|
+
outputSchema: operationOutput,
|
|
35
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
36
|
+
const number = versionNumber(args.php_version);
|
|
37
|
+
if (!number)
|
|
38
|
+
throw new ToolInputError(`Unknown PHP version "${args.php_version}": use a version like "8.3".`);
|
|
39
|
+
const org = organization(args.organization);
|
|
40
|
+
const setting = args.target === 'cli' ? 'cli-version' : 'site-version';
|
|
41
|
+
await client.put(`${serverPath(org, args.server)}/php/${setting}`, { body: { php_version: number }, signal });
|
|
42
|
+
const action = `set the default ${args.target} PHP version to ${number}`;
|
|
43
|
+
if (!args.wait)
|
|
44
|
+
return queued(action, CHECK_WITH);
|
|
45
|
+
const result = await waitFor({
|
|
46
|
+
poll: async () => (await readPhpSetting(client, org, args.server, setting, signal)).version,
|
|
47
|
+
phase: (version) => (version === number ? 'completed' : 'pending'),
|
|
48
|
+
describe: (version) => `Default ${args.target} PHP version is ${version}`,
|
|
49
|
+
timeoutSeconds: args.timeout_seconds,
|
|
50
|
+
context: { sleep, progress },
|
|
51
|
+
});
|
|
52
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { operationOutput, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
4
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT, serverPath } from '../servers/shared.js';
|
|
6
|
+
import { readPhpSetting } from './settings.js';
|
|
7
|
+
const CHECK_WITH = 'forge_get_php_settings';
|
|
8
|
+
export const setPhpOpcache = defineTool({
|
|
9
|
+
name: 'forge_set_php_opcache',
|
|
10
|
+
title: 'Enable or disable OPcache',
|
|
11
|
+
description: 'Enable or disable PHP OPcache on the server. Enabled OPcache speeds up PHP; with it, code changes are picked up only after PHP-FPM reloads (Forge deployments reload it).',
|
|
12
|
+
toolset: 'servers',
|
|
13
|
+
operations: [
|
|
14
|
+
'organizations.servers.php.opcache.store',
|
|
15
|
+
'organizations.servers.php.opcache.destroy',
|
|
16
|
+
'organizations.servers.php.opcache.show',
|
|
17
|
+
],
|
|
18
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
19
|
+
readOnly: false,
|
|
20
|
+
destructive: false,
|
|
21
|
+
idempotent: true,
|
|
22
|
+
async: true,
|
|
23
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
24
|
+
inputSchema: {
|
|
25
|
+
organization: organizationInput,
|
|
26
|
+
server: serverInput,
|
|
27
|
+
enabled: z.boolean(),
|
|
28
|
+
...waitInput(120),
|
|
29
|
+
},
|
|
30
|
+
outputSchema: operationOutput,
|
|
31
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
32
|
+
const org = organization(args.organization);
|
|
33
|
+
const path = `${serverPath(org, args.server)}/php/opcache`;
|
|
34
|
+
if (args.enabled) {
|
|
35
|
+
await client.post(path, { signal });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await client.delete(path, { signal });
|
|
39
|
+
}
|
|
40
|
+
const action = `${args.enabled ? 'enable' : 'disable'} OPcache`;
|
|
41
|
+
if (!args.wait)
|
|
42
|
+
return queued(action, CHECK_WITH);
|
|
43
|
+
const result = await waitFor({
|
|
44
|
+
poll: async () => (await readPhpSetting(client, org, args.server, 'opcache', signal)).opcache_enabled,
|
|
45
|
+
phase: (enabled) => (enabled === args.enabled ? 'completed' : 'pending'),
|
|
46
|
+
describe: () => `Waiting for OPcache to be ${args.enabled ? 'enabled' : 'disabled'}`,
|
|
47
|
+
timeoutSeconds: args.timeout_seconds,
|
|
48
|
+
context: { sleep, progress },
|
|
49
|
+
});
|
|
50
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
2
|
+
import { serverPath } from '../servers/shared.js';
|
|
3
|
+
/** Reads one PHP setting endpoint (`/php/<name>`) and returns its attributes. */
|
|
4
|
+
export async function readPhpSetting(client, org, server, name, signal) {
|
|
5
|
+
const response = await client.get(`${serverPath(org, server)}/php/${name}`, { signal });
|
|
6
|
+
return flattenSingle(response.data);
|
|
7
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection } from '../../forge/jsonapi.js';
|
|
3
|
+
import { phaseOf } from '../shared/async.js';
|
|
4
|
+
import { pick } from '../shared/schemas.js';
|
|
5
|
+
import { ToolInputError } from '../errors.js';
|
|
6
|
+
import { serverPath } from '../servers/shared.js';
|
|
7
|
+
/** Version numbers used by the default-version endpoints, matching the install identifiers. */
|
|
8
|
+
const VERSION_NUMBERS = {
|
|
9
|
+
php56: '5.6',
|
|
10
|
+
php70: '7.0',
|
|
11
|
+
php71: '7.1',
|
|
12
|
+
php72: '7.2',
|
|
13
|
+
php73: '7.3',
|
|
14
|
+
php74: '7.4',
|
|
15
|
+
php80: '8.0',
|
|
16
|
+
php81: '8.1',
|
|
17
|
+
php82: '8.2',
|
|
18
|
+
php83: '8.3',
|
|
19
|
+
php84: '8.4',
|
|
20
|
+
php85: '8.5',
|
|
21
|
+
};
|
|
22
|
+
export const PHP_VERSION_NUMBERS = ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'];
|
|
23
|
+
/** "php83", "8.3", "php8.3" or "PHP 8.3" → "8.3"; undefined when not a known version. */
|
|
24
|
+
export function versionNumber(value) {
|
|
25
|
+
const compact = value.trim().toLowerCase().replace(/\s+/g, '');
|
|
26
|
+
const dotted = compact.replace(/^php/, '');
|
|
27
|
+
if (PHP_VERSION_NUMBERS.includes(dotted))
|
|
28
|
+
return dotted;
|
|
29
|
+
return VERSION_NUMBERS[compact];
|
|
30
|
+
}
|
|
31
|
+
export const phpVersionRefInput = z
|
|
32
|
+
.union([z.number().int().nonnegative(), z.string().min(1)])
|
|
33
|
+
.describe('Installed PHP version: its ID (from forge_list_php_versions) or the version, e.g. "8.3" or "php83".');
|
|
34
|
+
export function phpVersionsPath(org, server) {
|
|
35
|
+
return `${serverPath(org, server)}/php/versions`;
|
|
36
|
+
}
|
|
37
|
+
export const phpVersionOutput = z.looseObject({
|
|
38
|
+
id: z.string().describe('PHP version ID.'),
|
|
39
|
+
version: z.string().nullable().describe('e.g. "8.3".'),
|
|
40
|
+
binary_name: z.string().nullable().describe('e.g. "php8.3".'),
|
|
41
|
+
status: z.string().nullable(),
|
|
42
|
+
created_at: z.string().nullable(),
|
|
43
|
+
});
|
|
44
|
+
export function formatPhpVersion(flat) {
|
|
45
|
+
return pick(flat, ['id', 'version', 'binary_name', 'status', 'created_at']);
|
|
46
|
+
}
|
|
47
|
+
/** Installed version with this number, or undefined. */
|
|
48
|
+
export async function findPhpVersion(client, org, server, number, signal) {
|
|
49
|
+
const response = await client.get(phpVersionsPath(org, server), {
|
|
50
|
+
query: { filter: { version: number }, page: { size: 100 } },
|
|
51
|
+
signal,
|
|
52
|
+
});
|
|
53
|
+
const match = flattenCollection(response.data).items.find((item) => item.version === number);
|
|
54
|
+
return match ? formatPhpVersion(match) : undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Resolves an ID or a version ("8.3", "php83") to the ID of the installed version. */
|
|
57
|
+
export async function resolvePhpVersionId(client, org, server, ref, signal) {
|
|
58
|
+
// Digits only is an ID; versions are written with a dot ("8.3") or a "php" prefix.
|
|
59
|
+
if (typeof ref === 'number' || /^\d+$/.test(ref))
|
|
60
|
+
return String(ref);
|
|
61
|
+
const number = versionNumber(ref);
|
|
62
|
+
if (!number)
|
|
63
|
+
throw new ToolInputError(`Unknown PHP version "${ref}": use an ID from forge_list_php_versions or a version like "8.3".`);
|
|
64
|
+
const installed = await findPhpVersion(client, org, server, number, signal);
|
|
65
|
+
if (!installed) {
|
|
66
|
+
throw new ToolInputError(`PHP ${number} is not installed on server ${server}. Install it with forge_install_php_version.`);
|
|
67
|
+
}
|
|
68
|
+
return installed.id;
|
|
69
|
+
}
|
|
70
|
+
export function phpVersionPhase(status) {
|
|
71
|
+
return phaseOf(status, { pending: ['installing', 'updating', 'removing', 'uninstalling'], failed: ['failed'] });
|
|
72
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { operationOutput, orGone, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
4
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { phpVersionRefInput, phpVersionsPath, resolvePhpVersionId } from './shared.js';
|
|
7
|
+
const CHECK_WITH = 'forge_list_php_versions';
|
|
8
|
+
export const uninstallPhpVersion = defineTool({
|
|
9
|
+
name: 'forge_uninstall_php_version',
|
|
10
|
+
title: 'Uninstall PHP version',
|
|
11
|
+
description: 'Uninstall a PHP version from a server. Sites still using it stop working: move them first with forge_update_site.',
|
|
12
|
+
toolset: 'servers',
|
|
13
|
+
operations: [
|
|
14
|
+
'organizations.servers.php.versions.destroy',
|
|
15
|
+
'organizations.servers.php.versions.show',
|
|
16
|
+
'organizations.servers.php.versions.index',
|
|
17
|
+
],
|
|
18
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
19
|
+
readOnly: false,
|
|
20
|
+
destructive: true,
|
|
21
|
+
idempotent: true,
|
|
22
|
+
async: true,
|
|
23
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
24
|
+
inputSchema: {
|
|
25
|
+
organization: organizationInput,
|
|
26
|
+
server: serverInput,
|
|
27
|
+
php_version: phpVersionRefInput,
|
|
28
|
+
...waitInput(180),
|
|
29
|
+
},
|
|
30
|
+
outputSchema: operationOutput,
|
|
31
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
32
|
+
const org = organization(args.organization);
|
|
33
|
+
const id = await resolvePhpVersionId(client, org, args.server, args.php_version, signal);
|
|
34
|
+
const path = `${phpVersionsPath(org, args.server)}/${encodeURIComponent(id)}`;
|
|
35
|
+
await client.delete(path, { signal });
|
|
36
|
+
const action = `uninstall PHP ${args.php_version}`;
|
|
37
|
+
if (!args.wait)
|
|
38
|
+
return queued(action, CHECK_WITH);
|
|
39
|
+
const result = await waitFor({
|
|
40
|
+
poll: () => orGone(async () => flattenSingle((await client.get(path, { signal })).data)),
|
|
41
|
+
phase: (version) => (version === null ? 'completed' : 'pending'),
|
|
42
|
+
describe: (version) => `PHP ${args.php_version} is ${version?.status ?? 'removing'}`,
|
|
43
|
+
timeoutSeconds: args.timeout_seconds,
|
|
44
|
+
context: { sleep, progress },
|
|
45
|
+
});
|
|
46
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { operationOutput, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
4
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { phpConfigInput, poolUserInput, readPhpConfig } from './config-files.js';
|
|
7
|
+
import { phpVersionRefInput, phpVersionsPath, resolvePhpVersionId } from './shared.js';
|
|
8
|
+
const CHECK_WITH = 'forge_get_php_config';
|
|
9
|
+
export const updatePhpConfig = defineTool({
|
|
10
|
+
name: 'forge_update_php_config',
|
|
11
|
+
title: 'Replace PHP configuration file',
|
|
12
|
+
description: 'Replace a whole configuration file of an installed PHP version (FPM or CLI php.ini, or the FPM pool) and reload PHP. Read it first with forge_get_php_config and send the complete new file: an invalid file can stop PHP-FPM and every site using it.',
|
|
13
|
+
toolset: 'servers',
|
|
14
|
+
operations: [
|
|
15
|
+
'organizations.servers.php.versions.configs.fpm.update',
|
|
16
|
+
'organizations.servers.php.versions.configs.cli.update',
|
|
17
|
+
'organizations.servers.php.versions.configs.pool.update',
|
|
18
|
+
'organizations.servers.php.versions.configs.fpm.show',
|
|
19
|
+
'organizations.servers.php.versions.configs.cli.show',
|
|
20
|
+
'organizations.servers.php.versions.configs.pool.show',
|
|
21
|
+
'organizations.servers.php.versions.index',
|
|
22
|
+
],
|
|
23
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
24
|
+
readOnly: false,
|
|
25
|
+
destructive: true,
|
|
26
|
+
idempotent: true,
|
|
27
|
+
async: true,
|
|
28
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
29
|
+
inputSchema: {
|
|
30
|
+
organization: organizationInput,
|
|
31
|
+
server: serverInput,
|
|
32
|
+
php_version: phpVersionRefInput,
|
|
33
|
+
config: phpConfigInput,
|
|
34
|
+
content: z.string().min(1).describe('The complete new configuration file.'),
|
|
35
|
+
user: poolUserInput,
|
|
36
|
+
...waitInput(120),
|
|
37
|
+
},
|
|
38
|
+
outputSchema: operationOutput,
|
|
39
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
40
|
+
const org = organization(args.organization);
|
|
41
|
+
const id = await resolvePhpVersionId(client, org, args.server, args.php_version, signal);
|
|
42
|
+
const path = `${phpVersionsPath(org, args.server)}/${encodeURIComponent(id)}/configs/${args.config}`;
|
|
43
|
+
const user = args.config === 'pool' ? args.user : undefined;
|
|
44
|
+
await client.put(path, { body: { config: args.content, ...(user ? { user } : {}) }, signal });
|
|
45
|
+
const action = `update the ${args.config} configuration`;
|
|
46
|
+
if (!args.wait)
|
|
47
|
+
return queued(action, CHECK_WITH);
|
|
48
|
+
const result = await waitFor({
|
|
49
|
+
poll: () => readPhpConfig(client, path, user, signal),
|
|
50
|
+
phase: (content) => (content.trimEnd() === args.content.trimEnd() ? 'completed' : 'pending'),
|
|
51
|
+
describe: () => `Waiting for the ${args.config} configuration to be applied`,
|
|
52
|
+
timeoutSeconds: args.timeout_seconds,
|
|
53
|
+
context: { sleep, progress },
|
|
54
|
+
});
|
|
55
|
+
return outcome(result, {
|
|
56
|
+
action,
|
|
57
|
+
checkWith: CHECK_WITH,
|
|
58
|
+
timeoutSeconds: args.timeout_seconds,
|
|
59
|
+
detail: result.status === 'in_progress' ? 'If PHP rejected the file, check forge_list_server_events.' : undefined,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { ToolInputError } from '../errors.js';
|
|
4
|
+
import { operationOutput, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
5
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
6
|
+
import { SERVER_NOT_FOUND_HINT, serverPath } from '../servers/shared.js';
|
|
7
|
+
import { readPhpSetting } from './settings.js';
|
|
8
|
+
const CHECK_WITH = 'forge_get_php_settings';
|
|
9
|
+
export const updatePhpLimits = defineTool({
|
|
10
|
+
name: 'forge_update_php_limits',
|
|
11
|
+
title: 'Update PHP limits',
|
|
12
|
+
description: 'Change the max upload size and/or max execution time of every PHP version on the server. PHP-FPM is reloaded.',
|
|
13
|
+
toolset: 'servers',
|
|
14
|
+
operations: [
|
|
15
|
+
'organizations.servers.php.max-upload-size.update',
|
|
16
|
+
'organizations.servers.php.max-execution-time.update',
|
|
17
|
+
'organizations.servers.php.max-upload-size.show',
|
|
18
|
+
'organizations.servers.php.max-execution-time.show',
|
|
19
|
+
],
|
|
20
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
21
|
+
readOnly: false,
|
|
22
|
+
destructive: false,
|
|
23
|
+
idempotent: true,
|
|
24
|
+
async: true,
|
|
25
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
26
|
+
inputSchema: {
|
|
27
|
+
organization: organizationInput,
|
|
28
|
+
server: serverInput,
|
|
29
|
+
max_upload_size: z.number().int().min(1).optional().describe('Max upload size in MB.'),
|
|
30
|
+
max_execution_time: z.number().int().min(1).optional().describe('Max execution time in seconds.'),
|
|
31
|
+
...waitInput(120),
|
|
32
|
+
},
|
|
33
|
+
outputSchema: operationOutput,
|
|
34
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
35
|
+
if (args.max_upload_size === undefined && args.max_execution_time === undefined) {
|
|
36
|
+
throw new ToolInputError('Pass `max_upload_size` and/or `max_execution_time`.');
|
|
37
|
+
}
|
|
38
|
+
const org = organization(args.organization);
|
|
39
|
+
const base = `${serverPath(org, args.server)}/php`;
|
|
40
|
+
if (args.max_upload_size !== undefined) {
|
|
41
|
+
await client.put(`${base}/max-upload-size`, { body: { max_upload_size: args.max_upload_size }, signal });
|
|
42
|
+
}
|
|
43
|
+
if (args.max_execution_time !== undefined) {
|
|
44
|
+
await client.put(`${base}/max-execution-time`, { body: { max_execution_time: args.max_execution_time }, signal });
|
|
45
|
+
}
|
|
46
|
+
const action = 'update the PHP limits';
|
|
47
|
+
if (!args.wait)
|
|
48
|
+
return queued(action, CHECK_WITH);
|
|
49
|
+
const result = await waitFor({
|
|
50
|
+
poll: async () => ({
|
|
51
|
+
upload: args.max_upload_size === undefined ? undefined : (await readPhpSetting(client, org, args.server, 'max-upload-size', signal)).max_upload_size,
|
|
52
|
+
execution: args.max_execution_time === undefined ? undefined : (await readPhpSetting(client, org, args.server, 'max-execution-time', signal)).max_execution_time,
|
|
53
|
+
}),
|
|
54
|
+
phase: (current) => (args.max_upload_size === undefined || current.upload === args.max_upload_size) &&
|
|
55
|
+
(args.max_execution_time === undefined || current.execution === args.max_execution_time)
|
|
56
|
+
? 'completed'
|
|
57
|
+
: 'pending',
|
|
58
|
+
describe: () => 'Waiting for the PHP limits to be applied',
|
|
59
|
+
timeoutSeconds: args.timeout_seconds,
|
|
60
|
+
context: { sleep, progress },
|
|
61
|
+
});
|
|
62
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, queued } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
5
|
+
import { phpVersionRefInput, phpVersionsPath, resolvePhpVersionId } from './shared.js';
|
|
6
|
+
export const upgradePhpVersion = defineTool({
|
|
7
|
+
name: 'forge_upgrade_php_version',
|
|
8
|
+
title: 'Upgrade PHP to the latest patch release',
|
|
9
|
+
description: 'Update an installed PHP version to its latest patch release (e.g. 8.3.x). PHP-FPM restarts during the update.',
|
|
10
|
+
toolset: 'servers',
|
|
11
|
+
operations: ['organizations.servers.php.versions.update', 'organizations.servers.php.versions.index'],
|
|
12
|
+
permissions: ['server:manage-php', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: false,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
php_version: phpVersionRefInput,
|
|
22
|
+
},
|
|
23
|
+
outputSchema: operationOutput,
|
|
24
|
+
async handler(args, { client, organization, signal }) {
|
|
25
|
+
const org = organization(args.organization);
|
|
26
|
+
const id = await resolvePhpVersionId(client, org, args.server, args.php_version, signal);
|
|
27
|
+
await client.put(`${phpVersionsPath(org, args.server)}/${encodeURIComponent(id)}`, { signal });
|
|
28
|
+
// The patch level is not exposed by the API: the server events record the outcome.
|
|
29
|
+
return queued(`upgrade PHP ${args.php_version}`, 'forge_list_server_events', `(server ${args.server})`);
|
|
30
|
+
},
|
|
31
|
+
});
|
package/dist/tools/registry.js
CHANGED
|
@@ -3,6 +3,15 @@ import { deleteCertificate } from './certificates/delete-certificate.js';
|
|
|
3
3
|
import { getCertificate } from './certificates/get-certificate.js';
|
|
4
4
|
import { listCertificates } from './certificates/list-certificates.js';
|
|
5
5
|
import { runCertificateAction } from './certificates/run-certificate-action.js';
|
|
6
|
+
import { createDatabase } from './databases/create-database.js';
|
|
7
|
+
import { createDatabaseUser } from './databases/create-database-user.js';
|
|
8
|
+
import { deleteDatabase } from './databases/delete-database.js';
|
|
9
|
+
import { deleteDatabaseUser } from './databases/delete-database-user.js';
|
|
10
|
+
import { listDatabaseUsers } from './databases/list-database-users.js';
|
|
11
|
+
import { listDatabases } from './databases/list-databases.js';
|
|
12
|
+
import { syncDatabases } from './databases/sync-databases.js';
|
|
13
|
+
import { updateDatabaseRootPassword } from './databases/update-database-root-password.js';
|
|
14
|
+
import { updateDatabaseUser } from './databases/update-database-user.js';
|
|
6
15
|
import { createDeployKey } from './deployments/create-deploy-key.js';
|
|
7
16
|
import { createDeploymentWebhook } from './deployments/create-deployment-webhook.js';
|
|
8
17
|
import { deleteDeployKey } from './deployments/delete-deploy-key.js';
|
|
@@ -28,6 +37,16 @@ import { updateDomain } from './domains/update-domain.js';
|
|
|
28
37
|
import { getServerEvent } from './events/get-server-event.js';
|
|
29
38
|
import { listServerEvents } from './events/list-server-events.js';
|
|
30
39
|
import { listOrganizations } from './organizations/list-organizations.js';
|
|
40
|
+
import { getPhpConfig } from './php/get-php-config.js';
|
|
41
|
+
import { getPhpSettings } from './php/get-php-settings.js';
|
|
42
|
+
import { installPhpVersion } from './php/install-php-version.js';
|
|
43
|
+
import { listPhpVersions } from './php/list-php-versions.js';
|
|
44
|
+
import { setDefaultPhpVersion } from './php/set-default-php-version.js';
|
|
45
|
+
import { setPhpOpcache } from './php/set-php-opcache.js';
|
|
46
|
+
import { uninstallPhpVersion } from './php/uninstall-php-version.js';
|
|
47
|
+
import { updatePhpConfig } from './php/update-php-config.js';
|
|
48
|
+
import { updatePhpLimits } from './php/update-php-limits.js';
|
|
49
|
+
import { upgradePhpVersion } from './php/upgrade-php-version.js';
|
|
31
50
|
import { archiveServer } from './servers/archive-server.js';
|
|
32
51
|
import { clearServerLog } from './servers/clear-server-log.js';
|
|
33
52
|
import { deleteServer } from './servers/delete-server.js';
|
|
@@ -106,6 +125,27 @@ export const ALL_TOOLS = [
|
|
|
106
125
|
archiveServer,
|
|
107
126
|
unarchiveServer,
|
|
108
127
|
deleteServer,
|
|
128
|
+
// servers: PHP
|
|
129
|
+
listPhpVersions,
|
|
130
|
+
getPhpSettings,
|
|
131
|
+
installPhpVersion,
|
|
132
|
+
upgradePhpVersion,
|
|
133
|
+
uninstallPhpVersion,
|
|
134
|
+
setDefaultPhpVersion,
|
|
135
|
+
updatePhpLimits,
|
|
136
|
+
setPhpOpcache,
|
|
137
|
+
getPhpConfig,
|
|
138
|
+
updatePhpConfig,
|
|
139
|
+
// databases
|
|
140
|
+
listDatabases,
|
|
141
|
+
createDatabase,
|
|
142
|
+
deleteDatabase,
|
|
143
|
+
syncDatabases,
|
|
144
|
+
listDatabaseUsers,
|
|
145
|
+
createDatabaseUser,
|
|
146
|
+
updateDatabaseUser,
|
|
147
|
+
deleteDatabaseUser,
|
|
148
|
+
updateDatabaseRootPassword,
|
|
109
149
|
// deployments
|
|
110
150
|
listDeployments,
|
|
111
151
|
getDeployment,
|