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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const apiClient = require('./apiClient');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
async function packageApp(options) {
|
|
6
|
+
const { default: chalk } = await import('chalk');
|
|
7
|
+
const { default: ora } = await import('ora');
|
|
8
|
+
const { serverUrl, appName, dest: destFolder } = options;
|
|
9
|
+
const spinner = ora(`Requesting package for '${appName}' from server...`).start();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// This returns a readable stream of the file being downloaded.
|
|
13
|
+
const response = await apiClient.packageApp(serverUrl, appName);
|
|
14
|
+
const fileStream = response.data;
|
|
15
|
+
|
|
16
|
+
let fileName = `${appName}.gin`;
|
|
17
|
+
const contentDisposition = response.headers['content-disposition'];
|
|
18
|
+
if (contentDisposition) {
|
|
19
|
+
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
|
20
|
+
if (fileNameMatch && fileNameMatch.length > 1) {
|
|
21
|
+
fileName = fileNameMatch[1];
|
|
22
|
+
spinner.info(`Server suggested filename: ${fileName}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let destPathRoot = process.cwd(); // Default to the current working directory
|
|
27
|
+
if (destFolder) {
|
|
28
|
+
try {
|
|
29
|
+
const userDest = path.resolve(destFolder);
|
|
30
|
+
// fs-extra's ensureDirSync is like `mkdir -p`, safe to run even if it exists.
|
|
31
|
+
fs.ensureDirSync(userDest);
|
|
32
|
+
destPathRoot = userDest;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
spinner.warn(chalk.yellow(`Could not create or access destination folder '${destFolder}'. Falling back to the current directory.`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const finalDestPath = path.join(destPathRoot, fileName);
|
|
38
|
+
const writer = fs.createWriteStream(finalDestPath);
|
|
39
|
+
|
|
40
|
+
spinner.text = `Downloading package to ${fileName}...`;
|
|
41
|
+
|
|
42
|
+
// Pipe the download stream to the file writer stream.
|
|
43
|
+
fileStream.pipe(writer);
|
|
44
|
+
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
writer.on('finish', resolve);
|
|
47
|
+
writer.on('error', reject);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
spinner.succeed(chalk.bgGreen(`✅ Success!`), chalk.blueBright(`Application '${appName}' packaged to:`));
|
|
51
|
+
console.log(chalk.cyan(` ${finalDestPath}`));
|
|
52
|
+
|
|
53
|
+
} catch (err) {
|
|
54
|
+
spinner.fail(chalk.bgRed('Packaging failed.'));
|
|
55
|
+
if (err.errors) { //for AggregateError
|
|
56
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
57
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
58
|
+
} else {
|
|
59
|
+
const message = _getHttpClientErrorMessage(err);
|
|
60
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${message}`));
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { packageApp };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const argon2 = require('argon2');
|
|
4
|
+
const { getProjectRoot, getWebRoot } = require('./utils');
|
|
5
|
+
const { _unzipBuffer } = require('./installerUtils');
|
|
6
|
+
|
|
7
|
+
async function resetGlade() {
|
|
8
|
+
const { default: chalk } = await import('chalk');
|
|
9
|
+
const { default: inquirer } = await import('inquirer');
|
|
10
|
+
const { default: ora } = await import('ora');
|
|
11
|
+
const spinner = ora();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
console.log(chalk.blueBright('⚠️ Glade Admin Panel Install/Reset Utility ⚠️'));
|
|
15
|
+
|
|
16
|
+
const projectRoot = getProjectRoot();
|
|
17
|
+
const webRoot = getWebRoot(projectRoot);
|
|
18
|
+
const gladeAppPath = path.join(webRoot, 'glade');
|
|
19
|
+
|
|
20
|
+
// --- THIS IS THE NEW, MORE ROBUST LOGIC ---
|
|
21
|
+
if (fs.existsSync(gladeAppPath)) {
|
|
22
|
+
// If the folder exists, we are in "reset" mode. We need confirmation.
|
|
23
|
+
const { confirmation } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'input',
|
|
26
|
+
name: 'confirmation',
|
|
27
|
+
message: `This will ${chalk.bgRed('PERMANENTLY DELETE')} the existing 'glade' application and reinstall it.\n To confirm, please type the name of the app ('glade'):`,
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
if (confirmation !== 'glade') {
|
|
32
|
+
console.log(chalk.yellow('Reset cancelled. No changes were made.'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Deletion
|
|
37
|
+
spinner.start('Deleting current `glade` installation...');
|
|
38
|
+
fs.removeSync(gladeAppPath);
|
|
39
|
+
spinner.succeed('Current `glade` installation deleted.');
|
|
40
|
+
}
|
|
41
|
+
// --- END OF NEW LOGIC ---
|
|
42
|
+
// If the folder didn't exist, we just proceed directly to installation.
|
|
43
|
+
|
|
44
|
+
// --- Re-installation ---
|
|
45
|
+
spinner.start('Installing a clean version of `glade`...');
|
|
46
|
+
const gladeGinPath = require.resolve('gingee/templates/glade.gin');
|
|
47
|
+
const gladePackageBuffer = fs.readFileSync(gladeGinPath);
|
|
48
|
+
await _unzipBuffer(gladePackageBuffer, gladeAppPath);
|
|
49
|
+
spinner.succeed('Clean `glade` version installed.');
|
|
50
|
+
|
|
51
|
+
// --- Re-configuration (Wizard runs in both cases) ---
|
|
52
|
+
console.log(chalk.blueBright('\nPlease set the administrator credentials.'));
|
|
53
|
+
const newCreds = await inquirer.prompt([
|
|
54
|
+
{ type: 'input', name: 'adminUser', message: 'Enter a username for the Glade admin panel:', default: 'admin' },
|
|
55
|
+
{ type: 'password', name: 'adminPass', message: 'Enter a password for the Glade admin:', mask: '*' },
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
if (!newCreds.adminPass) {
|
|
59
|
+
throw new Error("Admin password cannot be empty.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
spinner.start('Configuring admin credentials...');
|
|
63
|
+
const passwordHash = await argon2.hash(newCreds.adminPass);
|
|
64
|
+
const gladeAppConfigPath = path.join(gladeAppPath, 'box', 'app.json');
|
|
65
|
+
const gladeAppConfig = fs.readJsonSync(gladeAppConfigPath);
|
|
66
|
+
gladeAppConfig.env.ADMIN_USERNAME = newCreds.adminUser;
|
|
67
|
+
gladeAppConfig.env.ADMIN_PASSWORD_HASH = passwordHash;
|
|
68
|
+
fs.writeJsonSync(gladeAppConfigPath, gladeAppConfig, { spaces: 2 });
|
|
69
|
+
spinner.succeed('Admin credentials configured securely.');
|
|
70
|
+
|
|
71
|
+
spinner.start('Granting default permissions to Glade...');
|
|
72
|
+
const permissionsFilePath = path.join(projectRoot, 'settings', 'permissions.json');
|
|
73
|
+
const permissionsConfig = {
|
|
74
|
+
"glade": {
|
|
75
|
+
"granted": ["platform", "fs"]
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
fs.writeJsonSync(permissionsFilePath, permissionsConfig, { spaces: 2 });
|
|
79
|
+
spinner.succeed('Default permissions for Glade configured.');
|
|
80
|
+
|
|
81
|
+
console.log(chalk.bgGreen(`\n✅ Success!`), chalk.blueBright(`The 'glade' admin panel is ready.`));
|
|
82
|
+
|
|
83
|
+
} catch (err) {
|
|
84
|
+
spinner.fail(chalk.bgRed('Operation failed.'));
|
|
85
|
+
if (err.errors) { //for AggregateError
|
|
86
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
87
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
88
|
+
} else {
|
|
89
|
+
console.error(chalk.bgRed(`\nError: `), chalk.blueBright(`${err.message}`));
|
|
90
|
+
}
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { resetGlade };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const argon2 = require('argon2');
|
|
4
|
+
const { getProjectRoot, getWebRoot } = require('./utils');
|
|
5
|
+
|
|
6
|
+
async function resetPwd() {
|
|
7
|
+
const { default: chalk } = await import('chalk');
|
|
8
|
+
const { default: inquirer } = await import('inquirer');
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
console.log(chalk.blueBright('Glade Admin Password Reset Utility'));
|
|
12
|
+
|
|
13
|
+
const projectRoot = getProjectRoot();
|
|
14
|
+
const webRoot = getWebRoot(projectRoot);
|
|
15
|
+
const gladeAppPath = path.join(webRoot, 'glade');
|
|
16
|
+
const gladeAppConfigPath = path.join(gladeAppPath, 'box', 'app.json');
|
|
17
|
+
|
|
18
|
+
// Verify that the glade app and its config exist
|
|
19
|
+
if (!fs.existsSync(gladeAppConfigPath)) {
|
|
20
|
+
throw new Error("Could not find the `glade` application's app.json file. Is glade installed?");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// --- Run the interactive wizard ---
|
|
24
|
+
const answers = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: 'password',
|
|
27
|
+
name: 'newPassword',
|
|
28
|
+
message: 'Enter the new password for the Glade admin:',
|
|
29
|
+
mask: '*',
|
|
30
|
+
validate: input => {
|
|
31
|
+
if (!input || input.length < 8) {
|
|
32
|
+
return 'Password must be at least 8 characters long.';
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'password',
|
|
39
|
+
name: 'confirmPassword',
|
|
40
|
+
message: 'Confirm the new password:',
|
|
41
|
+
mask: '*',
|
|
42
|
+
}
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (answers.newPassword !== answers.confirmPassword) {
|
|
46
|
+
throw new Error("Passwords do not match. Please try again.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(chalk.blueBright('Hashing new password...'));
|
|
50
|
+
|
|
51
|
+
// --- Hash the password ---
|
|
52
|
+
const passwordHash = await argon2.hash(answers.newPassword);
|
|
53
|
+
|
|
54
|
+
// --- Safely update app.json ---
|
|
55
|
+
const appConfig = fs.readJsonSync(gladeAppConfigPath);
|
|
56
|
+
|
|
57
|
+
// Ensure the env object exists
|
|
58
|
+
if (!appConfig.env) {
|
|
59
|
+
appConfig.env = {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
appConfig.env.ADMIN_PASSWORD_HASH = passwordHash;
|
|
63
|
+
|
|
64
|
+
fs.writeJsonSync(gladeAppConfigPath, appConfig, { spaces: 2 });
|
|
65
|
+
|
|
66
|
+
console.log(chalk.bgGreen(`\n✅ Success!`), chalk.blueBright(`The Glade admin password has been reset.`));
|
|
67
|
+
console.log(chalk.blueBright(` You can now log in with your new password.`));
|
|
68
|
+
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.errors) { //for AggregateError
|
|
71
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
72
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
73
|
+
} else {
|
|
74
|
+
console.error(chalk.bgRed(`\nError: `), chalk.blueBright(`${err.message}`));
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { resetPwd };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const apiClient = require('./apiClient');
|
|
2
|
+
const { _getHttpClientErrorMessage, _getRollbackPermissions, _readAppPreset, _validateAppPreset } = require('./installerUtils'); // Use the new, specific utility
|
|
3
|
+
|
|
4
|
+
async function rollbackApp(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
|
+
let finalPermissions;
|
|
10
|
+
const spinner = ora();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
if (presetFilePath) {
|
|
14
|
+
// --- NON-INTERACTIVE MODE ---
|
|
15
|
+
console.log(chalk.blueBright(`Running in non-interactive mode using preset file: ${presetFilePath}`));
|
|
16
|
+
const preset = _readAppPreset(presetFilePath);
|
|
17
|
+
_validateAppPreset(preset, 'rollback');
|
|
18
|
+
finalPermissions = preset.rollback.consent.grantPermissions;
|
|
19
|
+
console.log(chalk.blueBright(`Permissions to be granted upon rollback: [${finalPermissions.join(', ')}]`));
|
|
20
|
+
} else {
|
|
21
|
+
spinner.start(`Analyzing latest backup for '${appName}'...`);
|
|
22
|
+
const analysis = await apiClient.analyzeBackup(serverUrl, appName);
|
|
23
|
+
if (analysis.status !== 'success') throw new Error(analysis.error || 'Failed to analyze backup.');
|
|
24
|
+
|
|
25
|
+
const currentPermsResponse = await apiClient.getAppPermissions(serverUrl, appName);
|
|
26
|
+
if (currentPermsResponse.status !== 'success') throw new Error(currentPermsResponse.error || 'Failed to get current permissions.');
|
|
27
|
+
spinner.succeed('Backup analysis complete.');
|
|
28
|
+
|
|
29
|
+
console.log(chalk.blueBright(`\nThis will roll back '${appName}' to version ${analysis.version}.`));
|
|
30
|
+
|
|
31
|
+
const { proceed } = await inquirer.prompt([{
|
|
32
|
+
type: 'confirm',
|
|
33
|
+
name: 'proceed',
|
|
34
|
+
message: 'Are you sure you want to proceed with the rollback?',
|
|
35
|
+
default: false
|
|
36
|
+
}]);
|
|
37
|
+
if (!proceed) throw new Error('Rollback cancelled by user.');
|
|
38
|
+
finalPermissions = await _getRollbackPermissions(analysis.permissions, currentPermsResponse.grantedPermissions);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
spinner.start(`Executing rollback for '${appName}'...`);
|
|
42
|
+
const result = await apiClient.rollbackApp(serverUrl, appName, finalPermissions);
|
|
43
|
+
|
|
44
|
+
if (result.status !== 'success') {
|
|
45
|
+
throw new Error(result.message || 'Server responded with an unknown error.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
spinner.succeed(chalk.bgGreen(`✅ Success!`), chalk.blueBright(`App '${appName}' has been rolled back.`));
|
|
49
|
+
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner.fail(chalk.blueBright('Rollback failed.'));
|
|
52
|
+
if (err.errors) { //for AggregateError
|
|
53
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
54
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
55
|
+
} else {
|
|
56
|
+
const message = _getHttpClientErrorMessage(err);
|
|
57
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(message));
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { rollbackApp };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Dynamically and safely require the correct service library based on the OS.
|
|
5
|
+
let Service;
|
|
6
|
+
const platform = os.platform();
|
|
7
|
+
let platformName = '';
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
if (platform === 'win32') {
|
|
11
|
+
Service = require('node-windows').Service;
|
|
12
|
+
platformName = 'Windows';
|
|
13
|
+
} else if (platform === 'linux') {
|
|
14
|
+
Service = require('node-linux').Service;
|
|
15
|
+
platformName = 'Linux (systemd)';
|
|
16
|
+
} else if (platform === 'darwin') {
|
|
17
|
+
Service = require('node-mac').Service;
|
|
18
|
+
platformName = 'macOS (launchd)';
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// This will catch the error if the optional dependency was not installed.
|
|
22
|
+
Service = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a configured Service instance.
|
|
28
|
+
* @returns {Service|null}
|
|
29
|
+
*/
|
|
30
|
+
function getService() {
|
|
31
|
+
if (!Service) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const { getProjectRoot } = require('./utils');
|
|
35
|
+
const projectRoot = getProjectRoot();
|
|
36
|
+
const scriptPath = path.join(projectRoot, 'start.js');
|
|
37
|
+
|
|
38
|
+
return new Service({
|
|
39
|
+
name: 'Gingee Server',
|
|
40
|
+
description: `Gingee server instance running at ${projectRoot}`,
|
|
41
|
+
script: scriptPath
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function install() {
|
|
46
|
+
const { default: chalk } = await import('chalk');
|
|
47
|
+
const svc = getService();
|
|
48
|
+
|
|
49
|
+
if (!svc) {
|
|
50
|
+
console.error(chalk.bgRed('Error:'), chalk.blueBright('Service installation is not supported on this operating system.'));
|
|
51
|
+
console.log(chalk.yellow('For other systems, we recommend using a process manager like PM2.'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
svc.on('install', () => {
|
|
56
|
+
console.log(chalk.bgGreen('✅ Success!'), chalk.blueBright(`Service installed for ${platformName}.`));
|
|
57
|
+
console.log('Starting the service...');
|
|
58
|
+
svc.start();
|
|
59
|
+
console.log(chalk.blueBright('Service started. Your Gingee server is now running in the background.'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
svc.on('alreadyinstalled', () => {
|
|
63
|
+
console.log(chalk.yellow('This service is already installed.'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
svc.on('invalidinstallation', () => {
|
|
67
|
+
console.error(chalk.bgRed('Error: Invalid service installation. Do you have the necessary permissions?'));
|
|
68
|
+
console.log(chalk.yellow('On Linux/macOS, you may need to run this command with `sudo`.'));
|
|
69
|
+
console.log(chalk.yellow('On Windows, you may need to run from an Administrator terminal.'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(chalk.blue(`Attempting to install the Gingee service for ${platformName}...`));
|
|
73
|
+
svc.install();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- We can now implement the other service commands ---
|
|
77
|
+
|
|
78
|
+
async function uninstall() {
|
|
79
|
+
const { default: chalk } = await import('chalk');
|
|
80
|
+
const svc = getService();
|
|
81
|
+
if (!svc) {
|
|
82
|
+
console.error(chalk.bgRed('Error:'), chalk.blueBright('Service installation is not supported on this operating system.'));
|
|
83
|
+
console.log(chalk.yellow('For other systems, we recommend using a process manager like PM2.'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
svc.on('uninstall', () => {
|
|
88
|
+
console.log(chalk.bgGreen('✅ Success!'), chalk.blueBright('Service uninstalled.'));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(chalk.blue('Attempting to uninstall the service...'));
|
|
92
|
+
svc.uninstall();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function start() {
|
|
96
|
+
const { default: chalk } = await import('chalk');
|
|
97
|
+
const svc = getService();
|
|
98
|
+
if (!svc) {
|
|
99
|
+
console.error(chalk.bgRed('Error:'), chalk.blueBright('Service installation is not supported on this operating system.'));
|
|
100
|
+
console.log(chalk.yellow('For other systems, we recommend using a process manager like PM2.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
svc.on('start', () => console.log(chalk.bgGreen('Service started.')));
|
|
105
|
+
console.log(chalk.blue('Attempting to start the service...'));
|
|
106
|
+
svc.start();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function stop() {
|
|
110
|
+
const { default: chalk } = await import('chalk');
|
|
111
|
+
const svc = getService();
|
|
112
|
+
if (!svc) {
|
|
113
|
+
console.error(chalk.bgRed('Error:'), chalk.blueBright('Service installation is not supported on this operating system.'));
|
|
114
|
+
console.log(chalk.yellow('For other systems, we recommend using a process manager like PM2.'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
svc.on('stop', () => console.log(chalk.yellow('Service stopped.')));
|
|
119
|
+
console.log(chalk.blue('Attempting to stop the service...'));
|
|
120
|
+
svc.stop();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { install, uninstall, start, stop };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { _getHttpClientErrorMessage, _unzipBuffer, _repackApp, _getUpgradePermissions, _getDbRequirements, _readAppPreset, _validateAppPreset, _substituteEnvVars } = require('./installerUtils');
|
|
5
|
+
const apiClient = require('./apiClient');
|
|
6
|
+
|
|
7
|
+
async function upgradeApp(options) {
|
|
8
|
+
const { default: chalk } = await import('chalk');
|
|
9
|
+
const { default: ora } = await import('ora');
|
|
10
|
+
const tempDir = path.join(os.tmpdir(), `gingee-upgrade-${Date.now()}`);
|
|
11
|
+
|
|
12
|
+
const { serverUrl, appName, ginPath: ginFilePath, file: presetFilePath } = options;
|
|
13
|
+
let finalPermissions, finalDbConfig;
|
|
14
|
+
const spinner = ora();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(ginFilePath)) {
|
|
19
|
+
throw new Error(`Package file not found at: ${ginFilePath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
spinner.start(`Reading new package ${path.basename(ginFilePath)}...`);
|
|
23
|
+
const packageBuffer = fs.readFileSync(ginFilePath);
|
|
24
|
+
await fs.ensureDir(tempDir);
|
|
25
|
+
const unpackedPath = path.join(tempDir, 'unpacked');
|
|
26
|
+
await _unzipBuffer(packageBuffer, unpackedPath);
|
|
27
|
+
spinner.succeed('New package unpacked.');
|
|
28
|
+
|
|
29
|
+
if (presetFilePath) {
|
|
30
|
+
// --- NON-INTERACTIVE MODE ---
|
|
31
|
+
console.log(chalk.blueBright(`Running in non-interactive mode using preset file: ${presetFilePath}`));
|
|
32
|
+
const preset = _readAppPreset(presetFilePath);
|
|
33
|
+
_validateAppPreset(preset, 'upgrade');
|
|
34
|
+
finalPermissions = preset.upgrade.consent.grantPermissions;
|
|
35
|
+
finalDbConfig = _substituteEnvVars(preset.upgrade.config.db);
|
|
36
|
+
} else {
|
|
37
|
+
spinner.start(`Fetching current permissions for '${appName}'...`);
|
|
38
|
+
const currentPermsResponse = await apiClient.getAppPermissions(serverUrl, appName);
|
|
39
|
+
if (currentPermsResponse.status !== 'success') {
|
|
40
|
+
throw new Error(currentPermsResponse.error || 'Could not fetch current permissions.');
|
|
41
|
+
}
|
|
42
|
+
spinner.succeed('Current permissions fetched.');
|
|
43
|
+
|
|
44
|
+
finalPermissions = await _getUpgradePermissions(unpackedPath, currentPermsResponse.grantedPermissions);
|
|
45
|
+
|
|
46
|
+
finalDbConfig = await _getDbRequirements(unpackedPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
spinner.start('Applying configuration and repacking...');
|
|
50
|
+
const appJsonPath = path.join(unpackedPath, 'box', 'app.json');
|
|
51
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
52
|
+
if (finalDbConfig.length > 0) appJson.db = finalDbConfig;
|
|
53
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
54
|
+
|
|
55
|
+
const finalPackageBuffer = await _repackApp(unpackedPath);
|
|
56
|
+
spinner.succeed('Configuration applied and package repacked.');
|
|
57
|
+
|
|
58
|
+
spinner.start(`Upgrading '${appName}' on ${serverUrl}...`);
|
|
59
|
+
const result = await apiClient.upgradeApp(serverUrl, appName, `${appName}.gin`, finalPackageBuffer, finalPermissions);
|
|
60
|
+
|
|
61
|
+
if (result.status !== 'success') {
|
|
62
|
+
throw new Error(result.message || 'Server responded with an unknown error.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
spinner.succeed(chalk.bgGreen(`✅ Success!`), chalk.blueBright(result.message || `App '${appName}' upgraded.`));
|
|
66
|
+
|
|
67
|
+
} catch (err) {
|
|
68
|
+
spinner.fail(chalk.bgRed('Upgrade failed.'));
|
|
69
|
+
if (err.errors) { //for AggregateError
|
|
70
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
71
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
72
|
+
} else {
|
|
73
|
+
const message = _getHttpClientErrorMessage(err);
|
|
74
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${message}`));
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { upgradeApp };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const {
|
|
6
|
+
_unzipBuffer,
|
|
7
|
+
_repackApp,
|
|
8
|
+
_getUpgradePermissions,
|
|
9
|
+
_getDbRequirements,
|
|
10
|
+
_resolveStoreUrl,
|
|
11
|
+
_resolveDownloadUrl
|
|
12
|
+
} = require('./installerUtils');
|
|
13
|
+
const apiClient = require('./apiClient');
|
|
14
|
+
|
|
15
|
+
async function upgradeStoreApp(appName, options) {
|
|
16
|
+
const { default: chalk } = await import('chalk');
|
|
17
|
+
const { default: ora } = await import('ora');
|
|
18
|
+
const spinner = ora();
|
|
19
|
+
const tempDir = path.join(os.tmpdir(), `gingee-upgrade-store-${Date.now()}`);
|
|
20
|
+
|
|
21
|
+
const { gStoreUrl, serverUrl } = options;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// 1. Resolve URL and fetch the store manifest
|
|
25
|
+
const resolvedStoreUrl = _resolveStoreUrl(gStoreUrl);
|
|
26
|
+
spinner.start(`Fetching manifest from ${resolvedStoreUrl}...`);
|
|
27
|
+
|
|
28
|
+
const manifestResponse = await axios.get(resolvedStoreUrl);
|
|
29
|
+
const appConfig = manifestResponse.data.apps.find(a => a.name === appName);
|
|
30
|
+
if (!appConfig) {
|
|
31
|
+
throw new Error(`App '${appName}' not found in the store manifest.`);
|
|
32
|
+
}
|
|
33
|
+
spinner.succeed('Found app in store manifest.');
|
|
34
|
+
|
|
35
|
+
// 2. Resolve the download URL and download the new package
|
|
36
|
+
const downloadUrl = _resolveDownloadUrl(resolvedStoreUrl, appConfig.download_url);
|
|
37
|
+
spinner.start(`Downloading new package from ${downloadUrl}...`);
|
|
38
|
+
const downloadResponse = await axios.get(downloadUrl, { responseType: 'arraybuffer' });
|
|
39
|
+
const packageBuffer = downloadResponse.data;
|
|
40
|
+
spinner.succeed('New package downloaded.');
|
|
41
|
+
|
|
42
|
+
// 3. Unpack the new package to a temporary directory
|
|
43
|
+
await fs.ensureDir(tempDir);
|
|
44
|
+
const unpackedPath = path.join(tempDir, 'unpacked');
|
|
45
|
+
await _unzipBuffer(packageBuffer, unpackedPath);
|
|
46
|
+
|
|
47
|
+
// 4. Fetch the currently granted permissions for the installed app
|
|
48
|
+
spinner.start(`Fetching current permissions for '${appName}'...`);
|
|
49
|
+
const currentPermsResponse = await apiClient.getAppPermissions(serverUrl, appName);
|
|
50
|
+
if (currentPermsResponse.status !== 'success') {
|
|
51
|
+
throw new Error(currentPermsResponse.error || 'Could not fetch current permissions.');
|
|
52
|
+
}
|
|
53
|
+
spinner.succeed('Current permissions fetched.');
|
|
54
|
+
|
|
55
|
+
// 5. Run the interactive permission comparison and consent prompt
|
|
56
|
+
const grantedPermissions = await _getUpgradePermissions(unpackedPath, currentPermsResponse.grantedPermissions);
|
|
57
|
+
|
|
58
|
+
// 6. Run the interactive database configuration prompt
|
|
59
|
+
const dbConfigUpdates = await _getDbRequirements(unpackedPath);
|
|
60
|
+
|
|
61
|
+
// 7. Repackage the app with the new configuration
|
|
62
|
+
spinner.start('Applying configuration and repacking...');
|
|
63
|
+
const appJsonPath = path.join(unpackedPath, 'box', 'app.json');
|
|
64
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
65
|
+
if (dbConfigUpdates.length > 0) {
|
|
66
|
+
appJson.db = dbConfigUpdates;
|
|
67
|
+
}
|
|
68
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
69
|
+
|
|
70
|
+
const finalPackageBuffer = await _repackApp(unpackedPath);
|
|
71
|
+
spinner.succeed('Configuration applied and package repacked.');
|
|
72
|
+
|
|
73
|
+
// 8. Execute the upgrade via the API client
|
|
74
|
+
spinner.start(`Upgrading '${appName}' on ${serverUrl}...`);
|
|
75
|
+
const result = await apiClient.upgradeApp(serverUrl, appName, `${appName}.gin`, finalPackageBuffer, grantedPermissions);
|
|
76
|
+
|
|
77
|
+
if (result.status !== 'success') {
|
|
78
|
+
throw new Error(result.message || 'Server responded with an unknown error.');
|
|
79
|
+
}
|
|
80
|
+
spinner.succeed(chalk.bgGreen(`✅ Success! App '${appName}' upgraded on ${serverUrl}.`));
|
|
81
|
+
|
|
82
|
+
} catch (err) {
|
|
83
|
+
spinner.fail(chalk.bgRed('Upgrade failed.'));
|
|
84
|
+
if (err.errors) { //for AggregateError
|
|
85
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
86
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
87
|
+
} else {
|
|
88
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${err.message}`));
|
|
89
|
+
}
|
|
90
|
+
process.exit(1);
|
|
91
|
+
} finally {
|
|
92
|
+
// 9. Cleanup
|
|
93
|
+
await fs.remove(tempDir);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { upgradeStoreApp };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { error } = require('console');
|
|
5
|
+
|
|
6
|
+
function getProjectRoot() {
|
|
7
|
+
const projectRoot = process.cwd();
|
|
8
|
+
if (!fs.existsSync(path.join(projectRoot, 'gingee.json'))) {
|
|
9
|
+
throw new Error('This command must be run from the root of a Gingee project.');
|
|
10
|
+
}
|
|
11
|
+
return projectRoot;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads the gingee.json file and resolves the correct, absolute web_root path.
|
|
16
|
+
* Correctly handles both relative and absolute paths from the config.
|
|
17
|
+
* @param {string} projectRoot - The absolute path to the project root.
|
|
18
|
+
* @returns {string} The absolute path to the web root directory.
|
|
19
|
+
*/
|
|
20
|
+
function getWebRoot(projectRoot) {
|
|
21
|
+
const configPath = path.join(projectRoot, 'gingee.json');
|
|
22
|
+
const config = fs.readJsonSync(configPath);
|
|
23
|
+
const configWebPath = config.web_root || './web';
|
|
24
|
+
|
|
25
|
+
// --- THIS IS THE EXPLICIT AND CORRECT LOGIC ---
|
|
26
|
+
if (path.isAbsolute(configWebPath)) {
|
|
27
|
+
// If the path is already absolute, use it directly.
|
|
28
|
+
return configWebPath;
|
|
29
|
+
} else {
|
|
30
|
+
// If it's relative, resolve it from the project's root.
|
|
31
|
+
return path.resolve(projectRoot, configWebPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { getProjectRoot, getWebRoot };
|