gingee-cli 1.0.1 → 1.0.3

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.
@@ -24,7 +24,10 @@ async function addApp(appName) {
24
24
  type: 'list',
25
25
  name: 'type',
26
26
  message: 'What type of app is this?',
27
- choices: ['MPA', 'SPA'],
27
+ choices: [
28
+ { name: 'MPA (Multi-Page App, Traditional Web App)', value: 'MPA' },
29
+ { name: 'SPA (Single Page App, React/Vue/etc.)', value: 'SPA' }
30
+ ],
28
31
  default: 'MPA',
29
32
  },
30
33
  {
@@ -41,27 +44,27 @@ async function addApp(appName) {
41
44
  when: (ans) => ans.configureDb,
42
45
  },
43
46
  {
44
- type: 'input', name: 'dbName', message: 'Connection Name (e.g., main_db):', default: 'main_db',
47
+ type: 'input', name: 'dbName', message: 'Connection Name (e.g., main_db):', default: 'main_db',
45
48
  when: (ans) => ans.configureDb,
46
49
  },
47
50
  {
48
- type: 'input', name: 'dbHost', message: 'Database Host:', default: 'localhost',
51
+ type: 'input', name: 'dbHost', message: 'Database Host:', default: 'localhost',
49
52
  when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
50
53
  },
51
54
  {
52
- type: 'input', name: 'dbUser', message: 'Database User:',
55
+ type: 'input', name: 'dbUser', message: 'Database User:',
53
56
  when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
54
57
  },
55
58
  {
56
- type: 'password', name: 'dbPass', message: 'Database Password:', mask: '*',
59
+ type: 'password', name: 'dbPass', message: 'Database Password:', mask: '*',
57
60
  when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
58
61
  },
59
62
  {
60
- type: 'input', name: 'dbDatabase', message: 'Database Name:',
63
+ type: 'input', name: 'dbDatabase', message: 'Database Name:',
61
64
  when: (ans) => ans.configureDb && ans.dbType !== 'sqlite',
62
65
  },
63
66
  {
64
- type: 'input', name: 'dbFile', message: 'Database File Path (relative to box folder):', default: 'data/app.db',
67
+ type: 'input', name: 'dbFile', message: 'Database File Path (relative to box folder):', default: 'data/app.db',
65
68
  when: (ans) => ans.dbType === 'sqlite',
66
69
  },
67
70
  {
@@ -72,45 +75,100 @@ async function addApp(appName) {
72
75
  },
73
76
  ]);
