gingee-cli 1.0.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/LICENSE +21 -0
- package/README.md +163 -0
- package/commands/addApp.js +126 -0
- package/commands/addScript.js +45 -0
- package/commands/apiClient.js +139 -0
- package/commands/deleteApp.js +62 -0
- package/commands/init.js +133 -0
- package/commands/installApp.js +85 -0
- package/commands/installStoreApp.js +87 -0
- package/commands/installerUtils.js +430 -0
- package/commands/listApps.js +41 -0
- package/commands/listBackups.js +41 -0
- package/commands/listStoreApps.js +60 -0
- package/commands/login.js +70 -0
- package/commands/logout.js +10 -0
- package/commands/packageApp.js +66 -0
- package/commands/resetGlade.js +95 -0
- package/commands/resetPwd.js +80 -0
- package/commands/rollbackApp.js +63 -0
- package/commands/service.js +123 -0
- package/commands/upgradeApp.js +80 -0
- package/commands/upgradeStoreApp.js +97 -0
- package/commands/utils.js +35 -0
- package/index.js +154 -0
- package/package.json +51 -0
- package/templates/project/ecosystem.config.js +34 -0
- package/templates/project/gingee.json +34 -0
- package/templates/project/package.json +15 -0
- package/templates/project/start.js +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2025] [Vignesh Swaminathan]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Gingee CLI: Command Reference
|
|
2
|
+
|
|
3
|
+
The `gingee-cli` is the official, all-in-one command-line interface for the **[Gingee platform](https://github.com/gingerhome/gingee)**. It is a powerful tool for both developers and system administrators, designed to streamline every phase of the application lifecycle, from initial project creation to ongoing production management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The `gingee-cli` is designed to be installed globally on your machine, making it available everywhere.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g gingee-cli
|
|
11
|
+
```
|
|
12
|
+
After installation, you will have access to the `gingee-cli` command in your terminal.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
### Project Initialization
|
|
19
|
+
|
|
20
|
+
This is the main entry point for starting a new Gingee project.
|
|
21
|
+
|
|
22
|
+
#### `init <project-name>`
|
|
23
|
+
|
|
24
|
+
Scaffolds a complete, new Gingee project in a new directory. It launches an interactive wizard to guide you through the setup.
|
|
25
|
+
|
|
26
|
+
**Usage:**
|
|
27
|
+
```bash
|
|
28
|
+
gingee-cli init my-awesome-project```
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Wizard Prompts:**
|
|
32
|
+
- `Administrator Username for glade:` Sets the initial username for the bundled Glade admin panel. Defaults to `admin`.
|
|
33
|
+
- `Administrator Password for glade:` Securely prompts for the admin password. This is hashed and stored in Glade's configuration.
|
|
34
|
+
- `Install npm dependencies automatically?` If yes (default), it will run `npm install` so the project is ready to run immediately.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Local Scaffolding
|
|
39
|
+
|
|
40
|
+
These commands should be run from the root directory of an existing Gingee project.
|
|
41
|
+
|
|
42
|
+
#### `add-app <app-name>`
|
|
43
|
+
|
|
44
|
+
Scaffolds a new, working "hello world" application inside your project's `web` directory.
|
|
45
|
+
|
|
46
|
+
**Usage:**
|
|
47
|
+
```bash
|
|
48
|
+
gingee-cli add-app my-blog
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Wizard Prompts:**
|
|
52
|
+
- `What type of app is this?` Choose between `MPA` (Multi-Page App, default) or `SPA` (Single Page Application, for React/Vue/Angular).
|
|
53
|
+
- `Would you like to configure a database connection?` If yes, it will guide you through setting up the `db` block in the new app's `app.json`.
|
|
54
|
+
- `Generate a JWT secret for this app?` If yes, it will automatically generate a secure secret and add it to `app.json`.
|
|
55
|
+
|
|
56
|
+
#### `add-script <app-name> <script-path>`
|
|
57
|
+
|
|
58
|
+
Quickly creates a new server script file, pre-populated with the standard Gingee boilerplate.
|
|
59
|
+
|
|
60
|
+
**Usage:**
|
|
61
|
+
```bash
|
|
62
|
+
gingee-cli add-script my-blog api/posts
|
|
63
|
+
```
|
|
64
|
+
Creates ./web/my-blog/box/api/posts.js
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Server Administration
|
|
69
|
+
|
|
70
|
+
These commands interact with the API of a live, running `glade` instance. They require you to be authenticated via the `login` command.
|
|
71
|
+
|
|
72
|
+
#### `login [server-url]`
|
|
73
|
+
|
|
74
|
+
Authenticates the CLI with a Glade admin panel and saves the session for subsequent commands.
|
|
75
|
+
|
|
76
|
+
**Usage:**
|
|
77
|
+
- Login to a local server
|
|
78
|
+
```bash
|
|
79
|
+
gingee-cli login
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- Login to a remote server
|
|
83
|
+
```bash
|
|
84
|
+
gingee-cli login https://prod.my-server.com
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Options:**
|
|
88
|
+
- `-u, --username <username>`: Provide the username non-interactively. Defaults to `admin`.
|
|
89
|
+
- `-p, --password <password>`: Provide the password non-interactively. If this option is omitted, you will be securely prompted to enter a password.
|
|
90
|
+
|
|
91
|
+
#### `logout [server-url]`
|
|
92
|
+
|
|
93
|
+
Logs out of a specific Glade session by deleting the stored credentials.
|
|
94
|
+
|
|
95
|
+
**Usage:**
|
|
96
|
+
```bash
|
|
97
|
+
gingee-cli logout https://prod.my-server.com
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### `list-apps`
|
|
101
|
+
|
|
102
|
+
Lists all applications installed on the target server.
|
|
103
|
+
|
|
104
|
+
**Usage:**
|
|
105
|
+
```bash
|
|
106
|
+
gingee-cli list-apps --server https://prod.my-server.com
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Options:**
|
|
110
|
+
- `-s, --server <url>` (Optional): The base URL of the target Gingee server. Defaults to the server you last logged into, or `http://localhost:7070`.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Application Lifecycle Management
|
|
115
|
+
|
|
116
|
+
These powerful commands allow for remote deployment and management of your applications.
|
|
117
|
+
|
|
118
|
+
| Command | Description |
|
|
119
|
+
| :--- | :--- |
|
|
120
|
+
| **`package-app`** | Packages a live application from the server into a distributable `.gin` archive file. |
|
|
121
|
+
| **`install-app`** | Installs a new application onto a server from a local `.gin` package file. |
|
|
122
|
+
| **`upgrade-app`** | Upgrades an existing application on a server using a new `.gin` package file. |
|
|
123
|
+
| **`delete-app`** | Permanently deletes an application and all its content from the server. |
|
|
124
|
+
|
|
125
|
+
**Common Options for Lifecycle Commands:**
|
|
126
|
+
- `-s, --server <url>` (Optional): The URL of the target server. Defaults to the last-logged-in server.
|
|
127
|
+
- `-a, --appName <app-name>` (Required): The name of the target application.
|
|
128
|
+
- `-p, --ginPath <path>` (Required for install/upgrade): The path to the local `.gin` package file.
|
|
129
|
+
|
|
130
|
+
**Example Usage:**```bash
|
|
131
|
+
# Upgrade the 'my-blog' app on a production server
|
|
132
|
+
gingee-cli upgrade-app --appName my-blog --ginPath ./builds/my-blog-v2.gin --server https://prod.server```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### Backup & Recovery
|
|
137
|
+
|
|
138
|
+
Commands for the disaster recovery and rollback features.
|
|
139
|
+
|
|
140
|
+
| Command | Description |
|
|
141
|
+
| :--- | :--- |
|
|
142
|
+
| **`list-app-backups`** | Lists all available `.gin` backup files for an application stored on the server. |
|
|
143
|
+
| **`rollback-app`** | Rolls an application back to its most recently created backup on the server. |
|
|
144
|
+
|
|
145
|
+
**Common Options for Recovery Commands:**
|
|
146
|
+
- `-s, --server <url>` (Optional): The URL of the target server.
|
|
147
|
+
- `-a, --appName <app-name>` (Required): The name of the target application.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Service Management
|
|
152
|
+
|
|
153
|
+
Commands for running Gingee as a native background service. These commands must be run from a project's root directory and typically require `sudo` or Administrator privileges.
|
|
154
|
+
|
|
155
|
+
- **`service install`**: Installs and starts the server as a background service.
|
|
156
|
+
- **`service uninstall`**: Stops and removes the background service.
|
|
157
|
+
- **`service start`**: Manually starts the installed service.
|
|
158
|
+
- **`service stop`**: Manually stops the installed service.
|
|
159
|
+
|
|
160
|
+
### Local Utilities
|
|
161
|
+
|
|
162
|
+
- **`reset-pwd`**: A local recovery tool. Prompts for a new admin user password for `glade` admin panel.
|
|
163
|
+
- **`reset-glade`**: A local recovery tool that performs a clean re-installation or reset of the `glade` admin panel.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto'); // Use Node's built-in crypto for the JWT
|
|
4
|
+
const { getProjectRoot, getWebRoot } = require('./utils');
|
|
5
|
+
|
|
6
|
+
async function addApp(appName) {
|
|
7
|
+
const { default: chalk } = await import('chalk');
|
|
8
|
+
const { default: inquirer } = await import('inquirer');
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const projectRoot = getProjectRoot();
|
|
12
|
+
const webRoot = getWebRoot(projectRoot);
|
|
13
|
+
const appPath = path.join(webRoot, appName);
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(appPath)) {
|
|
16
|
+
throw new Error(`An application named '${appName}' already exists at ${appPath}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`Creating a new Gingee app named '${appName}'...`);
|
|
20
|
+
|
|
21
|
+
// --- Run the interactive wizard ---
|
|
22
|
+
const answers = await inquirer.prompt([
|
|
23
|
+
{
|
|
24
|
+
type: 'list',
|
|
25
|
+
name: 'type',
|
|
26
|
+
message: 'What type of app is this?',
|
|
27
|
+
choices: ['MPA', 'SPA'],
|
|
28
|
+
default: 'MPA',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'confirm',
|
|
32
|
+
name: 'configureDb',
|
|
33
|
+
message: 'Would you like to configure a database connection now?',
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'list',
|
|
38
|
+
name: 'dbType',
|
|
39
|
+
message: 'Select Database Type:',
|
|
40
|
+
choices: ['postgres', 'mysql', 'sqlite', 'mssql', 'oracle'],
|
|
41
|
+
when: (ans) => ans.configureDb,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'input', name: 'dbName', message: 'Connection Name (e.g., main_db):', default: 'main_db',
|
|
45
|
+
when: (ans) => ans.configureDb,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'input', name: 'dbHost', message: 'Database Host:', default: 'localhost',
|
|
49
|
+
when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'input', name: 'dbUser', message: 'Database User:',
|
|
53
|
+
when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'password', name: 'dbPass', message: 'Database Password:', mask: '*',
|
|
57
|
+
when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'input', name: 'dbDatabase', message: 'Database Name:',
|
|
61
|
+
when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'input', name: 'dbFile', message: 'Database File Path (relative to box folder):', default: 'data/app.db',
|
|
65
|
+
when: (ans) => ans.dbType === 'sqlite',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'confirm',
|
|
69
|
+
name: 'configureJwt',
|
|
70
|
+
message: 'Generate a JWT secret for this app?',
|
|
71
|
+
default: false,
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// --- Scaffold the files and folders ---
|
|
76
|
+
console.log('Scaffolding app structure...');
|
|
77
|
+
const boxPath = path.join(appPath, 'box');
|
|
78
|
+
fs.ensureDirSync(boxPath);
|
|
79
|
+
fs.ensureDirSync(path.join(appPath, 'css'));
|
|
80
|
+
fs.ensureDirSync(path.join(appPath, 'images'));
|
|
81
|
+
fs.ensureDirSync(path.join(appPath, 'scripts'));
|
|
82
|
+
|
|
83
|
+
// --- Create app.json ---
|
|
84
|
+
const appConfig = { name: appName, version: '1.0.0', type: answers.type };
|
|
85
|
+
if (answers.configureDb) {
|
|
86
|
+
appConfig.db = [{
|
|
87
|
+
type: answers.dbType,
|
|
88
|
+
name: answers.dbName,
|
|
89
|
+
host: answers.dbHost,
|
|
90
|
+
user: answers.dbUser,
|
|
91
|
+
password: answers.dbPass,
|
|
92
|
+
database: answers.dbType === 'sqlite' ? answers.dbFile : answers.dbDatabase,
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
if (answers.configureJwt) {
|
|
96
|
+
appConfig.jwt_secret = crypto.randomBytes(32).toString('hex');
|
|
97
|
+
}
|
|
98
|
+
fs.writeJsonSync(path.join(boxPath, 'app.json'), appConfig, { spaces: 2 });
|
|
99
|
+
|
|
100
|
+
// --- Create hello.js ---
|
|
101
|
+
const helloScriptContent = `module.exports = async function() {\n await gingee(async ($g) => {\n $g.response.send({ message: 'Hello from the ${appName} server script!' });\n });\n};`;
|
|
102
|
+
fs.writeFileSync(path.join(boxPath, 'hello.js'), helloScriptContent);
|
|
103
|
+
|
|
104
|
+
// --- Create index.html ---
|
|
105
|
+
const indexHtmlContent = `<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>${appName}</title>\n</head>\n<body>\n <h1>${appName}</h1>\n <button id="helloButton">Say Hello</button>\n <div id="response" style="margin-top: 1rem; font-family: monospace;"></div>\n <script src="/${appName}/scripts/cl_app.js"></script>\n</body>\n</html>`;
|
|
106
|
+
fs.writeFileSync(path.join(appPath, 'index.html'), indexHtmlContent);
|
|
107
|
+
|
|
108
|
+
// --- Create cl_app.js ---
|
|
109
|
+
const clAppJsContent = `document.getElementById('helloButton').addEventListener('click', async () => {\n const responseElement = document.getElementById('response');\n responseElement.innerText = 'Loading...';\n try {\n const res = await fetch('/${appName}/hello');\n const data = await res.json();\n responseElement.innerText = \`Server says: \${data.message}\`;\n } catch (err) {\n responseElement.innerText = 'Error: Could not connect to server.';\n }\n});`;
|
|
110
|
+
fs.writeFileSync(path.join(appPath, 'scripts', 'cl_app.js'), clAppJsContent);
|
|
111
|
+
|
|
112
|
+
console.log(chalk.bgGreen(`\n✅ Success!`), chalk.blueBright(`App '${appName}' created.`));
|
|
113
|
+
console.log(` Navigate to /${appName} in your browser to see it in action.`);
|
|
114
|
+
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err.errors) { //for AggregateError
|
|
117
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
118
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
119
|
+
} else {
|
|
120
|
+
console.error(chalk.bgRed(`\nError: `), chalk.blueBright(`${err.message}`));
|
|
121
|
+
}
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { addApp };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getProjectRoot, getWebRoot } = require('./utils');
|
|
4
|
+
|
|
5
|
+
// The function signature now accepts two distinct arguments.
|
|
6
|
+
async function addScript(appName, scriptPath) {
|
|
7
|
+
const { default: chalk } = await import('chalk');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const projectRoot = getProjectRoot();
|
|
11
|
+
const webRoot = getWebRoot(projectRoot);
|
|
12
|
+
|
|
13
|
+
const appPath = path.join(webRoot, appName);
|
|
14
|
+
if (!fs.existsSync(appPath)) {
|
|
15
|
+
throw new Error(`Application '${appName}' does not exist at: ${appPath}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const finalPath = path.join(appPath, 'box', scriptPath + '.js');
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(finalPath)) {
|
|
21
|
+
throw new Error(`Script already exists at: ${path.relative(projectRoot, finalPath)}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fs.ensureDirSync(path.dirname(finalPath));
|
|
25
|
+
|
|
26
|
+
const boilerplate = `module.exports = async function() {\n await gingee(async ($g) => {\n // Your script logic goes here\n $g.response.send('Hello from ${scriptPath}');\n });\n};`;
|
|
27
|
+
|
|
28
|
+
fs.writeFileSync(finalPath, boilerplate);
|
|
29
|
+
|
|
30
|
+
console.log(chalk.bgGreen(`✅ Success!`), chalk.blueBright(`Script created at:`));
|
|
31
|
+
console.log(chalk.blueBright(` ${path.relative(projectRoot, finalPath)}`));
|
|
32
|
+
console.log(`\n You can access it at the URL: /${appName}/${scriptPath}`);
|
|
33
|
+
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.errors) { //for AggregateError
|
|
36
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
37
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
38
|
+
} else {
|
|
39
|
+
console.error(chalk.bgRed(`\nError: `), chalk.blueBright(`${err.message}`));
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { addScript };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const configDir = path.join(os.homedir(), '.gingee');
|
|
8
|
+
const credsDir = path.join(configDir, 'sessions');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanitizes a server URL into a safe filename.
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
function getCredsFilePath(serverUrl) {
|
|
15
|
+
// Use a hash to create a unique, fixed-length, safe filename
|
|
16
|
+
const hash = crypto.createHash('sha256').update(serverUrl).digest('hex');
|
|
17
|
+
if (!fs.existsSync(credsDir)) {
|
|
18
|
+
fs.mkdirSync(credsDir);
|
|
19
|
+
}
|
|
20
|
+
return path.join(credsDir, `${hash}.json`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an authenticated axios instance by manually adding the session cookie.
|
|
25
|
+
* @returns {Promise<{client: import('axios').AxiosInstance, serverUrl: string}>}
|
|
26
|
+
*/
|
|
27
|
+
async function getAuthenticatedClient(serverUrl = 'http://localhost:7070') {
|
|
28
|
+
const credsPath = getCredsFilePath(serverUrl);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(credsPath)) {
|
|
31
|
+
throw new Error("You are not logged in. Please run `gingee-cli login <server-url>` first.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { cookie } = fs.readJsonSync(credsPath);
|
|
35
|
+
if (!serverUrl || !cookie) {
|
|
36
|
+
throw new Error("Credentials file is corrupted or empty. Please log in again.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create a standard axios instance.
|
|
40
|
+
const client = axios.create();
|
|
41
|
+
|
|
42
|
+
// Manually set the 'Cookie' header for all requests made with this instance.
|
|
43
|
+
client.defaults.headers.common['Cookie'] = cookie;
|
|
44
|
+
|
|
45
|
+
return { client, serverUrl };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deleteSession(serverUrl) {
|
|
49
|
+
const credsPath = getCredsFilePath(serverUrl);
|
|
50
|
+
if (fs.existsSync(credsPath)) {
|
|
51
|
+
fs.removeSync(credsPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function installApp(serverUrl, appName, ginFileName, ginFileBuffer, permissions) {
|
|
56
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
57
|
+
const FormData = require('form-data');
|
|
58
|
+
|
|
59
|
+
const form = new FormData();
|
|
60
|
+
form.append('appName', appName);
|
|
61
|
+
form.append('package', ginFileBuffer, ginFileName);
|
|
62
|
+
form.append('permissions', JSON.stringify(permissions));
|
|
63
|
+
|
|
64
|
+
const response = await client.post(`${serverUrl}/glade/api/install`, form, {
|
|
65
|
+
headers: form.getHeaders()
|
|
66
|
+
});
|
|
67
|
+
return response.data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function upgradeApp(serverUrl, appName, ginFileName, ginFileBuffer, permissions) {
|
|
71
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
72
|
+
const FormData = require('form-data');
|
|
73
|
+
|
|
74
|
+
const form = new FormData();
|
|
75
|
+
form.append('appName', appName);
|
|
76
|
+
form.append('package', ginFileBuffer, ginFileName);
|
|
77
|
+
form.append('permissions', JSON.stringify(permissions));
|
|
78
|
+
|
|
79
|
+
const response = await client.post(`${serverUrl}/glade/api/upgrade`, form, {
|
|
80
|
+
headers: form.getHeaders()
|
|
81
|
+
});
|
|
82
|
+
return response.data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function deleteApp(serverUrl, appName) {
|
|
86
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
87
|
+
const response = await client.get(`${serverUrl}/glade/api/delete?app=${appName}`);
|
|
88
|
+
return response.data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function rollbackApp(serverUrl, appName, grantedPermissions) {
|
|
92
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
93
|
+
const response = await client.post(`${serverUrl}/glade/api/rollback`, {
|
|
94
|
+
appName,
|
|
95
|
+
permissions: grantedPermissions
|
|
96
|
+
});
|
|
97
|
+
return response.data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function listBackups(serverUrl, appName) {
|
|
101
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
102
|
+
const response = await client.get(`${serverUrl}/glade/api/list-backups?app=${appName}`);
|
|
103
|
+
return response.data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function packageApp(serverUrl, appName) {
|
|
107
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
108
|
+
// CRITICAL: We expect a binary stream, so set responseType to 'stream'.
|
|
109
|
+
const response = await client.get(`${serverUrl}/glade/api/package?app=${appName}`, {
|
|
110
|
+
responseType: 'stream'
|
|
111
|
+
});
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function analyzeBackup(serverUrl, appName) {
|
|
116
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
117
|
+
const response = await client.get(`${serverUrl}/glade/api/analyze-backup?app=${appName}`);
|
|
118
|
+
return response.data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function getAppPermissions(serverUrl, appName) {
|
|
122
|
+
const { client } = await getAuthenticatedClient(serverUrl);
|
|
123
|
+
const response = await client.get(`${serverUrl}/glade/api/get-permissions?app=${appName}`);
|
|
124
|
+
return response.data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
getCredsFilePath,
|
|
129
|
+
getAuthenticatedClient,
|
|
130
|
+
deleteSession,
|
|
131
|
+
installApp,
|
|
132
|
+
upgradeApp,
|
|
133
|
+
deleteApp,
|
|
134
|
+
rollbackApp,
|
|
135
|
+
listBackups,
|
|
136
|
+
packageApp,
|
|
137
|
+
analyzeBackup,
|
|
138
|
+
getAppPermissions
|
|
139
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const apiClient = require('./apiClient');
|
|
2
|
+
const { _readAppPreset, _validateAppPreset } = require('./installerUtils');
|
|
3
|
+
|
|
4
|
+
async function deleteApp(options) {
|
|
5
|
+
const { default: chalk } = await import('chalk');
|
|
6
|
+
const { default: ora } = await import('ora');
|
|
7
|
+
const { default: inquirer } = await import('inquirer');
|
|
8
|
+
const { serverUrl, appName, file: presetFilePath } = options;
|
|
9
|
+
|
|
10
|
+
const spinner = ora();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
let confirmation = false;
|
|
14
|
+
if (presetFilePath) {
|
|
15
|
+
// --- NON-INTERACTIVE MODE ---
|
|
16
|
+
console.log(chalk.blue(`Running in non-interactive mode using preset file: ${presetFilePath}`));
|
|
17
|
+
const preset = _readAppPreset(presetFilePath);
|
|
18
|
+
_validateAppPreset(preset, 'delete');
|
|
19
|
+
confirmation = preset.delete.confirm; // This will be true if validation passed
|
|
20
|
+
} else {
|
|
21
|
+
const { confirmDelete } = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'confirmDelete',
|
|
25
|
+
message: `This will ${chalk.bgRed('PERMANENTLY DELETE')} the application '${chalk.bgRed(appName)}' and all its content.\n This action cannot be undone. To confirm, please type the name of the app:`
|
|
26
|
+
}
|
|
27
|
+
]);
|
|
28
|
+
if (confirmDelete === appName) {
|
|
29
|
+
confirmation = true;
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error('Confirmation text did not match. Deletion cancelled.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (confirmation) {
|
|
36
|
+
spinner.start(`Deleting app '${appName}'...`)
|
|
37
|
+
const result = await apiClient.deleteApp(serverUrl, appName);
|
|
38
|
+
|
|
39
|
+
if (result.status !== 'success') {
|
|
40
|
+
throw new Error(result.message || 'Server responded with an unknown error.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
spinner.succeed(chalk.bgGreen(`✅ Success!`));
|
|
44
|
+
console.log(chalk.blueBright(`App '${appName}' deleted from Gingee Server - ${serverUrl}`));
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.bgGreen(`Canceled: `), chalk.blueBright('Deletion cancelled. No changes made.'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spinner.fail(chalk.bgRed('Deletion failed.'));
|
|
51
|
+
if (err.errors) { //for AggregateError
|
|
52
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
53
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
54
|
+
} else {
|
|
55
|
+
const message = err.response ? (err.response.data.message || 'Server error.') : err.message;
|
|
56
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${message}`));
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { deleteApp };
|