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/commands/init.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const argon2 = require('argon2');
|
|
5
|
+
const { _unzipBuffer } = require('./installerUtils');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async function init(projectName) {
|
|
9
|
+
const { default: ora } = await import('ora');
|
|
10
|
+
const { default: chalk } = await import('chalk');
|
|
11
|
+
const { default: inquirer } = await import('inquirer');
|
|
12
|
+
|
|
13
|
+
const spinner = ora();
|
|
14
|
+
try {
|
|
15
|
+
console.log(chalk.blueBright('🚀 Welcome to Gingee! Let\'s create your new project.'));
|
|
16
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
17
|
+
|
|
18
|
+
let currentPath = process.cwd();
|
|
19
|
+
while (currentPath !== path.parse(currentPath).root) {
|
|
20
|
+
if (fs.existsSync(path.join(currentPath, 'gingee.json'))) {
|
|
21
|
+
throw new Error(`Command cannot be run inside an existing Gingee project.\nDetected project root at: ${currentPath}`);
|
|
22
|
+
}
|
|
23
|
+
currentPath = path.dirname(currentPath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(projectPath)) {
|
|
27
|
+
throw new Error(`Directory '${projectName}' already exists. Please choose another name.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const answers = await inquirer.prompt([
|
|
31
|
+
{ type: 'input', name: 'adminUser', message: 'Enter a username for the Glade admin panel:', default: 'admin' },
|
|
32
|
+
{
|
|
33
|
+
type: 'password', name: 'adminPass', message: 'Enter a password for the Glade admin:', mask: '*', validate: input => {
|
|
34
|
+
if (!input || input.length < 8) {
|
|
35
|
+
return 'Password must be at least 8 characters long.';
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'password',
|
|
42
|
+
name: 'confirmPassword',
|
|
43
|
+
message: 'Confirm the new password:',
|
|
44
|
+
mask: '*'
|
|
45
|
+
},
|
|
46
|
+
{ type: 'confirm', name: 'installDeps', message: 'Install npm dependencies automatically?', default: true },
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
if (answers.adminPass !== answers.confirmPassword) {
|
|
50
|
+
throw new Error("Passwords do not match. Please try again.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!answers.adminPass) {
|
|
54
|
+
throw new Error("Admin password cannot be empty.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
spinner.start('Scaffolding project files...');
|
|
58
|
+
fs.mkdirSync(projectPath);
|
|
59
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'project');
|
|
60
|
+
fs.copySync(templatePath, projectPath);
|
|
61
|
+
|
|
62
|
+
const pkgJsonPath = path.join(projectPath, 'package.json');
|
|
63
|
+
const pkgJson = fs.readJsonSync(pkgJsonPath);
|
|
64
|
+
pkgJson.name = projectName.toLowerCase().replace(/\s+/g, '-');
|
|
65
|
+
fs.writeJsonSync(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
66
|
+
fs.mkdirSync(path.join(projectPath, 'settings', 'ssl'), { recursive: true });
|
|
67
|
+
fs.mkdirSync(path.join(projectPath, 'backups'), { recursive: true });
|
|
68
|
+
fs.mkdirSync(path.join(projectPath, 'logs'), { recursive: true });
|
|
69
|
+
fs.mkdirSync(path.join(projectPath, 'temp'), { recursive: true });
|
|
70
|
+
|
|
71
|
+
spinner.succeed('Project files scaffolded.');
|
|
72
|
+
spinner.start('Installing `glade` admin panel...');
|
|
73
|
+
|
|
74
|
+
// Find the glade.gin file using require.resolve, which is robust.
|
|
75
|
+
// It looks for the 'gingee' package in the CLI's own node_modules.
|
|
76
|
+
const gladeGinPath = require.resolve('gingee/templates/glade.gin');
|
|
77
|
+
const gladePackageBuffer = fs.readFileSync(gladeGinPath);
|
|
78
|
+
const gladeDestPath = path.join(projectPath, 'web', 'glade');
|
|
79
|
+
|
|
80
|
+
await _unzipBuffer(gladePackageBuffer, gladeDestPath);
|
|
81
|
+
spinner.succeed('`glade` admin panel installed.');
|
|
82
|
+
spinner.start('Configuring admin credentials...');
|
|
83
|
+
|
|
84
|
+
// Use the CLI's own argon2 dependency to hash the password.
|
|
85
|
+
const passwordHash = await argon2.hash(answers.adminPass);
|
|
86
|
+
const gladeAppConfigPath = path.join(gladeDestPath, 'box', 'app.json');
|
|
87
|
+
const gladeAppConfig = fs.readJsonSync(gladeAppConfigPath);
|
|
88
|
+
gladeAppConfig.env.ADMIN_USERNAME = answers.adminUser;
|
|
89
|
+
gladeAppConfig.env.ADMIN_PASSWORD_HASH = passwordHash;
|
|
90
|
+
fs.writeJsonSync(gladeAppConfigPath, gladeAppConfig, { spaces: 2 });
|
|
91
|
+
spinner.succeed('Admin credentials configured securely.');
|
|
92
|
+
|
|
93
|
+
spinner.start('Granting default permissions to Glade...');
|
|
94
|
+
const permissionsFilePath = path.join(projectPath, 'settings', 'permissions.json');
|
|
95
|
+
const permissionsConfig = {
|
|
96
|
+
"glade": {
|
|
97
|
+
"granted": ["platform", "fs"]
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
fs.writeJsonSync(permissionsFilePath, permissionsConfig, { spaces: 2 });
|
|
101
|
+
spinner.succeed('Default permissions for Glade configured.');
|
|
102
|
+
|
|
103
|
+
if (answers.installDeps) {
|
|
104
|
+
spinner.start('Installing dependencies with npm (this may take a moment)...');
|
|
105
|
+
// Run `npm install` in the new project's directory
|
|
106
|
+
execSync('npm install', { cwd: projectPath, stdio: 'ignore' });
|
|
107
|
+
spinner.succeed('Dependencies installed.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(chalk.bgGreen(`\n✅ Success!`), chalk.blueBright(`Your Gingee project "${projectName}" is ready.`));
|
|
111
|
+
console.log(`\nTo get started, run the following commands:\n`);
|
|
112
|
+
console.log(chalk.blueBright(` cd ${projectName}`));
|
|
113
|
+
//console.log(chalk.blueBright(` git init && git add . && git commit -m "Initial commit"`));
|
|
114
|
+
console.log(chalk.blueBright(` npm run start`));
|
|
115
|
+
|
|
116
|
+
console.log(`\n\nFor production, you have two options:`);
|
|
117
|
+
console.log(chalk.cyan(` 1. Native Service: sudo gingee-cli service install`));
|
|
118
|
+
console.log(chalk.cyan(` 2. PM2: pm2 start`));
|
|
119
|
+
console.log(` (Customize your PM2 deployment in ecosystem.config.js)`);
|
|
120
|
+
|
|
121
|
+
} catch (err) {
|
|
122
|
+
spinner.fail(chalk.bgRed('ERROR!: '));
|
|
123
|
+
if (err.errors) { //for AggregateError
|
|
124
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
125
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
126
|
+
} else {
|
|
127
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${err.message}`));
|
|
128
|
+
}
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { init };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {
|
|
4
|
+
_unzipBuffer,
|
|
5
|
+
_repackApp,
|
|
6
|
+
_getPermissions,
|
|
7
|
+
_getDbRequirements,
|
|
8
|
+
_getHttpClientErrorMessage,
|
|
9
|
+
_readAppPreset,
|
|
10
|
+
_validateAppPreset,
|
|
11
|
+
_substituteEnvVars
|
|
12
|
+
} = require('./installerUtils');
|
|
13
|
+
const apiClient = require('./apiClient');
|
|
14
|
+
|
|
15
|
+
async function installApp(options) {
|
|
16
|
+
const { default: chalk } = await import('chalk');
|
|
17
|
+
const { default: ora } = await import('ora');
|
|
18
|
+
const spinner = ora('Preparing to install application...').start();
|
|
19
|
+
const tempDir = path.join(require('os').tmpdir(), `gingee-install-local-${Date.now()}`);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const { serverUrl = 'http://localhost:7070', appName, ginPath: ginFilePath, file: presetFilePath } = options;
|
|
23
|
+
let finalPermissions, finalDbConfig;
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(ginFilePath)) {
|
|
26
|
+
throw new Error(`Package file not found at: ${ginFilePath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.start(`Reading package ${path.basename(ginFilePath)}...`);
|
|
30
|
+
const packageBuffer = fs.readFileSync(ginFilePath);
|
|
31
|
+
|
|
32
|
+
// Unpack and get user consent/config
|
|
33
|
+
await fs.ensureDir(tempDir);
|
|
34
|
+
const unpackedPath = path.join(tempDir, 'unpacked');
|
|
35
|
+
await _unzipBuffer(packageBuffer, unpackedPath);
|
|
36
|
+
spinner.succeed('Package read.');
|
|
37
|
+
|
|
38
|
+
if(presetFilePath) {
|
|
39
|
+
console.log(chalk.blueBright(`Running in non-interactive mode using preset file: ${presetFilePath}`));
|
|
40
|
+
const preset = _readAppPreset(presetFilePath);
|
|
41
|
+
_validateAppPreset(preset, 'install'); // Assumes 'install' key exists in preset
|
|
42
|
+
finalPermissions = preset.install.consent.grantPermissions;
|
|
43
|
+
finalDbConfig = _substituteEnvVars(preset.install.config.db);
|
|
44
|
+
}else{
|
|
45
|
+
finalPermissions = await _getPermissions(unpackedPath);
|
|
46
|
+
finalDbConfig = await _getDbRequirements(unpackedPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Modify and Repack
|
|
50
|
+
spinner.start('Applying configuration and repacking...');
|
|
51
|
+
const appJsonPath = path.join(unpackedPath, 'box', 'app.json');
|
|
52
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
53
|
+
if (finalDbConfig.length > 0) {
|
|
54
|
+
appJson.db = finalDbConfig;
|
|
55
|
+
}
|
|
56
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
57
|
+
|
|
58
|
+
const finalPackageBuffer = await _repackApp(unpackedPath);
|
|
59
|
+
spinner.succeed('Configuration applied and package repacked.');
|
|
60
|
+
|
|
61
|
+
// Infer appName from the filename without the .gin extension
|
|
62
|
+
spinner.text = `Installing app '${appName}' to server ${serverUrl}...`;
|
|
63
|
+
|
|
64
|
+
const result = await apiClient.installApp(serverUrl, appName, path.basename(ginFilePath), finalPackageBuffer, finalPermissions);
|
|
65
|
+
if (result.status !== 'success') {
|
|
66
|
+
throw new Error(result.message || 'Server responded with an unknown error.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const successMsg = chalk.bgGreen('✅ Success!') + chalk.blueBright(` App '${appName}' installed.`);
|
|
70
|
+
spinner.succeed(successMsg);
|
|
71
|
+
|
|
72
|
+
} catch (err) {
|
|
73
|
+
spinner.fail(chalk.bgRed('Installation failed.'));
|
|
74
|
+
if (err.errors) { //for AggregateError
|
|
75
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
76
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
77
|
+
} else {
|
|
78
|
+
const message = _getHttpClientErrorMessage(err);
|
|
79
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${message}`));
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { installApp };
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
_getPermissions,
|
|
9
|
+
_getDbRequirements,
|
|
10
|
+
_resolveStoreUrl,
|
|
11
|
+
_resolveDownloadUrl,
|
|
12
|
+
_getHttpClientErrorMessage
|
|
13
|
+
} = require('./installerUtils');
|
|
14
|
+
const apiClient = require('./apiClient');
|
|
15
|
+
|
|
16
|
+
async function installStoreApp(appName, options) {
|
|
17
|
+
const { default: chalk } = await import('chalk');
|
|
18
|
+
const { default: ora } = await import('ora');
|
|
19
|
+
const spinner = ora();
|
|
20
|
+
const tempDir = path.join(os.tmpdir(), `gingee-install-${Date.now()}`);
|
|
21
|
+
|
|
22
|
+
const { gStoreUrl, serverUrl } = options;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Step 1: Resolve the manifest URL using the new utility
|
|
26
|
+
const resolvedStoreUrl = _resolveStoreUrl(gStoreUrl);
|
|
27
|
+
spinner.start(`Fetching manifest from ${resolvedStoreUrl}...`);
|
|
28
|
+
|
|
29
|
+
const manifestResponse = await axios.get(resolvedStoreUrl);
|
|
30
|
+
const appConfig = manifestResponse.data.apps.find(a => a.name === appName);
|
|
31
|
+
if (!appConfig) {
|
|
32
|
+
throw new Error(`App '${appName}' not found in the store manifest.`);
|
|
33
|
+
}
|
|
34
|
+
spinner.succeed('Found app in store manifest.');
|
|
35
|
+
|
|
36
|
+
// Step 2: Resolve the download URL using the new utility
|
|
37
|
+
const downloadUrl = _resolveDownloadUrl(resolvedStoreUrl, appConfig.download_url);
|
|
38
|
+
|
|
39
|
+
spinner.start(`Downloading package from ${downloadUrl}...`);
|
|
40
|
+
const downloadResponse = await axios.get(downloadUrl, { responseType: 'arraybuffer' });
|
|
41
|
+
const packageBuffer = downloadResponse.data;
|
|
42
|
+
spinner.succeed('Package downloaded.');
|
|
43
|
+
|
|
44
|
+
// Steps 3, 4, and 5 (Unpack, get user consent, repack, and install) are now identical
|
|
45
|
+
// to the logic we've already defined, as they operate on the buffer.
|
|
46
|
+
await fs.ensureDir(tempDir);
|
|
47
|
+
const unpackedPath = path.join(tempDir, 'unpacked');
|
|
48
|
+
await _unzipBuffer(packageBuffer, unpackedPath);
|
|
49
|
+
|
|
50
|
+
const grantedPermissions = await _getPermissions(unpackedPath);
|
|
51
|
+
const dbConfigUpdates = await _getDbRequirements(unpackedPath);
|
|
52
|
+
|
|
53
|
+
spinner.start('Applying configuration and repacking...');
|
|
54
|
+
const appJsonPath = path.join(unpackedPath, 'box', 'app.json');
|
|
55
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
56
|
+
if (dbConfigUpdates.length > 0) {
|
|
57
|
+
appJson.db = dbConfigUpdates;
|
|
58
|
+
}
|
|
59
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
60
|
+
|
|
61
|
+
const finalPackageBuffer = await _repackApp(unpackedPath);
|
|
62
|
+
spinner.succeed('Configuration applied and package repacked.');
|
|
63
|
+
|
|
64
|
+
spinner.start(`Installing '${appName}' on ${serverUrl}...`);
|
|
65
|
+
const result = await apiClient.installApp(serverUrl, appName, `${appName}.gin`, finalPackageBuffer, grantedPermissions);
|
|
66
|
+
if (result.status !== 'success') {
|
|
67
|
+
throw new Error(result.message || 'Server responded with an unknown error.');
|
|
68
|
+
}
|
|
69
|
+
spinner.succeed(chalk.bgGreen(`✅ Success!`));
|
|
70
|
+
console.log(chalk.blueBright(` App '${appName}' installed on ${serverUrl}.`));
|
|
71
|
+
|
|
72
|
+
} catch (err) {
|
|
73
|
+
spinner.fail(chalk.bgRed('Installation failed.'));
|
|
74
|
+
if (err.errors) { //for AggregateError
|
|
75
|
+
const messages = err.errors.map(e => e.message).join('\n');
|
|
76
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${messages}`));
|
|
77
|
+
} else {
|
|
78
|
+
const message = _getHttpClientErrorMessage(err);
|
|
79
|
+
console.error(chalk.bgRed(`Error: `), chalk.blueBright(`${message}`));
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
} finally {
|
|
83
|
+
await fs.remove(tempDir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { installStoreApp };
|