74
77
 
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 });
78
+ if (answers.type === 'SPA') {
79
+ // --- Scaffolding files and folders for SPA ---
80
+ console.log('Scaffolding SPA app structure...');
81
+ const boxPath = path.join(appPath, 'box');
82
+ const apiPath = path.join(boxPath, 'api');
83
+ fs.ensureDirSync(apiPath);
84
+
85
+ // Create sample API endpoint
86
+ const sampleApiContent = `module.exports = async function() {\n await gingee(async ($g) => {\n $g.response.send({ message: 'Hello from your Gingee SPA backend!' });\n });\n};`;
87
+ fs.writeFileSync(path.join(apiPath, 'hello.js'), sampleApiContent);
88
+
89
+ // Create the pre-configured app.json from template
90
+ const templatePath = path.join(__dirname, '..', 'templates', 'spa-generic', 'app.json');
91
+ const appConfig = fs.readJsonSync(templatePath);
92
+ appConfig.name = appName;
93
+
94
+ if (answers.configureDb) {
95
+ appConfig.db = [{
96
+ type: answers.dbType,
97
+ name: answers.dbName,
98
+ host: answers.dbHost,
99
+ user: answers.dbUser,
100
+ password: answers.dbPass,
101
+ database: answers.dbType === 'sqlite' ? answers.dbFile : answers.dbDatabase,
102
+ }];
103
+ }
104
+ if (answers.configureJwt) {
105
+ appConfig.jwt_secret = crypto.randomBytes(32).toString('hex');
106
+ }
107
+ fs.writeJsonSync(path.join(boxPath, 'app.json'), appConfig, { spaces: 2 });
108
+
109
+ } else {
99
110
 
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);
111
+ // --- Scaffold the files and folders for MPA ---
112
+ console.log('Scaffolding MPA app structure...');
113
+ const boxPath = path.join(appPath, 'box');
114
+ fs.ensureDirSync(boxPath);
115
+ fs.ensureDirSync(path.join(appPath, 'css'));
116
+ fs.ensureDirSync(path.join(appPath, 'images'));
117
+ fs.ensureDirSync(path.join(appPath, 'scripts'));
103
118
 
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);
119
+ // --- Create app.json ---
120
+ const appConfig = { name: appName, version: '1.0.0', type: answers.type };
121
+ if (answers.configureDb) {
122
+ appConfig.db = [{
123
+ type: answers.dbType,
124
+ name: answers.dbName,
125
+ host: answers.dbHost,
126
+ user: answers.dbUser,
127
+ password: answers.dbPass,
128
+ database: answers.dbType === 'sqlite' ? answers.dbFile : answers.dbDatabase,
129
+ }];
130
+ }
131
+ if (answers.configureJwt) {
132
+ appConfig.jwt_secret = crypto.randomBytes(32).toString('hex');
133
+ }
134
+ fs.writeJsonSync(path.join(boxPath, 'app.json'), appConfig, { spaces: 2 });
107
135
 
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);
136
+ // --- Create hello.js ---
137
+ const helloScriptContent = `module.exports = async function() {\n await gingee(async ($g) => {\n $g.response.send({ message: 'Hello from the ${appName} server script!' });\n });\n};`;
138
+ fs.writeFileSync(path.join(boxPath, 'hello.js'), helloScriptContent);
139
+
140
+ // --- Create index.html ---
141
+ 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>`;
142
+ fs.writeFileSync(path.join(appPath, 'index.html'), indexHtmlContent);
143
+
144
+ // --- Create cl_app.js ---
145
+ 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});`;
146
+ fs.writeFileSync(path.join(appPath, 'scripts', 'cl_app.js'), clAppJsContent);
147
+ }
111
148
 
112
149
  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.`);
150
+ if (answers.type === 'SPA') {
151
+ console.log(chalk.blueBright(`\nYour Gingee app is ready. Now, set up your frontend framework:`));
152
+ console.log(chalk.blueBright(`\n1. Navigate into your new app's folder:`));
153
+ console.log(chalk.white(` cd web/${appName}`));
154
+ console.log(chalk.blueBright(`\n2. Use your favorite tool to initialize your project HERE.`));
155
+ console.log(chalk.dim(` (For example, to use Vite + React, run:)`));
156
+ console.log(chalk.white(` npm create vite@latest`));
157
+
158
+ console.log(chalk.blueBright(`\n3. IMPORTANT After initializing, you MUST configure your frontend tool`));
159
+ console.log(chalk.blueBright(`to use a base path. For Vite, edit 'vite.config.js' and add:`));
160
+ console.log(chalk.white(` base: '/${appName}/'`));
161
+
162
+ console.log(chalk.blueBright(`\n4. Install the frontend dependencies:`));
163
+ console.log(chalk.white(` npm install`));
164
+ console.log(chalk.blueBright(`\n5. Review 'box/app.json' to ensure the proxy and build paths match your tool.`));
165
+ console.log(chalk.blueBright(`\n6. Return to the project root and start the server:`));
166
+ console.log(chalk.white(` cd ../..`));
167
+ console.log(chalk.white(` npm start`));
168
+ console.log('\n\n');
169
+ } else {
170
+ console.log(` Navigate to /${appName} in your browser to see it in action.`);
171
+ }
114
172
 
