@webship.co/workspace-cpanel 0.0.0-stage → 1.0.0-alpha1

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 CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # @webship.co/workspace-cpanel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ cPanel for the [Webship Workspace](https://github.com/webship/workspace) dashboard: the **CPANEL**
4
+ menu on every site row, a live-site chip on the project card, "Link to cPanel" when a new folder is
5
+ made, and the `cmd-tools-cpanel.sh` command in every site workspace. A provider of
6
+ [`@webship.co/workspace-hosting`](https://github.com/webship/workspace-hosting), which draws the menu,
7
+ the environments modal, the get-database-and-files flow and the New-folder section for every host.
8
+
9
+ cPanel has no CLI of its own, so the dashboard talks to the UAPI over HTTPS (port 2083) with an API token per account, and the pulls, the deploy and the log go over SSH (`ssh`, `rsync`, `drush sql:dump` in the remote docroot). The link is `user@host:/home/user/public_html`; the environments are the account's domains, each a live site.
10
+
11
+ ## Use it
12
+
13
+ ```bash
14
+ cd ~/workspace
15
+ bash core/scripts/plugins.sh --require github:webship/workspace-cpanel
16
+ bash core/scripts/plugins.sh --activate cpanel
17
+ bash core/scripts/plugins.sh --install cpanel
18
+ bash core/scripts/plugins.sh --configure cpanel # core/config/plugin.cpanel.settings.yml
19
+ ```
20
+
21
+ The same four steps are buttons on the dashboard's **Settings → Plugins** page.
22
+
23
+ ## The command
24
+
25
+ ```bash
26
+ cd ~/workspace/dev
27
+ bash cmd-tools-cpanel.sh <project> [verb]
28
+ ```
29
+
30
+ | Verb | What it does |
31
+ | --- | --- |
32
+ | `--link` <value> | Link the project to a cPanel account and site: user@host:/remote/docroot (the account user, the server host, the site's document root on it). |
33
+ | `--push` | Deploy: rsync the local docroot to the remote document root (excludes sites/default/files, settings.php, .git, .ddev; deletes what is gone locally). Outward-facing: asks before syncing. |
34
+ | `--environment` <value> | The domain for THIS run (an account can serve several). Default: the one remembered by --select. |
35
+ | `--environments` | List the account's domains (UAPI DomainInfo::list_domains) and disk usage. |
36
+ | `--select` <value> | Remember which domain this project works against (in .cpanel/workspace.yml, git-excluded). Checked against the account's domains. |
37
+ | `--pull-db` | Dump the site's database over SSH (drush sql:dump in the remote docroot) and import it into DDEV. REPLACES the local database — the current one is exported to backups/ first. Client data. |
38
+ | `--pull-files` | rsync the remote sites/default/files into the local docroot (adds and overwrites, never deletes). Client data. |
39
+ | `--logs` | Show the last 50 lines of the site's error_log over SSH. |
40
+ | `--no-safety-backup` | Skip exporting the local database before --pull-db replaces it. |
41
+ | `--force` | Allow --link to relink a project that already points at a different account. |
42
+
43
+ The link lives in the project's `.cpanel/workspace.yml` (git-excluded); the dashboard reads it to know
44
+ the project is linked and which environment it works against.
45
+
46
+ ## What the package holds
47
+
48
+ | Path | What it is |
49
+ | --- | --- |
50
+ | `src/index.ts` | `register(api)`: `registerHosting(api, cpanelProvider(api))` |
51
+ | `src/lib/provider.ts` | The cPanel provider: the credentials, the environments and their URLs, the verbs, the pulls, the console |
52
+ | `src/lib/link.ts` | The link file reader |
53
+ | `commands/cmd-tools-cpanel.sh` | The per-workspace command |
54
+ | `scripts/functions/fun-cpanel.sh` | The provider's bash hooks (its API) |
55
+ | `scripts/functions/fun-cpanel-tool.sh` | The family's dispatcher: link file, environment selection, safety export, status |
56
+ | `scripts/args/arg-cpanel.sh` | Its argparse |
57
+ | `scripts/host-install.sh` | The host side: the CLI and the credential check |
58
+ | `config/plugin.cpanel.settings.yml` | The settings template |
59
+
60
+ The `workspacePlugin` block in `package.json` is the manifest the installer reads.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Installed by the cPanel plugin (plugins.sh --install cpanel); __WORKSPACE__ is the workspace it was installed into.
4
+ #
5
+ # The local ⇄ cloud bridge for one project on cPanel: show its link and environments, link it,
6
+ # bring a database and files down into DDEV, deploy, read logs.
7
+
8
+ # Bootstrap.
9
+ # Find the workspace tooling from this script, so a fresh clone needs no setup.
10
+ WORKSPACE_SCRIPTS="${WORKSPACE_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
11
+ source ${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
12
+
13
+ # Load workspace settings and extra lists.
14
+ eval $(parse_yaml ${WORKSPACE_CONFIG}/workspace.__WORKSPACE__.settings.yml);
15
+
16
+ ARGPARSE_DESCRIPTION="Show, link, pull from or deploy a __WORKSPACE__ project on cPanel"
17
+ # The args file comes with the plugin: core/scripts/plugins/cpanel/ is where plugins.sh --install puts it.
18
+ source ${WORKSPACE_SCRIPTS}/plugins/cpanel/args/arg-cpanel.sh || exit 1 ;
19
+
20
+ shift $#;
21
+
22
+ cpanel_project_tool ;
@@ -0,0 +1,9 @@
1
+ # cPanel — core/config/plugin.cpanel.settings.yml
2
+ #
3
+ # How the dashboard reaches cPanel accounts: the UAPI over HTTPS with an API token (cPanel → Security → Manage API Tokens), one token per account. Which account a project belongs to is in its link (host and user); the token for that account is looked up here by user.
4
+ # A map of cPanel user → API token (cPanel → Security → Manage API Tokens), one per account the dashboard lists domains for. Example: {webshipco: XXXX}. Injected only into cPanel jobs, never printed.
5
+ api_tokens:
6
+ # webshipco: CHANGE_ME
7
+ CHANGE_ME_USER: CHANGE_ME
8
+ # The private key the pulls and deploys use to reach the accounts over SSH (authorized in cPanel → Security → SSH Access). Default: ~/.ssh/id_ed25519.
9
+ ssh_key: ~/.ssh/id_ed25519
@@ -0,0 +1,7 @@
1
+ # Labels for core/config/plugin.cpanel.settings.yml, merged into the Settings page.
2
+ api_tokens:
3
+ label: cPanel API tokens by user
4
+ info: 'A map of cPanel user → API token (cPanel → Security → Manage API Tokens), one per account the dashboard lists domains for. Example: a nested map, one user per line. Injected only into cPanel jobs, never printed.'
5
+ ssh_key:
6
+ label: SSH key for the pulls
7
+ info: 'The private key the pulls and deploys use to reach the accounts over SSH (authorized in cPanel → Security → SSH Access). Default: ~/.ssh/id_ed25519.'
@@ -0,0 +1,16 @@
1
+ /**
2
+ * cPanel for Webship Workspace.
3
+ *
4
+ * The CPANEL menu on every site row, a live-site chip, the New-folder link option and the
5
+ * `/cpanel-site/<workspace>/<project>` jump: all of it is the hosting family's surface
6
+ * (@webship.co/workspace-hosting). This package is the cPanel provider plus what the installer places
7
+ * (the per-workspace command, its bash functions, the settings template, the host install).
8
+ *
9
+ * Everything the dashboard is asked for goes through `api`; this package requires nothing from
10
+ * the dashboard's own files.
11
+ */
12
+ import type { PluginApi } from '@webship.co/workspace-plugin-api';
13
+ import { cpanelProvider } from './lib/provider';
14
+ export { cpanelProvider };
15
+ export { cpanelLinkOf } from './lib/link';
16
+ export declare function register(api: PluginApi): void;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * cPanel for Webship Workspace.
4
+ *
5
+ * The CPANEL menu on every site row, a live-site chip, the New-folder link option and the
6
+ * `/cpanel-site/<workspace>/<project>` jump: all of it is the hosting family's surface
7
+ * (@webship.co/workspace-hosting). This package is the cPanel provider plus what the installer places
8
+ * (the per-workspace command, its bash functions, the settings template, the host install).
9
+ *
10
+ * Everything the dashboard is asked for goes through `api`; this package requires nothing from
11
+ * the dashboard's own files.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.cpanelLinkOf = exports.cpanelProvider = void 0;
15
+ exports.register = register;
16
+ const workspace_hosting_1 = require("@webship.co/workspace-hosting");
17
+ const provider_1 = require("./lib/provider");
18
+ Object.defineProperty(exports, "cpanelProvider", { enumerable: true, get: function () { return provider_1.cpanelProvider; } });
19
+ var link_1 = require("./lib/link");
20
+ Object.defineProperty(exports, "cpanelLinkOf", { enumerable: true, get: function () { return link_1.cpanelLinkOf; } });
21
+ function register(api) {
22
+ (0, workspace_hosting_1.registerHosting)(api, (0, provider_1.cpanelProvider)(api));
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAUH,4BAEC;AATD,qEAAgE;AAEhE,6CAAgD;AAEvC,+FAFA,yBAAc,OAEA;AACvB,mCAA0C;AAAjC,oGAAA,YAAY,OAAA;AAErB,SAAgB,QAAQ,CAAC,GAAc;IACrC,IAAA,mCAAe,EAAC,GAAG,EAAE,IAAA,yBAAc,EAAC,GAAG,CAAC,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether a project is linked to cPanel, and to what: the `.cpanel/workspace.yml` file that
3
+ * `cmd-tools-cpanel.sh --link` writes (git-excluded), plus the branch the local code is on.
4
+ */
5
+ import type { HostingLink } from '@webship.co/workspace-hosting';
6
+ export interface cPanelLink extends HostingLink {
7
+ /** Every key of the link file, for the provider's own needs (region, resource group, host…). */
8
+ fields: Record<string, string>;
9
+ }
10
+ export declare function readLinkFile(projectDir: string): Record<string, string> | null;
11
+ export declare function cpanelLinkOf(projectDir: string): cPanelLink | null;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Whether a project is linked to cPanel, and to what: the `.cpanel/workspace.yml` file that
4
+ * `cmd-tools-cpanel.sh --link` writes (git-excluded), plus the branch the local code is on.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.readLinkFile = readLinkFile;
11
+ exports.cpanelLinkOf = cpanelLinkOf;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ function readLinkFile(projectDir) {
15
+ let text;
16
+ try {
17
+ text = fs_1.default.readFileSync(path_1.default.join(projectDir, '.cpanel', 'workspace.yml'), 'utf8');
18
+ }
19
+ catch (_) {
20
+ return null;
21
+ }
22
+ const fields = {};
23
+ for (const line of text.split('\n')) {
24
+ const m = line.match(/^([a-z_]+):\s*(.*?)\s*$/);
25
+ if (m)
26
+ fields[m[1]] = m[2].replace(/^["']|["']$/g, '');
27
+ }
28
+ return fields;
29
+ }
30
+ function cpanelLinkOf(projectDir) {
31
+ const fields = readLinkFile(projectDir);
32
+ if (!fields || !fields.id)
33
+ return null;
34
+ let branch = null;
35
+ try {
36
+ const head = fs_1.default.readFileSync(path_1.default.join(projectDir, '.git', 'HEAD'), 'utf8').trim();
37
+ branch = (head.match(/^ref: refs\/heads\/(.+)$/) || [])[1] || null;
38
+ }
39
+ catch (_) { /* not a git clone */ }
40
+ return { id: fields.id, env: fields.env || null, branch, fields };
41
+ }
42
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../../../src/lib/link.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;AAWH,oCASC;AAED,oCASC;AA7BD,4CAAoB;AACpB,gDAAwB;AAQxB,SAAgB,YAAY,CAAC,UAAkB;IAC7C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QAAC,IAAI,GAAG,YAAE,CAAC,YAAY,CAAC,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IACrH,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAChD,IAAI,CAAC;YAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,YAAY,CAAC,UAAkB;IAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAE,CAAC,YAAY,CAAC,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACnF,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACrE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACrC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACpE,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * cPanel, as a hosting provider.
3
+ *
4
+ * The link is `.cpanel/workspace.yml` (user, host, remote docroot); the account's domains come from
5
+ * the UAPI over HTTPS with the account's API token from plugin.cpanel.settings.yml; the pulls, the
6
+ * deploy and the log go over SSH through cmd-tools-cpanel.sh in a screen session.
7
+ */
8
+ import type { PluginApi } from '@webship.co/workspace-plugin-api';
9
+ import type { HostingProvider } from '@webship.co/workspace-hosting';
10
+ export declare function cpanelProvider(api: PluginApi): HostingProvider;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * cPanel, as a hosting provider.
4
+ *
5
+ * The link is `.cpanel/workspace.yml` (user, host, remote docroot); the account's domains come from
6
+ * the UAPI over HTTPS with the account's API token from plugin.cpanel.settings.yml; the pulls, the
7
+ * deploy and the log go over SSH through cmd-tools-cpanel.sh in a screen session.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.cpanelProvider = cpanelProvider;
11
+ const workspace_hosting_1 = require("@webship.co/workspace-hosting");
12
+ const link_1 = require("./link");
13
+ function cpanelProvider(api) {
14
+ const settings = () => api.settings.read() || {};
15
+ /** The token of one account: `api_tokens` is a map by user. */
16
+ const tokenFor = (user) => {
17
+ const map = settings().api_tokens;
18
+ if (map && typeof map === 'object')
19
+ return (0, workspace_hosting_1.configured)(map[user]);
20
+ return null;
21
+ };
22
+ const jobEnv = () => {
23
+ const map = settings().api_tokens;
24
+ const any = map && typeof map === 'object' && Object.values(map).some((v) => (0, workspace_hosting_1.configured)(v));
25
+ return any ? { ...process.env, CPANEL_API_TOKENS: JSON.stringify(map), CPANEL_SSH_KEY: String(settings().ssh_key || '~/.ssh/id_ed25519') } : null;
26
+ };
27
+ async function environments(link, lib, env) {
28
+ const fields = link.fields || {};
29
+ const user = fields.user;
30
+ const host = fields.host;
31
+ const id = link.id;
32
+ const token = user ? tokenFor(user) : null;
33
+ if (!token)
34
+ return { id, rows: [], why: `no token for ${user || 'the account'}` };
35
+ const out = await lib.run('curl', ['-sS', '-k', '-m', '30', '-H', `Authorization: cpanel ${user}:${token}`, `https://${host}:2083/execute/DomainInfo/list_domains`], process.env.HOME, { timeoutMs: 40 * 1000, env });
36
+ if (!out.ok)
37
+ return { id, rows: [], why: 'list failed' };
38
+ let data;
39
+ try {
40
+ data = JSON.parse(String(out.stdout)).data || {};
41
+ }
42
+ catch (_) {
43
+ return { id, rows: [], why: 'list failed' };
44
+ }
45
+ const rows = [];
46
+ if (data.main_domain)
47
+ rows.push({ title: `${data.main_domain} (main)`, envId: data.main_domain, status: 'active', url: `https://${data.main_domain}` });
48
+ for (const d of [...(data.addon_domains || []), ...(data.sub_domains || [])])
49
+ rows.push({ title: d, envId: d, status: 'active', url: `https://${d}` });
50
+ for (const d of data.parked_domains || [])
51
+ rows.push({ title: `${d} (alias)`, envId: d, status: 'alias' });
52
+ return { id, rows };
53
+ }
54
+ return {
55
+ id: 'cpanel',
56
+ label: 'CPANEL',
57
+ title: 'cPanel',
58
+ script: 'cmd-tools-cpanel.sh',
59
+ detect: (projectDir) => (0, link_1.cpanelLinkOf)(projectDir),
60
+ linkHint: (project) => `bash cmd-tools-cpanel.sh ${project} --link user@host:/home/user/public_html`,
61
+ consoleUrl: (link) => { const h = link.fields && link.fields.host; return h ? `https://${h}:2083/` : null; },
62
+ jobEnv,
63
+ tokenHint: 'api_tokens (by cPanel user) in core/config/plugin.cpanel.settings.yml',
64
+ environments,
65
+ tools: {
66
+ status: { verb: 'cpanel', label: 'Link and domains', icon: 'info', args: [] },
67
+ logs: { verb: 'cpanel-logs', label: 'Last 50 lines of error_log', icon: 'file-text', args: ['--logs'] },
68
+ deploy: { verb: 'cpanel-push', label: 'Deploy with rsync…', icon: 'push', args: ['--push'] },
69
+ },
70
+ pull: {
71
+ db: true, files: true,
72
+ dbArgs: (e) => ['--environment', e, '--pull-db'],
73
+ filesArgs: (e) => ['--environment', e, '--pull-files'],
74
+ noSafetyBackupArg: '--no-safety-backup',
75
+ },
76
+ folder: {
77
+ idPattern: /^[a-z0-9_.-]+@[a-z0-9.-]+:\/.+$/i,
78
+ idPlaceholder: 'webshipco@server.example.com:/home/webshipco/public_html',
79
+ idHelp: 'A cPanel link is user@host:/remote/docroot — the account user, the server, and the site\'s document root on it.',
80
+ },
81
+ menuNote: 'The pulls, the deploy and the log go over SSH with the key in the settings; the API token only lists domains',
82
+ };
83
+ }
84
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/lib/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ Feature: The cPanel plugin on a project row
2
+ Once the plugin is activated and installed, every site row carries the CPANEL menu, and the Plugins
3
+ page lists the plugin with its four steps.
4
+
5
+ Scenario: A site row offers the CPANEL menu, captioned with its link state
6
+ Given I am on "/dev"
7
+ Then the element ".act-menu-cpanel" should be displayed
8
+ And I should see "CPANEL"
9
+ And the element ".act-menu-cpanel + .act-status" should be displayed
10
+
11
+ Scenario: An unlinked project's menu opens and says how to link it
12
+ Given I am on "/dev"
13
+ And I click on the element ".project-row .act-menu-cpanel button"
14
+ Then ".uk-dropdown.uk-open" should be visible
15
+ And I should see "Not linked to cPanel"
16
+
17
+ Scenario: The plugin is listed on the Plugins page
18
+ Given I am on "/settings/plugins"
19
+ Then I should see "cPanel"
20
+ And I should see "@webship.co/workspace-cpanel"
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@webship.co/workspace-cpanel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-alpha1",
4
+ "description": "cPanel hosting for Webship Workspace: the CPANEL menu on every site row, the account's domains as live sites, get database and files over SSH, deploy with rsync, the error log, the cPanel console. A provider of @webship.co/workspace-hosting.",
5
+ "main": "dist/src/index.js",
6
+ "types": "dist/src/index.d.ts",
7
+ "license": "ISC",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/webship/workspace-cpanel.git"
11
+ },
12
+ "files": [
13
+ "dist/src",
14
+ "commands",
15
+ "config",
16
+ "features",
17
+ "scripts",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "prepare": "npm run build",
23
+ "test": "npm run build && node --test dist/tests/*.test.js",
24
+ "lint": "tsc -p tsconfig.json --noEmit"
25
+ },
26
+ "workspacePlugin": {
27
+ "name": "cpanel",
28
+ "label": "cPanel",
29
+ "settingsFile": "plugin.cpanel.settings.yml",
30
+ "settingsTemplate": "config/plugin.cpanel.settings.yml",
31
+ "labels": "config/settings.labels.yml",
32
+ "legacySettingsKey": "cpanel",
33
+ "typeActions": {
34
+ "cpanel": [
35
+ "sites"
36
+ ]
37
+ },
38
+ "commands": {
39
+ "types": [
40
+ "sites"
41
+ ],
42
+ "workspaces": [
43
+ "dev",
44
+ "test",
45
+ "demos",
46
+ "sandboxes",
47
+ "projects"
48
+ ],
49
+ "files": [
50
+ "commands/cmd-tools-cpanel.sh"
51
+ ]
52
+ },
53
+ "scripts": {
54
+ "functions": [
55
+ "scripts/functions/fun-cpanel.sh",
56
+ "scripts/functions/fun-cpanel-tool.sh"
57
+ ],
58
+ "args": [
59
+ "scripts/args/arg-cpanel.sh"
60
+ ],
61
+ "hostInstall": "scripts/host-install.sh"
62
+ },
63
+ "features": [
64
+ "features/cpanel.feature"
65
+ ]
66
+ },
67
+ "dependencies": {
68
+ "@webship.co/workspace-hosting": "^1.0.0-alpha1"
69
+ },
70
+ "devDependencies": {
71
+ "@types/node": "^24.0.0",
72
+ "@webship.co/workspace-plugin-api": "^1.0.0-alpha1",
73
+ "typescript": "^5.9.0"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+
3
+ argparse "$@" <<ARGEOF || exit 1
4
+ parser.add_argument('PROJECT_NAME',
5
+ help='The project directory in this workspace.')
6
+ parser.add_argument('-l', '--link',
7
+ default="_none_",
8
+ help='Link the project to a cPanel account and site: user@host:/remote/docroot (the account user, the server host, the site's document root on it).')
9
+ parser.add_argument('-p', '--push',
10
+ action='store_true',
11
+ default=False,
12
+ help='Deploy: rsync the local docroot to the remote document root (excludes sites/default/files, settings.php, .git, .ddev; deletes what is gone locally). Outward-facing: asks before syncing.')
13
+ parser.add_argument('-e', '--environment',
14
+ default="_none_",
15
+ help='The domain for THIS run (an account can serve several). Default: the one remembered by --select.')
16
+ parser.add_argument('-E', '--environments',
17
+ action='store_true',
18
+ default=False,
19
+ help='List the account's domains (UAPI DomainInfo::list_domains) and disk usage.')
20
+ parser.add_argument('-s', '--select',
21
+ default="_none_",
22
+ help='Remember which domain this project works against (in .cpanel/workspace.yml, git-excluded). Checked against the account's domains.')
23
+ parser.add_argument('-D', '--pull-db',
24
+ action='store_true',
25
+ default=False,
26
+ help='Dump the site's database over SSH (drush sql:dump in the remote docroot) and import it into DDEV. REPLACES the local database — the current one is exported to backups/ first. Client data.')
27
+ parser.add_argument('-F', '--pull-files',
28
+ action='store_true',
29
+ default=False,
30
+ help='rsync the remote sites/default/files into the local docroot (adds and overwrites, never deletes). Client data.')
31
+ parser.add_argument('-g', '--logs',
32
+ action='store_true',
33
+ default=False,
34
+ help='Show the last 50 lines of the site's error_log over SSH.')
35
+ parser.add_argument('-B', '--no-safety-backup',
36
+ action='store_true',
37
+ default=False,
38
+ help='Skip exporting the local database before --pull-db replaces it.')
39
+ parser.add_argument('-f', '--force',
40
+ action='store_true',
41
+ default=False,
42
+ help='Allow --link to relink a project that already points at a different account.')
43
+ ARGEOF
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # The cPanel tool's dispatcher: generated by the hosting family's scaffolder, the same for every
4
+ # provider. The provider's own hooks live in fun-cpanel.sh:
5
+ # ensure_cpanel the CLI and credentials are usable, or say how
6
+ # cpanel_do_link <project_path> <id> link the project (writes the link file through cpanel_link_set)
7
+ # cpanel_list_environments <id> one environment id per line (for --select's check)
8
+ # cpanel_do_environments <id> print the environments for a person
9
+ # cpanel_pull_db <project_path> <id> <env> dump the environment's database to stdout path given in $1… (see fun-cpanel.sh)
10
+ # cpanel_pull_files <project_path> <id> <env>
11
+ # cpanel_do_push <project_path> <id> <env> deploy
12
+ # cpanel_do_logs <project_path> <id> <env>
13
+ # cpanel_do_activities <project_path> <id>
14
+ # cpanel_status <project_path> <id> the closing status block
15
+
16
+ CPANEL_LINK_FILE=".cpanel/workspace.yml";
17
+
18
+ # One key of the link file, or nothing.
19
+ function cpanel_link_get() {
20
+ local project_path="$1" key="$2";
21
+ [ -f "${project_path}/${CPANEL_LINK_FILE}" ] || return 0 ;
22
+ grep -E "^${key}:" "${project_path}/${CPANEL_LINK_FILE}" | head -1 | sed -E 's/^[a-z_]+:[[:space:]]*//; s/^"//; s/"$//' ;
23
+ }
24
+
25
+ # Set one key of the link file, creating it (git-excluded: it is this machine's, not the repo's).
26
+ function cpanel_link_set() {
27
+ local project_path="$1" key="$2" value="$3" file;
28
+ file="${project_path}/${CPANEL_LINK_FILE}";
29
+ mkdir -p "$(dirname "${file}")" ;
30
+ touch "${file}" ;
31
+ if grep -qE "^${key}:" "${file}" ; then
32
+ sed -i -E "s|^${key}:.*|${key}: ${value}|" "${file}" ;
33
+ else
34
+ printf '%s: %s\n' "${key}" "${value}" >> "${file}" ;
35
+ fi
36
+ if [ -d "${project_path}/.git" ] && ! grep -qx "/.cpanel/" "${project_path}/.git/info/exclude" 2>/dev/null ; then
37
+ mkdir -p "${project_path}/.git/info" && printf '/.cpanel/\n' >> "${project_path}/.git/info/exclude" ;
38
+ fi
39
+ }
40
+
41
+ function cpanel_linked_id() { cpanel_link_get "$1" id ; }
42
+ function cpanel_selected_env() { cpanel_link_get "$1" env ; }
43
+
44
+ # Which environment a run acts on: --environment for this run, else the remembered selection, else
45
+ # nothing. Deliberately NOT defaulting to production: pulling live data because nobody said
46
+ # otherwise is exactly the accident worth preventing.
47
+ function cpanel_target_env() {
48
+ if [ ! "${ENVIRONMENT:-_none_}" == '_none_' ] ; then printf '%s' "${ENVIRONMENT}"; return 0 ; fi
49
+ cpanel_selected_env "$1";
50
+ }
51
+
52
+ # Where this project serves from: Drupal's two conventions, plus the flat case.
53
+ function cpanel_docroot() {
54
+ for candidate in web docroot ; do
55
+ [ -d "$1/${candidate}" ] && { printf '%s' "${candidate}"; return 0 ; }
56
+ done
57
+ printf '%s' "";
58
+ }
59
+
60
+ # The local database is replaced by a pull, so it is exported first unless --no-safety-backup.
61
+ function cpanel_safety_export() {
62
+ local project_path="$1" safety="$2";
63
+ if [ "${NO_SAFETY_BACKUP:-no}" == 'yes' ] ; then
64
+ echo "Skipping the local database export, as asked. The current local data will be lost.";
65
+ else
66
+ echo "Exporting the current local database first.";
67
+ (cd "${project_path}" && ddev export-db --file="${safety}") \
68
+ || echo " (no local database to export yet — continuing)";
69
+ fi
70
+ }
71
+
72
+ function cpanel_import_dump() {
73
+ local project_path="$1" dump="$2";
74
+ echo "Importing it into DDEV.";
75
+ (cd "${project_path}" && ddev start -y > /dev/null && ddev import-db --file="${dump}") || return 1 ;
76
+ echo " Drupal sites usually need: ddev drush updb -y && ddev drush cr";
77
+ }
78
+
79
+ function cpanel_project_tool() {
80
+ local project_path="${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}";
81
+ local linked_id="";
82
+
83
+ if [ ! -d "${project_path}" ]; then
84
+ echo "No such project: ${project_path}";
85
+ return 1 ;
86
+ fi
87
+ cd "${project_path}" ;
88
+ linked_id="$(cpanel_linked_id "${project_path}")" ;
89
+
90
+ if [ ! "${LINK:-_none_}" == '_none_' ] ; then
91
+ ensure_cpanel || return 1 ;
92
+ if [ -n "${linked_id}" ] && [ ! "${linked_id}" == "${LINK}" ] && [ ! "${FORCE:-no}" == 'yes' ] ; then
93
+ echo "${doc_name}/${PROJECT_NAME} is already linked to cPanel ${linked_id}." ;
94
+ echo "Re-run with --force to relink it to ${LINK}." ;
95
+ return 1 ;
96
+ fi
97
+ cpanel_do_link "${project_path}" "${LINK}" || return 1 ;
98
+ linked_id="$(cpanel_linked_id "${project_path}")" ;
99
+ echo "Linked ${doc_name}/${PROJECT_NAME} → ${linked_id}." ;
100
+ fi
101
+
102
+ local needs_link="";
103
+ for flag in "${ENVIRONMENTS:-no}" "${PULL_DB:-no}" "${PULL_FILES:-no}" "${PUSH:-no}" "${LOGS:-no}" "${ACTIVITIES:-no}" ; do
104
+ [ "${flag}" == 'yes' ] && needs_link=yes ;
105
+ done
106
+ [ ! "${SELECT:-_none_}" == '_none_' ] && needs_link=yes ;
107
+ if [ -n "${needs_link}" ] ; then
108
+ if [ -z "${linked_id}" ] ; then
109
+ echo "Not linked to cPanel — run with --link <id> first." ;
110
+ return 1 ;
111
+ fi
112
+ ensure_cpanel || return 1 ;
113
+ fi
114
+
115
+ if [ "${ENVIRONMENTS:-no}" == 'yes' ] ; then
116
+ cpanel_do_environments "${project_path}" "${linked_id}" || return 1 ;
117
+ echo "";
118
+ echo " Select one: bash cmd-tools-cpanel.sh ${PROJECT_NAME} --select <environment>";
119
+ fi
120
+
121
+ if [ ! "${SELECT:-_none_}" == '_none_' ] ; then
122
+ # Checked against the real list rather than taken on trust: a typo would otherwise be
123
+ # discovered by a pull that dumps the wrong environment, or none.
124
+ if ! cpanel_list_environments "${project_path}" "${linked_id}" 2>/dev/null | grep -qx "${SELECT}" ; then
125
+ echo "cPanel ${linked_id} has no environment called '${SELECT}'." ;
126
+ echo " List them: bash cmd-tools-cpanel.sh ${PROJECT_NAME} --environments" ;
127
+ return 1 ;
128
+ fi
129
+ cpanel_link_set "${project_path}" env "${SELECT}" ;
130
+ echo "${doc_name}/${PROJECT_NAME} now works against the ${SELECT} environment." ;
131
+ fi
132
+
133
+ if [ "${PULL_DB:-no}" == 'yes' ] || [ "${PULL_FILES:-no}" == 'yes' ] || [ "${PUSH:-no}" == 'yes' ] ; then
134
+ local target_env;
135
+ target_env="$(cpanel_target_env "${project_path}")";
136
+ if [ -z "${target_env}" ] ; then
137
+ echo "No environment selected. Pick one first — a pull or a deploy must never guess which environment it targets:" ;
138
+ echo " bash cmd-tools-cpanel.sh ${PROJECT_NAME} --environments" ;
139
+ echo " bash cmd-tools-cpanel.sh ${PROJECT_NAME} --select <environment>" ;
140
+ echo " (or name it for this run: --environment <environment>)" ;
141
+ return 1 ;
142
+ fi
143
+ [ "${PULL_DB:-no}" == 'yes' ] && { cpanel_pull_db "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
144
+ [ "${PULL_FILES:-no}" == 'yes' ] && { cpanel_pull_files "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
145
+ [ "${PUSH:-no}" == 'yes' ] && { cpanel_do_push "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
146
+ fi
147
+
148
+ if [ "${LOGS:-no}" == 'yes' ] ; then
149
+ cpanel_do_logs "${project_path}" "${linked_id}" "$(cpanel_target_env "${project_path}")" || true ;
150
+ fi
151
+ if [ "${ACTIVITIES:-no}" == 'yes' ] ; then
152
+ cpanel_do_activities "${project_path}" "${linked_id}" || true ;
153
+ fi
154
+
155
+ # Default (and after any verb): status.
156
+ echo "";
157
+ echo "*---------------------------------------------------------------------*";
158
+ echo " ${doc_name}/${PROJECT_NAME} on cPanel";
159
+ if [ -z "${linked_id}" ]; then
160
+ echo " not linked. Link it with:";
161
+ echo " bash cmd-tools-cpanel.sh ${PROJECT_NAME} --link <id>";
162
+ cpanel_status "${project_path}" "" ;
163
+ else
164
+ echo " linked: ${linked_id}";
165
+ local selected;
166
+ selected="$(cpanel_selected_env "${project_path}")";
167
+ echo " env: ${selected:-none selected — --environments then --select <name>}";
168
+ cpanel_status "${project_path}" "${linked_id}" ;
169
+ fi
170
+ echo "*---------------------------------------------------------------------*";
171
+
172
+ cd "${WORKSPACE_ROOT}/${doc_name}";
173
+ }
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # cPanel accounts: the UAPI over HTTPS (port 2083, an API token per account) for the domains, and
4
+ # SSH for everything that touches the site (the database dump, the files, the deploy, the log). The
5
+ # hooks the hosting dispatcher (fun-cpanel-tool.sh) calls for one project directory. Everything
6
+ # defaults to read-only; --link, --push, --pull-db and --pull-files each demand their flag.
7
+
8
+ # The dashboard passes the tokens map as CPANEL_API_TOKENS (JSON by user) and the key as
9
+ # CPANEL_SSH_KEY; on the host they come from core/config/plugin.cpanel.settings.yml.
10
+ function cpanel_token_for() {
11
+ local user="$1";
12
+ if [ -n "${CPANEL_API_TOKENS:-}" ] ; then
13
+ printf '%s' "${CPANEL_API_TOKENS}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1],""))' "${user}" 2>/dev/null ;
14
+ else
15
+ # Either `api_tokens: {user: token}` on one line or a nested map under `api_tokens:`.
16
+ python3 - "${WORKSPACE_CONFIG}/plugin.cpanel.settings.yml" "${user}" <<'PY' 2>/dev/null
17
+ import re,sys,io
18
+ s=io.open(sys.argv[1],encoding='utf-8').read()
19
+ want=sys.argv[2]
20
+ m=re.search(r'^api_tokens:[ \t]*(\{.*\})[ \t]*$', s, re.M)
21
+ if m:
22
+ for pair in m.group(1).strip('{}').split(','):
23
+ if ':' in pair:
24
+ k,v=pair.split(':',1)
25
+ if k.strip().strip('"\'')==want: print(v.strip().strip('"\'')); break
26
+ else:
27
+ on=False
28
+ for line in s.split('\n'):
29
+ if re.match(r'^api_tokens:\s*$', line): on=True; continue
30
+ if on:
31
+ if line and not line.startswith(' '): break
32
+ mm=re.match(r'^\s+([^#\s:]+):\s*(.*?)\s*$', line)
33
+ if mm and mm.group(1)==want: print(mm.group(2).strip('"\'')); break
34
+ PY
35
+ fi
36
+ }
37
+
38
+ function cpanel_ssh_key() {
39
+ local k="${CPANEL_SSH_KEY:-${ssh_key:-~/.ssh/id_ed25519}}";
40
+ printf '%s' "${k/#\~/${HOME}}";
41
+ }
42
+
43
+ function ensure_cpanel() {
44
+ for tool in ssh rsync curl python3 ; do
45
+ command -v "${tool}" > /dev/null 2>&1 || { echo "${tool} is not installed, and the cPanel tool needs it."; return 1; } ;
46
+ done
47
+ return 0 ;
48
+ }
49
+
50
+ # The link is user@host:/remote/docroot, kept as three keys; the id the dashboard shows is user@host.
51
+ function cpanel_do_link() {
52
+ local project_path="$1" spec="$2" user host remote;
53
+ [[ "${spec}" =~ ^([^@]+)@([^:]+):(/.+)$ ]] || { echo "A cPanel link is user@host:/remote/docroot, got: ${spec}"; return 1; } ;
54
+ user="${BASH_REMATCH[1]}"; host="${BASH_REMATCH[2]}"; remote="${BASH_REMATCH[3]}";
55
+ cpanel_link_set "${project_path}" id "${user}@${host}" ;
56
+ cpanel_link_set "${project_path}" user "${user}" ;
57
+ cpanel_link_set "${project_path}" host "${host}" ;
58
+ cpanel_link_set "${project_path}" remote "${remote}" ;
59
+ if ssh -i "$(cpanel_ssh_key)" -o BatchMode=yes -o ConnectTimeout=10 "${user}@${host}" "test -d '${remote}'" 2>/dev/null ; then
60
+ echo " SSH reaches ${user}@${host} and ${remote} exists." ;
61
+ else
62
+ echo " SSH could not reach ${user}@${host} with $(cpanel_ssh_key) (or ${remote} is missing): authorize the key in cPanel → Security → SSH Access." ;
63
+ fi
64
+ }
65
+
66
+ function cpanel_uapi() {
67
+ local project_path="$1" call="$2" user host token;
68
+ user="$(cpanel_link_get "${project_path}" user)"; host="$(cpanel_link_get "${project_path}" host)";
69
+ token="$(cpanel_token_for "${user}")";
70
+ [ -z "${token}" ] && { echo "No API token for cPanel user ${user} in plugin.cpanel.settings.yml (api_tokens)." >&2; return 1; } ;
71
+ curl -sS -k -m 30 -H "Authorization: cpanel ${user}:${token}" "https://${host}:2083/execute/${call}" ;
72
+ }
73
+
74
+ function cpanel_list_environments() {
75
+ cpanel_uapi "$1" DomainInfo/list_domains | python3 -c 'import json,sys
76
+ d=json.load(sys.stdin).get("data",{})
77
+ for x in [d.get("main_domain")]+d.get("addon_domains",[])+d.get("sub_domains",[]):
78
+ if x: print(x)' 2>/dev/null ;
79
+ }
80
+
81
+ function cpanel_do_environments() {
82
+ local project_path="$1";
83
+ cpanel_uapi "${project_path}" DomainInfo/list_domains | python3 -c 'import json,sys
84
+ d=json.load(sys.stdin).get("data",{})
85
+ print(" %-40s %s" % (d.get("main_domain",""), "main https://"+d.get("main_domain","")))
86
+ for x in d.get("addon_domains",[]): print(" %-40s %s" % (x, "addon https://"+x))
87
+ for x in d.get("sub_domains",[]): print(" %-40s %s" % (x, "sub https://"+x))
88
+ for x in d.get("parked_domains",[]): print(" %-40s %s" % (x, "alias"))' || return 1 ;
89
+ cpanel_uapi "${project_path}" Quota/get_quota_info 2>/dev/null | python3 -c 'import json,sys
90
+ d=json.load(sys.stdin).get("data",{})
91
+ print(" disk: %s MB of %s MB" % (d.get("megabytes_used","?"), d.get("megabyte_limit","?")))' 2>/dev/null ;
92
+ }
93
+
94
+ function cpanel_ssh() {
95
+ local project_path="$1"; shift;
96
+ ssh -i "$(cpanel_ssh_key)" -o BatchMode=yes -o ConnectTimeout=15 "$(cpanel_link_get "${project_path}" id)" "$@" ;
97
+ }
98
+
99
+ function cpanel_pull_db() {
100
+ local project_path="$1" acct="$2" env="$3" stamp out dump safety remote;
101
+ remote="$(cpanel_link_get "${project_path}" remote)";
102
+ stamp="$(date +%Y-%m-%d-%H%M%S)";
103
+ out="${backups}/${doc_name}/${PROJECT_NAME}";
104
+ mkdir -p "${out}" ;
105
+ dump="${out}/${PROJECT_NAME}-cpanel-${env}-${stamp}.sql.gz";
106
+ safety="${out}/${PROJECT_NAME}-before-cpanel-pull-${stamp}.sql.gz";
107
+ echo "";
108
+ echo "Pulling the database of ${env} on ${acct} (drush sql:dump in ${remote}).";
109
+ echo " This is client data: it lands on this machine and in ${out}.";
110
+ echo "";
111
+ cpanel_safety_export "${project_path}" "${safety}" ;
112
+ echo "Dumping over SSH.";
113
+ cpanel_ssh "${project_path}" "cd '${remote}' && (command -v drush >/dev/null 2>&1 && drush sql:dump --gzip || ../vendor/bin/drush sql:dump --gzip)" > "${dump}" || return 1 ;
114
+ [ -s "${dump}" ] || { echo "The dump is empty — is drush on the server (vendor/bin/drush beside the docroot)?"; return 1; } ;
115
+ cpanel_import_dump "${project_path}" "${dump}" || return 1 ;
116
+ echo "";
117
+ echo " Imported ${env} into ${doc_name}/${PROJECT_NAME}.";
118
+ echo " pulled dump: ${dump}";
119
+ echo " local backup: ${safety}";
120
+ }
121
+
122
+ function cpanel_pull_files() {
123
+ local project_path="$1" acct="$2" env="$3" docroot remote;
124
+ remote="$(cpanel_link_get "${project_path}" remote)";
125
+ docroot="$(cpanel_docroot "${project_path}")";
126
+ [ -z "${docroot}" ] && { echo "No web/ or docroot/ in ${doc_name}/${PROJECT_NAME} — nothing to pull files into."; return 1; } ;
127
+ echo "";
128
+ echo "rsync ${acct}:${remote}/sites/default/files → ${docroot}/sites/default/files (adds and overwrites, never deletes).";
129
+ mkdir -p "${project_path}/${docroot}/sites/default/files" ;
130
+ rsync -az --info=progress2 -e "ssh -i $(cpanel_ssh_key) -o BatchMode=yes" "${acct}:${remote}/sites/default/files/" "${project_path}/${docroot}/sites/default/files/" || return 1 ;
131
+ echo " If Drupal still shows broken images: ddev drush cr";
132
+ }
133
+
134
+ function cpanel_do_push() {
135
+ local project_path="$1" acct="$2" env="$3" docroot remote a;
136
+ remote="$(cpanel_link_get "${project_path}" remote)";
137
+ docroot="$(cpanel_docroot "${project_path}")";
138
+ [ -z "${docroot}" ] && { echo "No web/ or docroot/ in ${doc_name}/${PROJECT_NAME} — nothing to deploy."; return 1; } ;
139
+ echo "Deploy ${docroot}/ of ${doc_name}/${PROJECT_NAME} → ${acct}:${remote}/ (${env})";
140
+ echo " rsync --delete, excluding sites/default/files, sites/*/settings*.php, .git, .ddev. The live site changes.";
141
+ read -r -p " Continue? [y/N] " a < /dev/tty || return 1 ;
142
+ [[ "${a}" =~ ^[Yy] ]] || { echo "Not deployed."; return 1; } ;
143
+ rsync -az --delete --info=progress2 -e "ssh -i $(cpanel_ssh_key) -o BatchMode=yes" \
144
+ --exclude 'sites/default/files' --exclude 'sites/*/settings*.php' --exclude '.git' --exclude '.ddev' \
145
+ "${project_path}/${docroot}/" "${acct}:${remote}/" || return 1 ;
146
+ if [ -d "${project_path}/vendor" ] ; then
147
+ echo " vendor/ is beside the docroot locally: syncing it to $(dirname "${remote}")/vendor/ too.";
148
+ rsync -az --delete -e "ssh -i $(cpanel_ssh_key) -o BatchMode=yes" "${project_path}/vendor/" "${acct}:$(dirname "${remote}")/vendor/" || return 1 ;
149
+ fi
150
+ echo " Deployed. Clear caches on the server: ssh ${acct} 'cd ${remote} && drush cr'";
151
+ }
152
+
153
+ function cpanel_do_logs() {
154
+ local project_path="$1" remote;
155
+ remote="$(cpanel_link_get "${project_path}" remote)";
156
+ cpanel_ssh "${project_path}" "for f in '${remote}/error_log' \"\$HOME/logs/\"*error_log; do [ -f \"\$f\" ] && { echo \"— \$f —\"; tail -50 \"\$f\"; }; done" ;
157
+ }
158
+
159
+ function cpanel_status() {
160
+ local project_path="$1" acct="$2";
161
+ if [ -z "${acct}" ] ; then
162
+ echo " (the link is user@host:/remote/docroot)";
163
+ return 0 ;
164
+ fi
165
+ echo " remote: $(cpanel_link_get "${project_path}" remote)";
166
+ echo "";
167
+ cpanel_do_environments "${project_path}" ;
168
+ echo "";
169
+ echo " Console: https://$(cpanel_link_get "${project_path}" host):2083/";
170
+ echo " Data: --pull-db and --pull-files bring the site down into DDEV over SSH; --push deploys with rsync.";
171
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ ## The host side of the cPanel plugin: the CLI on this machine and the dashboard's credentials.
3
+ ## Run by core/scripts/plugins.sh --install cpanel, and by core/scripts/install.sh for every installed
4
+ ## plugin that ships one. Idempotent: whatever is already there is reported, not redone. Offered,
5
+ ## never automatic: a hosting CLI carries cloud credentials for real environments.
6
+ set -u
7
+ WORKSPACE_ROOT="${WORKSPACE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." && pwd)}";
8
+
9
+ STATE="$(grep -E '^api_tokens:' "${WORKSPACE_ROOT}/core/config/plugin.cpanel.settings.yml" 2>/dev/null | head -1 | sed -E 's/^[a-z_]+:[[:space:]]*//')";
10
+ case "${STATE}" in
11
+ ''|CHANGE_ME|"''"|'""') printf ' ✗ %-16s %s\n' "api_tokens" "not set in plugin.cpanel.settings.yml — the dashboard cannot reach cPanel" ;;
12
+ *) printf ' ✓ %-16s %s\n' "api_tokens" "set (plugin.cpanel.settings.yml)" ;;
13
+ esac
14
+