115
173
  } catch (err) {
116
174
  if (err.errors) { //for AggregateError
@@ -14,9 +14,7 @@ const credsDir = path.join(configDir, 'sessions');
14
14
  function getCredsFilePath(serverUrl) {
15
15
  // Use a hash to create a unique, fixed-length, safe filename
16
16
  const hash = crypto.createHash('sha256').update(serverUrl).digest('hex');
17
- if (!fs.existsSync(credsDir)) {
18
- fs.mkdirSync(credsDir);
19
- }
17
+ fs.ensureDirSync(credsDir);
20
18
  return path.join(credsDir, `${hash}.json`);
21
19
  }
22
20
 
@@ -45,6 +43,26 @@ async function getAuthenticatedClient(serverUrl = 'http://localhost:7070') {
45
43
  return { client, serverUrl };
46
44
  }
47
45
 
46
+ async function ensureAuthenticated(serverUrl) {
47
+ const { default: ora } = await import('ora');
48
+ const spinner = ora('Verifying session...').start();
49
+ try {
50
+ const { client } = await getAuthenticatedClient(serverUrl);
51
+ // Use a simple, lightweight endpoint for the check.
52
+ await client.get(`${serverUrl}/glade/api/apps`);
53
+ spinner.succeed('Session verified.');
54
+ } catch (err) {
55
+ spinner.fail('Session verification failed.');
56
+ if (err.response && err.response.status === 401) {
57
+ throw new Error(`Authentication failed. Your session may have expired. Please run 'gingee-cli login --serverUrl ${serverUrl}' again.`);
58
+ }else if(err.message.includes('You are not logged in')) {
59
+ throw err; // Propagate the not logged in error as is.
60
+ }
61
+ // For other errors (e.g., network, server down), provide a generic message.
62
+ throw new Error(`Could not connect to the server at ${serverUrl}. Please ensure it is running and accessible.`);
63
+ }
64
+ }
65
+
48
66
  function deleteSession(serverUrl) {
49
67
  const credsPath = getCredsFilePath(serverUrl);
50
68
  if (fs.existsSync(credsPath)) {
@@ -127,6 +145,7 @@ async function getAppPermissions(serverUrl, appName) {
127
145
  module.exports = {
128
146
  getCredsFilePath,
129
147
  getAuthenticatedClient,
148
+ ensureAuthenticated,
130
149
  deleteSession,
131
150
  installApp,
132
151
  upgradeApp,
@@ -10,6 +10,8 @@ async function deleteApp(options) {
10
10
  const spinner = ora();
11
11
 
12
12
  try {
13
+ await apiClient.ensureAuthenticated(serverUrl);
14
+
13
15
  let confirmation = false;
14
16
  if (presetFilePath) {
15
17
  // --- NON-INTERACTIVE MODE ---
@@ -22,6 +22,8 @@ async function installApp(options) {
22
22
  const { serverUrl = 'http://localhost:7070', appName, ginPath: ginFilePath, file: presetFilePath } = options;
23
23
  let finalPermissions, finalDbConfig;
24
24
 
25
+ await apiClient.ensureAuthenticated(serverUrl);
26
+
25
27
  if (!fs.existsSync(ginFilePath)) {
26
28
  throw new Error(`Package file not found at: ${ginFilePath}`);
27
29
  }
@@ -22,12 +22,14 @@ async function installStoreApp(appName, options) {
22
22
  const { gStoreUrl, serverUrl } = options;
23
23
 
24
24
  try {
25
+ await apiClient.ensureAuthenticated(serverUrl);
26
+
25
27
  // Step 1: Resolve the manifest URL using the new utility
26
28
  const resolvedStoreUrl = _resolveStoreUrl(gStoreUrl);
27
29
  spinner.start(`Fetching manifest from ${resolvedStoreUrl}...`);
28
30
 
29
31
  const manifestResponse = await axios.get(resolvedStoreUrl);
30
- const appConfig = manifestResponse.data.apps.find(a => a.name === appName);
32
+ const appConfig = manifestResponse.data.apps.find(a => a.installName === appName);
31
33
  if (!appConfig) {
32
34
  throw new Error(`App '${appName}' not found in the store manifest.`);
33
35
  }
@@ -1,13 +1,18 @@
1
- const { getAuthenticatedClient } = require('./apiClient');
1
+ const apiClient = require('./apiClient');
2
2
 
3
3
  async function listApps(options) {
4
4
  const { default: chalk } = await import('chalk');
5
5
  const { default: ora } = await import('ora');
6
- const spinner = ora('Fetching application list...').start();
6
+ const spinner = ora();
7
7
 
8
8
  try {
9
9
  const { serverUrl = 'http://localhost:7070' } = options;
10
- const { client } = await getAuthenticatedClient(serverUrl);
10
+
11
+ await apiClient.ensureAuthenticated(serverUrl);
12
+
13
+ spinner.start('Fetching installed applications...');
14
+
15
+ const { client } = await apiClient.getAuthenticatedClient(serverUrl);
11
16
  const response = await client.get(`${serverUrl}/glade/api/apps`);
12
17
 
13
18
  if (response.data.status !== 'success' || !response.data.apps) {
@@ -1,12 +1,16 @@
1
1
  const apiClient = require('./apiClient');
2
+ const { _getHttpClientErrorMessage } = require('./installerUtils');
2
3
 
3
4
  async function listBackups(options) {
4
5
  const { default: chalk } = await import('chalk');
5
6
  const { default: ora } = await import('ora');
6
7
  const { serverUrl, appName } = options;
7
- const spinner = ora(`Fetching backups for '${appName}'...`).start();
8
+ const spinner = ora();
8
9
 
9
10
  try {
11
+ await apiClient.ensureAuthenticated(serverUrl);
12
+
13
+ spinner.start(`Fetching backups for '${appName}'...`);
10
14
  const result = await apiClient.listBackups(serverUrl, appName);
11
15
 
12
16
  if (result.status !== 'success' || !result.backups) {
@@ -1,14 +1,18 @@
1
1
  const apiClient = require('./apiClient');
2
2
  const fs = require('fs-extra');
3
3
  const path = require('path');
4
+ const { _getHttpClientErrorMessage } = require('./installerUtils');
4
5
 
5
6
  async function packageApp(options) {
6
7
  const { default: chalk } = await import('chalk');
7
8
  const { default: ora } = await import('ora');
8
9
  const { serverUrl, appName, dest: destFolder } = options;
9
- const spinner = ora(`Requesting package for '${appName}' from server...`).start();
10
+ const spinner = ora();
10
11
 
11
12
  try {
13
+ await apiClient.ensureAuthenticated(serverUrl);
14
+
15
+ spinner.start(`Requesting package for '${appName}' from server...`);
12
16
  // This returns a readable stream of the file being downloaded.
13
17
  const response = await apiClient.packageApp(serverUrl, appName);
14
18
  const fileStream = response.data;
@@ -10,6 +10,8 @@ async function rollbackApp(options) {
10
10
  const spinner = ora();
11
11
 
12
12
  try {
13
+ await apiClient.ensureAuthenticated(serverUrl);
14
+
13
15
  if (presetFilePath) {
14
16
  // --- NON-INTERACTIVE MODE ---
15
17
  console.log(chalk.blueBright(`Running in non-interactive mode using preset file: ${presetFilePath}`));
@@ -15,6 +15,8 @@ async function upgradeApp(options) {
15
15
 
16
16
  try {
17
17
 
18
+ await apiClient.ensureAuthenticated(serverUrl);
19
+
18
20
  if (!fs.existsSync(ginFilePath)) {
19
21
  throw new Error(`Package file not found at: ${ginFilePath}`);
20
22
  }
@@ -21,12 +21,14 @@ async function upgradeStoreApp(appName, options) {
21
21
  const { gStoreUrl, serverUrl } = options;
22
22
 
23
23
  try {
24
+ await apiClient.ensureAuthenticated(serverUrl);
25
+
24
26
  // 1. Resolve URL and fetch the store manifest
25
27
  const resolvedStoreUrl = _resolveStoreUrl(gStoreUrl);
26
28
  spinner.start(`Fetching manifest from ${resolvedStoreUrl}...`);
27
29
 
28
30
  const manifestResponse = await axios.get(resolvedStoreUrl);
29
- const appConfig = manifestResponse.data.apps.find(a => a.name === appName);
31
+ const appConfig = manifestResponse.data.apps.find(a => a.installName === appName);
30
32
  if (!appConfig) {
31
33
  throw new Error(`App '${appName}' not found in the store manifest.`);
32
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gingee-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "The Gingee Command Line Interface (CLI), official command line tool for creating and managing Gingee projects.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "commander": "^14.0.0",
42
42
  "form-data": "^4.0.4",
43
43
  "fs-extra": "^11.3.1",
44
- "gingee": "^1.0.0",
44
+ "gingee": "^1.0.2",
45
45
  "inquirer": "^12.9.2",
46
46
  "ora": "^8.2.0",
47
47
  "yauzl": "^3.2.0"
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "my-spa-app",
3
+ "version": "1.0.0",
4
+ "type": "SPA",
5
+ "spa": {
6
+ "enabled": true,
7
+ "dev_server_proxy": "http://localhost:5173",
8
+ "build_path": "./dist",
9
+ "fallback_path": "index.html"
10
+ },
11
+ "db": []
12
+ }